美天赚api

This commit is contained in:
wukesheng 2024-06-22 20:28:21 +08:00
parent ce6650721f
commit 8d99e8fa41
9 changed files with 524 additions and 53 deletions

View File

@ -0,0 +1,86 @@
package meituan_media
import (
"context"
"errors"
"net/http"
"github.com/zeromicro/go-zero/core/logx"
"gitee.com/chengdu-lenntc/third-platform-sdk/client"
"gitee.com/chengdu-lenntc/third-platform-sdk/util"
)
// MeituanMediaApi 美团-美天赚
// Api defines the interface of t3_union api
type MeituanMediaApi interface {
// Sign 签名
Sign(methodType string, url string, data map[string]interface{}) string
// GenerateLink 生成推广链接
GenerateLink(ctx context.Context, req GenerateLinkRequest) (*GenerateLinkResponse, error)
// QueryOrderList 查询订单列表
QueryOrderList(ctx context.Context, req QueryOrderListRequest) (*QueryOrderData, error)
}
type meituanMediaApiImpl struct {
log logx.Logger
client *Client
sign *Sign
}
func newMeituanMediaApiImpl(log logx.Logger, client *Client, sign *Sign) MeituanMediaApi {
return &meituanMediaApiImpl{
log: log,
client: client,
sign: sign,
}
}
// Sign todo:: 签名
func (a *meituanMediaApiImpl) Sign(methodType string, uri string, data map[string]interface{}) string {
headers := a.sign.BuildHeader(methodType, uri, data)
if sign, ok := headers[SCaSignature]; ok {
return sign
}
return ""
}
// GenerateLink 生成推广链接
func (a *meituanMediaApiImpl) GenerateLink(ctx context.Context, req GenerateLinkRequest) (*GenerateLinkResponse, error) {
args := util.StructToMap(req)
headers := a.sign.BuildHeader(http.MethodPost, GetLinkUri, args)
for k, v := range a.client.headers {
headers[k] = v
}
request := &client.HttpRequest{Headers: headers, BodyArgs: args}
response := new(GenerateLinkResponse)
if err := a.client.HttpPost(GetLinkUrl, request, &client.HttpResponse{Result: response}); err != nil {
return nil, err
}
if response.Code != 0 {
a.log.WithFields(logx.LogField{Key: "data", Value: map[string]any{"req": req, "args": args, "resp": response}}).
Errorf("[meituanMediaApiImpl][GenerateLink] response result error: %s", response.Message)
return nil, errors.New(response.Message)
}
return response, nil
}
// QueryOrderList 查询订单列表
func (a *meituanMediaApiImpl) QueryOrderList(ctx context.Context, req QueryOrderListRequest) (*QueryOrderData, error) {
args := util.StructToMap(req)
headers := a.sign.BuildHeader(http.MethodPost, GetOrderListUri, args)
for k, v := range a.client.headers {
headers[k] = v
}
request := &client.HttpRequest{Headers: headers, BodyArgs: args}
response := new(QueryOrderListResponse)
if err := a.client.HttpPost(GetOrderListUrl, request, &client.HttpResponse{Result: response}); err != nil {
return nil, err
}
if response.Code != 0 {
a.log.WithFields(logx.LogField{Key: "data", Value: map[string]any{"req": req, "resp": response}}).
Errorf("[t3UnionApiImpl][QueryOrderList] response result error: %s", response.Message)
return nil, errors.New(response.Message)
}
return response.Data, nil
}

View File

