From b4c1d11cf235f0d58dafb0c5784070fc6daffc67 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Fri, 3 Jul 2026 15:05:54 +0800 Subject: [PATCH] chore: remove abandoned betterauth-hono-drizzle-migration change (0/43 tasks) --- ...07-03-betterauth-hono-drizzle-migration.md | 1693 ----------------- 1 file changed, 1693 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-03-betterauth-hono-drizzle-migration.md diff --git a/docs/superpowers/plans/2026-07-03-betterauth-hono-drizzle-migration.md b/docs/superpowers/plans/2026-07-03-betterauth-hono-drizzle-migration.md deleted file mode 100644 index 94972e7..0000000 --- a/docs/superpowers/plans/2026-07-03-betterauth-hono-drizzle-migration.md +++ /dev/null @@ -1,1693 +0,0 @@ ---- -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 | 部署+清理+验证 | 完成迁移 |