From c33fb469d910cd84234c26e8bce63788ad49c049 Mon Sep 17 00:00:00 2001 From: zhx Date: Thu, 25 Jun 2026 22:09:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=89=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/admin-openapi.json | 27 +++++++++ src/features/payment/api.ts | 26 ++++++--- .../payment/hooks/useThirdPartyPaymentPage.js | 31 ++++++++++ .../payment/pages/ThirdPartyPaymentPage.jsx | 57 ++++++++++++------- .../pages/ThirdPartyPaymentPage.test.jsx | 18 ++++++ src/features/payment/payment.module.css | 17 +++++- src/shared/api/generated/endpoints.ts | 8 +++ src/shared/api/generated/schema.d.ts | 34 +++++++++++ src/shared/api/types.ts | 20 +++++++ 9 files changed, 208 insertions(+), 30 deletions(-) diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index 16e2113..cfc7d3e 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -2739,6 +2739,33 @@ "x-permissions": ["payment-third-party:update"] } }, + "/admin/payment/third-party-methods/sync": { + "post": { + "operationId": "syncThirdPartyPaymentMethods", + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "providerCode": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "x-permission": "payment-third-party:update", + "x-permissions": ["payment-third-party:update"] + } + }, "/admin/payment/third-party-rates/{method_id}": { "patch": { "operationId": "updateThirdPartyPaymentRate", diff --git a/src/features/payment/api.ts b/src/features/payment/api.ts index d51ef44..4ec9073 100644 --- a/src/features/payment/api.ts +++ b/src/features/payment/api.ts @@ -10,6 +10,8 @@ import type { RechargeProductPayload, TemporaryPaymentLinkDto, ThirdPartyPaymentChannelDto, + ThirdPartyPaymentMethodSyncDto, + ThirdPartyPaymentMethodSyncPayload, ThirdPartyPaymentMethodDto, ThirdPartyPaymentRateSyncPayload, ThirdPartyPaymentRateSyncDto, @@ -75,13 +77,10 @@ export function listThirdPartyPaymentChannels(): Promise> { const endpoint = API_ENDPOINTS.listTemporaryPaymentLinks; - return apiRequest>( - apiEndpointPath(API_OPERATIONS.listTemporaryPaymentLinks), - { - method: endpoint.method, - query, - }, - ); + return apiRequest>(apiEndpointPath(API_OPERATIONS.listTemporaryPaymentLinks), { + method: endpoint.method, + query, + }); } export function getTemporaryPaymentLink(orderId: EntityId): Promise { @@ -122,6 +121,19 @@ export function updateThirdPartyPaymentRate( ); } +export function syncThirdPartyPaymentMethods( + payload: ThirdPartyPaymentMethodSyncPayload = { providerCode: "v5pay" }, +): Promise { + const endpoint = API_ENDPOINTS.syncThirdPartyPaymentMethods; + return apiRequest( + apiEndpointPath(API_OPERATIONS.syncThirdPartyPaymentMethods), + { + body: payload, + method: endpoint.method, + }, + ); +} + export function syncThirdPartyPaymentRates( payload: ThirdPartyPaymentRateSyncPayload = { markupPercent: 0 }, ): Promise { diff --git a/src/features/payment/hooks/useThirdPartyPaymentPage.js b/src/features/payment/hooks/useThirdPartyPaymentPage.js index 1d97e65..77e0265 100644 --- a/src/features/payment/hooks/useThirdPartyPaymentPage.js +++ b/src/features/payment/hooks/useThirdPartyPaymentPage.js @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { listThirdPartyPaymentChannels, + syncThirdPartyPaymentMethods, setThirdPartyPaymentMethodStatus, syncThirdPartyPaymentRates, updateThirdPartyPaymentRate, @@ -90,6 +91,25 @@ export function useThirdPartyPaymentPage() { setSyncRateDialogOpen(false); }; + const syncPaymentMethods = async (channel) => { + // 支付方式同步只开放给 V5Pay;MiFaPay 暂无可查询当前商户已开通方式的后端接口。 + if (!abilities.canUpdateThirdParty || channel?.providerCode !== "v5pay") { + return; + } + const actionKey = `sync-methods:${channel.providerCode}`; + setLoadingAction(actionKey); + try { + const summary = await syncThirdPartyPaymentMethods({ providerCode: channel.providerCode }); + const fresh = await result.reload(); + setRateDrafts(rateDraftsFromChannels(fresh?.items || [])); + showToast(paymentMethodSyncToast(summary), summary?.failedCountryCount ? "warning" : "success"); + } catch (err) { + showToast(err.message || "同步支付方式失败", "error"); + } finally { + setLoadingAction(""); + } + }; + const toggleMethod = async (method, active) => { // 权限判断放在动作入口,避免只依赖按钮禁用;接口失败时保留原状态并提示。 if (!abilities.canUpdateThirdParty) { @@ -172,6 +192,7 @@ export function useThirdPartyPaymentPage() { saveRate, setRateDraft, setSyncRateMarkupPercent, + syncPaymentMethods, submitSyncRates, syncRateDialogOpen, syncRateForm, @@ -188,3 +209,13 @@ function rateDraftsFromChannels(channels) { return drafts; }, {}); } + +function paymentMethodSyncToast(summary = {}) { + const created = summary.createdCount || 0; + const updated = summary.updatedCount || 0; + const failed = summary.failedCountryCount || 0; + const skipped = summary.skippedCount || 0; + const suffix = failed ? `,失败 ${failed} 个国家` : ""; + const skippedSuffix = skipped ? `,跳过 ${skipped} 个方式` : ""; + return `支付方式已同步:新增 ${created} 个,更新 ${updated} 个${skippedSuffix}${suffix}`; +} diff --git a/src/features/payment/pages/ThirdPartyPaymentPage.jsx b/src/features/payment/pages/ThirdPartyPaymentPage.jsx index f486221..7cb9237 100644 --- a/src/features/payment/pages/ThirdPartyPaymentPage.jsx +++ b/src/features/payment/pages/ThirdPartyPaymentPage.jsx @@ -53,7 +53,7 @@ export function ThirdPartyPaymentContent({ embedded = false, showProductButton =
{page.channels.map((channel) => ( - + + {channel.providerCode === "v5pay" ? ( + + ) : null} +
))}
@@ -196,12 +213,10 @@ export function ThirdPartyPaymentContent({ embedded = false, showProductButton = return {content}; } -function StatusPill({ active }) { - return {active ? "已启用" : "已停用"}; -} - function MethodLogo({ method }) { - const label = String(method.methodName || method.payType || method.payWay || "Pay").slice(0, 2).toUpperCase(); + const label = String(method.methodName || method.payType || method.payWay || "Pay") + .slice(0, 2) + .toUpperCase(); if (method.logoUrl) { return ; } diff --git a/src/features/payment/pages/ThirdPartyPaymentPage.test.jsx b/src/features/payment/pages/ThirdPartyPaymentPage.test.jsx index d35eb16..2996f0e 100644 --- a/src/features/payment/pages/ThirdPartyPaymentPage.test.jsx +++ b/src/features/payment/pages/ThirdPartyPaymentPage.test.jsx @@ -40,6 +40,23 @@ test("third-party payment rate dialog submits markup percent", () => { expect(page.submitSyncRates).toHaveBeenCalledTimes(1); }); +test("third-party payment page shows v5pay method sync button instead of status text", () => { + const v5pay = { providerCode: "v5pay", providerName: "V5Pay", status: "active", methods: [] }; + const page = pageFixture({ + activeChannel: v5pay, + channels: [{ providerCode: "mifapay", providerName: "MiFaPay", status: "active", methods: [] }, v5pay], + syncPaymentMethods: vi.fn(), + }); + vi.mocked(useThirdPartyPaymentPage).mockReturnValue(page); + + renderPage(); + + expect(screen.queryByText("已启用")).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "同步支付方式" })); + + expect(page.syncPaymentMethods).toHaveBeenCalledWith(v5pay); +}); + function renderPage() { return render( @@ -66,6 +83,7 @@ function pageFixture(overrides = {}) { saveRate: vi.fn(), setRateDraft: vi.fn(), setSyncRateMarkupPercent: vi.fn(), + syncPaymentMethods: vi.fn(), submitSyncRates: vi.fn((event) => event.preventDefault()), syncRateDialogOpen: false, syncRateForm: { markupPercent: "0" }, diff --git a/src/features/payment/payment.module.css b/src/features/payment/payment.module.css index 2d79019..be61812 100644 --- a/src/features/payment/payment.module.css +++ b/src/features/payment/payment.module.css @@ -131,9 +131,8 @@ border-radius: var(--radius-md); background: var(--bg-card); color: var(--text-primary); - cursor: pointer; gap: var(--space-2); - grid-template-columns: 24px minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr) auto; text-align: left; } @@ -143,6 +142,20 @@ background: var(--active-surface); } +.channelSelectButton { + display: grid; + min-width: 0; + align-items: center; + border: 0; + background: transparent; + color: inherit; + cursor: pointer; + gap: var(--space-2); + grid-template-columns: 24px minmax(0, 1fr); + padding: 0; + text-align: left; +} + .channelExpandIcon { display: inline-flex; align-items: center; diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index 044f9f8..f501a52 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -242,6 +242,7 @@ export const API_OPERATIONS = { startRobotRoom: "startRobotRoom", stopRobotRoom: "stopRobotRoom", syncPermissions: "syncPermissions", + syncThirdPartyPaymentMethods: "syncThirdPartyPaymentMethods", syncThirdPartyPaymentRates: "syncThirdPartyPaymentRates", updateAchievementDefinition: "updateAchievementDefinition", updateAppVersion: "updateAppVersion", @@ -1892,6 +1893,13 @@ export const API_ENDPOINTS: Record = { permission: "permission:sync", permissions: ["permission:sync"], }, + syncThirdPartyPaymentMethods: { + method: "POST", + operationId: API_OPERATIONS.syncThirdPartyPaymentMethods, + path: "/v1/admin/payment/third-party-methods/sync", + permission: "payment-third-party:update", + permissions: ["payment-third-party:update"], + }, syncThirdPartyPaymentRates: { method: "POST", operationId: API_OPERATIONS.syncThirdPartyPaymentRates, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index 73ca482..1ed50bf 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -1924,6 +1924,22 @@ export interface paths { patch: operations["setThirdPartyPaymentMethodStatus"]; trace?: never; }; + "/admin/payment/third-party-methods/sync": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["syncThirdPartyPaymentMethods"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/payment/third-party-rates/{method_id}": { parameters: { query?: never; @@ -6093,6 +6109,24 @@ export interface operations { 200: components["responses"]["EmptyResponse"]; }; }; + syncThirdPartyPaymentMethods: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + providerCode?: string; + }; + }; + }; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; updateThirdPartyPaymentRate: { parameters: { query?: never; diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index ff280b6..ec9d102 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -477,6 +477,26 @@ export interface ThirdPartyPaymentRateSyncPayload { markupPercent: number; } +export interface ThirdPartyPaymentMethodSyncPayload { + providerCode?: string; +} + +export interface ThirdPartyPaymentMethodSyncDto { + createdCount: number; + failedCountryCount?: number; + fetchedCountryCount?: number; + issues?: Array<{ + code?: string; + countryCode?: string; + currencyCode?: string; + message?: string; + }>; + providerCode?: string; + scannedCountryCount?: number; + skippedCount?: number; + updatedCount: number; +} + export interface TemporaryPaymentLinkDto { appCode?: string; audienceType?: string;