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,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAA27UlEQVR42u2deXhURbr/v3X6dHe6sxIS0gkGAhgmIGFRMAgJBhQZRURQRsAZWXRcZ7t3Bi4MTBi4MKMyz8x15ucyIoiGsIoRUEQUDZiEICJhkyUmAoGks0HW7vRyTv3+6IRBWZJKuquX1Od5fHxIuk5VnXS9tb3f9yUIABYvXqx3Op3dnE5nqKqqwYSQEEKIgVKqB6CjlMqUUgmAxtttFfgFCiFEJYQ4AdgJITZKqZVS2ihJUpMsyw2yLF9evny5zdsN7SzE2w1gJSMjQ7Lb7VEOhyMaQDSlNJJSamj9fVhYmCE+Pj48KioqOCwsLDQ0NDRcp9PpNBqNXpZlrSRJftdnAT9UVaVOp9OhKIrNbrfbGxoa6urr6xuqq6ubSktL6+rr662tnyWEWAkhlwBUabXaKp1OV71s2TLV231gwS8Gw6JFiww2my2eUhpHKY2hlMoGg0GXnJwc369fv77R0dG9w8PDTUajMU6n04UR4hfdEvgZlFLY7fZ6i8VSVldXZ66qqjpXXFxccuzYsVKr1WonhDgJIRWEkDK9Xl+6YsUKqxuq9Sg+O1IWL16ss1qtCQB6q6oaLcuyPHTo0D6DBw8eZjKZ+oeHhydqtVrZ2+0UCBwOh7Ourq7IbDafOXr06OHCwsLvnU6nU5KkKgDnDAbD2eXLl9u93c7r4XMGYMGCBdEOhyNRVdVeOp1On5KSMmDIkCGjYmNjhxkMhhBvt08gaAur1dpYXl5++MiRI/kHDhw4abfbbZIknddqtUUvvvhilbfbdzU+YQAyMjKI1WqNV1V1gKqqUYmJiXFjxoxJT0hIGB0cHBzu7fYJBB2lqamp7uzZs3n79u3LKSoqKpMkqVqSpJMGg6F02bJl1Nvt86oBWLJkCSwWSy9FUQYDiBg1alTyyJEjH4iNjR0kSZK3341A4DYURYHZbD5eUFCwMz8//xiAWo1Gc9RoNJ5funSp19rlNQMwf/78aEVRRgCITE9PHzFy5Mgp3bt3jxcHeIJAhlKKmpqa0oKCguycnJyDAC5pNJqDL7/8sle2BtxH26JFi/TNzc3DVFXtN3LkyOT09PTp0dHRvcXAF3QlKKWoqqo6l5OTs7GgoOCYJEnFQUFBh1esWMHVt4DbqMvIyCAWi+VWVVWHmkwm05QpU2b27dt3pFjqC7oyqqqipKSkIDs7e73ZbDZLklRoNBq/43U+wMUALFy4MNhms6XKshwzadKkCXfccccUg8FgcMOjBYKAwGq1Wg8dOpS9Y8eOT5xOZ4Ver8/961//2uTpej1uAObNm3eLoih3JSUl3Tp58uTnevToEe/pOgUCf6WysrJ027Ztr586deo7jUazf+XKlRc8WZ/HDEBGRoZksViGKYqSNHHixHtTU1Nn6vV6nSc7IxAEAjabzZ6bm7v+o48++kyj0ZwyGo2HPeVi7BFxzMKFC4OtVuu4kJCQpNmzZz89fPjwSbIsCyGOQNAOZFnW9OnTZ2ifPn1iT58+XdHY2Bidnp5enpub63B3XW4flPPnzw93OBz3JiUlJc+ePXthfHx8kjjhFwjYIIQgKioqPjk5+c6qqqpzFRUV4WlpaeV5eXluvSVwqwGYN29etKIo99x5553DH3300fnh4eER3N+cQBBAGI3GkAEDBoxqamq6UFpaakxNTa3Kz8+3uOv5bjMA8+bN6+l0OtPHjRs3ZuLEib82GAx6r701gSCA0Gq12sTExBRVVWuKi4vltLS0y/n5+Q3ueLZbDMC8efP6OJ3O1EmTJv103LhxT+p0OqHSEwjcSMu5wHCDwWA9deqUMy0trTE/P7+2s8/ttAFomflTp02bNnXUqFEzZFkWG36BwANoNBrSq1evIRERETh+/HhjWlrapc6uBDplAObNmxftdDrTJ02a9NNRo0bN0GjEQb9A4EkkSUJcXNzAoKCgplOnTtnT0tIqOnMm0OERO3/+/HBFUe4ZN27cmHHjxj0pZn6BgA+SJOGWW25JVlW1sqSkhKSlpV3s6O1AhwzAwoULgx0Ox7133nnn8IkTJ/5a7PkFAr5oNBrSu3fvYQ0NDWdLS0t16enppR3xE2A2ABkZGZLVah2XlJSU/Oijj84Xp/0CgXeQZVnTt2/fO8rLy7+tqKjQjR8/vmTv3r1MIiJmKZ7FYhkWGhoaP3Xq1N8ajUYh6BEIvIjRaDRMnTr1t6GhofEWi2UYa3mmFUCLsGf4nDlzno6Pj0/yducFAoHLWSguLi7q4MGD37f4CNS3t2y7DcDChQtDHA7H2IkTJ04YPnz4JOHeKxD4DpGRkfFarbb+zJkzzenp6edzc3PbFYW4XQagJWjn2KSkpEGTJk36tRD2CAS+BSEEPXv2HHjx4sVjFRUVcst5QJvl2nUGYLFYbpVlOWby5MnPCkmvQOCb6PV63eTJk5+VZTnGYrHc2p4ybc7kixYt0jscjrsnT5784IABA1K93UmBQHBjgoODww0Gg/XkyZO148aNK/7yyy+Vm32+zRVAc3Pz7SaTyXTHHXdM8XbnBAJB29xxxx1TTCaTqbm5uc1bgZsagPnz50erqtp3ypQpM0UMP4HAPzAYDIYpU6bMVFW13/z586Nv9tkbGoAlS5ZAUZQRd911V3Lfvn1HertTAoGg/fTt23fkyJEjkxVFGbFkyZIbfu6GBsBisfQGEHn33XdPF6G7BQL/QpIkpKenTwcQ2TKWr/+56/0wIyODKIqSPHbs2BHR0dG9IRAI/I7o6OjeY8eOHaEoSnJGRsZ1HXeuawCsVms8gIiUlJQpwuFHIPBPCCFISUmZAiCiZUxfw3UNgKqqA0aPHp3cvXt3EcNfIPBjunfvHj9q1KhkVVUHXO/31xiABQsWRKuqGpWSkvKAmP0FAv+GEIKRI0c+oKpq1IIFC665EbjGADgcjsTExMQ4k8k0yNuNFwgEncdkMg1KTEyMczgciT/+3Q8MwOLFi3WqqvYaM2bMWBHeSyAIDDQaDcaMGZOuqmqvxYsX/8CV/wcGwGq1Juh0On1CQsIobzdaIBC4j4SEhNE6nU5vtVoTrv75j7cAvVNSUgYEBweHe7vBAoHAfQQHB4enpKQMAPCDa/0rBmDRokUGVVWjhwwZImZ/gSAAGTJkyChVVaMXLVp0xa3/igGw2WzxsizLsbGxzGGFBAKB7xMbGztMlmXZZrNdud6/YgAopXFDhw7tYzAYQrzdUIFA4H4MBkPI0KFD+1BK41p/JqEl0i+lNGbw4MFi9hcIApjBgwcPo5TGZGRkSGg1AHa7PYpSKptMpv7ebqBAIPAcJpOpP6VUttvtUWg1AA6HI9pgMOjCw8MTO12DQCDwWcLDwxMNBoPO4XBE46ozgOjk5OR4rVYrMvwIBAGMVquVk5OT4wH8xwBQSiP79evX19uNEwgEnqdfv359KaWRACAtXrxYTyk1CN2/QNA1iI6O7kUpNSxevFgvOZ3ObnDtDWK83TCBQOB5wsPDYwHA6XR2k5xOZyhc6YV6erthAoHA8xiNxji4DECopKpqcFhYmEGn04V5u2ECgcDz6HS6sLCwMIOqqsESISQkPj4+XAT/EAi6BoQQtIz5EIkQYoiKigr2dqMEAgE/oqKiggkhBolSqg8LCxPLf4GgCxEWFhZGKdVLAHShoaHCAAgEXYiWMa+TKKWyTqfTe7tBAoGAHzqdTk8plSVKqaTRaETKb4GgC6HRaHSUUkkCoJFlWevtBgkEAn60jHmNSxMsSeIOUCDoQrSOeaH+8wHee+89HDhwgFt9Wq0Wv/3tbxET037v73oFWHwRqHF46SV1URQVWNUPCPVQfl5hALzM2bNn8corr6C5uRmUUi51ajQa9O/fH7Nnz253mTANMCAIeL4SgFgvcmNOdyDEg8m5Rd5vL6KqKjIzM2G1WrkNfgBQFAWZmZkoKytjKjc3Gkg2MBURdIIQAvxvT8/aW2EAvMiJEyewb98+r9RdV1eHDRs2QFGUdpfRE+D3JrFs5IEE4HcxQJyHj+eFAfASqqpiy5YtuHz5stfa8NFHH+G7775jKjOtGzDc6LUmdxniZOCpaM/vtoQB8BL79+/Hrl27vNqGpqYmvPvuu3A6ne0uY5SAP8UBwnHEcxAK/DoG6MXhJQsD4AUsFgvWrl0LVVW92g5FUbB7924UFhYylbs3DHggHAC/Y4suxYAg4FkOsz+EAfAOeXl5OHbsmLebAbjiQWLNmjWw2+3tLqMjwPxYIFwkkHY7WrhWWGGc3q0wAJxpamrCm2++ybTs9jRffvkl8vPzmcqMMAI/ixSrAHczMgSYGMGvPmEAOLNnzx4UFRV5uxk/QK/X45133oHFYml3GZkAL/Tw7B11V0MH4PcxnnP6uR7iz8eRy5cvIzMzE7LsexdpJ0+eRG5uLlOZQQbg8ShvtzxwSAsF7uMszBcGgCPbt29HcXGxt5txXZxOJ7Zs2YLGxsZ2l9EAWGgCwsS3qNPIABbHAgbO71L86ThRXl6O9evXQ5J895UfOnQIH3/8MVOZXjrgNz3EF6mzPBwBjPJCXm7xd+MApRSbNm1CdXW1t5tyUyRJwoYNG1BTU9PuMgTAb2KAWCEo7zBGAizr6bpd4Y0wABw4ffo0NmzYAH+IvHzx4kVs3bqVSZsQJQP/HSM0Qh3lZ5HAT4K8U7cwAB7G6XRi7dq1TD733kRRFHz88ceorKxsdxkC4OlooJ8ILMeMDsCf47w3EIUB8DBHjx5FQUGBt5vBRGlpKbKysphWASES8EeTd5ax/gqBa+UU70W/amEAPIjT6cT69euZTtZ9hezsbHz//fdMZX4WKeTCLMRqXOpKbw5CYQA8SF5eHnJycrzdjA7R3NyMNWvWMHksBkuua0GxE2gbDYDfmIDuXnYJEQbAQzQ2NuLtt9/2djM6DKUUn332GbPX4kMRQHqot1vv+/TXu85NvL1jEgbAA1BK8emnn+LMmTPebkqncDqd+Ne//sUkFNISICMOEDuBGyNRl+Cnmw+IqYQB8ACNjY3Izs5mGji+ytdff42DBw8ylRkeDEzsJoRCNyI1BJjSzdutcCEMgJuhlOKjjz7yGblvZ1FVFW+++Saam5vbXUZHgD9wFrX4C3oC/CoGCPL22r8F8SdyM5cuXcKGDRug0fjA+s5NnDhxAvv372cqc4cRmMRR1uoXUGB0SEswFR9BGAA3QinF5s2bcfHiRW83xe1kZWWhoaGh3Z+XCfDHWCEXvhotAeabXLclvoIPNcX/uXjxIjZv3uztZniE48ePIy8vj6lMkgGYLeTCV3i4GzDWx25IfE+Y7qe0xtpnmSU7i9FohKIosNlsHq/L4XDgtddew8iRIxER0b61fatceMMloMZDAZAIgASd60S9vWeOBMBZO3CJo3d2UMuKyNc8JYUBcBPHjx/HJ598wq2+0NBQLF++HJs3b0Z+fr7HE4sQQlBeXo7s7GzMnj273cKmWC3wfDSwohzwRAjUaBn4rD/Ql8H7SAUwpwR4l1NEdgJgZiQw2AfvRsUWwA3Y7XasXr2aq8tvSkoKRowYgVmzZsHh4JOwj1KKHTt2oLy8vN1lCFw3Ap6QC0sA/mBiG/wAcLoZyOKYjiFUAyz2ouDnZvhim/yOr7/+GocOHeJWn06nw8yZM6HT6TB48GCkp6dzSy128eJF7Nixg6m+MI0r2o27/QISdMB0xvt0B3UlOeWpzXw6Cujto4kUhAHoJHa7HRs3buSyD2/lwQcfxKBBgwBXnnc888wzMBj4rC8VRcE777zDfNMxqztwuzubSIHfdkBJ940F+KiOy6sCAMRogAVeFvzcDF9tl9+wd+9e5mCanSEyMhIzZ878QWix/v37Y/z48dzaYLPZsGrVKqYYBwbJJX5xl1BokAGYw3jDYKPA0jLX/3mgocCiWO8Lfm6GMACdoL6+HqtXr+Ya5++hhx5C7969f/AzjUaDWbNmITSUzx0TIQRffPEFjh8/zlTusW7AXcFuqL9lYLF6Gu5rAD6t5/KKAAD9gnw/arIwAB2EUoqdO3fi7NmzXOojhCAmJgbTp0+/7gl8QkICpkyZwu0swGazYd26dUyrgCAJyOgJyJ1s4sgQdi/DJhVYUgY4Oc7+801ApI87hAoD0EHq6+uxfft2rhl+Zs6ciaioG08p06dPR79+/bi0RVEU7NmzhzmvYFoIMLkTLsISdQXQZPWme78W+LoJ3PS3dwUDj3fnU1dnEAagg2RnZ+PUqVNc6iKEIC4uDpMmTbrp53r06IEJEyZw25JIkoS3336b6RpSJsALMUBwBwfiPWHAGMbw2ZcV4B9mgM9lqUvw87wPCX5uhjAAHcBsNmPz5s3cBprdbsdTTz2FsLC208Y89NBDiI2N5fYuvvrqK+a8gqNDgJ92ILuwvoPhszdfAo5Zub0SjAwGHvQTIZQwAIxQSrFx40amqLmdgRCCoUOH4t57723X56OjozFnzhxu6cdUVcXatWthtbZ/hOmIyzFGz/Lto8CsSOBOxkPEGifwl3KA10ZN0+Kc5C9SaD9ppu9w/vx5bNmyhVt9hBDMmjWL6Z5/7NixSEpK4paH4MSJE9izZw9TmSGM13ixMjAvlu0LqwJ4pQI4zzEuy4PhwP2c8/t1BmEAGHA4HFi9ejVXp59hw4Zh9OjRTGXCw8Mxc+ZMqKonvO+vRVVVbNy4EXV17fewIQD+K8bly9+ezz7RgbwDJTbgzWp+B38GySX40fjB3r8VYQAYOHXqFPNM1xlUVcXTTz8NvZ7dfSY1NRWDBg3itgo4efIk3n//faZryP564JftCIzZUwu8wBhAkwJ4tQKo4ndJg0e6ucKh+RPCALQTu92O119/nevsP378eAwbNqxDZY1GI55++mlubSWEYOvWrcxnI7+OBkw3WwVQV/JRVpffYhvw72rPKBCvR3BLYhR/G1D+1l6vcfDgQXz99dfc6gsODsa0adM6ddMwYsQIpKSkcGtzeXk5s1DIpHUdmt3oRuDWIFf4bBZUAMvKACvHoKSzugNJXsrv1xmEAWgHFosFWVlZ3PbUaFnCd3T2b0Wn0+HZZ5/l1ubWVUBFRQVTuTlRru3ANc8D8OseQDijN92XjcBWjnLfaA2wMNb7Mf47gjAA7aCgoIDZ460zmEwmzJ071y2BRQcMGICJEydy81moqKjAm2++yWQsu2mAP/d0efldTXIQMJfRl95Jgf9XwXH2b1El3uKn6dGFAWiDuro6vPbaa9xi/BNCcP/997vNpVej0WD27NkID+cTilaSJOzatYs5KcqDEcAQ43/+LQNYEsceVHRPA/BBLb+UBD/RA88wblF8CWEA2mDXrl0oLS3lVp/JZMKUKVPc+szevXvjwQcf5NYHm82GzMxMJqFQqORS+GlaRu6YUGAiozedjQJ/5ej0IwH4rQmI8mG5b3v6ILgBdXV12LZtG9MXuTM4nU5MnToVcXFxbn2uJEmYPn06IiMjufSDEILc3FycPn2aqdwD4S6xkB7An2Jdrr8sfFAL5HJMxDwoCJjB55V6DGEAbkJ2djbzl7ijEEKQkJCAyZMne+T5MTExmDFjBreEJU1NTXjttdeYg4Zk9AQeCHOlz2KhXgH+ZuYY6osC82OBCB+X+7aFMAA3oKKiAps2beLmSGO32/HEE094dJa+//773b66uBmHDh3CV199xVRmVAjw914u1SAL6y+1yH05cVcwMNVPBD83QxiA69Dq2lpdXc2tzv79++O+++7zaB0xMTGYOnUqt6AhTqcT7777LlNeQT0BEhgdHy8rwN/K+bn8aolr728IgNETAF1wP+fPn0d2dja3gUIpxTPPPIOQELZ1r1Vli3BDCMHDDz/MbRVAKcW3336Lffv2ea4OAK9XAsUcBT+pwZ0LauJLCAPwIxRFwapVq7jG+L/99tuRmprKVEYF8Nw54Hj7J1egJaHICy+8wPUs4K233mISCrFw3u669+c5+y/t6R/BPtqDMAA/4syZM/j000+51jl37lwEBbH5keY2ANtqgX9Wsh98paamYsCAAdz6V1JSgt27d3tkRfWvSqCc070fAfBwhOumIlAQBuAqHA4H3njjDW5LfwC48847ceeddzKX+78KoE4FttawR7sJCQnB448/zjWe4VtvvYXLl93rn/u9DVhbxW/2D9MAv4/hUxcvhAG4isLCQuTm5nIzAOHh4fjlL3/JvBzfWQ98VO/a/9ZT1/UXa7TbtLQ0jBw5ktstR2VlJbZv3+62d0sBvGgGavjJMzA5nD0ika8jDEALzc3NWLNmDbcBQQjBXXfdhcGDBzOVs1FXok37VeNo22VgP+MVWFBQEJ577jluZwGyLGPz5s1MeQVvxjcWIKuGS9MBAN1agn0EyNb/CsIAtJCXl4dvvvmGW31GoxFPPPEE8wDcVgsc+tFgb6TAkouuvHcs3Hbbbbj33nu5rHgopTCbzfjggw86XZ+DAv9bBlg4Cn6ejAJ+4ody37YQBqAlxv+GDRu4ufxqNBr87Gc/Q//+/ZnKNbQkt7heaqucBiC7lq0dsixj1qxZ7Yo27K5+r1u3rtPJVPY3uUQ/vMZ/vNYV5jsQEQYAwCeffMJd7ttWjP8fQwG8Uw2cusGBHyXAsosuI8FC3759PeZ+fE0bKYXVasXq1as7bGxtLdl9G3nt/VXgdyagj49m9+0sXd4AVFdX4/333+dWn6qquP/++9GrVy+mchUO4GXzzU+8T9qA9Yz74tbVCC+hkEajQV5eHk6ePNmh8h/VAXkcBT+36P1f8HMzurwB+Pzzz/H9999zqYsQgqioKEydOpWpHAXwTg1woQ1vNxXASjP77BgXF4cpU6Zwi3hUW1uLb7/9lrlckwr8pYxfnD+05Pcz+Wmwj/bQpQ1AVVUVVq1axe0+XFVVzJ49Gz169GAqV2oH3qh0LfPbotgO/JMtIhcA4NFHH8Utt9zi8XfQmuSU1fMRcIX5OmTxeBOvMCAI+Hn3wDv5v5ouawBUVUVmZqbbnVNuBCEEffv2xcSJE5nL/rMSOMvg6/5aFXCBMRFeVFQUHnnkEY+HDnM4HJgxYwazHqHSCbzIUfAjwyVN7ubnct+26LIGoLi4GB9++CG3+hwOB+bOncscmutbK7Cqku2LX+ZwrQJYlsqEEEyePNnjeQXj4+Px0EMPMZd7txo4yah76AyDja4sP4FOlzQAqqpi3bp1aGriJyAfPHgw0tPT2drZ4u1Wz7jppQDW1bCnxIqIiMCsWbM8dhagKAqeeOIJdOvWjancxRaDxmv21xEgI5Y9HqE/0gW6eC1nzpzBjh07uN37y7KMZ555him/HwAUNAHbazv2xS93uJbMrD2cOHEiBg4c6JH3kJSUhJ/+9KfM5V6tAEp55fYGMC4UmNgFZn90RQPQKvflFSYbLYKf4cOHM5d7udwl+OkQBNhwGTjBKBTS6/V4/PHH3Z5dWFEU/OIXv2COeVBkA96t4Sj4kVzxCFkjEvkrXc4AHDx4EPv37+dWnyzLePLJJ5nz+33eAHxc37m6653AvyrYVwHp6en4yU9+4tb3MHDgQNxzzz3MWot/VbrONHgxPgwYGUBy37boUgbAbrfjnXfe4XbtRwjBvffe2yHBz+KLPxT8dKwBQNYl4ADjUYfBYMBzzz0Ho9HIVvAGBAUF4amnnmI2gketLu/H9lx/uoNwAvyPH+b36wxdqa/Iz8/H119/zc3hJTQ0FI8//jjzrLflEnDQTffdVupyEW5mNCa33347xowZ4xahUEdSnLfm92M9AO0wFJge6X/ZfTtLlzEAjY2NeOedd7jG+Rs/fjzzUtqiAi9XsOv7b8an9cDnjNsJnU7XoT379Z4zZ84c6HRszvSf1QOfNrjvHbSFSQYWxQW208/16DIGYOfOnTh69Ci3+uLi4jBr1izmw8a3a4BjbvZ2UwnwpwsuV1oWbr31VkyYMKHDKyaNRoOJEycyJzlVAfy9AmjgFeSfAk93IAV5INAlDMDly5fx3nvvcQ32cd999zF7u1U5gb97yNvtGyuwhdHpUaPRYMaMGcz39q1ERERg6tSpzO99ey3wST0/uW9vLfBLP87v1xkC3gBQSrFz585Oa9BZ6NatGx555BG2draceH/vqRNvArxiBi4xnn/27t0bU6dOZV4FUEqRlpaGpKQkpnIWFfiLe4IGtbOhwHMxQM8AFvzcjIA3AK1yX14Hfw6HA4899hjz7H/RAayr8uysd8QKbGJcBUiShGnTpqF79+7tLkMIgdFoxMyZM5ln/221wDccM/z00blm/662928loA0ApRSZmZnc5L4A0K9fP0ybNo2tnXDd13ts9m+th7jkwhWMq4Do6Gg89thjTOHLHnvsMfTp04epnjoFeKkMUHiNxpb8foEu+LkZAW0AiouLkZ2dzc3rj1KKOXPmMIfYOmIF3uIU3vp7m8vYsKw0CCGYOnUqevbs2a7PmkwmTJs2jXn2f7vG9S54QACMDgFmBbjcty0C1gAoioKsrCxYrZy+US1BNlkFPwBwuhlo4nXiRVyutacYlXXdunXD9OnT2/yc3W7H9OnTmWMeVDiAf5Tz+0bqCPCbmMDI79cZArb7JSUl2LNnD7f6CCGYM2cOQkNDmcs+GgHcw9H99KLD5SHI2r9JkyYhPj7+pp+Lj4/HAw88wDz7r6oCznN0+R0dEjj5/TpDQBoARVHw6quvcpX7pqamYtSoUR0qqyHAb0xACKe1qNqisDvOuDgyGAx46qmnbqhqbN0CRUSwjayzduDfHDP86Ajw5zhXJuKuTkAagMOHD2P//v3c7v11Oh1+/vOfM/u6X016KDAhHNwuv+tUV6wBVl+bsWPH3jCc+cCBAzFhwgTm9/5qJXsEo45CAEyJAFK7kODnZgScAXA4HHjzzTe5XftRSjFmzBgMHTq0U8/RE1cASgOnWYkCyL7EnlfQaDRi1qxZ1wxyQgjmzp3L7Dp83AqsreY3+4dKwG96dO2Dv6sJOAPQmuGHl89/SEgIZs+e7ZYUW8OMwIz2X7d3GguAJRfYMwqNGjUK99xzz5V/E0IwYsQI5kCfFMD/lgPVvFx+ATwW2bXkvm0RUAagqakJb7/9Ntc6J0yYgMTERLc8S0uA/4kFIjneS2+vA75gFN3IsowZM2Zcme2DgoLw5JNPQqtlc6fLaQB21PHra2t+v4D60neSgHoXOTk5HU440RGCg4MxY8YMtybYvFUPzIniuEQlwNIywMq4Y7rtttswbtw4KIqCUaNGMW+Bminwkpm93g5DgV9FAwldUPBzMwLGANTV1XHd+2u1WjzyyCPM3m5tIQH4bQ8gwr0RuW5KfgOwm1Eu3LoKMBgMHdoC7W1wrQB40VMLzGVzTegSBIQBoJRix44duHjxIrc6IyIi8PDDD3vk2bfogD/EcFwFSMDyMqCWcS/er18//PGPf+yQ4GfpDZKcegQKPNsD6CVm/2sICANgNpuxZcsWrnW2xymmoxAAL3Bern5tAd7rgFDo/vvvZ772y64FDvIU/OiBZ6MD5MvuZvz+nVBKkZ2djdLSUm51du/eHTNmzPCon0G4Blgc68pQw4UWoVANo1CIdenfqAL/LAf4RGUEJAosigWiOG6p/Am/NwDnzp1DVlYWN8GPVqvF888/zxwk49SpUzCbzUxlpkcCyWypBDrFmWZgVbVn69h0CfiKY36/kSHAzzoWz6RL4NcGwOl0Yt26dbBY+H2jBg4ciLFjxzK384033sC6deuYDimNErA4DtBwFAq9XgWU2Dzz+Eon8Ncyft86PXFd+4V2YblvW/i1ATh58iQ+//xzt17D3QxZljFt2jRmue+hQ4dw+PBh7N69mzky0aRwYAxHx5ULdldyUU/wVpUrezEvRgYD97L9qbocfmsAKKV477330NjYyK2+QYMGdWj237RpEywWC2pra5Gdnc3kpaglwJJbAB2nVYAKYH0NUOLmgVruAF5nTHLaGTQAMoTgp0381gB888032LVrF7d7/6CgIDz33HPMgp+8vDzs27cPlFKoqoqtW7fiu+++Y3pGihEYyzFXXbkTWFHGll24LV6pcMmQeTE5gu/KyV/xSwPgcDiwZs0absk9WwU/ycnJTOWsVitWr179g5/ZbDasXr2aqe1BLS6sRo6z2abLruSk7qDIBrzJMcNPCFxy366S368z+KUByM3NxZEjR7jVFxQUhCeeeII9ucVnn+HMmTM/+BkhBPv378c333zD9KzUEGAqxwAWTSrwL3PnE5RQuJx+LnMU/DzaHbiN4+2JP+N3BqC5uRlvvfUWmpsZY1p1EEII7rnnnhtq4G9EY2MjNm7ceN08hFarFRs3bmRaBUgAfm8CunO8z97YgbyCP6agiT0ScWcIIULww4JfvSdKKT799NNrZlVPEhISglmzZjHfNOzatQvffvvtdX+nqipyc3ORl5fH9MzBRpdvAE8X4T9d6Lhgx0Zdcl93pjlri+ejXYIqQfvwKwNQV1eHrKwsblp/ABg/fjwSEhKYytTW1iIrK+umRqMjQUsluDQCPFcBOY2u3IIdKtsA5HQyxTkLPSTgD7Ei2AcLfmMAKKXYtm0biouLudUZGRmJp556imn2b81EdP78+TY/e+TIEXz66adMbUrQufzauYXOJ8DKcpeAhwU7BV42u7IT84BQ4PkY4fLLit8YgKqqKmzatInb7K/RaDBp0iRER7MljauqqkJWVla7dAJOpxOZmZmor2ebJn/DWdmW1wR8UMtWZlstsI+j3DdBBzwvQn0x4xcGQFVVbNu2DeXl/JLGdUTw0+qcVFZW1u4yxcXF2LVrF5Nhi5aBeSZ+AUQpAf5a3v6T/HoFeJmj4EcDYGGc670I2PALA3D+/Hls2rSJm8svpRSzZs1iyocHABcuXMDWrVuZ2ilJEtatW4eqKjb/219EArdzvOo60Qy83U6h0Ie1QCFHwc+oEOAxIfjpED5vABRFwbvvvovqag/L1K5i6NCheOihh5hmf1VVsWHDBtTVsQe5O3/+PDZs2MC0CgjTuPLa8foDUgD/VwmUteHNV68Ay8oAJ6e1uJ4A/x3jeh8CdnzeAJw+fRqff/45ZJnP+k6n0+HRRx+9YfKLG1FUVIStW7d2qE5ZlrF9+3bmmAYPRgAjjVxeCwDgot2lFryRmaJwJfg47SE14fUYHQLcJwQ/HcanDQClFJs2beIq+ElKSsKYMWOYyjmdTrz99tudck2ura3Fli1bmFYBwS1yYV5nASqAd6pdrr3Xo9LpWiXwOonTwhU0xejT32LfxqdfXWFhIT7++GNu9bUmtwgODmYqd/z4ceTm5naqbkmSsGvXLpSUlDCVuzcMeIjjDFjqAP5hvtbmUAD/rADMHAU/Pw137f8FHcdnDYDD4cCqVau4Cn7Gjx+PlJQU5nZmZWV12jWZUoqamhrmPmsJ8CuOGYUAYF3NtYd8Z+3AvyrdqyC8ab8BrOgp5L6dxWcNQKvgh1d+v+DgYEyfPp05uUVBQQE+//xzt7RBkqQrmY1YuDsUeICjXLixJa9ga0YhBa6owg2cRj8BMDtSCH7cgU8aAJvNhnfffRd2O5/wMZRS3H333bjtttuYyjU1NWHt2rVuNVLNzc3IzMxkinOgI67gF914nYQT4KNLwNEWL+ZvLMB6xnTjnUFPgP8Wgh+34JPvcPfu3Th69Cg3rz9ZlvHEE08w3zTs37//hoKfjkIpRUFBAfLz85nKJRtcee94rYibiEsoZKfAX8pcmX54Mbc7kBjEr75Axud8pxoaGrBmzRpuS38ASExMRFlZGZMzjqIo+OCDD2C3293eVlVVsXr1agwfPhxBQe37phMAv+7hctnldRD3SQPw+wvAZxxdfqM0wNKeLu8/QefxKQNAKcX27du5ZvgBgDNnzmDBggXM5ZxOp8cM1YkTJ7B3715MmDCh3WUGBgEzI4G/V3j0dV1BBfD/KvnUhZYv6ws9+KohAx2f2gLU1NRg06ZN3OL8taKqKhwOB/N/ntyitK4CGhrYptc/xAC92c4x/YYEvcsAiIN/9+EzBoBSis2bN3PN8OPrnDlzBjt37mQyNLFa4L84CoV4oaHAQpMQ/LgbnzEAZWVlzEKaQEer1SIzMxOXLrEdsf+cc0YhHtwZ7IqGJHAvPmEAVFXFW2+9hZqaGm83xaeglKK8vJx5W9RdBv5gAgjfnZTH0BPgdybh8usJfOKVFhUVcRX8+BOSJGHr1q24cOECU7kp3YBkjkIhTzI6BJjI0dGpK+F1A6AoCjZu3Mh82NVVoJTi0qVL2Lp1K9NZQKgE/Lkn+PnmeghCgQUml/BJ4H68/lpPnDiBnJwcbtl9/RGNRoMPP/wQ586dYyr30zBgrJ9LZSeGA+mh3m5F4OLVUedwOPDqq68yx8Trily+fBmvvvoq0yrAIAFLegJ6P70RkAEsinMJngSewasGYN++fSgsLOTq9eevtAqFDh06xFRuVDAwIcI/785nRAIj2JTZAka8ZgAsFgvWr1/PTe4bCNjtdmRmZjKJpLTEFTTE307QdS3ZfcWlsGfx2tfiiy++4JrhJ1A4evQoDhw4wFRmhBF43M/u0OdEAf1Ehh+P4xUDYLPZ8MEHHzBlxRG4aGhowOrVq5ml0n8wASY/uWUNl4D/ERl+uOAVA7Br1y4cPnzY2333W06cOIHdu3czlblVz1cu3GGoS9WYwDHxSVeGuwGor6/H+vXrvd1vv4ZSiqysLNTWtj9dDwHwuxggxsdXAX11wG9i/MBQBQjcDcCuXbtw9uxZb/fb7ykpKcFnn33GVCZBB/zKh9V0GgDPxgjBD0+4GoC6ujpkZ2eLk383oCgK1q5dy6yfmBMF9PfRw7WfBAFzo7zdiq4FVwPw8ccf4+TJk97uc8Bw8eJFbN++nUkoFKcF/ivG9+TChALzTUB3ce/HFW4GoKamBuvWrROCHzei0Wjw3nvvMSdNnR4JJPtYTL07goFHRH4/7nAxAJRSfPjhhzCbzd7ub8BRXl7OnFEoXOOKqkt8ZBWggetsIsTPnJUCAS6vvKysDFu2bPF2XwMSjUaDHTt2MGcUerQbkOojWXXuMIrZ31t43ABQSvHee+9xze7blaCUoq6uDuvXr2c6CwiRXK623t6QkRaXXzH7ewcJrog8HlsMlpWVYfPmzXA6nd7ua0Czfft2FBYWMpVJDwUmR3i33emhwAQ/lyz7I61jXgKgOJ1Oj0SSVxQFa9asgc3GMV90F4VSyuwiLBNgnheDbRgkYFGsqx0CvrSMeUUihKiKonhkhJ47dw579uzxdl+7DEeOHMFXX33FVGa4EZjqpVXAvaEi2Ie3UBTFRghRZUKI0+6BJHyqquKVV14RgT45YrPZ8O9//5spo5CGuGbhHbVALcddmhbA4lgh9/UWdrvdTghxygDsDQ0Nbg/JQwjB0qVLsXTpUm/3tcuh07EpaW4NAs4k8w0fKAGI8vYJZBemoaGhDoBdJoTY6j0Qk4sQgogIL58wCdqFBsL/vqtRX1/fQAixSZRSa3V1dZO3GyQQCPhRXV3dRCm1SpTSxtLS0jpeqbgFAoF3oZSiZcw3SpIkNdXX11vtdrsIzSsQdAHsdnt9fX29VZKkJkmW5Qa4gnSWebthAoHA87SOdVmWGyRZli/DpdVnk5QJBAK/pK6uzgyXAbgsLV++3EYIsVZVVZ33dsMEAoHnqaqqOkcIsS5fvtwmwXVld6m4uJhNTiYQCPyS4uLiEkLIJVylBqw6duxYqcPhEIodgSCAcTgczmPHjpUCqEKrAdBqtVVWq9VeV1dX5O0GCgQCz1FXV1dktVrtWq32PwZAp9NVE0KcZrNZpOoRCAIYs9l8hhDi1Ol01Wg1AMuWLVMJIRVHjx4V2ToEggDm6NGjhwkhFcuWLVNxdUQgQkhZYWHh91artdHbjRQIBO7HarU2FhYWfk8IueLzc8UA6PX6UqfT6SwvLxerAIEgACkvLz/sdDqder2+tPVnVwzAihUrrJIkVR05ciTf2w0VCATu58iRI/mSJFWtWLHiSlbeHweDOnfgwIGTTU1Ndd5urEAgcB9NTU11Bw4cOAng3NU//4EBMBgMZ+12u+3s2bNiFSAQBBBnz57Ns9vtNoPB8IPEnD8wAMuXL7dLknR+3759X4j8fQJBYKAoCvbt25cjSdL55cuX/yD83zXxYLVabVFRUVGZ2Ww+7u2GCwSCzmM2m48XFRWVabXaaxz9rjEAL774YpUkSdUFBQU7RZAQgcC/oZSioKBgpyRJ1S+++GLVj39/3YjwkiSdzM/PP1ZTU1ParloEAoFPUlNTU5qfn39MkqTrpuW+rgEwGAylAGoLCgqyxSpAIPBPWmb/bAC1LWP6Gq5rAJYtW0Y1Gs3RnJycg1VVVefarEkgEPgcVVVV53Jycg5qNJqjy5Ytu+5MfsOkUEaj8TyASzk5ORtZkk4KBALvo6oqcnJyNgK41DKWr8sNDcDSpUuh0WgOFhQUHCspKSnwdocEAkH7KSkpKSgoKDim0WgO3iw5z03TQr788stVkiQVZ2dnr7darVYIBAKfx2q1WrOzs9dLklT88ssvV93ss23mhQ0KCjpsNpvNhw4dyvZ2xwQCQdscOnQo22w2m4OCgtoU9rWZm/HLL79URo8e7SgqKrINGTJkeHBwcLi3OygQCK5PZWVl6dq1a1cB+Pqll16qbOvz7coMbzQav3M6nRXbtm17w2azuT2TsEAg6Dw2m82+bdu2151OZ4XRaPyuPWXalZ157969SE9PN1dUVHTX6/XNffr0GUoI8XZ/BQJBC5RS7N27N3P//v0H9Hr9nhUrVrRrom53evbc3FxHampq/enTp9U+ffrERkVFxXu70wKBwMWZM2f2b9iwYZMsy7kvvfRSTXvLtdsAAEB+fn59amqq7vTp0xXJycl3Go3GEG93XCDo6tTU1FSsXr36706n89jf/va30yxl23UGcDVGo/FwQ0ND6fvvv/+KxWIRV4MCgRexWCzW999//5WGhoZSo9HIHM6PaQUA13kATU9PL6+oqAhvamq6kJiYmCLLMvNzBAJB57DZbI7t27f/48iRI0f0ev0X7d33X02HBm5ubq4jLS2tvLS01Kiqak2fPn2GazQacSooEHDC4XCou3fv/ndeXl6BLMufvfTSSw0deU6HZ+68vDxbampqVXFxsWwwGKy9evUaIknMOwqBQMCIoij48ssvMz/55JPPZVn+YuXKlZc6+qxOLd3z8/MtaWlpl0+dOuWMiIhAXFzcQGEEBALPoSgKvvrqq63Z2dkfyrK8b+XKlebOPK/Te/f8/PyGtLS0xuPHjzcGBQU13XLLLcliOyAQuB+Hw6F++eWXmS2Df//KlSvPd/aZbjm8y8/Pr01LS7t06tQpu6qqlb179x4mDgYFAvdhs9kcu3fv/nfLsn+fOwY/3GUA8J+VQEVJSQlpaGg427dv3zu0Wq3Wq29NIAgALBaLdfv27f9oOfD7orPL/qtx6yzdciZwsbS0VFdeXv5tQkLCIOEsJBB0nJqamorNmze/fOTIkSOyLH/WmQO/6+H2ZXpeXp4tPT29tKKiQldYWHgoLi4uKjIyMl5oBwSC9kMpRVFR0f7Vq1f/vays7LRer/+io1d9N8Mj+/Tc3FzH+PHjSywWCw4ePPi9Vqut79mz50BxLiAQtI3NZrPv3bs3c8OGDZucTuex4ODgvI44+bQHj0/L8+bNu0VRlLuSkpISJ0+e/GyPHj2EiEgguAGVlZWl27Zte+PUqVNFGo1m/8qVKy94sj6Pz8j5+fn16enp5ysqKuQDBw58ZTAYrNHR0beKA0KB4D9YrVbrgQMHtqxdu3ZVZWVlkV6v//yll16q9nS93DbmGRkZxGKx3Kqq6lCTyWSaMmXKzL59+44UjkOCroyqqigpKSnIzs5ebzabzZIkFRqNxu9uFMbb3XA/mVu0aJG+ubn5dlVV+951113Jd9999/To6Oje4pBQ0JWglLbG7d9YUFBwTJKkkqCgoG9WrFhh49kOr426+fPnRyuKMgJA5NixY0ekpKRM6d69u7gtEAQ0lFLU1NSUHjhwIPuLL744COCSRqM52Fb0Xk/h1dG2ZMkSWCyW3oqiJAOIGD16dHJKSsoDJpNpkEYjLgwEgYOiKDCbzccPHDiwMy8v7xiAWo1Gc8xoNJ67Wdx+T+MT021GRgaxWq3xqqoOUFU1KjExMW7MmDFjExISRokoxAJ/pqmpqe7s2bP5+/bt+6KoqKhMkqRqSZJOGgyGUl77/JvhEwbgahYsWBDtcDgSVVXtpdPp9CkpKQOGDBkyKjY2dpjBYBBehQKfx2q1NpaXlx8+cuRI/oEDB07a7XabJEnntVpt0fVSdHsTnzMArSxevFhntVoTAPRWVTValmV56NChfQYPHjzMZDL1Dw8PT9RqtbK32ykQOBwOZ11dXZHZbD5z9OjRw4WFhd87nU6nJElVAM4ZDIazy5cv98lw+j5rAK5m0aJFBpvNFk8pjaOUxlBKZYPBoEtOTo7v169f3+jo6N7h4eExRqOxp06nCxMHiQJPQCmF3W6vt1gsF+vq6iqqqqrOFRcXlxw7dqzUarXaCSFOQkgFIaRMr9eXrlixwudjZvrdSMnIyJDsdnuUw+GIBhBNKY2klBpafx8WFmaIj48Pj4qKCg4LCwsLDQ0N0+l0eo1Go5NlWStJkt/1WcAPVVWp0+l0KIpit9vttoaGhvr6+vr66urqptLS0rr6+vorg5oQYiWEXAJQpdVqq3Q6XfWyZcv8KpV2QAyGxYsX651OZzen0xmqqmowISSEEGKglOoB6CilMqVU4uH5KAgIFEKISghxArATQmyUUiultFGSpCZZlhtkWb68fPlyrnf2nuD/A5HKaHrEnC50AAAAAElFTkSuQmCC"; + +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 ( + + 支付FM + + ); + } + 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 ( + + 支付FM + + ); + } return (