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