diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 11e739e..9cd2214 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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]) +} diff --git a/src/modules/secret/secret.module.ts b/src/modules/secret/secret.module.ts index 9396655..0c030bc 100644 --- a/src/modules/secret/secret.module.ts +++ b/src/modules/secret/secret.module.ts @@ -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 {} diff --git a/src/modules/secret/vendor-bill.controller.ts b/src/modules/secret/vendor-bill.controller.ts new file mode 100644 index 0000000..2afe036 --- /dev/null +++ b/src/modules/secret/vendor-bill.controller.ts @@ -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) { + 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 }; + } +}