优化支付退款钱包链路

This commit is contained in:
yml
2026-06-03 21:47:04 +08:00
parent 04193ae687
commit c80d3f960e
26 changed files with 1342 additions and 504 deletions
@@ -72,6 +72,56 @@ type QueryPaymentResponse struct {
Raw map[string]string
}
type CreateRefundRequest struct {
ThirdOrderID string // 原支付商户订单号
LeshuaOrderID string // 原支付乐刷订单号(优先使用)
MerchantRefundID string // 商户退款单号(唯一)
RefundAmountCent int64 // 退款金额(分)
NotifyURL string
Attach string
}
type CreateRefundResponse struct {
RespCode string
ResultCode string
ErrorCode string
ErrorMessage string
MerchantID string
ThirdOrderID string
LeshuaOrderID string
MerchantRefundID string
LeshuaRefundID string
RefundAmount string
TotalAmount string
OrderBalance string
Status string
Raw map[string]string
}
type QueryRefundRequest struct {
ThirdOrderID string
LeshuaOrderID string
MerchantRefundID string
LeshuaRefundID string
}
type QueryRefundResponse struct {
RespCode string
ResultCode string
ErrorCode string
ErrorMessage string
MerchantID string
ThirdOrderID string
LeshuaOrderID string
MerchantRefundID string
LeshuaRefundID string
Status string
RefundAmount string
TotalAmount string
RefundTime string
Raw map[string]string
}
type VerifyNotifyResult struct {
OK bool
MatchedKey string
@@ -179,6 +229,100 @@ func (c *Client) QueryPayment(ctx context.Context, thirdOrderID, providerOrderID
}, nil
}
func (c *Client) CreateRefund(ctx context.Context, req CreateRefundRequest) (*CreateRefundResponse, map[string]string, error) {
if err := c.validate(); err != nil {
return nil, nil, err
}
params := map[string]string{
"service": "unified_refund",
"merchant_id": c.cfg.MerchantID,
"merchant_refund_id": req.MerchantRefundID,
"refund_amount": fmt.Sprintf("%d", req.RefundAmountCent),
"nonce_str": Nonce(32),
}
if req.LeshuaOrderID != "" {
params["leshua_order_id"] = req.LeshuaOrderID
} else if req.ThirdOrderID != "" {
params["third_order_id"] = req.ThirdOrderID
}
if req.Attach != "" {
params["attach"] = sanitizeText(req.Attach, 64)
}
if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
params["sign_type"] = c.cfg.SignType
}
if req.NotifyURL != "" {
params["notify_url"] = req.NotifyURL
}
params["sign"] = Sign(params, c.cfg.SignKey, SignOptions{})
raw, err := c.post(ctx, params)
if err != nil {
return nil, params, err
}
resp := &CreateRefundResponse{
RespCode: raw["resp_code"],
ResultCode: raw["result_code"],
ErrorCode: raw["error_code"],
ErrorMessage: firstNonEmpty(raw["error_msg"], raw["resp_msg"]),
MerchantID: raw["merchant_id"],
ThirdOrderID: raw["third_order_id"],
LeshuaOrderID: raw["leshua_order_id"],
MerchantRefundID: raw["merchant_refund_id"],
LeshuaRefundID: raw["leshua_refund_id"],
RefundAmount: raw["refund_amount"],
TotalAmount: raw["total_amount"],
OrderBalance: raw["order_balance"],
Status: raw["status"],
Raw: raw,
}
return resp, params, nil
}
func (c *Client) QueryRefund(ctx context.Context, req QueryRefundRequest) (*QueryRefundResponse, error) {
if err := c.validate(); err != nil {
return nil, err
}
params := map[string]string{
"service": "unified_query_refund",
"merchant_id": c.cfg.MerchantID,
"nonce_str": Nonce(32),
}
if req.LeshuaOrderID != "" {
params["leshua_order_id"] = req.LeshuaOrderID
} else if req.ThirdOrderID != "" {
params["third_order_id"] = req.ThirdOrderID
}
if req.LeshuaRefundID != "" {
params["leshua_refund_id"] = req.LeshuaRefundID
} else if req.MerchantRefundID != "" {
params["merchant_refund_id"] = req.MerchantRefundID
}
if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
params["sign_type"] = c.cfg.SignType
}
params["sign"] = Sign(params, c.cfg.SignKey, SignOptions{})
raw, err := c.post(ctx, params)
if err != nil {
return nil, err
}
return &QueryRefundResponse{
RespCode: raw["resp_code"],
ResultCode: raw["result_code"],
ErrorCode: raw["error_code"],
ErrorMessage: firstNonEmpty(raw["error_msg"], raw["resp_msg"]),
MerchantID: raw["merchant_id"],
ThirdOrderID: raw["third_order_id"],
LeshuaOrderID: raw["leshua_order_id"],
MerchantRefundID: raw["merchant_refund_id"],
LeshuaRefundID: raw["leshua_refund_id"],
Status: raw["status"],
RefundAmount: raw["refund_amount"],
TotalAmount: raw["total_amount"],
RefundTime: raw["refund_time"],
Raw: raw,
}, nil
}
func (c *Client) VerifyNotify(params map[string]string) bool {
return c.VerifyNotifyDetail(params).OK
}
@@ -159,3 +159,60 @@ func TestParsePayloadSupportsFormAndXML(t *testing.T) {
t.Fatalf("ParsePayload(xml).coupon = %q, exists=%v; want empty value", value, ok)
}
}
func TestSignRefundUsesSameAlgorithm(t *testing.T) {
params := map[string]string{
"service": "unified_refund",
"merchant_id": "1234567890",
"merchant_refund_id": "REF001",
"refund_amount": "100",
"leshua_order_id": "LS1",
"nonce_str": "abc",
"sign": "ignored",
}
got := Sign(params, "secret", SignOptions{})
baseString := SignBaseString(params, SignOptions{})
wantBaseString := "leshua_order_id=LS1&merchant_id=1234567890&merchant_refund_id=REF001&nonce_str=abc&refund_amount=100&service=unified_refund"
if baseString != wantBaseString {
t.Fatalf("SignBaseString() = %s, want %s", baseString, wantBaseString)
}
want := Sign(map[string]string{
"service": "unified_refund",
"merchant_id": "1234567890",
"merchant_refund_id": "REF001",
"refund_amount": "100",
"leshua_order_id": "LS1",
"nonce_str": "abc",
}, "secret", SignOptions{})
if got != want {
t.Fatalf("Sign() = %s, want %s", got, want)
}
}
func TestVerifyRefundNotify(t *testing.T) {
client := NewClient(config.LeshuaPaymentConfig{NotifyKey: "notify-secret"})
params := map[string]string{
"merchant_id": "1234567890",
"third_order_id": "NO1",
"leshua_order_id": "LS1",
"merchant_refund_id": "REF001",
"leshua_refund_id": "LREF001",
"refund_amount": "100",
"total_amount": "200",
"status": "11",
"attach": "",
}
params["sign"] = Sign(params, "notify-secret", SignOptions{
IncludeEmpty: true,
ExcludeKeys: []string{"error_code", "leshua", "sign"},
})
if !client.VerifyNotify(params) {
t.Fatal("VerifyNotify() = false, want true for refund notify")
}
params["status"] = "12"
if client.VerifyNotify(params) {
t.Fatal("VerifyNotify() = true after status changed without re-sign, want false")
}
}