# Dashboard 后端迁移计划:JS + Sequelize → TypeScript + Drizzle ## 概述 将 dashboard 后端从 JavaScript + Sequelize(CommonJS)迁移至 TypeScript + Drizzle ORM(ESM),提升类型安全性和开发体验,同时为官网 CMS 模块(`/api/www/*`)奠定技术基础。 **迁移策略**:渐进式,逐模块替换,确保每个阶段可独立验证。 --- ## 现有代码清单 ### Models(9 个表) | 文件 | 表名 | 说明 | |------|------|------| | Brand.js | brand | 耳机品牌 | | Model.js | model | 耳机型号 | | Ota.js | ota | OTA 固件 | | OtaTargetDevice.js | ota_target_device | OTA 目标设备 | | BlackList.js | black_list | 黑名单 | | DashboardUser.js | dashboard_user | 后台用户 | | ShareCodeLog.js | share_code_log | 分享码日志 | | UserActive.js | user_active | 用户活跃记录 | | UserDevice.js | user_device | 用户设备 | ### Routes(9 个路由模块) | 文件 | 体量 | 复杂度 | 迁移优先级 | |------|------|--------|-----------| | auth.js | 3.6KB | ⭐ | P1(先迁) | | shareCodeLogs.js | 2.7KB | ⭐ | P1 | | brands.js | 4.8KB | ⭐ | P1 | | blacklist.js | 6.6KB | ⭐⭐ | P2 | | users.js | 6.2KB | ⭐⭐ | P2 | | otaTargetDevice.js | 7.8KB | ⭐⭐ | P2 | | dashboard.js | 9.5KB | ⭐⭐ | P2 | | ota.js | 9.9KB | ⭐⭐⭐ | P3 | | models.js | 26.1KB | ⭐⭐⭐ | P3(最后) | ### Services(6 个) | 文件 | 说明 | |------|------| | curveClient.js | 频响曲线 API 客户端 | | eqCacheStorage.js | Redis EQ 缓存 | | measurementStorage.js | S3 测量文件存储 | | otaStorage.js | OTA 固件存储 | | squiglink.js | Squiglink 数据服务 | | userBootstrap.js | 超级管理员初始化 | ### 其他 | 模块 | 文件 | 说明 | |------|------|------| | validators/ | 5 个 | 已使用 Zod,迁移成本极低 | | middleware/ | auth.js, bodyLimit.js | JWT 鉴权 + 请求体限制 | | config/ | database.js, env.js, loadEnv.js, logger.js, redis.js | 基础配置 | | utils/ | 3 个 | 工具函数 | --- ## 迁移阶段 ### Phase 1:搭建 TypeScript 基础设施 **目标**:项目能运行 TS,新旧代码可共存。 - [ ] 安装依赖:`typescript`、`tsx`、`drizzle-orm`、`drizzle-kit`、`@types/express`、`@types/cors` - [ ] 移除依赖:`sequelize`(Phase 4 最后移除) - [ ] 创建 `tsconfig.json` ```jsonc { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "esModuleInterop": true, "strict": true, "outDir": "dist", "rootDir": "src", "skipLibCheck": true, "resolveJsonModule": true, "declaration": true, "paths": { "@/*": ["./src/*"] } }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"] } ``` - [ ] 创建 `drizzle.config.ts` ```ts import { defineConfig } from 'drizzle-kit'; export default defineConfig({ schema: './src/schemas/index.ts', out: './drizzle', dialect: 'mysql', dbCredentials: { host: process.env.DATABASE_HOST || 'localhost', port: Number(process.env.DATABASE_PORT || 3306), user: process.env.DATABASE_USER || 'root', password: process.env.DATABASE_PASSWORD || '', database: process.env.DATABASE_NAME || 'audio', }, }); ``` - [ ] 更新 `package.json` scripts: ```json { "type": "module", "scripts": { "dev": "tsx watch src/app.ts", "build": "tsc", "start": "node dist/app.js", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio" } } ``` - [ ] 创建 `src/config/database.ts`(Drizzle 连接) ```ts import { drizzle } from 'drizzle-orm/mysql2'; import mysql from 'mysql2/promise'; import * as schema from '../schemas'; const pool = mysql.createPool({ host: process.env.DATABASE_HOST || 'localhost', port: Number(process.env.DATABASE_PORT || 3306), user: process.env.DATABASE_USER || 'root', password: process.env.DATABASE_PASSWORD || '', database: process.env.DATABASE_NAME || 'audio', charset: 'utf8mb4', waitForConnections: true, connectionLimit: 10, }); export const db = drizzle(pool, { schema, mode: 'default' }); ``` **验证**:`tsx src/app.ts` 能正常启动(此时仍用旧 JS 代码)。 --- ### Phase 2:迁移 Models → Drizzle Schema **目标**:所有表定义用 Drizzle schema 表达,生成初始迁移。 - [ ] 创建 `src/schemas/` 目录 - [ ] 逐表编写 Drizzle schema: ```ts // src/schemas/brand.ts import { mysqlTable, int, varchar, datetime } from 'drizzle-orm/mysql-core'; import { sql } from 'drizzle-orm'; export const brands = mysqlTable('brand', { id: int('id').primaryKey().autoincrement(), name: varchar('name', { length: 100 }).notNull(), // ... }); ``` - [ ] 创建 `src/schemas/index.ts` 统一导出 - [ ] 运行 `drizzle-kit generate` 生成初始迁移(与现有表结构对齐) - [ ] 验证:`drizzle-kit studio` 能正确读取现有数据 **注意**:现有表已存在,首次迁移使用 `drizzle-kit push`(同步 schema 到数据库而不执行 SQL),或生成迁移后标记为已执行。 --- ### Phase 3:逐个迁移 Routes **目标**:将路由处理函数中的 Sequelize 查询替换为 Drizzle。 **迁移顺序**(从简到繁): | 批次 | 路由 | 原因 | |------|------|------| | 第 1 批 | auth.ts, shareCodeLogs.ts, brands.ts | 逻辑简单,CRUD 为主 | | 第 2 批 | blacklist.ts, users.ts, otaTargetDevice.ts, dashboard.ts | 中等复杂度 | | 第 3 批 | ota.ts, models.ts | 最复杂,涉及文件上传/外部服务 | **每个路由的迁移步骤**: 1. 重命名 `.js` → `.ts` 2. `require()` → `import` 3. Sequelize 查询 → Drizzle 查询: ```ts // Before (Sequelize) const brands = await Brand.findAll({ where: { name: { [Op.like]: `%${keyword}%` } } }); // After (Drizzle) const result = await db.select().from(brands).where(like(brands.name, `%${keyword}%`)); ``` 4. 添加请求/响应类型(配合 Zod validator) 5. 验证接口行为不变 --- ### Phase 4:迁移 Services + 清理 **目标**:全部代码 TS 化,移除 Sequelize。 - [ ] 迁移 6 个 service 文件(主要是加类型标注,逻辑不变) - [ ] 迁移 middleware(auth.ts, bodyLimit.ts) - [ ] 迁移 config(env.ts, logger.ts, redis.ts) - [ ] 迁移 utils - [ ] 迁移入口 `app.js` → `app.ts` - [ ] 删除所有旧 `.js` 文件 - [ ] 卸载 `sequelize` 依赖 - [ ] 全量 `tsc --noEmit` 通过 - [ ] 更新 Dockerfile(构建步骤改为 `tsc && node dist/app.js`) --- ### Phase 5:新增官网 CMS 模块 **目标**:在迁移完成的基础上,开发 `/api/www/*` 接口。 - [ ] 设计 CMS 数据表(pages、sections、banners、products、news、faq、site_settings 等) - [ ] 编写 Drizzle schema + 迁移 - [ ] 实现前台接口 `/api/www/*`(公开) - [ ] 实现管理接口 `/api/admin/www/*`(JWT 鉴权) - [ ] dashboard 前端新增「官网管理」菜单 --- ## 风险与注意事项 | 风险 | 应对 | |------|------| | 迁移期间线上服务中断 | 渐进式迁移,新旧代码共存,逐模块切换 | | Sequelize 隐式行为(如自动时间戳) | 现有配置已关闭 timestamps,影响小 | | `sequelize.sync()` 自动建表 → 手动迁移 | Phase 2 完成后用 drizzle-kit 管理,不再自动 sync | | CommonJS → ESM 兼容问题 | 部分依赖(如 multer)可能需要 `esModuleInterop` 或动态 import | | models.js 路由 26KB 过大 | 迁移时拆分为 models + measurements 两个路由文件 | --- ## 预计工时 | 阶段 | 预计时间 | |------|----------| | Phase 1:TS 基础设施 | 0.5 天 | | Phase 2:Schema 迁移 | 0.5 天 | | Phase 3:Routes 迁移 | 1~1.5 天 | | Phase 4:Services + 清理 | 0.5 天 | | Phase 5:CMS 模块 | 另计(属于新功能开发) | | **合计(不含 Phase 5)** | **2.5~3 天** |