From b36f2ea63a900a2b1724a426a071c6a67d37d4ef Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Fri, 17 Jul 2026 07:56:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=8E=E5=B8=82=E5=90=88=E4=BC=99=E4=BA=BAH5?= =?UTF-8?q?=E7=AB=AF=E7=BB=93=E7=AE=97=E5=AE=A1=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin-web/src/lib/api.ts | 5 + apps/admin-web/src/pages/DashboardPage.tsx | 42 ++++ apps/admin-web/src/pages/PartnerBillsPage.tsx | 109 ++++++++-- apps/h5-partner/src/pages/BillsPage.tsx | 197 +++++++++++++----- apps/h5-partner/src/pages/CenterPage.tsx | 16 +- apps/h5-partner/src/pages/SettlementPage.tsx | 17 +- apps/h5-partner/src/styles.css | 5 + packages/shared-types/src/partner-log.ts | 2 +- packages/shared-types/src/settlement.ts | 9 +- server/dukang-api/prisma/schema.prisma | 2 + .../hq-operation/hq-operation.constants.ts | 2 + .../modules/ops/admin-dashboard.service.ts | 6 +- .../settlement/settlement.controller.ts | 19 ++ .../modules/settlement/settlement.service.ts | 69 +++++- 14 files changed, 418 insertions(+), 82 deletions(-) diff --git a/apps/admin-web/src/lib/api.ts b/apps/admin-web/src/lib/api.ts index 909870f..04fa6fa 100644 --- a/apps/admin-web/src/lib/api.ts +++ b/apps/admin-web/src/lib/api.ts @@ -79,6 +79,11 @@ export type DashboardStats = { partnersTotal: number; redeemToday: number; deliveriesTotal: number; + pendingPayouts?: number; + /** 合伙人已确认、待总部审核打款 */ + pendingBills?: number; + pendingPartnerDraftBills?: number; + openTickets?: number; ordersByStatus: Array<{ status: string; count: number }>; }; diff --git a/apps/admin-web/src/pages/DashboardPage.tsx b/apps/admin-web/src/pages/DashboardPage.tsx index 06b08ba..c9e6637 100644 --- a/apps/admin-web/src/pages/DashboardPage.tsx +++ b/apps/admin-web/src/pages/DashboardPage.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; import { Button, Card, Col, Descriptions, Modal, Row, Space, Statistic, Table, Typography, message, } from 'antd'; @@ -164,6 +165,47 @@ export default function DashboardPage() { + + + + 去处理} + > + 0 ? '#fa8c16' : undefined }} + /> + + 合伙人已确认申请,待总部通过或驳回 + {(stats?.pendingPartnerDraftBills ?? 0) > 0 + ? `(另有 ${stats?.pendingPartnerDraftBills} 笔待合伙人确认)` + : ''} + + + + + 去处理} + > + 0 ? '#fa8c16' : undefined }} + /> + + + + + + + + + diff --git a/apps/admin-web/src/pages/PartnerBillsPage.tsx b/apps/admin-web/src/pages/PartnerBillsPage.tsx index d9cd155..8680957 100644 --- a/apps/admin-web/src/pages/PartnerBillsPage.tsx +++ b/apps/admin-web/src/pages/PartnerBillsPage.tsx @@ -4,6 +4,7 @@ import { Card, DatePicker, Form, + Input, Modal, Select, Space, @@ -29,6 +30,7 @@ type Row = { status: string; periodStart: string; periodEnd: string; + rejectReason?: string | null; partner?: { companyName?: string; phone?: string }; partnerAccount?: { companyName?: string; phone?: string }; }; @@ -36,15 +38,17 @@ type Row = { type PartnerOption = { id: string; companyName: string; phone?: string }; const STATUS_LABELS: Record = { - DRAFT: '草稿', - CONFIRMED: '已确认', + DRAFT: '待合伙人确认', + CONFIRMED: '待打款审核', PAID: '已打款', + REJECTED: '已驳回', }; const STATUS_COLORS: Record = { DRAFT: 'default', - CONFIRMED: 'blue', + CONFIRMED: 'orange', PAID: 'green', + REJECTED: 'red', }; export default function PartnerBillsPage() { @@ -66,6 +70,10 @@ export default function PartnerBillsPage() { const [genOpen, setGenOpen] = useState(false); const [genForm] = Form.useForm(); const [exporting, setExporting] = useState(false); + const [rejectOpen, setRejectOpen] = useState(false); + const [rejectTarget, setRejectTarget] = useState(null); + const [rejectForm] = Form.useForm(); + const [rejecting, setRejecting] = useState(false); useEffect(() => { void request>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) @@ -95,7 +103,7 @@ export default function PartnerBillsPage() { async function confirmBill(id: string) { await request(`/admin/partner-bills/${id}/confirm`, { method: 'POST' }); - message.success('已确认'); + message.success('已代确认,进入待打款审核'); reload(); } @@ -104,10 +112,33 @@ export default function PartnerBillsPage() { method: 'POST', body: JSON.stringify({ paymentRef: `PAY-${Date.now()}` }), }); - message.success('已标记打款'); + message.success('已通过并标记打款'); reload(); } + function openReject(row: Row) { + setRejectTarget(row); + rejectForm.resetFields(); + setRejectOpen(true); + } + + async function submitReject(values: { reason: string }) { + if (!rejectTarget) return; + setRejecting(true); + try { + await request(`/admin/partner-bills/${rejectTarget.id}/reject`, { + method: 'POST', + body: JSON.stringify({ reason: values.reason.trim() }), + }); + message.success('已驳回打款申请'); + setRejectOpen(false); + setRejectTarget(null); + reload(); + } finally { + setRejecting(false); + } + } + async function exportExcel() { setExporting(true); try { @@ -163,24 +194,38 @@ export default function PartnerBillsPage() { { title: '状态', dataIndex: 'status', - width: 90, - render: (s) => {STATUS_LABELS[s] || s}, + width: 120, + render: (s, row) => ( + + {STATUS_LABELS[s] || s} + {s === 'REJECTED' && row.rejectReason ? ( + + {row.rejectReason} + + ) : null} + + ), }, { title: '操作', - width: 160, + width: 220, fixed: 'right', render: (_, row) => ( - + {row.status === 'DRAFT' && ( )} {row.status === 'CONFIRMED' && ( - + <> + + + )} ), @@ -194,7 +239,7 @@ export default function PartnerBillsPage() { 合伙人账单 - T+30 结算:按自然月汇总酒单佣金(酒单价 × 酒单佣金比例)与门店核销佣金(核销额 × 核销佣金比例) + T+30 结算:合伙人确认并申请打款后,总部在此审核通过或驳回(驳回须填写理由) @@ -242,7 +287,7 @@ export default function PartnerBillsPage() { setConfirmed(e.target.checked)} /> - 我已核对数据无误,同意根据此账单进行结算 - - - +
+ + +
+ )} ); diff --git a/apps/h5-partner/src/pages/CenterPage.tsx b/apps/h5-partner/src/pages/CenterPage.tsx index 58535ad..3e25753 100644 --- a/apps/h5-partner/src/pages/CenterPage.tsx +++ b/apps/h5-partner/src/pages/CenterPage.tsx @@ -53,12 +53,16 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag }, [isPrimary]); const finance = useMemo(() => { - const pending = bills.filter((b) => b.status === 'DRAFT'); + const pending = bills.filter((b) => b.status === 'DRAFT' || b.status === 'REJECTED'); const settled = bills.filter((b) => b.status === 'PAID'); const paidCount = settled.length; + const rejectedCount = bills.filter((b) => b.status === 'REJECTED').length; return { pendingBillCount: pending.length, - pendingTotal: pending.reduce((s, b) => s + Number(b.totalAmount || 0), 0), + rejectedCount, + pendingTotal: bills + .filter((b) => b.status === 'DRAFT' || b.status === 'CONFIRMED' || b.status === 'REJECTED') + .reduce((s, b) => s + Number(b.totalAmount || 0), 0), settledTotal: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0), balance: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0), paidCount, @@ -180,9 +184,13 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag receipt_long
-

待确认账单

+

+ {finance.rejectedCount > 0 ? '待处理账单' : '待确认账单'} +

- 您有 {finance.pendingBillCount} 笔账单待确认 + {finance.rejectedCount > 0 + ? `您有 ${finance.pendingBillCount} 笔账单待处理(含 ${finance.rejectedCount} 笔已驳回)` + : `您有 ${finance.pendingBillCount} 笔账单待确认`}

diff --git a/apps/h5-partner/src/pages/SettlementPage.tsx b/apps/h5-partner/src/pages/SettlementPage.tsx index 71b645c..f843c66 100644 --- a/apps/h5-partner/src/pages/SettlementPage.tsx +++ b/apps/h5-partner/src/pages/SettlementPage.tsx @@ -4,7 +4,7 @@ import PageHeader from '@dukang/shared-ui/PageHeader'; import type { PartnerBillDto } from '@dukang/shared-types'; import { isLoggedIn, request } from '../lib/api'; -type StatusFilter = 'all' | 'pending' | 'settled' | 'reviewing'; +type StatusFilter = 'all' | 'pending' | 'settled' | 'reviewing' | 'rejected'; function fmtMoney(n: number) { return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); @@ -15,6 +15,7 @@ function billStatusLabel(status: string) { case 'DRAFT': return '待结算'; case 'CONFIRMED': return '审核中'; case 'PAID': return '已结算'; + case 'REJECTED': return '已驳回'; default: return status; } } @@ -24,6 +25,7 @@ function billStatusClass(status: string) { case 'DRAFT': return 'partner-settlement-status--pending'; case 'CONFIRMED': return 'partner-settlement-status--reviewing'; case 'PAID': return 'partner-settlement-status--settled'; + case 'REJECTED': return 'partner-settlement-status--rejected'; default: return ''; } } @@ -70,9 +72,10 @@ export default function SettlementPage() { const matchMonth = d.getFullYear() === month.year && d.getMonth() + 1 === month.month; if (!matchMonth) return false; if (statusFilter === 'all') return true; - if (statusFilter === 'pending') return b.status === 'DRAFT'; + if (statusFilter === 'pending') return b.status === 'DRAFT' || b.status === 'REJECTED'; if (statusFilter === 'settled') return b.status === 'PAID'; if (statusFilter === 'reviewing') return b.status === 'CONFIRMED'; + if (statusFilter === 'rejected') return b.status === 'REJECTED'; return true; }); }, [bills, month, statusFilter]); @@ -93,8 +96,9 @@ export default function SettlementPage() { const statusTabs: Array<{ key: StatusFilter; label: string }> = [ { key: 'all', label: '全部' }, { key: 'pending', label: '待结算' }, - { key: 'settled', label: '已结算' }, { key: 'reviewing', label: '审核中' }, + { key: 'rejected', label: '已驳回' }, + { key: 'settled', label: '已结算' }, ]; return ( @@ -188,7 +192,12 @@ export default function SettlementPage() { 订单分佣 ¥{fmtMoney(Number(bill.orderCommission || 0))}

-

+ {bill.status === 'REJECTED' && bill.rejectReason ? ( +

+ 驳回:{bill.rejectReason} +

+ ) : null} +

¥{fmtMoney(Number(bill.totalAmount || 0))}

diff --git a/apps/h5-partner/src/styles.css b/apps/h5-partner/src/styles.css index b366bc0..de0138a 100644 --- a/apps/h5-partner/src/styles.css +++ b/apps/h5-partner/src/styles.css @@ -2985,6 +2985,11 @@ nav.app-tabbar .app-tabbar-label { color: var(--color-success-green); } +.partner-settlement-status--rejected { + background: rgba(166, 29, 36, 0.12); + color: var(--color-heritage-red); +} + .partner-settlement-item-meta { display: flex; align-items: center; diff --git a/packages/shared-types/src/partner-log.ts b/packages/shared-types/src/partner-log.ts index 900d660..b17fee7 100644 --- a/packages/shared-types/src/partner-log.ts +++ b/packages/shared-types/src/partner-log.ts @@ -25,7 +25,7 @@ export const PARTNER_LOG_EVENT_CATEGORIES: Record = { [HqOperationAction.PARTNER_BILL_GENERATE]: '生成合伙人账单', [HqOperationAction.PARTNER_BILL_CONFIRM]: '确认合伙人账单', [HqOperationAction.PARTNER_BILL_MARK_PAID]: '合伙人账单结算', + [HqOperationAction.PARTNER_BILL_REJECT]: '驳回合伙人打款申请', [HqOperationAction.REDEEM_DEBUG_CREATE_TOKEN]: '核销调试-生成码', [HqOperationAction.REDEEM_DEBUG_CONFIRM]: '核销调试-确认核销', [HqOperationAction.PROMO_CODE_CREATE]: '创建推广码', diff --git a/server/dukang-api/src/modules/ops/admin-dashboard.service.ts b/server/dukang-api/src/modules/ops/admin-dashboard.service.ts index 95babe4..a1eb27a 100644 --- a/server/dukang-api/src/modules/ops/admin-dashboard.service.ts +++ b/server/dukang-api/src/modules/ops/admin-dashboard.service.ts @@ -22,6 +22,7 @@ export class AdminDashboardService { deliveriesTotal, pendingPayouts, pendingBills, + pendingPartnerDraftBills, openTickets, ] = await Promise.all([ this.prisma.user.count({ where: { status: 1, mergedIntoUserId: null } }), @@ -42,7 +43,8 @@ export class AdminDashboardService { this.prisma.redeemRecord.count({ where: { createdAt: { gte: todayStart } } }), this.prisma.orderDelivery.count(), this.prisma.storePayout.count({ where: { status: 'PENDING' } }), - this.prisma.partnerBill.count({ where: { status: { in: ['DRAFT', 'CONFIRMED'] } } }), + this.prisma.partnerBill.count({ where: { status: 'CONFIRMED' } }), + this.prisma.partnerBill.count({ where: { status: 'DRAFT' } }), this.prisma.commonTicket.count({ where: { status: { in: ['PENDING', 'OPEN'] } } }), ]); @@ -57,7 +59,9 @@ export class AdminDashboardService { redeemToday, deliveriesTotal, pendingPayouts, + /** 合伙人已确认、待总部审核打款 */ pendingBills, + pendingPartnerDraftBills, openTickets, ordersByStatus: ordersByStatus.map((row) => ({ status: row.status, diff --git a/server/dukang-api/src/modules/settlement/settlement.controller.ts b/server/dukang-api/src/modules/settlement/settlement.controller.ts index 1256233..4117757 100644 --- a/server/dukang-api/src/modules/settlement/settlement.controller.ts +++ b/server/dukang-api/src/modules/settlement/settlement.controller.ts @@ -25,6 +25,11 @@ export class SettlementController { bills(@CurrentUser() user: AuthUser) { return this.settlementService.listPartnerBills(user.actorId); } + + @Post('bills/:id/confirm') + confirm(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.settlementService.partnerConfirmBill(user.actorId, BigInt(id)); + } } @Controller('shop/payouts') @@ -193,6 +198,20 @@ export class AdminPartnerBillController { markPaid(@Param('id') id: string, @Body() body: { paymentRef?: string }) { return this.settlementService.markPartnerBillPaid(BigInt(id), body); } + + @Post(':id/reject') + @HqOperation({ + action: HqOperationAction.PARTNER_BILL_REJECT, + refType: 'PARTNER_BILL', + refIdParam: 'id', + includeBody: true, + }) + reject(@Param('id') id: string, @Body() body: { reason?: string }) { + if (!body?.reason?.trim()) { + throw new BadRequestException('请填写驳回理由'); + } + return this.settlementService.rejectPartnerBill(BigInt(id), body.reason); + } } @Controller('admin/winery-bills') diff --git a/server/dukang-api/src/modules/settlement/settlement.service.ts b/server/dukang-api/src/modules/settlement/settlement.service.ts index ea8dae4..b32e329 100644 --- a/server/dukang-api/src/modules/settlement/settlement.service.ts +++ b/server/dukang-api/src/modules/settlement/settlement.service.ts @@ -529,11 +529,70 @@ export class SettlementService { async confirmPartnerBill(id: bigint) { const bill = await this.prisma.partnerBill.findUnique({ where: { id } }); if (!bill) throw new NotFoundException('账单不存在'); - if (bill.status !== 'DRAFT') throw new BadRequestException('仅草稿可确认'); + if (bill.status !== 'DRAFT' && bill.status !== 'REJECTED') { + throw new BadRequestException('仅待确认或已驳回账单可确认'); + } const updated = await this.prisma.partnerBill.update({ where: { id }, - data: { status: 'CONFIRMED', confirmedAt: new Date() }, + data: { + status: 'CONFIRMED', + confirmedAt: new Date(), + rejectReason: null, + }, + }); + + return serializeBigInt(updated); + } + + /** 合伙人端:确认账单并申请打款(DRAFT/REJECTED → CONFIRMED) */ + async partnerConfirmBill(partnerAccountId: bigint, billId: bigint) { + const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId); + const bill = await this.prisma.partnerBill.findUnique({ where: { id: billId } }); + if (!bill || bill.partnerAccountId !== primary.id) { + throw new NotFoundException('账单不存在'); + } + if (bill.status !== 'DRAFT' && bill.status !== 'REJECTED') { + throw new BadRequestException('当前账单不可再次申请打款'); + } + + const updated = await this.prisma.partnerBill.update({ + where: { id: billId }, + data: { + status: 'CONFIRMED', + confirmedAt: new Date(), + rejectReason: null, + }, + }); + + this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', { + partnerAccountId: primary.id, + eventName: 'partner_bill_confirm', + refType: 'PARTNER_BILL', + refId: billId, + extraJson: { billNo: bill.billNo }, + }); + + return serializeBigInt(updated); + } + + async rejectPartnerBill(id: bigint, reason: string) { + const rejectReason = reason.trim(); + if (!rejectReason) throw new BadRequestException('请填写驳回理由'); + if (rejectReason.length > 500) throw new BadRequestException('驳回理由不能超过 500 字'); + + const bill = await this.prisma.partnerBill.findUnique({ where: { id } }); + if (!bill) throw new NotFoundException('账单不存在'); + if (bill.status !== 'CONFIRMED') { + throw new BadRequestException('仅待打款审核的账单可驳回'); + } + + const updated = await this.prisma.partnerBill.update({ + where: { id }, + data: { + status: 'REJECTED', + rejectReason, + }, }); return serializeBigInt(updated); @@ -542,11 +601,11 @@ export class SettlementService { async markPartnerBillPaid(id: bigint, dto: { paymentRef?: string }) { const bill = await this.prisma.partnerBill.findUnique({ where: { id } }); if (!bill) throw new NotFoundException('账单不存在'); - if (bill.status !== 'CONFIRMED') throw new BadRequestException('仅已确认账单可标记打款'); + if (bill.status !== 'CONFIRMED') throw new BadRequestException('仅待打款审核的账单可标记打款'); const updated = await this.prisma.partnerBill.update({ where: { id }, - data: { status: 'PAID', paidAt: new Date() }, + data: { status: 'PAID', paidAt: new Date(), rejectReason: null }, }); return serializeBigInt(updated); @@ -577,6 +636,7 @@ export class SettlementService { '状态', '确认时间', '打款时间', + '驳回理由', ].join(','); const rows = bills.map((b) => [ @@ -591,6 +651,7 @@ export class SettlementService { b.status, b.confirmedAt ? b.confirmedAt.toISOString().slice(0, 10) : '', b.paidAt ? b.paidAt.toISOString().slice(0, 10) : '', + csvEscape(b.rejectReason ?? ''), ].join(','), ); return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: bills.length };