feat(settlement): 统一门店账单列表合并 T+1 与手动提现
总部财务以门店账单为唯一入口;新增 store-settlements API,去掉独立提现审菜单并更新企微引导文案。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user