@ -0,0 +1,70 @@
package meituan_media
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/suite"
"github.com/zeromicro/go-zero/core/logx"
)
// api-单元测试
type apiClientSuite struct {
suite.Suite
api MeituanMediaApi
}
func TestApiClient(t *testing.T) {
suite.Run(t, new(apiClientSuite))
}
func (a *apiClientSuite) SetupSuite() {
log := logx.WithContext(context.Background())
apiClient := NewApiClient(log, AuthConfig{
AppKey: "edf37a6019e045aeaec646220e4bd369",
AppSecret: "975a5782165041be891c098cd3afe4ce",
})
a.api = apiClient
}
func (a *apiClientSuite) Test_Sign() {
data := map[string]interface{}{
"name": "zhangsan",
"phone": "13800000001",
}
sign := a.api.Sign("POST", "/t3/union/test", data)
a.T().Logf("=====[TestSign] sign: %s", sign)
}
func (a *apiClientSuite) Test_GenerateLink() {
req := GenerateLinkRequest{
//LinkType: 1,
}
result, err := a.api.GenerateLink(context.Background(), req)
if !a.NoError(err) {
a.T().Errorf("========[Test_GenerateLink] response error:%s", err)
return
}
resultByte, err := json.Marshal(result)
if err != nil {
a.T().Errorf("========[Test_GenerateLink] json_marshal error:%s", err)
return
}
a.T().Logf("=====[Test_GenerateLink] result: %s", string(resultByte))
}
func (a *apiClientSuite) Test_QueryOrderList() {
req := QueryOrderListRequest{}
result, err := a.api.QueryOrderList(context.Background(), req)
if !a.NoError(err) {
a.T().Errorf("========[Test_QueryOrderList] response error:%s", err)
return
}
resultByte, err := json.Marshal(result)
if err != nil {
a.T().Errorf("========[Test_QueryOrderList] json_marshal error:%s", err)
return
}
a.T().Logf("=====[Test_QueryOrderList] result: %s", string(resultByte))
}

View File

@ -0,0 +1,39 @@
package meituan_media
import (
"github.com/zeromicro/go-zero/core/logx"
"gitee.com/chengdu-lenntc/third-platform-sdk/client"
)
// AuthConfig api鉴权参数
type AuthConfig struct {
AppKey string // 应用key
AppSecret string // 应用秘钥
SupplierID string // 供应商ID账号
}
// 连接第三方平台的client
type Client struct {
log logx.Logger
authConfig AuthConfig
client.HttpClient
headers map[string]string
}
func NewApiClient(log logx.Logger, conf AuthConfig) MeituanMediaApi {
clt := newClient(log, conf)
sign := newSign(conf.AppKey, conf.AppSecret)
return newMeituanMediaApiImpl(log, clt, sign)
}
func newClient(log logx.Logger, conf AuthConfig) *Client {
return &Client{
log: log,
authConfig: conf,
HttpClient: client.NewHttpClient(log),
headers: map[string]string{
"Content-Type": "application/json",
},
}
}

View File

@ -0,0 +1,27 @@
package meituan_media
// 相关地址
const (
SiteDomain = "https://media.meituan.com" // Domain 后台域名
SiteUrl = "https://media.meituan.com/pc/index.html#/" // SiteUrl 后台地址
DocUrl = "https://media.meituan.com/pc/index.html#/help" // DocUrl 文档地址
ApiDocUrl = "https://media.meituan.com/pc/index.html#/materials/api" // ApiDocUrl api文档地址
)
// header头信息
const (
SCaApp = "S-Ca-App"
SCaTimestamp = "S-Ca-Timestamp"
ContentMd5 = "Content-MD5"
SCaSignature = "S-Ca-Signature"
SCaSignatureHeaders = "S-Ca-Signature-Headers"
)
// 接口地址
const (
ApiDomain = "https://media.meituan.com" // Domain api域名
GetLinkUri = "/cps_open/common/api/v1/get_referral_link"
GetOrderListUri = "/cps_open/common/api/v1/query_order"
GetLinkUrl = ApiDomain + GetLinkUri
GetOrderListUrl = ApiDomain + GetOrderListUri
)

View File

