diff --git a/hb/valid.md b/hb/valid.md new file mode 100644 index 0000000..68054c6 --- /dev/null +++ b/hb/valid.md @@ -0,0 +1,816 @@ +// File: hb/go.mod +```go +module hb + +go 1.21 +``` + +// File: hb/types.go +```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 +```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 +```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 +```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 +```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 不应该表示退款最终成功") + } +} +``` \ No newline at end of file