添加文件: client.go

This commit is contained in:
renzhiyuan 2026-08-28 16:16:44 +08:00
parent b93a3b87c1
commit 0facb7f445
1 changed files with 378 additions and 0 deletions

378
client.go Normal file
View File

@ -0,0 +1,378 @@
// 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