import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Put, Query } from '@nestjs/common'; import { z } from 'zod'; import { CurrentUser, Public, RequiresAudience, type AuthPrincipal } from '../../common/auth'; import { AppError } from '../../common/errors'; import { AuthService } from '../auth/auth.service'; import { AdminService } from './admin.service'; const loginSchema = z.object({ username: z.string().min(1), password: z.string().min(1) }); const bootstrapSchema = z.object({ username: z.string().min(3), password: z.string().min(8) }); const updateUserSchema = z .object({ nickname: z.string().max(32).optional(), headicon_id: z.coerce.number().int().min(0).optional(), status: z.coerce.number().int().min(0).max(1).optional(), }) .strict(); const settingSchema = z.object({ key: z.string().min(1).max(64), value: z.string(), remark: z.string().optional() }); const grantSchema = z.object({ userId: z.coerce.number().int().positive(), courseId: z.coerce.number().int().positive(), expireAt: z.coerce.date().nullish(), note: z.string().max(255).optional(), }); const refundSchema = z.object({ reason: z.string().min(1).max(100) }); /** 管理员登录。必须公开,否则登录接口本身会被守卫拦住。 */ @Public() @Controller('admin/v1/auth') export class AdminAuthController { constructor( private readonly admin: AdminService, private readonly auth: AuthService, ) {} @Post('login') async login(@Body() body: unknown) { const parsed = loginSchema.safeParse(body); if (!parsed.success) throw AppError.badRequest('请输入账号与密码'); const admin = await this.admin.verifyAdmin(parsed.data.username, parsed.data.password); return { accessToken: await this.auth.signAdminToken(admin.id), admin }; } /** * 首次部署创建超管。仅当系统中尚无任何管理员时生效, * 因此可以安全地保留在接口里作为初始化入口。 */ @Post('bootstrap') bootstrap(@Body() body: unknown) { const parsed = bootstrapSchema.safeParse(body); if (!parsed.success) throw AppError.badRequest('用户名至少 3 位,密码至少 8 位'); return this.admin.bootstrapAdmin(parsed.data.username, parsed.data.password); } } /** 管理后台主接口。全部要求管理员身份。 */ @RequiresAudience('admin') @Controller('admin/v1') export class AdminController { constructor(private readonly admin: AdminService) {} @Get('stats') stats() { return this.admin.stats(); } // ── 用户 ─────────────────────────────────────────────────────────────── @Get('users') listUsers( @Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('keyword') keyword?: string, ) { return this.admin.listUsers(Number(page) || 1, Number(pageSize) || 20, keyword ?? ''); } @Patch('users/:id') updateUser(@Param('id', ParseIntPipe) id: number, @Body() body: unknown) { const parsed = updateUserSchema.safeParse(body); if (!parsed.success) throw AppError.badRequest('参数错误'); return this.admin.updateUser(id, parsed.data); } // ── 通用内容 CRUD ────────────────────────────────────────────────────── @Get('data/:resource') listResource( @Param('resource') resource: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('keyword') keyword?: string, @Query('courseId') courseId?: string, ) { const filter = courseId ? { course_id: Number(courseId) } : {}; return this.admin.listResource(resource, Number(page) || 1, Number(pageSize) || 20, keyword ?? '', filter); } @Post('data/:resource') createResource(@Param('resource') resource: string, @Body() body: unknown) { return this.admin.createResource(resource, body); } @Patch('data/:resource/:id') updateResource( @Param('resource') resource: string, @Param('id', ParseIntPipe) id: number, @Body() body: unknown, ) { return this.admin.updateResource(resource, id, body); } @Delete('data/:resource/:id') deleteResource(@Param('resource') resource: string, @Param('id', ParseIntPipe) id: number) { return this.admin.deleteResource(resource, id); } // ── 配置 ─────────────────────────────────────────────────────────────── @Get('settings') listSettings() { return this.admin.listSettings(); } @Put('settings') upsertSetting(@Body() body: unknown) { const parsed = settingSchema.safeParse(body); if (!parsed.success) throw AppError.badRequest('参数错误'); return this.admin.upsertSetting(parsed.data.key, parsed.data.value, parsed.data.remark); } // ── 订单 / 权益 ──────────────────────────────────────────────────────── @Get('orders') listOrders(@Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('status') status?: string) { const s = status === undefined || status === '' ? undefined : Number(status); return this.admin.listOrders(Number(page) || 1, Number(pageSize) || 20, Number.isFinite(s) ? s : undefined); } @Post('orders/:id/refund') refundOrder(@Param('id', ParseIntPipe) id: number, @Body() body: unknown) { const parsed = refundSchema.safeParse(body); if (!parsed.success) throw AppError.badRequest('请填写退款原因'); return this.admin.refundOrder(id, parsed.data.reason); } @Get('entitlements') listEntitlements(@Query('userId', ParseIntPipe) userId: number) { return this.admin.listEntitlements(userId); } @Post('entitlements') grantEntitlement(@Body() body: unknown) { const parsed = grantSchema.safeParse(body); if (!parsed.success) throw AppError.badRequest('参数错误'); return this.admin.grantEntitlement({ userId: parsed.data.userId, courseId: parsed.data.courseId, expireAt: parsed.data.expireAt ?? null, note: parsed.data.note, }); } @Delete('entitlements') revokeEntitlement(@Query('userId', ParseIntPipe) userId: number, @Query('courseId', ParseIntPipe) courseId: number) { return this.admin.revokeEntitlement(userId, courseId); } @Get('me') me(@CurrentUser() admin: AuthPrincipal) { return { id: admin.id, audience: admin.audience }; } }