feat(payment): 支付插件 - 接入支付FM官方标准插件 official-payment-zhifufm

This commit is contained in:
Danborad
2026-09-19 00:42:57 +08:00
parent 46a32a957e
commit 11a4320ad8
26 changed files with 722 additions and 22 deletions

View File

@@ -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)
}
}

View File

@@ -7,8 +7,12 @@ import (
const (
PaymentPluginWeChatNative = "official-payment-wechat-native"
PaymentPluginAlipayPage = "official-payment-alipay-page"
PaymentPluginXunHuPay = "official-payment-xunhupay"
PaymentPluginZhiFuFM = "official-payment-zhifufm"
PaymentProviderWeChat = "wechat-native"
PaymentProviderAlipay = "alipay-page-pay"
PaymentProviderXunHuPay = "xunhupay-aggregate"
PaymentProviderZhiFuFM = "zhifufm-pay"
)
func bundledPaymentPluginManifests() []protocol.Manifest {

View File

@@ -151,3 +151,38 @@ func TestXunHuPayOfficialPackageIsPaymentPlugin(t *testing.T) {
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)
}
}

View File

@@ -144,13 +144,16 @@ func RegisterPaymentRoutes(r *gin.RouterGroup, svc *service.Service) {
// Provider callbacks are intentionally unauthenticated at the application
// layer. Authenticity is established by the pinned provider config and raw
// 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)
rawBody, err := io.ReadAll(c.Request.Body)
if err != nil {
writePaymentNotificationFailure(c, svc, c.Param("providerId"), http.StatusBadRequest)
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)
if err != nil {
status := http.StatusInternalServerError
@@ -167,7 +170,10 @@ func RegisterPaymentRoutes(r *gin.RouterGroup, svc *service.Service) {
return
}
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) {
orderID := strings.ToLower(strings.TrimSpace(c.Query("orderId")))
if !paymentOrderIDPattern.MatchString(orderID) {

View File

@@ -16,6 +16,7 @@ var officialPaymentPackageIDs = []string{
"official-payment-wechat-native",
"official-payment-alipay-page",
"official-payment-xunhupay",
"official-payment-zhifufm",
}
var officialPaymentTaggedArtifacts = []string{

View File

@@ -25,7 +25,9 @@ const (
ProviderWeChatNative = "wechat-native"
ProviderAlipayPage = "alipay-page-pay"
ProviderXunHuPay = "xunhupay-aggregate"
ProviderZhiFuFM = "zhifufm-pay"
PluginWeChatNative = "official-payment-wechat-native"
PluginAlipayPage = "official-payment-alipay-page"
PluginXunHuPay = "official-payment-xunhupay"
PluginZhiFuFM = "official-payment-zhifufm"
)

View File

@@ -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("无法解析支付通知内容")
}

View File

@@ -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)
}
}

View File

@@ -3,7 +3,7 @@ set -eu
root_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && 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
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-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-zhifufm ./cmd/payment-zhifufm "$target_goos" "$target_goarch"
done
package_plugin() {

Binary file not shown.

View File

@@ -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` 跳转模式。

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -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 -->

File diff suppressed because one or more lines are too long

View File

@@ -118,7 +118,7 @@ if [ "$require_all" = true ]; then
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"
provider="$backend_dir/provider"
package_file="$root_dir/$package_id.yingce-plugin"

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

View File

@@ -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 { App, Button, DatePicker, Descriptions, Drawer, Form, Input, InputNumber, Select, Tabs, Typography } from "antd";
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) {
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
}

View File

@@ -1,6 +1,7 @@
import { App, Button, Input, Select } from "antd";
import { Switch } from "@/pages/admin/ui/controls";
import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons";
import { ZHIFUFM_LOGO_SRC } from "@/components/payment-brand-icons";
import type { ColumnsType } from "antd/es/table";
import { CloudUpload, PlugZap, RefreshCw, Search, Trash2, UsersRound } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
@@ -350,6 +351,13 @@ function PluginBrandIcon({ pluginId }: { pluginId: string }) {
</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 (
<span className="grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-foreground/65">
<PlugZap className="size-4" aria-hidden="true" />