sdk_LinkedMall-20260828161641/client.go

378 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 为 *CreatePurchaseOrderRespDataPurchaseOrderId
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 为 *CreateRefundOrderRespDataRefundOrderId
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