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() {
@@ -280,7 +325,7 @@ export default function PartnerBillsPage() {
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
- scroll={{ x: 1200 }}
+ scroll={{ x: 1280 }}
pagination={{
current: page,
pageSize,
@@ -320,6 +365,38 @@ export default function PartnerBillsPage() {
+
+ {
+ setRejectOpen(false);
+ setRejectTarget(null);
+ }}
+ footer={null}
+ destroyOnClose
+ >
+
+ 账单 {rejectTarget?.billNo} · {rejectTarget ? partnerName(rejectTarget) : ''}
+
+ 驳回理由将展示在合伙人 H5 账单页。
+
+
+
+
+
+
+
);
}
diff --git a/apps/h5-partner/src/pages/BillsPage.tsx b/apps/h5-partner/src/pages/BillsPage.tsx
index 2132f08..9f6ad20 100644
--- a/apps/h5-partner/src/pages/BillsPage.tsx
+++ b/apps/h5-partner/src/pages/BillsPage.tsx
@@ -1,42 +1,96 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
+import type { PartnerBillDto } from '@dukang/shared-types';
import { isLoggedIn, request } from '../lib/api';
+import { toastError, toastSuccess } from '../lib/toast';
+
+function fmtMoney(n: number) {
+ return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+}
+
+function billStatusLabel(status: string) {
+ switch (status) {
+ case 'DRAFT': return '待确认';
+ case 'CONFIRMED': return '审核中';
+ case 'PAID': return '已结算';
+ case 'REJECTED': return '已驳回';
+ default: return status;
+ }
+}
+
+function canApplyPayment(status: string) {
+ return status === 'DRAFT' || status === 'REJECTED';
+}
export default function BillsPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
- const [bills, setBills] = useState>>([]);
+ const [bills, setBills] = useState([]);
const [confirmed, setConfirmed] = useState(false);
const [submitting, setSubmitting] = useState(false);
+ const [loading, setLoading] = useState(true);
+
+ async function loadBills() {
+ setLoading(true);
+ try {
+ const list = await request('PARTNER_H5', '/partner/settlement/bills');
+ setBills(Array.isArray(list) ? list : []);
+ } catch {
+ setBills([]);
+ } finally {
+ setLoading(false);
+ }
+ }
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
- request>>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
+ void loadBills();
}, [navigate]);
const billId = searchParams.get('id');
+ const actionable = useMemo(
+ () => bills.filter((b) => canApplyPayment(b.status)),
+ [bills],
+ );
const bill = billId
- ? bills.find((b) => String(b.id) === billId) ?? bills[0]
- : bills[0];
+ ? bills.find((b) => String(b.id) === billId) ?? actionable[0] ?? bills[0]
+ : actionable[0] ?? bills[0];
- function confirmBill() {
- if (!confirmed) return;
+ const status = String(bill?.status || '');
+ const showApplyForm = !!bill && canApplyPayment(status);
+ const isReviewing = status === 'CONFIRMED';
+ const isRejected = status === 'REJECTED';
+ const isPaid = status === 'PAID';
+
+ async function confirmBill() {
+ if (!bill || !confirmed || !canApplyPayment(String(bill.status))) return;
setSubmitting(true);
- setTimeout(() => {
+ try {
+ await request('PARTNER_H5', `/partner/settlement/bills/${bill.id}/confirm`, {
+ method: 'POST',
+ });
+ toastSuccess('申请已提交,请耐心等待总部打款审核');
+ setConfirmed(false);
+ await loadBills();
+ } catch (e) {
+ toastError(e instanceof Error ? e.message : '提交失败');
+ } finally {
setSubmitting(false);
- alert('申请已提交,请耐心等待打款审核。(preV1 演示)');
- }, 1500);
+ }
}
return (
-
navigate('/center/settlement')} />
+ navigate('/center/settlement')} />
@@ -45,17 +99,28 @@ export default function BillsPage() {
数据核算
-
2
+
+ {showApplyForm && !confirmed ? '2' : (
+ check
+ )}
+
账单确认
-
3
-
申请打款
+
+ {isPaid ? (
+ check
+ ) : '3'}
+
+
+ {isRejected ? '已驳回' : isPaid ? '已打款' : '申请打款'}
+
- {!bill &&
暂无账单
}
+ {loading &&
加载中…
}
+ {!loading && !bill &&
暂无账单
}
{bill && (
@@ -66,25 +131,46 @@ export default function BillsPage() {
SETTLEMENT PERIOD
{String(bill.billNo || '月度结算账单')}
- {String(bill.status || '待确认')}
+
+ {billStatusLabel(status)}
+
+ {isRejected && bill.rejectReason && (
+
+
error
+
+
打款申请已驳回
+
{bill.rejectReason}
+
+
+ )}
+
+ {isReviewing && (
+
+
hourglass_top
+
+ 您已提交打款申请,总部审核中,请耐心等待。
+
+
+ )}
+
应结总金额
¥
- {Number(bill.totalAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
+ {fmtMoney(Number(bill.totalAmount || 0))}
订单分佣收入
- ¥ {(Number(bill.totalAmount || 0) * 0.82).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
+ ¥ {fmtMoney(Number(bill.orderCommission || 0))}
核销权益分佣
- ¥ {(Number(bill.totalAmount || 0) * 0.18).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
+ ¥ {fmtMoney(Number(bill.redeemCommission || 0))}
@@ -93,45 +179,58 @@ export default function BillsPage() {
{bills.length > 1 && (
-
历史账单
- {bills.slice(1).map((b) => (
-
+
全部账单
+ {bills.map((b) => (
+
+
))}
)}
-
-
info
-
- 账单确认后将正式进入打款流程。如有异议,请在确认前联系城市运营经理核实数据。
-
-
+ {showApplyForm && (
+ <>
+
+
info
+
+ {isRejected
+ ? '请根据驳回理由核对后重新确认并申请打款。'
+ : '账单确认后将正式进入打款审核。如有异议,请在确认前联系城市运营经理核实数据。'}
+
+
- {bill && (
-
+
+ >
)}
);
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 };