From 4cfca2640cf41ec96f661e6875880e91a54d8d14 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Fri, 28 Aug 2026 09:34:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6:=20sdk=5FL?= =?UTF-8?q?inkedMall/client.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk_LinkedMall/client.go | 678 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 678 insertions(+) create mode 100644 sdk_LinkedMall/client.go diff --git a/sdk_LinkedMall/client.go b/sdk_LinkedMall/client.go new file mode 100644 index 0000000..485da3e --- /dev/null +++ b/sdk_LinkedMall/client.go @@ -0,0 +1,678 @@ +package sdk_LinkedMall + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + "github.com/alibabacloud-go/tea/v2" +) + +const ( + defaultRegionId = "cn-zhangjiakou" + defaultEndpoint = "linkedmall.cn-zhangjiakou.aliyuncs.com" + basePath = "/opensaas-s2b/opensaas-s2b-biz-trade/v2" + readTimeout = 30000 + connectTimeout = 10000 +) + +// Client LinkedMall SDK 客户端 +type Client struct { + inner *openapi.Client +} + +// NewClient 创建一个新的LinkedMall SDK客户端 +// +// 使用RAM子账号的AccessKey初始化,禁止使用主账号AK。 +// 示例: +// +// cli, err := NewClient("your-access-key-id", "your-access-key-secret") +// if err != nil { +// panic(err) +// } +func NewClient(accessKeyId, accessKeySecret string) (*Client, error) { + config := &openapi.Config{ + AccessKeyId: tea.String(accessKeyId), + AccessKeySecret: tea.String(accessKeySecret), + RegionId: tea.String(defaultRegionId), + Endpoint: tea.String(defaultEndpoint), + ReadTimeout: tea.Int(readTimeout), + ConnectTimeout: tea.Int(connectTimeout), + } + c, err := openapi.NewClient(config) + if err != nil { + return nil, fmt.Errorf("failed to create openapi client: %w", err) + } + return &Client{inner: c}, nil +} + +// buildPath 构建完整的请求路径 +func buildPath(suffix string) string { + return basePath + suffix +} + +// toQueryMap 将结构体转为查询参数map +func toQueryMap(params interface{}) map[string]*string { + result := make(map[string]*string) + if params == nil { + return result + } + + // 使用json标签序列化后转为map + data, _ := json.Marshal(params) + var rawMap map[string]interface{} + if err := json.Unmarshal(data, &rawMap); err != nil { + return result + } + + for k, v := range rawMap { + switch val := v.(type) { + case string: + if val != "" { + result[k] = tea.String(val) + } + case float64: + if val != 0 { + result[k] = tea.String(strconv.FormatInt(int64(val), 10)) + } + case bool: + result[k] = tea.String(strconv.FormatBool(val)) + default: + // 忽略复杂类型 + } + } + return result +} + +// doRequest 执行HTTP请求并返回响应体字节 +func (c *Client) doRequest(method, path string, query map[string]*string, body interface{}) ([]byte, error) { + resp, err := c.inner.RoaRequest( + tea.String(method), + tea.String(path), + query, + body, + nil, + ) + if err != nil { + return nil, fmt.Errorf("roa request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected http status %d, body: %s", resp.StatusCode, string(bodyBytes)) + } + + return io.ReadAll(resp.Body) +} + +// parseResponse 解析通用响应,如果业务失败返回 *APIError +func (c *Client) parseResponse(data []byte, target interface{}) error { + var commonResp CommonResponse + if err := json.Unmarshal(data, &commonResp); err != nil { + return fmt.Errorf("failed to parse common response: %w, body: %s", err, string(data)) + } + + if !commonResp.Success { + return &APIError{ + Code: commonResp.Code, + Message: commonResp.Message, + SubCode: commonResp.SubCode, + SubMessage: commonResp.SubMessage, + RequestId: commonResp.RequestId, + } + } + + if target != nil && len(commonResp.Data) > 0 { + if err := json.Unmarshal(commonResp.Data, target); err != nil { + return fmt.Errorf("failed to parse data field: %w, data: %s", err, string(commonResp.Data)) + } + } + return nil +} + +// ============================================================ +// 采购方店铺 (Purchaser Shop) +// ============================================================ + +// ListPurchaserShops 分页获取采购方店铺列表 +func (c *Client) ListPurchaserShops(req *ListPurchaserShopsReq) (*ListPurchaserShopsRespData, error) { + path := buildPath("/purchaser-shops") + query := toQueryMap(req) + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result ListPurchaserShopsRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetPurchaserShop 获取单个采购店铺详情 +func (c *Client) GetPurchaserShop(shopId string) (*GetPurchaserShopRespData, error) { + path := buildPath("/purchaser-shops/" + shopId) + + data, err := c.doRequest("GET", path, nil, nil) + if err != nil { + return nil, err + } + + var result GetPurchaserShopRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// 选品池商品 (Selection Product) +// ============================================================ + +// ListSelectionProducts 分页查询选品池商品列表 +func (c *Client) ListSelectionProducts(req *ListSelectionProductsReq) (*ListSelectionProductsRespData, error) { + path := buildPath("/selection-products") + query := toQueryMap(req) + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result ListSelectionProductsRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetSelectionProduct 查询选品池商品详情 +// +// divisionCode 为可选参数,传入五级乡镇区域编码后可返回该区域可售状态和库存。 +func (c *Client) GetSelectionProduct(productId, purchaserId, divisionCode string) (*GetSelectionProductRespData, error) { + path := buildPath("/selection-products/" + productId) + query := map[string]*string{ + "PurchaserId": tea.String(purchaserId), + } + if divisionCode != "" { + query["DivisionCode"] = tea.String(divisionCode) + } + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result GetSelectionProductRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetSelectionProductSaleInfo 查询选品池商品销售信息 +func (c *Client) GetSelectionProductSaleInfo(productId, purchaserId, divisionCode string) (*GetSelectionProductSaleInfoRespData, error) { + path := buildPath("/selection-products/" + productId + "/sale-info") + query := map[string]*string{ + "PurchaserId": tea.String(purchaserId), + } + if divisionCode != "" { + query["DivisionCode"] = tea.String(divisionCode) + } + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result GetSelectionProductSaleInfoRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListSelectionProductSaleInfos 批量查询商品销售信息 +func (c *Client) ListSelectionProductSaleInfos(req *ListSelectionProductSaleInfosReq) (*ListSelectionProductSaleInfosRespData, error) { + path := buildPath("/selection-products:batch-sale-info") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result ListSelectionProductSaleInfosRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListSelectionSkuSaleInfos 批量查询SKU销售信息 +func (c *Client) ListSelectionSkuSaleInfos(req *ListSelectionSkuSaleInfosReq) (*ListSelectionSkuSaleInfosRespData, error) { + path := buildPath("/selection-skus:batch-sale-info") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result ListSelectionSkuSaleInfosRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListCategories 查询类目列表 +func (c *Client) ListCategories(req *ListCategoriesReq) (*ListCategoriesRespData, error) { + path := buildPath("/categories") + query := toQueryMap(req) + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result ListCategoriesRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// SearchProducts 搜索选品池商品 +func (c *Client) SearchProducts(req *SearchProductsReq) (*SearchProductsRespData, error) { + path := buildPath("/selection-products:search") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result SearchProductsRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// 选品池入库/出库 +// ============================================================ + +// SelectionGroupAddProduct 选品池商品入库 +func (c *Client) SelectionGroupAddProduct(req *SelectionGroupAddProductReq) (*OperationRespData, error) { + path := buildPath("/selection-group/products:add") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result OperationRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// SelectionGroupRemoveProduct 选品池商品出库 +func (c *Client) SelectionGroupRemoveProduct(req *SelectionGroupRemoveProductReq) (*OperationRespData, error) { + path := buildPath("/selection-group/products:remove") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result OperationRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// 采购单 (Purchase Order) +// ============================================================ + +// RenderPurchaseOrder 采购单渲染(下单预校验) +// +// 下单前必须调用,获取实时价格、校验商品是否可售。 +func (c *Client) RenderPurchaseOrder(req *RenderPurchaseOrderReq) (*RenderPurchaseOrderRespData, error) { + path := buildPath("/purchase-orders:render") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result RenderPurchaseOrderRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// SplitPurchaseOrder 采购单渲染并拆单 +// +// 返回拆成多份子单结构,用于前端展示,拆单结果用于创建采购单入参。 +func (c *Client) SplitPurchaseOrder(req *SplitPurchaseOrderReq) (*SplitPurchaseOrderRespData, error) { + path := buildPath("/purchase-orders:split-render") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result SplitPurchaseOrderRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// CreatePurchaseOrder 创建采购单【异步】 +// +// 重要:接口只返回采购单号,不代表下单成功,必须等待回调通知,再查询订单接口。 +// 禁止在同步返回后直接标记本地订单成功。 +func (c *Client) CreatePurchaseOrder(req *CreatePurchaseOrderReq) (*CreatePurchaseOrderRespData, error) { + path := buildPath("/purchase-orders") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result CreatePurchaseOrderRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetPurchaseOrderStatus 获取采购单状态 +// +// 状态枚举:INIT(初始化)、PROCESS(处理中)、SUCCESS(成功)、FAIL(失败)、CLOSED(已关闭) +func (c *Client) GetPurchaseOrderStatus(purchaseOrderId, purchaserId string) (*GetPurchaseOrderStatusRespData, error) { + path := buildPath("/purchase-orders/" + purchaseOrderId + "/status") + query := map[string]*string{ + "PurchaserId": tea.String(purchaserId), + } + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result GetPurchaseOrderStatusRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// 订单 (Order) +// ============================================================ + +// GetOrder 获取订单详情 +func (c *Client) GetOrder(orderId, purchaserId string) (*GetOrderRespData, error) { + path := buildPath("/orders/" + orderId) + query := map[string]*string{ + "PurchaserId": tea.String(purchaserId), + } + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result GetOrderRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// QueryOrders 查询订单列表 +func (c *Client) QueryOrders(req *QueryOrdersReq) (*QueryOrdersRespData, error) { + path := buildPath("/orders") + query := toQueryMap(req) + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result QueryOrdersRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ListLogisticsOrders 查询订单物流信息 +func (c *Client) ListLogisticsOrders(orderId, purchaserId string) (*ListLogisticsOrdersRespData, error) { + path := buildPath("/orders/" + orderId + "/logistics") + query := map[string]*string{ + "PurchaserId": tea.String(purchaserId), + } + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result ListLogisticsOrdersRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ConfirmDisburse 确认收货 +func (c *Client) ConfirmDisburse(orderId, purchaserId string) (*OperationRespData, error) { + path := buildPath("/orders/" + orderId + ":confirm-disburse") + body := map[string]*string{ + "PurchaserId": tea.String(purchaserId), + } + + data, err := c.doRequest("POST", path, nil, body) + if err != nil { + return nil, err + } + + var result OperationRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// 售后单 (Refund Order) +// ============================================================ + +// RenderRefundOrder 售后渲染预校验 +func (c *Client) RenderRefundOrder(req *RenderRefundOrderReq) (*RenderRefundOrderRespData, error) { + path := buildPath("/refund-orders:render") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result RenderRefundOrderRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// CreateRefundOrder 创建售后单 +func (c *Client) CreateRefundOrder(req *CreateRefundOrderReq) (*CreateRefundOrderRespData, error) { + path := buildPath("/refund-orders") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result CreateRefundOrderRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// CancelRefundOrder 取消售后单 +func (c *Client) CancelRefundOrder(refundOrderId, purchaserId string) (*CancelRefundOrderRespData, error) { + path := buildPath("/refund-orders/" + refundOrderId + ":cancel") + body := map[string]*string{ + "PurchaserId": tea.String(purchaserId), + } + + data, err := c.doRequest("POST", path, nil, body) + if err != nil { + return nil, err + } + + var result CancelRefundOrderRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetRefundOrder 获取售后单详情 +func (c *Client) GetRefundOrder(refundOrderId, purchaserId string) (*GetRefundOrderRespData, error) { + path := buildPath("/refund-orders/" + refundOrderId) + query := map[string]*string{ + "PurchaserId": tea.String(purchaserId), + } + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result GetRefundOrderRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// CreateGoodsShippingNotice 回填退货物流运单 +func (c *Client) CreateGoodsShippingNotice(refundOrderId string, req *CreateGoodsShippingNoticeReq) (*CreateGoodsShippingNoticeRespData, error) { + path := buildPath("/refund-orders/" + refundOrderId + ":fill-logistics") + + data, err := c.doRequest("POST", path, nil, req) + if err != nil { + return nil, err + } + + var result CreateGoodsShippingNoticeRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// 区域编码 (Division Code) +// ============================================================ + +// QueryChildDivisionCode 查询子区域编码 +// +// 不传ParentDivisionCode返回省级编码; +// 传入省返回市,传入市返回区,传入区返回乡镇(五级)。 +// 下单、商品可售校验必须拿到乡镇级DivisionCode。 +func (c *Client) QueryChildDivisionCode(parentDivisionCode string) (*QueryChildDivisionCodeRespData, error) { + path := buildPath("/divisions:children") + var query map[string]*string + if parentDivisionCode != "" { + query = map[string]*string{ + "ParentDivisionCode": tea.String(parentDivisionCode), + } + } + + data, err := c.doRequest("GET", path, query, nil) + if err != nil { + return nil, err + } + + var result QueryChildDivisionCodeRespData + if err := c.parseResponse(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// 回调解析工具 +// ============================================================ + +// CallbackEvent 回调通知事件结构体 +type CallbackEvent struct { + EventCode string `json:"EventCode"` // 事件类型 + PurchaseOrderId string `json:"PurchaseOrderId"` // 采购单ID + OrderIdList []string `json:"OrderIdList"` // 附属真实订单ID数组 + Status string `json:"Status"` // 状态 + RequestId string `json:"RequestId"` // 请求ID +} + +// ParseCallbackEvent 解析回调通知的JSON Body +// +// 业务侧在回调接口中接收阿里云POST请求,解析事件后做本地状态同步。 +// 处理完成后需返回 {"success":true} 告知阿里云消费成功。 +func ParseCallbackEvent(body []byte) (*CallbackEvent, error) { + var event CallbackEvent + if err := json.Unmarshal(body, &event); err != nil { + return nil, fmt.Errorf("failed to parse callback event: %w", err) + } + return &event, nil +} + +// CallbackSuccessResponse 回调成功响应 +// +// 业务处理完回调事件后,返回此结构告知阿里云消费成功。 +func CallbackSuccessResponse() string { + return `{"success":true}` +} + +// IsAPIError 判断错误是否为指定错误码 +func IsAPIError(err error, code string) bool { + if apiErr, ok := err.(*APIError); ok { + return apiErr.Code == code + } + return false +} + +// ============================================================ +// 辅助函数 +// ============================================================ + +// ToQueryString 将结构体转为URL查询字符串(仅用于调试) +func ToQueryString(params interface{}) string { + query := toQueryMap(params) + if len(query) == 0 { + return "" + } + var parts []string + for k, v := range query { + if v != nil { + parts = append(parts, k+"="+*v) + } + } + return strings.Join(parts, "&") +} \ No newline at end of file