57 KiB
change, design-doc, base-ref
| change | design-doc | base-ref |
|---|---|---|
| betterauth-hono-drizzle-migration | docs/superpowers/specs/2026-07-03-betterauth-hono-drizzle-migration-design.md | 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
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:
{
"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
{
"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 特有配置):
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"typecheck": {
"dependsOn": ["^build"]
}
}
}
- Step 4: 创建最小 Hono 入口验证项目可启动
创建 apps/server/src/index.ts:
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: 验证启动
cd apps/server && pnpm dev
# 预期:Server running on http://localhost:3003
# curl http://localhost:3003/api/health → {"status":"ok","timestamp":"..."}
- Step 6: Commit
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_permissions5 张 Drizzle 表定义 -
Consumes: Task 1 (项目结构)
-
Step 1: 创建 drizzle.config.ts
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 表)
// 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 导出
// apps/server/src/db/schema/index.ts
export * from './auth';
- Step 4: Commit
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_items7 张表 -
Consumes: Task 2 (schema barrel)
-
Step 1: students 表
// 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 表
// 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 表
// 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)
// 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)
// 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
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_logs5 张表 + DB 连接工厂 + seed 脚本 -
Consumes: Tasks 2-3 (schema 定义)
-
Step 1: 剩余 5 张业务表定义
// 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: 数据库连接工厂
// 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 实现幂等。
核心流程:
- 插入 49 权限码(
onConflictDoNothing,按 code 唯一键) - 插入 4 预设角色(
onConflictDoNothing,按 name 唯一键) - 查询所有权限和角色,构建角色-权限关联
- 按 group 匹配权限给各角色(超管=全部,宿管=8组,老师=student:view,机构=3组)
- 初始化 admin 用户(若 users 表为空),密码 bcrypt hash,分配超管角色
完整代码参考现有 rbac.service.ts:120-191 的 seedData 逻辑。
- Step 4: Commit
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: 环境变量工具
// 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 实例配置
// 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 初始化无错误
cd apps/server && pnpm typecheck
- Step 4: Commit
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 认证中间件
// 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: 权限检查中间件
// 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 挂载中间件
// apps/server/src/index.ts 增加:
import { authMiddleware } from './middleware/auth';
app.use('/api/*', authMiddleware);
- Step 4: Commit
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: 实现认证路由
// 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<string>();
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
// 在 apps/server/src/index.ts 中添加:
import { authRoute } from './routes/auth';
app.route('/api/auth', authRoute);
- Step 3: Commit
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 查询。
// 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
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:
// 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: 权限查询路由
// 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<string, typeof perms>();
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
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
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() 方式:
// 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: 模板下载
// 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 导入
// 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
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
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
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
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
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
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
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
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: 实现日志记录工具函数
// 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<LogParams, 'ipAddress' | 'userAgent'>
) {
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
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 配置
// 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 配置
// 在 apps/server/src/index.ts 中:
import { cors } from 'hono/cors';
app.use('*', cors({ origin: '*', credentials: true }));
- Step 3: 全局错误处理
// 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: 组装中间件栈
// 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
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: 创建通用校验工具
// 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 校验。示例:
// 在 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
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:
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_datavolume -
后端依赖改为 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
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 残留文件
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 依赖
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: 构建验证
cd apps/server && pnpm typecheck && pnpm build
# 预期:无类型错误,dist/index.js 生成成功
- Step 4: 启动验证
cd apps/server && pnpm dev
# curl http://localhost:3003/api/health → 200
- Step 5: Commit
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 | 部署+清理+验证 | 完成迁移 |