@ -0,0 +1,117 @@
package meituan_media
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"time"
"gitee.com/chengdu-lenntc/third-platform-sdk/util"
)
type Sign struct {
AppKey string // 应用key
AppSecret string // 应用秘钥
}
func newSign(appKey, appSecret string) *Sign {
return &Sign{
AppKey: appKey,
AppSecret: appSecret,
}
}
func (s *Sign) BuildHeader(methodType string, uri string, data map[string]any) map[string]string {
headers := map[string]string{
SCaApp: s.AppKey, // 应用app_key
SCaTimestamp: fmt.Sprintf("%d", time.Now().UnixMilli()), // 请求发起时间戳(毫秒)有效时1分钟
SCaSignatureHeaders: "S-Ca-App,S-Ca-Timestamp", // 将需要签名的header
ContentMd5: s.contentMD5(methodType, data), // body数据Md5加密
}
headers[SCaSignature] = s.GetSign(methodType, uri, data, headers)
return headers
}
func (s *Sign) GetSign(methodType string, uri string, data map[string]any, signHeaders map[string]string) string {
httpMethod := s.httpMethod(methodType)
contentMD5 := s.contentMD5(methodType, data)
headers := s.headers(signHeaders)
url := s.url(methodType, uri, data)
strSign := httpMethod + `\n` + contentMD5 + `\n` + headers + url
hm := hmac.New(sha256.New, []byte(s.AppSecret))
hm.Write([]byte(strSign))
signStr := base64.StdEncoding.EncodeToString([]byte(hex.EncodeToString(hm.Sum([]byte("")))))
return signStr
}
// 请求方式大写
func (s *Sign) httpMethod(methodType string) string {
return strings.ToUpper(methodType)
}
// 请求参数执行base64+md5的值
func (s *Sign) contentMD5(methodType string, data map[string]any) string {
methodType = s.httpMethod(methodType)
if (methodType == http.MethodPost || methodType == http.MethodPut) && data != nil {
dataByte, _ := json.Marshal(data)
return base64.StdEncoding.EncodeToString([]byte(util.Md5String(string(dataByte))))
} else {
return ""
}
}
// 签名计算Header的Key拼接
func (s *Sign) headers(signHeaders map[string]string) string {
var str = ""
// key排序
sortData := sort.StringSlice{}
for k := range signHeaders {
if k != SCaSignature && k != SCaSignatureHeaders && k != ContentMd5 {
sortData = append(sortData, k)
}
}
sortData.Sort()
for _, k := range sortData {
str += k + ":" + signHeaders[k] + `\n`
}
return str
}
// url拼接
// post直接返回pathget有参数的情况下拼接url
func (s *Sign) url(methodType, uri string, data map[string]any) string {
var query = ""
methodType = s.httpMethod(methodType)
// key排序
sortData := sort.StringSlice{}
for k := range data {
sortData = append(sortData, k)
}
sortData.Sort()
if len(sortData) > 0 && methodType == http.MethodGet {
for num, key := range sortData {
value, err := util.ToString(data[key])
if err != nil {
return ""
}
str1 := ""
if len(sortData)-1 != num {
str1 = "&"
}
if len(value) > 0 {
query += fmt.Sprintf("%s=%s", key, value)
} else {
query += key
}
query += str1
}
}
return fmt.Sprintf("%s%s", uri, query)
}

View File

