增加ExecuteGoodsCreatNew
This commit is contained in:
commit
0241ebaaab
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Code coverage profiles and other test artifacts
|
||||||
|
*.out
|
||||||
|
coverage.*
|
||||||
|
*.coverprofile
|
||||||
|
profile.cov
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Editor/IDE
|
||||||
|
.idea/
|
||||||
|
# .vscode/
|
||||||
BIN
address.xlsx
Normal file
BIN
address.xlsx
Normal file
Binary file not shown.
BIN
cmd/batchCreat.dll-OLD
Normal file
BIN
cmd/batchCreat.dll-OLD
Normal file
Binary file not shown.
111
cmd/batchCreat.h
Normal file
111
cmd/batchCreat.h
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/* Code generated by cmd/cgo; DO NOT EDIT. */
|
||||||
|
|
||||||
|
/* package command-line-arguments */
|
||||||
|
|
||||||
|
|
||||||
|
#line 1 "cgo-builtin-export-prolog"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifndef GO_CGO_EXPORT_PROLOGUE_H
|
||||||
|
#define GO_CGO_EXPORT_PROLOGUE_H
|
||||||
|
|
||||||
|
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||||
|
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
|
||||||
|
extern size_t _GoStringLen(_GoString_ s);
|
||||||
|
extern const char *_GoStringPtr(_GoString_ s);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Start of preamble from import "C" comments. */
|
||||||
|
|
||||||
|
|
||||||
|
#line 3 "main.go"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#line 1 "cgo-generated-wrapper"
|
||||||
|
|
||||||
|
|
||||||
|
/* End of preamble from import "C" comments. */
|
||||||
|
|
||||||
|
|
||||||
|
/* Start of boilerplate cgo prologue. */
|
||||||
|
#line 1 "cgo-gcc-export-header-prolog"
|
||||||
|
|
||||||
|
#ifndef GO_CGO_PROLOGUE_H
|
||||||
|
#define GO_CGO_PROLOGUE_H
|
||||||
|
|
||||||
|
typedef signed char GoInt8;
|
||||||
|
typedef unsigned char GoUint8;
|
||||||
|
typedef short GoInt16;
|
||||||
|
typedef unsigned short GoUint16;
|
||||||
|
typedef int GoInt32;
|
||||||
|
typedef unsigned int GoUint32;
|
||||||
|
typedef long long GoInt64;
|
||||||
|
typedef unsigned long long GoUint64;
|
||||||
|
typedef GoInt64 GoInt;
|
||||||
|
typedef GoUint64 GoUint;
|
||||||
|
typedef size_t GoUintptr;
|
||||||
|
typedef float GoFloat32;
|
||||||
|
typedef double GoFloat64;
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#if !defined(__cplusplus) || _MSVC_LANG <= 201402L
|
||||||
|
#include <complex.h>
|
||||||
|
typedef _Fcomplex GoComplex64;
|
||||||
|
typedef _Dcomplex GoComplex128;
|
||||||
|
#else
|
||||||
|
#include <complex>
|
||||||
|
typedef std::complex<float> GoComplex64;
|
||||||
|
typedef std::complex<double> GoComplex128;
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
typedef float _Complex GoComplex64;
|
||||||
|
typedef double _Complex GoComplex128;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
static assertion to make sure the file is being used on architecture
|
||||||
|
at least with matching size of GoInt.
|
||||||
|
*/
|
||||||
|
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
|
||||||
|
|
||||||
|
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||||
|
typedef _GoString_ GoString;
|
||||||
|
#endif
|
||||||
|
typedef void *GoMap;
|
||||||
|
typedef void *GoChan;
|
||||||
|
typedef struct { void *t; void *v; } GoInterface;
|
||||||
|
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* End of boilerplate cgo prologue. */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern __declspec(dllexport) void FreeCString(char* str);
|
||||||
|
extern __declspec(dllexport) char* StartServer(char* configFile);
|
||||||
|
extern __declspec(dllexport) char* StopServer(void);
|
||||||
|
extern __declspec(dllexport) char* GetServerStatus(void);
|
||||||
|
extern __declspec(dllexport) char* GetServerAddress(void);
|
||||||
|
extern __declspec(dllexport) char* ReloadConfig(char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsCreat(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteOtherTypeGoods(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsPublish(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsDownShelf(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsFlash(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsEditPrice(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsEditStock(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteSelectGoodsListPrice(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteSelectShopListPrice(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGetGoodsDetail(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteCountOuterId(char* bodyJson, char* configFile);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
1255
cmd/main.go
Normal file
1255
cmd/main.go
Normal file
File diff suppressed because it is too large
Load Diff
115
cmd/xy.h
Normal file
115
cmd/xy.h
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/* Code generated by cmd/cgo; DO NOT EDIT. */
|
||||||
|
|
||||||
|
/* package command-line-arguments */
|
||||||
|
|
||||||
|
|
||||||
|
#line 1 "cgo-builtin-export-prolog"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifndef GO_CGO_EXPORT_PROLOGUE_H
|
||||||
|
#define GO_CGO_EXPORT_PROLOGUE_H
|
||||||
|
|
||||||
|
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||||
|
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
|
||||||
|
extern size_t _GoStringLen(_GoString_ s);
|
||||||
|
extern const char *_GoStringPtr(_GoString_ s);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Start of preamble from import "C" comments. */
|
||||||
|
|
||||||
|
|
||||||
|
#line 3 "main.go"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#line 1 "cgo-generated-wrapper"
|
||||||
|
|
||||||
|
|
||||||
|
/* End of preamble from import "C" comments. */
|
||||||
|
|
||||||
|
|
||||||
|
/* Start of boilerplate cgo prologue. */
|
||||||
|
#line 1 "cgo-gcc-export-header-prolog"
|
||||||
|
|
||||||
|
#ifndef GO_CGO_PROLOGUE_H
|
||||||
|
#define GO_CGO_PROLOGUE_H
|
||||||
|
|
||||||
|
typedef signed char GoInt8;
|
||||||
|
typedef unsigned char GoUint8;
|
||||||
|
typedef short GoInt16;
|
||||||
|
typedef unsigned short GoUint16;
|
||||||
|
typedef int GoInt32;
|
||||||
|
typedef unsigned int GoUint32;
|
||||||
|
typedef long long GoInt64;
|
||||||
|
typedef unsigned long long GoUint64;
|
||||||
|
typedef GoInt64 GoInt;
|
||||||
|
typedef GoUint64 GoUint;
|
||||||
|
typedef size_t GoUintptr;
|
||||||
|
typedef float GoFloat32;
|
||||||
|
typedef double GoFloat64;
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#if !defined(__cplusplus) || _MSVC_LANG <= 201402L
|
||||||
|
#include <complex.h>
|
||||||
|
typedef _Fcomplex GoComplex64;
|
||||||
|
typedef _Dcomplex GoComplex128;
|
||||||
|
#else
|
||||||
|
#include <complex>
|
||||||
|
typedef std::complex<float> GoComplex64;
|
||||||
|
typedef std::complex<double> GoComplex128;
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
typedef float _Complex GoComplex64;
|
||||||
|
typedef double _Complex GoComplex128;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
static assertion to make sure the file is being used on architecture
|
||||||
|
at least with matching size of GoInt.
|
||||||
|
*/
|
||||||
|
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
|
||||||
|
|
||||||
|
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||||
|
typedef _GoString_ GoString;
|
||||||
|
#endif
|
||||||
|
typedef void *GoMap;
|
||||||
|
typedef void *GoChan;
|
||||||
|
typedef struct { void *t; void *v; } GoInterface;
|
||||||
|
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* End of boilerplate cgo prologue. */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern __declspec(dllexport) void FreeCString(char* str);
|
||||||
|
extern __declspec(dllexport) char* StartServer(char* configFile);
|
||||||
|
extern __declspec(dllexport) char* StopServer(void);
|
||||||
|
extern __declspec(dllexport) char* GetServerStatus(void);
|
||||||
|
extern __declspec(dllexport) char* GetServerAddress(void);
|
||||||
|
extern __declspec(dllexport) char* ReloadConfig(char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsCreat(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsCreatNew(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteOtherTypeGoods(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsPublish(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsDownShelf(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsFlash(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsEditPrice(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGoodsEditStock(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteSelectGoodsListPrice(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteSelectShopListPrice(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteOpenExpressCompanies(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteOpenOrderShip(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteXyOrderSynchronization(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteGetGoodsDetail(char* bodyJson, char* configFile);
|
||||||
|
extern __declspec(dllexport) char* ExecuteCountOuterId(char* bodyJson, char* configFile);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
25
config.ini
Normal file
25
config.ini
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[app]
|
||||||
|
AppId = 1228288260261189
|
||||||
|
AppSecret = aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k
|
||||||
|
Domain = https://open.goofish.pro
|
||||||
|
[http]
|
||||||
|
Addr = 127.0.0.1:53368
|
||||||
|
[categoryListRequest]
|
||||||
|
Path = /api/open/product/category/list
|
||||||
|
ItemBizType: 2
|
||||||
|
SpBizType: 24
|
||||||
|
[batchCreatRequest]
|
||||||
|
Path = /api/open/product/batchCreate
|
||||||
|
[file]
|
||||||
|
TxtPath = productCategory.txt
|
||||||
|
ExcelPath = address.xlsx
|
||||||
|
SheetName = Result
|
||||||
|
[redis]
|
||||||
|
Password = Long6166@@
|
||||||
|
Addr = 127.0.0.1:6379
|
||||||
|
Db = 7
|
||||||
|
[tokenBucket]
|
||||||
|
BucketKeyPrefix = "token_bucket_"
|
||||||
|
TokenPerSecond = 10
|
||||||
|
BucketSize = 100
|
||||||
|
Delay = 100
|
||||||
331
controller/creatBatch.go
Normal file
331
controller/creatBatch.go
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/checkUtil"
|
||||||
|
"xianyv/utils/creatGoodsUtil"
|
||||||
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoodsController struct {
|
||||||
|
ExcelPath string
|
||||||
|
TxtPath string
|
||||||
|
SheetName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化缓存结构,避免复杂锁嵌套
|
||||||
|
type FileCache struct {
|
||||||
|
categoryMap map[string]string
|
||||||
|
categoryTime time.Time
|
||||||
|
categoryMutex sync.RWMutex
|
||||||
|
|
||||||
|
// Excel数据内存缓存,key为sheetName,value为map[col][int32]bool
|
||||||
|
excelCache map[string]map[string]map[int32]bool
|
||||||
|
excelCacheTime map[string]time.Time
|
||||||
|
excelCacheMutex sync.RWMutex
|
||||||
|
excelCacheDuration time.Duration
|
||||||
|
|
||||||
|
// Excel操作全局互斥锁(兼容老逻辑,后续可移除)
|
||||||
|
excelMutex sync.Mutex
|
||||||
|
cacheDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
fileCacheInstance *FileCache
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetFileCache() *FileCache {
|
||||||
|
once.Do(func() {
|
||||||
|
fileCacheInstance = &FileCache{
|
||||||
|
cacheDuration: 5 * time.Minute,
|
||||||
|
excelCache: make(map[string]map[string]map[int32]bool),
|
||||||
|
excelCacheTime: make(map[string]time.Time),
|
||||||
|
excelCacheDuration: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return fileCacheInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预加载Excel校验数据到内存
|
||||||
|
func (fc *FileCache) PreloadExcelCache(filename, sheetName string, columns []string) error {
|
||||||
|
fc.excelCacheMutex.Lock()
|
||||||
|
defer fc.excelCacheMutex.Unlock()
|
||||||
|
|
||||||
|
// 只加载指定sheet和列
|
||||||
|
cache := make(map[string]map[int32]bool) // col -> set of int32
|
||||||
|
|
||||||
|
f, err := excelize.OpenFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
rows, err := f.GetRows(sheetName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, col := range columns {
|
||||||
|
cache[col] = make(map[int32]bool)
|
||||||
|
}
|
||||||
|
for rowNum := 1; rowNum <= len(rows); rowNum++ {
|
||||||
|
for _, col := range columns {
|
||||||
|
cellAddress := fmt.Sprintf("%s%d", col, rowNum)
|
||||||
|
cellValue, err := f.GetCellValue(sheetName, cellAddress)
|
||||||
|
if err != nil || cellValue == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cellInt, err := strconv.ParseInt(cellValue, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cache[col][int32(cellInt)] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fc.excelCache == nil {
|
||||||
|
fc.excelCache = make(map[string]map[string]map[int32]bool)
|
||||||
|
}
|
||||||
|
fc.excelCache[sheetName] = cache
|
||||||
|
if fc.excelCacheTime == nil {
|
||||||
|
fc.excelCacheTime = make(map[string]time.Time)
|
||||||
|
}
|
||||||
|
fc.excelCacheTime[sheetName] = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoryMap 获取类目映射(简化版本)
|
||||||
|
func (fc *FileCache) GetCategoryMap(txtPath string) (map[string]string, error) {
|
||||||
|
fc.categoryMutex.RLock()
|
||||||
|
if fc.categoryMap != nil && time.Since(fc.categoryTime) < fc.cacheDuration {
|
||||||
|
defer fc.categoryMutex.RUnlock()
|
||||||
|
return fc.categoryMap, nil
|
||||||
|
}
|
||||||
|
fc.categoryMutex.RUnlock()
|
||||||
|
|
||||||
|
fc.categoryMutex.Lock()
|
||||||
|
defer fc.categoryMutex.Unlock()
|
||||||
|
|
||||||
|
// 双重检查
|
||||||
|
if fc.categoryMap != nil && time.Since(fc.categoryTime) < fc.cacheDuration {
|
||||||
|
return fc.categoryMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("更新类目缓存...")
|
||||||
|
categoryMap, err := checkUtil.CheckValuesInTxt(txtPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fc.categoryMap = categoryMap
|
||||||
|
fc.categoryTime = time.Now()
|
||||||
|
log.Printf("类目缓存更新完成,共 %d 个类目", len(categoryMap))
|
||||||
|
return categoryMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckValuesInExcelWithMutex 优先用内存缓存校验,若无则自动预加载
|
||||||
|
func (fc *FileCache) CheckValuesInExcelWithMutex(filename, sheetName string, values map[string]int32) (map[string]bool, error) {
|
||||||
|
// 先尝试用内存缓存
|
||||||
|
fc.excelCacheMutex.RLock()
|
||||||
|
cache, ok := fc.excelCache[sheetName]
|
||||||
|
fc.excelCacheMutex.RUnlock()
|
||||||
|
columns := make([]string, 0, len(values))
|
||||||
|
for col := range values {
|
||||||
|
columns = append(columns, col)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
// 缓存不存在,自动预加载
|
||||||
|
if err := fc.PreloadExcelCache(filename, sheetName, columns); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fc.excelCacheMutex.RLock()
|
||||||
|
cache, ok = fc.excelCache[sheetName]
|
||||||
|
fc.excelCacheMutex.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Excel缓存加载失败: %s", sheetName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 校验
|
||||||
|
result := make(map[string]bool)
|
||||||
|
for col, target := range values {
|
||||||
|
if colMap, ok := cache[col]; ok {
|
||||||
|
result[col] = colMap[target]
|
||||||
|
} else {
|
||||||
|
result[col] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createErrorResponse 创建错误响应
|
||||||
|
func createErrorResponse(format string, args ...interface{}) ([]byte, error) {
|
||||||
|
resp := _type.ErrResponse{
|
||||||
|
Code: 0,
|
||||||
|
Msg: fmt.Sprintf(format, args...),
|
||||||
|
Data: struct{}{},
|
||||||
|
}
|
||||||
|
response, err := json.Marshal(resp)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoodsCreatController 修复死锁问题的版本
|
||||||
|
func (c *GoodsController) GoodsCreatController(body _type.Body, batchCreatRequest, domain string, flag bool) ([]byte, error) {
|
||||||
|
startTime := time.Now()
|
||||||
|
defer func() {
|
||||||
|
log.Printf("GoodsCreatController 执行时间: %v", time.Since(startTime))
|
||||||
|
}()
|
||||||
|
|
||||||
|
fileCache := GetFileCache()
|
||||||
|
|
||||||
|
// 类目ID基础校验
|
||||||
|
catStart := time.Now()
|
||||||
|
categoryMap, err := fileCache.GetCategoryMap(c.TxtPath)
|
||||||
|
log.Printf("GetCategoryMap elapsed=%v", time.Since(catStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("读取文件失败: %v", err)
|
||||||
|
return createErrorResponse("读取文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := categoryMap[body.CatIds]; !exists {
|
||||||
|
log.Printf("错误的类目ID: %s", body.CatIds)
|
||||||
|
return createErrorResponse("错误的类目ID: %s", body.CatIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 省市区基础校验
|
||||||
|
valuesToCheck := map[string]int32{
|
||||||
|
"A": body.Province,
|
||||||
|
"C": body.City,
|
||||||
|
"E": body.District,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStart := time.Now()
|
||||||
|
checkResults, err := fileCache.CheckValuesInExcelWithMutex(c.ExcelPath, c.SheetName, valuesToCheck)
|
||||||
|
log.Printf("CheckValuesInExcelWithMutex elapsed=%v", time.Since(checkStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Excel检查失败: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("检查结果: %v", checkResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务逻辑处理
|
||||||
|
switch body.TypePlatform {
|
||||||
|
case 4:
|
||||||
|
createStart := time.Now()
|
||||||
|
createResponse, err := creatGoodsUtil.XianYvCreat(body, batchCreatRequest, body.AppId, body.AppSecret, domain, flag)
|
||||||
|
log.Printf("XianYvCreat elapsed=%v", time.Since(createStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("创建商品失败: %v", err)
|
||||||
|
return createErrorResponse("创建商品失败: %v", err)
|
||||||
|
}
|
||||||
|
return createResponse, nil
|
||||||
|
default:
|
||||||
|
return createErrorResponse("平台编号有误: %d", body.TypePlatform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoodsCreatController 分离BookData的全局变量
|
||||||
|
func (c *GoodsController) GoodsCreatControllerNew(body _type.BodyNew, batchCreatRequest, domain string, flag bool) ([]byte, error) {
|
||||||
|
startTime := time.Now()
|
||||||
|
defer func() {
|
||||||
|
log.Printf("GoodsCreatController 执行时间: %v", time.Since(startTime))
|
||||||
|
}()
|
||||||
|
|
||||||
|
fileCache := GetFileCache()
|
||||||
|
|
||||||
|
// 类目ID基础校验
|
||||||
|
catStart := time.Now()
|
||||||
|
categoryMap, err := fileCache.GetCategoryMap(c.TxtPath)
|
||||||
|
log.Printf("GetCategoryMap elapsed=%v", time.Since(catStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("读取文件失败: %v", err)
|
||||||
|
return createErrorResponse("读取文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := categoryMap[body.CatIds]; !exists {
|
||||||
|
log.Printf("错误的类目ID: %s", body.CatIds)
|
||||||
|
return createErrorResponse("错误的类目ID: %s", body.CatIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 省市区基础校验
|
||||||
|
valuesToCheck := map[string]int32{
|
||||||
|
"A": body.Province,
|
||||||
|
"C": body.City,
|
||||||
|
"E": body.District,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStart := time.Now()
|
||||||
|
checkResults, err := fileCache.CheckValuesInExcelWithMutex(c.ExcelPath, c.SheetName, valuesToCheck)
|
||||||
|
log.Printf("CheckValuesInExcelWithMutex elapsed=%v", time.Since(checkStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Excel检查失败: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("检查结果: %v", checkResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务逻辑处理
|
||||||
|
switch body.TypePlatform {
|
||||||
|
case 4:
|
||||||
|
createStart := time.Now()
|
||||||
|
if len(body.BookData) > 0 {
|
||||||
|
createResponse, err := creatGoodsUtil.XianYvCreatNew(body, batchCreatRequest, body.AppId, body.AppSecret, domain, flag)
|
||||||
|
log.Printf("XianYvCreat elapsed=%v", time.Since(createStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("创建商品失败: %v", err)
|
||||||
|
return createErrorResponse("创建商品失败: %v", err)
|
||||||
|
}
|
||||||
|
return createResponse, nil
|
||||||
|
} else {
|
||||||
|
createResponse, err := creatGoodsUtil.XianYvCreatNoIsbn(body, batchCreatRequest, body.AppId, body.AppSecret, domain, flag)
|
||||||
|
log.Printf("XianYvCreat elapsed=%v", time.Since(createStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("创建商品失败: %v", err)
|
||||||
|
return createErrorResponse("创建商品失败: %v", err)
|
||||||
|
}
|
||||||
|
return createResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return createErrorResponse("平台编号有误: %d", body.TypePlatform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新指定sheet的Excel缓存(重新加载)
|
||||||
|
func (fc *FileCache) RefreshExcelCache(filename, sheetName string, columns []string) error {
|
||||||
|
return fc.PreloadExcelCache(filename, sheetName, columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空所有Excel缓存(下次校验时会自动重新加载)
|
||||||
|
func (fc *FileCache) ClearAllExcelCache() {
|
||||||
|
fc.excelCacheMutex.Lock()
|
||||||
|
defer fc.excelCacheMutex.Unlock()
|
||||||
|
fc.excelCache = make(map[string]map[string]map[int32]bool)
|
||||||
|
fc.excelCacheTime = make(map[string]time.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接在内存缓存校验Excel值
|
||||||
|
// values: map[col]目标值
|
||||||
|
// 返回: map[col]是否存在
|
||||||
|
func (fc *FileCache) CheckValuesInExcelCache(sheetName string, values map[string]int32) (map[string]bool, error) {
|
||||||
|
fc.excelCacheMutex.RLock()
|
||||||
|
defer fc.excelCacheMutex.RUnlock()
|
||||||
|
result := make(map[string]bool)
|
||||||
|
cache, ok := fc.excelCache[sheetName]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Excel缓存未加载: %s", sheetName)
|
||||||
|
}
|
||||||
|
for col, target := range values {
|
||||||
|
if colMap, ok := cache[col]; ok {
|
||||||
|
result[col] = colMap[target]
|
||||||
|
} else {
|
||||||
|
result[col] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
12
controller/downShelf.go
Normal file
12
controller/downShelf.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DownShelf(body _type.DownAndFlash) ([]byte, error) {
|
||||||
|
path := "/api/open/product/downShelf"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(body.AppId, body.AppSecret, "https://open.goofish.pro", path, body)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
12
controller/editPrices.go
Normal file
12
controller/editPrices.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EditPrices(edit _type.EditPrices) ([]byte, error) {
|
||||||
|
path := "/api/open/product/edit"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(edit.AppId, edit.AppSecret, "https://open.goofish.pro", path, edit)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
12
controller/editStock.go
Normal file
12
controller/editStock.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EditStock(edit _type.EditStock) ([]byte, error) {
|
||||||
|
path := "/api/open/product/edit"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(edit.AppId, edit.AppSecret, "https://open.goofish.pro", path, edit)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
12
controller/flash.go
Normal file
12
controller/flash.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Flash(body _type.DownAndFlash) ([]byte, error) {
|
||||||
|
path := "/api/open/product/edit"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(body.AppId, body.AppSecret, "https://open.goofish.pro", path, body)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
12
controller/getGoosDetail.go
Normal file
12
controller/getGoosDetail.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetGoosDetail(body _type.GetGoosDetail) ([]byte, error) {
|
||||||
|
path := "/api/open/product/detail"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(body.AppId, body.AppSecret, "https://open.goofish.pro", path, body)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
12
controller/getOrdeList.go
Normal file
12
controller/getOrdeList.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetOrderList(edit _type.GetShopList) ([]byte, error) {
|
||||||
|
path := "/api/open/order/list"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(edit.AppId, edit.AppSecret, "https://open.goofish.pro", path, edit)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
12
controller/openExpressCompanies.go
Normal file
12
controller/openExpressCompanies.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExpressCompanies(body _type.GetShopList) ([]byte, error) {
|
||||||
|
path := "/api/open/express/companies"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(body.AppId, body.AppSecret, "https://open.goofish.pro", path, body)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
12
controller/openOrderShip.go
Normal file
12
controller/openOrderShip.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OrderShip(body _type.GetOrderShip) ([]byte, error) {
|
||||||
|
path := "/api/open/order/ship"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(body.AppId, body.AppSecret, "https://open.goofish.pro", path, body)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
12
controller/publish.go
Normal file
12
controller/publish.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Publish(body _type.ListedProducts) ([]byte, error) {
|
||||||
|
path := "/api/open/product/publish"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(body.AppId, body.AppSecret, "https://open.goofish.pro", path, body)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
19
controller/selectGoodsList.go
Normal file
19
controller/selectGoodsList.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SelectGoodsListWithTime(list _type.SelectGoodsListWithTime) ([]byte, error) {
|
||||||
|
path := "/api/open/product/list"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(list.AppId, list.AppSecret, "https://open.goofish.pro", path, list)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SelectGoodsListWithoutTime(list _type.SelectGoodsListWithoutTime) ([]byte, error) {
|
||||||
|
path := "/api/open/product/list"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(list.AppId, list.AppSecret, "https://open.goofish.pro", path, list)
|
||||||
|
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
12
controller/selectShopList.go
Normal file
12
controller/selectShopList.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetShopList(body _type.GetShopList) ([]byte, error) {
|
||||||
|
path := "/api/open/user/authorize/list"
|
||||||
|
response, err := requestUtil.MakeAPIRequest(body.AppId, body.AppSecret, "https://open.goofish.pro", path, body)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
45
controller/xyOrderSynchronization.go
Normal file
45
controller/xyOrderSynchronization.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExpressListResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data *ExpressData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpressData struct {
|
||||||
|
List []ExpressCompany `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpressCompany struct {
|
||||||
|
Code string `json:"code"` // 快递公司代码
|
||||||
|
ExpressAlias string `json:"express_alias"` // 快递别名(可能为空)
|
||||||
|
ExpressName string `json:"express_name"` // 快递公司全名
|
||||||
|
IsHot bool `json:"is_hot"` // 是否热门
|
||||||
|
}
|
||||||
|
|
||||||
|
func XyOrderSynchronization(body _type.GetOrderShip) ([]byte, error) {
|
||||||
|
companiesPath := "/api/open/express/companies"
|
||||||
|
var by _type.GetShopList
|
||||||
|
by.AppId = body.AppId
|
||||||
|
by.AppSecret = body.AppSecret
|
||||||
|
companiesResponse, err := requestUtil.MakeAPIRequest(body.AppId, body.AppSecret, "https://open.goofish.pro", companiesPath, by)
|
||||||
|
var expressListResponse ExpressListResponse
|
||||||
|
if err := json.Unmarshal(companiesResponse, &expressListResponse); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析 errorWrapper 失败: %v", err)
|
||||||
|
}
|
||||||
|
for _, list := range expressListResponse.Data.List {
|
||||||
|
if body.ExpressName == list.ExpressName {
|
||||||
|
body.ExpressCode = list.Code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shipPath := "/api/open/order/ship"
|
||||||
|
shipResponse, err := requestUtil.MakeAPIRequest(body.AppId, body.AppSecret, "https://open.goofish.pro", shipPath, body)
|
||||||
|
return shipResponse, err
|
||||||
|
}
|
||||||
24
go.mod
Normal file
24
go.mod
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
module xianyv
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-ini/ini v1.67.0
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
|
github.com/xuri/excelize/v2 v2.9.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||||
|
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
github.com/tiendc/go-deepcopy v1.6.0 // indirect
|
||||||
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
|
github.com/xuri/nfp v0.0.1 // indirect
|
||||||
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
|
golang.org/x/net v0.40.0 // indirect
|
||||||
|
golang.org/x/text v0.25.0 // indirect
|
||||||
|
)
|
||||||
57
go.sum
Normal file
57
go.sum
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
|
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||||
|
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||||
|
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||||
|
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
|
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||||
|
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
|
||||||
|
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
||||||
|
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
|
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
|
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
||||||
|
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
||||||
|
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
||||||
|
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||||
|
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||||
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
1036
http/route/routes.go
Normal file
1036
http/route/routes.go
Normal file
File diff suppressed because it is too large
Load Diff
94
http/service/httpServer.go
Normal file
94
http/service/httpServer.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPServer 封装HTTP服务器功能
|
||||||
|
type HTTPServer struct {
|
||||||
|
server *http.Server
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer 创建一个新的HTTP服务器实例
|
||||||
|
func NewServer(addr string, handler http.Handler) *HTTPServer {
|
||||||
|
return &HTTPServer{
|
||||||
|
server: &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: handler,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start 启动HTTP服务器(在协程中),支持context
|
||||||
|
func (s *HTTPServer) Start(ctx context.Context) error {
|
||||||
|
log.Printf("HTTP服务器正在启动,监听地址: %s", s.server.Addr)
|
||||||
|
|
||||||
|
// 监听context取消信号
|
||||||
|
shutdownChan := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
log.Println("接收到context取消信号,开始关闭服务器...")
|
||||||
|
close(shutdownChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer s.wg.Done()
|
||||||
|
|
||||||
|
if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Printf("服务器启动失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 给服务器一点启动时间
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// 等待服务器启动完成或context取消
|
||||||
|
select {
|
||||||
|
case <-shutdownChan:
|
||||||
|
// 如果context在启动过程中被取消,立即关闭服务器
|
||||||
|
if err := s.Stop(); err != nil {
|
||||||
|
log.Printf("启动过程中关闭服务器失败: %v", err)
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
log.Println("HTTP服务器启动成功")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 优雅关闭HTTP服务器
|
||||||
|
func (s *HTTPServer) Stop() error {
|
||||||
|
log.Println("正在关闭HTTP服务器...")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := s.server.Shutdown(ctx); err != nil {
|
||||||
|
return fmt.Errorf("服务器关闭失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待服务器协程结束
|
||||||
|
s.wg.Wait()
|
||||||
|
log.Println("HTTP服务器已成功关闭")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartWithContext 可选:提供一个更简洁的启动方法(如果你需要)
|
||||||
|
func (s *HTTPServer) StartWithContext(ctx context.Context) <-chan error {
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
errChan <- s.Start(ctx)
|
||||||
|
close(errChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return errChan
|
||||||
|
}
|
||||||
37
type/config.go
Normal file
37
type/config.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package _type
|
||||||
|
|
||||||
|
// Config 配置文件结构体
|
||||||
|
type Config struct {
|
||||||
|
App struct {
|
||||||
|
AppId int `ini:"app.AppId"`
|
||||||
|
AppSecret string `ini:"app.AppSecret"`
|
||||||
|
Domain string `ini:"app.Domain"`
|
||||||
|
}
|
||||||
|
Http struct {
|
||||||
|
Addr string `ini:"http.Addr"`
|
||||||
|
}
|
||||||
|
CategoryListRequest struct {
|
||||||
|
Path string `ini:"categoryListRequest.Path"`
|
||||||
|
ItemBizType int `ini:"categoryListRequest.ItemBizType"`
|
||||||
|
SpBizType int `ini:"categoryListRequest.SpBizType"`
|
||||||
|
}
|
||||||
|
BatchCreatRequest struct {
|
||||||
|
Path string `ini:"batchCreatRequest.Path"`
|
||||||
|
}
|
||||||
|
File struct {
|
||||||
|
TxtPath string `ini:"file.TxtPath"`
|
||||||
|
ExcelPath string `ini:"file.ExcelPath"`
|
||||||
|
SheetName string `ini:"file.SheetName"`
|
||||||
|
}
|
||||||
|
Redis struct {
|
||||||
|
Password string `ini:"redis.Password"`
|
||||||
|
Addr string `ini:"redis.Addr"`
|
||||||
|
DB int `ini:"redis.Db"`
|
||||||
|
}
|
||||||
|
TokenBuckets struct {
|
||||||
|
BucketKeyPrefix string `ini:"tokenBuckets.BucketKeyPrefix"`
|
||||||
|
TokenPerSecond int `ini:"tokenBuckets.TokenPerSecond"`
|
||||||
|
BucketSize int `ini:"tokenBuckets.BucketSize"`
|
||||||
|
Delay int `ini:"tokenBuckets.Delay"`
|
||||||
|
}
|
||||||
|
}
|
||||||
1440
type/type.go
Normal file
1440
type/type.go
Normal file
File diff suppressed because it is too large
Load Diff
108
utils/checkUtil/checkUtil.go
Normal file
108
utils/checkUtil/checkUtil.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package checkUtil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckValuesInExcel(filename, sheetName string, values map[string]int32) (map[string]bool, error) {
|
||||||
|
result := make(map[string]bool)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
f, err := excelize.OpenFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
logElapsed := func() {
|
||||||
|
// record elapsed for this helper
|
||||||
|
fmt.Printf("CheckValuesInExcel elapsed=%v\n", time.Since(start))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化结果map
|
||||||
|
for key := range values {
|
||||||
|
result[key] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有行
|
||||||
|
rows, err := f.GetRows(sheetName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历所有行检查每个值
|
||||||
|
for rowNum := 1; rowNum <= len(rows); rowNum++ {
|
||||||
|
for col, targetValue := range values {
|
||||||
|
// 如果该列已经找到匹配值,跳过检查
|
||||||
|
if result[col] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cellAddress := fmt.Sprintf("%s%d", col, rowNum)
|
||||||
|
cellValue, err := f.GetCellValue(sheetName, cellAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数值比较
|
||||||
|
cellInt, err := strconv.ParseInt(cellValue, 10, 32)
|
||||||
|
if int32(cellInt) == targetValue {
|
||||||
|
result[col] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logElapsed()
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckValuesInTxt(path string) (map[string]string, error) {
|
||||||
|
start := time.Now()
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("无法打开文件: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
categoryMap := make(map[string]string)
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// 跳过空行
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按冒号分割
|
||||||
|
parts := strings.Split(line, ":")
|
||||||
|
|
||||||
|
// 确保分割后有两个部分
|
||||||
|
if len(parts) == 2 {
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
categoryMap[key] = value
|
||||||
|
} else {
|
||||||
|
fmt.Printf("警告: 无法解析行: %s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查扫描错误
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
fmt.Printf("读取文件失败: %v\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// log elapsed (use fmt to avoid importing extra log package here)
|
||||||
|
fmt.Printf("CheckValuesInTxt elapsed=%v\n", time.Since(start))
|
||||||
|
return categoryMap, nil
|
||||||
|
}
|
||||||
317
utils/creatGoodsUtil/creatGoodsUtil.go
Normal file
317
utils/creatGoodsUtil/creatGoodsUtil.go
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
package creatGoodsUtil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/requestUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func XianYvCreat(body _type.Body, requestPath string, appId int, appSecret, domain string, flag bool) ([]byte, error) {
|
||||||
|
funcStart := time.Now()
|
||||||
|
|
||||||
|
type CreateProductRequest struct {
|
||||||
|
ProductData []_type.BashCreat `json:"product_data"`
|
||||||
|
}
|
||||||
|
var allItems []_type.BashCreat
|
||||||
|
for i := 0; i < len(body.BookData); i++ {
|
||||||
|
var item _type.BashCreat
|
||||||
|
var catId string
|
||||||
|
if body.BookData[i].CatIds != "" {
|
||||||
|
catId = body.BookData[i].CatIds
|
||||||
|
} else {
|
||||||
|
catId = body.CatIds
|
||||||
|
}
|
||||||
|
// 赋值item
|
||||||
|
item.ItemKey = body.ItemKey
|
||||||
|
item.ChannelCatId = catId
|
||||||
|
item.Price = body.BookData[i].Prices[0]
|
||||||
|
item.OriginalPrice = body.BookData[i].Prices[1]
|
||||||
|
item.Stock = body.BookData[i].Stock
|
||||||
|
item.ItemBizType = body.BookData[i].ItemBizType
|
||||||
|
item.SpBizType = body.BookData[i].SpBizType
|
||||||
|
item.OuterId = body.OuterId
|
||||||
|
|
||||||
|
fmt.Printf("body: %v \n", body)
|
||||||
|
fmt.Printf("body.OuterId: %s \n", body.OuterId)
|
||||||
|
fmt.Printf("item.OuterId: %+v \n", item.OuterId)
|
||||||
|
if flag {
|
||||||
|
item.ItemBizType = 2
|
||||||
|
item.SpBizType = 99
|
||||||
|
}
|
||||||
|
|
||||||
|
var publishShops []_type.PublishShop
|
||||||
|
// 循环所有的Shop来赋值PublishShop
|
||||||
|
for j := 0; j < len(body.Shop); j++ {
|
||||||
|
var province, city, district int32
|
||||||
|
if body.Shop[j].Province == 0 {
|
||||||
|
province = body.Province
|
||||||
|
} else {
|
||||||
|
province = body.Shop[j].Province
|
||||||
|
}
|
||||||
|
if body.Shop[j].City == 0 {
|
||||||
|
city = body.City
|
||||||
|
} else {
|
||||||
|
city = body.Shop[j].City
|
||||||
|
}
|
||||||
|
if body.Shop[j].District == 0 {
|
||||||
|
district = body.District
|
||||||
|
} else {
|
||||||
|
district = body.Shop[j].District
|
||||||
|
}
|
||||||
|
|
||||||
|
publishShop := _type.PublishShop{
|
||||||
|
Images: append(body.Shop[j].MainImgs, body.Shop[j].ContentImgs...),
|
||||||
|
UserName: body.Shop[j].UserName,
|
||||||
|
Province: province,
|
||||||
|
City: city,
|
||||||
|
District: district,
|
||||||
|
Title: body.Shop[j].Title,
|
||||||
|
Content: body.Shop[j].Content,
|
||||||
|
}
|
||||||
|
publishShops = append(publishShops, publishShop)
|
||||||
|
}
|
||||||
|
item.PublishShop = publishShops
|
||||||
|
|
||||||
|
item.BookData = _type.BookData{
|
||||||
|
ISBN: body.BookData[i].ISBN,
|
||||||
|
Title: body.BookData[i].Title,
|
||||||
|
Author: body.BookData[i].Author,
|
||||||
|
Publisher: body.BookData[i].Publisher,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := item.Validate(); err != nil {
|
||||||
|
log.Println("商品信息有误: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allItems = append(allItems, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("build allItems elapsed=%v", time.Since(funcStart))
|
||||||
|
|
||||||
|
if len(allItems) == 0 {
|
||||||
|
return nil, fmt.Errorf("没有有效的商品可以创建")
|
||||||
|
}
|
||||||
|
request := CreateProductRequest{
|
||||||
|
ProductData: allItems,
|
||||||
|
}
|
||||||
|
fmt.Printf("正在创建 %d 个商品...\n", len(allItems))
|
||||||
|
fmt.Printf("请求数据: %v\n", request)
|
||||||
|
|
||||||
|
createPath := requestPath
|
||||||
|
reqStart := time.Now()
|
||||||
|
createResponse, err := requestUtil.MakeAPIRequest(appId, appSecret, domain, createPath, request)
|
||||||
|
log.Printf("MakeAPIRequest elapsed=%v", time.Since(reqStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("创建商品失败: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 打印结果
|
||||||
|
fmt.Println("创建商品响应:")
|
||||||
|
fmt.Println(string(createResponse))
|
||||||
|
log.Printf("XianYvCreat total elapsed=%v", time.Since(funcStart))
|
||||||
|
return createResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建:有Isbn增加bookdata参数
|
||||||
|
func XianYvCreatNew(body _type.BodyNew, requestPath string, appId int, appSecret, domain string, flag bool) ([]byte, error) {
|
||||||
|
funcStart := time.Now()
|
||||||
|
|
||||||
|
type CreateProductRequest struct {
|
||||||
|
ProductData []_type.BashCreat `json:"product_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var allItems []_type.BashCreat
|
||||||
|
var item _type.BashCreat
|
||||||
|
|
||||||
|
var createResponse []byte
|
||||||
|
// 赋值itemNoIsbn
|
||||||
|
item.ItemKey = body.ItemKey
|
||||||
|
item.ChannelCatId = body.CatIds
|
||||||
|
item.Price = body.Price
|
||||||
|
item.OriginalPrice = body.OriginalPrice
|
||||||
|
item.Stock = body.Stock
|
||||||
|
item.ItemBizType = body.ItemBizType
|
||||||
|
item.SpBizType = body.SpBizType
|
||||||
|
item.OuterId = body.OuterId
|
||||||
|
|
||||||
|
fmt.Printf("body: %v \n", body)
|
||||||
|
fmt.Printf("body.OuterId: %s \n", body.OuterId)
|
||||||
|
fmt.Printf("item.OuterId: %+v \n", item.OuterId)
|
||||||
|
if flag {
|
||||||
|
item.ItemBizType = 2
|
||||||
|
item.SpBizType = 99
|
||||||
|
}
|
||||||
|
|
||||||
|
var publishShops []_type.PublishShop
|
||||||
|
// 循环所有的Shop来赋值PublishShop
|
||||||
|
for j := 0; j < len(body.Shop); j++ {
|
||||||
|
var province, city, district int32
|
||||||
|
if body.Shop[j].Province == 0 {
|
||||||
|
province = body.Province
|
||||||
|
} else {
|
||||||
|
province = body.Shop[j].Province
|
||||||
|
}
|
||||||
|
if body.Shop[j].City == 0 {
|
||||||
|
city = body.City
|
||||||
|
} else {
|
||||||
|
city = body.Shop[j].City
|
||||||
|
}
|
||||||
|
if body.Shop[j].District == 0 {
|
||||||
|
district = body.District
|
||||||
|
} else {
|
||||||
|
district = body.Shop[j].District
|
||||||
|
}
|
||||||
|
|
||||||
|
publishShop := _type.PublishShop{
|
||||||
|
Images: append(body.Shop[j].MainImgs, body.Shop[j].ContentImgs...),
|
||||||
|
UserName: body.Shop[j].UserName,
|
||||||
|
Province: province,
|
||||||
|
City: city,
|
||||||
|
District: district,
|
||||||
|
Title: body.Shop[j].Title,
|
||||||
|
Content: body.Shop[j].Content,
|
||||||
|
}
|
||||||
|
publishShops = append(publishShops, publishShop)
|
||||||
|
}
|
||||||
|
item.PublishShop = publishShops
|
||||||
|
|
||||||
|
item.BookData = _type.BookData{
|
||||||
|
ISBN: body.BookData[0].ISBN,
|
||||||
|
Title: body.BookData[0].Title,
|
||||||
|
Author: body.BookData[0].Author,
|
||||||
|
Publisher: body.BookData[0].Publisher,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := item.Validate(); err != nil {
|
||||||
|
log.Println("商品信息有误: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allItems = append(allItems, item)
|
||||||
|
|
||||||
|
log.Printf("build allItems elapsed=%v", time.Since(funcStart))
|
||||||
|
|
||||||
|
if len(allItems) == 0 {
|
||||||
|
return nil, fmt.Errorf("没有有效的商品可以创建")
|
||||||
|
}
|
||||||
|
request := CreateProductRequest{
|
||||||
|
ProductData: allItems,
|
||||||
|
}
|
||||||
|
fmt.Printf("正在创建 %d 个商品...\n", len(allItems))
|
||||||
|
fmt.Printf("请求数据: %v\n", request)
|
||||||
|
|
||||||
|
createPath := requestPath
|
||||||
|
reqStart := time.Now()
|
||||||
|
createResponse, err := requestUtil.MakeAPIRequest(appId, appSecret, domain, createPath, request)
|
||||||
|
log.Printf("MakeAPIRequest elapsed=%v", time.Since(reqStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("创建商品失败: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 打印结果
|
||||||
|
fmt.Println("创建商品响应:")
|
||||||
|
fmt.Println(string(createResponse))
|
||||||
|
log.Printf("XianYvCreat total elapsed=%v", time.Since(funcStart))
|
||||||
|
|
||||||
|
return createResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建:无Isbn不加bookdata参数
|
||||||
|
func XianYvCreatNoIsbn(body _type.BodyNew, requestPath string, appId int, appSecret, domain string, flag bool) ([]byte, error) {
|
||||||
|
funcStart := time.Now()
|
||||||
|
|
||||||
|
type CreateProductRequestNoIsbn struct {
|
||||||
|
ProductData []_type.BashCreatNew `json:"product_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var allItemsNoIsbn []_type.BashCreatNew
|
||||||
|
var itemNoIsbn _type.BashCreatNew
|
||||||
|
|
||||||
|
var createResponse []byte
|
||||||
|
// 赋值itemNoIsbn
|
||||||
|
itemNoIsbn.ItemKey = body.ItemKey
|
||||||
|
itemNoIsbn.ChannelCatId = body.CatIds
|
||||||
|
itemNoIsbn.Price = body.Price
|
||||||
|
itemNoIsbn.OriginalPrice = body.OriginalPrice
|
||||||
|
itemNoIsbn.Stock = body.Stock
|
||||||
|
itemNoIsbn.ItemBizType = body.ItemBizType
|
||||||
|
itemNoIsbn.SpBizType = body.SpBizType
|
||||||
|
itemNoIsbn.OuterId = body.OuterId
|
||||||
|
|
||||||
|
fmt.Printf("body: %v \n", body)
|
||||||
|
fmt.Printf("body.OuterId: %s \n", body.OuterId)
|
||||||
|
fmt.Printf("itemNoIsbn.OuterId: %+v \n", itemNoIsbn.OuterId)
|
||||||
|
if flag {
|
||||||
|
itemNoIsbn.ItemBizType = 2
|
||||||
|
itemNoIsbn.SpBizType = 99
|
||||||
|
}
|
||||||
|
|
||||||
|
var publishShops []_type.PublishShop
|
||||||
|
// 循环所有的Shop来赋值PublishShop
|
||||||
|
for j := 0; j < len(body.Shop); j++ {
|
||||||
|
var province, city, district int32
|
||||||
|
if body.Shop[j].Province == 0 {
|
||||||
|
province = body.Province
|
||||||
|
} else {
|
||||||
|
province = body.Shop[j].Province
|
||||||
|
}
|
||||||
|
if body.Shop[j].City == 0 {
|
||||||
|
city = body.City
|
||||||
|
} else {
|
||||||
|
city = body.Shop[j].City
|
||||||
|
}
|
||||||
|
if body.Shop[j].District == 0 {
|
||||||
|
district = body.District
|
||||||
|
} else {
|
||||||
|
district = body.Shop[j].District
|
||||||
|
}
|
||||||
|
|
||||||
|
publishShop := _type.PublishShop{
|
||||||
|
Images: append(body.Shop[j].MainImgs, body.Shop[j].ContentImgs...),
|
||||||
|
UserName: body.Shop[j].UserName,
|
||||||
|
Province: province,
|
||||||
|
City: city,
|
||||||
|
District: district,
|
||||||
|
Title: body.Shop[j].Title,
|
||||||
|
Content: body.Shop[j].Content,
|
||||||
|
}
|
||||||
|
publishShops = append(publishShops, publishShop)
|
||||||
|
}
|
||||||
|
itemNoIsbn.PublishShop = publishShops
|
||||||
|
|
||||||
|
if err := itemNoIsbn.ValidateNew(); err != nil {
|
||||||
|
log.Println("商品信息有误: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allItemsNoIsbn = append(allItemsNoIsbn, itemNoIsbn)
|
||||||
|
|
||||||
|
log.Printf("build allItems elapsed=%v", time.Since(funcStart))
|
||||||
|
|
||||||
|
if len(allItemsNoIsbn) == 0 {
|
||||||
|
return nil, fmt.Errorf("没有有效的商品可以创建")
|
||||||
|
}
|
||||||
|
request := CreateProductRequestNoIsbn{
|
||||||
|
ProductData: allItemsNoIsbn,
|
||||||
|
}
|
||||||
|
fmt.Printf("正在创建 %d 个商品...\n", len(allItemsNoIsbn))
|
||||||
|
fmt.Printf("请求数据: %v\n", request)
|
||||||
|
|
||||||
|
createPath := requestPath
|
||||||
|
reqStart := time.Now()
|
||||||
|
createResponse, err := requestUtil.MakeAPIRequest(appId, appSecret, domain, createPath, request)
|
||||||
|
log.Printf("MakeAPIRequest elapsed=%v", time.Since(reqStart))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("创建商品失败: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 打印结果
|
||||||
|
fmt.Println("创建商品响应:")
|
||||||
|
fmt.Println(string(createResponse))
|
||||||
|
log.Printf("XianYvCreat total elapsed=%v", time.Since(funcStart))
|
||||||
|
|
||||||
|
//*******************************End
|
||||||
|
|
||||||
|
return createResponse, nil
|
||||||
|
}
|
||||||
378
utils/iniConfigUtil/iniConfigUtil.go
Normal file
378
utils/iniConfigUtil/iniConfigUtil.go
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
// Package utils 提供配置文件加载工具,支持从INI文件加载配置到结构体
|
||||||
|
// 支持类型: int, string, bool, float, time.Duration 及其切片类型
|
||||||
|
// 特性:
|
||||||
|
// - 支持默认值标签 `default`
|
||||||
|
// - 支持INI路径标签 `ini:"section.key"`
|
||||||
|
// - 支持时间单位转换标签 `multiplier`
|
||||||
|
// - 自动处理切片类型(逗号分隔)
|
||||||
|
package iniConfigUtil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-ini/ini"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadConfig 从INI文件加载配置到结构体
|
||||||
|
// 参数:
|
||||||
|
//
|
||||||
|
// configPtr: 指向配置结构体的指针(必须是结构体指针)
|
||||||
|
// filename: INI配置文件的路径//
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
//
|
||||||
|
// error: 加载成功返回nil,失败返回ConfigError详细错误信息
|
||||||
|
//
|
||||||
|
// 用法说明:
|
||||||
|
// 1. 定义配置结构体,使用标签声明INI映射关系和默认值
|
||||||
|
// 2. 调用LoadConfig(&config, "config.ini")
|
||||||
|
// 3. 检查错误并应用配置
|
||||||
|
//
|
||||||
|
// 示例结构体:
|
||||||
|
//
|
||||||
|
// type AppConfig struct {
|
||||||
|
// Port int `ini:"service.port" default:"8080"`
|
||||||
|
// Timeout time.Duration `ini:"service.timeout" multiplier:"1s"`
|
||||||
|
// Features []string `ini:"service.features"`
|
||||||
|
// }
|
||||||
|
func LoadConfig(configPtr interface{}, filename string) error {
|
||||||
|
// 验证输入必须是指针
|
||||||
|
if reflect.TypeOf(configPtr).Kind() != reflect.Ptr {
|
||||||
|
return &ConfigError{Message: "configPtr必须是指向结构体的指针"} // 返回错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证输入必须指向结构体
|
||||||
|
configValue := reflect.ValueOf(configPtr).Elem()
|
||||||
|
// 确保输入是结构体
|
||||||
|
if configValue.Kind() != reflect.Struct {
|
||||||
|
return &ConfigError{Message: "configPtr必须指向结构体"} // 返回错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值(如果结构体有默认值)
|
||||||
|
setDefaultValues(configValue)
|
||||||
|
|
||||||
|
// 检查配置文件是否存在
|
||||||
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||||
|
log.Printf("配置文件不存在: %s, 使用默认值", filename) // 打印信息
|
||||||
|
return nil // 返回错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载INI文件
|
||||||
|
iniCfg, err := ini.Load(filename)
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
return &ConfigError{Message: "加载配置文件失败", Cause: err} // 返回错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射配置到结构体
|
||||||
|
if err := mapConfig(iniCfg, configValue); err != nil {
|
||||||
|
return err // 返回错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊类型(如time.Duration)
|
||||||
|
processSpecialTypes(configValue)
|
||||||
|
|
||||||
|
// 返回成功
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDefaultValues 设置结构体字段的默认值
|
||||||
|
// 遍历结构体字段,检测`default`标签并设置初始值
|
||||||
|
// setDefaultValues 设置结构体字段的默认值
|
||||||
|
func setDefaultValues(configValue reflect.Value) {
|
||||||
|
// 获取结构体类型
|
||||||
|
configType := configValue.Type()
|
||||||
|
|
||||||
|
// 遍历字段
|
||||||
|
for i := 0; i < configType.NumField(); i++ {
|
||||||
|
// 获取字段信息
|
||||||
|
field := configType.Field(i)
|
||||||
|
// 获取字段值
|
||||||
|
fieldValue := configValue.Field(i)
|
||||||
|
|
||||||
|
// 如果字段是结构体,递归处理
|
||||||
|
if fieldValue.Kind() == reflect.Struct {
|
||||||
|
setDefaultValues(fieldValue)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果字段已经设置了值,跳过
|
||||||
|
if !fieldValue.IsZero() {
|
||||||
|
continue // 跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有默认值标签
|
||||||
|
if defaultValue, ok := field.Tag.Lookup("default"); ok {
|
||||||
|
setValueFromString(fieldValue, defaultValue) // 设置字段值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapConfig 将INI配置映射到结构体字段
|
||||||
|
// 解析`ini`标签获取section和key,读取对应配置值
|
||||||
|
func mapConfig(iniCfg *ini.File, configValue reflect.Value) error {
|
||||||
|
// 获取结构体类型
|
||||||
|
configType := configValue.Type()
|
||||||
|
|
||||||
|
// 遍历字段
|
||||||
|
for i := 0; i < configType.NumField(); i++ {
|
||||||
|
field := configType.Field(i) // 获取字段信息
|
||||||
|
fieldValue := configValue.Field(i) // 获取字段值
|
||||||
|
|
||||||
|
// 如果字段是结构体,递归处理
|
||||||
|
if fieldValue.Kind() == reflect.Struct {
|
||||||
|
if err := mapConfig(iniCfg, fieldValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取INI标签
|
||||||
|
iniTag, ok := field.Tag.Lookup("ini")
|
||||||
|
// 如果没有INI标签,跳过这个字段
|
||||||
|
if !ok || iniTag == "" {
|
||||||
|
continue // 跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析INI键名(支持section.key格式)
|
||||||
|
sectionName, keyName := parseIniTag(iniTag)
|
||||||
|
|
||||||
|
// 获取INI值
|
||||||
|
section, err := iniCfg.GetSection(sectionName)
|
||||||
|
// 如果section不存在
|
||||||
|
if err != nil {
|
||||||
|
continue // 跳过这个字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取INI键值
|
||||||
|
key, err := section.GetKey(keyName)
|
||||||
|
// 如果key不存在
|
||||||
|
if err != nil {
|
||||||
|
continue //跳过这个字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置结构体字段值
|
||||||
|
if err := setFieldValue(fieldValue, key); err != nil {
|
||||||
|
// 返回错误
|
||||||
|
return &ConfigError{
|
||||||
|
Message: "设置字段值失败", // 返回错误信息
|
||||||
|
Cause: err, // 返回错误原因
|
||||||
|
Field: field.Name, // 返回字段名称
|
||||||
|
Tag: iniTag, // 返回标签
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIniTag 解析INI标签格式
|
||||||
|
// 输入: "section.key" 格式的字符串
|
||||||
|
// 返回: (section名称, key名称)
|
||||||
|
func parseIniTag(tag string) (section, key string) {
|
||||||
|
// 默认section
|
||||||
|
section = "DEFAULT"
|
||||||
|
// 默认key
|
||||||
|
key = tag
|
||||||
|
|
||||||
|
// 检查是否有section前缀
|
||||||
|
if parts := regexp.MustCompile(`^(\w+)\.(\w+)$`).FindStringSubmatch(tag); len(parts) == 3 {
|
||||||
|
section = parts[1] // 设置section
|
||||||
|
key = parts[2] // 设置key
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回section和key
|
||||||
|
return section, key
|
||||||
|
}
|
||||||
|
|
||||||
|
// setFieldValue 根据INI键值设置结构体字段值
|
||||||
|
// 自动处理基础类型和time.Duration类型转换
|
||||||
|
func setFieldValue(fieldValue reflect.Value, key *ini.Key) error {
|
||||||
|
// 检查字段类型
|
||||||
|
switch fieldValue.Kind() {
|
||||||
|
case reflect.String: // 字符串类型
|
||||||
|
fieldValue.SetString(key.String()) // 设置字段值
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // 整数类型
|
||||||
|
// 特殊处理time.Duration类型
|
||||||
|
if fieldValue.Type() == reflect.TypeOf(time.Duration(0)) {
|
||||||
|
duration, err := time.ParseDuration(key.String()) // 解析字符串为time.Duration
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
return err // 返回错误
|
||||||
|
}
|
||||||
|
fieldValue.SetInt(int64(duration)) // 设置字段值
|
||||||
|
} else {
|
||||||
|
intValue, err := key.Int() // 获取整数值
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
return err // 返回错误
|
||||||
|
}
|
||||||
|
fieldValue.SetInt(int64(intValue)) // 设置字段值
|
||||||
|
}
|
||||||
|
case reflect.Bool: // 布尔类型
|
||||||
|
boolValue, err := key.Bool() // 获取布尔值
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
return err // 返回错误
|
||||||
|
}
|
||||||
|
fieldValue.SetBool(boolValue) // 设置字段值
|
||||||
|
case reflect.Float32, reflect.Float64: // 浮点类型
|
||||||
|
floatValue, err := key.Float64() // 获取浮点值
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
return err // 返回错误
|
||||||
|
}
|
||||||
|
fieldValue.SetFloat(floatValue) // 设置字段值
|
||||||
|
case reflect.Slice: // 切片类型
|
||||||
|
return setSliceValue(fieldValue, key) // 处理切片类型字段
|
||||||
|
default: // 其他类型
|
||||||
|
return &ConfigError{Message: "不支持的字段类型", FieldType: fieldValue.Type().String()} // 返回错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回成功
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSliceValue 设置切片类型字段值
|
||||||
|
// 将逗号分隔的字符串解析为指定类型的切片
|
||||||
|
func setSliceValue(fieldValue reflect.Value, key *ini.Key) error {
|
||||||
|
// 获取切片元素类型
|
||||||
|
sliceType := fieldValue.Type().Elem()
|
||||||
|
// 获取逗号分隔的值
|
||||||
|
values := key.Strings(",")
|
||||||
|
|
||||||
|
// 创建新切片
|
||||||
|
slice := reflect.MakeSlice(fieldValue.Type(), len(values), len(values))
|
||||||
|
|
||||||
|
// 遍历值
|
||||||
|
for i, val := range values {
|
||||||
|
elemValue := reflect.New(sliceType).Elem() // 创建新元素
|
||||||
|
|
||||||
|
// 根据切片元素类型设置值
|
||||||
|
switch sliceType.Kind() {
|
||||||
|
case reflect.String: // 字符串类型
|
||||||
|
elemValue.SetString(val) // 设置字段值
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // 整数类型
|
||||||
|
intVal, err := strconv.ParseInt(val, 10, 64) // 解析字符串为整数
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
return err // 返回错误
|
||||||
|
}
|
||||||
|
elemValue.SetInt(intVal) // 设置字段值
|
||||||
|
case reflect.Float32, reflect.Float64: // 浮点类型
|
||||||
|
floatVal, err := strconv.ParseFloat(val, 64) // 解析字符串为浮点数
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
return err // 返回错误
|
||||||
|
}
|
||||||
|
elemValue.SetFloat(floatVal) // 设置字段值
|
||||||
|
case reflect.Bool: // 布尔类型
|
||||||
|
boolVal, err := strconv.ParseBool(val) // 解析字符串为布尔值
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
return err // 返回错误
|
||||||
|
}
|
||||||
|
elemValue.SetBool(boolVal) // 设置字段值
|
||||||
|
default:
|
||||||
|
return &ConfigError{Message: "不支持的切片元素类型", FieldType: sliceType.String()} // 返回错误
|
||||||
|
}
|
||||||
|
|
||||||
|
slice.Index(i).Set(elemValue) // 设置切片元素
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置切片字段值
|
||||||
|
fieldValue.Set(slice)
|
||||||
|
// 返回成功
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setValueFromString 从字符串解析值到结构体字段(用于默认值)
|
||||||
|
// 支持基础类型转换,不处理复杂类型
|
||||||
|
func setValueFromString(fieldValue reflect.Value, valueStr string) {
|
||||||
|
switch fieldValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
fieldValue.SetString(valueStr)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
if intValue, err := strconv.ParseInt(valueStr, 10, 64); err == nil {
|
||||||
|
fieldValue.SetInt(intValue)
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
if boolValue, err := strconv.ParseBool(valueStr); err == nil {
|
||||||
|
fieldValue.SetBool(boolValue)
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
if floatValue, err := strconv.ParseFloat(valueStr, 64); err == nil {
|
||||||
|
fieldValue.SetFloat(floatValue)
|
||||||
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
// 切片类型需要特殊处理,这里简化处理
|
||||||
|
default:
|
||||||
|
// 保留原panic调用,但修改提示信息
|
||||||
|
panic("未处理的默认值类型")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processSpecialTypes 处理特殊类型转换
|
||||||
|
// 当前支持time.Duration的倍数转换(使用multiplier标签)
|
||||||
|
// processSpecialTypes 处理特殊类型转换
|
||||||
|
func processSpecialTypes(configValue reflect.Value) {
|
||||||
|
// 获取结构体类型
|
||||||
|
configType := configValue.Type()
|
||||||
|
|
||||||
|
// 遍历结构体字段
|
||||||
|
for i := 0; i < configType.NumField(); i++ {
|
||||||
|
field := configType.Field(i) // 获取字段
|
||||||
|
fieldValue := configValue.Field(i) // 获取字段值
|
||||||
|
|
||||||
|
// 如果字段是结构体,递归处理
|
||||||
|
if fieldValue.Kind() == reflect.Struct {
|
||||||
|
processSpecialTypes(fieldValue)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理time.Duration类型的倍数转换
|
||||||
|
if fieldValue.Type() == reflect.TypeOf(time.Duration(0)) {
|
||||||
|
// 检查是否存在multiplier标签
|
||||||
|
if multiplier, ok := field.Tag.Lookup("multiplier"); ok {
|
||||||
|
// 解析multiplier标签
|
||||||
|
if mult, err := time.ParseDuration(multiplier); err == nil {
|
||||||
|
duration := time.Duration(fieldValue.Int()) // 将字段值转换为time.Duration
|
||||||
|
fieldValue.SetInt(int64(duration * mult)) // 将字段值设置为转换后的时间
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigError 自定义配置错误类型
|
||||||
|
// 包含错误原因、字段信息和原始错误
|
||||||
|
type ConfigError struct {
|
||||||
|
Message string
|
||||||
|
Cause error
|
||||||
|
Field string
|
||||||
|
FieldType string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error 实现error接口,提供详细错误信息
|
||||||
|
func (e *ConfigError) Error() string {
|
||||||
|
msg := "配置错误: " + e.Message
|
||||||
|
if e.Field != "" {
|
||||||
|
msg += " [字段: " + e.Field + "]"
|
||||||
|
}
|
||||||
|
if e.FieldType != "" {
|
||||||
|
msg += " [类型: " + e.FieldType + "]"
|
||||||
|
}
|
||||||
|
if e.Tag != "" {
|
||||||
|
msg += " [标签: " + e.Tag + "]"
|
||||||
|
}
|
||||||
|
if e.Cause != nil {
|
||||||
|
msg += " - " + e.Cause.Error()
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
19
utils/md5Util/md5Util.go
Normal file
19
utils/md5Util/md5Util.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package md5Util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenSign 生成签名
|
||||||
|
func GenSign(appId int, appSecret string, timestamp int64, jsonStr []byte) string {
|
||||||
|
bodyMd5 := genMd5(string(jsonStr))
|
||||||
|
return genMd5(fmt.Sprintf("%v,%v,%v,%v", appId, bodyMd5, timestamp, appSecret))
|
||||||
|
}
|
||||||
|
|
||||||
|
func genMd5(str string) string {
|
||||||
|
h := md5.New()
|
||||||
|
h.Write([]byte(str))
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
61
utils/redisConnectUtil/redisConnectUtil.go
Normal file
61
utils/redisConnectUtil/redisConnectUtil.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package redisConnectUtil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitRedis 初始化Redis连接
|
||||||
|
func InitRedis(addr, password string, db int) *redis.Client {
|
||||||
|
|
||||||
|
log.Print("初始化Redis连接.....")
|
||||||
|
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: addr,
|
||||||
|
Password: password,
|
||||||
|
DB: db,
|
||||||
|
PoolSize: 10,
|
||||||
|
OnConnect: func(ctx context.Context, cn *redis.Conn) error {
|
||||||
|
// 连接建立后立即选择数据库
|
||||||
|
if db > 0 {
|
||||||
|
return cn.Select(ctx, db).Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, err := client.Ping(ctx).Result(); err != nil {
|
||||||
|
log.Fatalf("Redis连接失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Redis连接成功: addr=%s, db=%d", addr, db)
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeCloseRedis 安全关闭Redis连接
|
||||||
|
func SafeCloseRedis(client *redis.Client) {
|
||||||
|
|
||||||
|
log.Print("安全关闭Redis连接.....")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Redis关闭时发生panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if client != nil {
|
||||||
|
if err := client.Close(); err != nil {
|
||||||
|
log.Printf("Redis关闭错误: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("Redis连接已安全关闭")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
172
utils/requestUtil/requestUtil.go
Normal file
172
utils/requestUtil/requestUtil.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package requestUtil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
_type "xianyv/type"
|
||||||
|
"xianyv/utils/md5Util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MakeAPIRequest 通用API请求函数
|
||||||
|
func MakeAPIRequest(appId int, appSecret, domain, path string, requestData interface{}) ([]byte, error) {
|
||||||
|
// json格式化
|
||||||
|
bytesData, err := json.Marshal(requestData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("JSON序列化错误: %v", err)
|
||||||
|
}
|
||||||
|
// 发起请求时的时间戳(秒)
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
// 获取签名
|
||||||
|
sign := md5Util.GenSign(appId, appSecret, timestamp, bytesData)
|
||||||
|
|
||||||
|
fmt.Printf("sign: %v \n", sign)
|
||||||
|
// 请求url
|
||||||
|
url := fmt.Sprintf("%v%v?appid=%v×tamp=%v&sign=%v", domain, path, appId, timestamp, sign)
|
||||||
|
fmt.Printf("请求url: %v \n", url)
|
||||||
|
// 发起请求,使用带超时的 client 并限制可读取的响应大小,防止对端返回过大内容导致 OOM
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
|
resp, err := client.Post(url, "application/json", bytes.NewReader(bytesData))
|
||||||
|
if err != nil {
|
||||||
|
req := _type.ErrResponse{
|
||||||
|
Code: 0,
|
||||||
|
Msg: fmt.Errorf("HTTP请求错误: %v", err).Error(),
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
response, _ := json.Marshal(req)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
fmt.Printf("响应: %v \n", resp)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 限制最大可读响应大小,例如 10MB(视实际需求调整)
|
||||||
|
const maxRespBytes = 10 * 1024 * 1024 // 10MB
|
||||||
|
limitedReader := io.LimitReader(resp.Body, maxRespBytes+1)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(limitedReader)
|
||||||
|
if err != nil {
|
||||||
|
req := _type.ErrResponse{
|
||||||
|
Code: 0,
|
||||||
|
Msg: fmt.Errorf("读取响应错误: %v", err).Error(),
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
response, _ := json.Marshal(req)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果响应超过限制,返回错误
|
||||||
|
if int64(len(body)) > 10*1024*1024 {
|
||||||
|
req := _type.ErrResponse{
|
||||||
|
Code: 0,
|
||||||
|
Msg: fmt.Errorf("响应大小超过限制: %d bytes", len(body)).Error(),
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
response, _ := json.Marshal(req)
|
||||||
|
return response, fmt.Errorf("response too large: %d bytes", len(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestParams 解析请求参数
|
||||||
|
func RequestParams(r *http.Request) (_type.Body, error) {
|
||||||
|
var body _type.Body
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// 转换各个字段
|
||||||
|
appIdStr := r.FormValue("appId")
|
||||||
|
body.AppSecret = r.FormValue("appSecret")
|
||||||
|
body.OuterId = r.FormValue("token")
|
||||||
|
body.Token = r.FormValue("token")
|
||||||
|
apiShopIdStr := r.FormValue("apiShopId")
|
||||||
|
typePlatformStr := r.FormValue("typePlatform")
|
||||||
|
shopIdStr := r.FormValue("shopId")
|
||||||
|
body.ShopToken = r.FormValue("shopToken")
|
||||||
|
body.ShopName = r.FormValue("shopName")
|
||||||
|
provinceStr := r.FormValue("province")
|
||||||
|
cityStr := r.FormValue("city")
|
||||||
|
districtStr := r.FormValue("district")
|
||||||
|
body.TypeClass = r.FormValue("typeClass")
|
||||||
|
body.TypeGoods = r.FormValue("typeGoods")
|
||||||
|
body.CatIds = r.FormValue("catIds")
|
||||||
|
shopStr := r.FormValue("shop[]")
|
||||||
|
stuffStatusStr := r.FormValue("stuffStatus")
|
||||||
|
bookDataStr := r.FormValue("bookData[]")
|
||||||
|
body.ItemKey = r.FormValue("itemKey")
|
||||||
|
body.OuterId = r.FormValue("outerId")
|
||||||
|
|
||||||
|
// 转换数值参数
|
||||||
|
body.AppId, _ = strconv.Atoi(appIdStr)
|
||||||
|
body.ApiShopId, _ = strconv.Atoi(apiShopIdStr)
|
||||||
|
body.TypePlatform, _ = strconv.Atoi(typePlatformStr)
|
||||||
|
body.ShopId, _ = strconv.Atoi(shopIdStr)
|
||||||
|
if i, err := strconv.ParseInt(provinceStr, 10, 32); err == nil {
|
||||||
|
body.Province = int32(i)
|
||||||
|
} else {
|
||||||
|
body.Province = 0
|
||||||
|
}
|
||||||
|
if i, err := strconv.ParseInt(cityStr, 10, 32); err == nil {
|
||||||
|
body.City = int32(i)
|
||||||
|
} else {
|
||||||
|
body.City = 0
|
||||||
|
}
|
||||||
|
if i, err := strconv.ParseInt(districtStr, 10, 32); err == nil {
|
||||||
|
body.District = int32(i)
|
||||||
|
} else {
|
||||||
|
body.District = 0
|
||||||
|
}
|
||||||
|
body.StuffStatus, _ = strconv.Atoi(stuffStatusStr)
|
||||||
|
|
||||||
|
// shop非空校验,修复字符编码问题
|
||||||
|
if shopStr != "" {
|
||||||
|
|
||||||
|
// 清理可能的非法字符
|
||||||
|
shopStr = strings.TrimSpace(shopStr)
|
||||||
|
shopStr = strings.Trim(shopStr, `"'`)
|
||||||
|
|
||||||
|
// 移除BOM标记
|
||||||
|
if strings.HasPrefix(shopStr, "\uFEFF") {
|
||||||
|
shopStr = strings.TrimPrefix(shopStr, "\uFEFF")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(shopStr), &body.Shop)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("JSON格式错误,Shop: %v", err)
|
||||||
|
log.Printf("错误数据内容: %s", shopStr)
|
||||||
|
return body, fmt.Errorf("JSON格式错误,Shop: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Println("Shop不应为空")
|
||||||
|
return body, errors.New("Shop不应为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// bookData非空校验,修复字符编码问题
|
||||||
|
if bookDataStr != "" {
|
||||||
|
|
||||||
|
bookDataStr = strings.TrimSpace(bookDataStr)
|
||||||
|
bookDataStr = strings.Trim(bookDataStr, `"'`)
|
||||||
|
|
||||||
|
if strings.HasPrefix(bookDataStr, "\uFEFF") {
|
||||||
|
bookDataStr = strings.TrimPrefix(bookDataStr, "\uFEFF")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(bookDataStr), &body.BookData)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("JSON格式错误,解析bookData: %v", err)
|
||||||
|
return body, fmt.Errorf("JSON格式错误,解析bookData: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("bookData不应为空")
|
||||||
|
return body, errors.New("bookData不应为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
183
utils/tokenBucketUtil/tokenBucketUtil.go
Normal file
183
utils/tokenBucketUtil/tokenBucketUtil.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
// tokenBucketUtil/token_producer.go
|
||||||
|
package tokenBucketUtil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenBucketConfig 令牌桶配置
|
||||||
|
type TokenBucketConfig struct {
|
||||||
|
BucketKeyPrefix string `ini:"tokenBucket.BucketKeyPrefix"` // 令牌桶key前缀
|
||||||
|
TokensPerSecond int `ini:"tokenBucket.TokensPerSecond"` // 每秒生成令牌数
|
||||||
|
BucketSize int `ini:"tokenBucket.BucketSize"` // 令牌桶容量
|
||||||
|
Delay int `ini:"tokenBucket.Delay"` // 延迟(ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenProducer 令牌生产者
|
||||||
|
type TokenProducer struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
client *redis.Client
|
||||||
|
config TokenBucketConfig
|
||||||
|
wg sync.WaitGroup
|
||||||
|
stopFuncs map[string]func() // 存储不同key的停止函数
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTokenProducer 启动令牌桶生产者
|
||||||
|
func StartTokenProducer(client *redis.Client, config TokenBucketConfig) *TokenProducer {
|
||||||
|
log.Printf("启动令牌桶生产者[前缀: %s].....", config.BucketKeyPrefix)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
producer := &TokenProducer{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
client: client,
|
||||||
|
config: config,
|
||||||
|
stopFuncs: make(map[string]func()),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("令牌桶生产者[前缀: %s]已启动: %d tokens/s, 容量: %d",
|
||||||
|
config.BucketKeyPrefix, config.TokensPerSecond, config.BucketSize)
|
||||||
|
|
||||||
|
return producer
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBucket 为特定的appSecret添加令牌桶
|
||||||
|
func (p *TokenProducer) AddBucket(appSecret string) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
bucketKey := p.config.BucketKeyPrefix + appSecret
|
||||||
|
|
||||||
|
// 如果已经存在,先停止旧的
|
||||||
|
if stopFunc, exists := p.stopFuncs[bucketKey]; exists {
|
||||||
|
stopFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化令牌桶
|
||||||
|
initBucket(p.client, bucketKey, p.config.BucketSize)
|
||||||
|
|
||||||
|
// 启动单个令牌桶的生产者
|
||||||
|
ctx, cancel := context.WithCancel(p.ctx)
|
||||||
|
|
||||||
|
p.wg.Add(1)
|
||||||
|
go p.runSingleBucket(ctx, bucketKey, cancel)
|
||||||
|
|
||||||
|
p.stopFuncs[bucketKey] = cancel
|
||||||
|
log.Printf("添加令牌桶: %s", bucketKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveBucket 移除特定的令牌桶
|
||||||
|
func (p *TokenProducer) RemoveBucket(appSecret string) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
bucketKey := p.config.BucketKeyPrefix + appSecret
|
||||||
|
if stopFunc, exists := p.stopFuncs[bucketKey]; exists {
|
||||||
|
stopFunc()
|
||||||
|
delete(p.stopFuncs, bucketKey)
|
||||||
|
log.Printf("移除令牌桶: %s", bucketKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止所有令牌桶生产者
|
||||||
|
func (p *TokenProducer) Stop() {
|
||||||
|
log.Printf("停止所有令牌桶生产者...")
|
||||||
|
p.cancel()
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
for key, stopFunc := range p.stopFuncs {
|
||||||
|
stopFunc()
|
||||||
|
delete(p.stopFuncs, key)
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
p.wg.Wait()
|
||||||
|
log.Printf("所有令牌桶生产者已停止")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runSingleBucket 运行单个令牌桶的生产者
|
||||||
|
func (p *TokenProducer) runSingleBucket(ctx context.Context, bucketKey string, cancel context.CancelFunc) {
|
||||||
|
defer p.wg.Done()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Duration(p.config.Delay) * time.Millisecond / time.Duration(p.config.TokensPerSecond))
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// 设置键的过期时间为桶填满所需时间的2倍
|
||||||
|
expireTime := (p.config.BucketSize/p.config.TokensPerSecond + 1) * 2
|
||||||
|
log.Printf("令牌桶[%s]过期时间: %d秒", bucketKey, expireTime)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
p.produceToken(bucketKey, expireTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// produceToken 生产令牌
|
||||||
|
func (p *TokenProducer) produceToken(bucketKey string, expireTime int) {
|
||||||
|
luaScript := `
|
||||||
|
local bucket_key = KEYS[1]
|
||||||
|
local bucket_size = tonumber(ARGV[1])
|
||||||
|
local expire_time = tonumber(ARGV[2])
|
||||||
|
|
||||||
|
local current_tokens = tonumber(redis.call('get', bucket_key)) or 0
|
||||||
|
|
||||||
|
if current_tokens < bucket_size then
|
||||||
|
redis.call('setex', bucket_key, expire_time, current_tokens + 1)
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
redis.call('setex', bucket_key, expire_time, bucket_size)
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := p.client.Eval(p.ctx, luaScript, []string{bucketKey}, p.config.BucketSize, expireTime).Int()
|
||||||
|
if err != nil {
|
||||||
|
if p.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("令牌桶[%s]生产失败: %v", bucketKey, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前令牌数用于日志记录
|
||||||
|
currentTokens, err := p.client.Get(p.ctx, bucketKey).Int()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("令牌桶[%s]获取当前令牌数失败: %v", bucketKey, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录令牌桶的进度
|
||||||
|
if result == 1 && (currentTokens%10 == 0 || currentTokens == p.config.BucketSize) {
|
||||||
|
log.Printf("令牌桶[%s]当前令牌数: %d/%d", bucketKey, currentTokens, p.config.BucketSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化令牌桶
|
||||||
|
func initBucket(client *redis.Client, key string, bucketSize int) {
|
||||||
|
log.Printf("初始化令牌桶[%s].....", key)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 删除现有键(如果有)
|
||||||
|
client.Del(ctx, key)
|
||||||
|
|
||||||
|
// 初始化空桶
|
||||||
|
err := client.Set(ctx, key, 0, 0).Err()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("初始化令牌桶[%s]失败: %v", key, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("令牌桶[%s]初始化成功,初始容量: 0/%d", key, bucketSize)
|
||||||
|
}
|
||||||
276
utils/tokenConsumerUtil/tokenConsumerUtil.go
Normal file
276
utils/tokenConsumerUtil/tokenConsumerUtil.go
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
package tokenConsumerUtil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedisTokenConsumerService struct {
|
||||||
|
redisClient *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TOKEN_BUCKET_KEY = "pdd:goods:add"
|
||||||
|
COUNTER_KEY_PREFIX = "counter:"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lua脚本:原子性地消费令牌
|
||||||
|
var consumeTokenScript = `
|
||||||
|
local currentTokens = tonumber(redis.call('get', KEYS[1]) or 0)
|
||||||
|
if currentTokens > 0 then
|
||||||
|
redis.call('decr', KEYS[1])
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end`
|
||||||
|
|
||||||
|
func NewRedisTokenConsumerService(redisClient *redis.Client) *RedisTokenConsumerService {
|
||||||
|
return &RedisTokenConsumerService{
|
||||||
|
redisClient: redisClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryAcquireToken 尝试获取令牌
|
||||||
|
func (s *RedisTokenConsumerService) TryAcquireToken() bool {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
script := redis.NewScript(consumeTokenScript)
|
||||||
|
|
||||||
|
result, err := script.Run(ctx, s.redisClient, []string{TOKEN_BUCKET_KEY}).Int64()
|
||||||
|
if err != nil {
|
||||||
|
// Redis操作异常,视为获取失败
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireTokenWithRetry 带重试的获取令牌
|
||||||
|
func (s *RedisTokenConsumerService) AcquireTokenWithRetry(maxWaitSeconds int) bool {
|
||||||
|
for i := 0; i < maxWaitSeconds; i++ {
|
||||||
|
if s.TryAcquireToken() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待一段时间后重试(10ms)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementCounter 原子性地递增计数器
|
||||||
|
func (s *RedisTokenConsumerService) IncrementCounter(fixedParam string) int64 {
|
||||||
|
return s.IncrementCounterWithExpire(fixedParam, 120) // 默认过期时间120秒
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementCounterWithExpire 原子性地递增计数器(带过期时间)
|
||||||
|
func (s *RedisTokenConsumerService) IncrementCounterWithExpire(fixedParam string, expireSeconds int) int64 {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 获取当前秒级时间戳
|
||||||
|
currentSecond := time.Now().Unix()
|
||||||
|
counterKey := s.buildCounterKey(currentSecond, fixedParam)
|
||||||
|
|
||||||
|
// 使用管道确保原子性
|
||||||
|
pipe := s.redisClient.Pipeline()
|
||||||
|
incr := pipe.Incr(ctx, counterKey)
|
||||||
|
pipe.Expire(ctx, counterKey, time.Duration(expireSeconds)*time.Second)
|
||||||
|
|
||||||
|
_, err := pipe.Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Redis操作异常,返回0
|
||||||
|
fmt.Printf("Redis error: %v\n", err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return incr.Val()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementCounters 批量递增计数器
|
||||||
|
func (s *RedisTokenConsumerService) IncrementCounters(fixedParams []string, expireSeconds int) []int64 {
|
||||||
|
results := make([]int64, len(fixedParams))
|
||||||
|
for i, param := range fixedParams {
|
||||||
|
results[i] = s.IncrementCounterWithExpire(param, expireSeconds)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentCounterValue 获取当前计数器的值(不递增)
|
||||||
|
func (s *RedisTokenConsumerService) GetCurrentCounterValue(fixedParam string) int64 {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
currentSecond := time.Now().Unix()
|
||||||
|
counterKey := s.buildCounterKey(currentSecond, fixedParam)
|
||||||
|
|
||||||
|
val, err := s.redisClient.Get(ctx, counterKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.convertToLong(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildCounterKey 构建计数器Redis key
|
||||||
|
func (s *RedisTokenConsumerService) buildCounterKey(timestamp int64, fixedParam string) string {
|
||||||
|
return COUNTER_KEY_PREFIX + fixedParam + ":" + strconv.FormatInt(timestamp, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCounterValueByTimestamp 获取指定时间戳和参数的计数器值
|
||||||
|
func (s *RedisTokenConsumerService) GetCounterValueByTimestamp(timestamp int64, fixedParam string) int64 {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
counterKey := s.buildCounterKey(timestamp, fixedParam)
|
||||||
|
val, err := s.redisClient.Get(ctx, counterKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.convertToLong(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCounter 删除指定时间戳和参数的计数器
|
||||||
|
func (s *RedisTokenConsumerService) DeleteCounter(timestamp int64, fixedParam string) bool {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
counterKey := s.buildCounterKey(timestamp, fixedParam)
|
||||||
|
|
||||||
|
result, err := s.redisClient.Del(ctx, counterKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCurrentCounter 删除当前时间的计数器
|
||||||
|
func (s *RedisTokenConsumerService) DeleteCurrentCounter(fixedParam string) bool {
|
||||||
|
currentSecond := time.Now().Unix()
|
||||||
|
return s.DeleteCounter(currentSecond, fixedParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToLong 将字符串转换为int64类型
|
||||||
|
func (s *RedisTokenConsumerService) convertToLong(value string) int64 {
|
||||||
|
if value == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCounterExpire 设置计数器的过期时间
|
||||||
|
func (s *RedisTokenConsumerService) SetCounterExpire(fixedParam string, expireSeconds int) bool {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
currentSecond := time.Now().Unix()
|
||||||
|
counterKey := s.buildCounterKey(currentSecond, fixedParam)
|
||||||
|
|
||||||
|
result, err := s.redisClient.Expire(ctx, counterKey, time.Duration(expireSeconds)*time.Second).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCounterTtl 获取计数器的剩余过期时间
|
||||||
|
func (s *RedisTokenConsumerService) GetCounterTtl(fixedParam string) time.Duration {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
currentSecond := time.Now().Unix()
|
||||||
|
counterKey := s.buildCounterKey(currentSecond, fixedParam)
|
||||||
|
|
||||||
|
ttl, err := s.redisClient.TTL(ctx, counterKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
return -2
|
||||||
|
}
|
||||||
|
|
||||||
|
return ttl
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugCounter 调试方法:检查key的详细信息
|
||||||
|
func (s *RedisTokenConsumerService) DebugCounter(fixedParam string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
currentSecond := time.Now().Unix()
|
||||||
|
counterKey := s.buildCounterKey(currentSecond, fixedParam)
|
||||||
|
|
||||||
|
ttl, err := s.redisClient.TTL(ctx, counterKey).Result()
|
||||||
|
value, err2 := s.redisClient.Get(ctx, counterKey).Result()
|
||||||
|
exists, err3 := s.redisClient.Exists(ctx, counterKey).Result()
|
||||||
|
|
||||||
|
fmt.Println("=== Counter Debug Info ===")
|
||||||
|
fmt.Println("Key:", counterKey)
|
||||||
|
fmt.Println("Exists:", exists > 0)
|
||||||
|
fmt.Println("Value:", value)
|
||||||
|
fmt.Println("TTL:", ttl)
|
||||||
|
fmt.Println("Is permanent:", ttl == -1)
|
||||||
|
|
||||||
|
if err != nil || err2 != nil || err3 != nil {
|
||||||
|
fmt.Println("Debug error occurred")
|
||||||
|
}
|
||||||
|
fmt.Println("==========================")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CounterExists 检查计数器是否存在
|
||||||
|
func (s *RedisTokenConsumerService) CounterExists(fixedParam string) bool {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
currentSecond := time.Now().Unix()
|
||||||
|
counterKey := s.buildCounterKey(currentSecond, fixedParam)
|
||||||
|
|
||||||
|
result, err := s.redisClient.Exists(ctx, counterKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetCounter 重置计数器(删除并重新创建)
|
||||||
|
func (s *RedisTokenConsumerService) ResetCounter(fixedParam string, expireSeconds int) int64 {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
currentSecond := time.Now().Unix()
|
||||||
|
counterKey := s.buildCounterKey(currentSecond, fixedParam)
|
||||||
|
|
||||||
|
// 先删除
|
||||||
|
s.redisClient.Del(ctx, counterKey)
|
||||||
|
|
||||||
|
// 重新创建并设置过期时间
|
||||||
|
err := s.redisClient.Set(ctx, counterKey, 1, time.Duration(expireSeconds)*time.Second).Err()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
func main() {
|
||||||
|
// 初始化Redis客户端
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: "localhost:6379",
|
||||||
|
Password: "", // 无密码
|
||||||
|
DB: 0, // 使用默认DB
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建服务实例
|
||||||
|
service := NewRedisTokenConsumerService(rdb)
|
||||||
|
|
||||||
|
// 测试令牌获取
|
||||||
|
success := service.TryAcquireToken()
|
||||||
|
fmt.Printf("Token acquired: %v\n", success)
|
||||||
|
|
||||||
|
// 测试计数器
|
||||||
|
count := service.IncrementCounter("test_param")
|
||||||
|
fmt.Printf("Counter value: %d\n", count)
|
||||||
|
}
|
||||||
239
咸鱼发布dll.md
Normal file
239
咸鱼发布dll.md
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
##### FreeCString(str *C.char)
|
||||||
|
|
||||||
|
接收其他函数返回值之后,释放内存,参考示例
|
||||||
|
|
||||||
|
##### 内存释放示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
func example () {
|
||||||
|
// ...其他逻辑
|
||||||
|
var res = StartServer (configFile *C.char)
|
||||||
|
FreeCString(res) //释放内存
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### StartServer (configFile *C.char)
|
||||||
|
|
||||||
|
启动http服务器,参数配置文件路径,不提供默认使用工程根目录config.ini
|
||||||
|
|
||||||
|
返回C字符串启动消息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### StopServer
|
||||||
|
|
||||||
|
停止HTTP服务器
|
||||||
|
|
||||||
|
返回C字符串停止消息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### GetServerStatus
|
||||||
|
|
||||||
|
获取服务器当前状态
|
||||||
|
|
||||||
|
返回C字符串指针消息,running/stopped,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### GetServerAddress
|
||||||
|
|
||||||
|
获取服务器监听地址
|
||||||
|
|
||||||
|
返回C字符串指针服务器地址消息,未运行返回空串,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### ReloadConfig(configFile *C.char)
|
||||||
|
|
||||||
|
重新加载配置文件,参数配置文件路径,不提供默认使用根目录config.ini
|
||||||
|
|
||||||
|
返回C字符串加载结果消息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 以下都需要传递appid和appSecret ###
|
||||||
|
|
||||||
|
##### ExecuteGoodsCreat(bodyJson *C.char, configFile *C.char)
|
||||||
|
|
||||||
|
*管道通信直接调用此函数*
|
||||||
|
|
||||||
|
执行商品创建操作,参数商品信息,参考示例
|
||||||
|
|
||||||
|
返回C字符串指针创建商品结果信息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### 商品信息参考示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"appId": 1228288260261189,
|
||||||
|
"appSecret": "aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k",
|
||||||
|
"token": "",
|
||||||
|
"apiShopId": 0,
|
||||||
|
"typePlatform": 4,
|
||||||
|
"shopId": 0,
|
||||||
|
"shopToken": "",
|
||||||
|
"shopName": "",
|
||||||
|
"province": 210000,
|
||||||
|
"city": 210100,
|
||||||
|
"district": 210101,
|
||||||
|
"typeClass": "",
|
||||||
|
"typeGoods": "",
|
||||||
|
"catIds": "d14d229692616168b108d382c4e6ea42",
|
||||||
|
"shop": [
|
||||||
|
{
|
||||||
|
"userName": "xy938400231518",
|
||||||
|
"province": 210000,
|
||||||
|
"city": 210100,
|
||||||
|
"district": 210101,
|
||||||
|
"title": "牧羊少年奇幻之旅",
|
||||||
|
"content": "牧羊少年奇幻之旅",
|
||||||
|
"mainImgs": ["https://img.cdn1.vip/i/68cf5cb4e5840_1758420148.webp"],
|
||||||
|
"contentImgs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stuffStatus": 90,
|
||||||
|
"bookData": [
|
||||||
|
{
|
||||||
|
"ISBN": "9787530217054",
|
||||||
|
"Title": "牧羊少年奇幻之旅",
|
||||||
|
"Author": "保罗·柯艾略",
|
||||||
|
"Publisher": "北京十月文艺出版",
|
||||||
|
"itemBizType": 2,
|
||||||
|
"spBizType": 24,
|
||||||
|
"prices": [199999, 299999],
|
||||||
|
"stock": 100,
|
||||||
|
"catIds": "22e1d81dc4cf3a25a7f7e02f36b0b49a"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemKey": "itemAAAAA1111"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### ExecuteGoodsPublish(bodyJson *C.char, configFile *C.char)
|
||||||
|
|
||||||
|
*管道通信直接调用此函数*
|
||||||
|
|
||||||
|
执行商品上架操作,参数上架信息,参考示例
|
||||||
|
|
||||||
|
返回C字符串指针行商品上架结果信息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
##### 上架信息参考示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"product_id": 1250927879325125,
|
||||||
|
"user_name": ["xy938400231518"],
|
||||||
|
"specify_publish_time": "",
|
||||||
|
"notify_url": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 追加下架,改价,擦亮 ####
|
||||||
|
|
||||||
|
##### ExecuteGoodsDownShelf(bodyJson *C.char, configFile *C.char) ######
|
||||||
|
|
||||||
|
*管道通信直接调用此函数*
|
||||||
|
|
||||||
|
执行商品下架操作,参数管家商品ID,参考示例
|
||||||
|
|
||||||
|
返回C字符串指针行商品下架结果信息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
##### 下架信息参考示例 #####
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"product_id": 1250927879325125
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### ExecuteGoodsFlash(bodyJson *C.char, configFile *C.char) #####
|
||||||
|
|
||||||
|
*管道通信直接调用此函数*
|
||||||
|
|
||||||
|
执行商品擦亮操作,参数管家商品ID,参考示例
|
||||||
|
|
||||||
|
返回C字符串指针行商品擦亮结果信息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
##### 擦亮信息参考示例 #####
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"product_id": 1250927879325125
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### ExecuteGoodsEditPrice(bodyJson *C.char, configFile *C.char) #####
|
||||||
|
|
||||||
|
*管道通信直接调用此函数*
|
||||||
|
|
||||||
|
执行商品改价操作,参数管家商品ID,参考示例
|
||||||
|
|
||||||
|
返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
##### 改价信息参考示例(单位:分) #####
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"product_id": 1250927879325125,
|
||||||
|
"price": 550000,
|
||||||
|
"originalPrice": 770000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### ExecuteGoodsEditStock(bodyJson *C.char, configFile *C.char) #####
|
||||||
|
|
||||||
|
*管道通信直接调用此函数*
|
||||||
|
|
||||||
|
执行商品改库存操作,参数管家商品ID,参考示例
|
||||||
|
|
||||||
|
返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
##### 改库存信息参考示例(单位:分) #####
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"product_id": 1250927879325125,
|
||||||
|
"stock": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### ExecuteSelectGoodsListPrice(bodyJson *C.char, configFile *C.char) #####
|
||||||
|
|
||||||
|
*管道通信直接调用此函数*
|
||||||
|
|
||||||
|
查询店铺列表操作,参数管家商品ID,参考示例
|
||||||
|
|
||||||
|
返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放
|
||||||
|
|
||||||
|
##### 查询参考示例(单位:分) #####
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
//online_time 字段可传空
|
||||||
|
"online_time": [
|
||||||
|
1690300800,
|
||||||
|
1690366883
|
||||||
|
],
|
||||||
|
"product_status": 22
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user