城市合伙人H5端结算审核

This commit is contained in:
2026-07-17 07:56:43 +08:00
parent 7fa2c6fbed
commit b36f2ea63a
14 changed files with 418 additions and 82 deletions
+2
View File
@@ -183,6 +183,7 @@ enum PartnerBillStatus {
DRAFT
CONFIRMED
PAID
REJECTED
}
enum StoreStatus {
@@ -623,6 +624,7 @@ model PartnerBill {
status PartnerBillStatus @default(DRAFT)
confirmedAt DateTime? @map("confirmed_at") @db.DateTime(3)
paidAt DateTime? @map("paid_at") @db.DateTime(3)
rejectReason String? @map("reject_reason") @db.VarChar(500)
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
partnerAccount PartnerAccount @relation(fields: [partnerAccountId], references: [id], onDelete: Restrict)
@@ -46,6 +46,7 @@ export const HqOperationAction = {
PARTNER_BILL_GENERATE: 'PARTNER_BILL_GENERATE',
PARTNER_BILL_CONFIRM: 'PARTNER_BILL_CONFIRM',
PARTNER_BILL_MARK_PAID: 'PARTNER_BILL_MARK_PAID',
PARTNER_BILL_REJECT: 'PARTNER_BILL_REJECT',
REDEEM_DEBUG_CREATE_TOKEN: 'REDEEM_DEBUG_CREATE_TOKEN',
REDEEM_DEBUG_CONFIRM: 'REDEEM_DEBUG_CONFIRM',
PROMO_CODE_CREATE: 'PROMO_CODE_CREATE',
@@ -108,6 +109,7 @@ export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
[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]: '创建推广码',
@@ -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,
@@ -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')
@@ -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 };