809 lines
24 KiB
Go
809 lines
24 KiB
Go
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
|
|
}
|