commit e3429f9badc5a2816572825c9451ec1e38ee2682 Author: Cai1Cai1 Date: Thu Jun 25 10:15:10 2026 +0800 init:xianyu_noisbn_publishGoods diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01c8f0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/logs/ +/planB/logs/ +/planA.exe +/taskDb.db +/planB/planB.exe +/.idea +/planB/.idea +/planB/img/ diff --git a/__debug_bin.exe b/__debug_bin.exe new file mode 100644 index 0000000..b72141d Binary files /dev/null and b/__debug_bin.exe differ diff --git a/__debug_bin.exe2740605166 b/__debug_bin.exe2740605166 new file mode 100644 index 0000000..94118f3 Binary files /dev/null and b/__debug_bin.exe2740605166 differ diff --git a/__debug_bin.exe630942224 b/__debug_bin.exe630942224 new file mode 100644 index 0000000..0835a47 Binary files /dev/null and b/__debug_bin.exe630942224 differ diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..e78f296 --- /dev/null +++ b/config.yaml @@ -0,0 +1,133 @@ +server: + port: "8080" #服务器端口 + f_port : "8284" #F程序端口 + filter: 1 #是否开启违禁词过滤器 0=关闭 1=开启 + replace_mark: "0" #标题违规词是否替换* 0 不替换 1 替换(替换会继续发布,不替换则不发布) + redis_exp: 192 #redis过期时间 192小时(8天) + read_db: "mysql" #读数据库 mysql sqlite + err_pause_time: 3000 #错误暂停时间(毫秒) + sign_key: "jRQdCh52Z55Kzh1hADaA2ZtdTKetj2PXk60Tz5Yc0iz9aD8Wafbk7CwAZ8cz69A9zb9caZ3k9dnR3Ys06J5nYFPrZ0xE9p6TY8DCD538ryiRjW81YTPmk41tCEnXizPh" #签名密钥 + data_day: 2 #数据保存时间(天) + is_c: true #是否启动 C程序 +speed: #限速器 + pdd_speed: 18 #拼多多 每秒多少个任务 + xianyu_speed: 5 #闲鱼 每秒多少个任务 + watermark: 15 #打水印速率的个数 +minio: #minio 图片空间 + url: "103.236.68.64:19000" #minio地址 + access_key_id: "minio" #minio keyId + secret_access_key: "bhkXyaD2WdAF7C6z" #minio key + bucket_name: "task-xianyu" #存储桶 + target_dir: "test" #目标目录 + use_ssl: false #是否使用 SSL +alive: + fluent: 50 #存活状态-流畅时间(毫秒) + slow: 200 #存活状态-缓慢时间(毫秒) +pool_config: + size: 500 #协程数量 + with_expiry_duration: 10 #过期时间 + with_pre_alloc: true #预分配 + with_max_blocking_tasks: 2000 #阻塞任务数 + with_nonblocking : true #非阻塞 +mysql_config: + db_name: "task_user" #数据库名称 + user: "root" #数据库用户名 + password: "root" #数据库密码 + host: "127.0.0.1" #数据库地址 + port: 3306 #数据库端口 + loglevel: "silent" #数据库日志级别 info=开发环境:输出所有日志(SQL、执行时间等) warn=预发布:输出警告+错误 error=生产环境:只输出错误日志 silent=生产环境:关闭所有日志 + max_retry_times: 3 #数据库重试次数 + base_retry_interval: 100 #数据库重试间隔(毫秒) + max_retry_interval: 3 #数据库最大重试间隔(秒) + max_open_conns: 100 #数据库最大打开连接数 + max_idle_conns: 20 #数据库最大空闲连接数 + conn_max_idle_time: 30 #数据库连接最大空闲时间(分钟) + conn_max_lifetime: 1 #数据库连接最大生命周期(小数) +psi_mysql_config: + db_name: "psi" #数据库名称 + user: "psi" #数据库用户名 + password: "6d7f5DK2G5PCasBp" #数据库密码 + host: "175.27.224.66" #数据库地址 + port: 3306 #数据库端口 + loglevel: "silent" #数据库日志级别 info=开发环境:输出所有日志(SQL、执行时间等) warn=预发布:输出警告+错误 error=生产环境:只输出错误日志 silent=生产环境:关闭所有日志 + max_retry_times: 3 #数据库重试次数 + base_retry_interval: 100 #数据库重试间隔(毫秒) + max_retry_interval: 3 #数据库最大重试间隔(秒) + max_open_conns: 100 #数据库最大打开连接数 + max_idle_conns: 20 #数据库最大空闲连接数 + conn_max_idle_time: 30 #数据库连接最大空闲时间(分钟) + conn_max_lifetime: 1 #数据库连接最大生命周期(小数) +redis_config: + - db_name: "任务池" + db: 0 + addr: "127.0.0.1:6379" + password: "123456" + - db_name: "书品库" + db: 7 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "店铺信息" + db: 8 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "出版社信息列表" + db: 3 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "省市区列表" + db: 4 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有图片的 isbn" + db: 5 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有书籍的 isbn" + db: 6 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "淘宝店铺" + db: 9 + addr: "36.212.12.247:6379" + password: "long6166@@" +pdd_config: + client_id: "203c5a7ba8bd4b8488d5e26f93052642" #拼多多clientId + client_secret: "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" #拼多多clientSecret +kfz_config: + app_id: 576 #孔夫子appid + app_secret: "256e10220c5b307f5172b1a49c11467a6cfa8038bbe2a7feccc42231852324f8" #孔夫子appsecret +http_url: + task_url: "http://127.0.0.1:8080" #A 程序接口地址 +file_url: + xian_yu_dll: "D:\\source\\planA\\planB\\modules\\xianYu" #闲鱼 DLL库路径 + pdd_dll: "D:\\source\\planA\\planB\\modules\\pdd" #拼多多 DLL库路径 + kfz_dll: "D:\\source\\planA\\planB\\modules\\kfz" #孔夫子 DLL库路径 + log_dll: "D:\\source\\planA\\planB\\modules\\logs" #日志 DLL库路径 + image_dll: "D:\\source\\planA\\planB\\modules\\image" #水印 DLL库路径 + b_file_name: "D:\\source\\planA\\planB\\planB.exe" #B 程序文件路径 + c_file_name: "D:\\source\\planA\\planC\\planC.exe" #C 程序文件路径 + d_file_name: "D:\\source\\planA\\planD\\planD.exe" #D 程序文件路径 + e_file_name: "D:\\source\\planA\\planE\\planE.exe" #E 程序文件路径 + f_file_name: "D:\\source\\planA\\planF\\planF.exe" #F 程序文件路径 + create_task_url: "https://api.buzhiyushu.cn/zhishu/baseInfo/addNewTask" #新增任务接口 + create_task_notice_url: "http://36.212.12.92:8055/task" #核价软件提交数据通知接口 + create_operation_task_notice_url: "http://36.212.12.92:8055/taskV2" #操作商品任务核价软件提交数据通知接口 + banned_word_substitution_url : "http://36.212.12.247:13001/task/getFilterSetNew" #违禁词替换接口 + xy_banned_word_substitution_url : "http://146.56.192.164:19095/health" #闲鱼违禁词替换接口 + pdd_token_url: "https://api.buzhiyushu.cn/huidiao/pdd/getToken" #获取系统规定拼多多 token + deduction_url: "https://api.buzhiyushu.cn/zhishu/userRecharge/apiBalancePayment" #扣费接口 + pdd_get_goods_url: "http://pdd.buzhiyushu.cn/api/pdd/auth/getShopGoodsList" #查询拼多多商品接口 + pdd_get_goods_detail_url: "http://192.168.101.127:8085/api/pdd/auth/newGetShopGoodsDetailList" #查询拼多多商品详情列表接口 + pdd_add_goods_url: "http://192.168.101.127:18099/task/putShopGoods" #添加拼多多商品接口 + pdd_get_sku_id: "http://192.168.101.127:18099/shopGoods/getShopGoods" #批量获取 skuId接口 + xianyu_add_goods_url: "http://119.45.237.193:14008/task/putShopGoods" #添加闲鱼商品接口 + kfz_add_goods_url: "http://119.45.237.193:14009/task/kfzverifyPricePublishGoods" #添加孔夫子商品接口 + del_task_url: "http://119.45.237.193:14008/shopGoods/delShopGoodsk" #删除任务通知接口 + backup_url: "C:\\file\\backup" #备份文件路径 + pdd_goods_details_url: "D:\\file\\pdd_goods_details" #保存拼多多详情路径 + update_token_url: "http://146.56.227.42:9099/api/updateToken" #更新拼多多 token 到redis + kfz_img_temp_url: "D:\\file\\kfzImg" #孔夫子图片临时路径 + kfz_img_http_url: "https://www0.kfzimg.com/" #孔夫子图片 http 路径 + get_pdd_goods_shopid_isbn_url : "http://192.168.101.127:18099/shopGoods/selectTrilateralIds" #获取拼多多商品 shopId 和 isbn + get_subscription_expiration_date_url : "http://119.45.237.193:9096/api/user/getKfzUserRecbusiness" #获取订阅到期时间 + pdd_img_temp_url: "D:\\file\\kfzImg" #拼多多图片临时路径 \ No newline at end of file diff --git a/controlState/lock/lock.go b/controlState/lock/lock.go new file mode 100644 index 0000000..a9b61dc --- /dev/null +++ b/controlState/lock/lock.go @@ -0,0 +1,36 @@ +package lock + +import "sync" + +// 用sync.Map替代原生map,天然支持并发安全 +var lock sync.Map + +// GetLock 获取锁(返回true表示已上锁,false表示未上锁) +func GetLock(key string) bool { + v, ok := lock.Load(key) + if !ok { + return false + } + // 断言为bool类型(确保存储的是布尔值) + locked, ok := v.(bool) + return ok && locked +} + +// SetLock 设置锁(原子操作) +func SetLock(key string) { + lock.Store(key, true) +} + +// DestroyLock 销毁锁(原子操作) +func DestroyLock(key string) { + lock.Delete(key) +} + +// TryLock 尝试加锁(核心:检查+设置原子化) +// 返回true表示加锁成功,false表示已被上锁 +func TryLock(key string) bool { + // LoadOrStore:如果key不存在则存储值,返回false;如果已存在则返回true + _, loaded := lock.LoadOrStore(key, true) + // loaded为true表示已上锁,返回false;loaded为false表示加锁成功,返回true + return !loaded +} diff --git a/controlState/serviceAlive/serviceAlive.go b/controlState/serviceAlive/serviceAlive.go new file mode 100644 index 0000000..56ce5db --- /dev/null +++ b/controlState/serviceAlive/serviceAlive.go @@ -0,0 +1,55 @@ +package serviceAlive + +// ServiceStatus 定义服务状态结构体 +type ServiceStatus struct { + Times int // 次数/状态值 + Msg string // 消息信息 +} + +var Service = map[string]ServiceStatus{ + "mysql": {Times: 0, Msg: ""}, + "redis": {Times: 0, Msg: ""}, + "sqlite": {Times: 0, Msg: ""}, + "pdd": {Times: 0, Msg: ""}, + "通知取出bodyOver接口": {Times: 0, Msg: ""}, + "违禁词替换接口": {Times: 0, Msg: ""}, + "闲鱼违禁词": {Times: 0, Msg: ""}, +} + +// SetServiceAlive 设置服务状态 +func SetServiceAlive(key string, times int) { + if status, ok := Service[key]; ok { + status.Times = times + Service[key] = status + } +} + +// SetServiceAliveWithMsg 设置服务状态(带消息) +func SetServiceAliveWithMsg(key string, times int, msg string) { + Service[key] = ServiceStatus{ + Times: times, + Msg: msg, + } +} + +// UpdateServiceTimes 只更新服务次数 +func UpdateServiceTimes(key string, times int) { + if status, ok := Service[key]; ok { + status.Times = times + Service[key] = status + } +} + +// UpdateServiceMsg 只更新服务消息 +func UpdateServiceMsg(key string, msg string) { + if status, ok := Service[key]; ok { + status.Msg = msg + Service[key] = status + } +} + +// GetServiceStatus 获取服务状态 +func GetServiceStatus(key string) (ServiceStatus, bool) { + status, ok := Service[key] + return status, ok +} diff --git a/controller/admin.go b/controller/admin.go new file mode 100644 index 0000000..3fcf4f5 --- /dev/null +++ b/controller/admin.go @@ -0,0 +1,33 @@ +package controller + +import ( + "net/http" + "planA/tool" + + "github.com/gorilla/mux" +) + +// DelRedisTask 删除redis中指定任务 +func DelRedisTask(httpMsg http.ResponseWriter, data *http.Request) { + // 从路径参数获取 id + vars := mux.Vars(data) + taskId := vars["id"] + // 验证 taskId + if taskId == "" { + errMsg := "任务 ID不能为空" + tool.Error(httpMsg, errMsg, http.StatusBadRequest) + return + } + // 删除任务 + tool.Success(httpMsg, taskId) +} + +// DelMysqlTask 删除mysql中指定任务 +func DelMysqlTask(httpMsg http.ResponseWriter, data *http.Request) { + tool.Success(httpMsg, "删除mysql中指定任务") +} + +// DelSqliteTask 删除sqlite中指定任务 +func DelSqliteTask(httpMsg http.ResponseWriter, data *http.Request) { + tool.Success(httpMsg, "删除sqlite中指定任务") +} diff --git a/controller/alive.go b/controller/alive.go new file mode 100644 index 0000000..1f0c3c3 --- /dev/null +++ b/controller/alive.go @@ -0,0 +1,37 @@ +package controller + +import ( + "net/http" + "planA/controlState/serviceAlive" + config2 "planA/initialization/config" + "planA/tool" +) + +// GetServiceAliveList 获取存活状态列表 +func GetServiceAliveList(httpMsg http.ResponseWriter, data *http.Request) { + + //获取存活状态列表 + aliveConfig, getAliveConfigErr := config2.GetAliveConfig() + if getAliveConfigErr != nil { + tool.Error(httpMsg, getAliveConfigErr.Error(), http.StatusInternalServerError) + return + } + var ret []map[string]interface{} + alive := serviceAlive.Service + for k, v := range alive { + status := 0 + // v.Times 是原来的计数值 + if v.Times > aliveConfig.Fluent && v.Times < aliveConfig.Slow { + status = 1 + } else if v.Times >= aliveConfig.Slow { + status = 2 + } + ret = append(ret, map[string]interface{}{ + "name": k, + "times": v.Times, // 修改为 v.Times + "msg": v.Msg, // 新增 msg 字段 + "status": status, + }) + } + tool.Success(httpMsg, ret) +} diff --git a/controller/body.go b/controller/body.go new file mode 100644 index 0000000..3716896 --- /dev/null +++ b/controller/body.go @@ -0,0 +1,77 @@ +package controller + +import ( + "encoding/json" + "net/http" + "planA/service" + "planA/tool" + _type "planA/type" + "planA/validator" +) + +// GetTbOneBodyWait 查询淘宝bodyWait +func GetTbOneBodyWait(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.GetBodyWaitOneValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + + // 查询bodyWait + bodyWait, getTbBodyWaitErr := service.GetOneBodyFromRight(dataVal.TaskId) + if getTbBodyWaitErr != nil { + tool.Error(httpMsg, getTbBodyWaitErr.Error(), http.StatusInternalServerError) + return + } + + // 将 bodyWait 转为结构体 + var bodyWaits _type.TaskBody + unmarshalErr := json.Unmarshal([]byte(bodyWait), &bodyWaits) + if unmarshalErr != nil { + tool.Error(httpMsg, unmarshalErr.Error(), http.StatusInternalServerError) + return + } + + // 返回结果 + tool.Success(httpMsg, bodyWaits) +} + +// InsertTbBodyOver 插入 bodyOver +func InsertTbBodyOver(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.InsertTbBodyOver(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + + //解析bodyover + var bodyOver _type.TaskBody + unmarshalErr := json.Unmarshal([]byte(dataVal.Data), &bodyOver) + if unmarshalErr != nil { + tool.Error(httpMsg, unmarshalErr.Error(), http.StatusInternalServerError) + return + } + + //// 更新任务尾 + //updateTaskFootersErr := service.UpdateTaskFooters(dataVal.TaskId, bodyOver.Detail.Status, 1) + //if updateTaskFootersErr != nil { + // tool.Error(httpMsg, updateTaskFootersErr.Error(), http.StatusInternalServerError) + // return + //} + //// 更新任务头 + //updateTaskHeadersErr := service.UpdateTaskHeaders(dataVal.TaskId, bodyOver.Detail.Status, 1) + //if updateTaskHeadersErr != nil { + // tool.Error(httpMsg, updateTaskHeadersErr.Error(), http.StatusInternalServerError) + // return + //} + + //插入bodyOver + insertErr := service.InsertOneBodyOver(dataVal.TaskId, dataVal.Data) + if insertErr != nil { + tool.Error(httpMsg, insertErr.Error(), http.StatusInternalServerError) + return + } + tool.Success(httpMsg, "") +} diff --git a/controller/delTask.go b/controller/delTask.go new file mode 100644 index 0000000..ca5329b --- /dev/null +++ b/controller/delTask.go @@ -0,0 +1,515 @@ +package controller + +import ( + "database/sql" + "encoding/json" + "errors" + "net/http" + planBType "planA/planB/type" + "planA/service" + serviceMysql "planA/service/mysql" + "planA/tool" + toolPdd "planA/tool/pdd" + _type "planA/type" + mysqlType "planA/type/mysql" + "planA/validator" + "strconv" + "time" +) + +// GetDelTaskByPage 分页查询 删除任务 +func GetDelTaskByPage(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.GetDelTaskValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + delTaskArr, total, getDelTaskByPageErr := serviceMysql.GetDelTaskByPage(page, size, "") + if getDelTaskByPageErr != nil { + return + } + + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": delTaskArr, + } + tool.Success(httpMsg, dataRet) + return +} + +// GetDelTaskByPageByUserId 分页查询 删除任务-用户 +func GetDelTaskByPageByUserId(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.GetDelTaskByUserIdValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + delTaskArr, total, getDelTaskByPageErr := serviceMysql.GetDelTaskByPage(page, size, dataVal.UserId) + if getDelTaskByPageErr != nil { + return + } + + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": delTaskArr, + } + tool.Success(httpMsg, dataRet) + return +} + +// GetDelTaskDetail 获取删除任务详情列表 +func GetDelTaskDetail(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.GetDelTaskDetailValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + delTaskArr, total, getDelTaskByPageErr := serviceMysql.GetDelTaskDetailByPage(page, size, dataVal.TaskId, 3) + if getDelTaskByPageErr != nil { + return + } + + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": delTaskArr, + } + tool.Success(httpMsg, dataRet) + +} + +// CreateTbDelTask 创建淘宝删除任务 +func CreateTbDelTask(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.CreateTbDelTaskValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + delNum := data.FormValue("del_num") + taskCount := 0 + if dataVal.TaskType == "1" || dataVal.TaskType == "2" { + if delNum == "" || delNum == "0" { + errMsg := "删除数量不能为空与0" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 将 dataVal.TaskCount 转为 int + var err error + taskCount, err = strconv.Atoi(delNum) + if err != nil { + errMsg := "任务数量转换失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + delTime := data.FormValue("del_time") + delTimes := time.Time{} + if dataVal.TaskType == "3" { + if delTime == "" || delTime == "0" { + errMsg := "删除时间不能为空" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 将 dataVal.DelTime 转为 time.Time + layout := "2006-01-02 15:04:05" // 常见格式 + var err error + delTimes, err = time.Parse(layout, delTime) + if err != nil { + errMsg := "时间转换失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + + // 将 dataVal.TaskType 转为 int + taskType, err := strconv.Atoi(dataVal.TaskType) + if err != nil { + errMsg := "任务类型转换失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 查询店铺数据 + shopDataStr, err := service.GetTaskShop(dataVal.ShopID) + if err != nil { + errMsg := "获取店铺数据失败: shopId " + dataVal.ShopID + " " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 解析 json数据 + shopData, err := toolPdd.ParseShopData(shopDataStr) + if err != nil { + errMsg := "解析店铺数据失败:" + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + if shopData.Shop == nil { + errMsg := "店铺数据为空" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + //请求创建任务接口并获取任务 id + taskId, err := CreateTaskRequest(dataVal.ShopID, dataVal.TaskType) + if err != nil { + errMsg := "店铺ID " + dataVal.ShopID + " 淘宝删除任务 请求创建任务接口失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + taskTypes := 0 + if taskType == 1 { + taskTypes = 5 + } else if taskType == 2 { + taskTypes = 10 + } else if taskType == 3 { + taskTypes = 11 + } + + var priceRange []_type.PriceRange + priceTemplateRangeStr := shopData.PriceTemplate.RangePrice + err = json.Unmarshal([]byte(priceTemplateRangeStr), &priceRange) + if err != nil { + errMsg := "解析价格模板失败:" + err.Error() + " 原始数据:" + shopData.PriceTemplate.RangePrice + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + createAt := time.Now().Unix() + taskData, createTaskDataErr := CreateTaskData(taskId, int64(taskTypes), createAt, shopData.Shop, priceRange, shopData.Spec, shopData.ShopDetail, shopData.ShopContext, strconv.Itoa(taskCount), 1, 1, "1") + if createTaskDataErr != nil { + errMsg := "创建任务数据失败: " + createTaskDataErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 将 taskData 转为json + taskDataJson, err := json.Marshal(taskData) + if err != nil { + errMsg := "任务数据转换失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + header := string(taskDataJson) + + createAts := time.Unix(createAt, 0) + status := 0 + taskCountOver := 0 + // 创建任务 + task := mysqlType.DelTask{ + UserID: &shopData.Shop.CreateBy, + ShopID: &dataVal.ShopID, + TaskID: &taskId, + ShopType: &dataVal.TaskType, + TaskType: &taskType, + ShopName: &shopData.Shop.ShopName, + TaskCount: &taskCount, + Header: &header, + CreateAt: &createAts, + StopAt: &delTimes, + Status: &status, + TaskCountOver: &taskCountOver, + } + createDelTaskErr := serviceMysql.CreateDelTask(task) + if createDelTaskErr != nil { + errMsg := "创建任务失败: " + createDelTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 创建详情表 + createTableIfNotExistsErr := serviceMysql.CreateTableIfNotExists(taskId) + if createTableIfNotExistsErr != nil { + errMsg := "创建详情表失败: " + createTableIfNotExistsErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + tool.Success(httpMsg, taskId) +} + +// CreateTbDelTaskDetails 插入淘宝删除任务数据 +func CreateTbDelTaskDetails(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, createTaskValidatorErr := validator.CreateTbDelTaskDetailsValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + if dataVal.Status == "" { + dataVal.Status = "0" + } + + //判断任务是否存在 + _, getTaskErr := serviceMysql.GetDelTaskByTaskId(dataVal.TaskID) + if getTaskErr != nil { + if errors.Is(getTaskErr, sql.ErrNoRows) || getTaskErr.Error() == "record not found" { + errMsg := "任务不存在: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + errMsg := "获取任务失败: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 创建详情表 + createTableIfNotExistsErr := serviceMysql.CreateTableIfNotExists(dataVal.TaskID) + if createTableIfNotExistsErr != nil { + errMsg := "创建详情表失败: " + createTableIfNotExistsErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 将 string 类型的 Status 转换为 int64 + var statusInt64 int64 + if dataVal.Status != "" { + statusInt64, _ = strconv.ParseInt(dataVal.Status, 10, 64) + } + + // 转换 GoodsId + var goodsIdInt64 int64 + if dataVal.GoodsId != "" { + if val, err := strconv.ParseInt(dataVal.GoodsId, 10, 64); err == nil { + goodsIdInt64 = val + } + } + + // 转换 TaskID + var taskIDInt64 int64 + if dataVal.TaskID != "" { + if val, err := strconv.ParseInt(dataVal.TaskID, 10, 64); err == nil { + taskIDInt64 = val + } + } + + // 获取当前时间 + createAtTime := time.Unix(time.Now().Unix(), 0) + createAtStr := time.Now().Format("2006-01-02") + + // 插入任务详情 + taskDetails := planBType.DelTaskDetail{ + TaskID: &dataVal.TaskID, + Isbn: &dataVal.Isbn, + BookName: &dataVal.BookName, + GoodsID: &goodsIdInt64, + Status: &statusInt64, + Err: &dataVal.Err, + DeleteAt: &createAtTime, + DeleteDate: &createAtStr, + CreateAt: &createAtTime, + } + insertTbDelTaskDetailsErr := serviceMysql.InsertDelTaskDetail(taskIDInt64, taskDetails) + if insertTbDelTaskDetailsErr != nil { + errMsg := "插入任务详情失败: " + insertTbDelTaskDetailsErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + tool.Success(httpMsg, "") +} + +// UpdateTbDelTaskDetailsStatus 修改指定淘宝删除任务详情状态 +func UpdateTbDelTaskDetailsStatus(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, createTaskValidatorErr := validator.UpdateTbDelTaskDetailsStatusValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + + // 判断任务是否存在 + _, getTaskErr := serviceMysql.GetDelTaskByTaskId(dataVal.TaskID) + if getTaskErr != nil { + if errors.Is(getTaskErr, sql.ErrNoRows) || getTaskErr.Error() == "record not found" { + errMsg := "任务不存在: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + errMsg := "获取任务失败: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 将 dataVal.Status 转为int + statusInt, err := strconv.Atoi(dataVal.Status) + if err != nil { + errMsg := "状态值格式错误" + tool.Error(httpMsg, errMsg, http.StatusBadRequest) + return + } + + updateTbDelTaskDetailsStatusErr := serviceMysql.UpdateDelTaskDetailStatus(dataVal.TaskID, dataVal.GoodsId, statusInt, dataVal.Err) + if updateTbDelTaskDetailsStatusErr != nil { + errMsg := "修改任务详情状态失败: " + updateTbDelTaskDetailsStatusErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + tool.Success(httpMsg, "") +} + +// UpdateTbDelTaskProgress 修改淘宝任务进度 +func UpdateTbDelTaskProgress(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, createTaskValidatorErr := validator.UpdateTbDelTaskProgressValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + + // 判断任务是否存在 + _, getTaskErr := serviceMysql.GetDelTaskByTaskId(dataVal.TaskID) + if getTaskErr != nil { + if errors.Is(getTaskErr, sql.ErrNoRows) || getTaskErr.Error() == "record not found" { + errMsg := "任务不存在: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + errMsg := "获取任务失败: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 将 dataVal.Num 转为 int + numInt, err := strconv.Atoi(dataVal.Num) + if err != nil { + errMsg := "进度值格式错误" + tool.Error(httpMsg, errMsg, http.StatusBadRequest) + return + } + + updateTbDelTaskProgressErr := serviceMysql.UpdateDelTaskProgress(dataVal.TaskID, numInt) + if updateTbDelTaskProgressErr != nil { + errMsg := "修改任务进度失败: " + updateTbDelTaskProgressErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + tool.Success(httpMsg, "") +} + +// UpdateTbDelTaskStatus 修改淘宝任务状态 +func UpdateTbDelTaskStatus(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, createTaskValidatorErr := validator.UpdateTbDelTaskStatusValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + + // 判断任务是否存在 + _, getTaskErr := serviceMysql.GetDelTaskByTaskId(dataVal.TaskID) + if getTaskErr != nil { + if errors.Is(getTaskErr, sql.ErrNoRows) || getTaskErr.Error() == "record not found" { + errMsg := "任务不存在: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + errMsg := "获取任务失败: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 将 dataVal.Status 转为 int + statusInt, err := strconv.Atoi(dataVal.Status) + if err != nil { + errMsg := "状态值格式错误" + tool.Error(httpMsg, errMsg, http.StatusBadRequest) + return + } + + updateTbDelTaskStatusErr := serviceMysql.UpdateDelTaskStatusByTaskId(dataVal.TaskID, statusInt) + if updateTbDelTaskStatusErr != nil { + errMsg := "修改任务状态失败: " + updateTbDelTaskStatusErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + tool.Success(httpMsg, "") + +} + +// GetTbDelTaskDetailsWait 获取指定任务待执行的任务详情 +func GetTbDelTaskDetailsWait(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.GetDelTaskDetailValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + delTaskArr, total, getDelTaskByPageErr := serviceMysql.GetDelTaskDetailByPage(page, size, dataVal.TaskId, 0) + if getDelTaskByPageErr != nil { + return + } + + dataArr := []map[string]interface{}{} + for _, v := range delTaskArr { + datas := map[string]interface{}{ + "task_id": v.TaskID, + "isbn": v.Isbn, + "book_name": v.BookName, + "goods_id": v.GoodsID, + "status": v.Status, + "create_at": v.CreateAt, + } + dataArr = append(dataArr, datas) + } + + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": dataArr, + } + tool.Success(httpMsg, dataRet) +} + +// GetTbDelTaskByTaskId 根据任务id 查询任务 +func GetTbDelTaskByTaskId(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.TaskIdValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + + // 判断任务是否存在 + task, getTaskErr := serviceMysql.GetDelTaskByTaskId(dataVal.TaskID) + if getTaskErr != nil { + if errors.Is(getTaskErr, sql.ErrNoRows) || getTaskErr.Error() == "record not found" { + errMsg := "任务不存在: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + errMsg := "获取任务失败: " + getTaskErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + datas := map[string]interface{}{ + "task_id": task.TaskID, + "shop_id": task.ShopID, + "task_type": task.TaskType, + "status": task.Status, + "task_count": task.TaskCount, + "task_count_over": task.TaskCountOver, + "stop_at": task.StopAt, + "create_at": task.CreateAt, + } + + tool.Success(httpMsg, datas) +} diff --git a/controller/export.go b/controller/export.go new file mode 100644 index 0000000..cb62f36 --- /dev/null +++ b/controller/export.go @@ -0,0 +1,510 @@ +package controller + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "planA/modules/logs" + "planA/rep" + "planA/service" + "planA/tool" + _type "planA/type" + "planA/validator" + + "github.com/go-redis/redis/v8" +) + +// GetExportTask 导出任务列表 +func GetExportTask(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, GetExportValidatorErr := validator.GetExportValidator(data) + if GetExportValidatorErr != nil { + tool.Error(httpMsg, GetExportValidatorErr.Error(), http.StatusInternalServerError) + return + } + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + + read := rep.CreateDbFactoryRead() + records, total, getTaskRecordsListErr := read.GetTaskExportList(page, size, "") + if getTaskRecordsListErr != nil { + errMsg := getTaskRecordsListErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + var dataTaskAll []map[string]interface{} + for _, v := range records { + complete, getExportFileProgressErr := service.GetExportFileProgress(v.TaskId) + if getExportFileProgressErr != nil { + errMsg := "获取任务进度失败: " + getExportFileProgressErr.Error() + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + continue + } + taskExportdata := map[string]interface{}{ + "task_id": v.TaskId, + "shop_name": v.ShopName, + "status": v.Status, + "total": v.Total, + "file_url": v.FileUrl, + "complete_at": v.CompleteAt.Time, + "create_at": v.CreateAt, + "complete": complete, + } + dataTaskAll = append(dataTaskAll, taskExportdata) + } + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": dataTaskAll, + } + tool.Success(httpMsg, dataRet) +} + +// GetExportTaskByUserId 导出任务列表-用户 +func GetExportTaskByUserId(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, GetExportByUserIdValidatorErr := validator.GetExportByUserIdValidator(data) + if GetExportByUserIdValidatorErr != nil { + tool.Error(httpMsg, GetExportByUserIdValidatorErr.Error(), http.StatusInternalServerError) + return + } + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + + read := rep.CreateDbFactoryRead() + records, total, getTaskRecordsListErr := read.GetTaskExportList(page, size, dataVal.UserID) + if getTaskRecordsListErr != nil { + errMsg := getTaskRecordsListErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + dataTaskAll := []map[string]interface{}{} + for _, v := range records { + complete, getExportFileProgressErr := service.GetExportFileProgress(v.TaskId) + if errors.Is(getExportFileProgressErr, redis.Nil) { + complete = int(v.Total) + } else if getExportFileProgressErr != nil { + errMsg := "获取任务进度失败: " + getExportFileProgressErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + taskExportdata := map[string]interface{}{ + "task_id": v.TaskId, + "shop_name": v.ShopName, + "status": v.Status, + "total": v.Total, + "file_url": v.FileUrl, + "complete_at": v.CompleteAt.Time, + "create_at": v.CreateAt, + "complete": complete, + } + dataTaskAll = append(dataTaskAll, taskExportdata) + } + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": dataTaskAll, + } + tool.Success(httpMsg, dataRet) +} + +// ExportTaskDetail 根据任务 id导出任务详情 +func ExportTaskDetail(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, GetExportDetailValidatorErr := validator.GetExportDetailValidator(data) + if GetExportDetailValidatorErr != nil { + tool.Error(httpMsg, GetExportDetailValidatorErr.Error(), http.StatusInternalServerError) + return + } + + read := rep.CreateDbFactoryRead() + + //查询是任务信息 + taskRecord, getTaskRecordsByTaskIDErr := read.GetTaskRecordsByTaskId(dataVal.TaskID) + if getTaskRecordsByTaskIDErr != nil { + errMsg := fmt.Sprintf("获取任务信息失败 %v", getTaskRecordsByTaskIDErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + if taskRecord.IsExport == 1 { + errMsg := "任务已导出过,请在下载中心查看" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + //获取任务详情总数 + total, GetBodyOverCount := service.GetBodyOverCount(dataVal.TaskID) + if GetBodyOverCount != nil { + errMsg := fmt.Sprintf("获取任务详情总数失败 %v", GetBodyOverCount) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + //查询导出任务是存在 + taskExport, getTaskExportByTaskIdErr := read.GetTaskExportByTaskId(dataVal.TaskID) + if getTaskExportByTaskIdErr != nil { + errMsg := fmt.Sprintf("获取任务信息失败 %v", getTaskExportByTaskIdErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + if taskExport.Id == 0 { + //创建一条导出任务 + var status int64 + var fileUrl string + mysqlCreateTaskExportErr := mysqlWrite.CreateTaskExport(_type.TaskExportDTO{ + UserId: taskRecord.UserId, + ShopId: taskRecord.ShopId, + TaskId: taskRecord.TaskId, + ShopName: taskRecord.ShopName, + FileUrl: fileUrl, + Status: status, + Total: total, + CompleteAt: sql.NullTime{}, + }) + if mysqlCreateTaskExportErr != nil { + errMsg := fmt.Sprintf("写入任务信息失败 %v", mysqlCreateTaskExportErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + sqLiteCreateTaskExportErr := sqliteWrite.CreateTaskExport(_type.TaskExportDTO{ + UserId: taskRecord.UserId, + ShopId: taskRecord.ShopId, + TaskId: taskRecord.TaskId, + ShopName: taskRecord.ShopName, + FileUrl: fileUrl, + Status: status, + Total: total, + CompleteAt: sql.NullTime{}, + }) + if sqLiteCreateTaskExportErr != nil { + errMsg := fmt.Sprintf("写入任务信息失败 %v", sqLiteCreateTaskExportErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } else { + newTotal := taskExport.Total + total + // 如果数据存在 清空完成时间 并且 修改任务总数量 + mysqlUpdateTaskExportErr := mysqlWrite.UpdateTaskExport(_type.TaskExportDTO{ + Id: taskExport.Id, + UserId: taskExport.UserId, + ShopId: taskExport.ShopId, + TaskId: taskExport.TaskId, + ShopName: taskExport.ShopName, + FileUrl: taskExport.FileUrl, + Status: taskExport.Status, + Total: newTotal, + CompleteAt: sql.NullTime{}, + }) + if mysqlUpdateTaskExportErr != nil { + errMsg := fmt.Sprintf("修改任务信息失败 %v", mysqlUpdateTaskExportErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + sqLiteUpdateTaskExportErr := sqliteWrite.UpdateTaskExport(_type.TaskExportDTO{ + Id: taskExport.Id, + UserId: taskExport.UserId, + ShopId: taskExport.ShopId, + TaskId: taskExport.TaskId, + ShopName: taskExport.ShopName, + FileUrl: taskExport.FileUrl, + Status: taskExport.Status, + Total: newTotal, + CompleteAt: sql.NullTime{}, + }) + if sqLiteUpdateTaskExportErr != nil { + errMsg := fmt.Sprintf("修改任务信息失败 %v", sqLiteUpdateTaskExportErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + //修改任务导出状态 + mysqlUpdateTaskRecordsErr := mysqlWrite.UpdateTaskRecords(_type.TaskRecordsDTO{ + Id: taskRecord.Id, + UserId: taskRecord.UserId, + ShopId: taskRecord.ShopId, + TaskId: taskRecord.TaskId, + ShopName: taskRecord.ShopName, + IsExport: 1, + TaskType: taskRecord.TaskType, + }) + if mysqlUpdateTaskRecordsErr != nil { + errMsg := fmt.Sprintf("修改任务导出状态失败 %v", mysqlUpdateTaskRecordsErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + sqLiteUpdateTaskRecordsErr := sqliteWrite.UpdateTaskRecords(_type.TaskRecordsDTO{ + Id: taskRecord.Id, + UserId: taskRecord.UserId, + ShopId: taskRecord.ShopId, + TaskId: taskRecord.TaskId, + ShopName: taskRecord.ShopName, + IsExport: 1, + TaskType: taskRecord.TaskType, + }) + if sqLiteUpdateTaskRecordsErr != nil { + errMsg := fmt.Sprintf("修改任务导出状态失败 %v", sqLiteUpdateTaskRecordsErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + go ExportCSV(dataVal.TaskID, total, taskRecord.TaskType) + tool.Success(httpMsg, "") +} + +// ExportTaskDetailByUserId 根据任务 id导出任务详情-用户 +func ExportTaskDetailByUserId(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, GetExportDetailValidatorErr := validator.GetExportDetailByUserIdValidator(data) + if GetExportDetailValidatorErr != nil { + tool.Error(httpMsg, GetExportDetailValidatorErr.Error(), http.StatusInternalServerError) + return + } + read := rep.CreateDbFactoryRead() + + //查询任务信息 + task, getTaskRecordsByTaskIdErr := read.GetTaskRecordsByTaskId(dataVal.TaskID) + if getTaskRecordsByTaskIdErr != nil { + errMsg := fmt.Sprintf("获取任务信息失败 %v", getTaskRecordsByTaskIdErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 验证用户 + if dataVal.UserID != fmt.Sprintf("%v", task.UserId) { + errMsg := "用户验证失败" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + if task.IsExport == 1 { + errMsg := "任务已导出过,请在下载中心查看" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + //获取任务详情总数 + total, GetBodyOverCount := service.GetBodyOverCount(dataVal.TaskID) + if GetBodyOverCount != nil { + errMsg := fmt.Sprintf("获取任务详情总数失败 %v", GetBodyOverCount) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + //查询导出任务是存在 + taskExport, getTaskExportByTaskIdErr := read.GetTaskExportByTaskId(dataVal.TaskID) + if getTaskExportByTaskIdErr != nil { + errMsg := fmt.Sprintf("获取任务信息失败 %v", getTaskExportByTaskIdErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + if taskExport.Id == 0 { + //向导出任务表写入一条数据 + mysqlCreateTaskExportErr := mysqlWrite.CreateTaskExport(_type.TaskExportDTO{ + UserId: task.UserId, + ShopId: task.ShopId, + TaskId: dataVal.TaskID, + ShopName: task.ShopName, + FileUrl: "", + Status: 0, + Total: total, + CompleteAt: sql.NullTime{}, + }) + if mysqlCreateTaskExportErr != nil { + errMsg := fmt.Sprintf("写入任务信息失败 %v", mysqlCreateTaskExportErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + sqLiteCreateTaskExport := sqliteWrite.CreateTaskExport(_type.TaskExportDTO{ + UserId: task.UserId, + ShopId: task.ShopId, + TaskId: dataVal.TaskID, + ShopName: task.ShopName, + FileUrl: "", + Status: 0, + Total: total, + CompleteAt: sql.NullTime{}, + }) + if sqLiteCreateTaskExport != nil { + errMsg := fmt.Sprintf("写入任务信息失败 %v", sqLiteCreateTaskExport) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } else { + // 如果数据存在 清空完成时间 并且 修改任务总数量 + mysqlUpdateTaskExportErr := mysqlWrite.UpdateTaskExport(_type.TaskExportDTO{ + Id: taskExport.Id, + UserId: taskExport.UserId, + ShopId: taskExport.ShopId, + TaskId: taskExport.TaskId, + ShopName: taskExport.ShopName, + FileUrl: taskExport.FileUrl, + Status: taskExport.Status, + Total: taskExport.Total + total, + CompleteAt: sql.NullTime{}, + }) + if mysqlUpdateTaskExportErr != nil { + errMsg := fmt.Sprintf("修改任务信息失败 %v", mysqlUpdateTaskExportErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + sqLiteUpdateTaskExportErr := sqliteWrite.UpdateTaskExport(_type.TaskExportDTO{ + Id: taskExport.Id, + UserId: taskExport.UserId, + ShopId: taskExport.ShopId, + TaskId: taskExport.TaskId, + ShopName: taskExport.ShopName, + FileUrl: taskExport.FileUrl, + Status: taskExport.Status, + Total: taskExport.Total + total, + CompleteAt: sql.NullTime{}, + }) + if sqLiteUpdateTaskExportErr != nil { + errMsg := fmt.Sprintf("修改任务信息失败 %v", sqLiteUpdateTaskExportErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + + //修改任务导出状态 + mysqlUpdateTaskExportStatusErr := mysqlWrite.UpdateTaskExportStatus(dataVal.TaskID, 1, "") + if mysqlUpdateTaskExportStatusErr != nil { + errMsg := fmt.Sprintf("修改任务导出状态失败 %v", mysqlUpdateTaskExportStatusErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + sqLiteUpdateTaskExportStatusErr := sqliteWrite.UpdateTaskExportStatus(dataVal.TaskID, 1, "") + if sqLiteUpdateTaskExportStatusErr != nil { + errMsg := fmt.Sprintf("修改任务导出状态失败 %v", sqLiteUpdateTaskExportStatusErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + go ExportCSV(dataVal.TaskID, total, task.TaskType) + tool.Success(httpMsg, "") +} + +// ExportCSV 导出CSV +// taskId 任务id +// total 总数 +// taskType 任务类型 +// ExportCSV 导出CSV +// taskId 任务id +// total 总数 +func ExportCSV(taskId string, total int64, taskType int64) { + // 定义每次获取的数量 + batchSize := 1000 + csvFileName := fmt.Sprintf("%v.csv", taskId) + + // 定义导出目录 + exportDir := "file/export" + // 检查并创建目录(如果不存在) + err := os.MkdirAll(exportDir, 0755) + if err != nil { + errMsg := fmt.Sprintf("创建目录失败: %v", err) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + + // 拼接完整的文件路径 + fullPath := filepath.Join(exportDir, csvFileName) + + // 检查文件是否已存在 + fileExists := false + if _, err := os.Stat(fullPath); err == nil { + fileExists = true + fmt.Printf("文件已存在: %s,将在末尾追加数据\n", fullPath) + } else if !os.IsNotExist(err) { + // 其他错误(如权限问题) + errMsg := fmt.Sprintf("检查文件状态失败: %v", err) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + + // 初始化偏移量 + page := 1 + // 标记是否是第一次写入(用于写入CSV表头) + // 如果文件已存在,则不需要写入表头 + isFirstWrite := !fileExists + + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + + // 更新任务导出状态-导出中 + mysqlUpdateTaskExportStatusErr := mysqlWrite.UpdateTaskExportStatus(taskId, 1, "") + if mysqlUpdateTaskExportStatusErr != nil { + errMsg := fmt.Sprintf("更新任务导出状态失败: %v", mysqlUpdateTaskExportStatusErr) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + } + sqLiteUpdateTaskExportStatusErr := sqliteWrite.UpdateTaskExportStatus(taskId, 1, "") + if sqLiteUpdateTaskExportStatusErr != nil { + errMsg := fmt.Sprintf("更新任务导出状态失败: %v", sqLiteUpdateTaskExportStatusErr) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + } + + // 循环获取并写入数据 + for { + // 每次获取 batchSize条数据 + dataBatch, _, err := service.GetBodyOverDataByBatch(taskId, page, batchSize) + if err != nil { + errMsg := fmt.Sprintf("获取任务详情批次数据失败 page:%d, err:%v", page, err) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + fmt.Printf("数据长度: %v", len(dataBatch)) + // 没有数据了,退出循环 + if len(dataBatch) == 0 { + // 导出完成 + mysqlUpdateTaskExportStatusErr = mysqlWrite.UpdateTaskExportStatus(taskId, 2, fullPath) + if mysqlUpdateTaskExportStatusErr != nil { + errMsg := fmt.Sprintf("更新任务导出状态失败: %v", mysqlUpdateTaskExportStatusErr) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + } + sqLiteUpdateTaskExportStatusErr = sqliteWrite.UpdateTaskExportStatus(taskId, 2, fullPath) + if sqLiteUpdateTaskExportStatusErr != nil { + errMsg := fmt.Sprintf("更新任务导出状态失败: %v", sqLiteUpdateTaskExportStatusErr) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + } + + // 清空body_over + clearBodyOverErr := service.ClearBodyOver(taskId) + if clearBodyOverErr != nil { + errMsg := fmt.Sprintf("清空body_over失败: %v", clearBodyOverErr) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + break + } + + // 追加写入CSV文件 + // 注意:AppendToCSV函数需要修改以支持文件存在时的追加模式 + if writeErr := AppendToCSV(fullPath, dataBatch, isFirstWrite, taskId, taskType); writeErr != nil { + errMsg := fmt.Sprintf("写入CSV文件失败 page:%d, err:%v", page, writeErr) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + + // 第一次写入后标记为false(后续不再写表头) + if isFirstWrite { + isFirstWrite = false + } + + // 更新偏移量 + page++ + } +} diff --git a/controller/shop.go b/controller/shop.go new file mode 100644 index 0000000..f81c663 --- /dev/null +++ b/controller/shop.go @@ -0,0 +1,36 @@ +package controller + +import ( + "net/http" + "planA/service" + "planA/tool" + toolPdd "planA/tool/pdd" + "planA/validator" +) + +// GetShopInfo 查询店铺信息 +func GetShopInfo(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.GetShopInfoValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + + // 查询店铺数据 + shopDataStr, err := service.GetTaskShop(dataVal.ShopId) + if err != nil { + errMsg := "获取店铺数据失败: shopId " + dataVal.ShopId + " " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 解析 json数据 + shopData, err := toolPdd.ParseShopData(shopDataStr) + if err != nil { + errMsg := "解析店铺数据失败:" + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + tool.Success(httpMsg, shopData) + +} diff --git a/controller/task.go b/controller/task.go new file mode 100644 index 0000000..737ae0b --- /dev/null +++ b/controller/task.go @@ -0,0 +1,1752 @@ +package controller + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "errors" + "planA/initialization/golabl" + "planA/rep" + serviceMysql "planA/service/mysql" + "planA/tool/process" + "planA/type/mysql" + "planA/validator" + + "fmt" + "io" + "net/http" + "os" + "planA/controlState/lock" + "planA/initialization/config" + "planA/modules/logs" + "planA/modules/pdd" + "planA/service" + "planA/tool" + toolPdd "planA/tool/pdd" + _type "planA/type" + "reflect" + "strconv" + "strings" + "sync/atomic" + "time" + + psiMysqlService "planA/service/psiMysql" + + psiMysqlType "planA/type/psiMysql" + + _redis "github.com/go-redis/redis/v8" +) + +// CreateTask 创建任务 +func CreateTask(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.CreateTaskValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + //将 imgTypeStr 转为 int64 + imgType, err := strconv.ParseInt(dataVal.ImgType, 10, 64) + if err != nil { + errMsg := "图片类型转换失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //将 taskTypeStr 转为 int64 + taskType, err := strconv.ParseInt(dataVal.TaskType, 10, 64) + if err != nil { + errMsg := "任务类型转换失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //将 updateTypeStr 转为 int64 + updateType := int64(0) + if dataVal.UpdateType != "" { + updateType, err = strconv.ParseInt(dataVal.UpdateType, 10, 64) + if err != nil { + errMsg := "更新方式转换失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + // 查询店铺数据 + shopDataStr, err := service.GetTaskShop(dataVal.ShopID) + if err != nil { + errMsg := "获取店铺数据失败: shopId " + dataVal.ShopID + " " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 解析 json数据 + shopData, err := toolPdd.ParseShopData(shopDataStr) + if err != nil { + errMsg := "解析店铺数据失败:" + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 定义变量 + var spec *_type.Spec + var context *_type.ShopContext + var priceRange []_type.PriceRange + // 实例化 + spec = &_type.Spec{} // 指向零值结构体的指针 + context = &_type.ShopContext{} // 指向零值结构体的指针 + priceRange = []_type.PriceRange{} // 空切片 + + if shopData.Shop == nil { + errMsg := "店铺数据为空" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + shop := shopData.Shop + + // 不是淘宝店铺 + if shop.ShopType != "6" { + // 校验店铺订阅时间是否到期 + checkShopSubscriptionExpirationErr := checkShopSubscriptionExpiration(shop.ID, dataVal.ShopType, shop.SkuSpec, shop.Deregulation, shop.ExpirationTime) + if checkShopSubscriptionExpirationErr != nil { + errMsg := checkShopSubscriptionExpirationErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + if shopData.ShopDetail == nil { + errMsg := "未设置商品详情" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + detail := shopData.ShopDetail + + if shop.ShopType != dataVal.ShopType { + errMsg := "店铺类型不匹配 错误店铺类型:" + shop.ShopType + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //dataVal.ShopType = dataVal.ShopType + + //扣费 + //userId := strconv.FormatInt(shop.CreateBy, 10) + //_, taskDeductionErr := TaskDeduction(shopID, userId) + //if taskDeductionErr != nil { + // errMsg := "请求创建任务接口失败: " + taskDeductionErr.Error() + "店铺id:" + shopID + "用户id:" + userId + // tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + // return + //} + + //验证 拼多多 店铺规格信息是否正确 + if dataVal.ShopType == "1" { + pddDll, initPddSOErr := pdd.InitPddDll() + if initPddSOErr != nil { + errMsg := "初始化pdd.so失败: " + initPddSOErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //使用类目预测接口测试Token 是否有效 + err := toolPdd.BuildPddGoodsOuterCatMappingGet(pddDll, shop.Token) + + if err != nil && err.Error() == "拼多多Token已过期" { + //更新token + reqData := map[string]string{ + "shopId": dataVal.ShopID, + } + _, submitFormDataErr := tool.SubmitFormData(golabl.Config.FileUrl.UpdateTokenUrl, reqData) + if submitFormDataErr != nil { + fmt.Println("提交表单数据失败:", submitFormDataErr) + return + } + } else if err != nil { + tool.Error(httpMsg, err.Error(), http.StatusInternalServerError) + return + } + } + + //请求创建任务接口并获取任务 id + taskId, err := CreateTaskRequest(dataVal.ShopID, dataVal.TaskType) + if err != nil { + errMsg := "店铺ID " + dataVal.ShopID + " 任务类型" + dataVal.ShopType + " 请求创建任务接口失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + //如果是拉取任务则校验是否已有在执行的 不能是淘宝 taskType == 3(拉取任务) || (taskType == 4 && dataVal.ShopType == "1")(拼多多拉取详情任务) + if dataVal.ShopType != "6" && (taskType == 3 || (taskType == 4 && dataVal.ShopType == "1")) { + //查询店铺拉取商品或者拉取商品详情所有任务 + read := rep.CreateDbFactoryRead() + taskList, getTaskByShopIdAndTaskTypeErr := read.GetTaskByShopIdAndTaskType(shopData.Shop.ID, taskType) + if getTaskByShopIdAndTaskTypeErr != nil { + errMsg := "获取任务失败: " + getTaskByShopIdAndTaskTypeErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + for _, task := range taskList { + // 获取 header信息 + taskHeader, getTaskHeaderErr := service.GetTaskHeader(task.TaskId) + if getTaskHeaderErr != nil { + errMsg := "获取任务头失败: " + getTaskHeaderErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //判断这个店铺是否已经存在正在执行的任务 + if taskHeader.Status == 1 || taskHeader.Status == 10 { + errMsg := "当前店铺已经有此类任务正在执行中" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + } else if shop.ShopType != "6" && (taskType == 1 || taskType == 2 || taskType == 6 || taskType == 7 || taskType == 8 || taskType == 9) { + if shopData.Spec == nil { + errMsg := "未设置规格" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + spec = shopData.Spec + //如果价格模版为空,则返回错误信息 + if shopData.PriceTemplate == nil { + errMsg := "未选择价格模版" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + context = shopData.ShopContext + //如果价格模版为空,则返回错误信息 + if shopData.PriceTemplate == nil { + errMsg := "未选择价格模版" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + priceTemplateRangeStr := shopData.PriceTemplate.RangePrice + err = json.Unmarshal([]byte(priceTemplateRangeStr), &priceRange) + if err != nil { + errMsg := "解析价格模板失败:" + err.Error() + " 原始数据:" + shopData.PriceTemplate.RangePrice + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + ////验证店铺规格信息是否正确 + //if dataVal.ShopType == "1" && dataVal.TaskType == "1" { + // pddDll, initPddSOErr := pdd.InitPddDll() + // if initPddSOErr != nil { + // errMsg := "初始化pdd.so失败: " + initPddSOErr.Error() + // tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + // return + // } + // _, buildPddGoodsSpecIdErr := buildPddGoodsSpecId(pddDll, shop.Token, spec.SpecTypeID, spec.SpecName) + // if buildPddGoodsSpecIdErr != nil { + // errMsg := "构建规格ID失败: " + buildPddGoodsSpecIdErr.Error() + // tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + // return + // } + //} + } + + fmt.Printf("店铺ID: %s, 店铺类型: %s, 任务类型: %s, 更新方式: %s, 任务数量: %s 任务id: %s \n", dataVal.ShopID, dataVal.ShopType, dataVal.TaskType, dataVal.UpdateType, dataVal.TaskCount, taskId) + + // 创建任务逻辑... + createAt := time.Now().Unix() + task, err := CreateTaskData(taskId, taskType, createAt, shop, priceRange, spec, detail, context, dataVal.TaskCount, imgType, updateType, shopData.PriceTemplate.PriceType) + if err != nil { + errMsg := "创建任务失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + //推送 redis + err = service.UpdateTaskHeader(taskId, task.Header) + if err != nil { + errMsg := "保存任务头失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + //更新任务尾 + err = service.UpdateTaskFooter(taskId, &task.Footer) + if err != nil { + errMsg := "保存任务尾失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + if (taskType == 10 || taskType == 11) && shop.ShopType != "6" { + var createDelTask mysql.DelTask + userId := shop.CreateBy + // 将 task.Header 转为 json字符串 + headerJson, marshalErr := json.Marshal(task.Header) + if marshalErr != nil { + errMsg := "任务头转换json失败: " + marshalErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + headerStr := string(headerJson) + taskCountOver := 0 + status := 0 + + if taskType == 10 { + taskType := 2 + //将 DelNum 转为 int64 + taskCount, err := strconv.ParseInt(dataVal.DelNum, 10, 64) + if err != nil { + errMsg := "任务类型转换失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + taskCountInt := int(taskCount) + createDelTask = mysql.DelTask{ + UserID: &userId, + ShopID: &shop.ID, + TaskID: &taskId, + TaskType: &taskType, + ShopType: &shop.ShopType, + Status: &status, + ShopName: &shop.ShopName, + TaskCountOver: &taskCountOver, + TaskCount: &taskCountInt, + Header: &headerStr, + CreateAt: nil, + } + } else { + taskType := 3 + taskCount := 0 + // 将时间字符串转换为时间戳 int64 + layout := "2006-01-02 15:04:05" + loc, _ := time.LoadLocation("Asia/Shanghai") // 北京时间 + t, err := time.ParseInLocation(layout, dataVal.DelTime, loc) + if err != nil { + errMsg := "时间解析失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + delTimeInt64 := t.Unix() + + // 转换为 time.Time 类型 + delTimeObj := time.Unix(delTimeInt64, 0) + createDelTask = mysql.DelTask{ + UserID: &userId, + ShopID: &shop.ID, + TaskID: &taskId, + TaskType: &taskType, + ShopType: &shop.ShopType, + Status: &status, + ShopName: &shop.ShopName, + TaskCountOver: &taskCountOver, + TaskCount: &taskCount, + Header: &headerStr, + StopAt: &delTimeObj, + CreateAt: nil, + } + } + err = serviceMysql.CreateDelTask(createDelTask) + if err != nil { + errMsg := "创建删除任务失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + } else if shop.ShopType != "6" && (taskType == 3 || (taskType == 4 && dataVal.ShopType == "1")) { + //如果是拉取任务则直接执行 B方法程序 taskType == 3(拉取任务) || (taskType == 4 && dataVal.ShopType == "1")(拼多多拉取详情任务) + // 执行 B方法程序 + _, runTaskWorkerErr := process.RunTaskWorker(taskId) + if runTaskWorkerErr != nil { + //fmt.Printf("执行B程序出错: %v\n", runTaskWorkerErr) + return + } + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + + //写入 mysql数据 + mysqlCreateTaskRecordsErr := mysqlWrite.CreateTaskRecords(_type.TaskRecordsDTO{ + UserId: shopData.Shop.CreateBy, + ShopId: shopData.Shop.ID, + TaskId: taskId, + ShopName: shop.ShopName, + TaskType: taskType, + }) + if mysqlCreateTaskRecordsErr != nil { + errMsg := "插入任务用户失败: " + mysqlCreateTaskRecordsErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //写入 sqlite数据 + sqliteTaskExportErr := sqliteWrite.CreateTaskRecords(_type.TaskRecordsDTO{ + UserId: shopData.Shop.CreateBy, + ShopId: shopData.Shop.ID, + TaskId: taskId, + ShopName: shop.ShopName, + TaskType: taskType, + }) + if sqliteTaskExportErr != nil { + errMsg := "插入任务用户失败: " + sqliteTaskExportErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } else { + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + + //写入 mysql数据 + mysqlCreateTaskRecordsErr := mysqlWrite.CreateTaskRecords(_type.TaskRecordsDTO{ + UserId: shopData.Shop.CreateBy, + ShopId: shopData.Shop.ID, + TaskId: taskId, + ShopName: shop.ShopName, + TaskType: taskType, + }) + if mysqlCreateTaskRecordsErr != nil { + errMsg := "插入任务用户失败: " + mysqlCreateTaskRecordsErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //写入 sqlite数据 + sqliteTaskExportErr := sqliteWrite.CreateTaskRecords(_type.TaskRecordsDTO{ + UserId: shopData.Shop.CreateBy, + ShopId: shopData.Shop.ID, + TaskId: taskId, + ShopName: shop.ShopName, + TaskType: taskType, + }) + if sqliteTaskExportErr != nil { + errMsg := "插入任务用户失败: " + sqliteTaskExportErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + + // 返回成功响应 + tool.Success(httpMsg, taskId) +} + +// SetTaskBody 置任务体 +func SetTaskBody(httpMsg http.ResponseWriter, data *http.Request) { + + // 方法1:直接使用multipart reader(最安全) + contentType := data.Header.Get("Content-Type") + if !strings.Contains(contentType, "multipart/form-data") { + tool.Error(httpMsg, "Content-Type必须是multipart/form-data", http.StatusBadRequest) + return + } + + // 移除请求体大小限制 + const maxInt64 = 1<<63 - 1 + data.Body = http.MaxBytesReader(httpMsg, data.Body, maxInt64) + + // 创建multipart reader + reader, err := data.MultipartReader() + if err != nil { + tool.Error(httpMsg, "创建multipart reader失败: "+err.Error(), http.StatusInternalServerError) + return + } + + var bodyData []string + var taskId string + + // 流式处理每个部分 + for { + part, err := reader.NextPart() + if err == io.EOF { + break + } + if err != nil { + tool.Error(httpMsg, "读取表单部分失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 读取这部分的内容 + var buf bytes.Buffer + if _, err := io.Copy(&buf, part); err != nil { + tool.Error(httpMsg, "读取数据失败: "+err.Error(), http.StatusInternalServerError) + return + } + + content := buf.String() + formName := part.FormName() + + if formName == "body" { + bodyData = append(bodyData, content) + } else if formName == "task_id" { + taskId = content + } + } + // 验证任务 ID + if taskId == "" { + errMsg := "任务 ID 不能为空" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + //验证状态 + header, getTaskHeaderErr := service.GetTaskHeader(taskId) + if getTaskHeaderErr != nil { + errMsg := "获取任务头失败: " + getTaskHeaderErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + if header.Status == _type.TaskStatusStopped { + errMsg := "任务已停止" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + if header.TaskId == "" { + errMsg := "任务不存在或已经删除" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 更新任务数 + go UpdateTaskCount(bodyData, taskId) + + // 返回成功响应 + tool.Success(httpMsg, "") +} + +// PauseTask 暂停任务 +func PauseTask(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, updateTaskStatusValidatorErr := validator.TaskIdValidator(data) + if updateTaskStatusValidatorErr != nil { + tool.Error(httpMsg, updateTaskStatusValidatorErr.Error(), http.StatusInternalServerError) + return + } + + // 验证状态 + header, getTaskHeaderErr := service.GetTaskHeader(dataVal.TaskID) + if getTaskHeaderErr != nil { + errMsg := "获取任务头失败: " + getTaskHeaderErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + if header.Status != _type.TaskStatusRunning { + errMsg := "当前状态不是执行中" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + read := rep.CreateDbFactoryRead() + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + // 查询当前任务信息 + taskRecords, getTaskRecordsByTaskIdErr := read.GetTaskRecordsByTaskId(dataVal.TaskID) + if getTaskRecordsByTaskIdErr != nil { + errMsg := fmt.Sprintf("获取任务信息失败 %v", getTaskRecordsByTaskIdErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 查询当前导出任务信息 + taskExport, getTaskExportByTaskIdErr := read.GetTaskExportByTaskId(dataVal.TaskID) + if getTaskExportByTaskIdErr != nil { + return + } + // 暂停时将task_records表状态改为未导出状态 + mysqlUpdateTaskRecordsErr := mysqlWrite.UpdateTaskRecords(_type.TaskRecordsDTO{ + UserId: taskRecords.UserId, + ShopId: taskRecords.ShopId, + TaskId: taskRecords.TaskId, + ShopName: taskRecords.ShopName, + IsExport: 0, + TaskType: taskRecords.TaskType, + }) + if mysqlUpdateTaskRecordsErr != nil { + errMsg := "更新任务用户失败: " + mysqlUpdateTaskRecordsErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + sqliteUpdateTaskRecordsErr := sqliteWrite.UpdateTaskRecords(_type.TaskRecordsDTO{ + UserId: taskRecords.UserId, + ShopId: taskRecords.ShopId, + TaskId: taskRecords.TaskId, + ShopName: taskRecords.ShopName, + IsExport: 0, + TaskType: taskRecords.TaskType, + }) + if sqliteUpdateTaskRecordsErr != nil { + errMsg := "更新任务用户失败: " + sqliteUpdateTaskRecordsErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 暂停时将task_export状态改为未导出状态 + mysqlUpdateTaskExportStatusErr := mysqlWrite.UpdateTaskExportStatus(taskExport.TaskId, 1, taskExport.FileUrl) + if mysqlUpdateTaskExportStatusErr != nil { + errMsg := "更新任务用户失败: " + mysqlUpdateTaskExportStatusErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + sqliteUpdateTaskExportStatusErr := sqliteWrite.UpdateTaskExportStatus(taskExport.TaskId, 1, taskExport.FileUrl) + if sqliteUpdateTaskExportStatusErr != nil { + errMsg := "更新任务用户失败: " + sqliteUpdateTaskExportStatusErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 暂停 B程序 + suspendProcessErr := process.SuspendProcess(dataVal.TaskID) + if suspendProcessErr != nil { + errMsg := "暂停任务失败: " + suspendProcessErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 返回成功响应 + tool.Success(httpMsg, "") +} + +// ResumeTask 恢复任务 +func ResumeTask(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, updateTaskStatusValidatorErr := validator.TaskIdValidator(data) + if updateTaskStatusValidatorErr != nil { + tool.Error(httpMsg, updateTaskStatusValidatorErr.Error(), http.StatusInternalServerError) + return + } + //验证状态 + header, getTaskHeaderErr := service.GetTaskHeader(dataVal.TaskID) + if getTaskHeaderErr != nil { + errMsg := "获取任务头失败: " + getTaskHeaderErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + if header.Status != _type.TaskStatusPaused { + errMsg := "当前状态不是暂停" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 恢复 B程序 + suspendProcessErr := process.ResumeProcess(dataVal.TaskID) + if suspendProcessErr != nil { + errMsg := "恢复进程失败: " + suspendProcessErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 返回成功响应 + tool.Success(httpMsg, "") +} + +// StopTask 停止任务 +func StopTask(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, updateTaskStatusValidatorErr := validator.TaskIdValidator(data) + if updateTaskStatusValidatorErr != nil { + tool.Error(httpMsg, updateTaskStatusValidatorErr.Error(), http.StatusInternalServerError) + return + } + + //验证状态 + header, getTaskHeaderErr := service.GetTaskHeader(dataVal.TaskID) + if getTaskHeaderErr != nil { + errMsg := "获取任务头失败: " + getTaskHeaderErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + if header.Status == _type.TaskStatusOver { + errMsg := "任务已完成" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 停止 B程序 + stopProcessErr := process.StopTask(dataVal.TaskID) + if stopProcessErr != nil { + errMsg := "停止进程失败: " + stopProcessErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 返回成功响应 + tool.Success(httpMsg, "") +} + +// DelTask 删除任务 +func DelTask(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, updateTaskStatusValidatorErr := validator.TaskIdValidator(data) + if updateTaskStatusValidatorErr != nil { + tool.Error(httpMsg, updateTaskStatusValidatorErr.Error(), http.StatusInternalServerError) + return + } + + //获取任务状态 + header, getTaskHeaderErr := service.GetTaskHeader(dataVal.TaskID) + if getTaskHeaderErr != nil { + errMsg := "获取任务头失败: " + getTaskHeaderErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 如果任务是暂停则先恢复 + if header.Status == _type.TaskStatusPaused { + // 恢复 B程序 + suspendProcessErr := process.ResumeProcess(dataVal.TaskID) + if suspendProcessErr != nil { + errMsg := "恢复进程失败: " + suspendProcessErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 停止 B程序 清空任务 + stopProcessErr := process.StopTask(dataVal.TaskID) + if stopProcessErr != nil { + errMsg := "停止进程失败: " + stopProcessErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + + // 删除 redis中的内容 + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + delTaskErr := service.DelTask(dataVal.TaskID) + if delTaskErr != nil { + errMsg := "删除任务失败: " + delTaskErr.Error() + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + //删除 mysql中TaskRecords指定数据 + mysqlDeleteTaskRecordsByTaskIdErr := mysqlWrite.DeleteTaskRecordsByTaskId(dataVal.TaskID) + if mysqlDeleteTaskRecordsByTaskIdErr != nil { + errMsg := "删除任务失败: " + mysqlDeleteTaskRecordsByTaskIdErr.Error() + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + // 删除 sqlite中TaskRecords指定数据 + sqLiteDeleteTaskRecordsByTaskIDErr := sqliteWrite.DeleteTaskRecordsByTaskId(dataVal.TaskID) + if sqLiteDeleteTaskRecordsByTaskIDErr != nil { + errMsg := "删除任务失败: " + delTaskErr.Error() + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + //3秒后再次删除,避免删除期间body进入数据 + go func() { + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + // 删除任务 延迟3后删除 + time.Sleep(time.Duration(3) * time.Second) + delTaskErr := service.DelTask(dataVal.TaskID) + if delTaskErr != nil { + errMsg := "删除任务失败: " + delTaskErr.Error() + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + //删除 mysql中TaskRecords指定数据 + mysqlDeleteTaskRecordsByTaskIdErr := mysqlWrite.DeleteTaskRecordsByTaskId(dataVal.TaskID) + if mysqlDeleteTaskRecordsByTaskIdErr != nil { + errMsg := "删除任务失败: " + delTaskErr.Error() + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + // 删除 sqlite中TaskRecords指定数据 + sqLiteDeleteTaskRecordsByTaskIDErr := sqliteWrite.DeleteTaskRecordsByTaskId(dataVal.TaskID) + if sqLiteDeleteTaskRecordsByTaskIDErr != nil { + errMsg := "删除任务失败: " + sqLiteDeleteTaskRecordsByTaskIDErr.Error() + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + }() + + tool.Success(httpMsg, "") +} + +// OverTask 任务完成 +func OverTask(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, updateTaskStatusValidatorErr := validator.TaskIdValidator(data) + if updateTaskStatusValidatorErr != nil { + tool.Error(httpMsg, updateTaskStatusValidatorErr.Error(), http.StatusInternalServerError) + return + } + + //查询 header 信息 + header, getTaskHeaderErr := service.GetTaskHeader(dataVal.TaskID) + if getTaskHeaderErr != nil { + fmt.Printf("获取footer 信息失败 %v", getTaskHeaderErr) + return + } + if header.Status != _type.TaskStatusStopped { + //推送 redis + status := int64(_type.TaskStatusOver) + err := service.UpdateHeaderStatus(dataVal.TaskID, status) + if err != nil { + errMsg := err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + lock.DestroyLock(dataVal.TaskID) //销毁锁 + } + if header.TaskType == 5 { + taskNoticeRequestErr := OperationGoodsTaskNoticeRequest(header.TaskId, header.ShopId) + if taskNoticeRequestErr != nil { + return + } + } else { + taskNoticeRequestErr := TaskNoticeRequest(header.TaskId) + if taskNoticeRequestErr != nil { + return + } + } + // 返回成功响应 + tool.Success(httpMsg, "") +} + +// GetTask 任务列表 +func GetTask(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, getTaskValidatorErr := validator.GetTaskValidator(data) + if getTaskValidatorErr != nil { + tool.Error(httpMsg, getTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + + taskTypeInt64 := int64(0) + var taskTypeAtoiErr error + if dataVal.TaskType != "" { + //将 taskTypeStr 转为 int + var temp int + temp, taskTypeAtoiErr = strconv.Atoi(dataVal.TaskType) + if taskTypeAtoiErr != nil { + errMsg := "任务类型转换失败" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + taskTypeInt64 = int64(temp) + } + + read := rep.CreateDbFactoryRead() + records, total, getTaskRecordsListErr := read.GetTaskRecordsList(_type.GetTaskRecordsListReq{ + UserId: "", + TaskId: dataVal.TaskID, + TaskType: taskTypeInt64, + ShopName: dataVal.ShopName, + Page: page, + Size: size, + }) + if getTaskRecordsListErr != nil { + errMsg := getTaskRecordsListErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + dataTaskAll := []map[string]interface{}{} + for _, v := range records { + //查询 header 信息 + header, getTaskHeaderErr := service.GetTaskHeader(v.TaskId) + if getTaskHeaderErr != nil { + errMsg := fmt.Sprintf("获取footer 信息失败 %v", getTaskHeaderErr) + fmt.Println(errMsg) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //获取 footer 信息 + footer, getTaskFooterErr := service.GetTaskFooter(v.TaskId) + if getTaskFooterErr != nil { + errMsg := fmt.Sprintf("获取footer 信息失败 %v", getTaskFooterErr) + fmt.Println(errMsg) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //获取 body_over 信息 + bodyOver, _, GetTaskBodyOverErr := service.GetTaskBodyOver(v.TaskId, 0, 10) + if GetTaskBodyOverErr != nil { + errMsg := fmt.Sprintf("获取body_over 信息失败 %v", GetTaskBodyOverErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + footerData := map[string]interface{}{ + "task_count_true": footer.TaskCountTrue, + "task_count_success": footer.TaskCountSuccess.Load(), + "task_count_error": footer.TaskCountError.Load(), + "task_count_wait": footer.TaskCountWait.Load(), + "task_count_over": footer.TaskCountOver.Load(), + "task_qpm": footer.TaskQpm, + "last_index": footer.LastIndex, + "task_count": footer.TaskCount, + } + header.ShopMsg.Token = "****暂不展示*****" + dataTask := map[string]interface{}{ + "header": header, + "footer": footerData, + "body_over": bodyOver, + "is_export": v.IsExport, + } + dataTaskAll = append(dataTaskAll, dataTask) + } + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": dataTaskAll, + } + tool.Success(httpMsg, dataRet) +} + +// GetTaskByUserId 获取用户任务 +func GetTaskByUserId(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, getTaskByUserIdValidatorErr := validator.GetTaskByUserIdValidator(data) + if getTaskByUserIdValidatorErr != nil { + tool.Error(httpMsg, getTaskByUserIdValidatorErr.Error(), http.StatusInternalServerError) + return + } + // 获取分页参数 + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + taskTypeInt64 := int64(0) + var parseIntTaskTypeErr error + if dataVal.TaskType != "" { + //将taskType 转换为 int64 + taskTypeInt64, parseIntTaskTypeErr = strconv.ParseInt(dataVal.TaskType, 10, 64) + if parseIntTaskTypeErr != nil { + errMsg := "任务类型转换失败" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + } + + read := rep.CreateDbFactoryRead() + records, total, GetTaskUserListErr := read.GetTaskRecordsList(_type.GetTaskRecordsListReq{ + UserId: dataVal.UserID, + TaskId: dataVal.TaskID, + TaskType: taskTypeInt64, + ShopName: dataVal.ShopName, + Page: page, + Size: size, + }) + if GetTaskUserListErr != nil { + errMsg := GetTaskUserListErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + dataTaskAll := []map[string]interface{}{} + for _, v := range records { + //查询 header 信息 + header, getTaskHeaderErr := service.GetTaskHeader(v.TaskId) + if getTaskHeaderErr != nil { + errMsg := fmt.Sprintf("获取footer 信息失败 %v", getTaskHeaderErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //获取 footer 信息 + footer, getTaskFooterErr := service.GetTaskFooter(v.TaskId) + if getTaskFooterErr != nil { + errMsg := fmt.Sprintf("获取footer 信息失败 %v", getTaskFooterErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //获取 body_over 信息 + bodyOver, _, GetTaskBodyOverErr := service.GetTaskBodyOver(v.TaskId, 0, 10) + if GetTaskBodyOverErr != nil { + errMsg := fmt.Sprintf("获取body_over 信息失败 %v", GetTaskBodyOverErr) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + footerData := map[string]interface{}{ + "task_count_true": footer.TaskCountTrue, + "task_count_success": footer.TaskCountSuccess.Load(), + "task_count_error": footer.TaskCountError.Load(), + "task_count_wait": footer.TaskCountWait.Load(), + "task_count_over": footer.TaskCountOver.Load(), + "task_qpm": footer.TaskQpm, + "last_index": footer.LastIndex, + "task_count": footer.TaskCount, + } + dataTask := map[string]interface{}{ + "header": header, + "footer": footerData, + "body_over": bodyOver, + "is_export": v.IsExport, + } + dataTaskAll = append(dataTaskAll, dataTask) + } + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": dataTaskAll, + } + tool.Success(httpMsg, dataRet) +} + +// GetTaskHeader 获取 header信息 +func GetTaskHeader(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, getHeaderValidatorErr := validator.TaskIdValidator(data) + if getHeaderValidatorErr != nil { + tool.Error(httpMsg, getHeaderValidatorErr.Error(), http.StatusInternalServerError) + return + } + header, getTaskHeaderErr := service.GetTaskHeader(dataVal.TaskID) + if getTaskHeaderErr != nil { + errMsg := getTaskHeaderErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //判断数据是否为空 + if header.TaskId == "" { + tool.Success(httpMsg, "") + return + } + header.ShopMsg.Token = "****暂不展示*****" + tool.Success(httpMsg, header) +} + +// GetBodyOver 获取body_over +func GetBodyOver(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, getBodyOverValidatorValidatorErr := validator.GetBodyOverValidator(data) + if getBodyOverValidatorValidatorErr != nil { + tool.Error(httpMsg, getBodyOverValidatorValidatorErr.Error(), http.StatusInternalServerError) + return + } + // 获取分页参数 + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + bodyOver, total, getTaskBodyOverLimit10Err := service.GetTaskBodyOver(dataVal.TaskID, page, size) + if getTaskBodyOverLimit10Err != nil { + errMsg := getTaskBodyOverLimit10Err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": bodyOver, + } + tool.Success(httpMsg, dataRet) +} + +// GetTaskList 获取任务列表 +func GetTaskList(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, getTaskValidatorErr := validator.GetTaskValidator(data) + if getTaskValidatorErr != nil { + tool.Error(httpMsg, getTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + page, size := tool.SetPage(dataVal.Page, dataVal.Size) + + taskTypeInt64 := int64(0) + var taskTypeAtoiErr error + if dataVal.TaskType != "" { + //将 taskTypeStr 转为 int + var temp int + temp, taskTypeAtoiErr = strconv.Atoi(dataVal.TaskType) + if taskTypeAtoiErr != nil { + errMsg := "任务类型转换失败" + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + taskTypeInt64 = int64(temp) + } + + read := rep.CreateDbFactoryRead() + records, total, getTaskRecordsListErr := read.GetTaskRecordsList(_type.GetTaskRecordsListReq{ + UserId: "", + TaskId: dataVal.TaskID, + TaskType: taskTypeInt64, + ShopName: dataVal.ShopName, + Page: page, + Size: size, + }) + if getTaskRecordsListErr != nil { + errMsg := getTaskRecordsListErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + dataTaskAll := []map[string]interface{}{} + for _, v := range records { + //查询 header 信息 + header, getTaskHeaderErr := service.GetTaskHeader(v.TaskId) + if getTaskHeaderErr != nil { + errMsg := fmt.Sprintf("获取footer 信息失败 %v", getTaskHeaderErr) + fmt.Println(errMsg) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //获取 footer 信息 + footer, getTaskFooterErr := service.GetTaskFooter(v.TaskId) + if getTaskFooterErr != nil { + errMsg := fmt.Sprintf("获取footer 信息失败 %v", getTaskFooterErr) + fmt.Println(errMsg) + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + footerData := map[string]interface{}{ + "task_count_true": footer.TaskCountTrue, + "task_count_success": footer.TaskCountSuccess.Load(), + "task_count_error": footer.TaskCountError.Load(), + "task_count_wait": footer.TaskCountWait.Load(), + "task_count_over": footer.TaskCountOver.Load(), + "task_qpm": footer.TaskQpm, + "last_index": footer.LastIndex, + "task_count": footer.TaskCount, + } + dataTask := map[string]interface{}{ + "header": header, + "footer": footerData, + "is_export": v.IsExport, + } + dataTaskAll = append(dataTaskAll, dataTask) + } + dataRet := map[string]interface{}{ + "page": page, + "size": size, + "total": total, + "list": dataTaskAll, + } + tool.Success(httpMsg, dataRet) +} + +func B(httpMsg http.ResponseWriter, data *http.Request) { + taskID := "111" + _, callSendPublishingErr := process.RunTaskWorker(taskID) + if callSendPublishingErr != nil { + logStr := fmt.Sprintf("执行B程序失败: [taskId] %v [error] %v", taskID, callSendPublishingErr.Error()) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, logStr) + tool.Error(httpMsg, callSendPublishingErr.Error(), http.StatusInternalServerError) + return + } + tool.Success(httpMsg, "") +} + +// UpdateTaskProgress 更新任务进度 +func UpdateTaskProgress(httpMsg http.ResponseWriter, data *http.Request) { + + // 验证表单 + dataVal, updateTaskProgressValidatorErr := validator.UpdateTaskProgressValidator(data) + if updateTaskProgressValidatorErr != nil { + tool.Error(httpMsg, updateTaskProgressValidatorErr.Error(), http.StatusInternalServerError) + return + } + + // 将 dataVal.Status 转为int64 + statusInt64, err := strconv.ParseInt(dataVal.Status, 10, 64) + if err != nil { + tool.Error(httpMsg, "状态值格式错误", http.StatusBadRequest) + return + } + + // 将 dataVal.Num 转为int64 + numInt64, err := strconv.ParseInt(dataVal.Num, 10, 64) + if err != nil { + tool.Error(httpMsg, "进度数值格式错误", http.StatusBadRequest) + return + } + + // 更新任务尾 + updateTaskFootersErr := service.UpdateTaskFooters(dataVal.TaskID, statusInt64, numInt64) + if updateTaskFootersErr != nil { + tool.Error(httpMsg, updateTaskFootersErr.Error(), http.StatusInternalServerError) + return + } + // 更新任务头 + updateTaskHeadersErr := service.UpdateTaskHeaders(dataVal.TaskID, statusInt64, numInt64) + if updateTaskHeadersErr != nil { + tool.Error(httpMsg, updateTaskHeadersErr.Error(), http.StatusInternalServerError) + return + } + tool.Success(httpMsg, "") + +} + +//****************************工具**************************************// + +// CreateTaskData 创建task数据 +// @param taskId 任务ID +// @param taskType 任务类型 +// @param createAt 创建时间 +// @param shop 店铺信息 +// @param priceRange 价格模版 +// @param spec 商品规格 +// @param context 店铺描述 +// @param taskCount 任务数量 +// @param detail 店铺详情 +// @param imgType 图片类型 +// @return *_type.Task 任务数据 +// @return error 错误 +func CreateTaskData(taskId string, taskType int64, createAt int64, shop *_type.Shop, priceRange []_type.PriceRange, spec *_type.Spec, detail *_type.ShopDetail, context *_type.ShopContext, taskCount string, imgType int64, updateType int64, priceType string) (*_type.Task, error) { + var task _type.Task + //处理价格模版 + var priceModArr []_type.PriceMod + for _, v := range priceRange { + adjustPercentInt64, err := parseAdjustPercent(v.AdjustPercent) + if err != nil { + return &task, fmt.Errorf("价格模版 adjustPercent 转换失败: %v", err) + } + priceMod := _type.PriceMod{ + Min: v.MinPrice, + Max: v.MaxPrice, + MarkupRate: adjustPercentInt64, + MarkupValue: v.AdjustAmount, + } + priceModArr = append(priceModArr, priceMod) + } + var token string + var districtId int64 + var districtType string + //处理 Token + if shop.ShopType == "1" || shop.ShopType == "2" || shop.ShopType == "6" { //拼多店铺、孔夫子、淘宝 + token = shop.Token + } else if shop.ShopType == "5" { // 闲鱼店铺 + token = fmt.Sprintf("{\"app_id\":%v,\"app_secret\":\"%v\",\"username\":\"%v\"}", shop.MallID, shop.Token, shop.ShopKey) + districtId = detail.DistrictId + districtType = detail.DistrictType + } + // specTypeID 转换为int64 + var specTypeID int64 + var parseAdjustPercentErr error + if spec.SpecTypeID != "" { + specTypeID, parseAdjustPercentErr = parseAdjustPercent(spec.SpecTypeID) + if parseAdjustPercentErr != nil { + return &task, fmt.Errorf("规格类型ID 转换失败: %v", parseAdjustPercentErr) + } + } + //shopCount 转换为int64 + taskCountInt64, err := strconv.ParseInt(taskCount, 10, 64) + if err != nil { + return &task, fmt.Errorf("shopCount 转换为int64 转换失败: %v", err) + } + //发货时间 + shipmentLimitSecond := int64(24 * 60 * 60) //默认发货时间24小时 + if detail.ShipmentLimitSecond != "1" { + shipmentLimitSecond = shipmentLimitSecond * 2 //发货时间48小时 + } + task = _type.Task{ + Header: _type.TaskHeader{ + TaskId: taskId, + TaskType: taskType, + ShopId: shop.ID, + ShopName: shop.ShopName, + ShopType: shop.ShopType, + ShopMsg: _type.ShopMsg{ + ID: detail.ID, //店铺详情 ID + ShopAliasName: shop.ShopName, //店铺别名 + ShopName: shop.ShopName, //店铺名称 + Token: token, //店铺 token【如果是咸鱼店铺,此token则是应用密钥】 + GoodsNamePrefix: detail.TitlePrefix, //商品名称前缀 + GoodsNameSuffix: detail.TitleSuffix, //商品名称后缀 + TitleConsistOf: detail.TitleConsistOf, //商品名称组成 + SpaceCharacter: detail.SpaceCharacter, //间隔字符 0无间隔 1空格 + WatermarkImgUrl: detail.WatermarkImgUrl, //水印图片 + WatermarkPosition: detail.WatermarkPosition, //水印位置 0全部 1第一张 + CarouseLastImgUrlArray: tool.FilterStrings(detail.CarouseLastImgUrlArray), //轮播图最后图片[]string(tool.FilterStrings 函数为去掉数组中的空、图片不合法等字符串,因为原始数据中可能会出现空字符串导致商品发布报图片信息错误) + GoodsDetailFirstImgUrlArray: tool.FilterStrings(detail.GoodsDetailFirstImgUrlArray), //商品详情首图URL数组[]string(tool.FilterStrings 函数为去掉数组中的空、图片不合法字符串,因为原始数据中可能会出现空字符串导致商品发布报图片信息错误) + GoodsDetailLastImgUrlArray: tool.FilterStrings(detail.GoodsDetailLastImgUrlArray), //商品详情最后图片URL数组(tool.FilterStrings 函数为去掉数组中的空、图片不合法等字符串,因为原始数据中可能会出现空字符串导致商品发布报图片信息错误) + IsFolt: detail.Fake == "1", //是否支持假一赔十,false-不支持,true-支持 + IsPreSale: detail.Presale == "1" || detail.Presale == "2", //是否预售,true-预售商品,false-非预售商品 + IsRefundable: detail.SevenDays == "1", //是否7天无理由退换货,true-支持,false-不支持 + ShipmentLimitSecond: shipmentLimitSecond, //承诺发货时间(秒) + CostTemplateId: detail.TemplateId, //物流运费模板 ID + SpecName: spec.SpecTypeName, //规格名称 + SpecId: specTypeID, //规格 ID + SpecChildName: spec.SpecName, //规格子名称 + DefStock: int32(detail.StockDeff), //默认库存 + TwoDiscount: detail.TowDiscount, //2折 + IsSecondHand: detail.IsSecondHand == "1", //是否二手 1 -二手商品 ,0-全新商品 + DistrictMsg: _type.DistrictMsg{ + DistrictId: districtId, + DistrictType: districtType, + }, + ShopContext: context.Context, //店铺描述 + SkuWatermarkImgUrl: detail.SkuWatermarkImgUrl, //sku 水印图片 + PublishType: detail.PublishType, //发布方式 0=24(图书类目) 1=99(其他类目)【限闲鱼店铺使用】 + CategoryId: detail.CategoryId, //类目 Id【限闲鱼店铺使用】 + SpecCompose: spec.SpecCompose, //规格组合类型 0=自定义 1=Isbn 2=书名 3=货号 + SpecPrefix: spec.SpecPrefix, //规格前缀 + SpecSuffix: spec.SpecSuffix, //规格后缀 + IsParcel: detail.IsParcel, //是否包邮 + BookWeight: detail.BookWeight, //书籍重量 + StandardNumber: detail.StandardNumber, //商品标准本数 + ConditionDef: detail.ConditionDef, //商品品相 + SpecCodeCompose: spec.SpecCodeCompose, //规格编码组合类型 0=货号 1=ISBN + }, + PriceMod: priceModArr, //价格模版 + PriceType: priceType, //价格类型 + ShipPriceMod: "", //运费模版 + TaskCount: taskCountInt64, //任务数量 + TaskCountTrue: 0, //真实任务数量 + TaskCountWait: 0, //等待任务数量 + TaskCountOver: 0, //任务完成数量 + TaskCountSuccess: 0, //任务成功数量 + TaskCountError: 0, //任务失败数量 + Status: _type.TaskStatusRunning, //任务状态 1=运行中 2=暂停中 3=完成 + TaskQpm: 0, //任务 QPM + TaskCreateAt: createAt, //任务创建时间 + TaskOverAt: 0, //任务完成时间 + LastIndex: 0, //最后索引 + ImgType: imgType, //图片类型 0=无图片 1=轮播图 2=商品详情首图 3=商品详情最后图片 + UpdateType: updateType, // 更新方式(仅核价发布或核价表格发布使用) 1 过滤重复 2 全新上传 + Pool: _type.PoolConfig{ //协程池配置 + Size: 500, //协程数量 + WithExpiryDuration: 10, //过期时间 + WithPreAlloc: true, //预分配 + WithMaxBlockingTasks: 2000, //阻塞任务数 + WithNonblocking: true, //非阻塞 + }, + }, + BodyOver: _type.TaskBody{}, + Footer: _type.TaskFooter{ + TaskCount: taskCountInt64, //任务数量 + TaskCountTrue: 0, //真实任务数量 + TaskCountWait: atomic.Int64{}, //等待任务数量 + TaskCountOver: atomic.Int64{}, //任务完成数量 + TaskCountSuccess: atomic.Int64{}, //任务成功数量 + TaskCountError: atomic.Int64{}, //任务失败数量 + TaskQpm: 0, //任务QPM + LastIndex: 0, //最后索引 + }, + } + return &task, nil +} + +// UpdateTaskCount 更新任务数量 +// @param bodyData body数据 +// @param taskId 任务ID +func UpdateTaskCount(bodyData []string, taskId string) { + + //查询 header 信息 + header, getTaskHeaderErr := service.GetTaskHeader(taskId) + if getTaskHeaderErr != nil { + fmt.Printf("获取footer 信息失败 %v", getTaskHeaderErr) + return + } + + // 1. 先执行AddTask,统一判断是否需要后续操作 + count := AddTask(taskId, bodyData, header) + if count <= 0 { + fmt.Printf("找到的书品为0,所以不提交到redis") + return + } + if header.ShopType != "6" { + // 执行 B方法程序 + _, runTaskWorkerErr := process.RunTaskWorker(taskId) + if runTaskWorkerErr != nil { + //fmt.Printf("执行B程序出错: %v\n", runTaskWorkerErr) + return + } + } +} + +func AddTask(taskId string, bodyData []string, header _type.TaskHeader) int { + + if header.Status == _type.TaskStatusOver { + updateHeaderStatusErr := service.UpdateHeaderStatus(taskId, int64(_type.TaskStatusRunning)) + if updateHeaderStatusErr != nil { + fmt.Printf("更新header 状态失败 %v", updateHeaderStatusErr) + return 0 + } + } + // 遍历 bodyData 写入redis + var num atomic.Int64 + for _, v := range bodyData { + var taskBody _type.TaskBody + // 清理JSON字符串(去除可能的空格和换行) + jsonStr := strings.TrimSpace(v) + if err := json.Unmarshal([]byte(jsonStr), &taskBody); err != nil { + fmt.Printf("解析失败: %v %v\n", err, jsonStr) + continue + } + var bookInfo _type.BookInfo + var GetTaskBookErr error + // 书品处理 + if header.TaskType == 1 || header.TaskType == 2 || header.TaskType == 5 || header.TaskType == 6 || header.TaskType == 7 || header.TaskType == 8 || header.TaskType == 9 { + // 连接DB[b] 获取书品信息,#操作商品的isbn13个0则不查询isbn + if !(header.TaskType == 5 && taskBody.BookInfo.Isbn == "0000000000000") { + //判断isbn是否包含- 或者前三个是678开头的13位数字 + if (strings.Contains(taskBody.BookInfo.Isbn, "-") || strings.HasPrefix(taskBody.BookInfo.Isbn, "678")) && header.TaskType == 7 { + //截取 - 之前的字符串 + isbn := taskBody.BookInfo.Isbn + fisbn := "0" + var psiBookInfo psiMysqlType.BookInfo + var GetBookInfoErr error + if strings.Contains(taskBody.BookInfo.Isbn, "-") { + isbn = strings.Split(taskBody.BookInfo.Isbn, "-")[0] + fisbn = strings.Split(taskBody.BookInfo.Isbn, "-")[1] + psiBookInfo, GetBookInfoErr = psiMysqlService.GetBookInfo(isbn, fisbn) + + if GetBookInfoErr != nil { + if errors.Is(GetBookInfoErr, _redis.Nil) { + setNoBookCountErr := service.SetNoBookCount(taskBody.BookInfo.Isbn) + if setNoBookCountErr != nil { + fmt.Printf("设置无书品数量失败 isbn:%v", taskBody.BookInfo.Isbn) + } + } + } + } else { + psiBookInfo, GetBookInfoErr = psiMysqlService.GetBookInfoSingle(taskBody.BookInfo.Isbn) + + if GetBookInfoErr != nil { + if errors.Is(GetBookInfoErr, _redis.Nil) { + setNoBookCountErr := service.SetNoBookCount(taskBody.BookInfo.Isbn) + if setNoBookCountErr != nil { + fmt.Printf("设置无书品数量失败 isbn:%v", taskBody.BookInfo.Isbn) + } + } + } + } + + //处理图书图片// liveImage := strings.Split(psiBookInfo.LiveImage, ",") + var liveImage []string + json.Unmarshal([]byte(psiBookInfo.LiveImage), &liveImage) + + //处理类目 + //psiBookInfo.CatID = {"xian_yu_cat_id": "", "kong_fu_zi_cat_id": "", "pin_duo_duo_cat_id": ""} 解析到 _type.CatIdObject{} 中 + var catIdObject _type.CatIdObject + unmarshalErr := json.Unmarshal([]byte(psiBookInfo.CatID), &catIdObject) + if unmarshalErr != nil { + fmt.Printf("获取BookInfo失败-原因: %v\n", unmarshalErr) + continue + } + + bookInfo = _type.BookInfo{ + Isbn: psiBookInfo.ISBN, + BookName: psiBookInfo.BookName, + Author: psiBookInfo.Author, + Publishing: psiBookInfo.Publishing, + PublicationDate: psiBookInfo.PublicationDate, + Binding: psiBookInfo.Binding, + PagesCount: psiBookInfo.PagesCount, + WordsCount: psiBookInfo.WordsCount, + Format: psiBookInfo.Format, + ImageObject: _type.ImageObject{ + CarouselUrlArray: liveImage, + }, + Price: psiBookInfo.Price, + CatIdObject: catIdObject, + } + } else { + bookInfo, GetTaskBookErr = service.GetTaskBook(taskBody.BookInfo.Isbn) + if GetTaskBookErr != nil { + if errors.Is(GetTaskBookErr, _redis.Nil) { + setNoBookCountErr := service.SetNoBookCount(taskBody.BookInfo.Isbn) + if setNoBookCountErr != nil { + fmt.Printf("设置无书品数量失败 isbn:%v", taskBody.BookInfo.Isbn) + } + } + if header.TaskType != 5 { + fmt.Printf("获取BookInfo失败-原因: %v\n", GetTaskBookErr) + continue + } + } + } + + //如果是增量库存,则使用增量库存传递过来的图书名称 + if header.TaskType == 7 { + bookName := taskBody.BookInfo.BookName + if bookName != "" { + bookInfo.BookName = bookName + } + } + } + // 图片处理 + if header.TaskType == 1 || header.TaskType == 2 || header.TaskType == 6 || header.TaskType == 7 || header.TaskType == 8 || header.TaskType == 9 { + //处理图片 仅官图不处理 + if header.ImgType == 2 { //仅实拍图,使用传递过来的图片 + bookInfo.ImageObject.CarouselUrlArray = taskBody.BookInfo.ImageObject.CarouselUrlArray + } else if header.ImgType == 3 { // 优先官图,优先使用 bookInfo中的图片,如果没有使用传递过来的图片 + if len(bookInfo.ImageObject.CarouselUrlArray) == 0 { + bookInfo.ImageObject.CarouselUrlArray = taskBody.BookInfo.ImageObject.CarouselUrlArray + } + } else if header.ImgType == 4 { //优先实拍,优先使用 传递过来的图片,如果没有使用bookInfo中的图片 + if len(taskBody.BookInfo.ImageObject.CarouselUrlArray) > 0 { + bookInfo.ImageObject.CarouselUrlArray = taskBody.BookInfo.ImageObject.CarouselUrlArray + } + } + if header.ShopType == "1" || header.ShopType == "5" { + // 类目 Id处理 + var catId string + pinDuoDuoCatIdArr := tool.StringToArray(bookInfo.CatIdObject.PinDuoDuoCatId.String()) + if len(pinDuoDuoCatIdArr) == 3 { + catId = pinDuoDuoCatIdArr[2] + } else if len(pinDuoDuoCatIdArr) == 4 { + catId = pinDuoDuoCatIdArr[3] + } + if header.ShopType == "1" { + bookInfo.CatIdObject.PinDuoDuoCatId = _type.FlexibleStr(catId) + } else if header.ShopType == "5" { + bookInfo.CatIdObject.XianYuCatId = _type.FlexibleStr(bookInfo.CatIdObject.XianYuCatId.String()) + } + } + } + //表格上传处理 + if header.TaskType == 2 { + // 书名处理,如果传递的存在,则使用传递的 + if taskBody.BookInfo.BookName != "" { + bookInfo.BookName = taskBody.BookInfo.BookName + } + // 图片处理,如果传递的存在,则使用传递的 + if len(taskBody.BookInfo.ImageObject.CarouselUrlArray) > 0 { + bookInfo.ImageObject.CarouselUrlArray = taskBody.BookInfo.ImageObject.CarouselUrlArray + } + } + // 更新 BookInfo + taskBody.BookInfo = bookInfo + } + + // 更新 BodyWait + err := service.UpdateTaskBodyWait(taskId, taskBody) + if err != nil { + fmt.Println(err.Error()) + return 0 + } + //延迟1毫秒 + num.Add(1) + err = service.UpdateTaskCountTrue(taskId, 1) + } + if header.TaskType == 5 { + taskNoticeRequestErr := OperationGoodsTaskNoticeRequest(taskId, header.ShopId) + if taskNoticeRequestErr != nil { + return 0 + } + } else { + taskNoticeRequestErr := TaskNoticeRequest(taskId) + if taskNoticeRequestErr != nil { + return 0 + } + } + return int(num.Load()) +} + +// 处理adjustPercent字段(可能是int或string) +// @param adjustPercent adjustPercent字段 +// @return int64 处理后的数据 +// @return error 错误信息 +func parseAdjustPercent(adjustPercent interface{}) (int64, error) { + if adjustPercent == nil { + return 0, nil + } + //判断 adjustPercent 是否字符串 如果是 字符串转为 int64 + if reflect.TypeOf(adjustPercent).Kind() == reflect.String { + adjustPercentStr := adjustPercent.(string) + adjustPercentInt, err := strconv.Atoi(adjustPercentStr) + if err != nil { + return 0, err + } + return int64(adjustPercentInt), nil + } + //如果是 float64 + if reflect.TypeOf(adjustPercent).Kind() == reflect.Float64 { + return int64(adjustPercent.(float64)), nil + } + return adjustPercent.(int64), nil +} + +// CreateTaskRequest 请求接口创建任务 +func CreateTaskRequest(shopId string, taskType string) (string, error) { + + fileUrlConfig, getFileUrlConfigErr := config.GetFileUrlConfig() + if getFileUrlConfigErr != nil { + errMsg := "获取文件路径配置失败: " + getFileUrlConfigErr.Error() + return "", fmt.Errorf(errMsg) + } + taskTypeName := tool.GetTaskTypeName(taskType) + dataMap := map[string]string{ + "shopId": shopId, + "taskType": "NEW_ADD_TASK", + "fileName": taskTypeName, + } + taskDataStr, submitFormDataErr := tool.SubmitFormData(fileUrlConfig.CreateTaskUrl, dataMap) + if submitFormDataErr != nil { + errMsg := "提交表单数据失败 " + submitFormDataErr.Error() + return "", fmt.Errorf(errMsg) + } + var taskData _type.CreateTaskResponse + unmarshalErr := json.Unmarshal([]byte(taskDataStr), &taskData) + if unmarshalErr != nil { + errMsg := "解析任务数据失败: " + unmarshalErr.Error() + " 原始数据" + taskDataStr + return "", fmt.Errorf(errMsg) + } + if taskData.Code != 200 { + errMsg := "请求接口 " + fileUrlConfig.CreateTaskUrl + " data " + taskDataStr + return "", fmt.Errorf(errMsg) + } + return taskData.TaskID, nil +} + +// TaskNoticeRequest 任务有等待数据通知接口 +func TaskNoticeRequest(taskId string) error { + + fileUrlConfig, getFileUrlConfigErr := config.GetFileUrlConfig() + if getFileUrlConfigErr != nil { + return fmt.Errorf("获取文件路径配置失败: %v", getFileUrlConfigErr) + } + data := map[string]string{ + "taskId": taskId, + } + _, submitFormDataErr := tool.SubmitFormData(fileUrlConfig.CreateTaskNoticeUrl, data) + if submitFormDataErr != nil { + return fmt.Errorf("提交表单数据失败: %v", submitFormDataErr) + } + return nil +} + +// OperationGoodsTaskNoticeRequest 操作商品任务有等待数据通知接口 +func OperationGoodsTaskNoticeRequest(taskId string, shopId string) error { + + fileUrlConfig, getFileUrlConfigErr := config.GetFileUrlConfig() + if getFileUrlConfigErr != nil { + return fmt.Errorf("获取文件路径配置失败: %v", getFileUrlConfigErr) + } + data := map[string]string{ + "taskId": taskId, + "shopId": shopId, + } + _, submitFormDataErr := tool.SubmitFormData(fileUrlConfig.CreateOperationTaskNoticeUrl, data) + if submitFormDataErr != nil { + return fmt.Errorf("提交表单数据失败: %v", submitFormDataErr) + } + return nil +} + +// TaskDeduction 创建任务扣费 +func TaskDeduction(shopId string, userId string) (_type.TaskDeductionResponse, error) { + var taskDeductionData _type.TaskDeductionResponse + fileUrlConfig, getFileUrlConfigErr := config.GetFileUrlConfig() + if getFileUrlConfigErr != nil { + return taskDeductionData, fmt.Errorf("获取文件路径配置失败: %v", getFileUrlConfigErr) + } + + dataMap := map[string]string{ + "userId": userId, + "shopId": shopId, + "logType": "2", + "rechargPrice": "1", + } + taskDeductionStr, submitFormDataErr := tool.SubmitFormData(fileUrlConfig.DeductionUrl, dataMap) + if submitFormDataErr != nil { + return taskDeductionData, fmt.Errorf("提交表单数据失败 %v", submitFormDataErr) + } + unmarshalErr := json.Unmarshal([]byte(taskDeductionStr), &taskDeductionData) + if unmarshalErr != nil { + errMsg := "解析任务数据失败: " + unmarshalErr.Error() + " 原始数据" + taskDeductionStr + return taskDeductionData, fmt.Errorf(errMsg) + } + if taskDeductionData.Code != 200 { + errMsg := "请求接口 " + fileUrlConfig.CreateTaskUrl + " data " + taskDeductionStr + return taskDeductionData, fmt.Errorf(errMsg) + } + return taskDeductionData, nil +} + +// AppendToCSV 追加写入数据到CSV文件 +// @param fileName 文件名 +// @param data 数据 +// @param writeHeader 是否写入表头 +// @param taskId 任务ID +// @param taskType 任务类型 +// @return error +func AppendToCSV(fileName string, data []_type.TaskBody, writeHeader bool, taskId string, taskType int64) error { + + // 打开文件(不存在则创建,存在则追加) + file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("打开CSV文件失败: %v", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + // 第一次写入时添加表头 + if writeHeader && len(data) > 0 { + // 根据TaskBody的字段定义表头,这里需要你根据实际结构体调整 + headers := []string{ + "ISBN", "书名", "状态", "错误信息", // 示例表头,替换为你实际的字段名 + } + if err := writer.Write(headers); err != nil { + return fmt.Errorf("写入CSV表头失败: %v", err) + } + } + + // 写入数据行 + for _, item := range data { + statusStr := "正确" + if item.Detail.Status != 1 { + statusStr = "错误" + } + // 将TaskBody转换为字符串切片,需要根据实际结构体字段调整 + errStr := item.Detail.Error + if taskType == 3 || taskType == 4 { + errStr = "" + } + record := []string{ + item.BookInfo.Isbn, + item.BookInfo.BookName, + statusStr, + errStr, + } + if err := writer.Write(record); err != nil { + return fmt.Errorf("写入CSV数据失败: %v, 数据: %+v", err, item) + } + // 更新redis中的Complete字段,展示导出进度 + err := service.UpdateExportFileProgress(taskId) + if err != nil { + return fmt.Errorf("更新redis进度失败: %v", err) + } + } + + return nil +} + +// buildPddGoodsSpecId 根据名称获取规格信息 +// @param pddDll pddDLL对象 +// @param token 授权令牌 +// @param specId 商品规格id +// @param specName 规格名称 +// @return DllGoodsSpec 规格信息 +// @return error 错误信息 +func buildPddGoodsSpecId(pddDll *pdd.PddDLL, token string, id string, name string) (_type.DllGoodsSpec, error) { + + var spec _type.DllGoodsSpec + client, err := config.GetPddClient() + if err != nil { + return spec, err + } + //发送请求 生成商家自定义的规格 + clientId := client.ClientId + clientSecret := client.ClientSecret + specStr, err := pddDll.PddGoodsSpecIdGet(clientId, clientSecret, token, id, name) + if err != nil { + return spec, err + } + + // 解析JSON字符串 + err = json.Unmarshal([]byte(specStr), &spec) + if err != nil { + return spec, fmt.Errorf("解析拼多多 PddGoodsSpecIdGet 接口返回json失败: %v [拼多多数据:%v]", err, specStr) + } + return spec, nil +} + +// 验证店铺订阅是否到期 +func checkShopSubscriptionExpiration(taskId string, shopType string, skuSpec string, deregulation string, expirationTime string) error { + if shopType == "2" || shopType == "5" { + // 检验店铺订阅时间是否到期 + expirationTime, err := tool.GetSubscriptionExpirationTime(taskId) + if err != nil { + return fmt.Errorf("获取用户订阅到期时间失败: %v", err) + } + // 明确单位:假设 expirationTime 是秒级时间戳 + now := time.Now().Unix() + if now > expirationTime { + expirationDateTime := time.Unix(expirationTime, 0).Format("2006-01-02 15:04:05") + return fmt.Errorf("店铺已到期,到期时间:" + expirationDateTime) + } + return nil + } else { + // 解析时间字符串 + expTime, err := time.Parse("2006-01-02 15:04:05", expirationTime) + if err != nil { + return fmt.Errorf("时间格式错误: %v", err) + } + + // 判断是否大于当前时间 + if !expTime.After(time.Now()) { + return fmt.Errorf("店铺已到期,到期时间:%s", expTime.Format("2006-01-02 15:04:05")) + } + // 如果是拼多多店铺的话校验下 是否试用 + if shopType == "1" && strings.Contains(skuSpec, "7天") { + // 是否开启使用无上限 + if deregulation == "1" { + return fmt.Errorf("无法创建任务,请去ERP店铺列表订阅或者开通七天无上限限免") + } + } + return nil + } +} diff --git a/controller/uploadImg.go b/controller/uploadImg.go new file mode 100644 index 0000000..bf64c23 --- /dev/null +++ b/controller/uploadImg.go @@ -0,0 +1,85 @@ +package controller + +import ( + "encoding/json" + "fmt" + "net/http" + "os/exec" + "planA/initialization/golabl" + planBTypeModules "planA/planB/type/modules" + "planA/service" + "planA/tool" + toolPdd "planA/tool/pdd" + "planA/validator" + "strings" +) + +// ImgUploadToPdd 上传图片到拼多多 +func ImgUploadToPdd(httpMsg http.ResponseWriter, data *http.Request) { + // 验证表单 + dataVal, createTaskValidatorErr := validator.ImgUploadToPddValidator(data) + if createTaskValidatorErr != nil { + tool.Error(httpMsg, createTaskValidatorErr.Error(), http.StatusInternalServerError) + return + } + // 查询店铺数据 + shopDataStr, err := service.GetTaskShop(dataVal.ShopId) + if err != nil { + errMsg := "获取店铺数据失败: shopId " + dataVal.ShopId + " " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 解析 json数据 + shopData, err := toolPdd.ParseShopData(shopDataStr) + if err != nil { + errMsg := "解析店铺数据失败:" + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + token := shopData.Shop.Token + e, calleErr := callExe(dataVal.ImgUrl, token) + if calleErr != nil { + errMsg := "调用E程序失败: " + calleErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + //判断字符串 e 是否包含错误 + if strings.Contains(e, "错误") { + errMsg := "调用E程序失败: " + e + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + var pddImg planBTypeModules.GoodsImageUploadResponse + + // 解析 JSON字符串 + unmarshalErr := json.Unmarshal([]byte(e), &pddImg) + if unmarshalErr != nil { + errMsg := "解析E程序返回数据失败: " + unmarshalErr.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + tool.Success(httpMsg, pddImg.GoodsImageUploadResponse.ImageURL) +} + +// 调用E程序 +func callExe(imgUrl string, token string) (string, error) { + // 调用exe,传入两个参数 + cmd := exec.Command(golabl.Config.FileUrl.EFileName, imgUrl, token) + + // 执行命令并获取输出 + output, err := cmd.Output() + if err != nil { + // 如果命令执行失败,获取错误信息 + if exitError, ok := err.(*exec.ExitError); ok { + return "", fmt.Errorf("程序执行失败,退出码: %d, 错误信息: %s", + exitError.ExitCode(), string(exitError.Stderr)) + } + return "", fmt.Errorf("执行失败: %v", err) + } + + // 返回exe打印的数据(去除首尾空白字符) + result := strings.TrimSpace(string(output)) + return result, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f62d5b5 --- /dev/null +++ b/go.mod @@ -0,0 +1,75 @@ +module planA + +go 1.25.0 + +require ( + github.com/go-playground/validator/v10 v10.30.1 + github.com/go-redis/redis/v8 v8.11.5 + github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 + github.com/minio/minio-go/v7 v7.1.0 + github.com/panjf2000/ants/v2 v2.11.4 + github.com/robfig/cron/v3 v3.0.1 + golang.org/x/time v0.14.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/mysql v1.6.0 + gorm.io/gorm v1.31.1 + modernc.org/sqlite v1.46.1 +) + +require ( + filippo.io/edwards25519 v1.2.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.12.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.2 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tinylib/msgp v1.6.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2f48b09 --- /dev/null +++ b/go.sum @@ -0,0 +1,209 @@ +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +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.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +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.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +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.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +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/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +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/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +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/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE= +github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw= +github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8= +github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +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/panjf2000/ants/v2 v2.11.4 h1:UJQbtN1jIcI5CYNocTj0fuAUYvsLjPoYi0YuhqV/Y48= +github.com/panjf2000/ants/v2 v2.11.4/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= +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/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +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.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +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.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/initialization/c/c.go b/initialization/c/c.go new file mode 100644 index 0000000..0249c4b --- /dev/null +++ b/initialization/c/c.go @@ -0,0 +1,18 @@ +package c + +import ( + "fmt" + "planA/initialization/golabl" + "planA/tool/process" +) + +// RunC 运行C代码 +func RunC() { + if golabl.Config.Server.IsC { + //启动 C程序 + if err := process.RunCProgram(); err != nil { + fmt.Println("启动C程序失败:", err) + return + } + } +} diff --git a/initialization/config/config.go b/initialization/config/config.go new file mode 100644 index 0000000..b98dd33 --- /dev/null +++ b/initialization/config/config.go @@ -0,0 +1,108 @@ +package config + +import ( + "encoding/json" + "fmt" + "planA/initialization/golabl" + _configDll "planA/modules/config" + "planA/tool" + _type "planA/type" +) + +var ( + gDir string +) + +// Init 初始化 +// @param dir string 配置文件目录 +// @return _type.Config 配置文件信息 +// @return error 错误信息 +func Init(dir string) error { + gDir = dir + // 判断 ctx 是否取消 + checkContextErr := tool.CheckContext(golabl.Ctx) + // 判断 结果 + if checkContextErr != nil { + // 返回 且 返回错误 + return checkContextErr + } + //读取配置文件 + var config _type.Config + dll, initConfigDLLErr := _configDll.InitConfigDLL() + if initConfigDLLErr != nil { + return initConfigDLLErr + } + configJson, ReadConfigFileErr := dll.ReadConfigFile(dir, "config.yaml") + if ReadConfigFileErr != nil { + return fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + golabl.Config = config + return nil +} + +// GetPddClient 获取拼多多配置 +// @return _type.PddConfig 拼多多配置 +// @return error 错误信息 +func GetPddClient() (_type.PddConfig, error) { + //读取配置文件 + var config _type.Config + dll, initConfigDLLErr := _configDll.InitConfigDLL() + if initConfigDLLErr != nil { + return _type.PddConfig{}, initConfigDLLErr + } + configJson, ReadConfigFileErr := dll.ReadConfigFile(gDir, "config.yaml") + if ReadConfigFileErr != nil { + return _type.PddConfig{}, fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return _type.PddConfig{}, fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + return config.PddConfig, nil +} + +// GetFileUrlConfig 获取文件路径配置 +// @return _type.FileUrl 文件路径配置 +// @return error 错误信息 +func GetFileUrlConfig() (_type.FileUrl, error) { + //读取配置文件 + var config _type.Config + dll, initConfigDLLErr := _configDll.InitConfigDLL() + if initConfigDLLErr != nil { + return _type.FileUrl{}, initConfigDLLErr + } + configJson, ReadConfigFileErr := dll.ReadConfigFile(gDir, "config.yaml") + if ReadConfigFileErr != nil { + return _type.FileUrl{}, fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return _type.FileUrl{}, fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + return config.FileUrl, nil +} + +// GetAliveConfig 获取存活状态配置 +// @return _type.Alive 存活状态配置 +// @return error 错误信息 +func GetAliveConfig() (_type.Alive, error) { + //读取配置文件 + var config _type.Config + dll, initConfigDLLErr := _configDll.InitConfigDLL() + if initConfigDLLErr != nil { + return _type.Alive{}, initConfigDLLErr + } + configJson, ReadConfigFileErr := dll.ReadConfigFile(gDir, "config.yaml") + if ReadConfigFileErr != nil { + return _type.Alive{}, fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return _type.Alive{}, fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + return config.Alive, nil +} diff --git a/initialization/cron/cron.go b/initialization/cron/cron.go new file mode 100644 index 0000000..fc4f253 --- /dev/null +++ b/initialization/cron/cron.go @@ -0,0 +1,124 @@ +package cron + +import ( + "fmt" + "planA/modules/logs" + + "github.com/robfig/cron/v3" +) + +// Init 定时器初始化 +func Init() { + c := cron.New(cron.WithSeconds()) // 支持秒级别的精度 + // 每日执行删除sqlite过期记录 + _, delSqlIteErr := c.AddFunc("0 0 0 * * ?", func() { + DeleteOldSkuWatermarkImage() //删除过期的 sku水印图片 + DeleteOldWatermarkImage() //删除过期的水印图片 + DeleteOldExportFile() //删除过期的导出文件 + DeleteOldRedis() //删除 redis中过期数据 + DeleteOldRecords() //删除task_record过期记录 + DeleteOldExport() //删除task_export过期记录 + DeleteZipFile() //删除 zip文件 + DeleteDelTaskAndDelTaskDetails() //删除任务 + }) + if delSqlIteErr != nil { + logs.LoggingMiddleware("error", "定时任务 每日执行删除sqlite过期记录 失败") + return + } + //心跳检测 10秒 + _, heartbeatErr := c.AddFunc("0/10 * * * * ?", func() { + CheckBannedWordSubstitutionUrlAlive() // 违禁词替换心跳 + CheckMysqlAlive() // mysql 心跳 + CheckRedisAlive() // redis 心跳 + CheckPddAlive() // 拼多多心跳 + CheckCreateTaskNoticeUrlAlive() // 创建任务通知心跳 + CheckXyBannedWord() // 闲鱼违禁词 + return + }) + if heartbeatErr != nil { + logs.LoggingMiddleware("error", "定时任务 心跳检测 失败") + return + } + // 60秒钟检测一次 + _, bErr := c.AddFunc("0/60 * * * * ?", func() { + B() + }) + if bErr != nil { + logs.LoggingMiddleware("error", "定时任务 B 函数 启动失败") + return + } + + // 每日执行删除过期日志文件 + _, delLogErr := c.AddFunc("0 0 0 * * ?", func() { + DeleteOldLog("logs\\debug") + DeleteOldLog("logs\\info") + DeleteOldLog("logs\\warning") + DeleteOldLog("logs\\error") + DeleteOldLog("logs\\success") + }) + if delLogErr != nil { + logs.LoggingMiddleware("error", "定时任务 删除过期日志文件 启动失败") + return + } + + // 启动删除任务 + _, executeDelTaskErr := c.AddFunc("0/10 * * * * ?", func() { + ExecuteDelTask() + }) + if executeDelTaskErr != nil { + logs.LoggingMiddleware("error", "定时任务 启动删除任务 启动失败") + return + } + + // 30分钟执行一次 + _, verifyTokenErr := c.AddFunc("0 0/30 * * * ?", func() { + VerifyToken() + }) + if verifyTokenErr != nil { + logs.LoggingMiddleware("error", "定时任务 30分钟执行一次 启动失败") + return + } + + // 删除指定目录的文件夹 + _, delDirFolderErr := c.AddFunc("0 0 0 * * ?", func() { + DeleteKfzTempImg() + }) + if delDirFolderErr != nil { + logs.LoggingMiddleware("error", "定时任务 删除指定目录的文件夹 启动失败") + return + } + + // 五秒执行一次 + _, runFErr := c.AddFunc("0/5 * * * * ?", func() { + runFErr := RunF() + if runFErr != nil { + fmt.Println(runFErr) + logs.LoggingMiddleware("error", "定时任务 启动planF.exe 启动失败") + return + } + }) + if runFErr != nil { + logs.LoggingMiddleware("error", "定时任务 启动planF.exe 启动失败") + return + } + + //// 备份 body_backup到硬盘 - 每分钟执行一次,使用锁防止并发(挪到C.exe) + //_, backupBodyBackupErr := c.AddFunc("0 * * * * ?", func() { + // BackupBodyBackup() + //}) + //if backupBodyBackupErr != nil { + // logs.LoggingMiddleware("error", "定时任务 备份 body_backup到硬盘 启动失败") + // return + //} + // + //// 每天上午9点压缩昨天csv文件(挪到C.exe) + //_, zipBackupFileErr := c.AddFunc("0 0 9 * * ?", func() { + // ZipBackupFile() + //}) + //if zipBackupFileErr != nil { + // logs.LoggingMiddleware("error", "定时任务 zipBackupFile 启动失败") + // return + //} + + c.Start() // 启动调度器(非阻塞) +} diff --git a/initialization/cron/task.go b/initialization/cron/task.go new file mode 100644 index 0000000..e037098 --- /dev/null +++ b/initialization/cron/task.go @@ -0,0 +1,841 @@ +package cron + +import ( + "archive/zip" + "encoding/csv" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "planA/controlState/serviceAlive" + "planA/controller" + "planA/initialization/config" + "planA/initialization/golabl" + "planA/modules/logs" + "planA/modules/pdd" + "planA/rep" + "planA/service" + "planA/service/mysql" + "planA/tool" + toolPdd "planA/tool/pdd" + "planA/tool/process" + _type "planA/type" + "regexp" + "strings" + "time" +) + +// DeleteOldExportFile 删除N天前的导出文件 +func DeleteOldExportFile() { + read := rep.CreateDbFactoryRead() + lite, getTaskExportOldListErr := read.GetTaskExportOldList() + if getTaskExportOldListErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "获取SQLite中N天前的记录失败:"+getTaskExportOldListErr.Error()) + return + } + for _, v := range lite { + removeErr := os.Remove(v.FileUrl) + if removeErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "删除文件失败:"+removeErr.Error()) + continue + } + } +} + +// DeleteOldRecords 删除 task_records 表中N天前的记录 +func DeleteOldRecords() { + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + mysqlDeleteTaskRecordsOldDataErr := mysqlWrite.DeleteTaskRecordsOldData() + if mysqlDeleteTaskRecordsOldDataErr != nil { + errMsg := fmt.Sprintf("删除task_records表中N天前的记录失败: %v", mysqlDeleteTaskRecordsOldDataErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + sqLiteDeleteTaskRecordsOldDataErr := sqliteWrite.DeleteTaskRecordsOldData() + if sqLiteDeleteTaskRecordsOldDataErr != nil { + errMsg := fmt.Sprintf("删除task_records表中N天前的记录失败: %v", sqLiteDeleteTaskRecordsOldDataErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } +} + +// DeleteOldExport 删除 task_export 表中N天前的记录 +func DeleteOldExport() { + mysqlWrite, sqliteWrite := rep.CreateDbFactoryWrite() + mysqlDeleteTaskExportOldDataErr := mysqlWrite.DeleteTaskExportOldData() + if mysqlDeleteTaskExportOldDataErr != nil { + errMsg := fmt.Sprintf("删除task_export表中N天前的记录失败: %v", mysqlDeleteTaskExportOldDataErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + sqliteDeleteTaskExportOldDataErr := sqliteWrite.DeleteTaskExportOldData() + if sqliteDeleteTaskExportOldDataErr != nil { + errMsg := fmt.Sprintf("删除task_export表中N天前的记录失败: %v", sqliteDeleteTaskExportOldDataErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } +} + +// CheckMysqlAlive mysql心跳 +func CheckMysqlAlive() { + //计算心跳时间 + start := time.Now() + mysql.GetTaskRecordsByTaskId("1") + elapsed := time.Since(start) + elapsedMs := int(elapsed.Milliseconds()) //将time.Duration类型转换为int类型的毫秒 + //设置状态 + serviceAlive.SetServiceAlive("mysql", elapsedMs) +} + +// CheckRedisAlive redis心跳 +func CheckRedisAlive() { + //计算心跳时间 + start := time.Now() + service.GetTaskBookPing() + elapsed := time.Since(start) + elapsedMs := int(elapsed.Milliseconds()) //将time.Duration类型转换为int类型的毫秒 + //设置状态 + serviceAlive.SetServiceAlive("redis", elapsedMs) +} + +// CheckPddAlive 拼多多心跳 +func CheckPddAlive() { + token := "" + //获取系统规定拼多多 token + //urlConfig, _ := config.GetFileUrlConfig() + //_, token, HttpGetRequestErr := tool.HttpGetRequest(urlConfig.PddTokenUrl) + //if HttpGetRequestErr != nil { + // logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "获取系统规定拼多多 token失败:"+HttpGetRequestErr.Error()) + // return + //} + + pddDll, initPddSOErr := pdd.InitPddDll() + if initPddSOErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "初始化拼多多dll文件失败:"+initPddSOErr.Error()) + return + } + client, err := config.GetPddClient() + if err != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "获取拼多多配置失败:"+err.Error()) + return + } + + //计算心跳时间 + start := time.Now() + _, pddTimeGetErr := pddDll.PddTimeGet(client.ClientId, client.ClientSecret, token) + if pddTimeGetErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "获取拼多多系统时间失败:"+pddTimeGetErr.Error()) + return + } + elapsed := time.Since(start) + elapsedMs := int(elapsed.Milliseconds()) //将time.Duration类型转换为int类型的毫秒 + //设置状态 + serviceAlive.SetServiceAlive("pdd", elapsedMs) +} + +// CheckCreateTaskNoticeUrlAlive 价软件提交数据通知接口心跳 +func CheckCreateTaskNoticeUrlAlive() { + //计算心跳时间 + start := time.Now() + controller.TaskNoticeRequest("ping") + elapsed := time.Since(start) + elapsedMs := int(elapsed.Milliseconds()) //将time.Duration类型转换为int类型的毫秒 + //设置状态 + serviceAlive.SetServiceAlive("通知取出bodyOver接口", elapsedMs) +} + +// CheckBannedWordSubstitutionUrlAlive 违禁词接口心跳 +func CheckBannedWordSubstitutionUrlAlive() { + + urlConfig, _ := config.GetFileUrlConfig() + bannerWordDataReq := map[string]string{ + "isbn": "9787508618388", + "bookName": "麦迪逊大道之王:大卫·奥格威转", + "author": "[美]肯尼斯·罗曼", + "publisher": "中信出版社", + "shopId": "2029141110649929729", + "replaceMark": "1", + } + //计算心跳时间 + start := time.Now() + tool.HttpBannedWordSubstitution(urlConfig.BannedWordSubstitutionUrl, bannerWordDataReq) + elapsed := time.Since(start) + elapsedMs := int(elapsed.Milliseconds()) //将time.Duration类型转换为int类型的毫秒 + //设置状态 + serviceAlive.SetServiceAlive("违禁词替换接口", elapsedMs) +} + +// CheckXyBannedWord 闲鱼违禁词 +func CheckXyBannedWord() { + url := golabl.Config.FileUrl.XYBannedWordSubstitutionUrl + + //计算心跳时间 + start := time.Now() + // 发送 GET 请求 + resp, err := http.Get(url) + if err != nil { + fmt.Printf("请求失败: %v\n", err) + return + } + defer resp.Body.Close() + + // 读取响应体 + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("读取响应失败: %v\n", err) + return + } + elapsed := time.Since(start) + elapsedMs := int(elapsed.Milliseconds()) //将time.Duration类型转换为int类型的毫秒 + //设置状态 + serviceAlive.SetServiceAlive("闲鱼违禁词", elapsedMs) + + // 解析 JSON + var healthResp _type.HealthResponse + err = json.Unmarshal(body, &healthResp) + if err != nil { + fmt.Printf("解析 JSON 失败: %v\n", err) + return + } + + // 判断 code 是否为 200 + if healthResp.Code != 200 { + serviceAlive.SetServiceAliveWithMsg("闲鱼违禁词", elapsedMs, healthResp.Message) + } +} + +// DeleteOldRedis 删除redisN天前的数据 +func DeleteOldRedis() { + read := rep.CreateDbFactoryRead() + list, getTaskRecordsOldListtErr := read.GetTaskRecordsOldList() + if getTaskRecordsOldListtErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "获取task_export中N天前的记录失败:"+getTaskRecordsOldListtErr.Error()) + return + } + for _, v := range list { + err := service.DelTask(v.TaskId) + if err != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "删除任务失败:"+err.Error()) + continue + } + + } +} + +// B 程序守护 +func B() { + read := rep.CreateDbFactorySqliteRead() + //查询task_records中24小时内的所有数据 + records, getTaskRecords24HourErr := read.GetTaskRecords24Hour() + if getTaskRecords24HourErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "获取所有任务记录失败:"+getTaskRecords24HourErr.Error()) + return + } + for _, v := range records { + //获取 header 信息 + header, getTaskHeaderErr := service.GetTaskHeader(v.TaskId) + if getTaskHeaderErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "获取header 信息失败:"+getTaskHeaderErr.Error()) + continue + } + // 不能是淘宝的 + if header.Status != 0 && header.ShopType != "6" { + // 启动 B程序 + _, runTaskWorkerErr := process.RunTaskWorker(v.TaskId) + if runTaskWorkerErr != nil { + //logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "启动B程序失败:"+runTaskWorkerErr.Error()) + continue + } + fmt.Println("守护进程成功启动任务B程序的窗口 任务ID:" + v.TaskId) + } + } +} + +// DeleteOldLog 删除日志N天以上的日志文件 +func DeleteOldLog(dir string) { + // 配置参数 + pattern := `^[^-]+-[a-z]+-(\d{4}-\d{2}-\d{2})(?:-\d{2})?\.log$` // 匹配两种格式:ERROR-task-2026-03-23-04.log 和 ERROR-task-2026-03-23.log + retentionDays := 3 // 保留天数 + + // 计算截止时间 + cutoffTime := time.Now().AddDate(0, 0, -retentionDays) + + // 编译正则表达式 + regex, err := regexp.Compile(pattern) + if err != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("无效的正则表达式模式: %v", err)) + return + } + + // 遍历目录 + entries, err := os.ReadDir(dir) + if err != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("无法读取目录: %v", err)) + return + } + + var cleanedCount int + var errors []string + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + filename := entry.Name() + + // 检查文件名是否匹配模式并提取日期 + matches := regex.FindStringSubmatch(filename) + if len(matches) != 2 { + continue + } + + // 解析文件名中的日期 + dateStr := matches[1] // 格式: 2026-03-23 + fileDate, err := time.Parse("2006-01-02", dateStr) + if err != nil { + errors = append(errors, fmt.Sprintf("解析日期失败 %s: %v", filename, err)) + continue + } + + // 检查文件日期是否早于截止时间 + if fileDate.Before(cutoffTime) { + filePath := filepath.Join(dir, filename) + // 删除文件 + if err := os.Remove(filePath); err != nil { + errors = append(errors, fmt.Sprintf("删除失败 %s: %v", filename, err)) + } else { + cleanedCount++ + } + } + } + + // 输出清理结果 + if cleanedCount > 0 || len(errors) > 0 { + logs.LoggingMiddleware(logs.LOG_LEVEL_INFO, fmt.Sprintf("清理完成: 删除了 %d 个文件", cleanedCount)) + } + + if len(errors) > 0 { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("清理过程中遇到 %d 个错误", len(errors))) + for _, errMsg := range errors { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + } + } +} + +// DeleteOldWatermarkImage 删除N天以上的水印图片 +func DeleteOldWatermarkImage() { + // 目标根目录(你提供的目录) + rootDir := `img\watermark` + + // 计算需要删除的截止时间:当前时间 - N天 + days := golabl.Config.Server.DataDay + expireTime := time.Now().AddDate(0, 0, -days) + + // 遍历根目录 + err := filepath.Walk(rootDir, func(path string, f os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("无法访问目录 %s: %v", path, err) + } + + // 只处理一级子文件夹(不递归) + if path == rootDir { + return nil + } + if !f.IsDir() { + return nil + } + + // 解析文件夹名称为日期(格式:2006-01-02) + dirName := f.Name() + dirTime, err := time.Parse("2006-01-02", dirName) + if err != nil { + // 不是日期格式的文件夹跳过 + return nil + } + + // 判断是否超过N天 + if dirTime.Before(expireTime) { + // 删除文件夹(包括里面所有内容) + err := os.RemoveAll(path) + if err != nil { + return fmt.Errorf("无法删除目录 %s: %v", path, err) + } + } + + // 只处理一级子目录,不递归深入 + return filepath.SkipDir + }) + if err != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("删除N天以上的水印图片失败: %v", err.Error())) + } +} + +// DeleteOldSkuWatermarkImage 删除N天以上的sku水印图片 +func DeleteOldSkuWatermarkImage() { + // 目标根目录(你提供的目录) + rootDir := `img\skuwatermark` + + // 计算需要删除的截止时间:当前时间 - N天 + days := golabl.Config.Server.DataDay + expireTime := time.Now().AddDate(0, 0, -days) + + // 遍历根目录 + err := filepath.Walk(rootDir, func(path string, f os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("无法访问目录 %s: %v", path, err) + } + + // 只处理一级子文件夹(不递归) + if path == rootDir { + return nil + } + if !f.IsDir() { + return nil + } + + // 解析文件夹名称为日期(格式:2006-01-02) + dirName := f.Name() + dirTime, err := time.Parse("2006-01-02", dirName) + if err != nil { + // 不是日期格式的文件夹跳过 + return nil + } + + // 判断是否超过N天 + if dirTime.Before(expireTime) { + // 删除文件夹(包括里面所有内容) + err := os.RemoveAll(path) + if err != nil { + return fmt.Errorf("无法删除目录 %s: %v", path, err) + } + } + + // 只处理一级子目录,不递归深入 + return filepath.SkipDir + }) + if err != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("删除N天以上的水印图片失败: %v", err.Error())) + } +} + +// BackupBodyBackup 备份 body_backup到硬盘 +func BackupBodyBackup() { + + // 定义导出目录 + csvUrl := golabl.Config.FileUrl.BackupUrl + fmt.Println("路径:" + csvUrl) + // 获取所有任务数据 + read := rep.CreateDbFactoryRead() + list, getAllTaskErr := read.GetAllTask() + if getAllTaskErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "获取所有任务数据失败:"+getAllTaskErr.Error()) + return + } + + for _, v := range list { + + dateDir := v.CreateAt.Format("2006-01-02") // 按日期分组 + // 构建完整的目录路径 + taskCsvUrl := filepath.Join(csvUrl, dateDir) + + // 获取 backup数据长度 + backupLen, getBodyBackupLenErr := service.GetBodyBackupLen(v.TaskId) + if getBodyBackupLenErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("获取任务 %s 的backup长度失败:%v", v.TaskId, getBodyBackupLenErr)) + continue // 跳过当前任务,继续处理下一个 + } + if backupLen == 0 { + // 如果 backup中没有数据则跳过 + continue + } + + csvFileName := fmt.Sprintf("%v.csv", v.TaskId) + // 检查并创建目录(如果不存在) + err := os.MkdirAll(taskCsvUrl, 0755) + if err != nil { + errMsg := fmt.Sprintf("创建目录失败: %v", err) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return + } + + // 拼接完整的文件路径 + fullPath := filepath.Join(taskCsvUrl, csvFileName) + + // 判断文件是否存在,决定是创建新文件还是追加写入 + var file *os.File + if _, err := os.Stat(fullPath); err == nil { + // 文件存在,以追加模式打开 + file, err = os.OpenFile(fullPath, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + errMsg := fmt.Sprintf("打开文件失败: %v", err) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + continue + } + } else if os.IsNotExist(err) { + // 文件不存在,创建新文件 + file, err = os.Create(fullPath) + if err != nil { + errMsg := fmt.Sprintf("创建文件失败: %v", err) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + continue + } + } else { + // 其他错误(如权限问题) + errMsg := fmt.Sprintf("检查文件状态失败: %v", err) + fmt.Println(errMsg) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + continue + } + defer file.Close() + + // 创建 CSV写入器 + writer := csv.NewWriter(file) + defer writer.Flush() + + // 循环获取并写入数据 + for i := 0; i < int(backupLen); i++ { + // 获取 backup数据 + one, getBodyBackupOneErr := service.GetBodyBackupOne(v.TaskId) + if getBodyBackupOneErr != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("获取任务 %s 的body_backup数据失败:%v", v.TaskId, getBodyBackupOneErr)) + break // 跳出当前循环,继续下一个任务 + } + + // 将数据写入到CSV文件的一行(A列) + // 假设 one 是字符串类型,如果是结构体需要根据实际字段调整 + record := []string{one} // 如果 one 不是字符串,需要转换,例如 fmt.Sprintf("%v", one) + + if err := writer.Write(record); err != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("写入CSV数据失败:%v", err)) + break + } + } + + logs.LoggingMiddleware(logs.LOG_LEVEL_INFO, fmt.Sprintf("任务 %s 的数据已成功写入文件:%s", v.TaskId, fullPath)) + } +} + +// ZipBackupFile 压缩backup文件 +func ZipBackupFile() { + csvUrl := golabl.Config.FileUrl.BackupUrl + // 获取昨天的日期 + yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") + // 拼接完整的文件路径 + taskCsvUrl := filepath.Join(csvUrl, yesterday) + + // 检查源目录是否存在 + srcInfo, err := os.Stat(taskCsvUrl) + if os.IsNotExist(err) { + log.Printf("目录不存在: %s", taskCsvUrl) + return + } + + // 如果不是目录,则直接返回 + if !srcInfo.IsDir() { + log.Printf("路径不是目录: %s", taskCsvUrl) + return + } + + // 创建zip文件 + zipFileName := taskCsvUrl + ".zip" + zipFile, err := os.Create(zipFileName) + if err != nil { + log.Printf("创建zip文件失败: %v", err) + return + } + defer zipFile.Close() + + // 创建zip writer + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + // 遍历目录并添加文件 + err = filepath.Walk(taskCsvUrl, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // 跳过目录本身 + if info.IsDir() { + return nil + } + + // 获取相对路径作为zip内的文件名 + relPath, err := filepath.Rel(taskCsvUrl, path) + if err != nil { + return err + } + + // 创建zip中的文件头 + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name = filepath.Join(yesterday, relPath) // 保留目录结构 + header.Method = zip.Deflate + + // 创建zip中的文件 + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + // 打开并复制源文件 + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(writer, file) + return err + }) + + if err != nil { + log.Printf("压缩失败: %v", err) + os.Remove(zipFileName) // 删除不完整的zip文件 + return + } + + log.Printf("压缩成功: %s", zipFileName) + + // 压缩成功后删除原目录 + err = os.RemoveAll(taskCsvUrl) + if err != nil { + log.Printf("删除原目录失败: %v", err) + return + } + log.Printf("成功删除原目录: %s", taskCsvUrl) +} + +// DeleteZipFile 删除zip文件 +func DeleteZipFile() { + zipDir := golabl.Config.FileUrl.BackupUrl + day := golabl.Config.Server.DataDay + + // 获取当前时间 + now := time.Now() + // 计算截止时间(当天0点减去指定天数) + cutoffTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -day) + + // 读取目录 + entries, err := os.ReadDir(zipDir) + if err != nil { + fmt.Printf("读取目录失败: %v\n", err) + return + } + + deletedCount := 0 + for _, entry := range entries { + if entry.IsDir() { + continue + } + + fileName := entry.Name() + if !strings.HasSuffix(fileName, ".zip") { + continue + } + + // 从文件名解析日期(格式:2026-04-10.zip) + dateStr := strings.TrimSuffix(fileName, ".zip") + fileDate, err := time.Parse("2006-01-02", dateStr) + if err != nil { + // 如果文件名不符合日期格式,跳过 + continue + } + + // 如果文件日期早于或等于截止时间,则删除 + if !fileDate.After(cutoffTime) { + filePath := filepath.Join(zipDir, fileName) + err := os.Remove(filePath) + if err != nil { + fmt.Printf("删除文件失败 %s: %v\n", filePath, err) + } else { + fmt.Printf("已删除文件: %s\n", filePath) + deletedCount++ + } + } + } + + fmt.Printf("共删除 %d 个%d天前的zip文件\n", deletedCount, day) +} + +// ExecuteDelTask 查询del_task 表中待执行 +func ExecuteDelTask() { + delTask, err := mysql.GetDelTask() + if err != nil { + fmt.Println("查询del_task 表中待执行失败:", err) + } + for _, v := range delTask { + //如果是暂停中的任务,将任务状态修改为执行中 + if *v.Status == 2 { + // 要求 v.PauseAt 不能等于 nil 并且 v.PauseAt 必须大于当前时间24小时 + if v.PauseAt != nil && !time.Now().After(v.PauseAt.Add(24*time.Hour)) { + continue + } + //修改任务状态 + err := mysql.UpdateDelTaskStatus(v.ID) + if err != nil { + fmt.Println("修改任务状态失败:", err) + continue + } + } + //启动任务 + _, err := process.RunDprogram(*v.TaskID) + if err != nil { + fmt.Println("启动任务失败:", err) + continue + } + } +} + +// DeleteDelTaskAndDelTaskDetails 清理删除任务与删除任务详情过期的数据 +func DeleteDelTaskAndDelTaskDetails() { + defer func() { + if r := recover(); r != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("DeleteDelTaskAndDelTaskDetails panic: %v", r)) + } + }() + + task, getExpiredDelTaskErr := mysql.GetExpiredDelTask() + if getExpiredDelTaskErr != nil { + errMsg := fmt.Sprintf("查询过期的删除任务失败:%v", getExpiredDelTaskErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + fmt.Println(errMsg) + return + } + + for _, v := range task { + // 检查必要字段是否为 nil + if v.TaskType == nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_WARNING, fmt.Sprintf("任务记录中 TaskType 为 nil,跳过处理 ID: %d", v.ID)) + continue + } + + if v.TaskID == nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_WARNING, fmt.Sprintf("任务记录中 TaskID 为 nil,跳过处理 ID: %d", v.ID)) + continue + } + + // 处理任务类型 2 或 3 + if *v.TaskType == 2 || *v.TaskType == 3 { + // 删除 header 与 footer + delTaskErr := service.DelTask(*v.TaskID) + if delTaskErr != nil { + errMsg := fmt.Sprintf("删除任务失败 TaskID: %s, Error: %v", *v.TaskID, delTaskErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + fmt.Println(errMsg) + // 注意:这里用 continue 而不是 return,避免一个任务失败影响其他任务 + continue + } + } + + // 删除删除任务明细表 + deleteDelTaskDetailErr := mysql.DeleteDelTaskDetail(*v.TaskID) + if deleteDelTaskDetailErr != nil { + errMsg := fmt.Sprintf("删除删除任务明细表失败 TaskID: %s, Error: %v", *v.TaskID, deleteDelTaskDetailErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + fmt.Println(errMsg) + continue + } + + // 删除任务 + deleteDelTaskByIdErr := mysql.DeleteDelTaskById(v.ID) + if deleteDelTaskByIdErr != nil { + errMsg := fmt.Sprintf("删除任务失败 ID: %d, Error: %v", v.ID, deleteDelTaskByIdErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + fmt.Println(errMsg) + continue + } + + logs.LoggingMiddleware(logs.LOG_LEVEL_INFO, fmt.Sprintf("成功清理任务 ID: %d, TaskID: %s", v.ID, *v.TaskID)) + } +} + +// VerifyToken 验证token过期 +func VerifyToken() { + list, getPddTokenListErr := service.GetPddTokenList() + if getPddTokenListErr != nil { + fmt.Println("获取token列表失败:", getPddTokenListErr) + return + } + pddDll, initPddSOErr := pdd.InitPddDll() + if initPddSOErr != nil { + fmt.Println("初始化pdd.so失败:", initPddSOErr) + return + } + for _, v := range list { + //使用类目预测接口测试Token 是否有效 + buildPddGoodsOuterCatMappingGetErr := toolPdd.BuildPddGoodsOuterCatMappingGet(pddDll, v.Token) + if buildPddGoodsOuterCatMappingGetErr != nil { + if buildPddGoodsOuterCatMappingGetErr.Error() == "拼多多Token已过期" { + //fmt.Printf("token 过期的店铺 %v 店铺id %v\n", v.ShopName, v.ID) + reqData := map[string]string{ + "shopId": v.ID, + } + _, submitFormDataErr := tool.SubmitFormData(golabl.Config.FileUrl.UpdateTokenUrl, reqData) + if submitFormDataErr != nil { + fmt.Println("提交表单数据失败:", submitFormDataErr) + return + } + } + } + } +} + +// DeleteKfzTempImg 删除本地临时的孔夫子图片 +func DeleteKfzTempImg() { + err := tool.CleanOldFolders(golabl.Config.FileUrl.KfzImgTempUrl, 3) + if err != nil { + fmt.Println("删除本地临时的孔夫子图片失败:", err) + } +} + +/////////////////*********检查F程序启动***********///////////////////////// + +func RunF() error { + exeName := golabl.Config.FileUrl.FFileName + running, err := isProcessRunning(exeName) + if err != nil { + return fmt.Errorf("检查进程状态出错: %v\n", err) + } + + if !running { + runFProgramErr := process.RunFProgram() + if runFProgramErr != nil { + return runFProgramErr + } + } + return nil +} +func isProcessRunning(exePath string) (bool, error) { + exeName := filepath.Base(exePath) + + // 方法1:使用 tasklist(适用于Windows) + cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("IMAGENAME eq %s", exeName), "/NH", "/FO", "CSV") + output, err := cmd.Output() + if err != nil { + // 检查是否是"未找到进程"的错误 + if exitErr, ok := err.(*exec.ExitError); ok { + if exitErr.ExitCode() == 1 { + return false, nil + } + } + return false, err + } + + outputStr := string(output) + // 如果输出包含进程名且不包含"No tasks",则认为进程在运行 + return strings.Contains(outputStr, exeName) && + !strings.Contains(outputStr, "INFO: No tasks"), nil +} + +/////////////////*********检查F程序启动***********///////////////////////// diff --git a/initialization/golabl/golabl.go b/initialization/golabl/golabl.go new file mode 100644 index 0000000..9d16885 --- /dev/null +++ b/initialization/golabl/golabl.go @@ -0,0 +1,28 @@ +package golabl + +import ( + "context" + "database/sql" + _type "planA/type" + "time" + + "github.com/go-playground/validator/v10" + "github.com/go-redis/redis/v8" + "github.com/gorilla/mux" + "gorm.io/gorm" +) + +var ( + Ctx context.Context + Config _type.Config + MysqlDb *gorm.DB + PsiMysqlDb *gorm.DB + RedisDbA *redis.Client + RedisDbB *redis.Client + RedisDbC *redis.Client + RedisDbD *redis.Client + SqliteDb *sql.DB + Router = mux.NewRouter() + RedisExp = time.Duration(Config.Server.RedisExp) * time.Hour + Validator *validator.Validate +) diff --git a/initialization/init.go b/initialization/init.go new file mode 100644 index 0000000..3ae9de0 --- /dev/null +++ b/initialization/init.go @@ -0,0 +1,102 @@ +package initialization + +import ( + "context" + "fmt" + "log" + "net/http" + "planA/initialization/c" + "planA/initialization/config" + "planA/initialization/cron" + "planA/initialization/golabl" + "planA/initialization/middle" + "planA/initialization/mysql" + "planA/initialization/redis" + "planA/initialization/router" + "planA/initialization/sqLite" + "planA/initialization/validator" +) + +func Init() error { + //初始化上下文 + golabl.Ctx = context.Background() + // 初始化配置 + configErr := config.Init("") + if configErr != nil { + return fmt.Errorf("初始化配置失败: %v", configErr) + } + // 初始化 mysql + mysqlErr := mysql.Init() + if mysqlErr != nil { + return fmt.Errorf("初始化mysql失败: %v", mysqlErr) + } + // 初始化 redis + redisErr := redis.Init() + if redisErr != nil { + return fmt.Errorf("初始化redis失败: %v", redisErr) + } + // 初始化 sqlite + sqliteErr := sqLite.Init() + if sqliteErr != nil { + return fmt.Errorf("初始化sqlite失败: %v", sqliteErr) + } + // 初始化验证器 + validator.Init() + // 初始化定时任务(非阻塞,因此不需要返回错误) + cron.Init() + //初始化中间件 + middle.Init() + //初始化路由 + router.Init() + //运行 C程序 + c.RunC() + return nil + +} + +// Server 启动服务 +func Server() { + // 从配置获取端口并启动服务 + port := ":" + golabl.Config.Server.Port + fmt.Printf("服务器启动在 http://localhost%s\n", port) + // 打印所有可用端点(控制台输出) + printAvailableEndpoints() + // 启动HTTP服务,如果失败则记录致命错误 + log.Fatal(http.ListenAndServe(port, golabl.Router)) +} + +// printAvailableEndpoints 打印所有可用的API端点 +func printAvailableEndpoints() { + fmt.Println("\n========== 可用API端点 ==========") + + fmt.Println("\n【任务管理】") + fmt.Println(" POST /task/create - 创建新任务") + fmt.Println(" GET /task/pause/{id} - 暂停任务") + fmt.Println(" GET /task/resume/{id} - 恢复任务") + fmt.Println(" GET /task/stop/{id} - 停止任务") + fmt.Println(" GET /task/over/{id} - 完成任务") + fmt.Println(" GET /task/get - 获取任务列表(支持查询参数)") + fmt.Println(" GET /task/getByUserId - 根据用户ID获取任务") + fmt.Println(" POST /task/setTaskBody - 设置任务内容") + fmt.Println(" GET /task/b - 运行B程序") + + fmt.Println("\n【任务导出】") + fmt.Println(" GET /task/export/exportTaskDetail/{id} - 导出指定任务详情") + fmt.Println(" GET /task/export/exportTaskDetail/{userId}/{id} - 导出指定用户的指定任务详情") + fmt.Println(" GET /task/export/get - 获取所有导出任务列表") + fmt.Println(" GET /task/export/get/{userId} - 获取指定用户的导出任务列表") + + fmt.Println("\n【商品任务】") + fmt.Println(" POST /task/goods/add - 添加商品任务") + fmt.Println(" GET /task/goods/get/{id} - 获取指定商品任务详情") + fmt.Println(" PUT /task/goods/set/{id} - 更新指定商品任务") + fmt.Println(" DELETE /task/goods/del/{id} - 删除指定商品任务") + + fmt.Println("\n【系统工具】") + fmt.Println(" GET /alive/get - 获取服务存活状态列表") + fmt.Println(" GET /health - 健康检查") + fmt.Println(" GET /export/ - 导出文件下载服务") + fmt.Println(" GET / - 服务欢迎页") + + fmt.Println("\n=====================================") +} diff --git a/initialization/middle/cors.go b/initialization/middle/cors.go new file mode 100644 index 0000000..c4276a0 --- /dev/null +++ b/initialization/middle/cors.go @@ -0,0 +1,25 @@ +package middle + +import ( + "net/http" +) + +// Cors 跨域中间件 +func Cors(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 设置CORS头 + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH") + + // 处理OPTIONS请求 + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusNoContent) // 204 + return + } + + // 处理正常请求 + next.ServeHTTP(w, r) + }) +} diff --git a/initialization/middle/logs.go b/initialization/middle/logs.go new file mode 100644 index 0000000..1570518 --- /dev/null +++ b/initialization/middle/logs.go @@ -0,0 +1,378 @@ +package middle + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net" + "net/http" + _myLogs "planA/modules/logs" + "strings" + "time" +) + +// LoggingMiddleware 中间自动记录 +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 初始化日志 + if err := _myLogs.InitializeLogger("logs"); err != nil { + return + } + + if err := _myLogs.SetLogTaskType("task"); err != nil { + return + } + + // 记录请求开始时间 + startTime := time.Now() + + // 收集基本信息 + clientIP := getClientIP(r) + userAgent := r.UserAgent() + referer := r.Referer() + + // 记录请求信息(但不立即打印,等待收集完数据) + baseMsg := fmt.Sprintf( + "Request: %s %s | ClientIP: %s | User-Agent: %s | Referer: %s", + r.Method, + r.URL.Path, + clientIP, + userAgent, + referer, + ) + + // 处理不同类型的请求数据 + var requestData string + var requestBody []byte + + // 如果是需要记录数据的请求方法 + if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" { + contentType := r.Header.Get("Content-Type") + contentLength := r.ContentLength + + // 根据 Content-Type 处理不同的数据格式 + if strings.Contains(contentType, "multipart/form-data") { + // 处理 multipart/form-data + requestData = processMultipartFormData(r, baseMsg) + } else if strings.Contains(contentType, "application/x-www-form-urlencoded") { + // 处理表单数据 + requestData = processFormData(r) + } else if strings.Contains(contentType, "application/json") || + strings.Contains(contentType, "text/plain") || + strings.Contains(contentType, "application/xml") { + // 处理 JSON、文本等 + requestData = processBodyData(r, &requestBody) + } else { + // 其他类型 + requestData = fmt.Sprintf("Content-Type: %s, Content-Length: %d", contentType, contentLength) + } + } else if r.Method == "GET" || r.Method == "HEAD" { + // GET 请求参数 + requestData = fmt.Sprintf("Query: %s", r.URL.RawQuery) + } + + // 组合完整的日志消息 + fullMsg := baseMsg + if requestData != "" { + fullMsg += " | " + requestData + } + + // 记录请求头信息(可选) + headers := []string{"Authorization", "Accept", "Accept-Encoding"} + for _, header := range headers { + if value := r.Header.Get(header); value != "" { + fullMsg += fmt.Sprintf(" | %s: %s", header, sanitizeHeaderValue(header, value)) + } + } + + // 记录请求信息 + if err := _myLogs.LogInfo(fullMsg); err != nil { + return + } + + // 如果读取了请求体,需要恢复它 + if requestBody != nil { + // 重新设置请求体 + r.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + } + + // 使用 ResponseWriter 包装器 + crw := &captureResponseWriter{ + ResponseWriter: w, + statusCode: 200, + } + + // 处理请求 + next.ServeHTTP(crw, r) + + // 记录响应信息 + duration := time.Since(startTime) + responseMsg := fmt.Sprintf( + "Response: %s %s | Status: %d | Duration: %v | Size: %d", + r.Method, + r.URL.Path, + crw.statusCode, + duration, + crw.size, + ) + + _myLogs.LogInfo(responseMsg) + }) +} + +// 处理 multipart/form-data +func processMultipartFormData(r *http.Request, baseMsg string) string { + // 保存原始请求体 + body, err := io.ReadAll(r.Body) + if err != nil { + return fmt.Sprintf("Error reading body: %v", err) + } + + // 恢复请求体供后续使用 + r.Body = io.NopCloser(bytes.NewBuffer(body)) + + // 解析 multipart + reader := bytes.NewReader(body) + boundary := extractBoundary(r.Header.Get("Content-Type")) + if boundary == "" { + return fmt.Sprintf("Content-Type: multipart/form-data (no boundary)") + } + + mr := multipart.NewReader(reader, boundary) + + var formData []string + sensitiveFields := []string{"password", "token", "secret", "key", "file"} + + for { + part, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + formData = append(formData, fmt.Sprintf("Parse error: %v", err)) + break + } + + fieldName := part.FormName() + fileName := part.FileName() + + if fileName != "" { + // 这是文件上传 + formData = append(formData, fmt.Sprintf("%s: [FILE] %s (size unknown)", fieldName, fileName)) + } else { + // 这是普通字段 + partData, _ := io.ReadAll(part) + value := string(partData) + + // 检查是否是敏感字段 + isSensitive := false + for _, sensitive := range sensitiveFields { + if strings.Contains(strings.ToLower(fieldName), sensitive) { + isSensitive = true + break + } + } + + if isSensitive { + formData = append(formData, fmt.Sprintf("%s: [FILTERED]", fieldName)) + } else { + // 限制长度,避免日志过大 + if len(value) > 100 { + value = value[:100] + "...(truncated)" + } + formData = append(formData, fmt.Sprintf("%s: %s", fieldName, value)) + } + } + part.Close() + } + + if len(formData) > 0 { + return fmt.Sprintf("FormData: %s", strings.Join(formData, ", ")) + } + + return "FormData: (empty)" +} + +// 从 Content-Type 提取 boundary +func extractBoundary(contentType string) string { + parts := strings.Split(contentType, "boundary=") + if len(parts) > 1 { + return strings.Trim(parts[1], "\" ;") + } + return "" +} + +// 处理普通表单数据 +func processFormData(r *http.Request) string { + // 复制请求以便解析 + r2 := r.Clone(r.Context()) + if err := r2.ParseForm(); err != nil { + return fmt.Sprintf("Form parse error: %v", err) + } + + var formData []string + sensitiveFields := []string{"password", "token", "secret", "key"} + + for key, values := range r2.Form { + isSensitive := false + for _, sensitive := range sensitiveFields { + if strings.Contains(strings.ToLower(key), sensitive) { + isSensitive = true + break + } + } + + if isSensitive { + formData = append(formData, fmt.Sprintf("%s: [FILTERED]", key)) + } else { + valueStr := strings.Join(values, ",") + if len(valueStr) > 100 { + valueStr = valueStr[:100] + "...(truncated)" + } + formData = append(formData, fmt.Sprintf("%s: %s", key, valueStr)) + } + } + + if len(formData) > 0 { + return fmt.Sprintf("Form: %s", strings.Join(formData, ", ")) + } + + return "Form: (empty)" +} + +// 处理请求体数据(JSON、文本等) +func processBodyData(r *http.Request, bodyBuf *[]byte) string { + contentType := r.Header.Get("Content-Type") + contentLength := r.ContentLength + + // 读取请求体 + body, err := io.ReadAll(r.Body) + if err != nil { + return fmt.Sprintf("Content-Type: %s, Content-Length: %d, Error reading: %v", + contentType, contentLength, err) + } + + // 保存到缓冲区,以便后续恢复 + *bodyBuf = body + + // 如果是 JSON,尝试美化输出 + if strings.Contains(contentType, "application/json") && len(body) > 0 { + var js map[string]interface{} + if err := json.Unmarshal(body, &js); err == nil { + // 过滤敏感字段 + js = sanitizeJSON(js) + + // 转换为字符串,限制长度 + jsonStr, _ := json.Marshal(js) + if len(jsonStr) > 200 { + jsonStr = jsonStr[:200] + return fmt.Sprintf("JSON: %s...(truncated)", string(jsonStr)) + } + return fmt.Sprintf("JSON: %s", string(jsonStr)) + } + } + + // 普通文本 + if len(body) > 0 { + // 限制长度 + bodyStr := string(body) + if len(bodyStr) > 200 { + bodyStr = bodyStr[:200] + "...(truncated)" + } + return fmt.Sprintf("Body: %s", bodyStr) + } + + return fmt.Sprintf("Content-Type: %s, Content-Length: %d", contentType, contentLength) +} + +// 过滤 JSON 中的敏感字段 +func sanitizeJSON(data map[string]interface{}) map[string]interface{} { + sensitiveFields := []string{"password", "token", "secret", "key", "creditCard", "ssn"} + + for key, value := range data { + keyLower := strings.ToLower(key) + + // 检查是否是敏感字段 + isSensitive := false + for _, sensitive := range sensitiveFields { + if strings.Contains(keyLower, sensitive) { + isSensitive = true + break + } + } + + if isSensitive { + data[key] = "[FILTERED]" + } else if subMap, ok := value.(map[string]interface{}); ok { + // 递归处理嵌套对象 + data[key] = sanitizeJSON(subMap) + } else if arr, ok := value.([]interface{}); ok { + // 处理数组 + for i, item := range arr { + if subMap, ok := item.(map[string]interface{}); ok { + arr[i] = sanitizeJSON(subMap) + } + } + } + } + + return data +} + +// 获取客户端真实 IP(保持不变) +func getClientIP(r *http.Request) string { + if ip := r.Header.Get("X-Forwarded-For"); ip != "" { + return strings.Split(ip, ",")[0] + } + if ip := r.Header.Get("X-Real-IP"); ip != "" { + return ip + } + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + return ip +} + +// 清理头信息(保持不变) +func sanitizeHeaderValue(header, value string) string { + if strings.ToLower(header) == "authorization" { + parts := strings.Split(value, " ") + if len(parts) > 1 { + return parts[0] + " [FILTERED]" + } + return "[FILTERED]" + } + return value +} + +// ResponseWriter 包装器(保持不变) +type captureResponseWriter struct { + http.ResponseWriter + statusCode int + size int64 + wroteHeader bool +} + +func (crw *captureResponseWriter) WriteHeader(statusCode int) { + if !crw.wroteHeader { + crw.statusCode = statusCode + crw.wroteHeader = true + crw.ResponseWriter.WriteHeader(statusCode) + } +} + +func (crw *captureResponseWriter) Write(b []byte) (int, error) { + if !crw.wroteHeader { + crw.WriteHeader(http.StatusOK) + } + n, err := crw.ResponseWriter.Write(b) + crw.size += int64(n) + return n, err +} + +func (crw *captureResponseWriter) Unwrap() http.ResponseWriter { + return crw.ResponseWriter +} diff --git a/initialization/middle/middle.go b/initialization/middle/middle.go new file mode 100644 index 0000000..d359a3f --- /dev/null +++ b/initialization/middle/middle.go @@ -0,0 +1,11 @@ +package middle + +import ( + "planA/initialization/golabl" +) + +// Init 初始化中间件 +func Init() { + golabl.Router.Use(Cors) //跨域 + golabl.Router.Use(Response) //响应 +} diff --git a/initialization/middle/response.go b/initialization/middle/response.go new file mode 100644 index 0000000..6a53b1a --- /dev/null +++ b/initialization/middle/response.go @@ -0,0 +1,12 @@ +package middle + +import "net/http" + +func Response(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 全局设置 Content-Type 为 application/json + w.Header().Set("Content-Type", "application/json") + // 调用下一个处理函数(核心业务逻辑) + next.ServeHTTP(w, r) + }) +} diff --git a/initialization/middle/sign.go b/initialization/middle/sign.go new file mode 100644 index 0000000..e87e572 --- /dev/null +++ b/initialization/middle/sign.go @@ -0,0 +1,66 @@ +package middle + +import ( + "bytes" + "io" + "mime/multipart" + "net/http" + "planA/tool" + "strings" +) + +func Sign(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 1. 读取请求体并备份 + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "读取请求失败", http.StatusBadRequest) + return + } + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + // 2. 获取 boundary + var boundary string + ct := r.Header.Get("Content-Type") + if strings.Contains(ct, "multipart/form-data") { + for _, p := range strings.Split(ct, ";") { + p := strings.TrimSpace(p) + if strings.HasPrefix(p, "boundary=") { + boundary = strings.TrimPrefix(p, "boundary=") + break + } + } + } + + // 3. 解析所有字段,同名字段拼接成一个字符串(验签用) + paramMap := make(map[string]string) + if boundary != "" { + reader := multipart.NewReader(bytes.NewBuffer(bodyBytes), boundary) + for { + part, err := reader.NextPart() + if err != nil { + break + } + name := part.FormName() + val, _ := io.ReadAll(part) + // 同名字段拼接(关键!!!) + paramMap[name] += string(val) + part.Close() + } + } + + // 4. 验签 + sign := paramMap["sign"] + if sign == "" { + tool.Error(w, "签名不能为空", http.StatusBadRequest) + return + } + if !tool.VerifySign(paramMap) { + tool.Error(w, "签名失败", http.StatusBadRequest) + return + } + + // 5. 放行 + next.ServeHTTP(w, r) + }) +} diff --git a/initialization/mysql/mysql.go b/initialization/mysql/mysql.go new file mode 100644 index 0000000..8aab2e0 --- /dev/null +++ b/initialization/mysql/mysql.go @@ -0,0 +1,175 @@ +package mysql + +import ( + "fmt" + "planA/initialization/golabl" + "time" + + mysqlModle "planA/type/mysql" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// Init 初始化数据库连接 +// @return error 错误信息 +func Init() error { + //mysql + mysqlDBInitErr := mysqlDBInit() + if mysqlDBInitErr != nil { + return mysqlDBInitErr + } + //psiMysqlInit + psiMysqlInitErr := psiMysqlInit() + if psiMysqlInitErr != nil { + return psiMysqlInitErr + } + return nil +} + +// mysqlDBInit +func mysqlDBInit() error { + + // 1. 获取mysql配置 + mysqlConfig := golabl.Config.MysqlConfig + + // 2. 配置 DSN + dsn := fmt.Sprintf( + "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + mysqlConfig.User, + mysqlConfig.Password, + mysqlConfig.Host, + mysqlConfig.Port, + mysqlConfig.DBName, + ) + + // 3. 配置 GORM 连接选项 + + logLevel := logger.Silent + switch mysqlConfig.Loglevel { + case "info": + logLevel = logger.Info + case "warn": + logLevel = logger.Warn + case "error": + logLevel = logger.Error + case "silent": + logLevel = logger.Silent + } + + gormConfig := &gorm.Config{ + Logger: logger.Default.LogMode(logLevel), //日志级别 + DisableForeignKeyConstraintWhenMigrating: true, //不创建外键约束 + } + + // 4. 连接数据库 + db, openErr := gorm.Open(mysql.Open(dsn), gormConfig) + if openErr != nil { + return openErr + } + + // 5. 获取底层 sql.DB,配置连接池 + sqlDB, dbErr := db.DB() + if dbErr != nil { + return dbErr + } + // 连接池优化 + 保活配置 + sqlDB.SetMaxOpenConns(mysqlConfig.MaxOpenConns) + sqlDB.SetMaxIdleConns(mysqlConfig.MaxIdleConns) + sqlDB.SetConnMaxIdleTime(mysqlConfig.ConnMaxIdleTime * time.Minute) + sqlDB.SetConnMaxLifetime(mysqlConfig.ConnMaxLifetime * time.Hour) + + // 5. 验证连接 + if dbPingErr := sqlDB.Ping(); dbPingErr != nil { + return dbPingErr + } + + // 6. 迁移表结构 + if migrateErr := Migrate(db); migrateErr != nil { + return migrateErr + } + + // 7. 保存db实例 + golabl.MysqlDb = db + return nil +} + +// Migrate 迁移表 +func Migrate(db *gorm.DB) error { + // task_records表 + if err := mysqlModle.MigrateTaskRecords(db); err != nil { + return err + } + // task_export表 + if err := mysqlModle.MigrateTaskExport(db); err != nil { + return err + } + // del_task表 + if err := mysqlModle.MigrateDelTask(db); err != nil { + return err + } + return nil +} + +// psiMysql +func psiMysqlInit() error { + + // 1. 获取mysql配置 + mysqlConfig := golabl.Config.PsiMysqlConfig + + // 2. 配置 DSN + dsn := fmt.Sprintf( + "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + mysqlConfig.User, + mysqlConfig.Password, + mysqlConfig.Host, + mysqlConfig.Port, + mysqlConfig.DBName, + ) + + // 3. 配置 GORM 连接选项 + + logLevel := logger.Silent + switch mysqlConfig.Loglevel { + case "info": + logLevel = logger.Info + case "warn": + logLevel = logger.Warn + case "error": + logLevel = logger.Error + case "silent": + logLevel = logger.Silent + } + + gormConfig := &gorm.Config{ + Logger: logger.Default.LogMode(logLevel), //日志级别 + DisableForeignKeyConstraintWhenMigrating: true, //不创建外键约束 + } + + // 4. 连接数据库 + db, openErr := gorm.Open(mysql.Open(dsn), gormConfig) + if openErr != nil { + return openErr + } + + // 5. 获取底层 sql.DB,配置连接池 + sqlDB, dbErr := db.DB() + if dbErr != nil { + return dbErr + } + // 连接池优化 + 保活配置 + sqlDB.SetMaxOpenConns(mysqlConfig.MaxOpenConns) + sqlDB.SetMaxIdleConns(mysqlConfig.MaxIdleConns) + sqlDB.SetConnMaxIdleTime(mysqlConfig.ConnMaxIdleTime * time.Minute) + sqlDB.SetConnMaxLifetime(mysqlConfig.ConnMaxLifetime * time.Hour) + + // 5. 验证连接 + if dbPingErr := sqlDB.Ping(); dbPingErr != nil { + return dbPingErr + } + + // 7. 保存db实例 + golabl.PsiMysqlDb = db + return nil +} diff --git a/initialization/redis/redis.go b/initialization/redis/redis.go new file mode 100644 index 0000000..ea4f1b3 --- /dev/null +++ b/initialization/redis/redis.go @@ -0,0 +1,78 @@ +package redis + +import ( + "fmt" + "planA/initialization/golabl" + _type "planA/type" + "time" + + "github.com/go-redis/redis/v8" +) + +// Init 初始化Redis连接 +// @return error 错误信息 +func Init() error { + + // 1. 获取redis配置 + redisConfig := golabl.Config.RedisConfig + redisClientA, redisErr := NewRedisClient(redisConfig[0]) + if redisErr != nil { + return fmt.Errorf("初始化 redis %v db%v 失败: %v\n", redisConfig[0].Addr, redisConfig[0].DB, redisErr) + } + golabl.RedisDbA = redisClientA + + // Redis B - Redis实例 + redisClientB, redisErr := NewRedisClient(redisConfig[1]) + if redisErr != nil { + return fmt.Errorf("初始化 redis %v db%v 失败: %v\n", redisConfig[1].Addr, redisConfig[1].DB, redisErr) + } + golabl.RedisDbB = redisClientB + + // Redis C - Redis实例 + redisClientC, redisErr := NewRedisClient(redisConfig[2]) + if redisErr != nil { + return fmt.Errorf("初始化 redis %v db%v 失败: %v\n", redisConfig[2].Addr, redisConfig[2].DB, redisErr) + } + golabl.RedisDbC = redisClientC + + // Redis D - Redis实例 + redisClientD, redisErr := NewRedisClient(redisConfig[6]) + if redisErr != nil { + return fmt.Errorf("初始化 redis %v db%v 失败: %v\n", redisConfig[6].Addr, redisConfig[6].DB, redisErr) + } + golabl.RedisDbD = redisClientD + + //设置默认过期时间 + golabl.RedisExp = time.Duration(golabl.Config.Server.RedisExp) * time.Hour + return nil +} + +// NewRedisClient 创建redis 客户端 +// @param config redis配置 +// @return *redis.Client redis客户端 +// @return error 错误信息 +func NewRedisClient(config _type.RedisConfig) (*redis.Client, error) { + ctx := golabl.Ctx + rdb := redis.NewClient(&redis.Options{ + Addr: config.Addr, // 连接地址 + Password: config.Password, // 密码 + DB: config.DB, // 数据库 + PoolSize: config.PoolSize, // 连接池大小 + PoolTimeout: time.Duration(config.PoolTimeout), // 连接池超时时间 + ReadTimeout: time.Duration(config.ReadTimeout), // 读取超时 + WriteTimeout: time.Duration(config.WriteTimeout), // 写入超时 + DialTimeout: time.Duration(config.DialTimeout), // 连接超时 + IdleTimeout: time.Duration(config.IdleTimeout), // 空闲超时 + MinIdleConns: config.MinIdleConns, // 最小空闲连接数 + IdleCheckFrequency: time.Duration(config.IdleCheckFrequency), // 空闲检查频率 + MaxRetries: config.MaxRetries, // 最大重试次数 + MaxRetryBackoff: time.Duration(config.MaxRetryBackoff), // 最大重试间隔 + MinRetryBackoff: time.Duration(config.MinRetryBackoff), // 最小重试间隔 + }) + // 测试连接 + _, err := rdb.Ping(ctx).Result() + if err != nil { + return rdb, err + } + return rdb, nil +} diff --git a/initialization/router/router.go b/initialization/router/router.go new file mode 100644 index 0000000..8e60cdc --- /dev/null +++ b/initialization/router/router.go @@ -0,0 +1,17 @@ +package router + +import "planA/router" + +// Init 初始化路由 +func Init() { + router.DefaultInit() + router.TaskInit() + router.TaskExportInit() + router.StaticInit() + router.AdmiinInir() + router.Alive() + router.DelTaskInit() + router.UploadImgInit() + router.ShopInit() + router.BodyInit() +} diff --git a/initialization/sqLite/sqLite.go b/initialization/sqLite/sqLite.go new file mode 100644 index 0000000..9c81925 --- /dev/null +++ b/initialization/sqLite/sqLite.go @@ -0,0 +1,46 @@ +package sqLite + +import ( + "database/sql" + "errors" + "fmt" + "planA/initialization/golabl" + sqLiteServer "planA/service/sqLite" + + _ "modernc.org/sqlite" +) + +// Init 初始化sqlIte连接 +// @return error 错误信息 +func Init() error { + // 1. 打开数据库 + db, err := sql.Open("sqlite", "./taskDb.db") + if err != nil { + return errors.New("打开sqLite数据库失败:" + err.Error()) + } + + // 测试连接 + err = db.Ping() + if err != nil { + return errors.New("无法连接到sqLite数据库:" + err.Error()) + } + golabl.SqliteDb = db + // 自动创建表 + if err := CreateTable(); err != nil { + return err + } + return nil +} + +// CreateTable 自动建表 +func CreateTable() error { + createTaskIdTabErr := sqLiteServer.CreateTaskIdTab() + if createTaskIdTabErr != nil { + return fmt.Errorf("自动创建表失败: %v", createTaskIdTabErr) + } + createTaskExportTabErr := sqLiteServer.CreateTaskExportTab() + if createTaskExportTabErr != nil { + return fmt.Errorf("自动创建表失败: %v", createTaskExportTabErr) + } + return nil +} diff --git a/initialization/validator/validator.go b/initialization/validator/validator.go new file mode 100644 index 0000000..9085c56 --- /dev/null +++ b/initialization/validator/validator.go @@ -0,0 +1,11 @@ +package validator + +import ( + "planA/initialization/golabl" + + "github.com/go-playground/validator/v10" +) + +func Init() { + golabl.Validator = validator.New() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c3aebbd --- /dev/null +++ b/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "planA/initialization" +) + +func main() { + // 初始化 + err := initialization.Init() + if err != nil { + fmt.Println("初始化失败:", err) + return + } + //启动服务 + initialization.Server() +} diff --git a/modules/config/config.dll b/modules/config/config.dll new file mode 100644 index 0000000..4e1d91b Binary files /dev/null and b/modules/config/config.dll differ diff --git a/modules/config/conifg.go b/modules/config/conifg.go new file mode 100644 index 0000000..a8138f3 --- /dev/null +++ b/modules/config/conifg.go @@ -0,0 +1,74 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// ConfigDLL 配置文件读取DLL结构 +type ConfigDLL struct { + dll *syscall.DLL + readConfigFile *syscall.Proc // 读取配置文件 + getVersion *syscall.Proc // 获取版本信息 + freeCString *syscall.Proc // 释放C字符串 +} + +// InitConfigDLL 初始化ConfigDLL +func InitConfigDLL() (*ConfigDLL, error) { + dllPath := filepath.Join("modules/config/", "config.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("config DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载config DLL 失败: %s", err) + } else { + return &ConfigDLL{ + dll: dll, + readConfigFile: dll.MustFindProc("ReadConfigFile"), + getVersion: dll.MustFindProc("GetVersion"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +// cStr 获取C字符串 +func (m *ConfigDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} + +// ReadConfigFile 读取配置文件 +func (m *ConfigDLL) ReadConfigFile(filePath, fileName string) (string, error) { + proc, err := m.dll.FindProc("ReadConfigFile") + if err != nil { + return "", fmt.Errorf("找不到函数 ReadConfigFile: %v", err) + } + + filePathPtr, _ := syscall.BytePtrFromString(filePath) + fileNamePtr, _ := syscall.BytePtrFromString(fileName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(filePathPtr)), + uintptr(unsafe.Pointer(fileNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} diff --git a/modules/image/image.dll b/modules/image/image.dll new file mode 100644 index 0000000..ed7d1c7 Binary files /dev/null and b/modules/image/image.dll differ diff --git a/modules/image/image.go b/modules/image/image.go new file mode 100644 index 0000000..bf88774 --- /dev/null +++ b/modules/image/image.go @@ -0,0 +1,107 @@ +package image + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "planA/planB/config" + "syscall" + "unsafe" +) + +var ( + gImageDll *ImageDLL +) + +// ImageDLL 图片工具DLL结构 +type ImageDLL struct { + Dll *syscall.DLL + AddWatermarkFromURLEx *syscall.Proc // 打水印 +} + +// InitImageDll 初始化 imageDLL +func InitImageDll() (*ImageDLL, error) { + fileConfig, getDllFileConfigErr := config.GetFileUrlConfig() + if getDllFileConfigErr != nil { + return nil, getDllFileConfigErr + } + dllPath := filepath.Join(fileConfig.ImageDll, "image.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("Image DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载Image DLL 失败: %s", err) + } + gImageDll = &ImageDLL{ + Dll: dll, + AddWatermarkFromURLEx: dll.MustFindProc("AddWatermarkFromURLEx"), + } + return gImageDll, nil +} + +// WatermarkConfig 添加水印 +type WatermarkConfig struct { + SourceImageURL string // 源图片URL地址 + WatermarkURL string // 水印图片URL地址 + Opacity float64 // 不透明度 (0.0-1.0) + Position string // 位置: center, top-left, top-right, bottom-left, bottom-right, tile + TileSpacing int // 平铺时的间距 + Scale float64 // 水印缩放比例 (0.0-1.0) + Rotation float64 // 旋转角度 (度数) + XOffset int // X轴偏移量 + YOffset int // Y轴偏移量 + Timeout int // 下载超时时间(秒),默认30秒 + OutputFormat string // 输出格式: "jpeg", "png", "auto"(默认auto,根据源图片格式)auto + JPEGQuality int // JPEG质量 (1-100),默认95 +} + +// AddWatermarkFromURLExs 添加水印 +func (m *ImageDLL) AddWatermarkFromURLExs(sourceImageUrl, watermarkUrl string) (string, error) { + + watermarkConfig := WatermarkConfig{ + SourceImageURL: sourceImageUrl, + WatermarkURL: watermarkUrl, + Position: "center", + Opacity: 1.0, + Scale: 1.0, + TileSpacing: 50, + Timeout: 30, + OutputFormat: "jpeg", + JPEGQuality: 95, + } + watermarkConfigJson, err := json.Marshal(watermarkConfig) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + + proc, err := m.Dll.FindProc("AddWatermarkFromURLEx") + if err != nil { + return "", fmt.Errorf("找不到函数 AddWatermarkFromURLEx: %v", err) + } + watermarkConfigJsonPtr, _ := syscall.BytePtrFromString(string(watermarkConfigJson)) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(watermarkConfigJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} diff --git a/modules/logs/dll.go b/modules/logs/dll.go new file mode 100644 index 0000000..48250ed --- /dev/null +++ b/modules/logs/dll.go @@ -0,0 +1,395 @@ +package logs + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "planA/initialization/golabl" + "runtime" + "syscall" + "unsafe" +) + +const ( + LOG_LEVEL_DEBUG = "DEBUG" + LOG_LEVEL_INFO = "INFO" + LOG_LEVEL_WARNING = "WARNING" + LOG_LEVEL_ERROR = "ERROR" + LOG_LEVEL_SUCCESS = "SUCCESS" +) + +// LoggerDLL 封装 logger.dll 操作 +type LoggerDLL struct { + dll *syscall.LazyDLL + createLogger *syscall.LazyProc + createContext *syscall.LazyProc + logInfo *syscall.LazyProc + logError *syscall.LazyProc + logWarning *syscall.LazyProc + logSuccess *syscall.LazyProc + freeString *syscall.LazyProc + closeAllLoggers *syscall.LazyProc +} + +// LoggerConfig logger配置结构 +type LoggerConfig struct { + LogDir string `json:"log_dir"` + SplitType int `json:"split_type"` + RotateType int `json:"rotate_type"` + MaxSize int64 `json:"max_size"` + MaxCount int `json:"max_count"` + Level int `json:"level"` + EnableCaller bool `json:"enable_caller"` + DefaultTaskType string `json:"default_task_type"` +} + +var loggerDLLInstance *LoggerDLL +var loggerHandle string +var loggerContextHandle string + +// ensureLoggerDLL 确保logger DLL已加载 +func ensureLoggerDLL() (*LoggerDLL, error) { + if loggerDLLInstance != nil { + return loggerDLLInstance, nil + } + + // 检查是否在Windows平台 + if runtime.GOOS != "windows" { + return nil, fmt.Errorf("logger DLL only supported on Windows platform") + } + + dllPath := filepath.Join(golabl.Config.FileUrl.LogDll, "logger.dll") + + // 检查文件是否存在 + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + // 尝试从当前目录查找 + if _, err := os.Stat("logger.dll"); err == nil { + dllPath = "logger.dll" + } else { + return nil, fmt.Errorf("logger DLL not found at %s", dllPath) + } + } + + dll := syscall.NewLazyDLL(dllPath) + + loggerDLLInstance = &LoggerDLL{ + dll: dll, + createLogger: dll.NewProc("CreateLogger"), + createContext: dll.NewProc("CreateContextWithTaskType"), + logInfo: dll.NewProc("LogInfo"), + logError: dll.NewProc("LogError"), + logWarning: dll.NewProc("LogWarning"), + logSuccess: dll.NewProc("LogSuccess"), + freeString: dll.NewProc("FreeString"), + closeAllLoggers: dll.NewProc("CloseAllLoggers"), + } + + return loggerDLLInstance, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// InitializeLogger 初始化logger +func InitializeLogger(logDir string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + // 确保日志目录存在 + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 创建logger配置 + config := LoggerConfig{ + LogDir: logDir, + SplitType: 2, // SplitByDay + RotateType: 0, // RotateBySize + MaxSize: 100 * 1024 * 1024, // 100MB + MaxCount: 10, + Level: 1, // LevelInfo - 只显示INFO及以上级别的日志 + EnableCaller: true, + DefaultTaskType: "main", + } + + configJSON, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("序列化配置失败: %v", err) + } + + // 调用CreateLogger + configPtr, _ := syscall.BytePtrFromString(string(configJSON)) + ret, _, _ := m.createLogger.Call(uintptr(unsafe.Pointer(configPtr))) + + if ret == 0 { + return fmt.Errorf("创建logger失败") + } + + // 获取logger句柄 + handle := cStr(ret) + loggerHandle = handle + + // 释放返回的字符串 + m.freeString.Call(ret) + + // 创建默认上下文 + return createLoggerContext("main") +} + +// createLoggerContext 创建带任务类型的logger上下文 +func createLoggerContext(taskType string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerHandle == "" { + return fmt.Errorf("logger未初始化") + } + + handlePtr, _ := syscall.BytePtrFromString(loggerHandle) + taskTypePtr, _ := syscall.BytePtrFromString(taskType) + + ret, _, _ := m.createContext.Call( + uintptr(unsafe.Pointer(handlePtr)), + uintptr(unsafe.Pointer(taskTypePtr)), + ) + + if ret == 0 { + return fmt.Errorf("创建logger上下文失败") + } + + // 获取上下文句柄 + loggerContextHandle = cStr(ret) + + // 释放返回的字符串 + m.freeString.Call(ret) + + return nil +} + +// SetLogTaskType 设置当前日志任务类型 +func SetLogTaskType(taskType string) error { + return createLoggerContext(taskType) +} + +// LogInfo 记录信息日志 +func LogInfo(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logInfo.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogError 记录错误日志 +func LogError(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logError.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogWarning 记录警告日志 +func LogWarning(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logWarning.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogSuccess 记录成功日志 +func LogSuccess(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logSuccess.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// CloseLogger 关闭logger +func CloseLogger() error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + ret, _, _ := m.closeAllLoggers.Call() + if ret == 0 { + return fmt.Errorf("关闭logger失败") + } + + m.freeString.Call(ret) + + loggerHandle = "" + loggerContextHandle = "" + loggerDLLInstance = nil + + return nil +} + +// GetLoggerHandle 获取当前logger句柄(用于外部调用) +func GetLoggerHandle() string { + return loggerContextHandle +} + +// IsLoggerInitialized 检查logger是否已初始化 +func IsLoggerInitialized() bool { + return loggerHandle != "" && loggerContextHandle != "" +} + +// SetConsoleOutput 设置控制台输出开关 +func SetConsoleOutput(enabled bool) { + if enabled { + os.Setenv("LOG_CONSOLE", "true") + } else { + os.Setenv("LOG_CONSOLE", "false") + } +} + +// LogWithLevel 带级别的日志记录,可以精确控制显示 +func LogWithLevel(level, message string, showConsole bool) { + if !IsLoggerInitialized() { + return + } + + switch level { + case "ERROR": + LogError(message) + case "WARNING": + LogWarning(message) + case "SUCCESS": + LogSuccess(message) + case "INFO": + LogInfo(message) + default: + LogInfo(message) + } +} + +// LogOnlyFile 仅写入文件,不输出到控制台 +func LogOnlyFile(level, message string) { + // 临时禁用控制台输出 + os.Setenv("LOG_CONSOLE", "false") + LogWithLevel(level, message, false) +} + +// LogConsoleAndFile 同时输出到控制台和文件 +func LogConsoleAndFile(level, message string) { + // 临时启用控制台输出 + os.Setenv("LOG_CONSOLE", "true") + LogWithLevel(level, message, true) +} + +// LoggingMiddleware 记录日志 +func LoggingMiddleware(level string, str string) { + initializeLoggerErr := InitializeLogger("logs") + if initializeLoggerErr != nil { + fmt.Println("初始化日志失败:", initializeLoggerErr) + return + } + setLogTaskTypeErr := SetLogTaskType("task") + if setLogTaskTypeErr != nil { + fmt.Println("设置日志任务类型失败:", setLogTaskTypeErr) + return + } + + switch { + case level == LOG_LEVEL_ERROR: + fmt.Println(str) + logErrorErr := LogError(str) + if logErrorErr != nil { + fmt.Println("记录错误日志失败:", logErrorErr) + return + } + case level == LOG_LEVEL_WARNING: + logWarningErr := LogWarning(str) + if logWarningErr != nil { + fmt.Println("记录警告日志失败:", logWarningErr) + return + } + case level == LOG_LEVEL_SUCCESS: + logSuccessErr := LogSuccess(str) + if logSuccessErr != nil { + fmt.Println("记录成功日志失败:", logSuccessErr) + return + } + default: + logInfoErr := LogInfo(str) + if logInfoErr != nil { + fmt.Println("记录信息日志失败:", logInfoErr) + return + } + } +} diff --git a/modules/logs/logger.dll b/modules/logs/logger.dll new file mode 100644 index 0000000..52e722b Binary files /dev/null and b/modules/logs/logger.dll differ diff --git a/modules/logs/logger.md b/modules/logs/logger.md new file mode 100644 index 0000000..411f310 --- /dev/null +++ b/modules/logs/logger.md @@ -0,0 +1,602 @@ +# logger.dll 使用教程 +## 1. 创建DLL工具实例 +### 加载DLL文件 +```gotemplate +package logs + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "syscall" + "unsafe" +) + +// LoggerDLL 封装 logger.dll 操作 +type LoggerDLL struct { + dll *syscall.LazyDLL + createLogger *syscall.LazyProc + createContext *syscall.LazyProc + logInfo *syscall.LazyProc + logError *syscall.LazyProc + logWarning *syscall.LazyProc + logSuccess *syscall.LazyProc + freeString *syscall.LazyProc + closeAllLoggers *syscall.LazyProc +} + +// LoggerConfig logger配置结构 +type LoggerConfig struct { + LogDir string `json:"log_dir"` + SplitType int `json:"split_type"` + RotateType int `json:"rotate_type"` + MaxSize int64 `json:"max_size"` + MaxCount int `json:"max_count"` + Level int `json:"level"` + EnableCaller bool `json:"enable_caller"` + DefaultTaskType string `json:"default_task_type"` +} + +var loggerDLLInstance *LoggerDLL +var loggerHandle string +var loggerContextHandle string + +// ensureLoggerDLL 确保logger DLL已加载 +func ensureLoggerDLL() (*LoggerDLL, error) { + if loggerDLLInstance != nil { + return loggerDLLInstance, nil + } + + // 检查是否在Windows平台 + if runtime.GOOS != "windows" { + return nil, fmt.Errorf("logger DLL only supported on Windows platform") + } + + // logger.dll 位于 dll/logger.dll + //dllPath := filepath.Join("modules", "logs", "logger.dll") + dllPath := "D:\\www\\wwwroot\\planA\\modules\\logs\\logger.dll" + + // 检查文件是否存在 + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + // 尝试从当前目录查找 + if _, err := os.Stat("logger.dll"); err == nil { + dllPath = "logger.dll" + } else { + return nil, fmt.Errorf("logger DLL not found at %s", dllPath) + } + } + + dll := syscall.NewLazyDLL(dllPath) + + loggerDLLInstance = &LoggerDLL{ + dll: dll, + createLogger: dll.NewProc("CreateLogger"), + createContext: dll.NewProc("CreateContextWithTaskType"), + logInfo: dll.NewProc("LogInfo"), + logError: dll.NewProc("LogError"), + logWarning: dll.NewProc("LogWarning"), + logSuccess: dll.NewProc("LogSuccess"), + freeString: dll.NewProc("FreeString"), + closeAllLoggers: dll.NewProc("CloseAllLoggers"), + } + + return loggerDLLInstance, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// InitializeLogger 初始化logger +func InitializeLogger(logDir string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + // 确保日志目录存在 + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 创建logger配置 + config := LoggerConfig{ + LogDir: logDir, + SplitType: 1, // SplitByDay + RotateType: 0, // RotateBySize + MaxSize: 100 * 1024 * 1024, // 100MB + MaxCount: 10, + Level: 1, // LevelInfo - 只显示INFO及以上级别的日志 + EnableCaller: true, + DefaultTaskType: "main", + } + + configJSON, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("序列化配置失败: %v", err) + } + + // 调用CreateLogger + configPtr, _ := syscall.BytePtrFromString(string(configJSON)) + ret, _, _ := m.createLogger.Call(uintptr(unsafe.Pointer(configPtr))) + + if ret == 0 { + return fmt.Errorf("创建logger失败") + } + + // 获取logger句柄 + handle := cStr(ret) + loggerHandle = handle + + // 释放返回的字符串 + m.freeString.Call(ret) + + // 创建默认上下文 + return createLoggerContext("main") +} + +// createLoggerContext 创建带任务类型的logger上下文 +func createLoggerContext(taskType string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerHandle == "" { + return fmt.Errorf("logger未初始化") + } + + handlePtr, _ := syscall.BytePtrFromString(loggerHandle) + taskTypePtr, _ := syscall.BytePtrFromString(taskType) + + ret, _, _ := m.createContext.Call( + uintptr(unsafe.Pointer(handlePtr)), + uintptr(unsafe.Pointer(taskTypePtr)), + ) + + if ret == 0 { + return fmt.Errorf("创建logger上下文失败") + } + + // 获取上下文句柄 + loggerContextHandle = cStr(ret) + + // 释放返回的字符串 + m.freeString.Call(ret) + + return nil +} + +// SetLogTaskType 设置当前日志任务类型 +func SetLogTaskType(taskType string) error { + return createLoggerContext(taskType) +} + +// LogInfo 记录信息日志 +func LogInfo(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logInfo.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogError 记录错误日志 +func LogError(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logError.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogWarning 记录警告日志 +func LogWarning(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logWarning.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogSuccess 记录成功日志 +func LogSuccess(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logSuccess.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// CloseLogger 关闭logger +func CloseLogger() error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + ret, _, _ := m.closeAllLoggers.Call() + if ret == 0 { + return fmt.Errorf("关闭logger失败") + } + + m.freeString.Call(ret) + + loggerHandle = "" + loggerContextHandle = "" + loggerDLLInstance = nil + + return nil +} + +// GetLoggerHandle 获取当前logger句柄(用于外部调用) +func GetLoggerHandle() string { + return loggerContextHandle +} + +// IsLoggerInitialized 检查logger是否已初始化 +func IsLoggerInitialized() bool { + return loggerHandle != "" && loggerContextHandle != "" +} + +// SetConsoleOutput 设置控制台输出开关 +func SetConsoleOutput(enabled bool) { + if enabled { + os.Setenv("LOG_CONSOLE", "true") + } else { + os.Setenv("LOG_CONSOLE", "false") + } +} + +// LogWithLevel 带级别的日志记录,可以精确控制显示 +func LogWithLevel(level, message string, showConsole bool) { + if !IsLoggerInitialized() { + return + } + + switch level { + case "ERROR": + LogError(message) + case "WARNING": + LogWarning(message) + case "SUCCESS": + LogSuccess(message) + case "INFO": + LogInfo(message) + default: + LogInfo(message) + } +} + +// LogOnlyFile 仅写入文件,不输出到控制台 +func LogOnlyFile(level, message string) { + // 临时禁用控制台输出 + os.Setenv("LOG_CONSOLE", "false") + LogWithLevel(level, message, false) +} + +// LogConsoleAndFile 同时输出到控制台和文件 +func LogConsoleAndFile(level, message string) { + // 临时启用控制台输出 + os.Setenv("LOG_CONSOLE", "true") + LogWithLevel(level, message, true) +} + +``` + +# 接口详情 +## 创建日志器--CreateLogger +### 请求信息 +```gotemplate +dll.CreateLogger(configJSON) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| configJSON | string | 是 | 配置信息JSON字符串 | +#### 配置JSON结构 +```json +{ + "log_dir": "/path/to/logs", + "split_type": 0, + "rotate_type": 0, + "max_size": 104857600, + "max_count": 30, + "level": 1, + "enable_caller": true, + "default_task_type": "main" +} +``` +#### 参数说明: +```text +log_dir: 日志目录路径 +split_type: 分片方式(0=按月,1=按天,2=按小时,3=按分钟,4=按秒) +rotate_type: 轮转方式(0=按大小,1=按数量) +max_size: 最大文件大小(字节),仅在rotate_type=0时有效 +max_count: 最大文件数量,仅在rotate_type=1时有效 +level: 日志级别(0=SUCCESS,1=INFO,2=WARNING,3=ERROR) +enable_caller: 是否启用调用者信息 +default_task_type: 默认任务类型 +``` +### 响应示例 +```json +"错误: 创建日志目录失败: permission denied" +``` + +## 创建带任务类型的上下文--CreateContextWithTaskType +### 请求信息 +```gotemplate +dll.CreateContextWithTaskType(loggerHandle, taskType) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| loggerHandle | string | 是 | 日志器句柄 | +| taskType | string | 是 | 任务类型 | +### 响应示例 +```json +"ctx_1645497600000000000" +``` +#### 错误响应示例 +```json +"错误: 无效的logger句柄" +``` + +## 记录信息日志--LogInfo +### 请求信息 +```gotemplate +dll.LogInfo(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 记录错误日志--LogError +### 请求信息 +```gotemplate +dll.LogError(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 记录警告日志--LogWarning +### 请求信息 +```gotemplate +dll.LogWarning(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 记录成功日志--LogSuccess +### 请求信息 +```gotemplate +dll.LogSuccess(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 获取日志条目--GetLogs +### 请求信息 +```gotemplate +dll.GetLogs(loggerHandle, configJSON) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------| +| loggerHandle | string | 是 | 日志器句柄 | +| configJSON | string | 是 | 查询配置JSON | +#### 查询配置JSON结构 +```json +{ + "level": 1, + "task_type": "main", + "start_time": "2024-01-01 00:00:00", + "end_time": "2024-01-31 23:59:59", + "max_entries": 1000 +} +``` +#### 参数说明: +```text +level: 日志级别(-1表示所有级别) +task_type: 任务类型(空字符串表示所有任务类型) +start_time: 开始时间(格式: 2006-01-02 15:04:05) +end_time: 结束时间(格式: 2006-01-02 15:04:05) +max_entries: 最大返回条目数(0表示使用默认值1000) +``` +### 响应示例 +```json +{ + "count": 125, + "entries": [ + { + "timestamp": "2024-01-15 10:30:45.123", + "level": "INFO", + "task_type": "main", + "caller": "logger.go:256", + "message": "系统启动完成" + }, + { + "timestamp": "2024-01-15 10:31:15.456", + "level": "ERROR", + "task_type": "backup", + "caller": "backup.go:89", + "message": "备份文件失败: 磁盘空间不足" + } + ] +} +``` +### 错误响应示例 +```json +{"error": "无效的logger句柄"} +``` + +## 获取日志文件列表--GetLogFiles +### 请求信息 +```gotemplate +dll.GetLogFiles(loggerHandle) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------| +| loggerHandle | string | 是 | 日志器句柄 | +### 响应示例 +```json +{ + "count": 8, + "files": [ + { + "level": "INFO", + "task_type": "main", + "file_name": "INFO-main-2024-01.logs", + "file_size": 1048576, + "mod_time": "2024-01-15 10:30:45" + }, + { + "level": "ERROR", + "task_type": "backup", + "file_name": "ERROR-backup-2024-01.logs", + "file_size": 51200, + "mod_time": "2024-01-15 10:31:15" + } + ] +} +``` +### 错误响应示例 +```json +{"error": "无效的logger句柄"} +``` + +## 获取版本信息--GetVersion +### 请求信息 +```gotemplate +dll.GetVersion() +``` +### 请求参数 +```text +无参数 +``` +### 响应示例 +```json +"v1" +``` + +## 关闭所有日志器--CloseAllLoggers +### 请求信息 +```gotemplate +dll.CloseAllLoggers() +``` +### 请求参数 +```text +无参数 +``` +### 响应示例 +```json +"成功关闭所有logger" +``` +### 错误响应示例 +```json +"关闭了5个logger,其中1个出错,最后错误: close file error" +``` + +## 释放C字符串内存--FreeString +### 请求信息 +```gotemplate +dll.FreeString(str) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------| +| str | string | 是 | 需要释放的字符串 | \ No newline at end of file diff --git a/modules/pdd/pdd.dll b/modules/pdd/pdd.dll new file mode 100644 index 0000000..532e5c9 Binary files /dev/null and b/modules/pdd/pdd.dll differ diff --git a/modules/pdd/pdd.go b/modules/pdd/pdd.go new file mode 100644 index 0000000..224a8b8 --- /dev/null +++ b/modules/pdd/pdd.go @@ -0,0 +1,223 @@ +package pdd + +import ( + "fmt" + "os" + "path/filepath" + "planA/initialization/golabl" + "syscall" + "unsafe" +) + +var ( + gPddDll *PddDLL +) + +// PddResponse 定义完整的响应结构(包含成功和失败两种情况) +type PddResponse struct { + SuccessResponse *PddSuccessResponse `json:"outer_cat_mapping_get_response,omitempty"` + ErrorResponse *PddErrorResponse `json:"error_response,omitempty"` +} +type PddSuccessResponse struct { + OuterCatMappingGetResponse PddCategoryMappingResponse `json:"outer_cat_mapping_get_response"` +} + +// PddCategoryMappingResponse 定义拼多多API响应结构(根据文档规范) +type PddCategoryMappingResponse struct { + CatID1 int64 `json:"cat_id1"` // 一级类目 ID + CatID2 int64 `json:"cat_id2"` // 二级类目 ID + CatID3 int64 `json:"cat_id3"` // 三级类目 ID + CatID4 int64 `json:"cat_id4"` // 四级类目 ID + RequestID string `json:"request_id"` // 请求 ID +} + +// PddDLL 拼多多工具DLL结构 +type PddDLL struct { + Dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} +type PddErrorResponse struct { + ErrorCode int64 `json:"error_code"` // 错误码 + ErrorMsg string `json:"error_msg"` // 错误信息 + SubCode *string `json:"sub_code"` // 子错误码 + SubMsg string `json:"sub_msg"` // 子错误信息 + RequestID string `json:"request_id"` // 请求ID +} + +// InitPddDll 初始化 pddDLL +func InitPddDll() (*PddDLL, error) { + dllPath := filepath.Join(golabl.Config.FileUrl.PddDll, "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } + gPddDll = &PddDLL{ + Dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + } + return gPddDll, nil +} + +// PddGoodsOuterCatMappingGet 类目预测 +func (m *PddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsAdd 商品新增 +func (m *PddDLL) PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsAdd") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsAdd: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsAddJsonPtr, _ := syscall.BytePtrFromString(goodsAddJson) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsAddJsonPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// PddGoodsSpecIdGet 生成商家自定义的规格 +func (m *PddDLL) PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsSpecIdGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSpecIdGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + parentSpecIdPtr, _ := syscall.BytePtrFromString(parentSpecId) + specNamePtr, _ := syscall.BytePtrFromString(specName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(parentSpecIdPtr)), + uintptr(unsafe.Pointer(specNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsCommitDetailGet 获取商品提交的商品详情 +func (m *PddDLL) PddGoodsCommitDetailGet(clientId, clientSecret, accessToken, goodsCommitId, goodsId string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsCommitDetailGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsCommitIdPtr, _ := syscall.BytePtrFromString(goodsCommitId) + goodsIdPtr, _ := syscall.BytePtrFromString(goodsId) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsCommitIdPtr)), + uintptr(unsafe.Pointer(goodsIdPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddTimeGet 获取拼多多系统时间 +func (m *PddDLL) PddTimeGet(clientId, clientSecret, accessToken string) (string, error) { + proc, err := m.Dll.FindProc("PddTimeGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsImageUpload 上传图片 +func (m *PddDLL) PddGoodsImageUpload(clientId, clientSecret, accessToken, imgBase64 string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsImageUpload") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsImageUpload: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + imgBase64Ptr, _ := syscall.BytePtrFromString(imgBase64) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(imgBase64Ptr)), + ) + result := cStr(resultPtr) + return result, nil +} diff --git a/modules/pdd/pdd.md b/modules/pdd/pdd.md new file mode 100644 index 0000000..2c11768 --- /dev/null +++ b/modules/pdd/pdd.md @@ -0,0 +1,863 @@ +# pdd.dll 使用教程 +## 1.创建DLL工具实例 +### 加载DLL文件 +```gotemplate +// PddDLL 拼多多工具DLL结构 +type pddDLL struct { + dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} + +// <初始化pddDLL> +func InitPddDLL() (*pddDLL, error) { + dllPath := filepath.Join("dll", "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } else { + return &pddDLL{ + dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +dll, err := InitPddDLL() +``` + +### 获取C字符串 +```gotemplate +// cStr 获取C字符串 +func (m *pddDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} +``` + +## 2. 使用dll函数示例 +```gotemplate +// 类目预测 +func (m *pddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} +``` + +# 接口详情 +## 1. 类目预测--PddGoodsOuterCatMappingGet +### 请求信息 +```gotemplate +dll.PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, +outerCatId, outerCatName, outerGoodsName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| outerCatId | string | 是 | 外部平台类目ID | +| outerCatName | string | 是 | 外部平台类目名称 | +| outerGoodsName | string | 是 | 外部商品名称 | +### 响应示例 +```json +{ + "outer_cat_mapping_get_response": { + "cat_id2": 16028, + "cat_id3": 16031, + "cat_id1": 15543, + "request_id": "17666480184871649", + "cat_id4": 0 + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 2. 快递公司查看--PddLogisticsCompaniesGet +### 请求信息 +```gotemplate +dll.PddLogisticsCompaniesGet(clientId, clientSecret) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +### 响应示例 +```json +{ + "logistics_companies_get_response": { + "logistics_companies": [ + { + "available": 1, + "code": "SF", + "id": 1, + "logistics_company": "顺丰速运" + }, + { + "available": 1, + "code": "STO", + "id": 2, + "logistics_company": "申通快递" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 3. erp打单信息同步--PddErpOrderSync +### 请求信息 +```gotemplate +dll.PddErpOrderSync(clientId, clientSecret, accessToken, logisticsId, +orderSn, orderState, waybillNo) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsId | string | 是 | 物流公司ID | +| orderSn | string | 是 | 拼多多订单号 | +| orderState | string | 是 | 订单状态 | +| waybillNo | string | 是 | 运单号 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871650" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 4. 拼多多订单同步--PddOrderSynchronization +### 请求信息 +```gotemplate +dll.PddOrderSynchronization(clientId, clientSecret, accessToken, logisticsCompany, logisticsOnlineSendJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsCompany | string | 是 | 物流公司名称 | +| logisticsOnlineSendJson | string | 是 | 拼多多订单同步json字符串 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871651" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 5. 商品图片上传接口--PddGoodsImgUpload +### 请求信息 +```gotemplate +dll.PddGoodsImgUpload(clientId, clientSecret, accessToken, filePath) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +### 响应示例 +```json +{ + "goods_img_upload_response": { + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "request_id": "17666480184871652" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 6. 商品新增接口--PddGoodsAdd +### 请求信息 +```gotemplate +dll.PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| goodsAddJson | string | 是 | 商品信息JSON字符串 | +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456789, + "goods_name": "测试商品", + "goods_sn": "G202501200001", + "request_id": "17666480184871653" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 7. 联合拼多多图片上传的商品新增--SelfPddGoodsAdd +### 请求信息 +```gotemplate +dll.SelfPddGoodsAdd(clientId, clientSecret, accessToken, filePath, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +| goodsAddJson | string | 是 | 商品信息JSON字符串(不需包含image_url)| +#### 接口说明 +此接口为组合接口,内部执行以下步骤: +1.上传商品主图文件到拼多多服务器 +2.获取图片URL并自动填充到商品信息中 +3.调用商品新增接口创建商品 +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456790, + "goods_name": "测试商品", + "goods_sn": "G202501200002", + "request_id": "17666480184871654" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 8. 批量数据解密脱敏接口--PddOpenDecryptMaskBatch +### 请求信息 +```gotemplate +dll.PddOpenDecryptMaskBatch(clientId, clientSecret, accessToken, reqJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| reqJson | string | 是 | 信息JSON字符串 | +#### 信息JSON结构示例 +```json +[ + { + "data_tag": "251229-272441044622514", + "encrypted_data": "~AgAAAAPlscEH0psOJAEXpTdsLOWvDJ9bB7IEjIoqNfiDhhJR9NHOxsdZ+PEFluSSCngCikoDU+CP/sSXZJ92ic7+PdNlJNLA7g/6VUMDWF6RvjW9IeRN+lKNarsjWDQR~0~" + } +] +``` +### 响应示例 +```json +{ + "open_decrypt_mask_batch_response": { + "data_decrypt_list": [ + { + "data_tag": "str", + "data_type": 0, + "decrypted_data": "str", + "encrypted_data": "str", + "error_code": 0, + "error_msg": "str" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 生成商家自定义的规格--PddGoodsSpecIdGet +### 请求信息 +```gotemplate +dll.PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| parentSpecId | string | 是 | 拼多多标准规格ID | +| specName | string | 是 | 商家编辑的规格值,如颜色规格下设置白色属性 | +### 响应参数 +```json +{ + "goods_spec_id_get_response": { + "parent_spec_id": 0, + "spec_id": 0, + "spec_name": "str" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 修改商品SKU价格--PddGoodsSkuPriceUpdate +### 请求信息 +```gotemplate +dll.PddGoodsSkuPriceUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 价格更新请求JSON字符串 | +#### 请求JSON结构 +```json +{ + "goods_id": "必填,商品id,类型为LONG", + "ignore_edit_warn": "非必填,是否获取商品发布警告信息,默认为忽略,类型为BOOLEAN", + "market_price": "非必填,参考价(单位分),类型为LONG", + "market_price_in_yuan": "非必填,参考价(单位元),类型为STRING", + "sku_price_list": [ + { + "group_price": "非必填,拼团购买价格(单位分),类型为LONG", + "is_onsale": "非必填,sku上架状态,0-已下架,1-上架中,类型为INTEGER", + "single_price": "非必填,单独购买价格(单位分),类型为LONG", + "sku_id": "必填,sku标识,类型为LONG" + } + ], + "sync_goods_operate": "非必填,提交后上架状态,0:上架,1:保持原样,类型为INTEGER", + "two_pieces_discount": "非必填,满2件折扣,可选范围0-100,0表示取消,95表示95折,设置需先查询规则接口获取实际可填范围,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_update_sku_price_response": { + "goods_commit_id": 0, + "is_success": true + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 商品库存更新接口--PddGoodsQuantityUpdate +### 请求信息 +```gotemplate +dll.PddGoodsQuantityUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 库存更新请求JSON字符串 | +#### 请求JSON结构 request 字符串 +```json +{ + "force_update": "非必填,是否强制更新,仅update_type=1(全量更新)时有效,默认值false;force_update=false时,quantity不能小于预扣库存;force_update=true时,代表强制更新,当quantity<预扣库存时,不报错,直接将quantity清0,类型为BOOLEAN", + "goods_id": "必填,商品id,类型为LONG", + "outer_id": "非必填,sku商家编码,类型为STRING", + "quantity": "必填,库存修改值。当全量更新库存时,quantity必须为大于等于0的正整数;当增量更新库存时,quantity为整数,可小于等于0。若增量更新时传入的库存为负数,则负数与实际库存之和不能小于0。比如当前实际库存为1,传入增量更新quantity=-1,库存改为0,类型为LONG", + "sku_id": "非必填,sku_id和outer_id必填一个,类型为LONG", + "update_type": "非必填,库存更新方式,可选。1为全量更新,2为增量更新。如果不填,默认为全量更新,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_quantity_update_response": { + "is_success": false + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 获取商品信息接口 -- OutPddAuthGetCommitDetailt +### 请求信息 +```gotemplate +dll.OutPddAuthGetCommitDetailt(goodsCommitId, goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsCommitId | string | 是 | 商品提交ID | +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json + +``` + + +## 获取商品详情信息接口 -- OutPddAuthGetGoodsDetail +### 请求信息 +```gotemplate +dll.OutPddAuthGetGoodsDetail(goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "bad_fruit_claim": 0, + "buy_limit": 999999, + "carousel_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2025-06-30/4539f740-331b-4687-aa00-5c96855de6cd.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/b0e89e39-c97b-475d-9be2-f1909e30acb5.jpeg" + ], + "cat_id": 15678, + "cost_template_id": 655688447565777, + "country_id": 0, + "customer_num": 2, + "customs": "", + "detail_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/b691c104-baf8-42b2-97e2-b7258113114b.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/53e6f7ff-d15e-4e8f-8625-e293717ca1e4.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/ecff591d-32a6-42c9-ba5a-6a42829092a8.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/7034f8a0-5d88-49f8-a96f-608abb8cac80.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/e10c2b6c-d4de-4fdd-8d48-f0a334735e9a.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/c19358fb-0a4d-49ad-bcc8-b2980e938064.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/1deeb9c0-7212-432b-a309-f774db6e1adb.jpeg" + ], + "goods_desc": "书名:金属工艺学 下 第6版,作者:'邓文英,宋力宏主编',ISBN:9787040456295,出版社:高等教育出版社", + "goods_id": 770621582375, + "goods_name": "金属工艺学 下 第6版 邓文英,宋力宏主编 高等教育出版社 978", + "goods_property_list": [ + { + "punit": "", + "ref_pid": 425, + "template_pid": 401030, + "vid": 0, + "vvalue": "9787040456295" + }, + { + "punit": "", + "ref_pid": 876, + "template_pid": 401029, + "vid": 0, + "vvalue": "金属工艺学 下 第6版" + }, + { + "punit": "页", + "ref_pid": 692, + "template_pid": 401032, + "vid": 0, + "vvalue": "157" + }, + { + "punit": "元", + "ref_pid": 879, + "template_pid": 401034, + "vid": 0, + "vvalue": "24.70" + }, + { + "punit": "", + "ref_pid": 882, + "template_pid": 401037, + "vid": 0, + "vvalue": "邓文英,宋力宏主编" + }, + { + "punit": "", + "ref_pid": 880, + "template_pid": 401035, + "vid": 483761, + "vvalue": "高等教育出版社" + }, + { + "punit": "", + "ref_pid": 888, + "template_pid": 401043, + "vid": 0, + "vvalue": "平装" + } + ], + "goods_type": 1, + "image_url": "", + "invoice_status": 0, + "is_customs": 0, + "is_folt": 0, + "is_group_pre_sale": 0, + "is_pre_sale": 0, + "is_refundable": 1, + "is_sku_pre_sale": 0, + "market_price": 5948, + "order_limit": 999999, + "outer_goods_id": "9787040456295", + "oversea_type": 0, + "pre_sale_time": 0, + "privacy_delivery": 0, + "quan_guo_lian_bao": 0, + "second_hand": 1, + "shipment_limit_second": 172800, + "sku_list": [ + { + "is_onsale": 1, + "limit_quantity": 999999, + "multi_price": 1487, + "out_sku_sn": "9787040456295", + "price": 1587, + "quantity": 0, + "reserve_quantity": 0, + "sku_id": 1753931570290, + "sku_pre_sale_time": 0, + "spec": [ + { + "parent_id": 1216, + "parent_name": "尺寸", + "spec_id": 27632894279, + "spec_name": "单本 无附赠 超七天不退换" + } + ], + "thumb_url": "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "weight": 500 + } + ], + "status": 4, + "tiny_name": "金属工艺学 下 第6", + "two_pieces_discount": 96, + "video_gallery": [], + "warehouse": "", + "warm_tips": "", + "zhi_huan_bu_xiu": 0 +} +``` + +## 生成自定义规格接口 -- OutPddAuthSetSpec +### 请求信息 +```gotemplate +dll.OutPddAuthSetSpec(specTypeId, specName, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| specTypeId | int | 是 | 规格类型ID | +| specName | string | 是 | 规格名称 | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "parentSpecId": 3820, + "specName": "全新", + "specId": 1080396526 +} +``` + +## 修改价格接口 -- OutPddAuthUpdatePrice +### 请求信息 +```gotemplate +dll.OutPddAuthUpdatePrice(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 修改库存接口 -- OutPddAuthUpdateStock +### 请求信息 +```gotemplate +dll.OutPddAuthUpdateStock(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 12.释放C字符串内存--FreeCString +### 请求信息 +```gotemplate +dll.FreeCString(str) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| str | string | 是 | 需要释放的字符串 | diff --git a/modules/xianYu/address.xlsx b/modules/xianYu/address.xlsx new file mode 100644 index 0000000..7c91f53 Binary files /dev/null and b/modules/xianYu/address.xlsx differ diff --git a/modules/xianYu/config.ini b/modules/xianYu/config.ini new file mode 100644 index 0000000..6755855 --- /dev/null +++ b/modules/xianYu/config.ini @@ -0,0 +1,25 @@ +[app] +AppId = 1228288260261189 +AppSecret = aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k +Domain = https://open.goofish.pro +[http] +Addr = 127.0.0.1:53368 +[categoryListRequest] +Path = /api/open/product/category/list +ItemBizType: 2 +SpBizType: 24 +[batchCreatRequest] +Path = /api/open/product/batchCreate +[file] +TxtPath = modules/xianYu/productCategory.txt +ExcelPath = modules/xianYu/address.xlsx +SheetName = Result +[redis] +Password = Long6166@@ +Addr = 127.0.0.1:6379 +Db = 5 +[tokenBucket] +BucketKeyPrefix = "token_bucket_" +TokenPerSecond = 10 +BucketSize = 100 +Delay = 100 diff --git a/modules/xianYu/productCategory.txt b/modules/xianYu/productCategory.txt new file mode 100644 index 0000000..a0a9fbc --- /dev/null +++ b/modules/xianYu/productCategory.txt @@ -0,0 +1,284 @@ +cbf4e2ec8f2013d31921b9e373cead75:电视剧 +cbf4e2ec8f2013d3267e0a01017d9f44:电影 +cbf4e2ec8f2013d36f38848189966e7d:生活 +cbf4e2ec8f2013d3ac899d2620c5df2b:成人教育音像 +cbf4e2ec8f2013d3acde29f76907b07f:动画 +cbf4e2ec8f2013d3e10cfa39bf43dc0f:儿童教育音像 +d14d229692616168b108d382c4e6ea42:废品回收 +d816d18aa66dfb3d1921b9e373cead75:励志成长 +dbaba36adf47af96b108d382c4e6ea42:不干胶标签 +e59460ef9961e2bd28d88a08a19453dc:古典吉他 +e59460ef9961e2bda7f7e02f36b0b49a:电箱吉他 +86cddebb2de0815c1921b9e373cead75:桌面文件柜 +86cddebb2de0815c267e0a01017d9f44:资料册 +86cddebb2de0815c6f38848189966e7d:镇纸 +86cddebb2de0815ca7f7e02f36b0b49a:文件袋 +86cddebb2de0815cacde29f76907b07f:文房墨汁 +86cddebb2de0815ce10cfa39bf43dc0f:文房四宝套装 +879b743300e7a58137b3d33c282f2081:古筝 +8bd8d9724880b84d28d88a08a19453dc:学习笔记 +a457d6fc43c609bdac899d2620c5df2b:单据收据 +a457d6fc43c609bdacde29f76907b07f:印台 +a9ef3505c7fe4b661921b9e373cead75:勾线笔 +a9ef3505c7fe4b66a7f7e02f36b0b49a:电子阅览器/电纸书 +ab78823bfd3c7134b108d382c4e6ea42:经济管理 +ac69f9982deabde1acde29f76907b07f:民谣吉他 +ac69f9982deabde1e10cfa39bf43dc0f:架子鼓 +b12c1c13a8dc3b2b6f38848189966e7d:POP广告纸 +b12c1c13a8dc3b2ba7f7e02f36b0b49a:修正贴 +b12c1c13a8dc3b2bac899d2620c5df2b:学生用印 +b12c1c13a8dc3b2bacde29f76907b07f:名片 +b2b61c32fc4c904428d88a08a19453dc:背胶证件照 +b3b713b29220947237b3d33c282f2081:台历 +4c49139fe1b6ae4aac899d2620c5df2b:童书育儿 +4fecb084c468ed626f38848189966e7d:黑板 +5042edcbd2cc4b94ac899d2620c5df2b:生活百科 +621bd460d751e0fc37b3d33c282f2081:订书机 +701ed8603d74ee60b108d382c4e6ea42:报纸 +722d38201b9c8cba267e0a01017d9f44:社科心理 +7912befd7e1215d11921b9e373cead75:挂历 +7dba397e41d08d4937b3d33c282f2081:拆信刀 +7eb776b01814cc6e1921b9e373cead75:教材教辅 +22e1d81dc4cf3a25a7f7e02f36b0b49a:图书 +2dfa3034d88aedcc1921b9e373cead75:期刊/杂志 +31329c43789fae0437b3d33c282f2081:戏曲综艺 +31329c43789fae04a7f7e02f36b0b49a:音乐唱片/专辑 +322a73805c38995f6f38848189966e7d:宝珠笔 +3cdbae6d47df9251a7f7e02f36b0b49a:电子资料 +22d3cfff678abab1e10cfa39bf43dc0f:握笔器 +b7fd03d456abe3011921b9e373cead75:活页替芯 +b7fd03d456abe301b108d382c4e6ea42:索引纸 +b7fd03d456abe301e10cfa39bf43dc0f:拍纸本 +c230ba4ca293f3b528d88a08a19453dc:马克笔 +c230ba4ca293f3b5a7f7e02f36b0b49a:钢笔 +c230ba4ca293f3b5ac899d2620c5df2b:铅笔 +c3c6e8d1d63c0618b108d382c4e6ea42:文学/小说 +c58d3dbcff05e404acde29f76907b07f:笔筒 +eac1d67ece5fa9b16f38848189966e7d:钢琴 +ee8603696d446e931921b9e373cead75:电钢琴 +06d80b131d7b0b616f38848189966e7d:毛笔 +0e28c0f1f1e57eb1ac899d2620c5df2b:地图 +0f75076039b85f74267e0a01017d9f44:计算器 +0f75076039b85f7428d88a08a19453dc:尺 +0f75076039b85f746f38848189966e7d:板擦 +0f75076039b85f74b108d382c4e6ea42:算盘 +11c38799bd389b3828d88a08a19453dc:漫画书籍 +ac69f9982deabde1a7f7e02f36b0b49a:上弦器 +83f9286d1ea41056ac899d2620c5df2b:其他吉他配件 +e59460ef9961e2bd1921b9e373cead75:变调夹 +83f9286d1ea4105637b3d33c282f2081:古典吉他弦 +e59460ef9961e2bdacde29f76907b07f:吉他单块效果器 +83f9286d1ea41056267e0a01017d9f44:吉他效果器配件 +83f9286d1ea41056b108d382c4e6ea42:吉他电源 +ac69f9982deabde1267e0a01017d9f44:吉他综合效果器 +e59460ef9961e2bdb108d382c4e6ea42:吉他背包琴盒 +83f9286d1ea4105628d88a08a19453dc:吉他背带 +83f9286d1ea410561921b9e373cead75:吉他连接线 +e59460ef9961e2bd6f38848189966e7d:吊架 +ac69f9982deabde1ac899d2620c5df2b:弦枕 +ac69f9982deabde11921b9e373cead75:弦柱 +e59460ef9961e2bd267e0a01017d9f44:拨片 +ac69f9982deabde128d88a08a19453dc:拾音器 +83f9286d1ea41056e10cfa39bf43dc0f:曼陀铃弦 +83f9286d1ea410566f38848189966e7d:民谣吉他弦 +ac69f9982deabde137b3d33c282f2081:清洁保护品 +83f9286d1ea41056a7f7e02f36b0b49a:滑棒指套 +e59460ef9961e2bde10cfa39bf43dc0f:电吉他弦 +e59460ef9961e2bdac899d2620c5df2b:背带钮 +83f9286d1ea41056acde29f76907b07f:脚凳 +ac69f9982deabde1b108d382c4e6ea42:调音器 +e59460ef9961e2bd37b3d33c282f2081:电吉他 +c6d5c9e68467b108ac899d2620c5df2b:哑鼓垫 +c6d5c9e68467b10837b3d33c282f2081:镲片 +c6d5c9e68467b10828d88a08a19453dc:鼓凳 +ac69f9982deabde16f38848189966e7d:鼓刷 +c6d5c9e68467b108b108d382c4e6ea42:鼓架镲架 +c6d5c9e68467b108a7f7e02f36b0b49a:鼓棒鼓锤 +1cac27c660d7b098b108d382c4e6ea42:唢呐 +1cac27c660d7b098267e0a01017d9f44:埙 +f22578f0c6a8eaa5267e0a01017d9f44:尺八 +f22578f0c6a8eaa51921b9e373cead75:巴乌 +1cac27c660d7b09828d88a08a19453dc:笙 +f22578f0c6a8eaa5acde29f76907b07f:笛子 +f22578f0c6a8eaa5e10cfa39bf43dc0f:管子 +1cac27c660d7b0981921b9e373cead75:箫 +1cac27c660d7b098a7f7e02f36b0b49a:芦笙 +1cac27c660d7b09837b3d33c282f2081:葫芦丝 +f22578f0c6a8eaa56f38848189966e7d:葫芦笙 +1cac27c660d7b098ac899d2620c5df2b:陶笛 +879b743300e7a581acde29f76907b07f:三弦 +1cac27c660d7b098e10cfa39bf43dc0f:冬不拉 +1cac27c660d7b0986f38848189966e7d:古琴 +1cac27c660d7b098acde29f76907b07f:弹布尔 +879b743300e7a581e10cfa39bf43dc0f:扬琴 +879b743300e7a5816f38848189966e7d:月琴 +879b743300e7a58128d88a08a19453dc:柳琴 +879b743300e7a5811921b9e373cead75:热瓦普 +879b743300e7a581b108d382c4e6ea42:琵琶 +879b743300e7a581ac899d2620c5df2b:秦琴 +879b743300e7a581a7f7e02f36b0b49a:箜篌 +879b743300e7a581267e0a01017d9f44:阮 +a2eba09f5b889a7c28d88a08a19453dc:中胡 +7d61e938542f6790b108d382c4e6ea42:二胡 +7d61e938542f6790267e0a01017d9f44:京二胡 +7d61e938542f6790acde29f76907b07f:京胡 +7d61e938542f679028d88a08a19453dc:低音胡 +a2eba09f5b889a7c37b3d33c282f2081:四胡 +a2eba09f5b889a7cb108d382c4e6ea42:坠琴 +7d61e938542f6790a7f7e02f36b0b49a:板胡 +a2eba09f5b889a7ca7f7e02f36b0b49a:椰胡 +7d61e938542f679037b3d33c282f2081:艾捷克 +7d61e938542f67901921b9e373cead75:革胡 +7d61e938542f67906f38848189966e7d:马头琴 +7d61e938542f6790e10cfa39bf43dc0f:马骨胡 +7d61e938542f6790ac899d2620c5df2b:高胡 +882b39ff0db2dd0037b3d33c282f2081:军镲 +00a32e7ff35aaf9e267e0a01017d9f44:大钹 +00a32e7ff35aaf9ee10cfa39bf43dc0f:大铙 +00a32e7ff35aaf9eacde29f76907b07f:大顶钹 +00a32e7ff35aaf9e1921b9e373cead75:川钹 +00a32e7ff35aaf9e6f38848189966e7d:广钹 +882b39ff0db2dd00a7f7e02f36b0b49a:快板 +882b39ff0db2dd0028d88a08a19453dc:拍板 +00a32e7ff35aaf9eb108d382c4e6ea42:梆子 +882b39ff0db2dd001921b9e373cead75:水镲 +882b39ff0db2dd00b108d382c4e6ea42:碰钟 +882b39ff0db2dd00acde29f76907b07f:秧歌镲 +882b39ff0db2dd00e10cfa39bf43dc0f:腰鼓镲 +882b39ff0db2dd00ac899d2620c5df2b:萨巴依 +882b39ff0db2dd00267e0a01017d9f44:铜书板 +00a32e7ff35aaf9eac899d2620c5df2b:镲锅 +0ea61a801ba323c1267e0a01017d9f44:堂鼓 +00a32e7ff35aaf9e28d88a08a19453dc:战鼓 +0ea61a801ba323c11921b9e373cead75:排鼓 +0ea61a801ba323c1b108d382c4e6ea42:板鼓 +00a32e7ff35aaf9e37b3d33c282f2081:秧歌鼓 +0ea61a801ba323c1e10cfa39bf43dc0f:细腰鼓 +00a32e7ff35aaf9ea7f7e02f36b0b49a:腰鼓 +0ea61a801ba323c1ac899d2620c5df2b:花盆鼓 +0ea61a801ba323c16f38848189966e7d:象脚鼓 +0ea61a801ba323c1acde29f76907b07f:铜鼓 +a7133eb411b587cf1921b9e373cead75:空灵鼓/无忧鼓 +0ea61a801ba323c1a7f7e02f36b0b49a:云锣 +a2eba09f5b889a7c267e0a01017d9f44:京锣 +a2eba09f5b889a7cac899d2620c5df2b:低音锣 +a2eba09f5b889a7cacde29f76907b07f:开道锣 +a2eba09f5b889a7ce10cfa39bf43dc0f:手锣 +0ea61a801ba323c137b3d33c282f2081:武锣 +0ea61a801ba323c128d88a08a19453dc:舟山锣 +a2eba09f5b889a7c6f38848189966e7d:苏锣 +a2eba09f5b889a7c1921b9e373cead75:虎音锣 +33a0daa5d89d68fa1921b9e373cead75:宣纸 +b12c1c13a8dc3b2b1921b9e373cead75:吊牌 +b12c1c13a8dc3b2be10cfa39bf43dc0f:自封袋 +b12c1c13a8dc3b2b267e0a01017d9f44:贺卡明信片 +22d3cfff678abab11921b9e373cead75:书皮 +b12c1c13a8dc3b2b37b3d33c282f2081:修正带 +b12c1c13a8dc3b2b28d88a08a19453dc:修正液 +22d3cfff678abab1a7f7e02f36b0b49a:削笔器 +22d3cfff678abab128d88a08a19453dc:可爱印泥 +b12c1c13a8dc3b2bb108d382c4e6ea42:学生书包 +22d3cfff678abab1acde29f76907b07f:文具套装 +22d3cfff678abab1267e0a01017d9f44:文具盒 +22d3cfff678abab16f38848189966e7d:橡皮 +22d3cfff678abab1b108d382c4e6ea42:练字帖 +22d3cfff678abab1ac899d2620c5df2b:视力保护器 +dbaba36adf47af9637b3d33c282f2081:笔袋 +54e552aa1c9b2cbcacde29f76907b07f:彩泥橡皮泥 +bf164bd2e8dd8cebb108d382c4e6ea42:便条照片夹 +bf164bd2e8dd8ceb28d88a08a19453dc:便签盒座 +bf164bd2e8dd8cebe10cfa39bf43dc0f:卡套证件套 +bf164bd2e8dd8ceb6f38848189966e7d:名片册 +86cddebb2de0815c37b3d33c282f2081:名片盒 +bf164bd2e8dd8cebacde29f76907b07f:快劳夹 +86cddebb2de0815c28d88a08a19453dc:文件夹 +86cddebb2de0815cb108d382c4e6ea42:文件架 +bf164bd2e8dd8ceb1921b9e373cead75:档案盒 +bf164bd2e8dd8cebac899d2620c5df2b:档案袋 +86cddebb2de0815cac899d2620c5df2b:相册 +1ad9ac4511bbb8646f38848189966e7d:笔插 +bf164bd2e8dd8ceba7f7e02f36b0b49a:笔架 +bf164bd2e8dd8ceb267e0a01017d9f44:风琴包 +d665d5e1347fa192a7f7e02f36b0b49a:地球仪 +bf164bd2e8dd8ceb37b3d33c282f2081:展板 +d665d5e1347fa192267e0a01017d9f44:教学仪器器材 +d665d5e1347fa1921921b9e373cead75:教鞭 +bb9bba251ee78e59267e0a01017d9f44:旗帜 +d665d5e1347fa19237b3d33c282f2081:提示牌 +d665d5e1347fa192b108d382c4e6ea42:激光笔 +0f75076039b85f74acde29f76907b07f:白板 +0f75076039b85f74e10cfa39bf43dc0f:白板笔 +d665d5e1347fa19228d88a08a19453dc:粉笔 +d665d5e1347fa192acde29f76907b07f:绿板 +d665d5e1347fa1926f38848189966e7d:荧光板 +d665d5e1347fa192ac899d2620c5df2b:计划表 +d665d5e1347fa192e10cfa39bf43dc0f:软木板 +a457d6fc43c609bda7f7e02f36b0b49a:中性笔 +c230ba4ca293f3b5e10cfa39bf43dc0f:圆珠笔 +c230ba4ca293f3b51921b9e373cead75:铅芯 +f9910185f1984f2937b3d33c282f2081:正姿笔 +c230ba4ca293f3b5acde29f76907b07f:油漆笔 +c230ba4ca293f3b5b108d382c4e6ea42:泡泡笔 +c230ba4ca293f3b537b3d33c282f2081:墨水墨囊 +c230ba4ca293f3b5267e0a01017d9f44:荧光笔 +f4a071d4dba28eccac899d2620c5df2b:记号笔 +c230ba4ca293f3b56f38848189966e7d:针管笔 +dfdbd3409fadcd3f6f38848189966e7d:其他笔 +58e84885c426409e267e0a01017d9f44:书签 +b7fd03d456abe30128d88a08a19453dc:便签 +e9fa1ad466b79d97b108d382c4e6ea42:信封 +af2cf5b1faa3537a1921b9e373cead75:信纸 +b7fd03d456abe30137b3d33c282f2081:包装纸 +e9fa1ad466b79d97a7f7e02f36b0b49a:纪念册 +b7fd03d456abe301ac899d2620c5df2b:复写纸 +b7fd03d456abe301267e0a01017d9f44:奖状证书 +e9fa1ad466b79d971921b9e373cead75:手工纸 +e9fa1ad466b79d9728d88a08a19453dc:草稿纸 +b7fd03d456abe3016f38848189966e7d:日记本 +e9fa1ad466b79d97ac899d2620c5df2b:硬面抄 +b7fd03d456abe301a7f7e02f36b0b49a:记事本 +b7fd03d456abe301acde29f76907b07f:课业本 +e9fa1ad466b79d9737b3d33c282f2081:通讯录 +dbaba36adf47af966f38848189966e7d:磁性贴 +6c0543ec11db7e61267e0a01017d9f44:贴纸/标签 +0f75076039b85f741921b9e373cead75:圆规 +0f75076039b85f74ac899d2620c5df2b:显微镜 +1c75d8021bacf61e267e0a01017d9f44:放大镜 +a9ef3505c7fe4b66b108d382c4e6ea42:丙烯颜料 +823f8d7bd96780d0ac899d2620c5df2b:书法用纸 +a9ef3505c7fe4b66ac899d2620c5df2b:儿童填色本 +a9ef3505c7fe4b66267e0a01017d9f44:国画颜料 +823f8d7bd96780d037b3d33c282f2081:描图硫酸纸 +5fd3299edc3ff44a37b3d33c282f2081:毛边纸 +823f8d7bd96780d01921b9e373cead75:水彩笔 +823f8d7bd96780d0267e0a01017d9f44:水彩颜料 +823f8d7bd96780d0acde29f76907b07f:水粉水彩油画笔 +823f8d7bd96780d0e10cfa39bf43dc0f:水粉颜料 +0f75076039b85f7437b3d33c282f2081:油画棒 +0f75076039b85f74a7f7e02f36b0b49a:油画颜料 +a9ef3505c7fe4b66acde29f76907b07f:画板画架 +823f8d7bd96780d0b108d382c4e6ea42:石膏像 +823f8d7bd96780d06f38848189966e7d:素描本 +a9ef3505c7fe4b66e10cfa39bf43dc0f:绘图纸 +823f8d7bd96780d028d88a08a19453dc:色卡 +a9ef3505c7fe4b666f38848189966e7d:蜡笔 +823f8d7bd96780d0a7f7e02f36b0b49a:铅画纸 +7dba397e41d08d49a7f7e02f36b0b49a:裁剪刀片 +7dba397e41d08d49b108d382c4e6ea42:雕刻垫板 +7dba397e41d08d49ac899d2620c5df2b:切纸刀 +7dba397e41d08d4928d88a08a19453dc:美工刀 +356e5d8126d3aefaa7f7e02f36b0b49a:裁剪剪刀 +e9fa1ad466b79d97267e0a01017d9f44:回形针 +621bd460d751e0fca7f7e02f36b0b49a:回形针盒 +621bd460d751e0fcb108d382c4e6ea42:图钉工字钉 +e9fa1ad466b79d97e10cfa39bf43dc0f:大头针 +e9fa1ad466b79d97acde29f76907b07f:打孔机 +621bd460d751e0fc28d88a08a19453dc:票夹长尾夹 +e9fa1ad466b79d976f38848189966e7d:订书钉 +a457d6fc43c609bd1921b9e373cead75:凭证 +a457d6fc43c609bde10cfa39bf43dc0f:印油印泥 +a457d6fc43c609bd28d88a08a19453dc:报表 +a457d6fc43c609bd267e0a01017d9f44:湿手器 +a457d6fc43c609bdb108d382c4e6ea42:财务证明用品 +a457d6fc43c609bd6f38848189966e7d:账本账册 +740736cf215b7509a7f7e02f36b0b49a:电子壁纸 \ No newline at end of file diff --git a/modules/xianYu/xianYu.go b/modules/xianYu/xianYu.go new file mode 100644 index 0000000..f82de2b --- /dev/null +++ b/modules/xianYu/xianYu.go @@ -0,0 +1,94 @@ +package xianYu + +import ( + "fmt" + "os" + "path/filepath" + "planA/initialization/golabl" + "syscall" + "unsafe" +) + +var ( + gXianYuDll *XianYuDLL +) + +// XianYuDLL 闲鱼工具DLL结构 +type XianYuDLL struct { + Dll *syscall.DLL + freeCString *syscall.Proc // 释放C字符串 +} + +// InitXianYuDll 初始化 XianYuDLL +func InitXianYuDll() (*XianYuDLL, error) { + if gXianYuDll != nil { + return gXianYuDll, nil + } + dllPath := filepath.Join(golabl.Config.FileUrl.XianYuDll, "xy.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("XianYu DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载XianYu DLL 失败: %s", err) + } + gXianYuDll = &XianYuDLL{ + Dll: dll, + freeCString: dll.MustFindProc("FreeCString"), + } + return gXianYuDll, nil +} + +// XianYuGoodsAdd 商品新增 +func (m *XianYuDLL) XianYuGoodsAdd(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsCreat") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsCreat: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// XianYuLaunchGoods 商品上架 +func (m *XianYuDLL) XianYuLaunchGoods(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsPublish") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsPublish: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} diff --git a/modules/xianYu/xy.dll b/modules/xianYu/xy.dll new file mode 100644 index 0000000..af6630d Binary files /dev/null and b/modules/xianYu/xy.dll differ diff --git a/modules/xianYu/咸鱼发布dll.md b/modules/xianYu/咸鱼发布dll.md new file mode 100644 index 0000000..000ac8c --- /dev/null +++ b/modules/xianYu/咸鱼发布dll.md @@ -0,0 +1,239 @@ +##### FreeCString(str *C.char) + +接收其他函数返回值之后,释放内存,参考示例 + +##### 内存释放示例 + +```go +func example () { + // ...其他逻辑 + var res = StartServer (configFile *C.char) + FreeCString(res) //释放内存 +} +``` + + + +##### StartServer (configFile *C.char) + +启动http服务器,参数配置文件路径,不提供默认使用工程根目录config.ini + +返回C字符串启动消息,接收后使用FreeCString进行内存释放 + + + +##### StopServer + +停止HTTP服务器 + +返回C字符串停止消息,接收后使用FreeCString进行内存释放 + + + +##### GetServerStatus + +获取服务器当前状态 + +返回C字符串指针消息,running/stopped,接收后使用FreeCString进行内存释放 + + + +##### GetServerAddress + +获取服务器监听地址 + +返回C字符串指针服务器地址消息,未运行返回空串,接收后使用FreeCString进行内存释放 + + + +##### ReloadConfig(configFile *C.char) + +重新加载配置文件,参数配置文件路径,不提供默认使用根目录config.ini + +返回C字符串加载结果消息,接收后使用FreeCString进行内存释放 + + + + + +### 以下都需要传递appid和appSecret ### + +##### ExecuteGoodsCreat(bodyJson *C.char, configFile *C.char) + +*管道通信直接调用此函数* + +执行商品创建操作,参数商品信息,参考示例 + +返回C字符串指针创建商品结果信息,接收后使用FreeCString进行内存释放 + + + +##### 商品信息参考示例 + +```json +{ + "appId": 1228288260261189, + "appSecret": "aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k", + "token": "", + "apiShopId": 0, + "typePlatform": 4, + "shopId": 0, + "shopToken": "", + "shopName": "", + "province": 210000, + "city": 210100, + "district": 210101, + "typeClass": "", + "typeGoods": "", + "catIds": "d14d229692616168b108d382c4e6ea42", + "shop": [ + { + "userName": "xy938400231518", + "province": 210000, + "city": 210100, + "district": 210101, + "title": "牧羊少年奇幻之旅", + "content": "牧羊少年奇幻之旅", + "mainImgs": ["https://img.cdn1.vip/i/68cf5cb4e5840_1758420148.webp"], + "contentImgs": [] + } + ], + "stuffStatus": 90, + "bookData": [ + { + "ISBN": "9787530217054", + "Title": "牧羊少年奇幻之旅", + "Author": "保罗·柯艾略", + "Publisher": "北京十月文艺出版", + "itemBizType": 2, + "spBizType": 24, + "prices": [199999, 299999], + "stock": 100, + "catIds": "22e1d81dc4cf3a25a7f7e02f36b0b49a" + } + ], + "itemKey": "itemAAAAA1111" +} +``` + + + +##### ExecuteGoodsPublish(bodyJson *C.char, configFile *C.char) + +*管道通信直接调用此函数* + +执行商品上架操作,参数上架信息,参考示例 + +返回C字符串指针行商品上架结果信息,接收后使用FreeCString进行内存释放 + +##### 上架信息参考示例 + +```json +{ + "product_id": 1250927879325125, + "user_name": ["xy938400231518"], + "specify_publish_time": "", + "notify_url": "" +} +``` + + + +#### 追加下架,改价,擦亮 #### + +##### ExecuteGoodsDownShelf(bodyJson *C.char, configFile *C.char) ###### + +*管道通信直接调用此函数* + +执行商品下架操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品下架结果信息,接收后使用FreeCString进行内存释放 + +##### 下架信息参考示例 ##### + +```json +{ + "product_id": 1250927879325125 +} +``` + + + +##### ExecuteGoodsFlash(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品擦亮操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品擦亮结果信息,接收后使用FreeCString进行内存释放 + +##### 擦亮信息参考示例 ##### + +```json +{ + "product_id": 1250927879325125 +} +``` + + + +##### ExecuteGoodsEditPrice(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品改价操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 改价信息参考示例(单位:分) ##### + +```json +{ + "product_id": 1250927879325125, + "price": 550000, + "originalPrice": 770000 +} +``` + + + +##### ExecuteGoodsEditStock(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品改库存操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 改库存信息参考示例(单位:分) ##### + +```json +{ + "product_id": 1250927879325125, + "stock": 10 +} +``` + + + +##### ExecuteSelectGoodsListPrice(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +查询店铺列表操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 查询参考示例(单位:分) ##### + +```json +{ + //online_time 字段可传空 + "online_time": [ + 1690300800, + 1690366883 + ], + "product_status": 22 +} +``` + diff --git a/planA.md b/planA.md new file mode 100644 index 0000000..2633641 --- /dev/null +++ b/planA.md @@ -0,0 +1,46 @@ +# Plan A +## 目录结构 +```gotemplate +controller 逻辑控制 +controlState 全局状态控制 + |-lock 状态锁 + |-serviceAlive 服务存活状态 +export 导出的csv文件 +initialization 初始化 + |-config 初始化配置文件 + |-cron 初始化定时任务 + |-golabl 初始化全局变量 + |-middle 初始化中间件 + |-mysql 初始化mysql数据库 + |-redis 初始化redis数据库 + |-router 初始化路由 + |-sqlite 初始化sqlite数据库 + |-validator 初始化验证器 + |-init.go 初始化文件 +logs 日志 +modules DLL模块 +planB 模块B +rep 工厂模式接口 +router 路由 +service 服务(针对数据库相关操作) +tool 工具 +type 结构体 + |-mysql mysql结构体 + |-redis redis结构体 + |-sqlite sqlite结构体 + |-validator 验证器结构体 +validator 验证器 +config.yaml 配置文件 +taskDb.db sqlite数据库(自动创建) +``` + +## 目录结构 + +``` +planA web服务器 +planB 任务执行器 +planC 同步redis数据到硬盘 +planD 删除任务 +planE 图片上传到拼多多图片空间(未使用) +planF 获取12个商品信息,主要转发小军的接口信息 +``` \ No newline at end of file diff --git a/planB/config.yaml b/planB/config.yaml new file mode 100644 index 0000000..3d380fb --- /dev/null +++ b/planB/config.yaml @@ -0,0 +1,128 @@ +server: + port: "8080" #服务器端口 + f_port : "8284" #F程序端口 + filter: 1 #是否开启违禁词过滤器 0=关闭 1=开启 + replace_mark: "0" #标题违规词是否替换* 0 不替换 1 替换(替换会继续发布,不替换则不发布) + redis_exp: 192 #redis过期时间 192小时(8天) + read_db: "sqlite" #读数据库 mysql sqlite + err_pause_time: 3000 #错误暂停时间(毫秒) + sign_key: "jRQdCh52Z55Kzh1hADaA2ZtdTKetj2PXk60Tz5Yc0iz9aD8Wafbk7CwAZ8cz69A9zb9caZ3k9dnR3Ys06J5nYFPrZ0xE9p6TY8DCD538ryiRjW81YTPmk41tCEnXizPh" #签名密钥 + data_day: 2 #数据保存时间(天) + is_c: true #是否启动 C程序 +speed: #限速器 + pdd_speed: 18 #拼多多 每秒多少个任务 + xianyu_speed: 5 #闲鱼 每秒多少个任务 + watermark: 15 #打水印速率的个数 +minio: #minio 图片空间 + url: "shxy.image.yushutx.com" #minio地址 + access_key_id: "minioadmin" #minio keyId + secret_access_key: "minioadmin" #minio key + bucket_name: "task-xianyu" #存储桶 + target_dir: "test/2025" #目标目录 + use_ssl: true #是否使用 SSL +alive: + fluent: 50 #存活状态-流畅时间(毫秒) + slow: 200 #存活状态-缓慢时间(毫秒) +pool_config: + size: 6 #协程数量 + with_expiry_duration: 10 #过期时间 + with_pre_alloc: true #预分配 + with_max_blocking_tasks: 600 #阻塞任务数 + with_nonblocking : true #非阻塞 +mysql_config: + db_name: "task_user" #数据库名称 + user: "root" #数据库用户名 + password: "root" #数据库密码 + host: "127.0.0.1" #数据库地址 + port: 3306 #数据库端口 + loglevel: "info" #数据库日志级别 info=开发环境:输出所有日志(SQL、执行时间等) warn=预发布:输出警告+错误 error=生产环境:只输出错误日志 silent=生产环境:关闭所有日志 + max_retry_times: 3 #数据库重试次数 + base_retry_interval: 100 #数据库重试间隔(毫秒) + max_retry_interval: 3 #数据库最大重试间隔(秒) + max_open_conns: 100 #数据库最大打开连接数 + max_idle_conns: 20 #数据库最大空闲连接数 + conn_max_idle_time: 30 #数据库连接最大空闲时间(分钟) + conn_max_lifetime: 1 #数据库连接最大生命周期(小数) +redis_config: + - db_name: "任务池" + db: 0 + addr: "127.0.0.1:6379" + password: "123456" + - db_name: "书品库" + db: 7 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "店铺信息" + db: 8 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "出版社信息列表" + db: 3 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "省市区列表" + db: 4 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有图片的 isbn" + db: 5 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有书籍的 isbn" + db: 6 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "拼多多操作回调数据库" + db: 14 + addr: "36.212.20.113:7963" + password: "j8nZ4jra2E" +pdd_config: + client_id: "203c5a7ba8bd4b8488d5e26f93052642" #拼多多clientId + client_secret: "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" #拼多多clientSecret +kfz_config: + app_id: 576 #孔夫子appid + app_secret: "256e10220c5b307f5172b1a49c11467a6cfa8038bbe2a7feccc42231852324f8" #孔夫子appsecret +taobao_config: + app_key: 852496 #平台分配的 App Key + app_secret: "6876bd91e93840wet8d264255a24d892" #App 密钥,用于 HMAC-MD5 签名计算 + token: "f614764edd33345a41acd10ed3edaa9d" #App 密钥,用于 HMAC-MD5 签名计算 + ati: "455224047430" #设备/用户标识 + user_id: "1779953588275" #当前操作用户 ID + company_id: "1779953588275" #公司/店铺所属 ID + base_url: "https://fxzs.yulinkai.com" #API 基础域名 + local_img_dir: "D:\\file\\taobao" #图片本地缓存目录 + request_timeout: 20 #请求超时时间(秒) +http_url: + task_url: "http://127.0.0.1:8080" #A 程序接口地址 +file_url: + xian_yu_dll: "D:\\source\\planA\\planB\\modules\\xianYu" #闲鱼 DLL库路径 + pdd_dll: "D:\\source\\planA\\planB\\modules\\pdd" #拼多多 DLL库路径 + kfz_dll: "D:\\source\\planA\\planB\\modules\\kfz" #孔夫子 DLL库路径 + log_dll: "D:\\source\\planA\\planB\\modules\\logs" #日志 DLL库路径 + image_dll: "D:\\source\\planA\\planB\\modules\\image" #水印 DLL库路径 + b_file_name: "D:\\source\\planA\\planB\\planB.exe" #B 程序文件路径 + c_file_name: "D:\\source\\planA\\planC\\planC.exe" #C 程序文件路径 + d_file_name: "D:\\source\\planA\\planD\\planD.exe" #D 程序文件路径 + e_file_name: "D:\\source\\planA\\planE\\planE.exe" #E 程序文件路径 + f_file_name: "D:\\source\\planA\\planE\\planF.exe" #F 程序文件路径 + create_task_url: "https://api.buzhiyushu.cn/zhishu/baseInfo/addNewTask" #新增任务接口 + create_task_notice_url: "http://36.212.12.92:8055/task" #核价软件提交数据通知接口 + create_operation_task_notice_url: "http://36.212.12.92:8055/taskV2" #操作商品任务核价软件提交数据通知接口 + banned_word_substitution_url : "http://36.212.12.247:13001/task/getFilterSetNew" #违禁词替换接口 + pdd_token_url: "https://api.buzhiyushu.cn/huidiao/pdd/getToken" #获取系统规定拼多多 token + deduction_url: "https://api.buzhiyushu.cn/zhishu/userRecharge/apiBalancePayment" #扣费接口 + pdd_get_goods_url: "http://pdd.buzhiyushu.cn/api/pdd/auth/getShopGoodsList" #查询拼多多商品接口 + pdd_get_goods_detail_url: "http://pdd.buzhiyushu.cn/api/pdd/auth/newGetShopGoodsDetailList" #查询拼多多商品详情列表接口 + pdd_add_goods_url: "http://119.45.237.193:14003/task/putShopGoods" #添加拼多多商品接口 + pdd_get_sku_id: "http://192.168.101.127:18099/shopGoods/getShopGoods" #批量获取 skuId接口 + xianyu_add_goods_url: "http://119.45.237.193:14008/task/putShopGoods" #添加闲鱼商品接口 + kfz_add_goods_url: "http://119.45.237.193:14009/task/kfzverifyPricePublishGoods" #添加孔夫子商品接口 + del_task_url: "http://119.45.237.193:14008/shopGoods/delShopGoodsk" #删除任务通知接口 + backup_url: "D:\\file\\backup" #备份文件路径 + pdd_goods_details_url: "D:\\file\\pdd_goods_details" #保存拼多多详情路径 + update_token_url: "http://146.56.227.42:9099/api/updateToken" #更新拼多多 token 到redis + kfz_img_temp_url: "D:\\file\\kfzImg" #孔夫子图片临时路径 + kfz_img_http_url: "https://www0.kfzimg.com/" #孔夫子图片 http 路径 + get_pdd_goods_shopid_isbn_url : "http://119.45.237.193:14008/shopGoods/selectTrilateralIds" #获取拼多多商品 shopId 和 isbn + get_subscription_expiration_date_url : "http://119.45.237.193:9096/api/user/getKfzUserRecbusiness" #获取订阅到期时间 + pdd_img_temp_url: "D:\\file\\pddImg" #拼多多图片临时路径 \ No newline at end of file diff --git a/planB/dispatcher/dispatcher.go b/planB/dispatcher/dispatcher.go new file mode 100644 index 0000000..5369b3b --- /dev/null +++ b/planB/dispatcher/dispatcher.go @@ -0,0 +1,36 @@ +package dispatcher + +import ( + "fmt" + "planA/planB/initialization/golabl" + planAType "planA/type" +) + +// Go 调度任务 +// @param bodyWait 任务体 +// @return string 任务ID +// @return error 错误信息 +func Go(bodyWait planAType.TaskBody) (string, error) { + switch golabl.TaskType { + case golabl.TaskTypeAddGoodsTask: + return golabl.Platform.AddGoodsTask(bodyWait) // 添加商品 + + //挪到了main方法中执行 + //case "GetGoodsTask": + // return golabl.Platform.GetGoodsTask() // 获取商品 + + case golabl.TaskTypeSetGoodsTask: + + return golabl.Platform.SetGoodsTask(), nil // 修改商品 + + case golabl.TaskTypeOperationGoodsTask: + return golabl.Platform.OperationGoodsTask(bodyWait) // 操作商品 + + case golabl.TaskTypeIncStock: + return golabl.Platform.IncStockTask(bodyWait) // 增量库存 + + default: + + return "", fmt.Errorf("没有此任务类型") + } +} diff --git a/planB/dispatcher/kongfuzi/kongfizi.go b/planB/dispatcher/kongfuzi/kongfizi.go new file mode 100644 index 0000000..1656103 --- /dev/null +++ b/planB/dispatcher/kongfuzi/kongfizi.go @@ -0,0 +1,1633 @@ +package kongfuzi + +import ( + "encoding/json" + "errors" + "fmt" + "planA/planB/initialization/golabl" + "planA/planB/logic" + "planA/planB/modules/logs" + "planA/planB/service" + "planA/planB/tool" + planBTypeKfz "planA/planB/type/kfz" + planAType "planA/type" + "strconv" + "strings" + "time" +) + +type KongFuZi struct { +} + +// NewKongFuZi 创建孔夫子平台 +func NewKongFuZi() *KongFuZi { + return &KongFuZi{} +} + +// AddGoodsTask 添加商品 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return error 错误 +func (kongFuZi *KongFuZi) AddGoodsTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + + taskMsg, publishGoodsErr := publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, publishGoodsErr) + } + + taskMsg.Detail.Error = "发布成功!" + return tool.ReturnSuccess(taskMsg) +} + +// GetGoodsTask 获取商品 +// @return string body 信息 +// @return error 错误 +func (kongFuZi *KongFuZi) GetGoodsTask() (string, error) { + // 生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + + const pageSize = 100 + const maxRecordsPerRange = 10000 // 每个时间范围最多获取10000条 + const timeRangeDays = 30 // 时间范围天数 + + var lastAddTime int64 = 0 // 上次拉取的最后一条商品的添加时间(秒级时间戳) + + // 统计变量 + totalFetched := 0 // 总共获取到的商品数(包括重复) + duplicateCount := 0 // 重复商品数量 + uniqueCount := 0 // 不重复商品数量 + + //查询body_wait是否存在,如果存在则证明不是第一次执行 + //要获取body_wait中最后一条数据的添加时间作为查询的开始时间 + exist, isTaskBodyWaitExistErr := service.IsTaskBodyWaitExist() + if isTaskBodyWaitExistErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, isTaskBodyWaitExistErr) + } + + if exist { + // 获取最后一条数据的添加时间 + lastBodyWaitDataJson, getLastGoodsAddTimeErr := service.GetTaskBodyWaitLast() + if getLastGoodsAddTimeErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, getLastGoodsAddTimeErr) + } + // 解析 lastBodyWaitData 到结构体 + var lastBodyWaitData planAType.TaskBody + unmarshalErr := json.Unmarshal([]byte(lastBodyWaitDataJson), &lastBodyWaitData) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, unmarshalErr) + } + // 将数据的添加时间给到 lastAddTime + lastAddTime = lastBodyWaitData.BookInfo.Price + // 第二阶段:使用时间范围分批拉取(goroutine + 120秒超时,防止 DLL 永久阻塞) + phaseTwoResultCh := make(chan error, 1) + go func() { + fmt.Println("[DEBUG] PhaseTwoGoods goroutine started (exist=true branch)") + phaseTwoResultCh <- PhaseTwoGoods(pageSize, &totalFetched, &lastAddTime, maxRecordsPerRange, timeRangeDays) + }() + // 等待 PhaseTwoGoods 完成或超时 + select { + case phaseTwoErr := <-phaseTwoResultCh: + fmt.Printf("[DEBUG] PhaseTwoGoods completed (exist=true): %v\n", phaseTwoErr) + if phaseTwoErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, phaseTwoErr) + } + case <-time.After(120 * time.Second): + fmt.Println("[ERROR] PhaseTwoGoods 超时(120秒),可能是 DLL 函数 KongfzShopItemList 不可用") + return "", fmt.Errorf("PhaseTwoGoods 超时(120秒),可能是 DLL 函数 KongfzShopItemList 不可用") + } + } else { + // 第一阶段:获取第一页商品数量作为总数 + firstTimeGoodsErr := phaseOneGoods(pageSize, &totalFetched) + if firstTimeGoodsErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, firstTimeGoodsErr) + } + + // 计算起始时间(当前时间往前180天) + lastAddTime = time.Now().Unix() - 180*24*60*60 + // 第二阶段:使用时间范围分批拉取(goroutine + 120秒超时,防止 DLL 永久阻塞) + phaseTwoResultCh := make(chan error, 1) + go func() { + fmt.Println("[DEBUG] PhaseTwoGoods goroutine started") + phaseTwoResultCh <- PhaseTwoGoods(pageSize, &totalFetched, &lastAddTime, maxRecordsPerRange, timeRangeDays) + }() + // 等待 PhaseTwoGoods 完成或超时 + select { + case phaseTwoErr := <-phaseTwoResultCh: + fmt.Printf("[DEBUG] PhaseTwoGoods completed: %v\n", phaseTwoErr) + if phaseTwoErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, phaseTwoErr) + } + case <-time.After(120 * time.Second): + fmt.Println("[ERROR] PhaseTwoGoods 超时(120秒),可能是 DLL 函数 KongfzShopItemList 不可用") + return "", fmt.Errorf("PhaseTwoGoods 超时(120秒),可能是 DLL 函数 KongfzShopItemList 不可用") + } + } + //更新状态为推送中 + updateTaskStatusErr := service.UpdateTaskStatus(planAType.TaskStatusPushTaskStatus) + if updateTaskStatusErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, updateTaskStatusErr) + } + + // 重新设置任务进度 + if updateTaskHeaderErr := service.SetTaskCount(strconv.FormatInt(golabl.Task.Footer.TaskCountTrue, 10)); updateTaskHeaderErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, updateTaskHeaderErr) + } + + // 去重复与保存 + deduplicateToBodyOverErr := deduplicateToBodyOver(&duplicateCount, &uniqueCount) + if deduplicateToBodyOverErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, deduplicateToBodyOverErr) + } + + // 输出统计信息 + statsLogMsg := fmt.Sprintf(` +════════════════════════════════════════════════════════════════ +【孔夫子店铺拉取】 +请求ID:%s +时间: %s +店铺ID:%v +店铺名称:%v +总共获取商品数(含重复): %d +不重复商品数: %d +重复商品数: %d +重复率: %.2f%% +════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + golabl.Task.TaskId, + golabl.Task.Header.ShopName, + totalFetched, + uniqueCount, + duplicateCount, + float64(duplicateCount)/float64(totalFetched)*100) + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, statsLogMsg) + + return tool.ReturnSuccess(planAType.TaskBody{}) +} + +// OperationGoodsTask 操作商品 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return string error 错误 +func (kongFuZi *KongFuZi) OperationGoodsTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + switch taskMsg.Detail.Status { + case 1: + return executeGoodsLaunch(logUuid, taskMsg) //上架 {"book_info":{"isbn":"9787115600387"},"detail":{"goods_id":1562238986012229,"status":1}} + case 2: + return executeGoodsDownShelf(logUuid, taskMsg) // 下架 {"book_info":{"isbn":"9787115600387"},"detail":{"goods_id":1562238986012229,"status":2}} + case 4: + return executeGoodsUpdateStock(logUuid, taskMsg) //修改商品库存 {"book_info":{"isbn":"9787115600387"},"detail":{"goods_id":1562238986012229,"status":4,"stock":2}} + case 5: + return executeGoodsUpdatePrice(logUuid, taskMsg) //修改商品价格 {"book_info":{"isbn":"9787115600387"},"detail":{"goods_id":1562238986012229,"status":5,"price":5000}} + case 6: + //发布商品 + taskMsg, publishGoodsErr := publishGoods(logUuid, taskMsg) //{"book_info":{"isbn":"9787800822780","detail":{"price":5000,"shipping_cost":5000,"status":6,"stock":1 }} + if publishGoodsErr != nil { + return "", publishGoodsErr + } + return tool.ReturnSuccess(taskMsg) + case 7: + //删除商品 + logic.DelTask(taskMsg) + //延迟 10 秒 + time.Sleep(10 * time.Second) + //发布商品 + taskMsg, publishGoodsErr := publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, publishGoodsErr) + } + return tool.ReturnSuccess(taskMsg) + default: + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("未知操作类型")) + } +} + +// IncStockTask 增量库存 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return error 错误 +func (kongFuZi *KongFuZi) IncStockTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + // 获取商品id + getGoodsByShopIdAndIsbn, GetGoodsByShopIdAndIsbnErr := tool.GetGoodsByShopIdAndIsbn(golabl.Task.Header.ShopId, taskMsg.BookInfo.Isbn) + if GetGoodsByShopIdAndIsbnErr != nil { + return "", GetGoodsByShopIdAndIsbnErr + } + if getGoodsByShopIdAndIsbn.Code != "200" { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("请求ERP获取商品编码与skuid失败: %v", getGoodsByShopIdAndIsbn)) + } + if len(getGoodsByShopIdAndIsbn.Data) == 0 { + //新发布 + task, addGoodsTaskErr := kongFuZi.AddGoodsTask(taskMsg) + if addGoodsTaskErr != nil { + return "", addGoodsTaskErr + } + return task, nil + } else { + // 当前任务的品相和价格 + taskCondition := taskMsg.Detail.Condition + taskPrice := taskMsg.Detail.Price // 单位:分 + + // 价格 + 运费(如果 PriceType != "0") + if golabl.Task.Header.PriceType != "0" { + taskPrice = taskPrice + taskMsg.Detail.ShippingCost + } + + // 价格模板计算 + taskPrice = tool.BuildPrice(golabl.Task.Header.PriceMod, taskPrice) + if taskPrice == 0 { + taskMsg.Detail.Error = "任务价格不在价格模板区间内!" + return tool.ReturnSuccess(taskMsg) + } + + // 1元 = 100分,价格相差1元以上即 >= 100分 + const priceDiffThreshold = 100 // 1元 + + // 收集所有匹配条件的商品(品相相等且价格差<1元) + var matchedItems []struct { + TrilateralId string + SkuId string + Stock int64 + Price int64 + PriceDiff int64 // 价格差绝对值 + } + + // 记录不匹配原因 + hasConditionMismatch := false + hasPriceMismatch := false + var firstMismatchReason string + + for _, item := range getGoodsByShopIdAndIsbn.Data { + // 解析品相 + itemQuality, _ := strconv.ParseInt(item.Quality, 10, 64) + // 解析价格(单位:分) + itemPrice, _ := strconv.ParseInt(item.TotalPrice, 10, 64) + // 解析库存 + itemStock, _ := strconv.ParseInt(item.Stock, 10, 64) + + // 计算价格差(绝对值) + priceDiff := abs(itemPrice - taskPrice) + + // 品相不相等 + if itemQuality != taskCondition { + hasConditionMismatch = true + if firstMismatchReason == "" { + firstMismatchReason = fmt.Sprintf("商品[%s]品相不匹配: 任务品相=%d, 商品品相=%d", item.TrilateralId, taskCondition, itemQuality) + } + continue + } + + // 价格相差1元以上 + if priceDiff >= priceDiffThreshold { + hasPriceMismatch = true + if firstMismatchReason == "" { + firstMismatchReason = fmt.Sprintf("商品[%s]价格相差超过1元: 任务价格=%d分, 商品价格=%d分, 差价=%d分", item.TrilateralId, taskPrice, itemPrice, priceDiff) + } + continue + } + + // 品相相等且价格相差小于1元 → 加入候选列表 + matchedItems = append(matchedItems, struct { + TrilateralId string + SkuId string + Stock int64 + Price int64 + PriceDiff int64 + }{ + TrilateralId: item.TrilateralId, + SkuId: item.SkuId, + Stock: itemStock, + Price: itemPrice, + PriceDiff: priceDiff, + }) + } + + // 逻辑: + // 1. 所有品相不相等 → 重新发布(matchedItems为空,因为没有品相相等的) + // 2. 所有价格相差1元以上 → 重新发布(matchedItems为空,因为没有价格差<1元的) + // 3. 否则 → 找到价格相差最小的增加库存,如果多个最小差价一样则对第一条增加库存 + if len(matchedItems) == 0 { + // 所有商品都不满足条件(品相不相等 或 价格相差≥1元)→ 重新发布 + if hasConditionMismatch && hasPriceMismatch { + fmt.Printf("[重新发布] %s\n", firstMismatchReason) + } else if hasConditionMismatch { + fmt.Printf("[重新发布] %s\n", firstMismatchReason) + } else { + fmt.Printf("[重新发布] %s\n", firstMismatchReason) + } + + task, addGoodsTaskErr := kongFuZi.AddGoodsTask(taskMsg) + if addGoodsTaskErr != nil { + return "", addGoodsTaskErr + } + return task, nil + } + + // 找到价格相差最小的商品,如果多个最小差价一样则对第一条增加库存 + minDiff := int64(999999999) + var targetItem struct { + TrilateralId string + SkuId string + Stock int64 + } + + for _, item := range matchedItems { + // 找到更小的差价,或者差价相等但为第一条 + if item.PriceDiff < minDiff || (item.PriceDiff == minDiff && targetItem.TrilateralId == "") { + minDiff = item.PriceDiff + targetItem.TrilateralId = item.TrilateralId + targetItem.SkuId = item.SkuId + targetItem.Stock = item.Stock + } + } + + // 将 targetItem.TrilateralId 转为 int64 + trilateralId, trilateralIdParseIntErr := strconv.ParseInt(targetItem.TrilateralId, 10, 64) + if trilateralIdParseIntErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, trilateralIdParseIntErr) + } + + // 获取商品详情 + getGoodsListReq := planBTypeKfz.GetGoodsListReq{ + ItemId: targetItem.TrilateralId, + } + + getGoodsListReqJson, marshalErr := json.Marshal(getGoodsListReq) + if marshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, marshalErr) + } + + // 发送请求 + goodsListResp, goodsListRespErr := sendGoodsListRequest(string(getGoodsListReqJson)) + if goodsListRespErr != nil { + return "", fmt.Errorf("获取商品列表失败 %v", goodsListRespErr) + } + + //检验商品数量 + if len(goodsListResp.SuccessResponse.List) == 0 { + //在孔夫子中未查询到该商品id 重新新发布 + task, addGoodsTaskErr := kongFuZi.AddGoodsTask(taskMsg) + if addGoodsTaskErr != nil { + return "", addGoodsTaskErr + } + // 记录特殊错误 + + //将task 转为 结构体 + taskBody := planAType.TaskBody{} + unmarshalErr := json.Unmarshal([]byte(task), &taskBody) + if unmarshalErr != nil { + return "", fmt.Errorf("转换taskBody失败: %v", unmarshalErr) + } + taskBody.Detail.Error = fmt.Sprintf("未查询到商品id: %v", trilateralId) + " " + taskBody.Detail.Error + + //将taskBody 转为json + taskByte, marshalErrs := json.Marshal(taskBody) + if marshalErrs != nil { + return "", fmt.Errorf("转换taskBody失败: %v", marshalErrs) + } + task = string(taskByte) + return task, nil + } + + //增量修改库存 + taskMsg.Detail.GoodsId = trilateralId + taskMsg.Detail.Stock = taskMsg.Detail.Stock + int32(goodsListResp.SuccessResponse.List[0].Number) + quantity, updateGoodsQuantityErr := executeGoodsUpdateStock(logUuid, taskMsg) + if updateGoodsQuantityErr != nil { + return "", updateGoodsQuantityErr + } + return quantity, nil + } +} + +// abs 返回绝对值 +func abs(x int64) int64 { + if x < 0 { + return -x + } + return x +} + +func (kongFuZi *KongFuZi) SetGoodsTask() string { + //TODO implement me + return "" +} + +// *******************************私有方法************************************ // + +// UploadImageToKfz 将图片上传到孔夫子图片空间 +// @param imgUrl 图片地址 +// @return string 图片地址 +// @return error 错误 +func UploadImageToKfz(imgUrl string) (string, error) { + + //将图片保存到本地 + imgTempUrl, saveBase64ImageByDateErr := tool.SaveBase64ImageByDate(imgUrl, golabl.Config.FileUrl.KfzImgTempUrl) + if saveBase64ImageByDateErr != nil { + return "", fmt.Errorf("保存图片失败 %v", saveBase64ImageByDateErr) + } + //将图片上传到孔夫子 + upload, kfzGoodsImageUploadErr := golabl.KfzDll.KfzGoodsImageUpload(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token, imgTempUrl) + if kfzGoodsImageUploadErr != nil { + return "", fmt.Errorf("上传图片失败1 %v", kfzGoodsImageUploadErr) + } + // 解析数据 + var uploadData planBTypeKfz.UploadImgRet + unmarshalErr := json.Unmarshal([]byte(upload), &uploadData) + if unmarshalErr != nil { + return "", fmt.Errorf("解析上传图片数据失败 %v", unmarshalErr) + } + // 修复:先判断 ErrorResponse 是否为 nil + if uploadData.ErrorResponse != nil && uploadData.ErrorResponse.Code != 0 { + fmt.Println("错误码:", uploadData.ErrorResponse.Code) + return "", fmt.Errorf("上传图片失败2 错误码 %v 错误描述 %v", uploadData.ErrorResponse.Code, uploadData.ErrorResponse.SubMsg) + } + + // 修复:判断 SuccessResponse 是否为 nil + if uploadData.SuccessResponse == nil { + return "", fmt.Errorf("上传图片成功但返回数据为空") + } + return uploadData.SuccessResponse.Image.Url, nil +} + +// 商品新增 +// @param logUuid 日志ID +// @param goodsInfo 商品信息 +// @return GoodsAddResponseWrapper 商品新增结果 +// @return string 商品新增结果json +// @return error 错误信息 +func addGoods(logUuid string, goodsInfoStr string) (planBTypeKfz.AddGoodsRet, string, error) { + var goodsAdd planBTypeKfz.AddGoodsRet + //发送请求 + goodsAddStr, publishGoodsErr := golabl.KfzDll.PublishGoods(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token, string(goodsInfoStr)) + //判断是否成功 + if strings.Contains(goodsAddStr, "失败") || strings.Contains(goodsAddStr, "错误码") { + //记录请求日志 + addGoodsReqMsg := fmt.Sprintf(` + ════════════════════════════════════════════════════════════════ + 【孔夫子商品添加请求】 + 请求ID: %s + 时间: %s + 参数: %s + ════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + string(goodsInfoStr)) + + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, addGoodsReqMsg) + return goodsAdd, goodsAddStr, errors.New("孔夫子 GoodsAdd 错误:" + goodsAddStr) + } + if publishGoodsErr != nil { + return goodsAdd, "", publishGoodsErr + } + jsonUnmarshal := json.Unmarshal([]byte(goodsAddStr), &goodsAdd) + if jsonUnmarshal != nil { + return goodsAdd, "", fmt.Errorf("解析孔夫子 GoodsAdd 接口返回json失败: %v", jsonUnmarshal) + } + return goodsAdd, goodsAddStr, nil +} + +// phaseOneGoods 第一阶段拉取商品信息(获取总数) +// @param pageSize 每页数量 +// @param totalFetched 获取到的商品总数 +// @return error 错误信息 +func phaseOneGoods(pageSize int, totalFetched *int) error { + // 第一阶段:获取第一页商品,获取总数 + getGoodsListReq := planBTypeKfz.GetGoodsListReq{ + Type: "sale", // 第一阶段不传时间范围,获取所有商品的第一页 + PageNum: 1, + PageSize: pageSize, + SortOrder: "addTime", + SortType: "ASC", + } + + getGoodsListReqJson, marshalErr := json.Marshal(getGoodsListReq) + if marshalErr != nil { + return fmt.Errorf("参数转换错误 %v", marshalErr) + } + // 发送请求 + goodsListResp, goodsListRespErr := sendGoodsListRequest(string(getGoodsListReqJson)) + if goodsListRespErr != nil { + return fmt.Errorf("获取商品列表失败: %v", goodsListRespErr) + } + + if goodsListResp.SuccessResponse == nil { + return fmt.Errorf("孔夫子商品列表返回数据为空") + } + + // 更新进度总数 + totalCount := goodsListResp.SuccessResponse.Total + fmt.Println("总数 ", totalCount) + if updateTaskHeaderErr := service.SetTaskCount(strconv.Itoa(totalCount)); updateTaskHeaderErr != nil { + return updateTaskHeaderErr + } + + // 收集商品数据并写入 body_wait + for _, goods := range goodsListResp.SuccessResponse.List { + *totalFetched++ + + // 将商品转为 JSON 存入 Detail.Error + jsonData, jsonMarshalErr := json.Marshal(goods) + if jsonMarshalErr != nil { + return fmt.Errorf("将商品转为json失败: %v\n", jsonMarshalErr) + } + + // 解析添加时间转为时间戳(秒) + addTimeUnix := parseAddTimeToUnix(goods.AddTime) + + // 构建 bodyWait + bodyWait := planAType.TaskBody{ + BookInfo: planAType.BookInfo{ + Isbn: goods.Isbn, + BookName: goods.ItemName, + Author: goods.Author, + Publishing: goods.Press, + PublicationDate: goods.PubDate, + Binding: goods.Binding, + PagesCount: int64(goods.PageNum), + Format: 0, + WordsCount: 0, + Price: addTimeUnix, // 使用添加时间作为 Price 字段存储 + }, + Detail: planAType.TaskDetail{ + Error: string(jsonData), + GoodsId: goods.ItemId, + Stock: int32(goods.Number), + }, + } + + // 将 bodyWait 转为 json + bodyWaitJson, jsonMarshalErr := json.Marshal(bodyWait) + if jsonMarshalErr != nil { + return fmt.Errorf("将bodyWait转为json失败: %v\n", jsonMarshalErr) + } + + // 写入 body_wait + addTaskToBodyWaitErr := service.AddTaskToBodyWait(string(bodyWaitJson)) + if addTaskToBodyWaitErr != nil { + return addTaskToBodyWaitErr + } + } + + fmt.Printf("第一阶段 - 总数:%v 当前已取出:%v \n", totalCount, *totalFetched) + return nil +} + +// PhaseTwoGoods 第二阶段拉取商品信息(按时间范围分批拉取) +// @param pageSize 每页数量 +// @param totalFetched 获取到的商品总数 +// @param lastAddTime 最后一条数据的添加时间(秒级时间戳) +// @param maxRecordsPerRange 每次请求的时间范围最多获取的记录数 +// @param timeRangeDays 时间范围天数 +// @return error 错误信息 +func PhaseTwoGoods(pageSize int, totalFetched *int, lastAddTime *int64, maxRecordsPerRange int, timeRangeDays int) error { + fmt.Printf("[DEBUG] PhaseTwoGoods called, lastAddTime=%d, currentTime=%d\n", *lastAddTime, time.Now().Unix()) + if *lastAddTime > 0 { + currentAddTimeFrom := *lastAddTime + maxLoopCount := 100 // 最大循环次数保护 + loopCount := 0 + var lastPageGoodsList []planBTypeKfz.KfzGoodsItem // 记录上一页的商品列表 + + // 去重:加载 body_wait 中已有的 GoodsId,避免 phaseOneGoods 和 PhaseTwoGoods 重复写入同一商品 + bodyWaitCount, _ := service.GetTaskBodyWaitCount() + existingGoodsIds := make(map[int64]bool) + if bodyWaitCount > 0 { + existingPage := 1 + existingPageSize := 1000 + for { + existingList, _ := service.GetTaskBodyWaitList(existingPage, existingPageSize) + if len(existingList) == 0 { + break + } + for _, v := range existingList { + var taskBody planAType.TaskBody + if err := json.Unmarshal([]byte(v), &taskBody); err == nil { + existingGoodsIds[taskBody.Detail.GoodsId] = true + } + } + existingPage++ + } + fmt.Printf("[dedup] PhaseTwoGoods: 加载 body_wait 中已有 %d 个商品ID用于去重\n", len(existingGoodsIds)) + } + + for loopCount < maxLoopCount { + loopCount++ + + // 检查开始时间是否已超过当前时间 + if currentAddTimeFrom > time.Now().Unix() { + fmt.Printf("开始时间 %d 已超过当前时间,停止获取\n", currentAddTimeFrom) + break + } + + // 每次循环都重新设置结束时间为开始时间+timeRangeDays天 + currentAddTimeEnd := currentAddTimeFrom + int64(timeRangeDays)*24*60*60 + fmt.Printf("开始获取时间范围: %d (%s) 到 %d (%s)\n", + currentAddTimeFrom, time.Unix(currentAddTimeFrom, 0).Format("2006-01-02 15:04:05"), + currentAddTimeEnd, time.Unix(currentAddTimeEnd, 0).Format("2006-01-02 15:04:05")) + + currentPage := 1 + batchGoodsCount := 0 + lastItemAddTime := int64(0) + hasDataInRange := false + lastPageGoodsList = nil // 重置上一页商品列表 + + // 在当前时间范围内分页获取数据 + for { + getGoodsListReq := planBTypeKfz.GetGoodsListReq{ + Type: "sale", + PageNum: currentPage, + PageSize: pageSize, + AddTimeBegin: formatUnixTime(currentAddTimeFrom), + AddTimeEnd: formatUnixTime(currentAddTimeEnd), + SortOrder: "addTime", + SortType: "ASC", + } + + getGoodsListReqJson, marshalErr := json.Marshal(getGoodsListReq) + if marshalErr != nil { + return fmt.Errorf("参数转换错误 %v", marshalErr) + } + + goodsListResp, goodsListRespErr := sendGoodsListRequest(string(getGoodsListReqJson)) + if goodsListRespErr != nil { + // DLL 不可用时直接退出第二阶段,跳到去重写入步骤 + fmt.Printf("获取商品列表失败,退出第二阶段: %v\n", goodsListRespErr) + return nil + } + + if goodsListResp.SuccessResponse == nil || len(goodsListResp.SuccessResponse.List) == 0 { + // 如果当前页是第一页且没有数据 + if currentPage == 1 { + // 整个时间范围都没有数据,直接推进到结束时间 + currentAddTimeFrom = currentAddTimeEnd + fmt.Printf("时间范围 %d - %d 内无数据,推进开始时间到: %d\n", currentAddTimeFrom-int64(timeRangeDays)*24*60*60, currentAddTimeEnd, currentAddTimeFrom) + break + } + + // 当前页没有数据,但上一页有数据 + if len(lastPageGoodsList) > 0 { + lastItemOfLastPage := lastPageGoodsList[len(lastPageGoodsList)-1] + newStartTime := parseAddTimeToUnix(lastItemOfLastPage.AddTime) + + if newStartTime > currentAddTimeFrom { + currentAddTimeFrom = newStartTime + fmt.Printf("当前页无数据,使用上一页最后一条商品时间作为新开始时间: %d\n", currentAddTimeFrom) + } else { + currentAddTimeFrom = newStartTime + 1 + fmt.Printf("当前页无数据,时间相同,将时间加1秒推进: %d\n", currentAddTimeFrom) + } + } else { + currentAddTimeFrom = currentAddTimeEnd + fmt.Printf("当前页无数据且无上一页数据,将开始时间推进到结束时间: %d\n", currentAddTimeFrom) + } + + hasDataInRange = false + break + } + + // 有数据,记录上一页的商品列表 + lastPageGoodsList = goodsListResp.SuccessResponse.List + hasDataInRange = true + + // 收集商品数据并统计 + for _, goods := range goodsListResp.SuccessResponse.List { + *totalFetched++ + + // 将商品转为 JSON 存入 Detail.Error + jsonData, jsonMarshalErr := json.Marshal(goods) + if jsonMarshalErr != nil { + return fmt.Errorf("将商品转为json失败: %v\n", jsonMarshalErr) + } + + // 解析添加时间转为时间戳(秒) + addTimeUnix := parseAddTimeToUnix(goods.AddTime) + + // 构建 bodyWait + bodyWait := planAType.TaskBody{ + BookInfo: planAType.BookInfo{ + Isbn: goods.Isbn, + BookName: goods.ItemName, + Author: goods.Author, + Publishing: goods.Press, + PublicationDate: goods.PubDate, + Binding: goods.Binding, + PagesCount: int64(goods.PageNum), + Format: 0, + WordsCount: 0, + Price: addTimeUnix, + }, + Detail: planAType.TaskDetail{ + Error: string(jsonData), + GoodsId: goods.ItemId, + Stock: int32(goods.Number), + }, + } + + bodyWaitJson, jsonMarshalErr := json.Marshal(bodyWait) + if jsonMarshalErr != nil { + return fmt.Errorf("将bodyWait转为json失败: %v\n", jsonMarshalErr) + } + + // 去重:检查 GoodsId 是否已存在于 body_wait(phaseOneGoods 可能已写过) + if existingGoodsIds[goods.ItemId] { + fmt.Printf("[dedup] 跳过重复商品 GoodsId=%d (%s)\n", goods.ItemId, goods.ItemName) + continue + } + existingGoodsIds[goods.ItemId] = true + addTaskToBodyWaitErr := service.AddTaskToBodyWait(string(bodyWaitJson)) + if addTaskToBodyWaitErr != nil { + return addTaskToBodyWaitErr + } + } + + batchGoodsCount += len(goodsListResp.SuccessResponse.List) + + // 记录最后一条商品的添加时间 + lastItem := goodsListResp.SuccessResponse.List[len(goodsListResp.SuccessResponse.List)-1] + lastItemAddTime = parseAddTimeToUnix(lastItem.AddTime) + + fmt.Printf("第二阶段 - 当前时间范围已获取: %d 条,累计总数: %d,当前页码: %d,最后商品时间: %d\n", + batchGoodsCount, *totalFetched, currentPage, lastItemAddTime) + + // 判断是否需要结束当前时间范围 + if batchGoodsCount >= maxRecordsPerRange || len(goodsListResp.SuccessResponse.List) < pageSize { + if lastItemAddTime == currentAddTimeFrom { + currentAddTimeFrom = lastItemAddTime + 1 + fmt.Printf("最后商品时间与开始时间相同,推进1秒: %d -> %d\n", lastItemAddTime, currentAddTimeFrom) + } else { + currentAddTimeFrom = lastItemAddTime + } + fmt.Printf("当前时间范围已获取 %d 条数据,准备进入下一时间范围 更新开始时间为: %d \n", batchGoodsCount, currentAddTimeFrom) + break + } + + currentPage++ + + // 获取 footer 信息 + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + con := int64(len(goodsListResp.SuccessResponse.List)) + if con >= golabl.Task.Footer.TaskCountTrue { + con = golabl.Task.Footer.TaskCountTrue - con + } + if updateTaskProgressErr := tool.UpdateTaskProgress(con); updateTaskProgressErr != nil { + return updateTaskProgressErr + } + + // 暂停200毫秒 + time.Sleep(200 * time.Millisecond) + } + + // 判断是否需要继续循环 + if !hasDataInRange { + if currentAddTimeFrom > time.Now().Unix() { + fmt.Printf("开始时间 %d 已超过当前时间,停止获取\n", currentAddTimeFrom) + break + } + fmt.Printf("继续下一轮查询,新起始时间: %d\n", currentAddTimeFrom) + continue + } + + if batchGoodsCount < maxRecordsPerRange && currentAddTimeFrom > time.Now().Unix() { + fmt.Printf("当前批次获取 %d 条数据,少于 %d,且开始时间已超过当前时间,已完成所有数据获取\n", batchGoodsCount, maxRecordsPerRange) + break + } + + // 暂停200毫秒 + time.Sleep(200 * time.Millisecond) + } + + if loopCount >= maxLoopCount { + fmt.Printf("警告:已达到最大循环次数 %d,强制退出\n", maxLoopCount) + } + } + return nil +} + +// deduplicateToBodyOver 读取body_wait去重复后写入到body_over中 +// @param duplicateCount 重复商品数量 +// @param uniqueCount 不重复商品数量 +// @return error 错误信息 +func deduplicateToBodyOver(duplicateCount *int, uniqueCount *int) error { + page := 1 + pageSize := 1000 + var dataList []planBTypeKfz.KfzGoodsItem + + // 在循环外维护一个已处理的商品 ID集合 + processedGoodsIds := make(map[int64]bool) + + // 在循环前删除 body_over与body_backup,避免重复写入 + deleteTaskBodyOverErr := service.DeleteTaskBodyOver() + if deleteTaskBodyOverErr != nil { + return deleteTaskBodyOverErr + } + deleteTaskBodyBackupErr := service.DeleteTaskBodyBackup() + if deleteTaskBodyBackupErr != nil { + return deleteTaskBodyBackupErr + } + + num := 0 + + // 获取body_wait总数量 + bodyWaitCount, getTaskBodyWaitCountErr := service.GetTaskBodyWaitCount() + if getTaskBodyWaitCountErr != nil { + return getTaskBodyWaitCountErr + } + pageTotal := (bodyWaitCount + int64(pageSize) - 1) / int64(pageSize) + + for { + list, getTaskBodyOverListErr := service.GetTaskBodyWaitList(page, pageSize) + if getTaskBodyOverListErr != nil { + return getTaskBodyOverListErr + } + if len(list) <= 0 { + break + } + + for _, v := range list { + var taskBody planAType.TaskBody + jsonUnmarshalErr := json.Unmarshal([]byte(v), &taskBody) + if jsonUnmarshalErr != nil { + return fmt.Errorf("将json转为结构体失败: %v\n", jsonUnmarshalErr) + } + + if !processedGoodsIds[taskBody.Detail.GoodsId] { + processedGoodsIds[taskBody.Detail.GoodsId] = true + *uniqueCount++ + + var kfzGoodsItem planBTypeKfz.KfzGoodsItem + jsonUnmarshalErr = json.Unmarshal([]byte(taskBody.Detail.Error), &kfzGoodsItem) + if jsonUnmarshalErr != nil { + return fmt.Errorf("将json转为结构体失败: %v\n", jsonUnmarshalErr) + } + + dataList = append(dataList, kfzGoodsItem) + + // 写入到body_over + taskBody.Detail.Status = 1 + addTaskToBodyOverErr := service.AddTaskToBodyOver(taskBody, []string{"body_over", "body_backup"}) + if addTaskToBodyOverErr != nil { + return addTaskToBodyOverErr + } + } else { + *duplicateCount++ + } + } + + // 将获取的数据推送写入店铺商品数据接口(待实现) + if len(dataList) > 0 { + pageFlag := 0 + //如果是最后一批数据则将 pageFlag 设置为 1 + if page == int(pageTotal) { + pageFlag = 1 + } + pushShopGoodsDataRet, pushShopGoodsDataErr := pushShopGoodsData(dataList, pageFlag) + if pushShopGoodsDataErr != nil { + return pushShopGoodsDataErr + } + var pushShopGoodsDatas planBTypeKfz.AddGoodsToErpRet + jsonUnmarshalErr := json.Unmarshal([]byte(pushShopGoodsDataRet), &pushShopGoodsDatas) + if jsonUnmarshalErr != nil { + return fmt.Errorf("将json转为结构体失败: %v\n", jsonUnmarshalErr) + } + if pushShopGoodsDatas.Code != 200 { + return fmt.Errorf("推送商品数据失败: %v\n", pushShopGoodsDatas.Msg) + } + fmt.Printf("当前页 %v 总页数 %v 当前数据量 %v 总数据量 %v \n", page, pageTotal, len(dataList), num) + } + + num = num + len(dataList) + page++ + + // 获取 footer信息 + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + con := int64(len(list)) + if con >= golabl.Task.Footer.TaskCountTrue { + con = golabl.Task.Footer.TaskCountTrue - con + } + if updateTaskProgressErr := tool.UpdateTaskProgress(con); updateTaskProgressErr != nil { + return updateTaskProgressErr + } + + // 清空 dataList + dataList = []planBTypeKfz.KfzGoodsItem{} + // 暂停1秒 + time.Sleep(1 * time.Second) + } + + // 删除body_wait + deleteTaskBodyWaitErr := service.DeleteTaskBodyWait() + if deleteTaskBodyWaitErr != nil { + return deleteTaskBodyWaitErr + } + return nil +} + +// sendGoodsListRequest 发送商品列表请求 +// @param reqJson 请求JSON +// @return GetGoodsListResp 响应结构体 +// @return error 错误 +func sendGoodsListRequest(reqJson string) (*planBTypeKfz.GetGoodsListResp, error) { + // 使用超时包装,防止 DLL 调用永久阻塞 + // 使用容量为1的缓冲通道,避免 goroutine 泄露 + resultCh := make(chan string, 1) + errCh := make(chan error, 1) + + go func() { + fmt.Println("[DEBUG] goroutine started, about to call GetGoodsList...") + goodsListStr, getGoodsListErr := golabl.KfzDll.GetGoodsList( + golabl.Config.KfzConfig.AppId, + golabl.Config.KfzConfig.AppSecret, + golabl.Task.Header.ShopMsg.Token, + reqJson) + fmt.Println("[DEBUG] GetGoodsList returned, err:", getGoodsListErr) + if getGoodsListErr != nil { + errCh <- getGoodsListErr + return + } + // 使用 select+default 确保超时后 goroutine 能安全退出 + select { + case resultCh <- goodsListStr: + default: + // channel 已满(超时已触发),直接退出 + fmt.Println("[DEBUG] resultCh full, goroutine exiting") + } + }() + + // 等待结果或超时(30秒) + select { + case goodsListStr := <-resultCh: + // 正常返回 + var goodsListResp planBTypeKfz.GetGoodsListResp + fmt.Println(goodsListStr) + unmarshalErr := json.Unmarshal([]byte(goodsListStr), &goodsListResp) + if unmarshalErr != nil { + fmt.Printf("解析孔夫子商品列表返回json失败: %v [孔夫子数据:%v]", unmarshalErr, goodsListStr) + return nil, fmt.Errorf("解析孔夫子商品列表返回json失败: %v [孔夫子数据:%v]", unmarshalErr, goodsListStr) + } + return &goodsListResp, nil + case err := <-errCh: + return nil, err + case <-time.After(30 * time.Second): + return nil, fmt.Errorf("DLL 调用 GetGoodsList 超时(30秒),可能是 DLL 函数不可用或网络问题") + } +} + +// parseAddTimeToUnix 获取商品的添加时间(Unix时间戳,秒) +// @param addTime 添加时间(Unix时间戳,秒级) +// @return int64 Unix时间戳(秒) +func parseAddTimeToUnix(addTime int64) int64 { + return addTime +} + +// formatUnixTime 将Unix时间戳(秒)转为孔夫子时间格式字符串 +// @param unixTime Unix时间戳(秒) +// @return string 时间字符串(格式:yyyy-MM-dd HH:mm:ss) +func formatUnixTime(unixTime int64) string { + if unixTime <= 0 { + return "" + } + return time.Unix(unixTime, 0).Format("2006-01-02 15:04:05") +} + +// 上架 +func executeGoodsLaunch(logUuid string, taskMsg planAType.TaskBody) (string, error) { + // 上架商品 + launchGoodsInfo := planBTypeKfz.Product{ItemId: strconv.FormatInt(taskMsg.Detail.GoodsId, 10)} + //转为json + jsonData, marshalErr := json.Marshal(launchGoodsInfo) + if marshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, marshalErr) + } + var launchGoods planBTypeKfz.ProductRet + launchGoodsStr, kfzPutOnSaleErr := golabl.KfzDll.PutOnSale(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token, string(jsonData)) + if kfzPutOnSaleErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, kfzPutOnSaleErr) + } + unmarshalErr := json.Unmarshal([]byte(launchGoodsStr), &launchGoods) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, unmarshalErr) + } + if launchGoods.ErrorResponse != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("上架商品失败 %s", launchGoods.ErrorResponse)) + } + return tool.ReturnSuccess(taskMsg) +} + +// 下架 +func executeGoodsDownShelf(logUuid string, taskMsg planAType.TaskBody) (string, error) { + // 下架商品 + launchGoodsInfo := planBTypeKfz.Product{ItemId: strconv.FormatInt(taskMsg.Detail.GoodsId, 10)} + //转为json + jsonData, marshalErr := json.Marshal(launchGoodsInfo) + if marshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, marshalErr) + } + var launchGoods planBTypeKfz.ProductRet + launchGoodsStr, kfzPutOffSaleErr := golabl.KfzDll.PutOffSale(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token, string(jsonData)) + if kfzPutOffSaleErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, kfzPutOffSaleErr) + } + unmarshalErr := json.Unmarshal([]byte(launchGoodsStr), &launchGoods) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, unmarshalErr) + } + if launchGoods.ErrorResponse != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("下架商品失败 %s", launchGoods.ErrorResponse)) + } + return tool.ReturnSuccess(taskMsg) +} + +// 改价格 +func executeGoodsUpdatePrice(logUuid string, taskMsg planAType.TaskBody) (string, error) { + // 价格0 不能发布 + if taskMsg.Detail.Price == 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("拼多多商品 价格不能为0")) + } + //将价格由分转为元 + price := tool.FenToYuanFloat64(taskMsg.Detail.Price) + // 改价格 + launchGoodsInfo := planBTypeKfz.UpdatePriceReq{ItemId: strconv.FormatInt(taskMsg.Detail.GoodsId, 10), Price: price} + //转为json + jsonData, marshalErr := json.Marshal(launchGoodsInfo) + if marshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, marshalErr) + } + var launchGoods planBTypeKfz.UpdatePriceRet + launchGoodsStr, kfzUpdateGoodsPriceErr := golabl.KfzDll.UpdateGoodsPrice(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token, string(jsonData)) + if kfzUpdateGoodsPriceErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, kfzUpdateGoodsPriceErr) + } + unmarshalErr := json.Unmarshal([]byte(launchGoodsStr), &launchGoods) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, unmarshalErr) + } + if launchGoods.ErrorResponse != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("改价格商品失败 %s", launchGoods.ErrorResponse)) + } + return tool.ReturnSuccess(taskMsg) +} + +// 改库存 +func executeGoodsUpdateStock(logUuid string, taskMsg planAType.TaskBody) (string, error) { + // 改库存 + launchGoodsInfo := planBTypeKfz.UpdateStockReq{ItemId: strconv.FormatInt(taskMsg.Detail.GoodsId, 10), Number: int64(taskMsg.Detail.Stock)} + //转为json + jsonData, marshalErr := json.Marshal(launchGoodsInfo) + if marshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, marshalErr) + } + var launchGoods planBTypeKfz.ProductRet + launchGoodsStr, kfzUpdateGoodsStockErr := golabl.KfzDll.UpdateGoodsStock(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token, string(jsonData)) + if kfzUpdateGoodsStockErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, kfzUpdateGoodsStockErr) + } + unmarshalErr := json.Unmarshal([]byte(launchGoodsStr), &launchGoods) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, unmarshalErr) + } + if launchGoods.ErrorResponse != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("改库存商品失败 %s", launchGoods.ErrorResponse)) + } + taskMsg.Detail.Error = "增加库存成功!" + return tool.ReturnSuccess(taskMsg) +} + +// 添加商品到ERP接口 +func pushShopGoodsData(shopGoodsList []planBTypeKfz.KfzGoodsItem, pageFlag int) (string, error) { + + // 拼接商品列表 + var goodsList []map[string]string + for _, shopGoods := range shopGoodsList { + + goods := map[string]string{ + "isbn": shopGoods.Isbn, + "itemName": shopGoods.ItemName, + "price": fmt.Sprintf("%.2f", shopGoods.Price), + "quality": strconv.Itoa(int(shopGoods.Quality)), + "author": shopGoods.Author, + "press": shopGoods.Press, + "pubDate": shopGoods.PubDate, + "itemId": strconv.FormatInt(shopGoods.ItemId, 10), + "addTime": strconv.FormatInt(shopGoods.AddTime, 10), + "beginSaleTime": strconv.FormatInt(int64(shopGoods.BeginSaleTime), 10), + "isDraft": strconv.Itoa(shopGoods.IsDraft), + "discount": strconv.Itoa(shopGoods.Discount), + "stock": strconv.Itoa(shopGoods.Number), + "myCatId": strconv.Itoa(shopGoods.MyCatId), + "bearShipping": shopGoods.BearShipping, + "weight": fmt.Sprintf("%.2f", shopGoods.Weight), + "catId": strconv.FormatInt(int64(shopGoods.CatId), 10), + "isNewBook": strconv.Itoa(shopGoods.IsNewBook), + "bizType": strconv.Itoa(shopGoods.BizType), + "certifyStatus": shopGoods.CertifyStatus, + "weightPiece": fmt.Sprintf("%.2f", shopGoods.WeightPiece), + "mouldId": strconv.Itoa(shopGoods.MouldId), + "booklibId": strconv.Itoa(int(shopGoods.BooklibId)), + "isOnSale": strconv.Itoa(shopGoods.IsOnSale), + "isDelete": strconv.Itoa(shopGoods.IsDelete), + "updateTime": shopGoods.UpdateTime, + "endSaleTime": strconv.FormatInt(int64(shopGoods.EndSaleTime), 10), + "userId": strconv.Itoa(int(shopGoods.UserId)), + "imgUrl": golabl.Config.FileUrl.KfzImgHttpUrl + shopGoods.ImgUrl, + "oriPrice": fmt.Sprintf("%.2f", shopGoods.OriPrice), + "itemSn": shopGoods.ItemSn, + } + + goodsList = append(goodsList, goods) + + } + + shopGoodslistData := map[string]interface{}{ + "total": strconv.Itoa(len(goodsList)), + "list": goodsList, + } + + data := map[string]interface{}{ + "shopId": golabl.Task.Header.ShopId, + "shopType": golabl.Task.Header.ShopType, + "token": golabl.Task.Header.ShopMsg.Token, + "sycFlag": 1, + "taskId": golabl.Task.TaskId, + "shopGoodsResponse": shopGoodslistData, // 关键修改 + "pageFlag": pageFlag, + } + + //转为json + jsonData, marshalErr := json.Marshal(data) + if marshalErr != nil { + return "", marshalErr + } + + response, err := tool.PostJSON(golabl.Config.FileUrl.KfzAddGoodsUrl, string(jsonData)) + if err != nil { + return "", err + } + + return response, nil +} + +// 孔夫子发布 +func publishGoods(logUuid string, taskMsg planAType.TaskBody) (planAType.TaskBody, error) { + // 价格不能小于0 + if taskMsg.Detail.Price <= 0 { + return taskMsg, fmt.Errorf("价格不能小于等于0") + } + + //获取出版社信息并解析1 + if getPublishingErr := service.GetPublishingVid(&taskMsg); getPublishingErr != nil { + return taskMsg, fmt.Errorf("获取出版社信息失败-原因来自:%v", getPublishingErr) + } + + //违规词处理1 + if golabl.Config.Server.Filter == 1 { + //开启违规词处理 + if taskMsgErr := tool.FilterWord(&taskMsg); taskMsgErr != nil { + return taskMsg, taskMsgErr + } + } + + // 售价 + 运费 + if golabl.Task.Header.PriceType != "0" { + taskMsg.Detail.Price = taskMsg.Detail.Price + taskMsg.Detail.ShippingCost + } + // 价格处理 + price := tool.BuildPrice(golabl.Task.Header.PriceMod, taskMsg.Detail.Price) + if price == 0 { + return taskMsg, fmt.Errorf("不在价格区间内 isbn %v 原始价格 %v 当前价格 %v 价格模版 %v", taskMsg.BookInfo.Isbn, taskMsg.Detail.Price, price, golabl.Task.Header.PriceMod) + } + taskMsg.Detail.Price = price + + url := "http://127.0.0.1:8095" + tool.HttpGetRequest(url) + + var jsonData []byte + var marshalErr error + var img string + var skuCode string + if strings.HasPrefix(taskMsg.BookInfo.Isbn, "678") { + //模板13 无isbn + goodsAdd13, err := template13(taskMsg) + if err != nil { + return planAType.TaskBody{}, err + } + //将 goodsAdd 转为json字符串 + jsonData, marshalErr = json.Marshal(goodsAdd13) + if marshalErr != nil { + return taskMsg, fmt.Errorf("构建商品参数失败 %v", marshalErr) + } + img = goodsAdd13.ImgUrl + skuCode = goodsAdd13.ItemSn + + } else { + //模板17 普通图书 + goodsAdd17, err := template17(taskMsg) + if err != nil { + return planAType.TaskBody{}, err + } + //将 goodsAdd 转为json字符串 + jsonData, marshalErr = json.Marshal(goodsAdd17) + if marshalErr != nil { + return taskMsg, fmt.Errorf("构建商品参数失败 %v", marshalErr) + } + img = goodsAdd17.ImgUrl + skuCode = goodsAdd17.ItemSn + } + + // 构建商品编码 + outGoodsId := "" + if taskMsg.Detail.OutGoodsId != "" { + outGoodsId = taskMsg.Detail.OutGoodsId + } else { + outGoodsId = taskMsg.BookInfo.Isbn + } + + // 新增商品 + goods, _, addGoodsErr := addGoods(logUuid, string(jsonData)) + if addGoodsErr != nil { + // 如果错误信息包含 "该图书必须使用图书条目库上传",则修改错误信息 + if strings.Contains(addGoodsErr.Error(), "该图书必须使用图书条目库上传") { + addGoodsErr = fmt.Errorf("该图书可能涉及政治请手动上传") + } + return taskMsg, fmt.Errorf("新增商品错误 %v", addGoodsErr) + } + + //判断错误 + if goods.ErrorResponse != nil { + return taskMsg, fmt.Errorf("新增商品失败 %v", goods.ErrorResponse.SubMsg) + } + taskMsg.Detail.GoodsId = goods.SuccessResponse.Item.ItemId + taskMsg.Detail.OutGoodsId = outGoodsId + taskMsg.Detail.Img = img + taskMsg.Detail.SkuCode = skuCode + return taskMsg, nil +} + +// 模板17 +func template17(taskMsg planAType.TaskBody) (planBTypeKfz.GoodsAdd17, error) { + var goodsAdd planBTypeKfz.GoodsAdd17 + + // *********************构建参数 开始******************************** // + + //模板编号 默认 17 + goodsAdd.Tpl = "17" + + //分类编号 + if value, exists := golabl.KfzGetCommonCategory[string(taskMsg.BookInfo.CatIdObject.KongFuZiCatId)]; exists { + goodsAdd.CatId = value + } else { + goodsAdd.CatId = "43000000000000000" + } + + //构建商品名称 + goodsAdd.ItemName = tool.BuildGoodsName( + golabl.Task.Header.ShopMsg.GoodsNamePrefix, // 商品名称前缀 + golabl.Task.Header.ShopMsg.GoodsNameSuffix, // 商品名称后缀 + golabl.Task.Header.ShopMsg.TitleConsistOf, // 标题组成 + golabl.Task.Header.ShopMsg.SpaceCharacter, // 间隔符 + taskMsg.BookInfo) // 图书信息 + taskMsg.Detail.GoodsName = goodsAdd.ItemName + + //售价 + goodsAdd.Price = tool.FenToYuan(taskMsg.Detail.Price) + + //库存 + if taskMsg.Detail.Stock == 0 && (golabl.Task.Header.TaskType == 1 || golabl.Task.Header.TaskType == 2 || golabl.Task.Header.TaskType == 6) { + //如果库存为0 则给默认库存 + taskMsg.Detail.Stock = golabl.Task.Header.ShopMsg.DefStock + } else { + if taskMsg.Detail.Stock == 0 && golabl.Task.Header.TaskType == 8 { + return goodsAdd, fmt.Errorf("库存不能为0") + } + } + goodsAdd.Number = strconv.FormatInt(int64(taskMsg.Detail.Stock), 10) + + //品相 + goodsAdd.Quality = strconv.FormatInt(taskMsg.Detail.Condition, 10) + if goodsAdd.Quality == "" || goodsAdd.Quality == "0" { + goodsAdd.Quality = strconv.FormatInt(golabl.Task.Header.ShopMsg.ConditionDef, 10) + } + + //货号 + goodsAdd.ItemSn = taskMsg.Detail.SkuCode + + if len(taskMsg.BookInfo.ImageObject.CarouselUrlArray) == 0 { + // 无图片信息 isbn计次 + setNoImgCountErr := service.SetNoImgCount(taskMsg.BookInfo.Isbn) + if setNoImgCountErr != nil { + return goodsAdd, fmt.Errorf("无图片信息isbn计次错误 isbn %v %v", taskMsg.BookInfo.Isbn, setNoImgCountErr.Error()) + } + return goodsAdd, fmt.Errorf("缺少轮播图") + } + + // 将 轮播图不是 孔夫子图片空间的图片替换成孔夫子图片空间图片 + for k, v := range taskMsg.BookInfo.ImageObject.CarouselUrlArray { + if !strings.Contains(v, "kfzimg.com") { + kfz, uploadImageToKfzErr := UploadImageToKfz(v) + if uploadImageToKfzErr != nil { + return goodsAdd, uploadImageToKfzErr + } + // 添加到轮播图 + taskMsg.BookInfo.ImageObject.CarouselUrlArray[k] = kfz + } + } + //将 最后的图片不是 孔夫子图片空间的图片替换成孔夫子图片空间图片 + for k, v := range golabl.Task.Header.ShopMsg.CarouseLastImgUrlArray { + if !strings.Contains(v, "kfzimg.com") { + kfz, uploadImageToKfzErr := UploadImageToKfz(v) + if uploadImageToKfzErr != nil { + return goodsAdd, uploadImageToKfzErr + } + // 添加到轮播图 + golabl.Task.Header.ShopMsg.CarouseLastImgUrlArray[k] = kfz + } + } + //处理图片 + oldCarouselUrlArray := append([]string{}, taskMsg.BookInfo.ImageObject.CarouselUrlArray...) //原始轮播图,用于后续处理,不会被打上水印 + // 存在水印图片,则打水印 + if golabl.Task.Header.ShopMsg.WatermarkImgUrl != "" { + //获取水印图片 + watermarkImgUrl, watermarkImgErr := tool.GetWatermarkImg() + if watermarkImgErr != nil { + return goodsAdd, fmt.Errorf("获取水印图片失败 %v", watermarkImgErr) + } + //打水印 + watermarkFromURLExsBase64Arr, watermarkFromURLExsErr := tool.AddWatermarkFromURLExs(taskMsg.BookInfo.ImageObject.CarouselUrlArray, watermarkImgUrl, golabl.Task.Header.ShopMsg.WatermarkPosition) + if watermarkFromURLExsErr != nil { + return goodsAdd, fmt.Errorf("图片打水印失败 %v", watermarkFromURLExsErr) + } + //图片上传到孔夫子 + toPdd, uploadImageToPddErr := tool.UploadImageToKfz(watermarkFromURLExsBase64Arr) + if uploadImageToPddErr != nil { + return goodsAdd, fmt.Errorf("图片上传到拼多多失败 %v", uploadImageToPddErr) + } + //将上传的图片替换到商品轮播图中 + for i := 0; i < len(toPdd); i++ { + taskMsg.BookInfo.ImageObject.CarouselUrlArray[i] = toPdd[i] + } + } + + imgUrlArr := tool.BuildCarouselGallery(golabl.Task.Header.ShopMsg.CarouseLastImgUrlArray, oldCarouselUrlArray, taskMsg.BookInfo.ImageObject.CarouselUrlArray, golabl.Task.Header.ShopMsg.WatermarkPosition) + + //商品主图 + goodsAdd.ImgUrl = imgUrlArr[0] + + //多个商品图片 + goodsAdd.Images = strings.Join(imgUrlArr, ";") + + //运费设置 + bearShipping := "seller" + if golabl.Task.Header.ShopMsg.IsParcel == "0" { + bearShipping = "buyer" + // 设置运费模板编号 + goodsAdd.MouldId = "1" + } + goodsAdd.BearShipping = bearShipping + + //运费模板编号 + //将 golabl.Task.Header.ShopMsg.CostTemplateId 转为int + costTemplateId, atoiErr := strconv.Atoi(golabl.Task.Header.ShopMsg.CostTemplateId) + if atoiErr != nil { + return goodsAdd, fmt.Errorf("运费模板编号转换错误 %v", atoiErr) + } + goodsAdd.MouldId = strconv.Itoa(costTemplateId) + + //重量 + goodsAdd.Weight = fmt.Sprintf("%v", float64(golabl.Task.Header.ShopMsg.BookWeight/100)) + + //商品标准本数 + goodsAdd.WeightPiece = fmt.Sprintf("%v", float64(golabl.Task.Header.ShopMsg.StandardNumber/100)) + + // *********************构建模板参数 开始******************************** // + + // ISBN + goodsAdd.Isbn = taskMsg.BookInfo.Isbn + + // 作者 + if taskMsg.BookInfo.Author == "" { + taskMsg.BookInfo.Author = "佚名" + } + goodsAdd.Author = taskMsg.BookInfo.Author + + // 出版社 + if taskMsg.Publishing.Value == "" { + taskMsg.Publishing.Value = "2006-01" + } + goodsAdd.Press = taskMsg.Publishing.Value + + // 出版社日期 + goodsAdd.PubDate = taskMsg.BookInfo.PublicationDate + + // 装帧 + if taskMsg.BookInfo.Binding == "" { + taskMsg.BookInfo.Binding = "平装" + } + goodsAdd.Binding = taskMsg.BookInfo.Binding + + // 开本 + goodsAdd.PageSize = strconv.FormatInt(taskMsg.BookInfo.Format, 10) + + // 页数 + goodsAdd.PageNum = strconv.FormatInt(taskMsg.BookInfo.PagesCount, 10) + + // 字数 + goodsAdd.WordNum = fmt.Sprintf("%v", float64(taskMsg.BookInfo.WordsCount/1000)) + + // 图书定价 + goodsAdd.OriPrice = fmt.Sprintf("%v", float64(tool.BuildGoodsPrice(taskMsg.Detail.Price)/100)) + + return goodsAdd, nil +} + +// 模板 13 +func template13(taskMsg planAType.TaskBody) (planBTypeKfz.GoodsAdd13, error) { + var goodsAdd planBTypeKfz.GoodsAdd13 + + // *********************构建参数 开始******************************** // + + //模板编号 默认 17 + goodsAdd.Tpl = "17" + + //分类编号 + if value, exists := golabl.KfzGetCommonCategory[string(taskMsg.BookInfo.CatIdObject.KongFuZiCatId)]; exists { + goodsAdd.CatId = value + } else { + goodsAdd.CatId = "43000000000000000" + } + + //构建商品名称 + goodsAdd.ItemName = tool.BuildGoodsName( + golabl.Task.Header.ShopMsg.GoodsNamePrefix, // 商品名称前缀 + golabl.Task.Header.ShopMsg.GoodsNameSuffix, // 商品名称后缀 + golabl.Task.Header.ShopMsg.TitleConsistOf, // 标题组成 + golabl.Task.Header.ShopMsg.SpaceCharacter, // 间隔符 + taskMsg.BookInfo) // 图书信息 + taskMsg.Detail.GoodsName = goodsAdd.ItemName + + //售价 + goodsAdd.Price = tool.FenToYuan(taskMsg.Detail.Price) + + //库存 + if taskMsg.Detail.Stock == 0 && (golabl.Task.Header.TaskType == 1 || golabl.Task.Header.TaskType == 2 || golabl.Task.Header.TaskType == 6) { + //如果库存为0 则给默认库存 + taskMsg.Detail.Stock = golabl.Task.Header.ShopMsg.DefStock + } else { + if taskMsg.Detail.Stock == 0 && golabl.Task.Header.TaskType == 8 { + return goodsAdd, fmt.Errorf("库存不能为0") + } + } + goodsAdd.Number = strconv.FormatInt(int64(taskMsg.Detail.Stock), 10) + + //品相 + goodsAdd.Quality = strconv.FormatInt(taskMsg.Detail.Condition, 10) + if goodsAdd.Quality == "" || goodsAdd.Quality == "0" { + goodsAdd.Quality = strconv.FormatInt(golabl.Task.Header.ShopMsg.ConditionDef, 10) + } + + //货号 + goodsAdd.ItemSn = taskMsg.Detail.SkuCode + + if len(taskMsg.BookInfo.ImageObject.CarouselUrlArray) == 0 { + // 无图片信息 isbn计次 + setNoImgCountErr := service.SetNoImgCount(taskMsg.BookInfo.Isbn) + if setNoImgCountErr != nil { + return goodsAdd, fmt.Errorf("无图片信息isbn计次错误 isbn %v %v", taskMsg.BookInfo.Isbn, setNoImgCountErr.Error()) + } + return goodsAdd, fmt.Errorf("缺少轮播图") + } + + // 将 轮播图不是 孔夫子图片空间的图片替换成孔夫子图片空间图片 + for k, v := range taskMsg.BookInfo.ImageObject.CarouselUrlArray { + if !strings.Contains(v, "kfzimg.com") { + kfz, uploadImageToKfzErr := UploadImageToKfz(v) + if uploadImageToKfzErr != nil { + return goodsAdd, uploadImageToKfzErr + } + // 添加到轮播图 + taskMsg.BookInfo.ImageObject.CarouselUrlArray[k] = kfz + } + } + //将 最后的图片不是 孔夫子图片空间的图片替换成孔夫子图片空间图片 + for k, v := range golabl.Task.Header.ShopMsg.CarouseLastImgUrlArray { + if !strings.Contains(v, "kfzimg.com") { + kfz, uploadImageToKfzErr := UploadImageToKfz(v) + if uploadImageToKfzErr != nil { + return goodsAdd, uploadImageToKfzErr + } + // 添加到轮播图 + golabl.Task.Header.ShopMsg.CarouseLastImgUrlArray[k] = kfz + } + } + //处理图片 + oldCarouselUrlArray := append([]string{}, taskMsg.BookInfo.ImageObject.CarouselUrlArray...) //原始轮播图,用于后续处理,不会被打上水印 + // 存在水印图片,则打水印 + if golabl.Task.Header.ShopMsg.WatermarkImgUrl != "" { + //获取水印图片 + watermarkImgUrl, watermarkImgErr := tool.GetWatermarkImg() + if watermarkImgErr != nil { + return goodsAdd, fmt.Errorf("获取水印图片失败 %v", watermarkImgErr) + } + //打水印 + watermarkFromURLExsBase64Arr, watermarkFromURLExsErr := tool.AddWatermarkFromURLExs(taskMsg.BookInfo.ImageObject.CarouselUrlArray, watermarkImgUrl, golabl.Task.Header.ShopMsg.WatermarkPosition) + if watermarkFromURLExsErr != nil { + return goodsAdd, fmt.Errorf("图片打水印失败 %v", watermarkFromURLExsErr) + } + //图片上传到孔夫子 + toPdd, uploadImageToPddErr := tool.UploadImageToKfz(watermarkFromURLExsBase64Arr) + if uploadImageToPddErr != nil { + return goodsAdd, fmt.Errorf("图片上传到拼多多失败 %v", uploadImageToPddErr) + } + //将上传的图片替换到商品轮播图中 + for i := 0; i < len(toPdd); i++ { + taskMsg.BookInfo.ImageObject.CarouselUrlArray[i] = toPdd[i] + } + } + + imgUrlArr := tool.BuildCarouselGallery(golabl.Task.Header.ShopMsg.CarouseLastImgUrlArray, oldCarouselUrlArray, taskMsg.BookInfo.ImageObject.CarouselUrlArray, golabl.Task.Header.ShopMsg.WatermarkPosition) + + //商品主图 + goodsAdd.ImgUrl = imgUrlArr[0] + + //多个商品图片 + goodsAdd.Images = strings.Join(imgUrlArr, ";") + + //运费设置 + bearShipping := "seller" + if golabl.Task.Header.ShopMsg.IsParcel == "0" { + bearShipping = "buyer" + // 设置运费模板编号 + goodsAdd.MouldId = "1" + } + goodsAdd.BearShipping = bearShipping + + //运费模板编号 + //将 golabl.Task.Header.ShopMsg.CostTemplateId 转为int + costTemplateId, atoiErr := strconv.Atoi(golabl.Task.Header.ShopMsg.CostTemplateId) + if atoiErr != nil { + return goodsAdd, fmt.Errorf("运费模板编号转换错误 %v", atoiErr) + } + goodsAdd.MouldId = strconv.Itoa(costTemplateId) + + //重量 + goodsAdd.Weight = fmt.Sprintf("%v", float64(golabl.Task.Header.ShopMsg.BookWeight/100)) + + //商品标准本数 + goodsAdd.WeightPiece = fmt.Sprintf("%v", float64(golabl.Task.Header.ShopMsg.StandardNumber/100)) + + // *********************构建模板参数 开始******************************** // + + // 作者 + if taskMsg.BookInfo.Author == "" { + taskMsg.BookInfo.Author = "佚名" + } + goodsAdd.Author = taskMsg.BookInfo.Author + + // 出版社 + if taskMsg.Publishing.Value == "" { + taskMsg.Publishing.Value = "2006-01" + } + goodsAdd.Press = taskMsg.Publishing.Value + + // 出版社日期 + goodsAdd.PubDate = taskMsg.BookInfo.PublicationDate + + // 装帧 + if taskMsg.BookInfo.Binding == "" { + taskMsg.BookInfo.Binding = "平装" + } + goodsAdd.Binding = taskMsg.BookInfo.Binding + + // 开本 + goodsAdd.PageSize = strconv.FormatInt(taskMsg.BookInfo.Format, 10) + + // 页数 + goodsAdd.PageNum = strconv.FormatInt(taskMsg.BookInfo.PagesCount, 10) + + // 字数 + goodsAdd.WordNum = fmt.Sprintf("%v", float64(taskMsg.BookInfo.WordsCount/1000)) + + // 图书定价 + goodsAdd.OriPrice = fmt.Sprintf("%v", float64(tool.BuildGoodsPrice(taskMsg.Detail.Price)/100)) + + return goodsAdd, nil +} diff --git a/planB/dispatcher/pinduoduo/pinduoduo.go b/planB/dispatcher/pinduoduo/pinduoduo.go new file mode 100644 index 0000000..f3c7604 --- /dev/null +++ b/planB/dispatcher/pinduoduo/pinduoduo.go @@ -0,0 +1,1715 @@ +package pinduoduo + +import ( + "encoding/json" + "errors" + "fmt" + "path/filepath" + "planA/planB/initialization/golabl" + "planA/planB/logic" + "planA/planB/modules/logs" + "planA/planB/service" + "planA/planB/tool" + planBTypeModules "planA/planB/type/modules" + planBTypePinduoduo "planA/planB/type/pinduoduo" + planAType "planA/type" + "strconv" + "strings" + "time" +) + +type PinDuoDuo struct { +} + +// NewPinDuoDuo 创建拼多多平台 +// @return *PinDuoDuo +func NewPinDuoDuo() *PinDuoDuo { + return &PinDuoDuo{} +} + +// AddGoodsTask 添加商品 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return error 错误 +func (pinDuoDuo *PinDuoDuo) AddGoodsTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + taskMsg, publishGoodsErr := publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return "", publishGoodsErr + } + return tool.ReturnSuccess(taskMsg) +} + +// GetGoodsTask 获取商品 +// @return string body 信息 +// @return error 错误 +func (pinDuoDuo *PinDuoDuo) GetGoodsTask() (string, error) { + // 生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + + const pageSize = 100 + const maxPage = 100 + const maxRecordsPerRange = 10000 // 每个时间范围最多获取10000条 + + var lastCreatedAt int64 = 0 + + // 统计变量 + totalFetched := 0 // 总共获取到的商品数(包括重复) + duplicateCount := 0 // 重复商品数量 + uniqueCount := 0 // 不重复商品数量 + + //查询body_wait是否存在,如果存在则证明不是第一次执行要获取body_wait中最后一条数据的创建时间作为查询的开始时间 + exist, isTaskBodyWaitExistErr := service.IsTaskBodyWaitExist() + if isTaskBodyWaitExistErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, isTaskBodyWaitExistErr) + } + if exist { + // 获取最后一条数据的创建时间 + lastBodyWaitDataJson, getLastGoodsCreateTimeErr := service.GetTaskBodyWaitLast() + if getLastGoodsCreateTimeErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, getLastGoodsCreateTimeErr) + } + // 解析 lastBodyWaitData 到结构体 + var lastBodyWaitData planAType.TaskBody + unmarshalErr := json.Unmarshal([]byte(lastBodyWaitDataJson), &lastBodyWaitData) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, unmarshalErr) + } + //将数据的创建时间给到 lastCreatedAt + lastCreatedAt = lastBodyWaitData.BookInfo.Price + //第二阶段 获取商品 + phaseTwoGoodsErr := PhaseTwoGoods(pageSize, &totalFetched, &lastCreatedAt, maxRecordsPerRange) + if phaseTwoGoodsErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, phaseTwoGoodsErr) + } + } else { + //第一阶段 拉取任务数据 + firstTimeGoodsErr := phaseOneGoods(pageSize, maxPage, &totalFetched, &lastCreatedAt) + if firstTimeGoodsErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, firstTimeGoodsErr) + } + //第二阶段 获取商品 + phaseTwoGoodsErr := PhaseTwoGoods(pageSize, &totalFetched, &lastCreatedAt, maxRecordsPerRange) + if phaseTwoGoodsErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, phaseTwoGoodsErr) + } + } + + //更新状态为推送中 + updateTaskStatusErr := service.UpdateTaskStatus(planAType.TaskStatusPushTaskStatus) + if updateTaskStatusErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, updateTaskStatusErr) + } + + //重新设置任务进度 + if updateTaskHeaderErr := service.SetTaskCount(strconv.FormatInt(golabl.Task.Footer.TaskCountTrue, 10)); updateTaskHeaderErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, updateTaskHeaderErr) + } + + //去重复与保存 + deduplicateToBodyOverErr := deduplicateToBodyOver(&duplicateCount, &uniqueCount) + if deduplicateToBodyOverErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, deduplicateToBodyOverErr) + } + + // 输出统计信息 + statsLogMsg := fmt.Sprintf(` +════════════════════════════════════════════════════════════════ +【拼多多店铺拉取】 +请求ID:%s +时间: %s +店铺ID:%v +店铺名称:%v +总共获取商品数(含重复): %d +不重复商品数: %d +重复商品数: %d +重复率: %.2f%% +════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + golabl.Task.TaskId, + golabl.Task.Header.ShopName, + totalFetched, + uniqueCount, + duplicateCount, + float64(duplicateCount)/float64(totalFetched)*100) + fmt.Println(statsLogMsg) + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, statsLogMsg) + + return tool.ReturnSuccess(planAType.TaskBody{}) +} + +// OperationGoodsTask 操作商品 +// @param taskMsg 任务内容 +// @return string body 信息 +func (pinDuoDuo *PinDuoDuo) OperationGoodsTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + switch taskMsg.Detail.Status { + case 1, 2: + return setSaleStatusGoodsTask(logUuid, taskMsg) //设置商品上下架状态 status=1 上架 status=2 下架 {"book_info":{"isbn":"9787543982888"},"detail":{"goods_id":936170582125,"status":2}} + case 4: + return updateGoodsQuantity(logUuid, taskMsg, 1, 0) //修改商品库存 {"book_info":{"isbn":"9787532080786"},"detail":{"goods_id":935177284615,"status":4,"stock":2,"sku_id":1882660479308}} + case 5: + return updateSkuPrice(logUuid, taskMsg) //修改商品价格 {"book_info":{"isbn":"9787543982888"},"detail":{"goods_id":939229985495,"status":5,"price":5000,"sku_id":1886207421871}} + case 6: + //发布商品 + taskMsg, publishGoodsErr := publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, publishGoodsErr) + } + return tool.ReturnSuccess(taskMsg) + case 7: + //下架 + taskMsg.Detail.Status = 2 + _, setSaleStatusGoodsTaskErr := setSaleStatusGoodsTask(logUuid, taskMsg) + if setSaleStatusGoodsTaskErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, setSaleStatusGoodsTaskErr) + } + //删除商品 + logic.DelTask(taskMsg) + //发布商品 + taskMsg, publishGoodsErr := publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, publishGoodsErr) + } + return tool.ReturnSuccess(taskMsg) + default: + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("未知操作类型 %v", taskMsg.Detail.Status)) + } +} + +// IncStockTask 增量库存 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return error 错误 +func (pinDuoDuo *PinDuoDuo) IncStockTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + + // 获取商品id + getGoodsByShopIdAndIsbn, GetGoodsByShopIdAndIsbnErr := tool.GetGoodsByShopIdAndIsbn(golabl.Task.Header.ShopId, taskMsg.BookInfo.Isbn) + if GetGoodsByShopIdAndIsbnErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, GetGoodsByShopIdAndIsbnErr) + } + if getGoodsByShopIdAndIsbn.Code != "200" { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("ERP未找到商品")) + } + if len(getGoodsByShopIdAndIsbn.Data) == 0 { + //新发布 + task, addGoodsTaskErr := publishGoods(logUuid, taskMsg) + if addGoodsTaskErr != nil { + return "", addGoodsTaskErr + } + + //根据回调查询商品回到信息直到成功 + //found := false + //startTime := time.Now() + //maxDuration := 3 * time.Minute // 最大查询时间 3分钟 + + //for !found { + // // 检查是否超时 + // if time.Since(startTime) > maxDuration { + // fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~超时了~~~~~~~~~~~~~~~~~~~~~~~~~~") + // found = true + // break // 跳出内层循环 + // } + list, getPddNoticeMsgErr := service.GetPddNoticeMsg(golabl.Task.TaskId) + if getPddNoticeMsgErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, getPddNoticeMsgErr) + } + for _, v := range list { + var pddNoticeMsg planBTypePinduoduo.PddNoticeMsg + unmarshalErr := json.Unmarshal([]byte(v), &pddNoticeMsg) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, unmarshalErr) + } + //必须是发布上架 + if pddNoticeMsg.Type == "pdd_goods_GoodsOnShelf" { + // task.Detail.GoodsId 转为字符串 + goodsId := strconv.FormatInt(task.Detail.GoodsId, 10) + if pddNoticeMsg.GoodsId == goodsId { + fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~找到了~~~~~~~~~~~~~~~~~~~~~~~~~~") + //跳出最外层循环 + //found = true + break // 跳出内层循环 + } + } + } + //// 避免过于频繁的查询,可以添加短暂延迟 + //if !found { + // time.Sleep(1 * time.Second) + //} + //} + task.Detail.Error = "发布成功!" + return tool.ReturnSuccess(task) + } else { + // 当前任务的价格 + taskPrice := taskMsg.Detail.Price // 单位:分 + + // 价格 + 运费(如果 PriceType != "0") + if golabl.Task.Header.PriceType != "0" { + taskPrice = taskPrice + taskMsg.Detail.ShippingCost + } + + // 价格模板计算 + taskPrice = tool.BuildPrice(golabl.Task.Header.PriceMod, taskPrice) + if taskPrice == 0 { + taskMsg.Detail.Error = "任务价格不在价格模板区间内!" + return tool.ReturnSuccess(taskMsg) + } + + // 1元 = 100分,价格相差1元以上即 >= 100分 + const priceDiffThreshold = 100 // 1元 + + // 收集所有匹配条件的商品(价格差<1元) + var matchedItems []struct { + TrilateralId string + SkuId string + Stock int64 + Price int64 + PriceDiff int64 // 价格差绝对值 + } + + // 记录不匹配原因 + var firstMismatchReason string + + for _, item := range getGoodsByShopIdAndIsbn.Data { + // 解析价格(单位:分) + itemPrice, _ := strconv.ParseInt(item.TotalPrice, 10, 64) + // 解析库存 + itemStock, _ := strconv.ParseInt(item.Stock, 10, 64) + + // 计算价格差(绝对值) + priceDiff := abs(itemPrice - taskPrice) + + // 价格相差1元以上 + if priceDiff >= priceDiffThreshold { + if firstMismatchReason == "" { + firstMismatchReason = fmt.Sprintf("商品[%s]价格相差超过1元: 任务价格=%d分, 商品价格=%d分, 差价=%d分", item.TrilateralId, taskPrice, itemPrice, priceDiff) + } + continue + } + + // 价格相差小于1元 → 加入候选列表 + matchedItems = append(matchedItems, struct { + TrilateralId string + SkuId string + Stock int64 + Price int64 + PriceDiff int64 + }{ + TrilateralId: item.TrilateralId, + SkuId: item.SkuId, + Stock: itemStock, + Price: itemPrice, + PriceDiff: priceDiff, + }) + } + + // 逻辑: + // 1. 所有价格相差1元以上 → 重新发布 + // 2. 否则 → 找到价格相差最小的增加库存,如果多个最小差价一样则对第一条增加库存 + if len(matchedItems) == 0 { + // 所有商品价格相差≥1元 → 重新发布 + fmt.Printf("[重新发布] %s\n", firstMismatchReason) + + task, addGoodsTaskErr := publishGoods(logUuid, taskMsg) + if addGoodsTaskErr != nil { + return "", addGoodsTaskErr + } + task.Detail.Error = "所有商品价格相差超过1元,重新发布成功!" + return tool.ReturnSuccess(task) + } + + // 找到价格相差最小的商品,如果多个最小差价一样则对第一条增加库存 + minDiff := int64(999999999) + var targetItem struct { + TrilateralId string + SkuId string + Stock int64 + } + + for _, item := range matchedItems { + // 找到更小的差价,或者差价相等但为第一条 + if item.PriceDiff < minDiff || (item.PriceDiff == minDiff && targetItem.TrilateralId == "") { + minDiff = item.PriceDiff + targetItem.TrilateralId = item.TrilateralId + targetItem.SkuId = item.SkuId + targetItem.Stock = item.Stock + } + } + + // 将 targetItem.SkuId 转为 int64 + skuId, skuIdParseIntErr := strconv.ParseInt(targetItem.SkuId, 10, 64) + if skuIdParseIntErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, skuIdParseIntErr) + } + + // 将 targetItem.TrilateralId 转为 int64 + trilateralId, trilateralIdParseIntErr := strconv.ParseInt(targetItem.TrilateralId, 10, 64) + if trilateralIdParseIntErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, trilateralIdParseIntErr) + } + + //增量修改库存 + taskMsg.Detail.GoodsId = trilateralId + taskMsg.Detail.SkuId = skuId + quantity, updateGoodsQuantityErr := updateGoodsQuantity(logUuid, taskMsg, 2, targetItem.Stock) + if updateGoodsQuantityErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, updateGoodsQuantityErr) + } + + return quantity, nil + } +} + +// abs 返回绝对值 +func abs(x int64) int64 { + if x < 0 { + return -x + } + return x +} + +func (pinDuoDuo *PinDuoDuo) SetGoodsTask() string { + return "" +} + +// *******************************私有方法************************************ // + +// 构建商品属性列表 +// @param isbn +// @param bookName 书名 +// @param pageCount 页数 +// @param price 价格 +// @param publishingVid 出版社Vid +// @param author 作者 +// @param format 开本 +// @param binding 装帧 +// @param wordsCount 字数 +// @param publicationDate 出版时间 +// @return []GoodsProperties 商品属性列表 +func buildGoodsPropertiesList(isbn, bookName string, pageCount, price int64, publishingVid int64, author string, format int64, binding string, wordsCount int64, publicationDate string) []planBTypePinduoduo.GoodsProperties { + var goodsPropertiesArr []planBTypePinduoduo.GoodsProperties + //isbn + if isbn != "" { + goodsPropertiesIsbn := planBTypePinduoduo.GoodsProperties{ + RefPid: 425, + Value: isbn, + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsPropertiesIsbn) + } + + //书名 + if bookName != "" { + goodsPropertiesBookName := planBTypePinduoduo.GoodsProperties{ + RefPid: 876, + Value: bookName, + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsPropertiesBookName) + } + + //页数 + if pageCount == 0 { + pageCount = 200 + } + goodsPropertiesPageNum := planBTypePinduoduo.GoodsProperties{ + RefPid: 692, + Value: strconv.FormatInt(pageCount, 10), + ValueUnit: "页", + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsPropertiesPageNum) + + //定价 + goodsPropertiesPrice := planBTypePinduoduo.GoodsProperties{ + RefPid: 879, + Value: strconv.FormatInt(price/100, 10), + ValueUnit: "元", + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsPropertiesPrice) + + //出版社 + if publishingVid != 0 { + goodsPropertiesPublishing := planBTypePinduoduo.GoodsProperties{ + RefPid: 880, + Vid: publishingVid, + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsPropertiesPublishing) + } + + //作者 + if author != "" { + goodsPropertiesAuthor := planBTypePinduoduo.GoodsProperties{ + RefPid: 882, + Value: author, + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsPropertiesAuthor) + } + + //开本 + if format != 0 { + goodsPropertiesFormat := planBTypePinduoduo.GoodsProperties{ + RefPid: 890, + Value: strconv.FormatInt(format, 10), + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsPropertiesFormat) + } + + //装帧类型 + if binding != "" { + goodsPropertiesBinding := planBTypePinduoduo.GoodsProperties{ + RefPid: 888, + Value: binding, + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsPropertiesBinding) + } + + //字数 + if wordsCount != 0 { + goodsWordsCountBinding := planBTypePinduoduo.GoodsProperties{ + RefPid: 887, + Value: strconv.FormatInt(wordsCount, 10), + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsWordsCountBinding) + } + + //出版时间(部分数据出版时间是1970-01,视为没有出版时间) + if publicationDate != "" && publicationDate != "1970-01" { + goodsPublicationDateBinding := planBTypePinduoduo.GoodsProperties{ + RefPid: 881, + Value: publicationDate, + } + goodsPropertiesArr = append(goodsPropertiesArr, goodsPublicationDateBinding) + } + return goodsPropertiesArr +} + +// sku规格生成 +// @param price 价格 +// @param thumbUrl 缩略图 +// @param stock 库存 +// @param outSkuSn 商品编码 +// @param specName 规格名称 +// @param isOnsale 上架状态 +// @return Sku sku规格 +// @return error 错误信息 +func buildSkuList(price int64, thumbUrl string, stock int64, outSkuSn string, specChildName string, isOnsale int64) (planBTypePinduoduo.Sku, error) { + //构建变量 + specId := golabl.Task.Header.ShopMsg.SpecId + specName := golabl.Task.Header.ShopMsg.SpecName + // 构建 Spec列表 + var sku planBTypePinduoduo.Sku + goodsSpec, buildPddGoodsSpecIdErr := buildPddGoodsSpecId(specId, specChildName) + if buildPddGoodsSpecIdErr != nil { + return sku, buildPddGoodsSpecIdErr + } + + // 构建SKU_Properties列表 + skuProperty := planBTypePinduoduo.SkuProperty{ + Punit: specName, // 属性单位 + RefPid: specId, // 属性id + Value: goodsSpec.DllGoodsSpec.SpecName, // 属性值 + Vid: goodsSpec.DllGoodsSpec.SpecID, // 属性值id + } + skuProperties := []planBTypePinduoduo.SkuProperty{skuProperty} + + specIdList := "[" + strconv.FormatInt(skuProperty.Vid, 10) + "]" + // 构建 SKU列表 + var onsale int64 + if isOnsale == 0 { + onsale = 1 + } else { + onsale = 0 + } + sku = planBTypePinduoduo.Sku{ + IsOnsale: onsale, //上架状态,0-已下架,1-上架中 + LimitQuantity: 999, //sku购买限制,只入参999 + MultiPrice: price, //团购价格,单位为分 + Price: price + 100, //单买价格,单位为分 + SkuProperties: skuProperties, //sku属性列表 + ThumbUrl: thumbUrl, //缩略图 + SpecIdList: specIdList, //商品规格列表 + Quantity: stock, //商品库存初始数量 + Weight: 250, //重量单位g + OutSkuSn: outSkuSn, //商品编码 + } + return sku, nil +} + +// buildPddGoodsSpecId 根据名称获取规格信息 +// @param specId 商品规格id +// @param specName 规格名称 +// @return DllGoodsSpec 规格信息 +// @return error 错误信息 +func buildPddGoodsSpecId(id int64, name string) (planAType.DllGoodsSpec, error) { + var spec planAType.DllGoodsSpec + specStr, err := golabl.PddDll.PddGoodsSpecIdGet(golabl.Config.PddConfig.ClientId, golabl.Config.PddConfig.ClientSecret, golabl.Task.Header.ShopMsg.Token, strconv.FormatInt(id, 10), name) + if err != nil { + return spec, err + } + // 解析JSON字符串 + err = json.Unmarshal([]byte(specStr), &spec) + if err != nil { + return spec, fmt.Errorf("解析拼多多 PddGoodsSpecIdGet 接口返回json失败: %v [拼多多数据:%v]", err, specStr) + } + return spec, nil +} + +// 商品新增 +// @param logUuid 日志ID +// @param goodsInfo 商品信息 +// @return GoodsAddResponseWrapper 商品新增结果 +// @return string 商品新增结果json +// @return error 错误信息 +func addGoods(logUuid string, goodsInfo planBTypePinduoduo.GoodsAdd) (planBTypePinduoduo.GoodsAddResponseWrapper, string, error) { + var goodsAdd planBTypePinduoduo.GoodsAddResponseWrapper + goodsInfoStr, jsonMarshalErr := json.Marshal(goodsInfo) + if jsonMarshalErr != nil { + return goodsAdd, "", jsonMarshalErr + } + //发送请求 + goodsAddStr, pddGoodsAddErr := golabl.PddDll.PddGoodsAdd(golabl.Config.PddConfig.ClientId, golabl.Config.PddConfig.ClientSecret, golabl.Task.Header.ShopMsg.Token, string(goodsInfoStr)) + //判断是否成功 + if strings.Contains(goodsAddStr, "请求失败") || strings.Contains(goodsAddStr, "错误码") { + //记录请求日志 + // 记录请求日志 + addGoodsReqMsg := fmt.Sprintf(` +════════════════════════════════════════════════════════════════ +【拼多多商品添加请求】 +请求ID: %s +时间: %s +参数: %s +════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + string(goodsInfoStr)) + + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, addGoodsReqMsg) + return goodsAdd, goodsAddStr, errors.New("拼多多 PddGoodsAdd 错误:" + goodsAddStr) + } + if pddGoodsAddErr != nil { + return goodsAdd, "", pddGoodsAddErr + } + jsonUnmarshal := json.Unmarshal([]byte(goodsAddStr), &goodsAdd) + if jsonUnmarshal != nil { + return goodsAdd, "", fmt.Errorf("解析拼多多 PddGoodsAdd 接口返回json失败: %v", jsonUnmarshal) + } + return goodsAdd, goodsAddStr, nil +} + +// 获取商品提交的商品详情 +// @param goodsCommitId 商品提交ID +// @param goodsId 商品ID +// @return GoodsCommitDetailResponse 商品提交详情 +// @return error 错误信息 +func getGoodsCommitDetail(goodsCommitId int64, goodsId int64) (planBTypePinduoduo.GoodsCommitDetailResponse, string, error) { + var goodsCommitDetail planBTypePinduoduo.GoodsCommitDetailResponse + goodsCommitDetailStr, pddGoodsCommitDetailGetErr := golabl.PddDll.PddGoodsCommitDetailGet(golabl.Config.PddConfig.ClientId, golabl.Config.PddConfig.ClientSecret, golabl.Task.Header.ShopMsg.Token, strconv.FormatInt(goodsCommitId, 10), strconv.FormatInt(goodsId, 10)) + if pddGoodsCommitDetailGetErr != nil { + return goodsCommitDetail, "", pddGoodsCommitDetailGetErr + } + unmarshalErr := json.Unmarshal([]byte(goodsCommitDetailStr), &goodsCommitDetail) + if unmarshalErr != nil { + return goodsCommitDetail, "", fmt.Errorf("解析拼多多 PddGoodsCommitDetailGet 接口返回json失败: %v [拼多多数据:%v]", unmarshalErr, goodsCommitDetailStr) + } + return goodsCommitDetail, goodsCommitDetailStr, nil +} + +// 第一阶段拉取商品信息 +// @param maxPage 最大页数 +// @param pageSize 每页数量 +// @param totalFetched 获取到的商品总数 +// @param lastCreatedAt 最后一条数据的创建时间 +// @return error 错误信息 +func phaseOneGoods(maxPage int, pageSize int, totalFetched *int, lastCreatedAt *int64) error { + // 第一阶段:获取第1页到第100页,不传入时间参数 + for page := 1; page <= maxPage; page++ { + // 定义参数 + params := map[string]string{ + "accessToken": golabl.Task.Header.ShopMsg.Token, + "page": strconv.Itoa(page), + "pageSize": strconv.Itoa(pageSize), + } + + goodsList, goodsListStr, getGoodsListErr := tool.GetPddGoodsList(params) + if getGoodsListErr != nil { + return fmt.Errorf("获取商品列表失败,页码: %d, 错误: %v", page, getGoodsListErr) + } + if goodsListStr == "{}" { + //如果读取不到数据,重试一次 + fmt.Println("通过容器获取获取商品列表数据失败,重试一次") + goodsList, goodsListStr, getGoodsListErr = tool.GetPddGoodsList(params) + if getGoodsListErr != nil { + return fmt.Errorf("获取商品列表失败,页码: %d, 错误: %v", page, getGoodsListErr) + } + } + if goodsListStr == "{}" { + fmt.Println("---------------------------------------错误!!!!goodsListStr----------------------------------") + fmt.Println(goodsListStr) + fmt.Println("---------------------------------------错误!!!!goodsListStr----------------------------------") + return fmt.Errorf("容器返回数据为空") + } + + //更新 header进度总数 + if page == 1 { + // 更新进度 + fmt.Println("总数 ", strconv.Itoa(goodsList.TotalCount)) + if updateTaskHeaderErr := service.SetTaskCount(strconv.Itoa(goodsList.TotalCount)); updateTaskHeaderErr != nil { + return updateTaskHeaderErr + } + } + + //如果是需要拉取详情的商品 + if golabl.Task.Header.TaskType == 4 { + // 获取原始商品列表 + originalGoodsList := goodsList.GoodsList + totalCount := len(originalGoodsList) + + if totalCount == 0 { + return nil // 或继续后续处理 + } + + // 存储所有获取到的商品详情 + allGoodsDetailList := make([]planBTypePinduoduo.GoodsItem, 0, totalCount) + + // 每100条调用一次 + batchSize := 100 + n := 0 + for i := 0; i < totalCount; i += batchSize { + // 计算当前批次的起始和结束位置 + end := i + batchSize + if end > totalCount { + end = totalCount + } + batch := originalGoodsList[i:end] + n++ + // 调用接口获取商品详情 + fmt.Printf("第 %v 页 第 %v 次 \n", page, n) + goodsDetailList, goodsDetailListStr, getPddGoodsDetailErr := tool.GetPddGoodsDetail(batch) + if getPddGoodsDetailErr != nil { + fmt.Println("----------------------------错误!!!!goodsDetailList-------------------------------") + fmt.Printf("batch start %d end %d, batch size %d, total %d\n", i, end, len(batch), totalCount) + fmt.Println(goodsDetailListStr) + fmt.Println("----------------------------错误!!!!goodsDetailList-------------------------------") + return getPddGoodsDetailErr + } + + // 将当前批次的结果添加到总结果中 + allGoodsDetailList = append(allGoodsDetailList, goodsDetailList...) + } + + // 赋值回原变量 + goodsList.GoodsList = allGoodsDetailList + } + // 收集商品数据并统计 + for _, goods := range goodsList.GoodsList { + *totalFetched++ + //写入到数据库中 + //将goods转为json + jsonData, jsonMarshalErr := json.Marshal(goods) + if jsonMarshalErr != nil { + return fmt.Errorf("将商品转为json失败: %v\n", jsonMarshalErr) + } + //写入到数据库中 + if len(goods.SkuList) <= 0 { + return fmt.Errorf("商品sku列表为空 goodsId %v", goods.GoodsId) + } + bodyWait := planAType.TaskBody{ + BookInfo: planAType.BookInfo{ + Isbn: goods.SkuList[0].OuterId, + BookName: goods.GoodsName, + Author: "", + Publishing: "", + PublicationDate: "", + Binding: "", + PagesCount: 0, + WordsCount: 0, + Format: 0, + Price: goods.CreatedAt, + }, + Detail: planAType.TaskDetail{ + Status: 1, + Error: string(jsonData), + GoodsId: goods.GoodsId, + Stock: int32(goods.SkuList[0].ReserveQuantity), + }, + } + // 将bodyWait 转为json + bodyWaitJson, jsonMarshalErr := json.Marshal(bodyWait) + if jsonMarshalErr != nil { + return fmt.Errorf("将bodyWait转为json失败: %v\n", jsonMarshalErr) + } + //写入 body_wait + addTaskToBodyWaitErr := service.AddTaskToBodyWait(string(bodyWaitJson)) + if addTaskToBodyWaitErr != nil { + return addTaskToBodyWaitErr + } + } + + // 记录最后一页的最后一条数据的创建时间 + if page == maxPage && len(goodsList.GoodsList) > 0 { + *lastCreatedAt = goodsList.GoodsList[len(goodsList.GoodsList)-1].CreatedAt + fmt.Printf("最后一页,最后一条数据的创建时间: %v", *lastCreatedAt) + } + + // 如果没有更多数据,提前退出 + if len(goodsList.GoodsList) == 0 { + fmt.Println("没有更多数据,退出循环 ") + fmt.Println(goodsListStr) + break + } + + // 获取 footer信息 + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + con := int64(len(goodsList.GoodsList)) + if con >= golabl.Task.Footer.TaskCountTrue { + con = golabl.Task.Footer.TaskCountTrue - con + } + // 更新 进度 + if updateTaskProgressErr := tool.UpdateTaskProgress(con); updateTaskProgressErr != nil { + return updateTaskProgressErr + } + + // 可选:添加延迟,避免请求过快 + // 暂停200豪秒 + time.Sleep(200 * time.Millisecond) + fmt.Printf("第一阶段 - 总数:%v 当前已取出:%v \n", goodsList.TotalCount, *totalFetched) + } + return nil +} + +// PhaseTwoGoods 第二阶段拉取商品信息 +// @param pageSize 每页数量 +// @param totalFetched 获取到的商品总数 +// @param lastCreatedAt 最后一条数据的创建时间 +// @param maxRecordsPerRange 每次请求的时间范围最多获取的记录数 +// @return error 错误信息 +func PhaseTwoGoods(pageSize int, totalFetched *int, lastCreatedAt *int64, maxRecordsPerRange int) error { + // 第二阶段:使用时间范围分批次获取数据,每批最多获取10000条 + // 设置结束时间为开始时间+30天 + endTime := *lastCreatedAt + 30*24*60*60 + fmt.Printf("第二阶段开始,结束时间设置为: %d (%s)\n", endTime, time.Unix(endTime, 0).Format("2006-01-02 15:04:05")) + + if *lastCreatedAt > 0 { + currentCreatedAtFrom := *lastCreatedAt + maxLoopCount := 100 // 最大循环次数保护 + loopCount := 0 + var lastPageGoodsList []planBTypePinduoduo.GoodsItem // 记录上一页的商品列表 + + for loopCount < maxLoopCount { + loopCount++ + + // 检查开始时间是否已超过当前时间 + if currentCreatedAtFrom > time.Now().Unix() { + fmt.Printf("开始时间 %d 已超过当前时间,停止获取\n", currentCreatedAtFrom) + break + } + + // 每次循环都重新设置结束时间为开始时间+30天 + currentCreatedAtEnd := currentCreatedAtFrom + 30*24*60*60 + + fmt.Printf("开始获取时间范围: %d 到 %d\n", currentCreatedAtFrom, currentCreatedAtEnd) + + currentPage := 1 + batchGoodsCount := 0 + lastItemCreatedAt := int64(0) + hasDataInRange := false + lastPageGoodsList = nil // 重置上一页商品列表 + + // 在当前时间范围内分页获取数据 + for { + params := map[string]string{ + "accessToken": golabl.Task.Header.ShopMsg.Token, + "page": strconv.Itoa(currentPage), + "pageSize": strconv.Itoa(pageSize), + "createdAtFrom": strconv.FormatInt(currentCreatedAtFrom, 10), + "createdAtEnd": strconv.FormatInt(currentCreatedAtEnd, 10), + } + + goodsList, goodsListStr, getGoodsListErr := tool.GetPddGoodsList(params) + if getGoodsListErr != nil { + return fmt.Errorf("获取商品列表失败(时间范围),页码: %d, 错误: %v", currentPage, getGoodsListErr) + } + if goodsListStr == "{}" { + fmt.Println("通过容器获取获取商品列表数据失败,重试一次") + goodsList, goodsListStr, getGoodsListErr = tool.GetPddGoodsList(params) + if getGoodsListErr != nil { + return fmt.Errorf("获取商品列表失败(时间范围),页码: %d, 错误: %v", currentPage, getGoodsListErr) + } + } + if goodsListStr == "{}" { + return fmt.Errorf("容器返回数据为空") + } + + //如果是需要拉取详情的商品 + if golabl.Task.Header.TaskType == 4 { + // 获取原始商品列表 + originalGoodsList := goodsList.GoodsList + totalCount := len(originalGoodsList) + + if totalCount == 0 { + return nil // 或继续后续处理 + } + + // 存储所有获取到的商品详情 + allGoodsDetailList := make([]planBTypePinduoduo.GoodsItem, 0, totalCount) + + // 每100条调用一次 + batchSize := 100 + n := 0 + for i := 0; i < totalCount; i += batchSize { + // 计算当前批次的起始和结束位置 + end := i + batchSize + if end > totalCount { + end = totalCount + } + batch := originalGoodsList[i:end] + n++ + // 调用接口获取商品详情 + fmt.Printf(" 第 %v 次 \n", n) + goodsDetailList, goodsDetailListStr, getPddGoodsDetailErr := tool.GetPddGoodsDetail(batch) + if getPddGoodsDetailErr != nil { + fmt.Println("----------------------------错误!!!!goodsDetailList-------------------------------") + fmt.Printf("batch start %d end %d, batch size %d, total %d\n", i, end, len(batch), totalCount) + fmt.Println(goodsDetailListStr) + fmt.Println("----------------------------错误!!!!goodsDetailList-------------------------------") + return getPddGoodsDetailErr + } + + // 将当前批次的结果添加到总结果中 + allGoodsDetailList = append(allGoodsDetailList, goodsDetailList...) + } + + // 赋值回原变量 + goodsList.GoodsList = allGoodsDetailList + } + // 如果当前页没有数据 + if len(goodsList.GoodsList) == 0 { + // 如果当前页是第一页且没有数据 + if currentPage == 1 { + // 整个时间范围都没有数据,直接推进到结束时间 + currentCreatedAtFrom = currentCreatedAtEnd + fmt.Printf("时间范围 %d - %d 内无数据,推进开始时间到: %d\n", currentCreatedAtFrom-30*24*60*60, currentCreatedAtEnd, currentCreatedAtFrom) + break + } + + // 当前页没有数据,但上一页有数据 + // 取上一页最后一条数据的创建时间和GoodsId作为新的开始位置 + if len(lastPageGoodsList) > 0 { + lastItemOfLastPage := lastPageGoodsList[len(lastPageGoodsList)-1] + newStartTime := lastItemOfLastPage.CreatedAt + lastGoodsId := lastItemOfLastPage.GoodsId + + // 使用基于 GoodsId的定位策略 + if newStartTime > currentCreatedAtFrom { + currentCreatedAtFrom = newStartTime + fmt.Printf("当前页无数据,使用上一页最后一条商品时间作为新开始时间: %d, 最后商品ID: %d\n", currentCreatedAtFrom, lastGoodsId) + } else { + // 如果时间相同,需要基于GoodsId来推进,这里简单地将时间加1秒 + currentCreatedAtFrom = newStartTime + 1 + fmt.Printf("当前页无数据,时间相同,将时间加1秒推进: %d\n", currentCreatedAtFrom) + } + } else { + // 理论上不会走到这里,但为了安全,将开始时间推进到结束时间 + currentCreatedAtFrom = currentCreatedAtEnd + fmt.Printf("当前页无数据且无上一页数据,将开始时间推进到结束时间: %d\n", currentCreatedAtFrom) + } + + hasDataInRange = false + break + } + + // 有数据,记录上一页的商品列表 + lastPageGoodsList = goodsList.GoodsList + hasDataInRange = true + + // 收集商品数据并统计 + for _, goods := range goodsList.GoodsList { + *totalFetched++ + // 写入到数据库中 + // 将goods转为json + jsonData, jsonMarshalErr := json.Marshal(goods) + if jsonMarshalErr != nil { + return fmt.Errorf("将商品转为json失败: %v\n", jsonMarshalErr) + } + // 写入到数据库中 + if len(goods.SkuList) <= 0 { + return fmt.Errorf("商品sku列表为空 goodsId %v", goods.GoodsId) + } + bodyWait := planAType.TaskBody{ + BookInfo: planAType.BookInfo{ + Isbn: goods.SkuList[0].OuterId, + BookName: goods.GoodsName, + Author: "", + Publishing: "", + PublicationDate: "", + Binding: "", + PagesCount: 0, + WordsCount: 0, + Format: 0, + Price: goods.CreatedAt, + }, + Detail: planAType.TaskDetail{ + Error: string(jsonData), + GoodsId: goods.GoodsId, + Stock: int32(goods.SkuList[0].ReserveQuantity), + }, + } + // 将bodyWait 转为json + bodyWaitJson, jsonMarshalErr := json.Marshal(bodyWait) + if jsonMarshalErr != nil { + return fmt.Errorf("将bodyWait转为json失败: %v\n", jsonMarshalErr) + } + //写入 body_wait + addTaskToBodyWaitErr := service.AddTaskToBodyWait(string(bodyWaitJson)) + if addTaskToBodyWaitErr != nil { + return addTaskToBodyWaitErr + } + } + + batchGoodsCount += len(goodsList.GoodsList) + + // 记录最后一条商品的创建时间 + lastItem := goodsList.GoodsList[len(goodsList.GoodsList)-1] + lastItemCreatedAt = lastItem.CreatedAt + + fmt.Printf("第二阶段 - 当前时间范围已获取: %d 条,累计总数: %d,当前页码: %d,最后商品时间: %d\n", + batchGoodsCount, *totalFetched, currentPage, lastItemCreatedAt) + + // 判断是否需要结束当前时间范围 + // 1. 如果当前批次已经达到或超过 maxRecordsPerRange + // 2. 或者返回的数据少于 pageSize(说明没有下一页了) + if batchGoodsCount >= maxRecordsPerRange || len(goodsList.GoodsList) < pageSize { + // 关键修复:使用最后一条商品的时间作为新的开始时间 + // 如果最后一条商品时间等于当前开始时间,则加1秒避免死循环 + if lastItemCreatedAt == currentCreatedAtFrom { + currentCreatedAtFrom = lastItemCreatedAt + 1 + fmt.Printf("最后商品时间与开始时间相同,推进1秒: %d -> %d\n", lastItemCreatedAt, currentCreatedAtFrom) + } else { + currentCreatedAtFrom = lastItemCreatedAt + } + fmt.Printf("当前时间范围已获取 %d 条数据,准备进入下一时间范围 更新开始时间为: %d \n", batchGoodsCount, currentCreatedAtFrom) + break + } + + currentPage++ + + // 获取 footer信息 + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + con := int64(len(goodsList.GoodsList)) + if con >= golabl.Task.Footer.TaskCountTrue { + con = golabl.Task.Footer.TaskCountTrue - con + } + // 更新 进度 + if updateTaskProgressErr := tool.UpdateTaskProgress(con); updateTaskProgressErr != nil { + return updateTaskProgressErr + } + + // 暂停200豪秒 + time.Sleep(200 * time.Millisecond) + } + + // 判断是否需要继续循环 + // 情况1:当前时间范围内没有获取到任何数据 + if !hasDataInRange { + // 检查新的开始时间是否已超过当前时间 + if currentCreatedAtFrom > time.Now().Unix() { + fmt.Printf("开始时间 %d 已超过当前时间,停止获取\n", currentCreatedAtFrom) + break + } + // 否则继续下一轮循环 + fmt.Printf("继续下一轮查询,新起始时间: %d\n", currentCreatedAtFrom) + continue + } + + // 情况2:当前批次获取的数据少于 maxRecordsPerRange 并且开始时间大于当前时间,说明已经没有更多数据了 + if batchGoodsCount < maxRecordsPerRange && currentCreatedAtFrom > time.Now().Unix() { + fmt.Printf("当前批次获取 %d 条数据,少于 %d,且开始时间已超过当前时间,已完成所有数据获取\n", batchGoodsCount, maxRecordsPerRange) + break + } + + // 暂停200豪秒 + time.Sleep(200 * time.Millisecond) + } + + if loopCount >= maxLoopCount { + fmt.Printf("警告:已达到最大循环次数 %d,强制退出\n", maxLoopCount) + } + } + return nil +} + +// 拉取任务 读取body_wait去重复后写入到body_over中 +// @param duplicateCount int64 重复商品数量 +// @param uniqueCount int64 不重复商品数量 +// @return error 错误信息 +func deduplicateToBodyOver(duplicateCount *int, uniqueCount *int) error { + page := 1 + pageSize := 1000 + var dataList []planBTypePinduoduo.GoodsItem + // 在循环外维护一个已处理的商品 ID集合 + processedGoodsIds := make(map[int64]bool) + //在循环前删除 body_over与body_backup,避免重复写入 + deleteTaskBodyOverErr := service.DeleteTaskBodyOver() + if deleteTaskBodyOverErr != nil { + return deleteTaskBodyOverErr + } + deleteTaskBodyBackupErr := service.DeleteTaskBodyBackup() + if deleteTaskBodyBackupErr != nil { + return deleteTaskBodyBackupErr + } + num := 0 + //获取body_wait总数量 + bodyWaitCount, getTaskBodyWaitCountErr := service.GetTaskBodyWaitCount() + if getTaskBodyWaitCountErr != nil { + return getTaskBodyWaitCountErr + } + pageTotal := (bodyWaitCount + int64(pageSize) - 1) / int64(pageSize) + for { + list, getTaskBodyOverListErr := service.GetTaskBodyWaitList(page, pageSize) + if getTaskBodyOverListErr != nil { + return getTaskBodyOverListErr + } + if len(list) <= 0 { + // 没有数据,结束循环 + break + } + for _, v := range list { + // 解析v 到结构体 + goods := planAType.TaskBody{} + jsonUnmarshalErr := json.Unmarshal([]byte(v), &goods) + if jsonUnmarshalErr != nil { + return fmt.Errorf("将json转为结构体失败: %v\n", jsonUnmarshalErr) + } + if !processedGoodsIds[goods.Detail.GoodsId] { + //写入到去重复集合 + processedGoodsIds[goods.Detail.GoodsId] = true + //不重复数据 计次 + *uniqueCount++ + // goods.Detail.Error(原始json到结构体) + var GoodsItem planBTypePinduoduo.GoodsItem + jsonUnmarshalErr = json.Unmarshal([]byte(goods.Detail.Error), &GoodsItem) + if jsonUnmarshalErr != nil { + return fmt.Errorf("将json转为结构体失败: %v\n", jsonUnmarshalErr) + } + //记录到data数组中,之后推送到写入店铺商品数据接口 + dataList = append(dataList, GoodsItem) + //写入到body_over + goods.Detail.Status = 1 + addTaskToBodyOverErr := service.AddTaskToBodyOver(goods, []string{"body_over", "body_backup"}) + if addTaskToBodyOverErr != nil { + return addTaskToBodyOverErr + } + + //将指定店铺信息记录到本地 + isFileShopId, isShopIDExistsErr := tool.IsShopIDExists(golabl.Task.Header.ShopId) + if isShopIDExistsErr != nil { + return isShopIDExistsErr + } + if isFileShopId { + text := goods.BookInfo.Isbn + " " + GoodsItem.BigImg + txtUrl := golabl.Config.FileUrl.PddGoodsDetailsUrl + fileName := golabl.Task.Header.TaskId + // 构建完整的文件路径 + filePath := filepath.Join(txtUrl, fileName+".txt") + // 写入文件 + if err := tool.AppendTextToFile(filePath, text); err != nil { + fmt.Println("保存详情信息到文本 失败", err.Error()) + } + } + } else { + //重复数据 计次 + *duplicateCount++ + } + } + // 将获取的数据推送写入店铺商品数据接口 + ret, retStr, writePddGoodsDataErr := tool.WritePddGoodsData(dataList, page, pageTotal) + if writePddGoodsDataErr != nil { + return writePddGoodsDataErr + } + if ret.Code != "200" { + return fmt.Errorf("添加商品失败 %v", retStr) + } + num = num + len(dataList) + fmt.Printf("开始添加商品信息到系统店铺中 当前页 %v 总页数 %v 当前数据量 %v 总数据量 %v \n", page, pageTotal, len(dataList), num) + page++ + + // 获取 footer信息 + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + con := int64(len(list)) + if con >= golabl.Task.Footer.TaskCountTrue { + con = golabl.Task.Footer.TaskCountTrue - con + } + // 更新 进度 + if updateTaskProgressErr := tool.UpdateTaskProgress(con); updateTaskProgressErr != nil { + return updateTaskProgressErr + } + + //清空 dataStr + dataList = []planBTypePinduoduo.GoodsItem{} + // 暂停1秒 + time.Sleep(1 * time.Second) + } + // 删除body_wait + deleteTaskBodyWaitErr := service.DeleteTaskBodyWait() + if deleteTaskBodyWaitErr != nil { + return deleteTaskBodyWaitErr + } + return nil +} + +// setSaleStatusGoodsTask 设置商品上下架状态 +// @param taskMsg 任务内容 +// @return string body 信息 +func setSaleStatusGoodsTask(logUuid string, taskMsg planAType.TaskBody) (string, error) { + // 拼多多商品 Id不能为空 + if taskMsg.Detail.GoodsId == 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("拼多多商品 Id不能为空")) + } + + var reqDataInfo planBTypePinduoduo.SetSaleStatusGoodsTaskReq + reqDataInfo.GoodsId = taskMsg.Detail.GoodsId + if taskMsg.Detail.Status == 1 { + reqDataInfo.IsOnsale = 1 //上架 + } else if taskMsg.Detail.Status == 2 { + reqDataInfo.IsOnsale = 0 //下架 + } else { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("任务类型错误")) + } + setSoleStatusGoodsRet, _, err := setSoleStatusGoods(logUuid, reqDataInfo) + if err != nil { + return "", err + } + if !setSoleStatusGoodsRet.GoodsSaleStatusSetResponse.IsSuccess { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("设置商品上下架状态失败")) + } + return tool.ReturnSuccess(taskMsg) +} + +// setSoleStatusGoods 商品上下架 +// @param logUuid 日志ID +// @param reqDataInfo 请求信息 +// @return SetSaleStatusGoodsTaskResponse 结果 +// @return string 结果json +// @return error 错误信息 +func setSoleStatusGoods(logUuid string, reqDataInfo planBTypePinduoduo.SetSaleStatusGoodsTaskReq) (planBTypePinduoduo.SetSaleStatusGoodsTaskResponse, string, error) { + var setSoleStatusGoods planBTypePinduoduo.SetSaleStatusGoodsTaskResponse + goodsInfoStr, jsonMarshalErr := json.Marshal(reqDataInfo) + if jsonMarshalErr != nil { + return setSoleStatusGoods, "", jsonMarshalErr + } + //发送请求 + goodsSoleStatusStr, pddGoodsSoleStatusErr := golabl.PddDll.PddGoodsSaleStatusSet(golabl.Config.PddConfig.ClientId, golabl.Config.PddConfig.ClientSecret, golabl.Task.Header.ShopMsg.Token, string(goodsInfoStr)) + //判断是否成功 + if strings.Contains(goodsSoleStatusStr, "请求失败") || strings.Contains(goodsSoleStatusStr, "错误码") { + //记录请求日志 + reqMsg := fmt.Sprintf(` +════════════════════════════════════════════════════════════════ +【拼多多上下架请求】 +请求ID: %s +时间: %s +参数: %s +════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + string(goodsInfoStr)) + + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, reqMsg) + return setSoleStatusGoods, goodsSoleStatusStr, errors.New("拼多多 PddGoodsSaleStatusSet 错误:" + goodsSoleStatusStr) + } + if pddGoodsSoleStatusErr != nil { + return setSoleStatusGoods, "", pddGoodsSoleStatusErr + } + jsonUnmarshal := json.Unmarshal([]byte(goodsSoleStatusStr), &setSoleStatusGoods) + if jsonUnmarshal != nil { + return setSoleStatusGoods, "", fmt.Errorf("解析拼多多 PddGoodsAdd 接口返回json失败: %v", jsonUnmarshal) + } + return setSoleStatusGoods, goodsSoleStatusStr, nil +} + +// updateGoodsQuantity 修改库存 +func updateGoodsQuantity(logUuid string, taskMsg planAType.TaskBody, UpdateType int, stock int64) (string, error) { + // 拼多多商品 Id不能为空 + if taskMsg.Detail.GoodsId == 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("拼多多商品 Id不能为空")) + } + // 拼多多商品 SkuId不能为空 + if taskMsg.Detail.SkuId == 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("拼多多商品 SkuId不能为空")) + } + reqDataInfo := planBTypePinduoduo.UpdateGoodsQuantity{ + ForceUpdate: true, + GoodsId: taskMsg.Detail.GoodsId, + SkuId: taskMsg.Detail.SkuId, + Quantity: int64(taskMsg.Detail.Stock), + UpdateType: UpdateType, + } + delGoodsRet, _, err := quantityGoods(logUuid, reqDataInfo) + if err != nil { + return "", err + } + if !delGoodsRet.GoodsQuantityUpdateResponse.IsSuccess { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("修改商品库存失败")) + } + if UpdateType == 2 { + taskMsg.Detail.Stock = taskMsg.Detail.Stock + int32(stock) + } + taskMsg.Detail.Error = "增加库存成功!" + return tool.ReturnSuccess(taskMsg) +} + +// quantityGoods 修改库存 +// @param logUuid 日志ID +// @param reqDataInfo 请求信息 +// @return GoodsAddResponseWrapper 结果 +// @return string 结果json +// @return error 错误信息 +func quantityGoods(logUuid string, reqDataInfo planBTypePinduoduo.UpdateGoodsQuantity) (planBTypePinduoduo.UpdateGoodsQuantityResponse, string, error) { + var quantityGoods planBTypePinduoduo.UpdateGoodsQuantityResponse + goodsInfoStr, jsonMarshalErr := json.Marshal(reqDataInfo) + if jsonMarshalErr != nil { + return quantityGoods, "", jsonMarshalErr + } + //发送请求 + quantityGoodsStr, pddGoodsQuantityErr := golabl.PddDll.PddGoodsQuantityUpdate(golabl.Config.PddConfig.ClientId, golabl.Config.PddConfig.ClientSecret, golabl.Task.Header.ShopMsg.Token, string(goodsInfoStr)) + //判断是否成功 + if strings.Contains(quantityGoodsStr, "请求失败") || strings.Contains(quantityGoodsStr, "错误码") { + //记录请求日志 + reqMsg := fmt.Sprintf(` + ════════════════════════════════════════════════════════════════ + 【拼多多修改商品库存请求】 + 请求ID: %s + 时间: %s + 参数: %s + ════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + string(goodsInfoStr)) + + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, reqMsg) + return quantityGoods, quantityGoodsStr, errors.New("拼多多 quantityGoods 错误:" + quantityGoodsStr) + } + if pddGoodsQuantityErr != nil { + return quantityGoods, "", pddGoodsQuantityErr + } + jsonUnmarshal := json.Unmarshal([]byte(quantityGoodsStr), &quantityGoods) + if jsonUnmarshal != nil { + return quantityGoods, "", fmt.Errorf("解析拼多多 quantityGoods 接口返回json失败: %v", jsonUnmarshal) + } + return quantityGoods, quantityGoodsStr, nil +} + +// updateSkuPrice 修改商品价格 +func updateSkuPrice(logUuid string, taskMsg planAType.TaskBody) (string, error) { + // 拼多多商品 Id不能为空 + if taskMsg.Detail.GoodsId == 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("拼多多商品 Id不能为空")) + } + // 拼多多商品 SkuId不能为空 + if taskMsg.Detail.SkuId == 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("拼多多商品 SkuId不能为空")) + } + // 价格0 不能发布 + if taskMsg.Detail.Price == 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("拼多多商品 价格不能为0")) + } + fen := tool.BuildGoodsPrice(taskMsg.Detail.Price) + yuan := tool.FenToYuan(fen) + reqDataInfo := planBTypePinduoduo.UpdateSkuPrice{ + GoodsId: taskMsg.Detail.GoodsId, + MarketPrice: fen, + MarketPriceInYuan: yuan, + SkuPriceList: []planBTypePinduoduo.SkuPriceItem{ + { + GroupPrice: taskMsg.Detail.Price, + SinglePrice: taskMsg.Detail.Price + 100, + SkuId: taskMsg.Detail.SkuId, + }, + }, + } + delGoodsRet, _, err := skuPrice(logUuid, reqDataInfo) + if err != nil { + return "", err + } + if !delGoodsRet.GoodsUpdateSkuPriceResponse.IsSuccess { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("修改商品库存失败")) + } + return tool.ReturnSuccess(taskMsg) +} + +// skuPrice 修改价格 +// @param logUuid 日志ID +// @param reqDataInfo 请求信息 +// @return GoodsAddResponseWrapper 结果 +// @return string 结果json +// @return error 错误信息 +func skuPrice(logUuid string, reqDataInfo planBTypePinduoduo.UpdateSkuPrice) (planBTypePinduoduo.UpdateGoodsSkuPriceResponse, string, error) { + var skuPriceGoods planBTypePinduoduo.UpdateGoodsSkuPriceResponse + goodsInfoStr, jsonMarshalErr := json.Marshal(reqDataInfo) + if jsonMarshalErr != nil { + return skuPriceGoods, "", jsonMarshalErr + } + //发送请求 + skuPriceGoodsStr, pddGoodsSkuPriceErr := golabl.PddDll.PddGoodsSkuPriceUpdate(golabl.Config.PddConfig.ClientId, golabl.Config.PddConfig.ClientSecret, golabl.Task.Header.ShopMsg.Token, string(goodsInfoStr)) + //判断是否成功 + if strings.Contains(skuPriceGoodsStr, "请求失败") || strings.Contains(skuPriceGoodsStr, "错误码") { + //记录请求日志 + reqMsg := fmt.Sprintf(` + ════════════════════════════════════════════════════════════════ + 【拼多多修改商品价格请求】 + 请求ID: %s + 时间: %s + 参数: %s + ════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + string(goodsInfoStr)) + + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, reqMsg) + return skuPriceGoods, skuPriceGoodsStr, errors.New("拼多多 skuPrice 错误:" + skuPriceGoodsStr) + } + if pddGoodsSkuPriceErr != nil { + return skuPriceGoods, "", pddGoodsSkuPriceErr + } + jsonUnmarshal := json.Unmarshal([]byte(skuPriceGoodsStr), &skuPriceGoods) + if jsonUnmarshal != nil { + return skuPriceGoods, "", fmt.Errorf("解析拼多多 skuPrice 接口返回json失败: %v", jsonUnmarshal) + } + return skuPriceGoods, skuPriceGoodsStr, nil +} + +// 拼多多发布 +func publishGoods(logUuid string, taskMsg planAType.TaskBody) (planAType.TaskBody, error) { + // 价格不能小于0 + if taskMsg.Detail.Price <= 0 { + return taskMsg, fmt.Errorf("价格不能小于等于0") + } + + //获取出版社信息并解析1 + if getPublishingErr := service.GetPublishingVid(&taskMsg); getPublishingErr != nil { + return taskMsg, fmt.Errorf("获取出版社信息失败-原因来自:%v", getPublishingErr) + } + + //违规词处理1 + if golabl.Config.Server.Filter == 1 { + //开启违规词处理 + if taskMsgErr := tool.FilterWord(&taskMsg); taskMsgErr != nil { + return taskMsg, taskMsgErr + } + } + + //价格 + 运费 + if golabl.Task.Header.PriceType != "0" { + taskMsg.Detail.Price = taskMsg.Detail.Price + taskMsg.Detail.ShippingCost + } + + // 价格处理 + price := tool.BuildPrice(golabl.Task.Header.PriceMod, taskMsg.Detail.Price) + if price == 0 { + return taskMsg, fmt.Errorf("不在价格区间内 isbn %v 原始价格 %v 当前价格 %v 价格模版 %v", taskMsg.BookInfo.Isbn, taskMsg.Detail.Price, price, golabl.Task.Header.PriceMod) + } + + taskMsg.Detail.Price = price + + var goodsAdd planBTypePinduoduo.GoodsAdd + + // *********************构建参数 开始******************************** // + + //构建商品名称 + goodsAdd.GoodsName = tool.BuildGoodsName( + golabl.Task.Header.ShopMsg.GoodsNamePrefix, // 商品名称前缀 + golabl.Task.Header.ShopMsg.GoodsNameSuffix, // 商品名称后缀 + golabl.Task.Header.ShopMsg.TitleConsistOf, // 标题组成 + golabl.Task.Header.ShopMsg.SpaceCharacter, // 间隔符 + taskMsg.BookInfo) // 图书信息 + taskMsg.Detail.GoodsName = goodsAdd.GoodsName + + // 构建轮播图 + //if taskHeader.ShopMsg.WatermarkImgUrl == "" && len(taskHeader.ShopMsg.CarouseLastImgUrlArray) == 0 && len(taskMsg.BookInfo.ImageObject.CarouselUrlArray) == 0 && taskMsg.BookInfo.ImageObject.DefaultImageUrl == "" { + // return tool.ReturnErr(logUuid, taskMsg, _type.GoodsTypeAdd,fmt.Errorf("缺少构造轮播图图片-未提交 isbn %v", taskMsg.BookInfo.Isbn)) + //} + if len(taskMsg.BookInfo.ImageObject.CarouselUrlArray) == 0 { + // 无图片信息 isbn计次 + setNoImgCountErr := service.SetNoImgCount(taskMsg.BookInfo.Isbn) + if setNoImgCountErr != nil { + return taskMsg, fmt.Errorf("无图片信息isbn计次错误 isbn %v %v", taskMsg.BookInfo.Isbn, setNoImgCountErr.Error()) + } + return taskMsg, fmt.Errorf("缺少轮播图") + } + + //判断是否拼多多的图片,如果非拼多多图片则上传到拼多多的图片空间 + for k, v := range taskMsg.BookInfo.ImageObject.CarouselUrlArray { + // 判断v 是否包含字符串 img.pddpic.com + if !strings.Contains(v, "img.pddpic.com") { + tempUrl, saveBase64ImageByDateErr := tool.SaveBase64ImageByDate(v, golabl.Config.FileUrl.PddImgTempUrl) + if saveBase64ImageByDateErr != nil { + return taskMsg, saveBase64ImageByDateErr + } + //转为base64 + imgBase64, format, processImageErr := tool.ProcessImage(tempUrl) + if processImageErr != nil { + return taskMsg, processImageErr + } + + // 将base64字符串包装成 ImageResult 类型 + imageResult := []planBTypeModules.ImageResult{ + { + Success: true, // 假设图片处理成功 + Format: format, // 或者根据实际情况获取图片格式 + Data: imgBase64, // base64数据 + }, + } + + //上传到拼多多图片空间 + toPdd, uploadImageToPddErr := tool.UploadImageToPdd(imageResult) + if uploadImageToPddErr != nil { + return taskMsg, fmt.Errorf("上传图片到拼多多失败 %v", uploadImageToPddErr) + } + taskMsg.BookInfo.ImageObject.CarouselUrlArray[k] = toPdd[0] + } + } + + oldCarouselUrlArray := append([]string{}, taskMsg.BookInfo.ImageObject.CarouselUrlArray...) //原始轮播图,用于后续处理,不会被打上水印 + + // 存在水印图片,则打水印 + if golabl.Task.Header.ShopMsg.WatermarkImgUrl != "" { + //获取水印图片 + watermarkImgUrl, watermarkImgErr := tool.GetWatermarkImg() + if watermarkImgErr != nil { + return taskMsg, fmt.Errorf("获取水印图片失败 %v", watermarkImgErr) + } + //打水印 + watermarkFromURLExsBase64Arr, watermarkFromURLExsErr := tool.AddWatermarkFromURLExs(taskMsg.BookInfo.ImageObject.CarouselUrlArray, watermarkImgUrl, golabl.Task.Header.ShopMsg.WatermarkPosition) + if watermarkFromURLExsErr != nil { + return taskMsg, fmt.Errorf("图片打水印失败 %v", watermarkFromURLExsErr) + } + //图片上传到拼多多 + toPdd, uploadImageToPddErr := tool.UploadImageToPdd(watermarkFromURLExsBase64Arr) + if uploadImageToPddErr != nil { + return taskMsg, fmt.Errorf("图片上传到拼多多失败 %v", uploadImageToPddErr) + } + //将上传的图片替换到商品轮播图中 + for i := 0; i < len(toPdd); i++ { + taskMsg.BookInfo.ImageObject.CarouselUrlArray[i] = toPdd[i] + } + } + + goodsAdd.CarouselGallery = tool.BuildCarouselGallery(golabl.Task.Header.ShopMsg.CarouseLastImgUrlArray, oldCarouselUrlArray, taskMsg.BookInfo.ImageObject.CarouselUrlArray, golabl.Task.Header.ShopMsg.WatermarkPosition) + + if len(taskMsg.BookInfo.ImageObject.DetailUrlObject.LiveShootingUrl) == 0 && len(oldCarouselUrlArray) > 0 { + taskMsg.BookInfo.ImageObject.DetailUrlObject.LiveShootingUrl = []string{oldCarouselUrlArray[0]} + } + if len(goodsAdd.CarouselGallery) == 0 { + return taskMsg, fmt.Errorf("缺少构造轮播图图片-未提交 isbn %v", taskMsg.BookInfo.Isbn) + } + // 构建详情图 + goodsAdd.DetailGallery = tool.BuildDetailGallery(golabl.Task.Header.ShopMsg.GoodsDetailFirstImgUrlArray, golabl.Task.Header.ShopMsg.GoodsDetailLastImgUrlArray, taskMsg.BookInfo.ImageObject.DetailUrlObject, oldCarouselUrlArray[0]) + if len(goodsAdd.DetailGallery) == 0 { + return taskMsg, fmt.Errorf("缺少构造详情图-未提交 isbn %v", taskMsg.BookInfo.Isbn) + } + + // 构建 catId + var catID int64 + + if taskMsg.BookInfo.CatIdObject.PinDuoDuoCatId == "" { + // 调用拼多多 SDK 取类目信息 + pddCalbackStr, pddGoodsOuterCatMappingGetErr := golabl.PddDll.PddGoodsOuterCatMappingGet(golabl.Config.PddConfig.ClientId, golabl.Config.PddConfig.ClientSecret, golabl.Task.Header.ShopMsg.Token, "15543", "书籍/杂志/报纸", "书籍 "+taskMsg.BookInfo.BookName) + if pddGoodsOuterCatMappingGetErr != nil { + return taskMsg, fmt.Errorf("调用DLL类目映射失败 %w", pddGoodsOuterCatMappingGetErr) + } + + // 解析返回的 JSON 字符串 + var response planBTypePinduoduo.PddSuccessResponse + if unmarshalErr := json.Unmarshal([]byte(pddCalbackStr), &response); unmarshalErr != nil { + return taskMsg, fmt.Errorf("json.Unmarshal错误 %w %v", unmarshalErr, pddCalbackStr) + } + + // 判断 catID4 是否为0 + if response.OuterCatMappingGetResponse.CatID4 != 0 { + catID = response.OuterCatMappingGetResponse.CatID4 + } else { + catID = response.OuterCatMappingGetResponse.CatID3 + } + } else { + // 数据库原本存储的为字符串 转成int64再使用 + retCatID, toInt64Err := taskMsg.BookInfo.CatIdObject.PinDuoDuoCatId.ToInt64() + if toInt64Err != nil { + return taskMsg, fmt.Errorf("转换catId错误 %w", toInt64Err) + } + catID = retCatID + } + + // 设置 catId + goodsAdd.CatId = catID + + // 构建商品类型 + goodsAdd.GoodsType = 1 + + // 构建参考价格 + goodsAdd.MarketPrice = tool.BuildGoodsPrice(price) + + // 构建商品编码 + if taskMsg.Detail.OutGoodsId != "" { + goodsAdd.OutGoodsId = taskMsg.Detail.OutGoodsId + } else { + goodsAdd.OutGoodsId = taskMsg.BookInfo.Isbn + } + + // 是否支持假一赔十 + goodsAdd.IsFolt = golabl.Task.Header.ShopMsg.IsFolt + + // 是否预售 + goodsAdd.IsPreSale = golabl.Task.Header.ShopMsg.IsPreSale + + // 是否支持7天无理由退换货 + goodsAdd.IsRefundable = golabl.Task.Header.ShopMsg.IsRefundable + + // 构建是否是二手商品 + goodsAdd.SecondHand = golabl.Task.Header.ShopMsg.IsSecondHand + + // 构建物流运费模板 ID + goodsAdd.CostTemplateId = golabl.Task.Header.ShopMsg.CostTemplateId + + // 构建承诺发货时间 + goodsAdd.ShipmentLimitSecond = 48 * 60 * 60 + + //满两件折扣 + goodsAdd.TwoPiecesDiscount = golabl.Task.Header.ShopMsg.TwoDiscount + + //构建 + taskMsgBookInfoPrice := taskMsg.BookInfo.Price + if taskMsgBookInfoPrice < 10000 { + taskMsgBookInfoPrice = 10000 + } + + goodsAdd.GoodsProperties = buildGoodsPropertiesList( + taskMsg.BookInfo.Isbn, // ISBN + goodsAdd.GoodsName, // 商品名称 + taskMsg.BookInfo.PagesCount, // 页数 + taskMsgBookInfoPrice, // 价格 + taskMsg.Publishing.Vid, // 出版社 Vid + taskMsg.BookInfo.Author, // 作者 + taskMsg.BookInfo.Format, // 开本 + taskMsg.BookInfo.Binding, // 装帧 + taskMsg.BookInfo.WordsCount, // 字数 + taskMsg.BookInfo.PublicationDate, // 出版时间 + ) + + //库存 + if taskMsg.Detail.Stock == 0 && (golabl.Task.Header.TaskType == 1 || golabl.Task.Header.TaskType == 2 || golabl.Task.Header.TaskType == 6) { + //如果库存为0 则给默认库存 + taskMsg.Detail.Stock = golabl.Task.Header.ShopMsg.DefStock + } else { + if taskMsg.Detail.Stock == 0 && golabl.Task.Header.TaskType == 8 { + return taskMsg, fmt.Errorf("库存不能为0") + } + } + + //生成一个2秒的延迟 + url := "http://127.0.0.1:8095" + tool.HttpGetRequest(url) + + // 规格编号 + if taskMsg.Detail.SkuCode == "" { + taskMsg.Detail.SkuCode = goodsAdd.OutGoodsId + } + + //构建 sku信息 + skuThumbnail := oldCarouselUrlArray[0] + if golabl.Task.Header.ShopMsg.SkuWatermarkImgUrl != "" { + //获取水印图片 + skuWatermarkImgUrl, skuWatermarkImgErr := tool.GetSkuWatermarkImg() + if skuWatermarkImgErr != nil { + return taskMsg, fmt.Errorf("获取水印图片失败 %v", skuWatermarkImgErr) + } + //sku 打水印 + skuThumbnailArr := []string{skuThumbnail} + skuWatermarkFromURLExsBase64Arr, skuWatermarkFromURLExsErr := tool.AddWatermarkFromURLExs(skuThumbnailArr, skuWatermarkImgUrl, "1") + if skuWatermarkFromURLExsErr != nil { + return taskMsg, fmt.Errorf("sku图片打水印失败 %v", skuWatermarkFromURLExsErr) + } + //sku 图片上传到拼多多 + skuToPdd, uploadImageToPddErr := tool.UploadImageToPdd(skuWatermarkFromURLExsBase64Arr) + if uploadImageToPddErr != nil { + return taskMsg, fmt.Errorf("图片上传到拼多多失败 %v", uploadImageToPddErr) + } + //将上传的图片替换到商品轮播图中 + skuThumbnail = skuToPdd[0] + } + + //构建规格名称 + var specChildName string + if golabl.Task.Header.ShopMsg.SpecCompose == "0" { + specChildName = golabl.Task.Header.ShopMsg.SpecChildName + } else if golabl.Task.Header.ShopMsg.SpecCompose == "1" { + // 前缀+isbn+后缀 + specChildName = golabl.Task.Header.ShopMsg.SpecPrefix + taskMsg.BookInfo.Isbn + golabl.Task.Header.ShopMsg.SpecSuffix + } else if golabl.Task.Header.ShopMsg.SpecCompose == "2" { + // 前缀+书名+后缀 + specChildName = golabl.Task.Header.ShopMsg.SpecPrefix + taskMsg.BookInfo.BookName + golabl.Task.Header.ShopMsg.SpecSuffix + } else if golabl.Task.Header.ShopMsg.SpecCompose == "3" { + // 前缀+货号+后缀 + specChildName = golabl.Task.Header.ShopMsg.SpecPrefix + strconv.FormatInt(taskMsg.Detail.SkuId, 10) + golabl.Task.Header.ShopMsg.SpecSuffix + } + // 如果规格名称超过30个字符,截取前30个字符 + if tool.StringLength(specChildName) > 30 { + specChildName = tool.SubstringByWidth(specChildName, 30) + } + + // 根据配置重新构建skuCode + if golabl.Task.Header.ShopMsg.SpecCodeCompose == "1" { + taskMsg.Detail.SkuCode = taskMsg.BookInfo.Isbn + } + + //构建 sku信息 + sku, err := buildSkuList(price, skuThumbnail, int64(taskMsg.Detail.Stock), taskMsg.Detail.SkuCode, specChildName, taskMsg.Detail.IsOnsale) + if err != nil { + return taskMsg, err + } + goodsAdd.SkuList = []planBTypePinduoduo.Sku{sku} + + // *********************构建参数 结束******************************** // + + // 发送请求 + goodsAddRet, _, err := addGoods(logUuid, goodsAdd) + if err != nil { + return taskMsg, fmt.Errorf("商品提交 %v", err) + } + + // 获取商品提交的商品详情 + goodsCommitDetail, _, getGoodsCommitDetailErr := getGoodsCommitDetail(goodsAddRet.Response.GoodsCommitID, goodsAddRet.Response.GoodsID) + if getGoodsCommitDetailErr != nil { + return taskMsg, fmt.Errorf("获取商品提交的商品详情失败 %w", getGoodsCommitDetailErr) + } + + //拼接接口调用成功的返回数据 + if len(goodsCommitDetail.GoodsCommitDetailResponse.SkuList) > 0 { + //taskMsg.Detail.SkuCode = goodsCommitDetail.GoodsCommitDetailResponse.SkuList[0].OutSkuSn + taskMsg.Detail.SkuId = goodsCommitDetail.GoodsCommitDetailResponse.SkuList[0].SkuID + } + taskMsg.Detail.GoodsId = goodsAddRet.Response.GoodsID + taskMsg.Detail.ReturnId = goodsAddRet.Response.GoodsCommitID + taskMsg.Detail.OutGoodsId = goodsAdd.OutGoodsId + taskMsg.Detail.Img = goodsAdd.CarouselGallery[0] + return taskMsg, nil +} diff --git a/planB/dispatcher/taobao/taobao.go b/planB/dispatcher/taobao/taobao.go new file mode 100644 index 0000000..35d222e --- /dev/null +++ b/planB/dispatcher/taobao/taobao.go @@ -0,0 +1,1042 @@ +package taobao + +import ( + "bytes" + "crypto/hmac" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "os" + "planA/planB/initialization/golabl" + "planA/planB/modules/logs" + "planA/planB/service" + "planA/planB/tool" + planAType "planA/type" + "sort" + "strconv" + "strings" + "time" +) + +// Taobao 淘宝平台结构体 +type Taobao struct { + client *http.Client +} + +// NewTaobao 创建淘宝平台 +// @return *Taobao +func NewTaobao() *Taobao { + return &Taobao{ + client: &http.Client{ + Timeout: 20 * time.Second, + }, + } +} + +// AddGoodsTask 添加商品 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return error 错误 +func (t *Taobao) AddGoodsTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + taskMsg, publishGoodsErr := t.publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, publishGoodsErr) + } + return tool.ReturnSuccess(taskMsg) +} + +// GetGoodsTask 获取商品 +// @return string body 信息 +// @return error 错误 +func (t *Taobao) GetGoodsTask() (string, error) { + // 生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + + const pageSize = 200 + const maxPage = 50 + + var totalFetched int + var duplicateCount int + var uniqueCount int + + // 第一阶段:只拉取任务数据,更新总数,不写入wait + firstTimeGoodsErr := t.phaseOneGoodsOnlyCount(pageSize, maxPage) + if firstTimeGoodsErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, firstTimeGoodsErr) + } + + // 查询body_wait是否存在,确定第二阶段的开始时间 + exist, isTaskBodyWaitExistErr := service.IsTaskBodyWaitExist() + if isTaskBodyWaitExistErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, isTaskBodyWaitExistErr) + } + + var startPage int + if exist { + // 获取body_wait数量,计算应该从第几页开始 + bodyWaitCount, getCountErr := service.GetTaskBodyWaitCount() + if getCountErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, getCountErr) + } + startPage = int(bodyWaitCount/pageSize) + 1 + fmt.Printf("body_wait已存在 %d 条数据,从第 %d 页开始拉取\n", bodyWaitCount, startPage) + } else { + startPage = 1 + } + + // 第二阶段:获取商品(写入wait) + phaseTwoGoodsErr := t.phaseTwoGoods(startPage, maxPage, pageSize, &totalFetched) + if phaseTwoGoodsErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, phaseTwoGoodsErr) + } + + // 更新状态为推送中 + updateTaskStatusErr := service.UpdateTaskStatus(planAType.TaskStatusPushTaskStatus) + if updateTaskStatusErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, updateTaskStatusErr) + } + + // 重新设置任务进度 + if updateTaskHeaderErr := service.SetTaskCount(strconv.FormatInt(golabl.Task.Footer.TaskCountTrue, 10)); updateTaskHeaderErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, updateTaskHeaderErr) + } + + // 去重复与保存 + deduplicateToBodyOverErr := t.deduplicateToBodyOver(&duplicateCount, &uniqueCount) + if deduplicateToBodyOverErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, deduplicateToBodyOverErr) + } + + // 输出统计信息 + statsLogMsg := fmt.Sprintf(` +════════════════════════════════════════════════════════════════ +【淘宝店铺拉取】 +请求ID:%s +时间: %s +店铺ID:%v +店铺名称:%v +总共获取商品数(含重复): %d +不重复商品数: %d +重复商品数: %d +重复率: %.2f%% +════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + golabl.Task.TaskId, + golabl.Task.Header.ShopName, + totalFetched, + uniqueCount, + duplicateCount, + float64(duplicateCount)/float64(totalFetched)*100) + fmt.Println(statsLogMsg) + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, statsLogMsg) + + return tool.ReturnSuccess(planAType.TaskBody{}) +} + +// OperationGoodsTask 操作商品 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return error 错误 +func (t *Taobao) OperationGoodsTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + + //暂停 2 秒 + time.Sleep(2 * time.Second) + + switch taskMsg.Detail.Status { + case 1: + return t.upOne(logUuid, taskMsg) //上架 + case 2: + return t.downOne(logUuid, taskMsg) //下架 + case 4: + return t.updateStock(logUuid, taskMsg) //修改库存 + case 5: + return t.updatePrice(logUuid, taskMsg) //修改价格 + case 6: + //发布商品 + taskMsg, publishGoodsErr := t.publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, publishGoodsErr) + } + return tool.ReturnSuccess(taskMsg) + case 7: + //下架 + _, downShelfErr := t.downOne(logUuid, taskMsg) + if downShelfErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, downShelfErr) + } + //发布商品 + taskMsg, publishGoodsErr := t.publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, publishGoodsErr) + } + return tool.ReturnSuccess(taskMsg) + default: + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("未知操作类型 %v", taskMsg.Detail.Status)) + } +} + +// IncStockTask 增量库存 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return error 错误 +func (t *Taobao) IncStockTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + + // 获取商品id + getGoodsByShopIdAndIsbn, GetGoodsByShopIdAndIsbnErr := tool.GetGoodsByShopIdAndIsbn(golabl.Task.Header.ShopId, taskMsg.BookInfo.Isbn) + if GetGoodsByShopIdAndIsbnErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, GetGoodsByShopIdAndIsbnErr) + } + if getGoodsByShopIdAndIsbn.Code != "200" { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("ERP未找到商品")) + } + if len(getGoodsByShopIdAndIsbn.Data) == 0 { + //新发布 + task, addGoodsTaskErr := t.publishGoods(logUuid, taskMsg) + if addGoodsTaskErr != nil { + return "", addGoodsTaskErr + } + return tool.ReturnSuccess(task) + } else { + // 将 getGoodsByShopIdAndIsbn.Data[0].TrilateralId 转为 int64 + trilateralId, trilateralIdParseIntErr := strconv.ParseInt(getGoodsByShopIdAndIsbn.Data[0].TrilateralId, 10, 64) + if trilateralIdParseIntErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, trilateralIdParseIntErr) + } + + // 将 getGoodsByShopIdAndIsbn.Data[0].Stock 转为 int64 + stock, stockParseIntErr := strconv.ParseInt(getGoodsByShopIdAndIsbn.Data[0].Stock, 10, 64) + if stockParseIntErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, stockParseIntErr) + } + + //增量修改库存 + taskMsg.Detail.GoodsId = trilateralId + taskMsg.Detail.Stock = taskMsg.Detail.Stock + int32(stock) + return t.updateStock(logUuid, taskMsg) + } +} + +func (t *Taobao) SetGoodsTask() string { + return "" +} + +// *******************************私有方法************************************ // + +// publishGoods 发布商品核心逻辑 +func (t *Taobao) publishGoods(logUuid string, taskMsg planAType.TaskBody) (planAType.TaskBody, error) { + // 价格不能小于0 + if taskMsg.Detail.Price <= 0 { + return taskMsg, fmt.Errorf("价格不能小于等于0") + } + + //获取出版社信息并解析 + if getPublishingErr := service.GetPublishingVid(&taskMsg); getPublishingErr != nil { + return taskMsg, fmt.Errorf("获取出版社信息失败-原因来自:%v", getPublishingErr) + } + + //违规词处理 + if golabl.Config.Server.Filter == 1 { + if taskMsgErr := tool.FilterWord(&taskMsg); taskMsgErr != nil { + return taskMsg, taskMsgErr + } + } + + //价格 + 运费 + if golabl.Task.Header.PriceType != "0" { + taskMsg.Detail.Price = taskMsg.Detail.Price + taskMsg.Detail.ShippingCost + } + + // 价格处理 + price := tool.BuildPrice(golabl.Task.Header.PriceMod, taskMsg.Detail.Price) + if price == 0 { + return taskMsg, fmt.Errorf("不在价格区间内 isbn %v 原始价格 %v 当前价格 %v 价格模版 %v", taskMsg.BookInfo.Isbn, taskMsg.Detail.Price, price, golabl.Task.Header.PriceMod) + } + taskMsg.Detail.Price = price + + //构建商品名称 + goodsName := tool.BuildGoodsName( + golabl.Task.Header.ShopMsg.GoodsNamePrefix, + golabl.Task.Header.ShopMsg.GoodsNameSuffix, + golabl.Task.Header.ShopMsg.TitleConsistOf, + golabl.Task.Header.ShopMsg.SpaceCharacter, + taskMsg.BookInfo) + taskMsg.Detail.GoodsName = goodsName + + // 检查轮播图 + if len(taskMsg.BookInfo.ImageObject.CarouselUrlArray) == 0 { + setNoImgCountErr := service.SetNoImgCount(taskMsg.BookInfo.Isbn) + if setNoImgCountErr != nil { + return taskMsg, fmt.Errorf("无图片信息isbn计次错误 isbn %v %v", taskMsg.BookInfo.Isbn, setNoImgCountErr.Error()) + } + return taskMsg, fmt.Errorf("缺少轮播图") + } + + oldCarouselUrlArray := append([]string{}, taskMsg.BookInfo.ImageObject.CarouselUrlArray...) + + // 存在水印图片,则打水印 + var mainImgLocalPath string + if golabl.Task.Header.ShopMsg.WatermarkImgUrl != "" { + watermarkImgUrl, watermarkImgErr := tool.GetWatermarkImg() + if watermarkImgErr != nil { + return taskMsg, fmt.Errorf("获取水印图片失败 %v", watermarkImgErr) + } + watermarkFromURLExsBase64Arr, watermarkFromURLExsErr := tool.AddWatermarkFromURLExs(taskMsg.BookInfo.ImageObject.CarouselUrlArray, watermarkImgUrl, golabl.Task.Header.ShopMsg.WatermarkPosition) + if watermarkFromURLExsErr != nil { + return taskMsg, fmt.Errorf("图片打水印失败 %v", watermarkFromURLExsErr) + } + // 第一张作为主图,保存到本地 + if len(watermarkFromURLExsBase64Arr) > 0 { + // 从 ImageResult 中提取 base64 数据 + base64Data := watermarkFromURLExsBase64Arr[0].Data + // 保存到本地 + savedPath, saveErr := tool.SaveBase64ImageByDate(base64Data, golabl.Config.TaobaoConfig.LocalImgDir) + if saveErr != nil { + return taskMsg, fmt.Errorf("保存水印图片到本地失败 %v", saveErr) + } + mainImgLocalPath = savedPath + } + } else { + // 没有水印,直接下载原图到本地 + savedPath, saveErr := tool.SaveBase64ImageByDate(oldCarouselUrlArray[0], golabl.Config.TaobaoConfig.LocalImgDir) + if saveErr != nil { + return taskMsg, fmt.Errorf("保存原图到本地失败 %v", saveErr) + } + mainImgLocalPath = savedPath + } + + // 上传到淘宝图片空间 + mainImg, uploadErr := t.uploadImageToTaobao(mainImgLocalPath, taskMsg.BookInfo.BookName) + if uploadErr != nil { + return taskMsg, fmt.Errorf("上传图片到淘宝图片空间失败 %v", uploadErr) + } + + // 库存处理 + if taskMsg.Detail.Stock == 0 && (golabl.Task.Header.TaskType == 1 || golabl.Task.Header.TaskType == 2 || golabl.Task.Header.TaskType == 6) { + taskMsg.Detail.Stock = golabl.Task.Header.ShopMsg.DefStock + } + + // 调用淘宝API发布商品 + ret, err := t.tushuAdd(goodsName, int(taskMsg.Detail.Stock), float64(price)/100, mainImg, taskMsg) + if err != nil { + return taskMsg, fmt.Errorf("淘宝商品发布失败: %v", err) + } + + // 解析返回结果,获取商品ID + var result map[string]interface{} + if unmarshalErr := json.Unmarshal(ret, &result); unmarshalErr != nil { + return taskMsg, fmt.Errorf("解析淘宝返回结果失败: %v", unmarshalErr) + } + + // 提取商品ID(根据实际返回结构调整) + if data, ok := result["data"].(map[string]interface{}); ok { + if numIid, ok := data["num_iid"].(string); ok { + taskMsg.Detail.GoodsId, _ = strconv.ParseInt(numIid, 10, 64) + } + } + + taskMsg.Detail.OutGoodsId = taskMsg.BookInfo.Isbn + taskMsg.Detail.Img = mainImg + + return taskMsg, nil +} + +// uploadImageToTaobao 上传图片到淘宝图片空间 +// 参数: +// - localPath: 本地图片路径 +// - title: 商品标题(用于生成文件名) +// +// 返回: +// - 淘宝图片空间URL +// - 错误信息 +func (t *Taobao) uploadImageToTaobao(localPath string, title string) (string, error) { + taobaoConfig := golabl.Config.TaobaoConfig + + // 生成安全文件名 + imgTitle := sanitizeFileName(title) + if imgTitle == "" { + imgTitle = fmt.Sprintf("img_%d", time.Now().Unix()) + } + + // 打开本地文件 + file, err := os.Open(localPath) + if err != nil { + return "", fmt.Errorf("打开本地图片文件失败: %w", err) + } + defer file.Close() + + // 构造 multipart/form-data 请求 + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", imgTitle+".jpg") + if err != nil { + return "", fmt.Errorf("创建 multipart 文件部分失败: %w", err) + } + if _, err := io.Copy(part, file); err != nil { + return "", fmt.Errorf("复制文件内容失败: %w", err) + } + writer.WriteField("token", taobaoConfig.Token) + writer.Close() + + uploadURL := taobaoConfig.BaseURL + "/GetJsNamesForUrl.do?loadImg=" + req, err := http.NewRequest("POST", uploadURL, body) + if err != nil { + return "", fmt.Errorf("创建上传请求失败: %w", err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0") + req.Header.Set("Origin", "https://fxzsweb.yulinkai.com") + req.Header.Set("Referer", "https://fxzsweb.yulinkai.com/") + + client := &http.Client{Timeout: 50 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("上传图片请求失败: %w", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + + // 解析 JSON 响应 + var result map[string]interface{} + if err := json.Unmarshal(respBody, &result); err != nil { + return "", fmt.Errorf("解析上传响应 JSON 失败: %w", err) + } + + data, ok := result["data"].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("上传响应缺少 data 字段: %s", string(respBody)) + } + + tbImagePath, ok := data["path"].(string) + if !ok || tbImagePath == "" { + return "", fmt.Errorf("上传响应缺少 path 字段: %s", string(respBody)) + } + + return tbImagePath, nil +} + +// sanitizeFileName 清理文件名,移除非法字符 +func sanitizeFileName(name string) string { + // 移除或替换 Windows 和 Unix 不允许的字符 + invalidChars := []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|"} + result := name + for _, char := range invalidChars { + result = strings.ReplaceAll(result, char, "_") + } + // 限制长度 + if len(result) > 50 { + result = result[:50] + } + return result +} + +// tushuAdd 调用淘宝API发布图书商品 +func (t *Taobao) tushuAdd(title string, num int, price float64, picUrl string, taskMsg planAType.TaskBody) ([]byte, error) { + timestamp := time.Now().UnixMilli() + + // 解析店铺Token获取配置信息 + taobaoConfig := golabl.Config.TaobaoConfig + + appInfo := map[string]interface{}{ + "app_key": taobaoConfig.AppKey, + "app_secret": taobaoConfig.AppSecret, + "format": "json", + "ipaddress": "127.0.0.1", + "method": "ylk.base.add.book", + "partner_id": "sdk-1.0", + "sign_method": "hmac", + "timestamp": timestamp, + "token": golabl.Task.Header.ShopMsg.Token, + "v": "2.0", + } + + // bookInfo(商品信息) + bookInfo := map[string]interface{}{ + "title": title, + "outerId": taskMsg.BookInfo.Isbn, + "price": price, + "cid": 50010485, // 图书类目 + "postageId": golabl.Task.Header.ShopMsg.CostTemplateId, + "num": num, + "desc": title, + "sellerCids": "", + "picUrl": picUrl, + "approveStatus": "instock", // 仓库中(不立即上架) + "bookName": taskMsg.BookInfo.BookName, + "deliveryTimeType": 0, + "newFlag": 1, + } + + bookInfoJSON, _ := json.Marshal(bookInfo) + + formData := map[string]interface{}{ + "user_id": taobaoConfig.UserID, + "company_id": taobaoConfig.CompanyID, + "author_shop_name": golabl.Task.Header.ShopName, + "book_info": string(bookInfoJSON), + } + + sign := t.sign(appInfo, formData) + + // 拼接完整 URL + URL := fmt.Sprintf("%s/router/ylk/base/add/book?"+ + "app_key=%d&format=json&ipaddress=127.0.0.1&partner_id=sdk-1.0&sign_method=hmac×tamp=%d&v=2.0&"+ + "token=%s&method=ylk.base.add.book&sign=%s&ati=%s", + taobaoConfig.BaseURL, taobaoConfig.AppKey, timestamp, golabl.Task.Header.ShopMsg.Token, sign, taobaoConfig.Ati) + + // 构造 form data(URL 编码) + form := url.Values{} + form.Set("user_id", taobaoConfig.UserID) + form.Set("company_id", taobaoConfig.CompanyID) + form.Set("author_shop_name", golabl.Task.Header.ShopName) + form.Set("book_info", url.QueryEscape(string(bookInfoJSON))) + + resp, err := t.client.PostForm(URL, form) + if err != nil { + return nil, fmt.Errorf("发布商品请求失败: %w", err) + } + defer resp.Body.Close() + + body, readAllErr := io.ReadAll(resp.Body) + if readAllErr != nil { + return nil, fmt.Errorf("读取响应失败: %w", readAllErr) + } + fmt.Println("----------------------------------------------body---------------------------------------------") + fmt.Println(string(body)) + fmt.Println("----------------------------------------------body---------------------------------------------") + + // 发布后等待 5 秒(平台限流) + time.Sleep(5 * time.Second) + + return nil, nil +} + +// sign 计算 API 请求签名(HMAC-MD5,结果为大写十六进制字符串) +func (t *Taobao) sign(appInfo, formData map[string]interface{}) string { + data := make(map[string]interface{}) + for k, v := range appInfo { + data[k] = v + } + for k, v := range formData { + data[k] = v + } + + keys := make([]string, 0, len(data)) + for k := range data { + keys = append(keys, k) + } + sort.Strings(keys) + + var sb strings.Builder + for _, k := range keys { + sb.WriteString(k) + sb.WriteString(fmt.Sprintf("%v", data[k])) + } + concatStr := sb.String() + + taobaoConfig := golabl.Config.TaobaoConfig + mac := hmac.New(md5.New, []byte(taobaoConfig.AppSecret)) + mac.Write([]byte(concatStr)) + return strings.ToUpper(hex.EncodeToString(mac.Sum(nil))) +} + +// phaseOneGoodsOnlyCount 第一阶段只获取总数 +func (t *Taobao) phaseOneGoodsOnlyCount(pageSize int, maxPage int) error { + for page := 1; page <= maxPage; page++ { + items, err := t.tushuGetList(page, pageSize) + if err != nil { + return fmt.Errorf("获取商品列表失败,页码: %d, 错误: %v", page, err) + } + if items == nil || len(items) == 0 { + break + } + + // 第一页更新总数 + if page == 1 { + // 淘宝API不直接返回总数,这里用估算或从header中获取 + fmt.Printf("第一阶段完成,当前页获取 %d 条商品\n", len(items)) + } + } + return nil +} + +// phaseTwoGoods 第二阶段拉取商品 +func (t *Taobao) phaseTwoGoods(startPage int, maxPage int, pageSize int, totalFetched *int) error { + for page := startPage; page <= maxPage; page++ { + items, err := t.tushuGetList(page, pageSize) + if err != nil { + return fmt.Errorf("获取商品列表失败,页码: %d, 错误: %v", page, err) + } + if items == nil || len(items) == 0 { + fmt.Printf("第 %d 页无数据,结束拉取\n", page) + break + } + + // 处理商品数据 + for _, item := range items { + *totalFetched++ + + itemMap, ok := item["item"].(map[string]interface{}) + if !ok { + continue + } + + numIid, _ := itemMap["numIid"].(string) + title, _ := itemMap["title"].(string) + outerId, _ := itemMap["outerId"].(string) + priceStr, _ := itemMap["price"].(string) + num, _ := itemMap["num"].(float64) + + price, _ := strconv.ParseFloat(priceStr, 64) + + // 转换为TaskBody + bodyWait := planAType.TaskBody{ + BookInfo: planAType.BookInfo{ + Isbn: outerId, + BookName: title, + Price: int64(price * 100), + }, + Detail: planAType.TaskDetail{ + Status: 1, + GoodsId: 0, + Stock: int32(num), + Error: numIid, // 临时存储numIid + }, + } + + // 转为JSON + bodyWaitJson, jsonMarshalErr := json.Marshal(bodyWait) + if jsonMarshalErr != nil { + return fmt.Errorf("将bodyWait转为json失败: %v\n", jsonMarshalErr) + } + + // 写入 body_wait + addTaskToBodyWaitErr := service.AddTaskToBodyWait(string(bodyWaitJson)) + if addTaskToBodyWaitErr != nil { + return addTaskToBodyWaitErr + } + } + + // 更新进度 + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + con := int64(len(items)) + if con >= golabl.Task.Footer.TaskCountTrue { + con = golabl.Task.Footer.TaskCountTrue - con + } + if updateTaskProgressErr := tool.UpdateTaskProgress(con); updateTaskProgressErr != nil { + return updateTaskProgressErr + } + + fmt.Printf("第二阶段 - 页码: %d, 本页获取: %d 条,累计: %d\n", page, len(items), *totalFetched) + time.Sleep(200 * time.Millisecond) + } + return nil +} + +// tushuGetList 获取淘宝商品列表 +func (t *Taobao) tushuGetList(page int, pageSize int) ([]map[string]interface{}, error) { + //timestamp := time.Now().UnixMilli() + //taobaoConfig := golabl.Config.TaobaoConfig + // + //appInfo := map[string]interface{}{ + // "app_key": taobaoConfig.AppKey, + // "app_secret": taobaoConfig.AppSecret, + // "format": "json", + // "ipaddress": "127.0.0.1", + // "method": "ylk.item.tb.api.list", + // "partner_id": "sdk-1.0", + // "sign_method": "hmac", + // "timestamp": timestamp, + // "token": golabl.Task.Header.ShopMsg.Token, + // "v": "2.0", + //} + // + //formData := map[string]interface{}{ + // "user_id": taobaoConfig.UserId, + // "company_id": taobaoConfig.CompanyId, + // "approve_status": "onsale", + // "shop_name": golabl.Task.Header.ShopName, + // "diagnose_flag": 1, + // "page_no": page, + // "page_size": pageSize, + // "order_by": "modified:asc", + //} + // + //sign := t.sign(appInfo, formData) + // + //URL := fmt.Sprintf("%s/router/ylk/item/tb/api/list?"+ + // "app_key=%d&format=json&ipaddress=127.0.0.1&partner_id=sdk-1.0&sign_method=hmac×tamp=%d&v=2.0&"+ + // "token=%s&method=ylk.item.tb.api.list&sign=%s&ati=%s", + // taobaoConfig.BaseUrl, taobaoConfig.AppKey, timestamp, golabl.Task.Header.ShopMsg.Token, sign, taobaoConfig.Ati) + // + //form := url.Values{} + //for k, v := range formData { + // form.Set(k, fmt.Sprintf("%v", v)) + //} + // + //resp, err := t.client.PostForm(URL, form) + //if err != nil { + // return nil, fmt.Errorf("获取商品列表请求失败: %w", err) + //} + //defer resp.Body.Close() + // + //body, _ := io.ReadAll(resp.Body) + // + //var result map[string]interface{} + //if err := json.Unmarshal(body, &result); err != nil { + // return nil, fmt.Errorf("解析响应 JSON 失败: %w", err) + //} + // + //items, ok := result["items"].([]interface{}) + //if !ok || items == nil { + // return nil, nil + //} + // + //resultItems := make([]map[string]interface{}, 0, len(items)) + //for _, itm := range items { + // if m, ok := itm.(map[string]interface{}); ok { + // resultItems = append(resultItems, m) + // } + //} + + return nil, nil +} + +// deduplicateToBodyOver 去重并写入body_over +func (t *Taobao) deduplicateToBodyOver(duplicateCount *int, uniqueCount *int) error { + page := 1 + pageSize := 1000 + + processedGoodsIds := make(map[string]bool) + + deleteTaskBodyOverErr := service.DeleteTaskBodyOver() + if deleteTaskBodyOverErr != nil { + return deleteTaskBodyOverErr + } + deleteTaskBodyBackupErr := service.DeleteTaskBodyBackup() + if deleteTaskBodyBackupErr != nil { + return deleteTaskBodyBackupErr + } + + bodyWaitCount, getTaskBodyWaitCountErr := service.GetTaskBodyWaitCount() + if getTaskBodyWaitCountErr != nil { + return getTaskBodyWaitCountErr + } + pageTotal := (bodyWaitCount + int64(pageSize) - 1) / int64(pageSize) + + for { + list, getTaskBodyOverListErr := service.GetTaskBodyWaitList(page, pageSize) + if getTaskBodyOverListErr != nil { + return getTaskBodyOverListErr + } + if len(list) <= 0 { + break + } + + for _, v := range list { + goods := planAType.TaskBody{} + jsonUnmarshalErr := json.Unmarshal([]byte(v), &goods) + if jsonUnmarshalErr != nil { + return fmt.Errorf("将json转为结构体失败: %v\n", jsonUnmarshalErr) + } + + // 使用outerId(ISBN)作为去重key + goodsKey := goods.BookInfo.Isbn + if goodsKey == "" { + goodsKey = goods.Detail.Error // 使用numIid作为备选 + } + + if !processedGoodsIds[goodsKey] { + processedGoodsIds[goodsKey] = true + *uniqueCount++ + + goods.Detail.Status = 1 + addTaskToBodyOverErr := service.AddTaskToBodyOver(goods, []string{"body_over", "body_backup"}) + if addTaskToBodyOverErr != nil { + return addTaskToBodyOverErr + } + } else { + *duplicateCount++ + } + } + + fmt.Printf("去重处理中 - 当前页 %v 总页数 %v\n", page, pageTotal) + page++ + + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + con := int64(len(list)) + if con >= golabl.Task.Footer.TaskCountTrue { + con = golabl.Task.Footer.TaskCountTrue - con + } + if updateTaskProgressErr := tool.UpdateTaskProgress(con); updateTaskProgressErr != nil { + return updateTaskProgressErr + } + + time.Sleep(1 * time.Second) + } + + deleteTaskBodyWaitErr := service.DeleteTaskBodyWait() + if deleteTaskBodyWaitErr != nil { + return deleteTaskBodyWaitErr + } + return nil +} + +// upOne 上架商品 +func (t *Taobao) upOne(logUuid string, taskMsg planAType.TaskBody) (string, error) { + //if taskMsg.Detail.GoodsId == 0 { + // return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("淘宝商品 Id不能为空")) + //} + // + //timestamp := time.Now().UnixMilli() + //taobaoConfig := golabl.Config.TaobaoConfig + // + //appInfo := map[string]interface{}{ + // "app_key": taobaoConfig.AppKey, + // "app_secret": taobaoConfig.AppSecret, + // "format": "json", + // "ipaddress": "127.0.0.1", + // "method": "ylk.item.listing", + // "partner_id": "sdk-1.0", + // "sign_method": "hmac", + // "timestamp": timestamp, + // "token": golabl.Task.Header.ShopMsg.Token, + // "v": "2.0", + //} + // + //uid := strconv.FormatInt(time.Now().UnixMilli(), 10) + //formData := map[string]interface{}{ + // "user_id": uid, + // "company_id": uid, + // "num_iid": strconv.FormatInt(taskMsg.Detail.GoodsId, 10), + // "nick": golabl.Task.Header.ShopName, + // "num": "2", + //} + // + //sign := t.sign(appInfo, formData) + // + //URL := fmt.Sprintf("%s/router/ylk/item/listing?"+ + // "app_key=%d&format=json&ipaddress=127.0.0.1&partner_id=sdk-1.0&sign_method=hmac×tamp=%d&v=2.0&"+ + // "token=%s&method=ylk.item.listing&sign=%s&ati=%s", + // taobaoConfig.BaseUrl, taobaoConfig.AppKey, timestamp, golabl.Task.Header.ShopMsg.Token, sign, taobaoConfig.Ati) + // + //form := url.Values{} + //for k, v := range formData { + // form.Set(k, fmt.Sprintf("%v", v)) + //} + // + //resp, err := t.client.PostForm(URL, form) + //if err != nil { + // return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("上架请求失败: %v", err)) + //} + //defer resp.Body.Close() + + return tool.ReturnSuccess(taskMsg) +} + +// downOne 下架商品 +func (t *Taobao) downOne(logUuid string, taskMsg planAType.TaskBody) (string, error) { + //if taskMsg.Detail.GoodsId == 0 { + // return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("淘宝商品 Id不能为空")) + //} + // + //timestamp := time.Now().UnixMilli() + //taobaoConfig := golabl.Config.TaobaoConfig + // + //appInfo := map[string]interface{}{ + // "app_key": taobaoConfig.AppKey, + // "app_secret": taobaoConfig.AppSecret, + // "format": "json", + // "ipaddress": "127.0.0.1", + // "method": "ylk.item.delisting", + // "partner_id": "sdk-1.0", + // "sign_method": "hmac", + // "timestamp": timestamp, + // "token": golabl.Task.Header.ShopMsg.Token, + // "v": "2.0", + //} + // + //uid := strconv.FormatInt(time.Now().UnixMilli(), 10) + //formData := map[string]interface{}{ + // "user_id": uid, + // "company_id": uid, + // "num_iid": strconv.FormatInt(taskMsg.Detail.GoodsId, 10), + // "nick": golabl.Task.Header.ShopName, + //} + // + //sign := t.sign(appInfo, formData) + // + //URL := fmt.Sprintf("%s/router/ylk/item/delisting?"+ + // "app_key=%d&format=json&ipaddress=127.0.0.1&partner_id=sdk-1.0&sign_method=hmac×tamp=%d&v=2.0&"+ + // "token=%s&method=ylk.item.delisting&sign=%s&ati=%s", + // taobaoConfig.BaseUrl, taobaoConfig.AppKey, timestamp, golabl.Task.Header.ShopMsg.Token, sign, taobaoConfig.Ati) + // + //form := url.Values{} + //for k, v := range formData { + // form.Set(k, fmt.Sprintf("%v", v)) + //} + // + //resp, err := t.client.PostForm(URL, form) + //if err != nil { + // return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("下架请求失败: %v", err)) + //} + //defer resp.Body.Close() + + return tool.ReturnSuccess(taskMsg) +} + +// updateStock 修改库存 +func (t *Taobao) updateStock(logUuid string, taskMsg planAType.TaskBody) (string, error) { + //if taskMsg.Detail.GoodsId == 0 { + // return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("淘宝商品 Id不能为空")) + //} + // + //timestamp := time.Now().UnixMilli() + //taobaoConfig := golabl.Config.TaobaoConfig + // + //appInfo := map[string]interface{}{ + // "app_key": taobaoConfig.AppKey, + // "app_secret": taobaoConfig.AppSecret, + // "format": "json", + // "ipaddress": "127.0.0.1", + // "method": "ylk.item.operate.update", + // "partner_id": "sdk-1.0", + // "sign_method": "hmac", + // "timestamp": timestamp, + // "token": golabl.Task.Header.ShopMsg.Token, + // "v": "2.0", + //} + // + //itemUpdateObj := map[string]interface{}{ + // "num": taskMsg.Detail.Stock, + // "nick": golabl.Task.Header.ShopName, + // "outerId": taskMsg.BookInfo.Isbn, + // "listSku": []interface{}{}, + //} + // + //itemUpdateJSON, _ := json.Marshal(itemUpdateObj) + //itemUpdateEncoded := url.QueryEscape(string(itemUpdateJSON)) + // + //formData := map[string]interface{}{ + // "company_id": taobaoConfig.CompanyId, + // "itemUpdate": string(itemUpdateJSON), + // "num_iid": strconv.FormatInt(taskMsg.Detail.GoodsId, 10), + // "user_id": taobaoConfig.UserId, + //} + // + //sign := t.sign(appInfo, formData) + // + //URL := fmt.Sprintf("%s/router/ylk/item/operate/update?"+ + // "app_key=%d&format=json&ipaddress=127.0.0.1&partner_id=sdk-1.0&sign_method=hmac×tamp=%d&v=2.0&"+ + // "token=%s&method=ylk.item.operate.update&sign=%s&ati=%s", + // taobaoConfig.BaseUrl, taobaoConfig.AppKey, timestamp, golabl.Task.Header.ShopMsg.Token, sign, taobaoConfig.Ati) + // + //form := url.Values{} + //form.Set("company_id", taobaoConfig.CompanyId) + //form.Set("itemUpdate", itemUpdateEncoded) + //form.Set("num_iid", strconv.FormatInt(taskMsg.Detail.GoodsId, 10)) + //form.Set("user_id", taobaoConfig.UserId) + // + //resp, err := t.client.PostForm(URL, form) + //if err != nil { + // return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("修改库存请求失败: %v", err)) + //} + //defer resp.Body.Close() + + return tool.ReturnSuccess(taskMsg) +} + +// updatePrice 修改价格 +func (t *Taobao) updatePrice(logUuid string, taskMsg planAType.TaskBody) (string, error) { + //if taskMsg.Detail.GoodsId == 0 { + // return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("淘宝商品 Id不能为空")) + //} + //if taskMsg.Detail.Price == 0 { + // return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("淘宝商品价格不能为0")) + //} + // + //timestamp := time.Now().UnixMilli() + //taobaoConfig := golabl.Config.TaobaoConfig + // + //appInfo := map[string]interface{}{ + // "app_key": taobaoConfig.AppKey, + // "app_secret": taobaoConfig.AppSecret, + // "format": "json", + // "ipaddress": "127.0.0.1", + // "method": "ylk.item.operate.update", + // "partner_id": "sdk-1.0", + // "sign_method": "hmac", + // "timestamp": timestamp, + // "token": golabl.Task.Header.ShopMsg.Token, + // "v": "2.0", + //} + // + //fen := tool.BuildGoodsPrice(taskMsg.Detail.Price) + //yuan := tool.FenToYuan(fen) + // + //itemUpdateObj := map[string]interface{}{ + // "price": yuan, + // "nick": golabl.Task.Header.ShopName, + // "outerId": taskMsg.BookInfo.Isbn, + // "listSku": []interface{}{}, + //} + // + //itemUpdateJSON, _ := json.Marshal(itemUpdateObj) + //itemUpdateEncoded := url.QueryEscape(string(itemUpdateJSON)) + // + //formData := map[string]interface{}{ + // "company_id": taobaoConfig.CompanyId, + // "itemUpdate": string(itemUpdateJSON), + // "num_iid": strconv.FormatInt(taskMsg.Detail.GoodsId, 10), + // "user_id": taobaoConfig.UserId, + //} + // + //sign := t.sign(appInfo, formData) + // + //URL := fmt.Sprintf("%s/router/ylk/item/operate/update?"+ + // "app_key=%d&format=json&ipaddress=127.0.0.1&partner_id=sdk-1.0&sign_method=hmac×tamp=%d&v=2.0&"+ + // "token=%s&method=ylk.item.operate.update&sign=%s&ati=%s", + // taobaoConfig.BaseUrl, taobaoConfig.AppKey, timestamp, golabl.Task.Header.ShopMsg.Token, sign, taobaoConfig.Ati) + // + //form := url.Values{} + //form.Set("company_id", taobaoConfig.CompanyId) + //form.Set("itemUpdate", itemUpdateEncoded) + //form.Set("num_iid", strconv.FormatInt(taskMsg.Detail.GoodsId, 10)) + //form.Set("user_id", taobaoConfig.UserId) + // + //resp, err := t.client.PostForm(URL, form) + //if err != nil { + // return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("修改价格请求失败: %v", err)) + //} + //defer resp.Body.Close() + + return tool.ReturnSuccess(taskMsg) +} diff --git a/planB/dispatcher/xianyu/xianyu.go b/planB/dispatcher/xianyu/xianyu.go new file mode 100644 index 0000000..6b99112 --- /dev/null +++ b/planB/dispatcher/xianyu/xianyu.go @@ -0,0 +1,1495 @@ +package xianyu + +import ( + "encoding/json" + "errors" + "fmt" + "planA/planB/initialization/golabl" + "planA/planB/modules/logs" + "planA/planB/service" + "planA/planB/tool" + planBType "planA/planB/type" + planBTypeXianyu "planA/planB/type/xianyu" + planAType "planA/type" + "strconv" + "strings" + "time" +) + +type XianYu struct { +} + +// NewXianYu 创建闲鱼平台 +func NewXianYu() *XianYu { + return &XianYu{} +} + +// AddGoodsTask 添加商品 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return error 错误 +func (xianYu *XianYu) AddGoodsTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + taskMsg, publishGoodsErr := publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, publishGoodsErr) + } + taskMsg.Detail.Error = "发布成功!" + return tool.ReturnSuccess(taskMsg) +} + +func (xianYu *XianYu) GetGoodsTask() (string, error) { + // 生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + + const pageSize = 100 + const maxPage = 100 + const maxRecordsPerRange = 10000 // 每个时间范围最多获取10000条 + var lastUpdateTime int64 = 0 + + // 统计变量 + totalFetched := 0 // 总共获取到的商品数(包括重复) + duplicateCount := 0 // 重复商品数量 + uniqueCount := 0 // 不重复商品数量 + + // 解析应用 id与应用秘钥 + var token planBTypeXianyu.Token + unmarshalErr := json.Unmarshal([]byte(golabl.Task.Header.ShopMsg.Token), &token) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, fmt.Errorf("解析应用id与应用秘钥失败: %v", unmarshalErr)) + } + + // 第一阶段:只拉取任务数据,更新总数,不写入wait + firstTimeGoodsErr := xianYu.phaseOneGoodsOnlyCount(token, pageSize, maxPage) + if firstTimeGoodsErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, firstTimeGoodsErr) + } + + // 查询body_wait是否存在,确定第二阶段的开始时间 + exist, isTaskBodyWaitExistErr := service.IsTaskBodyWaitExist() + if isTaskBodyWaitExistErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, isTaskBodyWaitExistErr) + } + + if exist { + // 获取最后一条数据的更新时间作为开始时间 + lastBodyWaitDataJson, getLastGoodsUpdateTimeErr := service.GetTaskBodyWaitLast() + if getLastGoodsUpdateTimeErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, getLastGoodsUpdateTimeErr) + } + // 解析 lastBodyWaitData 到结构体 + var lastBodyWaitData planAType.TaskBody + unmarshalErr := json.Unmarshal([]byte(lastBodyWaitDataJson), &lastBodyWaitData) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, unmarshalErr) + } + lastUpdateTime = lastBodyWaitData.BookInfo.Price - (86400 * 30) //wait中最后一条数据的更新时间-30天作为下次的开始时间 + // 将数据的更新时间给到 lastUpdateTime + fmt.Println("使用wait中最后一条数据的时间作为开始时间: ", lastUpdateTime) + } else { + // 如果没有wait数据,使用当前时间180天前的时间戳作为开始时间 + lastUpdateTime = time.Now().Unix() - 180*24*60*60 + fmt.Println("没有wait数据,使用180天前的时间作为开始时间: ", lastUpdateTime, time.Unix(lastUpdateTime, 0).Format("2006-01-02 15:04:05")) + } + + // 第二阶段:获取商品(写入wait) + phaseTwoGoodsErr := xianYu.phaseTwoGoods(token, pageSize, &totalFetched, &lastUpdateTime, maxRecordsPerRange) + if phaseTwoGoodsErr != nil { + fmt.Println(phaseTwoGoodsErr.Error()) + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, phaseTwoGoodsErr) + } + + // 更新状态为推送中 + updateTaskStatusErr := service.UpdateTaskStatus(planAType.TaskStatusPushTaskStatus) + if updateTaskStatusErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, updateTaskStatusErr) + } + + // 重新设置任务进度 + if updateTaskHeaderErr := service.SetTaskCount(strconv.FormatInt(golabl.Task.Footer.TaskCountTrue, 10)); updateTaskHeaderErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, updateTaskHeaderErr) + } + + // 去重复与保存 + deduplicateToBodyOverErr := xianYu.deduplicateToBodyOver(&duplicateCount, &uniqueCount) + if deduplicateToBodyOverErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, deduplicateToBodyOverErr) + } + + // 输出统计信息 + statsLogMsg := fmt.Sprintf(` + ════════════════════════════════════════════════════════════════ + 【闲鱼店铺拉取】 + 请求ID:%s + 时间: %s + 店铺ID:%v + 店铺名称:%v + 总共获取商品数(含重复): %d + 不重复商品数: %d + 重复商品数: %d + 重复率: %.2f%% + ════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + golabl.Task.TaskId, + golabl.Task.Header.ShopName, + totalFetched, + uniqueCount, + duplicateCount, + float64(duplicateCount)/float64(totalFetched)*100) + fmt.Println(statsLogMsg) + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, statsLogMsg) + + return tool.ReturnSuccess(planAType.TaskBody{}) +} + +// OperationGoodsTask 操作商品 +// @param taskMsg 任务内容 +// @return string body 信息 +// @return string error 错误 +func (xianYu *XianYu) OperationGoodsTask(taskMsg planAType.TaskBody) (string, error) { + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + //暂停 2 秒 + time.Sleep(2 * time.Second) + fmt.Println(taskMsg.Detail.Status) + switch taskMsg.Detail.Status { + case 1: + return executeGoodsLaunch(logUuid, taskMsg) //上架 {"book_info":{"isbn":"9787115600387"},"detail":{"goods_id":1562238986012229,"status":1}} + case 2: + return executeGoodsDownShelf(logUuid, taskMsg) // 下架 {"book_info":{"isbn":"9787115600387"},"detail":{"goods_id":1562238986012229,"status":2}} + case 4: + return executeGoodsUpdateStock(logUuid, taskMsg) //修改商品库存 {"book_info":{"isbn":"9787115600387"},"detail":{"goods_id":1562238986012229,"status":4,"stock":2}} + case 5: + return executeGoodsUpdatePrice(logUuid, taskMsg) //修改商品价格 {"book_info":{"isbn":"9787115600387"},"detail":{"goods_id":1562238986012229,"status":5,"price":5000}} + case 6: + //发布商品 + taskMsg, publishGoodsErr := publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return "", publishGoodsErr + } + return tool.ReturnSuccess(taskMsg) + case 7: + //下架 + _, setSaleStatusGoodsTaskErr := executeGoodsDownShelf(logUuid, taskMsg) + if setSaleStatusGoodsTaskErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, setSaleStatusGoodsTaskErr) + } + //发布商品 + taskMsg, publishGoodsErr := publishGoods(logUuid, taskMsg) + if publishGoodsErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, publishGoodsErr) + } + return tool.ReturnSuccess(taskMsg) + default: + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("未知操作类型")) + } +} + +// IncStockTask 增量库存b +// @param taskMsg 任务内容 +// @return string body 信息 +// @return string error 错误 +func (xianYu *XianYu) IncStockTask(taskMsg planAType.TaskBody) (string, error) { + + //生成唯一请求标识(用于出错精准查询日志) + logUuid, generateUUIDErr := tool.GenerateUUID() + if generateUUIDErr != nil { + return "", fmt.Errorf("生成唯一请求标识失败: %v", generateUUIDErr) + } + + // 获取商品id + getGoodsByShopIdAndIsbn, GetGoodsByShopIdAndIsbnErr := tool.GetGoodsByShopIdAndIsbn(golabl.Task.Header.ShopId, taskMsg.BookInfo.Isbn) + if GetGoodsByShopIdAndIsbnErr != nil { + return "", GetGoodsByShopIdAndIsbnErr + } + if getGoodsByShopIdAndIsbn.Code != "200" { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("请求ERP获取商品编码与skuid失败: %v", getGoodsByShopIdAndIsbn)) + } + if len(getGoodsByShopIdAndIsbn.Data) == 0 { + //新发布 + task, addGoodsTaskErr := xianYu.AddGoodsTask(taskMsg) + if addGoodsTaskErr != nil { + return "", addGoodsTaskErr + } + return task, nil + } else { + // 当前任务的价格 + taskPrice := taskMsg.Detail.Price // 单位:分 + + // 价格 + 运费(如果 PriceType != "0") + if golabl.Task.Header.PriceType != "0" { + taskPrice = taskPrice + taskMsg.Detail.ShippingCost + } + + // 价格模板计算 + taskPrice = tool.BuildPrice(golabl.Task.Header.PriceMod, taskPrice) + if taskPrice == 0 { + taskMsg.Detail.Error = "任务价格不在价格模板区间内!" + return tool.ReturnSuccess(taskMsg) + } + + // 1元 = 100分,价格相差1元以上即 >= 100分 + const priceDiffThreshold = 100 // 1元 + + // 收集所有匹配条件的商品(价格差<1元) + var matchedItems []struct { + TrilateralId string + SkuId string + Stock int64 + Price int64 + PriceDiff int64 // 价格差绝对值 + } + + // 记录不匹配原因 + var firstMismatchReason string + + for _, item := range getGoodsByShopIdAndIsbn.Data { + // 解析价格(单位:分) + itemPrice, _ := strconv.ParseInt(item.TotalPrice, 10, 64) + // 解析库存 + itemStock, _ := strconv.ParseInt(item.Stock, 10, 64) + + // 计算价格差(绝对值) + priceDiff := abs(itemPrice - taskPrice) + + // 价格相差1元以上 + if priceDiff >= priceDiffThreshold { + if firstMismatchReason == "" { + firstMismatchReason = fmt.Sprintf("商品[%s]价格相差超过1元: 任务价格=%d分, 商品价格=%d分, 差价=%d分", item.TrilateralId, taskPrice, itemPrice, priceDiff) + } + continue + } + + // 价格相差小于1元 → 加入候选列表 + matchedItems = append(matchedItems, struct { + TrilateralId string + SkuId string + Stock int64 + Price int64 + PriceDiff int64 + }{ + TrilateralId: item.TrilateralId, + SkuId: item.SkuId, + Stock: itemStock, + Price: itemPrice, + PriceDiff: priceDiff, + }) + } + + // 逻辑: + // 1. 所有价格相差1元以上 → 重新发布 + // 2. 否则 → 找到价格相差最小的增加库存,如果多个最小差价一样则对第一条增加库存 + if len(matchedItems) == 0 { + // 所有商品价格相差≥1元 → 重新发布 + fmt.Printf("[重新发布] %s\n", firstMismatchReason) + + task, addGoodsTaskErr := xianYu.AddGoodsTask(taskMsg) + if addGoodsTaskErr != nil { + return "", addGoodsTaskErr + } + return task, nil + } + + // 找到价格相差最小的商品,如果多个最小差价一样则对第一条增加库存 + minDiff := int64(999999999) + var targetItem struct { + TrilateralId string + SkuId string + Stock int64 + } + + for _, item := range matchedItems { + // 找到更小的差价,或者差价相等但为第一条 + if item.PriceDiff < minDiff || (item.PriceDiff == minDiff && targetItem.TrilateralId == "") { + minDiff = item.PriceDiff + targetItem.TrilateralId = item.TrilateralId + targetItem.SkuId = item.SkuId + targetItem.Stock = item.Stock + } + } + + // 将 targetItem.TrilateralId 转为 int64 + trilateralId, trilateralIdParseIntErr := strconv.ParseInt(targetItem.TrilateralId, 10, 64) + if trilateralIdParseIntErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, trilateralIdParseIntErr) + } + + // 解析应用 id与应用秘钥 + var token planBTypeXianyu.Token + unmarshalErr := json.Unmarshal([]byte(golabl.Task.Header.ShopMsg.Token), &token) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, planAType.TaskBody{}, golabl.TaskType, fmt.Errorf("解析应用id与应用秘钥失败: %v", unmarshalErr)) + } + + // 获取商品详情 + getGoodsDetailReq := planBTypeXianyu.GoodsDetailReq{ + AppId: token.AppId, + AppSecret: token.AppSecret, + ProductId: trilateralId, + } + + // 发送请求 + goodsDetailResp, goodsDetailRespErr := xianYu.getGoodsDetail(getGoodsDetailReq) + if goodsDetailRespErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("获取商品详情失败: %v", goodsDetailRespErr)) + } + + var goodDetailRet planBTypeXianyu.GoodDetailRet + unmarshalErr = json.Unmarshal([]byte(goodsDetailResp), &goodDetailRet) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("解析商品详情失败: %v", unmarshalErr)) + } + + //检验商品数量 + if goodDetailRet.Code == 200 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("在闲鱼中未查询到该商品id %v %v", trilateralId, goodDetailRet.Msg)) + } + + //增量修改库存 + taskMsg.Detail.GoodsId = trilateralId + taskMsg.Detail.Stock = taskMsg.Detail.Stock + goodDetailRet.Data.Stock + quantity, updateGoodsQuantityErr := executeGoodsUpdateStock(logUuid, taskMsg) + if updateGoodsQuantityErr != nil { + return "", updateGoodsQuantityErr + } + + //暂停 5秒 + time.Sleep(5 * time.Second) + return quantity, nil + } +} + +// abs 返回绝对值 +func abs(x int64) int64 { + if x < 0 { + return -x + } + return x +} + +func (xianYu *XianYu) SetGoodsTask() string { + return "闲鱼商品修改任务" + +} + +// *******************************私有方法************************************ // + +// 获取省市区 信息 +func getProvinceCityDistrict(types int64, id int) (int, int, int, error) { + if types == 0 { // 直接指定区域的省市区 + //根据区id 获取省、市、区code + provinceCode, cityCode, districtCode, getRegionIdErr := service.GetRegionId(strconv.Itoa(id)) + if getRegionIdErr != nil { + return 0, 0, 0, getRegionIdErr + } + return provinceCode, cityCode, districtCode, nil + } else if types == 1 { // 返回指定省下的随机区 + region, getRandomDistrictInProvinceErr := service.GetRandomDistrictInProvince(id) + if getRandomDistrictInProvinceErr != nil { + return 0, 0, 0, getRandomDistrictInProvinceErr + } + //根据区id 获取省、市、区code + provinceCode, cityCode, districtCode, getRegionIdErr := service.GetRegionId(region["id"]) + if getRegionIdErr != nil { + return 0, 0, 0, getRegionIdErr + } + return provinceCode, cityCode, districtCode, nil + + } else if types == 2 { //在全国返回随机省下的随机区 + region, getRandomDistrictErr := service.GetRandomDistrict() + if getRandomDistrictErr != nil { + return 0, 0, 0, getRandomDistrictErr + } + //根据区id 获取省、市、区code + provinceCode, cityCode, districtCode, getRegionIdErr := service.GetRegionId(region["id"]) + if getRegionIdErr != nil { + return 0, 0, 0, getRegionIdErr + } + return provinceCode, cityCode, districtCode, nil + } + return 0, 0, 0, fmt.Errorf("参数错误") +} + +// 商品新增 +// @param token 授权令牌 +// @param logUuid 日志ID +// @param goodsInfo 添加商品信息 +// @return XianYuAddGoodsResponse 商品新增结果 +// @return string 添加商品结果json +// @return error 错误信息 +func addGoods(logUuid string, goodsInfo planBTypeXianyu.GoodsAdd) (planBTypeXianyu.XianYuAddGoodsResponse, string, error) { + var goodsAdd planBTypeXianyu.XianYuAddGoodsResponse + goodsInfoStr, marshalErr := json.Marshal(goodsInfo) + if marshalErr != nil { + return goodsAdd, "", marshalErr + } + + goodsAddStr, xianYuGoodsAddErr := golabl.XianYuDll.XianYuGoodsAddNew(string(goodsInfoStr), golabl.Config.FileUrl.XianYuDll) + if xianYuGoodsAddErr != nil { + return goodsAdd, "", xianYuGoodsAddErr + } + unmarshalErr := json.Unmarshal([]byte(goodsAddStr), &goodsAdd) + if unmarshalErr != nil { + return goodsAdd, "", unmarshalErr + } + if goodsAdd.Code != 0 || goodsAdd.Msg != "OK" { + //记录请求日志 + addGoodsReqMsg := fmt.Sprintf(` +════════════════════════════════════════════════════════════════ +【闲鱼商品添加请求】 +请求ID: %s +时间: %s +参数: %s +════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + string(goodsInfoStr)) + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, addGoodsReqMsg) + return goodsAdd, goodsAddStr, errors.New("闲鱼 XianYuGoodsAdd 错误:" + goodsAddStr) + } + return goodsAdd, goodsAddStr, nil +} + +// 商品上架 +func launchGoods(logUuid string, launchGoodsInfo planBTypeXianyu.Product) (planBTypeXianyu.XianYuAddGoodsResponse, string, error) { + var launchGoods planBTypeXianyu.XianYuAddGoodsResponse + launchGoodsInfoStr, marshalErr := json.Marshal(launchGoodsInfo) + if marshalErr != nil { + return launchGoods, "", marshalErr + } + launchGoodsStr, xianYuLaunchGoodsAddErr := golabl.XianYuDll.XianYuLaunchGoods(string(launchGoodsInfoStr), golabl.Config.FileUrl.XianYuDll) + if xianYuLaunchGoodsAddErr != nil { + return launchGoods, "", xianYuLaunchGoodsAddErr + } + unmarshalErr := json.Unmarshal([]byte(launchGoodsStr), &launchGoods) + if unmarshalErr != nil { + return launchGoods, "", unmarshalErr + } + if launchGoods.Code != 0 { + //记录请求日志 + addGoodsReqMsg := fmt.Sprintf(` + ════════════════════════════════════════════════════════════════ + 【闲鱼上架商品请求】 + 请求ID: %s + 时间: %s + 参数: %s + ════════════════════════════════════════════════════════════════`, + logUuid, + time.Now().Format("2006-01-02 15:04:05.000"), + string(launchGoodsInfoStr)) + tool.LoggingMiddleware(logs.LOG_LEVEL_INFO, addGoodsReqMsg) + return launchGoods, launchGoodsStr, errors.New("闲鱼 XianYuLaunchGoods 错误:" + launchGoodsStr) + } + return launchGoods, launchGoodsStr, nil +} + +// phaseOneGoodsOnlyCount 第一阶段只获取商品总数,不写入wait队列 +func (xianYu *XianYu) phaseOneGoodsOnlyCount(token planBTypeXianyu.Token, pageSize int, maxPage int) error { + for page := 1; page <= maxPage; page++ { + xianYuListReq := planBTypeXianyu.GoodsListReq{ + AppId: token.AppId, + AppSecret: token.AppSecret, + UpdateTime: nil, // 不传入时间,获取所有商品 + ProductStatus: 22, + PageNo: page, + PageSize: pageSize, + } + + listJson, err := xianYu.sendGoodsListRequest(xianYuListReq) + if err != nil { + return fmt.Errorf("获取商品列表失败,页码: %d, 错误: %v", page, err) + } + + var list planBTypeXianyu.GoodsListRet + err = json.Unmarshal([]byte(listJson), &list) + if err != nil { + return fmt.Errorf("解析响应失败,页码: %d, 错误: %v", page, err) + } + if list.Code != 0 { + return fmt.Errorf("获取商品列表失败 code=%d, msg=%s", list.Code, list.Msg) + } + + // 更新header进度总数(第一页) + if page == 1 { + fmt.Println("总数: ", strconv.Itoa(list.Data.Count)) + if updateTaskHeaderErr := service.SetTaskCount(strconv.Itoa(list.Data.Count)); updateTaskHeaderErr != nil { + return updateTaskHeaderErr + } + // 获取到总数后即可退出,不需要继续拉取 + fmt.Println("第一阶段完成,已获取商品总数,不写入wait队列") + break + } + } + return nil +} + +// phaseTwoGoods 第二阶段拉取商品信息(按时间范围分批) +// 修改:结束时间固定为当前时间,开始时间为当前时间往前推30天 +func (xianYu *XianYu) phaseTwoGoods(token planBTypeXianyu.Token, pageSize int, totalFetched *int, lastUpdateTime *int64, maxRecordsPerRange int) error { + // 第二阶段:以当前时间为结束时间,开始时间为当前时间往前推30天 + // 注意:lastUpdateTime 参数在此版本中不再使用,改为固定从"当前时间-30天"开始 + now := time.Now().Unix() + endTime := now // 结束时间固定为当前时间 + currentUpdateTimeFrom := now - 30*24*60*60 // 开始时间 = 当前时间 - 30天 + + fmt.Printf("第二阶段开始,开始时间: %d (%s), 结束时间: %d (%s) [当前时间]", + currentUpdateTimeFrom, time.Unix(currentUpdateTimeFrom, 0).Format("2006-01-02 15:04:05"), + endTime, time.Unix(endTime, 0).Format("2006-01-02 15:04:05")) + + if currentUpdateTimeFrom > 0 { + //currentUpdateTimeFrom := *lastUpdateTime + maxLoopCount := 100 // 最大循环次数保护 + loopCount := 0 + + var currentUpdateTimeEnd int64 // 声明在循环外部,避免每次迭代重置 + + for loopCount < maxLoopCount { + loopCount++ + + // 检查开始时间是否已超过当前时间 + if currentUpdateTimeFrom > time.Now().Unix() { + fmt.Printf("开始时间 %d 已超过当前时间,停止获取\n", currentUpdateTimeFrom) + break + } + + // 设置结束时间:首次循环用当前时间,后续循环由pageExceededThreshold或>=10000条件块设置 + if loopCount == 1 { + currentUpdateTimeEnd = endTime // 首次循环赋值 + } + + // 检查结束时间是否已超过半年(180天),超过则停止获取 + halfYearAgo := time.Now().Unix() - 180*24*60*60 + fmt.Printf("[调试] loopCount=%d, currentUpdateTimeEnd=%d (%s), halfYearAgo=%d (%s), = maxLoopCount { + fmt.Printf("达到最大循环次数 %d,强制退出\n", maxLoopCount) + break + } + + fmt.Printf("开始获取时间范围: %d (%s) 到 %d (%s)\n", + currentUpdateTimeFrom, time.Unix(currentUpdateTimeFrom, 0).Format("2006-01-02 15:04:05"), + currentUpdateTimeEnd, time.Unix(currentUpdateTimeEnd, 0).Format("2006-01-02 15:04:05")) + + currentPage := 1 + batchGoodsCount := 0 + lastItemUpdateTime := int64(0) + pageExceededThreshold := false // 标记是否页码超过阈值 + + // 在当前时间范围内分页获取数据 + for { + // 检查当前页码是否超过100 + if currentPage > 100 { + fmt.Printf("警告:当前页码 %d 已超过100,将使用上一次的结束时间作为新的开始时间\n", currentPage) + pageExceededThreshold = true + break + } + + xianYuListReq := planBTypeXianyu.GoodsListReq{ + AppId: token.AppId, + AppSecret: token.AppSecret, + UpdateTime: []int64{currentUpdateTimeFrom, currentUpdateTimeEnd}, + ProductStatus: 22, + PageNo: currentPage, + PageSize: pageSize, + } + + listJson, err := xianYu.sendGoodsListRequest(xianYuListReq) + if err != nil { + return fmt.Errorf("获取商品列表失败(时间范围),页码: %d, 错误: %v", currentPage, err) + } + + var list planBTypeXianyu.GoodsListRet + err = json.Unmarshal([]byte(listJson), &list) + if err != nil { + return fmt.Errorf("解析响应失败(时间范围),页码: %d, 错误: %v", currentPage, err) + } + + if list.Code == 100001 { + fmt.Println("大于半年了,结束查询111") + fmt.Println("开始时间", time.Unix(currentUpdateTimeFrom, 0).Format("2006-01-02 15:04:05")) + fmt.Println("结束时间", time.Unix(currentUpdateTimeEnd, 0).Format("2006-01-02 15:04:05")) + break + } + if list.Code != 0 { + return fmt.Errorf("获取商品列表失败 code=%d, msg=%s", list.Code, list.Msg) + } + + // 如果当前页没有数据 + if len(list.Data.List) == 0 { + // 如果当前页是第一页且没有数据,说明整个时间范围都没有数据 + if currentPage == 1 { + fmt.Printf("时间范围 %d - %d 内无数据\n", currentUpdateTimeFrom, currentUpdateTimeEnd) + break + } + // 当前页没有数据,但前面有数据,说明当前时间范围的数据已取完 + fmt.Printf("当前时间范围数据已取完,共获取 %d 条数据\n", batchGoodsCount) + break + } + + // 有数据 + + // 收集商品数据并统计 + for _, goods := range list.Data.List { + *totalFetched++ + // 获取商品详情并写入数据库 + err = xianYu.processGoodsDetail(goods, token) + if err != nil { + fmt.Printf("处理商品 %s 失败: %v\n", goods.ProductID, err) + continue + } + //拉取后暂停0.01秒 + time.Sleep(10 * time.Millisecond) + } + + batchGoodsCount += len(list.Data.List) + + // 记录最后一条商品的更新时间 + lastItem := list.Data.List[len(list.Data.List)-1] + lastItemUpdateTime = lastItem.UpdateTime + + fmt.Printf("第二阶段 - 当前时间范围已获取: %d 条,累计总数: %d,当前页码: %d,最后商品时间: %d (%s)\n", + batchGoodsCount, *totalFetched, currentPage, + lastItemUpdateTime, time.Unix(lastItemUpdateTime, 0).Format("2006-01-02 15:04:05")) + + // 更新进度 + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + processed := int64(len(list.Data.List)) + if updateTaskProgressErr := tool.UpdateTaskProgress(processed); updateTaskProgressErr != nil { + return updateTaskProgressErr + } + + // 判断是否继续下一页 + // 如果返回的数据少于 pageSize,说明没有下一页了 + if len(list.Data.List) < pageSize { + fmt.Printf("当前页数据不足 %d 条,当前时间范围数据已取完\n", pageSize) + break + } + + currentPage++ + } + + // 不足10000条时,以上一轮的开始时间作为本次的结束时间 + // 本次结束时间 = 上一轮开始时间,本次开始时间 = 本次结束时间 - 30天 + if pageExceededThreshold { + if lastItemUpdateTime > 0 { + currentUpdateTimeEnd = lastItemUpdateTime + currentUpdateTimeFrom = currentUpdateTimeEnd - 30*24*60*60 + fmt.Printf("页码超过100,使用最后一页最后一条时间 %d (%s) 作为新的结束时间,开始时间: %d (%s)\n", + currentUpdateTimeEnd, time.Unix(currentUpdateTimeEnd, 0).Format("2006-01-02 15:04:05"), + currentUpdateTimeFrom, time.Unix(currentUpdateTimeFrom, 0).Format("2006-01-02 15:04:05")) + } else { + currentUpdateTimeEnd = currentUpdateTimeFrom + currentUpdateTimeFrom = currentUpdateTimeFrom - 30*24*60*60 + fmt.Printf("页码超过100但无数据,回退30天\n") + } + continue + } + + if batchGoodsCount == 0 { + currentUpdateTimeEnd = currentUpdateTimeFrom + currentUpdateTimeFrom = currentUpdateTimeFrom - 30*24*60*60 + fmt.Printf("未获取到数据,时间窗口往前移动30天\n") + } else { + currentUpdateTimeEnd = currentUpdateTimeFrom + currentUpdateTimeFrom = currentUpdateTimeEnd - 30*24*60*60 + fmt.Printf("获取 %d 条(<10000),以上一轮开始时间作为结束时间,开始时间: %d (%s)\n", + batchGoodsCount, currentUpdateTimeFrom, time.Unix(currentUpdateTimeFrom, 0).Format("2006-01-02 15:04:05")) + } + + // 检查新的开始时间是否已超过当前时间 + if currentUpdateTimeFrom > time.Now().Unix() { + fmt.Printf("开始时间 %d (%s) 已超过当前时间 %d (%s),停止获取\n", + currentUpdateTimeFrom, time.Unix(currentUpdateTimeFrom, 0).Format("2006-01-02 15:04:05"), + time.Now().Unix(), time.Now().Format("2006-01-02 15:04:05")) + break + } + } + if loopCount >= maxLoopCount { + fmt.Printf("警告:已达到最大循环次数 %d,强制退出\n", maxLoopCount) + } + } + return nil +} + +// deduplicateToBodyOver 拉取任务读取body_wait去重复后写入到body_over中 +func (xianYu *XianYu) deduplicateToBodyOver(duplicateCount *int, uniqueCount *int) error { + page := 1 + pageSize := 100 + + // 按店铺存储去重后的商品数据 + shopGoodsMap := make(map[string][]planBTypeXianyu.GoodsDetailRet) + + // 修改:使用复合key(店铺名+商品ID)进行去重,避免同一店铺重复相同商品 + processedKeys := make(map[string]bool) + + // 在循环前删除 body_over与body_backup,避免重复写入 + deleteTaskBodyOverErr := service.DeleteTaskBodyOver() + if deleteTaskBodyOverErr != nil { + return deleteTaskBodyOverErr + } + deleteTaskBodyBackupErr := service.DeleteTaskBodyBackup() + if deleteTaskBodyBackupErr != nil { + return deleteTaskBodyBackupErr + } + + num := 0 + // 获取body_wait总数量 + bodyWaitCount, getTaskBodyWaitCountErr := service.GetTaskBodyWaitCount() + if getTaskBodyWaitCountErr != nil { + return getTaskBodyWaitCountErr + } + pageTotal := (bodyWaitCount + int64(pageSize) - 1) / int64(pageSize) + + // 调试计数器 + debugCount := 0 + for { + list, getTaskBodyOverListErr := service.GetTaskBodyWaitList(page, pageSize) + if getTaskBodyOverListErr != nil { + return getTaskBodyOverListErr + } + if len(list) <= 0 { + // 没有数据,结束循环 + break + } + for _, v := range list { + // 解析v到结构体 + goods := planAType.TaskBody{} + jsonUnmarshalErr := json.Unmarshal([]byte(v), &goods) + if jsonUnmarshalErr != nil { + return fmt.Errorf("将json转为结构体失败: %v\n", jsonUnmarshalErr) + } + // 解析商品详情获取真实的商品ID + var goodsItem planBTypeXianyu.GoodsDetailRet + jsonUnmarshalErr = json.Unmarshal([]byte(goods.Detail.Error), &goodsItem) + if jsonUnmarshalErr != nil { + return fmt.Errorf("将json转为结构体失败: %v\n", jsonUnmarshalErr) + } + + //提取 Isbn + if goodsItem.BookData.ISBN == "" { + goodsItem.BookData.ISBN = tool.ExtractISBN978(goodsItem.Title) + } + //Isbn 为空则跳过 + if goodsItem.BookData.ISBN == "" { + fmt.Println("####################商品无法获取 Isbn,跳过:", goodsItem.Title) + continue + } + + // 使用 ProductID 作为唯一标识(int64类型) + goodsId := goodsItem.ProductID + + // 获取闲鱼会员名 + username := "" + if len(goodsItem.PublishShop) > 0 { + username = goodsItem.PublishShop[0].UserName + } + + // 修改:使用店铺名+商品ID作为复合key进行去重 + uniqueKey := fmt.Sprintf("%s_%d", username, goodsId) + + // 调试:打印前10条数据的ID + if debugCount < 10 { + fmt.Printf("[去重调试] 第%d条 - 商品ID: %d, 店铺: %s, 复合Key: %s, Title: %s\n", + debugCount+1, goodsId, username, uniqueKey, goodsItem.Title) + debugCount++ + } + + if !processedKeys[uniqueKey] { + // 标记为已处理 + processedKeys[uniqueKey] = true + *uniqueCount++ + + // 按店铺暂存数据 + shopGoodsMap[username] = append(shopGoodsMap[username], goodsItem) + + // 写入到body_over + goods.Detail.Status = 1 + addTaskToBodyOverErr := service.AddTaskToBodyOver(goods, []string{"body_over", "body_backup"}) + if addTaskToBodyOverErr != nil { + return addTaskToBodyOverErr + } + + // 检查每个店铺的商品数量,达到batchSize则推送 + if len(shopGoodsMap[username]) >= pageSize { + fmt.Println("推送 username ", username, " 长度 ", len(shopGoodsMap[username])) + _, err := pushShopGoodsData(username, shopGoodsMap[username], int64(page), pageTotal, &num) + if err != nil { + return err + } + // 清空该店铺的数据 + shopGoodsMap[username] = []planBTypeXianyu.GoodsDetailRet{} + } + } else { + // 重复数据 计次 + *duplicateCount++ + // 调试:打印前10条重复数据 + if *duplicateCount <= 10 { + fmt.Printf("[去重调试] 发现重复商品: 店铺=%s, 商品ID=%d, 复合Key=%s\n", username, goodsId, uniqueKey) + } + } + } + + page++ + + // 更新进度 + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + con := int64(len(list)) + if con >= golabl.Task.Footer.TaskCountTrue { + con = golabl.Task.Footer.TaskCountTrue - con + } + if updateTaskProgressErr := tool.UpdateTaskProgress(con); updateTaskProgressErr != nil { + return updateTaskProgressErr + } + + // 暂停1秒 + time.Sleep(1 * time.Second) + } + + // 循环结束后,推送所有店铺剩余的数据(不足batchSize的部分) + for shopId, goodsList := range shopGoodsMap { + if len(goodsList) > 0 { + _, err := pushShopGoodsData(shopId, goodsList, pageTotal, pageTotal, &num) + if err != nil { + return err + } + //打印每个店铺的商品数量 + fmt.Println("推送 username ", shopId, " 长度 ", len(goodsList)) + } + } + + // 删除body_wait + deleteTaskBodyWaitErr := service.DeleteTaskBodyWait() + if deleteTaskBodyWaitErr != nil { + return deleteTaskBodyWaitErr + } + + fmt.Printf("[去重完成] 总处理: %d, 唯一: %d, 重复: %d\n", + *uniqueCount+*duplicateCount, *uniqueCount, *duplicateCount) + + return nil +} + +// pushShopGoodsData 推送单个店铺的商品数据到接口 +func pushShopGoodsData(username string, goodsList []planBTypeXianyu.GoodsDetailRet, currentPage int64, totalPage int64, totalCount *int) (string, error) { + if len(goodsList) == 0 { + return "", nil + } + + // 将获取的数据推送写入店铺商品数据接口 + ret, retStr, writeXianyuGoodsDataErr := writeXianyuGoodsData(goodsList, username, int(currentPage), totalPage) + if writeXianyuGoodsDataErr != nil { + return "", writeXianyuGoodsDataErr + } + if ret.Code != "200" { + return retStr, fmt.Errorf("添加商品失败 %v", retStr) + } + + *totalCount = *totalCount + len(goodsList) + + return retStr, nil +} + +// processGoodsDetail 处理单个商品的详情并写入数据库(修改版) +func (xianYu *XianYu) processGoodsDetail(goodsItem planBTypeXianyu.GoodsListRetProduct, token planBTypeXianyu.Token) error { + // 获取商品详情 + xianYuDetailReq := planBTypeXianyu.GoodsDetailReq{ + AppId: token.AppId, + AppSecret: token.AppSecret, + ProductId: goodsItem.ProductID, + } + + detailJson, err := xianYu.getGoodsDetail(xianYuDetailReq) + if err != nil { + return err + } + + var goodDetailRet planBTypeXianyu.GoodDetailRet + err = json.Unmarshal([]byte(detailJson), &goodDetailRet) + if err != nil { + return err + } + + // 检查返回码 + if goodDetailRet.Code != 0 { + return fmt.Errorf("获取商品详情失败 code=%d, msg=%s", goodDetailRet.Code, goodDetailRet.Msg) + } + + // 将内容转为 json + detailDataJson, marshalErr := json.Marshal(goodDetailRet.Data) + if marshalErr != nil { + return marshalErr + } + + // 调试:打印商品ID信息 + //fmt.Printf("[商品处理] ProductID: %v, 详情ProductID: %d, Title: %s\n", + // goodsItem.ProductID, goodDetailRet.Data.ProductID, goodDetailRet.Data.Title) + + // 构建任务数据 + bodyWait := planAType.TaskBody{ + BookInfo: planAType.BookInfo{ + Isbn: goodDetailRet.Data.BookData.ISBN, + BookName: goodDetailRet.Data.Title, + Author: goodDetailRet.Data.BookData.Author, + Publishing: goodDetailRet.Data.BookData.Publisher, + PublicationDate: "", + Binding: "", + PagesCount: 0, + WordsCount: 0, + Format: 0, + Price: goodsItem.UpdateTime, // 使用UpdateTime作为时间戳 + }, + Detail: planAType.TaskDetail{ + Error: string(detailDataJson), + GoodsId: goodDetailRet.Data.ProductID, // 使用详情中的ProductID + Stock: goodDetailRet.Data.Stock, + }, + } + + // 验证商品 ID不为空 + if bodyWait.Detail.GoodsId == 0 { + fmt.Printf("[警告] 商品 %s 的GoodsId为0\n", goodsItem.ProductID) + } + + // 写入数据库 + bodyWaitJson, err := json.Marshal(bodyWait) + if err != nil { + return fmt.Errorf("将bodyWait转为json失败: %v", err) + } + + return service.AddTaskToBodyWait(string(bodyWaitJson)) +} + +// 发送商品列表请求 +func (xianYu *XianYu) sendGoodsListRequest(req planBTypeXianyu.GoodsListReq) (string, error) { + reqJson, marshalErr := json.Marshal(req) + if marshalErr != nil { + return "", marshalErr + } + + listJson, err := golabl.XianYuDll.XianYuGetGoodsList(string(reqJson), golabl.Config.FileUrl.XianYuDll) + if err != nil { + return "", err + } + return listJson, nil +} + +// 获取商品详情 +func (xianYu *XianYu) getGoodsDetail(req planBTypeXianyu.GoodsDetailReq) (string, error) { + reqJson, marshalErr := json.Marshal(req) + if marshalErr != nil { + return "", marshalErr + } + + detailJson, err := golabl.XianYuDll.XianYuGetGoodsDetail(string(reqJson), golabl.Config.FileUrl.XianYuDll) + if err != nil { + return "", err + } + return detailJson, nil +} + +// writeXianyuGoodsData 写入商品数据 +// @param goodsListStr 商品列表 +// @param username 闲鱼会员名 +// @param page 当前页 +// @param pageTotal 总页数 +// @return error 错误信息 +func writeXianyuGoodsData(goodsListStr []planBTypeXianyu.GoodsDetailRet, username string, page int, pageTotal int64) (planBType.AsyncTaskResponse, string, error) { + var ret planBType.AsyncTaskResponse + marshal, marshalErr := json.Marshal(goodsListStr) + if marshalErr != nil { + return ret, "", marshalErr + } + params := map[string]string{ + "taskId": golabl.Task.TaskId, + "shopId": username, + "goodsListStr": string(marshal), + "allNum": strconv.FormatInt(pageTotal, 10), + "num": strconv.Itoa(page), + } + // 将 params 转为 json + paramsJson, marshalErr := json.Marshal(params) + if marshalErr != nil { + return ret, "", marshalErr + } + fmt.Println(string(paramsJson)) + retStr, submitFormDataErr := tool.SubmitFormData(golabl.Config.FileUrl.XianYuAddGoodsUrl, params) + if submitFormDataErr != nil { + return ret, retStr, submitFormDataErr + } + unmarshalErr := json.Unmarshal([]byte(retStr), &ret) + if unmarshalErr != nil { + return ret, retStr, unmarshalErr + } + return ret, retStr, nil +} + +// executeGoodsLaunch 上架商品 +// @param logUuid 日志ID +// @param taskMsg 任务内容 +// @return error 错误信息 +func executeGoodsLaunch(logUuid string, taskMsg planAType.TaskBody) (string, error) { + // 解析应用 id与应用秘钥 + var token planBTypeXianyu.Token + unmarshalErr := json.Unmarshal([]byte(golabl.Task.Header.ShopMsg.Token), &token) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("解析应用id与应用秘钥 taskHeader.ShopMsg.Token = %v %w", golabl.Task.Header.ShopMsg.Token, unmarshalErr)) + } + + // 上架商品 + launchGoodsInfo := planBTypeXianyu.Product{ + AppId: token.AppId, + AppSecret: token.AppSecret, + ProductID: taskMsg.Detail.GoodsId, + SpecifyPublishTime: "", + UserName: []string{token.Username}, + } + //转为json + jsonData, marshalErr := json.Marshal(launchGoodsInfo) + if marshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, marshalErr) + } + var launchGoods planBTypeXianyu.XianYuAddGoodsResponse + launchGoodsStr, xianYuLaunchGoodsAddErr := golabl.XianYuDll.XianYuLaunchGoods(string(jsonData), golabl.Config.FileUrl.XianYuDll) + if xianYuLaunchGoodsAddErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, xianYuLaunchGoodsAddErr) + } + unmarshalErr = json.Unmarshal([]byte(launchGoodsStr), &launchGoods) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, unmarshalErr) + } + if launchGoods.Code != 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("上架商品失败 %s", launchGoods.Msg)) + } + return tool.ReturnSuccess(taskMsg) +} + +// executeGoodsDownShelf 下架商品 +// @param logUuid 日志ID +// @param taskMsg 任务内容 +// @return error 错误信息 +func executeGoodsDownShelf(logUuid string, taskMsg planAType.TaskBody) (string, error) { + var downShelf planBTypeXianyu.DownShelf + + // 解析应用 id与应用秘钥 + var token planBTypeXianyu.Token + unmarshalErr := json.Unmarshal([]byte(golabl.Task.Header.ShopMsg.Token), &token) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("解析应用id与应用秘钥 taskHeader.ShopMsg.Token = %v %w", golabl.Task.Header.ShopMsg.Token, unmarshalErr)) + } + downShelf.AppId = token.AppId + downShelf.AppSecret = token.AppSecret + downShelf.ProductID = taskMsg.Detail.GoodsId + //转为json + jsonData, marshalErr := json.Marshal(downShelf) + if marshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, marshalErr) + } + shelf, xianYuExecuteGoodsDownShelfErr := golabl.XianYuDll.XianYuExecuteGoodsDownShelf(string(jsonData), golabl.Config.FileUrl.XianYuDll) + if xianYuExecuteGoodsDownShelfErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, xianYuExecuteGoodsDownShelfErr) + } + var downShelfRes planBTypeXianyu.XianYuAddGoodsResponse + unmarshalErr = json.Unmarshal([]byte(shelf), &downShelfRes) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, unmarshalErr) + } + if downShelfRes.Code != 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("下架商品失败 %s", downShelfRes.Msg)) + } + return tool.ReturnSuccess(taskMsg) +} + +// executeGoodsUpdateStock 修改库存 +func executeGoodsUpdateStock(logUuid string, taskMsg planAType.TaskBody) (string, error) { + var updateStock planBTypeXianyu.UpdateStock + + // 解析应用 id与应用秘钥 + var token planBTypeXianyu.Token + unmarshalErr := json.Unmarshal([]byte(golabl.Task.Header.ShopMsg.Token), &token) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("解析应用id与应用秘钥 taskHeader.ShopMsg.Token = %v %w", golabl.Task.Header.ShopMsg.Token, unmarshalErr)) + } + updateStock.AppId = token.AppId + updateStock.AppSecret = token.AppSecret + updateStock.ProductID = taskMsg.Detail.GoodsId + updateStock.Stock = taskMsg.Detail.Stock + //转为json + jsonData, marshalErr := json.Marshal(updateStock) + if marshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, marshalErr) + } + updateStockStr, xianYuExecuteGoodsUpdateStockErr := golabl.XianYuDll.XianYuExecuteGoodsUpdateStock(string(jsonData), golabl.Config.FileUrl.XianYuDll) + if xianYuExecuteGoodsUpdateStockErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, xianYuExecuteGoodsUpdateStockErr) + } + var updateStockRes planBTypeXianyu.XianYuAddGoodsResponse + unmarshalErr = json.Unmarshal([]byte(updateStockStr), &updateStockRes) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, unmarshalErr) + } + if updateStockRes.Code != 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("修改商库存品失败 %s", updateStockRes.Msg)) + } + taskMsg.Detail.Error = "增加库存成功!" + return tool.ReturnSuccess(taskMsg) +} + +// 修改价格 +func executeGoodsUpdatePrice(logUuid string, taskMsg planAType.TaskBody) (string, error) { + var updatePrice planBTypeXianyu.UpdatePrice + + // 价格0 不能发布 + if taskMsg.Detail.Price == 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("拼多多商品 价格不能为0")) + } + + // 解析应用 id与应用秘钥 + var token planBTypeXianyu.Token + unmarshalErr := json.Unmarshal([]byte(golabl.Task.Header.ShopMsg.Token), &token) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("解析应用id与应用秘钥 taskHeader.ShopMsg.Token = %v %w", golabl.Task.Header.ShopMsg.Token, unmarshalErr)) + } + updatePrice.AppId = token.AppId + updatePrice.AppSecret = token.AppSecret + updatePrice.ProductID = taskMsg.Detail.GoodsId + updatePrice.Price = taskMsg.Detail.Price + updatePrice.OriginalPrice = tool.BuildGoodsPrice(taskMsg.Detail.Price) + //转为json + jsonData, marshalErr := json.Marshal(updatePrice) + if marshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, marshalErr) + } + updatePriceStr, xianYuExecuteGoodsUpdatePrice := golabl.XianYuDll.XianYuExecuteGoodsUpdatePrice(string(jsonData), golabl.Config.FileUrl.XianYuDll) + if xianYuExecuteGoodsUpdatePrice != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, xianYuExecuteGoodsUpdatePrice) + } + var updatePriceRes planBTypeXianyu.XianYuAddGoodsResponse + unmarshalErr = json.Unmarshal([]byte(updatePriceStr), &updatePriceRes) + if unmarshalErr != nil { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, unmarshalErr) + } + if updatePriceRes.Code != 0 { + return tool.ReturnErr(logUuid, taskMsg, golabl.TaskType, fmt.Errorf("修改商品价格失败 %s", updatePriceRes.Msg)) + } + return tool.ReturnSuccess(taskMsg) +} + +// 闲鱼发布 +func publishGoods(logUuid string, taskMsg planAType.TaskBody) (planAType.TaskBody, error) { + // 价格不能小于0 + if taskMsg.Detail.Price <= 0 { + return taskMsg, fmt.Errorf("价格不能小于等于0") + } + + //获取出版社信息并解析 + if getPublishingErr := service.GetPublishingVid(&taskMsg); getPublishingErr != nil { + return taskMsg, fmt.Errorf("获取出版社信息失败-原因来自:%v", getPublishingErr) + } + + //违规词处理 + if golabl.Config.Server.Filter == 1 { + //开启违规词处理 + if taskMsgErr := tool.FilterWord(&taskMsg); taskMsgErr != nil { + return taskMsg, taskMsgErr + } + } + + // 构建参数 + var goodsAdd planBTypeXianyu.GoodsAdd + + // 解析应用 id与应用秘钥 + var token planBTypeXianyu.Token + unmarshalErr := json.Unmarshal([]byte(golabl.Task.Header.ShopMsg.Token), &token) + if unmarshalErr != nil { + return taskMsg, fmt.Errorf("解析应用id与应用秘钥 taskHeader.ShopMsg.Token = %v %w", golabl.Task.Header.ShopMsg.Token, unmarshalErr) + } + // 应用 ID + goodsAdd.AppId = token.AppId + + // 应用密钥 + goodsAdd.AppSecret = token.AppSecret + + // token + goodsAdd.Token = "" + + // API 使用的店铺ID + goodsAdd.ApiShopId = 0 + + // 平台类型 + goodsAdd.TypePlatform = 4 + + // 店铺 ID + goodsAdd.ShopId = 0 + + // 店铺 Token + goodsAdd.ShopToken = "" + + // 店铺名称 + goodsAdd.ShopName = "" + + // 发货省,格式为省级行政区划代码(如210000代表辽宁省) + provinceCode, cityCode, districtCode, getProvinceCityDistrictErr := getProvinceCityDistrict(0, 20) + if getProvinceCityDistrictErr != nil { + return taskMsg, fmt.Errorf("获取省、市、区信息失败: %v", getProvinceCityDistrictErr) + } + goodsAdd.Province = provinceCode + + // 发货市,格式为市级行政区划代码(如210100代表沈阳市) + goodsAdd.City = cityCode + + // 发货区,格式为区级行政区划代码(如210101代表和平区) + goodsAdd.District = districtCode + + // 商品类型 + goodsAdd.TypeGoods = "" + + // 分类类型 + goodsAdd.TypeClass = "" + + // 类目 ID + spBizType := int32(24) //默认 图书类目 + goodsAdd.CatIds = "c3c6e8d1d63c0618b108d382c4e6ea42" //默认类目ID(文学,小说) + if golabl.Task.Header.ShopMsg.PublishType == "1" { + spBizType = 99 //其他 + goodsAdd.CatIds = golabl.Task.Header.ShopMsg.CategoryId //根据用户选择 + } + isbn := taskMsg.BookInfo.Isbn + // 如果isbn是678开头的 + if strings.HasPrefix(taskMsg.BookInfo.Isbn, "678") { + //如果类目ID为空,则使用默认类目ID(其他闲置) + // goodsAdd.CatIds = "86cddebb2de0815c267e0a01017d9f44" //资料册 + // goodsAdd.CatIds = "2dfa3034d88aedcc1921b9e373cead75" //期刊/杂志 + goodsAdd.CatIds = "8bd8d9724880b84d28d88a08a19453dc" //学习笔记(无ISBN可以发布成功)OK + spBizType = 99 //(其他) + // isbn = "" + fmt.Println("Misty-goodsAdd.CatIds:", goodsAdd.CatIds) + } + + goodsAdd.SpBizType = spBizType + + fmt.Println("一级类目:", spBizType) + fmt.Println("二级类目:", goodsAdd.CatIds) + fmt.Println("isbn:", isbn) + + if len(taskMsg.BookInfo.ImageObject.CarouselUrlArray) == 0 { + // 无图片信息 isbn计次 + setNoImgCountErr := service.SetNoImgCount(taskMsg.BookInfo.Isbn) + if setNoImgCountErr != nil { + return taskMsg, fmt.Errorf("无图片信息isbn计次错误 isbn %v %v", taskMsg.BookInfo.Isbn, setNoImgCountErr.Error()) + } + return taskMsg, fmt.Errorf("缺少轮播图") + } + // 构建详情图 + contentImgs := tool.BuildDetailGallery(golabl.Task.Header.ShopMsg.GoodsDetailFirstImgUrlArray, golabl.Task.Header.ShopMsg.GoodsDetailLastImgUrlArray, taskMsg.BookInfo.ImageObject.DetailUrlObject, taskMsg.BookInfo.ImageObject.CarouselUrlArray[0]) + + oldCarouselUrlArray := append([]string{}, taskMsg.BookInfo.ImageObject.CarouselUrlArray...) //原始轮播图,用于后续处理,不会被打上水印 + + //存在水印图片,则打水印 + fmt.Println("水印图片地址:", golabl.Task.Header.ShopMsg.WatermarkImgUrl) + if golabl.Task.Header.ShopMsg.WatermarkImgUrl != "" { + //获取水印图片 + watermarkImgUrl, watermarkImgErr := tool.GetWatermarkImg() + if watermarkImgErr != nil { + return taskMsg, fmt.Errorf("1图片打水印失败 %v", fmt.Errorf("获取水印图片失败 %v", watermarkImgErr)) + } + + //打水印 + watermarkFromURLExsBase64Arr, watermarkFromURLExsErr := tool.AddWatermarkFromURLExs(taskMsg.BookInfo.ImageObject.CarouselUrlArray, watermarkImgUrl, golabl.Task.Header.ShopMsg.WatermarkPosition) + if watermarkFromURLExsErr != nil { + return taskMsg, fmt.Errorf("2图片打水印失败 %v", watermarkFromURLExsErr) + } + + //图片上传到图片空间 + toMinIo, uploadToMinIoErr := tool.UploadToMinIo(watermarkFromURLExsBase64Arr) + if uploadToMinIoErr != nil { + return taskMsg, fmt.Errorf("图片上传到图片空间失败 %v", uploadToMinIoErr) + } + + //将上传的图片替换到商品轮播图中 + for i := 0; i < len(toMinIo); i++ { + taskMsg.BookInfo.ImageObject.CarouselUrlArray[i] = toMinIo[i] + } + } + + // 构建主图(轮播图) + //refactorCarouselGallery := tool.BuildCarouselGalleryOld(golabl.Task.Header.ShopMsg.CarouseLastImgUrlArray, taskMsg.BookInfo.ImageObject.CarouselUrlArray) + refactorCarouselGallery := tool.BuildCarouselGallery(golabl.Task.Header.ShopMsg.CarouseLastImgUrlArray, oldCarouselUrlArray, taskMsg.BookInfo.ImageObject.CarouselUrlArray, golabl.Task.Header.ShopMsg.WatermarkPosition) + + // 如果轮播图没有图片,并且是优先官图,则使用默认图片 + if len(refactorCarouselGallery) == 0 && golabl.Task.Header.ImgType == 3 && taskMsg.BookInfo.ImageObject.DefaultImageUrl != "" { + refactorCarouselGallery = append(refactorCarouselGallery, taskMsg.BookInfo.ImageObject.DefaultImageUrl) + } + + if len(taskMsg.BookInfo.ImageObject.DetailUrlObject.LiveShootingUrl) == 0 && len(refactorCarouselGallery) > 0 { + taskMsg.BookInfo.ImageObject.DetailUrlObject.LiveShootingUrl = []string{refactorCarouselGallery[0]} + } + if len(refactorCarouselGallery) == 0 { + return taskMsg, fmt.Errorf("缺少构造轮播图图片-未提交 isbn %v", taskMsg.BookInfo.Isbn) + } + //构建商品名称 + title := tool.BuildGoodsName( + golabl.Task.Header.ShopMsg.GoodsNamePrefix, // 商品名称前缀 + golabl.Task.Header.ShopMsg.GoodsNameSuffix, // 商品名称后缀 + golabl.Task.Header.ShopMsg.TitleConsistOf, // 标题组成 + golabl.Task.Header.ShopMsg.SpaceCharacter, // 间隔符 + taskMsg.BookInfo) // 图书信息 + taskMsg.Detail.GoodsName = title + + // 构建商品信息 + content := taskMsg.BookInfo.BookName + " " + taskMsg.BookInfo.Isbn + " " + taskMsg.BookInfo.Author + " " + taskMsg.BookInfo.Publishing + content = content + "\n" + golabl.Task.Header.ShopMsg.ShopContext + + // 店铺信息 + goodsAdd.Shop = []planBTypeXianyu.ShopInfo{ + { + UserName: token.Username, + Province: provinceCode, + City: cityCode, + District: districtCode, + Title: title, + Content: content, + MainImgs: refactorCarouselGallery, + ContentImgs: contentImgs, + }, + } + + // 成色 + goodsAdd.StuffStatus = taskMsg.Detail.Condition + if goodsAdd.StuffStatus == 0 { + goodsAdd.StuffStatus = 90 + } + + //库存 + if taskMsg.Detail.Stock == 0 && (golabl.Task.Header.TaskType == 1 || golabl.Task.Header.TaskType == 2 || golabl.Task.Header.TaskType == 6) { + //如果库存为0 则给默认库存 + taskMsg.Detail.Stock = golabl.Task.Header.ShopMsg.DefStock + } else { + if taskMsg.Detail.Stock == 0 && golabl.Task.Header.TaskType == 8 { + return taskMsg, fmt.Errorf("库存不能为0") + } + } + + url := "http://127.0.0.1:8095" + tool.HttpGetRequest(url) + + //价格 + 运费 + if golabl.Task.Header.PriceType != "0" { + taskMsg.Detail.Price = taskMsg.Detail.Price + taskMsg.Detail.ShippingCost + } + + //构建参考价格 + price := tool.BuildPrice(golabl.Task.Header.PriceMod, taskMsg.Detail.Price) + if price == 0 { + return taskMsg, fmt.Errorf("不在价格区间内 isbn:%v", taskMsg.BookInfo.Isbn) + } + + taskMsg.Detail.Price = price + + //构建定价 + taskMsgBookInfoPrice := tool.BuildGoodsPrice(price) + + goodsAdd.ItemBizType = 2 + goodsAdd.SpBizType = spBizType + goodsAdd.Price = taskMsgBookInfoPrice + goodsAdd.Stock = taskMsg.Detail.Stock + + // 图书类商品信息 + if strings.HasPrefix(taskMsg.BookInfo.Isbn, "678") { + + } else { + goodsAdd.BookData = []planBTypeXianyu.BookInfo{ + { + ISBN: isbn, + Title: title, + Author: taskMsg.BookInfo.Author, + Publisher: taskMsg.Publishing.Value, + }, + } + } + + // 构建商品编码 + outGoodsId := "" + if taskMsg.Detail.OutGoodsId != "" { + outGoodsId = taskMsg.Detail.OutGoodsId + } else { + outGoodsId = taskMsg.BookInfo.Isbn + } + + //货号 + skuCode := "" + if taskMsg.Detail.SkuCode != "" { + skuCode = taskMsg.Detail.SkuCode + } else { + skuCode = outGoodsId + } + + goodsAdd.OuterId = skuCode + goodsAdd.SkuItems = []planBTypeXianyu.SkuItems{ + { + OuterID: outGoodsId, + Price: taskMsg.Detail.Price, + SkuText: taskMsg.BookInfo.BookName, + Stock: taskMsg.Detail.Stock, + }, + } + + // 闲鱼批次商品 KEY + goodsAdd.ItemKey = strconv.FormatInt(time.Now().Unix(), 10) + + // 新增商品 + goodsAddRet, goodsAddStr, err := addGoods(logUuid, goodsAdd) + if err != nil { + return taskMsg, fmt.Errorf("商品提交 %v", err) + } + + if len(goodsAddRet.Data.Success) <= 0 { + return taskMsg, fmt.Errorf("新增商品失败 %v 闲鱼返回信息 %v", err, goodsAddStr) + } + // 上架商品 + launchGoodsInfo := planBTypeXianyu.Product{ + AppId: token.AppId, + AppSecret: token.AppSecret, + Token: "", + NotifyURL: "", + ProductID: goodsAddRet.Data.Success[0].ProductID, + SpecifyPublishTime: "", + UserName: []string{token.Username}, + } + //延迟1分钟 + time.Sleep(time.Minute) + //商品上架 + if taskMsg.Detail.IsOnsale == 0 { + _, _, err = launchGoods(logUuid, launchGoodsInfo) + if err != nil { + return taskMsg, fmt.Errorf("商品提交 %v", err) + } + } + taskMsg.Detail.GoodsId = goodsAddRet.Data.Success[0].ProductID + taskMsg.Detail.OutGoodsId = outGoodsId + taskMsg.Detail.Img = refactorCarouselGallery[0] + return taskMsg, nil +} diff --git a/planB/go.sum b/planB/go.sum new file mode 100644 index 0000000..e44c211 --- /dev/null +++ b/planB/go.sum @@ -0,0 +1,40 @@ +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8= +github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY= +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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/planB/initialization/config/config.go b/planB/initialization/config/config.go new file mode 100644 index 0000000..987db0b --- /dev/null +++ b/planB/initialization/config/config.go @@ -0,0 +1,46 @@ +package config + +import ( + "encoding/json" + "fmt" + "planA/planB/initialization/golabl" + planBConfig "planA/planB/modules/config" + "planA/tool" + planAtype "planA/type" +) + +// GetConfigSetToG 获取配置文件并保存到全局变量中 +// @return error 错误信息 +func GetConfigSetToG() error { + // 检查全局 CTX 是否失效 以防止重复初始化和 ctx 失效 导致程序崩溃 + checkContextErr := tool.CheckContext(golabl.Ctx) + if checkContextErr != nil { + // 返回 且 返回错误 + return checkContextErr + } + + //读取配置文件 + var config planAtype.Config + + // 加载 config.dll + dll, initConfigDLLErr := planBConfig.InitConfigDLL() + if initConfigDLLErr != nil { + return initConfigDLLErr + } + + // 读取配置文件 + configJson, ReadConfigFileErr := dll.ReadConfigFile("", "config.yaml") + if ReadConfigFileErr != nil { + return fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + // 转换配置文件到 JSON + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + + // 保存到全局变量 + golabl.Config = config + // 返回 + return nil +} diff --git a/planB/initialization/dll/dll.go b/planB/initialization/dll/dll.go new file mode 100644 index 0000000..fe635dd --- /dev/null +++ b/planB/initialization/dll/dll.go @@ -0,0 +1,39 @@ +package dll + +import ( + "planA/planB/initialization/dll/image" + "planA/planB/initialization/dll/kfz" + "planA/planB/initialization/dll/logs" + "planA/planB/initialization/dll/pdd" + "planA/planB/initialization/dll/xianYu" +) + +// GetDllSetToG 获取DLL +func GetDllSetToG() error { + // 初始化 PddDll + getPddDllSetToGErr := pdd.GetPddDllSetToG() + if getPddDllSetToGErr != nil { + return getPddDllSetToGErr + } + // 初始化 ImageDll + getImageDllSetToGErr := image.GetImageDllSetToG() + if getImageDllSetToGErr != nil { + return getImageDllSetToGErr + } + // 初始化 XianYuDll + getXianYuDllSetToGErr := xianYu.GetXianYuDllSetToG() + if getXianYuDllSetToGErr != nil { + return getXianYuDllSetToGErr + } + // 初始化 LogrDll + getLogrDllSetToGErr := logs.GetLogrDllSetToG() + if getLogrDllSetToGErr != nil { + return getLogrDllSetToGErr + } + // 获取KfzDll + getKfzDllSetToGErr := kfz.GetKfzDllSetToG() + if getKfzDllSetToGErr != nil { + return getKfzDllSetToGErr + } + return nil +} diff --git a/planB/initialization/dll/image/image.go b/planB/initialization/dll/image/image.go new file mode 100644 index 0000000..7544975 --- /dev/null +++ b/planB/initialization/dll/image/image.go @@ -0,0 +1,16 @@ +package image + +import ( + "planA/planB/initialization/golabl" + "planA/planB/modules/image" +) + +// GetImageDllSetToG 获取图片DLL +func GetImageDllSetToG() error { + imageDll, imageDllErr := image.InitImageDll(golabl.Config.FileUrl.ImageDll) + if imageDllErr != nil { + return imageDllErr + } + golabl.ImageDll = imageDll + return nil +} diff --git a/planB/initialization/dll/kfz/kfz.go b/planB/initialization/dll/kfz/kfz.go new file mode 100644 index 0000000..b4eaf3e --- /dev/null +++ b/planB/initialization/dll/kfz/kfz.go @@ -0,0 +1,17 @@ +package kfz + +import ( + "planA/planB/initialization/golabl" + "planA/planB/modules/kfz" +) + +// GetKfzDllSetToG 获取孔夫子DLL +func GetKfzDllSetToG() error { + // 初始化 KfzDll + kfzDll, err := kfz.InitKfzDll(golabl.Config.FileUrl.KfzDll) + if err != nil { + return err + } + golabl.KfzDll = kfzDll + return nil +} diff --git a/planB/initialization/dll/logs/logs.go b/planB/initialization/dll/logs/logs.go new file mode 100644 index 0000000..aa27169 --- /dev/null +++ b/planB/initialization/dll/logs/logs.go @@ -0,0 +1,16 @@ +package logs + +import ( + "planA/planB/initialization/golabl" + "planA/planB/modules/logs" +) + +// GetLogrDllSetToG 获取日志DLL +func GetLogrDllSetToG() error { + dll, ensureLoggerDLLErr := logs.EnsureLoggerDLL(golabl.Config.FileUrl.LogDll) + if ensureLoggerDLLErr != nil { + return ensureLoggerDLLErr + } + golabl.LogDll = dll + return nil +} diff --git a/planB/initialization/dll/pdd/pdd.go b/planB/initialization/dll/pdd/pdd.go new file mode 100644 index 0000000..213bc71 --- /dev/null +++ b/planB/initialization/dll/pdd/pdd.go @@ -0,0 +1,17 @@ +package pdd + +import ( + "planA/planB/initialization/golabl" + "planA/planB/modules/pdd" +) + +// GetPddDllSetToG 获取拼多多DLL +func GetPddDllSetToG() error { + // 初始化 PddDll + pddDll, err := pdd.InitPddDll(golabl.Config.FileUrl.PddDll) + if err != nil { + return err + } + golabl.PddDll = pddDll + return nil +} diff --git a/planB/initialization/dll/xianYu/xianYu.go b/planB/initialization/dll/xianYu/xianYu.go new file mode 100644 index 0000000..ff5fa09 --- /dev/null +++ b/planB/initialization/dll/xianYu/xianYu.go @@ -0,0 +1,16 @@ +package xianYu + +import ( + "planA/planB/initialization/golabl" + "planA/planB/modules/xianYu" +) + +// GetXianYuDllSetToG 获取闲鱼DLL +func GetXianYuDllSetToG() error { + xianYuDll, xianYuDllErr := xianYu.InitXianYuDll(golabl.Config.FileUrl.XianYuDll) + if xianYuDllErr != nil { + return xianYuDllErr + } + golabl.XianYuDll = xianYuDll + return nil +} diff --git a/planB/initialization/golabl/golabl.go b/planB/initialization/golabl/golabl.go new file mode 100644 index 0000000..8385a7f --- /dev/null +++ b/planB/initialization/golabl/golabl.go @@ -0,0 +1,59 @@ +package golabl + +import ( + "context" + "planA/planB/interfaces" + "planA/planB/modules/image" + "planA/planB/modules/kfz" + "planA/planB/modules/logs" + "planA/planB/modules/pdd" + xianYuDll "planA/planB/modules/xianYu" + + planBType "planA/planB/type" + planAType "planA/type" + + "golang.org/x/time/rate" + "gorm.io/gorm" +) + +var ( + Ctx context.Context // 全局上下文 + Speed *rate.Limiter // 全局令牌桶限速器 + Config planAType.Config // 全局配置 + Redis planBType.Redis // 全局 Redis + Task *planBType.Task // 全局任务 + Pool planBType.Pool // 全局线程池 + Logic planBType.Logic // 全局逻辑控制 + Platform interfaces.GoodsTask // 全局平台对象 + TaskType string // 全局任务类型 + MinIo *planBType.MinIOClient // 全局 MinIO + PddDll *pdd.PddDLL // 全局拼多多 DLL + ImageDll *image.ImageDLL // 全局 ImageDll + XianYuDll *xianYuDll.XianYuDLL // 全局 闲鱼 DLL + LogDll *logs.LoggerDLL // 全局日志 DLL + KfzDll *kfz.KfzDLL // 全局孔夫子 DLL + MysqlDb *gorm.DB // 全局 mysql + KfzGetCommonCategory map[string]string // 孔夫子商品分类列表 +) + +// 任务 body 状态 +const ( + BodyStatusSuccess int64 = 1 // 正常 + BodyStatusError int64 = 2 // 错误 +) + +// 任务类型 +const ( + TaskTypeAddGoodsTask string = "AddGoodsTask" // 添加商品 + TaskTypeGetGoodsTask string = "GetGoodsTask" // 获取商品 + TaskTypeSetGoodsTask string = "SetGoodsTask" // 修改商品 + TaskTypeOperationGoodsTask string = "OperationGoodsTask" // 操作商品 + TaskTypeIncStock string = "IncStock" // 增量库存 +) + +// 错误集 +const ( + LastIndexRedisNil int64 = 10001 // redis 多次读Nil + LastIndexGoodsMaxRestriction int64 = 11002 // 店铺已达到最大商品限制 + LastIndexFilteWordErr int64 = 10003 // 过滤关键词异常 +) diff --git a/planB/initialization/init.go b/planB/initialization/init.go new file mode 100644 index 0000000..df7efda --- /dev/null +++ b/planB/initialization/init.go @@ -0,0 +1,95 @@ +package initialization + +import ( + "context" + "fmt" + "planA/planB/initialization/config" + "planA/planB/initialization/dll" + "planA/planB/initialization/golabl" + "planA/planB/initialization/kfz" + "planA/planB/initialization/minIo" + "planA/planB/initialization/mysql" + "planA/planB/initialization/platform" + "planA/planB/initialization/pool" + "planA/planB/initialization/redis" + "planA/planB/initialization/speed" + "planA/planB/initialization/task" + "planA/planB/initialization/taskType" + "planA/planB/initialization/title" + planBType "planA/planB/type" + planAType "planA/type" +) + +// Init 初始化 +func Init(taskId string) error { + //初始化上下文 + if golabl.Ctx == nil { + golabl.Ctx = context.Background() + } + + // 初始化配置文件 + if configErr := config.GetConfigSetToG(); configErr != nil { + return fmt.Errorf("初始化配置文件失败:%v", configErr) + } + + // 初始化 redis + if redisErr := redis.LinkRedisSetToG(); redisErr != nil { + return fmt.Errorf("初始化redis失败: %v", redisErr) + } + + // 初始化 mysql + if mysqlErr := mysql.LikeMysqlSetToG(); mysqlErr != nil { + return fmt.Errorf("初始化mysql失败: %v", mysqlErr) + } + + // 初始化 task + golabl.Task = &planBType.Task{ + TaskId: taskId, + Header: &planAType.TaskHeader{}, + Footer: &planAType.TaskFooter{}, + BodyWait: &planAType.TaskBody{}, + BodyOver: &planAType.TaskBody{}, + BodyBackup: &planAType.TaskBody{}, + } + if taskErr := task.GetTaskHeaderAndFooterSetToG(); taskErr != nil { + return fmt.Errorf("初始化任务失败: %v", taskErr) + } + + // 初始化限速器 + speed.Init() + + // 初始化 协程池 + if poolErr := pool.CreatePoolToG(); poolErr != nil { + return fmt.Errorf("初始化协程池失败: %v", poolErr) + } + + // 初始化平台 + if platformErr := platform.GetPlatformSetToG(); platformErr != nil { + return fmt.Errorf("初始化平台失败: %v", platformErr) + } + + // 初始化任务类型 + if taskTypeErr := taskType.GetTaskTypeSetToG(); taskTypeErr != nil { + return fmt.Errorf("初始化任务类型失败: %v", taskTypeErr) + } + + // 初始化图片空间 + if newMinIOClientErr := minIo.NewMinIOClient(); newMinIOClientErr != nil { + return fmt.Errorf("初始化图片空间失败: %v", newMinIOClientErr) + } + + // 初始化 DLL + if dllErr := dll.GetDllSetToG(); dllErr != nil { + return fmt.Errorf("初始化DLL失败: %v", dllErr) + } + + // 初始化 孔夫子公共分类 + if getKfzGoodsCategorySetToGErr := kfz.GetKfzGetCommonCategorySetToG(); getKfzGoodsCategorySetToGErr != nil { + return fmt.Errorf("初始化孔夫子公共分类失败: %v", getKfzGoodsCategorySetToGErr) + } + + //设置窗口标题 + title.SetWinTitle() + + return nil +} diff --git a/planB/initialization/kfz/kfz.go b/planB/initialization/kfz/kfz.go new file mode 100644 index 0000000..2bfed20 --- /dev/null +++ b/planB/initialization/kfz/kfz.go @@ -0,0 +1,95 @@ +package kfz + +import ( + "encoding/json" + "fmt" + "planA/planB/initialization/golabl" + planBTypeKfz "planA/planB/type/kfz" +) + +func GetKfzGetCommonCategorySetToG() error { + if golabl.Task.Header.ShopType == "2" { + //获取孔夫子商品分类 + goodsCategoryList, getGoodsCategoryListErr := golabl.KfzDll.GetCommonCategory(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token) + if getGoodsCategoryListErr != nil { + return getGoodsCategoryListErr + } + //转为结构体 + var kfzGoodsCategoryList planBTypeKfz.KfzCategoryRet + unmarshalErr := json.Unmarshal([]byte(goodsCategoryList), &kfzGoodsCategoryList) + if unmarshalErr != nil { + return unmarshalErr + } + //判断是否错误 + if kfzGoodsCategoryList.ErrorResponse != nil { + return fmt.Errorf("获取商品公共分类失败 %v", kfzGoodsCategoryList.ErrorResponse) + } + //设置为全局 + golabl.KfzGetCommonCategory = make(map[string]string) + + // 使用递归函数遍历所有分类,传入路径前缀 + for _, level1 := range kfzGoodsCategoryList.SuccessResponse { + collectCategoriesWithPath(level1, "") + } + } + return nil +} + +// 递归收集分类,带路径 +func collectCategoriesWithPath(category interface{}, parentPath string) { + switch v := category.(type) { + case planBTypeKfz.CategoryLevel1: + currentPath := v.Name + if v.Name != "" && v.Id != "" { + golabl.KfzGetCommonCategory[currentPath] = v.Id + } + for _, child := range v.Children { + collectCategoriesWithPath(child, currentPath) + } + case planBTypeKfz.CategoryLevel2: + currentPath := buildPath(parentPath, v.Name) + if v.Name != "" && v.Id != "" { + golabl.KfzGetCommonCategory[currentPath] = v.Id + } + for _, child := range v.Children { + collectCategoriesWithPath(child, currentPath) + } + case planBTypeKfz.CategoryLevel3: + currentPath := buildPath(parentPath, v.Name) + if v.Name != "" && v.Id != "" { + golabl.KfzGetCommonCategory[currentPath] = v.Id + } + for _, child := range v.Children { + collectCategoriesWithPath(child, currentPath) + } + case planBTypeKfz.CategoryLevel4: + currentPath := buildPath(parentPath, v.Name) + if v.Name != "" && v.Id != "" { + golabl.KfzGetCommonCategory[currentPath] = v.Id + } + for _, child := range v.Children { + collectCategoriesWithPath(child, currentPath) + } + case planBTypeKfz.CategoryLevel5: + currentPath := buildPath(parentPath, v.Name) + if v.Name != "" && v.Id != "" { + golabl.KfzGetCommonCategory[currentPath] = v.Id + } + for _, child := range v.Children { + collectCategoriesWithPath(child, currentPath) + } + case planBTypeKfz.CategoryLevel6: + currentPath := buildPath(parentPath, v.Name) + if v.Name != "" && v.Id != "" { + golabl.KfzGetCommonCategory[currentPath] = v.Id + } + } +} + +// 构建路径,用 / 连接 +func buildPath(parentPath, currentName string) string { + if parentPath == "" { + return currentName + } + return parentPath + "/" + currentName +} diff --git a/planB/initialization/minIo/minIo.go b/planB/initialization/minIo/minIo.go new file mode 100644 index 0000000..72982c0 --- /dev/null +++ b/planB/initialization/minIo/minIo.go @@ -0,0 +1,30 @@ +package minIo + +import ( + "planA/planB/initialization/golabl" + PlanBType "planA/planB/type" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +// NewMinIOClient 创建 MinIO 客户端实例 +func NewMinIOClient() error { + client, newMinIoErr := minio.New(golabl.Config.Minio.Url, &minio.Options{ + Creds: credentials.NewStaticV4(golabl.Config.Minio.AccessKeyID, golabl.Config.Minio.SecretAccessKey, ""), + Secure: false, + }) + if newMinIoErr != nil { + return newMinIoErr + } + + golabl.MinIo = &PlanBType.MinIOClient{ + Client: client, + Endpoint: golabl.Config.Minio.Url, + AccessKey: golabl.Config.Minio.AccessKeyID, + SecretKey: golabl.Config.Minio.SecretAccessKey, + UseSSL: golabl.Config.Minio.UseSSL, + BucketName: golabl.Config.Minio.BucketName, + } + return nil +} diff --git a/planB/initialization/mysql/mysql.go b/planB/initialization/mysql/mysql.go new file mode 100644 index 0000000..a174e32 --- /dev/null +++ b/planB/initialization/mysql/mysql.go @@ -0,0 +1,73 @@ +package mysql + +import ( + "fmt" + "planA/planB/initialization/golabl" + "time" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// LikeMysqlSetToG 链接mysql并保留到全局变量中 +func LikeMysqlSetToG() error { + + // 1. 获取mysql配置 + mysqlConfig := golabl.Config.MysqlConfig + + // 2. 配置 DSN + dsn := fmt.Sprintf( + "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + mysqlConfig.User, + mysqlConfig.Password, + mysqlConfig.Host, + mysqlConfig.Port, + mysqlConfig.DBName, + ) + + // 3. 配置 GORM 连接选项 + + logLevel := logger.Silent + switch mysqlConfig.Loglevel { + case "info": + logLevel = logger.Info + case "warn": + logLevel = logger.Warn + case "error": + logLevel = logger.Error + case "silent": + logLevel = logger.Silent + } + + gormConfig := &gorm.Config{ + Logger: logger.Default.LogMode(logLevel), //日志级别 + DisableForeignKeyConstraintWhenMigrating: true, //不创建外键约束 + } + + // 4. 连接数据库 + db, openErr := gorm.Open(mysql.Open(dsn), gormConfig) + if openErr != nil { + return openErr + } + + // 5. 获取底层 sql.DB,配置连接池 + sqlDB, dbErr := db.DB() + if dbErr != nil { + return dbErr + } + // 连接池优化 + 保活配置 + sqlDB.SetMaxOpenConns(mysqlConfig.MaxOpenConns) + sqlDB.SetMaxIdleConns(mysqlConfig.MaxIdleConns) + sqlDB.SetConnMaxIdleTime(mysqlConfig.ConnMaxIdleTime * time.Minute) + sqlDB.SetConnMaxLifetime(mysqlConfig.ConnMaxLifetime * time.Hour) + + // 5. 验证连接 + if dbPingErr := sqlDB.Ping(); dbPingErr != nil { + return dbPingErr + } + + // 7. 保存db实例 + golabl.MysqlDb = db + return nil +} diff --git a/planB/initialization/platform/platform.go b/planB/initialization/platform/platform.go new file mode 100644 index 0000000..3eb3c7e --- /dev/null +++ b/planB/initialization/platform/platform.go @@ -0,0 +1,30 @@ +package platform + +import ( + "errors" + "planA/planB/dispatcher/kongfuzi" + pinDuoDuo "planA/planB/dispatcher/pinduoduo" + "planA/planB/dispatcher/taobao" + "planA/planB/dispatcher/xianyu" + "planA/planB/initialization/golabl" +) + +// GetPlatformSetToG 获取平台并保存到全局变量中 +func GetPlatformSetToG() error { + switch golabl.Task.Header.ShopType { + case "1": + golabl.Platform = pinDuoDuo.NewPinDuoDuo() + return nil + case "2": + golabl.Platform = kongfuzi.NewKongFuZi() + return nil + case "5": + golabl.Platform = xianyu.NewXianYu() + return nil + case "6": + golabl.Platform = taobao.NewTaobao() + return nil + default: + return errors.New("错误!") + } +} diff --git a/planB/initialization/pool/pool.go b/planB/initialization/pool/pool.go new file mode 100644 index 0000000..73a2680 --- /dev/null +++ b/planB/initialization/pool/pool.go @@ -0,0 +1,30 @@ +package pool + +import ( + "fmt" + "planA/planB/initialization/golabl" + "sync" + "time" + + "github.com/panjf2000/ants/v2" +) + +// CreatePoolToG 创建协程池到全局变量中 +// @return error 错误信息 +func CreatePoolToG() error { + // 创建协程池 + pool, err := ants.NewPool( + golabl.Config.PoolConfig.Size, + ants.WithExpiryDuration(time.Duration(golabl.Config.PoolConfig.WithExpiryDuration)*time.Second), // 过期时间 + ants.WithPreAlloc(golabl.Config.PoolConfig.WithPreAlloc), // 预分配 + ants.WithMaxBlockingTasks(golabl.Config.PoolConfig.WithMaxBlockingTasks), // 最大阻塞任务数 + ants.WithNonblocking(golabl.Config.PoolConfig.WithNonblocking), // 非阻塞 + ants.WithPanicHandler(func(p interface{}) { fmt.Printf("panic: %v", p) }), // panic 处理 + ) + if err != nil { + return err + } + golabl.Pool.Pool = pool + golabl.Pool.Wg = &sync.WaitGroup{} + return nil +} diff --git a/planB/initialization/redis/redis.go b/planB/initialization/redis/redis.go new file mode 100644 index 0000000..9796a41 --- /dev/null +++ b/planB/initialization/redis/redis.go @@ -0,0 +1,82 @@ +package redis + +import ( + "fmt" + "planA/planB/initialization/golabl" + planAType "planA/type" + "time" + + "github.com/go-redis/redis/v8" +) + +// LinkRedisSetToG 链接redis并保留到全局变量中 +// @return error 错误信息 +func LinkRedisSetToG() error { + + // 1. 获取redis配置 + redisConfig := golabl.Config.RedisConfig + redisClientA, redisErr := NewRedisClient(redisConfig[0]) + if redisErr != nil { + return fmt.Errorf("初始化 redis %v db%v 失败: %v\n", redisConfig[0].Addr, redisConfig[0].DB, redisErr) + } + golabl.Redis.RedisDbA = redisClientA + // Redis B - Redis实例 + redisClientB, redisErr := NewRedisClient(redisConfig[3]) + if redisErr != nil { + return fmt.Errorf("初始化 redis %v db%v 失败: %v\n", redisConfig[3].Addr, redisConfig[3].DB, redisErr) + } + golabl.Redis.RedisDbB = redisClientB + + // Redis C - Redis实例 + redisClientC, redisErr := NewRedisClient(redisConfig[4]) + if redisErr != nil { + return fmt.Errorf("初始化 redis %v db%v 失败: %v\n", redisConfig[4].Addr, redisConfig[4].DB, redisErr) + } + golabl.Redis.RedisDbC = redisClientC + + // Redis D - Redis实例 + redisClientD, redisErr := NewRedisClient(redisConfig[5]) + if redisErr != nil { + return fmt.Errorf("初始化 redis %v db%v 失败: %v\n", redisConfig[5].Addr, redisConfig[5].DB, redisErr) + } + golabl.Redis.RedisDbD = redisClientD + + // Redis E - Redis实例 + redisClientE, redisErr := NewRedisClient(redisConfig[2]) + if redisErr != nil { + return fmt.Errorf("初始化 redis %v db%v 失败: %v\n", redisConfig[2].Addr, redisConfig[2].DB, redisErr) + } + golabl.Redis.RedisDbE = redisClientE + + return nil +} + +// NewRedisClient 创建redis 客户端 +// @param config redis配置 +// @return *redis.Client redis客户端 +// @return error 错误信息 +func NewRedisClient(config planAType.RedisConfig) (*redis.Client, error) { + ctx := golabl.Ctx + rdb := redis.NewClient(&redis.Options{ + Addr: config.Addr, // 连接地址 + Password: config.Password, // 密码 + DB: config.DB, // 数据库 + PoolSize: config.PoolSize, // 连接池大小 + PoolTimeout: time.Duration(config.PoolTimeout), // 连接池超时时间 + ReadTimeout: time.Duration(config.ReadTimeout), // 读取超时 + WriteTimeout: time.Duration(config.WriteTimeout), // 写入超时 + DialTimeout: time.Duration(config.DialTimeout), // 连接超时 + IdleTimeout: time.Duration(config.IdleTimeout), // 空闲超时 + MinIdleConns: config.MinIdleConns, // 最小空闲连接数 + IdleCheckFrequency: time.Duration(config.IdleCheckFrequency), // 空闲检查频率 + MaxRetries: config.MaxRetries, // 最大重试次数 + MaxRetryBackoff: time.Duration(config.MaxRetryBackoff), // 最大重试间隔 + MinRetryBackoff: time.Duration(config.MinRetryBackoff), // 最小重试间隔 + }) + // 测试连接 + _, err := rdb.Ping(ctx).Result() + if err != nil { + return rdb, err + } + return rdb, nil +} diff --git a/planB/initialization/speed/speed.go b/planB/initialization/speed/speed.go new file mode 100644 index 0000000..ba12617 --- /dev/null +++ b/planB/initialization/speed/speed.go @@ -0,0 +1,32 @@ +package speed + +import ( + "planA/planB/initialization/golabl" + + "golang.org/x/time/rate" +) + +// Init 初始化 限速器 +func Init() { + //默认为18 + speed := 18 + //根据平台设置速率 + switch golabl.Task.Header.ShopType { + case "1": + speed = golabl.Config.Speed.PddSpeed + //case 2: + case "5": + speed = golabl.Config.Speed.XianyuSpeed + default: + speed = 18 + } + //如果需要打水印,则速率下降为10 + if golabl.Task.Header.ShopMsg.WatermarkImgUrl != "" && golabl.Task.Header.ShopType == "1" { + speed = golabl.Config.Speed.Watermark + if speed == 0 { + speed = 10 + } + } + //初始化限速器 + golabl.Speed = rate.NewLimiter(rate.Limit(speed), 1) +} diff --git a/planB/initialization/task/task.go b/planB/initialization/task/task.go new file mode 100644 index 0000000..1385063 --- /dev/null +++ b/planB/initialization/task/task.go @@ -0,0 +1,20 @@ +package task + +import ( + "fmt" + "planA/planB/service" +) + +// GetTaskHeaderAndFooterSetToG 获取任务头和尾并保存到全局变量中 +// @return error 错误信息 +func GetTaskHeaderAndFooterSetToG() error { + // 获取任务头 + if err := service.GetTaskHeader(); err != nil { + return fmt.Errorf("获取任务头失败 %v", err) + } + // 获取任务尾 + if err := service.GetTaskFooter(); err != nil { + return fmt.Errorf("获取任务尾失败 %v", err) + } + return nil +} diff --git a/planB/initialization/taskType/taskType.go b/planB/initialization/taskType/taskType.go new file mode 100644 index 0000000..2ac8ceb --- /dev/null +++ b/planB/initialization/taskType/taskType.go @@ -0,0 +1,44 @@ +package taskType + +import ( + "errors" + "fmt" + "planA/planB/initialization/golabl" +) + +// GetTaskTypeSetToG 获取任务类型并保存到全局变量中 +// @return error 错误信息 +func GetTaskTypeSetToG() error { + switch golabl.Task.Header.TaskType { + case 1: //核价发布 + golabl.TaskType = golabl.TaskTypeAddGoodsTask + return nil + case 2: //表格发布 + golabl.TaskType = golabl.TaskTypeAddGoodsTask + return nil + case 3: //获取商品 + golabl.TaskType = golabl.TaskTypeGetGoodsTask + return nil + case 4: //获取拼多多详情商品 + golabl.TaskType = golabl.TaskTypeGetGoodsTask + return nil + case 5: //操作商品 + golabl.TaskType = golabl.TaskTypeOperationGoodsTask + return nil + case 6: //核价表格发布 + golabl.TaskType = golabl.TaskTypeAddGoodsTask + return nil + case 7: //增量库存 + golabl.TaskType = golabl.TaskTypeIncStock + return nil + case 8: //自营书品发布 + golabl.TaskType = golabl.TaskTypeAddGoodsTask + return nil + case 9: //核价改价 + golabl.TaskType = golabl.TaskTypeOperationGoodsTask + return nil + default: + fmt.Println(golabl.Task.Header.TaskType) + return errors.New("错误!") + } +} diff --git a/planB/initialization/title/title.go b/planB/initialization/title/title.go new file mode 100644 index 0000000..b2ae908 --- /dev/null +++ b/planB/initialization/title/title.go @@ -0,0 +1,82 @@ +package title + +import ( + "fmt" + "planA/planB/initialization/golabl" + "syscall" + "time" + "unsafe" +) + +// SetWinTitle 设置窗口标题 +func SetWinTitle() { + title := "" + + //平台 + switch golabl.Task.Header.ShopType { + case "1": + title = title + "【拼多多】" + case "2": + title = title + "【孔夫子】" + case "5": + title = title + "【闲鱼】" + default: + title = title + "【其他平台 " + golabl.Task.Header.ShopType + "】" + } + + //店铺名称 + title = title + "【" + golabl.Task.Header.ShopName + "】" + + //任务类型 + switch golabl.Task.Header.TaskType { + case 1: + title = title + "【核价发布】" + case 2: + title = title + "【表格发布】" + case 3: + title = title + "【拉取商品】" + case 4: + title = title + "【拉取商品详情】" + case 5: + title = title + "【操作商品】" + case 6: + title = title + "【核价表格发布】" + case 7: + title = title + "【增量库存】" + default: + title = title + "【其他任务类型 " + fmt.Sprint(golabl.Task.Header.TaskType) + "】" + } + + //图片类型 + switch golabl.Task.Header.ImgType { + case 1: + title = title + "【仅官图】" + case 2: + title = title + "【实拍图】" + case 3: + title = title + "【优先官图】" + case 4: + title = title + "【优先实拍图】" + default: + title = title + "【其他图片类型 " + fmt.Sprint(golabl.Task.Header.ImgType) + "】" + } + + //创建时间 + createTime := time.Unix(golabl.Task.Header.TaskCreateAt, 0) + timeStr := createTime.Format("2006-01-02 15:04:05") + title = title + "【创建时间 " + timeStr + "】" + + //任务 id + title = title + golabl.Task.Header.TaskId + setConsoleTitle(title) +} + +// SetConsoleTitle 设置窗口标题 +// @param title 标题 +func setConsoleTitle(title string) { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + procSetConsoleTitle := kernel32.NewProc("SetConsoleTitleW") + // 将字符串转换为UTF-16指针 + titlePtr, _ := syscall.UTF16PtrFromString(title) + procSetConsoleTitle.Call(uintptr(unsafe.Pointer(titlePtr))) +} diff --git a/planB/interfaces/interfaces.go b/planB/interfaces/interfaces.go new file mode 100644 index 0000000..bc21c60 --- /dev/null +++ b/planB/interfaces/interfaces.go @@ -0,0 +1,23 @@ +package interfaces + +import ( + planAType "planA/type" +) + +// GoodsTask 商品任务接口 +type GoodsTask interface { + // AddGoodsTask 添加商品任务 + AddGoodsTask(bodyWait planAType.TaskBody) (string, error) + + // SetGoodsTask 设置商品任务 + SetGoodsTask() string + + // GetGoodsTask 获取商品任务 + GetGoodsTask() (string, error) + + // OperationGoodsTask 操作商品任务 + OperationGoodsTask(bodyWait planAType.TaskBody) (string, error) + + // IncStockTask 增量库存 + IncStockTask(bodyWait planAType.TaskBody) (string, error) +} diff --git a/planB/logic/logic.go b/planB/logic/logic.go new file mode 100644 index 0000000..a660afc --- /dev/null +++ b/planB/logic/logic.go @@ -0,0 +1,410 @@ +package logic + +import ( + "encoding/json" + "errors" + "fmt" + "planA/planB/dispatcher" + "planA/planB/initialization/config" + "planA/planB/initialization/golabl" + "planA/planB/initialization/task" + "planA/planB/modules/logs" + "planA/planB/service" + "planA/planB/tool" + planAType "planA/type" + planATypeMysql "planA/type/mysql" + redisType "planA/type/redis" + "strings" + "sync/atomic" + "time" + + "github.com/go-redis/redis/v8" +) + +var Goto bool = false + +// Logic 执行任务 +func Logic() { + //loop: + // 开始读取待处理任务 等待任务数必须大于0 + for golabl.Task.Footer.TaskCountWait.Load() > 0 { + // 任务索引 + atomic.AddInt64(&golabl.Logic.TaskIndex, 1) + + //TODO 在更新config方法出去后应该去除该代码 每次重新获取配置文件 + if configErr := config.GetConfigSetToG(); configErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, configErr.Error()) + return + } + + // 使用令牌桶进行速率控制(每秒20个) + if err := golabl.Speed.Wait(golabl.Ctx); err != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("令牌桶等待失败-原因来自于:%v", err)) + continue + } + + //TODO 重新获取任务头尾 + if taskErr := task.GetTaskHeaderAndFooterSetToG(); taskErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, taskErr.Error()) + continue + } + + // 如果连续读出 redisNil 的次数大于100 + if atomic.LoadInt64(&golabl.Logic.RedisNilCon) > 100 { + //Goto = true + + // 等待所有任务完成 暂停 5 秒 + golabl.Pool.Wg.Wait() + fmt.Println("等待当前所有协程完成后 暂停5秒,如果等待的任务真的是0的话,则通知A完成任务!") + time.Sleep(5 * time.Second) + + //获取任务真实的 wait数量 + count, getTaskBodyWaitCountErr := service.GetTaskBodyWaitCount() + if getTaskBodyWaitCountErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("获取任务任务真实的 wait数量失败-原因来自:%v", getTaskBodyWaitCountErr)) + return + } + // 如果数量真的是0,则完成任务 + if count == 0 { + break + } + + atomic.StoreInt64(&golabl.Logic.RedisNilCon, 0) + } + + // 创建等待 + golabl.Pool.Wg.Add(1) + + //协程池 提交 + if golabl.Task.Header.TaskType == 7 { + // 单线程执行 + taskExecute() + if taskPoolErr := golabl.Pool.Pool.Submit(taskExecute); taskPoolErr != nil { + golabl.Pool.Wg.Done() + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("协程池意外-原因来自:%d", taskPoolErr)) + } else { + golabl.Pool.Wg.Done() + } + } else { + // 多线程执行 + if taskPoolErr := golabl.Pool.Pool.Submit(func() { + defer golabl.Pool.Wg.Done() + taskExecute() + }); taskPoolErr != nil { + golabl.Pool.Wg.Done() + } + } + + // 判断 任务数是否超过1000 并且 判断是否执行到了1000的倍数 + if golabl.Task.Header.TaskCountTrue > 1000 && golabl.Logic.TaskIndex%1000 == 0 { + // 更新任务头部信息 + updateTaskHeaderErr := tool.UpdateTaskHeader() + if updateTaskHeaderErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("更新任务头信息失败-原因来自:%v", updateTaskHeaderErr)) + } + } + } + + // 等待所有任务完成 + golabl.Pool.Wg.Wait() + + //等待指定时间后重新执行循环 + //if Goto == true { + // golabl.Logic.RedisNilCon = 0 + // golabl.Logic.LastIndex = golabl.LastIndexRedisNil + // fmt.Printf("连续读出 redisNil 的次数 %v 暂停%v毫秒", golabl.Logic.RedisNilCon, golabl.Config.Server.ErrPauseTime) + // time.Sleep(time.Duration(golabl.Config.Server.ErrPauseTime) * time.Millisecond) + // goto loop + //} + + // 更新任务头部信息 + if updateTaskHeaderErr := tool.UpdateTaskHeader(); updateTaskHeaderErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("更新任务头信息失败-原因来自:%v", updateTaskHeaderErr)) + } + + // 通知 A程序任务完成 + httpTaskStatusOverErr := tool.NotifyA() + if httpTaskStatusOverErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, httpTaskStatusOverErr.Error()) + } + + // 延迟2分钟 + time.Sleep(2 * time.Minute) +} + +// 任务执行 +func taskExecute() { + //初始化 变量 + status := golabl.BodyStatusSuccess //默认的书籍执行状态· + errorStr := "执行成功" //默认的书籍执行描述 + + // 获取任务信息 + taskMsg, taskMsgErr := service.GetTaskToPopFromBodyWait() + + if errors.Is(taskMsgErr, redis.Nil) { + //redis 读nil空+1 + fmt.Printf("第 %v 次读出 Redis Nil \n", atomic.LoadInt64(&golabl.Logic.RedisNilCon)) + atomic.AddInt64(&golabl.Logic.RedisNilCon, 1) + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("获取任务信息失败-原因来自:%v", taskMsgErr)) + return + } else if taskMsgErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("获取任务信息失败-原因来自:%v", taskMsgErr)) + return + } + + //设置混合任务成功状态 + if golabl.Task.Header.TaskType == 5 || golabl.Task.Header.TaskType == 9 { + switch taskMsg.Detail.Status { + case 1: + errorStr = "设置商品上架 " + errorStr + //执行任务 + status, errorStr, taskMsg = exeTask(taskMsg, status, errorStr) + case 2: + errorStr = "设置商品下架 " + errorStr + //执行任务 + status, errorStr, taskMsg = exeTask(taskMsg, status, errorStr) + case 3: + //删除商品的任务存储到 mysql中 + //删除商品 {"book_info":{"isbn":"9787543982888"},"detail":{"goods_id":935670364385,"status":3}} + DelTask(taskMsg) + errorStr = "删除商品 已转转移至删除中心" + case 4: + errorStr = "修改商品库存 " + errorStr + //执行任务 + status, errorStr, taskMsg = exeTask(taskMsg, status, errorStr) + case 5: + errorStr = "修改商品价格 " + errorStr + //执行任务 + status, errorStr, taskMsg = exeTask(taskMsg, status, errorStr) + case 6: + errorStr = "发布商品 " + errorStr + //执行任务 + status, errorStr, taskMsg = exeTask(taskMsg, status, errorStr) + case 7: + errorStr = "删除并重新发布 " + errorStr + //执行任务 + status, errorStr, taskMsg = exeTask(taskMsg, status, errorStr) + + default: + //执行任务 + status, errorStr, taskMsg = exeTask(taskMsg, status, errorStr) + } + // 更新任务信息 + taskMsg.Detail.Status = status + taskMsg.Detail.Error = errorStr + } else if golabl.Task.Header.TaskType == 7 { + //执行任务 + status, errorStr, taskMsg = exeTask(taskMsg, status, errorStr) + taskMsg.Detail.Status = status + if status != 1 { + taskMsg.Detail.Error = errorStr + } + } else { + //执行任务 + status, errorStr, taskMsg = exeTask(taskMsg, status, errorStr) + // 更新任务信息 + taskMsg.Detail.Status = status + taskMsg.Detail.Error = errorStr + } + + //isbn 不为空的添加到body中,比如拉取店铺商品信息isbn可以返回空的 + if taskMsg.BookInfo.Isbn != "" && (golabl.TaskType == "3" || golabl.TaskType == "4") { + // 添加任务到bodyOver、bodyData、bodyBackup + if addTaskToBodyOverErr := service.AddTaskToBodyOver(taskMsg, []string{}); addTaskToBodyOverErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("任务失败 添加到BodyOver失败-原因:%v", addTaskToBodyOverErr)) + } + } else { + if taskMsg.BookInfo.Isbn == "" && taskMsg.BookInfo.BookName == "" { + taskMsg.BookInfo.BookName = "暂无书品信息" + } + // 添加任务到bodyOver、bodyData、bodyBackup + if addTaskToBodyOverErr := service.AddTaskToBodyOver(taskMsg, []string{}); addTaskToBodyOverErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("任务失败 添加到BodyOver失败-原因:%v", addTaskToBodyOverErr)) + } + } + + // 更新 footer信息 + if updateTaskFooterErr := service.UpdateTaskFooter(status, 1); updateTaskFooterErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("任务失败 添加到BodyOver失败-原因:%v", updateTaskFooterErr)) + } + + // 如果错误是 店铺商品发布达到上限则暂停程序 + if strings.Contains(errorStr, "店铺内发布商品总数已达到上限") { + golabl.Task.Header.LastIndex = golabl.LastIndexGoodsMaxRestriction + //暂停 B程序运行 + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "任务失败 添加到BodyOver失败-原因:店铺内发布商品总数已达到上限") + pauseTaskErr := tool.PauseTask() + if pauseTaskErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, "任务失败 添加到BodyOver失败-原因:店铺内发布商品总数已达到上限") + } + } + + fmt.Println(errorStr) +} + +//****************************工具**************************************// + +// parseShopData 解析店铺数据 +// @param shopData 店铺数据 +// @return *_type.ShopInfo 店铺信息 +func parseShopData(shopData string) (*planAType.ShopInfo, error) { + shopData = strings.TrimSpace(shopData) + + // 直接解析为 RedisData数组 + var redisData []redisType.RedisData + err := json.Unmarshal([]byte(shopData), &redisData) + if err != nil { + // 尝试另一种格式:可能是单对象而不是数组 + var singleData redisType.RedisData + if singleErr := json.Unmarshal([]byte(shopData), &singleData); singleErr == nil { + redisData = []redisType.RedisData{singleData} + } else { + return nil, fmt.Errorf("JSON解析失败: %v, 原始数据: %s", err, shopData[:min(100, len(shopData))]) + } + } + + shopInfo := &planAType.ShopInfo{} + + // 遍历所有数据,根据source_table分类 + for _, item := range redisData { + switch item.SourceTable { + case "t_shop": + var shop planAType.Shop + if err := json.Unmarshal(item.Data, &shop); err == nil { + shopInfo.Shop = &shop + } else { + fmt.Printf("解析t_shop失败: %v\n", err) + } + case "t_shop_detail": + var detail planAType.ShopDetail + if err := json.Unmarshal(item.Data, &detail); err == nil { + shopInfo.ShopDetail = &detail + } else { + fmt.Printf("解析t_shop_detail失败: %v\n", err) + } + case "t_shop_context": + var context planAType.ShopContext + if err := json.Unmarshal(item.Data, &context); err == nil { + shopInfo.ShopContext = &context + } else { + fmt.Printf("解析t_shop_context失败: %v\n", err) + } + case "t_spec": + var spec planAType.Spec + if err := json.Unmarshal(item.Data, &spec); err == nil { + shopInfo.Spec = &spec + } else { + fmt.Printf("解析t_spec失败: %v\n", err) + } + case "t_price_template": + var template planAType.PriceTemplate + if err := json.Unmarshal(item.Data, &template); err == nil { + shopInfo.PriceTemplate = &template + } else { + fmt.Printf("解析t_price_template失败: %v\n", err) + } + default: + fmt.Printf("未知的source_table: %s\n", item.SourceTable) + } + } + + return shopInfo, nil +} + +// 调度任务 +func exeTask(taskMsg planAType.TaskBody, status int64, errorStr string) (int64, string, planAType.TaskBody) { + // 任务调度 + bodyOverJson, err := dispatcher.Go(taskMsg) + if err != nil { + //任务调度失败 + status = golabl.BodyStatusError + errorStr = fmt.Sprintf("任务调度失败-原因来自:%v", err) + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("任务调度失败-原因来自:%v", err)) + } else { + //任务调度成功 + var bodyOver planAType.TaskBody + unmarshalErr := json.Unmarshal([]byte(bodyOverJson), &bodyOver) + if unmarshalErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("bodyOver json.Unmarshal错误-原因:%v", unmarshalErr)) + } + //更新 taskMsg + taskMsg = bodyOver + } + return status, errorStr, taskMsg +} + +// DelTask 删除任务 +func DelTask(taskMsg planAType.TaskBody) { + //删除商品的任务存储到 mysql中 + //删除商品 {"book_info":{"isbn":"9787543982888"},"detail":{"goods_id":935670364385,"status":3}} + delTask, isExistDelTask, delTaskErr := service.GetDelTaskByTaskId() + if !isExistDelTask && delTaskErr == nil { + taskCount := 0 + taskCountOver := 0 + sta := 0 + //将header 转为json + headerByte, headerJsonErr := json.Marshal(golabl.Task.Header) + if headerJsonErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("将header 转为json失败-原因来自:%v", headerJsonErr)) + return + } + headerJson := string(headerByte) + currentTime := time.Now() + // 查询店铺数据 + shopDataStr, getTaskShopErr := service.GetTaskShop(golabl.Task.Header.ShopId) + if getTaskShopErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("查询店铺数据失败:%v", headerJsonErr)) + return + } + // 解析 json数据 + shopData, parseShopDataErr := parseShopData(shopDataStr) + if parseShopDataErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("解析店铺数据失败:%v", parseShopDataErr)) + return + } + userId := shopData.Shop.CreateBy + taskType := 1 + //不存在 mysql任务则创建 + createDelTask := planATypeMysql.DelTask{ + UserID: &userId, + ShopID: &golabl.Task.Header.ShopId, + TaskID: &golabl.Task.Header.TaskId, + ShopName: &golabl.Task.Header.ShopName, + ShopType: &shopData.Shop.ShopType, + TaskCount: &taskCount, + TaskCountOver: &taskCountOver, + Status: &sta, + TaskType: &taskType, + Header: &headerJson, + CreateAt: ¤tTime, + } + var err error + delTask, err = service.CreateDelTask(createDelTask) + if err != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("创建删除任务失败-原因来自:%v", err)) + return + } + } else if delTaskErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("获取删除任务失败-原因来自:%v", delTaskErr)) + return + } + + //将任务状态修改为执行中 + updateDelTaskStatusToDoingErr := service.UpdateDelTaskStatusToDoing() + if updateDelTaskStatusToDoingErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("将删除任务状态修改为执行中失败-原因来自:%v", updateDelTaskStatusToDoingErr)) + return + } + // 将明细的删除任务转移到 mysql中 + insertDelTaskDetailErr := service.InsertDelTaskDetail(delTask.ID, taskMsg) + if insertDelTaskDetailErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("将明细的删除任务转移到 mysql中失败-原因来自:%v", insertDelTaskDetailErr)) + return + } + // 添加删除任务数量 + addDelTaskDetailCountErr := service.AddDelTaskDetailCount() + if addDelTaskDetailCountErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("添加删除任务数量失败-原因来自:%v", addDelTaskDetailCountErr)) + return + } +} diff --git a/planB/main.go b/planB/main.go new file mode 100644 index 0000000..a8304c8 --- /dev/null +++ b/planB/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + "planA/planB/initialization" + "planA/planB/initialization/golabl" + "planA/planB/logic" + "planA/planB/modules/logs" + "planA/planB/tool" + "planA/planB/validation" + //"planA/planB/initialization" + //"planA/planB/initialization/golabl" + //"planA/planB/logic" + //"planA/planB/modules/logs" + //"planA/planB/tool" + //"planA/planB/validation" + "time" +) + +func main() { + //校验参数 + taskId, validationErr := validation.Validation() + if validationErr != nil { + fmt.Println(validationErr) + return + } + + // 是否测试模式 + if taskId == "111" { + //test() + return + } + + // 初始化配置 + err := initialization.Init(taskId) + if err != nil { + fmt.Println(err) + return + } + + // 拉取商品列表与拼多多商品详情列表 + if golabl.Task.Header.TaskType == 3 || (golabl.Task.Header.TaskType == 4 && golabl.Task.Header.ShopType == "1") { + _, getGoodsTask := golabl.Platform.GetGoodsTask() + if getGoodsTask != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, getGoodsTask.Error()) + } + // 通知 A程序任务完成 + httpTaskStatusOverErr := tool.NotifyA() + if httpTaskStatusOverErr != nil { + tool.LoggingMiddleware(logs.LOG_LEVEL_ERROR, httpTaskStatusOverErr.Error()) + } + //延迟3分钟,并且循环打印每秒倒计时 + totalSeconds := 180 // 3分钟 = 180秒 + for i := totalSeconds; i >= 0; i-- { + minutes := i / 60 + seconds := i % 60 + fmt.Printf("\r剩余时间: %02d:%02d", minutes, seconds) + if i > 0 { + time.Sleep(1 * time.Second) + } + } + } else { + // 执行任务 + logic.Logic() + } +} + +// 测试模式 +func test() { + //循环1000次 + for i := 0; i < 1000; i++ { + //每秒打印 i + fmt.Printf("i:%v\n", i) + time.Sleep(time.Second) + } +} diff --git a/planB/modules/config/config.dll b/planB/modules/config/config.dll new file mode 100644 index 0000000..4e1d91b Binary files /dev/null and b/planB/modules/config/config.dll differ diff --git a/planB/modules/config/conifg.go b/planB/modules/config/conifg.go new file mode 100644 index 0000000..a8138f3 --- /dev/null +++ b/planB/modules/config/conifg.go @@ -0,0 +1,74 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// ConfigDLL 配置文件读取DLL结构 +type ConfigDLL struct { + dll *syscall.DLL + readConfigFile *syscall.Proc // 读取配置文件 + getVersion *syscall.Proc // 获取版本信息 + freeCString *syscall.Proc // 释放C字符串 +} + +// InitConfigDLL 初始化ConfigDLL +func InitConfigDLL() (*ConfigDLL, error) { + dllPath := filepath.Join("modules/config/", "config.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("config DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载config DLL 失败: %s", err) + } else { + return &ConfigDLL{ + dll: dll, + readConfigFile: dll.MustFindProc("ReadConfigFile"), + getVersion: dll.MustFindProc("GetVersion"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +// cStr 获取C字符串 +func (m *ConfigDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} + +// ReadConfigFile 读取配置文件 +func (m *ConfigDLL) ReadConfigFile(filePath, fileName string) (string, error) { + proc, err := m.dll.FindProc("ReadConfigFile") + if err != nil { + return "", fmt.Errorf("找不到函数 ReadConfigFile: %v", err) + } + + filePathPtr, _ := syscall.BytePtrFromString(filePath) + fileNamePtr, _ := syscall.BytePtrFromString(fileName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(filePathPtr)), + uintptr(unsafe.Pointer(fileNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} diff --git a/planB/modules/image/image.dll b/planB/modules/image/image.dll new file mode 100644 index 0000000..1975e04 Binary files /dev/null and b/planB/modules/image/image.dll differ diff --git a/planB/modules/image/image.go b/planB/modules/image/image.go new file mode 100644 index 0000000..1e4bbee --- /dev/null +++ b/planB/modules/image/image.go @@ -0,0 +1,141 @@ +package image + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +var ( + gImageDll *ImageDLL + + // Windows API - 使用 C 运行时库 + libc = syscall.NewLazyDLL("msvcrt.dll") + procFree = libc.NewProc("free") + procMalloc = libc.NewProc("malloc") +) + +// ImageDLL 图片工具DLL结构 +type ImageDLL struct { + Dll *syscall.DLL + AddWatermarkFromURLEx *syscall.Proc // 打水印 +} + +// InitImageDll 初始化 imageDLL +func InitImageDll(url string) (*ImageDLL, error) { + dllPath := filepath.Join(url, "image.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("Image DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载Image DLL 失败: %s", err) + } + gImageDll = &ImageDLL{ + Dll: dll, + AddWatermarkFromURLEx: dll.MustFindProc("AddWatermarkFromURLEx"), + } + return gImageDll, nil +} + +// WatermarkConfig 添加水印 +type WatermarkConfig struct { + SourceImageURL string // 源图片URL地址 + WatermarkURL string // 水印图片URL地址 + WatermarkBase64 string // 水印图片base64编码字符串(新增,优先使用) + Opacity float64 // 不透明度 (0.0-1.0) + Position string // 位置: center, top-left, top-right, bottom-left, bottom-right, tile + TileSpacing int // 平铺时的间距 + Scale float64 // 水印缩放比例 (0.0-1.0) + Rotation float64 // 旋转角度 (度数) + XOffset int // X轴偏移量 + YOffset int // Y轴偏移量 + Timeout int // 下载超时时间(秒),默认30秒 + OutputFormat string // 输出格式: "jpeg", "png", "auto"(默认auto,根据源图片格式)auto + JPEGQuality int // JPEG质量 (1-100),默认95 + TargetWidth int // 目标宽度(0表示不缩放) + TargetHeight int // 目标高度(0表示不缩放) + ResizeMode string // 缩放模式: "fit"(适应,保持比例,可能有黑边), "fill"(填充,裁剪), "stretch"(拉伸) +} + +// AddWatermarkFromURLExs 添加水印 +func (m *ImageDLL) AddWatermarkFromURLExs(sourceImageUrl, watermarkUrl string) (string, error) { + watermarkConfig := WatermarkConfig{ + SourceImageURL: sourceImageUrl, + WatermarkBase64: watermarkUrl, + Position: "center", + Opacity: 1.0, + Scale: 1.0, + TileSpacing: 50, + Timeout: 30, + OutputFormat: "jpeg", + JPEGQuality: 95, + TargetWidth: 800, + TargetHeight: 800, + ResizeMode: "fit", + } + watermarkConfigJson, err := json.Marshal(watermarkConfig) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + + proc, err := m.Dll.FindProc("AddWatermarkFromURLEx") + if err != nil { + return "", fmt.Errorf("找不到函数 AddWatermarkFromURLEx: %v", err) + } + + // 分配内存并确保释放 + jsonStr := string(watermarkConfigJson) + jsonPtr := cString(jsonStr) + defer freeCString(jsonPtr) + + // 调用 DLL 函数 + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(jsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// cString 分配 C 字符串(使用 malloc) +func cString(str string) unsafe.Pointer { + // 计算需要的内存大小 + size := len(str) + 1 + ptr, _, _ := procMalloc.Call(uintptr(size)) + if ptr == 0 { + return nil + } + // 复制字符串内容 + for i := 0; i < len(str); i++ { + *(*byte)(unsafe.Pointer(ptr + uintptr(i))) = str[i] + } + *(*byte)(unsafe.Pointer(ptr + uintptr(len(str)))) = 0 // 结尾加 \0 + return unsafe.Pointer(ptr) +} + +// freeCString 释放 C 字符串 +func freeCString(ptr unsafe.Pointer) { + if ptr != nil { + procFree.Call(uintptr(ptr)) + } +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} diff --git a/planB/modules/kfz/kfz.dll b/planB/modules/kfz/kfz.dll new file mode 100644 index 0000000..a7464cc Binary files /dev/null and b/planB/modules/kfz/kfz.dll differ diff --git a/planB/modules/kfz/kfz.go b/planB/modules/kfz/kfz.go new file mode 100644 index 0000000..7023678 --- /dev/null +++ b/planB/modules/kfz/kfz.go @@ -0,0 +1,248 @@ +package kfz + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// KfzDLL 孔夫子工具DLL结构 +type KfzDLL struct { + Dll *syscall.DLL + freeCString *syscall.Proc // 释放C字符串 +} + +// InitKfzDll 初始化 kfzDLL +func InitKfzDll(url string) (*KfzDLL, error) { + dllPath := filepath.Join(url, "kfz.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("kfz DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } + gKfzDll := &KfzDLL{ + Dll: dll, + freeCString: dll.MustFindProc("FreeCString"), + } + return gKfzDll, nil +} + +// PublishGoods 发布商品 +func (m *KfzDLL) PublishGoods(appId int, clientSecret, accessToken, goodsAddJson string) (string, error) { + + proc, err := m.Dll.FindProc("KongfzShopItemAdd") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemAdd: %v", err) + } + + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsAddJsonPtr, _ := syscall.BytePtrFromString(goodsAddJson) + + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsAddJsonPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// KfzGoodsImageUpload 将图片上传到孔夫子图片空间 +func (m *KfzDLL) KfzGoodsImageUpload(appId int, clientSecret, accessToken, filePath string) (string, error) { + proc, err := m.Dll.FindProc("KongfzImageUpload") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzImageUpload: %v", err) + } + //appIdPtr, _ := syscall.BytePtrFromString(appId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsAddJsonPtr, _ := syscall.BytePtrFromString(filePath) + savePathPtr, _ := syscall.BytePtrFromString("") + + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsAddJsonPtr)), + uintptr(unsafe.Pointer(savePathPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// GetGoodsCategoryList 获取本店商品分类列表 +func (m *KfzDLL) GetGoodsCategoryList(appId int, clientSecret, accessToken string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopCategoryNameList") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopCategoryNameList: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// GetCommonCategory 获取公用分类数据 +func (m *KfzDLL) GetCommonCategory(appId int, clientSecret, accessToken string) (string, error) { + proc, err := m.Dll.FindProc("KongfzCommonCategory") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzCommonCategory: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// GetGoodsList 获取商品列表 +func (m *KfzDLL) GetGoodsList(appId int, clientSecret, accessToken, getGoodsListReqJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemList") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemList: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + getGoodsListReqJsonPtr, _ := syscall.BytePtrFromString(getGoodsListReqJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(getGoodsListReqJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PutOnSale 上架 +func (m *KfzDLL) PutOnSale(appId int, clientSecret, accessToken, putOnSaleJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemListing") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemListing: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + putOnSaleJsonPtr, _ := syscall.BytePtrFromString(putOnSaleJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(putOnSaleJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PutOffSale 下架 +func (m *KfzDLL) PutOffSale(appId int, clientSecret, accessToken, putOffSaleJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemDelisting") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemDelisting: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + putOffSaleJsonPtr, _ := syscall.BytePtrFromString(putOffSaleJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(putOffSaleJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// UpdateGoodsStock 修改商品库存 +func (m *KfzDLL) UpdateGoodsStock(appId int, clientSecret, accessToken, updateGoodsStockJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemNumberUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemNumberUpdate: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + updateGoodsStockJsonPtr, _ := syscall.BytePtrFromString(updateGoodsStockJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(updateGoodsStockJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// UpdateGoodsPrice 修改商品价格 +func (m *KfzDLL) UpdateGoodsPrice(appId int, clientSecret, accessToken, updateGoodsPriceJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemPriceUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemPriceUpdate: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + updateGoodsPriceJsonPtr, _ := syscall.BytePtrFromString(updateGoodsPriceJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(updateGoodsPriceJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// DeleteGoods 删除商品 +func (m *KfzDLL) DeleteGoods(appId int, clientSecret, accessToken, deleteGoodsJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemDelete") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemDelete: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + deleteGoodsJsonPtr, _ := syscall.BytePtrFromString(deleteGoodsJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(deleteGoodsJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} diff --git a/planB/modules/logs/dll.go b/planB/modules/logs/dll.go new file mode 100644 index 0000000..a8d6c79 --- /dev/null +++ b/planB/modules/logs/dll.go @@ -0,0 +1,322 @@ +package logs + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + "syscall" + "unsafe" +) + +const ( + LOG_LEVEL_DEBUG = "DEBUG" + LOG_LEVEL_INFO = "INFO" + LOG_LEVEL_WARNING = "WARNING" + LOG_LEVEL_ERROR = "ERROR" + LOG_LEVEL_SUCCESS = "SUCCESS" +) + +// LoggerDLL 封装 logger.dll 操作 +type LoggerDLL struct { + dll *syscall.LazyDLL + createLogger *syscall.LazyProc + createContext *syscall.LazyProc + logInfo *syscall.LazyProc + logError *syscall.LazyProc + logWarning *syscall.LazyProc + logSuccess *syscall.LazyProc + freeString *syscall.LazyProc + closeAllLoggers *syscall.LazyProc +} + +// LoggerConfig logger配置结构 +type LoggerConfig struct { + LogDir string `json:"log_dir"` + SplitType int `json:"split_type"` + RotateType int `json:"rotate_type"` + MaxSize int64 `json:"max_size"` + MaxCount int `json:"max_count"` + Level int `json:"level"` + EnableCaller bool `json:"enable_caller"` + DefaultTaskType string `json:"default_task_type"` +} + +var loggerDLLInstance *LoggerDLL +var loggerHandle string +var loggerContextHandle string + +// EnsureLoggerDLL 确保logger DLL已加载 +func EnsureLoggerDLL(url string) (*LoggerDLL, error) { + if loggerDLLInstance != nil { + return loggerDLLInstance, nil + } + + // 检查是否在Windows平台 + if runtime.GOOS != "windows" { + return nil, fmt.Errorf("logger DLL only supported on Windows platform") + } + + dllPath := filepath.Join(url, "logger.dll") + + // 检查文件是否存在 + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + // 尝试从当前目录查找 + if _, err := os.Stat("logger.dll"); err == nil { + dllPath = "logger.dll" + } else { + return nil, fmt.Errorf("logger DLL not found at %s", dllPath) + } + } + + dll := syscall.NewLazyDLL(dllPath) + + loggerDLLInstance = &LoggerDLL{ + dll: dll, + createLogger: dll.NewProc("CreateLogger"), + createContext: dll.NewProc("CreateContextWithTaskType"), + logInfo: dll.NewProc("LogInfo"), + logError: dll.NewProc("LogError"), + logWarning: dll.NewProc("LogWarning"), + logSuccess: dll.NewProc("LogSuccess"), + freeString: dll.NewProc("FreeString"), + closeAllLoggers: dll.NewProc("CloseAllLoggers"), + } + + return loggerDLLInstance, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// InitializeLogger 初始化logger +func InitializeLogger(m *LoggerDLL, logDir string) error { + + // 确保日志目录存在 + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 创建 logger配置 + config := LoggerConfig{ + LogDir: logDir, + SplitType: 2, // SplitByDay + RotateType: 0, // RotateBySize + MaxSize: 100 * 1024 * 1024, // 100MB + MaxCount: 10, + Level: 1, // LevelInfo - 只显示INFO及以上级别的日志 + EnableCaller: true, + DefaultTaskType: "main", + } + + configJSON, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("序列化配置失败: %v", err) + } + + // 调用CreateLogger + configPtr, _ := syscall.BytePtrFromString(string(configJSON)) + ret, _, _ := m.createLogger.Call(uintptr(unsafe.Pointer(configPtr))) + + if ret == 0 { + return fmt.Errorf("创建logger失败") + } + + // 获取logger句柄 + handle := cStr(ret) + loggerHandle = handle + + // 释放返回的字符串 + m.freeString.Call(ret) + + // 创建默认上下文 + return createLoggerContext(m, "main") +} + +// createLoggerContext 创建带任务类型的logger上下文 +func createLoggerContext(m *LoggerDLL, taskType string) error { + + if loggerHandle == "" { + return fmt.Errorf("logger未初始化") + } + + handlePtr, _ := syscall.BytePtrFromString(loggerHandle) + taskTypePtr, _ := syscall.BytePtrFromString(taskType) + + ret, _, _ := m.createContext.Call( + uintptr(unsafe.Pointer(handlePtr)), + uintptr(unsafe.Pointer(taskTypePtr)), + ) + + if ret == 0 { + return fmt.Errorf("创建logger上下文失败") + } + + // 获取上下文句柄 + loggerContextHandle = cStr(ret) + + // 释放返回的字符串 + m.freeString.Call(ret) + + return nil +} + +// SetLogTaskType 设置当前日志任务类型 +func SetLogTaskType(m *LoggerDLL, taskType string) error { + return createLoggerContext(m, taskType) +} + +// LogInfo 记录信息日志 +func LogInfo(m *LoggerDLL, message string) error { + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logInfo.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogError 记录错误日志 +func LogError(m *LoggerDLL, message string) error { + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logError.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogWarning 记录警告日志 +func LogWarning(m *LoggerDLL, message string) error { + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logWarning.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogSuccess 记录成功日志 +func LogSuccess(m *LoggerDLL, message string) error { + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logSuccess.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// CloseLogger 关闭logger +func CloseLogger(m *LoggerDLL) error { + ret, _, _ := m.closeAllLoggers.Call() + if ret == 0 { + return fmt.Errorf("关闭logger失败") + } + + m.freeString.Call(ret) + + loggerHandle = "" + loggerContextHandle = "" + loggerDLLInstance = nil + + return nil +} + +// GetLoggerHandle 获取当前logger句柄(用于外部调用) +func GetLoggerHandle() string { + return loggerContextHandle +} + +// IsLoggerInitialized 检查logger是否已初始化 +func IsLoggerInitialized() bool { + return loggerHandle != "" && loggerContextHandle != "" +} + +// SetConsoleOutput 设置控制台输出开关 +func SetConsoleOutput(enabled bool) { + if enabled { + os.Setenv("LOG_CONSOLE", "true") + } else { + os.Setenv("LOG_CONSOLE", "false") + } +} + +// LogWithLevel 带级别的日志记录,可以精确控制显示 +func LogWithLevel(m *LoggerDLL, level, message string, showConsole bool) { + if !IsLoggerInitialized() { + return + } + + switch level { + case "ERROR": + LogError(m, message) + case "WARNING": + LogWarning(m, message) + case "SUCCESS": + LogSuccess(m, message) + case "INFO": + LogInfo(m, message) + default: + LogInfo(m, message) + } +} + +// LogOnlyFile 仅写入文件,不输出到控制台 +func LogOnlyFile(m *LoggerDLL, level, message string) { + // 临时禁用控制台输出 + os.Setenv("LOG_CONSOLE", "false") + LogWithLevel(m, level, message, false) +} + +// LogConsoleAndFile 同时输出到控制台和文件 +func LogConsoleAndFile(m *LoggerDLL, level, message string) { + // 临时启用控制台输出 + os.Setenv("LOG_CONSOLE", "true") + LogWithLevel(m, level, message, true) +} diff --git a/planB/modules/logs/logger.dll b/planB/modules/logs/logger.dll new file mode 100644 index 0000000..52e722b Binary files /dev/null and b/planB/modules/logs/logger.dll differ diff --git a/planB/modules/logs/logger.md b/planB/modules/logs/logger.md new file mode 100644 index 0000000..411f310 --- /dev/null +++ b/planB/modules/logs/logger.md @@ -0,0 +1,602 @@ +# logger.dll 使用教程 +## 1. 创建DLL工具实例 +### 加载DLL文件 +```gotemplate +package logs + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "syscall" + "unsafe" +) + +// LoggerDLL 封装 logger.dll 操作 +type LoggerDLL struct { + dll *syscall.LazyDLL + createLogger *syscall.LazyProc + createContext *syscall.LazyProc + logInfo *syscall.LazyProc + logError *syscall.LazyProc + logWarning *syscall.LazyProc + logSuccess *syscall.LazyProc + freeString *syscall.LazyProc + closeAllLoggers *syscall.LazyProc +} + +// LoggerConfig logger配置结构 +type LoggerConfig struct { + LogDir string `json:"log_dir"` + SplitType int `json:"split_type"` + RotateType int `json:"rotate_type"` + MaxSize int64 `json:"max_size"` + MaxCount int `json:"max_count"` + Level int `json:"level"` + EnableCaller bool `json:"enable_caller"` + DefaultTaskType string `json:"default_task_type"` +} + +var loggerDLLInstance *LoggerDLL +var loggerHandle string +var loggerContextHandle string + +// ensureLoggerDLL 确保logger DLL已加载 +func ensureLoggerDLL() (*LoggerDLL, error) { + if loggerDLLInstance != nil { + return loggerDLLInstance, nil + } + + // 检查是否在Windows平台 + if runtime.GOOS != "windows" { + return nil, fmt.Errorf("logger DLL only supported on Windows platform") + } + + // logger.dll 位于 dll/logger.dll + //dllPath := filepath.Join("modules", "logs", "logger.dll") + dllPath := "D:\\www\\wwwroot\\planA\\modules\\logs\\logger.dll" + + // 检查文件是否存在 + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + // 尝试从当前目录查找 + if _, err := os.Stat("logger.dll"); err == nil { + dllPath = "logger.dll" + } else { + return nil, fmt.Errorf("logger DLL not found at %s", dllPath) + } + } + + dll := syscall.NewLazyDLL(dllPath) + + loggerDLLInstance = &LoggerDLL{ + dll: dll, + createLogger: dll.NewProc("CreateLogger"), + createContext: dll.NewProc("CreateContextWithTaskType"), + logInfo: dll.NewProc("LogInfo"), + logError: dll.NewProc("LogError"), + logWarning: dll.NewProc("LogWarning"), + logSuccess: dll.NewProc("LogSuccess"), + freeString: dll.NewProc("FreeString"), + closeAllLoggers: dll.NewProc("CloseAllLoggers"), + } + + return loggerDLLInstance, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// InitializeLogger 初始化logger +func InitializeLogger(logDir string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + // 确保日志目录存在 + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 创建logger配置 + config := LoggerConfig{ + LogDir: logDir, + SplitType: 1, // SplitByDay + RotateType: 0, // RotateBySize + MaxSize: 100 * 1024 * 1024, // 100MB + MaxCount: 10, + Level: 1, // LevelInfo - 只显示INFO及以上级别的日志 + EnableCaller: true, + DefaultTaskType: "main", + } + + configJSON, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("序列化配置失败: %v", err) + } + + // 调用CreateLogger + configPtr, _ := syscall.BytePtrFromString(string(configJSON)) + ret, _, _ := m.createLogger.Call(uintptr(unsafe.Pointer(configPtr))) + + if ret == 0 { + return fmt.Errorf("创建logger失败") + } + + // 获取logger句柄 + handle := cStr(ret) + loggerHandle = handle + + // 释放返回的字符串 + m.freeString.Call(ret) + + // 创建默认上下文 + return createLoggerContext("main") +} + +// createLoggerContext 创建带任务类型的logger上下文 +func createLoggerContext(taskType string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerHandle == "" { + return fmt.Errorf("logger未初始化") + } + + handlePtr, _ := syscall.BytePtrFromString(loggerHandle) + taskTypePtr, _ := syscall.BytePtrFromString(taskType) + + ret, _, _ := m.createContext.Call( + uintptr(unsafe.Pointer(handlePtr)), + uintptr(unsafe.Pointer(taskTypePtr)), + ) + + if ret == 0 { + return fmt.Errorf("创建logger上下文失败") + } + + // 获取上下文句柄 + loggerContextHandle = cStr(ret) + + // 释放返回的字符串 + m.freeString.Call(ret) + + return nil +} + +// SetLogTaskType 设置当前日志任务类型 +func SetLogTaskType(taskType string) error { + return createLoggerContext(taskType) +} + +// LogInfo 记录信息日志 +func LogInfo(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logInfo.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogError 记录错误日志 +func LogError(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logError.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogWarning 记录警告日志 +func LogWarning(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logWarning.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogSuccess 记录成功日志 +func LogSuccess(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logSuccess.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// CloseLogger 关闭logger +func CloseLogger() error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + ret, _, _ := m.closeAllLoggers.Call() + if ret == 0 { + return fmt.Errorf("关闭logger失败") + } + + m.freeString.Call(ret) + + loggerHandle = "" + loggerContextHandle = "" + loggerDLLInstance = nil + + return nil +} + +// GetLoggerHandle 获取当前logger句柄(用于外部调用) +func GetLoggerHandle() string { + return loggerContextHandle +} + +// IsLoggerInitialized 检查logger是否已初始化 +func IsLoggerInitialized() bool { + return loggerHandle != "" && loggerContextHandle != "" +} + +// SetConsoleOutput 设置控制台输出开关 +func SetConsoleOutput(enabled bool) { + if enabled { + os.Setenv("LOG_CONSOLE", "true") + } else { + os.Setenv("LOG_CONSOLE", "false") + } +} + +// LogWithLevel 带级别的日志记录,可以精确控制显示 +func LogWithLevel(level, message string, showConsole bool) { + if !IsLoggerInitialized() { + return + } + + switch level { + case "ERROR": + LogError(message) + case "WARNING": + LogWarning(message) + case "SUCCESS": + LogSuccess(message) + case "INFO": + LogInfo(message) + default: + LogInfo(message) + } +} + +// LogOnlyFile 仅写入文件,不输出到控制台 +func LogOnlyFile(level, message string) { + // 临时禁用控制台输出 + os.Setenv("LOG_CONSOLE", "false") + LogWithLevel(level, message, false) +} + +// LogConsoleAndFile 同时输出到控制台和文件 +func LogConsoleAndFile(level, message string) { + // 临时启用控制台输出 + os.Setenv("LOG_CONSOLE", "true") + LogWithLevel(level, message, true) +} + +``` + +# 接口详情 +## 创建日志器--CreateLogger +### 请求信息 +```gotemplate +dll.CreateLogger(configJSON) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| configJSON | string | 是 | 配置信息JSON字符串 | +#### 配置JSON结构 +```json +{ + "log_dir": "/path/to/logs", + "split_type": 0, + "rotate_type": 0, + "max_size": 104857600, + "max_count": 30, + "level": 1, + "enable_caller": true, + "default_task_type": "main" +} +``` +#### 参数说明: +```text +log_dir: 日志目录路径 +split_type: 分片方式(0=按月,1=按天,2=按小时,3=按分钟,4=按秒) +rotate_type: 轮转方式(0=按大小,1=按数量) +max_size: 最大文件大小(字节),仅在rotate_type=0时有效 +max_count: 最大文件数量,仅在rotate_type=1时有效 +level: 日志级别(0=SUCCESS,1=INFO,2=WARNING,3=ERROR) +enable_caller: 是否启用调用者信息 +default_task_type: 默认任务类型 +``` +### 响应示例 +```json +"错误: 创建日志目录失败: permission denied" +``` + +## 创建带任务类型的上下文--CreateContextWithTaskType +### 请求信息 +```gotemplate +dll.CreateContextWithTaskType(loggerHandle, taskType) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| loggerHandle | string | 是 | 日志器句柄 | +| taskType | string | 是 | 任务类型 | +### 响应示例 +```json +"ctx_1645497600000000000" +``` +#### 错误响应示例 +```json +"错误: 无效的logger句柄" +``` + +## 记录信息日志--LogInfo +### 请求信息 +```gotemplate +dll.LogInfo(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 记录错误日志--LogError +### 请求信息 +```gotemplate +dll.LogError(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 记录警告日志--LogWarning +### 请求信息 +```gotemplate +dll.LogWarning(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 记录成功日志--LogSuccess +### 请求信息 +```gotemplate +dll.LogSuccess(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 获取日志条目--GetLogs +### 请求信息 +```gotemplate +dll.GetLogs(loggerHandle, configJSON) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------| +| loggerHandle | string | 是 | 日志器句柄 | +| configJSON | string | 是 | 查询配置JSON | +#### 查询配置JSON结构 +```json +{ + "level": 1, + "task_type": "main", + "start_time": "2024-01-01 00:00:00", + "end_time": "2024-01-31 23:59:59", + "max_entries": 1000 +} +``` +#### 参数说明: +```text +level: 日志级别(-1表示所有级别) +task_type: 任务类型(空字符串表示所有任务类型) +start_time: 开始时间(格式: 2006-01-02 15:04:05) +end_time: 结束时间(格式: 2006-01-02 15:04:05) +max_entries: 最大返回条目数(0表示使用默认值1000) +``` +### 响应示例 +```json +{ + "count": 125, + "entries": [ + { + "timestamp": "2024-01-15 10:30:45.123", + "level": "INFO", + "task_type": "main", + "caller": "logger.go:256", + "message": "系统启动完成" + }, + { + "timestamp": "2024-01-15 10:31:15.456", + "level": "ERROR", + "task_type": "backup", + "caller": "backup.go:89", + "message": "备份文件失败: 磁盘空间不足" + } + ] +} +``` +### 错误响应示例 +```json +{"error": "无效的logger句柄"} +``` + +## 获取日志文件列表--GetLogFiles +### 请求信息 +```gotemplate +dll.GetLogFiles(loggerHandle) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------| +| loggerHandle | string | 是 | 日志器句柄 | +### 响应示例 +```json +{ + "count": 8, + "files": [ + { + "level": "INFO", + "task_type": "main", + "file_name": "INFO-main-2024-01.logs", + "file_size": 1048576, + "mod_time": "2024-01-15 10:30:45" + }, + { + "level": "ERROR", + "task_type": "backup", + "file_name": "ERROR-backup-2024-01.logs", + "file_size": 51200, + "mod_time": "2024-01-15 10:31:15" + } + ] +} +``` +### 错误响应示例 +```json +{"error": "无效的logger句柄"} +``` + +## 获取版本信息--GetVersion +### 请求信息 +```gotemplate +dll.GetVersion() +``` +### 请求参数 +```text +无参数 +``` +### 响应示例 +```json +"v1" +``` + +## 关闭所有日志器--CloseAllLoggers +### 请求信息 +```gotemplate +dll.CloseAllLoggers() +``` +### 请求参数 +```text +无参数 +``` +### 响应示例 +```json +"成功关闭所有logger" +``` +### 错误响应示例 +```json +"关闭了5个logger,其中1个出错,最后错误: close file error" +``` + +## 释放C字符串内存--FreeString +### 请求信息 +```gotemplate +dll.FreeString(str) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------| +| str | string | 是 | 需要释放的字符串 | \ No newline at end of file diff --git a/planB/modules/pdd/pdd.dll b/planB/modules/pdd/pdd.dll new file mode 100644 index 0000000..532e5c9 Binary files /dev/null and b/planB/modules/pdd/pdd.dll differ diff --git a/planB/modules/pdd/pdd.go b/planB/modules/pdd/pdd.go new file mode 100644 index 0000000..4f25821 --- /dev/null +++ b/planB/modules/pdd/pdd.go @@ -0,0 +1,337 @@ +package pdd + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +var ( + gPddDll *PddDLL +) + +// PddResponse 定义完整的响应结构(包含成功和失败两种情况) +type PddResponse struct { + SuccessResponse *PddSuccessResponse `json:"outer_cat_mapping_get_response,omitempty"` + ErrorResponse *PddErrorResponse `json:"error_response,omitempty"` +} +type PddSuccessResponse struct { + OuterCatMappingGetResponse PddCategoryMappingResponse `json:"outer_cat_mapping_get_response"` +} + +// PddCategoryMappingResponse 定义拼多多API响应结构(根据文档规范) +type PddCategoryMappingResponse struct { + CatID1 int64 `json:"cat_id1"` // 一级类目 ID + CatID2 int64 `json:"cat_id2"` // 二级类目 ID + CatID3 int64 `json:"cat_id3"` // 三级类目 ID + CatID4 int64 `json:"cat_id4"` // 四级类目 ID + RequestID string `json:"request_id"` // 请求 ID +} + +// PddDLL 拼多多工具DLL结构 +type PddDLL struct { + Dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} +type PddErrorResponse struct { + ErrorCode int64 `json:"error_code"` // 错误码 + ErrorMsg string `json:"error_msg"` // 错误信息 + SubCode *string `json:"sub_code"` // 子错误码 + SubMsg string `json:"sub_msg"` // 子错误信息 + RequestID string `json:"request_id"` // 请求ID +} + +// InitPddDll 初始化 pddDLL +func InitPddDll(url string) (*PddDLL, error) { + dllPath := filepath.Join(url, "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } + gPddDll = &PddDLL{ + Dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + } + return gPddDll, nil +} + +// PddGoodsOuterCatMappingGet 类目预测 +func (m *PddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsAdd 商品新增 +func (m *PddDLL) PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsAdd") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsAdd: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsAddJsonPtr, _ := syscall.BytePtrFromString(goodsAddJson) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsAddJsonPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// PddGoodsSpecIdGet 生成商家自定义的规格 +func (m *PddDLL) PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsSpecIdGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSpecIdGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + parentSpecIdPtr, _ := syscall.BytePtrFromString(parentSpecId) + specNamePtr, _ := syscall.BytePtrFromString(specName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(parentSpecIdPtr)), + uintptr(unsafe.Pointer(specNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsCommitDetailGet 获取商品提交的商品详情 +func (m *PddDLL) PddGoodsCommitDetailGet(clientId, clientSecret, accessToken, goodsCommitId, goodsId string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsCommitDetailGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsCommitIdPtr, _ := syscall.BytePtrFromString(goodsCommitId) + goodsIdPtr, _ := syscall.BytePtrFromString(goodsId) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsCommitIdPtr)), + uintptr(unsafe.Pointer(goodsIdPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddTimeGet 获取拼多多系统时间 +func (m *PddDLL) PddTimeGet(clientId, clientSecret, accessToken string) (string, error) { + proc, err := m.Dll.FindProc("PddTimeGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsImageUpload 上传图片 +func (m *PddDLL) PddGoodsImageUpload(clientId, clientSecret, accessToken, imgBase64 string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsImageUpload") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsImageUpload: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + imgBase64Ptr, _ := syscall.BytePtrFromString(imgBase64) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(imgBase64Ptr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsListGet 获取店铺商品 +func (m *PddDLL) PddGoodsListGet(clientId, clientSecret, accessToken string, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsListGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsListGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsSaleStatusSet 设置上下架状态 +func (m *PddDLL) PddGoodsSaleStatusSet(clientId, clientSecret, accessToken, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsSaleStatusSet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSaleStatusSet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddDeleteGoodsCommit 删除商品 +func (m *PddDLL) PddDeleteGoodsCommit(clientId, clientSecret, accessToken, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddDeleteGoodsCommit") + if err != nil { + return "", fmt.Errorf("找不到函数 PddDeleteGoodsCommit: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsQuantityUpdate 更新库存 +func (m *PddDLL) PddGoodsQuantityUpdate(clientId, clientSecret, accessToken, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsQuantityUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsQuantityUpdate: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsSkuPriceUpdate 更新价格 +func (m *PddDLL) PddGoodsSkuPriceUpdate(clientId, clientSecret, accessToken, params string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsSkuPriceUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSkuPriceUpdate: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} diff --git a/planB/modules/pdd/pdd.md b/planB/modules/pdd/pdd.md new file mode 100644 index 0000000..2c11768 --- /dev/null +++ b/planB/modules/pdd/pdd.md @@ -0,0 +1,863 @@ +# pdd.dll 使用教程 +## 1.创建DLL工具实例 +### 加载DLL文件 +```gotemplate +// PddDLL 拼多多工具DLL结构 +type pddDLL struct { + dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} + +// <初始化pddDLL> +func InitPddDLL() (*pddDLL, error) { + dllPath := filepath.Join("dll", "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } else { + return &pddDLL{ + dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +dll, err := InitPddDLL() +``` + +### 获取C字符串 +```gotemplate +// cStr 获取C字符串 +func (m *pddDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} +``` + +## 2. 使用dll函数示例 +```gotemplate +// 类目预测 +func (m *pddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} +``` + +# 接口详情 +## 1. 类目预测--PddGoodsOuterCatMappingGet +### 请求信息 +```gotemplate +dll.PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, +outerCatId, outerCatName, outerGoodsName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| outerCatId | string | 是 | 外部平台类目ID | +| outerCatName | string | 是 | 外部平台类目名称 | +| outerGoodsName | string | 是 | 外部商品名称 | +### 响应示例 +```json +{ + "outer_cat_mapping_get_response": { + "cat_id2": 16028, + "cat_id3": 16031, + "cat_id1": 15543, + "request_id": "17666480184871649", + "cat_id4": 0 + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 2. 快递公司查看--PddLogisticsCompaniesGet +### 请求信息 +```gotemplate +dll.PddLogisticsCompaniesGet(clientId, clientSecret) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +### 响应示例 +```json +{ + "logistics_companies_get_response": { + "logistics_companies": [ + { + "available": 1, + "code": "SF", + "id": 1, + "logistics_company": "顺丰速运" + }, + { + "available": 1, + "code": "STO", + "id": 2, + "logistics_company": "申通快递" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 3. erp打单信息同步--PddErpOrderSync +### 请求信息 +```gotemplate +dll.PddErpOrderSync(clientId, clientSecret, accessToken, logisticsId, +orderSn, orderState, waybillNo) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsId | string | 是 | 物流公司ID | +| orderSn | string | 是 | 拼多多订单号 | +| orderState | string | 是 | 订单状态 | +| waybillNo | string | 是 | 运单号 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871650" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 4. 拼多多订单同步--PddOrderSynchronization +### 请求信息 +```gotemplate +dll.PddOrderSynchronization(clientId, clientSecret, accessToken, logisticsCompany, logisticsOnlineSendJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsCompany | string | 是 | 物流公司名称 | +| logisticsOnlineSendJson | string | 是 | 拼多多订单同步json字符串 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871651" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 5. 商品图片上传接口--PddGoodsImgUpload +### 请求信息 +```gotemplate +dll.PddGoodsImgUpload(clientId, clientSecret, accessToken, filePath) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +### 响应示例 +```json +{ + "goods_img_upload_response": { + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "request_id": "17666480184871652" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 6. 商品新增接口--PddGoodsAdd +### 请求信息 +```gotemplate +dll.PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| goodsAddJson | string | 是 | 商品信息JSON字符串 | +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456789, + "goods_name": "测试商品", + "goods_sn": "G202501200001", + "request_id": "17666480184871653" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 7. 联合拼多多图片上传的商品新增--SelfPddGoodsAdd +### 请求信息 +```gotemplate +dll.SelfPddGoodsAdd(clientId, clientSecret, accessToken, filePath, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +| goodsAddJson | string | 是 | 商品信息JSON字符串(不需包含image_url)| +#### 接口说明 +此接口为组合接口,内部执行以下步骤: +1.上传商品主图文件到拼多多服务器 +2.获取图片URL并自动填充到商品信息中 +3.调用商品新增接口创建商品 +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456790, + "goods_name": "测试商品", + "goods_sn": "G202501200002", + "request_id": "17666480184871654" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 8. 批量数据解密脱敏接口--PddOpenDecryptMaskBatch +### 请求信息 +```gotemplate +dll.PddOpenDecryptMaskBatch(clientId, clientSecret, accessToken, reqJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| reqJson | string | 是 | 信息JSON字符串 | +#### 信息JSON结构示例 +```json +[ + { + "data_tag": "251229-272441044622514", + "encrypted_data": "~AgAAAAPlscEH0psOJAEXpTdsLOWvDJ9bB7IEjIoqNfiDhhJR9NHOxsdZ+PEFluSSCngCikoDU+CP/sSXZJ92ic7+PdNlJNLA7g/6VUMDWF6RvjW9IeRN+lKNarsjWDQR~0~" + } +] +``` +### 响应示例 +```json +{ + "open_decrypt_mask_batch_response": { + "data_decrypt_list": [ + { + "data_tag": "str", + "data_type": 0, + "decrypted_data": "str", + "encrypted_data": "str", + "error_code": 0, + "error_msg": "str" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 生成商家自定义的规格--PddGoodsSpecIdGet +### 请求信息 +```gotemplate +dll.PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| parentSpecId | string | 是 | 拼多多标准规格ID | +| specName | string | 是 | 商家编辑的规格值,如颜色规格下设置白色属性 | +### 响应参数 +```json +{ + "goods_spec_id_get_response": { + "parent_spec_id": 0, + "spec_id": 0, + "spec_name": "str" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 修改商品SKU价格--PddGoodsSkuPriceUpdate +### 请求信息 +```gotemplate +dll.PddGoodsSkuPriceUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 价格更新请求JSON字符串 | +#### 请求JSON结构 +```json +{ + "goods_id": "必填,商品id,类型为LONG", + "ignore_edit_warn": "非必填,是否获取商品发布警告信息,默认为忽略,类型为BOOLEAN", + "market_price": "非必填,参考价(单位分),类型为LONG", + "market_price_in_yuan": "非必填,参考价(单位元),类型为STRING", + "sku_price_list": [ + { + "group_price": "非必填,拼团购买价格(单位分),类型为LONG", + "is_onsale": "非必填,sku上架状态,0-已下架,1-上架中,类型为INTEGER", + "single_price": "非必填,单独购买价格(单位分),类型为LONG", + "sku_id": "必填,sku标识,类型为LONG" + } + ], + "sync_goods_operate": "非必填,提交后上架状态,0:上架,1:保持原样,类型为INTEGER", + "two_pieces_discount": "非必填,满2件折扣,可选范围0-100,0表示取消,95表示95折,设置需先查询规则接口获取实际可填范围,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_update_sku_price_response": { + "goods_commit_id": 0, + "is_success": true + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 商品库存更新接口--PddGoodsQuantityUpdate +### 请求信息 +```gotemplate +dll.PddGoodsQuantityUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 库存更新请求JSON字符串 | +#### 请求JSON结构 request 字符串 +```json +{ + "force_update": "非必填,是否强制更新,仅update_type=1(全量更新)时有效,默认值false;force_update=false时,quantity不能小于预扣库存;force_update=true时,代表强制更新,当quantity<预扣库存时,不报错,直接将quantity清0,类型为BOOLEAN", + "goods_id": "必填,商品id,类型为LONG", + "outer_id": "非必填,sku商家编码,类型为STRING", + "quantity": "必填,库存修改值。当全量更新库存时,quantity必须为大于等于0的正整数;当增量更新库存时,quantity为整数,可小于等于0。若增量更新时传入的库存为负数,则负数与实际库存之和不能小于0。比如当前实际库存为1,传入增量更新quantity=-1,库存改为0,类型为LONG", + "sku_id": "非必填,sku_id和outer_id必填一个,类型为LONG", + "update_type": "非必填,库存更新方式,可选。1为全量更新,2为增量更新。如果不填,默认为全量更新,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_quantity_update_response": { + "is_success": false + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 获取商品信息接口 -- OutPddAuthGetCommitDetailt +### 请求信息 +```gotemplate +dll.OutPddAuthGetCommitDetailt(goodsCommitId, goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsCommitId | string | 是 | 商品提交ID | +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json + +``` + + +## 获取商品详情信息接口 -- OutPddAuthGetGoodsDetail +### 请求信息 +```gotemplate +dll.OutPddAuthGetGoodsDetail(goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "bad_fruit_claim": 0, + "buy_limit": 999999, + "carousel_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2025-06-30/4539f740-331b-4687-aa00-5c96855de6cd.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/b0e89e39-c97b-475d-9be2-f1909e30acb5.jpeg" + ], + "cat_id": 15678, + "cost_template_id": 655688447565777, + "country_id": 0, + "customer_num": 2, + "customs": "", + "detail_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/b691c104-baf8-42b2-97e2-b7258113114b.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/53e6f7ff-d15e-4e8f-8625-e293717ca1e4.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/ecff591d-32a6-42c9-ba5a-6a42829092a8.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/7034f8a0-5d88-49f8-a96f-608abb8cac80.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/e10c2b6c-d4de-4fdd-8d48-f0a334735e9a.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/c19358fb-0a4d-49ad-bcc8-b2980e938064.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/1deeb9c0-7212-432b-a309-f774db6e1adb.jpeg" + ], + "goods_desc": "书名:金属工艺学 下 第6版,作者:'邓文英,宋力宏主编',ISBN:9787040456295,出版社:高等教育出版社", + "goods_id": 770621582375, + "goods_name": "金属工艺学 下 第6版 邓文英,宋力宏主编 高等教育出版社 978", + "goods_property_list": [ + { + "punit": "", + "ref_pid": 425, + "template_pid": 401030, + "vid": 0, + "vvalue": "9787040456295" + }, + { + "punit": "", + "ref_pid": 876, + "template_pid": 401029, + "vid": 0, + "vvalue": "金属工艺学 下 第6版" + }, + { + "punit": "页", + "ref_pid": 692, + "template_pid": 401032, + "vid": 0, + "vvalue": "157" + }, + { + "punit": "元", + "ref_pid": 879, + "template_pid": 401034, + "vid": 0, + "vvalue": "24.70" + }, + { + "punit": "", + "ref_pid": 882, + "template_pid": 401037, + "vid": 0, + "vvalue": "邓文英,宋力宏主编" + }, + { + "punit": "", + "ref_pid": 880, + "template_pid": 401035, + "vid": 483761, + "vvalue": "高等教育出版社" + }, + { + "punit": "", + "ref_pid": 888, + "template_pid": 401043, + "vid": 0, + "vvalue": "平装" + } + ], + "goods_type": 1, + "image_url": "", + "invoice_status": 0, + "is_customs": 0, + "is_folt": 0, + "is_group_pre_sale": 0, + "is_pre_sale": 0, + "is_refundable": 1, + "is_sku_pre_sale": 0, + "market_price": 5948, + "order_limit": 999999, + "outer_goods_id": "9787040456295", + "oversea_type": 0, + "pre_sale_time": 0, + "privacy_delivery": 0, + "quan_guo_lian_bao": 0, + "second_hand": 1, + "shipment_limit_second": 172800, + "sku_list": [ + { + "is_onsale": 1, + "limit_quantity": 999999, + "multi_price": 1487, + "out_sku_sn": "9787040456295", + "price": 1587, + "quantity": 0, + "reserve_quantity": 0, + "sku_id": 1753931570290, + "sku_pre_sale_time": 0, + "spec": [ + { + "parent_id": 1216, + "parent_name": "尺寸", + "spec_id": 27632894279, + "spec_name": "单本 无附赠 超七天不退换" + } + ], + "thumb_url": "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "weight": 500 + } + ], + "status": 4, + "tiny_name": "金属工艺学 下 第6", + "two_pieces_discount": 96, + "video_gallery": [], + "warehouse": "", + "warm_tips": "", + "zhi_huan_bu_xiu": 0 +} +``` + +## 生成自定义规格接口 -- OutPddAuthSetSpec +### 请求信息 +```gotemplate +dll.OutPddAuthSetSpec(specTypeId, specName, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| specTypeId | int | 是 | 规格类型ID | +| specName | string | 是 | 规格名称 | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "parentSpecId": 3820, + "specName": "全新", + "specId": 1080396526 +} +``` + +## 修改价格接口 -- OutPddAuthUpdatePrice +### 请求信息 +```gotemplate +dll.OutPddAuthUpdatePrice(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 修改库存接口 -- OutPddAuthUpdateStock +### 请求信息 +```gotemplate +dll.OutPddAuthUpdateStock(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 12.释放C字符串内存--FreeCString +### 请求信息 +```gotemplate +dll.FreeCString(str) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| str | string | 是 | 需要释放的字符串 | diff --git a/planB/modules/xianYu/address.xlsx b/planB/modules/xianYu/address.xlsx new file mode 100644 index 0000000..7c91f53 Binary files /dev/null and b/planB/modules/xianYu/address.xlsx differ diff --git a/planB/modules/xianYu/bak/xy.dll b/planB/modules/xianYu/bak/xy.dll new file mode 100644 index 0000000..58007f4 Binary files /dev/null and b/planB/modules/xianYu/bak/xy.dll differ diff --git a/planB/modules/xianYu/config.ini b/planB/modules/xianYu/config.ini new file mode 100644 index 0000000..6755855 --- /dev/null +++ b/planB/modules/xianYu/config.ini @@ -0,0 +1,25 @@ +[app] +AppId = 1228288260261189 +AppSecret = aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k +Domain = https://open.goofish.pro +[http] +Addr = 127.0.0.1:53368 +[categoryListRequest] +Path = /api/open/product/category/list +ItemBizType: 2 +SpBizType: 24 +[batchCreatRequest] +Path = /api/open/product/batchCreate +[file] +TxtPath = modules/xianYu/productCategory.txt +ExcelPath = modules/xianYu/address.xlsx +SheetName = Result +[redis] +Password = Long6166@@ +Addr = 127.0.0.1:6379 +Db = 5 +[tokenBucket] +BucketKeyPrefix = "token_bucket_" +TokenPerSecond = 10 +BucketSize = 100 +Delay = 100 diff --git a/planB/modules/xianYu/productCategory.txt b/planB/modules/xianYu/productCategory.txt new file mode 100644 index 0000000..a0a9fbc --- /dev/null +++ b/planB/modules/xianYu/productCategory.txt @@ -0,0 +1,284 @@ +cbf4e2ec8f2013d31921b9e373cead75:电视剧 +cbf4e2ec8f2013d3267e0a01017d9f44:电影 +cbf4e2ec8f2013d36f38848189966e7d:生活 +cbf4e2ec8f2013d3ac899d2620c5df2b:成人教育音像 +cbf4e2ec8f2013d3acde29f76907b07f:动画 +cbf4e2ec8f2013d3e10cfa39bf43dc0f:儿童教育音像 +d14d229692616168b108d382c4e6ea42:废品回收 +d816d18aa66dfb3d1921b9e373cead75:励志成长 +dbaba36adf47af96b108d382c4e6ea42:不干胶标签 +e59460ef9961e2bd28d88a08a19453dc:古典吉他 +e59460ef9961e2bda7f7e02f36b0b49a:电箱吉他 +86cddebb2de0815c1921b9e373cead75:桌面文件柜 +86cddebb2de0815c267e0a01017d9f44:资料册 +86cddebb2de0815c6f38848189966e7d:镇纸 +86cddebb2de0815ca7f7e02f36b0b49a:文件袋 +86cddebb2de0815cacde29f76907b07f:文房墨汁 +86cddebb2de0815ce10cfa39bf43dc0f:文房四宝套装 +879b743300e7a58137b3d33c282f2081:古筝 +8bd8d9724880b84d28d88a08a19453dc:学习笔记 +a457d6fc43c609bdac899d2620c5df2b:单据收据 +a457d6fc43c609bdacde29f76907b07f:印台 +a9ef3505c7fe4b661921b9e373cead75:勾线笔 +a9ef3505c7fe4b66a7f7e02f36b0b49a:电子阅览器/电纸书 +ab78823bfd3c7134b108d382c4e6ea42:经济管理 +ac69f9982deabde1acde29f76907b07f:民谣吉他 +ac69f9982deabde1e10cfa39bf43dc0f:架子鼓 +b12c1c13a8dc3b2b6f38848189966e7d:POP广告纸 +b12c1c13a8dc3b2ba7f7e02f36b0b49a:修正贴 +b12c1c13a8dc3b2bac899d2620c5df2b:学生用印 +b12c1c13a8dc3b2bacde29f76907b07f:名片 +b2b61c32fc4c904428d88a08a19453dc:背胶证件照 +b3b713b29220947237b3d33c282f2081:台历 +4c49139fe1b6ae4aac899d2620c5df2b:童书育儿 +4fecb084c468ed626f38848189966e7d:黑板 +5042edcbd2cc4b94ac899d2620c5df2b:生活百科 +621bd460d751e0fc37b3d33c282f2081:订书机 +701ed8603d74ee60b108d382c4e6ea42:报纸 +722d38201b9c8cba267e0a01017d9f44:社科心理 +7912befd7e1215d11921b9e373cead75:挂历 +7dba397e41d08d4937b3d33c282f2081:拆信刀 +7eb776b01814cc6e1921b9e373cead75:教材教辅 +22e1d81dc4cf3a25a7f7e02f36b0b49a:图书 +2dfa3034d88aedcc1921b9e373cead75:期刊/杂志 +31329c43789fae0437b3d33c282f2081:戏曲综艺 +31329c43789fae04a7f7e02f36b0b49a:音乐唱片/专辑 +322a73805c38995f6f38848189966e7d:宝珠笔 +3cdbae6d47df9251a7f7e02f36b0b49a:电子资料 +22d3cfff678abab1e10cfa39bf43dc0f:握笔器 +b7fd03d456abe3011921b9e373cead75:活页替芯 +b7fd03d456abe301b108d382c4e6ea42:索引纸 +b7fd03d456abe301e10cfa39bf43dc0f:拍纸本 +c230ba4ca293f3b528d88a08a19453dc:马克笔 +c230ba4ca293f3b5a7f7e02f36b0b49a:钢笔 +c230ba4ca293f3b5ac899d2620c5df2b:铅笔 +c3c6e8d1d63c0618b108d382c4e6ea42:文学/小说 +c58d3dbcff05e404acde29f76907b07f:笔筒 +eac1d67ece5fa9b16f38848189966e7d:钢琴 +ee8603696d446e931921b9e373cead75:电钢琴 +06d80b131d7b0b616f38848189966e7d:毛笔 +0e28c0f1f1e57eb1ac899d2620c5df2b:地图 +0f75076039b85f74267e0a01017d9f44:计算器 +0f75076039b85f7428d88a08a19453dc:尺 +0f75076039b85f746f38848189966e7d:板擦 +0f75076039b85f74b108d382c4e6ea42:算盘 +11c38799bd389b3828d88a08a19453dc:漫画书籍 +ac69f9982deabde1a7f7e02f36b0b49a:上弦器 +83f9286d1ea41056ac899d2620c5df2b:其他吉他配件 +e59460ef9961e2bd1921b9e373cead75:变调夹 +83f9286d1ea4105637b3d33c282f2081:古典吉他弦 +e59460ef9961e2bdacde29f76907b07f:吉他单块效果器 +83f9286d1ea41056267e0a01017d9f44:吉他效果器配件 +83f9286d1ea41056b108d382c4e6ea42:吉他电源 +ac69f9982deabde1267e0a01017d9f44:吉他综合效果器 +e59460ef9961e2bdb108d382c4e6ea42:吉他背包琴盒 +83f9286d1ea4105628d88a08a19453dc:吉他背带 +83f9286d1ea410561921b9e373cead75:吉他连接线 +e59460ef9961e2bd6f38848189966e7d:吊架 +ac69f9982deabde1ac899d2620c5df2b:弦枕 +ac69f9982deabde11921b9e373cead75:弦柱 +e59460ef9961e2bd267e0a01017d9f44:拨片 +ac69f9982deabde128d88a08a19453dc:拾音器 +83f9286d1ea41056e10cfa39bf43dc0f:曼陀铃弦 +83f9286d1ea410566f38848189966e7d:民谣吉他弦 +ac69f9982deabde137b3d33c282f2081:清洁保护品 +83f9286d1ea41056a7f7e02f36b0b49a:滑棒指套 +e59460ef9961e2bde10cfa39bf43dc0f:电吉他弦 +e59460ef9961e2bdac899d2620c5df2b:背带钮 +83f9286d1ea41056acde29f76907b07f:脚凳 +ac69f9982deabde1b108d382c4e6ea42:调音器 +e59460ef9961e2bd37b3d33c282f2081:电吉他 +c6d5c9e68467b108ac899d2620c5df2b:哑鼓垫 +c6d5c9e68467b10837b3d33c282f2081:镲片 +c6d5c9e68467b10828d88a08a19453dc:鼓凳 +ac69f9982deabde16f38848189966e7d:鼓刷 +c6d5c9e68467b108b108d382c4e6ea42:鼓架镲架 +c6d5c9e68467b108a7f7e02f36b0b49a:鼓棒鼓锤 +1cac27c660d7b098b108d382c4e6ea42:唢呐 +1cac27c660d7b098267e0a01017d9f44:埙 +f22578f0c6a8eaa5267e0a01017d9f44:尺八 +f22578f0c6a8eaa51921b9e373cead75:巴乌 +1cac27c660d7b09828d88a08a19453dc:笙 +f22578f0c6a8eaa5acde29f76907b07f:笛子 +f22578f0c6a8eaa5e10cfa39bf43dc0f:管子 +1cac27c660d7b0981921b9e373cead75:箫 +1cac27c660d7b098a7f7e02f36b0b49a:芦笙 +1cac27c660d7b09837b3d33c282f2081:葫芦丝 +f22578f0c6a8eaa56f38848189966e7d:葫芦笙 +1cac27c660d7b098ac899d2620c5df2b:陶笛 +879b743300e7a581acde29f76907b07f:三弦 +1cac27c660d7b098e10cfa39bf43dc0f:冬不拉 +1cac27c660d7b0986f38848189966e7d:古琴 +1cac27c660d7b098acde29f76907b07f:弹布尔 +879b743300e7a581e10cfa39bf43dc0f:扬琴 +879b743300e7a5816f38848189966e7d:月琴 +879b743300e7a58128d88a08a19453dc:柳琴 +879b743300e7a5811921b9e373cead75:热瓦普 +879b743300e7a581b108d382c4e6ea42:琵琶 +879b743300e7a581ac899d2620c5df2b:秦琴 +879b743300e7a581a7f7e02f36b0b49a:箜篌 +879b743300e7a581267e0a01017d9f44:阮 +a2eba09f5b889a7c28d88a08a19453dc:中胡 +7d61e938542f6790b108d382c4e6ea42:二胡 +7d61e938542f6790267e0a01017d9f44:京二胡 +7d61e938542f6790acde29f76907b07f:京胡 +7d61e938542f679028d88a08a19453dc:低音胡 +a2eba09f5b889a7c37b3d33c282f2081:四胡 +a2eba09f5b889a7cb108d382c4e6ea42:坠琴 +7d61e938542f6790a7f7e02f36b0b49a:板胡 +a2eba09f5b889a7ca7f7e02f36b0b49a:椰胡 +7d61e938542f679037b3d33c282f2081:艾捷克 +7d61e938542f67901921b9e373cead75:革胡 +7d61e938542f67906f38848189966e7d:马头琴 +7d61e938542f6790e10cfa39bf43dc0f:马骨胡 +7d61e938542f6790ac899d2620c5df2b:高胡 +882b39ff0db2dd0037b3d33c282f2081:军镲 +00a32e7ff35aaf9e267e0a01017d9f44:大钹 +00a32e7ff35aaf9ee10cfa39bf43dc0f:大铙 +00a32e7ff35aaf9eacde29f76907b07f:大顶钹 +00a32e7ff35aaf9e1921b9e373cead75:川钹 +00a32e7ff35aaf9e6f38848189966e7d:广钹 +882b39ff0db2dd00a7f7e02f36b0b49a:快板 +882b39ff0db2dd0028d88a08a19453dc:拍板 +00a32e7ff35aaf9eb108d382c4e6ea42:梆子 +882b39ff0db2dd001921b9e373cead75:水镲 +882b39ff0db2dd00b108d382c4e6ea42:碰钟 +882b39ff0db2dd00acde29f76907b07f:秧歌镲 +882b39ff0db2dd00e10cfa39bf43dc0f:腰鼓镲 +882b39ff0db2dd00ac899d2620c5df2b:萨巴依 +882b39ff0db2dd00267e0a01017d9f44:铜书板 +00a32e7ff35aaf9eac899d2620c5df2b:镲锅 +0ea61a801ba323c1267e0a01017d9f44:堂鼓 +00a32e7ff35aaf9e28d88a08a19453dc:战鼓 +0ea61a801ba323c11921b9e373cead75:排鼓 +0ea61a801ba323c1b108d382c4e6ea42:板鼓 +00a32e7ff35aaf9e37b3d33c282f2081:秧歌鼓 +0ea61a801ba323c1e10cfa39bf43dc0f:细腰鼓 +00a32e7ff35aaf9ea7f7e02f36b0b49a:腰鼓 +0ea61a801ba323c1ac899d2620c5df2b:花盆鼓 +0ea61a801ba323c16f38848189966e7d:象脚鼓 +0ea61a801ba323c1acde29f76907b07f:铜鼓 +a7133eb411b587cf1921b9e373cead75:空灵鼓/无忧鼓 +0ea61a801ba323c1a7f7e02f36b0b49a:云锣 +a2eba09f5b889a7c267e0a01017d9f44:京锣 +a2eba09f5b889a7cac899d2620c5df2b:低音锣 +a2eba09f5b889a7cacde29f76907b07f:开道锣 +a2eba09f5b889a7ce10cfa39bf43dc0f:手锣 +0ea61a801ba323c137b3d33c282f2081:武锣 +0ea61a801ba323c128d88a08a19453dc:舟山锣 +a2eba09f5b889a7c6f38848189966e7d:苏锣 +a2eba09f5b889a7c1921b9e373cead75:虎音锣 +33a0daa5d89d68fa1921b9e373cead75:宣纸 +b12c1c13a8dc3b2b1921b9e373cead75:吊牌 +b12c1c13a8dc3b2be10cfa39bf43dc0f:自封袋 +b12c1c13a8dc3b2b267e0a01017d9f44:贺卡明信片 +22d3cfff678abab11921b9e373cead75:书皮 +b12c1c13a8dc3b2b37b3d33c282f2081:修正带 +b12c1c13a8dc3b2b28d88a08a19453dc:修正液 +22d3cfff678abab1a7f7e02f36b0b49a:削笔器 +22d3cfff678abab128d88a08a19453dc:可爱印泥 +b12c1c13a8dc3b2bb108d382c4e6ea42:学生书包 +22d3cfff678abab1acde29f76907b07f:文具套装 +22d3cfff678abab1267e0a01017d9f44:文具盒 +22d3cfff678abab16f38848189966e7d:橡皮 +22d3cfff678abab1b108d382c4e6ea42:练字帖 +22d3cfff678abab1ac899d2620c5df2b:视力保护器 +dbaba36adf47af9637b3d33c282f2081:笔袋 +54e552aa1c9b2cbcacde29f76907b07f:彩泥橡皮泥 +bf164bd2e8dd8cebb108d382c4e6ea42:便条照片夹 +bf164bd2e8dd8ceb28d88a08a19453dc:便签盒座 +bf164bd2e8dd8cebe10cfa39bf43dc0f:卡套证件套 +bf164bd2e8dd8ceb6f38848189966e7d:名片册 +86cddebb2de0815c37b3d33c282f2081:名片盒 +bf164bd2e8dd8cebacde29f76907b07f:快劳夹 +86cddebb2de0815c28d88a08a19453dc:文件夹 +86cddebb2de0815cb108d382c4e6ea42:文件架 +bf164bd2e8dd8ceb1921b9e373cead75:档案盒 +bf164bd2e8dd8cebac899d2620c5df2b:档案袋 +86cddebb2de0815cac899d2620c5df2b:相册 +1ad9ac4511bbb8646f38848189966e7d:笔插 +bf164bd2e8dd8ceba7f7e02f36b0b49a:笔架 +bf164bd2e8dd8ceb267e0a01017d9f44:风琴包 +d665d5e1347fa192a7f7e02f36b0b49a:地球仪 +bf164bd2e8dd8ceb37b3d33c282f2081:展板 +d665d5e1347fa192267e0a01017d9f44:教学仪器器材 +d665d5e1347fa1921921b9e373cead75:教鞭 +bb9bba251ee78e59267e0a01017d9f44:旗帜 +d665d5e1347fa19237b3d33c282f2081:提示牌 +d665d5e1347fa192b108d382c4e6ea42:激光笔 +0f75076039b85f74acde29f76907b07f:白板 +0f75076039b85f74e10cfa39bf43dc0f:白板笔 +d665d5e1347fa19228d88a08a19453dc:粉笔 +d665d5e1347fa192acde29f76907b07f:绿板 +d665d5e1347fa1926f38848189966e7d:荧光板 +d665d5e1347fa192ac899d2620c5df2b:计划表 +d665d5e1347fa192e10cfa39bf43dc0f:软木板 +a457d6fc43c609bda7f7e02f36b0b49a:中性笔 +c230ba4ca293f3b5e10cfa39bf43dc0f:圆珠笔 +c230ba4ca293f3b51921b9e373cead75:铅芯 +f9910185f1984f2937b3d33c282f2081:正姿笔 +c230ba4ca293f3b5acde29f76907b07f:油漆笔 +c230ba4ca293f3b5b108d382c4e6ea42:泡泡笔 +c230ba4ca293f3b537b3d33c282f2081:墨水墨囊 +c230ba4ca293f3b5267e0a01017d9f44:荧光笔 +f4a071d4dba28eccac899d2620c5df2b:记号笔 +c230ba4ca293f3b56f38848189966e7d:针管笔 +dfdbd3409fadcd3f6f38848189966e7d:其他笔 +58e84885c426409e267e0a01017d9f44:书签 +b7fd03d456abe30128d88a08a19453dc:便签 +e9fa1ad466b79d97b108d382c4e6ea42:信封 +af2cf5b1faa3537a1921b9e373cead75:信纸 +b7fd03d456abe30137b3d33c282f2081:包装纸 +e9fa1ad466b79d97a7f7e02f36b0b49a:纪念册 +b7fd03d456abe301ac899d2620c5df2b:复写纸 +b7fd03d456abe301267e0a01017d9f44:奖状证书 +e9fa1ad466b79d971921b9e373cead75:手工纸 +e9fa1ad466b79d9728d88a08a19453dc:草稿纸 +b7fd03d456abe3016f38848189966e7d:日记本 +e9fa1ad466b79d97ac899d2620c5df2b:硬面抄 +b7fd03d456abe301a7f7e02f36b0b49a:记事本 +b7fd03d456abe301acde29f76907b07f:课业本 +e9fa1ad466b79d9737b3d33c282f2081:通讯录 +dbaba36adf47af966f38848189966e7d:磁性贴 +6c0543ec11db7e61267e0a01017d9f44:贴纸/标签 +0f75076039b85f741921b9e373cead75:圆规 +0f75076039b85f74ac899d2620c5df2b:显微镜 +1c75d8021bacf61e267e0a01017d9f44:放大镜 +a9ef3505c7fe4b66b108d382c4e6ea42:丙烯颜料 +823f8d7bd96780d0ac899d2620c5df2b:书法用纸 +a9ef3505c7fe4b66ac899d2620c5df2b:儿童填色本 +a9ef3505c7fe4b66267e0a01017d9f44:国画颜料 +823f8d7bd96780d037b3d33c282f2081:描图硫酸纸 +5fd3299edc3ff44a37b3d33c282f2081:毛边纸 +823f8d7bd96780d01921b9e373cead75:水彩笔 +823f8d7bd96780d0267e0a01017d9f44:水彩颜料 +823f8d7bd96780d0acde29f76907b07f:水粉水彩油画笔 +823f8d7bd96780d0e10cfa39bf43dc0f:水粉颜料 +0f75076039b85f7437b3d33c282f2081:油画棒 +0f75076039b85f74a7f7e02f36b0b49a:油画颜料 +a9ef3505c7fe4b66acde29f76907b07f:画板画架 +823f8d7bd96780d0b108d382c4e6ea42:石膏像 +823f8d7bd96780d06f38848189966e7d:素描本 +a9ef3505c7fe4b66e10cfa39bf43dc0f:绘图纸 +823f8d7bd96780d028d88a08a19453dc:色卡 +a9ef3505c7fe4b666f38848189966e7d:蜡笔 +823f8d7bd96780d0a7f7e02f36b0b49a:铅画纸 +7dba397e41d08d49a7f7e02f36b0b49a:裁剪刀片 +7dba397e41d08d49b108d382c4e6ea42:雕刻垫板 +7dba397e41d08d49ac899d2620c5df2b:切纸刀 +7dba397e41d08d4928d88a08a19453dc:美工刀 +356e5d8126d3aefaa7f7e02f36b0b49a:裁剪剪刀 +e9fa1ad466b79d97267e0a01017d9f44:回形针 +621bd460d751e0fca7f7e02f36b0b49a:回形针盒 +621bd460d751e0fcb108d382c4e6ea42:图钉工字钉 +e9fa1ad466b79d97e10cfa39bf43dc0f:大头针 +e9fa1ad466b79d97acde29f76907b07f:打孔机 +621bd460d751e0fc28d88a08a19453dc:票夹长尾夹 +e9fa1ad466b79d976f38848189966e7d:订书钉 +a457d6fc43c609bd1921b9e373cead75:凭证 +a457d6fc43c609bde10cfa39bf43dc0f:印油印泥 +a457d6fc43c609bd28d88a08a19453dc:报表 +a457d6fc43c609bd267e0a01017d9f44:湿手器 +a457d6fc43c609bdb108d382c4e6ea42:财务证明用品 +a457d6fc43c609bd6f38848189966e7d:账本账册 +740736cf215b7509a7f7e02f36b0b49a:电子壁纸 \ No newline at end of file diff --git a/planB/modules/xianYu/xianYu.go b/planB/modules/xianYu/xianYu.go new file mode 100644 index 0000000..d5466da --- /dev/null +++ b/planB/modules/xianYu/xianYu.go @@ -0,0 +1,195 @@ +package xianYu + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +var ( + gXianYuDll *XianYuDLL +) + +// XianYuDLL 闲鱼工具DLL结构 +type XianYuDLL struct { + Dll *syscall.DLL + freeCString *syscall.Proc // 释放C字符串 +} + +// InitXianYuDll 初始化 XianYuDLL +func InitXianYuDll(url string) (*XianYuDLL, error) { + if gXianYuDll != nil { + return gXianYuDll, nil + } + dllPath := filepath.Join(url, "xy.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("XianYu DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载XianYu DLL 失败: %s", err) + } + gXianYuDll = &XianYuDLL{ + Dll: dll, + freeCString: dll.MustFindProc("FreeCString"), + } + return gXianYuDll, nil +} + +// XianYuGoodsAdd 商品新增 +func (m *XianYuDLL) XianYuGoodsAdd(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsCreat") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsCreat: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuGoodsAddCheckIsbn 商品新增检查ISBN +func (m *XianYuDLL) XianYuGoodsAddNew(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsCreatNew") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsCreatNew: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuLaunchGoods 商品上架 +func (m *XianYuDLL) XianYuLaunchGoods(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsPublish") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsPublish: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuGetGoodsList 拉取商品列表 +func (m *XianYuDLL) XianYuGetGoodsList(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteSelectGoodsListPrice") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteSelectGoodsListPrice: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuGetGoodsDetail 拉取商品详情 +func (m *XianYuDLL) XianYuGetGoodsDetail(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGetGoodsDetail") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGetGoodsDetail: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuExecuteGoodsDownShelf 下架商品 +func (m *XianYuDLL) XianYuExecuteGoodsDownShelf(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsDownShelf") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsDownShelf: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuExecuteGoodsUpdateStock 修改库存 +func (m *XianYuDLL) XianYuExecuteGoodsUpdateStock(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsEditStock") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsEditStock: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuExecuteGoodsUpdatePrice 修改价格 +func (m *XianYuDLL) XianYuExecuteGoodsUpdatePrice(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsEditPrice") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsEditPrice: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} diff --git a/planB/modules/xianYu/xy.dll b/planB/modules/xianYu/xy.dll new file mode 100644 index 0000000..ad30ec5 Binary files /dev/null and b/planB/modules/xianYu/xy.dll differ diff --git a/planB/modules/xianYu/咸鱼发布dll.md b/planB/modules/xianYu/咸鱼发布dll.md new file mode 100644 index 0000000..000ac8c --- /dev/null +++ b/planB/modules/xianYu/咸鱼发布dll.md @@ -0,0 +1,239 @@ +##### FreeCString(str *C.char) + +接收其他函数返回值之后,释放内存,参考示例 + +##### 内存释放示例 + +```go +func example () { + // ...其他逻辑 + var res = StartServer (configFile *C.char) + FreeCString(res) //释放内存 +} +``` + + + +##### StartServer (configFile *C.char) + +启动http服务器,参数配置文件路径,不提供默认使用工程根目录config.ini + +返回C字符串启动消息,接收后使用FreeCString进行内存释放 + + + +##### StopServer + +停止HTTP服务器 + +返回C字符串停止消息,接收后使用FreeCString进行内存释放 + + + +##### GetServerStatus + +获取服务器当前状态 + +返回C字符串指针消息,running/stopped,接收后使用FreeCString进行内存释放 + + + +##### GetServerAddress + +获取服务器监听地址 + +返回C字符串指针服务器地址消息,未运行返回空串,接收后使用FreeCString进行内存释放 + + + +##### ReloadConfig(configFile *C.char) + +重新加载配置文件,参数配置文件路径,不提供默认使用根目录config.ini + +返回C字符串加载结果消息,接收后使用FreeCString进行内存释放 + + + + + +### 以下都需要传递appid和appSecret ### + +##### ExecuteGoodsCreat(bodyJson *C.char, configFile *C.char) + +*管道通信直接调用此函数* + +执行商品创建操作,参数商品信息,参考示例 + +返回C字符串指针创建商品结果信息,接收后使用FreeCString进行内存释放 + + + +##### 商品信息参考示例 + +```json +{ + "appId": 1228288260261189, + "appSecret": "aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k", + "token": "", + "apiShopId": 0, + "typePlatform": 4, + "shopId": 0, + "shopToken": "", + "shopName": "", + "province": 210000, + "city": 210100, + "district": 210101, + "typeClass": "", + "typeGoods": "", + "catIds": "d14d229692616168b108d382c4e6ea42", + "shop": [ + { + "userName": "xy938400231518", + "province": 210000, + "city": 210100, + "district": 210101, + "title": "牧羊少年奇幻之旅", + "content": "牧羊少年奇幻之旅", + "mainImgs": ["https://img.cdn1.vip/i/68cf5cb4e5840_1758420148.webp"], + "contentImgs": [] + } + ], + "stuffStatus": 90, + "bookData": [ + { + "ISBN": "9787530217054", + "Title": "牧羊少年奇幻之旅", + "Author": "保罗·柯艾略", + "Publisher": "北京十月文艺出版", + "itemBizType": 2, + "spBizType": 24, + "prices": [199999, 299999], + "stock": 100, + "catIds": "22e1d81dc4cf3a25a7f7e02f36b0b49a" + } + ], + "itemKey": "itemAAAAA1111" +} +``` + + + +##### ExecuteGoodsPublish(bodyJson *C.char, configFile *C.char) + +*管道通信直接调用此函数* + +执行商品上架操作,参数上架信息,参考示例 + +返回C字符串指针行商品上架结果信息,接收后使用FreeCString进行内存释放 + +##### 上架信息参考示例 + +```json +{ + "product_id": 1250927879325125, + "user_name": ["xy938400231518"], + "specify_publish_time": "", + "notify_url": "" +} +``` + + + +#### 追加下架,改价,擦亮 #### + +##### ExecuteGoodsDownShelf(bodyJson *C.char, configFile *C.char) ###### + +*管道通信直接调用此函数* + +执行商品下架操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品下架结果信息,接收后使用FreeCString进行内存释放 + +##### 下架信息参考示例 ##### + +```json +{ + "product_id": 1250927879325125 +} +``` + + + +##### ExecuteGoodsFlash(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品擦亮操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品擦亮结果信息,接收后使用FreeCString进行内存释放 + +##### 擦亮信息参考示例 ##### + +```json +{ + "product_id": 1250927879325125 +} +``` + + + +##### ExecuteGoodsEditPrice(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品改价操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 改价信息参考示例(单位:分) ##### + +```json +{ + "product_id": 1250927879325125, + "price": 550000, + "originalPrice": 770000 +} +``` + + + +##### ExecuteGoodsEditStock(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品改库存操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 改库存信息参考示例(单位:分) ##### + +```json +{ + "product_id": 1250927879325125, + "stock": 10 +} +``` + + + +##### ExecuteSelectGoodsListPrice(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +查询店铺列表操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 查询参考示例(单位:分) ##### + +```json +{ + //online_time 字段可传空 + "online_time": [ + 1690300800, + 1690366883 + ], + "product_status": 22 +} +``` + diff --git a/planB/planB.md b/planB/planB.md new file mode 100644 index 0000000..66cc211 --- /dev/null +++ b/planB/planB.md @@ -0,0 +1,29 @@ +# Plan B +## 目录结构 +```gotemplate +dispatcher 具体执行的平台操作(工厂模式,发布商品、上下架等) + |-kongfuzi 孔夫子 + |-pinduoduo 拼多多 + |-xianyu 闲鱼 +initialization 初始化 + |-config 初始化配置文件 + |-golabl 初始化全局变量 + |-platform 初始化任务平台(拼多多、闲鱼等) + |-pool 初始化协程池 + |-redis 初始化redis + |-speed 初始化限速器 + |-task 初始化任务(获取header与footer) + |-taskType 初始化任务类型(发布商品、上下架等) + |-init.go 初始化文件 +interfaces 工厂模式接口 +logic 逻辑执行 +modules DLL模块 +service 服务(针对数据库相关操作) +tool 工具 +type 结构体 + |-pinduoduo 拼多多结构体 + |-xianyu 闲鱼结构体 +validation 验证器 + + +``` \ No newline at end of file diff --git a/planB/service/delTask.go b/planB/service/delTask.go new file mode 100644 index 0000000..ebaca3d --- /dev/null +++ b/planB/service/delTask.go @@ -0,0 +1,50 @@ +package service + +import ( + "errors" + "planA/planB/initialization/golabl" + planATypeMysql "planA/type/mysql" + + "gorm.io/gorm" +) + +// GetDelTaskByTaskId 获取当前删除任务 +// @return planATypeMysql.DelTask 删除任务信息 +// @return bool 是否存在 +// @return error 错误信息 +func GetDelTaskByTaskId() (planATypeMysql.DelTask, bool, error) { + var delTask planATypeMysql.DelTask + err := golabl.MysqlDb.Where("task_id = ?", golabl.Task.TaskId).First(&delTask).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // 记录不存在 + return delTask, false, nil + } + // 其他错误 + return delTask, false, err + } + + // 记录存在 + return delTask, true, nil +} + +// CreateDelTask 创建删除任务 +// @param delTask 删除任务信息 +// @return planATypeMysql.DelTask 删除任务信息 +// @return error 错误信息 +func CreateDelTask(delTask planATypeMysql.DelTask) (planATypeMysql.DelTask, error) { + err := golabl.MysqlDb.Create(&delTask).Error + return delTask, err +} + +// AddDelTaskDetailCount 增加删除任务详情的任务数 +// @return error 错误信息 +func AddDelTaskDetailCount() error { + return golabl.MysqlDb.Model(&planATypeMysql.DelTask{}).Where("task_id = ?", golabl.Task.TaskId).Update("task_count", gorm.Expr("task_count + ?", 1)).Error +} + +// UpdateDelTaskStatusToDoing 将完成的任务状态修改为执行中 +func UpdateDelTaskStatusToDoing() error { + return golabl.MysqlDb.Model(&planATypeMysql.DelTask{}).Where("task_id = ?", golabl.Task.TaskId).Update("status", 1).Error +} diff --git a/planB/service/delTaskDetails.go b/planB/service/delTaskDetails.go new file mode 100644 index 0000000..5e0808c --- /dev/null +++ b/planB/service/delTaskDetails.go @@ -0,0 +1,107 @@ +package service + +import ( + "encoding/json" + "fmt" + "planA/planB/initialization/golabl" + planBType "planA/planB/type" + planAType "planA/type" + "time" +) + +// CreateTableIfNotExists 创建表 +// @return error 错误信息 +func CreateTableIfNotExists() error { + var dleTaskDetailsTable = fmt.Sprintf("del_task_details_%v", golabl.Task.TaskId) + // 检查表是否存在 + if !golabl.MysqlDb.Migrator().HasTable(dleTaskDetailsTable) { + sql := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s ( + id int(11) NOT NULL AUTO_INCREMENT, + del_task_id int(11) DEFAULT '0' COMMENT '删除任务id', + task_id varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT '任务id', + isbn varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT 'isbn', + book_name varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT '商品名称', + token varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT 'token', + goods_id bigint(11) DEFAULT NULL COMMENT '商品id', + json text COLLATE utf8_unicode_ci COMMENT '原始字符串', + status int(11) DEFAULT '0' COMMENT '状态: 1=正常 2=错误', + err text COLLATE utf8_unicode_ci COMMENT '错误信息', + delete_at datetime DEFAULT NULL COMMENT '请求删除商品时间', + delete_date date DEFAULT NULL COMMENT '请求删除商品日期', + create_at datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (id), + KEY del_task_id (del_task_id, task_id, goods_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci`, dleTaskDetailsTable) + + if err := golabl.MysqlDb.Exec(sql).Error; err != nil { + return fmt.Errorf("创建 %v 表失败: %v", dleTaskDetailsTable, err) + } + } + return nil +} + +// InsertDelTaskDetail 插入单条删除任务详情数据 +func InsertDelTaskDetail(delTaskID int64, detail planAType.TaskBody) error { + var dleTaskDetailsTable = fmt.Sprintf("del_task_details_%v", golabl.Task.TaskId) + + // 先检查并创建表 + if err := CreateTableIfNotExists(); err != nil { + return err + } + + now := time.Now() + + //将detail转为 json + jsonByte, err := json.Marshal(detail) + if err != nil { + return err + } + jsonStr := string(jsonByte) + + delTaskDetail := &planBType.DelTaskDetail{ + DelTaskID: &delTaskID, + TaskID: &golabl.Task.TaskId, + BookName: &detail.BookInfo.BookName, + Token: &golabl.Task.Header.ShopMsg.Token, + Isbn: &detail.BookInfo.Isbn, + GoodsID: &detail.Detail.GoodsId, + JSON: &jsonStr, + DeleteAt: nil, + DeleteDate: nil, + CreateAt: &now, + } + + // 使用动态表名插入 + result := golabl.MysqlDb.Table(dleTaskDetailsTable).Create(delTaskDetail) + if result.Error != nil { + return fmt.Errorf("插入数据失败: %v", result.Error) + } + return nil +} + +// BatchInsertDelTaskDetails 批量插入删除任务详情数据 +func BatchInsertDelTaskDetails(details []planBType.DelTaskDetail) error { + + var dleTaskDetailsTable = fmt.Sprintf("del_task_details_%v", golabl.Task.TaskId) + // 先检查并创建表 + if err := CreateTableIfNotExists(); err != nil { + return err + } + + if len(details) == 0 { + return nil + } + + now := time.Now() + for i := range details { + details[i].CreateAt = &now + } + + // 批量插入,每批1000条 + batchSize := 1000 + result := golabl.MysqlDb.Table(dleTaskDetailsTable).CreateInBatches(details, batchSize) + if result.Error != nil { + return fmt.Errorf("批量插入数据失败: %v", result.Error) + } + return nil +} diff --git a/planB/service/pddNoticeMsg.go b/planB/service/pddNoticeMsg.go new file mode 100644 index 0000000..43c1d80 --- /dev/null +++ b/planB/service/pddNoticeMsg.go @@ -0,0 +1,21 @@ +package service + +import "planA/planB/initialization/golabl" + +// GetPddNoticeMsg 获取拼多多通知消息 +// @param shopId 店铺ID +// @return []string 消息列表 +// @return error 错误 +func GetPddNoticeMsg(shopId string) ([]string, error) { + // 测试 client 是否可用 + pingErr := golabl.Redis.RedisDbA.Ping(golabl.Ctx).Err() + if pingErr != nil { + return []string{}, pingErr + } + //获取所有list数据 + list, lRangeErr := golabl.Redis.RedisDbA.LRange(golabl.Ctx, shopId, 0, -1).Result() + if lRangeErr != nil { + return []string{}, lRangeErr + } + return list, nil +} diff --git a/planB/service/publishing.go b/planB/service/publishing.go new file mode 100644 index 0000000..6206c98 --- /dev/null +++ b/planB/service/publishing.go @@ -0,0 +1,37 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "planA/planB/initialization/golabl" + planAType "planA/type" + + "github.com/go-redis/redis/v8" +) + +// GetPublishingVid 获取出版社信息Vid +// @param taskMsg 任务信息 +// @return _type.Publishing 出版社信息 +func GetPublishingVid(taskMsg *planAType.TaskBody) error { + var publishing planAType.Publishing + //获取出版社信息 + publishingStr, getErr := golabl.Redis.RedisDbB.Get(golabl.Ctx, "publisher:name:"+taskMsg.BookInfo.Publishing).Result() + if getErr != nil { + // 出版社不存在,给个默认的 + if errors.Is(getErr, redis.Nil) { + publishing.Value = "北京大学出版社" + publishing.Vid = 483727 + taskMsg.Publishing = publishing + return nil + } + return getErr + } + //转为结构体 + unmarshalErr := json.Unmarshal([]byte(publishingStr), &publishing) + if unmarshalErr != nil { + return fmt.Errorf("出版社json转结构体失败 %v", unmarshalErr) + } + taskMsg.Publishing = publishing + return nil +} diff --git a/planB/service/region.go b/planB/service/region.go new file mode 100644 index 0000000..35b6dfe --- /dev/null +++ b/planB/service/region.go @@ -0,0 +1,72 @@ +package service + +import ( + "fmt" + "planA/planB/initialization/golabl" + "strconv" +) + +// GetRegionId 根据地区Id获取获取省市信息 +// @param districtID 地区ID +// @return int 省份ID +// @return int 城市ID +// @return int 区县ID +// @return error 错误信息 +func GetRegionId(districtID string) (int, int, int, error) { + //获取区县 code + district, err := golabl.Redis.RedisDbC.HGetAll(golabl.Ctx, fmt.Sprintf("region:%s", districtID)).Result() + if err != nil { + return 0, 0, 0, err + } + // 将 district["code"] 转为 int + districtCode, districtErr := strconv.Atoi(district["code"]) + if districtErr != nil { + return 0, 0, 0, fmt.Errorf("区县code转换失败 id %v %v", districtID, districtErr) + } + //获取市 code + city, err := golabl.Redis.RedisDbC.HGetAll(golabl.Ctx, fmt.Sprintf("region:%s", district["pid"])).Result() + if err != nil { + return 0, 0, 0, err + } + // 将 city["code"] 转为 int + cityCode, cityErr := strconv.Atoi(city["code"]) + if cityErr != nil { + return 0, 0, 0, fmt.Errorf("市code转换失败 id %v %v", district["pid"], err) + } + //获取市 province + province, err := golabl.Redis.RedisDbC.HGetAll(golabl.Ctx, fmt.Sprintf("region:%s", city["pid"])).Result() + if err != nil { + return 0, 0, 0, err + } + // 将 province["code"] 转为 int + provinceCode, provinceErr := strconv.Atoi(province["code"]) + if provinceErr != nil { + return 0, 0, 0, fmt.Errorf("省code转换失败 id %v %v", city["pid"], err) + } + return provinceCode, cityCode, districtCode, nil +} + +// GetRandomDistrictInProvince 在指定省内随机获取一个区级地区 +func GetRandomDistrictInProvince(provinceID int) (map[string]string, error) { + // 从该省份的区级地区集合中随机获取一个 ID + provinceKey := fmt.Sprintf("province:%d:districts", provinceID) + districtID, err := golabl.Redis.RedisDbC.SRandMember(golabl.Ctx, provinceKey).Result() + if err != nil { + return nil, err + } + + // 获取该地区的详细信息 + return golabl.Redis.RedisDbC.HGetAll(golabl.Ctx, fmt.Sprintf("region:%s", districtID)).Result() +} + +// GetRandomDistrict 随机获取一个区级地区 +func GetRandomDistrict() (map[string]string, error) { + // 从所有区级地区集合中随机获取一个 ID + districtID, err := golabl.Redis.RedisDbC.SRandMember(golabl.Ctx, "all:districts").Result() + if err != nil { + return nil, err + } + + // 获取该地区的详细信息 + return golabl.Redis.RedisDbC.HGetAll(golabl.Ctx, fmt.Sprintf("region:%s", districtID)).Result() +} diff --git a/planB/service/task.go b/planB/service/task.go new file mode 100644 index 0000000..d2b3afa --- /dev/null +++ b/planB/service/task.go @@ -0,0 +1,543 @@ +package service + +import ( + "encoding/json" + "fmt" + "planA/planB/initialization/golabl" + planAType "planA/type" + "strconv" + "strings" +) + +// GetTaskHeader 获取任务头 +// @param header *_type.TaskHeader 任务头 +// @return error 错误信息 +func GetTaskHeader() error { + // 测试 client 是否可用 + pingErr := golabl.Redis.RedisDbA.Ping(golabl.Ctx).Err() + if pingErr != nil { + return pingErr + } + // 拼接 key 值 + headerKey := fmt.Sprintf("%s:header", golabl.Task.TaskId) + // 获取 header 数据 + headerData, hGetAllErr := golabl.Redis.RedisDbA.HGetAll(golabl.Ctx, headerKey).Result() + if hGetAllErr != nil { + return fmt.Errorf("获取 header 失败 %w", hGetAllErr) + } + // 判断 headerData 是否为空 + if headerData == nil || len(headerData) == 0 { + return fmt.Errorf("获取 header 失败 %s", "任务信息为空") + } + // 解析 header 数据 + parseTaskHeaderErr := parseTaskHeader(headerData) + if parseTaskHeaderErr != nil { + return fmt.Errorf("解析 header 失败 %w", parseTaskHeaderErr) + } + // 返回结果 + return nil +} + +// GetTaskFooter 获取任务尾 +// @param error 错误信息 +func GetTaskFooter() error { + // 测试 client 是否可用 + pingErr := golabl.Redis.RedisDbA.Ping(golabl.Ctx).Err() + if pingErr != nil { + return pingErr + } + // 拼接 key 值 + footerKey := fmt.Sprintf("%s:footer", golabl.Task.TaskId) + // 获取 footer 数据 + footerData, HGetAllErr := golabl.Redis.RedisDbA.HGetAll(golabl.Ctx, footerKey).Result() + if HGetAllErr != nil { + return fmt.Errorf("获取 footer 失败: %w", HGetAllErr) + } + + // 解析 footer 数据 + parseTaskFooterErr := parseTaskFooter(footerData, golabl.Task.Footer) + if parseTaskFooterErr != nil { + return fmt.Errorf("解析 footer 失败: %w", parseTaskFooterErr) + } + + // 返回结果 + return nil +} + +// UpdateTaskHeaderCount 更新任务头 +// @return error 错误信息 +func UpdateTaskHeaderCount() error { + // 测试 client 是否可用 + err := golabl.Redis.RedisDbA.Ping(golabl.Ctx).Err() + if err != nil { + return err + } + + // 检查键是否存在 + exists, existsErr := golabl.Redis.RedisDbA.Exists(golabl.Ctx, golabl.Task.TaskId+":header").Result() + if existsErr != nil { + return existsErr + } + + // 键不存在 + if exists == 0 { + return fmt.Errorf("任务不存在%v", golabl.Task.TaskId) + } + + // 使用 Pipeline 逐个字段更新 + pipe := golabl.Redis.RedisDbA.Pipeline() + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":header", "task_count_wait", golabl.Task.Header.TaskCountWait) + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":header", "task_count_over", golabl.Task.Header.TaskCountOver) + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":header", "task_count_success", golabl.Task.Header.TaskCountSuccess) + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":header", "task_count_error", golabl.Task.Header.TaskCountError) + _, ExecErr := pipe.Exec(golabl.Ctx) + if ExecErr != nil { + return ExecErr + } + + // 返回结果 + return nil +} + +// UpdateTaskFooter 更新任务尾 +// @param returnErr int64 类型 1=正确 2= 错误 +// @param count int64 类型 更新的数据 +// @return error 错误信息 +func UpdateTaskFooter(returnErr int64, count int64) error { + // 测试 client 是否可用 + err := golabl.Redis.RedisDbA.Ping(golabl.Ctx).Err() + if err != nil { + return err + } + + // 检查键是否存在 + footerKey := golabl.Task.TaskId + ":footer" + exists, existsErr := golabl.Redis.RedisDbA.Exists(golabl.Ctx, footerKey).Result() + if existsErr != nil { + return existsErr + } + // 键不存在 + if exists == 0 { + return fmt.Errorf("任务不存在%v", golabl.Task.TaskId) + } + + // 使用 Pipeline 逐个字段更新 + pipe := golabl.Redis.RedisDbA.Pipeline() + // 更新任务尾 + if returnErr == 1 { + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_success", count) + } else { + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_error", count) + } + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_wait", -count) + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_over", count) + _, ExecErr := pipe.Exec(golabl.Ctx) + if ExecErr != nil { + return ExecErr + } + + // 返回结果 + return nil +} + +// GetTaskToPopFromBodyWait 获取任务信息 +// @return _type.TaskBody 任务信息 +// @return error 错误信息 +func GetTaskToPopFromBodyWait() (planAType.TaskBody, error) { + // 测试 client 是否可用 + pingErr := golabl.Redis.RedisDbA.Ping(golabl.Ctx).Err() + if pingErr != nil { + return planAType.TaskBody{}, pingErr + } + // 获取 body 数据 + bodyData, rPopErr := golabl.Redis.RedisDbA.LPop(golabl.Ctx, golabl.Task.TaskId+":body_wait").Result() + if rPopErr != nil { + return planAType.TaskBody{}, rPopErr + } + + // 判断 body 数据是否为空 + if bodyData == "" { + return planAType.TaskBody{}, fmt.Errorf("任务详情信息为空") + } + // 解析 bodyDetail 数据 + taskBody, parseTaskBodyErr := parseTaskBody(bodyData) + if parseTaskBodyErr != nil { + return planAType.TaskBody{}, fmt.Errorf("解析任务详情信息失败: %v\n", parseTaskBodyErr) + } + // 返回结果 + return taskBody, nil +} + +// SetNoImgCount 无图片信息isbn计次 +// @param isbn +// @return error 错误信息 +func SetNoImgCount(isbn string) error { + key := "noImgInfo" + return golabl.Redis.RedisDbD.ZIncrBy(golabl.Ctx, key, 1, isbn).Err() +} + +// AddTaskToBodyOver 添加任务到完成任务池 +// @param taskBody _type.TaskBody 任务信息 +// @param addType []string 写入类型 ["body_over","body_data","body_backup"] +// @return error 错误信息 +func AddTaskToBodyOver(taskBody planAType.TaskBody, addType []string) error { + // 测试 client 是否可用 + pingErr := golabl.Redis.RedisDbA.Ping(golabl.Ctx).Err() + if pingErr != nil { + return pingErr + } + + // 序列化任务数据 + taskBodyStr, jsonMarshalErr := json.Marshal(taskBody) + if jsonMarshalErr != nil { + return fmt.Errorf("任务信息转换失败: %v\n", jsonMarshalErr) + } + + // 使用事务确保 LPUSH 操作的原子性 + pipe := golabl.Redis.RedisDbA.TxPipeline() + + // 判断需要写入哪些类型 + // 如果 addType 为空数组,则全部写入 + if len(addType) == 0 { + // 全部写入 + pipe.LPush(golabl.Ctx, golabl.Task.TaskId+":body_over", taskBodyStr) + pipe.LPush(golabl.Ctx, golabl.Task.TaskId+":body_data", taskBodyStr) + pipe.LPush(golabl.Ctx, golabl.Task.TaskId+":body_backup", taskBodyStr) + } else { + // 根据传入的类型动态写入 + for _, t := range addType { + switch t { + case "body_over": + pipe.LPush(golabl.Ctx, golabl.Task.TaskId+":body_over", taskBodyStr) + case "body_data": + pipe.LPush(golabl.Ctx, golabl.Task.TaskId+":body_data", taskBodyStr) + case "body_backup": + pipe.LPush(golabl.Ctx, golabl.Task.TaskId+":body_backup", taskBodyStr) + default: + // 忽略未知类型,或者可以根据需要返回错误 + continue + } + } + } + + // 执行事务 + _, execErr := pipe.Exec(golabl.Ctx) + if execErr != nil { + return fmt.Errorf("添加任务到完成任务池失败: %v\n", execErr) + } + + // 返回结果 + return nil +} + +// GetTaskBodyWaitCount 获取指定body_wait的真实数量 +func GetTaskBodyWaitCount() (int64, error) { + return golabl.Redis.RedisDbA.LLen(golabl.Ctx, golabl.Task.TaskId+":body_wait").Result() +} + +// IsTaskBodyWaitExist 查询body_wait是否存在 +func IsTaskBodyWaitExist() (bool, error) { + count, err := golabl.Redis.RedisDbA.Exists(golabl.Ctx, golabl.Task.TaskId+":body_wait").Result() + if err != nil { + return false, err + } + return count > 0, nil +} + +// GetTaskBodyWaitLast 获取body_wait中最后一条数据 +func GetTaskBodyWaitLast() (string, error) { + return golabl.Redis.RedisDbA.LIndex(golabl.Ctx, golabl.Task.TaskId+":body_wait", 0).Result() +} + +// AddTaskToBodyWait 写入到body_wait中 +// @param taskBody _type.TaskBody 任务信息 +// @return error 错误信息 +func AddTaskToBodyWait(bodyWaitJson string) error { + return golabl.Redis.RedisDbA.LPush(golabl.Ctx, golabl.Task.TaskId+":body_wait", bodyWaitJson).Err() +} + +// GetTaskBodyWaitList 读取body_wait 数据 +// @param page int 页码 +// @param pageSize int 页大小 +// @return []string body_wait 数据 +// @return error 错误信息 +func GetTaskBodyWaitList(page int, pageSize int) ([]string, error) { + // 计算起始索引和结束索引 + // Redis LRange 使用 0-based 索引 + // 第1页: 0 到 pageSize-1 + // 第2页: pageSize 到 2*pageSize-1 + start := (page - 1) * pageSize + end := start + pageSize - 1 + + // 如果页码小于1,默认为第1页 + if page < 1 { + page = 1 + start = 0 + end = pageSize - 1 + } + + // 如果页大小小于1,设置默认值 + if pageSize < 1 { + pageSize = 10 + end = start + pageSize - 1 + } + + // 获取指定范围的数据 + return golabl.Redis.RedisDbA.LRange(golabl.Ctx, golabl.Task.TaskId+":body_wait", int64(start), int64(end)).Result() +} + +// DeleteTaskBodyWait 删除body_wait 数据 +func DeleteTaskBodyWait() error { + return golabl.Redis.RedisDbA.Del(golabl.Ctx, golabl.Task.TaskId+":body_wait").Err() +} + +// DeleteTaskBodyOver 删除body_over 数据 +func DeleteTaskBodyOver() error { + return golabl.Redis.RedisDbA.Del(golabl.Ctx, golabl.Task.TaskId+":body_over").Err() +} + +// DeleteTaskBodyBackup 删除body_backup 数据 +func DeleteTaskBodyBackup() error { + return golabl.Redis.RedisDbA.Del(golabl.Ctx, golabl.Task.TaskId+":body_backup").Err() +} + +// SetTaskCount 设置 任务数 +// @param value string +// @return error 错误信息 +func SetTaskCount(value string) error { + + // 使用事务确保 操作的原子性 + pipe := golabl.Redis.RedisDbA.TxPipeline() + + taskCountTrueKey := "task_count_true" + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":header", taskCountTrueKey, value) + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":footer", taskCountTrueKey, value) + + taskCountWaitKey := "task_count_wait" + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":header", taskCountWaitKey, value) + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":footer", taskCountWaitKey, value) + + taskCountOverKey := "task_count_over" + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":header", taskCountOverKey, 0) + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":footer", taskCountOverKey, 0) + + taskCountSuccessKey := "task_count_success" + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":header", taskCountSuccessKey, 0) + pipe.HSet(golabl.Ctx, golabl.Task.TaskId+":footer", taskCountSuccessKey, 0) + + // 执行事务 + _, execErr := pipe.Exec(golabl.Ctx) + if execErr != nil { + return fmt.Errorf("更新 总任务数失败: %v\n", execErr) + } + return nil +} + +// UpdateTaskStatus 更新任务状态 +// @param status int 任务状态 +// @return error 错误信息 +func UpdateTaskStatus(status int) error { + return golabl.Redis.RedisDbA.HSet(golabl.Ctx, golabl.Task.TaskId+":header", "status", status).Err() +} + +// =========================== 以下是私有方法 =========================== + +// 解析任务头 +func parseTaskHeader(taskHeader map[string]string) error { + + // 解析 header task_id + if golabl.Task.Header.TaskId = taskHeader["task_id"]; golabl.Task.Header.TaskId == "" { + return fmt.Errorf("参数错误: %s", "task_id 为 空") + } + // 解析 header shop_id + if golabl.Task.Header.ShopId = taskHeader["shop_id"]; golabl.Task.Header.ShopId == "" { + return fmt.Errorf("参数错误: %s", "shop_id 为 空") + } + // 解析 header shop_name + if golabl.Task.Header.ShopName, _ = taskHeader["shop_name"]; golabl.Task.Header.ShopName == "" { + return fmt.Errorf("参数错误: %s", "shop_name 为 空") + } + // 解析 header shop_msg + err := json.Unmarshal([]byte(taskHeader["shop_msg"]), &golabl.Task.Header.ShopMsg) + if err != nil { + return fmt.Errorf("参数错误: %s", "shop_msg 转结构体失败 shopMsg:="+taskHeader["shop_msg"]) + } + // 解析 header price_mod + err = json.Unmarshal([]byte(taskHeader["price_mod"]), &golabl.Task.Header.PriceMod) + if err != nil { + return fmt.Errorf("参数错误: %s", "price_mod 转结构体失败 priceMod:="+taskHeader["price_mod"]) + } + + // 解析 header ship_price_mod + //if header.ShipPriceMod, _ = taskHeader["ship_price_mod"]; header.ShipPriceMod == "" { + // return fmt.Errorf("参数错误: %s", "ship_price_mod 为 空") + //} + // 解析 header task_type + if golabl.Task.Header.TaskType, _ = strconv.ParseInt(taskHeader["task_type"], 10, 64); golabl.Task.Header.TaskType == 0 { + return fmt.Errorf("参数错误: %s", "task_type 为 空") + } + // 解析 header shop_type + if golabl.Task.Header.ShopType, _ = taskHeader["shop_type"]; golabl.Task.Header.ShopType == "" { + return fmt.Errorf("参数错误: %s", "shop_type 为 空") + } + // 解析 header price_type + if golabl.Task.Header.PriceType, _ = taskHeader["price_type"]; golabl.Task.Header.PriceType == "" { + return fmt.Errorf("参数错误: %s", "price_type 为 空") + } + // 解析 header task_count + if golabl.Task.Header.TaskCount, _ = strconv.ParseInt(taskHeader["task_count"], 10, 64); golabl.Task.Header.TaskCount == 0 { + //return fmt.Errorf("参数错误: %s", "task_count 为 空") + } + // 解析 header task_count_true + if golabl.Task.Header.TaskCountTrue, _ = strconv.ParseInt(taskHeader["task_count_true"], 10, 64); golabl.Task.Header.TaskCountTrue == 0 { + //return fmt.Errorf("参数错误: %s ", "task_count_true 为 空") + } + // 解析 header task_count_wait + if golabl.Task.Header.TaskCountWait, _ = strconv.ParseInt(taskHeader["task_count_wait"], 10, 64); golabl.Task.Header.TaskCountWait == 0 { + //return fmt.Errorf("参数错误: %s", "task_count_wait 为 空") + } + // 解析 header task_count_over + if golabl.Task.Header.TaskCountOver, _ = strconv.ParseInt(taskHeader["task_count_over"], 10, 64); golabl.Task.Header.TaskCountOver == 0 { + //return fmt.Errorf("参数错误: %s", "task_count_over 为 空") + } + // 解析 header task_count_success + if golabl.Task.Header.TaskCountSuccess, _ = strconv.ParseInt(taskHeader["task_count_success"], 10, 64); golabl.Task.Header.TaskCountSuccess == 0 { + //return fmt.Errorf("参数错误: %s", "task_count_success 为 空") + } + // 解析 header task_count_error + if golabl.Task.Header.TaskCountError, _ = strconv.ParseInt(taskHeader["task_count_error"], 10, 64); golabl.Task.Header.TaskCountError == 0 { + //return fmt.Errorf("参数错误: %s", "task_count_error 为 空") + } + // 将headerData["status"] 转换为 TaskStatus + taskStatus, _ := strconv.ParseInt(taskHeader["status"], 10, 64) + // 解析 header status + if golabl.Task.Header.Status = planAType.TaskStatus(taskStatus); golabl.Task.Header.Status == 5 { + return fmt.Errorf("参数错误: %s", "Status 为 已完成任务") + } + // 解析 header task_qpm + if golabl.Task.Header.TaskQpm, _ = strconv.ParseInt(taskHeader["task_qpm"], 10, 64); golabl.Task.Header.TaskQpm == 0 { + //return fmt.Errorf("参数错误: %s", "task_qpm 为 空") + } + // 解析 header task_create_at + if golabl.Task.Header.TaskCreateAt, _ = strconv.ParseInt(taskHeader["task_create_at"], 10, 64); golabl.Task.Header.TaskCreateAt == 0 { + //return fmt.Errorf("参数错误: %s", "task_create_at 为 空") + } + // 解析 header task_over_at + if golabl.Task.Header.TaskOverAt, _ = strconv.ParseInt(taskHeader["task_over_at"], 10, 64); golabl.Task.Header.TaskOverAt == 0 { + //return fmt.Errorf("参数错误: %s", "task_over_at 为 空") + } + // 解析 header last_index + if golabl.Task.Header.LastIndex, _ = strconv.ParseInt(taskHeader["last_index"], 10, 64); golabl.Task.Header.LastIndex == 0 { + //return fmt.Errorf("参数错误: %s", "last_index 为 空") + } + // 解析 header img_type + if golabl.Task.Header.ImgType, _ = strconv.ParseInt(taskHeader["img_type"], 10, 64); golabl.Task.Header.ImgType == 0 { + //return fmt.Errorf("参数错误: %s", "last_index 为 空") + } + // 解析 header pool + if taskHeader["pool"] != "" { + err = json.Unmarshal([]byte(taskHeader["pool"]), &golabl.Task.Header.Pool) + if err != nil { + return fmt.Errorf("参数错误: %s", "pool 转结构体失败 pool:="+taskHeader["pool"]) + } + } else { + // 空字符串时,初始化为空的切片或结构体 + golabl.Task.Header.Pool = planAType.PoolConfig{} // 如果是切片类型 + } + + // 返回结果 + return nil +} + +// 解析任务尾 +func parseTaskFooter(taskFooter map[string]string, footer *planAType.TaskFooter) error { + // 解析 footer task_count + if footer.TaskCount, _ = strconv.ParseInt(taskFooter["task_count"], 10, 64); footer.TaskCount == 0 { + } + // 解析 footer task_count_true + if footer.TaskCountTrue, _ = strconv.ParseInt(taskFooter["task_count_true"], 10, 64); footer.TaskCountTrue == 0 { + } + // 解析 footer task_count_wait + taskCountWait, _ := strconv.ParseInt(taskFooter["task_count_wait"], 10, 64) + if taskCountWait == 0 { + } + footer.TaskCountWait.Store(taskCountWait) + // 解析 footer task_count_over + taskCountOver, _ := strconv.ParseInt(taskFooter["task_count_over"], 10, 64) + if taskCountOver == 0 { + } + footer.TaskCountOver.Store(taskCountOver) + // 解析 footer task_count_success + taskCountSuccess, _ := strconv.ParseInt(taskFooter["task_count_success"], 10, 64) + if taskCountSuccess == 0 { + } + footer.TaskCountSuccess.Store(taskCountSuccess) + // 解析 footer task_count_error + taskCountError, _ := strconv.ParseInt(taskFooter["task_count_error"], 10, 64) + if taskCountError == 0 { + } + footer.TaskCountError.Store(taskCountError) + // 解析 footer task_qpm + if footer.TaskQpm, _ = strconv.ParseInt(taskFooter["task_qpm"], 10, 64); footer.TaskQpm == 0 { + } + // 解析 footer last_index + if footer.LastIndex, _ = strconv.ParseInt(taskFooter["last_index"], 10, 64); footer.LastIndex == 0 { + } + + // 返回结果 + return nil +} + +// 解析任务主体 +func parseTaskBody(taskBodyStr string) (planAType.TaskBody, error) { + // 初始化 body + var body planAType.TaskBody + // 解析 bookInfo 到 结构体 + UnmarshalErr := json.Unmarshal([]byte(taskBodyStr), &body) + if UnmarshalErr != nil { + return planAType.TaskBody{}, UnmarshalErr + } + + // 返回结果 + return body, nil +} + +// ============================================ +// 店铺信息操作 +// 数据结构: 支持String/List/Hash多种类型 +// 键格式: {shopID} +// ============================================ + +// GetTaskShop 获取店铺信息 +// @param shopID 店铺ID +// @return string 店铺信息字符串 +// @return error 错误信息 +func GetTaskShop(shopID string) (string, error) { + // 检查键类型 + keyType, err := golabl.Redis.RedisDbE.Type(golabl.Ctx, shopID).Result() + if err != nil { + return "", fmt.Errorf("检查Redis key类型失败: %w", err) + } + switch keyType { + case "string": + return golabl.Redis.RedisDbE.Get(golabl.Ctx, shopID).Result() + + case "list": + items, err := golabl.Redis.RedisDbE.LRange(golabl.Ctx, shopID, 0, -1).Result() + if err != nil { + return "", fmt.Errorf("获取list数据失败: %w", err) + } + return "[" + strings.Join(items, ",") + "]", nil + + case "hash": + hashData, err := golabl.Redis.RedisDbE.HGetAll(golabl.Ctx, shopID).Result() + if err != nil { + return "", fmt.Errorf("获取hash数据失败: %w", err) + } + jsonData, _ := json.Marshal(hashData) + return string(jsonData), nil + + default: + return "", fmt.Errorf("不支持的数据类型: %s", keyType) + } +} diff --git a/planB/shopId.txt b/planB/shopId.txt new file mode 100644 index 0000000..588eebd --- /dev/null +++ b/planB/shopId.txt @@ -0,0 +1,4 @@ +2026324046684069890 +2045316145789939713 +2045096232894738434 +2042843272765263874 \ No newline at end of file diff --git a/planB/tool/a.go b/planB/tool/a.go new file mode 100644 index 0000000..d1ed157 --- /dev/null +++ b/planB/tool/a.go @@ -0,0 +1,35 @@ +package tool + +import ( + "encoding/json" + "fmt" + "planA/planB/initialization/golabl" + planBType "planA/planB/type" +) + +// NotifyA 通知A程序任务完成 +// @return error 错误信息 +func NotifyA() error { + httpTaskStatusOverUrl := golabl.Config.HttpUrl.TaskUrl + "/task/over/" + golabl.Task.TaskId + httpCode, httpTaskStatusOverBody, httpTaskStatusOverErr := HttpGetRequest(httpTaskStatusOverUrl) + if httpTaskStatusOverErr != nil { + return fmt.Errorf("通知A程序任务完成失败-原因来自:%v", httpTaskStatusOverErr) + } + // 对通知结果状态进行判断 + var httpTaskStatusOverRes planBType.HttpRes + unmarshalErr := json.Unmarshal([]byte(httpTaskStatusOverBody), &httpTaskStatusOverRes) + if unmarshalErr != nil { + return fmt.Errorf("通知A程序任务完成失败-原因来自 json.Unmarshal错误: %w %v", unmarshalErr, httpTaskStatusOverBody) + } + if httpTaskStatusOverRes.Code != "200" { + return fmt.Errorf("通知A程序任务完成失败-原因来自: url=%v httpCode=%v A程序返回信息=%v\n", httpTaskStatusOverUrl, httpCode, httpTaskStatusOverBody) + } + return nil +} + +// PauseTask 暂停B程序运行 +// @return error 错误信息 +func PauseTask() error { + _, _, err := HttpGetRequest(golabl.Config.HttpUrl.TaskUrl + "/task/pause/" + golabl.Task.TaskId) + return err +} diff --git a/planB/tool/filterWord.go b/planB/tool/filterWord.go new file mode 100644 index 0000000..77645dc --- /dev/null +++ b/planB/tool/filterWord.go @@ -0,0 +1,53 @@ +package tool + +import ( + "encoding/json" + "fmt" + "planA/planB/initialization/golabl" + planBType "planA/planB/type" +) + +// HttpFilterWord 违禁词处理 +// @param isbn ISBN +// @param bookName 书名 +// @param author 作者 +// @param publishing 出版社 +// @return planBType.HttpFilterWordRes 违禁词处理结果 +// @return error 错误信息 +func HttpFilterWord(isbn, bookName, author, publishing string) (planBType.HttpFilterWordRes, error) { + var resDta planBType.HttpFilterWordRes + + //请求数据 + filterWordReq := map[string]string{ + "isbn": fmt.Sprintf("%v", isbn), + "bookName": fmt.Sprintf("%v", bookName), + "author": fmt.Sprintf("%v", author), + "publisher": fmt.Sprintf("%v", publishing), + "shopId": golabl.Task.Header.ShopId, + "replaceMark": golabl.Config.Server.ReplaceMark, + } + // 构建带参数的 URL + reqUrl, err := BuildURLWithParams(golabl.Config.FileUrl.BannedWordSubstitutionUrl, filterWordReq) + if err != nil { + return resDta, fmt.Errorf("构建URL失败: %v", err) + } + + // 发送 GET请求 + _, resStr, httpGetRequestErr := HttpGetRequest(reqUrl) + + if httpGetRequestErr != nil { + return resDta, httpGetRequestErr + } + + // 将字符串转换为结构体 + jsonErr := json.Unmarshal([]byte(resStr), &resDta) + if jsonErr != nil { + return resDta, jsonErr + } + + if resDta.Code != "200" { + return resDta, fmt.Errorf("请求违禁词接口错误 错误: url %s %s", reqUrl, resStr) + } + // 返回结果 + return resDta, nil +} diff --git a/planB/tool/http.go b/planB/tool/http.go new file mode 100644 index 0000000..76af12a --- /dev/null +++ b/planB/tool/http.go @@ -0,0 +1,136 @@ +package tool + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "time" +) + +// HttpGetRequest 发起 GET 请求 +// @param url 请求地址 +// @return int 响应状态码 +// @return string 响应内容 +// @return error 错误信息 +func HttpGetRequest(url string) (int, string, error) { + resp, httpGetErr := http.Get(url) + if httpGetErr != nil { + return 0, "", fmt.Errorf("http get 请求失败: %v %v", url, httpGetErr) + } + defer resp.Body.Close() // 重要:必须关闭响应体 + + // 读取响应内容 + body, err := io.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, "", fmt.Errorf("http get 读取响应失败: %v %v", url, err) + } + return resp.StatusCode, string(body), nil +} + +// SubmitFormData 提交表单数据 +// @param url 请求地址 +// @param params 表单数据 +// @return error 错误信息 +func SubmitFormData(url string, params map[string]string) (string, error) { + // 创建multipart writer + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // 添加文本字段 + for key, value := range params { + err := writer.WriteField(key, value) + if err != nil { + return "", fmt.Errorf("write field error: %v", err) + } + } + + // 关闭writer + writer.Close() + + // 创建请求 + req, err := http.NewRequest("POST", url, body) + if err != nil { + return "", fmt.Errorf("create request error: %v", err) + } + + // 设置Content-Type + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("send request error: %v", err) + } + defer resp.Body.Close() + + // 读取响应 + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read response error: %v", err) + } + + return string(respBody), nil +} + +// BuildURLWithParams 将map参数拼接到URL后面 +func BuildURLWithParams(baseURL string, params map[string]string) (string, error) { + if len(params) == 0 { + return baseURL, nil + } + + // 解析基础URL + parsedURL, err := url.Parse(baseURL) + if err != nil { + return "", fmt.Errorf("解析URL失败: %v", err) + } + + // 获取现有的查询参数 + query := parsedURL.Query() + + // 添加新的参数 + for key, value := range params { + query.Set(key, value) + } + // 重新编码查询参数 + parsedURL.RawQuery = query.Encode() + + return parsedURL.String(), nil +} + +// PostJSON 发送HTTP POST JSON请求 +// 参数: +// +// url: 请求的URL地址 +// jsonStr: 请求的JSON字符串 +// +// 返回: +// +// responseBody: 响应体内容 +// statusCode: HTTP状态码 +// error: 错误信息 +func PostJSON(url string, jsonStr string) (responseBody string, err error) { + var client = &http.Client{Timeout: 15 * time.Second} + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(jsonStr))) + if err != nil { + return "", fmt.Errorf("创建请求失败: %w", err) + } + + // 设置 JSON 请求头 + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("请求失败: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取响应失败: %w", err) + } + return string(respBody), nil +} diff --git a/planB/tool/isbn.go b/planB/tool/isbn.go new file mode 100644 index 0000000..d405aff --- /dev/null +++ b/planB/tool/isbn.go @@ -0,0 +1,104 @@ +package tool + +import ( + "regexp" + "strings" +) + +// ExtractISBN978 从字符串中提取 978 开头的 ISBN-13 +// 返回第一个匹配到的 ISBN,如果没有匹配则返回空字符串 +func ExtractISBN978(text string) string { + // 匹配 978 开头的13位数字(可能包含连字符或空格) + // 格式:978-xxx-xxxxx-xxx 或 978xxxxxxxxxx + isbnRegex := regexp.MustCompile(`978[\s-]?\d{1,5}[\s-]?\d{1,7}[\s-]?\d{1,6}[\s-]?\d`) + + matches := isbnRegex.FindAllString(text, -1) + for _, match := range matches { + cleaned := cleanISBN978(match) + if isValidISBN13(cleaned) { + return cleaned + } + } + return "" +} + +// ExtractAllISBN978 从字符串中提取所有 978 开头的 ISBN-13 +func ExtractAllISBN978(text string) []string { + isbnRegex := regexp.MustCompile(`978[\s-]?\d{1,5}[\s-]?\d{1,7}[\s-]?\d{1,6}[\s-]?\d`) + + var results []string + matches := isbnRegex.FindAllString(text, -1) + for _, match := range matches { + cleaned := cleanISBN978(match) + if isValidISBN13(cleaned) && !contains(results, cleaned) { + results = append(results, cleaned) + } + } + return results +} + +// cleanISBN978 清理 ISBN,移除连字符和空格 +func cleanISBN978(isbn string) string { + re := regexp.MustCompile(`[-\s]`) + return re.ReplaceAllString(isbn, "") +} + +// isValidISBN13 验证 ISBN-13 +func isValidISBN13(isbn string) bool { + if len(isbn) != 13 { + return false + } + + var sum int + for i, ch := range isbn { + if ch < '0' || ch > '9' { + return false + } + digit := int(ch - '0') + if i%2 == 0 { + sum += digit + } else { + sum += digit * 3 + } + } + return sum%10 == 0 +} + +// contains 辅助函数 +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +// 更简单的版本:只提取13位数字并检查是否以978开头 +// ExtractISBN978Simple 简单版本,只匹配连续的数字 +func ExtractISBN978Simple(text string) string { + // 匹配13位连续的数字 + re := regexp.MustCompile(`\d{13}`) + matches := re.FindAllString(text, -1) + + for _, match := range matches { + if strings.HasPrefix(match, "978") && isValidISBN13(match) { + return match + } + } + return "" +} + +// ExtractAllISBN978Simple 简单版本,提取所有13位数字中以978开头的 +func ExtractAllISBN978Simple(text string) []string { + re := regexp.MustCompile(`\d{13}`) + matches := re.FindAllString(text, -1) + + var results []string + for _, match := range matches { + if strings.HasPrefix(match, "978") && isValidISBN13(match) && !contains(results, match) { + results = append(results, match) + } + } + return results +} diff --git a/planB/tool/log.go b/planB/tool/log.go new file mode 100644 index 0000000..b1950e3 --- /dev/null +++ b/planB/tool/log.go @@ -0,0 +1,12 @@ +package tool + +import ( + "planA/planB/initialization/golabl" +) + +func SteLog(msg string) string { + platform := GetPlatformName() + taskTypeName := GetTaskType() + log := "[任务id:" + golabl.Task.TaskId + "]" + "[店铺id:" + golabl.Task.Header.ShopId + "]" + "[店铺名称:" + golabl.Task.Header.ShopName + "]" + "[店铺类型:" + platform + "]" + "[任务类型:" + taskTypeName + "]" + return log + msg +} diff --git a/planB/tool/logr.go b/planB/tool/logr.go new file mode 100644 index 0000000..565dfa5 --- /dev/null +++ b/planB/tool/logr.go @@ -0,0 +1,71 @@ +package tool + +import ( + "fmt" + "planA/planB/initialization/golabl" + "planA/planB/modules/logs" + "sync" +) + +// LoggingMiddleware 记录日志 +// 全局计数器,用于控制错误日志打印频率 +var errorLogCounter = 0 +var errorLogMutex sync.Mutex + +func LoggingMiddleware(level string, str string) { + m := golabl.LogDll + initializeLoggerErr := logs.InitializeLogger(m, "logs") + if initializeLoggerErr != nil { + fmt.Println("初始化日志失败:", initializeLoggerErr) + return + } + setLogTaskTypeErr := logs.SetLogTaskType(m, "task") + if setLogTaskTypeErr != nil { + fmt.Println("设置日志任务类型失败:", setLogTaskTypeErr) + return + } + str = SteLog(str) + + // 设置打印间隔,例如每100条打印一次 + printInterval := 100 // 可以修改这个数字来调整打印频率 + + switch { + case level == logs.LOG_LEVEL_ERROR: + // 控制打印频率 + shouldPrint := false + errorLogMutex.Lock() + errorLogCounter++ + if errorLogCounter%printInterval == 0 { + shouldPrint = true + } + errorLogMutex.Unlock() + + if shouldPrint { + fmt.Println(str) + } + + logErrorErr := logs.LogError(m, str) + if logErrorErr != nil { + fmt.Println("记录错误日志失败:", logErrorErr) + return + } + case level == logs.LOG_LEVEL_WARNING: + logWarningErr := logs.LogWarning(m, str) + if logWarningErr != nil { + fmt.Println("记录警告日志失败:", logWarningErr) + return + } + case level == logs.LOG_LEVEL_SUCCESS: + logSuccessErr := logs.LogSuccess(m, str) + if logSuccessErr != nil { + fmt.Println("记录成功日志失败:", logSuccessErr) + return + } + default: + logInfoErr := logs.LogInfo(m, str) + if logInfoErr != nil { + fmt.Println("记录信息日志失败:", logInfoErr) + return + } + } +} diff --git a/planB/tool/minio.go b/planB/tool/minio.go new file mode 100644 index 0000000..80d78fd --- /dev/null +++ b/planB/tool/minio.go @@ -0,0 +1,282 @@ +package tool + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "planA/planB/initialization/golabl" + planBTypeModules "planA/planB/type/modules" + "strings" + "time" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/lifecycle" +) + +// UploadToMinIo 上传到图片空间 +// @param watermarkFromURLExsBase64Arr 水印图片信息 +// @return []string 上传后的图片链接 +func UploadToMinIo(watermarkFromURLExsBase64Arr []planBTypeModules.ImageResult) ([]string, error) { + var imageUrlArr []string + for _, watermarkFromURLExsBase64 := range watermarkFromURLExsBase64Arr { + imageUrl, uploadBase64ImageErr := UploadBase64Image(watermarkFromURLExsBase64.Data, "") + if uploadBase64ImageErr != nil { + return imageUrlArr, nil + } + imageUrlArr = append(imageUrlArr, imageUrl) + } + return imageUrlArr, nil +} + +// UploadBase64Image 传入 base64 图片信息与目录上传到图片空间 +// 参数 base64Data: base64 编码的图片数据(可以是纯 base64 或带 data:image/xxx;base64, 前缀) +// 参数 fileName: 自定义文件名(可选,为空则自动生成) +// 返回: 上传后的完整对象名称(含目录)和错误信息 +func UploadBase64Image(base64Data, fileName string) (string, error) { + // 解析 base64 数据,分离 MIME 类型和纯 base64 内容 + var pureBase64 string + var contentType string + + // 检查是否包含 data URL 前缀 + if strings.Contains(base64Data, ";base64,") { + // 格式: data:image/jpeg;base64,xxxxx + parts := strings.SplitN(base64Data, ";base64,", 2) + if len(parts) != 2 { + return "", fmt.Errorf("无效的 base64 数据格式") + } + // 提取 MIME 类型,去掉 "data:" 前缀 + mimePart := strings.TrimPrefix(parts[0], "data:") + contentType = mimePart + pureBase64 = parts[1] + } else { + // 假设是纯 base64 数据,需要根据内容判断类型 + pureBase64 = base64Data + // 尝试通过 base64 内容的前几个字节判断图片类型 + contentType = detectImageTypeFromBase64(pureBase64) + if contentType == "" { + contentType = "image/jpeg" // 默认类型 + } + } + + // 解码 base64 数据 + imageData, err := base64.StdEncoding.DecodeString(pureBase64) + if err != nil { + return "", fmt.Errorf("base64 解码失败: %v", err) + } + + // 如果未提供文件名,则自动生成 + if fileName == "" { + // 根据 MIME 类型确定扩展名 + ext := getExtensionFromMimeType(contentType) + fileName = generateRandomFileName(ext) + } + + // 将图片数据转换为 io.Reader + reader := bytes.NewReader(imageData) + + // 设置全局生命周期规则(所有文件7天后自动删除) + err = SetLifecyclePolicy(7) + if err != nil { + fmt.Printf("设置生命周期规则失败: %v\n", err) + // 继续执行,不影响上传 + } + + // 生成按日期目录的对象名称 + objectName := generateObjectName(string(imageData)) + + // 上传到 MinIO + url, err := UploadWithExpiry( + reader, + objectName, + contentType, + int64(len(imageData)), + time.Now().Add(7*24*time.Hour), // 7天后过期 + ) + if err != nil { + return "", fmt.Errorf("上传图片失败: %v", err) + } + if golabl.Config.Minio.UseSSL { + url = "https://" + url + } else { + url = "http://" + url + } + return url, nil +} + +// detectImageTypeFromBase64 通过 base64 解码后的前几个字节判断图片类型 +func detectImageTypeFromBase64(base64Str string) string { + // 解码前几个字节用于判断 + decoded, err := base64.StdEncoding.DecodeString(base64Str[:min(32, len(base64Str))]) + if err != nil { + return "" + } + + // 检查文件头 + if len(decoded) >= 4 { + // JPEG: FF D8 + if decoded[0] == 0xFF && decoded[1] == 0xD8 { + return "image/jpeg" + } + // PNG: 89 50 4E 47 + if decoded[0] == 0x89 && decoded[1] == 0x50 && decoded[2] == 0x4E && decoded[3] == 0x47 { + return "image/png" + } + // GIF: 47 49 46 + if decoded[0] == 0x47 && decoded[1] == 0x49 && decoded[2] == 0x46 { + return "image/gif" + } + // WEBP: 52 49 46 46 + if decoded[0] == 0x52 && decoded[1] == 0x49 && decoded[2] == 0x46 && decoded[3] == 0x46 { + return "image/webp" + } + // BMP: 42 4D + if decoded[0] == 0x42 && decoded[1] == 0x4D { + return "image/bmp" + } + } + return "" +} + +// getExtensionFromMimeType 根据 MIME 类型获取文件扩展名 +func getExtensionFromMimeType(mimeType string) string { + switch mimeType { + case "image/jpeg": + return ".jpg" + case "image/png": + return ".png" + case "image/gif": + return ".gif" + case "image/webp": + return ".webp" + case "image/bmp": + return ".bmp" + default: + return ".jpg" + } +} + +// generateRandomFileName 生成随机文件名 +func generateRandomFileName(ext string) string { + // 这里使用简单的随机数生成,实际应用中可以改用 UUID 或时间戳 + // 为了示例简单,使用时间戳 + return fmt.Sprintf("%d%s", getTimestampNano(), ext) +} + +// getTimestampNano 获取纳秒时间戳 +func getTimestampNano() int64 { + return time.Now().UnixNano() +} + +// SetLifecyclePolicy 设置生命周期规则 - 自动删除超过指定天数的文件 +func SetLifecyclePolicy(expiryDays int) error { + ctx := context.Background() + + // 创建生命周期配置 + lifecycleStr := fmt.Sprintf(`{ + "Rules": [ + { + "ID": "auto-delete-rule", + "Status": "Enabled", + "Filter": { + "Prefix": "" + }, + "Expiration": { + "Days": %d + } + } + ] + }`, expiryDays) + + var config lifecycle.Configuration + if err := json.Unmarshal([]byte(lifecycleStr), &config); err != nil { + return fmt.Errorf("解析生命周期配置失败: %v", err) + } + + // 设置生命周期配置 + if err := golabl.MinIo.Client.SetBucketLifecycle(ctx, golabl.Config.Minio.BucketName, &config); err != nil { + return fmt.Errorf("设置生命周期规则失败: %v", err) + } + + return nil +} + +// 生成按日期组织的对象名称(格式:2026-05-21/时间戳.扩展名) +// imgBase64: Base64编码的图片字符串,格式如 "data:image/jpeg;base64,/9j/4AAQ..." 或直接的Base64字符串 +func generateObjectName(imgBase64 string) string { + // 获取当前时间的日期字符串(YYYY-MM-DD格式) + now := time.Now() + dateDir := now.Format("2006-01-02") // 格式:2026-05-21 + + // 生成时间戳和文件名 + timestamp := now.UnixNano() + + // 从Base64字符串中提取扩展名 + ext := getExtFromBase64(imgBase64) + if ext == "" { + ext = ".jpg" + } + + // 构建目录结构: 2026-05-21/时间戳.扩展名 + objectName := fmt.Sprintf("%s/%d%s", dateDir, timestamp, ext) + + return objectName +} + +// 从Base64字符串中获取文件扩展名 +func getExtFromBase64(base64Str string) string { + // 查找 data:image/ 格式的MIME类型 + if strings.Contains(base64Str, "data:image/") { + // 提取MIME类型部分,格式如 "data:image/jpeg;base64," + parts := strings.SplitN(base64Str, ";", 2) + if len(parts) > 0 { + mimePart := parts[0] + // 提取 image/ 后面的格式 + imageType := strings.TrimPrefix(mimePart, "data:image/") + + // 根据MIME类型返回对应的扩展名 + switch imageType { + case "jpeg", "jpg": + return ".jpg" + case "png": + return ".png" + case "gif": + return ".gif" + case "webp": + return ".webp" + case "bmp": + return ".bmp" + default: + return "." + imageType + } + } + } + + // 如果没有MIME信息,默认返回空 + return "" +} + +// UploadWithExpiry 上传文件并设置自定义元数据(用于单独控制每个文件的过期时间) +func UploadWithExpiry(imgData io.Reader, objectName, contentType string, size int64, expiryTime time.Time) (string, error) { + ctx := context.Background() + + // 设置过期时间到元数据 + metadata := map[string]string{ + "expiry-date": expiryTime.Format(time.RFC3339), + "delete-after": fmt.Sprintf("%d", int(time.Until(expiryTime).Hours()/24)), + } + + // 上传文件 + _, err := golabl.MinIo.Client.PutObject(ctx, golabl.Config.Minio.BucketName, objectName, imgData, size, minio.PutObjectOptions{ + ContentType: contentType, + UserMetadata: metadata, + }) + if err != nil { + return "", fmt.Errorf("上传失败: %v", err) + } + + url := fmt.Sprintf("%s/%s/%s", golabl.Config.Minio.Url, golabl.Config.Minio.BucketName, objectName) + return url, nil +} diff --git a/planB/tool/pdd.go b/planB/tool/pdd.go new file mode 100644 index 0000000..172c3e9 --- /dev/null +++ b/planB/tool/pdd.go @@ -0,0 +1,81 @@ +package tool + +import ( + "encoding/json" + "planA/planB/initialization/golabl" + planBType "planA/planB/type" + planBTypePinduoduo "planA/planB/type/pinduoduo" + "planA/tool" + "strconv" +) + +// GetPddGoodsList 获取商品列表 +// @param params 查询参数 +// @return planBTypePinduoduo.GoodsListResponse 商品列表 +// @return error 错误信息 +func GetPddGoodsList(params map[string]string) (planBTypePinduoduo.GoodsListResponse, string, error) { + var goodsListt planBTypePinduoduo.GoodsListResponse + url := golabl.Config.FileUrl.PddGetGoodsUrl + withParams, buildURLWithParamsErr := BuildURLWithParams(url, params) + if buildURLWithParamsErr != nil { + return goodsListt, "", buildURLWithParamsErr + } + _, resStr, httpGetRequestErr := HttpGetRequest(withParams) + if httpGetRequestErr != nil { + return goodsListt, resStr, httpGetRequestErr + } + unmarshalErr := json.Unmarshal([]byte(resStr), &goodsListt) + if unmarshalErr != nil { + return goodsListt, resStr, unmarshalErr + } + return goodsListt, resStr, nil +} + +// WritePddGoodsData 写入商品数据 +// @param goodsListStr 商品列表 +// @return error 错误信息 +func WritePddGoodsData(goodsListStr []planBTypePinduoduo.GoodsItem, page int, pageTotal int64) (planBType.AsyncTaskResponse, string, error) { + var ret planBType.AsyncTaskResponse + marshal, marshalErr := json.Marshal(goodsListStr) + if marshalErr != nil { + return ret, "", marshalErr + } + params := map[string]string{ + "taskId": golabl.Task.TaskId, + "shopId": golabl.Task.Header.ShopId, + "goodsListStr": string(marshal), + "allNum": strconv.FormatInt(pageTotal, 10), + "num": strconv.Itoa(page), + } + retStr, submitFormDataErr := tool.SubmitFormData(golabl.Config.FileUrl.PddAddGoodsUrl, params) + if submitFormDataErr != nil { + return ret, retStr, submitFormDataErr + } + unmarshalErr := json.Unmarshal([]byte(retStr), &ret) + if unmarshalErr != nil { + return ret, retStr, unmarshalErr + } + return ret, retStr, nil +} + +// GetPddGoodsDetail 获取商品详情 +func GetPddGoodsDetail(goodsListStr []planBTypePinduoduo.GoodsItem) ([]planBTypePinduoduo.GoodsItem, string, error) { + var ret []planBTypePinduoduo.GoodsItem + marshal, marshalErr := json.Marshal(goodsListStr) + if marshalErr != nil { + return ret, "", marshalErr + } + params := map[string]string{ + "accessToken": golabl.Task.Header.ShopMsg.Token, + "goodsListGetResponse": string(marshal), + } + retStr, submitFormDataErr := tool.SubmitFormData(golabl.Config.FileUrl.PddGetGoodsDetailUrl, params) + if submitFormDataErr != nil { + return ret, retStr, submitFormDataErr + } + unmarshalErr := json.Unmarshal([]byte(retStr), &ret) + if unmarshalErr != nil { + return ret, retStr, unmarshalErr + } + return ret, retStr, nil +} diff --git a/planB/tool/tool.go b/planB/tool/tool.go new file mode 100644 index 0000000..6e867f3 --- /dev/null +++ b/planB/tool/tool.go @@ -0,0 +1,855 @@ +package tool + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "image" + "image/jpeg" + "image/png" + "io" + "net/http" + "os" + "path" + "path/filepath" + "planA/planB/initialization/golabl" + "planA/planB/service" + planBType "planA/planB/type" + planBTypeModules "planA/planB/type/modules" + planAType "planA/type" + "strconv" + "strings" + "time" + + "github.com/nfnt/resize" +) + +// BuildPrice 价格处理 +// @param priceMods 价格处理列表 +// @param price 价格 +// @return int64 处理后的价格 +func BuildPrice(priceMods []planAType.PriceMod, price int64) int64 { + for _, mod := range priceMods { + if price >= mod.Min && price <= mod.Max { + newPrice := price * (100 + mod.MarkupRate) / 100 + newPrice += mod.MarkupValue + return newPrice + } + } + return 0 // 没有匹配到价格模版,直接返回0 +} + +// ReturnErr 接口返回错误处理 +func ReturnErr(logUuid string, taskMsg planAType.TaskBody, typeStr string, err error) (string, error) { + dataRetBaty, marshalErr := json.Marshal(taskMsg) + if marshalErr != nil { + return string(dataRetBaty), fmt.Errorf("[%s] json.Marshal错误: %v", logUuid, marshalErr) + } + return string(dataRetBaty), fmt.Errorf("[%s] %v错误: %v", logUuid, typeStr, err) +} + +// BuildGoodsName 构建商品名称 +// @param goodsNamePrefix 商品名称前缀 +// @param goodsNameSuffix 商品名称后缀 +// @param titleConsistOf 标题组成 +// @param spaceCharacter 间隔符 1=空格 +// @param bookInfo 图书信息 +// @return string 商品名称 +func BuildGoodsName(goodsNamePrefix string, goodsNameSuffix string, titleConsistOf string, spaceCharacter string, bookInfo planAType.BookInfo) string { + // 解析标题组成 + if titleConsistOf == "" { + titleConsistOf = "1:true" // 默认使用书名 + } + + // 解析标题组成 + titleOfArr := strings.Split(titleConsistOf, ",") + + // 间隔符 + separator := "" + if spaceCharacter == "1" { + separator = " " + } + + // 构建标题 + title := goodsNamePrefix + separator + + // 遍历标题组成 + for _, item := range titleOfArr { + // 解析标题组成 + parts := strings.Split(item, ":") + // 判断是否需要添加标题 + if len(parts) == 2 && parts[1] == "true" { + switch parts[0] { + case "0": // ISBN + title += separator + bookInfo.Isbn + case "1": // 书名 + title += separator + bookInfo.BookName + case "2": // 作者 + title += separator + bookInfo.Author + case "3": // 出版社 + title += separator + bookInfo.Publishing + case "4": // 出版时间 + title += separator + bookInfo.PublicationDate + case "5": // 装帧 + title += separator + bookInfo.Binding + case "6": // 开本 + title += separator + strconv.FormatInt(bookInfo.Format, 10) + } + } + } + + // 添加后缀 + title += separator + goodsNameSuffix + + // 如果标题超过60个字符,截取前60个字符 + if StringLength(title) > 60 { + title = SubstringByWidth(title, 60) + } + //去掉首尾双引号 + title = strings.Trim(title, "\"") + return title +} + +// BuildCarouselGallery 构建轮播图 +// @param carouseLastImgUrlArray 最后一张图 +// @param oldCarouselUrlArray 旧轮播图 +// @param carouselUrlArray 轮播图组 +// @param watermarkPosition 水印位置 0 全部 1第一张 +// @return []string 轮播图组 +func BuildCarouselGallery(carouseLastImgUrlArray []string, oldCarouselUrlArray []string, carouselUrlArray []string, watermarkPosition string) []string { + // 查看轮播图组长度 + if len(carouselUrlArray)+len(carouseLastImgUrlArray) < 10 { + length := 10 - (len(carouselUrlArray) + len(carouseLastImgUrlArray)) + // 向轮播图组中添加图片 添加最后一张图片 + if len(carouselUrlArray) > 0 { + for i := 0; i < length; i++ { + if carouselUrlArray[len(carouselUrlArray)-1] != "" { + if watermarkPosition == "1" { + // 使用不打水印的图片补充 + carouselUrlArray = append(carouselUrlArray, oldCarouselUrlArray[len(oldCarouselUrlArray)-1]) + } else { + // 使用打水印的图片补充 + carouselUrlArray = append(carouselUrlArray, carouselUrlArray[len(carouselUrlArray)-1]) + } + } + } + } + } + // 合并数组 + carouselUrlArray = append(carouselUrlArray, carouseLastImgUrlArray...) + + return carouselUrlArray +} + +// BuildCarouselGalleryOld 构建轮播图 +// @param carouseLastImgUrlArray 最后一张图 +// @param carouselUrlArray 轮播图组 +// @return []string 轮播图组 +func BuildCarouselGalleryOld(carouseLastImgUrlArray []string, carouselUrlArray []string) []string { + // 查看轮播图组长度 + if len(carouselUrlArray)+len(carouseLastImgUrlArray) < 10 { + length := 10 - (len(carouselUrlArray) + len(carouseLastImgUrlArray)) + // 向轮播图组中添加图片 添加最后一张图片 + if len(carouselUrlArray) > 0 { + for i := 0; i < length; i++ { + if carouselUrlArray[len(carouselUrlArray)-1] != "" { + carouselUrlArray = append(carouselUrlArray, carouselUrlArray[len(carouselUrlArray)-1]) + } + } + } + } + // 合并数组 + carouselUrlArray = append(carouselUrlArray, carouseLastImgUrlArray...) + + return carouselUrlArray +} + +// BuildDetailGallery 构建详情图 +// @param goodsDetailFirstImgUrlArray 商详头图 +// @param goodsDetailLastImgUrlArray 商详尾图 +// @param detailUrlObject 商详图片 +// @param mainImage 主图 +// @return []string 详情图组 +func BuildDetailGallery(goodsDetailFirstImgUrlArray []string, goodsDetailLastImgUrlArray []string, detailUrlObject planAType.DetailImageObject, mainImage string) []string { + // 合并数组 简介图+目录图 + imgArr := append(detailUrlObject.IntroductionUrl, detailUrlObject.CatalogueUrl...) + // 合并数组 简介图+目录图+实拍图 + //imgArr = append(imgArr, detailUrlObject.LiveShootingUrl...) + // 合并数组 简介图+目录图+实拍图+主图 + imgArr = append(imgArr, mainImage) + // 合并数组 简介图+目录图+实拍图+主图+其他图 + imgArr = append(imgArr, detailUrlObject.OtherUrl...) + // 合并数组 商详头图+简介图+目录图+实拍图+主图+其他图 + imgArr = append(goodsDetailFirstImgUrlArray, imgArr...) + // 合并数组 商详头图+简介图+目录图+实拍图+主图+其他图+商详尾图 + imgArr = append(imgArr, goodsDetailLastImgUrlArray...) + return imgArr +} + +// BuildGoodsPrice 构建商品价格 +// @param bookInfoPrice 图书价格 +// @return int64 商品价格 +func BuildGoodsPrice(price int64) int64 { + return price * 4 +} + +// ReturnSuccess 添加商品返回成功处理 +func ReturnSuccess(taskMsg planAType.TaskBody) (string, error) { + dataRetBaty, marshalErr := json.Marshal(taskMsg) + if marshalErr != nil { + return string(dataRetBaty), fmt.Errorf("json.Marshal错误: %w", marshalErr) + } + return string(dataRetBaty), nil +} + +// StringLength 计算字符串显示长度 +// @param s 字符串 +// @return int 字符串显示长度 +func StringLength(s string) int { + length := 0 + for _, r := range s { + if r > 255 { // 非ASCII字符(如中文) + length += 2 + } else { // ASCII字符(如英文、数字) + length += 1 + } + } + return length +} + +// SubstringByWidth 按显示宽度截取字符串 +func SubstringByWidth(s string, maxWidth int) string { + width := 0 + for i, r := range s { + if r > 255 { + width += 2 + } else { + width += 1 + } + + if width > maxWidth { + return s[:i] // 返回截取的部分 + } + } + return s // 如果整个字符串都不超过maxWidth,返回原字符串 +} + +// FilterWord 违规词处理 +// @param taskMsg 任务信息 +func FilterWord(taskMsg *planAType.TaskBody) error { + substitution, httpBannedWordSubstitutionErr := HttpFilterWord(taskMsg.BookInfo.Isbn, taskMsg.BookInfo.BookName, taskMsg.BookInfo.Author, taskMsg.BookInfo.Publishing) + if httpBannedWordSubstitutionErr != nil { + return fmt.Errorf("HttpFilterWord 违禁词处理失败-原因来自:%v", httpBannedWordSubstitutionErr) + } + if golabl.Config.Server.ReplaceMark == "0" && len(substitution.Data) > 0 { + errMsg := "违规词命中 " + for _, v := range substitution.Data { + errMsg = errMsg + " " + v.AddTxt + "(" + v.MatchType + ") " + } + return fmt.Errorf(errMsg) + } + if golabl.Config.Server.ReplaceMark == "1" && len(substitution.Data) > 0 { + //替换违禁词 + taskMsg.BookInfo.BookName = substitution.BookName + taskMsg.BookInfo.Author = substitution.Author + taskMsg.BookInfo.Publishing = substitution.Publisher + taskMsg.BookInfo.Isbn = substitution.Isbn + } + + return nil +} + +// AddWatermarkFromURLExs 打水印 +// @param imgUrl 轮播图组 +// @param watermarkImgUrl 水印图片 +// @param watermarkPosition 水印位置 0 全部 1第一张 +// @return []string 轮播图组 +// @return error 错误信息 +func AddWatermarkFromURLExs(imgUrl []string, watermarkImgUrl string, watermarkPosition string) ([]planBTypeModules.ImageResult, error) { + var watermarkFromURLExsBase64Arr []planBTypeModules.ImageResult + // 循环轮播图组给图片打水印 + for i := 0; i < len(imgUrl); i++ { + var newImgJson string + var addWatermarkFromURLExsErr error + + // 给图片打水印,带重试机制,最大重试次数为3 + maxRetries := 3 + for retryCount := 0; retryCount <= maxRetries; retryCount++ { + + newImgJson, addWatermarkFromURLExsErr = golabl.ImageDll.AddWatermarkFromURLExs(imgUrl[i], watermarkImgUrl) + + // 判断是否包含超时错误 + if addWatermarkFromURLExsErr != nil && strings.Contains(addWatermarkFromURLExsErr.Error(), "dialing to the given TCP address timed out") { + if retryCount < maxRetries { + // 重试前等待一段时间(可选) + time.Sleep(time.Duration(retryCount+1) * time.Second) + continue + } + } + // 如果没有错误或者不是超时错误,跳出重试循环 + break + } + + if addWatermarkFromURLExsErr != nil { + return watermarkFromURLExsBase64Arr, fmt.Errorf("给图片打水印错误 %w", addWatermarkFromURLExsErr) + } + + // 将 newImg 转为结构体 + var newImg planBTypeModules.ImageResult + unmarshalErr := json.Unmarshal([]byte(newImgJson), &newImg) + if unmarshalErr != nil { + return nil, fmt.Errorf("解析失败 %w 原始数据 %v", unmarshalErr, newImgJson) + } + watermarkFromURLExsBase64Arr = append(watermarkFromURLExsBase64Arr, newImg) + + if watermarkPosition == "1" { + break + } + } + return watermarkFromURLExsBase64Arr, nil +} + +// UploadImageToPdd 将图片上传到拼多多 +// @param watermarkFromURLExsBase64Arr 待上传的base64图片列表 +// @return []string 图片列表 +// @return error 错误信息 +func UploadImageToPdd(watermarkFromURLExsBase64Arr []planBTypeModules.ImageResult) ([]string, error) { + var imageUrlArr []string + for _, watermarkFromURLExsBase64 := range watermarkFromURLExsBase64Arr { + var pddImg planBTypeModules.GoodsImageUploadResponse + imageUrl, pddGoodsImageUploadErr := golabl.PddDll.PddGoodsImageUpload(golabl.Config.PddConfig.ClientId, golabl.Config.PddConfig.ClientSecret, golabl.Task.Header.ShopMsg.Token, watermarkFromURLExsBase64.Data) + if pddGoodsImageUploadErr != nil { + return imageUrlArr, pddGoodsImageUploadErr + } + // 解析 JSON字符串 + unmarshalErr := json.Unmarshal([]byte(imageUrl), &pddImg) + if unmarshalErr != nil { + return imageUrlArr, fmt.Errorf("解析拼多多 PddGoodsImageUpload 错误: %v [拼多多数据:%v]", unmarshalErr, imageUrl) + } + imageUrlArr = append(imageUrlArr, pddImg.GoodsImageUploadResponse.ImageURL) + } + return imageUrlArr, nil +} + +// GetPlatformName 获取平台名称 +func GetPlatformName() string { + title := "" + switch golabl.Task.Header.ShopType { + //case 2: + // return kongFuZi.NewKongfuzi(), nil + case "1": + title = title + "拼多多" + case "5": + title = title + "闲鱼" + default: + title = title + "其他平台 " + golabl.Task.Header.ShopType + } + return title +} + +// GetTaskType 获取店铺类型 +func GetTaskType() string { + switch golabl.Task.Header.TaskType { + case 1: //核价发布 + return "核价发布" + case 2: //表格发布 + return "表格发布" + case 3: //获取商品 + return "获取商品" + default: + return "错误!" + } +} + +// GetWatermarkImg 获取水印图片 +// @return string 水印图片 base64 +func GetWatermarkImg() (string, error) { + // 1. 获取日期 + t := time.Unix(golabl.Task.Header.TaskCreateAt, 0) + yearMonthDay := t.Format("2006-01-02") + + // 2. 获取文件后缀(去除前面的.) + extWithDot := path.Ext(golabl.Task.Header.ShopMsg.WatermarkImgUrl) + // 拼接本地存储路径 + imgUrl := "img/watermark/" + yearMonthDay + "/" + golabl.Task.TaskId + extWithDot + + // 3. 判断本地文件是否存在 + if _, err := os.Stat(imgUrl); err == nil { + // 文件存在 → 直接读取并转base64返回 + imgBytes, err := os.ReadFile(imgUrl) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(imgBytes), nil + } + + // 4. 文件不存在 → 创建目录、下载图片、保存本地、转base64 + // 创建多级目录 + dirPath := filepath.Dir(imgUrl) + if err := os.MkdirAll(dirPath, 0755); err != nil { + return "", err + } + + // 下载远程图片 + resp, err := http.Get(golabl.Task.Header.ShopMsg.WatermarkImgUrl) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // 判断下载响应是否成功 + if resp.StatusCode != http.StatusOK { + fmt.Printf("下载水印图片响应异常: %s\n", resp.Status) + return "", fmt.Errorf("下载水印图片失败: %s", resp.Status) + } + + // 读取图片二进制数据 + imgBytes, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("读取下载的图片数据失败: %v\n", err) + return "", err + } + + // 保存图片到本地 + if err := os.WriteFile(imgUrl, imgBytes, 0644); err != nil { + fmt.Printf("保存水印图片到本地失败: %v\n", err) + return "", err + } + + // 5. 转base64返回 + return base64.StdEncoding.EncodeToString(imgBytes), nil +} + +// GetSkuWatermarkImg 获取sku水印图片 +// @return string 水印图片 base64 +func GetSkuWatermarkImg() (string, error) { + // 1. 获取日期 + t := time.Unix(golabl.Task.Header.TaskCreateAt, 0) + yearMonthDay := t.Format("2006-01-02") + + // 2. 获取文件后缀(去除前面的.) + extWithDot := path.Ext(golabl.Task.Header.ShopMsg.SkuWatermarkImgUrl) + // 拼接本地存储路径 + imgUrl := "img/skuwatermark/" + yearMonthDay + "/" + golabl.Task.TaskId + extWithDot + + // 3. 判断本地文件是否存在 + if _, err := os.Stat(imgUrl); err == nil { + // 文件存在 → 直接读取并转base64返回 + imgBytes, err := os.ReadFile(imgUrl) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(imgBytes), nil + } + + // 4. 文件不存在 → 创建目录、下载图片、保存本地、转base64 + // 创建多级目录 + dirPath := filepath.Dir(imgUrl) + if err := os.MkdirAll(dirPath, 0755); err != nil { + return "", err + } + + // 下载远程图片 + resp, err := http.Get(golabl.Task.Header.ShopMsg.SkuWatermarkImgUrl) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // 判断下载响应是否成功 + if resp.StatusCode != http.StatusOK { + fmt.Printf("下载水印图片响应异常: %s\n", resp.Status) + return "", fmt.Errorf("下载水印图片失败: %s", resp.Status) + } + + // 读取图片二进制数据 + imgBytes, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("读取下载的图片数据失败: %v\n", err) + return "", err + } + + // 保存图片到本地 + if err := os.WriteFile(imgUrl, imgBytes, 0644); err != nil { + fmt.Printf("保存水印图片到本地失败: %v\n", err) + return "", err + } + + // 5. 转base64返回 + return base64.StdEncoding.EncodeToString(imgBytes), nil +} + +// UpdateTaskHeader 更新头部信息 +// @return error 错误信息 +func UpdateTaskHeader() error { + //通过 footer 来更新 header 的计数 + golabl.Task.Header.TaskCountWait = golabl.Task.Footer.TaskCountWait.Load() + golabl.Task.Header.TaskCountOver = golabl.Task.Footer.TaskCountOver.Load() + golabl.Task.Header.TaskCountSuccess = golabl.Task.Footer.TaskCountSuccess.Load() + golabl.Task.Header.TaskCountError = golabl.Task.Footer.TaskCountError.Load() + golabl.Task.Header.LastIndex = golabl.Logic.LastIndex + return service.UpdateTaskHeaderCount() +} + +// UpdateTaskProgress 更新拉取商品进度 +// @param con int64 更新进度数 +// @return error 错误信息 +func UpdateTaskProgress(con int64) error { + // 更新 进度 + if updateTaskFooterErr := service.UpdateTaskFooter(1, con); updateTaskFooterErr != nil { + return updateTaskFooterErr + } + // 重新获取 footer信息 + if getTaskFooterErr := service.GetTaskFooter(); getTaskFooterErr != nil { + return getTaskFooterErr + } + if updateTaskHeaderCountErr := UpdateTaskHeader(); updateTaskHeaderCountErr != nil { + return updateTaskHeaderCountErr + } + return nil +} + +// FenToYuan 将金额从分转换为元 +// 参数:fen - 金额(分),int64类型 +// 返回值:金额(元),string类型 +func FenToYuan(fen int64) string { + yuan := float64(fen) / 100.0 + return fmt.Sprintf("%.2f", yuan) +} + +// FenToYuanFloat64 将金额从分转换为元 +// 参数:fen - 金额(分),int64类型 +// 返回值:金额(元),float64类型 +func FenToYuanFloat64(fen int64) float64 { + return float64(fen) / 100.0 +} + +// IsShopIDExists 判断店铺ID是否存在于shopId.txt文件中 +func IsShopIDExists(targetShopID string) (bool, error) { + // 打开文件 + file, err := os.Open("shopId.txt") + if err != nil { + return false, fmt.Errorf("无法打开文件: %w", err) + } + defer file.Close() + + // 逐行扫描 + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) // 去除空格和换行符 + // 忽略空行 + if line == "" { + continue + } + if line == targetShopID { + return true, nil + } + } + + if err := scanner.Err(); err != nil { + return false, fmt.Errorf("读取文件出错: %w", err) + } + + return false, nil +} + +// AppendTextToFile 在文件中追加文本 +// @param filePath 文件路径 +// @param text 要追加的文本 +// @return error 错误信息 +func AppendTextToFile(filePath string, text string) error { + // 获取文件所在的目录 + dir := filepath.Dir(filePath) + + // 创建目录(如果不存在) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("创建目录失败: %v", err) + } + + // 以追加模式打开文件,如果文件不存在则创建 + file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("打开文件失败: %v", err) + } + defer file.Close() + + // 写入文本并添加换行符 + if _, err := file.WriteString(text + "\n"); err != nil { + return fmt.Errorf("写入文件失败: %v", err) + } + + return nil +} + +// UploadImageToKfz 将图片上传到孔夫子 +// @param watermarkFromURLExsBase64Arr 待上传的base64图片列表 +// @return []string 图片列表 +// @return error 错误信息 +func UploadImageToKfz(watermarkFromURLExsBase64Arr []planBTypeModules.ImageResult) ([]string, error) { + + var imageUrlArr []string + for _, watermarkFromURLExsBase64 := range watermarkFromURLExsBase64Arr { + //将图片保存到本地 + imgTempUrl, saveBase64ImageByDateErr := SaveBase64ImageByDate(watermarkFromURLExsBase64.Data, golabl.Config.FileUrl.KfzImgTempUrl) + if saveBase64ImageByDateErr != nil { + return nil, saveBase64ImageByDateErr + } + //将图片上传到孔夫子 + _, kfzGoodsImageUploadErr := golabl.KfzDll.KfzGoodsImageUpload(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, golabl.Task.Header.ShopMsg.Token, imgTempUrl) + if kfzGoodsImageUploadErr != nil { + return nil, kfzGoodsImageUploadErr + } + } + return imageUrlArr, nil +} + +// SaveBase64ImageByDate 保存base64图片或下载URL图片到按年月日组织的文件夹中 +// 参数1: input - base64编码的图片字符串 或 网络图片地址 +// 参数2: basePath - 基础路径(如 D:\\file\\kfzImg) +// 返回: 保存的完整图片地址和错误信息 +func SaveBase64ImageByDate(input string, url string) (string, error) { + // 去除首尾空格 + input = strings.TrimSpace(input) + + var imageData []byte + var err error + + // 判断是否为URL(以http://或https://开头) + if strings.HasPrefix(strings.ToLower(input), "http://") || + strings.HasPrefix(strings.ToLower(input), "https://") { + // 处理URL情况:下载图片 + imageData, err = downloadImage(input) + if err != nil { + return "", fmt.Errorf("下载图片失败: %v", err) + } + } else { + // 处理base64情况 + imageData, err = decodeBase64Image(input) + if err != nil { + return "", fmt.Errorf("base64解码失败: %v", err) + } + } + + // 生成按年月日的文件夹路径 + now := time.Now() + dateFolder := now.Format("2006-01-02") // 格式: 2026-05-11 + saveDir := filepath.Join(url, dateFolder) + + // 创建目录(如果不存在) + if err := os.MkdirAll(saveDir, 0755); err != nil { + return "", fmt.Errorf("创建目录失败: %v", err) + } + + // 生成唯一文件名(使用时间戳+随机数避免重名) + timestamp := now.UnixNano() + filename := fmt.Sprintf("%d.png", timestamp) + savePath := filepath.Join(saveDir, filename) + + // 写入文件 + err = os.WriteFile(savePath, imageData, 0644) + if err != nil { + return "", fmt.Errorf("保存图片失败: %v", err) + } + + return savePath, nil +} + +// decodeBase64Image 解码base64图片 +func decodeBase64Image(base64Str string) ([]byte, error) { + // 去除可能存在的base64头部信息 + if idx := strings.Index(base64Str, ","); idx != -1 { + base64Str = base64Str[idx+1:] + } + + // 解码base64字符串 + imageData, err := base64.StdEncoding.DecodeString(base64Str) + if err != nil { + return nil, err + } + + return imageData, nil +} + +// downloadImage 下载网络图片 +func downloadImage(url string) ([]byte, error) { + // 创建HTTP客户端(设置超时时间) + client := &http.Client{ + Timeout: 30 * time.Second, + } + + // 发送GET请求 + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("HTTP请求失败: %v", err) + } + defer resp.Body.Close() + + // 检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP状态码异常: %d", resp.StatusCode) + } + + // 检查Content-Type是否为图片 + contentType := resp.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "image/") { + return nil, fmt.Errorf("非图片资源: Content-Type=%s", contentType) + } + + // 读取图片数据 + imageData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取图片数据失败: %v", err) + } + + return imageData, nil +} + +// ProcessImage 处理图片(支持本地文件路径和HTTP URL) +func ProcessImage(imageURL string) (string, string, error) { + // 判断是本地文件还是HTTP URL + if strings.HasPrefix(imageURL, "http://") || strings.HasPrefix(imageURL, "https://") { + // HTTP URL:从网络获取图片 + img, format, err := GetImageFromURL(imageURL) + if err != nil { + return "", "", err + } + return processImageAndConvert(img, format) + } else { + // 本地文件路径:从本地读取图片 + img, format, err := GetImageFromLocalFile(imageURL) + if err != nil { + return "", "", err + } + return processImageAndConvert(img, format) + } +} + +// processImageAndConvert 处理图片并转换为base64 +func processImageAndConvert(img image.Image, format string) (string, string, error) { + bounds := img.Bounds() + fmt.Printf("原始尺寸: %dx%d\n", bounds.Dx(), bounds.Dy()) + + var processedImg image.Image + if bounds.Dx() == 800 && bounds.Dy() == 800 { + processedImg = img + } else { + processedImg = ResizeImageHighQuality(img, 800, 800) + } + + // 转换为base64 + base64Str, err := ImageToBase64(processedImg, format) + if err != nil { + return "", "", fmt.Errorf("转换为base64失败: %v", err) + } + + return base64Str, format, nil +} + +// GetImageFromLocalFile 从本地文件获取图片 +func GetImageFromLocalFile(filePath string) (image.Image, string, error) { + // 打开本地文件 + file, err := os.Open(filePath) + if err != nil { + return nil, "", fmt.Errorf("打开本地文件失败: %v", err) + } + defer file.Close() + + // 读取文件数据 + data, err := io.ReadAll(file) + if err != nil { + return nil, "", fmt.Errorf("读取本地文件失败: %v", err) + } + + // 尝试解码PNG + img, err := png.Decode(bytes.NewReader(data)) + if err == nil { + return img, "png", nil + } + + // 尝试解码JPEG + img, err = jpeg.Decode(bytes.NewReader(data)) + if err == nil { + return img, "jpg", nil + } + + return nil, "", fmt.Errorf("不支持的图片格式") +} + +// ResizeImageHighQuality 高质量缩放图片 +func ResizeImageHighQuality(img image.Image, width, height uint) image.Image { + return resize.Resize(width, height, img, resize.Lanczos3) +} + +// GetImageFromURL 从URL获取图片 +func GetImageFromURL(url string) (image.Image, string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, "", err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, "", err + } + + // 尝试解码PNG + img, err := png.Decode(strings.NewReader(string(data))) + if err == nil { + return img, "png", nil + } + + // 尝试解码JPEG + img, err = jpeg.Decode(strings.NewReader(string(data))) + if err == nil { + return img, "jpg", nil + } + + return nil, "", fmt.Errorf("不支持的图片格式") +} + +// ImageToBase64 将图片转换为base64 +func ImageToBase64(img image.Image, format string) (string, error) { + buf := new(strings.Builder) + + if format == "png" { + err := png.Encode(buf, img) + if err != nil { + return "", err + } + } else { + err := jpeg.Encode(buf, img, &jpeg.Options{Quality: 95}) + if err != nil { + return "", err + } + } + + base64Str := base64.StdEncoding.EncodeToString([]byte(buf.String())) + return fmt.Sprintf("data:image/%s;base64,%s", format, base64Str), nil +} + +// GetGoodsByShopIdAndIsbn 根据店铺id与isbn获取商品 +func GetGoodsByShopIdAndIsbn(shopId, isbn string) (planBType.GetShopGoodsByShopIdAndIsbn, error) { + + var getShopGoodsByShopIdAndIsbn planBType.GetShopGoodsByShopIdAndIsbn + + params := map[string]string{ + "shopId": shopId, + "isbn": isbn, + } + withParams, buildURLWithParamsErr := BuildURLWithParams(golabl.Config.FileUrl.GetPddGoodsShopIdIsbnUrl, params) + if buildURLWithParamsErr != nil { + return getShopGoodsByShopIdAndIsbn, buildURLWithParamsErr + } + _, resStr, httpGetRequestErr := HttpGetRequest(withParams) + if httpGetRequestErr != nil { + return getShopGoodsByShopIdAndIsbn, httpGetRequestErr + } + unmarshalErr := json.Unmarshal([]byte(resStr), &getShopGoodsByShopIdAndIsbn) + if unmarshalErr != nil { + return getShopGoodsByShopIdAndIsbn, unmarshalErr + } + return getShopGoodsByShopIdAndIsbn, nil +} diff --git a/planB/tool/uuid.go b/planB/tool/uuid.go new file mode 100644 index 0000000..712324e --- /dev/null +++ b/planB/tool/uuid.go @@ -0,0 +1,19 @@ +package tool + +import ( + "fmt" + + "github.com/google/uuid" +) + +// GenerateUUID 生成一个版本4(随机)的UUID字符串 +// 返回值:uuid字符串,错误信息(如果生成失败) +func GenerateUUID() (string, error) { + // NewUUID 生成版本4的随机UUID(最常用的类型) + uuidObj, err := uuid.NewRandom() + if err != nil { + return "", fmt.Errorf("生成UUID失败: %v", err) + } + // 将UUID对象转为字符串(标准格式:8-4-4-4-12) + return uuidObj.String(), nil +} diff --git a/planB/tool/xianYu.go b/planB/tool/xianYu.go new file mode 100644 index 0000000..05b1676 --- /dev/null +++ b/planB/tool/xianYu.go @@ -0,0 +1 @@ +package tool diff --git a/planB/type/delTaskDetails.go b/planB/type/delTaskDetails.go new file mode 100644 index 0000000..ebc4851 --- /dev/null +++ b/planB/type/delTaskDetails.go @@ -0,0 +1,20 @@ +package _type + +import "time" + +// DelTaskDetail 删除任务详情表结构 +type DelTaskDetail struct { + ID int `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + DelTaskID *int64 `gorm:"column:del_task_id;default:0" json:"del_task_id"` // 删除任务 id + TaskID *string `gorm:"column:task_id;default:'';size:255" json:"task_id"` // 任务 id + Isbn *string `gorm:"column:isbn;default:'';size:255" json:"isbn"` // isbn + BookName *string `gorm:"column:book_name;default:'';size:255" json:"book_name"` // 图书名称 + Token *string `gorm:"column:token;default:'';size:255" json:"token"` // token + GoodsID *int64 `gorm:"column:goods_id" json:"goods_id"` // 商品 id + JSON *string `gorm:"column:json;type:text" json:"json"` // 原始字符串 + Status *int64 `gorm:"column:status;default:0" json:"status"` // 状态: 1=正常 2=错误 + Err *string `gorm:"column:err;type:text" json:"err"` // 错误信息 + DeleteAt *time.Time `gorm:"column:delete_at" json:"delete_at"` // 删除商品时间 + DeleteDate *string `gorm:"column:delete_date" json:"delete_date"` // 删除商品日期 + CreateAt *time.Time `gorm:"column:create_at" json:"create_at"` // 创建时间 +} diff --git a/planB/type/filterWord.go b/planB/type/filterWord.go new file mode 100644 index 0000000..3c2e687 --- /dev/null +++ b/planB/type/filterWord.go @@ -0,0 +1,23 @@ +package _type + +// HttpFilterWordRes 违规词查询响应结构体 +type HttpFilterWordRes struct { + Msg string `json:"msg"` // 查询成功 + Code string `json:"code"` // 状态码 200 + Data []MatchRule `json:"data"` // 匹配规则列表 + Success bool `json:"success"` // 是否成功 + Author string `json:"author"` // 作者 + Isbn string `json:"isbn"` // ISBN + Publisher string `json:"publisher"` // 出版社 + BookName string `json:"bookName"` // 书名(可能包含***) +} + +// MatchRule 匹配规则结构体 +type MatchRule struct { + //CreateBy string `json:"createBy"` // 创建人ID + MatchType string `json:"matchType"` // 匹配类型:ISBN匹配/书名匹配 + AddTxt string `json:"addTxt"` // 匹配文本 + ID int64 `json:"id"` // 规则ID + Sort string `json:"sort"` // 排序信息 "0,3" + LimitationType string `json:"limitationType"` // 限制类型 "0"/"1"/"6" +} diff --git a/planB/type/golab.go b/planB/type/golab.go new file mode 100644 index 0000000..030643d --- /dev/null +++ b/planB/type/golab.go @@ -0,0 +1,44 @@ +package _type + +import ( + "sync" + + "github.com/go-redis/redis/v8" + "github.com/panjf2000/ants/v2" + + planAType "planA/type" +) + +// Redis 存储结构 +type Redis struct { + RedisDbA *redis.Client // 任务数据库 + RedisDbB *redis.Client // 出版社数据库 + RedisDbC *redis.Client // 地区数据库 + RedisDbD *redis.Client // 没有书籍的 isbn数据库 + RedisDbE *redis.Client // 店铺库 + RedisDbF *redis.Client // 拼多多操作回调数据库 +} + +// Task 任务结构 +type Task struct { + TaskId string // 任务ID + Header *planAType.TaskHeader // 任务头 + Footer *planAType.TaskFooter // 任务尾 + BodyWait *planAType.TaskBody // 任务等待 + BodyOver *planAType.TaskBody // 任务完成 + BodyBackup *planAType.TaskBody // 任务备份 +} + +// Pool 线程池结构 +type Pool struct { + Pool *ants.Pool // 线程池 + Wg *sync.WaitGroup // 等待组 +} + +// Logic 逻辑控制结构 +type Logic struct { + TaskIndex int64 //已读取的 body_wait索引 + RedisNilCon int64 //连续读出 redisNil 的次数 + ReplaceMarkCon int64 //连续违规词出现的次数 + LastIndex int64 //记录程序集错误 +} diff --git a/planB/type/http.go b/planB/type/http.go new file mode 100644 index 0000000..6de62e1 --- /dev/null +++ b/planB/type/http.go @@ -0,0 +1,8 @@ +package _type + +// HttpRes 通用响应结构体 +type HttpRes struct { + Code string `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` // omitempty 表示如果为空则忽略 +} diff --git a/planB/type/kfz/kfz.go b/planB/type/kfz/kfz.go new file mode 100644 index 0000000..1e4918c --- /dev/null +++ b/planB/type/kfz/kfz.go @@ -0,0 +1,401 @@ +package kfz + +// GoodsAdd 商品添加结构体 +type GoodsAdd struct { + Tpl string `json:"tpl"` // 模板编号,取值范围:1~17,必须 + CatId string `json:"catId"` // 分类编号,必须 + MyCatId string `json:"myCatId,omitempty"` // 本店分类,可选 + ItemName string `json:"itemName"` // 商品名称,长度限制200字符,必须 + ImportantDesc string `json:"importantDesc,omitempty"` // 推荐语,长度限制200字符,可选 + Price string `json:"price"` // 售价,0.01~99999999.99,必须 + Number string `json:"number"` // 库存,1~9999,必须 + Quality string `json:"quality"` // 品相,可以是编号(int)或文字(string),取值:10,20,30,40,50,60,65,70,75,80,85,90,95,100,必须 + QualityDesc string `json:"qualityDesc,omitempty"` // 品相描述,长度限制400字符,可选 + ItemSn string `json:"itemSn,omitempty"` // 货号,长度限制20字符,可选 + ImgUrl string `json:"imgUrl"` // 商品主图,必须 + Images string `json:"images,omitempty"` // 多个商品图片路径,用英文分号隔开,最多30张,可选 + ItemDesc string `json:"itemDesc,omitempty"` // 商品描述,长度限制10000字符,可选 + BearShipping string `json:"bearShipping"` // 运费设置:seller(卖家包邮),buyer(买家承担运费),必须 + MouldId string `json:"mouldId,omitempty"` // 运费模板编号,bearShipping=buyer时必填 + Weight string `json:"weight,omitempty"` // 商品重量(千克),选择按重量模板时必填,0.01~9999.99 + WeightPiece string `json:"weightPiece,omitempty"` // 商品标准本数,选择按标准本模板时必填,0.01~9999.99 +} + +// GoodsAdd17 商品添加结构体(分类17专用) +type GoodsAdd17 struct { + GoodsAdd + Isbn string `json:"isbn"` // ISBN号,必须 + Author string `json:"author"` // 作者,长度限制120字符,必须 + Press string `json:"press"` // 出版社,长度限制120字符,必须 + PubDate string `json:"pubDate"` // 出版日期,格式:yyyy-mm,必须 + Edition string `json:"edition,omitempty"` // 版次,取值范围:1~9999,可选 + PrintingTime string `json:"printingTime,omitempty"` // 印刷时间,格式:yyyy-mm,填写printTimes时必填,不能早于出版时间,可选 + PrintTimes string `json:"printTimes,omitempty"` // 印次,取值范围:1~9999,可选 + PrintingNum string `json:"printingNum,omitempty"` // 印数,单位:千册,取值范围:0.001~99999.999,可选 + Binding interface{} `json:"binding"` // 装帧,可以是编号(int)或文字(string),必须 + PageSize string `json:"pageSize,omitempty"` // 开本,可自己填写,可选 + Paper interface{} `json:"paper,omitempty"` // 纸张,可以是编号(int)或文字(string),可选 + PageNum string `json:"pageNum,omitempty"` // 页数,取值范围:1~99999,可选 + WordNum string `json:"wordNum,omitempty"` // 字数,单位:千字,取值范围:0~99999.999,可选 + OriPrice string `json:"oriPrice,omitempty"` // 图书定价,取值范围:0.01~99999999.99,可选 + ForeignName string `json:"foreignName,omitempty"` // 原版书名,长度限制200字符,可选 + UnifiedIsbn string `json:"unifiedIsbn,omitempty"` // 统一书号,长度限制20字符,可选 + PublishedIn string `json:"publishedIn,omitempty"` // 出版地,长度限制100字符,可选 + Language string `json:"language,omitempty"` // 语种,长度限制30字符,可选 + OriginalLanguage string `json:"originalLanguage,omitempty"` // 原版语种,长度限制30字符,可选 + Series string `json:"series,omitempty"` // 丛书系列,长度限制60字符,可选 + ContentIntro string `json:"contentIntro,omitempty"` // 内容介绍,长度限制10000字符,可选 + AuthorIntro string `json:"authorIntro,omitempty"` // 作者介绍,长度限制10000字符,可选 + Directory string `json:"directory,omitempty"` // 目录,长度限制5000字符,可选 +} + +// GoodsAdd13 商品添加结构体(分类13专用) +type GoodsAdd13 struct { + GoodsAdd + Author string `json:"author"` // 作者,长度限制120字符,必须 + Press string `json:"press"` // 出版社,长度限制120字符,必须 + PubDate string `json:"pubDate"` // 出版日期,格式:yyyy-mm,必须 + Edition string `json:"edition,omitempty"` // 版次,取值范围:1~9999,可选 + PrintingTime string `json:"printingTime,omitempty"` // 印刷时间,格式:yyyy-mm,填写printTimes时必填,不能早于出版时间,可选 + PrintTimes string `json:"printTimes,omitempty"` // 印次,取值范围:1~9999,可选 + PrintingNum string `json:"printingNum,omitempty"` // 印数,单位:千册,取值范围:0.001~99999.999,可选 + Binding interface{} `json:"binding"` // 装帧,可以是编号(int)或文字(string),必须 + PageSize string `json:"pageSize,omitempty"` // 开本,可自己填写,可选 + Paper interface{} `json:"paper,omitempty"` // 纸张,可以是编号(int)或文字(string),可选 + PageNum string `json:"pageNum,omitempty"` // 页数,取值范围:1~99999,可选 + WordNum string `json:"wordNum,omitempty"` // 字数,单位:千字,取值范围:0~99999.999,可选 + OriPrice string `json:"oriPrice,omitempty"` // 图书定价,取值范围:0.01~99999999.99,可选 +} + +// UploadImgRet 图片上传返回结构体 +type UploadImgRet struct { + ErrorResponse *ErrorResponse `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse *UploadImgRetSuccessResponse `json:"successResponse"` +} + +type ErrorResponse struct { + Code int `json:"code"` + Data interface{} `json:"data"` + Msg string `json:"msg"` + SubCode string `json:"subCode"` + SubMsg string `json:"subMsg"` +} + +type UploadImgRetSuccessResponse struct { + Image Image `json:"image"` +} + +type Image struct { + Url string `json:"url"` +} + +// KfzCategoryList 获取本店分类返回结构体 +type KfzCategoryList struct { + ErrorResponse interface{} `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse []SuccessResponseItem `json:"successResponse"` +} + +type SuccessResponseItem struct { + Name string `json:"name"` + Value int64 `json:"value"` +} + +// KfzCategoryRet 获取公共分类返回结构体 +type KfzCategoryRet struct { + ErrorResponse interface{} `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse []CategoryLevel1 `json:"successResponse"` +} + +// 一级分类 (图书, 艺术品收藏, 文创与周边) +type CategoryLevel1 struct { + Children []CategoryLevel2 `json:"children"` + HasLeaf int `json:"hasLeaf"` + Id string `json:"id"` + Level int `json:"level"` + Name string `json:"name"` + Type int `json:"type"` + Years interface{} `json:"years,omitempty"` // 有些没有years字段 +} + +// 二级分类 +type CategoryLevel2 struct { + Children []CategoryLevel3 `json:"children"` + HasLeaf int `json:"hasLeaf"` + Id string `json:"id"` + Level int `json:"level"` + Name string `json:"name"` + Type int `json:"type"` + Tpl int `json:"tpl,omitempty"` // 有些有tpl字段 + Years interface{} `json:"years,omitempty"` // 有些有years字段 +} + +// 三级分类 +type CategoryLevel3 struct { + Children []CategoryLevel4 `json:"children,omitempty"` + HasLeaf int `json:"hasLeaf"` + Id string `json:"id"` + Level int `json:"level"` + Name string `json:"name"` + Tpl int `json:"tpl,omitempty"` + Type int `json:"type,omitempty"` + Years interface{} `json:"years,omitempty"` + EndYears interface{} `json:"endYears,omitempty"` // 有些有endYears字段 +} + +// 四级分类 +type CategoryLevel4 struct { + Children []CategoryLevel5 `json:"children,omitempty"` + HasLeaf int `json:"hasLeaf"` + Id string `json:"id"` + Level int `json:"level"` + Name string `json:"name"` + Tpl int `json:"tpl,omitempty"` + Years interface{} `json:"years,omitempty"` + EndYears interface{} `json:"endYears,omitempty"` +} + +// 五级分类 +type CategoryLevel5 struct { + Children []CategoryLevel6 `json:"children,omitempty"` + HasLeaf int `json:"hasLeaf"` + Id string `json:"id"` + Level int `json:"level"` + Name string `json:"name"` + Tpl int `json:"tpl,omitempty"` + Years interface{} `json:"years,omitempty"` + EndYears interface{} `json:"endYears,omitempty"` +} + +// 六级分类 (最深层级) +type CategoryLevel6 struct { + HasLeaf int `json:"hasLeaf"` + Id string `json:"id"` + Level int `json:"level"` + Name string `json:"name"` + Tpl int `json:"tpl,omitempty"` + Years interface{} `json:"years,omitempty"` + EndYears interface{} `json:"endYears,omitempty"` +} + +// AddGoodsRet 通用响应结构体 +type AddGoodsRet struct { + ErrorResponse *AddGoodsError `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse *AddGoodsRetSuccessResponse `json:"successResponse"` +} + +// AddGoodsError 错误响应详情 +type AddGoodsError struct { + Code int `json:"code"` + Data map[string]string `json:"data"` // 字段错误时使用 map,也可用具体结构体 + Msg string `json:"msg"` + SubCode string `json:"subCode"` + SubMsg string `json:"subMsg"` +} + +// AddGoodsRetSuccessResponse 成功响应详情 +type AddGoodsRetSuccessResponse struct { + Item Item `json:"item"` +} + +// Item 商品详情 +type Item struct { + AddTime string `json:"addTime"` + ItemId int64 `json:"itemId"` +} + +// GetGoodsListReq 获取商品列表请求结构体 +type GetGoodsListReq struct { + ItemId string `json:"itemId"` + Type string `json:"type"` + PageNum int `json:"pageNum"` + PageSize int `json:"pageSize"` + AddTimeBegin string `json:"addTimeBegin"` + AddTimeEnd string `json:"addTimeEnd"` + SortOrder string `json:"sortOrder"` + SortType string `json:"sortType"` +} + +// GetGoodsListResp 获取商品列表响应结构体 +type GetGoodsListResp struct { + ErrorResponse interface{} `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse *GetGoodsListSuccessResp `json:"successResponse"` +} + +// GetGoodsListSuccessResp 成功响应详情 +type GetGoodsListSuccessResp struct { + List []KfzGoodsItem `json:"list"` + PageNum int `json:"pageNum"` + PageSize int `json:"pageSize"` + Pages int `json:"pages"` + Size int `json:"size"` + Total int `json:"total"` +} + +// KfzGoodsItem 孔夫子商品项 +type KfzGoodsItem struct { + ItemId int64 `json:"itemId"` // 商品编号 + AddTime int64 `json:"addTime"` // 添加时间(Unix时间戳,秒级) + ItemName string `json:"itemName"` // 商品名称 + Price float64 `json:"price"` // 售价(单位:元) + Number int `json:"number"` // 库存数量 + Quality int64 `json:"quality"` // 品相(如:85、90) + QualityDesc string `json:"qualityDesc"` // 品相描述 + ImgUrl string `json:"imgUrl"` // 商品主图URL + Images string `json:"images"` // 商品图片列表 + CatId uint64 `json:"catId"` // 商品分类编号(可能超过int64范围,使用uint64) + MyCatId int `json:"myCatId"` // 本店分类编号 + ItemSn string `json:"itemSn"` // 货号 + BearShipping string `json:"bearShipping"` // 运费设置(seller/buyer) + MouldId int `json:"mouldId"` // 运费模板编号 + Weight float64 `json:"weight"` // 商品重量(千克) + WeightPiece float64 `json:"weightPiece"` // 商品标准本数 + ItemDesc string `json:"itemDesc"` // 商品描述 + Isbn string `json:"isbn"` // ISBN号 + Author string `json:"author"` // 作者 + Press string `json:"press"` // 出版社 + PubDate string `json:"pubDate"` // 出版日期 + OriPrice float64 `json:"oriPrice"` // 图书定价 + Binding string `json:"binding"` // 装帧 + PageSize string `json:"pageSize"` // 开本 + PageNum int `json:"pageNum"` // 页数 + Tpl int `json:"tpl"` // 模板编号 + ImportantDesc string `json:"importantDesc"` // 推荐语 + // 以下为实际API返回的额外字段 + BeginSaleTime uint64 `json:"beginSaleTime"` // 上架时间(Unix时间戳) + EndSaleTime uint64 `json:"endSaleTime"` // 下架时间(Unix时间戳) + BizType int `json:"bizType"` // 业务类型 + BooklibId uint64 `json:"booklibId"` // 书库ID + CertifyStatus string `json:"certifyStatus"` // 认证状态 + Discount int `json:"discount"` // 折扣 + IsDelete int `json:"isDelete"` // 是否删除 + IsDraft int `json:"isDraft"` // 是否草稿 + IsNewBook int `json:"isNewBook"` // 是否新书 + IsOnSale int `json:"isOnSale"` // 是否上架 + ProductArea uint64 `json:"productArea"` // 产地 + UpdateTime string `json:"updateTime"` // 更新时间(yyyy-MM-dd HH:mm:ss) + UserId uint64 `json:"userId"` // 用户ID + Years uint64 `json:"years"` // 年份 +} + +// Product 上架与下架请求结构体 +type Product struct { + ItemId string `json:"itemId"` +} + +// ProductRet 上架与下架返回结构体 +type ProductRet struct { + ErrorResponse interface{} `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse struct { + Item struct { + BeginSaleTime string `json:"beginSaleTime"` + CertifyStatus string `json:"certifyStatus"` + EndSaleTime string `json:"endSaleTime"` + IsOnSale string `json:"isOnSale"` + ItemId int `json:"itemId"` + UpdateTime string `json:"updateTime"` + } `json:"item"` + } `json:"successResponse"` +} + +// UpdatePriceReq 改价格请求结构体 +type UpdatePriceReq struct { + ItemId string `json:"itemId"` + Price float64 `json:"price"` +} + +// UpdatePriceRet 改价格返回结构体 +type UpdatePriceRet struct { + ErrorResponse interface{} `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse struct { + Item struct { + UpdateTime string `json:"updateTime"` + ItemId int `json:"itemId"` + Price string `json:"price"` + } `json:"item"` + } `json:"successResponse"` +} + +// UpdateStockReq 改库存请求结构体 +type UpdateStockReq struct { + ItemId string `json:"itemId"` + Number int64 `json:"number"` +} + +// UpdateStockRet 改库存返回结构体 +type UpdateStockRet struct { + ErrorResponse interface{} `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse struct { + Item struct { + UpdateTime string `json:"updateTime"` + ItemId int `json:"itemId"` + Number string `json:"number"` + } `json:"item"` + } `json:"successResponse"` +} + +// AddGoodsToErp 获取商品后请求ERP结构体 +type AddGoodsToErp struct { + ShopId string `json:"shopId"` + ShopType string `json:"shopType"` + Token string `json:"token"` + SycFlag int `json:"sycFlag"` + TaskId string `json:"taskId"` + PageFlag int `json:"pageFlag"` + GoodsList []GoodsList +} +type GoodsList struct { + ISBN string `json:"isbn"` + ItemName string `json:"itemName"` + Price string `json:"price"` + Quality string `json:"quality"` + Author string `json:"author"` + Press string `json:"press"` + PubDate string `json:"pubDate"` + ItemId string `json:"itemId"` + AddTime string `json:"addTime"` + BeginSaleTime string `json:"beginSaleTime"` + IsDraft string `json:"isDraft"` + Discount string `json:"discount"` + Stock string `json:"stock"` + MyCatId string `json:"myCatId"` + BearShipping string `json:"bearShipping"` + Weight string `json:"weight"` + CatId string `json:"catId"` + IsNewBook string `json:"isNewBook"` + BizType string `json:"bizType"` + CertifyStatus string `json:"certifyStatus"` + WeightPiece string `json:"weightPiece"` + MouldId string `json:"mouldId"` + BooklibId string `json:"booklibId"` + IsOnSale string `json:"isOnSale"` + IsDelete string `json:"isDelete"` + UpdateTime string `json:"updateTime"` + EndSaleTime string `json:"endSaleTime"` + UserId string `json:"userId"` + ImgUrl string `json:"imgUrl"` + OriPrice string `json:"oriPrice"` + ItemSn string `json:"itemSn"` +} + +// AddGoodsToErpRet 获取商品后请求ERP返回结构体 +type AddGoodsToErpRet struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data bool `json:"data"` +} diff --git a/planB/type/minIo.go b/planB/type/minIo.go new file mode 100644 index 0000000..bc042c8 --- /dev/null +++ b/planB/type/minIo.go @@ -0,0 +1,15 @@ +package _type + +import ( + "github.com/minio/minio-go/v7" +) + +// MinIOClient 封装 MinIO 客户端和配置 +type MinIOClient struct { + Client *minio.Client + BucketName string + Endpoint string + AccessKey string + SecretKey string + UseSSL bool +} diff --git a/planB/type/modules/image.go b/planB/type/modules/image.go new file mode 100644 index 0000000..99e6de2 --- /dev/null +++ b/planB/type/modules/image.go @@ -0,0 +1,8 @@ +package modules + +// ImageResult 定义图片打水印返回结构 +type ImageResult struct { + Success bool `json:"success"` + Format string `json:"format"` + Data string `json:"data"` +} diff --git a/planB/type/modules/pdd.go b/planB/type/modules/pdd.go new file mode 100644 index 0000000..c96a3e4 --- /dev/null +++ b/planB/type/modules/pdd.go @@ -0,0 +1,9 @@ +package modules + +// GoodsImageUploadResponse 商品图片上传响应结构 +type GoodsImageUploadResponse struct { + GoodsImageUploadResponse struct { + ImageURL string `json:"image_url"` + RequestID string `json:"request_id"` + } `json:"goods_image_upload_response"` +} diff --git a/planB/type/pinduoduo/goodsAdd.go b/planB/type/pinduoduo/goodsAdd.go new file mode 100644 index 0000000..c02077e --- /dev/null +++ b/planB/type/pinduoduo/goodsAdd.go @@ -0,0 +1,262 @@ +package pinduoduo + +type GoodsAdd struct { + GoodsName string `json:"goods_name"` // 商品名称 + CarouselGallery []string `json:"carousel_gallery"` // 轮播图 + CatId int64 `json:"cat_id"` // 商品分类 + GoodsType int64 `json:"goods_type"` // 商品类型 1-国内普通商品,2-一般贸易,3-保税仓BBC直供,4-海外BC直邮 ,5-流量 ,6-话费 ,7-优惠券 ,8-QQ充值 ,9-加油卡,15-商家卡券,18-海外CC行邮 19-平台卡券 + MarketPrice int64 `json:"market_price"` // 参考价格,单位为分 + DetailGallery []string `json:"detail_gallery"` // 详情图 + OutGoodsId string `json:"out_goods_id"` // 商品ID + SkuList []Sku `json:"sku_list"` // SKU列表 + IsFolt bool `json:"is_folt"` // 是否支持假一赔十,false-不支持,true-支持 + IsPreSale bool `json:"is_pre_sale"` // 是否预售,true-预售商品,false-非预售商品 + IsRefundable bool `json:"is_refundable"` // 是否7天无理由退换货,true-支持,false-不支持 + SecondHand bool `json:"second_hand"` // 是否二手商品, true -二手商品 ,false-全新商品 + CostTemplateId string `json:"cost_template_id"` // 物流运费模板ID + CountryId int64 `json:"country_id"` // 国家ID + ShipmentLimitSecond int64 `json:"shipment_limit_second"` // 承诺发货时间(秒),普通、进口商品可选48小时或24小时;直邮商品(goods_type=4)只可入参120小时,直供商品(goods_type=3)只可入参96小时;is_pre_sale为true时不必传 + TwoPiecesDiscount int64 `json:"two_pieces_discount"` // 满2件折扣,可选范围0-100, 0表示取消,95表示95折,设置需先查询规则接口获取实际可填范围 + GoodsProperties []GoodsProperties `json:"goods_properties"` //商品属性列表 +} +type GoodsProperties struct { + RefPid int64 `json:"ref_pid"` // 属性名称 + Value string `json:"value"` // 属性值 + ValueUnit string `json:"value_unit"` // 属性单位 + Vid int64 `json:"vid"` // 属性值 id +} + +type Sku struct { + IsOnsale int64 `json:"is_onsale"` // sku上架状态,0-已下架,1-上架中 + LimitQuantity int64 `json:"limit_quantity"` // sku购买限制,只入参999 + MultiPrice int64 `json:"multi_price"` // 商品团购价格,单位为分 + Price int64 `json:"price"` // 商品单买价格,单位为分 + SkuProperties []SkuProperty `json:"sku_properties"` // sku属性列表 + Quantity int64 `json:"quantity"` // 商品sku库存初始数量,后续库存update只使用stocks.update接口进行调用 + ThumbUrl string `json:"thumb_url"` // sku 缩略图 + SpecIdList string `json:"spec_id_list"` // 商品规格列表,根据pdd.goods.spec.id.get生成的规格属性id,例如:颜色规格下商家新增白色和黑色,大小规格下商家新增L和XL,则由4种spec组合,入参一种组合即可,在skulist中需要有4个spec组合的sku,示例:[20,5] + Weight int64 `json:"weight"` // 重量,单位为g + OutSkuSn string `json:"out_sku_sn"` // 商品 sku编号 +} + +type SkuProperty struct { + Punit string `json:"punit"` // 属性单位 + RefPid int64 `json:"ref_pid"` // 属性id + Value string `json:"value"` // 属性值 + Vid int64 `json:"vid"` // 属性值id +} + +// PriceMod 价格处理 +type PriceMod struct { + Min int64 `json:"min"` // 价格区间最小值 + Max int64 `json:"max"` // 价格区间最大值 + MarkupRate int64 `json:"markup_rate"` // 加价比例 + MarkupValue int64 `json:"markup_value"` // 价格区间加价值 +} + +// GoodsCommitDetail 获取商品提交的商品详情 +type GoodsCommitDetail struct { + GoodsCommitId int64 `json:"goods_commit_id"` // 商品提交id + GoodsId int64 `json:"goods_id"` // 商品id +} + +// PddSuccessResponse 拼多多接口 PddGoodsOuterCatMappingGet 返回结构体 +type PddSuccessResponse struct { + OuterCatMappingGetResponse PddCategoryMappingResponse `json:"outer_cat_mapping_get_response"` +} +type PddCategoryMappingResponse struct { + CatID1 int64 `json:"cat_id1"` // 一级类目 ID + CatID2 int64 `json:"cat_id2"` // 二级类目 ID + CatID3 int64 `json:"cat_id3"` // 三级类目 ID + CatID4 int64 `json:"cat_id4"` // 四级类目 ID + RequestID string `json:"request_id"` // 请求 ID +} + +// GoodsAddResponseWrapper 拼多多接口 PddGoodsAdd 返回结构体 +type GoodsAddResponseWrapper struct { + Response GoodsAddData `json:"goods_add_response"` +} +type GoodsAddData struct { + GoodsCommitID int64 `json:"goods_commit_id"` + GoodsID int64 `json:"goods_id"` + MatchedSpuID *int64 `json:"matched_spu_id"` // null值需要特殊处理 + RequestID string `json:"request_id"` +} + +// GoodsCommitDetailResponse 拼多多接口 PddGoodsCommitDetail 响应结构体 +type GoodsCommitDetailResponse struct { + GoodsCommitDetailResponse struct { + BadFruitClaim int `json:"bad_fruit_claim"` + BuyLimit int `json:"buy_limit"` + CarouselGalleryList []string `json:"carousel_gallery_list"` + CatID int `json:"cat_id"` + CommitMessage interface{} `json:"commit_message"` + CostTemplateID int64 `json:"cost_template_id"` + CountryID int `json:"country_id"` + CustomerNum int `json:"customer_num"` + Customs string `json:"customs"` + Deleted int `json:"deleted"` + DeliveryOneDay interface{} `json:"delivery_one_day"` + DeliveryType interface{} `json:"delivery_type"` + DetailGalleryList []string `json:"detail_gallery_list"` + ElecGoodsAttributes interface{} `json:"elec_goods_attributes"` + EndProductionDate interface{} `json:"end_production_date"` + FabricContentID interface{} `json:"fabric_content_id"` + FabricID interface{} `json:"fabric_id"` + GoodsCommitID int64 `json:"goods_commit_id"` + GoodsDesc string `json:"goods_desc"` + GoodsID int64 `json:"goods_id"` + GoodsName string `json:"goods_name"` + GoodsPattern int `json:"goods_pattern"` + GoodsPropertyList []interface{} `json:"goods_property_list"` + GoodsStatus int `json:"goods_status"` + GoodsTradeAttr interface{} `json:"goods_trade_attr"` + GoodsTravelAttr interface{} `json:"goods_travel_attr"` + GoodsType int `json:"goods_type"` + HdThumbURL string `json:"hd_thumb_url"` + ImageURL string `json:"image_url"` + InvoiceStatus int `json:"invoice_status"` + IsCustoms int `json:"is_customs"` + IsFolt int `json:"is_folt"` + IsGroupPreSale interface{} `json:"is_group_pre_sale"` + IsPreSale int `json:"is_pre_sale"` + IsRefundable int `json:"is_refundable"` + IsSkuPreSale int `json:"is_sku_pre_sale"` + LackOfWeightClaim interface{} `json:"lack_of_weight_claim"` + LocalServiceIDList interface{} `json:"local_service_id_list"` + MaiJiaZiTi interface{} `json:"mai_jia_zi_ti"` + MarketPrice int `json:"market_price"` + OrderLimit int `json:"order_limit"` + OriginCountryID int `json:"origin_country_id"` + OutSourceGoodsID interface{} `json:"out_source_goods_id"` + OutSourceType interface{} `json:"out_source_type"` + OuterGoodsID string `json:"outer_goods_id"` + OverseaGoods interface{} `json:"oversea_goods"` + OverseaType int `json:"oversea_type"` + PaperLength interface{} `json:"paper_length"` + PaperNetWeight interface{} `json:"paper_net_weight"` + PaperPliesNum interface{} `json:"paper_plies_num"` + PaperWidth interface{} `json:"paper_width"` + PreSaleTime int `json:"pre_sale_time"` + PrivacyDelivery int `json:"privacy_delivery"` + ProductionLicense interface{} `json:"production_license"` + ProductionStandardNumber interface{} `json:"production_standard_number"` + QuanGuoLianBao int `json:"quan_guo_lian_bao"` + RequestID string `json:"request_id"` + SecondHand int `json:"second_hand"` + ShangMenAnZhuang interface{} `json:"shang_men_an_zhuang"` + ShelfLife interface{} `json:"shelf_life"` + ShipmentLimitSecond int `json:"shipment_limit_second"` + ShopGroupID interface{} `json:"shop_group_id"` + ShopGroupName interface{} `json:"shop_group_name"` + SizeSpecID interface{} `json:"size_spec_id"` + SkuList []SkuInfo `json:"sku_list"` + SkuType interface{} `json:"sku_type"` + SongHuoAnZhuang interface{} `json:"song_huo_an_zhuang"` + SongHuoRuHu interface{} `json:"song_huo_ru_hu"` + StartProductionDate interface{} `json:"start_production_date"` + ThumbURL string `json:"thumb_url"` + TinyName string `json:"tiny_name"` + TwoPiecesDiscount int `json:"two_pieces_discount"` + VideoGallery []VideoInfo `json:"video_gallery"` + Warehouse string `json:"warehouse"` + WarmTips string `json:"warm_tips"` + ZhiHuanBuXiu int `json:"zhi_huan_bu_xiu"` + } `json:"goods_commit_detail_response"` +} + +// SkuInfo 定义SKU信息 +type SkuInfo struct { + IsOnsale int `json:"is_onsale"` + Length interface{} `json:"length"` + LimitQuantity int `json:"limit_quantity"` + MultiPrice int `json:"multi_price"` + OutSkuSn string `json:"out_sku_sn"` + OutSourceSkuID interface{} `json:"out_source_sku_id"` + OverseaSku interface{} `json:"oversea_sku"` + Price int `json:"price"` + Quantity int `json:"quantity"` + ReserveQuantity int `json:"reserve_quantity"` + SkuID int64 `json:"sku_id"` + SkuPreSaleTime int `json:"sku_pre_sale_time"` + SkuPropertyList []SkuProperty `json:"sku_property_list"` + Spec []Spec `json:"spec"` + ThumbURL string `json:"thumb_url"` + Weight int `json:"weight"` +} + +// Spec 定义规格信息 +type Spec struct { + ParentID int `json:"parent_id"` + ParentName string `json:"parent_name"` + SpecID int64 `json:"spec_id"` + SpecName string `json:"spec_name"` + SpecNote interface{} `json:"spec_note"` +} + +// VideoInfo 定义视频信息 +type VideoInfo struct { + FileID interface{} `json:"file_id"` + VideoURL interface{} `json:"video_url"` +} + +// SetSaleStatusGoodsTaskReq 设置商品上下架状态 +type SetSaleStatusGoodsTaskReq struct { + GoodsId int64 `json:"goods_id"` // 拼多多商品 id + IsOnsale int `json:"is_onsale"` // 上下架状态:1:上架 0:下架 +} + +// SetSaleStatusGoodsTaskResponse 拼多多 pddGoodsSaleStatusSet 响应结构 +type SetSaleStatusGoodsTaskResponse struct { + GoodsSaleStatusSetResponse GoodsSaleStatusSetResponse `json:"goods_sale_status_set_response"` +} +type GoodsSaleStatusSetResponse struct { + IsSuccess bool `json:"is_success"` + Msg *string `json:"msg"` // 或使用 *string, 因为原值为null + RequestId string `json:"request_id"` +} + +// DeleteGoodsCommitResponse 删除商品响应结构 +type DeleteGoodsCommitResponse struct { + OpenAPIResponse bool `json:"open_api_response"` + RequestID string `json:"request_id"` +} + +// UpdateGoodsQuantity 更新库存 +type UpdateGoodsQuantity struct { + ForceUpdate bool `json:"force_update"` + GoodsId int64 `json:"goods_id"` + SkuId int64 `json:"sku_id"` + Quantity int64 `json:"quantity"` + UpdateType int `json:"update_type"` +} + +// UpdateGoodsQuantityResponse 更新库存响应结构 +type UpdateGoodsQuantityResponse struct { + GoodsQuantityUpdateResponse struct { + IsSuccess bool `json:"is_success"` + RequestID string `json:"request_id"` + } `json:"goods_quantity_update_response"` +} + +// UpdateSkuPrice 更新sku价格 +type UpdateSkuPrice struct { + GoodsId int64 `json:"goods_id"` + MarketPrice int64 `json:"market_price"` + MarketPriceInYuan string `json:"market_price_in_yuan"` + SkuPriceList []SkuPriceItem `json:"sku_price_list"` +} + +type SkuPriceItem struct { + GroupPrice int64 `json:"group_price"` + SinglePrice int64 `json:"single_price"` + SkuId int64 `json:"sku_id"` +} + +// UpdateGoodsSkuPriceResponse 更新sku价格响应结构 +type UpdateGoodsSkuPriceResponse struct { + GoodsUpdateSkuPriceResponse struct { + IsSuccess bool `json:"is_success"` + GoodsCommitId int64 `json:"goods_commit_id"` + } `json:"goods_update_sku_price_response"` +} diff --git a/planB/type/pinduoduo/goodsGet.go b/planB/type/pinduoduo/goodsGet.go new file mode 100644 index 0000000..c3bd246 --- /dev/null +++ b/planB/type/pinduoduo/goodsGet.go @@ -0,0 +1,47 @@ +package pinduoduo + +// GoodsQueryParams 商品查询参数 +type GoodsQueryParams struct { + Page int `json:"page" form:"page"` // 返回页码,默认1 + PageSize int `json:"page_size" form:"page_size"` // 返回数量,默认100,最大100 +} + +// GoodsListResponse 响应结构体 +type GoodsListResponse struct { + GoodsList []GoodsItem `json:"goods_list"` + TotalCount int `json:"total_count"` +} + +type GoodsItem struct { + CreatedAt int64 `json:"created_at"` + GoodsId int64 `json:"goods_id"` + GoodsName string `json:"goods_name"` + GoodsQuantity int `json:"goods_quantity"` + GoodsReserveQuantity int `json:"goods_reserve_quantity"` + ImageUrl string `json:"image_url"` + IsMoreSku int `json:"is_more_sku"` + IsOnsale int `json:"is_onsale"` + SkuList []SkuItem `json:"sku_list"` + ThumbUrl string `json:"thumb_url"` + Price int64 `json:"price"` + SkuCode string `json:"skuCode"` + SkuPropertyList []SkuPropertyList `json:"skuPropertyList"` + BigImg string `json:"bigImg"` +} + +type SkuPropertyList struct { + Punit string `json:"punit"` + RefPid int `json:"ref_pid"` + Value string `json:"value"` + Vid int `json:"vid"` +} + +type SkuItem struct { + IsSkuOnsale int `json:"is_sku_onsale"` + OuterGoodsId string `json:"outer_goods_id"` + OuterId string `json:"outer_id"` + ReserveQuantity int `json:"reserve_quantity"` + SkuId int64 `json:"sku_id"` + SkuQuantity int `json:"sku_quantity"` + Spec string `json:"spec"` +} diff --git a/planB/type/pinduoduo/pddNoticeMsg.go b/planB/type/pinduoduo/pddNoticeMsg.go new file mode 100644 index 0000000..9ecf811 --- /dev/null +++ b/planB/type/pinduoduo/pddNoticeMsg.go @@ -0,0 +1,7 @@ +package pinduoduo + +type PddNoticeMsg struct { + MallId string `json:"mallId"` + GoodsId string `json:"goodsId"` + Type string `json:"type"` +} diff --git a/planB/type/taobao/goodsGet.go b/planB/type/taobao/goodsGet.go new file mode 100644 index 0000000..0810810 --- /dev/null +++ b/planB/type/taobao/goodsGet.go @@ -0,0 +1,36 @@ +package taobao + +// TbGoodsListResponse 淘宝商品列表响应 +type TbGoodsListResponse struct { + Items []TbGoodsItemWrap `json:"items"` // 商品列表 +} + +// TbGoodsItemWrap 淘宝商品包装结构(items 数组中每项为 {"item": {...}}) +type TbGoodsItemWrap struct { + Item TbGoodsItem `json:"item"` +} + +// TbGoodsItem 淘宝商品信息 +type TbGoodsItem struct { + NumIid string `json:"numIid"` // 商品数字ID + Title string `json:"title"` // 商品标题 + Price string `json:"price"` // 商品价格(字符串,如 "99.00") + PicUrl string `json:"picUrl"` // 主图URL + Desc string `json:"desc"` // 商品描述HTML + Num int64 `json:"num"` // 库存数量 + ApproveStatus string `json:"approveStatus"` // 商品状态: onsale/instock + Modified string `json:"modified"` // 修改时间(字符串) + OuterId string `json:"outerId"` // 商家编码(ISBN等) + Nick string `json:"nick"` // 掌柜昵称 + Type string `json:"type"` // 商品类型 + Cid int64 `json:"cid"` // 类目ID + BigImg string `json:"bigImg"` // 大图URL(兼容拼多多字段命名) +} + +// TbSkuProperty 淘宝SKU属性(兼容字段) +type TbSkuProperty struct { + Punit string `json:"punit"` + RefPid int `json:"ref_pid"` + Value string `json:"value"` + Vid int `json:"vid"` +} diff --git a/planB/type/taobao/taobao.go b/planB/type/taobao/taobao.go new file mode 100644 index 0000000..428c585 --- /dev/null +++ b/planB/type/taobao/taobao.go @@ -0,0 +1,10 @@ +package taobao + +import ( + "net/http" +) + +// Tushu 图书商品管理工具主结构体。 +type Tushu struct { + Client *http.Client +} diff --git a/planB/type/type.go b/planB/type/type.go new file mode 100644 index 0000000..f05ff88 --- /dev/null +++ b/planB/type/type.go @@ -0,0 +1,24 @@ +package _type + +// GoodsType 接口类型 +type GoodsType string + +// AsyncTaskResponse 添加商品数据返回结构体 +type AsyncTaskResponse struct { + Msg string `json:"msg"` // 消息说明 + CurrentProgress int `json:"currentProgress"` // 当前进度 + Code string `json:"code"` // 状态码 + TaskKey string `json:"taskKey"` // 任务唯一标识 + TotalCount int `json:"totalCount"` // 总数量 +} + +type GetShopGoodsByShopIdAndIsbn struct { + Code string `json:"code"` + Data []struct { + Stock string `json:"stock"` + TrilateralId string `json:"trilateralId"` + SkuId string `json:"skuId"` + TotalPrice string `json:"totalPrice"` + Quality string `json:"quality"` + } `json:"data"` +} diff --git a/planB/type/xianyu/goodsAdd.go b/planB/type/xianyu/goodsAdd.go new file mode 100644 index 0000000..9934133 --- /dev/null +++ b/planB/type/xianyu/goodsAdd.go @@ -0,0 +1,168 @@ +package xianyu + +// GoodsAdd 商品新增请求结构体 +// 用于向各电商平台(闲鱼、拼多多、淘宝等)提交商品上架的相关信息 +type GoodsAdd struct { + AppId int64 `json:"appId"` // 应用 id + AppSecret string `json:"appSecret"` // 应用密钥[选填,有些平台需要] + Token string `json:"token"` // token[选填,有些平台需要] + ApiShopId int `json:"apiShopId"` // API使用的店铺ID[选填,有些平台需要] + TypePlatform int `json:"typePlatform"` // 平台类型 0-预留 1-拼多多 2-淘宝 3-京东 4-闲鱼 105-孔夫子 + ShopId int64 `json:"shopId"` // 店铺 ID + ShopToken string `json:"shopToken"` // 店铺 Token + ShopName string `json:"shopName"` // 店铺名称 + Province int `json:"province"` // 发货省,格式为省级行政区划代码(如210000代表辽宁省) + City int `json:"city"` // 发货市,格式为市级行政区划代码(如210100代表沈阳市) + District int `json:"district"` // 发货区,格式为区级行政区划代码(如210101代表和平区) + TypeClass string `json:"typeClass"` // 分类类型 + TypeGoods string `json:"typeGoods"` // 商品类型 + CatIds string `json:"catIds"` // 类目 ID + SkuMsgs []SkuMsg `json:"skuMsgs"` // 商品 SKU信息列表[选填] + Shop []ShopInfo `json:"shop"` // 闲鱼用店铺信息 + StuffStatus int64 `json:"stuffStatus"` // 成色,90代表对应成色等级 + BookData []BookInfo `json:"bookData"` // 图书类商品专属信息列表 + ItemKey string `json:"itemKey"` // 闲鱼批次商品 KEY + SkuItems []SkuItems `json:"skuItems"` // 商品SKU信息 + OuterId string `json:"outerId"` // 商品外部ID + Price int64 `json:"price"` // 商品价格 + OriginalPrice int64 `json:"originalPrice"` // 商品原价 + Stock int32 `json:"stock"` // 商品库存 + ItemBizType int32 `json:"itemBizType"` // 商品业务类型 + SpBizType int32 `json:"spBizType"` // 商品行业类型 +} + +// GoodsAddNoIsbn 商品新增请求结构体 +// 用于向各电商平台(闲鱼、拼多多、淘宝等)提交商品上架的相关信息 +type GoodsAddNoIsbn struct { + AppId int64 `json:"appId"` // 应用 id + AppSecret string `json:"appSecret"` // 应用密钥[选填,有些平台需要] + Token string `json:"token"` // token[选填,有些平台需要] + ApiShopId int `json:"apiShopId"` // API使用的店铺ID[选填,有些平台需要] + TypePlatform int `json:"typePlatform"` // 平台类型 0-预留 1-拼多多 2-淘宝 3-京东 4-闲鱼 105-孔夫子 + ShopId int64 `json:"shopId"` // 店铺 ID + ShopToken string `json:"shopToken"` // 店铺 Token + ShopName string `json:"shopName"` // 店铺名称 + Province int `json:"province"` // 发货省,格式为省级行政区划代码(如210000代表辽宁省) + City int `json:"city"` // 发货市,格式为市级行政区划代码(如210100代表沈阳市) + District int `json:"district"` // 发货区,格式为区级行政区划代码(如210101代表和平区) + TypeClass string `json:"typeClass"` // 分类类型 + TypeGoods string `json:"typeGoods"` // 商品类型 + CatIds string `json:"catIds"` // 类目 ID + SkuMsgs []SkuMsg `json:"skuMsgs"` // 商品 SKU信息列表[选填] + Shop []ShopInfo `json:"shop"` // 闲鱼用店铺信息 + StuffStatus int64 `json:"stuffStatus"` // 成色,90代表对应成色等级 + ItemKey string `json:"itemKey"` // 闲鱼批次商品 KEY + SkuItems []SkuItems `json:"skuItems"` // 商品SKU信息 + OuterId string `json:"outerId"` // 商品外部ID +} + +// ShopInfo 闲鱼店铺信息结构体 +// 包含闲鱼平台商品上架所需的店铺及商品基础信息 +type ShopInfo struct { + UserName string `json:"userName"` // 闲鱼会员名(必填) + Province int `json:"province"` // 发货省(必填),行政区划代码格式 + City int `json:"city"` // 发货市(必填),行政区划代码格式 + District int `json:"district"` // 发货区(必填),行政区划代码格式 + Title string `json:"title"` // 商品标题(必填) + Content string `json:"content"` // 商品描述(必填) + MainImgs []string `json:"mainImgs"` // 商品主图(必填),图片URL列表 + ContentImgs []string `json:"contentImgs"` // 商品内容图(选填),图片URL列表 +} + +// BookInfo 图书类商品信息结构体 +// 包含图书类商品上架所需的专属信息,适用于闲鱼等平台的图书品类 +type BookInfo struct { + ISBN string `json:"ISBN"` // ISBN编号(必填),图书唯一标识 + Title string `json:"Title"` // 书名(必填) + Author string `json:"Author"` // 作者(选填) + Publisher string `json:"Publisher"` // 出版社(选填) + ItemBizType int `json:"itemBizType"` // 闲鱼商品类型(枚举),2:普通商品(必填) + SpBizType int `json:"spBizType"` // 闲鱼行业类型(枚举),24:图书(必填) + Prices []int64 `json:"prices"` // 商品价格(必填),格式为[商品原价,商品售价],单位为分 + Stock int64 `json:"stock"` // 库存(必填),商品可售数量 + CatIds string `json:"catIds"` // 商品类目ID(必填) +} + +type SkuItems struct { + Price int64 `json:"price"` + Stock int32 `json:"stock"` + SkuText string `json:"sku_text"` + OuterID string `json:"outer_id"` +} + +// SkuMsg 商品SKU信息结构体 +// 补充定义原需求中提到的skuMsgs字段对应的结构体,保证结构体完整性 +type SkuMsg struct { + Key string `json:"key"` // 主键(必填) + Value string `json:"value"` // 值(必填) + Title string `json:"title"` // 商品标题(必填) + CatIds string `json:"cat_ids"` // 商品类目(必填) + MainImgs []string `json:"mainImgs"` // 商品主图(必填),图片URL列表 + ContentImgs []string `json:"contentImgs"` // 商品内容图(选填),图片URL列表 + ItemBizType int `json:"itemBizType"` // 闲鱼商品类型(枚举),2:普通商品(必填) + SpBizType int `json:"spBizType"` // 闲鱼行业类型(枚举),24:图书(必填) + Prices []int `json:"prices"` // 商品价格(必填),[商品售价,商品原价],单位为分 + Stock int `json:"stock"` // 库存(必填) + Content string `json:"content"` // 商品描述(必填) + UserName string `json:"userName"` // 闲鱼会员名(必填) +} + +// Token 闲鱼店铺token传递的是json串 +type Token struct { + AppId int64 `json:"app_id"` + AppSecret string `json:"app_secret"` + Username string `json:"username"` +} + +// Product 上架商品结构体 +type Product struct { + AppId int64 `json:"appId"` // 应用 id + AppSecret string `json:"appSecret"` // 应用密钥[选填,有些平台需要] + Token string `json:"token"` // token[选填,有些平台需要] + ProductID int64 `json:"product_id"` // 商品 ID + UserName []string `json:"user_name"` // 会员名 + SpecifyPublishTime string `json:"specify_publish_time"` // 指定发布时间 + NotifyURL string `json:"notify_url"` // 回调地址 +} + +// XianYuAddGoodsResponse 闲鱼商品新增响应结构体 +type XianYuAddGoodsResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data GoodsResData `json:"data"` +} + +type GoodsResData struct { + Success []SuccessItem `json:"success"` + Error []interface{} `json:"error"` // 空数组,使用 interface{} 或定义具体结构 +} + +type SuccessItem struct { + ItemKey string `json:"item_key"` + ProductID int64 `json:"product_id"` // 注意:这个数字较大,使用 int64 + ProductStatus int `json:"product_status"` +} + +// DownShelf 下架商品结构体 +type DownShelf struct { + AppId int64 `json:"appId"` // 应用 id + AppSecret string `json:"appSecret"` // 应用密钥[选填,有些平台需要] + ProductID int64 `json:"product_id"` // 商品 ID +} + +// UpdateStock 修改库存结构体 +type UpdateStock struct { + AppId int64 `json:"appId"` // 应用 id + AppSecret string `json:"appSecret"` // 应用密钥[选填,有些平台需要] + ProductID int64 `json:"product_id"` // 商品 ID + Stock int32 `json:"stock"` // 库存 +} + +// UpdatePrice 修改价格结构体 +type UpdatePrice struct { + AppId int64 `json:"appId"` // 应用 id + AppSecret string `json:"appSecret"` // 应用密钥[选填,有些平台需要] + ProductID int64 `json:"product_id"` // 商品 ID + Price int64 `json:"price"` // 价格 + OriginalPrice int64 `json:"originalPrice"` // 原价 +} diff --git a/planB/type/xianyu/goodsGet.go b/planB/type/xianyu/goodsGet.go new file mode 100644 index 0000000..86acc23 --- /dev/null +++ b/planB/type/xianyu/goodsGet.go @@ -0,0 +1,116 @@ +package xianyu + +// GoodsListReq 获取列表请求结构体 +type GoodsListReq struct { + AppId int64 `json:"appId"` // 应用 id + AppSecret string `json:"appSecret"` // 应用密钥[选填,有些平台需要] + UpdateTime []int64 `json:"update_time"` // 可传空 + ProductStatus int `json:"product_status"` + PageNo int `json:"page_no"` // 页码 + PageSize int `json:"page_size"` // 页大小 +} + +// GoodsListRet 获取列表返回结构体 +type GoodsListRet struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data GoodsListRetData `json:"data"` +} + +// GoodsListRetData 结构 +type GoodsListRetData struct { + List []GoodsListRetProduct `json:"list"` + Count int `json:"count"` + PageNo int `json:"page_no"` + PageSize int `json:"page_size"` +} + +// GoodsListRetProduct 商品结构 +type GoodsListRetProduct struct { + ProductID int64 `json:"product_id"` + ProductStatus int `json:"product_status"` + ItemBizType int `json:"item_biz_type"` + SpBizType int `json:"sp_biz_type"` + ChannelCatID string `json:"channel_cat_id"` + OriginalPrice int `json:"original_price"` + Price int `json:"price"` + Stock int `json:"stock"` + Sold int `json:"sold"` + Title string `json:"title"` + DistrictID int `json:"district_id"` + OuterID string `json:"outer_id"` + StuffStatus int `json:"stuff_status"` + ExpressFee int `json:"express_fee"` + SpecType int `json:"spec_type"` + Source int `json:"source"` + SpecifyPublishTime int64 `json:"specify_publish_time"` + OnlineTime int64 `json:"online_time"` + OfflineTime int64 `json:"offline_time"` + SoldTime int64 `json:"sold_time"` + UpdateTime int64 `json:"update_time"` + CreateTime int64 `json:"create_time"` +} + +// GoodsDetailReq 请求详商品情结构体 +type GoodsDetailReq struct { + AppId int64 `json:"appId"` // 应用 id + AppSecret string `json:"appSecret"` // 应用密钥[选填,有些平台需要] + ProductId int64 `json:"product_id"` // 管家商品id +} + +// GoodDetailRet 获取列表返回结构体 +type GoodDetailRet struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data GoodsDetailRet `json:"data"` +} + +// GoodsDetailRet 获取商品详情返回结构体 +type GoodsDetailRet struct { + ProductID int64 `json:"product_id"` + ProductStatus int `json:"product_status"` + PublishStatus int `json:"publish_status"` + ItemBizType int `json:"item_biz_type"` + SpBizType int `json:"sp_biz_type"` + FlashSaleType int `json:"flash_sale_type"` + ChannelCatID string `json:"channel_cat_id"` + Title string `json:"title"` + Price int `json:"price"` + OriginalPrice int `json:"original_price"` + ExpressFee int `json:"express_fee"` + Stock int32 `json:"stock"` + Sold int `json:"sold"` + OuterID string `json:"outer_id"` + StuffStatus int `json:"stuff_status"` + SpecifyPublishTime string `json:"specify_publish_time"` + PublishShop []PublishShop `json:"publish_shop"` + SpecType int `json:"spec_type"` + BookData BookData `json:"book_data"` + OnlineTime int64 `json:"online_time"` + OfflineTime int64 `json:"offline_time"` + SoldTime int64 `json:"sold_time"` + UpdateTime int64 `json:"update_time"` + CreateTime int64 `json:"create_time"` + IsTaxIncluded bool `json:"is_tax_included"` +} + +type PublishShop struct { + UserName string `json:"user_name"` + ItemID int64 `json:"item_id"` + Province int `json:"province"` + City int `json:"city"` + District int `json:"district"` + Title string `json:"title"` + Content string `json:"content"` + Images []string `json:"images"` + Status int `json:"status"` + WhiteImages string `json:"white_images"` + ServiceSupport string `json:"service_support"` +} + +type BookData struct { + ISBN string `json:"isbn"` + Title string `json:"title"` + Author string `json:"author"` + Publisher string `json:"publisher"` +} diff --git a/planB/validation/validation.go b/planB/validation/validation.go new file mode 100644 index 0000000..a0b52cb --- /dev/null +++ b/planB/validation/validation.go @@ -0,0 +1,14 @@ +package validation + +import ( + "fmt" + "os" +) + +func Validation() (string, error) { + taskId := os.Args[1] + if taskId == "" { + return "", fmt.Errorf("任务Id 不能为空") + } + return taskId, nil +} diff --git a/planC/config.yaml b/planC/config.yaml new file mode 100644 index 0000000..e8daa14 --- /dev/null +++ b/planC/config.yaml @@ -0,0 +1,95 @@ +server: + port: "8080" #服务器端口 + filter: 1 #是否开启违禁词过滤器 0=关闭 1=开启 + replace_mark: "0" #标题违规词是否替换* 0 不替换 1 替换(替换会继续发布,不替换则不发布) + redis_exp: 192 #redis过期时间 192小时(8天) + read_db: "mysql" #读数据库 mysql sqlite + err_pause_time: 3000 #错误暂停时间(毫秒) + sign_key: "jRQdCh52Z55Kzh1hADaA2ZtdTKetj2PXk60Tz5Yc0iz9aD8Wafbk7CwAZ8cz69A9zb9caZ3k9dnR3Ys06J5nYFPrZ0xE9p6TY8DCD538ryiRjW81YTPmk41tCEnXizPh" #签名密钥 + data_day: 2 #数据保存时间(天) + is_c: true #是否启动 C程序 +speed: #限速器 + pdd_speed: 18 #拼多多 每秒多少个任务 + xianyu_speed: 5 #闲鱼 每秒多少个任务 + watermark: 15 #打水印速率的个数 +minio: #minio 图片空间 + url: "103.236.68.64:19000" #minio地址 + access_key_id: "minio" #minio keyId + secret_access_key: "bhkXyaD2WdAF7C6z" #minio key + bucket_name: "my-pics" #存储桶 + target_dir: "test/2025" #目标目录 + use_ssl: false #是否使用 SSL +alive: + fluent: 50 #存活状态-流畅时间(毫秒) + slow: 200 #存活状态-缓慢时间(毫秒) +pool_config: + size: 500 #协程数量 + with_expiry_duration: 10 #过期时间 + with_pre_alloc: true #预分配 + with_max_blocking_tasks: 2000 #阻塞任务数 + with_nonblocking : true #非阻塞 +mysql_config: + db_name: "task_user" #数据库名称 + user: "root" #数据库用户名 + password: "root" #数据库密码 + host: "127.0.0.1" #数据库地址 + port: 3306 #数据库端口 + loglevel: "info" #数据库日志级别 info=开发环境:输出所有日志(SQL、执行时间等) warn=预发布:输出警告+错误 error=生产环境:只输出错误日志 silent=生产环境:关闭所有日志 + max_retry_times: 3 #数据库重试次数 + base_retry_interval: 100 #数据库重试间隔(毫秒) + max_retry_interval: 3 #数据库最大重试间隔(秒) + max_open_conns: 100 #数据库最大打开连接数 + max_idle_conns: 20 #数据库最大空闲连接数 + conn_max_idle_time: 30 #数据库连接最大空闲时间(分钟) + conn_max_lifetime: 1 #数据库连接最大生命周期(小数) +redis_config: + - db_name: "任务池" + db: 0 + addr: "127.0.0.1:6379" + password: "123456" + - db_name: "书品库" + db: 1 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "店铺信息" + db: 2 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "出版社信息列表" + db: 3 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "省市区列表" + db: 4 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有图片的 isbn" + db: 5 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有书籍的 isbn" + db: 6 + addr: "36.212.12.247:6379" + password: "long6166@@" +pdd_config: + client_id: "203c5a7ba8bd4b8488d5e26f93052642" #拼多多clientId + client_secret: "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" #拼多多clientSecret +http_url: + task_url: "http://127.0.0.1:8080" #A 程序接口地址 +file_url: + xian_yu_dll: "D:\\source\\planA\\planB\\modules\\xianYu" #闲鱼 DLL库路径 + pdd_dll: "D:\\source\\planA\\planB\\modules\\pdd" #拼多多 DLL库路径 + kfz_dll: "D:\\source\\planA\\planB\\modules\\kfz" #孔夫子 DLL库路径 + log_dll: "D:\\source\\planA\\planB\\modules\\logs" #日志 DLL库路径 + image_dll: "D:\\source\\planA\\planB\\modules\\image" #水印 DLL库路径 + b_file_name: "D:\\source\\planA\\planB\\planB.exe" #B 程序文件路径 + c_file_name: "D:\\source\\planA\\planC\\planC.exe" #C 程序文件路径 + create_task_url: "https://api.buzhiyushu.cn/zhishu/baseInfo/addNewTask" #新增任务接口 + create_task_notice_url: "http://36.212.1.63:8055/task" #核价软件提交数据通知接口 + banned_word_substitution_url : "http://36.212.16.27:13001/task/getFilterSetNew" #违禁词替换接口 + pdd_token_url: "https://api.buzhiyushu.cn/huidiao/pdd/getToken" #获取系统规定拼多多 token + deduction_url: "https://api.buzhiyushu.cn/zhishu/userRecharge/apiBalancePayment" #扣费接口 + pdd_get_goods_url: "http://pdd.buzhiyushu.cn/api/pdd/auth/getShopGoodsList" #查询拼多多商品接口 + pdd_get_goods_detail_url: "http://192.168.101.127:8085/api/pdd/auth/newGetShopGoodsDetailList" #查询拼多多商品详情列表接口 + pdd_add_goods_url: "http://192.168.101.127:18099/task/putShopGoods" #添加拼多多商品接口 + backup_url: "C:\\file\\backup" #备份文件路径 \ No newline at end of file diff --git a/planC/main.go b/planC/main.go new file mode 100644 index 0000000..cc03bba --- /dev/null +++ b/planC/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "context" + "fmt" + "planA/initialization/config" + planACron "planA/initialization/cron" + "planA/initialization/golabl" + "planA/initialization/mysql" + "planA/initialization/redis" + "planA/initialization/sqLite" + "sync" + + "github.com/robfig/cron/v3" +) + +var ( + backupMutex sync.Mutex +) + +func Init() error { + //初始化上下文 + golabl.Ctx = context.Background() + // 初始化配置 + configErr := config.Init("") + if configErr != nil { + return fmt.Errorf("初始化配置失败: %v", configErr) + } + // 初始化 mysql + mysqlErr := mysql.Init() + if mysqlErr != nil { + return fmt.Errorf("初始化mysql失败: %v", mysqlErr) + } + // 初始化 redis + redisErr := redis.Init() + if redisErr != nil { + return fmt.Errorf("初始化redis失败: %v", redisErr) + } + // 初始化 sqlite + sqliteErr := sqLite.Init() + if sqliteErr != nil { + return fmt.Errorf("初始化sqlite失败: %v", sqliteErr) + } + return nil +} + +func main() { + + // 初始化 + err := Init() + if err != nil { + fmt.Println("初始化失败:", err) + return + } + + fmt.Println("定时任务 启动成功") + + c := cron.New(cron.WithSeconds()) // 支持秒级别的精度 + + // 备份 body_backup到硬盘 - 每分钟执行一次,使用锁防止并发 + _, backupBodyBackupErr := c.AddFunc("0 * * * * ?", func() { + // 尝试获取锁,如果锁已被占用则直接返回 + if !backupMutex.TryLock() { + fmt.Println("上一次备份任务尚未完成,跳过本次执行") + return + } + defer backupMutex.Unlock() + fmt.Println("开始备份 body_backup到硬盘") + planACron.BackupBodyBackup() + fmt.Println("备份 body_backup到硬盘完成") + + }) + if backupBodyBackupErr != nil { + fmt.Println("定时任务 备份 body_backup到硬盘 启动失败") + return + } + + // 每天上午9点压缩昨天csv文件 + _, zipBackupFileErr := c.AddFunc("0 0 9 * * ?", func() { + fmt.Println("开始压缩昨天 csv文件") + planACron.ZipBackupFile() + }) + if zipBackupFileErr != nil { + fmt.Println("定时任务 zipBackupFile 启动失败") + return + } + + c.Run() // 启动调度器(阻塞运行) +} diff --git a/planC/modules/config/config.dll b/planC/modules/config/config.dll new file mode 100644 index 0000000..4e1d91b Binary files /dev/null and b/planC/modules/config/config.dll differ diff --git a/planC/modules/config/conifg.go b/planC/modules/config/conifg.go new file mode 100644 index 0000000..a8138f3 --- /dev/null +++ b/planC/modules/config/conifg.go @@ -0,0 +1,74 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// ConfigDLL 配置文件读取DLL结构 +type ConfigDLL struct { + dll *syscall.DLL + readConfigFile *syscall.Proc // 读取配置文件 + getVersion *syscall.Proc // 获取版本信息 + freeCString *syscall.Proc // 释放C字符串 +} + +// InitConfigDLL 初始化ConfigDLL +func InitConfigDLL() (*ConfigDLL, error) { + dllPath := filepath.Join("modules/config/", "config.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("config DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载config DLL 失败: %s", err) + } else { + return &ConfigDLL{ + dll: dll, + readConfigFile: dll.MustFindProc("ReadConfigFile"), + getVersion: dll.MustFindProc("GetVersion"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +// cStr 获取C字符串 +func (m *ConfigDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} + +// ReadConfigFile 读取配置文件 +func (m *ConfigDLL) ReadConfigFile(filePath, fileName string) (string, error) { + proc, err := m.dll.FindProc("ReadConfigFile") + if err != nil { + return "", fmt.Errorf("找不到函数 ReadConfigFile: %v", err) + } + + filePathPtr, _ := syscall.BytePtrFromString(filePath) + fileNamePtr, _ := syscall.BytePtrFromString(fileName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(filePathPtr)), + uintptr(unsafe.Pointer(fileNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} diff --git a/planC/modules/image/image.dll b/planC/modules/image/image.dll new file mode 100644 index 0000000..ed7d1c7 Binary files /dev/null and b/planC/modules/image/image.dll differ diff --git a/planC/modules/image/image.go b/planC/modules/image/image.go new file mode 100644 index 0000000..bf88774 --- /dev/null +++ b/planC/modules/image/image.go @@ -0,0 +1,107 @@ +package image + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "planA/planB/config" + "syscall" + "unsafe" +) + +var ( + gImageDll *ImageDLL +) + +// ImageDLL 图片工具DLL结构 +type ImageDLL struct { + Dll *syscall.DLL + AddWatermarkFromURLEx *syscall.Proc // 打水印 +} + +// InitImageDll 初始化 imageDLL +func InitImageDll() (*ImageDLL, error) { + fileConfig, getDllFileConfigErr := config.GetFileUrlConfig() + if getDllFileConfigErr != nil { + return nil, getDllFileConfigErr + } + dllPath := filepath.Join(fileConfig.ImageDll, "image.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("Image DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载Image DLL 失败: %s", err) + } + gImageDll = &ImageDLL{ + Dll: dll, + AddWatermarkFromURLEx: dll.MustFindProc("AddWatermarkFromURLEx"), + } + return gImageDll, nil +} + +// WatermarkConfig 添加水印 +type WatermarkConfig struct { + SourceImageURL string // 源图片URL地址 + WatermarkURL string // 水印图片URL地址 + Opacity float64 // 不透明度 (0.0-1.0) + Position string // 位置: center, top-left, top-right, bottom-left, bottom-right, tile + TileSpacing int // 平铺时的间距 + Scale float64 // 水印缩放比例 (0.0-1.0) + Rotation float64 // 旋转角度 (度数) + XOffset int // X轴偏移量 + YOffset int // Y轴偏移量 + Timeout int // 下载超时时间(秒),默认30秒 + OutputFormat string // 输出格式: "jpeg", "png", "auto"(默认auto,根据源图片格式)auto + JPEGQuality int // JPEG质量 (1-100),默认95 +} + +// AddWatermarkFromURLExs 添加水印 +func (m *ImageDLL) AddWatermarkFromURLExs(sourceImageUrl, watermarkUrl string) (string, error) { + + watermarkConfig := WatermarkConfig{ + SourceImageURL: sourceImageUrl, + WatermarkURL: watermarkUrl, + Position: "center", + Opacity: 1.0, + Scale: 1.0, + TileSpacing: 50, + Timeout: 30, + OutputFormat: "jpeg", + JPEGQuality: 95, + } + watermarkConfigJson, err := json.Marshal(watermarkConfig) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + + proc, err := m.Dll.FindProc("AddWatermarkFromURLEx") + if err != nil { + return "", fmt.Errorf("找不到函数 AddWatermarkFromURLEx: %v", err) + } + watermarkConfigJsonPtr, _ := syscall.BytePtrFromString(string(watermarkConfigJson)) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(watermarkConfigJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} diff --git a/planC/modules/logs/dll.go b/planC/modules/logs/dll.go new file mode 100644 index 0000000..48250ed --- /dev/null +++ b/planC/modules/logs/dll.go @@ -0,0 +1,395 @@ +package logs + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "planA/initialization/golabl" + "runtime" + "syscall" + "unsafe" +) + +const ( + LOG_LEVEL_DEBUG = "DEBUG" + LOG_LEVEL_INFO = "INFO" + LOG_LEVEL_WARNING = "WARNING" + LOG_LEVEL_ERROR = "ERROR" + LOG_LEVEL_SUCCESS = "SUCCESS" +) + +// LoggerDLL 封装 logger.dll 操作 +type LoggerDLL struct { + dll *syscall.LazyDLL + createLogger *syscall.LazyProc + createContext *syscall.LazyProc + logInfo *syscall.LazyProc + logError *syscall.LazyProc + logWarning *syscall.LazyProc + logSuccess *syscall.LazyProc + freeString *syscall.LazyProc + closeAllLoggers *syscall.LazyProc +} + +// LoggerConfig logger配置结构 +type LoggerConfig struct { + LogDir string `json:"log_dir"` + SplitType int `json:"split_type"` + RotateType int `json:"rotate_type"` + MaxSize int64 `json:"max_size"` + MaxCount int `json:"max_count"` + Level int `json:"level"` + EnableCaller bool `json:"enable_caller"` + DefaultTaskType string `json:"default_task_type"` +} + +var loggerDLLInstance *LoggerDLL +var loggerHandle string +var loggerContextHandle string + +// ensureLoggerDLL 确保logger DLL已加载 +func ensureLoggerDLL() (*LoggerDLL, error) { + if loggerDLLInstance != nil { + return loggerDLLInstance, nil + } + + // 检查是否在Windows平台 + if runtime.GOOS != "windows" { + return nil, fmt.Errorf("logger DLL only supported on Windows platform") + } + + dllPath := filepath.Join(golabl.Config.FileUrl.LogDll, "logger.dll") + + // 检查文件是否存在 + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + // 尝试从当前目录查找 + if _, err := os.Stat("logger.dll"); err == nil { + dllPath = "logger.dll" + } else { + return nil, fmt.Errorf("logger DLL not found at %s", dllPath) + } + } + + dll := syscall.NewLazyDLL(dllPath) + + loggerDLLInstance = &LoggerDLL{ + dll: dll, + createLogger: dll.NewProc("CreateLogger"), + createContext: dll.NewProc("CreateContextWithTaskType"), + logInfo: dll.NewProc("LogInfo"), + logError: dll.NewProc("LogError"), + logWarning: dll.NewProc("LogWarning"), + logSuccess: dll.NewProc("LogSuccess"), + freeString: dll.NewProc("FreeString"), + closeAllLoggers: dll.NewProc("CloseAllLoggers"), + } + + return loggerDLLInstance, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// InitializeLogger 初始化logger +func InitializeLogger(logDir string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + // 确保日志目录存在 + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 创建logger配置 + config := LoggerConfig{ + LogDir: logDir, + SplitType: 2, // SplitByDay + RotateType: 0, // RotateBySize + MaxSize: 100 * 1024 * 1024, // 100MB + MaxCount: 10, + Level: 1, // LevelInfo - 只显示INFO及以上级别的日志 + EnableCaller: true, + DefaultTaskType: "main", + } + + configJSON, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("序列化配置失败: %v", err) + } + + // 调用CreateLogger + configPtr, _ := syscall.BytePtrFromString(string(configJSON)) + ret, _, _ := m.createLogger.Call(uintptr(unsafe.Pointer(configPtr))) + + if ret == 0 { + return fmt.Errorf("创建logger失败") + } + + // 获取logger句柄 + handle := cStr(ret) + loggerHandle = handle + + // 释放返回的字符串 + m.freeString.Call(ret) + + // 创建默认上下文 + return createLoggerContext("main") +} + +// createLoggerContext 创建带任务类型的logger上下文 +func createLoggerContext(taskType string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerHandle == "" { + return fmt.Errorf("logger未初始化") + } + + handlePtr, _ := syscall.BytePtrFromString(loggerHandle) + taskTypePtr, _ := syscall.BytePtrFromString(taskType) + + ret, _, _ := m.createContext.Call( + uintptr(unsafe.Pointer(handlePtr)), + uintptr(unsafe.Pointer(taskTypePtr)), + ) + + if ret == 0 { + return fmt.Errorf("创建logger上下文失败") + } + + // 获取上下文句柄 + loggerContextHandle = cStr(ret) + + // 释放返回的字符串 + m.freeString.Call(ret) + + return nil +} + +// SetLogTaskType 设置当前日志任务类型 +func SetLogTaskType(taskType string) error { + return createLoggerContext(taskType) +} + +// LogInfo 记录信息日志 +func LogInfo(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logInfo.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogError 记录错误日志 +func LogError(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logError.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogWarning 记录警告日志 +func LogWarning(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logWarning.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogSuccess 记录成功日志 +func LogSuccess(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logSuccess.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// CloseLogger 关闭logger +func CloseLogger() error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + ret, _, _ := m.closeAllLoggers.Call() + if ret == 0 { + return fmt.Errorf("关闭logger失败") + } + + m.freeString.Call(ret) + + loggerHandle = "" + loggerContextHandle = "" + loggerDLLInstance = nil + + return nil +} + +// GetLoggerHandle 获取当前logger句柄(用于外部调用) +func GetLoggerHandle() string { + return loggerContextHandle +} + +// IsLoggerInitialized 检查logger是否已初始化 +func IsLoggerInitialized() bool { + return loggerHandle != "" && loggerContextHandle != "" +} + +// SetConsoleOutput 设置控制台输出开关 +func SetConsoleOutput(enabled bool) { + if enabled { + os.Setenv("LOG_CONSOLE", "true") + } else { + os.Setenv("LOG_CONSOLE", "false") + } +} + +// LogWithLevel 带级别的日志记录,可以精确控制显示 +func LogWithLevel(level, message string, showConsole bool) { + if !IsLoggerInitialized() { + return + } + + switch level { + case "ERROR": + LogError(message) + case "WARNING": + LogWarning(message) + case "SUCCESS": + LogSuccess(message) + case "INFO": + LogInfo(message) + default: + LogInfo(message) + } +} + +// LogOnlyFile 仅写入文件,不输出到控制台 +func LogOnlyFile(level, message string) { + // 临时禁用控制台输出 + os.Setenv("LOG_CONSOLE", "false") + LogWithLevel(level, message, false) +} + +// LogConsoleAndFile 同时输出到控制台和文件 +func LogConsoleAndFile(level, message string) { + // 临时启用控制台输出 + os.Setenv("LOG_CONSOLE", "true") + LogWithLevel(level, message, true) +} + +// LoggingMiddleware 记录日志 +func LoggingMiddleware(level string, str string) { + initializeLoggerErr := InitializeLogger("logs") + if initializeLoggerErr != nil { + fmt.Println("初始化日志失败:", initializeLoggerErr) + return + } + setLogTaskTypeErr := SetLogTaskType("task") + if setLogTaskTypeErr != nil { + fmt.Println("设置日志任务类型失败:", setLogTaskTypeErr) + return + } + + switch { + case level == LOG_LEVEL_ERROR: + fmt.Println(str) + logErrorErr := LogError(str) + if logErrorErr != nil { + fmt.Println("记录错误日志失败:", logErrorErr) + return + } + case level == LOG_LEVEL_WARNING: + logWarningErr := LogWarning(str) + if logWarningErr != nil { + fmt.Println("记录警告日志失败:", logWarningErr) + return + } + case level == LOG_LEVEL_SUCCESS: + logSuccessErr := LogSuccess(str) + if logSuccessErr != nil { + fmt.Println("记录成功日志失败:", logSuccessErr) + return + } + default: + logInfoErr := LogInfo(str) + if logInfoErr != nil { + fmt.Println("记录信息日志失败:", logInfoErr) + return + } + } +} diff --git a/planC/modules/logs/logger.dll b/planC/modules/logs/logger.dll new file mode 100644 index 0000000..52e722b Binary files /dev/null and b/planC/modules/logs/logger.dll differ diff --git a/planC/modules/logs/logger.md b/planC/modules/logs/logger.md new file mode 100644 index 0000000..411f310 --- /dev/null +++ b/planC/modules/logs/logger.md @@ -0,0 +1,602 @@ +# logger.dll 使用教程 +## 1. 创建DLL工具实例 +### 加载DLL文件 +```gotemplate +package logs + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "syscall" + "unsafe" +) + +// LoggerDLL 封装 logger.dll 操作 +type LoggerDLL struct { + dll *syscall.LazyDLL + createLogger *syscall.LazyProc + createContext *syscall.LazyProc + logInfo *syscall.LazyProc + logError *syscall.LazyProc + logWarning *syscall.LazyProc + logSuccess *syscall.LazyProc + freeString *syscall.LazyProc + closeAllLoggers *syscall.LazyProc +} + +// LoggerConfig logger配置结构 +type LoggerConfig struct { + LogDir string `json:"log_dir"` + SplitType int `json:"split_type"` + RotateType int `json:"rotate_type"` + MaxSize int64 `json:"max_size"` + MaxCount int `json:"max_count"` + Level int `json:"level"` + EnableCaller bool `json:"enable_caller"` + DefaultTaskType string `json:"default_task_type"` +} + +var loggerDLLInstance *LoggerDLL +var loggerHandle string +var loggerContextHandle string + +// ensureLoggerDLL 确保logger DLL已加载 +func ensureLoggerDLL() (*LoggerDLL, error) { + if loggerDLLInstance != nil { + return loggerDLLInstance, nil + } + + // 检查是否在Windows平台 + if runtime.GOOS != "windows" { + return nil, fmt.Errorf("logger DLL only supported on Windows platform") + } + + // logger.dll 位于 dll/logger.dll + //dllPath := filepath.Join("modules", "logs", "logger.dll") + dllPath := "D:\\www\\wwwroot\\planA\\modules\\logs\\logger.dll" + + // 检查文件是否存在 + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + // 尝试从当前目录查找 + if _, err := os.Stat("logger.dll"); err == nil { + dllPath = "logger.dll" + } else { + return nil, fmt.Errorf("logger DLL not found at %s", dllPath) + } + } + + dll := syscall.NewLazyDLL(dllPath) + + loggerDLLInstance = &LoggerDLL{ + dll: dll, + createLogger: dll.NewProc("CreateLogger"), + createContext: dll.NewProc("CreateContextWithTaskType"), + logInfo: dll.NewProc("LogInfo"), + logError: dll.NewProc("LogError"), + logWarning: dll.NewProc("LogWarning"), + logSuccess: dll.NewProc("LogSuccess"), + freeString: dll.NewProc("FreeString"), + closeAllLoggers: dll.NewProc("CloseAllLoggers"), + } + + return loggerDLLInstance, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// InitializeLogger 初始化logger +func InitializeLogger(logDir string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + // 确保日志目录存在 + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 创建logger配置 + config := LoggerConfig{ + LogDir: logDir, + SplitType: 1, // SplitByDay + RotateType: 0, // RotateBySize + MaxSize: 100 * 1024 * 1024, // 100MB + MaxCount: 10, + Level: 1, // LevelInfo - 只显示INFO及以上级别的日志 + EnableCaller: true, + DefaultTaskType: "main", + } + + configJSON, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("序列化配置失败: %v", err) + } + + // 调用CreateLogger + configPtr, _ := syscall.BytePtrFromString(string(configJSON)) + ret, _, _ := m.createLogger.Call(uintptr(unsafe.Pointer(configPtr))) + + if ret == 0 { + return fmt.Errorf("创建logger失败") + } + + // 获取logger句柄 + handle := cStr(ret) + loggerHandle = handle + + // 释放返回的字符串 + m.freeString.Call(ret) + + // 创建默认上下文 + return createLoggerContext("main") +} + +// createLoggerContext 创建带任务类型的logger上下文 +func createLoggerContext(taskType string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerHandle == "" { + return fmt.Errorf("logger未初始化") + } + + handlePtr, _ := syscall.BytePtrFromString(loggerHandle) + taskTypePtr, _ := syscall.BytePtrFromString(taskType) + + ret, _, _ := m.createContext.Call( + uintptr(unsafe.Pointer(handlePtr)), + uintptr(unsafe.Pointer(taskTypePtr)), + ) + + if ret == 0 { + return fmt.Errorf("创建logger上下文失败") + } + + // 获取上下文句柄 + loggerContextHandle = cStr(ret) + + // 释放返回的字符串 + m.freeString.Call(ret) + + return nil +} + +// SetLogTaskType 设置当前日志任务类型 +func SetLogTaskType(taskType string) error { + return createLoggerContext(taskType) +} + +// LogInfo 记录信息日志 +func LogInfo(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logInfo.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogError 记录错误日志 +func LogError(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logError.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogWarning 记录警告日志 +func LogWarning(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logWarning.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// LogSuccess 记录成功日志 +func LogSuccess(message string) error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + if loggerContextHandle == "" { + return fmt.Errorf("logger上下文未初始化") + } + + ctxPtr, _ := syscall.BytePtrFromString(loggerContextHandle) + msgPtr, _ := syscall.BytePtrFromString(message) + + m.logSuccess.Call( + uintptr(unsafe.Pointer(ctxPtr)), + uintptr(unsafe.Pointer(msgPtr)), + ) + + return nil +} + +// CloseLogger 关闭logger +func CloseLogger() error { + m, err := ensureLoggerDLL() + if err != nil { + return err + } + + ret, _, _ := m.closeAllLoggers.Call() + if ret == 0 { + return fmt.Errorf("关闭logger失败") + } + + m.freeString.Call(ret) + + loggerHandle = "" + loggerContextHandle = "" + loggerDLLInstance = nil + + return nil +} + +// GetLoggerHandle 获取当前logger句柄(用于外部调用) +func GetLoggerHandle() string { + return loggerContextHandle +} + +// IsLoggerInitialized 检查logger是否已初始化 +func IsLoggerInitialized() bool { + return loggerHandle != "" && loggerContextHandle != "" +} + +// SetConsoleOutput 设置控制台输出开关 +func SetConsoleOutput(enabled bool) { + if enabled { + os.Setenv("LOG_CONSOLE", "true") + } else { + os.Setenv("LOG_CONSOLE", "false") + } +} + +// LogWithLevel 带级别的日志记录,可以精确控制显示 +func LogWithLevel(level, message string, showConsole bool) { + if !IsLoggerInitialized() { + return + } + + switch level { + case "ERROR": + LogError(message) + case "WARNING": + LogWarning(message) + case "SUCCESS": + LogSuccess(message) + case "INFO": + LogInfo(message) + default: + LogInfo(message) + } +} + +// LogOnlyFile 仅写入文件,不输出到控制台 +func LogOnlyFile(level, message string) { + // 临时禁用控制台输出 + os.Setenv("LOG_CONSOLE", "false") + LogWithLevel(level, message, false) +} + +// LogConsoleAndFile 同时输出到控制台和文件 +func LogConsoleAndFile(level, message string) { + // 临时启用控制台输出 + os.Setenv("LOG_CONSOLE", "true") + LogWithLevel(level, message, true) +} + +``` + +# 接口详情 +## 创建日志器--CreateLogger +### 请求信息 +```gotemplate +dll.CreateLogger(configJSON) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| configJSON | string | 是 | 配置信息JSON字符串 | +#### 配置JSON结构 +```json +{ + "log_dir": "/path/to/logs", + "split_type": 0, + "rotate_type": 0, + "max_size": 104857600, + "max_count": 30, + "level": 1, + "enable_caller": true, + "default_task_type": "main" +} +``` +#### 参数说明: +```text +log_dir: 日志目录路径 +split_type: 分片方式(0=按月,1=按天,2=按小时,3=按分钟,4=按秒) +rotate_type: 轮转方式(0=按大小,1=按数量) +max_size: 最大文件大小(字节),仅在rotate_type=0时有效 +max_count: 最大文件数量,仅在rotate_type=1时有效 +level: 日志级别(0=SUCCESS,1=INFO,2=WARNING,3=ERROR) +enable_caller: 是否启用调用者信息 +default_task_type: 默认任务类型 +``` +### 响应示例 +```json +"错误: 创建日志目录失败: permission denied" +``` + +## 创建带任务类型的上下文--CreateContextWithTaskType +### 请求信息 +```gotemplate +dll.CreateContextWithTaskType(loggerHandle, taskType) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| loggerHandle | string | 是 | 日志器句柄 | +| taskType | string | 是 | 任务类型 | +### 响应示例 +```json +"ctx_1645497600000000000" +``` +#### 错误响应示例 +```json +"错误: 无效的logger句柄" +``` + +## 记录信息日志--LogInfo +### 请求信息 +```gotemplate +dll.LogInfo(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 记录错误日志--LogError +### 请求信息 +```gotemplate +dll.LogError(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 记录警告日志--LogWarning +### 请求信息 +```gotemplate +dll.LogWarning(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 记录成功日志--LogSuccess +### 请求信息 +```gotemplate +dll.LogSuccess(ctxHandle, message) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| ctxHandle | string | 是 | 上下文句柄 | +| message | string | 是 | 日志消息 | +### 响应示例 +```text +无返回值,日志将写入到对应的日志文件中。 +``` + +## 获取日志条目--GetLogs +### 请求信息 +```gotemplate +dll.GetLogs(loggerHandle, configJSON) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------| +| loggerHandle | string | 是 | 日志器句柄 | +| configJSON | string | 是 | 查询配置JSON | +#### 查询配置JSON结构 +```json +{ + "level": 1, + "task_type": "main", + "start_time": "2024-01-01 00:00:00", + "end_time": "2024-01-31 23:59:59", + "max_entries": 1000 +} +``` +#### 参数说明: +```text +level: 日志级别(-1表示所有级别) +task_type: 任务类型(空字符串表示所有任务类型) +start_time: 开始时间(格式: 2006-01-02 15:04:05) +end_time: 结束时间(格式: 2006-01-02 15:04:05) +max_entries: 最大返回条目数(0表示使用默认值1000) +``` +### 响应示例 +```json +{ + "count": 125, + "entries": [ + { + "timestamp": "2024-01-15 10:30:45.123", + "level": "INFO", + "task_type": "main", + "caller": "logger.go:256", + "message": "系统启动完成" + }, + { + "timestamp": "2024-01-15 10:31:15.456", + "level": "ERROR", + "task_type": "backup", + "caller": "backup.go:89", + "message": "备份文件失败: 磁盘空间不足" + } + ] +} +``` +### 错误响应示例 +```json +{"error": "无效的logger句柄"} +``` + +## 获取日志文件列表--GetLogFiles +### 请求信息 +```gotemplate +dll.GetLogFiles(loggerHandle) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------| +| loggerHandle | string | 是 | 日志器句柄 | +### 响应示例 +```json +{ + "count": 8, + "files": [ + { + "level": "INFO", + "task_type": "main", + "file_name": "INFO-main-2024-01.logs", + "file_size": 1048576, + "mod_time": "2024-01-15 10:30:45" + }, + { + "level": "ERROR", + "task_type": "backup", + "file_name": "ERROR-backup-2024-01.logs", + "file_size": 51200, + "mod_time": "2024-01-15 10:31:15" + } + ] +} +``` +### 错误响应示例 +```json +{"error": "无效的logger句柄"} +``` + +## 获取版本信息--GetVersion +### 请求信息 +```gotemplate +dll.GetVersion() +``` +### 请求参数 +```text +无参数 +``` +### 响应示例 +```json +"v1" +``` + +## 关闭所有日志器--CloseAllLoggers +### 请求信息 +```gotemplate +dll.CloseAllLoggers() +``` +### 请求参数 +```text +无参数 +``` +### 响应示例 +```json +"成功关闭所有logger" +``` +### 错误响应示例 +```json +"关闭了5个logger,其中1个出错,最后错误: close file error" +``` + +## 释放C字符串内存--FreeString +### 请求信息 +```gotemplate +dll.FreeString(str) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------| +| str | string | 是 | 需要释放的字符串 | \ No newline at end of file diff --git a/planC/modules/pdd/pdd.dll b/planC/modules/pdd/pdd.dll new file mode 100644 index 0000000..532e5c9 Binary files /dev/null and b/planC/modules/pdd/pdd.dll differ diff --git a/planC/modules/pdd/pdd.go b/planC/modules/pdd/pdd.go new file mode 100644 index 0000000..224a8b8 --- /dev/null +++ b/planC/modules/pdd/pdd.go @@ -0,0 +1,223 @@ +package pdd + +import ( + "fmt" + "os" + "path/filepath" + "planA/initialization/golabl" + "syscall" + "unsafe" +) + +var ( + gPddDll *PddDLL +) + +// PddResponse 定义完整的响应结构(包含成功和失败两种情况) +type PddResponse struct { + SuccessResponse *PddSuccessResponse `json:"outer_cat_mapping_get_response,omitempty"` + ErrorResponse *PddErrorResponse `json:"error_response,omitempty"` +} +type PddSuccessResponse struct { + OuterCatMappingGetResponse PddCategoryMappingResponse `json:"outer_cat_mapping_get_response"` +} + +// PddCategoryMappingResponse 定义拼多多API响应结构(根据文档规范) +type PddCategoryMappingResponse struct { + CatID1 int64 `json:"cat_id1"` // 一级类目 ID + CatID2 int64 `json:"cat_id2"` // 二级类目 ID + CatID3 int64 `json:"cat_id3"` // 三级类目 ID + CatID4 int64 `json:"cat_id4"` // 四级类目 ID + RequestID string `json:"request_id"` // 请求 ID +} + +// PddDLL 拼多多工具DLL结构 +type PddDLL struct { + Dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} +type PddErrorResponse struct { + ErrorCode int64 `json:"error_code"` // 错误码 + ErrorMsg string `json:"error_msg"` // 错误信息 + SubCode *string `json:"sub_code"` // 子错误码 + SubMsg string `json:"sub_msg"` // 子错误信息 + RequestID string `json:"request_id"` // 请求ID +} + +// InitPddDll 初始化 pddDLL +func InitPddDll() (*PddDLL, error) { + dllPath := filepath.Join(golabl.Config.FileUrl.PddDll, "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } + gPddDll = &PddDLL{ + Dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + } + return gPddDll, nil +} + +// PddGoodsOuterCatMappingGet 类目预测 +func (m *PddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsAdd 商品新增 +func (m *PddDLL) PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsAdd") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsAdd: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsAddJsonPtr, _ := syscall.BytePtrFromString(goodsAddJson) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsAddJsonPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// PddGoodsSpecIdGet 生成商家自定义的规格 +func (m *PddDLL) PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsSpecIdGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSpecIdGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + parentSpecIdPtr, _ := syscall.BytePtrFromString(parentSpecId) + specNamePtr, _ := syscall.BytePtrFromString(specName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(parentSpecIdPtr)), + uintptr(unsafe.Pointer(specNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsCommitDetailGet 获取商品提交的商品详情 +func (m *PddDLL) PddGoodsCommitDetailGet(clientId, clientSecret, accessToken, goodsCommitId, goodsId string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsCommitDetailGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsCommitIdPtr, _ := syscall.BytePtrFromString(goodsCommitId) + goodsIdPtr, _ := syscall.BytePtrFromString(goodsId) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsCommitIdPtr)), + uintptr(unsafe.Pointer(goodsIdPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddTimeGet 获取拼多多系统时间 +func (m *PddDLL) PddTimeGet(clientId, clientSecret, accessToken string) (string, error) { + proc, err := m.Dll.FindProc("PddTimeGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsImageUpload 上传图片 +func (m *PddDLL) PddGoodsImageUpload(clientId, clientSecret, accessToken, imgBase64 string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsImageUpload") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsImageUpload: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + imgBase64Ptr, _ := syscall.BytePtrFromString(imgBase64) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(imgBase64Ptr)), + ) + result := cStr(resultPtr) + return result, nil +} diff --git a/planC/modules/pdd/pdd.md b/planC/modules/pdd/pdd.md new file mode 100644 index 0000000..2c11768 --- /dev/null +++ b/planC/modules/pdd/pdd.md @@ -0,0 +1,863 @@ +# pdd.dll 使用教程 +## 1.创建DLL工具实例 +### 加载DLL文件 +```gotemplate +// PddDLL 拼多多工具DLL结构 +type pddDLL struct { + dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} + +// <初始化pddDLL> +func InitPddDLL() (*pddDLL, error) { + dllPath := filepath.Join("dll", "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } else { + return &pddDLL{ + dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +dll, err := InitPddDLL() +``` + +### 获取C字符串 +```gotemplate +// cStr 获取C字符串 +func (m *pddDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} +``` + +## 2. 使用dll函数示例 +```gotemplate +// 类目预测 +func (m *pddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} +``` + +# 接口详情 +## 1. 类目预测--PddGoodsOuterCatMappingGet +### 请求信息 +```gotemplate +dll.PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, +outerCatId, outerCatName, outerGoodsName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| outerCatId | string | 是 | 外部平台类目ID | +| outerCatName | string | 是 | 外部平台类目名称 | +| outerGoodsName | string | 是 | 外部商品名称 | +### 响应示例 +```json +{ + "outer_cat_mapping_get_response": { + "cat_id2": 16028, + "cat_id3": 16031, + "cat_id1": 15543, + "request_id": "17666480184871649", + "cat_id4": 0 + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 2. 快递公司查看--PddLogisticsCompaniesGet +### 请求信息 +```gotemplate +dll.PddLogisticsCompaniesGet(clientId, clientSecret) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +### 响应示例 +```json +{ + "logistics_companies_get_response": { + "logistics_companies": [ + { + "available": 1, + "code": "SF", + "id": 1, + "logistics_company": "顺丰速运" + }, + { + "available": 1, + "code": "STO", + "id": 2, + "logistics_company": "申通快递" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 3. erp打单信息同步--PddErpOrderSync +### 请求信息 +```gotemplate +dll.PddErpOrderSync(clientId, clientSecret, accessToken, logisticsId, +orderSn, orderState, waybillNo) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsId | string | 是 | 物流公司ID | +| orderSn | string | 是 | 拼多多订单号 | +| orderState | string | 是 | 订单状态 | +| waybillNo | string | 是 | 运单号 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871650" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 4. 拼多多订单同步--PddOrderSynchronization +### 请求信息 +```gotemplate +dll.PddOrderSynchronization(clientId, clientSecret, accessToken, logisticsCompany, logisticsOnlineSendJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsCompany | string | 是 | 物流公司名称 | +| logisticsOnlineSendJson | string | 是 | 拼多多订单同步json字符串 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871651" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 5. 商品图片上传接口--PddGoodsImgUpload +### 请求信息 +```gotemplate +dll.PddGoodsImgUpload(clientId, clientSecret, accessToken, filePath) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +### 响应示例 +```json +{ + "goods_img_upload_response": { + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "request_id": "17666480184871652" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 6. 商品新增接口--PddGoodsAdd +### 请求信息 +```gotemplate +dll.PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| goodsAddJson | string | 是 | 商品信息JSON字符串 | +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456789, + "goods_name": "测试商品", + "goods_sn": "G202501200001", + "request_id": "17666480184871653" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 7. 联合拼多多图片上传的商品新增--SelfPddGoodsAdd +### 请求信息 +```gotemplate +dll.SelfPddGoodsAdd(clientId, clientSecret, accessToken, filePath, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +| goodsAddJson | string | 是 | 商品信息JSON字符串(不需包含image_url)| +#### 接口说明 +此接口为组合接口,内部执行以下步骤: +1.上传商品主图文件到拼多多服务器 +2.获取图片URL并自动填充到商品信息中 +3.调用商品新增接口创建商品 +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456790, + "goods_name": "测试商品", + "goods_sn": "G202501200002", + "request_id": "17666480184871654" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 8. 批量数据解密脱敏接口--PddOpenDecryptMaskBatch +### 请求信息 +```gotemplate +dll.PddOpenDecryptMaskBatch(clientId, clientSecret, accessToken, reqJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| reqJson | string | 是 | 信息JSON字符串 | +#### 信息JSON结构示例 +```json +[ + { + "data_tag": "251229-272441044622514", + "encrypted_data": "~AgAAAAPlscEH0psOJAEXpTdsLOWvDJ9bB7IEjIoqNfiDhhJR9NHOxsdZ+PEFluSSCngCikoDU+CP/sSXZJ92ic7+PdNlJNLA7g/6VUMDWF6RvjW9IeRN+lKNarsjWDQR~0~" + } +] +``` +### 响应示例 +```json +{ + "open_decrypt_mask_batch_response": { + "data_decrypt_list": [ + { + "data_tag": "str", + "data_type": 0, + "decrypted_data": "str", + "encrypted_data": "str", + "error_code": 0, + "error_msg": "str" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 生成商家自定义的规格--PddGoodsSpecIdGet +### 请求信息 +```gotemplate +dll.PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| parentSpecId | string | 是 | 拼多多标准规格ID | +| specName | string | 是 | 商家编辑的规格值,如颜色规格下设置白色属性 | +### 响应参数 +```json +{ + "goods_spec_id_get_response": { + "parent_spec_id": 0, + "spec_id": 0, + "spec_name": "str" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 修改商品SKU价格--PddGoodsSkuPriceUpdate +### 请求信息 +```gotemplate +dll.PddGoodsSkuPriceUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 价格更新请求JSON字符串 | +#### 请求JSON结构 +```json +{ + "goods_id": "必填,商品id,类型为LONG", + "ignore_edit_warn": "非必填,是否获取商品发布警告信息,默认为忽略,类型为BOOLEAN", + "market_price": "非必填,参考价(单位分),类型为LONG", + "market_price_in_yuan": "非必填,参考价(单位元),类型为STRING", + "sku_price_list": [ + { + "group_price": "非必填,拼团购买价格(单位分),类型为LONG", + "is_onsale": "非必填,sku上架状态,0-已下架,1-上架中,类型为INTEGER", + "single_price": "非必填,单独购买价格(单位分),类型为LONG", + "sku_id": "必填,sku标识,类型为LONG" + } + ], + "sync_goods_operate": "非必填,提交后上架状态,0:上架,1:保持原样,类型为INTEGER", + "two_pieces_discount": "非必填,满2件折扣,可选范围0-100,0表示取消,95表示95折,设置需先查询规则接口获取实际可填范围,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_update_sku_price_response": { + "goods_commit_id": 0, + "is_success": true + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 商品库存更新接口--PddGoodsQuantityUpdate +### 请求信息 +```gotemplate +dll.PddGoodsQuantityUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 库存更新请求JSON字符串 | +#### 请求JSON结构 request 字符串 +```json +{ + "force_update": "非必填,是否强制更新,仅update_type=1(全量更新)时有效,默认值false;force_update=false时,quantity不能小于预扣库存;force_update=true时,代表强制更新,当quantity<预扣库存时,不报错,直接将quantity清0,类型为BOOLEAN", + "goods_id": "必填,商品id,类型为LONG", + "outer_id": "非必填,sku商家编码,类型为STRING", + "quantity": "必填,库存修改值。当全量更新库存时,quantity必须为大于等于0的正整数;当增量更新库存时,quantity为整数,可小于等于0。若增量更新时传入的库存为负数,则负数与实际库存之和不能小于0。比如当前实际库存为1,传入增量更新quantity=-1,库存改为0,类型为LONG", + "sku_id": "非必填,sku_id和outer_id必填一个,类型为LONG", + "update_type": "非必填,库存更新方式,可选。1为全量更新,2为增量更新。如果不填,默认为全量更新,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_quantity_update_response": { + "is_success": false + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 获取商品信息接口 -- OutPddAuthGetCommitDetailt +### 请求信息 +```gotemplate +dll.OutPddAuthGetCommitDetailt(goodsCommitId, goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsCommitId | string | 是 | 商品提交ID | +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json + +``` + + +## 获取商品详情信息接口 -- OutPddAuthGetGoodsDetail +### 请求信息 +```gotemplate +dll.OutPddAuthGetGoodsDetail(goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "bad_fruit_claim": 0, + "buy_limit": 999999, + "carousel_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2025-06-30/4539f740-331b-4687-aa00-5c96855de6cd.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/b0e89e39-c97b-475d-9be2-f1909e30acb5.jpeg" + ], + "cat_id": 15678, + "cost_template_id": 655688447565777, + "country_id": 0, + "customer_num": 2, + "customs": "", + "detail_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/b691c104-baf8-42b2-97e2-b7258113114b.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/53e6f7ff-d15e-4e8f-8625-e293717ca1e4.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/ecff591d-32a6-42c9-ba5a-6a42829092a8.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/7034f8a0-5d88-49f8-a96f-608abb8cac80.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/e10c2b6c-d4de-4fdd-8d48-f0a334735e9a.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/c19358fb-0a4d-49ad-bcc8-b2980e938064.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/1deeb9c0-7212-432b-a309-f774db6e1adb.jpeg" + ], + "goods_desc": "书名:金属工艺学 下 第6版,作者:'邓文英,宋力宏主编',ISBN:9787040456295,出版社:高等教育出版社", + "goods_id": 770621582375, + "goods_name": "金属工艺学 下 第6版 邓文英,宋力宏主编 高等教育出版社 978", + "goods_property_list": [ + { + "punit": "", + "ref_pid": 425, + "template_pid": 401030, + "vid": 0, + "vvalue": "9787040456295" + }, + { + "punit": "", + "ref_pid": 876, + "template_pid": 401029, + "vid": 0, + "vvalue": "金属工艺学 下 第6版" + }, + { + "punit": "页", + "ref_pid": 692, + "template_pid": 401032, + "vid": 0, + "vvalue": "157" + }, + { + "punit": "元", + "ref_pid": 879, + "template_pid": 401034, + "vid": 0, + "vvalue": "24.70" + }, + { + "punit": "", + "ref_pid": 882, + "template_pid": 401037, + "vid": 0, + "vvalue": "邓文英,宋力宏主编" + }, + { + "punit": "", + "ref_pid": 880, + "template_pid": 401035, + "vid": 483761, + "vvalue": "高等教育出版社" + }, + { + "punit": "", + "ref_pid": 888, + "template_pid": 401043, + "vid": 0, + "vvalue": "平装" + } + ], + "goods_type": 1, + "image_url": "", + "invoice_status": 0, + "is_customs": 0, + "is_folt": 0, + "is_group_pre_sale": 0, + "is_pre_sale": 0, + "is_refundable": 1, + "is_sku_pre_sale": 0, + "market_price": 5948, + "order_limit": 999999, + "outer_goods_id": "9787040456295", + "oversea_type": 0, + "pre_sale_time": 0, + "privacy_delivery": 0, + "quan_guo_lian_bao": 0, + "second_hand": 1, + "shipment_limit_second": 172800, + "sku_list": [ + { + "is_onsale": 1, + "limit_quantity": 999999, + "multi_price": 1487, + "out_sku_sn": "9787040456295", + "price": 1587, + "quantity": 0, + "reserve_quantity": 0, + "sku_id": 1753931570290, + "sku_pre_sale_time": 0, + "spec": [ + { + "parent_id": 1216, + "parent_name": "尺寸", + "spec_id": 27632894279, + "spec_name": "单本 无附赠 超七天不退换" + } + ], + "thumb_url": "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "weight": 500 + } + ], + "status": 4, + "tiny_name": "金属工艺学 下 第6", + "two_pieces_discount": 96, + "video_gallery": [], + "warehouse": "", + "warm_tips": "", + "zhi_huan_bu_xiu": 0 +} +``` + +## 生成自定义规格接口 -- OutPddAuthSetSpec +### 请求信息 +```gotemplate +dll.OutPddAuthSetSpec(specTypeId, specName, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| specTypeId | int | 是 | 规格类型ID | +| specName | string | 是 | 规格名称 | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "parentSpecId": 3820, + "specName": "全新", + "specId": 1080396526 +} +``` + +## 修改价格接口 -- OutPddAuthUpdatePrice +### 请求信息 +```gotemplate +dll.OutPddAuthUpdatePrice(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 修改库存接口 -- OutPddAuthUpdateStock +### 请求信息 +```gotemplate +dll.OutPddAuthUpdateStock(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 12.释放C字符串内存--FreeCString +### 请求信息 +```gotemplate +dll.FreeCString(str) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| str | string | 是 | 需要释放的字符串 | diff --git a/planC/modules/xianYu/address.xlsx b/planC/modules/xianYu/address.xlsx new file mode 100644 index 0000000..7c91f53 Binary files /dev/null and b/planC/modules/xianYu/address.xlsx differ diff --git a/planC/modules/xianYu/config.ini b/planC/modules/xianYu/config.ini new file mode 100644 index 0000000..6755855 --- /dev/null +++ b/planC/modules/xianYu/config.ini @@ -0,0 +1,25 @@ +[app] +AppId = 1228288260261189 +AppSecret = aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k +Domain = https://open.goofish.pro +[http] +Addr = 127.0.0.1:53368 +[categoryListRequest] +Path = /api/open/product/category/list +ItemBizType: 2 +SpBizType: 24 +[batchCreatRequest] +Path = /api/open/product/batchCreate +[file] +TxtPath = modules/xianYu/productCategory.txt +ExcelPath = modules/xianYu/address.xlsx +SheetName = Result +[redis] +Password = Long6166@@ +Addr = 127.0.0.1:6379 +Db = 5 +[tokenBucket] +BucketKeyPrefix = "token_bucket_" +TokenPerSecond = 10 +BucketSize = 100 +Delay = 100 diff --git a/planC/modules/xianYu/productCategory.txt b/planC/modules/xianYu/productCategory.txt new file mode 100644 index 0000000..a0a9fbc --- /dev/null +++ b/planC/modules/xianYu/productCategory.txt @@ -0,0 +1,284 @@ +cbf4e2ec8f2013d31921b9e373cead75:电视剧 +cbf4e2ec8f2013d3267e0a01017d9f44:电影 +cbf4e2ec8f2013d36f38848189966e7d:生活 +cbf4e2ec8f2013d3ac899d2620c5df2b:成人教育音像 +cbf4e2ec8f2013d3acde29f76907b07f:动画 +cbf4e2ec8f2013d3e10cfa39bf43dc0f:儿童教育音像 +d14d229692616168b108d382c4e6ea42:废品回收 +d816d18aa66dfb3d1921b9e373cead75:励志成长 +dbaba36adf47af96b108d382c4e6ea42:不干胶标签 +e59460ef9961e2bd28d88a08a19453dc:古典吉他 +e59460ef9961e2bda7f7e02f36b0b49a:电箱吉他 +86cddebb2de0815c1921b9e373cead75:桌面文件柜 +86cddebb2de0815c267e0a01017d9f44:资料册 +86cddebb2de0815c6f38848189966e7d:镇纸 +86cddebb2de0815ca7f7e02f36b0b49a:文件袋 +86cddebb2de0815cacde29f76907b07f:文房墨汁 +86cddebb2de0815ce10cfa39bf43dc0f:文房四宝套装 +879b743300e7a58137b3d33c282f2081:古筝 +8bd8d9724880b84d28d88a08a19453dc:学习笔记 +a457d6fc43c609bdac899d2620c5df2b:单据收据 +a457d6fc43c609bdacde29f76907b07f:印台 +a9ef3505c7fe4b661921b9e373cead75:勾线笔 +a9ef3505c7fe4b66a7f7e02f36b0b49a:电子阅览器/电纸书 +ab78823bfd3c7134b108d382c4e6ea42:经济管理 +ac69f9982deabde1acde29f76907b07f:民谣吉他 +ac69f9982deabde1e10cfa39bf43dc0f:架子鼓 +b12c1c13a8dc3b2b6f38848189966e7d:POP广告纸 +b12c1c13a8dc3b2ba7f7e02f36b0b49a:修正贴 +b12c1c13a8dc3b2bac899d2620c5df2b:学生用印 +b12c1c13a8dc3b2bacde29f76907b07f:名片 +b2b61c32fc4c904428d88a08a19453dc:背胶证件照 +b3b713b29220947237b3d33c282f2081:台历 +4c49139fe1b6ae4aac899d2620c5df2b:童书育儿 +4fecb084c468ed626f38848189966e7d:黑板 +5042edcbd2cc4b94ac899d2620c5df2b:生活百科 +621bd460d751e0fc37b3d33c282f2081:订书机 +701ed8603d74ee60b108d382c4e6ea42:报纸 +722d38201b9c8cba267e0a01017d9f44:社科心理 +7912befd7e1215d11921b9e373cead75:挂历 +7dba397e41d08d4937b3d33c282f2081:拆信刀 +7eb776b01814cc6e1921b9e373cead75:教材教辅 +22e1d81dc4cf3a25a7f7e02f36b0b49a:图书 +2dfa3034d88aedcc1921b9e373cead75:期刊/杂志 +31329c43789fae0437b3d33c282f2081:戏曲综艺 +31329c43789fae04a7f7e02f36b0b49a:音乐唱片/专辑 +322a73805c38995f6f38848189966e7d:宝珠笔 +3cdbae6d47df9251a7f7e02f36b0b49a:电子资料 +22d3cfff678abab1e10cfa39bf43dc0f:握笔器 +b7fd03d456abe3011921b9e373cead75:活页替芯 +b7fd03d456abe301b108d382c4e6ea42:索引纸 +b7fd03d456abe301e10cfa39bf43dc0f:拍纸本 +c230ba4ca293f3b528d88a08a19453dc:马克笔 +c230ba4ca293f3b5a7f7e02f36b0b49a:钢笔 +c230ba4ca293f3b5ac899d2620c5df2b:铅笔 +c3c6e8d1d63c0618b108d382c4e6ea42:文学/小说 +c58d3dbcff05e404acde29f76907b07f:笔筒 +eac1d67ece5fa9b16f38848189966e7d:钢琴 +ee8603696d446e931921b9e373cead75:电钢琴 +06d80b131d7b0b616f38848189966e7d:毛笔 +0e28c0f1f1e57eb1ac899d2620c5df2b:地图 +0f75076039b85f74267e0a01017d9f44:计算器 +0f75076039b85f7428d88a08a19453dc:尺 +0f75076039b85f746f38848189966e7d:板擦 +0f75076039b85f74b108d382c4e6ea42:算盘 +11c38799bd389b3828d88a08a19453dc:漫画书籍 +ac69f9982deabde1a7f7e02f36b0b49a:上弦器 +83f9286d1ea41056ac899d2620c5df2b:其他吉他配件 +e59460ef9961e2bd1921b9e373cead75:变调夹 +83f9286d1ea4105637b3d33c282f2081:古典吉他弦 +e59460ef9961e2bdacde29f76907b07f:吉他单块效果器 +83f9286d1ea41056267e0a01017d9f44:吉他效果器配件 +83f9286d1ea41056b108d382c4e6ea42:吉他电源 +ac69f9982deabde1267e0a01017d9f44:吉他综合效果器 +e59460ef9961e2bdb108d382c4e6ea42:吉他背包琴盒 +83f9286d1ea4105628d88a08a19453dc:吉他背带 +83f9286d1ea410561921b9e373cead75:吉他连接线 +e59460ef9961e2bd6f38848189966e7d:吊架 +ac69f9982deabde1ac899d2620c5df2b:弦枕 +ac69f9982deabde11921b9e373cead75:弦柱 +e59460ef9961e2bd267e0a01017d9f44:拨片 +ac69f9982deabde128d88a08a19453dc:拾音器 +83f9286d1ea41056e10cfa39bf43dc0f:曼陀铃弦 +83f9286d1ea410566f38848189966e7d:民谣吉他弦 +ac69f9982deabde137b3d33c282f2081:清洁保护品 +83f9286d1ea41056a7f7e02f36b0b49a:滑棒指套 +e59460ef9961e2bde10cfa39bf43dc0f:电吉他弦 +e59460ef9961e2bdac899d2620c5df2b:背带钮 +83f9286d1ea41056acde29f76907b07f:脚凳 +ac69f9982deabde1b108d382c4e6ea42:调音器 +e59460ef9961e2bd37b3d33c282f2081:电吉他 +c6d5c9e68467b108ac899d2620c5df2b:哑鼓垫 +c6d5c9e68467b10837b3d33c282f2081:镲片 +c6d5c9e68467b10828d88a08a19453dc:鼓凳 +ac69f9982deabde16f38848189966e7d:鼓刷 +c6d5c9e68467b108b108d382c4e6ea42:鼓架镲架 +c6d5c9e68467b108a7f7e02f36b0b49a:鼓棒鼓锤 +1cac27c660d7b098b108d382c4e6ea42:唢呐 +1cac27c660d7b098267e0a01017d9f44:埙 +f22578f0c6a8eaa5267e0a01017d9f44:尺八 +f22578f0c6a8eaa51921b9e373cead75:巴乌 +1cac27c660d7b09828d88a08a19453dc:笙 +f22578f0c6a8eaa5acde29f76907b07f:笛子 +f22578f0c6a8eaa5e10cfa39bf43dc0f:管子 +1cac27c660d7b0981921b9e373cead75:箫 +1cac27c660d7b098a7f7e02f36b0b49a:芦笙 +1cac27c660d7b09837b3d33c282f2081:葫芦丝 +f22578f0c6a8eaa56f38848189966e7d:葫芦笙 +1cac27c660d7b098ac899d2620c5df2b:陶笛 +879b743300e7a581acde29f76907b07f:三弦 +1cac27c660d7b098e10cfa39bf43dc0f:冬不拉 +1cac27c660d7b0986f38848189966e7d:古琴 +1cac27c660d7b098acde29f76907b07f:弹布尔 +879b743300e7a581e10cfa39bf43dc0f:扬琴 +879b743300e7a5816f38848189966e7d:月琴 +879b743300e7a58128d88a08a19453dc:柳琴 +879b743300e7a5811921b9e373cead75:热瓦普 +879b743300e7a581b108d382c4e6ea42:琵琶 +879b743300e7a581ac899d2620c5df2b:秦琴 +879b743300e7a581a7f7e02f36b0b49a:箜篌 +879b743300e7a581267e0a01017d9f44:阮 +a2eba09f5b889a7c28d88a08a19453dc:中胡 +7d61e938542f6790b108d382c4e6ea42:二胡 +7d61e938542f6790267e0a01017d9f44:京二胡 +7d61e938542f6790acde29f76907b07f:京胡 +7d61e938542f679028d88a08a19453dc:低音胡 +a2eba09f5b889a7c37b3d33c282f2081:四胡 +a2eba09f5b889a7cb108d382c4e6ea42:坠琴 +7d61e938542f6790a7f7e02f36b0b49a:板胡 +a2eba09f5b889a7ca7f7e02f36b0b49a:椰胡 +7d61e938542f679037b3d33c282f2081:艾捷克 +7d61e938542f67901921b9e373cead75:革胡 +7d61e938542f67906f38848189966e7d:马头琴 +7d61e938542f6790e10cfa39bf43dc0f:马骨胡 +7d61e938542f6790ac899d2620c5df2b:高胡 +882b39ff0db2dd0037b3d33c282f2081:军镲 +00a32e7ff35aaf9e267e0a01017d9f44:大钹 +00a32e7ff35aaf9ee10cfa39bf43dc0f:大铙 +00a32e7ff35aaf9eacde29f76907b07f:大顶钹 +00a32e7ff35aaf9e1921b9e373cead75:川钹 +00a32e7ff35aaf9e6f38848189966e7d:广钹 +882b39ff0db2dd00a7f7e02f36b0b49a:快板 +882b39ff0db2dd0028d88a08a19453dc:拍板 +00a32e7ff35aaf9eb108d382c4e6ea42:梆子 +882b39ff0db2dd001921b9e373cead75:水镲 +882b39ff0db2dd00b108d382c4e6ea42:碰钟 +882b39ff0db2dd00acde29f76907b07f:秧歌镲 +882b39ff0db2dd00e10cfa39bf43dc0f:腰鼓镲 +882b39ff0db2dd00ac899d2620c5df2b:萨巴依 +882b39ff0db2dd00267e0a01017d9f44:铜书板 +00a32e7ff35aaf9eac899d2620c5df2b:镲锅 +0ea61a801ba323c1267e0a01017d9f44:堂鼓 +00a32e7ff35aaf9e28d88a08a19453dc:战鼓 +0ea61a801ba323c11921b9e373cead75:排鼓 +0ea61a801ba323c1b108d382c4e6ea42:板鼓 +00a32e7ff35aaf9e37b3d33c282f2081:秧歌鼓 +0ea61a801ba323c1e10cfa39bf43dc0f:细腰鼓 +00a32e7ff35aaf9ea7f7e02f36b0b49a:腰鼓 +0ea61a801ba323c1ac899d2620c5df2b:花盆鼓 +0ea61a801ba323c16f38848189966e7d:象脚鼓 +0ea61a801ba323c1acde29f76907b07f:铜鼓 +a7133eb411b587cf1921b9e373cead75:空灵鼓/无忧鼓 +0ea61a801ba323c1a7f7e02f36b0b49a:云锣 +a2eba09f5b889a7c267e0a01017d9f44:京锣 +a2eba09f5b889a7cac899d2620c5df2b:低音锣 +a2eba09f5b889a7cacde29f76907b07f:开道锣 +a2eba09f5b889a7ce10cfa39bf43dc0f:手锣 +0ea61a801ba323c137b3d33c282f2081:武锣 +0ea61a801ba323c128d88a08a19453dc:舟山锣 +a2eba09f5b889a7c6f38848189966e7d:苏锣 +a2eba09f5b889a7c1921b9e373cead75:虎音锣 +33a0daa5d89d68fa1921b9e373cead75:宣纸 +b12c1c13a8dc3b2b1921b9e373cead75:吊牌 +b12c1c13a8dc3b2be10cfa39bf43dc0f:自封袋 +b12c1c13a8dc3b2b267e0a01017d9f44:贺卡明信片 +22d3cfff678abab11921b9e373cead75:书皮 +b12c1c13a8dc3b2b37b3d33c282f2081:修正带 +b12c1c13a8dc3b2b28d88a08a19453dc:修正液 +22d3cfff678abab1a7f7e02f36b0b49a:削笔器 +22d3cfff678abab128d88a08a19453dc:可爱印泥 +b12c1c13a8dc3b2bb108d382c4e6ea42:学生书包 +22d3cfff678abab1acde29f76907b07f:文具套装 +22d3cfff678abab1267e0a01017d9f44:文具盒 +22d3cfff678abab16f38848189966e7d:橡皮 +22d3cfff678abab1b108d382c4e6ea42:练字帖 +22d3cfff678abab1ac899d2620c5df2b:视力保护器 +dbaba36adf47af9637b3d33c282f2081:笔袋 +54e552aa1c9b2cbcacde29f76907b07f:彩泥橡皮泥 +bf164bd2e8dd8cebb108d382c4e6ea42:便条照片夹 +bf164bd2e8dd8ceb28d88a08a19453dc:便签盒座 +bf164bd2e8dd8cebe10cfa39bf43dc0f:卡套证件套 +bf164bd2e8dd8ceb6f38848189966e7d:名片册 +86cddebb2de0815c37b3d33c282f2081:名片盒 +bf164bd2e8dd8cebacde29f76907b07f:快劳夹 +86cddebb2de0815c28d88a08a19453dc:文件夹 +86cddebb2de0815cb108d382c4e6ea42:文件架 +bf164bd2e8dd8ceb1921b9e373cead75:档案盒 +bf164bd2e8dd8cebac899d2620c5df2b:档案袋 +86cddebb2de0815cac899d2620c5df2b:相册 +1ad9ac4511bbb8646f38848189966e7d:笔插 +bf164bd2e8dd8ceba7f7e02f36b0b49a:笔架 +bf164bd2e8dd8ceb267e0a01017d9f44:风琴包 +d665d5e1347fa192a7f7e02f36b0b49a:地球仪 +bf164bd2e8dd8ceb37b3d33c282f2081:展板 +d665d5e1347fa192267e0a01017d9f44:教学仪器器材 +d665d5e1347fa1921921b9e373cead75:教鞭 +bb9bba251ee78e59267e0a01017d9f44:旗帜 +d665d5e1347fa19237b3d33c282f2081:提示牌 +d665d5e1347fa192b108d382c4e6ea42:激光笔 +0f75076039b85f74acde29f76907b07f:白板 +0f75076039b85f74e10cfa39bf43dc0f:白板笔 +d665d5e1347fa19228d88a08a19453dc:粉笔 +d665d5e1347fa192acde29f76907b07f:绿板 +d665d5e1347fa1926f38848189966e7d:荧光板 +d665d5e1347fa192ac899d2620c5df2b:计划表 +d665d5e1347fa192e10cfa39bf43dc0f:软木板 +a457d6fc43c609bda7f7e02f36b0b49a:中性笔 +c230ba4ca293f3b5e10cfa39bf43dc0f:圆珠笔 +c230ba4ca293f3b51921b9e373cead75:铅芯 +f9910185f1984f2937b3d33c282f2081:正姿笔 +c230ba4ca293f3b5acde29f76907b07f:油漆笔 +c230ba4ca293f3b5b108d382c4e6ea42:泡泡笔 +c230ba4ca293f3b537b3d33c282f2081:墨水墨囊 +c230ba4ca293f3b5267e0a01017d9f44:荧光笔 +f4a071d4dba28eccac899d2620c5df2b:记号笔 +c230ba4ca293f3b56f38848189966e7d:针管笔 +dfdbd3409fadcd3f6f38848189966e7d:其他笔 +58e84885c426409e267e0a01017d9f44:书签 +b7fd03d456abe30128d88a08a19453dc:便签 +e9fa1ad466b79d97b108d382c4e6ea42:信封 +af2cf5b1faa3537a1921b9e373cead75:信纸 +b7fd03d456abe30137b3d33c282f2081:包装纸 +e9fa1ad466b79d97a7f7e02f36b0b49a:纪念册 +b7fd03d456abe301ac899d2620c5df2b:复写纸 +b7fd03d456abe301267e0a01017d9f44:奖状证书 +e9fa1ad466b79d971921b9e373cead75:手工纸 +e9fa1ad466b79d9728d88a08a19453dc:草稿纸 +b7fd03d456abe3016f38848189966e7d:日记本 +e9fa1ad466b79d97ac899d2620c5df2b:硬面抄 +b7fd03d456abe301a7f7e02f36b0b49a:记事本 +b7fd03d456abe301acde29f76907b07f:课业本 +e9fa1ad466b79d9737b3d33c282f2081:通讯录 +dbaba36adf47af966f38848189966e7d:磁性贴 +6c0543ec11db7e61267e0a01017d9f44:贴纸/标签 +0f75076039b85f741921b9e373cead75:圆规 +0f75076039b85f74ac899d2620c5df2b:显微镜 +1c75d8021bacf61e267e0a01017d9f44:放大镜 +a9ef3505c7fe4b66b108d382c4e6ea42:丙烯颜料 +823f8d7bd96780d0ac899d2620c5df2b:书法用纸 +a9ef3505c7fe4b66ac899d2620c5df2b:儿童填色本 +a9ef3505c7fe4b66267e0a01017d9f44:国画颜料 +823f8d7bd96780d037b3d33c282f2081:描图硫酸纸 +5fd3299edc3ff44a37b3d33c282f2081:毛边纸 +823f8d7bd96780d01921b9e373cead75:水彩笔 +823f8d7bd96780d0267e0a01017d9f44:水彩颜料 +823f8d7bd96780d0acde29f76907b07f:水粉水彩油画笔 +823f8d7bd96780d0e10cfa39bf43dc0f:水粉颜料 +0f75076039b85f7437b3d33c282f2081:油画棒 +0f75076039b85f74a7f7e02f36b0b49a:油画颜料 +a9ef3505c7fe4b66acde29f76907b07f:画板画架 +823f8d7bd96780d0b108d382c4e6ea42:石膏像 +823f8d7bd96780d06f38848189966e7d:素描本 +a9ef3505c7fe4b66e10cfa39bf43dc0f:绘图纸 +823f8d7bd96780d028d88a08a19453dc:色卡 +a9ef3505c7fe4b666f38848189966e7d:蜡笔 +823f8d7bd96780d0a7f7e02f36b0b49a:铅画纸 +7dba397e41d08d49a7f7e02f36b0b49a:裁剪刀片 +7dba397e41d08d49b108d382c4e6ea42:雕刻垫板 +7dba397e41d08d49ac899d2620c5df2b:切纸刀 +7dba397e41d08d4928d88a08a19453dc:美工刀 +356e5d8126d3aefaa7f7e02f36b0b49a:裁剪剪刀 +e9fa1ad466b79d97267e0a01017d9f44:回形针 +621bd460d751e0fca7f7e02f36b0b49a:回形针盒 +621bd460d751e0fcb108d382c4e6ea42:图钉工字钉 +e9fa1ad466b79d97e10cfa39bf43dc0f:大头针 +e9fa1ad466b79d97acde29f76907b07f:打孔机 +621bd460d751e0fc28d88a08a19453dc:票夹长尾夹 +e9fa1ad466b79d976f38848189966e7d:订书钉 +a457d6fc43c609bd1921b9e373cead75:凭证 +a457d6fc43c609bde10cfa39bf43dc0f:印油印泥 +a457d6fc43c609bd28d88a08a19453dc:报表 +a457d6fc43c609bd267e0a01017d9f44:湿手器 +a457d6fc43c609bdb108d382c4e6ea42:财务证明用品 +a457d6fc43c609bd6f38848189966e7d:账本账册 +740736cf215b7509a7f7e02f36b0b49a:电子壁纸 \ No newline at end of file diff --git a/planC/modules/xianYu/xianYu.go b/planC/modules/xianYu/xianYu.go new file mode 100644 index 0000000..f82de2b --- /dev/null +++ b/planC/modules/xianYu/xianYu.go @@ -0,0 +1,94 @@ +package xianYu + +import ( + "fmt" + "os" + "path/filepath" + "planA/initialization/golabl" + "syscall" + "unsafe" +) + +var ( + gXianYuDll *XianYuDLL +) + +// XianYuDLL 闲鱼工具DLL结构 +type XianYuDLL struct { + Dll *syscall.DLL + freeCString *syscall.Proc // 释放C字符串 +} + +// InitXianYuDll 初始化 XianYuDLL +func InitXianYuDll() (*XianYuDLL, error) { + if gXianYuDll != nil { + return gXianYuDll, nil + } + dllPath := filepath.Join(golabl.Config.FileUrl.XianYuDll, "xy.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("XianYu DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载XianYu DLL 失败: %s", err) + } + gXianYuDll = &XianYuDLL{ + Dll: dll, + freeCString: dll.MustFindProc("FreeCString"), + } + return gXianYuDll, nil +} + +// XianYuGoodsAdd 商品新增 +func (m *XianYuDLL) XianYuGoodsAdd(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsCreat") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsCreat: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// XianYuLaunchGoods 商品上架 +func (m *XianYuDLL) XianYuLaunchGoods(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsPublish") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsPublish: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} diff --git a/planC/modules/xianYu/xy.dll b/planC/modules/xianYu/xy.dll new file mode 100644 index 0000000..af6630d Binary files /dev/null and b/planC/modules/xianYu/xy.dll differ diff --git a/planC/modules/xianYu/咸鱼发布dll.md b/planC/modules/xianYu/咸鱼发布dll.md new file mode 100644 index 0000000..000ac8c --- /dev/null +++ b/planC/modules/xianYu/咸鱼发布dll.md @@ -0,0 +1,239 @@ +##### FreeCString(str *C.char) + +接收其他函数返回值之后,释放内存,参考示例 + +##### 内存释放示例 + +```go +func example () { + // ...其他逻辑 + var res = StartServer (configFile *C.char) + FreeCString(res) //释放内存 +} +``` + + + +##### StartServer (configFile *C.char) + +启动http服务器,参数配置文件路径,不提供默认使用工程根目录config.ini + +返回C字符串启动消息,接收后使用FreeCString进行内存释放 + + + +##### StopServer + +停止HTTP服务器 + +返回C字符串停止消息,接收后使用FreeCString进行内存释放 + + + +##### GetServerStatus + +获取服务器当前状态 + +返回C字符串指针消息,running/stopped,接收后使用FreeCString进行内存释放 + + + +##### GetServerAddress + +获取服务器监听地址 + +返回C字符串指针服务器地址消息,未运行返回空串,接收后使用FreeCString进行内存释放 + + + +##### ReloadConfig(configFile *C.char) + +重新加载配置文件,参数配置文件路径,不提供默认使用根目录config.ini + +返回C字符串加载结果消息,接收后使用FreeCString进行内存释放 + + + + + +### 以下都需要传递appid和appSecret ### + +##### ExecuteGoodsCreat(bodyJson *C.char, configFile *C.char) + +*管道通信直接调用此函数* + +执行商品创建操作,参数商品信息,参考示例 + +返回C字符串指针创建商品结果信息,接收后使用FreeCString进行内存释放 + + + +##### 商品信息参考示例 + +```json +{ + "appId": 1228288260261189, + "appSecret": "aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k", + "token": "", + "apiShopId": 0, + "typePlatform": 4, + "shopId": 0, + "shopToken": "", + "shopName": "", + "province": 210000, + "city": 210100, + "district": 210101, + "typeClass": "", + "typeGoods": "", + "catIds": "d14d229692616168b108d382c4e6ea42", + "shop": [ + { + "userName": "xy938400231518", + "province": 210000, + "city": 210100, + "district": 210101, + "title": "牧羊少年奇幻之旅", + "content": "牧羊少年奇幻之旅", + "mainImgs": ["https://img.cdn1.vip/i/68cf5cb4e5840_1758420148.webp"], + "contentImgs": [] + } + ], + "stuffStatus": 90, + "bookData": [ + { + "ISBN": "9787530217054", + "Title": "牧羊少年奇幻之旅", + "Author": "保罗·柯艾略", + "Publisher": "北京十月文艺出版", + "itemBizType": 2, + "spBizType": 24, + "prices": [199999, 299999], + "stock": 100, + "catIds": "22e1d81dc4cf3a25a7f7e02f36b0b49a" + } + ], + "itemKey": "itemAAAAA1111" +} +``` + + + +##### ExecuteGoodsPublish(bodyJson *C.char, configFile *C.char) + +*管道通信直接调用此函数* + +执行商品上架操作,参数上架信息,参考示例 + +返回C字符串指针行商品上架结果信息,接收后使用FreeCString进行内存释放 + +##### 上架信息参考示例 + +```json +{ + "product_id": 1250927879325125, + "user_name": ["xy938400231518"], + "specify_publish_time": "", + "notify_url": "" +} +``` + + + +#### 追加下架,改价,擦亮 #### + +##### ExecuteGoodsDownShelf(bodyJson *C.char, configFile *C.char) ###### + +*管道通信直接调用此函数* + +执行商品下架操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品下架结果信息,接收后使用FreeCString进行内存释放 + +##### 下架信息参考示例 ##### + +```json +{ + "product_id": 1250927879325125 +} +``` + + + +##### ExecuteGoodsFlash(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品擦亮操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品擦亮结果信息,接收后使用FreeCString进行内存释放 + +##### 擦亮信息参考示例 ##### + +```json +{ + "product_id": 1250927879325125 +} +``` + + + +##### ExecuteGoodsEditPrice(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品改价操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 改价信息参考示例(单位:分) ##### + +```json +{ + "product_id": 1250927879325125, + "price": 550000, + "originalPrice": 770000 +} +``` + + + +##### ExecuteGoodsEditStock(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品改库存操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 改库存信息参考示例(单位:分) ##### + +```json +{ + "product_id": 1250927879325125, + "stock": 10 +} +``` + + + +##### ExecuteSelectGoodsListPrice(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +查询店铺列表操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 查询参考示例(单位:分) ##### + +```json +{ + //online_time 字段可传空 + "online_time": [ + 1690300800, + 1690366883 + ], + "product_status": 22 +} +``` + diff --git a/planD/config.yaml b/planD/config.yaml new file mode 100644 index 0000000..7f23f85 --- /dev/null +++ b/planD/config.yaml @@ -0,0 +1,102 @@ +server: + port: "8080" #服务器端口 + filter: 1 #是否开启违禁词过滤器 0=关闭 1=开启 + replace_mark: "0" #标题违规词是否替换* 0 不替换 1 替换(替换会继续发布,不替换则不发布) + redis_exp: 192 #redis过期时间 192小时(8天) + read_db: "mysql" #读数据库 mysql sqlite + err_pause_time: 3000 #错误暂停时间(毫秒) + sign_key: "jRQdCh52Z55Kzh1hADaA2ZtdTKetj2PXk60Tz5Yc0iz9aD8Wafbk7CwAZ8cz69A9zb9caZ3k9dnR3Ys06J5nYFPrZ0xE9p6TY8DCD538ryiRjW81YTPmk41tCEnXizPh" #签名密钥 + data_day: 2 #数据保存时间(天) + is_c: true #是否启动 C程序 +speed: #限速器 + pdd_speed: 18 #拼多多 每秒多少个任务 + xianyu_speed: 5 #闲鱼 每秒多少个任务 + watermark: 15 #打水印速率的个数 +minio: #minio 图片空间 + url: "103.236.68.64:19000" #minio地址 + access_key_id: "minio" #minio keyId + secret_access_key: "bhkXyaD2WdAF7C6z" #minio key + bucket_name: "my-pics" #存储桶 + target_dir: "test/2025" #目标目录 + use_ssl: false #是否使用 SSL +alive: + fluent: 50 #存活状态-流畅时间(毫秒) + slow: 200 #存活状态-缓慢时间(毫秒) +pool_config: + size: 500 #协程数量 + with_expiry_duration: 10 #过期时间 + with_pre_alloc: true #预分配 + with_max_blocking_tasks: 2000 #阻塞任务数 + with_nonblocking : true #非阻塞 +mysql_config: + db_name: "task_user" #数据库名称 + user: "task_user" #数据库用户名 + password: "BCdmsRshrHHKdmJT" #数据库密码 + host: "36.212.12.247" #数据库地址 + port: 3306 #数据库端口 + loglevel: "silent" #数据库日志级别 info=开发环境:输出所有日志(SQL、执行时间等) warn=预发布:输出警告+错误 error=生产环境:只输出错误日志 silent=生产环境:关闭所有日志 + max_retry_times: 3 #数据库重试次数 + base_retry_interval: 100 #数据库重试间隔(毫秒) + max_retry_interval: 3 #数据库最大重试间隔(秒) + max_open_conns: 100 #数据库最大打开连接数 + max_idle_conns: 20 #数据库最大空闲连接数 + conn_max_idle_time: 30 #数据库连接最大空闲时间(分钟) + conn_max_lifetime: 1 #数据库连接最大生命周期(小数) +redis_config: + - db_name: "任务池" + db: 0 + addr: "127.0.0.1:6379" + password: "123456" + - db_name: "书品库" + db: 7 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "店铺信息" + db: 2 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "出版社信息列表" + db: 3 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "省市区列表" + db: 4 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有图片的 isbn" + db: 5 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有书籍的 isbn" + db: 6 + addr: "36.212.12.247:6379" + password: "long6166@@" +pdd_config: + client_id: "203c5a7ba8bd4b8488d5e26f93052642" #拼多多clientId + client_secret: "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" #拼多多clientSecret +kfz_config: + app_id: 576 #孔夫子appid + app_secret: "256e10220c5b307f5172b1a49c11467a6cfa8038bbe2a7feccc42231852324f8" #孔夫子appsecret +http_url: + task_url: "http://127.0.0.1:8080" #A 程序接口地址 +file_url: + xian_yu_dll: "D:\\source\\planA\\planB\\modules\\xianYu" #闲鱼 DLL库路径 + pdd_dll: "D:\\source\\planA\\planD\\modules\\pdd" #拼多多 DLL库路径 + kfz_dll: "D:\\source\\planA\\planB\\modules\\kfz" #孔夫子 DLL库路径 + log_dll: "D:\\source\\planA\\planB\\modules\\logs" #日志 DLL库路径 + image_dll: "D:\\source\\planA\\planB\\modules\\image" #水印 DLL库路径 + b_file_name: "D:\\source\\planA\\planB\\planB.exe" #B 程序文件路径 + c_file_name: "D:\\source\\planA\\planC\\planC.exe" #C 程序文件路径 + create_task_url: "https://api.buzhiyushu.cn/zhishu/baseInfo/addNewTask" #新增任务接口 + create_task_notice_url: "http://36.212.12.92:8055/task" #核价软件提交数据通知接口 + create_operation_task_notice_url: "http://36.212.12.92:8055/taskV2" #操作商品任务核价软件提交数据通知接口 + banned_word_substitution_url : "http://36.212.16.27:13001/task/getFilterSetNew" #违禁词替换接口 + pdd_token_url: "https://api.buzhiyushu.cn/huidiao/pdd/getToken" #获取系统规定拼多多 token + deduction_url: "https://api.buzhiyushu.cn/zhishu/userRecharge/apiBalancePayment" #扣费接口 + pdd_get_goods_url: "http://pdd.buzhiyushu.cn/api/pdd/auth/getShopGoodsList" #查询拼多多商品接口 + pdd_get_goods_detail_url: "http://192.168.101.127:8085/api/pdd/auth/newGetShopGoodsDetailList" #查询拼多多商品详情列表接口 + pdd_add_goods_url: "http://192.168.101.127:18099/task/putShopGoods" #添加拼多多商品接口 + pdd_get_sku_id: "http://192.168.101.127:18099/shopGoods/getShopGoods" #批量获取 skuId接口 + xianyu_add_goods_url: "http://192.168.101.127:18099/task/putShopGoods" #添加闲鱼商品接口 + del_task_url: "http://119.45.237.193:14008/shopGoods/delShopGoods" #删除任务通知接口 + backup_url: "C:\\file\\backup" #备份文件路径 \ No newline at end of file diff --git a/planD/initialization/config/config.go b/planD/initialization/config/config.go new file mode 100644 index 0000000..7aaabc9 --- /dev/null +++ b/planD/initialization/config/config.go @@ -0,0 +1,46 @@ +package config + +import ( + "encoding/json" + "fmt" + planBConfig "planA/planB/modules/config" + "planA/planD/initialization/golabl" + "planA/tool" + planAtype "planA/type" +) + +// GetConfigSetToG 获取配置文件并保存到全局变量中 +// @return error 错误信息 +func GetConfigSetToG() error { + // 检查全局 CTX 是否失效 以防止重复初始化和 ctx 失效 导致程序崩溃 + checkContextErr := tool.CheckContext(golabl.Ctx) + if checkContextErr != nil { + // 返回 且 返回错误 + return checkContextErr + } + + //读取配置文件 + var config planAtype.Config + + // 加载 config.dll + dll, initConfigDLLErr := planBConfig.InitConfigDLL() + if initConfigDLLErr != nil { + return initConfigDLLErr + } + + // 读取配置文件 + configJson, ReadConfigFileErr := dll.ReadConfigFile("", "config.yaml") + if ReadConfigFileErr != nil { + return fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + // 转换配置文件到 JSON + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + + // 保存到全局变量 + golabl.Config = config + // 返回 + return nil +} diff --git a/planD/initialization/dll/dll.go b/planD/initialization/dll/dll.go new file mode 100644 index 0000000..485ffb6 --- /dev/null +++ b/planD/initialization/dll/dll.go @@ -0,0 +1,22 @@ +package dll + +import ( + "planA/planD/initialization/dll/kfz" + "planA/planD/initialization/dll/pdd" +) + +// GetDllSetToG 获取DLL +func GetDllSetToG() error { + // 初始化 PddDll + getPddDllSetToGErr := pdd.GetPddDllSetToG() + if getPddDllSetToGErr != nil { + return getPddDllSetToGErr + } + + // 初始化 孔夫子DLL + getKfzDllSetToGErr := kfz.GetKfzDllSetToG() + if getKfzDllSetToGErr != nil { + return getKfzDllSetToGErr + } + return nil +} diff --git a/planD/initialization/dll/kfz/kfz.go b/planD/initialization/dll/kfz/kfz.go new file mode 100644 index 0000000..9a09b65 --- /dev/null +++ b/planD/initialization/dll/kfz/kfz.go @@ -0,0 +1,17 @@ +package kfz + +import ( + "planA/planD/initialization/golabl" + "planA/planD/modules/kfz" +) + +// GetKfzDllSetToG 获取孔夫子DLL +func GetKfzDllSetToG() error { + // 初始化 KfzDll + kfzDll, err := kfz.InitKfzDll(golabl.Config.FileUrl.KfzDll) + if err != nil { + return err + } + golabl.KfzDll = kfzDll + return nil +} diff --git a/planD/initialization/dll/pdd/pdd.go b/planD/initialization/dll/pdd/pdd.go new file mode 100644 index 0000000..74c29cd --- /dev/null +++ b/planD/initialization/dll/pdd/pdd.go @@ -0,0 +1,17 @@ +package pdd + +import ( + "planA/planD/initialization/golabl" + "planA/planD/modules/pdd" +) + +// GetPddDllSetToG 获取拼多多DLL +func GetPddDllSetToG() error { + // 初始化 PddDll + pddDll, err := pdd.InitPddDll(golabl.Config.FileUrl.PddDll) + if err != nil { + return err + } + golabl.PddDll = pddDll + return nil +} diff --git a/planD/initialization/golabl/golabl.go b/planD/initialization/golabl/golabl.go new file mode 100644 index 0000000..8b679b8 --- /dev/null +++ b/planD/initialization/golabl/golabl.go @@ -0,0 +1,23 @@ +package golabl + +import ( + "context" + planBType "planA/planB/type" + "planA/planD/modules/kfz" + "planA/planD/modules/pdd" + + "gorm.io/gorm" + + planAType "planA/type" +) + +var ( + Ctx context.Context // 全局上下文 + Config planAType.Config // 全局配置 + MysqlDb *gorm.DB // 全局 mysql + TaskId string // 全局任务 ID + PddDll *pdd.PddDLL // 全局拼多多 DLL + KfzDll *kfz.KfzDLL // 全局孔夫子 DLL + + Redis planBType.Redis // 全局 Redis +) diff --git a/planD/initialization/init.go b/planD/initialization/init.go new file mode 100644 index 0000000..e62d0dc --- /dev/null +++ b/planD/initialization/init.go @@ -0,0 +1,39 @@ +package initialization + +import ( + "context" + "fmt" + "planA/planD/initialization/config" + "planA/planD/initialization/dll" + "planA/planD/initialization/golabl" + "planA/planD/initialization/mysql" +) + +func Init(taskId string) error { + + //初始化上下文 + if golabl.Ctx == nil { + golabl.Ctx = context.Background() + } + + // 初始化配置 + if configErr := config.GetConfigSetToG(); configErr != nil { + // 配置初始化失败 + return configErr + } + + // 初始化 任务id + golabl.TaskId = taskId + + // 初始化 mysql + if err := mysql.LikeMysqlSetToG(); err != nil { + // 初始化失败 + return err + } + + // 初始化 DLL + if dllErr := dll.GetDllSetToG(); dllErr != nil { + return fmt.Errorf("初始化DLL失败: %v", dllErr) + } + return nil +} diff --git a/planD/initialization/mysql/mysql.go b/planD/initialization/mysql/mysql.go new file mode 100644 index 0000000..a60ea50 --- /dev/null +++ b/planD/initialization/mysql/mysql.go @@ -0,0 +1,73 @@ +package mysql + +import ( + "fmt" + "planA/planD/initialization/golabl" + "time" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// LikeMysqlSetToG 链接mysql并保留到全局变量中 +func LikeMysqlSetToG() error { + + // 1. 获取mysql配置 + mysqlConfig := golabl.Config.MysqlConfig + + // 2. 配置 DSN + dsn := fmt.Sprintf( + "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + mysqlConfig.User, + mysqlConfig.Password, + mysqlConfig.Host, + mysqlConfig.Port, + mysqlConfig.DBName, + ) + + // 3. 配置 GORM 连接选项 + + logLevel := logger.Silent + switch mysqlConfig.Loglevel { + case "info": + logLevel = logger.Info + case "warn": + logLevel = logger.Warn + case "error": + logLevel = logger.Error + case "silent": + logLevel = logger.Silent + } + + gormConfig := &gorm.Config{ + Logger: logger.Default.LogMode(logLevel), //日志级别 + DisableForeignKeyConstraintWhenMigrating: true, //不创建外键约束 + } + + // 4. 连接数据库 + db, openErr := gorm.Open(mysql.Open(dsn), gormConfig) + if openErr != nil { + return openErr + } + + // 5. 获取底层 sql.DB,配置连接池 + sqlDB, dbErr := db.DB() + if dbErr != nil { + return dbErr + } + // 连接池优化 + 保活配置 + sqlDB.SetMaxOpenConns(mysqlConfig.MaxOpenConns) + sqlDB.SetMaxIdleConns(mysqlConfig.MaxIdleConns) + sqlDB.SetConnMaxIdleTime(mysqlConfig.ConnMaxIdleTime * time.Minute) + sqlDB.SetConnMaxLifetime(mysqlConfig.ConnMaxLifetime * time.Hour) + + // 5. 验证连接 + if dbPingErr := sqlDB.Ping(); dbPingErr != nil { + return dbPingErr + } + + // 7. 保存db实例 + golabl.MysqlDb = db + return nil +} diff --git a/planD/logic/logic.go b/planD/logic/logic.go new file mode 100644 index 0000000..4e0ba92 --- /dev/null +++ b/planD/logic/logic.go @@ -0,0 +1,500 @@ +package logic + +import ( + "encoding/json" + "errors" + "fmt" + planBTypePinduoduo "planA/planB/type/pinduoduo" + "planA/planD/initialization/golabl" + "planA/planD/service" + "planA/planD/tool" + planDTypeKfz "planA/planD/type/kfz" + planDTypePinduoduo "planA/planD/type/pinduoduo" + planAType "planA/type" + "planA/type/mysql" + "strconv" + "strings" + + "gorm.io/gorm" +) + +func Logic() error { + task, err := service.GetDelTask() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("任务不存在") + } + return fmt.Errorf("查询任务失败: %w", err) + } + + if *task.ShopType == "1" { + if err := PddLogic(task); err != nil { + return err + } + } else if *task.ShopType == "2" { + if err := KongFiZLogic(task); err != nil { + return err + } + } else if *task.ShopType == "5" { + if err := XianYuLogic(); err != nil { + return err + } + } else { + return errors.New("任务类型错误") + } + + //重新查询数据库,判断任务是否完成 + task, err = service.GetDelTask() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("任务不存在") + } + return fmt.Errorf("查询任务失败: %w", err) + } + if *task.TaskCountOver >= *task.TaskCount { + return service.UpdateTaskStatus(3) + } + return nil +} + +func PddLogic(task mysql.DelTask) error { + switch *task.TaskType { + case 1: + if err := PddRegularTask(task); err != nil { + return err + } + case 2: + if err := PddCountTask(task); err != nil { + return err + } + case 3: + if err := PddTimeTask(task); err != nil { + return err + } + default: + return errors.New("任务类型错误") + } + return nil +} + +func KongFiZLogic(task mysql.DelTask) error { + switch *task.TaskType { + case 1: + if err := KfzRegularTask(task); err != nil { + return err + } + default: + return errors.New("任务类型错误") + } + return nil +} + +func XianYuLogic() error { + //闲鱼没有删除 + if err := service.UpdateTaskStatus(3); err != nil { + return err + } + fmt.Println("任务完成!") + return nil +} + +// 通用通知函数 +func notifyDeletedGoods(shopId string, goodsIds []int64) error { + if len(goodsIds) == 0 { + return nil + } + goodsIdsJSON, err := json.Marshal(goodsIds) + if err != nil { + return err + } + _, err = tool.SubmitFormData(golabl.Config.FileUrl.DelTaskUrl, map[string]string{ + "shopId": shopId, + "data": string(goodsIdsJSON), + }) + return err +} + +// 处理删除成功后的逻辑 +func handleDeleteSuccess(task mysql.DelTask, goodsId int64, goodsName, outerId, token string, deleteGoodsId *[]int64) error { + // 写入redis + if err := service.InsertDelTaskDetail(task.ID, *task.TaskID, token, goodsName, outerId, goodsId); err != nil { + return err + } + + // 收集商品ID + *deleteGoodsId = append(*deleteGoodsId, goodsId) + + // 每达到1000条,立即通知一次 + if len(*deleteGoodsId) >= 1000 { + if err := notifyDeletedGoods(*task.ShopID, *deleteGoodsId); err != nil { + return err + } + *deleteGoodsId = []int64{} // 清空 + } + + fmt.Printf("商品id: %v 删除成功\n", goodsId) + return nil +} + +// 处理删除上限错误 +func handleLimitError(task mysql.DelTask, deleteGoodsId []int64) error { + if err := notifyDeletedGoods(*task.ShopID, deleteGoodsId); err != nil { + return err + } + if err := service.UpdateTaskStatus(2); err != nil { + return err + } + return fmt.Errorf("----您当日所删除的商品已达上限或无法获取需要删除的数据----\n") +} + +// 更新任务进度和完成状态 +func updateTaskProgress() (bool, error) { + if err := service.UpdateTaskCountOver(); err != nil { + return false, err + } + + over, err := service.GetTaskCountOver() + if err != nil { + return false, err + } + + if over == 0 { + if err := service.UpdateTaskStatus(3); err != nil { + return false, err + } + fmt.Println("任务完成!") + return true, nil + } + return false, nil +} + +///////////////////////////////////////////////////////////拼多多///////////////////////////////////////////////////////////////////////////////// + +func PddRegularTask(task mysql.DelTask) error { + delTask, err := service.GetMax5000WaitDelTask() + if err != nil { + return err + } + + var deleteGoodsId []int64 + + if len(delTask) == 0 { + if err := handleLimitError(task, deleteGoodsId); err != nil { + return err + } + } + + for _, v := range delTask { + status := 1 + errMsg := "执行成功" + deleteErr := PddDeleteGoodsTask(*v.GoodsID, *v.Token) + + if deleteErr != nil && strings.Contains(deleteErr.Error(), "您当日所删除的商品已达上限") { + if err := handleLimitError(task, deleteGoodsId); err != nil { + return err + } + status = 0 + errMsg = deleteErr.Error() + fmt.Printf("商品id: %v Err %v\n", v.GoodsID, deleteErr.Error()) + } else if deleteErr != nil { + status = 2 + errMsg = deleteErr.Error() + fmt.Printf("商品id: %v Err %v\n", v.GoodsID, deleteErr.Error()) + } else { + if err := handleDeleteSuccess(task, *v.GoodsID, "", "", *v.Token, &deleteGoodsId); err != nil { + return err + } + } + + if err := service.UpdateDelTaskDetailStatus(v.ID, status, errMsg); err != nil { + return err + } + + if _, err := updateTaskProgress(); err != nil { + return err + } + } + + return notifyDeletedGoods(*task.ShopID, deleteGoodsId) +} + +// 获取商品列表的通用函数 +func getGoodsList(token string, maxTotal int) ([]planBTypePinduoduo.GoodsItem, error) { + var goodsList []planBTypePinduoduo.GoodsItem + page := 1 + pageSize := 100 + + for len(goodsList) < maxTotal { + params := map[string]string{ + "accessToken": token, + "page": strconv.Itoa(page), + "pageSize": strconv.Itoa(pageSize), + "isOnsale": "0", + } + + resp, _, err := tool.GetPddGoodsList(params) + if err != nil { + return nil, err + } + + if len(resp.GoodsList) == 0 { + break + } + + goodsList = append(goodsList, resp.GoodsList...) + + if len(goodsList) >= resp.TotalCount || len(goodsList) >= maxTotal { + break + } + page++ + } + + if len(goodsList) > maxTotal { + goodsList = goodsList[:maxTotal] + } + return goodsList, nil +} + +// 通用删除逻辑 +func processDeletions(task mysql.DelTask, goodsList []planBTypePinduoduo.GoodsItem, token string) error { + var deleteGoodsId []int64 + + // 只查询一次任务状态 + currentTask, err := service.GetDelTask() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("任务不存在") + } + return fmt.Errorf("查询任务失败: %w", err) + } + + //修改状态为执行中 + updateTaskStatusErr := service.UpdateTaskStatus(1) + if updateTaskStatusErr != nil { + return fmt.Errorf("更新任务状态失败: %w", updateTaskStatusErr) + } + + for _, v := range goodsList { + // 检查任务是否已完成 + if *currentTask.TaskCountOver > *currentTask.TaskCount { + break + } + deleteErr := PddDeleteGoodsTask(v.GoodsId, token) + + if deleteErr != nil && strings.Contains(deleteErr.Error(), "您当日所删除的商品已达上限") { + return handleLimitError(task, deleteGoodsId) + } else if deleteErr != nil { + fmt.Printf("商品id: %v Err %v\n", v.GoodsId, deleteErr.Error()) + } else { + outerId := "" + if len(v.SkuList) > 0 { + outerId = v.SkuList[0].OuterId + } + if err := handleDeleteSuccess(task, v.GoodsId, v.GoodsName, outerId, token, &deleteGoodsId); err != nil { + return err + } + } + + // 每删除一个,任务的完成数+1 + if err := service.UpdateTaskCountOver(); err != nil { + return err + } + + // 更新当前任务计数(避免每次都查询数据库) + *currentTask.TaskCount++ + } + + // 循环结束后通知剩余数据 + if len(deleteGoodsId) > 0 { + return notifyDeletedGoods(*task.ShopID, deleteGoodsId) + } + return nil +} + +func PddCountTask(task mysql.DelTask) error { + var header planAType.TaskHeader + if err := json.Unmarshal([]byte(*task.Header), &header); err != nil { + return err + } + + var dleNum int + dleNum = *task.TaskCount - *task.TaskCountOver + if dleNum > 5000 { + dleNum = 5000 + } + fmt.Println("任务ID ", golabl.TaskId, " 删除数量 :", dleNum) + + goodsList, err := getGoodsList(header.ShopMsg.Token, dleNum) + if err != nil { + return err + } + fmt.Println("获取删除数量的长度 ", len(goodsList)) + + if len(goodsList) == 0 { + if err := handleLimitError(task, []int64{}); err != nil { + return err + } + } + + return processDeletions(task, goodsList, header.ShopMsg.Token) +} + +func PddTimeTask(task mysql.DelTask) error { + var header planAType.TaskHeader + if err := json.Unmarshal([]byte(*task.Header), &header); err != nil { + return err + } + + goodsList, err := getGoodsList(header.ShopMsg.Token, 5000) + if err != nil { + return err + } + + updateTaskCountAndTaskCountOverErr := service.UpdateTaskCountAndTaskCountOver() + if updateTaskCountAndTaskCountOverErr != nil { + return updateTaskCountAndTaskCountOverErr + } + + // 过滤掉创建时间大于停止时间的商品 + filteredList := make([]planBTypePinduoduo.GoodsItem, 0, len(goodsList)) + for _, v := range goodsList { + if v.CreatedAt <= task.StopAt.Unix() { + filteredList = append(filteredList, v) + updateTaskCountErr := service.UpdateTaskCount() + if updateTaskCountErr != nil { + return updateTaskCountErr + } + } + } + fmt.Printf("获取删除数量的长度 %v\n", len(filteredList)) + + if len(filteredList) == 0 { + if err := handleLimitError(task, []int64{}); err != nil { + return err + } + } + + if err := processDeletions(task, filteredList, header.ShopMsg.Token); err != nil { + return err + } + + // 如果有商品被过滤掉,标记任务完成 + if len(filteredList) < len(goodsList) { + return service.UpdateTaskStatus(3) + } + return nil +} + +func PddDeleteGoodsTask(goodsId int64, token string) error { + if goodsId == 0 { + return fmt.Errorf("商品Id不能为空") + } + + reqDataInfo := planDTypePinduoduo.DeleteGoodsCommit{GoodsIds: []int64{goodsId}} + delGoodsRet, _, err := PddDelGoods(reqDataInfo, token) + if err != nil { + return err + } + if !delGoodsRet.OpenAPIResponse { + return errors.New("删除商品失败") + } + return nil +} + +func PddDelGoods(reqDataInfo planDTypePinduoduo.DeleteGoodsCommit, token string) (planDTypePinduoduo.DeleteGoodsCommitResponse, string, error) { + var delGoods planDTypePinduoduo.DeleteGoodsCommitResponse + goodsInfoStr, err := json.Marshal(reqDataInfo) + if err != nil { + return delGoods, "", err + } + + delGoodsStr, err := golabl.PddDll.PddDeleteGoodsCommit( + golabl.Config.PddConfig.ClientId, + golabl.Config.PddConfig.ClientSecret, + token, + string(goodsInfoStr), + ) + + if strings.Contains(delGoodsStr, "请求失败") || strings.Contains(delGoodsStr, "错误码") { + return delGoods, delGoodsStr, errors.New("拼多多 DelGoods 错误:" + delGoodsStr) + } + if err != nil { + return delGoods, "", err + } + if err := json.Unmarshal([]byte(delGoodsStr), &delGoods); err != nil { + return delGoods, "", fmt.Errorf("解析拼多多 DelGoods 接口返回json失败: %v", err) + } + return delGoods, delGoodsStr, nil +} + +///////////////////////////////////////////////////////////孔夫子///////////////////////////////////////////////////////////////////////////////// + +func KfzRegularTask(task mysql.DelTask) error { + delTask, err := service.GetMax5000WaitDelTask() + if err != nil { + return err + } + + var deleteGoodsId []int64 + + for _, v := range delTask { + status := 1 + errMsg := "执行成功" + deleteErr := KfzDeleteGoodsTask(*v.GoodsID, *v.Token) + + if deleteErr != nil { + status = 2 + errMsg = deleteErr.Error() + fmt.Printf("商品id: %v Err %v\n", v.GoodsID, deleteErr.Error()) + } else { + if err := handleDeleteSuccess(task, *v.GoodsID, "", "", *v.Token, &deleteGoodsId); err != nil { + return err + } + } + + if err := service.UpdateDelTaskDetailStatus(v.ID, status, errMsg); err != nil { + return err + } + + if _, err := updateTaskProgress(); err != nil { + return err + } + } + + return notifyDeletedGoods(*task.ShopID, deleteGoodsId) +} + +func KfzDeleteGoodsTask(goodsId int64, token string) error { + if goodsId == 0 { + return fmt.Errorf("商品Id不能为空") + } + + reqDataInfo := planDTypeKfz.DeleteGoodsCommit{ItemId: strconv.FormatInt(goodsId, 10)} + delGoodsRet, _, err := KfzDelGoods(reqDataInfo, token) + if err != nil { + return err + } + if delGoodsRet.ErrorResponse != nil { + return errors.New("删除商品失败") + } + return nil +} + +func KfzDelGoods(reqDataInfo planDTypeKfz.DeleteGoodsCommit, token string) (planDTypeKfz.DeleteGoodsCommitResponse, string, error) { + var delGoods planDTypeKfz.DeleteGoodsCommitResponse + goodsInfoStr, err := json.Marshal(reqDataInfo) + if err != nil { + return delGoods, "", err + } + delGoodsStr, err := golabl.KfzDll.DeleteGoods(golabl.Config.KfzConfig.AppId, golabl.Config.KfzConfig.AppSecret, token, string(goodsInfoStr)) + if strings.Contains(delGoodsStr, "失败") || strings.Contains(delGoodsStr, "错误") { + return delGoods, delGoodsStr, errors.New("孔夫子 DelGoods 错误:" + delGoodsStr) + } + if err != nil { + return delGoods, "", err + } + if err := json.Unmarshal([]byte(delGoodsStr), &delGoods); err != nil { + return delGoods, "", fmt.Errorf("解析孔夫子 DelGoods 接口返回json失败: %v", err) + } + return delGoods, delGoodsStr, nil +} diff --git a/planD/main.go b/planD/main.go new file mode 100644 index 0000000..9324a0b --- /dev/null +++ b/planD/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "planA/planD/initialization" + "planA/planD/logic" + "planA/planD/validation" + "time" +) + +func main() { + + //校验参数 + taskId, validationErr := validation.Validation() + if validationErr != nil { + fmt.Println(validationErr) + return + } + + // 初始化 + err := initialization.Init(taskId) + if err != nil { + fmt.Println("初始化失败:", err) + return + } + + //执行 + err = logic.Logic() + if err != nil { + fmt.Println("执行失败:", err) + return + } + + // 暂停1分钟,并循环倒计时 + fmt.Println("\n✅ 任务执行完成!", taskId) + fmt.Println("⏸️ 暂停1分钟后自动退出...") + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for i := 60; i > 0; i-- { + minutes := i / 60 + seconds := i % 60 + fmt.Printf("\r⏰ 剩余时间: %02d:%02d", minutes, seconds) + <-ticker.C + } + fmt.Println("\n✨ 程序自动退出") +} diff --git a/planD/modules/config/config.dll b/planD/modules/config/config.dll new file mode 100644 index 0000000..4e1d91b Binary files /dev/null and b/planD/modules/config/config.dll differ diff --git a/planD/modules/config/conifg.go b/planD/modules/config/conifg.go new file mode 100644 index 0000000..a8138f3 --- /dev/null +++ b/planD/modules/config/conifg.go @@ -0,0 +1,74 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// ConfigDLL 配置文件读取DLL结构 +type ConfigDLL struct { + dll *syscall.DLL + readConfigFile *syscall.Proc // 读取配置文件 + getVersion *syscall.Proc // 获取版本信息 + freeCString *syscall.Proc // 释放C字符串 +} + +// InitConfigDLL 初始化ConfigDLL +func InitConfigDLL() (*ConfigDLL, error) { + dllPath := filepath.Join("modules/config/", "config.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("config DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载config DLL 失败: %s", err) + } else { + return &ConfigDLL{ + dll: dll, + readConfigFile: dll.MustFindProc("ReadConfigFile"), + getVersion: dll.MustFindProc("GetVersion"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +// cStr 获取C字符串 +func (m *ConfigDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} + +// ReadConfigFile 读取配置文件 +func (m *ConfigDLL) ReadConfigFile(filePath, fileName string) (string, error) { + proc, err := m.dll.FindProc("ReadConfigFile") + if err != nil { + return "", fmt.Errorf("找不到函数 ReadConfigFile: %v", err) + } + + filePathPtr, _ := syscall.BytePtrFromString(filePath) + fileNamePtr, _ := syscall.BytePtrFromString(fileName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(filePathPtr)), + uintptr(unsafe.Pointer(fileNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} diff --git a/planD/modules/kfz/kfz.dll b/planD/modules/kfz/kfz.dll new file mode 100644 index 0000000..e69de29 diff --git a/planD/modules/kfz/kfz.go b/planD/modules/kfz/kfz.go new file mode 100644 index 0000000..95fdd9d --- /dev/null +++ b/planD/modules/kfz/kfz.go @@ -0,0 +1,253 @@ +package kfz + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// KfzDLL 孔夫子工具DLL结构 +type KfzDLL struct { + Dll *syscall.DLL + freeCString *syscall.Proc // 释放C字符串 +} + +// InitKfzDll 初始化 kfzDLL +func InitKfzDll(url string) (*KfzDLL, error) { + dllPath := filepath.Join(url, "kfz.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("kfz DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } + gKfzDll := &KfzDLL{ + Dll: dll, + freeCString: dll.MustFindProc("FreeCString"), + } + return gKfzDll, nil +} + +// PublishGoods 发布商品 +func (m *KfzDLL) PublishGoods(appId int, clientSecret, accessToken, goodsAddJson string) (string, error) { + + proc, err := m.Dll.FindProc("KongfzShopItemAdd") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemAdd: %v", err) + } + + fmt.Println("appId", appId) + fmt.Println("clientSecret", clientSecret) + fmt.Println("accessToken", accessToken) + fmt.Println("goodsAddJson", goodsAddJson) + + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsAddJsonPtr, _ := syscall.BytePtrFromString(goodsAddJson) + + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsAddJsonPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// KfzGoodsImageUpload 将图片上传到孔夫子图片空间 +func (m *KfzDLL) KfzGoodsImageUpload(appId int, clientSecret, accessToken, filePath string) (string, error) { + proc, err := m.Dll.FindProc("KongfzImageUpload") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzImageUpload: %v", err) + } + //appIdPtr, _ := syscall.BytePtrFromString(appId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsAddJsonPtr, _ := syscall.BytePtrFromString(filePath) + savePathPtr, _ := syscall.BytePtrFromString("") + + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsAddJsonPtr)), + uintptr(unsafe.Pointer(savePathPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// GetGoodsCategoryList 获取本店商品分类列表 +func (m *KfzDLL) GetGoodsCategoryList(appId int, clientSecret, accessToken string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopCategoryNameList") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopCategoryNameList: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// GetCommonCategory 获取公用分类数据 +func (m *KfzDLL) GetCommonCategory(appId int, clientSecret, accessToken string) (string, error) { + proc, err := m.Dll.FindProc("KongfzCommonCategory") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzCommonCategory: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// GetGoodsList 获取商品列表 +func (m *KfzDLL) GetGoodsList(appId int, clientSecret, accessToken, getGoodsListReqJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemList") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemList: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + getGoodsListReqJsonPtr, _ := syscall.BytePtrFromString(getGoodsListReqJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(getGoodsListReqJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PutOnSale 上架 +func (m *KfzDLL) PutOnSale(appId int, clientSecret, accessToken, putOnSaleJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemListing") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemListing: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + putOnSaleJsonPtr, _ := syscall.BytePtrFromString(putOnSaleJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(putOnSaleJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PutOffSale 下架 +func (m *KfzDLL) PutOffSale(appId int, clientSecret, accessToken, putOffSaleJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemDelisting") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemDelisting: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + putOffSaleJsonPtr, _ := syscall.BytePtrFromString(putOffSaleJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(putOffSaleJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// UpdateGoodsStock 修改商品库存 +func (m *KfzDLL) UpdateGoodsStock(appId int, clientSecret, accessToken, updateGoodsStockJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemNumberUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemNumberUpdate: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + updateGoodsStockJsonPtr, _ := syscall.BytePtrFromString(updateGoodsStockJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(updateGoodsStockJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// UpdateGoodsPrice 修改商品价格 +func (m *KfzDLL) UpdateGoodsPrice(appId int, clientSecret, accessToken, updateGoodsPriceJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemPriceUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemPriceUpdate: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + updateGoodsPriceJsonPtr, _ := syscall.BytePtrFromString(updateGoodsPriceJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(updateGoodsPriceJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// DeleteGoods 删除商品 +func (m *KfzDLL) DeleteGoods(appId int, clientSecret, accessToken, deleteGoodsJson string) (string, error) { + proc, err := m.Dll.FindProc("KongfzShopItemDelete") + if err != nil { + return "", fmt.Errorf("找不到函数 KongfzShopItemDelete: %v", err) + } + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + deleteGoodsJsonPtr, _ := syscall.BytePtrFromString(deleteGoodsJson) + resultPtr, _, _ := proc.Call( + uintptr(appId), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(deleteGoodsJsonPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} diff --git a/planD/modules/pdd/pdd.dll b/planD/modules/pdd/pdd.dll new file mode 100644 index 0000000..532e5c9 Binary files /dev/null and b/planD/modules/pdd/pdd.dll differ diff --git a/planD/modules/pdd/pdd.go b/planD/modules/pdd/pdd.go new file mode 100644 index 0000000..4f25821 --- /dev/null +++ b/planD/modules/pdd/pdd.go @@ -0,0 +1,337 @@ +package pdd + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +var ( + gPddDll *PddDLL +) + +// PddResponse 定义完整的响应结构(包含成功和失败两种情况) +type PddResponse struct { + SuccessResponse *PddSuccessResponse `json:"outer_cat_mapping_get_response,omitempty"` + ErrorResponse *PddErrorResponse `json:"error_response,omitempty"` +} +type PddSuccessResponse struct { + OuterCatMappingGetResponse PddCategoryMappingResponse `json:"outer_cat_mapping_get_response"` +} + +// PddCategoryMappingResponse 定义拼多多API响应结构(根据文档规范) +type PddCategoryMappingResponse struct { + CatID1 int64 `json:"cat_id1"` // 一级类目 ID + CatID2 int64 `json:"cat_id2"` // 二级类目 ID + CatID3 int64 `json:"cat_id3"` // 三级类目 ID + CatID4 int64 `json:"cat_id4"` // 四级类目 ID + RequestID string `json:"request_id"` // 请求 ID +} + +// PddDLL 拼多多工具DLL结构 +type PddDLL struct { + Dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} +type PddErrorResponse struct { + ErrorCode int64 `json:"error_code"` // 错误码 + ErrorMsg string `json:"error_msg"` // 错误信息 + SubCode *string `json:"sub_code"` // 子错误码 + SubMsg string `json:"sub_msg"` // 子错误信息 + RequestID string `json:"request_id"` // 请求ID +} + +// InitPddDll 初始化 pddDLL +func InitPddDll(url string) (*PddDLL, error) { + dllPath := filepath.Join(url, "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } + gPddDll = &PddDLL{ + Dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + } + return gPddDll, nil +} + +// PddGoodsOuterCatMappingGet 类目预测 +func (m *PddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsAdd 商品新增 +func (m *PddDLL) PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsAdd") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsAdd: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsAddJsonPtr, _ := syscall.BytePtrFromString(goodsAddJson) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsAddJsonPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// PddGoodsSpecIdGet 生成商家自定义的规格 +func (m *PddDLL) PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsSpecIdGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSpecIdGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + parentSpecIdPtr, _ := syscall.BytePtrFromString(parentSpecId) + specNamePtr, _ := syscall.BytePtrFromString(specName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(parentSpecIdPtr)), + uintptr(unsafe.Pointer(specNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsCommitDetailGet 获取商品提交的商品详情 +func (m *PddDLL) PddGoodsCommitDetailGet(clientId, clientSecret, accessToken, goodsCommitId, goodsId string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsCommitDetailGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsCommitIdPtr, _ := syscall.BytePtrFromString(goodsCommitId) + goodsIdPtr, _ := syscall.BytePtrFromString(goodsId) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsCommitIdPtr)), + uintptr(unsafe.Pointer(goodsIdPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddTimeGet 获取拼多多系统时间 +func (m *PddDLL) PddTimeGet(clientId, clientSecret, accessToken string) (string, error) { + proc, err := m.Dll.FindProc("PddTimeGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsImageUpload 上传图片 +func (m *PddDLL) PddGoodsImageUpload(clientId, clientSecret, accessToken, imgBase64 string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsImageUpload") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsImageUpload: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + imgBase64Ptr, _ := syscall.BytePtrFromString(imgBase64) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(imgBase64Ptr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsListGet 获取店铺商品 +func (m *PddDLL) PddGoodsListGet(clientId, clientSecret, accessToken string, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsListGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsListGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsSaleStatusSet 设置上下架状态 +func (m *PddDLL) PddGoodsSaleStatusSet(clientId, clientSecret, accessToken, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsSaleStatusSet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSaleStatusSet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddDeleteGoodsCommit 删除商品 +func (m *PddDLL) PddDeleteGoodsCommit(clientId, clientSecret, accessToken, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddDeleteGoodsCommit") + if err != nil { + return "", fmt.Errorf("找不到函数 PddDeleteGoodsCommit: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsQuantityUpdate 更新库存 +func (m *PddDLL) PddGoodsQuantityUpdate(clientId, clientSecret, accessToken, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsQuantityUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsQuantityUpdate: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsSkuPriceUpdate 更新价格 +func (m *PddDLL) PddGoodsSkuPriceUpdate(clientId, clientSecret, accessToken, params string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsSkuPriceUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSkuPriceUpdate: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} diff --git a/planD/modules/pdd/pdd.md b/planD/modules/pdd/pdd.md new file mode 100644 index 0000000..2c11768 --- /dev/null +++ b/planD/modules/pdd/pdd.md @@ -0,0 +1,863 @@ +# pdd.dll 使用教程 +## 1.创建DLL工具实例 +### 加载DLL文件 +```gotemplate +// PddDLL 拼多多工具DLL结构 +type pddDLL struct { + dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} + +// <初始化pddDLL> +func InitPddDLL() (*pddDLL, error) { + dllPath := filepath.Join("dll", "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } else { + return &pddDLL{ + dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +dll, err := InitPddDLL() +``` + +### 获取C字符串 +```gotemplate +// cStr 获取C字符串 +func (m *pddDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} +``` + +## 2. 使用dll函数示例 +```gotemplate +// 类目预测 +func (m *pddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} +``` + +# 接口详情 +## 1. 类目预测--PddGoodsOuterCatMappingGet +### 请求信息 +```gotemplate +dll.PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, +outerCatId, outerCatName, outerGoodsName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| outerCatId | string | 是 | 外部平台类目ID | +| outerCatName | string | 是 | 外部平台类目名称 | +| outerGoodsName | string | 是 | 外部商品名称 | +### 响应示例 +```json +{ + "outer_cat_mapping_get_response": { + "cat_id2": 16028, + "cat_id3": 16031, + "cat_id1": 15543, + "request_id": "17666480184871649", + "cat_id4": 0 + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 2. 快递公司查看--PddLogisticsCompaniesGet +### 请求信息 +```gotemplate +dll.PddLogisticsCompaniesGet(clientId, clientSecret) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +### 响应示例 +```json +{ + "logistics_companies_get_response": { + "logistics_companies": [ + { + "available": 1, + "code": "SF", + "id": 1, + "logistics_company": "顺丰速运" + }, + { + "available": 1, + "code": "STO", + "id": 2, + "logistics_company": "申通快递" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 3. erp打单信息同步--PddErpOrderSync +### 请求信息 +```gotemplate +dll.PddErpOrderSync(clientId, clientSecret, accessToken, logisticsId, +orderSn, orderState, waybillNo) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsId | string | 是 | 物流公司ID | +| orderSn | string | 是 | 拼多多订单号 | +| orderState | string | 是 | 订单状态 | +| waybillNo | string | 是 | 运单号 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871650" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 4. 拼多多订单同步--PddOrderSynchronization +### 请求信息 +```gotemplate +dll.PddOrderSynchronization(clientId, clientSecret, accessToken, logisticsCompany, logisticsOnlineSendJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsCompany | string | 是 | 物流公司名称 | +| logisticsOnlineSendJson | string | 是 | 拼多多订单同步json字符串 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871651" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 5. 商品图片上传接口--PddGoodsImgUpload +### 请求信息 +```gotemplate +dll.PddGoodsImgUpload(clientId, clientSecret, accessToken, filePath) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +### 响应示例 +```json +{ + "goods_img_upload_response": { + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "request_id": "17666480184871652" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 6. 商品新增接口--PddGoodsAdd +### 请求信息 +```gotemplate +dll.PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| goodsAddJson | string | 是 | 商品信息JSON字符串 | +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456789, + "goods_name": "测试商品", + "goods_sn": "G202501200001", + "request_id": "17666480184871653" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 7. 联合拼多多图片上传的商品新增--SelfPddGoodsAdd +### 请求信息 +```gotemplate +dll.SelfPddGoodsAdd(clientId, clientSecret, accessToken, filePath, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +| goodsAddJson | string | 是 | 商品信息JSON字符串(不需包含image_url)| +#### 接口说明 +此接口为组合接口,内部执行以下步骤: +1.上传商品主图文件到拼多多服务器 +2.获取图片URL并自动填充到商品信息中 +3.调用商品新增接口创建商品 +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456790, + "goods_name": "测试商品", + "goods_sn": "G202501200002", + "request_id": "17666480184871654" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 8. 批量数据解密脱敏接口--PddOpenDecryptMaskBatch +### 请求信息 +```gotemplate +dll.PddOpenDecryptMaskBatch(clientId, clientSecret, accessToken, reqJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| reqJson | string | 是 | 信息JSON字符串 | +#### 信息JSON结构示例 +```json +[ + { + "data_tag": "251229-272441044622514", + "encrypted_data": "~AgAAAAPlscEH0psOJAEXpTdsLOWvDJ9bB7IEjIoqNfiDhhJR9NHOxsdZ+PEFluSSCngCikoDU+CP/sSXZJ92ic7+PdNlJNLA7g/6VUMDWF6RvjW9IeRN+lKNarsjWDQR~0~" + } +] +``` +### 响应示例 +```json +{ + "open_decrypt_mask_batch_response": { + "data_decrypt_list": [ + { + "data_tag": "str", + "data_type": 0, + "decrypted_data": "str", + "encrypted_data": "str", + "error_code": 0, + "error_msg": "str" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 生成商家自定义的规格--PddGoodsSpecIdGet +### 请求信息 +```gotemplate +dll.PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| parentSpecId | string | 是 | 拼多多标准规格ID | +| specName | string | 是 | 商家编辑的规格值,如颜色规格下设置白色属性 | +### 响应参数 +```json +{ + "goods_spec_id_get_response": { + "parent_spec_id": 0, + "spec_id": 0, + "spec_name": "str" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 修改商品SKU价格--PddGoodsSkuPriceUpdate +### 请求信息 +```gotemplate +dll.PddGoodsSkuPriceUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 价格更新请求JSON字符串 | +#### 请求JSON结构 +```json +{ + "goods_id": "必填,商品id,类型为LONG", + "ignore_edit_warn": "非必填,是否获取商品发布警告信息,默认为忽略,类型为BOOLEAN", + "market_price": "非必填,参考价(单位分),类型为LONG", + "market_price_in_yuan": "非必填,参考价(单位元),类型为STRING", + "sku_price_list": [ + { + "group_price": "非必填,拼团购买价格(单位分),类型为LONG", + "is_onsale": "非必填,sku上架状态,0-已下架,1-上架中,类型为INTEGER", + "single_price": "非必填,单独购买价格(单位分),类型为LONG", + "sku_id": "必填,sku标识,类型为LONG" + } + ], + "sync_goods_operate": "非必填,提交后上架状态,0:上架,1:保持原样,类型为INTEGER", + "two_pieces_discount": "非必填,满2件折扣,可选范围0-100,0表示取消,95表示95折,设置需先查询规则接口获取实际可填范围,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_update_sku_price_response": { + "goods_commit_id": 0, + "is_success": true + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 商品库存更新接口--PddGoodsQuantityUpdate +### 请求信息 +```gotemplate +dll.PddGoodsQuantityUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 库存更新请求JSON字符串 | +#### 请求JSON结构 request 字符串 +```json +{ + "force_update": "非必填,是否强制更新,仅update_type=1(全量更新)时有效,默认值false;force_update=false时,quantity不能小于预扣库存;force_update=true时,代表强制更新,当quantity<预扣库存时,不报错,直接将quantity清0,类型为BOOLEAN", + "goods_id": "必填,商品id,类型为LONG", + "outer_id": "非必填,sku商家编码,类型为STRING", + "quantity": "必填,库存修改值。当全量更新库存时,quantity必须为大于等于0的正整数;当增量更新库存时,quantity为整数,可小于等于0。若增量更新时传入的库存为负数,则负数与实际库存之和不能小于0。比如当前实际库存为1,传入增量更新quantity=-1,库存改为0,类型为LONG", + "sku_id": "非必填,sku_id和outer_id必填一个,类型为LONG", + "update_type": "非必填,库存更新方式,可选。1为全量更新,2为增量更新。如果不填,默认为全量更新,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_quantity_update_response": { + "is_success": false + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 获取商品信息接口 -- OutPddAuthGetCommitDetailt +### 请求信息 +```gotemplate +dll.OutPddAuthGetCommitDetailt(goodsCommitId, goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsCommitId | string | 是 | 商品提交ID | +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json + +``` + + +## 获取商品详情信息接口 -- OutPddAuthGetGoodsDetail +### 请求信息 +```gotemplate +dll.OutPddAuthGetGoodsDetail(goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "bad_fruit_claim": 0, + "buy_limit": 999999, + "carousel_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2025-06-30/4539f740-331b-4687-aa00-5c96855de6cd.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/b0e89e39-c97b-475d-9be2-f1909e30acb5.jpeg" + ], + "cat_id": 15678, + "cost_template_id": 655688447565777, + "country_id": 0, + "customer_num": 2, + "customs": "", + "detail_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/b691c104-baf8-42b2-97e2-b7258113114b.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/53e6f7ff-d15e-4e8f-8625-e293717ca1e4.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/ecff591d-32a6-42c9-ba5a-6a42829092a8.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/7034f8a0-5d88-49f8-a96f-608abb8cac80.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/e10c2b6c-d4de-4fdd-8d48-f0a334735e9a.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/c19358fb-0a4d-49ad-bcc8-b2980e938064.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/1deeb9c0-7212-432b-a309-f774db6e1adb.jpeg" + ], + "goods_desc": "书名:金属工艺学 下 第6版,作者:'邓文英,宋力宏主编',ISBN:9787040456295,出版社:高等教育出版社", + "goods_id": 770621582375, + "goods_name": "金属工艺学 下 第6版 邓文英,宋力宏主编 高等教育出版社 978", + "goods_property_list": [ + { + "punit": "", + "ref_pid": 425, + "template_pid": 401030, + "vid": 0, + "vvalue": "9787040456295" + }, + { + "punit": "", + "ref_pid": 876, + "template_pid": 401029, + "vid": 0, + "vvalue": "金属工艺学 下 第6版" + }, + { + "punit": "页", + "ref_pid": 692, + "template_pid": 401032, + "vid": 0, + "vvalue": "157" + }, + { + "punit": "元", + "ref_pid": 879, + "template_pid": 401034, + "vid": 0, + "vvalue": "24.70" + }, + { + "punit": "", + "ref_pid": 882, + "template_pid": 401037, + "vid": 0, + "vvalue": "邓文英,宋力宏主编" + }, + { + "punit": "", + "ref_pid": 880, + "template_pid": 401035, + "vid": 483761, + "vvalue": "高等教育出版社" + }, + { + "punit": "", + "ref_pid": 888, + "template_pid": 401043, + "vid": 0, + "vvalue": "平装" + } + ], + "goods_type": 1, + "image_url": "", + "invoice_status": 0, + "is_customs": 0, + "is_folt": 0, + "is_group_pre_sale": 0, + "is_pre_sale": 0, + "is_refundable": 1, + "is_sku_pre_sale": 0, + "market_price": 5948, + "order_limit": 999999, + "outer_goods_id": "9787040456295", + "oversea_type": 0, + "pre_sale_time": 0, + "privacy_delivery": 0, + "quan_guo_lian_bao": 0, + "second_hand": 1, + "shipment_limit_second": 172800, + "sku_list": [ + { + "is_onsale": 1, + "limit_quantity": 999999, + "multi_price": 1487, + "out_sku_sn": "9787040456295", + "price": 1587, + "quantity": 0, + "reserve_quantity": 0, + "sku_id": 1753931570290, + "sku_pre_sale_time": 0, + "spec": [ + { + "parent_id": 1216, + "parent_name": "尺寸", + "spec_id": 27632894279, + "spec_name": "单本 无附赠 超七天不退换" + } + ], + "thumb_url": "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "weight": 500 + } + ], + "status": 4, + "tiny_name": "金属工艺学 下 第6", + "two_pieces_discount": 96, + "video_gallery": [], + "warehouse": "", + "warm_tips": "", + "zhi_huan_bu_xiu": 0 +} +``` + +## 生成自定义规格接口 -- OutPddAuthSetSpec +### 请求信息 +```gotemplate +dll.OutPddAuthSetSpec(specTypeId, specName, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| specTypeId | int | 是 | 规格类型ID | +| specName | string | 是 | 规格名称 | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "parentSpecId": 3820, + "specName": "全新", + "specId": 1080396526 +} +``` + +## 修改价格接口 -- OutPddAuthUpdatePrice +### 请求信息 +```gotemplate +dll.OutPddAuthUpdatePrice(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 修改库存接口 -- OutPddAuthUpdateStock +### 请求信息 +```gotemplate +dll.OutPddAuthUpdateStock(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 12.释放C字符串内存--FreeCString +### 请求信息 +```gotemplate +dll.FreeCString(str) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| str | string | 是 | 需要释放的字符串 | diff --git a/planD/service/del_task.go b/planD/service/del_task.go new file mode 100644 index 0000000..3e72026 --- /dev/null +++ b/planD/service/del_task.go @@ -0,0 +1,60 @@ +package service + +import ( + "planA/planD/initialization/golabl" + planAType "planA/type/mysql" + "time" + + "gorm.io/gorm" +) + +// GetDelTask 查询指定的删除任务 +func GetDelTask() (planAType.DelTask, error) { + var delTask planAType.DelTask + err := golabl.MysqlDb.Where("task_id = ?", golabl.TaskId).First(&delTask).Error + return delTask, err +} + +// UpdateTaskCountOver 根据task_id 将 task_count_over +1 +func UpdateTaskCountOver() error { + return golabl.MysqlDb.Model(&planAType.DelTask{}). + Where("task_id = ?", golabl.TaskId). + Updates(map[string]interface{}{ + "task_count_over": gorm.Expr("task_count_over + 1"), + }).Error +} + +// UpdateTaskCount 根据task_id 将 task_count +1 +func UpdateTaskCount() error { + return golabl.MysqlDb.Model(&planAType.DelTask{}). + Where("task_id = ?", golabl.TaskId). + Updates(map[string]interface{}{ + "task_count": gorm.Expr("task_count + 1"), + }).Error +} + +// UpdateTaskCountAndTaskCountOver 根据task_id 将 task_count与task_count_over 设置为0 +func UpdateTaskCountAndTaskCountOver() error { + return golabl.MysqlDb.Model(&planAType.DelTask{}). + Where("task_id = ?", golabl.TaskId). + Updates(map[string]interface{}{ + "task_count": 0, + "task_count_over": 0, + }).Error +} + +// UpdateTaskStatus 根据task_id 修改 status +func UpdateTaskStatus(status int) error { + updateData := map[string]interface{}{ + "status": status, + } + + // 如果 status=2,设置 pause_at 为当前时间 + if status == 2 { + updateData["pause_at"] = time.Now() + } + + return golabl.MysqlDb.Model(&planAType.DelTask{}). + Where("task_id = ?", golabl.TaskId). + Updates(updateData).Error +} diff --git a/planD/service/del_task_details.go b/planD/service/del_task_details.go new file mode 100644 index 0000000..bf04ecb --- /dev/null +++ b/planD/service/del_task_details.go @@ -0,0 +1,106 @@ +package service + +import ( + "fmt" + planBType "planA/planB/type" + "planA/planD/initialization/golabl" + "time" +) + +// CreateTableIfNotExists 创建表 +// @return error 错误信息 +func CreateTableIfNotExists(taskId string) error { + var dleTaskDetailsTable = fmt.Sprintf("del_task_details_%v", taskId) + // 检查表是否存在 + if !golabl.MysqlDb.Migrator().HasTable(dleTaskDetailsTable) { + sql := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s ( + id int(11) NOT NULL AUTO_INCREMENT, + del_task_id int(11) DEFAULT '0' COMMENT '删除任务id', + task_id varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT '任务id', + isbn varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT 'isbn', + book_name varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT '商品名称', + token varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT 'token', + goods_id bigint(11) DEFAULT NULL COMMENT '商品id', + json text COLLATE utf8_unicode_ci COMMENT '原始字符串', + status int(11) DEFAULT '0' COMMENT '状态: 1=正常 2=错误', + err text COLLATE utf8_unicode_ci COMMENT '错误信息', + delete_at datetime DEFAULT NULL COMMENT '请求删除商品时间', + delete_date date DEFAULT NULL COMMENT '请求删除商品日期', + create_at datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (id), + KEY del_task_id (del_task_id, task_id, goods_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci`, dleTaskDetailsTable) + + if err := golabl.MysqlDb.Exec(sql).Error; err != nil { + return fmt.Errorf("创建 %v 表失败: %v", dleTaskDetailsTable, err) + } + } + return nil +} + +// GetMax5000WaitDelTask 查询最大5000条等待删除的任务 +func GetMax5000WaitDelTask() ([]*planBType.DelTaskDetail, error) { + var delTask []*planBType.DelTaskDetail + err := golabl.MysqlDb.Table("del_task_details_"+golabl.TaskId).Where("status = ?", 0).Limit(5000).Find(&delTask).Error + return delTask, err +} + +// UpdateDelTaskDetailStatus 根据goodsId修改数据状态 +func UpdateDelTaskDetailStatus(id int, status int, err string) error { + now := time.Now() // 获取当前时间 + return golabl.MysqlDb.Table("del_task_details_"+golabl.TaskId). + Where("id = ?", id). + Updates(map[string]interface{}{ + "status": status, + "err": err, + "delete_at": now, + "delete_date": now.Format("2006-01-02"), + }).Error +} + +// GetTaskCountOver 获取未完成的任务数量 +func GetTaskCountOver() (int64, error) { + var count int64 + err := golabl.MysqlDb.Table("del_task_details_"+golabl.TaskId).Where("status = ?", 0).Count(&count).Error + return count, err +} + +// InsertDelTaskDetail 插入单条删除任务详情数据 +func InsertDelTaskDetail(delTaskID int64, taskId string, token string, bookName string, isbn string, goodsId int64) error { + var dleTaskDetailsTable = fmt.Sprintf("del_task_details_%v", taskId) + + // 先检查并创建表 + if err := CreateTableIfNotExists(taskId); err != nil { + return err + } + + // 检查数据是否存在 + var count int64 + if err := golabl.MysqlDb.Table(dleTaskDetailsTable).Where("task_id = ? AND goods_id = ?", taskId, goodsId).Count(&count).Error; err != nil { + return fmt.Errorf("查询数据失败: %v", err) + } + if count == 0 { + now := time.Now() + status := int64(1) + + delTaskDetail := &planBType.DelTaskDetail{ + DelTaskID: &delTaskID, + TaskID: &taskId, + BookName: &bookName, + Token: &token, + Isbn: &isbn, + GoodsID: &goodsId, + Status: &status, + DeleteAt: nil, + DeleteDate: nil, + CreateAt: &now, + } + + // 使用动态表名插入 + result := golabl.MysqlDb.Table(dleTaskDetailsTable).Create(delTaskDetail) + if result.Error != nil { + return fmt.Errorf("插入数据失败: %v", result.Error) + } + } + return nil +} diff --git a/planD/tool/http.go b/planD/tool/http.go new file mode 100644 index 0000000..dbeca0c --- /dev/null +++ b/planD/tool/http.go @@ -0,0 +1,101 @@ +package tool + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" +) + +// HttpGetRequest 发起 GET 请求 +// @param url 请求地址 +// @return int 响应状态码 +// @return string 响应内容 +// @return error 错误信息 +func HttpGetRequest(url string) (int, string, error) { + resp, httpGetErr := http.Get(url) + if httpGetErr != nil { + return 0, "", fmt.Errorf("http get 请求失败: %v %v", url, httpGetErr) + } + defer resp.Body.Close() // 重要:必须关闭响应体 + + // 读取响应内容 + body, err := io.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, "", fmt.Errorf("http get 读取响应失败: %v %v", url, err) + } + return resp.StatusCode, string(body), nil +} + +// SubmitFormData 提交表单数据 +// @param url 请求地址 +// @param params 表单数据 +// @return error 错误信息 +func SubmitFormData(url string, params map[string]string) (string, error) { + // 创建multipart writer + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // 添加文本字段 + for key, value := range params { + err := writer.WriteField(key, value) + if err != nil { + return "", fmt.Errorf("write field error: %v", err) + } + } + + // 关闭writer + writer.Close() + + // 创建请求 + req, err := http.NewRequest("POST", url, body) + if err != nil { + return "", fmt.Errorf("create request error: %v", err) + } + + // 设置Content-Type + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("send request error: %v", err) + } + defer resp.Body.Close() + + // 读取响应 + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read response error: %v", err) + } + + return string(respBody), nil +} + +// BuildURLWithParams 将map参数拼接到URL后面 +func BuildURLWithParams(baseURL string, params map[string]string) (string, error) { + if len(params) == 0 { + return baseURL, nil + } + + // 解析基础URL + parsedURL, err := url.Parse(baseURL) + if err != nil { + return "", fmt.Errorf("解析URL失败: %v", err) + } + + // 获取现有的查询参数 + query := parsedURL.Query() + + // 添加新的参数 + for key, value := range params { + query.Set(key, value) + } + // 重新编码查询参数 + parsedURL.RawQuery = query.Encode() + + return parsedURL.String(), nil +} diff --git a/planD/tool/tool.go b/planD/tool/tool.go new file mode 100644 index 0000000..b896438 --- /dev/null +++ b/planD/tool/tool.go @@ -0,0 +1,29 @@ +package tool + +import ( + "encoding/json" + planBTypePinduoduo "planA/planB/type/pinduoduo" + "planA/planD/initialization/golabl" +) + +// GetPddGoodsList 获取商品列表 +// @param params 查询参数 +// @return planBTypePinduoduo.GoodsListResponse 商品列表 +// @return error 错误信息 +func GetPddGoodsList(params map[string]string) (planBTypePinduoduo.GoodsListResponse, string, error) { + var goodsListt planBTypePinduoduo.GoodsListResponse + url := golabl.Config.FileUrl.PddGetGoodsUrl + withParams, buildURLWithParamsErr := BuildURLWithParams(url, params) + if buildURLWithParamsErr != nil { + return goodsListt, "", buildURLWithParamsErr + } + _, resStr, httpGetRequestErr := HttpGetRequest(withParams) + if httpGetRequestErr != nil { + return goodsListt, resStr, httpGetRequestErr + } + unmarshalErr := json.Unmarshal([]byte(resStr), &goodsListt) + if unmarshalErr != nil { + return goodsListt, resStr, unmarshalErr + } + return goodsListt, resStr, nil +} diff --git a/planD/type/kfz/kfz.go b/planD/type/kfz/kfz.go new file mode 100644 index 0000000..141ee16 --- /dev/null +++ b/planD/type/kfz/kfz.go @@ -0,0 +1,24 @@ +package kfz + +// DeleteGoodsCommit 删除商品 +type DeleteGoodsCommit struct { + ItemId string `json:"itemId"` +} + +// DeleteGoodsCommitResponse 删除商品返回结构 +type DeleteGoodsCommitResponse struct { + ErrorResponse interface{} `json:"errorResponse"` // 根据实际类型可替换为具体结构体 + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse *SuccessResponse `json:"successResponse"` +} + +type SuccessResponse struct { + Item *Item `json:"item"` +} + +type Item struct { + IsDelete string `json:"isDelete"` + ItemId int64 `json:"itemId"` + UpdateTime string `json:"updateTime"` +} diff --git a/planD/type/pinduoduo/pinduoduo.go b/planD/type/pinduoduo/pinduoduo.go new file mode 100644 index 0000000..cadc5be --- /dev/null +++ b/planD/type/pinduoduo/pinduoduo.go @@ -0,0 +1,12 @@ +package pinduoduo + +// DeleteGoodsCommit 删除商品 +type DeleteGoodsCommit struct { + GoodsIds []int64 `json:"goods_ids"` +} + +// DeleteGoodsCommitResponse 删除商品响应结构 +type DeleteGoodsCommitResponse struct { + OpenAPIResponse bool `json:"open_api_response"` + RequestID string `json:"request_id"` +} diff --git a/planD/validation/validation.go b/planD/validation/validation.go new file mode 100644 index 0000000..a0b52cb --- /dev/null +++ b/planD/validation/validation.go @@ -0,0 +1,14 @@ +package validation + +import ( + "fmt" + "os" +) + +func Validation() (string, error) { + taskId := os.Args[1] + if taskId == "" { + return "", fmt.Errorf("任务Id 不能为空") + } + return taskId, nil +} diff --git a/planE/config.yaml b/planE/config.yaml new file mode 100644 index 0000000..dd8a6f0 --- /dev/null +++ b/planE/config.yaml @@ -0,0 +1,5 @@ +pdd_config: + client_id: "203c5a7ba8bd4b8488d5e26f93052642" #拼多多clientId + client_secret: "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" #拼多多clientSecret +file_url: + pdd_dll: "D:\\source\\planA\\planE\\modules\\pdd" #拼多多 DLL库路径 \ No newline at end of file diff --git a/planE/initialization/config/config.go b/planE/initialization/config/config.go new file mode 100644 index 0000000..653b9b1 --- /dev/null +++ b/planE/initialization/config/config.go @@ -0,0 +1,46 @@ +package config + +import ( + "encoding/json" + "fmt" + "planA/planE/initialization/golabl" + planBConfig "planA/planE/modules/config" + "planA/tool" + planAtype "planA/type" +) + +// GetConfigSetToG 获取配置文件并保存到全局变量中 +// @return error 错误信息 +func GetConfigSetToG() error { + // 检查全局 CTX 是否失效 以防止重复初始化和 ctx 失效 导致程序崩溃 + checkContextErr := tool.CheckContext(golabl.Ctx) + if checkContextErr != nil { + // 返回 且 返回错误 + return checkContextErr + } + + //读取配置文件 + var config planAtype.Config + + // 加载 config.dll + dll, initConfigDLLErr := planBConfig.InitConfigDLL() + if initConfigDLLErr != nil { + return initConfigDLLErr + } + + // 读取配置文件 + configJson, ReadConfigFileErr := dll.ReadConfigFile("", "config.yaml") + if ReadConfigFileErr != nil { + return fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + // 转换配置文件到 JSON + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + + // 保存到全局变量 + golabl.Config = config + // 返回 + return nil +} diff --git a/planE/initialization/dll/dll.go b/planE/initialization/dll/dll.go new file mode 100644 index 0000000..30ae5ee --- /dev/null +++ b/planE/initialization/dll/dll.go @@ -0,0 +1,15 @@ +package dll + +import ( + "planA/planE/initialization/dll/pdd" +) + +// GetDllSetToG 获取DLL +func GetDllSetToG() error { + // 初始化 PddDll + getPddDllSetToGErr := pdd.GetPddDllSetToG() + if getPddDllSetToGErr != nil { + return getPddDllSetToGErr + } + return nil +} diff --git a/planE/initialization/dll/pdd/pdd.go b/planE/initialization/dll/pdd/pdd.go new file mode 100644 index 0000000..051f83e --- /dev/null +++ b/planE/initialization/dll/pdd/pdd.go @@ -0,0 +1,17 @@ +package pdd + +import ( + "planA/planE/initialization/golabl" + "planA/planE/modules/pdd" +) + +// GetPddDllSetToG 获取拼多多DLL +func GetPddDllSetToG() error { + // 初始化 PddDll + pddDll, err := pdd.InitPddDll(golabl.Config.FileUrl.PddDll) + if err != nil { + return err + } + golabl.PddDll = pddDll + return nil +} diff --git a/planE/initialization/golabl/golabl.go b/planE/initialization/golabl/golabl.go new file mode 100644 index 0000000..13ebabf --- /dev/null +++ b/planE/initialization/golabl/golabl.go @@ -0,0 +1,13 @@ +package golabl + +import ( + "context" + "planA/planE/modules/pdd" + planAType "planA/type" +) + +var ( + Ctx context.Context // 全局上下文 + PddDll *pdd.PddDLL // 全局拼多多 DLL + Config planAType.Config // 全局配置 +) diff --git a/planE/initialization/init.go b/planE/initialization/init.go new file mode 100644 index 0000000..e3e2183 --- /dev/null +++ b/planE/initialization/init.go @@ -0,0 +1,28 @@ +package initialization + +import ( + "context" + "fmt" + "planA/planE/initialization/config" + "planA/planE/initialization/dll" + "planA/planE/initialization/golabl" +) + +// Init 初始化 +func Init() error { + //初始化上下文 + if golabl.Ctx == nil { + golabl.Ctx = context.Background() + } + // 初始化配置文件 + if configErr := config.GetConfigSetToG(); configErr != nil { + return fmt.Errorf("初始化配置文件失败:%v", configErr) + } + + // 初始化 DLL + if dllErr := dll.GetDllSetToG(); dllErr != nil { + return fmt.Errorf("初始化DLL失败: %v", dllErr) + } + + return nil +} diff --git a/planE/logic/logic.go b/planE/logic/logic.go new file mode 100644 index 0000000..9108c55 --- /dev/null +++ b/planE/logic/logic.go @@ -0,0 +1,69 @@ +package logic + +import ( + "encoding/base64" + "fmt" + "io" + "net/http" + "planA/planE/initialization/golabl" + "strings" +) + +func Logic(imgUrl string, token string) { + //将web图片转为base64 + imgBase64, imageToBase64Err := ImageToBase64(imgUrl) + if imageToBase64Err != nil { + fmt.Println(imageToBase64Err) + return + } + upload, PddGoodsImageUploadErr := golabl.PddDll.PddGoodsImageUpload(golabl.Config.PddConfig.ClientId, golabl.Config.PddConfig.ClientSecret, token, imgBase64) + if PddGoodsImageUploadErr != nil { + fmt.Println(PddGoodsImageUploadErr) + return + } + fmt.Println(upload) +} + +// ImageToBase64 将网络图片转换为 Base64 字符串 +func ImageToBase64(imageURL string) (string, error) { + // 1. 发送 HTTP GET 请求获取图片 + resp, err := http.Get(imageURL) + if err != nil { + return "", fmt.Errorf("failed to download image: %w", err) + } + defer resp.Body.Close() + + // 检查 HTTP 状态码 + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP error: %s", resp.Status) + } + + // 2. 读取图片数据 + imageData, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read image data: %w", err) + } + + // 3. 转换为 Base64 + base64Str := base64.StdEncoding.EncodeToString(imageData) + + // 4. 可选:获取 Content-Type 并拼接 Data URL 格式 + contentType := resp.Header.Get("Content-Type") + if contentType == "" { + // 根据文件扩展名推测 MIME 类型 + if strings.HasSuffix(imageURL, ".png") { + contentType = "image/png" + } else if strings.HasSuffix(imageURL, ".jpg") || strings.HasSuffix(imageURL, ".jpeg") { + contentType = "image/jpeg" + } else if strings.HasSuffix(imageURL, ".gif") { + contentType = "image/gif" + } else if strings.HasSuffix(imageURL, ".webp") { + contentType = "image/webp" + } else { + contentType = "application/octet-stream" + } + } + + // 返回 Data URL 格式(可直接用于 img 标签的 src 属性) + return fmt.Sprintf("data:%s;base64,%s", contentType, base64Str), nil +} diff --git a/planE/main.go b/planE/main.go new file mode 100644 index 0000000..175942f --- /dev/null +++ b/planE/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "os" + "planA/planE/initialization" + "planA/planE/logic" +) + +func main() { + + imgUrl := os.Args[1] + token := os.Args[2] + + err := initialization.Init() + if err != nil { + fmt.Println("初始化失败:", err) + return + } + logic.Logic(imgUrl, token) +} diff --git a/planE/modules/config/config.dll b/planE/modules/config/config.dll new file mode 100644 index 0000000..4e1d91b Binary files /dev/null and b/planE/modules/config/config.dll differ diff --git a/planE/modules/config/conifg.go b/planE/modules/config/conifg.go new file mode 100644 index 0000000..a8138f3 --- /dev/null +++ b/planE/modules/config/conifg.go @@ -0,0 +1,74 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// ConfigDLL 配置文件读取DLL结构 +type ConfigDLL struct { + dll *syscall.DLL + readConfigFile *syscall.Proc // 读取配置文件 + getVersion *syscall.Proc // 获取版本信息 + freeCString *syscall.Proc // 释放C字符串 +} + +// InitConfigDLL 初始化ConfigDLL +func InitConfigDLL() (*ConfigDLL, error) { + dllPath := filepath.Join("modules/config/", "config.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("config DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载config DLL 失败: %s", err) + } else { + return &ConfigDLL{ + dll: dll, + readConfigFile: dll.MustFindProc("ReadConfigFile"), + getVersion: dll.MustFindProc("GetVersion"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +// cStr 获取C字符串 +func (m *ConfigDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} + +// ReadConfigFile 读取配置文件 +func (m *ConfigDLL) ReadConfigFile(filePath, fileName string) (string, error) { + proc, err := m.dll.FindProc("ReadConfigFile") + if err != nil { + return "", fmt.Errorf("找不到函数 ReadConfigFile: %v", err) + } + + filePathPtr, _ := syscall.BytePtrFromString(filePath) + fileNamePtr, _ := syscall.BytePtrFromString(fileName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(filePathPtr)), + uintptr(unsafe.Pointer(fileNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} diff --git a/planE/modules/pdd/pdd.dll b/planE/modules/pdd/pdd.dll new file mode 100644 index 0000000..532e5c9 Binary files /dev/null and b/planE/modules/pdd/pdd.dll differ diff --git a/planE/modules/pdd/pdd.go b/planE/modules/pdd/pdd.go new file mode 100644 index 0000000..4f25821 --- /dev/null +++ b/planE/modules/pdd/pdd.go @@ -0,0 +1,337 @@ +package pdd + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +var ( + gPddDll *PddDLL +) + +// PddResponse 定义完整的响应结构(包含成功和失败两种情况) +type PddResponse struct { + SuccessResponse *PddSuccessResponse `json:"outer_cat_mapping_get_response,omitempty"` + ErrorResponse *PddErrorResponse `json:"error_response,omitempty"` +} +type PddSuccessResponse struct { + OuterCatMappingGetResponse PddCategoryMappingResponse `json:"outer_cat_mapping_get_response"` +} + +// PddCategoryMappingResponse 定义拼多多API响应结构(根据文档规范) +type PddCategoryMappingResponse struct { + CatID1 int64 `json:"cat_id1"` // 一级类目 ID + CatID2 int64 `json:"cat_id2"` // 二级类目 ID + CatID3 int64 `json:"cat_id3"` // 三级类目 ID + CatID4 int64 `json:"cat_id4"` // 四级类目 ID + RequestID string `json:"request_id"` // 请求 ID +} + +// PddDLL 拼多多工具DLL结构 +type PddDLL struct { + Dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} +type PddErrorResponse struct { + ErrorCode int64 `json:"error_code"` // 错误码 + ErrorMsg string `json:"error_msg"` // 错误信息 + SubCode *string `json:"sub_code"` // 子错误码 + SubMsg string `json:"sub_msg"` // 子错误信息 + RequestID string `json:"request_id"` // 请求ID +} + +// InitPddDll 初始化 pddDLL +func InitPddDll(url string) (*PddDLL, error) { + dllPath := filepath.Join(url, "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } + gPddDll = &PddDLL{ + Dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + } + return gPddDll, nil +} + +// PddGoodsOuterCatMappingGet 类目预测 +func (m *PddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsAdd 商品新增 +func (m *PddDLL) PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsAdd") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsAdd: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsAddJsonPtr, _ := syscall.BytePtrFromString(goodsAddJson) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsAddJsonPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} + +// PddGoodsSpecIdGet 生成商家自定义的规格 +func (m *PddDLL) PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsSpecIdGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSpecIdGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + parentSpecIdPtr, _ := syscall.BytePtrFromString(parentSpecId) + specNamePtr, _ := syscall.BytePtrFromString(specName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(parentSpecIdPtr)), + uintptr(unsafe.Pointer(specNamePtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsCommitDetailGet 获取商品提交的商品详情 +func (m *PddDLL) PddGoodsCommitDetailGet(clientId, clientSecret, accessToken, goodsCommitId, goodsId string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsCommitDetailGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + goodsCommitIdPtr, _ := syscall.BytePtrFromString(goodsCommitId) + goodsIdPtr, _ := syscall.BytePtrFromString(goodsId) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(goodsCommitIdPtr)), + uintptr(unsafe.Pointer(goodsIdPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddTimeGet 获取拼多多系统时间 +func (m *PddDLL) PddTimeGet(clientId, clientSecret, accessToken string) (string, error) { + proc, err := m.Dll.FindProc("PddTimeGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsCommitDetailGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsImageUpload 上传图片 +func (m *PddDLL) PddGoodsImageUpload(clientId, clientSecret, accessToken, imgBase64 string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsImageUpload") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsImageUpload: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + imgBase64Ptr, _ := syscall.BytePtrFromString(imgBase64) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(imgBase64Ptr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsListGet 获取店铺商品 +func (m *PddDLL) PddGoodsListGet(clientId, clientSecret, accessToken string, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsListGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsListGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsSaleStatusSet 设置上下架状态 +func (m *PddDLL) PddGoodsSaleStatusSet(clientId, clientSecret, accessToken, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsSaleStatusSet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSaleStatusSet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddDeleteGoodsCommit 删除商品 +func (m *PddDLL) PddDeleteGoodsCommit(clientId, clientSecret, accessToken, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddDeleteGoodsCommit") + if err != nil { + return "", fmt.Errorf("找不到函数 PddDeleteGoodsCommit: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsQuantityUpdate 更新库存 +func (m *PddDLL) PddGoodsQuantityUpdate(clientId, clientSecret, accessToken, params string) (string, error) { + + proc, err := m.Dll.FindProc("PddGoodsQuantityUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsQuantityUpdate: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// PddGoodsSkuPriceUpdate 更新价格 +func (m *PddDLL) PddGoodsSkuPriceUpdate(clientId, clientSecret, accessToken, params string) (string, error) { + proc, err := m.Dll.FindProc("PddGoodsSkuPriceUpdate") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsSkuPriceUpdate: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + paramsPtr, _ := syscall.BytePtrFromString(params) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(paramsPtr)), + ) + result := cStr(resultPtr) + return result, nil +} diff --git a/planE/modules/pdd/pdd.md b/planE/modules/pdd/pdd.md new file mode 100644 index 0000000..2c11768 --- /dev/null +++ b/planE/modules/pdd/pdd.md @@ -0,0 +1,863 @@ +# pdd.dll 使用教程 +## 1.创建DLL工具实例 +### 加载DLL文件 +```gotemplate +// PddDLL 拼多多工具DLL结构 +type pddDLL struct { + dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} + +// <初始化pddDLL> +func InitPddDLL() (*pddDLL, error) { + dllPath := filepath.Join("dll", "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } else { + return &pddDLL{ + dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +dll, err := InitPddDLL() +``` + +### 获取C字符串 +```gotemplate +// cStr 获取C字符串 +func (m *pddDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} +``` + +## 2. 使用dll函数示例 +```gotemplate +// 类目预测 +func (m *pddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} +``` + +# 接口详情 +## 1. 类目预测--PddGoodsOuterCatMappingGet +### 请求信息 +```gotemplate +dll.PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, +outerCatId, outerCatName, outerGoodsName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| outerCatId | string | 是 | 外部平台类目ID | +| outerCatName | string | 是 | 外部平台类目名称 | +| outerGoodsName | string | 是 | 外部商品名称 | +### 响应示例 +```json +{ + "outer_cat_mapping_get_response": { + "cat_id2": 16028, + "cat_id3": 16031, + "cat_id1": 15543, + "request_id": "17666480184871649", + "cat_id4": 0 + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 2. 快递公司查看--PddLogisticsCompaniesGet +### 请求信息 +```gotemplate +dll.PddLogisticsCompaniesGet(clientId, clientSecret) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +### 响应示例 +```json +{ + "logistics_companies_get_response": { + "logistics_companies": [ + { + "available": 1, + "code": "SF", + "id": 1, + "logistics_company": "顺丰速运" + }, + { + "available": 1, + "code": "STO", + "id": 2, + "logistics_company": "申通快递" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 3. erp打单信息同步--PddErpOrderSync +### 请求信息 +```gotemplate +dll.PddErpOrderSync(clientId, clientSecret, accessToken, logisticsId, +orderSn, orderState, waybillNo) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsId | string | 是 | 物流公司ID | +| orderSn | string | 是 | 拼多多订单号 | +| orderState | string | 是 | 订单状态 | +| waybillNo | string | 是 | 运单号 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871650" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 4. 拼多多订单同步--PddOrderSynchronization +### 请求信息 +```gotemplate +dll.PddOrderSynchronization(clientId, clientSecret, accessToken, logisticsCompany, logisticsOnlineSendJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsCompany | string | 是 | 物流公司名称 | +| logisticsOnlineSendJson | string | 是 | 拼多多订单同步json字符串 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871651" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 5. 商品图片上传接口--PddGoodsImgUpload +### 请求信息 +```gotemplate +dll.PddGoodsImgUpload(clientId, clientSecret, accessToken, filePath) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +### 响应示例 +```json +{ + "goods_img_upload_response": { + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "request_id": "17666480184871652" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 6. 商品新增接口--PddGoodsAdd +### 请求信息 +```gotemplate +dll.PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| goodsAddJson | string | 是 | 商品信息JSON字符串 | +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456789, + "goods_name": "测试商品", + "goods_sn": "G202501200001", + "request_id": "17666480184871653" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 7. 联合拼多多图片上传的商品新增--SelfPddGoodsAdd +### 请求信息 +```gotemplate +dll.SelfPddGoodsAdd(clientId, clientSecret, accessToken, filePath, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +| goodsAddJson | string | 是 | 商品信息JSON字符串(不需包含image_url)| +#### 接口说明 +此接口为组合接口,内部执行以下步骤: +1.上传商品主图文件到拼多多服务器 +2.获取图片URL并自动填充到商品信息中 +3.调用商品新增接口创建商品 +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456790, + "goods_name": "测试商品", + "goods_sn": "G202501200002", + "request_id": "17666480184871654" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 8. 批量数据解密脱敏接口--PddOpenDecryptMaskBatch +### 请求信息 +```gotemplate +dll.PddOpenDecryptMaskBatch(clientId, clientSecret, accessToken, reqJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| reqJson | string | 是 | 信息JSON字符串 | +#### 信息JSON结构示例 +```json +[ + { + "data_tag": "251229-272441044622514", + "encrypted_data": "~AgAAAAPlscEH0psOJAEXpTdsLOWvDJ9bB7IEjIoqNfiDhhJR9NHOxsdZ+PEFluSSCngCikoDU+CP/sSXZJ92ic7+PdNlJNLA7g/6VUMDWF6RvjW9IeRN+lKNarsjWDQR~0~" + } +] +``` +### 响应示例 +```json +{ + "open_decrypt_mask_batch_response": { + "data_decrypt_list": [ + { + "data_tag": "str", + "data_type": 0, + "decrypted_data": "str", + "encrypted_data": "str", + "error_code": 0, + "error_msg": "str" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 生成商家自定义的规格--PddGoodsSpecIdGet +### 请求信息 +```gotemplate +dll.PddGoodsSpecIdGet(clientId, clientSecret, accessToken, parentSpecId, specName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| parentSpecId | string | 是 | 拼多多标准规格ID | +| specName | string | 是 | 商家编辑的规格值,如颜色规格下设置白色属性 | +### 响应参数 +```json +{ + "goods_spec_id_get_response": { + "parent_spec_id": 0, + "spec_id": 0, + "spec_name": "str" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 修改商品SKU价格--PddGoodsSkuPriceUpdate +### 请求信息 +```gotemplate +dll.PddGoodsSkuPriceUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 价格更新请求JSON字符串 | +#### 请求JSON结构 +```json +{ + "goods_id": "必填,商品id,类型为LONG", + "ignore_edit_warn": "非必填,是否获取商品发布警告信息,默认为忽略,类型为BOOLEAN", + "market_price": "非必填,参考价(单位分),类型为LONG", + "market_price_in_yuan": "非必填,参考价(单位元),类型为STRING", + "sku_price_list": [ + { + "group_price": "非必填,拼团购买价格(单位分),类型为LONG", + "is_onsale": "非必填,sku上架状态,0-已下架,1-上架中,类型为INTEGER", + "single_price": "非必填,单独购买价格(单位分),类型为LONG", + "sku_id": "必填,sku标识,类型为LONG" + } + ], + "sync_goods_operate": "非必填,提交后上架状态,0:上架,1:保持原样,类型为INTEGER", + "two_pieces_discount": "非必填,满2件折扣,可选范围0-100,0表示取消,95表示95折,设置需先查询规则接口获取实际可填范围,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_update_sku_price_response": { + "goods_commit_id": 0, + "is_success": true + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 商品库存更新接口--PddGoodsQuantityUpdate +### 请求信息 +```gotemplate +dll.PddGoodsQuantityUpdate(clientId, clientSecret, accessToken, request) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| request | string | 是 | 库存更新请求JSON字符串 | +#### 请求JSON结构 request 字符串 +```json +{ + "force_update": "非必填,是否强制更新,仅update_type=1(全量更新)时有效,默认值false;force_update=false时,quantity不能小于预扣库存;force_update=true时,代表强制更新,当quantity<预扣库存时,不报错,直接将quantity清0,类型为BOOLEAN", + "goods_id": "必填,商品id,类型为LONG", + "outer_id": "非必填,sku商家编码,类型为STRING", + "quantity": "必填,库存修改值。当全量更新库存时,quantity必须为大于等于0的正整数;当增量更新库存时,quantity为整数,可小于等于0。若增量更新时传入的库存为负数,则负数与实际库存之和不能小于0。比如当前实际库存为1,传入增量更新quantity=-1,库存改为0,类型为LONG", + "sku_id": "非必填,sku_id和outer_id必填一个,类型为LONG", + "update_type": "非必填,库存更新方式,可选。1为全量更新,2为增量更新。如果不填,默认为全量更新,类型为INTEGER" +} +``` +### 响应参数 +```json +{ + "goods_quantity_update_response": { + "is_success": false + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 获取商品信息接口 -- OutPddAuthGetCommitDetailt +### 请求信息 +```gotemplate +dll.OutPddAuthGetCommitDetailt(goodsCommitId, goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsCommitId | string | 是 | 商品提交ID | +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json + +``` + + +## 获取商品详情信息接口 -- OutPddAuthGetGoodsDetail +### 请求信息 +```gotemplate +dll.OutPddAuthGetGoodsDetail(goodsId, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| goodsId | string | 是 | 商品ID | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "bad_fruit_claim": 0, + "buy_limit": 999999, + "carousel_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2023-09-07/02a5c39a-7a90-4530-a338-3e87095a21a9.png", + "https://img.pddpic.com/open-gw/2025-06-30/4539f740-331b-4687-aa00-5c96855de6cd.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/b0e89e39-c97b-475d-9be2-f1909e30acb5.jpeg" + ], + "cat_id": 15678, + "cost_template_id": 655688447565777, + "country_id": 0, + "customer_num": 2, + "customs": "", + "detail_gallery_list": [ + "https://img.pddpic.com/open-gw/2025-06-30/b691c104-baf8-42b2-97e2-b7258113114b.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/53e6f7ff-d15e-4e8f-8625-e293717ca1e4.jpeg", + "https://img.pddpic.com/open-gw/2023-09-07/ecff591d-32a6-42c9-ba5a-6a42829092a8.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/7034f8a0-5d88-49f8-a96f-608abb8cac80.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/e10c2b6c-d4de-4fdd-8d48-f0a334735e9a.jpeg", + "https://img.pddpic.com/open-gw/2023-10-16/c19358fb-0a4d-49ad-bcc8-b2980e938064.jpeg", + "https://img.pddpic.com/open-gw/2025-06-30/1deeb9c0-7212-432b-a309-f774db6e1adb.jpeg" + ], + "goods_desc": "书名:金属工艺学 下 第6版,作者:'邓文英,宋力宏主编',ISBN:9787040456295,出版社:高等教育出版社", + "goods_id": 770621582375, + "goods_name": "金属工艺学 下 第6版 邓文英,宋力宏主编 高等教育出版社 978", + "goods_property_list": [ + { + "punit": "", + "ref_pid": 425, + "template_pid": 401030, + "vid": 0, + "vvalue": "9787040456295" + }, + { + "punit": "", + "ref_pid": 876, + "template_pid": 401029, + "vid": 0, + "vvalue": "金属工艺学 下 第6版" + }, + { + "punit": "页", + "ref_pid": 692, + "template_pid": 401032, + "vid": 0, + "vvalue": "157" + }, + { + "punit": "元", + "ref_pid": 879, + "template_pid": 401034, + "vid": 0, + "vvalue": "24.70" + }, + { + "punit": "", + "ref_pid": 882, + "template_pid": 401037, + "vid": 0, + "vvalue": "邓文英,宋力宏主编" + }, + { + "punit": "", + "ref_pid": 880, + "template_pid": 401035, + "vid": 483761, + "vvalue": "高等教育出版社" + }, + { + "punit": "", + "ref_pid": 888, + "template_pid": 401043, + "vid": 0, + "vvalue": "平装" + } + ], + "goods_type": 1, + "image_url": "", + "invoice_status": 0, + "is_customs": 0, + "is_folt": 0, + "is_group_pre_sale": 0, + "is_pre_sale": 0, + "is_refundable": 1, + "is_sku_pre_sale": 0, + "market_price": 5948, + "order_limit": 999999, + "outer_goods_id": "9787040456295", + "oversea_type": 0, + "pre_sale_time": 0, + "privacy_delivery": 0, + "quan_guo_lian_bao": 0, + "second_hand": 1, + "shipment_limit_second": 172800, + "sku_list": [ + { + "is_onsale": 1, + "limit_quantity": 999999, + "multi_price": 1487, + "out_sku_sn": "9787040456295", + "price": 1587, + "quantity": 0, + "reserve_quantity": 0, + "sku_id": 1753931570290, + "sku_pre_sale_time": 0, + "spec": [ + { + "parent_id": 1216, + "parent_name": "尺寸", + "spec_id": 27632894279, + "spec_name": "单本 无附赠 超七天不退换" + } + ], + "thumb_url": "https://img.pddpic.com/open-gw/2025-06-30/59c30d4c-193f-40a3-a639-7af59a381ec5.jpeg", + "weight": 500 + } + ], + "status": 4, + "tiny_name": "金属工艺学 下 第6", + "two_pieces_discount": 96, + "video_gallery": [], + "warehouse": "", + "warm_tips": "", + "zhi_huan_bu_xiu": 0 +} +``` + +## 生成自定义规格接口 -- OutPddAuthSetSpec +### 请求信息 +```gotemplate +dll.OutPddAuthSetSpec(specTypeId, specName, accessToken) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| specTypeId | int | 是 | 规格类型ID | +| specName | string | 是 | 规格名称 | +| accessToken | string | 是 | 授权令牌 | +### 响应参数 +```json +{ + "parentSpecId": 3820, + "specName": "全新", + "specId": 1080396526 +} +``` + +## 修改价格接口 -- OutPddAuthUpdatePrice +### 请求信息 +```gotemplate +dll.OutPddAuthUpdatePrice(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 修改库存接口 -- OutPddAuthUpdateStock +### 请求信息 +```gotemplate +dll.OutPddAuthUpdateStock(jsonData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|-----------------| +| jsonData | int | 是 | 价格修改信息JSON字符串 | +### 响应参数 +```json +[ + { + "success": true, + "msg": "操作成功" + }, + { + "success": false, + "msg": "操作失败" + } +] +``` + +## 12.释放C字符串内存--FreeCString +### 请求信息 +```gotemplate +dll.FreeCString(str) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| str | string | 是 | 需要释放的字符串 | diff --git a/planF/config.yaml b/planF/config.yaml new file mode 100644 index 0000000..0f78fa9 --- /dev/null +++ b/planF/config.yaml @@ -0,0 +1,111 @@ +server: + port: "8080" #服务器端口 + f_port : "8284" #F程序端口 + filter: 1 #是否开启违禁词过滤器 0=关闭 1=开启 + replace_mark: "0" #标题违规词是否替换* 0 不替换 1 替换(替换会继续发布,不替换则不发布) + redis_exp: 192 #redis过期时间 192小时(8天) + read_db: "mysql" #读数据库 mysql sqlite + err_pause_time: 3000 #错误暂停时间(毫秒) + sign_key: "jRQdCh52Z55Kzh1hADaA2ZtdTKetj2PXk60Tz5Yc0iz9aD8Wafbk7CwAZ8cz69A9zb9caZ3k9dnR3Ys06J5nYFPrZ0xE9p6TY8DCD538ryiRjW81YTPmk41tCEnXizPh" #签名密钥 + data_day: 2 #数据保存时间(天) + is_c: true #是否启动 C程序 +speed: #限速器 + pdd_speed: 18 #拼多多 每秒多少个任务 + xianyu_speed: 5 #闲鱼 每秒多少个任务 + watermark: 15 #打水印速率的个数 +minio: #minio 图片空间 + url: "103.236.68.64:19000" #minio地址 + access_key_id: "minio" #minio keyId + secret_access_key: "bhkXyaD2WdAF7C6z" #minio key + bucket_name: "my-pics" #存储桶 + target_dir: "test/2025" #目标目录 + use_ssl: false #是否使用 SSL +alive: + fluent: 50 #存活状态-流畅时间(毫秒) + slow: 200 #存活状态-缓慢时间(毫秒) +pool_config: + size: 500 #协程数量 + with_expiry_duration: 10 #过期时间 + with_pre_alloc: true #预分配 + with_max_blocking_tasks: 2000 #阻塞任务数 + with_nonblocking : true #非阻塞 +mysql_config: + db_name: "task_user" #数据库名称 + user: "root" #数据库用户名 + password: "root" #数据库密码 + host: "127.0.0.1" #数据库地址 + port: 3306 #数据库端口 + loglevel: "silent" #数据库日志级别 info=开发环境:输出所有日志(SQL、执行时间等) warn=预发布:输出警告+错误 error=生产环境:只输出错误日志 silent=生产环境:关闭所有日志 + max_retry_times: 3 #数据库重试次数 + base_retry_interval: 100 #数据库重试间隔(毫秒) + max_retry_interval: 3 #数据库最大重试间隔(秒) + max_open_conns: 100 #数据库最大打开连接数 + max_idle_conns: 20 #数据库最大空闲连接数 + conn_max_idle_time: 30 #数据库连接最大空闲时间(分钟) + conn_max_lifetime: 1 #数据库连接最大生命周期(小数) +redis_config: + - db_name: "任务池" + db: 0 + addr: "127.0.0.1:6379" + password: "123456" + - db_name: "书品库" + db: 7 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "店铺信息" + db: 8 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "出版社信息列表" + db: 3 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "省市区列表" + db: 4 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有图片的 isbn" + db: 5 + addr: "36.212.12.247:6379" + password: "long6166@@" + - db_name: "没有书籍的 isbn" + db: 6 + addr: "36.212.12.247:6379" + password: "long6166@@" +pdd_config: + client_id: "203c5a7ba8bd4b8488d5e26f93052642" #拼多多clientId + client_secret: "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" #拼多多clientSecret +kfz_config: + app_id: 576 #孔夫子appid + app_secret: "256e10220c5b307f5172b1a49c11467a6cfa8038bbe2a7feccc42231852324f8" #孔夫子appsecret +http_url: + task_url: "http://127.0.0.1:8080" #A 程序接口地址 +file_url: + xian_yu_dll: "D:\\source\\planA\\planB\\modules\\xianYu" #闲鱼 DLL库路径 + pdd_dll: "D:\\source\\planA\\planB\\modules\\pdd" #拼多多 DLL库路径 + kfz_dll: "D:\\source\\planA\\planB\\modules\\kfz" #孔夫子 DLL库路径 + log_dll: "D:\\source\\planA\\planB\\modules\\logs" #日志 DLL库路径 + image_dll: "D:\\source\\planA\\planB\\modules\\image" #水印 DLL库路径 + b_file_name: "D:\\source\\planA\\planB\\planB.exe" #B 程序文件路径 + c_file_name: "D:\\source\\planA\\planC\\planC.exe" #C 程序文件路径 + d_file_name: "D:\\source\\planA\\planD\\planD.exe" #D 程序文件路径 + e_file_name: "D:\\source\\planA\\planE\\planE.exe" #E 程序文件路径 + create_task_url: "https://api.buzhiyushu.cn/zhishu/baseInfo/addNewTask" #新增任务接口 + create_task_notice_url: "http://192.168.101.127:8055/task" #核价软件提交数据通知接口 + create_operation_task_notice_url: "http://192.168.101.127:8055/taskV2" #操作商品任务核价软件提交数据通知接口 + banned_word_substitution_url : "http://36.212.12.247:13001/task/getFilterSetNew" #违禁词替换接口 + pdd_token_url: "https://api.buzhiyushu.cn/huidiao/pdd/getToken" #获取系统规定拼多多 token + deduction_url: "https://api.buzhiyushu.cn/zhishu/userRecharge/apiBalancePayment" #扣费接口 + pdd_get_goods_url: "http://pdd.buzhiyushu.cn/api/pdd/auth/getShopGoodsList" #查询拼多多商品接口 + pdd_get_goods_detail_url: "http://192.168.101.127:8085/api/pdd/auth/newGetShopGoodsDetailList" #查询拼多多商品详情列表接口 + pdd_add_goods_url: "http://192.168.101.127:18099/task/putShopGoods" #添加拼多多商品接口 + pdd_get_sku_id: "http://192.168.101.127:18099/shopGoods/getShopGoods" #批量获取 skuId接口 + xianyu_add_goods_url: "http://192.168.101.127:18099/task/putShopGoods" #添加闲鱼商品接口 + kfz_add_goods_url: "http://119.45.237.193:14009/task/kfzverifyPricePublishGoods" #添加孔夫子商品接口 + del_task_url: "http://119.45.237.193:14008/shopGoods/delShopGoodsk" #删除任务通知接口 + backup_url: "C:\\file\\backup" #备份文件路径 + pdd_goods_details_url: "D:\\file\\pdd_goods_details" #保存拼多多详情路径 + update_token_url: "http://146.56.227.42:9099/api/updateToken" #更新拼多多 token 到redis + kfz_img_temp_url: "D:\\file\\kfzImg" #孔夫子图片临时路径 + kfz_img_http_url: "https://www0.kfzimg.com/" #孔夫子图片 http 路径 + get_pdd_goods_shopid_isbn_url : "http://192.168.101.127:18099/shopGoods/selectTrilateralIds" #获取拼多多商品 shopId 和 isbn \ No newline at end of file diff --git a/planF/controller/api.go b/planF/controller/api.go new file mode 100644 index 0000000..a9ddf70 --- /dev/null +++ b/planF/controller/api.go @@ -0,0 +1,60 @@ +package controller + +import ( + "bytes" + "io" + "net/http" + "planA/planF/tool" +) + +func Test(httpMsg http.ResponseWriter, data *http.Request) { + // 1. 读取接收到的POST JSON数据 + body, err := io.ReadAll(data.Body) + if err != nil { + errMsg := "读取请求数据失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + // 2. 以同样的方式请求目标URL + url := "http://36.212.8.40:8539/api/sales/query" + + // 创建新的请求 + req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + errMsg := "创建请求失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 复制原始请求的Header(可选,根据需要) + req.Header.Set("Content-Type", "application/json") + // 如果需要传递其他headers,可以复制 + for key, values := range data.Header { + for _, value := range values { + req.Header.Add(key, value) + } + } + + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + errMsg := "请求目标URL失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + // 3. 读取目标URL返回的数据 + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + errMsg := "读取响应数据失败: " + err.Error() + tool.Error(httpMsg, errMsg, http.StatusInternalServerError) + return + } + + // 4. 返回目标URL返回的原始数据 + httpMsg.Header().Set("Content-Type", "application/json") + httpMsg.WriteHeader(resp.StatusCode) + httpMsg.Write(responseBody) +} diff --git a/planF/initialization/config/config.go b/planF/initialization/config/config.go new file mode 100644 index 0000000..033a271 --- /dev/null +++ b/planF/initialization/config/config.go @@ -0,0 +1,108 @@ +package config + +import ( + "encoding/json" + "fmt" + _configDll "planA/modules/config" + "planA/planF/initialization/golabl" + "planA/planF/tool" + _type "planA/type" +) + +var ( + gDir string +) + +// Init 初始化 +// @param dir string 配置文件目录 +// @return _type.Config 配置文件信息 +// @return error 错误信息 +func Init(dir string) error { + gDir = dir + // 判断 ctx 是否取消 + checkContextErr := tool.CheckContext(golabl.Ctx) + // 判断 结果 + if checkContextErr != nil { + // 返回 且 返回错误 + return checkContextErr + } + //读取配置文件 + var config _type.Config + dll, initConfigDLLErr := _configDll.InitConfigDLL() + if initConfigDLLErr != nil { + return initConfigDLLErr + } + configJson, ReadConfigFileErr := dll.ReadConfigFile(dir, "config.yaml") + if ReadConfigFileErr != nil { + return fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + golabl.Config = config + return nil +} + +// GetPddClient 获取拼多多配置 +// @return _type.PddConfig 拼多多配置 +// @return error 错误信息 +func GetPddClient() (_type.PddConfig, error) { + //读取配置文件 + var config _type.Config + dll, initConfigDLLErr := _configDll.InitConfigDLL() + if initConfigDLLErr != nil { + return _type.PddConfig{}, initConfigDLLErr + } + configJson, ReadConfigFileErr := dll.ReadConfigFile(gDir, "config.yaml") + if ReadConfigFileErr != nil { + return _type.PddConfig{}, fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return _type.PddConfig{}, fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + return config.PddConfig, nil +} + +// GetFileUrlConfig 获取文件路径配置 +// @return _type.FileUrl 文件路径配置 +// @return error 错误信息 +func GetFileUrlConfig() (_type.FileUrl, error) { + //读取配置文件 + var config _type.Config + dll, initConfigDLLErr := _configDll.InitConfigDLL() + if initConfigDLLErr != nil { + return _type.FileUrl{}, initConfigDLLErr + } + configJson, ReadConfigFileErr := dll.ReadConfigFile(gDir, "config.yaml") + if ReadConfigFileErr != nil { + return _type.FileUrl{}, fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return _type.FileUrl{}, fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + return config.FileUrl, nil +} + +// GetAliveConfig 获取存活状态配置 +// @return _type.Alive 存活状态配置 +// @return error 错误信息 +func GetAliveConfig() (_type.Alive, error) { + //读取配置文件 + var config _type.Config + dll, initConfigDLLErr := _configDll.InitConfigDLL() + if initConfigDLLErr != nil { + return _type.Alive{}, initConfigDLLErr + } + configJson, ReadConfigFileErr := dll.ReadConfigFile(gDir, "config.yaml") + if ReadConfigFileErr != nil { + return _type.Alive{}, fmt.Errorf("读取配置文件失败:%v", ReadConfigFileErr) + } + jsonUnmarshalErr := json.Unmarshal([]byte(configJson), &config) + if jsonUnmarshalErr != nil { + return _type.Alive{}, fmt.Errorf("解析配置文件失败:%v", jsonUnmarshalErr) + } + return config.Alive, nil +} diff --git a/planF/initialization/golabl/golabl.go b/planF/initialization/golabl/golabl.go new file mode 100644 index 0000000..e1cd3d3 --- /dev/null +++ b/planF/initialization/golabl/golabl.go @@ -0,0 +1,14 @@ +package golabl + +import ( + "context" + _type "planA/type" + + "github.com/gorilla/mux" +) + +var ( + Ctx context.Context + Config _type.Config + Router = mux.NewRouter() +) diff --git a/planF/initialization/init.go b/planF/initialization/init.go new file mode 100644 index 0000000..c21a070 --- /dev/null +++ b/planF/initialization/init.go @@ -0,0 +1,42 @@ +package initialization + +import ( + "context" + "fmt" + "log" + "net/http" + "planA/planF/initialization/config" + "planA/planF/initialization/golabl" + "planA/planF/initialization/router" +) + +func Init() error { + //初始化上下文 + golabl.Ctx = context.Background() + // 初始化配置 + configErr := config.Init("") + if configErr != nil { + return fmt.Errorf("初始化配置失败: %v", configErr) + } + //初始化路由 + router.Init() + return nil +} + +// Server 启动服务 +func Server() { + // 从配置获取端口并启动服务 + port := ":" + golabl.Config.Server.FPort + fmt.Printf("服务器启动在 http://localhost%s\n", port) + // 打印所有可用端点(控制台输出) + printAvailableEndpoints() + // 启动HTTP服务,如果失败则记录致命错误 + log.Fatal(http.ListenAndServe(port, golabl.Router)) +} + +// printAvailableEndpoints 打印所有可用的API端点 +func printAvailableEndpoints() { + fmt.Println("\n========== 可用API端点 ==========") + + fmt.Println("\n=====================================") +} diff --git a/planF/initialization/router/router.go b/planF/initialization/router/router.go new file mode 100644 index 0000000..46921da --- /dev/null +++ b/planF/initialization/router/router.go @@ -0,0 +1,8 @@ +package router + +import "planA/planF/router" + +// Init 初始化路由 +func Init() { + router.ApiInir() +} diff --git a/planF/main.go b/planF/main.go new file mode 100644 index 0000000..0a466bd --- /dev/null +++ b/planF/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "planA/planF/initialization" +) + +func main() { + // 初始化 + err := initialization.Init() + if err != nil { + fmt.Println("初始化失败:", err) + return + } + //启动服务 + initialization.Server() +} diff --git a/planF/modules/config/config.dll b/planF/modules/config/config.dll new file mode 100644 index 0000000..4e1d91b Binary files /dev/null and b/planF/modules/config/config.dll differ diff --git a/planF/modules/config/conifg.go b/planF/modules/config/conifg.go new file mode 100644 index 0000000..a8138f3 --- /dev/null +++ b/planF/modules/config/conifg.go @@ -0,0 +1,74 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// ConfigDLL 配置文件读取DLL结构 +type ConfigDLL struct { + dll *syscall.DLL + readConfigFile *syscall.Proc // 读取配置文件 + getVersion *syscall.Proc // 获取版本信息 + freeCString *syscall.Proc // 释放C字符串 +} + +// InitConfigDLL 初始化ConfigDLL +func InitConfigDLL() (*ConfigDLL, error) { + dllPath := filepath.Join("modules/config/", "config.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("config DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载config DLL 失败: %s", err) + } else { + return &ConfigDLL{ + dll: dll, + readConfigFile: dll.MustFindProc("ReadConfigFile"), + getVersion: dll.MustFindProc("GetVersion"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +// cStr 获取C字符串 +func (m *ConfigDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} + +// ReadConfigFile 读取配置文件 +func (m *ConfigDLL) ReadConfigFile(filePath, fileName string) (string, error) { + proc, err := m.dll.FindProc("ReadConfigFile") + if err != nil { + return "", fmt.Errorf("找不到函数 ReadConfigFile: %v", err) + } + + filePathPtr, _ := syscall.BytePtrFromString(filePath) + fileNamePtr, _ := syscall.BytePtrFromString(fileName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(filePathPtr)), + uintptr(unsafe.Pointer(fileNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} diff --git a/planF/router/api.go b/planF/router/api.go new file mode 100644 index 0000000..cdd4b9e --- /dev/null +++ b/planF/router/api.go @@ -0,0 +1,12 @@ +package router + +import ( + "planA/planF/controller" + "planA/planF/initialization/golabl" +) + +// ApiInir 初始化api路由 +func ApiInir() { + adminExportRouter := golabl.Router.PathPrefix("/api").Subrouter() + adminExportRouter.HandleFunc("/test", controller.Test).Methods("POST") // 删除 redis中指定任务 +} diff --git a/planF/tool/tool.go b/planF/tool/tool.go new file mode 100644 index 0000000..c404990 --- /dev/null +++ b/planF/tool/tool.go @@ -0,0 +1,45 @@ +package tool + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" +) + +// CheckContext 检查上下文是否取消 +func CheckContext(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() // 返回取消原因 + default: + return nil // 上下文仍然有效 + } +} + +// Success 成功响应 +// @param httpMsg http.ResponseWriter +// @param data 返回的数据 +func Success(httpMsg http.ResponseWriter, data any) { + ret := map[string]interface{}{ + "code": "200", + "msg": "成功", + "data": data, + } + json.NewEncoder(httpMsg).Encode(ret) +} + +// Error 错误响应 +// @param httpMsg http.ResponseWriter +// @param msg 错误信息 +// @param code 错误码 +func Error(httpMsg http.ResponseWriter, msg string, code int) { + fmt.Println("错误:" + msg) + codeStr := strconv.FormatInt(int64(code), 10) + ret := map[string]interface{}{ + "code": codeStr, + "msg": msg, + } + json.NewEncoder(httpMsg).Encode(ret) +} diff --git a/rep/factory.go b/rep/factory.go new file mode 100644 index 0000000..2c65b52 --- /dev/null +++ b/rep/factory.go @@ -0,0 +1,49 @@ +package rep + +import ( + "planA/initialization/golabl" + "planA/rep/i" + "planA/rep/impl/mysql" + "planA/rep/impl/sqlite" +) + +type Db interface { + i.TaskExport + i.TaskRecords +} + +// CreateDbFactoryWrite 创建写数据库工厂 +// @return Db mysql写数据库 +// @return Db sqlite写数据库 +func CreateDbFactoryWrite() (Db, Db) { + return &mysql.GormAdapter{ + DB: golabl.MysqlDb, + }, &sqLite.SqlAdapter{ + DB: golabl.SqliteDb, + } +} + +// CreateDbFactoryRead 创建读数据库工厂 +// @return Db 读数据库 +func CreateDbFactoryRead() Db { + var read Db + read = &mysql.GormAdapter{ + DB: golabl.MysqlDb, + } + if golabl.Config.Server.ReadDb == "sqlite" { + read = &sqLite.SqlAdapter{ + DB: golabl.SqliteDb, + } + } + return read +} + +// CreateDbFactorySqliteRead 创建sqlite读数据库工厂 +// @return Db 读数据库 +func CreateDbFactorySqliteRead() Db { + var read Db + read = &sqLite.SqlAdapter{ + DB: golabl.SqliteDb, + } + return read +} diff --git a/rep/i/taskExport.go b/rep/i/taskExport.go new file mode 100644 index 0000000..6c41fe9 --- /dev/null +++ b/rep/i/taskExport.go @@ -0,0 +1,15 @@ +package i + +import ( + _type "planA/type" +) + +type TaskExport interface { + CreateTaskExport(export _type.TaskExportDTO) error //创建导出任务 + GetTaskExportList(page, pageSize int, userId string) ([]*_type.TaskExportDTO, int64, error) //获取导出任务列表 + GetTaskExportByTaskId(taskId string) (_type.TaskExportDTO, error) //根据任务 ID获取导出任务 + UpdateTaskExport(export _type.TaskExportDTO) error //更新导出任务 + UpdateTaskExportStatus(taskId string, status int64, fileUrl string) error + GetTaskExportOldList() ([]*_type.TaskExportDTO, error) //获取导出任务旧数据 + DeleteTaskExportOldData() error //删除导出任务旧数据 +} diff --git a/rep/i/taskRecords.go b/rep/i/taskRecords.go new file mode 100644 index 0000000..542e68e --- /dev/null +++ b/rep/i/taskRecords.go @@ -0,0 +1,18 @@ +package i + +import ( + _type "planA/type" +) + +type TaskRecords interface { + CreateTaskRecords(user _type.TaskRecordsDTO) error //创建任务记录 + GetTaskRecordsList(params _type.GetTaskRecordsListReq) ([]*_type.TaskRecordsDTO, int64, error) //获取任务记录列表 + GetTaskRecordsByTaskId(taskId string) (*_type.TaskRecordsDTO, error) //根据任务 ID获取任务记录 + UpdateTaskRecords(user _type.TaskRecordsDTO) error //更新任务记录 + GetTaskRecordsOldList() ([]*_type.TaskRecordsDTO, error) //获取任务记录旧数据列表 + DeleteTaskRecordsOldData() error //删除任务记录旧数据 + DeleteTaskRecordsByTaskId(taskId string) error //根据任务 ID删除任务记录 + GetTaskRecords24Hour() ([]*_type.TaskRecordsDTO, error) //获取24小时内的数据 + GetTaskByShopIdAndTaskType(taskId string, taskType int64) ([]*_type.TaskRecordsDTO, error) //根据 shopId和 taskType获取任务记录 + GetAllTask() ([]*_type.TaskRecordsDTO, error) //获取所有任务 +} diff --git a/rep/impl/mysql/struct.go b/rep/impl/mysql/struct.go new file mode 100644 index 0000000..ec6f376 --- /dev/null +++ b/rep/impl/mysql/struct.go @@ -0,0 +1,9 @@ +package mysql + +import ( + "gorm.io/gorm" +) + +type GormAdapter struct { + DB *gorm.DB +} diff --git a/rep/impl/mysql/taskExport.go b/rep/impl/mysql/taskExport.go new file mode 100644 index 0000000..a6b9f19 --- /dev/null +++ b/rep/impl/mysql/taskExport.go @@ -0,0 +1,107 @@ +package mysql + +import ( + "database/sql" + mysqlServer "planA/service/mysql" + _type "planA/type" + mysqlType "planA/type/mysql" +) + +// CreateTaskExport 创建任务导出 +// @param export 任务导出 +// @return error 错误信息 +func (g *GormAdapter) CreateTaskExport(export _type.TaskExportDTO) error { + _, err := mysqlServer.CreateTaskExport(mysqlType.TaskExport{ + ID: export.Id, + UserID: &export.UserId, + ShopID: &export.ShopId, + TaskID: &export.TaskId, + ShopName: &export.ShopName, + FileUrl: &export.FileUrl, + Status: &export.Status, + Total: &export.Total, + CompleteAt: &export.CompleteAt, + }) + return err +} + +// GetTaskExportList 获取任务导出列表 +// @param params 查询参数 +// @return []mysqlType.TaskExportDTO 任务导出列表 +// @return error 错误信息 +func (g *GormAdapter) GetTaskExportList(page, pageSize int, userId string) ([]*_type.TaskExportDTO, int64, error) { + list, count, err := mysqlServer.GetTaskExportList(page, pageSize, userId) + listDTO := convertMysqlTaskExportToDTO(list) + return listDTO, count, err +} + +// GetTaskExportByTaskId 根据任务 ID获取导出任务 +// @param taskId 任务 ID +// @return *mysqlType.TaskExportDTO 导出任务 +// @return error 错误信息 +func (g *GormAdapter) GetTaskExportByTaskId(taskId string) (_type.TaskExportDTO, error) { + return _type.TaskExportDTO{}, nil +} + +// UpdateTaskExport 更新导出任务 +// @param export 导出任务 +// @return error 错误信息 +func (g *GormAdapter) UpdateTaskExport(export _type.TaskExportDTO) error { + err := mysqlServer.UpdateTaskExport(mysqlType.TaskExport{ + UserID: &export.UserId, + ShopID: &export.ShopId, + TaskID: &export.TaskId, + ShopName: &export.ShopName, + FileUrl: &export.FileUrl, + Status: &export.Status, + Total: &export.Total, + CompleteAt: &export.CompleteAt, + }) + return err +} + +// GetTaskExportOldList 获取任务导出旧数据列表 +func (g *GormAdapter) GetTaskExportOldList() ([]*_type.TaskExportDTO, error) { + list, err := mysqlServer.GetOldExportSQLite() + listDTO := convertMysqlTaskExportToDTO(list) + return listDTO, err +} + +// DeleteTaskExportOldData 删除任务导出旧数据 +func (g *GormAdapter) DeleteTaskExportOldData() error { + return mysqlServer.DeleteOldExport() +} + +// UpdateTaskExportStatus 更新任务导出状态 +// @param taskId 任务 ID +// @param status 状态 +// @param fileUrl 文件路径 +// @return error 错误信息 +func (s *GormAdapter) UpdateTaskExportStatus(taskId string, status int64, fileUrl string) error { + return mysqlServer.UpdateTaskExportStatus(taskId, status, fileUrl) +} + +func convertMysqlTaskExportToDTO(records []mysqlType.TaskExport) []*_type.TaskExportDTO { + dtos := make([]*_type.TaskExportDTO, len(records)) + for i, r := range records { + dtos[i] = &_type.TaskExportDTO{ + Id: r.ID, + UserId: *r.UserID, + ShopId: *r.ShopID, + TaskId: *r.TaskID, + ShopName: *r.ShopName, + FileUrl: *r.FileUrl, + Status: *r.Status, + Total: *r.Total, + // 安全处理 CompleteAt + CompleteAt: func() sql.NullTime { + if r.CompleteAt != nil { + return *r.CompleteAt + } + return sql.NullTime{Valid: false} + }(), + CreateAt: *r.CreateAt, + } + } + return dtos +} diff --git a/rep/impl/mysql/taskRecords.go b/rep/impl/mysql/taskRecords.go new file mode 100644 index 0000000..72b7ba0 --- /dev/null +++ b/rep/impl/mysql/taskRecords.go @@ -0,0 +1,143 @@ +package mysql + +import ( + mysqlServer "planA/service/mysql" + _type "planA/type" + mysqlType "planA/type/mysql" + "time" +) + +// CreateTaskRecords 创建任务记录 +// @param export 任务记录 +// @return error 错误信息 +func (g *GormAdapter) CreateTaskRecords(export _type.TaskRecordsDTO) error { + return mysqlServer.CreateTaskRecords(&mysqlType.TaskRecords{ + UserID: &export.UserId, + ShopID: &export.ShopId, + TaskID: &export.TaskId, + ShopName: &export.ShopName, + IsExport: &export.IsExport, + TaskType: &export.TaskType, + }) +} + +// GetTaskRecordsList 获取任务记录列表 +// @param params 查询参数 +// @return []mysqlType.TaskExport 任务记录列表 +// @return error 错误信息 +func (g *GormAdapter) GetTaskRecordsList(params _type.GetTaskRecordsListReq) ([]*_type.TaskRecordsDTO, int64, error) { + list, count, err := mysqlServer.GetTaskRecordsList(&mysqlType.GetTaskRecordsByUserIdParams{ + UserID: params.UserId, + ShopName: params.ShopName, + TaskID: params.TaskId, + TaskType: params.TaskType, + Page: _type.Page{ + PageNum: params.Page, + PageSize: params.Size, + }, + }) + listDTO := convertMysqlTaskRecordsToDTO(list) + return listDTO, count, err +} + +// GetTaskRecordsByTaskId 根据任务ID获取任务记录 +// @param taskId 任务ID +// @return *mysqlType.TaskExport 任务记录 +// @return error 错误信息 +func (g *GormAdapter) GetTaskRecordsByTaskId(taskId string) (*_type.TaskRecordsDTO, error) { + info, err := mysqlServer.GetTaskRecordsByTaskId(taskId) + infoDTO := _type.TaskRecordsDTO{ + Id: info.ID, + UserId: *info.UserID, + ShopId: *info.ShopID, + TaskId: *info.TaskID, + ShopName: *info.ShopName, + IsExport: *info.IsExport, + TaskType: *info.TaskType, + CreateAt: time.Now(), + } + return &infoDTO, err +} + +// UpdateTaskRecords 更新任务记录 +// @param export 任务记录 +// @return error 错误信息 +func (g *GormAdapter) UpdateTaskRecords(user _type.TaskRecordsDTO) error { + return mysqlServer.UpdateTaskRecords(&mysqlType.TaskRecords{ + UserID: &user.UserId, + ShopID: &user.ShopId, + TaskID: &user.TaskId, + ShopName: &user.ShopName, + IsExport: &user.IsExport, + TaskType: &user.TaskType, + }) +} + +// GetTaskRecordsOldList 获取任务记录旧数据列表 +// @return *mysqlType.TaskExport 任务记录列表 +// @return error 错误信息 +func (g *GormAdapter) GetTaskRecordsOldList() ([]*_type.TaskRecordsDTO, error) { + list, err := mysqlServer.GetTaskRecordsOldList() + listDTO := convertMysqlTaskRecordsToDTO(list) + return listDTO, err +} + +// DeleteTaskRecordsOldData 删除任务记录旧数据 +// @param taskId 任务ID +// @return error 错误信息 +func (g *GormAdapter) DeleteTaskRecordsOldData() error { + return mysqlServer.DeleteOldTaskRecords() +} + +// DeleteTaskRecordsByTaskId 根据任务 ID删除任务记录 +func (g *GormAdapter) DeleteTaskRecordsByTaskId(taskId string) error { + return mysqlServer.DeleteTaskRecordsByTaskId(taskId) +} + +// GetTaskRecords24Hour 获取24小时内的任务记录 +func (g *GormAdapter) GetTaskRecords24Hour() ([]*_type.TaskRecordsDTO, error) { + list, err := mysqlServer.GetTaskRecords24Hour() + listDTO := convertMysqlTaskRecordsToDTO(list) + return listDTO, err +} + +// GetTaskByShopIdAndTaskType 根据 shopId和 taskType获取任务记录 +// @param taskId 任务ID +// @param taskType 任务类型 +// @return []*_type.TaskRecordsDTO 任务列表 +// @return error 错误信息 +func (g *GormAdapter) GetTaskByShopIdAndTaskType(taskId string, taskType int64) ([]*_type.TaskRecordsDTO, error) { + list, err := mysqlServer.GetTaskByShopIdAndTaskType(taskId, taskType) + listDTO := convertMysqlTaskRecordsToDTO(list) + return listDTO, err +} + +// GetAllTask 获取所有的任务记录 +// @return []*_type.TaskRecordsDTO 所有任务列表 +// @return error 错误信息 +func (g *GormAdapter) GetAllTask() ([]*_type.TaskRecordsDTO, error) { + list, err := mysqlServer.GetAllTask() + listDTO := convertMysqlTaskRecordsToDTO(list) + return listDTO, err +} + +// convertMysqlToDTO 转换mysqlType.TaskExport为_type.TaskExport +// @param records mysqlType.TaskExport列表 +// @return []*_type.TaskExport _type.TaskExport列表 +// @return error 错误信息 +func convertMysqlTaskRecordsToDTO(records []*mysqlType.TaskRecords) []*_type.TaskRecordsDTO { + dtos := make([]*_type.TaskRecordsDTO, len(records)) + for i, r := range records { + dtos[i] = &_type.TaskRecordsDTO{ + Id: r.ID, + UserId: *r.UserID, + ShopId: *r.ShopID, + TaskId: *r.TaskID, + ShopName: *r.ShopName, + IsExport: *r.IsExport, + TaskType: *r.TaskType, + CreateAt: *r.CreateAt, + } + } + return dtos +} diff --git a/rep/impl/sqLite/struct.go b/rep/impl/sqLite/struct.go new file mode 100644 index 0000000..55bc026 --- /dev/null +++ b/rep/impl/sqLite/struct.go @@ -0,0 +1,7 @@ +package sqLite + +import "database/sql" + +type SqlAdapter struct { + DB *sql.DB +} diff --git a/rep/impl/sqLite/taskExport.go b/rep/impl/sqLite/taskExport.go new file mode 100644 index 0000000..35f84ca --- /dev/null +++ b/rep/impl/sqLite/taskExport.go @@ -0,0 +1,114 @@ +package sqLite + +import ( + sqLiteServer "planA/service/sqLite" + _type "planA/type" + sqliteType "planA/type/sqLite" +) + +// CreateTaskExport 创建任务导出表 +// @param export 任务导出表 +// @return error 错误信息 +func (s *SqlAdapter) CreateTaskExport(export _type.TaskExportDTO) error { + _, err := sqLiteServer.CreateTaskExport(sqliteType.TaskExport{ + UserID: export.UserId, + ShopID: export.ShopId, + TaskID: export.TaskId, + ShopName: export.ShopName, + FileUrl: export.FileUrl, + Status: export.Status, + Total: export.Total, + CompleteAt: export.CompleteAt, + }) + return err +} + +// GetTaskExportList 获取任务导出列表 +// @param page 分页 +// @param pageSize 每页数量 +// @param userId 用户ID +// @return []mysqlType.TaskExportDTO 任务导出列表 +// @return error 错误信息 +func (s *SqlAdapter) GetTaskExportList(page, pageSize int, userId string) ([]*_type.TaskExportDTO, int64, error) { + list, count, err := sqLiteServer.GetTaskExportsList(page, pageSize, userId) + listDTO := convertSqliteTaskExportToDTO(list) + return listDTO, count, err +} + +// GetTaskExportByTaskId 根据任务 ID获取导出任务 +// @param taskId 任务 ID +// @return *mysqlType.TaskExportDTO 导出任务 +// @return error 错误信息 +func (s *SqlAdapter) GetTaskExportByTaskId(taskId string) (_type.TaskExportDTO, error) { + info, err := sqLiteServer.GetTaskExportByTaskID(taskId) + infoDTO := _type.TaskExportDTO{ + Id: info.ID, + UserId: info.UserID, + ShopId: info.ShopID, + TaskId: info.TaskID, + ShopName: info.ShopName, + FileUrl: info.FileUrl, + Status: info.Status, + Total: info.Total, + CompleteAt: info.CompleteAt, + CreateAt: info.CreateAt, + } + return infoDTO, err +} + +// UpdateTaskExport 更新导出任务 +// @param export 导出任务 +// @return error 错误信息 +func (s *SqlAdapter) UpdateTaskExport(export _type.TaskExportDTO) error { + err := sqLiteServer.UpdateTaskExport(sqliteType.TaskExport{ + UserID: export.UserId, + ShopID: export.ShopId, + TaskID: export.TaskId, + ShopName: export.ShopName, + FileUrl: export.FileUrl, + Status: export.Status, + Total: export.Total, + CompleteAt: export.CompleteAt, + }) + return err +} + +// GetTaskExportOldList 获取任务导出旧数据列表 +func (s *SqlAdapter) GetTaskExportOldList() ([]*_type.TaskExportDTO, error) { + list, err := sqLiteServer.GetOldExport() + listDTO := convertSqliteTaskExportToDTO(list) + return listDTO, err +} + +// DeleteTaskExportOldData 删除任务导出旧数据 +func (s *SqlAdapter) DeleteTaskExportOldData() error { + return sqLiteServer.DeleteOldExport() +} + +// UpdateTaskExportStatus 更新任务导出状态 +// @param taskId 任务 ID +// @param status 状态 +// @param fileUrl 文件路径 +// @return error 错误信息 +func (s *SqlAdapter) UpdateTaskExportStatus(taskId string, status int64, fileUrl string) error { + return sqLiteServer.UpdateTaskExportStatus(taskId, status, fileUrl) +} + +func convertSqliteTaskExportToDTO(records []sqliteType.TaskExport) []*_type.TaskExportDTO { + dtos := make([]*_type.TaskExportDTO, len(records)) + for i, r := range records { + dtos[i] = &_type.TaskExportDTO{ + Id: r.ID, + UserId: r.UserID, + ShopId: r.ShopID, + TaskId: r.TaskID, + ShopName: r.ShopName, + FileUrl: r.FileUrl, + Status: r.Status, + Total: r.Total, + CompleteAt: r.CompleteAt, + CreateAt: r.CreateAt, + } + } + return dtos +} diff --git a/rep/impl/sqLite/taskRecords.go b/rep/impl/sqLite/taskRecords.go new file mode 100644 index 0000000..b4aa6cc --- /dev/null +++ b/rep/impl/sqLite/taskRecords.go @@ -0,0 +1,137 @@ +package sqLite + +import ( + sqLiteServer "planA/service/sqLite" + _type "planA/type" + sqliteType "planA/type/sqLite" + "time" +) + +// CreateTaskRecords 创建任务记录 +// @param export 任务记录 +// @return error 错误信息 +func (s *SqlAdapter) CreateTaskRecords(records _type.TaskRecordsDTO) error { + return sqLiteServer.CreateTaskRecords(sqliteType.TaskRecords{ + UserID: records.UserId, + ShopID: records.ShopId, + TaskID: records.TaskId, + ShopName: records.ShopName, + IsExport: records.IsExport, + TaskType: records.TaskType, + CreateAt: time.Time{}, + }) +} + +// GetTaskRecordsList 获取任务记录列表 +// @param params 查询参数 +// @return []mysqlType.TaskExport 任务记录列表 +// @return error 错误信息 +func (s *SqlAdapter) GetTaskRecordsList(params _type.GetTaskRecordsListReq) ([]*_type.TaskRecordsDTO, int64, error) { + list, count, err := sqLiteServer.GetTaskRecordsList(sqliteType.GetTaskRecordsByUserIdParams{ + UserID: params.UserId, + ShopName: params.ShopName, + TaskID: params.TaskId, + TaskType: params.TaskType, + Page: _type.Page{ + PageNum: params.Page, + PageSize: params.Size, + }, + }) + listDTO := convertSqliteTaskRecordsToDTO(list) + return listDTO, count, err +} + +// GetTaskRecordsByTaskId 根据任务ID获取任务记录 +func (s *SqlAdapter) GetTaskRecordsByTaskId(taskId string) (*_type.TaskRecordsDTO, error) { + info, err := sqLiteServer.GetTaskRecordByTaskID(taskId) + infoDTO := _type.TaskRecordsDTO{ + Id: info.ID, + UserId: info.UserID, + ShopId: info.ShopID, + TaskId: info.TaskID, + ShopName: info.ShopName, + IsExport: info.IsExport, + TaskType: info.TaskType, + CreateAt: time.Now(), + } + return &infoDTO, err +} + +// UpdateTaskRecords 更新任务记录 +// @param export 任务记录 +// @return error 错误信息 +func (s *SqlAdapter) UpdateTaskRecords(user _type.TaskRecordsDTO) error { + return sqLiteServer.UpdateTaskRecord(sqliteType.TaskRecords{ + ID: user.Id, + UserID: user.UserId, + ShopID: user.ShopId, + TaskID: user.TaskId, + ShopName: user.ShopName, + IsExport: user.IsExport, + TaskType: user.TaskType, + }) +} + +// GetTaskRecordsOldList 获取任务记录旧数据列表 +// @return *mysqlType.TaskExport 任务记录列表 +// @return error 错误信息 +func (s *SqlAdapter) GetTaskRecordsOldList() ([]*_type.TaskRecordsDTO, error) { + list, err := sqLiteServer.GetTaskRecordsOldList() + listDTO := convertSqliteTaskRecordsToDTO(list) + return listDTO, err +} + +// DeleteTaskRecordsOldData 删除任务记录旧数据 +// @return error 错误信息 +func (s *SqlAdapter) DeleteTaskRecordsOldData() error { + return sqLiteServer.DeleteOldTaskRecords() +} + +// DeleteTaskRecordsByTaskId 根据任务 ID删除任务记录 +func (s *SqlAdapter) DeleteTaskRecordsByTaskId(taskId string) error { + return sqLiteServer.DeleteTaskRecordsByTaskID(taskId) +} + +// GetTaskRecords24Hour 获取24小时内的任务记录 +func (s *SqlAdapter) GetTaskRecords24Hour() ([]*_type.TaskRecordsDTO, error) { + list, err := sqLiteServer.GetTaskRecords24Hour() + listDTO := convertSqliteTaskRecordsToDTO(list) + return listDTO, err +} + +// GetTaskByShopIdAndTaskType 根据 shopId和 taskType获取任务记录 +// @param taskId 任务ID +// @param taskType 任务类型 +// @return []*_type.TaskRecordsDTO 任务列表 +// @return error 错误信息 +func (s *SqlAdapter) GetTaskByShopIdAndTaskType(taskId string, taskType int64) ([]*_type.TaskRecordsDTO, error) { + list, err := sqLiteServer.GetTaskByShopIdAndTaskType(taskId, taskType) + listDTO := convertSqliteTaskRecordsToDTO(list) + return listDTO, err +} + +// GetAllTask 获取所有的任务记录 +// @return []*_type.TaskRecordsDTO 所有任务列表 +// @return error 错误信息 +func (s *SqlAdapter) GetAllTask() ([]*_type.TaskRecordsDTO, error) { + list, err := sqLiteServer.GetAllTask() + listDTO := convertSqliteTaskRecordsToDTO(list) + return listDTO, err +} + +func convertSqliteTaskRecordsToDTO(records []sqliteType.TaskRecords) []*_type.TaskRecordsDTO { + dtos := make([]*_type.TaskRecordsDTO, len(records)) + for i, r := range records { + dtos[i] = &_type.TaskRecordsDTO{ + Id: r.ID, + UserId: r.UserID, + ShopId: r.UserID, + TaskId: r.TaskID, + ShopName: r.ShopName, + IsExport: r.IsExport, + TaskType: r.TaskType, + CreateAt: r.CreateAt, + } + } + return dtos +} diff --git a/router/admin.go b/router/admin.go new file mode 100644 index 0000000..3cf847f --- /dev/null +++ b/router/admin.go @@ -0,0 +1,15 @@ +package router + +import ( + "planA/controller" + "planA/initialization/golabl" +) + +// AdmiinInir 开发者管理 +func AdmiinInir() { + adminExportRouter := golabl.Router.PathPrefix("/admin").Subrouter() + adminExportRouter.HandleFunc("/delRedisTask/{id}", controller.DelRedisTask).Methods("GET") // 删除 redis中指定任务 + adminExportRouter.HandleFunc("/delMysqlTask/{id}", controller.DelMysqlTask).Methods("GET") // 删除 mysql中指定任务 + adminExportRouter.HandleFunc("/delSqliteTask/{id}", controller.DelSqliteTask).Methods("GET") // 删除 sqlite中指定任务 + adminExportRouter.HandleFunc("/delSqliteTask/{id}", controller.DelSqliteTask).Methods("GET") // 删除 sqlite中指定任务 +} diff --git a/router/alive.go b/router/alive.go new file mode 100644 index 0000000..771a395 --- /dev/null +++ b/router/alive.go @@ -0,0 +1,12 @@ +package router + +import ( + "planA/controller" + "planA/initialization/golabl" +) + +// Alive 初始化工具 +func Alive() { + aliveRouter := golabl.Router.PathPrefix("/alive").Subrouter() + aliveRouter.HandleFunc("/get", controller.GetServiceAliveList).Methods("GET") // 获取服务存活状态列表 +} diff --git a/router/body.go b/router/body.go new file mode 100644 index 0000000..9e66e01 --- /dev/null +++ b/router/body.go @@ -0,0 +1,13 @@ +package router + +import ( + "planA/controller" + "planA/initialization/golabl" +) + +// BodyInit 任务体信息 +func BodyInit() { + taskRouter := golabl.Router.PathPrefix("/body").Subrouter() + taskRouter.HandleFunc("/getOneBody/{taskId}", controller.GetTbOneBodyWait).Methods("GET") // 获取body信息 + taskRouter.HandleFunc("/insterOneBodyOver", controller.InsertTbBodyOver).Methods("POST") // 插入bodyOver +} diff --git a/router/default.go b/router/default.go new file mode 100644 index 0000000..3ce779b --- /dev/null +++ b/router/default.go @@ -0,0 +1,20 @@ +package router + +import ( + "net/http" + "planA/initialization/golabl" + "planA/tool" + _type "planA/type" +) + +// DefaultInit 初始化默认路由 +func DefaultInit() { + // 访问根路径时的欢迎页面 + golabl.Router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + tool.JsonResponse(w, http.StatusOK, _type.APIResponse{ + Success: true, + Message: "任务管理服务已启动", + Data: "可用端点: /task/create, /task/goods/*, /task/pause/{id}, /task/resume/{id}, /task/stop/{id}, /task/setTaskBody, /health", + }) + }) +} diff --git a/router/delTask.go b/router/delTask.go new file mode 100644 index 0000000..fdec998 --- /dev/null +++ b/router/delTask.go @@ -0,0 +1,21 @@ +package router + +import ( + "planA/controller" + "planA/initialization/golabl" +) + +// DelTaskInit 删除任务初始化 +func DelTaskInit() { + delTaskRouter := golabl.Router.PathPrefix("/deltask").Subrouter() + delTaskRouter.HandleFunc("/getDelTask", controller.GetDelTaskByPage).Methods("GET") // 分页查询删除任务 + delTaskRouter.HandleFunc("/getDelTaskByUserId/{id}", controller.GetDelTaskByPageByUserId).Methods("GET") // 分页查询删除任务-用户 + delTaskRouter.HandleFunc("/getDelTaskDetail/{id}", controller.GetDelTaskDetail).Methods("GET") // 分页查询删除任务详情 + delTaskRouter.HandleFunc("/createTbDelTask", controller.CreateTbDelTask).Methods("POST") // 创建淘宝删除任务 + delTaskRouter.HandleFunc("/createTbDelTaskDetails", controller.CreateTbDelTaskDetails).Methods("POST") // 插入淘宝删除任务 + delTaskRouter.HandleFunc("/updateTbDelTaskDetailsStatus", controller.UpdateTbDelTaskDetailsStatus).Methods("POST") // 修改指定淘宝删除任务详情状态 + delTaskRouter.HandleFunc("/updateTbDelTaskProgress", controller.UpdateTbDelTaskProgress).Methods("POST") // 修改指定淘宝任务进度 + delTaskRouter.HandleFunc("/updateTbDelTaskStatus", controller.UpdateTbDelTaskStatus).Methods("POST") // 修改指定淘宝任务状态 + delTaskRouter.HandleFunc("/getTbDelTaskDetailsWait/{id}", controller.GetTbDelTaskDetailsWait).Methods("GET") // 获取淘宝删除任务详情-待处理 + delTaskRouter.HandleFunc("/getTbDelTaskByTaskId/{id}", controller.GetTbDelTaskByTaskId).Methods("GET") // 获取任务数据 +} diff --git a/router/shop.go b/router/shop.go new file mode 100644 index 0000000..b3da6df --- /dev/null +++ b/router/shop.go @@ -0,0 +1,12 @@ +package router + +import ( + "planA/controller" + "planA/initialization/golabl" +) + +// ShopInit 店铺信息 +func ShopInit() { + taskRouter := golabl.Router.PathPrefix("/shop").Subrouter() + taskRouter.HandleFunc("/get/{shopId}", controller.GetShopInfo).Methods("GET") // 获取店铺信息 +} diff --git a/router/static.go b/router/static.go new file mode 100644 index 0000000..1e3d61c --- /dev/null +++ b/router/static.go @@ -0,0 +1,12 @@ +package router + +import ( + "net/http" + "planA/initialization/golabl" +) + +// StaticInit 静态文件初始化 +func StaticInit() { + golabl.Router.PathPrefix("/export/").Handler(http.StripPrefix("/export/", http.FileServer(http.Dir("./export")))) + golabl.Router.PathPrefix("/file/export/").Handler(http.StripPrefix("/file/export/", http.FileServer(http.Dir("./file/export")))) +} diff --git a/router/task.go b/router/task.go new file mode 100644 index 0000000..d851136 --- /dev/null +++ b/router/task.go @@ -0,0 +1,29 @@ +package router + +import ( + "planA/controller" + "planA/initialization/golabl" +) + +// TaskInit 任务初始化 +func TaskInit() { + taskRouter := golabl.Router.PathPrefix("/task").Subrouter() + // ====================== 【需要验签】的接口 ====================== + //taskRouter.Handle("/create", middle.Sign(http.HandlerFunc(controller.CreateTask))).Methods("POST") // 创建新任务 + //taskRouter.Handle("/setTaskBody", middle.Sign(http.HandlerFunc(controller.SetTaskBody))).Methods("POST") // 设置任务执行内容 + // ====================== 【不需要验签】的接口 ====================== + taskRouter.HandleFunc("/create", controller.CreateTask).Methods("POST") // 创建新任务 + taskRouter.HandleFunc("/setTaskBody", controller.SetTaskBody).Methods("POST") // 设置任务执行内容 + taskRouter.HandleFunc("/pause/{id}", controller.PauseTask).Methods("GET") // 暂停指定任务 + taskRouter.HandleFunc("/resume/{id}", controller.ResumeTask).Methods("GET") // 恢复指定任务 + taskRouter.HandleFunc("/stop/{id}", controller.StopTask).Methods("GET") // 停止指定任务 + taskRouter.HandleFunc("/over/{id}", controller.OverTask).Methods("GET") // 完成任务 + taskRouter.HandleFunc("/get", controller.GetTask).Methods("GET") // 获取任务列表(支持查询参数) + taskRouter.HandleFunc("/getByUserId", controller.GetTaskByUserId).Methods("GET") // 根据用户 ID获取任务 获取任务列表(支持查询参数) + taskRouter.HandleFunc("/del/{id}", controller.DelTask).Methods("GET") // 删除任务 + taskRouter.HandleFunc("/b", controller.B).Methods("GET") // 运行B程序(特殊功能) + taskRouter.HandleFunc("/header/get/{id}", controller.GetTaskHeader).Methods("GET") // 获取任务 header信息 + taskRouter.HandleFunc("/getOver/{id}", controller.GetBodyOver).Methods("GET") // 根据任务ID 获取body_over + taskRouter.HandleFunc("/updateTaskProgress", controller.UpdateTaskProgress).Methods("POST") // 更新任务进度 + //taskRouter.HandleFunc("/getTaskList", controller.GetTaskList).Methods("GET") // 获取 task列表 +} diff --git a/router/taskExport.go b/router/taskExport.go new file mode 100644 index 0000000..edec407 --- /dev/null +++ b/router/taskExport.go @@ -0,0 +1,15 @@ +package router + +import ( + "planA/controller" + "planA/initialization/golabl" +) + +// TaskExportInit 任务导出初始化 +func TaskExportInit() { + taskExportRouter := golabl.Router.PathPrefix("/task/export").Subrouter() + taskExportRouter.HandleFunc("/exportTaskDetail/{id}", controller.ExportTaskDetail).Methods("GET") // 导出任务详情(Excel/CSV) + taskExportRouter.HandleFunc("/exportTaskDetail/{userId}/{id}", controller.ExportTaskDetailByUserId).Methods("GET") // 根据用户 ID导出任务详情(Excel/CSV) + taskExportRouter.HandleFunc("/get", controller.GetExportTask).Methods("GET") // 获取导出任务列表 + taskExportRouter.HandleFunc("/get/{userId}", controller.GetExportTaskByUserId).Methods("GET") // 根据用户 ID导出获取导出任务列表 +} diff --git a/router/uploadImg.go b/router/uploadImg.go new file mode 100644 index 0000000..d9ca705 --- /dev/null +++ b/router/uploadImg.go @@ -0,0 +1,17 @@ +package router + +import ( + "net/http" + "planA/controller" + "planA/initialization/golabl" + "planA/initialization/middle" +) + +// UploadImgInit 任务初始化 +func UploadImgInit() { + uploadImgRouter := golabl.Router.PathPrefix("/uploadImg").Subrouter() + // ====================== 【需要验签】的接口 ====================== + uploadImgRouter.Handle("/ImgUploadToPdd", middle.Sign(http.HandlerFunc(controller.ImgUploadToPdd))).Methods("POST") // 上传图片到拼多多 + // ====================== 【不需要验签】的接口 ====================== + //uploadImgRouter.HandleFunc("/ImgUploadToPdd", controller.ImgUploadToPdd).Methods("POST") // 创建新任务 +} diff --git a/service/body.go b/service/body.go new file mode 100644 index 0000000..1c7ceca --- /dev/null +++ b/service/body.go @@ -0,0 +1,108 @@ +package service + +import ( + "fmt" + "planA/initialization/golabl" +) + +// GetOneBodyFromRight 从右侧获取一条body数据 +// 从Redis list中获取最后一条数据并删除原数据 +// @param taskId 任务id +// @return string 获取的数据 +// @return error 错误信息 +func GetOneBodyFromRight(taskId string) (string, error) { + key := taskId + ":body_wait" + // RPop: 从列表右侧获取并删除最后一个元素(原子操作) + data, err := golabl.RedisDbA.RPop(golabl.Ctx, key).Result() + if err != nil { + return "", fmt.Errorf("从Redis获取body数据失败: %w", err) + } + + return data, nil +} + +// InsertOneBodyOver 插入一条bodyOver数据 +func InsertOneBodyOver(taskId string, value string) error { + key := taskId + ":body_over" + return golabl.RedisDbA.LPush(golabl.Ctx, key, value).Err() +} + +// UpdateTaskFooters 更新任务尾 +// @param returnErr int64 类型 1=正确 2= 错误 +// @param count int64 类型 更新的数据 +// @return error 错误信息 +func UpdateTaskFooters(taskId string, returnErr int64, count int64) error { + // 测试 client 是否可用 + err := golabl.RedisDbA.Ping(golabl.Ctx).Err() + if err != nil { + return err + } + + // 检查键是否存在 + footerKey := taskId + ":footer" + exists, existsErr := golabl.RedisDbA.Exists(golabl.Ctx, footerKey).Result() + if existsErr != nil { + return existsErr + } + // 键不存在 + if exists == 0 { + return fmt.Errorf("任务不存在%v", taskId) + } + + // 使用 Pipeline 逐个字段更新 + pipe := golabl.RedisDbA.Pipeline() + // 更新任务尾 + if returnErr == 1 { + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_success", count) + } else { + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_error", count) + } + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_wait", -count) + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_over", count) + _, ExecErr := pipe.Exec(golabl.Ctx) + if ExecErr != nil { + return ExecErr + } + // 返回结果 + return nil +} + +// UpdateTaskHeaders 更新任务尾 +// @param returnErr int64 类型 1=正确 2= 错误 +// @param count int64 类型 更新的数据 +// @return error 错误信息 +func UpdateTaskHeaders(taskId string, returnErr int64, count int64) error { + // 测试 client 是否可用 + err := golabl.RedisDbA.Ping(golabl.Ctx).Err() + if err != nil { + return err + } + + // 检查键是否存在 + footerKey := taskId + ":header" + exists, existsErr := golabl.RedisDbA.Exists(golabl.Ctx, footerKey).Result() + if existsErr != nil { + return existsErr + } + // 键不存在 + if exists == 0 { + return fmt.Errorf("任务不存在%v", taskId) + } + + // 使用 Pipeline 逐个字段更新 + pipe := golabl.RedisDbA.Pipeline() + // 更新任务尾 + if returnErr == 1 { + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_success", count) + } else { + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_error", count) + } + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_wait", -count) + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_over", count) + _, ExecErr := pipe.Exec(golabl.Ctx) + if ExecErr != nil { + return ExecErr + } + // 返回结果 + return nil +} diff --git a/service/book.go b/service/book.go new file mode 100644 index 0000000..e120e8e --- /dev/null +++ b/service/book.go @@ -0,0 +1,38 @@ +package service + +import ( + "encoding/json" + "fmt" + "planA/initialization/golabl" + _type "planA/type" +) + +// ============================================ +// 书籍信息操作 +// 数据结构: String (JSON格式) +// 键格式: {bookKey} +// ============================================ + +// GetTaskBook 获取书籍信息 +// @param bookKey 书籍键 +// @return _type.BookInfo 书籍信息 +// @return error 错误信息 +func GetTaskBook(bookKey string) (_type.BookInfo, error) { + var book _type.BookInfo + + bookStr, err := golabl.RedisDbB.Get(golabl.Ctx, bookKey).Result() + if err != nil { + return book, fmt.Errorf("获取书品信息错误: key=%v err=%w", bookKey, err) + } + + if err := json.Unmarshal([]byte(bookStr), &book); err != nil { + return book, fmt.Errorf("JSON解析错误: %w", err) + } + + return book, nil +} + +// GetTaskBookPing 测试书籍信息连接(仅用于心跳检测) +func GetTaskBookPing() { + golabl.RedisDbB.Get(golabl.Ctx, "ping..") +} diff --git a/service/mysql/delTask.go b/service/mysql/delTask.go new file mode 100644 index 0000000..e56a378 --- /dev/null +++ b/service/mysql/delTask.go @@ -0,0 +1,175 @@ +package mysql + +import ( + "fmt" + "planA/initialization/golabl" + planBType "planA/planB/type" + mysqlType "planA/type/mysql" + "time" + + "gorm.io/gorm" +) + +// GetDelTask 查询del_task 表中待执行 +func GetDelTask() ([]mysqlType.DelTask, error) { + var delTask []mysqlType.DelTask + err := golabl.MysqlDb.Where("status IN ? and shop_type != 6", []int{0, 1, 2}).Find(&delTask).Error + if err != nil { + fmt.Println("查询del_task 表中待执行失败:", err) + } + return delTask, err +} + +// UpdateDelTaskStatus 根据id 将查询del_task 的status 修改为 1 +func UpdateDelTaskStatus(id int64) error { + err := golabl.MysqlDb.Model(&mysqlType.DelTask{}).Where("id = ?", id).Update("status", 1).Error + return err +} + +// GetDelTaskByTaskId 根据tasId查询删除任务 +func GetDelTaskByTaskId(taskId string) (mysqlType.DelTask, error) { + var delTask mysqlType.DelTask + err := golabl.MysqlDb.Where("task_id = ?", taskId).First(&delTask).Error + return delTask, err +} + +// UpdateDelTaskPidByTaskId 根据tasId 清空 pid +func UpdateDelTaskPidByTaskId(taskId string) error { + err := golabl.MysqlDb.Model(&mysqlType.DelTask{}).Where("task_id = ?", taskId).Update("pid", "").Error + return err +} + +// UpdateDelTaskPidByTaskIdAndPid 根据tasId 设置 pid +func UpdateDelTaskPidByTaskIdAndPid(taskId string, pid string) error { + err := golabl.MysqlDb.Model(&mysqlType.DelTask{}).Where("task_id = ?", taskId).Update("pid", pid).Error + return err +} + +// GetDelTaskByPage 分页查询 +func GetDelTaskByPage(pageNum int, pageSize int, userId string) ([]mysqlType.DelTask, int, error) { + var delTask []mysqlType.DelTask + var total int64 + + // 构建基础查询 + query := golabl.MysqlDb.Model(&mysqlType.DelTask{}) + + // 如果userId不为空,添加where条件 + if userId != "" { + query = query.Where("user_id = ?", userId) + } + + // 查询总记录数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("查询总数失败: %w", err) + } + + // 如果总数为0,直接返回空切片 + if total == 0 { + return []mysqlType.DelTask{}, 0, nil + } + + // 计算偏移量 + offset := (pageNum - 1) * pageSize + + // 检查偏移量是否超出范围 + if offset >= int(total) { + return []mysqlType.DelTask{}, int(total), nil + } + + // 分页查询数据 + err := query. + Limit(pageSize). + Offset(offset). + Order("id DESC"). // 建议添加排序,保证分页顺序稳定 + Find(&delTask).Error + + if err != nil { + return nil, 0, fmt.Errorf("分页查询失败: %w", err) + } + + return delTask, int(total), nil +} + +// GetDelTaskDetailByPage 查询删除任务详情 +func GetDelTaskDetailByPage(pageNum int, pageSize int, taskId string, status int) ([]planBType.DelTaskDetail, int, error) { + var delTaskDetail []planBType.DelTaskDetail + var total int64 + tableName := "del_task_details_" + taskId + // 构建基础查询 + query := golabl.MysqlDb.Table(tableName) + + if status != 3 { + query = query.Where("status = ?", status) + } + + // 查询总记录数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("查询总数失败: %w", err) + } + + // 如果总数为0,直接返回空切片 + if total == 0 { + return []planBType.DelTaskDetail{}, 0, nil + } + + // 计算偏移量 + offset := (pageNum - 1) * pageSize + + // 检查偏移量是否超出范围 + if offset >= int(total) { + return []planBType.DelTaskDetail{}, int(total), nil + } + + // 分页查询数据 + err := query. + Limit(pageSize). + Offset(offset). + Order("id DESC"). // 建议添加排序,保证分页顺序稳定 + Find(&delTaskDetail).Error + + if err != nil { + return nil, 0, fmt.Errorf("分页查询失败: %w", err) + } + + return delTaskDetail, int(total), nil +} + +// GetExpiredDelTask 查询过期的删除任务 +func GetExpiredDelTask() ([]mysqlType.DelTask, error) { + var delTask []mysqlType.DelTask + // 假设过期时间是7天前,根据实际需求调整 + expireTime := time.Now().Add(-7 * 24 * time.Hour) + err := golabl.MysqlDb.Where("status = 3 and create_at < ?", expireTime).Find(&delTask).Error + return delTask, err +} + +// DeleteDelTaskDetail 删除任务明细表 +func DeleteDelTaskDetail(taskId string) error { + tableName := "del_task_details_" + taskId + err := golabl.MysqlDb.Migrator().DropTable(tableName) + return err +} + +// DeleteDelTaskById 根据id 删除 删除任务中的数据 +func DeleteDelTaskById(id int64) error { + err := golabl.MysqlDb.Where("id = ?", id).Delete(&mysqlType.DelTask{}).Error + return err +} + +// CreateDelTask 创建task 删除任务 +func CreateDelTask(data mysqlType.DelTask) error { + return golabl.MysqlDb.Create(&data).Error +} + +// UpdateDelTaskProgress 修改任务进度 +func UpdateDelTaskProgress(taskId string, num int) error { + // 使用 SQL 表达式在原有值基础上累加 + return golabl.MysqlDb.Model(&mysqlType.DelTask{}). + Where("task_id = ?", taskId). + Update("task_count_over", gorm.Expr("task_count_over + ?", num)).Error +} + +// UpdateDelTaskStatusByTaskId 修改任务状态 +func UpdateDelTaskStatusByTaskId(taskId string, status int) error { + return golabl.MysqlDb.Model(&mysqlType.DelTask{}).Where("task_id = ?", taskId).Update("status", status).Error +} diff --git a/service/mysql/delTaskDetails.go b/service/mysql/delTaskDetails.go new file mode 100644 index 0000000..c19af50 --- /dev/null +++ b/service/mysql/delTaskDetails.go @@ -0,0 +1,64 @@ +package mysql + +import ( + "fmt" + "planA/initialization/golabl" + planBType "planA/planB/type" +) + +// CreateTableIfNotExists 创建表 +// @return error 错误信息 +func CreateTableIfNotExists(taskId string) error { + var dleTaskDetailsTable = fmt.Sprintf("del_task_details_%v", taskId) + // 检查表是否存在 + if !golabl.MysqlDb.Migrator().HasTable(dleTaskDetailsTable) { + sql := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s ( + id int(11) NOT NULL AUTO_INCREMENT, + del_task_id int(11) DEFAULT '0' COMMENT '删除任务id', + task_id varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT '任务id', + isbn varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT 'isbn', + book_name varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT '商品名称', + token varchar(255) COLLATE utf8_unicode_ci DEFAULT '' COMMENT 'token', + goods_id bigint(11) DEFAULT NULL COMMENT '商品id', + json text COLLATE utf8_unicode_ci COMMENT '原始字符串', + status int(11) DEFAULT '0' COMMENT '状态: 1=正常 2=错误', + err text COLLATE utf8_unicode_ci COMMENT '错误信息', + delete_at datetime DEFAULT NULL COMMENT '请求删除商品时间', + delete_date date DEFAULT NULL COMMENT '请求删除商品日期', + create_at datetime DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (id), + KEY del_task_id (del_task_id, task_id, goods_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci`, dleTaskDetailsTable) + + if err := golabl.MysqlDb.Exec(sql).Error; err != nil { + return fmt.Errorf("创建 %v 表失败: %v", dleTaskDetailsTable, err) + } + } + return nil +} + +// InsertDelTaskDetail 插入单条删除任务详情数据 +func InsertDelTaskDetail(delTaskID int64, detail planBType.DelTaskDetail) error { + var dleTaskDetailsTable = fmt.Sprintf("del_task_details_%v", delTaskID) + + // 使用动态表名插入 + result := golabl.MysqlDb.Table(dleTaskDetailsTable).Create(&detail) + if result.Error != nil { + return fmt.Errorf("插入数据失败: %v", result.Error) + } + return nil +} + +// UpdateDelTaskDetailStatus 修改指定任务的任务详情状态 +func UpdateDelTaskDetailStatus(taskID string, goodsID string, status int, err string) error { + var dleTaskDetailsTable = fmt.Sprintf("del_task_details_%v", taskID) + + // 准备要更新的字段 + updates := map[string]interface{}{ + "status": status, + "err": err, + } + + result := golabl.MysqlDb.Table(dleTaskDetailsTable).Where("goods_id = ?", goodsID).Updates(updates) + return result.Error +} diff --git a/service/mysql/taskExport.go b/service/mysql/taskExport.go new file mode 100644 index 0000000..29ea8ef --- /dev/null +++ b/service/mysql/taskExport.go @@ -0,0 +1,110 @@ +package mysql + +import ( + "database/sql" + "planA/initialization/golabl" + "planA/tool" + mysqlType "planA/type/mysql" + "time" +) + +// CreateTaskExport 向task_export表插入一条记录 +// @param export TaskExport 要插入的导出记录 +// @return int64 插入记录的自增ID +// @return error 错误信息 + +func CreateTaskExport(export mysqlType.TaskExport) (int64, error) { + // 创建记录 + createAt := time.Now() + export.CreateAt = &createAt + result := golabl.MysqlDb.Model(&mysqlType.TaskExport{}).Create(&export) + // 检查是否有错误 + if result.Error != nil { + return 0, result.Error + } + // 返回插入的自增 ID + return export.ID, nil +} + +// UpdateTaskExportStatus 更新task_export表中的status字段 +// @param taskId string 任务ID +// @param status int64 状态 +// @param fullPath string 文件路径 +// @return error 错误信息 + +func UpdateTaskExportStatus(taskId string, status int64, fullPath string) error { + var err error + if status == 2 { + completeAt := &sql.NullTime{ + Time: time.Now(), + Valid: true, + } + err = golabl.MysqlDb.Model(&mysqlType.TaskExport{}).Where("task_id = ?", taskId).Updates(&mysqlType.TaskExport{ + FileUrl: &fullPath, + Status: &status, + CompleteAt: completeAt, + }).Error + } else { + err = golabl.MysqlDb.Model(&mysqlType.TaskExport{}).Where("task_id = ?", taskId).Update("status", status).Error + } + return err +} + +// GetTaskExportList 分页查询task_export表记录 +// @param page 分页参数 +// @param pageSize int 每页数量 +// @param userId string 用户ID +// @return []*TaskUser 查询结果 +// @return int64 总条数 +// @return error 错误信息 +func GetTaskExportList(page, pageSize int, userId string) ([]mysqlType.TaskExport, int64, error) { + var taskExport []mysqlType.TaskExport + var total int64 + //获取页 + pageSize, offset := tool.GetPage(page, pageSize) + // 构建查询条件 + query := golabl.MysqlDb.Model(&mysqlType.TaskExport{}) + if userId != "" { + query = query.Where("user_id = ?", userId) + } + // 查询总条数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + if total == 0 { + return taskExport, total, nil + } + // 分页查询数据 + err := query.Order("id DESC"). + Offset(offset). + Limit(pageSize). + Find(&taskExport).Error + if err != nil { + return nil, 0, err + } + return taskExport, total, err +} + +// DeleteOldExport 删除task_export表中N天前的记录 +// @return error 错误信息 +func DeleteOldExport() error { + days := golabl.Config.Server.DataDay + threeDaysAgo := time.Now().AddDate(0, 0, -days) + return golabl.MysqlDb.Where("create_at < ?", threeDaysAgo).Delete(&mysqlType.TaskExport{}).Error +} + +// GetOldExportSQLite 获取task_export表中N天前的记录 +func GetOldExportSQLite() ([]mysqlType.TaskExport, error) { + var taskExport []mysqlType.TaskExport + days := golabl.Config.Server.DataDay + threeDaysAgo := time.Now().AddDate(0, 0, -days) + err := golabl.MysqlDb.Where("create_at < ?", threeDaysAgo).Find(&taskExport).Error + return taskExport, err +} + +// UpdateTaskExport 更新task_export信息 +// @param taskExport mysqlType.TaskExport 要更新的任务信息 +// @return error 错误信息 +func UpdateTaskExport(taskExport mysqlType.TaskExport) error { + return golabl.MysqlDb.Model(&mysqlType.TaskExport{}).Where("task_id = ?", taskExport.TaskID).Updates(taskExport).Error +} diff --git a/service/mysql/taskRecords.go b/service/mysql/taskRecords.go new file mode 100644 index 0000000..9d6c26d --- /dev/null +++ b/service/mysql/taskRecords.go @@ -0,0 +1,148 @@ +package mysql + +import ( + "errors" + "planA/initialization/golabl" + "planA/tool" + mysqlType "planA/type/mysql" + "time" + + "gorm.io/gorm" +) + +// CreateTaskRecords 向task_records表插入单条数据 +// @param TaskRecords 要插入的task_records数据 +// @return error 错误信息 +func CreateTaskRecords(TaskRecords *mysqlType.TaskRecords) error { + err := golabl.MysqlDb.Create(TaskRecords).Error + return err +} + +// GetTaskRecordsList 分页查询任务-用户关联表数据 +// @param params 分页查询参数 +// @return []*TaskRecords 查询结果 +// @return int64 总条数 +// @return error 错误信息 +func GetTaskRecordsList(params *mysqlType.GetTaskRecordsByUserIdParams) ([]*mysqlType.TaskRecords, int64, error) { + var TaskRecordss []*mysqlType.TaskRecords + var total int64 + //获取页 + pageSize, offset := tool.GetPage(params.Page.PageNum, params.Page.PageSize) + // 构建查询条件 + query := golabl.MysqlDb.Model(&mysqlType.TaskRecords{}) + if params.UserID != "" { + query = query.Where("user_id = ?", params.UserID) + } + if params.ShopName != "" { + query = query.Where("shop_name = ?", params.ShopName) + } + if params.TaskID != "" { + query = query.Where("task_id = ?", params.TaskID) + } + if params.TaskType != 0 { + query = query.Where("task_type = ?", params.TaskType) + } + // 查询总条数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + if total == 0 { + return TaskRecordss, total, nil + } + // 分页查询数据 + err := query.Order("id DESC"). + Offset(offset). + Limit(pageSize). + Find(&TaskRecordss).Error + if err != nil { + return nil, 0, err + } + return TaskRecordss, total, err +} + +// GetTaskRecordsByTaskId 根据TaskId查询task_records表数据 +// @param taskId 任务Id +// @return *TaskRecords 查询结果 +// @return error 错误信息 +func GetTaskRecordsByTaskId(taskId string) (mysqlType.TaskRecords, error) { + var TaskRecords mysqlType.TaskRecords + + err := golabl.MysqlDb.Where("task_id = ?", taskId).First(&TaskRecords).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return TaskRecords, nil + } + if err != nil { + return TaskRecords, err + } + + return TaskRecords, nil +} + +// UpdateTaskRecords 根据任务ID更新数据 +func UpdateTaskRecords(record *mysqlType.TaskRecords) error { + return golabl.MysqlDb.Model(&mysqlType.TaskRecords{}).Where("task_id = ?", record.TaskID).Updates(record).Error +} + +// DeleteOldTaskRecords 删除大于N天的数据 +// @return error 错误信息 +func DeleteOldTaskRecords() error { + days := golabl.Config.Server.DataDay + threeDaysAgo := time.Now().AddDate(0, 0, -days) + return golabl.MysqlDb.Where("create_at < ?", threeDaysAgo).Delete(&mysqlType.TaskRecords{}).Error +} + +// DeleteTaskRecordsByTaskId 根据任务ID删除数据 +// @param taskId 任务ID +// @return error 错误信息 +func DeleteTaskRecordsByTaskId(taskId string) error { + return golabl.MysqlDb.Where("task_id = ?", taskId).Delete(&mysqlType.TaskRecords{}).Error +} + +// GetTaskRecords24Hour 获取24小时内的数据 +func GetTaskRecords24Hour() ([]*mysqlType.TaskRecords, error) { + var tasks []*mysqlType.TaskRecords + now := time.Now() + twentyFourHoursAgo := now.Add(-24 * time.Hour) + tenMinutesAgo := now.Add(-10 * time.Minute) + + err := golabl.MysqlDb.Where("create_at >= ? AND create_at <= ?", + twentyFourHoursAgo, tenMinutesAgo). + Order("create_at DESC"). + Find(&tasks).Error + + return tasks, err +} + +// GetTaskRecordsOldList 获取task_records表中N天前的记录 +func GetTaskRecordsOldList() ([]*mysqlType.TaskRecords, error) { + var tasks []*mysqlType.TaskRecords + days := golabl.Config.Server.DataDay + threeDaysAgo := time.Now().AddDate(0, 0, -days) + + err := golabl.MysqlDb.Where("create_at < ?", + threeDaysAgo). + Order("create_at DESC"). + Find(&tasks).Error + + return tasks, err +} + +// GetTaskByShopIdAndTaskType 根据 shopId和 taskType获取任务记录 +// @param taskId 任务ID +// @param taskType 任务类型 +// @return []mysqlType.TaskRecords 任务列表 +// @return error 错误信息 +func GetTaskByShopIdAndTaskType(taskId string, taskType int64) ([]*mysqlType.TaskRecords, error) { + var task []*mysqlType.TaskRecords + err := golabl.MysqlDb.Model(&mysqlType.TaskRecords{}).Where("shop_id = ? AND task_type = ?", taskId, taskType).Find(&task).Error + return task, err +} + +// GetAllTask 查询所有的任务 +// @return []*mysqlType.TaskRecords 所有任务 +// @return error 错误信息 +func GetAllTask() ([]*mysqlType.TaskRecords, error) { + var task []*mysqlType.TaskRecords + err := golabl.MysqlDb.Model(&mysqlType.TaskRecords{}).Find(&task).Error + return task, err +} diff --git a/service/noBook.go b/service/noBook.go new file mode 100644 index 0000000..77f1530 --- /dev/null +++ b/service/noBook.go @@ -0,0 +1,13 @@ +package service + +import ( + "planA/initialization/golabl" +) + +// SetNoBookCount 无书籍信息isbn计次 +// @param key 键 +// @return error 错误信息 +func SetNoBookCount(isbn string) error { + key := "noBookInfo" + return golabl.RedisDbD.ZIncrBy(golabl.Ctx, key, 1, isbn).Err() +} diff --git a/service/psiMysql/bookInfo.go b/service/psiMysql/bookInfo.go new file mode 100644 index 0000000..ee7c115 --- /dev/null +++ b/service/psiMysql/bookInfo.go @@ -0,0 +1,18 @@ +package psiMysql + +import ( + "planA/initialization/golabl" + psiMysqlType "planA/type/psiMysql" +) + +// GetBookInfo 获取书籍信息 +func GetBookInfo(isbn string, fisbn string) (bookInfo psiMysqlType.BookInfo, err error) { + err = golabl.PsiMysqlDb.Where("isbn = ? and fisbn = ?", isbn, fisbn).First(&bookInfo).Error + return bookInfo, err +} + +// GetBookInfoSingle 获取书籍信息 +func GetBookInfoSingle(isbn string) (bookInfo psiMysqlType.BookInfo, err error) { + err = golabl.PsiMysqlDb.Where("isbn = ?", isbn).First(&bookInfo).Error + return bookInfo, err +} diff --git a/service/shop.go b/service/shop.go new file mode 100644 index 0000000..3450419 --- /dev/null +++ b/service/shop.go @@ -0,0 +1,48 @@ +package service + +import ( + "encoding/json" + "fmt" + "planA/initialization/golabl" + "strings" +) + +// ============================================ +// 店铺信息操作 +// 数据结构: 支持String/List/Hash多种类型 +// 键格式: {shopID} +// ============================================ + +// GetTaskShop 获取店铺信息 +// @param shopID 店铺ID +// @return string 店铺信息字符串 +// @return error 错误信息 +func GetTaskShop(shopID string) (string, error) { + // 检查键类型 + keyType, err := golabl.RedisDbC.Type(golabl.Ctx, shopID).Result() + if err != nil { + return "", fmt.Errorf("检查Redis key类型失败: %w", err) + } + switch keyType { + case "string": + return golabl.RedisDbC.Get(golabl.Ctx, shopID).Result() + + case "list": + items, err := golabl.RedisDbC.LRange(golabl.Ctx, shopID, 0, -1).Result() + if err != nil { + return "", fmt.Errorf("获取list数据失败: %w", err) + } + return "[" + strings.Join(items, ",") + "]", nil + + case "hash": + hashData, err := golabl.RedisDbC.HGetAll(golabl.Ctx, shopID).Result() + if err != nil { + return "", fmt.Errorf("获取hash数据失败: %w", err) + } + jsonData, _ := json.Marshal(hashData) + return string(jsonData), nil + + default: + return "", fmt.Errorf("不支持的数据类型: %s", keyType) + } +} diff --git a/service/sqLite/taskExport.go b/service/sqLite/taskExport.go new file mode 100644 index 0000000..9af85bb --- /dev/null +++ b/service/sqLite/taskExport.go @@ -0,0 +1,336 @@ +package sqLite + +import ( + "database/sql" + "errors" + "fmt" + "planA/initialization/golabl" + "planA/tool" + sqLiteType "planA/type/sqLite" + "strings" + "time" +) + +// CreateTaskExportTab 创建task_export表 +// @return error 错误信息 +func CreateTaskExportTab() error { + createTableSQL := ` + CREATE TABLE IF NOT EXISTS task_export ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id VARCHAR(100) NOT NULL, + shop_id VARCHAR(100) NOT NULL, + task_id VARCHAR(100) NOT NULL, + shop_name VARCHAR(100) NOT NULL, + file_url VARCHAR(300), + status INTEGER NOT NULL DEFAULT 0, + total INTEGER NOT NULL DEFAULT 0, + complete_at DATETIME, + create_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_task_id ON task_export(task_id); + CREATE INDEX IF NOT EXISTS idx_status ON task_export(status); + CREATE INDEX IF NOT EXISTS idx_create_at ON task_export(create_at); +` + _, err := golabl.SqliteDb.Exec(createTableSQL) + if err != nil { + return err + } + return nil +} + +// CreateTaskExport 向task_export表插入一条记录 +// @param export TaskExport 要插入的导出记录 +// @return int64 插入记录的自增ID +// @return error 错误信息 +func CreateTaskExport(export sqLiteType.TaskExport) (int64, error) { + // 在 Go 代码中计算当前时间 + currentTime := time.Now().Format("2006-01-02 15:04:05") + + insertSQL := `INSERT INTO task_export (user_id, shop_id, task_id, shop_name, file_url, status,total,create_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + result, err := golabl.SqliteDb.Exec( + insertSQL, + export.UserID, // user_id + export.ShopID, // shop_id + export.TaskID, // task_id + export.ShopName, // shop_name + export.FileUrl, // file_url(允许空字符串) + export.Status, // status + export.Total, // total + currentTime, + ) + if err != nil { + return 0, err + } + // 获取插入记录的自增ID(SQLite中LastInsertId()返回rowid) + insertID, err := result.LastInsertId() + if err != nil { + return 0, err // 获取ID失败返回0和错误 + } + + return insertID, nil // 成功返回自增ID和nil +} + +// UpdateTaskExportStatus 更新task_export表中的status字段 +// @param taskId string 任务Id +// @param status int64 状态 +// @param fullPath string 文件路径 +// @return error 错误信息 +func UpdateTaskExportStatus(taskId string, status int64, fullPath string) error { + var err error + if status == 2 { + // 当status=2时,同时更新status、complete_at和file_url字段 + _, err = golabl.SqliteDb.Exec( + "UPDATE task_export SET status = ?, complete_at = ?, file_url = ? WHERE task_id = ?", + status, + time.Now().Format("2006-01-02 15:04:05"), // 设置为当前系统时间 + fullPath, + taskId, + ) + } else { + // 其他状态只更新status字段 + _, err = golabl.SqliteDb.Exec( + "UPDATE task_export SET status = ? WHERE task_id = ?", + status, + taskId, + ) + } + return err +} + +// GetTaskExportsList 分页查询task_export表记录(无查询条件) +// @param page 页码(从1开始) +// @param pageSize 每页条数 +// @param userId 用户ID +// @return []TaskExport 记录列表 +// @return int64 总记录数 +// @return error 错误信息 +func GetTaskExportsList(page, pageSize int, userId string) ([]sqLiteType.TaskExport, int64, error) { + // 参数校验 + pageSize, offset := tool.GetPage(page, pageSize) + + // 构建查询条件(当前为空) + var conditions []string + var args []interface{} + + // 如果userId不为空,则添加用户ID条件 + if userId != "" { + conditions = append(conditions, "user_id = ?") + args = append(args, userId) + } + + // 构建 WHERE子句 + whereClause := "" + if len(conditions) > 0 { + whereClause = "WHERE " + strings.Join(conditions, " AND ") + } + + // 查询总数 + var total int64 + countSQL := fmt.Sprintf(`SELECT COUNT(*) FROM task_export %s`, whereClause) + + var countErr error + if len(args) > 0 { + countErr = golabl.SqliteDb.QueryRow(countSQL, args...).Scan(&total) + } else { + countErr = golabl.SqliteDb.QueryRow(countSQL).Scan(&total) + } + + if countErr != nil { + return nil, 0, fmt.Errorf("查询总数失败: %v", countErr) + } + + // 分页查询 + querySQL := fmt.Sprintf(` + SELECT id, user_id, task_id, shop_name, file_url, status, total, complete_at, create_at + FROM task_export + %s + ORDER BY create_at DESC + LIMIT ? OFFSET ? + `, whereClause) + // 添加分页参数到args + queryArgs := append(args, pageSize, offset) + + rows, err := golabl.SqliteDb.Query(querySQL, queryArgs...) + if err != nil { + return nil, 0, fmt.Errorf("查询失败: %v", err) + } + defer rows.Close() + + var records []sqLiteType.TaskExport + + for rows.Next() { + var record sqLiteType.TaskExport + // 扫描所有字段 + err = rows.Scan( + &record.ID, + &record.UserID, + &record.TaskID, + &record.ShopName, + &record.FileUrl, + &record.Status, + &record.Total, + &record.CompleteAt, + &record.CreateAt, + ) + if err != nil { + return nil, 0, fmt.Errorf("扫描数据失败: %v", err) + } + records = append(records, record) + } + + if err = rows.Err(); err != nil { + return nil, 0, fmt.Errorf("遍历结果集错误: %v", err) + } + + return records, total, nil +} + +// DeleteOldExport 删除task_export表中N天前的记录 +// @return error 错误信息 +func DeleteOldExport() error { + days := golabl.Config.Server.DataDay + sql := fmt.Sprintf("DELETE FROM task_export WHERE create_at < datetime('now','localtime', '-%d days')", days) + result, err := golabl.SqliteDb.Exec(sql) + if err != nil { + return fmt.Errorf("删除旧数据失败: %v", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("获取影响行数失败: %v", err) + } + + fmt.Printf("已删除 %d 条大于N天的记录\n", rowsAffected) + return nil +} + +// GetOldExport 获取task_export表中N天前的记录 +// @return []TaskExport N天前的记录 +// @return error 错误信息 +func GetOldExport() ([]sqLiteType.TaskExport, error) { + days := golabl.Config.Server.DataDay + sevenDaysAgo := time.Now().AddDate(0, 0, -days) + + // 查询N天前的记录 + rows, err := golabl.SqliteDb.Query(` + SELECT id, user_id, task_id, shop_name, file_url, status, total, complete_at, create_at + FROM task_export + WHERE create_at < ? + ORDER BY create_at ASC + `, sevenDaysAgo) + + if err != nil { + return nil, fmt.Errorf("查询N天前导出记录失败: %v", err) + } + defer rows.Close() + + var tasks []sqLiteType.TaskExport + + for rows.Next() { + var task sqLiteType.TaskExport + var completeAt, createAt sql.NullTime + + err := rows.Scan( + &task.ID, + &task.UserID, + &task.TaskID, + &task.ShopName, + &task.FileUrl, + &task.Status, + &task.Total, + &completeAt, + &createAt, + ) + + if err != nil { + return nil, fmt.Errorf("扫描导出记录失败: %v", err) + } + + // 转换时间字段 + if completeAt.Valid { + task.CompleteAt = sql.NullTime{ + Time: completeAt.Time, + Valid: true, + } + } + if createAt.Valid { + task.CreateAt = createAt.Time + } + + tasks = append(tasks, task) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("遍历导出记录失败: %v", err) + } + + return tasks, nil +} + +// GetTaskExportByTaskID 根据任务ID获取导出记录 +// @param taskID string 任务ID +// @return sqLiteType.TaskExport 导出记录 +// @return error 错误信息 +func GetTaskExportByTaskID(taskID string) (sqLiteType.TaskExport, error) { + query := `SELECT id, user_id, shop_id, task_id, shop_name, file_url, + status, total, complete_at, create_at + FROM task_export + WHERE task_id = ?` + + var task sqLiteType.TaskExport + err := golabl.SqliteDb.QueryRow(query, taskID).Scan( + &task.ID, + &task.UserID, + &task.ShopID, + &task.TaskID, + &task.ShopName, + &task.FileUrl, + &task.Status, + &task.Total, + &task.CompleteAt, + &task.CreateAt, + ) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return task, nil // 未找到记录 + } + return task, err + } + + return task, nil +} + +// UpdateTaskExport 更新task_export信息 +// @param export sqLiteType.TaskExport 要更新的任务信息 +// @return error 错误信息 +func UpdateTaskExport(export sqLiteType.TaskExport) error { + query := ` + UPDATE task_export + SET user_id = ?, + shop_id = ?, + task_id = ?, + shop_name = ?, + file_url = ?, + status = ?, + total = ?, + complete_at = ? + WHERE task_id = ? + ` + + result, err := golabl.SqliteDb.Exec(query, export.TaskID, export.ShopID, export.TaskID, export.ShopName, export.FileUrl, export.Status, export.Total, export.CompleteAt, export.TaskID) + if err != nil { + return fmt.Errorf("更新任务失败: %v", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("获取影响行数失败: %v", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("未找到task_id为 %s 的任务", export.TaskID) + } + return nil +} diff --git a/service/sqLite/taskRecords.go b/service/sqLite/taskRecords.go new file mode 100644 index 0000000..1f0aa87 --- /dev/null +++ b/service/sqLite/taskRecords.go @@ -0,0 +1,369 @@ +package sqLite + +import ( + "fmt" + "planA/initialization/golabl" + "planA/tool" + sqLiteType "planA/type/sqLite" + "strings" + "time" +) + +// CreateTaskIdTab 创建task_records表 +// @return error 错误信息 +func CreateTaskIdTab() error { + createTableSQL := ` + CREATE TABLE IF NOT EXISTS task_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id VARCHAR(100) NOT NULL, + shop_id VARCHAR(100) NOT NULL, + task_id VARCHAR(100) NOT NULL, + shop_name VARCHAR(100) NOT NULL, + is_export INTEGER NOT NULL DEFAULT 0, + task_type INTEGER NOT NULL DEFAULT 0, + create_at DATETIME NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_task_id ON task_records(task_id); + CREATE INDEX IF NOT EXISTS idx_create_at ON task_records(create_at); + ` + _, err := golabl.SqliteDb.Exec(createTableSQL) + if err != nil { + return err + } + return nil +} + +// CreateTaskRecords 向task_records表插入一条记录 +// @param record TaskRecord 要插入的记录 +// @return error 错误信息 +func CreateTaskRecords(record sqLiteType.TaskRecords) error { + // 在 Go 代码中计算当前时间 + currentTime := time.Now().Format("2006-01-02 15:04:05") + + insertSQL := `INSERT INTO task_records (user_id,shop_id,task_id,shop_name,task_type, create_at) VALUES (?, ?, ?, ?, ?, ?)` + result, err := golabl.SqliteDb.Exec(insertSQL, record.UserID, record.ShopID, record.TaskID, record.ShopName, record.TaskType, currentTime) + if err != nil { + return err + } + + lastID, err := result.LastInsertId() + if err != nil { + return err + } + + record.ID = lastID + return nil +} + +// GetTaskRecordsList 分页查询task_records表记录 +// @param page 页码(从1开始) +// @param pageSize 每页条数 +// @param taskId 任务ID(可选,为空时不作为条件) +// @param shopName 店铺名称(可选,为空时不作为条件) +// @param taskType 任务类型(可选,为空时不作为条件) +// @return []TaskRecord 记录列表 +// @return int64 总记录数 +// @return error 错误信息 +func GetTaskRecordsList(params sqLiteType.GetTaskRecordsByUserIdParams) ([]sqLiteType.TaskRecords, int64, error) { + // 参数校验 + pageSize, offset := tool.GetPage(params.Page.PageNum, params.Page.PageSize) + + // 构建查询条件 + var conditions []string + var args []interface{} + + if params.TaskID != "" { + conditions = append(conditions, "task_id = ?") + args = append(args, params.TaskID) + } + + if params.ShopName != "" { + conditions = append(conditions, "shop_name = ?") + args = append(args, params.ShopName) + } + if params.TaskType != 0 { + conditions = append(conditions, "task_type = ?") + args = append(args, params.TaskType) + } + + // 构建 WHERE子句 + whereClause := "" + if len(conditions) > 0 { + whereClause = "WHERE " + strings.Join(conditions, " AND ") + } + + // 查询总数 + var total int64 + countSQL := fmt.Sprintf(`SELECT COUNT(*) FROM task_records %s`, whereClause) + var countErr error + if len(args) > 0 { + countErr = golabl.SqliteDb.QueryRow(countSQL, args...).Scan(&total) + } else { + countErr = golabl.SqliteDb.QueryRow(countSQL).Scan(&total) + } + + if countErr != nil { + return nil, 0, fmt.Errorf("查询总数失败: %v", countErr) + } + + // 分页查询 + querySQL := fmt.Sprintf(` + SELECT id,user_id, shop_id, task_id, shop_name,is_export, task_type,create_at + FROM task_records + %s + ORDER BY id DESC + LIMIT ? OFFSET ? + `, whereClause) + + // 添加分页参数到 args + queryArgs := append(args, pageSize, offset) + rows, err := golabl.SqliteDb.Query(querySQL, queryArgs...) + if err != nil { + return nil, 0, fmt.Errorf("查询失败: %v", err) + } + defer rows.Close() + + var records []sqLiteType.TaskRecords + + for rows.Next() { + var record sqLiteType.TaskRecords + err = rows.Scan(&record.ID, &record.UserID, &record.ShopID, &record.TaskID, &record.ShopName, &record.IsExport, &record.TaskType, &record.CreateAt) + if err != nil { + return nil, 0, fmt.Errorf("扫描数据失败: %v", err) + } + records = append(records, record) + } + + if err = rows.Err(); err != nil { + return nil, 0, fmt.Errorf("遍历结果集错误: %v", err) + } + + return records, total, nil +} + +// UpdateTaskRecord 根据任务ID更新任务记录 +func UpdateTaskRecord(record sqLiteType.TaskRecords) error { + updateSQL := `UPDATE task_records SET user_id = ?, shop_id = ?, task_id = ?, shop_name = ?, task_type = ?, is_export = ? WHERE id = ?` + _, err := golabl.SqliteDb.Exec(updateSQL, record.UserID, record.ShopID, record.TaskID, record.ShopName, record.TaskType, record.IsExport, record.ID) + return err +} + +// DeleteTaskRecordsByTaskID 根据任务ID删除数据 +// @param taskID 任务ID +// @return error 错误 +func DeleteTaskRecordsByTaskID(taskID string) error { + _, err := golabl.SqliteDb.Exec("DELETE FROM task_records WHERE task_id = ?", taskID) + return err +} + +// GetTaskRecordByTaskID 根据taskId查询单个任务记录 +// @param taskID 任务ID +// @return *TaskRecord 记录指针 +// @return error 错误信息 +func GetTaskRecordByTaskID(taskID string) (*sqLiteType.TaskRecords, error) { + query := `SELECT id,user_id, shop_id, task_id, shop_name, task_type, create_at + FROM task_records + WHERE task_id = ? + LIMIT 1` + + var record sqLiteType.TaskRecords + var createAtStr string + + err := golabl.SqliteDb.QueryRow(query, taskID).Scan( + &record.ID, + &record.UserID, + &record.ShopID, + &record.TaskID, + &record.ShopName, + &record.TaskType, + &createAtStr, + ) + + if err != nil { + return nil, fmt.Errorf("查询失败: %v", err) + } + + return &record, nil +} + +// DeleteOldTaskRecords 删除task_records表中N天前的记录 +// @return error 错误信息 +func DeleteOldTaskRecords() error { + // 使用SQLite的date函数计算N天前 + days := golabl.Config.Server.DataDay + deleteSQL := fmt.Sprintf(` + DELETE FROM task_records + WHERE create_at < datetime('now', 'localtime', '-%d days') +`, days) + result, err := golabl.SqliteDb.Exec(deleteSQL) + if err != nil { + return fmt.Errorf("删除旧数据失败: %v", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("获取影响行数失败: %v", err) + } + + fmt.Printf("已删除 %d 条大于N天的记录\n", rowsAffected) + return nil +} + +// GetTaskRecords24Hour 查询task_records中24小时内的所有数据 +func GetTaskRecords24Hour() ([]sqLiteType.TaskRecords, error) { + // 查询24小时内的记录,按创建时间倒序排列 + querySQL := ` + SELECT id, user_id, shop_id, task_id, shop_name, is_export, task_type, create_at + FROM task_records + WHERE create_at >= datetime('now', 'localtime', '-24 hours') + AND create_at <= datetime('now', 'localtime', '-10 minutes') + ORDER BY create_at DESC` + + // 执行查询 + rows, err := golabl.SqliteDb.Query(querySQL) + if err != nil { + return nil, fmt.Errorf("查询24小时内任务记录失败: %v", err) + } + defer rows.Close() // 确保结果集最终被关闭 + + // 初始化结果切片 + var records []sqLiteType.TaskRecords + + // 遍历查询结果 + for rows.Next() { + var record sqLiteType.TaskRecords + // 扫描每一行数据到结构体中 + err = rows.Scan( + &record.ID, + &record.UserID, + &record.ShopID, + &record.TaskID, + &record.ShopName, + &record.IsExport, + &record.TaskType, + &record.CreateAt, + ) + if err != nil { + return nil, fmt.Errorf("扫描任务记录数据失败: %v", err) + } + records = append(records, record) + } + + // 检查遍历过程中是否有错误 + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("遍历任务记录结果集错误: %v", err) + } + + // 返回查询结果 + return records, nil +} + +// GetTaskRecordsOldList 获取task_records表中N天前的记录 +func GetTaskRecordsOldList() ([]sqLiteType.TaskRecords, error) { + days := golabl.Config.Server.DataDay + querySQL := fmt.Sprintf(`SELECT id, user_id, shop_id, task_id, shop_name, is_export, task_type, create_at + FROM task_records + WHERE create_at < datetime('now', 'localtime', '-%d days') +`, days) + // 执行查询 + rows, err := golabl.SqliteDb.Query(querySQL) + if err != nil { + return nil, fmt.Errorf("查询24小时内任务记录失败: %v", err) + } + defer rows.Close() // 确保结果集最终被关闭 + // 初始化结果切片 + var records []sqLiteType.TaskRecords + + // 遍历查询结果 + for rows.Next() { + var record sqLiteType.TaskRecords + // 扫描每一行数据到结构体中 + err = rows.Scan( + &record.ID, + &record.UserID, + &record.ShopID, + &record.TaskID, + &record.ShopName, + &record.IsExport, + &record.TaskType, + &record.CreateAt, + ) + if err != nil { + return nil, fmt.Errorf("扫描任务记录数据失败: %v", err) + } + records = append(records, record) + } + + // 检查遍历过程中是否有错误 + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("遍历任务记录结果集错误: %v", err) + } + + // 返回查询结果 + return records, nil +} + +// GetTaskByShopIdAndTaskType 根据 shopId和 taskType获取任务记录 +// @param taskId 任务ID +// @param taskType 任务类型 +// @return []sqLiteType.TaskRecords 任务列表 +// @return error 错误信息 +func GetTaskByShopIdAndTaskType(taskId string, taskType int64) ([]sqLiteType.TaskRecords, error) { + query := `SELECT id, user_id, shop_id, task_id, shop_name, task_type, create_at + FROM task_records + WHERE shop_id = ? AND task_type = ?` + var records []sqLiteType.TaskRecords + rows, err := golabl.SqliteDb.Query(query, taskId, taskType) + if err != nil { + return nil, fmt.Errorf("查询任务记录失败: %v", err) + } + defer rows.Close() + for rows.Next() { + var record sqLiteType.TaskRecords + err = rows.Scan( + &record.ID, + &record.UserID, + &record.ShopID, + &record.TaskID, + &record.ShopName, + &record.TaskType, + &record.CreateAt, + ) + if err != nil { + return nil, fmt.Errorf("扫描任务记录数据失败: %v", err) + } + records = append(records, record) + } + return records, nil +} + +// GetAllTask 查询所有任务 +func GetAllTask() ([]sqLiteType.TaskRecords, error) { + + query := `SELECT id, user_id, shop_id, task_id, shop_name, task_type, create_at + FROM task_records` + var records []sqLiteType.TaskRecords + rows, err := golabl.SqliteDb.Query(query) + if err != nil { + return nil, fmt.Errorf("查询任务记录失败: %v", err) + } + defer rows.Close() + for rows.Next() { + var record sqLiteType.TaskRecords + err = rows.Scan( + &record.ID, + &record.UserID, + &record.ShopID, + &record.TaskID, + &record.ShopName, + &record.TaskType, + &record.CreateAt, + ) + if err != nil { + return nil, fmt.Errorf("扫描任务记录数据失败: %v", err) + } + records = append(records, record) + } + return records, nil +} diff --git a/service/task.go b/service/task.go new file mode 100644 index 0000000..1572a6a --- /dev/null +++ b/service/task.go @@ -0,0 +1,664 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "planA/initialization/golabl" + "planA/tool" + toolPdd "planA/tool/pdd" + _type "planA/type" + "strconv" + "time" + + "github.com/go-redis/redis/v8" +) + +// ============================================ +// 任务头信息(Header)操作 +// 数据结构: Hash +// 键格式: {taskKey}:header +// ============================================ + +// GetTaskHeader 获取任务头信息 +// @param client Redis客户端 +// @param taskKey 任务键 +// @return _type.TaskHeader 任务头信息 +// @return error 错误信息 +func GetTaskHeader(taskKey string) (_type.TaskHeader, error) { + var header _type.TaskHeader + headerKey := getHeaderKey(taskKey) + headerMap, err := golabl.RedisDbA.HGetAll(golabl.Ctx, headerKey).Result() + if err != nil { + return header, err + } + + return parseHeaderMap(headerMap) +} + +// UpdateTaskHeader 更新任务头信息 +// @param taskKey 任务键 +// @param header 任务头信息 +// @return error 错误信息 +func UpdateTaskHeader(taskKey string, header _type.TaskHeader) error { + // 将结构体转为 map + headerMap, err := tool.StructToMap(header) + if err != nil { + return fmt.Errorf("转换Header为map失败: %w", err) + } + + // 特殊处理price_mod字段 + priceModJSON, err := json.Marshal(headerMap["price_mod"]) + if err != nil { + return fmt.Errorf("转换price_mod为JSON失败: %w", err) + } + headerMap["price_mod"] = priceModJSON + // 保存到 Redis + headerKey := getHeaderKey(taskKey) + + if err := saveHashMap(headerKey, headerMap); err != nil { + return err + } + return nil +} + +// UpdateHeaderStatus 更新任务头信息中的状态 +// @param client Redis客户端 +// @param taskKey 任务键 +// @param status 任务状态(1=运行中 2=已暂停 3=已停止) +// @return error 错误信息 +func UpdateHeaderStatus(taskKey string, status int64) error { + headerKey := getHeaderKey(taskKey) + if err := golabl.RedisDbA.HSet(golabl.Ctx, headerKey, "status", status).Err(); err != nil { + return err + } + golabl.RedisDbA.Expire(golabl.Ctx, headerKey, golabl.RedisExp) + return nil +} + +// ============================================ +// 任务体信息(Body)操作 +// 数据结构: List +// 键格式: {taskKey}:body_wait - 等待处理的任务队列 +// {taskKey}:body_over - 已完成处理的任务队列 +// ============================================ + +// UpdateTaskBodyWait 添加任务到等待队列 +// @param taskKey 任务键 +// @param taskBody 任务体数据 +// @return error 错误信息 +func UpdateTaskBodyWait(taskKey string, taskBody _type.TaskBody) error { + bodyWaitKey := getBodyWaitKey(taskKey) + + // 序列化任务数据 + bodyWaitJSON, err := json.Marshal(taskBody) + if err != nil { + return fmt.Errorf("序列化任务数据失败: %w", err) + } + + // 推送到列表尾部 + return golabl.RedisDbA.RPush(golabl.Ctx, bodyWaitKey, string(bodyWaitJSON)).Err() +} + +// GetListLength 获取等待队列长度 +// @param taskKey 任务键 +// @return int64 队列长度 +// @return error 错误信息 +func GetListLength(taskKey string) (int64, error) { + bodyWaitKey := getBodyWaitKey(taskKey) + return golabl.RedisDbA.LLen(golabl.Ctx, bodyWaitKey).Result() +} + +// GetTaskBodyOver 分页获取已完成任务 +// @param taskKey 任务键 +// @param page 页码 +// @param size 每页数量 +// @return []_type.TaskBody 任务体列表 +// @return error 错误信息 +func GetTaskBodyOver(taskKey string, page int, size int) ([]_type.TaskBody, int64, error) { + return GetBodyOverDataByBatch(taskKey, page, size) +} + +// GetBodyOverCount 获取已完成任务总数 +// @param taskKey 任务键 +// @return int64 总数 +// @return error 错误信息 +func GetBodyOverCount(taskKey string) (int64, error) { + bodyOverKey := getBodyOverKey(taskKey) + return golabl.RedisDbA.LLen(golabl.Ctx, bodyOverKey).Result() +} + +// GetBodyOverDataByBatch 批量获取已完成任务数据 +// @param taskKey 任务键 +// @param page 页 +// @param size 页数量 +// @return []_type.TaskBody 任务体列表 +// @return int64 总数 +// @return error 错误信息 +func GetBodyOverDataByBatch(taskKey string, page, size int) ([]_type.TaskBody, int64, error) { + var bodyOverArr []_type.TaskBody + + bodyOverKey := getBodyOverKey(taskKey) + + // 获取总数 + total, err := golabl.RedisDbA.LLen(golabl.Ctx, bodyOverKey).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return bodyOverArr, 0, nil + } + return bodyOverArr, 0, fmt.Errorf("获取body_over总数错误: %w", err) + } + + // 计算起始索引(从0开始) + start := (page - 1) * size + end := start + size - 1 + + // 如果起始索引超出范围,直接返回空数据 + if start >= int(total) { + return bodyOverArr, total, nil + } + + bodyOverStr, err := golabl.RedisDbA.LRange(golabl.Ctx, bodyOverKey, int64(start), int64(end)).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return bodyOverArr, total, nil + } + return bodyOverArr, 0, fmt.Errorf("获取body_over数据错误: %w", err) + } + + list, err := parseTaskBodyList(bodyOverStr) + if err != nil { + return nil, 0, err + } + + return list, total, nil +} + +// ClearBodyOver 清空已完成任务队列 +// @param client Redis客户端 +// @param taskKey 任务键 +// @return error 错误信息 +func ClearBodyOver(taskKey string) error { + return golabl.RedisDbA.Del(golabl.Ctx, getBodyOverKey(taskKey)).Err() +} + +// ============================================ +// 任务尾信息(Footer)操作 +// 数据结构: Hash +// 键格式: {taskKey}:footer +// ============================================ + +// GetTaskFooter 获取任务尾信息 +// @param taskKey 任务键 +// @return _type.TaskFooter 任务尾信息 +// @return error 错误信息 +func GetTaskFooter(taskKey string) (_type.TaskFooter, error) { + var footer _type.TaskFooter + footerKey := getFooterKey(taskKey) + footerMap, err := golabl.RedisDbA.HGetAll(golabl.Ctx, footerKey).Result() + if err != nil { + return footer, fmt.Errorf("获取Footer失败: %w", err) + } + + return footer, parseTaskFooter(footerMap, &footer) +} + +// UpdateTaskFooter 更新任务尾信息 +// @param taskKey 任务键 +// @param footer 任务尾信息 +// @return error 错误信息 +func UpdateTaskFooter(taskKey string, footer *_type.TaskFooter) error { + footerMap := map[string]interface{}{ + "task_count": footer.TaskCount, + "task_count_true": footer.TaskCountTrue, + "task_count_wait": footer.TaskCountWait.Load(), + "task_count_over": footer.TaskCountOver.Load(), + "task_count_success": footer.TaskCountSuccess.Load(), + "task_count_error": footer.TaskCountError.Load(), + "task_qpm": footer.TaskQpm, + "last_index": footer.LastIndex, + } + + footerKey := getFooterKey(taskKey) + if err := saveHashMap(footerKey, footerMap); err != nil { + return err + } + + return nil +} + +// ============================================ +// 导出文件(BodyFile)操作 +// 数据结构: Hash +// 键格式: {taskKey}:body_file +// ============================================ + +// UpdateExportFileProgress 更新导出文件进度(每次自增+1) +// @param taskKey 任务键 +// @return error 错误信息 +func UpdateExportFileProgress(taskKey string) error { + // 使用 HIncrBy 对指定字段进行自增操作,每次增加1 + return golabl.RedisDbA.HIncrBy(golabl.Ctx, getBodyFileKey(taskKey), "complete", 1).Err() +} + +// GetExportFileProgress 获取导出文件进度 +// @param taskKey 任务键 +// @return int 完成进度 +// @return error 错误信息 +func GetExportFileProgress(taskKey string) (int, error) { + return golabl.RedisDbA.HGet(golabl.Ctx, getBodyFileKey(taskKey), "complete").Int() +} + +// ============================================ +// 进程号管理操作 +// 数据结构: Hash字段 +// 键格式: {headerKey} +// 字段: process_number +// ============================================ + +// GetProcessId 获取进程号 +// @param taskKey 键 +// @return string 进程号 +// @return error 错误信息 +func GetProcessId(taskKey string) (string, error) { + headerKey := getHeaderKey(taskKey) + return golabl.RedisDbA.HGet(golabl.Ctx, headerKey, "process_number").Result() +} + +// SetProcessId 设置进程号 +// @param taskKey 键 +// @param processId 进程号 +// @return error 错误信息 +func SetProcessId(taskKey string, processId string) error { + headerKey := getHeaderKey(taskKey) + golabl.RedisDbA.HSet(golabl.Ctx, headerKey, "process_number", processId).Err() + golabl.RedisDbA.HSet(golabl.Ctx, headerKey, "process_number", processId).Err() + return golabl.RedisDbA.HSet(golabl.Ctx, headerKey, "process_number", processId).Err() +} + +// DeleteProcessId 删除进程号 +// @param taskKey 头信息键 +// @return error 错误信息 +func DeleteProcessId(taskKey string) error { + headerKey := getHeaderKey(taskKey) + return golabl.RedisDbA.HDel(golabl.Ctx, headerKey, "process_number").Err() +} + +// ============================================ +// 复合操作(涉及多个数据结构) +// ============================================ + +// UpdateTaskCountTrue 更新任务计数(原子操作) +// 同时更新Header和Footer中的task_count_true,以及Footer中的task_count_wait +// @param taskKey 任务键 +// @param num 增减数量 +// @return error 错误信息 +func UpdateTaskCountTrue(taskKey string, num int64) error { + // 使用Pipeline确保原子性 + pipe := golabl.RedisDbA.Pipeline() + + // 更新Header + headerKey := getHeaderKey(taskKey) + pipe.HIncrBy(golabl.Ctx, headerKey, "task_count_true", num) + + // 更新Footer + footerKey := getFooterKey(taskKey) + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_true", num) + pipe.HIncrBy(golabl.Ctx, footerKey, "task_count_wait", num) + + // 设置过期时间 + pipe.Expire(golabl.Ctx, headerKey, golabl.RedisExp) + pipe.Expire(golabl.Ctx, footerKey, golabl.RedisExp) + + return executePipeline(pipe) +} + +// StopTask 停止任务 +// 更新Header状态为已停止,并清空等待队列 +// @param taskId 任务ID +// @return error 错误信息 +func StopTask(taskId string) error { + // 开启事务 + pipe := golabl.RedisDbA.TxPipeline() + + // 更新任务状态 + headerKey := getHeaderKey(taskId) + pipe.HSet(golabl.Ctx, headerKey, "status", int64(_type.TaskStatusStopped)) + + // 设置过期时间 + pipe.Expire(golabl.Ctx, headerKey, golabl.RedisExp) + + // 清空等待队列 + bodyWaitKey := getBodyWaitKey(taskId) + pipe.Del(golabl.Ctx, bodyWaitKey) + + return executePipeline(pipe) +} + +// DelTask 删除任务 +// @param taskId 任务ID +// @return error 错误信息 +func DelTask(taskId string) error { + // 开启事务 + pipe := golabl.RedisDbA.TxPipeline() + + // 删除 header + pipe.Del(golabl.Ctx, getHeaderKey(taskId)) + // 删除 footer + pipe.Del(golabl.Ctx, getFooterKey(taskId)) + // 删除 body_wait + pipe.Del(golabl.Ctx, getBodyWaitKey(taskId)) + // 删除 body_over + pipe.Del(golabl.Ctx, getBodyOverKey(taskId)) + // 删除 body_file + pipe.Del(golabl.Ctx, getBodyFileKey(taskId)) + // 删除body_data + pipe.Del(golabl.Ctx, getBodyDataKey(taskId)) + //删除 body_backup + pipe.Del(golabl.Ctx, getBodyBackupKey(taskId)) + + return executePipeline(pipe) +} + +// GetBodyBackupLen 获取body_backup长度 +// @param taskId 任务ID +// @return int64 body_backup长度 +// @return error 错误信息 +func GetBodyBackupLen(taskId string) (int64, error) { + return golabl.RedisDbA.LLen(golabl.Ctx, getBodyBackupKey(taskId)).Result() +} + +// GetBodyBackupOne 读取一条body_backup数据 +// @param taskId 任务ID +// @return string body_backup数据 +// @return error 错误信息 +func GetBodyBackupOne(taskId string) (string, error) { + return golabl.RedisDbA.LPop(golabl.Ctx, getBodyBackupKey(taskId)).Result() +} + +//********************************************以下为是有方法*****************************************// + +// getHeaderKey 获取任务头信息的Redis键 +// @param taskKey 任务键 +// @return string 头信息键 +func getHeaderKey(taskKey string) string { + return taskKey + ":header" +} + +// getFooterKey 获取任务尾信息的Redis键 +// @param taskKey 任务键 +// @return string 尾信息键 +func getFooterKey(taskKey string) string { + return taskKey + ":footer" +} + +// getBodyWaitKey 获取等待任务体的Redis键 +// @param taskKey 任务键 +// @return string 等待任务体键 +func getBodyWaitKey(taskKey string) string { + return taskKey + ":body_wait" +} + +// getBodyOverKey 获取已完成任务体的Redis键 +// @param taskKey 任务键 +// @return string 已完成任务体键 +func getBodyOverKey(taskKey string) string { + return taskKey + ":body_over" +} + +// getBodyDataKey 获取已完成任务体的Redis键 +// @param taskKey 任务键 +// @return string 已完成任务体键 +func getBodyDataKey(taskKey string) string { + return taskKey + ":body_data" +} + +// getBodyBackupKey 获取已完成任务体的Redis键 +// @param taskKey 任务键 +// @return string 已完成任务体键 +func getBodyBackupKey(taskKey string) string { + return taskKey + ":body_backup" +} + +// getBodyFileKey 获取已完成任务体的Redis键 +// @param taskKey 任务键 +// @return string 已完成任务体键 +func getBodyFileKey(taskKey string) string { + return taskKey + ":body_file" +} + +// parseHeaderMap 从map解析任务头信息 +// @param headerMap 头信息map +// @return _type.TaskHeader 解析后的头信息 +// @return error 错误信息 +func parseHeaderMap(headerMap map[string]string) (_type.TaskHeader, error) { + info := _type.TaskHeader{} + + for key, value := range headerMap { + switch key { + case "last_index", "task_count", "task_count_error", + "task_count_over", "task_count_success", "task_count_true", + "task_count_wait", "task_create_at", "task_over_at", "task_qpm", "task_type", "img_type", "update_type": + parseIntField(&info, key, value) + + case "price_mod": + var priceMod []_type.PriceMod + if err := json.Unmarshal([]byte(value), &priceMod); err == nil { + info.PriceMod = priceMod + } + + case "shop_msg": + var shopMsg _type.ShopMsg + if err := json.Unmarshal([]byte(value), &shopMsg); err == nil { + info.ShopMsg = shopMsg + } + + case "status": + if v, err := strconv.ParseInt(value, 10, 64); err == nil { + info.Status = _type.TaskStatus(v) + } + + case "shop_id", "ship_price_mod", "shop_name", "shop_type", "task_id": + setStringField(&info, key, value) + } + } + return info, nil +} + +// parseIntField 解析整数字段 +func parseIntField(info *_type.TaskHeader, key, value string) { + if v, err := strconv.ParseInt(value, 10, 64); err == nil { + switch key { + case "last_index": + info.LastIndex = v + case "task_count": + info.TaskCount = v + case "task_count_error": + info.TaskCountError = v + case "task_count_over": + info.TaskCountOver = v + case "task_count_success": + info.TaskCountSuccess = v + case "task_count_true": + info.TaskCountTrue = v + case "task_count_wait": + info.TaskCountWait = v + case "task_create_at": + info.TaskCreateAt = v + case "task_over_at": + info.TaskOverAt = v + case "task_qpm": + info.TaskQpm = v + case "task_type": + info.TaskType = v + case "img_type": + info.ImgType = v + case "update_type": + info.UpdateType = v + + } + } +} + +// setStringField 设置字符串字段 +func setStringField(info *_type.TaskHeader, key, value string) { + switch key { + case "ship_price_mod": + info.ShipPriceMod = value + case "shop_name": + info.ShopName = value + case "shop_type": + info.ShopType = value + case "task_id": + info.TaskId = value + case "shop_id": + info.ShopId = value + } +} + +// saveHashMap 保存哈希映射到Redis +// @param key Redis键 +// @param data 数据映射 +// @return error 错误信息 +func saveHashMap(key string, data map[string]interface{}) error { + for field, value := range data { + if err := golabl.RedisDbA.HSet(golabl.Ctx, key, field, value).Err(); err != nil { + return fmt.Errorf("保存字段 %s 失败: %w (值: %v)", field, err, value) + } + } + golabl.RedisDbA.Expire(golabl.Ctx, key, golabl.RedisExp) + return nil +} + +// parseTaskBodyList 解析任务体列表 +// @param bodyStrs 任务体字符串列表 +// @return []_type.TaskBody 解析后的任务体列表 +// @return error 错误信息 +func parseTaskBodyList(bodyStrs []string) ([]_type.TaskBody, error) { + var bodyList []_type.TaskBody + + for _, str := range bodyStrs { + var body _type.TaskBody + if err := json.Unmarshal([]byte(str), &body); err != nil { + return bodyList, fmt.Errorf("JSON解析错误: %w, 数据: %s", err, str) + } + bodyList = append(bodyList, body) + } + + return bodyList, nil +} + +// parseTaskFooter 解析任务尾信息 +// @param taskFooter 尾信息map +// @param footer 目标尾信息结构体 +// @return error 错误信息 +func parseTaskFooter(taskFooter map[string]string, footer *_type.TaskFooter) error { + var err error + + if footer.TaskCount, err = strconv.ParseInt(taskFooter["task_count"], 10, 64); err != nil { + footer.TaskCount = 0 + } + + if footer.TaskCountTrue, err = strconv.ParseInt(taskFooter["task_count_true"], 10, 64); err != nil { + footer.TaskCountTrue = 0 + } + + if taskCountWait, err := strconv.ParseInt(taskFooter["task_count_wait"], 10, 64); err == nil { + footer.TaskCountWait.Store(taskCountWait) + } + + if taskCountOver, err := strconv.ParseInt(taskFooter["task_count_over"], 10, 64); err == nil { + footer.TaskCountOver.Store(taskCountOver) + } + + if taskCountSuccess, err := strconv.ParseInt(taskFooter["task_count_success"], 10, 64); err == nil { + footer.TaskCountSuccess.Store(taskCountSuccess) + } + + if taskCountError, err := strconv.ParseInt(taskFooter["task_count_error"], 10, 64); err == nil { + footer.TaskCountError.Store(taskCountError) + } + + if footer.TaskQpm, err = strconv.ParseInt(taskFooter["task_qpm"], 10, 64); err != nil { + footer.TaskQpm = 0 + } + + if footer.LastIndex, err = strconv.ParseInt(taskFooter["last_index"], 10, 64); err != nil { + footer.LastIndex = 0 + } + + return nil +} + +// executePipeline 执行Redis管道操作 +// @param pipe Redis管道 +// @return error 错误信息 +func executePipeline(pipe redis.Pipeliner) error { + _, err := pipe.Exec(golabl.Ctx) + return err +} + +// ============================================ +// 其他 +// ============================================ + +// GetPddTokenList 获取token列表 +// @return []string token列表 +// @return error 错误信息 +func GetPddTokenList() ([]_type.Shop, error) { + var shopList []_type.Shop + //获取 店铺列表中所有的key + iter := golabl.RedisDbC.Scan(golabl.Ctx, 0, "*", 0).Iterator() + for iter.Next(golabl.Ctx) { + key := iter.Val() + //获取店铺信息 + shopInfo, getTaskShopErr := GetTaskShop(key) + if getTaskShopErr != nil { + return shopList, fmt.Errorf("获取店铺信息失败:" + getTaskShopErr.Error()) + } + // 解析 json数据 + shopData, err := toolPdd.ParseShopData(shopInfo) + if err != nil { + return shopList, fmt.Errorf("解析店铺数据失败:" + err.Error()) + } + if shopData.Shop == nil { + // 没有店铺数据 + continue + } + if shopData.Shop.ExpirationTime == "" { + // 过期时间为空 + continue + } + // 筛选出拼多多的店铺并且订阅未到期的 + expirationTime, err := parseTime(shopData.Shop.ExpirationTime) + if err != nil { + return shopList, fmt.Errorf("时间解析失败: %s, 原始值: %s", err.Error(), shopData.Shop.ExpirationTime) + } + + now := time.Now() + if shopData.Shop.ShopType == "1" && expirationTime.After(now) && shopData.Shop.DelFlag == "0" { + shopList = append(shopList, *shopData.Shop) + } + } + return shopList, nil +} + +// parseTime 解析时间字符串,支持多种格式 +func parseTime(timeStr string) (time.Time, error) { + // 定义支持的时间格式列表 + layouts := []string{ + time.RFC3339, // "2006-01-02T15:04:05Z07:00" + "2006-01-02T15:04:05-07:00", // "2026-03-27T21:31:17+08:00" + "2006-01-02 15:04:05", // "2026-04-05 23:59:59" + "2006-01-02 15:04:05 -0700", // 带时区但空格分隔的格式 + "2006-01-02T15:04:05", // 不带时区的T分隔格式 + } + + for _, layout := range layouts { + if t, err := time.Parse(layout, timeStr); err == nil { + return t, nil + } + } + + return time.Time{}, fmt.Errorf("无法解析时间字符串") +} diff --git a/test/check_redis/go.mod b/test/check_redis/go.mod new file mode 100644 index 0000000..1fb8cc1 --- /dev/null +++ b/test/check_redis/go.mod @@ -0,0 +1,10 @@ +module check_redis + +go 1.25 + +require github.com/go-redis/redis/v8 v8.11.5 + +require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect +) diff --git a/test/check_redis/go.sum b/test/check_redis/go.sum new file mode 100644 index 0000000..17906ec --- /dev/null +++ b/test/check_redis/go.sum @@ -0,0 +1,24 @@ +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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= diff --git a/test/check_redis/main.go b/test/check_redis/main.go new file mode 100644 index 0000000..6968f85 --- /dev/null +++ b/test/check_redis/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-redis/redis/v8" +) + +func main() { + r := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", DB: 0}) + ctx := context.Background() + + // 检查最新上下架任务的 body_over + tid := "2047906786013282306" + cnt, _ := r.LLen(ctx, tid+":body_over").Result() + fmt.Printf("body_over count: %d\n", cnt) + + for i := int64(0); i < cnt; i++ { + val, err := r.LIndex(ctx, tid+":body_over", i).Result() + if err != nil { + fmt.Printf(" [%d] error: %v\n", i, err) + continue + } + var item map[string]interface{} + json.Unmarshal([]byte(val), &item) + + bi, _ := item["book_info"].(map[string]interface{}) + isbn, _ := bi["isbn"].(string) + detail, _ := item["detail"].(map[string]interface{}) + + errMsg, _ := detail["error"].(string) + status, _ := detail["status"].(float64) + price, _ := detail["price"].(float64) + skuID, _ := detail["sku_id"].(float64) + goodsID, _ := detail["goods_id"].(float64) + + fmt.Printf(" [%d] isbn=%s status=%.0f price=%.0f sku_id=%.0f goods_id=%.0f error=%s\n", + i, isbn, status, price, skuID, goodsID, truncate(errMsg, 200)) + } +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} diff --git a/test/go.mod b/test/go.mod new file mode 100644 index 0000000..6ab5e3d --- /dev/null +++ b/test/go.mod @@ -0,0 +1,11 @@ +module plantest + +go 1.21 + +require github.com/go-redis/redis/v8 v8.11.5 + +require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/test/go.sum b/test/go.sum new file mode 100644 index 0000000..22f1327 --- /dev/null +++ b/test/go.sum @@ -0,0 +1,27 @@ +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/main.go b/test/main.go new file mode 100644 index 0000000..53925cd --- /dev/null +++ b/test/main.go @@ -0,0 +1,4399 @@ +package main + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "plantest/modules/kfz" + "plantest/modules/xianYu" + + "github.com/go-redis/redis/v8" + "gopkg.in/yaml.v3" +) + +// ============================================================ +// 配置(从 test.yaml 加载) +// ============================================================ + +// Config YAML 配置结构体 + +type RedisConfig struct { + Addr string `yaml:"addr"` + DB int `yaml:"db"` + Password string `yaml:"password"` +} + +type PDDConfig struct { + ShopID string `yaml:"shop_id"` + ShopType string `yaml:"shop_type"` + AppID string `yaml:"app_id"` + AppKey string `yaml:"app_key"` + VerifyURL string `yaml:"verify_url"` + VerifyBasicAuth string `yaml:"verify_basic_auth"` +} + +type XianyuConfig struct { + ShopID string `yaml:"shop_id"` + ShopType string `yaml:"shop_type"` + AppID int64 `yaml:"app_id"` + AppSecret string `yaml:"app_secret"` + Domain string `yaml:"domain"` + DLLPath string `yaml:"dll_path"` +} + +type KfzConfig struct { + ShopID string `yaml:"shop_id"` + ShopType string `yaml:"shop_type"` + AppID int `yaml:"app_id"` + AppSecret string `yaml:"app_secret"` + DLLPath string `yaml:"dll_path"` +} + +type TimeoutConfig struct { + WaitTimeout int `yaml:"wait_timeout"` + PollInterval int `yaml:"poll_interval"` + HTTPClientTimeout int `yaml:"http_client_timeout"` + CurlTimeout int `yaml:"curl_timeout"` + CurlRetryInterval int `yaml:"curl_retry_interval"` +} + +type PddPricePublishDelays struct { + AfterSend int `yaml:"after_send"` +} + +type PddPriceChangeDelays struct { + AfterCreateQueryDetail int `yaml:"after_create_query_detail"` + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterSendAPICheck int `yaml:"after_send_api_check"` +} + +type PddStockChangeDelays struct { + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterSendAPICheck int `yaml:"after_send_api_check"` +} + +type PddShelfOnOffDelays struct { + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterWaitAPICheck int `yaml:"after_wait_api_check"` + WaitBodyOverTimeout int `yaml:"wait_body_over_timeout"` +} + +type PddGoodsDeleteDelays struct { + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterSendAPICheck int `yaml:"after_send_api_check"` +} + +type XyPricePublishDelays struct { + AfterSend int `yaml:"after_send"` +} + +type XyPriceChangeDelays struct { + AfterCreateQueryDetail int `yaml:"after_create_query_detail"` + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterSendAPICheck int `yaml:"after_send_api_check"` +} + +type XyStockChangeDelays struct { + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterSendAPICheck int `yaml:"after_send_api_check"` +} + +type XyShelfOnOffDelays struct { + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterWaitAPICheck int `yaml:"after_wait_api_check"` + WaitBodyOverTimeout int `yaml:"wait_body_over_timeout"` +} + +type KfzPricePublishDelays struct { + AfterSend int `yaml:"after_send"` +} + +type KfzPriceChangeDelays struct { + AfterCreateQueryDetail int `yaml:"after_create_query_detail"` + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterSendAPICheck int `yaml:"after_send_api_check"` +} + +type KfzStockChangeDelays struct { + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterSendAPICheck int `yaml:"after_send_api_check"` +} + +type KfzShelfOnOffDelays struct { + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterWaitAPICheck int `yaml:"after_wait_api_check"` + WaitBodyOverTimeout int `yaml:"wait_body_over_timeout"` +} + +type KfzGoodsDeleteDelays struct { + AfterSendRedisCheck int `yaml:"after_send_redis_check"` + AfterSendAPICheck int `yaml:"after_send_api_check"` +} + +type DelaysConfig struct { + PddPricePublish PddPricePublishDelays `yaml:"pdd_price_publish"` + PddPriceChange PddPriceChangeDelays `yaml:"pdd_price_change"` + PddStockChange PddStockChangeDelays `yaml:"pdd_stock_change"` + PddShelfOnOff PddShelfOnOffDelays `yaml:"pdd_shelf_on_off"` + PddGoodsDelete PddGoodsDeleteDelays `yaml:"pdd_goods_delete"` + XyPricePublish XyPricePublishDelays `yaml:"xy_price_publish"` + XyPriceChange XyPriceChangeDelays `yaml:"xy_price_change"` + XyStockChange XyStockChangeDelays `yaml:"xy_stock_change"` + XyShelfOnOff XyShelfOnOffDelays `yaml:"xy_shelf_on_off"` + KfzPricePublish KfzPricePublishDelays `yaml:"kfz_price_publish"` + KfzPriceChange KfzPriceChangeDelays `yaml:"kfz_price_change"` + KfzStockChange KfzStockChangeDelays `yaml:"kfz_stock_change"` + KfzShelfOnOff KfzShelfOnOffDelays `yaml:"kfz_shelf_on_off"` + KfzGoodsDelete KfzGoodsDeleteDelays `yaml:"kfz_goods_delete"` +} + +type PddPricePublishTestData struct { + ISBNSuccess string `yaml:"isbn_success"` + PriceSuccess int64 `yaml:"price_success"` + ISBNPriceZero string `yaml:"isbn_price_zero"` + PriceZero int64 `yaml:"price_zero"` + ISBNBannedWord string `yaml:"isbn_banned_word"` + PriceBanned int64 `yaml:"price_banned"` +} + +type PddPriceChangeTestData struct { + NewPrice int64 `yaml:"new_price"` +} + +type PddStockChangeTestData struct { + NewStock int64 `yaml:"new_stock"` +} + +type XyPricePublishTestData struct { + ISBNSuccess string `yaml:"isbn_success"` + PriceSuccess int64 `yaml:"price_success"` +} + +type XyPriceChangeTestData struct { + NewPrice int64 `yaml:"new_price"` +} + +type XyStockChangeTestData struct { + NewStock int64 `yaml:"new_stock"` +} + +type KfzPricePublishTestData struct { + ISBNSuccess string `yaml:"isbn_success"` + PriceSuccess int64 `yaml:"price_success"` +} + +type KfzPriceChangeTestData struct { + NewPrice int64 `yaml:"new_price"` +} + +type KfzStockChangeTestData struct { + NewStock int64 `yaml:"new_stock"` +} + +type PullGoodsTestData struct { + SearchPageSize int64 `yaml:"search_page_size"` + BodyWaitMaxSearch int64 `yaml:"body_wait_max_search"` +} + +type TestDataConfig struct { + PddPricePublish PddPricePublishTestData `yaml:"pdd_price_publish"` + PddPriceChange PddPriceChangeTestData `yaml:"pdd_price_change"` + PddStockChange PddStockChangeTestData `yaml:"pdd_stock_change"` + XyPricePublish XyPricePublishTestData `yaml:"xy_price_publish"` + XyPriceChange XyPriceChangeTestData `yaml:"xy_price_change"` + XyStockChange XyStockChangeTestData `yaml:"xy_stock_change"` + KfzPricePublish KfzPricePublishTestData `yaml:"kfz_price_publish"` + KfzPriceChange KfzPriceChangeTestData `yaml:"kfz_price_change"` + KfzStockChange KfzStockChangeTestData `yaml:"kfz_stock_change"` + PullGoods PullGoodsTestData `yaml:"pull_goods"` +} + +type TaskTypeConfig struct { + PricePublish string `yaml:"price_publish"` + PullGoods string `yaml:"pull_goods"` + PriceStockShelf string `yaml:"price_stock_shelf"` + XyPriceStockShelf string `yaml:"xy_price_stock_shelf"` +} + +type TaskCreateConfig struct { + TaskCount string `yaml:"task_count"` + ImgType string `yaml:"img_type"` +} + +type BodyOverMinConfig struct { + PddPricePublish int `yaml:"pdd_price_publish"` + PddPriceChange int `yaml:"pdd_price_change"` + PddShelfOnOff int `yaml:"pdd_shelf_on_off"` + PddGoodsDelete int `yaml:"pdd_goods_delete"` + XyPriceChange int `yaml:"xy_price_change"` + XyShelfOnOff int `yaml:"xy_shelf_on_off"` + KfzPricePublish int `yaml:"kfz_price_publish"` + KfzPriceChange int `yaml:"kfz_price_change"` + KfzStockChange int `yaml:"kfz_stock_change"` + KfzShelfOnOff int `yaml:"kfz_shelf_on_off"` + KfzGoodsDelete int `yaml:"kfz_goods_delete"` +} + +type Config struct { + BaseURL string `yaml:"base_url"` + Redis RedisConfig `yaml:"redis"` + PDD PDDConfig `yaml:"pdd"` + Xianyu XianyuConfig `yaml:"xianyu"` + Kfz KfzConfig `yaml:"kfz"` + Timeout TimeoutConfig `yaml:"timeout"` + Delays DelaysConfig `yaml:"delays"` + TestData TestDataConfig `yaml:"test_data"` + TaskType TaskTypeConfig `yaml:"task_type"` + TaskCreate TaskCreateConfig `yaml:"task_create"` + GoodsStatus map[int]string `yaml:"goods_status"` + BodyOverMin BodyOverMinConfig `yaml:"body_over_min"` +} + +// 全局配置变量(从 test.yaml 加载,保持与原 const 同名以最小化代码改动) +var ( + BaseURL string + ShopID string + ShopType string + XyShopID string + XyShopType string + PddAppID string + PddAppKey string + RedisAddr string + RedisDB int + RedisPwd string + + // 等待后台处理超时 + WaitTimeout time.Duration + // 轮询间隔 + PollInterval time.Duration + + // 校验接口配置 + VerifyURL string + VerifyBasicAuth string + + // 闲鱼 DLL 配置 + XyAppID int64 + XyAppSecret string + XyDllPath string + XyDomain string + + // 孔夫子 DLL 配置 + KfzAppID int + KfzAppSecret string + KfzDllPath string + + // 场景延迟配置 + DelayPddPublishAfterSend time.Duration + DelayPddChangeAfterQuery time.Duration + DelayPddChangeAfterSend time.Duration + DelayPddChangeAfterAPI time.Duration + DelayPddStockAfterSend time.Duration + DelayPddStockAfterAPI time.Duration + DelayPddShelfAfterSend time.Duration + DelayPddShelfAfterWait time.Duration + DelayPddShelfBodyOverTimeout time.Duration + DelayPddDeleteAfterSend time.Duration + DelayPddDeleteAfterAPI time.Duration + DelayXyPublishAfterSend time.Duration + DelayXyPriceChangeAfterQuery time.Duration + DelayXyPriceChangeAfterSend time.Duration + DelayXyPriceChangeAfterAPI time.Duration + DelayXyStockAfterSend time.Duration + DelayXyStockAfterAPI time.Duration + DelayXyShelfAfterSend time.Duration + DelayXyShelfAfterWait time.Duration + DelayXyShelfBodyOverTimeout time.Duration + + // 孔夫子场景延迟 + DelayKfzPublishAfterSend time.Duration + DelayKfzPriceChangeAfterQuery time.Duration + DelayKfzPriceChangeAfterSend time.Duration + DelayKfzPriceChangeAfterAPI time.Duration + DelayKfzStockAfterSend time.Duration + DelayKfzStockAfterAPI time.Duration + DelayKfzShelfAfterSend time.Duration + DelayKfzShelfAfterWait time.Duration + DelayKfzShelfBodyOverTimeout time.Duration + DelayKfzDeleteAfterSend time.Duration + DelayKfzDeleteAfterAPI time.Duration + + // 测试数据 + TestISBNSuccess string + TestPriceSuccess int64 + TestISBNPriceZero string + TestPriceZero int64 + TestISBNBanned string + TestPriceBanned int64 + TestNewPrice int64 + TestNewStock int64 + TestXyISBNSuccess string + TestXyPriceSuccess int64 + TestXyNewPrice int64 + TestXyNewStock int64 + TestKfzISBNSuccess string + TestKfzPriceSuccess int64 + TestKfzNewPrice int64 + TestKfzNewStock int64 + + // 拉取搜索配置 + SearchPageSize int64 + BodyWaitMaxSearch int64 + + // 任务类型 + TaskTypePricePublish string + TaskTypePullGoods string + TaskTypePriceStockShelf string + TaskTypeXyPriceStockShelf string + + // 任务创建参数 + TaskCount string + ImgType string + + // 商品状态映射 + StatusName map[int]string + + // body_over 最少条数 + BodyOverMinPublish int + BodyOverMinChange int + BodyOverMinShelf int + BodyOverMinDelete int + BodyOverMinXyPriceChange int + BodyOverMinXyShelfOnOff int + BodyOverMinKfzPublish int + BodyOverMinKfzPriceChange int + BodyOverMinKfzStockChange int + BodyOverMinKfzShelfOnOff int + BodyOverMinKfzDelete int + + // HTTP 客户端超时 + HTTPClientTimeout time.Duration + CurlTimeout time.Duration + CurlRetryInterval time.Duration +) + +// configPath 配置文件路径 +var configPath = "test.yaml" + +// loadConfig 从 YAML 文件加载配置 +func loadConfig(path string) error { + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("读取配置文件失败: %w", err) + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return fmt.Errorf("解析配置文件失败: %w", err) + } + + // 基础配置 + BaseURL = cfg.BaseURL + + // Redis + RedisAddr = cfg.Redis.Addr + RedisDB = cfg.Redis.DB + RedisPwd = cfg.Redis.Password + + // 拼多多 + ShopID = cfg.PDD.ShopID + ShopType = cfg.PDD.ShopType + PddAppID = cfg.PDD.AppID + PddAppKey = cfg.PDD.AppKey + VerifyURL = cfg.PDD.VerifyURL + VerifyBasicAuth = cfg.PDD.VerifyBasicAuth + + // 闲鱼 + XyShopID = cfg.Xianyu.ShopID + XyShopType = cfg.Xianyu.ShopType + XyAppID = cfg.Xianyu.AppID + XyAppSecret = cfg.Xianyu.AppSecret + XyDllPath = cfg.Xianyu.DLLPath + XyDomain = cfg.Xianyu.Domain + + // 孔夫子 + KfzAppID = cfg.Kfz.AppID + KfzAppSecret = cfg.Kfz.AppSecret + KfzDllPath = cfg.Kfz.DLLPath + + // 超时 + WaitTimeout = time.Duration(cfg.Timeout.WaitTimeout) * time.Second + PollInterval = time.Duration(cfg.Timeout.PollInterval) * time.Second + HTTPClientTimeout = time.Duration(cfg.Timeout.HTTPClientTimeout) * time.Second + CurlTimeout = time.Duration(cfg.Timeout.CurlTimeout) * time.Second + CurlRetryInterval = time.Duration(cfg.Timeout.CurlRetryInterval) * time.Second + + // 场景延迟 + DelayPddPublishAfterSend = time.Duration(cfg.Delays.PddPricePublish.AfterSend) * time.Second + DelayPddChangeAfterQuery = time.Duration(cfg.Delays.PddPriceChange.AfterCreateQueryDetail) * time.Second + DelayPddChangeAfterSend = time.Duration(cfg.Delays.PddPriceChange.AfterSendRedisCheck) * time.Second + DelayPddChangeAfterAPI = time.Duration(cfg.Delays.PddPriceChange.AfterSendAPICheck) * time.Second + DelayPddStockAfterSend = time.Duration(cfg.Delays.PddStockChange.AfterSendRedisCheck) * time.Second + DelayPddStockAfterAPI = time.Duration(cfg.Delays.PddStockChange.AfterSendAPICheck) * time.Second + DelayPddShelfAfterSend = time.Duration(cfg.Delays.PddShelfOnOff.AfterSendRedisCheck) * time.Second + DelayPddShelfAfterWait = time.Duration(cfg.Delays.PddShelfOnOff.AfterWaitAPICheck) * time.Second + DelayPddShelfBodyOverTimeout = time.Duration(cfg.Delays.PddShelfOnOff.WaitBodyOverTimeout) * time.Second + DelayXyPublishAfterSend = time.Duration(cfg.Delays.XyPricePublish.AfterSend) * time.Second + DelayXyPriceChangeAfterQuery = time.Duration(cfg.Delays.XyPriceChange.AfterCreateQueryDetail) * time.Second + DelayXyPriceChangeAfterSend = time.Duration(cfg.Delays.XyPriceChange.AfterSendRedisCheck) * time.Second + DelayXyPriceChangeAfterAPI = time.Duration(cfg.Delays.XyPriceChange.AfterSendAPICheck) * time.Second + DelayXyStockAfterSend = time.Duration(cfg.Delays.XyStockChange.AfterSendRedisCheck) * time.Second + DelayXyStockAfterAPI = time.Duration(cfg.Delays.XyStockChange.AfterSendAPICheck) * time.Second + DelayXyShelfAfterSend = time.Duration(cfg.Delays.XyShelfOnOff.AfterSendRedisCheck) * time.Second + DelayXyShelfAfterWait = time.Duration(cfg.Delays.XyShelfOnOff.AfterWaitAPICheck) * time.Second + DelayXyShelfBodyOverTimeout = time.Duration(cfg.Delays.XyShelfOnOff.WaitBodyOverTimeout) * time.Second + + // 孔夫子场景延迟 + DelayKfzPublishAfterSend = time.Duration(cfg.Delays.KfzPricePublish.AfterSend) * time.Second + DelayKfzPriceChangeAfterQuery = time.Duration(cfg.Delays.KfzPriceChange.AfterCreateQueryDetail) * time.Second + DelayKfzPriceChangeAfterSend = time.Duration(cfg.Delays.KfzPriceChange.AfterSendRedisCheck) * time.Second + DelayKfzPriceChangeAfterAPI = time.Duration(cfg.Delays.KfzPriceChange.AfterSendAPICheck) * time.Second + DelayKfzStockAfterSend = time.Duration(cfg.Delays.KfzStockChange.AfterSendRedisCheck) * time.Second + DelayKfzStockAfterAPI = time.Duration(cfg.Delays.KfzStockChange.AfterSendAPICheck) * time.Second + DelayKfzShelfAfterSend = time.Duration(cfg.Delays.KfzShelfOnOff.AfterSendRedisCheck) * time.Second + DelayKfzShelfAfterWait = time.Duration(cfg.Delays.KfzShelfOnOff.AfterWaitAPICheck) * time.Second + DelayKfzShelfBodyOverTimeout = time.Duration(cfg.Delays.KfzShelfOnOff.WaitBodyOverTimeout) * time.Second + DelayKfzDeleteAfterSend = time.Duration(cfg.Delays.KfzGoodsDelete.AfterSendRedisCheck) * time.Second + DelayKfzDeleteAfterAPI = time.Duration(cfg.Delays.KfzGoodsDelete.AfterSendAPICheck) * time.Second + + // 测试数据 + TestISBNSuccess = cfg.TestData.PddPricePublish.ISBNSuccess + TestPriceSuccess = cfg.TestData.PddPricePublish.PriceSuccess + TestISBNPriceZero = cfg.TestData.PddPricePublish.ISBNPriceZero + TestPriceZero = cfg.TestData.PddPricePublish.PriceZero + TestISBNBanned = cfg.TestData.PddPricePublish.ISBNBannedWord + TestPriceBanned = cfg.TestData.PddPricePublish.PriceBanned + TestNewPrice = cfg.TestData.PddPriceChange.NewPrice + TestNewStock = cfg.TestData.PddStockChange.NewStock + TestXyISBNSuccess = cfg.TestData.XyPricePublish.ISBNSuccess + TestXyPriceSuccess = cfg.TestData.XyPricePublish.PriceSuccess + TestXyNewPrice = cfg.TestData.XyPriceChange.NewPrice + TestXyNewStock = cfg.TestData.XyStockChange.NewStock + TestKfzISBNSuccess = cfg.TestData.KfzPricePublish.ISBNSuccess + TestKfzPriceSuccess = cfg.TestData.KfzPricePublish.PriceSuccess + TestKfzNewPrice = cfg.TestData.KfzPriceChange.NewPrice + TestKfzNewStock = cfg.TestData.KfzStockChange.NewStock + + // 拉取搜索 + SearchPageSize = cfg.TestData.PullGoods.SearchPageSize + BodyWaitMaxSearch = cfg.TestData.PullGoods.BodyWaitMaxSearch + + // 任务类型 + TaskTypePricePublish = cfg.TaskType.PricePublish + TaskTypePullGoods = cfg.TaskType.PullGoods + TaskTypePriceStockShelf = cfg.TaskType.PriceStockShelf + TaskTypeXyPriceStockShelf = cfg.TaskType.XyPriceStockShelf + + // 任务创建参数 + TaskCount = cfg.TaskCreate.TaskCount + ImgType = cfg.TaskCreate.ImgType + + // 商品状态映射 + StatusName = cfg.GoodsStatus + if StatusName == nil { + StatusName = map[int]string{1: "上架", 2: "下架", 3: "售罄", 4: "已删除"} + } + + // body_over 最少条数 + BodyOverMinPublish = cfg.BodyOverMin.PddPricePublish + BodyOverMinChange = cfg.BodyOverMin.PddPriceChange + BodyOverMinShelf = cfg.BodyOverMin.PddShelfOnOff + BodyOverMinDelete = cfg.BodyOverMin.PddGoodsDelete + BodyOverMinXyPriceChange = cfg.BodyOverMin.XyPriceChange + BodyOverMinXyShelfOnOff = cfg.BodyOverMin.XyShelfOnOff + BodyOverMinKfzPublish = cfg.BodyOverMin.KfzPricePublish + BodyOverMinKfzPriceChange = cfg.BodyOverMin.KfzPriceChange + BodyOverMinKfzStockChange = cfg.BodyOverMin.KfzStockChange + BodyOverMinKfzShelfOnOff = cfg.BodyOverMin.KfzShelfOnOff + BodyOverMinKfzDelete = cfg.BodyOverMin.KfzGoodsDelete + + DelayPddDeleteAfterSend = time.Duration(cfg.Delays.PddGoodsDelete.AfterSendRedisCheck) * time.Second + DelayPddDeleteAfterAPI = time.Duration(cfg.Delays.PddGoodsDelete.AfterSendAPICheck) * time.Second + + // 更新 httpClient 超时 + httpClient = &http.Client{Timeout: HTTPClientTimeout} + + return nil +} + +// countdownDelay 带倒计时的延迟 +func countdownDelay(d time.Duration, desc string) { + fmt.Printf("\n⏳ 延迟 %v 后%s,等待后台处理完成...\n", d, desc) + delayStart := time.Now() + for remaining := d; remaining > 0; remaining = d - time.Since(delayStart) { + fmt.Printf("\r 倒计时: %v ", remaining.Round(time.Second)) + time.Sleep(1 * time.Second) + } + fmt.Printf("\r ✅ 延迟结束,开始校验 \n") +} + +// ============================================================ +// 数据结构 +// ============================================================ + +// APIResponse 接口统一响应 +type APIResponse struct { + Code string `json:"code"` + Data interface{} `json:"data"` + Msg string `json:"msg"` +} + +// TestResult 单条测试结果 +type TestResult struct { + Category string + Name string + Status string // PASS / FAIL / ERROR + Detail string + Duration time.Duration +} + +// ============================================================ +// 全局变量 +// ============================================================ +var ( + results []TestResult + httpClient *http.Client + redisCtx = context.Background() + redisClient *redis.Client + reportDir string + + // 跨场景数据传递(拼多多) + priceTaskID string // 场景一 创建核价发布任务返回的 task_id + successISBN string // 场景一中 detail.error 包含"执行成功"对应的 isbn + successGoodsID int64 // 场景一中执行成功对应的 goods_id + mixedTaskID string // 场景二 创建改价格任务返回的 task_id(task_type=5,也用于场景三改库存、场景四上下架) + successSkuID int64 // 场景二步骤2查询商品详情时从响应sku_list中提取的 sku_id + pullTaskID string // 场景五 创建商品拉取任务返回的 task_id(task_type=3) + pullGoodsID int64 // 场景五 拉取任务中场景一 ISBN 对应的 goods_id + + // 闲鱼场景 + xyTaskID string // 场景七 闲鱼核价发布任务 task_id + xyModTaskID string // 场景十/十一/十二 闲鱼改库存/下架/上架 共用的任务 task_id + xySuccessISBN string // 场景七中执行成功的ISBN + xySuccessGoodsID int64 // 场景七中执行成功对应的goods_id + xyPullTaskID string // 场景八 闲鱼商品拉取任务 task_id + xyPullGoodsID int64 // 场景八 拉取任务中找到的goods_id +) + +// 孔夫子任务跟踪变量 +var ( + kfzTaskID string // 孔夫子任务 task_id + kfzSuccessISBN string // 核价发布成功ISBN + kfzSuccessGoodsID int64 // 核价发布成功goods_id +) + +// ============================================================ +// HTTP 工具 +// ============================================================ + +var AppKey string // 签名密钥 + +// SignParams 对请求参数进行MD5签名 +// SignParams 对请求参数进行MD5签名 +var SignSecretKey = "jRQdCh52Z55Kzh1hADaA2ZtdTKetj2PXk60Tz5Yc0iz9aD8Wafbk7CwAZ8cz69A9zb9caZ3k9dnR3Ys06J5nYFPrZ0xE9p6TY8DCD538ryiRjW81YTPmk41tCEnXizPh" + +func SignParams(params map[string]string) string { + // 过滤需要签名的参数(排除空值、sign、sign_type) + filteredParams := make(map[string]string) + for k, v := range params { + if v != "" && k != "sign" && k != "sign_type" { + filteredParams[k] = v + } + } + + // 提取键名并排序 + keys := make([]string, 0, len(filteredParams)) + for k := range filteredParams { + keys = append(keys, k) + } + sort.Strings(keys) + + // 拼接参数字符串: key=value&key=value... + var builder strings.Builder + for i, k := range keys { + if i > 0 { + builder.WriteString("&") + } + builder.WriteString(k) + builder.WriteString("=") + builder.WriteString(filteredParams[k]) + } + + // 末尾加上签名密钥 + signStr := builder.String() + "&key=" + SignSecretKey + + // MD5 哈希并转大写十六进制 + hash := md5.Sum([]byte(signStr)) + return strings.ToUpper(hex.EncodeToString(hash[:])) +} + +// postMultipart 发送 multipart/form-data POST 请求 +func postMultipart(url string, fields map[string]string) (*APIResponse, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + for k, v := range fields { + if err := writer.WriteField(k, v); err != nil { + return nil, fmt.Errorf("写入字段 %s 失败: %w", k, err) + } + } + writer.Close() + + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %w", err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("请求失败: %w", err) + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取响应失败: %w", err) + } + + var apiResp APIResponse + if err := json.Unmarshal(b, &apiResp); err != nil { + return nil, fmt.Errorf("JSON解析失败 [%s]: %w", truncate(string(b), 300), err) + } + return &apiResp, nil +} + +// ============================================================ +// Redis 工具 +// ============================================================ + +func initRedis() error { + redisClient = redis.NewClient(&redis.Options{ + Addr: RedisAddr, + Password: RedisPwd, + DB: RedisDB, + }) + _, err := redisClient.Ping(redisCtx).Result() + return err +} + +// getBodyOverFromRedis 从 Redis 读取 body_over list(指定范围) +func getBodyOverFromRedis(taskID string, start, stop int64) ([]map[string]interface{}, error) { + key := taskID + ":body_over" + vals, err := redisClient.LRange(redisCtx, key, start, stop).Result() + if err != nil { + return nil, fmt.Errorf("Redis LRange %s 失败: %w", key, err) + } + + var result []map[string]interface{} + for _, v := range vals { + var item map[string]interface{} + if err := json.Unmarshal([]byte(v), &item); err != nil { + continue + } + result = append(result, item) + } + return result, nil +} + +// getBodyOverCount 获取 body_over 数量 +func getBodyOverCount(taskID string) (int64, error) { + key := taskID + ":body_over" + return redisClient.LLen(redisCtx, key).Result() +} + +// getHeaderStatus 获取任务状态 +func getHeaderStatus(taskID string) (int64, error) { + key := taskID + ":header" + v, err := redisClient.HGet(redisCtx, key, "status").Result() + if err != nil { + return 0, err + } + return strconv.ParseInt(v, 10, 64) +} + +// ============================================================ +// 等待工具 +// ============================================================ + +// waitBodyOverMin 等待 body_over 至少有 minCount 条 +func waitBodyOverMin(taskID string, minCount int, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + round := 0 + for time.Now().Before(deadline) { + cnt, err := getBodyOverCount(taskID) + if err == nil && cnt >= int64(minCount) { + return nil + } + round++ + if round%5 == 0 { + fmt.Printf(" ⏳ 等待中... body_over=%d (需要≥%d)\n", cnt, minCount) + } + time.Sleep(PollInterval) + } + cnt, _ := getBodyOverCount(taskID) + return fmt.Errorf("等待超时 (%v),body_over 数量 %d < %d", timeout, cnt, minCount) +} + +// ============================================================ +// 通用解析工具 +// ============================================================ + +// extractGoodsStatus 从商品详情 map 中提取 status 和 goods_name +// 商品状态:1=上架,2=下架,3=售罄,4=已删除 +func extractGoodsStatus(m map[string]interface{}) (int, string) { + status := -1 + goodsName := "" + + if v, ok := m["status"]; ok { + switch val := v.(type) { + case float64: + status = int(val) + case string: + status, _ = strconv.Atoi(val) + } + } + + if v, ok := m["goods_name"]; ok { + if s, ok := v.(string); ok { + goodsName = s + } + } + + return status, goodsName +} + +// queryGoodsDetail 用 curl 调用商品详情接口,返回解析后的 map +// 支持重试,新商品在 PDD 上需要同步时间 +func queryGoodsDetail(accessToken string, goodsID int64, maxRetries int) (map[string]interface{}, error) { + goodsIDStr := fmt.Sprintf("%d", goodsID) + + for retry := 1; retry <= maxRetries; retry++ { + curlArgs := []string{ + "-s", + "--request", "GET", + VerifyURL, + "--header", "Authorization: Basic " + VerifyBasicAuth, + "--form", "accessToken=" + accessToken, + "--form", "goodsId=" + goodsIDStr, + } + + if retry == 1 { + fmt.Printf(" 📋 curl --request GET %s --form accessToken=*** --form goodsId=%s\n", VerifyURL, goodsIDStr) + } + + ctx, cancel := context.WithTimeout(context.Background(), CurlTimeout) + cmd := exec.CommandContext(ctx, "curl", curlArgs...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + cancel() + if retry == maxRetries { + return nil, fmt.Errorf("curl 执行失败: %v, stderr: %s", err, truncate(stderr.String(), 200)) + } + fmt.Printf(" ⚠️ curl 失败(第%d次),重试...\n", retry) + time.Sleep(CurlRetryInterval) + continue + } + cancel() + + rawStr := strings.TrimSpace(stdout.String()) + fmt.Printf(" 📋 curl 响应(第%d次): %s\n", retry, truncate(rawStr, 300)) + + // 检查是否返回有效数据(非 null/空) + if rawStr == "null" || rawStr == "" { + if retry < maxRetries { + fmt.Printf(" ⚠️ 响应为空(第%d次),重试中...\n", retry) + time.Sleep(CurlRetryInterval) + } + continue + } + + var rawMap map[string]interface{} + if err := json.Unmarshal([]byte(rawStr), &rawMap); err != nil { + if retry < maxRetries { + fmt.Printf(" ⚠️ JSON解析失败(第%d次),重试中...\n", retry) + time.Sleep(CurlRetryInterval) + } + continue + } + + // 检查是否有 goods_id(有效商品数据) + if rawMap["goods_id"] != nil { + fmt.Printf(" ✅ 获取到有效商品数据\n") + return rawMap, nil + } + + // 可能在 data 字段内 + if dataMap, ok := rawMap["data"].(map[string]interface{}); ok && dataMap["goods_id"] != nil { + fmt.Printf(" ✅ 获取到有效商品数据(data层)\n") + return rawMap, nil + } + + if retry < maxRetries { + fmt.Printf(" ⚠️ 响应无有效商品数据(第%d次),重试中...\n", retry) + time.Sleep(CurlRetryInterval) + } + } + + return nil, fmt.Errorf("重试 %d 次后仍未获取到有效商品数据", maxRetries) +} + +// extractSkuID 从商品详情 map 中提取第一个 sku_id +func extractSkuID(rawMap map[string]interface{}) int64 { + // 尝试从顶层 sku_list 提取 + if skuList, ok := rawMap["sku_list"].([]interface{}); ok && len(skuList) > 0 { + if sku0, ok := skuList[0].(map[string]interface{}); ok { + if v, ok := sku0["sku_id"].(float64); ok { + return int64(v) + } + } + } + // 尝试从 data 层提取 + if dataMap, ok := rawMap["data"].(map[string]interface{}); ok { + if skuList, ok := dataMap["sku_list"].([]interface{}); ok && len(skuList) > 0 { + if sku0, ok := skuList[0].(map[string]interface{}); ok { + if v, ok := sku0["sku_id"].(float64); ok { + return int64(v) + } + } + } + } + return 0 +} + +// extractSkuQuantity 从商品详情 map 中查找匹配 sku_id 的 SKU,提取 quantity(库存) +func extractSkuQuantity(rawMap map[string]interface{}, targetSkuID int64) int64 { + var skuList []interface{} + if sl, ok := rawMap["sku_list"].([]interface{}); ok { + skuList = sl + } else if dataMap, ok := rawMap["data"].(map[string]interface{}); ok { + if sl, ok := dataMap["sku_list"].([]interface{}); ok { + skuList = sl + } + } + + for _, item := range skuList { + sku, ok := item.(map[string]interface{}) + if !ok { + continue + } + if sid, ok := sku["sku_id"].(float64); ok && int64(sid) == targetSkuID { + if v, ok := sku["quantity"].(float64); ok { + return int64(v) + } + } + } + + // 没匹配到 sku_id,用第一个 + if len(skuList) > 0 { + if sku0, ok := skuList[0].(map[string]interface{}); ok { + if v, ok := sku0["quantity"].(float64); ok { + return int64(v) + } + } + } + return -1 +} + +// extractSkuPrice 从商品详情 map 中查找匹配 sku_id 的 SKU,提取 multi_price 和 price +func extractSkuPrice(rawMap map[string]interface{}, targetSkuID int64) (multiPrice int64, basePrice int64) { + multiPrice = -1 + basePrice = -1 + + // 查找 sku_list + var skuList []interface{} + if sl, ok := rawMap["sku_list"].([]interface{}); ok { + skuList = sl + } else if dataMap, ok := rawMap["data"].(map[string]interface{}); ok { + if sl, ok := dataMap["sku_list"].([]interface{}); ok { + skuList = sl + } + } + + for _, item := range skuList { + sku, ok := item.(map[string]interface{}) + if !ok { + continue + } + if sid, ok := sku["sku_id"].(float64); ok && int64(sid) == targetSkuID { + if v, ok := sku["multi_price"].(float64); ok { + multiPrice = int64(v) + } + if v, ok := sku["price"].(float64); ok { + basePrice = int64(v) + } + return + } + } + + // 没匹配到 sku_id,用第一个 + if len(skuList) > 0 { + if sku0, ok := skuList[0].(map[string]interface{}); ok { + if v, ok := sku0["multi_price"].(float64); ok { + multiPrice = int64(v) + } + if v, ok := sku0["price"].(float64); ok { + basePrice = int64(v) + } + } + } + return +} + +// getAccessToken 从 Redis 获取 accessToken +func getAccessToken(taskID string) (string, error) { + if taskID == "" { + return "", fmt.Errorf("task_id 为空") + } + headerKey := taskID + ":header" + shopMsgJSON, err := redisClient.HGet(redisCtx, headerKey, "shop_msg").Result() + if err != nil { + return "", fmt.Errorf("Redis HGet %s shop_msg 失败: %w", headerKey, err) + } + + var shopMsg struct { + Token string `json:"token"` + } + if err := json.Unmarshal([]byte(shopMsgJSON), &shopMsg); err != nil { + return "", fmt.Errorf("解析 shop_msg JSON 失败: %w, 原始: %s", err, truncate(shopMsgJSON, 200)) + } + if shopMsg.Token == "" { + return "", fmt.Errorf("shop_msg.token 为空") + } + return shopMsg.Token, nil +} + +// ============================================================ +// 闲鱼 DLL 查询辅助函数 +// ============================================================ + +// getXyAccessToken 从 Redis 获取闲鱼的 accessToken(token 字段) +func getXyAccessToken() (string, error) { + if xyTaskID == "" { + return "", fmt.Errorf("xyTaskID 为空,场景七未成功创建任务") + } + headerKey := xyTaskID + ":header" + shopMsgJSON, err := redisClient.HGet(redisCtx, headerKey, "shop_msg").Result() + if err != nil { + return "", fmt.Errorf("Redis HGet %s shop_msg 失败: %w", headerKey, err) + } + var shopMsg struct { + Token string `json:"token"` + } + if err := json.Unmarshal([]byte(shopMsgJSON), &shopMsg); err != nil { + return "", fmt.Errorf("解析 shop_msg JSON 失败: %w, 原始: %s", err, truncate(shopMsgJSON, 200)) + } + if shopMsg.Token == "" { + return "", fmt.Errorf("shop_msg.token 为空") + } + return shopMsg.Token, nil +} + +// findXyGoodsByISBN 用 DLL ExecuteSelectGoodsListPrice 按 ISBN 搜索商品,返回 product_id +func findXyGoodsByISBN(isbn string, accessToken string) (int64, error) { + xyDll, err := xianYu.InitXianYuDll(XyDllPath) + if err != nil { + return 0, fmt.Errorf("初始化闲鱼DLL失败: %v", err) + } + // online_time 传空数组,product_status=22 表示在售 + queryReq := map[string]interface{}{ + "appId": XyAppID, + "appSecret": XyAppSecret, + "token": accessToken, + "online_time": []interface{}{}, + "product_status": 22, + } + queryJSON, _ := json.Marshal(queryReq) + result, err := xyDll.XianYuGetGoodsList(string(queryJSON), XyDllPath) + if err != nil { + return 0, fmt.Errorf("XianYuGetGoodsList 失败: %v", err) + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultMap); err != nil { + return 0, fmt.Errorf("解析商品列表 JSON 失败: %v, 原始: %s", err, truncate(result, 200)) + } + code, _ := resultMap["code"].(float64) + if code != 200 && code != 0 { + msg, _ := resultMap["msg"].(string) + return 0, fmt.Errorf("查询商品列表失败: code=%.0f msg=%s", code, msg) + } + // 遍历列表匹配 ISBN + data, ok := resultMap["data"].(map[string]interface{}) + if !ok { + return 0, fmt.Errorf("data 字段格式异常,无法解析商品列表") + } + for _, itemRaw := range data { + item, ok := itemRaw.(map[string]interface{}) + if !ok { + continue + } + if itemISBN, ok := item["isbn"].(string); ok && itemISBN == isbn { + if pid, ok := item["product_id"].(float64); ok { + return int64(pid), nil + } + } + } + return 0, fmt.Errorf("ISBN=%s 在商品列表中未找到", isbn) +} + +// getXyGoodsPrice 用 DLL ExecuteGetGoodsDetail 查询商品价格(返回分) +func getXyGoodsPrice(productID int64, accessToken string) (int64, error) { + xyDll, err := xianYu.InitXianYuDll(XyDllPath) + if err != nil { + return 0, fmt.Errorf("初始化闲鱼DLL失败: %v", err) + } + queryReq := map[string]interface{}{ + "appId": XyAppID, + "appSecret": XyAppSecret, + "token": accessToken, + "product_id": productID, + } + queryJSON, _ := json.Marshal(queryReq) + result, err := xyDll.XianYuGetGoodsDetail(string(queryJSON), XyDllPath) + if err != nil { + return 0, fmt.Errorf("XianYuGetGoodsDetail 失败: %v", err) + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultMap); err != nil { + return 0, fmt.Errorf("解析商品详情 JSON 失败: %v", err) + } + code, _ := resultMap["code"].(float64) + if code != 200 && code != 0 { + msg, _ := resultMap["msg"].(string) + return 0, fmt.Errorf("查询商品详情失败: code=%.0f msg=%s", code, msg) + } + if price, ok := resultMap["price"].(float64); ok { + return int64(price), nil + } + if data, ok := resultMap["data"].(map[string]interface{}); ok { + if price, ok := data["price"].(float64); ok { + return int64(price), nil + } + } + return 0, fmt.Errorf("商品详情中未找到 price 字段") +} + +// getXyGoodsStock 用 DLL ExecuteGetGoodsDetail 查询商品库存 +func getXyGoodsStock(productID int64, accessToken string) (int64, error) { + xyDll, err := xianYu.InitXianYuDll(XyDllPath) + if err != nil { + return 0, fmt.Errorf("初始化闲鱼DLL失败: %v", err) + } + queryReq := map[string]interface{}{ + "appId": XyAppID, + "appSecret": XyAppSecret, + "token": accessToken, + "product_id": productID, + } + queryJSON, _ := json.Marshal(queryReq) + result, err := xyDll.XianYuGetGoodsDetail(string(queryJSON), XyDllPath) + if err != nil { + return 0, fmt.Errorf("XianYuGetGoodsDetail 失败: %v", err) + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultMap); err != nil { + return 0, fmt.Errorf("解析商品详情 JSON 失败: %v", err) + } + code, _ := resultMap["code"].(float64) + if code != 200 && code != 0 { + msg, _ := resultMap["msg"].(string) + return 0, fmt.Errorf("查询商品详情失败: code=%.0f msg=%s", code, msg) + } + if stock, ok := resultMap["stock"].(float64); ok { + return int64(stock), nil + } + if data, ok := resultMap["data"].(map[string]interface{}); ok { + if stock, ok := data["stock"].(float64); ok { + return int64(stock), nil + } + } + return 0, fmt.Errorf("商品详情中未找到 stock 字段") +} + +// getXyGoodsStatus 用 DLL ExecuteGetGoodsDetail 查询商品状态 +// 返回值:1=上架 2=下架 3=售罄 4=已删除,-1=未知 +func getXyGoodsStatus(productID int64, accessToken string) (int, error) { + xyDll, err := xianYu.InitXianYuDll(XyDllPath) + if err != nil { + return -1, fmt.Errorf("初始化闲鱼DLL失败: %v", err) + } + queryReq := map[string]interface{}{ + "appId": XyAppID, + "appSecret": XyAppSecret, + "token": accessToken, + "product_id": productID, + } + queryJSON, _ := json.Marshal(queryReq) + result, err := xyDll.XianYuGetGoodsDetail(string(queryJSON), XyDllPath) + if err != nil { + return -1, fmt.Errorf("XianYuGetGoodsDetail 失败: %v", err) + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultMap); err != nil { + return -1, fmt.Errorf("解析商品详情 JSON 失败: %v", err) + } + code, _ := resultMap["code"].(float64) + if code != 200 && code != 0 { + msg, _ := resultMap["msg"].(string) + return -1, fmt.Errorf("查询商品详情失败: code=%.0f msg=%s", code, msg) + } + if ps, ok := resultMap["product_status"].(float64); ok { + return int(ps), nil + } + if data, ok := resultMap["data"].(map[string]interface{}); ok { + if ps, ok := data["product_status"].(float64); ok { + return int(ps), nil + } + } + return -1, fmt.Errorf("商品详情中未找到 product_status 字段") +} + +// ============================================================ +// 孔夫子 DLL 查询辅助函数 +// ============================================================ + +// getKfzAccessToken 从 Redis 获取孔夫子的 accessToken(token 字段) +func getKfzAccessToken() (string, error) { + if kfzTaskID == "" { + return "", fmt.Errorf("kfzTaskID 为空,场景未成功创建任务") + } + headerKey := kfzTaskID + ":header" + shopMsgJSON, err := redisClient.HGet(redisCtx, headerKey, "shop_msg").Result() + if err != nil { + return "", fmt.Errorf("Redis HGet %s shop_msg 失败: %w", headerKey, err) + } + var shopMsg struct { + Token string `json:"token"` + } + if err := json.Unmarshal([]byte(shopMsgJSON), &shopMsg); err != nil { + return "", fmt.Errorf("解析 shop_msg JSON 失败: %w, 原始: %s", err, truncate(shopMsgJSON, 200)) + } + if shopMsg.Token == "" { + return "", fmt.Errorf("shop_msg.token 为空") + } + return shopMsg.Token, nil +} + +// findKfzGoodsByISBN 用 DLL GetGoodsList 按 ISBN 搜索商品,返回 itemId +func findKfzGoodsByISBN(isbn string, accessToken string) (int64, error) { + kfzDll, err := kfz.InitKfzDll(KfzDllPath) + if err != nil { + return 0, fmt.Errorf("初始化孔夫子DLL失败: %v", err) + } + // type="sale" 表示在售,pageNum=1 + queryReq := kfz.GetGoodsListReq{ + Type: "sale", + PageNum: 1, + PageSize: 50, + SortOrder: "addTime", + SortType: "DESC", + } + queryJSON, _ := json.Marshal(queryReq) + result, err := kfzDll.GetGoodsList(KfzAppID, KfzAppSecret, accessToken, string(queryJSON)) + if err != nil { + return 0, fmt.Errorf("KongfzShopItemList 失败: %v", err) + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultMap); err != nil { + return 0, fmt.Errorf("解析商品列表 JSON 失败: %v, 原始: %s", err, truncate(result, 200)) + } + // 检查 errorResponse + if errResp, ok := resultMap["errorResponse"].(map[string]interface{}); ok { + if code, _ := errResp["code"].(float64); code != 0 { + msg, _ := errResp["msg"].(string) + return 0, fmt.Errorf("查询商品列表失败: code=%.0f msg=%s", code, msg) + } + } + // 遍历列表匹配 ISBN + if successResp, ok := resultMap["successResponse"].(map[string]interface{}); ok { + if list, ok := successResp["list"].([]interface{}); ok { + for _, itemRaw := range list { + item, ok := itemRaw.(map[string]interface{}) + if !ok { + continue + } + if itemISBN, ok := item["isbn"].(string); ok && itemISBN == isbn { + if itemId, ok := item["itemId"].(float64); ok { + return int64(itemId), nil + } + } + } + } + } + return 0, fmt.Errorf("ISBN=%s 在商品列表中未找到", isbn) +} + +// getKfzGoodsPrice 从商品列表中获取指定 itemId 的商品价格(返回元) +func getKfzGoodsPrice(itemId int64, accessToken string) (float64, error) { + kfzDll, err := kfz.InitKfzDll(KfzDllPath) + if err != nil { + return 0, fmt.Errorf("初始化孔夫子DLL失败: %v", err) + } + queryReq := kfz.GetGoodsListReq{ + Type: "sale", + PageNum: 1, + PageSize: 50, + } + queryJSON, _ := json.Marshal(queryReq) + result, err := kfzDll.GetGoodsList(KfzAppID, KfzAppSecret, accessToken, string(queryJSON)) + if err != nil { + return 0, fmt.Errorf("KongfzShopItemList 失败: %v", err) + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultMap); err != nil { + return 0, fmt.Errorf("解析商品列表 JSON 失败: %v", err) + } + if successResp, ok := resultMap["successResponse"].(map[string]interface{}); ok { + if list, ok := successResp["list"].([]interface{}); ok { + for _, itemRaw := range list { + if item, ok := itemRaw.(map[string]interface{}); ok { + if id, ok := item["itemId"].(float64); ok && id == float64(itemId) { + if price, ok := item["price"].(float64); ok { + return price, nil + } + } + } + } + } + } + return 0, fmt.Errorf("itemId=%d 未找到对应商品或价格字段", itemId) +} + +// getKfzGoodsStock 从商品列表中获取指定 itemId 的商品库存 +func getKfzGoodsStock(itemId int64, accessToken string) (int, error) { + kfzDll, err := kfz.InitKfzDll(KfzDllPath) + if err != nil { + return 0, fmt.Errorf("初始化孔夫子DLL失败: %v", err) + } + queryReq := kfz.GetGoodsListReq{ + Type: "sale", + PageNum: 1, + PageSize: 50, + } + queryJSON, _ := json.Marshal(queryReq) + result, err := kfzDll.GetGoodsList(KfzAppID, KfzAppSecret, accessToken, string(queryJSON)) + if err != nil { + return 0, fmt.Errorf("KongfzShopItemList 失败: %v", err) + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultMap); err != nil { + return 0, fmt.Errorf("解析商品列表 JSON 失败: %v", err) + } + if successResp, ok := resultMap["successResponse"].(map[string]interface{}); ok { + if list, ok := successResp["list"].([]interface{}); ok { + for _, itemRaw := range list { + if item, ok := itemRaw.(map[string]interface{}); ok { + if id, ok := item["itemId"].(float64); ok && id == float64(itemId) { + if stock, ok := item["number"].(float64); ok { + return int(stock), nil + } + } + } + } + } + } + return 0, fmt.Errorf("itemId=%d 未找到对应商品或库存字段", itemId) +} + +// getKfzGoodsStatus 从商品列表中获取指定 itemId 的商品上下架状态 +// isOnSale: 1=上架 0=下架 +func getKfzGoodsStatus(itemId int64, accessToken string) (int, error) { + kfzDll, err := kfz.InitKfzDll(KfzDllPath) + if err != nil { + return -1, fmt.Errorf("初始化孔夫子DLL失败: %v", err) + } + queryReq := kfz.GetGoodsListReq{ + Type: "sale", + PageNum: 1, + PageSize: 50, + } + queryJSON, _ := json.Marshal(queryReq) + result, err := kfzDll.GetGoodsList(KfzAppID, KfzAppSecret, accessToken, string(queryJSON)) + if err != nil { + return -1, fmt.Errorf("KongfzShopItemList 失败: %v", err) + } + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultMap); err != nil { + return -1, fmt.Errorf("解析商品列表 JSON 失败: %v", err) + } + if successResp, ok := resultMap["successResponse"].(map[string]interface{}); ok { + if list, ok := successResp["list"].([]interface{}); ok { + for _, itemRaw := range list { + if item, ok := itemRaw.(map[string]interface{}); ok { + if id, ok := item["itemId"].(float64); ok && id == float64(itemId) { + if isOnSale, ok := item["isOnSale"].(float64); ok { + return int(isOnSale), nil + } + } + } + } + } + } + return -1, fmt.Errorf("itemId=%d 未找到对应商品", itemId) +} + +// ============================================================ +// 测试结果工具 +// ============================================================ + +func pass(cat, name, detail string, elapsed time.Duration) { + results = append(results, TestResult{Category: cat, Name: name, Status: "PASS", Detail: detail, Duration: elapsed}) + fmt.Printf(" ✅ PASS (%s)\n 详情: %s\n", elapsed.Round(time.Millisecond), detail) +} + +func fail(cat, name, detail string, elapsed time.Duration) { + results = append(results, TestResult{Category: cat, Name: name, Status: "FAIL", Detail: detail, Duration: elapsed}) + fmt.Printf(" ❌ FAIL (%s)\n 详情: %s\n", elapsed.Round(time.Millisecond), detail) +} + +func errCase(cat, name, detail string, elapsed time.Duration) { + results = append(results, TestResult{Category: cat, Name: name, Status: "ERROR", Detail: detail, Duration: elapsed}) + fmt.Printf(" 🔴 ERROR (%s)\n 详情: %s\n", elapsed.Round(time.Millisecond), detail) +} + +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} + +// ============================================================ +// 报告生成 +// ============================================================ + +func sanitize(s string) string { + s = strings.ReplaceAll(s, "|", "\\|") + s = strings.ReplaceAll(s, "\n", " ") + return strings.TrimSpace(s) +} + +func generateReport() string { + var sb strings.Builder + + sb.WriteString("# 📋 PlanA API 批量测试报告\n\n") + sb.WriteString(fmt.Sprintf("**测试时间**: %s \n", time.Now().Format("2006-01-02 15:04:05"))) + sb.WriteString(fmt.Sprintf("**接口地址**: `%s` \n", BaseURL)) + sb.WriteString(fmt.Sprintf("**ShopID**: `%s` \n", ShopID)) + sb.WriteString(fmt.Sprintf("**拼多多应用ID**: `%s` \n", PddAppID)) + sb.WriteString(fmt.Sprintf("**Redis**: `%s` DB=%d \n\n", RedisAddr, RedisDB)) + + total := len(results) + p, f, e := 0, 0, 0 + for _, r := range results { + switch r.Status { + case "PASS": + p++ + case "FAIL": + f++ + default: + e++ + } + } + rate := 0.0 + if total > 0 { + rate = float64(p) / float64(total) * 100 + } + + sb.WriteString("## 📊 测试汇总\n\n") + sb.WriteString("| 指标 | 值 |\n|---|---|\n") + sb.WriteString(fmt.Sprintf("| 通过率 | %d/%d (%.1f%%) |\n", p, total, rate)) + sb.WriteString(fmt.Sprintf("| ✅ PASS | %d |\n", p)) + sb.WriteString(fmt.Sprintf("| ❌ FAIL | %d |\n", f)) + sb.WriteString(fmt.Sprintf("| 🔴 ERROR | %d |\n\n", e)) + + if priceTaskID != "" || mixedTaskID != "" || successISBN != "" { + sb.WriteString("## 📌 跨场景数据记录\n\n") + sb.WriteString("| 数据项 | 值 |\n|---|---|\n") + if priceTaskID != "" { + sb.WriteString(fmt.Sprintf("| 核价发布 task_id | `%s` |\n", priceTaskID)) + } + if mixedTaskID != "" { + sb.WriteString(fmt.Sprintf("| 改价格/上下架 task_id | `%s` |\n", mixedTaskID)) + } + if successISBN != "" { + sb.WriteString(fmt.Sprintf("| 执行成功 ISBN | `%s` |\n", successISBN)) + } + if successGoodsID != 0 { + sb.WriteString(fmt.Sprintf("| 执行成功 GoodsID | `%d` |\n", successGoodsID)) + } + if successSkuID != 0 { + sb.WriteString(fmt.Sprintf("| 执行成功 SkuID | `%d` |\n", successSkuID)) + } + if pullTaskID != "" { + sb.WriteString(fmt.Sprintf("| 商品拉取 task_id | `%s` |\n", pullTaskID)) + } + if pullGoodsID != 0 { + sb.WriteString(fmt.Sprintf("| 拉取到的 GoodsID | `%d` |\n", pullGoodsID)) + } + sb.WriteString("\n") + } + + curCat := "" + idx := 0 + for _, r := range results { + if r.Category != curCat { + curCat = r.Category + if idx > 0 { + sb.WriteString("\n") + } + sb.WriteString(fmt.Sprintf("## %s\n\n", curCat)) + sb.WriteString("| # | 用例 | 状态 | 耗时 | 详情 |\n") + sb.WriteString("|---|---|---|---|---|\n") + idx = 0 + } + idx++ + emoji := map[string]string{"PASS": "✅", "FAIL": "❌", "ERROR": "🔴"}[r.Status] + detail := sanitize(r.Detail) + if len(detail) > 200 { + detail = detail[:200] + "..." + } + sb.WriteString(fmt.Sprintf("| %d | %s | %s %s | %s | %s |\n", + idx, r.Name, emoji, r.Status, r.Duration.Round(time.Millisecond), detail)) + } + + sb.WriteString("\n---\n") + sb.WriteString(fmt.Sprintf("*报告生成: %s*", time.Now().Format("2006-01-02 15:04:05"))) + return sb.String() +} + +// ============================================================ +// 测试七:闲鱼核价发布任务 +// ============================================================ + +func testXyPricePublish() { + cat := "七、闲鱼核价发布任务" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + var tid string + + // ---------- 步骤1:创建闲鱼核价发布任务 ---------- + { + name := "1、创建闲鱼核价发布任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": XyShopID, + "shop_type": XyShopType, + "task_count": TaskCount, + "task_type": TaskTypePricePublish, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + if dataStr == "" { + fail(cat, name, "返回 data 为空", elapsed) + return + } + tid = dataStr + xyTaskID = tid + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + if tid == "" { + fmt.Println("⚠️ 任务创建失败,跳过后续步骤") + return + } + fmt.Printf("\n 📌 闲鱼核价发布 task_id: %s\n", tid) + + // ---------- 步骤2:发送 isbn(期望:执行成功)---------- + { + name := fmt.Sprintf("2、发送任务数据【isbn=%s, price=%d】期望:执行成功", TestXyISBNSuccess, TestXyPriceSuccess) + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"price":%d}}`, TestXyISBNSuccess, TestXyPriceSuccess) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, "接口返回成功", elapsed) + } + + // 延迟后校验 + countdownDelay(DelayXyPublishAfterSend, "校验") + + // ---------- 步骤3:Redis 校验 body_over(仅校验执行成功)---------- + { + name := "3、Redis 校验 - body_over 中 detail.error 包含'执行成功'" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + fmt.Printf(" ⏳ 等待后台处理完成(最多 %v)...\n", WaitTimeout) + + if err := waitBodyOverMin(tid, 1, WaitTimeout); err != nil { + errCase(cat, name, "等待超时: "+err.Error(), time.Since(start)) + return + } + + totalCnt, _ := getBodyOverCount(tid) + fmt.Printf(" 📊 body_over 总数: %d,开始校验...\n", totalCnt) + + targetISBN := TestXyISBNSuccess + var detailLines []string + found := false + var successGoodsID int64 + + const pageSize int64 = 100 + for offset := int64(0); offset < totalCnt && !found; offset += pageSize { + items, err := getBodyOverFromRedis(tid, offset, offset+pageSize-1) + if err != nil || len(items) == 0 { + break + } + + for _, item := range items { + isbn := "" + if bi, ok := item["book_info"].(map[string]interface{}); ok { + if v, ok := bi["isbn"].(string); ok { + isbn = v + } + } + if isbn != targetISBN { + continue + } + + var errMsg string + var goodsID float64 + var detailStatus float64 + if detail, ok := item["detail"].(map[string]interface{}); ok { + if v, ok := detail["error"].(string); ok { + errMsg = v + } + if v, ok := detail["goods_id"].(float64); ok { + goodsID = v + } + if v, ok := detail["status"].(float64); ok { + detailStatus = v + } + } + + found = true + errorMatched := strings.Contains(errMsg, "执行成功") || detailStatus == 1 + hasGoodsID := goodsID > 0 + + if errorMatched && hasGoodsID { + successGoodsID = int64(goodsID) + detailLines = append(detailLines, fmt.Sprintf("ISBN=%s ✅ 匹配'执行成功' (goods_id=%.0f)", isbn, goodsID)) + } else { + failReason := "" + if !hasGoodsID { + failReason = " [原因: goods_id为空]" + } else if !errorMatched { + failReason = " [原因: error不含'执行成功'且status!=1]" + } + detailLines = append(detailLines, + fmt.Sprintf("ISBN=%s ❌ 期望'执行成功'%s 实际error='%s', status=%.0f, goods_id=%.0f", + isbn, failReason, truncate(errMsg, 80), detailStatus, goodsID)) + } + break + } + } + + if !found { + detailLines = append(detailLines, fmt.Sprintf("❌ 未在body_over中找到ISBN=%s", targetISBN)) + } + + detail := strings.Join(detailLines, " | ") + elapsed := time.Since(start) + + if found && successGoodsID > 0 { + pass(cat, name, detail, elapsed) + } else { + fail(cat, name, detail, elapsed) + } + + // 保存成功的goods_id供后续测试使用 + if successGoodsID > 0 { + xySuccessGoodsID = successGoodsID + xySuccessISBN = targetISBN + fmt.Printf("\n 📌 记录执行成功数据: ISBN=%s, goods_id=%d\n", targetISBN, successGoodsID) + } + } + + // ---------- 步骤5:DLL校验 - 通过闲鱼API查询商品详情 ---------- + if xySuccessGoodsID > 0 { + name := "5、DLL校验 - 通过闲鱼API查询商品详情" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 初始化DLL + xyDll, err := xianYu.InitXianYuDll(XyDllPath) + if err != nil { + fail(cat, name, fmt.Sprintf("初始化闲鱼DLL失败: %v", err), time.Since(start)) + } else { + // 构建查询请求 - 使用goods_id作为product_id查询 + // 注意:这里假设goods_id就是product_id,如果不一致需要调整 + queryReq := map[string]interface{}{ + "appId": XyAppID, + "appSecret": XyAppSecret, + "product_id": xySuccessGoodsID, + } + queryJSON, _ := json.Marshal(queryReq) + + fmt.Printf(" 📋 查询商品详情: product_id=%d\n", xySuccessGoodsID) + result, err := xyDll.XianYuGetGoodsDetail(string(queryJSON), XyDllPath) + elapsed := time.Since(start) + + if err != nil { + fail(cat, name, fmt.Sprintf("DLL调用失败: %v", err), elapsed) + } else { + // 解析返回结果 + var resultMap map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultMap); err != nil { + // 如果解析失败,直接显示原始结果 + pass(cat, name, fmt.Sprintf("DLL返回: %s", truncate(result, 200)), elapsed) + } else { + // 检查返回结果中是否包含商品信息 + code, _ := resultMap["code"].(float64) + if code == 200 || code == 0 { + pass(cat, name, fmt.Sprintf("商品详情查询成功 ✅ (product_id=%d)", xySuccessGoodsID), elapsed) + } else { + msg, _ := resultMap["msg"].(string) + fail(cat, name, fmt.Sprintf("商品详情查询失败: %s", msg), elapsed) + } + } + } + } + } else { + fmt.Printf("\n ⚠️ 未找到执行成功的商品,跳过DLL校验\n") + } +} + +// ============================================================ +// 测试一:拼多多核价发布任务 +// ============================================================ + +func testPddPricePublish() { + cat := "一、拼多多核价发布任务" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + var tid string + + // ---------- 步骤1:创建核价发布任务 ---------- + { + name := "1、创建拼多多核价发布任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": ShopID, + "shop_type": ShopType, + "task_count": TaskCount, + "task_type": TaskTypePricePublish, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + if dataStr == "" { + fail(cat, name, "返回 data 为空", elapsed) + return + } + tid = dataStr + priceTaskID = tid + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + if tid == "" { + fmt.Println("⚠️ 任务创建失败,跳过后续步骤") + return + } + fmt.Printf("\n 📌 核价发布 task_id: %s\n", tid) + + // ---------- 步骤2:发送 isbn(期望:执行成功)---------- + { + name := fmt.Sprintf("2、发送任务数据【isbn=%s, price=%d】期望:执行成功", TestISBNSuccess, TestPriceSuccess) + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"price":%d}}`, TestISBNSuccess, TestPriceSuccess) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, "接口返回成功", elapsed) + } + + // ---------- 步骤3a:发送 isbn(期望:价格不能小于等于0)---------- + { + name := fmt.Sprintf("3a、发送任务数据【isbn=%s, price=%d】期望:价格不能小于等于0", TestISBNPriceZero, TestPriceZero) + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"price":%d}}`, TestISBNPriceZero, TestPriceZero) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, "接口返回成功", elapsed) + } + + // ---------- 步骤3b:发送 isbn(期望:违规词命中)---------- + { + name := fmt.Sprintf("3b、发送任务数据【isbn=%s, price=%d】期望:违规词命中", TestISBNBanned, TestPriceBanned) + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"price":%d}}`, TestISBNBanned, TestPriceBanned) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, "接口返回成功", elapsed) + } + + // 延迟后校验 + countdownDelay(DelayPddPublishAfterSend, "校验") + + // ---------- 步骤4:Redis 校验 body_over ---------- + { + name := "4、Redis 校验 - body_over 中 detail.error 匹配期望结果" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + fmt.Printf(" ⏳ 等待后台处理完成(最多 %v)...\n", WaitTimeout) + + if err := waitBodyOverMin(tid, BodyOverMinPublish, WaitTimeout); err != nil { + errCase(cat, name, "等待超时: "+err.Error(), time.Since(start)) + return + } + + totalCnt, _ := getBodyOverCount(tid) + fmt.Printf(" 📊 body_over 总数: %d,开始校验...\n", totalCnt) + + expected := map[string]string{ + TestISBNSuccess: "执行成功", + TestISBNPriceZero: "价格不能小于等于0", + TestISBNBanned: "违规词命中", + } + + var detailLines []string + allPass := true + matchedCount := 0 + + const pageSize int64 = 100 + for offset := int64(0); offset < totalCnt; offset += pageSize { + items, err := getBodyOverFromRedis(tid, offset, offset+pageSize-1) + if err != nil || len(items) == 0 { + break + } + + for _, item := range items { + isbn := "" + if bi, ok := item["book_info"].(map[string]interface{}); ok { + if v, ok := bi["isbn"].(string); ok { + isbn = v + } + } + if isbn == "" { + continue + } + + want, hasExpect := expected[isbn] + if !hasExpect { + continue + } + + matchedCount++ + + var errMsg string + var goodsID float64 + var detailStatus float64 + if detail, ok := item["detail"].(map[string]interface{}); ok { + if v, ok := detail["error"].(string); ok { + errMsg = v + } + if v, ok := detail["goods_id"].(float64); ok { + goodsID = v + } + if v, ok := detail["status"].(float64); ok { + detailStatus = v + } + } + + matched := false + if want == "执行成功" { + errorMatched := strings.Contains(errMsg, "执行成功") || detailStatus == 1 + hasGoodsID := goodsID > 0 + matched = errorMatched && hasGoodsID + } else { + matched = strings.Contains(errMsg, want) + } + + if matched { + detailLines = append(detailLines, fmt.Sprintf("ISBN=%s ✅ 匹配'%s' (goods_id=%.0f)", isbn, want, goodsID)) + if want == "执行成功" { + successISBN = isbn + successGoodsID = int64(goodsID) + } + } else { + failReason := "" + if want == "执行成功" { + if goodsID <= 0 { + failReason = " [原因: goods_id为空,执行成功但无商品ID]" + } else { + failReason = " [原因: error不含'执行成功'且status!=1]" + } + } + detailLines = append(detailLines, + fmt.Sprintf("ISBN=%s ❌ 期望'%s'%s 实际error='%s', status=%.0f, goods_id=%.0f", + isbn, want, failReason, truncate(errMsg, 80), detailStatus, goodsID)) + allPass = false + } + } + } + + if matchedCount < len(expected) { + detailLines = append(detailLines, + fmt.Sprintf("⚠️ 仅匹配到 %d/%d 个期望ISBN", matchedCount, len(expected))) + allPass = false + } + + detail := strings.Join(detailLines, " | ") + elapsed := time.Since(start) + + if allPass { + pass(cat, name, detail, elapsed) + } else { + fail(cat, name, detail, elapsed) + } + } +} + +// 测试一结束 + +// ============================================================ +// 测试二:拼多多改价格 +// ============================================================ + +func testPddPriceChange() { + cat := "二、拼多多改价格" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + // 前置条件检查 + if successISBN == "" || successGoodsID == 0 { + fmt.Println("⚠️ 场景一未匹配到执行成功数据(isbn/goods_id),跳过改价格测试") + fail(cat, "前置条件", "场景一未匹配到执行成功数据(isbn/goods_id)", 0) + return + } + + // ---------- 步骤1:创建改价格任务(task_type=5)---------- + var tid string + { + name := "1、创建拼多多改价格、上下架、改库存任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": ShopID, + "shop_type": ShopType, + "task_count": TaskCount, + "task_type": TaskTypePriceStockShelf, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + if dataStr == "" { + fail(cat, name, "返回 data 为空", elapsed) + return + } + tid = dataStr + mixedTaskID = tid + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + if tid == "" { + fmt.Println("⚠️ 改价格任务创建失败,跳过后续步骤") + return + } + fmt.Printf("\n 📌 改价格/上下架 task_id: %s\n", tid) + + // ---------- 步骤2:查询商品详情,获取 sku_id ---------- + { + name := "2、查询商品详情,获取 sku_id" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 延迟等待新商品在 PDD 上同步 + countdownDelay(DelayPddChangeAfterQuery, "查询商品详情") + + // 获取 accessToken + accessToken, err := getAccessToken(priceTaskID) + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + fmt.Printf(" 🔑 accessToken: %s...%s\n", accessToken[:8], accessToken[len(accessToken)-8:]) + fmt.Printf(" 📦 goodsId: %d\n", successGoodsID) + + // 查询商品详情(最多重试5次,新商品需要同步时间) + rawMap, err := queryGoodsDetail(accessToken, successGoodsID, 5) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, fmt.Sprintf("查询商品详情失败: %v", err), elapsed) + return + } + + // 提取 sku_id + successSkuID = extractSkuID(rawMap) + if successSkuID == 0 { + fail(cat, name, "响应中未找到 sku_id", elapsed) + return + } + + // 提取商品状态 + goodsStatus, goodsName := extractGoodsStatus(rawMap) + statusStr := fmt.Sprintf("%d", goodsStatus) + if n, ok := StatusName[goodsStatus]; ok { + statusStr = n + } + + // 提取当前价格(用于后续对比) + multiPrice, basePrice := extractSkuPrice(rawMap, successSkuID) + + fmt.Printf(" 📦 sku_id: %d\n", successSkuID) + fmt.Printf(" 📦 商品状态: %s(%s)\n", statusStr, StatusName[goodsStatus]) + fmt.Printf(" 📦 当前价格: multi_price=%d, price=%d\n", multiPrice, basePrice) + + if goodsStatus != 1 { + fail(cat, name, fmt.Sprintf("商品非上架状态 status=%d(%s),改价格需要商品在上架状态。goodsName=%s", + goodsStatus, statusStr, truncate(goodsName, 50)), elapsed) + return + } + + pass(cat, name, fmt.Sprintf("sku_id=%d 商品上架中 status=1(上架) multi_price=%d price=%d goodsName=%s", + successSkuID, multiPrice, basePrice, truncate(goodsName, 50)), elapsed) + } + + if successSkuID == 0 { + fmt.Println("⚠️ 未获取到 sku_id,跳过后续改价格步骤") + return + } + + // 测试改价格:从配置读取 + testPrice := TestNewPrice + + fmt.Printf("\n 📌 isbn: %s, goods_id: %d, sku_id: %d\n", successISBN, successGoodsID, successSkuID) + fmt.Printf(" 📌 改价格为: %d(分)= %.2f元\n", testPrice, float64(testPrice)/100) + + // ---------- 步骤3:发送改价格任务 ---------- + { + name := "3、发送任务数据【改价格】" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + bodyJSON := fmt.Sprintf( + `{"book_info":{"isbn":"%s"},"detail":{"goods_id":%d,"status":5,"price":%d,"sku_id":%d}}`, + successISBN, successGoodsID, testPrice, successSkuID) + fmt.Printf(" 📋 task_id: %s\n", tid) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + + // 打印原始响应 + rawJSON, _ := json.Marshal(resp) + fmt.Printf(" 📥 原始响应: %s\n", string(rawJSON)) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("ISBN=%s GoodsID=%d sku_id=%d price=%d(%.2f元) 接口返回成功", + successISBN, successGoodsID, successSkuID, testPrice, float64(testPrice)/100), elapsed) + } + + // 延迟后 Redis 校验 + countdownDelay(DelayPddChangeAfterSend, "校验") + + // ---------- 步骤4:Redis 校验改价格结果 ---------- + { + name := "4、Redis 校验改价格结果(body_over)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 等待改价格任务处理完成 + fmt.Printf(" ⏳ 等待改价格任务处理完成(检查 body_over)...\n") + deadline := time.Now().Add(WaitTimeout) + processed := false + for time.Now().Before(deadline) { + cnt, _ := getBodyOverCount(tid) + if cnt > 0 { + fmt.Printf(" ✅ 改价格任务已处理完成(body_over 条目数: %d)\n", cnt) + processed = true + break + } + time.Sleep(PollInterval) + } + if !processed { + fail(cat, name, "等待超时,body_over 仍无数据", time.Since(start)) + return + } + + // 读取 body_over 最新的条目(最后一条) + totalCnt, _ := getBodyOverCount(tid) + items, err := getBodyOverFromRedis(tid, totalCnt-1, totalCnt-1) + elapsed := time.Since(start) + + if err != nil || len(items) == 0 { + errCase(cat, name, fmt.Sprintf("读取 body_over 失败: %v", err), elapsed) + return + } + + item := items[0] + detail, _ := item["detail"].(map[string]interface{}) + errMsg, _ := detail["error"].(string) + priceField, _ := detail["price"].(float64) + skuIDField, _ := detail["sku_id"].(float64) + + fmt.Printf(" 📋 error: %s\n", truncate(errMsg, 200)) + fmt.Printf(" 📋 price: %.0f, sku_id: %.0f\n", priceField, skuIDField) + + if strings.Contains(errMsg, "执行成功") || strings.Contains(errMsg, "成功") { + pass(cat, name, fmt.Sprintf("改价格执行成功: error='%s', price=%.0f, sku_id=%.0f", + truncate(errMsg, 100), priceField, skuIDField), elapsed) + } else { + fail(cat, name, fmt.Sprintf("改价格失败: error='%s'", truncate(errMsg, 200)), elapsed) + } + } + + // ---------- 步骤5:校验改价格状态(接口验证)---------- + // 调用商品详情接口,对比 sku_list 中的 multi_price 是否与传入的 price 一致 + { + name := "5、校验改价格状态(接口验证)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 延迟后接口校验 + countdownDelay(DelayPddChangeAfterAPI, "接口校验") + + // 获取 accessToken + accessToken, err := getAccessToken(priceTaskID) + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + + // 查询商品详情 + rawMap, err := queryGoodsDetail(accessToken, successGoodsID, 3) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, fmt.Sprintf("查询商品详情失败: %v", err), elapsed) + return + } + + // 提取价格 + multiPrice, basePrice := extractSkuPrice(rawMap, successSkuID) + + // 提取商品状态 + goodsStatus, _ := extractGoodsStatus(rawMap) + statusStr := fmt.Sprintf("%d", goodsStatus) + if n, ok := StatusName[goodsStatus]; ok { + statusStr = n + } + + fmt.Printf(" 📦 sku_id=%d: multi_price=%d, price=%d, 期望price=%d\n", + successSkuID, multiPrice, basePrice, testPrice) + fmt.Printf(" 📦 商品状态: %s(%s)\n", statusStr, StatusName[goodsStatus]) + + // 价格对比逻辑 + if multiPrice == testPrice { + pass(cat, name, fmt.Sprintf("multi_price=%d 与传入 price=%d 一致 ✅", + multiPrice, testPrice), elapsed) + } else if basePrice == testPrice { + discountStr := "" + if v, ok := rawMap["two_pieces_discount"].(float64); ok { + discountStr = fmt.Sprintf("two_pieces_discount=%d%%", int(v)) + } + pass(cat, name, fmt.Sprintf("price=%d 与传入一致,multi_price=%d(%s 折扣导致差异)", + basePrice, multiPrice, discountStr), elapsed) + } else { + fail(cat, name, fmt.Sprintf("价格不匹配:传入price=%d,实际multi_price=%d, price=%d(商品状态=%s)", + testPrice, multiPrice, basePrice, statusStr), elapsed) + } + } +} + +// ============================================================ +// 测试三:拼多多上下架 +// ============================================================ + +// ============================================================ +// 测试三:拼多多改库存 +// ============================================================ + +func testPddStockChange() { + cat := "三、拼多多改库存" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + // 前置条件检查 + if mixedTaskID == "" { + fmt.Println("⚠️ 改价格 task_id 为空,跳过改库存测试") + fail(cat, "前置条件", "场景二未创建改价格任务,无法获取 task_id", 0) + return + } + if successISBN == "" || successGoodsID == 0 || successSkuID == 0 { + fmt.Println("⚠️ 场景一/二未获取到必要数据(isbn/goods_id/sku_id),跳过改库存测试") + fail(cat, "前置条件", "场景一/二未获取到必要数据(isbn/goods_id/sku_id)", 0) + return + } + + fmt.Printf("\n 📌 使用改价格 task_id: %s(复用 task_type=5 任务)\n", mixedTaskID) + + // 测试改库存:从配置读取 + testStock := TestNewStock + + fmt.Printf(" 📌 isbn: %s, goods_id: %d, sku_id: %d\n", successISBN, successGoodsID, successSkuID) + fmt.Printf(" 📌 改库存为: %d\n", testStock) + + // ---------- 步骤1:发送改库存任务 ---------- + { + name := "1、发送任务数据【改库存】" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + bodyJSON := fmt.Sprintf( + `{"book_info":{"isbn":"%s"},"detail":{"goods_id":%d,"status":4,"stock":%d,"sku_id":%d}}`, + successISBN, successGoodsID, testStock, successSkuID) + fmt.Printf(" 📋 task_id: %s\n", mixedTaskID) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + + params := map[string]string{ + "task_id": mixedTaskID, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("ISBN=%s GoodsID=%d sku_id=%d stock=%d status=4(改库存) 接口返回成功", + successISBN, successGoodsID, successSkuID, testStock), elapsed) + } + + // 延迟后 Redis 校验 + countdownDelay(DelayPddStockAfterSend, "校验") + + // ---------- 步骤2:Redis 校验改库存结果 ---------- + { + name := "2、Redis 校验改库存结果(body_over)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 等待改库存任务处理完成 + fmt.Printf(" ⏳ 等待改库存任务处理完成(检查 body_over)...\n") + deadline := time.Now().Add(WaitTimeout) + processed := false + for time.Now().Before(deadline) { + cnt, _ := getBodyOverCount(mixedTaskID) + if cnt > 0 { + fmt.Printf(" ✅ 改库存任务已处理完成(body_over 条目数: %d)\n", cnt) + processed = true + break + } + time.Sleep(PollInterval) + } + if !processed { + fail(cat, name, "等待超时,body_over 仍无数据", time.Since(start)) + return + } + + // 读取 body_over 最新的条目(最后一条) + totalCnt, _ := getBodyOverCount(mixedTaskID) + items, err := getBodyOverFromRedis(mixedTaskID, totalCnt-1, totalCnt-1) + elapsed := time.Since(start) + + if err != nil || len(items) == 0 { + errCase(cat, name, fmt.Sprintf("读取 body_over 失败: %v", err), elapsed) + return + } + + item := items[0] + detail, _ := item["detail"].(map[string]interface{}) + errMsg, _ := detail["error"].(string) + stockField, _ := detail["stock"].(float64) + skuIDField, _ := detail["sku_id"].(float64) + + fmt.Printf(" 📋 error: %s\n", truncate(errMsg, 200)) + fmt.Printf(" 📋 stock: %.0f, sku_id: %.0f\n", stockField, skuIDField) + + if strings.Contains(errMsg, "执行成功") || strings.Contains(errMsg, "成功") { + pass(cat, name, fmt.Sprintf("改库存执行成功: error='%s', stock=%.0f, sku_id=%.0f", + truncate(errMsg, 100), stockField, skuIDField), elapsed) + } else { + fail(cat, name, fmt.Sprintf("改库存失败: error='%s'", truncate(errMsg, 200)), elapsed) + } + } + + // ---------- 步骤3:校验库存(接口验证)---------- + { + name := "3、校验库存(接口验证)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 延迟后接口校验 + countdownDelay(DelayPddStockAfterAPI, "接口校验") + + // 获取 accessToken + accessToken, err := getAccessToken(priceTaskID) + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + + // 查询商品详情 + rawMap, err := queryGoodsDetail(accessToken, successGoodsID, 3) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, fmt.Sprintf("查询商品详情失败: %v", err), elapsed) + return + } + + // 提取库存 + quantity := extractSkuQuantity(rawMap, successSkuID) + + // 提取商品状态 + goodsStatus, _ := extractGoodsStatus(rawMap) + statusStr := fmt.Sprintf("%d", goodsStatus) + if n, ok := StatusName[goodsStatus]; ok { + statusStr = n + } + + fmt.Printf(" 📦 sku_id=%d: quantity=%d, 期望stock=%d\n", successSkuID, quantity, testStock) + fmt.Printf(" 📦 商品状态: %s(%s)\n", statusStr, StatusName[goodsStatus]) + + if quantity == testStock { + pass(cat, name, fmt.Sprintf("库存匹配: quantity=%d 与传入 stock=%d 一致 ✅(商品状态=%s)", + quantity, testStock, statusStr), elapsed) + } else if quantity == -1 { + errCase(cat, name, fmt.Sprintf("未能从响应中提取库存数量"), elapsed) + } else { + fail(cat, name, fmt.Sprintf("库存不匹配:传入stock=%d,实际quantity=%d(商品状态=%s)", + testStock, quantity, statusStr), elapsed) + } + } +} + +// ============================================================ +// 测试四:拼多多上下架 +// ============================================================ + +func testPddShelfOnOff() { + cat := "四、拼多多上下架" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + // 前置条件检查 + if mixedTaskID == "" { + fmt.Println("⚠️ 改价格 task_id 为空,跳过上下架测试") + fail(cat, "前置条件", "场景二未创建改价格任务,无法获取 task_id", 0) + return + } + if successISBN == "" || successGoodsID == 0 { + fmt.Println("⚠️ 场景一未匹配到执行成功数据,跳过上下架测试") + fail(cat, "前置条件", "场景一未匹配到执行成功数据(isbn/goods_id)", 0) + return + } + + fmt.Printf("\n 📌 使用改价格 task_id: %s(复用 task_type=5 任务)\n", mixedTaskID) + + // ---------- 步骤1:发送下架任务 ---------- + { + name := "1、发送任务数据【下架】" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"goods_id":%d,"status":2}}`, + successISBN, successGoodsID) + fmt.Printf(" 📋 task_id: %s\n", mixedTaskID) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + + params := map[string]string{ + "task_id": mixedTaskID, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("ISBN=%s GoodsID=%d status=2(下架) 接口返回成功", successISBN, successGoodsID), elapsed) + } + + // 延迟后校验 + countdownDelay(DelayPddShelfAfterSend, "校验") + + // ---------- 等待下架任务处理完成 ---------- + { + fmt.Printf("\n ⏳ 等待下架任务处理完成(检查 body_over)...\n") + // 改价格 + 改库存 已有 2 条,下架后应该达到 3 条 + if err := waitBodyOverMin(mixedTaskID, BodyOverMinShelf, DelayPddShelfBodyOverTimeout); err != nil { + fmt.Printf(" ⚠️ 等待超时: %v,继续尝试校验\n", err) + } else { + fmt.Printf(" ✅ 下架任务已处理完成\n") + } + } + + // ---------- 步骤2:校验下架状态 ---------- + // 校验接口:从配置读取 URL 和 Basic Auth + // 响应 status 字段:1=上架,2=下架,3=售罄,4=已删除 + // 期望:status=2(下架) + { + name := "2、校验下架状态" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 获取 accessToken + accessToken, err := getAccessToken(priceTaskID) + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + fmt.Printf(" 🔑 accessToken: %s...%s\n", accessToken[:8], accessToken[len(accessToken)-8:]) + fmt.Printf(" 📦 goodsId: %d\n", successGoodsID) + + // 延迟后接口校验 + countdownDelay(DelayPddShelfAfterWait, "接口校验") + + // 查询商品详情(最多重试5次) + rawMap, err := queryGoodsDetail(accessToken, successGoodsID, 5) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, fmt.Sprintf("查询商品详情失败: %v", err), elapsed) + return + } + + // 提取商品状态 + goodsStatus, goodsName := extractGoodsStatus(rawMap) + // 尝试从 data 层提取 + if goodsStatus == -1 { + if dataMap, ok := rawMap["data"].(map[string]interface{}); ok { + goodsStatus, goodsName = extractGoodsStatus(dataMap) + } + } + // 尝试从嵌套层提取 + if goodsStatus == -1 { + for _, key := range []string{"goods", "goodsDetail", "result"} { + if nested, ok := rawMap[key].(map[string]interface{}); ok { + goodsStatus, goodsName = extractGoodsStatus(nested) + if goodsStatus != -1 { + break + } + } + } + } + + // 状态码映射 + statusStr := fmt.Sprintf("%d", goodsStatus) + if n, ok := StatusName[goodsStatus]; ok { + statusStr = n + } + + if goodsStatus == 2 { + pass(cat, name, fmt.Sprintf("商品已下架 status=2(下架) goodsId=%d goodsName=%s", + successGoodsID, truncate(goodsName, 50)), elapsed) + } else if goodsStatus == 1 { + fail(cat, name, fmt.Sprintf("商品仍处于上架状态 status=1(上架) goodsId=%d,下架可能未生效", successGoodsID), elapsed) + } else if goodsStatus == 3 { + fail(cat, name, fmt.Sprintf("商品状态为售罄 status=3(售罄) goodsId=%d,非预期的下架状态", successGoodsID), elapsed) + } else if goodsStatus == 4 { + errCase(cat, name, fmt.Sprintf("商品已删除 status=4(已删除) goodsId=%d", successGoodsID), elapsed) + } else { + errCase(cat, name, fmt.Sprintf("未能解析商品状态 status=%d(%s)", + goodsStatus, statusStr), elapsed) + } + } +} + +// ============================================================ +// 测试四(补充):拼多多删除商品 +// ============================================================ + +func testPddGoodsDelete() { + cat := "四(补充)、拼多多删除商品" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + // 前置条件检查 + if mixedTaskID == "" { + fmt.Println("⚠️ 改价格 task_id 为空,跳过删除测试") + fail(cat, "前置条件", "场景二未创建改价格任务,无法获取 task_id", 0) + return + } + if successISBN == "" || successGoodsID == 0 { + fmt.Println("⚠️ 场景一未匹配到执行成功数据,跳过删除测试") + fail(cat, "前置条件", "场景一未匹配到执行成功数据(isbn/goods_id)", 0) + return + } + + fmt.Printf("\n 📌 使用改价格 task_id: %s(复用 task_type=5 任务)\n", mixedTaskID) + + // ---------- 步骤1:发送删除任务 ---------- + { + name := "1、发送任务数据【删除】" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"goods_id":%d,"status":3}}`, + successISBN, successGoodsID) + fmt.Printf(" 📋 task_id: %s\n", mixedTaskID) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + + params := map[string]string{ + "task_id": mixedTaskID, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("ISBN=%s GoodsID=%d status=3(删除) 接口返回成功", successISBN, successGoodsID), elapsed) + } + + // 延迟后校验 + countdownDelay(DelayPddDeleteAfterSend, "校验") + + // ---------- 步骤2:等待删除任务处理完成 ---------- + { + name := "2、等待任务处理完成" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + fmt.Printf(" ⏳ 等待删除任务处理完成(检查 body_over)...\n") + + // 上下架已有 3 条,删除后再增加 1 条,应达到 4 条 + if err := waitBodyOverMin(mixedTaskID, BodyOverMinDelete, DelayPddShelfBodyOverTimeout); err != nil { + fmt.Printf(" ⚠️ 等待超时: %v,继续尝试校验\n", err) + } else { + fmt.Printf(" ✅ 删除任务已处理完成\n") + } + elapsed := time.Since(start) + pass(cat, name, fmt.Sprintf("body_over ≥ %d", BodyOverMinDelete), elapsed) + } + + // 步骤3:延迟后接口校验 + countdownDelay(DelayPddDeleteAfterAPI, "接口校验") + + // ---------- 步骤4:校验删除状态 ---------- + { + name := "4、校验删除状态" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 获取 accessToken + accessToken, err := getAccessToken(priceTaskID) + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + fmt.Printf(" 🔑 accessToken: %s...%s\n", accessToken[:8], accessToken[len(accessToken)-8:]) + fmt.Printf(" 📦 goodsId: %d\n", successGoodsID) + + // 查询商品详情(最多重试5次) + rawMap, err := queryGoodsDetail(accessToken, successGoodsID, 5) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, fmt.Sprintf("查询商品详情失败: %v", err), elapsed) + return + } + + // 提取商品状态 + goodsStatus, goodsName := extractGoodsStatus(rawMap) + if goodsStatus == -1 { + if dataMap, ok := rawMap["data"].(map[string]interface{}); ok { + goodsStatus, goodsName = extractGoodsStatus(dataMap) + } + } + if goodsStatus == -1 { + for _, key := range []string{"goods", "goodsDetail", "result"} { + if nested, ok := rawMap[key].(map[string]interface{}); ok { + goodsStatus, goodsName = extractGoodsStatus(nested) + if goodsStatus != -1 { + break + } + } + } + } + + var statusStr string + if n, ok := StatusName[goodsStatus]; ok { + statusStr = n + } + fmt.Printf(" 📊 商品状态: status=%d(%s), name=%s\n", goodsStatus, statusStr, goodsName) + + if goodsStatus == 4 { + pass(cat, name, fmt.Sprintf("商品已删除 status=4(已删除) goodsId=%d", successGoodsID), elapsed) + } else if goodsStatus == 2 { + fail(cat, name, fmt.Sprintf("商品状态为下架 status=2(下架) goodsId=%d,非预期的删除状态", successGoodsID), elapsed) + } else if goodsStatus == 3 { + fail(cat, name, fmt.Sprintf("商品状态为售罄 status=3(售罄) goodsId=%d,非预期的删除状态", successGoodsID), elapsed) + } else if goodsStatus == 1 { + fail(cat, name, fmt.Sprintf("商品状态为上架 status=1(上架) goodsId=%d,非预期的删除状态", successGoodsID), elapsed) + } else { + fail(cat, name, fmt.Sprintf("未能解析商品状态 status=%d, name=%s", goodsStatus, goodsName), elapsed) + } + } +} + +// ============================================================ +// 测试五:拼多多商品拉取任务 +// ============================================================ + +func testPddPullGoods() { + cat := "五、拼多多商品拉取" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + // ---------- 步骤1:创建商品拉取任务 ---------- + var tid string + { + name := "1、创建拼多多商品拉取任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": ShopID, + "shop_type": ShopType, + "task_count": TaskCount, + "task_type": TaskTypePullGoods, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + if dataStr == "" { + fail(cat, name, "返回 data 为空", elapsed) + return + } + tid = dataStr + pullTaskID = tid + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + if tid == "" { + fmt.Println("⚠️ 拉取任务创建失败,跳过后续步骤") + return + } + fmt.Printf("\n 📌 商品拉取 task_id: %s\n", tid) + + // ---------- 步骤2:等待任务完成(status=4)并校验 ---------- + { + name := "2、等待任务完成并校验" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 2a: 等待 header status=4 + fmt.Printf(" ⏳ 等待任务完成(header status=4,无时间限制,全店拉取数据量大)...\n") + waitStart := time.Now() + for { + st, err := getHeaderStatus(tid) + if err == nil && st == 4 { + fmt.Printf(" ✅ 任务状态已变为 status=4(耗时 %v)\n", time.Since(waitStart).Round(time.Second)) + break + } + // 每30秒打印一次进度 + if int(time.Since(waitStart).Seconds()) > 0 && int(time.Since(waitStart).Seconds())%30 == 0 { + bodyOverCnt, _ := getBodyOverCount(tid) + fmt.Printf(" ⏳ 已等待 %v,当前 status=%d,body_over=%d\n", + time.Since(waitStart).Round(time.Second), st, bodyOverCnt) + } + time.Sleep(PollInterval) + } + + // 2b: 对比 task_count_true 与 body_over 数量 + headerKey := tid + ":header" + taskCountTrueStr, err := redisClient.HGet(redisCtx, headerKey, "task_count_true").Result() + if err != nil { + fail(cat, name, fmt.Sprintf("获取 task_count_true 失败: %v", err), time.Since(start)) + return + } + taskCountTrue, err := strconv.ParseInt(taskCountTrueStr, 10, 64) + if err != nil { + fail(cat, name, fmt.Sprintf("解析 task_count_true 失败: %v", err), time.Since(start)) + return + } + + bodyOverCount, err := getBodyOverCount(tid) + if err != nil { + fail(cat, name, fmt.Sprintf("获取 body_over 数量失败: %v", err), time.Since(start)) + return + } + + fmt.Printf(" 📊 task_count_true=%d, body_over 数量=%d\n", taskCountTrue, bodyOverCount) + + if bodyOverCount > taskCountTrue { + fail(cat, name, fmt.Sprintf("body_over 数量(%d) > task_count_true(%d),不一致", bodyOverCount, taskCountTrue), time.Since(start)) + return + } + fmt.Printf(" ✅ body_over 数量(%d) ≤ task_count_true(%d),一致\n", bodyOverCount, taskCountTrue) + + // 2c: 检查场景一 ISBN 是否存在于 body_over 或 body_wait 中 + if successISBN == "" { + fail(cat, name, "场景一未匹配到执行成功数据(isbn 为空),无法检查拉取结果", time.Since(start)) + return + } + + foundISBN := false + var foundGoodsID int64 + var foundIn string + + // 先检查 body_over(分批搜索,全店拉取数据量大) + fmt.Printf(" 🔍 在 body_over 中搜索 ISBN=%s(共 %d 条,分批搜索)...\n", successISBN, bodyOverCount) + searchPageSize := SearchPageSize + for offset := int64(0); offset < bodyOverCount; offset += searchPageSize { + end := offset + searchPageSize - 1 + if end >= bodyOverCount { + end = bodyOverCount - 1 + } + items, err := getBodyOverFromRedis(tid, offset, end) + if err != nil || len(items) == 0 { + break + } + for _, item := range items { + if bi, ok := item["book_info"].(map[string]interface{}); ok { + if isbn, ok := bi["isbn"].(string); ok && isbn == successISBN { + foundISBN = true + foundIn = "body_over" + if detail, ok := item["detail"].(map[string]interface{}); ok { + if v, ok := detail["goods_id"].(float64); ok { + foundGoodsID = int64(v) + } + } + break + } + } + } + if foundISBN { + break + } + // 进度提示 + if (offset+searchPageSize)%BodyWaitMaxSearch == 0 { + fmt.Printf(" 🔍 已搜索 %d/%d 条...\n", offset+searchPageSize, bodyOverCount) + } + } + + // 如果 body_over 没找到,检查 body_wait + if !foundISBN { + bodyWaitKey := tid + ":body_wait" + bodyWaitCount, err := redisClient.LLen(redisCtx, bodyWaitKey).Result() + if err == nil && bodyWaitCount > 0 { + fmt.Printf(" 🔍 body_over 未找到,正在搜索 body_wait (%d 条)...\n", bodyWaitCount) + maxSearch := bodyWaitCount + if maxSearch > BodyWaitMaxSearch { + maxSearch = BodyWaitMaxSearch + fmt.Printf(" ⚠️ body_wait 数据量巨大,仅搜索前 %d 条\n", BodyWaitMaxSearch) + } + bodyWaitVals, err := redisClient.LRange(redisCtx, bodyWaitKey, 0, maxSearch-1).Result() + if err == nil { + for _, v := range bodyWaitVals { + var item map[string]interface{} + if json.Unmarshal([]byte(v), &item) != nil { + continue + } + if bi, ok := item["book_info"].(map[string]interface{}); ok { + if isbn, ok := bi["isbn"].(string); ok && isbn == successISBN { + foundISBN = true + foundIn = "body_wait" + if detail, ok := item["detail"].(map[string]interface{}); ok { + if v, ok := detail["goods_id"].(float64); ok { + foundGoodsID = int64(v) + } + } + break + } + } + } + } + } + } + + elapsed := time.Since(start) + + if foundISBN { + pullGoodsID = foundGoodsID + goodsIDStr := "" + if foundGoodsID > 0 { + goodsIDStr = fmt.Sprintf(", goods_id=%d", foundGoodsID) + } + pass(cat, name, fmt.Sprintf("status=4 ✅ | body_over(%d) ≤ task_count_true(%d) ✅ | ISBN=%s 在 %s 中找到%s", + bodyOverCount, taskCountTrue, successISBN, foundIn, goodsIDStr), elapsed) + } else { + fail(cat, name, fmt.Sprintf("status=4 ✅ | body_over(%d) ≤ task_count_true(%d) ✅ | 但 ISBN=%s 不在 body_over 也不在 body_wait(前%d条) 中", + bodyOverCount, taskCountTrue, successISBN, BodyWaitMaxSearch), elapsed) + } + } +} + +// ============================================================ +// 测试九:闲鱼改价格 +// ============================================================ + +func testXyPriceChange() { + cat := "九、闲鱼改价格" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + var tid string + + // 前置条件 + if xyTaskID == "" || xySuccessISBN == "" { + fmt.Println("⚠️ 场景七未创建闲鱼任务,跳过") + return + } + fmt.Printf("\n 📌 复用场景七 task_id: %s\n", xyTaskID) + fmt.Printf(" 📌 目标 ISBN: %s\n", xySuccessISBN) + + // 步骤1:创建改价格任务(task_type=5,与闲鱼共用) + { + name := "1、创建闲鱼改价格任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": XyShopID, + "shop_type": XyShopType, + "task_count": TaskCount, + "task_type": TaskTypeXyPriceStockShelf, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + tid = dataStr + fmt.Printf(" ✅ task_id=%s\n", tid) + } + + if tid == "" { + fmt.Println("⚠️ 任务创建失败,跳过") + return + } + + // 步骤2:查询商品详情(改价格前) + var basePrice int64 + { + name := "2、查询商品详情(改价格前)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + accessToken, err := getXyAccessToken() + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + + // 使用核价发布步骤已获取的 goods_id + xyGoodsID := xySuccessGoodsID + if xyGoodsID == 0 { + fail(cat, name, "xySuccessGoodsID 为空,场景七未成功获取商品ID", time.Since(start)) + return + } + fmt.Printf(" 🔑 accessToken: %s...%s\n", accessToken[:8], accessToken[len(accessToken)-8:]) + fmt.Printf(" 📦 goodsId: %d\n", xyGoodsID) + + price, err := getXyGoodsPrice(xyGoodsID, accessToken) + elapsed := time.Since(start) + if err != nil { + fail(cat, name, fmt.Sprintf("查询价格失败: %v", err), elapsed) + return + } + basePrice = price + fmt.Printf(" 💰 当前价格: %d (分)\n", basePrice) + pass(cat, name, fmt.Sprintf("goodsId=%d 当前价格=%d", xyGoodsID, basePrice), elapsed) + } + + countdownDelay(DelayXyPriceChangeAfterQuery, "改价格") + + // 步骤3:发送改价格任务 + { + name := "3、发送改价格任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + xyGoodsID := xySuccessGoodsID + if xyGoodsID == 0 { + fail(cat, name, "xySuccessGoodsID 为空", time.Since(start)) + return + } + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"goods_id":%d,"price":%d,"status":5}}`, + xySuccessISBN, xyGoodsID, TestXyNewPrice) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("改价格目标=%d (分)", TestXyNewPrice), elapsed) + } + + countdownDelay(DelayXyPriceChangeAfterSend, "Redis校验") + + // 步骤4:校验 body_over + { + name := "4、校验 body_over" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + if err := waitBodyOverMin(tid, 1, 30); err != nil { + fail(cat, name, fmt.Sprintf("body_over < 1: %v", err), time.Since(start)) + } else { + pass(cat, name, "body_over ≥ 1", time.Since(start)) + } + } + + countdownDelay(DelayXyPriceChangeAfterAPI, "接口校验") + + // 步骤5:接口校验 + { + name := "5、接口校验(价格已修改)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + accessToken, _ := getXyAccessToken() + xyGoodsID := xySuccessGoodsID + newPrice, err := getXyGoodsPrice(xyGoodsID, accessToken) + elapsed := time.Since(start) + if err != nil { + fail(cat, name, fmt.Sprintf("查询价格失败: %v", err), elapsed) + return + } + fmt.Printf(" 📊 当前价格: %d (分)\n", newPrice) + if newPrice == TestXyNewPrice { + pass(cat, name, fmt.Sprintf("价格已改为 %d", TestXyNewPrice), elapsed) + } else { + fail(cat, name, fmt.Sprintf("价格未变更: 期望=%d 实际=%d", TestXyNewPrice, newPrice), elapsed) + } + } +} + +// ============================================================ +// 测试十:闲鱼改库存 +// ============================================================ + +func testXyStockChange() { + cat := "十、闲鱼改库存" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + // 复用九创建的改价格任务 + fmt.Printf("\n 📌 目标 ISBN: %s\n", xySuccessISBN) + + // 步骤1:创建改库存任务 + { + name := "1、创建闲鱼改库存任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": XyShopID, + "shop_type": XyShopType, + "task_count": TaskCount, + "task_type": TaskTypeXyPriceStockShelf, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + xyModTaskID = dataStr + fmt.Printf(" ✅ task_id=%s\n", xyModTaskID) + } + + if xyModTaskID == "" { + fmt.Println("⚠️ 任务创建失败,跳过") + return + } + + // 步骤2:发送改库存任务 + { + name := "2、发送改库存任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + xyGoodsID := xySuccessGoodsID + if xyGoodsID == 0 { + fail(cat, name, "xySuccessGoodsID 为空", time.Since(start)) + return + } + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"goods_id":%d,"stock":%d,"status":4}}`, + xySuccessISBN, xyGoodsID, TestXyNewStock) + fmt.Printf(" 📋 task_id: %s\n", xyModTaskID) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + params := map[string]string{ + "task_id": xyModTaskID, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("改库存目标=%d", TestXyNewStock), elapsed) + } + + countdownDelay(DelayXyStockAfterSend, "Redis校验") + + // 步骤3:校验 body_over + { + name := "3、校验 body_over" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + if err := waitBodyOverMin(xyModTaskID, 1, 30); err != nil { + fail(cat, name, fmt.Sprintf("body_over < 1: %v", err), time.Since(start)) + } else { + pass(cat, name, "body_over ≥ 1", time.Since(start)) + } + } + + countdownDelay(DelayXyStockAfterAPI, "接口校验") + + // 步骤4:接口校验 + { + name := "4、接口校验(库存已修改)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + accessToken, _ := getXyAccessToken() + xyGoodsID := xySuccessGoodsID + newStock, err := getXyGoodsStock(xyGoodsID, accessToken) + elapsed := time.Since(start) + if err != nil { + fail(cat, name, fmt.Sprintf("查询库存失败: %v", err), elapsed) + return + } + fmt.Printf(" 📊 当前库存: %d\n", newStock) + if newStock == TestXyNewStock { + pass(cat, name, fmt.Sprintf("库存已改为 %d", TestXyNewStock), elapsed) + } else { + fail(cat, name, fmt.Sprintf("库存未变更: 期望=%d 实际=%d", TestXyNewStock, newStock), elapsed) + } + } +} + +// ============================================================ +// 测试十一:闲鱼上下架 +// ============================================================ + +func testXyShelfOff() { + cat := "十一、闲鱼下架" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + // 前置条件检查 + if xyModTaskID == "" { + fmt.Println("⚠️ 闲鱼核价发布 task_id 为空,跳过下架测试") + fail(cat, "前置条件", "场景七未创建核价发布任务,无法获取 task_id", 0) + return + } + if xySuccessISBN == "" || xySuccessGoodsID == 0 { + fmt.Println("⚠️ 场景七未匹配到执行成功数据,跳过下架测试") + fail(cat, "前置条件", "场景七未匹配到执行成功数据(isbn/goods_id)", 0) + return + } + + fmt.Printf("\n 📌 复用场景七 task_id: %s\n", xyModTaskID) + fmt.Printf(" 📌 目标 ISBN: %s\n", xySuccessISBN) + + // ---------- 步骤1:发送下架任务 ---------- + { + name := "1、发送任务数据【下架】" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"goods_id":%d,"status":2}}`, + xySuccessISBN, xySuccessGoodsID) + fmt.Printf(" 📋 task_id: %s\n", xyModTaskID) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + + params := map[string]string{ + "task_id": xyModTaskID, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("ISBN=%s GoodsID=%d status=2(下架) 接口返回成功", xySuccessISBN, xySuccessGoodsID), elapsed) + } + + countdownDelay(DelayXyShelfAfterSend, "校验") + + // ---------- 等待下架任务处理完成 ---------- + { + fmt.Printf("\n ⏳ 等待下架任务处理完成(检查 body_over)...\n") + if err := waitBodyOverMin(xyModTaskID, BodyOverMinXyShelfOnOff, DelayXyShelfBodyOverTimeout); err != nil { + fmt.Printf(" ⚠️ 等待超时: %v,继续尝试校验\n", err) + } else { + fmt.Printf(" ✅ 下架任务已处理完成\n") + } + } + + countdownDelay(DelayXyShelfAfterWait, "接口校验") + + // ---------- 步骤2:校验下架状态 ---------- + { + name := "2、校验下架状态" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + accessToken, _ := getXyAccessToken() + status, err := getXyGoodsStatus(xySuccessGoodsID, accessToken) + elapsed := time.Since(start) + if err != nil { + fail(cat, name, fmt.Sprintf("查询状态失败: %v", err), elapsed) + return + } + statusStr := "" + if n, ok := StatusName[status]; ok { + statusStr = n + } + fmt.Printf(" 📊 商品状态: status=%d(%s)\n", status, statusStr) + if status == 2 { + pass(cat, name, fmt.Sprintf("商品已下架 status=2(下架) goodsId=%d", xySuccessGoodsID), elapsed) + } else if status == 1 { + fail(cat, name, fmt.Sprintf("商品仍处于上架状态 status=1(上架) goodsId=%d,下架可能未生效", xySuccessGoodsID), elapsed) + } else { + fail(cat, name, fmt.Sprintf("状态不符合预期: 期望=status=2(下架) 实际=status=%d(%s)", status, statusStr), elapsed) + } + } +} + +// ============================================================ +// 测试十二:闲鱼上架 +// ============================================================ +func testXyShelfOn() { + cat := "十二、闲鱼上架" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + // 前置条件检查 + if xyModTaskID == "" { + fmt.Println("⚠️ 闲鱼核价发布 task_id 为空,跳过上架测试") + fail(cat, "前置条件", "场景七未创建核价发布任务,无法获取 task_id", 0) + return + } + if xySuccessISBN == "" || xySuccessGoodsID == 0 { + fmt.Println("⚠️ 场景七未匹配到执行成功数据,跳过上架测试") + fail(cat, "前置条件", "场景七未匹配到执行成功数据(isbn/goods_id)", 0) + return + } + + fmt.Printf("\n 📌 复用场景七 task_id: %s\n", xyModTaskID) + fmt.Printf(" 📌 目标 ISBN: %s\n", xySuccessISBN) + + // ---------- 步骤1:发送上架任务 ---------- + { + name := "1、发送任务数据【上架】" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"goods_id":%d,"status":1}}`, + xySuccessISBN, xySuccessGoodsID) + fmt.Printf(" 📋 task_id: %s\n", xyModTaskID) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + + params := map[string]string{ + "task_id": xyModTaskID, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("ISBN=%s GoodsID=%d status=1(上架) 接口返回成功", xySuccessISBN, xySuccessGoodsID), elapsed) + } + + countdownDelay(DelayXyShelfAfterSend, "校验") + + // ---------- 等待上架任务处理完成 ---------- + { + fmt.Printf("\n ⏳ 等待上架任务处理完成(检查 body_over)...\n") + if err := waitBodyOverMin(xyModTaskID, BodyOverMinXyShelfOnOff, DelayXyShelfBodyOverTimeout); err != nil { + fmt.Printf(" ⚠️ 等待超时: %v,继续尝试校验\n", err) + } else { + fmt.Printf(" ✅ 上架任务已处理完成\n") + } + } + + countdownDelay(DelayXyShelfAfterWait, "接口校验") + + // ---------- 步骤2:校验上架状态 ---------- + { + name := "2、校验上架状态" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + accessToken, _ := getXyAccessToken() + status, err := getXyGoodsStatus(xySuccessGoodsID, accessToken) + elapsed := time.Since(start) + if err != nil { + fail(cat, name, fmt.Sprintf("查询状态失败: %v", err), elapsed) + return + } + statusStr := "" + if n, ok := StatusName[status]; ok { + statusStr = n + } + fmt.Printf(" 📊 商品状态: status=%d(%s)\n", status, statusStr) + if status == 1 { + pass(cat, name, fmt.Sprintf("商品已上架 status=1(上架) goodsId=%d", xySuccessGoodsID), elapsed) + } else if status == 2 { + fail(cat, name, fmt.Sprintf("商品仍处于下架状态 status=2(下架) goodsId=%d,上架可能未生效", xySuccessGoodsID), elapsed) + } else { + fail(cat, name, fmt.Sprintf("状态不符合预期: 期望=status=1(上架) 实际=status=%d(%s)", status, statusStr), elapsed) + } + } +} + +// ============================================================ +// 测试八:闲鱼商品拉取任务 +// ============================================================ +func testXyPullGoods() { + cat := "八、闲鱼商品拉取任务" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + var tid string + + // ---------- 步骤1:创建闲鱼商品拉取任务 ---------- + { + name := "1、创建闲鱼商品拉取任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + fields := map[string]string{ + "shop_id": XyShopID, + "shop_type": XyShopType, + "task_count": TaskCount, + "task_type": TaskTypePullGoods, + "img_type": ImgType, + } + fields["sign"] = SignParams(fields) + + resp, err := postMultipart(BaseURL+"/task/create", fields) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + if dataStr == "" { + fail(cat, name, "返回 data 为空", elapsed) + return + } + tid = dataStr + xyPullTaskID = tid + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + if tid == "" { + fmt.Println("⚠️ 拉取任务创建失败,跳过后续步骤") + return + } + fmt.Printf("\n 📌 闲鱼商品拉取 task_id: %s\n", tid) + + // ---------- 步骤2:等待任务完成(status=4)并校验 ---------- + { + name := "2、等待任务完成并校验" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 2a: 等待 header status=4 + fmt.Printf(" ⏳ 等待任务完成(header status=4,无时间限制,拉取数据量大)...\n") + waitStart := time.Now() + for { + st, err := getHeaderStatus(tid) + if err == nil && st == 4 { + fmt.Printf(" ✅ 任务状态已变为 status=4(耗时 %v)\n", time.Since(waitStart).Round(time.Second)) + break + } + // 每30秒打印一次进度 + if int(time.Since(waitStart).Seconds()) > 0 && int(time.Since(waitStart).Seconds())%30 == 0 { + bodyOverCnt, _ := getBodyOverCount(tid) + fmt.Printf(" ⏳ 已等待 %v,当前 status=%d,body_over=%d\n", + time.Since(waitStart).Round(time.Second), st, bodyOverCnt) + } + time.Sleep(PollInterval) + } + + // 2b: 对比 task_count_true 与 body_over 数量 + headerKey := tid + ":header" + taskCountTrueStr, err := redisClient.HGet(redisCtx, headerKey, "task_count_true").Result() + if err != nil { + fail(cat, name, fmt.Sprintf("获取 task_count_true 失败: %v", err), time.Since(start)) + return + } + taskCountTrue, err := strconv.ParseInt(taskCountTrueStr, 10, 64) + if err != nil { + fail(cat, name, fmt.Sprintf("解析 task_count_true 失败: %v", err), time.Since(start)) + return + } + + bodyOverCount, err := getBodyOverCount(tid) + if err != nil { + fail(cat, name, fmt.Sprintf("获取 body_over 数量失败: %v", err), time.Since(start)) + return + } + + fmt.Printf(" 📊 task_count_true=%d, body_over 数量=%d\n", taskCountTrue, bodyOverCount) + + if bodyOverCount > taskCountTrue { + fail(cat, name, fmt.Sprintf("body_over 数量(%d) > task_count_true(%d),不一致", bodyOverCount, taskCountTrue), time.Since(start)) + return + } + fmt.Printf(" ✅ body_over 数量(%d) ≤ task_count_true(%d),一致\n", bodyOverCount, taskCountTrue) + + // 2c: 检查场景七 ISBN 是否存在于 body_over 或 body_wait 中 + if xySuccessISBN == "" { + fail(cat, name, "场景七未匹配到执行成功数据(isbn 为空),无法检查拉取结果", time.Since(start)) + return + } + + foundISBN := false + var foundGoodsID int64 + var foundIn string + + // 先检查 body_over(分批搜索,数据量大) + fmt.Printf(" 🔍 在 body_over 中搜索 ISBN=%s(共 %d 条,分批搜索)...\n", xySuccessISBN, bodyOverCount) + searchPageSize := SearchPageSize + for offset := int64(0); offset < bodyOverCount; offset += searchPageSize { + end := offset + searchPageSize - 1 + if end >= bodyOverCount { + end = bodyOverCount - 1 + } + items, err := getBodyOverFromRedis(tid, offset, end) + if err != nil || len(items) == 0 { + break + } + for _, item := range items { + if bi, ok := item["book_info"].(map[string]interface{}); ok { + if isbn, ok := bi["isbn"].(string); ok && isbn == xySuccessISBN { + foundISBN = true + foundIn = "body_over" + if detail, ok := item["detail"].(map[string]interface{}); ok { + if v, ok := detail["goods_id"].(float64); ok { + foundGoodsID = int64(v) + } + } + break + } + } + } + if foundISBN { + break + } + // 进度提示 + if (offset+searchPageSize)%BodyWaitMaxSearch == 0 { + fmt.Printf(" 🔍 已搜索 %d/%d 条...\n", offset+searchPageSize, bodyOverCount) + } + } + + // 如果 body_over 没找到,检查 body_wait + if !foundISBN { + bodyWaitKey := tid + ":body_wait" + bodyWaitCount, err := redisClient.LLen(redisCtx, bodyWaitKey).Result() + if err == nil && bodyWaitCount > 0 { + fmt.Printf(" 🔍 body_over 未找到,正在搜索 body_wait (%d 条)...\n", bodyWaitCount) + maxSearch := bodyWaitCount + if maxSearch > BodyWaitMaxSearch { + maxSearch = BodyWaitMaxSearch + fmt.Printf(" ⚠️ body_wait 数据量巨大,仅搜索前 %d 条\n", BodyWaitMaxSearch) + } + bodyWaitVals, err := redisClient.LRange(redisCtx, bodyWaitKey, 0, maxSearch-1).Result() + if err == nil { + for _, v := range bodyWaitVals { + var item map[string]interface{} + if json.Unmarshal([]byte(v), &item) != nil { + continue + } + if bi, ok := item["book_info"].(map[string]interface{}); ok { + if isbn, ok := bi["isbn"].(string); ok && isbn == xySuccessISBN { + foundISBN = true + foundIn = "body_wait" + if detail, ok := item["detail"].(map[string]interface{}); ok { + if v, ok := detail["goods_id"].(float64); ok { + foundGoodsID = int64(v) + } + } + break + } + } + } + } + } + } + + elapsed := time.Since(start) + + if foundISBN { + xyPullGoodsID = foundGoodsID + goodsIDStr := "" + if foundGoodsID > 0 { + goodsIDStr = fmt.Sprintf(", goods_id=%d", foundGoodsID) + } + pass(cat, name, fmt.Sprintf("status=4 ✅ | body_over(%d) ≤ task_count_true(%d) ✅ | ISBN=%s 在 %s 中找到%s", + bodyOverCount, taskCountTrue, xySuccessISBN, foundIn, goodsIDStr), elapsed) + } else { + fail(cat, name, fmt.Sprintf("status=4 ✅ | body_over(%d) ≤ task_count_true(%d) ✅ | 但 ISBN=%s 不在 body_over 也不在 body_wait(前%d条) 中", + bodyOverCount, taskCountTrue, xySuccessISBN, BodyWaitMaxSearch), elapsed) + } + } +} + +// ============================================================ +// 主函数 +// ============================================================ + +// ============================================================ +// 孔夫子测试函数 +// ============================================================ + +// testKfzPricePublish 孔夫子核价发布 +func testKfzPricePublish() { + cat := "孔夫子核价发布" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + var tid string + + // 步骤1:创建任务 + { + name := "1、创建孔夫子核价发布任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": ShopID, + "shop_type": ShopType, + "task_count": TaskCount, + "task_type": TaskTypePricePublish, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + if dataStr == "" { + fail(cat, name, "返回 data 为空", elapsed) + return + } + tid = dataStr + kfzTaskID = tid + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + if tid == "" { + fmt.Println("⚠️ 任务创建失败,跳过后续步骤") + return + } + fmt.Printf("\n 📌 孔夫子 task_id: %s\n", tid) + + // 步骤2:发送 ISBN 数据 + { + name := fmt.Sprintf("2、发送任务数据【isbn=%s, price=%d】期望:执行成功", TestKfzISBNSuccess, TestKfzPriceSuccess) + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"price":%d}}`, TestKfzISBNSuccess, TestKfzPriceSuccess) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, "接口返回成功", elapsed) + } + + countdownDelay(DelayKfzPublishAfterSend, "Redis校验") + + // 步骤3:Redis 校验 body_over + { + name := "3、Redis 校验 - body_over 中执行成功" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + fmt.Printf(" ⏳ 等待后台处理完成(最多 %v)...\n", WaitTimeout) + + if err := waitBodyOverMin(tid, 1, WaitTimeout); err != nil { + errCase(cat, name, "等待超时: "+err.Error(), time.Since(start)) + return + } + + totalCnt, _ := getBodyOverCount(tid) + fmt.Printf(" 📊 body_over 总数: %d,开始校验...\n", totalCnt) + + targetISBN := TestKfzISBNSuccess + found := false + var foundGoodsID int64 + + for offset := int64(0); offset < totalCnt && !found; offset += 100 { + items, err := getBodyOverFromRedis(tid, offset, offset+99) + if err != nil || len(items) == 0 { + break + } + for _, item := range items { + isbn := "" + if bi, ok := item["book_info"].(map[string]interface{}); ok { + if v, ok := bi["isbn"].(string); ok { + isbn = v + } + } + if isbn != targetISBN { + continue + } + // 找到了,提取 itemId + if detail, ok := item["detail"].(map[string]interface{}); ok { + if itemId, ok := detail["itemId"].(float64); ok { + foundGoodsID = int64(itemId) + found = true + break + } + } + } + } + + if found { + kfzSuccessISBN = targetISBN + kfzSuccessGoodsID = foundGoodsID + pass(cat, name, fmt.Sprintf("ISBN=%s 找到,itemId=%d", targetISBN, foundGoodsID), time.Since(start)) + fmt.Printf("\n 📌 核价发布成功 - ISBN=%s, itemId=%d\n", targetISBN, foundGoodsID) + } else { + fail(cat, name, fmt.Sprintf("ISBN=%s 在 body_over 中未找到", targetISBN), time.Since(start)) + } + } +} + +// testKfzPriceChange 孔夫子改价格 +func testKfzPriceChange() { + cat := "孔夫子改价格" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + if kfzTaskID == "" || kfzSuccessISBN == "" { + fmt.Println("⚠️ 孔夫子发布任务未创建,跳过") + return + } + fmt.Printf("\n 📌 task_id: %s\n", kfzTaskID) + fmt.Printf(" 📌 目标 ISBN: %s, itemId: %d\n", kfzSuccessISBN, kfzSuccessGoodsID) + + // 步骤1:创建改价格任务 + var tid string + { + name := "1、创建孔夫子改价格任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": ShopID, + "shop_type": ShopType, + "task_count": TaskCount, + "task_type": TaskTypePriceStockShelf, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + tid = dataStr + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + countdownDelay(DelayKfzPriceChangeAfterQuery, "改价格") + + // 步骤2:发送改价格任务 + { + name := "2、发送改价格任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"itemId":%d,"price":%d}}`, + kfzSuccessISBN, kfzSuccessGoodsID, TestKfzNewPrice) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("改价格目标=%d", TestKfzNewPrice), elapsed) + } + + countdownDelay(DelayKfzPriceChangeAfterSend, "Redis校验") + + // 步骤3:校验 body_over + { + name := "3、校验 body_over" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + if err := waitBodyOverMin(tid, 1, 30); err != nil { + fail(cat, name, fmt.Sprintf("body_over < 1: %v", err), time.Since(start)) + } else { + pass(cat, name, "body_over ≥ 1", time.Since(start)) + } + } + + countdownDelay(DelayKfzPriceChangeAfterAPI, "接口校验") + + // 步骤4:接口校验 + { + name := "4、接口校验(价格已修改)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + accessToken, err := getKfzAccessToken() + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + fmt.Printf(" 🔑 token: %s...%s\n", accessToken[:8], accessToken[len(accessToken)-8:]) + + newPrice, err := getKfzGoodsPrice(kfzSuccessGoodsID, accessToken) + elapsed := time.Since(start) + if err != nil { + fail(cat, name, fmt.Sprintf("查询价格失败: %v", err), elapsed) + return + } + fmt.Printf(" 📊 当前价格: %.2f (元)\n", newPrice) + if newPrice == float64(TestKfzNewPrice)/100 { + pass(cat, name, fmt.Sprintf("价格已改为 %.2f", float64(TestKfzNewPrice)/100), elapsed) + } else { + fail(cat, name, fmt.Sprintf("价格未变更: 期望=%.2f 实际=%.2f", float64(TestKfzNewPrice)/100, newPrice), elapsed) + } + } +} + +// testKfzStockChange 孔夫子改库存 +func testKfzStockChange() { + cat := "孔夫子改库存" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + if kfzSuccessGoodsID == 0 { + fmt.Println("⚠️ 孔夫子发布任务未创建有效商品,跳过") + return + } + fmt.Printf("\n 📌 itemId: %d\n", kfzSuccessGoodsID) + + var tid string + // 步骤1:创建改库存任务 + { + name := "1、创建孔夫子改库存任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": ShopID, + "shop_type": ShopType, + "task_count": TaskCount, + "task_type": TaskTypePriceStockShelf, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + tid = dataStr + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + // 步骤2:发送改库存任务 + { + name := "2、发送改库存任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"itemId":%d,"stock":%d}}`, + kfzSuccessISBN, kfzSuccessGoodsID, TestKfzNewStock) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, fmt.Sprintf("改库存目标=%d", TestKfzNewStock), elapsed) + } + + countdownDelay(DelayKfzStockAfterSend, "Redis校验") + + // 步骤3:校验 body_over + { + name := "3、校验 body_over" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + if err := waitBodyOverMin(tid, 1, 30); err != nil { + fail(cat, name, fmt.Sprintf("body_over < 1: %v", err), time.Since(start)) + } else { + pass(cat, name, "body_over ≥ 1", time.Since(start)) + } + } + + countdownDelay(DelayKfzStockAfterAPI, "接口校验") + + // 步骤4:接口校验 + { + name := "4、接口校验(库存已修改)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + accessToken, err := getKfzAccessToken() + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + + newStock, err := getKfzGoodsStock(kfzSuccessGoodsID, accessToken) + elapsed := time.Since(start) + if err != nil { + fail(cat, name, fmt.Sprintf("查询库存失败: %v", err), elapsed) + return + } + fmt.Printf(" 📊 当前库存: %d\n", newStock) + if newStock == int(TestKfzNewStock) { + pass(cat, name, fmt.Sprintf("库存已改为 %d", TestKfzNewStock), elapsed) + } else { + fail(cat, name, fmt.Sprintf("库存未变更: 期望=%d 实际=%d", TestKfzNewStock, newStock), elapsed) + } + } +} + +// testKfzShelfOnOff 孔夫子上下架 +func testKfzShelfOnOff() { + cat := "孔夫子上下架" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + if kfzSuccessGoodsID == 0 { + fmt.Println("⚠️ 孔夫子发布任务未创建有效商品,跳过") + return + } + fmt.Printf("\n 📌 itemId: %d\n", kfzSuccessGoodsID) + + var tid string + + // 步骤1:创建上下架任务 + { + name := "1、创建孔夫子上下架任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": ShopID, + "shop_type": ShopType, + "task_count": TaskCount, + "task_type": TaskTypePriceStockShelf, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + tid = dataStr + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + // 步骤2:发送上架任务(status=1) + { + name := "2、发送上架任务(status=1)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"itemId":%d,"status":1}}`, + kfzSuccessISBN, kfzSuccessGoodsID) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, "上架任务发送成功", elapsed) + } + + countdownDelay(DelayKfzShelfAfterSend, "处理") + + // 步骤3:等待处理 + { + name := "3、等待任务处理" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + fmt.Printf(" ⏳ 等待后台处理完成(最多 %v)...\n", WaitTimeout) + + if err := waitBodyOverMin(tid, 1, WaitTimeout); err != nil { + errCase(cat, name, "等待超时: "+err.Error(), time.Since(start)) + return + } + pass(cat, name, "任务处理完成", time.Since(start)) + } + + countdownDelay(DelayKfzShelfAfterWait, "接口校验") + + // 步骤4:接口校验(上架) + { + name := "4、接口校验(上架)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + accessToken, err := getKfzAccessToken() + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + + status, err := getKfzGoodsStatus(kfzSuccessGoodsID, accessToken) + elapsed := time.Since(start) + if err != nil { + fail(cat, name, fmt.Sprintf("查询上下架状态失败: %v", err), elapsed) + return + } + fmt.Printf(" 📊 当前上下架状态: isOnSale=%d (1=上架)\n", status) + if status == 1 { + pass(cat, name, "商品已上架", elapsed) + } else { + fail(cat, name, fmt.Sprintf("商品未上架: isOnSale=%d", status), elapsed) + } + } + + // 步骤5:发送下架任务(status=2) + { + name := "5、发送下架任务(status=2)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"itemId":%d,"status":2}}`, + kfzSuccessISBN, kfzSuccessGoodsID) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, "下架任务发送成功", elapsed) + } + + countdownDelay(DelayKfzShelfAfterSend, "处理") + + // 步骤6:等待处理 + { + name := "6、等待任务处理" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + if err := waitBodyOverMin(tid, 2, WaitTimeout); err != nil { + errCase(cat, name, "等待超时: "+err.Error(), time.Since(start)) + return + } + pass(cat, name, "任务处理完成", time.Since(start)) + } + + countdownDelay(DelayKfzShelfAfterWait, "接口校验") + + // 步骤7:接口校验(下架) + { + name := "7、接口校验(下架)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + accessToken, err := getKfzAccessToken() + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + + status, err := getKfzGoodsStatus(kfzSuccessGoodsID, accessToken) + elapsed := time.Since(start) + if err != nil { + fail(cat, name, fmt.Sprintf("查询上下架状态失败: %v", err), elapsed) + return + } + fmt.Printf(" 📊 当前上下架状态: isOnSale=%d (0=下架)\n", status) + if status == 0 { + pass(cat, name, "商品已下架", elapsed) + } else { + fail(cat, name, fmt.Sprintf("商品未下架: isOnSale=%d", status), elapsed) + } + } +} + +// testKfzGoodsDelete 孔夫子删除商品 +func testKfzGoodsDelete() { + cat := "孔夫子删除商品" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + if kfzSuccessGoodsID == 0 { + fmt.Println("⚠️ 孔夫子发布任务未创建有效商品,跳过") + return + } + fmt.Printf("\n 📌 itemId: %d\n", kfzSuccessGoodsID) + + var tid string + // 步骤1:创建删除任务 + { + name := "1、创建孔夫子删除任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + params := map[string]string{ + "shop_id": ShopID, + "shop_type": ShopType, + "task_count": TaskCount, + "task_type": TaskTypePriceStockShelf, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + tid = dataStr + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + // 步骤2:发送删除任务(status=4 表示删除操作) + { + name := "2、发送删除任务(status=4)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + bodyJSON := fmt.Sprintf(`{"book_info":{"isbn":"%s"},"detail":{"itemId":%d,"status":4}}`, + kfzSuccessISBN, kfzSuccessGoodsID) + fmt.Printf(" 📋 body: %s\n", bodyJSON) + params := map[string]string{ + "task_id": tid, + "body": bodyJSON, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/setTaskBody", params) + elapsed := time.Since(start) + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + pass(cat, name, "删除任务发送成功", elapsed) + } + + countdownDelay(DelayKfzDeleteAfterSend, "Redis校验") + + // 步骤3:校验 body_over + { + name := "3、校验 body_over" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + fmt.Printf(" ⏳ 等待后台处理完成(最多 %v)...\n", WaitTimeout) + + if err := waitBodyOverMin(tid, 1, WaitTimeout); err != nil { + errCase(cat, name, "等待超时: "+err.Error(), time.Since(start)) + return + } + pass(cat, name, "body_over ≥ 1", time.Since(start)) + } + + countdownDelay(DelayKfzDeleteAfterAPI, "接口校验") + + // 步骤4:接口校验(调用 DLL 验证商品是否已删除) + { + name := "4、接口校验(商品已删除)" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + accessToken, err := getKfzAccessToken() + if err != nil { + fail(cat, name, fmt.Sprintf("获取 accessToken 失败: %v", err), time.Since(start)) + return + } + fmt.Printf(" 🔑 token: %s...%s\n", accessToken[:8], accessToken[len(accessToken)-8:]) + + // 调用 DLL 搜索该 ISBN,看是否还能找到 + _, err = findKfzGoodsByISBN(kfzSuccessISBN, accessToken) + elapsed := time.Since(start) + if err != nil && strings.Contains(err.Error(), "未找到") { + // 商品已被删除,找不到了 + pass(cat, name, fmt.Sprintf("商品已删除(ISBN=%s 无法在列表中找到)", kfzSuccessISBN), elapsed) + } else if err != nil { + fail(cat, name, fmt.Sprintf("查询商品失败: %v", err), elapsed) + } else { + // 还能找到,说明删除失败 + fail(cat, name, fmt.Sprintf("商品未删除,ISBN=%s 仍在列表中", kfzSuccessISBN), elapsed) + } + } +} + +// testKfzPullGoods 孔夫子商品拉取 +func testKfzPullGoods() { + cat := "孔夫子商品拉取" + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println(" " + cat) + fmt.Println(strings.Repeat("=", 60)) + + var tid string + + // 步骤1:创建拉取任务 + { + name := "1、创建孔夫子商品拉取任务" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + params := map[string]string{ + "shop_id": ShopID, + "shop_type": ShopType, + "task_count": TaskCount, + "task_type": TaskTypePullGoods, + "img_type": ImgType, + } + params["sign"] = SignParams(params) + resp, err := postMultipart(BaseURL+"/task/create", params) + elapsed := time.Since(start) + + if err != nil { + errCase(cat, name, err.Error(), elapsed) + return + } + if resp.Code != "200" { + fail(cat, name, fmt.Sprintf("code=%s msg=%s", resp.Code, resp.Msg), elapsed) + return + } + dataStr, _ := resp.Data.(string) + if dataStr == "" { + fail(cat, name, "返回 data 为空", elapsed) + return + } + tid = dataStr + pass(cat, name, fmt.Sprintf("task_id=%s", tid), elapsed) + } + + if tid == "" { + fmt.Println("⚠️ 拉取任务创建失败,跳过后续步骤") + return + } + fmt.Printf("\n 📌 孔夫子商品拉取 task_id: %s\n", tid) + + // 步骤2:等待任务完成(无时间限制,拉取数据量大) + { + name := "2、等待任务完成并校验" + start := time.Now() + fmt.Printf("\n[步骤] %s\n", name) + + // 等待 header status=4 + fmt.Printf(" ⏳ 等待任务完成(header status=4,无时间限制)...\n") + waitStart := time.Now() + for { + st, err := getHeaderStatus(tid) + if err == nil && st == 4 { + fmt.Printf(" ✅ 任务状态已变为 status=4(耗时 %v)\n", time.Since(waitStart).Round(time.Second)) + break + } + if int(time.Since(waitStart).Seconds()) > 0 && int(time.Since(waitStart).Seconds())%30 == 0 { + bodyOverCnt, _ := getBodyOverCount(tid) + fmt.Printf(" ⏳ 已等待 %v,status=%d,body_over=%d\n", + time.Since(waitStart).Round(time.Second), st, bodyOverCnt) + } + time.Sleep(PollInterval) + } + + // 校验 task_count_true 与 body_over 数量 + headerKey := tid + ":header" + taskCountTrueStr, err := redisClient.HGet(redisCtx, headerKey, "task_count_true").Result() + if err != nil { + fail(cat, name, fmt.Sprintf("获取 task_count_true 失败: %v", err), time.Since(start)) + return + } + taskCountTrue, _ := strconv.ParseInt(taskCountTrueStr, 10, 64) + bodyOverCount, err := getBodyOverCount(tid) + if err != nil { + fail(cat, name, fmt.Sprintf("获取 body_over 数量失败: %v", err), time.Since(start)) + return + } + + fmt.Printf(" 📊 task_count_true=%d, body_over=%d\n", taskCountTrue, bodyOverCount) + if bodyOverCount > taskCountTrue { + fail(cat, name, fmt.Sprintf("body_over(%d) > task_count_true(%d)", bodyOverCount, taskCountTrue), time.Since(start)) + } else { + pass(cat, name, fmt.Sprintf("body_over ≤ task_count_true"), time.Since(start)) + } + } +} + +// ============================================================ +// 主函数 +// ============================================================ + +func main() { + reportDir, _ = os.Getwd() + + // 加载配置 + configFile := filepath.Join(reportDir, configPath) + if err := loadConfig(configFile); err != nil { + fmt.Printf("🔴 加载配置文件失败: %v\n", err) + os.Exit(1) + } + + fmt.Println(strings.Repeat("#", 60)) + fmt.Println(" 🧪 PlanA API 批量测试") + fmt.Println(strings.Repeat("#", 60)) + fmt.Printf("接口地址 : %s\n", BaseURL) + fmt.Printf("ShopID : %s\n", ShopID) + fmt.Printf("拼多多应用ID : %s\n", PddAppID) + fmt.Printf("Redis : %s (DB=%d)\n\n", RedisAddr, RedisDB) + + // 预检 HTTP 服务 + fmt.Println("🔍 预检 HTTP 服务...") + resp, err := httpClient.Get(BaseURL + "/task/get?page=1&size=1") + if err != nil { + fmt.Printf("🔴 无法连接 %s: %v\n", BaseURL, err) + fmt.Printf("请确认 planA HTTP 服务已启动 (%s)\n", BaseURL) + os.Exit(1) + } + resp.Body.Close() + fmt.Println("✅ HTTP 服务正常\n") + + // 预检 Redis + fmt.Println("🔍 预检 Redis...") + if err := initRedis(); err != nil { + fmt.Printf("🔴 无法连接 Redis %s: %v\n", RedisAddr, err) + fmt.Println("请确认 Redis 服务已启动") + os.Exit(1) + } + fmt.Println("✅ Redis 连接正常\n") + + // 预检 curl + fmt.Println("🔍 预检 curl...") + if _, err := exec.LookPath("curl"); err != nil { + fmt.Println("🔴 curl 未安装,校验步骤需要 curl") + fmt.Println("请安装 curl: https://curl.se/download.html") + os.Exit(1) + } + fmt.Println("✅ curl 可用\n") + + // // 拼多多测试 + // testPddPricePublish() + // testPddPriceChange() + // testPddStockChange() + // testPddShelfOnOff() + // testPddGoodsDelete() + // testPddPullGoods() + + // 闲鱼测试 + testXyPricePublish() + testXyPriceChange() + testXyStockChange() + testXyShelfOff() + testXyShelfOn() + //testXyPullGoods() + + // 生成并保存报告 + fmt.Println(strings.Repeat("-", 60)) + fmt.Println("📊 生成测试报告...") + + report := generateReport() + reportFile := filepath.Join(reportDir, "test_report.md") + if err := os.WriteFile(reportFile, []byte(report), 0644); err != nil { + fmt.Printf("⚠️ 写入报告失败: %v\n", err) + } else { + abs, _ := filepath.Abs(reportFile) + fmt.Printf("✅ 报告已保存: %s\n", abs) + } + + // 汇总 + total := len(results) + p, f, e := 0, 0, 0 + for _, r := range results { + switch r.Status { + case "PASS": + p++ + case "FAIL": + f++ + default: + e++ + } + } + rate := 0.0 + if total > 0 { + rate = float64(p) / float64(total) * 100 + } + + fmt.Println(strings.Repeat("=", 60)) + fmt.Printf(" 📊 测试汇总: %d/%d 通过 (%.1f%%)\n", p, total, rate) + fmt.Printf(" ✅ PASS: %d | ❌ FAIL: %d | 🔴 ERROR: %d\n", p, f, e) + fmt.Println(strings.Repeat("=", 60)) + + if f > 0 || e > 0 { + os.Exit(1) + } +} diff --git a/test/modules/kfz/kfz.dll b/test/modules/kfz/kfz.dll new file mode 100644 index 0000000..e69de29 diff --git a/test/modules/kfz/kfz_types.go b/test/modules/kfz/kfz_types.go new file mode 100644 index 0000000..988b2fe --- /dev/null +++ b/test/modules/kfz/kfz_types.go @@ -0,0 +1,103 @@ +package kfz + +// GetGoodsListReq 获取商品列表请求结构体 +type GetGoodsListReq struct { + Type string `json:"type"` + PageNum int `json:"pageNum"` + PageSize int `json:"pageSize"` + AddTimeBegin string `json:"addTimeBegin"` + AddTimeEnd string `json:"addTimeEnd"` + SortOrder string `json:"sortOrder"` + SortType string `json:"sortType"` +} + +// GetGoodsListResp 获取商品列表响应结构体 +type GetGoodsListResp struct { + ErrorResponse interface{} `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse *GetGoodsListSuccessResp `json:"successResponse"` +} + +// GetGoodsListSuccessResp 成功响应详情 +type GetGoodsListSuccessResp struct { + List []KfzGoodsItem `json:"list"` + PageNum int `json:"pageNum"` + PageSize int `json:"pageSize"` + Pages int `json:"pages"` + Size int `json:"size"` + Total int `json:"total"` +} + +// KfzGoodsItem 孔夫子商品项 +type KfzGoodsItem struct { + ItemId int64 `json:"itemId"` + AddTime int64 `json:"addTime"` + ItemName string `json:"itemName"` + Price float64 `json:"price"` + Number int `json:"number"` + Quality int64 `json:"quality"` + QualityDesc string `json:"qualityDesc"` + ImgUrl string `json:"imgUrl"` + Images string `json:"images"` + CatId uint64 `json:"catId"` + MyCatId int `json:"myCatId"` + ItemSn string `json:"itemSn"` + BearShipping string `json:"bearShipping"` + MouldId int `json:"mouldId"` + Weight float64 `json:"weight"` + WeightPiece float64 `json:"weightPiece"` + ItemDesc string `json:"itemDesc"` + Isbn string `json:"isbn"` + Author string `json:"author"` + Press string `json:"press"` + PubDate string `json:"pubDate"` + OriPrice float64 `json:"oriPrice"` + Binding string `json:"binding"` + PageSize string `json:"pageSize"` + PageNum int `json:"pageNum"` + Tpl int `json:"tpl"` + ImportantDesc string `json:"importantDesc"` + BeginSaleTime uint64 `json:"beginSaleTime"` + EndSaleTime uint64 `json:"endSaleTime"` + BizType int `json:"bizType"` + BooklibId uint64 `json:"booklibId"` + CertifyStatus string `json:"certifyStatus"` + Discount int `json:"discount"` + IsDelete int `json:"isDelete"` + IsDraft int `json:"isDraft"` + IsNewBook int `json:"isNewBook"` + IsOnSale int `json:"isOnSale"` + ProductArea uint64 `json:"productArea"` + UpdateTime string `json:"updateTime"` + UserId uint64 `json:"userId"` + Years uint64 `json:"years"` +} + +// ProductRet 上架/下架/改价/改库存/删除通用返回结构体 +type ProductRet struct { + ErrorResponse interface{} `json:"errorResponse"` + RequestId string `json:"requestId"` + RequestMethod string `json:"requestMethod"` + SuccessResponse struct { + Item struct { + BeginSaleTime string `json:"beginSaleTime"` + CertifyStatus string `json:"certifyStatus"` + EndSaleTime string `json:"endSaleTime"` + IsOnSale string `json:"isOnSale"` + ItemId int `json:"itemId"` + UpdateTime string `json:"updateTime"` + Price string `json:"price"` + Number string `json:"number"` + } `json:"item"` + } `json:"successResponse"` +} + +// ErrorResponse 通用错误响应 +type ErrorResponse struct { + Code int `json:"code"` + Data interface{} `json:"data"` + Msg string `json:"msg"` + SubCode string `json:"subCode"` + SubMsg string `json:"subMsg"` +} diff --git a/test/modules/pdd/pdd.dll b/test/modules/pdd/pdd.dll new file mode 100644 index 0000000..e69de29 diff --git a/test/modules/pdd/pdd.md b/test/modules/pdd/pdd.md new file mode 100644 index 0000000..e69de29 diff --git a/test/modules/xianYu/address.xlsx b/test/modules/xianYu/address.xlsx new file mode 100644 index 0000000..7c91f53 Binary files /dev/null and b/test/modules/xianYu/address.xlsx differ diff --git a/test/modules/xianYu/config.ini b/test/modules/xianYu/config.ini new file mode 100644 index 0000000..6755855 --- /dev/null +++ b/test/modules/xianYu/config.ini @@ -0,0 +1,25 @@ +[app] +AppId = 1228288260261189 +AppSecret = aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k +Domain = https://open.goofish.pro +[http] +Addr = 127.0.0.1:53368 +[categoryListRequest] +Path = /api/open/product/category/list +ItemBizType: 2 +SpBizType: 24 +[batchCreatRequest] +Path = /api/open/product/batchCreate +[file] +TxtPath = modules/xianYu/productCategory.txt +ExcelPath = modules/xianYu/address.xlsx +SheetName = Result +[redis] +Password = Long6166@@ +Addr = 127.0.0.1:6379 +Db = 5 +[tokenBucket] +BucketKeyPrefix = "token_bucket_" +TokenPerSecond = 10 +BucketSize = 100 +Delay = 100 diff --git a/test/modules/xianYu/productCategory.txt b/test/modules/xianYu/productCategory.txt new file mode 100644 index 0000000..a0a9fbc --- /dev/null +++ b/test/modules/xianYu/productCategory.txt @@ -0,0 +1,284 @@ +cbf4e2ec8f2013d31921b9e373cead75:电视剧 +cbf4e2ec8f2013d3267e0a01017d9f44:电影 +cbf4e2ec8f2013d36f38848189966e7d:生活 +cbf4e2ec8f2013d3ac899d2620c5df2b:成人教育音像 +cbf4e2ec8f2013d3acde29f76907b07f:动画 +cbf4e2ec8f2013d3e10cfa39bf43dc0f:儿童教育音像 +d14d229692616168b108d382c4e6ea42:废品回收 +d816d18aa66dfb3d1921b9e373cead75:励志成长 +dbaba36adf47af96b108d382c4e6ea42:不干胶标签 +e59460ef9961e2bd28d88a08a19453dc:古典吉他 +e59460ef9961e2bda7f7e02f36b0b49a:电箱吉他 +86cddebb2de0815c1921b9e373cead75:桌面文件柜 +86cddebb2de0815c267e0a01017d9f44:资料册 +86cddebb2de0815c6f38848189966e7d:镇纸 +86cddebb2de0815ca7f7e02f36b0b49a:文件袋 +86cddebb2de0815cacde29f76907b07f:文房墨汁 +86cddebb2de0815ce10cfa39bf43dc0f:文房四宝套装 +879b743300e7a58137b3d33c282f2081:古筝 +8bd8d9724880b84d28d88a08a19453dc:学习笔记 +a457d6fc43c609bdac899d2620c5df2b:单据收据 +a457d6fc43c609bdacde29f76907b07f:印台 +a9ef3505c7fe4b661921b9e373cead75:勾线笔 +a9ef3505c7fe4b66a7f7e02f36b0b49a:电子阅览器/电纸书 +ab78823bfd3c7134b108d382c4e6ea42:经济管理 +ac69f9982deabde1acde29f76907b07f:民谣吉他 +ac69f9982deabde1e10cfa39bf43dc0f:架子鼓 +b12c1c13a8dc3b2b6f38848189966e7d:POP广告纸 +b12c1c13a8dc3b2ba7f7e02f36b0b49a:修正贴 +b12c1c13a8dc3b2bac899d2620c5df2b:学生用印 +b12c1c13a8dc3b2bacde29f76907b07f:名片 +b2b61c32fc4c904428d88a08a19453dc:背胶证件照 +b3b713b29220947237b3d33c282f2081:台历 +4c49139fe1b6ae4aac899d2620c5df2b:童书育儿 +4fecb084c468ed626f38848189966e7d:黑板 +5042edcbd2cc4b94ac899d2620c5df2b:生活百科 +621bd460d751e0fc37b3d33c282f2081:订书机 +701ed8603d74ee60b108d382c4e6ea42:报纸 +722d38201b9c8cba267e0a01017d9f44:社科心理 +7912befd7e1215d11921b9e373cead75:挂历 +7dba397e41d08d4937b3d33c282f2081:拆信刀 +7eb776b01814cc6e1921b9e373cead75:教材教辅 +22e1d81dc4cf3a25a7f7e02f36b0b49a:图书 +2dfa3034d88aedcc1921b9e373cead75:期刊/杂志 +31329c43789fae0437b3d33c282f2081:戏曲综艺 +31329c43789fae04a7f7e02f36b0b49a:音乐唱片/专辑 +322a73805c38995f6f38848189966e7d:宝珠笔 +3cdbae6d47df9251a7f7e02f36b0b49a:电子资料 +22d3cfff678abab1e10cfa39bf43dc0f:握笔器 +b7fd03d456abe3011921b9e373cead75:活页替芯 +b7fd03d456abe301b108d382c4e6ea42:索引纸 +b7fd03d456abe301e10cfa39bf43dc0f:拍纸本 +c230ba4ca293f3b528d88a08a19453dc:马克笔 +c230ba4ca293f3b5a7f7e02f36b0b49a:钢笔 +c230ba4ca293f3b5ac899d2620c5df2b:铅笔 +c3c6e8d1d63c0618b108d382c4e6ea42:文学/小说 +c58d3dbcff05e404acde29f76907b07f:笔筒 +eac1d67ece5fa9b16f38848189966e7d:钢琴 +ee8603696d446e931921b9e373cead75:电钢琴 +06d80b131d7b0b616f38848189966e7d:毛笔 +0e28c0f1f1e57eb1ac899d2620c5df2b:地图 +0f75076039b85f74267e0a01017d9f44:计算器 +0f75076039b85f7428d88a08a19453dc:尺 +0f75076039b85f746f38848189966e7d:板擦 +0f75076039b85f74b108d382c4e6ea42:算盘 +11c38799bd389b3828d88a08a19453dc:漫画书籍 +ac69f9982deabde1a7f7e02f36b0b49a:上弦器 +83f9286d1ea41056ac899d2620c5df2b:其他吉他配件 +e59460ef9961e2bd1921b9e373cead75:变调夹 +83f9286d1ea4105637b3d33c282f2081:古典吉他弦 +e59460ef9961e2bdacde29f76907b07f:吉他单块效果器 +83f9286d1ea41056267e0a01017d9f44:吉他效果器配件 +83f9286d1ea41056b108d382c4e6ea42:吉他电源 +ac69f9982deabde1267e0a01017d9f44:吉他综合效果器 +e59460ef9961e2bdb108d382c4e6ea42:吉他背包琴盒 +83f9286d1ea4105628d88a08a19453dc:吉他背带 +83f9286d1ea410561921b9e373cead75:吉他连接线 +e59460ef9961e2bd6f38848189966e7d:吊架 +ac69f9982deabde1ac899d2620c5df2b:弦枕 +ac69f9982deabde11921b9e373cead75:弦柱 +e59460ef9961e2bd267e0a01017d9f44:拨片 +ac69f9982deabde128d88a08a19453dc:拾音器 +83f9286d1ea41056e10cfa39bf43dc0f:曼陀铃弦 +83f9286d1ea410566f38848189966e7d:民谣吉他弦 +ac69f9982deabde137b3d33c282f2081:清洁保护品 +83f9286d1ea41056a7f7e02f36b0b49a:滑棒指套 +e59460ef9961e2bde10cfa39bf43dc0f:电吉他弦 +e59460ef9961e2bdac899d2620c5df2b:背带钮 +83f9286d1ea41056acde29f76907b07f:脚凳 +ac69f9982deabde1b108d382c4e6ea42:调音器 +e59460ef9961e2bd37b3d33c282f2081:电吉他 +c6d5c9e68467b108ac899d2620c5df2b:哑鼓垫 +c6d5c9e68467b10837b3d33c282f2081:镲片 +c6d5c9e68467b10828d88a08a19453dc:鼓凳 +ac69f9982deabde16f38848189966e7d:鼓刷 +c6d5c9e68467b108b108d382c4e6ea42:鼓架镲架 +c6d5c9e68467b108a7f7e02f36b0b49a:鼓棒鼓锤 +1cac27c660d7b098b108d382c4e6ea42:唢呐 +1cac27c660d7b098267e0a01017d9f44:埙 +f22578f0c6a8eaa5267e0a01017d9f44:尺八 +f22578f0c6a8eaa51921b9e373cead75:巴乌 +1cac27c660d7b09828d88a08a19453dc:笙 +f22578f0c6a8eaa5acde29f76907b07f:笛子 +f22578f0c6a8eaa5e10cfa39bf43dc0f:管子 +1cac27c660d7b0981921b9e373cead75:箫 +1cac27c660d7b098a7f7e02f36b0b49a:芦笙 +1cac27c660d7b09837b3d33c282f2081:葫芦丝 +f22578f0c6a8eaa56f38848189966e7d:葫芦笙 +1cac27c660d7b098ac899d2620c5df2b:陶笛 +879b743300e7a581acde29f76907b07f:三弦 +1cac27c660d7b098e10cfa39bf43dc0f:冬不拉 +1cac27c660d7b0986f38848189966e7d:古琴 +1cac27c660d7b098acde29f76907b07f:弹布尔 +879b743300e7a581e10cfa39bf43dc0f:扬琴 +879b743300e7a5816f38848189966e7d:月琴 +879b743300e7a58128d88a08a19453dc:柳琴 +879b743300e7a5811921b9e373cead75:热瓦普 +879b743300e7a581b108d382c4e6ea42:琵琶 +879b743300e7a581ac899d2620c5df2b:秦琴 +879b743300e7a581a7f7e02f36b0b49a:箜篌 +879b743300e7a581267e0a01017d9f44:阮 +a2eba09f5b889a7c28d88a08a19453dc:中胡 +7d61e938542f6790b108d382c4e6ea42:二胡 +7d61e938542f6790267e0a01017d9f44:京二胡 +7d61e938542f6790acde29f76907b07f:京胡 +7d61e938542f679028d88a08a19453dc:低音胡 +a2eba09f5b889a7c37b3d33c282f2081:四胡 +a2eba09f5b889a7cb108d382c4e6ea42:坠琴 +7d61e938542f6790a7f7e02f36b0b49a:板胡 +a2eba09f5b889a7ca7f7e02f36b0b49a:椰胡 +7d61e938542f679037b3d33c282f2081:艾捷克 +7d61e938542f67901921b9e373cead75:革胡 +7d61e938542f67906f38848189966e7d:马头琴 +7d61e938542f6790e10cfa39bf43dc0f:马骨胡 +7d61e938542f6790ac899d2620c5df2b:高胡 +882b39ff0db2dd0037b3d33c282f2081:军镲 +00a32e7ff35aaf9e267e0a01017d9f44:大钹 +00a32e7ff35aaf9ee10cfa39bf43dc0f:大铙 +00a32e7ff35aaf9eacde29f76907b07f:大顶钹 +00a32e7ff35aaf9e1921b9e373cead75:川钹 +00a32e7ff35aaf9e6f38848189966e7d:广钹 +882b39ff0db2dd00a7f7e02f36b0b49a:快板 +882b39ff0db2dd0028d88a08a19453dc:拍板 +00a32e7ff35aaf9eb108d382c4e6ea42:梆子 +882b39ff0db2dd001921b9e373cead75:水镲 +882b39ff0db2dd00b108d382c4e6ea42:碰钟 +882b39ff0db2dd00acde29f76907b07f:秧歌镲 +882b39ff0db2dd00e10cfa39bf43dc0f:腰鼓镲 +882b39ff0db2dd00ac899d2620c5df2b:萨巴依 +882b39ff0db2dd00267e0a01017d9f44:铜书板 +00a32e7ff35aaf9eac899d2620c5df2b:镲锅 +0ea61a801ba323c1267e0a01017d9f44:堂鼓 +00a32e7ff35aaf9e28d88a08a19453dc:战鼓 +0ea61a801ba323c11921b9e373cead75:排鼓 +0ea61a801ba323c1b108d382c4e6ea42:板鼓 +00a32e7ff35aaf9e37b3d33c282f2081:秧歌鼓 +0ea61a801ba323c1e10cfa39bf43dc0f:细腰鼓 +00a32e7ff35aaf9ea7f7e02f36b0b49a:腰鼓 +0ea61a801ba323c1ac899d2620c5df2b:花盆鼓 +0ea61a801ba323c16f38848189966e7d:象脚鼓 +0ea61a801ba323c1acde29f76907b07f:铜鼓 +a7133eb411b587cf1921b9e373cead75:空灵鼓/无忧鼓 +0ea61a801ba323c1a7f7e02f36b0b49a:云锣 +a2eba09f5b889a7c267e0a01017d9f44:京锣 +a2eba09f5b889a7cac899d2620c5df2b:低音锣 +a2eba09f5b889a7cacde29f76907b07f:开道锣 +a2eba09f5b889a7ce10cfa39bf43dc0f:手锣 +0ea61a801ba323c137b3d33c282f2081:武锣 +0ea61a801ba323c128d88a08a19453dc:舟山锣 +a2eba09f5b889a7c6f38848189966e7d:苏锣 +a2eba09f5b889a7c1921b9e373cead75:虎音锣 +33a0daa5d89d68fa1921b9e373cead75:宣纸 +b12c1c13a8dc3b2b1921b9e373cead75:吊牌 +b12c1c13a8dc3b2be10cfa39bf43dc0f:自封袋 +b12c1c13a8dc3b2b267e0a01017d9f44:贺卡明信片 +22d3cfff678abab11921b9e373cead75:书皮 +b12c1c13a8dc3b2b37b3d33c282f2081:修正带 +b12c1c13a8dc3b2b28d88a08a19453dc:修正液 +22d3cfff678abab1a7f7e02f36b0b49a:削笔器 +22d3cfff678abab128d88a08a19453dc:可爱印泥 +b12c1c13a8dc3b2bb108d382c4e6ea42:学生书包 +22d3cfff678abab1acde29f76907b07f:文具套装 +22d3cfff678abab1267e0a01017d9f44:文具盒 +22d3cfff678abab16f38848189966e7d:橡皮 +22d3cfff678abab1b108d382c4e6ea42:练字帖 +22d3cfff678abab1ac899d2620c5df2b:视力保护器 +dbaba36adf47af9637b3d33c282f2081:笔袋 +54e552aa1c9b2cbcacde29f76907b07f:彩泥橡皮泥 +bf164bd2e8dd8cebb108d382c4e6ea42:便条照片夹 +bf164bd2e8dd8ceb28d88a08a19453dc:便签盒座 +bf164bd2e8dd8cebe10cfa39bf43dc0f:卡套证件套 +bf164bd2e8dd8ceb6f38848189966e7d:名片册 +86cddebb2de0815c37b3d33c282f2081:名片盒 +bf164bd2e8dd8cebacde29f76907b07f:快劳夹 +86cddebb2de0815c28d88a08a19453dc:文件夹 +86cddebb2de0815cb108d382c4e6ea42:文件架 +bf164bd2e8dd8ceb1921b9e373cead75:档案盒 +bf164bd2e8dd8cebac899d2620c5df2b:档案袋 +86cddebb2de0815cac899d2620c5df2b:相册 +1ad9ac4511bbb8646f38848189966e7d:笔插 +bf164bd2e8dd8ceba7f7e02f36b0b49a:笔架 +bf164bd2e8dd8ceb267e0a01017d9f44:风琴包 +d665d5e1347fa192a7f7e02f36b0b49a:地球仪 +bf164bd2e8dd8ceb37b3d33c282f2081:展板 +d665d5e1347fa192267e0a01017d9f44:教学仪器器材 +d665d5e1347fa1921921b9e373cead75:教鞭 +bb9bba251ee78e59267e0a01017d9f44:旗帜 +d665d5e1347fa19237b3d33c282f2081:提示牌 +d665d5e1347fa192b108d382c4e6ea42:激光笔 +0f75076039b85f74acde29f76907b07f:白板 +0f75076039b85f74e10cfa39bf43dc0f:白板笔 +d665d5e1347fa19228d88a08a19453dc:粉笔 +d665d5e1347fa192acde29f76907b07f:绿板 +d665d5e1347fa1926f38848189966e7d:荧光板 +d665d5e1347fa192ac899d2620c5df2b:计划表 +d665d5e1347fa192e10cfa39bf43dc0f:软木板 +a457d6fc43c609bda7f7e02f36b0b49a:中性笔 +c230ba4ca293f3b5e10cfa39bf43dc0f:圆珠笔 +c230ba4ca293f3b51921b9e373cead75:铅芯 +f9910185f1984f2937b3d33c282f2081:正姿笔 +c230ba4ca293f3b5acde29f76907b07f:油漆笔 +c230ba4ca293f3b5b108d382c4e6ea42:泡泡笔 +c230ba4ca293f3b537b3d33c282f2081:墨水墨囊 +c230ba4ca293f3b5267e0a01017d9f44:荧光笔 +f4a071d4dba28eccac899d2620c5df2b:记号笔 +c230ba4ca293f3b56f38848189966e7d:针管笔 +dfdbd3409fadcd3f6f38848189966e7d:其他笔 +58e84885c426409e267e0a01017d9f44:书签 +b7fd03d456abe30128d88a08a19453dc:便签 +e9fa1ad466b79d97b108d382c4e6ea42:信封 +af2cf5b1faa3537a1921b9e373cead75:信纸 +b7fd03d456abe30137b3d33c282f2081:包装纸 +e9fa1ad466b79d97a7f7e02f36b0b49a:纪念册 +b7fd03d456abe301ac899d2620c5df2b:复写纸 +b7fd03d456abe301267e0a01017d9f44:奖状证书 +e9fa1ad466b79d971921b9e373cead75:手工纸 +e9fa1ad466b79d9728d88a08a19453dc:草稿纸 +b7fd03d456abe3016f38848189966e7d:日记本 +e9fa1ad466b79d97ac899d2620c5df2b:硬面抄 +b7fd03d456abe301a7f7e02f36b0b49a:记事本 +b7fd03d456abe301acde29f76907b07f:课业本 +e9fa1ad466b79d9737b3d33c282f2081:通讯录 +dbaba36adf47af966f38848189966e7d:磁性贴 +6c0543ec11db7e61267e0a01017d9f44:贴纸/标签 +0f75076039b85f741921b9e373cead75:圆规 +0f75076039b85f74ac899d2620c5df2b:显微镜 +1c75d8021bacf61e267e0a01017d9f44:放大镜 +a9ef3505c7fe4b66b108d382c4e6ea42:丙烯颜料 +823f8d7bd96780d0ac899d2620c5df2b:书法用纸 +a9ef3505c7fe4b66ac899d2620c5df2b:儿童填色本 +a9ef3505c7fe4b66267e0a01017d9f44:国画颜料 +823f8d7bd96780d037b3d33c282f2081:描图硫酸纸 +5fd3299edc3ff44a37b3d33c282f2081:毛边纸 +823f8d7bd96780d01921b9e373cead75:水彩笔 +823f8d7bd96780d0267e0a01017d9f44:水彩颜料 +823f8d7bd96780d0acde29f76907b07f:水粉水彩油画笔 +823f8d7bd96780d0e10cfa39bf43dc0f:水粉颜料 +0f75076039b85f7437b3d33c282f2081:油画棒 +0f75076039b85f74a7f7e02f36b0b49a:油画颜料 +a9ef3505c7fe4b66acde29f76907b07f:画板画架 +823f8d7bd96780d0b108d382c4e6ea42:石膏像 +823f8d7bd96780d06f38848189966e7d:素描本 +a9ef3505c7fe4b66e10cfa39bf43dc0f:绘图纸 +823f8d7bd96780d028d88a08a19453dc:色卡 +a9ef3505c7fe4b666f38848189966e7d:蜡笔 +823f8d7bd96780d0a7f7e02f36b0b49a:铅画纸 +7dba397e41d08d49a7f7e02f36b0b49a:裁剪刀片 +7dba397e41d08d49b108d382c4e6ea42:雕刻垫板 +7dba397e41d08d49ac899d2620c5df2b:切纸刀 +7dba397e41d08d4928d88a08a19453dc:美工刀 +356e5d8126d3aefaa7f7e02f36b0b49a:裁剪剪刀 +e9fa1ad466b79d97267e0a01017d9f44:回形针 +621bd460d751e0fca7f7e02f36b0b49a:回形针盒 +621bd460d751e0fcb108d382c4e6ea42:图钉工字钉 +e9fa1ad466b79d97e10cfa39bf43dc0f:大头针 +e9fa1ad466b79d97acde29f76907b07f:打孔机 +621bd460d751e0fc28d88a08a19453dc:票夹长尾夹 +e9fa1ad466b79d976f38848189966e7d:订书钉 +a457d6fc43c609bd1921b9e373cead75:凭证 +a457d6fc43c609bde10cfa39bf43dc0f:印油印泥 +a457d6fc43c609bd28d88a08a19453dc:报表 +a457d6fc43c609bd267e0a01017d9f44:湿手器 +a457d6fc43c609bdb108d382c4e6ea42:财务证明用品 +a457d6fc43c609bd6f38848189966e7d:账本账册 +740736cf215b7509a7f7e02f36b0b49a:电子壁纸 \ No newline at end of file diff --git a/test/modules/xianYu/xianYu.go b/test/modules/xianYu/xianYu.go new file mode 100644 index 0000000..4718979 --- /dev/null +++ b/test/modules/xianYu/xianYu.go @@ -0,0 +1,179 @@ +package xianYu + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +var ( + gXianYuDll *XianYuDLL +) + +// XianYuDLL 闲鱼工具DLL结构 +type XianYuDLL struct { + Dll *syscall.DLL + freeCString *syscall.Proc // 释放C字符串 +} + +// InitXianYuDll 初始化 XianYuDLL +func InitXianYuDll(url string) (*XianYuDLL, error) { + if gXianYuDll != nil { + return gXianYuDll, nil + } + dllPath := filepath.Join(url, "xy.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("XianYu DLL 不存在: %s", dllPath) + } + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载XianYu DLL 失败: %s", err) + } + gXianYuDll = &XianYuDLL{ + Dll: dll, + freeCString: dll.MustFindProc("FreeCString"), + } + return gXianYuDll, nil +} + +// XianYuGoodsAdd 商品新增 + +func (m *XianYuDLL) XianYuGoodsAdd(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsCreat") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsCreat: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuLaunchGoods 商品上架 +func (m *XianYuDLL) XianYuLaunchGoods(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsPublish") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsPublish: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuGetGoodsList 拉取商品列表 +func (m *XianYuDLL) XianYuGetGoodsList(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteSelectGoodsListPrice") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteSelectGoodsListPrice: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuGetGoodsDetail 拉取商品详情 +func (m *XianYuDLL) XianYuGetGoodsDetail(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGetGoodsDetail") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGetGoodsDetail: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuExecuteGoodsDownShelf 下架商品 +func (m *XianYuDLL) XianYuExecuteGoodsDownShelf(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsDownShelf") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsDownShelf: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuExecuteGoodsUpdateStock 修改库存 +func (m *XianYuDLL) XianYuExecuteGoodsUpdateStock(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsEditStock") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsEditStock: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// XianYuExecuteGoodsUpdatePrice 修改价格 +func (m *XianYuDLL) XianYuExecuteGoodsUpdatePrice(bodyJson string, configFile string) (string, error) { + proc, err := m.Dll.FindProc("ExecuteGoodsEditPrice") + if err != nil { + return "", fmt.Errorf("找不到函数 ExecuteGoodsEditPrice: %v", err) + } + bodyJsonPtr, _ := syscall.BytePtrFromString(bodyJson) + configFile = configFile + "\\config.ini" + configFilePtr, _ := syscall.BytePtrFromString(configFile) + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(bodyJsonPtr)), + uintptr(unsafe.Pointer(configFilePtr)), + ) + result := cStr(resultPtr) + return result, nil +} + +// cStr 将 C 字符串指针转换为 Go 字符串 +func cStr(ptr uintptr) string { + if ptr == 0 { + return "" + } + var b []byte + for { + c := *(*byte)(unsafe.Pointer(ptr)) + if c == 0 { + break + } + b = append(b, c) + ptr++ + } + return string(b) +} diff --git a/test/modules/xianYu/xy.dll b/test/modules/xianYu/xy.dll new file mode 100644 index 0000000..e1f3d8d Binary files /dev/null and b/test/modules/xianYu/xy.dll differ diff --git a/test/modules/xianYu/xy1.dll b/test/modules/xianYu/xy1.dll new file mode 100644 index 0000000..af6630d Binary files /dev/null and b/test/modules/xianYu/xy1.dll differ diff --git a/test/modules/xianYu/咸鱼发布dll.md b/test/modules/xianYu/咸鱼发布dll.md new file mode 100644 index 0000000..000ac8c --- /dev/null +++ b/test/modules/xianYu/咸鱼发布dll.md @@ -0,0 +1,239 @@ +##### FreeCString(str *C.char) + +接收其他函数返回值之后,释放内存,参考示例 + +##### 内存释放示例 + +```go +func example () { + // ...其他逻辑 + var res = StartServer (configFile *C.char) + FreeCString(res) //释放内存 +} +``` + + + +##### StartServer (configFile *C.char) + +启动http服务器,参数配置文件路径,不提供默认使用工程根目录config.ini + +返回C字符串启动消息,接收后使用FreeCString进行内存释放 + + + +##### StopServer + +停止HTTP服务器 + +返回C字符串停止消息,接收后使用FreeCString进行内存释放 + + + +##### GetServerStatus + +获取服务器当前状态 + +返回C字符串指针消息,running/stopped,接收后使用FreeCString进行内存释放 + + + +##### GetServerAddress + +获取服务器监听地址 + +返回C字符串指针服务器地址消息,未运行返回空串,接收后使用FreeCString进行内存释放 + + + +##### ReloadConfig(configFile *C.char) + +重新加载配置文件,参数配置文件路径,不提供默认使用根目录config.ini + +返回C字符串加载结果消息,接收后使用FreeCString进行内存释放 + + + + + +### 以下都需要传递appid和appSecret ### + +##### ExecuteGoodsCreat(bodyJson *C.char, configFile *C.char) + +*管道通信直接调用此函数* + +执行商品创建操作,参数商品信息,参考示例 + +返回C字符串指针创建商品结果信息,接收后使用FreeCString进行内存释放 + + + +##### 商品信息参考示例 + +```json +{ + "appId": 1228288260261189, + "appSecret": "aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k", + "token": "", + "apiShopId": 0, + "typePlatform": 4, + "shopId": 0, + "shopToken": "", + "shopName": "", + "province": 210000, + "city": 210100, + "district": 210101, + "typeClass": "", + "typeGoods": "", + "catIds": "d14d229692616168b108d382c4e6ea42", + "shop": [ + { + "userName": "xy938400231518", + "province": 210000, + "city": 210100, + "district": 210101, + "title": "牧羊少年奇幻之旅", + "content": "牧羊少年奇幻之旅", + "mainImgs": ["https://img.cdn1.vip/i/68cf5cb4e5840_1758420148.webp"], + "contentImgs": [] + } + ], + "stuffStatus": 90, + "bookData": [ + { + "ISBN": "9787530217054", + "Title": "牧羊少年奇幻之旅", + "Author": "保罗·柯艾略", + "Publisher": "北京十月文艺出版", + "itemBizType": 2, + "spBizType": 24, + "prices": [199999, 299999], + "stock": 100, + "catIds": "22e1d81dc4cf3a25a7f7e02f36b0b49a" + } + ], + "itemKey": "itemAAAAA1111" +} +``` + + + +##### ExecuteGoodsPublish(bodyJson *C.char, configFile *C.char) + +*管道通信直接调用此函数* + +执行商品上架操作,参数上架信息,参考示例 + +返回C字符串指针行商品上架结果信息,接收后使用FreeCString进行内存释放 + +##### 上架信息参考示例 + +```json +{ + "product_id": 1250927879325125, + "user_name": ["xy938400231518"], + "specify_publish_time": "", + "notify_url": "" +} +``` + + + +#### 追加下架,改价,擦亮 #### + +##### ExecuteGoodsDownShelf(bodyJson *C.char, configFile *C.char) ###### + +*管道通信直接调用此函数* + +执行商品下架操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品下架结果信息,接收后使用FreeCString进行内存释放 + +##### 下架信息参考示例 ##### + +```json +{ + "product_id": 1250927879325125 +} +``` + + + +##### ExecuteGoodsFlash(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品擦亮操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品擦亮结果信息,接收后使用FreeCString进行内存释放 + +##### 擦亮信息参考示例 ##### + +```json +{ + "product_id": 1250927879325125 +} +``` + + + +##### ExecuteGoodsEditPrice(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品改价操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 改价信息参考示例(单位:分) ##### + +```json +{ + "product_id": 1250927879325125, + "price": 550000, + "originalPrice": 770000 +} +``` + + + +##### ExecuteGoodsEditStock(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +执行商品改库存操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 改库存信息参考示例(单位:分) ##### + +```json +{ + "product_id": 1250927879325125, + "stock": 10 +} +``` + + + +##### ExecuteSelectGoodsListPrice(bodyJson *C.char, configFile *C.char) ##### + +*管道通信直接调用此函数* + +查询店铺列表操作,参数管家商品ID,参考示例 + +返回C字符串指针行商品改价结果信息,接收后使用FreeCString进行内存释放 + +##### 查询参考示例(单位:分) ##### + +```json +{ + //online_time 字段可传空 + "online_time": [ + 1690300800, + 1690366883 + ], + "product_status": 22 +} +``` + diff --git a/test/plantest.exe b/test/plantest.exe new file mode 100644 index 0000000..4b44537 Binary files /dev/null and b/test/plantest.exe differ diff --git a/test/test.yaml b/test/test.yaml new file mode 100644 index 0000000..027f0f5 --- /dev/null +++ b/test/test.yaml @@ -0,0 +1,234 @@ +# PlanA API 批量测试配置文件 +# 所有配置从此文件读取,main.go 中不再硬编码 + +# ============================================================ +# 基础服务 +# ============================================================ +base_url: "http://127.0.0.1:8080" # planA 接口地址 + +# ============================================================ +# Redis 配置 +# ============================================================ +redis: + addr: "127.0.0.1:6379" + db: 0 # 拼多多使用 DB 0 + password: "123456" + +# ============================================================ +# 拼多多配置 +# ============================================================ +pdd: + shop_id: "2031193954362281985" + shop_type: "1" + app_id: "203c5a7ba8bd4b8488d5e26f93052642" + app_key: "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" + verify_url: "http://pdd.buzhiyushu.cn/api/pdd/auth/newGetShopGoodsDetailOne" + verify_basic_auth: "ZWxhc3RpYzo1bVJESVVnNTJWQzBmcDE0bnctRg==" + +# ============================================================ +# 闲鱼配置 +# ============================================================ +xianyu: + shop_id: "1995773417159127041" + shop_type: "5" + app_id: 1228288260261189 + app_secret: "aq9gAwrwp6WGZkMRqKIXmnu2c2uCm82k" + domain: "https://open.goofish.pro" + dll_path: "modules/xianYu" # DLL 模块路径 + +# ============================================================ +# 孔夫子配置 +# ============================================================ +kfz: + shop_id: "576" + shop_type: "4" # 孔夫子 shop_type=4 + app_id: 576 + app_secret: "256e10220c5b307f5172b1a49c11467a6cfa8038bbe2a7feccc42231852324f8" + dll_path: "modules/kfz" # DLL 模块路径 + +# ============================================================ +# 超时与等待配置 +# ============================================================ +timeout: + wait_timeout: 180 # 等待后台处理超时(秒) + poll_interval: 3 # 轮询间隔(秒) + http_client_timeout: 60 # HTTP 客户端超时(秒) + curl_timeout: 30 # curl 单次请求超时(秒) + curl_retry_interval: 5 # curl 重试间隔(秒) + +# ============================================================ +# 场景延迟配置(操作后等待平台同步的时间,单位:秒) +# ============================================================ +delays: + # 场景一:拼多多核价发布 + pdd_price_publish: + after_send: 180 # 步骤3b发送后 → 步骤4校验前 + + # 场景二:拼多多改价格 + pdd_price_change: + after_create_query_detail: 20 # 创建任务后查询商品详情前 + after_send_redis_check: 10 # 步骤3发送后 → 步骤4 Redis校验前 + after_send_api_check: 180 # 步骤4后 → 步骤5平台接口校验前 + + # 场景三:拼多多改库存 + pdd_stock_change: + after_send_redis_check: 10 # 步骤1发送后 → 步骤2 Redis校验前 + after_send_api_check: 10 # 步骤2后 → 步骤3平台接口校验前 + + # 场景四:拼多多上下架 + pdd_shelf_on_off: + after_send_redis_check: 10 # 步骤1发送后 → 等待处理前 + after_wait_api_check: 10 # 等待处理后 → 步骤2平台接口校验前 + wait_body_over_timeout: 120 # 等待 body_over 最少条数的超时(秒) + + # 场景四(补充):拼多多删除商品 + pdd_goods_delete: + after_send_redis_check: 10 # 步骤1发送后 → 步骤2 Redis校验前 + after_send_api_check: 10 # 步骤2后 → 步骤3平台接口校验前 + + # 场景七:闲鱼核价发布 + xy_price_publish: + after_send: 80 # 步骤2发送后 → 步骤3校验前(1分20秒) + + # 场景九:闲鱼改价格 + xy_price_change: + after_create_query_detail: 20 # 创建任务后查询商品详情前 + after_send_redis_check: 10 # 步骤3发送后 → 步骤4 Redis校验前 + after_send_api_check: 10 # 步骤4后 → 步骤5平台接口校验前 + + # 场景十:闲鱼改库存 + xy_stock_change: + after_send_redis_check: 10 # 步骤1发送后 → 步骤2 Redis校验前 + after_send_api_check: 10 # 步骤2后 → 步骤3平台接口校验前 + + # 场景十一:闲鱼上下架 + xy_shelf_on_off: + after_send_redis_check: 10 # 步骤1发送后 → 等待处理前 + after_wait_api_check: 10 # 等待处理后 → 步骤2平台接口校验前 + wait_body_over_timeout: 120 # 等待 body_over 最少条数的超时(秒) + + # 孔夫子核价发布 + kfz_price_publish: + after_send: 10 # 步骤2发送后 → 步骤3校验前 + + # 孔夫子改价格 + kfz_price_change: + after_create_query_detail: 20 # 创建任务后查询商品详情前 + after_send_redis_check: 10 # 步骤3发送后 → 步骤4 Redis校验前 + after_send_api_check: 10 # 步骤4后 → 步骤5平台接口校验前 + + # 孔夫子改库存 + kfz_stock_change: + after_send_redis_check: 10 # 步骤1发送后 → 步骤2 Redis校验前 + after_send_api_check: 10 # 步骤2后 → 步骤3平台接口校验前 + + # 孔夫子上下架 + kfz_shelf_on_off: + after_send_redis_check: 10 # 步骤1发送后 → 等待处理前 + after_wait_api_check: 10 # 等待处理后 → 步骤2平台接口校验前 + wait_body_over_timeout: 120 # 等待 body_over 最少条数的超时(秒) + + + # 孔夫子删除商品 + kfz_goods_delete: + after_send_redis_check: 10 # 步骤1发送后 → 步骤2 Redis校验前 + after_send_api_check: 10 # 步骤2后 → 步骤3平台接口校验前 + +# ============================================================ +# 测试数据配置 +# ============================================================ +test_data: + # 拼多多核价发布测试数据(场景一) + pdd_price_publish: + isbn_success: "9787115600387" # 期望:执行成功 + price_success: 1900 # 价格(分) + isbn_price_zero: "9787223022231" # 期望:价格不能小于等于0 + price_zero: 0 + isbn_banned_word: "9787530216965" # 期望:违规词命中 + price_banned: 1900 + + # 拼多多改价格测试数据(场景二) + pdd_price_change: + new_price: 5000 # 改价格目标价(分)= 50元 + + # 拼多多改库存测试数据(场景三) + pdd_stock_change: + new_stock: 2 # 改库存目标数量 + + # 闲鱼核价发布测试数据(场景七) + xy_price_publish: + isbn_success: "9787115600387" + price_success: 1900 + + # 闲鱼改价格测试数据(场景九) + xy_price_change: + new_price: 5000 # 改价格目标价(分)= 50元 + + # 闲鱼改库存测试数据(场景十) + xy_stock_change: + new_stock: 2 # 改库存目标数量 + + # 孔夫子核价发布测试数据 + kfz_price_publish: + isbn_success: "9787115600387" # 期望:执行成功 + price_success: 1900 # 价格(分) + + + # 孔夫子改价格测试数据 + kfz_price_change: + new_price: 5000 # 改价格目标价(分)= 50元 + + # 孔夫子改库存测试数据 + kfz_stock_change: + new_stock: 2 # 改库存目标数量 + + # 商品拉取搜索配置 + pull_goods: + search_page_size: 500 # 分批搜索每批大小 + body_wait_max_search: 5000 # body_wait 最大搜索条数 + +# ============================================================ +# 任务类型映射 +# ============================================================ +task_type: + price_publish: "1" # 核价发布 + pull_goods: "3" # 商品拉取 + price_stock_shelf: "5" # 改价格/改库存/上下架(拼多多共用) + xy_price_stock_shelf: "5" # 闲鱼:改价格/改库存/上下架(共用) + kfz_price_stock_shelf: "5" # 孔夫子:改价格/改库存/上下架(共用) + +# ============================================================ +# 任务创建通用参数 +# ============================================================ +task_create: + task_count: "1" + img_type: "1" + +# ============================================================ +# 商品状态码映射 +# ============================================================ +goods_status: + 1: "上架" + 2: "下架" + 3: "售罄" + 4: "已删除" + +# ============================================================ +# body_over 最少条数配置(waitBodyOverMin 的 minCount 参数) +# ============================================================ +body_over_min: + pdd_price_publish: 1 # 场景一:1条执行成功数据 + pdd_price_change: 3 # 场景二:3条(核价1+改价1+改库存1) + pdd_shelf_on_off: 3 # 场景四:3条(与改价格共享任务) + pdd_goods_delete: 4 # 场景四补充:4条(再增加1条删除) + + # body_over 闲鱼 + xy_price_change: 3 # 场景九:3条(核价1+改价格1+改库存1) + xy_shelf_on_off: 3 # 场景十一:3条(改价格共享任务) + + # body_over 孔夫子 + kfz_price_publish: 1 # 1条执行成功数据 + kfz_price_change: 3 # 3条(核价1+改价格1+改库存1) + kfz_stock_change: 3 # 3条(共享) + kfz_shelf_on_off: 3 # 3条(共享) + kfz_goods_delete: 4 # 4条(再增加1条删除) diff --git a/test/test_report.md b/test/test_report.md new file mode 100644 index 0000000..3b15694 --- /dev/null +++ b/test/test_report.md @@ -0,0 +1,89 @@ +# 📋 PlanA API 批量测试报告 + +**测试时间**: 2026-04-30 14:32:53 +**接口地址**: `http://36.212.7.246:8283` +**ShopID**: `2042843272765263874` +**拼多多应用ID**: `203c5a7ba8bd4b8488d5e26f93052642` +**Redis**: `36.212.12.247:6379` DB=0 + +## 📊 测试汇总 + +| 指标 | 值 | +|---|---| +| 通过率 | 23/23 (100.0%) | +| ✅ PASS | 23 | +| ❌ FAIL | 0 | +| 🔴 ERROR | 0 | + +## 📌 跨场景数据记录 + +| 数据项 | 值 | +|---|---| +| 核价发布 task_id | `2049734868307259394` | +| 改价格/上下架 task_id | `2049735627845382145` | +| 执行成功 ISBN | `9787115600387` | +| 执行成功 GoodsID | `947375510416` | +| 执行成功 SkuID | `1893463827063` | +| 商品拉取 task_id | `2049736687976689665` | +| 拉取到的 GoodsID | `944452541081` | + +## 一、拼多多核价发布任务 + +| # | 用例 | 状态 | 耗时 | 详情 | +|---|---|---|---|---| +| 1 | 1、创建拼多多核价发布任务 | ✅ PASS | 581ms | task_id=2049734868307259394 | +| 2 | 2、发送任务数据【isbn=9787115600387, price=1900】期望:执行成功 | ✅ PASS | 63ms | 接口返回成功 | +| 3 | 3a、发送任务数据【isbn=9787223022231, price=0】期望:价格不能小于等于0 | ✅ PASS | 61ms | 接口返回成功 | +| 4 | 3b、发送任务数据【isbn=9787530216965, price=1900】期望:违规词命中 | ✅ PASS | 63ms | 接口返回成功 | +| 5 | 4、Redis 校验 - body_over 中 detail.error 匹配期望结果 | ✅ PASS | 210ms | ISBN=9787115600387 ✅ 匹配'执行成功' (goods_id=947375510416) \| ISBN=9787530216965 ✅ 匹配'违规词命中' (goods_id=0) \| ISBN=9787223022231 ✅ 匹配'价格不能小于等于0' (goods_id... | + +## 二、拼多多改价格 + +| # | 用例 | 状态 | 耗时 | 详情 | +|---|---|---|---|---| +| 1 | 1、创建拼多多改价格、上下架、改库存任务 | ✅ PASS | 546ms | task_id=2049735627845382145 | +| 2 | 2、查询商品详情,获取 sku_id | ✅ PASS | 20.457s | sku_id=1893463827063 商品上架中 status=1(上架) multi_price=2900 price=3000 goodsName=Cinema 4D核心应用案例教程(全彩慕课... | +| 3 | 3、发送任务数据【改价格】 | ✅ PASS | 61ms | ISBN=9787115600387 GoodsID=947375510416 sku_id=1893463827063 price=5000(50.00元) 接口返回成功 | +| 4 | 4、Redis 校验改价格结果(body_over) | ✅ PASS | 218ms | 改价格执行成功: error='修改商品价格 执行成功', price=5000, sku_id=1893463827063 | +| 5 | 5、校验改价格状态(接口验证) | ✅ PASS | 3m0.463s | multi_price=5000 与传入 price=5000 一致 ✅ | + +## 三、拼多多改库存 + +| # | 用例 | 状态 | 耗时 | 详情 | +|---|---|---|---|---| +| 1 | 1、发送任务数据【改库存】 | ✅ PASS | 105ms | ISBN=9787115600387 GoodsID=947375510416 sku_id=1893463827063 stock=2 status=4(改库存) 接口返回成功 | +| 2 | 2、Redis 校验改库存结果(body_over) | ✅ PASS | 196ms | 改库存执行成功: error='修改商品价格 执行成功', stock=0, sku_id=1893463827063 | +| 3 | 3、校验库存(接口验证) | ✅ PASS | 10.322s | 库存匹配: quantity=2 与传入 stock=2 一致 ✅(商品状态=上架) | + +## 四、拼多多上下架 + +| # | 用例 | 状态 | 耗时 | 详情 | +|---|---|---|---|---| +| 1 | 1、发送任务数据【下架】 | ✅ PASS | 60ms | ISBN=9787115600387 GoodsID=947375510416 status=2(下架) 接口返回成功 | +| 2 | 2、校验下架状态 | ✅ PASS | 10.343s | 商品已下架 status=2(下架) goodsId=947375510416 goodsName=Cinema 4D核心应用案例教程(全彩慕课... | + +## 五、拼多多商品拉取 + +| # | 用例 | 状态 | 耗时 | 详情 | +|---|---|---|---|---| +| 1 | 1、创建拼多多商品拉取任务 | ✅ PASS | 1.621s | task_id=2049736687976689665 | +| 2 | 2、等待任务完成并校验 | ✅ PASS | 6m53.765s | status=4 ✅ \| body_over(47328) ≤ task_count_true(47328) ✅ \| ISBN=9787115600387 在 body_over 中找到, goods_id=944452541081 | + +## 七、闲鱼核价发布任务 + +| # | 用例 | 状态 | 耗时 | 详情 | +|---|---|---|---|---| +| 1 | 1、创建闲鱼核价发布任务 | ✅ PASS | 263ms | task_id=2049738429527207937 | +| 2 | 2、发送任务数据【isbn=9787115600387, price=1900】期望:执行成功 | ✅ PASS | 55ms | 接口返回成功 | +| 3 | 3、Redis 校验 - body_over 中 detail.error 包含'执行成功' | ✅ PASS | 53.317s | ISBN=9787115600387 ✅ 匹配'执行成功' (goods_id=1562238986012229) | +| 4 | 5、DLL校验 - 通过闲鱼API查询商品详情 | ✅ PASS | 570ms | 商品详情查询成功 ✅ (product_id=1562238986012229) | + +## 八、闲鱼商品拉取任务 + +| # | 用例 | 状态 | 耗时 | 详情 | +|---|---|---|---|---| +| 1 | 1、创建闲鱼商品拉取任务 | ✅ PASS | 1.391s | task_id=2049738698541477890 | +| 2 | 2、等待任务完成并校验 | ✅ PASS | 6.39s | status=4 ✅ \| body_over(2) ≤ task_count_true(8) ✅ \| ISBN=9787115600387 在 body_over 中找到, goods_id=1562238986012229 | + +--- +*报告生成: 2026-04-30 14:32:53* \ No newline at end of file diff --git a/tool/http.go b/tool/http.go new file mode 100644 index 0000000..c509302 --- /dev/null +++ b/tool/http.go @@ -0,0 +1,75 @@ +package tool + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" +) + +// HttpGetRequest 发起 GET 请求 +// @param url 请求地址 +// @return int 响应状态码 +// @return string 响应内容 +// @return error 错误信息 +func HttpGetRequest(url string) (int, string, error) { + resp, httpGetErr := http.Get(url) + if httpGetErr != nil { + return 0, "", fmt.Errorf("http get 请求失败: %v %v", url, httpGetErr) + } + defer resp.Body.Close() // 重要:必须关闭响应体 + + // 读取响应内容 + body, err := io.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, "", fmt.Errorf("http get 读取响应失败: %v %v", url, httpGetErr) + } + return resp.StatusCode, string(body), nil +} + +// SubmitFormData 提交表单数据 +// @param url 请求地址 +// @param params 表单数据 +// @return error 错误信息 +func SubmitFormData(url string, params map[string]string) (string, error) { + // 创建multipart writer + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // 添加文本字段 + for key, value := range params { + err := writer.WriteField(key, value) + if err != nil { + return "", fmt.Errorf("write field error: %v", err) + } + } + + // 关闭writer + writer.Close() + + // 创建请求 + req, err := http.NewRequest("POST", url, body) + if err != nil { + return "", fmt.Errorf("create request error: %v", err) + } + + // 设置Content-Type + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("send request error: %v", err) + } + defer resp.Body.Close() + + // 读取响应 + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read response error: %v", err) + } + + return string(respBody), nil +} diff --git a/tool/page.go b/tool/page.go new file mode 100644 index 0000000..692fa9c --- /dev/null +++ b/tool/page.go @@ -0,0 +1,16 @@ +package tool + +// GetPage 获取页 +// @param PageSiz 每页条数 +// @return int 页码 +func GetPage(pageNum int, PageSiz int) (int, int) { + if pageNum < 1 { + pageNum = 1 + } + if PageSiz < 1 || PageSiz > 100 { + PageSiz = 20 + } + + offset := (pageNum - 1) * PageSiz + return PageSiz, offset +} diff --git a/tool/pdd/pdd.go b/tool/pdd/pdd.go new file mode 100644 index 0000000..54af2d3 --- /dev/null +++ b/tool/pdd/pdd.go @@ -0,0 +1,108 @@ +package pdd + +import ( + "encoding/json" + "errors" + "fmt" + "planA/initialization/config" + "planA/modules/pdd" + _type "planA/type" + redisType "planA/type/redis" + "strings" +) + +// ParseShopData 解析店铺数据 +// @param shopData 店铺数据 +// @return *_type.ShopInfo 店铺信息 +func ParseShopData(shopData string) (*_type.ShopInfo, error) { + shopData = strings.TrimSpace(shopData) + + // 直接解析为 RedisData数组 + var redisData []redisType.RedisData + err := json.Unmarshal([]byte(shopData), &redisData) + if err != nil { + // 尝试另一种格式:可能是单对象而不是数组 + var singleData redisType.RedisData + if singleErr := json.Unmarshal([]byte(shopData), &singleData); singleErr == nil { + redisData = []redisType.RedisData{singleData} + } else { + return nil, fmt.Errorf("JSON解析失败: %v, 原始数据: %s", err, shopData[:min(100, len(shopData))]) + } + } + + shopInfo := &_type.ShopInfo{} + + // 遍历所有数据,根据source_table分类 + for _, item := range redisData { + switch item.SourceTable { + case "t_shop": + var shop _type.Shop + if err := json.Unmarshal(item.Data, &shop); err == nil { + shopInfo.Shop = &shop + } else { + fmt.Printf("解析t_shop失败 : %v\n", err) + } + case "t_shop_detail": + var detail _type.ShopDetail + if err := json.Unmarshal(item.Data, &detail); err == nil { + shopInfo.ShopDetail = &detail + } else { + fmt.Printf("解析t_shop_detail失败: %v\n", err) + } + case "t_shop_context": + var context _type.ShopContext + if err := json.Unmarshal(item.Data, &context); err == nil { + shopInfo.ShopContext = &context + } else { + fmt.Printf("解析t_shop_context失败: %v\n", err) + } + case "t_spec": + var spec _type.Spec + if err := json.Unmarshal(item.Data, &spec); err == nil { + shopInfo.Spec = &spec + } else { + fmt.Printf("解析t_spec失败: %v\n", err) + } + case "t_price_template": + var template _type.PriceTemplate + if err := json.Unmarshal(item.Data, &template); err == nil { + shopInfo.PriceTemplate = &template + } else { + fmt.Printf("解析t_price_template失败: %v\n", err) + } + default: + fmt.Printf("未知的source_table: %s\n", item.SourceTable) + } + } + + return shopInfo, nil +} + +// BuildPddGoodsOuterCatMappingGet 根据名称获取类目信息 +// @param pddDll pddDLL对象 +// @param token 授权令牌 +// @return error 错误信息 +func BuildPddGoodsOuterCatMappingGet(pddDll *pdd.PddDLL, token string) error { + + pddDll, initPddSOErr := pdd.InitPddDll() + if initPddSOErr != nil { + errMsg := "初始化pdd.so失败: " + initPddSOErr.Error() + return errors.New(errMsg) + } + client, err := config.GetPddClient() + if err != nil { + errMsg := "获取拼多多client失败: " + err.Error() + return errors.New(errMsg) + } + pddCalbackStr, pddGoodsOuterCatMappingGetErr := pddDll.PddGoodsOuterCatMappingGet(client.ClientId, client.ClientSecret, token, "15543", "书籍/杂志/报纸", "书籍 ") + if pddGoodsOuterCatMappingGetErr != nil { + errMsg := "调用DLL类目映射失败 %w" + pddGoodsOuterCatMappingGetErr.Error() + return errors.New(errMsg) + } + //判断 pddCalbackStr 中是否包含 access_token已过期 + if strings.Contains(pddCalbackStr, "access_token已过期") { + errMsg := "拼多多Token已过期" + return errors.New(errMsg) + } + return nil +} diff --git a/tool/process/process.go b/tool/process/process.go new file mode 100644 index 0000000..af301d0 --- /dev/null +++ b/tool/process/process.go @@ -0,0 +1,874 @@ +package process + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "planA/controlState/lock" + "planA/initialization/config" + "planA/modules/logs" + "planA/service" + planAMysql "planA/service/mysql" + _type "planA/type" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + _redis "github.com/go-redis/redis/v8" +) + +var ( + modkernel32 = syscall.NewLazyDLL("kernel32.dll") + modntdll = syscall.NewLazyDLL("ntdll.dll") + + procOpenProcess = modkernel32.NewProc("OpenProcess") + procCloseHandle = modkernel32.NewProc("CloseHandle") + procNtSuspendProcess = modntdll.NewProc("NtSuspendProcess") + procNtResumeProcess = modntdll.NewProc("NtResumeProcess") +) + +const ( + PROCESS_SUSPEND_RESUME = 0x0800 +) + +// RunTaskWorker 启动 B程序 +// @param taskId 任务ID +// @return string 进程ID +// @return error 错误 +func RunTaskWorker(taskId string) (string, error) { + // 1. 尝试加锁 + if !lock.TryLock(taskId) { + return "", fmt.Errorf("taskId %s 已被上锁,跳过B程序执行", taskId) + } + // 2 加锁成功:执行B程序,确保defer释放锁(即使执行出错也能解锁) + defer lock.DestroyLock(taskId) + + // 3 判断pid是否存在 + processId, getProcessIdErr := service.GetProcessId(taskId) + // 检查是否有错误(排除redis key不存在的情况) + if getProcessIdErr != nil && !errors.Is(getProcessIdErr, _redis.Nil) { + return "", getProcessIdErr + } + + // 4 判断pid是否存在 + if processId != "" { + //验证进程是否真实存在 + if !IsProcessExistWindows(processId) { + // 删除 header中的pid + deleteProcessIdErr := service.DeleteProcessId(taskId) + if deleteProcessIdErr != nil { + return "", deleteProcessIdErr + } + } else { + // 返回 pid + return processId, nil + } + } + + // 5 判断任务状态 + taskStatus, getTaskStatusErr := service.GetTaskHeader(taskId) + if getTaskStatusErr != nil { + return "", getTaskStatusErr + } + if taskStatus.Status == _type.TaskStatusPaused { + return "", fmt.Errorf("任务暂停中,请先尝试恢复") + } + if taskStatus.Status == _type.TaskStatusStopped || taskStatus.Status == _type.TaskStatusOver { + return "", fmt.Errorf("任务已结束,不支持重启") + } + + // 6 启动B程序 + _, CallSendPublishingERR := CallSendPublishing(taskId) + if CallSendPublishingERR != nil { + return "", CallSendPublishingERR + } + return processId, nil +} + +// RunCProgram 启动 C程序 +func RunCProgram() error { + // 1. 构建并验证程序路径 + fileUrlConfig, getFileUrlConfigErr := config.GetFileUrlConfig() + if getFileUrlConfigErr != nil { + errMsg := fmt.Sprintf("获取文件路径配置失败: %v", getFileUrlConfigErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return fmt.Errorf(errMsg) + } + programPath := fileUrlConfig.CFileName + + // 2. 验证程序路径是否存在 + absProgramPath, err := filepath.Abs(programPath) + if err != nil { + errMsg := fmt.Sprintf("转换程序路径为绝对路径失败: %s, 原始路径: %s", err, programPath) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return fmt.Errorf(errMsg) + } + + // 3. 验证程序文件 + _, statErr := os.Stat(absProgramPath) + if statErr != nil { + errMsg := fmt.Sprintf("程序文件不存在或无访问权限: %s, 错误: %v", absProgramPath, statErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return fmt.Errorf(errMsg) + } + // 4. 修改PowerShell脚本 - 不等待进程退出 + psScript := fmt.Sprintf(` + $ErrorActionPreference = "Stop" + $programPath = "%s" + try { + if (-not (Test-Path $programPath -PathType Leaf)) { + throw "程序文件不存在: $programPath" + } + + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $programPath + $psi.UseShellExecute = $true + $psi.WindowStyle = 'Normal' + $psi.WorkingDirectory = (Split-Path $programPath -Parent) + $psi.CreateNoWindow = $false + + Write-Host "正在启动程序: $programPath" + $process = [System.Diagnostics.Process]::Start($psi) + Write-Host "程序启动成功,PID: $($process.Id)" + + # 不要调用 WaitForExit(),让PowerShell立即退出 + # 但确保进程独立运行 + $process.Dispose() + + Write-Host "程序已启动,PowerShell即将退出" + } catch { + Write-Error "启动程序失败: $_" + exit 1 + } + `, programPath) + + // 5. 执行PowerShell命令 - 不等待完成 + cmd := exec.Command("powershell", "-NoProfile", "-Command", psScript) + + if runtime.GOOS == "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: 0x00000010, // CREATE_NEW_CONSOLE + HideWindow: false, + } + } + + // 6. 启动PowerShell但不等待(或者等待短时间后分离) + err = cmd.Start() + if err != nil { + return fmt.Errorf("启动程序失败: %v", err) + } + + // 可选:等待1秒让程序启动,然后让PowerShell独立运行 + go func() { + time.Sleep(1 * time.Second) + cmd.Process.Release() // 释放进程句柄,让PowerShell独立运行 + }() + + logs.LoggingMiddleware(logs.LOG_LEVEL_INFO, "C程序已启动") + return nil +} + +// RunFProgram 启动 F程序 +func RunFProgram() error { + // 1. 构建并验证程序路径 + fileUrlConfig, getFileUrlConfigErr := config.GetFileUrlConfig() + if getFileUrlConfigErr != nil { + errMsg := fmt.Sprintf("获取文件路径配置失败: %v", getFileUrlConfigErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return fmt.Errorf(errMsg) + } + programPath := fileUrlConfig.FFileName + + // 2. 验证程序路径是否存在 + absProgramPath, err := filepath.Abs(programPath) + if err != nil { + errMsg := fmt.Sprintf("转换程序路径为绝对路径失败: %s, 原始路径: %s", err, programPath) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return fmt.Errorf(errMsg) + } + + // 3. 验证程序文件 + _, statErr := os.Stat(absProgramPath) + if statErr != nil { + errMsg := fmt.Sprintf("程序文件不存在或无访问权限: %s, 错误: %v", absProgramPath, statErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return fmt.Errorf(errMsg) + } + // 4. 修改PowerShell脚本 - 不等待进程退出 + psScript := fmt.Sprintf(` + $ErrorActionPreference = "Stop" + $programPath = "%s" + try { + if (-not (Test-Path $programPath -PathType Leaf)) { + throw "程序文件不存在: $programPath" + } + + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $programPath + $psi.UseShellExecute = $true + $psi.WindowStyle = 'Normal' + $psi.WorkingDirectory = (Split-Path $programPath -Parent) + $psi.CreateNoWindow = $false + + Write-Host "正在启动程序: $programPath" + $process = [System.Diagnostics.Process]::Start($psi) + Write-Host "程序启动成功,PID: $($process.Id)" + + # 不要调用 WaitForExit(),让PowerShell立即退出 + # 但确保进程独立运行 + $process.Dispose() + + Write-Host "程序已启动,PowerShell即将退出" + } catch { + Write-Error "启动程序失败: $_" + exit 1 + } + `, programPath) + + // 5. 执行PowerShell命令 - 不等待完成 + cmd := exec.Command("powershell", "-NoProfile", "-Command", psScript) + + if runtime.GOOS == "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: 0x00000010, // CREATE_NEW_CONSOLE + HideWindow: false, + } + } + + // 6. 启动PowerShell但不等待(或者等待短时间后分离) + err = cmd.Start() + if err != nil { + return fmt.Errorf("启动程序失败: %v", err) + } + + // 可选:等待1秒让程序启动,然后让PowerShell独立运行 + go func() { + time.Sleep(1 * time.Second) + cmd.Process.Release() // 释放进程句柄,让PowerShell独立运行 + }() + + logs.LoggingMiddleware(logs.LOG_LEVEL_INFO, "C程序已启动") + return nil +} + +// RunDprogram 启动 D程序 +// @param taskId 任务ID +// @return string 进程ID +// @return error 错误 +func RunDprogram(taskId string) (string, error) { + taskIdKey := taskId + "_d" + // 1. 尝试加锁 + if !lock.TryLock(taskIdKey) { + return "", fmt.Errorf("taskId %s 已被上锁,跳过D程序执行", taskId) + } + // 2 加锁成功:执行B程序,确保defer释放锁(即使执行出错也能解锁) + defer lock.DestroyLock(taskIdKey) + + // 3 判断pid是否存在 + delTask, getDelTaskByTaskIdErr := planAMysql.GetDelTaskByTaskId(taskId) + if getDelTaskByTaskIdErr != nil { + return "", getDelTaskByTaskIdErr + } + var processId string + if delTask.Pid == nil { + processId = "" + } else { + processId = *delTask.Pid + } + + // 4 判断pid是否存在 + if processId != "" { + //验证进程是否真实存在 + if !IsProcessExistWindows(processId) { + // 删除 del_task中的pid + updateDelTaskPidByTaskIdErr := planAMysql.UpdateDelTaskPidByTaskId(taskId) + if updateDelTaskPidByTaskIdErr != nil { + return "", updateDelTaskPidByTaskIdErr + } + } else { + fmt.Println("程序存在 , 不启动D程序") + // 返回 pid + return processId, nil + } + } + + // 6 启动D程序 + _, CallSendPublishingERR := CallSendPublishingMysql(taskId) + if CallSendPublishingERR != nil { + return "", CallSendPublishingERR + } + return processId, nil +} + +// SuspendProcess 暂停指定PID的进程 +// @param taskId 任务ID +// @return error 错误 +func SuspendProcess(taskId string) error { + + // 1. 判断 pid是否存在 + processId, getProcessIdErr := service.GetProcessId(taskId) + // 检查是否有错误(排除redis key不存在的情况) + if getProcessIdErr != nil && !errors.Is(getProcessIdErr, _redis.Nil) { + return getProcessIdErr + } + + // 2. 判断pid是否存在 + if processId != "" { + //验证进程是否真实存在 + if IsProcessExistWindows(processId) { + + // 将字符串转换为整数 + pid, err := strconv.Atoi(processId) + if err != nil { + return fmt.Errorf("PID格式错误: %s, 错误: %v", processId, err) + } + + // 检查 PID是否有效 + if pid <= 0 { + return fmt.Errorf("PID必须为正整数") + } + + // 打开进程 + hProcess, _, err := procOpenProcess.Call( + PROCESS_SUSPEND_RESUME, + uintptr(0), + uintptr(pid), + ) + if hProcess == 0 { + return fmt.Errorf("打开进程失败: %v", err) + } + defer procCloseHandle.Call(hProcess) + + // 暂停进程 + callSstatus, _, _ := procNtSuspendProcess.Call(hProcess) + if callSstatus != 0 { + return fmt.Errorf("NtSuspendProcess 失败: 0x%X", callSstatus) + } + } + } + + // 3. 修改Header中的状态 + status := int64(_type.TaskStatusPaused) + updateHeaderStatusErr := service.UpdateHeaderStatus(taskId, status) + if updateHeaderStatusErr != nil { + return updateHeaderStatusErr + } + return nil +} + +// ResumeProcess 恢复指定PID的进程 +// @param taskId 任务ID +// @return error 错误 +func ResumeProcess(taskId string) error { + // 1. 查询PID + processId, getProcessIdErr := service.GetProcessId(taskId) + if getProcessIdErr != nil { + return getProcessIdErr + } + + // 2. 将字符串转换为整数 + pid, err := strconv.Atoi(processId) + if err != nil { + return fmt.Errorf("PID格式错误: %s, 错误: %v", processId, err) + } + + // 3. 检查PID是否有效 + if pid <= 0 { + return fmt.Errorf("PID必须为正整数") + } + + //验证进程是否真实存在 + if IsProcessExistWindows(processId) { + // 4. 打开进程 + hProcess, _, err := procOpenProcess.Call( + PROCESS_SUSPEND_RESUME, + uintptr(0), + uintptr(pid), + ) + if hProcess == 0 { + return fmt.Errorf("打开进程失败: %v", err) + } + defer procCloseHandle.Call(hProcess) + + // 5. 恢复进程 + callStatus, _, _ := procNtResumeProcess.Call(hProcess) + if callStatus != 0 { + return fmt.Errorf("NtResumeProcess 失败: 0x%X", callStatus) + } + } + + // 6. 修改Header中的状态 + status := int64(_type.TaskStatusRunning) + updateHeaderStatusErr := service.UpdateHeaderStatus(taskId, status) + if updateHeaderStatusErr != nil { + return updateHeaderStatusErr + } + return nil +} + +// StopTask 停止任务 +// @param taskId 队列名称 +// @return error 错误 +func StopTask(taskId string) error { + + // 1. 判断 pid是否存在 + processId, getProcessIdErr := service.GetProcessId(taskId) + // 检查是否有错误(排除redis key不存在的情况) + if getProcessIdErr != nil && !errors.Is(getProcessIdErr, _redis.Nil) { + return getProcessIdErr + } + + // 2. 判断pid是否存在 + if processId != "" { + //验证进程是否真实存在 + if IsProcessExistWindows(processId) { + // 恢复 B程序避免程序处于暂停状态 + resumeProcessErr := ResumeProcess(taskId) + if resumeProcessErr != nil { + return resumeProcessErr + } + } + } + + // 3. 修改 Header中的状态 并且 删除 bodyWait 中的数据 + stopTaskErr := service.StopTask(taskId) + if stopTaskErr != nil { + return stopTaskErr + } + return nil +} +func CallSendPublishing(taskId string) (*os.Process, error) { + // 1. 基础入参校验 + if taskId == "" { + return nil, errors.New("队列名称qName不能为空") + } + + // 先在Redis中创建一个占位符,表示进程即将启动 + placeholderPID := "starting" + setProcessIdErr := service.SetProcessId(taskId, placeholderPID) + if setProcessIdErr != nil { + errMsg := fmt.Sprintf("保存进程占位符到Redis失败: %v, taskId: %s", setProcessIdErr, taskId) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 2. 构建并验证程序路径 + fileUrlConfig, getFileUrlConfigErr := config.GetFileUrlConfig() + if getFileUrlConfigErr != nil { + errMsg := fmt.Sprintf("获取文件路径配置失败: %v", getFileUrlConfigErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + programPath := fileUrlConfig.BFileName + + // 3. 验证程序路径是否存在 + absProgramPath, err := filepath.Abs(programPath) + if err != nil { + errMsg := fmt.Sprintf("转换程序路径为绝对路径失败: %s, 原始路径: %s", err, programPath) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 4. 验证程序路径 + _, statErr := os.Stat(absProgramPath) + if statErr != nil { + errMsg := fmt.Sprintf("程序文件不存在或无访问权限: %s, 错误: %v", absProgramPath, statErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 5. 修复后的PowerShell脚本 + psScript := fmt.Sprintf(` +# 设置错误捕获模式 +$ErrorActionPreference = "Stop" +$programPath = "%s" +$arguments = "%s" + +try { + # 再次验证程序存在性 + if (-not (Test-Path $programPath -PathType Leaf)) { + throw "程序文件不存在: $programPath" + } + + # 构建进程启动信息 + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $programPath + $psi.Arguments = $arguments + $psi.UseShellExecute = $true + $psi.WindowStyle = 'Normal' + $psi.WorkingDirectory = (Split-Path $programPath -Parent) # 设置工作目录为程序所在目录 + + # 启动进程 + Write-Host "开始启动程序: $programPath 参数: $arguments" + $process = [System.Diagnostics.Process]::Start($psi) + Write-Host "程序启动成功,PID: $($process.Id)" + + # 等待窗口出现,设置超时时间 + $timeout = 3000 + $startTime = Get-Date + $hwnd = $null + + # 等待窗口句柄不为空 + while ($true) { + try { + $process.Refresh() + $hwnd = $process.MainWindowHandle + if ($hwnd -and $hwnd -ne [IntPtr]::Zero) { + break + } + } catch { + # 忽略刷新错误 + } + + if (((Get-Date) - $startTime).TotalMilliseconds -ge $timeout) { + Write-Warning "等待窗口句柄超时 (PID: $($process.Id))" + break + } + Start-Sleep -Milliseconds 50 + } + + # 尝试将窗口前置(仅在成功获取窗口句柄时) + if ($hwnd -and $hwnd -ne [IntPtr]::Zero) { + try { + Add-Type @" + using System; + using System.Runtime.InteropServices; + public class WindowHelper { + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + [DllImport("user32.dll")] + public static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] + public static extern bool AllowSetForegroundWindow(uint dwProcessId); + } +"@ + + [WindowHelper]::AllowSetForegroundWindow($process.Id) + [WindowHelper]::ShowWindow($hwnd, 9) # 9 = SW_RESTORE + [WindowHelper]::SetForegroundWindow($hwnd) + Write-Host "成功将窗口前置,句柄: $hwnd" + } catch { + Write-Warning "窗口前置操作失败: $_" + # 不抛出异常,继续执行 + } + } else { + Write-Warning "未能获取窗口句柄,程序可能没有窗口界面 (PID: $($process.Id))" + } + + # 输出PID供Go解析 + Write-Output $process.Id +} catch { + # 捕获所有异常并输出 + Write-Error "启动程序失败: $_" + exit 1 # 返回非0退出码 +} +`, absProgramPath, taskId) + + // 6. 执行PowerShell命令,同时捕获stdout和stderr + cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psScript) + if runtime.GOOS == "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: 0x00000010, // CREATE_NEW_CONSOLE - 创建新控制台 + } + } + + // 7. 同时捕获标准输出和标准错误 + var stdout, stderr strings.Builder + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // 8. 执行命令 + runErr := cmd.Run() + + // 9. 输出所有PowerShell的输出 + stdoutStr := stdout.String() + stderrStr := stderr.String() + + // 记录标准输出(调试用) + if stdoutStr != "" { + logs.LoggingMiddleware(logs.LOG_LEVEL_INFO, fmt.Sprintf("PowerShell标准输出: %s", stdoutStr)) + } + + // 记录标准错误(警告信息不算错误) + if stderrStr != "" { + // 检查是否为警告信息 + if strings.Contains(stderrStr, "WARNING:") { + logs.LoggingMiddleware(logs.LOG_LEVEL_WARNING, fmt.Sprintf("PowerShell警告: %s", stderrStr)) + } else { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("PowerShell错误: %s", stderrStr)) + } + } + + // 10. 检查命令执行是否失败 + if runErr != nil { + errMsg := fmt.Sprintf("PowerShell执行失败: %v, 错误输出: %s", runErr, stderrStr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 11. 解析PID(增加校验) + var pid uint32 + lines := strings.Split(stdoutStr, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // 跳过非纯数字行(如 Write-Host 的输出) + pidInt, err := strconv.Atoi(line) + if err == nil && pidInt > 0 { + pid = uint32(pidInt) + logs.LoggingMiddleware(logs.LOG_LEVEL_INFO, fmt.Sprintf("成功解析PID: %d", pid)) + break // 找到有效PID立即退出 + } + } + + if pid == 0 { + errMsg := fmt.Sprintf("未解析到有效PID,PowerShell输出: %s, 错误输出: %s", stdoutStr, stderrStr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 12. 更新Redis中的PID + processID := fmt.Sprintf("%d", pid) + setProcessIdErr = service.SetProcessId(taskId, processID) + if setProcessIdErr != nil { + errMsg := fmt.Sprintf("更新进程PID到Redis失败: %v, taskId: %s, PID: %d", setProcessIdErr, taskId, pid) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 13. 返回进程句柄 + process := &os.Process{Pid: int(pid)} + return process, nil +} +func CallSendPublishingMysql(taskId string) (*os.Process, error) { + // 1. 基础入参校验 + if taskId == "" { + return nil, errors.New("taskId不能为空") + } + + // 先在Redis中创建一个占位符,表示进程即将启动 + placeholderPID := "starting" + setProcessIdErr := planAMysql.UpdateDelTaskPidByTaskIdAndPid(taskId, placeholderPID) + if setProcessIdErr != nil { + errMsg := fmt.Sprintf("保存进程占位符到Redis失败: %v, taskId: %s", setProcessIdErr, taskId) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 2. 构建并验证程序路径 + fileUrlConfig, getFileUrlConfigErr := config.GetFileUrlConfig() + if getFileUrlConfigErr != nil { + errMsg := fmt.Sprintf("获取文件路径配置失败: %v", getFileUrlConfigErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + programPath := fileUrlConfig.DFileName + + // 3. 验证程序路径是否存在 + absProgramPath, err := filepath.Abs(programPath) + if err != nil { + errMsg := fmt.Sprintf("转换程序路径为绝对路径失败: %s, 原始路径: %s", err, programPath) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 4. 验证程序路径 + _, statErr := os.Stat(absProgramPath) + if statErr != nil { + errMsg := fmt.Sprintf("程序文件不存在或无访问权限: %s, 错误: %v", absProgramPath, statErr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 5. 修复后的PowerShell脚本 + psScript := fmt.Sprintf(` +# 设置错误捕获模式 +$ErrorActionPreference = "Stop" +$programPath = "%s" +$arguments = "%s" + +try { + # 再次验证程序存在性 + if (-not (Test-Path $programPath -PathType Leaf)) { + throw "程序文件不存在: $programPath" + } + + # 构建进程启动信息 + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $programPath + $psi.Arguments = $arguments + $psi.UseShellExecute = $true + $psi.WindowStyle = 'Normal' + $psi.WorkingDirectory = (Split-Path $programPath -Parent) # 设置工作目录为程序所在目录 + + # 启动进程 + Write-Host "开始启动程序: $programPath 参数: $arguments" + $process = [System.Diagnostics.Process]::Start($psi) + Write-Host "程序启动成功,PID: $($process.Id)" + + # 等待窗口出现,设置超时时间 + $timeout = 3000 + $startTime = Get-Date + $hwnd = $null + + # 等待窗口句柄不为空 + while ($true) { + try { + $process.Refresh() + $hwnd = $process.MainWindowHandle + if ($hwnd -and $hwnd -ne [IntPtr]::Zero) { + break + } + } catch { + # 忽略刷新错误 + } + + if (((Get-Date) - $startTime).TotalMilliseconds -ge $timeout) { + Write-Warning "等待窗口句柄超时 (PID: $($process.Id))" + break + } + Start-Sleep -Milliseconds 50 + } + + # 尝试将窗口前置(仅在成功获取窗口句柄时) + if ($hwnd -and $hwnd -ne [IntPtr]::Zero) { + try { + Add-Type @" + using System; + using System.Runtime.InteropServices; + public class WindowHelper { + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + [DllImport("user32.dll")] + public static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] + public static extern bool AllowSetForegroundWindow(uint dwProcessId); + } +"@ + + [WindowHelper]::AllowSetForegroundWindow($process.Id) + [WindowHelper]::ShowWindow($hwnd, 9) # 9 = SW_RESTORE + [WindowHelper]::SetForegroundWindow($hwnd) + Write-Host "成功将窗口前置,句柄: $hwnd" + } catch { + Write-Warning "窗口前置操作失败: $_" + # 不抛出异常,继续执行 + } + } else { + Write-Warning "未能获取窗口句柄,程序可能没有窗口界面 (PID: $($process.Id))" + } + + # 输出PID供Go解析 + Write-Output $process.Id +} catch { + # 捕获所有异常并输出 + Write-Error "启动程序失败: $_" + exit 1 # 返回非0退出码 +} +`, absProgramPath, taskId) + + // 6. 执行PowerShell命令,同时捕获stdout和stderr + cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psScript) + if runtime.GOOS == "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: 0x00000010, // CREATE_NEW_CONSOLE - 创建新控制台 + } + } + + // 7. 同时捕获标准输出和标准错误 + var stdout, stderr strings.Builder + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // 8. 执行命令 + runErr := cmd.Run() + + // 9. 输出所有PowerShell的输出 + stdoutStr := stdout.String() + stderrStr := stderr.String() + + // 记录标准输出(调试用) + if stdoutStr != "" { + logs.LoggingMiddleware(logs.LOG_LEVEL_INFO, fmt.Sprintf("PowerShell标准输出: %s", stdoutStr)) + } + + // 记录标准错误(警告信息不算错误) + if stderrStr != "" { + // 检查是否为警告信息 + if strings.Contains(stderrStr, "WARNING:") { + logs.LoggingMiddleware(logs.LOG_LEVEL_WARNING, fmt.Sprintf("PowerShell警告: %s", stderrStr)) + } else { + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, fmt.Sprintf("PowerShell错误: %s", stderrStr)) + } + } + + // 10. 检查命令执行是否失败 + if runErr != nil { + errMsg := fmt.Sprintf("PowerShell执行失败: %v, 错误输出: %s", runErr, stderrStr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 11. 解析PID(增加校验) + var pid uint32 + lines := strings.Split(stdoutStr, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // 跳过非纯数字行(如 Write-Host 的输出) + pidInt, err := strconv.Atoi(line) + if err == nil && pidInt > 0 { + pid = uint32(pidInt) + logs.LoggingMiddleware(logs.LOG_LEVEL_INFO, fmt.Sprintf("成功解析PID: %d", pid)) + break // 找到有效PID立即退出 + } + } + + if pid == 0 { + errMsg := fmt.Sprintf("未解析到有效PID,PowerShell输出: %s, 错误输出: %s", stdoutStr, stderrStr) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 12. 更新Redis中的PID + processID := fmt.Sprintf("%d", pid) + setProcessIdErr = planAMysql.UpdateDelTaskPidByTaskIdAndPid(taskId, processID) + if setProcessIdErr != nil { + errMsg := fmt.Sprintf("更新进程PID到Redis失败: %v, taskId: %s, PID: %d", setProcessIdErr, taskId, pid) + logs.LoggingMiddleware(logs.LOG_LEVEL_ERROR, errMsg) + return nil, fmt.Errorf(errMsg) + } + + // 13. 返回进程句柄 + process := &os.Process{Pid: int(pid)} + return process, nil +} + +// IsProcessExistWindows 检查Windows进程是否存在 +func IsProcessExistWindows(pid string) bool { + if pid == "" { + return false + } + pid64, err := strconv.ParseInt(pid, 10, 0) + if err != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_WARNING, fmt.Sprintf("转换进程PID:%v失败: %v", pid, err)) + return false + } + pidInt := int(pid64) + // 使用 tasklist命令检查进程是否存在 + cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pidInt)) + output, err := cmd.Output() + if err != nil { + logs.LoggingMiddleware(logs.LOG_LEVEL_WARNING, fmt.Sprintf("检查进程PID:%v失败: %v", pid, err)) + return false + } + return strings.Contains(strings.ToLower(string(output)), fmt.Sprintf("%d", pidInt)) +} diff --git a/tool/sign.go b/tool/sign.go new file mode 100644 index 0000000..bcd6d18 --- /dev/null +++ b/tool/sign.go @@ -0,0 +1,57 @@ +package tool + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "planA/initialization/golabl" + "sort" + "strings" +) + +// SignParams 对参数进行签名(类似支付宝、微信支付) +func SignParams(params map[string]string) string { + SecretKey := golabl.Config.Server.SignKey + // 1. 过滤空值 + filteredParams := make(map[string]string) + for k, v := range params { + if v != "" && k != "sign" && k != "sign_type" { + filteredParams[k] = v + } + } + + // 2. 排序 + keys := make([]string, 0, len(filteredParams)) + for k := range filteredParams { + keys = append(keys, k) + } + sort.Strings(keys) + + // 3. 拼接字符串 + var builder strings.Builder + for i, k := range keys { + if i > 0 { + builder.WriteString("&") + } + builder.WriteString(k) + builder.WriteString("=") + builder.WriteString(fmt.Sprintf("%v", filteredParams[k])) + } + + // 4. 追加密钥并签名 + queryStr := builder.String() + signStr := queryStr + "&key=" + SecretKey + + // MD5签名(常用) + hash := md5.Sum([]byte(signStr)) + return strings.ToUpper(hex.EncodeToString(hash[:])) +} + +// VerifySign 验证签名 +func VerifySign(params map[string]string) bool { + if sign, ok := params["sign"]; ok { + expectedSign := SignParams(params) + return expectedSign == sign + } + return false +} diff --git a/tool/tool.go b/tool/tool.go new file mode 100644 index 0000000..b43b30e --- /dev/null +++ b/tool/tool.go @@ -0,0 +1,393 @@ +package tool + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "planA/initialization/golabl" + _type "planA/type" + "reflect" + "strconv" + "strings" + "time" +) + +// CheckContext 检查上下文是否取消 +func CheckContext(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() // 返回取消原因 + default: + return nil // 上下文仍然有效 + } +} + +// StructToMap 使用反射将结构体转换为map[string]interfaces{} +// @param obj 需要转换的数据 +// @return map[string]interface{} 转换后的数据 +// @return error 错误信息 +func StructToMap(obj interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + val := reflect.ValueOf(obj) + typ := reflect.TypeOf(obj) + + // 如果是指针,获取指向的值 + if val.Kind() == reflect.Ptr { + val = val.Elem() + typ = typ.Elem() + } + + // 确保是结构体 + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("参数必须是结构体") + } + + // 遍历结构体字段 + for i := 0; i < val.NumField(); i++ { + field := typ.Field(i) + fieldValue := val.Field(i) + + // 跳过不可导出的字段 + if !field.IsExported() { + continue + } + + // 获取json标签作为字段名 + jsonTag := field.Tag.Get("json") + fieldName := field.Name + + if jsonTag != "" { + // 处理 json tag,忽略 omitempty 等选项 + if idx := strings.Index(jsonTag, ","); idx != -1 { + fieldName = jsonTag[:idx] + } else { + fieldName = jsonTag + } + + // 跳过标记为 "-" 的字段 + if fieldName == "-" { + continue + } + } + + // 检查字段类型 + switch fieldValue.Kind() { + case reflect.Struct: + // 处理结构体字段(嵌套结构体) + nestedValue := fieldValue.Interface() + + // 检查是否是 time.Time 类型 + if _, ok := nestedValue.(time.Time); ok { + // time.Time 类型转换为时间戳 + result[fieldName] = nestedValue.(time.Time).Unix() + continue + } + + // 检查是否是自定义类型 + switch v := nestedValue.(type) { + case _type.TaskStatus: + // TaskStatus 转换为 int64 + result[fieldName] = int64(v) + case _type.ShopMsg: + // ShopMsg 转换为 JSON 字符串 + shopMsgJSON, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("序列化 ShopMsg 失败: %v", err) + } + result[fieldName] = string(shopMsgJSON) + case _type.PriceMod: + // PriceMod 转换为 JSON 字符串 + priceModJSON, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("序列化 PriceMod 失败: %v", err) + } + result[fieldName] = string(priceModJSON) + default: + // 其他结构体转换为 JSON 字符串 + nestedJSON, err := json.Marshal(nestedValue) + if err != nil { + return nil, fmt.Errorf("序列化字段 %s 失败: %v", fieldName, err) + } + result[fieldName] = string(nestedJSON) + } + + default: + // 处理基础类型字段 + if !fieldValue.CanInterface() { + continue + } + + // 处理指针类型 + if fieldValue.Kind() == reflect.Ptr && !fieldValue.IsNil() { + elemValue := fieldValue.Elem() + if elemValue.Kind() == reflect.Struct { + // 指针指向结构体,转换为 JSON 字符串 + nestedJSON, err := json.Marshal(elemValue.Interface()) + if err != nil { + return nil, fmt.Errorf("序列化指针字段 %s 失败: %v", fieldName, err) + } + result[fieldName] = string(nestedJSON) + } else { + // 指针指向基础类型 + result[fieldName] = elemValue.Interface() + } + } else { + // 直接存储值,但处理自定义类型 + switch v := fieldValue.Interface().(type) { + case _type.TaskStatus: + // TaskStatus 转换为 int64 + result[fieldName] = int64(v) + default: + result[fieldName] = fieldValue.Interface() + } + } + } + } + + return result, nil +} + +// SetPage 分页处理 +func SetPage(pageStr string, sizeStr string) (int, int) { + // 处理页码,默认为1 + page := 1 + if pageStr != "" { + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + } + + // 处理每页条数,默认为10 + size := 10 + if sizeStr != "" { + if s, err := strconv.Atoi(sizeStr); err == nil && s > 0 { + // 可以限制最大条数 + if s > 100 { + size = 100 + } else { + size = s + } + } + } + return page, size +} + +// Success 成功响应 +// @param httpMsg http.ResponseWriter +// @param data 返回的数据 +func Success(httpMsg http.ResponseWriter, data any) { + ret := map[string]interface{}{ + "code": "200", + "msg": "成功", + "data": data, + } + json.NewEncoder(httpMsg).Encode(ret) +} + +// Error 错误响应 +// @param httpMsg http.ResponseWriter +// @param msg 错误信息 +// @param code 错误码 +func Error(httpMsg http.ResponseWriter, msg string, code int) { + fmt.Println("错误:" + msg) + codeStr := strconv.FormatInt(int64(code), 10) + ret := map[string]interface{}{ + "code": codeStr, + "msg": msg, + } + json.NewEncoder(httpMsg).Encode(ret) +} + +// HttpBannedWordSubstitution 违禁词处理 +func HttpBannedWordSubstitution(url string, reqData map[string]string) (_type.HttpBannedWordSubstitutionBookInfoRes, error) { + var resDta _type.HttpBannedWordSubstitutionBookInfoRes + + // 构建带参数的 URL + reqUrl, err := BuildURLWithParams(url, reqData) + if err != nil { + return resDta, fmt.Errorf("构建URL失败: %v", err) + } + + // 发送 GET请求 + _, resStr, httpGetRequestErr := HttpGetRequest(reqUrl) + + if httpGetRequestErr != nil { + return resDta, httpGetRequestErr + } + + // 将字符串转换为结构体 + jsonErr := json.Unmarshal([]byte(resStr), &resDta) + if jsonErr != nil { + return resDta, jsonErr + } + + if resDta.Code != "200" { + return resDta, fmt.Errorf("请求违禁词接口错误 错误: url %s %s", reqUrl, resStr) + } + // 返回结果 + return resDta, nil +} + +// BuildURLWithParams 将map参数拼接到URL后面 +func BuildURLWithParams(baseURL string, params map[string]string) (string, error) { + if len(params) == 0 { + return baseURL, nil + } + + // 解析基础URL + parsedURL, err := url.Parse(baseURL) + if err != nil { + return "", fmt.Errorf("解析URL失败: %v", err) + } + + // 获取现有的查询参数 + query := parsedURL.Query() + + // 添加新的参数 + for key, value := range params { + query.Set(key, value) + } + + // 重新编码查询参数 + parsedURL.RawQuery = query.Encode() + + return parsedURL.String(), nil +} + +// FilterStrings 过滤掉数组中的空字符串 +func FilterStrings(s []string) []string { + result := make([]string, 0, len(s)) + + // 固定写死要过滤的字符串 + filterMap := map[string]bool{ + "": true, + "图片格式错误:图片格式非jpeg,请自查图片格式": true, + } + + for _, str := range s { + if !filterMap[str] { + result = append(result, str) + } + } + return result +} + +// JsonResponse JSON响应工具函数 +// 统一处理API响应的JSON格式化和发送 +// 参数: +// - w: HTTP响应写入器 +// - statusCode: HTTP状态码 +// - resp: API响应数据 +func JsonResponse(w http.ResponseWriter, statusCode int, resp _type.APIResponse) { + // 设置响应头为JSON格式 + w.Header().Set("Content-Type", "application/json") + // 设置HTTP状态码 + w.WriteHeader(statusCode) + + // 编码JSON并写入响应体 + err := json.NewEncoder(w).Encode(resp) + if err != nil { + // 编码失败时打印错误到控制台(不中断请求) + fmt.Printf("JSON编码错误: %v\n", err) + return + } +} + +// StringToArray 将字符串根据/转为数组 +func StringToArray(str string) []string { + + // 1. 分割字符串 + parts := strings.Split(str, "/") + + // 2. 创建结果切片 + result := make([]string, 0, len(parts)) + + // 3. 遍历转换 + for _, part := range parts { + result = append(result, part) + } + return result +} + +// GetTaskTypeName 获取任务类型名称 +// @param taskType 任务类型 +// @return string 任务类型名称 +func GetTaskTypeName(taskType string) string { + switch taskType { + case "1": + return "新发布商品任务" + case "2": + return "新发布商品任务" + case "3": + return "新拉取商品任务" + } + return "未知任务类型" +} + +// CleanOldFolders 删除指定目录中超过保留天数的日期文件夹 +// dirPath: 要清理的目录路径 +// keepDays: 保留的天数(保留最近 keepDays 天的文件夹) +func CleanOldFolders(dirPath string, keepDays int) error { + // 读取目录内容 + entries, err := os.ReadDir(dirPath) + if err != nil { + return fmt.Errorf("读取目录失败: %v", err) + } + + // 计算截止日期(保留最近 keepDays 天,即删除 keepDays 天之前的) + cutoffDate := time.Now().AddDate(0, 0, -keepDays) + + deletedCount := 0 + for _, entry := range entries { + // 只处理目录 + if !entry.IsDir() { + continue + } + + folderName := entry.Name() + + // 尝试解析文件夹名为日期格式 (2006-01-02) + folderDate, err := time.Parse("2006-01-02", folderName) + if err != nil { + // 不是日期格式的文件夹,跳过 + fmt.Printf("跳过非日期格式文件夹: %s\n", folderName) + continue + } + + // 如果文件夹日期早于截止日期,则删除 + if folderDate.Before(cutoffDate) { + folderPath := filepath.Join(dirPath, folderName) + fmt.Printf("删除过期文件夹: %s\n", folderPath) + + err := os.RemoveAll(folderPath) + if err != nil { + fmt.Printf("删除失败 %s: %v\n", folderPath, err) + } else { + deletedCount++ + } + } + } + + fmt.Printf("共删除 %d 个过期文件夹\n", deletedCount) + return nil +} + +// GetSubscriptionExpirationTime 根据用户id获取订阅到期时间 +func GetSubscriptionExpirationTime(shopId string) (int64, error) { + url := golabl.Config.FileUrl.GetSubscriptionExpirationDateUrl + "?userId=" + shopId + _, dataStr, httpGetRequestErr := HttpGetRequest(url) + if httpGetRequestErr != nil { + return 0, httpGetRequestErr + } + var getSubscriptionExpirationDateUrl _type.GetSubscriptionExpirationDateUrl + jsonErr := json.Unmarshal([]byte(dataStr), &getSubscriptionExpirationDateUrl) + if jsonErr != nil { + return 0, jsonErr + } + return getSubscriptionExpirationDateUrl.Data.ExpirationDate, nil +} diff --git a/type/bannedWords.go b/type/bannedWords.go new file mode 100644 index 0000000..c742f60 --- /dev/null +++ b/type/bannedWords.go @@ -0,0 +1,25 @@ +package _type + +// 违禁词结构体 + +// HttpBannedWordSubstitutionBookInfoRes 违禁词响应结构体 +type HttpBannedWordSubstitutionBookInfoRes struct { + Msg string `json:"msg"` // 查询成功 + Code string `json:"code"` // 状态码 200 + Data []MatchRule `json:"data"` // 匹配规则列表 + Success bool `json:"success"` // 是否成功 + Author string `json:"author"` // 作者 + Isbn string `json:"isbn"` // ISBN + Publisher string `json:"publisher"` // 出版社 + BookName string `json:"bookName"` // 书名(可能包含***) +} + +// MatchRule 匹配规则结构体 +type MatchRule struct { + CreateBy string `json:"createBy"` // 创建人ID + MatchType string `json:"matchType"` // 匹配类型:ISBN匹配/书名匹配 + AddTxt string `json:"addTxt"` // 匹配文本 + ID int64 `json:"id"` // 规则ID + Sort string `json:"sort"` // 排序信息 "0,3" + LimitationType string `json:"limitationType"` // 限制类型 "0"/"1"/"6" +} diff --git a/type/book.go b/type/book.go new file mode 100644 index 0000000..5f29921 --- /dev/null +++ b/type/book.go @@ -0,0 +1,32 @@ +package _type + +// 书籍结构体 + +// Book 书籍信息 +type Book struct { + ID int64 `json:"id"` + BookName string `json:"book_name"` + BookPic BookImage `json:"book_pic"` //pddPath 轮播图第一张 官图 + BookPicS BookImage `json:"book_pic_s"` //pddPath 轮播图第一张 实拍图 + BookPicObj string `json:"book_pic_obj"` + BookDetailImage BookImage `json:"book_detail_image"` //pddPath 详情图 + BookPicB string `json:"book_pic_b"` //pddPath 白底图 + BookDirectoryImage BookImage `json:"book_directory_image"` //pddPath 目录图 + ISBN string `json:"isbn"` + Author string `json:"author"` //作者 + Category string `json:"category"` //分类 + Publisher string `json:"publisher"` //出版社 + PublicationTime string `json:"publication_time"` //出版时间 + BindingLayout string `json:"binding_layout"` //装帧 + FixPrice float64 `json:"fix_price"` //售价 + Content string `json:"content"` //简介 + IsSuit int64 `json:"is_suit"` //套装 + IsIllegal int64 `json:"is_illegal"` // + IsReturn int64 `json:"is_return"` //驳回 + IsFilter string `json:"is_filter"` //过滤 +} + +type BookImage struct { + LocalPath string `json:"localPath"` + PddPath string `json:"pddPath"` //轮播图第一张 官图 +} diff --git a/type/common.go b/type/common.go new file mode 100644 index 0000000..bea9476 --- /dev/null +++ b/type/common.go @@ -0,0 +1,24 @@ +package _type + +//通用结构体 + +// CreateTaskResponse 创建任务响应结构体 +type CreateTaskResponse struct { + Msg string `json:"msg"` + Code int `json:"code"` + TaskID string `json:"taskId"` +} + +type Page struct { + PageNum int // 页码,从1开始 + PageSize int // 每页条数 +} + +// APIResponse API响应结构体 +// 用于统一API接口的返回格式 +type APIResponse struct { + Success bool `json:"success"` // 请求是否成功 + Message string `json:"message"` // 响应消息 + Data interface{} `json:"data,omitempty"` // 响应数据(可选) + Error string `json:"error,omitempty"` // 错误信息(可选) +} diff --git a/type/config.go b/type/config.go new file mode 100644 index 0000000..a12b327 --- /dev/null +++ b/type/config.go @@ -0,0 +1,175 @@ +package _type + +import "time" + +//配置结构体 + +// Config 配置结构 +type Config struct { + Server Server `json:"server"` + Speed Speed `json:"speed"` + Minio Minio `json:"minio"` + Alive Alive `json:"alive"` + MysqlConfig MysqlConfig `json:"mysql_config"` + PsiMysqlConfig MysqlConfig `json:"psi_mysql_config"` + PoolConfig PoolConfig `json:"pool_config"` + RedisConfig []RedisConfig `json:"redis_config"` + PddConfig PddConfig `json:"pdd_config"` + KfzConfig KfzConfig `json:"kfz_config"` + TaobaoConfig TaobaoConfig `json:"taobao_config"` + AppBConfig AppBConfig `json:"app_b_config"` + HttpUrl HttpUrl `json:"http_url"` + FileUrl FileUrl `json:"file_url"` +} + +// Server 服务器配置结构 +type Server struct { + Port string `json:"port"` + FPort string `json:"f_port"` + Filter int `json:"filter"` + ReplaceMark string `json:"replace_mark"` + RedisExp int `json:"redis_exp"` + ReadDb string `json:"read_db"` + ErrPauseTime int `json:"err_pause_time"` + SignKey string `json:"sign_key"` + DataDay int `json:"data_day"` + IsC bool `json:"is_c"` +} + +// Speed 限速器结构 +type Speed struct { + PddSpeed int `json:"pdd_speed"` + XianyuSpeed int `json:"xianyu_speed"` + Watermark int `json:"watermark"` +} + +// Minio 图片空间结构域 +type Minio struct { + Url string `json:"url"` + AccessKeyID string `json:"access_key_id"` + SecretAccessKey string `json:"secret_access_key"` + BucketName string `json:"bucket_name"` + TargetDir string `json:"target_dir"` + UseSSL bool `json:"use_ssl"` +} + +// Alive 存活状态结构 +type Alive struct { + Fluent int `json:"fluent"` + Slow int `json:"slow"` +} + +// MysqlConfig Mysql 配置结构 +type MysqlConfig struct { + User string `json:"user"` + Password string `json:"password"` + Host string `json:"host"` + Port int `json:"port"` + DBName string `json:"db_name"` + Loglevel string `json:"loglevel"` + MaxRetryTimes int `json:"max_retry_times"` + BaseRetryInterval time.Duration `json:"base_retry_interval"` + MaxRetryInterval time.Duration `json:"max_retry_interval"` + MaxOpenConns int `json:"max_open_conns"` + MaxIdleConns int `json:"max_idle_conns"` + ConnMaxIdleTime time.Duration `json:"conn_max_idle_time"` + ConnMaxLifetime time.Duration `json:"conn_max_lifetime"` +} + +// RedisConfig Redis 配置结构 +type RedisConfig struct { + Addr string `json:"addr"` + Password string `json:"password"` + DB int `json:"db"` + PoolSize int `json:"pool_size"` + PoolTimeout int `json:"pool_timeout"` + ReadTimeout int `json:"read_timeout"` + WriteTimeout int `json:"write_timeout"` + DialTimeout int `json:"dial_timeout"` + IdleTimeout int `json:"idle_timeout"` + MinIdleConns int `json:"min_idle_conns"` + IdleCheckFrequency int `json:"idle_check_frequency"` + MaxRetries int `json:"max_retries"` + MaxRetryBackoff int `json:"max_retry_backoff"` + MinRetryBackoff int `json:"min_retry_backoff"` +} + +// PoolConfig 协程池配置结构 +type PoolConfig struct { + Size int `json:"size"` // 协程数 + WithExpiryDuration int `json:"with_expiry_duration"` // 过期时间 + WithPreAlloc bool `json:"with_pre_alloc"` // 预分配 + WithMaxBlockingTasks int `json:"with_max_blocking_tasks"` // 最大阻塞任务数 + WithNonblocking bool `json:"with_nonblocking"` // 非阻塞 +} + +// AppBConfig 应用配置结构 +type AppBConfig struct { + AppName string `json:"app_name"` // 应用名称 + AppDir string `json:"app_dir"` // 应用目录 +} + +// PddConfig 拼多多配置 +type PddConfig struct { + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} + +// KfzConfig 孔夫子配置 +type KfzConfig struct { + AppId int `json:"app_id"` + AppSecret string `json:"app_secret"` +} + +// TaobaoConfig 淘宝配置 +type TaobaoConfig struct { + AppKey int `json:"app_key"` // 平台分配的 App Key + AppSecret string `json:"app_secret"` // App 密钥,用于 HMAC-MD5 签名计算 + Ati string `json:"ati"` // 设备/用户标识 + UserID string `json:"user_id"` // 当前操作用户 ID + CompanyID string `json:"company_id"` // 公司/店铺所属 ID + BaseURL string `json:"base_url"` // API 基础域名 + LocalImgDir string `json:"local_img_dir"` // 图片本地缓存目录 + RequestTimeout int `json:"request_timeout"` // 请求超时时间(秒) + Token string `json:"token"` //token +} + +// HttpUrl 请求路径 +type HttpUrl struct { + TaskUrl string `json:"task_url"` +} + +type FileUrl struct { + PddDll string `json:"pdd_dll"` + KfzDll string `json:"kfz_dll"` + XianYuDll string `json:"xian_yu_dll"` + LogDll string `json:"log_dll"` + ImageDll string `json:"image_dll"` + BFileName string `json:"b_file_name"` + CFileName string `json:"c_file_name"` + DFileName string `json:"d_file_name"` + EFileName string `json:"e_file_name"` + FFileName string `json:"f_file_name"` + CreateTaskUrl string `json:"create_task_url"` + BannedWordSubstitutionUrl string `json:"banned_word_substitution_url"` + CreateTaskNoticeUrl string `json:"create_task_notice_url"` + CreateOperationTaskNoticeUrl string `json:"create_operation_task_notice_url"` + PddTokenUrl string `json:"pdd_token_url"` + DeductionUrl string `json:"deduction_url"` + PddGetGoodsUrl string `json:"pdd_get_goods_url"` + PddGetGoodsDetailUrl string `json:"pdd_get_goods_detail_url"` + PddAddGoodsUrl string `json:"pdd_add_goods_url"` + XianYuAddGoodsUrl string `json:"xianyu_add_goods_url"` + KfzAddGoodsUrl string `json:"kfz_add_goods_url"` + PddGetSkuId string `json:"pdd_get_sku_id"` + DelTaskUrl string `json:"del_task_url"` + BackupUrl string `json:"backup_url"` + PddGoodsDetailsUrl string `json:"pdd_goods_details_url"` + UpdateTokenUrl string `json:"update_token_url"` + KfzImgTempUrl string `json:"kfz_img_temp_url"` + KfzImgHttpUrl string `json:"kfz_img_http_url"` + GetPddGoodsShopIdIsbnUrl string `json:"get_pdd_goods_shopid_isbn_url"` + GetSubscriptionExpirationDateUrl string `json:"get_subscription_expiration_date_url"` + XYBannedWordSubstitutionUrl string `json:"xy_banned_word_substitution_url"` + PddImgTempUrl string `json:"pdd_img_temp_url"` +} diff --git a/type/cost.go b/type/cost.go new file mode 100644 index 0000000..f1bdf4a --- /dev/null +++ b/type/cost.go @@ -0,0 +1,9 @@ +package _type + +//费用结构体 + +// TaskDeductionResponse 扣费结构体 +type TaskDeductionResponse struct { + Code int `json:"code"` // 响应状态码 + Msg string `json:"msg"` // 响应消息 +} diff --git a/type/mysql/delTask.go b/type/mysql/delTask.go new file mode 100644 index 0000000..115de55 --- /dev/null +++ b/type/mysql/delTask.go @@ -0,0 +1,68 @@ +package mysql + +import ( + "time" + + "gorm.io/gorm" +) + +// DelTask 删除任务表 +// 对应数据库中的 del_task 表 +type DelTask struct { + // ID 主键,自增 + ID int64 `gorm:"column:id;type:int(11);primaryKey;autoIncrement;comment:主键ID" json:"id"` + + // UserID 用户ID + UserID *string `gorm:"column:user_id;type:varchar(64);index:idx_user_shop_task;comment:用户ID" json:"user_id,omitempty"` + + // ShopID 店铺ID + ShopID *string `gorm:"column:shop_id;type:bigint(64);index:idx_user_shop_task;comment:店铺ID" json:"shop_id,omitempty"` + + // TaskID 任务ID + TaskID *string `gorm:"column:task_id;type:varchar(64);index:idx_user_shop_task;comment:任务ID" json:"task_id,omitempty"` + + // ShopType 任务类型 + ShopType *string `gorm:"column:shop_type;type:varchar(1);index:idx_user_shop_task;comment:店铺类型 1=拼多多店铺 2=孔夫子 5=闲鱼 6=淘宝" json:"shop_type,omitempty"` + + // pid pid + Pid *string `gorm:"column:pid;type:varchar(64);index:idx_user_shop_task;comment:pid" json:"pid,omitempty"` + + // TaskType 任务类型 + TaskType *int `gorm:"column:task_type;type:int(11);index:idx_user_shop_task;comment:任务类型 1=常规删除 2=数量删除 3=时间删除" json:"task_type,omitempty"` + + // ShopName 店铺名称 + ShopName *string `gorm:"column:shop_name;type:varchar(128);index:idx_user_shop_task;comment:店铺名称" json:"shop_name,omitempty"` + + // TaskCount 任务数 + TaskCount *int `gorm:"column:task_count;type:int(11);index:idx_user_shop_task;comment:任务数" json:"task_count,omitempty"` + + // TaskCountOver 完成任务数 + TaskCountOver *int `gorm:"column:task_count_over;type:int(11);index:idx_user_shop_task;comment:完成任务数" json:"task_count_over,omitempty"` + + // Status 状态 + Status *int `gorm:"column:status;type:int(11);index:idx_user_shop_task;comment:状态 1=执行中 2=暂停 3=完成" json:"status,omitempty"` + + // header 任务头信息 + Header *string `gorm:"column:header;type:text;comment:任务头" json:"header,omitempty"` + + // pauseAt 暂停时间 + PauseAt *time.Time `gorm:"column:pause_at;type:datetime;comment:暂停时间" json:"pause_at"` + + // StopAt 终止时间 + StopAt *time.Time `gorm:"column:stop_at;type:datetime;comment:终止时间(时间删除任务)" json:"stop_at"` + + // CreateAt 创建时间(GORM会自动维护创建时间) + CreateAt *time.Time `gorm:"column:create_at;type:datetime;autoCreateTime;comment:创建时间" json:"create_at,omitempty"` +} + +// TableName 指定结构体对应的数据库表名 +func (t *DelTask) TableName() string { + return "del_task" +} + +// MigrateDelTask 初始化表结构/索引 +// @param db 数据库连接实例 +// @return error 错误信息 +func MigrateDelTask(db *gorm.DB) error { + return db.AutoMigrate(&DelTask{}) +} diff --git a/type/mysql/taskExport.go b/type/mysql/taskExport.go new file mode 100644 index 0000000..293ba12 --- /dev/null +++ b/type/mysql/taskExport.go @@ -0,0 +1,61 @@ +package mysql + +import ( + "database/sql" + _type "planA/type" + "time" + + "gorm.io/gorm" +) + +// PageQueryTaskExportParams 分页查询参数 +type PageQueryTaskExportParams struct { + UserID int64 + Page _type.Page +} + +// TaskExport +// 对应数据库中的 task_export 表 +type TaskExport struct { + // ID 主键,自增 + ID int64 `gorm:"column:id;type:int(11);primaryKey;autoIncrement;comment:主键ID" json:"id"` + + // UserID 用户ID + UserID *string `gorm:"column:user_id;type:varchar(64);index:idx_user_shop_task;comment:用户ID" json:"user_id,omitempty"` + + // ShopID 店铺ID + ShopID *string `gorm:"column:shop_id;type:varchar(64);index:idx_user_shop_task;comment:店铺ID" json:"shop_id,omitempty"` + + // TaskID 任务ID + TaskID *string `gorm:"column:task_id;type:varchar(64);index:idx_user_shop_task;comment:任务ID" json:"task_id,omitempty"` + + // ShopName 店铺名称 + ShopName *string `gorm:"column:shop_name;type:varchar(128);index:idx_user_shop_task;comment:店铺名称" json:"shop_name,omitempty"` + + // FileUrl 导出文件 URL + FileUrl *string `gorm:"column:file_url;type:varchar(256);comment:导出文件URL" json:"file_url"` + + // Status 状态(0:未开始 1:进行中 2:完成) + Status *int64 `gorm:"column:status;type:tinyint(1);default:0;comment:状态(0:未开始 1:进行中 2:完成)" json:"status,omitempty"` + + // Total 总数量 + Total *int64 `gorm:"column:total;type:int(11);comment:总数量" json:"total,omitempty"` + + // CompleteAt 完成时间 + CompleteAt *sql.NullTime `gorm:"column:complete_at;type:datetime;comment:完成时间" json:"complete_at"` + + // CreateAt 创建时间(GORM会自动维护创建时间) + CreateAt *time.Time `gorm:"column:create_at;type:datetime;autoCreateTime;comment:创建时间" json:"create_at,omitempty"` +} + +// TableName 指定结构体对应的数据库表名 +func (t *TaskExport) TableName() string { + return "task_export" +} + +// MigrateTaskExport 初始化表结构/索引 +// @param db 数据库连接实例 +// @return error 错误信息 +func MigrateTaskExport(db *gorm.DB) error { + return db.AutoMigrate(&TaskExport{}) +} diff --git a/type/mysql/taskRecords.go b/type/mysql/taskRecords.go new file mode 100644 index 0000000..9312764 --- /dev/null +++ b/type/mysql/taskRecords.go @@ -0,0 +1,59 @@ +package mysql + +import ( + _type "planA/type" + "time" + + "gorm.io/gorm" +) + +// GetTaskRecordsByUserIdParams 分页查询参数 +type GetTaskRecordsByUserIdParams struct { + UserID string // 要查询的用户ID(必传) + ShopName string // 店铺名称(可选,非空则过滤) + TaskID string // 任务ID(可选,非空则过滤) + TaskType int64 // 任务类型(可选,非空则过滤) + Page _type.Page +} + +// TaskRecords 任务-用户关联表 +// 对应数据库中的 task_record 表 +// TaskRecords 任务-用户关联表 +// 对应数据库中的 task_record 表 +type TaskRecords struct { + // ID 主键,自增 + ID int64 `gorm:"column:id;type:int(11);primaryKey;autoIncrement;comment:主键ID" json:"id"` + + // UserID 用户ID + UserID *string `gorm:"column:user_id;type:varchar(64);index:idx_user_shop_task;comment:用户ID" json:"user_id,omitempty"` + + // ShopID 店铺ID + ShopID *string `gorm:"column:shop_id;type:varchar(64);index:idx_user_shop_task;comment:店铺ID" json:"shop_id,omitempty"` + + // TaskID 任务ID + TaskID *string `gorm:"column:task_id;type:varchar(64);index:idx_user_shop_task;comment:任务ID" json:"task_id,omitempty"` + + // ShopName 店铺名称 + ShopName *string `gorm:"column:shop_name;type:varchar(128);index:idx_user_shop_task;comment:店铺名称" json:"shop_name,omitempty"` + + // IsExport 是否导出,默认为false(0) + IsExport *int64 `gorm:"column:is_export;type:tinyint(1);default:0;comment:是否导出(0:否 1:是)" json:"is_export,omitempty"` + + // TaskType 任务类型,默认为 1 核价发布 2 表格发布 3:拉取商品 4:拉取商品详情 + TaskType *int64 `gorm:"column:task_type;type:tinyint(1);default:1;comment:任务类型(1:核价发布 2:表格发布 3:拉取商品 4:拉取商品详情)" json:"task_type,omitempty"` + + // CreateAt 创建时间(GORM会自动维护创建时间) + CreateAt *time.Time `gorm:"column:create_at;type:datetime;autoCreateTime;comment:创建时间" json:"create_at,omitempty"` +} + +// TableName 指定结构体对应的数据库表名 +func (t *TaskRecords) TableName() string { + return "task_records" +} + +// MigrateTaskRecords 初始化表结构/索引 +// @param db 数据库连接实例 +// @return error 错误信息 +func MigrateTaskRecords(db *gorm.DB) error { + return db.AutoMigrate(&TaskRecords{}) +} diff --git a/type/pdd.go b/type/pdd.go new file mode 100644 index 0000000..ebbbdaa --- /dev/null +++ b/type/pdd.go @@ -0,0 +1,14 @@ +package _type + +//拼多多结构体 + +// DllGoodsSpec 拼多多接口 PddGoodsSpecIdGet 返回结构体 +type DllGoodsSpec struct { + DllGoodsSpec GoodsSpec `json:"goods_spec_id_get_response"` +} +type GoodsSpec struct { + ParentSpecID int64 `json:"parent_spec_id"` + RequestID string `json:"request_id"` + SpecID int64 `json:"spec_id"` + SpecName string `json:"spec_name"` +} diff --git a/type/psiMysql/bookInfo.go b/type/psiMysql/bookInfo.go new file mode 100644 index 0000000..5419e05 --- /dev/null +++ b/type/psiMysql/bookInfo.go @@ -0,0 +1,28 @@ +package psiMysql + +// BookInfo 书籍信息表结构体 +type BookInfo struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id" db:"id"` // 自增ID + FID int64 `gorm:"column:fid;not null;default:0" json:"fid" db:"fid"` // 父级ID + Type int8 `gorm:"column:type;not null" json:"type" db:"type"` // 类型 1正常 2套装书 3 一号多书 4无书号 + ISBN string `gorm:"column:isbn;not null;default:'';size:20" json:"isbn" db:"isbn"` // ISBN + BookName string `gorm:"column:book_name;not null;default:'';size:100" json:"book_name" db:"book_name"` // 书名 + Author string `gorm:"column:author;not null;default:'';size:100" json:"author" db:"author"` // 作者 + Publishing string `gorm:"column:publishing;not null;default:'';size:50" json:"publishing" db:"publishing"` // 出版社 + PublicationDate string `gorm:"column:publication_date;not null;default:'';size:10" json:"publication_date" db:"publication_date"` // 出版日期 + PublicationTime int64 `gorm:"column:publication_time;not null;default:0" json:"publication_time" db:"publication_time"` // 出版日期时间戳 + Binding string `gorm:"column:binding;not null;default:'';size:10" json:"binding" db:"binding"` // 装帧 + PagesCount int64 `gorm:"column:pages_count;not null;default:0" json:"pages_count" db:"pages_count"` // 页数 + WordsCount int64 `gorm:"column:words_count;not null;default:0" json:"words_count" db:"words_count"` // 字数 + Format int64 `gorm:"column:format;not null;default:0" json:"format" db:"format"` // 开本 + Price int64 `gorm:"column:price;not null;default:0" json:"price" db:"price"` // 价格 + CatID string `gorm:"column:cat_id;type:json;not null" json:"cat_id" db:"cat_id"` // 类目json + FISBN string `gorm:"column:fisbn;not null;default:'';size:20" json:"fisbn" db:"fisbn"` // FISBN + FBookName string `gorm:"column:f_book_name;not null;default:'';size:100" json:"f_book_name" db:"f_book_name"` // 副书名 + LiveImage string `gorm:"column:live_image;type:json;not null" json:"live_image" db:"live_image"` // 实拍图json +} + +// TableName 指定表名 +func (BookInfo) TableName() string { + return "book_info" +} diff --git a/type/redis/redis.go b/type/redis/redis.go new file mode 100644 index 0000000..bb75e62 --- /dev/null +++ b/type/redis/redis.go @@ -0,0 +1,9 @@ +package redis + +import "encoding/json" + +// RedisData 原始Redis数据结构 +type RedisData struct { + SourceTable string `json:"source_table"` + Data json.RawMessage `json:"data"` +} diff --git a/type/shop.go b/type/shop.go new file mode 100644 index 0000000..3772fce --- /dev/null +++ b/type/shop.go @@ -0,0 +1,118 @@ +package _type + +// 店铺结构体 + +// ShopInfo 完整的店铺信息 +type ShopInfo struct { + Shop *Shop `json:"shop"` + ShopDetail *ShopDetail `json:"shop_detail"` + ShopContext *ShopContext `json:"shop_context"` + Spec *Spec `json:"spec"` + PriceTemplate *PriceTemplate `json:"price_template"` +} + +// Shop 店铺基本信息 +type Shop struct { + ID string `json:"id"` + ShopKey string `json:"shop_key"` + ShopName string `json:"shop_name"` + ShopAliasName string `json:"shop_alias_name"` + ShopType string `json:"shop_type"` + ShopAuthorize string `json:"shop_authorize"` + Status string `json:"status"` + MallID int64 `json:"mall_id"` + Token string `json:"token"` + RefreshToken interface{} `json:"refresh_token"` + CreateTime string `json:"create_time"` + UpdateTime string `json:"update_time"` + ExpirationTime string `json:"expiration_time"` + PublishType string `json:"publish_type"` + SkuSpec string `json:"sku_spec"` + StartUpdatedAt int64 `json:"start_updated_at"` + TenantID string `json:"tenant_id"` + DelFlag string `json:"del_flag"` // 逻辑删 + CreateBy string `json:"create_by" db:"create_by"` // 创建人 + Deregulation string `json:"deregulation"` +} + +// ShopDetail 店铺详情 +type ShopDetail struct { + ID string `json:"id"` + ShopID string `json:"shop_id"` //店铺 ID + SaleTemplateID string `json:"sale_template_id"` //运费模版 ID + LowPrice int `json:"low_price"` //最低价格 + HighPrice int `json:"high_price"` //最高价格 + StockDeff int `json:"stock_deff"` //库存 + TemplateId string `json:"template_id"` //物流运费模版 ID + TitlePrefix string `json:"title_prefix"` //标题前缀 + TitleSuffix string `json:"title_suffix"` //标题后缀 + TitleConsistOf string `json:"title_consist_of"` //标题包含信息 + SpaceCharacter string `json:"space_character"` //是否使用空格 + SevenDays string `json:"seven_days"` //是否支持7天无理由退换货 + Presale string `json:"presale"` //是否预售 + Fake string `json:"fake"` //是否支持假一赔十,false-不支持,true-支持 + IsPreSale string `json:"is_pre_sale"` //是否预售,true-预售商品,false-非预售商品 + IsRefundable bool `json:"is_refundable"` //是否7天无理由退换货,true-支持,false-不支持 + IsSecondHand string `json:"is_second_hand"` //是否二手 1 -二手商品 ,0-全新商品 + ShipmentLimitSecond string `json:"shipment_limit_second"` //承诺发货时间(秒) + CostTemplateId int64 `json:"cost_template_id"` //物流运费模板 ID + TowDiscount int64 `json:"two_discount"` //两件折扣 + WatermarkImgUrl string `json:"watermark_img_url"` //水印图片链接 + WatermarkPosition string `json:"watermark_position"` //水印位置 0全部 1第一张 + DistrictId int64 `json:"district_id"` //地区类型 0 指定区县 1 指定省 2 全国 + DistrictType string `json:"district_type"` //地区 ID 【district_type=0 区县ID district_type=1 省ID district_type=2 全国(空值)】 + CarouseLastImgUrlArray []string `json:"carouse_last_img_url_array"` //轮播图最后图片 + GoodsDetailFirstImgUrlArray []string `json:"goods_detail_first_img_url_array"` //商品详情首图 URL 数组 + GoodsDetailLastImgUrlArray []string `json:"goods_detail_last_img_url_array"` //商品详情最后图片 URL 数组 + SkuWatermarkImgUrl string `json:"sku_watermark_img_url"` //sku 水印图片链接 + PublishType string `json:"publish_type"` //发布方式 0=24(图书类目) 1=99(其他类目)【限闲鱼店铺使用】 + CategoryId string `json:"category_id"` //类目 ID【限闲鱼店铺使用】 + IsParcel string `json:"is_parcel"` //是否包邮 0不包邮 1 包邮 + BookWeight int64 `json:"book_weight"` //图书重量【限孔夫子使用】 + StandardNumber int64 `json:"standard_number"` //商品标准本数【限孔夫子使用】 + ConditionDef int64 `json:"condition_def"` //品相【限孔夫子使用】 +} + +// ShopContext 店铺上下文 +type ShopContext struct { + ID string `json:"id"` + ShopID string `json:"shop_id"` + Context string `json:"context"` + CreateTime string `json:"create_time"` + UpdateTime string `json:"update_time"` +} + +// Spec 规格信息 +type Spec struct { + ID string `json:"id"` + ShopID string `json:"shop_id"` + SpecName string `json:"spec_name"` + SpecTypeID string `json:"spec_type_id"` + SpecTypeName string `json:"spec_type_name"` + SpecCompose string `json:"spec_compose"` + SpecPrefix string `json:"spec_prefix"` + SpecSuffix string `json:"spec_suffix"` + SpecCodeCompose string `json:"spec_code_compose"` + CreateTime string `json:"create_time"` + UpdateTime string `json:"update_time"` +} + +// PriceTemplate 价格模板 +type PriceTemplate struct { + AddAmount int `json:"add_amount"` + DelFlag string `json:"del_flag"` + HighPrice int64 `json:"high_price"` + ID string `json:"id"` + LowPrice int64 `json:"low_price"` + PriceType string `json:"price_type"` + Proportion int `json:"proportion"` + RangePrice string `json:"range_price"` // 解析后的价格区间 + Status string `json:"status"` + TemplateName string `json:"template_name"` +} + +// TbShop 淘宝店铺结构体 +type TbShop struct { + ShopID string `json:"shop_id"` + ShopName string `json:"shop_name"` +} diff --git a/type/sqLite/taskExport.go b/type/sqLite/taskExport.go new file mode 100644 index 0000000..198d7a5 --- /dev/null +++ b/type/sqLite/taskExport.go @@ -0,0 +1,21 @@ +package sqLite + +import ( + "database/sql" + "time" +) + +// TaskExport 定义任务导出表(task_export)对应的结构体 +// 该表用于存储任务导出的详细信息 +type TaskExport struct { + ID int64 // 主键 ID + UserID string // 用户 ID + ShopID string // 店铺 ID + TaskID string // 任务 ID + ShopName string // 店铺名称 + FileUrl string // 导出文件 URL + Status int64 // 状态(0:未开始 1:进行中 2:完成) + Total int64 // 总数量 + CompleteAt sql.NullTime // 完成时间 + CreateAt time.Time // 创建时间 +} diff --git a/type/sqLite/taskRecords.go b/type/sqLite/taskRecords.go new file mode 100644 index 0000000..899de81 --- /dev/null +++ b/type/sqLite/taskRecords.go @@ -0,0 +1,28 @@ +package sqLite + +import ( + _type "planA/type" + "time" +) + +// TaskRecords 定义任务记录表(task_records)对应的结构体 +// 该表用于存储任务的基本信息 +type TaskRecords struct { + ID int64 // 主键 ID + UserID string // 用户 ID + ShopID string // 店铺 ID + TaskID string // 任务 ID + ShopName string // 店铺名称 + IsExport int64 // 是否已导出(0:未导出 1:已导出) + TaskType int64 // 任务类型(1:核价发布 2:表格发布 3:拉取商品 4:拉取商品详情) + CreateAt time.Time // 创建时间 +} + +// GetTaskRecordsByUserIdParams 分页查询参数 +type GetTaskRecordsByUserIdParams struct { + UserID string // 要查询的用户ID(必传) + ShopName string // 店铺名称(可选,非空则过滤) + TaskID string // 任务ID(可选,非空则过滤) + TaskType int64 // 任务类型(可选,非空则过滤) + Page _type.Page +} diff --git a/type/task.go b/type/task.go new file mode 100644 index 0000000..92d45b2 --- /dev/null +++ b/type/task.go @@ -0,0 +1,278 @@ +package _type + +import ( + "encoding/json" + "fmt" + "strconv" + "sync/atomic" + "time" +) + +// 任务结构体 + +// Task 关键数据结构 +type Task struct { + Header TaskHeader `json:"header"` // 任务头 + BodyWait TaskBody `json:"body_wait"` // 任务队列 + BodyOver TaskBody `json:"body_over"` // 已完成任务队列 + Footer TaskFooter `json:"footer"` // 任务尾 +} + +// TaskHeader 任务头结构 +type TaskHeader struct { + TaskId string `json:"task_id"` // 任务 ID + TaskType int64 `json:"task_type"` // 任务类型 + ShopId string `json:"shop_id"` // 店铺 ID + ShopName string `json:"shop_name"` // 店铺名称 + ShopType string `json:"shop_type"` // 店铺类型 + ShopMsg ShopMsg `json:"shop_msg"` // 店铺信息 + PriceMod []PriceMod `json:"price_mod"` // 价格模版 + PriceType string `json:"price_type"` // 价格类型 + ShipPriceMod string `json:"ship_price_mod"` // 运费模版 + TaskCount int64 `json:"task_count"` // 任务数量 + TaskCountTrue int64 `json:"task_count_true"` // 任务数量(真实) + TaskCountWait int64 `json:"task_count_wait"` // 任务数量(等待) + TaskCountOver int64 `json:"task_count_over"` // 任务数量(结束) + TaskCountSuccess int64 `json:"task_count_success"` // 任务数量(成功) + TaskCountError int64 `json:"task_count_error"` // 任务数量(错误) + Status TaskStatus `json:"status"` // 任务状态 + TaskQpm int64 `json:"task_qpm"` // 任务 QPM + TaskCreateAt int64 `json:"task_create_at"` // 任务创建时间 + TaskOverAt int64 `json:"task_over_at"` // 任务结束时间 + LastIndex int64 `json:"last_index"` // 最后任务索引(记录程序集错误 10001=body_wait中没有数据一致读取,11002=店铺发布商品已达到上限,10003=过滤关键词异常) + ImgType int64 `json:"img_type"` // 图片类型 1仅官图 2 实拍图 3 优先官图 4 优先实拍图 + UpdateType int64 `json:"update_type"` // 更新方式(仅核价发布或核价表格发布使用) 1 过滤重复 2 全新上传 + Pool PoolConfig `json:"pool"` // 线程池配置 +} + +// TaskBody 任务主体结构 +type TaskBody struct { + BookInfo BookInfo `json:"book_info"` + Detail TaskDetail `json:"detail"` + Publishing Publishing `json:"publishing"` //出版社信息 +} + +// BookInfo 书籍信息结构 +type BookInfo struct { + Isbn string `json:"isbn"` // ISBN + BookName string `json:"book_name"` // 书名 + Author string `json:"author"` // 作者 + Publishing string `json:"publishing"` // 出版社 + PublicationDate string `json:"publication_date"` // 出版时间 + Binding string `json:"binding"` // 装帧 + PagesCount int64 `json:"pages_count"` // 页数 + WordsCount int64 `json:"words_count"` // 字数 + Format int64 `json:"format"` // 开本 + ImageObject ImageObject `json:"image_object"` // 图片 + Price int64 `json:"price"` // 售价 + CatIdObject CatIdObject `json:"cat_id"` // 分类 (冗余 保留下原始字符串) +} + +// TaskFooter 任务项结构 +type TaskFooter struct { + TaskCount int64 `json:"task_count"` // 任务数量 + TaskCountTrue int64 `json:"task_count_true"` // 任务数量(真实) + TaskCountWait atomic.Int64 `json:"task_count_wait"` // 任务数量(等待) + TaskCountOver atomic.Int64 `json:"task_count_over"` // 任务数量(结束) + TaskCountSuccess atomic.Int64 `json:"task_count_success"` // 任务数量(成功) + TaskCountError atomic.Int64 `json:"task_count_error"` // 任务数量(错误) + TaskQpm int64 `json:"task_qpm"` // 任务 QPM + LastIndex int64 `json:"last_index"` // 最后任务索引 +} + +// ShopMsg 店铺信息结构体 +type ShopMsg struct { + ID string `json:"id"` + ShopAliasName string `json:"shop_alias_name"` + ShopName string `json:"shop_name"` + Token string `json:"token"` //店铺 token 店铺类型=拼多多店铺,此token则是常规token 店铺类型=咸鱼店铺,此token则是【应用Id:应用密钥】 + GoodsNamePrefix string `json:"goods_name_prefix"` //店铺名称前缀 + GoodsNameSuffix string `json:"goods_name_suffix"` //店铺名称后缀 + TitleConsistOf string `json:"title_consist_of"` //标题包含信息 如:作者、出版社等等 + SpaceCharacter string `json:"space_character"` //是否使用空格 1为使用 + WatermarkImgUrl string `json:"watermark_img_url"` //水印图片链接 + WatermarkPosition string `json:"watermark_position"` //水印位置 水印位置 0全部 1第一张 + CarouseLastImgUrlArray []string `json:"carouse_last_img_url_array"` //轮播图最后图片 + GoodsDetailFirstImgUrlArray []string `json:"goods_detail_first_img_url_array"` //商品详情首图 URL 数组 + GoodsDetailLastImgUrlArray []string `json:"goods_detail_last_img_url_array"` //商品详情最后图片 URL 数组 + SpecName string `json:"spec_name"` //规格名称 + SpecId int64 `json:"spec_id"` //规格 ID + SpecChildName string `json:"spec_child_name"` //子规格名称 + SpecCompose string `json:"spec_compose"` //规格组合类型 0=自定义 1=Isbn 2=书名 3=货号 + IsFolt bool `json:"is_fotl"` //是否支持假一赔十,false-不支持,true-支持 + SpecPrefix string `json:"spec_prefix"` //规格前缀 + SpecSuffix string `json:"spec_suffix"` //规格后缀 + PublishType string `json:"publish_type"` //发布方式 0=24(图书类目) 1=99(其他类目)【限闲鱼店铺使用】 + CategoryId string `json:"category_id"` //类目 ID【限闲鱼店铺使用】 + IsPreSale bool `json:"is_pre_sale"` //是否预售,true-预售商品,false-非预售商品 + IsRefundable bool `json:"is_refundable"` //是否7天无理由退换货,true-支持,false-不支持 + IsSecondHand bool `json:"is_second_hand"` //是否二手 true -二手商品 ,false-全新商品 + ShipmentLimitSecond int64 `json:"shipment_limit_second"` //承诺发货时间(秒) + CostTemplateId string `json:"cost_template_id"` //物流运费模板 ID + DefStock int32 `json:"def_stock"` // 默认库存 + TwoDiscount int64 `json:"two_discount"` // 两件折扣 + DistrictMsg DistrictMsg `json:"district_msg"` // 地区信息【限闲鱼使用】 + ShopContext string `json:"shop_context"` // 店铺描述 + SkuWatermarkImgUrl string `json:"sku_watermark_img_url"` //sku 水印图片链接 + IsParcel string `json:"is_parcel"` //是否包邮 0不包邮 1 包邮【限孔夫子使用】 + BookWeight int64 `json:"book_weight"` //图书重量【限孔夫子使用】 + StandardNumber int64 `json:"standard_number"` //商品标准本数【限孔夫子使用】 + ConditionDef int64 `json:"condition_def"` // 默认品相【限孔夫子使用】 + SpecCodeCompose string `json:"spec_code_compose"` //规格编码组合类型 0=货号 1=Isbn +} + +// PriceMod 价格模版结构体 +type PriceMod struct { + Min int64 `json:"min"` // 价格区间最小值 + Max int64 `json:"max"` // 价格区间最大值 + MarkupRate int64 `json:"markup_rate"` // 加价比例 + MarkupValue int64 `json:"markup_value"` // 价格区间加价值 +} + +// TaskStatus 任务状态 +type TaskStatus int64 + +const ( + TaskStatusRunning TaskStatus = 1 // 运行中 + TaskStatusPaused TaskStatus = 2 // 已暂停 + TaskStatusStopped TaskStatus = 3 // 已停止 + TaskStatusOver TaskStatus = 4 // 已完成 + TaskStatusPushTaskStatus = 10 // 推送中(拉取任务) +) + +// TaskDetail 详情结构 +type TaskDetail struct { + Condition int64 `json:"condition"` // 品相 + Price int64 `json:"price"` // 价格 + Stock int32 `json:"stock"` // 库存 + Status int64 `json:"status"` // 状态 0=失败 1=成功 + Error string `json:"error"` // 错误信息 + GoodsId int64 `json:"goods_id"` // 商品 ID + ReturnId int64 `json:"return_id"` // 拼多多返回 ID + SkuCode string `json:"sku_code"` // 规格编码(sku维度) + SkuId int64 `json:"sku_id"` // sku 编码(货号) + Img string `json:"img"` // 图片 + OutGoodsId string `json:"out_goods_id"` // 商品编码 + GoodsName string `json:"goods_name"` // 商品名称 + IsOnsale int64 `json:"is_onsale"` // 是否上架 0=上架状态,1=下架状态 + ShippingCost int64 `json:"shipping_cost"` // 运费 + Msg string `json:"msg"` // 消息 +} + +// ImageObject 图片对象结构 +type ImageObject struct { + CarouselUrlArray []string `json:"carousel_url_array"` // 轮播图 + WhiteBackgroundUrl string `json:"white_background_url"` // 白底图 + DetailUrlObject DetailImageObject `json:"detail_url_object"` // 详情对象 + DefaultImageUrl string `json:"default_image_url"` // 默认图 +} + +// CatIdObject 平台分类结构 +type CatIdObject struct { + PinDuoDuoCatId FlexibleStr `json:"pin_duo_duo_cat_id"` // 拼多多分类 ID + KongFuZiCatId FlexibleStr `json:"kong_fu_zi_cat_id"` // 孔夫子分类 ID + XianYuCatId FlexibleStr `json:"xian_yu_cat_id"` // 闲鱼分类 ID +} + +// FlexibleInt64 ====================== 临时 ====================== +type FlexibleStr string + +// UnmarshalJSON 反序列化:接受数字、布尔值、字符串等任意类型,都转换为字符串 +func (fi *FlexibleStr) UnmarshalJSON(data []byte) error { + // 1. 尝试直接解析为字符串 + var s string + if err := json.Unmarshal(data, &s); err == nil { + *fi = FlexibleStr(s) + return nil + } + + // 2. 尝试解析为数字 + var num json.Number + if err := json.Unmarshal(data, &num); err == nil { + *fi = FlexibleStr(num.String()) + return nil + } + + // 3. 尝试解析为布尔值 + var b bool + if err := json.Unmarshal(data, &b); err == nil { + *fi = FlexibleStr(strconv.FormatBool(b)) + return nil + } + + // 4. 其他任意类型 + var any interface{} + if err := json.Unmarshal(data, &any); err != nil { + return err + } + + // 将任意类型转为字符串 + *fi = FlexibleStr(fmt.Sprintf("%v", any)) + return nil +} + +// MarshalJSON 序列化:总是输出为字符串 +func (fi FlexibleStr) MarshalJSON() ([]byte, error) { + return json.Marshal(string(fi)) +} + +// String 实现 Stringer 接口 +func (fi FlexibleStr) String() string { + return string(fi) +} + +// ToInt64 如果需要转换为 int64 +func (fi FlexibleStr) ToInt64() (int64, error) { + return strconv.ParseInt(string(fi), 10, 64) +} + +// ToFloat64 如果需要转换为 float64 +func (fi FlexibleStr) ToFloat64() (float64, error) { + return strconv.ParseFloat(string(fi), 64) +} + +// ToBool 如果需要转换为 bool +func (fi FlexibleStr) ToBool() (bool, error) { + return strconv.ParseBool(string(fi)) +} + +// FlexibleInt64 ====================== 临时 ====================== +// DetailImageObject 详情图片对象结构 +type DetailImageObject struct { + IntroductionUrl []string `json:"introduction_url"` // 简介图 + CatalogueUrl []string `json:"catalogue_url"` // 目录图 + LiveShootingUrl []string `json:"live_shooting_url"` // 实拍图 + OtherUrl []string `json:"other_url"` // 其他图 +} + +// PriceRange 价格区间 +type PriceRange struct { + MinPrice int64 `json:"minPrice"` + MaxPrice int64 `json:"maxPrice"` + AdjustPercent interface{} `json:"adjustPercent"` // 可能是 int 或 string + AdjustAmount int64 `json:"adjustAmount"` +} + +// DistrictMsg 地区信息 +type DistrictMsg struct { + DistrictId int64 `json:"district_id"` + DistrictType string `json:"district_type"` +} + +// Publishing Redis中存储的出版社信息结构体 +type Publishing struct { + Value string `json:"value"` + Vid int64 `json:"vid"` +} + +type GetSubscriptionExpirationDateUrl struct { + Code int `json:"code"` + Message string `json:"message"` + Data GetSubscriptionExpirationDateUrlData `json:"data"` + Timestamp time.Time `json:"timestamp"` // 也可使用 time.Time 类型,根据实际需要选择 +} + +type GetSubscriptionExpirationDateUrlData struct { + ExpirationDate int64 `json:"expirationDate"` + IsVip bool `json:"isVip"` +} diff --git a/type/taskExport.go b/type/taskExport.go new file mode 100644 index 0000000..886acef --- /dev/null +++ b/type/taskExport.go @@ -0,0 +1,27 @@ +package _type + +import ( + "database/sql" + "time" +) + +// TaskExportDTO 导出任务结构体 +type TaskExportDTO struct { + Id int64 `json:"id"` + UserId string `json:"user_id"` + ShopId string `json:"shop_id"` + TaskId string `json:"task_id"` + ShopName string `json:"shop_name"` + FileUrl string `json:"file_url"` + Status int64 `json:"status"` + Total int64 `json:"total"` + CompleteAt sql.NullTime `json:"complete_at"` + CreateAt time.Time `json:"create_at"` +} + +// GetTaskExportListReq 获取导出任务列表 +type GetTaskExportListReq struct { + UserId int64 `json:"user_id"` + Page int + Size int +} diff --git a/type/taskRecord.go b/type/taskRecord.go new file mode 100644 index 0000000..f1d4bcd --- /dev/null +++ b/type/taskRecord.go @@ -0,0 +1,31 @@ +package _type + +import "time" + +// TaskRecordsDTO 任务记录 +type TaskRecordsDTO struct { + Id int64 `json:"id"` + UserId string `json:"user_id"` + ShopId string `json:"shop_id"` + TaskId string `json:"task_id"` + ShopName string `json:"shop_name"` + IsExport int64 `json:"is_export"` + TaskType int64 `json:"task_type"` + CreateAt time.Time `json:"create_at"` +} + +// GetTaskRecordsListReq 获取任务记录列表 +type GetTaskRecordsListReq struct { + UserId string `json:"user_id"` + TaskId string `json:"task_id"` + TaskType int64 `json:"task_type"` + ShopName string `json:"shop_name"` + Page int + Size int +} + +// GetTaskRecordsByTaskId 获取任务记录 +type GetTaskRecordsByTaskId struct { + UserId int64 `json:"user_id"` + TaskId string `json:"task_id"` +} diff --git a/type/type.go b/type/type.go new file mode 100644 index 0000000..b8a2017 --- /dev/null +++ b/type/type.go @@ -0,0 +1,7 @@ +package _type + +type HealthResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Code int `json:"code"` +} diff --git a/type/validator/body.go b/type/validator/body.go new file mode 100644 index 0000000..ba714a2 --- /dev/null +++ b/type/validator/body.go @@ -0,0 +1,12 @@ +package validator + +// InsterBodyOver 像bodyOver中插入一条数据 +type InsterBodyOver struct { + TaskId string `form:"task_id" validate:"required"` //必填 + Data string `form:"data" validate:"required"` //必填 +} + +// GetTaskIdToBody 获取与body +type GetTaskIdToBody struct { + TaskId string `form:"task_id" validate:"required"` //必填 +} diff --git a/type/validator/delTask.go b/type/validator/delTask.go new file mode 100644 index 0000000..c6d9980 --- /dev/null +++ b/type/validator/delTask.go @@ -0,0 +1,57 @@ +package validator + +// GetDelTask 获取任务列表结构体 +type GetDelTask struct { + Page string `form:"page"` + Size string `form:"size"` +} + +// GetDelTaskByUserId 获取任务列表结构体 +type GetDelTaskByUserId struct { + UserId string `form:"userId"` + Page string `form:"page"` + Size string `form:"size"` +} + +// GetDelTaskDetail 获取任务详情列表 +type GetDelTaskDetail struct { + TaskId string `form:"taskId"` + Page string `form:"page"` + Size string `form:"size"` +} + +// CreateTbDelTask 创建淘宝删除任务结构体 +type CreateTbDelTask struct { + ShopID string `form:"shop_id" validate:"required,min=3,max=20"` + TaskType string `form:"task_type" validate:"required,oneof=1 2 3"` +} + +// CreateTbDelTaskDetails 插入淘宝删除任务数据c +type CreateTbDelTaskDetails struct { + TaskID string `form:"task_id" validate:"required"` + Isbn string `form:"isbn" validate:"required"` + BookName string `form:"title" validate:"required"` + GoodsId string `form:"goods_id" validate:"required"` + Status string `form:"status"` + Err string `form:"err" validate:"required"` +} + +// UpdateTbDelTaskDetailsStatus 修改指定淘宝删除任务详情状态 +type UpdateTbDelTaskDetailsStatus struct { + TaskID string `form:"task_id" validate:"required"` + GoodsId string `form:"goods_id" validate:"required"` + Status string `form:"status" validate:"required,oneof=1 2"` + Err string `form:"err" validate:"required"` +} + +// UpdateTbDelTaskProgress 修改删除任务进度验证 +type UpdateTbDelTaskProgress struct { + TaskID string `form:"task_id" validate:"required"` + Num string `form:"num" validate:"required"` +} + +// UpdateTbDelTaskStatus 修改删除任务状态验证 +type UpdateTbDelTaskStatus struct { + TaskID string `form:"task_id" validate:"required"` + Status string `form:"status" validate:"required,oneof=1 2"` +} diff --git a/type/validator/export.go b/type/validator/export.go new file mode 100644 index 0000000..52dde82 --- /dev/null +++ b/type/validator/export.go @@ -0,0 +1,25 @@ +package validator + +// GetExportTask 获取导出任务列表结构体 +type GetExportTask struct { + Page string `form:"page"` + Size string `form:"size"` +} + +// GetExportTaskByUserId 获取导出任务列表结构体-用户 +type GetExportTaskByUserId struct { + UserID string `form:"user_id" validate:"required"` + Page string `form:"page"` + Size string `form:"size"` +} + +// ExportTaskDetail 根据任务 id导出任务详情结构体 +type ExportTaskDetail struct { + TaskID string `form:"task_id" validate:"required"` //必填 +} + +// ExportTaskDetailByUserId 根据任务 id导出任务详情结构体-用户 +type ExportTaskDetailByUserId struct { + TaskID string `form:"task_id" validate:"required"` //必填 + UserID string `form:"user_id" validate:"required"` //必填 +} diff --git a/type/validator/shop.go b/type/validator/shop.go new file mode 100644 index 0000000..a5c567d --- /dev/null +++ b/type/validator/shop.go @@ -0,0 +1,12 @@ +package validator + +// GetShopInfo 获取店铺信息验证结构体 +type GetShopInfo struct { + ShopId string `form:"shop_id" validate:"required"` //必填 +} + +// CreateTbShop 创建淘宝店铺数据 +type CreateTbShop struct { + ShopId string `form:"shop_id" validate:"required"` + ShopName string `form:"shop_name" validate:"required"` +} diff --git a/type/validator/task.go b/type/validator/task.go new file mode 100644 index 0000000..ab8f440 --- /dev/null +++ b/type/validator/task.go @@ -0,0 +1,58 @@ +package validator + +// CreateTask 创建任务结构体 +type CreateTask struct { + ShopID string `form:"shop_id" validate:"required,min=3,max=20"` // 必填,长度3-20 + ShopType string `form:"shop_type" validate:"required,oneof=1 2 5 6"` // 必填,只能是1、2、5、6 + TaskCount string `form:"task_count" validate:"required,numeric,min=1"` // 必填,数字且最小值为1 + TaskType string `form:"task_type" validate:"required,oneof=1 2 3 4 5 6 7 8 9 10 11"` // 必填,只能是1、2、3、4、5、6、7、8、9、10、11 + ImgType string `form:"img_type" validate:"required,oneof=1 2 3 4"` // 必填,只能是1、2、3、4 + UpdateType string `form:"update_type" ` // 非必填项 + DelNum string `form:"del_num"` //非必填项 + DelTime string `form:"del_time"` //非必填项 +} + +// CreateTbTask 创建淘宝任务结构体 +type CreateTbTask struct { + ShopID string `form:"shop_id" validate:"required,min=3,max=20"` // 必填,长度3-20 + TaskCount string `form:"task_count" validate:"required,numeric,min=1"` // 必填,数字且最小值为1 + TaskType string `form:"task_type" validate:"required,oneof=1 2 3 4 5 6 7 8 9 10 11"` // 必填,只能是1、2、3、4、5、6、7、8、9、10、11 +} + +// UpdateTaskStatus 更改任务状态结构体 +type UpdateTaskStatus struct { + TaskID string `form:"task_id" validate:"required"` //必填 +} + +// GetTask 获取任务列表结构体 +type GetTask struct { + Page string `form:"page"` + Size string `form:"size"` + TaskID string `form:"task_id"` + ShopName string `form:"shop_name"` + TaskType string `form:"task_type"` +} + +// GetTaskByUserId 获取用户任务列表结构体 +type GetTaskByUserId struct { + Page string `form:"page"` + Size string `form:"size"` + TaskID string `form:"task_id"` + ShopName string `form:"shop_name"` + TaskType string `form:"task_type"` + UserID string `form:"user_id" validate:"required"` //必填 +} + +// GetBodyOver 获取bodyOver 结构体 +type GetBodyOver struct { + TaskID string `form:"task_id" validate:"required"` //必填 + Page string `form:"page"` + Size string `form:"size"` +} + +// UpdateTaskProgress 更新任务进度 结构体 +type UpdateTaskProgress struct { + TaskID string `form:"task_id" validate:"required"` //必填 + Status string `form:"status" validate:"required"` //必填 + Num string `form:"num" validate:"required"` //必填 +} diff --git a/type/validator/uploadImg.go b/type/validator/uploadImg.go new file mode 100644 index 0000000..c552eaa --- /dev/null +++ b/type/validator/uploadImg.go @@ -0,0 +1,7 @@ +package validator + +// ImgUploadToPdd 上传图片到拼多多结构体 +type ImgUploadToPdd struct { + ImgUrl string `form:"img_url" validate:"required"` // 必填 + ShopId string `form:"shop_id" validate:"required"` // 必填 +} diff --git a/validator/body.go b/validator/body.go new file mode 100644 index 0000000..2895e77 --- /dev/null +++ b/validator/body.go @@ -0,0 +1,41 @@ +package validator + +import ( + "fmt" + "net/http" + "planA/initialization/golabl" + taskValidator "planA/type/validator" + + "github.com/gorilla/mux" +) + +// GetBodyWaitOneValidator 获取一条body数据参数验证 +func GetBodyWaitOneValidator(data *http.Request) (taskValidator.GetTaskIdToBody, error) { + vars := mux.Vars(data) + taskId := vars["taskId"] + + form := taskValidator.GetTaskIdToBody{ + TaskId: taskId, + } + fieldCN := map[string]string{"taskId": "任务ID"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// InsertTbBodyOver 插入body数据参数验证 +func InsertTbBodyOver(data *http.Request) (taskValidator.InsterBodyOver, error) { + + form := taskValidator.InsterBodyOver{ + TaskId: data.FormValue("task_id"), + Data: data.FormValue("data"), + } + fieldCN := map[string]string{"taskId": "任务ID"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} diff --git a/validator/delTask.go b/validator/delTask.go new file mode 100644 index 0000000..b7bf459 --- /dev/null +++ b/validator/delTask.go @@ -0,0 +1,141 @@ +package validator + +import ( + "fmt" + "net/http" + "planA/initialization/golabl" + taskValidator "planA/type/validator" + + "github.com/gorilla/mux" +) + +// GetDelTaskValidator 获取删除任务列表验证 +func GetDelTaskValidator(data *http.Request) (taskValidator.GetDelTask, error) { + form := taskValidator.GetDelTask{ + Page: data.URL.Query().Get("page"), + Size: data.URL.Query().Get("size"), + } + fieldCN := map[string]string{"Page": "页码", "Size": "每页数量"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// GetDelTaskByUserIdValidator 获取删除任务列表验证 +func GetDelTaskByUserIdValidator(data *http.Request) (taskValidator.GetDelTaskByUserId, error) { + vars := mux.Vars(data) + userId := vars["id"] + + form := taskValidator.GetDelTaskByUserId{ + UserId: userId, + Page: data.URL.Query().Get("page"), + Size: data.URL.Query().Get("size"), + } + fieldCN := map[string]string{"Page": "页码", "Size": "每页数量"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// GetDelTaskDetailValidator 获取删除任务详情列表验证 +func GetDelTaskDetailValidator(data *http.Request) (taskValidator.GetDelTaskDetail, error) { + vars := mux.Vars(data) + taskId := vars["id"] + + form := taskValidator.GetDelTaskDetail{ + TaskId: taskId, + Page: data.URL.Query().Get("page"), + Size: data.URL.Query().Get("size"), + } + fieldCN := map[string]string{"Page": "页码", "Size": "每页数量"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// CreateTbDelTaskValidator 创建淘宝删除任务 +func CreateTbDelTaskValidator(data *http.Request) (taskValidator.CreateTbDelTask, error) { + + form := taskValidator.CreateTbDelTask{ + ShopID: data.FormValue("shop_id"), + TaskType: data.FormValue("task_type"), + } + fieldCN := map[string]string{"shop_id": "店铺id", "task_type": "任务类型"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// CreateTbDelTaskDetailsValidator 插入淘宝删除任务数据 +func CreateTbDelTaskDetailsValidator(data *http.Request) (taskValidator.CreateTbDelTaskDetails, error) { + + form := taskValidator.CreateTbDelTaskDetails{ + TaskID: data.FormValue("task_id"), + Isbn: data.FormValue("isbn"), + BookName: data.FormValue("book_name"), + GoodsId: data.FormValue("goods_id"), + Status: data.FormValue("status"), + Err: data.FormValue("err"), + } + fieldCN := map[string]string{"task_id": "任务id", "isbn": "ISBN", "book_name": "书名", "goods_id": "商品id", "status": "状态", "err": "错误信息"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// UpdateTbDelTaskDetailsStatusValidator 修改指定淘宝删除任务详情状态 +func UpdateTbDelTaskDetailsStatusValidator(data *http.Request) (taskValidator.UpdateTbDelTaskDetailsStatus, error) { + + form := taskValidator.UpdateTbDelTaskDetailsStatus{ + TaskID: data.FormValue("task_id"), + GoodsId: data.FormValue("goods_id"), + Status: data.FormValue("status"), + Err: data.FormValue("err"), + } + fieldCN := map[string]string{"task_id": "任务id", "goods_id": "商品id", "status": "状态", "err": "Err不能为空"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// UpdateTbDelTaskProgressValidator 修改指定任务进度 +func UpdateTbDelTaskProgressValidator(data *http.Request) (taskValidator.UpdateTbDelTaskProgress, error) { + + form := taskValidator.UpdateTbDelTaskProgress{ + TaskID: data.FormValue("task_id"), + Num: data.FormValue("num"), + } + fieldCN := map[string]string{"task_id": "任务id", "num": "增加进度"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// UpdateTbDelTaskStatusValidator 修改指定任务状态 +func UpdateTbDelTaskStatusValidator(data *http.Request) (taskValidator.UpdateTbDelTaskStatus, error) { + + form := taskValidator.UpdateTbDelTaskStatus{ + TaskID: data.FormValue("task_id"), + Status: data.FormValue("status"), + } + fieldCN := map[string]string{"task_id": "任务id", "status": "状态"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} diff --git a/validator/export.go b/validator/export.go new file mode 100644 index 0000000..563f144 --- /dev/null +++ b/validator/export.go @@ -0,0 +1,76 @@ +package validator + +import ( + "fmt" + "net/http" + "planA/initialization/golabl" + taskValidator "planA/type/validator" + + "github.com/gorilla/mux" +) + +// GetExportValidator 获取导出列表验证 +func GetExportValidator(data *http.Request) (taskValidator.GetExportTask, error) { + form := taskValidator.GetExportTask{ + Page: data.URL.Query().Get("page"), + Size: data.URL.Query().Get("size"), + } + fieldCN := map[string]string{"Page": "页码", "Size": "每页数量"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// GetExportByUserIdValidator 获取导出列表验证-用户 +func GetExportByUserIdValidator(data *http.Request) (taskValidator.GetTaskByUserId, error) { + vars := mux.Vars(data) + userId := vars["userId"] + + form := taskValidator.GetTaskByUserId{ + UserID: userId, + Page: data.URL.Query().Get("page"), + Size: data.URL.Query().Get("size"), + } + fieldCN := map[string]string{"UserID": "用户 Id", "Page": "页码", "Size": "每页数量"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// GetExportDetailValidator 获取导出详情验证 +func GetExportDetailValidator(data *http.Request) (taskValidator.ExportTaskDetail, error) { + vars := mux.Vars(data) + taskId := vars["id"] + + form := taskValidator.ExportTaskDetail{ + TaskID: taskId, + } + fieldCN := map[string]string{"TaskID": "导出任务 Id"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// GetExportDetailByUserIdValidator 获取导出详情验证-用户 +func GetExportDetailByUserIdValidator(data *http.Request) (taskValidator.ExportTaskDetailByUserId, error) { + vars := mux.Vars(data) + taskId := vars["id"] + userId := vars["userId"] + + form := taskValidator.ExportTaskDetailByUserId{ + TaskID: taskId, + UserID: userId, + } + fieldCN := map[string]string{"TaskID": "导出任务 Id", "UserId": "用户 Id"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} diff --git a/validator/shop.go b/validator/shop.go new file mode 100644 index 0000000..1cb38c9 --- /dev/null +++ b/validator/shop.go @@ -0,0 +1,40 @@ +package validator + +import ( + "fmt" + "net/http" + "planA/initialization/golabl" + taskValidator "planA/type/validator" + + "github.com/gorilla/mux" +) + +// GetShopInfoValidator 获取店铺信息参数验证 +func GetShopInfoValidator(data *http.Request) (taskValidator.GetShopInfo, error) { + vars := mux.Vars(data) + shopId := vars["shopId"] + + form := taskValidator.GetShopInfo{ + ShopId: shopId, + } + fieldCN := map[string]string{"ShopId": "店铺ID"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// CreateTbShopValidator 创建淘宝店铺数据 +func CreateTbShopValidator(data *http.Request) (taskValidator.CreateTbShop, error) { + form := taskValidator.CreateTbShop{ + ShopId: data.FormValue("shop_id"), + ShopName: data.FormValue("shop_name"), + } + fieldCN := map[string]string{"ShopId": "店铺ID", "ShopName": "店铺名称"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} diff --git a/validator/task.go b/validator/task.go new file mode 100644 index 0000000..cfb3d60 --- /dev/null +++ b/validator/task.go @@ -0,0 +1,131 @@ +package validator + +import ( + "fmt" + "net/http" + "planA/initialization/golabl" + + taskValidator "planA/type/validator" + + "github.com/gorilla/mux" +) + +// CreateTaskValidator 创建任务验证 +func CreateTaskValidator(data *http.Request) (taskValidator.CreateTask, error) { + form := taskValidator.CreateTask{ + ShopID: data.FormValue("shop_id"), + ShopType: data.FormValue("shop_type"), + TaskCount: data.FormValue("task_count"), + TaskType: data.FormValue("task_type"), + ImgType: data.FormValue("img_type"), + UpdateType: data.FormValue("update_type"), + DelNum: data.FormValue("del_num"), + DelTime: data.FormValue("del_time"), + } + fieldCN := map[string]string{"ShopID": "店铺ID", "ShopType": "店铺类型", "TaskCount": "任务数量", "TaskType": "任务类型", "ImgType": "图片类型"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// CreateTbTaskValidator 创建淘宝任务验证 +func CreateTbTaskValidator(data *http.Request) (taskValidator.CreateTbTask, error) { + form := taskValidator.CreateTbTask{ + ShopID: data.FormValue("shop_id"), + TaskCount: data.FormValue("task_count"), + TaskType: data.FormValue("task_type"), + } + fieldCN := map[string]string{"ShopID": "店铺ID", "TaskCount": "任务数量", "TaskType": "任务类型"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// TaskIdValidator 验证任务id +func TaskIdValidator(data *http.Request) (taskValidator.UpdateTaskStatus, error) { + vars := mux.Vars(data) + taskId := vars["id"] + + form := taskValidator.UpdateTaskStatus{ + TaskID: taskId, + } + fieldCN := map[string]string{"TaskID": "任务ID"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// GetTaskValidator 获取任务列表验证 +func GetTaskValidator(data *http.Request) (taskValidator.GetTask, error) { + form := taskValidator.GetTask{ + Page: data.URL.Query().Get("page"), + Size: data.URL.Query().Get("size"), + TaskID: data.URL.Query().Get("task_id"), + ShopName: data.URL.Query().Get("shop_name"), + TaskType: data.URL.Query().Get("task_type"), + } + fieldCN := map[string]string{"Page": "页码", "Size": "每页数量", "TaskID": "任务ID", "ShopName": "店铺名称", "TaskType": "任务类型"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// GetTaskByUserIdValidator 获取用户任务列表验证 +func GetTaskByUserIdValidator(data *http.Request) (taskValidator.GetTaskByUserId, error) { + form := taskValidator.GetTaskByUserId{ + Page: data.URL.Query().Get("page"), + Size: data.URL.Query().Get("size"), + TaskID: data.URL.Query().Get("task_id"), + ShopName: data.URL.Query().Get("shop_name"), + TaskType: data.URL.Query().Get("task_type"), + UserID: data.URL.Query().Get("user_id"), + } + fieldCN := map[string]string{"Page": "页码", "Size": "每页数量", "TaskID": "任务ID", "ShopName": "店铺名称", "TaskType": "任务类型", "UserID": "用户ID"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// GetBodyOverValidator 获取bodyOver 验证 +func GetBodyOverValidator(data *http.Request) (taskValidator.GetBodyOver, error) { + vars := mux.Vars(data) + taskId := vars["id"] + + form := taskValidator.GetBodyOver{ + TaskID: taskId, + Page: data.URL.Query().Get("page"), + Size: data.URL.Query().Get("size"), + } + fieldCN := map[string]string{"Page": "页码", "Size": "每页数量", "TaskID": "任务ID"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} + +// UpdateTaskProgressValidator 更新任务进度 结构体 +func UpdateTaskProgressValidator(data *http.Request) (taskValidator.UpdateTaskProgress, error) { + + form := taskValidator.UpdateTaskProgress{ + TaskID: data.FormValue("task_id"), + Status: data.FormValue("status"), + Num: data.FormValue("num"), + } + fieldCN := map[string]string{"task_id": "任务ID", "status": "任务状态", "num": "任务进度数"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} diff --git a/validator/uploadImg.go b/validator/uploadImg.go new file mode 100644 index 0000000..a861d08 --- /dev/null +++ b/validator/uploadImg.go @@ -0,0 +1,22 @@ +package validator + +import ( + "fmt" + "net/http" + "planA/initialization/golabl" + taskValidator "planA/type/validator" +) + +// ImgUploadToPddValidator 上传图片到拼多多验证 +func ImgUploadToPddValidator(data *http.Request) (taskValidator.ImgUploadToPdd, error) { + form := taskValidator.ImgUploadToPdd{ + ImgUrl: data.FormValue("img_url"), + ShopId: data.FormValue("shop_id"), + } + fieldCN := map[string]string{"ImgUrl": "图片url", "ShopId": "店铺ID"} + if err := golabl.Validator.Struct(form); err != nil { + errMsg := ValidatorRule(err, fieldCN) + return form, fmt.Errorf("参数错误:%s", errMsg) + } + return form, nil +} diff --git a/validator/validator.go b/validator/validator.go new file mode 100644 index 0000000..6bb9b58 --- /dev/null +++ b/validator/validator.go @@ -0,0 +1,79 @@ +package validator + +import ( + "fmt" + "strings" + + "github.com/go-playground/validator/v10" +) + +// ValidatorRule 验证规则 +func ValidatorRule(err error, fieldCN map[string]string) string { + if err == nil { + return "" + } + + // 断言为validator的验证错误类型 + validationErrs, ok := err.(validator.ValidationErrors) + if !ok { + return "参数验证失败:" + err.Error() + } + + // 遍历错误(返回第一个错误,符合接口友好性;如需返回所有错误可改为拼接字符串) + for _, e := range validationErrs { + field := e.Field() // 获取当前错误的字段名(如 ShopType) + tag := e.Tag() // 获取验证规则(如 oneof) + param := e.Param() // 获取规则参数(如 1 2 5) + fieldName := field // 默认使用原字段名 + if cn, ok := fieldCN[field]; ok { + fieldName = cn // 替换为中文名称 + } + + switch tag { + case "required": + return fmt.Sprintf("%s为必填项", fieldName) + case "email": + return fmt.Sprintf("%s格式不正确", fieldName) + case "min": + // 区分字符串长度min和数字值min + if isNumericField(field) { + return fmt.Sprintf("%s不能小于%s", fieldName, param) + } + return fmt.Sprintf("%s长度不能少于%s个字符", fieldName, param) + case "max": + if isNumericField(field) { + return fmt.Sprintf("%s不能大于%s", fieldName, param) + } + return fmt.Sprintf("%s长度不能超过%s个字符", fieldName, param) + case "gte": + return fmt.Sprintf("%s必须大于等于%s", fieldName, param) + case "lte": + return fmt.Sprintf("%s必须小于等于%s", fieldName, param) + case "phone": + return fmt.Sprintf("%s格式不正确(请填写11位手机号)", fieldName) + case "oneof": + // 格式化oneof的可选值(如 "1 2 5" → "1、2、5") + options := strings.ReplaceAll(param, " ", "、") + return fmt.Sprintf("%s只能填写%s中的一个", fieldName, options) + case "numeric": + return fmt.Sprintf("%s必须是数字格式", fieldName) + case "shop_type_only_5": + return fmt.Sprintf("%s仅允许填写5", fieldName) + default: + return fmt.Sprintf("%s验证失败(规则:%s)", fieldName, tag) + } + } + + return "" +} + +// isNumericField 判断字段是否为数字类型(用于区分min/max是长度还是数值) +func isNumericField(field string) bool { + numericFields := []string{"TaskCount", "Age"} + for _, f := range numericFields { + if f == field { + return true + } + } + return false +}