接入顺成支付并优化配置列表
This commit is contained in:
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><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></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><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?mid=1&pu=2</jspay_url></schc></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&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 {
|
||||
|
||||
@@ -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")
|
||||
{
|
||||
|
||||
@@ -34,6 +34,11 @@ const defaultGateways: Record<string, string> = {
|
||||
mock: '',
|
||||
}
|
||||
|
||||
const shunchengGateways: Record<string, string> = {
|
||||
production: 'http://jyzxapi.ydxnhb.com',
|
||||
sandbox: 'http://jyzxapi.ydxnhb.com',
|
||||
}
|
||||
|
||||
const lakalaGateways: Record<string, string> = {
|
||||
production: 'https://s2.lakala.com',
|
||||
sandbox: 'https://test.wsmsd.cn/sit',
|
||||
@@ -42,12 +47,14 @@ const lakalaGateways: Record<string, string> = {
|
||||
const defaultNotifyURLs: Record<string, string> = {
|
||||
leshua: 'https://hao858.com/api/payments/leshua/notify',
|
||||
lakala: 'https://hao858.com/api/payments/lakala/notify',
|
||||
shuncheng: 'https://hao858.com/api/payments/shuncheng/notify',
|
||||
mock: '',
|
||||
}
|
||||
|
||||
const defaultJumpURLs: Record<string, string> = {
|
||||
leshua: 'https://hao858.com/orders',
|
||||
lakala: 'https://hao858.com/orders',
|
||||
shuncheng: 'https://hao858.com/orders',
|
||||
mock: '',
|
||||
}
|
||||
|
||||
@@ -66,6 +73,9 @@ function defaultGateway(provider: string, environment = defaultEnvironment(provi
|
||||
if (provider === 'lakala') {
|
||||
return lakalaGateways[environment] || lakalaGateways.production
|
||||
}
|
||||
if (provider === 'shuncheng') {
|
||||
return shunchengGateways[environment] || shunchengGateways.production
|
||||
}
|
||||
return defaultGateways[provider] || ''
|
||||
}
|
||||
|
||||
@@ -74,6 +84,11 @@ function isDefaultLakalaGateway(value?: string) {
|
||||
return !value || Object.values(lakalaGateways).includes(value)
|
||||
}
|
||||
|
||||
// isDefaultShunchengGateway 判断当前网关是否仍是顺成系统默认值。
|
||||
function isDefaultShunchengGateway(value?: string) {
|
||||
return !value || Object.values(shunchengGateways).includes(value)
|
||||
}
|
||||
|
||||
function defaultExtraConfig(provider: string) {
|
||||
if (provider === 'lakala') {
|
||||
return {
|
||||
@@ -85,6 +100,14 @@ function defaultExtraConfig(provider: string) {
|
||||
order_expire_minutes: 15,
|
||||
}
|
||||
}
|
||||
if (provider === 'shuncheng') {
|
||||
return {
|
||||
secret_id: '',
|
||||
shop_no: '',
|
||||
refund_type: '',
|
||||
token_refresh_margin_seconds: 300,
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -125,8 +148,13 @@ const dialogTitle = computed(() => {
|
||||
|
||||
const isReadonly = computed(() => props.mode === 'view')
|
||||
const isLakala = computed(() => formData.value.provider === 'lakala')
|
||||
const isShuncheng = computed(() => formData.value.provider === 'shuncheng')
|
||||
const isMock = computed(() => formData.value.provider === 'mock')
|
||||
const signKeyLabel = computed(() => (isLakala.value ? '商户私钥' : '签名密钥'))
|
||||
const signKeyLabel = computed(() => {
|
||||
if (isLakala.value) return '商户私钥'
|
||||
if (isShuncheng.value) return 'SecretKey'
|
||||
return '签名密钥'
|
||||
})
|
||||
const notifyKeyLabel = computed(() => (isLakala.value ? '通知证书' : '通知密钥'))
|
||||
const extraConfig = computed<Record<string, any>>(() => formData.value.extra_config || {})
|
||||
|
||||
@@ -200,10 +228,15 @@ function handlePayWayChange(payWay: string | number | boolean) {
|
||||
syncLakalaPayMode(String(payWay))
|
||||
}
|
||||
|
||||
// handleEnvironmentChange 处理环境切换,并同步拉卡拉默认网关。
|
||||
// handleEnvironmentChange 处理环境切换,并同步渠道默认网关。
|
||||
function handleEnvironmentChange(environment: string) {
|
||||
if (!isLakala.value || !isDefaultLakalaGateway(formData.value.gateway_url)) return
|
||||
formData.value.gateway_url = defaultGateway('lakala', environment)
|
||||
if (isLakala.value && isDefaultLakalaGateway(formData.value.gateway_url)) {
|
||||
formData.value.gateway_url = defaultGateway('lakala', environment)
|
||||
return
|
||||
}
|
||||
if (isShuncheng.value && isDefaultShunchengGateway(formData.value.gateway_url)) {
|
||||
formData.value.gateway_url = defaultGateway('shuncheng', environment)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewSecrets() {
|
||||
@@ -284,6 +317,7 @@ function handleClose() {
|
||||
>
|
||||
<el-option label="乐刷支付" value="leshua" />
|
||||
<el-option label="拉卡拉支付" value="lakala" />
|
||||
<el-option label="顺成支付" value="shuncheng" />
|
||||
<el-option label="模拟支付" value="mock" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -328,6 +362,14 @@ function handleClose() {
|
||||
<el-input v-model="extraConfig.term_no" placeholder="拉卡拉终端号" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isShuncheng" label="Secret ID" required>
|
||||
<el-input
|
||||
v-model="extraConfig.secret_id"
|
||||
placeholder="顺成接口调用凭据 SecretId"
|
||||
:disabled="isReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="!isMock" :label="signKeyLabel" required>
|
||||
<el-input
|
||||
v-model="formData.sign_key"
|
||||
@@ -436,6 +478,37 @@ function handleClose() {
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isShuncheng" label="门店编号">
|
||||
<el-input
|
||||
v-model="extraConfig.shop_no"
|
||||
placeholder="可选,顺成商户门店编号"
|
||||
:disabled="isReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isShuncheng" label="退款出资">
|
||||
<el-select
|
||||
v-model="extraConfig.refund_type"
|
||||
clearable
|
||||
placeholder="使用顺成默认规则"
|
||||
:disabled="isReadonly"
|
||||
>
|
||||
<el-option label="顺成默认" value="" />
|
||||
<el-option label="当日交易抵扣" value="0" />
|
||||
<el-option label="优先交易抵扣,不足走余额" value="1" />
|
||||
<el-option label="仅商户余额" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isShuncheng" label="Token刷新秒">
|
||||
<el-input-number
|
||||
v-model="extraConfig.token_refresh_margin_seconds"
|
||||
:min="60"
|
||||
:max="1800"
|
||||
:disabled="isReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="支付形态">
|
||||
<el-select v-model="formData.jspay_flag" :disabled="isReadonly">
|
||||
<el-option label="H5 / JSAPI" value="1" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Plus, Refresh, View, Edit, Delete, Download, Upload } from '@element-plus/icons-vue'
|
||||
import { Plus, Refresh, View, Edit, Delete, Download, Upload, StarFilled } from '@element-plus/icons-vue'
|
||||
|
||||
import {
|
||||
fetchPaymentConfigs,
|
||||
@@ -37,6 +37,7 @@ const providerOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '乐刷支付', value: 'leshua' },
|
||||
{ label: '拉卡拉支付', value: 'lakala' },
|
||||
{ label: '顺成支付', value: 'shuncheng' },
|
||||
{ label: '模拟支付', value: 'mock' },
|
||||
]
|
||||
|
||||
@@ -216,6 +217,7 @@ function formatProvider(provider: string) {
|
||||
const map: Record<string, string> = {
|
||||
leshua: '乐刷支付',
|
||||
lakala: '拉卡拉支付',
|
||||
shuncheng: '顺成支付',
|
||||
mock: '模拟支付',
|
||||
}
|
||||
return map[provider] || provider
|
||||
@@ -230,13 +232,13 @@ function formatPayWay(payWay: string) {
|
||||
return map[payWay] || payWay || '-'
|
||||
}
|
||||
|
||||
// getPayWayType 按支付方式返回标签颜色。
|
||||
function getPayWayType(payWay: string) {
|
||||
const map: Record<string, 'success' | 'primary' | 'info'> = {
|
||||
ZFBZF: 'primary',
|
||||
WXZF: 'success',
|
||||
// getPayWayClass 按支付方式返回标签样式,避免与状态/环境标签混淆。
|
||||
function getPayWayClass(payWay: string) {
|
||||
const map: Record<string, string> = {
|
||||
ZFBZF: 'pay-way-alipay',
|
||||
WXZF: 'pay-way-wechat',
|
||||
}
|
||||
return map[payWay] || 'info'
|
||||
return map[payWay] || 'pay-way-other'
|
||||
}
|
||||
|
||||
function formatStatus(status: string) {
|
||||
@@ -260,13 +262,30 @@ function formatAmount(amountCent: number) {
|
||||
return formatCent(amountCent)
|
||||
}
|
||||
|
||||
function getStatusType(status: string) {
|
||||
const map: Record<string, 'success' | 'info' | 'warning'> = {
|
||||
active: 'success',
|
||||
disabled: 'info',
|
||||
testing: 'warning',
|
||||
function getEnvironmentClass(env: string) {
|
||||
const map: Record<string, string> = {
|
||||
production: 'env-production',
|
||||
sandbox: 'env-sandbox',
|
||||
}
|
||||
return map[status] || 'info'
|
||||
return map[env] || 'env-other'
|
||||
}
|
||||
|
||||
function getStatusClass(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
active: 'config-active',
|
||||
disabled: 'config-disabled',
|
||||
testing: 'config-testing',
|
||||
}
|
||||
return map[status] || 'config-disabled'
|
||||
}
|
||||
|
||||
function isDefaultConfig(row: PaymentConfig) {
|
||||
const value = row.is_default as unknown
|
||||
return value === true || value === 'true' || value === 1
|
||||
}
|
||||
|
||||
function tableRowClassName({ row }: { row: PaymentConfig }) {
|
||||
return isDefaultConfig(row) ? 'payment-config-row-default' : ''
|
||||
}
|
||||
|
||||
function canDeleteConfig(row: PaymentConfig) {
|
||||
@@ -357,7 +376,13 @@ function deleteDisabledReason(row: PaymentConfig) {
|
||||
@change="handleImportFile"
|
||||
/>
|
||||
|
||||
<el-table :data="filteredConfigs" v-loading="loading" class="payment-config-table" stripe>
|
||||
<el-table
|
||||
:data="filteredConfigs"
|
||||
v-loading="loading"
|
||||
class="payment-config-table"
|
||||
:row-class-name="tableRowClassName"
|
||||
stripe
|
||||
>
|
||||
<el-table-column prop="name" label="配置名称" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="provider" label="服务商" width="120">
|
||||
<template #default="{ row }">
|
||||
@@ -366,7 +391,7 @@ function deleteDisabledReason(row: PaymentConfig) {
|
||||
</el-table-column>
|
||||
<el-table-column prop="pay_way" label="支付方式" width="104" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPayWayType(row.pay_way)" size="small">
|
||||
<el-tag class="config-tag" :class="getPayWayClass(row.pay_way)" size="small">
|
||||
{{ formatPayWay(row.pay_way) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
@@ -374,21 +399,25 @@ function deleteDisabledReason(row: PaymentConfig) {
|
||||
<el-table-column prop="merchant_id" label="商户号" min-width="150" />
|
||||
<el-table-column prop="environment" label="环境" width="86" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.environment === 'production' ? 'success' : 'warning'" size="small">
|
||||
<el-tag class="config-tag" :class="getEnvironmentClass(row.environment)" size="small">
|
||||
{{ formatEnvironment(row.environment) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="86" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" size="small">
|
||||
<el-tag class="config-tag" :class="getStatusClass(row.status)" size="small">
|
||||
{{ formatStatus(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_default" label="默认" width="86" align="center">
|
||||
<el-table-column prop="is_default" label="默认渠道" width="118" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_default" type="primary" size="small">默认</el-tag>
|
||||
<span v-if="isDefaultConfig(row)" class="default-badge">
|
||||
<el-icon><StarFilled /></el-icon>
|
||||
默认渠道
|
||||
</span>
|
||||
<span v-else class="standby-badge">备用</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="成功支付" min-width="150">
|
||||
@@ -524,6 +553,105 @@ function deleteDisabledReason(row: PaymentConfig) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.payment-config-table :deep(.payment-config-row-default td.el-table__cell) {
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.payment-config-table :deep(.payment-config-row-default td.el-table__cell:first-child) {
|
||||
box-shadow: inset 3px 0 0 #2563eb;
|
||||
}
|
||||
|
||||
.config-tag {
|
||||
min-width: 46px;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.config-tag.pay-way-alipay {
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
.config-tag.pay-way-wechat {
|
||||
color: #047857;
|
||||
background: #ecfdf5;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
|
||||
.config-tag.pay-way-other {
|
||||
color: #475569;
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.config-tag.env-production {
|
||||
color: #475569;
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.config-tag.env-sandbox {
|
||||
color: #92400e;
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.config-tag.env-other {
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.config-tag.config-active {
|
||||
color: #075985;
|
||||
background: #e0f2fe;
|
||||
border-color: #7dd3fc;
|
||||
}
|
||||
|
||||
.config-tag.config-disabled {
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.config-tag.config-testing {
|
||||
color: #92400e;
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.default-badge,
|
||||
.standby-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 72px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.default-badge {
|
||||
gap: 4px;
|
||||
color: #ffffff;
|
||||
background: #1d4ed8;
|
||||
box-shadow: 0 4px 10px rgb(29 78 216 / 0.16);
|
||||
}
|
||||
|
||||
.default-badge .el-icon {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.standby-badge {
|
||||
color: #94a3b8;
|
||||
background: transparent;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.usage-cell {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
|
||||
@@ -186,6 +186,7 @@ function jsonText(value: unknown) {
|
||||
<el-select v-model="filters.provider" clearable placeholder="全部渠道" class="full-control">
|
||||
<el-option label="拉卡拉" value="lakala" />
|
||||
<el-option label="乐刷" value="leshua" />
|
||||
<el-option label="顺成" value="shuncheng" />
|
||||
<el-option label="Mock" value="mock" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
Reference in New Issue
Block a user