Merge remote-tracking branch 'origin/master'

This commit is contained in:
Administrator 2026-06-26 11:53:18 +08:00
commit 9d9a94aac6
7 changed files with 622 additions and 16 deletions

View File

@ -56,6 +56,21 @@ external_api:
es_update_book_url: "https://book.center.yushutx.com/api/es/updateBookFieldsByISBN" es_update_book_url: "https://book.center.yushutx.com/api/es/updateBookFieldsByISBN"
# sync_task_url: "http://192.168.101.156:8080/task/create" # sync_task_url: "http://192.168.101.156:8080/task/create"
sync_task_url: "http://36.212.7.246:8283/task/create" sync_task_url: "http://36.212.7.246:8283/task/create"
# sync_task_body_url: "http://192.168.101.156:8080./task/setTaskBody" # sync_task_body_url: "http://192.168.101.156:8080/task/setTaskBody"
sync_task_body_url: "http://36.212.7.246:8283/task/setTaskBody" sync_task_body_url: "http://36.212.7.246:8283/task/setTaskBody"
timeout: 30
wangdian:
url: "https://api.wangdian.cn/openapi2/purchase_order_push.php"
sandbox_url: "https://sandbox.wangdian.cn/openapi2/purchase_order_push.php"
provider_query_url: "https://api.wangdian.cn/openapi2/purchase_provider_query.php"
provider_query_sandbox: "https://sandbox.wangdian.cn/openapi2/purchase_provider_query.php"
warehouse_query_url: "https://api.wangdian.cn/openapi2/warehouse_query.php"
warehouse_query_sandbox: "https://sandbox.wangdian.cn/openapi2/warehouse_query.php"
goods_query_url: "https://api.wangdian.cn/openapi2/goods_query.php"
goods_query_sandbox: "https://sandbox.wangdian.cn/openapi2/goods_query.php"
sandbox: true
sid: "apidevnew2"
appkey: "skxz2-test"
appsecret: "85bf423bb"
timeout: 30 timeout: 30

View File

