hb-20260724155322/hb/valid.md

24 KiB
Raw Blame History

// File: hb/go.mod

module hb

go 1.21

// File: hb/types.go

package hb

// RefundRequest 退款接口请求参数
type RefundRequest struct {
	MerchantId   string `json:"merchantId"`             // 商户编号
	RequestId    string `json:"requestId"`              // 商户请求号
	SignType     string `json:"signType"`               // 签名方式MD5 或 RSA
	Type         string `json:"type"`                   // 接口类型OrderRefund
	Version      string `json:"version"`                // 版本号2.0.0
	OrderId      string `json:"orderId"`                // 商户订单号
	Amount       string `json:"amount"`                 // 退款金额,以分为单位
	MerchantCert string `json:"merchantCert,omitempty"` // 商户证书公钥(不参与签名)
	Hmac         string `json:"hmac,omitempty"`         // 签名数据
}

// RefundResponse 退款接口响应参数
type RefundResponse struct {
	MerchantId string `json:"merchantId"`             // 商户编号
	PayNo      string `json:"payNo"`                  // 手机支付平台退款交易流水号
	ReturnCode string `json:"returnCode"`             // 返回码
	Message    string `json:"message"`                // 返回码描述信息
	SignType   string `json:"signType"`               // 签名方式MD5 或 RSA
	Type       string `json:"type"`                   // 接口类型OrderRefund
	Version    string `json:"version"`                // 版本号2.0.0
	Amount     string `json:"amount"`                 // 退款金额,单位为分
	OrderId    string `json:"orderId"`                // 商户订单号
	Status     string `json:"status"`                 // 退款结果SUCCESS / FAILED
	ServerCert string `json:"serverCert,omitempty"`   // 服务器证书公钥(不参与签名)
	Hmac       string `json:"hmac,omitempty"`         // 签名数据
}

// RefundStatus 退款状态常量
const (
	RefundStatusSuccess = "SUCCESS"
	RefundStatusFailed  = "FAILED"
)

// SignType 签名方式常量
const (
	SignTypeMD5 = "MD5"
	SignTypeRSA = "RSA"
)

// 接口类型常量
const (
	InterfaceTypeOrderRefund = "OrderRefund"
)

// 版本号常量
const (
	Version = "2.0.0"
)

// 返回码常量
const (
	ReturnCodeSuccess    = "000000"
	ReturnCodeSuccessAlt = "MCG00000"
)

// File: hb/errors.go

package hb

import "fmt"

// Error 自定义错误类型
type Error struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Err     error  `json:"-"`
}