@ -0,0 +1,92 @@
package meituan_media
// GenerateLinkRequest 生成推广链接请求
type GenerateLinkRequest struct {
LinkType int32 `json:"linkType"` // 必填 链接类型枚举值1 H5长链接2 H5短链接3 deeplink(唤起)链接4 微信小程序唤起路径
Platform int32 `json:"platform,omitempty"` // 非必填 商品所属业务一级分类类型请求的商品推广链接所属的业务类型信息即只有输入skuViewId时才需要传本字段1 到家及其他业务类型2 到店业务类型不填则默认1
BizLine int32 `json:"bizLine,omitempty"` // 非必填 商品所属业务二级分类类型请求的商品推广链接所属的业务类型信息即只有输入skuViewId时才需要传本字段当字段platform为1选择到家及其他业务类型时5 医药不填则默认null表示外卖商品券当字段platform为2选择到店业务类型时1 到餐2 到综 3酒店 4门票 不填则默认1
ActId string `json:"actId,omitempty"` // 非必填 活动物料ID我要推广-活动推广中第一列的id信息和商品id、活动链接三选一填写不能全填
SkuViewId string `json:"skuViewId,omitempty"` // 非必填 商品id对商品查询接口返回的skuViewid和活动物料ID、活动链接三选一不能全填
Sid string `json:"sid,omitempty"` // 非必填 二级媒体身份标识用于渠道效果追踪限制64个字符仅支持英文、数字和下划线
Text string `json:"text,omitempty"` // 非必填 只支持到家外卖商品券、买菜业务类型链接和活动物料链接。活动链接即想要推广的目标链接出参会返回成自己可推的链接限定为当前可推广的活动链接或者商品券链接请求内容尽量保持在200字以内文本中仅存在一个http协议头的链接
}
// GenerateLinkResponse 生成推广链接响应
type GenerateLinkResponse struct {
Code int32 `json:"code"` // 响应码0成功其他失败
Message string `json:"message"` // 响应文案
Data string `json:"data"` // 返回对应的推广链接,这里的链接才能实现跟单计佣
SkuViewId string `json:"skuViewId"` // 若用text进行入参取链且返回的推广链接为商品券链接则返回对应商品的展示ID可以根据该ID查商品券接口获取对应的展示信息和佣金信息
}
// QueryOrderListRequest 查询订单列表请求
type QueryOrderListRequest struct {
Platform int32 `json:"platform"` // 非必填 商品所属业务一级分类类型1 到家及其他业务类型2 到店业务类型包含到店美食、休闲生活、酒店、门票不填则默认1
BusinessLine []int32 `json:"businessLine"` // 非必填 业务线标识1当platform为1选择到家及其他业务类型时业务线枚举为1外卖订单 WAIMAI 2闪购红包 3酒旅 4美团电商订单团好货 5医药 6拼好饭 7商品超值券包 COUPON 8买菜 MAICAI 11闪购商品不传则默认传空表示非售卖券包订单类型的全部查询。若输入参数含7 商品超值券包则只返回商品超值券包订单2当platform为2选择到店业务类型 时业务线枚举1到餐 2到综 3酒店 4门票不填则默认传1
ActId int64 `json:"actId"` // 非必填 活动物料id我要推广-活动推广中第一列的id信息不传则返回所有actId的数据建议查询订单时传入actId
Sid string `json:"sid"` // 非必填 二级推广位id最长64位不传则返回所有sid的数据
OrderId string `json:"orderId"` // 非必填 订单id入参后可与业务线标识businessLine配合使用输入的orderId需要与businessLine能对应上。举例如查询商品超值券包订单时orderId传券包订单号businessLine传7除此以外其他查询筛选条件不生效不传业务线标识businessLine则默认仅查非券包订单
StartTime int32 `json:"startTime"` // 非必填 查询时间类型对应的查询开始时间10位时间戳表示单位秒
EndTime int32 `json:"endTime"` // 非必填 查询时间类型对应的查询结束时间10位时间戳表示单位秒
Page int32 `json:"page"` // 非必填 页码默认1从1开始,若searchType选择2本字段必须传1若不传参数默认1
Limit int32 `json:"limit"` // 非必填 每页限制条数默认100最大支持100
QueryTimeType int32 `json:"queryTimeType"` // 非必填 查询时间类型,枚举值, 1 按订单支付时间查询, 2 按照更新时间查询, 默认为1
TradeType int32 `json:"tradeType"` // 非必填 交易类型1表示CPS2表示CPA
ScrollId string `json:"scrollId"` // 非必填 分页id当searchType选择2逐页查询时本字段为必填。若不填写默认查询首页。取值为上一页查询时出参的scrollId字段
SearchType int32 `json:"searchType"` // 非必填 订单分页查询方案选择不填则默认为1。1 分页查询最多能查询到1万条订单当选择本查询方案page参数不能为空。此查询方式后续不再维护建议使用2逐页查询。2 逐页查询不限制查询订单数只能逐页查询不能指定页数当选择本查询方案需配合scrollId参数使用
CityNames []string `json:"cityNames"` // 非必填 可输入城市名称圈定特定城市的订单单次最多查询10个城市英文逗号分隔。不传则默认全部城市订单。 注:如需确认城市具体名称,可参考后台订单明细页的城市筛选项,或参考具体活动的城市命名。目前只支持到家业务类型-商品超值券包业务线。
}
// QueryOrderListResponse 查询订单列表响应
type QueryOrderListResponse struct {
Code int `json:"code"` // 响应码0成功其他值为失败
Message string `json:"message"` // 响应文案
Data *QueryOrderData `json:"data"` // 响应结果信息
}
type QueryOrderData struct {
ActId int64 `json:"actId"` // 活动物料id我要推广-活动推广中第一列的id信息
SkuCount int32 `json:"skuCount"` // 查询返回本页的数量合计无实际使用场景若查询订单购买商品数可以看返回的dataList中skuCount
ScrollId string `json:"scrollId"` // 分页id当searchType选择2逐页查询时出参会返回本字段。用于下一页查询的scrollId字段入参使用
DataList []*OrderItem `json:"dataList"` // 数据列表
}
type OrderItem struct {
BusinessLine int32 `json:"businessLine"` // 业务线,同入参枚举说明
OrderId string `json:"orderId"` // 订单ID
PayTime int64 `json:"payTime"` // 订单支付时间
PayPrice string `json:"payPrice"` // 订单支付价格。针对到餐、到综、酒店、闪购、医药业务类型,为父订单的支付价格,单位元
UpdateTime int64 `json:"updateTime"` // 订单最近一次的更新时间。到家外卖商品券、到家医药、到家闪购商品业务、到店到餐、到综、酒店类型,订单时间为用户买券包的更新时间,非每张券的更新时间。针对以上业务类型,建议查询单张券的更新时间
CommissionRate string `json:"commissionRate"` // 订单预估佣金比例300表示3%
Profit string `json:"profit"` // 订单整体的预估佣金收入单位元1.60表示1.6元
CpaProfit string `json:"cpaProfit"` // cpa类型的预估佣金收入单位元6.50表示6.5元
Sid string `json:"sid"` // 二级媒体身份标识,用于渠道效果追踪
ProductId string `json:"productId"` // 产品ID对应商品查询接口的skuViewId目前只支持到家外卖商品券、到家医药、到家闪购商品业务、到店业务类型
ProductName string `json:"productName"` // 产品名称,外卖订单展示店铺名称,到店取单个商品券的名称、其他展示全部商品名称
OrderDetail *OrderDetail `json:"orderDetail"` // 订单详情,只支持到家外卖商品券、到家医药、到家闪购商品业务、到店到餐、到综、酒店类型返回数据
RefundPrice string `json:"refundPrice"` // 只对非到店到餐、非到综、非酒店业务类型有效。订单维度退款价格,该笔订单用户发生退款行为时的退款计佣金额之和,超值券包订单本期不返回退款数据,单位元
RefundTime string `json:"refundTime"` // 只对非到店到餐、非到综、非酒店业务类型有效。订单维度最新一次发生退款的时间;超值券包订单本期不返回退款数据,单位元
RefundProfit string `json:"refundProfit"` // 只对非到店到餐、非到综、非酒店业务类型有效。订单维度退款预估佣金,该笔订单用户发生退款行为时的退款预估佣金金额之和;超值券包订单本期不返回退款数据,单位元
CpaRefundProfit string `json:"cpaRefundProfit"` // cpa退款预估佣金单位元
Status string `json:"status"` // 表示订单维度状态,枚举有 2付款如果是CPA订单则表示奖励已创建 3完成 4取消 5风控 6结算
TradeType int32 `json:"tradeType"` // 交易类型1cps2cpa
ActId int64 `json:"actId"` // 活动物料id我要推广-活动推广中第一列的id信息
Appkey string `json:"appkey"` // 归因到的appKey对应取链时入参的appkey
SkuCount int32 `json:"skuCount"` // 表示sku数量团好货和券包类型的CPS订单返回有值其余类型订单不返回该值
CityName string `json:"cityName"` // 订单所属的城市,目前支持二级城市粒度。目前只支持到家业务类型-商品超值券包业务线。
}
// 订单详情
type OrderDetail struct {
CouponStatus string `json:"couponStatus"` // 非必填 本期只有到到家外卖商品券、到家医药、到家闪购商品业务、到店到餐、到综、酒店业务类型展示订单明细,表示商品券/子订单推广计佣状态1、付款2、完成或券已核销3、结算4、失效含取消或风控的情况
ItemOrderId string `json:"itemOrderId"` // 非必填 针对到店到餐、到综、酒店商品券,返回商品券的子订单号。其他业务类型不返回
FinishTime string `json:"finishTime"` // 非必填 1、针对到家外卖商品券返回商品券核销完成履约的实物菜品订单号对应的完成时间2、针对到家医药&闪购商品返回商品订单完成时间3、针对到店到餐、到综、酒店子订单返回子订单对应的券核销时间
BasicAmount string `json:"basicAmount"` // 非必填 商品的计佣金额,每个商品对应的支付分摊金额,单位元
CouponFee string `json:"couponFee"` // 非必填 商品的佣金当推广状态为失效、取消、风控时佣金值为0单位元
OrderViewId string `json:"orderViewId"` // 非必填 只对到家外卖商品券有效。商品券的核销完成履约的实物菜品订单号
RefundAmount string `json:"refundAmount"` // 非必填 到店到餐、到综、酒店子订单、到家闪购商品、到家医药业务类型的退款金额,到家其他业务类型不返回数据,单位元
RefundFee string `json:"refundFee"` // 非必填 到店到餐、到综、酒店子订单、到家闪购商品、到家医药业务类型的退款佣金,到家其他业务类型不返回数据,单位元
RefundTime string `json:"refundTime"` // 非必填 到店到餐、到综、酒店子订单、到家闪购商品、到家医药业务类型的退款时间,到家其他业务类型不返回数据
SettleTime string `json:"settleTime"` // 非必填 到家商品券/到家闪购商品/到店到餐/到综/酒店子订单的结算时间,完成并且进入结算账期时则变为结算状态。若存在多次结算记录则取最新结算时间
UpdateTime string `json:"updateTime"` // 非必填 到家商品券/到家闪购商品/到家医药/到店到餐、到综、酒店子订单的更新时间
}

