diff --git a/platform/meituan_media/api.go b/platform/meituan_media/api.go new file mode 100644 index 0000000..fa6ac65 --- /dev/null +++ b/platform/meituan_media/api.go @@ -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 +} diff --git a/platform/meituan_media/api_test.go b/platform/meituan_media/api_test.go new file mode 100644 index 0000000..a3e8dc6 --- /dev/null +++ b/platform/meituan_media/api_test.go @@ -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)) +} diff --git a/platform/meituan_media/client.go b/platform/meituan_media/client.go new file mode 100644 index 0000000..fe11ce3 --- /dev/null +++ b/platform/meituan_media/client.go @@ -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", + }, + } +} diff --git a/platform/meituan_media/consts.go b/platform/meituan_media/consts.go new file mode 100644 index 0000000..da0e2cf --- /dev/null +++ b/platform/meituan_media/consts.go @@ -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 +) diff --git a/platform/meituan_media/sign.go b/platform/meituan_media/sign.go new file mode 100644 index 0000000..5f3f887 --- /dev/null +++ b/platform/meituan_media/sign.go @@ -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直接返回path,get有参数的情况下拼接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) +} diff --git a/platform/meituan_media/types.go b/platform/meituan_media/types.go new file mode 100644 index 0000000..c32b24c --- /dev/null +++ b/platform/meituan_media/types.go @@ -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表示CPS,2表示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"` // 交易类型,1:cps,2:cpa + 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"` // 非必填 到家商品券/到家闪购商品/到家医药/到店到餐、到综、酒店子订单的更新时间 +} diff --git a/platform/t3-union/api.go b/platform/t3-union/api.go index 90d2bad..c0b6bc6 100644 --- a/platform/t3-union/api.go +++ b/platform/t3-union/api.go @@ -2,14 +2,8 @@ package t3_union import ( "context" - "encoding/json" "errors" - "fmt" - "math/rand" "net/http" - "sort" - "strings" - "time" "github.com/zeromicro/go-zero/core/logx" @@ -35,60 +29,21 @@ type T3UnionApi interface { type t3UnionApiImpl struct { log logx.Logger client *Client + sign *Sign } func newT3UnionApiImpl(log logx.Logger, client *Client) T3UnionApi { + sign := newSign(client.authConfig.AppKey, client.authConfig.AppSecret) return &t3UnionApiImpl{ log: log, 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 签名 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 { return sign } @@ -98,7 +53,7 @@ func (a *t3UnionApiImpl) Sign(methodType string, uri string, dataMap map[string] // GenerateLink 生成短链 func (a *t3UnionApiImpl) GenerateLink(ctx context.Context, req GenerateLinkRequest) (*GenerateLinkData, error) { 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 { headers[k] = v } @@ -118,7 +73,7 @@ func (a *t3UnionApiImpl) GenerateLink(ctx context.Context, req GenerateLinkReque // GenerateCode 生成二维码 func (a *t3UnionApiImpl) GenerateCode(ctx context.Context, req GenerateCodeRequest) (*GenerateCodeData, error) { 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 { headers[k] = v } @@ -138,7 +93,7 @@ func (a *t3UnionApiImpl) GenerateCode(ctx context.Context, req GenerateCodeReque // GeneratePoster 生成推广海报 func (a *t3UnionApiImpl) GeneratePoster(ctx context.Context, req GeneratePosterRequest) (*GeneratePosterData, error) { 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 { headers[k] = v } @@ -161,7 +116,7 @@ func (a *t3UnionApiImpl) QueryOrderList(ctx context.Context, req QueryOrderListR req.SupplierUuid = a.client.authConfig.SupplierID } 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 { headers[k] = v } diff --git a/platform/t3-union/sign.go b/platform/t3-union/sign.go new file mode 100644 index 0000000..51fb112 --- /dev/null +++ b/platform/t3-union/sign.go @@ -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()) +} diff --git a/util/util.go b/util/util.go index bd17954..9f74881 100644 --- a/util/util.go +++ b/util/util.go @@ -1,5 +1,10 @@ package util +import ( + "errors" + "fmt" +) + //func StructToMap(obj interface{}) map[string]any { // objValue := reflect.ValueOf(obj) // objType := objValue.Type() @@ -13,3 +18,17 @@ package util // } // 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") +}