feat:邮储支付fix

This commit is contained in:
wolter 2026-08-10 11:31:00 +08:00
parent 7f2aceb5a7
commit 2543a0fd4d
11 changed files with 244 additions and 200 deletions

View File

@ -46,7 +46,7 @@ func PayUrl(c *gin.Context) {
res.Order = thirdpay.NewOrdersResp(pay.Order)
res.Url = pay.Url
res.ThirdMsg = pay.ThirdMsg
res.PayInfo = pay.PayInfo
res.JsInfo = pay.JsApiInfo
controllers.ApiRes(c, res, pay.PayCode)
return

View File

@ -64,8 +64,8 @@ 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"`
PayInfo string `json:"pay_info,omitempty"` // 支付参数邮储app支付参数
}
type PayChannelListRequest struct {

View File

@ -44,6 +44,7 @@ type Pay struct {
Url string // 支付链接
ThirdMsg string // 第三方错误信息
JsApiInfo *front.WxMiniPayResponse // jsapi支付参数
PayInfo string // 支付参数邮储app支付参数
}
func NewPayWithPayCheck(paycheck *PayCheck) *Pay {
@ -217,7 +218,7 @@ func (w *Pay) PayUrl() (url string) {
}
} else if w.PayParam.Channel.ChannelType == common.PAY_CHANNEL_PSBC {
// 邮储支付返回 handCodePay 参数
w.ThirdMsg = res.Result
w.PayInfo = res.Result
} else {
w.Url = res.Result
}

View File

@ -46,9 +46,8 @@ func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
encryptedBytes, _ := json.Marshal(encryptedReq)
url := c.cfg.FileHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
// head 同时作为 HTTP Header 明文传输
head := requestBody["head"].(map[string]string)
body, err := c.doPost(url, head, encryptedBytes)
// head 同时作为 HTTP Header 明文传输(对齐 YouChuKoffee 的 doPost 方式)
body, err := c.doPost(url, BusiMainId, "ufile.query.commonQuery", encryptedBytes)
if err != nil {
return nil, err
}
@ -103,9 +102,8 @@ 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
// head 同时作为 HTTP Header 明文传输
head := requestBody["head"].(map[string]string)
body, err := c.doPost(url, head, encryptedBytes)
// head 同时作为 HTTP Header 明文传输(对齐 YouChuKoffee 的 doPost 方式)
body, err := c.doPost(url, BusiMainId, "ufile.download.commonDownload", encryptedBytes)
if err != nil {
return nil, err
}

View File

@ -8,6 +8,7 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math/big"
@ -16,9 +17,9 @@ import (
"strings"
"time"
"PaymentCenter/app/third/paymentService/psbc/internal/sm2"
psbcutil "PaymentCenter/app/third/paymentService/psbc/internal/util"
"github.com/ZZMarquis/gm/sm4"
"github.com/tjfoc/gmsm/sm2"
"github.com/tjfoc/gmsm/x509"
)
// Client 支付客户端
@ -33,17 +34,23 @@ func NewClient(cfg Config) *Client {
// 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) {
// 对齐 YouChuKoffee 的 postbank.doPost 流程youchu_api.go/config.go
func (c *Client) doPost(url string, partnerTxSriNo string, method string, encryptedBody []byte) ([]byte, error) {
req, err := http.NewRequest("POST", url, bytes.NewReader(encryptedBody))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
// 构建 HTTP Header对齐 YouChuKoffee 的 doPost 方式
req.Header.Set("partnerTxSriNo", partnerTxSriNo)
req.Header.Set("reqTime", time.Now().Format("20060102150405"))
req.Header.Set("method", method)
req.Header.Set("version", "1")
req.Header.Set("merchantId", c.cfg.MerchantId)
req.Header.Set("appID", c.cfg.AppID)
req.Header.Set("accessType", "API")
req.Header.Set("reserve", "")
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)
@ -106,6 +113,7 @@ func RandomNumber(n int) string {
// EncryptMobile 手机银行支付参数加密
// 返回: tysdPayParams(base64), tysdEncryptKey(base64)
// 使用 YouChuKoffee 加密包psbc/internal/sm2 + psbc/internal/util
func (c *Client) EncryptMobile(inputJson string, signStr string) (string, string, error) {
defer func() {
if err := recover(); err != nil {
@ -113,14 +121,14 @@ func (c *Client) EncryptMobile(inputJson string, signStr string) (string, string
}
}()
sm4Key := generateSM4Key()
sm4Key := psbcutil.GenerateSM4Key()
// SM2 加密 SM4 密钥(使用银行公钥
bankPubKey, err := x509.ReadPublicKeyFromHex(c.cfg.BankKey)
// SM2 加密 SM4 密钥(使用银行公钥,使用 YouChuKoffee 的 Sm2Encrypt
bankPubKey, err := sm2.ReadPublicKeyFromHex(c.cfg.BankKey)
if err != nil {
return "", "", fmt.Errorf("读取银行公钥失败: %v", err)
}
encryptKeyBytes, err := sm2.Encrypt(bankPubKey, sm4Key, nil, sm2.C1C3C2)
encryptKeyBytes, err := psbcutil.Sm2Encrypt(bankPubKey, sm4Key)
if err != nil {
return "", "", fmt.Errorf("sm2加密失败: %v", err)
}
@ -129,8 +137,8 @@ func (c *Client) EncryptMobile(inputJson string, signStr string) (string, string
encryptKeyBytes = encryptKeyBytes[1:]
}
// SM2 签名(与 YouChuKoffee 对齐:UserID 传 nil默认值 1234567812345678
merchantPrivKey, err := x509.ReadPrivateKeyFromHex(c.cfg.PrivateKey)
// SM2 签名(使用 YouChuKoffee 的 Sm2SignUserID 传 nil默认值 1234567812345678
merchantPrivKey, err := sm2.ReadPrivateKeyFromHex(c.cfg.PrivateKey)
if err != nil {
return "", "", fmt.Errorf("读取商户私钥失败: %v", err)
}
@ -141,8 +149,10 @@ func (c *Client) EncryptMobile(inputJson string, signStr string) (string, string
sig := encodeBase64(rsToBytes(r, s))
// 拼接参数并 SM4 ECB 加密
// 对齐 YouChuKoffee: sm43.ECBEncrypt(sm4Key, sm43.PKCS5Padding([]byte(param), 16))
// sm4ECBEncrypt 内部已处理 padding调用方不需要再 pad
param := fmt.Sprintf("%s&sign=%s", inputJson, sig)
tmp, err := sm4ECBEncrypt(sm4Key, pkcs5Padding([]byte(param), 16))
tmp, err := sm4ECBEncrypt(sm4Key, []byte(param))
if err != nil {
return "", "", fmt.Errorf("sm4加密失败: %v", err)
}
@ -151,91 +161,113 @@ func (c *Client) EncryptMobile(inputJson string, signStr string) (string, string
}
// DecryptResponse 解密银行响应
func (c *Client) DecryptResponse(respJson string, isRequest bool) (string, error) {
var reqData map[string]string
if err := json.Unmarshal([]byte(respJson), &reqData); err != nil {
return "", fmt.Errorf("解析响应JSON失败: %v响应内容: %s", err, respJson)
}
dataKey := "response"
func (c *Client) DecryptResponse(response string, isRequest bool) (Rsponse string, err error) {
var MerchantId, PrivateKey, PublicKey string
if isRequest {
dataKey = "request"
MerchantId = c.cfg.MerchantId
PrivateKey = c.cfg.PrivateKeyCallback
PublicKey = c.cfg.SopPublicKey
} else {
MerchantId = c.cfg.MerchantId
PrivateKey = c.cfg.PrivateKeyCallback
PublicKey = c.cfg.SopPublicKey
}
_, hasData := reqData[dataKey]
_, hasSignature := reqData["signature"]
_, hasEncryptKey := reqData["encryptKey"]
if !hasData || !hasSignature || !hasEncryptKey {
if code, ok := reqData["code"]; ok {
msg := reqData["msg"]
if msg == "" {
msg = reqData["message"]
}
return "", fmt.Errorf("银行返回错误,错误码: %s错误信息: %s", code, msg)
}
if respCode, ok := reqData["respCode"]; ok {
respMsg := reqData["respMsg"]
return "", fmt.Errorf("银行返回错误,错误码: %s错误信息: %s", respCode, respMsg)
}
missingFields := []string{}
if !hasData {
missingFields = append(missingFields, dataKey)
}
if !hasSignature {
missingFields = append(missingFields, "signature")
}
if !hasEncryptKey {
missingFields = append(missingFields, "encryptKey")
}
return "", fmt.Errorf("响应格式不正确,缺少字段: %v响应内容: %s", missingFields, respJson)
}
reqData["accessToken"] = ""
inData := reqData[dataKey]
inSignature := reqData["signature"]
inEncryptKey := reqData["encryptKey"]
// 验签
checked := c.verify(fmt.Sprintf("%s%s%s", inData, inEncryptKey, ""), inSignature)
if !checked {
return "", fmt.Errorf("签名验证失败")
}
// 解密 SM4 密钥
privKey, err := x509.ReadPrivateKeyFromHex(c.cfg.PrivateKey)
encrypt, err := Decrypt(MerchantId, PrivateKey, PublicKey, response, isRequest)
if err != nil {
return "", fmt.Errorf("读取私钥失败: %v", err)
return
}
var RsponseData map[string]interface{}
err = json.Unmarshal([]byte(encrypt), &RsponseData)
if err != nil {
return
} else {
if RsponseData["body"] != nil {
if body, ok := RsponseData["body"].(string); ok {
Rsponse = body
} else {
notify, _ := json.Marshal(RsponseData["body"].(map[string]interface{}))
Rsponse = string(notify)
}
}
}
return
}
func Decrypt(merchantId, privateKey, sopPublicKey, respJson string, isRequest bool) (string, error) {
var reqData map[string]string
err := json.Unmarshal([]byte(respJson), &reqData)
if err != nil {
return "", err
}
reqData["accessToken"] = ""
keys := [4]string{}
if isRequest {
keys = [4]string{"request", "signature", "encryptKey", "accessToken"}
} else {
keys = [4]string{"response", "signature", "encryptKey", "accessToken"}
}
var inEncryptKey, inAccessToken, inData, inSignature string
for i := 0; i < 4; i++ {
data, err := checkInData(reqData, keys[i])
if err != nil {
return "", err
}
switch keys[i] {
case "request", "response":
inData = data
case "signature":
inSignature = data
case "encryptKey":
inEncryptKey = data
case "accessToken":
inAccessToken = data
}
}
checked := verify(fmt.Sprintf("%s%s%s", inData, inEncryptKey, inAccessToken), inSignature, sopPublicKey, merchantId)
if !checked {
return "", errors.New("签名验证失败")
}
priKey, err := sm2.ReadPrivateKeyFromHex(privateKey)
if err != nil {
return "", errors.New("读取私钥失败")
}
hexEncryptKey, err := hex.DecodeString(inEncryptKey)
if err != nil {
return "", fmt.Errorf("解密sm4key失败: %v", err)
return "", errors.New("解密sm4key失败")
}
sm4Key, err := sm2.Decrypt(privKey, hexEncryptKey, sm2.C1C3C2)
sm4Key, err := psbcutil.Sm2Decrypt(priKey, hexEncryptKey)
if err != nil {
return "", fmt.Errorf("解密sm2key失败: %v", err)
return "", errors.New("解密sm2key失败")
}
// 解密数据
request, _ := base64.StdEncoding.DecodeString(inData)
encryptedSm4Key, err := sm4.CBCDecrypt(sm4Key, getSM4IV(), request)
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
}
}
encryptedSm4Key, err := sm4.CBCDecrypt(sm4Key, psbcutil.GetSM4IV(), request)
return string(psbcutil.Padding(encryptedSm4Key, 0)), nil
}
return decryptedStr, nil
func checkInData(reqData map[string]string, key string) (string, error) {
data, ok := reqData[key]
if !ok {
return "", errors.New("请求数据中不存在" + key)
}
return data, nil
}
func verify(content string, signature string, publicKeyStr string, merchantId string) bool {
pubKey, err := sm2.ReadPublicKeyFromHex(publicKeyStr)
//content = ""
if err != nil {
panic(fmt.Sprintf("pubKeyBytes sm2 ReadPublicKeyFromHex err: %v", err))
}
r, s := signToRS(signature)
return sm2.Sm2Verify(pubKey, []byte(content), []byte(merchantId), r, s)
}
// VerifySignature 验签
@ -244,7 +276,7 @@ func (c *Client) VerifySignature(content, signature string) bool {
}
func (c *Client) verify(content, signature string) bool {
pubKey, err := x509.ReadPublicKeyFromHex(c.cfg.SopPublicKey)
pubKey, err := sm2.ReadPublicKeyFromHex(c.cfg.SopPublicKey)
if err != nil {
panic(fmt.Sprintf("pubKeyBytes sm2 ReadPublicKeyFromHex err: %v", err))
}

View File

@ -6,11 +6,12 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"strings"
"PaymentCenter/app/third/paymentService/psbc/internal/sm2"
psbcutil "PaymentCenter/app/third/paymentService/psbc/internal/util"
"github.com/ZZMarquis/gm/sm4"
"github.com/tjfoc/gmsm/sm2"
"github.com/tjfoc/gmsm/x509"
)
// EncryptRequest 加密请求
@ -30,11 +31,11 @@ func EncryptRequest(request interface{}, cfg Config) (map[string]string, error)
return nil, fmt.Errorf("序列化请求失败: %v", err)
}
inputJson := string(inputBytes)
// Step 2: SM4 CBC 加密
sm4Key := generateSM4Key()
iv := getSM4IV()
paddedData := pkcs5Padding([]byte(inputJson), sm4.BlockSize)
sm4Key := psbcutil.GenerateSM4Key()
iv := psbcutil.GetSM4IV() // UISwD9fW6cFh9SNS
paddedData := psbcutil.Padding([]byte(inputJson), 1)
tmp, err := sm4.CBCEncrypt(sm4Key, iv, paddedData)
if err != nil {
return nil, fmt.Errorf("SM4加密失败: %v", err)
@ -42,12 +43,12 @@ func EncryptRequest(request interface{}, cfg Config) (map[string]string, error)
responseMsg := base64.StdEncoding.EncodeToString(tmp)
responseMsg = addNewline(responseMsg) // 对齐 YouChuKoffee每76字符加 \r\n
// Step 3: SM2 加密 SM4 密钥
sopPubKey, err := x509.ReadPublicKeyFromHex(cfg.SopPublicKey)
// Step 3: SM2 加密 SM4 密钥(使用 copied sm2 包,对齐 YouChuKoffee 的 Sm2Encrypt
sopPubKey, err := sm2.ReadPublicKeyFromHex(cfg.SopPublicKey)
if err != nil {
return nil, fmt.Errorf("读取SOP公钥失败: %v", err)
}
encryptKeyBytes, err := sm2.Encrypt(sopPubKey, sm4Key, rand.Reader, sm2.C1C3C2)
encryptKeyBytes, err := psbcutil.Sm2Encrypt(sopPubKey, sm4Key)
if err != nil {
return nil, fmt.Errorf("SM2加密SM4密钥失败: %v", err)
}
@ -55,7 +56,8 @@ func EncryptRequest(request interface{}, cfg Config) (map[string]string, error)
// Step 4: 签名(对齐 YouChuKoffee sign()UserID = MerchantId
signContent := fmt.Sprintf("%s%s%s", responseMsg, encryptKey, accessToken)
signature, err := client.signWithUserID(signContent, []byte(cfg.MerchantId))
signature, err := sign(cfg.MerchantId, client.cfg.PrivateKeyCallback, signContent)
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
@ -71,16 +73,23 @@ func EncryptRequest(request interface{}, cfg Config) (map[string]string, error)
// 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)
func sign(merchantId string, privateKeyHex string, signContent string) (string, error) {
privateKey, err := sm2.ReadPrivateKeyFromHex(privateKeyHex)
if err != nil {
return "", fmt.Errorf("读取私钥失败: %v", err)
return "", err
}
r, s, err := sm2.Sm2Sign(privKey, []byte(content), userId, rand.Reader)
r, s, err := sm2.Sm2Sign(privateKey, []byte(signContent), []byte(merchantId), rand.Reader)
if err != nil {
return "", fmt.Errorf("SM2签名失败: %v", err)
return "", err
}
return r.Text(16) + "#" + s.Text(16), nil
return rSToSign(r, s), nil
}
func rSToSign(r *big.Int, s *big.Int) string {
rStr := r.Text(16)
sStr := s.Text(16)
return fmt.Sprintf("%s#%s", rStr, sStr)
}
// addNewline 每76个字符添加 \r\n对齐 YouChuKoffee 的 postbank.addNewline

View File

@ -35,7 +35,7 @@ func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) {
"sourceId": "16",
"reqTraceId": now + RandomNumber(10),
"reqDate": now, // 14 位 YYYYMMDDHHmmss
"txnDt": now,
"txnDt": time.Now().Format("20060102"), // 8 位 YYYYMMDD对齐 YouChuKoffee
"mchtNo": c.cfg.MchtNo,
"oldSeqNo": orderNo,
},
@ -48,12 +48,13 @@ func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) {
return nil, fmt.Errorf("加密请求失败: %v", err)
}
BusiMainId = "202607301754241710448735"
encryptedBytes, _ := json.Marshal(encryptedReq)
url := c.cfg.OrderHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
// head 同时作为 HTTP Header 明文传输
head := requestBody["head"].(map[string]string)
body, err := c.doPost(url, head, encryptedBytes)
// head 同时作为 HTTP Header 明文传输(对齐 YouChuKoffee 的 doPost 方式)
body, err := c.doPost(url, BusiMainId, "b2c.gatewaypay.orderQuery", encryptedBytes)
if err != nil {
return nil, err
}

View File

@ -53,9 +53,8 @@ func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
encryptedBytes, _ := json.Marshal(encryptedReq)
url := c.cfg.OrderHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId
// head 同时作为 HTTP Header 明文传输
head := requestBody["head"].(map[string]string)
body, err := c.doPost(url, head, encryptedBytes)
// head 同时作为 HTTP Header 明文传输(对齐 YouChuKoffee 的 doPost 方式)
body, err := c.doPost(url, BusiMainId, "b2c.gatewaypay.orderRefund", encryptedBytes)
if err != nil {
return nil, err
}

View File

@ -23,6 +23,7 @@ type Config struct {
FileHost string
ShopId string
Psbcmcc string // 三方服务商标识,如 LSXD、JBZ
PrivateKeyCallback string // 邮储响应解密的私钥
}
// PaymentLinkRequest 生成支付链接请求

View File

@ -17,6 +17,7 @@ type PsbcPay struct {
AppID string `json:"app_id"` // 应用ID
SopPublicKey string `json:"sop_public_key"` // SOP公钥
PrivateKey string `json:"private_key"` // 商户私钥
PrivateKeyCallback string `json:"private_key_callback"` // 邮储响应解密的私钥
Pubkey string `json:"pubkey"` // 商户公钥
BankKey string `json:"bank_key"` // 银行公钥
Sha string `json:"sha"` // SHA密钥
@ -50,6 +51,7 @@ func PsbcInitClient(psbcConfig PsbcPay) *psbc.Client {
FileHost: psbcConfig.FileHost,
ShopId: psbcConfig.ShopId,
Psbcmcc: psbcConfig.Psbcmcc,
PrivateKeyCallback: psbcConfig.PrivateKeyCallback,
}
return psbc.NewClient(cfg)
}

View File

@ -40,6 +40,7 @@ func TestPsbcPay(t *testing.T) {
AppID: "961925472724332544001",
SopPublicKey: "04CABE03249C94BDC8A6A4440DA1B2ADFACF73F4340E5F1B9A76463694B44C2E5600A9BEAA035739383C292CF9F1C4695FAAC7963CD5033D5D647A6B1EBE78EC6A",
PrivateKey: "140c5c68da1829b79d8e4629a8b9548bd694ccc7061fbacb1079aed0e5ac33fa",
PrivateKeyCallback: "1F0E2F085955461A9B87820AFBD513712CAEA89687BE657DE4EC91613BE62D32",
Pubkey: "0447fe9ed13aac6200c92b893ad1289a98437c8404e64b1d2f7c755698a8b8e8acb35c1db2bedc5213c930bdfc10efb0e12269cf6a06877679f114fe5ae9dd4469",
BankKey: "04E0E2E37F11901483CDFBC47F489D87D5D78C55DD7F919B73DEA83007748668B7871A1BA9608F156E25B7D64C7821379BAC1E2C591D5A50FF311D1AAE026C1DAE",
Sha: "636879vrt96t57w6f59",
@ -69,18 +70,18 @@ func TestPsbcPay(t *testing.T) {
PayerClientIp: "192.168.110.235",
Psbc: psbc,
}
//支付
orderResult := PaymentService(c, request)
t.Log(orderResult)
////支付
//orderResult := PaymentService(c, request)
//t.Log(orderResult)
//// 查询订单
//qreq := PayOrderQueryRequest{
// OrderId: request.OrderId,
// PayChannel: 3,
// Psbc: request.Psbc,
//}
//queryResult := PayOrderQuery(c, qreq)
//t.Log(queryResult)
// 查询订单
qreq := PayOrderQueryRequest{
OrderId: request.OrderId,
PayChannel: 3,
Psbc: request.Psbc,
}
queryResult := PayOrderQuery(c, qreq)
t.Log(queryResult)
//// 退款
//refund := OrderRefundRequest{