// Package sdk_LinkedMall 是 LinkedMall OpenAPI 的 Go SDK。 // 当前覆盖指定的 25 个接口,覆盖:采购方店铺、选品池、类目、搜索、 // 采购单(渲染/拆单/创建/状态)、订单、物流、确认收货、售后、区域编码等。 // // 客户端基于阿里云通用 OpenAPI 客户端(darabonba-openapi/v2)发起 ROA 调用, // ROA 签名由底层客户端自动完成。所有接口返回统一外层结构 CommonResponse, // 必须判断 Success 字段为 true 才代表业务成功。 package sdk_LinkedMall import ( "context" "encoding/json" "fmt" "io" "reflect" "strconv" "strings" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" ) // 常量定义。 const ( // RegionID 为华北3(张家口)。 RegionID = "cn-zhangjiakou" // Endpoint 为网关访问地址。 Endpoint = "linkedmall.cn-zhangjiakou.aliyuncs.com" // APIVersion 为 API 版本号。 APIVersion = "linkedmall/2023-09-30" // BasePath 为 HTTP 基础路径前缀。 BasePath = "/opensaas-s2b/opensaas-s2b-biz-trade/v2" ) // Client 是 LinkedMall OpenAPI 客户端。 // // 通过 RAM 子账号 AccessKey 初始化,禁止使用主账号 AccessKey; // 签名类型为 ROA 签名,由底层 darabonba-openapi/v2 客户端自动处理,业务无需自行实现。 type Client struct { api *openapi.Client } // NewClient 使用 RAM 子账号 AccessKeyId / AccessKeySecret 创建客户端。 // // 建议通过环境变量注入密钥,避免硬编码到代码中。 func NewClient(accessKeyID, accessKeySecret string) (*Client, error) { config := &openapi.Config{ AccessKeyId: strPtr(accessKeyID), AccessKeySecret: strPtr(accessKeySecret), RegionId: strPtr(RegionID), Endpoint: strPtr(Endpoint), ReadTimeout: intPtr(30000), ConnectTimeout: intPtr(10000), } api, err := openapi.NewClient(config) if err != nil { return nil, fmt.Errorf("create openapi client: %w", err) } return &Client{api: api}, nil } // NewLinkedMallClient 是 NewClient 的别名,便于文档示例直接使用。 func NewLinkedMallClient(accessKeyID, accessKeySecret string) (*Client, error) { return NewClient(accessKeyID, accessKeySecret) } // doRoa 发起一次 ROA 请求并解析统一的公共响应结构。 // // dataOut 用于接收业务 Data,传入对应响应数据类型的指针; // 若为 nil,则 Data 保持为 json.RawMessage。 func (c *Client) doRoa(ctx context.Context, method, path string, query map[string]*string, body interface{}, dataOut interface{}) (*CommonResponse, error) { if err := ctx.Err(); err != nil { return nil, err } fullPath := BasePath + path var bodyBytes []byte var err error if body != nil { bodyBytes, err = MarshalJSON(body) if err != nil { return nil, fmt.Errorf("marshal request body: %w", err) } } resp, err := c.api.RoaRequest(strPtr(method), strPtr(fullPath), query, bodyBytes, nil) if err != nil { return nil, fmt.Errorf("roa request failed [%s %s]: %w", method, fullPath, err) } if resp == nil || resp.Body == nil { return nil, fmt.Errorf("roa request returned empty response [%s %s]", method, fullPath) } respBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response body: %w", err) } var raw rawResponse if err := json.Unmarshal(respBytes, &raw); err != nil { return nil, fmt.Errorf("unmarshal response [%s %s] body=%s: %w", method, fullPath, string(respBytes), err) } out := &CommonResponse{ RequestId: raw.RequestId, Success: raw.Success, Code: raw.Code, Message: raw.Message, SubCode: raw.SubCode, SubMessage: raw.SubMessage, } if len(raw.Data) > 0 { if dataOut != nil { if err := json.Unmarshal(raw.Data, dataOut); err != nil { return nil, fmt.Errorf("unmarshal response Data [%s %s]: %w", method, fullPath, err) } out.Data = dataOut } else { out.Data = raw.Data } } return out, nil } // structToQuery 将请求结构体转换为 ROA query 参数。 // 仅序列化带 json tag 且非零值的字段;json:"-" 或空值字段被忽略。 func structToQuery(v interface{}) map[string]*string { query := make(map[string]*string) if v == nil { return query } rv := reflect.ValueOf(v) if rv.Kind() == reflect.Ptr { if rv.IsNil() { return query } rv = rv.Elem() } if rv.Kind() != reflect.Struct { return query } rt := rv.Type() for i := 0; i < rv.NumField(); i++ { sf := rt.Field(i) if !sf.IsExported() { continue } name := sf.Tag.Get("json") if name == "" || name == "-" { continue } if idx := strings.Index(name, ","); idx >= 0 { name = name[:idx] } if name == "" { continue } fv := rv.Field(i) switch fv.Kind() { case reflect.String: if s := fv.String(); s != "" { query[name] = strPtr(s) } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if n := fv.Int(); n != 0 { query[name] = strPtr(strconv.FormatInt(n, 10)) } case reflect.Bool: if fv.Bool() { query[name] = strPtr("true") } } } return query } // buildPath 将路径中的 {Param} 占位符替换为实际值。 func buildPath(path string, params map[string]string) string { for k, v := range params { path = strings.ReplaceAll(path, "{"+k+"}", v) } return path } // ListPurchaserShops 分页获取采购方店铺列表。 // // 请求字段:PageNum(页码,默认1)、PageSize(每页条数,最大100)。 // 返回 Data 为 *ListPurchaserShopsRespData。 // 必须判断 resp.Success 为 true 才算业务成功。 func (c *Client) ListPurchaserShops(ctx context.Context, req *ListPurchaserShopsRequest) (*CommonResponse, error) { return c.doRoa(ctx, "GET", "/purchaser-shops", structToQuery(req), nil, &ListPurchaserShopsRespData{}) } // GetPurchaserShop 获取单个采购店铺详情。 // // ShopId 为路径参数。返回 Data 为 *GetPurchaserShopRespData。 func (c *Client) GetPurchaserShop(ctx context.Context, req *GetPurchaserShopRequest) (*CommonResponse, error) { path := buildPath("/purchaser-shops/{ShopId}", map[string]string{"ShopId": req.ShopId}) return c.doRoa(ctx, "GET", path, structToQuery(req), nil, &GetPurchaserShopRespData{}) } // ListSelectionProducts 分页查询选品池商品列表。 // // 请求字段:PageNum、PageSize(最大100)、PurchaserId(必填)。 // 返回 Data 为 *ListSelectionProductsRespData。 func (c *Client) ListSelectionProducts(ctx context.Context, req *ListSelectionProductsRequest) (*CommonResponse, error) { return c.doRoa(ctx, "GET", "/selection-products", structToQuery(req), nil, &ListSelectionProductsRespData{}) } // GetSelectionProduct 查询选品池商品详情。 // // ProductId 为路径参数;支持传入五级乡镇 DivisionCode 校验该区域是否可售。 // 返回 Data 为 *GetSelectionProductRespData。 func (c *Client) GetSelectionProduct(ctx context.Context, req *GetSelectionProductRequest) (*CommonResponse, error) { path := buildPath("/selection-products/{ProductId}", map[string]string{"ProductId": req.ProductId}) return c.doRoa(ctx, "GET", path, structToQuery(req), nil, &GetSelectionProductRespData{}) } // ListCategories 查询类目列表。 // // 不传 ParentCategoryId 时查询一级类目。返回 Data 为 *ListCategoriesRespData。 func (c *Client) ListCategories(ctx context.Context, req *ListCategoriesRequest) (*CommonResponse, error) { return c.doRoa(ctx, "GET", "/categories", structToQuery(req), nil, &ListCategoriesRespData{}) } // SearchProducts 搜索选品池商品。 // // 返回 Data 为 *SearchProductsRespData。 func (c *Client) SearchProducts(ctx context.Context, req *SearchProductsRequest) (*CommonResponse, error) { return c.doRoa(ctx, "POST", "/selection-products:search", nil, req, &SearchProductsRespData{}) } // SelectionGroupAddProduct 选品池商品入库。 // // 返回 Data 为 *SelectionGroupChangeProductRespData。 func (c *Client) SelectionGroupAddProduct(ctx context.Context, req *SelectionGroupAddProductRequest) (*CommonResponse, error) { return c.doRoa(ctx, "POST", "/selection-group/products:add", nil, req, &SelectionGroupChangeProductRespData{}) } // SelectionGroupRemoveProduct 选品池商品出库。 // // 返回 Data 为 *SelectionGroupChangeProductRespData。 func (c *Client) SelectionGroupRemoveProduct(ctx context.Context, req *SelectionGroupRemoveProductRequest) (*CommonResponse, error) { return c.doRoa(ctx, "POST", "/selection-group/products:remove", nil, req, &SelectionGroupChangeProductRespData{}) } // RenderPurchaseOrder 采购单渲染(下单预校验)。 // // 下单前必须调用,获取实时价格、校验商品是否可售。 // 返回 Data 为 *RenderPurchaseOrderRespData。 func (c *Client) RenderPurchaseOrder(ctx context.Context, req *RenderPurchaseOrderReq) (*CommonResponse, error) { return c.doRoa(ctx, "POST", "/purchase-orders:render", nil, req, &RenderPurchaseOrderRespData{}) } // SplitPurchaseOrder 采购单渲染并拆单。 // // 请求体结构与 RenderPurchaseOrderReq 完全一致。 // 返回 Data 为 *SplitPurchaseOrderRespData,拆单结果用于创建采购单入参。 func (c *Client) SplitPurchaseOrder(ctx context.Context, req *RenderPurchaseOrderReq) (*CommonResponse, error) { return c.doRoa(ctx, "POST", "/purchase-orders:split-render", nil, req, &SplitPurchaseOrderRespData{}) } // CreatePurchaseOrder 创建采购单【异步】。 // // 注意:接口只返回采购单号,不代表下单成功,必须等待回调通知后再查询订单接口。 // 返回 Data 为 *CreatePurchaseOrderRespData(PurchaseOrderId)。 func (c *Client) CreatePurchaseOrder(ctx context.Context, req *CreatePurchaseOrderReq) (*CommonResponse, error) { return c.doRoa(ctx, "POST", "/purchase-orders", nil, req, &CreatePurchaseOrderRespData{}) } // GetPurchaseOrderStatus 获取采购单状态。 // // PurchaseOrderId 为路径参数。返回 Data 为 *GetPurchaseOrderStatusRespData。 // 状态枚举:INIT、PROCESS、SUCCESS、FAIL、CLOSED。 func (c *Client) GetPurchaseOrderStatus(ctx context.Context, req *GetPurchaseOrderStatusRequest) (*CommonResponse, error) { path := buildPath("/purchase-orders/{PurchaseOrderId}/status", map[string]string{"PurchaseOrderId": req.PurchaseOrderId}) return c.doRoa(ctx, "GET", path, structToQuery(req), nil, &GetPurchaseOrderStatusRespData{}) } // GetOrder 获取订单详情。 // // OrderId 为路径参数。返回 Data 为 *GetOrderRespData。 func (c *Client) GetOrder(ctx context.Context, req *GetOrderRequest) (*CommonResponse, error) { path := buildPath("/orders/{OrderId}", map[string]string{"OrderId": req.OrderId}) return c.doRoa(ctx, "GET", path, structToQuery(req), nil, &GetOrderRespData{}) } // QueryOrders 查询订单列表。 // // 返回 Data 为 *QueryOrdersRespData。 func (c *Client) QueryOrders(ctx context.Context, req *QueryOrdersRequest) (*CommonResponse, error) { return c.doRoa(ctx, "GET", "/orders", structToQuery(req), nil, &QueryOrdersRespData{}) } // ListLogisticsOrders 查询订单物流信息。 // // OrderId 为路径参数。返回 Data 为 *ListLogisticsOrdersRespData。 func (c *Client) ListLogisticsOrders(ctx context.Context, req *ListLogisticsOrdersRequest) (*CommonResponse, error) { path := buildPath("/orders/{OrderId}/logistics", map[string]string{"OrderId": req.OrderId}) return c.doRoa(ctx, "GET", path, structToQuery(req), nil, &ListLogisticsOrdersRespData{}) } // ConfirmDisburse 确认收货。 // // OrderId 为路径参数,请求体为 {"PurchaserId":"xxx"}。 func (c *Client) ConfirmDisburse(ctx context.Context, req *ConfirmDisburseRequest) (*CommonResponse, error) { path := buildPath("/orders/{OrderId}:confirm-disburse", map[string]string{"OrderId": req.OrderId}) body := map[string]string{"PurchaserId": req.PurchaserId} return c.doRoa(ctx, "POST", path, nil, body, nil) } // RenderRefundOrder 售后渲染预校验。 // // 返回 Data 为 *RenderRefundOrderRespData。 func (c *Client) RenderRefundOrder(ctx context.Context, req *RenderRefundOrderReq) (*CommonResponse, error) { return c.doRoa(ctx, "POST", "/refund-orders:render", nil, req, &RenderRefundOrderRespData{}) } // CreateRefundOrder 创建售后单。 // // 返回 Data 为 *CreateRefundOrderRespData(RefundOrderId)。 func (c *Client) CreateRefundOrder(ctx context.Context, req *CreateRefundOrderReq) (*CommonResponse, error) { return c.doRoa(ctx, "POST", "/refund-orders", nil, req, &CreateRefundOrderRespData{}) } // CancelRefundOrder 取消售后单。 // // RefundOrderId 为路径参数,请求体为 {"PurchaserId":"xxx"}。 func (c *Client) CancelRefundOrder(ctx context.Context, req *CancelRefundOrderRequest) (*CommonResponse, error) { path := buildPath("/refund-orders/{RefundOrderId}:cancel", map[string]string{"RefundOrderId": req.RefundOrderId}) body := map[string]string{"PurchaserId": req.PurchaserId} return c.doRoa(ctx, "POST", path, nil, body, nil) } // GetRefundOrder 获取售后单详情。 // // RefundOrderId 为路径参数。返回 Data 为 *GetRefundOrderRespData。 func (c *Client) GetRefundOrder(ctx context.Context, req *GetRefundOrderRequest) (*CommonResponse, error) { path := buildPath("/refund-orders/{RefundOrderId}", map[string]string{"RefundOrderId": req.RefundOrderId}) return c.doRoa(ctx, "GET", path, structToQuery(req), nil, &GetRefundOrderRespData{}) } // CreateGoodsShippingNotice 回填退货物流运单。 // // RefundOrderId 为路径参数。 func (c *Client) CreateGoodsShippingNotice(ctx context.Context, req *CreateGoodsShippingNoticeReq) (*CommonResponse, error) { path := buildPath("/refund-orders/{RefundOrderId}:fill-logistics", map[string]string{"RefundOrderId": req.RefundOrderId}) return c.doRoa(ctx, "POST", path, nil, req, nil) } // QueryChildDivisionCode 查询子区域编码。 // // 不传 ParentDivisionCode 返回省级;传入省返回市,传入市返回区,传入区返回乡镇(五级)。 // 下单、商品可售校验必须使用乡镇级 DivisionCode。 // 返回 Data 为 *QueryChildDivisionCodeRespData。 func (c *Client) QueryChildDivisionCode(ctx context.Context, req *QueryChildDivisionCodeRequest) (*CommonResponse, error) { return c.doRoa(ctx, "GET", "/divisions:children", structToQuery(req), nil, &QueryChildDivisionCodeRespData{}) } // ParseCallbackEvent 解析阿里云消息回调的请求体。 // // 收到回调后应返回 CallbackSuccessBody(),否则阿里云会重试推送。 // 同一事件可能多次推送,业务侧回调处理需做好幂等。 func ParseCallbackEvent(body []byte) (*CallbackEvent, error) { var evt CallbackEvent if err := json.Unmarshal(body, &evt); err != nil { return nil, fmt.Errorf("parse callback event: %w", err) } return &evt, nil } // CallbackSuccessBody 返回告知阿里云消费成功的响应体 {"success":true}。 func CallbackSuccessBody() []byte { return []byte(`{"success":true}`) } // strPtr 返回 string 变量的指针。 func strPtr(s string) *string { return &s } // intPtr 返回 int 变量的指针。 func intPtr(i int) *int { return &i } ``` ```go