接入顺成支付并优化配置列表

This commit is contained in:
yml
2026-06-18 21:33:15 +08:00
parent 40ae14d450
commit 6d385478a6
13 changed files with 1763 additions and 40 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,266 @@
package shuncheng
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"
)
func TestSignUsesDocumentedBase64LowerMD5(t *testing.T) {
params := map[string]string{
"service": "get_tdcode",
"merchant_id": "1234567890",
"third_order_id": "NO1",
"amount": "100",
"nonce_str": "abc",
"req_serial_no": "req001",
"empty": "",
"sign": "ignored",
}
got := SignStrings(params, "secret")
want := "ZTM5YTQ1NDk4YmRhODJlYTYyOWQzMGI2N2M5YzFjYjQ="
if got != want {
t.Fatalf("SignStrings() = %s, want %s", got, want)
}
baseString := SignBaseString(stringMapToAny(params))
wantBaseString := "amount=100&merchant_id=1234567890&nonce_str=abc&req_serial_no=req001&service=get_tdcode&third_order_id=NO1"
if baseString != wantBaseString {
t.Fatalf("SignBaseString() = %s, want %s", baseString, wantBaseString)
}
}
func TestSignSortsNestedObjectLikeDocs(t *testing.T) {
params := map[string]any{
"reqSerialNo": "20260318110810747",
"data": map[string]any{
"merchantId": "M1",
"applyAmount": 10000,
"reqId": "R1",
"ignored": nil,
},
}
baseString := SignBaseString(params)
wantBaseString := `data={"applyAmount":10000,"merchantId":"M1","reqId":"R1"}&reqSerialNo=20260318110810747`
if baseString != wantBaseString {
t.Fatalf("SignBaseString() = %s, want %s", baseString, wantBaseString)
}
got := Sign(params, "secret")
want := "N2VlOWY3OWJlZmU4YThiNjEwN2Y1NWYxNzI0ZjY0NTY="
if got != want {
t.Fatalf("Sign() = %s, want %s", got, want)
}
}
func TestParsePayloadNormalizesXMLAliases(t *testing.T) {
params, err := ParsePayload([]byte(`<schc>
<resp_code><![CDATA[200]]></resp_code>
<result_code><![CDATA[0]]></result_code>
<third_order_id><![CDATA[PAY1]]></third_order_id>
<schc_order_id><![CDATA[SC1]]></schc_order_id>
<schc_refund_id><![CDATA[RF1]]></schc_refund_id>
<refund_amount><![CDATA[100]]></refund_amount>
<status><![CDATA[11]]></status>
</schc>`))
if err != nil {
t.Fatalf("ParsePayload() error = %v", err)
}
if params["provider_order_id"] != "SC1" {
t.Fatalf("provider_order_id = %q, want SC1", params["provider_order_id"])
}
if params["provider_refund_id"] != "RF1" {
t.Fatalf("provider_refund_id = %q, want RF1", params["provider_refund_id"])
}
}
func TestParsePayloadExtractsEscapedXMLFromHTML(t *testing.T) {
params, err := ParsePayload([]byte(`<html><body>&lt;schc&gt;&lt;resp_code&gt;200&lt;/resp_code&gt;&lt;result_code&gt;0&lt;/result_code&gt;&lt;third_order_id&gt;PAY1&lt;/third_order_id&gt;&lt;schc_order_id&gt;SC1&lt;/schc_order_id&gt;&lt;jspay_url&gt;https://pay.example/sc1&lt;/jspay_url&gt;&lt;/schc&gt;</body></html>`))
if err != nil {
t.Fatalf("ParsePayload() error = %v", err)
}
if params["provider_order_id"] != "SC1" || params["jspay_url"] != "https://pay.example/sc1" {
t.Fatalf("ParsePayload() = %#v, want extracted shuncheng xml", params)
}
}
func TestParsePayloadAcceptsEscapedURLQueryInHTMLXML(t *testing.T) {
params, err := ParsePayload([]byte(`<html><body>&lt;schc&gt;&lt;resp_code&gt;200&lt;/resp_code&gt;&lt;result_code&gt;0&lt;/result_code&gt;&lt;third_order_id&gt;PAY1&lt;/third_order_id&gt;&lt;schc_order_id&gt;SC1&lt;/schc_order_id&gt;&lt;jspay_url&gt;https://pay.example/sc1?mid=1&amp;pu=2&lt;/jspay_url&gt;&lt;/schc&gt;</body></html>`))
if err != nil {
t.Fatalf("ParsePayload() error = %v", err)
}
if params["jspay_url"] != "https://pay.example/sc1?mid=1&pu=2" {
t.Fatalf("jspay_url = %q, want query decoded", params["jspay_url"])
}
}
func TestParsePayloadAcceptsJSONStringXML(t *testing.T) {
xmlText := `<?xml version="1.0" encoding="utf-16"?><schc xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><resp_code>200</resp_code><merchant_id>5169118450</merchant_id><sign>00EFB11258D694634F727A8FCCF92290</sign><cost_time>4</cost_time><sign_type>MD5</sign_type><third_order_id>PAY1</third_order_id><jspay_url>https://pay.example/sc1?mid=1&amp;pu=2</jspay_url><result_code>0</result_code><schc_order_id>SC1</schc_order_id></schc>`
body, err := json.Marshal(xmlText)
if err != nil {
t.Fatalf("json marshal xml text: %v", err)
}
params, err := ParsePayload(body)
if err != nil {
t.Fatalf("ParsePayload() error = %v", err)
}
if params["provider_order_id"] != "SC1" || params["jspay_url"] != "https://pay.example/sc1?mid=1&pu=2" {
t.Fatalf("ParsePayload() = %#v, want xml string decoded", params)
}
}
func TestParsePayloadTrimsBOMBeforeXML(t *testing.T) {
params, err := ParsePayload([]byte("\xef\xbb\xbf<schc><resp_code>200</resp_code><result_code>0</result_code><third_order_id>PAY1</third_order_id><schc_order_id>SC1</schc_order_id></schc>"))
if err != nil {
t.Fatalf("ParsePayload() error = %v", err)
}
if params["provider_order_id"] != "SC1" {
t.Fatalf("provider_order_id = %q, want SC1", params["provider_order_id"])
}
}
func TestParsePayloadRejectsPlainHTML(t *testing.T) {
if _, err := ParsePayload([]byte(`<html><body>invalid semicolon; separator</body></html>`)); err == nil {
t.Fatal("ParsePayload() error = nil, want unsupported html error")
}
}
func TestVerifyNotifyAcceptsDocumentedAndLegacySignatures(t *testing.T) {
client := NewClient(Config{NotifyKey: "notify-secret"})
params := map[string]string{
"merchant_id": "1234567890",
"third_order_id": "NO1",
"schc_order_id": "SC1",
"amount": "100",
"status": "2",
"sign_type": "MD5",
}
params["sign"] = SignStrings(params, "notify-secret")
if result := client.VerifyNotifyDetail(params); !result.OK {
t.Fatalf("VerifyNotifyDetail().OK = false, got=%s expected=%v", result.Got, result.Expected)
}
params["sign"] = SignStringsUpperMD5(params, "notify-secret")
if result := client.VerifyNotifyDetail(params); !result.OK {
t.Fatalf("VerifyNotifyDetail() legacy upper md5 OK = false, got=%s expected=%v", result.Got, result.Expected)
}
params["amount"] = "101"
if client.VerifyNotify(params) {
t.Fatal("VerifyNotify() = true after amount changed, want false")
}
}
func TestCreatePaymentFetchesTokenOnceAndPostsSignedJSON(t *testing.T) {
resetTokenCacheForTest()
var tokenCount int
var createCount int
var captured map[string]string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case endpointToken:
tokenCount++
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"respCode":"200","data":"token-1"}`))
case endpointSimplePay:
createCount++
if r.Header.Get("Authorization") != "Bearer token-1" {
t.Fatalf("Authorization = %q, want Bearer token-1", r.Header.Get("Authorization"))
}
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
t.Fatalf("decode create payment request: %v", err)
}
if captured["sign"] != SignStrings(captured, "secret-key") {
t.Fatalf("sign = %q, want recalculated %q", captured["sign"], SignStrings(captured, "secret-key"))
}
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(`<schc><resp_code>200</resp_code><result_code>0</result_code><third_order_id>PAY1</third_order_id><schc_order_id>SC1</schc_order_id><jspay_url>https://pay.example/sc1</jspay_url></schc>`))
default:
t.Fatalf("unexpected path %s", r.URL.Path)
}
}))
defer server.Close()
client := NewClient(Config{
GatewayURL: server.URL,
MerchantID: "merchant-1",
SecretID: "secret-id",
SecretKey: "secret-key",
NotifyURL: "https://example.com/notify",
})
for i := 0; i < 2; i++ {
resp, err := client.CreatePayment(context.Background(), CreatePaymentRequest{
ThirdOrderID: "PAY1",
AmountCent: 100,
Body: "租号订单",
})
if err != nil {
t.Fatalf("CreatePayment() error = %v", err)
}
if !resp.OK || resp.ProviderOrderID != "SC1" || resp.JSPayURL == "" {
t.Fatalf("CreatePayment() response = %+v", resp)
}
}
if tokenCount != 1 {
t.Fatalf("tokenCount = %d, want 1", tokenCount)
}
if createCount != 2 {
t.Fatalf("createCount = %d, want 2", createCount)
}
if captured["notify_url"] != "https://example.com/notify" {
t.Fatalf("notify_url = %q, want configured notify url", captured["notify_url"])
}
}
func TestUnauthorizedRefreshesTokenOnce(t *testing.T) {
resetTokenCacheForTest()
var tokenCount int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case endpointToken:
tokenCount++
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"respCode":"200","data":"token-` + strconv.Itoa(tokenCount) + `"}`))
case endpointPaymentQuery:
if r.Header.Get("Authorization") == "Bearer token-1" {
w.WriteHeader(http.StatusUnauthorized)
return
}
if r.Header.Get("Authorization") != "Bearer token-2" {
t.Fatalf("Authorization = %q, want Bearer token-2", r.Header.Get("Authorization"))
}
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(`<schc><resp_code>200</resp_code><result_code>0</result_code><third_order_id>PAY1</third_order_id><schc_order_id>SC1</schc_order_id><status>2</status></schc>`))
default:
t.Fatalf("unexpected path %s", r.URL.Path)
}
}))
defer server.Close()
client := NewClient(Config{
GatewayURL: server.URL,
MerchantID: "merchant-1",
SecretID: "secret-id",
SecretKey: "secret-key",
})
resp, err := client.QueryPayment(context.Background(), "PAY1", "")
if err != nil {
t.Fatalf("QueryPayment() error = %v", err)
}
if !resp.OK || resp.Status != "paid" || resp.ProviderOrderID != "SC1" {
t.Fatalf("QueryPayment() response = %+v", resp)
}
if tokenCount != 2 {
t.Fatalf("tokenCount = %d, want 2", tokenCount)
}
}
func resetTokenCacheForTest() {
tokenCache.Lock()
tokenCache.items = map[string]tokenEntry{}
tokenCache.Unlock()
}
@@ -5,9 +5,11 @@ import (
"fmt"
"strconv"
"strings"
"time"
"hfb_sys/backend/internal/integrations/payment/lakala"
"hfb_sys/backend/internal/integrations/payment/leshua"
"hfb_sys/backend/internal/integrations/payment/shuncheng"
"hfb_sys/backend/internal/modules/paymentconfig"
)
@@ -91,6 +93,7 @@ type channelQueryRefundResponse struct {
Status string
RefundAmount string
RefundTime string
RawRequest map[string]string
Raw map[string]string
}
@@ -168,7 +171,12 @@ func (c leshuaChannel) CreateRefund(ctx context.Context, req channelCreateRefund
Attach: req.Attach,
})
if err != nil {
return nil, err
if rawReq == nil {
return nil, err
}
return &channelCreateRefundResponse{
RawRequest: rawReq,
}, err
}
return &channelCreateRefundResponse{
OK: resp.RespCode == "0" && resp.ResultCode == "0",
@@ -285,7 +293,12 @@ func (c lakalaChannel) CreateRefund(ctx context.Context, req channelCreateRefund
ClientIP: req.ClientIP,
})
if err != nil {
return nil, err
if resp == nil {
return nil, err
}
return &channelCreateRefundResponse{
RawRequest: resp.RawRequest,
}, err
}
return &channelCreateRefundResponse{
OK: resp.OK,
@@ -305,6 +318,129 @@ func (c lakalaChannel) QueryRefund(ctx context.Context, req channelQueryRefundRe
MerchantRefundID: req.MerchantRefundID,
ProviderRefundID: req.ProviderRefundID,
})
if err != nil {
if resp == nil {
return nil, err
}
return &channelQueryRefundResponse{
RawRequest: resp.RawRequest,
}, err
}
return &channelQueryRefundResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderRefundID: resp.ProviderRefundID,
Status: resp.Status,
RefundAmount: resp.RefundAmount,
RefundTime: resp.RefundTime,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
}, nil
}
func (c lakalaChannel) VerifyNotify(params map[string]string, rawPayload string, contentType string, authorization string) (channelVerifyNotifyResult, error) {
verify := c.client.VerifyNotifyDetail(rawPayload, authorization)
result := channelVerifyNotifyResult{
OK: verify.OK,
MatchedKey: verify.MatchedKey,
Got: verify.Got,
Expected: verify.Expected,
BaseString: verify.BaseString,
ParamKeys: verify.ParamKeys,
}
if !result.OK {
return result, ErrPaymentVerifyFailed
}
return result, nil
}
type shunchengChannel struct {
client *shuncheng.Client
}
func newShunchengChannel(cfg shuncheng.Config) channelClient {
return shunchengChannel{client: shuncheng.NewClient(cfg)}
}
func (c shunchengChannel) CreatePayment(ctx context.Context, req channelCreatePaymentRequest) (*channelCreatePaymentResponse, error) {
resp, err := c.client.CreatePayment(ctx, shuncheng.CreatePaymentRequest{
ThirdOrderID: req.ThirdOrderID,
AmountCent: req.AmountCent,
PayWay: req.PayWay,
JSPayFlag: req.JSPayFlag,
NotifyURL: req.NotifyURL,
JumpURL: req.JumpURL,
ClientIP: req.ClientIP,
Body: req.Body,
Attach: req.Attach,
})
if err != nil {
return nil, err
}
return &channelCreatePaymentResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderOrderID: resp.ProviderOrderID,
PayWay: resp.PayWay,
Status: resp.Status,
PayTime: resp.PayTime,
TDCode: resp.TDCode,
JSPayURL: resp.JSPayURL,
JSPayInfo: resp.JSPayInfo,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
}, nil
}
func (c shunchengChannel) QueryPayment(ctx context.Context, thirdOrderID string, providerOrderID string) (*channelQueryPaymentResponse, error) {
resp, err := c.client.QueryPayment(ctx, thirdOrderID, providerOrderID)
if err != nil {
return nil, err
}
return &channelQueryPaymentResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderOrderID: resp.ProviderOrderID,
Status: resp.Status,
Amount: resp.Amount,
PayWay: resp.PayWay,
PayTime: resp.PayTime,
Raw: resp.Raw,
}, nil
}
func (c shunchengChannel) CreateRefund(ctx context.Context, req channelCreateRefundRequest) (*channelCreateRefundResponse, error) {
resp, err := c.client.CreateRefund(ctx, shuncheng.CreateRefundRequest{
ThirdOrderID: req.ThirdOrderID,
ProviderOrderID: req.ProviderOrderID,
MerchantRefundID: req.MerchantRefundID,
RefundAmountCent: req.RefundAmountCent,
NotifyURL: req.NotifyURL,
Attach: req.Attach,
RefundReason: req.Remark,
ClientIP: req.ClientIP,
})
if err != nil {
return nil, err
}
return &channelCreateRefundResponse{
OK: resp.OK,
ErrorMessage: resp.ErrorMessage,
ProviderRefundID: resp.ProviderRefundID,
Status: resp.Status,
RefundAmount: resp.RefundAmount,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
}, nil
}
func (c shunchengChannel) QueryRefund(ctx context.Context, req channelQueryRefundRequest) (*channelQueryRefundResponse, error) {
resp, err := c.client.QueryRefund(ctx, shuncheng.QueryRefundRequest{
ThirdOrderID: req.ThirdOrderID,
ProviderOrderID: req.ProviderOrderID,
MerchantRefundID: req.MerchantRefundID,
ProviderRefundID: req.ProviderRefundID,
})
if err != nil {
return nil, err
}
@@ -319,8 +455,8 @@ func (c lakalaChannel) QueryRefund(ctx context.Context, req channelQueryRefundRe
}, nil
}
func (c lakalaChannel) VerifyNotify(params map[string]string, rawPayload string, contentType string, authorization string) (channelVerifyNotifyResult, error) {
verify := c.client.VerifyNotifyDetail(rawPayload, authorization)
func (c shunchengChannel) VerifyNotify(params map[string]string, rawPayload string, contentType string, authorization string) (channelVerifyNotifyResult, error) {
verify := c.client.VerifyNotifyDetail(params)
result := channelVerifyNotifyResult{
OK: verify.OK,
MatchedKey: verify.MatchedKey,
@@ -366,6 +502,22 @@ func buildChannelClient(dto *paymentconfig.ConfigDTO) (channelClient, error) {
PayMode: extraString(dto.ExtraConfig, "pay_mode"),
OrderExpireMinutes: extraInt(dto.ExtraConfig, "order_expire_minutes"),
})), nil
case "shuncheng":
return withChannelBreaker("shuncheng", newShunchengChannel(shuncheng.Config{
GatewayURL: dto.GatewayURL,
MerchantID: dto.MerchantID,
SecretID: extraString(dto.ExtraConfig, "secret_id"),
SecretKey: dto.SignKey,
NotifyKey: dto.NotifyKey,
NotifyURL: dto.NotifyURL,
JumpURL: dto.JumpURL,
PayWay: firstNonEmpty(dto.PayWay, "ZFBZF"),
JSPayFlag: firstNonEmpty(dto.JSPayFlag, "2"),
SignType: firstNonEmpty(dto.SignType, "MD5"),
ShopNo: extraString(dto.ExtraConfig, "shop_no"),
RefundType: extraString(dto.ExtraConfig, "refund_type"),
TokenMargin: time.Duration(extraInt(dto.ExtraConfig, "token_refresh_margin_seconds")) * time.Second,
})), nil
case "mock":
return nil, nil
default:
@@ -398,7 +550,7 @@ func normalizeLeshuaRefundStatus(status string) string {
}
func normalizeNotifyPaymentStatus(provider string, status string) string {
if provider == "leshua" {
if provider == "leshua" || provider == "shuncheng" {
return normalizeLeshuaPaymentStatus(status)
}
switch status {
@@ -410,7 +562,7 @@ func normalizeNotifyPaymentStatus(provider string, status string) string {
}
func normalizeNotifyRefundStatus(provider string, status string) string {
if provider == "leshua" {
if provider == "leshua" || provider == "shuncheng" {
return normalizeLeshuaRefundStatus(status)
}
switch status {
@@ -63,7 +63,7 @@ func (r *Repository) confirmPaid(ctx context.Context, payment *model.PaymentOrde
}
updates := map[string]any{
"status": "paid",
"provider_order_id": firstNonEmpty(raw["provider_order_id"], raw["leshua_order_id"], raw["pay_order_no"], raw["trade_no"], latest.ProviderOrderID),
"provider_order_id": firstNonEmpty(raw["provider_order_id"], raw["schc_order_id"], raw["leshua_order_id"], raw["pay_order_no"], raw["trade_no"], latest.ProviderOrderID),
"raw_response": jsonMap(withRawSource(raw, source)),
"paid_at": paidAt,
}
@@ -9,6 +9,7 @@ import (
"hfb_sys/backend/internal/integrations/payment/lakala"
"hfb_sys/backend/internal/integrations/payment/leshua"
"hfb_sys/backend/internal/integrations/payment/shuncheng"
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/internal/modules/order"
"hfb_sys/backend/pkg/response"
@@ -154,6 +155,40 @@ func (h *Handler) LakalaNotify(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "执行成功"})
}
func (h *Handler) ShunchengNotify(c *gin.Context) {
body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20))
if err != nil {
c.String(http.StatusOK, "FAIL")
return
}
params, err := shuncheng.ParsePayload(body)
if err != nil {
c.String(http.StatusOK, "FAIL")
return
}
rawPayload := string(body)
contentType := c.GetHeader("Content-Type")
authorization := c.GetHeader("Authorization")
log.Printf(
"[payment] shuncheng notify received third_order_id=%s schc_order_id=%s schc_refund_id=%s status=%s amount=%s content_type=%s raw_payload=%s",
params["third_order_id"],
params["schc_order_id"],
params["schc_refund_id"],
params["status"],
params["amount"],
contentType,
rawPayload,
)
result, err := h.service.HandleNotify(c.Request.Context(), "shuncheng", params, rawPayload, contentType, authorization)
if err != nil || result == nil || !result.OK {
log.Printf("[payment] shuncheng notify failed third_order_id=%s err=%v", params["third_order_id"], err)
c.String(http.StatusOK, "FAIL")
return
}
log.Printf("[payment] shuncheng notify processed third_order_id=%s status=%s", params["third_order_id"], params["status"])
c.String(http.StatusOK, result.Message)
}
func currentUserID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextUserID)
if !ok {
@@ -106,14 +106,13 @@ func (r *Repository) runtimeConfigForPayment(ctx context.Context, payment *model
return nil, err
}
}
if provider != "leshua" {
if provider == "lakala" && r.configRepo != nil {
dto, err := r.configRepo.FindDefaultByProviderPayWay(ctx, provider, payWay, true)
if err != nil {
return nil, err
}
return runtimeConfigFromDTO(dto), nil
}
if provider == "mock" {
return &runtimePaymentConfig{
Provider: provider,
MerchantID: merchantID,
}, nil
}
if provider != "leshua" && provider != "lakala" && provider != "shuncheng" {
return &runtimePaymentConfig{
Provider: provider,
MerchantID: merchantID,
@@ -24,6 +24,7 @@ var (
ErrAppIDRequired = errors.New("extra_config.app_id is required for lakala provider")
ErrSerialNoRequired = errors.New("extra_config.serial_no is required for lakala provider")
ErrTermNoRequired = errors.New("extra_config.term_no is required for lakala provider")
ErrSecretIDRequired = errors.New("extra_config.secret_id is required for shuncheng provider")
)
// DTO 数据传输对象
@@ -57,7 +58,7 @@ type ConfigDTO struct {
// CreateRequest 创建支付配置请求
type CreateRequest struct {
Name string `json:"name" binding:"required"`
Provider string `json:"provider" binding:"required,oneof=leshua lakala mock"`
Provider string `json:"provider" binding:"required,oneof=leshua lakala shuncheng mock"`
MerchantID string `json:"merchant_id" binding:"required"`
GatewayURL string `json:"gateway_url"`
SignKey string `json:"sign_key"`
@@ -48,6 +48,28 @@ func TestImportCreateRequestPreservesTestingConfig(t *testing.T) {
}
}
func TestValidateCreateRequestRequiresShunchengSecretID(t *testing.T) {
repo := &Repository{}
req := CreateRequest{
Name: "顺成生产商户",
Provider: "shuncheng",
MerchantID: "0000000038",
GatewayURL: "http://jyzxapi.ydxnhb.com",
SignKey: "secret-key",
NotifyKey: "notify-key",
NotifyURL: "https://example.com/api/payments/shuncheng/notify",
PayWay: "ZFBZF",
}
if err := repo.validateCreateRequest(req); err != ErrSecretIDRequired {
t.Fatalf("validateCreateRequest() error = %v, want ErrSecretIDRequired", err)
}
req.ExtraConfig = map[string]any{"secret_id": "SEC-1"}
if err := repo.validateCreateRequest(req); err != nil {
t.Fatalf("validateCreateRequest() error = %v, want nil", err)
}
}
// TestListUsesSuccessfulPaymentStats 验证配置页只统计成功支付,不把打开二维码或取消订单算作成交。
func TestListUsesSuccessfulPaymentStats(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
@@ -62,12 +62,29 @@ func (r *Repository) validateCreateRequest(req CreateRequest) error {
return ErrTermNoRequired
}
}
if req.Provider == "shuncheng" {
if req.GatewayURL == "" {
return ErrGatewayURLRequired
}
if req.SignKey == "" {
return ErrSignKeyRequired
}
if req.NotifyKey == "" {
return ErrNotifyKeyRequired
}
if req.NotifyURL == "" {
return ErrNotifyURLRequired
}
if extraString(req.ExtraConfig, "secret_id") == "" {
return ErrSecretIDRequired
}
}
return nil
}
func isValidProvider(value string) bool {
return value == "leshua" || value == "lakala" || value == "mock"
return value == "leshua" || value == "lakala" || value == "shuncheng" || value == "mock"
}
func isValidSignType(value string) bool {
+1
View File
@@ -344,6 +344,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
api.GET("/public/files/object", fileHandler.PublicObject)
api.POST("/payments/leshua/notify", paymentHandler.LeshuaNotify)
api.POST("/payments/lakala/notify", paymentHandler.LakalaNotify)
api.POST("/payments/shuncheng/notify", paymentHandler.ShunchengNotify)
openRoutes := api.Group("/open")
{