feat(payment): PR #538 支付FM插件 - 合并 Danborad 的支付插件实现
This commit is contained in:
commit
bc6897b208
26 files changed
+722
-22
No files matched your search
@@ -0,0 +1,16 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
paymentplugins "infinite-canvas/backend/payment-plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
provider := paymentplugins.NewZhiFuFMProvider(&http.Client{Timeout: 25 * time.Second})
|
||||||
|
if err := paymentplugins.RunRPC(provider, os.Stdin, os.Stdout); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,12 @@ import (
|
|||||||
const (
|
const (
|
||||||
PaymentPluginWeChatNative = "official-payment-wechat-native"
|
PaymentPluginWeChatNative = "official-payment-wechat-native"
|
||||||
PaymentPluginAlipayPage = "official-payment-alipay-page"
|
PaymentPluginAlipayPage = "official-payment-alipay-page"
|
||||||
|
PaymentPluginXunHuPay = "official-payment-xunhupay"
|
||||||
|
PaymentPluginZhiFuFM = "official-payment-zhifufm"
|
||||||
PaymentProviderWeChat = "wechat-native"
|
PaymentProviderWeChat = "wechat-native"
|
||||||
PaymentProviderAlipay = "alipay-page-pay"
|
PaymentProviderAlipay = "alipay-page-pay"
|
||||||
|
PaymentProviderXunHuPay = "xunhupay-aggregate"
|
||||||
|
PaymentProviderZhiFuFM = "zhifufm-pay"
|
||||||
)
|
)
|
||||||
|
|
||||||
func bundledPaymentPluginManifests() []protocol.Manifest {
|
func bundledPaymentPluginManifests() []protocol.Manifest {
|
||||||
|
|||||||
@@ -151,3 +151,38 @@ func TestXunHuPayOfficialPackageIsPaymentPlugin(t *testing.T) {
|
|||||||
t.Fatalf("xunhupay provider type = %T", provider)
|
t.Fatalf("xunhupay provider type = %T", provider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestZhiFuFMOfficialPackageIsPaymentPlugin(t *testing.T) {
|
||||||
|
center, err := newPluginRuntime(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var plugin PluginView
|
||||||
|
for _, item := range center.list() {
|
||||||
|
if item.Manifest.ID == "official-payment-zhifufm" {
|
||||||
|
plugin = item
|
||||||
|
plugin.Management = pluginManagementFromView(item)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if plugin.Manifest.ID == "" {
|
||||||
|
t.Fatal("official-payment-zhifufm is missing")
|
||||||
|
}
|
||||||
|
if plugin.Management.Kind != PluginKindPayment || plugin.Source != PluginOriginOfficial {
|
||||||
|
t.Fatalf("zhifufm plugin = %#v", plugin)
|
||||||
|
}
|
||||||
|
if len(plugin.Manifest.Contributes.PaymentProviders) != 1 || plugin.Manifest.Contributes.PaymentProviders[0].ID != "zhifufm-pay" {
|
||||||
|
t.Fatalf("zhifufm contributions = %#v", plugin.Manifest.Contributes.PaymentProviders)
|
||||||
|
}
|
||||||
|
registry := center.paymentRegistrySnapshot()
|
||||||
|
if registry == nil {
|
||||||
|
t.Fatal("payment registry is nil")
|
||||||
|
}
|
||||||
|
provider, ok := registry.Get("zhifufm-pay")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("zhifufm-pay provider is missing")
|
||||||
|
}
|
||||||
|
if _, ok := provider.(*payment.RPCProvider); !ok {
|
||||||
|
t.Fatalf("zhifufm provider type = %T", provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -144,13 +144,16 @@ func RegisterPaymentRoutes(r *gin.RouterGroup, svc *service.Service) {
|
|||||||
// Provider callbacks are intentionally unauthenticated at the application
|
// Provider callbacks are intentionally unauthenticated at the application
|
||||||
// layer. Authenticity is established by the pinned provider config and raw
|
// layer. Authenticity is established by the pinned provider config and raw
|
||||||
// request signature before any durable event is accepted.
|
// request signature before any durable event is accepted.
|
||||||
r.POST("/payments/notify/:providerId/:configId", func(c *gin.Context) {
|
notifyHandler := func(c *gin.Context) {
|
||||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, paymentNotificationMaxBytes)
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, paymentNotificationMaxBytes)
|
||||||
rawBody, err := io.ReadAll(c.Request.Body)
|
rawBody, err := io.ReadAll(c.Request.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writePaymentNotificationFailure(c, svc, c.Param("providerId"), http.StatusBadRequest)
|
writePaymentNotificationFailure(c, svc, c.Param("providerId"), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(rawBody) == 0 && c.Request.URL.RawQuery != "" {
|
||||||
|
rawBody = []byte(c.Request.URL.RawQuery)
|
||||||
|
}
|
||||||
err = svc.AcceptPaymentNotification(c.Request.Context(), c.Param("providerId"), c.Param("configId"), c.Request.Header.Clone(), rawBody)
|
err = svc.AcceptPaymentNotification(c.Request.Context(), c.Param("providerId"), c.Param("configId"), c.Request.Header.Clone(), rawBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusInternalServerError
|
status := http.StatusInternalServerError
|
||||||
@@ -167,7 +170,10 @@ func RegisterPaymentRoutes(r *gin.RouterGroup, svc *service.Service) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Status(status)
|
c.Status(status)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
r.POST("/payments/notify/:providerId/:configId", notifyHandler)
|
||||||
|
r.GET("/payments/notify/:providerId/:configId", notifyHandler)
|
||||||
r.GET("/payments/return/:providerId", func(c *gin.Context) {
|
r.GET("/payments/return/:providerId", func(c *gin.Context) {
|
||||||
orderID := strings.ToLower(strings.TrimSpace(c.Query("orderId")))
|
orderID := strings.ToLower(strings.TrimSpace(c.Query("orderId")))
|
||||||
if !paymentOrderIDPattern.MatchString(orderID) {
|
if !paymentOrderIDPattern.MatchString(orderID) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ var officialPaymentPackageIDs = []string{
|
|||||||
"official-payment-wechat-native",
|
"official-payment-wechat-native",
|
||||||
"official-payment-alipay-page",
|
"official-payment-alipay-page",
|
||||||
"official-payment-xunhupay",
|
"official-payment-xunhupay",
|
||||||
|
"official-payment-zhifufm",
|
||||||
}
|
}
|
||||||
|
|
||||||
var officialPaymentTaggedArtifacts = []string{
|
var officialPaymentTaggedArtifacts = []string{
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ const (
|
|||||||
ProviderWeChatNative = "wechat-native"
|
ProviderWeChatNative = "wechat-native"
|
||||||
ProviderAlipayPage = "alipay-page-pay"
|
ProviderAlipayPage = "alipay-page-pay"
|
||||||
ProviderXunHuPay = "xunhupay-aggregate"
|
ProviderXunHuPay = "xunhupay-aggregate"
|
||||||
|
ProviderZhiFuFM = "zhifufm-pay"
|
||||||
PluginWeChatNative = "official-payment-wechat-native"
|
PluginWeChatNative = "official-payment-wechat-native"
|
||||||
PluginAlipayPage = "official-payment-alipay-page"
|
PluginAlipayPage = "official-payment-alipay-page"
|
||||||
PluginXunHuPay = "official-payment-xunhupay"
|
PluginXunHuPay = "official-payment-xunhupay"
|
||||||
|
PluginZhiFuFM = "official-payment-zhifufm"
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package paymentplugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultZhiFuFMGateway = "https://api-5a0zvlcbi80.zhifu.fm.it88168.com/api"
|
||||||
|
zhifuFMStartOrderPath = "/startOrder"
|
||||||
|
zhifuFMQRLifetime = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type ZhiFuFMProvider struct {
|
||||||
|
client *http.Client
|
||||||
|
now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZhiFuFMProvider(client *http.Client) *ZhiFuFMProvider {
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
return &ZhiFuFMProvider{client: client, now: time.Now}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhiFuFMProvider) Descriptor() Descriptor {
|
||||||
|
return Descriptor{
|
||||||
|
ID: ProviderZhiFuFM, PluginID: PluginZhiFuFM, PluginVersion: "1.0.0",
|
||||||
|
Name: "支付FM", Icon: "assets/icon.svg", CheckoutMode: "redirect",
|
||||||
|
IdentityFields: []string{"merchantNum"},
|
||||||
|
NotificationSuccess: NotificationResponse{Status: 200, ContentType: "text/plain; charset=utf-8", Body: "success"},
|
||||||
|
NotificationFailure: NotificationResponse{Status: 400, ContentType: "text/plain; charset=utf-8", Body: "fail"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhiFuFMProvider) ValidateConfig(config Config) error {
|
||||||
|
if strings.TrimSpace(config["merchantNum"]) == "" {
|
||||||
|
return errors.New("支付FM配置缺少商户号 merchantNum")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(config["secretKey"]) == "" {
|
||||||
|
return errors.New("支付FM配置缺少接入密钥 secretKey")
|
||||||
|
}
|
||||||
|
mode := zhifuFMCheckoutMode(config)
|
||||||
|
if mode != "qr_code" && mode != "redirect" {
|
||||||
|
return errors.New("支付FM checkoutMode 必须是 qr_code 或 redirect")
|
||||||
|
}
|
||||||
|
gateway := zhifuFMGateway(config)
|
||||||
|
if _, err := url.Parse(gateway); err != nil || (!strings.HasPrefix(gateway, "http://") && !strings.HasPrefix(gateway, "https://")) {
|
||||||
|
return fmt.Errorf("支付FM网关地址无效: %s", gateway)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type zhifuFMStartOrderResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Data struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PayURL string `json:"payUrl"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhiFuFMProvider) CreateOrder(ctx context.Context, config Config, request CreateRequest) (Checkout, error) {
|
||||||
|
if err := p.ValidateConfig(config); err != nil {
|
||||||
|
return Checkout{}, err
|
||||||
|
}
|
||||||
|
if request.AmountFen <= 0 || request.Currency != "CNY" || request.MerchantOrderNo == "" || request.NotifyURL == "" {
|
||||||
|
return Checkout{}, errors.New("支付FM下单参数无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
merchantNum := strings.TrimSpace(config["merchantNum"])
|
||||||
|
secretKey := strings.TrimSpace(config["secretKey"])
|
||||||
|
amountYuan := fmt.Sprintf("%.2f", float64(request.AmountFen)/100.0)
|
||||||
|
|
||||||
|
// sign = md5(商户号 + 商户订单号 + 支付金额 + 异步通知地址 + 接入密钥)
|
||||||
|
signRaw := merchantNum + request.MerchantOrderNo + amountYuan + request.NotifyURL + secretKey
|
||||||
|
sign := md5Hex(signRaw)
|
||||||
|
|
||||||
|
payType := strings.TrimSpace(config["payType"])
|
||||||
|
if payType == "" {
|
||||||
|
payType = "alipay"
|
||||||
|
}
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("merchantNum", merchantNum)
|
||||||
|
form.Set("orderNo", request.MerchantOrderNo)
|
||||||
|
form.Set("amount", amountYuan)
|
||||||
|
form.Set("notifyUrl", request.NotifyURL)
|
||||||
|
form.Set("payType", payType)
|
||||||
|
form.Set("sign", sign)
|
||||||
|
form.Set("returnType", "json")
|
||||||
|
form.Set("apiMode", "post_form")
|
||||||
|
if request.ReturnURL != "" {
|
||||||
|
form.Set("returnUrl", request.ReturnURL)
|
||||||
|
}
|
||||||
|
if request.Description != "" {
|
||||||
|
form.Set("subject", request.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
gateway := zhifuFMGateway(config)
|
||||||
|
endpoint := strings.TrimSuffix(gateway, "/") + zhifuFMStartOrderPath
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return Checkout{}, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := p.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return Checkout{}, &ProviderError{Code: "zhifufm_transport_error", Message: "支付FM网络请求失败", Temporary: true, Cause: err}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
|
||||||
|
if err != nil {
|
||||||
|
return Checkout{}, &ProviderError{Code: "zhifufm_response_read_error", Message: "读取支付FM响应失败", Temporary: true, Cause: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res zhifuFMStartOrderResponse
|
||||||
|
if err := json.Unmarshal(body, &res); err != nil {
|
||||||
|
return Checkout{}, &ProviderError{Code: "zhifufm_response_parse_error", Message: fmt.Sprintf("解析支付FM响应失败 (HTTP %d): %s", resp.StatusCode, string(body)), Temporary: true, Cause: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !res.Success || res.Code != 200 || res.Data.PayURL == "" {
|
||||||
|
msg := res.Msg
|
||||||
|
if msg == "" {
|
||||||
|
msg = fmt.Sprintf("创建订单失败(code=%d)", res.Code)
|
||||||
|
}
|
||||||
|
return Checkout{}, &ProviderError{Code: "zhifufm_order_create_failed", Message: fmt.Sprintf("支付FM创建订单失败: %s", msg)}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := zhifuFMCheckoutMode(config)
|
||||||
|
expiresAt := request.ExpiresAt
|
||||||
|
if expiresAt.IsZero() {
|
||||||
|
expiresAt = p.now().Add(zhifuFMQRLifetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Checkout{
|
||||||
|
Mode: mode,
|
||||||
|
Value: res.Data.PayURL,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhiFuFMProvider) QueryOrder(ctx context.Context, config Config, request QueryRequest) (Result, error) {
|
||||||
|
return Result{
|
||||||
|
MerchantOrderNo: request.MerchantOrderNo,
|
||||||
|
Currency: "CNY",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhiFuFMProvider) CloseOrder(ctx context.Context, config Config, request CloseRequest) (Result, error) {
|
||||||
|
return Result{
|
||||||
|
MerchantOrderNo: request.MerchantOrderNo,
|
||||||
|
Closed: true,
|
||||||
|
ProviderStatus: "CLOSED",
|
||||||
|
Currency: "CNY",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhiFuFMProvider) VerifyNotification(_ context.Context, config Config, headers http.Header, rawBody []byte) (Notification, error) {
|
||||||
|
if err := p.ValidateConfig(config); err != nil {
|
||||||
|
return Notification{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := parseNotificationValues(rawBody)
|
||||||
|
if err != nil {
|
||||||
|
return Notification{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
merchantNum := strings.TrimSpace(params.Get("merchantNum"))
|
||||||
|
orderNo := strings.TrimSpace(params.Get("orderNo"))
|
||||||
|
amountStr := strings.TrimSpace(params.Get("amount"))
|
||||||
|
state := strings.TrimSpace(params.Get("state"))
|
||||||
|
platformOrderNo := strings.TrimSpace(params.Get("platformOrderNo"))
|
||||||
|
sign := strings.TrimSpace(params.Get("sign"))
|
||||||
|
|
||||||
|
if merchantNum != strings.TrimSpace(config["merchantNum"]) {
|
||||||
|
return Notification{}, errors.New("支付FM异步通知商户号不匹配")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验签: sign = md5(state + merchantNum + orderNo + amount + secretKey)
|
||||||
|
expectedSign := md5Hex(state + merchantNum + orderNo + amountStr + strings.TrimSpace(config["secretKey"]))
|
||||||
|
if !strings.EqualFold(sign, expectedSign) {
|
||||||
|
return Notification{}, fmt.Errorf("支付FM异步通知签名不匹配: got %s, want %s", sign, expectedSign)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualAmountStr := strings.TrimSpace(params.Get("actualPayAmount"))
|
||||||
|
targetAmountStr := amountStr
|
||||||
|
if actualAmountStr != "" {
|
||||||
|
targetAmountStr = actualAmountStr
|
||||||
|
}
|
||||||
|
|
||||||
|
amountFen, err := parseYuanToFen(targetAmountStr)
|
||||||
|
if err != nil {
|
||||||
|
amountFen, err = parseYuanToFen(amountStr)
|
||||||
|
if err != nil {
|
||||||
|
return Notification{}, fmt.Errorf("支付FM异步通知金额格式错误: %s", amountStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paid := state == "1"
|
||||||
|
paidAt := time.Now()
|
||||||
|
if payTimeStr := strings.TrimSpace(params.Get("payTime")); payTimeStr != "" {
|
||||||
|
if t, err := time.ParseInLocation("2006-01-02 15:04:05", payTimeStr, time.Local); err == nil {
|
||||||
|
paidAt = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventID := platformOrderNo
|
||||||
|
if eventID == "" {
|
||||||
|
eventID = orderNo + ":" + state
|
||||||
|
}
|
||||||
|
|
||||||
|
return Notification{
|
||||||
|
EventID: eventID,
|
||||||
|
Result: Result{
|
||||||
|
MerchantOrderNo: orderNo,
|
||||||
|
ProviderTradeNo: platformOrderNo,
|
||||||
|
ProviderStatus: state,
|
||||||
|
AmountFen: amountFen,
|
||||||
|
Currency: "CNY",
|
||||||
|
Paid: paid,
|
||||||
|
Closed: !paid,
|
||||||
|
PaidAt: paidAt,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ZhiFuFMProvider) DownloadTradeBill(context.Context, Config, time.Time) ([]BillRecord, error) {
|
||||||
|
return nil, fmt.Errorf("%w: 支付FM未提供交易账单下载接口", ErrTradeBillNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func zhifuFMGateway(config Config) string {
|
||||||
|
g := strings.TrimSpace(config["gateway"])
|
||||||
|
if g == "" {
|
||||||
|
return defaultZhiFuFMGateway
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(g, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func zhifuFMCheckoutMode(config Config) string {
|
||||||
|
mode := strings.TrimSpace(config["checkoutMode"])
|
||||||
|
if mode == "qr_code" {
|
||||||
|
return "qr_code"
|
||||||
|
}
|
||||||
|
return "redirect"
|
||||||
|
}
|
||||||
|
|
||||||
|
func md5Hex(s string) string {
|
||||||
|
h := md5.Sum([]byte(s))
|
||||||
|
return hex.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNotificationValues(rawBody []byte) (url.Values, error) {
|
||||||
|
bodyStr := string(rawBody)
|
||||||
|
if strings.Contains(bodyStr, "=") {
|
||||||
|
return url.ParseQuery(bodyStr)
|
||||||
|
}
|
||||||
|
var jsonMap map[string]any
|
||||||
|
if err := json.Unmarshal(rawBody, &jsonMap); err == nil {
|
||||||
|
vals := url.Values{}
|
||||||
|
for k, v := range jsonMap {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
vals.Set(k, val)
|
||||||
|
case float64:
|
||||||
|
vals.Set(k, strconv.FormatFloat(val, 'f', -1, 64))
|
||||||
|
case int64:
|
||||||
|
vals.Set(k, strconv.FormatInt(val, 10))
|
||||||
|
default:
|
||||||
|
vals.Set(k, fmt.Sprint(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("无法解析支付通知内容")
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package paymentplugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestZhiFuFMProviderCreateOrderAndVerifyNotification(t *testing.T) {
|
||||||
|
merchantNum := "694771317639364608"
|
||||||
|
secretKey := "f17bc85d99c1d2b4b4d81eaba1489c7e"
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/startOrder" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
vals, _ := parseNotificationValues(body)
|
||||||
|
if vals.Get("merchantNum") != merchantNum {
|
||||||
|
t.Errorf("got merchantNum = %q, want %q", vals.Get("merchantNum"), merchantNum)
|
||||||
|
}
|
||||||
|
expectedSign := md5Hex(merchantNum + vals.Get("orderNo") + vals.Get("amount") + vals.Get("notifyUrl") + secretKey)
|
||||||
|
if vals.Get("sign") != expectedSign {
|
||||||
|
t.Errorf("got sign = %q, want %q", vals.Get("sign"), expectedSign)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`{
|
||||||
|
"success": true,
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"timestamp": 1720000000000,
|
||||||
|
"data": {
|
||||||
|
"id": "ZFM202609180001",
|
||||||
|
"payUrl": "https://page.zhifu.fm/pay?id=ZFM202609180001"
|
||||||
|
}
|
||||||
|
}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
provider := NewZhiFuFMProvider(server.Client())
|
||||||
|
config := Config{
|
||||||
|
"merchantNum": merchantNum,
|
||||||
|
"secretKey": secretKey,
|
||||||
|
"gateway": server.URL,
|
||||||
|
"checkoutMode": "redirect",
|
||||||
|
}
|
||||||
|
|
||||||
|
checkout, err := provider.CreateOrder(context.Background(), config, CreateRequest{
|
||||||
|
MerchantOrderNo: "TEST_ORDER_12345",
|
||||||
|
Description: "充值 1000 积分",
|
||||||
|
AmountFen: 1000, // 10.00 元
|
||||||
|
Currency: "CNY",
|
||||||
|
NotifyURL: "https://my.domain/api/payments/notify",
|
||||||
|
ExpiresAt: time.Now().Add(10 * time.Minute),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateOrder failed: %v", err)
|
||||||
|
}
|
||||||
|
if checkout.Mode != "redirect" || checkout.Value != "https://page.zhifu.fm/pay?id=ZFM202609180001" {
|
||||||
|
t.Fatalf("unexpected checkout: %#v", checkout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟异步通知回调
|
||||||
|
// sign = md5(state + merchantNum + orderNo + amount + secretKey)
|
||||||
|
state := "1"
|
||||||
|
orderNo := "TEST_ORDER_12345"
|
||||||
|
amount := "10.00"
|
||||||
|
notifySign := md5Hex(state + merchantNum + orderNo + amount + secretKey)
|
||||||
|
notifyForm := "merchantNum=" + merchantNum + "&orderNo=" + orderNo + "&amount=" + amount + "&state=" + state + "&platformOrderNo=ZFM202609180001&payTime=2026-09-18+12:00:00&sign=" + notifySign
|
||||||
|
|
||||||
|
notification, err := provider.VerifyNotification(context.Background(), config, nil, []byte(notifyForm))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VerifyNotification failed: %v", err)
|
||||||
|
}
|
||||||
|
if !notification.Paid || notification.AmountFen != 1000 || notification.MerchantOrderNo != "TEST_ORDER_12345" {
|
||||||
|
t.Fatalf("unexpected notification: %#v", notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ set -eu
|
|||||||
|
|
||||||
root_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
root_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
repo_dir=$(CDPATH= cd -- "$root_dir/.." && pwd)
|
repo_dir=$(CDPATH= cd -- "$root_dir/.." && pwd)
|
||||||
payment_plugins="official-payment-wechat-native official-payment-alipay-page official-payment-xunhupay"
|
payment_plugins="official-payment-wechat-native official-payment-alipay-page official-payment-xunhupay official-payment-zhifufm"
|
||||||
payments_only=false
|
payments_only=false
|
||||||
default_payment_targets="linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64"
|
default_payment_targets="linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64"
|
||||||
|
|
||||||
@@ -118,6 +118,7 @@ for target in $payment_targets; do
|
|||||||
build_payment_provider official-payment-wechat-native ./cmd/payment-wechat "$target_goos" "$target_goarch"
|
build_payment_provider official-payment-wechat-native ./cmd/payment-wechat "$target_goos" "$target_goarch"
|
||||||
build_payment_provider official-payment-alipay-page ./cmd/payment-alipay "$target_goos" "$target_goarch"
|
build_payment_provider official-payment-alipay-page ./cmd/payment-alipay "$target_goos" "$target_goarch"
|
||||||
build_payment_provider official-payment-xunhupay ./cmd/payment-xunhupay "$target_goos" "$target_goarch"
|
build_payment_provider official-payment-xunhupay ./cmd/payment-xunhupay "$target_goos" "$target_goarch"
|
||||||
|
build_payment_provider official-payment-zhifufm ./cmd/payment-zhifufm "$target_goos" "$target_goarch"
|
||||||
done
|
done
|
||||||
|
|
||||||
package_plugin() {
|
package_plugin() {
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
# 支付FM 聚合支付插件
|
||||||
|
|
||||||
|
支持对接支付FM免签个人收款码与签约支付通道,支持支付宝/微信扫码及收银台跳转。
|
||||||
|
|
||||||
|
### 插件配置参数
|
||||||
|
- **服务器公网地址**: 本站对外公网地址,用于接收 `notifyUrl` 异步回调通知。
|
||||||
|
- **商户号 (merchantNum)**: 支付FM商户后台【用户中心】查看的商户号。
|
||||||
|
- **接入密钥 (secretKey)**: 支付FM商户后台【用户中心】查看的接口接入密钥。
|
||||||
|
- **接口根地址**: 支付FM后台显示的专属根地址,例如 `https://api-5a0zvlcbi80.zhifu.fm.it88168.com/api`。
|
||||||
|
- **默认支付通道 (payType)**: 推荐使用 `aloop` / `tloop` 或 `alipay` / `wechat`。
|
||||||
|
- **收银方式**: 默认为 `redirect` 跳转模式。
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||||
|
<image href="data:image/png;base64,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 truncated
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,119 @@
|
|||||||
|
# 支付FM 接口协议说明
|
||||||
|
|
||||||
|
遵循 `yingce.payment/v1` 标准,自动转换影策系统订单至支付FM `/startOrder` 接口,并在收到通知时按 MD5 规则完成异步验签。
|
||||||
|
|
||||||
|
<!-- YINGCE_MANIFEST_CONTRACT_START -->
|
||||||
|
## Manifest 完整接口定义
|
||||||
|
|
||||||
|
以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apiVersion": "yingce.plugin/v1",
|
||||||
|
"id": "official-payment-zhifufm",
|
||||||
|
"name": "支付FM",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "支付FM",
|
||||||
|
"description": "支付FM个人免签/签约聚合支付适配器,支持微信/支付宝扫码或收银台跳转。",
|
||||||
|
"enabled": true,
|
||||||
|
"installable": true,
|
||||||
|
"runtime": {
|
||||||
|
"backend": "rpc",
|
||||||
|
"backendEntry": "backend/provider"
|
||||||
|
},
|
||||||
|
"surfaces": [
|
||||||
|
"wallet",
|
||||||
|
"settings"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"payment.create",
|
||||||
|
"payment.query",
|
||||||
|
"payment.close",
|
||||||
|
"payment.reconcile"
|
||||||
|
],
|
||||||
|
"configuration": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "publicBaseUrl",
|
||||||
|
"type": "url",
|
||||||
|
"label": "服务器公网地址",
|
||||||
|
"required": true,
|
||||||
|
"description": "用于生成异步通知 notifyUrl 地址,必须可被支付FM外网服务器访问回调。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "merchantNum",
|
||||||
|
"type": "string",
|
||||||
|
"label": "商户号 (merchantNum)",
|
||||||
|
"required": true,
|
||||||
|
"description": "支付FM商户后台【用户中心】查看的商户号(8-18位数字)。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "secretKey",
|
||||||
|
"type": "password",
|
||||||
|
"label": "接入密钥 (secretKey)",
|
||||||
|
"required": true,
|
||||||
|
"secret": true,
|
||||||
|
"description": "支付FM商户后台【用户中心】查看的接口接入密钥。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gateway",
|
||||||
|
"type": "url",
|
||||||
|
"label": "接口根地址",
|
||||||
|
"required": true,
|
||||||
|
"default": "https://api-5a0zvlcbi80.zhifu.fm.it88168.com/api",
|
||||||
|
"description": "支付FM分配的接口根地址,如 https://api-5a0zvlcbi80.zhifu.fm.it88168.com/api。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payType",
|
||||||
|
"type": "string",
|
||||||
|
"label": "默认支付通道 (payType)",
|
||||||
|
"required": false,
|
||||||
|
"default": "alipay",
|
||||||
|
"description": "支付产品通道:alipay(支付宝)、wechat(微信)、aloop(支付宝轮询池)、tloop(微信轮询池)等。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "checkoutMode",
|
||||||
|
"type": "string",
|
||||||
|
"label": "收银方式",
|
||||||
|
"required": false,
|
||||||
|
"default": "redirect",
|
||||||
|
"values": [
|
||||||
|
"redirect",
|
||||||
|
"qr_code"
|
||||||
|
],
|
||||||
|
"description": "redirect 跳转支付FM收银页;qr_code 在钱包中直接将 payUrl 渲染为二维码扫码。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contributes": {
|
||||||
|
"paymentProviders": [
|
||||||
|
{
|
||||||
|
"id": "zhifufm-pay",
|
||||||
|
"label": "支付FM",
|
||||||
|
"icon": "assets/icon.svg",
|
||||||
|
"checkoutMode": "redirect",
|
||||||
|
"identityFields": [
|
||||||
|
"merchantNum"
|
||||||
|
],
|
||||||
|
"expiryPolicy": {
|
||||||
|
"defaultMinutes": 10,
|
||||||
|
"minMinutes": 3,
|
||||||
|
"maxMinutes": 15
|
||||||
|
},
|
||||||
|
"notificationSuccess": {
|
||||||
|
"status": 200,
|
||||||
|
"contentType": "text/plain; charset=utf-8",
|
||||||
|
"body": "success"
|
||||||
|
},
|
||||||
|
"notificationFailure": {
|
||||||
|
"status": 400,
|
||||||
|
"contentType": "text/plain; charset=utf-8",
|
||||||
|
"body": "fail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"documentation": "<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
<!-- YINGCE_MANIFEST_CONTRACT_END -->
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"apiVersion": "yingce.plugin/v1",
|
||||||
|
"id": "official-payment-zhifufm",
|
||||||
|
"name": "支付FM",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "支付FM",
|
||||||
|
"description": "支付FM个人免签/签约聚合支付适配器,支持微信/支付宝扫码或收银台跳转。",
|
||||||
|
"enabled": true,
|
||||||
|
"installable": true,
|
||||||
|
"runtime": {
|
||||||
|
"backend": "rpc",
|
||||||
|
"backendEntry": "backend/provider"
|
||||||
|
},
|
||||||
|
"surfaces": [
|
||||||
|
"wallet",
|
||||||
|
"settings"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"payment.create",
|
||||||
|
"payment.query",
|
||||||
|
"payment.close",
|
||||||
|
"payment.reconcile"
|
||||||
|
],
|
||||||
|
"configuration": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "publicBaseUrl",
|
||||||
|
"type": "url",
|
||||||
|
"label": "服务器公网地址",
|
||||||
|
"required": true,
|
||||||
|
"description": "用于生成异步通知 notifyUrl 地址,必须可被支付FM外网服务器访问回调。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "merchantNum",
|
||||||
|
"type": "string",
|
||||||
|
"label": "商户号 (merchantNum)",
|
||||||
|
"required": true,
|
||||||
|
"description": "支付FM商户后台【用户中心】查看的商户号(8-18位数字)。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "secretKey",
|
||||||
|
"type": "password",
|
||||||
|
"label": "接入密钥 (secretKey)",
|
||||||
|
"required": true,
|
||||||
|
"secret": true,
|
||||||
|
"description": "支付FM商户后台【用户中心】查看的接口接入密钥。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gateway",
|
||||||
|
"type": "url",
|
||||||
|
"label": "接口根地址",
|
||||||
|
"required": true,
|
||||||
|
"default": "https://api-5a0zvlcbi80.zhifu.fm.it88168.com/api",
|
||||||
|
"description": "支付FM分配的接口根地址,如 https://api-5a0zvlcbi80.zhifu.fm.it88168.com/api。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payType",
|
||||||
|
"type": "string",
|
||||||
|
"label": "默认支付通道 (payType)",
|
||||||
|
"required": false,
|
||||||
|
"default": "alipay",
|
||||||
|
"description": "支付产品通道:alipay(支付宝)、wechat(微信)、aloop(支付宝轮询池)、tloop(微信轮询池)等。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "checkoutMode",
|
||||||
|
"type": "string",
|
||||||
|
"label": "收银方式",
|
||||||
|
"required": false,
|
||||||
|
"default": "redirect",
|
||||||
|
"values": [
|
||||||
|
"redirect",
|
||||||
|
"qr_code"
|
||||||
|
],
|
||||||
|
"description": "redirect 跳转支付FM收银页;qr_code 在钱包中直接将 payUrl 渲染为二维码扫码。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"contributes": {
|
||||||
|
"paymentProviders": [
|
||||||
|
{
|
||||||
|
"id": "zhifufm-pay",
|
||||||
|
"label": "支付FM",
|
||||||
|
"icon": "assets/icon.svg",
|
||||||
|
"checkoutMode": "redirect",
|
||||||
|
"identityFields": [
|
||||||
|
"merchantNum"
|
||||||
|
],
|
||||||
|
"expiryPolicy": {
|
||||||
|
"defaultMinutes": 10,
|
||||||
|
"minMinutes": 3,
|
||||||
|
"maxMinutes": 15
|
||||||
|
},
|
||||||
|
"notificationSuccess": {
|
||||||
|
"status": 200,
|
||||||
|
"contentType": "text/plain; charset=utf-8",
|
||||||
|
"body": "success"
|
||||||
|
},
|
||||||
|
"notificationFailure": {
|
||||||
|
"status": 400,
|
||||||
|
"contentType": "text/plain; charset=utf-8",
|
||||||
|
"body": "fail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"documentation": "# 支付FM 聚合支付插件\n\n支持对接支付FM免签个人收款码与签约支付通道,支持支付宝/微信扫码及收银台跳转。\n\n### 插件配置参数\n- **服务器公网地址**: 本站对外公网地址,用于接收 `notifyUrl` 异步回调通知。\n- **商户号 (merchantNum)**: 支付FM商户后台【用户中心】查看的商户号。\n- **接入密钥 (secretKey)**: 支付FM商户后台【用户中心】查看的接口接入密钥。\n- **接口根地址**: 支付FM后台显示的专属根地址,例如 `https://api-5a0zvlcbi80.zhifu.fm.it88168.com/api`。\n- **默认支付通道 (payType)**: 推荐使用 `aloop` / `tloop` 或 `alipay` / `wechat`。\n- **收银方式**: 默认为 `redirect` 跳转模式。\n\n---\n\n# 支付FM 接口协议说明\n\n遵循 `yingce.payment/v1` 标准,自动转换影策系统订单至支付FM `/startOrder` 接口,并在收到通知时按 MD5 规则完成异步验签。\n\n<!-- YINGCE_MANIFEST_CONTRACT_START -->\n## Manifest 完整接口定义\n\n以下 JSON 与插件包内实际 `manifest.json` 逐字段一致,覆盖插件身份、权限、配置、鉴权、参数、校验、创建、Agent、查询、取消、结果下载、响应和 Agent 响应映射。`documentation` 字段的值就是当前完整文档;为避免文档在自身内部无限递归,JSON 中仅用等义占位文本表示正文。\n\n```json\n{\n \"apiVersion\": \"yingce.plugin/v1\",\n \"id\": \"official-payment-zhifufm\",\n \"name\": \"支付FM\",\n \"version\": \"1.0.0\",\n \"author\": \"支付FM\",\n \"description\": \"支付FM个人免签/签约聚合支付适配器,支持微信/支付宝扫码或收银台跳转。\",\n \"enabled\": true,\n \"installable\": true,\n \"runtime\": {\n \"backend\": \"rpc\",\n \"backendEntry\": \"backend/provider\"\n },\n \"surfaces\": [\n \"wallet\",\n \"settings\"\n ],\n \"permissions\": [\n \"payment.create\",\n \"payment.query\",\n \"payment.close\",\n \"payment.reconcile\"\n ],\n \"configuration\": {\n \"fields\": [\n {\n \"name\": \"publicBaseUrl\",\n \"type\": \"url\",\n \"label\": \"服务器公网地址\",\n \"required\": true,\n \"description\": \"用于生成异步通知 notifyUrl 地址,必须可被支付FM外网服务器访问回调。\"\n },\n {\n \"name\": \"merchantNum\",\n \"type\": \"string\",\n \"label\": \"商户号 (merchantNum)\",\n \"required\": true,\n \"description\": \"支付FM商户后台【用户中心】查看的商户号(8-18位数字)。\"\n },\n {\n \"name\": \"secretKey\",\n \"type\": \"password\",\n \"label\": \"接入密钥 (secretKey)\",\n \"required\": true,\n \"secret\": true,\n \"description\": \"支付FM商户后台【用户中心】查看的接口接入密钥。\"\n },\n {\n \"name\": \"gateway\",\n \"type\": \"url\",\n \"label\": \"接口根地址\",\n \"required\": true,\n \"default\": \"https://api-5a0zvlcbi80.zhifu.fm.it88168.com/api\",\n \"description\": \"支付FM分配的接口根地址,如 https://api-5a0zvlcbi80.zhifu.fm.it88168.com/api。\"\n },\n {\n \"name\": \"payType\",\n \"type\": \"string\",\n \"label\": \"默认支付通道 (payType)\",\n \"required\": false,\n \"default\": \"alipay\",\n \"description\": \"支付产品通道:alipay(支付宝)、wechat(微信)、aloop(支付宝轮询池)、tloop(微信轮询池)等。\"\n },\n {\n \"name\": \"checkoutMode\",\n \"type\": \"string\",\n \"label\": \"收银方式\",\n \"required\": false,\n \"default\": \"redirect\",\n \"values\": [\n \"redirect\",\n \"qr_code\"\n ],\n \"description\": \"redirect 跳转支付FM收银页;qr_code 在钱包中直接将 payUrl 渲染为二维码扫码。\"\n }\n ]\n },\n \"contributes\": {\n \"paymentProviders\": [\n {\n \"id\": \"zhifufm-pay\",\n \"label\": \"支付FM\",\n \"icon\": \"assets/icon.svg\",\n \"checkoutMode\": \"redirect\",\n \"identityFields\": [\n \"merchantNum\"\n ],\n \"expiryPolicy\": {\n \"defaultMinutes\": 10,\n \"minMinutes\": 3,\n \"maxMinutes\": 15\n },\n \"notificationSuccess\": {\n \"status\": 200,\n \"contentType\": \"text/plain; charset=utf-8\",\n \"body\": \"success\"\n },\n \"notificationFailure\": {\n \"status\": 400,\n \"contentType\": \"text/plain; charset=utf-8\",\n \"body\": \"fail\"\n }\n }\n ]\n },\n \"documentation\": \"<当前插件的完整 documentation,由 README.md 与 docs/interface.md 拼接而成;为避免 JSON 递归,此处不重复展开正文。>\"\n}\n```\nLine truncated
|
||||||
|
}
|
||||||
@@ -118,7 +118,7 @@ if [ "$require_all" = true ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for package_id in official-payment-wechat-native official-payment-alipay-page official-payment-xunhupay; do
|
for package_id in official-payment-wechat-native official-payment-alipay-page official-payment-xunhupay official-payment-zhifufm; do
|
||||||
backend_dir="$root_dir/$package_id/backend"
|
backend_dir="$root_dir/$package_id/backend"
|
||||||
provider="$backend_dir/provider"
|
provider="$backend_dir/provider"
|
||||||
package_file="$root_dir/$package_id.yingce-plugin"
|
package_file="$root_dir/$package_id.yingce-plugin"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons";
|
||||||
|
|
||||||
|
export const ZHIFUFM_LOGO_SRC =
|
||||||
|
"data:image/png;base64,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
Line truncated
|
||||||
|
|
||||||
|
export function PaymentBrandIcon({ providerId, compact = false }: { providerId: string; compact?: boolean }) {
|
||||||
|
const size = compact ? "size-6" : "size-10";
|
||||||
|
if (providerId === "wechat-native" || providerId === "official-payment-wechat-native") {
|
||||||
|
return (
|
||||||
|
<span className={`grid ${size} shrink-0 place-items-center rounded-lg bg-[#07c160]/10 text-[#07c160]`}>
|
||||||
|
<WechatFilled className={compact ? "text-sm" : "text-xl"} aria-hidden />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (providerId === "alipay-page-pay" || providerId === "official-payment-alipay-page") {
|
||||||
|
return (
|
||||||
|
<span className={`grid ${size} shrink-0 place-items-center rounded-lg bg-[#1677ff]/10 text-[#1677ff]`}>
|
||||||
|
<AlipayCircleFilled className={compact ? "text-sm" : "text-xl"} aria-hidden />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (providerId === "zhifufm-pay" || providerId === "official-payment-zhifufm") {
|
||||||
|
return (
|
||||||
|
<span className={`grid ${size} shrink-0 place-items-center rounded-lg overflow-hidden border border-border/40 bg-card shadow-xs`}>
|
||||||
|
<img src={ZHIFUFM_LOGO_SRC} alt="支付FM" className="size-full object-contain select-none pointer-events-none rounded-lg" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span className={`grid ${size} shrink-0 place-items-center rounded-lg bg-muted text-xs font-semibold`}>PAY</span>;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons";
|
import { PaymentBrandIcon } from "@/components/payment-brand-icons";
|
||||||
import { Callout } from "@/pages/admin/ui/controls";
|
import { Callout } from "@/pages/admin/ui/controls";
|
||||||
import { App, Button, DatePicker, Descriptions, Drawer, Form, Input, InputNumber, Select, Tabs, Typography } from "antd";
|
import { App, Button, DatePicker, Descriptions, Drawer, Form, Input, InputNumber, Select, Tabs, Typography } from "antd";
|
||||||
import { AdminDrawer } from "@/pages/admin/ui/overlays";
|
import { AdminDrawer } from "@/pages/admin/ui/overlays";
|
||||||
@@ -912,23 +912,6 @@ export default function AdminPaymentsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaymentBrandIcon({ providerId, compact = false }: { providerId: string; compact?: boolean }) {
|
|
||||||
const size = compact ? "size-6" : "size-10";
|
|
||||||
if (providerId === "wechat-native")
|
|
||||||
return (
|
|
||||||
<span className={`grid ${size} shrink-0 place-items-center rounded-lg bg-[#07c160]/10 text-[#07c160]`}>
|
|
||||||
<WechatFilled className={compact ? "text-sm" : "text-xl"} aria-hidden />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
if (providerId === "alipay-page-pay")
|
|
||||||
return (
|
|
||||||
<span className={`grid ${size} shrink-0 place-items-center rounded-lg bg-[#1677ff]/10 text-[#1677ff]`}>
|
|
||||||
<AlipayCircleFilled className={compact ? "text-sm" : "text-xl"} aria-hidden />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
return <span className={`grid ${size} shrink-0 place-items-center rounded-lg bg-muted text-xs`}>PAY</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDateTime(value: string) {
|
function formatDateTime(value: string) {
|
||||||
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
|
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { App, Button, Input, Select } from "antd";
|
import { App, Button, Input, Select } from "antd";
|
||||||
import { Switch } from "@/pages/admin/ui/controls";
|
import { Switch } from "@/pages/admin/ui/controls";
|
||||||
import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons";
|
import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons";
|
||||||
|
import { ZHIFUFM_LOGO_SRC } from "@/components/payment-brand-icons";
|
||||||
import type { ColumnsType } from "antd/es/table";
|
import type { ColumnsType } from "antd/es/table";
|
||||||
import { CloudUpload, PlugZap, RefreshCw, Search, Trash2, UsersRound } from "lucide-react";
|
import { CloudUpload, PlugZap, RefreshCw, Search, Trash2, UsersRound } from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
@@ -350,6 +351,13 @@ function PluginBrandIcon({ pluginId }: { pluginId: string }) {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (pluginId === "official-payment-zhifufm") {
|
||||||
|
return (
|
||||||
|
<span className="grid size-9 shrink-0 place-items-center rounded-lg overflow-hidden border border-border/40 bg-card shadow-xs">
|
||||||
|
<img src={ZHIFUFM_LOGO_SRC} alt="支付FM" className="size-full object-contain select-none pointer-events-none rounded-lg" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span className="grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-foreground/65">
|
<span className="grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-foreground/65">
|
||||||
<PlugZap className="size-4" aria-hidden="true" />
|
<PlugZap className="size-4" aria-hidden="true" />
|
||||||
|
|||||||
Reference in new issue
Block a user