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