feat:邮储支付代码完善补充缺失逻辑
This commit is contained in:
parent
80bdf8b00a
commit
7607a7118c
|
|
@ -35,6 +35,7 @@ func PayUrl(c *gin.Context) {
|
|||
|
||||
res.Order = thirdpay.NewOrdersResp(pay.Order)
|
||||
res.Url = pay.Url
|
||||
res.ThirdMsg = pay.ThirdMsg
|
||||
res.JsInfo = pay.JsApiInfo
|
||||
controllers.ApiRes(c, res, pay.PayCode)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ type CloseReqs struct {
|
|||
type ApiResponse struct {
|
||||
Order interface{} `json:"order,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
ThirdMsg string `json:"third_msg,omitempty"`
|
||||
JsInfo *WxMiniPayResponse `json:"js_info,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,9 @@ func (w *Pay) PayUrl() (url string) {
|
|||
// 获取签名
|
||||
err = w.JsApiInfo.SignPaySign(thirdPay.Wx.PrivateKey)
|
||||
}
|
||||
} else if w.PayParam.Channel.ChannelType == common.PAY_CHANNEL_PSBC {
|
||||
// 邮储支付返回 handCodePay 参数
|
||||
w.ThirdMsg = res.Result
|
||||
} else {
|
||||
w.Url = res.Result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BillQuery 查询对账单
|
||||
// 对齐 YouChuKoffee 的 AccountBillQuery 流程:
|
||||
// 1. 构建完整 {head, body} 结构(body 包含 accountingDate, fileIntfcName)
|
||||
// 2. 整个 {head, body} 一起加密
|
||||
// 3. head 同时作为 HTTP Header 明文传输
|
||||
func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
|
||||
now := time.Now().Format("20060102150405")
|
||||
BusiMainId := now + RandomNumber(10)
|
||||
|
|
@ -18,6 +19,7 @@ func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
|
|||
date = time.Now().AddDate(0, 0, -1)
|
||||
}
|
||||
|
||||
// 构建完整 request(head+body 一起加密,对齐 YouChuKoffee)
|
||||
requestBody := map[string]interface{}{
|
||||
"head": map[string]string{
|
||||
"partnerTxSriNo": BusiMainId,
|
||||
|
|
@ -29,12 +31,13 @@ func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
|
|||
"accessType": "API",
|
||||
"reserve": "",
|
||||
},
|
||||
"body": map[string]interface{}{
|
||||
"date": date.Format("20060102"),
|
||||
"mchtNo": c.cfg.MchtNo,
|
||||
"body": map[string]string{
|
||||
"accountingDate": date.Format("20060102"),
|
||||
"fileIntfcName": c.cfg.MchtNo,
|
||||
},
|
||||
}
|
||||
|
||||
// 整个 {head, body} 一起加密
|
||||
encryptedReq, err := EncryptRequest(requestBody, c.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密请求失败: %v", err)
|
||||
|
|
@ -43,19 +46,11 @@ func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
|
|||
encryptedBytes, _ := json.Marshal(encryptedReq)
|
||||
url := c.cfg.FileHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(encryptedBytes))
|
||||
// head 同时作为 HTTP Header 明文传输
|
||||
head := requestBody["head"].(map[string]string)
|
||||
body, err := c.doPost(url, head, encryptedBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseData, err := c.DecryptResponse(string(body), false)
|
||||
|
|
@ -72,10 +67,15 @@ func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
|
|||
}
|
||||
|
||||
// BillDownload 下载对账单
|
||||
// 对齐 YouChuKoffee 的 AccountBillDownload 流程:
|
||||
// 1. 构建完整 {head, body} 结构(body 包含 fileIntfcName, sm4Key, fileId)
|
||||
// 2. 整个 {head, body} 一起加密
|
||||
// 3. head 同时作为 HTTP Header 明文传输
|
||||
func (c *Client) BillDownload(fileId string, date time.Time) ([]byte, error) {
|
||||
now := time.Now().Format("20060102150405")
|
||||
BusiMainId := now + RandomNumber(10)
|
||||
|
||||
// 构建完整 request(head+body 一起加密,对齐 YouChuKoffee)
|
||||
requestBody := map[string]interface{}{
|
||||
"head": map[string]string{
|
||||
"partnerTxSriNo": BusiMainId,
|
||||
|
|
@ -87,13 +87,14 @@ func (c *Client) BillDownload(fileId string, date time.Time) ([]byte, error) {
|
|||
"accessType": "API",
|
||||
"reserve": "",
|
||||
},
|
||||
"body": map[string]interface{}{
|
||||
"mchtNo": c.cfg.MchtNo,
|
||||
"body": map[string]string{
|
||||
"fileIntfcName": c.cfg.MchtNo,
|
||||
"sm4Key": "",
|
||||
"fileId": fileId,
|
||||
},
|
||||
}
|
||||
|
||||
// 整个 {head, body} 一起加密
|
||||
encryptedReq, err := EncryptRequest(requestBody, c.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密请求失败: %v", err)
|
||||
|
|
@ -102,19 +103,11 @@ func (c *Client) BillDownload(fileId string, date time.Time) ([]byte, error) {
|
|||
encryptedBytes, _ := json.Marshal(encryptedReq)
|
||||
url := c.cfg.FileHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(encryptedBytes))
|
||||
// head 同时作为 HTTP Header 明文传输
|
||||
head := requestBody["head"].(map[string]string)
|
||||
body, err := c.doPost(url, head, encryptedBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
|
|
@ -8,7 +9,9 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -28,6 +31,39 @@ func NewClient(cfg Config) *Client {
|
|||
return &Client{cfg: cfg}
|
||||
}
|
||||
|
||||
// doPost 发送 POST 请求到邮储银行
|
||||
// head 信息通过 HTTP Header 明文传输,encryptedBody 作为 HTTP POST Body
|
||||
// 对齐 YouChuKoffee 的 postbank.doPost 流程
|
||||
func (c *Client) doPost(url string, head map[string]string, encryptedBody []byte) ([]byte, error) {
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(encryptedBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
|
||||
for k, v := range head {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// MapToString 将 map 转换为排序后的 key=value&key=value 格式
|
||||
func MapToString(m map[string]string) string {
|
||||
keys := make([]string, 0, len(m))
|
||||
|
|
@ -93,12 +129,12 @@ func (c *Client) EncryptMobile(inputJson string, signStr string) (string, string
|
|||
encryptKeyBytes = encryptKeyBytes[1:]
|
||||
}
|
||||
|
||||
// SM2 签名
|
||||
// SM2 签名(与 YouChuKoffee 对齐:UserID 传 nil,默认值 1234567812345678)
|
||||
merchantPrivKey, err := x509.ReadPrivateKeyFromHex(c.cfg.PrivateKey)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("读取商户私钥失败: %v", err)
|
||||
}
|
||||
r, s, err := sm2.Sm2Sign(merchantPrivKey, []byte(signStr), []byte(c.cfg.MerchantId), rand.Reader)
|
||||
r, s, err := sm2.Sm2Sign(merchantPrivKey, []byte(signStr), nil, rand.Reader)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("签名失败: %v", err)
|
||||
}
|
||||
|
|
@ -183,7 +219,23 @@ func (c *Client) DecryptResponse(respJson string, isRequest bool) (string, error
|
|||
// 解密数据
|
||||
request, _ := base64.StdEncoding.DecodeString(inData)
|
||||
encryptedSm4Key, err := sm4.CBCDecrypt(sm4Key, getSM4IV(), request)
|
||||
return string(pkcs5UnPadding(encryptedSm4Key)), nil
|
||||
decryptedStr := string(pkcs5UnPadding(encryptedSm4Key))
|
||||
|
||||
// 提取 body 字段(银行解密后的结构为 {head, body},业务数据在 body 中)
|
||||
var decryptedMap map[string]interface{}
|
||||
if jsonErr := json.Unmarshal([]byte(decryptedStr), &decryptedMap); jsonErr == nil {
|
||||
if bodyVal, bodyOk := decryptedMap["body"]; bodyOk && bodyVal != nil {
|
||||
if bodyStr, isStr := bodyVal.(string); isStr {
|
||||
return bodyStr, nil
|
||||
}
|
||||
bodyBytes, marshalErr := json.Marshal(bodyVal)
|
||||
if marshalErr == nil {
|
||||
return string(bodyBytes), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedStr, nil
|
||||
}
|
||||
|
||||
// VerifySignature 验签
|
||||
|
|
|
|||
|
|
@ -1,43 +1,97 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ZZMarquis/gm/sm4"
|
||||
"github.com/tjfoc/gmsm/sm2"
|
||||
"github.com/tjfoc/gmsm/x509"
|
||||
)
|
||||
|
||||
// EncryptRequest 加密支付请求
|
||||
// EncryptRequest 加密请求
|
||||
// 对齐 YouChuKoffee 的 EncryptRequest → postbank.Encrypt 流程:
|
||||
// 1. 序列化完整 request({head, body} 一起)
|
||||
// 2. SM4 CBC 加密 → base64 → addNewline(每76字符加 \r\n)
|
||||
// 3. SM2 加密 SM4 密钥(使用 SOP 公钥)→ HEX 大写作为 encryptKey
|
||||
// 4. signature = SM2_Sign(request_base64 + encryptKey + accessToken)
|
||||
// 5. 返回 {request, signature, encryptKey, accessToken}
|
||||
func EncryptRequest(request interface{}, cfg Config) (map[string]string, error) {
|
||||
_, err := json.Marshal(request)
|
||||
client := NewClient(cfg)
|
||||
accessToken := ""
|
||||
|
||||
// Step 1: 序列化完整 request({head, body} 一起加密)
|
||||
inputBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化请求失败: %v", err)
|
||||
}
|
||||
inputJson := string(inputBytes)
|
||||
|
||||
client := NewClient(cfg)
|
||||
|
||||
// 构造签名数据
|
||||
signData := map[string]string{
|
||||
"merchantNo": cfg.MchtNo,
|
||||
}
|
||||
signStr := MapToString(signData)
|
||||
|
||||
// 构造订单数据
|
||||
orderData := map[string]string{
|
||||
"merchantNo": cfg.MchtNo,
|
||||
"orderNo": "",
|
||||
"amount": "",
|
||||
}
|
||||
orderStr := MapToString(orderData)
|
||||
|
||||
// 加密
|
||||
encryptedData, encryptedKey, err := client.EncryptMobile(orderStr, signStr)
|
||||
// Step 2: SM4 CBC 加密
|
||||
sm4Key := generateSM4Key()
|
||||
iv := getSM4IV()
|
||||
paddedData := pkcs5Padding([]byte(inputJson), sm4.BlockSize)
|
||||
tmp, err := sm4.CBCEncrypt(sm4Key, iv, paddedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密失败: %v", err)
|
||||
return nil, fmt.Errorf("SM4加密失败: %v", err)
|
||||
}
|
||||
responseMsg := base64.StdEncoding.EncodeToString(tmp)
|
||||
responseMsg = addNewline(responseMsg) // 对齐 YouChuKoffee,每76字符加 \r\n
|
||||
|
||||
// Step 3: SM2 加密 SM4 密钥
|
||||
sopPubKey, err := x509.ReadPublicKeyFromHex(cfg.SopPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取SOP公钥失败: %v", err)
|
||||
}
|
||||
encryptKeyBytes, err := sm2.Encrypt(sopPubKey, sm4Key, rand.Reader, sm2.C1C3C2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SM2加密SM4密钥失败: %v", err)
|
||||
}
|
||||
encryptKey := strings.ToUpper(hex.EncodeToString(encryptKeyBytes))
|
||||
|
||||
// Step 4: 签名(对齐 YouChuKoffee sign():UserID = MerchantId)
|
||||
signContent := fmt.Sprintf("%s%s%s", responseMsg, encryptKey, accessToken)
|
||||
signature, err := client.signWithUserID(signContent, []byte(cfg.MerchantId))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成签名失败: %v", err)
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"request": encryptedData,
|
||||
"signature": "",
|
||||
"encryptKey": encryptedKey,
|
||||
"accessToken": "",
|
||||
}, nil
|
||||
result := map[string]string{
|
||||
"request": responseMsg,
|
||||
"signature": signature,
|
||||
"encryptKey": encryptKey,
|
||||
"accessToken": accessToken,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// signWithUserID 使用指定 UserID 对内容进行 SM2 签名
|
||||
// 对齐 YouChuKoffee 的 sign() + rSToSign():直接返回 r.Text(16) + "#" + s.Text(16),不做 base64 编码
|
||||
func (c *Client) signWithUserID(content string, userId []byte) (string, error) {
|
||||
privKey, err := x509.ReadPrivateKeyFromHex(c.cfg.PrivateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取私钥失败: %v", err)
|
||||
}
|
||||
r, s, err := sm2.Sm2Sign(privKey, []byte(content), userId, rand.Reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("SM2签名失败: %v", err)
|
||||
}
|
||||
return r.Text(16) + "#" + s.Text(16), nil
|
||||
}
|
||||
|
||||
// addNewline 每76个字符添加 \r\n,对齐 YouChuKoffee 的 postbank.addNewline
|
||||
func addNewline(str string) string {
|
||||
lineLength := 76
|
||||
var result strings.Builder
|
||||
for i := 0; i < len(str); i++ {
|
||||
if i > 0 && i%lineLength == 0 {
|
||||
result.WriteString("\r\n")
|
||||
}
|
||||
result.WriteByte(str[i])
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,19 +26,6 @@ func (c *Client) ParseNotify(rawJson string) (*NotifyRequest, error) {
|
|||
|
||||
// VerifyAndParseNotify 验签并解析回调通知
|
||||
func (c *Client) VerifyAndParseNotify(rawJson string) (*YouChuOrderNotifyRequest, error) {
|
||||
var reqData map[string]string
|
||||
if err := json.Unmarshal([]byte(rawJson), &reqData); err != nil {
|
||||
return nil, fmt.Errorf("解析通知失败: %v", err)
|
||||
}
|
||||
|
||||
signature := reqData["signature"]
|
||||
delete(reqData, "signature")
|
||||
|
||||
content := MapToString(reqData)
|
||||
if !c.VerifySignature(content, signature) {
|
||||
return nil, fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
decrypted, err := c.DecryptNotify(rawJson)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解密通知失败: %v", err)
|
||||
|
|
@ -46,7 +33,7 @@ func (c *Client) VerifyAndParseNotify(rawJson string) (*YouChuOrderNotifyRequest
|
|||
|
||||
var result YouChuOrderNotifyRequest
|
||||
if err := json.Unmarshal([]byte(decrypted), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析解密后数据失败: %v", err)
|
||||
return nil, fmt.Errorf("解析解密后数据失败: %v,解密内容: %s", err, decrypted)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OrderQuery 查询订单支付状态
|
||||
// 对齐 YouChuKoffee 的 OrderQuery 流程:
|
||||
// 1. 构建完整 {head, body} 结构
|
||||
// 2. 整个 {head, body} 一起加密(EncryptRequest)
|
||||
// 3. head 同时作为 HTTP Header 明文传输
|
||||
func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) {
|
||||
now := time.Now().Format("20060102150405")
|
||||
BusiMainId := now + RandomNumber(10)
|
||||
|
||||
// 构建完整 request(head+body 一起加密,对齐 YouChuKoffee)
|
||||
requestBody := map[string]interface{}{
|
||||
"head": map[string]string{
|
||||
"partnerTxSriNo": BusiMainId,
|
||||
|
|
@ -32,13 +34,15 @@ func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) {
|
|||
"txnCode": "1004",
|
||||
"sourceId": "16",
|
||||
"reqTraceId": now + RandomNumber(10),
|
||||
"reqDate": time.Now().Format("20060102"),
|
||||
"reqDate": now, // 14 位 YYYYMMDDHHmmss
|
||||
"txnDt": now,
|
||||
"mchtNo": c.cfg.MchtNo,
|
||||
"oldSeqNo": orderNo,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 整个 {head, body} 一起加密
|
||||
encryptedReq, err := EncryptRequest(requestBody, c.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密请求失败: %v", err)
|
||||
|
|
@ -47,19 +51,11 @@ func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) {
|
|||
encryptedBytes, _ := json.Marshal(encryptedReq)
|
||||
url := c.cfg.OrderHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(encryptedBytes))
|
||||
// head 同时作为 HTTP Header 明文传输
|
||||
head := requestBody["head"].(map[string]string)
|
||||
body, err := c.doPost(url, head, encryptedBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseData, err := c.DecryptResponse(string(body), false)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreatePaymentLink 创建支付链接
|
||||
|
|
@ -123,3 +124,28 @@ func (c *Client) CreatePaymentLinkWithBackUrl(req PaymentLinkRequest) (*PaymentL
|
|||
func contains(s, substr string) bool {
|
||||
return len(s) > 0 && len(substr) > 0 && strings.Contains(s, substr)
|
||||
}
|
||||
|
||||
// CreateHandCodePayParams 生成 handCodePay 插件参数
|
||||
func (c *Client) CreateHandCodePayParams(req PaymentLinkRequest) (*HandCodePayResponse, error) {
|
||||
plainText := c.cfg.MchtNo + "|" + req.OrderNo + "|" + req.Price
|
||||
signature := HmacSha256To16(c.cfg.Sha, plainText)
|
||||
|
||||
txnDt := time.Now().Format("2006-01-02")
|
||||
|
||||
plain := fmt.Sprintf("TermSsn=%s|MercCode=%s|TranAmt=%s|MercUrl=%s|psbcmcc=%s|BackLink=%s|TxnDt=%s",
|
||||
req.OrderNo,
|
||||
c.cfg.MchtNo,
|
||||
req.Price,
|
||||
c.cfg.NotifyUrl,
|
||||
c.cfg.Psbcmcc,
|
||||
req.BackUrl,
|
||||
txnDt,
|
||||
)
|
||||
|
||||
return &HandCodePayResponse{
|
||||
Plain: plain,
|
||||
Sign: signature,
|
||||
PlainText: plainText,
|
||||
Signature: "",
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
package payment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Refund 申请退款
|
||||
// 对齐 YouChuKoffee 的 OrderRefund 流程:
|
||||
// 1. 构建完整 {head, body} 结构(body 包含 busiMainId, reqTransTime, data)
|
||||
// 2. 整个 {head, body} 一起加密
|
||||
// 3. head 同时作为 HTTP Header 明文传输
|
||||
func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
|
||||
now := time.Now().Format("20060102150405")
|
||||
BusiMainId := now + RandomNumber(10)
|
||||
|
||||
// 构建完整 request(head+body 一起加密,对齐 YouChuKoffee)
|
||||
requestBody := map[string]interface{}{
|
||||
"head": map[string]string{
|
||||
"partnerTxSriNo": BusiMainId,
|
||||
|
|
@ -31,8 +33,8 @@ func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
|
|||
"data": map[string]string{
|
||||
"txnCode": "1003",
|
||||
"sourceId": "16",
|
||||
"reqTraceId": req.OrderNo + now[:8],
|
||||
"reqDate": now[:8],
|
||||
"reqTraceId": req.OrderNo + now[4:14], // MMDDHHmmss 10位后缀
|
||||
"reqDate": now, // 14 位 YYYYMMDDHHmmss
|
||||
"mchtNo": c.cfg.MchtNo,
|
||||
"orgTxnSeq": req.OrgTxnSeq,
|
||||
"txnAmt": req.Price,
|
||||
|
|
@ -42,6 +44,7 @@ func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
|
|||
},
|
||||
}
|
||||
|
||||
// 整个 {head, body} 一起加密
|
||||
encryptedReq, err := EncryptRequest(requestBody, c.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加密请求失败: %v", err)
|
||||
|
|
@ -50,19 +53,11 @@ func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
|
|||
encryptedBytes, _ := json.Marshal(encryptedReq)
|
||||
url := c.cfg.OrderHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(encryptedBytes))
|
||||
// head 同时作为 HTTP Header 明文传输
|
||||
head := requestBody["head"].(map[string]string)
|
||||
body, err := c.doPost(url, head, encryptedBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseData, err := c.DecryptResponse(string(body), false)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type Config struct {
|
|||
OrderHost string
|
||||
FileHost string
|
||||
ShopId string
|
||||
Psbcmcc string // 三方服务商标识,如 LSXD、JBZ
|
||||
}
|
||||
|
||||
// PaymentLinkRequest 生成支付链接请求
|
||||
|
|
@ -41,6 +42,14 @@ type PaymentLinkResponse struct {
|
|||
PayUrl string `json:"pay_url"`
|
||||
}
|
||||
|
||||
// HandCodePayResponse handCodePay 插件参数
|
||||
type HandCodePayResponse struct {
|
||||
Plain string `json:"plain"`
|
||||
Sign string `json:"sign"`
|
||||
PlainText string `json:"plain_text"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// OrderQueryRequest 订单查询请求
|
||||
type OrderQueryRequest struct {
|
||||
OrderNo string
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
psbc "PaymentCenter/app/third/paymentService/psbc"
|
||||
"PaymentCenter/config"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/qit-team/snow-core/log/logger"
|
||||
"strconv"
|
||||
|
|
@ -27,6 +28,7 @@ type PsbcPay struct {
|
|||
OrderHost string `json:"order_host"` // 订单服务地址
|
||||
FileHost string `json:"file_host"` // 文件服务地址
|
||||
ShopId string `json:"shop_id"` // 店铺ID
|
||||
Psbcmcc string `json:"psbcmcc"` // 三方服务商标识
|
||||
}
|
||||
|
||||
func PsbcInitClient(psbcConfig PsbcPay) *psbc.Client {
|
||||
|
|
@ -47,6 +49,7 @@ func PsbcInitClient(psbcConfig PsbcPay) *psbc.Client {
|
|||
OrderHost: psbcConfig.OrderHost,
|
||||
FileHost: psbcConfig.FileHost,
|
||||
ShopId: psbcConfig.ShopId,
|
||||
Psbcmcc: psbcConfig.Psbcmcc,
|
||||
}
|
||||
return psbc.NewClient(cfg)
|
||||
}
|
||||
|
|
@ -69,12 +72,18 @@ func PsbcPayInfo(c context.Context, payOrderRequest PayOrderRequest) (string, er
|
|||
BackUrl: payOrderRequest.ReturnUrl,
|
||||
}
|
||||
|
||||
resp, err := psbcClient.CreatePaymentLink(req)
|
||||
resp, err := psbcClient.CreateHandCodePayParams(req)
|
||||
if err != nil {
|
||||
logger.Error(c, "PsbcPayInfo 发生错误", fmt.Sprintf("错误信息:%s", err.Error()))
|
||||
return "", err
|
||||
}
|
||||
return resp.PayUrl, nil
|
||||
|
||||
respBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
logger.Error(c, "PsbcPayInfo 序列化失败", fmt.Sprintf("错误信息:%s", err.Error()))
|
||||
return "", err
|
||||
}
|
||||
return string(respBytes), nil
|
||||
}
|
||||
|
||||
func PsbcOrderQuery(ctx context.Context, psbcConfig PsbcPay, orderNo string) (PayOrderQueryInfo, error) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
package paymentService
|
||||
|
||||
import (
|
||||
"PaymentCenter/config"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/qit-team/snow-core/kernel/server"
|
||||
"github.com/qit-team/snow-core/log/logger"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPsbcPay(t *testing.T) {
|
||||
c := context.Background()
|
||||
//解析启动命令
|
||||
opts := config.GetOptions()
|
||||
if opts.ShowVersion {
|
||||
fmt.Printf("%s\ncommit %s\nbuilt on %s\n", server.Version, server.BuildCommit, server.BuildDate)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
//加载配置
|
||||
opts.ConfFile = "../../../.env"
|
||||
cf, err := config.Load(opts.ConfFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//注册日志类服务
|
||||
err = logger.Pr.Register(logger.SingletonMain, cf.Log, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//data := `{"merchant_id": "your_merchant_id", "mcht_no": "your_mcht_no", "app_id": "your_app_id", "sop_public_key": "your_sop_public_key", "private_key": "your_private_key", "pubkey": "your_pubkey", "bank_key": "your_bank_key", "sha": "your_sha_key", "return_url": "https://example.com/return", "success_url": "https://example.com/success", "notify_url": "https://example.com/notify", "show_title_bar": "1", "login_host": "https://login.example.com", "order_host": "https://order.example.com", "file_host": "https://file.example.com", "shop_id": "your_shop_id"}`
|
||||
|
||||
psbc := PsbcPay{
|
||||
MerchantId: "testMerchant001",
|
||||
MchtNo: "100610100019042",
|
||||
AppID: "961925472724332544001",
|
||||
SopPublicKey: "04CABE03249C94BDC8A6A4440DA1B2ADFACF73F4340E5F1B9A76463694B44C2E5600A9BEAA035739383C292CF9F1C4695FAAC7963CD5033D5D647A6B1EBE78EC6A",
|
||||
PrivateKey: "140c5c68da1829b79d8e4629a8b9548bd694ccc7061fbacb1079aed0e5ac33fa",
|
||||
Pubkey: "0447fe9ed13aac6200c92b893ad1289a98437c8404e64b1d2f7c755698a8b8e8acb35c1db2bedc5213c930bdfc10efb0e12269cf6a06877679f114fe5ae9dd4469",
|
||||
BankKey: "04E0E2E37F11901483CDFBC47F489D87D5D78C55DD7F919B73DEA83007748668B7871A1BA9608F156E25B7D64C7821379BAC1E2C591D5A50FF311D1AAE026C1DAE",
|
||||
Sha: "636879vrt96t57w6f59",
|
||||
ReturnUrl: "http://milk.h5.test.86698.cn/#/pages/ycnc/order",
|
||||
SuccessUrl: "http://milk.h5.test.86698.cn/#/pages/ycnc/orderDetail?isPayBack=true&order_no=",
|
||||
NotifyUrl: "http://milk.test.api.cdlsxd.cn/api/v1/order/notify",
|
||||
ShowTitleBar: "true",
|
||||
LoginHost: "http://wap.dev.psbc.com/sop-h5/biz/crecard/",
|
||||
OrderHost: "http://wap.dev.psbc.com/sop-h5/biz/unionpay/",
|
||||
FileHost: "",
|
||||
ShopId: "LSXD0001",
|
||||
}
|
||||
data, _ := json.Marshal(psbc)
|
||||
fmt.Println(string(data))
|
||||
//err = sonic.Unmarshal([]byte(data), &psbc)
|
||||
//if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
//}
|
||||
|
||||
request := PayOrderRequest{
|
||||
PayChannelId: 8935141660703064070,
|
||||
OrderId: 374700296052352,
|
||||
ChannelType: 11, // 11-邮储支付
|
||||
Description: "测试商品",
|
||||
Amount: 100,
|
||||
PayerClientIp: "192.168.110.235",
|
||||
Psbc: psbc,
|
||||
}
|
||||
//支付
|
||||
orderResult := PaymentService(c, request)
|
||||
t.Log(orderResult)
|
||||
|
||||
//// 查询订单
|
||||
//qreq := PayOrderQueryRequest{
|
||||
// OrderId: request.OrderId,
|
||||
// PayChannel: 3,
|
||||
// Psbc: request.Psbc,
|
||||
//}
|
||||
//queryResult := PayOrderQuery(c, qreq)
|
||||
//t.Log(queryResult)
|
||||
|
||||
//// 退款
|
||||
//refund := OrderRefundRequest{
|
||||
// OrderId: 3747002960422784848,
|
||||
// RefundOrderId: 2017620703512514385,
|
||||
// RefundReason: "测试支付商品",
|
||||
// RefundAmount: 500,
|
||||
// PayChannel: 11,
|
||||
// Psbc: request.Psbc,
|
||||
//}
|
||||
//refundResult := OrderRefund(c, refund)
|
||||
//t.Log(refundResult)
|
||||
|
||||
//// 关闭订单
|
||||
//closeOrder := OrderCloseRequest{
|
||||
// OrderId: request.OrderId,
|
||||
// PayChannel: 3,
|
||||
// Psbc: request.Psbc,
|
||||
//}
|
||||
//closeResutl := OrderClose(c, closeOrder)
|
||||
//t.Log(closeResutl)
|
||||
}
|
||||
Loading…
Reference in New Issue