package service import ( "encoding/json" "fmt" "log" "psi/config" "psi/database" "psi/models" "psi/utils" "strconv" "time" "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"` } 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.URL if cfg.Sandbox && cfg.SandboxURL != "" { url = cfg.SandboxURL } if url == "" { url = "https://api.wangdian.cn/openapi2/purchase_order_push.php" } 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 }