@ -18,6 +18,7 @@ type Config struct {
ES ESConfig `yaml:"es"` ES ESConfig `yaml:"es"`
OCR OCRConfig `yaml:"ocr"` OCR OCRConfig `yaml:"ocr"`
ExternalAPI ExternalAPIConfig `yaml:"external_api"` ExternalAPI ExternalAPIConfig `yaml:"external_api"`
Wangdian WangdianConfig `yaml:"wangdian"`
} }
type ServerConfig struct { type ServerConfig struct {
@ -74,6 +75,22 @@ type ExternalAPIConfig struct {
Timeout int `yaml:"timeout"` Timeout int `yaml:"timeout"`
} }
type WangdianConfig struct {
URL string `yaml:"url"`
SandboxURL string `yaml:"sandbox_url"`
ProviderQueryURL string `yaml:"provider_query_url"`
ProviderQuerySandbox string `yaml:"provider_query_sandbox"`
WarehouseQueryURL string `yaml:"warehouse_query_url"`
WarehouseQuerySandbox string `yaml:"warehouse_query_sandbox"`
GoodsQueryURL string `yaml:"goods_query_url"`
GoodsQuerySandbox string `yaml:"goods_query_sandbox"`
Sandbox bool `yaml:"sandbox"`
Sid string `yaml:"sid"`
AppKey string `yaml:"appkey"`
AppSecret string `yaml:"appsecret"`
Timeout int `yaml:"timeout"`
}
var AppConfig *Config var AppConfig *Config
func Init() { func Init() {

115
controllers/wangdian.go Normal file
View File

@ -0,0 +1,115 @@
package controllers
import (
"net/http"
"psi/constant"
"psi/database"
"psi/service"
"psi/utils"
"strconv"
"github.com/gin-gonic/gin"
)
type WangdianApi struct{}
func (i *WangdianApi) CreatePurchaseOrder(c *gin.Context) {
raw := c.PostForm("purchase_order_id")
if raw == "" {
raw = c.Query("purchase_order_id")
}
purchaseOrderID, err := strconv.ParseInt(raw, 10, 64)
if err != nil || purchaseOrderID <= 0 {
utils.FailWithRequestLog(constant.LoggerChannelRequest, "参数错误: purchase_order_id 必须为正整数", nil, c, nil)
return
}
resp, err := service.PushPurchaseOrder(purchaseOrderID, database.GetDB(c))
if err != nil {
utils.FailWithRequestLog(constant.LoggerChannelWork, "推送采购单到旺店通失败: "+err.Error(), err, c, nil)
return
}
c.JSON(http.StatusOK, gin.H{
"code": resp.Code,
"message": resp.Message,
})
}
func (i *WangdianApi) QueryProvider(c *gin.Context) {
column := c.Query("column")
providerNo := c.Query("provider_no")
providerName := c.Query("provider_name")
pageSizeStr := c.Query("page_size")
pageNoStr := c.Query("page_no")
pageSize, _ := strconv.Atoi(pageSizeStr)
pageNo, _ := strconv.Atoi(pageNoStr)
resp, err := service.QueryProvider(column, providerNo, providerName, pageSize, pageNo)
if err != nil {
utils.FailWithRequestLog(constant.LoggerChannelWork, "查询旺店通供应商失败: "+err.Error(), err, c, nil)
return
}
c.JSON(http.StatusOK, gin.H{
"code": resp.Code,
"message": resp.Message,
"total_count": resp.TotalCount,
"provider_list": resp.ProviderList,
})
}
func (i *WangdianApi) QueryWarehouse(c *gin.Context) {
warehouseNo := c.Query("warehouse_no")
pageSizeStr := c.Query("page_size")
pageNoStr := c.Query("page_no")
isDisabled := c.Query("is_disabled")
pageSize, _ := strconv.Atoi(pageSizeStr)
pageNo, _ := strconv.Atoi(pageNoStr)
resp, err := service.QueryWarehouse(warehouseNo, 0, 0, pageSize, pageNo, isDisabled)
if err != nil {
utils.FailWithRequestLog(constant.LoggerChannelWork, "查询旺店通仓库失败: "+err.Error(), err, c, nil)
return
}
c.JSON(http.StatusOK, gin.H{
"code": resp.Code,
"message": resp.Message,
"total_count": resp.TotalCount,
"warehouses": resp.Warehouses,
})
}
func (i *WangdianApi) QueryGoods(c *gin.Context) {
specNo := c.Query("spec_no")
goodsNo := c.Query("goods_no")
brandNo := c.Query("brand_no")
className := c.Query("class_name")
barcode := c.Query("barcode")
startTime := c.Query("start_time")
endTime := c.Query("end_time")
pageSizeStr := c.Query("page_size")
pageNoStr := c.Query("page_no")
pageSize, _ := strconv.Atoi(pageSizeStr)
if pageSize <= 0 {
pageSize = 40
}
pageNo, _ := strconv.Atoi(pageNoStr)
resp, err := service.QueryGoods(specNo, goodsNo, brandNo, className, barcode, startTime, endTime, 0, pageSize, pageNo)
if err != nil {
utils.FailWithRequestLog(constant.LoggerChannelWork, "查询旺店通商品失败: "+err.Error(), err, c, nil)
return
}
c.JSON(http.StatusOK, gin.H{
"code": resp.Code,
"message": resp.Message,
"total_count": resp.TotalCount,
"goods_list": resp.GoodsList,
})
}

View File

@ -1,15 +1,16 @@
package database package database
import ( import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log" "log"
"os" "os"
"psi/config" "psi/config"
"psi/models" "psi/models"
"psi/utils" "psi/utils"
"time" "time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
) )
var DB *gorm.DB var DB *gorm.DB

View File

@ -2,11 +2,12 @@ package router
import ( import (
"fmt" "fmt"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"psi/config" "psi/config"
"psi/controllers" "psi/controllers"
"psi/middleware" "psi/middleware"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
) )
var employeeApi = &controllers.EmployeeApi{} var employeeApi = &controllers.EmployeeApi{}
@ -39,6 +40,7 @@ var splitAccountConfigApi = &controllers.SplitAccountConfigApi{}
var splitAccountDeductionLogApi = &controllers.SplitAccountDeductionLogApi{} var splitAccountDeductionLogApi = &controllers.SplitAccountDeductionLogApi{}
var configApi = &controllers.ConfigApi{} var configApi = &controllers.ConfigApi{}
var cancelLogisticsApi = &controllers.CancelLogisticsApi{} var cancelLogisticsApi = &controllers.CancelLogisticsApi{}
var wangdianApi = &controllers.WangdianApi{}
func Run() { func Run() {
defer func() { defer func() {
@ -110,6 +112,16 @@ func initRouter() (r *gin.Engine) {
sign.POST("/logistics/cancel", cancelLogisticsApi.CancelLogistics) // 取消物流单号 sign.POST("/logistics/cancel", cancelLogisticsApi.CancelLogistics) // 取消物流单号
} }
// 需要认证(JWT)但不签名验证的接口
authOnly := api.Group("")
authOnly.Use(middleware.JWTAuth())
{
authOnly.POST("/wangdian/purchase-order-push", wangdianApi.CreatePurchaseOrder) // 推送采购单到旺店通
authOnly.GET("/wangdian/query-provider", wangdianApi.QueryProvider) // 查询旺店通供应商
authOnly.GET("/wangdian/query-warehouse", wangdianApi.QueryWarehouse) // 查询旺店通仓库
authOnly.GET("/wangdian/query-goods", wangdianApi.QueryGoods) // 查询旺店通商品
}
// 需要认证的接口 // 需要认证的接口
auth := api.Group("") auth := api.Group("")
auth.Use(middleware.APISign()) auth.Use(middleware.APISign())

386
service/wangdian.go Normal file
View File

@ -0,0 +1,386 @@
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"`
}
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"`
}
func wangdianURL(sandboxURL, productionURL string) string {
cfg := config.AppConfig.Wangdian
if cfg.Sandbox && sandboxURL != "" {
return sandboxURL
}
if productionURL != "" {
return productionURL
}
return productionURL
}
// 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 := wangdianURL(cfg.ProviderQuerySandbox, cfg.ProviderQueryURL)
if url == "" {
url = "https://api.wangdian.cn/openapi2/purchase_provider_query.php"
}
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
}
// QueryWarehouse 查询旺店通仓库
func QueryWarehouse(warehouseNo string, warehouseType int, subType int, pageSize, pageNo int, isDisabled string) (*WangdianWarehouseQueryResponse, 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 warehouseNo != "" {
params["warehouse_no"] = warehouseNo
}
if pageSize > 0 {
params["page_size"] = strconv.Itoa(pageSize)
}
if pageNo > 0 {
params["page_no"] = strconv.Itoa(pageNo)
}
if isDisabled != "" {
params["is_disabled"] = isDisabled
}
params["sign"] = utils.WangdianSign(params, cfg.AppSecret)
url := wangdianURL(cfg.WarehouseQuerySandbox, cfg.WarehouseQueryURL)
if url == "" {
url = "https://api.wangdian.cn/openapi2/warehouse_query.php"
}
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 WangdianWarehouseQueryResponse
if err := json.Unmarshal([]byte(respBody), &resp); err != nil {
return nil, fmt.Errorf("解析响应失败(raw=%s): %v", respBody, err)
}
return &resp, nil
}
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 := wangdianURL(cfg.GoodsQuerySandbox, cfg.GoodsQueryURL)
if url == "" {
url = "https://api.wangdian.cn/openapi2/goods_query.php"
}
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
}
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 := wangdianURL(cfg.SandboxURL, cfg.URL)
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
}

