--- change: betterauth-hono-drizzle-migration design-doc: docs/superpowers/specs/2026-07-03-betterauth-hono-drizzle-migration-design.md base-ref: ca3511d57d9b9b5b52afe02ca4ffc7fa112bc404 --- # NestJS → HonoJS + better-auth + Drizzle + PGlite/PostgreSQL 迁移实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 apps/server 从 NestJS + TypeORM + MySQL/better-sqlite3 全栈替换为 HonoJS + Drizzle ORM + better-auth + PGlite(dev)/PostgreSQL(prod),保持 60+ API 端点兼容、49 权限点不变、前端零改动。 **Architecture:** HonoJS 作为 HTTP 框架,better-auth 接管认证与 RBAC,Drizzle ORM 替代 TypeORM 管理 16 张表,PGlite 提供零 Docker 的本地开发体验,PostgreSQL 用于生产。中间件栈:CORS → RateLimit → better-auth JWT → PermissionCheck → ZodValidator → Route Handler。 **Tech Stack:** Hono 4.x, @hono/node-server, better-auth, drizzle-orm, @electric-sql/pglite, pg, zod, @hono/zod-validator, hono-rate-limiter, exceljs, pdfkit, bcryptjs ## Global Constraints - Node.js ≥ 20, TypeScript 5.x, pnpm (monorepo) - 所有 API 端点路径和响应格式必须与 NestJS 版本保持兼容 - 49 个权限码(14 分组)完整保留,前端 `@RequirePermission` 等效逻辑零改动 - 4 个预设角色(admin/supervisor/teacher/institution)权限分配不变 - PGlite 数据文件路径:`apps/server/pglite-data/`,加入 .gitignore - better-auth JWT payload 必须包含 `{ sub: id, username, permissions: string[] }` - 开发命令:`tsx watch src/index.ts`(热重载),构建:`tsc -p tsconfig.build.json` --- ### Task 1: 项目初始化与环境搭建 **Files:** - Modify: `apps/server/package.json` - Create: `apps/server/tsconfig.build.json` - Modify: `apps/server/tsconfig.json` - Modify: `turbo.json` - Create: `apps/server/.gitignore`(追加 pglite-data/) **Interfaces:** - Produces: `pnpm dev` 可启动空 Hono 服务器在 :3003 - [ ] **Step 1: 更新 package.json — 替换依赖和 scripts** ```bash cd apps/server # 移除 NestJS 相关依赖 pnpm remove @nestjs/common @nestjs/core @nestjs/config @nestjs/jwt @nestjs/passport \ @nestjs/platform-express @nestjs/throttler @nestjs/typeorm @nestjs/cli \ @nestjs/schematics @nestjs/testing typeorm mysql2 better-sqlite3 \ passport passport-jwt passport-local class-transformer class-validator \ reflect-metadata rxjs multer @types/multer @types/better-sqlite3 @types/express # 安装新依赖 pnpm add hono @hono/node-server better-auth drizzle-orm @electric-sql/pglite pg \ exceljs pdfkit bcryptjs zod @hono/zod-validator hono-rate-limiter dotenv # 安装 dev 依赖 pnpm add -D drizzle-kit tsx @types/pg @types/bcryptjs @types/pdfkit ``` 更新 `apps/server/package.json` scripts: ```json { "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc -p tsconfig.build.json", "start": "node dist/index.js", "typecheck": "tsc -p tsconfig.build.json --noEmit", "db:push": "drizzle-kit push", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:studio": "drizzle-kit studio", "db:seed": "tsx src/db/seed.ts" } } ``` - [ ] **Step 2: 创建 tsconfig.build.json** ```json { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist", "test"] } ``` - [ ] **Step 3: 更新 turbo.json** 确保 `apps/server` 的 build 和 dev pipeline 配置正确(移除 NestJS 特有配置): ```json { "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "dev": { "cache": false, "persistent": true }, "typecheck": { "dependsOn": ["^build"] } } } ``` - [ ] **Step 4: 创建最小 Hono 入口验证项目可启动** 创建 `apps/server/src/index.ts`: ```typescript import { Hono } from 'hono'; import { serve } from '@hono/node-server'; const app = new Hono(); app.get('/api/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() })); const port = Number(process.env.PORT) || 3003; console.log(`Server running on http://localhost:${port}`); serve({ fetch: app.fetch, port }); ``` - [ ] **Step 5: 验证启动** ```bash cd apps/server && pnpm dev # 预期:Server running on http://localhost:3003 # curl http://localhost:3003/api/health → {"status":"ok","timestamp":"..."} ``` - [ ] **Step 6: Commit** ```bash git add apps/server/package.json apps/server/pnpm-lock.yaml apps/server/tsconfig.build.json \ apps/server/tsconfig.json turbo.json apps/server/.gitignore apps/server/src/index.ts git commit -m "feat: initialize HonoJS project structure, replace NestJS dependencies" ``` --- ### Task 2: 数据库 Schema 定义 — 认证相关表 **Files:** - Create: `apps/server/src/db/schema/auth.ts` - Create: `apps/server/src/db/schema/index.ts` - Create: `apps/server/drizzle.config.ts` **Interfaces:** - Produces: `users`, `roles`, `permissions`, `user_roles`, `role_permissions` 5 张 Drizzle 表定义 - Consumes: Task 1 (项目结构) - [ ] **Step 1: 创建 drizzle.config.ts** ```typescript import { defineConfig } from 'drizzle-kit'; export default defineConfig({ schema: './src/db/schema/index.ts', out: './drizzle', dialect: 'postgresql', dbCredentials: { host: process.env.DB_HOST || 'localhost', port: Number(process.env.DB_PORT) || 5432, user: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || 'postgres', database: process.env.DB_DATABASE || 'gongxue', ssl: process.env.DB_SSL === 'true', }, }); ``` - [ ] **Step 2: 定义 Drizzle schema — auth.ts(认证 5 表)** ```typescript // apps/server/src/db/schema/auth.ts import { pgTable, serial, varchar, boolean, timestamp, integer, primaryKey } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'; export const users = pgTable('users', { id: serial('id').primaryKey(), username: varchar('username', { length: 50 }).unique().notNull(), passwordHash: varchar('password_hash', { length: 255 }).notNull(), name: varchar('name', { length: 50 }), isActive: boolean('is_active').default(true).notNull(), lastLoginAt: timestamp('last_login_at'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); export const roles = pgTable('roles', { id: serial('id').primaryKey(), name: varchar('name', { length: 30 }).unique().notNull(), description: varchar('description', { length: 200 }), isSystem: boolean('is_system').default(false).notNull(), status: integer('status').default(1).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); export const permissions = pgTable('permissions', { id: serial('id').primaryKey(), code: varchar('code', { length: 50 }).unique().notNull(), name: varchar('name', { length: 50 }).notNull(), group: varchar('group', { length: 30 }).notNull(), description: varchar('description', { length: 200 }), }); export const userRoles = pgTable('user_roles', { userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), roleId: integer('role_id').references(() => roles.id, { onDelete: 'cascade' }).notNull(), }, (t) => ({ pk: primaryKey({ columns: [t.userId, t.roleId] }) })); export const rolePermissions = pgTable('role_permissions', { roleId: integer('role_id').references(() => roles.id, { onDelete: 'cascade' }).notNull(), permissionId: integer('permission_id').references(() => permissions.id, { onDelete: 'cascade' }).notNull(), }, (t) => ({ pk: primaryKey({ columns: [t.roleId, t.permissionId] }) })); // Relations export const usersRelations = relations(users, ({ many }) => ({ userRoles: many(userRoles), })); export const rolesRelations = relations(roles, ({ many }) => ({ userRoles: many(userRoles), rolePermissions: many(rolePermissions), })); export const permissionsRelations = relations(permissions, ({ many }) => ({ rolePermissions: many(rolePermissions), })); export const userRolesRelations = relations(userRoles, ({ one }) => ({ user: one(users, { fields: [userRoles.userId], references: [users.id] }), role: one(roles, { fields: [userRoles.roleId], references: [roles.id] }), })); export const rolePermissionsRelations = relations(rolePermissions, ({ one }) => ({ role: one(roles, { fields: [rolePermissions.roleId], references: [roles.id] }), permission: one(permissions, { fields: [rolePermissions.permissionId], references: [permissions.id] }), })); ``` - [ ] **Step 3: 创建 schema barrel 导出** ```typescript // apps/server/src/db/schema/index.ts export * from './auth'; ``` - [ ] **Step 4: Commit** ```bash git add apps/server/drizzle.config.ts apps/server/src/db/schema/auth.ts apps/server/src/db/schema/index.ts git commit -m "feat: add Drizzle schema for auth tables (users, roles, permissions, user_roles, role_permissions)" ``` --- ### Task 3: 数据库 Schema 定义 — 业务表 **Files:** - Create: `apps/server/src/db/schema/student.ts` - Create: `apps/server/src/db/schema/room.ts` - Create: `apps/server/src/db/schema/occupancy.ts` - Create: `apps/server/src/db/schema/expense.ts` - Create: `apps/server/src/db/schema/bill.ts` - Modify: `apps/server/src/db/schema/index.ts` **Interfaces:** - Produces: `students`, `rooms`, `occupancies`, `room_expenses`, `personal_expenses`, `bills`, `bill_items` 7 张表 - Consumes: Task 2 (schema barrel) - [ ] **Step 1: students 表** ```typescript // apps/server/src/db/schema/student.ts import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core'; export const students = pgTable('students', { id: serial('id').primaryKey(), name: varchar('name', { length: 50 }).notNull(), phone: varchar('phone', { length: 20 }), idNumber: varchar('id_number', { length: 30 }), gender: varchar('gender', { length: 10 }), ethnicity: varchar('ethnicity', { length: 20 }), emergencyContact: varchar('emergency_contact', { length: 50 }), emergencyPhone: varchar('emergency_phone', { length: 20 }), status: varchar('status', { length: 20 }).default('active').notNull(), organization: varchar('organization', { length: 100 }), supervisor: varchar('supervisor', { length: 50 }), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); ``` - [ ] **Step 2: rooms 表** ```typescript // apps/server/src/db/schema/room.ts import { pgTable, serial, varchar, integer, timestamp } from 'drizzle-orm/pg-core'; export const rooms = pgTable('rooms', { id: serial('id').primaryKey(), roomNumber: varchar('room_number', { length: 20 }).unique().notNull(), building: varchar('building', { length: 50 }), floor: integer('floor'), capacity: integer('capacity').notNull(), status: varchar('status', { length: 20 }).default('available').notNull(), roomType: varchar('room_type', { length: 20 }), gender: varchar('gender', { length: 10 }), createdAt: timestamp('created_at').defaultNow().notNull(), }); ``` - [ ] **Step 3: occupancies 表** ```typescript // apps/server/src/db/schema/occupancy.ts import { pgTable, serial, integer, varchar, date, timestamp } from 'drizzle-orm/pg-core'; import { students } from './student'; import { rooms } from './room'; export const occupancies = pgTable('occupancies', { id: serial('id').primaryKey(), studentId: integer('student_id').references(() => students.id, { onDelete: 'cascade' }).notNull(), roomId: integer('room_id').references(() => rooms.id, { onDelete: 'cascade' }).notNull(), checkInDate: date('check_in_date').notNull(), checkOutDate: date('check_out_date'), price: integer('price'), status: varchar('status', { length: 20 }).default('active').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); ``` - [ ] **Step 4: expenses 表(room_expenses + personal_expenses)** ```typescript // apps/server/src/db/schema/expense.ts import { pgTable, serial, integer, varchar, decimal, date, timestamp } from 'drizzle-orm/pg-core'; import { rooms } from './room'; import { students } from './student'; export const roomExpenses = pgTable('room_expenses', { id: serial('id').primaryKey(), roomId: integer('room_id').references(() => rooms.id, { onDelete: 'cascade' }).notNull(), name: varchar('name', { length: 100 }).notNull(), amount: decimal('amount', { precision: 10, scale: 2 }).notNull(), periodStart: date('period_start').notNull(), periodEnd: date('period_end').notNull(), status: varchar('status', { length: 20 }).default('active').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); export const personalExpenses = pgTable('personal_expenses', { id: serial('id').primaryKey(), studentId: integer('student_id').references(() => students.id, { onDelete: 'cascade' }).notNull(), name: varchar('name', { length: 100 }).notNull(), amount: decimal('amount', { precision: 10, scale: 2 }).notNull(), periodStart: date('period_start').notNull(), periodEnd: date('period_end').notNull(), status: varchar('status', { length: 20 }).default('active').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); ``` - [ ] **Step 5: bills 表(bills + bill_items)** ```typescript // apps/server/src/db/schema/bill.ts import { pgTable, serial, integer, varchar, decimal, date, timestamp } from 'drizzle-orm/pg-core'; import { students } from './student'; export const bills = pgTable('bills', { id: serial('id').primaryKey(), studentId: integer('student_id').references(() => students.id, { onDelete: 'cascade' }).notNull(), billNumber: varchar('bill_number', { length: 50 }).notNull(), totalAmount: decimal('total_amount', { precision: 10, scale: 2 }).notNull(), status: varchar('status', { length: 20 }).default('pending').notNull(), periodStart: date('period_start'), periodEnd: date('period_end'), issuedAt: timestamp('issued_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); export const billItems = pgTable('bill_items', { id: serial('id').primaryKey(), billId: integer('bill_id').references(() => bills.id, { onDelete: 'cascade' }).notNull(), name: varchar('name', { length: 100 }).notNull(), amount: decimal('amount', { precision: 10, scale: 2 }).notNull(), type: varchar('type', { length: 20 }).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), }); ``` - [ ] **Step 6: Commit** ```bash git add apps/server/src/db/schema/ git commit -m "feat: add Drizzle schema for business tables (students, rooms, occupancies, expenses, bills)" ``` --- ### Task 4: 数据库 Schema 定义 — 其他表 + 连接工厂 + Seed **Files:** - Create: `apps/server/src/db/schema/classroom.ts` - Create: `apps/server/src/db/schema/deposit.ts` - Create: `apps/server/src/db/schema/operation-log.ts` - Modify: `apps/server/src/db/schema/index.ts`(导出所有 16 表) - Create: `apps/server/src/db/index.ts`(连接工厂) - Create: `apps/server/src/db/seed.ts`(幂等 seed) **Interfaces:** - Produces: `classrooms`, `tenants`, `classroom_rentals`, `deposits`, `operation_logs` 5 张表 + DB 连接工厂 + seed 脚本 - Consumes: Tasks 2-3 (schema 定义) - [ ] **Step 1: 剩余 5 张业务表定义** ```typescript // apps/server/src/db/schema/classroom.ts import { pgTable, serial, varchar, integer, decimal, timestamp } from 'drizzle-orm/pg-core'; export const classrooms = pgTable('classrooms', { id: serial('id').primaryKey(), name: varchar('name', { length: 100 }).notNull(), capacity: integer('capacity'), location: varchar('location', { length: 200 }), pricePerDay: decimal('price_per_day', { precision: 10, scale: 2 }), status: varchar('status', { length: 20 }).default('available').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); export const tenants = pgTable('tenants', { id: serial('id').primaryKey(), name: varchar('name', { length: 100 }).notNull(), contactPerson: varchar('contact_person', { length: 50 }), phone: varchar('phone', { length: 20 }), notes: varchar('notes', { length: 500 }), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); export const classroomRentals = pgTable('classroom_rentals', { id: serial('id').primaryKey(), classroomId: integer('classroom_id').references(() => classrooms.id, { onDelete: 'cascade' }).notNull(), tenantId: integer('tenant_id').references(() => tenants.id, { onDelete: 'cascade' }).notNull(), rentalDate: date('rental_date').notNull(), startTime: varchar('start_time', { length: 10 }), endTime: varchar('end_time', { length: 10 }), price: decimal('price', { precision: 10, scale: 2 }), status: varchar('status', { length: 20 }).default('active').notNull(), notes: varchar('notes', { length: 500 }), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); // apps/server/src/db/schema/deposit.ts export const deposits = pgTable('deposits', { id: serial('id').primaryKey(), studentId: integer('student_id').references(() => students.id, { onDelete: 'cascade' }).notNull(), amount: decimal('amount', { precision: 10, scale: 2 }).notNull(), notes: varchar('notes', { length: 500 }), status: varchar('status', { length: 20 }).default('active').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), }); // apps/server/src/db/schema/operation-log.ts export const operationLogs = pgTable('operation_logs', { id: serial('id').primaryKey(), userId: integer('user_id'), username: varchar('username', { length: 50 }), module: varchar('module', { length: 50 }), action: varchar('action', { length: 50 }), targetId: integer('target_id'), targetType: varchar('target_type', { length: 50 }), detail: text('detail'), ipAddress: varchar('ip_address', { length: 50 }), userAgent: varchar('user_agent', { length: 500 }), status: varchar('status', { length: 20 }).default('success'), createdAt: timestamp('created_at').defaultNow().notNull(), }); ``` - [ ] **Step 2: 数据库连接工厂** ```typescript // apps/server/src/db/index.ts import { drizzle } from 'drizzle-orm/pglite'; import { PGlite } from '@electric-sql/pglite'; import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import * as schema from './schema'; const isProd = process.env.NODE_ENV === 'production'; function createDevDb() { const client = new PGlite('pglite-data'); return drizzle(client, { schema }); } function createProdDb() { const pool = new Pool({ host: process.env.DB_HOST || 'localhost', port: Number(process.env.DB_PORT) || 5432, user: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || 'postgres', database: process.env.DB_DATABASE || 'gongxue', ssl: process.env.DB_SSL === 'true', }); return drizzlePg(pool, { schema }); } export const db = isProd ? createProdDb() : createDevDb(); ``` - [ ] **Step 3: 幂等 seed 脚本** 基于现有 `rbac.service.ts` 中的 `seedData()` 逻辑,改写为 Drizzle 版本(49 权限码 + 4 预设角色 + admin 用户)。保持相同的 seed 数据内容,用 Drizzle `insert...onConflictDoNothing` 实现幂等。 核心流程: 1. 插入 49 权限码(`onConflictDoNothing`,按 code 唯一键) 2. 插入 4 预设角色(`onConflictDoNothing`,按 name 唯一键) 3. 查询所有权限和角色,构建角色-权限关联 4. 按 group 匹配权限给各角色(超管=全部,宿管=8组,老师=student:view,机构=3组) 5. 初始化 admin 用户(若 users 表为空),密码 bcrypt hash,分配超管角色 完整代码参考现有 `rbac.service.ts:120-191` 的 seedData 逻辑。 - [ ] **Step 4: Commit** ```bash git add apps/server/src/db/ git commit -m "feat: add remaining Drizzle schemas, DB connection factory, and idempotent seed script" ``` --- ### Task 5: better-auth 实例配置 **Files:** - Create: `apps/server/src/auth/index.ts` - Create: `apps/server/src/lib/env.ts`(环境变量加载) **Interfaces:** - Produces: `auth` 实例(含 username + jwt + admin 插件)、`getAuth` / `getSession` 工具函数 - Consumes: Tasks 2-4 (schema + db) - [ ] **Step 1: 环境变量工具** ```typescript // apps/server/src/lib/env.ts import { config } from 'dotenv'; config(); export const env = { PORT: Number(process.env.PORT) || 3003, NODE_ENV: process.env.NODE_ENV || 'development', DB_HOST: process.env.DB_HOST || 'localhost', DB_PORT: Number(process.env.DB_PORT) || 5432, DB_USERNAME: process.env.DB_USERNAME || 'postgres', DB_PASSWORD: process.env.DB_PASSWORD || 'postgres', DB_DATABASE: process.env.DB_DATABASE || 'gongxue', BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET || 'dorm-billing-jwt-secret-key-2024', BETTER_AUTH_URL: process.env.BETTER_AUTH_URL || 'http://localhost:3003', ADMIN_PASSWORD: process.env.ADMIN_PASSWORD || 'admin123', }; ``` - [ ] **Step 2: better-auth 实例配置** ```typescript // apps/server/src/auth/index.ts import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { username, jwt, admin } from 'better-auth/plugins'; import { db } from '../db'; import * as schema from '../db/schema'; import { env } from '../lib/env'; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: 'pg', schema: { users: schema.users, roles: schema.roles, permissions: schema.permissions, userRoles: schema.userRoles, rolePermissions: schema.rolePermissions, }, }), emailAndPassword: { enabled: false, }, plugins: [ username(), jwt({ jwt: { secret: env.BETTER_AUTH_SECRET, expiresIn: '7d', }, }), admin(), ], hooks: { after: { // JWT 签发后注入 permissions 到 claims createJwt: async (ctx) => { // 查询用户角色和权限,注入 permissions 数组到 JWT payload }, }, }, }); // 便捷导出 export const getSession = auth.api.getSession; ``` **注意**: better-auth JWT plugin + admin plugin 的 `createJwt` hook 需要在 build 阶段验证具体 API。根据 better-auth 文档,可能需要通过 hook 或 middleware 注入 `permissions` 到 JWT claims。如果 hook 不支持,备选方案是在登录接口中自行组装 JWT claims。 - [ ] **Step 3: 验证 better-auth 初始化无错误** ```bash cd apps/server && pnpm typecheck ``` - [ ] **Step 4: Commit** ```bash git add apps/server/src/auth/ apps/server/src/lib/ git commit -m "feat: configure better-auth instance with username, jwt, and admin plugins" ``` --- ### Task 6: JWT 认证中间件 + 权限检查中间件 **Files:** - Create: `apps/server/src/middleware/auth.ts` - Create: `apps/server/src/middleware/permission.ts` **Interfaces:** - Produces: `authMiddleware`(等效 JwtAuthGuard)、`requirePermission(code)`(等效 @RequirePermission) - Consumes: Task 5 (auth 实例) - [ ] **Step 1: JWT 认证中间件** ```typescript // apps/server/src/middleware/auth.ts import { createMiddleware } from 'hono/factory'; import { getSession } from '../auth'; // 全局认证中间件:从 Authorization header 提取 JWT,校验并注入 session 到 context export const authMiddleware = createMiddleware(async (c, next) => { const authHeader = c.req.header('Authorization'); if (!authHeader?.startsWith('Bearer ')) { // 不直接拒绝,留给下游 permission 中间件处理 return next(); } const token = authHeader.slice(7); try { const session = await getSession(token); if (session) { c.set('session', session); c.set('user', session.user); } } catch { // token 无效,继续(权限中间件会拒绝) } await next(); }); ``` - [ ] **Step 2: 权限检查中间件** ```typescript // apps/server/src/middleware/permission.ts import { createMiddleware } from 'hono/factory'; // 路由级权限检查中间件(等效 @RequirePermission 装饰器) export function requirePermission(code: string) { return createMiddleware(async (c, next) => { const session = c.get('session'); if (!session) { return c.json({ message: '未登录', statusCode: 401 }, 401); } const perms: string[] = session.user?.permissions || []; // 拥有通配符 *(超管)或指定权限码即可通过 if (perms.includes('*') || perms.includes(code)) { return next(); } return c.json({ message: '权限不足', statusCode: 403 }, 403); }); } ``` - [ ] **Step 3: 更新 index.ts 挂载中间件** ```typescript // apps/server/src/index.ts 增加: import { authMiddleware } from './middleware/auth'; app.use('/api/*', authMiddleware); ``` - [ ] **Step 4: Commit** ```bash git add apps/server/src/middleware/ git commit -m "feat: implement JWT auth middleware and permission check middleware" ``` --- ### Task 7: 登录接口 **Files:** - Create: `apps/server/src/routes/auth.ts` - Modify: `apps/server/src/index.ts`(挂载路由) **Interfaces:** - Produces: `POST /api/auth/login`、`GET /api/auth/profile` - Consumes: Tasks 5-6 (auth 实例 + 中间件) - [ ] **Step 1: 实现认证路由** ```typescript // apps/server/src/routes/auth.ts import { Hono } from 'hono'; import { auth } from '../auth'; import { db } from '../db'; import * as schema from '../db/schema'; import { eq } from 'drizzle-orm'; import { requirePermission } from '../middleware/permission'; import { logOperation } from '../middleware/operation-log'; const authRoute = new Hono(); // POST /api/auth/login — 包装 better-auth signInUsername authRoute.post('/login', async (c) => { const { username, password } = await c.req.json(); const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'; const ua = (c.req.header('user-agent') || '').substring(0, 500); try { const result = await auth.api.signInUsername({ body: { username, password }, headers: c.req.raw.headers, }); // 查询完整用户信息(含角色、权限) const user = await db.query.users.findFirst({ where: eq(schema.users.username, username), with: { userRoles: { with: { role: { with: { rolePermissions: { with: { permission: true } } } } } }, }); // 收集权限码 const permSet = new Set(); const roleNames: string[] = []; if (user?.userRoles) { for (const ur of user.userRoles) { if (ur.role?.status === 1) { roleNames.push(ur.role.name); for (const rp of ur.role.rolePermissions || []) { if (rp.permission) permSet.add(rp.permission.code); } } } } // 更新登录时间 await db.update(schema.users).set({ lastLoginAt: new Date() }).where(eq(schema.users.id, user!.id)); // 记录操作日志 await db.insert(schema.operationLogs).values({ userId: user!.id, username, module: '认证', action: '登录成功', ipAddress: ip, userAgent: ua, status: 'success', }); return c.json({ access_token: result?.token, user: { id: user!.id, username: user!.username, name: user!.name, roles: roleNames, permissions: [...permSet] }, }); } catch (e: any) { await db.insert(schema.operationLogs).values({ username, module: '认证', action: '登录失败', detail: e.message || '密码错误', ipAddress: ip, userAgent: ua, status: 'fail', }); return c.json({ message: e.message || '用户名或密码错误', statusCode: 401 }, 401); } }); // GET /api/auth/profile — 获取当前用户信息 authRoute.get('/profile', requirePermission('dashboard:view'), (c) => { const user = c.get('user'); return c.json(user); }); export { authRoute }; ``` - [ ] **Step 2: 挂载路由到主 app** ```typescript // 在 apps/server/src/index.ts 中添加: import { authRoute } from './routes/auth'; app.route('/api/auth', authRoute); ``` - [ ] **Step 3: Commit** ```bash git add apps/server/src/routes/auth.ts apps/server/src/index.ts git commit -m "feat: implement login endpoint (POST /api/auth/login) with failed attempt tracking" ``` --- ### Task 8: 用户管理 CRUD 路由 **Files:** - Create: `apps/server/src/routes/users.ts` - Modify: `apps/server/src/index.ts` **Interfaces:** - Produces: `GET/POST/PUT/DELETE /api/users`、`PUT /api/users/:id/reset-password` - Consumes: Task 6 (中间件)、Task 4 (db) - [ ] **Step 1: 实现用户管理路由** 基于现有 `rbac.service.ts` 的 `findAllUsers`、`createUser`、`updateUser`、`resetPassword`、`deleteUser` 方法,转换为 Drizzle 查询。 ```typescript // apps/server/src/routes/users.ts import { Hono } from 'hono'; import { db } from '../db'; import * as schema from '../db/schema'; import { eq } from 'drizzle-orm'; import bcrypt from 'bcryptjs'; import { requirePermission } from '../middleware/permission'; import { logOperation } from '../middleware/operation-log'; const usersRoute = new Hono(); // GET /api/users usersRoute.get('/', requirePermission('user:view'), async (c) => { const users = await db.query.users.findMany({ with: { userRoles: { with: { role: true } } }, orderBy: (users, { desc }) => [desc(users.createdAt)], }); return c.json(users.map((u) => ({ id: u.id, username: u.username, name: u.name, isActive: u.isActive, lastLoginAt: u.lastLoginAt, createdAt: u.createdAt, updatedAt: u.updatedAt, roles: u.userRoles?.map((ur) => ({ id: ur.role.id, name: ur.role.name })) || [], }))); }); // POST /api/users usersRoute.post('/', requirePermission('user:create'), async (c) => { const { username, password, name, roleIds } = await c.req.json(); const existing = await db.query.users.findFirst({ where: eq(schema.users.username, username) }); if (existing) return c.json({ message: '用户名已存在' }, 400); const hash = await bcrypt.hash(password, 10); const [user] = await db.insert(schema.users).values({ username, passwordHash: hash, name }).returning(); if (roleIds?.length) { await db.insert(schema.userRoles).values(roleIds.map((rid: number) => ({ userId: user.id, roleId: rid }))); } return c.json({ message: '用户创建成功' }); }); // PUT /api/users/:id usersRoute.put('/:id', requirePermission('user:edit'), async (c) => { const id = Number(c.req.param('id')); const { username, name, isActive, roleIds } = await c.req.json(); // ... 更新逻辑(参考 rbac.service.ts:305-322) await db.update(schema.users).set({ name, isActive, username }).where(eq(schema.users.id, id)); // 更新角色关联 await db.delete(schema.userRoles).where(eq(schema.userRoles.userId, id)); if (roleIds?.length) { await db.insert(schema.userRoles).values(roleIds.map((rid: number) => ({ userId: id, roleId: rid }))); } return c.json({ message: '更新成功' }); }); // PUT /api/users/:id/reset-password usersRoute.put('/:id/reset-password', requirePermission('user:reset-password'), async (c) => { const id = Number(c.req.param('id')); const { password } = await c.req.json(); await db.update(schema.users).set({ passwordHash: await bcrypt.hash(password, 10) }).where(eq(schema.users.id, id)); return c.json({ message: '密码已重置' }); }); // DELETE /api/users/:id usersRoute.delete('/:id', requirePermission('user:delete'), async (c) => { const id = Number(c.req.param('id')); const user = await db.query.users.findFirst({ where: eq(schema.users.id, id) }); if (!user) return c.json({ message: '用户不存在' }, 404); if (user.username === 'admin') return c.json({ message: '不能删除默认管理员' }, 400); await db.delete(schema.users).where(eq(schema.users.id, id)); return c.json({ message: '用户已删除' }); }); export { usersRoute }; ``` - [ ] **Step 2: Commit** ```bash git add apps/server/src/routes/users.ts apps/server/src/index.ts git commit -m "feat: implement user management CRUD routes (GET/POST/PUT/DELETE /api/users)" ``` --- ### Task 9: 角色管理 CRUD 路由 + 权限查询 **Files:** - Create: `apps/server/src/routes/roles.ts` - Create: `apps/server/src/routes/permissions.ts` - Modify: `apps/server/src/index.ts` **Interfaces:** - Produces: `GET/POST/PUT/DELETE /api/roles`、`GET /api/permissions`、`GET /api/permissions/tree` - Consumes: Task 6 (中间件) - [ ] **Step 1: 角色管理路由** 基于 `rbac.service.ts:193-238`,转换为 Drizzle: ```typescript // apps/server/src/routes/roles.ts import { Hono } from 'hono'; import { db } from '../db'; import * as schema from '../db/schema'; import { eq } from 'drizzle-orm'; import { requirePermission } from '../middleware/permission'; const rolesRoute = new Hono(); rolesRoute.get('/', requirePermission('role:view'), async (c) => { const roles = await db.query.roles.findMany({ with: { rolePermissions: { with: { permission: true } } }, orderBy: (roles, { asc }) => [asc(roles.id)], }); return c.json(roles.map((r) => ({ ...r, permissions: r.rolePermissions?.map((rp) => rp.permission) || [], }))); }); rolesRoute.post('/', requirePermission('role:create'), async (c) => { const { name, description, permissionIds } = await c.req.json(); const [role] = await db.insert(schema.roles).values({ name, description }).returning(); if (permissionIds?.length) { await db.insert(schema.rolePermissions).values( permissionIds.map((pid: number) => ({ roleId: role.id, permissionId: pid })) ); } return c.json(role); }); rolesRoute.put('/:id', requirePermission('role:edit'), async (c) => { const id = Number(c.req.param('id')); const role = await db.query.roles.findFirst({ where: eq(schema.roles.id, id) }); if (!role) return c.json({ message: '角色不存在' }, 404); const { name, description, permissionIds } = await c.req.json(); if (role.isSystem && name !== undefined) return c.json({ message: '系统角色不可改名' }, 400); await db.update(schema.roles).set({ name, description }).where(eq(schema.roles.id, id)); if (permissionIds !== undefined) { await db.delete(schema.rolePermissions).where(eq(schema.rolePermissions.roleId, id)); if (permissionIds.length > 0) { await db.insert(schema.rolePermissions).values( permissionIds.map((pid: number) => ({ roleId: id, permissionId: pid })) ); } } return c.json({ message: '更新成功' }); }); rolesRoute.delete('/:id', requirePermission('role:delete'), async (c) => { const id = Number(c.req.param('id')); const role = await db.query.roles.findFirst({ where: eq(schema.roles.id, id) }); if (!role) return c.json({ message: '角色不存在' }, 404); if (role.isSystem) return c.json({ message: '系统角色不可删除' }, 400); await db.delete(schema.roles).where(eq(schema.roles.id, id)); return c.json({ message: '角色已删除' }); }); export { rolesRoute }; ``` - [ ] **Step 2: 权限查询路由** ```typescript // apps/server/src/routes/permissions.ts import { Hono } from 'hono'; import { db } from '../db'; import { requirePermission } from '../middleware/permission'; const permissionsRoute = new Hono(); permissionsRoute.get('/', requirePermission('dashboard:view'), async (c) => { const perms = await db.query.permissions.findMany({ orderBy: (permissions, { asc }) => [asc(permissions.group), asc(permissions.code)], }); return c.json(perms); }); permissionsRoute.get('/tree', requirePermission('dashboard:view'), async (c) => { const perms = await db.query.permissions.findMany({ orderBy: (permissions, { asc }) => [asc(permissions.group), asc(permissions.code)], }); const map = new Map(); for (const p of perms) { if (!map.has(p.group)) map.set(p.group, []); map.get(p.group)!.push(p); } return c.json(Array.from(map.entries()).map(([group, permissions]) => ({ group, permissions }))); }); export { permissionsRoute }; ``` - [ ] **Step 3: Commit** ```bash git add apps/server/src/routes/roles.ts apps/server/src/routes/permissions.ts apps/server/src/index.ts git commit -m "feat: implement role management CRUD and permission query routes" ``` --- ### Task 10: 学生管理路由 **Files:** - Create: `apps/server/src/routes/students.ts` - Modify: `apps/server/src/index.ts` **Interfaces:** - Produces: `GET/POST/PUT/DELETE /api/students`、`PUT /api/students/:id/restore`、`POST /api/students/batch-delete` - Consumes: Task 6 (中间件)、Task 4 (db) - [ ] **Step 1: 实现学生 CRUD** 基于现有 `students.controller.ts`(274 行),逐方法转换为 Hono + Drizzle 版本。关键转换: | NestJS | HonoJS | |--------|--------| | `@Query('name')` | `c.req.query('name')` | | `@Param('id')` | `c.req.param('id')` | | `@Body() dto` | `await c.req.json()` | | `this.service.findAll({...})` | `db.query.students.findMany({...})` | | `this.service.create(dto)` | `db.insert(schema.students).values({...})` | | `this.service.update(+id, dto)` | `db.update(schema.students).set({...}).where(eq(...))` | | `this.service.remove(+id)` | `db.update(...).set({ status: 'archived' })`(软删除) | 需要保持的 API 行为: - 列表支持 `?name=`, `?status=`, `?includeArchived=true` 查询参数 - 删除 = 软删除(设置 status='archived'),非物理删除 - 批量删除 = `POST /api/students/batch-delete` + `{ ids: number[] }` - 恢复 = `PUT /api/students/:id/restore`(设置 status='active') 完整代码约 200 行(参考 students.controller.ts)。 - [ ] **Step 2: 挂载路由、操作日志中间件集成** 在路由 handler 中调用 `logOperation(c, { userId, username, module, action, ... })` 记录操作日志。 - [ ] **Step 3: Commit** ```bash git add apps/server/src/routes/students.ts apps/server/src/index.ts git commit -m "feat: implement student CRUD routes with soft-delete, batch ops, and restore" ``` --- ### Task 11: 学生 Excel 导入导出 **Files:** - Modify: `apps/server/src/routes/students.ts`(追加导出、导入、模板端点) - Create: `apps/server/src/utils/excel.ts`(可选) **Interfaces:** - Consumes: Task 10 (学生基础 CRUD) - [ ] **Step 1: Excel 导出** 转换为 HonoJS 的 `c.body()` 方式: ```typescript // GET /api/students/export studentsRoute.get('/export', requirePermission('student:export'), async (c) => { const includeArchived = c.req.query('includeArchived') === 'true'; const students = await db.query.students.findMany({ where: includeArchived ? undefined : (students, { ne }) => [ne(students.status, 'archived')], }); const workbook = new ExcelJS.Workbook(); const ws = workbook.addWorksheet('学生名单'); // ... 列定义(同 NestJS 版本 students.controller.ts:47-62) for (const s of students) { ws.addRow({...}); } const buffer = await workbook.xlsx.writeBuffer(); return c.body(buffer as any, 200, { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'Content-Disposition': 'attachment; filename=students.xlsx', }); }); ``` - [ ] **Step 2: 模板下载** ```typescript // GET /api/students/template studentsRoute.get('/template', requirePermission('student:view'), async (c) => { // ... 同 NestJS students.controller.ts:93-129 const buffer = await workbook.xlsx.writeBuffer(); return c.body(buffer as any, 200, { 'Content-Type': '...', 'Content-Disposition': 'attachment; filename=student_template.xlsx' }); }); ``` - [ ] **Step 3: Excel 导入** ```typescript // POST /api/students/import studentsRoute.post('/import', requirePermission('student:import'), async (c) => { const body = await c.req.parseBody(); const file = body['file'] as File; const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer); // ... 解析逻辑(同 NestJS students.controller.ts:228-270) return c.json(result); }); ``` - [ ] **Step 4: Commit** ```bash git add apps/server/src/routes/students.ts git commit -m "feat: implement student Excel import/export and template download" ``` --- ### Task 12: 宿舍管理路由 **Files:** - Create: `apps/server/src/routes/rooms.ts` - Modify: `apps/server/src/index.ts` - [ ] **Step 1: 实现宿舍 CRUD** 基于 `rooms.controller.ts` / `rooms.service.ts`,转换为 Hono + Drizzle。端点: - `GET /api/rooms` — 列表(支持 ?building=, ?status=, ?roomType=) - `GET /api/rooms/:id` — 详情 - `POST /api/rooms` — 创建 - `PUT /api/rooms/:id` — 更新 - `DELETE /api/rooms/:id` — 删除(检查无活跃入住记录) 约 150 行代码。 - [ ] **Step 2: Commit** ```bash git add apps/server/src/routes/rooms.ts apps/server/src/index.ts git commit -m "feat: implement room CRUD routes" ``` --- ### Task 13: 入住管理路由 **Files:** - Create: `apps/server/src/routes/occupancies.ts` - Modify: `apps/server/src/index.ts` - [ ] **Step 1: 实现入住管理 CRUD + Excel 导入** 端点: - `GET /api/occupancies` — 列表(含 student/room join 数据) - `POST /api/occupancies` — 办理入住 - `PUT /api/occupancies/:id` — 更新(调换宿舍) - `DELETE /api/occupancies/:id` — 退宿(设置 checkOutDate + status='inactive') - `POST /api/occupancies/import` — Excel 导入 约 180 行代码。 - [ ] **Step 2: Commit** ```bash git add apps/server/src/routes/occupancies.ts apps/server/src/index.ts git commit -m "feat: implement occupancy management routes with Excel import" ``` --- ### Task 14: 费用管理路由 **Files:** - Create: `apps/server/src/routes/expenses.ts` - Modify: `apps/server/src/index.ts` - [ ] **Step 1: 实现宿舍费用 + 个人费用** 端点(统一在 `/api/expenses` 下): - `GET /api/expenses/room` — 宿舍费用列表(含 room join) - `POST /api/expenses/room` — 录入宿舍费用 - `PUT /api/expenses/room/:id` — 编辑 - `DELETE /api/expenses/room/:id` — 删除 - `POST /api/expenses/room/import` — Excel 导入 - `GET /api/expenses/personal` — 个人费用列表(含 student join) - `POST /api/expenses/personal` — 录入 - `PUT /api/expenses/personal/:id` — 编辑 - `DELETE /api/expenses/personal/:id` — 删除 - `POST /api/expenses/personal/import` — Excel 导入 约 250 行代码。 - [ ] **Step 2: Commit** ```bash git add apps/server/src/routes/expenses.ts apps/server/src/index.ts git commit -m "feat: implement room and personal expense management routes" ``` --- ### Task 15: 账单管理路由 **Files:** - Create: `apps/server/src/routes/bills.ts` - Modify: `apps/server/src/index.ts` - [ ] **Step 1: 实现账单 CRUD + 生成 + Excel/PDF 导出** 端点: - `GET /api/bills` — 列表(含 student join,?status=, ?studentId=) - `POST /api/bills` — 生成账单(自动汇总 student 的活跃费用 → bill_items) - `PUT /api/bills/:id` — 更新 - `DELETE /api/bills/:id` — 删除 - `PUT /api/bills/:id/confirm` — 确认账单 - `GET /api/bills/export-excel` — Excel 导出 - `GET /api/bills/:id/pdf` — PDF 导出(PDFKit) PDF 生成使用 `c.body(buffer, 200, { 'Content-Type': 'application/pdf' })`。 约 300 行代码。 - [ ] **Step 2: Commit** ```bash git add apps/server/src/routes/bills.ts apps/server/src/index.ts git commit -m "feat: implement bill management routes with Excel/PDF export" ``` --- ### Task 16: 押金管理路由 **Files:** - Create: `apps/server/src/routes/deposits.ts` - Modify: `apps/server/src/index.ts` - [ ] **Step 1: 实现押金 CRUD** 端点:`GET/POST/PUT/DELETE /api/deposits`。约 100 行代码。 - [ ] **Step 2: Commit** ```bash git add apps/server/src/routes/deposits.ts apps/server/src/index.ts git commit -m "feat: implement deposit CRUD routes" ``` --- ### Task 17: 教室与租赁方管理路由 **Files:** - Create: `apps/server/src/routes/classrooms.ts` - Create: `apps/server/src/routes/tenants.ts` - Create: `apps/server/src/routes/classroom-rentals.ts` - Modify: `apps/server/src/index.ts` - [ ] **Step 1: 教室 CRUD**(含 Excel 导入,约 150 行) - [ ] **Step 2: 租赁方 CRUD**(约 80 行) - [ ] **Step 3: 租赁订单 CRUD**(约 150 行) 合并为一个 commit。 - [ ] **Step 4: Commit** ```bash git add apps/server/src/routes/classrooms.ts apps/server/src/routes/tenants.ts \ apps/server/src/routes/classroom-rentals.ts apps/server/src/index.ts git commit -m "feat: implement classroom, tenant, and classroom rental management routes" ``` --- ### Task 18: 操作日志与数据面板路由 **Files:** - Create: `apps/server/src/routes/operation-logs.ts` - Create: `apps/server/src/routes/dashboard.ts` - Modify: `apps/server/src/index.ts` - [ ] **Step 1: 操作日志查询路由** 端点: - `GET /api/operation-logs` — 分页列表(?page=, ?limit=, ?module=, ?action=, ?username=) - `GET /api/operation-logs/:id` — 详情 约 80 行代码。 - [ ] **Step 2: 数据面板路由** 端点:`GET /api/dashboard` — 聚合统计(学生数、宿舍占用率、费用总额、账单状态分布)。按现有 `dashboard.service.ts` 逻辑转换为 Drizzle 聚合查询。 约 60 行代码。 - [ ] **Step 3: Commit** ```bash git add apps/server/src/routes/operation-logs.ts apps/server/src/routes/dashboard.ts apps/server/src/index.ts git commit -m "feat: implement operation log query and dashboard aggregation routes" ``` --- ### Task 19: 操作日志记录中间件 **Files:** - Create: `apps/server/src/middleware/operation-log.ts` - Modify: 所有路由文件(集成日志记录调用) - [ ] **Step 1: 实现日志记录工具函数** ```typescript // apps/server/src/middleware/operation-log.ts import { db } from '../db'; import * as schema from '../db/schema'; interface LogParams { userId?: number; username?: string; module: string; action: string; targetId?: number; targetType?: string; detail?: string; ipAddress?: string; userAgent?: string; status?: 'success' | 'fail'; } export async function logOperation( c: any, // Hono Context params: Omit ) { const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'; const ua = (c.req.header('user-agent') || '').substring(0, 500); await db.insert(schema.operationLogs).values({ userId: params.userId, username: params.username, module: params.module, action: params.action, targetId: params.targetId, targetType: params.targetType, detail: params.detail, ipAddress: ip, userAgent: ua, status: params.status || 'success', }); } ``` - [ ] **Step 2: 在所有写操作路由中集成日志记录** 登录、学生创建/编辑/删除、宿舍创建/编辑/删除等所有写操作调用 `logOperation()`。 - [ ] **Step 3: Commit** ```bash git add apps/server/src/middleware/operation-log.ts git commit -m "feat: implement operation log middleware with automatic IP/UA capture" ``` --- ### Task 20: 限流 + CORS + 全局错误处理中间件 **Files:** - Create: `apps/server/src/middleware/rate-limit.ts` - Create: `apps/server/src/middleware/error-handler.ts` - Modify: `apps/server/src/index.ts`(挂载中间件栈) - [ ] **Step 1: Rate Limiter 配置** ```typescript // apps/server/src/middleware/rate-limit.ts import { rateLimiter } from 'hono-rate-limiter'; export const rateLimitMiddleware = rateLimiter({ windowMs: 60 * 1000, // 1 分钟 limit: 100, standardHeaders: true, legacyHeaders: false, keyGenerator: (c) => c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown', }); ``` - [ ] **Step 2: CORS 配置** ```typescript // 在 apps/server/src/index.ts 中: import { cors } from 'hono/cors'; app.use('*', cors({ origin: '*', credentials: true })); ``` - [ ] **Step 3: 全局错误处理** ```typescript // apps/server/src/middleware/error-handler.ts import { createMiddleware } from 'hono/factory'; export const errorHandler = createMiddleware(async (c, next) => { try { await next(); } catch (err: any) { console.error('[Error]', err.message || err); return c.json({ message: err.message || '服务器内部错误', statusCode: err.status || 500, }, err.status || 500); } }); ``` - [ ] **Step 4: 组装中间件栈** ```typescript // apps/server/src/index.ts 最终中间件顺序: app.use('*', errorHandler); app.use('*', cors({ origin: '*', credentials: true })); app.use('/api/*', rateLimitMiddleware); app.use('/api/*', authMiddleware); ``` - [ ] **Step 5: Commit** ```bash git add apps/server/src/middleware/ src/index.ts git commit -m "feat: add CORS, rate limiter, and global error handler middleware" ``` --- ### Task 21: Zod 校验集成 **Files:** - Modify: 所有路由文件(为 POST/PUT 端点添加 Zod schema 校验) - Create: `apps/server/src/utils/validate.ts` - [ ] **Step 1: 创建通用校验工具** ```typescript // apps/server/src/utils/validate.ts import { z } from 'zod'; import { zValidator } from '@hono/zod-validator'; export { z, zValidator }; ``` - [ ] **Step 2: 为主要 DTO 定义 Zod schema** 为 student、room、occupancy、expense、bill、user、role 的 POST/PUT body 添加 Zod 校验。示例: ```typescript // 在 students.ts 中: import { zValidator, z } from '../utils/validate'; const createStudentSchema = z.object({ name: z.string().min(1).max(50), phone: z.string().max(20).optional(), idNumber: z.string().max(30).optional(), gender: z.string().max(10).optional(), // ... }); studentsRoute.post('/', requirePermission('student:create'), zValidator('json', createStudentSchema), async (c) => { const dto = c.req.valid('json'); // ... }); ``` - [ ] **Step 3: Commit** ```bash git add apps/server/src/utils/validate.ts apps/server/src/routes/ git commit -m "feat: integrate Zod validation for all POST/PUT endpoints" ``` --- ### Task 22: 部署配置更新 **Files:** - Modify: `apps/server/Dockerfile` - Modify: `docker-compose.yml` - Modify: `apps/server/.env.example` - Create: `apps/server/.env` - [ ] **Step 1: 更新 Dockerfile** 移除 NestJS 构建依赖,适配 HonoJS: ```dockerfile FROM node:20-alpine AS builder WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN corepack enable && pnpm install --frozen-lockfile COPY . . RUN pnpm build FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./ EXPOSE 3003 CMD ["node", "dist/index.js"] ``` - [ ] **Step 2: 更新 docker-compose.yml** - 移除 `mysql` 服务和 `mysql_data` volume - 后端依赖改为 external PostgreSQL(通过环境变量连接) - 更新 `backend` 服务的环境变量列表(移除 DB_TYPE,添加 DB_HOST/DB_PORT/DB_USERNAME/DB_PASSWORD/DB_DATABASE/BETTER_AUTH_SECRET) - [ ] **Step 3: 更新 .env 示例** ``` PORT=3003 NODE_ENV=development DB_HOST=localhost DB_PORT=5432 DB_USERNAME=postgres DB_PASSWORD=postgres DB_DATABASE=gongxue BETTER_AUTH_SECRET=your-secret-key-change-in-production ADMIN_PASSWORD=admin123 ``` - [ ] **Step 4: Commit** ```bash git add apps/server/Dockerfile docker-compose.yml apps/server/.env.example apps/server/.env git commit -m "chore: update Dockerfile, docker-compose, and env config for HonoJS + PostgreSQL" ``` --- ### Task 23: 清理 NestJS 残留 + 最终验证 **Files:** - Delete: 所有 NestJS 特有文件和目录(见下方列表) - Modify: `apps/server/package.json`(清理残留 dep) - [ ] **Step 1: 删除 NestJS 残留文件** ```bash cd apps/server rm -rf src/app.module.ts src/app.controller.ts src/app.service.ts src/app.controller.spec.ts rm -rf src/auth/strategies/ src/auth/guards/ src/auth/decorators/ src/auth/auth.module.ts rm -rf src/entities/ rm -rf src/students/students.module.ts src/rooms/rooms.module.ts rm -rf src/occupancies/occupancies.module.ts src/expenses/expenses.module.ts rm -rf src/bills/bills.module.ts src/deposits/deposits.module.ts rm -rf src/classrooms/classrooms.module.ts src/classroom-rentals/classroom-rentals.module.ts rm -rf src/tenants/tenants.module.ts src/operation-logs/operation-logs.module.ts rm -rf src/dashboard/dashboard.module.ts src/rbac/rbac.module.ts rm -rf src/common/ src/global.d.ts rm -rf nest-cli.json test/ ``` - [ ] **Step 2: 验证 pnpm remove 清理所有 NestJS 依赖** ```bash cd apps/server pnpm remove @nestjs/common @nestjs/core @nestjs/config @nestjs/jwt @nestjs/passport \ @nestjs/platform-express @nestjs/throttler @nestjs/typeorm @nestjs/cli \ @nestjs/schematics @nestjs/testing typeorm mysql2 better-sqlite3 \ passport passport-jwt passport-local class-transformer class-validator \ reflect-metadata rxjs multer @types/multer @types/better-sqlite3 @types/express \ @types/passport-jwt @types/passport-local 2>/dev/null || true ``` - [ ] **Step 3: 构建验证** ```bash cd apps/server && pnpm typecheck && pnpm build # 预期:无类型错误,dist/index.js 生成成功 ``` - [ ] **Step 4: 启动验证** ```bash cd apps/server && pnpm dev # curl http://localhost:3003/api/health → 200 ``` - [ ] **Step 5: Commit** ```bash git add -A apps/server/ git commit -m "chore: remove all NestJS residual files and dependencies, finalize HonoJS migration" ``` --- ### Task 24: 端到端兼容性验证清单 **检查项(手动 + 自动化)**: - [ ] 24.1 所有 60+ API 端点可正常访问(对照 NestJS 端点列表逐一验证) - [ ] 24.2 登录 → JWT token → 受保护路由 全流程可用 - [ ] 24.3 49 个权限点检查正常(admin/supervisor/teacher/institution 权限隔离) - [ ] 24.4 Excel 导入导出 round-trip(导出 → 修改 → 导入 → 验证) - [ ] 24.5 PDF 账单生成可用 - [ ] 24.6 操作日志记录完整(登录、CRUD 操作均有日志) - [ ] 24.7 Rate limiter 生效(100 req/min) - [ ] 24.8 PGlite 开发环境零 Docker 启动 - [ ] 24.9 前端 23 个页面功能回归正常(无 API 兼容性错误) 此 task 检查项确认后勾选。 --- ## 执行顺序与依赖图 ``` Task 1 ─────────────────────────────────────────────────────────────┐ ↓ │ Task 2 (auth schema) ──→ Task 3 (biz schema) ──→ Task 4 (other + db) │ ↓ ↓ ↓ │ Task 5 (better-auth) ←────┴───────────────────────┘ │ ↓ │ Task 6 (auth middleware) │ ↓ │ Task 7 (login) │ ↓ │ Task 8 (users CRUD) ──→ Task 9 (roles + permissions) │ ↓ ↓ │ Task 10 (students) ←──────┴─────────────────────────────────────┐ │ ↓ │ │ Task 11 (student Excel) │ │ ↓ │ │ Task 12 (rooms) ──→ Task 13 (occupancies) ──→ Task 14 (expenses) │ │ ↓ ↓ │ │ └──────────→ Task 15 (bills) │ │ ↓ │ │ Task 16 (deposits) ←─────────────────┘ │ │ ↓ │ │ Task 17 (classroom + tenant + rental) │ │ ↓ │ │ Task 18 (logs + dashboard) ←─────────────────────────────────────┘ │ ↓ │ Task 19 (log middleware) ←── 可与其他路由同时进行 │ ↓ │ Task 20 (rate-limit + CORS + error) │ ↓ │ Task 21 (Zod validation) │ ↓ │ Task 22 (deploy config) ──→ Task 23 (cleanup) ──→ Task 24 (verify) │ ↑ │ └── 可与 Task 10-21 并行 ←─────────────────────────────────────────┘ ``` ## 分批实施建议 | 批次 | Tasks | 预计工作量 | 关键产出 | |------|-------|-----------|---------| | 批 1 (基础) | 1-6 | 全局基础 | Hono + Drizzle + better-auth 可启动 | | 批 2 (认证) | 7-9 | 认证 + RBAC | 登录/用户/角色/权限全功能 | | 批 3 (核心业务) | 10-15 | 学生/宿舍/费用/账单 | 核心模块可用 | | 批 4 (其他业务) | 16-18 | 押金/教室/日志 | 全模块完成 | | 批 5 (横切) | 19-21 | 中间件 | 日志/限流/校验 | | 批 6 (收尾) | 22-24 | 部署+清理+验证 | 完成迁移 |