first commit

This commit is contained in:
97694731 2026-06-15 16:18:50 +08:00
commit 74fee5dbd9
31 changed files with 2996 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

13
.idea/getErpSendPublishing.iml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true">
<buildTags>
<option name="os" value="linux" />
</buildTags>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
<functions>
<function importPath="getErpSendPublishing/utils/dbConnectUtil" name="InitDB" />
</functions>
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/getErpSendPublishing.iml" filepath="$PROJECT_DIR$/.idea/getErpSendPublishing.iml" />
</modules>
</component>
</project>

BIN
Linux/getErpSendPubishing Normal file

Binary file not shown.

Binary file not shown.

57
cache/cache.go vendored Normal file
View File

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

23
config.ini Normal file
View File

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

646
controller/moveRepeat.go Normal file
View File

@ -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,
})
}

BIN
dll/csv.dll Normal file

Binary file not shown.

BIN
getErpSendPublishing.rar Normal file

Binary file not shown.

BIN
getErpSendPublishing_linux Normal file

Binary file not shown.

58
go.mod Normal file
View File

@ -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
)

143
go.sum Normal file
View File

@ -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=

116
main.go Normal file
View File

@ -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("服务已关闭")
}

BIN
parseFormDataTest.exe Normal file

Binary file not shown.

BIN
so/csv.so Normal file

Binary file not shown.

16
temp/mian.go Normal file
View File

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

71
test_redis_pool.go Normal file
View File

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

22
utils/ApiResponse.go Normal file
View File

@ -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 // 服务器内部错误(如数据库异常)
)

447
utils/PageQuery.go Normal file
View File

@ -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"`
}

View File

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

379
utils/iniConfigUtil.go Normal file
View File

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

24
utils/json_util.go Normal file
View File

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

47
utils/map_util.go Normal file
View File

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

View File

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

359
utils/redis_util.go Normal file
View File

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

174
utils/redis_util_two.go Normal file
View File

@ -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()
}

26
utils/response.go Normal file
View File

@ -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=200Message="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)
}

18
utils/string_util.go Normal file
View File

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

167
utils/writeToRedis.go Normal file
View File

@ -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是shopidfield是加密后的ISBNvalue是执行时间
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
}