Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
9d9a94aac6
17
config.yaml
17
config.yaml
@ -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
|
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
|
||||||
@ -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
115
controllers/wangdian.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
386
service/wangdian.go
Normal 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
|
||||||
|
}
|
||||||
@ -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[:])
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user