增加ExecuteGoodsCreatNew

This commit is contained in:
Cai1Cai1 2026-06-25 10:23:45 +08:00
commit 0241ebaaab
35 changed files with 6494 additions and 0 deletions

32
.gitignore vendored Normal file
View 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

Binary file not shown.

BIN
cmd/batchCreat.dll-OLD Normal file

Binary file not shown.

111
cmd/batchCreat.h Normal file
View 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

File diff suppressed because it is too large Load Diff

115
cmd/xy.h Normal file
View 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
View 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
View 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为sheetNamevalue为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
View 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
View 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
View 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
View 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
}

View 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
View 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
}

View 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
}

View 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
View 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
}

View 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
}

View 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
}

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
}

View 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
}

View 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
View 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))
}

View 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连接已安全关闭")
}
}
}

View 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&timestamp=%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
}

View 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)
}

View 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
View 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
}
```