diff --git a/docs/Phase1-验收报告.md b/docs/Phase1-验收报告.md index ba9a345..a37f949 100644 --- a/docs/Phase1-验收报告.md +++ b/docs/Phase1-验收报告.md @@ -194,9 +194,9 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB ## 5. 已知限制 1. 移动端订单列表/详情与消息中心已接入真实接口;下单、支付、交接、归还、结账等操作仍待迁移。 -2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁,其余页面仍为统一占位。 +2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板,其余页面仍为统一占位。 3. 分销后台已完成主题、Logo、登录页与 Shell 轻改,页面结构未重写。 -4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`;其余页面仍需核对。 +4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`;其余页面仍需核对。 5. SVG 中文尚未转曲。 6. 入口包已降至 495 kB(gzip 161 kB);AntD 共享基础设施仍在入口,后续可拆独立入口。 7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理与审核在本批次提交。 diff --git a/docs/Phase2-财务看板验收.md b/docs/Phase2-财务看板验收.md new file mode 100644 index 0000000..5e3ceab --- /dev/null +++ b/docs/Phase2-财务看板验收.md @@ -0,0 +1,65 @@ +# Phase 2 验收报告(后台财务看板) + +> 日期:2026-10-30 +> 项目:解忧商行(hfb_jysh) +> 范围:`/console-jy8k3m/finance` +> 结论:**本切片通过,财务总览、每日趋势与财务明细已接真实接口** + +--- + +## 1. 接口与权限 + +| 能力 | 接口 | 权限码 | +| --- | --- | --- | +| 财务总览 | `GET /api/admin/finance/dashboard` | `wallet:view` | +| 财务明细 | `GET /api/admin/finance/details` | `wallet:view` | + +日期参数:`start_date` / `end_date`(`YYYY-MM-DD`)。前端菜单权限已从推断值 `finance:view` 修正为 `wallet:view`。 + +## 2. 页面结构 + +`frontend/src/features/admin/AdminFinancePage.tsx` + +- 顶部日期区间:默认近 7 天,可切换任意区间;总览与明细共用区间 +- 主 KPI:总流水、总退款、平台收入、号主应得、渠道净额、资金异常、待打款、提现待审 +- 次级摘要:待结算订单、线下提号、撞车商城净额、运营支出 +- 每日趋势:流水、退款、渠道净额、平台收入、号主应得、结算/支付订单数、运营支出、已打款 +- 财务明细:订单号、双方、订单金额、结账金额、支付/退款/渠道净额、财务状态、结算时间 +- 明细支持订单号精确查询、日期类型(下单/结算/线下结算)、分页 +- 点击订单号跳转订单管理并带上 `keyword` 查询 + +## 3. 财务状态 + +| 值 | 文案 | +| --- | --- | +| `normal` | 正常 | +| `refund_pending` | 退款中 | +| `refund_failed` | 退款失败 | +| `settlement_diff` | 结算差异 | +| `offline_settlement_pending` | 待线下结算 | + +## 4. 验证结果 + +```text +npx tsc --noEmit 通过 +npm run build 通过(入口 496.68 kB,gzip 161.26 kB) +GET /api/admin/finance/dashboard?start_date=2026-10-24&end_date=2026-10-30 ok,返回 summary/daily/各摘要 +GET /api/admin/finance/details?...&page=1&page_size=5 ok,{"items":[],"total":0} +GET /console-jy8k3m/finance 200 +``` + +开发库没有订单流水,总览与明细均为 0;接口字段与 DTO 已核对。 + +## 5. 已知限制 + +1. 未接线下出款、人工出款、运营支出登记与作废接口(需要 `finance:manual_disbursement` / `finance:operating_expense`)。 +2. 未做图表可视化,当前以 KPI + 表格呈现。 +3. 财务明细不支持按财务状态筛选(后端未提供该参数)。 +4. 未做导出与对账单下载。 + +## 6. 下一步建议 + +1. 后台公告管理与系统配置。 +2. 后台审计日志。 +3. 线下出款/运营支出登记。 +4. 移动端下单、支付与交接/归还/结账。 diff --git a/docs/项目计划.md b/docs/项目计划.md index ce9285b..4b4a65e 100644 --- a/docs/项目计划.md +++ b/docs/项目计划.md @@ -395,7 +395,7 @@ hfb_jysh/ ### Phase 4:管理后台核心(4–5 周) -> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁;财务看板、系统配置与审计日志待迁移。 +> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉与财务看板;线下出款/运营支出、系统配置与审计日志待迁移。 - 后台登录、工作台、订单列表、订单详情。 - 商品列表、商品详情、审核。 diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 0aa8aac..9f771d9 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -93,6 +93,11 @@ const AdminDisputesPage = lazy(() => default: module.AdminDisputesPage, })) ) +const AdminFinancePage = lazy(() => + import('@/features/admin/AdminFinancePage').then(module => ({ + default: module.AdminFinancePage, + })) +) const AdminLayout = lazy(() => import('@/layouts/AdminLayout').then(module => ({ default: module.AdminLayout })) ) @@ -101,7 +106,6 @@ const AdminSectionPlaceholder = lazy(() => ) const adminSections: Array<{ path: string; title: string; description: string }> = [ - { path: 'finance', title: '财务看板', description: '查看收入、退款、结算与资金趋势。' }, { path: 'distribution/sites', title: '分销站点', description: '管理分站、规则、结算与站点钱包。' }, { path: 'announcements', title: '公告管理', description: '维护主站与分站公告。' }, { path: 'system-configs', title: '系统配置', description: '维护首页、发布、订单与支付配置。' }, @@ -144,6 +148,7 @@ export function AppRouter() { } /> } /> } /> + } /> {adminSections.map(section => ( = [ + { title: '日期', dataIndex: 'date', width: 120, fixed: 'left' }, + { + title: '流水', + dataIndex: 'total_flow_amount_cent', + width: 130, + align: 'right', + render: value => formatMoney(value), + }, + { + title: '退款', + dataIndex: 'total_refund_amount_cent', + width: 120, + align: 'right', + render: value => formatMoney(value), + }, + { + title: '渠道净额', + dataIndex: 'channel_net_amount_cent', + width: 130, + align: 'right', + render: value => formatMoney(value), + }, + { + title: '平台收入', + dataIndex: 'platform_income_amount_cent', + width: 130, + align: 'right', + render: value => {formatMoney(value)}, + }, + { + title: '号主应得', + dataIndex: 'owner_should_income_amount_cent', + width: 130, + align: 'right', + render: value => formatMoney(value), + }, + { + title: '已结算订单', + dataIndex: 'settled_order_count', + width: 110, + align: 'right', + }, + { + title: '支付订单', + dataIndex: 'paid_order_count', + width: 100, + align: 'right', + }, + { + title: '运营支出', + dataIndex: 'operating_expense_amount_cent', + width: 130, + align: 'right', + render: value => formatMoney(value), + }, + { + title: '已打款', + dataIndex: 'disbursement_paid_amount_cent', + width: 130, + align: 'right', + render: value => formatMoney(value), + }, +] + +function buildDetailColumns(onOpenOrder: (orderNo: string) => void): TableColumnsType { + return [ + { + title: '订单', + dataIndex: 'order_no', + width: 200, + fixed: 'left', + render: (_, row) => ( +
+ + + 结算 {formatText(row.settlement_status)} +
+ ), + }, + { + title: '用户', + key: 'users', + width: 170, + render: (_, row) => ( +
+ 租客 {formatText(row.renter_phone)} + 号主 {formatText(row.owner_phone)} +
+ ), + }, + { + title: '订单金额', + key: 'order_amount', + width: 140, + align: 'right', + render: (_, row) => ( +
+ {formatMoney(row.order_rent_amount_cent)} + 押金 {formatMoney(row.order_deposit_amount_cent)} +
+ ), + }, + { + title: '结账', + key: 'checkout', + width: 160, + align: 'right', + render: (_, row) => ( +
+ 租金 {formatMoney(row.checkout_rent_amount_cent)} + 号主 {formatMoney(row.checkout_owner_income_cent)} + 平台费 {formatMoney(row.checkout_platform_fee_cent)} +
+ ), + }, + { + title: '支付 / 退款', + key: 'payment', + width: 160, + align: 'right', + render: (_, row) => ( +
+ {formatMoney(row.paid_amount_cent)} + 退款 {formatMoney(row.refunded_amount_cent)} + 渠道净额 {formatMoney(row.channel_net_amount_cent)} +
+ ), + }, + { + title: '财务状态', + dataIndex: 'finance_status', + width: 130, + render: value => , + }, + { + title: '结算时间', + dataIndex: 'settled_at', + width: 170, + render: value => formatDateTime(value), + }, + { + title: '操作', + key: 'actions', + width: 100, + fixed: 'right', + render: (_, row) => ( + + ), + }, + ] +} + +export function AdminFinancePage() { + const navigate = useNavigate() + const [range, setRange] = useState<[Dayjs, Dayjs]>([ + dayjs().subtract(6, 'day'), + dayjs(), + ]) + const [keyword, setKeyword] = useState('') + const [appliedKeyword, setAppliedKeyword] = useState('') + const [dateType, setDateType] = useState('settled') + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(20) + + const startDate = range[0].format('YYYY-MM-DD') + const endDate = range[1].format('YYYY-MM-DD') + + const dashboard = useQuery({ + queryKey: ['finance-dashboard', startDate, endDate], + queryFn: () => fetchFinanceDashboard({ start_date: startDate, end_date: endDate }), + staleTime: 60_000, + }) + + const details = useQuery({ + queryKey: ['finance-details', startDate, endDate, appliedKeyword, dateType, page, pageSize], + queryFn: () => + fetchFinanceDetails({ + start_date: startDate, + end_date: endDate, + order_no: appliedKeyword, + date_type: dateType, + page, + page_size: pageSize, + }), + placeholderData: keepPreviousData, + }) + + const detailColumns = useMemo( + () => buildDetailColumns(orderNo => navigate(`/orders?keyword=${encodeURIComponent(orderNo)}`)), + [navigate] + ) + + const summary = dashboard.data?.summary + const disbursement = dashboard.data?.disbursement_summary + const pickup = dashboard.data?.pickup_summary + const mohong = dashboard.data?.mohong_summary + const expense = dashboard.data?.operating_expense_summary + const daily = dashboard.data?.daily_items ?? [] + + const applyDetailFilters = () => { + setAppliedKeyword(keyword.trim()) + setPage(1) + } + + const resetDetailFilters = () => { + setKeyword('') + setAppliedKeyword('') + setPage(1) + } + + const errorText = + dashboard.error instanceof Error ? dashboard.error.message : '请稍后重试或重新登录后台。' + + return ( +
+ + + {startDate} ~ {endDate} + + {dashboard.isFetching ? 正在更新 : null} + + } + extra={ + + { + if (value?.[0] && value[1]) { + setRange([value[0], value[1]]) + setPage(1) + } + }} + /> + + + } + /> + + {dashboard.isError ? ( + void dashboard.refetch()}> + 重试 + + } + /> + ) : null} + + + + +
+ +
+ + + 支付 {summary?.successful_pay_count ?? 0} 笔 · 退款{' '} + {summary?.successful_refund_count ?? 0} 笔 + +
+ + + +
+ +
+ + + 待退款 {formatMoney(summary?.pending_refund_amount_cent ?? 0)} ·{' '} + {summary?.pending_refund_count ?? 0} 笔 + +
+ + + +
+ +
+ + + 订单 {formatMoney(summary?.normal_order_profit_amount_cent ?? 0)} · 提号{' '} + {formatMoney(summary?.pickup_profit_amount_cent ?? 0)} + +
+ + + +
+ +
+ + + 钱包入账 {formatMoney(summary?.owner_wallet_income_amount_cent ?? 0)} + +
+ + + +
+ +
+ + + 已结算订单 {summary?.settled_order_count ?? 0} 笔 + +
+ + + +
+ +
+ + + 结算差异 {formatMoney(summary?.settlement_diff_amount_cent ?? 0)} + +
+ + + +
+ +
+ + + {disbursement?.pending_payment_count ?? 0} 笔 · 已打款{' '} + {formatMoney(disbursement?.paid_amount_cent ?? 0)} + +
+ + + +
+ +
+ + + {disbursement?.withdrawal_review_count ?? 0} 笔 · 待打款{' '} + {formatMoney(disbursement?.withdrawal_pending_amount_cent ?? 0)} + +
+ +
+ + + + +
+ 待结算订单 + {summary?.pending_settle_order_count ?? 0} +
+ 待线下结算 {formatMoney(summary?.offline_settlement_pending_amount_cent ?? 0)} +
+ + + +
+ 线下提号 + {pickup?.completed_count ?? 0} +
+ + 利润 {formatMoney(pickup?.profit_amount_cent ?? 0)} · 待结{' '} + {formatMoney(pickup?.pending_amount_cent ?? 0)} + +
+ + + +
+ 撞车商城净额 + {formatMoney(mohong?.net_amount_cent ?? 0)} +
+ + 流水 {formatMoney(mohong?.flow_amount_cent ?? 0)} · 退款{' '} + {formatMoney(mohong?.refund_amount_cent ?? 0)} + +
+ + + +
+ 运营支出 + {formatMoney(expense?.amount_cent ?? 0)} +
+ {expense?.count ?? 0} 笔已登记支出 +
+ +
+ + + className="admin-finance-daily" + title={每日趋势} + extra={数据更新于 {formatDateTime(dashboard.data?.generated_at)}} + rowKey="date" + columns={dailyColumns} + dataSource={daily} + loading={dashboard.isFetching} + emptyDescription="所选区间没有财务数据" + /> + + { + setDateType(value) + setPage(1) + }} + /> + } + > + + setKeyword(event.target.value)} + /> + + + + + title={财务明细} + extra={ + + {details.data ? `共 ${details.data.total} 条` : '加载中'} + + } + rowKey="order_id" + columns={detailColumns} + dataSource={details.data?.items ?? []} + loading={details.isFetching} + total={details.data?.total ?? 0} + page={details.data?.page ?? page} + pageSize={details.data?.page_size ?? pageSize} + onPageChange={(nextPage, nextPageSize) => { + setPage(nextPageSize !== pageSize ? 1 : nextPage) + setPageSize(nextPageSize) + }} + emptyDescription="所选区间没有财务明细" + /> +
+ ) +} diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts index 0142fbc..c884885 100644 --- a/frontend/src/features/admin/api.ts +++ b/frontend/src/features/admin/api.ts @@ -20,6 +20,8 @@ import type { AdminWithdrawalPage, AdminWithdrawalQuery, DashboardData, + FinanceDashboard, + FinanceDetailPage, } from '@/features/admin/types' export async function fetchAdminCaptcha() { @@ -311,3 +313,35 @@ export async function arbitrateAdminDispute( ) return data.data } + +export interface FinanceRangeQuery { + start_date?: string + end_date?: string +} + +export async function fetchFinanceDashboard(range: FinanceRangeQuery) { + const { data } = await adminClient.get>('/admin/finance/dashboard', { + params: compactParams(range), + }) + return data.data +} + +export async function fetchFinanceDetails( + query: FinanceRangeQuery & { + order_no?: string + date_type?: 'created' | 'settled' | 'offline_settled' + page?: number + page_size?: number + } +) { + const { data } = await adminClient.get>>( + '/admin/finance/details', + { params: compactParams(query) } + ) + return { + items: data.data.items ?? [], + total: data.data.total ?? 0, + page: data.data.page ?? query.page ?? 1, + page_size: data.data.page_size ?? query.page_size ?? 20, + } +} diff --git a/frontend/src/features/admin/labels.ts b/frontend/src/features/admin/labels.ts index b6537ab..3c148ef 100644 --- a/frontend/src/features/admin/labels.ts +++ b/frontend/src/features/admin/labels.ts @@ -189,3 +189,11 @@ export const withdrawalStatusMap: Record = { } export const withdrawalStatusOptions = toOptions(withdrawalStatusMap) + +export const financeStatusMap: Record = { + normal: { label: '正常', tone: 'success' }, + refund_pending: { label: '退款中', tone: 'blue' }, + refund_failed: { label: '退款失败', tone: 'red' }, + settlement_diff: { label: '结算差异', tone: 'orange' }, + offline_settlement_pending: { label: '待线下结算', tone: 'gold' }, +} diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts index 8573c57..d07054f 100644 --- a/frontend/src/features/admin/types.ts +++ b/frontend/src/features/admin/types.ts @@ -404,3 +404,132 @@ export interface AdminDisputePage { page: number page_size: number } + +export interface FinanceSummary { + total_flow_amount_cent: number + total_refund_amount_cent: number + pending_refund_amount_cent: number + channel_net_amount_cent: number + platform_income_amount_cent: number + normal_order_profit_amount_cent: number + pickup_profit_amount_cent: number + owner_should_income_amount_cent: number + owner_wallet_income_amount_cent: number + owner_effective_settlement_amount_cent: number + offline_settlement_amount_cent: number + offline_settlement_pending_amount_cent: number + settlement_diff_amount_cent: number + estimated_income_amount_cent: number + successful_pay_count: number + successful_refund_count: number + pending_refund_count: number + settled_order_count: number + pending_settle_order_count: number + offline_settlement_pending_count: number + financial_exception_count: number +} + +export interface FinanceDaily { + date: string + total_flow_amount_cent: number + total_refund_amount_cent: number + platform_income_amount_cent: number + channel_net_amount_cent: number + owner_should_income_amount_cent: number + settled_order_count: number + paid_order_count: number + operating_expense_amount_cent: number + disbursement_paid_amount_cent: number +} + +export interface FinancePickupSummary { + settled_amount_cent: number + profit_amount_cent: number + completed_count: number + in_progress_count: number + pending_amount_cent: number + pending_count: number +} + +export interface FinanceMohongSummary { + flow_amount_cent: number + refund_amount_cent: number + net_amount_cent: number + paid_order_count: number +} + +export interface FinanceDisbursementSummary { + paid_amount_cent: number + paid_count: number + pending_payment_amount_cent: number + pending_payment_count: number + withdrawal_amount_cent: number + withdrawal_fee_amount_cent: number + withdrawal_paid_amount_cent: number + withdrawal_paid_count: number + withdrawal_pending_amount_cent: number + withdrawal_pending_count: number + withdrawal_review_amount_cent: number + withdrawal_review_count: number + manual_paid_amount_cent: number + manual_paid_count: number +} + +export interface FinanceOperatingExpenseSummary { + amount_cent: number + count: number +} + +export interface FinanceDashboard { + summary: FinanceSummary + daily_items: FinanceDaily[] + pickup_summary: FinancePickupSummary + mohong_summary: FinanceMohongSummary + disbursement_summary: FinanceDisbursementSummary + operating_expense_summary: FinanceOperatingExpenseSummary + generated_at: string +} + +export interface FinanceDetail { + order_id: number + order_no: string + order_status: string + settlement_status: string + refund_status: string + handoff_mode: string + settlement_mode: string + offline_settlement_status: string + offline_settlement_amount_cent: number + renter_id: number + renter_phone: string + renter_nickname: string + owner_id: number + owner_phone: string + owner_nickname: string + order_rent_amount_cent: number + order_deposit_amount_cent: number + checkout_rent_amount_cent: number + checkout_renter_refund_cent: number + checkout_owner_income_cent: number + checkout_platform_fee_cent: number + owner_wallet_income_amount_cent: number + owner_effective_settlement_amount_cent: number + paid_amount_cent: number + refunded_amount_cent: number + refunding_amount_cent: number + failed_refund_amount_cent: number + channel_net_amount_cent: number + platform_net_amount_cent: number + settlement_diff_amount_cent: number + finance_status: string + created_at: string + settled_at?: string | null + offline_settled_at?: string | null +} + +export interface FinanceDetailPage { + items: FinanceDetail[] + total: number + page: number + page_size: number +} diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx index e74a9ce..dfe0e6f 100644 --- a/frontend/src/layouts/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout.tsx @@ -83,7 +83,7 @@ const allNavGroups: NavGroup[] = [ label: '财务', icon: , children: [ - { key: '/finance', label: '财务看板', permission: 'finance:view' }, + { key: '/finance', label: '财务看板', permission: 'wallet:view' }, { key: '/withdrawals', label: '提现审核', permission: 'withdrawal:list' }, ], }, diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 6132960..2289c48 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1403,6 +1403,15 @@ a { margin-bottom: 16px; } +.admin-finance-mini { + margin-top: 16px; +} + +.admin-finance-mini .admin-pending-card > .ant-card-body > span { + color: var(--ink-muted); + font-size: 11px; +} + .admin-detail { display: flex; flex-direction: column;