feat:邮储支付代码完善补充缺失逻辑

This commit is contained in:
wolter 2026-07-30 14:37:43 +08:00
parent 80bdf8b00a
commit 7607a7118c
13 changed files with 347 additions and 116 deletions

View File

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

View File

@ -63,6 +63,7 @@ type CloseReqs struct {
type ApiResponse struct { type ApiResponse struct {
Order interface{} `json:"order,omitempty"` Order interface{} `json:"order,omitempty"`
Url string `json:"url,omitempty"` Url string `json:"url,omitempty"`
ThirdMsg string `json:"third_msg,omitempty"`
JsInfo *WxMiniPayResponse `json:"js_info,omitempty"` JsInfo *WxMiniPayResponse `json:"js_info,omitempty"`
} }

View File

@ -211,6 +211,9 @@ func (w *Pay) PayUrl() (url string) {
// 获取签名 // 获取签名
err = w.JsApiInfo.SignPaySign(thirdPay.Wx.PrivateKey) err = w.JsApiInfo.SignPaySign(thirdPay.Wx.PrivateKey)
} }
} else if w.PayParam.Channel.ChannelType == common.PAY_CHANNEL_PSBC {
// 邮储支付返回 handCodePay 参数
w.ThirdMsg = res.Result
} else { } else {
w.Url = res.Result w.Url = res.Result
} }

View File