View File

@ -2,14 +2,8 @@ package t3_union
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"math/rand"
"net/http" "net/http"
"sort"
"strings"
"time"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
@ -35,60 +29,21 @@ type T3UnionApi interface {
type t3UnionApiImpl struct { type t3UnionApiImpl struct {
log logx.Logger log logx.Logger
client *Client client *Client
sign *Sign
} }
func newT3UnionApiImpl(log logx.Logger, client *Client) T3UnionApi { func newT3UnionApiImpl(log logx.Logger, client *Client) T3UnionApi {
sign := newSign(client.authConfig.AppKey, client.authConfig.AppSecret)
return &t3UnionApiImpl{ return &t3UnionApiImpl{
log: log, log: log,
client: client, client: client,
sign: sign,
} }
} }
func (a *t3UnionApiImpl) buildHeader(methodType string, uri string, data map[string]any) map[string]string {
// 随机数
tn := time.Now()
src := rand.NewSource(tn.Unix())
randNum := rand.New(src).Int31n(10000)
nonce := util.Md5String(fmt.Sprintf("%d_nonce_%d", tn.UnixMicro(), randNum))
headers := map[string]string{
XT3Nonce: nonce, // 请求标识10分钟内保证唯一性
XT3Timestamp: fmt.Sprintf("%d", time.Now().UnixMilli()), // 请求发起时间戳(毫秒)有效时1分钟
XT3Version: "V1", // 版本(固定值传"V1"
XT3Key: a.client.authConfig.AppKey, // 使用前联系分配账号APP Key
XT3SignatureMethod: "MD5", // 固定值传 "MD5"
}
headers[XT3Signature] = a.getSign(methodType, uri, headers, data) // 在获得签名后赋值
headers[XT3SignatureHeaders] = "x-t3-nonce,x-t3-timestamp,x-t3-version,x-t3-key,x-t3-signature-method" // 固定传值
return headers
}
func (a *t3UnionApiImpl) getSign(methodType string, uri string, headers map[string]string, data map[string]any) string {
// key排序
arr := sort.StringSlice{}
for k := range headers {
if k != XT3Signature && k != XT3SignatureHeaders {
arr = append(arr, k)
}
}
arr.Sort()
// 参数拼接
var build strings.Builder
build.WriteString(fmt.Sprintf("%s%s", methodType, uri))
for _, k := range arr {
build.WriteString(fmt.Sprintf("%s:%v", k, headers[k]))
}
// 请求参数body转json string后用MD5进行加密
dataByte, _ := json.Marshal(data)
build.WriteString(util.Md5String(string(dataByte)))
build.WriteString(a.client.authConfig.AppSecret)
return util.Md5String(build.String())
}
// Sign 签名 // Sign 签名
func (a *t3UnionApiImpl) Sign(methodType string, uri string, dataMap map[string]any) string { func (a *t3UnionApiImpl) Sign(methodType string, uri string, dataMap map[string]any) string {
headers := a.buildHeader(methodType, uri, dataMap) headers := a.sign.BuildHeader(methodType, uri, dataMap)
if sign, ok := headers[XT3Signature]; ok { if sign, ok := headers[XT3Signature]; ok {
return sign return sign
} }
@ -98,7 +53,7 @@ func (a *t3UnionApiImpl) Sign(methodType string, uri string, dataMap map[string]
// GenerateLink 生成短链 // GenerateLink 生成短链
func (a *t3UnionApiImpl) GenerateLink(ctx context.Context, req GenerateLinkRequest) (*GenerateLinkData, error) { func (a *t3UnionApiImpl) GenerateLink(ctx context.Context, req GenerateLinkRequest) (*GenerateLinkData, error) {
args := util.StructToMap(req) args := util.StructToMap(req)
headers := a.buildHeader(http.MethodPost, GetLinkUri, args) headers := a.sign.BuildHeader(http.MethodPost, GetLinkUri, args)
for k, v := range a.client.headers { for k, v := range a.client.headers {
headers[k] = v headers[k] = v
} }
@ -118,7 +73,7 @@ func (a *t3UnionApiImpl) GenerateLink(ctx context.Context, req GenerateLinkReque
// GenerateCode 生成二维码 // GenerateCode 生成二维码
func (a *t3UnionApiImpl) GenerateCode(ctx context.Context, req GenerateCodeRequest) (*GenerateCodeData, error) { func (a *t3UnionApiImpl) GenerateCode(ctx context.Context, req GenerateCodeRequest) (*GenerateCodeData, error) {
args := util.StructToMap(req) args := util.StructToMap(req)
headers := a.buildHeader(http.MethodPost, GetMiniCodeUri, args) headers := a.sign.BuildHeader(http.MethodPost, GetMiniCodeUri, args)
for k, v := range a.client.headers { for k, v := range a.client.headers {
headers[k] = v headers[k] = v
} }
@ -138,7 +93,7 @@ func (a *t3UnionApiImpl) GenerateCode(ctx context.Context, req GenerateCodeReque
// GeneratePoster 生成推广海报 // GeneratePoster 生成推广海报
func (a *t3UnionApiImpl) GeneratePoster(ctx context.Context, req GeneratePosterRequest) (*GeneratePosterData, error) { func (a *t3UnionApiImpl) GeneratePoster(ctx context.Context, req GeneratePosterRequest) (*GeneratePosterData, error) {
args := util.StructToMap(req) args := util.StructToMap(req)
headers := a.buildHeader(http.MethodPost, GetPosterUri, args) headers := a.sign.BuildHeader(http.MethodPost, GetPosterUri, args)
for k, v := range a.client.headers { for k, v := range a.client.headers {
headers[k] = v headers[k] = v
} }
@ -161,7 +116,7 @@ func (a *t3UnionApiImpl) QueryOrderList(ctx context.Context, req QueryOrderListR
req.SupplierUuid = a.client.authConfig.SupplierID req.SupplierUuid = a.client.authConfig.SupplierID
} }
args := util.StructToMap(req) args := util.StructToMap(req)
headers := a.buildHeader(http.MethodPost, GetOrderListUri, args) headers := a.sign.BuildHeader(http.MethodPost, GetOrderListUri, args)
for k, v := range a.client.headers { for k, v := range a.client.headers {
headers[k] = v headers[k] = v
} }

66
platform/t3-union/sign.go Normal file
View File

@ -0,0 +1,66 @@
package t3_union
import (
"encoding/json"
"fmt"
"math/rand"
"sort"
"strings"
"time"
"gitee.com/chengdu-lenntc/third-platform-sdk/util"
)
type Sign struct {
AppKey string // 应用key
AppSecret string // 应用秘钥
}
func newSign(appKey, appSecret string) *Sign {
return &Sign{
AppKey: appKey,
AppSecret: appSecret,
}
}
func (s *Sign) BuildHeader(methodType string, uri string, data map[string]any) map[string]string {
// 随机数
tn := time.Now()
src := rand.NewSource(tn.Unix())
randNum := rand.New(src).Int31n(10000)
nonce := util.Md5String(fmt.Sprintf("%d_nonce_%d", tn.UnixMicro(), randNum))
headers := map[string]string{
XT3Nonce: nonce, // 请求标识10分钟内保证唯一性
XT3Timestamp: fmt.Sprintf("%d", time.Now().UnixMilli()), // 请求发起时间戳(毫秒)有效时1分钟
XT3Version: "V1", // 版本(固定值传"V1"
XT3Key: s.AppKey, // 使用前联系分配账号APP Key
XT3SignatureMethod: "MD5", // 固定值传 "MD5"
}
headers[XT3Signature] = s.GetSign(methodType, uri, headers, data) // 在获得签名后赋值
headers[XT3SignatureHeaders] = "x-t3-nonce,x-t3-timestamp,x-t3-version,x-t3-key,x-t3-signature-method" // 固定传值
return headers
}
func (s *Sign) GetSign(methodType string, uri string, headers map[string]string, data map[string]any) string {
// key排序
arr := sort.StringSlice{}
for k := range headers {
if k != XT3Signature && k != XT3SignatureHeaders {
arr = append(arr, k)
}
}
arr.Sort()
// 参数拼接
var build strings.Builder
build.WriteString(fmt.Sprintf("%s%s", methodType, uri))
for _, k := range arr {
build.WriteString(fmt.Sprintf("%s:%v", k, headers[k]))
}
// 请求参数body转json string后用MD5进行加密
dataByte, _ := json.Marshal(data)
build.WriteString(util.Md5String(string(dataByte)))
build.WriteString(s.AppSecret)
return util.Md5String(build.String())
}

View File

@ -1,5 +1,10 @@
package util package util
import (
"errors"
"fmt"
)
//func StructToMap(obj interface{}) map[string]any { //func StructToMap(obj interface{}) map[string]any {
// objValue := reflect.ValueOf(obj) // objValue := reflect.ValueOf(obj)
// objType := objValue.Type() // objType := objValue.Type()
@ -13,3 +18,17 @@ package util
// } // }
// return data // return data
//} //}
func ToString(value any) (string, error) {
switch val := value.(type) {
case string:
return val, nil
case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return fmt.Sprint(val), nil
case []byte:
return string(val), nil
case fmt.Stringer:
return val.String(), nil
}
return "", errors.New("无法转为string")
}