diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx
index 1ab9e14..0e373ea 100644
--- a/apps/admin-web/src/App.tsx
+++ b/apps/admin-web/src/App.tsx
@@ -30,6 +30,7 @@ import ProductsPage from './pages/ProductsPage';
import ProductDetailTemplatesPage from './pages/ProductDetailTemplatesPage';
import ResourcesPage from './pages/ResourcesPage';
import StoreBillsPage from './pages/StoreBillsPage';
+import StoreWithdrawalsPage from './pages/StoreWithdrawalsPage';
import PartnerBillsPage from './pages/PartnerBillsPage';
import WineryBillsPage from './pages/WineryBillsPage';
import LogisticsBillsPage from './pages/LogisticsBillsPage';
@@ -96,11 +97,13 @@ export default function App() {
} />
} />
} />
+ } />
} />
} />
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx
index 8406a2a..b8240b9 100644
--- a/apps/admin-web/src/layouts/AdminLayout.tsx
+++ b/apps/admin-web/src/layouts/AdminLayout.tsx
@@ -74,6 +74,7 @@ const MENU_ITEMS: MenuProps['items'] = [
label: '财务',
children: [
{ key: '/finance/store-bills', label: '门店账单' },
+ { key: '/finance/store-withdrawals', label: '门店提现审' },
{ key: '/finance/partner-bills', label: '合伙人账单' },
{ key: '/finance/winery-bills', label: '酒厂账单' },
{ key: '/finance/logistics-bills', label: '物流对账' },
@@ -157,6 +158,7 @@ function menuAllowed(key: string, permissionKeys: string[]): boolean {
'/fulfillment-providers': 'partners',
'finance-group': 'finance',
'/finance/store-bills': 'finance',
+ '/finance/store-withdrawals': 'finance',
'/finance/partner-bills': 'finance',
'/finance/winery-bills': 'finance',
'/finance/logistics-bills': 'finance',
diff --git a/apps/admin-web/src/lib/api.ts b/apps/admin-web/src/lib/api.ts
index d7f5c9c..8abd79b 100644
--- a/apps/admin-web/src/lib/api.ts
+++ b/apps/admin-web/src/lib/api.ts
@@ -85,6 +85,8 @@ export type DashboardStats = {
pendingBills?: number;
pendingPartnerDraftBills?: number;
openTickets?: number;
+ pendingStoreWithdrawals?: number;
+ overdueStoreWithdrawals?: number;
ordersByStatus: Array<{ status: string; count: number }>;
};
diff --git a/apps/admin-web/src/lib/storeCreate.ts b/apps/admin-web/src/lib/storeCreate.ts
index 37fa198..555d155 100644
--- a/apps/admin-web/src/lib/storeCreate.ts
+++ b/apps/admin-web/src/lib/storeCreate.ts
@@ -29,6 +29,7 @@ export type StoreCreateForm = {
settlementRate?: number;
visibilityWhitelistEnabled?: boolean;
visibilityPhones?: string[];
+ withdrawWhitelistEnabled?: boolean;
};
const PHONE_RE = /^1\d{10}$/;
diff --git a/apps/admin-web/src/pages/DashboardPage.tsx b/apps/admin-web/src/pages/DashboardPage.tsx
index 041a715..9a2d77c 100644
--- a/apps/admin-web/src/pages/DashboardPage.tsx
+++ b/apps/admin-web/src/pages/DashboardPage.tsx
@@ -717,6 +717,26 @@ export default function DashboardPage() {
/>
+
+ 去处理}
+ >
+ 0 ? '#cf1322' : (stats?.pendingStoreWithdrawals ?? 0) > 0 ? '#fa8c16' : undefined,
+ }}
+ />
+
+ {(stats?.overdueStoreWithdrawals ?? 0) > 0
+ ? `超时未审 ${stats?.overdueStoreWithdrawals} 笔(FIN-003)`
+ : '工作日 T+0 审完'}
+
+
+
diff --git a/apps/admin-web/src/pages/StoreWithdrawalsPage.tsx b/apps/admin-web/src/pages/StoreWithdrawalsPage.tsx
new file mode 100644
index 0000000..3c163e3
--- /dev/null
+++ b/apps/admin-web/src/pages/StoreWithdrawalsPage.tsx
@@ -0,0 +1,394 @@
+import { useEffect, useState } from 'react';
+import {
+ Button,
+ DatePicker,
+ Descriptions,
+ Drawer,
+ Form,
+ Input,
+ Modal,
+ Select,
+ Space,
+ Table,
+ Tag,
+ Typography,
+ message,
+} from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import type { Dayjs } from 'dayjs';
+import { STORE_WITHDRAW_STATUS_LABELS, type StoreWithdrawStatus } from '@dukang/shared-types';
+import { request, type Paginated } from '../lib/api';
+import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
+import { useAdminList } from '../lib/useAdminList';
+
+type Row = {
+ id: string;
+ withdrawNo: string;
+ amount: number;
+ payoutCount: number;
+ status: StoreWithdrawStatus;
+ appliedAt: string;
+ reviewedAt?: string | null;
+ paidAt?: string | null;
+ paymentRef?: string | null;
+ rejectReason?: string | null;
+ overdue?: boolean;
+ store?: { id: string; name: string; cityName: string; phone?: string };
+};
+
+type StoreOption = { id: string; name: string; phone: string };
+
+const STATUS_COLORS: Record = {
+ PENDING_REVIEW: 'orange',
+ REJECTED: 'red',
+ PAID: 'green',
+};
+
+export default function StoreWithdrawalsPage() {
+ const [form] = Form.useForm();
+ const [filters, setFilters] = useState>({ status: 'PENDING_REVIEW' });
+ const [stores, setStores] = useState([]);
+ const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList(
+ '/admin/store-withdrawals',
+ () => {
+ const qs = new URLSearchParams();
+ if (filters.status) qs.set('status', filters.status);
+ if (filters.storeId) qs.set('storeId', filters.storeId);
+ if (filters.dateFrom) qs.set('dateFrom', filters.dateFrom);
+ if (filters.dateTo) qs.set('dateTo', filters.dateTo);
+ return qs;
+ },
+ [filters],
+ );
+ const [detail, setDetail] = useState | null>(null);
+ const [drawerOpen, setDrawerOpen] = useState(false);
+ const [overdueSummary, setOverdueSummary] = useState<{
+ pendingCount: number;
+ overdueCount: number;
+ } | null>(null);
+
+ useEffect(() => {
+ void request>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
+ .then((res) => setStores(res.items))
+ .catch(() => {});
+ void request<{ pendingCount: number; overdueCount: number }>(
+ '/admin/store-withdrawals/overdue-summary',
+ )
+ .then(setOverdueSummary)
+ .catch(() => {});
+ }, []);
+
+ async function openDetail(id: string) {
+ const d = await request>(`/admin/store-withdrawals/${id}`);
+ setDetail(d);
+ setDrawerOpen(true);
+ }
+
+ function approve(id: string) {
+ let paymentRef = '';
+ Modal.confirm({
+ title: '审核通过并标记已结算?',
+ content: (
+ {
+ paymentRef = e.target.value;
+ }}
+ />
+ ),
+ okText: '通过并结算',
+ cancelText: '取消',
+ onOk: async () => {
+ await request(`/admin/store-withdrawals/${id}/approve`, {
+ method: 'POST',
+ body: JSON.stringify({ paymentRef: paymentRef.trim() || undefined }),
+ });
+ message.success('已通过并标记已结算');
+ setDrawerOpen(false);
+ reload();
+ void request<{ pendingCount: number; overdueCount: number }>(
+ '/admin/store-withdrawals/overdue-summary',
+ )
+ .then(setOverdueSummary)
+ .catch(() => {});
+ },
+ });
+ }
+
+ function reject(id: string) {
+ let reason = '';
+ Modal.confirm({
+ title: '驳回提现申请?',
+ content: (
+ {
+ reason = e.target.value;
+ }}
+ />
+ ),
+ okText: '确认驳回',
+ okButtonProps: { danger: true },
+ cancelText: '取消',
+ onOk: async () => {
+ if (!reason.trim()) {
+ message.error('请填写驳回理由');
+ throw new Error('reason required');
+ }
+ await request(`/admin/store-withdrawals/${id}/reject`, {
+ method: 'POST',
+ body: JSON.stringify({ reason: reason.trim() }),
+ });
+ message.success('已驳回');
+ setDrawerOpen(false);
+ reload();
+ },
+ });
+ }
+
+ const columns: ColumnsType = [
+ {
+ title: '提现单号',
+ dataIndex: 'withdrawNo',
+ width: 180,
+ render: (v, row) => (
+
+ void openDetail(row.id)}>{v}
+ {row.overdue ? 超时 : null}
+
+ ),
+ },
+ {
+ title: '门店',
+ dataIndex: ['store', 'name'],
+ width: 160,
+ render: (_, row) => (
+
+
{row.store?.name || '—'}
+
+ {row.store?.cityName} {row.store?.phone}
+
+
+ ),
+ },
+ {
+ title: '金额',
+ dataIndex: 'amount',
+ width: 110,
+ render: (v) => `¥${Number(v).toFixed(2)}`,
+ },
+ { title: '明细笔数', dataIndex: 'payoutCount', width: 90 },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 100,
+ render: (v: StoreWithdrawStatus) => (
+ {STORE_WITHDRAW_STATUS_LABELS[v] ?? v}
+ ),
+ },
+ {
+ title: '申请时间',
+ dataIndex: 'appliedAt',
+ width: 170,
+ render: (v) => fmtTime(v),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 180,
+ fixed: 'right',
+ render: (_, row) => (
+
+
+ {row.status === 'PENDING_REVIEW' ? (
+ <>
+
+
+ >
+ ) : null}
+
+ ),
+ },
+ ];
+
+ const detailItems = (detail?.items as Array> | undefined) ?? [];
+ const storeAccount = detail?.storeAccount as
+ | {
+ name?: string;
+ phone?: string;
+ bankAccountName?: string;
+ bankAccountNo?: string;
+ bankBranch?: string;
+ }
+ | undefined;
+
+ return (
+
+
+ 门店提现审
+
+ {overdueSummary ? (
+
+ 待审 {overdueSummary.pendingCount} 笔
+ {overdueSummary.overdueCount > 0 ? (
+
+ {' '}
+ · 超时未审 {overdueSummary.overdueCount} 笔(FIN-003)
+
+ ) : null}
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(row.overdue ? 'ant-table-row-selected' : '')}
+ pagination={{
+ current: page,
+ pageSize,
+ total: data?.total ?? 0,
+ showSizeChanger: true,
+ onChange: (p, ps) => {
+ setPage(p);
+ setPageSize(ps);
+ },
+ }}
+ />
+
+ setDrawerOpen(false)}
+ extra={
+ detail?.status === 'PENDING_REVIEW' ? (
+
+
+
+
+ ) : null
+ }
+ >
+ {detail ? (
+ <>
+
+ {String(detail.withdrawNo)}
+
+
+ {STORE_WITHDRAW_STATUS_LABELS[detail.status as StoreWithdrawStatus] ??
+ String(detail.status)}
+
+ {detail.overdue ? 超时 : null}
+
+ ¥{Number(detail.amount).toFixed(2)}
+ {String(detail.payoutCount)}
+ {fmtTime(String(detail.appliedAt))}
+ {detail.rejectReason ? (
+ {String(detail.rejectReason)}
+ ) : null}
+ {detail.paymentRef ? (
+ {String(detail.paymentRef)}
+ ) : null}
+
+ {storeAccount?.bankAccountName || '—'}
+
+
+ {storeAccount?.bankAccountNo || '—'}
+
+ {storeAccount?.bankBranch || '—'}
+
+
+
+ 关联结算明细
+
+ String((r as { id?: string }).id)}
+ pagination={false}
+ dataSource={detailItems}
+ columns={[
+ {
+ title: '核销单号',
+ render: (_, r) => {
+ const payout = (r as { storePayout?: { redeemRecord?: { redeemNo?: string } } })
+ .storePayout;
+ return payout?.redeemRecord?.redeemNo || '—';
+ },
+ },
+ {
+ title: '结算额',
+ render: (_, r) => {
+ const payout = (r as { storePayout?: { payoutAmount?: number } }).storePayout;
+ return `¥${Number(payout?.payoutAmount ?? 0).toFixed(2)}`;
+ },
+ },
+ ]}
+ />
+ >
+ ) : null}
+
+
+ );
+}
diff --git a/apps/admin-web/src/pages/StoresPage.tsx b/apps/admin-web/src/pages/StoresPage.tsx
index 1785d61..0e710f5 100644
--- a/apps/admin-web/src/pages/StoresPage.tsx
+++ b/apps/admin-web/src/pages/StoresPage.tsx
@@ -373,6 +373,7 @@ type StoreRow = {
coverUrl: string | null;
createdAt: string;
visibilityWhitelistEnabled?: boolean;
+ withdrawWhitelistEnabled?: boolean;
visibilityPhones?: string[];
cityRef?: { name: string; code: string };
partner?: { companyName: string };
@@ -592,6 +593,7 @@ export default function StoresPage() {
bankAccountNo: account?.bankAccountNo || undefined,
bankBranch: account?.bankBranch || undefined,
visibilityWhitelistEnabled: !!d.visibilityWhitelistEnabled,
+ withdrawWhitelistEnabled: !!d.withdrawWhitelistEnabled,
visibilityPhones: Array.isArray(d.visibilityPhones)
? (d.visibilityPhones as string[])
: [],
@@ -635,6 +637,7 @@ export default function StoresPage() {
bankAccountNo: v.bankAccountNo ?? null,
bankBranch: v.bankBranch ?? null,
visibilityWhitelistEnabled: !!v.visibilityWhitelistEnabled,
+ withdrawWhitelistEnabled: !!v.withdrawWhitelistEnabled,
visibilityPhones: ((v.visibilityPhones as string[] | undefined) ?? [])
.map((p) => String(p || '').replace(/\D/g, '').trim())
.filter(Boolean),
@@ -811,6 +814,7 @@ export default function StoresPage() {
bankBranch: values.bankBranch.trim(),
settlementRate: Number(values.settlementRate ?? 60) / 100,
visibilityWhitelistEnabled: !!values.visibilityWhitelistEnabled,
+ withdrawWhitelistEnabled: !!values.withdrawWhitelistEnabled,
visibilityPhones: (values.visibilityPhones ?? [])
.map((p: string) => String(p || '').replace(/\D/g, '').trim())
.filter(Boolean),
@@ -1180,6 +1184,14 @@ export default function StoresPage() {
+
+
+
@@ -1426,6 +1438,14 @@ export default function StoresPage() {
+
+
+
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/h5-shop/src/pages/MinePage.tsx b/apps/h5-shop/src/pages/MinePage.tsx
index 72a0bf0..62a489e 100644
--- a/apps/h5-shop/src/pages/MinePage.tsx
+++ b/apps/h5-shop/src/pages/MinePage.tsx
@@ -141,6 +141,10 @@ export default function MinePage() {
切换门店
) : null}
+
{profile?.isPrimary ? (