func (e *Error) Error() string {
	if e.Err != nil {
		return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
	}
	return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

func (e *Error) Unwrap() error {
	return e.Err
}

// NewError 创建新的错误
func NewError(code, message string, err error) *Error {
	return &Error{
		Code:    code,
		Message: message,
		Err:     err,
	}
}

// 预定义错误
var (
	ErrSignTypeInvalid    = NewError("SIGN_TYPE_INVALID", "不支持的签名方式,仅支持 MD5 或 RSA", nil)
	ErrSignVerifyFailed   = NewError("SIGN_VERIFY_FAILED", "签名验证失败", nil)
	ErrPrivateKeyInvalid  = NewError("PRIVATE_KEY_INVALID", "私钥格式无效", nil)
	ErrPublicKeyInvalid   = NewError("PUBLIC_KEY_INVALID", "公钥格式无效", nil)
	ErrNetwork            = NewError("NETWORK_ERROR", "网络请求失败", nil)
	ErrResponseParse      = NewError("RESPONSE_PARSE_ERROR", "响应解析失败", nil)
	ErrMerchantIdEmpty    = NewError("MERCHANT_ID_EMPTY", "商户编号不能为空", nil)
	ErrSignKeyEmpty       = NewError("SIGN_KEY_EMPTY", "商户密钥不能为空", nil)
	ErrRequestIdEmpty     = NewError("REQUEST_ID_EMPTY", "商户请求号不能为空", nil)
	ErrOrderIdEmpty       = NewError("ORDER_ID_EMPTY", "商户订单号不能为空", nil)
	ErrAmountEmpty        = NewError("AMOUNT_EMPTY", "退款金额不能为空", nil)
)

// 错误码映射
var ErrorCodeMap = map[string]string{
	"C01088": "退货金额域非法",
	"C01081": "产品名称域越界",
	"C01033": "产品名称域为空",
	"C01030": "有效期单位域不合法",
	"C01028": "有效期数量域为0",
	"C01027": "有效期数量域为空",
	"C01025": "订单日期域不合法",
	"C01024": "订单日期域为空",
	"C01019": "币种域取值不合法",
	"C01018": "币种域为空",
	"C01014": "订单金额域为空",
	"D99981": "订单已过期",
	"D23190": "退款日期超过最大有效期",
	"D22407": "退款金额大于可退款金额",
	"D22401": "退货金额大于可退货金额",
	"D22208": "订单状态异常",
	"D22201": "订单不存在",
	"D22224": "账户支付方式不正确",
	"D77040": "商户没有开通此类交易的权限",
	"IPS0008": "签名不符,一般都是由于中文编码引起",
	"IPS0001": "一般都是因为商户编号或者密钥不对引起",
}

// GetErrorDescription 根据错误码获取错误描述
func GetErrorDescription(code string) string {
	if desc, ok := ErrorCodeMap[code]; ok {
		return desc
	}
	return "未知错误码"
}

// File: hb/crypto.go

package hb

import (
	"crypto"
	"crypto/md5"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha1"
	"crypto/x509"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"strings"
)

// buildSignString 按文档指定顺序拼接签名串(请求)
// 参数顺序merchantId, requestId, signType, type, version, orderId, amount
func buildSignString(params map[string]string, orderedKeys []string) string {
	var parts []string
	for _, k := range orderedKeys {
		if v, ok := params[k]; ok {
			parts = append(parts, fmt.Sprintf("%s=%s", k, v))
		}
	}
	return strings.Join(parts, "&")
}

// getRequestSignKeys 获取请求签名参数顺序
func getRequestSignKeys() []string {
	return []string{"merchantId", "requestId", "signType", "type", "version", "orderId", "amount"}
}

// getResponseSignKeys 获取响应签名参数顺序
func getResponseSignKeys() []string {
	return []string{"merchantId", "payNo", "returnCode", "message", "signType", "type", "version", "amount", "orderId", "status"}
}

// SignMD5 MD5签名
// 在待签名数据之后加上商户密钥signKey生成MD5摘要
func SignMD5(signStr, signKey string) string {
	data := signStr + signKey
	hash := md5.Sum([]byte(data))
	return hex.EncodeToString(hash[:])
}

// VerifyMD5 验证MD5签名
func VerifyMD5(signStr, signKey, hmac string) bool {
	return SignMD5(signStr, signKey) == hmac
}

// SignRSA RSA签名配合SHA-1
// 使用商户私钥对签名串进行RSA-SHA1签名
func SignRSA(signStr string, privateKeyPEM string) (string, error) {
	block, _ := pem.Decode([]byte(privateKeyPEM))
	if block == nil {
		return "", fmt.Errorf("failed to decode PEM private key")
	}

	var privateKey *rsa.PrivateKey

	// 尝试PKCS8格式
	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err == nil {
		var ok bool
		privateKey, ok = key.(*rsa.PrivateKey)
		if !ok {
			return "", fmt.Errorf("not a RSA private key")
		}
	} else {
		// 尝试PKCS1格式
		privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
		if err != nil {
			return "", fmt.Errorf("failed to parse private key (PKCS1/PKCS8): %v", err)
		}
	}

	// 计算SHA-1哈希
	hash := sha1.Sum([]byte(signStr))

	// RSA签名
	signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA1, hash[:])
	if err != nil {
		return "", fmt.Errorf("RSA sign failed: %v", err)
	}

	return hex.EncodeToString(signature), nil
}

