添加文件: 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