package service import ( "encoding/json" "fmt" "log" "psi/config" "psi/database" "psi/models" "psi/utils" "strconv" "time" "gorm.io/datatypes" "gorm.io/gorm" ) type WangdianPurchaseInfo struct { ProviderNo string `json:"provider_no"` WarehouseNo string `json:"warehouse_no"` OuterNo string `json:"outer_no"` IsUseOuterNo int8 `json:"is_use_outer_no,omitempty"` IsCheck int8 `json:"is_check,omitempty"` Contact string `json:"contact,omitempty"` PurchaseName string `json:"purchase_name,omitempty"` Telno string `json:"telno,omitempty"` ReceiveAddress string `json:"receive_address,omitempty"` LogisticsType int8 `json:"logistics_type,omitempty"` // 暂未使用 运货方式(物流公司) ExpectArriveTime string `json:"expect_arrive_time,omitempty"` Remark string `json:"remark,omitempty"` OtherFee float64 `json:"other_fee,omitempty"` // 暂未使用 其他费用 PostFee float64 `json:"post_fee,omitempty"` // 暂未使用 邮资 Prop1 string `json:"prop1,omitempty"` // 暂未使用 自定义属性1 Prop2 string `json:"prop2,omitempty"` // 暂未使用 自定义属性2 DetailsList []WangdianPurchaseDetailItem `json:"details_list"` } type WangdianPurchaseDetailItem struct { SpecNo string `json:"spec_no"` Num float64 `json:"num"` Price float64 `json:"price"` Discount float64 `json:"discount,omitempty"` Tax float64 `json:"tax,omitempty"` //暂未使用 税率 TaxPrice float64 `json:"tax_price,omitempty"` //暂未使用 税后单价 TaxAmount float64 `json:"tax_amount,omitempty"` //暂未使用 税后金额 Remark string `json:"remark,omitempty"` Prop1 string `json:"prop1,omitempty"` // 暂未使用 自定义属性1 Prop2 string `json:"prop2,omitempty"` // 暂未使用 自定义属性2 } type WangdianPurchasePushResponse struct { Code int `json:"code"` Message string `json:"message"` } type WangdianProviderQueryResponse struct { Code int `json:"code"` Message string `json:"message"` TotalCount int `json:"total_count"` ProviderList []map[string]interface{} `json:"provider_list"` } type WangdianWarehouseQueryResponse struct { Code int `json:"code"` Message string `json:"message"` TotalCount int `json:"total_count"` Warehouses []map[string]interface{} `json:"warehouses"` } const ( wangdianPrefix = "【旺店通】" apiPurchasePush = "purchase_order_push.php" apiProviderQuery = "purchase_provider_query.php" apiWarehouseQuery = "warehouse_query.php" apiGoodsQuery = "goods_query.php" apiGoodsPush = "goods_push.php" ) type WangdianGoodsPushResponse struct { Code int `json:"code"` Message string `json:"message"` } // QueryProvider 查询旺店通供应商 func QueryProvider(column, providerNo, providerName string, pageSize, pageNo int) (*WangdianProviderQueryResponse, error) { cfg := config.AppConfig.Wangdian if cfg.Sid == "" || cfg.AppKey == "" || cfg.AppSecret == "" { return nil, fmt.Errorf("旺店通接口配置不完整(sid/appkey/appsecret)") } params := map[string]string{ "sid": cfg.Sid, "appkey": cfg.AppKey, "timestamp": strconv.FormatInt(time.Now().Unix(), 10), } if column != "" { params["column"] = column } if providerNo != "" { params["provider_no"] = providerNo } if providerName != "" { params["provider_name"] = providerName } if pageSize > 0 { params["page_size"] = strconv.Itoa(pageSize) } if pageNo > 0 { params["page_no"] = strconv.Itoa(pageNo) } params["sign"] = utils.WangdianSign(params, cfg.AppSecret) url := cfg.GetURL(apiProviderQuery) timeout := cfg.Timeout if timeout <= 0 { timeout = 30 } log.Printf("[旺店通供应商查询] 请求URL: %s (sandbox=%v), 参数: sid=%s appkey=%s timestamp=%s sign=%s", url, cfg.Sandbox, cfg.Sid, cfg.AppKey, params["timestamp"], params["sign"]) respBody, err := utils.SubmitFormDataWithTimeout(url, params, timeout) if err != nil { return nil, fmt.Errorf("查询请求失败: %v", err) } log.Printf("[旺店通供应商查询] 返回信息: %s", string(respBody)) var resp WangdianProviderQueryResponse if err := json.Unmarshal([]byte(respBody), &resp); err != nil { return nil, fmt.Errorf("解析响应失败(raw=%s): %v", respBody, err) } return &resp, nil } // SyncWarehouse 从旺店通同步仓库数据到本地 func SyncWarehouse(db *gorm.DB, taskID int64) error { cfg := config.AppConfig.Wangdian if cfg.Sid == "" || cfg.AppKey == "" || cfg.AppSecret == "" { return fmt.Errorf("旺店通接口配置不完整(sid/appkey/appsecret)") } timeout := cfg.Timeout if timeout <= 0 { timeout = 30 } pageSize := 100 pageNo := 0 requestCount := 0 totalCount := 0 totalPages := 0 totalSynced := 0 for { params := map[string]string{ "sid": cfg.Sid, "appkey": cfg.AppKey, "timestamp": strconv.FormatInt(time.Now().Unix(), 10), "page_size": strconv.Itoa(pageSize), "page_no": strconv.Itoa(pageNo), } params["sign"] = utils.WangdianSign(params, cfg.AppSecret) url := cfg.GetURL(apiWarehouseQuery) respBody, err := utils.SubmitFormDataWithTimeout(url, params, timeout) requestCount++ if err != nil { log.Printf("[旺店通仓库同步] 查询第%d页失败(网络): %v, 已请求%d次", pageNo, err, requestCount) break } var pageResp struct { Code int `json:"code"` Message string `json:"message"` TotalCount int `json:"total_count"` Warehouses []map[string]interface{} `json:"warehouses"` } if err := json.Unmarshal([]byte(respBody), &pageResp); err != nil { log.Printf("[旺店通仓库同步] 解析第%d页失败: %v", pageNo, err) break } if pageResp.Code != 0 { log.Printf("[旺店通仓库同步] 第%d页返回错误(code=%d): %s, 已请求%d次", pageNo, pageResp.Code, pageResp.Message, requestCount) break } if pageNo == 0 { totalCount = pageResp.TotalCount totalPages = (totalCount + pageSize - 1) / pageSize } n := upsertWarehouses(db, pageResp.Warehouses) totalSynced += n UpdateSyncTaskProgress(db, taskID, totalSynced, totalCount) log.Printf("[旺店通仓库同步] 第%d/%d页完成, 本页写入%d个仓库, 累计%d/%d, 已请求%d次", pageNo+1, totalPages, n, totalSynced, totalCount, requestCount) if len(pageResp.Warehouses) < pageSize { break } pageNo++ time.Sleep(500 * time.Millisecond) } log.Printf("[旺店通仓库同步] 同步结束, 共同步 %d/%d 个仓库", totalSynced, totalCount) CompleteSyncTask(db, taskID) return nil } func upsertWarehouses(db *gorm.DB, warehouses []map[string]interface{}) int { now := time.Now().Unix() synced := 0 for _, wh := range warehouses { code := toString(wh["warehouse_no"]) if code == "" { continue } var existing models.Warehouse err := db.Where("code = ? AND is_del = 0", code).First(&existing).Error if err == nil { continue } status := int8(1) if toString(wh["is_disabled"]) == "1" { status = 0 } warehouse := models.Warehouse{ Code: code, Name: wangdianPrefix + toString(wh["name"]), ContactPerson: toString(wh["contact"]), ContactPhone: toString(wh["telno"]), Province: toString(wh["province"]), City: toString(wh["city"]), District: toString(wh["district"]), Address: toString(wh["address"]), Status: status, CreatedAt: now, UpdatedAt: now, Type: 1, } if err := db.Create(&warehouse).Error; err != nil { log.Printf("[旺店通仓库同步] 创建仓库 %s 失败: %v", code, err) continue } synced++ } return synced } func toString(v interface{}) string { if v == nil { return "" } switch val := v.(type) { case string: return val default: return fmt.Sprintf("%v", val) } } // SyncProvider 从旺店通同步供应商数据到本地 func SyncProvider(db *gorm.DB, taskID int64) error { cfg := config.AppConfig.Wangdian if cfg.Sid == "" || cfg.AppKey == "" || cfg.AppSecret == "" { return fmt.Errorf("旺店通接口配置不完整(sid/appkey/appsecret)") } timeout := cfg.Timeout if timeout <= 0 { timeout = 30 } pageSize := 100 pageNo := 0 requestCount := 0 totalCount := 0 totalPages := 0 totalSynced := 0 for { params := map[string]string{ "sid": cfg.Sid, "appkey": cfg.AppKey, "timestamp": strconv.FormatInt(time.Now().Unix(), 10), "page_size": strconv.Itoa(pageSize), "page_no": strconv.Itoa(pageNo), } params["sign"] = utils.WangdianSign(params, cfg.AppSecret) url := cfg.GetURL(apiProviderQuery) respBody, err := utils.SubmitFormDataWithTimeout(url, params, timeout) requestCount++ if err != nil { log.Printf("[旺店通供应商同步] 查询第%d页失败(网络): %v, 已请求%d次", pageNo, err, requestCount) break } var pageResp struct { Code int `json:"code"` Message string `json:"message"` TotalCount int `json:"total_count"` ProviderList []map[string]interface{} `json:"provider_list"` } if err := json.Unmarshal([]byte(respBody), &pageResp); err != nil { log.Printf("[旺店通供应商同步] 解析第%d页失败: %v", pageNo, err) break } if pageResp.Code != 0 { log.Printf("[旺店通供应商同步] 第%d页返回错误(code=%d): %s, 已请求%d次", pageNo, pageResp.Code, pageResp.Message, requestCount) break } if pageNo == 0 { totalCount = pageResp.TotalCount totalPages = (totalCount + pageSize - 1) / pageSize } n := upsertProviders(db, pageResp.ProviderList) totalSynced += n UpdateSyncTaskProgress(db, taskID, totalSynced, totalCount) log.Printf("[旺店通供应商同步] 第%d/%d页完成, 本页写入%d个供应商, 累计%d/%d, 已请求%d次", pageNo+1, totalPages, n, totalSynced, totalCount, requestCount) if len(pageResp.ProviderList) < pageSize { break } pageNo++ time.Sleep(500 * time.Millisecond) } log.Printf("[旺店通供应商同步] 同步结束, 共同步 %d/%d 个供应商", totalSynced, totalCount) CompleteSyncTask(db, taskID) return nil } func upsertProviders(db *gorm.DB, providers []map[string]interface{}) int { now := time.Now().Unix() synced := 0 for _, p := range providers { code := toString(p["provider_no"]) if code == "" { continue } var existing models.Supplier err := db.Where("code = ? AND is_del = 0", code).First(&existing).Error if err == nil { continue } status := int8(1) if toString(p["is_disabled"]) == "1" { status = 0 } supplier := models.Supplier{ Code: code, Name: wangdianPrefix + toString(p["provider_name"]), ContactPerson: toString(p["contact"]), ContactPhone: toString(p["telno"]), Address: toString(p["address"]), Status: status, CreatedAt: now, UpdatedAt: now, } if err := db.Create(&supplier).Error; err != nil { log.Printf("[旺店通供应商同步] 创建供应商 %s 失败: %v", code, err) continue } synced++ } return synced } type WangdianGoodsQueryResponse struct { Code int `json:"code"` Message string `json:"message"` TotalCount int `json:"total_count"` GoodsList []map[string]interface{} `json:"goods_list"` } // QueryGoods 查询旺店通商品 func QueryGoods(specNo, goodsNo, brandNo, className, barcode, startTime, endTime string, deleted int, pageSize, pageNo int) (*WangdianGoodsQueryResponse, error) { cfg := config.AppConfig.Wangdian if cfg.Sid == "" || cfg.AppKey == "" || cfg.AppSecret == "" { return nil, fmt.Errorf("旺店通接口配置不完整(sid/appkey/appsecret)") } params := map[string]string{ "sid": cfg.Sid, "appkey": cfg.AppKey, "timestamp": strconv.FormatInt(time.Now().Unix(), 10), } if specNo != "" { params["spec_no"] = specNo } if goodsNo != "" { params["goods_no"] = goodsNo } if brandNo != "" { params["brand_no"] = brandNo } if className != "" { params["class_name"] = className } if barcode != "" { params["barcode"] = barcode } if startTime != "" { params["start_time"] = startTime } if endTime != "" { params["end_time"] = endTime } params["page_size"] = strconv.Itoa(pageSize) params["page_no"] = strconv.Itoa(pageNo) params["sign"] = utils.WangdianSign(params, cfg.AppSecret) url := cfg.GetURL(apiGoodsQuery) timeout := cfg.Timeout if timeout <= 0 { timeout = 30 } log.Printf("[旺店通商品查询] 请求URL: %s (sandbox=%v), 参数: sid=%s appkey=%s timestamp=%s sign=%s", url, cfg.Sandbox, cfg.Sid, cfg.AppKey, params["timestamp"], params["sign"]) respBody, err := utils.SubmitFormDataWithTimeout(url, params, timeout) if err != nil { return nil, fmt.Errorf("查询请求失败: %v", err) } log.Printf("[旺店通商品查询] 返回信息: %s", string(respBody)) var resp WangdianGoodsQueryResponse if err := json.Unmarshal([]byte(respBody), &resp); err != nil { return nil, fmt.Errorf("解析响应失败(raw=%s): %v", respBody, err) } return &resp, nil } // SyncGoods 从旺店通同步商品数据到本地 func SyncGoods(db *gorm.DB, taskID int64, startTime, endTime string, aboutID int64) error { cfg := config.AppConfig.Wangdian if cfg.Sid == "" || cfg.AppKey == "" || cfg.AppSecret == "" { return fmt.Errorf("旺店通接口配置不完整(sid/appkey/appsecret)") } timeout := cfg.Timeout if timeout <= 0 { timeout = 30 } if startTime == "" || endTime == "" { return fmt.Errorf("start_time 和 end_time 不能为空") } startT, err := time.Parse("2006-01-02 15:04:05", startTime) if err != nil { return fmt.Errorf("start_time 格式无效, 正确格式: 2006-01-02 15:04:05") } endT, err := time.Parse("2006-01-02 15:04:05", endTime) if err != nil { return fmt.Errorf("end_time 格式无效, 正确格式: 2006-01-02 15:04:05") } if endT.Sub(startT) > 30*24*time.Hour { return fmt.Errorf("时间跨度不能超过30天") } pageSize := 100 pageNo := 0 requestCount := 0 totalCount := 0 totalPages := 0 totalSynced := 0 for { params := map[string]string{ "sid": cfg.Sid, "appkey": cfg.AppKey, "timestamp": strconv.FormatInt(time.Now().Unix(), 10), "page_size": strconv.Itoa(pageSize), "page_no": strconv.Itoa(pageNo), "start_time": startTime, "end_time": endTime, } params["sign"] = utils.WangdianSign(params, cfg.AppSecret) url := cfg.GetURL(apiGoodsQuery) respBody, err := utils.SubmitFormDataWithTimeout(url, params, timeout) requestCount++ if err != nil { log.Printf("[旺店通商品同步] 查询第%d页失败(网络): %v, 已请求%d次", pageNo, err, requestCount) break } var pageResp struct { Code int `json:"code"` Message string `json:"message"` TotalCount int `json:"total_count"` GoodsList []map[string]interface{} `json:"goods_list"` } if err := json.Unmarshal([]byte(respBody), &pageResp); err != nil { log.Printf("[旺店通商品同步] 解析第%d页失败: %v", pageNo, err) break } if pageResp.Code != 0 { log.Printf("[旺店通商品同步] 第%d页返回错误(code=%d): %s, 已请求%d次", pageNo, pageResp.Code, pageResp.Message, requestCount) break } if pageNo == 0 { totalCount = pageResp.TotalCount totalPages = (totalCount + pageSize - 1) / pageSize } n := upsertGoods(db, pageResp.GoodsList, aboutID) totalSynced += n UpdateSyncTaskProgress(db, taskID, totalSynced, totalCount) log.Printf("[旺店通商品同步] 第%d/%d页完成, 本页写入%d个商品, 累计%d/%d, 已请求%d次", pageNo+1, totalPages, n, totalSynced, totalCount, requestCount) if len(pageResp.GoodsList) < pageSize { break } pageNo++ time.Sleep(500 * time.Millisecond) } log.Printf("[旺店通商品同步] 同步结束, 共同步 %d/%d 个商品", totalSynced, totalCount) CompleteSyncTask(db, taskID) return nil } func upsertGoods(db *gorm.DB, goodsList []map[string]interface{}, aboutID int64) int { now := time.Now().Unix() synced := 0 for _, g := range goodsList { goodsName := toString(g["goods_name"]) if goodsName == "" { continue } goodsPic := toString(g["pic_url"]) specList, ok := g["spec_list"].([]interface{}) if !ok || len(specList) == 0 { barcode := toString(g["barcode"]) if barcode == "" { continue } var existing models.Product if err := db.Where("barcode = ? AND is_del = 0", barcode).First(&existing).Error; err == nil { continue } product := models.Product{ AboutId: aboutID, Name: wangdianPrefix + goodsName, Barcode: barcode, Status: 1, LiveImage: goodsPicToJSON(goodsPic), Price: parsePrice(g["lowest_price"]), SalePrice: parsePrice(g["retail_price"]), CreatedAt: now, UpdatedAt: now, } if err := db.Create(&product).Error; err != nil { log.Printf("[旺店通商品同步] 创建商品 %s 失败: %v", barcode, err) continue } synced++ continue } for _, spec := range specList { specMap, ok := spec.(map[string]interface{}) if !ok { continue } barcode := toString(specMap["barcode"]) if barcode == "" { barcode = toString(specMap["spec_no"]) if barcode == "" { continue } } var existing models.Product if err := db.Where("barcode = ? AND is_del = 0", barcode).First(&existing).Error; err == nil { continue } specPic := toString(specMap["pic_url"]) if specPic == "" { specPic = goodsPic } product := models.Product{ AboutId: aboutID, Name: wangdianPrefix + goodsName, Barcode: barcode, Status: 1, LiveImage: goodsPicToJSON(specPic), Price: parsePrice(specMap["lowest_price"]), SalePrice: parsePrice(specMap["retail_price"]), CreatedAt: now, UpdatedAt: now, } if err := db.Create(&product).Error; err != nil { log.Printf("[旺店通商品同步] 创建商品 %s 失败: %v", barcode, err) continue } synced++ } } return synced } func goodsPicToJSON(picURL string) datatypes.JSON { if picURL == "" { return datatypes.JSON([]byte("[]")) } return datatypes.JSON([]byte(`["` + picURL + `"]`)) } func parsePrice(v interface{}) int64 { s := toString(v) if s == "" { return 0 } f, err := strconv.ParseFloat(s, 64) if err != nil { return 0 } return int64(f * 100) } // PushGoods 推送货品档案到旺店通 func PushGoods(goodsListJSON string) (*WangdianGoodsPushResponse, error) { cfg := config.AppConfig.Wangdian if cfg.Sid == "" || cfg.AppKey == "" || cfg.AppSecret == "" { return nil, fmt.Errorf("旺店通接口配置不完整(sid/appkey/appsecret)") } timestamp := time.Now().Unix() params := map[string]string{ "sid": cfg.Sid, "appkey": cfg.AppKey, "timestamp": strconv.FormatInt(timestamp, 10), "goods_list": goodsListJSON, } params["sign"] = utils.WangdianSign(params, cfg.AppSecret) url := cfg.GetURL(apiGoodsPush) timeout := cfg.Timeout if timeout <= 0 { timeout = 30 } log.Printf("[旺店通货品推送] 请求URL: %s (sandbox=%v)", url, cfg.Sandbox) respBody, err := utils.SubmitFormDataWithTimeout(url, params, timeout) if err != nil { return nil, fmt.Errorf("推送请求失败: %v", err) } log.Printf("[旺店通货品推送] 返回信息: %s", string(respBody)) var resp WangdianGoodsPushResponse if err := json.Unmarshal([]byte(respBody), &resp); err != nil { return nil, fmt.Errorf("解析响应失败(raw=%s): %v", respBody, err) } return &resp, nil } func PushPurchaseOrder(purchaseOrderID int64, db ...*gorm.DB) (*WangdianPurchasePushResponse, error) { databaseConn := database.OptionalDB(db...) dbName := databaseConn.Migrator().CurrentDatabase() log.Printf("[旺店通推送] 使用数据库: %s", dbName) // 1. 查询采购单 var po models.PurchaseOrder if err := databaseConn.Where("id = ? AND is_del = 0", purchaseOrderID).First(&po).Error; err != nil { return nil, fmt.Errorf("采购单不存在: %v", err) } // 2. 查询供应商编号(必须) var supplier models.Supplier if err := databaseConn.Select("code, contact_person, contact_phone, address"). Where("id = ? AND is_del = 0", po.SupplierID).First(&supplier).Error; err != nil { return nil, fmt.Errorf("获取供应商信息失败: %v", err) } if supplier.Code == "" { return nil, fmt.Errorf("供应商编码为空,请先维护供应商档案") } // 3. 查询仓库编号(必须) var warehouse models.Warehouse if err := databaseConn.Select("code, contact_person, contact_phone, address"). Where("id = ? AND is_del = 0", po.WarehouseID).First(&warehouse).Error; err != nil { return nil, fmt.Errorf("获取仓库信息失败: %v", err) } if warehouse.Code == "" { return nil, fmt.Errorf("仓库编码为空,请先维护仓库档案") } // 4. 查询采购明细,关联商品条码 type itemRow struct { models.PurchaseOrderItem Barcode string `gorm:"column:barcode"` } var items []itemRow if err := databaseConn.Table("purchase_order_item"). Select("purchase_order_item.*, product.barcode"). Joins("LEFT JOIN product ON purchase_order_item.product_id = product.id AND product.is_del = 0"). Where("purchase_order_item.purchase_order_id = ? AND purchase_order_item.is_del = 0", purchaseOrderID). Debug().Scan(&items).Error; err != nil { return nil, fmt.Errorf("获取采购明细失败: %v", err) } if len(items) == 0 { return nil, fmt.Errorf("采购明细为空,无法推送") } // 5. 组装 details_list detailList := make([]WangdianPurchaseDetailItem, 0, len(items)) for _, it := range items { specNo := it.Barcode if specNo == "" { specNo = strconv.FormatInt(it.ProductID, 10) } detailList = append(detailList, WangdianPurchaseDetailItem{ SpecNo: specNo, Num: float64(it.Quantity), Price: float64(it.UnitPrice) / 100.0, Discount: 1.0, }) } // 6. 组装 purchase_info info := WangdianPurchaseInfo{ ProviderNo: supplier.Code, WarehouseNo: warehouse.Code, OuterNo: po.PoNo, IsUseOuterNo: 0, IsCheck: 0, Contact: warehouse.ContactPerson, PurchaseName: warehouse.ContactPerson, Telno: warehouse.ContactPhone, ReceiveAddress: warehouse.Address, Remark: po.Remark, DetailsList: detailList, } if po.ExpectedArrivalDate > 0 { info.ExpectArriveTime = time.Unix(po.ExpectedArrivalDate, 0).Format("2006-01-02 15:04:05") } infoBytes, err := json.Marshal(info) if err != nil { return nil, fmt.Errorf("序列化采购信息失败: %v", err) } // 7. 校验配置 cfg := config.AppConfig.Wangdian if cfg.Sid == "" || cfg.AppKey == "" || cfg.AppSecret == "" { return nil, fmt.Errorf("旺店通接口配置不完整(sid/appkey/appsecret)") } // 8. 组装公共请求参数 + 计算签名 timestamp := time.Now().Unix() params := map[string]string{ "sid": cfg.Sid, "appkey": cfg.AppKey, "timestamp": strconv.FormatInt(timestamp, 10), "purchase_info": string(infoBytes), } params["sign"] = utils.WangdianSign(params, cfg.AppSecret) // 9. 提交请求 url := cfg.GetURL(apiPurchasePush) timeout := cfg.Timeout if timeout <= 0 { timeout = 30 } log.Printf("[旺店通推送] 请求URL: %s (sandbox=%v, form-urlencoded), 参数: sid=%s appkey=%s timestamp=%s sign=%s", url, cfg.Sandbox, cfg.Sid, cfg.AppKey, params["timestamp"], params["sign"]) log.Printf("[旺店通推送] purchase_info: %s", string(infoBytes)) respBody, err := utils.SubmitFormDataWithTimeout(url, params, timeout) if err != nil { return nil, fmt.Errorf("推送请求失败: %v", err) } log.Printf("[旺店通推送] 返回信息: %s", string(respBody)) var resp WangdianPurchasePushResponse if err := json.Unmarshal([]byte(respBody), &resp); err != nil { return nil, fmt.Errorf("解析响应失败(raw=%s): %v", respBody, err) } return &resp, nil }