View File

@ -7,12 +7,15 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"io" "io"
"log"
"math/rand" "math/rand"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url"
"sort" "sort"
"strings" "strings"
"time" "time"
"unicode/utf8"
) )
// GenerateEmployeeID 生成工号 (5位数字不足补零) // GenerateEmployeeID 生成工号 (5位数字不足补零)
@ -242,31 +245,23 @@ func SubmitMultiBody(url string, taskID string, bodyList []string, sign string)
// @param params 表单数据 // @param params 表单数据
// @return error 错误信息 // @return error 错误信息
func SubmitFormData(url string, params map[string]string) (string, error) { func SubmitFormData(url string, params map[string]string) (string, error) {
// 创建multipart writer
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
// 添加文本字段
for key, value := range params { for key, value := range params {
err := writer.WriteField(key, value) err := writer.WriteField(key, value)
if err != nil { if err != nil {
return "", fmt.Errorf("write field error: %v", err) return "", fmt.Errorf("write field error: %v", err)
} }
} }
// 关闭writer
writer.Close() writer.Close()
// 创建请求
req, err := http.NewRequest("POST", url, body) req, err := http.NewRequest("POST", url, body)
if err != nil { if err != nil {
return "", fmt.Errorf("create request error: %v", err) return "", fmt.Errorf("create request error: %v", err)
} }
// 设置Content-Type
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
// 发送请求
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@ -274,11 +269,76 @@ func SubmitFormData(url string, params map[string]string) (string, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", fmt.Errorf("read response error: %v", err) return "", fmt.Errorf("read response error: %v", err)
} }
return string(respBody), nil return string(respBody), nil
} }
// SubmitFormDataWithTimeout 提交表单数据(application/x-www-form-urlencoded)
func SubmitFormDataWithTimeout(requestURL string, params map[string]string, timeoutSeconds int) (string, error) {
form := url.Values{}
for key, value := range params {
form.Set(key, value)
}
log.Printf("[旺店通推送] 发送form-urlencoded:\n%s", form.Encode())
req, err := http.NewRequest("POST", requestURL, strings.NewReader(form.Encode()))
if err != nil {
return "", fmt.Errorf("create request error: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
timeout := time.Duration(timeoutSeconds) * time.Second
if timeout <= 0 {
timeout = 30 * time.Second
}
client := &http.Client{Timeout: timeout}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("send request error: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read response error: %v", err)
}
return string(respBody), nil
}
// WangdianSign 旺店通接口签名算法
// 与PHP SDK保持一致使用UTF-8字符长度(iconv_strlen)而非字节长度
func WangdianSign(params map[string]string, appSecret string) string {
keys := make([]string, 0, len(params))
for k := range params {
if k == "sign" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var builder strings.Builder
for idx, key := range keys {
value := params[key]
keyLen := utf8.RuneCountInString(key)
valLen := utf8.RuneCountInString(value)
builder.WriteString(fmt.Sprintf("%02d", keyLen))
builder.WriteString("-")
builder.WriteString(key)
builder.WriteString(":")
builder.WriteString(fmt.Sprintf("%04d", valLen))
builder.WriteString("-")
builder.WriteString(value)
if idx < len(keys)-1 {
builder.WriteString(";")
}
}
builder.WriteString(appSecret)
hash := md5.Sum([]byte(builder.String()))
return hex.EncodeToString(hash[:])
}