@ -1,15 +1,16 @@
package payment package payment
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"time" "time"
) )
// BillQuery 查询对账单 // 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) { func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
now := time.Now().Format("20060102150405") now := time.Now().Format("20060102150405")
BusiMainId := now + RandomNumber(10) BusiMainId := now + RandomNumber(10)
@ -18,6 +19,7 @@ func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
date = time.Now().AddDate(0, 0, -1) date = time.Now().AddDate(0, 0, -1)
} }
// 构建完整 requesthead+body 一起加密,对齐 YouChuKoffee
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"head": map[string]string{ "head": map[string]string{
"partnerTxSriNo": BusiMainId, "partnerTxSriNo": BusiMainId,
@ -29,12 +31,13 @@ func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
"accessType": "API", "accessType": "API",
"reserve": "", "reserve": "",
}, },
"body": map[string]interface{}{ "body": map[string]string{
"date": date.Format("20060102"), "accountingDate": date.Format("20060102"),
"mchtNo": c.cfg.MchtNo, "fileIntfcName": c.cfg.MchtNo,
}, },
} }
// 整个 {head, body} 一起加密
encryptedReq, err := EncryptRequest(requestBody, c.cfg) encryptedReq, err := EncryptRequest(requestBody, c.cfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("加密请求失败: %v", err) return nil, fmt.Errorf("加密请求失败: %v", err)
@ -43,19 +46,11 @@ func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
encryptedBytes, _ := json.Marshal(encryptedReq) encryptedBytes, _ := json.Marshal(encryptedReq)
url := c.cfg.FileHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId 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 { if err != nil {
return nil, fmt.Errorf("请求失败: %v", err) return nil, 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))
} }
responseData, err := c.DecryptResponse(string(body), false) responseData, err := c.DecryptResponse(string(body), false)
@ -72,10 +67,15 @@ func (c *Client) BillQuery(date time.Time) (*BillQueryResponse, error) {
} }
// BillDownload 下载对账单 // 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) { func (c *Client) BillDownload(fileId string, date time.Time) ([]byte, error) {
now := time.Now().Format("20060102150405") now := time.Now().Format("20060102150405")
BusiMainId := now + RandomNumber(10) BusiMainId := now + RandomNumber(10)
// 构建完整 requesthead+body 一起加密,对齐 YouChuKoffee
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"head": map[string]string{ "head": map[string]string{
"partnerTxSriNo": BusiMainId, "partnerTxSriNo": BusiMainId,
@ -87,13 +87,14 @@ func (c *Client) BillDownload(fileId string, date time.Time) ([]byte, error) {
"accessType": "API", "accessType": "API",
"reserve": "", "reserve": "",
}, },
"body": map[string]interface{}{ "body": map[string]string{
"mchtNo": c.cfg.MchtNo, "fileIntfcName": c.cfg.MchtNo,
"sm4Key": "", "sm4Key": "",
"fileId": fileId, "fileId": fileId,
}, },
} }
// 整个 {head, body} 一起加密
encryptedReq, err := EncryptRequest(requestBody, c.cfg) encryptedReq, err := EncryptRequest(requestBody, c.cfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("加密请求失败: %v", err) 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) encryptedBytes, _ := json.Marshal(encryptedReq)
url := c.cfg.FileHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId 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 { if err != nil {
return nil, fmt.Errorf("请求失败: %v", err) return nil, 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 return body, nil

View File

@ -1,6 +1,7 @@
package payment package payment
import ( import (
"bytes"
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
@ -8,7 +9,9 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"math/big" "math/big"
"net/http"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -28,6 +31,39 @@ func NewClient(cfg Config) *Client {
return &Client{cfg: cfg} 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 格式 // MapToString 将 map 转换为排序后的 key=value&key=value 格式
func MapToString(m map[string]string) string { func MapToString(m map[string]string) string {
keys := make([]string, 0, len(m)) keys := make([]string, 0, len(m))
@ -93,12 +129,12 @@ func (c *Client) EncryptMobile(inputJson string, signStr string) (string, string
encryptKeyBytes = encryptKeyBytes[1:] encryptKeyBytes = encryptKeyBytes[1:]
} }
// SM2 签名 // SM2 签名(与 YouChuKoffee 对齐UserID 传 nil默认值 1234567812345678
merchantPrivKey, err := x509.ReadPrivateKeyFromHex(c.cfg.PrivateKey) merchantPrivKey, err := x509.ReadPrivateKeyFromHex(c.cfg.PrivateKey)
if err != nil { if err != nil {
return "", "", fmt.Errorf("读取商户私钥失败: %v", err) 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 { if err != nil {
return "", "", fmt.Errorf("签名失败: %v", err) return "", "", fmt.Errorf("签名失败: %v", err)
} }
@ -183,7 +219,23 @@ func (c *Client) DecryptResponse(respJson string, isRequest bool) (string, error
// 解密数据 // 解密数据
request, _ := base64.StdEncoding.DecodeString(inData) request, _ := base64.StdEncoding.DecodeString(inData)
encryptedSm4Key, err := sm4.CBCDecrypt(sm4Key, getSM4IV(), request) 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 验签 // VerifySignature 验签

View File

@ -1,43 +1,97 @@
package payment package payment
import ( import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "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) { 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 { if err != nil {
return nil, fmt.Errorf("序列化请求失败: %v", err) return nil, fmt.Errorf("序列化请求失败: %v", err)
} }
inputJson := string(inputBytes)
client := NewClient(cfg) // Step 2: SM4 CBC 加密
sm4Key := generateSM4Key()
// 构造签名数据 iv := getSM4IV()
signData := map[string]string{ paddedData := pkcs5Padding([]byte(inputJson), sm4.BlockSize)
"merchantNo": cfg.MchtNo, tmp, err := sm4.CBCEncrypt(sm4Key, iv, paddedData)
}
signStr := MapToString(signData)
// 构造订单数据
orderData := map[string]string{
"merchantNo": cfg.MchtNo,
"orderNo": "",
"amount": "",
}
orderStr := MapToString(orderData)
// 加密
encryptedData, encryptedKey, err := client.EncryptMobile(orderStr, signStr)
if err != nil { 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{ result := map[string]string{
"request": encryptedData, "request": responseMsg,
"signature": "", "signature": signature,
"encryptKey": encryptedKey, "encryptKey": encryptKey,
"accessToken": "", "accessToken": accessToken,
}, nil }
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()
} }

View File

@ -26,19 +26,6 @@ func (c *Client) ParseNotify(rawJson string) (*NotifyRequest, error) {
// VerifyAndParseNotify 验签并解析回调通知 // VerifyAndParseNotify 验签并解析回调通知
func (c *Client) VerifyAndParseNotify(rawJson string) (*YouChuOrderNotifyRequest, error) { 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) decrypted, err := c.DecryptNotify(rawJson)
if err != nil { if err != nil {
return nil, fmt.Errorf("解密通知失败: %v", err) return nil, fmt.Errorf("解密通知失败: %v", err)
@ -46,7 +33,7 @@ func (c *Client) VerifyAndParseNotify(rawJson string) (*YouChuOrderNotifyRequest
var result YouChuOrderNotifyRequest var result YouChuOrderNotifyRequest
if err := json.Unmarshal([]byte(decrypted), &result); err != nil { 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 return &result, nil

View File

@ -1,19 +1,21 @@
package payment package payment
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"time" "time"
) )
// OrderQuery 查询订单支付状态 // OrderQuery 查询订单支付状态
// 对齐 YouChuKoffee 的 OrderQuery 流程:
// 1. 构建完整 {head, body} 结构
// 2. 整个 {head, body} 一起加密EncryptRequest
// 3. head 同时作为 HTTP Header 明文传输
func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) { func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) {
now := time.Now().Format("20060102150405") now := time.Now().Format("20060102150405")
BusiMainId := now + RandomNumber(10) BusiMainId := now + RandomNumber(10)
// 构建完整 requesthead+body 一起加密,对齐 YouChuKoffee
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"head": map[string]string{ "head": map[string]string{
"partnerTxSriNo": BusiMainId, "partnerTxSriNo": BusiMainId,
@ -32,13 +34,15 @@ func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) {
"txnCode": "1004", "txnCode": "1004",
"sourceId": "16", "sourceId": "16",
"reqTraceId": now + RandomNumber(10), "reqTraceId": now + RandomNumber(10),
"reqDate": time.Now().Format("20060102"), "reqDate": now, // 14 位 YYYYMMDDHHmmss
"txnDt": now,
"mchtNo": c.cfg.MchtNo, "mchtNo": c.cfg.MchtNo,
"oldSeqNo": orderNo, "oldSeqNo": orderNo,
}, },
}, },
} }
// 整个 {head, body} 一起加密
encryptedReq, err := EncryptRequest(requestBody, c.cfg) encryptedReq, err := EncryptRequest(requestBody, c.cfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("加密请求失败: %v", err) return nil, fmt.Errorf("加密请求失败: %v", err)
@ -47,19 +51,11 @@ func (c *Client) OrderQuery(orderNo string) (*OrderQueryResponse, error) {
encryptedBytes, _ := json.Marshal(encryptedReq) encryptedBytes, _ := json.Marshal(encryptedReq)
url := c.cfg.OrderHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId 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 { if err != nil {
return nil, fmt.Errorf("请求失败: %v", err) return nil, 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))
} }
responseData, err := c.DecryptResponse(string(body), false) responseData, err := c.DecryptResponse(string(body), false)

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"strings" "strings"
"time"
) )
// CreatePaymentLink 创建支付链接 // CreatePaymentLink 创建支付链接
@ -123,3 +124,28 @@ func (c *Client) CreatePaymentLinkWithBackUrl(req PaymentLinkRequest) (*PaymentL
func contains(s, substr string) bool { func contains(s, substr string) bool {
return len(s) > 0 && len(substr) > 0 && strings.Contains(s, substr) 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
}

View File

@ -1,19 +1,21 @@
package payment package payment
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"time" "time"
) )
// Refund 申请退款 // 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) { func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
now := time.Now().Format("20060102150405") now := time.Now().Format("20060102150405")
BusiMainId := now + RandomNumber(10) BusiMainId := now + RandomNumber(10)
// 构建完整 requesthead+body 一起加密,对齐 YouChuKoffee
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"head": map[string]string{ "head": map[string]string{
"partnerTxSriNo": BusiMainId, "partnerTxSriNo": BusiMainId,
@ -31,8 +33,8 @@ func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
"data": map[string]string{ "data": map[string]string{
"txnCode": "1003", "txnCode": "1003",
"sourceId": "16", "sourceId": "16",
"reqTraceId": req.OrderNo + now[:8], "reqTraceId": req.OrderNo + now[4:14], // MMDDHHmmss 10位后缀
"reqDate": now[:8], "reqDate": now, // 14 位 YYYYMMDDHHmmss
"mchtNo": c.cfg.MchtNo, "mchtNo": c.cfg.MchtNo,
"orgTxnSeq": req.OrgTxnSeq, "orgTxnSeq": req.OrgTxnSeq,
"txnAmt": req.Price, "txnAmt": req.Price,
@ -42,6 +44,7 @@ func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
}, },
} }
// 整个 {head, body} 一起加密
encryptedReq, err := EncryptRequest(requestBody, c.cfg) encryptedReq, err := EncryptRequest(requestBody, c.cfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("加密请求失败: %v", err) return nil, fmt.Errorf("加密请求失败: %v", err)
@ -50,19 +53,11 @@ func (c *Client) Refund(req RefundRequest) (*RefundResponse, error) {
encryptedBytes, _ := json.Marshal(encryptedReq) encryptedBytes, _ := json.Marshal(encryptedReq)
url := c.cfg.OrderHost + c.cfg.MerchantId + ".htm?partnerTxSriNo=" + BusiMainId 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 { if err != nil {
return nil, fmt.Errorf("请求失败: %v", err) return nil, 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))
} }
responseData, err := c.DecryptResponse(string(body), false) responseData, err := c.DecryptResponse(string(body), false)

View File

@ -22,6 +22,7 @@ type Config struct {
OrderHost string OrderHost string
FileHost string FileHost string
ShopId string ShopId string
Psbcmcc string // 三方服务商标识,如 LSXD、JBZ
} }
// PaymentLinkRequest 生成支付链接请求 // PaymentLinkRequest 生成支付链接请求
@ -41,6 +42,14 @@ type PaymentLinkResponse struct {
PayUrl string `json:"pay_url"` 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 订单查询请求 // OrderQueryRequest 订单查询请求
type OrderQueryRequest struct { type OrderQueryRequest struct {
OrderNo string OrderNo string

View File

@ -5,6 +5,7 @@ import (
psbc "PaymentCenter/app/third/paymentService/psbc" psbc "PaymentCenter/app/third/paymentService/psbc"
"PaymentCenter/config" "PaymentCenter/config"
"context" "context"
"encoding/json"
"fmt" "fmt"
"github.com/qit-team/snow-core/log/logger" "github.com/qit-team/snow-core/log/logger"
"strconv" "strconv"
@ -27,6 +28,7 @@ type PsbcPay struct {
OrderHost string `json:"order_host"` // 订单服务地址 OrderHost string `json:"order_host"` // 订单服务地址
FileHost string `json:"file_host"` // 文件服务地址 FileHost string `json:"file_host"` // 文件服务地址
ShopId string `json:"shop_id"` // 店铺ID ShopId string `json:"shop_id"` // 店铺ID
Psbcmcc string `json:"psbcmcc"` // 三方服务商标识
} }
func PsbcInitClient(psbcConfig PsbcPay) *psbc.Client { func PsbcInitClient(psbcConfig PsbcPay) *psbc.Client {
@ -47,6 +49,7 @@ func PsbcInitClient(psbcConfig PsbcPay) *psbc.Client {
OrderHost: psbcConfig.OrderHost, OrderHost: psbcConfig.OrderHost,
FileHost: psbcConfig.FileHost, FileHost: psbcConfig.FileHost,
ShopId: psbcConfig.ShopId, ShopId: psbcConfig.ShopId,
Psbcmcc: psbcConfig.Psbcmcc,
} }
return psbc.NewClient(cfg) return psbc.NewClient(cfg)
} }
@ -69,12 +72,18 @@ func PsbcPayInfo(c context.Context, payOrderRequest PayOrderRequest) (string, er
BackUrl: payOrderRequest.ReturnUrl, BackUrl: payOrderRequest.ReturnUrl,
} }
resp, err := psbcClient.CreatePaymentLink(req) resp, err := psbcClient.CreateHandCodePayParams(req)
if err != nil { if err != nil {
logger.Error(c, "PsbcPayInfo 发生错误", fmt.Sprintf("错误信息:%s", err.Error())) logger.Error(c, "PsbcPayInfo 发生错误", fmt.Sprintf("错误信息:%s", err.Error()))
return "", err 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) { func PsbcOrderQuery(ctx context.Context, psbcConfig PsbcPay, orderNo string) (PayOrderQueryInfo, error) {

View File

@ -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)
}