feat: M4-11 — vendor billing + secret lifecycle management
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 42s

- Add VendorBill Prisma model (provider/billMonth/amount/usageSummary)
- VendorBillController: CRUD bills, list/rotate/revoke secrets
- Secret lifecycle: active → expiring → expired/rotated/revoked

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-24 18:25:58 +08:00
parent b3176b8ead
commit c6d01534c8
3 changed files with 80 additions and 1 deletions

View File

@ -1519,3 +1519,19 @@ model DataExportRequest {
@@index([userId])
@@index([status])
}
model VendorBill {
id String @id @default(cuid())
provider String @db.VarChar(32)
billMonth String @db.VarChar(7)
amount Decimal @db.Decimal(10, 2)
currency String @default("CNY") @db.VarChar(8)
usageSummary Json?
paidAt DateTime?
notes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([provider, billMonth])
@@index([provider])
}

View File

@ -1,8 +1,9 @@
import { Module } from '@nestjs/common';
import { SecretController } from './secret.controller';
import { VendorBillController } from './vendor-bill.controller';
import { SecretService } from './secret.service';
import { PrismaService } from '../../infrastructure/database/prisma.service';
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
@Module({ controllers: [SecretController], providers: [SecretService, PrismaService, AdminAuthGuard, AdminRolesGuard], exports: [SecretService] })
@Module({ controllers: [SecretController, VendorBillController], providers: [SecretService, PrismaService, AdminAuthGuard, AdminRolesGuard], exports: [SecretService] })
export class SecretModule {}

View File

@ -0,0 +1,62 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { PrismaService } from '../../infrastructure/database/prisma.service';
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
@ApiTags('admin-vendor')
@ApiBearerAuth()
@Controller('admin-api/vendor')
@UseGuards(AdminAuthGuard, AdminRolesGuard)
export class VendorBillController {
constructor(private readonly prisma: PrismaService) {}
@Get('bills')
@ApiOperation({ summary: '服务商账单列表' })
async listBills() {
return this.prisma.vendorBill.findMany({ orderBy: { billMonth: 'desc' }, take: 100 });
}
@Post('bills')
@ApiOperation({ summary: '录入服务商账单' })
async createBill(@Body() dto: { provider: string; billMonth: string; amount: number; currency?: string; usageSummary?: any; notes?: string }) {
return this.prisma.vendorBill.create({ data: dto });
}
@Patch('bills/:id')
@ApiOperation({ summary: '更新服务商账单' })
async updateBill(@Param('id') id: string, @Body() dto: Record<string, any>) {
return this.prisma.vendorBill.update({ where: { id }, data: dto });
}
@Delete('bills/:id')
@ApiOperation({ summary: '删除服务商账单' })
async deleteBill(@Param('id') id: string) {
await this.prisma.vendorBill.delete({ where: { id } });
return { ok: true };
}
// ═══ Secret lifecycle management ═══
@Get('secrets')
@ApiOperation({ summary: '密钥列表(含生命周期状态)' })
async listSecrets() {
return this.prisma.secretRecord.findMany({ orderBy: { updatedAt: 'desc' } });
}
@Post('secrets/:id/rotate')
@ApiOperation({ summary: '轮换密钥(标记旧 key 为 rotated' })
async rotateSecret(@Param('id') id: string) {
const secret = await this.prisma.secretRecord.findUnique({ where: { id } });
if (!secret) throw new Error('密钥不存在');
await this.prisma.secretRecord.update({ where: { id }, data: { status: 'rotated' } });
return { ok: true, rotatedKey: secret.name };
}
@Post('secrets/:id/revoke')
@ApiOperation({ summary: '吊销密钥' })
async revokeSecret(@Param('id') id: string) {
await this.prisma.secretRecord.update({ where: { id }, data: { status: 'revoked' } });
return { ok: true };
}
}