// VerifyRSA 验证RSA签名
// 使用手机支付公钥验证响应签名
func VerifyRSA(signStr, signatureHex, publicKeyPEM string) error {
	block, _ := pem.Decode([]byte(publicKeyPEM))
	if block == nil {
		return fmt.Errorf("failed to decode PEM public key")
	}

	// 尝试解析公钥
	pub, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		// 尝试解析证书
		cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return fmt.Errorf("failed to parse public key: %v", err)
		}
		pub = cert.PublicKey
	}

	rsaPub, ok := pub.(*rsa.PublicKey)
	if !ok {
		return fmt.Errorf("not a RSA public key")
	}

	// 解码签名
	signature, err := hex.DecodeString(signatureHex)
	if err != nil {
		return fmt.Errorf("failed to decode signature hex: %v", err)
	}

	// 计算SHA-1哈希
	hash := sha1.Sum([]byte(signStr))

	// 验证签名
	err = rsa.VerifyPKCS1v15(rsaPub, crypto.SHA1, hash[:], signature)
	if err != nil {
		return fmt.Errorf("RSA verify failed: %v", err)
	}

	return nil
}

// extractCertFromPEM 从PEM格式的证书中提取公钥PEM
// 有些场景下serverCert返回的是证书需要提取公钥
func extractPublicKeyFromCert(certPEM string) (string, error) {
	block, _ := pem.Decode([]byte(certPEM))
	if block == nil {
		return "", fmt.Errorf("failed to decode PEM certificate")
	}

	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		return "", fmt.Errorf("failed to parse certificate: %v", err)
	}

	pubKeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
	if err != nil {
		return "", fmt.Errorf("failed to marshal public key: %v", err)
	}

	pubKeyPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: pubKeyBytes,
	})

	return string(pubKeyPEM), nil
}

// File: hb/client.go

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
}

// File: hb/example_test.go

package hb

import (
	"context"
	"fmt"
	"log"
	"testing"
)

// ExampleClient_Refund_MD5 使用MD5签名的退款示例
func ExampleClient_Refund_MD5() {
	// 创建客户端使用MD5签名
	client, err := NewClient(
		"888009941110054",
		WithSignKey("your-64-digit-sign-key-here"),
		WithBaseURL("https://uatipos.10086.cn"), // 测试环境
	)
	if err != nil {
		log.Fatalf("创建客户端失败: %v", err)
	}

	// 构建退款请求
	req := &RefundRequest{
		RequestId: "20211105053044",
		SignType:  SignTypeMD5,
		OrderId:   "20211105045315",
		Amount:    "1",
	}

	// 发送退款请求
	resp, err := client.Refund(context.Background(), req)
	if err != nil {
		log.Fatalf("退款请求失败: %v", err)
	}

	fmt.Printf("返回码: %s\n", resp.ReturnCode)
	fmt.Printf("退款结果: %s\n", resp.Status)
	fmt.Printf("流水号: %s\n", resp.PayNo)

	// 判断退款请求是否成功
	if IsRefundRequestSuccess(resp.ReturnCode) {
		fmt.Println("退款请求提交成功")
	}

	// 判断退款最终结果
	if IsRefundSuccess(resp.Status) {
		fmt.Println("退款最终成功")
	}

	// Output:
	// 返回码: 000000
	// 退款结果: SUCCESS
	// 流水号: 205206151912517707
	// 退款请求提交成功
	// 退款最终成功
}

// ExampleClient_Refund_RSA 使用RSA签名的退款示例
func ExampleClient_Refund_RSA() {
	// 商户RSA私钥PEM格式
	privateKey := `-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJ...(你的私钥)
-----END PRIVATE KEY-----`

	// 创建客户端使用RSA签名
	client, err := NewClient(
		"888009941110054",
		WithPrivateKey(privateKey),
		WithBaseURL("https://ipos.10086.cn"), // 生产环境
	)
	if err != nil {
		log.Fatalf("创建客户端失败: %v", err)
	}

	// 构建退款请求
	req := &RefundRequest{
		RequestId: "20211105053044",
		SignType:  SignTypeRSA,
		OrderId:   "20211105045315",
		Amount:    "1",
	}

	// 发送退款请求
	resp, err := client.Refund(context.Background(), req)
	if err != nil {
		log.Fatalf("退款请求失败: %v", err)
	}

	fmt.Printf("返回码: %s\n", resp.ReturnCode)
	fmt.Printf("退款结果: %s\n", resp.Status)

	// Output:
	// 返回码: 000000
	// 退款结果: SUCCESS
}

