diff --git a/backend/cmd/payment-zhifufm/main.go b/backend/cmd/payment-zhifufm/main.go
new file mode 100644
index 000000000..f84e58fc5
--- /dev/null
+++ b/backend/cmd/payment-zhifufm/main.go
@@ -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)
+ }
+}
diff --git a/backend/internal/app/payment_plugins.go b/backend/internal/app/payment_plugins.go
index 1331dcd42..8c3c50b7d 100644
--- a/backend/internal/app/payment_plugins.go
+++ b/backend/internal/app/payment_plugins.go
@@ -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 {
diff --git a/backend/internal/app/payment_plugins_test.go b/backend/internal/app/payment_plugins_test.go
index e28ab2d90..883fa5c1a 100644
--- a/backend/internal/app/payment_plugins_test.go
+++ b/backend/internal/app/payment_plugins_test.go
@@ -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)
+ }
+}
diff --git a/backend/internal/handler/payment.go b/backend/internal/handler/payment.go
index e12ac6803..06994802e 100644
--- a/backend/internal/handler/payment.go
+++ b/backend/internal/handler/payment.go
@@ -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) {
diff --git a/backend/internal/protocol/payment_artifact_test.go b/backend/internal/protocol/payment_artifact_test.go
index 25f1185da..ffa8d7f66 100644
--- a/backend/internal/protocol/payment_artifact_test.go
+++ b/backend/internal/protocol/payment_artifact_test.go
@@ -16,6 +16,7 @@ var officialPaymentPackageIDs = []string{
"official-payment-wechat-native",
"official-payment-alipay-page",
"official-payment-xunhupay",
+ "official-payment-zhifufm",
}
var officialPaymentTaggedArtifacts = []string{
diff --git a/backend/payment-plugins/aliases.go b/backend/payment-plugins/aliases.go
index 6ddecb1f3..674ab8914 100644
--- a/backend/payment-plugins/aliases.go
+++ b/backend/payment-plugins/aliases.go
@@ -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"
)
diff --git a/backend/payment-plugins/zhifufm.go b/backend/payment-plugins/zhifufm.go
new file mode 100644
index 000000000..4856caec3
--- /dev/null
+++ b/backend/payment-plugins/zhifufm.go
@@ -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("无法解析支付通知内容")
+}
diff --git a/backend/payment-plugins/zhifufm_test.go b/backend/payment-plugins/zhifufm_test.go
new file mode 100644
index 000000000..fd5aa7d74
--- /dev/null
+++ b/backend/payment-plugins/zhifufm_test.go
@@ -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)
+ }
+}
diff --git a/plugin-packages/build-packages.sh b/plugin-packages/build-packages.sh
index 5ece7f3e9..b6d486373 100755
--- a/plugin-packages/build-packages.sh
+++ b/plugin-packages/build-packages.sh
@@ -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() {
diff --git a/plugin-packages/official-payment-zhifufm.yingce-plugin b/plugin-packages/official-payment-zhifufm.yingce-plugin
new file mode 100644
index 000000000..26a52080f
Binary files /dev/null and b/plugin-packages/official-payment-zhifufm.yingce-plugin differ
diff --git a/plugin-packages/official-payment-zhifufm/README.md b/plugin-packages/official-payment-zhifufm/README.md
new file mode 100644
index 000000000..48aaf5b07
--- /dev/null
+++ b/plugin-packages/official-payment-zhifufm/README.md
@@ -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` 跳转模式。
diff --git a/plugin-packages/official-payment-zhifufm/assets/icon.svg b/plugin-packages/official-payment-zhifufm/assets/icon.svg
new file mode 100644
index 000000000..ae57140ea
--- /dev/null
+++ b/plugin-packages/official-payment-zhifufm/assets/icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/plugin-packages/official-payment-zhifufm/backend/provider b/plugin-packages/official-payment-zhifufm/backend/provider
new file mode 100644
index 000000000..56fe3d8f0
Binary files /dev/null and b/plugin-packages/official-payment-zhifufm/backend/provider differ
diff --git a/plugin-packages/official-payment-zhifufm/backend/provider-darwin-amd64 b/plugin-packages/official-payment-zhifufm/backend/provider-darwin-amd64
new file mode 100644
index 000000000..57b43ebcd
Binary files /dev/null and b/plugin-packages/official-payment-zhifufm/backend/provider-darwin-amd64 differ
diff --git a/plugin-packages/official-payment-zhifufm/backend/provider-darwin-arm64 b/plugin-packages/official-payment-zhifufm/backend/provider-darwin-arm64
new file mode 100644
index 000000000..c6776367c
Binary files /dev/null and b/plugin-packages/official-payment-zhifufm/backend/provider-darwin-arm64 differ
diff --git a/plugin-packages/official-payment-zhifufm/backend/provider-linux-amd64 b/plugin-packages/official-payment-zhifufm/backend/provider-linux-amd64
new file mode 100644
index 000000000..56fe3d8f0
Binary files /dev/null and b/plugin-packages/official-payment-zhifufm/backend/provider-linux-amd64 differ
diff --git a/plugin-packages/official-payment-zhifufm/backend/provider-linux-arm64 b/plugin-packages/official-payment-zhifufm/backend/provider-linux-arm64
new file mode 100644
index 000000000..eccea4c00
Binary files /dev/null and b/plugin-packages/official-payment-zhifufm/backend/provider-linux-arm64 differ
diff --git a/plugin-packages/official-payment-zhifufm/backend/provider-windows-amd64.exe b/plugin-packages/official-payment-zhifufm/backend/provider-windows-amd64.exe
new file mode 100644
index 000000000..fc16319df
Binary files /dev/null and b/plugin-packages/official-payment-zhifufm/backend/provider-windows-amd64.exe differ
diff --git a/plugin-packages/official-payment-zhifufm/backend/provider-windows-arm64.exe b/plugin-packages/official-payment-zhifufm/backend/provider-windows-arm64.exe
new file mode 100644
index 000000000..672df1325
Binary files /dev/null and b/plugin-packages/official-payment-zhifufm/backend/provider-windows-arm64.exe differ
diff --git a/plugin-packages/official-payment-zhifufm/docs/interface.md b/plugin-packages/official-payment-zhifufm/docs/interface.md
new file mode 100644
index 000000000..35c4e7a8f
--- /dev/null
+++ b/plugin-packages/official-payment-zhifufm/docs/interface.md
@@ -0,0 +1,119 @@
+# 支付FM 接口协议说明
+
+遵循 `yingce.payment/v1` 标准,自动转换影策系统订单至支付FM `/startOrder` 接口,并在收到通知时按 MD5 规则完成异步验签。
+
+
+## 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 递归,此处不重复展开正文。>"
+}
+```
+
diff --git a/plugin-packages/official-payment-zhifufm/manifest.json b/plugin-packages/official-payment-zhifufm/manifest.json
new file mode 100644
index 000000000..e39e4f75b
--- /dev/null
+++ b/plugin-packages/official-payment-zhifufm/manifest.json
@@ -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\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```\n\n"
+}
diff --git a/plugin-packages/verify-payment-packages.sh b/plugin-packages/verify-payment-packages.sh
index e44029e81..2d44adec2 100644
--- a/plugin-packages/verify-payment-packages.sh
+++ b/plugin-packages/verify-payment-packages.sh
@@ -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"
diff --git a/web/public/icons/zhifufm-logo.png b/web/public/icons/zhifufm-logo.png
new file mode 100644
index 000000000..5f2801b6d
Binary files /dev/null and b/web/public/icons/zhifufm-logo.png differ
diff --git a/web/src/components/payment-brand-icons.tsx b/web/src/components/payment-brand-icons.tsx
new file mode 100644
index 000000000..f9920be4f
--- /dev/null
+++ b/web/src/components/payment-brand-icons.tsx
@@ -0,0 +1,30 @@
+import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons";
+
+export const ZHIFUFM_LOGO_SRC =
+ "data:image/png;base64,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";
+
+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 (
+
+
+
+ );
+ }
+ if (providerId === "alipay-page-pay" || providerId === "official-payment-alipay-page") {
+ return (
+
+
+
+ );
+ }
+ if (providerId === "zhifufm-pay" || providerId === "official-payment-zhifufm") {
+ return (
+
+
+
+ );
+ }
+ return PAY;
+}
diff --git a/web/src/pages/admin/payments/payments-page.tsx b/web/src/pages/admin/payments/payments-page.tsx
index 92597e4c8..43420f59d 100644
--- a/web/src/pages/admin/payments/payments-page.tsx
+++ b/web/src/pages/admin/payments/payments-page.tsx
@@ -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 (
-
-
-
- );
- if (providerId === "alipay-page-pay")
- return (
-
-
-
- );
- return PAY;
-}
-
function formatDateTime(value: string) {
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
}
diff --git a/web/src/pages/admin/plugins/plugins-page.tsx b/web/src/pages/admin/plugins/plugins-page.tsx
index ebe7d3d09..2d42eb998 100644
--- a/web/src/pages/admin/plugins/plugins-page.tsx
+++ b/web/src/pages/admin/plugins/plugins-page.tsx
@@ -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 }) {
);
}
+ if (pluginId === "official-payment-zhifufm") {
+ return (
+
+
+
+ );
+ }
return (