From b7b4dd006643cb563fa054c7550bdb46517f048f Mon Sep 17 00:00:00 2001 From: kvtodev Date: Mon, 13 Jul 2026 13:52:40 +0800 Subject: [PATCH 01/30] extract mud point wallet entry component for reuse in tauri app --- .../PlatformMudPointWalletEntry.test.tsx | 5 +++-- .../src/components}/PlatformMudPointWalletEntry.tsx | 7 +++---- .../shared/src/icons}/topbar-wallet.png | Bin .../common/platformMudPointWalletModel.ts | 10 ---------- .../image-editor/ImageCanvasTopbarView.test.tsx | 3 --- .../image-editor/ImageCanvasTopbarView.tsx | 2 +- .../platform-entry/PlatformProfileRechargeModal.tsx | 2 +- .../rpg-entry/RpgEntryHomeView.recharge.test.tsx | 3 --- src/components/rpg-entry/RpgEntryHomeView.tsx | 2 +- 9 files changed, 9 insertions(+), 25 deletions(-) rename {src/components/common => packages/shared/src/components}/PlatformMudPointWalletEntry.test.tsx (97%) rename {src/components/common => packages/shared/src/components}/PlatformMudPointWalletEntry.tsx (97%) rename {public/creation-home => packages/shared/src/icons}/topbar-wallet.png (100%) delete mode 100644 src/components/common/platformMudPointWalletModel.ts diff --git a/src/components/common/PlatformMudPointWalletEntry.test.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx similarity index 97% rename from src/components/common/PlatformMudPointWalletEntry.test.tsx rename to packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx index 1156a20d4..08d3011d9 100644 --- a/src/components/common/PlatformMudPointWalletEntry.test.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx @@ -4,8 +4,9 @@ import { act, fireEvent, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect, test, vi } from 'vitest'; -import { PlatformMudPointWalletEntry } from './PlatformMudPointWalletEntry'; -import { formatMudPointCount } from './platformMudPointWalletModel'; +import { PlatformMudPointWalletEntry } from './PlatformMudPointWalletEntry.tsx'; + +import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; const breakdown = { totalPoints: 207, diff --git a/src/components/common/PlatformMudPointWalletEntry.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx similarity index 97% rename from src/components/common/PlatformMudPointWalletEntry.tsx rename to packages/shared/src/components/PlatformMudPointWalletEntry.tsx index 6f2b0f218..d4aec9a44 100644 --- a/src/components/common/PlatformMudPointWalletEntry.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx @@ -8,10 +8,9 @@ import { useState, } from 'react'; -import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; -import { formatMudPointCount } from './platformMudPointWalletModel'; - -const MUD_POINT_ICON_SRC = '/creation-home/topbar-wallet.png'; +import type { ProfileMudPointBalance } from '@/packages/shared/src'; +import MUD_POINT_ICON_SRC from '@/packages/shared/src/icons/topbar-wallet.png'; +import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; export type PlatformMudPointWalletEntryProps = { balance: number | null; diff --git a/public/creation-home/topbar-wallet.png b/packages/shared/src/icons/topbar-wallet.png similarity index 100% rename from public/creation-home/topbar-wallet.png rename to packages/shared/src/icons/topbar-wallet.png diff --git a/src/components/common/platformMudPointWalletModel.ts b/src/components/common/platformMudPointWalletModel.ts deleted file mode 100644 index 0236383c0..000000000 --- a/src/components/common/platformMudPointWalletModel.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function formatMudPointCount(value: number, compact = false) { - const normalizedValue = Math.max(0, Math.round(value)); - if (compact && normalizedValue >= 100_000_000) { - return `${(normalizedValue / 100_000_000).toFixed(1)}亿`; - } - if (compact && normalizedValue >= 10_000) { - return `${(normalizedValue / 10_000).toFixed(1)}万`; - } - return normalizedValue.toLocaleString('zh-CN'); -} diff --git a/src/components/image-editor/ImageCanvasTopbarView.test.tsx b/src/components/image-editor/ImageCanvasTopbarView.test.tsx index 1bbf80ec6..c9eb10d4b 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.test.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.test.tsx @@ -108,9 +108,6 @@ describe('ImageCanvasTopbarView', () => { }); expect(walletChip.textContent).toBe('泥点 1.2万'); - expect(walletChip.querySelector('img')?.getAttribute('src')).toBe( - '/creation-home/topbar-wallet.png', - ); await user.hover(walletChip); const details = screen.getByRole('dialog', { name: '泥点账户详情' }); diff --git a/src/components/image-editor/ImageCanvasTopbarView.tsx b/src/components/image-editor/ImageCanvasTopbarView.tsx index 72fa0e4ab..49607699a 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.tsx @@ -8,7 +8,7 @@ import { } from 'lucide-react'; import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; -import { PlatformMudPointWalletEntry } from '../common/PlatformMudPointWalletEntry'; +import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; diff --git a/src/components/platform-entry/PlatformProfileRechargeModal.tsx b/src/components/platform-entry/PlatformProfileRechargeModal.tsx index 26964cc30..329dc3a0f 100644 --- a/src/components/platform-entry/PlatformProfileRechargeModal.tsx +++ b/src/components/platform-entry/PlatformProfileRechargeModal.tsx @@ -8,7 +8,6 @@ import type { import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; -import { formatMudPointCount } from '../common/platformMudPointWalletModel'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; @@ -16,6 +15,7 @@ import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { formatRechargePrice } from '../rpg-entry/rpgEntryProfileFundsViewModel'; import { PlatformProfileModalShell } from './PlatformProfileModalShell'; import type { NativeWechatPaymentState } from './usePlatformProfileCenterController'; +import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180; export type PlatformProfileRechargeModalProps = { diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index d0298e996..789e0e466 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -4235,9 +4235,6 @@ test('logged in create tab shows real wallet balance beside the brand', () => { expect(walletEntry?.className).toContain( 'platform-mobile-create-wallet-chip', ); - expect(walletButton.querySelector('img')?.getAttribute('src')).toBe( - '/creation-home/topbar-wallet.png', - ); expect( within(topbar as HTMLElement).getByRole('button', { name: '充值' }), ).toBeTruthy(); diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 78b66671a..2a662dfe7 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -89,7 +89,7 @@ import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFilterToolbar } from '../common/PlatformFilterToolbar'; import { PlatformIconButton } from '../common/PlatformIconButton'; -import { PlatformMudPointWalletEntry } from '../common/PlatformMudPointWalletEntry'; +import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { From 9831571ddad991fef75965281979c3a52c3984ee Mon Sep 17 00:00:00 2001 From: kvtodev Date: Mon, 13 Jul 2026 14:03:02 +0800 Subject: [PATCH 02/30] fix resolve res in Vitest --- vitest.config.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index 87ae74b8b..30f506d44 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,14 @@ +import path from 'node:path'; + import {defineConfig} from 'vitest/config'; export default defineConfig({ + resolve: { + alias: { + // Keep shared components' application-root imports resolvable in tests. + '@': path.resolve(__dirname, '.'), + }, + }, test: { environment: 'node', globals: true, @@ -12,6 +20,7 @@ export default defineConfig({ 'miniprogram/**/*.test.js', 'scripts/**/*.test.ts', 'packages/shared/src/**/*.test.ts', + 'packages/shared/src/**/*.test.tsx', ], }, }); From 9c982992a7db24633da4cb7ab42e28d9e5caaa8b Mon Sep 17 00:00:00 2001 From: kvtodev Date: Mon, 13 Jul 2026 14:18:21 +0800 Subject: [PATCH 03/30] update import paths for PlatformMudPointWalletEntry --- src/components/image-editor/ImageCanvasTopbarView.tsx | 2 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/image-editor/ImageCanvasTopbarView.tsx b/src/components/image-editor/ImageCanvasTopbarView.tsx index 49607699a..f61a51064 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.tsx @@ -8,7 +8,7 @@ import { } from 'lucide-react'; import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; -import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; +import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 2a662dfe7..82cc8ced7 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -89,7 +89,7 @@ import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFilterToolbar } from '../common/PlatformFilterToolbar'; import { PlatformIconButton } from '../common/PlatformIconButton'; -import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; +import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry'; import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { From 071dac67c1016c308cd875eea652f2a744869ab3 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Mon, 13 Jul 2026 14:46:24 +0800 Subject: [PATCH 04/30] fix import extension for formatMudPointCount in PlatformMudPointWalletEntry --- packages/shared/src/components/PlatformMudPointWalletEntry.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx index d4aec9a44..3b519f9b7 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx @@ -10,7 +10,7 @@ import { import type { ProfileMudPointBalance } from '@/packages/shared/src'; import MUD_POINT_ICON_SRC from '@/packages/shared/src/icons/topbar-wallet.png'; -import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; +import { formatMudPointCount } from '@/packages/shared/src/utils/format'; export type PlatformMudPointWalletEntryProps = { balance: number | null; From c038275e112c56ee74fe6b6f299415a4d85137b4 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Mon, 13 Jul 2026 15:49:34 +0800 Subject: [PATCH 05/30] add back moved code to git --- packages/shared/src/utils/format.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/shared/src/utils/format.ts diff --git a/packages/shared/src/utils/format.ts b/packages/shared/src/utils/format.ts new file mode 100644 index 000000000..0236383c0 --- /dev/null +++ b/packages/shared/src/utils/format.ts @@ -0,0 +1,10 @@ +export function formatMudPointCount(value: number, compact = false) { + const normalizedValue = Math.max(0, Math.round(value)); + if (compact && normalizedValue >= 100_000_000) { + return `${(normalizedValue / 100_000_000).toFixed(1)}亿`; + } + if (compact && normalizedValue >= 10_000) { + return `${(normalizedValue / 10_000).toFixed(1)}万`; + } + return normalizedValue.toLocaleString('zh-CN'); +} From a3034b1d5109d3bf320ccbe1643977975f670360 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Mon, 13 Jul 2026 18:08:46 +0800 Subject: [PATCH 06/30] fix import --- .../shared/src/components/PlatformMudPointWalletEntry.test.tsx | 3 +-- packages/shared/src/index.ts | 2 ++ src/components/image-editor/ImageCanvasTopbarView.tsx | 2 +- src/components/platform-entry/PlatformProfileRechargeModal.tsx | 2 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx index 08d3011d9..25070c10a 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx @@ -4,10 +4,9 @@ import { act, fireEvent, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect, test, vi } from 'vitest'; +import { formatMudPointCount } from '../index'; import { PlatformMudPointWalletEntry } from './PlatformMudPointWalletEntry.tsx'; -import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; - const breakdown = { totalPoints: 207, permanentPoints: 100, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 23bbb0c45..47ac81b61 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -53,3 +53,5 @@ export type * from './contracts/visualNovel'; export * from './http'; export * from './llm/narrativeLanguage'; export * from './llm/parsers'; +export * from './utils/format'; +export * from './components/PlatformMudPointWalletEntry'; diff --git a/src/components/image-editor/ImageCanvasTopbarView.tsx b/src/components/image-editor/ImageCanvasTopbarView.tsx index f61a51064..113059241 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.tsx @@ -8,7 +8,7 @@ import { } from 'lucide-react'; import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; -import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry'; +import { PlatformMudPointWalletEntry } from '@/packages/shared/src'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; diff --git a/src/components/platform-entry/PlatformProfileRechargeModal.tsx b/src/components/platform-entry/PlatformProfileRechargeModal.tsx index 329dc3a0f..deb8f3f87 100644 --- a/src/components/platform-entry/PlatformProfileRechargeModal.tsx +++ b/src/components/platform-entry/PlatformProfileRechargeModal.tsx @@ -15,7 +15,7 @@ import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { formatRechargePrice } from '../rpg-entry/rpgEntryProfileFundsViewModel'; import { PlatformProfileModalShell } from './PlatformProfileModalShell'; import type { NativeWechatPaymentState } from './usePlatformProfileCenterController'; -import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; +import { formatMudPointCount } from '@/packages/shared/src'; const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180; export type PlatformProfileRechargeModalProps = { diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 82cc8ced7..6f17ea58e 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -89,7 +89,7 @@ import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFilterToolbar } from '../common/PlatformFilterToolbar'; import { PlatformIconButton } from '../common/PlatformIconButton'; -import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry'; +import { PlatformMudPointWalletEntry } from '@/packages/shared/src'; import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { From e6d0ccc24313217454754bbee59276ee144dfc77 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Mon, 13 Jul 2026 18:48:58 +0800 Subject: [PATCH 07/30] use `import { formatMudPointCount } from '@genarrative/shared'` style --- packages/shared/package.json | 6 +++++- .../src/components/PlatformMudPointWalletEntry.test.tsx | 2 +- .../shared/src/components/PlatformMudPointWalletEntry.tsx | 6 +++--- src/components/image-editor/ImageCanvasTopbarView.tsx | 2 +- .../platform-entry/PlatformProfileRechargeModal.tsx | 2 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 2 +- tsconfig.json | 3 +++ vite.config.ts | 1 + vitest.config.ts | 1 + 9 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/shared/package.json b/packages/shared/package.json index 61866ae7c..455134555 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -2,5 +2,9 @@ "name": "@genarrative/shared", "private": true, "version": "0.1.0", - "type": "module" + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "types": "./src/index.ts" } diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx index 25070c10a..01edad980 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx @@ -4,7 +4,7 @@ import { act, fireEvent, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect, test, vi } from 'vitest'; -import { formatMudPointCount } from '../index'; +import { formatMudPointCount } from '@genarrative/shared'; import { PlatformMudPointWalletEntry } from './PlatformMudPointWalletEntry.tsx'; const breakdown = { diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx index 3b519f9b7..4128cd48e 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx @@ -8,9 +8,9 @@ import { useState, } from 'react'; -import type { ProfileMudPointBalance } from '@/packages/shared/src'; -import MUD_POINT_ICON_SRC from '@/packages/shared/src/icons/topbar-wallet.png'; -import { formatMudPointCount } from '@/packages/shared/src/utils/format'; +import type { ProfileMudPointBalance } from '../index'; +import MUD_POINT_ICON_SRC from '../icons/topbar-wallet.png'; +import { formatMudPointCount } from '../utils/format'; export type PlatformMudPointWalletEntryProps = { balance: number | null; diff --git a/src/components/image-editor/ImageCanvasTopbarView.tsx b/src/components/image-editor/ImageCanvasTopbarView.tsx index 113059241..606ab3fb6 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.tsx @@ -8,7 +8,7 @@ import { } from 'lucide-react'; import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; -import { PlatformMudPointWalletEntry } from '@/packages/shared/src'; +import { PlatformMudPointWalletEntry } from '@genarrative/shared'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; diff --git a/src/components/platform-entry/PlatformProfileRechargeModal.tsx b/src/components/platform-entry/PlatformProfileRechargeModal.tsx index deb8f3f87..1d535ec70 100644 --- a/src/components/platform-entry/PlatformProfileRechargeModal.tsx +++ b/src/components/platform-entry/PlatformProfileRechargeModal.tsx @@ -15,7 +15,7 @@ import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { formatRechargePrice } from '../rpg-entry/rpgEntryProfileFundsViewModel'; import { PlatformProfileModalShell } from './PlatformProfileModalShell'; import type { NativeWechatPaymentState } from './usePlatformProfileCenterController'; -import { formatMudPointCount } from '@/packages/shared/src'; +import { formatMudPointCount } from '@genarrative/shared'; const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180; export type PlatformProfileRechargeModalProps = { diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 6f17ea58e..7d00dae74 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -89,7 +89,7 @@ import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFilterToolbar } from '../common/PlatformFilterToolbar'; import { PlatformIconButton } from '../common/PlatformIconButton'; -import { PlatformMudPointWalletEntry } from '@/packages/shared/src'; +import { PlatformMudPointWalletEntry } from '@genarrative/shared'; import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { diff --git a/tsconfig.json b/tsconfig.json index fb623bd18..42f06c521 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,9 @@ "paths": { "@/*": [ "./*" + ], + "@genarrative/shared": [ + "./packages/shared/src/index.ts" ] }, "allowImportingTsExtensions": true, diff --git a/vite.config.ts b/vite.config.ts index afe9b24a4..fdfa8ade2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -53,6 +53,7 @@ export default defineConfig(({mode}) => { resolve: { alias: { '@': path.resolve(__dirname, '.'), + '@genarrative/shared': path.resolve(__dirname, 'packages/shared/src/index.ts'), }, }, optimizeDeps: { diff --git a/vitest.config.ts b/vitest.config.ts index 30f506d44..df50756c6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ alias: { // Keep shared components' application-root imports resolvable in tests. '@': path.resolve(__dirname, '.'), + '@genarrative/shared': path.resolve(__dirname, 'packages/shared/src/index.ts'), }, }, test: { From 5244dbfcd8ab3452778733115054faa45cc190fe Mon Sep 17 00:00:00 2001 From: kvtodev Date: Tue, 14 Jul 2026 10:55:22 +0800 Subject: [PATCH 08/30] rollback: all in relative path now --- packages/shared/package.json | 6 +----- .../src/components/PlatformMudPointWalletEntry.test.tsx | 2 +- .../shared/src/components/PlatformMudPointWalletEntry.tsx | 2 +- src/components/image-editor/ImageCanvasTopbarView.tsx | 2 +- .../platform-entry/PlatformProfileRechargeModal.tsx | 2 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 2 +- tsconfig.json | 3 --- vite.config.ts | 1 - vitest.config.ts | 1 - 9 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/shared/package.json b/packages/shared/package.json index 455134555..61866ae7c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -2,9 +2,5 @@ "name": "@genarrative/shared", "private": true, "version": "0.1.0", - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "types": "./src/index.ts" + "type": "module" } diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx index 01edad980..e6aca12a9 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx @@ -4,7 +4,7 @@ import { act, fireEvent, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect, test, vi } from 'vitest'; -import { formatMudPointCount } from '@genarrative/shared'; +import { formatMudPointCount } from '../utils/format'; import { PlatformMudPointWalletEntry } from './PlatformMudPointWalletEntry.tsx'; const breakdown = { diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx index 4128cd48e..41d017a5d 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx @@ -8,7 +8,7 @@ import { useState, } from 'react'; -import type { ProfileMudPointBalance } from '../index'; +import type { ProfileMudPointBalance } from '../contracts/runtime'; import MUD_POINT_ICON_SRC from '../icons/topbar-wallet.png'; import { formatMudPointCount } from '../utils/format'; diff --git a/src/components/image-editor/ImageCanvasTopbarView.tsx b/src/components/image-editor/ImageCanvasTopbarView.tsx index 606ab3fb6..37f5f1092 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.tsx @@ -8,7 +8,7 @@ import { } from 'lucide-react'; import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; -import { PlatformMudPointWalletEntry } from '@genarrative/shared'; +import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; diff --git a/src/components/platform-entry/PlatformProfileRechargeModal.tsx b/src/components/platform-entry/PlatformProfileRechargeModal.tsx index 1d535ec70..12d3baa5e 100644 --- a/src/components/platform-entry/PlatformProfileRechargeModal.tsx +++ b/src/components/platform-entry/PlatformProfileRechargeModal.tsx @@ -15,7 +15,7 @@ import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { formatRechargePrice } from '../rpg-entry/rpgEntryProfileFundsViewModel'; import { PlatformProfileModalShell } from './PlatformProfileModalShell'; import type { NativeWechatPaymentState } from './usePlatformProfileCenterController'; -import { formatMudPointCount } from '@genarrative/shared'; +import { formatMudPointCount } from '../../../packages/shared/src/utils/format'; const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180; export type PlatformProfileRechargeModalProps = { diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 7d00dae74..70272c476 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -89,7 +89,7 @@ import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFilterToolbar } from '../common/PlatformFilterToolbar'; import { PlatformIconButton } from '../common/PlatformIconButton'; -import { PlatformMudPointWalletEntry } from '@genarrative/shared'; +import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry'; import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { diff --git a/tsconfig.json b/tsconfig.json index 42f06c521..fb623bd18 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,9 +19,6 @@ "paths": { "@/*": [ "./*" - ], - "@genarrative/shared": [ - "./packages/shared/src/index.ts" ] }, "allowImportingTsExtensions": true, diff --git a/vite.config.ts b/vite.config.ts index fdfa8ade2..afe9b24a4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -53,7 +53,6 @@ export default defineConfig(({mode}) => { resolve: { alias: { '@': path.resolve(__dirname, '.'), - '@genarrative/shared': path.resolve(__dirname, 'packages/shared/src/index.ts'), }, }, optimizeDeps: { diff --git a/vitest.config.ts b/vitest.config.ts index df50756c6..30f506d44 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,7 +7,6 @@ export default defineConfig({ alias: { // Keep shared components' application-root imports resolvable in tests. '@': path.resolve(__dirname, '.'), - '@genarrative/shared': path.resolve(__dirname, 'packages/shared/src/index.ts'), }, }, test: { From d307abf7a3a6edfcb469d327c49b93d0a2658a5e Mon Sep 17 00:00:00 2001 From: kvtodev Date: Tue, 14 Jul 2026 11:04:01 +0800 Subject: [PATCH 09/30] removed component export from index --- packages/shared/src/index.ts | 2 -- src/components/image-editor/ImageCanvasTopbarView.tsx | 2 +- src/components/platform-entry/PlatformProfileRechargeModal.tsx | 2 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 47ac81b61..23bbb0c45 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -53,5 +53,3 @@ export type * from './contracts/visualNovel'; export * from './http'; export * from './llm/narrativeLanguage'; export * from './llm/parsers'; -export * from './utils/format'; -export * from './components/PlatformMudPointWalletEntry'; diff --git a/src/components/image-editor/ImageCanvasTopbarView.tsx b/src/components/image-editor/ImageCanvasTopbarView.tsx index 37f5f1092..5b9e61ca9 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.tsx @@ -8,7 +8,7 @@ import { } from 'lucide-react'; import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; -import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry'; +import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; diff --git a/src/components/platform-entry/PlatformProfileRechargeModal.tsx b/src/components/platform-entry/PlatformProfileRechargeModal.tsx index 12d3baa5e..329dc3a0f 100644 --- a/src/components/platform-entry/PlatformProfileRechargeModal.tsx +++ b/src/components/platform-entry/PlatformProfileRechargeModal.tsx @@ -15,7 +15,7 @@ import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { formatRechargePrice } from '../rpg-entry/rpgEntryProfileFundsViewModel'; import { PlatformProfileModalShell } from './PlatformProfileModalShell'; import type { NativeWechatPaymentState } from './usePlatformProfileCenterController'; -import { formatMudPointCount } from '../../../packages/shared/src/utils/format'; +import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180; export type PlatformProfileRechargeModalProps = { diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 70272c476..9b5e9c06f 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -89,7 +89,7 @@ import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFilterToolbar } from '../common/PlatformFilterToolbar'; import { PlatformIconButton } from '../common/PlatformIconButton'; -import { PlatformMudPointWalletEntry } from '../../../packages/shared/src/components/PlatformMudPointWalletEntry'; +import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { From 8d7394eda10ee1812eb24a98b5cd2b0729e79756 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Tue, 14 Jul 2026 11:05:09 +0800 Subject: [PATCH 10/30] fix simple import --- src/components/image-editor/ImageCanvasTopbarView.tsx | 3 ++- src/components/platform-entry/PlatformProfileRechargeModal.tsx | 3 ++- src/components/rpg-entry/RpgEntryHomeView.tsx | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/image-editor/ImageCanvasTopbarView.tsx b/src/components/image-editor/ImageCanvasTopbarView.tsx index 5b9e61ca9..ba20c2e4c 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.tsx @@ -7,8 +7,9 @@ import { X, } from 'lucide-react'; -import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; + +import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; diff --git a/src/components/platform-entry/PlatformProfileRechargeModal.tsx b/src/components/platform-entry/PlatformProfileRechargeModal.tsx index 329dc3a0f..f5acb0023 100644 --- a/src/components/platform-entry/PlatformProfileRechargeModal.tsx +++ b/src/components/platform-entry/PlatformProfileRechargeModal.tsx @@ -1,6 +1,8 @@ import QRCode from 'qrcode'; import { useCallback, useEffect, useState } from 'react'; +import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; + import type { ProfileRechargeCenterResponse, ProfileRechargeProduct, @@ -15,7 +17,6 @@ import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { formatRechargePrice } from '../rpg-entry/rpgEntryProfileFundsViewModel'; import { PlatformProfileModalShell } from './PlatformProfileModalShell'; import type { NativeWechatPaymentState } from './usePlatformProfileCenterController'; -import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180; export type PlatformProfileRechargeModalProps = { diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 9b5e9c06f..1dd71dfb8 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -40,6 +40,8 @@ import { useState, } from 'react'; +import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; + import profileClockImage from '../../../media/profile/_Image (1).png'; import profileGamepadImage from '../../../media/profile/_Image (2).png'; import profileStillLifeImage from '../../../media/profile/_Image (3).png'; @@ -89,7 +91,6 @@ import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFilterToolbar } from '../common/PlatformFilterToolbar'; import { PlatformIconButton } from '../common/PlatformIconButton'; -import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { From b13d73c5cfd8e5d3b9bbebe18209fc6db09f5108 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Tue, 14 Jul 2026 11:11:11 +0800 Subject: [PATCH 11/30] sync doc --- docs/project-memory/shared-memory/project-overview.md | 2 +- docs/project-memory/shared-memory/team-conventions.md | 1 + docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md | 2 +- docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md | 2 +- docs/【项目基线】当前产品与工程约束-2026-05-15.md | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/project-memory/shared-memory/project-overview.md b/docs/project-memory/shared-memory/project-overview.md index fa617a0c1..c38f161ff 100644 --- a/docs/project-memory/shared-memory/project-overview.md +++ b/docs/project-memory/shared-memory/project-overview.md @@ -45,7 +45,7 @@ server-rs + Axum + SpacetimeDB - `spacetime-client`:后端访问 SpacetimeDB 的 typed facade。 - `module-*`:纯领域模型、命令、应用规则、领域事件和领域错误。 - `platform-*`:OSS、LLM、认证、语音等外部平台能力。 -- `shared-contracts` / `packages/shared`:前后端 DTO 和公开契约。 +- `shared-contracts` / `packages/shared`:前后端 DTO、公开契约和跨页面复用的无业务真相 TypeScript 代码;`packages/shared` 可承载共享 UI 组件与纯工具,但不承载领域规则、后端副作用或正式状态。 - 前端:表现、交互、临时 UI 状态和后端结果渲染。 明确废弃:旧 `server-node`、Express、PostgreSQL、Go 服务端、`maincloud`、人工 `spacetime --root-dir` 口径,以及前端承接正式业务真相的路线。 diff --git a/docs/project-memory/shared-memory/team-conventions.md b/docs/project-memory/shared-memory/team-conventions.md index ef7364769..d2fe77662 100644 --- a/docs/project-memory/shared-memory/team-conventions.md +++ b/docs/project-memory/shared-memory/team-conventions.md @@ -47,6 +47,7 @@ - 新增 Markdown 文档时,文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;只在任务需要时重命名历史文档,避免无关大 diff。 - 涉及中文文本时注意 UTF-8 编码和乱码排查。 - 涉及后端时遵循 DDD 分层,不把业务真相下沉到前端或临时兼容层。 +- `packages/shared` 用于前后端 DTO、公开契约及跨页面复用的无业务真相 UI 组件和纯工具;不得把领域规则、后端副作用或正式状态放入其中。 - `maincloud` / `Maincloud` / `MAINCLOUD` 相关代码、脚本、测试、环境变量、命令和文档要求均视为历史残留,禁止新增、运行或引用;API smoke 统一使用 `npm run dev:api-server` 与 `/healthz`。 - 涉及 SpacetimeDB 表结构、发布或迁移时,先看 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` 和 `SPACETIMEDB_TABLE_CATALOG.md`。 - 涉及生产发布、服务器配置、Jenkins Job 重建或回滚时,先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。 diff --git a/docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md b/docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md index a2d523728..d93111563 100644 --- a/docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md +++ b/docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md @@ -66,7 +66,7 @@ RAG 默认不安装运行时依赖,也不把 LanceDB、Transformers.js 或本 - 领域规则沉到 `module-*`;SpacetimeDB 表、reducer、procedure、事务 adapter 和 row mapper 留在 `spacetime-module`。 - 后端访问 SpacetimeDB 统一经 `spacetime-client` facade。 - HTTP / SSE / BFF 和外部副作用编排留在 `api-server`;OSS、LLM、认证、语音等外部平台能力留在 `platform-*`。 -- 前后端 DTO 和公开契约留在 `shared-contracts` / `packages/shared`。 +- 前后端 DTO、公开契约和跨页面复用的无业务真相 TypeScript 代码留在 `shared-contracts` / `packages/shared`;共享 UI 组件与纯工具可留在 `packages/shared`,但领域规则、后端副作用和正式状态不得下沉。 - 契约、路由、DTO 去留和 breaking change 以当前后端架构文档、`api-server/src/app.rs`、`shared-contracts` 和 `packages/shared` 为准。 后端修改后按当前 DDD 文档执行验收。涉及 API smoke 时,使用 `npm run dev:api-server` 重新拉起后端并检查 `/healthz`;不要使用旧 `maincloud` 启动口径。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 42818aef7..656e33f91 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -34,7 +34,7 @@ SpacetimeDB 版本口径:当前 Rust crate `spacetimedb`、`spacetimedb-sdk` 3. SpacetimeDB 表结构、reducer、procedure 和持久化 row shape 留在 `spacetime-module`。 4. 后端访问 SpacetimeDB 必须经 `spacetime-client` facade。 5. HTTP 鉴权、BFF 聚合、SSE、外部模型编排、OSS 上传和第三方回调在 `api-server`。 -6. 前端共享 DTO 通过 `shared-contracts` 和 `packages/shared` 对齐,不在页面内重新发明旧接口。 +6. 前端共享 DTO 通过 `shared-contracts` 和 `packages/shared` 对齐,不在页面内重新发明旧接口;`packages/shared` 还可复用无业务真相的 UI 组件和纯工具,领域规则、后端副作用与正式状态仍留在各自分层。 7. 微信能力按两层收口:`server-rs/crates/platform-wechat` 承载微信协议 client、订阅消息 `stable_token` / `subscribeMessage.send`、微信支付 V3 / 虚拟支付消息推送的 HTTP header、签名、验签、解密、mock 响应和协议 payload 解析;`server-rs/crates/api-server/src/wechat.rs` 与 `wechat/*` 承载 Axum handler、AppConfig 到平台配置的映射、Genarrative 用户 / 订单 / 钱包 / SSE / 错误 envelope 编排。`platform-auth` 当前仍承载微信 OAuth / 小程序登录 provider 协议,`api-server::wechat::provider` 只作为组合根 adapter,不在业务 handler 内散落 provider 构造。 验证: diff --git a/docs/【项目基线】当前产品与工程约束-2026-05-15.md b/docs/【项目基线】当前产品与工程约束-2026-05-15.md index 8665e27cf..55fdf5369 100644 --- a/docs/【项目基线】当前产品与工程约束-2026-05-15.md +++ b/docs/【项目基线】当前产品与工程约束-2026-05-15.md @@ -81,7 +81,7 @@ server-rs + Axum + SpacetimeDB - `spacetime-client`:后端访问 SpacetimeDB 的 typed facade。 - `module-*`:纯领域模型、命令、应用规则、领域事件和领域错误。 - `platform-*`:OSS、LLM、认证、语音等外部平台能力。 -- `shared-contracts` / `packages/shared`:前后端 DTO 和公开契约。 +- `shared-contracts` / `packages/shared`:前后端 DTO、公开契约和跨页面复用的无业务真相 TypeScript 代码;`packages/shared` 可承载共享 UI 组件与纯工具,但不承载领域规则、后端副作用或正式状态。 - 前端:表现、交互、临时 UI 状态和后端结果渲染。 明确废弃: From 11abb434386a6c1f5d42ec894fdff3661b066502 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Tue, 14 Jul 2026 11:12:07 +0800 Subject: [PATCH 12/30] if it run , dont del it --- public/creation-home/topbar-wallet.png | Bin 0 -> 95544 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/creation-home/topbar-wallet.png diff --git a/public/creation-home/topbar-wallet.png b/public/creation-home/topbar-wallet.png new file mode 100644 index 0000000000000000000000000000000000000000..37043028d5d63d617ca91d7aeb2d7f1845e8e578 GIT binary patch literal 95544 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 S-~KP)?NgLhlc|$35C1=aO6o2E literal 0 HcmV?d00001 From c70a33d682b8d4d61e453728bca7807b40ea20b0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 14 Jul 2026 18:09:50 +0800 Subject: [PATCH 13/30] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=80=80=E6=AC=BE?= =?UTF-8?q?=E5=A4=8D=E6=A0=B8=E4=B8=8E=E6=B8=A0=E9=81=93=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 退款 observation ID 按来源与事实指纹稳定区分 人工复核只豁免本次批准的订单冲突并持久化错误码 普通微信渠道在查单前拒绝虚拟支付、会员与非正式渠道 网络结果未知时当前弹窗直接复用原请求继续核对 按 release 首次上线基线移除未发布退款结构兼容并同步文档与绑定 --- .../src/pages/AdminRechargeOrderPage.test.tsx | 13 +- .../src/pages/AdminRechargeOrderPage.tsx | 1 + .../shared-memory/decision-log.md | 3 +- ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 10 +- .../crates/api-server/src/admin_recharge.rs | 128 +++++++++++++++--- .../profile_recharge_refund_type.rs | 7 + .../crates/spacetime-module/src/migration.rs | 64 --------- .../spacetime-module/src/runtime/profile.rs | 70 ++++++++-- 8 files changed, 193 insertions(+), 103 deletions(-) diff --git a/apps/admin-web/src/pages/AdminRechargeOrderPage.test.tsx b/apps/admin-web/src/pages/AdminRechargeOrderPage.test.tsx index 92224d082..450949062 100644 --- a/apps/admin-web/src/pages/AdminRechargeOrderPage.test.tsx +++ b/apps/admin-web/src/pages/AdminRechargeOrderPage.test.tsx @@ -274,7 +274,7 @@ test('已登记的部分退款允许在 REFUND 支付状态下继续退剩余额 ).toBe(false); }); -test('网络结果不明后重新预检会复用稳定 requestId 且双击只提交一次', async () => { +test('网络结果不明后当前弹窗直接复用稳定 requestId', async () => { const user = userEvent.setup(); vi.mocked(executeAdminRechargeRefund) .mockRejectedValueOnce(new Error('network disconnected')) @@ -284,21 +284,26 @@ test('网络结果不明后重新预检会复用稳定 requestId 且双击只提 await user.type(screen.getByLabelText('退款金额(元)'), '3.00'); await user.type(screen.getByLabelText('退款原因'), '用户申请'); await user.click(screen.getByRole('button', { name: '核验支付账单' })); - await user.dblClick(screen.getByRole('button', { name: '确认退款' })); + await user.click(screen.getByRole('button', { name: '确认退款' })); expect(await screen.findByText(/微信侧退款状态未知/)).toBeTruthy(); expect(executeAdminRechargeRefund).toHaveBeenCalledTimes(1); const firstRequestId = vi.mocked(executeAdminRechargeRefund).mock .calls[0]?.[1].requestId; - await user.click(screen.getByRole('button', { name: '核验支付账单' })); - await user.dblClick(screen.getByRole('button', { name: '确认退款' })); + expect( + screen + .getByRole('button', { name: '核验支付账单' }) + .hasAttribute('disabled'), + ).toBe(true); + await user.click(screen.getByRole('button', { name: '继续核对退款' })); await waitFor(() => expect(executeAdminRechargeRefund).toHaveBeenCalledTimes(2), ); expect( vi.mocked(executeAdminRechargeRefund).mock.calls[1]?.[1].requestId, ).toBe(firstRequestId); + expect(previewAdminRechargeRefund).toHaveBeenCalledTimes(1); }); test('刷新后根据 active hold 恢复原退款请求并复用 requestId', async () => { diff --git a/apps/admin-web/src/pages/AdminRechargeOrderPage.tsx b/apps/admin-web/src/pages/AdminRechargeOrderPage.tsx index 5ad5a1a25..26a5f32d7 100644 --- a/apps/admin-web/src/pages/AdminRechargeOrderPage.tsx +++ b/apps/admin-web/src/pages/AdminRechargeOrderPage.tsx @@ -345,6 +345,7 @@ export function AdminRechargeOrderPage({ if (!isAdminApiError(error)) { setProviderStatusUnknown(true); setPreview(null); + setResumableRefundRequest(requestContext); setRefundError(unknownProviderStatusMessage); } else { removePersistedRefundRequestContext(token, refundOrder.orderId); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 8f5ace7b2..04a67ec11 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4081,7 +4081,8 @@ - 背景:普通 V3 退款已经能从回调、查单和账单收口现金事实,但后台主动退款若先调微信再扣泥点,会在用户余额不足时产生本可避免的欠账;后台各页面也没有统一查询用户余额、绑定状态和充值订单的入口。 - 决策:新增 `profile_recharge_refund_hold`,后台执行退款前按累计部分退款公式原子占用本次应追回的永久泥点,再使用客户端稳定 `requestId` 派生 `out_refund_no` 调微信。部分退款额外占用 1 泥点并发舍入缓冲,防止占用创建后到达的外部退款跨越累计 `floor` 边界;全额退款不加缓冲。`SUCCESS` 扣款并结算匹配占用,`CLOSED` 释放,`PROCESSING / ABNORMAL` 保持;结果未知时不擅自释放,至少等待 10 分钟并连续 3 次退款查单收到官方 `RESOURCE_NOT_EXISTS` 才由 worker 释放。普通消费必须排除全部活动占用。 - 欠账与冻结:支付侧已经成功退款时不能回滚现金事实;永久泥点不足部分继续只写订单 settlement 的 `unrecovered_points`,限制普通消费,后续永久泥点优先自动偿还。每日免费和会员周期泥点不参与。人工冻结单独使用 `profile_wallet_manual_restriction`,解除人工冻结不解除退款欠账限制。 -- 人工复核:交易号或订单总额冲突只允许管理员确认退款归属,退款行追加不可变的管理员、原因、时间后重新运行标准结算;该操作不是“直接解冻”,余额不足仍形成欠账。会员退款、未知错误和非法结算计划不显示该入口,也不能复用泥点钱包冻结语义。 +- 人工复核:交易号或订单总额冲突只允许管理员确认退款归属,退款行追加不可变的管理员、原因、时间和获批错误码后重新运行标准结算;一次确认只豁免对应冲突,其他不变量继续 fail-closed。该操作不是“直接解冻”,余额不足仍形成欠账。会员退款、未知错误和非法结算计划不显示该入口,也不能复用泥点钱包冻结语义。 +- 发布基线:退款功能和退款事实表尚未进入 release,首次上线只以 release 已有 schema 和数据为迁移基线;不为 master 上未发布的退款中间结构保留旧行 normalization 或历史人工复核兼容。 - 后台边界:充值订单、预检、执行、应急退款号登记、用户详情和钱包冻结均只挂在管理员鉴权路由。用户详情由 `user_id` 或陶泥号经认证服务解析,返回头像、昵称、脱敏手机号、绑定状态、钱包分桶、占用、欠账和最近订单;后台语义明确的用户字段复用同一个图标按钮和弹窗,管理员主体及 `admin:*` 合成 ID 不打开用户详情。 - 部分退款预检:微信支付查单 `trade_state=REFUND` 只表示已发生退款,不代表全额退款。刷新已登记退款后,本地累计成功退款大于 0 且小于订单总额、且不存在非终态退款、活动 hold、欠账或人工冻结时,可以继续退本地剩余额度;没有本地成功退款事实能解释 `REFUND` 时继续失败关闭并要求登记或账单对账。 - 影响范围:`module-runtime`、`spacetime-module`、`spacetime-client`、`api-server` 管理员 BFF / refund worker、`shared-contracts` 与 `apps/admin-web`。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 95588882a..0796c8489 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -203,13 +203,13 @@ npm run check:server-rs-ddd 13. 所有微信真实渠道都以微信支付通知或服务端查单确认 `SUCCESS` 为到账事实;小程序、H5 跳转和 Native 二维码返回都不能直接发放泥点或会员。 14. 微信 JSAPI / H5 / 小程序 / Native 下单统一显式传 5 分钟 `time_expire`,格式为 RFC3339 秒级时间;Native 额外通过 `wechatNativePayment.expiresAt` 下发给前端二维码弹窗展示。 15. 真实微信渠道的新建 pending 充值订单会写入 SpacetimeDB 原生 scheduled 表 `profile_recharge_order_expiration_timer`。到期 reducer 只做数据库内状态转换:订单仍为 `pending` 时更新为 `expired` 并写 `expired_at`,同时删除 timer。HTTP `api-server` 只订阅这张活跃 timer 表的删除事件,收到 `order_id` 后通过 procedure 重新读取订单,只有状态确认为 `expired` 才执行微信查单补偿;支付或主动关闭同样会删除 timer,但会被状态判断忽略。监听断线期间遗漏的删除事件由未检查过期订单 catch-up 补齐,不订阅完整 `profile_recharge_order` 历史表。普通微信支付查单中 `SUCCESS` 可把 `expired` 补确认成 `paid` 入账;`NOTPAY` 会调用微信关单并把本地订单保持为 `expired`;`CLOSED` / `REVOKED` / `PAYERROR` / `ORDER_NOT_EXIST` 只记录检查结果。`wechat_mp_virtual` 使用小程序 `access_token` 和虚拟支付 AppKey 调用官方 `/xpay/query_order`,只在返回单号、支付类型 `order_type=0/7`、金额、合法 `paid_time` 与本地契约一致且状态为 `2/3/4` 时补入账;退款类型 `1/8` 不得触发充值,其余已知状态只记录检查结果。`short_series_goods` 从 `status=2` 恢复时先幂等入账,再调用 `/xpay/notify_provide_goods`,失败后允许基于本地 `paid` 状态只重试发货;`short_series_coin` 不调用现金单发货接口。`external-generation-worker` / controller 不处理充值过期。 -16. 普通微信支付 V3 退款事实由 `profile_recharge_refund` 保存,回调、退款 API 响应、主动查单和交易账单发现统一调用 `record_profile_recharge_refund_observation_and_return`。`out_refund_no` 是商户幂等键,`provider_refund_id` 唯一;重复 observation 必须校验订单、交易、金额、状态和指纹,不允许仅按主键直接吞掉冲突。 +16. 普通微信支付 V3 退款事实由 `profile_recharge_refund` 保存,回调、退款 API 响应、主动查单和交易账单发现统一调用 `record_profile_recharge_refund_observation_and_return`。`out_refund_no` 是商户幂等键,`provider_refund_id` 唯一;退款申请响应和主动查单等生产者生成 observation ID 时必须同时纳入来源和事实指纹,同一来源同一事实稳定重放、不同来源不得复用 ID;重复 observation 必须校验订单、交易、金额、状态、来源和指纹,不允许仅按主键直接吞掉冲突。 17. `profile_recharge_order_refund_settlement` 按原订单聚合累计成功退款金额和权益回收。部分退款不改充值订单 `paid`;累计金额等于订单金额时才改为 `refunded`。历史支付和首充资格以 `paid_at` 是否存在判断,退款不把用户重新变成首充。 -18. 泥点退款按累计成功退款比例计算目标回收量,全额退款强制精确回收原 `points_delta`。自动回收只扣普通永久泥点,每日免费和会员周期限时泥点保持不变;不足部分持久化为 `shortfall` 并冻结正式钱包消费,后续 worker 只重试本地回收。已成功退款的泥点订单若出现微信交易号、订单总额冲突或结算计划无效,必须将 settlement 标记为 `wallet_frozen` 并阻断普通消费。管理员只能对交易号或订单总额冲突执行“确认退款归属”:procedure 在退款行尾部不可变记录管理员、原因和时间后重新运行标准结算,能追回的永久泥点照常扣除,余额不足继续形成欠账;不能直接清空冻结或伪造已追回量。非法结算计划仍保持冻结等待数据/代码修复。流水来源为 `recharge_refund_recovery`。会员充值没有可逆 grant 快照,退款统一标记 `manual_review`,不猜测回滚档位、有效期或周期泥点,也不复用泥点退款冻结语义。 +18. 泥点退款按累计成功退款比例计算目标回收量,全额退款强制精确回收原 `points_delta`。自动回收只扣普通永久泥点,每日免费和会员周期限时泥点保持不变;不足部分持久化为 `shortfall` 并冻结正式钱包消费,后续 worker 只重试本地回收。已成功退款的泥点订单若出现微信交易号、订单总额冲突或结算计划无效,必须将 settlement 标记为 `wallet_frozen` 并阻断普通消费。管理员只能对交易号或订单总额冲突执行“确认退款归属”:procedure 在退款行尾部不可变记录管理员、原因、时间和本次获批的错误码,再重新运行标准结算;一次确认只豁免对应的交易号或订单总额冲突,渠道、支付状态及其他未获批冲突继续 fail-closed。能追回的永久泥点照常扣除,余额不足继续形成欠账;不能直接清空冻结或伪造已追回量。非法结算计划仍保持冻结等待数据/代码修复。流水来源为 `recharge_refund_recovery`。会员充值没有可逆 grant 快照,退款统一标记 `manual_review`,不猜测回滚档位、有效期或周期泥点,也不复用泥点退款冻结语义。 19. 外部现金退款 `SUCCESS` 必须先持久化并 ACK,即使本地订单缺失、金额冲突、权益不足或会员需要人工处理,也不能回滚已经发生的现金事实。冲突 observation 记录 resolution code 并进入告警;只有验签、解密、契约解析或 SpacetimeDB 持久化失败才让微信重试。 20. 主动查询与退款交易账单 worker 只由 HTTP 角色运行并由 `WECHAT_PAY_REFUND_RECONCILIATION_ENABLED=true` 显式开启。生产 env 示例和 API deploy 会为缺失配置补 `true`;当 `WECHAT_PAY_ENABLED=true + WECHAT_PAY_PROVIDER=real` 时显式关闭该开关必须阻断发布,启动日志也必须明确记录启用或异常关闭状态。非终态退款按 1 / 5 / 10 / 20 / 30 分钟衰减查单;北京时间次日 10 点后按 30 个稳定分片轮转补扫微信 API 可查询的近 90 天 `bill_type=REFUND` 交易账单,每 30 分钟覆盖完整窗口。单行失败不阻塞同日其他退款或其他日期,但该日不写完成 checkpoint 并继续重试;昨日返回 `NO_STATEMENT_EXIST` 时至少延迟到次日 10 点后再确认空账单。账单申请响应验签,GZIP 解压后按 SHA1 验真,CSV 用结构化 parser 和十进制定点金额解析;发现手工退款后必须再查单取得当前状态。 21. 普通 V3 支付通知同时校验 AppID、商户号、本地订单渠道、金额和微信支付单号;`success_time` 缺失或非法时拒绝,不能用本机时间补齐。晚到通知遇到 `refunded` 订单只做交易号一致性幂等校验,不再次发放权益。 -22. 后台主动退款只支持普通 V3 泥点订单。`api-server` 先做微信支付订单查单预检,再调用 SpacetimeDB procedure 原子创建退款 hold;只有 hold 成功才允许调用微信退款。虚拟支付、会员、未支付、对账未完成、退款已满额、人工冻结、退款欠账或永久泥点不足必须 fail-closed。 +22. 后台主动退款只支持 `wechat_mp`、`wechat_jsapi`、`wechat_h5`、`wechat_native` 普通 V3 泥点订单。`api-server` 必须先按正式支付渠道和商品类型拦截不支持的订单,再做微信支付订单查单预检,然后调用 SpacetimeDB procedure 原子创建退款 hold;只有 hold 成功才允许调用微信退款。`wechat_mp_virtual`、历史非正式渠道值、会员、未支付、对账未完成、退款已满额、人工冻结、退款欠账或永久泥点不足必须在调用普通 V3 provider 前 fail-closed。 23. `profile_recharge_refund_hold` 以稳定 `out_refund_no` 为主键,保存订单、用户、本次退款金额、占用永久泥点、管理员、原因和 `active / settled / released` 状态。部分退款的 hold 在累计应追回增量之外额外保留 1 泥点并发舍入缓冲,全额退款不加缓冲;活动 hold 不改变钱包总额,但普通钱包消费必须预留全部活动 hold;成功退款 observation 扣款并结算匹配 hold,关闭退款释放 hold,外部退款追回不得消耗其他活动 hold。 24. 退款欠账继续以 `profile_recharge_order_refund_settlement.unrecovered_points` 为唯一真相;不新增平行 debt 累计。`profile_wallet_manual_restriction` 只保存人工冻结,普通消费同时检查人工冻结与退款欠账。后续永久泥点到账后继续偿还欠账,每日免费与会员周期泥点不参与;解除人工冻结不得清除退款欠账限制。 25. 管理员充值订单、用户详情、退款预检/执行、应急退款号登记、退款人工复核和钱包冻结接口只留在 `api-server` 管理员鉴权路由。人工复核 BFF 必须从管理员会话写入操作人,要求非空原因,并调用 runtime service identity 受限 procedure;前端不得自行改 settlement 或钱包冻结。外部微信副作用由 `platform-wechat` 执行,退款/hold/钱包事务留在 `spacetime-module`,后台前端只展示 BFF 返回的正式状态。 @@ -762,14 +762,14 @@ npm run check:server-rs-ddd - Rust 结构体:`ProfileRechargeRefund` - 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs` -- 作用:普通微信支付 V3 退款单聚合。以 `out_refund_no` 为主键、`provider_refund_id` 唯一,保存原订单/微信支付单、四个分金额、微信状态、最近观察、权益目标/已回收/未回收量和人工处理错误码。交易号或订单总额冲突经管理员确认归属后,追加保存处理管理员、非空原因和处理时间;三字段只写一次,作为重新执行正式结算的审计事实。数据库迁移导入旧退款行时,缺失的三个审计字段必须补 `null`,保证升级前冷备可恢复。 +- 作用:普通微信支付 V3 退款单聚合。以 `out_refund_no` 为主键、`provider_refund_id` 唯一,保存原订单/微信支付单、四个分金额、微信状态、最近观察、权益目标/已回收/未回收量和人工处理错误码。交易号或订单总额冲突经管理员确认归属后,追加保存处理管理员、非空原因、处理时间和本次获批的错误码;四字段只写一次,作为重新执行正式结算的审计事实。 - 索引:`by_profile_recharge_refund_order_id`、`by_profile_recharge_refund_status_updated_at`。 ### `profile_recharge_refund_observation` - Rust 结构体:`ProfileRechargeRefundObservation` - 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs` -- 作用:退款事实的追加观察记录。保存 callback / api_request / query / trade_bill 来源、稳定 observation ID、金额、状态、脱敏通知引用、事实指纹和 resolution code;不保存回调密文、签名、密钥、原始 CSV 或短时下载 URL。 +- 作用:退款事实的追加观察记录。保存 callback / api_request / query / trade_bill 来源、按“来源 + 事实指纹”稳定生成的 observation ID、金额、状态、脱敏通知引用、事实指纹和 resolution code;同一事实从不同来源进入时使用不同 ID,不保存回调密文、签名、密钥、原始 CSV 或短时下载 URL。 - 索引:`by_profile_recharge_refund_observation_out_refund_no`、`by_profile_recharge_refund_observation_order_id`。 ### `profile_recharge_order_refund_settlement` diff --git a/server-rs/crates/api-server/src/admin_recharge.rs b/server-rs/crates/api-server/src/admin_recharge.rs index 289510f95..f4ec6c325 100644 --- a/server-rs/crates/api-server/src/admin_recharge.rs +++ b/server-rs/crates/api-server/src/admin_recharge.rs @@ -8,8 +8,11 @@ use axum::{ }; use module_auth::AuthUser; use module_runtime::{ - RuntimeProfileAdminWalletSnapshot, RuntimeProfileRechargeOrderAdminEntrySnapshot, - RuntimeProfileRechargeOrderStatus, RuntimeProfileRechargeRefundHoldSnapshot, + PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI, + PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM, + PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE, RuntimeProfileAdminWalletSnapshot, + RuntimeProfileRechargeOrderAdminEntrySnapshot, RuntimeProfileRechargeOrderStatus, + RuntimeProfileRechargeProductKind, RuntimeProfileRechargeRefundHoldSnapshot, RuntimeProfileRechargeRefundHoldStatus, RuntimeProfileRechargeRefundObservationSource, RuntimeProfileRechargeRefundRecoveryStatus, RuntimeProfileRechargeRefundSnapshot, RuntimeProfileRechargeRefundStatus, build_runtime_profile_admin_wallet_get_input, @@ -255,6 +258,8 @@ pub async fn admin_execute_recharge_refund( let out_refund_no = build_out_refund_no(&payload.order_id, request_id); let refund_reason = normalize_refund_reason(payload.reason.as_deref()); let mut entry = load_order(&state, &request_context, &payload.order_id).await?; + validate_refund_provider_route(&entry) + .map_err(|error| error_response(&request_context, error))?; let has_matching_hold = entry.active_hold.as_ref().is_some_and(|hold| { hold.out_refund_no == out_refund_no && hold.refund_cents == payload.refund_amount_cents }); @@ -540,6 +545,8 @@ async fn reconcile_and_validate_order( Response, > { let mut entry = load_order(state, request_context, &order_id).await?; + validate_refund_provider_route(&entry) + .map_err(|error| error_response(request_context, error))?; let mut payment_check = query_and_validate_payment(state, &entry) .await .map_err(|error| error_response(request_context, error))?; @@ -688,10 +695,7 @@ async fn persist_refund_fact( ) -> Result<(), SpacetimeClientError> { let fingerprint = refund_fact_fingerprint(refund); let observation = build_wechat_pay_refund_observation( - format!( - "admin-refund-observation-{}", - short_hash(fingerprint.as_bytes()) - ), + admin_refund_observation_id(source, &fingerprint), source, None, fingerprint, @@ -728,11 +732,10 @@ fn validate_refund_amount( fn order_refund_block_reason( entry: &RuntimeProfileRechargeOrderAdminEntrySnapshot, ) -> Option<&'static str> { - if entry.order.kind.as_str() != "points" { - return Some("membership_not_supported"); - } - if !is_ordinary_wechat_v3_channel(&entry.order.payment_channel) { - return Some("payment_channel_not_supported"); + if let Some(reason_code) = + refund_provider_route_block_reason(entry.order.kind, &entry.order.payment_channel) + { + return Some(reason_code); } if !matches!( entry.order.status, @@ -1125,13 +1128,53 @@ fn short_hash(value: &[u8]) -> String { hex::encode(&Sha256::digest(value)[..8]) } +fn admin_refund_observation_id( + source: RuntimeProfileRechargeRefundObservationSource, + fingerprint: &str, +) -> String { + format!( + "admin-refund-observation-{}-{}", + source.as_str(), + short_hash(fingerprint.as_bytes()) + ) +} + fn is_ordinary_wechat_v3_channel(value: &str) -> bool { matches!( value, - "wechat_mini_program" | "wechat_jsapi" | "wechat_h5" | "wechat_native" + PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM + | PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI + | PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5 + | PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE ) } +fn refund_provider_route_block_reason( + kind: RuntimeProfileRechargeProductKind, + payment_channel: &str, +) -> Option<&'static str> { + if kind != RuntimeProfileRechargeProductKind::Points { + return Some("membership_not_supported"); + } + if !is_ordinary_wechat_v3_channel(payment_channel) { + return Some("payment_channel_not_supported"); + } + None +} + +fn validate_refund_provider_route( + entry: &RuntimeProfileRechargeOrderAdminEntrySnapshot, +) -> Result<(), AppError> { + if let Some(reason_code) = + refund_provider_route_block_reason(entry.order.kind, &entry.order.payment_channel) + { + return Err(AppError::from_status(StatusCode::CONFLICT) + .with_message(refund_block_message(reason_code)) + .with_details(json!({"reasonCode": reason_code}))); + } + Ok(()) +} + fn classify_refund_block_reason(message: &str) -> &'static str { if message.contains("永久泥点不足") || message.contains("可用") { "insufficient_permanent_points" @@ -1195,13 +1238,17 @@ mod tests { body::Body, http::{Request, StatusCode}, }; - use module_runtime::RuntimeProfileRechargeRefundHoldStatus; + use module_runtime::{ + RuntimeProfileRechargeProductKind, RuntimeProfileRechargeRefundHoldStatus, + RuntimeProfileRechargeRefundObservationSource, + }; use tower::ServiceExt; use super::{ - build_out_refund_no, classify_refund_block_reason, is_likely_wechat_refund_id, - normalize_request_id, payment_check_block_reason_code, - payment_trade_state_allows_additional_refund, refund_block_message, + admin_refund_observation_id, build_out_refund_no, classify_refund_block_reason, + is_likely_wechat_refund_id, is_ordinary_wechat_v3_channel, normalize_request_id, + payment_check_block_reason_code, payment_trade_state_allows_additional_refund, + refund_block_message, refund_provider_route_block_reason, validate_prepared_refund_hold_status, }; use crate::{app::build_router, config::AppConfig, state::AppState}; @@ -1229,6 +1276,55 @@ mod tests { assert!(!is_likely_wechat_refund_id("refund-1001")); } + #[test] + fn admin_refund_observation_ids_are_stable_per_source_and_fact() { + let fingerprint = "sha256:refund-fact"; + let api_request = admin_refund_observation_id( + RuntimeProfileRechargeRefundObservationSource::ApiRequest, + fingerprint, + ); + let repeated_api_request = admin_refund_observation_id( + RuntimeProfileRechargeRefundObservationSource::ApiRequest, + fingerprint, + ); + let query = admin_refund_observation_id( + RuntimeProfileRechargeRefundObservationSource::Query, + fingerprint, + ); + + assert_eq!(api_request, repeated_api_request); + assert_ne!(api_request, query); + } + + #[test] + fn refund_provider_route_uses_canonical_payment_channels() { + assert!(is_ordinary_wechat_v3_channel("wechat_mp")); + assert!(is_ordinary_wechat_v3_channel("wechat_jsapi")); + assert!(!is_ordinary_wechat_v3_channel("wechat_mini_program")); + assert!(!is_ordinary_wechat_v3_channel("wechat_mp_virtual")); + assert_eq!( + refund_provider_route_block_reason( + RuntimeProfileRechargeProductKind::Points, + "wechat_mp" + ), + None + ); + assert_eq!( + refund_provider_route_block_reason( + RuntimeProfileRechargeProductKind::Points, + "wechat_mp_virtual" + ), + Some("payment_channel_not_supported") + ); + assert_eq!( + refund_provider_route_block_reason( + RuntimeProfileRechargeProductKind::Membership, + "wechat_native" + ), + Some("membership_not_supported") + ); + } + #[test] fn reconciled_partial_refund_keeps_the_remaining_amount_refundable() { assert!(payment_trade_state_allows_additional_refund( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_recharge_refund_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_recharge_refund_type.rs index b71253bba..9eeba7cd5 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/profile_recharge_refund_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_recharge_refund_type.rs @@ -36,6 +36,7 @@ pub struct ProfileRechargeRefund { pub manual_review_resolved_by_admin_user_id: Option, pub manual_review_resolution_reason: Option, pub manual_review_resolved_at: Option<__sdk::Timestamp>, + pub manual_review_resolved_error_code: Option, } impl __sdk::InModule for ProfileRechargeRefund { @@ -82,6 +83,8 @@ pub struct ProfileRechargeRefundCols { __sdk::__query_builder::Col>, pub manual_review_resolved_at: __sdk::__query_builder::Col>, + pub manual_review_resolved_error_code: + __sdk::__query_builder::Col>, } impl __sdk::__query_builder::HasCols for ProfileRechargeRefund { @@ -137,6 +140,10 @@ impl __sdk::__query_builder::HasCols for ProfileRechargeRefund { table_name, "manual_review_resolved_at", ), + manual_review_resolved_error_code: __sdk::__query_builder::Col::new( + table_name, + "manual_review_resolved_error_code", + ), } } } diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index 96a0506c6..c67a80267 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -1345,20 +1345,6 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde .or_insert(serde_json::Value::Null); } } - if table_name == "profile_recharge_refund" { - if let Some(object) = next_value.as_object_mut() { - // 中文注释:退款人工复核审计字段晚于退款事实表加入,旧迁移包按尚未处理兼容。 - object - .entry("manual_review_resolved_by_admin_user_id".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("manual_review_resolution_reason".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("manual_review_resolved_at".to_string()) - .or_insert(serde_json::Value::Null); - } - } if table_name == "external_generation_job_summary" { if let Some(object) = next_value.as_object_mut() { // 中文注释:非阻断完成告警晚于轻量任务摘要表加入,旧迁移包按无告警兼容。 @@ -1676,56 +1662,6 @@ mod migration_bootstrap_secret_tests { assert_eq!(normalized["warning_message"], serde_json::Value::Null); } - #[test] - fn old_profile_recharge_refund_rows_import_with_empty_manual_review_audit() { - let row = ProfileRechargeRefund { - out_refund_no: "refund-legacy".to_string(), - provider_refund_id: "wx-refund-legacy".to_string(), - order_id: "order-legacy".to_string(), - provider_transaction_id: "wx-transaction-legacy".to_string(), - user_id: Some("user-legacy".to_string()), - provider_status: RuntimeProfileRechargeRefundStatus::Success, - total_cents: 600, - refund_cents: 300, - payer_total_cents: 600, - payer_refund_cents: 300, - success_at: Some(Timestamp::from_micros_since_unix_epoch(200)), - first_observed_at: Timestamp::from_micros_since_unix_epoch(100), - updated_at: Timestamp::from_micros_since_unix_epoch(200), - last_observation_source: RuntimeProfileRechargeRefundObservationSource::Query, - last_observation_id: "observation-legacy".to_string(), - order_settled_at: None, - target_recovery_points: 30, - recovered_points: 30, - unrecovered_points: 0, - recovery_status: RuntimeProfileRechargeRefundRecoveryStatus::Applied, - last_recovery_ledger_id: Some("ledger-legacy".to_string()), - last_error_code: None, - manual_review_resolved_by_admin_user_id: None, - manual_review_resolution_reason: None, - manual_review_resolved_at: None, - }; - let mut legacy_value = row_to_json(&row).expect("退款旧行应可序列化"); - let object = legacy_value - .as_object_mut() - .expect("退款迁移行应为 JSON object"); - object.remove("manual_review_resolved_by_admin_user_id"); - object.remove("manual_review_resolution_reason"); - object.remove("manual_review_resolved_at"); - - let normalized = normalize_migration_row("profile_recharge_refund", &legacy_value); - let restored = row_from_json::( - "profile_recharge_refund", - &normalized, - &mut Vec::new(), - ) - .expect("缺少人工复核字段的旧退款行应可导入"); - - assert!(restored.manual_review_resolved_by_admin_user_id.is_none()); - assert!(restored.manual_review_resolution_reason.is_none()); - assert!(restored.manual_review_resolved_at.is_none()); - } - #[test] fn bootstrap_secret_sha256_is_stable_and_does_not_contain_plaintext() { let secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; diff --git a/server-rs/crates/spacetime-module/src/runtime/profile.rs b/server-rs/crates/spacetime-module/src/runtime/profile.rs index 706139824..678980d8d 100644 --- a/server-rs/crates/spacetime-module/src/runtime/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/profile.rs @@ -512,6 +512,8 @@ pub struct ProfileRechargeRefund { pub(crate) manual_review_resolution_reason: Option, #[default(None::)] pub(crate) manual_review_resolved_at: Option, + #[default(None::)] + pub(crate) manual_review_resolved_error_code: Option, } #[spacetimedb::table( @@ -2727,6 +2729,7 @@ mod tests { manual_review_resolved_by_admin_user_id: None, manual_review_resolution_reason: None, manual_review_resolved_at: None, + manual_review_resolved_error_code: None, } } @@ -2751,7 +2754,7 @@ mod tests { } #[test] - fn confirmed_manual_review_suppresses_only_the_order_match_conflict() { + fn confirmed_manual_review_suppresses_only_the_approved_order_match_conflict() { let order = recharge_order_for_refund(RuntimeProfileRechargeOrderStatus::Paid); let mut refund = recharge_refund_for_order(&order); refund.provider_transaction_id = "wx-transaction-other".to_string(); @@ -2761,6 +2764,8 @@ mod tests { ); refund.manual_review_resolved_at = Some(Timestamp::from_micros_since_unix_epoch(300)); + refund.manual_review_resolved_error_code = + Some("provider_transaction_id_mismatch".to_string()); assert_eq!( unresolved_profile_recharge_refund_order_match_error(&order, &refund), None @@ -2769,6 +2774,18 @@ mod tests { validate_profile_recharge_refund_order_match(&order, &refund), Err("provider_transaction_id_mismatch".to_string()) ); + + refund.total_cents = order.amount_cents + 1; + assert_eq!( + unresolved_profile_recharge_refund_order_match_error(&order, &refund), + Some("order_total_mismatch".to_string()) + ); + + refund.manual_review_resolved_error_code = Some("order_total_mismatch".to_string()); + assert_eq!( + unresolved_profile_recharge_refund_order_match_error(&order, &refund), + Some("provider_transaction_id_mismatch".to_string()) + ); } #[test] @@ -4617,10 +4634,10 @@ fn resolve_profile_recharge_refund_manual_review( } let review_code = refund .last_error_code - .as_deref() + .clone() .ok_or_else(|| "退款缺少人工复核原因".to_string())?; if !matches!( - review_code, + review_code.as_str(), "provider_transaction_id_mismatch" | "order_total_mismatch" ) { return Err(format!( @@ -4636,10 +4653,20 @@ fn resolve_profile_recharge_refund_manual_review( if order.kind != RuntimeProfileRechargeProductKind::Points { return Err("会员退款不能使用泥点钱包人工复核入口".to_string()); } + if let Some(remaining_code) = profile_recharge_refund_order_match_error_except( + &order, + &refund, + Some(review_code.as_str()), + ) { + return Err(format!( + "确认当前退款冲突后仍存在未获批准的订单不匹配: {remaining_code}" + )); + } refund.manual_review_resolved_by_admin_user_id = Some(validated.admin_user_id); refund.manual_review_resolution_reason = Some(validated.reason); refund.manual_review_resolved_at = Some(ctx.timestamp); + refund.manual_review_resolved_error_code = Some(review_code); refund.updated_at = ctx.timestamp; let (settlement, resolution_code) = reconcile_profile_recharge_refund_success(ctx, &mut refund); if refund.recovery_status == RuntimeProfileRechargeRefundRecoveryStatus::ManualReview { @@ -5041,6 +5068,7 @@ fn record_profile_recharge_refund_observation( manual_review_resolved_by_admin_user_id: None, manual_review_resolution_reason: None, manual_review_resolved_at: None, + manual_review_resolved_error_code: None, }, }; @@ -5211,10 +5239,19 @@ fn reconcile_profile_recharge_refund_success( (Some(settlement), resolution_code.to_string()) } +#[cfg(test)] fn validate_profile_recharge_refund_order_match( order: &ProfileRechargeOrder, refund: &ProfileRechargeRefund, ) -> Result<(), String> { + profile_recharge_refund_order_match_error_except(order, refund, None).map_or(Ok(()), Err) +} + +fn profile_recharge_refund_order_match_error_except( + order: &ProfileRechargeOrder, + refund: &ProfileRechargeRefund, + approved_error_code: Option<&str>, +) -> Option { if !matches!( order.payment_channel.as_str(), PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM @@ -5222,7 +5259,7 @@ fn validate_profile_recharge_refund_order_match( | PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5 | PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE ) { - return Err("payment_channel_not_v3".to_string()); + return Some("payment_channel_not_v3".to_string()); } if order.paid_at.is_none() || !matches!( @@ -5230,24 +5267,31 @@ fn validate_profile_recharge_refund_order_match( RuntimeProfileRechargeOrderStatus::Paid | RuntimeProfileRechargeOrderStatus::Refunded ) { - return Err("order_not_paid".to_string()); + return Some("order_not_paid".to_string()); } - if order.provider_transaction_id.as_deref() != Some(refund.provider_transaction_id.as_str()) { - return Err("provider_transaction_id_mismatch".to_string()); + if order.provider_transaction_id.as_deref() != Some(refund.provider_transaction_id.as_str()) + && approved_error_code != Some("provider_transaction_id_mismatch") + { + return Some("provider_transaction_id_mismatch".to_string()); } - if order.amount_cents != refund.total_cents { - return Err("order_total_mismatch".to_string()); + if order.amount_cents != refund.total_cents + && approved_error_code != Some("order_total_mismatch") + { + return Some("order_total_mismatch".to_string()); } - Ok(()) + None } fn unresolved_profile_recharge_refund_order_match_error( order: &ProfileRechargeOrder, refund: &ProfileRechargeRefund, ) -> Option { - validate_profile_recharge_refund_order_match(order, refund) - .err() - .filter(|_| refund.manual_review_resolved_at.is_none()) + let approved_error_code = refund + .manual_review_resolved_at + .is_some() + .then_some(refund.manual_review_resolved_error_code.as_deref()) + .flatten(); + profile_recharge_refund_order_match_error_except(order, refund, approved_error_code) } fn mark_profile_recharge_refund_manual_review( From a597fcd076e5fccdc5df53a8ddb5246c0835e162 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 14 Jul 2026 19:20:25 +0800 Subject: [PATCH 14/30] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E9=80=80=E6=AC=BE?= =?UTF-8?q?=E4=BA=BA=E5=B7=A5=E5=A4=8D=E6=A0=B8=E4=B8=8E=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐退款事实对比字段与人工复核审计展示 人工复核重放严格校验管理员、原因和错误码 退款占用重试完整匹配管理员与归一化原因 统一退款原因持久化上限并修复刷新恢复 补充前后端回归测试、生成绑定与数据契约文档 --- apps/admin-web/src/api/adminApiClient.test.ts | 38 ++++-- apps/admin-web/src/api/adminApiTypes.ts | 4 + .../src/pages/AdminRechargeOrderPage.test.tsx | 99 +++++++++++++- .../src/pages/AdminRechargeOrderPage.tsx | 93 ++++++++++++- .../shared-memory/decision-log.md | 4 +- ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 8 +- .../crates/api-server/src/admin_recharge.rs | 122 +++++++++++++++++- .../profile_recharge_refund_reconciliation.rs | 1 + .../crates/module-runtime/src/commands.rs | 27 ++++ server-rs/crates/module-runtime/src/domain.rs | 2 + .../crates/shared-contracts/src/admin.rs | 13 +- .../src/mapper/runtime_profile.rs | 2 + ...refund_manual_review_resolve_input_type.rs | 1 + ...e_profile_recharge_refund_snapshot_type.rs | 1 + .../spacetime-module/src/runtime/profile.rs | 101 +++++++++++++++ 15 files changed, 480 insertions(+), 36 deletions(-) diff --git a/apps/admin-web/src/api/adminApiClient.test.ts b/apps/admin-web/src/api/adminApiClient.test.ts index b2127d332..a673e0e01 100644 --- a/apps/admin-web/src/api/adminApiClient.test.ts +++ b/apps/admin-web/src/api/adminApiClient.test.ts @@ -1,4 +1,4 @@ -import {afterEach, expect, test, vi} from 'vitest'; +import { afterEach, expect, test, vi } from 'vitest'; import { executeAdminRechargeRefund, @@ -13,9 +13,9 @@ afterEach(() => { test('充值订单查询按后台契约序列化筛选参数', async () => { const fetchMock = vi.fn().mockResolvedValue( - new Response(JSON.stringify({entries: []}), { + new Response(JSON.stringify({ entries: [] }), { status: 200, - headers: {'content-type': 'application/json'}, + headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); @@ -47,12 +47,14 @@ test('充值订单查询按后台契约序列化筛选参数', async () => { }); test('用户详情只发送实际提供的用户定位字段', async () => { - const fetchMock = vi.fn().mockResolvedValue( - new Response(JSON.stringify({userId: 'user-1'}), {status: 200}), - ); + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ userId: 'user-1' }), { status: 200 }), + ); vi.stubGlobal('fetch', fetchMock); - await getAdminUserDetail('token-1', {publicUserCode: 'TN1001'}); + await getAdminUserDetail('token-1', { publicUserCode: 'TN1001' }); const requestUrl = String(fetchMock.mock.calls[0]?.[0]); const parsed = new URL(requestUrl, 'http://admin.local'); @@ -63,9 +65,13 @@ test('用户详情只发送实际提供的用户定位字段', async () => { }); test('退款执行使用独立 execute 管理员路由', async () => { - const fetchMock = vi.fn().mockResolvedValue( - new Response(JSON.stringify({outRefundNo: 'refund-1'}), {status: 200}), - ); + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ outRefundNo: 'refund-1' }), { + status: 200, + }), + ); vi.stubGlobal('fetch', fetchMock); await executeAdminRechargeRefund('token-1', { @@ -92,14 +98,19 @@ test('退款执行使用独立 execute 管理员路由', async () => { }); test('退款人工复核使用独立 resolve 管理员路由', async () => { - const fetchMock = vi.fn().mockResolvedValue( - new Response(JSON.stringify({outRefundNo: 'refund-1'}), {status: 200}), - ); + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ outRefundNo: 'refund-1' }), { + status: 200, + }), + ); vi.stubGlobal('fetch', fetchMock); await resolveAdminRechargeRefundManualReview('token-1', { outRefundNo: 'refund-1', reason: '已核对微信商户平台原始账单', + expectedErrorCode: 'provider_transaction_id_mismatch', }); expect(String(fetchMock.mock.calls[0]?.[0])).toBe( @@ -111,6 +122,7 @@ test('退款人工复核使用独立 resolve 管理员路由', async () => { body: JSON.stringify({ outRefundNo: 'refund-1', reason: '已核对微信商户平台原始账单', + expectedErrorCode: 'provider_transaction_id_mismatch', }), }), ); diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 1afe7f04f..31afc2d78 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -785,7 +785,9 @@ export interface AdminProfileWalletPayload { export interface AdminRechargeRefundPayload { outRefundNo: string; providerRefundId: string; + providerTransactionId: string; providerStatus: string; + totalCents: number; refundCents: number; payerRefundCents: number; successAtMicros?: number | null; @@ -800,6 +802,7 @@ export interface AdminRechargeRefundPayload { manualReviewResolvedByAdminUserId?: string | null; manualReviewResolutionReason?: string | null; manualReviewResolvedAtMicros?: number | null; + manualReviewResolvedErrorCode?: string | null; } export interface AdminRechargeRefundHoldPayload { @@ -882,6 +885,7 @@ export interface AdminRechargeRefundRegisterRequest { export interface AdminRechargeRefundManualReviewResolveRequest { outRefundNo: string; reason: string; + expectedErrorCode: string; } export interface AdminWalletRestrictionRequest { diff --git a/apps/admin-web/src/pages/AdminRechargeOrderPage.test.tsx b/apps/admin-web/src/pages/AdminRechargeOrderPage.test.tsx index 450949062..81e4fcbde 100644 --- a/apps/admin-web/src/pages/AdminRechargeOrderPage.test.tsx +++ b/apps/admin-web/src/pages/AdminRechargeOrderPage.test.tsx @@ -355,6 +355,56 @@ test('刷新后根据 active hold 恢复原退款请求并复用 requestId', asy expect(previewAdminRechargeRefund).toHaveBeenCalledTimes(1); }); +test('超长退款原因按服务端上限持久化并可在刷新后恢复', async () => { + const user = userEvent.setup(); + const normalizedReason = '退'.repeat(80); + vi.mocked(executeAdminRechargeRefund) + .mockRejectedValueOnce(new Error('network disconnected')) + .mockResolvedValueOnce(actionFor(baseOrder)); + const firstRender = renderPage(); + await openPartialRefund(user); + await user.type(screen.getByLabelText('退款金额(元)'), '3.00'); + await user.type(screen.getByLabelText('退款原因'), `${normalizedReason}多`); + await user.click(screen.getByRole('button', { name: '核验支付账单' })); + await user.click(screen.getByRole('button', { name: '确认退款' })); + + expect(await screen.findByText(/微信侧退款状态未知/)).toBeTruthy(); + const firstRequestId = vi.mocked(executeAdminRechargeRefund).mock + .calls[0]?.[1].requestId; + expect(vi.mocked(executeAdminRechargeRefund).mock.calls[0]?.[1].reason).toBe( + normalizedReason, + ); + firstRender.unmount(); + + const activeHoldOrder = orderWithActiveHold(); + activeHoldOrder.activeHold = { + ...activeHoldOrder.activeHold!, + reason: normalizedReason, + }; + vi.mocked(listAdminRechargeOrders).mockResolvedValue({ + entries: [activeHoldOrder], + }); + renderPage(); + await user.click(await screen.findByRole('button', { name: '核对退款' })); + + expect(screen.getByRole('dialog', { name: '退款处理' })).toBeTruthy(); + expect(screen.getByLabelText('退款原因')).toHaveProperty( + 'value', + normalizedReason, + ); + await user.click(screen.getByRole('button', { name: '继续核对退款' })); + + await waitFor(() => + expect(executeAdminRechargeRefund).toHaveBeenCalledTimes(2), + ); + expect(vi.mocked(executeAdminRechargeRefund).mock.calls[1]?.[1]).toEqual( + expect.objectContaining({ + reason: normalizedReason, + requestId: firstRequestId, + }), + ); +}); + test('过期退款请求上下文不会恢复 active hold,只开放安全查单', async () => { const user = userEvent.setup(); vi.mocked(executeAdminRechargeRefund).mockRejectedValueOnce( @@ -474,7 +524,16 @@ test.each(['provider_transaction_id_mismatch', 'order_total_mismatch'])( entries: [ { ...baseOrder, - refunds: [manualReviewRefund({ lastErrorCode })], + refunds: [ + manualReviewRefund({ + lastErrorCode, + providerTransactionId: + lastErrorCode === 'provider_transaction_id_mismatch' + ? 'wx-transaction-other' + : 'wx-transaction-1', + totalCents: lastErrorCode === 'order_total_mismatch' ? 700 : 600, + }), + ], }, ], }); @@ -485,6 +544,29 @@ test.each(['provider_transaction_id_mismatch', 'order_total_mismatch'])( const submitButton = within(dialog).getByRole('button', { name: '确认并追回', }); + expect( + within(dialog).getByText( + lastErrorCode === 'provider_transaction_id_mismatch' + ? '微信交易单号不一致' + : '订单总额不一致', + ), + ).toBeTruthy(); + expect( + within(dialog).getByText('本地微信交易单号').parentElement?.textContent, + ).toContain('wx-transaction-1'); + expect( + within(dialog).getByText('退款微信交易单号').parentElement?.textContent, + ).toContain( + lastErrorCode === 'provider_transaction_id_mismatch' + ? 'wx-transaction-other' + : 'wx-transaction-1', + ); + expect( + within(dialog).getByText('本地订单总额').parentElement?.textContent, + ).toContain('¥6.00'); + expect( + within(dialog).getByText('退款订单总额').parentElement?.textContent, + ).toContain(lastErrorCode === 'order_total_mismatch' ? '¥7.00' : '¥6.00'); expect(submitButton.hasAttribute('disabled')).toBe(true); await user.type( within(dialog).getByRole('textbox', { name: '人工复核原因' }), @@ -498,6 +580,7 @@ test.each(['provider_transaction_id_mismatch', 'order_total_mismatch'])( { outRefundNo: 'refund-manual-1', reason: '已核对微信商户平台原始账单', + expectedErrorCode: lastErrorCode, }, ); }); @@ -516,7 +599,12 @@ test('非白名单、已处理和会员退款不显示人工复核按钮', async ...baseOrder, orderId: 'order-resolved', refunds: [ - manualReviewRefund({ manualReviewResolvedAtMicros: 1_720_000 }), + manualReviewRefund({ + manualReviewResolvedAtMicros: 1_720_000, + manualReviewResolvedByAdminUserId: 'admin-1', + manualReviewResolutionReason: '已核对商户平台', + manualReviewResolvedErrorCode: 'provider_transaction_id_mismatch', + }), ], }, { @@ -531,6 +619,10 @@ test('非白名单、已处理和会员退款不显示人工复核按钮', async await screen.findByText('order-membership'); expect(screen.queryByRole('button', { name: '人工复核' })).toBeNull(); + expect(screen.getByText(/复核 微信交易单号不一致/).textContent).toContain( + 'admin-1', + ); + expect(screen.getByText(/已核对商户平台/)).toBeTruthy(); }); test('支付侧已退款但泥点不足时持续展示异常欠账与消费限制', async () => { @@ -650,7 +742,9 @@ function manualReviewRefund( return { outRefundNo: 'refund-manual-1', providerRefundId: 'provider-refund-1', + providerTransactionId: 'wx-transaction-other', providerStatus: 'SUCCESS', + totalCents: 600, refundCents: 300, payerRefundCents: 300, successAtMicros: 1_720_000_000_000_000, @@ -665,6 +759,7 @@ function manualReviewRefund( manualReviewResolvedByAdminUserId: null, manualReviewResolutionReason: null, manualReviewResolvedAtMicros: null, + manualReviewResolvedErrorCode: null, ...overrides, }; } diff --git a/apps/admin-web/src/pages/AdminRechargeOrderPage.tsx b/apps/admin-web/src/pages/AdminRechargeOrderPage.tsx index 26a5f32d7..d1f4848b0 100644 --- a/apps/admin-web/src/pages/AdminRechargeOrderPage.tsx +++ b/apps/admin-web/src/pages/AdminRechargeOrderPage.tsx @@ -59,7 +59,7 @@ interface PersistedRefundRequestContext { } interface ManualReviewTarget { - orderId: string; + order: AdminRechargeOrderEntryPayload; refund: AdminRechargeRefundPayload; } @@ -277,7 +277,7 @@ export function AdminRechargeOrderPage({ ) { return; } - const reason = refundReason.trim(); + const reason = normalizeRefundReason(refundReason); if (!reason) { setRefundError('请填写退款原因'); return; @@ -403,7 +403,7 @@ export function AdminRechargeOrderPage({ order: AdminRechargeOrderEntryPayload, refund: AdminRechargeRefundPayload, ) { - setManualReviewTarget({ orderId: order.orderId, refund }); + setManualReviewTarget({ order, refund }); setManualReviewReason(''); setManualReviewError(''); } @@ -411,15 +411,23 @@ export function AdminRechargeOrderPage({ async function handleResolveManualReview(event: FormEvent) { event.preventDefault(); const reason = manualReviewReason.trim(); - if (!manualReviewTarget || manualReviewLockRef.current || !reason) { + const expectedErrorCode = manualReviewTarget?.refund.lastErrorCode?.trim(); + if ( + !manualReviewTarget || + manualReviewLockRef.current || + !reason || + !expectedErrorCode + ) { if (!reason) { setManualReviewError('请填写人工复核原因'); + } else if (!expectedErrorCode) { + setManualReviewError('退款人工复核状态已变化,请刷新后重试'); } return; } const confirmed = await confirmWrite({ action: '确认微信退款并追回泥点', - target: `${manualReviewTarget.orderId} / ${manualReviewTarget.refund.outRefundNo}`, + target: `${manualReviewTarget.order.orderId} / ${manualReviewTarget.refund.outRefundNo}`, }); if (!confirmed || manualReviewLockRef.current) { return; @@ -432,6 +440,7 @@ export function AdminRechargeOrderPage({ const response = await resolveAdminRechargeRefundManualReview(token, { outRefundNo: manualReviewTarget.refund.outRefundNo, reason, + expectedErrorCode, }); setLastAction(response); upsertOrder(response.order); @@ -773,6 +782,7 @@ export function AdminRechargeOrderPage({ setRefundReason(event.target.value)} /> @@ -967,6 +977,36 @@ export function AdminRechargeOrderPage({