feat(settlement): redesign factory/partner/store statement bills
CI / verify (pull_request) Has been cancelled

Add WineryBill/StoreBill tables, partner review-send-confirm flow, daily/monthly cron, and admin multi-select confirm with status filters.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-22 17:25:49 +08:00
parent b534a569f8
commit b392c28787
22 changed files with 1549 additions and 543 deletions
@@ -47,10 +47,17 @@ export const HqOperationAction = {
TICKET_REJECT: 'TICKET_REJECT',
STORE_PAYOUT_CONFIRM: 'STORE_PAYOUT_CONFIRM',
STORE_PAYOUT_BATCH_CONFIRM: 'STORE_PAYOUT_BATCH_CONFIRM',
STORE_BILL_CONFIRM: 'STORE_BILL_CONFIRM',
STORE_BILL_BATCH_CONFIRM: 'STORE_BILL_BATCH_CONFIRM',
PARTNER_BILL_GENERATE: 'PARTNER_BILL_GENERATE',
PARTNER_BILL_SEND: 'PARTNER_BILL_SEND',
PARTNER_BILL_BATCH_SEND: 'PARTNER_BILL_BATCH_SEND',
PARTNER_BILL_CONFIRM: 'PARTNER_BILL_CONFIRM',
PARTNER_BILL_MARK_PAID: 'PARTNER_BILL_MARK_PAID',
PARTNER_BILL_BATCH_MARK_PAID: 'PARTNER_BILL_BATCH_MARK_PAID',
PARTNER_BILL_REJECT: 'PARTNER_BILL_REJECT',
WINERY_BILL_CONFIRM: 'WINERY_BILL_CONFIRM',
WINERY_BILL_BATCH_CONFIRM: 'WINERY_BILL_BATCH_CONFIRM',
REDEEM_DEBUG_CREATE_TOKEN: 'REDEEM_DEBUG_CREATE_TOKEN',
REDEEM_DEBUG_CONFIRM: 'REDEEM_DEBUG_CONFIRM',
PROMO_CODE_CREATE: 'PROMO_CODE_CREATE',
@@ -114,10 +121,17 @@ export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
[HqOperationAction.TICKET_REJECT]: '工单驳回',
[HqOperationAction.STORE_PAYOUT_CONFIRM]: '门店打款确认',
[HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM]: '批量门店打款',
[HqOperationAction.STORE_BILL_CONFIRM]: '门店对账单确认打款',
[HqOperationAction.STORE_BILL_BATCH_CONFIRM]: '批量门店对账单打款',
[HqOperationAction.PARTNER_BILL_GENERATE]: '生成合伙人账单',
[HqOperationAction.PARTNER_BILL_SEND]: '发送合伙人账单',
[HqOperationAction.PARTNER_BILL_BATCH_SEND]: '批量发送合伙人账单',
[HqOperationAction.PARTNER_BILL_CONFIRM]: '确认合伙人账单',
[HqOperationAction.PARTNER_BILL_MARK_PAID]: '合伙人账单结算',
[HqOperationAction.PARTNER_BILL_BATCH_MARK_PAID]: '批量合伙人账单结算',
[HqOperationAction.PARTNER_BILL_REJECT]: '驳回合伙人打款申请',
[HqOperationAction.WINERY_BILL_CONFIRM]: '酒厂对账单确认打款',
[HqOperationAction.WINERY_BILL_BATCH_CONFIRM]: '批量酒厂对账单打款',
[HqOperationAction.REDEEM_DEBUG_CREATE_TOKEN]: '核销调试-生成码',
[HqOperationAction.REDEEM_DEBUG_CONFIRM]: '核销调试-确认核销',
[HqOperationAction.PROMO_CODE_CREATE]: '创建推广码',
+10 -2
View File
@@ -1,11 +1,19 @@
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { BullModule } from '@nestjs/bullmq';
import { TradeModule } from '../modules/trade/trade.module';
import { SettlementModule } from '../modules/settlement/settlement.module';
import { DeliveryProcessor } from './delivery.processor';
import { SettlementScheduler } from './settlement.scheduler';
import { DELIVERY_QUEUE } from './jobs.constants';
@Module({
imports: [BullModule.registerQueue({ name: DELIVERY_QUEUE }), TradeModule],
providers: [DeliveryProcessor],
imports: [
ScheduleModule.forRoot(),
BullModule.registerQueue({ name: DELIVERY_QUEUE }),
TradeModule,
SettlementModule,
],
providers: [DeliveryProcessor, SettlementScheduler],
})
export class JobsModule {}
@@ -0,0 +1,45 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { SettlementService } from '../modules/settlement/settlement.service';
/**
* 财务对账单定时任务(Asia/Shanghai
* - 每日 08:00:酒厂日账单 + 门店日账单(统计昨日 00:00~今日 00:00
* - 每月 1 日 08:00:合伙人上一自然月账单
*/
@Injectable()
export class SettlementScheduler {
private readonly logger = new Logger(SettlementScheduler.name);
constructor(private readonly settlementService: SettlementService) {}
@Cron('0 8 * * *', { timeZone: 'Asia/Shanghai' })
async handleDailyBills() {
this.logger.log('Daily settlement bills job start');
try {
const winery = await this.settlementService.generateWineryBillForDay();
this.logger.log(`Winery bill: ${JSON.stringify(winery)}`);
} catch (e) {
this.logger.error('Winery bill job failed', e instanceof Error ? e.stack : e);
}
try {
const store = await this.settlementService.generateStoreBillsForDay();
this.logger.log(`Store bills: ${JSON.stringify(store)}`);
} catch (e) {
this.logger.error('Store bill job failed', e instanceof Error ? e.stack : e);
}
}
@Cron('0 8 1 * *', { timeZone: 'Asia/Shanghai' })
async handleMonthlyPartnerBills() {
this.logger.log('Monthly partner bills job start');
try {
const result = await this.settlementService.generatePreviousMonthPartnerBills();
this.logger.log(
`Partner bills: total=${result.total} success=${result.success} failed=${result.failed}`,
);
} catch (e) {
this.logger.error('Partner bill job failed', e instanceof Error ? e.stack : e);
}
}
}
@@ -43,8 +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: 'CONFIRMED' } }),
this.prisma.partnerBill.count({ where: { status: 'DRAFT' } }),
this.prisma.partnerBill.count({ where: { status: 'UNPAID' } }),
this.prisma.partnerBill.count({ where: { status: 'PENDING_REVIEW' } }),
this.prisma.commonTicket.count({ where: { status: { in: ['PENDING', 'OPEN'] } } }),
]);
@@ -28,6 +28,11 @@ export class SettlementController {
return this.settlementService.listPartnerBills(user.actorId);
}
@Post('bills/batch-confirm')
batchConfirm(@CurrentUser() user: AuthUser, @Body() body: { ids: string[] }) {
return this.settlementService.batchPartnerConfirmBills(user.actorId, body.ids ?? []);
}
@Post('bills/:id/confirm')
confirm(@CurrentUser() user: AuthUser, @Param('id') id: string) {
return this.settlementService.partnerConfirmBill(user.actorId, BigInt(id));
@@ -126,6 +131,65 @@ export class AdminStorePayoutController {
}
}
@Controller('admin/store-bills')
@UseGuards(HqAuthGuard)
export class AdminStoreBillController {
constructor(private readonly settlementService: SettlementService) {}
@Get()
list(@Query() query: Record<string, string>) {
return this.settlementService.listAdminStoreBills({
page: query.page ? Number(query.page) : 1,
pageSize: query.pageSize ? Number(query.pageSize) : 20,
status: query.status,
storeId: query.storeId,
dateFrom: query.dateFrom,
dateTo: query.dateTo,
});
}
@Get('export')
export(@Query() query: Record<string, string>) {
return this.settlementService.exportAdminStoreBills({
status: query.status,
storeId: query.storeId,
dateFrom: query.dateFrom,
dateTo: query.dateTo,
});
}
@Post('generate')
generate() {
return this.settlementService.generateStoreBillsForDay();
}
@Post('batch-confirm')
@HqOperation({
action: HqOperationAction.STORE_BILL_BATCH_CONFIRM,
refType: 'STORE_BILL',
batch: true,
includeBody: true,
})
batchConfirm(@Body() body: { ids: string[] }) {
return this.settlementService.batchConfirmStoreBills(body.ids ?? []);
}
@Get(':id')
detail(@Param('id') id: string) {
return this.settlementService.getAdminStoreBill(BigInt(id));
}
@Post(':id/confirm')
@HqOperation({
action: HqOperationAction.STORE_BILL_CONFIRM,
refType: 'STORE_BILL',
refIdParam: 'id',
})
confirm(@Param('id') id: string) {
return this.settlementService.confirmStoreBill(BigInt(id));
}
}
@Controller('admin/partner-bills')
@UseGuards(HqAuthGuard)
export class AdminPartnerBillController {
@@ -175,11 +239,43 @@ export class AdminPartnerBillController {
});
}
@Post('batch-send')
@HqOperation({
action: HqOperationAction.PARTNER_BILL_BATCH_SEND,
refType: 'PARTNER_BILL',
batch: true,
includeBody: true,
})
batchSend(@Body() body: { ids: string[] }) {
return this.settlementService.batchSendPartnerBills(body.ids ?? []);
}
@Post('batch-mark-paid')
@HqOperation({
action: HqOperationAction.PARTNER_BILL_BATCH_MARK_PAID,
refType: 'PARTNER_BILL',
batch: true,
includeBody: true,
})
batchMarkPaid(@Body() body: { ids: string[] }) {
return this.settlementService.batchMarkPartnerBillsPaid(body.ids ?? []);
}
@Get(':id')
detail(@Param('id') id: string) {
return this.settlementService.getAdminPartnerBill(BigInt(id));
}
@Post(':id/send')
@HqOperation({
action: HqOperationAction.PARTNER_BILL_SEND,
refType: 'PARTNER_BILL',
refIdParam: 'id',
})
send(@Param('id') id: string) {
return this.settlementService.sendPartnerBill(BigInt(id));
}
@Post(':id/confirm')
@HqOperation({
action: HqOperationAction.PARTNER_BILL_CONFIRM,
@@ -226,20 +322,55 @@ export class AdminWineryBillController {
return this.settlementService.listAdminWineryBills({
page: query.page ? Number(query.page) : 1,
pageSize: query.pageSize ? Number(query.pageSize) : 20,
status: query.status,
dateFrom: query.dateFrom,
dateTo: query.dateTo,
year: query.year ? Number(query.year) : undefined,
month: query.month ? Number(query.month) : undefined,
deliveryType: query.deliveryType,
});
}
@Get('export')
export(@Query() query: Record<string, string>) {
return this.settlementService.exportAdminWineryBills({
status: query.status,
dateFrom: query.dateFrom,
dateTo: query.dateTo,
year: query.year ? Number(query.year) : undefined,
month: query.month ? Number(query.month) : undefined,
deliveryType: query.deliveryType,
});
}
@Post('generate')
generate() {
return this.settlementService.generateWineryBillForDay();
}
@Post('batch-confirm')
@HqOperation({
action: HqOperationAction.WINERY_BILL_BATCH_CONFIRM,
refType: 'WINERY_BILL',
batch: true,
includeBody: true,
})
batchConfirm(@Body() body: { ids: string[] }) {
return this.settlementService.batchConfirmWineryBills(body.ids ?? []);
}
@Get(':id')
detail(@Param('id') id: string) {
return this.settlementService.getAdminWineryBill(BigInt(id));
}
@Post(':id/confirm')
@HqOperation({
action: HqOperationAction.WINERY_BILL_CONFIRM,
refType: 'WINERY_BILL',
refIdParam: 'id',
})
confirm(@Param('id') id: string) {
return this.settlementService.confirmWineryBill(BigInt(id));
}
}
@Controller('partner/me')
@@ -280,7 +411,6 @@ export class PartnerMeController {
});
}
// 门店类子账号若未配置权限,补齐开店/门店管理,便于开闭店与重传资料
if (account.isPrimary !== 1) {
const perms = Array.isArray(account.permissions) ? (account.permissions as string[]) : [];
const hasStorePerm = perms.includes('store:create') || perms.includes('store:manage');
@@ -5,6 +5,7 @@ import { CityScopeModule } from '../city-scope/city-scope.module';
import { SettlementService } from './settlement.service';
import {
AdminPartnerBillController,
AdminStoreBillController,
AdminStorePayoutController,
AdminWineryBillController,
PartnerMeController,
@@ -19,6 +20,7 @@ import {
PartnerMeController,
ShopPayoutController,
AdminStorePayoutController,
AdminStoreBillController,
AdminPartnerBillController,
AdminWineryBillController,
],
File diff suppressed because it is too large Load Diff