forked from go-pay/gopay
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
711 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package douyin | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/go-pay/gopay" | ||
) | ||
|
||
// https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/interface-request-credential/non-user-authorization/get-access-token | ||
func (u *Client) GetAccessToken(ctx context.Context) (resp *AccessTokenResp, err error) { | ||
bm := make(gopay.BodyMap) | ||
bm.Set("appid", u.AppId). | ||
Set("secret", u.ApiKey). | ||
Set("grant_type", "client_credential") | ||
|
||
resp = new(AccessTokenResp) | ||
if _, err = u.hc.Req().Post(u.BaseURL+"/api/apps/v2/token").SendBodyMap(bm).EndStruct(ctx, resp); err != nil { | ||
return nil, err | ||
} | ||
|
||
return resp, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package douyin | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
|
||
"github.com/go-pay/gopay" | ||
) | ||
|
||
// https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/content-security/picture-detect-v2 | ||
// 图片检测V2 | ||
func (u *Client) CensorImage(ctx context.Context, bm gopay.BodyMap) (resp *CensorImgResponse, err error) { | ||
var bs []byte | ||
if bs, err = u.doPost(ctx, "/api/apps/censor/image", bm); err != nil { | ||
return nil, err | ||
} | ||
resp = new(CensorImgResponse) | ||
if err = json.Unmarshal(bs, resp); err != nil { | ||
fmt.Printf("err:%s\n\n\n", err) | ||
return nil, fmt.Errorf("[%w], bytes: %s", gopay.UnmarshalErr, string(bs)) | ||
} | ||
return | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package douyin | ||
|
||
type CensorImgResponse struct { | ||
ErrCode int `json:"error,omitempty"` | ||
ErrMsg string `json:"message,omitempty"` | ||
Predicts []Predict `json:"predicts,omitempty"` | ||
ModelName string `json:"model_name,omitempty"` | ||
Hit bool `json:"hit,omitempty"` | ||
} | ||
|
||
type Predict struct { | ||
ModelName string `json:"model_name,omitempty"` | ||
Hit bool `json:"hit,omitempty"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package douyin | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/go-pay/gopay" | ||
) | ||
|
||
func TestClient_CensorImage(t *testing.T) { | ||
ast, err := client.GetAccessToken(ctx) | ||
if err != nil { | ||
t.Log(err) | ||
} | ||
bm := make(gopay.BodyMap) | ||
bm.Set("access_token", ast.Data.AccessToken) | ||
bm.Set("image", "http://xxx.jpg") | ||
resp, err := client.CensorImage(ctx, bm) | ||
if err != nil { | ||
t.Logf("fail:%v", err) | ||
return | ||
} | ||
t.Logf("resp:%+v, \nerr:%s", resp, err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
package douyin | ||
|
||
import ( | ||
"context" | ||
"crypto" | ||
"crypto/hmac" | ||
"crypto/md5" | ||
"crypto/rsa" | ||
"crypto/sha1" | ||
"crypto/sha256" | ||
"crypto/x509" | ||
"encoding/base64" | ||
"encoding/pem" | ||
"fmt" | ||
"hash" | ||
"net/http" | ||
"sort" | ||
"strings" | ||
"sync" | ||
|
||
"github.com/go-pay/gopay" | ||
"github.com/go-pay/xhttp" | ||
"github.com/go-pay/xlog" | ||
) | ||
|
||
// Client douyin | ||
type Client struct { | ||
AppId string | ||
MchId string | ||
ApiKey string | ||
BaseURL string | ||
NotifyUrl string | ||
Salt string // 盐值 | ||
Token string // 支付设置 token | ||
IsProd bool // 是否生产环境 | ||
ctx context.Context // 上下文 | ||
DebugSwitch gopay.DebugSwitch // 调试开关,是否打印日志 | ||
mu sync.RWMutex | ||
sha256Hash hash.Hash | ||
md5Hash hash.Hash | ||
hc *xhttp.Client | ||
tlsHc *xhttp.Client | ||
} | ||
|
||
// NewClient 初始化抖音支付客户端 | ||
// appId:应用ID | ||
// mchId:商户ID | ||
// ApiKey:API秘钥值 | ||
// IsProd:是否是正式环境 | ||
func NewClient(appId, mchId, apiKey string, isProd bool) (client *Client) { | ||
return &Client{ | ||
AppId: appId, | ||
MchId: mchId, | ||
ApiKey: apiKey, | ||
IsProd: isProd, | ||
ctx: context.Background(), | ||
DebugSwitch: gopay.DebugOff, | ||
sha256Hash: hmac.New(sha256.New, []byte(apiKey)), | ||
md5Hash: md5.New(), | ||
hc: xhttp.NewClient(), | ||
tlsHc: xhttp.NewClient(), | ||
} | ||
} | ||
|
||
// SetBodySize 设置http response body size(MB) | ||
func (c *Client) SetBodySize(sizeMB int) { | ||
if sizeMB > 0 { | ||
c.hc.SetBodySize(sizeMB) | ||
} | ||
} | ||
|
||
func (c *Client) SetToken(token string) { | ||
c.Token = token | ||
} | ||
|
||
// 担保支付回调签名算法 | ||
// 参数:"strArr" 所有字段(验证时注意不包含 sign 签名本身,不包含空字段与 type 常量字段)内容与平台上配置的 token | ||
func (c *Client) VerifySign(notifyReq *NotifyRequest) (err error) { | ||
strArr := []string{c.Token, notifyReq.Timestamp, notifyReq.Nonce, notifyReq.Msg} | ||
sort.Strings(strArr) | ||
h := sha1.New() | ||
h.Write([]byte(strings.Join(strArr, ""))) | ||
validSign := fmt.Sprintf("%x", h.Sum(nil)) | ||
|
||
if notifyReq.MsgSignature != validSign { | ||
return fmt.Errorf("签名验证失败") | ||
} | ||
return | ||
} | ||
|
||
// 获取签名字符串 | ||
func (c *Client) getRsaSign(bm gopay.BodyMap) (sign string, err error) { | ||
var paramsArr []string | ||
if c.Salt == "" { | ||
return "", fmt.Errorf("签名缺少必要的参数") | ||
} | ||
for k, v := range bm { | ||
if k == "other_settle_params" || k == "app_id" || k == "thirdparty_id" || k == "sign" { | ||
continue | ||
} | ||
value := strings.TrimSpace(fmt.Sprintf("%v", v)) | ||
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") && len(value) > 1 { | ||
value = value[1 : len(value)-1] | ||
} | ||
value = strings.TrimSpace(value) | ||
if value == "" || value == "null" { | ||
continue | ||
} | ||
paramsArr = append(paramsArr, value) | ||
} | ||
paramsArr = append(paramsArr, c.Salt) | ||
sort.Strings(paramsArr) | ||
c.mu.Lock() | ||
defer func() { | ||
c.md5Hash.Reset() | ||
c.mu.Unlock() | ||
}() | ||
c.md5Hash.Write([]byte(strings.Join(paramsArr, "&"))) | ||
|
||
return fmt.Sprintf("%x", c.md5Hash.Sum(nil)), nil | ||
} | ||
|
||
// POST 发起请求 | ||
func (c *Client) doPost(ctx context.Context, path string, bm gopay.BodyMap) (bs []byte, err error) { | ||
sign, err := c.getRsaSign(bm) | ||
if err != nil { | ||
return nil, fmt.Errorf("GetRsaSign Error: %w", err) | ||
} | ||
bm.Set("app_id", c.AppId) | ||
bm.Set("sign", sign) | ||
fmt.Println(sign) | ||
req := c.hc.Req() | ||
req.Header.Add("Accept", "application/json") | ||
if c.DebugSwitch == gopay.DebugOn { | ||
xlog.Debugf("DouYin_Req_Path: %s", path) | ||
xlog.Debugf("DouYin_Req_Body: %s", bm.JsonBody()) | ||
xlog.Debugf("DouYin_Req_Headers: %#v", req.Header) | ||
fmt.Println(bm.JsonBody()) | ||
} | ||
res, bs, err := req.Post(c.BaseURL + path).SendBodyMap(bm).EndBytes(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if res.StatusCode != http.StatusOK { | ||
return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode) | ||
} | ||
return bs, nil | ||
} | ||
|
||
// 验签示例 | ||
func DyCheckSign(timestamp, nonce, body, signature, pubKeyStr string) (bool, error) { | ||
pubKey, err := PemToRSAPublicKey(pubKeyStr) // 注意验签时publicKey使用平台公钥而非应用公钥 | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
hashed := sha256.Sum256([]byte(timestamp + "\n" + nonce + "\n" + body + "\n")) | ||
signBytes, err := base64.StdEncoding.DecodeString(signature) | ||
if err != nil { | ||
return false, err | ||
} | ||
err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], signBytes) | ||
return err == nil, nil | ||
} | ||
|
||
func PemToRSAPublicKey(pemKeyStr string) (*rsa.PublicKey, error) { | ||
block, _ := pem.Decode([]byte(pemKeyStr)) | ||
if block == nil || len(block.Bytes) == 0 { | ||
return nil, fmt.Errorf("empty block in pem string") | ||
} | ||
key, err := x509.ParsePKIXPublicKey(block.Bytes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
switch key := key.(type) { | ||
case *rsa.PublicKey: | ||
return key, nil | ||
default: | ||
return nil, fmt.Errorf("not rsa public key") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package douyin | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"testing" | ||
|
||
"github.com/go-pay/gopay" | ||
) | ||
|
||
var ( | ||
client *Client | ||
appId = "" | ||
mchId = "" | ||
apiKey = "" | ||
salt = "" | ||
baseUrl = "https://developer.toutiao.com" | ||
ctx = context.Background() | ||
token = "" | ||
) | ||
|
||
func TestMain(m *testing.M) { | ||
client = NewClient(appId, mchId, apiKey, false) | ||
client.DebugSwitch = gopay.DebugOn | ||
client.Salt = salt | ||
client.BaseURL = baseUrl | ||
client.SetToken(token) | ||
os.Exit(m.Run()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package douyin | ||
|
||
type NotifyRequest struct { | ||
Timestamp string `json:"timestamp,omitempty"` // UTC时间戳 | ||
Nonce string `json:"nonce,omitempty"` // 随机字符串 | ||
Msg string `json:"msg,omitempty"` | ||
MsgSignature string `json:"msg_signature,omitempty"` | ||
Type string `json:"type,omitempty"` | ||
} | ||
|
||
type NotifyResp struct { | ||
ErrNo int `json:"err_no"` | ||
ErrTips string `json:"err_tips"` | ||
} | ||
|
||
type NotifyMsgResp struct { | ||
AppId string `json:"appid,omitempty"` // 当前交易发起的小程序id | ||
CpOrderno string `json:"cp_orderno,omitempty"` // 开发者侧的订单号 | ||
CpExtra string `json:"cp_extra,omitempty"` // 预下单时开发者传入字段 | ||
Way string `json:"way,omitempty"` // 支付渠道 1-微信支付,2-支付宝支付,10-抖音支付 | ||
ChannelNo string `json:"channel_no,omitempty"` // 支付渠道侧单号 | ||
PaymentOrderNo string `json:"payment_order_no,omitempty"` // 支付渠道侧PC单号 | ||
TotalAmount int `json:"total_amount,omitempty"` // 支付金额,单位为分 | ||
Status string `json:"status,omitempty"` // 固定 SUCCESS | ||
ItemId string `json:"item_id,omitempty"` // 订单来源视频对应视频 id | ||
SellerUid string `json:"seller_uid,omitempty"` // 该笔交易卖家商户号 | ||
PaidAt int64 `json:"paid_at,omitempty"` // 支付时间,Unix 时间戳 | ||
OrderId string `json:"order_id,omitempty"` // 抖音侧订单号 | ||
} | ||
|
||
type PushOrderResponse struct { | ||
ErrCode int64 `json:"err_code,omitempty"` // 执行结果 | ||
ErrMsg string `json:"err_msg,omitempty"` // 返回错误信息 | ||
Body string `json:"data,omitempty"` // POI 等关联业务推送结果,非 POI 订单为空,JSON 字符串 | ||
} | ||
|
||
type AccessTokenReq struct { | ||
AppId string `json:"appid"` | ||
Secret string `json:"secret"` | ||
GrantType string `json:"grant_type"` | ||
} | ||
|
||
type AccessTokenResp struct { | ||
Data struct { | ||
AccessToken string `json:"access_token"` | ||
ExpiresIn int `json:"expires_in"` | ||
} `json:"data"` | ||
ErrCode int `json:"err_no"` | ||
ErrMsg string `json:"err_tips"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package douyin | ||
|
||
type CreateOrderData struct { | ||
OrderId string `json:"order_id,omitempty"` // 抖音侧的订单号 | ||
OrderToken string `json:"order_token,omitempty"` | ||
} | ||
|
||
type CreateOrderResponse struct { | ||
ErrNo int `json:"err_no,omitempty"` // 执行结果 | ||
ErrTips string `json:"err_tips,omitempty"` // 返回错误信息 | ||
Data *CreateOrderData `json:"data"` | ||
} | ||
|
||
type QueryOrderResponse struct { | ||
ErrNo int `json:"err_no,omitempty"` // 执行结果 | ||
ErrTips string `json:"err_tips,omitempty"` // 返回错误信息 | ||
OutOrderNo string `json:"out_order_no,omitempty"` // 开发者侧的订单号 | ||
OrderId string `json:"order_id,omitempty"` // 抖音侧的订单号 | ||
PaymentInfo *PaymentInfoData `json:"payment_info,omitempty"` // 支付信息 | ||
CpsInfo *CpsInfoData `json:"cps_info,omitempty"` // cps信息 | ||
} | ||
|
||
type PaymentInfoData struct { | ||
TotalFee int `json:"total_fee,omitempty"` // 支付金额 | ||
OrderStatus string `json:"order_status,omitempty"` // 支付状态枚举值 | ||
PayTime string `json:"pay_time,omitempty"` // 支付完成时间 | ||
Way int `json:"way,omitempty"` // 支付渠道 | ||
ChannelNo string `json:"channel_no,omitempty"` // 支付渠道侧的支付单号 | ||
SellerUid string `json:"seller_uid,omitempty"` // 该笔交易卖家商户号 | ||
ItemId string `json:"item_id,omitempty"` // 订单来源视频对应视频 id | ||
CpExtra string `json:"cp_extra,omitempty"` // 预下单时开发者传入字段 | ||
} | ||
|
||
type CpsInfoData struct { | ||
TotalFee string `json:"share_amount,omitempty"` // 达人分佣金额 | ||
DouyinId string `json:"douyin_id,omitempty"` // 达人抖音号 | ||
Nickname string `json:"nickname,omitempty"` // 达人昵称 | ||
} |
Oops, something went wrong.