import { BadRequestException, Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { IsOptional, IsString } from 'class-validator'; import { DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS } from '@dukang/shared-types'; import { SettlementService } from './settlement.service'; import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard'; import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard'; import { ShopStoreGuard } from '../../common/guards/shop-store.guard'; import { ShopPrimaryGuard } from '../../common/guards/shop-primary.guard'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { PrismaService } from '../../common/prisma/prisma.module'; import { PartnerCityService } from '../city-scope/partner-city.service'; class UpdatePartnerMeDto { @IsOptional() @IsString() name?: string; } @Controller('partner/settlement') @UseGuards(JwtAuthGuard, PartnerPrimaryGuard) export class SettlementController { constructor(private readonly settlementService: SettlementService) {} @Get('bills') bills(@CurrentUser() user: AuthUser) { return this.settlementService.listPartnerBills(user.actorId); } @Get('bills/:id') billDetail(@CurrentUser() user: AuthUser, @Param('id') id: string) { return this.settlementService.getPartnerBill(user.actorId, BigInt(id)); } @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)); } } @Controller('shop/payouts') @UseGuards(JwtAuthGuard, ShopStoreGuard) export class ShopPayoutController { constructor(private readonly settlementService: SettlementService) {} @Get() list( @CurrentUser() user: AuthUser, @Query('page') page = '1', @Query('pageSize') pageSize = '20', ) { return this.settlementService.listShopPayouts( user.actorId, user.storeId!, Number(page), Number(pageSize), ); } } @Controller('shop/withdraw') @UseGuards(JwtAuthGuard, ShopStoreGuard) export class ShopWithdrawController { constructor(private readonly settlementService: SettlementService) {} @Get('summary') summary(@CurrentUser() user: AuthUser) { return this.settlementService.getShopWithdrawSummary(user.actorId, user.storeId!); } @Get('requests') requests( @CurrentUser() user: AuthUser, @Query('page') page = '1', @Query('pageSize') pageSize = '20', @Query('status') status?: string, ) { return this.settlementService.listShopWithdrawRequests(user.actorId, user.storeId!, { page: Number(page), pageSize: Number(pageSize), status, }); } @Post() @UseGuards(ShopPrimaryGuard) apply(@CurrentUser() user: AuthUser, @Body() body: { amount?: number }) { return this.settlementService.createStoreWithdrawRequest(user.actorId, user.storeId!, body); } } @Controller('admin/store-withdrawals') @UseGuards(HqAuthGuard) export class AdminStoreWithdrawController { constructor(private readonly settlementService: SettlementService) {} @Get('overdue-summary') overdueSummary() { return this.settlementService.getStoreWithdrawOverdueSummary(); } @Get() list(@Query() query: Record) { return this.settlementService.listAdminStoreWithdrawals({ 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(':id') detail(@Param('id') id: string) { return this.settlementService.getAdminStoreWithdrawal(BigInt(id)); } @Post(':id/approve') @HqOperation({ action: HqOperationAction.STORE_WITHDRAW_APPROVE, refType: 'STORE_WITHDRAW', refIdParam: 'id', includeBody: true, }) approve( @CurrentUser() user: AuthUser, @Param('id') id: string, @Body() body: { paymentRef?: string }, ) { return this.settlementService.approveStoreWithdraw(BigInt(id), user.actorId, body); } @Post(':id/reject') @HqOperation({ action: HqOperationAction.STORE_WITHDRAW_REJECT, refType: 'STORE_WITHDRAW', refIdParam: 'id', includeBody: true, }) reject( @CurrentUser() user: AuthUser, @Param('id') id: string, @Body() body: { reason?: string }, ) { if (!body?.reason?.trim()) { throw new BadRequestException('请填写驳回理由'); } return this.settlementService.rejectStoreWithdraw(BigInt(id), user.actorId, body.reason); } } @Controller('admin/store-payouts') @UseGuards(HqAuthGuard) export class AdminStorePayoutController { constructor(private readonly settlementService: SettlementService) {} @Get() list(@Query() query: Record) { return this.settlementService.listAdminStorePayouts({ 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('daily') daily(@Query() query: Record) { return this.settlementService.listAdminStoreDailyBills({ page: query.page ? Number(query.page) : 1, pageSize: query.pageSize ? Number(query.pageSize) : 50, status: query.status, storeId: query.storeId, dateFrom: query.dateFrom, dateTo: query.dateTo, }); } @Get('export') export(@Query() query: Record) { return this.settlementService.exportAdminStorePayouts({ status: query.status, storeId: query.storeId, dateFrom: query.dateFrom, dateTo: query.dateTo, }); } @Post('batch-confirm') @HqOperation({ action: HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM, refType: 'STORE_PAYOUT', batch: true, includeBody: true, }) batchConfirm(@Body() body: { ids: string[]; batchNo?: string }) { return this.settlementService.batchConfirmStorePayouts(body.ids ?? [], body); } @Post('scan-due') scanDue() { return this.settlementService.scanDueStorePayouts(); } @Get(':id') detail(@Param('id') id: string) { return this.settlementService.getAdminStorePayout(BigInt(id)); } @Post(':id/confirm') @HqOperation({ action: HqOperationAction.STORE_PAYOUT_CONFIRM, refType: 'STORE_PAYOUT', refIdParam: 'id', includeBody: true, }) confirm(@Param('id') id: string, @Body() body: { paymentRef?: string; batchNo?: string; remark?: string }) { return this.settlementService.confirmStorePayout(BigInt(id), body); } } @Controller('admin/store-settlements') @UseGuards(HqAuthGuard) export class AdminStoreSettlementController { constructor(private readonly settlementService: SettlementService) {} @Get() list(@Query() query: Record) { return this.settlementService.listAdminStoreSettlements({ page: query.page ? Number(query.page) : 1, pageSize: query.pageSize ? Number(query.pageSize) : 20, kind: query.kind, status: query.status, storeId: query.storeId, dateFrom: query.dateFrom, dateTo: query.dateTo, }); } } @Controller('admin/store-bills') @UseGuards(HqAuthGuard) export class AdminStoreBillController { constructor(private readonly settlementService: SettlementService) {} @Get() list(@Query() query: Record) { 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) { 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 { constructor(private readonly settlementService: SettlementService) {} @Post('generate') @HqOperation({ action: HqOperationAction.PARTNER_BILL_GENERATE, refType: 'PARTNER_BILL', refIdField: 'id', includeBody: true, }) generate(@Body() body: { partnerId: string; year: number; month: number }) { return this.settlementService.generatePartnerBill(body); } @Post('generate-all') @HqOperation({ action: HqOperationAction.PARTNER_BILL_GENERATE, refType: 'PARTNER_BILL', batch: true, includeBody: true, }) generateAll(@Body() body: { year: number; month: number }) { return this.settlementService.generateAllPartnerBills(body); } @Get() list(@Query() query: Record) { return this.settlementService.listAdminPartnerBills({ page: query.page ? Number(query.page) : 1, pageSize: query.pageSize ? Number(query.pageSize) : 20, status: query.status, partnerId: query.partnerId, year: query.year ? Number(query.year) : undefined, month: query.month ? Number(query.month) : undefined, }); } @Get('export') export(@Query() query: Record) { return this.settlementService.exportPartnerBills({ partnerId: query.partnerId, status: query.status, year: query.year ? Number(query.year) : undefined, month: query.month ? Number(query.month) : undefined, }); } @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, refType: 'PARTNER_BILL', refIdParam: 'id', }) confirm(@Param('id') id: string) { return this.settlementService.confirmPartnerBill(BigInt(id)); } @Post(':id/mark-paid') @HqOperation({ action: HqOperationAction.PARTNER_BILL_MARK_PAID, refType: 'PARTNER_BILL', refIdParam: 'id', includeBody: true, }) 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') @UseGuards(HqAuthGuard) export class AdminWineryBillController { constructor(private readonly settlementService: SettlementService) {} @Get() list(@Query() query: Record) { 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, }); } @Get('export') export(@Query() query: Record) { 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, }); } @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('admin/logistics-bills') @UseGuards(HqAuthGuard) export class AdminLogisticsBillController { constructor(private readonly settlementService: SettlementService) {} @Get() list(@Query() query: Record) { return this.settlementService.listAdminLogisticsBills({ page: query.page ? Number(query.page) : 1, pageSize: query.pageSize ? Number(query.pageSize) : 20, status: query.status, providerId: query.providerId, year: query.year ? Number(query.year) : undefined, month: query.month ? Number(query.month) : undefined, }); } @Get('provider-summary') providerSummary(@Query() query: Record) { return this.settlementService.listLogisticsProviderSummary({ year: query.year ? Number(query.year) : undefined, month: query.month ? Number(query.month) : undefined, }); } @Get('export') export(@Query() query: Record) { return this.settlementService.exportAdminLogisticsBills({ status: query.status, providerId: query.providerId, year: query.year ? Number(query.year) : undefined, month: query.month ? Number(query.month) : undefined, }); } @Post('generate') generate(@Body() body: { year: number; month: number; providerId?: string }) { if (!body?.year || !body?.month) { throw new BadRequestException('请指定年月'); } if (body.providerId) { return this.settlementService.generateLogisticsBill({ providerId: body.providerId, year: body.year, month: body.month, }); } return this.settlementService.generateAllLogisticsBills({ year: body.year, month: body.month, }); } @Post('batch-confirm') @HqOperation({ action: HqOperationAction.LOGISTICS_BILL_BATCH_CONFIRM, refType: 'LOGISTICS_BILL', batch: true, includeBody: true, }) batchConfirm(@Body() body: { ids: string[] }) { return this.settlementService.batchConfirmLogisticsBills(body.ids ?? []); } @Get(':id') detail(@Param('id') id: string) { return this.settlementService.getAdminLogisticsBill(BigInt(id)); } @Post(':id/confirm') @HqOperation({ action: HqOperationAction.LOGISTICS_BILL_CONFIRM, refType: 'LOGISTICS_BILL', refIdParam: 'id', }) confirm(@Param('id') id: string) { return this.settlementService.confirmLogisticsBill(BigInt(id)); } } @Controller('partner/me') @UseGuards(JwtAuthGuard) export class PartnerMeController { constructor( private readonly prisma: PrismaService, private readonly partnerCityService: PartnerCityService, ) {} @Get() async me(@CurrentUser() user: AuthUser) { return this.buildPartnerMe(user.actorId); } @Put() async update(@CurrentUser() user: AuthUser, @Body() dto: UpdatePartnerMeDto) { if (dto.name !== undefined && !dto.name.trim()) { throw new BadRequestException('姓名不能为空'); } if (dto.name !== undefined) { await this.prisma.partnerAccount.update({ where: { id: user.actorId }, data: { name: dto.name.trim() }, }); } return this.buildPartnerMe(user.actorId); } private async buildPartnerMe(actorId: bigint) { let account = await this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: actorId }, }); let primary = account; if (account.isPrimary !== 1 && account.parentAccountId) { primary = await this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: account.parentAccountId }, }); } if (account.isPrimary !== 1) { const perms = Array.isArray(account.permissions) ? (account.permissions as string[]) : []; const hasStorePerm = perms.includes('store:create') || perms.includes('store:manage'); const warehouseOnly = !hasStorePerm && perms.length > 0 && (perms.includes('warehouse:manage') || perms.includes('order:view')); if (!hasStorePerm && !warehouseOnly) { account = await this.prisma.partnerAccount.update({ where: { id: account.id }, data: { permissions: [...DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS] }, }); } } const hasWarehouseAccess = await this.partnerCityService.hasManagedWarehouse(primary.id); return { id: account.id.toString(), name: account.name, phone: account.phone, isPrimary: account.isPrimary === 1, staffRole: account.staffRole ?? undefined, permissions: Array.isArray(account.permissions) ? account.permissions : undefined, primaryAccountId: primary.id.toString(), primaryPhone: primary.phone, primaryName: primary.name, companyName: primary.companyName ?? undefined, hasWechat: !!account.wxOpenId, wxNickname: account.wxNickname ?? undefined, wxAvatarUrl: account.wxAvatarUrl ?? undefined, managedWarehouseId: primary.managedWarehouseId?.toString() ?? null, hasWarehouseAccess, }; } }