三方
This commit is contained in:
parent
2125f81baf
commit
c33fb469d9
@ -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",
|
||||
|
||||
@ -10,6 +10,8 @@ import type {
|
||||
RechargeProductPayload,
|
||||
TemporaryPaymentLinkDto,
|
||||
ThirdPartyPaymentChannelDto,
|
||||
ThirdPartyPaymentMethodSyncDto,
|
||||
ThirdPartyPaymentMethodSyncPayload,
|
||||
ThirdPartyPaymentMethodDto,
|
||||
ThirdPartyPaymentRateSyncPayload,
|
||||
ThirdPartyPaymentRateSyncDto,
|
||||
@ -75,13 +77,10 @@ export function listThirdPartyPaymentChannels(): Promise<ApiList<ThirdPartyPayme
|
||||
|
||||
export function listTemporaryPaymentLinks(query: PageQuery = {}): Promise<ApiPage<TemporaryPaymentLinkDto>> {
|
||||
const endpoint = API_ENDPOINTS.listTemporaryPaymentLinks;
|
||||
return apiRequest<ApiPage<TemporaryPaymentLinkDto>>(
|
||||
apiEndpointPath(API_OPERATIONS.listTemporaryPaymentLinks),
|
||||
{
|
||||
method: endpoint.method,
|
||||
query,
|
||||
},
|
||||
);
|
||||
return apiRequest<ApiPage<TemporaryPaymentLinkDto>>(apiEndpointPath(API_OPERATIONS.listTemporaryPaymentLinks), {
|
||||
method: endpoint.method,
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
export function getTemporaryPaymentLink(orderId: EntityId): Promise<TemporaryPaymentLinkDto> {
|
||||
@ -122,6 +121,19 @@ export function updateThirdPartyPaymentRate(
|
||||
);
|
||||
}
|
||||
|
||||
export function syncThirdPartyPaymentMethods(
|
||||
payload: ThirdPartyPaymentMethodSyncPayload = { providerCode: "v5pay" },
|
||||
): Promise<ThirdPartyPaymentMethodSyncDto> {
|
||||
const endpoint = API_ENDPOINTS.syncThirdPartyPaymentMethods;
|
||||
return apiRequest<ThirdPartyPaymentMethodSyncDto, ThirdPartyPaymentMethodSyncPayload>(
|
||||
apiEndpointPath(API_OPERATIONS.syncThirdPartyPaymentMethods),
|
||||
{
|
||||
body: payload,
|
||||
method: endpoint.method,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function syncThirdPartyPaymentRates(
|
||||
payload: ThirdPartyPaymentRateSyncPayload = { markupPercent: 0 },
|
||||
): Promise<ThirdPartyPaymentRateSyncDto> {
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ export function ThirdPartyPaymentContent({ embedded = false, showProductButton =
|
||||
<div className={styles.thirdPartyLayout}>
|
||||
<div className={styles.channelList}>
|
||||
{page.channels.map((channel) => (
|
||||
<button
|
||||
<div
|
||||
key={channel.providerCode}
|
||||
className={[
|
||||
styles.channelRow,
|
||||
@ -63,22 +63,39 @@ export function ThirdPartyPaymentContent({ embedded = false, showProductButton =
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
type="button"
|
||||
onClick={() => page.toggleChannel(channel.providerCode)}
|
||||
>
|
||||
<span className={styles.channelExpandIcon}>
|
||||
{page.expandedChannel === channel.providerCode ? (
|
||||
<KeyboardArrowDownOutlined fontSize="small" />
|
||||
) : (
|
||||
<KeyboardArrowRightOutlined fontSize="small" />
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.channelText}>
|
||||
<span>{channel.providerName || channel.providerCode}</span>
|
||||
<span>{channel.providerCode}</span>
|
||||
</span>
|
||||
<StatusPill active={channel.status === "active"} />
|
||||
</button>
|
||||
<button
|
||||
className={styles.channelSelectButton}
|
||||
type="button"
|
||||
onClick={() => page.toggleChannel(channel.providerCode)}
|
||||
>
|
||||
<span className={styles.channelExpandIcon}>
|
||||
{page.expandedChannel === channel.providerCode ? (
|
||||
<KeyboardArrowDownOutlined fontSize="small" />
|
||||
) : (
|
||||
<KeyboardArrowRightOutlined fontSize="small" />
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.channelText}>
|
||||
<span>{channel.providerName || channel.providerCode}</span>
|
||||
<span>{channel.providerCode}</span>
|
||||
</span>
|
||||
</button>
|
||||
{channel.providerCode === "v5pay" ? (
|
||||
<Button
|
||||
disabled={
|
||||
!page.abilities.canUpdateThirdParty ||
|
||||
page.loadingAction === `sync-methods:${channel.providerCode}`
|
||||
}
|
||||
startIcon={<SyncOutlined fontSize="small" />}
|
||||
onClick={() => page.syncPaymentMethods(channel)}
|
||||
>
|
||||
{page.loadingAction === `sync-methods:${channel.providerCode}`
|
||||
? "同步中"
|
||||
: "同步支付方式"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -196,12 +213,10 @@ export function ThirdPartyPaymentContent({ embedded = false, showProductButton =
|
||||
return <AdminListPage>{content}</AdminListPage>;
|
||||
}
|
||||
|
||||
function StatusPill({ active }) {
|
||||
return <span className={active ? styles.statusActive : styles.statusDisabled}>{active ? "已启用" : "已停用"}</span>;
|
||||
}
|
||||
|
||||
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 <img alt="" className={styles.methodLogo} src={method.logoUrl} />;
|
||||
}
|
||||
|
||||
@ -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(
|
||||
<MemoryRouter>
|
||||
@ -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" },
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<ApiOperationId, ApiEndpoint> = {
|
||||
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,
|
||||
|
||||
34
src/shared/api/generated/schema.d.ts
vendored
34
src/shared/api/generated/schema.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user