diff --git a/index.go b/index.go index 345fa5c..6d1fa96 100644 --- a/index.go +++ b/index.go @@ -7,6 +7,7 @@ import ( elemeunion "gitee.com/chengdu-lenntc/third-platform-sdk/platform/eleme-union" meituancsr "gitee.com/chengdu-lenntc/third-platform-sdk/platform/meituan-csr" meituanunion "gitee.com/chengdu-lenntc/third-platform-sdk/platform/meituan-union" + t3_union "gitee.com/chengdu-lenntc/third-platform-sdk/platform/t3-union" ) // Platform 第三方平台 @@ -19,14 +20,17 @@ const ( PlatformMeituanUnion = "meituan_union" // PlatformDidiUnion 滴滴联盟 PlatformDidiUnion = "didi_union" + // PlatformT3Union t3出行联盟 + PlatformT3Union = "t3_union" ) -// 平台名称 +// PlatformNameMap 平台名称 var PlatformNameMap = map[string]string{ PlatformElemeUnion: "饿了么联盟", PlatformMeituanCsr: "美团分销联盟", PlatformMeituanUnion: "美团联盟", PlatformDidiUnion: "滴滴联盟", + PlatformT3Union: "t3联盟", } // GetPlatformName 获取平台名称 @@ -53,3 +57,7 @@ func NewMeituanUnionApi(log logx.Logger, conf meituanunion.AuthConfig) meituanun func NewDidiUnionApi(log logx.Logger, conf didiunion.AuthConfig) didiunion.DidiUnionApi { return didiunion.NewApiClient(log, conf) } + +func NewT3UnionApi(log logx.Logger, conf t3_union.AuthConfig) t3_union.T3UnionApi { + return t3_union.NewApiClient(log, conf) +} diff --git a/platform/didi-union/api.go b/platform/didi-union/api.go index d17f4e8..36aad93 100644 --- a/platform/didi-union/api.go +++ b/platform/didi-union/api.go @@ -16,7 +16,7 @@ import ( // todo:: 定义统一的返回错误结构 // DidiUnionApi 调用第三方平台的api -// Api defines the interface of eleme_union api +// Api defines the interface of didi_union api type DidiUnionApi interface { // Sign 签名 Sign(data map[string]interface{}) string diff --git a/platform/meituan-csr/consts.go b/platform/meituan-csr/consts.go index ae404a2..5321eca 100644 --- a/platform/meituan-csr/consts.go +++ b/platform/meituan-csr/consts.go @@ -10,6 +10,6 @@ const ( // 接口地址 const ( - Domain = "https://union.dianping.com" // Domain 域名 - GetLinkUrl = Domain + "/api/promotion/link" + ApiDomain = "https://union.dianping.com" // Domain 域名 + GetLinkUrl = ApiDomain + "/api/promotion/link" ) diff --git a/platform/meituan-union/api.go b/platform/meituan-union/api.go index 346c7ea..e086437 100644 --- a/platform/meituan-union/api.go +++ b/platform/meituan-union/api.go @@ -85,7 +85,7 @@ func (a *meituanUnionApiImpl) MiniCode(params MiniCodeRequest) (*MimiCodeRespons queryArgs := util.StructToMap(request) req := &client.HttpRequest{Headers: a.client.Headers, QueryArgs: queryArgs} response := new(MimiCodeResponse) - if err := a.client.HttpGet(GetMiniCode, req, &client.HttpResponse{Result: response}); err != nil { + if err := a.client.HttpGet(GetMiniCodeUrl, req, &client.HttpResponse{Result: response}); err != nil { return nil, err } return response, nil @@ -103,7 +103,7 @@ func (a *meituanUnionApiImpl) GetOrderBySinge(params GetOrderBySingeRequest) (*G req := &client.HttpRequest{Headers: a.client.Headers, QueryArgs: queryArgs} // todo:: 返回完整的原始数据 response := new(GetOrderBySingeResponse) - if err := a.client.HttpGet(GetOrderSinge, req, &client.HttpResponse{Result: response}); err != nil { + if err := a.client.HttpGet(GetOrderSingeUrl, req, &client.HttpResponse{Result: response}); err != nil { return nil, err } return response, nil @@ -124,7 +124,7 @@ func (a *meituanUnionApiImpl) GetOrderByBatch(params GetOrderByBatchRequest) (*G req := &client.HttpRequest{Headers: a.client.Headers, QueryArgs: queryArgs} // todo:: 返回完整的原始数据 response := new(GetOrderByBatchResponse) - if err := a.client.HttpGet(GetOrderBatch, req, &client.HttpResponse{Result: response}); err != nil { + if err := a.client.HttpGet(GetOrderBatchUrl, req, &client.HttpResponse{Result: response}); err != nil { a.log.Errorf("GetOrderByBatch error: %v", err) return nil, err } diff --git a/platform/meituan-union/api_test.go b/platform/meituan-union/api_test.go index 6ba1c64..de6ccec 100644 --- a/platform/meituan-union/api_test.go +++ b/platform/meituan-union/api_test.go @@ -21,7 +21,7 @@ func TestApiClient(t *testing.T) { func (a *apiClientSuite) SetupSuite() { log := logx.WithContext(context.Background()) apiClient := NewApiClient(log, AuthConfig{ - AppKey: "8b0a6d711cd573b5b048c90820dbb3fe756", + //AppKey: "8b0a6d711cd573b5b048c90820dbb3fe756", SignKey: "3e4a697ecd9eafa27c2f3f4ccf22072d", NotifyKey: "gb8cwkj53x", }) diff --git a/platform/meituan-union/consts.go b/platform/meituan-union/consts.go index 5132028..436f8fe 100644 --- a/platform/meituan-union/consts.go +++ b/platform/meituan-union/consts.go @@ -10,9 +10,9 @@ const ( // 接口地址 const ( - Domain = "https://openapi.meituan.com" // Domain api域名 - GetLinkUrl = Domain + "/api/generateLink" - GetMiniCode = Domain + "/api/miniCode" - GetOrderSinge = Domain + "/api/order" - GetOrderBatch = Domain + "/api/orderList" + ApiDomain = "https://openapi.meituan.com" // Domain api域名 + GetLinkUrl = ApiDomain + "/api/generateLink" + GetMiniCodeUrl = ApiDomain + "/api/miniCode" + GetOrderSingeUrl = ApiDomain + "/api/order" + GetOrderBatchUrl = ApiDomain + "/api/orderList" ) diff --git a/platform/t3-union/api.go b/platform/t3-union/api.go new file mode 100644 index 0000000..04b33ca --- /dev/null +++ b/platform/t3-union/api.go @@ -0,0 +1,155 @@ +package t3_union + +import ( + "context" + "encoding/json" + "fmt" + "math/rand" + "net/http" + "sort" + "strings" + "time" + + "github.com/zeromicro/go-zero/core/logx" + + "gitee.com/chengdu-lenntc/third-platform-sdk/client" + "gitee.com/chengdu-lenntc/third-platform-sdk/util" +) + +// T3UnionApi 调用第三方平台的api +// Api defines the interface of t3_union api +type T3UnionApi interface { + // Sign 签名 + Sign(methodType string, url string, data map[string]interface{}) string + // GenerateLink 生成短链 + GenerateLink(ctx context.Context, req GenerateLinkRequest) (*GenerateLinkResponse, error) + // GenerateCode 生成二维码 + GenerateCode(ctx context.Context, req GenerateCodeRequest) (*GenerateCodeResponse, error) + // GeneratePoster 生成推广海报 + GeneratePoster(ctx context.Context, req GeneratePosterRequest) (*GeneratePosterResponse, error) + // QueryOrderList 查询订单列表 + QueryOrderList(ctx context.Context, req QueryOrderListRequest) (*QueryOrderListResponse, error) +} + +type t3UnionApiImpl struct { + log logx.Logger + client *Client +} + +func newT3UnionApiImpl(log logx.Logger, client *Client) T3UnionApi { + return &t3UnionApiImpl{ + log: log, + client: client, + } +} + +func (a *t3UnionApiImpl) buildHeader(methodType string, url 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, url, 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, url 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, url)) + 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, url string, dataMap map[string]any) string { + headers := a.buildHeader(methodType, url, dataMap) + if sign, ok := headers[XT3Signature]; ok { + return sign + } + return "" +} + +// GenerateLink 生成短链 +func (a *t3UnionApiImpl) GenerateLink(ctx context.Context, req GenerateLinkRequest) (*GenerateLinkResponse, error) { + args := util.StructToMap(req) + headers := a.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 + } + return response, nil +} + +// GenerateCode 生成二维码 +func (a *t3UnionApiImpl) GenerateCode(ctx context.Context, req GenerateCodeRequest) (*GenerateCodeResponse, error) { + args := util.StructToMap(req) + headers := a.buildHeader(http.MethodPost, GetMiniCodeUri, args) + for k, v := range a.client.headers { + headers[k] = v + } + request := &client.HttpRequest{Headers: headers, BodyArgs: args} + response := new(GenerateCodeResponse) + if err := a.client.HttpPost(GetMiniCodeUrl, request, &client.HttpResponse{Result: response}); err != nil { + return nil, err + } + return response, nil +} + +// GeneratePoster 生成推广海报 +func (a *t3UnionApiImpl) GeneratePoster(ctx context.Context, req GeneratePosterRequest) (*GeneratePosterResponse, error) { + args := util.StructToMap(req) + headers := a.buildHeader(http.MethodPost, GetPosterUri, args) + for k, v := range a.client.headers { + headers[k] = v + } + request := &client.HttpRequest{Headers: headers, BodyArgs: args} + response := new(GeneratePosterResponse) + if err := a.client.HttpPost(GetPosterUrl, request, &client.HttpResponse{Result: response}); err != nil { + return nil, err + } + return response, nil +} + +// QueryOrderList 查询订单列表 +func (a *t3UnionApiImpl) QueryOrderList(ctx context.Context, req QueryOrderListRequest) (*QueryOrderListResponse, error) { + args := util.StructToMap(req) + headers := a.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 + } + return response, nil +} diff --git a/platform/t3-union/api_test.go b/platform/t3-union/api_test.go new file mode 100644 index 0000000..0b725b8 --- /dev/null +++ b/platform/t3-union/api_test.go @@ -0,0 +1,120 @@ +package t3_union + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/zeromicro/go-zero/core/logx" +) + +// api-单元测试 +type apiClientSuite struct { + suite.Suite + api T3UnionApi +} + +func TestApiClient(t *testing.T) { + suite.Run(t, new(apiClientSuite)) +} + +func (a *apiClientSuite) SetupSuite() { + log := logx.WithContext(context.Background()) + apiClient := NewApiClient(log, AuthConfig{ + AppKey: "QOHEgCUTeK", + AppSecret: "tsTSxrCgibcFbxGOxRDEBGQUhRVJLsFs", + }) + 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{ + ActivityUuid: "c2873670644049da964c603a9a4e2a87", + SourceId: "yixiao", + SpotUuid: "1d444dbb3d6b475890dc9173f29fb66c", + LinkType: "WX", + } + 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_GenerateCode() { + req := GenerateCodeRequest{ + ActivityUuid: "c2873670644049da964c603a9a4e2a87", + SourceId: "yixiao", + SpotUuid: "1d444dbb3d6b475890dc9173f29fb66c", + Dsi: "6bc21eea27e341e994e8c998218be230", + Type: "WX", + } + result, err := a.api.GenerateCode(context.Background(), req) + if !a.NoError(err) { + a.T().Errorf("========[Test_GenerateCode] response error:%s", err) + return + } + resultByte, err := json.Marshal(result) + if err != nil { + a.T().Errorf("========[Test_GenerateCode] json_marshal error:%s", err) + return + } + a.T().Logf("=====[Test_GenerateCode] result: %s", string(resultByte)) +} + +func (a *apiClientSuite) Test_GeneratePoster() { + req := GeneratePosterRequest{ + ActivityUuid: "c2873670644049da964c603a9a4e2a87", + SourceId: "yixiao", + SpotUuid: "1d444dbb3d6b475890dc9173f29fb66c", + Dsi: "6bc21eea27e341e994e8c998218be230", + } + result, err := a.api.GeneratePoster(context.Background(), req) + if !a.NoError(err) { + a.T().Errorf("========[Test_GeneratePoster] response error:%s", err) + return + } + resultByte, err := json.Marshal(result) + if err != nil { + a.T().Errorf("========[Test_GeneratePoster] json_marshal error:%s", err) + return + } + a.T().Logf("=====[Test_GeneratePoster] result: %s", string(resultByte)) +} + +func (a *apiClientSuite) Test_QueryOrderList() { + req := QueryOrderListRequest{ + PageNo: 1, + PageSize: 1, + SupplierUuid: "13550119392", + StartTime: 1715702400000, + EndTime: 1718380800000, + } + 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/t3-union/client.go b/platform/t3-union/client.go new file mode 100644 index 0000000..6b78216 --- /dev/null +++ b/platform/t3-union/client.go @@ -0,0 +1,37 @@ +package t3_union + +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 // 应用秘钥 +} + +// 连接第三方平台的client +type Client struct { + log logx.Logger + authConfig AuthConfig + client.HttpClient + headers map[string]string +} + +func NewApiClient(log logx.Logger, conf AuthConfig) T3UnionApi { + clt := newClient(log, conf) + return newT3UnionApiImpl(log, clt) +} + +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/t3-union/consts.go b/platform/t3-union/consts.go new file mode 100644 index 0000000..662fe95 --- /dev/null +++ b/platform/t3-union/consts.go @@ -0,0 +1,25 @@ +package t3_union + +// header头信息 +const ( + XT3Nonce = "x-t3-nonce" + XT3Timestamp = "x-t3-timestamp" + XT3Version = "x-t3-version" + XT3Key = "x-t3-key" + XT3SignatureMethod = "x-t3-signature-method" + XT3Signature = "x-t3-signature" + XT3SignatureHeaders = "x-t3-signature-headers" +) + +// 接口地址 +const ( + ApiDomain = "https://exchange.t3go.cn" // Domain api域名 + GetLinkUri = "/openapi/marketing/union/v1/generate/link" + GetMiniCodeUri = "/openapi/marketing/union/v1/generate/code" + GetPosterUri = "/openapi/marketing/union/v1/generate/poster" + GetOrderListUri = "/openapi/marketing/union/v1/order/list" + GetLinkUrl = ApiDomain + GetLinkUri + GetMiniCodeUrl = ApiDomain + GetMiniCodeUri + GetPosterUrl = ApiDomain + GetPosterUri + GetOrderListUrl = ApiDomain + GetOrderListUri +) diff --git a/platform/t3-union/types.go b/platform/t3-union/types.go new file mode 100644 index 0000000..6cb4452 --- /dev/null +++ b/platform/t3-union/types.go @@ -0,0 +1,110 @@ +package t3_union + +// CommonResponse 公共响应数据 +type CommonResponse struct { + Msg string `json:"msg"` // 接口msg,详细错误原因 + Code int64 `json:"code"` // 接口响应状态 + BizCode string `json:"bizCode"` // 业务code + Success bool `json:"success"` // 响应成功状态 + Exception string `json:"exception"` // 异常信息 + Attachment string `json:"attachment"` // 唯一请求ID + ErrCode int64 `json:"errCode"` // 错误码 +} + +// GenerateLinkRequest 生成短链接请求 +type GenerateLinkRequest struct { + ActivityUuid string `json:"activityUuid"` // 活动uuid + SourceId string `json:"sourceId"` // 业务自定义sourceId + SpotUuid string `json:"spotUuid"` // 推广位uuid + LinkType string `json:"linkType"` // linkType: 链接类型,可选类型: WX:微信小程序短链, ALIPAY:支付宝小程序短链, H5:H5链接 +} + +// GenerateLinkResponse 生成短链接响应 +type GenerateLinkResponse struct { + CommonResponse + Data *GenerateLinkData `json:"data"` // 响应数据 +} + +type GenerateLinkData struct { + AppId string `json:"appId"` // 小程序appid + AppSource string `json:"appSource"` // 小程序原始ID + Dsi string `json:"dsi"` // 实例ID,可通过此ID去生成海报或者二维码 + Link string `json:"link"` // 生成的链接 +} + +// GenerateCodeRequest 生成二维码请求 +type GenerateCodeRequest struct { + ActivityUuid string `json:"activityUuid"` // 活动uuid + SourceId string `json:"sourceId"` // 业务自定义sourceId + SpotUuid string `json:"spotUuid"` // 推广位uuid + Dsi string `json:"dsi"` // 活动ID+推广位ID决定一个DSI + Type string `json:"type"` // type: 二维码类型,可选类型: WX:微信小程序, ALIPAY:支付宝小程序, H5:H5 +} + +// GenerateCodeResponse 生成二维码响应 +type GenerateCodeResponse struct { + CommonResponse + Data *GenerateCodeData `json:"data"` // 响应数据 +} + +type GenerateCodeData struct { + CodeLink string `json:"codeLink"` // 二维码链接 +} + +// GeneratePosterRequest 生成海报请求 +type GeneratePosterRequest struct { + ActivityUuid string `json:"activityUuid"` // 活动uuid + SourceId string `json:"sourceId"` // 业务自定义sourceId + SpotUuid string `json:"spotUuid"` // 推广位uuid + Dsi string `json:"dsi"` // 活动ID+推广位ID决定一个DSI +} + +// GeneratePosterResponse 生成海报响应 +type GeneratePosterResponse struct { + CommonResponse + Data *GeneratePosterData `json:"data"` // 响应数据 +} + +type GeneratePosterData struct { + PosterLink string `json:"posterLink"` // 生成的海报图链接 +} + +// QueryOrderListRequest 订单列表请求 +type QueryOrderListRequest struct { + PageNo int `json:"pageNo"` // 分页参数,从1开始 + PageSize int `json:"pageSize"` // 分页参数,每页数据数量 + SupplierUuid string `json:"supplierUuid"` // 供应商id(个人中心/我的账号) + StartTime int64 `json:"startTime"` // 开始时间(时间戳,毫秒) + EndTime int64 `json:"endTime"` // 结束时间(时间戳,毫秒) +} + +// QueryOrderListResponse 订单列表响应 +type QueryOrderListResponse struct { + CommonResponse + Data *QueryOrderData `json:"data"` // 响应数据 +} + +type QueryOrderData struct { + Total int64 `json:"total"` // 总数 + Data []*OrderItem `json:"data"` // 订单列表数据List + Empty bool `json:"empty"` +} + +// OrderItem 订单详情 +type OrderItem struct { + Uuid string `json:"uuid"` // 联盟订单记录uuid + BusinessId string `json:"businessId"` // 订单行程id + Rate float64 `json:"rate"` // 佣金比例 + Amount float64 `json:"amount"` // 金额(单位:元) + CpaType string `json:"cpaType"` // 分佣类型 + AttributionalFirstOrder bool `json:"attributionalFirstOrder"` // 归因订单类型; true归因首单;false非归因首单 + OrderAmount float64 `json:"orderAmount"` // 订单金额(单位:元) + PayAmount float64 `json:"payAmount"` // 实际支付金额(单位:元) + SourceId string `json:"sourceId"` // 业务溯源id + ParticipationStatus int `json:"participationStatus"` // 参与状态(0未成功,1成功) + FailReason string `json:"failReason"` // 失败原因 + UnionActivityUuid string `json:"unionActivityUuid"` // 联盟推广信息uuid(dsi) + ActivityUuid string `json:"activityUuid"` // 活动id + SpotUuid string `json:"spotUuid"` // 推广位uuid + CreateTime int64 `json:"createTime"` // 业务记录时间(时间戳, 毫秒) +}