From 7607a7118ca5d26eb86915f8b9a9107cef78b973 Mon Sep 17 00:00:00 2001 From: wolter <11@gmail> Date: Thu, 30 Jul 2026 14:37:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=82=AE=E5=82=A8=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=AE=8C=E5=96=84=E8=A1=A5=E5=85=85=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/http/controllers/front/api.go | 1 + app/http/entities/front/pay.go | 7 +- app/services/thirdpay/do/pay.go | 3 + app/third/paymentService/psbc/bill.go | 61 ++++++------- app/third/paymentService/psbc/client.go | 58 +++++++++++- app/third/paymentService/psbc/encrypt.go | 108 +++++++++++++++++------ app/third/paymentService/psbc/notify.go | 15 +--- app/third/paymentService/psbc/order.go | 28 +++--- app/third/paymentService/psbc/pay_url.go | 26 ++++++ app/third/paymentService/psbc/refund.go | 29 +++--- app/third/paymentService/psbc/types.go | 9 ++ app/third/paymentService/psbc_service.go | 13 ++- app/third/paymentService/psbc_test.go | 105 ++++++++++++++++++++++ 13 files changed, 347 insertions(+), 116 deletions(-) create mode 100644 app/third/paymentService/psbc_test.go diff --git a/app/http/controllers/front/api.go b/app/http/controllers/front/api.go index 4cf868b..8d5bb31 100644 --- a/app/http/controllers/front/api.go +++ b/app/http/controllers/front/api.go @@ -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 diff --git a/app/http/entities/front/pay.go b/app/http/entities/front/pay.go index bfe8cb6..7d09f6a 100644 --- a/app/http/entities/front/pay.go +++ b/app/http/entities/front/pay.go @@ -61,9 +61,10 @@ type CloseReqs struct { // api 接口返回数据, 统一返回结构, order数据会进行加密 type ApiResponse struct { - Order interface{} `json:"order,omitempty"` - Url string `json:"url,omitempty"` - JsInfo *WxMiniPayResponse `json:"js_info,omitempty"` + Order interface{} `json:"order,omitempty"` + Url string `json:"url,omitempty"` + ThirdMsg string `json:"third_msg,omitempty"` + JsInfo *WxMiniPayResponse `json:"js_info,omitempty"` } type PayChannelListRequest struct { diff --git a/app/services/thirdpay/do/pay.go b/app/services/thirdpay/do/pay.go index 45e9463..fb51061 100644 --- a/app/services/thirdpay/do/pay.go +++ b/app/services/thirdpay/do/pay.go @@ -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 } diff --git a/app/third/paymentService/psbc/bill.go b/app/third/paymentService/psbc/bill.go index ea39dd0..cd34dbc 100644 --- a/app/third/paymentService/psbc/bill.go +++ b/app/third/paymentService/psbc/bill.go @@ -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, - "sm4Key": "", - "fileId": fileId, + "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 diff --git a/app/third/paymentService/psbc/client.go b/app/third/paymentService/psbc/client.go index cea030e..90a7ee9 100644 --- a/app/third/paymentService/psbc/client.go +++ b/app/third/paymentService/psbc/client.go @@ -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 验签 diff --git a/app/third/paymentService/psbc/encrypt.go b/app/third/paymentService/psbc/encrypt.go index cb51672..7c4b20e 100644 --- a/app/third/paymentService/psbc/encrypt.go +++ b/app/third/paymentService/psbc/encrypt.go @@ -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() } diff --git a/app/third/paymentService/psbc/notify.go b/app/third/paymentService/psbc/notify.go index 9fbecad..5baaf64 100644 --- a/app/third/paymentService/psbc/notify.go +++ b/app/third/paymentService/psbc/notify.go @@ -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 diff --git a/app/third/paymentService/psbc/order.go b/app/third/paymentService/psbc/order.go index c5d897c..193c872 100644 --- a/app/third/paymentService/psbc/order.go +++ b/app/third/paymentService/psbc/order.go @@ -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) diff --git a/app/third/paymentService/psbc/pay_url.go b/app/third/paymentService/psbc/pay_url.go index de0e20c..a371d7b 100644 --- a/app/third/paymentService/psbc/pay_url.go +++ b/app/third/paymentService/psbc/pay_url.go @@ -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 +} diff --git a/app/third/paymentService/psbc/refund.go b/app/third/paymentService/psbc/refund.go index 8c7a699..c6df14c 100644 --- a/app/third/paymentService/psbc/refund.go +++ b/app/third/paymentService/psbc/refund.go @@ -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) diff --git a/app/third/paymentService/psbc/types.go b/app/third/paymentService/psbc/types.go index 62aa057..7102ca0 100644 --- a/app/third/paymentService/psbc/types.go +++ b/app/third/paymentService/psbc/types.go @@ -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 diff --git a/app/third/paymentService/psbc_service.go b/app/third/paymentService/psbc_service.go index 94c486c..82cfc68 100644 --- a/app/third/paymentService/psbc_service.go +++ b/app/third/paymentService/psbc_service.go @@ -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) { diff --git a/app/third/paymentService/psbc_test.go b/app/third/paymentService/psbc_test.go new file mode 100644 index 0000000..210a943 --- /dev/null +++ b/app/third/paymentService/psbc_test.go @@ -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) +}