// TestSignMD5 测试MD5签名
func TestSignMD5(t *testing.T) {
	signStr := "merchantId=888009941110054&requestId=20211105053044&signType=MD5&type=OrderRefund&version=2.0.0&orderId=20211105045315&amount=1"
	signKey := "test-sign-key-64-chars-length-string-for-testing"

	hmac := SignMD5(signStr, signKey)
	if hmac == "" {
		t.Error("MD5签名结果为空")
	}
	t.Logf("MD5签名结果: %s", hmac)

	// 验证签名
	if !VerifyMD5(signStr, signKey, hmac) {
		t.Error("MD5签名验证失败")
	}
}

// TestBuildSignString 测试签名串构建
func TestBuildSignString(t *testing.T) {
	params := map[string]string{
		"merchantId": "888009941110054",
		"requestId":  "20211105053044",
		"signType":   "MD5",
		"type":       "OrderRefund",
		"version":    "2.0.0",
		"orderId":    "20211105045315",
		"amount":     "1",
	}

	keys := getRequestSignKeys()
	signStr := buildSignString(params, keys)

	expected := "merchantId=888009941110054&requestId=20211105053044&signType=MD5&type=OrderRefund&version=2.0.0&orderId=20211105045315&amount=1"
	if signStr != expected {
		t.Errorf("签名串不匹配\n期望: %s\n实际: %s", expected, signStr)
	}
}

