- 门店对账单
+ 门店账单
- 按核销日汇总(每日 8:00 自动出账);未打款红色、已打款绿色
+ 含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口
+ {overdueSummary && overdueSummary.overdueCount > 0 ? (
+
+ 手动提现超时未审 {overdueSummary.overdueCount} 笔(待审共 {overdueSummary.pendingCount})
+
+ ) : null}
{summary && (
-
+
-
+
)}
@@ -208,16 +356,37 @@ export default function StoreBillsPage() {
form={form}
layout="inline"
style={{ marginBottom: 16 }}
- onFinish={(v: { storeId?: string; status?: string; range?: [Dayjs, Dayjs] }) => {
+ initialValues={{
+ kind: filters.kind || undefined,
+ status: filters.status || undefined,
+ }}
+ onFinish={(v: {
+ kind?: string;
+ storeId?: string;
+ status?: string;
+ range?: [Dayjs, Dayjs];
+ }) => {
setFilters({
+ kind: v.kind || '',
storeId: v.storeId || '',
status: v.status || '',
dateFrom: v.range?.[0] ? v.range[0].format('YYYY-MM-DD') : '',
dateTo: v.range?.[1] ? v.range[1].format('YYYY-MM-DD') : '',
});
setPage(1);
+ setSelectedKeys([]);
}}
>
+
+
-
+
@@ -249,6 +424,7 @@ export default function StoreBillsPage() {
form.resetFields();
setFilters({});
setPage(1);
+ setSelectedKeys([]);
}}
>
重置
@@ -256,33 +432,36 @@ export default function StoreBillsPage() {
`${r.kind}-${r.id}`}
className="admin-table-nowrap"
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
+ rowClassName={(row) => (row.overdue ? 'ant-table-row-selected' : '')}
rowSelection={{
selectedRowKeys: selectedKeys,
onChange: setSelectedKeys,
- getCheckboxProps: (r) => ({ disabled: r.status !== 'UNPAID' }),
+ getCheckboxProps: (r) => ({
+ disabled: !(r.kind === 'T1_BILL' && r.status === 'UNPAID'),
+ }),
}}
- scroll={{ x: 1200 }}
+ scroll={{ x: 1300 }}
pagination={{
current: page,
pageSize,
@@ -295,14 +474,25 @@ export default function StoreBillsPage() {
}}
/>
- setDrawerOpen(false)} width={520}>
- {detail && (
+ setDrawerOpen(false)}
+ width={detailKind === 'WITHDRAW' ? 640 : 520}
+ >
+ {detail && detailKind === 'T1_BILL' && (
<>
{String(detail.billNo)}
- {String(detail.billDate || '').slice(0, 10)}
- ¥{Number(detail.payoutAmount ?? 0).toFixed(2)}
- {STATUS_LABELS[String(detail.status)] || String(detail.status)}
+
+ {String(detail.billDate || '').slice(0, 10)}
+
+
+ ¥{Number(detail.payoutAmount ?? 0).toFixed(2)}
+
+
+ {storeSettlementStatusLabel('T1_BILL', String(detail.status))}
+
{detail.paidAt ? fmtTime(String(detail.paidAt)) : '—'}
@@ -318,7 +508,8 @@ export default function StoreBillsPage() {
columns={[
{
title: '核销单号',
- render: (_, r) => String((r.redeemRecord as { redeemNo?: string })?.redeemNo || '—'),
+ render: (_, r) =>
+ String((r.redeemRecord as { redeemNo?: string })?.redeemNo || '—'),
},
{
title: '金额',
@@ -334,6 +525,83 @@ export default function StoreBillsPage() {
/>
>
)}
+ {detail && detailKind === 'WITHDRAW' && (
+ <>
+
+ {String(detail.withdrawNo)}
+
+ ¥{Number(detail.amount ?? 0).toFixed(2)}
+
+
+ {storeSettlementStatusLabel('WITHDRAW', String(detail.status))}
+
+
+ {detail.appliedAt ? fmtTime(String(detail.appliedAt)) : '—'}
+
+
+ {detail.rejectReason ? String(detail.rejectReason) : '—'}
+
+
+ {detail.paymentRef ? String(detail.paymentRef) : '—'}
+
+
+ {storeAccount ? (
+ <>
+
+ 收款账户
+
+
+
+ {storeAccount.bankAccountName || '—'}
+
+
+ {storeAccount.bankAccountNo || '—'}
+
+
+ {storeAccount.bankBranch || '—'}
+
+
+ >
+ ) : null}
+
+ 提现明细
+
+ {
+ const payout = r.storePayout as
+ | { redeemRecord?: { redeemNo?: string }; payoutAmount?: number }
+ | undefined;
+ return String(payout?.redeemRecord?.redeemNo || '—');
+ },
+ },
+ {
+ title: '应付',
+ render: (_, r) => {
+ const payout = r.storePayout as { payoutAmount?: number } | undefined;
+ return `¥${Number(payout?.payoutAmount ?? 0).toFixed(2)}`;
+ },
+ },
+ ]}
+ />
+ {String(detail.status) === 'PENDING_REVIEW' ? (
+
+
+
+
+ ) : null}
+ >
+ )}
);
diff --git a/packages/shared-types/src/settlement.ts b/packages/shared-types/src/settlement.ts
index fca95d8..3697b0d 100644
--- a/packages/shared-types/src/settlement.ts
+++ b/packages/shared-types/src/settlement.ts
@@ -11,6 +11,44 @@ export const STORE_WITHDRAW_STATUS_LABELS: Record =
PAID: '已结算',
};
+/** 总部「门店账单」统一列表:T+1 出账 / 手动提现 */
+export type StoreSettlementKind = 'T1_BILL' | 'WITHDRAW';
+
+export const STORE_SETTLEMENT_KIND_LABELS: Record = {
+ T1_BILL: 'T+1出账',
+ WITHDRAW: '手动提现',
+};
+
+export type StoreSettlementStatus = FinancePayStatus | StoreWithdrawStatus;
+
+export const STORE_SETTLEMENT_STATUS_LABELS: Record = {
+ UNPAID: '未打款',
+ PAID: '已打款',
+ PENDING_REVIEW: '待审核',
+ REJECTED: '已驳回',
+};
+
+export interface StoreSettlementRowDto {
+ kind: StoreSettlementKind;
+ id: string;
+ billNo: string;
+ storeId: string;
+ amount: number;
+ status: StoreSettlementStatus;
+ date: string;
+ overdue?: boolean;
+ redeemCount?: number | null;
+ redeemAmount?: number | null;
+ settlementRate?: number | null;
+ payoutCount?: number | null;
+ store?: {
+ id: string;
+ name: string;
+ cityName: string;
+ phone?: string | null;
+ };
+}
+
export interface StoreWithdrawBankAccountDto {
bankAccountName?: string | null;
bankAccountNo?: string | null;
@@ -73,6 +111,19 @@ export const FINANCE_PAY_STATUS_LABELS: Record = {
PAID: '已打款',
};
+export function storeSettlementStatusLabel(
+ kind: StoreSettlementKind,
+ status: string,
+): string {
+ if (kind === 'WITHDRAW' && status in STORE_WITHDRAW_STATUS_LABELS) {
+ return STORE_WITHDRAW_STATUS_LABELS[status as StoreWithdrawStatus];
+ }
+ if (status in FINANCE_PAY_STATUS_LABELS) {
+ return FINANCE_PAY_STATUS_LABELS[status as FinancePayStatus];
+ }
+ return STORE_SETTLEMENT_STATUS_LABELS[status as StoreSettlementStatus] ?? status;
+}
+
export interface PartnerBillDto {
id: string;
billNo: string;
diff --git a/server/dukang-api/src/modules/settlement/settlement.controller.ts b/server/dukang-api/src/modules/settlement/settlement.controller.ts
index 58e63e6..171251f 100644
--- a/server/dukang-api/src/modules/settlement/settlement.controller.ts
+++ b/server/dukang-api/src/modules/settlement/settlement.controller.ts
@@ -224,6 +224,25 @@ export class AdminStorePayoutController {
}
}
+@Controller('admin/store-settlements')
+@UseGuards(HqAuthGuard)
+export class AdminStoreSettlementController {
+ constructor(private readonly settlementService: SettlementService) {}
+
+ @Get()
+ list(@Query() query: Record) {
+ return this.settlementService.listAdminStoreSettlements({
+ page: query.page ? Number(query.page) : 1,
+ pageSize: query.pageSize ? Number(query.pageSize) : 20,
+ kind: query.kind,
+ status: query.status,
+ storeId: query.storeId,
+ dateFrom: query.dateFrom,
+ dateTo: query.dateTo,
+ });
+ }
+}
+
@Controller('admin/store-bills')
@UseGuards(HqAuthGuard)
export class AdminStoreBillController {
diff --git a/server/dukang-api/src/modules/settlement/settlement.module.ts b/server/dukang-api/src/modules/settlement/settlement.module.ts
index e0348e9..bfff4ae 100644
--- a/server/dukang-api/src/modules/settlement/settlement.module.ts
+++ b/server/dukang-api/src/modules/settlement/settlement.module.ts
@@ -9,6 +9,7 @@ import {
AdminPartnerBillController,
AdminStoreBillController,
AdminStorePayoutController,
+ AdminStoreSettlementController,
AdminStoreWithdrawController,
AdminWineryBillController,
PartnerMeController,
@@ -26,6 +27,7 @@ import {
ShopWithdrawController,
AdminStoreWithdrawController,
AdminStorePayoutController,
+ AdminStoreSettlementController,
AdminStoreBillController,
AdminPartnerBillController,
AdminWineryBillController,
diff --git a/server/dukang-api/src/modules/settlement/settlement.service.ts b/server/dukang-api/src/modules/settlement/settlement.service.ts
index bb43b3b..163c15a 100644
--- a/server/dukang-api/src/modules/settlement/settlement.service.ts
+++ b/server/dukang-api/src/modules/settlement/settlement.service.ts
@@ -339,7 +339,7 @@ export class SettlementService {
`单号:${created.withdrawNo}`,
`金额:¥${Number(created.amount).toFixed(2)}`,
`明细:${created.payoutCount} 笔未出账核销(已锁定,不进入次日 T+1 出账)`,
- '请尽快在 HQ「财务 → 门店提现审」处理。',
+ '请尽快在 HQ「财务 → 门店账单(手动提现)」处理。',
].join('\n'),
dedupeKey: `store_withdraw_applied|${created.id.toString()}`,
dedupeTtlSec: 3600,
@@ -594,7 +594,7 @@ export class SettlementService {
detail: [
`待审 ${summary.pendingCount} 笔,超时 ${summary.overdueCount} 笔,超时金额 ¥${summary.overdueAmount.toFixed(2)}`,
...lines,
- '请尽快在 HQ「财务 → 门店提现审」处理。',
+ '请尽快在 HQ「财务 → 门店账单(手动提现)」处理。',
].join('\n'),
dedupeKey: `store_withdraw_overdue|${new Date().toISOString().slice(0, 10)}`,
dedupeTtlSec: 6 * 3600,
@@ -857,6 +857,152 @@ export class SettlementService {
return { billDate: billDate.toISOString().slice(0, 10), created, skipped, payoutLinked: payouts.length };
}
+ /**
+ * 总部「门店账单」统一列表:T+1 StoreBill + 手动提现 StoreWithdrawRequest
+ * 中小数据量内存合并后分页。
+ */
+ async listAdminStoreSettlements(query: {
+ page?: number;
+ pageSize?: number;
+ kind?: string;
+ status?: string;
+ storeId?: string;
+ dateFrom?: string;
+ dateTo?: string;
+ }) {
+ const page = query.page ?? 1;
+ const pageSize = query.pageSize ?? 20;
+ const kind = query.kind === 'T1_BILL' || query.kind === 'WITHDRAW' ? query.kind : undefined;
+ const status = query.status?.trim() || undefined;
+
+ const includeBills =
+ !kind || kind === 'T1_BILL'
+ ? !status || status === 'UNPAID' || status === 'PAID'
+ : false;
+ const includeWithdraws =
+ !kind || kind === 'WITHDRAW'
+ ? !status ||
+ status === 'PENDING_REVIEW' ||
+ status === 'REJECTED' ||
+ status === 'PAID'
+ : false;
+
+ const billWhere = includeBills
+ ? this.buildStoreBillWhere({
+ status: status === 'UNPAID' || status === 'PAID' ? status : undefined,
+ storeId: query.storeId,
+ dateFrom: query.dateFrom,
+ dateTo: query.dateTo,
+ })
+ : null;
+
+ const withdrawWhere: Prisma.StoreWithdrawRequestWhereInput | null = includeWithdraws
+ ? (() => {
+ const where: Prisma.StoreWithdrawRequestWhereInput = {};
+ if (status === 'PENDING_REVIEW' || status === 'REJECTED' || status === 'PAID') {
+ where.status = status;
+ }
+ if (query.storeId) where.storeId = BigInt(query.storeId);
+ if (query.dateFrom || query.dateTo) {
+ where.appliedAt = {};
+ if (query.dateFrom) where.appliedAt.gte = new Date(query.dateFrom);
+ if (query.dateTo) {
+ const end = new Date(query.dateTo);
+ end.setHours(23, 59, 59, 999);
+ where.appliedAt.lte = end;
+ }
+ }
+ return where;
+ })()
+ : null;
+
+ const bills = billWhere
+ ? await this.prisma.storeBill.findMany({
+ where: billWhere,
+ include: {
+ store: { select: { id: true, name: true, cityName: true, phone: true } },
+ },
+ })
+ : [];
+ const withdraws = withdrawWhere
+ ? await this.prisma.storeWithdrawRequest.findMany({
+ where: withdrawWhere,
+ include: {
+ store: { select: { id: true, name: true, cityName: true, phone: true } },
+ },
+ })
+ : [];
+
+ const now = new Date();
+ type UnifiedRow = {
+ kind: 'T1_BILL' | 'WITHDRAW';
+ id: bigint;
+ billNo: string;
+ storeId: bigint;
+ amount: number;
+ status: string;
+ date: Date;
+ overdue?: boolean;
+ redeemCount?: number;
+ redeemAmount?: number;
+ settlementRate?: number;
+ payoutCount?: number;
+ store?: { id: bigint; name: string; cityName: string; phone: string | null };
+ };
+
+ const rows: UnifiedRow[] = [
+ ...bills.map((b) => ({
+ kind: 'T1_BILL' as const,
+ id: b.id,
+ billNo: b.billNo,
+ storeId: b.storeId,
+ amount: Number(b.payoutAmount),
+ status: b.status,
+ date: b.billDate,
+ redeemCount: b.redeemCount,
+ redeemAmount: Number(b.redeemAmount),
+ settlementRate: Number(b.settlementRate),
+ store: b.store,
+ })),
+ ...withdraws.map((w) => ({
+ kind: 'WITHDRAW' as const,
+ id: w.id,
+ billNo: w.withdrawNo,
+ storeId: w.storeId,
+ amount: Number(w.amount),
+ status: w.status,
+ date: w.appliedAt,
+ overdue: w.status === 'PENDING_REVIEW' ? isWithdrawOverdue(w.appliedAt, now) : false,
+ payoutCount: w.payoutCount,
+ store: w.store,
+ })),
+ ];
+
+ rows.sort((a, b) => {
+ const dt = b.date.getTime() - a.date.getTime();
+ if (dt !== 0) return dt;
+ return Number(b.id - a.id);
+ });
+
+ const total = rows.length;
+ const slice = rows.slice((page - 1) * pageSize, page * pageSize);
+ const redeemAmount = bills.reduce((s, b) => s + Number(b.redeemAmount), 0);
+ const payoutAmount = rows.reduce((s, r) => s + r.amount, 0);
+
+ return serializeBigInt({
+ items: slice,
+ total,
+ page,
+ pageSize,
+ summary: {
+ count: total,
+ redeemAmount,
+ payoutAmount,
+ totalAmount: payoutAmount,
+ },
+ });
+ }
+
async listAdminStoreBills(query: {
page?: number;
pageSize?: number;