commit 74fee5dbd9f2269406ea0f559feeb66f368b4f0c Author: 97694731 <97694731@qq.com> Date: Mon Jun 15 16:18:50 2026 +0800 first commit diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/getErpSendPublishing.iml b/.idea/getErpSendPublishing.iml new file mode 100644 index 0000000..2d93557 --- /dev/null +++ b/.idea/getErpSendPublishing.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..14bf957 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ac134ec --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Linux/getErpSendPubishing b/Linux/getErpSendPubishing new file mode 100644 index 0000000..6298b34 Binary files /dev/null and b/Linux/getErpSendPubishing differ diff --git a/Linux/getErpSendPubishing-0518 b/Linux/getErpSendPubishing-0518 new file mode 100644 index 0000000..32c4844 Binary files /dev/null and b/Linux/getErpSendPubishing-0518 differ diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..139109e --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,57 @@ +// cache.go +package cache + +import ( + "context" + "encoding/json" + "time" + + "github.com/go-redis/redis/v8" +) + +var redisClient *redis.Client + +func NewRedisClient() *redis.Client { + if redisClient == nil { + redisClient = redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // 无密码 + DB: 0, // 默认数据库 + }) + + // 测试连接 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := redisClient.Ping(ctx).Result() + if err != nil { + panic("Redis连接失败: " + err.Error()) + } + } + + return redisClient +} + +// SetSecondCacheObject 设置二级缓存 +func SetSecondCacheObject(ctx context.Context, key string, value interface{}, expiration time.Duration) error { + client := NewRedisClient() + + jsonData, err := json.Marshal(value) + if err != nil { + return err + } + + return client.Set(ctx, key, jsonData, expiration).Err() +} + +// GetSecondCacheObject 获取二级缓存 +func GetSecondCacheObject(ctx context.Context, key string, result interface{}) error { + client := NewRedisClient() + + data, err := client.Get(ctx, key).Result() + if err != nil { + return err + } + + return json.Unmarshal([]byte(data), result) +} diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..9c58be2 --- /dev/null +++ b/config.ini @@ -0,0 +1,23 @@ +[Db1] +User = zhishu +Password = XsRR4K3ATizyc5BK +Host = 146.56.227.42 +Port = 3306 +[http] +Addr = 127.0.0.1:51368 +[Task] +User = root +Password = Long6166@@ +Host = nj-cynosdbmysql-grp-1v6vxn5f.sql.tencentcdb.com +Port = 26247 + +[Db] +User = root +Password = Long6166@@ +Host = nj-cynosdbmysql-grp-1v6vxn5f.sql.tencentcdb.com +Port = 26247 + +[Data] +data_count=1000 +pool_size=30 +limit_size=10 diff --git a/controller/moveRepeat.go b/controller/moveRepeat.go new file mode 100644 index 0000000..8d25043 --- /dev/null +++ b/controller/moveRepeat.go @@ -0,0 +1,646 @@ +package controller + +import ( + "context" + "crypto/md5" + "fmt" + "getErpSendPublishing/utils" + "getErpSendPublishing/utils/dbConnectUtil" + "log" + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" +) + +// GoodsFormRequest 定义接收的表单数据结构 +type GoodsFormRequest struct { + ShopID int64 `form:"shopid" binding:"required,min=1"` // 店铺ID,必须大于0 + GoodsID int64 `form:"goodsid" binding:"required,min=1"` // 商品ID,必须大于0 + ISBN string `form:"isbn" binding:"required"` // ISBN,必须提供 +} + +// CenterBookBatchItem 批量提交的单个商品数据结构 +type CenterBookBatchItem struct { + ISBN string `json:"isbn"` + TotalPrice string `json:"totalPrice"` + ImgBigUrl string `json:"imgBigUrl"` +} + +// GoodsController 控制器结构 +type GoodsController struct { + // 可以在这里注入服务层依赖 +} + +// BatchProcessCenterBooks 批量处理中心图书数据 +func BatchProcessCenterBooks(ctx *gin.Context) { + // 1. 直接获取参数 + shopIDStr := ctx.PostForm("shopid") + + // 2. 空值检查 + if shopIDStr == "" { + log.Printf("[WARN] shopid参数为空 (路径: %s, IP: %s)", + ctx.FullPath(), ctx.ClientIP()) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "shopid不能为空", + "success": false, + }) + return + } + + // 3. 转换并验证shopid格式 + shopID, err := strconv.ParseInt(shopIDStr, 10, 64) + if err != nil || shopID <= 0 { + log.Printf("[WARN] 无效的shopid: %s (错误: %v)", shopIDStr, err) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "shopid必须为正整数", + "success": false, + }) + return + } + + // 4. 解析JSON请求体中的图书列表 + var bookItems []CenterBookBatchItem + if err := ctx.BindJSON(&bookItems); err != nil { + log.Printf("[WARN] JSON解析失败: %v (路径: %s, IP: %s)", + err, ctx.FullPath(), ctx.ClientIP()) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "JSON格式错误", + "success": false, + }) + return + } + + // 5. 检查图书列表是否为空 + if len(bookItems) == 0 { + log.Printf("[WARN] 图书列表为空 (shopid: %d)", shopID) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "图书列表不能为空", + "success": false, + }) + return + } + + // 6. 使用全局的Redis连接池(避免每次创建新连接) + redisClient := utils.RedisTwoForParseFormData + if redisClient == nil { + log.Printf("[ERROR] Redis连接未初始化") + ctx.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "系统内部错误", + "success": false, + }) + return + } + + // 7. 构建Redis key + redisKey := fmt.Sprintf("%d", shopID) + + // 8. 去重处理:遍历图书列表,移除重复的ISBN + var deduplicatedItems []CenterBookBatchItem + var skippedItems []CenterBookBatchItem + + for _, item := range bookItems { + // 检查ISBN是否为空 + if item.ISBN == "" { + log.Printf("[WARN] 发现空的ISBN,跳过处理 (shopid: %d)", shopID) + continue + } + + // 生成MD5加密的ISBN + hash := md5.Sum([]byte(item.ISBN)) + encryptedIsbn := fmt.Sprintf("%x", hash) + + // 检查是否存在重复数据 + exists, err := redisClient.HGet(redisKey, encryptedIsbn) + if err == nil && exists != "" { + // 存在重复数据,记录并跳过 + log.Printf("[INFO] 检测到重复数据: shopid=%d, isbn=%s", shopID, item.ISBN) + skippedItems = append(skippedItems, item) + continue + } + + // 非重复数据,添加到去重后列表 + deduplicatedItems = append(deduplicatedItems, item) + } + + // 9. 如果没有有效数据,返回错误 + if len(deduplicatedItems) == 0 { + log.Printf("[INFO] 所有数据均为重复数据,无有效处理项 (shopid: %d)", shopID) + ctx.JSON(http.StatusOK, gin.H{ + "code": 1, + "message": "所有数据均为重复数据", + "success": false, + "data": gin.H{ + "total": len(bookItems), + "skipped": len(skippedItems), + "processed": 0, + "original_items": bookItems, + "deduplicated_items": deduplicatedItems, + "skipped_items": skippedItems, + }, + }) + return + } + + // 10. 关键成功日志 + log.Printf("[INFO] 批量处理图书数据: shopid=%d, 总数=%d, 去重后=%d, 跳过=%d", + shopID, len(bookItems), len(deduplicatedItems), len(skippedItems)) + + // 11. 返回结果 + ctx.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "success": true, + "data": gin.H{ + "shopid": shopID, + "total": len(bookItems), + "deduplicated_count": len(deduplicatedItems), + "skipped_count": len(skippedItems), + "original_items": bookItems, + "deduplicated_items": deduplicatedItems, + "skipped_items": skippedItems, + }, + }) +} + +// validateGoodsRequest 业务层面的验证 +func (c *GoodsController) validateGoodsRequest(ctx context.Context, req *GoodsFormRequest) error { + // 基础验证 + if req.ShopID <= 0 { + return fmt.Errorf("店铺ID必须大于0") + } + + if req.GoodsID <= 0 { + return fmt.Errorf("商品ID必须大于0") + } + + if req.ISBN == "" { + return fmt.Errorf("ISBN不能为空") + } + + // 业务限制(根据实际情况调整) + if req.ShopID > 1000000000 { + return fmt.Errorf("店铺ID超出系统限制") + } + + if req.GoodsID > 1000000000 { + return fmt.Errorf("商品ID超出系统限制") + } + + // ISBN长度限制 + if len(req.ISBN) > 20 { + return fmt.Errorf("ISBN长度超出限制") + } + + return nil +} + +// ParseFormData 表单解析 +func ParseFormData(ctx *gin.Context) { + // 1. 直接获取参数 + shopIDStr := ctx.PostForm("shopid") + isbn := ctx.PostForm("isbn") + + // 2. 空值检查(移除了 goodsid) + if shopIDStr == "" || isbn == "" { + log.Printf("[WARN] 参数为空 (路径: %s, IP: %s)", + ctx.FullPath(), ctx.ClientIP()) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "shopid和isbn不能为空", + "success": false, + }) + return + } + + // 3. 转换并验证数字格式(只保留 shopid 验证) + shopID, err := strconv.ParseInt(shopIDStr, 10, 64) + if err != nil || shopID <= 0 { + log.Printf("[WARN] 无效的shopid: %s (错误: %v)", shopIDStr, err) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "shopid必须为正整数", + "success": false, + }) + return + } + + // 4. 使用全局的Redis连接池(避免每次创建新连接) + redisClient := utils.RedisTwoForParseFormData + if redisClient == nil { + log.Printf("[ERROR] Redis连接未初始化") + ctx.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "系统内部错误", + "success": false, + }) + return + } + + // 5. 生成MD5加密的ISBN + hash := md5.Sum([]byte(isbn)) + encryptedIsbn := fmt.Sprintf("%x", hash) + + //fmt.Println("encryptedIsbn:", encryptedIsbn) + + // 6. 构建Redis key + redisKey := fmt.Sprintf("%d", shopID) + + // 7. 检查是否存在重复数据 + exists, err := redisClient.HGet(redisKey, encryptedIsbn) + if err == nil && exists != "" { + // 存在重复数据 + log.Printf("[INFO] 检测到重复数据: shopid=%d, isbn=%s", shopID, isbn) + ctx.JSON(http.StatusOK, gin.H{ + "code": 1, + "message": "已有重复数据", + "success": false, + }) + return + } + + // 8. 关键成功日志 + log.Printf("[INFO] 表单数据解析成功: shopid=%d, isbn=%s", shopID, isbn) + + // 9. 返回结果 + ctx.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": gin.H{ + "shopid": shopID, + "isbn": isbn, + }, + "success": true, + }) +} + +// NewParseFormData 处理包含更多参数的表单数据 +func NewParseFormData(ctx *gin.Context) { + // 1. 直接获取参数 + shopIDStr := ctx.PostForm("shopId") + isbn := ctx.PostForm("isbn") + shopType := ctx.PostForm("shopType") + price := ctx.PostForm("price") + condition := ctx.PostForm("condition") + + // 2. 空值检查 + if shopIDStr == "" || isbn == "" { + log.Printf("[WARN] 参数为空 (路径: %s, IP: %s)", + ctx.FullPath(), ctx.ClientIP()) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "shopId和isbn不能为空", + "success": false, + }) + return + } + + // 3. 转换并验证数字格式 + shopID, err := strconv.ParseInt(shopIDStr, 10, 64) + if err != nil || shopID <= 0 { + log.Printf("[WARN] 无效的shopId: %s (错误: %v)", shopIDStr, err) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "shopId必须为正整数", + "success": false, + }) + return + } + + // 4. 使用全局的Redis连接池(避免每次创建新连接) + redisClient := utils.RedisTwoForParseFormData + if redisClient == nil { + log.Printf("[ERROR] Redis连接未初始化") + ctx.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "系统内部错误", + "success": false, + }) + return + } + + // 5. 构建Redis key + redisKey := fmt.Sprintf("%d", shopID) + + // 6. 根据shopType生成不同的加密key + var encryptedKey string + switch shopType { + case "1", "5": + // shopType为1或5时,使用ISBN的MD5 + hash := md5.Sum([]byte(isbn)) + encryptedKey = fmt.Sprintf("%x", hash) + case "2": + // shopType为2时,拼接isbn:price:condition后MD5 + combinedKey := fmt.Sprintf("%s:%s:%s", isbn, price, condition) + hash := md5.Sum([]byte(combinedKey)) + encryptedKey = fmt.Sprintf("%x", hash) + default: + // 其他情况默认使用ISBN的MD5 + hash := md5.Sum([]byte(isbn)) + encryptedKey = fmt.Sprintf("%x", hash) + } + + // 7. 检查是否存在重复数据 + exists, err := redisClient.HGet(redisKey, encryptedKey) + if err == nil && exists != "" { + // 存在重复数据 + log.Printf("[INFO] 检测到重复数据: shopId=%d, isbn=%s, shopType=%s", shopID, isbn, shopType) + ctx.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "success": true, + "data": gin.H{ + "deduplicated": false, + "isRepeat": true, + "shopId": shopID, + "isbn": isbn, + "shopType": shopType, + "price": price, + "condition": condition, + }, + }) + return + } + + // 8. 关键成功日志 + log.Printf("[INFO] 数据检查完成: shopId=%d, isbn=%s, shopType=%s, price=%s, condition=%s", + shopID, isbn, shopType, price, condition) + + // 9. 返回结果 + ctx.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "success": true, + "data": gin.H{ + "deduplicated": true, + "isRepeat": false, + "shopId": shopID, + "isbn": isbn, + "shopType": shopType, + "price": price, + "condition": condition, + }, + }) +} + +// MultipleStores 闲鱼多点店铺去重 +func MultipleStores(ctx *gin.Context) { + // 1. 直接获取参数 + shopIDsStr := ctx.PostForm("shopIds") + isbn := ctx.PostForm("isbn") + + // 2. 空值检查 + if shopIDsStr == "" || isbn == "" { + log.Printf("[WARN] 参数为空 (路径: %s, IP: %s)", + ctx.FullPath(), ctx.ClientIP()) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "shopIds和isbn不能为空", + "success": false, + }) + return + } + + // 3. 使用全局的Redis连接池(避免每次创建新连接) + redisClient := utils.RedisTwoForParseFormData + if redisClient == nil { + log.Printf("[ERROR] Redis连接未初始化") + ctx.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "系统内部错误", + "success": false, + }) + return + } + + // 4. 生成MD5加密的ISBN + hash := md5.Sum([]byte(isbn)) + encryptedIsbn := fmt.Sprintf("%x", hash) + + // 5. 解析shopIds数组(支持逗号分隔,兼容JSON数组格式如 [id1,id2,id3]) + // 去除可能的首尾方括号 + shopIDsStr = strings.TrimSpace(shopIDsStr) + shopIDsStr = strings.TrimPrefix(shopIDsStr, "[") + shopIDsStr = strings.TrimSuffix(shopIDsStr, "]") + + shopIDStrs := strings.Split(shopIDsStr, ",") + + // 6. 遍历每个shopId,逐个检查是否存在重复数据 + for _, idStr := range shopIDStrs { + idStr = strings.TrimSpace(idStr) + + // 跳过空字符串 + if idStr == "" { + continue + } + + // 转换并验证shopId格式 + shopID, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || shopID <= 0 { + log.Printf("[WARN] 无效的shopId: %s (错误: %v)", idStr, err) + continue + } + + // 构建Redis key + redisKey := fmt.Sprintf("%d", shopID) + + // 检查是否存在重复数据 + exists, err := redisClient.HGet(redisKey, encryptedIsbn) + if err == nil && exists != "" { + // 存在重复数据,立即返回不继续查询 + log.Printf("[INFO] 检测到重复数据: shopId=%d, isbn=%s", shopID, isbn) + ctx.JSON(http.StatusOK, gin.H{ + "code": 1, + "message": "已有重复数据", + "success": false, + "data": gin.H{ + "shopId": shopID, + "isbn": isbn, + }, + }) + return + } + } + + // 7. 关键成功日志 + log.Printf("[INFO] 表单数据解析成功: shopIds=%s, isbn=%s", shopIDsStr, isbn) + + // 8. 返回结果 + ctx.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": gin.H{ + "shopIds": shopIDsStr, + "isbn": isbn, + }, + "success": true, + }) +} + +func SingleShopMultipleStores(ctx *gin.Context) { + // 1. 直接获取参数 + shopIDStr := ctx.PostForm("shopId") + isbn := ctx.PostForm("isbn") + + // 2. 空值检查(移除了 goodsid) + if shopIDStr == "" || isbn == "" { + log.Printf("[WARN] 参数为空 (路径: %s, IP: %s)", + ctx.FullPath(), ctx.ClientIP()) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "shopid和isbn不能为空", + "success": false, + }) + return + } + + // 3. 转换并验证数字格式(只保留 shopid 验证) + shopID, err := strconv.ParseInt(shopIDStr, 10, 64) + if err != nil || shopID <= 0 { + log.Printf("[WARN] 无效的shopid: %s (错误: %v)", shopIDStr, err) + + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "shopid必须为正整数", + "success": false, + }) + return + } + + // 4. 使用全局的Redis连接池(避免每次创建新连接) + redisClient := utils.RedisTwoForParseFormData + if redisClient == nil { + log.Printf("[ERROR] Redis连接未初始化") + ctx.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "系统内部错误", + "success": false, + }) + return + } + + // 5. 使用全局MySQL连接池(单例,在main.go启动时初始化) + db := dbConnectUtil.DB + if db == nil { + log.Printf("[ERROR] 全局MySQL连接池未初始化") + ctx.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "系统内部错误", + "success": false, + }) + return + } + + // 6. 生成MD5加密的ISBN + hash := md5.Sum([]byte(isbn)) + encryptedIsbn := fmt.Sprintf("%x", hash) + + // 7. 查询当前店铺的mall_id + var mallID string + err = db.QueryRow(` + SELECT mall_id + FROM t_shop + WHERE shop_type LIKE '%5%' + AND del_flag LIKE '%0%' + AND id = ?`, shopID).Scan(&mallID) + if err != nil { + // mall_id未找到,不做去重处理,直接返回成功 + log.Printf("[INFO] 未找到店铺mall_id: shopid=%d, err=%v", shopID, err) + ctx.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": gin.H{ + "shopid": shopID, + "isbn": isbn, + }, + "success": true, + }) + return + } + + // 8. 根据mall_id查询所有关联店铺id + rows, err := db.Query(` + SELECT id FROM t_shop + WHERE shop_type LIKE '%5%' + AND del_flag LIKE '%0%' + AND mall_id = ?`, mallID) + if err != nil { + log.Printf("[ERROR] 查询关联店铺失败: mall_id=%s, err=%v", mallID, err) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "系统内部错误", + "success": false, + }) + return + } + defer rows.Close() + + // 9. 遍历所有关联店铺id,参考ParseFormData第5步之后的逻辑检查重复 + var relatedShopIDs []int64 + for rows.Next() { + var relatedShopID int64 + if err := rows.Scan(&relatedShopID); err != nil { + log.Printf("[WARN] 扫描关联店铺id失败: %v", err) + continue + } + relatedShopIDs = append(relatedShopIDs, relatedShopID) + } + + for _, relatedShopID := range relatedShopIDs { + // 构建Redis key + redisKey := fmt.Sprintf("%d", relatedShopID) + + // 检查是否存在重复数据 + exists, err := redisClient.HGet(redisKey, encryptedIsbn) + if err == nil && exists != "" { + // 存在重复数据,立即返回不继续查询 + log.Printf("[INFO] 检测到重复数据: shopId=%d, isbn=%s (关联mall_id=%s)", relatedShopID, isbn, mallID) + ctx.JSON(http.StatusOK, gin.H{ + "code": 1, + "message": "已有重复数据", + "success": false, + "data": gin.H{ + "shopId": relatedShopID, + "isbn": isbn, + "mallId": mallID, + "relatedShopCount": len(relatedShopIDs), + }, + }) + return + } + } + + // 10. 关键成功日志 + log.Printf("[INFO] 多点店铺去重检查完成: shopid=%d, isbn=%s, mall_id=%s, 关联店铺数=%d", + shopID, isbn, mallID, len(relatedShopIDs)) + + // 11. 返回结果 + ctx.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": gin.H{ + "shopid": shopID, + "isbn": isbn, + "mallId": mallID, + "relatedShopCount": len(relatedShopIDs), + "relatedShopIds": relatedShopIDs, + }, + "success": true, + }) +} diff --git a/dll/csv.dll b/dll/csv.dll new file mode 100644 index 0000000..0729f49 Binary files /dev/null and b/dll/csv.dll differ diff --git a/getErpSendPublishing.rar b/getErpSendPublishing.rar new file mode 100644 index 0000000..b519ae4 Binary files /dev/null and b/getErpSendPublishing.rar differ diff --git a/getErpSendPublishing_linux b/getErpSendPublishing_linux new file mode 100644 index 0000000..89cf052 Binary files /dev/null and b/getErpSendPublishing_linux differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ab3fe75 --- /dev/null +++ b/go.mod @@ -0,0 +1,58 @@ +module getErpSendPublishing + +go 1.25.5 + +require ( + github.com/gin-gonic/gin v1.11.0 + github.com/go-ini/ini v1.67.0 + github.com/go-redis/redis/v8 v8.11.5 + github.com/go-sql-driver/mysql v1.9.3 + github.com/shopspring/decimal v1.4.0 + github.com/tealeg/xlsx/v3 v3.3.13 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/frankban/quicktest v1.14.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/google/btree v1.0.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/peterbourgon/diskv/v3 v3.0.1 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/rogpeppe/fastuuid v1.2.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2ac2a49 --- /dev/null +++ b/go.sum @@ -0,0 +1,143 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +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/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= +github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug= +github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa h1:2cO3RojjYl3hVTbEvJVqrMaFmORhL6O06qdW42toftk= +github.com/shabbyrobe/xmlwriter v0.0.0-20200208144257-9fca06d00ffa/go.mod h1:Yjr3bdWaVWyME1kha7X0jsz3k2DgXNa1Pj3XGyUAbx8= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/tealeg/xlsx/v3 v3.3.13 h1:Zk1Stj11MGRnOYI1st6av/Z2lIXp/jFZomrSWSeJLmY= +github.com/tealeg/xlsx/v3 v3.3.13/go.mod h1:KV4FTFtvGy0TBlOivJLZu/YNZk6e0Qtk7eOSglWksuA= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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/main.go b/main.go new file mode 100644 index 0000000..4a9e638 --- /dev/null +++ b/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "github.com/goccy/go-yaml" + + handler "getErpSendPublishing/controller" + "getErpSendPublishing/utils" + "getErpSendPublishing/utils/dbConnectUtil" + + "github.com/gin-gonic/gin" +) + +var config Config + +type Config struct { + RedisDb struct { + Addr string `yaml:"addr"` + Password string `yaml:"password"` + db int `yaml:"db"` + } `yaml:"redis-db"` + + RedisDbTwo struct { + Addr string `yaml:"addr"` + Password string `yaml:"password"` + db int `yaml:"db"` + } `yaml:"redis-dbTwo"` +} + +// 加载配置文件 +func loadConfig() error { + configFile, err := os.ReadFile("config.yaml") + if err != nil { + return fmt.Errorf("读取配置文件失败: %v", err) + } + + if err := yaml.Unmarshal(configFile, &config); err != nil { + return fmt.Errorf("解析配置文件失败: %v", err) + } + + return nil +} + +func main() { + // 加载配置 + if err := loadConfig(); err != nil { + log.Fatalf("加载配置失败: %v", err) + } + + log.Printf("config.RedisDb%v", config.RedisDb) + // 初始化默认Redis(用于任务存储)- 本地Redis + utils.InitRedis(config.RedisDb.Addr, config.RedisDb.Password, config.RedisDb.db) + + // 初始化缓存Redis(用于商品图片缓存)- 远程Redis + utils.InitCacheRedis(config.RedisDbTwo.Addr, config.RedisDbTwo.Password, config.RedisDbTwo.db) + + //// 初始化第二个Redis连接(库7,用于Header数据查询) + if err := utils.InitRedisTwo(); err != nil { + log.Fatalf("RedisTwo初始化失败: %v", err) + } + //log.Println("RedisTwo初始化成功(连接到103.236.74.207:6379,库7)") + + // 初始化专门用于ParseFormData函数的Redis连接(库13,用于查重) + if err := utils.InitRedisTwoForParseFormData(); err != nil { + log.Fatalf("RedisTwoForParseFormData初始化失败: %v", err) + } + log.Println("RedisTwoForParseFormData初始化成功(连接到36.212.1.63:6379,库13)") + + // 初始化全局MySQL连接池(单例,被所有查重接口复用,防止大量连接同时创建) + if _, err := dbConnectUtil.InitDB("zhishu", "XsRR4K3ATizyc5BK", "146.56.227.42", 3306); err != nil { + log.Fatalf("全局MySQL连接初始化失败: %v", err) + } + log.Println("全局MySQL连接池初始化成功(zhishu@146.56.227.42:3306),最大连接数=20") + + // 初始化Gin + router := gin.Default() + + // 注册路由 + + //查重 + router.POST("/api/goods/simple", handler.ParseFormData) + + //批量查重 + router.POST("/api/goods/batchDeduplication", handler.BatchProcessCenterBooks) + + //新 - 单挑查重 + router.POST("/api/goods/newParseFormData", handler.NewParseFormData) + + //新 - 闲鱼多店铺去重 + router.POST("/api/goods/goofish/multipleStores", handler.MultipleStores) + + //新 - 闲鱼多店去重,本地确认多店铺 + router.POST("/api/goods/goofish/singleShopMultipleStores", handler.SingleShopMultipleStores) + + // 启动HTTP服务器 + go func() { + log.Println("HTTP服务器启动在 :8182") + if err := router.Run(":8182"); err != nil { + log.Fatal("HTTP服务器启动失败:", err) + } + }() + + // 等待中断信号 + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + <-sigChan + log.Println("接收到中断信号,正在关闭服务...") + + log.Println("服务已关闭") +} diff --git a/parseFormDataTest.exe b/parseFormDataTest.exe new file mode 100644 index 0000000..9b74c01 Binary files /dev/null and b/parseFormDataTest.exe differ diff --git a/so/csv.so b/so/csv.so new file mode 100644 index 0000000..3437458 Binary files /dev/null and b/so/csv.so differ diff --git a/temp/mian.go b/temp/mian.go new file mode 100644 index 0000000..569aa5a --- /dev/null +++ b/temp/mian.go @@ -0,0 +1,16 @@ +package main + +import ( + "crypto/md5" + "fmt" + "log" +) + +func main() { + isbn := "9787107333316" + + hash := md5.Sum([]byte(isbn)) + encryptedIsbn := fmt.Sprintf("%x", hash) + + log.Println(encryptedIsbn) +} diff --git a/test_redis_pool.go b/test_redis_pool.go new file mode 100644 index 0000000..da8d83e --- /dev/null +++ b/test_redis_pool.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + "getErpSendPublishing/utils" + "io" + "log" + "net/http" + "strings" + "sync" + "time" +) + +func main() { + // 初始化Redis连接池 + if err := utils.InitRedisTwoForParseFormData(); err != nil { + log.Fatalf("Redis初始化失败: %v", err) + } + log.Println("Redis连接池初始化成功") + + // 模拟高频请求 + const concurrentRequests = 100 + const totalRequests = 1000 + + var wg sync.WaitGroup + wg.Add(totalRequests) + + start := time.Now() + + for i := 0; i < totalRequests; i++ { + go func(i int) { + defer wg.Done() + + // 构建请求体 + reqBody := fmt.Sprintf("shopid=123&isbn=ISBN%d", i) + + // 发送请求 + resp, err := http.Post("http://localhost:8182/api/goods/simple", "application/x-www-form-urlencoded", strings.NewReader(reqBody)) + if err != nil { + log.Printf("请求失败: %v", err) + return + } + defer resp.Body.Close() + + // 读取响应 + _, err = io.ReadAll(resp.Body) + if err != nil { + log.Printf("读取响应失败: %v", err) + return + } + + // 每100个请求打印一次进度 + if (i+1)%100 == 0 { + log.Printf("已完成 %d 个请求", i+1) + } + }(i) + + // 控制并发数 + if (i+1)%concurrentRequests == 0 { + time.Sleep(100 * time.Millisecond) + } + } + + // 等待所有请求完成 + wg.Wait() + + // 计算执行时间 + duration := time.Since(start) + log.Printf("完成 %d 个请求,耗时: %v", totalRequests, duration) + log.Printf("平均每个请求耗时: %v", duration/time.Duration(totalRequests)) +} diff --git a/utils/ApiResponse.go b/utils/ApiResponse.go new file mode 100644 index 0000000..333f230 --- /dev/null +++ b/utils/ApiResponse.go @@ -0,0 +1,22 @@ +package utils + +import "time" + +// ApiResponse 标准 API 响应结构(核心) +type ApiResponse struct { + Code int `json:"code"` // 业务状态码(非 HTTP 状态码) + Message string `json:"message"` // 提示信息(友好、明确) + Data any `json:"data"` // 返回数据(任意类型,可为 nil) + Timestamp time.Time `json:"timestamp"` +} + +// (可选)预定义业务状态码常量(根据项目调整) +const ( + CodeSuccess = 200 // 成功 + CodeParamMissing = 40001 // 参数缺失 + CodeParamInvalid = 40002 // 参数无效(格式/类型错误) + CodeUnauthorized = 40101 // 未授权(Token 失效/无权限) + CodeForbidden = 40301 // 禁止访问(权限不足) + CodeNotFound = 40401 // 资源不存在 + CodeInternalError = 50001 // 服务器内部错误(如数据库异常) +) diff --git a/utils/PageQuery.go b/utils/PageQuery.go new file mode 100644 index 0000000..6f79604 --- /dev/null +++ b/utils/PageQuery.go @@ -0,0 +1,447 @@ +package utils + +import ( + "database/sql" + "errors" + "fmt" + "strings" + "unicode" +) + +// PageQuery 分页查询实体类 +type PageQuery struct { + PageSize *int `json:"pageSize" form:"pageSize"` // 分页大小 + PageNum *int `json:"pageNum" form:"pageNum"` // 当前页数 + OrderByColumn string `json:"orderByColumn" form:"orderByColumn"` // 排序列 + IsAsc string `json:"isAsc" form:"isAsc"` // 排序的方向desc或者asc + + // 常量定义 + DefaultPageNum int // 当前记录起始索引 默认值 + DefaultPageSize int // 每页显示记录数 默认值 + MaxPageSize int // 最大分页大小限制 + MinSize int // 最小分页大小 +} + +// NewPageQuery 创建新的PageQuery实例 +func NewPageQuery() *PageQuery { + return &PageQuery{ + DefaultPageNum: 1, + DefaultPageSize: 20, + MaxPageSize: 1000, + MinSize: 0, + } +} + +// WithParams 设置分页参数 +func (p *PageQuery) WithParams(pageNum, pageSize *int) *PageQuery { + p.PageNum = pageNum + p.PageSize = pageSize + return p +} + +// WithOrder 设置排序参数 +func (p *PageQuery) WithOrder(orderByColumn, isAsc string) *PageQuery { + p.OrderByColumn = orderByColumn + p.IsAsc = isAsc + return p +} + +// BuildSQL 构建分页查询的SQL片段 +// 返回: ORDER BY 子句, LIMIT OFFSET 子句, 参数值, 错误 +func (p *PageQuery) BuildSQL() (orderBy string, limitOffset string, args []interface{}, err error) { + // 设置分页参数 + pageNum := p.getPageNum() + pageSize := p.getPageSize() + + // 验证分页参数 + if pageNum <= 0 { + pageNum = p.DefaultPageNum + } + if pageSize <= 0 { + pageSize = p.DefaultPageSize + } + // 限制最大分页大小 + if pageSize > p.MaxPageSize { + return "", "", nil, errors.New(fmt.Sprintf("分页大小不能超过 %d", p.MaxPageSize)) + } + + // 计算偏移量 + offset := (pageNum - 1) * pageSize + + // 构建排序条件 + if p.OrderByColumn != "" && p.IsAsc != "" { + orderBy, err = p.buildOrder() + if err != nil { + return "", "", nil, err + } + if orderBy != "" { + orderBy = "ORDER BY " + orderBy + } + } + + // 构建分页条件 + if pageSize > 0 { + limitOffset = "LIMIT ? OFFSET ?" + args = []interface{}{pageSize, offset} + } + + return orderBy, limitOffset, args, nil +} + +// BuildCountSQL 构建统计总数的SQL +func (p *PageQuery) BuildCountSQL(baseSQL string) (string, []interface{}) { + // 将SELECT语句转换为COUNT语句 + lowerSQL := strings.ToLower(baseSQL) + + // 移除ORDER BY子句(对于COUNT不需要排序) + if idx := strings.Index(lowerSQL, "order by"); idx != -1 { + baseSQL = baseSQL[:idx] + } + + // 将SELECT ... FROM 替换为 COUNT(*) + selectIdx := strings.Index(lowerSQL, "select") + fromIdx := strings.Index(lowerSQL, "from") + + if selectIdx != -1 && fromIdx != -1 { + countSQL := fmt.Sprintf("SELECT COUNT(*) %s", baseSQL[fromIdx:]) + return countSQL, nil + } + + // 如果无法处理,返回原样 + return baseSQL, nil +} + +// BuildPage 构建分页对象(返回通用的分页结构) +func (p *PageQuery) BuildPage() (*PageParam, error) { + // 设置合理的默认值 + if p.DefaultPageNum <= 0 { + p.DefaultPageNum = 1 + } + if p.DefaultPageSize <= 0 { + p.DefaultPageSize = 20 + } + if p.MaxPageSize <= 0 { + p.MaxPageSize = 1000 // 设置默认最大值 + } + + pageNum := p.getPageNum() + pageSize := p.getPageSize() + + // 验证 + if pageNum <= 0 { + pageNum = p.DefaultPageNum + } + if pageSize <= 0 { + pageSize = p.DefaultPageSize + } + + // 添加最小分页大小限制 + if pageSize < 1 { + pageSize = p.DefaultPageSize + } + + if pageSize > p.MaxPageSize { + // 提供更友好的错误信息 + return nil, fmt.Errorf("分页大小 %d 超过最大限制 %d", pageSize, p.MaxPageSize) + } + + offset := (pageNum - 1) * pageSize + + return &PageParam{ + PageNum: pageNum, + PageSize: pageSize, + Offset: offset, + OrderBy: p.OrderByColumn, + IsAsc: p.IsAsc, + }, nil +} + +// GetFirstNum 获取起始记录索引 +func (p *PageQuery) GetFirstNum() int { + pageNum := p.getPageNum() + pageSize := p.getPageSize() + return (pageNum - 1) * pageSize +} + +// GetPageNum 获取页码 +func (p *PageQuery) GetPageNum() int { + return p.getPageNum() +} + +// GetPageSize 获取分页大小 +func (p *PageQuery) GetPageSize() int { + return p.getPageSize() +} + +// getPageNum 获取页码(内部方法) +func (p *PageQuery) getPageNum() int { + if p.PageNum == nil || *p.PageNum <= 0 { + return p.DefaultPageNum + } + return *p.PageNum +} + +// getPageSize 获取分页大小(内部方法) +func (p *PageQuery) getPageSize() int { + if p.PageSize == nil || *p.PageSize <= 0 { + return p.DefaultPageSize + } + return *p.PageSize +} + +// buildOrder 构建排序条件 +func (p *PageQuery) buildOrder() (string, error) { + orderBy := p.escapeOrderBySql(p.OrderByColumn) + orderBy = p.toUnderScoreCase(orderBy) + + // 兼容前端排序类型 + p.IsAsc = strings.ReplaceAll(p.IsAsc, "ascending", "asc") + p.IsAsc = strings.ReplaceAll(p.IsAsc, "descending", "desc") + + // 分割排序字段和排序方式 + orderByArr := strings.Split(orderBy, ",") + isAscArr := strings.Split(p.IsAsc, ",") + + // 验证参数 + if len(isAscArr) != 1 && len(isAscArr) != len(orderByArr) { + return "", errors.New("排序参数有误") + } + + // 构建排序SQL + var orderList []string + for i, orderByStr := range orderByArr { + var isAscStr string + if len(isAscArr) == 1 { + isAscStr = isAscArr[0] + } else { + isAscStr = isAscArr[i] + } + + // 验证排序方向 + if strings.ToLower(isAscStr) == "asc" { + orderList = append(orderList, orderByStr+" ASC") + } else if strings.ToLower(isAscStr) == "desc" { + orderList = append(orderList, orderByStr+" DESC") + } else { + return "", errors.New("排序参数有误") + } + } + + return strings.Join(orderList, ", "), nil +} + +// escapeOrderBySql 转义排序SQL防止SQL注入 +func (p *PageQuery) escapeOrderBySql(orderBy string) string { + // 移除危险的SQL关键字 + dangerousKeywords := []string{ + "select", "insert", "update", "delete", "drop", "truncate", + "union", "join", "or", "and", "--", "#", "/*", "*/", + ";", "'", "\"", "`", + } + + orderBy = strings.ToLower(orderBy) + for _, keyword := range dangerousKeywords { + orderBy = strings.ReplaceAll(orderBy, keyword, "") + } + + // 只允许字母、数字、下划线、点、逗号和空格 + var safeStr strings.Builder + for _, r := range orderBy { + if unicode.IsLetter(r) || unicode.IsDigit(r) || + r == '_' || r == '.' || r == ',' || r == ' ' { + safeStr.WriteRune(r) + } + } + + return strings.TrimSpace(safeStr.String()) +} + +// toUnderScoreCase 驼峰转下划线 +func (p *PageQuery) toUnderScoreCase(str string) string { + if str == "" { + return "" + } + + var result strings.Builder + runes := []rune(str) + + for i, r := range runes { + if unicode.IsUpper(r) { + if i > 0 && unicode.IsLower(runes[i-1]) { + result.WriteRune('_') + } + result.WriteRune(unicode.ToLower(r)) + } else { + result.WriteRune(r) + } + } + + return result.String() +} + +// PageParam 分页参数结构 +type PageParam struct { + PageNum int `json:"pageNum"` + PageSize int `json:"pageSize"` + Offset int `json:"offset"` + OrderBy string `json:"orderBy"` + IsAsc string `json:"isAsc"` +} + +// PageResult 分页结果结构 +type PageResult[T any] struct { + PageNum int `json:"pageNum"` + PageSize int `json:"pageSize"` + Total int64 `json:"total"` + TotalPage int `json:"totalPage"` + List []T `json:"list"` +} + +// BuildPageResult 构建分页结果 +func BuildPageResult[T any](list []T, total int64, pageNum, pageSize int) *PageResult[T] { + totalPage := 0 + if pageSize > 0 { + totalPage = int((total + int64(pageSize) - 1) / int64(pageSize)) + } + + return &PageResult[T]{ + PageNum: pageNum, + PageSize: pageSize, + Total: total, + TotalPage: totalPage, + List: list, + } +} + +// QueryPage 执行分页查询(通用方法) +func QueryPage[T any](db *sql.DB, baseQuery string, args []interface{}, pageQuery *PageQuery) (*PageResult[T], error) { + // 构建分页SQL + orderBy, limitOffset, pageArgs, err := pageQuery.BuildSQL() + if err != nil { + return nil, err + } + + // 构建完整查询SQL + finalSQL := baseQuery + if orderBy != "" { + finalSQL += " " + orderBy + } + if limitOffset != "" { + finalSQL += " " + limitOffset + } + + // 合并参数 + finalArgs := args + if pageArgs != nil { + finalArgs = append(finalArgs, pageArgs...) + } + + // 执行查询 + rows, err := db.Query(finalSQL, finalArgs...) + if err != nil { + return nil, err + } + defer rows.Close() + + // 解析结果集 + var list []T + for rows.Next() { + var item T + // 注意:这里需要根据实际结构使用rows.Scan + // 这里是一个通用占位,实际使用需要具体实现 + list = append(list, item) + } + + // 查询总数 + countSQL, countArgs := pageQuery.BuildCountSQL(baseQuery) + var total int64 + if countArgs == nil { + countArgs = args + } + err = db.QueryRow(countSQL, countArgs...).Scan(&total) + if err != nil { + return nil, err + } + + // 构建分页结果 + pageNum := pageQuery.GetPageNum() + pageSize := pageQuery.GetPageSize() + return BuildPageResult(list, total, pageNum, pageSize), nil +} + +// 使用示例 +func ExampleUsage() { + // 1. 创建PageQuery + pageQuery := NewPageQuery() + pageSize := 10 + pageNum := 1 + pageQuery.WithParams(&pageNum, &pageSize). + WithOrder("createTime,id", "desc,asc") + + // 2. 获取数据库连接 + var db *sql.DB // 假设已初始化 + + // 3. 构建基础查询SQL + baseSQL := "SELECT id, name, price, create_time FROM goods WHERE status = ?" + args := []interface{}{1} + + // 4. 获取分页SQL片段 + orderBy, limitOffset, pageArgs, err := pageQuery.BuildSQL() + if err != nil { + panic(err) + } + + // 5. 构建完整SQL + finalSQL := baseSQL + if orderBy != "" { + finalSQL += " " + orderBy + } + if limitOffset != "" { + finalSQL += " " + limitOffset + } + + // 6. 合并参数 + finalArgs := append(args, pageArgs...) + + // 7. 执行查询 + rows, err := db.Query(finalSQL, finalArgs...) + if err != nil { + panic(err) + } + defer rows.Close() + + // 8. 查询总数 + countSQL, countArgs := pageQuery.BuildCountSQL(baseSQL) + if countArgs == nil { + countArgs = args + } + + var total int64 + err = db.QueryRow(countSQL, countArgs...).Scan(&total) + if err != nil { + panic(err) + } + + // 9. 解析结果 + var goods []Goods + for rows.Next() { + var g Goods + err := rows.Scan(&g.ID, &g.Name, &g.Price, &g.CreateTime) + if err != nil { + panic(err) + } + goods = append(goods, g) + } + + // 10. 构建分页结果 + result := BuildPageResult(goods, total, pageNum, pageSize) + _ = result +} + +// Goods 示例结构体 +type Goods struct { + ID int64 `db:"id"` + Name string `db:"name"` + Price float64 `db:"price"` + CreateTime string `db:"create_time"` +} diff --git a/utils/dbConnectUtil/dbConnectUtil.go b/utils/dbConnectUtil/dbConnectUtil.go new file mode 100644 index 0000000..71b8a86 --- /dev/null +++ b/utils/dbConnectUtil/dbConnectUtil.go @@ -0,0 +1,74 @@ +package dbConnectUtil + +import ( + "database/sql" + "fmt" + "log" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +// DB 数据库连接池 +// 定义两个全局变量分别存储不同的数据库连接 +var ( + DB *sql.DB // 第一个数据库连接 + DBErp *sql.DB // 第二个数据库连接 + DBTask *sql.DB +) + +// InitDB 初始化数据库连接 +func InitDB(username, password, host string, port int) (*sql.DB, error) { + log.Printf("开始初始化数据库连接") + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/zhishu?charset=utf8mb4&parseTime=True&loc=Local", + username, password, host, port) + + log.Printf("数据库连接参数: %s", dsn) + log.Print("开始连接数据库") + + var err error + DB, err = sql.Open("mysql", dsn) + if err != nil { + return DB, fmt.Errorf("数据库连接失败: %v", err) + } + + // 测试数据库连接 + err = DB.Ping() + if err != nil { + return DB, fmt.Errorf("数据库连接测试失败: %v", err) + } + + // 设置连接池参数 + DB.SetMaxOpenConns(20) + DB.SetMaxIdleConns(10) + DB.SetConnMaxLifetime(time.Hour) + return DB, nil +} + +func InitDBTask(username, password, host string, port int) (*sql.DB, error) { + log.Printf("开始初始化数据库连接") + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/task?charset=utf8mb4&parseTime=True&loc=Local", + username, password, host, port) + + log.Printf("数据库连接参数: %s", dsn) + log.Print("开始连接数据库") + + var err error + DBTask, err = sql.Open("mysql", dsn) + if err != nil { + return DBTask, fmt.Errorf("数据库连接失败: %v", err) + } + + // 测试数据库连接 + err = DBTask.Ping() + if err != nil { + return DBTask, fmt.Errorf("数据库连接测试失败: %v", err) + } + + // 设置连接池参数 + DBTask.SetMaxOpenConns(20) + DBTask.SetMaxIdleConns(10) + DBTask.SetConnMaxLifetime(time.Hour) + log.Printf("Task数据库连接初始化成功") + return DBTask, nil +} diff --git a/utils/iniConfigUtil.go b/utils/iniConfigUtil.go new file mode 100644 index 0000000..4b7eb27 --- /dev/null +++ b/utils/iniConfigUtil.go @@ -0,0 +1,379 @@ +// Package utils 提供配置文件加载工具,支持从INI文件加载配置到结构体 +// 支持类型: int, string, bool, float, time.Duration 及其切片类型 +// 特性: +// - 支持默认值标签 `default` +// - 支持INI路径标签 `ini:"section.key"` +// - 支持时间单位转换标签 `multiplier` +// - 自动处理切片类型(逗号分隔) +package utils + +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:"server.port" default:"8080"` +// Timeout time.Duration `ini:"server.timeout" multiplier:"1s"` +// Features []string `ini:"server.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/json_util.go b/utils/json_util.go new file mode 100644 index 0000000..9fb0731 --- /dev/null +++ b/utils/json_util.go @@ -0,0 +1,24 @@ +package utils + +import ( + "encoding/json" +) + +// JsonUtil JSON工具类 +type JsonUtil struct{} + +var Json = &JsonUtil{} + +// TransferToJson 对象转JSON字符串 +func (j *JsonUtil) TransferToJson(obj interface{}) string { + data, err := json.Marshal(obj) + if err != nil { + return "" + } + return string(data) +} + +// JsonToObject JSON字符串转对象 +func (j *JsonUtil) JsonToObject(jsonStr string, obj interface{}) error { + return json.Unmarshal([]byte(jsonStr), obj) +} diff --git a/utils/map_util.go b/utils/map_util.go new file mode 100644 index 0000000..09e4da7 --- /dev/null +++ b/utils/map_util.go @@ -0,0 +1,47 @@ +package utils + +import ( + "encoding/json" + "strconv" +) + +// MapUtils Map工具类 +type MapUtils struct{} + +var Map = &MapUtils{} + +// ConvertToObject Map转对象 +func (m *MapUtils) ConvertToObject(data map[string]interface{}, obj interface{}) error { + jsonData, err := json.Marshal(data) + if err != nil { + return err + } + return json.Unmarshal(jsonData, obj) +} + +// GetString 从map获取字符串 +func (m *MapUtils) GetString(data map[string]interface{}, key string) string { + if val, ok := data[key]; ok && val != nil { + return val.(string) + } + return "" +} + +// GetInt64 从map获取int64 +func (m *MapUtils) GetInt64(data map[string]interface{}, key string) int64 { + if val, ok := data[key]; ok && val != nil { + switch v := val.(type) { + case string: + if i, err := strconv.ParseInt(v, 10, 64); err == nil { + return i + } + case float64: + return int64(v) + case int: + return int64(v) + case int64: + return v + } + } + return 0 +} diff --git a/utils/redisConnectUtil/redisConnectUtil.go b/utils/redisConnectUtil/redisConnectUtil.go new file mode 100644 index 0000000..6ab0e3c --- /dev/null +++ b/utils/redisConnectUtil/redisConnectUtil.go @@ -0,0 +1,90 @@ +package redisConnectUtil + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/go-redis/redis/v8" +) + +var ( + RedisClient *redis.Client + ctx = context.Background() +) + +// InitRedis 初始化Redis连接 +func InitRedis(host, password string, port, db int) error { + // 如果Redis客户端已经存在且连接正常,直接返回 + if RedisClient != nil { + _, err := RedisClient.Ping(ctx).Result() + if err == nil { + log.Println("Redis连接已存在且正常") + return nil + } + // 连接异常,重新初始化 + log.Println("Redis连接异常,重新初始化") + } + + RedisClient = redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%d", host, port), + Password: password, + DB: db, + }) + + log.Printf("开始连接Redis.... Addr: %s:%d, password:%s, db:%d", host, port, password, db) + // 测试连接 + _, err := RedisClient.Ping(ctx).Result() + if err != nil { + return fmt.Errorf("Redis连接失败: %v", err) + } + + log.Println("Redis连接成功") + return nil +} + +// CloseRedis 关闭Redis连接 +func CloseRedis() { + if RedisClient != nil { + RedisClient.Close() + log.Println("Redis连接已关闭") + } +} + +// AcquireLock 获取分布式锁 +func AcquireLock(lockKey string, timeout time.Duration) (bool, error) { + // 检查Redis客户端是否已初始化 + if RedisClient == nil { + // 初始化Redis连接 + err := InitRedis("36.212.20.113", "j8nZ4jra2E", 7963, 13) + if err != nil { + return false, fmt.Errorf("Redis连接初始化失败: %v", err) + } + } + + // 尝试获取锁,设置过期时间防止死锁 + result, err := RedisClient.SetNX(ctx, lockKey, "1", timeout).Result() + if err != nil { + return false, fmt.Errorf("获取Redis锁失败: %v", err) + } + return result, nil +} + +// ReleaseLock 释放分布式锁 +func ReleaseLock(lockKey string) error { + // 检查Redis客户端是否已初始化 + if RedisClient == nil { + // 初始化Redis连接 + err := InitRedis("36.212.20.113", "j8nZ4jra2E", 7963, 13) + if err != nil { + return fmt.Errorf("Redis连接初始化失败: %v", err) + } + } + + _, err := RedisClient.Del(ctx, lockKey).Result() + if err != nil { + return fmt.Errorf("释放Redis锁失败: %v", err) + } + return nil +} diff --git a/utils/redis_util.go b/utils/redis_util.go new file mode 100644 index 0000000..2191e7f --- /dev/null +++ b/utils/redis_util.go @@ -0,0 +1,359 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "sync" + "time" + + "github.com/go-redis/redis/v8" +) + +// RedisUtils Redis工具类 +type RedisUtils struct { + client *redis.Client + ctx context.Context + processes map[string]*os.Process // 存储进程句柄的映射 + processesLock sync.Mutex // 保护进程映射的锁 +} + +var Redis *RedisUtils // 默认Redis(用于任务存储) +var CacheRedis *RedisUtils // 缓存Redis(用于商品图片缓存) + +// InitRedis 初始化默认Redis(用于任务存储) +func InitRedis(addr, password string, db int) { + Redis = &RedisUtils{ + client: redis.NewClient(&redis.Options{ + Addr: addr, + Password: password, + DB: db, + }), + ctx: context.Background(), + processes: make(map[string]*os.Process), + } +} + +// InitCacheRedis 初始化缓存Redis(用于商品图片缓存) +func InitCacheRedis(addr, password string, db int) { + CacheRedis = &RedisUtils{ + client: redis.NewClient(&redis.Options{ + Addr: addr, + Password: password, + DB: db, + }), + ctx: context.Background(), + } +} + +// GetSecondCacheObject 获取缓存对象(秒级缓存) +func (r *RedisUtils) GetSecondCacheObject(key string) (string, error) { + return r.client.Get(r.ctx, key).Result() +} + +// SetSecondCacheObject 设置缓存对象(秒级缓存) +func (r *RedisUtils) SetSecondCacheObject(key, value string) error { + // 默认缓存24小时 + return r.client.Set(r.ctx, key, value, 24*time.Hour).Err() +} + +// DelCacheObject 删除缓存 +func (r *RedisUtils) DelCacheObject(key string) error { + return r.client.Del(r.ctx, key).Err() +} + +// 任务存储相关方法 + +// SaveTaskToRedis 保存任务到Redis +func (r *RedisUtils) SaveTaskToRedis(taskKey string, task interface{}) error { + taskJSON, err := json.Marshal(task) + if err != nil { + return fmt.Errorf("序列化任务失败: %v", err) + } + + // 使用Hash存储任务信息,设置24小时过期 + err = r.client.HSet(r.ctx, "tasks", taskKey, taskJSON).Err() + if err != nil { + return fmt.Errorf("保存任务到Redis失败: %v", err) + } + + // 同时将任务ID添加到待处理任务队列 + err = r.client.LPush(r.ctx, "pending_tasks", taskKey).Err() + if err != nil { + return fmt.Errorf("添加到待处理队列失败: %v", err) + } + + // 设置过期时间 + r.client.Expire(r.ctx, "tasks", 24*time.Hour) + r.client.Expire(r.ctx, "pending_tasks", 24*time.Hour) + + return nil +} + +// SaveRunningTaskToRedis 保存运行任务到Redis +func (r *RedisUtils) SaveRunningTaskToRedis(runningTask interface{}) error { + taskJSON, err := json.Marshal(runningTask) + if err != nil { + return fmt.Errorf("序列化运行任务失败: %v", err) + } + + // 生成唯一键 + taskKey := fmt.Sprintf("running_task_%d", time.Now().UnixNano()) + + // 保存到运行任务Hash中 + err = r.client.HSet(r.ctx, "running_tasks", taskKey, taskJSON).Err() + if err != nil { + return fmt.Errorf("保存运行任务到Redis失败: %v", err) + } + + // 同时将任务键添加到待执行队列 + err = r.client.LPush(r.ctx, "pending_running_tasks", taskKey).Err() + if err != nil { + return fmt.Errorf("添加到待执行运行任务队列失败: %v", err) + } + + // 设置过期时间 + r.client.Expire(r.ctx, "running_tasks", 24*time.Hour) + r.client.Expire(r.ctx, "pending_running_tasks", 24*time.Hour) + + return nil +} + +// BatchSaveRunningTasksToRedis 批量保存运行任务到Redis(使用List类型队列) +func (r *RedisUtils) BatchSaveRunningTasksToRedis(tasks []interface{}) error { + if len(tasks) == 0 { + return nil + } + + // 使用pipeline批量操作 + pipe := r.client.Pipeline() + + // 用于收集所有唯一的队列名 + queueNames := make(map[string]bool) + + for _, task := range tasks { + taskJSON, err := json.Marshal(task) + if err != nil { + continue + } + + // 默认队列名 + queueName := "pending_running_tasks" + + // 尝试从JSON中提取字段以生成唯一的队列名 + var taskMap map[string]interface{} + if err := json.Unmarshal(taskJSON, &taskMap); err == nil { + + // 先尝试从外层提取 data 字段 + dataStr, hasData := taskMap["data"].(string) + if hasData && dataStr != "" { + // 解析 data 字段中的 JSON + var dataMap map[string]interface{} + if err := json.Unmarshal([]byte(dataStr), &dataMap); err == nil { + + // 从 data 中提取字段 + shopID, hasShopID := dataMap["shopId"].(string) + taskID, hasTaskID := dataMap["taskId"].(string) + taskType, hasTaskType := dataMap["taskType"].(string) + + // 提取 csvFileNameTimestamp + timestamp := "" + if ts, hasTimestamp := dataMap["csvFileNameTimestamp"].(string); hasTimestamp { + timestamp = ts + } else { + // 生成时间戳:年月日时分秒 + timestamp = time.Now().Format("20060102150405") + } + + // 如果能从 data 中提取到所有必需字段,使用特定队列名 + if hasShopID && hasTaskID && hasTaskType { + // 队列名格式: shopId_taskId_年月日时分秒_tasktype_1 + queueName = fmt.Sprintf("%s_%s_%s_%s_1", + shopID, + taskID, + timestamp, + taskType) + } + } + } + } + // 收集队列名 + queueNames[queueName] = true + + // 将任务JSON推入List队列左侧(LPUSH) + pipe.LPush(r.ctx, queueName, taskJSON) + + // 设置队列过期时间为24小时 + pipe.Expire(r.ctx, queueName, 24*time.Hour) + } + + // 执行批量操作 + _, err := pipe.Exec(r.ctx) + if err != nil { + return err + } + + // 为每个队列调用SendPublishing程序处理任务 + for queueName := range queueNames { + go func(qName string) { + if _, err := r.callSendPublishing(qName); err != nil { + log.Printf("调用SendPublishing程序失败: %v, 队列: %s", err, qName) + } + }(queueName) + } + + return nil +} + +// CallSendPublishing 调用SendPublishing程序处理任务(公开方法) +// 返回进程句柄和错误 +func (r *RedisUtils) CallSendPublishing(qName string) (*os.Process, error) { + return r.callSendPublishing(qName) +} + +// callSendPublishing 调用SendPublishing程序处理任务(内部方法) +// 返回进程句柄和错误 +func (r *RedisUtils) callSendPublishing(qName string) (*os.Process, error) { + log.Printf("准备启动SendPublishing程序,队列: %s", qName) + + // 为每个队列名启动一个SendPublishing程序实例 + // 构建SendPublishing程序路径(相对于项目根目录的父目录) + programPath := "/www/wwwroot/GetErpSendPubishing/SendPublishing" + + // 构建命令行参数:传递redisKey标志 + args := []string{"-redisKey", qName} + + // 启动SendPublishing程序 + cmd := exec.Command(programPath, args...) + + // 设置输出和错误输出 + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // 执行命令(不等待完成,异步执行) + if err := cmd.Start(); err != nil { + log.Printf("启动SendPublishing程序失败: %v, 队列: %s", err, qName) + return nil, fmt.Errorf("启动程序失败: %w", err) + } + + // 获取进程句柄 + process := cmd.Process + + // 保存进程句柄到映射中 + r.processesLock.Lock() + r.processes[qName] = process + r.processesLock.Unlock() + + log.Printf("SendPublishing程序已启动,PID: %d, 队列: %s", process.Pid, qName) + + // 启动一个goroutine等待进程结束,自动清理映射 + go func() { + err := cmd.Wait() + r.processesLock.Lock() + delete(r.processes, qName) + r.processesLock.Unlock() + + if err != nil { + log.Printf("SendPublishing程序异常退出,PID: %d, 队列: %s, 错误: %v", process.Pid, qName, err) + } else { + log.Printf("SendPublishing程序正常退出,PID: %d, 队列: %s", process.Pid, qName) + } + }() + + return process, nil +} + +// GetProcess 根据队列名称获取进程句柄 +func (r *RedisUtils) GetProcess(qName string) *os.Process { + r.processesLock.Lock() + defer r.processesLock.Unlock() + return r.processes[qName] +} + +// GetTaskFromRedis 从Redis获取任务 +func (r *RedisUtils) GetTaskFromRedis(taskKey string) (string, error) { + return r.client.HGet(r.ctx, "tasks", taskKey).Result() +} + +// GetRunningTaskFromRedis 从Redis获取运行任务 +func (r *RedisUtils) GetRunningTaskFromRedis(taskKey string) (string, error) { + return r.client.HGet(r.ctx, "running_tasks", taskKey).Result() +} + +// GetPendingRunningTask 从待执行队列获取一个运行任务 +func (r *RedisUtils) GetPendingRunningTask() (string, error) { + return r.client.RPop(r.ctx, "pending_running_tasks").Result() +} + +// GetTaskCount 获取任务数量 +func (r *RedisUtils) GetTaskCount() (int64, error) { + return r.client.HLen(r.ctx, "tasks").Result() +} + +// GetRunningTaskCount 获取运行任务数量 +func (r *RedisUtils) GetRunningTaskCount() (int64, error) { + return r.client.HLen(r.ctx, "running_tasks").Result() +} + +// GetClient 获取底层Redis客户端 +func (r *RedisUtils) GetClient() *redis.Client { + return r.client +} + +// GetContext 获取上下文 +func (r *RedisUtils) GetContext() context.Context { + return r.ctx +} + +// Get Redis Get方法 +func (r *RedisUtils) Get(key string) *redis.StringCmd { + return r.client.Get(r.ctx, key) +} + +// Set Redis Set方法 +func (r *RedisUtils) Set(key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + return r.client.Set(r.ctx, key, value, expiration) +} + +// Keys Redis Keys方法 +func (r *RedisUtils) Keys(pattern string) *redis.StringSliceCmd { + return r.client.Keys(r.ctx, pattern) +} + +// LPush Redis LPush方法 +func (r *RedisUtils) LPush(key string, values ...interface{}) *redis.IntCmd { + return r.client.LPush(r.ctx, key, values...) +} + +// RPush Redis RPush方法 +func (r *RedisUtils) RPush(key string, values ...interface{}) *redis.IntCmd { + return r.client.RPush(r.ctx, key, values...) +} + +// LPop Redis LPop方法 +func (r *RedisUtils) LPop(key string) *redis.StringCmd { + return r.client.LPop(r.ctx, key) +} + +// BLPop Redis BLPop方法 +func (r *RedisUtils) BLPop(timeout time.Duration, keys ...string) *redis.StringSliceCmd { + return r.client.BLPop(r.ctx, timeout, keys...) +} + +// LLen Redis LLen方法 +func (r *RedisUtils) LLen(key string) *redis.IntCmd { + return r.client.LLen(r.ctx, key) +} + +// Expire Redis Expire方法 +func (r *RedisUtils) Expire(key string, expiration time.Duration) *redis.BoolCmd { + return r.client.Expire(r.ctx, key, expiration) +} + +// TTL Redis TTL方法 +func (r *RedisUtils) TTL(key string) *redis.DurationCmd { + return r.client.TTL(r.ctx, key) +} diff --git a/utils/redis_util_two.go b/utils/redis_util_two.go new file mode 100644 index 0000000..378dffb --- /dev/null +++ b/utils/redis_util_two.go @@ -0,0 +1,174 @@ +package utils + +import ( + "context" + "fmt" + "time" + + "github.com/go-redis/redis/v8" +) + +// RedisUtilsTwo Redis工具类 +type RedisUtilsTwo struct { + client *redis.Client + ctx context.Context +} + +var RedisTwo *RedisUtilsTwo // 全局Redis实例(库7) +var RedisTwoForParseFormData *RedisUtilsTwo // 全局Redis实例(专门用于ParseFormData函数,库13) + +// InitRedisTwo 初始化第二个Redis连接(库7) +func InitRedisTwo() error { + RedisTwo = &RedisUtilsTwo{ + client: redis.NewClient(&redis.Options{ + Addr: "36.212.20.113:7963", + Password: "j8nZ4jra2E", + DB: 7, + // 连接池配置 + PoolSize: 10, // 最大连接数 + MinIdleConns: 5, // 最小空闲连接数 + }), + ctx: context.Background(), + } + + // 测试连接 + _, err := RedisTwo.client.Ping(RedisTwo.ctx).Result() + if err != nil { + return fmt.Errorf("连接Redis失败: %v", err) + } + + return nil +} + +// InitRedisTwoForParseFormData 初始化专门用于ParseFormData函数的Redis连接(库13) +func InitRedisTwoForParseFormData() error { + RedisTwoForParseFormData = &RedisUtilsTwo{ + client: redis.NewClient(&redis.Options{ + Addr: "36.212.12.92:6379", // 为ParseFormData函数使用的Redis地址 + Password: "long6166@@", + DB: 13, + // 连接池配置 + PoolSize: 50, // 最大连接数,根据实际并发情况调整 + MinIdleConns: 10, // 最小空闲连接数 + MaxRetries: 3, // 最大重试次数 + DialTimeout: 5 * time.Second, // 拨号超时时间 + ReadTimeout: 3 * time.Second, // 读取超时时间 + WriteTimeout: 3 * time.Second, // 写入超时时间 + PoolTimeout: 4 * time.Second, // 连接池超时时间 + }), + ctx: context.Background(), + } + + // 测试连接 + _, err := RedisTwoForParseFormData.client.Ping(RedisTwoForParseFormData.ctx).Result() + if err != nil { + return fmt.Errorf("连接Redis失败: %v", err) + } + + return nil +} + +// NewRedisUtilsTwo 创建一个新的Redis连接实例 +func NewRedisUtilsTwo(db int) (*RedisUtilsTwo, error) { + redisUtil := &RedisUtilsTwo{ + client: redis.NewClient(&redis.Options{ + Addr: "36.212.20.113:7963", + Password: "j8nZ4jra2E", + DB: db, + }), + ctx: context.Background(), + } + + // 测试连接 + _, err := redisUtil.client.Ping(redisUtil.ctx).Result() + if err != nil { + return nil, fmt.Errorf("连接Redis失败: %v", err) + } + + return redisUtil, nil +} + +// NewRedisUtilsTwoForParseFormData 为ParseFormData函数创建一个新的Redis连接实例(使用不同的Redis地址) +func NewRedisUtilsTwoForParseFormData(db int) (*RedisUtilsTwo, error) { + redisUtil := &RedisUtilsTwo{ + client: redis.NewClient(&redis.Options{ + Addr: "36.212.12.92:6379", // 为ParseFormData函数使用的Redis地址 + Password: "long6166@@", + DB: db, + }), + ctx: context.Background(), + } + + // 测试连接 + _, err := redisUtil.client.Ping(redisUtil.ctx).Result() + if err != nil { + return nil, fmt.Errorf("连接Redis失败: %v", err) + } + + return redisUtil, nil +} + +// GetClient 获取底层Redis客户端 +func (r *RedisUtilsTwo) GetClient() *redis.Client { + return r.client +} + +// GetContext 获取上下文 +func (r *RedisUtilsTwo) GetContext() context.Context { + return r.ctx +} + +// HGet 从Redis Hash中获取字段值 +func (r *RedisUtilsTwo) HGet(key, field string) (string, error) { + return r.client.HGet(r.ctx, key, field).Result() +} + +// HGetAll 获取Hash中的所有字段和值 +func (r *RedisUtilsTwo) HGetAll(key string) (map[string]string, error) { + return r.client.HGetAll(r.ctx, key).Result() +} + +// HSet 设置Hash中的字段值 +func (r *RedisUtilsTwo) HSet(key, field string, value interface{}) error { + return r.client.HSet(r.ctx, key, field, value).Err() +} + +// Exists 检查key是否存在 +func (r *RedisUtilsTwo) Exists(key string) (int64, error) { + return r.client.Exists(r.ctx, key).Result() +} + +// Get 获取key的值 +func (r *RedisUtilsTwo) Get(key string) (string, error) { + return r.client.Get(r.ctx, key).Result() +} + +// Set 设置key的值 +func (r *RedisUtilsTwo) Set(key string, value interface{}, expiration time.Duration) error { + return r.client.Set(r.ctx, key, value, expiration).Err() +} + +// Expire 设置key的过期时间 +func (r *RedisUtilsTwo) Expire(key string, expiration time.Duration) error { + return r.client.Expire(r.ctx, key, expiration).Err() +} + +// HIncrBy 对Hash中的字段值进行增量操作 +func (r *RedisUtilsTwo) HIncrBy(key, field string, value int64) (int64, error) { + return r.client.HIncrBy(r.ctx, key, field, value).Result() +} + +// LIndex 通过索引获取List中的元素 +func (r *RedisUtilsTwo) LIndex(key string, index int64) (string, error) { + return r.client.LIndex(r.ctx, key, index).Result() +} + +// LRange 获取List中指定范围内的元素 +func (r *RedisUtilsTwo) LRange(key string, start, stop int64) ([]string, error) { + return r.client.LRange(r.ctx, key, start, stop).Result() +} + +// Close 关闭Redis连接 +func (r *RedisUtilsTwo) Close() error { + return r.client.Close() +} diff --git a/utils/response.go b/utils/response.go new file mode 100644 index 0000000..71426d7 --- /dev/null +++ b/utils/response.go @@ -0,0 +1,26 @@ +package utils + +import ( + "github.com/gin-gonic/gin" + "net/http" + "time" +) + +func JSON(c *gin.Context, httpStatus int, code int, message string, data interface{}) { + c.JSON(httpStatus, ApiResponse{ + Code: code, + Message: message, + Data: data, + Timestamp: time.Now(), // 自动填充当前时间 + }) +} + +// Success 成功响应(简化版,Code=200,Message="success") +func Success(c *gin.Context, data any) { + JSON(c, http.StatusOK, CodeSuccess, "success", data) +} + +// Error 失败响应(简化版,自动匹配 HTTP 状态码) +func Error(c *gin.Context, httpStatus int, code int, message string) { + JSON(c, httpStatus, code, message, nil) +} diff --git a/utils/string_util.go b/utils/string_util.go new file mode 100644 index 0000000..1d0178e --- /dev/null +++ b/utils/string_util.go @@ -0,0 +1,18 @@ +package utils + +import "strings" + +// StringUtils 字符串工具类 +type StringUtils struct{} + +var Str = &StringUtils{} + +// IsEmpty 判断字符串是否为空 +func (s *StringUtils) IsEmpty(str string) bool { + return strings.TrimSpace(str) == "" +} + +// IsNotEmpty 判断字符串是否非空 +func (s *StringUtils) IsNotEmpty(str string) bool { + return !s.IsEmpty(str) +} diff --git a/utils/writeToRedis.go b/utils/writeToRedis.go new file mode 100644 index 0000000..0e66d9c --- /dev/null +++ b/utils/writeToRedis.go @@ -0,0 +1,167 @@ +package utils + +import ( + "context" + "crypto/md5" + "database/sql" + "fmt" + "getErpSendPublishing/utils/redisConnectUtil" + "log" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +func WriteToRedis(shopID int64) { + // 在开头调用judgISPdd函数 + if !judgISPdd(shopID) { + log.Println("此店铺不是pdd店铺") + return + } + + tableName := "t_running_task_" + fmt.Sprintf("%d", shopID) + + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + "root", "Long6166@@", "nj-cynosdbmysql-grp-1v6vxn5f.sql.tencentcdb.com", 26247, "task") + + taskDb, err := sql.Open("mysql", dsn) + if err != nil { + log.Printf("Task数据库连接失败: %v", err) + return + } + defer taskDb.Close() + + // 查询success_data字段中的isbn + query := fmt.Sprintf(` + SELECT isbn + FROM %s + WHERE isbn IS NOT NULL + AND isbn != '' + AND shop_id = ? + ORDER BY id`, tableName) + // 建议:在表 %s 上创建索引:CREATE INDEX idx_shop_id_isbn ON %s (shop_id, isbn); + + rows, err := taskDb.Query(query, shopID) + if err != nil { + log.Printf("查询ISBN失败: %v", err) + return + } + defer rows.Close() + + // 存储ISBN列表 + var isbnList []string + for rows.Next() { + var isbn string + if err := rows.Scan(&isbn); err != nil { + log.Printf("扫描ISBN失败: %v", err) + continue + } + isbnList = append(isbnList, isbn) + } + + if err := rows.Err(); err != nil { + log.Printf("遍历行时发生错误: %v", err) + } + + // 如果judgISPdd返回true,继续执行Redis相关操作 + err = redisConnectUtil.InitRedis("36.212.20.113", "j8nZ4jra2E", 7963, 13) + if err != nil { + log.Printf("Redis连接失败: %v", err) + return + } + + // 创建Redis key,使用shopID + redisKey := fmt.Sprintf("%d", shopID) + + // 执行时间 + execTime := time.Now().Format("2006-01-02 15:04:05") + + // 并行处理MD5加密,提高性能 + type encryptedISBN struct { + value string + } + + encryptedIsbnChan := make(chan encryptedISBN, len(isbnList)) + + // 启动多个goroutine并行处理MD5加密 + for i := 0; i < 10; i++ { // 使用10个goroutine + go func(start int) { + for j := start; j < len(isbnList); j += 10 { + isbn := isbnList[j] + // 加密ISBN(使用MD5) + hash := md5.Sum([]byte(isbn)) + // 将MD5 hash转换为十六进制字符串 + encryptedIsbn := fmt.Sprintf("%x", hash) + encryptedIsbnChan <- encryptedISBN{value: encryptedIsbn} + } + }(i) + } + + // 收集加密结果 + encryptedIsbnList := make([]string, 0, len(isbnList)) + for i := 0; i < len(isbnList); i++ { + encryptedIsbn := <-encryptedIsbnChan + encryptedIsbnList = append(encryptedIsbnList, encryptedIsbn.value) + } + close(encryptedIsbnChan) + + // 使用Redis管道批量执行HSet操作,减少网络往返 + pipe := redisConnectUtil.RedisClient.Pipeline() + + // 循环加密后的ISBN列表,执行Redis写入操作 + for i := 0; i < len(isbnList); i++ { + encryptedIsbn := encryptedIsbnList[i] + // 以加密后的ISBN作为子key,存储执行时间 + // 这里使用哈希表存储,key是shopid,field是加密后的ISBN,value是执行时间 + pipe.HSet(context.Background(), redisKey, encryptedIsbn, execTime) + } + + // 执行管道中的所有命令 + _, err = pipe.Exec(context.Background()) + if err != nil { + log.Printf("批量存储执行时间失败: %v", err) + } + + log.Printf("成功处理店铺 %d,存储 %d 条数据到Redis", shopID, len(isbnList)) +} + +func judgISPdd(shopID int64) bool { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + "zhishu", "XsRR4K3ATizyc5BK", "146.56.227.42", 3306, "zhishu") + + db, err := sql.Open("mysql", dsn) + if err != nil { + log.Printf("42db连接失败: %v", err) + return false + } + defer db.Close() + + // 测试数据库连接 + err = db.Ping() + if err != nil { + log.Printf("42db连接测试失败: %v", err) + return false + } + + query := `SELECT + CASE + WHEN shop_type = '1' THEN true + ELSE false + END AS result + FROM t_shop + WHERE id = ? AND del_flag = '0';` + + var result bool + err = db.QueryRow(query, shopID).Scan(&result) + if err != nil { + if err == sql.ErrNoRows { + log.Printf("未找到对应的店铺记录,ID: %d", shopID) + } else { + log.Printf("查询失败: %v", err) + } + return false + } + + log.Printf("judgISPdd查询结果: %v", result) + return result +}