// TestParseResponse 测试响应解析
func TestParseResponse(t *testing.T) {
	rawResponse := `merchantId=888009941110054&payNo=205206151912517707&returnCode=000000&message=SUCCESS&signType=RSA&type=OrderRefund&version=2.0.0&amount=1&orderId=20211105045315&status=SUCCESS&serverCert=3082042030820308A003020102020420B82E72300D06092A864886F70D0101050500302F310B300906035504061302434E3120301E06035504030C17434D434120456E74657270726973652043415F32303438301E170D3230303830363031343834395A170D3232303830363031343834395A3076310B300906035504061302434E310F300D06035504080C06E6B996E58D97310F300D06035504070C06E995BFE6B2993145304306035504030C3CE4B8ADE59BBDE7A7BBE58AA8E9809AE4BFA1E99B86E59BA2E6B996E58D97E69C89E99990E585ACE58FB8E794B5E5AD90E59586E58AA1E4B8ADE5BF8330820122300D06092A864886F70D01010105000382010F003082010A028201010092FCBC26F1107D35B966C0C098B1F31F8313103A29DC5C78A8056D1AFAC1E5BAA89F5AEE6D3E737D3554AE815040AD5A5D9BAF5BB5B4B45AA5622335E3B9F48D9C05DF29248DF944544D414513F37DFE7F618D0C62B24E1E90CFD1790791861E20FDEFC4288EA4BF906EE1A6B576D9FDE6108CF5A1ED01D1931F2C72AED5393AE9CB668BBF629B25A239E09C54D45B565FBA040963B011C7B80F7C93D4695F4568F6358D591FBCB1073C2583630EC858B7C21F5B7CBB8CC05431D93F4AC358F78A06D6E40D1C033083F403697C12A0A99FF6363D792F624B66FCA655E7057686BA5093A8F68597DB8F40865081A3EB43C1C52E91D35F83F9FF3BF40C9901D08B0203010001A381FC3081F930370603551D250101FF042D302B06082B0601050507030206082B0601050507030106096086480186F8420401060A2B0601040182370A0303301F0603551D23041830168014972A89BEE6A2AA3FA666133AE36A3FBA5D86697130710603551D1F046A30683066A02FA02D862B687474703A2F2F7777772E636D63612E6E65742F646F776E6C6F61642F63726C2F43524C3134322E63726CA233A431302F3120301E06035504030C17434D434120456E74657270726973652043415F32303438310B300906035504061302434E300B0603551D0F0404030200B1301D0603551D0E04160414F713DA66CF8A1F30440D83455C2AFC6560B040B6300D06092A864886F70D010105050003820101002AD8B85ABEEAE53C6002381120F7D6B23620464197FBBDB093D4BB98AF256E335F55A67E32BFDF165BB1E2DDA997A0727086D26F8427FE6B77F6F00C6168CE715E8D298D5EB47B7227CDB8094CB1047CBDEA3D911903DCAA57131DEE1CD0069BBF8C7940A3E68438CCCA1949FF045F7349AA865F7FAC7170B7D7339DEBC46930FF9B241200440EAFD280F868C13364D13ECEB04EA702D0479862F9590813AB94BE38DC84E366B884174731464111A1AFEB199E1D7D518651BB5F430926286CA2B5619CD54689F5851A231C1605146D4ACD6CCC54DB79CC49F992083D5DEACAFDDC2B8777B13B7D63FF7E35D1098BEFD6B550EC3328ECE0224F5AAD6C2B83FB85&hmac=00AAA308D1AA66B2E534D9177B408B55A0CC14F8B27ACE59F08150E66E51858F18070A0DD629FCBD47BB1A98A632DB3F492213A92F14885043EC68BD8180E58ED742D748B708B281693226A7D3BF326B812BA45E7E7D13B4D52498B3C6C83380827CD741A9EF0D99B1C96588C2BE56C79BDFC3813CA4E627142BA7717FFFB43BE53F9B3F2A6D3112D83A90C3D3859A8456BC892E6E960FE56F36F829FAFA7CAEF19D01D10C0BD2F2538827CFF4C642BBAA6423AED0CB8E0D980D7D0BDEB4B1A04B9DAC8867B724527423038D953603A07018BEC1980AD31EDD3E88977F72795DCFB0B3B674FA91A6443F9F0DB67B485B3F3AD5E48AA26F3641E44B64FC7B0920`

	resp, err := parseResponse(rawResponse)
	if err != nil {
		t.Fatalf("解析响应失败: %v", err)
	}

	if resp.MerchantId != "888009941110054" {
		t.Errorf("MerchantId不匹配: %s", resp.MerchantId)
	}
	if resp.PayNo != "205206151912517707" {
		t.Errorf("PayNo不匹配: %s", resp.PayNo)
	}
	if resp.ReturnCode != "000000" {
		t.Errorf("ReturnCode不匹配: %s", resp.ReturnCode)
	}
	if resp.Status != "SUCCESS" {
		t.Errorf("Status不匹配: %s", resp.Status)
	}
	if resp.SignType != "RSA" {
		t.Errorf("SignType不匹配: %s", resp.SignType)
	}
}

// TestErrorCode 测试错误码映射
func TestErrorCode(t *testing.T) {
	tests := []struct {
		code     string
		expected string
	}{
		{"000000", "未知错误码"},
		{"C01088", "退货金额域非法"},
		{"D22201", "订单不存在"},
		{"IPS0008", "签名不符,一般都是由于中文编码引起"},
		{"UNKNOWN", "未知错误码"},
	}

	for _, tt := range tests {
		desc := GetErrorDescription(tt.code)
		if desc != tt.expected {
			t.Errorf("错误码 %s: 期望 %s, 实际 %s", tt.code, tt.expected, desc)
		}
	}
}

// TestIsRefundSuccess 测试退款成功判断
func TestIsRefundSuccess(t *testing.T) {
	if !IsRefundRequestSuccess("000000") {
		t.Error("000000 应该表示退款请求成功")
	}
	if !IsRefundRequestSuccess("MCG00000") {
		t.Error("MCG00000 应该表示退款请求成功")
	}
	if IsRefundRequestSuccess("C01088") {
		t.Error("C01088 不应该表示退款请求成功")
	}

	if !IsRefundSuccess("SUCCESS") {
		t.Error("SUCCESS 应该表示退款最终成功")
	}
	if IsRefundSuccess("FAILED") {
		t.Error("FAILED 不应该表示退款最终成功")
	}
}