commit 0241ebaaab411f140881d59f72e1ea406b39f6f2 Author: Cai1Cai1 Date: Thu Jun 25 10:23:45 2026 +0800 增加ExecuteGoodsCreatNew diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3d87ba --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/address.xlsx b/address.xlsx new file mode 100644 index 0000000..7c91f53 Binary files /dev/null and b/address.xlsx differ diff --git a/cmd/batchCreat.dll-OLD b/cmd/batchCreat.dll-OLD new file mode 100644 index 0000000..20c6567 Binary files /dev/null and b/cmd/batchCreat.dll-OLD differ diff --git a/cmd/batchCreat.h b/cmd/batchCreat.h new file mode 100644 index 0000000..79f44a3 --- /dev/null +++ b/cmd/batchCreat.h @@ -0,0 +1,111 @@ +/* Code generated by cmd/cgo; DO NOT EDIT. */ + +/* package command-line-arguments */ + + +#line 1 "cgo-builtin-export-prolog" + +#include + +#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 +#include + +#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 +typedef _Fcomplex GoComplex64; +typedef _Dcomplex GoComplex128; +#else +#include +typedef std::complex GoComplex64; +typedef std::complex 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 diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..cdb8256 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,1255 @@ +package main + +/* +#include +#include +*/ +import "C" +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + "unicode/utf8" + "unsafe" + "xianyv/controller" + + route "xianyv/http/route" + "xianyv/http/service" + _type "xianyv/type" + "xianyv/utils/iniConfigUtil" + "xianyv/utils/tokenConsumerUtil" + + "github.com/redis/go-redis/v9" +) + +// ServerManager 使用结构体封装服务器状态,避免全局变量 +type ServerManager struct { + mu sync.RWMutex + server *service.HTTPServer + cfg _type.Config + isRunning bool + configPath string + configLoaded bool + tokenService *tokenConsumerUtil.RedisTokenConsumerService + redisClient *redis.Client + tokenInitOnce sync.Once + configLoadOnce sync.Once // 新增:专门用于配置加载 + configLoadErr error +} + +// ConsoleCommand 定义控制台命令结构 +type ConsoleCommand struct { + Action string `json:"action"` + Data map[string]interface{} `json:"data"` + Result chan ConsoleResult `json:"-"` +} + +// ConsoleResult 定义控制台命令结果 +type ConsoleResult struct { + Success bool `json:"success"` + Message string `json:"message"` + Data []byte `json:"data,omitempty"` +} + +var ( + serverManager *ServerManager + once sync.Once + consoleChan chan ConsoleCommand + consoleMode bool // 标记是否处于控制台模式 +) + +// 初始化ServerManager单例 +func getServerManager() *ServerManager { + once.Do(func() { + serverManager = &ServerManager{} + consoleChan = make(chan ConsoleCommand, 10) + }) + return serverManager +} + +//export FreeCString +func FreeCString(str *C.char) { + if str != nil { + C.free(unsafe.Pointer(str)) + } +} + +//export StartServer +func StartServer(configFile *C.char) *C.char { + manager := getServerManager() + manager.mu.Lock() + defer manager.mu.Unlock() + + if manager.isRunning { + return C.CString("Server is already running") + } + + // 设置配置文件路径 + if configFile != nil { + manager.configPath = C.GoString(configFile) + } else { + manager.configPath = "config.ini" + } + + // 读取配置文件 + if err := manager.readConfigFile(configFile); err != nil { + return C.CString("Error reading config: " + err.Error()) + } + + // 启动HTTP服务 + router := route.RegisterRoutes(&manager.cfg) + manager.server = service.NewServer(manager.cfg.Http.Addr, router) + + // 使用可取消的上下文 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // 启动服务器 + started := make(chan error, 1) + + go func() { + log.Println("HTTP服务启动中...") + started <- manager.server.Start(ctx) + }() + + // 等待服务器启动或超时 + select { + case err := <-started: + if err != nil { + return C.CString("Error starting service: " + err.Error()) + } + manager.isRunning = true + + return C.CString("Server started successfully on " + manager.cfg.Http.Addr) + case <-ctx.Done(): + return C.CString("Server start timeout") + } +} + +//export StopServer +func StopServer() *C.char { + manager := getServerManager() + manager.mu.Lock() + defer manager.mu.Unlock() + + if !manager.isRunning { + return C.CString("Server is not running") + } + + if manager.server != nil { + if err := manager.server.Stop(); err != nil { + return C.CString("Error stopping service: " + err.Error()) + } + manager.server = nil + } + + manager.isRunning = false + return C.CString("Server stopped successfully") +} + +//export GetServerStatus +func GetServerStatus() *C.char { + manager := getServerManager() + manager.mu.RLock() + defer manager.mu.RUnlock() + + if manager.isRunning { + return C.CString("Running") + } + return C.CString("Stopped") +} + +//export GetServerAddress +func GetServerAddress() *C.char { + manager := getServerManager() + manager.mu.RLock() + defer manager.mu.RUnlock() + + if manager.isRunning && manager.server != nil { + return C.CString(manager.cfg.Http.Addr) + } + return C.CString("") +} + +//export ReloadConfig +func ReloadConfig(configFile *C.char) *C.char { + manager := getServerManager() + manager.mu.Lock() + defer manager.mu.Unlock() + + if configFile != nil { + manager.configPath = C.GoString(configFile) + } + + if err := manager.readConfigFile(configFile); err != nil { + return C.CString("Error reloading config: " + err.Error()) + } + + return C.CString("Config reloaded successfully") +} + +//export ExecuteGoodsCreat +func ExecuteGoodsCreat(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + // 添加调试输出 + jsonStr := C.GoString(bodyJson) + + return manager.executeGoodsCreatInternal(jsonStr) +} + +//export ExecuteGoodsCreatNew +func ExecuteGoodsCreatNew(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + // 添加调试输出 + jsonStr := C.GoString(bodyJson) + + return manager.executeGoodsCreatInternalNew(jsonStr) +} + +//export ExecuteOtherTypeGoods +func ExecuteOtherTypeGoods(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + // 添加调试输出 + jsonStr := C.GoString(bodyJson) + + return manager.executeOtherTypeGoodsInternal(jsonStr) +} + +//export ExecuteGoodsPublish +func ExecuteGoodsPublish(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeGoodsPublishInternal(jsonStr) +} + +//export ExecuteGoodsDownShelf +func ExecuteGoodsDownShelf(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeGoodsDownShelfInternal(jsonStr) +} + +//export ExecuteGoodsFlash +func ExecuteGoodsFlash(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeGoodsFlashInternal(jsonStr) +} + +//export ExecuteGoodsEditPrice +func ExecuteGoodsEditPrice(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + jsonStr := C.GoString(bodyJson) + return manager.executeGoodsEditPriceInternal(jsonStr) +} + +//export ExecuteGoodsEditStock +func ExecuteGoodsEditStock(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeGoodsEditStockInternal(jsonStr) +} + +//export ExecuteSelectGoodsListPrice +func ExecuteSelectGoodsListPrice(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + res := manager.executeSelectGoodsListInternal(jsonStr) + + return res +} + +//export ExecuteSelectShopListPrice +func ExecuteSelectShopListPrice(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeSelectShopListInternal(jsonStr) +} + +//export ExecuteOpenExpressCompanies +func ExecuteOpenExpressCompanies(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeOpenExpressCompanies(jsonStr) +} + +//export ExecuteOpenOrderShip +func ExecuteOpenOrderShip(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeOpenOrderShip(jsonStr) +} + +//export ExecuteXyOrderSynchronization +func ExecuteXyOrderSynchronization(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeXyOrderSynchronization(jsonStr) +} + +//export ExecuteGetGoodsDetail +func ExecuteGetGoodsDetail(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeGetGoosDetailInternal(jsonStr) +} + +//export ExecuteCountOuterId +func ExecuteCountOuterId(bodyJson *C.char, configFile *C.char) *C.char { + manager := getServerManager() + + if err := manager.ensureConfigLoaded(configFile); err != nil { + return C.CString("加载配置时出错: " + err.Error()) + } + + jsonStr := C.GoString(bodyJson) + return manager.executeGetOuterIdInternal(jsonStr) +} + +func (sm *ServerManager) executeGoodsCreatInternal(jsonStr string) *C.char { + var memStatsStart, memStatsEnd runtime.MemStats + runtime.ReadMemStats(&memStatsStart) + start := time.Now() + + defer func() { + runtime.ReadMemStats(&memStatsEnd) + duration := time.Since(start) + alloc := memStatsEnd.TotalAlloc - memStatsStart.TotalAlloc + numGC := memStatsEnd.NumGC - memStatsStart.NumGC + + log.Printf("ExecuteGoodsCreat - Duration: %v, Alloc: %d bytes, GC: %d times", + duration, alloc, numGC) + }() + + // 清理输入字符串(计时) + t0 := time.Now() + cleanedJSON := cleanInputString(jsonStr) + log.Printf("cleanInputString elapsed=%v", time.Since(t0)) + + // 解析JSON(计时) + t1 := time.Now() + var body _type.Body + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + log.Printf("json.Unmarshal failed: %v (elapsed=%v)", err, time.Since(t1)) + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + log.Printf("json.Unmarshal elapsed=%v", time.Since(t1)) + + // 创建商品控制器(计时) + t2 := time.Now() + goodsController := &controller.GoodsController{ + ExcelPath: sm.cfg.File.ExcelPath, + TxtPath: sm.cfg.File.TxtPath, + SheetName: sm.cfg.File.SheetName, + } + log.Printf("create GoodsController elapsed=%v", time.Since(t2)) + + // 执行商品创建(计时) + t3 := time.Now() + createResponse, err := goodsController.GoodsCreatController( + body, + sm.cfg.BatchCreatRequest.Path, + sm.cfg.App.Domain, + false, + ) + log.Printf("GoodsCreatController elapsed=%v", time.Since(t3)) + + if err != nil { + if createResponse == nil || len(createResponse) == 0 { + return C.CString("") + } + log.Printf("alloc C.CString for error response elapsed=%v", time.Since(start)) + return C.CString(string(createResponse)) + } + + // 返回成功结果 + if createResponse == nil || len(createResponse) == 0 { + return C.CString("") + } + log.Printf("alloc C.CString for success response elapsed=%v", time.Since(start)) + return C.CString(string(createResponse)) +} + +func (sm *ServerManager) executeGoodsCreatInternalNew(jsonStr string) *C.char { + var memStatsStart, memStatsEnd runtime.MemStats + runtime.ReadMemStats(&memStatsStart) + start := time.Now() + + defer func() { + runtime.ReadMemStats(&memStatsEnd) + duration := time.Since(start) + alloc := memStatsEnd.TotalAlloc - memStatsStart.TotalAlloc + numGC := memStatsEnd.NumGC - memStatsStart.NumGC + + log.Printf("ExecuteGoodsCreat - Duration: %v, Alloc: %d bytes, GC: %d times", + duration, alloc, numGC) + }() + + // 清理输入字符串(计时) + t0 := time.Now() + cleanedJSON := cleanInputString(jsonStr) + log.Printf("cleanInputString elapsed=%v", time.Since(t0)) + + // 解析JSON(计时) + t1 := time.Now() + var body _type.BodyNew + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + log.Printf("json.Unmarshal failed: %v (elapsed=%v)", err, time.Since(t1)) + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + log.Printf("json.Unmarshal elapsed=%v", time.Since(t1)) + + // 创建商品控制器(计时) + t2 := time.Now() + goodsController := &controller.GoodsController{ + ExcelPath: sm.cfg.File.ExcelPath, + TxtPath: sm.cfg.File.TxtPath, + SheetName: sm.cfg.File.SheetName, + } + log.Printf("create GoodsController elapsed=%v", time.Since(t2)) + + // 执行商品创建(计时) + t3 := time.Now() + createResponse, err := goodsController.GoodsCreatControllerNew( + body, + sm.cfg.BatchCreatRequest.Path, + sm.cfg.App.Domain, + false, + ) + log.Printf("GoodsCreatController elapsed=%v", time.Since(t3)) + + if err != nil { + if createResponse == nil || len(createResponse) == 0 { + return C.CString("") + } + log.Printf("alloc C.CString for error response elapsed=%v", time.Since(start)) + return C.CString(string(createResponse)) + } + + // 返回成功结果 + if createResponse == nil || len(createResponse) == 0 { + return C.CString("") + } + log.Printf("alloc C.CString for success response elapsed=%v", time.Since(start)) + return C.CString(string(createResponse)) +} + +func (sm *ServerManager) executeOtherTypeGoodsInternal(jsonStr string) *C.char { + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + + // 解析JSON + var body _type.Body + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + // 创建商品控制器 + goodsController := &controller.GoodsController{ + ExcelPath: sm.cfg.File.ExcelPath, + TxtPath: sm.cfg.File.TxtPath, + SheetName: sm.cfg.File.SheetName, + } + + // 执行商品创建 + createResponse, err := goodsController.GoodsCreatController( + body, + sm.cfg.BatchCreatRequest.Path, + sm.cfg.App.Domain, + true, + ) + + if err != nil { + if createResponse == nil || len(createResponse) == 0 { + return C.CString("") + } + return C.CString(string(createResponse)) + } + + // 返回成功结果 + if createResponse == nil || len(createResponse) == 0 { + return C.CString("") + } + return C.CString(string(createResponse)) +} + +func (sm *ServerManager) executeGoodsPublishInternal(jsonStr string) *C.char { + + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + // 解析JSON + var body _type.ListedProducts + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + fmt.Printf("===========PublishBody: %v", body) + + response, err := controller.Publish(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeGoodsDownShelfInternal(jsonStr string) *C.char { + + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + time.Sleep(100 * time.Millisecond) + // 解析JSON + var body _type.DownAndFlash + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + fmt.Printf("===========DownShelfBody: %v", body) + response, err := controller.DownShelf(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeGoodsFlashInternal(jsonStr string) *C.char { + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + // 解析JSON + var body _type.DownAndFlash + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + fmt.Printf("===========FlashBody: %v", body) + response, err := controller.Flash(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeGoodsEditPriceInternal(jsonStr string) *C.char { + + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + // 解析JSON + var body _type.EditPrices + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + fmt.Printf("===========EditPriceBody: %v", body) + response, err := controller.EditPrices(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeSelectShopListInternal(jsonStr string) *C.char { + + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + // 解析JSON + var body _type.GetShopList + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + fmt.Printf("===========EditStockBody: %v", body) + response, err := controller.GetShopList(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeOpenExpressCompanies(jsonStr string) *C.char { + + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + // 解析JSON + var body _type.GetShopList + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + fmt.Printf("===========EditStockBody: %v", body) + response, err := controller.ExpressCompanies(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeOpenOrderShip(jsonStr string) *C.char { + + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + // 解析JSON + var body _type.GetOrderShip + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + fmt.Printf("===========EditStockBody: %v", body) + response, err := controller.OrderShip(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeXyOrderSynchronization(jsonStr string) *C.char { + + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + // 解析JSON + var body _type.GetOrderShip + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + fmt.Printf("===========EditStockBody: %v", body) + response, err := controller.XyOrderSynchronization(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeSelectGoodsListInternal(jsonStr string) *C.char { + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + + // 先检查JSON中是否有online_time字段 + var tempMap map[string]interface{} + if err := json.Unmarshal([]byte(cleanedJSON), &tempMap); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + var response []byte + var err error + + if onlineTime, exists := tempMap["online_time"]; exists && onlineTime != nil { + var bodyWithTime _type.SelectGoodsListWithTime + if err := json.Unmarshal([]byte(cleanedJSON), &bodyWithTime); err != nil { + return C.CString("Error: Invalid JSON format for WithTime - " + err.Error()) + } + fmt.Printf("===========SelectGoodsListWithTime: %v", bodyWithTime) + + response, err = controller.SelectGoodsListWithTime(bodyWithTime) + } else { + var bodyWithoutTime _type.SelectGoodsListWithoutTime + if err := json.Unmarshal([]byte(cleanedJSON), &bodyWithoutTime); err != nil { + return C.CString("Error: Invalid JSON format for WithoutTime - " + err.Error()) + } + fmt.Printf("===========SelectGoodsListWithoutTime: %v", bodyWithoutTime) + response, err = controller.SelectGoodsListWithoutTime(bodyWithoutTime) + } + + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + + return C.CString(string(response)) +} + +func (sm *ServerManager) executeGoodsEditStockInternal(jsonStr string) *C.char { + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + // 解析JSON + var body _type.EditStock + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + fmt.Printf("===========SelectShopListBody: %v", body) + response, err := controller.EditStock(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeGetGoosDetailInternal(jsonStr string) *C.char { + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + // 解析JSON + var body _type.GetGoosDetail + if err := json.Unmarshal([]byte(cleanedJSON), &body); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + fmt.Printf("===========GetGoodsDetailBody: %v", body) + response, err := controller.GetGoosDetail(body) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) +} + +func (sm *ServerManager) executeGetShopDetailInternal(jsonStr string) *C.char { + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + + // 先检查JSON中是否有online_time字段 + var tempMap map[string]interface{} + if err := json.Unmarshal([]byte(cleanedJSON), &tempMap); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + var response []byte + var err error + + if onlineTime, exists := tempMap["online_time"]; exists && onlineTime != nil { + var bodyWithTime _type.SelectGoodsListWithTime + if err := json.Unmarshal([]byte(cleanedJSON), &bodyWithTime); err != nil { + return C.CString("Error: Invalid JSON format for WithTime - " + err.Error()) + } + fmt.Printf("===========SelectGoodsListWithTime: %v", bodyWithTime) + + response, err = controller.SelectGoodsListWithTime(bodyWithTime) + } else { + var bodyWithoutTime _type.SelectGoodsListWithoutTime + if err := json.Unmarshal([]byte(cleanedJSON), &bodyWithoutTime); err != nil { + return C.CString("Error: Invalid JSON format for WithoutTime - " + err.Error()) + } + fmt.Printf("===========SelectGoodsListWithoutTime: %v", bodyWithoutTime) + response, err = controller.SelectGoodsListWithoutTime(bodyWithoutTime) + } + + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + + // 添加统计逻辑 + responseWithStats, err := addOuterIDStatsToResponse(response) + if err != nil { + // 如果统计失败,仍然返回原始数据 + fmt.Printf("===========统计outer_id失败: %v\n", err) + return C.CString(string(response)) + } + + return C.CString(string(responseWithStats)) +} + +func (sm *ServerManager) executeGetOuterIdInternal(jsonStr string) *C.char { + // 清理输入字符串 + cleanedJSON := cleanInputString(jsonStr) + + // 先检查JSON中是否有online_time字段 + var tempMap map[string]interface{} + if err := json.Unmarshal([]byte(cleanedJSON), &tempMap); err != nil { + return C.CString("Error: Invalid JSON format - " + err.Error()) + } + + var response []byte + var err error + + // 我们先调用一次拿到分页信息(count,page_size),然后根据 count 和 page_size 计算总页数,循环获取所有页并合并 list 来做整体统计 + var initialPageNo int32 = 1 + var pageSize int32 = 100 + var combinedList []map[string]interface{} + + if onlineTime, exists := tempMap["online_time"]; exists && onlineTime != nil { + var bodyWithTime _type.SelectGoodsListWithTime + if err := json.Unmarshal([]byte(cleanedJSON), &bodyWithTime); err != nil { + return C.CString("Error: Invalid JSON format for WithTime - " + err.Error()) + } + fmt.Printf("===========SelectGoodsListWithTime: %v", bodyWithTime) + + initialPageNo = bodyWithTime.PageNo + if bodyWithTime.PageSize > 0 { + pageSize = bodyWithTime.PageSize + } + + // 第一次请求 + response, err = controller.SelectGoodsListWithTime(bodyWithTime) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + + // 解析第一次响应以获取 count 和 list + var resp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Count int `json:"count"` + List []map[string]interface{} `json:"list"` + PageNo int `json:"page_no"` + PageSize int `json:"page_size"` + } `json:"data"` + } + if err := json.Unmarshal(response, &resp); err != nil { + // 无法解析则直接返回原始响应 + return C.CString(string(response)) + } + + combinedList = append(combinedList, resp.Data.List...) + + totalCount := resp.Data.Count + if resp.Data.PageSize > 0 { + pageSize = int32(resp.Data.PageSize) + } + + // 计算总页数 + totalPages := 1 + if pageSize > 0 { + totalPages = (totalCount + int(pageSize) - 1) / int(pageSize) + } + + // 逐页拉取其余页(跳过已取得的 initialPageNo) + for p := 1; p <= totalPages; p++ { + if int32(p) == initialPageNo { + continue + } + bodyWithTime.PageNo = int32(p) + pageResp, pageErr := controller.SelectGoodsListWithTime(bodyWithTime) + if pageErr != nil { + // 记录并继续(使用已有数据) + fmt.Printf("Warning: 获取第 %d 页失败: %v\n", p, pageErr) + continue + } + var pageParsed struct { + Data struct { + List []map[string]interface{} `json:"list"` + } `json:"data"` + } + if perr := json.Unmarshal(pageResp, &pageParsed); perr != nil { + fmt.Printf("Warning: 解析第 %d 页响应失败: %v\n", p, perr) + continue + } + combinedList = append(combinedList, pageParsed.Data.List...) + } + + // 将 combinedList 按要求注入到最终响应中并返回(在原始 response 的 data 中添加 outer_id_stats) + finalRespMap := make(map[string]interface{}) + if err := json.Unmarshal(response, &finalRespMap); err != nil { + ptr := C.CBytes(response) + return (*C.char)(ptr) + } + // 计算统计 + stats := make(map[string]interface{}) + outerCount := make(map[string]int) + for _, item := range combinedList { + if v, ok := item["outer_id"].(string); ok { + if v == "" { + outerCount["__empty__"]++ + } else { + outerCount[v]++ + } + } + } + stats["total_count"] = len(combinedList) + stats["distribution"] = outerCount + + // 注入到 data + if dataMap, ok := finalRespMap["data"].(map[string]interface{}); ok { + dataMap["outer_id_stats"] = stats + } + + bts, merr := json.Marshal(finalRespMap) + if merr != nil { + return C.CString(string(response)) + } + return C.CString(string(bts)) + + } else { + var bodyWithoutTime _type.SelectGoodsListWithoutTime + if err := json.Unmarshal([]byte(cleanedJSON), &bodyWithoutTime); err != nil { + return C.CString("Error: Invalid JSON format for WithoutTime - " + err.Error()) + } + fmt.Printf("===========SelectGoodsListWithoutTime: %v", bodyWithoutTime) + + initialPageNo = bodyWithoutTime.PageNo + if bodyWithoutTime.PageSize > 0 { + pageSize = bodyWithoutTime.PageSize + } + + response, err = controller.SelectGoodsListWithoutTime(bodyWithoutTime) + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + return C.CString(string(response)) + } + + var resp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Count int `json:"count"` + List []map[string]interface{} `json:"list"` + PageNo int `json:"page_no"` + PageSize int `json:"page_size"` + } `json:"data"` + } + if err := json.Unmarshal(response, &resp); err != nil { + return C.CString(string(response)) + } + + combinedList = append(combinedList, resp.Data.List...) + + totalCount := resp.Data.Count + if resp.Data.PageSize > 0 { + pageSize = int32(resp.Data.PageSize) + } + + totalPages := 1 + if pageSize > 0 { + totalPages = (totalCount + int(pageSize) - 1) / int(pageSize) + } + + for p := 1; p <= totalPages; p++ { + if int32(p) == initialPageNo { + continue + } + bodyWithoutTime.PageNo = int32(p) + pageResp, pageErr := controller.SelectGoodsListWithoutTime(bodyWithoutTime) + if pageErr != nil { + fmt.Printf("Warning: 获取第 %d 页失败: %v\n", p, pageErr) + continue + } + var pageParsed struct { + Data struct { + List []map[string]interface{} `json:"list"` + } `json:"data"` + } + if perr := json.Unmarshal(pageResp, &pageParsed); perr != nil { + fmt.Printf("Warning: 解析第 %d 页响应失败: %v\n", p, perr) + continue + } + combinedList = append(combinedList, pageParsed.Data.List...) + } + + finalRespMap := make(map[string]interface{}) + if err := json.Unmarshal(response, &finalRespMap); err != nil { + ptr := C.CBytes(response) + return (*C.char)(ptr) + } + + stats := make(map[string]interface{}) + outerCount := make(map[string]int) + for _, item := range combinedList { + if v, ok := item["outer_id"].(string); ok { + if v == "" { + outerCount["__empty__"]++ + } else { + outerCount[v]++ + } + } + } + stats["total_count"] = len(combinedList) + stats["distribution"] = outerCount + + if dataMap, ok := finalRespMap["data"].(map[string]interface{}); ok { + dataMap["outer_id_stats"] = stats + } + + bts, merr := json.Marshal(finalRespMap) + if merr != nil { + return C.CString(string(response)) + } + return C.CString(string(bts)) + } + + if err != nil { + if response == nil || len(response) == 0 { + return C.CString("") + } + ptr := C.CBytes(response) + return (*C.char)(ptr) + } + if response == nil || len(response) == 0 { + return C.CString("") + } + + return C.CString(string(response)) +} + +func main() { +} + +func (sm *ServerManager) readConfigFile(configFile *C.char) error { + + if configFile == nil { + return fmt.Errorf("configFile参数为空指针") + } + goConfigFile := C.GoString(configFile) + if !utf8.ValidString(goConfigFile) { + return fmt.Errorf("invalid UTF-8 encoding in config file path") + } + if goConfigFile == "" { + return fmt.Errorf("configFile参数为空字符串") + } + + // 设置硬编码路径或使用默认值 + if sm.configPath == "" { + sm.configPath = goConfigFile + log.Printf("使用配置文件路径: %s", sm.configPath) + } else { + log.Printf("警告:configPath参数将被忽略,使用Go代码中的硬编码配置路径: %s", sm.configPath) + } + + if _, err := os.Stat(sm.configPath); os.IsNotExist(err) { + exePath, _ := os.Executable() + exeDir := filepath.Dir(exePath) + defaultConfig := filepath.Join(exeDir, "config.ini") + + if _, err := os.Stat(defaultConfig); err == nil { + sm.configPath = defaultConfig + } else { + log.Printf("配置文件不存在: %s", sm.configPath) + return err + } + } + + if err := iniConfigUtil.LoadConfig(&sm.cfg, sm.configPath); err != nil { + log.Printf("加载配置失败: %v", err) + return err + } + + fmt.Printf("Config-bathCreat:\n requestPath: %v\n", sm.cfg.BatchCreatRequest.Path) + fmt.Printf("Config-File:\n txtPath: %v,\n excelPath: %v,\n sheetName: %v\n", sm.cfg.File.TxtPath, sm.cfg.File.ExcelPath, sm.cfg.File.SheetName) + + return nil +} + +func cleanInputString(input string) string { + // 移除BOM字符 + if len(input) >= 3 && input[0] == 0xEF && input[1] == 0xBB && input[2] == 0xBF { + input = input[3:] + } + + // 标准化换行符 + input = strings.ReplaceAll(input, "\r\n", "\n") + input = strings.ReplaceAll(input, "\r", "\n") + + // 移除首尾空白 + return strings.TrimSpace(input) +} + +// 统一的配置加载方法 +func (sm *ServerManager) ensureConfigLoaded(configFile *C.char) error { + sm.configLoadOnce.Do(func() { + sm.mu.Lock() + defer sm.mu.Unlock() + + if configFile != nil { + sm.configPath = C.GoString(configFile) + } else if sm.configPath == "" { + sm.configPath = "config.ini" + } + + sm.configLoadErr = sm.readConfigFile(configFile) + if sm.configLoadErr == nil { + sm.configLoaded = true + log.Printf("配置加载完成,AppId: %d", sm.cfg.App.AppId) + } + }) + return sm.configLoadErr +} + +// 将统计结果添加到响应中的辅助函数 +func addOuterIDStatsToResponse(originalResponse []byte) ([]byte, error) { + var responseMap map[string]interface{} + if err := json.Unmarshal(originalResponse, &responseMap); err != nil { + return nil, fmt.Errorf("解析响应JSON失败: %v", err) + } + + // 获取统计结果 + stats := getOuterIDStats(originalResponse) + + // 将统计结果添加到response的data中 + if data, exists := responseMap["data"].(map[string]interface{}); exists { + data["outer_id_stats"] = stats + responseMap["data"] = data + } + + return json.Marshal(responseMap) +} + +// 获取outer_id统计 +func getOuterIDStats(responseData []byte) map[string]interface{} { + stats := make(map[string]interface{}) + + var data struct { + Data struct { + List []struct { + OuterID string `json:"outer_id"` + } `json:"list"` + } `json:"data"` + } + + if err := json.Unmarshal(responseData, &data); err != nil { + return stats + } + + outerIDCount := make(map[string]int) + totalCount := len(data.Data.List) + + for _, item := range data.Data.List { + outerID := item.OuterID + if outerID == "" { + outerID = "(空)" + } + outerIDCount[outerID]++ + } + + stats["total_count"] = totalCount + stats["distribution"] = outerIDCount + + return stats +} diff --git a/cmd/xy.h b/cmd/xy.h new file mode 100644 index 0000000..ad62381 --- /dev/null +++ b/cmd/xy.h @@ -0,0 +1,115 @@ +/* Code generated by cmd/cgo; DO NOT EDIT. */ + +/* package command-line-arguments */ + + +#line 1 "cgo-builtin-export-prolog" + +#include + +#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 +#include + +#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 +typedef _Fcomplex GoComplex64; +typedef _Dcomplex GoComplex128; +#else +#include +typedef std::complex GoComplex64; +typedef std::complex 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 diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..88da1fb --- /dev/null +++ b/config.ini @@ -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 diff --git a/controller/creatBatch.go b/controller/creatBatch.go new file mode 100644 index 0000000..7247cb3 --- /dev/null +++ b/controller/creatBatch.go @@ -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 +} diff --git a/controller/downShelf.go b/controller/downShelf.go new file mode 100644 index 0000000..855976a --- /dev/null +++ b/controller/downShelf.go @@ -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 +} diff --git a/controller/editPrices.go b/controller/editPrices.go new file mode 100644 index 0000000..ed0a6b8 --- /dev/null +++ b/controller/editPrices.go @@ -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 +} diff --git a/controller/editStock.go b/controller/editStock.go new file mode 100644 index 0000000..15c03f2 --- /dev/null +++ b/controller/editStock.go @@ -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 +} diff --git a/controller/flash.go b/controller/flash.go new file mode 100644 index 0000000..8de7264 --- /dev/null +++ b/controller/flash.go @@ -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 +} diff --git a/controller/getGoosDetail.go b/controller/getGoosDetail.go new file mode 100644 index 0000000..063bf32 --- /dev/null +++ b/controller/getGoosDetail.go @@ -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 +} diff --git a/controller/getOrdeList.go b/controller/getOrdeList.go new file mode 100644 index 0000000..62eb353 --- /dev/null +++ b/controller/getOrdeList.go @@ -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 +} diff --git a/controller/openExpressCompanies.go b/controller/openExpressCompanies.go new file mode 100644 index 0000000..1c438ef --- /dev/null +++ b/controller/openExpressCompanies.go @@ -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 +} diff --git a/controller/openOrderShip.go b/controller/openOrderShip.go new file mode 100644 index 0000000..2ad6c43 --- /dev/null +++ b/controller/openOrderShip.go @@ -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 +} diff --git a/controller/publish.go b/controller/publish.go new file mode 100644 index 0000000..35b328c --- /dev/null +++ b/controller/publish.go @@ -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 +} diff --git a/controller/selectGoodsList.go b/controller/selectGoodsList.go new file mode 100644 index 0000000..2e23076 --- /dev/null +++ b/controller/selectGoodsList.go @@ -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 +} diff --git a/controller/selectShopList.go b/controller/selectShopList.go new file mode 100644 index 0000000..890619a --- /dev/null +++ b/controller/selectShopList.go @@ -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 +} diff --git a/controller/xyOrderSynchronization.go b/controller/xyOrderSynchronization.go new file mode 100644 index 0000000..d66060c --- /dev/null +++ b/controller/xyOrderSynchronization.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..deff817 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7d06f7d --- /dev/null +++ b/go.sum @@ -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= diff --git a/http/route/routes.go b/http/route/routes.go new file mode 100644 index 0000000..e803d9a --- /dev/null +++ b/http/route/routes.go @@ -0,0 +1,1036 @@ +package routes + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "runtime" + "strconv" + "strings" + "time" + "xianyv/controller" + _type "xianyv/type" + "xianyv/utils/requestUtil" +) + +// 简单的全局令牌桶限速器(本地测试用,QPS = 10) +const qpsLimit = 10 + +var tokenBucket chan struct{} + +func init() { + tokenBucket = make(chan struct{}, qpsLimit) + // 预先填充令牌 + for i := 0; i < qpsLimit; i++ { + tokenBucket <- struct{}{} + } + + // 以 QPS 速率补充令牌 + go func() { + ticker := time.NewTicker(time.Second / time.Duration(qpsLimit)) + defer ticker.Stop() + for range ticker.C { + select { + case tokenBucket <- struct{}{}: + default: + // bucket 已满,跳过 + } + } + }() +} + +type routeHandler struct { + cfg *_type.Config +} + +// RegisterRoutes 注册所有路由 +func RegisterRoutes(cfg *_type.Config) *http.ServeMux { + r := http.NewServeMux() + handler := &routeHandler{cfg: cfg} + + // 注册健康检查路由 + r.HandleFunc("GET /health", healthHandler) + + // 请求类目Id处理器,留作备用 + r.HandleFunc("POST /xianyv/getCategoryList", handler.getCategoryListHandler) + + // 发布处理器 + r.HandleFunc("POST /xianyv/creatGoodsBatch", handler.creatGoodsBatchHandler) + + // 上架处理器 + r.HandleFunc("POST /xianyv/publish", publishHandler) + + // 下架处理器 + r.HandleFunc("POST /xianyv/downShelf", downShelfHandler) + + // 擦亮商品处理器 + r.HandleFunc("POST /xianyv/flash", flashHandler) + + // 改价处理器 + r.HandleFunc("POST /xianyv/editPrices", editPricesHandler) + + // 改库存处理器 + r.HandleFunc("POST /xianyv/editStock", editStockHandler) + + // 查商品处理器 + r.HandleFunc("POST /xianyv/getGoodsList", getGoodsListHandler) + + // 新增:跨页统计 outer_id 的处理器,返回完整统计信息 + r.HandleFunc("POST /xianyv/getGoodsListAllStats", getGoodsListAllStatsHandler) + + // 查店铺处理器 + r.HandleFunc("POST /xianyv/getShopList", getShopListHandler) + + // 查询订单列表处理器 + r.HandleFunc("POST /xianyv/getOrderList", getOrderListHandler) + + // 发布其他类测试(实际不使用) + r.HandleFunc("POST /xianyv/onlineTest", handler.onlineTestHandler) + + // 获取商品详情处理器 + r.HandleFunc("POST /xianyv/getGoodsDetail", getGoodsDetailHandler) + + // 注册默认路由(处理未找到的路由) + r.HandleFunc("/", notFoundHandler) + + return r +} + +// 健康检查处理器 +func healthHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + response := map[string]interface{}{ + "status": "ok", + "timestamp": time.Now().Format(time.RFC3339), + "goroutines": runtime.NumGoroutine(), + "message": "服务运行正常", + } + + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("错误的编码响应: %v", err) + errorMsg := fmt.Sprintf(`{"error": "错误的编码响应: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 发布处理器 +func (h *routeHandler) creatGoodsBatchHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + var body _type.Body + // 请求参数处理(计时) + paramsStart := time.Now() + body, err := requestUtil.RequestParams(r) + if err != nil { + log.Printf("请求参数处理失败: %v (elapsed=%v)", err, time.Since(paramsStart)) + errMsg := fmt.Sprintf(`{"error": "请求参数处理失败: %v"}`, err) + http.Error(w, errMsg, http.StatusBadRequest) + return + } + log.Printf("请求参数解析完成 (elapsed=%v)", time.Since(paramsStart)) + + // 类目处理 + goodsController := &controller.GoodsController{ + ExcelPath: h.cfg.File.ExcelPath, + TxtPath: h.cfg.File.TxtPath, + SheetName: h.cfg.File.SheetName, + } + + // 调用业务逻辑(计时) + handlerStart := time.Now() + createResponse, err := goodsController.GoodsCreatController( + body, + h.cfg.BatchCreatRequest.Path, + h.cfg.App.Domain, + false, + ) + log.Printf("GoodsCreatController 完成 (elapsed=%v)", time.Since(handlerStart)) + + if err != nil { + errorMsg := fmt.Sprintf(`{"error": "请求解析或验证失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusBadRequest) + return + } + + // 返回Response结果 + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(createResponse)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 上架处理器 +func publishHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + var body _type.ListedProducts + body.SpecifyPublishTime = r.FormValue("specify_publish_time") + body.NotifyUrl = r.FormValue("notify_url") + appIdStr := r.FormValue("appId") + body.AppSecret = r.FormValue("appSecret") + body.AppId, _ = strconv.Atoi(appIdStr) + fmt.Printf("appId: %d", body.AppId) + body.NotifyUrl = "http://146.56.192.164:19095/huidiao/goofish/goofishGoods" + + // 从绝对路径读取txt文件 + filePath := "D:/workSpace/batchWorkSpace/josn/product_ids.txt" // 请替换为实际的绝对路径 + content, err := os.ReadFile(filePath) + if err != nil { + log.Printf("读取文件失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "读取文件失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + + // 按行分割文件内容 + lines := strings.Split(strings.TrimSpace(string(content)), "\n") + if len(lines) == 0 { + errorMsg := `{"error": "文件为空"}` + http.Error(w, errorMsg, http.StatusBadRequest) + return + } + + // 首先解析表单(只需要解析一次) + if err := r.ParseForm(); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + var responses []string + var hasError bool + + const qps = 9 + const interval = time.Second / time.Duration(qps) // 100ms + + // 循环处理每个productId + for i, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue // 跳过空行 + } + // 添加延时(从第二个请求开始) + if i > 0 { + time.Sleep(interval) + } + productIdStr := line + + body.ProductId, _ = strconv.Atoi(productIdStr) + + // 获取 user_name 数组 + body.UserName = r.Form["user_name[]"] + log.Printf("上架测试:%v", body) + + response, err := controller.Publish(body) + if err != nil { + log.Printf("上架商品失败 (productId: %s): %v", productIdStr, err) + errorResponse := fmt.Sprintf(`{"productId": "%s", "error": "上架商品失败: %v"}`, productIdStr, err) + responses = append(responses, errorResponse) + hasError = true + continue + } + + successResponse := fmt.Sprintf(`{"productId": "%s", "response": %s}`, productIdStr, string(response)) + fmt.Printf("successResponse: %v\n", successResponse) + } + + // 构建最终响应 + + if hasError { + w.WriteHeader(http.StatusMultiStatus) // 207 Multi-Status + } else { + w.WriteHeader(http.StatusOK) + } + +} + +// 下架处理器 +func downShelfHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var body _type.DownAndFlash + productIdStr := r.FormValue("productId") + appIdStr := r.FormValue("appId") + body.AppSecret = r.FormValue("appSecret") + if i, err := strconv.ParseInt(productIdStr, 10, 64); err == nil { + body.ProductId = int64(i) + } else { + body.ProductId = 0 + } + body.AppId, _ = strconv.Atoi(appIdStr) + + response, err := controller.DownShelf(body) + if err != nil { + log.Printf("下架商品失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "下架商品失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 擦亮商品处理器 +func flashHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var body _type.DownAndFlash + productIdStr := r.FormValue("productId") + appIdStr := r.FormValue("appId") + body.AppSecret = r.FormValue("appSecret") + if i, err := strconv.ParseInt(productIdStr, 10, 64); err == nil { + body.ProductId = int64(i) + } else { + body.ProductId = 0 + } + body.AppId, _ = strconv.Atoi(appIdStr) + + response, err := controller.Flash(body) + if err != nil { + log.Printf("擦亮商品失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "擦亮商品失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 改价处理器 +func editPricesHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var edit _type.EditPrices + productIdStr := r.FormValue("productId") + pricesStr := r.FormValue("price") + originalPriceStr := r.FormValue("originalPrice") + appIdStr := r.FormValue("appId") + edit.AppSecret = r.FormValue("appSecret") + if i, err := strconv.ParseInt(productIdStr, 10, 64); err == nil { + edit.ProductId = int64(i) + } else { + edit.ProductId = 0 + } + if i, err := strconv.ParseInt(pricesStr, 10, 64); err == nil { + edit.Price = int64(i) + } else { + edit.Price = 0 + } + if i, err := strconv.ParseInt(originalPriceStr, 10, 64); err == nil { + edit.OriginalPrice = int64(i) + } else { + edit.OriginalPrice = 0 + } + edit.AppId, _ = strconv.Atoi(appIdStr) + + fmt.Printf("修改价格参数:%v", edit) + response, err := controller.EditPrices(edit) + if err != nil { + log.Printf("修改价格失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "修改价格失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 修改库存处理器 +func editStockHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var edit _type.EditStock + productIdStr := r.FormValue("productId") + stockStr := r.FormValue("stock") + appIdStr := r.FormValue("appId") + edit.AppSecret = r.FormValue("appSecret") + if i, err := strconv.ParseInt(productIdStr, 10, 64); err == nil { + edit.ProductId = int64(i) + } else { + edit.ProductId = 0 + } + if i, err := strconv.ParseInt(stockStr, 10, 32); err == nil { + edit.Stock = int32(i) + } else { + edit.Stock = 0 + } + edit.AppId, _ = strconv.Atoi(appIdStr) + + fmt.Printf("修改库存参数:%v", edit) + response, err := controller.EditStock(edit) + if err != nil { + log.Printf("修改库存失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "修改库存失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 获取商品列表处理器 +func getGoodsListHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // 检查是否有online_time字段 + onlineTimeStr := r.FormValue("online_time") + + var response []byte + var err error + + if onlineTimeStr != "" { + var selectGoodsList _type.SelectGoodsListWithTime + timeStrs := strings.Split(onlineTimeStr, ",") + var onlineTime []int64 + for _, timeStr := range timeStrs { + if t, err := strconv.ParseInt(strings.TrimSpace(timeStr), 10, 64); err == nil { + onlineTime = append(onlineTime, t) + } + } + selectGoodsList.OfflineTime = onlineTime + + productStatusStr := r.FormValue("productStatus") + appIdStr := r.FormValue("appId") + selectGoodsList.AppSecret = r.FormValue("appSecret") + pageNoStr := r.FormValue("pageNo") + pageSizeStr := r.FormValue("pageSize") + + if i, err := strconv.ParseInt(productStatusStr, 10, 32); err == nil { + selectGoodsList.ProductStatus = int32(i) + } + if i, err := strconv.Atoi(appIdStr); err == nil { + selectGoodsList.AppId = i + } + if i, err := strconv.ParseInt(pageNoStr, 10, 32); err == nil { + selectGoodsList.PageNo = int32(i) + } + if i, err := strconv.ParseInt(pageSizeStr, 10, 32); err == nil { + selectGoodsList.PageSize = int32(i) + } + + response, err = controller.SelectGoodsListWithTime(selectGoodsList) + } else { + var selectGoodsList _type.SelectGoodsListWithoutTime + productStatusStr := r.FormValue("productStatus") + appIdStr := r.FormValue("appId") + selectGoodsList.AppSecret = r.FormValue("appSecret") + pageNoStr := r.FormValue("pageNo") + pageSizeStr := r.FormValue("pageSize") + + if i, err := strconv.ParseInt(productStatusStr, 10, 32); err == nil { + selectGoodsList.ProductStatus = int32(i) + } + if i, err := strconv.Atoi(appIdStr); err == nil { + selectGoodsList.AppId = i + } + if i, err := strconv.ParseInt(pageNoStr, 10, 32); err == nil { + selectGoodsList.PageNo = int32(i) + } + if i, err := strconv.ParseInt(pageSizeStr, 10, 32); err == nil { + selectGoodsList.PageSize = int32(i) + } + + response, err = controller.SelectGoodsListWithoutTime(selectGoodsList) + } + + if err != nil { + log.Printf("查询商品失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "查询商品失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + + // 添加统计功能 + responseWithStats, err := addOuterIDStatsToResponse(response) + if err != nil { + log.Printf("统计outer_id失败,返回原始数据: %v", err) + // 统计失败时返回原始数据 + responseWithStats = response + } else { + log.Printf("成功添加outer_id统计信息") + } + + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(responseWithStats)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// getGoodsListAllStatsHandler - 在 getGoodsList 的基础上做跨页拉取并统计所有页的 outer_id 分布, +// 返回原始分页响应并在 data.outer_id_stats 中附加总体统计信息 +func getGoodsListAllStatsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // 检查是否有online_time字段 + onlineTimeStr := r.FormValue("online_time") + + var response []byte + var err error + + // 通用变量 + var initialPageNo int32 = 1 + var pageSize int32 = 100 + var combinedList []map[string]interface{} + + if onlineTimeStr != "" { + var selectGoodsList _type.SelectGoodsListWithTime + timeStrs := strings.Split(onlineTimeStr, ",") + var onlineTime []int64 + for _, timeStr := range timeStrs { + if t, err := strconv.ParseInt(strings.TrimSpace(timeStr), 10, 64); err == nil { + onlineTime = append(onlineTime, t) + } + } + selectGoodsList.OfflineTime = onlineTime + + productStatusStr := r.FormValue("productStatus") + appIdStr := r.FormValue("appId") + selectGoodsList.AppSecret = r.FormValue("appSecret") + pageNoStr := r.FormValue("pageNo") + pageSizeStr := r.FormValue("pageSize") + + if i, err := strconv.ParseInt(productStatusStr, 10, 32); err == nil { + selectGoodsList.ProductStatus = int32(i) + } + if i, err := strconv.Atoi(appIdStr); err == nil { + selectGoodsList.AppId = i + } + if i, err := strconv.ParseInt(pageNoStr, 10, 32); err == nil { + selectGoodsList.PageNo = int32(i) + } + if i, err := strconv.ParseInt(pageSizeStr, 10, 32); err == nil { + selectGoodsList.PageSize = int32(i) + } + + initialPageNo = selectGoodsList.PageNo + if selectGoodsList.PageSize > 0 { + pageSize = selectGoodsList.PageSize + } + + // 第一次请求 + response, err = controller.SelectGoodsListWithTime(selectGoodsList) + if err != nil { + log.Printf("查询商品失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "查询商品失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + + var resp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Count int `json:"count"` + List []map[string]interface{} `json:"list"` + PageNo int `json:"page_no"` + PageSize int `json:"page_size"` + } `json:"data"` + } + if err := json.Unmarshal(response, &resp); err != nil { + // 无法解析则返回原始响应 + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + } + return + } + + combinedList = append(combinedList, resp.Data.List...) + + totalCount := resp.Data.Count + if resp.Data.PageSize > 0 { + pageSize = int32(resp.Data.PageSize) + } + + totalPages := 1 + if pageSize > 0 { + totalPages = (totalCount + int(pageSize) - 1) / int(pageSize) + } + + for p := 1; p <= totalPages; p++ { + if int32(p) == initialPageNo { + continue + } + selectGoodsList.PageNo = int32(p) + pageResp, pageErr := controller.SelectGoodsListWithTime(selectGoodsList) + if pageErr != nil { + log.Printf("Warning: 获取第 %d 页失败: %v", p, pageErr) + continue + } + var pageParsed struct { + Data struct { + List []map[string]interface{} `json:"list"` + } `json:"data"` + } + if perr := json.Unmarshal(pageResp, &pageParsed); perr != nil { + log.Printf("Warning: 解析第 %d 页响应失败: %v", p, perr) + continue + } + combinedList = append(combinedList, pageParsed.Data.List...) + } + + // 构造最终响应:在原始 response 的 data 中注入 outer_id_stats + finalRespMap := make(map[string]interface{}) + if err := json.Unmarshal(response, &finalRespMap); err != nil { + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + } + return + } + + // 计算统计 + outerCount := make(map[string]int) + for _, item := range combinedList { + if v, ok := item["outer_id"].(string); ok { + if v == "" { + outerCount["(空)"]++ + } else { + outerCount[v]++ + } + } + } + stats := map[string]interface{}{ + "total_count": len(combinedList), + "distribution": outerCount, + } + + if dataMap, ok := finalRespMap["data"].(map[string]interface{}); ok { + dataMap["outer_id_stats"] = stats + finalRespMap["data"] = dataMap + } + + bts, merr := json.Marshal(finalRespMap) + if merr != nil { + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + } + return + } + + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(bts)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + return + + } else { + // 无 time 字段的情况 + var selectGoodsList _type.SelectGoodsListWithoutTime + productStatusStr := r.FormValue("productStatus") + appIdStr := r.FormValue("appId") + selectGoodsList.AppSecret = r.FormValue("appSecret") + pageNoStr := r.FormValue("pageNo") + pageSizeStr := r.FormValue("pageSize") + + if i, err := strconv.ParseInt(productStatusStr, 10, 32); err == nil { + selectGoodsList.ProductStatus = int32(i) + } + if i, err := strconv.Atoi(appIdStr); err == nil { + selectGoodsList.AppId = i + } + if i, err := strconv.ParseInt(pageNoStr, 10, 32); err == nil { + selectGoodsList.PageNo = int32(i) + } + if i, err := strconv.ParseInt(pageSizeStr, 10, 32); err == nil { + selectGoodsList.PageSize = int32(i) + } + + initialPageNo = selectGoodsList.PageNo + if selectGoodsList.PageSize > 0 { + pageSize = selectGoodsList.PageSize + } + + response, err = controller.SelectGoodsListWithoutTime(selectGoodsList) + if err != nil { + log.Printf("查询商品失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "查询商品失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + + var resp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Count int `json:"count"` + List []map[string]interface{} `json:"list"` + PageNo int `json:"page_no"` + PageSize int `json:"page_size"` + } `json:"data"` + } + if err := json.Unmarshal(response, &resp); err != nil { + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + } + return + } + + combinedList = append(combinedList, resp.Data.List...) + + totalCount := resp.Data.Count + if resp.Data.PageSize > 0 { + pageSize = int32(resp.Data.PageSize) + } + + totalPages := 1 + if pageSize > 0 { + totalPages = (totalCount + int(pageSize) - 1) / int(pageSize) + } + + for p := 1; p <= totalPages; p++ { + if int32(p) == initialPageNo { + continue + } + selectGoodsList.PageNo = int32(p) + pageResp, pageErr := controller.SelectGoodsListWithoutTime(selectGoodsList) + if pageErr != nil { + log.Printf("Warning: 获取第 %d 页失败: %v", p, pageErr) + continue + } + var pageParsed struct { + Data struct { + List []map[string]interface{} `json:"list"` + } `json:"data"` + } + if perr := json.Unmarshal(pageResp, &pageParsed); perr != nil { + log.Printf("Warning: 解析第 %d 页响应失败: %v", p, perr) + continue + } + combinedList = append(combinedList, pageParsed.Data.List...) + } + + finalRespMap := make(map[string]interface{}) + if err := json.Unmarshal(response, &finalRespMap); err != nil { + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + } + return + } + + outerCount := make(map[string]int) + for _, item := range combinedList { + if v, ok := item["outer_id"].(string); ok { + if v == "" { + outerCount["(空)"]++ + } else { + outerCount[v]++ + } + } + } + stats := map[string]interface{}{ + "total_count": len(combinedList), + "distribution": outerCount, + } + + if dataMap, ok := finalRespMap["data"].(map[string]interface{}); ok { + dataMap["outer_id_stats"] = stats + finalRespMap["data"] = dataMap + } + + bts, merr := json.Marshal(finalRespMap) + if merr != nil { + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + } + return + } + + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(bts)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + return + } +} + +// 获取店铺列表处理器 +func getShopListHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var body _type.GetShopList + appIdStr := r.FormValue("appId") + body.AppSecret = r.FormValue("appSecret") + body.AppId, _ = strconv.Atoi(appIdStr) + response, err := controller.GetShopList(body) + if err != nil { + log.Printf("获取店铺列表失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "获取店铺列表失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 获取订单列表处理器 +func getOrderListHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var body _type.GetShopList + appIdStr := r.FormValue("appId") + body.AppSecret = r.FormValue("appSecret") + body.AppId, _ = strconv.Atoi(appIdStr) + response, err := controller.GetOrderList(body) + if err != nil { + log.Printf("获取店铺列表失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "获取店铺列表失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 发布处理器 +func (h *routeHandler) onlineTestHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var body _type.Body + + // 请求参数处理 + body, err := requestUtil.RequestParams(r) + if err != nil { + log.Printf("请求参数处理失败: %v", err) + errMsg := fmt.Sprintf(`{"error": "请求参数处理失败: %v"}`, err) + http.Error(w, errMsg, http.StatusBadRequest) + return + } + + // 类目处理 + goodsController := &controller.GoodsController{ + ExcelPath: h.cfg.File.ExcelPath, + TxtPath: h.cfg.File.TxtPath, + SheetName: h.cfg.File.SheetName, + } + + createResponse, err := goodsController.GoodsCreatController( + body, + h.cfg.BatchCreatRequest.Path, + h.cfg.App.Domain, + true, + ) + + if err != nil { + errorMsg := fmt.Sprintf(`{"error": "请求解析或验证失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusBadRequest) + return + } + + // 返回Response结果 + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(createResponse)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 获取商品详情处理器 +func getGoodsDetailHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var body _type.GetGoosDetail + productIdStr := r.FormValue("productId") + appIdStr := r.FormValue("appId") + body.AppSecret = r.FormValue("appSecret") + if i, err := strconv.ParseInt(productIdStr, 10, 64); err == nil { + body.ProductId = int64(i) + } else { + body.ProductId = 0 + } + body.AppId, _ = strconv.Atoi(appIdStr) + + response, err := controller.GetGoosDetail(body) + if err != nil { + log.Printf("获取商品详情失败: %v", err) + errorMsg := fmt.Sprintf(`{"error": "获取商品详情失败: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + if err = json.NewEncoder(w).Encode(json.RawMessage(response)); err != nil { + log.Printf("Failed to encode response: %v", err) + errorMsg := fmt.Sprintf(`{"error": "无法对响应进行编码: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 404 处理器 +func notFoundHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + response := map[string]interface{}{ + "error": "未找到路由", + "path": r.URL.Path, + "method": r.Method, + "code": 404, + "message": "请求的资源不存在", + } + + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("错误的编码响应: %v", err) + errorMsg := fmt.Sprintf(`{"error": "错误的编码响应: %v"}`, err) + http.Error(w, errorMsg, http.StatusInternalServerError) + return + } +} + +// 请求类目Id处理器 +func (h *routeHandler) getCategoryListHandler(w http.ResponseWriter, r *http.Request) { + fmt.Println("开始请求商品分类列表") + + // 创建或打开txt文件 + file, err := os.OpenFile("category_list.txt", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + log.Printf("创建文件失败: %v", err) + return + } + defer file.Close() + + // 请求商品分类列表 + fmt.Println("正在获取商品分类列表...") + categoryPath := h.cfg.CategoryListRequest.Path + categoryRequest := _type.CategoryListRequest{ + ItemBizType: h.cfg.CategoryListRequest.ItemBizType, // 普通商品类型 + SpBizType: h.cfg.CategoryListRequest.SpBizType, // 潮品行业类型(可选) + } + + categoryResponseBody, err := requestUtil.MakeAPIRequest(h.cfg.App.AppId, h.cfg.App.AppSecret, h.cfg.App.Domain, categoryPath, categoryRequest) + if err != nil { + log.Printf("获取商品分类列表失败: %v", err) + } else { + fmt.Println("商品分类列表响应:") + fmt.Println(string(categoryResponseBody)) + + // 解析响应以获取分类信息 + var categoryResponse _type.CategoryListResponse + if err := json.Unmarshal(categoryResponseBody, &categoryResponse); err != nil { + log.Printf("解析分类响应失败: %v", err) + } else if categoryResponse.Code == 0 { + fmt.Println("成功获取商品分类:") + + // 遍历分类列表,同时打印和写入文件 + for _, item := range categoryResponse.Data.List { + // 控制台打印 + fmt.Printf("行业: %s, 类目: %s (ID: %s)\n", + item.SpBizName, item.ChannelCatName, item.ChannelCatId) + + // 写入文件 + line := fmt.Sprintf("行业: %s, 类目: %s (ID: %s)\n", + item.SpBizName, item.ChannelCatName, item.ChannelCatId) + if _, err := file.WriteString(line); err != nil { + log.Printf("写入文件失败: %v", err) + } + } + + fmt.Printf("共获取 %d 个分类,已保存到 category_list.txt\n", len(categoryResponse.Data.List)) + } else { + errorMsg := fmt.Sprintf("获取分类失败: %s (代码: %d)\n", categoryResponse.Msg, categoryResponse.Code) + fmt.Printf(errorMsg) + file.WriteString(errorMsg) + } + } + + if err != nil { + log.Printf("获取商品分类列表失败: %v", err) + } + + fmt.Println("商品分类列表获取结束") +} + +// 将统计结果添加到响应中的辅助函数 +func addOuterIDStatsToResponse(originalResponse []byte) ([]byte, error) { + var responseMap map[string]interface{} + if err := json.Unmarshal(originalResponse, &responseMap); err != nil { + return nil, fmt.Errorf("解析响应JSON失败: %v", err) + } + + // 获取统计结果 + stats := getOuterIDStats(originalResponse) + + // 将统计结果添加到response的data中 + if data, exists := responseMap["data"].(map[string]interface{}); exists { + data["outer_id_stats"] = stats + responseMap["data"] = data + } + + return json.Marshal(responseMap) +} + +// 获取outer_id统计 +func getOuterIDStats(responseData []byte) map[string]interface{} { + stats := make(map[string]interface{}) + + var data struct { + Data struct { + List []struct { + OuterID string `json:"outer_id"` + } `json:"list"` + } `json:"data"` + } + + if err := json.Unmarshal(responseData, &data); err != nil { + return stats + } + + outerIDCount := make(map[string]int) + totalCount := len(data.Data.List) + + for _, item := range data.Data.List { + outerID := item.OuterID + if outerID == "" { + outerID = "(空)" + } + outerIDCount[outerID]++ + } + + stats["total_count"] = totalCount + stats["distribution"] = outerIDCount + + return stats +} diff --git a/http/service/httpServer.go b/http/service/httpServer.go new file mode 100644 index 0000000..02bc5c8 --- /dev/null +++ b/http/service/httpServer.go @@ -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 +} diff --git a/type/config.go b/type/config.go new file mode 100644 index 0000000..af0c69b --- /dev/null +++ b/type/config.go @@ -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"` + } +} diff --git a/type/type.go b/type/type.go new file mode 100644 index 0000000..043380e --- /dev/null +++ b/type/type.go @@ -0,0 +1,1440 @@ +package _type + +import ( + "fmt" + "reflect" + "strings" + "unicode/utf8" +) + +// ErrResponse 错误响应 +type ErrResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +// 品牌捡漏信息-生产信息 +type ExpireInfoBrand struct { + Num int32 `json:"num"` // 保质期(必需),范围:1-9999 + Unit string `json:"unit"` // 单位(必需),枚举值:天、月、年 +} + +// 品牌捡漏信息-有效期信息 +type ProductionInfoBrand struct { + Date string `json:"date"` // 食品生产日期(必需),长度限制:1-20个字符 +} + +// 品牌捡漏信息-新图片信息 +type ImageInfo struct { + Src string `json:"src"` // 图片地址(必需) + Width int32 `json:"width"` // 图片宽度(必需) + Height int32 `json:"height"` // 图片高度(必需) +} + +// CategoryListRequest 商品分类列表请求结构体 +type CategoryListRequest struct { + ItemBizType int `json:"item_biz_type"` // 商品类型(必需) + SpBizType int `json:"sp_biz_type,omitempty"` // 行业类型(可选) + FlashSaleType int `json:"flash_sale_type,omitempty"` // 闲鱼特卖类型(可选) +} + +// CategoryListResponse 商品分类列表响应结构体 +type CategoryListResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data CategoryData `json:"data"` +} + +// CategoryData 分类数据 +type CategoryData struct { + List []CategoryItem `json:"list"` +} + +// CategoryItem 分类项 +type CategoryItem struct { + SpBizType int `json:"sp_biz_type"` // 行业类型 + SpBizName string `json:"sp_biz_name"` // 行业名称 + ChannelCatId string `json:"channel_cat_id"` // 渠道类目ID + ChannelCatName string `json:"channel_cat_name"` // 渠道类目名称 +} + +// 咸鱼特卖-生产信息 +type ExpireInfoAdvent struct { + Num int32 `json:"num"` // 保质期(必需),范围:1-9999 + Unit string `json:"unit"` // 单位(必需),枚举值:天、月、年 +} + +// 咸鱼特卖-有效期信息 +type ProductionInfoAdvent struct { + Date string `json:"date"` // 食品生产日期(必需),长度限制:1-20个字符 +} + +// 食品生产信息-食品生产地信息 +type AddressInfo struct { + Detail string `json:"detail"` // 详细地址(必需),长度限制:1-60个字符 + Province int32 `json:"province"` // 生产地省份ID(必需) + City int32 `json:"city"` // 生产地城市ID(必需) + District int32 `json:"district"` // 生产地地区ID(必需) +} + +// 食品信息-食品有效期信息 +type ExpireInfoFood struct { + Num int32 `json:"num"` // 保质期(必需),范围:1-9999 + Unit string `json:"unit"` // 单位(必需),枚举值:天、月、年 +} + +// 食品信息-食品生产信息 +type ProductionInfoFood struct { + Date string `json:"date"` // 食品生产日期(必需),长度限制:1-20个字符 + Address AddressInfo `json:"address"` // 食品生产地信息(必需) +} + +// 严选3c-质检报告项 +type ReportItem struct { + AnswerID int32 `json:"answer_id"` // 选项ID(必需) + AnswerName string `json:"answer_name"` // 选项名称(必需) + AnswerType int32 `json:"answer_type"` // 选项类型(必需) + AnswerDesc string `json:"answer_desc"` // 选项描述(必需) + QuestionName string `json:"question_name"` // 问题名称(必需) + CategoryName string `json:"category_name"` // 分类名称(必需) + GroupName string `json:"group_name"` // 分组名称(必需) +} + +// 验货报告-美妆信息 +type BeautyMakeup struct { + Brand string `json:"brand"` // 品牌(必需) + Spec string `json:"spec"` // 规格(必需) + Level string `json:"level"` // 成色(必需) + OrgID int32 `json:"org_id"` // 检测机构ID(必需) + OrgName string `json:"org_name"` // 检测机构名称(必需) + Images []string `json:"images"` // 图片信息(必需),1-30个项 +} + +// 验货报告-文玩信息 +type Curio struct { + Size string `json:"size"` // 尺寸(必需) + Material string `json:"material"` // 材料(必需) + OrgID int32 `json:"org_id"` // 检测机构ID(必需) + OrgName string `json:"org_name"` // 检测机构名称(必需) + QcNo string `json:"qc_no"` // 验货编码(必需) + Images []string `json:"images"` // 图片信息(必需),1-30个项 +} + +// 验货报告-珠宝信息 +type Jewelry struct { + Shape string `json:"shape"` // 形状(必需) + Color string `json:"color"` // 颜色(必需) + Weight string `json:"weight"` // 重量(必需) + OrgName string `json:"org_name"` // 检测机构名称(必需) + QcNo string `json:"qc_no"` // 验货编码(必需) + QcDesc string `json:"qc_desc"` // 验货描述(必需) + Images []string `json:"images"` // 图片信息(必需),1-30个项 +} + +// 验货报告-游戏信息 +type Game struct { + Title string `json:"title"` // 报告标题(必需) + Platform string `json:"platform"` // 游戏平台(必需) + QcNo string `json:"qc_no"` // 验货编码(必需) + QcDesc string `json:"qc_desc"` // 验货描述(必需) + Images []string `json:"images"` // 图片信息(必需),1-30个项 +} + +// 验货报告-OpenProductReportUsedCar +type UsedCar struct { + ReportURL string `json:"report_url,omitempty"` // 验货报告链接(可选) + DrivingLicenseInfo string `json:"driving_license_info,omitempty"` // 行驶证主页图片(可选) + DrivingLicenseCarPhoto string `json:"driving_license_car_photo,omitempty"` // 行驶证车辆页图片(可选) + BusinessLicenseFront string `json:"business_license_front,omitempty"` // 营业执照图片(可选) + CarFunction string `json:"car_function,omitempty"` // 使用性质(可选) + CarVin string `json:"car_vin,omitempty"` // 车辆识别代码VIN码(可选) +} + +// 验货报告-奢品信息 +type Valuable struct { + OrgID int32 `json:"org_id"` // 检测机构ID(必需) + OrgName string `json:"org_name"` // 检测机构名称(必需) + QcNo string `json:"qc_no"` // 验货编码(必需) + QcDesc string `json:"qc_desc"` // 验货描述(必需) + Images []string `json:"images"` // 图片信息(必需),1-30个项 +} + +// 验货报告-严选3c信息 +type Yx3c struct { + ClassID int32 `json:"class_id"` // 品类ID(必需) + SubclassID int32 `json:"subclass_id"` // 子类ID(必需) + BrandID int32 `json:"brand_id"` // 品牌ID(必需) + BrandName string `json:"brand_name"` // 品牌名称(必需) + ModelID int32 `json:"model_id"` // 机型ID(必需) + ModelName string `json:"model_name"` // 机型名称(必需) + ModelSn string `json:"model_sn"` // IMEI/序列号(必需) + ReportUser string `json:"report_user"` // 质检人(必需) + ReportTime string `json:"report_time"` // 质检时间(必需) + ReportItems ReportItem `json:"report_items"` // 质检报告项(必需) + AnswerIDs []int32 `json:"answer_ids"` // 质检选项ID(必需),内部存储 +} + +// 商品属性(可选) +type ChannelPv struct { + PropertyID string `json:"property_id"` // 属性ID(必需)示例值: 83b8f62c43df34e6 + PropertyName string `json:"property_name"` // 属性名称(必需)示例值: 品牌 + ValueID string `json:"value_id"` // 属性值ID(必需)示例值: 76f78d92eeb4f5f6eccf7d4fabef47b6 + ValueName string `json:"value_name"` // 属性值名称(必需)示例值: Apple/苹果 +} + +// 发布店铺(必需) +type PublishShop struct { + UserName string `json:"user_name"` // 闲鱼会员名(必需) + Province int32 `json:"province"` // 商品发货省份(必需) + City int32 `json:"city"` // 商品发货城市(必需) + District int32 `json:"district"` // 商品发货地区(必需) + Title string `json:"title"` // 商品标题(必需),注意:一个中文按2个字符算,长度限制:1-60个字符 + Content string `json:"content"` // 商品描述(必需),注意:一个中文按2个字符算,不支持HTML代码,可使用\n换行,长度限制:5-5000字符 + Images []string `json:"images"` // 图片信息(必需),数量限制:1-30张 + WhiteImages string `json:"white_images"` // 商品白底图URL(可选),注意:1.如果传入会在闲鱼商品详情显示且无法删除只能修改 2.当商品类型是特卖类型(item_biz_type=24)时为必填 + ServiceSupport string `json:"service_support"` // 商品服务项(可选),多个时用英文逗号拼接。枚举值:SDR(七天无理由退货)、NFR(描述不符包邮退)、VNR(描述不符全额退-虚拟类)、FD_10MS(10分钟极速发货-虚拟类)、FD_24HS(24小时极速发货)、FD_48HS(48小时极速发货)、FD_GPA(正品保障) +} + +// 拍卖信息(可选) +type BidData struct { + BidStartTime string `json:"bid_start_time"` // 拍卖开始时间(必需),示例值:2023-07-21 00:00:00 + BidEndTime string `json:"bid_end_time"` // 拍卖结束时间(必需),示例值:2023-07-21 00:00:00 + BidReservePrice int64 `json:"bid_reserve_price"` // 拍卖起拍价(分)(必需),范围:100-9999999900 + BidIncreasePrice int64 `json:"bid_increase_price"` // 拍卖加拍价(分)(必需),范围:100-10000 + BidDeposit int64 `json:"bid_deposit"` // 拍卖保证金(分)(必需),范围:100-9999999900 +} + +// 多规格信息(可选) +type SkuItems struct { + Price int64 `json:"price"` // 商品售价(必需),单位:分,范围:1-9999999900 + Stock int32 `json:"stock"` // SKU库存(必需),范围:0-9999 + SkuText string `json:"sku_text"` // SKU规格(必需),格式:规格:属性,多个时使用";"拼接,如:颜色:白色;容量:128G + OuterId string `json:"outer_id"` // SKU商品编码(可选),长度限制:0-64个字符(一个中文按2个字符算) +} + +// 图书信息(可选) +type BookData struct { + ISBN string `json:"isbn"` // 图书ISBN码(必需),格式校验:/^((?:[0-9]{9}X|[0-9]{10})|(?:(?:97(?:8|9))[0-9]{10}))$/ + Title string `json:"title"` // 图书标题(必需) + Author string `json:"author"` // 图书作者(可选),长度限制:1-30个字符 + Publisher string `json:"publisher"` // 图书出版社(可选),长度限制:1-30个字符 +} + +// 食品信息(可选) +type FoodInfo struct { + Brand string `json:"brand"` // 食品品牌(必需),长度限制:1-30个字符 + Spec string `json:"spec"` // 食品规格(必需),长度限制:1-30个字符 + Pack string `json:"pack"` // 食品包装(必需),长度限制:1-10个字符 + Expire ExpireInfoFood `json:"expire"` // 食品有效期信息(必需) + Production ProductionInfoFood `json:"production"` // 食品生产信息(必需) +} + +// 验货报告信息 +type ReportData struct { + BeautyMakeup BeautyMakeup `json:"beauty_makeup,omitempty"` // 美妆信息(可选) + Curio Curio `json:"curio,omitempty"` // 文玩信息(可选) + Jewelry Jewelry `json:"jewelry,omitempty"` // 珠宝信息(可选) + Game Game `json:"game,omitempty"` // 游戏信息(可选) + UsedCar UsedCar `json:"used_car,omitempty"` // 二手车信息(可选) + Valuable Valuable `json:"valuable,omitempty"` // 奢品信息(可选) + Yx3c Yx3c `json:"yx_3c,omitempty"` // 严选3c信息(可选) +} + +// 闲鱼特卖信息 +type AdventData struct { + Expire ExpireInfoAdvent `json:"expire"` // 有效期信息 (必需) + Production ProductionInfoAdvent `json:"production"` // 生产信息 (必需) +} + +// 验货宝信息 +type InspectData struct { + TradeRule string `json:"trade_rule"` //交易规则(必需) + AssumeRule string `json:"assume_rule"` //验货费规则 (必需) +} + +// 品牌捡漏信息 +type BrandData struct { + Expire ExpireInfoBrand `json:"expire"` // 有效期信息 (可选) + Production ProductionInfoBrand `json:"production"` // 生产信息 (可选) + Supplier string `json:"supplier"` // 供应商名称(可选) + Images ImageInfo `json:"images"` // 新图片信息(可选) +} + +// 新图片信息 +type DetailImage struct { + Src string `json:"src"` // 图片地址(必需) + Width int32 `json:"width"` // 图片宽度(必需) + Height int32 `json:"height"` // 图片高度(必需) +} + +// 规格图片信息 +type SkuImages struct { + Src string `json:"src"` // 图片地址(必需) + Width int32 `json:"width"` // 图片宽度(必需) + Height int32 `json:"height"` // 图片高度(必需) + SkuText string `json:"sku_text"` // 规格属性(必需) +} + +// BashCreat 咸鱼批量创建商品 Body参数 +type BashCreat struct { + ItemKey string `json:"item_key"` // 批次商品KEY(必需),原样返回,用于匹配商家自己的商品 + ItemBizType int32 `json:"item_biz_type"` // 商品类型(必需),枚举值:2-普通商品,0-已验货,10-验货宝,16-品牌授权,19-闲鱼严选,24-闲鱼特卖,26-品牌捡漏 + SpBizType int32 `json:"sp_biz_type"` // 行业类型(必需),枚举值:1-手机,2-潮品,3-家电,8-乐器,9-3C数码,16-奢品,17-母婴,18-美妆个护,19-文玩/珠宝,20-游戏电玩,21-家居,22-虚拟游戏,23-租号,24-图书,25-卡券,27-食品,28-潮玩,29-二手车,30-宠植,31-工艺礼品,33-汽车服务,99-其他 + ChannelCatId string `json:"channel_cat_id"` // 商品类目ID(必需),通过查询商品类目接口获取类目参数 + ChannelPv []ChannelPv `json:"channel_pv"` // 商品属性(可选) + Price int64 `json:"price"` // 商品售价(必需),范围:1-9999999900,单位:分 + OriginalPrice int64 `json:"original_price"` // 商品原价(可选),范围:0-9999999900,单位:分 + ExpressFee int64 `json:"express_fee"` // 运费(可选),单位:分 + Stock int32 `json:"stock"` // 商品库存(必需),注意:多规格商品,必须是SKU库存的合计,范围:1-399960 + OuterId string `json:"outer_id"` // 商家编码(可选),长度限制:1-64个字符 + StuffStatus int32 `json:"stuff_status"` // 商品成色(可选),枚举值:0-无成色,100-全新,-1-准新,99-99新,95-95新,90-9新,80-8新,70-7新,60-6新,50-5新,40-未使用·中度瑕疵,30-未使用·轻微瑕疵,20-未使用·仅拆封,10-未使用·准新,100-全新未使用 + PublishShop []PublishShop `json:"publish_shop"` // 发布店铺(必需) + BidData []BidData `json:"bid_data"` // 拍卖信息(可选) + SkuItems []SkuItems `json:"sku_items"` // 商品多规格信息(可选) + BookData BookData `json:"book_data"` // 图书信息(可选) + FoodData []FoodInfo `json:"food_data"` // 食品信息(可选) + ReportData []ReportData `json:"report_data"` // 验货报告信息(可选) + FlashSaleType int32 `json:"flash_sale_type"` // 闲鱼特卖类型(可选),枚举值:1-临期,2-孤品,3-断码,4-微瑕,5-尾货,6-官翻,7-全新,8-福袋,99-其他,2601-微瑕,2602-临期,2603-清仓,2604-官翻 + AdventData []AdventData `json:"advent_data"` // 闲鱼特卖信息(可选) + InspectData []InspectData `json:"inspect_data"` // 验货宝信息(可选) + BrandData []BrandData `json:"brand_data"` // 品牌捡漏信息(可选) + DetailImages []DetailImage `json:"detail_images"` // 新图片信息(可选) + SkuImages []SkuImages `json:"sku_images"` // 规格图片(可选) +} + +// BashCreat 咸鱼批量创建商品 Body参数 +type BashCreatNew struct { + ItemKey string `json:"item_key"` // 批次商品KEY(必需),原样返回,用于匹配商家自己的商品 + ItemBizType int32 `json:"item_biz_type"` // 商品类型(必需),枚举值:2-普通商品,0-已验货,10-验货宝,16-品牌授权,19-闲鱼严选,24-闲鱼特卖,26-品牌捡漏 + SpBizType int32 `json:"sp_biz_type"` // 行业类型(必需),枚举值:1-手机,2-潮品,3-家电,8-乐器,9-3C数码,16-奢品,17-母婴,18-美妆个护,19-文玩/珠宝,20-游戏电玩,21-家居,22-虚拟游戏,23-租号,24-图书,25-卡券,27-食品,28-潮玩,29-二手车,30-宠植,31-工艺礼品,33-汽车服务,99-其他 + ChannelCatId string `json:"channel_cat_id"` // 商品类目ID(必需),通过查询商品类目接口获取类目参数 + ChannelPv []ChannelPv `json:"channel_pv"` // 商品属性(可选) + Price int64 `json:"price"` // 商品售价(必需),范围:1-9999999900,单位:分 + OriginalPrice int64 `json:"original_price"` // 商品原价(可选),范围:0-9999999900,单位:分 + ExpressFee int64 `json:"express_fee"` // 运费(可选),单位:分 + Stock int32 `json:"stock"` // 商品库存(必需),注意:多规格商品,必须是SKU库存的合计,范围:1-399960 + OuterId string `json:"outer_id"` // 商家编码(可选),长度限制:1-64个字符 + StuffStatus int32 `json:"stuff_status"` // 商品成色(可选),枚举值:0-无成色,100-全新,-1-准新,99-99新,95-95新,90-9新,80-8新,70-7新,60-6新,50-5新,40-未使用·中度瑕疵,30-未使用·轻微瑕疵,20-未使用·仅拆封,10-未使用·准新,100-全新未使用 + PublishShop []PublishShop `json:"publish_shop"` // 发布店铺(必需) + BidData []BidData `json:"bid_data"` // 拍卖信息(可选) + SkuItems []SkuItems `json:"sku_items"` // 商品多规格信息(可选) + // BookData BookData `json:"book_data"` // 图书信息(可选) + FoodData []FoodInfo `json:"food_data"` // 食品信息(可选) + ReportData []ReportData `json:"report_data"` // 验货报告信息(可选) + FlashSaleType int32 `json:"flash_sale_type"` // 闲鱼特卖类型(可选),枚举值:1-临期,2-孤品,3-断码,4-微瑕,5-尾货,6-官翻,7-全新,8-福袋,99-其他,2601-微瑕,2602-临期,2603-清仓,2604-官翻 + AdventData []AdventData `json:"advent_data"` // 闲鱼特卖信息(可选) + InspectData []InspectData `json:"inspect_data"` // 验货宝信息(可选) + BrandData []BrandData `json:"brand_data"` // 品牌捡漏信息(可选) + DetailImages []DetailImage `json:"detail_images"` // 新图片信息(可选) + SkuImages []SkuImages `json:"sku_images"` // 规格图片(可选) +} + +type BookDataInput struct { + ISBN string `json:"isbn"` // 图书ISBN码(必需),格式校验:/^((?:[0-9]{9}X|[0-9]{10})|(?:(?:97(?:8|9))[0-9]{10}))$/ + Title string `json:"title"` // 图书标题(必需) + Author string `json:"author"` // 图书作者(可选),长度限制:1-30个字符 + Publisher string `json:"publisher"` // 图书出版社(可选),长度限制:1-30个字符 + Prices []int64 `json:"prices"` // 价格数组[商品原价,商品售价,...] + Stock int32 `json:"stock"` // 库存 + ItemBizType int32 `json:"itemBizType"` // 咸鱼_商品类型(必需),枚举值:2-普通商品,0-已验货,10-验货宝,16-品牌授权,19-闲鱼严选,24-闲鱼特卖,26-品牌捡漏 + SpBizType int32 `json:"spBizType"` // 咸鱼_行业类型(必需),枚举值:1-手机,2-潮品,3-家电,8-乐器,9-3C数码,16-奢品,17-母婴,18-美妆个护,19-文玩/珠宝,20-游戏电玩,21-家居,22-虚拟游戏,23-租号,24-图书,25-卡券,27-食品,28-潮玩,29-二手车,30-宠植,31-工艺礼品,33-汽车服务,99-其他 + CatIds string `json:"catIds"` +} + +type BookDataInputNew struct { + ISBN string `json:"isbn"` // 图书ISBN码(必需),格式校验:/^((?:[0-9]{9}X|[0-9]{10})|(?:(?:97(?:8|9))[0-9]{10}))$/ + Title string `json:"title"` // 图书标题(必需) + Author string `json:"author"` // 图书作者(可选),长度限制:1-30个字符 + Publisher string `json:"publisher"` // 图书出版社(可选),长度限制:1-30个字符 +} + +type SkuMsg struct { + Key string `json:"key"` // 必填 + Value string `json:"value"` // 必填 + Title string `json:"title"` // 必填 + CatIds string `json:"cat_ids"` // 必填 + MainImgs []string `json:"mainImgs"` // 主图, 数组形式 + ContentImgs []string `json:"contentImgs"` // 内容图,数组形式 + Content string `json:"content"` // 商品描述 + UserName string `json:"userName"` // 用户名 +} + +type Shop struct { + UserName string `json:"userName"` // 用户名 + Province int32 `json:"province"` + City int32 `json:"city"` + District int32 `json:"district"` + Title string `json:"title"` // 必填 + Content string `json:"content"` // 商品描述 + MainImgs []string `json:"mainImgs"` // 主图, 数组形式 + ContentImgs []string `json:"contentImgs"` // 内容图,数组形式 +} + +type Body struct { + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` + OuterId string `json:"outerId"` + Token string `json:"token"` + ApiShopId int `json:"apiShopId"` + TypePlatform int `json:"typePlatform"` + ShopId int `json:"shopId"` + ShopToken string `json:"shopToken"` + ShopName string `json:"shopName"` + Province int32 `json:"province"` + City int32 `json:"city"` + District int32 `json:"district"` + TypeClass string `json:"typeClass"` + TypeGoods string `json:"typeGoods"` + CatIds string `json:"catIds"` + SkuMsgs []SkuMsg `json:"skuMsgs"` + StuffStatus int `json:"stuffStatus"` + ItemKey string `json:"itemKey"` + BookData []BookDataInput `json:"bookData"` + Shop []Shop `json:"shop"` +} + +type BodyNew struct { + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` + OuterId string `json:"outerId"` + Token string `json:"token"` + ApiShopId int `json:"apiShopId"` + TypePlatform int `json:"typePlatform"` + ShopId int `json:"shopId"` + ShopToken string `json:"shopToken"` + ShopName string `json:"shopName"` + Province int32 `json:"province"` + City int32 `json:"city"` + District int32 `json:"district"` + TypeClass string `json:"typeClass"` + TypeGoods string `json:"typeGoods"` + CatIds string `json:"catIds"` + SkuMsgs []SkuMsg `json:"skuMsgs"` + StuffStatus int `json:"stuffStatus"` + ItemKey string `json:"itemKey"` + BookData []BookDataInputNew `json:"bookData"` + Shop []Shop `json:"shop"` + Price int64 `json:"price"` + OriginalPrice int64 `json:"original_price"` + Stock int32 `json:"stock"` + ItemBizType int32 `json:"itemBizType"` + SpBizType int32 `json:"spBizType"` +} + +type ListedProducts struct { + ProductId int `json:"product_id"` //管家商品ID + UserName []string `json:"user_name"` //闲鱼会员名 + SpecifyPublishTime string `json:"specify_publish_time"` //定时上架时间 + NotifyUrl string `json:"notify_url"` //上架回调地址 + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` +} + +type DownAndFlash struct { + ProductId int64 `json:"product_id"` + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` +} + +type EditPrices struct { + ProductId int64 `json:"product_id"` + Price int64 `json:"price"` + OriginalPrice int64 `json:"original_price"` + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` +} + +type EditStock struct { + ProductId int64 `json:"product_id"` + Stock int32 `json:"stock"` + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` +} + +type SelectGoodsListWithTime struct { + OfflineTime []int64 `json:"offline_time"` + ProductStatus int32 `json:"product_status"` + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` + PageNo int32 `json:"page_no"` + PageSize int32 `json:"page_size"` +} + +type SelectGoodsListWithoutTime struct { + ProductStatus int32 `json:"product_status"` + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` + PageNo int32 `json:"page_no"` + PageSize int32 `json:"page_size"` +} + +type GetShopList struct { + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` +} + +type GetOrderShip struct { + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` + OrderNo string `json:"order_no"` + ShipName string `json:"ship_name"` + ShipMobile string `json:"ship_mobile"` + ShipDistrictID int `json:"ship_district_id"` + ShipProvName string `json:"ship_prov_name"` + ShipCityName string `json:"ship_city_name"` + ShipAreaName string `json:"ship_area_name"` + ShipAddress string `json:"ship_address"` + WaybillNo string `json:"waybill_no"` + ExpressName string `json:"express_name"` + ExpressCode string `json:"express_code"` +} + +type GetGoosDetail struct { + ProductId int64 `json:"product_id"` + AppId int `json:"appId"` + AppSecret string `json:"appSecret"` +} + +// ================校验相关函数=============== + +// ValidationError 校验错误类型 +type ValidationError struct { + Errors []string +} + +func (v *ValidationError) Error() string { + return strings.Join(v.Errors, "; ") +} + +func (v *ValidationError) Add(field, message string) { + v.Errors = append(v.Errors, fmt.Sprintf("%s: %s", field, message)) +} + +// 计算中文字符长度(一个中文算2个字符) +func chineseAwareLength(s string) int { + runeCount := utf8.RuneCountInString(s) + byteCount := len(s) + // 估算:每个中文字符大约占3字节,英文字符占1字节 + // 简单估算:长度 ≈ (字节数 + 字符数) / 2 + return (byteCount + runeCount) / 2 +} + +/** + * 各个内部结构体的校验方法 + */ + +func (e ExpireInfoBrand) Validate() error { + var errs ValidationError + if e.Num < 1 || e.Num > 9999 { + errs.Add("Num", "must be between 1 and 9999") + } + if e.Unit != "天" && e.Unit != "月" && e.Unit != "年" { + errs.Add("Unit", "must be one of: 天, 月, 年") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (p ProductionInfoBrand) Validate() error { + var errs ValidationError + if p.Date == "" { + errs.Add("Date", "is required") + } else if chineseAwareLength(p.Date) < 1 || chineseAwareLength(p.Date) > 20 { + errs.Add("Date", "must be between 1 and 20 characters") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (i ImageInfo) Validate() error { + var errs ValidationError + if i.Src == "" { + errs.Add("Src", "is required") + } + if i.Width <= 0 { + errs.Add("Width", "must be positive") + } + if i.Height <= 0 { + errs.Add("Height", "must be positive") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (e ExpireInfoAdvent) Validate() error { + var errs ValidationError + if e.Num < 1 || e.Num > 9999 { + errs.Add("Num", "must be between 1 and 9999") + } + if e.Unit != "天" && e.Unit != "月" && e.Unit != "年" { + errs.Add("Unit", "must be one of: 天, 月, 年") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (p ProductionInfoAdvent) Validate() error { + var errs ValidationError + if p.Date == "" { + errs.Add("Date", "is required") + } else if chineseAwareLength(p.Date) < 1 || chineseAwareLength(p.Date) > 20 { + errs.Add("Date", "must be between 1 and 20 characters") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (a AddressInfo) Validate() error { + var errs ValidationError + if a.Detail == "" { + errs.Add("Detail", "is required") + } else if chineseAwareLength(a.Detail) < 1 || chineseAwareLength(a.Detail) > 60 { + errs.Add("Detail", "must be between 1 and 60 characters") + } + if a.Province <= 0 { + errs.Add("Province", "is required and must be positive") + } + if a.City <= 0 { + errs.Add("City", "is required and must be positive") + } + if a.District <= 0 { + errs.Add("District", "is required and must be positive") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (e ExpireInfoFood) Validate() error { + var errs ValidationError + if e.Num < 1 || e.Num > 9999 { + errs.Add("Num", "must be between 1 and 9999") + } + if e.Unit != "天" && e.Unit != "月" && e.Unit != "年" { + errs.Add("Unit", "must be one of: 天, 月, 年") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (p ProductionInfoFood) Validate() error { + var errs ValidationError + if p.Date == "" { + errs.Add("Date", "is required") + } else if chineseAwareLength(p.Date) < 1 || chineseAwareLength(p.Date) > 20 { + errs.Add("Date", "must be between 1 and 20 characters") + } + if err := p.Address.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Address."+e, "") + } + } + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (p PublishShop) Validate() error { + var errs ValidationError + if p.UserName == "" { + errs.Add("UserName", "is required") + } + if p.Province <= 0 { + errs.Add("Province", "is required") + } + if p.City <= 0 { + errs.Add("City", "is required") + } + if p.District <= 0 { + errs.Add("District", "is required") + } + if p.Title == "" { + errs.Add("Title", "is required") + } else if length := chineseAwareLength(p.Title); length < 1 || length > 60 { + errs.Add("Title", fmt.Sprintf("must be between 1 and 60 characters, got %d", length)) + } + if p.Content == "" { + errs.Add("Content", "is required") + } else if length := chineseAwareLength(p.Content); length < 5 || length > 5000 { + errs.Add("Content", fmt.Sprintf("must be between 5 and 5000 characters, got %d", length)) + } + if len(p.Images) == 0 || len(p.Images) > 30 { + errs.Add("Images", "must have between 1 and 30 items") + } + if p.ServiceSupport != "" { + validServices := map[string]bool{ + "SDR": true, "NFR": true, "VNR": true, "FD_10MS": true, + "FD_24HS": true, "FD_48HS": true, "FD_GPA": true, + } + services := strings.Split(p.ServiceSupport, ",") + for _, s := range services { + if !validServices[s] { + errs.Add("ServiceSupport", fmt.Sprintf("invalid service: %s", s)) + } + } + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// beautyMakeup 校验方法 +func (b BeautyMakeup) Validate() error { + var errs ValidationError + + if b.Brand == "" { + errs.Add("Brand", "is required") + } + + if b.Spec == "" { + errs.Add("Spec", "is required") + } + + if b.Level == "" { + errs.Add("Level", "is required") + } + + if b.OrgID <= 0 { + errs.Add("OrgID", "is required") + } + + if b.OrgName == "" { + errs.Add("OrgName", "is required") + } + + if len(b.Images) == 0 || len(b.Images) > 30 { + errs.Add("Images", "must have between 1 and 30 items") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// curio 校验方法 +func (c Curio) Validate() error { + var errs ValidationError + + if c.Size == "" { + errs.Add("Size", "is required") + } + + if c.Material == "" { + errs.Add("Material", "is required") + } + + if c.OrgID <= 0 { + errs.Add("OrgID", "is required") + } + + if c.OrgName == "" { + errs.Add("OrgName", "is required") + } + + if c.QcNo == "" { + errs.Add("QcNo", "is required") + } + + if len(c.Images) == 0 || len(c.Images) > 30 { + errs.Add("Images", "must have between 1 and 30 items") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// jewelry 校验方法 +func (j Jewelry) Validate() error { + var errs ValidationError + + if j.Shape == "" { + errs.Add("Shape", "is required") + } + + if j.Color == "" { + errs.Add("Color", "is required") + } + + if j.Weight == "" { + errs.Add("Weight", "is required") + } + + if j.OrgName == "" { + errs.Add("OrgName", "is required") + } + + if j.QcNo == "" { + errs.Add("QcNo", "is required") + } + + if j.QcDesc == "" { + errs.Add("QcDesc", "is required") + } + + if len(j.Images) == 0 || len(j.Images) > 30 { + errs.Add("Images", "must have between 1 and 30 items") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// game 校验方法 +func (g Game) Validate() error { + var errs ValidationError + + if g.Title == "" { + errs.Add("Title", "is required") + } + + if g.Platform == "" { + errs.Add("Platform", "is required") + } + + if g.QcNo == "" { + errs.Add("QcNo", "is required") + } + + if g.QcDesc == "" { + errs.Add("QcDesc", "is required") + } + + if len(g.Images) == 0 || len(g.Images) > 30 { + errs.Add("Images", "must have between 1 and 30 items") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// usedCar 校验方法 +func (u UsedCar) Validate() error { + var errs ValidationError + + // usedCar 所有字段都是可选的,不需要必需校验 + // 这里可以添加一些格式校验,比如URL格式等 + + if u.ReportURL != "" { + // 可以添加URL格式校验 + if !strings.HasPrefix(u.ReportURL, "http") { + errs.Add("ReportURL", "must be a valid URL") + } + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// valuable 校验方法 +func (v Valuable) Validate() error { + var errs ValidationError + + if v.OrgID <= 0 { + errs.Add("OrgID", "is required") + } + + if v.OrgName == "" { + errs.Add("OrgName", "is required") + } + + if v.QcNo == "" { + errs.Add("QcNo", "is required") + } + + if v.QcDesc == "" { + errs.Add("QcDesc", "is required") + } + + if len(v.Images) == 0 || len(v.Images) > 30 { + errs.Add("Images", "must have between 1 and 30 items") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// yx3c 校验方法 +func (y Yx3c) Validate() error { + var errs ValidationError + + if y.ClassID <= 0 { + errs.Add("ClassID", "is required") + } + + if y.SubclassID <= 0 { + errs.Add("SubclassID", "is required") + } + + if y.BrandID <= 0 { + errs.Add("BrandID", "is required") + } + + if y.BrandName == "" { + errs.Add("BrandName", "is required") + } + + if y.ModelID <= 0 { + errs.Add("ModelID", "is required") + } + + if y.ModelName == "" { + errs.Add("ModelName", "is required") + } + + if y.ModelSn == "" { + errs.Add("ModelSn", "is required") + } + + if y.ReportUser == "" { + errs.Add("ReportUser", "is required") + } + + if y.ReportTime == "" { + errs.Add("ReportTime", "is required") + } + + // 校验质检报告项 + if err := y.ReportItems.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("ReportItems."+e, "") + } + } + } + + if len(y.AnswerIDs) == 0 { + errs.Add("AnswerIDs", "is required") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// reportItem 校验方法 +func (r ReportItem) Validate() error { + var errs ValidationError + + if r.AnswerID <= 0 { + errs.Add("AnswerID", "is required") + } + + if r.AnswerName == "" { + errs.Add("AnswerName", "is required") + } + + if r.AnswerType <= 0 { + errs.Add("AnswerType", "is required") + } + + if r.AnswerDesc == "" { + errs.Add("AnswerDesc", "is required") + } + + if r.QuestionName == "" { + errs.Add("QuestionName", "is required") + } + + if r.CategoryName == "" { + errs.Add("CategoryName", "is required") + } + + if r.GroupName == "" { + errs.Add("GroupName", "is required") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// bidData 的校验方法 +func (b BidData) Validate() error { + var errs ValidationError + if b.BidStartTime == "" { + errs.Add("BidStartTime", "is required") + } + if b.BidEndTime == "" { + errs.Add("BidEndTime", "is required") + } + if b.BidReservePrice < 100 || b.BidReservePrice > 9999999900 { + errs.Add("BidReservePrice", "must be between 100 and 9999999900") + } + if b.BidIncreasePrice < 100 || b.BidIncreasePrice > 10000 { + errs.Add("BidIncreasePrice", "must be between 100 and 10000") + } + if b.BidDeposit < 100 || b.BidDeposit > 9999999900 { + errs.Add("BidDeposit", "must be between 100 and 9999999900") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// skuItems 的校验方法 +func (s SkuItems) Validate() error { + var errs ValidationError + if s.Price < 1 || s.Price > 9999999900 { + errs.Add("Price", "must be between 1 and 9999999900") + } + if s.Stock < 0 || s.Stock > 9999 { + errs.Add("Stock", "must be between 0 and 9999") + } + if s.SkuText == "" { + errs.Add("SkuText", "is required") + } + if s.OuterId != "" && len(s.OuterId) > 64 { + errs.Add("OuterId", "must be at most 64 characters") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// bookData 的校验方法 +func (b BookData) Validate() error { + var errs ValidationError + if b.Title == "" { + errs.Add("标题", "必须输入") + } + if b.Author != "" && len(b.Author) > 30 { + errs.Add("作者", "最多30字符") + } + if b.Publisher != "" && len(b.Publisher) > 30 { + errs.Add("出版社", "最多30字符") + } + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// FoodInfo 的校验方法 +func (f FoodInfo) Validate() error { + var errs ValidationError + + if f.Brand == "" { + errs.Add("Brand", "is required") + } else if chineseAwareLength(f.Brand) > 30 { + errs.Add("Brand", "must be at most 30 characters") + } + + if f.Spec == "" { + errs.Add("Spec", "is required") + } else if chineseAwareLength(f.Spec) > 30 { + errs.Add("Spec", "must be at most 30 characters") + } + + if f.Pack == "" { + errs.Add("Pack", "is required") + } else if chineseAwareLength(f.Pack) > 10 { + errs.Add("Pack", "must be at most 10 characters") + } + + // 校验嵌套结构体 + if err := f.Expire.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Expire."+e, "") + } + } + } + + if err := f.Production.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Production."+e, "") + } + } + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// ReportData 的校验方法 +func (r ReportData) Validate() error { + var errs ValidationError + + // 美妆信息校验 + if !reflect.DeepEqual(r.BeautyMakeup, BeautyMakeup{}) { + if err := r.BeautyMakeup.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("BeautyMakeup."+e, "") + } + } + } + } + // 文玩信息校验 + if !reflect.DeepEqual(r.Curio, Curio{}) { + if err := r.Curio.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Curio."+e, "") + } + } + } + } + // 珠宝信息校验 + if !reflect.DeepEqual(r.Jewelry, Jewelry{}) { + if err := r.Jewelry.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Jewelry."+e, "") + } + } + } + } + // 游戏信息校验 + if !reflect.DeepEqual(r.Game, Game{}) { + if err := r.Game.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Game."+e, "") + } + } + } + } + // 二手车信息校验 + if !reflect.DeepEqual(r.UsedCar, UsedCar{}) { + if err := r.UsedCar.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("UsedCar."+e, "") + } + } + } + } + + // 奢品信息校验 + if !reflect.DeepEqual(r.Valuable, Valuable{}) { + if err := r.Valuable.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Valuable."+e, "") + } + } + } + } + + // 严选3c信息校验 + if !reflect.DeepEqual(r.Yx3c, Yx3c{}) { + if err := r.Yx3c.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Yx3c."+e, "") + } + } + } + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// AdventData 的校验方法 +func (a AdventData) Validate() error { + var errs ValidationError + + // 校验有效期信息 + if err := a.Expire.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Expire."+e, "") + } + } + } + + // 校验生产信息 + if err := a.Production.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Production."+e, "") + } + } + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// InspectData 的校验方法 +func (i InspectData) Validate() error { + var errs ValidationError + + if i.TradeRule == "" { + errs.Add("TradeRule", "is required") + } + + if i.AssumeRule == "" { + errs.Add("AssumeRule", "is required") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (b BrandData) Validate() error { + var errs ValidationError + + // 有效期信息(可选) + if !reflect.DeepEqual(b.Expire, ExpireInfoBrand{}) { + if err := b.Expire.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Expire."+e, "") + } + } + } + } + + // 生产信息(可选) + if !reflect.DeepEqual(b.Production, ProductionInfoBrand{}) { + if err := b.Production.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Production."+e, "") + } + } + } + } + + // 供应商名称(可选) + if b.Supplier != "" && chineseAwareLength(b.Supplier) > 100 { + errs.Add("Supplier", "must be at most 100 characters") + } + + // 新图片信息(可选) + if !reflect.DeepEqual(b.Images, ImageInfo{}) { + if err := b.Images.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add("Images."+e, "") + } + } + } + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (d DetailImage) Validate() error { + var errs ValidationError + + if d.Src == "" { + errs.Add("Src", "is required") + } + + if d.Width <= 0 { + errs.Add("Width", "must be positive") + } + + if d.Height <= 0 { + errs.Add("Height", "must be positive") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +func (s SkuImages) Validate() error { + var errs ValidationError + + if s.Src == "" { + errs.Add("Src", "is required") + } + + if s.Width <= 0 { + errs.Add("Width", "must be positive") + } + + if s.Height <= 0 { + errs.Add("Height", "must be positive") + } + + if s.SkuText == "" { + errs.Add("SkuText", "is required") + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +// 为其他可空结构体实现类似的 Validate() 方法... +// brandData, detailImage, skuImages + +/** + * Validate BashCreat校验 + */ +func (b BashCreat) Validate() error { + var errs ValidationError + + // SkuItems 切片验证 + if len(b.SkuItems) > 0 { + for i, skuItem := range b.SkuItems { + if err := skuItem.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add(fmt.Sprintf("SkuItems[%d].%s", i, e), "") + } + } else { + errs.Add(fmt.Sprintf("SkuItems[%d]", i), err.Error()) + } + } + } + } + + // BookData 切片验证 + if b.BookData.ISBN != "" || b.BookData.Title != "" { + if err := b.BookData.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add(fmt.Sprintf("BookData.%s", e), "") // 移除索引 + } + } else { + errs.Add("BookData", err.Error()) // 移除索引 + } + } + } + + // DetailImages 切片验证 + if len(b.DetailImages) > 0 { + for i, detailImage := range b.DetailImages { + if err := detailImage.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add(fmt.Sprintf("DetailImages[%d].%s", i, e), "") + } + } else { + errs.Add(fmt.Sprintf("DetailImages[%d]", i), err.Error()) + } + } + } + } + + // SkuImages 切片验证 + if len(b.SkuImages) > 0 { + for i, skuImage := range b.SkuImages { + if err := skuImage.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add(fmt.Sprintf("SkuImages[%d].%s", i, e), "") + } + } else { + errs.Add(fmt.Sprintf("SkuImages[%d]", i), err.Error()) + } + } + } + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} + +/** + * ValidateNew BashCreat校验 + */ +func (b BashCreatNew) ValidateNew() error { + var errs ValidationError + + // SkuItems 切片验证 + if len(b.SkuItems) > 0 { + for i, skuItem := range b.SkuItems { + if err := skuItem.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add(fmt.Sprintf("SkuItems[%d].%s", i, e), "") + } + } else { + errs.Add(fmt.Sprintf("SkuItems[%d]", i), err.Error()) + } + } + } + } + + // DetailImages 切片验证 + if len(b.DetailImages) > 0 { + for i, detailImage := range b.DetailImages { + if err := detailImage.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add(fmt.Sprintf("DetailImages[%d].%s", i, e), "") + } + } else { + errs.Add(fmt.Sprintf("DetailImages[%d]", i), err.Error()) + } + } + } + } + + // SkuImages 切片验证 + if len(b.SkuImages) > 0 { + for i, skuImage := range b.SkuImages { + if err := skuImage.Validate(); err != nil { + if verr, ok := err.(*ValidationError); ok { + for _, e := range verr.Errors { + errs.Add(fmt.Sprintf("SkuImages[%d].%s", i, e), "") + } + } else { + errs.Add(fmt.Sprintf("SkuImages[%d]", i), err.Error()) + } + } + } + } + + if len(errs.Errors) > 0 { + return &errs + } + return nil +} diff --git a/utils/checkUtil/checkUtil.go b/utils/checkUtil/checkUtil.go new file mode 100644 index 0000000..c94f8f3 --- /dev/null +++ b/utils/checkUtil/checkUtil.go @@ -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 +} diff --git a/utils/creatGoodsUtil/creatGoodsUtil.go b/utils/creatGoodsUtil/creatGoodsUtil.go new file mode 100644 index 0000000..9fc13fb --- /dev/null +++ b/utils/creatGoodsUtil/creatGoodsUtil.go @@ -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 +} diff --git a/utils/iniConfigUtil/iniConfigUtil.go b/utils/iniConfigUtil/iniConfigUtil.go new file mode 100644 index 0000000..37bb7bc --- /dev/null +++ b/utils/iniConfigUtil/iniConfigUtil.go @@ -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 +} diff --git a/utils/md5Util/md5Util.go b/utils/md5Util/md5Util.go new file mode 100644 index 0000000..9200e85 --- /dev/null +++ b/utils/md5Util/md5Util.go @@ -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)) +} diff --git a/utils/redisConnectUtil/redisConnectUtil.go b/utils/redisConnectUtil/redisConnectUtil.go new file mode 100644 index 0000000..c8b4775 --- /dev/null +++ b/utils/redisConnectUtil/redisConnectUtil.go @@ -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连接已安全关闭") + } + } +} diff --git a/utils/requestUtil/requestUtil.go b/utils/requestUtil/requestUtil.go new file mode 100644 index 0000000..e73e300 --- /dev/null +++ b/utils/requestUtil/requestUtil.go @@ -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 +} diff --git a/utils/tokenBucketUtil/tokenBucketUtil.go b/utils/tokenBucketUtil/tokenBucketUtil.go new file mode 100644 index 0000000..95c404c --- /dev/null +++ b/utils/tokenBucketUtil/tokenBucketUtil.go @@ -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) +} diff --git a/utils/tokenConsumerUtil/tokenConsumerUtil.go b/utils/tokenConsumerUtil/tokenConsumerUtil.go new file mode 100644 index 0000000..53d7922 --- /dev/null +++ b/utils/tokenConsumerUtil/tokenConsumerUtil.go @@ -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) +} diff --git a/咸鱼发布dll.md b/咸鱼发布dll.md new file mode 100644 index 0000000..000ac8c --- /dev/null +++ b/咸鱼发布dll.md @@ -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 +} +``` +