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 }