添加文件: hb/client.go
This commit is contained in:
parent
6f5b142bfc
commit
eefb14c0ec
|
|
@ -0,0 +1,299 @@
|
||||||
|
package hb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client 手机支付平台客户端
|
||||||
|
type Client struct {
|
||||||
|
merchantId string // 商户编号
|
||||||
|
signKey string // 商户密钥(MD5签名使用)
|
||||||
|
privateKeyPEM string // 商户RSA私钥(RSA签名使用)
|
||||||
|
baseURL string // 接口基础URL
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientOption 客户端配置选项
|
||||||
|
type ClientOption func(*Client)
|
||||||
|
|
||||||
|
// WithSignKey 设置MD5签名密钥
|
||||||
|
func WithSignKey(signKey string) ClientOption {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.signKey = signKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPrivateKey 设置RSA私钥(PEM格式)
|
||||||
|
func WithPrivateKey(privateKeyPEM string) ClientOption {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.privateKeyPEM = privateKeyPEM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBaseURL 设置接口基础URL
|
||||||
|
func WithBaseURL(baseURL string) ClientOption {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.baseURL = baseURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHTTPClient 设置自定义HTTP客户端
|
||||||
|
func WithHTTPClient(httpClient *http.Client) ClientOption {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.httpClient = httpClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 创建新的手机支付平台客户端
|
||||||
|
// merchantId: 商户编号
|
||||||
|
// opts: 配置选项
|
||||||
|
func NewClient(merchantId string, opts ...ClientOption) (*Client, error) {
|
||||||
|
if merchantId == "" {
|
||||||
|
return nil, ErrMerchantIdEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Client{
|
||||||
|
merchantId: merchantId,
|
||||||
|
baseURL: "https://ipos.10086.cn",
|
||||||
|
httpClient: &http.Client{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refund 退款接口
|
||||||
|
// 通过中国移动手机支付渠道,将已成功交易的款项退还给用户
|
||||||
|
func (c *Client) Refund(ctx context.Context, req *RefundRequest) (*RefundResponse, error) {
|
||||||
|
if req == nil {
|
||||||
|
return nil, fmt.Errorf("request cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置固定参数
|
||||||
|
req.MerchantId = c.merchantId
|
||||||
|
req.Type = InterfaceTypeOrderRefund
|
||||||
|
req.Version = Version
|
||||||
|
|
||||||
|
// 参数校验
|
||||||
|
if req.RequestId == "" {
|
||||||
|
return nil, ErrRequestIdEmpty
|
||||||
|
}
|
||||||
|
if req.OrderId == "" {
|
||||||
|
return nil, ErrOrderIdEmpty
|
||||||
|
}
|
||||||
|
if req.Amount == "" {
|
||||||
|
return nil, ErrAmountEmpty
|
||||||
|
}
|
||||||
|
if req.SignType == "" {
|
||||||
|
return nil, ErrSignTypeInvalid
|
||||||
|
}
|
||||||
|
if req.SignType != SignTypeMD5 && req.SignType != SignTypeRSA {
|
||||||
|
return nil, ErrSignTypeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
hmac, err := c.generateRequestSign(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate sign failed: %w", err)
|
||||||
|
}
|
||||||
|
req.Hmac = hmac
|
||||||
|
|
||||||
|
// 构建请求参数
|
||||||
|
params := c.buildRequestParams(req)
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := c.doPost(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证响应签名
|
||||||
|
if err := c.verifyResponseSign(resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRequestSign 生成请求签名
|
||||||
|
func (c *Client) generateRequestSign(req *RefundRequest) (string, error) {
|
||||||
|
// 构建参数字典
|
||||||
|
params := map[string]string{
|
||||||
|
"merchantId": req.MerchantId,
|
||||||
|
"requestId": req.RequestId,
|
||||||
|
"signType": req.SignType,
|
||||||
|
"type": req.Type,
|
||||||
|
"version": req.Version,
|
||||||
|
"orderId": req.OrderId,
|
||||||
|
"amount": req.Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按文档指定顺序拼接签名串
|
||||||
|
signStr := buildSignString(params, getRequestSignKeys())
|
||||||
|
|
||||||
|
switch req.SignType {
|
||||||
|
case SignTypeMD5:
|
||||||
|
if c.signKey == "" {
|
||||||
|
return "", ErrSignKeyEmpty
|
||||||
|
}
|
||||||
|
return SignMD5(signStr, c.signKey), nil
|
||||||
|
case SignTypeRSA:
|
||||||
|
if c.privateKeyPEM == "" {
|
||||||
|
return "", ErrPrivateKeyInvalid
|
||||||
|
}
|
||||||
|
return SignRSA(signStr, c.privateKeyPEM)
|
||||||
|
default:
|
||||||
|
return "", ErrSignTypeInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRequestParams 构建请求参数(用于HTTP请求)
|
||||||
|
func (c *Client) buildRequestParams(req *RefundRequest) url.Values {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("merchantId", req.MerchantId)
|
||||||
|
params.Set("requestId", req.RequestId)
|
||||||
|
params.Set("signType", req.SignType)
|
||||||
|
params.Set("type", req.Type)
|
||||||
|
params.Set("version", req.Version)
|
||||||
|
params.Set("orderId", req.OrderId)
|
||||||
|
params.Set("amount", req.Amount)
|
||||||
|
params.Set("hmac", req.Hmac)
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// doPost 发送POST请求
|
||||||
|
func (c *Client) doPost(ctx context.Context, params url.Values) (*RefundResponse, error) {
|
||||||
|
apiURL := c.baseURL + "/ips/cmpayService"
|
||||||
|
|
||||||
|
bodyStr := params.Encode()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(bodyStr))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("do request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseResponse(string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseResponse 解析响应字符串为结构体
|
||||||
|
func parseResponse(raw string) (*RefundResponse, error) {
|
||||||
|
resp := &RefundResponse{}
|
||||||
|
|
||||||
|
pairs := strings.Split(raw, "&")
|
||||||
|
for _, pair := range pairs {
|
||||||
|
eqIdx := strings.IndexByte(pair, '=')
|
||||||
|
if eqIdx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := pair[:eqIdx]
|
||||||
|
value := pair[eqIdx+1:]
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "merchantId":
|
||||||
|
resp.MerchantId = value
|
||||||
|
case "payNo":
|
||||||
|
resp.PayNo = value
|
||||||
|
case "returnCode":
|
||||||
|
resp.ReturnCode = value
|
||||||
|
case "message":
|
||||||
|
resp.Message = value
|
||||||
|
case "signType":
|
||||||
|
resp.SignType = value
|
||||||
|
case "type":
|
||||||
|
resp.Type = value
|
||||||
|
case "version":
|
||||||
|
resp.Version = value
|
||||||
|
case "amount":
|
||||||
|
resp.Amount = value
|
||||||
|
case "orderId":
|
||||||
|
resp.OrderId = value
|
||||||
|
case "status":
|
||||||
|
resp.Status = value
|
||||||
|
case "serverCert":
|
||||||
|
resp.ServerCert = value
|
||||||
|
case "hmac":
|
||||||
|
resp.Hmac = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyResponseSign 验证响应签名
|
||||||
|
func (c *Client) verifyResponseSign(resp *RefundResponse) error {
|
||||||
|
// 构建参数字典
|
||||||
|
params := map[string]string{
|
||||||
|
"merchantId": resp.MerchantId,
|
||||||
|
"payNo": resp.PayNo,
|
||||||
|
"returnCode": resp.ReturnCode,
|
||||||
|
"message": resp.Message,
|
||||||
|
"signType": resp.SignType,
|
||||||
|
"type": resp.Type,
|
||||||
|
"version": resp.Version,
|
||||||
|
"amount": resp.Amount,
|
||||||
|
"orderId": resp.OrderId,
|
||||||
|
"status": resp.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按文档指定顺序拼接签名串
|
||||||
|
signStr := buildSignString(params, getResponseSignKeys())
|
||||||
|
|
||||||
|
switch resp.SignType {
|
||||||
|
case SignTypeMD5:
|
||||||
|
if c.signKey == "" {
|
||||||
|
return ErrSignKeyEmpty
|
||||||
|
}
|
||||||
|
if !VerifyMD5(signStr, c.signKey, resp.Hmac) {
|
||||||
|
return ErrSignVerifyFailed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case SignTypeRSA:
|
||||||
|
if resp.ServerCert == "" {
|
||||||
|
return fmt.Errorf("response missing serverCert for RSA verification")
|
||||||
|
}
|
||||||
|
// 从证书中提取公钥
|
||||||
|
pubKeyPEM, err := extractPublicKeyFromCert(resp.ServerCert)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("extract public key from cert failed: %w", err)
|
||||||
|
}
|
||||||
|
return VerifyRSA(signStr, resp.Hmac, pubKeyPEM)
|
||||||
|
default:
|
||||||
|
return ErrSignTypeInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRefundSuccess 判断退款是否成功
|
||||||
|
// 返回码为 000000 或 MCG00000 表示退款请求成功
|
||||||
|
// 退款最终结果以 status 字段为准
|
||||||
|
func IsRefundRequestSuccess(returnCode string) bool {
|
||||||
|
return returnCode == ReturnCodeSuccess || returnCode == ReturnCodeSuccessAlt
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRefundSuccess 判断退款最终结果是否成功
|
||||||
|
func IsRefundSuccess(status string) bool {
|
||||||
|
return status == RefundStatusSuccess
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue