diff --git a/.cursor/rules/create-prd.mdc b/.cursor/rules/create-prd.mdc index 957cd02..48baaa5 100644 --- a/.cursor/rules/create-prd.mdc +++ b/.cursor/rules/create-prd.mdc @@ -1,5 +1,5 @@ --- -description: "全局规则" +description: "生成产品需求文档 (PRD)" globs: ["**/*"] alwaysApply: true --- diff --git a/.cursor/rules/create-task.mdc b/.cursor/rules/create-task.mdc index 20f65bb..0369c5b 100644 --- a/.cursor/rules/create-task.mdc +++ b/.cursor/rules/create-task.mdc @@ -1,5 +1,5 @@ --- -description: "全局规则" +description: "从PRD生成任务清单" globs: ["**/*"] alwaysApply: true --- diff --git a/.cursor/rules/elysia-rules.mdc b/.cursor/rules/elysia-rules.mdc index 3c7ff5f..31aa5fa 100644 --- a/.cursor/rules/elysia-rules.mdc +++ b/.cursor/rules/elysia-rules.mdc @@ -1,3 +1,9 @@ +--- +description: "ElysiaAPI开发流程" +globs: ["**/*"] +alwaysApply: true +--- + # ElysiaAPI开发流程 ## 0. 概览 diff --git a/.cursor/rules/task-execution.mdc b/.cursor/rules/task-execution.mdc index cddfc1b..289dce9 100644 --- a/.cursor/rules/task-execution.mdc +++ b/.cursor/rules/task-execution.mdc @@ -1,5 +1,5 @@ --- -description: "全局规则" +description: "任务执行流程" globs: ["**/*"] alwaysApply: true --- @@ -44,3 +44,41 @@ alwaysApply: true 8. 完成一个子任务后,提交git 9. 将已完成的任务归档到tasks的归档目录archive并修改文件名,文件名前添加时间 10. 将需求prd也归档 + + + +重要的事情说三遍 + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** + +**每一个子任务执行完暂停,等待我确认,不要再连续执行接下来的任务** diff --git a/.env b/.env new file mode 100644 index 0000000..893da6f --- /dev/null +++ b/.env @@ -0,0 +1,75 @@ +# ============================================== +# 项目环境变量配置文件 +# ============================================== + +# ============================================== +# 应用配置 +# ============================================== +NODE_ENV=development + +# ============================================== +# MySQL数据库配置 +# ============================================== +# 基本连接配置 +DB_HOST=uair.cc +DB_PORT=3306 +DB_USER=nie +DB_PASSWORD=nie +DB_NAME=nie + +# 连接池配置 +DB_CONNECTION_LIMIT=10 +DB_QUEUE_LIMIT=0 +DB_ACQUIRE_TIMEOUT=60000 +DB_TIMEOUT=60000 + +# ============================================== +# Redis配置 +# ============================================== +REDIS_CONNECT_NAME=cursor-init-redis +REDIS_HOST=uair.cc +REDIS_PORT=6379 +REDIS_USERNAME=default +REDIS_PASSWORD=nie +REDIS_DATABASE=0 + +# ============================================== +# JWT认证配置 +# ============================================== +JWT_SECRET=your_jwt_secret_change_this_in_production + +# ============================================== +# 日志配置 +# ============================================== +LOG_LEVEL=debug +LOG_MAX_FILES=30d +LOG_MAX_SIZE=70k +LOG_DIRECTORY=logs +LOG_CONSOLE=true + +# ==================== +# 邮件服务配置 +# ==================== + +# SMTP服务器配置 + +SMTP_USER=togy.gc@qq.com +SMTP_PASS=qyafspjzahydddaa +SMTP_FROM_EMAIL=togy.gc@qq.com # 必须与SMTP_USER一致! +SMTP_FROM_NAME=星撰玉衡 + +# SMTP连接超时配置(毫秒) +SMTP_TIMEOUT=60000 +SMTP_GREETING_TIMEOUT=30000 +SMTP_SOCKET_TIMEOUT=60000 + +# 邮件基础配置 +EMAIL_FROM=星撰玉衡 +EMAIL_REPLY_TO=togy.gc@qq.com + +# 邮件发送选项配置 +EMAIL_RETRY_ATTEMPTS=3 +EMAIL_RETRY_DELAY=5000 +EMAIL_ENABLE_QUEUE=false +EMAIL_MAX_RECIPIENTS=50 +EMAIL_RATE_LIMIT=100 diff --git a/.gitignore b/.gitignore index 15f8976..37707c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules/ dist/ -.env bun.lockb /logs \ No newline at end of file diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index 3c7ff5f..264b41f 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -119,6 +119,7 @@ src/plugins/ ``` ### 1.4 类型定义 (type/) + ``` src/type/ ├── config.type.ts # 配置相关类型 @@ -131,6 +132,7 @@ src/type/ ``` ### 1.5 工具函数 (utils/) + ``` src/utils/ ├── deviceInfo.ts # 设备信息工具 @@ -148,11 +150,13 @@ src/utils/ ``` ### 1.6 常量定义 (constants/) + ``` src/constants/ ├── swaggerTags.ts # Swagger标签定义:所有模块的tag应该集中在此定义 └── 其他常量 ``` + ### 1.7 数据库实体 ``` @@ -163,6 +167,7 @@ src/eneities/ ``` ### 1.8 文件命名约定 + - 模块名使用 **单数形式**:`auth`、`user`、`product`、`order` - 文件名格式:`[module].[type].ts` - 导出名格式:`[module][类型名]` @@ -182,11 +187,12 @@ src/eneities/ - 注意已经存在的工具函数,比如分页有现成的简化工具,且能够统一请求格式 ### 2.2 特别注意点 + **必须遵循的命名模式:** + - Request类型:`[动作][模块]Request` → `RegisterRequest` - Schema名:`[动作][模块]Schema` → `RegisterSchema` - ### 2.3 代码示例 ```ts @@ -206,38 +212,37 @@ import { createQuerySchema } from '@/utils/pagination'; * 用户列表查询参数Schema * @description 用户列表查询的请求参数验证规则 */ -export const UserListQuerySchema = createQuerySchema(t.Object({ - // 用户特有参数 - keyword: t.Optional(t.String({ - minLength: 1, - maxLength: 100, - description: '搜索关键词,支持用户名、邮箱模糊搜索', - examples: ['admin', 'test@example.com'] - })), - status: t.Optional(t.Union([ - t.Literal('active'), - t.Literal('inactive'), - t.Literal('pending') - ], { - description: '用户状态筛选', - examples: ['active', 'inactive', 'pending'] - })), - gender: t.Optional(t.Union([ - t.Literal(0), - t.Literal(1), - t.Literal(2), - t.Literal('0'), - t.Literal('1'), - t.Literal('2'), - ], { - description: '性别筛选:0-未知,1-男,2-女', - examples: [0, 1, 2] - })), - isRoot: t.Optional(t.Boolean({ - description: '是否超级管理员筛选', - examples: [true, false] - })) -})); +export const UserListQuerySchema = createQuerySchema( + t.Object({ + // 用户特有参数 + keyword: t.Optional( + t.String({ + minLength: 1, + maxLength: 100, + description: '搜索关键词,支持用户名、邮箱模糊搜索', + examples: ['admin', 'test@example.com'], + }), + ), + status: t.Optional( + t.Union([t.Literal('active'), t.Literal('inactive'), t.Literal('pending')], { + description: '用户状态筛选', + examples: ['active', 'inactive', 'pending'], + }), + ), + gender: t.Optional( + t.Union([t.Literal(0), t.Literal(1), t.Literal(2), t.Literal('0'), t.Literal('1'), t.Literal('2')], { + description: '性别筛选:0-未知,1-男,2-女', + examples: [0, 1, 2], + }), + ), + isRoot: t.Optional( + t.Boolean({ + description: '是否超级管理员筛选', + examples: [true, false], + }), + ), + }), +); /** 用户列表查询参数类型 */ export type UserListQueryRequest = Static; @@ -251,9 +256,10 @@ export type UserListQueryRequest = Static; - 错误相应只提供示例和描述 - 导出响应成功类型 - ### 3.2 特别注意点 + **必须遵循的命名模式:** + - Response格式定义:`[动作][模块]Response` → `RegisterResponse` - Response成功类型:`[动作][模块]SuccessType ` → `RegisterSuccessType ` @@ -273,8 +279,6 @@ import { t, type Static } from 'elysia'; import { responseWrapperSchema } from '@/utils/responseFormate'; import { createPaginationResponseSchema } from '@/utils/pagination'; - - /** * 用户列表项Schema * @description 用户列表中单个用户的数据结构 @@ -283,21 +287,21 @@ export const UserListItemSchema = t.Object({ /** 用户ID */ id: t.String({ description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', - examples: ['1', '2', '3'] + examples: ['1', '2', '3'], }), /** 用户名 */ username: t.String({ description: '用户名', - examples: ['admin', 'testuser'] + examples: ['admin', 'testuser'], }), - + /** 更多字段... */ /** 更新时间 */ updatedAt: t.String({ description: '更新时间', - examples: ['2024-12-19T10:30:00Z'] - }) + examples: ['2024-12-19T10:30:00Z'], + }), }); /** @@ -306,28 +310,34 @@ export const UserListItemSchema = t.Object({ */ export const GetUserListResponsesSchema = { 200: responseWrapperSchema(createPaginationResponseSchema(UserListItemSchema)), - 401: responseWrapperSchema(t.Object({ - error: t.String({ - description: '认证失败', - examples: ['未提供有效的认证令牌', '令牌已过期'] - }) - })), - 400: responseWrapperSchema(t.Object({ - error: t.String({ - description: '参数错误', - examples: ['分页参数无效', '搜索关键词格式错误'] - }) - })), - 500: responseWrapperSchema(t.Object({ - error: t.String({ - description: '服务器错误', - examples: ['内部服务器错误'] - }) - })) + 401: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '认证失败', + examples: ['未提供有效的认证令牌', '令牌已过期'], + }), + }), + ), + 400: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '参数错误', + examples: ['分页参数无效', '搜索关键词格式错误'], + }), + }), + ), + 500: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '服务器错误', + examples: ['内部服务器错误'], + }), + }), + ), }; /** 获取用户列表成功响应数据类型 */ -export type GetUserListSuccessType = Static; +export type GetUserListSuccessType = Static<(typeof GetUserListResponsesSchema)[200]>; ``` ## 4. Service(业务逻辑层) @@ -398,7 +408,7 @@ export class UserService { status: sysUsers.status, lastLoginAt: sysUsers.lastLoginAt, createdAt: sysUsers.createdAt, - updatedAt: sysUsers.updatedAt + updatedAt: sysUsers.updatedAt, }) .from(sysUsers) .where(eq(sysUsers.id, BigInt(userId))) @@ -406,34 +416,33 @@ export class UserService { if (!user || user.length === 0) { Logger.warn(`用户不存在:${userId}`); - throw new BusinessError( - `用户不存在:${userId}`, - 404 - ); + throw new BusinessError(`用户不存在:${userId}`, 404); } const userData = user[0]!; Logger.info(`获取用户信息成功:${userId} - ${userData.username}`); - return successResponse({ - id: userId, // 使用传入的字符串ID,避免精度丢失 - username: userData.username, - email: userData.email, - nickname: userData.nickname, - avatar: userData.avatar, - phone: userData.mobile, - status: userData.status, - lastLoginAt: userData.lastLoginAt || null, - createdAt: userData.createdAt, - updatedAt: userData.updatedAt - }, '获取用户信息成功'); + return successResponse( + { + id: userId, // 使用传入的字符串ID,避免精度丢失 + username: userData.username, + email: userData.email, + nickname: userData.nickname, + avatar: userData.avatar, + phone: userData.mobile, + status: userData.status, + lastLoginAt: userData.lastLoginAt || null, + createdAt: userData.createdAt, + updatedAt: userData.updatedAt, + }, + '获取用户信息成功', + ); } - } // 导出单例实例 -export const userService = new UserService(); +export const userService = new UserService(); ``` ## 5. Controllers(接口名称接入) @@ -463,21 +472,18 @@ export const authController = new Elysia() * @route POST /api/auth/register * @description 用户注册,包含验证码验证、用户名邮箱唯一性检查等 */ - .post( - '/register', - ({ body }) => authService.register(body), - { - body: RegisterSchema, - detail: { - summary: '用户注册', - description: '用户注册接口,需要提供用户名、邮箱、密码和验证码', - tags: [tags.auth], - operationId: 'registerUser', - }, - response: RegisterResponsesSchema, - } - ); + .post('/register', ({ body }) => authService.register(body), { + body: RegisterSchema, + detail: { + summary: '用户注册', + description: '用户注册接口,需要提供用户名、邮箱、密码和验证码', + tags: [tags.auth], + operationId: 'registerUser', + }, + response: RegisterResponsesSchema, + }); ``` + ## 6. 错误处理 ### 6.1 错误处理规范 @@ -494,10 +500,11 @@ import { BusinessError } from '@/utils/responseFormate'; // 抛出业务错误 throw new BusinessError('消息说明...', 409); ``` + ## 7. 测试用例文档 1. 分模块 2. 分接口 3. 测试名称 4. 场景 -5. 方法 \ No newline at end of file +5. 方法 diff --git a/aiChat/002-cursor_redis.md b/aiChat/002-cursor_redis.md index 35f9f8d..6d5f82d 100644 --- a/aiChat/002-cursor_redis.md +++ b/aiChat/002-cursor_redis.md @@ -1,11 +1,12 @@ # 创建redis连接测试 + _Exported on 2025/6/28 at GMT+8 22:07:57 from Cursor (1.1.6)_ --- **User** -不要修改我的redis.ts文件 给我写一个redis连接测试 +不要修改我的redis.ts文件 给我写一个redis连接测试 --- @@ -35,7 +36,7 @@ describe('Redis连接测试', () => { beforeAll(async () => { redis = new Redis(); // 等待连接建立 - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); }); afterAll(async () => { @@ -83,28 +84,28 @@ describe('Redis连接测试', () => { it('应该能够设置和获取字符串值', async () => { // 设置值 await redis.redisClient.set(testKey, testValue); - + // 获取值 const result = await redis.redisClient.get(testKey); expect(result).toBe(testValue); - + // 清理测试数据 await redis.redisClient.del(testKey); }); it('应该能够设置带过期时间的值', async () => { const expiryTime = 2; // 2秒过期 - + // 设置带过期时间的值 await redis.redisClient.setEx(testKey, expiryTime, testValue); - + // 立即获取应该有值 const result1 = await redis.redisClient.get(testKey); expect(result1).toBe(testValue); - + // 等待过期 - await new Promise(resolve => setTimeout(resolve, 2100)); - + await new Promise((resolve) => setTimeout(resolve, 2100)); + // 过期后应该为null const result2 = await redis.redisClient.get(testKey); expect(result2).toBeNull(); @@ -113,14 +114,14 @@ describe('Redis连接测试', () => { it('应该能够检查键是否存在', async () => { // 设置测试键 await redis.redisClient.set(testKey, testValue); - + // 检查存在 const exists1 = await redis.redisClient.exists(testKey); expect(exists1).toBe(1); - + // 删除键 await redis.redisClient.del(testKey); - + // 检查不存在 const exists2 = await redis.redisClient.exists(testKey); expect(exists2).toBe(0); @@ -129,11 +130,11 @@ describe('Redis连接测试', () => { it('应该能够删除键', async () => { // 设置测试键 await redis.redisClient.set(testKey, testValue); - + // 删除键 const deleteCount = await redis.redisClient.del(testKey); expect(deleteCount).toBe(1); - + // 验证键已被删除 const result = await redis.redisClient.get(testKey); expect(result).toBeNull(); @@ -150,11 +151,11 @@ describe('Redis连接测试', () => { it('应该能够设置和获取Hash字段', async () => { // 设置Hash字段 await redis.redisClient.hSet(hashKey, field1, value1); - + // 获取Hash字段 const result = await redis.redisClient.hGet(hashKey, field1); expect(result).toBe(value1); - + // 清理测试数据 await redis.redisClient.del(hashKey); }); @@ -165,14 +166,14 @@ describe('Redis连接测试', () => { [field1]: value1, [field2]: value2, }); - + // 获取所有Hash字段 const result = await redis.redisClient.hGetAll(hashKey); expect(result).toEqual({ [field1]: value1, [field2]: value2, }); - + // 清理测试数据 await redis.redisClient.del(hashKey); }); @@ -186,15 +187,15 @@ describe('Redis连接测试', () => { it('应该能够推入和弹出列表元素', async () => { // 推入元素 await redis.redisClient.lPush(listKey, value1, value2); - + // 获取列表长度 const length = await redis.redisClient.lLen(listKey); expect(length).toBe(2); - + // 弹出元素 const poppedValue = await redis.redisClient.lPop(listKey); expect(poppedValue).toBe(value2); - + // 清理测试数据 await redis.redisClient.del(listKey); }); @@ -204,11 +205,11 @@ describe('Redis连接测试', () => { it('健康检查在连接断开时应返回false', async () => { // 暂时断开连接 await redis.disconnectRedis(); - + // 健康检查应该返回false const isHealthy = await redis.checkRedisHealth(); expect(isHealthy).toBe(false); - + // 重新连接 await redis.connectRedis(); }); @@ -216,10 +217,10 @@ describe('Redis连接测试', () => { it('应该能够重新连接Redis', async () => { // 断开连接 await redis.disconnectRedis(); - + // 重新连接 await redis.connectRedis(); - + // 验证连接正常 const isHealthy = await redis.checkRedisHealth(); expect(isHealthy).toBe(true); @@ -230,17 +231,17 @@ describe('Redis连接测试', () => { it('应该能够快速执行大量set操作', async () => { const startTime = Date.now(); const operations = []; - + // 执行100次set操作 for (let i = 0; i < 100; i++) { operations.push(redis.redisClient.set(`perf_test_${i}`, `value_${i}`)); } - + await Promise.all(operations); const endTime = Date.now(); - + expect(endTime - startTime).toBeLessThan(1000); // 应该在1秒内完成 - + // 清理测试数据 const deleteOperations = []; for (let i = 0; i < 100; i++) { @@ -293,14 +294,14 @@ import { redisConfig } from '@/config/redis.config'; [field1]: value1, [field2]: value2, }); - + // 获取所有Hash字段 const result = await redis.redisClient.hGetAll(hashKey); expect(result).toEqual({ [field1]: value1, [field2]: value2, }); - + // 清理测试数据 await redis.redisClient.del(hashKey); }); @@ -311,14 +312,14 @@ import { redisConfig } from '@/config/redis.config'; // 设置多个Hash字段 await redis.redisClient.hSet(hashKey, field1, value1); await redis.redisClient.hSet(hashKey, field2, value2); - + // 获取所有Hash字段 const result = await redis.redisClient.hGetAll(hashKey); expect(result).toEqual({ [field1]: value1, [field2]: value2, }); - + // 清理测试数据 await redis.redisClient.del(hashKey); }); @@ -328,15 +329,15 @@ import { redisConfig } from '@/config/redis.config'; it('应该能够推入和弹出列表元素', async () => { // 推入元素 await redis.redisClient.lPush(listKey, value1, value2); - + // 获取列表长度 const length = await redis.redisClient.lLen(listKey); expect(length).toBe(2); - + // 弹出元素 const poppedValue = await redis.redisClient.lPop(listKey); expect(poppedValue).toBe(value2); - + // 清理测试数据 await redis.redisClient.del(listKey); }); @@ -347,15 +348,15 @@ import { redisConfig } from '@/config/redis.config'; // 推入元素 await redis.redisClient.lPush(listKey, value1); await redis.redisClient.lPush(listKey, value2); - + // 获取列表长度 const length = await redis.redisClient.lLen(listKey); expect(length).toBe(2); - + // 弹出元素 const poppedValue = await redis.redisClient.lPop(listKey); expect(poppedValue).toBe(value2); - + // 清理测试数据 await redis.redisClient.del(listKey); }); @@ -450,28 +451,34 @@ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); ## 📋 测试覆盖范围 ### 🔗 Redis连接管理 + - ✅ Redis服务器连接测试 - ✅ 连接状态检查 - ✅ Ping命令测试 ### 🔧 Redis基本操作 + - ✅ 字符串的设置和获取 - ✅ 带过期时间的键值对 - ✅ 键的存在性检查 - ✅ 键的删除操作 ### 📝 Redis Hash操作 + - ✅ Hash字段的设置和获取 - ✅ 多个Hash字段操作 ### 📜 Redis列表操作 + - ✅ 列表元素的推入和弹出 ### ⚠️ 错误处理测试 + - ✅ 连接断开时的健康检查 - ✅ 重新连接功能 ### ⚡ 性能测试 + - ✅ 大量并发操作性能测试 ## 🚀 如何运行测试 @@ -604,7 +611,7 @@ class HealthService { async getHealthStatus(ctx: Context): Promise { const startTime = Date.now(); const timestamp = new Date().toISOString(); - + try { // 并行检查所有依赖 const [mysqlStatus, redisStatus] = await Promise.allSettled([ @@ -614,8 +621,12 @@ class HealthService { /** 系统整体状态 */ const overallStatus = this.determineOverallStatus([ - mysqlStatus.status === 'fulfilled' ? mysqlStatus.value : { status: 'unhealthy', error: 'Connection failed' }, - redisStatus.status === 'fulfilled' ? redisStatus.value : { status: 'unhealthy', error: 'Connection failed' }, + mysqlStatus.status === 'fulfilled' + ? mysqlStatus.value + : { status: 'unhealthy', error: 'Connection failed' }, + redisStatus.status === 'fulfilled' + ? redisStatus.value + : { status: 'unhealthy', error: 'Connection failed' }, ]); const responseTime = Date.now() - startTime; @@ -631,8 +642,14 @@ class HealthService { version: process.env.npm_package_version || '1.0.0', environment: process.env.NODE_ENV || 'development', components: { - mysql: mysqlStatus.status === 'fulfilled' ? mysqlStatus.value : { status: 'unhealthy', error: 'Connection failed' }, - redis: redisStatus.status === 'fulfilled' ? redisStatus.value : { status: 'unhealthy', error: 'Connection failed' }, + mysql: + mysqlStatus.status === 'fulfilled' + ? mysqlStatus.value + : { status: 'unhealthy', error: 'Connection failed' }, + redis: + redisStatus.status === 'fulfilled' + ? redisStatus.value + : { status: 'unhealthy', error: 'Connection failed' }, }, }, }; @@ -663,14 +680,14 @@ class HealthService { async getDetailedHealthStatus(ctx: Context): Promise { const startTime = Date.now(); const timestamp = new Date().toISOString(); - + try { // 获取基本健康状态 const basicHealth = await this.getHealthStatus(ctx); - + // 获取系统资源信息 const systemInfo = this.getSystemInfo(); - + return { ...basicHealth, data: { @@ -711,7 +728,7 @@ class HealthService { const startTime = Date.now(); await mysql.execute('SELECT 1'); const responseTime = Date.now() - startTime; - + return { status: 'healthy', responseTime, @@ -744,7 +761,7 @@ class HealthService { const startTime = Date.now(); const isHealthy = await this.redis.checkRedisHealth(); const responseTime = Date.now() - startTime; - + if (isHealthy) { const redisStatus = this.redis.getRedisStatus(); return { @@ -782,9 +799,9 @@ class HealthService { * @returns 整体状态 */ private determineOverallStatus(components: ComponentStatus[]): 'healthy' | 'unhealthy' | 'degraded' { - const healthyCount = components.filter(c => c.status === 'healthy').length; + const healthyCount = components.filter((c) => c.status === 'healthy').length; const totalCount = components.length; - + if (healthyCount === totalCount) { return 'healthy'; } else if (healthyCount === 0) { @@ -1287,7 +1304,7 @@ import { app } from '@/app'; describe('健康检查接口测试', () => { beforeAll(async () => { // 等待应用启动 - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); }); describe('GET /api/health', () => { @@ -1298,14 +1315,14 @@ describe('健康检查接口测试', () => { headers: { 'Content-Type': 'application/json' }, }), ); - + const body = (await res.json()) as any; - + expect(res.status).toBe(200); expect(body.code).toBeTypeOf('number'); expect(body.message).toBeTypeOf('string'); expect(body.data).toBeTypeOf('object'); - + // 检查基本数据结构 expect(body.data.status).toMatch(/^(healthy|unhealthy|degraded)$/); expect(body.data.timestamp).toBeTypeOf('string'); @@ -1314,7 +1331,7 @@ describe('健康检查接口测试', () => { expect(body.data.version).toBeTypeOf('string'); expect(body.data.environment).toBeTypeOf('string'); expect(body.data.components).toBeTypeOf('object'); - + // 检查组件状态 if (body.data.components.mysql) { expect(body.data.components.mysql.status).toMatch(/^(healthy|unhealthy|degraded)$/); @@ -1330,9 +1347,9 @@ describe('健康检查接口测试', () => { method: 'GET', }), ); - + const body = (await res.json()) as any; - + // 验证ISO时间戳格式 const timestamp = new Date(body.data.timestamp); expect(timestamp.toISOString()).toBe(body.data.timestamp); @@ -1346,9 +1363,9 @@ describe('健康检查接口测试', () => { }), ); const endTime = Date.now(); - + const body = (await res.json()) as any; - + // 响应时间应该在合理范围内 expect(body.data.responseTime).toBeGreaterThan(0); expect(body.data.responseTime).toBeLessThan(endTime - startTime + 100); // 允许一定误差 @@ -1360,9 +1377,9 @@ describe('健康检查接口测试', () => { method: 'GET', }), ); - + const body = (await res.json()) as any; - + expect(body.data.environment).toMatch(/^(development|production|test)$/); expect(body.data.uptime).toBeGreaterThan(0); }); @@ -1376,21 +1393,21 @@ describe('健康检查接口测试', () => { headers: { 'Content-Type': 'application/json' }, }), ); - + const body = (await res.json()) as any; - + expect(res.status).toBe(200); expect(body.code).toBeTypeOf('number'); expect(body.message).toBeTypeOf('string'); expect(body.data).toBeTypeOf('object'); - + // 检查基本健康检查数据 expect(body.data.status).toMatch(/^(healthy|unhealthy|degraded)$/); expect(body.data.timestamp).toBeTypeOf('string'); expect(body.data.uptime).toBeTypeOf('number'); expect(body.data.responseTime).toBeTypeOf('number'); expect(body.data.components).toBeTypeOf('object'); - + // 检查详细信息 if (body.data.system) { expect(body.data.system.platform).toBeTypeOf('string'); @@ -1400,16 +1417,16 @@ describe('健康检查接口测试', () => { expect(body.data.system.pid).toBeTypeOf('number'); expect(body.data.system.cwd).toBeTypeOf('string'); } - + if (body.data.performance) { expect(body.data.performance.cpuUsage).toBeTypeOf('object'); expect(body.data.performance.memoryUsage).toBeTypeOf('object'); expect(body.data.performance.uptime).toBeTypeOf('number'); - + // 检查CPU使用情况 expect(body.data.performance.cpuUsage.user).toBeTypeOf('number'); expect(body.data.performance.cpuUsage.system).toBeTypeOf('number'); - + // 检查内存使用情况 expect(body.data.performance.memoryUsage.rss).toBeTypeOf('number'); expect(body.data.performance.memoryUsage.heapTotal).toBeTypeOf('number'); @@ -1425,9 +1442,9 @@ describe('健康检查接口测试', () => { method: 'GET', }), ); - + const body = (await res.json()) as any; - + if (body.data.system) { expect(body.data.system.runtime).toBe('Bun'); expect(body.data.system.pid).toBe(process.pid); @@ -1444,18 +1461,18 @@ describe('健康检查接口测试', () => { method: 'GET', }), ); - + const body = (await res.json()) as any; - + if (body.data.components.mysql) { expect(body.data.components.mysql.status).toMatch(/^(healthy|unhealthy|degraded)$/); - + if (body.data.components.mysql.details) { expect(body.data.components.mysql.details.connection).toMatch(/^(active|failed)$/); expect(body.data.components.mysql.details.host).toBeTypeOf('string'); expect(body.data.components.mysql.details.port).toBeTypeOf('string'); } - + if (body.data.components.mysql.responseTime) { expect(body.data.components.mysql.responseTime).toBeGreaterThan(0); } @@ -1468,16 +1485,16 @@ describe('健康检查接口测试', () => { method: 'GET', }), ); - + const body = (await res.json()) as any; - + if (body.data.components.redis) { expect(body.data.components.redis.status).toMatch(/^(healthy|unhealthy|degraded)$/); - + if (body.data.components.redis.details) { expect(body.data.components.redis.details.connection).toMatch(/^(active|failed)$/); } - + if (body.data.components.redis.responseTime) { expect(body.data.components.redis.responseTime).toBeGreaterThan(0); } @@ -1492,28 +1509,28 @@ describe('健康检查接口测试', () => { method: 'GET', }), ); - + const body = (await res.json()) as any; - + // 即使有组件异常,也应该返回结构化的响应 expect(res.status).toBe(200); expect(body.code).toBeTypeOf('number'); expect(body.message).toBeTypeOf('string'); expect(body.data).toBeTypeOf('object'); - + // 如果有组件异常,整体状态可能是degraded或unhealthy if (body.data.status === 'unhealthy' || body.data.status === 'degraded') { // 应该有组件错误信息 const components = body.data.components; let hasUnhealthyComponent = false; - + Object.values(components).forEach((component: any) => { if (component && component.status === 'unhealthy') { hasUnhealthyComponent = true; expect(component.error).toBeTypeOf('string'); } }); - + if (!hasUnhealthyComponent && body.data.error) { expect(body.data.error).toBeTypeOf('string'); } @@ -1524,32 +1541,32 @@ describe('健康检查接口测试', () => { describe('健康检查性能测试', () => { it('健康检查应该快速响应', async () => { const startTime = Date.now(); - + const res = await app.fetch( new Request('http://localhost/api/health', { method: 'GET', }), ); - + const endTime = Date.now(); const responseTime = endTime - startTime; - + expect(res.status).toBe(200); expect(responseTime).toBeLessThan(2000); // 应该在2秒内完成 }); it('详细健康检查应该在合理时间内完成', async () => { const startTime = Date.now(); - + const res = await app.fetch( new Request('http://localhost/api/health/detailed', { method: 'GET', }), ); - + const endTime = Date.now(); const responseTime = endTime - startTime; - + expect(res.status).toBe(200); expect(responseTime).toBeLessThan(3000); // 详细检查可能稍慢,但应该在3秒内完成 }); @@ -1557,20 +1574,20 @@ describe('健康检查接口测试', () => { it('并发健康检查应该正常处理', async () => { const promises = []; const concurrentRequests = 5; - + for (let i = 0; i < concurrentRequests; i++) { promises.push( app.fetch( new Request('http://localhost/api/health', { method: 'GET', }), - ) + ), ); } - + const responses = await Promise.all(promises); - - responses.forEach(res => { + + responses.forEach((res) => { expect(res.status).toBe(200); }); }); @@ -1628,11 +1645,13 @@ import { app } from '@/app'; ### ✅ 已实现的功能 #### 1. **健康检查控制器** (`src/controllers/health.controller.ts`) + - 📍 `/api/health` - 基本健康检查接口 -- 📍 `/api/health/detailed` - 详细健康检查接口 +- 📍 `/api/health/detailed` - 详细健康检查接口 - 📖 集成了完整的 Swagger 文档 #### 2. **健康检查服务** (`src/services/health.service.ts`) + - 🔄 **并行检查**:同时检查 MySQL 和 Redis 连接状态 - 📊 **三种状态**:healthy、unhealthy、degraded - ⏱️ **响应时间**:监控每个组件的响应时间 @@ -1641,18 +1660,22 @@ import { app } from '@/app'; - ⚠️ **错误处理**:完善的异常捕获和错误响应 #### 3. **类型定义** (`src/type/health.type.ts`) + - 完整的 TypeScript 类型定义 - 组件状态、系统信息、性能指标等类型 #### 4. **响应验证器** (`src/validators/health.response.ts`) + - Elysia TypeBox 严格验证规则 - 支持基本和详细健康检查响应验证 #### 5. **应用集成** (`src/app.ts`) + - 将健康检查控制器集成到主应用 - 替换了原有的简单健康检查接口 #### 6. **全面测试** (`src/tests/health.test.ts`) + - 🧪 基本健康检查接口测试 - 🔍 详细健康检查接口测试 - 🔗 依赖服务状态测试 @@ -1662,20 +1685,21 @@ import { app } from '@/app'; ### 🌟 接口特性 -| 特性 | 描述 | -|------|------| -| **依赖监控** | MySQL + Redis 连接状态实时检查 | +| 特性 | 描述 | +| ------------ | ----------------------------------- | +| **依赖监控** | MySQL + Redis 连接状态实时检查 | | **状态分级** | healthy/unhealthy/degraded 三级状态 | -| **性能监控** | CPU、内存、响应时间统计 | -| **错误友好** | 详细错误信息和降级策略 | -| **文档完善** | 自动生成 Swagger API 文档 | -| **测试全面** | 涵盖正常、异常、性能、并发场景 | +| **性能监控** | CPU、内存、响应时间统计 | +| **错误友好** | 详细错误信息和降级策略 | +| **文档完善** | 自动生成 Swagger API 文档 | +| **测试全面** | 涵盖正常、异常、性能、并发场景 | ### 📋 任务清单更新 任务清单已更新: + - ✅ 5.0 健康检查接口 - - ✅ 5.1 实现 /health 路由,返回服务与依赖健康状态 + - ✅ 5.1 实现 /health 路由,返回服务与依赖健康状态 你可以通过以下命令测试健康检查功能: @@ -1867,11 +1891,13 @@ import { healthResponse } from '@/validators/health.response'; 我已经修复了主要的导入问题,包括: ✅ **已修复的问题**: -- MySQL导入:从 `mysql` 改为 `pool` + +- MySQL导入:从 `mysql` 改为 `pool` - Logger参数:修复了错误日志记录的参数数量 - 类型导入:使用 `import type` 和内联类型定义 🔧 **当前状态**: + - 健康检查功能已完全实现 - 任务5.0已标记为完成 - 还有一些TypeScript路径别名相关的警告 @@ -1896,6 +1922,7 @@ ERROR undefined is not an object (evaluating 'this.checkMysqlHealth') 我看到了问题!测试虽然通过了,但有很多错误日志显示 `undefined is not an object (evaluating 'this.checkMysqlHealth')`。这是一个典型的 JavaScript `this` 上下文丢失的问题。 当我们在控制器中直接传递服务方法时: + ```typescript .get('/health', healthService.getHealthStatus, { ``` @@ -2191,11 +2218,7 @@ export const HealthCheckResponseSchema = t.Object({ code: t.Number(), message: t.String(), data: t.Object({ - status: t.Union([ - t.Literal('healthy'), - t.Literal('unhealthy'), - t.Literal('degraded'), - ], { + status: t.Union([t.Literal('healthy'), t.Literal('unhealthy'), t.Literal('degraded')], { description: '系统健康状态:healthy-健康,unhealthy-不健康,degraded-降级', }), timestamp: t.String({ @@ -2219,18 +2242,22 @@ export const HealthCheckResponseSchema = t.Object({ examples: ['development', 'production', 'test'], }), components: t.Object({ - mysql: t.Optional(t.Object({ - status: t.String(), - responseTime: t.Optional(t.Number()), - error: t.Optional(t.String()), - details: t.Optional(t.Any()), - })), - redis: t.Optional(t.Object({ - status: t.String(), - responseTime: t.Optional(t.Number()), - error: t.Optional(t.String()), - details: t.Optional(t.Any()), - })), + mysql: t.Optional( + t.Object({ + status: t.String(), + responseTime: t.Optional(t.Number()), + error: t.Optional(t.String()), + details: t.Optional(t.Any()), + }), + ), + redis: t.Optional( + t.Object({ + status: t.String(), + responseTime: t.Optional(t.Number()), + error: t.Optional(t.String()), + details: t.Optional(t.Any()), + }), + ), }), }), }); @@ -2856,9 +2883,9 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + expect(res.status).toBe(200); - + const contentType = res.headers.get('content-type'); expect(contentType).toContain('text/html'); }); @@ -2869,9 +2896,9 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const html = await res.text(); - + // 检查基本配置 expect(html).toContain('Cursor Init API服务'); expect(html).toContain('swagger-ui'); @@ -2885,20 +2912,20 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + expect(res.status).toBe(200); - + const contentType = res.headers.get('content-type'); expect(contentType).toContain('application/json'); - + const openApiDoc = await res.json(); - + // 验证OpenAPI文档结构 expect(openApiDoc).toHaveProperty('openapi'); expect(openApiDoc).toHaveProperty('info'); expect(openApiDoc).toHaveProperty('paths'); expect(openApiDoc).toHaveProperty('components'); - + // 验证基本信息 expect(openApiDoc.info.title).toBe('Cursor Init API服务'); expect(openApiDoc.info.version).toBe('1.0.0'); @@ -2911,9 +2938,9 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const openApiDoc = await res.json(); - + // 验证安全配置 expect(openApiDoc.components).toHaveProperty('securitySchemes'); expect(openApiDoc.components.securitySchemes).toHaveProperty('bearerAuth'); @@ -2928,13 +2955,13 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const openApiDoc = await res.json(); - + // 验证全局组件 expect(openApiDoc.components).toHaveProperty('schemas'); expect(openApiDoc.components).toHaveProperty('responses'); - + // 验证响应组件 const responses = openApiDoc.components.responses; expect(responses).toHaveProperty('Success'); @@ -2945,7 +2972,7 @@ describe('Swagger API文档测试', () => { expect(responses).toHaveProperty('ValidationError'); expect(responses).toHaveProperty('InternalError'); expect(responses).toHaveProperty('ServiceUnavailable'); - + // 验证Schema组件 const schemas = openApiDoc.components.schemas; expect(schemas).toHaveProperty('BaseResponse'); @@ -2960,13 +2987,13 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const openApiDoc = await res.json(); - + // 验证健康检查接口 expect(openApiDoc.paths).toHaveProperty('/api/health'); expect(openApiDoc.paths).toHaveProperty('/api/health/detailed'); - + const healthPath = openApiDoc.paths['/api/health']; expect(healthPath).toHaveProperty('get'); expect(healthPath.get).toHaveProperty('tags'); @@ -2981,12 +3008,12 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const openApiDoc = await res.json(); - + // 验证认证接口 expect(openApiDoc.paths).toHaveProperty('/api/auth/login'); - + const loginPath = openApiDoc.paths['/api/auth/login']; expect(loginPath).toHaveProperty('post'); expect(loginPath.post).toHaveProperty('tags'); @@ -3000,17 +3027,17 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const openApiDoc = await res.json(); - + // 验证标签 expect(openApiDoc).toHaveProperty('tags'); expect(Array.isArray(openApiDoc.tags)).toBe(true); - + const tagNames = openApiDoc.tags.map((tag: any) => tag.name); expect(tagNames).toContain('认证管理'); expect(tagNames).toContain('健康检查'); - + // 验证标签描述 const healthTag = openApiDoc.tags.find((tag: any) => tag.name === '健康检查'); expect(healthTag).toHaveProperty('description'); @@ -3025,9 +3052,9 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const openApiDoc = await res.json(); - + // 验证错误码描述在文档中 expect(openApiDoc.info.description).toContain('错误码说明'); expect(openApiDoc.info.description).toContain('| 错误码 | 说明 | 示例场景 |'); @@ -3042,9 +3069,9 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const openApiDoc = await res.json(); - + // 验证认证说明 expect(openApiDoc.info.description).toContain('认证说明'); expect(openApiDoc.info.description).toContain('Authorization: Bearer'); @@ -3058,9 +3085,9 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const openApiDoc = await res.json(); - + // 验证响应格式说明 expect(openApiDoc.info.description).toContain('响应格式'); expect(openApiDoc.info.description).toContain('"code": 0'); @@ -3074,16 +3101,16 @@ describe('Swagger API文档测试', () => { method: 'GET', }), ); - + const openApiDoc = await res.json(); - + // 验证示例响应 const successResponse = openApiDoc.components.responses.Success; expect(successResponse.content['application/json']).toHaveProperty('examples'); - + const errorResponse = openApiDoc.components.responses.BadRequest; expect(errorResponse.content['application/json']).toHaveProperty('examples'); - + const unauthorizedResponse = openApiDoc.components.responses.Unauthorized; expect(unauthorizedResponse.content['application/json']).toHaveProperty('examples'); expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('tokenExpired'); @@ -3095,32 +3122,32 @@ describe('Swagger API文档测试', () => { describe('Swagger文档性能测试', () => { it('文档页面应该快速加载', async () => { const startTime = Date.now(); - + const res = await app.fetch( new Request('http://localhost/docs', { method: 'GET', }), ); - + const endTime = Date.now(); const responseTime = endTime - startTime; - + expect(res.status).toBe(200); expect(responseTime).toBeLessThan(1000); // 应该在1秒内完成 }); it('JSON文档应该快速响应', async () => { const startTime = Date.now(); - + const res = await app.fetch( new Request('http://localhost/docs/json', { method: 'GET', }), ); - + const endTime = Date.now(); const responseTime = endTime - startTime; - + expect(res.status).toBe(200); expect(responseTime).toBeLessThan(500); // JSON文档应该更快 }); @@ -3251,4 +3278,3 @@ feat: 完成健康检查接口和Swagger文档完善 - src/tests/redis.test.ts - Redis连接测试 - src/tests/swagger.test.ts - Swagger文档功能测试 ``` - diff --git a/aiChat/003-cursor_.md b/aiChat/003-cursor_.md index 3689143..c8dfec2 100644 --- a/aiChat/003-cursor_.md +++ b/aiChat/003-cursor_.md @@ -1,4 +1,5 @@ # 数据库设计与表字段规划 + _Exported on 2025/6/29 at GMT+8 3:10:41 from Cursor (1.1.6)_ --- @@ -19,19 +20,19 @@ _Exported on 2025/6/29 at GMT+8 3:10:41 from Cursor (1.1.6)_ 1. **完整的ER图** - 使用Mermaid绘制,展示所有表的关系 2. **详细的表结构设计** - 每个表都包含: - - 完整的字段定义(类型、长度、默认值等) - - 软删除支持(deleted_at字段) - - 树形结构支持(pid字段) - - 审计字段(created_by, updated_by等) - - 索引设计 - - 数据约束(通过代码逻辑实现,不使用外键) + - 完整的字段定义(类型、长度、默认值等) + - 软删除支持(deleted_at字段) + - 树形结构支持(pid字段) + - 审计字段(created_by, updated_by等) + - 索引设计 + - 数据约束(通过代码逻辑实现,不使用外键) 3. **考虑的关键因素**: - - 高性能查询优化 - - 数据完整性保证 - - 扩展性设计 - - 并发控制 - - 数据安全 + - 高性能查询优化 + - 数据完整性保证 + - 扩展性设计 + - 并发控制 + - 数据安全 请确认是否删除现有文档,然后我将开始创建新的数据库设计。 @@ -47,7 +48,7 @@ _Exported on 2025/6/29 at GMT+8 3:10:41 from Cursor (1.1.6)_ 好的,我现在为您创建一个全新的、专业的M2基础用户系统数据库设计文档。 -```markdown +````markdown # M2 - 基础用户系统 - 数据库设计文档 ## 1. 概述 @@ -286,6 +287,7 @@ erDiagram sys_permissions ||--o| sys_permissions : "包含" sys_dict_items ||--o| sys_dict_items : "子项" ``` +```` ## 3. 表结构详细设计 @@ -701,7 +703,7 @@ CREATE INDEX `idx_created_at` ON `sys_operation_logs` (`created_at`); ```sql -- 创建root超级管理员 -INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `is_root`, `status`) +INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `is_root`, `status`) VALUES (1, 'root', 'root@system.local', '$2b$10$...', '超级管理员', TRUE, 'active'); ``` @@ -709,11 +711,11 @@ VALUES (1, 'root', 'root@system.local', '$2b$10$...', '超级管理员', TRUE, ' ```sql -- 超级管理员角色 -INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) +INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) VALUES (1, 'super_admin', '超级管理员', '拥有系统所有权限', TRUE, 'active'); -- 普通用户角色 -INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) +INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) VALUES (2, 'user', '普通用户', '普通注册用户默认角色', TRUE, 'active'); ``` @@ -721,7 +723,7 @@ VALUES (2, 'user', '普通用户', '普通注册用户默认角色', TRUE, 'acti ```sql -- 用户状态字典 -INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`) +INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`) VALUES (1, 'user_status', '用户状态', TRUE); INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `color`, `sort_order`) VALUES @@ -749,7 +751,8 @@ INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `col 3. **审批流程**:增加工作流相关表,支持复杂的审批场景 4. **消息通知**:增加消息表,支持站内信、邮件、短信等通知 5. **登录日志**:独立的登录日志表,记录所有登录尝试 -``` + +```` 我已经为您创建了全新的M2基础用户系统数据库设计文档 `prd/M2-基础用户系统-数据库设计.md`。 @@ -913,7 +916,7 @@ erDiagram datetime updated_at datetime deleted_at } - + role_permissions { bigint id PK bigint role_id @@ -924,7 +927,7 @@ erDiagram datetime updated_at datetime deleted_at } - + user_organizations { bigint id PK bigint user_id @@ -936,7 +939,7 @@ erDiagram datetime updated_at datetime deleted_at } - + user_tags { bigint id PK bigint user_id @@ -947,32 +950,34 @@ erDiagram datetime updated_at datetime deleted_at } -``` +```` ## 详细表结构设计 ### users - 用户表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 用户唯一ID | -| username | VARCHAR(50) | UNIQUE, NOT NULL | | 用户名 | -| email | VARCHAR(255) | UNIQUE, NOT NULL | | 邮箱地址 | -| password_hash | VARCHAR(255) | NOT NULL | | 密码哈希值 | -| status | VARCHAR(20) | NOT NULL | 'pending' | 用户状态 | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ------------- | ------------ | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 用户唯一ID | +| username | VARCHAR(50) | UNIQUE, NOT NULL | | 用户名 | +| email | VARCHAR(255) | UNIQUE, NOT NULL | | 邮箱地址 | +| password_hash | VARCHAR(255) | NOT NULL | | 密码哈希值 | +| status | VARCHAR(20) | NOT NULL | 'pending' | 用户状态 | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_users_username` (username) - `idx_users_email` (email) - `idx_users_status` (status) - `idx_users_deleted` (deleted_at) **数据约束**: + - `status` 值必须来自 `dictionary_items` 中 `user_status` 类型的字典项 - `password_hash` 使用 bcrypt 算法存储 - 应用层逻辑确保 `username` 只包含字母、数字和下划线 @@ -980,115 +985,124 @@ erDiagram ### roles - 角色表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 角色唯一ID | -| name | VARCHAR(50) | UNIQUE, NOT NULL | | 角色名称 | -| description | TEXT | | NULL | 角色描述 | -| parent_id | BIGINT | | NULL | 父角色ID | -| sort_order | INT | | 0 | 排序顺序 | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ----------- | ----------- | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 角色唯一ID | +| name | VARCHAR(50) | UNIQUE, NOT NULL | | 角色名称 | +| description | TEXT | | NULL | 角色描述 | +| parent_id | BIGINT | | NULL | 父角色ID | +| sort_order | INT | | 0 | 排序顺序 | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_roles_name` (name) - `idx_roles_parent` (parent_id) - `idx_roles_deleted` (deleted_at) **数据约束**: + - 应用层逻辑确保角色树形结构无循环引用 - 应用层逻辑确保 `root` 角色不可删除 - 应用层逻辑确保删除角色时处理子角色继承关系 ### permissions - 权限表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 权限唯一ID | -| action | VARCHAR(50) | NOT NULL | | 操作类型 | -| resource | VARCHAR(50) | NOT NULL | | 资源类型 | -| description | TEXT | | NULL | 权限描述 | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ----------- | ----------- | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 权限唯一ID | +| action | VARCHAR(50) | NOT NULL | | 操作类型 | +| resource | VARCHAR(50) | NOT NULL | | 资源类型 | +| description | TEXT | | NULL | 权限描述 | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_permissions_action` (action) - `idx_permissions_resource` (resource) - `idx_permissions_unique` (action, resource) UNIQUE - `idx_permissions_deleted` (deleted_at) **数据约束**: + - 应用层逻辑确保 `action` 和 `resource` 组合唯一 - 应用层逻辑确保权限分配时下级角色只能分配上级已有的权限 ### organizations - 组织表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 组织唯一ID | -| name | VARCHAR(100) | NOT NULL | | 组织名称 | -| description | TEXT | | NULL | 组织描述 | -| parent_id | BIGINT | | NULL | 父组织ID | -| type | VARCHAR(20) | | 'department' | 组织类型 | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ----------- | ------------ | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 组织唯一ID | +| name | VARCHAR(100) | NOT NULL | | 组织名称 | +| description | TEXT | | NULL | 组织描述 | +| parent_id | BIGINT | | NULL | 父组织ID | +| type | VARCHAR(20) | | 'department' | 组织类型 | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_organizations_name` (name) - `idx_organizations_parent` (parent_id) - `idx_organizations_type` (type) - `idx_organizations_deleted` (deleted_at) **数据约束**: + - 应用层逻辑确保组织树形结构无循环引用 - `type` 值必须来自 `dictionary_items` 中 `org_type` 类型的字典项 - 应用层逻辑确保删除组织时处理子组织继承关系 ### dictionary_types - 字典类型表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 类型唯一ID | -| type_name | VARCHAR(50) | UNIQUE, NOT NULL | | 字典类型名称 | -| description | TEXT | | NULL | 类型描述 | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ----------- | ----------- | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 类型唯一ID | +| type_name | VARCHAR(50) | UNIQUE, NOT NULL | | 字典类型名称 | +| description | TEXT | | NULL | 类型描述 | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_dict_types_name` (type_name) - `idx_dict_types_deleted` (deleted_at) **数据约束**: + - 应用层逻辑确保系统核心字典类型(如user_status)不可删除 ### dictionary_items - 字典条目表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 条目唯一ID | -| type_id | BIGINT | NOT NULL | | 关联字典类型ID | -| key | VARCHAR(50) | NOT NULL | | 键(程序中使用) | -| value | VARCHAR(255) | NOT NULL | | 值(UI显示) | -| sort_order | INT | | 0 | 排序顺序 | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ---------- | ------------ | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 条目唯一ID | +| type_id | BIGINT | NOT NULL | | 关联字典类型ID | +| key | VARCHAR(50) | NOT NULL | | 键(程序中使用) | +| value | VARCHAR(255) | NOT NULL | | 值(UI显示) | +| sort_order | INT | | 0 | 排序顺序 | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_dict_items_type` (type_id) - `idx_dict_items_key` (key) - `idx_dict_items_sort` (sort_order) @@ -1096,91 +1110,99 @@ erDiagram - `idx_dict_items_unique` (type_id, key) UNIQUE **数据约束**: + - 应用层逻辑确保同一类型下 `key` 唯一 - 应用层逻辑确保系统核心字典项(如active/inactive)不可删除 ### tags - 标签表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 标签唯一ID | -| name | VARCHAR(50) | UNIQUE, NOT NULL | | 标签名称 | -| category | VARCHAR(20) | | 'user' | 标签分类 | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ---------- | ----------- | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 标签唯一ID | +| name | VARCHAR(50) | UNIQUE, NOT NULL | | 标签名称 | +| category | VARCHAR(20) | | 'user' | 标签分类 | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_tags_name` (name) - `idx_tags_category` (category) - `idx_tags_deleted` (deleted_at) **数据约束**: + - 应用层逻辑确保 `category` 值来自 `dictionary_items` 中 `tag_category` 类型 ### user_roles - 用户角色关联表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 关联唯一ID | -| user_id | BIGINT | NOT NULL | | 用户ID | -| role_id | BIGINT | NOT NULL | | 角色ID | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ---------- | -------- | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 关联唯一ID | +| user_id | BIGINT | NOT NULL | | 用户ID | +| role_id | BIGINT | NOT NULL | | 角色ID | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_user_roles_user` (user_id) - `idx_user_roles_role` (role_id) - `idx_user_roles_unique` (user_id, role_id) UNIQUE - `idx_user_roles_deleted` (deleted_at) **数据约束**: + - 应用层逻辑确保用户不能重复分配同一角色 - 应用层逻辑确保 `root` 用户角色不可更改 ### role_permissions - 角色权限关联表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 关联唯一ID | -| role_id | BIGINT | NOT NULL | | 角色ID | -| permission_id | BIGINT | NOT NULL | | 权限ID | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ------------- | -------- | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 关联唯一ID | +| role_id | BIGINT | NOT NULL | | 角色ID | +| permission_id | BIGINT | NOT NULL | | 权限ID | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_role_perms_role` (role_id) - `idx_role_perms_perm` (permission_id) - `idx_role_perms_unique` (role_id, permission_id) UNIQUE - `idx_role_perms_deleted` (deleted_at) **数据约束**: + - 应用层逻辑确保角色不能重复分配同一权限 - 应用层逻辑确保权限分配符合角色层级规则 ### user_organizations - 用户组织关联表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 关联唯一ID | -| user_id | BIGINT | NOT NULL | | 用户ID | -| organization_id | BIGINT | NOT NULL | | 组织ID | -| is_primary | BOOLEAN | NOT NULL | FALSE | 是否为主组织 | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| --------------- | -------- | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 关联唯一ID | +| user_id | BIGINT | NOT NULL | | 用户ID | +| organization_id | BIGINT | NOT NULL | | 组织ID | +| is_primary | BOOLEAN | NOT NULL | FALSE | 是否为主组织 | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_user_orgs_user` (user_id) - `idx_user_orgs_org` (organization_id) - `idx_user_orgs_primary` (is_primary) @@ -1188,51 +1210,58 @@ erDiagram - `idx_user_orgs_deleted` (deleted_at) **数据约束**: + - 应用层逻辑确保每个用户有且只有一个主组织 - 应用层逻辑确保组织存在性验证 ### user_tags - 用户标签关联表 -| 字段名 | 类型 | 约束 | 默认值 | 描述 | -|-----------------|----------------|------------------------------|--------------|----------------------| -| id | BIGINT | PK, AUTO_INCREMENT | | 关联唯一ID | -| user_id | BIGINT | NOT NULL | | 用户ID | -| tag_id | BIGINT | NOT NULL | | 标签ID | -| created_by | BIGINT | | 0 | 创建者用户ID | -| updated_by | BIGINT | | 0 | 最后更新者用户ID | -| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | -| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | -| deleted_at | DATETIME | | NULL | 软删除时间 | +| 字段名 | 类型 | 约束 | 默认值 | 描述 | +| ---------- | -------- | ------------------ | ------------ | ---------------- | +| id | BIGINT | PK, AUTO_INCREMENT | | 关联唯一ID | +| user_id | BIGINT | NOT NULL | | 用户ID | +| tag_id | BIGINT | NOT NULL | | 标签ID | +| created_by | BIGINT | | 0 | 创建者用户ID | +| updated_by | BIGINT | | 0 | 最后更新者用户ID | +| created_at | DATETIME | NOT NULL | CURRENT_TIME | 创建时间 | +| updated_at | DATETIME | | CURRENT_TIME | 更新时间 | +| deleted_at | DATETIME | | NULL | 软删除时间 | **索引**: + - `idx_user_tags_user` (user_id) - `idx_user_tags_tag` (tag_id) - `idx_user_tags_unique` (user_id, tag_id) UNIQUE - `idx_user_tags_deleted` (deleted_at) **数据约束**: + - 应用层逻辑确保用户不能重复分配同一标签 ## 数据库设计说明 ### 1. 软删除实现 + - 所有表均包含 `deleted_at` 字段 - 当记录被删除时,设置 `deleted_at = CURRENT_TIMESTAMP` - 所有查询默认添加 `WHERE deleted_at IS NULL` 条件 - 索引设计优化软删除记录的查询性能 ### 2. 审计字段 + - `created_by` 记录创建者用户ID - `updated_by` 记录最后更新者用户ID - `created_at` 记录创建时间 - `updated_at` 记录最后更新时间 ### 3. 树形结构支持 + - `roles` 和 `organizations` 表通过 `parent_id` 实现树形结构 - 应用层逻辑处理树形结构的递归查询(使用闭包表或嵌套集模式) - 提供 `sort_order` 字段支持自定义排序 ### 4. 性能优化设计 + - 所有外键关联字段均添加索引 - 唯一约束字段添加唯一索引 - 高频查询字段(如用户名、邮箱)添加索引 @@ -1240,28 +1269,33 @@ erDiagram - 树形结构查询使用递归CTE或物化路径优化 ### 5. 数据完整性保证 + - 应用层事务处理关联操作(如删除角色时处理关联的用户角色) - 乐观锁机制(通过 updated_at 实现) - 业务逻辑验证数据一致性(如角色分配时验证权限继承关系) ### 6. 并发控制 + - 使用数据库事务保证数据操作的原子性 - 更新操作使用乐观锁机制(WHERE updated_at = last_updated_at) - 高频更新操作使用队列处理 ### 7. 数据安全 + - 敏感数据(密码)使用 bcrypt 强哈希算法存储 - 审计字段记录所有数据变更操作 - 访问控制确保只有授权用户能修改数据 - 数据传输使用 HTTPS 加密 ### 8. 扩展性设计 + - 表结构预留扩展字段(如 organizations 的 type 字段) - 使用字典表支持动态配置 - JSON 字段存储非结构化数据(可选扩展) - 水平分片设计支持大规模数据 ### 9. 数据初始化 + - 系统首次启动时自动执行 seeding 脚本 - 创建 root 用户和 super_admin 角色 - 初始化核心字典数据(用户状态、组织类型等) @@ -1275,7 +1309,7 @@ erDiagram 根据您提供的参考设计,我发现确实有一些可以优化的地方。让我更新数据库设计文档: -```markdown +````markdown # M2 - 基础用户系统 - 数据库设计文档 ## 1. 概述 @@ -1518,6 +1552,7 @@ erDiagram sys_dict_items ||--o| sys_dict_items : "子项" sys_dict_types ||--o| sys_dict_types : "包含" ``` +```` ## 3. 表结构详细设计 @@ -1979,11 +2014,11 @@ ALTER TABLE `sys_operation_logs` PARTITION BY RANGE (YEAR(created_at) * 100 + MO ```sql -- 创建root超级管理员 -INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `is_root`, `status`) +INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `is_root`, `status`) VALUES (1, 'root', 'root@system.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYpfQaXUIkRrPJK', '超级管理员', TRUE, 'active'); -- 创建系统管理员 -INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `status`) +INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `status`) VALUES (2, 'admin', 'admin@system.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYpfQaXUIkRrPJK', '系统管理员', 'active'); ``` @@ -1991,15 +2026,15 @@ VALUES (2, 'admin', 'admin@system.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQ ```sql -- 超级管理员角色 -INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) +INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) VALUES (1, 'super_admin', '超级管理员', '拥有系统所有权限', TRUE, 'active'); -- 系统管理员角色 -INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`, `pid`, `path`, `level`) +INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`, `pid`, `path`, `level`) VALUES (2, 'admin', '系统管理员', '负责系统配置和用户管理', TRUE, 'active', 1, '/1/2/', 2); -- 普通用户角色 -INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) +INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) VALUES (3, 'user', '普通用户', '普通注册用户默认角色', TRUE, 'active'); -- 分配角色 @@ -2091,7 +2126,7 @@ INSERT INTO `sys_permissions` (`id`, `code`, `name`, `type`, `resource`, `action (9, 'role:delete', '删除角色', 'api', 'role', 'delete'); -- 为超级管理员角色分配所有权限 -INSERT INTO `sys_role_permissions` (`role_id`, `permission_id`) +INSERT INTO `sys_role_permissions` (`role_id`, `permission_id`) SELECT 1, id FROM `sys_permissions`; ``` @@ -2139,12 +2174,14 @@ INSERT INTO `sys_tags` (`name`, `type`, `color`, `description`) VALUES ## 7. 性能优化建议 ### 7.1 查询优化 + - 使用覆盖索引减少回表查询 - 合理使用联合索引,注意索引顺序 - 避免在WHERE子句中对字段进行函数操作 - 使用EXPLAIN分析SQL执行计划 ### 7.2 树形结构查询优化 + ```sql -- 查询所有子节点(使用path) SELECT * FROM sys_roles WHERE path LIKE '/1/2/%' AND deleted_at IS NULL; @@ -2156,13 +2193,14 @@ SELECT * FROM sys_roles WHERE pid = 2 AND deleted_at IS NULL ORDER BY sort_order WITH RECURSIVE role_tree AS ( SELECT * FROM sys_roles WHERE id = 1 UNION ALL - SELECT r.* FROM sys_roles r + SELECT r.* FROM sys_roles r INNER JOIN role_tree rt ON r.pid = rt.id ) SELECT * FROM role_tree; ``` ### 7.3 权限查询优化 + ```sql -- 使用权限快照避免多表联查 SELECT permissions_snapshot FROM sys_roles WHERE id = ?; @@ -2172,7 +2210,7 @@ SELECT DISTINCT p.* FROM sys_permissions p JOIN sys_role_permissions rp ON p.id = rp.permission_id JOIN sys_user_roles ur ON rp.role_id = ur.role_id -WHERE ur.user_id = ? +WHERE ur.user_id = ? AND ur.expired_at IS NULL OR ur.expired_at > NOW() AND p.deleted_at IS NULL; ``` @@ -2198,7 +2236,8 @@ WHERE ur.user_id = ? 6. **审计日志**:记录所有敏感操作 7. **数据备份**:定期备份,异地容灾 8. **安全扫描**:定期进行安全漏洞扫描 -``` + +```` 我已经根据您的参考设计和建议对数据库设计文档进行了全面优化。主要改进包括: @@ -2228,20 +2267,23 @@ WHERE ur.user_id = ? └── 中国 ├── 北京 └── 上海 -``` +```` ### 4. **性能优化方案** + - ✅ 提供了具体的SQL查询优化示例 - ✅ 树形结构查询的多种实现方式(path查询、递归CTE) - ✅ 权限快照机制避免复杂联表 - ✅ 日志表分区策略实例 ### 5. **安全性增强** + - ✅ 密码加密成本因子提升到12 - ✅ 添加了完整的安全建议清单 - ✅ 操作日志增加IP索引便于安全审计 ### 6. **其他改进** + - ✅ 所有created_by和updated_by字段默认值设为0 - ✅ 补充了软删除回收站功能说明 - ✅ 添加了更多扩展建议(配置管理、定时任务等) @@ -2255,6 +2297,7 @@ WHERE ur.user_id = ? 5. **运维友好**:软删除、版本控制、数据归档策略 现在这个数据库设计已经非常完善,可以满足企业级应用的需求。接下来您想: + 1. 开始API接口设计? 2. 创建数据库初始化脚本? 3. 设计具体的业务流程? @@ -2281,6 +2324,7 @@ WHERE ur.user_id = ? **User** 根据以上内容,生成M2详细的接口设计,大概格式: + 1. 需求描述 2. 接口名称 3. 设计原因,如何解决问题 @@ -2293,7 +2337,6 @@ WHERE ur.user_id = ? 以上格式,如果有更好的建议可以补充,请按照以上要求设计接口,满足main.md M2数据库设计,概要设计等文档,restful规范,高性能,高安全性等等特征 - --- **Cursor** @@ -2315,8 +2358,10 @@ WHERE ur.user_id = ? ### 基础URL ``` + https://api.starzh.com/v1 -``` + +```` ### 统一响应格式 ```json @@ -2327,9 +2372,10 @@ https://api.starzh.com/v1 "timestamp": 1640995200000, // 时间戳 "requestId": "xxxx-xxxx-xxxx" // 请求ID,用于追踪 } -``` +```` ### 统一错误码规范 + - 0: 成功 - 400xx: 客户端错误(参数错误、验证失败等) - 401xx: 认证错误 @@ -2345,46 +2391,55 @@ https://api.starzh.com/v1 ### 2.1 用户注册 #### 需求描述 + 允许新用户通过邮箱注册账号,需要邮箱验证激活。支持基本的用户名、密码规则校验。 #### 接口名称 + 用户注册接口 #### 设计原因 + - 提供用户自主注册功能 - 通过邮箱验证确保用户真实性 - 防止恶意注册和垃圾账号 #### 请求类型 + POST #### 接口路径 + `/auth/register` #### 接口参数 + ```typescript { - username: string; // 用户名,3-50字符,字母开头,仅包含字母数字下划线 - email: string; // 邮箱地址,需符合邮箱格式 - password: string; // 密码,8-100字符,必须包含大小写字母和数字 - confirmPassword: string; // 确认密码,需与password一致 - captcha: string; // 图形验证码 - captchaId: string; // 验证码ID + username: string; // 用户名,3-50字符,字母开头,仅包含字母数字下划线 + email: string; // 邮箱地址,需符合邮箱格式 + password: string; // 密码,8-100字符,必须包含大小写字母和数字 + confirmPassword: string; // 确认密码,需与password一致 + captcha: string; // 图形验证码 + captchaId: string; // 验证码ID } ``` **参数约束原因**: + - username: 限制长度和字符类型,防止特殊字符引起的安全问题 - password: 强密码策略,提高账户安全性 - captcha: 防止机器人批量注册 #### 响应分类 + - 成功:201 Created - 注册成功,发送激活邮件 - 失败: - - 400 Bad Request - 参数验证失败 - - 409 Conflict - 用户名或邮箱已存在 + - 400 Bad Request - 参数验证失败 + - 409 Conflict - 用户名或邮箱已存在 #### 响应格式 + ```json // 成功响应 { @@ -2407,14 +2462,15 @@ POST ``` #### 开发思路 + 1. 参数验证层:使用Elysia的t验证器进行参数校验 2. 业务逻辑层: - - 验证图形验证码 - - 检查用户名和邮箱唯一性 - - 密码使用bcrypt加密(成本因子12) - - 生成用户记录(状态为inactive) - - 生成激活token并存入Redis(24小时过期) - - 发送激活邮件(异步队列) + - 验证图形验证码 + - 检查用户名和邮箱唯一性 + - 密码使用bcrypt加密(成本因子12) + - 生成用户记录(状态为inactive) + - 生成激活token并存入Redis(24小时过期) + - 发送激活邮件(异步队列) 3. 数据访问层:使用事务确保数据一致性 4. 日志记录:记录注册操作到sys_operation_logs @@ -2423,23 +2479,29 @@ POST ### 2.2 用户登录 #### 需求描述 + 支持用户名或邮箱登录,实现登录失败次数限制,防止暴力破解。 #### 接口名称 + 用户登录接口 #### 设计原因 + - 提供安全的身份认证机制 - 支持多种登录方式提升用户体验 - 防止暴力破解攻击 #### 请求类型 + POST #### 接口路径 + `/auth/login` #### 接口参数 + ```typescript { account: string; // 用户名或邮箱 @@ -2451,83 +2513,93 @@ POST ``` #### 响应分类 + - 成功:200 OK - 登录成功,返回token - 失败: - - 401 Unauthorized - 用户名或密码错误 - - 423 Locked - 账号被锁定 + - 401 Unauthorized - 用户名或密码错误 + - 423 Locked - 账号被锁定 #### 响应格式 + ```json // 成功响应 { - "code": 0, - "message": "登录成功", - "data": { - "token": "eyJhbGciOiJIUzI1NiIs...", - "refreshToken": "xxxx-xxxx-xxxx", - "expiresIn": 7200, - "user": { - "id": 1234567890, - "username": "john_doe", - "nickname": "John", - "avatar": "https://...", - "roles": ["user"] + "code": 0, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiIs...", + "refreshToken": "xxxx-xxxx-xxxx", + "expiresIn": 7200, + "user": { + "id": 1234567890, + "username": "john_doe", + "nickname": "John", + "avatar": "https://...", + "roles": ["user"] + } } - } } ``` #### 开发思路 + 1. 查询用户(支持username或email) 2. 检查账号状态和锁定状态 3. 验证密码 4. 处理登录失败: - - 增加失败次数 - - 超过5次锁定30分钟 - - 3次失败后要求验证码 + - 增加失败次数 + - 超过5次锁定30分钟 + - 3次失败后要求验证码 5. 登录成功: - - 清除失败次数 - - 生成JWT token - - 更新最后登录信息 - - 记录登录日志 + - 清除失败次数 + - 生成JWT token + - 更新最后登录信息 + - 记录登录日志 --- ### 2.3 刷新Token #### 需求描述 + 使用refreshToken换取新的accessToken,实现无感续期。 #### 接口名称 + Token刷新接口 #### 设计原因 + - 短期accessToken + 长期refreshToken模式 - 提高安全性,减少token泄露风险 - 实现用户无感续期体验 #### 请求类型 + POST #### 接口路径 + `/auth/refresh` #### 接口参数 + ```typescript { - refreshToken: string; // 刷新令牌 + refreshToken: string; // 刷新令牌 } ``` #### 响应格式 + ```json { - "code": 0, - "message": "刷新成功", - "data": { - "token": "new-access-token", - "expiresIn": 7200 - } + "code": 0, + "message": "刷新成功", + "data": { + "token": "new-access-token", + "expiresIn": 7200 + } } ``` @@ -2538,84 +2610,92 @@ POST ### 3.1 获取当前用户信息 #### 需求描述 + 获取当前登录用户的详细信息,包括基本信息、角色、权限、组织等。 #### 接口名称 + 获取当前用户信息接口 #### 设计原因 + - 前端需要用户信息进行界面渲染 - 权限控制需要用户角色和权限信息 - 统一的用户信息获取入口 #### 请求类型 + GET #### 接口路径 + `/users/me` #### 请求头 + ``` Authorization: Bearer ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "id": 1234567890, - "username": "john_doe", - "email": "john@example.com", - "mobile": "13800138000", - "nickname": "John", - "avatar": "https://...", - "gender": 1, - "birthday": "1990-01-01", - "bio": "Hello world", - "status": "active", - "lastLoginAt": "2024-01-01T12:00:00Z", - "roles": [ - { - "id": 1, - "code": "admin", - "name": "管理员" - } - ], - "permissions": [ - { - "code": "user:read", - "name": "查看用户", - "type": "api" - } - ], - "organizations": [ - { - "id": 1, - "name": "技术部", - "isPrimary": true, - "position": "工程师" - } - ], - "tags": [ - { - "id": 1, - "name": "VIP", - "color": "#ff4d4f" - } - ] - } + "code": 0, + "message": "success", + "data": { + "id": 1234567890, + "username": "john_doe", + "email": "john@example.com", + "mobile": "13800138000", + "nickname": "John", + "avatar": "https://...", + "gender": 1, + "birthday": "1990-01-01", + "bio": "Hello world", + "status": "active", + "lastLoginAt": "2024-01-01T12:00:00Z", + "roles": [ + { + "id": 1, + "code": "admin", + "name": "管理员" + } + ], + "permissions": [ + { + "code": "user:read", + "name": "查看用户", + "type": "api" + } + ], + "organizations": [ + { + "id": 1, + "name": "技术部", + "isPrimary": true, + "position": "工程师" + } + ], + "tags": [ + { + "id": 1, + "name": "VIP", + "color": "#ff4d4f" + } + ] + } } ``` #### 开发思路 + 1. 从JWT token中获取用户ID 2. 查询用户基本信息 3. 并行查询关联信息: - - 用户角色(包含权限快照) - - 用户组织 - - 用户标签 + - 用户角色(包含权限快照) + - 用户组织 + - 用户标签 4. 整合权限信息(角色权限去重) 5. 使用Redis缓存(5分钟过期) @@ -2624,23 +2704,29 @@ Authorization: Bearer ### 3.2 用户列表查询 #### 需求描述 + 分页查询用户列表,支持多条件筛选和排序。 #### 接口名称 + 用户列表查询接口 #### 设计原因 + - 管理员需要查看和管理所有用户 - 支持灵活的查询条件满足不同场景 - 分页设计避免大数据量查询 #### 请求类型 + GET #### 接口路径 + `/users` #### 接口参数 + ```typescript // Query Parameters { @@ -2659,45 +2745,47 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "items": [ - { - "id": 1234567890, - "username": "john_doe", - "email": "john@example.com", - "mobile": "138****8000", - "nickname": "John", - "avatar": "https://...", - "status": "active", - "roles": ["admin", "user"], - "organizations": ["技术部"], - "tags": ["VIP"], - "loginCount": 100, - "lastLoginAt": "2024-01-01T12:00:00Z", - "createdAt": "2023-01-01T00:00:00Z" - } - ], - "pagination": { - "page": 1, - "pageSize": 20, - "total": 100, - "totalPages": 5 + "code": 0, + "message": "success", + "data": { + "items": [ + { + "id": 1234567890, + "username": "john_doe", + "email": "john@example.com", + "mobile": "138****8000", + "nickname": "John", + "avatar": "https://...", + "status": "active", + "roles": ["admin", "user"], + "organizations": ["技术部"], + "tags": ["VIP"], + "loginCount": 100, + "lastLoginAt": "2024-01-01T12:00:00Z", + "createdAt": "2023-01-01T00:00:00Z" + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 100, + "totalPages": 5 + } } - } } ``` #### 开发思路 + 1. 参数验证和权限检查(需要user:read权限) 2. 构建查询条件: - - 关键词模糊查询 - - 状态精确匹配 - - 关联表过滤(角色、组织、标签) - - 时间范围查询 + - 关键词模糊查询 + - 状态精确匹配 + - 关联表过滤(角色、组织、标签) + - 时间范围查询 3. 执行分页查询(使用索引优化) 4. 批量查询关联信息(减少N+1查询) 5. 数据脱敏(手机号中间4位) @@ -2707,23 +2795,29 @@ GET ### 3.3 创建用户 #### 需求描述 + 管理员创建新用户,可直接设置角色、组织等信息。 #### 接口名称 + 创建用户接口 #### 设计原因 + - 管理员需要批量创建用户 - 可预设用户角色和组织 - 支持直接激活状态 #### 请求类型 + POST #### 接口路径 + `/users` #### 接口参数 + ```typescript { username: string; @@ -2747,13 +2841,14 @@ POST ``` #### 开发思路 + 1. 权限检查(需要user:create权限) 2. 参数验证(唯一性检查) 3. 使用事务处理: - - 创建用户记录 - - 分配角色 - - 分配组织 - - 分配标签 + - 创建用户记录 + - 分配角色 + - 分配组织 + - 分配标签 4. 发送通知邮件(可选) 5. 记录操作日志 @@ -2762,23 +2857,29 @@ POST ### 3.4 更新用户信息 #### 需求描述 + 更新用户基本信息和关联信息。 #### 接口名称 + 更新用户信息接口 #### 设计原因 + - 支持用户自主修改个人信息 - 管理员可修改用户所有信息 - 分离基本信息和关联信息更新 #### 请求类型 + PUT #### 接口路径 + `/users/{userId}` #### 接口参数 + ```typescript { email?: string; @@ -2801,9 +2902,10 @@ PUT ``` #### 开发思路 + 1. 权限检查: - - 用户可修改自己的基本信息 - - 管理员可修改所有信息 + - 用户可修改自己的基本信息 + - 管理员可修改所有信息 2. 参数验证(邮箱唯一性等) 3. 使用乐观锁防止并发冲突 4. 更新关联信息时先删后增 @@ -2814,27 +2916,33 @@ PUT ### 3.5 重置密码 #### 需求描述 + 用户忘记密码时通过邮箱重置。 #### 接口名称 + 申请重置密码接口 #### 请求类型 + POST #### 接口路径 + `/users/password/reset-request` #### 接口参数 + ```typescript { - email: string; - captcha: string; - captchaId: string; + email: string; + captcha: string; + captchaId: string; } ``` #### 开发思路 + 1. 验证邮箱存在 2. 生成重置token(6位数字或链接) 3. 存入Redis(30分钟过期) @@ -2848,23 +2956,29 @@ POST ### 4.1 角色树查询 #### 需求描述 + 查询角色树形结构,支持权限继承展示。 #### 接口名称 + 角色树查询接口 #### 设计原因 + - 角色具有层级关系需要树形展示 - 支持权限继承机制 - 便于理解角色体系 #### 请求类型 + GET #### 接口路径 + `/roles/tree` #### 接口参数 + ```typescript { status?: string; // 过滤状态 @@ -2873,34 +2987,36 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "super_admin", - "name": "超级管理员", - "description": "拥有所有权限", - "status": "active", - "isSystem": true, - "permissionCount": 50, - "children": [ + "code": 0, + "message": "success", + "data": [ { - "id": 2, - "code": "admin", - "name": "管理员", - "pid": 1, - "children": [] + "id": 1, + "code": "super_admin", + "name": "超级管理员", + "description": "拥有所有权限", + "status": "active", + "isSystem": true, + "permissionCount": 50, + "children": [ + { + "id": 2, + "code": "admin", + "name": "管理员", + "pid": 1, + "children": [] + } + ] } - ] - } - ] + ] } ``` #### 开发思路 + 1. 查询所有角色(使用path字段优化) 2. 构建树形结构(递归或迭代) 3. 统计每个角色的权限数量 @@ -2911,23 +3027,29 @@ GET ### 4.2 创建角色 #### 需求描述 + 创建新角色并分配权限,支持角色继承。 #### 接口名称 + 创建角色接口 #### 设计原因 + - 灵活的角色管理支持业务扩展 - 权限继承减少重复配置 - 下级角色权限不能超过上级 #### 请求类型 + POST #### 接口路径 + `/roles` #### 接口参数 + ```typescript { code: string; // 角色代码,唯一 @@ -2940,32 +3062,34 @@ POST ``` #### 响应格式 + ```json { - "code": 0, - "message": "创建成功", - "data": { - "id": 10, - "code": "editor", - "name": "编辑员", - "path": "/1/2/10/", - "level": 3 - } + "code": 0, + "message": "创建成功", + "data": { + "id": 10, + "code": "editor", + "name": "编辑员", + "path": "/1/2/10/", + "level": 3 + } } ``` #### 开发思路 + 1. 权限检查(需要role:create权限) 2. 验证角色代码唯一性 3. 验证父角色存在性 4. 检查权限分配合法性: - - 获取父角色所有权限 - - 确保新角色权限是父角色权限的子集 + - 获取父角色所有权限 + - 确保新角色权限是父角色权限的子集 5. 使用事务创建: - - 创建角色记录 - - 计算并设置path和level - - 分配权限 - - 生成权限快照 + - 创建角色记录 + - 计算并设置path和level + - 分配权限 + - 生成权限快照 6. 清除角色缓存 --- @@ -2973,23 +3097,29 @@ POST ### 4.3 更新角色权限 #### 需求描述 + 动态调整角色权限,支持批量操作。 #### 接口名称 + 更新角色权限接口 #### 设计原因 + - 业务变化需要调整权限 - 支持灵活的权限管理 - 保证权限继承一致性 #### 请求类型 + PUT #### 接口路径 + `/roles/{roleId}/permissions` #### 接口参数 + ```typescript { permissionIds: number[]; // 新的权限ID数组(全量) @@ -2997,13 +3127,14 @@ PUT ``` #### 开发思路 + 1. 检查角色是否为系统角色 2. 验证权限合法性(不超过父角色) 3. 对比新旧权限,计算差异 4. 使用事务更新: - - 删除旧权限关联 - - 创建新权限关联 - - 更新权限快照 + - 删除旧权限关联 + - 创建新权限关联 + - 更新权限快照 5. 级联更新子角色(移除超出的权限) 6. 清除相关用户的权限缓存 @@ -3014,23 +3145,29 @@ PUT ### 5.1 权限树查询 #### 需求描述 + 查询权限树形结构,按类型分组展示。 #### 接口名称 + 权限树查询接口 #### 设计原因 + - 权限具有层级关系(菜单-页面-按钮) - 便于权限分配时的选择 - 支持不同类型权限的管理 #### 请求类型 + GET #### 接口路径 + `/permissions/tree` #### 接口参数 + ```typescript { type?: string; // 权限类型:menu,button,api,data @@ -3039,40 +3176,42 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "system", - "name": "系统管理", - "type": "menu", - "children": [ + "code": 0, + "message": "success", + "data": [ { - "id": 2, - "code": "system:user", - "name": "用户管理", - "type": "menu", - "children": [ - { - "id": 3, - "code": "user:read", - "name": "查看用户", - "type": "api", - "resource": "user", - "action": "read" - } - ] + "id": 1, + "code": "system", + "name": "系统管理", + "type": "menu", + "children": [ + { + "id": 2, + "code": "system:user", + "name": "用户管理", + "type": "menu", + "children": [ + { + "id": 3, + "code": "user:read", + "name": "查看用户", + "type": "api", + "resource": "user", + "action": "read" + } + ] + } + ] } - ] - } - ] + ] } ``` #### 开发思路 + 1. 查询权限列表 2. 按类型分组 3. 构建树形结构 @@ -3083,38 +3222,45 @@ GET ### 5.2 批量创建权限 #### 需求描述 + 支持批量导入权限配置。 #### 接口名称 + 批量创建权限接口 #### 设计原因 + - 新模块上线需要批量添加权限 - 提高配置效率 - 支持权限模板导入 #### 请求类型 + POST #### 接口路径 + `/permissions/batch` #### 接口参数 + ```typescript { - permissions: Array<{ - code: string; - name: string; - type: string; - resource?: string; - action?: string; - pid?: number; - description?: string; - }> + permissions: Array<{ + code: string; + name: string; + type: string; + resource?: string; + action?: string; + pid?: number; + description?: string; + }>; } ``` #### 开发思路 + 1. 批量验证权限代码唯一性 2. 验证父权限存在性 3. 使用事务批量插入 @@ -3127,23 +3273,29 @@ POST ### 6.1 组织树查询 #### 需求描述 + 查询组织架构树,支持人员统计。 #### 接口名称 + 组织架构树查询接口 #### 设计原因 + - 直观展示组织层级关系 - 便于用户分配和管理 - 支持组织人员统计 #### 请求类型 + GET #### 接口路径 + `/organizations/tree` #### 接口参数 + ```typescript { type?: string; // 组织类型 @@ -3153,42 +3305,44 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "ROOT", - "name": "星撰集团", - "type": "company", - "userCount": 100, - "children": [ + "code": 0, + "message": "success", + "data": [ { - "id": 2, - "code": "TECH", - "name": "技术部", - "type": "department", - "userCount": 30, - "leader": { - "id": 123, - "name": "张三" - }, - "children": [] + "id": 1, + "code": "ROOT", + "name": "星撰集团", + "type": "company", + "userCount": 100, + "children": [ + { + "id": 2, + "code": "TECH", + "name": "技术部", + "type": "department", + "userCount": 30, + "leader": { + "id": 123, + "name": "张三" + }, + "children": [] + } + ] } - ] - } - ] + ] } ``` #### 开发思路 + 1. 查询组织列表 2. 构建树形结构 3. 如需统计: - - 批量查询用户组织关联 - - 聚合统计各组织人数 + - 批量查询用户组织关联 + - 聚合统计各组织人数 4. 查询负责人信息 5. 使用Redis缓存 @@ -3197,23 +3351,29 @@ GET ### 6.2 调整组织架构 #### 需求描述 + 支持拖拽调整组织层级关系。 #### 接口名称 + 调整组织架构接口 #### 设计原因 + - 组织架构会随业务发展调整 - 支持灵活的组织变更 - 保持数据完整性 #### 请求类型 + PUT #### 接口路径 + `/organizations/{organizationId}/move` #### 接口参数 + ```typescript { targetPid: number; // 目标父组织ID @@ -3222,13 +3382,14 @@ PUT ``` #### 开发思路 + 1. 验证目标组织存在性 2. 检查是否会形成循环 3. 使用事务更新: - - 更新pid - - 重新计算path和level - - 更新所有子组织的path - - 调整排序 + - 更新pid + - 重新计算path和level + - 更新所有子组织的path + - 调整排序 4. 记录组织变更日志 --- @@ -3238,43 +3399,49 @@ PUT ### 7.1 字典类型树查询 #### 需求描述 + 查询字典类型的树形结构,支持分类管理。 #### 接口名称 + 字典类型树查询接口 #### 设计原因 + - 字典类型支持分类管理 - 便于字典的组织和查找 - 区分系统字典和业务字典 #### 请求类型 + GET #### 接口路径 + `/dictionaries/types/tree` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "system", - "name": "系统字典", - "isSystem": true, - "children": [ + "code": 0, + "message": "success", + "data": [ { - "id": 10, - "code": "user_status", - "name": "用户状态", - "itemCount": 4 + "id": 1, + "code": "system", + "name": "系统字典", + "isSystem": true, + "children": [ + { + "id": 10, + "code": "user_status", + "name": "用户状态", + "itemCount": 4 + } + ] } - ] - } - ] + ] } ``` @@ -3283,23 +3450,29 @@ GET ### 7.2 获取字典项 #### 需求描述 + 根据字典类型获取所有字典项,支持树形字典。 #### 接口名称 + 获取字典项接口 #### 设计原因 + - 前端下拉框等组件需要字典数据 - 支持动态配置 - 减少硬编码 #### 请求类型 + GET #### 接口路径 + `/dictionaries/items` #### 接口参数 + ```typescript { typeCode: string; // 字典类型代码 @@ -3308,23 +3481,25 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "key": "active", - "value": "active", - "label": "正常", - "color": "#52c41a", - "sortOrder": 1 - } - ] + "code": 0, + "message": "success", + "data": [ + { + "key": "active", + "value": "active", + "label": "正常", + "color": "#52c41a", + "sortOrder": 1 + } + ] } ``` #### 开发思路 + 1. 根据typeCode查询字典类型 2. 查询该类型下的所有字典项 3. 如果是树形字典,构建树结构 @@ -3335,48 +3510,55 @@ GET ### 7.3 批量更新字典项 #### 需求描述 + 支持批量更新字典项,保持顺序。 #### 接口名称 + 批量更新字典项接口 #### 设计原因 + - 字典项通常需要批量维护 - 保持显示顺序的一致性 - 提高维护效率 #### 请求类型 + PUT #### 接口路径 + `/dictionaries/types/{typeId}/items/batch` #### 接口参数 + ```typescript { - items: Array<{ - id?: number; // 有ID为更新,无ID为新增 - key: string; - value: string; - label: string; - labelEn?: string; - pid?: number; - sortOrder: number; - status?: string; - color?: string; - extra?: object; - }> + items: Array<{ + id?: number; // 有ID为更新,无ID为新增 + key: string; + value: string; + label: string; + labelEn?: string; + pid?: number; + sortOrder: number; + status?: string; + color?: string; + extra?: object; + }>; } ``` #### 开发思路 + 1. 验证字典类型存在 2. 检查是否为系统字典 3. 验证key在类型内唯一 4. 使用事务处理: - - 标记删除不在列表中的项 - - 更新已存在的项 - - 创建新项 + - 标记删除不在列表中的项 + - 更新已存在的项 + - 创建新项 5. 清除字典缓存 --- @@ -3386,23 +3568,29 @@ PUT ### 8.1 标签列表查询 #### 需求描述 + 查询标签列表,支持按类型和使用频率排序。 #### 接口名称 + 标签列表查询接口 #### 设计原因 + - 便于标签的统一管理 - 支持热门标签展示 - 不同类型标签分开管理 #### 请求类型 + GET #### 接口路径 + `/tags` #### 接口参数 + ```typescript { type?: string; // 标签类型 @@ -3414,27 +3602,28 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "items": [ - { - "id": 1, - "name": "VIP", - "type": "user", - "color": "#ff4d4f", - "usageCount": 100, - "description": "VIP用户" - } - ], - "pagination": { - "page": 1, - "pageSize": 20, - "total": 50 + "code": 0, + "message": "success", + "data": { + "items": [ + { + "id": 1, + "name": "VIP", + "type": "user", + "color": "#ff4d4f", + "usageCount": 100, + "description": "VIP用户" + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 50 + } } - } } ``` @@ -3443,36 +3632,43 @@ GET ### 8.2 标签智能推荐 #### 需求描述 + 根据用户特征智能推荐标签。 #### 接口名称 + 标签推荐接口 #### 设计原因 + - 提高标签使用的准确性 - 基于规则引擎自动打标 - 减少人工维护成本 #### 请求类型 + POST #### 接口路径 + `/tags/recommend` #### 接口参数 + ```typescript { - userId: number; - type: string; // 推荐类型 + userId: number; + type: string; // 推荐类型 } ``` #### 开发思路 + 1. 获取用户特征数据 2. 执行推荐规则: - - 活跃度规则(登录次数、最后登录时间) - - 贡献度规则(内容创作数量) - - 消费规则(VIP等级) + - 活跃度规则(登录次数、最后登录时间) + - 贡献度规则(内容创作数量) + - 消费规则(VIP等级) 3. 返回推荐标签列表 4. 记录推荐日志 @@ -3483,26 +3679,31 @@ POST ### 9.1 图形验证码 #### 需求描述 + 生成图形验证码,用于注册、登录等场景。 #### 接口名称 + 获取图形验证码接口 #### 请求类型 + GET #### 接口路径 + `/captcha` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "captchaId": "xxxx-xxxx-xxxx", - "captchaImage": "data:image/svg+xml;base64,..." - } + "code": 0, + "message": "success", + "data": { + "captchaId": "xxxx-xxxx-xxxx", + "captchaImage": "data:image/svg+xml;base64,..." + } } ``` @@ -3511,38 +3712,45 @@ GET ### 9.2 文件上传 #### 需求描述 + 通用文件上传接口,支持头像等文件上传。 #### 接口名称 + 文件上传接口 #### 请求类型 + POST #### 接口路径 + `/upload` #### 请求格式 + multipart/form-data #### 接口参数 + ```typescript { - file: File; // 文件 - type: string; // 文件类型:avatar,document + file: File; // 文件 + type: string; // 文件类型:avatar,document } ``` #### 响应格式 + ```json { - "code": 0, - "message": "上传成功", - "data": { - "url": "https://oss.starzh.com/xxx.jpg", - "size": 102400, - "name": "avatar.jpg" - } + "code": 0, + "message": "上传成功", + "data": { + "url": "https://oss.starzh.com/xxx.jpg", + "size": 102400, + "name": "avatar.jpg" + } } ``` @@ -3551,17 +3759,20 @@ multipart/form-data ## 10. 安全性设计 ### 10.1 认证机制 + - JWT Token认证 - Token有效期2小时 - RefreshToken有效期7天 - 支持Token黑名单 ### 10.2 权限控制 + - 基于RBAC的权限模型 - 接口级别的权限控制 - 数据级别的权限过滤 ### 10.3 安全措施 + - 所有接口HTTPS加密传输 - 敏感操作二次验证 - SQL注入防护(参数化查询) @@ -3571,6 +3782,7 @@ multipart/form-data - IP黑白名单 ### 10.4 日志审计 + - 完整的操作日志 - 敏感操作告警 - 异常行为检测 @@ -3580,18 +3792,21 @@ multipart/form-data ## 11. 性能优化 ### 11.1 缓存策略 + - Redis缓存用户信息(5分钟) - 角色权限缓存(变更时失效) - 字典数据缓存(1小时) - HTTP缓存头优化 ### 11.2 查询优化 + - 合理使用数据库索引 - 避免N+1查询问题 - 大数据量分页查询 - 异步处理耗时操作 ### 11.3 并发控制 + - 接口限流(令牌桶) - 数据库连接池 - 异步任务队列 @@ -3603,35 +3818,36 @@ multipart/form-data ### 12.1 错误码规范 -| 错误码 | HTTP状态码 | 说明 | -|--------|------------|------| -| 0 | 200 | 成功 | -| 40001 | 400 | 参数错误 | -| 40002 | 400 | 参数格式错误 | -| 40003 | 400 | 参数超出范围 | -| 40101 | 401 | 未登录 | -| 40102 | 401 | Token过期 | -| 40103 | 401 | Token无效 | -| 40301 | 403 | 无权限 | -| 40302 | 403 | 账号被禁用 | -| 40401 | 404 | 资源不存在 | -| 40901 | 409 | 资源已存在 | -| 42301 | 423 | 账号被锁定 | -| 50001 | 500 | 服务器内部错误 | -| 50002 | 500 | 数据库错误 | -| 50003 | 503 | 服务不可用 | +| 错误码 | HTTP状态码 | 说明 | +| ------ | ---------- | -------------- | +| 0 | 200 | 成功 | +| 40001 | 400 | 参数错误 | +| 40002 | 400 | 参数格式错误 | +| 40003 | 400 | 参数超出范围 | +| 40101 | 401 | 未登录 | +| 40102 | 401 | Token过期 | +| 40103 | 401 | Token无效 | +| 40301 | 403 | 无权限 | +| 40302 | 403 | 账号被禁用 | +| 40401 | 404 | 资源不存在 | +| 40901 | 409 | 资源已存在 | +| 42301 | 423 | 账号被锁定 | +| 50001 | 500 | 服务器内部错误 | +| 50002 | 500 | 数据库错误 | +| 50003 | 503 | 服务不可用 | ### 12.2 错误响应示例 + ```json { - "code": 40001, - "message": "用户名不能为空", - "data": { - "field": "username", - "value": null - }, - "timestamp": 1640995200000, - "requestId": "xxxx-xxxx-xxxx" + "code": 40001, + "message": "用户名不能为空", + "data": { + "field": "username", + "value": null + }, + "timestamp": 1640995200000, + "requestId": "xxxx-xxxx-xxxx" } ``` @@ -3640,12 +3856,14 @@ multipart/form-data ## 13. 接口版本管理 ### 13.1 版本策略 + - URL路径版本化:/v1, /v2 - 向后兼容原则 - 废弃接口提前通知 - 版本迁移指南 ### 13.2 版本示例 + ``` /v1/users (当前版本) /v2/users (新版本,结构优化) @@ -3656,11 +3874,13 @@ multipart/form-data ## 14. 开发规范总结 ### 14.1 命名规范 + - RESTful资源命名使用复数 - 路径使用小写字母和连字符 - 查询参数使用驼峰命名 ### 14.2 状态码使用 + - 200: 成功 - 201: 创建成功 - 204: 无内容(删除成功) @@ -3672,6 +3892,7 @@ multipart/form-data - 500: 服务器错误 ### 14.3 最佳实践 + - 幂等性设计(PUT/DELETE) - 分页查询限制最大条数 - 敏感数据脱敏 @@ -3679,7 +3900,8 @@ multipart/form-data - 合理的缓存策略 - 完善的错误处理 - 详细的日志记录 -``` + +```` ```markdown # M2 - 基础用户系统 - 详细接口设计 @@ -3695,9 +3917,11 @@ multipart/form-data - **版本控制**:API版本化管理 ### 基础URL -``` +```` + https://api.starzh.com/v1 -``` + +```` ### 统一响应格式 ```json @@ -3708,9 +3932,10 @@ https://api.starzh.com/v1 "timestamp": 1640995200000, // 时间戳 "requestId": "xxxx-xxxx-xxxx" // 请求ID,用于追踪 } -``` +```` ### 统一错误码规范 + - 0: 成功 - 400xx: 客户端错误(参数错误、验证失败等) - 401xx: 认证错误 @@ -3726,46 +3951,55 @@ https://api.starzh.com/v1 ### 2.1 用户注册 #### 需求描述 + 允许新用户通过邮箱注册账号,需要邮箱验证激活。支持基本的用户名、密码规则校验。 #### 接口名称 + 用户注册接口 #### 设计原因 + - 提供用户自主注册功能 - 通过邮箱验证确保用户真实性 - 防止恶意注册和垃圾账号 #### 请求类型 + POST #### 接口路径 + `/auth/register` #### 接口参数 + ```typescript { - username: string; // 用户名,3-50字符,字母开头,仅包含字母数字下划线 - email: string; // 邮箱地址,需符合邮箱格式 - password: string; // 密码,8-100字符,必须包含大小写字母和数字 - confirmPassword: string; // 确认密码,需与password一致 - captcha: string; // 图形验证码 - captchaId: string; // 验证码ID + username: string; // 用户名,3-50字符,字母开头,仅包含字母数字下划线 + email: string; // 邮箱地址,需符合邮箱格式 + password: string; // 密码,8-100字符,必须包含大小写字母和数字 + confirmPassword: string; // 确认密码,需与password一致 + captcha: string; // 图形验证码 + captchaId: string; // 验证码ID } ``` **参数约束原因**: + - username: 限制长度和字符类型,防止特殊字符引起的安全问题 - password: 强密码策略,提高账户安全性 - captcha: 防止机器人批量注册 #### 响应分类 + - 成功:201 Created - 注册成功,发送激活邮件 - 失败: - - 400 Bad Request - 参数验证失败 - - 409 Conflict - 用户名或邮箱已存在 + - 400 Bad Request - 参数验证失败 + - 409 Conflict - 用户名或邮箱已存在 #### 响应格式 + ```json // 成功响应 { @@ -3788,14 +4022,15 @@ POST ``` #### 开发思路 + 1. 参数验证层:使用Elysia的t验证器进行参数校验 2. 业务逻辑层: - - 验证图形验证码 - - 检查用户名和邮箱唯一性 - - 密码使用bcrypt加密(成本因子12) - - 生成用户记录(状态为inactive) - - 生成激活token并存入Redis(24小时过期) - - 发送激活邮件(异步队列) + - 验证图形验证码 + - 检查用户名和邮箱唯一性 + - 密码使用bcrypt加密(成本因子12) + - 生成用户记录(状态为inactive) + - 生成激活token并存入Redis(24小时过期) + - 发送激活邮件(异步队列) 3. 数据访问层:使用事务确保数据一致性 4. 日志记录:记录注册操作到sys_operation_logs @@ -3804,23 +4039,29 @@ POST ### 2.2 用户登录 #### 需求描述 + 支持用户名或邮箱登录,实现登录失败次数限制,防止暴力破解。 #### 接口名称 + 用户登录接口 #### 设计原因 + - 提供安全的身份认证机制 - 支持多种登录方式提升用户体验 - 防止暴力破解攻击 #### 请求类型 + POST #### 接口路径 + `/auth/login` #### 接口参数 + ```typescript { account: string; // 用户名或邮箱 @@ -3832,87 +4073,98 @@ POST ``` #### 响应分类 + - 成功:200 OK - 登录成功,返回token - 失败: - - 401 Unauthorized - 用户名或密码错误 - - 423 Locked - 账号被锁定 + - 401 Unauthorized - 用户名或密码错误 + - 423 Locked - 账号被锁定 #### 响应格式 + ```json // 成功响应 { - "code": 0, - "message": "登录成功", - "data": { - "token": "eyJhbGciOiJIUzI1NiIs...", - "refreshToken": "xxxx-xxxx-xxxx", - "expiresIn": 7200, - "user": { - "id": 1234567890, - "username": "john_doe", - "nickname": "John", - "avatar": "https://...", - "roles": ["user"] + "code": 0, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiIs...", + "refreshToken": "xxxx-xxxx-xxxx", + "expiresIn": 7200, + "user": { + "id": 1234567890, + "username": "john_doe", + "nickname": "John", + "avatar": "https://...", + "roles": ["user"] + } } - } } ``` #### 开发思路 + 1. 查询用户(支持username或email) 2. 检查账号状态和锁定状态 3. 验证密码 4. 处理登录失败: - - 增加失败次数 - - 超过5次锁定30分钟 - - 3次失败后要求验证码 + - 增加失败次数 + - 超过5次锁定30分钟 + - 3次失败后要求验证码 5. 登录成功: - - 清除失败次数 - - 生成JWT token - - 更新最后登录信息 - - 记录登录日志 + - 清除失败次数 + - 生成JWT token + - 更新最后登录信息 + - 记录登录日志 --- ### 2.3 刷新Token #### 需求描述 + 使用refreshToken换取新的accessToken,实现无感续期。 #### 接口名称 + Token刷新接口 #### 设计原因 + - 短期accessToken + 长期refreshToken模式 - 提高安全性,减少token泄露风险 - 实现用户无感续期体验 #### 请求类型 + POST #### 接口路径 + `/auth/refresh` #### 接口参数 + ```typescript { - refreshToken: string; // 刷新令牌 + refreshToken: string; // 刷新令牌 } ``` #### 响应格式 + ```json { - "code": 0, - "message": "刷新成功", - "data": { - "token": "new-access-token", - "expiresIn": 7200 - } + "code": 0, + "message": "刷新成功", + "data": { + "token": "new-access-token", + "expiresIn": 7200 + } } ``` #### 开发思路 + 1. 验证refreshToken有效性 2. 检查用户状态 3. 生成新的accessToken @@ -3924,46 +4176,54 @@ POST ### 2.4 账号激活 #### 需求描述 + 用户通过邮件中的链接或验证码激活账号。 #### 接口名称 + 账号激活接口 #### 设计原因 + - 验证邮箱真实性 - 防止垃圾注册 - 提高用户质量 #### 请求类型 + POST #### 接口路径 + `/auth/activate` #### 接口参数 + ```typescript { - token: string; // 激活令牌(从邮件链接获取) - // 或 - email: string; // 邮箱 - code: string; // 6位数字激活码 + token: string; // 激活令牌(从邮件链接获取) + // 或 + email: string; // 邮箱 + code: string; // 6位数字激活码 } ``` #### 响应格式 + ```json { - "code": 0, - "message": "账号激活成功", - "data": { - "userId": 1234567890, - "username": "john_doe", - "status": "active" - } + "code": 0, + "message": "账号激活成功", + "data": { + "userId": 1234567890, + "username": "john_doe", + "status": "active" + } } ``` #### 开发思路 + 1. 验证激活token或激活码 2. 检查是否过期 3. 更新用户状态为active @@ -3976,42 +4236,51 @@ POST ### 2.5 退出登录 #### 需求描述 + 用户主动退出登录,清除token。 #### 接口名称 + 退出登录接口 #### 设计原因 + - 用户主动结束会话 - 清除服务端缓存 - 增强安全性 #### 请求类型 + POST #### 接口路径 + `/auth/logout` #### 请求头 + ``` Authorization: Bearer ``` #### 响应格式 + ```json { - "code": 0, - "message": "退出成功", - "data": null + "code": 0, + "message": "退出成功", + "data": null } ``` #### 开发思路 + 1. 获取当前token 2. 加入token黑名单(Redis) 3. 清除用户相关缓存 4. 记录退出日志 -``` + +```` ```markdown // ... existing code ... @@ -4038,9 +4307,11 @@ GET `/users/me` #### 请求头 -``` +```` + Authorization: Bearer -``` + +```` #### 响应格式 ```json @@ -4090,15 +4361,16 @@ Authorization: Bearer ] } } -``` +```` #### 开发思路 + 1. 从JWT token中获取用户ID 2. 查询用户基本信息 3. 并行查询关联信息: - - 用户角色(包含权限快照) - - 用户组织 - - 用户标签 + - 用户角色(包含权限快照) + - 用户组织 + - 用户标签 4. 整合权限信息(角色权限去重) 5. 使用Redis缓存(5分钟过期) @@ -4107,23 +4379,29 @@ Authorization: Bearer ### 3.2 用户列表查询 #### 需求描述 + 分页查询用户列表,支持多条件筛选和排序。 #### 接口名称 + 用户列表查询接口 #### 设计原因 + - 管理员需要查看和管理所有用户 - 支持灵活的查询条件满足不同场景 - 分页设计避免大数据量查询 #### 请求类型 + GET #### 接口路径 + `/users` #### 接口参数 + ```typescript // Query Parameters { @@ -4142,45 +4420,47 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "items": [ - { - "id": 1234567890, - "username": "john_doe", - "email": "john@example.com", - "mobile": "138****8000", - "nickname": "John", - "avatar": "https://...", - "status": "active", - "roles": ["admin", "user"], - "organizations": ["技术部"], - "tags": ["VIP"], - "loginCount": 100, - "lastLoginAt": "2024-01-01T12:00:00Z", - "createdAt": "2023-01-01T00:00:00Z" - } - ], - "pagination": { - "page": 1, - "pageSize": 20, - "total": 100, - "totalPages": 5 + "code": 0, + "message": "success", + "data": { + "items": [ + { + "id": 1234567890, + "username": "john_doe", + "email": "john@example.com", + "mobile": "138****8000", + "nickname": "John", + "avatar": "https://...", + "status": "active", + "roles": ["admin", "user"], + "organizations": ["技术部"], + "tags": ["VIP"], + "loginCount": 100, + "lastLoginAt": "2024-01-01T12:00:00Z", + "createdAt": "2023-01-01T00:00:00Z" + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 100, + "totalPages": 5 + } } - } } ``` #### 开发思路 + 1. 参数验证和权限检查(需要user:read权限) 2. 构建查询条件: - - 关键词模糊查询 - - 状态精确匹配 - - 关联表过滤(角色、组织、标签) - - 时间范围查询 + - 关键词模糊查询 + - 状态精确匹配 + - 关联表过滤(角色、组织、标签) + - 时间范围查询 3. 执行分页查询(使用索引优化) 4. 批量查询关联信息(减少N+1查询) 5. 数据脱敏(手机号中间4位) @@ -4190,23 +4470,29 @@ GET ### 3.3 创建用户 #### 需求描述 + 管理员创建新用户,可直接设置角色、组织等信息。 #### 接口名称 + 创建用户接口 #### 设计原因 + - 管理员需要批量创建用户 - 可预设用户角色和组织 - 支持直接激活状态 #### 请求类型 + POST #### 接口路径 + `/users` #### 接口参数 + ```typescript { username: string; @@ -4230,27 +4516,29 @@ POST ``` #### 响应格式 + ```json { - "code": 0, - "message": "创建成功", - "data": { - "id": 1234567890, - "username": "new_user", - "email": "new@example.com", - "status": "active" - } + "code": 0, + "message": "创建成功", + "data": { + "id": 1234567890, + "username": "new_user", + "email": "new@example.com", + "status": "active" + } } ``` #### 开发思路 + 1. 权限检查(需要user:create权限) 2. 参数验证(唯一性检查) 3. 使用事务处理: - - 创建用户记录 - - 分配角色 - - 分配组织 - - 分配标签 + - 创建用户记录 + - 分配角色 + - 分配组织 + - 分配标签 4. 发送通知邮件(可选) 5. 记录操作日志 @@ -4259,23 +4547,29 @@ POST ### 3.4 更新用户信息 #### 需求描述 + 更新用户基本信息和关联信息。 #### 接口名称 + 更新用户信息接口 #### 设计原因 + - 支持用户自主修改个人信息 - 管理员可修改用户所有信息 - 分离基本信息和关联信息更新 #### 请求类型 + PUT #### 接口路径 + `/users/{userId}` #### 接口参数 + ```typescript { email?: string; @@ -4298,24 +4592,26 @@ PUT ``` #### 响应格式 + ```json { - "code": 0, - "message": "更新成功", - "data": { - "id": 1234567890, - "username": "john_doe", - "email": "john@example.com", - "updatedAt": "2024-01-01T12:00:00Z" - } + "code": 0, + "message": "更新成功", + "data": { + "id": 1234567890, + "username": "john_doe", + "email": "john@example.com", + "updatedAt": "2024-01-01T12:00:00Z" + } } ``` #### 开发思路 + 1. 权限检查: - - 用户可修改自己的基本信息 - - 管理员可修改所有信息 - - root用户信息限制修改 + - 用户可修改自己的基本信息 + - 管理员可修改所有信息 + - root用户信息限制修改 2. 参数验证(邮箱唯一性等) 3. 使用乐观锁防止并发冲突 4. 更新关联信息时先删后增 @@ -4326,41 +4622,49 @@ PUT ### 3.5 修改密码 #### 需求描述 + 用户修改自己的密码,需要验证原密码。 #### 接口名称 + 修改密码接口 #### 设计原因 + - 用户自主管理密码 - 增强账户安全性 - 防止未授权修改 #### 请求类型 + PUT #### 接口路径 + `/users/me/password` #### 接口参数 + ```typescript { - oldPassword: string; // 原密码 - newPassword: string; // 新密码 - confirmPassword: string; // 确认新密码 + oldPassword: string; // 原密码 + newPassword: string; // 新密码 + confirmPassword: string; // 确认新密码 } ``` #### 响应格式 + ```json { - "code": 0, - "message": "密码修改成功", - "data": null + "code": 0, + "message": "密码修改成功", + "data": null } ``` #### 开发思路 + 1. 验证原密码正确性 2. 验证新密码符合规则 3. 更新密码(bcrypt加密) @@ -4372,41 +4676,49 @@ PUT ### 3.6 重置密码 #### 需求描述 + 用户忘记密码时通过邮箱重置。 #### 接口名称 + 申请重置密码接口 #### 设计原因 + - 用户忘记密码的补救措施 - 通过邮箱验证身份 - 防止恶意重置 #### 请求类型 + POST #### 接口路径 + `/users/password/reset-request` #### 接口参数 + ```typescript { - email: string; - captcha: string; - captchaId: string; + email: string; + captcha: string; + captchaId: string; } ``` #### 响应格式 + ```json { - "code": 0, - "message": "重置邮件已发送", - "data": null + "code": 0, + "message": "重置邮件已发送", + "data": null } ``` #### 开发思路 + 1. 验证邮箱存在 2. 验证图形验证码 3. 生成重置token(6位数字或链接) @@ -4419,37 +4731,44 @@ POST ### 3.7 确认重置密码 #### 需求描述 + 使用重置token设置新密码。 #### 接口名称 + 确认重置密码接口 #### 请求类型 + POST #### 接口路径 + `/users/password/reset-confirm` #### 接口参数 + ```typescript { - token: string; // 重置token - email: string; // 邮箱 - newPassword: string; // 新密码 - confirmPassword: string; // 确认密码 + token: string; // 重置token + email: string; // 邮箱 + newPassword: string; // 新密码 + confirmPassword: string; // 确认密码 } ``` #### 响应格式 + ```json { - "code": 0, - "message": "密码重置成功", - "data": null + "code": 0, + "message": "密码重置成功", + "data": null } ``` #### 开发思路 + 1. 验证token有效性和匹配性 2. 验证新密码规则 3. 更新用户密码 @@ -4462,32 +4781,39 @@ POST ### 3.8 删除用户 #### 需求描述 + 软删除用户,保留数据用于审计。 #### 接口名称 + 删除用户接口 #### 设计原因 + - 满足用户注销需求 - 保留数据用于审计 - 支持误删恢复 #### 请求类型 + DELETE #### 接口路径 + `/users/{userId}` #### 响应格式 + ```json { - "code": 0, - "message": "删除成功", - "data": null + "code": 0, + "message": "删除成功", + "data": null } ``` #### 开发思路 + 1. 权限检查(需要user:delete权限) 2. 检查是否为root用户(禁止删除) 3. 检查是否为自己(需要二次确认) @@ -4500,23 +4826,29 @@ DELETE ### 3.9 批量操作用户 #### 需求描述 + 批量启用、禁用、删除用户。 #### 接口名称 + 批量操作用户接口 #### 设计原因 + - 提高管理效率 - 支持批量处理 - 减少重复操作 #### 请求类型 + POST #### 接口路径 + `/users/batch` #### 接口参数 + ```typescript { userIds: number[]; // 用户ID数组 @@ -4525,25 +4857,28 @@ POST ``` #### 响应格式 + ```json { - "code": 0, - "message": "操作成功", - "data": { - "success": 10, - "failed": 0, - "errors": [] - } + "code": 0, + "message": "操作成功", + "data": { + "success": 10, + "failed": 0, + "errors": [] + } } ``` #### 开发思路 + 1. 权限检查 2. 过滤掉root用户 3. 批量执行操作 4. 记录操作结果 5. 返回执行统计 -``` + +```` ```markdown // ... existing code ... @@ -4575,37 +4910,39 @@ GET status?: string; // 过滤状态 withPermissions?: boolean; // 是否包含权限信息 } -``` +```` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "super_admin", - "name": "超级管理员", - "description": "拥有所有权限", - "status": "active", - "isSystem": true, - "permissionCount": 50, - "children": [ + "code": 0, + "message": "success", + "data": [ { - "id": 2, - "code": "admin", - "name": "管理员", - "pid": 1, - "children": [] + "id": 1, + "code": "super_admin", + "name": "超级管理员", + "description": "拥有所有权限", + "status": "active", + "isSystem": true, + "permissionCount": 50, + "children": [ + { + "id": 2, + "code": "admin", + "name": "管理员", + "pid": 1, + "children": [] + } + ] } - ] - } - ] + ] } ``` #### 开发思路 + 1. 查询所有角色(使用path字段优化) 2. 构建树形结构(递归或迭代) 3. 统计每个角色的权限数量 @@ -4616,23 +4953,29 @@ GET ### 4.2 创建角色 #### 需求描述 + 创建新角色并分配权限,支持角色继承。 #### 接口名称 + 创建角色接口 #### 设计原因 + - 灵活的角色管理支持业务扩展 - 权限继承减少重复配置 - 下级角色权限不能超过上级 #### 请求类型 + POST #### 接口路径 + `/roles` #### 接口参数 + ```typescript { code: string; // 角色代码,唯一 @@ -4645,32 +4988,34 @@ POST ``` #### 响应格式 + ```json { - "code": 0, - "message": "创建成功", - "data": { - "id": 10, - "code": "editor", - "name": "编辑员", - "path": "/1/2/10/", - "level": 3 - } + "code": 0, + "message": "创建成功", + "data": { + "id": 10, + "code": "editor", + "name": "编辑员", + "path": "/1/2/10/", + "level": 3 + } } ``` #### 开发思路 + 1. 权限检查(需要role:create权限) 2. 验证角色代码唯一性 3. 验证父角色存在性 4. 检查权限分配合法性: - - 获取父角色所有权限 - - 确保新角色权限是父角色权限的子集 + - 获取父角色所有权限 + - 确保新角色权限是父角色权限的子集 5. 使用事务创建: - - 创建角色记录 - - 计算并设置path和level - - 分配权限 - - 生成权限快照 + - 创建角色记录 + - 计算并设置path和level + - 分配权限 + - 生成权限快照 6. 清除角色缓存 --- @@ -4678,23 +5023,29 @@ POST ### 4.3 更新角色信息 #### 需求描述 + 更新角色基本信息,不包括权限。 #### 接口名称 + 更新角色信息接口 #### 设计原因 + - 角色信息可能需要调整 - 分离基本信息和权限更新 - 防止误操作 #### 请求类型 + PUT #### 接口路径 + `/roles/{roleId}` #### 接口参数 + ```typescript { name?: string; @@ -4705,6 +5056,7 @@ PUT ``` #### 开发思路 + 1. 权限检查(需要role:update权限) 2. 检查是否为系统角色(限制修改) 3. 更新角色信息 @@ -4715,23 +5067,29 @@ PUT ### 4.4 更新角色权限 #### 需求描述 + 动态调整角色权限,支持批量操作。 #### 接口名称 + 更新角色权限接口 #### 设计原因 + - 业务变化需要调整权限 - 支持灵活的权限管理 - 保证权限继承一致性 #### 请求类型 + PUT #### 接口路径 + `/roles/{roleId}/permissions` #### 接口参数 + ```typescript { permissionIds: number[]; // 新的权限ID数组(全量) @@ -4739,27 +5097,29 @@ PUT ``` #### 响应格式 + ```json { - "code": 0, - "message": "权限更新成功", - "data": { - "roleId": 10, - "permissionCount": 20, - "addedCount": 5, - "removedCount": 3 - } + "code": 0, + "message": "权限更新成功", + "data": { + "roleId": 10, + "permissionCount": 20, + "addedCount": 5, + "removedCount": 3 + } } ``` #### 开发思路 + 1. 检查角色是否为系统角色 2. 验证权限合法性(不超过父角色) 3. 对比新旧权限,计算差异 4. 使用事务更新: - - 删除旧权限关联 - - 创建新权限关联 - - 更新权限快照 + - 删除旧权限关联 + - 创建新权限关联 + - 更新权限快照 5. 级联更新子角色(移除超出的权限) 6. 清除相关用户的权限缓存 @@ -4768,23 +5128,29 @@ PUT ### 4.5 删除角色 #### 需求描述 + 删除角色,处理关联关系。 #### 接口名称 + 删除角色接口 #### 设计原因 + - 清理无用角色 - 保持系统整洁 - 防止权限泄露 #### 请求类型 + DELETE #### 接口路径 + `/roles/{roleId}` #### 开发思路 + 1. 权限检查(需要role:delete权限) 2. 检查是否为系统角色(禁止删除) 3. 检查是否有用户使用该角色 @@ -4797,23 +5163,29 @@ DELETE ### 4.6 角色用户查询 #### 需求描述 + 查询某个角色下的所有用户。 #### 接口名称 + 角色用户查询接口 #### 设计原因 + - 了解角色使用情况 - 便于角色管理 - 支持用户迁移 #### 请求类型 + GET #### 接口路径 + `/roles/{roleId}/users` #### 接口参数 + ```typescript { page?: number; @@ -4823,25 +5195,26 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "items": [ - { - "id": 123, - "username": "john_doe", - "email": "john@example.com", - "assignedAt": "2024-01-01T00:00:00Z" - } - ], - "pagination": { - "page": 1, - "pageSize": 20, - "total": 50 + "code": 0, + "message": "success", + "data": { + "items": [ + { + "id": 123, + "username": "john_doe", + "email": "john@example.com", + "assignedAt": "2024-01-01T00:00:00Z" + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 50 + } } - } } ``` @@ -4852,23 +5225,29 @@ GET ### 5.1 权限树查询 #### 需求描述 + 查询权限树形结构,按类型分组展示。 #### 接口名称 + 权限树查询接口 #### 设计原因 + - 权限具有层级关系(菜单-页面-按钮) - 便于权限分配时的选择 - 支持不同类型权限的管理 #### 请求类型 + GET #### 接口路径 + `/permissions/tree` #### 接口参数 + ```typescript { type?: string; // 权限类型:menu,button,api,data @@ -4877,40 +5256,42 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "system", - "name": "系统管理", - "type": "menu", - "children": [ + "code": 0, + "message": "success", + "data": [ { - "id": 2, - "code": "system:user", - "name": "用户管理", - "type": "menu", - "children": [ - { - "id": 3, - "code": "user:read", - "name": "查看用户", - "type": "api", - "resource": "user", - "action": "read" - } - ] + "id": 1, + "code": "system", + "name": "系统管理", + "type": "menu", + "children": [ + { + "id": 2, + "code": "system:user", + "name": "用户管理", + "type": "menu", + "children": [ + { + "id": 3, + "code": "user:read", + "name": "查看用户", + "type": "api", + "resource": "user", + "action": "read" + } + ] + } + ] } - ] - } - ] + ] } ``` #### 开发思路 + 1. 查询权限列表 2. 按类型分组 3. 构建树形结构 @@ -4921,23 +5302,29 @@ GET ### 5.2 创建权限 #### 需求描述 + 创建新的权限节点。 #### 接口名称 + 创建权限接口 #### 设计原因 + - 新功能需要新权限 - 灵活的权限扩展 - 支持细粒度控制 #### 请求类型 + POST #### 接口路径 + `/permissions` #### 接口参数 + ```typescript { code: string; // 权限代码 @@ -4952,6 +5339,7 @@ POST ``` #### 开发思路 + 1. 权限检查(需要permission:create权限) 2. 验证权限代码唯一性 3. 验证父权限存在性 @@ -4963,51 +5351,59 @@ POST ### 5.3 批量创建权限 #### 需求描述 + 支持批量导入权限配置。 #### 接口名称 + 批量创建权限接口 #### 设计原因 + - 新模块上线需要批量添加权限 - 提高配置效率 - 支持权限模板导入 #### 请求类型 + POST #### 接口路径 + `/permissions/batch` #### 接口参数 + ```typescript { - permissions: Array<{ - code: string; - name: string; - type: string; - resource?: string; - action?: string; - pid?: number; - description?: string; - }> + permissions: Array<{ + code: string; + name: string; + type: string; + resource?: string; + action?: string; + pid?: number; + description?: string; + }>; } ``` #### 响应格式 + ```json { - "code": 0, - "message": "批量创建成功", - "data": { - "success": 10, - "failed": 0, - "errors": [] - } + "code": 0, + "message": "批量创建成功", + "data": { + "success": 10, + "failed": 0, + "errors": [] + } } ``` #### 开发思路 + 1. 批量验证权限代码唯一性 2. 验证父权限存在性 3. 使用事务批量插入 @@ -5019,23 +5415,29 @@ POST ### 5.4 更新权限 #### 需求描述 + 更新权限信息。 #### 接口名称 + 更新权限接口 #### 设计原因 + - 权限名称或描述可能需要调整 - 元数据更新(如菜单图标) - 状态变更 #### 请求类型 + PUT #### 接口路径 + `/permissions/{permissionId}` #### 接口参数 + ```typescript { name?: string; @@ -5047,6 +5449,7 @@ PUT ``` #### 开发思路 + 1. 权限检查 2. 更新权限信息 3. 清除权限缓存 @@ -5057,23 +5460,29 @@ PUT ### 5.5 删除权限 #### 需求描述 + 删除权限节点及其子权限。 #### 接口名称 + 删除权限接口 #### 设计原因 + - 功能下线需要删除权限 - 保持权限体系整洁 - 防止权限泄露 #### 请求类型 + DELETE #### 接口路径 + `/permissions/{permissionId}` #### 开发思路 + 1. 权限检查(需要permission:delete权限) 2. 检查是否有角色使用该权限 3. 递归删除子权限 @@ -5084,23 +5493,29 @@ DELETE ### 5.6 权限检查接口 #### 需求描述 + 检查当前用户是否拥有特定权限。 #### 接口名称 + 权限检查接口 #### 设计原因 + - 前端动态控制界面元素 - 细粒度的功能控制 - 实时权限验证 #### 请求类型 + POST #### 接口路径 + `/permissions/check` #### 接口参数 + ```typescript { permissions: string[]; // 权限代码数组 @@ -5108,23 +5523,26 @@ POST ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "user:read": true, - "user:create": true, - "user:delete": false - } + "code": 0, + "message": "success", + "data": { + "user:read": true, + "user:create": true, + "user:delete": false + } } ``` #### 开发思路 + 1. 获取当前用户所有权限 2. 批量检查请求的权限 3. 返回权限检查结果 -``` + +```` ```markdown // ... existing code ... @@ -5157,45 +5575,47 @@ GET status?: string; // 状态 withStats?: boolean; // 是否包含统计信息 } -``` +```` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "ROOT", - "name": "星撰集团", - "type": "company", - "userCount": 100, - "children": [ + "code": 0, + "message": "success", + "data": [ { - "id": 2, - "code": "TECH", - "name": "技术部", - "type": "department", - "userCount": 30, - "leader": { - "id": 123, - "name": "张三" - }, - "children": [] + "id": 1, + "code": "ROOT", + "name": "星撰集团", + "type": "company", + "userCount": 100, + "children": [ + { + "id": 2, + "code": "TECH", + "name": "技术部", + "type": "department", + "userCount": 30, + "leader": { + "id": 123, + "name": "张三" + }, + "children": [] + } + ] } - ] - } - ] + ] } ``` #### 开发思路 + 1. 查询组织列表 2. 构建树形结构 3. 如需统计: - - 批量查询用户组织关联 - - 聚合统计各组织人数 + - 批量查询用户组织关联 + - 聚合统计各组织人数 4. 查询负责人信息 5. 使用Redis缓存 @@ -5204,23 +5624,29 @@ GET ### 6.2 创建组织 #### 需求描述 + 创建新的组织节点。 #### 接口名称 + 创建组织接口 #### 设计原因 + - 组织架构需要动态调整 - 支持业务扩展 - 灵活的组织管理 #### 请求类型 + POST #### 接口路径 + `/organizations` #### 接口参数 + ```typescript { code: string; // 组织代码 @@ -5236,6 +5662,7 @@ POST ``` #### 开发思路 + 1. 权限检查(需要organization:create权限) 2. 验证组织代码唯一性 3. 验证父组织存在性 @@ -5248,23 +5675,29 @@ POST ### 6.3 调整组织架构 #### 需求描述 + 支持拖拽调整组织层级关系。 #### 接口名称 + 调整组织架构接口 #### 设计原因 + - 组织架构会随业务发展调整 - 支持灵活的组织变更 - 保持数据完整性 #### 请求类型 + PUT #### 接口路径 + `/organizations/{organizationId}/move` #### 接口参数 + ```typescript { targetPid: number; // 目标父组织ID @@ -5273,27 +5706,29 @@ PUT ``` #### 响应格式 + ```json { - "code": 0, - "message": "调整成功", - "data": { - "id": 10, - "oldPath": "/1/2/10/", - "newPath": "/1/3/10/", - "affectedCount": 5 - } + "code": 0, + "message": "调整成功", + "data": { + "id": 10, + "oldPath": "/1/2/10/", + "newPath": "/1/3/10/", + "affectedCount": 5 + } } ``` #### 开发思路 + 1. 验证目标组织存在性 2. 检查是否会形成循环 3. 使用事务更新: - - 更新pid - - 重新计算path和level - - 更新所有子组织的path - - 调整排序 + - 更新pid + - 重新计算path和level + - 更新所有子组织的path + - 调整排序 4. 记录组织变更日志 5. 清除相关缓存 @@ -5302,23 +5737,29 @@ PUT ### 6.4 组织人员管理 #### 需求描述 + 管理组织内的人员分配。 #### 接口名称 + 组织人员分配接口 #### 设计原因 + - 灵活的人员调动 - 支持批量操作 - 记录调动历史 #### 请求类型 + POST #### 接口路径 + `/organizations/{organizationId}/users` #### 接口参数 + ```typescript { action: string; // 操作:add,remove,move @@ -5330,12 +5771,13 @@ POST ``` #### 开发思路 + 1. 权限检查 2. 验证用户和组织存在性 3. 执行批量操作: - - add: 添加用户到组织 - - remove: 从组织移除用户 - - move: 移动到其他组织 + - add: 添加用户到组织 + - remove: 从组织移除用户 + - move: 移动到其他组织 4. 记录人员变动日志 --- @@ -5343,23 +5785,29 @@ POST ### 6.5 删除组织 #### 需求描述 + 删除组织节点,处理人员安置。 #### 接口名称 + 删除组织接口 #### 设计原因 + - 组织调整需要删除节点 - 确保人员妥善安置 - 保持数据完整性 #### 请求类型 + DELETE #### 接口路径 + `/organizations/{organizationId}` #### 接口参数 + ```typescript { handleUsers?: string; // 人员处理方式:moveToParent,remove @@ -5368,6 +5816,7 @@ DELETE ``` #### 开发思路 + 1. 权限检查(需要organization:delete权限) 2. 检查是否有子组织 3. 处理组织内人员 @@ -5381,47 +5830,54 @@ DELETE ### 7.1 字典类型树查询 #### 需求描述 + 查询字典类型的树形结构,支持分类管理。 #### 接口名称 + 字典类型树查询接口 #### 设计原因 + - 字典类型支持分类管理 - 便于字典的组织和查找 - 区分系统字典和业务字典 #### 请求类型 + GET #### 接口路径 + `/dictionaries/types/tree` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "system", - "name": "系统字典", - "isSystem": true, - "children": [ + "code": 0, + "message": "success", + "data": [ { - "id": 10, - "code": "user_status", - "name": "用户状态", - "itemCount": 4 + "id": 1, + "code": "system", + "name": "系统字典", + "isSystem": true, + "children": [ + { + "id": 10, + "code": "user_status", + "name": "用户状态", + "itemCount": 4 + } + ] } - ] - } - ] + ] } ``` #### 开发思路 + 1. 查询所有字典类型 2. 构建树形结构 3. 统计每个类型的字典项数量 @@ -5432,23 +5888,29 @@ GET ### 7.2 获取字典项 #### 需求描述 + 根据字典类型获取所有字典项,支持树形字典。 #### 接口名称 + 获取字典项接口 #### 设计原因 + - 前端下拉框等组件需要字典数据 - 支持动态配置 - 减少硬编码 #### 请求类型 + GET #### 接口路径 + `/dictionaries/items` #### 接口参数 + ```typescript { typeCode: string; // 字典类型代码 @@ -5457,25 +5919,27 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": [ - { - "key": "active", - "value": "active", - "label": "正常", - "labelEn": "Active", - "color": "#52c41a", - "sortOrder": 1, - "extra": {} - } - ] + "code": 0, + "message": "success", + "data": [ + { + "key": "active", + "value": "active", + "label": "正常", + "labelEn": "Active", + "color": "#52c41a", + "sortOrder": 1, + "extra": {} + } + ] } ``` #### 开发思路 + 1. 根据typeCode查询字典类型 2. 查询该类型下的所有字典项 3. 如果是树形字典,构建树结构 @@ -5486,23 +5950,29 @@ GET ### 7.3 创建字典类型 #### 需求描述 + 创建新的字典类型。 #### 接口名称 + 创建字典类型接口 #### 设计原因 + - 业务扩展需要新字典 - 灵活的配置管理 - 支持分类组织 #### 请求类型 + POST #### 接口路径 + `/dictionaries/types` #### 接口参数 + ```typescript { code: string; // 字典类型代码 @@ -5514,6 +5984,7 @@ POST ``` #### 开发思路 + 1. 权限检查(需要dictionary:create权限) 2. 验证代码唯一性 3. 创建字典类型 @@ -5524,61 +5995,69 @@ POST ### 7.4 批量更新字典项 #### 需求描述 + 支持批量更新字典项,保持顺序。 #### 接口名称 + 批量更新字典项接口 #### 设计原因 + - 字典项通常需要批量维护 - 保持显示顺序的一致性 - 提高维护效率 #### 请求类型 + PUT #### 接口路径 + `/dictionaries/types/{typeId}/items/batch` #### 接口参数 + ```typescript { - items: Array<{ - id?: number; // 有ID为更新,无ID为新增 - key: string; - value: string; - label: string; - labelEn?: string; - pid?: number; - sortOrder: number; - status?: string; - color?: string; - extra?: object; - }> + items: Array<{ + id?: number; // 有ID为更新,无ID为新增 + key: string; + value: string; + label: string; + labelEn?: string; + pid?: number; + sortOrder: number; + status?: string; + color?: string; + extra?: object; + }>; } ``` #### 响应格式 + ```json { - "code": 0, - "message": "批量更新成功", - "data": { - "created": 3, - "updated": 5, - "deleted": 2 - } + "code": 0, + "message": "批量更新成功", + "data": { + "created": 3, + "updated": 5, + "deleted": 2 + } } ``` #### 开发思路 + 1. 验证字典类型存在 2. 检查是否为系统字典 3. 验证key在类型内唯一 4. 使用事务处理: - - 标记删除不在列表中的项 - - 更新已存在的项 - - 创建新项 + - 标记删除不在列表中的项 + - 更新已存在的项 + - 创建新项 5. 清除字典缓存 --- @@ -5586,23 +6065,29 @@ PUT ### 7.5 导出字典数据 #### 需求描述 + 导出字典配置用于备份或迁移。 #### 接口名称 + 导出字典数据接口 #### 设计原因 + - 配置备份需求 - 环境间迁移 - 批量导入导出 #### 请求类型 + GET #### 接口路径 + `/dictionaries/export` #### 接口参数 + ```typescript { typeIds?: number[]; // 指定类型ID,不传则导出全部 @@ -5611,12 +6096,14 @@ GET ``` #### 响应格式 + ``` Content-Type: application/octet-stream Content-Disposition: attachment; filename="dictionaries_20240101.json" ``` #### 开发思路 + 1. 查询指定的字典类型和项 2. 构建导出数据结构 3. 根据格式生成文件 @@ -5629,23 +6116,29 @@ Content-Disposition: attachment; filename="dictionaries_20240101.json" ### 8.1 标签列表查询 #### 需求描述 + 查询标签列表,支持按类型和使用频率排序。 #### 接口名称 + 标签列表查询接口 #### 设计原因 + - 便于标签的统一管理 - 支持热门标签展示 - 不同类型标签分开管理 #### 请求类型 + GET #### 接口路径 + `/tags` #### 接口参数 + ```typescript { type?: string; // 标签类型 @@ -5657,31 +6150,33 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "items": [ - { - "id": 1, - "name": "VIP", - "type": "user", - "color": "#ff4d4f", - "usageCount": 100, - "description": "VIP用户" - } - ], - "pagination": { - "page": 1, - "pageSize": 20, - "total": 50 + "code": 0, + "message": "success", + "data": { + "items": [ + { + "id": 1, + "name": "VIP", + "type": "user", + "color": "#ff4d4f", + "usageCount": 100, + "description": "VIP用户" + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 50 + } } - } } ``` #### 开发思路 + 1. 构建查询条件 2. 执行分页查询 3. 根据sortBy排序 @@ -5692,23 +6187,29 @@ GET ### 8.2 创建标签 #### 需求描述 + 创建新的标签。 #### 接口名称 + 创建标签接口 #### 设计原因 + - 灵活的标签扩展 - 支持自定义标签 - 统一标签管理 #### 请求类型 + POST #### 接口路径 + `/tags` #### 接口参数 + ```typescript { name: string; // 标签名称 @@ -5719,6 +6220,7 @@ POST ``` #### 开发思路 + 1. 权限检查(需要tag:create权限) 2. 验证标签名称唯一性(同类型内) 3. 创建标签记录 @@ -5729,23 +6231,29 @@ POST ### 8.3 标签使用统计 #### 需求描述 + 统计标签使用情况,生成报表。 #### 接口名称 + 标签使用统计接口 #### 设计原因 + - 了解标签使用情况 - 优化标签体系 - 数据分析支持 #### 请求类型 + GET #### 接口路径 + `/tags/statistics` #### 接口参数 + ```typescript { type?: string; // 标签类型 @@ -5756,35 +6264,37 @@ GET ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "summary": { - "totalTags": 50, - "totalUsage": 1000, - "avgUsage": 20 - }, - "topTags": [ - { - "id": 1, - "name": "VIP", - "usageCount": 100, - "percentage": 10 - } - ], - "trend": [ - { - "date": "2024-01-01", - "count": 20 - } - ] - } + "code": 0, + "message": "success", + "data": { + "summary": { + "totalTags": 50, + "totalUsage": 1000, + "avgUsage": 20 + }, + "topTags": [ + { + "id": 1, + "name": "VIP", + "usageCount": 100, + "percentage": 10 + } + ], + "trend": [ + { + "date": "2024-01-01", + "count": 20 + } + ] + } } ``` #### 开发思路 + 1. 统计基础数据 2. 查询热门标签 3. 计算使用趋势 @@ -5795,23 +6305,29 @@ GET ### 8.4 标签智能推荐 #### 需求描述 + 根据用户特征智能推荐标签。 #### 接口名称 + 标签推荐接口 #### 设计原因 + - 提高标签使用的准确性 - 基于规则引擎自动打标 - 减少人工维护成本 #### 请求类型 + POST #### 接口路径 + `/tags/recommend` #### 接口参数 + ```typescript { userId: number; // 用户ID @@ -5821,29 +6337,31 @@ POST ``` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "recommendations": [ - { - "tagId": 1, - "tagName": "活跃用户", - "confidence": 0.95, - "reason": "最近30天登录20次" - } - ] - } + "code": 0, + "message": "success", + "data": { + "recommendations": [ + { + "tagId": 1, + "tagName": "活跃用户", + "confidence": 0.95, + "reason": "最近30天登录20次" + } + ] + } } ``` #### 开发思路 + 1. 获取用户特征数据 2. 执行推荐规则: - - 活跃度规则(登录次数、最后登录时间) - - 贡献度规则(内容创作数量) - - 消费规则(VIP等级) + - 活跃度规则(登录次数、最后登录时间) + - 贡献度规则(内容创作数量) + - 消费规则(VIP等级) 3. 计算置信度 4. 返回推荐标签列表 5. 记录推荐日志 @@ -5853,23 +6371,29 @@ POST ### 8.5 批量打标签 #### 需求描述 + 为多个用户批量添加或移除标签。 #### 接口名称 + 批量打标签接口 #### 设计原因 + - 提高运营效率 - 支持批量操作 - 灵活的标签管理 #### 请求类型 + POST #### 接口路径 + `/tags/batch-assign` #### 接口参数 + ```typescript { userIds: number[]; // 用户ID数组 @@ -5879,25 +6403,28 @@ POST ``` #### 响应格式 + ```json { - "code": 0, - "message": "批量操作成功", - "data": { - "success": 50, - "failed": 0, - "errors": [] - } + "code": 0, + "message": "批量操作成功", + "data": { + "success": 50, + "failed": 0, + "errors": [] + } } ``` #### 开发思路 + 1. 权限检查 2. 验证用户和标签存在性 3. 批量执行操作 4. 更新标签使用统计 5. 返回执行结果 -``` + +```` ```markdown // ... existing code ... @@ -5930,21 +6457,23 @@ GET width?: number; // 图片宽度,默认120 height?: number; // 图片高度,默认40 } -``` +```` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "captchaId": "xxxx-xxxx-xxxx", - "captchaImage": "data:image/svg+xml;base64,..." - } + "code": 0, + "message": "success", + "data": { + "captchaId": "xxxx-xxxx-xxxx", + "captchaImage": "data:image/svg+xml;base64,..." + } } ``` #### 开发思路 + 1. 生成随机验证码文本 2. 生成SVG或图片 3. 存储到Redis(5分钟过期) @@ -5955,26 +6484,33 @@ GET ### 9.2 文件上传 #### 需求描述 + 通用文件上传接口,支持头像等文件上传。 #### 接口名称 + 文件上传接口 #### 设计原因 + - 统一的文件上传入口 - 支持多种文件类型 - 安全的文件处理 #### 请求类型 + POST #### 接口路径 + `/common/upload` #### 请求格式 + multipart/form-data #### 接口参数 + ```typescript { file: File; // 文件 @@ -5984,21 +6520,23 @@ multipart/form-data ``` #### 响应格式 + ```json { - "code": 0, - "message": "上传成功", - "data": { - "fileId": "file123456", - "url": "https://oss.starzh.com/xxx.jpg", - "size": 102400, - "name": "avatar.jpg", - "type": "image/jpeg" - } + "code": 0, + "message": "上传成功", + "data": { + "fileId": "file123456", + "url": "https://oss.starzh.com/xxx.jpg", + "size": 102400, + "name": "avatar.jpg", + "type": "image/jpeg" + } } ``` #### 开发思路 + 1. 验证文件类型和大小 2. 生成唯一文件名 3. 上传到OSS或本地存储 @@ -6010,23 +6548,29 @@ multipart/form-data ### 9.3 邮件发送 #### 需求描述 + 统一的邮件发送接口,用于系统通知。 #### 接口名称 + 邮件发送接口(内部接口) #### 设计原因 + - 统一邮件发送逻辑 - 支持模板化 - 便于监控和管理 #### 请求类型 + POST #### 接口路径 + `/internal/email/send` #### 接口参数 + ```typescript { to: string[]; // 收件人邮箱数组 @@ -6040,6 +6584,7 @@ POST ``` #### 开发思路 + 1. 加载邮件模板 2. 渲染模板内容 3. 调用邮件服务发送 @@ -6051,41 +6596,48 @@ POST ### 9.4 系统健康检查 #### 需求描述 + 提供系统健康状态检查接口。 #### 接口名称 + 健康检查接口 #### 设计原因 + - 监控系统状态 - 负载均衡健康检查 - 快速发现问题 #### 请求类型 + GET #### 接口路径 + `/health` #### 响应格式 + ```json { - "code": 0, - "message": "success", - "data": { - "status": "healthy", - "version": "1.0.0", - "uptime": 3600, - "services": { - "database": "ok", - "redis": "ok", - "elasticsearch": "ok" + "code": 0, + "message": "success", + "data": { + "status": "healthy", + "version": "1.0.0", + "uptime": 3600, + "services": { + "database": "ok", + "redis": "ok", + "elasticsearch": "ok" + } } - } } ``` #### 开发思路 + 1. 检查数据库连接 2. 检查Redis连接 3. 检查其他服务状态 @@ -6096,23 +6648,29 @@ GET ### 9.5 数据导入导出 #### 需求描述 + 支持批量数据的导入导出功能。 #### 接口名称 + 数据导出接口 #### 设计原因 + - 数据备份需求 - 批量数据处理 - 数据迁移支持 #### 请求类型 + POST #### 接口路径 + `/common/export` #### 接口参数 + ```typescript { module: string; // 模块:users,roles,organizations @@ -6123,18 +6681,20 @@ POST ``` #### 响应格式 + ```json { - "code": 0, - "message": "导出任务已创建", - "data": { - "taskId": "export-123456", - "status": "processing" - } + "code": 0, + "message": "导出任务已创建", + "data": { + "taskId": "export-123456", + "status": "processing" + } } ``` #### 开发思路 + 1. 创建导出任务 2. 异步处理数据查询和转换 3. 生成导出文件 @@ -6146,47 +6706,51 @@ POST ## 10. 安全性设计 ### 10.1 认证机制 + - **JWT Token认证** - - AccessToken有效期2小时 - - RefreshToken有效期7天 - - Token黑名单机制 - - 支持多设备登录管理 + - AccessToken有效期2小时 + - RefreshToken有效期7天 + - Token黑名单机制 + - 支持多设备登录管理 ### 10.2 权限控制 + - **基于RBAC的权限模型** - - 接口级别的权限控制 - - 数据级别的权限过滤 - - 动态权限加载 - - 权限缓存优化 + - 接口级别的权限控制 + - 数据级别的权限过滤 + - 动态权限加载 + - 权限缓存优化 ### 10.3 安全措施 + - **传输安全** - - 所有接口强制HTTPS - - 敏感数据加密传输 - - 防重放攻击(时间戳+nonce) + - 所有接口强制HTTPS + - 敏感数据加密传输 + - 防重放攻击(时间戳+nonce) - **输入验证** - - 参数类型和格式验证 - - SQL注入防护(参数化查询) - - XSS防护(输入过滤和转义) - - 文件上传安全检查 + - 参数类型和格式验证 + - SQL注入防护(参数化查询) + - XSS防护(输入过滤和转义) + - 文件上传安全检查 - **访问控制** - - IP白名单(管理接口) - - 请求频率限制 - - 并发连接限制 - - 异常行为检测 + - IP白名单(管理接口) + - 请求频率限制 + - 并发连接限制 + - 异常行为检测 ### 10.4 日志审计 + - **操作日志** - - 记录所有写操作 - - 包含用户、时间、IP、操作内容 - - 敏感操作实时告警 + - 记录所有写操作 + - 包含用户、时间、IP、操作内容 + - 敏感操作实时告警 - **安全日志** - - 登录失败记录 - - 权限拒绝记录 - - 异常请求记录 + - 登录失败记录 + - 权限拒绝记录 + - 异常请求记录 --- @@ -6194,42 +6758,45 @@ POST ### 11.1 缓存策略 -| 缓存类型 | 缓存内容 | 过期时间 | 更新策略 | -|---------|---------|---------|---------| -| 用户信息缓存 | 基本信息、角色、权限 | 5分钟 | 主动更新+过期刷新 | -| 角色权限缓存 | 角色树、权限快照 | 1小时 | 变更时删除 | -| 字典缓存 | 字典类型和项 | 24小时 | 变更时删除 | -| 验证码缓存 | 图形验证码 | 5分钟 | 使用后删除 | +| 缓存类型 | 缓存内容 | 过期时间 | 更新策略 | +| ------------ | -------------------- | -------- | ----------------- | +| 用户信息缓存 | 基本信息、角色、权限 | 5分钟 | 主动更新+过期刷新 | +| 角色权限缓存 | 角色树、权限快照 | 1小时 | 变更时删除 | +| 字典缓存 | 字典类型和项 | 24小时 | 变更时删除 | +| 验证码缓存 | 图形验证码 | 5分钟 | 使用后删除 | ### 11.2 查询优化 + - **索引优化** - - 基于查询模式设计索引 - - 定期分析慢查询日志 - - 避免全表扫描 + - 基于查询模式设计索引 + - 定期分析慢查询日志 + - 避免全表扫描 - **查询优化** - - 使用预编译语句 - - 避免N+1查询 - - 合理使用JOIN和子查询 - - 大数据量分页优化 + - 使用预编译语句 + - 避免N+1查询 + - 合理使用JOIN和子查询 + - 大数据量分页优化 ### 11.3 并发控制 + - **限流策略** - - 接口级别限流(令牌桶算法) - - 用户级别限流 - - IP级别限流 + - 接口级别限流(令牌桶算法) + - 用户级别限流 + - IP级别限流 - **资源控制** - - 数据库连接池管理 - - Redis连接池管理 - - 异步任务队列 + - 数据库连接池管理 + - Redis连接池管理 + - 异步任务队列 ### 11.4 异步处理 + - **异步任务** - - 邮件发送 - - 文件处理 - - 数据导入导出 - - 统计计算 + - 邮件发送 + - 文件处理 + - 数据导入导出 + - 统计计算 --- @@ -6237,118 +6804,126 @@ POST ### 12.1 错误码规范 -| 错误码范围 | 说明 | 示例 | -|-----------|------|------| -| 0 | 成功 | 操作成功 | -| 400xx | 客户端错误 | 40001-参数错误 | -| 401xx | 认证错误 | 40101-未登录 | -| 403xx | 权限错误 | 40301-无权限 | -| 404xx | 资源不存在 | 40401-用户不存在 | -| 409xx | 资源冲突 | 40901-用户名已存在 | -| 423xx | 资源锁定 | 42301-账号被锁定 | -| 429xx | 请求过多 | 42901-请求频率超限 | -| 500xx | 服务器错误 | 50001-内部错误 | -| 503xx | 服务不可用 | 50301-服务维护中 | +| 错误码范围 | 说明 | 示例 | +| ---------- | ---------- | ------------------ | +| 0 | 成功 | 操作成功 | +| 400xx | 客户端错误 | 40001-参数错误 | +| 401xx | 认证错误 | 40101-未登录 | +| 403xx | 权限错误 | 40301-无权限 | +| 404xx | 资源不存在 | 40401-用户不存在 | +| 409xx | 资源冲突 | 40901-用户名已存在 | +| 423xx | 资源锁定 | 42301-账号被锁定 | +| 429xx | 请求过多 | 42901-请求频率超限 | +| 500xx | 服务器错误 | 50001-内部错误 | +| 503xx | 服务不可用 | 50301-服务维护中 | ### 12.2 错误响应格式 + ```json { - "code": 40001, - "message": "参数验证失败:用户名长度必须在3-50个字符之间", - "data": { - "field": "username", - "value": "ab", - "rule": "minLength" - }, - "timestamp": 1640995200000, - "requestId": "req-xxxx-xxxx", - "path": "/api/v1/users" + "code": 40001, + "message": "参数验证失败:用户名长度必须在3-50个字符之间", + "data": { + "field": "username", + "value": "ab", + "rule": "minLength" + }, + "timestamp": 1640995200000, + "requestId": "req-xxxx-xxxx", + "path": "/api/v1/users" } ``` ### 12.3 错误处理最佳实践 + - **友好的错误提示** - - 使用用户能理解的语言 - - 提供解决建议 - - 避免暴露技术细节 + - 使用用户能理解的语言 + - 提供解决建议 + - 避免暴露技术细节 - **错误日志记录** - - 记录完整的错误堆栈 - - 包含请求上下文 - - 分级记录(info/warn/error) + - 记录完整的错误堆栈 + - 包含请求上下文 + - 分级记录(info/warn/error) - **错误监控告警** - - 错误率监控 - - 关键错误实时告警 - - 错误趋势分析 + - 错误率监控 + - 关键错误实时告警 + - 错误趋势分析 --- ## 13. 接口版本管理 ### 13.1 版本策略 + - **URL版本化** - - 格式:`/api/v{n}/...` - - 主版本号变更表示不兼容更新 - - 向后兼容原则 + - 格式:`/api/v{n}/...` + - 主版本号变更表示不兼容更新 + - 向后兼容原则 ### 13.2 版本迁移 + - **废弃通知** - - 提前3个月通知 - - 响应头添加废弃警告 - - 提供迁移文档 + - 提前3个月通知 + - 响应头添加废弃警告 + - 提供迁移文档 - **版本共存** - - 新旧版本并行运行 - - 逐步迁移用户 - - 设置旧版本下线时间 + - 新旧版本并行运行 + - 逐步迁移用户 + - 设置旧版本下线时间 --- ## 14. 开发规范总结 ### 14.1 接口设计原则 + - **RESTful规范** - - 使用标准HTTP方法 - - 资源导向的URL设计 - - 合理使用状态码 + - 使用标准HTTP方法 + - 资源导向的URL设计 + - 合理使用状态码 - **一致性原则** - - 统一的命名规范 - - 统一的响应格式 - - 统一的错误处理 + - 统一的命名规范 + - 统一的响应格式 + - 统一的错误处理 ### 14.2 最佳实践 + - **幂等性设计** - - GET/PUT/DELETE操作幂等 - - POST操作防重复提交 + - GET/PUT/DELETE操作幂等 + - POST操作防重复提交 - **分页设计** - - 统一分页参数 - - 限制最大返回条数 - - 提供总数统计 + - 统一分页参数 + - 限制最大返回条数 + - 提供总数统计 - **数据安全** - - 敏感数据脱敏 - - 防止信息泄露 - - 输入验证和过滤 + - 敏感数据脱敏 + - 防止信息泄露 + - 输入验证和过滤 ### 14.3 文档规范 + - **接口文档** - - 使用Swagger自动生成 - - 包含请求和响应示例 - - 说明业务场景 + - 使用Swagger自动生成 + - 包含请求和响应示例 + - 说明业务场景 - **变更记录** - - 记录所有API变更 - - 说明变更原因 - - 提供升级指南 + - 记录所有API变更 + - 说明变更原因 + - 提供升级指南 --- ## 15. 附录 ### 15.1 常用正则表达式 + ```javascript // 用户名:3-50位字母数字下划线,字母开头 const USERNAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_]{2,49}$/; @@ -6364,11 +6939,13 @@ const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,100}$ ``` ### 15.2 时间格式约定 + - 统一使用ISO 8601格式 - 示例:`2024-01-01T12:00:00Z` - 时区:统一使用UTC时间 ### 15.3 分页参数约定 + - `page`: 页码,从1开始 - `pageSize`: 每页条数,默认20,最大100 - `total`: 总记录数 @@ -6377,7 +6954,8 @@ const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,100}$ --- 本接口设计文档为M2基础用户系统的完整API规范,后续开发应严格遵循本文档的设计要求。 -``` + +```` ## 总结 @@ -6480,20 +7058,22 @@ PUT sortOrder: number; // 新的排序号 }> } -``` +```` #### 响应格式 + ```json { - "code": 0, - "message": "排序更新成功", - "data": { - "updated": 5 - } + "code": 0, + "message": "排序更新成功", + "data": { + "updated": 5 + } } ``` #### 开发思路 + 1. 权限检查(需要role:sort权限) 2. 批量更新sortOrder字段 3. 确保同级别角色排序唯一性 @@ -6505,23 +7085,29 @@ PUT ## 4.8 拖拽排序接口 #### 需求描述 + 支持前端拖拽方式调整角色顺序,更直观的操作方式。 #### 接口名称 + 拖拽排序接口 #### 设计原因 + - 提供更好的用户体验 - 直观的排序操作 - 减少误操作 #### 请求类型 + PUT #### 接口路径 + `/roles/{roleId}/move` #### 接口参数 + ```typescript { targetIndex: number; // 目标位置索引(0开始) @@ -6530,20 +7116,22 @@ PUT ``` #### 响应格式 + ```json { - "code": 0, - "message": "移动成功", - "data": { - "roleId": 10, - "oldIndex": 2, - "newIndex": 0, - "affectedRoles": [8, 9, 10] - } + "code": 0, + "message": "移动成功", + "data": { + "roleId": 10, + "oldIndex": 2, + "newIndex": 0, + "affectedRoles": [8, 9, 10] + } } ``` #### 开发思路 + 1. 计算目标位置的sortOrder值 2. 更新被移动角色的sortOrder 3. 调整其他角色的sortOrder(避免冲突) @@ -6555,33 +7143,40 @@ PUT ## 5.7 权限排序接口 #### 需求描述 + 调整权限的显示顺序,特别是菜单权限的排序。 #### 接口名称 + 权限排序接口 #### 设计原因 + - 菜单权限需要控制显示顺序 - 影响前端菜单渲染顺序 - 提升用户体验 #### 请求类型 + PUT #### 接口路径 + `/permissions/sort` #### 接口参数 + ```typescript { - items: Array<{ - id: number; - sortOrder: number; - }> + items: Array<{ + id: number; + sortOrder: number; + }>; } ``` #### 开发思路 + 1. 权限检查(需要permission:sort权限) 2. 批量更新权限排序 3. 确保同级权限排序唯一 @@ -6593,45 +7188,53 @@ PUT ## 6.6 组织架构排序接口 #### 需求描述 + 调整同级组织的显示顺序。 #### 接口名称 + 组织排序接口 #### 设计原因 + - 组织架构显示需要符合实际层级 - 重要部门需要前置显示 - 便于组织管理 #### 请求类型 + PUT #### 接口路径 + `/organizations/{pid}/sort` #### 接口参数 + ```typescript { - items: Array<{ - id: number; - sortOrder: number; - }> + items: Array<{ + id: number; + sortOrder: number; + }>; } ``` #### 响应格式 + ```json { - "code": 0, - "message": "组织排序更新成功", - "data": { - "parentId": 1, - "updated": 3 - } + "code": 0, + "message": "组织排序更新成功", + "data": { + "parentId": 1, + "updated": 3 + } } ``` #### 开发思路 + 1. 验证所有组织都属于同一父级 2. 批量更新sortOrder 3. 清除组织架构缓存 @@ -6642,45 +7245,53 @@ PUT ## 7.6 字典项排序接口 #### 需求描述 + 调整字典项的显示顺序,影响下拉框等组件的选项顺序。 #### 接口名称 + 字典项排序接口 #### 设计原因 + - 下拉框选项需要按业务逻辑排序 - 常用选项前置显示 - 提升用户操作效率 #### 请求类型 + PUT #### 接口路径 + `/dictionaries/types/{typeId}/items/sort` #### 接口参数 + ```typescript { - items: Array<{ - id: number; - sortOrder: number; - }> + items: Array<{ + id: number; + sortOrder: number; + }>; } ``` #### 响应格式 + ```json { - "code": 0, - "message": "字典项排序更新成功", - "data": { - "typeId": 1, - "updated": 4 - } + "code": 0, + "message": "字典项排序更新成功", + "data": { + "typeId": 1, + "updated": 4 + } } ``` #### 开发思路 + 1. 验证所有字典项都属于指定类型 2. 批量更新sortOrder 3. 清除字典缓存 @@ -6691,23 +7302,29 @@ PUT ## 8.6 标签排序接口 #### 需求描述 + 调整标签的显示顺序,支持热门标签前置。 #### 接口名称 + 标签排序接口 #### 设计原因 + - 热门标签需要前置显示 - 按业务重要性排序 - 提升标签选择效率 #### 请求类型 + PUT #### 接口路径 + `/tags/sort` #### 接口参数 + ```typescript { type?: string; // 标签类型,为空则排序全部 @@ -6719,18 +7336,20 @@ PUT ``` #### 响应格式 + ```json { - "code": 0, - "message": "标签排序更新成功", - "data": { - "type": "user", - "updated": 6 - } + "code": 0, + "message": "标签排序更新成功", + "data": { + "type": "user", + "updated": 6 + } } ``` #### 开发思路 + 1. 权限检查(需要tag:sort权限) 2. 验证标签类型一致性 3. 批量更新sortOrder @@ -6742,23 +7361,29 @@ PUT ## 9.6 通用排序接口 #### 需求描述 + 提供通用的排序接口,支持多种数据类型的排序。 #### 接口名称 + 通用排序接口 #### 设计原因 + - 统一的排序逻辑 - 减少代码重复 - 便于维护和扩展 #### 请求类型 + PUT #### 接口路径 + `/common/sort` #### 接口参数 + ```typescript { table: string; // 表名:roles,permissions,organizations,tags @@ -6772,19 +7397,21 @@ PUT ``` #### 响应格式 + ```json { - "code": 0, - "message": "排序更新成功", - "data": { - "table": "roles", - "scope": "pid=1", - "updated": 5 - } + "code": 0, + "message": "排序更新成功", + "data": { + "table": "roles", + "scope": "pid=1", + "updated": 5 + } } ``` #### 开发思路 + 1. 验证表名和字段的安全性 2. 权限检查(根据表名动态判断) 3. 构建更新SQL @@ -6793,7 +7420,8 @@ PUT 6. 返回更新结果 --- -``` + +```` 很好的问题!我已经为您补充了完整的排序接口设计。让我总结一下排序功能的完整方案: @@ -6842,9 +7470,10 @@ PUT sortOrder: number; }> } -``` +```` #### 🎯 拖拽支持 + ```typescript // 支持拖拽操作,自动计算新位置 { @@ -6854,16 +7483,18 @@ PUT ``` #### 🔄 缓存管理 + ```javascript // 排序更新后自动清除相关缓存 1. 清除角色缓存 -2. 清除权限缓存 +2. 清除权限缓存 3. 通知前端刷新 ``` ### 5️⃣ 排序的最佳实践 #### 📋 数据库设计 + ```sql -- 每个需要排序的表都有sortOrder字段 ALTER TABLE sys_roles ADD COLUMN sort_order INT DEFAULT 0; @@ -6874,57 +7505,63 @@ CREATE INDEX idx_roles_sort ON sys_roles(pid, sort_order); ``` #### 🔧 前端实现建议 + ```typescript // 1. 拖拽排序组件 const handleDragEnd = (result) => { - const { source, destination } = result; - if (!destination) return; - - // 调用排序接口 - updateSort({ - targetIndex: destination.index, - pid: currentParentId - }); + const { source, destination } = result; + if (!destination) return; + + // 调用排序接口 + updateSort({ + targetIndex: destination.index, + pid: currentParentId, + }); }; // 2. 批量排序 const batchUpdateSort = (items) => { - const sortData = items.map((item, index) => ({ - id: item.id, - sortOrder: (index + 1) * 10 // 留间隔便于插入 - })); - - api.put('/roles/sort', { items: sortData }); + const sortData = items.map((item, index) => ({ + id: item.id, + sortOrder: (index + 1) * 10, // 留间隔便于插入 + })); + + api.put('/roles/sort', { items: sortData }); }; ``` ### 6️⃣ 权限控制 + ```typescript // 每个排序接口都有对应的权限 -role:sort // 角色排序权限 -permission:sort // 权限排序权限 -organization:sort // 组织排序权限 -dictionary:sort // 字典排序权限 -tag:sort // 标签排序权限 +role: sort; // 角色排序权限 +permission: sort; // 权限排序权限 +organization: sort; // 组织排序权限 +dictionary: sort; // 字典排序权限 +tag: sort; // 标签排序权限 ``` ## 🚀 使用场景示例 ### 场景1:菜单管理 + 管理员需要调整系统菜单的显示顺序: + ```typescript PUT /permissions/sort { "items": [ { "id": 1, "sortOrder": 10 }, // 系统管理 - { "id": 2, "sortOrder": 20 }, // 用户管理 + { "id": 2, "sortOrder": 20 }, // 用户管理 { "id": 3, "sortOrder": 30 } // 角色管理 ] } ``` ### 场景2:下拉选项优化 + 优化用户状态下拉框的选项顺序: + ```typescript PUT /dictionaries/types/1/items/sort { @@ -6937,7 +7574,9 @@ PUT /dictionaries/types/1/items/sort ``` ### 场景3:拖拽操作 + 用户在前端拖拽角色到新位置: + ```typescript PUT /roles/10/move { @@ -6951,10 +7590,10 @@ PUT /roles/10/move ```typescript // 支持批量更新多个项的排序 { - items: Array<{ - id: number; - sortOrder: number; - }> + items: Array<{ + id: number; + sortOrder: number; + }>; } ``` @@ -6969,7 +7608,7 @@ PUT /roles/10/move ```javascript // 排序更新后自动清除相关缓存 1. 清除角色缓存 -2. 清除权限缓存 +2. 清除权限缓存 3. 通知前端刷新 ``` @@ -6985,34 +7624,34 @@ CREATE INDEX idx_roles_sort ON sys_roles(pid, sort_order); ```typescript // 1. 拖拽排序组件 const handleDragEnd = (result) => { - const { source, destination } = result; - if (!destination) return; - - // 调用排序接口 - updateSort({ - targetIndex: destination.index, - pid: currentParentId - }); + const { source, destination } = result; + if (!destination) return; + + // 调用排序接口 + updateSort({ + targetIndex: destination.index, + pid: currentParentId, + }); }; // 2. 批量排序 const batchUpdateSort = (items) => { - const sortData = items.map((item, index) => ({ - id: item.id, - sortOrder: (index + 1) * 10 // 留间隔便于插入 - })); - - api.put('/roles/sort', { items: sortData }); + const sortData = items.map((item, index) => ({ + id: item.id, + sortOrder: (index + 1) * 10, // 留间隔便于插入 + })); + + api.put('/roles/sort', { items: sortData }); }; ``` ```typescript // 每个排序接口都有对应的权限 -role:sort // 角色排序权限 -permission:sort // 权限排序权限 -organization:sort // 组织排序权限 -dictionary:sort // 字典排序权限 -tag:sort // 标签排序权限 +role: sort; // 角色排序权限 +permission: sort; // 权限排序权限 +organization: sort; // 组织排序权限 +dictionary: sort; // 字典排序权限 +tag: sort; // 标签排序权限 ``` ```typescript @@ -7020,7 +7659,7 @@ PUT /permissions/sort { "items": [ { "id": 1, "sortOrder": 10 }, // 系统管理 - { "id": 2, "sortOrder": 20 }, // 用户管理 + { "id": 2, "sortOrder": 20 }, // 用户管理 { "id": 3, "sortOrder": 30 } // 角色管理 ] } @@ -7066,17 +7705,19 @@ PUT /roles/10/move ## 🎯 推荐的Commit信息: ### 主要提交信息(简洁版): + ```bash git commit -m "feat(api): 新增排序接口设计并优化响应格式" -m "- 新增6个排序相关接口:角色、权限、组织、字典、标签排序" -m "- 支持拖拽排序和批量排序操作" -m "- 简化统一响应格式,移除timestamp和requestId字段" -m "- 完善M2基础用户系统接口设计文档" ``` ### 详细版本(如果需要更详细的描述): + ```bash git commit -m "feat(api): 新增完整的排序接口设计体系 ✨ 新增功能: - 角色排序接口 (PUT /roles/sort) -- 拖拽排序接口 (PUT /roles/{roleId}/move) +- 拖拽排序接口 (PUT /roles/{roleId}/move) - 权限排序接口 (PUT /permissions/sort) - 组织架构排序接口 (PUT /organizations/{pid}/sort) - 字典项排序接口 (PUT /dictionaries/types/{typeId}/items/sort) @@ -7096,17 +7737,18 @@ git commit -m "feat(api): 新增完整的排序接口设计体系 ``` ### 超简洁版本: + ```bash git commit -m "feat(api): 新增排序接口设计和响应格式优化" ``` ## 📋 提交内容摘要: -| 变更类型 | 变更内容 | 文件数量 | -|---------|---------|---------| -| ✨ 新增功能 | 7个排序相关接口设计 | 1 | -| 🔧 格式优化 | 简化响应格式结构 | 1 | -| 📚 文档完善 | 接口设计文档更新 | 1 | +| 变更类型 | 变更内容 | 文件数量 | +| ----------- | ------------------- | -------- | +| ✨ 新增功能 | 7个排序相关接口设计 | 1 | +| 🔧 格式优化 | 简化响应格式结构 | 1 | +| 📚 文档完善 | 接口设计文档更新 | 1 | 我建议使用**主要提交信息(简洁版)**,因为它既包含了关键信息,又保持了简洁性,符合项目的Git提交规范。您觉得哪个版本更合适? @@ -7119,7 +7761,7 @@ git commit -m "feat(api): 新增完整的排序接口设计体系 ✨ 新增功能: - 角色排序接口 (PUT /roles/sort) -- 拖拽排序接口 (PUT /roles/{roleId}/move) +- 拖拽排序接口 (PUT /roles/{roleId}/move) - 权限排序接口 (PUT /permissions/sort) - 组织架构排序接口 (PUT /organizations/{pid}/sort) - 字典项排序接口 (PUT /dictionaries/types/{typeId}/items/sort) @@ -7205,23 +7847,26 @@ git commit -m "feat(docs): 完成星撰平台M2基础用户系统完整设计文 ## 📋 提交内容详细分析: ### 📄 新增文档列表: -| 文档名称 | 类型 | 内容重点 | 文件大小 | -|---------|------|---------|---------| -| M2-基础用户系统-数据库设计.md | 数据库设计 | 12个核心表+ER图+索引优化 | 38KB | -| M2-基础用户系统-接口设计.md | API设计 | 41个接口+9大模块+排序功能 | 55KB | -| 星撰-开发计划.md | 项目规划 | 4阶段开发计划+功能模块 | 7KB | -| 星撰-概要设计.md | 架构设计 | 技术栈+系统架构+开发流程 | 11KB | -| 星撰-工程设计.md | 工程设计 | 技术指标+可行性+前沿技术 | 13KB | -| personal-website-comprehensive-prd.md | 产品需求 | 完整功能需求+用户故事 | 16KB | -| main.md | 功能概述 | 核心功能列表 | 2KB | + +| 文档名称 | 类型 | 内容重点 | 文件大小 | +| ------------------------------------- | ---------- | ------------------------- | -------- | +| M2-基础用户系统-数据库设计.md | 数据库设计 | 12个核心表+ER图+索引优化 | 38KB | +| M2-基础用户系统-接口设计.md | API设计 | 41个接口+9大模块+排序功能 | 55KB | +| 星撰-开发计划.md | 项目规划 | 4阶段开发计划+功能模块 | 7KB | +| 星撰-概要设计.md | 架构设计 | 技术栈+系统架构+开发流程 | 11KB | +| 星撰-工程设计.md | 工程设计 | 技术指标+可行性+前沿技术 | 13KB | +| personal-website-comprehensive-prd.md | 产品需求 | 完整功能需求+用户故事 | 16KB | +| main.md | 功能概述 | 核心功能列表 | 2KB | ### 🔧 修改文件: -| 文件名称 | 修改内容 | 目的 | -|---------|---------|------| + +| 文件名称 | 修改内容 | 目的 | +| -------------------------------------- | ------------ | ------------ | | .cursor/rules/elysia-backend-rules.mdc | 更新开发规范 | 完善项目规范 | -| src/controllers/health.controller.ts | 优化健康检查 | 改进监控功能 | +| src/controllers/health.controller.ts | 优化健康检查 | 改进监控功能 | ### ⚠️ 未跟踪文件: + - `bun.lock` - 依赖锁定文件,建议添加到版本控制 ## 🚀 简化版本(如果需要更简洁): @@ -7239,14 +7884,15 @@ git commit -m "feat(docs): 完成M2基础用户系统完整设计文档 ## 📝 提交建议: 1. **建议添加bun.lock文件**: + ```bash git add bun.lock ``` 2. **这是一个里程碑式的提交**,建议使用详细版本的commit信息,因为: - - 包含了完整的系统设计 - - 是项目的重要文档基础 - - 后续开发将基于这些设计文档 + - 包含了完整的系统设计 + - 是项目的重要文档基础 + - 后续开发将基于这些设计文档 您觉得这个commit信息如何?需要我调整哪些部分吗? @@ -7309,4 +7955,3 @@ git commit -m "feat(docs): 完成M2基础用户系统完整设计文档 ```shellscript git add bun.lock ``` - diff --git a/aiChat/004-cursor_.md b/aiChat/004-cursor_.md index cd552a0..fa3d639 100644 --- a/aiChat/004-cursor_.md +++ b/aiChat/004-cursor_.md @@ -1,4 +1,5 @@ # 分析项目后续计划和开发任务 + _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ --- @@ -16,6 +17,7 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 项目分析与建议 ### 🎯 **项目优势** + 1. **功能完整性**:涵盖了个人网站的核心需求,从内容管理到工具集成 2. **扩展性好**:采用了树形结构、标签系统等灵活设计 3. **实用性强**:密码管理、书签同步等工具功能解决实际需求 @@ -24,22 +26,26 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ### 💡 **建议优化** #### **架构层面** + 1. **微服务化考虑**:建议按模块拆分,便于后续维护和扩展 2. **缓存策略**:热点内容、用户权限等需要缓存支持 3. **搜索引擎**:集成Elasticsearch提升内容搜索体验 4. **CDN支持**:媒体文件、静态资源需要CDN加速 #### **安全层面** + 1. **数据加密**:敏感信息(密码、个人文件)需要加密存储 2. **访问控制**:完善的RBAC权限体系 3. **备份策略**:重要数据需要多重备份 #### **用户体验** + 1. **移动端适配**:响应式设计或开发App 2. **离线支持**:PWA技术支持离线阅读 3. **实时通知**:WebSocket支持实时评论、点赞通知 ### 📋 **开发优先级建议** + 1. **第一期**:基础用户系统 + 笔记系统核心功能 2. **第二期**:博客发布 + 个人空间 3. **第三期**:阅读模块 + 基础工具 @@ -50,27 +56,32 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ 在进行详细的数据库设计之前,我需要了解你的一些要求: ### A. 数据库选择 + 1. **MySQL** - 关系型,适合复杂查询 2. **PostgreSQL** - 功能更强大,支持JSON等 3. **MySQL + MongoDB** - 混合架构,结构化+非结构化数据 4. **MySQL + Elasticsearch** - 关系型+搜索引擎 ### B. 性能要求 + 1. 预期用户数量级别?(百、千、万、十万+) 2. 并发访问要求? 3. 数据存储量预期? ### C. 扩展性要求 + 1. 是否考虑分库分表? 2. 是否需要读写分离? 3. 是否考虑多地域部署? ### D. 特殊需求 + 1. 文件存储方式?(本地、OSS、自建NAS) 2. 媒体文件处理需求?(压缩、转码、缩略图) 3. 搜索功能重要程度?(简单搜索 vs 全文搜索) ### E. 开发环境 + 1. 是否需要Docker化部署? 2. 测试环境数据库要求? 3. 备份和恢复策略? @@ -95,9 +106,11 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 引言/概述 ### 项目背景 + 构建一个集成化的个人网站平台,涵盖内容创作、知识管理、生活记录、实用工具等多个维度,旨在打造一个功能完整、用户体验优秀的个人数字空间。 ### 核心价值 + - **知识管理中心**:统一管理个人笔记、博客、阅读内容 - **生活记录平台**:记录和分享个人生活点滴 - **实用工具集**:集成常用的个人效率工具 @@ -106,12 +119,14 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 目标 ### 主要目标 + 1. **建立完整的个人数字资产管理体系** 2. **提供便捷的内容创作和发布平台** 3. **构建实用的个人效率工具集** 4. **实现智能化的知识管理和检索** ### 可衡量目标 + - 支持多种内容格式(Markdown、富文本、思维导图、画板) - 实现秒级内容搜索响应 - 支持10+种实用工具集成 @@ -122,7 +137,9 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ### 1. 基础用户系统模块 #### 1.1 字典管理子系统 + **功能描述**:维护系统中使用的各类数据字典 + - **地区字典**:省份、城市、区县三级联动 - **分类字典**:菜单类别、标签类别、文件类型等 - **组织字典**:部门类型、角色类型、权限类型 @@ -130,265 +147,298 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ - **扩展字典**:支持自定义字典类型 #### 1.2 角色权限子系统 + **功能描述**:基于RBAC模型的权限管理体系 + - **角色管理**: - - 树形结构的角色层级 - - 角色继承机制 - - 角色标签和属性 - - 角色排序和分组 + - 树形结构的角色层级 + - 角色继承机制 + - 角色标签和属性 + - 角色排序和分组 - **权限管理**: - - 页面访问权限 - - 接口调用权限 - - 数据访问权限(行级、字段级) - - 操作权限(增删改查) + - 页面访问权限 + - 接口调用权限 + - 数据访问权限(行级、字段级) + - 操作权限(增删改查) - **权限分配**: - - 下级角色只能分配上级已有权限 - - 批量权限分配 - - 权限模板机制 + - 下级角色只能分配上级已有权限 + - 批量权限分配 + - 权限模板机制 #### 1.3 组织架构子系统 + **功能描述**:管理用户组织关系 + - **组织结构**: - - 树形组织架构 - - 支持多级嵌套 - - 组织属性和标签 + - 树形组织架构 + - 支持多级嵌套 + - 组织属性和标签 - **用户归属**: - - 用户可属于多个组织 - - 主组织和辅助组织 - - 组织内角色分配 + - 用户可属于多个组织 + - 主组织和辅助组织 + - 组织内角色分配 #### 1.4 用户管理子系统 + **功能描述**:完整的用户生命周期管理 + - **用户注册**: - - 邮箱注册验证 - - 手机号注册(可选) - - 社交账号登录集成 + - 邮箱注册验证 + - 手机号注册(可选) + - 社交账号登录集成 - **用户认证**: - - 密码登录 - - 登录错误次数限制 - - 账户锁定和解锁机制 - - 双因子认证(可选) + - 密码登录 + - 登录错误次数限制 + - 账户锁定和解锁机制 + - 双因子认证(可选) - **用户信息**: - - 基础信息:用户名、邮箱、手机、头像 - - 扩展信息:个人简介、兴趣标签、社交链接 - - 隐私设置:信息可见性控制 + - 基础信息:用户名、邮箱、手机、头像 + - 扩展信息:个人简介、兴趣标签、社交链接 + - 隐私设置:信息可见性控制 - **账户管理**: - - 密码修改和重置 - - 找回密码(邮箱/手机) - - 账户禁用和注销 - - Root超级管理员 + - 密码修改和重置 + - 找回密码(邮箱/手机) + - 账户禁用和注销 + - Root超级管理员 ### 2. 内容管理系统模块 #### 2.1 笔记管理子系统 + **功能描述**:个人知识管理和笔记系统 + - **笔记结构**: - - 树形文件夹结构 - - 无限层级嵌套 - - 文件夹和笔记混合组织 + - 树形文件夹结构 + - 无限层级嵌套 + - 文件夹和笔记混合组织 - **内容格式**: - - Markdown编辑器 - - 富文本编辑器 - - 思维导图工具 - - 在线画板工具 - - 代码片段高亮 + - Markdown编辑器 + - 富文本编辑器 + - 思维导图工具 + - 在线画板工具 + - 代码片段高亮 - **笔记功能**: - - 实时保存 - - 版本历史 - - 标签分类 - - 全文搜索 - - 笔记链接和引用 - - 附件管理(图片、文档、音视频) + - 实时保存 + - 版本历史 + - 标签分类 + - 全文搜索 + - 笔记链接和引用 + - 附件管理(图片、文档、音视频) - **协作功能**: - - 笔记分享(链接分享、权限控制) - - 协作编辑(可选) - - 评论和批注 + - 笔记分享(链接分享、权限控制) + - 协作编辑(可选) + - 评论和批注 #### 2.2 博客发布子系统 + **功能描述**:将笔记转化为公开博客 + - **发布机制**: - - 笔记一键发布为博客 - - 博客独立存储(修改笔记不影响已发布博客) - - 绑定关系维护 - - 重新发布和更新机制 + - 笔记一键发布为博客 + - 博客独立存储(修改笔记不影响已发布博客) + - 绑定关系维护 + - 重新发布和更新机制 - **博客管理**: - - 发布状态管理(草稿、已发布、下线) - - SEO优化(标题、描述、关键词) - - 分类和标签 - - 发布时间控制 + - 发布状态管理(草稿、已发布、下线) + - SEO优化(标题、描述、关键词) + - 分类和标签 + - 发布时间控制 - **互动功能**: - - 点赞和收藏 - - 评论系统(层级评论) - - 阅读量统计 - - 分享功能 + - 点赞和收藏 + - 评论系统(层级评论) + - 阅读量统计 + - 分享功能 - **展示功能**: - - 博客列表和详情页 - - 分类和标签筛选 - - 搜索功能 - - RSS订阅 + - 博客列表和详情页 + - 分类和标签筛选 + - 搜索功能 + - RSS订阅 #### 2.3 AI集成子系统 + **功能描述**:AI增强的内容创作和知识管理 + - **内容创作辅助**: - - AI写作建议 - - 文本润色和优化 - - 自动摘要生成 - - 关键词提取 + - AI写作建议 + - 文本润色和优化 + - 自动摘要生成 + - 关键词提取 - **知识库功能**: - - 笔记内容向量化 - - 智能检索和推荐 - - 相关内容关联 - - 问答系统 + - 笔记内容向量化 + - 智能检索和推荐 + - 相关内容关联 + - 问答系统 - **MCP集成**: - - 多模态内容处理 - - 代码理解和生成 - - 图像识别和描述 + - 多模态内容处理 + - 代码理解和生成 + - 图像识别和描述 ### 3. 个人空间模块 #### 3.1 生活日志子系统 + **功能描述**:记录和分享个人生活 + - **内容类型**: - - 文字日志 - - 图片分享(支持多图) - - 视频分享 - - 位置签到 - - 心情记录 + - 文字日志 + - 图片分享(支持多图) + - 视频分享 + - 位置签到 + - 心情记录 - **组织方式**: - - 时间轴展示 - - 分类管理 - - 标签系统 - - 重要程度标记 + - 时间轴展示 + - 分类管理 + - 标签系统 + - 重要程度标记 - **隐私控制**: - - 公开/私密/好友可见 - - 精细化权限控制 - - 访客记录 + - 公开/私密/好友可见 + - 精细化权限控制 + - 访客记录 #### 3.2 社交互动子系统 + **功能描述**:与访客的互动功能 + - **互动功能**: - - 点赞系统 - - 评论功能 - - 收藏功能 - - 分享功能 + - 点赞系统 + - 评论功能 + - 收藏功能 + - 分享功能 - **通知系统**: - - 实时通知 - - 消息聚合 - - 邮件通知(可选) + - 实时通知 + - 消息聚合 + - 邮件通知(可选) ### 4. 阅读管理模块 #### 4.1 电子书管理子系统 + **功能描述**:个人电子书库管理 + - **格式支持**: - - PDF、EPUB、MOBI、TXT等 - - 在线阅读器 - - 进度同步 + - PDF、EPUB、MOBI、TXT等 + - 在线阅读器 + - 进度同步 - **管理功能**: - - 书籍分类和标签 - - 阅读进度跟踪 - - 书签管理 - - 笔记和批注 + - 书籍分类和标签 + - 阅读进度跟踪 + - 书签管理 + - 笔记和批注 - **AI功能**: - - 内容解读和总结 - - 智能问答 - - 知识点提取 + - 内容解读和总结 + - 智能问答 + - 知识点提取 #### 4.2 阅读社区子系统 + **功能描述**:阅读交流和分享 + - **分享功能**: - - 读书笔记分享 - - 书评发布 - - 阅读进度分享 + - 读书笔记分享 + - 书评发布 + - 阅读进度分享 - **互动功能**: - - 评论和讨论 - - 推荐系统 - - 阅读排行 + - 评论和讨论 + - 推荐系统 + - 阅读排行 ### 5. 实用工具模块 #### 5.1 密码管理子系统 + **功能描述**:个人密码安全管理 + - **密码存储**: - - 加密存储 - - 分类管理 - - 标签系统 + - 加密存储 + - 分类管理 + - 标签系统 - **浏览器插件**: - - 自动填充 - - 密码生成 - - 同步功能 + - 自动填充 + - 密码生成 + - 同步功能 - **安全功能**: - - 主密码保护 - - 二次验证 - - 安全审计 + - 主密码保护 + - 二次验证 + - 安全审计 #### 5.2 书签同步子系统 + **功能描述**:跨设备书签管理 + - **同步功能**: - - 浏览器书签导入/导出 - - 实时同步 - - 冲突解决 + - 浏览器书签导入/导出 + - 实时同步 + - 冲突解决 - **管理功能**: - - 分类整理 - - 标签管理 - - 搜索功能 + - 分类整理 + - 标签管理 + - 搜索功能 - **浏览器插件**: - - 一键保存 - - 快速访问 - - 标签管理 + - 一键保存 + - 快速访问 + - 标签管理 #### 5.3 第三方平台同步子系统 + **功能描述**:整合各平台数据 + - **Bilibili收藏同步**: - - 收藏视频同步 - - 分类管理 - - 观看进度 + - 收藏视频同步 + - 分类管理 + - 观看进度 - **音乐平台同步**: - - QQ音乐歌单同步 - - 网易云音乐同步 - - 播放列表管理 + - QQ音乐歌单同步 + - 网易云音乐同步 + - 播放列表管理 #### 5.4 文件管理子系统 + **功能描述**:个人云存储和媒体中心 + - **存储功能**: - - NAS集成 - - 文件上传下载 - - 文件夹管理 - - 权限控制 + - NAS集成 + - 文件上传下载 + - 文件夹管理 + - 权限控制 - **媒体功能**: - - 在线音视频播放 - - 图片预览 - - 文档在线查看 + - 在线音视频播放 + - 图片预览 + - 文档在线查看 - **传输功能**: - - WebRTC点对点传输 - - 阿里云OSS集成 - - 断点续传 + - WebRTC点对点传输 + - 阿里云OSS集成 + - 断点续传 #### 5.5 信息聚合子系统 + **功能描述**:热点信息和内容聚合 + - **新闻聚合**: - - 多源新闻抓取 - - 分类展示 - - 关键词过滤 + - 多源新闻抓取 + - 分类展示 + - 关键词过滤 - **网页存储**: - - 网页离线保存 - - PDF格式转换 - - 全文搜索 + - 网页离线保存 + - PDF格式转换 + - 全文搜索 ## 技术架构需求 ### 后端技术栈 + - **框架**:Elysia + Bun.js - **数据库**:MySQL (主) + Redis (缓存) + Elasticsearch (搜索) - **对象存储**:支持本地存储、阿里云OSS、自建NAS - **消息队列**:Redis/RabbitMQ(处理异步任务) ### 前端技术栈 + - **Web端**:Vue.js/React + TypeScript - **移动端**:PWA或React Native - **浏览器插件**:Manifest V3 ### 基础设施 + - **部署**:Docker + Docker Compose - **监控**:日志收集、性能监控、错误追踪 - **安全**:HTTPS、数据加密、访问控制 @@ -396,16 +446,19 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 非功能性需求 ### 性能要求 + - **响应时间**:页面加载<2s,接口响应<500ms - **并发支持**:支持1000+并发用户 - **可用性**:99.9%系统可用性 ### 安全要求 + - **数据加密**:敏感数据加密存储和传输 - **访问控制**:完善的认证和授权机制 - **审计日志**:关键操作日志记录 ### 扩展性要求 + - **模块化设计**:支持功能模块独立部署 - **API设计**:RESTful API,支持版本控制 - **数据库**:支持读写分离和分库分表 @@ -413,24 +466,28 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 开发优先级 ### 第一期:核心基础(MVP) + 1. 基础用户系统(用户管理、角色权限) 2. 笔记系统核心功能 3. 基础博客发布 4. 系统基础架构 ### 第二期:内容增强 + 1. 完整的博客系统 2. 个人空间功能 3. 基础工具(密码管理、书签) 4. 搜索优化 ### 第三期:高级功能 + 1. 阅读管理系统 2. 文件管理系统 3. AI集成功能 4. 第三方平台同步 ### 第四期:生态完善 + 1. 移动端应用 2. 浏览器插件 3. 高级分析功能 @@ -439,11 +496,13 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 风险与挑战 ### 技术风险 + - **数据迁移**:大量历史数据的迁移和同步 - **性能优化**:大文件上传和处理 - **第三方集成**:API变更和限制 ### 产品风险 + - **功能复杂度**:避免功能过于复杂影响用户体验 - **数据安全**:个人敏感数据的安全保护 - **兼容性**:跨浏览器和设备兼容 @@ -451,16 +510,19 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 成功指标 ### 用户指标 + - 日活跃用户数 - 用户留存率 - 功能使用率 ### 技术指标 + - 系统响应时间 - 错误率控制 - 数据备份完整性 ### 业务指标 + - 内容创作量 - 用户满意度 - 功能完成度 @@ -468,16 +530,19 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 后续规划 ### 短期目标(6个月内) + - 完成第一期功能开发 - 系统稳定性优化 - 基础功能测试 ### 中期目标(1年内) + - 完成主要功能模块 - 移动端支持 - 性能优化 ### 长期目标(1年以上) + - AI功能深度集成 - 开放API生态 - 多语言支持 @@ -488,24 +553,28 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 📋 **文档亮点** ### 🎯 **结构化整理** + - 将你的5大功能模块细分为15个子系统 - 每个子系统都有明确的功能描述和实现要点 - 从简单的功能列表升级为完整的产品规划 ### 🔧 **技术架构明确** + - 基于你现有的 Elysia + Bun.js 技术栈 - 合理的数据库架构(MySQL + Redis + Elasticsearch) - 考虑了扩展性和性能需求 ### 📈 **开发优先级规划** + - **第一期(MVP)**:基础用户系统 + 笔记核心功能 - **第二期**:完整博客系统 + 个人空间 - **第三期**:阅读管理 + 高级工具 - **第四期**:移动端 + 生态完善 ### 💡 **重要补充** + 1. **非功能性需求**:性能、安全、扩展性指标 -2. **风险识别**:技术风险和产品风险预判 +2. **风险识别**:技术风险和产品风险预判 3. **成功指标**:可衡量的目标设定 4. **后续规划**:短中长期发展路线 @@ -541,6 +610,7 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ```markdown // ... existing code ... + - 提供完整的权限管理体系 ## 用户故事 (User Stories) @@ -556,41 +626,48 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ### 1. 基础用户系统模块 // ... existing code ... + - **状态字典**:用户状态、内容状态、审核状态等 - **扩展字典**:支持自定义字典类型 -**验收标准**: + **验收标准**: - 管理员可以增删改查字典类型和字典项。 - 系统关键状态(如用户禁用、启用)应与字典数据关联。 - 前端下拉框、选项等数据应能通过API动态获取字典。 #### 1.2 角色权限子系统 + **功能描述**:基于RBAC模型的权限管理体系 // ... existing code ... + - 批量权限分配 - 权限模板机制 -**验收标准**: + **验收标准**: - Root管理员拥有所有权限,且不可被修改。 - 角色创建时,其可分配的权限不能超过创建者拥有的权限。 - 用户访问未授权的页面或API时,应返回403 Forbidden状态。 - 角色的树形结构应能正确地在前端展示和操作。 #### 1.3 组织架构子系统 + **功能描述**:管理用户组织关系 // ... existing code ... + - 主组织和辅助组织 - 组织内角色分配 -**验收标准**: + **验收标准**: - 用户可以被分配到多个组织节点下。 - 组织架构支持拖拽调整层级和顺序。 - 管理员可以根据组织架构筛选和管理用户。 #### 1.4 用户管理子系统 + **功能描述**:完整的用户生命周期管理 // ... existing code ... + - 找回密码(邮箱/手机) - 账户禁用和注销 - Root超级管理员 -**验收标准**: + **验收标准**: - 新用户注册后,系统需发送验证邮件,点击链接后方可激活账户。 - 用户连续5次输错密码后,账户应被锁定30分钟。 - 用户可以随时修改自己的个人信息(除用户名外)。 @@ -599,28 +676,33 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ### 2. 内容管理系统模块 #### 2.1 笔记管理子系统 + // ... existing code ... + - 协作编辑(可选) - 评论和批注 -**验收标准**: + **验收标准**: - 笔记内容在编辑时应每隔30秒自动保存一次。 - 支持至少三级以上的树形目录结构。 - 全文搜索功能应能在1秒内返回标题或内容包含关键词的结果。 - 分享的笔记链接可以设置有效期和密码。 #### 2.2 博客发布子系统 + **功能描述**:将笔记转化为公开博客 // ... existing code ... + - 分类和标签筛选 - 搜索功能 - RSS订阅 -**验收标准**: + **验收标准**: - 从笔记发布博客后,二者内容各自独立。 - 博客可以关联多个分类和标签。 - 未发布的博客(草稿)在公开列表不可见。 - 评论支持层级回复,并有新评论通知。 // ... existing code ... + - 图像识别和描述 ## 高阶数据模型概览 (High-Level Data Model) @@ -628,33 +710,34 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ 此为概念模型,用于指导数据库设计,具体字段和关系将在详细设计阶段确定。 - **核心实体**: - - `User`: 存储用户信息 (id, username, email, password_hash) - - `Role`: 角色定义 (id, name, description) - - `Permission`: 权限定义 (id, action, resource) - - `Organization`: 组织架构 (id, name, parent_id) - - `Dictionary`: 数据字典 (id, type, key, value) - - `Note`: 笔记内容 (id, user_id, title, content, content_type, parent_id) - - `Post`: 博客文章 (id, user_id, source_note_id, title, content, status) - - `Tag`: 标签 (id, name) - - `Category`: 分类 (id, name) - - `Comment`: 评论 (id, post_id, user_id, content, parent_id) - - `File`: 文件管理 (id, user_id, file_name, path, type) - - `Bookmark`: 书签 (id, user_id, title, url) - - `Password`: 密码记录 (id, user_id, entry_name, username, encrypted_password) + - `User`: 存储用户信息 (id, username, email, password_hash) + - `Role`: 角色定义 (id, name, description) + - `Permission`: 权限定义 (id, action, resource) + - `Organization`: 组织架构 (id, name, parent_id) + - `Dictionary`: 数据字典 (id, type, key, value) + - `Note`: 笔记内容 (id, user_id, title, content, content_type, parent_id) + - `Post`: 博客文章 (id, user_id, source_note_id, title, content, status) + - `Tag`: 标签 (id, name) + - `Category`: 分类 (id, name) + - `Comment`: 评论 (id, post_id, user_id, content, parent_id) + - `File`: 文件管理 (id, user_id, file_name, path, type) + - `Bookmark`: 书签 (id, user_id, title, url) + - `Password`: 密码记录 (id, user_id, entry_name, username, encrypted_password) - **关系**: - - `User` (多) <-> (多) `Role` (通过 `UserRole` 中间表) - - `Role` (多) <-> (多) `Permission` (通过 `RolePermission` 中间表) - - `User` (多) <-> (多) `Organization` (通过 `UserOrganization` 中间表) - - `Note` (一) -> (多) `Post` - - `Post` (多) <-> (多) `Tag` (通过 `PostTag` 中间表) - - `Post` (多) <-> (多) `Category` (通过 `PostCategory` 中间表) - - `Post` (一) -> (多) `Comment` - - `User` (一) -> (多) `Note`, `Post`, `File`, `Bookmark`, `Password` + - `User` (多) <-> (多) `Role` (通过 `UserRole` 中间表) + - `Role` (多) <-> (多) `Permission` (通过 `RolePermission` 中间表) + - `User` (多) <-> (多) `Organization` (通过 `UserOrganization` 中间表) + - `Note` (一) -> (多) `Post` + - `Post` (多) <-> (多) `Tag` (通过 `PostTag` 中间表) + - `Post` (多) <-> (多) `Category` (通过 `PostCategory` 中间表) + - `Post` (一) -> (多) `Comment` + - `User` (一) -> (多) `Note`, `Post`, `File`, `Bookmark`, `Password` ## 关键用户交互流程 (Key User Flows) ### 流程一:新用户注册与激活 + 1. **访问首页**: 用户访问网站,点击“注册”按钮。 2. **填写信息**: 跳转至注册页面,要求输入用户名、邮箱和密码(密码需二次确认)。 3. **前端校验**: 实时校验用户名和邮箱是否已被占用,密码是否符合复杂度要求。 @@ -664,6 +747,7 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ 7. **激活验证**: 后端验证Token有效性,将用户状态更新为`active`,并引导用户至登录页面,提示“激活成功”。 ### 流程二:从笔记到博客的发布 + 1. **创建笔记**: 用户在笔记系统中创建并编辑一篇笔记,内容可以是Markdown、富文本等。 2. **发起发布**: 在笔记编辑页面,用户点击“发布为博客”按钮。 3. **配置博客信息**: 弹窗或新页面要求用户填写博客的URL slug、选择分类、添加标签、设置封面图等。 @@ -676,16 +760,18 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ 6. **发布成功**: 系统提示“发布成功”,并提供查看博客文章的链接。 7. **后续修改**: 用户后续在原笔记上的任何修改,都不会影响已发布的这篇博客文章。用户可在博客管理界面选择“从笔记更新内容”来同步最新修改。 - ### 3. 个人空间模块 #### 3.1 生活日志子系统 + // ... existing code ... + - **部署**:Docker + Docker Compose - **监控**:日志收集、性能监控、错误追踪 - **安全**:HTTPS、数据加密、访问控制 ### API设计原则 + - **RESTful风格**: 使用标准的HTTP方法 (GET, POST, PUT, DELETE, PATCH)。 - **版本控制**: API URL中应包含版本号,如 `/api/v1/...`。 - **统一响应格式**: 所有响应遵循 `{ "code": 0, "message": "success", "data": {} }` 结构。 @@ -696,6 +782,7 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 非功能性需求 ### 性能要求 + // ... existing code ... ``` @@ -761,52 +848,52 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ 为确保项目质量,我们设定以下可量化的关键技术指标: -| 类别 | 指标项 | 目标值 | 备注 | -|--------------|------------------------------|------------------------------------------------|----------------------------------------------| -| **性能** | API 平均响应时间 | < 200ms (95th percentile) | 针对核心数据读写操作 | -| | 复杂查询/搜索响应时间 | < 800ms | 如全文搜索、聚合分析 | -| | 页面首次内容绘制 (FCP) | < 1.5s | 核心页面的加载性能 | -| **并发** | MVP 阶段并发用户数 | 100+ | 系统可稳定支持的并发会话数 | -| | 长期目标并发用户数 | 1000+ | | -| **可用性** | 系统年可用性 (Uptime) | 99.9% | 相当于每年停机时间不超过 8.76 小时 | -| **稳定性** | 核心 API 成功率 | > 99.95% | | -| | 数据备份恢复点目标 (RPO) | 24 小时 | 每日自动备份,最多丢失 24 小时数据 | -| | 数据备份恢复时间目标 (RTO) | < 2 小时 | 从灾难发生到恢复服务所需时间 | -| **安全性** | 安全漏洞响应 | 遵循 OWASP Top 10 防御原则,0 高危漏洞 | 使用自动化工具扫描并定期审查 | -| | 敏感数据处理 | 密码、密钥等使用非对称加密或强哈希算法存储 | | -| **代码质量** | 核心模块单元测试覆盖率 | > 90% | 确保业务逻辑的稳定可靠 | -| | CI/CD 流水线单次执行时间 | < 10 分钟 | 从代码提交到构建部署完成 | +| 类别 | 指标项 | 目标值 | 备注 | +| ------------ | -------------------------- | ------------------------------------------ | ---------------------------------- | +| **性能** | API 平均响应时间 | < 200ms (95th percentile) | 针对核心数据读写操作 | +| | 复杂查询/搜索响应时间 | < 800ms | 如全文搜索、聚合分析 | +| | 页面首次内容绘制 (FCP) | < 1.5s | 核心页面的加载性能 | +| **并发** | MVP 阶段并发用户数 | 100+ | 系统可稳定支持的并发会话数 | +| | 长期目标并发用户数 | 1000+ | | +| **可用性** | 系统年可用性 (Uptime) | 99.9% | 相当于每年停机时间不超过 8.76 小时 | +| **稳定性** | 核心 API 成功率 | > 99.95% | | +| | 数据备份恢复点目标 (RPO) | 24 小时 | 每日自动备份,最多丢失 24 小时数据 | +| | 数据备份恢复时间目标 (RTO) | < 2 小时 | 从灾难发生到恢复服务所需时间 | +| **安全性** | 安全漏洞响应 | 遵循 OWASP Top 10 防御原则,0 高危漏洞 | 使用自动化工具扫描并定期审查 | +| | 敏感数据处理 | 密码、密钥等使用非对称加密或强哈希算法存储 | | +| **代码质量** | 核心模块单元测试覆盖率 | > 90% | 确保业务逻辑的稳定可靠 | +| | CI/CD 流水线单次执行时间 | < 10 分钟 | 从代码提交到构建部署完成 | --- ## 2. 可行性分析 (Feasibility Analysis) -- **技术可行性**: - - **技术栈成熟度**: 项目选用的 Elysia.js, Bun, Vue/React, MySQL, Redis 等技术均为成熟或高速发展的开源技术,拥有活跃的社区和丰富的文档,技术风险可控。 - - **实现复杂度**: 项目功能虽多,但采用模块化、分阶段(MVP)的开发策略,可将复杂系统分解为多个可管理的小模块,降低了单次开发的复杂度。 - - **团队技能**: 核心开发人员具备所需的全栈技术能力,能够驾驭该技术栈。 +- **技术可行性**: + - **技术栈成熟度**: 项目选用的 Elysia.js, Bun, Vue/React, MySQL, Redis 等技术均为成熟或高速发展的开源技术,拥有活跃的社区和丰富的文档,技术风险可控。 + - **实现复杂度**: 项目功能虽多,但采用模块化、分阶段(MVP)的开发策略,可将复杂系统分解为多个可管理的小模块,降低了单次开发的复杂度。 + - **团队技能**: 核心开发人员具备所需的全栈技术能力,能够驾驭该技术栈。 -- **经济可行性**: - - **初期成本**: 主要成本为服务器托管费用。初期可利用云服务商的免费套餐或低成本VPS,成本极低。所有核心软件均为开源,无授权费用。 - - **长期成本**: 随着用户量和数据量的增长,服务器和存储成本会线性增加,但成本模型清晰可控。 - - **开发成本**: 作为个人项目,主要为时间成本,无直接的人力资源开销。 +- **经济可行性**: + - **初期成本**: 主要成本为服务器托管费用。初期可利用云服务商的免费套餐或低成本VPS,成本极低。所有核心软件均为开源,无授权费用。 + - **长期成本**: 随着用户量和数据量的增长,服务器和存储成本会线性增加,但成本模型清晰可控。 + - **开发成本**: 作为个人项目,主要为时间成本,无直接的人力资源开销。 -- **操作与维护可行性**: - - **部署**: 采用 Docker 和 Docker Compose 进行容器化部署,实现了环境的标准化和一键部署,极大降低了操作和维护的复杂性。 - - **监控**: 规划了完善的日志和监控体系,能够主动发现和定位问题,提升了系统的可维护性。 - - **开发流程**: 明确的 PRD 和工程设计文档,结合 CI/CD 自动化流程,保障了开发过程的规范性和高效性。 +- **操作与维护可行性**: + - **部署**: 采用 Docker 和 Docker Compose 进行容器化部署,实现了环境的标准化和一键部署,极大降低了操作和维护的复杂性。 + - **监控**: 规划了完善的日志和监控体系,能够主动发现和定位问题,提升了系统的可维护性。 + - **开发流程**: 明确的 PRD 和工程设计文档,结合 CI/CD 自动化流程,保障了开发过程的规范性和高效性。 --- ## 3. 技术选型与论证 (Technology Stack Selection & Justification) -| 领域 | 技术选型 | 选型论证 | -|----------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **后端运行时** | **Bun.js** | 极高的执行效率,内置打包器、测试器、包管理器,提供了现代、一体化的开发体验,与 Elysia.js 完美契合。 | -| **后端框架** | **Elysia.js** | 基于 Bun 设计,性能卓越。提供端到端的类型安全(Type-Safe),开发体验极佳,插件生态丰富,非常适合构建高性能、类型健壮的 API 服务。 | -| **前端框架** | **Vue.js / React** | 两者均为业界领先的声明式 UI 框架,拥有强大的生态系统和组件库。选择其一可快速构建复杂、交互丰富的单页应用(SPA)。最终选择可根据个人偏好决定。 | +| 领域 | 技术选型 | 选型论证 | +| -------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **后端运行时** | **Bun.js** | 极高的执行效率,内置打包器、测试器、包管理器,提供了现代、一体化的开发体验,与 Elysia.js 完美契合。 | +| **后端框架** | **Elysia.js** | 基于 Bun 设计,性能卓越。提供端到端的类型安全(Type-Safe),开发体验极佳,插件生态丰富,非常适合构建高性能、类型健壮的 API 服务。 | +| **前端框架** | **Vue.js / React** | 两者均为业界领先的声明式 UI 框架,拥有强大的生态系统和组件库。选择其一可快速构建复杂、交互丰富的单页应用(SPA)。最终选择可根据个人偏好决定。 | | **数据库** | **MySQL (主) + Redis (缓存) + Elasticsearch (搜索)** | **MySQL**: 成熟可靠的关系型数据库,提供事务支持和数据一致性,适合存储结构化的核心业务数据。**Redis**: 高性能内存数据库,用于缓存、会话管理、消息队列等场景,能极大提升系统响应速度。**Elasticsearch**: 强大的全文搜索引擎,为笔记、博客等内容提供毫秒级的复杂搜索能力,是内容驱动型平台的关键组件。 | -| **部署方案** | **Docker + Docker Compose** | 实现开发、测试、生产环境的一致性,简化部署流程,便于服务的水平扩展和管理。 | +| **部署方案** | **Docker + Docker Compose** | 实现开发、测试、生产环境的一致性,简化部署流程,便于服务的水平扩展和管理。 | --- @@ -814,10 +901,10 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ 本项目在设计上保持开放,为未来集成前沿技术预留了接口: -- **深度 AI 集成 (LLM/RAG)**: 当前规划的 AI 功能是基础。未来可引入基于私有数据(笔记、文档)的检索增强生成(RAG)技术,将平台打造成一个真正个性化的智能问答和创作助手。 -- **去中心化身份与存储 (DID/IPFS)**: 探索使用去中心化身份标识(DID)进行用户认证,增强用户的自主权。核心的、不可变的笔记或博客可选择性地发布到 IPFS/Arweave 等去中心化存储网络,实现数据的永久保存和抗审查。 -- **边缘计算 (Edge Computing)**: 对于静态资源和一些低延迟要求的 API,可以利用边缘计算节点进行分发和计算,为全球用户提供更快的访问速度。 -- **WebAssembly (WASM)**: 对于前端的性能敏感模块,如富文本编辑器、思维导图、在线画板的核心引擎,未来可采用 Rust/C++ 编写并通过 WASM 在浏览器中运行,以获得接近原生的性能。 +- **深度 AI 集成 (LLM/RAG)**: 当前规划的 AI 功能是基础。未来可引入基于私有数据(笔记、文档)的检索增强生成(RAG)技术,将平台打造成一个真正个性化的智能问答和创作助手。 +- **去中心化身份与存储 (DID/IPFS)**: 探索使用去中心化身份标识(DID)进行用户认证,增强用户的自主权。核心的、不可变的笔记或博客可选择性地发布到 IPFS/Arweave 等去中心化存储网络,实现数据的永久保存和抗审查。 +- **边缘计算 (Edge Computing)**: 对于静态资源和一些低延迟要求的 API,可以利用边缘计算节点进行分发和计算,为全球用户提供更快的访问速度。 +- **WebAssembly (WASM)**: 对于前端的性能敏感模块,如富文本编辑器、思维导图、在线画板的核心引擎,未来可采用 Rust/C++ 编写并通过 WASM 在浏览器中运行,以获得接近原生的性能。 --- @@ -825,8 +912,8 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ 本项目的核心实用价值在于 **“终极整合”** 与 **“数据主权”**。 -- **解决的核心痛点**: 现代人的数字信息散落在各个孤立的平台(笔记在A应用,书签在B浏览器,密码在C工具,博客在D平台)。“星撰”旨在通过一个统一的平台整合这些功能,消除切换成本,构建个人数据的内在联系。 -- **为用户创造的价值**: +- **解决的核心痛点**: 现代人的数字信息散落在各个孤立的平台(笔记在A应用,书签在B浏览器,密码在C工具,博客在D平台)。“星撰”旨在通过一个统一的平台整合这些功能,消除切换成本,构建个人数据的内在联系。 +- **为用户创造的价值**: 1. **数据所有权**: 用户数据完全存储在自己的服务器上,彻底摆脱对商业平台的依赖和隐私泄露的担忧。 2. **高度可定制**: 作为一个自建项目,可以根据个人需求无限扩展和定制功能,不受商业产品的限制。 3. **效率提升**: 将多种高频工具集成,减少了跨应用的操作,提升了日常工作和学习的效率。 @@ -837,34 +924,34 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ 尽管项目初衷为个人使用,但其架构和功能具备一定的商用潜力。 -- **潜在市场**: 面向注重数据隐私、有极客精神、需要高度定制化解决方案的个人开发者、知识工作者、内容创作者。 -- **商业模式探索**: +- **潜在市场**: 面向注重数据隐私、有极客精神、需要高度定制化解决方案的个人开发者、知识工作者、内容创作者。 +- **商业模式探索**: 1. **SaaS 订阅服务**: 提供托管服务,用户无需自行部署。可通过免费增值模式(Freemium)吸引用户,高级功能(如无限AI调用、团队协作、更大存储空间)收费。 2. **开源+付费支持/高级版**: 项目核心开源,吸引社区贡献。提供付费的企业级支持、高级插件或一键部署的商业版。 3. **模板/插件市场**: 建立一个生态,允许开发者创建和销售主题模板、功能插件。 -- **核心竞争力**: 与 Notion, Obsidian 等产品的竞争中,其核心优势是 **开源、可自托管、高度集成**。这对于目标市场中的特定群体具有强大吸引力。 +- **核心竞争力**: 与 Notion, Obsidian 等产品的竞争中,其核心优势是 **开源、可自托管、高度集成**。这对于目标市场中的特定群体具有强大吸引力。 --- ## 7. 稳定性与可靠性设计 (Stability & Reliability Design) -- **冗余与备份**: 数据库采用主从复制(Read Replicas)实现读写分离和高可用。实施每日全量备份和增量备份策略,并定期进行恢复演练。 +- **冗余与备份**: 数据库采用主从复制(Read Replicas)实现读写分离和高可用。实施每日全量备份和增量备份策略,并定期进行恢复演练。 - - **无状态服务**: 后端 API 设计为无状态,所有会话信息存储在 Redis 中。这使得服务可以轻松地进行水平扩展和故障切换。 -- **优雅降级**: 当 Elasticsearch 或 Redis 等非核心依赖出现故障时,系统应能自动降级服务(如暂时关闭搜索功能、使用数据库替代缓存),保证核心功能(如读写笔记)不受影响。 -- **健康检查**: 对外提供健康检查 API (`/health`),供负载均衡和监控系统调用,实现故障节点的自动摘除。 -- **配置中心**: 所有配置(数据库连接、密钥等)通过环境变量或配置中心管理,实现配置与代码分离,增强安全性与灵活性。 +- **优雅降级**: 当 Elasticsearch 或 Redis 等非核心依赖出现故障时,系统应能自动降级服务(如暂时关闭搜索功能、使用数据库替代缓存),保证核心功能(如读写笔记)不受影响。 +- **健康检查**: 对外提供健康检查 API (`/health`),供负载均衡和监控系统调用,实现故障节点的自动摘除。 +- **配置中心**: 所有配置(数据库连接、密钥等)通过环境变量或配置中心管理,实现配置与代码分离,增强安全性与灵活性。 --- ## 8. 可拓展性设计 (Scalability & Extensibility Design) -- **水平扩展 (Scale Out)**: 无状态的后端服务设计,使其可以轻易地在负载均衡器后部署多个实例,以应对增长的流量。 -- **垂直扩展 (Scale Up)**: 系统也可以通过增加单个服务器的 CPU 和内存来提升处理能力。 -- **数据库扩展**: 初期采用读写分离。未来若数据量巨大,可根据业务模块进行垂直拆分(如用户库、内容库),或对超大表进行水平分片(Sharding)。 -- **功能扩展 (Extensibility)**: - - **模块化架构**: `src` 目录下的 `controllers`, `services`, `plugins` 结构清晰,新增功能模块不会影响现有逻辑。 - - **插件化设计**: 核心功能稳定后,可设计一套正式的插件机制,允许社区或自己以插件形式开发新功能(如新的编辑器、新的同步工具),实现真正的“热插拔”式扩展。 - - **事件驱动**: 通过引入消息队列,采用事件驱动模式解耦核心业务和附加业务(如发送邮件、生成通知、索引文档),提高系统的响应能力和可扩展性。 +- **水平扩展 (Scale Out)**: 无状态的后端服务设计,使其可以轻易地在负载均衡器后部署多个实例,以应对增长的流量。 +- **垂直扩展 (Scale Up)**: 系统也可以通过增加单个服务器的 CPU 和内存来提升处理能力。 +- **数据库扩展**: 初期采用读写分离。未来若数据量巨大,可根据业务模块进行垂直拆分(如用户库、内容库),或对超大表进行水平分片(Sharding)。 +- **功能扩展 (Extensibility)**: + - **模块化架构**: `src` 目录下的 `controllers`, `services`, `plugins` 结构清晰,新增功能模块不会影响现有逻辑。 + - **插件化设计**: 核心功能稳定后,可设计一套正式的插件机制,允许社区或自己以插件形式开发新功能(如新的编辑器、新的同步工具),实现真正的“热插拔”式扩展。 + - **事件驱动**: 通过引入消息队列,采用事件驱动模式解耦核心业务和附加业务(如发送邮件、生成通知、索引文档),提高系统的响应能力和可扩展性。 --- @@ -910,7 +997,7 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ 我将为你创建 `星撰-概要设计.md` 文件。 -```markdown +````markdown # 星撰个人综合平台 - 软件开发概要设计 --- @@ -918,52 +1005,61 @@ _Exported on 2025/6/29 at GMT+8 3:11:16 from Cursor (1.1.6)_ ## 1. 引言 ### 1.1. 文档目的 + 本概要设计文档旨在为“星撰”个人综合平台项目定义一个完整的软件开发生命周期(SDLC)框架。它将作为项目从规划、设计、实现到部署和维护的最高指导性文件,确保所有开发活动遵循标准化的软件工程最佳实践。 ### 1.2. 项目概述 + “星撰”是一个高度集成的个人数字化中枢,其核心功能、用户故事及非功能性需求已在《个人网站综合平台产品需求文档 (PRD)》中详细定义。本项目旨在将该PRD转化为一个稳定、可扩展、高质量的软件产品。 ### 1.3. 范围 + 本文档覆盖范围包括: -- 项目开发模型的选定。 -- 系统的高层体系结构设计。 -- 各阶段(设计、开发、测试、部署)的规划和规范。 -- 项目管理、风险控制和质量保证策略。 + +- 项目开发模型的选定。 +- 系统的高层体系结构设计。 +- 各阶段(设计、开发、测试、部署)的规划和规范。 +- 项目管理、风险控制和质量保证策略。 --- ## 2. 项目规划与管理 ### 2.1. 开发模型 + 本项目采用 **迭代式增量开发模型 (Iterative and Incremental Development)**。 -- **迭代式**: 将开发过程划分为多个短周期的迭代,每个迭代都包含需求分析、设计、编码、测试的完整流程。 -- **增量式**: 每个迭代都会产出一个可测试、可交付的软件增量。产品功能将分阶段(见PRD中的优先级规划)逐步构建和完善。 + +- **迭代式**: 将开发过程划分为多个短周期的迭代,每个迭代都包含需求分析、设计、编码、测试的完整流程。 +- **增量式**: 每个迭代都会产出一个可测试、可交付的软件增量。产品功能将分阶段(见PRD中的优先级规划)逐步构建和完善。 **优势**: 此模型能够快速交付核心功能(MVP),及时获取反馈,灵活应对需求变化,并有效控制项目风险。 ### 2.2. 项目里程碑 + 项目将按照PRD中的优先级划分为四个主要里程碑(阶段): -| 里程碑 | 名称 | 核心交付物 | 目标 | -|--------|--------------|------------------------------------------------------|------------------------------------| -| M1 | **核心基础 (MVP)** | 基础用户系统、笔记核心功能、基础博客发布、CI/CD流程建立 | 验证核心架构,跑通开发部署全流程 | -| M2 | **内容增强** | 完整博客系统、个人空间、基础工具、搜索引擎集成 | 完善内容生态,提升用户体验 | -| M3 | **高级功能** | 阅读管理、文件管理、AI功能集成、第三方平台同步 | 扩展平台能力,引入智能化 | -| M4 | **生态完善** | 移动端应用、浏览器插件、高级分析功能、性能深度优化 | 构建跨平台生态,实现长期稳定运营 | +| 里程碑 | 名称 | 核心交付物 | 目标 | +| ------ | ------------------ | ------------------------------------------------------- | -------------------------------- | +| M1 | **核心基础 (MVP)** | 基础用户系统、笔记核心功能、基础博客发布、CI/CD流程建立 | 验证核心架构,跑通开发部署全流程 | +| M2 | **内容增强** | 完整博客系统、个人空间、基础工具、搜索引擎集成 | 完善内容生态,提升用户体验 | +| M3 | **高级功能** | 阅读管理、文件管理、AI功能集成、第三方平台同步 | 扩展平台能力,引入智能化 | +| M4 | **生态完善** | 移动端应用、浏览器插件、高级分析功能、性能深度优化 | 构建跨平台生态,实现长期稳定运营 | ### 2.3. 风险管理 -| 风险类别 | 风险描述 | 可能性 | 影响 | 应对策略 | -|--------------|----------------------------------------|--------|------|------------------------------------------------------------------------| -| **技术风险** | 第三方API变更(如B站、QQ音乐) | 中 | 中 | 封装Adapter层,隔离变化;设计优雅降级机制。 | -| | 新技术引入(Bun/Elysia)的未知问题 | 低 | 中 | 保持对社区的关注,构建完善的测试体系以快速发现问题。 | -| **进度风险** | 个人项目时间投入不稳定 | 高 | 高 | 严格遵循任务清单,将大任务分解为小任务,利用碎片时间完成。 | -| **安全风险** | 个人敏感数据(密码、文件)泄露 | 中 | 高 | 强制加密存储;定期进行安全扫描;代码审查时关注安全漏洞。 | + +| 风险类别 | 风险描述 | 可能性 | 影响 | 应对策略 | +| ------------ | ---------------------------------- | ------ | ---- | ---------------------------------------------------------- | +| **技术风险** | 第三方API变更(如B站、QQ音乐) | 中 | 中 | 封装Adapter层,隔离变化;设计优雅降级机制。 | +| | 新技术引入(Bun/Elysia)的未知问题 | 低 | 中 | 保持对社区的关注,构建完善的测试体系以快速发现问题。 | +| **进度风险** | 个人项目时间投入不稳定 | 高 | 高 | 严格遵循任务清单,将大任务分解为小任务,利用碎片时间完成。 | +| **安全风险** | 个人敏感数据(密码、文件)泄露 | 中 | 高 | 强制加密存储;定期进行安全扫描;代码审查时关注安全漏洞。 | --- ## 3. 系统设计 ### 3.1. 系统架构 + 系统采用分层、模块化的架构,实现前后端分离。 ```mermaid @@ -1002,7 +1098,7 @@ graph TD Elasticsearch[(Elasticsearch - 全文搜索)] ObjectStorage[对象存储 (OSS/NAS)] end - + subgraph "第三方服务 (3rd Party Services)" EmailService[邮件服务] OAuth[OAuth 2.0 提供商] @@ -1043,13 +1139,17 @@ graph TD AuthService --> OAuth ContentService --> AI_API ``` -* **客户端层**: 负责用户交互和数据展示。 -* **服务端层**: 核心业务逻辑处理中心。通过API网关统一入口,内部按业务垂直划分为多个服务模块。 -* **数据与存储层**: 持久化存储数据,提供缓存和搜索能力。 -* **第三方服务**: 集成外部服务以完成特定功能。 +```` + +- **客户端层**: 负责用户交互和数据展示。 +- **服务端层**: 核心业务逻辑处理中心。通过API网关统一入口,内部按业务垂直划分为多个服务模块。 +- **数据与存储层**: 持久化存储数据,提供缓存和搜索能力。 +- **第三方服务**: 集成外部服务以完成特定功能。 ### 3.2. 模块划分 + 系统功能将按以下模块进行开发,每个模块对应`src/controllers`, `src/services`下的独立子目录。 + 1. **`core`**: 核心模块,包括用户、角色、权限、组织、字典等。 2. **`content`**: 内容模块,包括笔记、博客、评论、分类、标签等。 3. **`space`**: 个人空间模块,包括生活日志、社交互动。 @@ -1058,77 +1158,90 @@ graph TD 6. **`system`**: 系统模块,包括健康检查、监控、配置管理。 ### 3.3. 详细设计规划 + 本概要设计完成后,将对每个模块进行详细设计,产出物包括: -- **数据库设计文档**: E-R图、表结构定义(字段、类型、约束、索引)、数据字典。 -- **API接口设计文档**: 使用Swagger/OpenAPI规范,定义每个端点的URL、HTTP方法、请求参数、响应体结构及错误码。 -- **UI/UX设计稿**: 针对核心页面和流程,制作线框图或高保真原型。 + +- **数据库设计文档**: E-R图、表结构定义(字段、类型、约束、索引)、数据字典。 +- **API接口设计文档**: 使用Swagger/OpenAPI规范,定义每个端点的URL、HTTP方法、请求参数、响应体结构及错误码。 +- **UI/UX设计稿**: 针对核心页面和流程,制作线框图或高保真原型。 --- ## 4. 实现与编码 ### 4.1. 开发环境与工具 -- **IDE**: Visual Studio Code -- **语言**: TypeScript -- **运行时**: Bun.js -- **包管理器**: Bun -- **版本控制**: Git / GitHub -- **代码检查与格式化**: ESLint / Prettier + +- **IDE**: Visual Studio Code +- **语言**: TypeScript +- **运行时**: Bun.js +- **包管理器**: Bun +- **版本控制**: Git / GitHub +- **代码检查与格式化**: ESLint / Prettier ### 4.2. 编码规范 -- 严格遵守项目已定义的注释、命名、代码风格规范。 -- 所有业务逻辑必须在 `services` 层实现,`controllers` 层只负责路由和数据校验。 -- 禁止在代码中硬编码配置项(如密钥、端口),所有配置通过环境变量或配置文件注入。 + +- 严格遵守项目已定义的注释、命名、代码风格规范。 +- 所有业务逻辑必须在 `services` 层实现,`controllers` 层只负责路由和数据校验。 +- 禁止在代码中硬编码配置项(如密钥、端口),所有配置通过环境变量或配置文件注入。 ### 4.3. 版本控制策略 + 采用 **Gitflow工作流** 的简化版: -- `main`: 主分支,存放稳定、可发布的代码。每个里程碑完成后合并。 -- `develop`: 开发分支,集成了所有已完成的功能。 -- `feat/feature-name`: 功能分支,每个新功能或任务都在此分支开发,完成后合并到 `develop`。 -- `fix/bug-name`: Bug修复分支。 + +- `main`: 主分支,存放稳定、可发布的代码。每个里程碑完成后合并。 +- `develop`: 开发分支,集成了所有已完成的功能。 +- `feat/feature-name`: 功能分支,每个新功能或任务都在此分支开发,完成后合并到 `develop`。 +- `fix/bug-name`: Bug修复分支。 --- ## 5. 测试策略 ### 5.1. 测试层次 -- **单元测试**: 使用 `Vitest` 对 `services` 和 `utils` 中的核心函数进行测试,目标覆盖率 > 90%。 -- **接口测试 (集成测试)**: 使用 `supertest` 或Elysia的内置测试工具,对每个API端点进行测试,验证其与数据库、缓存的交互是否正确。 -- **端到端测试 (系统测试)**: 使用 `Playwright` 或 `Cypress` 对关键用户流程(如注册登录、发布博客)进行自动化测试。 -- **性能测试**: 使用 `autocannon` 对核心API进行压力测试,确保其满足技术指标。 + +- **单元测试**: 使用 `Vitest` 对 `services` 和 `utils` 中的核心函数进行测试,目标覆盖率 > 90%。 +- **接口测试 (集成测试)**: 使用 `supertest` 或Elysia的内置测试工具,对每个API端点进行测试,验证其与数据库、缓存的交互是否正确。 +- **端到端测试 (系统测试)**: 使用 `Playwright` 或 `Cypress` 对关键用户流程(如注册登录、发布博客)进行自动化测试。 +- **性能测试**: 使用 `autocannon` 对核心API进行压力测试,确保其满足技术指标。 ### 5.2. 测试执行 -- 所有代码提交前,必须在本地通过相关单元测试和接口测试。 -- CI/CD流水线将在每次向 `develop` 或 `main` 分支合并代码时,自动运行全量测试用例。测试不通过则禁止合并。 + +- 所有代码提交前,必须在本地通过相关单元测试和接口测试。 +- CI/CD流水线将在每次向 `develop` 或 `main` 分支合并代码时,自动运行全量测试用例。测试不通过则禁止合并。 --- ## 6. 部署与运维 ### 6.1. 部署流程 (CI/CD) + 使用 **GitHub Actions** 自动化构建、测试和部署流程。 + 1. **Push to `feat/*`**: 触发ESLint检查和单元测试。 2. **Merge to `develop`**: 触发全量测试,构建Docker镜像并推送到镜像仓库,然后部署到 **Staging (预发布)** 环境。 3. **Merge to `main`**: 触发全量测试,构建生产Docker镜像,并部署到 **Production (生产)** 环境。 ### 6.2. 基础设施 -- **运行环境**: Docker容器。 -- **容器编排**: Docker Compose (初期) / Kubernetes (未来扩展)。 -- **数据库/缓存**: 使用云服务商提供的托管服务或在Docker中自建。 + +- **运行环境**: Docker容器。 +- **容器编排**: Docker Compose (初期) / Kubernetes (未来扩展)。 +- **数据库/缓存**: 使用云服务商提供的托管服务或在Docker中自建。 ### 6.3. 运维监控 -- **日志**: 使用 `pino` 记录结构化日志,并聚合到统一的日志管理平台(如ELK Stack或Loki)。 -- **监控与告警**: 使用 `Prometheus` 收集应用指标,`Grafana` 进行可视化展示,并针对关键指标(如API错误率、延迟)设置告警。 + +- **日志**: 使用 `pino` 记录结构化日志,并聚合到统一的日志管理平台(如ELK Stack或Loki)。 +- **监控与告警**: 使用 `Prometheus` 收集应用指标,`Grafana` 进行可视化展示,并针对关键指标(如API错误率、延迟)设置告警。 --- ## 7. 维护与演进 -- **问题跟踪**: 使用GitHub Issues跟踪Bug和功能请求。 -- **文档同步**: 所有代码变更,若涉及架构、API或数据库设计,必须同步更新相关设计文档。 -- **定期审查**: 每个里程碑结束后,进行项目复盘,评估开发流程、代码质量和系统架构,并进行优化。 -- **依赖更新**: 定期(如每季度)审查并更新项目依赖,以修复潜在的安全漏洞和利用新特性。 -``` +- **问题跟踪**: 使用GitHub Issues跟踪Bug和功能请求。 +- **文档同步**: 所有代码变更,若涉及架构、API或数据库设计,必须同步更新相关设计文档。 +- **定期审查**: 每个里程碑结束后,进行项目复盘,评估开发流程、代码质量和系统架构,并进行优化。 +- **依赖更新**: 定期(如每季度)审查并更新项目依赖,以修复潜在的安全漏洞和利用新特性。 + +```` 我已经为你创建了 `星撰-概要设计.md` 文件。 @@ -1315,7 +1428,7 @@ graph TD - [ ] 对高频访问的接口增加更精细的缓存策略。 - [ ] **14.2. 安全加固**: 引入 Web 应用防火墙 (WAF),定期进行依赖项安全扫描和代码审计。 - [ ] **14.3. 文档完善**: 完善所有功能的开发者文档和用户手册。 -``` +```` 我已经为你创建了 `星撰-开发计划.md` 文件。 @@ -1347,6 +1460,7 @@ graph TD 我将为你重新组织并更新 `星撰-开发计划.md` 文件。 我将把计划调整为以下几个核心阶段: + 1. **项目初始化与基础设施** (这是任何开发前都必需的) 2. **基础用户系统** 3. **笔记与博客系统** @@ -1365,120 +1479,136 @@ graph TD ## 1. 引言 ### 1.1. 文档目的 + 本文档基于《产品需求文档 (PRD)》和《概要设计》,旨在将项目需求拆分为具体、可执行的开发任务,并将其组织成一个以核心功能模块为导向、分阶段的开发计划。本文档是项目执行阶段的核心路线图。 ### 1.2. 计划结构 + 本计划遵循迭代式增量开发模型,将整个项目划分为多个主要阶段。**此版本已根据 `main.md` 的功能列表进行重组**,每个阶段聚焦于一个完整的功能模块。 --- ## 阶段一: 项目初始化与基础设施 + **目标**: 搭建项目骨架,配置开发环境和规范,建立 CI/CD 基础流程。这是后续所有开发工作的基础。 -- [ ] **1.1. 项目脚手架**: 初始化 Elysia + Bun + TypeScript 项目,配置 `tsconfig.json` 和路径别名。 -- [ ] **1.2. 代码规范**: 配置 ESLint 和 Prettier,确保代码风格和质量一致。 -- [ ] **1.3. 配置文件管理**: 建立 `src/config` 目录,管理数据库、JWT、日志等配置,支持环境变量。 -- [ ] **1.4. 数据库连接**: 编写并测试 MySQL 和 Redis 的连接模块。 -- [ ] **1.5. 核心插件**: 实现全局错误处理、结构化日志 (`pino`) 和 Swagger API 文档插件。 -- [ ] **1.6. 容器化**: 编写 `Dockerfile` 和 `docker-compose.yml`,实现开发环境一键启动。 -- [ ] **1.7. CI/CD 基础**: 搭建基础的 GitHub Actions 工作流,实现提交代码时自动运行 lint 和测试。 + +- [ ] **1.1. 项目脚手架**: 初始化 Elysia + Bun + TypeScript 项目,配置 `tsconfig.json` 和路径别名。 +- [ ] **1.2. 代码规范**: 配置 ESLint 和 Prettier,确保代码风格和质量一致。 +- [ ] **1.3. 配置文件管理**: 建立 `src/config` 目录,管理数据库、JWT、日志等配置,支持环境变量。 +- [ ] **1.4. 数据库连接**: 编写并测试 MySQL 和 Redis 的连接模块。 +- [ ] **1.5. 核心插件**: 实现全局错误处理、结构化日志 (`pino`) 和 Swagger API 文档插件。 +- [ ] **1.6. 容器化**: 编写 `Dockerfile` 和 `docker-compose.yml`,实现开发环境一键启动。 +- [ ] **1.7. CI/CD 基础**: 搭建基础的 GitHub Actions 工作流,实现提交代码时自动运行 lint 和测试。 --- ## 阶段二: 基础用户系统 + **目标**: 实现一个完整的、支持RBAC的用户中心,包含用户、角色、权限、组织和字典等核心功能。 -- [ ] **2.1. 数据库设计**: 设计并创建 `users`, `roles`, `permissions`, `organizations`, `dictionaries` 及相关联的表。 -- [ ] **2.2. 用户认证 API**: - - [ ] `POST /api/v1/auth/register`: 实现用户注册(含数据校验、密码哈希)。 - - [ ] `POST /api/v1/auth/login`: 实现用户登录,成功后返回 JWT。 - - [ ] `GET /api/v1/users/me`: 创建受保护路由,获取当前登录用户信息。 -- [ ] **2.3. JWT与权限中间件**: - - [ ] 实现 JWT 校验中间件。 - - [ ] 实现一个基础的 RBAC 权限校验中间件。 -- [ ] **2.4. 字典管理 CRUD API**: 实现对数据字典类型和条目的增删改查。 -- [ ] **2.5. 角色管理 CRUD API**: 实现对角色的增删改查及权限分配。 -- [ ] **2.6. 组织架构 CRUD API**: 实现树形组织架构的增删改查。 -- [ ] **2.7. 用户管理 CRUD API**: 实现后台对用户的增删改查和角色分配。 + +- [ ] **2.1. 数据库设计**: 设计并创建 `users`, `roles`, `permissions`, `organizations`, `dictionaries` 及相关联的表。 +- [ ] **2.2. 用户认证 API**: + - [ ] `POST /api/v1/auth/register`: 实现用户注册(含数据校验、密码哈希)。 + - [ ] `POST /api/v1/auth/login`: 实现用户登录,成功后返回 JWT。 + - [ ] `GET /api/v1/users/me`: 创建受保护路由,获取当前登录用户信息。 +- [ ] **2.3. JWT与权限中间件**: + - [ ] 实现 JWT 校验中间件。 + - [ ] 实现一个基础的 RBAC 权限校验中间件。 +- [ ] **2.4. 字典管理 CRUD API**: 实现对数据字典类型和条目的增删改查。 +- [ ] **2.5. 角色管理 CRUD API**: 实现对角色的增删改查及权限分配。 +- [ ] **2.6. 组织架构 CRUD API**: 实现树形组织架构的增删改查。 +- [ ] **2.7. 用户管理 CRUD API**: 实现后台对用户的增删改查和角色分配。 --- ## 阶段三: 笔记与博客系统 + **目标**: 构建平台的内容核心,支持从私有笔记到公开博客的完整流程,并集成搜索和社交功能。 -- [ ] **3.1. 数据库设计**: 设计 `notes`, `posts`, `tags`, `categories`, `comments` 及相关关联表。 -- [ ] **3.2. 笔记核心 API**: - - [ ] 实现笔记的 CRUD (创建/读取/更新/删除)。 - - [ ] `GET /api/v1/notes`: 获取用户的笔记树形结构列表。 - - [ ] 实现笔记的标签管理功能。 -- [ ] **3.3. 博客发布流程 API**: - - [ ] `POST /api/v1/notes/:id/publish`: 从笔记一键发布为博客。 - - [ ] 实现博客的 CRUD,包括分类管理。 -- [ ] **3.4. 公开访问 API**: - - [ ] `GET /api/v1/public/posts`: (公开) 获取博客列表(分页、分类、标签筛选)。 - - [ ] `GET /api/v1/public/posts/:slug`: (公开) 获取单篇博客内容。 -- [ ] **3.5. 互动功能 API**: - - [ ] 实现博客的点赞、收藏功能。 - - [ ] 实现层级评论的发表和查看功能。 -- [ ] **3.6. 搜索引擎集成**: - - [ ] 配置 Elasticsearch 服务。 - - [ ] 编写服务将 `posts` 数据索引到 ES。 - - [ ] `GET /api/v1/public/search`: 实现基于 ES 的博客全文搜索 API。 + +- [ ] **3.1. 数据库设计**: 设计 `notes`, `posts`, `tags`, `categories`, `comments` 及相关关联表。 +- [ ] **3.2. 笔记核心 API**: + - [ ] 实现笔记的 CRUD (创建/读取/更新/删除)。 + - [ ] `GET /api/v1/notes`: 获取用户的笔记树形结构列表。 + - [ ] 实现笔记的标签管理功能。 +- [ ] **3.3. 博客发布流程 API**: + - [ ] `POST /api/v1/notes/:id/publish`: 从笔记一键发布为博客。 + - [ ] 实现博客的 CRUD,包括分类管理。 +- [ ] **3.4. 公开访问 API**: + - [ ] `GET /api/v1/public/posts`: (公开) 获取博客列表(分页、分类、标签筛选)。 + - [ ] `GET /api/v1/public/posts/:slug`: (公开) 获取单篇博客内容。 +- [ ] **3.5. 互动功能 API**: + - [ ] 实现博客的点赞、收藏功能。 + - [ ] 实现层级评论的发表和查看功能。 +- [ ] **3.6. 搜索引擎集成**: + - [ ] 配置 Elasticsearch 服务。 + - [ ] 编写服务将 `posts` 数据索引到 ES。 + - [ ] `GET /api/v1/public/search`: 实现基于 ES 的博客全文搜索 API。 --- ## 阶段四: 个人空间 + **目标**: 开发一个用于记录和分享个人生活点滴的模块。 -- [ ] **4.1. 数据库设计**: 设计 `life_logs` 表,支持文本、图片/视频链接、位置、心情等。 -- [ ] **4.2. 生活日志 CRUD API**: 实现生活日志的增删改查。 -- [ ] **4.3. 时间线 API**: `GET /api/v1/space/timeline` 按时间线获取日志列表。 -- [ ] **4.4. 隐私控制**: 为日志增加隐私设置(公开/私密/好友可见)。 + +- [ ] **4.1. 数据库设计**: 设计 `life_logs` 表,支持文本、图片/视频链接、位置、心情等。 +- [ ] **4.2. 生活日志 CRUD API**: 实现生活日志的增删改查。 +- [ ] **4.3. 时间线 API**: `GET /api/v1/space/timeline` 按时间线获取日志列表。 +- [ ] **4.4. 隐私控制**: 为日志增加隐私设置(公开/私密/好友可见)。 --- ## 阶段五: 阅读系统 + **目标**: 构建个人电子书库,支持在线阅读、批注和智能解读。 -- [ ] **5.1. 数据库设计**: 设计 `ebooks`, `annotations` (批注) 表。 -- [ ] **5.2. 电子书 API**: 实现电子书文件的上传、元数据解析和列表管理。 -- [ ] **5.3. 阅读功能 API**: 实现阅读时添加/查看批注、记录和同步阅读进度的功能。 -- [ ] **5.4. (选做) AI 解读 API**: `POST /api/v1/ebooks/:id/interpret` 对书籍内容进行摘要或问答。 + +- [ ] **5.1. 数据库设计**: 设计 `ebooks`, `annotations` (批注) 表。 +- [ ] **5.2. 电子书 API**: 实现电子书文件的上传、元数据解析和列表管理。 +- [ ] **5.3. 阅读功能 API**: 实现阅读时添加/查看批注、记录和同步阅读进度的功能。 +- [ ] **5.4. (选做) AI 解读 API**: `POST /api/v1/ebooks/:id/interpret` 对书籍内容进行摘要或问答。 --- ## 阶段六: 综合工具集 + **目标**: 集成一系列实用工具,提升平台的附加价值和用户粘性。 -- [ ] **6.1. 密码与书签管理**: - - [ ] 数据库设计 `passwords`, `bookmarks` 表,关键字段加密。 - - [ ] 实现密码管理器的安全 CRUD API。 - - [ ] 实现书签管理器的 CRUD API。 -- [ ] **6.2. 文件管理系统**: - - [ ] 数据库设计 `files` 表,存储文件元数据。 - - [ ] 封装一个支持本地/OSS的对象存储服务。 - - [ ] 实现文件的上传、下载、删除、重命名和目录管理 API。 -- [ ] **6.3. 第三方平台同步**: - - [ ] 设计同步任务管理模块和第三方平台适配器。 - - [ ] 实现 Bilibili 收藏夹同步适配器。 - - [ ] 实现 QQ音乐/网易云音乐 歌单同步适配器。 -- [ ] **6.4. 信息聚合工具**: - - [ ] 实现热点新闻捕获和聚合的后台任务。 - - [ ] `POST /api/v1/tools/web-to-pdf`: 实现网页离线存储为 PDF 的 API。 + +- [ ] **6.1. 密码与书签管理**: + - [ ] 数据库设计 `passwords`, `bookmarks` 表,关键字段加密。 + - [ ] 实现密码管理器的安全 CRUD API。 + - [ ] 实现书签管理器的 CRUD API。 +- [ ] **6.2. 文件管理系统**: + - [ ] 数据库设计 `files` 表,存储文件元数据。 + - [ ] 封装一个支持本地/OSS的对象存储服务。 + - [ ] 实现文件的上传、下载、删除、重命名和目录管理 API。 +- [ ] **6.3. 第三方平台同步**: + - [ ] 设计同步任务管理模块和第三方平台适配器。 + - [ ] 实现 Bilibili 收藏夹同步适配器。 + - [ ] 实现 QQ音乐/网易云音乐 歌单同步适配器。 +- [ ] **6.4. 信息聚合工具**: + - [ ] 实现热点新闻捕获和聚合的后台任务。 + - [ ] `POST /api/v1/tools/web-to-pdf`: 实现网页离线存储为 PDF 的 API。 --- ## 阶段七: 高级功能与生态完善 + **目标**: 引入AI、实时通知等高级功能,并通过跨平台应用完善生态。最后对系统进行加固和优化,确保长期稳定。 -- [ ] **7.1. AI 功能深度集成**: - - [ ] `POST /api/v1/ai/summary`: 对给定文本生成摘要。 - - [ ] `POST /api/v1/ai/polish`: 对给定文本进行润色。 - - [ ] (高级) 探索将笔记向量化,实现基于个人知识库的智能问答。 -- [ ] **7.2. 高级功能与优化**: - - [ ] 使用 WebSocket 实现评论、点赞等事件的实时通知。 - - [ ] 开发基础的网站访问统计和内容分析功能。 - - [ ] 设计并实现基于 OAuth 2.0 的开放 API 体系。 -- [ ] **7.3. 跨平台生态**: - - [ ] (前端) 将 Web 应用打包为 PWA。 - - [ ] (前端) 开发用于快速保存书签和填充密码的浏览器插件。 -- [ ] **7.4. 系统加固与维护**: - - [ ] 引入 CDN 加速静态资源,分析并优化慢查询 SQL。 - - [ ] 定期进行依赖项安全扫描和代码审计。 - - [ ] 完善所有功能的开发者文档和用户手册。 + +- [ ] **7.1. AI 功能深度集成**: + - [ ] `POST /api/v1/ai/summary`: 对给定文本生成摘要。 + - [ ] `POST /api/v1/ai/polish`: 对给定文本进行润色。 + - [ ] (高级) 探索将笔记向量化,实现基于个人知识库的智能问答。 +- [ ] **7.2. 高级功能与优化**: + - [ ] 使用 WebSocket 实现评论、点赞等事件的实时通知。 + - [ ] 开发基础的网站访问统计和内容分析功能。 + - [ ] 设计并实现基于 OAuth 2.0 的开放 API 体系。 +- [ ] **7.3. 跨平台生态**: + - [ ] (前端) 将 Web 应用打包为 PWA。 + - [ ] (前端) 开发用于快速保存书签和填充密码的浏览器插件。 +- [ ] **7.4. 系统加固与维护**: + - [ ] 引入 CDN 加速静态资源,分析并优化慢查询 SQL。 + - [ ] 定期进行依赖项安全扫描和代码审计。 + - [ ] 完善所有功能的开发者文档和用户手册。 ``` 好的,我已经按照你的要求,根据 `main.md` 的功能模块划分,对 `星撰-开发计划.md` 进行了全面重构。 @@ -1508,7 +1638,7 @@ graph TD 这是项目从宏观规划转向微观实现的关键一步。我将为你产出详尽的设计文档。 -```markdown +````markdown # M2 - 基础用户系统 - 详细设计 --- @@ -1516,13 +1646,16 @@ graph TD ## 1. 引言 ### 1.1. 文档目的 + 本文档旨在为“星撰”项目的 **第二阶段:基础用户系统** 提供详尽的技术设计方案。内容涵盖了该模块所需的数据库表结构设计和API接口契约定义,是后续开发、测试和前后端协作的直接依据。 ### 1.2. 模块范围 + 本设计覆盖的功能点包括: -- 用户认证(注册、登录) -- 用户、角色、权限、组织、字典的CRUD管理 -- 基于角色的访问控制(RBAC) + +- 用户认证(注册、登录) +- 用户、角色、权限、组织、字典的CRUD管理 +- 基于角色的访问控制(RBAC) --- @@ -1602,67 +1735,73 @@ erDiagram organizations ||--o{ user_organizations : "has" roles }o--o| roles : "is child of" organizations }o--o| organizations : "is child of" - ``` +```` ### 2.2. 表结构定义 #### `users` - 用户表 -| 字段名 | 类型 | 约束 | 描述 | -|-----------------|---------------|---------------------|----------------------| -| `id` | `BIGINT` | `PK`, `AI` | 用户唯一ID | -| `username` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL`| 用户名,用于登录 | -| `email` | `VARCHAR(255)`| `UNIQUE`, `NOT NULL`| 用户邮箱,用于登录/通知 | -| `password_hash` | `VARCHAR(255)`| `NOT NULL` | 哈希后的用户密码 | -| `status` | `VARCHAR(20)` | `NOT NULL`, `DEFAULT 'inactive'` | 用户状态(inactive, active, disabled) | -| `created_at` | `DATETIME` | `DEFAULT NOW()` | 创建时间 | -| `updated_at` | `DATETIME` | `DEFAULT NOW()` | 最后更新时间 | + +| 字段名 | 类型 | 约束 | 描述 | +| --------------- | -------------- | -------------------------------- | -------------------------------------- | +| `id` | `BIGINT` | `PK`, `AI` | 用户唯一ID | +| `username` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL` | 用户名,用于登录 | +| `email` | `VARCHAR(255)` | `UNIQUE`, `NOT NULL` | 用户邮箱,用于登录/通知 | +| `password_hash` | `VARCHAR(255)` | `NOT NULL` | 哈希后的用户密码 | +| `status` | `VARCHAR(20)` | `NOT NULL`, `DEFAULT 'inactive'` | 用户状态(inactive, active, disabled) | +| `created_at` | `DATETIME` | `DEFAULT NOW()` | 创建时间 | +| `updated_at` | `DATETIME` | `DEFAULT NOW()` | 最后更新时间 | #### `roles` - 角色表 -| 字段名 | 类型 | 约束 | 描述 | -|---------------|---------------|-------------------|--------------------------| -| `id` | `BIGINT` | `PK`, `AI` | 角色唯一ID | -| `name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL`| 角色名称(如:admin, member) | -| `description` | `TEXT` | | 角色描述 | -| `parent_id` | `BIGINT` | `NULLABLE`, `FK` | 父角色ID,用于实现角色继承 | -| `created_at` | `DATETIME` | `DEFAULT NOW()` | 创建时间 | -| `updated_at` | `DATETIME` | `DEFAULT NOW()` | 最后更新时间 | + +| 字段名 | 类型 | 约束 | 描述 | +| ------------- | ------------- | -------------------- | ----------------------------- | +| `id` | `BIGINT` | `PK`, `AI` | 角色唯一ID | +| `name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL` | 角色名称(如:admin, member) | +| `description` | `TEXT` | | 角色描述 | +| `parent_id` | `BIGINT` | `NULLABLE`, `FK` | 父角色ID,用于实现角色继承 | +| `created_at` | `DATETIME` | `DEFAULT NOW()` | 创建时间 | +| `updated_at` | `DATETIME` | `DEFAULT NOW()` | 最后更新时间 | #### `permissions` - 权限表 -| 字段名 | 类型 | 约束 | 描述 | -|---------------|---------------|------------------|------------------------------------| -| `id` | `BIGINT` | `PK`, `AI` | 权限唯一ID | -| `action` | `VARCHAR(50)` | `NOT NULL` | 动作 (如: `create`, `read`, `update`, `delete`) | -| `resource` | `VARCHAR(50)` | `NOT NULL` | 资源 (如: `note`, `user`, `role`) | -| `description` | `TEXT` | | 权限描述 | -| `created_at` | `DATETIME` | `DEFAULT NOW()` | 创建时间 | -| `updated_at` | `DATETIME` | `DEFAULT NOW()` | 最后更新时间 | + +| 字段名 | 类型 | 约束 | 描述 | +| ------------- | ------------- | --------------- | ----------------------------------------------- | +| `id` | `BIGINT` | `PK`, `AI` | 权限唯一ID | +| `action` | `VARCHAR(50)` | `NOT NULL` | 动作 (如: `create`, `read`, `update`, `delete`) | +| `resource` | `VARCHAR(50)` | `NOT NULL` | 资源 (如: `note`, `user`, `role`) | +| `description` | `TEXT` | | 权限描述 | +| `created_at` | `DATETIME` | `DEFAULT NOW()` | 创建时间 | +| `updated_at` | `DATETIME` | `DEFAULT NOW()` | 最后更新时间 | #### `organizations` - 组织架构表 -| 字段名 | 类型 | 约束 | 描述 | -|---------------|----------------|-------------------|------------------------------| -| `id` | `BIGINT` | `PK`, `AI` | 组织唯一ID | -| `name` | `VARCHAR(100)` | `NOT NULL` | 组织/部门名称 | -| `description` | `TEXT` | | 组织描述 | -| `parent_id` | `BIGINT` | `NULLABLE`, `FK` | 父组织ID,用于实现树形结构 | -| `created_at` | `DATETIME` | `DEFAULT NOW()` | 创建时间 | -| `updated_at` | `DATETIME` | `DEFAULT NOW()` | 最后更新时间 | + +| 字段名 | 类型 | 约束 | 描述 | +| ------------- | -------------- | ---------------- | -------------------------- | +| `id` | `BIGINT` | `PK`, `AI` | 组织唯一ID | +| `name` | `VARCHAR(100)` | `NOT NULL` | 组织/部门名称 | +| `description` | `TEXT` | | 组织描述 | +| `parent_id` | `BIGINT` | `NULLABLE`, `FK` | 父组织ID,用于实现树形结构 | +| `created_at` | `DATETIME` | `DEFAULT NOW()` | 创建时间 | +| `updated_at` | `DATETIME` | `DEFAULT NOW()` | 最后更新时间 | #### `dictionaries` - 数据字典表 -| 字段名 | 类型 | 约束 | 描述 | -|---------------|----------------|----------|------------------------------------| -| `id` | `BIGINT` | `PK`, `AI`| 字典条目唯一ID | -| `type` | `VARCHAR(50)` | `NOT NULL`| 字典类型 (如: `user_status`, `gender`) | -| `key` | `VARCHAR(50)` | `NOT NULL`| 键 (如: `active`) | -| `value` | `VARCHAR(255)` | `NOT NULL`| 值 (如: `活跃`) | -| `description` | `TEXT` | | 条目描述 | -| `created_at` | `DATETIME` | `DEFAULT NOW()`| 创建时间 | -| `updated_at` | `DATETIME` | `DEFAULT NOW()`| 最后更新时间 | + +| 字段名 | 类型 | 约束 | 描述 | +| ------------- | -------------- | --------------- | -------------------------------------- | +| `id` | `BIGINT` | `PK`, `AI` | 字典条目唯一ID | +| `type` | `VARCHAR(50)` | `NOT NULL` | 字典类型 (如: `user_status`, `gender`) | +| `key` | `VARCHAR(50)` | `NOT NULL` | 键 (如: `active`) | +| `value` | `VARCHAR(255)` | `NOT NULL` | 值 (如: `活跃`) | +| `description` | `TEXT` | | 条目描述 | +| `created_at` | `DATETIME` | `DEFAULT NOW()` | 创建时间 | +| `updated_at` | `DATETIME` | `DEFAULT NOW()` | 最后更新时间 | #### 关联表 -- **`user_roles`**: `user_id (PK, FK)`, `role_id (PK, FK)` -- **`role_permissions`**: `role_id (PK, FK)`, `permission_id (PK, FK)` -- **`user_organizations`**: `user_id (PK, FK)`, `organization_id (PK, FK)` + +- **`user_roles`**: `user_id (PK, FK)`, `role_id (PK, FK)` +- **`role_permissions`**: `role_id (PK, FK)`, `permission_id (PK, FK)` +- **`user_organizations`**: `user_id (PK, FK)`, `organization_id (PK, FK)` --- @@ -1670,100 +1809,116 @@ erDiagram **Base URL**: `/api/v1` **通用响应结构**: + ```json { - "code": 0, // 0表示成功,其他为错误码 - "message": "success", - "data": {} // 或 [] + "code": 0, // 0表示成功,其他为错误码 + "message": "success", + "data": {} // 或 [] } ``` ### 3.1. Auth - 认证接口 -- **Endpoint**: `/auth` -- **`POST /register`** - - **描述**: 用户注册 - - **请求体**: `application/json` - ```json - { + +- **Endpoint**: `/auth` +- **`POST /register`** + - **描述**: 用户注册 + - **请求体**: `application/json` + + ```json + { "username": "newuser", "email": "newuser@example.com", "password": "strong_password_123" - } - ``` - - **成功响应 (201)**: `data` 包含用户信息。 - - **错误响应 (400, 409)**: 参数错误, 用户名/邮箱已存在。 -- **`POST /login`** - - **描述**: 用户登录 - - **请求体**: `application/json` - ```json - { + } + ``` + + - **成功响应 (201)**: `data` 包含用户信息。 + - **错误响应 (400, 409)**: 参数错误, 用户名/邮箱已存在。 + +- **`POST /login`** + - **描述**: 用户登录 + - **请求体**: `application/json` + + ```json + { "username": "newuser", // or email "password": "strong_password_123" - } - ``` - - **成功响应 (200)**: `data` 包含 `token`。 - - **错误响应 (400, 401)**: 参数错误, 用户名或密码错误。 + } + ``` + + - **成功响应 (200)**: `data` 包含 `token`。 + - **错误响应 (400, 401)**: 参数错误, 用户名或密码错误。 ### 3.2. Users - 用户接口 -- **Endpoint**: `/users` (需要认证) -- **`GET /me`** - - **描述**: 获取当前登录用户的信息。 - - **成功响应 (200)**: `data` 包含当前用户的详细信息。 -- **`PUT /me`** - - **描述**: 更新当前登录用户的信息。 - - **请求体**: `application/json` (只包含可修改字段) - - **成功响应 (200)**: `data` 包含更新后的用户信息。 -- **`GET /`** - - **描述**: 获取用户列表(分页,需要Admin权限)。 - - **查询参数**: `page`, `pageSize`, `keyword` - - **成功响应 (200)**: `data` 包含用户列表和分页信息。 -- **`POST /{userId}/roles`** - - **描述**: 为指定用户分配角色(需要Admin权限)。 - - **请求体**: `application/json` - ```json - { + +- **Endpoint**: `/users` (需要认证) +- **`GET /me`** + - **描述**: 获取当前登录用户的信息。 + - **成功响应 (200)**: `data` 包含当前用户的详细信息。 +- **`PUT /me`** + - **描述**: 更新当前登录用户的信息。 + - **请求体**: `application/json` (只包含可修改字段) + - **成功响应 (200)**: `data` 包含更新后的用户信息。 +- **`GET /`** + - **描述**: 获取用户列表(分页,需要Admin权限)。 + - **查询参数**: `page`, `pageSize`, `keyword` + - **成功响应 (200)**: `data` 包含用户列表和分页信息。 +- **`POST /{userId}/roles`** + - **描述**: 为指定用户分配角色(需要Admin权限)。 + - **请求体**: `application/json` + + ```json + { "roleIds": [1, 2, 3] - } - ``` - - **成功响应 (204)**: No Content. + } + ``` + + - **成功响应 (204)**: No Content. ### 3.3. Roles - 角色接口 -- **Endpoint**: `/roles` (需要认证和相应权限) -- **`POST /`** - - **描述**: 创建新角色。 - - **请求体**: `application/json` - - **成功响应 (201)**: `data` 包含新创建的角色信息。 -- **`GET /`** - - **描述**: 获取所有角色列表(树形结构)。 - - **成功响应 (200)**: `data` 包含角色的树形数组。 -- **`PUT /{roleId}`** - - **描述**: 更新角色信息。 - - **成功响应 (200)**: `data` 包含更新后的角色信息。 -- **`DELETE /{roleId}`** - - **描述**: 删除角色。 - - **成功响应 (204)**: No Content. -- **`POST /{roleId}/permissions`** - - **描述**: 为角色分配权限。 - - **请求体**: `application/json` - ```json - { + +- **Endpoint**: `/roles` (需要认证和相应权限) +- **`POST /`** + - **描述**: 创建新角色。 + - **请求体**: `application/json` + - **成功响应 (201)**: `data` 包含新创建的角色信息。 +- **`GET /`** + - **描述**: 获取所有角色列表(树形结构)。 + - **成功响应 (200)**: `data` 包含角色的树形数组。 +- **`PUT /{roleId}`** + - **描述**: 更新角色信息。 + - **成功响应 (200)**: `data` 包含更新后的角色信息。 +- **`DELETE /{roleId}`** + - **描述**: 删除角色。 + - **成功响应 (204)**: No Content. +- **`POST /{roleId}/permissions`** + - **描述**: 为角色分配权限。 + - **请求体**: `application/json` + + ```json + { "permissionIds": [101, 102] - } - ``` - - **成功响应 (204)**: No Content. + } + ``` + + - **成功响应 (204)**: No Content. ### 3.4. Organizations - 组织接口 -- **Endpoint**: `/organizations` (需要认证和相应权限) -- *接口设计与 Roles 类似,提供树形结构的增删改查。* + +- **Endpoint**: `/organizations` (需要认证和相应权限) +- _接口设计与 Roles 类似,提供树形结构的增删改查。_ ### 3.5. Dictionaries - 字典接口 -- **Endpoint**: `/dictionaries` (需要认证和相应权限) -- **`GET /`** - - **描述**: 获取字典条目列表。 - - **查询参数**: `type` (用于筛选特定类型的字典)。 - - **成功响应 (200)**: `data` 包含字典条目数组。 -- *其他增删改查接口与 Roles 类似。* -``` + +- **Endpoint**: `/dictionaries` (需要认证和相应权限) +- **`GET /`** + - **描述**: 获取字典条目列表。 + - **查询参数**: `type` (用于筛选特定类型的字典)。 + - **成功响应 (200)**: `data` 包含字典条目数组。 +- _其他增删改查接口与 Roles 类似。_ + +```` 我已经为你创建了 `prd/M2-基础用户系统-详细设计.md` 文件。 @@ -1905,39 +2060,45 @@ erDiagram role_permissions { bigint role_id PK,FK; bigint permission_id PK,FK } user_organizations { bigint user_id PK,FK; bigint organization_id PK,FK } user_tags { bigint user_id PK,FK; bigint tag_id PK,FK } -``` +```` ### 2.2. 表结构定义 #### `roles` / `organizations` + - **`parent_id`**: `BIGINT NULL`, `FOREIGN KEY (parent_id) REFERENCES roles(id) ON DELETE SET NULL`。此字段用于实现树形层级结构。值为`NULL`表示为顶级角色/组织。 #### `dictionary_types` - 字典类型表 -| 字段名 | 类型 | 约束 | 描述 | -|---------------|---------------|---------------------|----------------------| -| `id` | `BIGINT` | `PK`, `AI` | 类型唯一ID | -| `type_name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL`| 字典类型名(如: `user_status`) | -| `description` | `TEXT` | | 类型描述 | + +| 字段名 | 类型 | 约束 | 描述 | +| ------------- | ------------- | -------------------- | ------------------------------- | +| `id` | `BIGINT` | `PK`, `AI` | 类型唯一ID | +| `type_name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL` | 字典类型名(如: `user_status`) | +| `description` | `TEXT` | | 类型描述 | #### `dictionary_items` - 字典条目表 -| 字段名 | 类型 | 约束 | 描述 | -|---------------|----------------|----------------------|----------------------| -| `id` | `BIGINT` | `PK`, `AI` | 条目唯一ID | -| `type_id` | `BIGINT` | `NOT NULL`, `FK` | 关联到`dictionary_types.id` | -| `key` | `VARCHAR(50)` | `NOT NULL` | 键 (程序中使用) | -| `value` | `VARCHAR(255)` | `NOT NULL` | 值 (UI中显示) | -| `sort_order` | `INT` | `DEFAULT 0` | 排序字段 | -*复合唯一键: `(type_id, key)`* + +| 字段名 | 类型 | 约束 | 描述 | +| ------------ | -------------- | ---------------- | --------------------------- | +| `id` | `BIGINT` | `PK`, `AI` | 条目唯一ID | +| `type_id` | `BIGINT` | `NOT NULL`, `FK` | 关联到`dictionary_types.id` | +| `key` | `VARCHAR(50)` | `NOT NULL` | 键 (程序中使用) | +| `value` | `VARCHAR(255)` | `NOT NULL` | 值 (UI中显示) | +| `sort_order` | `INT` | `DEFAULT 0` | 排序字段 | + +_复合唯一键: `(type_id, key)`_ #### `tags` - 标签表 -| 字段名 | 类型 | 约束 | 描述 | -|--------|---------------|---------------------|------------| -| `id` | `BIGINT` | `PK`, `AI` | 标签唯一ID | -| `name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL`| 标签名 | + +| 字段名 | 类型 | 约束 | 描述 | +| ------ | ------------- | -------------------- | ---------- | +| `id` | `BIGINT` | `PK`, `AI` | 标签唯一ID | +| `name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL` | 标签名 | #### `user_tags` - 用户标签关联表 -| 字段名 | 类型 | 约束 | 描述 | -|-----------|----------|----------|------------| + +| 字段名 | 类型 | 约束 | 描述 | +| --------- | -------- | ---------- | ---------- | | `user_id` | `BIGINT` | `PK`, `FK` | 关联用户ID | | `tag_id` | `BIGINT` | `PK`, `FK` | 关联标签ID | @@ -1946,10 +2107,10 @@ erDiagram ## 3. Root 用户与系统初始化 - **`root` 用户**: - - `root` 是一个特殊的超级管理员账户,拥有系统中所有权限。 - - 此账户 **不可通过 API 创建或删除**。它应在系统首次部署时,通过数据库 **Seeding (种子填充) 脚本** 创建。 - - `root` 用户的角色是固定的(如 `super_admin`),此角色同样通过 Seeding 创建,并关联所有已定义的权限。 - - 任何权限校验逻辑都必须对 `root` 用户或 `super_admin` 角色开绿灯。任何API都不能降低 `root` 的权限或删除该用户。 + - `root` 是一个特殊的超级管理员账户,拥有系统中所有权限。 + - 此账户 **不可通过 API 创建或删除**。它应在系统首次部署时,通过数据库 **Seeding (种子填充) 脚本** 创建。 + - `root` 用户的角色是固定的(如 `super_admin`),此角色同样通过 Seeding 创建,并关联所有已定义的权限。 + - 任何权限校验逻辑都必须对 `root` 用户或 `super_admin` 角色开绿灯。任何API都不能降低 `root` 的权限或删除该用户。 --- @@ -1957,99 +2118,118 @@ erDiagram **Base URL**: `/api/v1` **通用错误响应结构**: + ```json { - "code": 40001, // 详细业务错误码 - "message": "Validation failed: username must be at least 3 characters.", - "data": null + "code": 40001, // 详细业务错误码 + "message": "Validation failed: username must be at least 3 characters.", + "data": null } ``` ### 3.1. Auth - 认证接口 (`/auth`) #### `POST /register` - 用户注册 + - **权限**: Public - **请求体 (`application/json`)**: - | 名称 | 类型 | 必填 | 校验规则 | 描述 | + | 名称 | 类型 | 必填 | 校验规则 | 描述 | |------------|--------|------|----------------------------------------------------|----------------| - | `username` | string | 是 | min:3, max:50, 字母/数字/下划线组合 | 用户名 | - | `email` | string | 是 | valid email format | 邮箱地址 | - | `password` | string | 是 | min:8, max:100, 必须包含大小写字母和数字 | 密码 | + | `username` | string | 是 | min:3, max:50, 字母/数字/下划线组合 | 用户名 | + | `email` | string | 是 | valid email format | 邮箱地址 | + | `password` | string | 是 | min:8, max:100, 必须包含大小写字母和数字 | 密码 | - **成功响应 (201 Created)**: - ```json - { "code": 0, "message": "User registered successfully.", "data": { "id": 1, "username": "newuser", "email": "..." } } - ``` + ```json + { + "code": 0, + "message": "User registered successfully.", + "data": { "id": 1, "username": "newuser", "email": "..." } + } + ``` - **异常响应**: - | HTTP 码 | 业务码 | 原因 | + | HTTP 码 | 业务码 | 原因 | |---------|----------|----------------------------| - | 400 | 40001 | 请求参数不符合校验规则 | - | 409 | 40901 | 用户名或邮箱已被占用 | + | 400 | 40001 | 请求参数不符合校验规则 | + | 409 | 40901 | 用户名或邮箱已被占用 | #### `POST /login` - 用户登录 + - **权限**: Public - **请求体 (`application/json`)**: - | 名称 | 类型 | 必填 | 校验规则 | 描述 | + | 名称 | 类型 | 必填 | 校验规则 | 描述 | |------------|--------|------|------------------|------------------| - | `username` | string | 是 | - | 用户名或邮箱 | - | `password` | string | 是 | - | 密码 | + | `username` | string | 是 | - | 用户名或邮箱 | + | `password` | string | 是 | - | 密码 | - **成功响应 (200 OK)**: - ```json - { "code": 0, "message": "Login successful.", "data": { "token": "ey..." } } - ``` + ```json + { "code": 0, "message": "Login successful.", "data": { "token": "ey..." } } + ``` - **异常响应**: - | HTTP 码 | 业务码 | 原因 | + | HTTP 码 | 业务码 | 原因 | |---------|----------|--------------------------| - | 401 | 40101 | 用户名或密码错误 | - | 401 | 40102 | 账户被禁用或未激活 | + | 401 | 40101 | 用户名或密码错误 | + | 401 | 40102 | 账户被禁用或未激活 | --- + ### 3.2. Users - 用户接口 (`/users`) -*除特殊说明外,均需认证* + +_除特殊说明外,均需认证_ #### `GET /me` - 获取当前用户信息 + - **权限**: Authenticated - **成功响应 (200 OK)**: `data` 包含当前用户详细信息、关联的角色和权限列表。 #### `GET /` - 获取用户列表 + - **权限**: `read:users` - **查询参数**: - | 名称 | 类型 | 必填 | 描述 | + | 名称 | 类型 | 必填 | 描述 | |-----------|--------|------|------------------------------------| - | `page` | number | 否 | 页码, default 1 | - | `pageSize`| number | 否 | 每页数量, default 10 | - | `keyword` | string | 否 | 按用户名或邮箱模糊搜索 | + | `page` | number | 否 | 页码, default 1 | + | `pageSize`| number | 否 | 每页数量, default 10 | + | `keyword` | string | 否 | 按用户名或邮箱模糊搜索 | - **成功响应 (200 OK)**: `data` 包含 `items` (用户列表) 和 `pagination` (分页信息)。 #### `PUT /{userId}/status` - 更新用户状态 + - **权限**: `update:user_status` - **请求体**: `{ "status": "active" }` (status 必须是字典中 `user_status` 类型的值) - **成功响应 (200 OK)**: 返回更新后的用户信息。 - **异常响应**: - | HTTP 码 | 业务码 | 原因 | + | HTTP 码 | 业务码 | 原因 | |---------|----------|--------------------------| - | 403 | 40301 | 试图修改 `root` 用户状态 | - | 404 | 40401 | 指定用户不存在 | + | 403 | 40301 | 试图修改 `root` 用户状态 | + | 404 | 40401 | 指定用户不存在 | #### `POST /{userId}/tags` - 为用户打标签 + - **权限**: `update:user_tags` - **请求体**: `{ "tagIds": [1, 2, 3] }` - **成功响应 (204 No Content)**. --- + ### 3.3. Roles - 角色接口 (`/roles`) -*所有接口均需 `manage:roles` 权限* + +_所有接口均需 `manage:roles` 权限_ #### `GET /` - 获取角色列表 + - **描述**: 以树形结构返回所有角色。 - **成功响应 (200 OK)**: `data` 是一个包含 `children` 属性的树形数组。 #### `POST /` - 创建新角色 + - **请求体**: `{ "name": "editor", "description": "...", "parentId": 1, "permissionIds": [101, 102] }` - **成功响应 (201 Created)**: `data` 包含新创建的角色信息。 - **异常响应 (409 Conflict)**: 角色名已存在。 --- -*其他模块(Organizations, Dictionaries, Tags)的API将遵循类似的详细设计模式:提供完整的增删改查接口,明确定义权限、请求体、校验规则和所有可能的成功/异常响应。* + +_其他模块(Organizations, Dictionaries, Tags)的API将遵循类似的详细设计模式:提供完整的增删改查接口,明确定义权限、请求体、校验规则和所有可能的成功/异常响应。_ + ``` 我已经根据你的反馈,对 `prd/M2-基础用户系统-详细设计.md` 进行了全面的、深度的重写。 @@ -2076,3 +2256,4 @@ erDiagram 我相信这个V2版本的设计文档已经达到了你的要求。接下来,你可以放心地基于这份文档开始第二阶段的编码工作了。 +``` diff --git a/aiChat/005-cursor_.md b/aiChat/005-cursor_.md index 27369be..ff2b47c 100644 --- a/aiChat/005-cursor_.md +++ b/aiChat/005-cursor_.md @@ -1,4 +1,5 @@ # 开始集成邮件发送服务 + _Exported on 2025/7/4 at GMT+8 18:43:55 from Cursor (1.2.1)_ --- @@ -24,6 +25,7 @@ _Exported on 2025/7/4 at GMT+8 18:43:55 from Cursor (1.2.1)_ 现在我需要安装邮件发送服务所需的依赖包: ✅ 第一个子任务完成!已成功安装邮件发送服务所需的依赖包: + - nodemailer@7.0.4 (邮件发送核心库) - @types/nodemailer@6.4.17 (TypeScript类型定义) @@ -187,16 +189,18 @@ export const ENV = process.env.NODE_ENV || process.env.BUN_ENV || 'development'; ✅ 第二个子任务完成!已成功创建邮件配置文件: **创建的文件和功能:** + - `src/config/email.config.ts` - 包含完整的邮件服务配置 - - SMTP服务器配置(支持QQ邮箱等) - - 邮件基础配置(发件人、字符编码等) - - 邮件模板配置(激活、密码重置、欢迎、通知等) - - 发送选项配置(重试、队列、频率限制等) - - 配置验证和获取函数 + - SMTP服务器配置(支持QQ邮箱等) + - 邮件基础配置(发件人、字符编码等) + - 邮件模板配置(激活、密码重置、欢迎、通知等) + - 发送选项配置(重试、队列、频率限制等) + - 配置验证和获取函数 - 更新了 `src/config/index.ts` 以导出邮件配置 **支持的环境变量:** + - `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE` - SMTP服务器设置 - `SMTP_USER`, `SMTP_PASS` - 邮箱认证信息 - `EMAIL_FROM`, `EMAIL_REPLY_TO` - 发件人设置 @@ -479,16 +483,18 @@ export interface EmailHealthCheckResult { ✅ 第三个子任务完成!已成功创建邮件服务类型定义文件: **创建的类型定义:** + - `src/type/email.type.ts` - 包含完整的邮件服务类型定义 - - **基础类型**:EmailTransporter、EmailPriority、EmailTemplateType - - **邮件发送类型**:EmailSendOptions、EmailAttachment、EmailSendResult - - **模板系统类型**:EmailTemplateParams、EmailTemplateSendOptions - - **服务管理类型**:EmailServiceConfig、EmailServiceStatus - - **队列系统类型**:EmailQueueTask(支持异步邮件发送) - - **健康检查类型**:EmailHealthCheckResult - - **Elysia集成类型**:EmailContext(扩展Elysia上下文) + - **基础类型**:EmailTransporter、EmailPriority、EmailTemplateType + - **邮件发送类型**:EmailSendOptions、EmailAttachment、EmailSendResult + - **模板系统类型**:EmailTemplateParams、EmailTemplateSendOptions + - **服务管理类型**:EmailServiceConfig、EmailServiceStatus + - **队列系统类型**:EmailQueueTask(支持异步邮件发送) + - **健康检查类型**:EmailHealthCheckResult + - **Elysia集成类型**:EmailContext(扩展Elysia上下文) **类型特点:** + - 完整的类型安全支持 - 支持多种邮件发送模式(直接发送、模板发送、队列发送) - 支持附件、抄送、密送等完整邮件功能 @@ -621,7 +627,7 @@ export class EmailService { private updateStatus( status: EmailServiceStatus['status'], transporterStatus: EmailServiceStatus['transporterStatus'], - error?: string + error?: string, ): void { this._status = { status, @@ -767,7 +773,7 @@ export class EmailService { } // 等待重试延迟 - await new Promise(resolve => setTimeout(resolve, emailOptions.retryDelay)); + await new Promise((resolve) => setTimeout(resolve, emailOptions.retryDelay)); } } @@ -830,7 +836,7 @@ export class EmailService { */ private renderTemplate( templateType: EmailTemplateType, - params: EmailTemplateParams + params: EmailTemplateParams, ): { subject: string; html: string; text: string } { const template = emailTemplates[templateType]; const defaultParams = { @@ -1263,54 +1269,56 @@ export type EmailTemplateType = 'activation' | 'passwordReset' | 'welcome' | 'no ``` ```typescript - switch (templateType) { - case 'activation': - subject = template.subject; - html = this.getActivationTemplate(defaultParams); - text = `您好 ${defaultParams.nickname || defaultParams.username},\n\n请点击以下链接激活您的账户:\n${defaultParams.activationUrl}\n\n或使用激活码:${defaultParams.activationCode}\n\n链接将在 ${defaultParams.expireTime} 后过期。\n\n如果您没有注册账户,请忽略此邮件。\n\n${defaultParams.systemName}`; - break; +switch (templateType) { + case 'activation': + subject = template.subject; + html = this.getActivationTemplate(defaultParams); + text = `您好 ${defaultParams.nickname || defaultParams.username},\n\n请点击以下链接激活您的账户:\n${defaultParams.activationUrl}\n\n或使用激活码:${defaultParams.activationCode}\n\n链接将在 ${defaultParams.expireTime} 后过期。\n\n如果您没有注册账户,请忽略此邮件。\n\n${defaultParams.systemName}`; + break; - case 'passwordReset': - subject = emailTemplates.passwordReset.subject; - html = this.getPasswordResetTemplate(defaultParams); - text = `您好 ${defaultParams.nickname || defaultParams.username},\n\n我们收到了重置您账户密码的请求。请点击以下链接重置密码:\n${defaultParams.resetUrl}\n\n或使用重置码:${defaultParams.resetCode}\n\n链接将在 ${defaultParams.expireTime} 后过期。\n\n如果您没有请求重置密码,请忽略此邮件。\n\n${defaultParams.systemName}`; - break; + case 'passwordReset': + subject = emailTemplates.passwordReset.subject; + html = this.getPasswordResetTemplate(defaultParams); + text = `您好 ${defaultParams.nickname || defaultParams.username},\n\n我们收到了重置您账户密码的请求。请点击以下链接重置密码:\n${defaultParams.resetUrl}\n\n或使用重置码:${defaultParams.resetCode}\n\n链接将在 ${defaultParams.expireTime} 后过期。\n\n如果您没有请求重置密码,请忽略此邮件。\n\n${defaultParams.systemName}`; + break; - case 'welcome': - subject = template.subject; - html = this.getWelcomeTemplate(defaultParams); - text = `欢迎 ${defaultParams.nickname || defaultParams.username}!\n\n感谢您注册 ${defaultParams.systemName}。您的账户已成功激活。\n\n如果您有任何问题,请联系我们:${defaultParams.supportEmail}\n\n${defaultParams.systemName}`; - break; + case 'welcome': + subject = template.subject; + html = this.getWelcomeTemplate(defaultParams); + text = `欢迎 ${defaultParams.nickname || defaultParams.username}!\n\n感谢您注册 ${defaultParams.systemName}。您的账户已成功激活。\n\n如果您有任何问题,请联系我们:${defaultParams.supportEmail}\n\n${defaultParams.systemName}`; + break; - case 'passwordChanged': - subject = emailTemplates.passwordChanged.subject; - html = this.getPasswordChangedTemplate(defaultParams); - text = `您好 ${defaultParams.nickname || defaultParams.username},\n\n您的账户密码已成功修改。如果这不是您本人的操作,请立即联系我们。\n\n联系邮箱:${defaultParams.supportEmail}\n\n${defaultParams.systemName}`; - break; + case 'passwordChanged': + subject = emailTemplates.passwordChanged.subject; + html = this.getPasswordChangedTemplate(defaultParams); + text = `您好 ${defaultParams.nickname || defaultParams.username},\n\n您的账户密码已成功修改。如果这不是您本人的操作,请立即联系我们。\n\n联系邮箱:${defaultParams.supportEmail}\n\n${defaultParams.systemName}`; + break; - case 'notification': - subject = template.subject; - html = this.getNotificationTemplate(defaultParams); - text = (defaultParams as any).message || '您有新的系统通知'; - break; + case 'notification': + subject = template.subject; + html = this.getNotificationTemplate(defaultParams); + text = (defaultParams as any).message || '您有新的系统通知'; + break; - default: - throw new Error(`不支持的邮件模板类型: ${templateType}`); - } + default: + throw new Error(`不支持的邮件模板类型: ${templateType}`); +} ``` ✅ 第四个子任务完成!已成功实现邮件发送服务类: **创建的功能:** + - `src/plugins/email/email.service.ts` - 完整的邮件发送服务类 - - **核心功能**:单例模式管理、配置验证、连接池支持 - - **邮件发送**:支持重试机制、错误处理、详细日志记录 - - **模板系统**:5种内置模板(激活、密码重置、欢迎、密码修改通知、系统通知) - - **HTML模板**:响应式设计、美观的邮件样式 - - **健康检查**:SMTP连接验证、服务状态监控 - - **便捷方法**:initializeEmailService、sendEmail、sendTemplateEmail等 + - **核心功能**:单例模式管理、配置验证、连接池支持 + - **邮件发送**:支持重试机制、错误处理、详细日志记录 + - **模板系统**:5种内置模板(激活、密码重置、欢迎、密码修改通知、系统通知) + - **HTML模板**:响应式设计、美观的邮件样式 + - **健康检查**:SMTP连接验证、服务状态监控 + - **便捷方法**:initializeEmailService、sendEmail、sendTemplateEmail等 **支持的邮件类型:** + - 直接发送邮件(自定义内容) - 模板邮件发送(预定义模板) - 支持抄送、密送、附件 @@ -1318,6 +1326,7 @@ export type EmailTemplateType = 'activation' | 'passwordReset' | 'welcome' | 'no - 完整的错误处理和重试机制 **修复的问题:** + - 修正了nodemailer API调用 - 统一了模板类型命名(驼峰命名) - 处理了类型安全问题 @@ -1471,11 +1480,7 @@ export const emailPlugin = new Elysia({ name: 'email' }) /** * 发送欢迎邮件 */ - async sendWelcomeEmail(options: { - to: string; - username: string; - nickname?: string; - }): Promise { + async sendWelcomeEmail(options: { to: string; username: string; nickname?: string }): Promise { return await emailService.sendTemplateEmail({ to: options.to, template: 'welcome', @@ -1563,11 +1568,7 @@ export type EmailServiceDecorator = { resetCode: string; expireTime: string; }): Promise; - sendWelcomeEmail(options: { - to: string; - username: string; - nickname?: string; - }): Promise; + sendWelcomeEmail(options: { to: string; username: string; nickname?: string }): Promise; sendPasswordChangedEmail(options: { to: string; username: string; @@ -1618,11 +1619,7 @@ export type EmailServiceDecorator = { resetCode: string; expireTime: string; }): Promise; - sendWelcomeEmail(options: { - to: string; - username: string; - nickname?: string; - }): Promise; + sendWelcomeEmail(options: { to: string; username: string; nickname?: string }): Promise; sendPasswordChangedEmail(options: { to: string; username: string; @@ -1639,7 +1636,7 @@ export type EmailServiceDecorator = { }; ``` -```markdown +````markdown # 邮件服务插件 邮件服务插件为 Elysia 应用提供完整的邮件发送功能,包括模板邮件、重试机制、健康检查等。 @@ -1676,6 +1673,7 @@ EMAIL_REPLY_TO=your-email@qq.com EMAIL_RETRY_ATTEMPTS=3 EMAIL_RETRY_DELAY=5000 ``` +```` ### 2. 插件注册 @@ -1685,9 +1683,8 @@ EMAIL_RETRY_DELAY=5000 import { Elysia } from 'elysia'; import { emailPlugin } from '@/plugins/email/email.plugins'; -const app = new Elysia() - .use(emailPlugin) - // ... 其他插件 +const app = new Elysia().use(emailPlugin); +// ... 其他插件 ``` ## 使用方法 @@ -1798,6 +1795,7 @@ app.get('/email/health', async ({ emailService }) => { 发送自定义邮件。 **参数:** + - `options.to`: 收件人邮箱(字符串或数组) - `options.subject`: 邮件主题 - `options.html`: HTML 内容 @@ -1812,6 +1810,7 @@ app.get('/email/health', async ({ emailService }) => { 发送模板邮件。 **参数:** + - `options.to`: 收件人邮箱 - `options.template`: 模板类型 - `options.params`: 模板参数 @@ -1857,7 +1856,8 @@ app.get('/email/health', async ({ emailService }) => { 3. **发送限制**: 注意邮件提供商的发送频率限制 4. **模板自定义**: 可以通过修改服务类来自定义邮件模板 5. **安全性**: 避免在邮件内容中包含敏感信息 -``` + +```` ```typescript /** @@ -1891,26 +1891,28 @@ export const plugins = (app: Elysia) => .use(emailPlugin) // API 文档插件 .use(swaggerPlugin); -``` +```` ✅ 第五个子任务完成!已成功创建邮件服务Elysia插件: **创建的文件和功能:** + - `src/plugins/email/email.plugins.ts` - 邮件服务Elysia插件 - - **启动初始化**:验证配置、初始化服务、错误处理 - - **上下文装饰**:将邮件功能注入到Elysia上下文中 - - **便捷方法**:提供5种常用邮件发送方法 - - **状态管理**:健康检查、状态查询、初始化检查 - - **优雅关闭**:应用停止时正确关闭邮件服务 + - **启动初始化**:验证配置、初始化服务、错误处理 + - **上下文装饰**:将邮件功能注入到Elysia上下文中 + - **便捷方法**:提供5种常用邮件发送方法 + - **状态管理**:健康检查、状态查询、初始化检查 + - **优雅关闭**:应用停止时正确关闭邮件服务 - `src/plugins/email/README.md` - 详细的使用文档 - - 完整的API参考文档 - - 使用示例和最佳实践 - - 配置说明和注意事项 + - 完整的API参考文档 + - 使用示例和最佳实践 + - 配置说明和注意事项 - **插件集成**:更新了 `src/plugins/index.ts` 以注册邮件插件 **插件特性:** + - 🔧 **自动初始化**:应用启动时自动初始化邮件服务 - 🛡️ **容错处理**:邮件服务故障不影响应用启动 - 📝 **类型安全**:完整的TypeScript类型支持 @@ -1981,20 +1983,20 @@ y import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { setTimeout } from 'node:timers'; -import { - emailService, +import { + emailService, initializeEmailService, sendEmail, sendTemplateEmail, getEmailServiceStatus, checkEmailServiceHealth, - closeEmailService + closeEmailService, } from '@/plugins/email/email.service'; import { validateEmailConfig } from '@/config/email.config'; import type { EmailSendOptions, EmailTemplateSendOptions } from '@/type/email.type'; // 简单的延时函数 -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // 测试用的邮箱地址(请根据实际情况修改) const TEST_EMAIL = 'test@example.com'; @@ -2007,7 +2009,7 @@ describe('邮件服务测试', () => { beforeAll(async () => { // 检查邮件配置是否有效 isEmailConfigValid = validateEmailConfig(); - + if (isEmailConfigValid) { try { // 初始化邮件服务 @@ -2053,7 +2055,7 @@ describe('邮件服务测试', () => { describe('邮件服务健康检查', () => { it('应该执行健康检查并返回结果', async () => { const healthResult = await checkEmailServiceHealth(); - + expect(healthResult).toHaveProperty('status'); expect(healthResult).toHaveProperty('responseTime'); expect(healthResult).toHaveProperty('serviceStatus'); @@ -2067,41 +2069,49 @@ describe('邮件服务测试', () => { // 只在邮件配置有效时运行实际发送测试 const runSendTest = isEmailConfigValid ? it : it.skip; - runSendTest('应该能够发送简单文本邮件', async () => { - const emailOptions: EmailSendOptions = { - to: TEST_EMAIL, - subject: '测试邮件 - 简单文本', - text: '这是一封测试邮件的纯文本内容。', - html: '

这是一封测试邮件的HTML内容

', - }; + runSendTest( + '应该能够发送简单文本邮件', + async () => { + const emailOptions: EmailSendOptions = { + to: TEST_EMAIL, + subject: '测试邮件 - 简单文本', + text: '这是一封测试邮件的纯文本内容。', + html: '

这是一封测试邮件的HTML内容

', + }; - const result = await sendEmail(emailOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - expect(result.sentAt).toBeInstanceOf(Date); - - if (result.success) { - expect(result).toHaveProperty('messageId'); - expect(result.accepted).toContain(TEST_EMAIL); - console.log('邮件发送成功:', result.messageId); - } else { - console.warn('邮件发送失败:', result.error); - } - }, 10000); // 设置较长的超时时间 + const result = await sendEmail(emailOptions); - runSendTest('应该能够发送带抄送的邮件', async () => { - const emailOptions: EmailSendOptions = { - to: TEST_EMAIL, - cc: TEST_EMAIL, // 测试环境下抄送给同一个邮箱 - subject: '测试邮件 - 带抄送', - html: '

测试抄送功能

这封邮件测试抄送功能。

', - }; + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + expect(result.sentAt).toBeInstanceOf(Date); - const result = await sendEmail(emailOptions); - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - }, 10000); + if (result.success) { + expect(result).toHaveProperty('messageId'); + expect(result.accepted).toContain(TEST_EMAIL); + console.log('邮件发送成功:', result.messageId); + } else { + console.warn('邮件发送失败:', result.error); + } + }, + 10000, + ); // 设置较长的超时时间 + + runSendTest( + '应该能够发送带抄送的邮件', + async () => { + const emailOptions: EmailSendOptions = { + to: TEST_EMAIL, + cc: TEST_EMAIL, // 测试环境下抄送给同一个邮箱 + subject: '测试邮件 - 带抄送', + html: '

测试抄送功能

这封邮件测试抄送功能。

', + }; + + const result = await sendEmail(emailOptions); + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + }, + 10000, + ); it('应该处理邮件发送错误', async () => { const invalidEmailOptions: EmailSendOptions = { @@ -2111,11 +2121,11 @@ describe('邮件服务测试', () => { }; const result = await sendEmail(invalidEmailOptions); - + // 无论SMTP是否配置,无效邮箱都应该返回失败 expect(result).toHaveProperty('success'); expect(result).toHaveProperty('sentAt'); - + if (!result.success) { expect(result).toHaveProperty('error'); expect(result.rejected).toContain('invalid-email'); @@ -2126,118 +2136,138 @@ describe('邮件服务测试', () => { describe('模板邮件功能', () => { const runTemplateTest = isEmailConfigValid ? it : it.skip; - runTemplateTest('应该能够发送用户激活邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'activation', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - email: TEST_EMAIL, - activationUrl: 'https://example.com/activate?token=test123', - activationCode: '123456', - expireTime: '24小时', - }, - }; + runTemplateTest( + '应该能够发送用户激活邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'activation', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + email: TEST_EMAIL, + activationUrl: 'https://example.com/activate?token=test123', + activationCode: '123456', + expireTime: '24小时', + }, + }; - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - expect(result).toHaveProperty('messageId'); - console.log('激活邮件发送成功:', result.messageId); - } - }, 10000); + const result = await sendTemplateEmail(templateOptions); - runTemplateTest('应该能够发送密码重置邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'passwordReset', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - email: TEST_EMAIL, - resetUrl: 'https://example.com/reset?token=test123', - resetCode: '654321', - expireTime: '30分钟', - }, - }; + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - console.log('密码重置邮件发送成功:', result.messageId); - } - }, 10000); + if (result.success) { + expect(result).toHaveProperty('messageId'); + console.log('激活邮件发送成功:', result.messageId); + } + }, + 10000, + ); - runTemplateTest('应该能够发送欢迎邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'welcome', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - email: TEST_EMAIL, - }, - }; + runTemplateTest( + '应该能够发送密码重置邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'passwordReset', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + email: TEST_EMAIL, + resetUrl: 'https://example.com/reset?token=test123', + resetCode: '654321', + expireTime: '30分钟', + }, + }; - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - console.log('欢迎邮件发送成功:', result.messageId); - } - }, 10000); + const result = await sendTemplateEmail(templateOptions); - runTemplateTest('应该能够发送密码修改通知邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'passwordChanged', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - email: TEST_EMAIL, - }, - }; + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - console.log('密码修改通知邮件发送成功:', result.messageId); - } - }, 10000); + if (result.success) { + console.log('密码重置邮件发送成功:', result.messageId); + } + }, + 10000, + ); - runTemplateTest('应该能够发送系统通知邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'notification', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - message: '这是一条重要的系统通知,请及时查看您的账户状态。', - }, - subject: '重要系统通知', - }; + runTemplateTest( + '应该能够发送欢迎邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'welcome', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + email: TEST_EMAIL, + }, + }; - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - console.log('系统通知邮件发送成功:', result.messageId); - } - }, 10000); + const result = await sendTemplateEmail(templateOptions); + + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + + if (result.success) { + console.log('欢迎邮件发送成功:', result.messageId); + } + }, + 10000, + ); + + runTemplateTest( + '应该能够发送密码修改通知邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'passwordChanged', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + email: TEST_EMAIL, + }, + }; + + const result = await sendTemplateEmail(templateOptions); + + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + + if (result.success) { + console.log('密码修改通知邮件发送成功:', result.messageId); + } + }, + 10000, + ); + + runTemplateTest( + '应该能够发送系统通知邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'notification', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + message: '这是一条重要的系统通知,请及时查看您的账户状态。', + }, + subject: '重要系统通知', + }; + + const result = await sendTemplateEmail(templateOptions); + + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + + if (result.success) { + console.log('系统通知邮件发送成功:', result.messageId); + } + }, + 10000, + ); it('应该处理无效模板类型', async () => { const invalidTemplateOptions = { @@ -2249,7 +2279,7 @@ describe('邮件服务测试', () => { }; const result = await sendTemplateEmail(invalidTemplateOptions); - + expect(result.success).toBe(false); expect(result).toHaveProperty('error'); expect(result.error).toContain('未找到邮件模板'); @@ -2266,11 +2296,11 @@ describe('邮件服务测试', () => { }; const result = await sendEmail(emailOptions); - + // 如果SMTP未配置或连接失败,应该有适当的错误处理 expect(result).toHaveProperty('success'); expect(result).toHaveProperty('sentAt'); - + if (!result.success) { expect(result).toHaveProperty('error'); expect(typeof result.error).toBe('string'); @@ -2281,29 +2311,33 @@ describe('邮件服务测试', () => { describe('邮件服务性能测试', () => { const runPerformanceTest = isEmailConfigValid ? it : it.skip; - runPerformanceTest('应该能够并发发送多封邮件', async () => { - const emailPromises = []; - const emailCount = 3; // 限制并发数量以避免SMTP限制 + runPerformanceTest( + '应该能够并发发送多封邮件', + async () => { + const emailPromises = []; + const emailCount = 3; // 限制并发数量以避免SMTP限制 - for (let i = 0; i < emailCount; i++) { - const emailOptions: EmailSendOptions = { - to: TEST_EMAIL, - subject: `并发测试邮件 ${i + 1}`, - text: `这是第 ${i + 1} 封并发测试邮件`, - }; - - emailPromises.push(sendEmail(emailOptions)); - } + for (let i = 0; i < emailCount; i++) { + const emailOptions: EmailSendOptions = { + to: TEST_EMAIL, + subject: `并发测试邮件 ${i + 1}`, + text: `这是第 ${i + 1} 封并发测试邮件`, + }; - const results = await Promise.all(emailPromises); - - expect(results).toHaveLength(emailCount); - results.forEach((result, index) => { - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - console.log(`邮件 ${index + 1} 发送结果:`, result.success ? '成功' : '失败'); - }); - }, 30000); // 更长的超时时间用于并发测试 + emailPromises.push(sendEmail(emailOptions)); + } + + const results = await Promise.all(emailPromises); + + expect(results).toHaveLength(emailCount); + results.forEach((result, index) => { + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + console.log(`邮件 ${index + 1} 发送结果:`, result.success ? '成功' : '失败'); + }); + }, + 30000, + ); // 更长的超时时间用于并发测试 }); describe('邮件模板渲染测试', () => { @@ -2323,7 +2357,7 @@ describe('邮件服务测试', () => { // 我们可以通过发送结果来验证模板是否正确处理 const result = await sendTemplateEmail(templateOptions); expect(result).toHaveProperty('success'); - + // 模板渲染错误会导致发送失败 if (!result.success && result.error) { expect(result.error).not.toContain('模板渲染失败'); @@ -2340,6 +2374,7 @@ describe('邮件服务测试', () => { ## 🎉 测试结果总结 ### ✅ 成功的功能 + 1. **服务初始化** - 邮件服务成功连接和初始化 2. **配置验证** - 正确验证邮件配置有效性 3. **状态管理** - 服务状态检查和健康监控 @@ -2348,7 +2383,9 @@ describe('邮件服务测试', () => { 6. **并发处理** - 支持并发邮件发送 ### ⚠️ 需要配置的部分 + 邮件发送失败的原因是SMTP认证问题: + - **QQ邮箱需要授权码**:需要在QQ邮箱设置中生成授权码 - **From头部格式**:需要正确的邮箱地址格式 @@ -2367,13 +2404,14 @@ describe('邮件服务测试', () => { 现在按照rules完成git提交: -🎉 **邮件发送服务集成任务已完成!** +🎉 **邮件发送服务集成任务已完成!** ## 📋 任务完成情况 ✅ **所有8个子任务已完成**: + 1. 邮件服务基础配置 ✅ -2. 邮件配置文件创建 ✅ +2. 邮件配置文件创建 ✅ 3. 邮件服务类型定义 ✅ 4. 邮件发送服务类实现 ✅ 5. Elysia邮件服务插件 ✅ @@ -2386,6 +2424,7 @@ describe('邮件服务测试', () => { ## 🚀 邮件服务功能特点 ### 💼 核心功能 + - **完整的邮件发送服务**:支持文本和HTML邮件 - **5种内置模板**:用户激活、密码重置、欢迎邮件、系统通知、密码修改通知 - **智能重试机制**:自动重试失败的邮件发送 @@ -2393,6 +2432,7 @@ describe('邮件服务测试', () => { - **并发支持**:支持批量和并发邮件发送 ### 🔧 技术特点 + - **TypeScript类型安全**:完整的类型定义 - **Elysia插件集成**:便捷的API调用方式 - **配置灵活**:支持多种SMTP服务商 @@ -2409,8 +2449,8 @@ app.post('/send-activation', async ({ email }) => { params: { username: 'user123', nickname: '用户昵称', - activationUrl: 'https://yoursite.com/activate?token=xxx' - } + activationUrl: 'https://yoursite.com/activate?token=xxx', + }, }); return { success: result.success }; }); @@ -2433,8 +2473,8 @@ app.post('/send-activation', async ({ email }) => { params: { username: 'user123', nickname: '用户昵称', - activationUrl: 'https://yoursite.com/activate?token=xxx' - } + activationUrl: 'https://yoursite.com/activate?token=xxx', + }, }); return { success: result.success }; }); @@ -2460,12 +2500,12 @@ app.post('/send-activation', async ({ email }) => { * @description 简单的邮件发送测试demo,用于验证邮件服务是否正常工作 */ -import { +import { initializeEmailService, sendEmail, sendTemplateEmail, checkEmailServiceHealth, - closeEmailService + closeEmailService, } from '@/plugins/email/email.service'; import { validateEmailConfig } from '@/config/email.config'; import type { EmailSendOptions, EmailTemplateSendOptions } from '@/type/email.type'; @@ -2480,7 +2520,7 @@ class EmailDemo { async init(): Promise { try { console.log('🚀 正在初始化邮件服务...'); - + // 检查配置 const isConfigValid = validateEmailConfig(); if (!isConfigValid) { @@ -2491,7 +2531,7 @@ class EmailDemo { // 初始化服务 await initializeEmailService(); this.initialized = true; - + console.log('✅ 邮件服务初始化成功'); return true; } catch (error) { @@ -2520,7 +2560,7 @@ class EmailDemo { */ async sendSimpleEmail(to: string): Promise { console.log(`\n📧 发送简单测试邮件到: ${to}`); - + const emailOptions: EmailSendOptions = { to: to, subject: '邮件服务测试 - 简单邮件', @@ -2544,7 +2584,7 @@ class EmailDemo { try { const result = await sendEmail(emailOptions); - + if (result.success) { console.log('✅ 邮件发送成功!'); console.log(`📮 消息ID: ${result.messageId}`); @@ -2566,7 +2606,7 @@ class EmailDemo { */ async sendTemplateEmail(to: string): Promise { console.log(`\n📧 发送模板邮件到: ${to}`); - + const templateOptions: EmailTemplateSendOptions = { to: to, template: 'welcome', @@ -2579,7 +2619,7 @@ class EmailDemo { try { const result = await sendTemplateEmail(templateOptions); - + if (result.success) { console.log('✅ 模板邮件发送成功!'); console.log(`📮 消息ID: ${result.messageId}`); @@ -2611,7 +2651,7 @@ async function main() { console.log('='.repeat(50)); const demo = new EmailDemo(); - + try { // 初始化 const initSuccess = await demo.init(); @@ -2632,7 +2672,7 @@ async function main() { // 询问收件人邮箱 const testEmail = process.argv[2] || 'test@example.com'; console.log(`\n🎯 测试邮箱: ${testEmail}`); - + if (testEmail === 'test@example.com') { console.log('💡 提示: 可以通过参数指定邮箱地址'); console.log(' 示例: bun run src/tests/demo/emailDemo.ts your@email.com'); @@ -2640,18 +2680,17 @@ async function main() { // 发送测试邮件 await demo.sendSimpleEmail(testEmail); - + // 等待一下再发送模板邮件 console.log('\n⏳ 等待2秒后发送模板邮件...'); - await new Promise(resolve => setTimeout(resolve, 2000)); - + await new Promise((resolve) => setTimeout(resolve, 2000)); + await demo.sendTemplateEmail(testEmail); console.log('\n' + '='.repeat(50)); console.log('✨ Demo执行完成!请检查您的邮箱收件箱'); console.log('📬 如果没收到邮件,请检查垃圾邮件文件夹'); console.log('='.repeat(50)); - } catch (error) { console.log('💥 Demo执行过程中出现错误:', error); } finally { @@ -2676,15 +2715,11 @@ export { EmailDemo }; * 运行方式: bun run quick-email-test.ts your@email.com */ -import { - initializeEmailService, - sendEmail, - closeEmailService -} from './src/plugins/email/email.service'; +import { initializeEmailService, sendEmail, closeEmailService } from './src/plugins/email/email.service'; async function quickTest() { const testEmail = process.argv[2]; - + if (!testEmail) { console.log('❌ 请提供邮箱地址'); console.log('💡 使用方法: bun run quick-email-test.ts your@email.com'); @@ -2709,7 +2744,7 @@ async function quickTest() {

🎉 邮件测试成功!

如果您收到这封邮件,说明邮件服务配置正确。

发送时间: ${new Date().toLocaleString('zh-CN')}

- ` + `, }); if (result.success) { @@ -2720,7 +2755,6 @@ async function quickTest() { console.log('❌ 邮件发送失败'); console.log(`💥 错误: ${result.error}`); } - } catch (error) { console.log('💥 执行失败:', error); } finally { @@ -2786,6 +2820,7 @@ bun run quick-email-test.ts your@email.com ``` 如果一切配置正确,你应该会看到: + - ✅ 初始化成功 - ✅ 邮件发送成功 - 📮 消息ID: xxx @@ -2886,43 +2921,35 @@ import { validateEmailConfig, getEmailConfig } from './src/config/email.config'; function checkEnvVariables() { console.log('🔍 检查环境变量配置...\n'); - - const requiredVars = [ - 'SMTP_HOST', - 'SMTP_PORT', - 'SMTP_USER', - 'SMTP_PASS', - 'SMTP_FROM_EMAIL' - ]; - + + const requiredVars = ['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM_EMAIL']; + const missingVars: string[] = []; const presentVars: string[] = []; - - requiredVars.forEach(varName => { + + requiredVars.forEach((varName) => { const value = process.env[varName]; if (value) { presentVars.push(varName); // 对敏感信息进行脱敏显示 - const displayValue = varName === 'SMTP_PASS' - ? '*'.repeat(value.length) - : value; + const displayValue = varName === 'SMTP_PASS' ? '*'.repeat(value.length) : value; console.log(`✅ ${varName}: ${displayValue}`); } else { missingVars.push(varName); console.log(`❌ ${varName}: 未设置`); } }); - + console.log(`\n📊 配置状态: ${presentVars.length}/${requiredVars.length} 已配置`); - + if (missingVars.length > 0) { console.log(`\n🚨 缺少以下环境变量:`); - missingVars.forEach(varName => { + missingVars.forEach((varName) => { console.log(` - ${varName}`); }); return false; } - + return true; } @@ -2930,25 +2957,25 @@ function checkQQEmailConfig() { const user = process.env.SMTP_USER; const pass = process.env.SMTP_PASS; const host = process.env.SMTP_HOST; - + console.log('\n🔍 QQ邮箱配置检查...'); - + if (host === 'smtp.qq.com') { console.log('✅ 使用QQ邮箱SMTP服务器'); - + if (user && user.includes('@qq.com')) { console.log('✅ QQ邮箱地址格式正确'); } else { console.log('⚠️ 邮箱地址不是QQ邮箱'); } - + if (pass && pass.length === 16) { console.log('✅ 授权码长度正确(16位)'); } else if (pass && pass.length < 16) { console.log('⚠️ 授权码长度不正确,QQ邮箱授权码通常是16位'); console.log('💡 确认你使用的是授权码,不是QQ登录密码'); } - + console.log('\n💡 QQ邮箱使用提示:'); console.log('1. 确保已开启IMAP/SMTP服务'); console.log('2. 使用16位授权码,不是QQ密码'); @@ -2958,21 +2985,20 @@ function checkQQEmailConfig() { async function testConnection() { console.log('\n🌐 测试SMTP连接...'); - + try { const config = getEmailConfig(); console.log(`📡 连接到: ${config.host}:${config.port}`); console.log(`🔐 安全连接: ${config.secure ? 'SSL/TLS' : 'STARTTLS'}`); console.log(`👤 用户: ${config.auth.user}`); - + // 这里不实际连接,只是显示配置信息 console.log('✅ 配置信息已就绪,可以尝试发送测试邮件'); - } catch (error) { console.log('❌ 配置验证失败:', error); return false; } - + return true; } @@ -2980,27 +3006,27 @@ async function main() { console.log('='.repeat(60)); console.log('📧 邮件配置检查工具'); console.log('='.repeat(60)); - + // 检查环境变量 const hasAllVars = checkEnvVariables(); - + if (!hasAllVars) { console.log('\n❌ 配置不完整,请先设置所有必需的环境变量'); console.log('\n💡 参考配置文件: email-config-example.txt'); console.log('💡 创建.env文件并填入正确的配置信息'); return; } - + // 检查配置有效性 const isValid = validateEmailConfig(); console.log(`\n📋 配置验证: ${isValid ? '✅ 有效' : '❌ 无效'}`); - + // QQ邮箱特殊检查 checkQQEmailConfig(); - + // 测试连接配置 await testConnection(); - + console.log('\n' + '='.repeat(60)); if (isValid) { console.log('🎉 配置检查完成!可以尝试发送测试邮件了'); @@ -3019,21 +3045,21 @@ main().catch(console.error); ```typescript async function testConnection() { console.log('\n🌐 测试SMTP连接...'); - + try { const config = getEmailConfig(); console.log(`📡 连接到: ${config.host}:${config.port}`); console.log(`🔐 安全连接: ${config.secure ? 'SSL/TLS' : 'STARTTLS'}`); console.log(`👤 用户: ${config.auth.user}`); - + // 这里不实际连接,只是显示配置信息 console.log('✅ 配置信息已就绪,可以尝试发送测试邮件'); - + } catch (error) { console.log('❌ 配置验证失败:', error); return false; } - + return true; } @@ -3041,21 +3067,21 @@ async function testConnection() { async function testConnection() { console.log('\n🌐 测试SMTP连接...'); - + try { const config = getEmailConfig(); console.log(`📡 连接到: ${config.smtp.host}:${config.smtp.port}`); console.log(`🔐 安全连接: ${config.smtp.secure ? 'SSL/TLS' : 'STARTTLS'}`); console.log(`👤 用户: ${config.smtp.auth.user}`); - + // 这里不实际连接,只是显示配置信息 console.log('✅ 配置信息已就绪,可以尝试发送测试邮件'); - + } catch (error) { console.log('❌ 配置验证失败:', error); return false; } - + return true; } ``` @@ -3067,6 +3093,7 @@ async function testConnection() { ### 🔥 快速解决步骤 #### 1️⃣ 先检查当前配置 + ```bash bun run check-email-config.ts ``` @@ -3098,6 +3125,7 @@ from: process.env.EMAIL_FROM || `"星撰系统" <${smtpConfig.auth.user}>`, ## 🚨 问题原因 QQ邮箱要求: + 1. **From邮箱地址必须与SMTP认证用户名完全一致** 2. **From字段格式必须符合RFC5322标准** @@ -3169,8 +3197,8 @@ export const emailConfig = { // 准备邮件选项 - 确保From字段格式正确 const fromAddress = options.from || emailConfig.from; const fromName = (emailConfig as any).fromName || '星撰系统'; - const formattedFrom = fromAddress.includes('<') - ? fromAddress + const formattedFrom = fromAddress.includes('<') + ? fromAddress : `"${fromName}" <${fromAddress}>`; const mailOptions = { @@ -3218,8 +3246,8 @@ export const emailConfig = { // 准备邮件选项 - 确保From字段格式正确 const fromAddress = options.from || emailConfig.from; const fromName = (emailConfig as any).fromName || '星撰系统'; - const formattedFrom = fromAddress.includes('<') - ? fromAddress + const formattedFrom = fromAddress.includes('<') + ? fromAddress : `"${fromName}" <${fromAddress}>`; -> @@ -3227,8 +3255,8 @@ export const emailConfig = { // 准备邮件选项 - 确保From字段格式正确 const fromAddress = options.from || emailConfig.from; const fromName = emailConfig.fromName || '星撰系统'; - const formattedFrom = fromAddress.includes('<') - ? fromAddress + const formattedFrom = fromAddress.includes('<') + ? fromAddress : `"${fromName}" <${fromAddress}>`; ``` @@ -3236,8 +3264,8 @@ export const emailConfig = { // 准备邮件选项 - 确保From字段格式正确 const fromAddress = options.from || emailConfig.from; const fromName = emailConfig.fromName || '星撰系统'; - const formattedFrom = fromAddress.includes('<') - ? fromAddress + const formattedFrom = fromAddress.includes('<') + ? fromAddress : `"${fromName}" <${fromAddress}>`; -> @@ -3247,20 +3275,22 @@ export const emailConfig = { if (!fromAddress) { throw new Error('发件人邮箱地址不能为空,请检查SMTP_USER或SMTP_FROM_EMAIL环境变量'); } - + const fromName = emailConfig.fromName || '星撰系统'; - const formattedFrom = fromAddress.includes('<') - ? fromAddress + const formattedFrom = fromAddress.includes('<') + ? fromAddress : `"${fromName}" <${fromAddress}>`; ``` 现在创建一个正确的环境变量配置示例: -```markdown +````markdown # QQ邮箱配置指南 ## 🚨 问题原因 + 错误 `550 The "From" header is missing or invalid` 是因为QQ邮箱要求: + 1. **From字段的邮箱地址必须与SMTP认证用户名完全一致** 2. **From字段格式必须符合RFC5322标准** @@ -3296,10 +3326,12 @@ SMTP_FROM_NAME=星撰系统 # 其他配置 EMAIL_REPLY_TO=your_qq_email@qq.com ``` +```` ### 第三步:替换为你的真实信息 **示例配置:** + ```env SMTP_HOST=smtp.qq.com SMTP_PORT=587 @@ -3332,16 +3364,21 @@ bun run src/tests/demo/emailDemo.ts your_receive_email@example.com ## 🔧 常见问题 ### Q: 为什么要用授权码而不是QQ密码? + A: QQ邮箱的安全策略,第三方应用必须使用授权码 ### Q: 授权码在哪里生成? + A: QQ邮箱设置 → 账户 → 开启IMAP/SMTP服务时生成 ### Q: 为什么From地址必须与SMTP_USER一致? + A: QQ邮箱的反欺诈机制,防止伪造发件人 ### Q: 可以使用其他邮箱服务吗? + A: 可以,修改SMTP_HOST即可: + - 163邮箱:`smtp.163.com` - Gmail:`smtp.gmail.com` - 企业邮箱:根据提供商配置 @@ -3361,7 +3398,8 @@ SMTP_FROM_EMAIL=替换为你的QQ邮箱@qq.com SMTP_FROM_NAME=星撰系统 EMAIL_REPLY_TO=替换为你的QQ邮箱@qq.com ``` -``` + +```` ## 🎉 From字段问题已修复! @@ -3378,10 +3416,10 @@ EMAIL_REPLY_TO=替换为你的QQ邮箱@qq.com 2. **关键配置项**: ```env SMTP_USER=your_qq_email@qq.com - SMTP_PASS=your_16_digit_authorization_code + SMTP_PASS=your_16_digit_authorization_code SMTP_FROM_EMAIL=your_qq_email@qq.com # 必须与SMTP_USER一致! SMTP_FROM_NAME=星撰系统 - ``` +```` ### 🧪 测试修复效果 @@ -3403,7 +3441,7 @@ bun run quick-email-test.ts x71291@outlook.com ```dotenv SMTP_USER=your_qq_email@qq.com - SMTP_PASS=your_16_digit_authorization_code + SMTP_PASS=your_16_digit_authorization_code SMTP_FROM_EMAIL=your_qq_email@qq.com # 必须与SMTP_USER一致! SMTP_FROM_NAME=星撰系统 ``` @@ -3422,9 +3460,8 @@ bun run quick-email-test.ts x71291@outlook.com **Cursor** -🎉 **邮件发送服务集成任务完成!** +🎉 **邮件发送服务集成任务完成!** 现在按照rules执行任务完成流程: 好的,我直接进行git提交和任务归档: - diff --git a/docs/api-type-usage-examples.md b/docs/api-type-usage-examples.md index c16e8da..eeecbda 100644 --- a/docs/api-type-usage-examples.md +++ b/docs/api-type-usage-examples.md @@ -1,157 +1,156 @@ -# API Schema 类型使用指南 - -## 1. Schema 转 TypeScript 类型 - -在 `.schema.ts` 文件中定义并导出类型: - -```typescript -// example.schema.ts -import { t, type Static } from 'elysia'; - -// Schema 定义 -export const GetUserByUsernameSchema = t.Object({ - username: t.String({ - minLength: 2, - maxLength: 50, - description: '用户名', - }), -}); - -// 从 Schema 推断类型 -export type GetUserByUsernameParams = Static; -``` - -## 2. 在 Service 中使用类型 - -```typescript -// example.service.ts -import type { GetUserByUsernameParams, UserInfo } from './example.schema'; - -export class ExampleService { - // 使用类型注解参数 - async getUserByUsername(params: GetUserByUsernameParams): Promise { - const { username } = params; // TypeScript 会自动推断类型 - - // 业务逻辑... - return userResult; - } - - // 或者直接使用解构参数 - async getUserByUsername2({ username }: GetUserByUsernameParams): Promise { - // 业务逻辑... - return userResult; - } -} -``` - -## 3. 在 Controller 中使用类型 - -```typescript -// example.controller.ts -import type { GetUserByUsernameParams, UserInfo } from './example.schema'; -import { GetUserByUsernameSchema } from './example.schema'; - -export const controller = new Elysia() - .get( - '/user/:username', - async ({ params }) => { - // params 自动推断为 GetUserByUsernameParams 类型 - const userInfo: UserInfo = await service.getUserByUsername(params); - return successResponse(userInfo); - }, - { - // 使用 Schema 进行运行时验证 - params: GetUserByUsernameSchema, - } - ); -``` - -## 4. 在测试中使用类型 - -```typescript -// example.test.ts -import type { GetUserByUsernameParams, UserInfo } from './example.schema'; - -describe('用户查询测试', () => { - it('应该正确处理参数类型', () => { - // 类型安全的测试数据 - const validParams: GetUserByUsernameParams = { - username: 'testuser' - }; - - const invalidParams = { - username: 'a' // TypeScript 会提示这可能不符合验证规则 - }; - }); -}); -``` - -## 5. 工具函数中使用类型 - -```typescript -// utils/validators.ts -import type { GetUserByUsernameParams } from '../modules/example/example.schema'; - -// 类型安全的验证函数 -export function validateUsername(params: GetUserByUsernameParams): boolean { - return params.username.length >= 2 && params.username.length <= 50; -} - -// 类型安全的格式化函数 -export function formatUserQuery(params: GetUserByUsernameParams): string { - return `查询用户: ${params.username}`; -} -``` - -## 6. 响应类型使用示例 - -```typescript -// example.response.ts -import { t, type Static } from 'elysia'; -import { UserInfoSchema } from './example.schema'; - -export const GetUserSuccessResponse = t.Object({ - code: t.Literal(0), - message: t.String(), - data: UserInfoSchema, -}); - -// 导出响应类型 -export type GetUserSuccessResponseType = Static; -``` - -## 7. 完整的类型流转示例 - -```typescript -// 完整的类型安全流程 -import type { - GetUserByUsernameParams, - UserInfo -} from './example.schema'; - -// Service 层 -class UserService { - async getUser(params: GetUserByUsernameParams): Promise { - // params.username 有完整的类型提示 - // 返回值必须符合 UserInfo 类型 - } -} - -// Controller 层 -const controller = new Elysia() - .get('/user/:username', async ({ params }) => { - // params 自动推断类型 - const user = await userService.getUser(params); - // user 自动推断为 UserInfo 类型 - return { code: 0, message: '成功', data: user }; - }, { - params: GetUserByUsernameSchema, // 运行时验证 - }); -``` - -## 💡 最佳实践 - -1. **总是导出类型**:每个 Schema 都应该导出对应的 TypeScript 类型 -2. **类型注解**:在 Service 方法中明确使用类型注解 -3. **一致命名**:类型名应该与 Schema 名保持一致的命名规范 -4. **分离关注点**:Schema 用于运行时验证,Type 用于编译时类型检查 \ No newline at end of file +# API Schema 类型使用指南 + +## 1. Schema 转 TypeScript 类型 + +在 `.schema.ts` 文件中定义并导出类型: + +```typescript +// example.schema.ts +import { t, type Static } from 'elysia'; + +// Schema 定义 +export const GetUserByUsernameSchema = t.Object({ + username: t.String({ + minLength: 2, + maxLength: 50, + description: '用户名', + }), +}); + +// 从 Schema 推断类型 +export type GetUserByUsernameParams = Static; +``` + +## 2. 在 Service 中使用类型 + +```typescript +// example.service.ts +import type { GetUserByUsernameParams, UserInfo } from './example.schema'; + +export class ExampleService { + // 使用类型注解参数 + async getUserByUsername(params: GetUserByUsernameParams): Promise { + const { username } = params; // TypeScript 会自动推断类型 + + // 业务逻辑... + return userResult; + } + + // 或者直接使用解构参数 + async getUserByUsername2({ username }: GetUserByUsernameParams): Promise { + // 业务逻辑... + return userResult; + } +} +``` + +## 3. 在 Controller 中使用类型 + +```typescript +// example.controller.ts +import type { GetUserByUsernameParams, UserInfo } from './example.schema'; +import { GetUserByUsernameSchema } from './example.schema'; + +export const controller = new Elysia().get( + '/user/:username', + async ({ params }) => { + // params 自动推断为 GetUserByUsernameParams 类型 + const userInfo: UserInfo = await service.getUserByUsername(params); + return successResponse(userInfo); + }, + { + // 使用 Schema 进行运行时验证 + params: GetUserByUsernameSchema, + }, +); +``` + +## 4. 在测试中使用类型 + +```typescript +// example.test.ts +import type { GetUserByUsernameParams, UserInfo } from './example.schema'; + +describe('用户查询测试', () => { + it('应该正确处理参数类型', () => { + // 类型安全的测试数据 + const validParams: GetUserByUsernameParams = { + username: 'testuser', + }; + + const invalidParams = { + username: 'a', // TypeScript 会提示这可能不符合验证规则 + }; + }); +}); +``` + +## 5. 工具函数中使用类型 + +```typescript +// utils/validators.ts +import type { GetUserByUsernameParams } from '../modules/example/example.schema'; + +// 类型安全的验证函数 +export function validateUsername(params: GetUserByUsernameParams): boolean { + return params.username.length >= 2 && params.username.length <= 50; +} + +// 类型安全的格式化函数 +export function formatUserQuery(params: GetUserByUsernameParams): string { + return `查询用户: ${params.username}`; +} +``` + +## 6. 响应类型使用示例 + +```typescript +// example.response.ts +import { t, type Static } from 'elysia'; +import { UserInfoSchema } from './example.schema'; + +export const GetUserSuccessResponse = t.Object({ + code: t.Literal(0), + message: t.String(), + data: UserInfoSchema, +}); + +// 导出响应类型 +export type GetUserSuccessResponseType = Static; +``` + +## 7. 完整的类型流转示例 + +```typescript +// 完整的类型安全流程 +import type { GetUserByUsernameParams, UserInfo } from './example.schema'; + +// Service 层 +class UserService { + async getUser(params: GetUserByUsernameParams): Promise { + // params.username 有完整的类型提示 + // 返回值必须符合 UserInfo 类型 + } +} + +// Controller 层 +const controller = new Elysia().get( + '/user/:username', + async ({ params }) => { + // params 自动推断类型 + const user = await userService.getUser(params); + // user 自动推断为 UserInfo 类型 + return { code: 0, message: '成功', data: user }; + }, + { + params: GetUserByUsernameSchema, // 运行时验证 + }, +); +``` + +## 💡 最佳实践 + +1. **总是导出类型**:每个 Schema 都应该导出对应的 TypeScript 类型 +2. **类型注解**:在 Service 方法中明确使用类型注解 +3. **一致命名**:类型名应该与 Schema 名保持一致的命名规范 +4. **分离关注点**:Schema 用于运行时验证,Type 用于编译时类型检查 diff --git a/docs/distributed-lock-guide.md b/docs/distributed-lock-guide.md index 557798e..4716296 100644 --- a/docs/distributed-lock-guide.md +++ b/docs/distributed-lock-guide.md @@ -17,22 +17,25 @@ ### 1. 短期操作(推荐不开启自动续期) **适用场景**: + - 用户登录 - Token刷新 - 数据查询 - 简单的数据更新 **配置建议**: + ```typescript const lock = await DistributedLockService.acquire({ key: 'user:login:username', - ttl: 15, // 15秒过期 - timeout: 8000, // 8秒超时 - autoRenew: false // 不开启自动续期 + ttl: 15, // 15秒过期 + timeout: 8000, // 8秒超时 + autoRenew: false, // 不开启自动续期 }); ``` **优点**: + - 简单可靠,不会出现死锁 - 性能开销小 - 适合快速操作 @@ -40,23 +43,26 @@ const lock = await DistributedLockService.acquire({ ### 2. 长期操作(需要开启自动续期) **适用场景**: + - 用户注册(包含邮件发送) - 密码重置(包含邮件发送) - 文件上传 - 复杂的数据处理 **配置建议**: + ```typescript const lock = await DistributedLockService.acquire({ key: 'user:register:username:email', - ttl: 60, // 60秒过期 - timeout: 15000, // 15秒超时 - autoRenew: true, // 开启自动续期 - renewInterval: 20000 // 20秒续期一次 + ttl: 60, // 60秒过期 + timeout: 15000, // 15秒超时 + autoRenew: true, // 开启自动续期 + renewInterval: 20000, // 20秒续期一次 }); ``` **注意事项**: + - 必须确保在操作完成后手动释放锁 - 进程退出时会自动清理锁 - 续期失败时会记录警告日志 @@ -64,26 +70,29 @@ const lock = await DistributedLockService.acquire({ ## 锁键名设计规范 ### 1. 命名规则 + ``` {业务模块}:{操作类型}:{关键标识} ``` ### 2. 示例 + ```typescript // 用户注册锁 -'user:register:username:email' +'user:register:username:email'; // 用户登录锁 -'user:login:username' +'user:login:username'; // 密码重置锁 -'password:reset:email' +'password:reset:email'; // Token刷新锁 -'token:refresh:token_value' +'token:refresh:token_value'; ``` ### 3. 注意事项 + - 键名要具有唯一性 - 避免使用过长的键名 - 使用有意义的标识符 @@ -93,64 +102,71 @@ const lock = await DistributedLockService.acquire({ ### 1. 锁的粒度控制 **好的做法**: + ```typescript // 针对特定用户加锁 const lock = await DistributedLockService.acquire({ key: `user:login:${username}`, ttl: 15, - autoRenew: false + autoRenew: false, }); ``` **避免的做法**: + ```typescript // 锁的粒度太粗,影响其他用户 const lock = await DistributedLockService.acquire({ - key: 'user:login', // 所有用户登录都被阻塞 + key: 'user:login', // 所有用户登录都被阻塞 ttl: 15, - autoRenew: false + autoRenew: false, }); ``` ### 2. 超时时间设置 **原则**: + - 超时时间应该大于预期的操作时间 - 但不要设置过长,避免长时间阻塞 **建议**: + ```typescript // 快速操作 -timeout: 5000 // 5秒 +timeout: 5000; // 5秒 // 中等操作 -timeout: 10000 // 10秒 +timeout: 10000; // 10秒 // 慢速操作 -timeout: 30000 // 30秒 +timeout: 30000; // 30秒 ``` ### 3. TTL设置 **原则**: + - TTL应该大于操作时间 - 对于自动续期的锁,TTL可以设置得相对较短 **建议**: + ```typescript // 快速操作 -ttl: 10 // 10秒 +ttl: 10; // 10秒 // 中等操作 -ttl: 30 // 30秒 +ttl: 30; // 30秒 // 慢速操作 -ttl: 60 // 60秒 +ttl: 60; // 60秒 ``` ### 4. 错误处理 **必须使用 try-finally**: + ```typescript const lock = await DistributedLockService.acquire(config); @@ -166,12 +182,14 @@ try { ### 5. 监控和日志 **监控指标**: + - 锁获取成功率 - 锁等待时间 - 锁释放情况 - 死锁检测 **日志记录**: + ```typescript Logger.info(`获取分布式锁成功: ${lockKey}`); Logger.warn(`锁续期失败: ${lockKey}`); @@ -183,11 +201,13 @@ Logger.error(`获取锁超时: ${lockKey}`); ### 1. 死锁问题 **原因**: + - 进程崩溃但锁未释放 - 网络中断导致无法续期 - 业务逻辑异常导致锁未释放 **解决方案**: + - 设置合理的TTL - 使用try-finally确保锁释放 - 进程退出时自动清理锁 @@ -196,11 +216,13 @@ Logger.error(`获取锁超时: ${lockKey}`); ### 2. 性能问题 **原因**: + - 锁的粒度太粗 - 锁的持有时间过长 - 频繁的锁竞争 **解决方案**: + - 细化锁的粒度 - 优化业务逻辑,减少锁持有时间 - 使用读写锁分离 @@ -209,11 +231,13 @@ Logger.error(`获取锁超时: ${lockKey}`); ### 3. 一致性问题 **原因**: + - 锁释放时机不当 - 业务逻辑异常 - 并发控制不当 **解决方案**: + - 确保锁的原子性操作 - 使用事务保证数据一致性 - 添加业务层面的幂等性检查 @@ -238,9 +262,9 @@ async function complexOperation() { const lock = await DistributedLockService.acquire({ key: 'complex:operation', ttl: 60, - autoRenew: true + autoRenew: true, }); - + try { // 复杂业务逻辑 await step1(); @@ -263,4 +287,4 @@ async function complexOperation() { 5. **监控和日志**:及时发现问题 6. **定期清理**:防止死锁积累 -通过合理使用分布式锁,可以有效保证系统的数据一致性和业务正确性。 \ No newline at end of file +通过合理使用分布式锁,可以有效保证系统的数据一致性和业务正确性。 diff --git a/docs/elysiaAPI开发流程.md b/docs/elysiaAPI开发流程.md index 3c7ff5f..264b41f 100644 --- a/docs/elysiaAPI开发流程.md +++ b/docs/elysiaAPI开发流程.md @@ -119,6 +119,7 @@ src/plugins/ ``` ### 1.4 类型定义 (type/) + ``` src/type/ ├── config.type.ts # 配置相关类型 @@ -131,6 +132,7 @@ src/type/ ``` ### 1.5 工具函数 (utils/) + ``` src/utils/ ├── deviceInfo.ts # 设备信息工具 @@ -148,11 +150,13 @@ src/utils/ ``` ### 1.6 常量定义 (constants/) + ``` src/constants/ ├── swaggerTags.ts # Swagger标签定义:所有模块的tag应该集中在此定义 └── 其他常量 ``` + ### 1.7 数据库实体 ``` @@ -163,6 +167,7 @@ src/eneities/ ``` ### 1.8 文件命名约定 + - 模块名使用 **单数形式**:`auth`、`user`、`product`、`order` - 文件名格式:`[module].[type].ts` - 导出名格式:`[module][类型名]` @@ -182,11 +187,12 @@ src/eneities/ - 注意已经存在的工具函数,比如分页有现成的简化工具,且能够统一请求格式 ### 2.2 特别注意点 + **必须遵循的命名模式:** + - Request类型:`[动作][模块]Request` → `RegisterRequest` - Schema名:`[动作][模块]Schema` → `RegisterSchema` - ### 2.3 代码示例 ```ts @@ -206,38 +212,37 @@ import { createQuerySchema } from '@/utils/pagination'; * 用户列表查询参数Schema * @description 用户列表查询的请求参数验证规则 */ -export const UserListQuerySchema = createQuerySchema(t.Object({ - // 用户特有参数 - keyword: t.Optional(t.String({ - minLength: 1, - maxLength: 100, - description: '搜索关键词,支持用户名、邮箱模糊搜索', - examples: ['admin', 'test@example.com'] - })), - status: t.Optional(t.Union([ - t.Literal('active'), - t.Literal('inactive'), - t.Literal('pending') - ], { - description: '用户状态筛选', - examples: ['active', 'inactive', 'pending'] - })), - gender: t.Optional(t.Union([ - t.Literal(0), - t.Literal(1), - t.Literal(2), - t.Literal('0'), - t.Literal('1'), - t.Literal('2'), - ], { - description: '性别筛选:0-未知,1-男,2-女', - examples: [0, 1, 2] - })), - isRoot: t.Optional(t.Boolean({ - description: '是否超级管理员筛选', - examples: [true, false] - })) -})); +export const UserListQuerySchema = createQuerySchema( + t.Object({ + // 用户特有参数 + keyword: t.Optional( + t.String({ + minLength: 1, + maxLength: 100, + description: '搜索关键词,支持用户名、邮箱模糊搜索', + examples: ['admin', 'test@example.com'], + }), + ), + status: t.Optional( + t.Union([t.Literal('active'), t.Literal('inactive'), t.Literal('pending')], { + description: '用户状态筛选', + examples: ['active', 'inactive', 'pending'], + }), + ), + gender: t.Optional( + t.Union([t.Literal(0), t.Literal(1), t.Literal(2), t.Literal('0'), t.Literal('1'), t.Literal('2')], { + description: '性别筛选:0-未知,1-男,2-女', + examples: [0, 1, 2], + }), + ), + isRoot: t.Optional( + t.Boolean({ + description: '是否超级管理员筛选', + examples: [true, false], + }), + ), + }), +); /** 用户列表查询参数类型 */ export type UserListQueryRequest = Static; @@ -251,9 +256,10 @@ export type UserListQueryRequest = Static; - 错误相应只提供示例和描述 - 导出响应成功类型 - ### 3.2 特别注意点 + **必须遵循的命名模式:** + - Response格式定义:`[动作][模块]Response` → `RegisterResponse` - Response成功类型:`[动作][模块]SuccessType ` → `RegisterSuccessType ` @@ -273,8 +279,6 @@ import { t, type Static } from 'elysia'; import { responseWrapperSchema } from '@/utils/responseFormate'; import { createPaginationResponseSchema } from '@/utils/pagination'; - - /** * 用户列表项Schema * @description 用户列表中单个用户的数据结构 @@ -283,21 +287,21 @@ export const UserListItemSchema = t.Object({ /** 用户ID */ id: t.String({ description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', - examples: ['1', '2', '3'] + examples: ['1', '2', '3'], }), /** 用户名 */ username: t.String({ description: '用户名', - examples: ['admin', 'testuser'] + examples: ['admin', 'testuser'], }), - + /** 更多字段... */ /** 更新时间 */ updatedAt: t.String({ description: '更新时间', - examples: ['2024-12-19T10:30:00Z'] - }) + examples: ['2024-12-19T10:30:00Z'], + }), }); /** @@ -306,28 +310,34 @@ export const UserListItemSchema = t.Object({ */ export const GetUserListResponsesSchema = { 200: responseWrapperSchema(createPaginationResponseSchema(UserListItemSchema)), - 401: responseWrapperSchema(t.Object({ - error: t.String({ - description: '认证失败', - examples: ['未提供有效的认证令牌', '令牌已过期'] - }) - })), - 400: responseWrapperSchema(t.Object({ - error: t.String({ - description: '参数错误', - examples: ['分页参数无效', '搜索关键词格式错误'] - }) - })), - 500: responseWrapperSchema(t.Object({ - error: t.String({ - description: '服务器错误', - examples: ['内部服务器错误'] - }) - })) + 401: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '认证失败', + examples: ['未提供有效的认证令牌', '令牌已过期'], + }), + }), + ), + 400: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '参数错误', + examples: ['分页参数无效', '搜索关键词格式错误'], + }), + }), + ), + 500: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '服务器错误', + examples: ['内部服务器错误'], + }), + }), + ), }; /** 获取用户列表成功响应数据类型 */ -export type GetUserListSuccessType = Static; +export type GetUserListSuccessType = Static<(typeof GetUserListResponsesSchema)[200]>; ``` ## 4. Service(业务逻辑层) @@ -398,7 +408,7 @@ export class UserService { status: sysUsers.status, lastLoginAt: sysUsers.lastLoginAt, createdAt: sysUsers.createdAt, - updatedAt: sysUsers.updatedAt + updatedAt: sysUsers.updatedAt, }) .from(sysUsers) .where(eq(sysUsers.id, BigInt(userId))) @@ -406,34 +416,33 @@ export class UserService { if (!user || user.length === 0) { Logger.warn(`用户不存在:${userId}`); - throw new BusinessError( - `用户不存在:${userId}`, - 404 - ); + throw new BusinessError(`用户不存在:${userId}`, 404); } const userData = user[0]!; Logger.info(`获取用户信息成功:${userId} - ${userData.username}`); - return successResponse({ - id: userId, // 使用传入的字符串ID,避免精度丢失 - username: userData.username, - email: userData.email, - nickname: userData.nickname, - avatar: userData.avatar, - phone: userData.mobile, - status: userData.status, - lastLoginAt: userData.lastLoginAt || null, - createdAt: userData.createdAt, - updatedAt: userData.updatedAt - }, '获取用户信息成功'); + return successResponse( + { + id: userId, // 使用传入的字符串ID,避免精度丢失 + username: userData.username, + email: userData.email, + nickname: userData.nickname, + avatar: userData.avatar, + phone: userData.mobile, + status: userData.status, + lastLoginAt: userData.lastLoginAt || null, + createdAt: userData.createdAt, + updatedAt: userData.updatedAt, + }, + '获取用户信息成功', + ); } - } // 导出单例实例 -export const userService = new UserService(); +export const userService = new UserService(); ``` ## 5. Controllers(接口名称接入) @@ -463,21 +472,18 @@ export const authController = new Elysia() * @route POST /api/auth/register * @description 用户注册,包含验证码验证、用户名邮箱唯一性检查等 */ - .post( - '/register', - ({ body }) => authService.register(body), - { - body: RegisterSchema, - detail: { - summary: '用户注册', - description: '用户注册接口,需要提供用户名、邮箱、密码和验证码', - tags: [tags.auth], - operationId: 'registerUser', - }, - response: RegisterResponsesSchema, - } - ); + .post('/register', ({ body }) => authService.register(body), { + body: RegisterSchema, + detail: { + summary: '用户注册', + description: '用户注册接口,需要提供用户名、邮箱、密码和验证码', + tags: [tags.auth], + operationId: 'registerUser', + }, + response: RegisterResponsesSchema, + }); ``` + ## 6. 错误处理 ### 6.1 错误处理规范 @@ -494,10 +500,11 @@ import { BusinessError } from '@/utils/responseFormate'; // 抛出业务错误 throw new BusinessError('消息说明...', 409); ``` + ## 7. 测试用例文档 1. 分模块 2. 分接口 3. 测试名称 4. 场景 -5. 方法 \ No newline at end of file +5. 方法 diff --git a/docs/git-commit-rules.md b/docs/git-commit-rules.md index e379c0b..f4fc790 100644 --- a/docs/git-commit-rules.md +++ b/docs/git-commit-rules.md @@ -1,44 +1,47 @@ --- -description: -globs: +description: +globs: alwaysApply: true --- + ## Git 提交规范(Commit Message Rules) 1. **提交格式** - ``` - (): - ``` - - `type`:提交类型(必填) - - `scope`:影响范围(可选) - - `subject`:简要描述(必填) + + ``` + (): + ``` + + - `type`:提交类型(必填) + - `scope`:影响范围(可选) + - `subject`:简要描述(必填) 2. **type 类型** - - feat:新功能 - - fix:修复 bug - - docs:文档变更 - - style:代码格式(不影响功能,例如空格、分号等) - - refactor:代码重构(既不是新增功能,也不是修复 bug) - - perf:性能优化 - - test:增加或修改测试 - - chore:构建过程或辅助工具的变动 - - revert:回滚某个提交 + - feat:新功能 + - fix:修复 bug + - docs:文档变更 + - style:代码格式(不影响功能,例如空格、分号等) + - refactor:代码重构(既不是新增功能,也不是修复 bug) + - perf:性能优化 + - test:增加或修改测试 + - chore:构建过程或辅助工具的变动 + - revert:回滚某个提交 3. **scope 范围** - - 用于说明 commit 影响的模块或文件(如 user、api、core 等),可省略。 + - 用于说明 commit 影响的模块或文件(如 user、api、core 等),可省略。 4. **subject 描述** - - 简明扼要,建议不超过 50 字符 - - 以动词开头,首字母小写 - - 结尾不加句号 + - 简明扼要,建议不超过 50 字符 + - 以动词开头,首字母小写 + - 结尾不加句号 5. **正文(body)** - - 可选,详细描述本次提交的内容、动机、对比信息等 - - 建议每行不超过 72 字符 + - 可选,详细描述本次提交的内容、动机、对比信息等 + - 建议每行不超过 72 字符 6. **Footer** - - 可选,用于关联 issue 或进行破坏性变更说明 - - 例如:`BREAKING CHANGE: xxx` 或 `Closes #123` + - 可选,用于关联 issue 或进行破坏性变更说明 + - 例如:`BREAKING CHANGE: xxx` 或 `Closes #123` ### 示例 @@ -52,4 +55,4 @@ docs(readme): 更新安装说明 refactor(core): 优化数据处理逻辑 chore: 升级依赖包 -``` \ No newline at end of file +``` diff --git a/docs/jwt-usage-examples.md b/docs/jwt-usage-examples.md index b6a8db4..8d858a2 100644 --- a/docs/jwt-usage-examples.md +++ b/docs/jwt-usage-examples.md @@ -1,214 +1,204 @@ -# JWT 用户类型使用指南 - -## 概述 - -我们定义了完整的JWT类型系统,提供类型安全的JWT操作。 - -## 类型定义 - -### 1. JwtUserType - JWT中的用户信息 -```typescript -interface JwtUserType { - userId: number; - username: string; - email: string; - nickname?: string; - status: number; - role?: string; -} -``` - -### 2. JwtPayloadType - 完整的JWT载荷 -```typescript -interface JwtPayloadType extends JwtUserType { - iat: number; // 发行时间 - exp: number; // 过期时间 - sub?: string; // 主题 - iss?: string; // 发行者 - aud?: string; // 受众 - jti?: string; // JWT ID - nbf?: number; // 生效时间 -} -``` - -## 使用示例 - -### 1. 在认证Controller中生成JWT Token - -```typescript -// auth.controller.ts -import { createJwtPayload } from '@/utils/jwt.helper'; -import type { UserInfoType } from '@/modules/example/example.schema'; - -export const authController = new Elysia() - .use(jwtPlugin) - .post('/login', async ({ body, jwt }) => { - // 用户登录验证逻辑... - const userInfo: UserInfoType = await getUserFromDatabase(body.username); - - // 创建JWT载荷 - const payload = createJwtPayload(userInfo, { - role: 'user', // 可选的角色信息 - issuer: 'my-api', - audience: 'web-app', - }); - - // 生成Token - const token = await jwt.sign(payload); - - return { - code: 0, - message: '登录成功', - data: { - token, - user: payload, // 返回用户信息(不含敏感数据) - }, - }; - }); -``` - -### 2. 在需要认证的Controller中使用用户信息 - -```typescript -// user.controller.ts -import { formatUserForLog, isValidJwtUser } from '@/utils/jwt.helper'; -import type { JwtUserType } from '@/type/jwt.type'; - -export const userController = new Elysia() - .use(jwtAuthPlugin) - .get('/profile', async ({ user, payload }) => { - // user 自动推断为 JwtUserType 类型 - // payload 自动推断为 JwtPayloadType 类型 - - // 验证用户有效性 - if (!isValidJwtUser(payload)) { - Logger.warn(`无效用户尝试访问: ${formatUserForLog(user)}`); - return { code: 401, message: '用户状态异常', data: null }; - } - - // 使用类型安全的用户信息 - Logger.info(`用户查看个人资料: ${formatUserForLog(user)}`); - - // 获取完整的用户信息(从数据库) - const fullUserInfo = await getUserById(user.userId); - - return { - code: 0, - message: '获取成功', - data: fullUserInfo, - }; - }); -``` - -### 3. 在Service中使用JWT用户类型 - -```typescript -// user.service.ts -import type { JwtUserType } from '@/type/jwt.type'; - -export class UserService { - // 使用JWT用户类型作为参数 - async updateUserProfile(currentUser: JwtUserType, updateData: any) { - // 检查权限 - if (currentUser.status !== 1) { - throw new Error('用户状态异常,无法操作'); - } - - // 更新用户信息 - const updatedUser = await db.update(users) - .set(updateData) - .where(eq(users.id, currentUser.userId)); - - Logger.info(`用户资料更新: ${currentUser.username} (ID: ${currentUser.userId})`); - return updatedUser; - } - - // 根据JWT用户信息获取权限 - async getUserPermissions(jwtUser: JwtUserType): Promise { - const permissions = await db.select() - .from(userPermissions) - .where(eq(userPermissions.userId, jwtUser.userId)); - - return permissions.map(p => p.permission); - } -} -``` - -### 4. Token状态检查 - -```typescript -// middleware/token-check.ts -import { - isTokenExpiringSoon, - getTokenRemainingTime, - formatRemainingTime -} from '@/utils/jwt.helper'; - -export const tokenStatusMiddleware = (app: Elysia) => - app.derive(({ payload, user }) => { - if (!payload) return {}; - - // 检查Token是否即将过期 - const expiringSoon = isTokenExpiringSoon(payload, 30); // 30分钟阈值 - const remainingTime = getTokenRemainingTime(payload); - - if (expiringSoon) { - Logger.warn(`用户Token即将过期: ${user.username}, 剩余时间: ${formatRemainingTime(remainingTime)}`); - } - - return { - tokenInfo: { - expiringSoon, - remainingTime, - formattedTime: formatRemainingTime(remainingTime), - }, - }; - }); -``` - -### 5. 角色权限检查 - -```typescript -// middleware/role-check.ts -import type { JwtUserType } from '@/type/jwt.type'; - -export function requireRole(requiredRole: string) { - return (app: Elysia) => - app.onBeforeHandle(({ user, set }) => { - const jwtUser = user as JwtUserType; - - if (!jwtUser.role || jwtUser.role !== requiredRole) { - Logger.warn(`权限不足: 用户${jwtUser.username}尝试访问需要${requiredRole}角色的资源`); - set.status = 403; - return { - code: 403, - message: '权限不足', - data: null, - }; - } - }); -} - -// 使用示例 -export const adminController = new Elysia() - .use(jwtAuthPlugin) - .use(requireRole('admin')) - .get('/admin-only', () => { - return { message: '只有管理员能看到这个内容' }; - }); -``` - -## 🎯 类型安全的好处 - -1. **编译时检查**: TypeScript 会在编译时检查所有JWT相关操作 -2. **智能提示**: IDE 提供完整的属性提示和自动补全 -3. **重构安全**: 修改用户类型时,所有相关代码都会得到类型检查 -4. **文档作用**: 类型定义本身就是最好的文档 - -## 📝 最佳实践 - -1. **总是使用类型注解**: 在Service方法中明确使用 `JwtUserType` 类型 -2. **验证用户状态**: 使用 `isValidJwtUser()` 检查用户有效性 -3. **记录操作日志**: 使用 `formatUserForLog()` 格式化用户信息 -4. **检查Token状态**: 在关键操作前检查Token是否即将过期 -5. **权限分离**: 使用角色字段实现细粒度权限控制 \ No newline at end of file +# JWT 用户类型使用指南 + +## 概述 + +我们定义了完整的JWT类型系统,提供类型安全的JWT操作。 + +## 类型定义 + +### 1. JwtUserType - JWT中的用户信息 + +```typescript +interface JwtUserType { + userId: number; + username: string; + email: string; + nickname?: string; + status: number; + role?: string; +} +``` + +### 2. JwtPayloadType - 完整的JWT载荷 + +```typescript +interface JwtPayloadType extends JwtUserType { + iat: number; // 发行时间 + exp: number; // 过期时间 + sub?: string; // 主题 + iss?: string; // 发行者 + aud?: string; // 受众 + jti?: string; // JWT ID + nbf?: number; // 生效时间 +} +``` + +## 使用示例 + +### 1. 在认证Controller中生成JWT Token + +```typescript +// auth.controller.ts +import { createJwtPayload } from '@/utils/jwt.helper'; +import type { UserInfoType } from '@/modules/example/example.schema'; + +export const authController = new Elysia().use(jwtPlugin).post('/login', async ({ body, jwt }) => { + // 用户登录验证逻辑... + const userInfo: UserInfoType = await getUserFromDatabase(body.username); + + // 创建JWT载荷 + const payload = createJwtPayload(userInfo, { + role: 'user', // 可选的角色信息 + issuer: 'my-api', + audience: 'web-app', + }); + + // 生成Token + const token = await jwt.sign(payload); + + return { + code: 0, + message: '登录成功', + data: { + token, + user: payload, // 返回用户信息(不含敏感数据) + }, + }; +}); +``` + +### 2. 在需要认证的Controller中使用用户信息 + +```typescript +// user.controller.ts +import { formatUserForLog, isValidJwtUser } from '@/utils/jwt.helper'; +import type { JwtUserType } from '@/type/jwt.type'; + +export const userController = new Elysia().use(jwtAuthPlugin).get('/profile', async ({ user, payload }) => { + // user 自动推断为 JwtUserType 类型 + // payload 自动推断为 JwtPayloadType 类型 + + // 验证用户有效性 + if (!isValidJwtUser(payload)) { + Logger.warn(`无效用户尝试访问: ${formatUserForLog(user)}`); + return { code: 401, message: '用户状态异常', data: null }; + } + + // 使用类型安全的用户信息 + Logger.info(`用户查看个人资料: ${formatUserForLog(user)}`); + + // 获取完整的用户信息(从数据库) + const fullUserInfo = await getUserById(user.userId); + + return { + code: 0, + message: '获取成功', + data: fullUserInfo, + }; +}); +``` + +### 3. 在Service中使用JWT用户类型 + +```typescript +// user.service.ts +import type { JwtUserType } from '@/type/jwt.type'; + +export class UserService { + // 使用JWT用户类型作为参数 + async updateUserProfile(currentUser: JwtUserType, updateData: any) { + // 检查权限 + if (currentUser.status !== 1) { + throw new Error('用户状态异常,无法操作'); + } + + // 更新用户信息 + const updatedUser = await db.update(users).set(updateData).where(eq(users.id, currentUser.userId)); + + Logger.info(`用户资料更新: ${currentUser.username} (ID: ${currentUser.userId})`); + return updatedUser; + } + + // 根据JWT用户信息获取权限 + async getUserPermissions(jwtUser: JwtUserType): Promise { + const permissions = await db.select().from(userPermissions).where(eq(userPermissions.userId, jwtUser.userId)); + + return permissions.map((p) => p.permission); + } +} +``` + +### 4. Token状态检查 + +```typescript +// middleware/token-check.ts +import { isTokenExpiringSoon, getTokenRemainingTime, formatRemainingTime } from '@/utils/jwt.helper'; + +export const tokenStatusMiddleware = (app: Elysia) => + app.derive(({ payload, user }) => { + if (!payload) return {}; + + // 检查Token是否即将过期 + const expiringSoon = isTokenExpiringSoon(payload, 30); // 30分钟阈值 + const remainingTime = getTokenRemainingTime(payload); + + if (expiringSoon) { + Logger.warn(`用户Token即将过期: ${user.username}, 剩余时间: ${formatRemainingTime(remainingTime)}`); + } + + return { + tokenInfo: { + expiringSoon, + remainingTime, + formattedTime: formatRemainingTime(remainingTime), + }, + }; + }); +``` + +### 5. 角色权限检查 + +```typescript +// middleware/role-check.ts +import type { JwtUserType } from '@/type/jwt.type'; + +export function requireRole(requiredRole: string) { + return (app: Elysia) => + app.onBeforeHandle(({ user, set }) => { + const jwtUser = user as JwtUserType; + + if (!jwtUser.role || jwtUser.role !== requiredRole) { + Logger.warn(`权限不足: 用户${jwtUser.username}尝试访问需要${requiredRole}角色的资源`); + set.status = 403; + return { + code: 403, + message: '权限不足', + data: null, + }; + } + }); +} + +// 使用示例 +export const adminController = new Elysia() + .use(jwtAuthPlugin) + .use(requireRole('admin')) + .get('/admin-only', () => { + return { message: '只有管理员能看到这个内容' }; + }); +``` + +## 🎯 类型安全的好处 + +1. **编译时检查**: TypeScript 会在编译时检查所有JWT相关操作 +2. **智能提示**: IDE 提供完整的属性提示和自动补全 +3. **重构安全**: 修改用户类型时,所有相关代码都会得到类型检查 +4. **文档作用**: 类型定义本身就是最好的文档 + +## 📝 最佳实践 + +1. **总是使用类型注解**: 在Service方法中明确使用 `JwtUserType` 类型 +2. **验证用户状态**: 使用 `isValidJwtUser()` 检查用户有效性 +3. **记录操作日志**: 使用 `formatUserForLog()` 格式化用户信息 +4. **检查Token状态**: 在关键操作前检查Token是否即将过期 +5. **权限分离**: 使用角色字段实现细粒度权限控制 diff --git a/drizzle.config.ts b/drizzle.config.ts index d8484f1..e893c6e 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -9,14 +9,14 @@ import { dbConfig } from '@/config'; -/** +/** * Drizzle Kit配置对象 * 使用前需要安装: bun add drizzle-kit -D */ export default { /** 数据库类型 */ dialect: 'mysql', - + /** 数据库连接配置 */ dbCredentials: { host: dbConfig.host, @@ -25,21 +25,21 @@ export default { password: dbConfig.password, database: dbConfig.database, }, - + /** Schema文件路径 */ schema: './src/entities/schema.js', - + /** 迁移文件输出目录 */ out: './drizzle', - + /** 详细日志 */ verbose: true, - + /** 严格模式 */ strict: true, - + introspect: { // 启用驼峰命名 casing: 'camel', }, -} as const; \ No newline at end of file +} as const; diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index 6396652..d0f6b01 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,2100 +1,1903 @@ { - "id": "00000000-0000-0000-0000-000000000000", - "prevId": "", - "version": "5", - "dialect": "mysql", - "tables": { - "sys_dict_items": { - "name": "sys_dict_items", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "type_id": { - "autoincrement": false, - "name": "type_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "item_key": { - "autoincrement": false, - "name": "item_key", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "item_value": { - "autoincrement": false, - "name": "item_value", - "type": "varchar(200)", - "primaryKey": false, - "notNull": true - }, - "label": { - "autoincrement": false, - "name": "label", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "label_en": { - "autoincrement": false, - "name": "label_en", - "type": "varchar(200)", - "primaryKey": false, - "notNull": false - }, - "description": { - "autoincrement": false, - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "pid": { - "default": 0, - "autoincrement": false, - "name": "pid", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "path": { - "autoincrement": false, - "name": "path", - "type": "varchar(500)", - "primaryKey": false, - "notNull": false - }, - "level": { - "default": 1, - "autoincrement": false, - "name": "level", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "sort_order": { - "default": 0, - "autoincrement": false, - "name": "sort_order", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "status": { - "default": "'active'", - "autoincrement": false, - "name": "status", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true - }, - "css_class": { - "autoincrement": false, - "name": "css_class", - "type": "varchar(50)", - "primaryKey": false, - "notNull": false - }, - "color": { - "autoincrement": false, - "name": "color", - "type": "varchar(50)", - "primaryKey": false, - "notNull": false - }, - "extra": { - "autoincrement": false, - "name": "extra", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "updated_by": { - "autoincrement": false, - "name": "updated_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "updated_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "deleted_at": { - "autoincrement": false, - "name": "deleted_at", - "type": "datetime", - "primaryKey": false, - "notNull": false - } - }, - "compositePrimaryKeys": { - "sys_dict_items_id": { - "name": "sys_dict_items_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_deleted_at": { - "name": "idx_deleted_at", - "columns": [ - "deleted_at" - ], - "isUnique": false - }, - "idx_key": { - "name": "idx_key", - "columns": [ - "item_key" - ], - "isUnique": false - }, - "idx_pid": { - "name": "idx_pid", - "columns": [ - "pid" - ], - "isUnique": false - }, - "idx_sort": { - "name": "idx_sort", - "columns": [ - "type_id", - "sort_order" - ], - "isUnique": false - }, - "idx_status": { - "name": "idx_status", - "columns": [ - "status" - ], - "isUnique": false - }, - "idx_type_id": { - "name": "idx_type_id", - "columns": [ - "type_id" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_type_key": { - "name": "uk_type_key", - "columns": [ - "type_id", - "item_key", - "deleted_at" - ] - } - }, - "checkConstraint": {} - }, - "sys_dict_types": { - "name": "sys_dict_types", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "code": { - "autoincrement": false, - "name": "code", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "name": { - "autoincrement": false, - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "description": { - "autoincrement": false, - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "pid": { - "default": 0, - "autoincrement": false, - "name": "pid", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "path": { - "autoincrement": false, - "name": "path", - "type": "varchar(500)", - "primaryKey": false, - "notNull": false - }, - "level": { - "default": 1, - "autoincrement": false, - "name": "level", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "status": { - "default": "'active'", - "autoincrement": false, - "name": "status", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true - }, - "is_system": { - "default": 0, - "autoincrement": false, - "name": "is_system", - "type": "tinyint(1)", - "primaryKey": false, - "notNull": true - }, - "sort_order": { - "default": 0, - "autoincrement": false, - "name": "sort_order", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "updated_by": { - "autoincrement": false, - "name": "updated_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "updated_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "deleted_at": { - "autoincrement": false, - "name": "deleted_at", - "type": "datetime", - "primaryKey": false, - "notNull": false - } - }, - "compositePrimaryKeys": { - "sys_dict_types_id": { - "name": "sys_dict_types_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_deleted_at": { - "name": "idx_deleted_at", - "columns": [ - "deleted_at" - ], - "isUnique": false - }, - "idx_is_system": { - "name": "idx_is_system", - "columns": [ - "is_system" - ], - "isUnique": false - }, - "idx_name": { - "name": "idx_name", - "columns": [ - "name" - ], - "isUnique": false - }, - "idx_path": { - "name": "idx_path", - "columns": [ - "path" - ], - "isUnique": false - }, - "idx_pid": { - "name": "idx_pid", - "columns": [ - "pid" - ], - "isUnique": false - }, - "idx_sort": { - "name": "idx_sort", - "columns": [ - "pid", - "sort_order" - ], - "isUnique": false - }, - "idx_status": { - "name": "idx_status", - "columns": [ - "status" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_code": { - "name": "uk_code", - "columns": [ - "code", - "deleted_at" - ] - } - }, - "checkConstraint": {} - }, - "sys_operation_logs": { - "name": "sys_operation_logs", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "autoincrement": false, - "name": "user_id", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "username": { - "autoincrement": false, - "name": "username", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "module": { - "autoincrement": false, - "name": "module", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "action": { - "autoincrement": false, - "name": "action", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "target": { - "autoincrement": false, - "name": "target", - "type": "varchar(200)", - "primaryKey": false, - "notNull": false - }, - "target_id": { - "autoincrement": false, - "name": "target_id", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "request_data": { - "autoincrement": false, - "name": "request_data", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "response_data": { - "autoincrement": false, - "name": "response_data", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "autoincrement": false, - "name": "status", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true - }, - "ip": { - "autoincrement": false, - "name": "ip", - "type": "varchar(45)", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "autoincrement": false, - "name": "user_agent", - "type": "varchar(200)", - "primaryKey": false, - "notNull": false - }, - "duration": { - "autoincrement": false, - "name": "duration", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "error_msg": { - "autoincrement": false, - "name": "error_msg", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - } - }, - "compositePrimaryKeys": { - "sys_operation_logs_id": { - "name": "sys_operation_logs_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_created_at": { - "name": "idx_created_at", - "columns": [ - "created_at" - ], - "isUnique": false - }, - "idx_ip": { - "name": "idx_ip", - "columns": [ - "ip" - ], - "isUnique": false - }, - "idx_module_action": { - "name": "idx_module_action", - "columns": [ - "module", - "action" - ], - "isUnique": false - }, - "idx_status": { - "name": "idx_status", - "columns": [ - "status" - ], - "isUnique": false - }, - "idx_target": { - "name": "idx_target", - "columns": [ - "target_id" - ], - "isUnique": false - }, - "idx_user_id": { - "name": "idx_user_id", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": {}, - "checkConstraint": {} - }, - "sys_organizations": { - "name": "sys_organizations", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "code": { - "autoincrement": false, - "name": "code", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "name": { - "autoincrement": false, - "name": "name", - "type": "varchar(200)", - "primaryKey": false, - "notNull": true - }, - "full_name": { - "autoincrement": false, - "name": "full_name", - "type": "varchar(200)", - "primaryKey": false, - "notNull": false - }, - "description": { - "autoincrement": false, - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "pid": { - "default": 0, - "autoincrement": false, - "name": "pid", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "path": { - "autoincrement": false, - "name": "path", - "type": "varchar(500)", - "primaryKey": false, - "notNull": false - }, - "level": { - "default": 1, - "autoincrement": false, - "name": "level", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "type": { - "autoincrement": false, - "name": "type", - "type": "varchar(20)", - "primaryKey": false, - "notNull": false - }, - "status": { - "default": "'active'", - "autoincrement": false, - "name": "status", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true - }, - "sort_order": { - "default": 0, - "autoincrement": false, - "name": "sort_order", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "leader_id": { - "autoincrement": false, - "name": "leader_id", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "address": { - "autoincrement": false, - "name": "address", - "type": "varchar(200)", - "primaryKey": false, - "notNull": false - }, - "phone": { - "autoincrement": false, - "name": "phone", - "type": "varchar(50)", - "primaryKey": false, - "notNull": false - }, - "extra": { - "autoincrement": false, - "name": "extra", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "updated_by": { - "autoincrement": false, - "name": "updated_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "updated_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "deleted_at": { - "autoincrement": false, - "name": "deleted_at", - "type": "datetime", - "primaryKey": false, - "notNull": false - }, - "version": { - "default": 1, - "autoincrement": false, - "name": "version", - "type": "int", - "primaryKey": false, - "notNull": true - } - }, - "compositePrimaryKeys": { - "sys_organizations_id": { - "name": "sys_organizations_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_deleted_at": { - "name": "idx_deleted_at", - "columns": [ - "deleted_at" - ], - "isUnique": false - }, - "idx_leader_id": { - "name": "idx_leader_id", - "columns": [ - "leader_id" - ], - "isUnique": false - }, - "idx_name": { - "name": "idx_name", - "columns": [ - "name" - ], - "isUnique": false - }, - "idx_path": { - "name": "idx_path", - "columns": [ - "path" - ], - "isUnique": false - }, - "idx_pid": { - "name": "idx_pid", - "columns": [ - "pid" - ], - "isUnique": false - }, - "idx_sort": { - "name": "idx_sort", - "columns": [ - "pid", - "sort_order" - ], - "isUnique": false - }, - "idx_status": { - "name": "idx_status", - "columns": [ - "status" - ], - "isUnique": false - }, - "idx_type": { - "name": "idx_type", - "columns": [ - "type" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_code": { - "name": "uk_code", - "columns": [ - "code", - "deleted_at" - ] - } - }, - "checkConstraint": {} - }, - "sys_permissions": { - "name": "sys_permissions", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "code": { - "autoincrement": false, - "name": "code", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "name": { - "autoincrement": false, - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "type": { - "autoincrement": false, - "name": "type", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true - }, - "resource": { - "autoincrement": false, - "name": "resource", - "type": "varchar(50)", - "primaryKey": false, - "notNull": false - }, - "action": { - "autoincrement": false, - "name": "action", - "type": "varchar(50)", - "primaryKey": false, - "notNull": false - }, - "description": { - "autoincrement": false, - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "pid": { - "default": 0, - "autoincrement": false, - "name": "pid", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "path": { - "autoincrement": false, - "name": "path", - "type": "varchar(500)", - "primaryKey": false, - "notNull": false - }, - "level": { - "default": 1, - "autoincrement": false, - "name": "level", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "sort_order": { - "default": 0, - "autoincrement": false, - "name": "sort_order", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "status": { - "default": "'active'", - "autoincrement": false, - "name": "status", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true - }, - "meta": { - "autoincrement": false, - "name": "meta", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "updated_by": { - "autoincrement": false, - "name": "updated_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "updated_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "deleted_at": { - "autoincrement": false, - "name": "deleted_at", - "type": "datetime", - "primaryKey": false, - "notNull": false - } - }, - "compositePrimaryKeys": { - "sys_permissions_id": { - "name": "sys_permissions_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_deleted_at": { - "name": "idx_deleted_at", - "columns": [ - "deleted_at" - ], - "isUnique": false - }, - "idx_pid": { - "name": "idx_pid", - "columns": [ - "pid" - ], - "isUnique": false - }, - "idx_resource_action": { - "name": "idx_resource_action", - "columns": [ - "resource", - "action" - ], - "isUnique": false - }, - "idx_sort": { - "name": "idx_sort", - "columns": [ - "pid", - "sort_order" - ], - "isUnique": false - }, - "idx_status": { - "name": "idx_status", - "columns": [ - "status" - ], - "isUnique": false - }, - "idx_type": { - "name": "idx_type", - "columns": [ - "type" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_code": { - "name": "uk_code", - "columns": [ - "code", - "deleted_at" - ] - } - }, - "checkConstraint": {} - }, - "sys_role_permissions": { - "name": "sys_role_permissions", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "role_id": { - "autoincrement": false, - "name": "role_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "permission_id": { - "autoincrement": false, - "name": "permission_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "is_half": { - "default": 0, - "autoincrement": false, - "name": "is_half", - "type": "tinyint(1)", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - } - }, - "compositePrimaryKeys": { - "sys_role_permissions_id": { - "name": "sys_role_permissions_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_is_half": { - "name": "idx_is_half", - "columns": [ - "is_half" - ], - "isUnique": false - }, - "idx_permission_id": { - "name": "idx_permission_id", - "columns": [ - "permission_id" - ], - "isUnique": false - }, - "idx_role_id": { - "name": "idx_role_id", - "columns": [ - "role_id" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_role_permission": { - "name": "uk_role_permission", - "columns": [ - "role_id", - "permission_id" - ] - } - }, - "checkConstraint": {} - }, - "sys_roles": { - "name": "sys_roles", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "code": { - "autoincrement": false, - "name": "code", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "name": { - "autoincrement": false, - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "description": { - "autoincrement": false, - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "pid": { - "default": 0, - "autoincrement": false, - "name": "pid", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "path": { - "autoincrement": false, - "name": "path", - "type": "varchar(500)", - "primaryKey": false, - "notNull": false - }, - "level": { - "default": 1, - "autoincrement": false, - "name": "level", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "sort_order": { - "default": 0, - "autoincrement": false, - "name": "sort_order", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "status": { - "default": "'active'", - "autoincrement": false, - "name": "status", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true - }, - "is_system": { - "default": 0, - "autoincrement": false, - "name": "is_system", - "type": "tinyint(1)", - "primaryKey": false, - "notNull": true - }, - "permissions_snapshot": { - "autoincrement": false, - "name": "permissions_snapshot", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "extra": { - "autoincrement": false, - "name": "extra", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "updated_by": { - "autoincrement": false, - "name": "updated_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "updated_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "deleted_at": { - "autoincrement": false, - "name": "deleted_at", - "type": "datetime", - "primaryKey": false, - "notNull": false - }, - "version": { - "default": 1, - "autoincrement": false, - "name": "version", - "type": "int", - "primaryKey": false, - "notNull": true - } - }, - "compositePrimaryKeys": { - "sys_roles_id": { - "name": "sys_roles_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_deleted_at": { - "name": "idx_deleted_at", - "columns": [ - "deleted_at" - ], - "isUnique": false - }, - "idx_is_system": { - "name": "idx_is_system", - "columns": [ - "is_system" - ], - "isUnique": false - }, - "idx_name": { - "name": "idx_name", - "columns": [ - "name" - ], - "isUnique": false - }, - "idx_path": { - "name": "idx_path", - "columns": [ - "path" - ], - "isUnique": false - }, - "idx_pid": { - "name": "idx_pid", - "columns": [ - "pid" - ], - "isUnique": false - }, - "idx_sort": { - "name": "idx_sort", - "columns": [ - "pid", - "sort_order" - ], - "isUnique": false - }, - "idx_status": { - "name": "idx_status", - "columns": [ - "status" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_code": { - "name": "uk_code", - "columns": [ - "code", - "deleted_at" - ] - } - }, - "checkConstraint": {} - }, - "sys_tags": { - "name": "sys_tags", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "name": { - "autoincrement": false, - "name": "name", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "type": { - "default": "'user'", - "autoincrement": false, - "name": "type", - "type": "varchar(50)", - "primaryKey": false, - "notNull": false - }, - "color": { - "autoincrement": false, - "name": "color", - "type": "varchar(50)", - "primaryKey": false, - "notNull": false - }, - "description": { - "autoincrement": false, - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "usage_count": { - "default": 0, - "autoincrement": false, - "name": "usage_count", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "deleted_at": { - "autoincrement": false, - "name": "deleted_at", - "type": "datetime", - "primaryKey": false, - "notNull": false - } - }, - "compositePrimaryKeys": { - "sys_tags_id": { - "name": "sys_tags_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_deleted_at": { - "name": "idx_deleted_at", - "columns": [ - "deleted_at" - ], - "isUnique": false - }, - "idx_name": { - "name": "idx_name", - "columns": [ - "name" - ], - "isUnique": false - }, - "idx_type": { - "name": "idx_type", - "columns": [ - "type" - ], - "isUnique": false - }, - "idx_usage_count": { - "name": "idx_usage_count", - "columns": [ - "usage_count" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_name_type": { - "name": "uk_name_type", - "columns": [ - "name", - "type", - "deleted_at" - ] - } - }, - "checkConstraint": {} - }, - "sys_user_organizations": { - "name": "sys_user_organizations", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "autoincrement": false, - "name": "user_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "organization_id": { - "autoincrement": false, - "name": "organization_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "is_primary": { - "default": 0, - "autoincrement": false, - "name": "is_primary", - "type": "tinyint(1)", - "primaryKey": false, - "notNull": true - }, - "position": { - "autoincrement": false, - "name": "position", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "joined_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "joined_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - } - }, - "compositePrimaryKeys": { - "sys_user_organizations_id": { - "name": "sys_user_organizations_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_is_primary": { - "name": "idx_is_primary", - "columns": [ - "is_primary" - ], - "isUnique": false - }, - "idx_joined_at": { - "name": "idx_joined_at", - "columns": [ - "joined_at" - ], - "isUnique": false - }, - "idx_organization_id": { - "name": "idx_organization_id", - "columns": [ - "organization_id" - ], - "isUnique": false - }, - "idx_user_id": { - "name": "idx_user_id", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_user_org": { - "name": "uk_user_org", - "columns": [ - "user_id", - "organization_id" - ] - } - }, - "checkConstraint": {} - }, - "sys_user_roles": { - "name": "sys_user_roles", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "autoincrement": false, - "name": "user_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "role_id": { - "autoincrement": false, - "name": "role_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "expired_at": { - "autoincrement": false, - "name": "expired_at", - "type": "datetime", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - } - }, - "compositePrimaryKeys": { - "sys_user_roles_id": { - "name": "sys_user_roles_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_created_at": { - "name": "idx_created_at", - "columns": [ - "created_at" - ], - "isUnique": false - }, - "idx_expired_at": { - "name": "idx_expired_at", - "columns": [ - "expired_at" - ], - "isUnique": false - }, - "idx_role_id": { - "name": "idx_role_id", - "columns": [ - "role_id" - ], - "isUnique": false - }, - "idx_user_id": { - "name": "idx_user_id", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_user_role": { - "name": "uk_user_role", - "columns": [ - "user_id", - "role_id" - ] - } - }, - "checkConstraint": {} - }, - "sys_user_tags": { - "name": "sys_user_tags", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "autoincrement": false, - "name": "user_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "tag_id": { - "autoincrement": false, - "name": "tag_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - } - }, - "compositePrimaryKeys": { - "sys_user_tags_id": { - "name": "sys_user_tags_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_created_at": { - "name": "idx_created_at", - "columns": [ - "created_at" - ], - "isUnique": false - }, - "idx_tag_id": { - "name": "idx_tag_id", - "columns": [ - "tag_id" - ], - "isUnique": false - }, - "idx_user_id": { - "name": "idx_user_id", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_user_tag": { - "name": "uk_user_tag", - "columns": [ - "user_id", - "tag_id" - ] - } - }, - "checkConstraint": {} - }, - "sys_users": { - "name": "sys_users", - "columns": { - "id": { - "autoincrement": false, - "name": "id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "username": { - "autoincrement": false, - "name": "username", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "email": { - "autoincrement": false, - "name": "email", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "mobile": { - "autoincrement": false, - "name": "mobile", - "type": "varchar(20)", - "primaryKey": false, - "notNull": false - }, - "password_hash": { - "autoincrement": false, - "name": "password_hash", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "avatar": { - "autoincrement": false, - "name": "avatar", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "nickname": { - "autoincrement": false, - "name": "nickname", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "status": { - "default": "'active'", - "autoincrement": false, - "name": "status", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true - }, - "gender": { - "default": 0, - "autoincrement": false, - "name": "gender", - "type": "tinyint", - "primaryKey": false, - "notNull": false - }, - "birthday": { - "autoincrement": false, - "name": "birthday", - "type": "date", - "primaryKey": false, - "notNull": false - }, - "bio": { - "autoincrement": false, - "name": "bio", - "type": "varchar(500)", - "primaryKey": false, - "notNull": false - }, - "login_count": { - "default": 0, - "autoincrement": false, - "name": "login_count", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "last_login_at": { - "autoincrement": false, - "name": "last_login_at", - "type": "datetime", - "primaryKey": false, - "notNull": false - }, - "last_login_ip": { - "autoincrement": false, - "name": "last_login_ip", - "type": "varchar(45)", - "primaryKey": false, - "notNull": false - }, - "failed_attempts": { - "default": 0, - "autoincrement": false, - "name": "failed_attempts", - "type": "int", - "primaryKey": false, - "notNull": true - }, - "locked_until": { - "autoincrement": false, - "name": "locked_until", - "type": "datetime", - "primaryKey": false, - "notNull": false - }, - "is_root": { - "default": 0, - "autoincrement": false, - "name": "is_root", - "type": "tinyint(1)", - "primaryKey": false, - "notNull": true - }, - "extra": { - "autoincrement": false, - "name": "extra", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "autoincrement": false, - "name": "created_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "created_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "updated_by": { - "autoincrement": false, - "name": "updated_by", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "default": "(CURRENT_TIMESTAMP)", - "autoincrement": false, - "name": "updated_at", - "type": "datetime", - "primaryKey": false, - "notNull": true - }, - "deleted_at": { - "autoincrement": false, - "name": "deleted_at", - "type": "datetime", - "primaryKey": false, - "notNull": false - }, - "version": { - "default": 1, - "autoincrement": false, - "name": "version", - "type": "int", - "primaryKey": false, - "notNull": true - } - }, - "compositePrimaryKeys": { - "sys_users_id": { - "name": "sys_users_id", - "columns": [ - "id" - ] - } - }, - "indexes": { - "idx_created_at": { - "name": "idx_created_at", - "columns": [ - "created_at" - ], - "isUnique": false - }, - "idx_deleted_at": { - "name": "idx_deleted_at", - "columns": [ - "deleted_at" - ], - "isUnique": false - }, - "idx_is_root": { - "name": "idx_is_root", - "columns": [ - "is_root" - ], - "isUnique": false - }, - "idx_last_login": { - "name": "idx_last_login", - "columns": [ - "last_login_at" - ], - "isUnique": false - }, - "idx_mobile": { - "name": "idx_mobile", - "columns": [ - "mobile" - ], - "isUnique": false - }, - "idx_status": { - "name": "idx_status", - "columns": [ - "status" - ], - "isUnique": false - } - }, - "foreignKeys": {}, - "uniqueConstraints": { - "uk_email": { - "name": "uk_email", - "columns": [ - "email", - "deleted_at" - ] - }, - "uk_username": { - "name": "uk_username", - "columns": [ - "username", - "deleted_at" - ] - } - }, - "checkConstraint": {} - } - }, - "views": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { + "id": "00000000-0000-0000-0000-000000000000", + "prevId": "", + "version": "5", + "dialect": "mysql", "tables": { - "sys_dict_items": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - }, - "updated_at": { - "isDefaultAnExpression": true - } + "sys_dict_items": { + "name": "sys_dict_items", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "autoincrement": false, + "name": "type_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "autoincrement": false, + "name": "item_key", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "item_value": { + "autoincrement": false, + "name": "item_value", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "label": { + "autoincrement": false, + "name": "label", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "label_en": { + "autoincrement": false, + "name": "label_en", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "css_class": { + "autoincrement": false, + "name": "css_class", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "color": { + "autoincrement": false, + "name": "color", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "extra": { + "autoincrement": false, + "name": "extra", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + } + }, + "compositePrimaryKeys": { + "sys_dict_items_id": { + "name": "sys_dict_items_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": ["deleted_at"], + "isUnique": false + }, + "idx_key": { + "name": "idx_key", + "columns": ["item_key"], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": ["pid"], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": ["type_id", "sort_order"], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": ["status"], + "isUnique": false + }, + "idx_type_id": { + "name": "idx_type_id", + "columns": ["type_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_type_key": { + "name": "uk_type_key", + "columns": ["type_id", "item_key", "deleted_at"] + } + }, + "checkConstraint": {} + }, + "sys_dict_types": { + "name": "sys_dict_types", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "code": { + "autoincrement": false, + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "default": 0, + "autoincrement": false, + "name": "is_system", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + } + }, + "compositePrimaryKeys": { + "sys_dict_types_id": { + "name": "sys_dict_types_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": ["deleted_at"], + "isUnique": false + }, + "idx_is_system": { + "name": "idx_is_system", + "columns": ["is_system"], + "isUnique": false + }, + "idx_name": { + "name": "idx_name", + "columns": ["name"], + "isUnique": false + }, + "idx_path": { + "name": "idx_path", + "columns": ["path"], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": ["pid"], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": ["pid", "sort_order"], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": ["status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_code": { + "name": "uk_code", + "columns": ["code", "deleted_at"] + } + }, + "checkConstraint": {} + }, + "sys_operation_logs": { + "name": "sys_operation_logs", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "autoincrement": false, + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "username": { + "autoincrement": false, + "name": "username", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "module": { + "autoincrement": false, + "name": "module", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "action": { + "autoincrement": false, + "name": "action", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "target": { + "autoincrement": false, + "name": "target", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "autoincrement": false, + "name": "target_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "request_data": { + "autoincrement": false, + "name": "request_data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_data": { + "autoincrement": false, + "name": "response_data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "ip": { + "autoincrement": false, + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "autoincrement": false, + "name": "user_agent", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "duration": { + "autoincrement": false, + "name": "duration", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_msg": { + "autoincrement": false, + "name": "error_msg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_operation_logs_id": { + "name": "sys_operation_logs_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": ["created_at"], + "isUnique": false + }, + "idx_ip": { + "name": "idx_ip", + "columns": ["ip"], + "isUnique": false + }, + "idx_module_action": { + "name": "idx_module_action", + "columns": ["module", "action"], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": ["status"], + "isUnique": false + }, + "idx_target": { + "name": "idx_target", + "columns": ["target_id"], + "isUnique": false + }, + "idx_user_id": { + "name": "idx_user_id", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sys_organizations": { + "name": "sys_organizations", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "code": { + "autoincrement": false, + "name": "code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "autoincrement": false, + "name": "full_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "type": { + "autoincrement": false, + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "leader_id": { + "autoincrement": false, + "name": "leader_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "address": { + "autoincrement": false, + "name": "address", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "autoincrement": false, + "name": "phone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "extra": { + "autoincrement": false, + "name": "extra", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "version": { + "default": 1, + "autoincrement": false, + "name": "version", + "type": "int", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_organizations_id": { + "name": "sys_organizations_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": ["deleted_at"], + "isUnique": false + }, + "idx_leader_id": { + "name": "idx_leader_id", + "columns": ["leader_id"], + "isUnique": false + }, + "idx_name": { + "name": "idx_name", + "columns": ["name"], + "isUnique": false + }, + "idx_path": { + "name": "idx_path", + "columns": ["path"], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": ["pid"], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": ["pid", "sort_order"], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": ["status"], + "isUnique": false + }, + "idx_type": { + "name": "idx_type", + "columns": ["type"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_code": { + "name": "uk_code", + "columns": ["code", "deleted_at"] + } + }, + "checkConstraint": {} + }, + "sys_permissions": { + "name": "sys_permissions", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "code": { + "autoincrement": false, + "name": "code", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "type": { + "autoincrement": false, + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "resource": { + "autoincrement": false, + "name": "resource", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "action": { + "autoincrement": false, + "name": "action", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "meta": { + "autoincrement": false, + "name": "meta", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + } + }, + "compositePrimaryKeys": { + "sys_permissions_id": { + "name": "sys_permissions_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": ["deleted_at"], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": ["pid"], + "isUnique": false + }, + "idx_resource_action": { + "name": "idx_resource_action", + "columns": ["resource", "action"], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": ["pid", "sort_order"], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": ["status"], + "isUnique": false + }, + "idx_type": { + "name": "idx_type", + "columns": ["type"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_code": { + "name": "uk_code", + "columns": ["code", "deleted_at"] + } + }, + "checkConstraint": {} + }, + "sys_role_permissions": { + "name": "sys_role_permissions", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "autoincrement": false, + "name": "role_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "autoincrement": false, + "name": "permission_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "is_half": { + "default": 0, + "autoincrement": false, + "name": "is_half", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_role_permissions_id": { + "name": "sys_role_permissions_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_is_half": { + "name": "idx_is_half", + "columns": ["is_half"], + "isUnique": false + }, + "idx_permission_id": { + "name": "idx_permission_id", + "columns": ["permission_id"], + "isUnique": false + }, + "idx_role_id": { + "name": "idx_role_id", + "columns": ["role_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_role_permission": { + "name": "uk_role_permission", + "columns": ["role_id", "permission_id"] + } + }, + "checkConstraint": {} + }, + "sys_roles": { + "name": "sys_roles", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "code": { + "autoincrement": false, + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pid": { + "default": 0, + "autoincrement": false, + "name": "pid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "path": { + "autoincrement": false, + "name": "path", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "level": { + "default": 1, + "autoincrement": false, + "name": "level", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "default": 0, + "autoincrement": false, + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "default": 0, + "autoincrement": false, + "name": "is_system", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "permissions_snapshot": { + "autoincrement": false, + "name": "permissions_snapshot", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "extra": { + "autoincrement": false, + "name": "extra", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "version": { + "default": 1, + "autoincrement": false, + "name": "version", + "type": "int", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_roles_id": { + "name": "sys_roles_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": ["deleted_at"], + "isUnique": false + }, + "idx_is_system": { + "name": "idx_is_system", + "columns": ["is_system"], + "isUnique": false + }, + "idx_name": { + "name": "idx_name", + "columns": ["name"], + "isUnique": false + }, + "idx_path": { + "name": "idx_path", + "columns": ["path"], + "isUnique": false + }, + "idx_pid": { + "name": "idx_pid", + "columns": ["pid"], + "isUnique": false + }, + "idx_sort": { + "name": "idx_sort", + "columns": ["pid", "sort_order"], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": ["status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_code": { + "name": "uk_code", + "columns": ["code", "deleted_at"] + } + }, + "checkConstraint": {} + }, + "sys_tags": { + "name": "sys_tags", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "autoincrement": false, + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "type": { + "default": "'user'", + "autoincrement": false, + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "color": { + "autoincrement": false, + "name": "color", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "description": { + "autoincrement": false, + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "default": 0, + "autoincrement": false, + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + } + }, + "compositePrimaryKeys": { + "sys_tags_id": { + "name": "sys_tags_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": ["deleted_at"], + "isUnique": false + }, + "idx_name": { + "name": "idx_name", + "columns": ["name"], + "isUnique": false + }, + "idx_type": { + "name": "idx_type", + "columns": ["type"], + "isUnique": false + }, + "idx_usage_count": { + "name": "idx_usage_count", + "columns": ["usage_count"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_name_type": { + "name": "uk_name_type", + "columns": ["name", "type", "deleted_at"] + } + }, + "checkConstraint": {} + }, + "sys_user_organizations": { + "name": "sys_user_organizations", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "autoincrement": false, + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "autoincrement": false, + "name": "organization_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "default": 0, + "autoincrement": false, + "name": "is_primary", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "position": { + "autoincrement": false, + "name": "position", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "joined_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_user_organizations_id": { + "name": "sys_user_organizations_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_is_primary": { + "name": "idx_is_primary", + "columns": ["is_primary"], + "isUnique": false + }, + "idx_joined_at": { + "name": "idx_joined_at", + "columns": ["joined_at"], + "isUnique": false + }, + "idx_organization_id": { + "name": "idx_organization_id", + "columns": ["organization_id"], + "isUnique": false + }, + "idx_user_id": { + "name": "idx_user_id", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_user_org": { + "name": "uk_user_org", + "columns": ["user_id", "organization_id"] + } + }, + "checkConstraint": {} + }, + "sys_user_roles": { + "name": "sys_user_roles", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "autoincrement": false, + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "autoincrement": false, + "name": "role_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expired_at": { + "autoincrement": false, + "name": "expired_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_user_roles_id": { + "name": "sys_user_roles_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": ["created_at"], + "isUnique": false + }, + "idx_expired_at": { + "name": "idx_expired_at", + "columns": ["expired_at"], + "isUnique": false + }, + "idx_role_id": { + "name": "idx_role_id", + "columns": ["role_id"], + "isUnique": false + }, + "idx_user_id": { + "name": "idx_user_id", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_user_role": { + "name": "uk_user_role", + "columns": ["user_id", "role_id"] + } + }, + "checkConstraint": {} + }, + "sys_user_tags": { + "name": "sys_user_tags", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "autoincrement": false, + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "autoincrement": false, + "name": "tag_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_user_tags_id": { + "name": "sys_user_tags_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": ["created_at"], + "isUnique": false + }, + "idx_tag_id": { + "name": "idx_tag_id", + "columns": ["tag_id"], + "isUnique": false + }, + "idx_user_id": { + "name": "idx_user_id", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_user_tag": { + "name": "uk_user_tag", + "columns": ["user_id", "tag_id"] + } + }, + "checkConstraint": {} + }, + "sys_users": { + "name": "sys_users", + "columns": { + "id": { + "autoincrement": false, + "name": "id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "username": { + "autoincrement": false, + "name": "username", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "email": { + "autoincrement": false, + "name": "email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "mobile": { + "autoincrement": false, + "name": "mobile", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "autoincrement": false, + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "autoincrement": false, + "name": "avatar", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "nickname": { + "autoincrement": false, + "name": "nickname", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "status": { + "default": "'active'", + "autoincrement": false, + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "gender": { + "default": 0, + "autoincrement": false, + "name": "gender", + "type": "tinyint", + "primaryKey": false, + "notNull": false + }, + "birthday": { + "autoincrement": false, + "name": "birthday", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "bio": { + "autoincrement": false, + "name": "bio", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "login_count": { + "default": 0, + "autoincrement": false, + "name": "login_count", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "last_login_at": { + "autoincrement": false, + "name": "last_login_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "last_login_ip": { + "autoincrement": false, + "name": "last_login_ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": false + }, + "failed_attempts": { + "default": 0, + "autoincrement": false, + "name": "failed_attempts", + "type": "int", + "primaryKey": false, + "notNull": true + }, + "locked_until": { + "autoincrement": false, + "name": "locked_until", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "is_root": { + "default": 0, + "autoincrement": false, + "name": "is_root", + "type": "tinyint(1)", + "primaryKey": false, + "notNull": true + }, + "extra": { + "autoincrement": false, + "name": "extra", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "autoincrement": false, + "name": "created_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "created_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "autoincrement": false, + "name": "updated_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "default": "(CURRENT_TIMESTAMP)", + "autoincrement": false, + "name": "updated_at", + "type": "datetime", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "autoincrement": false, + "name": "deleted_at", + "type": "datetime", + "primaryKey": false, + "notNull": false + }, + "version": { + "default": 1, + "autoincrement": false, + "name": "version", + "type": "int", + "primaryKey": false, + "notNull": true + } + }, + "compositePrimaryKeys": { + "sys_users_id": { + "name": "sys_users_id", + "columns": ["id"] + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": ["created_at"], + "isUnique": false + }, + "idx_deleted_at": { + "name": "idx_deleted_at", + "columns": ["deleted_at"], + "isUnique": false + }, + "idx_is_root": { + "name": "idx_is_root", + "columns": ["is_root"], + "isUnique": false + }, + "idx_last_login": { + "name": "idx_last_login", + "columns": ["last_login_at"], + "isUnique": false + }, + "idx_mobile": { + "name": "idx_mobile", + "columns": ["mobile"], + "isUnique": false + }, + "idx_status": { + "name": "idx_status", + "columns": ["status"], + "isUnique": false + } + }, + "foreignKeys": {}, + "uniqueConstraints": { + "uk_email": { + "name": "uk_email", + "columns": ["email", "deleted_at"] + }, + "uk_username": { + "name": "uk_username", + "columns": ["username", "deleted_at"] + } + }, + "checkConstraint": {} } - }, - "sys_dict_types": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - }, - "updated_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_operation_logs": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_organizations": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - }, - "updated_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_permissions": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - }, - "updated_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_role_permissions": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_roles": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - }, - "updated_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_tags": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_user_organizations": { - "columns": { - "joined_at": { - "isDefaultAnExpression": true - }, - "created_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_user_roles": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_user_tags": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - } - } - }, - "sys_users": { - "columns": { - "created_at": { - "isDefaultAnExpression": true - }, - "updated_at": { - "isDefaultAnExpression": true - } - } - } }, - "indexes": {} - } -} \ No newline at end of file + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": { + "sys_dict_items": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_dict_types": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_operation_logs": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_organizations": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_permissions": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_role_permissions": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_roles": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_tags": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_user_organizations": { + "columns": { + "joined_at": { + "isDefaultAnExpression": true + }, + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_user_roles": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_user_tags": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + } + } + }, + "sys_users": { + "columns": { + "created_at": { + "isDefaultAnExpression": true + }, + "updated_at": { + "isDefaultAnExpression": true + } + } + } + }, + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index cb56d78..2ccc329 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -1,13 +1,13 @@ { - "version": "7", - "dialect": "mysql", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1751620262784, - "tag": "0000_nostalgic_eternity", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1751620262784, + "tag": "0000_nostalgic_eternity", + "breakpoints": true + } + ] +} diff --git a/drizzle/relations.ts b/drizzle/relations.ts index 80768e2..34c5368 100644 --- a/drizzle/relations.ts +++ b/drizzle/relations.ts @@ -1,3 +1,2 @@ -import { relations } from "drizzle-orm/relations"; -import { } from "./schema"; - +import { relations } from 'drizzle-orm/relations'; +import {} from './schema'; diff --git a/drizzle/schema.ts b/drizzle/schema.ts index e3da6d6..4f8045f 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,316 +1,359 @@ -import { mysqlTable, mysqlSchema, AnyMySqlColumn, index, primaryKey, unique, bigint, varchar, text, int, json, datetime, tinyint, date } from "drizzle-orm/mysql-core" -import { sql } from "drizzle-orm" +import { + mysqlTable, + mysqlSchema, + AnyMySqlColumn, + index, + primaryKey, + unique, + bigint, + varchar, + int, + json, + timestamp, + text, + datetime, + tinyint, + date, +} from 'drizzle-orm/mysql-core'; +import { sql } from 'drizzle-orm'; -export const sysDictItems = mysqlTable("sys_dict_items", { - id: bigint({ mode: "number" }).notNull(), - typeId: bigint("type_id", { mode: "number" }).notNull(), - itemKey: varchar("item_key", { length: 50 }).notNull(), - itemValue: varchar("item_value", { length: 200 }).notNull(), - label: varchar({ length: 100 }).notNull(), - labelEn: varchar("label_en", { length: 200 }), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - sortOrder: int("sort_order").default(0).notNull(), - status: varchar({ length: 20 }).default('active').notNull(), - cssClass: varchar("css_class", { length: 50 }), - color: varchar({ length: 50 }), - extra: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_key").on(table.itemKey), - index("idx_pid").on(table.pid), - index("idx_sort").on(table.typeId, table.sortOrder), - index("idx_status").on(table.status), - index("idx_type_id").on(table.typeId), - primaryKey({ columns: [table.id], name: "sys_dict_items_id"}), - unique("uk_type_key").on(table.typeId, table.itemKey, table.deletedAt), -]); +export const sysDict = mysqlTable( + 'sys_dict', + { + id: bigint({ mode: 'number' }).autoincrement().notNull(), + code: varchar({ length: 50 }).notNull(), + name: varchar({ length: 100 }).notNull(), + value: varchar({ length: 200 }), + description: varchar({ length: 500 }), + icon: varchar({ length: 100 }), + pid: bigint({ mode: 'number' }), + level: int().default(1).notNull(), + sortOrder: int('sort_order').default(0).notNull(), + status: varchar({ length: 20 }).default('active').notNull(), + isSystem: tinyint('is_system').default(0).notNull(), + color: varchar({ length: 20 }), + extra: json(), + createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().onUpdateNow().notNull(), + }, + (table) => [ + index('idx_level').on(table.level), + index('idx_pid').on(table.pid), + index('idx_sort').on(table.sortOrder), + index('idx_status').on(table.status), + primaryKey({ columns: [table.id], name: 'sys_dict_id' }), + unique('uk_code').on(table.code), + ], +); -export const sysDictTypes = mysqlTable("sys_dict_types", { - id: bigint({ mode: "number" }).notNull(), - code: varchar({ length: 50 }).notNull(), - name: varchar({ length: 100 }).notNull(), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - status: varchar({ length: 20 }).default('active').notNull(), - isSystem: tinyint("is_system").default(0).notNull(), - sortOrder: int("sort_order").default(0).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_is_system").on(table.isSystem), - index("idx_name").on(table.name), - index("idx_path").on(table.path), - index("idx_pid").on(table.pid), - index("idx_sort").on(table.pid, table.sortOrder), - index("idx_status").on(table.status), - primaryKey({ columns: [table.id], name: "sys_dict_types_id"}), - unique("uk_code").on(table.code, table.deletedAt), -]); +export const sysOperationLogs = mysqlTable( + 'sys_operation_logs', + { + id: bigint({ mode: 'number' }).notNull(), + userId: bigint('user_id', { mode: 'number' }), + username: varchar({ length: 100 }), + module: varchar({ length: 50 }).notNull(), + action: varchar({ length: 50 }).notNull(), + target: varchar({ length: 200 }), + targetId: bigint('target_id', { mode: 'number' }), + requestData: text('request_data'), + responseData: text('response_data'), + status: varchar({ length: 20 }).notNull(), + ip: varchar({ length: 45 }), + userAgent: varchar('user_agent', { length: 200 }), + duration: bigint({ mode: 'number' }), + errorMsg: text('error_msg'), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_created_at').on(table.createdAt), + index('idx_ip').on(table.ip), + index('idx_module_action').on(table.module, table.action), + index('idx_status').on(table.status), + index('idx_target').on(table.targetId), + index('idx_user_id').on(table.userId), + primaryKey({ columns: [table.id], name: 'sys_operation_logs_id' }), + ], +); -export const sysOperationLogs = mysqlTable("sys_operation_logs", { - id: bigint({ mode: "number" }).notNull(), - userId: bigint("user_id", { mode: "number" }), - username: varchar({ length: 100 }), - module: varchar({ length: 50 }).notNull(), - action: varchar({ length: 50 }).notNull(), - target: varchar({ length: 200 }), - targetId: bigint("target_id", { mode: "number" }), - requestData: text("request_data"), - responseData: text("response_data"), - status: varchar({ length: 20 }).notNull(), - ip: varchar({ length: 45 }), - userAgent: varchar("user_agent", { length: 200 }), - duration: bigint({ mode: "number" }), - errorMsg: text("error_msg"), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_created_at").on(table.createdAt), - index("idx_ip").on(table.ip), - index("idx_module_action").on(table.module, table.action), - index("idx_status").on(table.status), - index("idx_target").on(table.targetId), - index("idx_user_id").on(table.userId), - primaryKey({ columns: [table.id], name: "sys_operation_logs_id"}), -]); +export const sysOrganizations = mysqlTable( + 'sys_organizations', + { + id: bigint({ mode: 'number' }).notNull(), + code: varchar({ length: 100 }).notNull(), + name: varchar({ length: 200 }).notNull(), + fullName: varchar('full_name', { length: 200 }), + description: text(), + pid: bigint({ mode: 'number' }), + path: varchar({ length: 500 }), + level: int().default(1).notNull(), + type: varchar({ length: 20 }), + status: varchar({ length: 20 }).default('active').notNull(), + sortOrder: int('sort_order').default(0).notNull(), + leaderId: bigint('leader_id', { mode: 'number' }), + address: varchar({ length: 200 }), + phone: varchar({ length: 50 }), + extra: json(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + updatedBy: bigint('updated_by', { mode: 'number' }), + updatedAt: datetime('updated_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + version: int().default(1).notNull(), + }, + (table) => [ + index('idx_deleted_at').on(table.deletedAt), + index('idx_leader_id').on(table.leaderId), + index('idx_name').on(table.name), + index('idx_path').on(table.path), + index('idx_pid').on(table.pid), + index('idx_sort').on(table.pid, table.sortOrder), + index('idx_status').on(table.status), + index('idx_type').on(table.type), + primaryKey({ columns: [table.id], name: 'sys_organizations_id' }), + unique('uk_code').on(table.code, table.deletedAt), + ], +); -export const sysOrganizations = mysqlTable("sys_organizations", { - id: bigint({ mode: "number" }).notNull(), - code: varchar({ length: 100 }).notNull(), - name: varchar({ length: 200 }).notNull(), - fullName: varchar("full_name", { length: 200 }), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - type: varchar({ length: 20 }), - status: varchar({ length: 20 }).default('active').notNull(), - sortOrder: int("sort_order").default(0).notNull(), - leaderId: bigint("leader_id", { mode: "number" }), - address: varchar({ length: 200 }), - phone: varchar({ length: 50 }), - extra: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), - version: int().default(1).notNull(), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_leader_id").on(table.leaderId), - index("idx_name").on(table.name), - index("idx_path").on(table.path), - index("idx_pid").on(table.pid), - index("idx_sort").on(table.pid, table.sortOrder), - index("idx_status").on(table.status), - index("idx_type").on(table.type), - primaryKey({ columns: [table.id], name: "sys_organizations_id"}), - unique("uk_code").on(table.code, table.deletedAt), -]); +export const sysPermissions = mysqlTable( + 'sys_permissions', + { + id: bigint({ mode: 'number' }).notNull(), + code: varchar({ length: 100 }).notNull(), + name: varchar({ length: 100 }).notNull(), + type: varchar({ length: 20 }).notNull(), + resource: varchar({ length: 50 }), + action: varchar({ length: 50 }), + description: text(), + pid: bigint({ mode: 'number' }), + path: varchar({ length: 500 }), + level: int().default(1).notNull(), + sortOrder: int('sort_order').default(0).notNull(), + status: varchar({ length: 20 }).default('active').notNull(), + meta: json(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + updatedBy: bigint('updated_by', { mode: 'number' }), + updatedAt: datetime('updated_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + }, + (table) => [ + index('idx_deleted_at').on(table.deletedAt), + index('idx_pid').on(table.pid), + index('idx_resource_action').on(table.resource, table.action), + index('idx_sort').on(table.pid, table.sortOrder), + index('idx_status').on(table.status), + index('idx_type').on(table.type), + primaryKey({ columns: [table.id], name: 'sys_permissions_id' }), + unique('uk_code').on(table.code, table.deletedAt), + ], +); -export const sysPermissions = mysqlTable("sys_permissions", { - id: bigint({ mode: "number" }).notNull(), - code: varchar({ length: 100 }).notNull(), - name: varchar({ length: 100 }).notNull(), - type: varchar({ length: 20 }).notNull(), - resource: varchar({ length: 50 }), - action: varchar({ length: 50 }), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - sortOrder: int("sort_order").default(0).notNull(), - status: varchar({ length: 20 }).default('active').notNull(), - meta: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_pid").on(table.pid), - index("idx_resource_action").on(table.resource, table.action), - index("idx_sort").on(table.pid, table.sortOrder), - index("idx_status").on(table.status), - index("idx_type").on(table.type), - primaryKey({ columns: [table.id], name: "sys_permissions_id"}), - unique("uk_code").on(table.code, table.deletedAt), -]); +export const sysRolePermissions = mysqlTable( + 'sys_role_permissions', + { + id: bigint({ mode: 'number' }).notNull(), + roleId: bigint('role_id', { mode: 'number' }).notNull(), + permissionId: bigint('permission_id', { mode: 'number' }).notNull(), + isHalf: tinyint('is_half').default(0).notNull(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_is_half').on(table.isHalf), + index('idx_permission_id').on(table.permissionId), + index('idx_role_id').on(table.roleId), + primaryKey({ columns: [table.id], name: 'sys_role_permissions_id' }), + unique('uk_role_permission').on(table.roleId, table.permissionId), + ], +); -export const sysRolePermissions = mysqlTable("sys_role_permissions", { - id: bigint({ mode: "number" }).notNull(), - roleId: bigint("role_id", { mode: "number" }).notNull(), - permissionId: bigint("permission_id", { mode: "number" }).notNull(), - isHalf: tinyint("is_half").default(0).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_is_half").on(table.isHalf), - index("idx_permission_id").on(table.permissionId), - index("idx_role_id").on(table.roleId), - primaryKey({ columns: [table.id], name: "sys_role_permissions_id"}), - unique("uk_role_permission").on(table.roleId, table.permissionId), -]); +export const sysRoles = mysqlTable( + 'sys_roles', + { + id: bigint({ mode: 'number' }).notNull(), + code: varchar({ length: 50 }).notNull(), + name: varchar({ length: 100 }).notNull(), + description: text(), + pid: bigint({ mode: 'number' }), + path: varchar({ length: 500 }), + level: int().default(1).notNull(), + sortOrder: int('sort_order').default(0).notNull(), + status: varchar({ length: 20 }).default('active').notNull(), + isSystem: tinyint('is_system').default(0).notNull(), + permissionsSnapshot: json('permissions_snapshot'), + extra: json(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + updatedBy: bigint('updated_by', { mode: 'number' }), + updatedAt: datetime('updated_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + version: int().default(1).notNull(), + }, + (table) => [ + index('idx_deleted_at').on(table.deletedAt), + index('idx_is_system').on(table.isSystem), + index('idx_name').on(table.name), + index('idx_path').on(table.path), + index('idx_pid').on(table.pid), + index('idx_sort').on(table.pid, table.sortOrder), + index('idx_status').on(table.status), + primaryKey({ columns: [table.id], name: 'sys_roles_id' }), + unique('uk_code').on(table.code, table.deletedAt), + ], +); -export const sysRoles = mysqlTable("sys_roles", { - id: bigint({ mode: "number" }).notNull(), - code: varchar({ length: 50 }).notNull(), - name: varchar({ length: 100 }).notNull(), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - sortOrder: int("sort_order").default(0).notNull(), - status: varchar({ length: 20 }).default('active').notNull(), - isSystem: tinyint("is_system").default(0).notNull(), - permissionsSnapshot: json("permissions_snapshot"), - extra: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), - version: int().default(1).notNull(), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_is_system").on(table.isSystem), - index("idx_name").on(table.name), - index("idx_path").on(table.path), - index("idx_pid").on(table.pid), - index("idx_sort").on(table.pid, table.sortOrder), - index("idx_status").on(table.status), - primaryKey({ columns: [table.id], name: "sys_roles_id"}), - unique("uk_code").on(table.code, table.deletedAt), -]); +export const sysTags = mysqlTable( + 'sys_tags', + { + id: bigint({ mode: 'number' }).notNull(), + name: varchar({ length: 50 }).notNull(), + type: varchar({ length: 50 }).default('user'), + color: varchar({ length: 50 }), + description: text(), + usageCount: int('usage_count').default(0).notNull(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + }, + (table) => [ + index('idx_deleted_at').on(table.deletedAt), + index('idx_name').on(table.name), + index('idx_type').on(table.type), + index('idx_usage_count').on(table.usageCount), + primaryKey({ columns: [table.id], name: 'sys_tags_id' }), + unique('uk_name_type').on(table.name, table.type, table.deletedAt), + ], +); -export const sysTags = mysqlTable("sys_tags", { - id: bigint({ mode: "number" }).notNull(), - name: varchar({ length: 50 }).notNull(), - type: varchar({ length: 50 }).default('user'), - color: varchar({ length: 50 }), - description: text(), - usageCount: int("usage_count").default(0).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_name").on(table.name), - index("idx_type").on(table.type), - index("idx_usage_count").on(table.usageCount), - primaryKey({ columns: [table.id], name: "sys_tags_id"}), - unique("uk_name_type").on(table.name, table.type, table.deletedAt), -]); +export const sysUserOrganizations = mysqlTable( + 'sys_user_organizations', + { + id: bigint({ mode: 'number' }).notNull(), + userId: bigint('user_id', { mode: 'number' }).notNull(), + organizationId: bigint('organization_id', { mode: 'number' }).notNull(), + isPrimary: tinyint('is_primary').default(0).notNull(), + position: varchar({ length: 100 }), + joinedAt: datetime('joined_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_is_primary').on(table.isPrimary), + index('idx_joined_at').on(table.joinedAt), + index('idx_organization_id').on(table.organizationId), + index('idx_user_id').on(table.userId), + primaryKey({ columns: [table.id], name: 'sys_user_organizations_id' }), + unique('uk_user_org').on(table.userId, table.organizationId), + ], +); -export const sysUserOrganizations = mysqlTable("sys_user_organizations", { - id: bigint({ mode: "number" }).notNull(), - userId: bigint("user_id", { mode: "number" }).notNull(), - organizationId: bigint("organization_id", { mode: "number" }).notNull(), - isPrimary: tinyint("is_primary").default(0).notNull(), - position: varchar({ length: 100 }), - joinedAt: datetime("joined_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_is_primary").on(table.isPrimary), - index("idx_joined_at").on(table.joinedAt), - index("idx_organization_id").on(table.organizationId), - index("idx_user_id").on(table.userId), - primaryKey({ columns: [table.id], name: "sys_user_organizations_id"}), - unique("uk_user_org").on(table.userId, table.organizationId), -]); +export const sysUserRoles = mysqlTable( + 'sys_user_roles', + { + id: bigint({ mode: 'number' }).notNull(), + userId: bigint('user_id', { mode: 'number' }).notNull(), + roleId: bigint('role_id', { mode: 'number' }).notNull(), + expiredAt: datetime('expired_at', { mode: 'string' }), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_created_at').on(table.createdAt), + index('idx_expired_at').on(table.expiredAt), + index('idx_role_id').on(table.roleId), + index('idx_user_id').on(table.userId), + primaryKey({ columns: [table.id], name: 'sys_user_roles_id' }), + unique('uk_user_role').on(table.userId, table.roleId), + ], +); -export const sysUserRoles = mysqlTable("sys_user_roles", { - id: bigint({ mode: "number" }).notNull(), - userId: bigint("user_id", { mode: "number" }).notNull(), - roleId: bigint("role_id", { mode: "number" }).notNull(), - expiredAt: datetime("expired_at", { mode: 'string'}), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_created_at").on(table.createdAt), - index("idx_expired_at").on(table.expiredAt), - index("idx_role_id").on(table.roleId), - index("idx_user_id").on(table.userId), - primaryKey({ columns: [table.id], name: "sys_user_roles_id"}), - unique("uk_user_role").on(table.userId, table.roleId), -]); +export const sysUserTags = mysqlTable( + 'sys_user_tags', + { + id: bigint({ mode: 'number' }).notNull(), + userId: bigint('user_id', { mode: 'number' }).notNull(), + tagId: bigint('tag_id', { mode: 'number' }).notNull(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_created_at').on(table.createdAt), + index('idx_tag_id').on(table.tagId), + index('idx_user_id').on(table.userId), + primaryKey({ columns: [table.id], name: 'sys_user_tags_id' }), + unique('uk_user_tag').on(table.userId, table.tagId), + ], +); -export const sysUserTags = mysqlTable("sys_user_tags", { - id: bigint({ mode: "number" }).notNull(), - userId: bigint("user_id", { mode: "number" }).notNull(), - tagId: bigint("tag_id", { mode: "number" }).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_created_at").on(table.createdAt), - index("idx_tag_id").on(table.tagId), - index("idx_user_id").on(table.userId), - primaryKey({ columns: [table.id], name: "sys_user_tags_id"}), - unique("uk_user_tag").on(table.userId, table.tagId), -]); - -export const sysUsers = mysqlTable("sys_users", { - id: bigint({ mode: "number" }).notNull(), - username: varchar({ length: 50 }).notNull(), - email: varchar({ length: 100 }).notNull(), - mobile: varchar({ length: 20 }), - passwordHash: varchar("password_hash", { length: 255 }).notNull(), - avatar: varchar({ length: 255 }), - nickname: varchar({ length: 100 }), - status: varchar({ length: 20 }).default('active').notNull(), - gender: tinyint().default(0), - // you can use { mode: 'date' }, if you want to have Date as type for this column - birthday: date({ mode: 'string' }), - bio: varchar({ length: 500 }), - loginCount: int("login_count").default(0).notNull(), - lastLoginAt: datetime("last_login_at", { mode: 'string'}), - lastLoginIp: varchar("last_login_ip", { length: 45 }), - failedAttempts: int("failed_attempts").default(0).notNull(), - lockedUntil: datetime("locked_until", { mode: 'string'}), - isRoot: tinyint("is_root").default(0).notNull(), - extra: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), - version: int().default(1).notNull(), -}, -(table) => [ - index("idx_created_at").on(table.createdAt), - index("idx_deleted_at").on(table.deletedAt), - index("idx_is_root").on(table.isRoot), - index("idx_last_login").on(table.lastLoginAt), - index("idx_mobile").on(table.mobile), - index("idx_status").on(table.status), - primaryKey({ columns: [table.id], name: "sys_users_id"}), - unique("uk_email").on(table.email, table.deletedAt), - unique("uk_username").on(table.username, table.deletedAt), -]); +export const sysUsers = mysqlTable( + 'sys_users', + { + id: bigint({ mode: 'number' }).notNull(), + username: varchar({ length: 50 }).notNull(), + email: varchar({ length: 100 }).notNull(), + mobile: varchar({ length: 20 }), + passwordHash: varchar('password_hash', { length: 255 }).notNull(), + avatar: varchar({ length: 255 }), + nickname: varchar({ length: 100 }), + status: varchar({ length: 20 }).default('active').notNull(), + gender: tinyint().default(0), + // you can use { mode: 'date' }, if you want to have Date as type for this column + birthday: date({ mode: 'string' }), + bio: varchar({ length: 500 }), + loginCount: int('login_count').default(0).notNull(), + lastLoginAt: datetime('last_login_at', { mode: 'string' }), + lastLoginIp: varchar('last_login_ip', { length: 45 }), + failedAttempts: int('failed_attempts').default(0).notNull(), + lockedUntil: datetime('locked_until', { mode: 'string' }), + isRoot: tinyint('is_root').default(0).notNull(), + extra: json(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + updatedBy: bigint('updated_by', { mode: 'number' }), + updatedAt: datetime('updated_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + version: int().default(1).notNull(), + }, + (table) => [ + index('idx_created_at').on(table.createdAt), + index('idx_deleted_at').on(table.deletedAt), + index('idx_is_root').on(table.isRoot), + index('idx_last_login').on(table.lastLoginAt), + index('idx_mobile').on(table.mobile), + index('idx_status').on(table.status), + primaryKey({ columns: [table.id], name: 'sys_users_id' }), + unique('uk_email').on(table.email, table.deletedAt), + unique('uk_username').on(table.username, table.deletedAt), + ], +); diff --git a/prd/M2-基础用户系统-接口设计.md b/prd/M2-基础用户系统-接口设计.md index 3dc62ec..66eff0f 100644 --- a/prd/M2-基础用户系统-接口设计.md +++ b/prd/M2-基础用户系统-接口设计.md @@ -1,3024 +1,3453 @@ -# M2 - 基础用户系统 - 详细接口设计 - -## 1. 概述 - -本文档基于M2阶段的数据库设计和产品需求,提供完整的RESTful API接口设计方案。所有接口遵循以下原则: - -- **RESTful规范**:使用标准HTTP方法和状态码 -- **统一响应格式**:所有接口返回统一的JSON格式 -- **安全性**:JWT认证、权限控制、数据验证 -- **高性能**:合理的缓存策略、分页设计 -- **版本控制**:API版本化管理 - -### 基础URL -``` -https://api.starzh.com/v1 -``` - -### 统一响应格式 -```json -{ - "code": 0, // 业务状态码,0表示成功 - "message": "success", // 提示信息 - "data": {}, // 响应数据 -} -``` - -### 统一错误码规范 -- 0: 成功 -- 400xx: 客户端错误(参数错误、验证失败等) -- 401xx: 认证错误 -- 403xx: 权限错误 -- 404xx: 资源不存在 -- 409xx: 资源冲突 -- 500xx: 服务器错误 - ---- - -## 2. 认证模块 (Authentication) - -### 2.1 用户注册 - -#### 需求描述 -允许新用户通过邮箱注册账号,需要邮箱验证激活。支持基本的用户名、密码规则校验。 - -#### 接口名称 -用户注册接口 - -#### 设计原因 -- 提供用户自主注册功能 -- 通过邮箱验证确保用户真实性 -- 防止恶意注册和垃圾账号 - -#### 请求类型 -POST - -#### 接口路径 -`/auth/register` - -#### 接口参数 -```typescript -{ - username: string; // 用户名,3-50字符,字母开头,仅包含字母数字下划线 - email: string; // 邮箱地址,需符合邮箱格式 - password: string; // 密码,8-100字符,必须包含大小写字母和数字 - confirmPassword: string; // 确认密码,需与password一致 - captcha: string; // 图形验证码 - captchaId: string; // 验证码ID -} -``` - -**参数约束原因**: -- username: 限制长度和字符类型,防止特殊字符引起的安全问题 -- password: 强密码策略,提高账户安全性 -- captcha: 防止机器人批量注册 - -#### 响应分类 -- 成功:201 Created - 注册成功,发送激活邮件 -- 失败: - - 400 Bad Request - 参数验证失败 - - 409 Conflict - 用户名或邮箱已存在 - -#### 响应格式 -```json -// 成功响应 -{ - "code": 0, - "message": "注册成功,请查收邮件激活账号", - "data": { - "userId": 1234567890, - "username": "john_doe", - "email": "john@example.com", - "status": "inactive" - } -} - -// 失败响应示例 -{ - "code": 40901, - "message": "用户名已被占用", - "data": null -} -``` - -#### 开发思路 -1. 参数验证层:使用Elysia的t验证器进行参数校验 -2. 业务逻辑层: - - 验证图形验证码 - - 检查用户名和邮箱唯一性 - - 密码使用bcrypt加密(成本因子12) - - 生成用户记录(状态为inactive) - - 生成激活token并存入Redis(24小时过期) - - 发送激活邮件(异步队列) -3. 数据访问层:使用事务确保数据一致性 -4. 日志记录:记录注册操作到sys_operation_logs - ---- - -### 2.2 用户登录 - -#### 需求描述 -支持用户名或邮箱登录,实现登录失败次数限制,防止暴力破解。 - -#### 接口名称 -用户登录接口 - -#### 设计原因 -- 提供安全的身份认证机制 -- 支持多种登录方式提升用户体验 -- 防止暴力破解攻击 - -#### 请求类型 -POST - -#### 接口路径 -`/auth/login` - -#### 接口参数 -```typescript -{ - account: string; // 用户名或邮箱 - password: string; // 密码 - captcha?: string; // 验证码(连续失败3次后必填) - captchaId?: string; // 验证码ID - rememberMe?: boolean; // 记住我,默认false -} -``` - -#### 响应分类 -- 成功:200 OK - 登录成功,返回token -- 失败: - - 401 Unauthorized - 用户名或密码错误 - - 423 Locked - 账号被锁定 - -#### 响应格式 -```json -// 成功响应 -{ - "code": 0, - "message": "登录成功", - "data": { - "token": "eyJhbGciOiJIUzI1NiIs...", - "refreshToken": "xxxx-xxxx-xxxx", - "expiresIn": 7200, - "user": { - "id": 1234567890, - "username": "john_doe", - "nickname": "John", - "avatar": "https://...", - "roles": ["user"] - } - } -} -``` - -#### 开发思路 -1. 查询用户(支持username或email) -2. 检查账号状态和锁定状态 -3. 验证密码 -4. 处理登录失败: - - 增加失败次数 - - 超过5次锁定30分钟 - - 3次失败后要求验证码 -5. 登录成功: - - 清除失败次数 - - 生成JWT token - - 更新最后登录信息 - - 记录登录日志 - ---- - -### 2.3 刷新Token - -#### 需求描述 -使用refreshToken换取新的accessToken,实现无感续期。 - -#### 接口名称 -Token刷新接口 - -#### 设计原因 -- 短期accessToken + 长期refreshToken模式 -- 提高安全性,减少token泄露风险 -- 实现用户无感续期体验 - -#### 请求类型 -POST - -#### 接口路径 -`/auth/refresh` - -#### 接口参数 -```typescript -{ - refreshToken: string; // 刷新令牌 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "刷新成功", - "data": { - "token": "new-access-token", - "expiresIn": 7200 - } -} -``` - -#### 开发思路 -1. 验证refreshToken有效性 -2. 检查用户状态 -3. 生成新的accessToken -4. 可选:轮转refreshToken -5. 记录刷新日志 - ---- - -### 2.4 账号激活 - -#### 需求描述 -用户通过邮件中的链接或验证码激活账号。 - -#### 接口名称 -账号激活接口 - -#### 设计原因 -- 验证邮箱真实性 -- 防止垃圾注册 -- 提高用户质量 - -#### 请求类型 -POST - -#### 接口路径 -`/auth/activate` - -#### 接口参数 -```typescript -{ - token: string; // 激活令牌(从邮件链接获取) - // 或 - email: string; // 邮箱 - code: string; // 6位数字激活码 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "账号激活成功", - "data": { - "userId": 1234567890, - "username": "john_doe", - "status": "active" - } -} -``` - -#### 开发思路 -1. 验证激活token或激活码 -2. 检查是否过期 -3. 更新用户状态为active -4. 删除Redis中的激活信息 -5. 发送欢迎邮件 -6. 记录激活日志 - ---- - -### 2.5 退出登录 - -#### 需求描述 -用户主动退出登录,清除token。 - -#### 接口名称 -退出登录接口 - -#### 设计原因 -- 用户主动结束会话 -- 清除服务端缓存 -- 增强安全性 - -#### 请求类型 -POST - -#### 接口路径 -`/auth/logout` - -#### 请求头 -``` -Authorization: Bearer -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "退出成功", - "data": null -} -``` - -#### 开发思路 -1. 获取当前token -2. 加入token黑名单(Redis) -3. 清除用户相关缓存 -4. 记录退出日志 - ---- - -## 3. 用户管理模块 (User Management) - -### 3.1 获取当前用户信息 - -#### 需求描述 -获取当前登录用户的详细信息,包括基本信息、角色、权限、组织等。 - -#### 接口名称 -获取当前用户信息接口 - -#### 设计原因 -- 前端需要用户信息进行界面渲染 -- 权限控制需要用户角色和权限信息 -- 统一的用户信息获取入口 - -#### 请求类型 -GET - -#### 接口路径 -`/users/me` - -#### 请求头 -``` -Authorization: Bearer -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": { - "id": 1234567890, - "username": "john_doe", - "email": "john@example.com", - "mobile": "13800138000", - "nickname": "John", - "avatar": "https://...", - "gender": 1, - "birthday": "1990-01-01", - "bio": "Hello world", - "status": "active", - "lastLoginAt": "2024-01-01T12:00:00Z", - "roles": [ - { - "id": 1, - "code": "admin", - "name": "管理员" - } - ], - "permissions": [ - { - "code": "user:read", - "name": "查看用户", - "type": "api" - } - ], - "organizations": [ - { - "id": 1, - "name": "技术部", - "isPrimary": true, - "position": "工程师" - } - ], - "tags": [ - { - "id": 1, - "name": "VIP", - "color": "#ff4d4f" - } - ] - } -} -``` - -#### 开发思路 -1. 从JWT token中获取用户ID -2. 查询用户基本信息 -3. 并行查询关联信息: - - 用户角色(包含权限快照) - - 用户组织 - - 用户标签 -4. 整合权限信息(角色权限去重) -5. 使用Redis缓存(5分钟过期) - ---- - -### 3.2 用户列表查询 - -#### 需求描述 -分页查询用户列表,支持多条件筛选和排序。 - -#### 接口名称 -用户列表查询接口 - -#### 设计原因 -- 管理员需要查看和管理所有用户 -- 支持灵活的查询条件满足不同场景 -- 分页设计避免大数据量查询 - -#### 请求类型 -GET - -#### 接口路径 -`/users` - -#### 接口参数 -```typescript -// Query Parameters -{ - page?: number; // 页码,默认1 - pageSize?: number; // 每页数量,默认20,最大100 - keyword?: string; // 搜索关键词(用户名/邮箱/手机号/昵称) - status?: string; // 用户状态 - roleId?: number; // 角色ID - organizationId?: number; // 组织ID - tagId?: number; // 标签ID - startDate?: string; // 注册开始时间 - endDate?: string; // 注册结束时间 - sortBy?: string; // 排序字段:createdAt,lastLoginAt - sortOrder?: string; // 排序方向:asc,desc -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": { - "items": [ - { - "id": 1234567890, - "username": "john_doe", - "email": "john@example.com", - "mobile": "138****8000", - "nickname": "John", - "avatar": "https://...", - "status": "active", - "roles": ["admin", "user"], - "organizations": ["技术部"], - "tags": ["VIP"], - "loginCount": 100, - "lastLoginAt": "2024-01-01T12:00:00Z", - "createdAt": "2023-01-01T00:00:00Z" - } - ], - "pagination": { - "page": 1, - "pageSize": 20, - "total": 100, - "totalPages": 5 - } - } -} -``` - -#### 开发思路 -1. 参数验证和权限检查(需要user:read权限) -2. 构建查询条件: - - 关键词模糊查询 - - 状态精确匹配 - - 关联表过滤(角色、组织、标签) - - 时间范围查询 -3. 执行分页查询(使用索引优化) -4. 批量查询关联信息(减少N+1查询) -5. 数据脱敏(手机号中间4位) - ---- - -### 3.3 创建用户 - -#### 需求描述 -管理员创建新用户,可直接设置角色、组织等信息。 - -#### 接口名称 -创建用户接口 - -#### 设计原因 -- 管理员需要批量创建用户 -- 可预设用户角色和组织 -- 支持直接激活状态 - -#### 请求类型 -POST - -#### 接口路径 -`/users` - -#### 接口参数 -```typescript -{ - username: string; - email: string; - password: string; - mobile?: string; - nickname?: string; - status?: string; // 默认active - gender?: number; - birthday?: string; - bio?: string; - roleIds?: number[]; // 角色ID数组 - organizationIds?: Array<{ - id: number; - isPrimary: boolean; - position?: string; - }>; // 组织信息 - tagIds?: number[]; // 标签ID数组 - sendNotification?: boolean; // 是否发送通知邮件 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "创建成功", - "data": { - "id": 1234567890, - "username": "new_user", - "email": "new@example.com", - "status": "active" - } -} -``` - -#### 开发思路 -1. 权限检查(需要user:create权限) -2. 参数验证(唯一性检查) -3. 使用事务处理: - - 创建用户记录 - - 分配角色 - - 分配组织 - - 分配标签 -4. 发送通知邮件(可选) -5. 记录操作日志 - ---- - -### 3.4 更新用户信息 - -#### 需求描述 -更新用户基本信息和关联信息。 - -#### 接口名称 -更新用户信息接口 - -#### 设计原因 -- 支持用户自主修改个人信息 -- 管理员可修改用户所有信息 -- 分离基本信息和关联信息更新 - -#### 请求类型 -PUT - -#### 接口路径 -`/users/{userId}` - -#### 接口参数 -```typescript -{ - email?: string; - mobile?: string; - nickname?: string; - avatar?: string; - gender?: number; - birthday?: string; - bio?: string; - // 以下字段需要管理员权限 - status?: string; - roleIds?: number[]; - organizationIds?: Array<{ - id: number; - isPrimary: boolean; - position?: string; - }>; - tagIds?: number[]; -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "更新成功", - "data": { - "id": 1234567890, - "username": "john_doe", - "email": "john@example.com", - "updatedAt": "2024-01-01T12:00:00Z" - } -} -``` - -#### 开发思路 -1. 权限检查: - - 用户可修改自己的基本信息 - - 管理员可修改所有信息 - - root用户信息限制修改 -2. 参数验证(邮箱唯一性等) -3. 使用乐观锁防止并发冲突 -4. 更新关联信息时先删后增 -5. 清除相关缓存 - ---- - -### 3.5 修改密码 - -#### 需求描述 -用户修改自己的密码,需要验证原密码。 - -#### 接口名称 -修改密码接口 - -#### 设计原因 -- 用户自主管理密码 -- 增强账户安全性 -- 防止未授权修改 - -#### 请求类型 -PUT - -#### 接口路径 -`/users/me/password` - -#### 接口参数 -```typescript -{ - oldPassword: string; // 原密码 - newPassword: string; // 新密码 - confirmPassword: string; // 确认新密码 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "密码修改成功", - "data": null -} -``` - -#### 开发思路 -1. 验证原密码正确性 -2. 验证新密码符合规则 -3. 更新密码(bcrypt加密) -4. 清除所有该用户的token -5. 发送密码修改通知邮件 - ---- - -### 3.6 重置密码 - -#### 需求描述 -用户忘记密码时通过邮箱重置。 - -#### 接口名称 -申请重置密码接口 - -#### 设计原因 -- 用户忘记密码的补救措施 -- 通过邮箱验证身份 -- 防止恶意重置 - -#### 请求类型 -POST - -#### 接口路径 -`/users/password/reset-request` - -#### 接口参数 -```typescript -{ - email: string; - captcha: string; - captchaId: string; -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "重置邮件已发送", - "data": null -} -``` - -#### 开发思路 -1. 验证邮箱存在 -2. 验证图形验证码 -3. 生成重置token(6位数字或链接) -4. 存入Redis(30分钟过期) -5. 发送重置邮件 -6. 限制发送频率(1分钟内不可重复) - ---- - -### 3.7 确认重置密码 - -#### 需求描述 -使用重置token设置新密码。 - -#### 接口名称 -确认重置密码接口 - -#### 请求类型 -POST - -#### 接口路径 -`/users/password/reset-confirm` - -#### 接口参数 -```typescript -{ - token: string; // 重置token - email: string; // 邮箱 - newPassword: string; // 新密码 - confirmPassword: string; // 确认密码 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "密码重置成功", - "data": null -} -``` - -#### 开发思路 -1. 验证token有效性和匹配性 -2. 验证新密码规则 -3. 更新用户密码 -4. 删除重置token -5. 清除用户所有token -6. 发送重置成功通知 - ---- - -### 3.8 删除用户 - -#### 需求描述 -软删除用户,保留数据用于审计。 - -#### 接口名称 -删除用户接口 - -#### 设计原因 -- 满足用户注销需求 -- 保留数据用于审计 -- 支持误删恢复 - -#### 请求类型 -DELETE - -#### 接口路径 -`/users/{userId}` - -#### 响应格式 -```json -{ - "code": 0, - "message": "删除成功", - "data": null -} -``` - -#### 开发思路 -1. 权限检查(需要user:delete权限) -2. 检查是否为root用户(禁止删除) -3. 检查是否为自己(需要二次确认) -4. 更新deleted_at字段 -5. 清除用户所有会话 -6. 记录删除操作日志 - ---- - -### 3.9 批量操作用户 - -#### 需求描述 -批量启用、禁用、删除用户。 - -#### 接口名称 -批量操作用户接口 - -#### 设计原因 -- 提高管理效率 -- 支持批量处理 -- 减少重复操作 - -#### 请求类型 -POST - -#### 接口路径 -`/users/batch` - -#### 接口参数 -```typescript -{ - userIds: number[]; // 用户ID数组 - action: string; // 操作类型:enable,disable,delete -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "操作成功", - "data": { - "success": 10, - "failed": 0, - "errors": [] - } -} -``` - -#### 开发思路 -1. 权限检查 -2. 过滤掉root用户 -3. 批量执行操作 -4. 记录操作结果 -5. 返回执行统计 - ---- - -## 4. 角色管理模块 (Role Management) - -### 4.1 角色树查询 - -#### 需求描述 -查询角色树形结构,支持权限继承展示。 - -#### 接口名称 -角色树查询接口 - -#### 设计原因 -- 角色具有层级关系需要树形展示 -- 支持权限继承机制 -- 便于理解角色体系 - -#### 请求类型 -GET - -#### 接口路径 -`/roles/tree` - -#### 接口参数 -```typescript -{ - status?: string; // 过滤状态 - withPermissions?: boolean; // 是否包含权限信息 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "super_admin", - "name": "超级管理员", - "description": "拥有所有权限", - "status": "active", - "isSystem": true, - "permissionCount": 50, - "children": [ - { - "id": 2, - "code": "admin", - "name": "管理员", - "pid": 1, - "children": [] - } - ] - } - ] -} -``` - -#### 开发思路 -1. 查询所有角色(使用path字段优化) -2. 构建树形结构(递归或迭代) -3. 统计每个角色的权限数量 -4. 使用Redis缓存(角色变更时失效) - ---- - -### 4.2 创建角色 - -#### 需求描述 -创建新角色并分配权限,支持角色继承。 - -#### 接口名称 -创建角色接口 - -#### 设计原因 -- 灵活的角色管理支持业务扩展 -- 权限继承减少重复配置 -- 下级角色权限不能超过上级 - -#### 请求类型 -POST - -#### 接口路径 -`/roles` - -#### 接口参数 -```typescript -{ - code: string; // 角色代码,唯一 - name: string; // 角色名称 - description?: string; // 角色描述 - pid?: number; // 父角色ID - status?: string; // 状态,默认active - permissionIds: number[]; // 权限ID数组 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "创建成功", - "data": { - "id": 10, - "code": "editor", - "name": "编辑员", - "path": "/1/2/10/", - "level": 3 - } -} -``` - -#### 开发思路 -1. 权限检查(需要role:create权限) -2. 验证角色代码唯一性 -3. 验证父角色存在性 -4. 检查权限分配合法性: - - 获取父角色所有权限 - - 确保新角色权限是父角色权限的子集 -5. 使用事务创建: - - 创建角色记录 - - 计算并设置path和level - - 分配权限 - - 生成权限快照 -6. 清除角色缓存 - ---- - -### 4.3 更新角色信息 - -#### 需求描述 -更新角色基本信息,不包括权限。 - -#### 接口名称 -更新角色信息接口 - -#### 设计原因 -- 角色信息可能需要调整 -- 分离基本信息和权限更新 -- 防止误操作 - -#### 请求类型 -PUT - -#### 接口路径 -`/roles/{roleId}` - -#### 接口参数 -```typescript -{ - name?: string; - description?: string; - status?: string; - sortOrder?: number; -} -``` - -#### 开发思路 -1. 权限检查(需要role:update权限) -2. 检查是否为系统角色(限制修改) -3. 更新角色信息 -4. 清除角色缓存 - ---- - -### 4.4 更新角色权限 - -#### 需求描述 -动态调整角色权限,支持批量操作。 - -#### 接口名称 -更新角色权限接口 - -#### 设计原因 -- 业务变化需要调整权限 -- 支持灵活的权限管理 -- 保证权限继承一致性 - -#### 请求类型 -PUT - -#### 接口路径 -`/roles/{roleId}/permissions` - -#### 接口参数 -```typescript -{ - permissionIds: number[]; // 新的权限ID数组(全量) -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "权限更新成功", - "data": { - "roleId": 10, - "permissionCount": 20, - "addedCount": 5, - "removedCount": 3 - } -} -``` - -#### 开发思路 -1. 检查角色是否为系统角色 -2. 验证权限合法性(不超过父角色) -3. 对比新旧权限,计算差异 -4. 使用事务更新: - - 删除旧权限关联 - - 创建新权限关联 - - 更新权限快照 -5. 级联更新子角色(移除超出的权限) -6. 清除相关用户的权限缓存 - ---- - -### 4.5 删除角色 - -#### 需求描述 -删除角色,处理关联关系。 - -#### 接口名称 -删除角色接口 - -#### 设计原因 -- 清理无用角色 -- 保持系统整洁 -- 防止权限泄露 - -#### 请求类型 -DELETE - -#### 接口路径 -`/roles/{roleId}` - -#### 开发思路 -1. 权限检查(需要role:delete权限) -2. 检查是否为系统角色(禁止删除) -3. 检查是否有用户使用该角色 -4. 检查是否有子角色 -5. 软删除角色 -6. 清除相关缓存 - ---- - -### 4.6 角色用户查询 - -#### 需求描述 -查询某个角色下的所有用户。 - -#### 接口名称 -角色用户查询接口 - -#### 设计原因 -- 了解角色使用情况 -- 便于角色管理 -- 支持用户迁移 - -#### 请求类型 -GET - -#### 接口路径 -`/roles/{roleId}/users` - -#### 接口参数 -```typescript -{ - page?: number; - pageSize?: number; - keyword?: string; -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": { - "items": [ - { - "id": 123, - "username": "john_doe", - "email": "john@example.com", - "assignedAt": "2024-01-01T00:00:00Z" - } - ], - "pagination": { - "page": 1, - "pageSize": 20, - "total": 50 - } - } -} -``` - ---- - -### 4.7 角色排序接口 - -#### 需求描述 -支持手动调整角色的显示顺序,保证同级角色按指定顺序展示。 - -#### 接口名称 -角色排序接口 - -#### 设计原因 -- 角色列表需要按重要性排序 -- 下拉选择时按业务优先级展示 -- 管理员需要控制显示顺序 - -#### 请求类型 -PUT - -#### 接口路径 -`/roles/sort` - -#### 接口参数 -```typescript -{ - items: Array<{ - id: number; // 角色ID - sortOrder: number; // 新的排序号 - }> -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "排序更新成功", - "data": { - "updated": 5 - } -} -``` - -#### 开发思路 -1. 权限检查(需要role:sort权限) -2. 批量更新sortOrder字段 -3. 确保同级别角色排序唯一性 -4. 清除角色缓存 -5. 返回更新结果 - ---- - -### 4.8 拖拽排序接口 - -#### 需求描述 -支持前端拖拽方式调整角色顺序,更直观的操作方式。 - -#### 接口名称 -拖拽排序接口 - -#### 设计原因 -- 提供更好的用户体验 -- 直观的排序操作 -- 减少误操作 - -#### 请求类型 -PUT - -#### 接口路径 -`/roles/{roleId}/move` - -#### 接口参数 -```typescript -{ - targetIndex: number; // 目标位置索引(0开始) - pid?: number; // 父级ID,如果跨级移动 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "移动成功", - "data": { - "roleId": 10, - "oldIndex": 2, - "newIndex": 0, - "affectedRoles": [8, 9, 10] - } -} -``` - -#### 开发思路 -1. 计算目标位置的sortOrder值 -2. 更新被移动角色的sortOrder -3. 调整其他角色的sortOrder(避免冲突) -4. 如果跨级移动,同时更新pid -5. 清除相关缓存 - ---- - -## 5. 权限管理模块 (Permission Management) - -### 5.1 权限树查询 - -#### 需求描述 -查询权限树形结构,按类型分组展示。 - -#### 接口名称 -权限树查询接口 - -#### 设计原因 -- 权限具有层级关系(菜单-页面-按钮) -- 便于权限分配时的选择 -- 支持不同类型权限的管理 - -#### 请求类型 -GET - -#### 接口路径 -`/permissions/tree` - -#### 接口参数 -```typescript -{ - type?: string; // 权限类型:menu,button,api,data - status?: string; // 状态过滤 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "system", - "name": "系统管理", - "type": "menu", - "children": [ - { - "id": 2, - "code": "system:user", - "name": "用户管理", - "type": "menu", - "children": [ - { - "id": 3, - "code": "user:read", - "name": "查看用户", - "type": "api", - "resource": "user", - "action": "read" - } - ] - } - ] - } - ] -} -``` - -#### 开发思路 -1. 查询权限列表 -2. 按类型分组 -3. 构建树形结构 -4. 使用Redis缓存 - ---- - -### 5.2 创建权限 - -#### 需求描述 -创建新的权限节点。 - -#### 接口名称 -创建权限接口 - -#### 设计原因 -- 新功能需要新权限 -- 灵活的权限扩展 -- 支持细粒度控制 - -#### 请求类型 -POST - -#### 接口路径 -`/permissions` - -#### 接口参数 -```typescript -{ - code: string; // 权限代码 - name: string; // 权限名称 - type: string; // 权限类型 - resource?: string; // 资源标识 - action?: string; // 操作标识 - pid?: number; // 父权限ID - description?: string; // 描述 - meta?: object; // 元数据(如图标、路由) -} -``` - -#### 开发思路 -1. 权限检查(需要permission:create权限) -2. 验证权限代码唯一性 -3. 验证父权限存在性 -4. 创建权限记录 -5. 清除权限缓存 - ---- - -### 5.3 批量创建权限 - -#### 需求描述 -支持批量导入权限配置。 - -#### 接口名称 -批量创建权限接口 - -#### 设计原因 -- 新模块上线需要批量添加权限 -- 提高配置效率 -- 支持权限模板导入 - -#### 请求类型 -POST - -#### 接口路径 -`/permissions/batch` - -#### 接口参数 -```typescript -{ - permissions: Array<{ - code: string; - name: string; - type: string; - resource?: string; - action?: string; - pid?: number; - description?: string; - }> -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "批量创建成功", - "data": { - "success": 10, - "failed": 0, - "errors": [] - } -} -``` - -#### 开发思路 -1. 批量验证权限代码唯一性 -2. 验证父权限存在性 -3. 使用事务批量插入 -4. 返回执行结果统计 -5. 清除权限缓存 - ---- - -### 5.4 更新权限 - -#### 需求描述 -更新权限信息。 - -#### 接口名称 -更新权限接口 - -#### 设计原因 -- 权限名称或描述可能需要调整 -- 元数据更新(如菜单图标) -- 状态变更 - -#### 请求类型 -PUT - -#### 接口路径 -`/permissions/{permissionId}` - -#### 接口参数 -```typescript -{ - name?: string; - description?: string; - status?: string; - meta?: object; - sortOrder?: number; -} -``` - -#### 开发思路 -1. 权限检查 -2. 更新权限信息 -3. 清除权限缓存 -4. 通知相关角色缓存更新 - ---- - -### 5.5 删除权限 - -#### 需求描述 -删除权限节点及其子权限。 - -#### 接口名称 -删除权限接口 - -#### 设计原因 -- 功能下线需要删除权限 -- 保持权限体系整洁 -- 防止权限泄露 - -#### 请求类型 -DELETE - -#### 接口路径 -`/permissions/{permissionId}` - -#### 开发思路 -1. 权限检查(需要permission:delete权限) -2. 检查是否有角色使用该权限 -3. 递归删除子权限 -4. 清除相关缓存 - ---- - -### 5.6 权限检查接口 - -#### 需求描述 -检查当前用户是否拥有特定权限。 - -#### 接口名称 -权限检查接口 - -#### 设计原因 -- 前端动态控制界面元素 -- 细粒度的功能控制 -- 实时权限验证 - -#### 请求类型 -POST - -#### 接口路径 -`/permissions/check` - -#### 接口参数 -```typescript -{ - permissions: string[]; // 权限代码数组 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": { - "user:read": true, - "user:create": true, - "user:delete": false - } -} -``` - -#### 开发思路 -1. 获取当前用户所有权限 -2. 批量检查请求的权限 -3. 返回权限检查结果 - ---- - -### 5.7 权限排序接口 - -#### 需求描述 -调整权限的显示顺序,特别是菜单权限的排序。 - -#### 接口名称 -权限排序接口 - -#### 设计原因 -- 菜单权限需要控制显示顺序 -- 影响前端菜单渲染顺序 -- 提升用户体验 - -#### 请求类型 -PUT - -#### 接口路径 -`/permissions/sort` - -#### 接口参数 -```typescript -{ - items: Array<{ - id: number; - sortOrder: number; - }> -} -``` - -#### 开发思路 -1. 权限检查(需要permission:sort权限) -2. 批量更新权限排序 -3. 确保同级权限排序唯一 -4. 清除权限缓存 -5. 通知前端刷新菜单 - ---- - -## 6. 组织架构模块 (Organization Management) - -### 6.1 组织树查询 - -#### 需求描述 -查询组织架构树,支持人员统计。 - -#### 接口名称 -组织架构树查询接口 - -#### 设计原因 -- 直观展示组织层级关系 -- 便于用户分配和管理 -- 支持组织人员统计 - -#### 请求类型 -GET - -#### 接口路径 -`/organizations/tree` - -#### 接口参数 -```typescript -{ - type?: string; // 组织类型 - status?: string; // 状态 - withStats?: boolean; // 是否包含统计信息 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "ROOT", - "name": "星撰集团", - "type": "company", - "userCount": 100, - "children": [ - { - "id": 2, - "code": "TECH", - "name": "技术部", - "type": "department", - "userCount": 30, - "leader": { - "id": 123, - "name": "张三" - }, - "children": [] - } - ] - } - ] -} -``` - -#### 开发思路 -1. 查询组织列表 -2. 构建树形结构 -3. 如需统计: - - 批量查询用户组织关联 - - 聚合统计各组织人数 -4. 查询负责人信息 -5. 使用Redis缓存 - ---- - -### 6.2 创建组织 - -#### 需求描述 -创建新的组织节点。 - -#### 接口名称 -创建组织接口 - -#### 设计原因 -- 组织架构需要动态调整 -- 支持业务扩展 -- 灵活的组织管理 - -#### 请求类型 -POST - -#### 接口路径 -`/organizations` - -#### 接口参数 -```typescript -{ - code: string; // 组织代码 - name: string; // 组织名称 - fullName?: string; // 组织全称 - type: string; // 组织类型 - pid?: number; // 父组织ID - leaderId?: number; // 负责人ID - address?: string; // 地址 - phone?: string; // 电话 - description?: string; // 描述 -} -``` - -#### 开发思路 -1. 权限检查(需要organization:create权限) -2. 验证组织代码唯一性 -3. 验证父组织存在性 -4. 创建组织记录 -5. 计算path和level -6. 清除组织缓存 - ---- - -### 6.3 调整组织架构 - -#### 需求描述 -支持拖拽调整组织层级关系。 - -#### 接口名称 -调整组织架构接口 - -#### 设计原因 -- 组织架构会随业务发展调整 -- 支持灵活的组织变更 -- 保持数据完整性 - -#### 请求类型 -PUT - -#### 接口路径 -`/organizations/{organizationId}/move` - -#### 接口参数 -```typescript -{ - targetPid: number; // 目标父组织ID - targetIndex?: number; // 目标位置索引 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "调整成功", - "data": { - "id": 10, - "oldPath": "/1/2/10/", - "newPath": "/1/3/10/", - "affectedCount": 5 - } -} -``` - -#### 开发思路 -1. 验证目标组织存在性 -2. 检查是否会形成循环 -3. 使用事务更新: - - 更新pid - - 重新计算path和level - - 更新所有子组织的path - - 调整排序 -4. 记录组织变更日志 -5. 清除相关缓存 - ---- - -### 6.4 组织人员管理 - -#### 需求描述 -管理组织内的人员分配。 - -#### 接口名称 -组织人员分配接口 - -#### 设计原因 -- 灵活的人员调动 -- 支持批量操作 -- 记录调动历史 - -#### 请求类型 -POST - -#### 接口路径 -`/organizations/{organizationId}/users` - -#### 接口参数 -```typescript -{ - action: string; // 操作:add,remove,move - userIds: number[]; // 用户ID数组 - position?: string; // 职位(添加时使用) - isPrimary?: boolean; // 是否主组织 - targetOrgId?: number; // 目标组织ID(移动时使用) -} -``` - -#### 开发思路 -1. 权限检查 -2. 验证用户和组织存在性 -3. 执行批量操作: - - add: 添加用户到组织 - - remove: 从组织移除用户 - - move: 移动到其他组织 -4. 记录人员变动日志 - ---- - -### 6.5 删除组织 - -#### 需求描述 -删除组织节点,处理人员安置。 - -#### 接口名称 -删除组织接口 - -#### 设计原因 -- 组织调整需要删除节点 -- 确保人员妥善安置 -- 保持数据完整性 - -#### 请求类型 -DELETE - -#### 接口路径 -`/organizations/{organizationId}` - -#### 接口参数 -```typescript -{ - handleUsers?: string; // 人员处理方式:moveToParent,remove - targetOrgId?: number; // 目标组织ID(自定义迁移时使用) -} -``` - -#### 开发思路 -1. 权限检查(需要organization:delete权限) -2. 检查是否有子组织 -3. 处理组织内人员 -4. 软删除组织 -5. 清除相关缓存 - ---- - -### 6.6 组织架构排序接口 - -#### 需求描述 -调整同级组织的显示顺序。 - -#### 接口名称 -组织排序接口 - -#### 设计原因 -- 组织架构显示需要符合实际层级 -- 重要部门需要前置显示 -- 便于组织管理 - -#### 请求类型 -PUT - -#### 接口路径 -`/organizations/{pid}/sort` - -#### 接口参数 -```typescript -{ - items: Array<{ - id: number; - sortOrder: number; - }> -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "组织排序更新成功", - "data": { - "parentId": 1, - "updated": 3 - } -} -``` - -#### 开发思路 -1. 验证所有组织都属于同一父级 -2. 批量更新sortOrder -3. 清除组织架构缓存 -4. 返回更新结果 - ---- - -## 7. 字典管理模块 (Dictionary Management) - -### 7.1 字典类型树查询 - -#### 需求描述 -查询字典类型的树形结构,支持分类管理。 - -#### 接口名称 -字典类型树查询接口 - -#### 设计原因 -- 字典类型支持分类管理 -- 便于字典的组织和查找 -- 区分系统字典和业务字典 - -#### 请求类型 -GET - -#### 接口路径 -`/dictionaries/types/tree` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "code": "system", - "name": "系统字典", - "isSystem": true, - "children": [ - { - "id": 10, - "code": "user_status", - "name": "用户状态", - "itemCount": 4 - } - ] - } - ] -} -``` - -#### 开发思路 -1. 查询所有字典类型 -2. 构建树形结构 -3. 统计每个类型的字典项数量 -4. 使用Redis缓存 - ---- - -### 7.2 获取字典项 - -#### 需求描述 -根据字典类型获取所有字典项,支持树形字典。 - -#### 接口名称 -获取字典项接口 - -#### 设计原因 -- 前端下拉框等组件需要字典数据 -- 支持动态配置 -- 减少硬编码 - -#### 请求类型 -GET - -#### 接口路径 -`/dictionaries/items` - -#### 接口参数 -```typescript -{ - typeCode: string; // 字典类型代码 - tree?: boolean; // 是否返回树形结构 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": [ - { - "key": "active", - "value": "active", - "label": "正常", - "labelEn": "Active", - "color": "#52c41a", - "sortOrder": 1, - "extra": {} - } - ] -} -``` - -#### 开发思路 -1. 根据typeCode查询字典类型 -2. 查询该类型下的所有字典项 -3. 如果是树形字典,构建树结构 -4. 使用Redis缓存(字典变更少) - ---- - -### 7.3 创建字典类型 - -#### 需求描述 -创建新的字典类型。 - -#### 接口名称 -创建字典类型接口 - -#### 设计原因 -- 业务扩展需要新字典 -- 灵活的配置管理 -- 支持分类组织 - -#### 请求类型 -POST - -#### 接口路径 -`/dictionaries/types` - -#### 接口参数 -```typescript -{ - code: string; // 字典类型代码 - name: string; // 字典类型名称 - pid?: number; // 父类型ID - description?: string; // 描述 - sortOrder?: number; // 排序号 -} -``` - -#### 开发思路 -1. 权限检查(需要dictionary:create权限) -2. 验证代码唯一性 -3. 验证父权限存在性 -4. 创建字典类型 -5. 清除字典缓存 - ---- - -### 7.4 批量更新字典项 - -#### 需求描述 -支持批量更新字典项,保持顺序。 - -#### 接口名称 -批量更新字典项接口 - -#### 设计原因 -- 字典项通常需要批量维护 -- 保持显示顺序的一致性 -- 提高维护效率 - -#### 请求类型 -PUT - -#### 接口路径 -`/dictionaries/types/{typeId}/items/batch` - -#### 接口参数 -```typescript -{ - items: Array<{ - id?: number; // 有ID为更新,无ID为新增 - key: string; - value: string; - label: string; - labelEn?: string; - pid?: number; - sortOrder: number; - status?: string; - color?: string; - extra?: object; - }> -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "批量更新成功", - "data": { - "created": 3, - "updated": 5, - "deleted": 2 - } -} -``` - -#### 开发思路 -1. 验证字典类型存在 -2. 检查是否为系统字典 -3. 验证key在类型内唯一 -4. 使用事务处理: - - 标记删除不在列表中的项 - - 更新已存在的项 - - 创建新项 -5. 清除字典缓存 - ---- - -### 7.5 导出字典数据 - -#### 需求描述 -导出字典配置用于备份或迁移。 - -#### 接口名称 -导出字典数据接口 - -#### 设计原因 -- 配置备份需求 -- 环境间迁移 -- 批量导入导出 - -#### 请求类型 -GET - -#### 接口路径 -`/dictionaries/export` - -#### 接口参数 -```typescript -{ - typeIds?: number[]; // 指定类型ID,不传则导出全部 - format?: string; // 导出格式:json,excel -} -``` - -#### 响应格式 -``` -Content-Type: application/octet-stream -Content-Disposition: attachment; filename="dictionaries_20240101.json" -``` - -#### 开发思路 -1. 查询指定的字典类型和项 -2. 构建导出数据结构 -3. 根据格式生成文件 -4. 返回文件流 - ---- - -### 7.6 字典项排序接口 - -#### 需求描述 -调整字典项的显示顺序,影响下拉框等组件的选项顺序。 - -#### 接口名称 -字典项排序接口 - -#### 设计原因 -- 下拉框选项需要按业务逻辑排序 -- 常用选项前置显示 -- 提升用户操作效率 - -#### 请求类型 -PUT - -#### 接口路径 -`/dictionaries/types/{typeId}/items/sort` - -#### 接口参数 -```typescript -{ - items: Array<{ - id: number; - sortOrder: number; - }> -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "字典项排序更新成功", - "data": { - "typeId": 1, - "updated": 4 - } -} -``` - -#### 开发思路 -1. 验证所有字典项都属于指定类型 -2. 批量更新sortOrder -3. 清除字典缓存 -4. 返回更新结果 - ---- - -## 8. 标签管理模块 (Tag Management) - -### 8.1 标签列表查询 - -#### 需求描述 -查询标签列表,支持按类型和使用频率排序。 - -#### 接口名称 -标签列表查询接口 - -#### 设计原因 -- 便于标签的统一管理 -- 支持热门标签展示 -- 不同类型标签分开管理 - -#### 请求类型 -GET - -#### 接口路径 -`/tags` - -#### 接口参数 -```typescript -{ - type?: string; // 标签类型 - keyword?: string; // 搜索关键词 - sortBy?: string; // 排序:usageCount,createdAt - page?: number; - pageSize?: number; -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": { - "items": [ - { - "id": 1, - "name": "VIP", - "type": "user", - "color": "#ff4d4f", - "usageCount": 100, - "description": "VIP用户" - } - ], - "pagination": { - "page": 1, - "pageSize": 20, - "total": 50 - } - } -} -``` - -#### 开发思路 -1. 构建查询条件 -2. 执行分页查询 -3. 根据sortBy排序 -4. 返回标签列表 - ---- - -### 8.2 创建标签 - -#### 需求描述 -创建新的标签。 - -#### 接口名称 -创建标签接口 - -#### 设计原因 -- 灵活的标签扩展 -- 支持自定义标签 -- 统一标签管理 - -#### 请求类型 -POST - -#### 接口路径 -`/tags` - -#### 接口参数 -```typescript -{ - name: string; // 标签名称 - type: string; // 标签类型 - color?: string; // 标签颜色 - description?: string; // 描述 -} -``` - -#### 开发思路 -1. 权限检查(需要tag:create权限) -2. 验证标签名称唯一性(同类型内) -3. 创建标签记录 -4. 返回创建结果 - ---- - -### 8.3 标签使用统计 - -#### 需求描述 -统计标签使用情况,生成报表。 - -#### 接口名称 -标签使用统计接口 - -#### 设计原因 -- 了解标签使用情况 -- 优化标签体系 -- 数据分析支持 - -#### 请求类型 -GET - -#### 接口路径 -`/tags/statistics` - -#### 接口参数 -```typescript -{ - type?: string; // 标签类型 - top?: number; // 返回前N个 - startDate?: string; // 统计开始时间 - endDate?: string; // 统计结束时间 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": { - "summary": { - "totalTags": 50, - "totalUsage": 1000, - "avgUsage": 20 - }, - "topTags": [ - { - "id": 1, - "name": "VIP", - "usageCount": 100, - "percentage": 10 - } - ], - "trend": [ - { - "date": "2024-01-01", - "count": 20 - } - ] - } -} -``` - -#### 开发思路 -1. 统计基础数据 -2. 查询热门标签 -3. 计算使用趋势 -4. 整合返回结果 - ---- - -### 8.4 标签智能推荐 - -#### 需求描述 -根据用户特征智能推荐标签。 - -#### 接口名称 -标签推荐接口 - -#### 设计原因 -- 提高标签使用的准确性 -- 基于规则引擎自动打标 -- 减少人工维护成本 - -#### 请求类型 -POST - -#### 接口路径 -`/tags/recommend` - -#### 接口参数 -```typescript -{ - userId: number; // 用户ID - type: string; // 推荐类型 - limit?: number; // 推荐数量限制 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": { - "recommendations": [ - { - "tagId": 1, - "tagName": "活跃用户", - "confidence": 0.95, - "reason": "最近30天登录20次" - } - ] - } -} -``` - -#### 开发思路 -1. 获取用户特征数据 -2. 执行推荐规则: - - 活跃度规则(登录次数、最后登录时间) - - 贡献度规则(内容创作数量) - - 消费规则(VIP等级) -3. 计算置信度 -4. 返回推荐标签列表 -5. 记录推荐日志 - ---- - -### 8.5 批量打标签 - -#### 需求描述 -为多个用户批量添加或移除标签。 - -#### 接口名称 -批量打标签接口 - -#### 设计原因 -- 提高运营效率 -- 支持批量操作 -- 灵活的标签管理 - -#### 请求类型 -POST - -#### 接口路径 -`/tags/batch-assign` - -#### 接口参数 -```typescript -{ - userIds: number[]; // 用户ID数组 - tagIds: number[]; // 标签ID数组 - action: string; // 操作:add,remove -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "批量操作成功", - "data": { - "success": 50, - "failed": 0, - "errors": [] - } -} -``` - -#### 开发思路 -1. 权限检查 -2. 验证用户和标签存在性 -3. 批量执行操作 -4. 更新标签使用统计 -5. 返回执行结果 - ---- - -### 8.6 标签排序接口 - -#### 需求描述 -调整标签的显示顺序,支持热门标签前置。 - -#### 接口名称 -标签排序接口 - -#### 设计原因 -- 热门标签需要前置显示 -- 按业务重要性排序 -- 提升标签选择效率 - -#### 请求类型 -PUT - -#### 接口路径 -`/tags/sort` - -#### 接口参数 -```typescript -{ - type?: string; // 标签类型,为空则排序全部 - items: Array<{ - id: number; - sortOrder: number; - }> -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "标签排序更新成功", - "data": { - "type": "user", - "updated": 6 - } -} -``` - -#### 开发思路 -1. 权限检查(需要tag:sort权限) -2. 验证标签类型一致性 -3. 批量更新sortOrder -4. 更新标签缓存 -5. 返回更新结果 - ---- - -## 9. 通用功能接口 - -### 9.1 图形验证码 - -#### 需求描述 -生成图形验证码,用于注册、登录等场景。 - -#### 接口名称 -获取图形验证码接口 - -#### 设计原因 -- 防止机器人攻击 -- 增强系统安全性 -- 限制恶意请求 - -#### 请求类型 -GET - -#### 接口路径 -`/common/captcha` - -#### 接口参数 -```typescript -{ - type?: string; // 验证码类型:image,math - width?: number; // 图片宽度,默认120 - height?: number; // 图片高度,默认40 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": { - "captchaId": "xxxx-xxxx-xxxx", - "captchaImage": "data:image/svg+xml;base64,..." - } -} -``` - -#### 开发思路 -1. 生成随机验证码文本 -2. 生成SVG或图片 -3. 存储到Redis(5分钟过期) -4. 返回Base64编码的图片 - ---- - -### 9.2 文件上传 - -#### 需求描述 -通用文件上传接口,支持头像等文件上传。 - -#### 接口名称 -文件上传接口 - -#### 设计原因 -- 统一的文件上传入口 -- 支持多种文件类型 -- 安全的文件处理 - -#### 请求类型 -POST - -#### 接口路径 -`/common/upload` - -#### 请求格式 -multipart/form-data - -#### 接口参数 -```typescript -{ - file: File; // 文件 - type: string; // 文件类型:avatar,document,image - isPublic?: boolean; // 是否公开访问,默认false -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "上传成功", - "data": { - "fileId": "file123456", - "url": "https://oss.starzh.com/xxx.jpg", - "size": 102400, - "name": "avatar.jpg", - "type": "image/jpeg" - } -} -``` - -#### 开发思路 -1. 验证文件类型和大小 -2. 生成唯一文件名 -3. 上传到OSS或本地存储 -4. 记录文件信息到数据库 -5. 返回文件访问URL - ---- - -### 9.3 邮件发送 - -#### 需求描述 -统一的邮件发送接口,用于系统通知。 - -#### 接口名称 -邮件发送接口(内部接口) - -#### 设计原因 -- 统一邮件发送逻辑 -- 支持模板化 -- 便于监控和管理 - -#### 请求类型 -POST - -#### 接口路径 -`/internal/email/send` - -#### 接口参数 -```typescript -{ - to: string[]; // 收件人邮箱数组 - template: string; // 邮件模板ID - params: object; // 模板参数 - attachments?: Array<{ // 附件 - filename: string; - content: string; - }>; -} -``` - -#### 开发思路 -1. 加载邮件模板 -2. 渲染模板内容 -3. 调用邮件服务发送 -4. 记录发送日志 -5. 处理发送失败(重试机制) - ---- - -### 9.4 系统健康检查 - -#### 需求描述 -提供系统健康状态检查接口。 - -#### 接口名称 -健康检查接口 - -#### 设计原因 -- 监控系统状态 -- 负载均衡健康检查 -- 快速发现问题 - -#### 请求类型 -GET - -#### 接口路径 -`/health` - -#### 响应格式 -```json -{ - "code": 0, - "message": "success", - "data": { - "status": "healthy", - "version": "1.0.0", - "uptime": 3600, - "services": { - "database": "ok", - "redis": "ok", - "elasticsearch": "ok" - } - } -} -``` - -#### 开发思路 -1. 检查数据库连接 -2. 检查Redis连接 -3. 检查其他服务状态 -4. 返回综合健康状态 - ---- - -### 9.5 数据导入导出 - -#### 需求描述 -支持批量数据的导入导出功能。 - -#### 接口名称 -数据导出接口 - -#### 设计原因 -- 数据备份需求 -- 批量数据处理 -- 数据迁移支持 - -#### 请求类型 -POST - -#### 接口路径 -`/common/export` - -#### 接口参数 -```typescript -{ - module: string; // 模块:users,roles,organizations - format: string; // 格式:excel,csv,json - filters?: object; // 过滤条件 - fields?: string[]; // 导出字段 -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "导出任务已创建", - "data": { - "taskId": "export-123456", - "status": "processing" - } -} -``` - -#### 开发思路 -1. 创建导出任务 -2. 异步处理数据查询和转换 -3. 生成导出文件 -4. 通过WebSocket或轮询通知完成 -5. 提供下载链接 - ---- - -### 9.6 通用排序接口 - -#### 需求描述 -提供通用的排序接口,支持多种数据类型的排序。 - -#### 接口名称 -通用排序接口 - -#### 设计原因 -- 统一的排序逻辑 -- 减少代码重复 -- 便于维护和扩展 - -#### 请求类型 -PUT - -#### 接口路径 -`/common/sort` - -#### 接口参数 -```typescript -{ - table: string; // 表名:roles,permissions,organizations,tags - scopeField?: string; // 范围字段:pid,typeId等 - scopeValue?: any; // 范围值 - items: Array<{ - id: number; - sortOrder: number; - }> -} -``` - -#### 响应格式 -```json -{ - "code": 0, - "message": "排序更新成功", - "data": { - "table": "roles", - "scope": "pid=1", - "updated": 5 - } -} -``` - -#### 开发思路 -1. 验证表名和字段的安全性 -2. 权限检查(根据表名动态判断) -3. 构建更新SQL -4. 批量更新排序字段 -5. 清除相关缓存 -6. 返回更新结果 - ---- - -## 10. 安全性设计 - -### 10.1 认证机制 -- **JWT Token认证** - - AccessToken有效期2小时 - - RefreshToken有效期7天 - - Token黑名单机制 - - 支持多设备登录管理 - -### 10.2 权限控制 -- **基于RBAC的权限模型** - - 接口级别的权限控制 - - 数据级别的权限过滤 - - 动态权限加载 - - 权限缓存优化 - -### 10.3 安全措施 -- **传输安全** - - 所有接口强制HTTPS - - 敏感数据加密传输 - - 防重放攻击(时间戳+nonce) - -- **输入验证** - - 参数类型和格式验证 - - SQL注入防护(参数化查询) - - XSS防护(输入过滤和转义) - - 文件上传安全检查 - -- **访问控制** - - IP白名单(管理接口) - - 请求频率限制 - - 并发连接限制 - - 异常行为检测 - -### 10.4 日志审计 -- **操作日志** - - 记录所有写操作 - - 包含用户、时间、IP、操作内容 - - 敏感操作实时告警 - -- **安全日志** - - 登录失败记录 - - 权限拒绝记录 - - 异常请求记录 - ---- - -## 11. 性能优化 - -### 11.1 缓存策略 - -| 缓存类型 | 缓存内容 | 过期时间 | 更新策略 | -|---------|---------|---------|---------| -| 用户信息缓存 | 基本信息、角色、权限 | 5分钟 | 主动更新+过期刷新 | -| 角色权限缓存 | 角色树、权限快照 | 1小时 | 变更时删除 | -| 字典缓存 | 字典类型和项 | 24小时 | 变更时删除 | -| 验证码缓存 | 图形验证码 | 5分钟 | 使用后删除 | - -### 11.2 查询优化 -- **索引优化** - - 基于查询模式设计索引 - - 定期分析慢查询日志 - - 避免全表扫描 - -- **查询优化** - - 使用预编译语句 - - 避免N+1查询 - - 合理使用JOIN和子查询 - - 大数据量分页优化 - -### 11.3 并发控制 -- **限流策略** - - 接口级别限流(令牌桶算法) - - 用户级别限流 - - IP级别限流 - -- **资源控制** - - 数据库连接池管理 - - Redis连接池管理 - - 异步任务队列 - -### 11.4 异步处理 -- **异步任务** - - 邮件发送 - - 文件处理 - - 数据导入导出 - - 统计计算 - ---- - -## 12. 错误处理 - -### 12.1 错误码规范 - -| 错误码范围 | 说明 | 示例 | -|-----------|------|------| -| 0 | 成功 | 操作成功 | -| 400xx | 客户端错误 | 40001-参数错误 | -| 401xx | 认证错误 | 40101-未登录 | -| 403xx | 权限错误 | 40301-无权限 | -| 404xx | 资源不存在 | 40401-用户不存在 | -| 409xx | 资源冲突 | 40901-用户名已存在 | -| 423xx | 资源锁定 | 42301-账号被锁定 | -| 429xx | 请求过多 | 42901-请求频率超限 | -| 500xx | 服务器错误 | 50001-内部错误 | -| 503xx | 服务不可用 | 50301-服务维护中 | - -### 12.2 错误响应格式 -```json -{ - "code": 40001, - "message": "参数验证失败:用户名长度必须在3-50个字符之间", - "data": { - "field": "username", - "value": "ab", - "rule": "minLength" - }, -} -``` - -### 12.3 错误处理最佳实践 -- **友好的错误提示** - - 使用用户能理解的语言 - - 提供解决建议 - - 避免暴露技术细节 - -- **错误日志记录** - - 记录完整的错误堆栈 - - 包含请求上下文 - - 分级记录(info/warn/error) - -- **错误监控告警** - - 错误率监控 - - 关键错误实时告警 - - 错误趋势分析 - ---- - -## 13. 接口版本管理 - -### 13.1 版本策略 -- **URL版本化** - - 格式:`/api/v{n}/...` - - 主版本号变更表示不兼容更新 - - 向后兼容原则 - -### 13.2 版本迁移 -- **废弃通知** - - 提前3个月通知 - - 响应头添加废弃警告 - - 提供迁移文档 - -- **版本共存** - - 新旧版本并行运行 - - 逐步迁移用户 - - 设置旧版本下线时间 - ---- - -## 14. 开发规范总结 - -### 14.1 接口设计原则 -- **RESTful规范** - - 使用标准HTTP方法 - - 资源导向的URL设计 - - 合理使用状态码 - -- **一致性原则** - - 统一的命名规范 - - 统一的响应格式 - - 统一的错误处理 - -### 14.2 最佳实践 -- **幂等性设计** - - GET/PUT/DELETE操作幂等 - - POST操作防重复提交 - -- **分页设计** - - 统一分页参数 - - 限制最大返回条数 - - 提供总数统计 - -- **数据安全** - - 敏感数据脱敏 - - 防止信息泄露 - - 输入验证和过滤 - -### 14.3 文档规范 -- **接口文档** - - 使用Swagger自动生成 - - 包含请求和响应示例 - - 说明业务场景 - -- **变更记录** - - 记录所有API变更 - - 说明变更原因 - - 提供升级指南 - ---- - -## 15. 附录 - -### 15.1 常用正则表达式 -```javascript -// 用户名:3-50位字母数字下划线,字母开头 -const USERNAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_]{2,49}$/; - -// 邮箱 -const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - -// 手机号(中国大陆) -const MOBILE_REGEX = /^1[3-9]\d{9}$/; - -// 强密码:8-100位,包含大小写字母和数字 -const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,100}$/; -``` - -### 15.2 时间格式约定 -- 统一使用ISO 8601格式 -- 示例:`2024-01-01T12:00:00Z` -- 时区:统一使用UTC时间 - -### 15.3 分页参数约定 -- `page`: 页码,从1开始 -- `pageSize`: 每页条数,默认20,最大100 -- `total`: 总记录数 -- `totalPages`: 总页数 - ---- - -本接口设计文档为M2基础用户系统的完整API规范,后续开发应严格遵循本文档的设计要求。 \ No newline at end of file +# M2 - 基础用户系统 - 详细接口设计 + +## 1. 概述 + +本文档基于M2阶段的数据库设计和产品需求,提供完整的RESTful API接口设计方案。所有接口遵循以下原则: + +- **RESTful规范**:使用标准HTTP方法和状态码 +- **统一响应格式**:所有接口返回统一的JSON格式 +- **安全性**:JWT认证、权限控制、数据验证 +- **高性能**:合理的缓存策略、分页设计 +- **版本控制**:API版本化管理 + +### 基础URL + +``` +https://api.starzh.com/v1 +``` + +### 统一响应格式 + +```json +{ + "code": 0, // 业务状态码,0表示成功 + "message": "success", // 提示信息 + "data": {} // 响应数据 +} +``` + +### 统一错误码规范 + +- 0: 成功 +- 400xx: 客户端错误(参数错误、验证失败等) +- 401xx: 认证错误 +- 403xx: 权限错误 +- 404xx: 资源不存在 +- 409xx: 资源冲突 +- 500xx: 服务器错误 + +--- + +## 2. 认证模块 (Authentication) + +### 2.1 用户注册 + +#### 需求描述 + +允许新用户通过邮箱注册账号,需要邮箱验证激活。支持基本的用户名、密码规则校验。 + +#### 接口名称 + +用户注册接口 + +#### 设计原因 + +- 提供用户自主注册功能 +- 通过邮箱验证确保用户真实性 +- 防止恶意注册和垃圾账号 + +#### 请求类型 + +POST + +#### 接口路径 + +`/auth/register` + +#### 接口参数 + +```typescript +{ + username: string; // 用户名,3-50字符,字母开头,仅包含字母数字下划线 + email: string; // 邮箱地址,需符合邮箱格式 + password: string; // 密码,8-100字符,必须包含大小写字母和数字 + confirmPassword: string; // 确认密码,需与password一致 + captcha: string; // 图形验证码 + captchaId: string; // 验证码ID +} +``` + +**参数约束原因**: + +- username: 限制长度和字符类型,防止特殊字符引起的安全问题 +- password: 强密码策略,提高账户安全性 +- captcha: 防止机器人批量注册 + +#### 响应分类 + +- 成功:201 Created - 注册成功,发送激活邮件 +- 失败: + - 400 Bad Request - 参数验证失败 + - 409 Conflict - 用户名或邮箱已存在 + +#### 响应格式 + +```json +// 成功响应 +{ + "code": 0, + "message": "注册成功,请查收邮件激活账号", + "data": { + "userId": 1234567890, + "username": "john_doe", + "email": "john@example.com", + "status": "inactive" + } +} + +// 失败响应示例 +{ + "code": 40901, + "message": "用户名已被占用", + "data": null +} +``` + +#### 开发思路 + +1. 参数验证层:使用Elysia的t验证器进行参数校验 +2. 业务逻辑层: + - 验证图形验证码 + - 检查用户名和邮箱唯一性 + - 密码使用bcrypt加密(成本因子12) + - 生成用户记录(状态为inactive) + - 生成激活token并存入Redis(24小时过期) + - 发送激活邮件(异步队列) +3. 数据访问层:使用事务确保数据一致性 +4. 日志记录:记录注册操作到sys_operation_logs + +--- + +### 2.2 用户登录 + +#### 需求描述 + +支持用户名或邮箱登录,实现登录失败次数限制,防止暴力破解。 + +#### 接口名称 + +用户登录接口 + +#### 设计原因 + +- 提供安全的身份认证机制 +- 支持多种登录方式提升用户体验 +- 防止暴力破解攻击 + +#### 请求类型 + +POST + +#### 接口路径 + +`/auth/login` + +#### 接口参数 + +```typescript +{ + account: string; // 用户名或邮箱 + password: string; // 密码 + captcha?: string; // 验证码(连续失败3次后必填) + captchaId?: string; // 验证码ID + rememberMe?: boolean; // 记住我,默认false +} +``` + +#### 响应分类 + +- 成功:200 OK - 登录成功,返回token +- 失败: + - 401 Unauthorized - 用户名或密码错误 + - 423 Locked - 账号被锁定 + +#### 响应格式 + +```json +// 成功响应 +{ + "code": 0, + "message": "登录成功", + "data": { + "token": "eyJhbGciOiJIUzI1NiIs...", + "refreshToken": "xxxx-xxxx-xxxx", + "expiresIn": 7200, + "user": { + "id": 1234567890, + "username": "john_doe", + "nickname": "John", + "avatar": "https://...", + "roles": ["user"] + } + } +} +``` + +#### 开发思路 + +1. 查询用户(支持username或email) +2. 检查账号状态和锁定状态 +3. 验证密码 +4. 处理登录失败: + - 增加失败次数 + - 超过5次锁定30分钟 + - 3次失败后要求验证码 +5. 登录成功: + - 清除失败次数 + - 生成JWT token + - 更新最后登录信息 + - 记录登录日志 + +--- + +### 2.3 刷新Token + +#### 需求描述 + +使用refreshToken换取新的accessToken,实现无感续期。 + +#### 接口名称 + +Token刷新接口 + +#### 设计原因 + +- 短期accessToken + 长期refreshToken模式 +- 提高安全性,减少token泄露风险 +- 实现用户无感续期体验 + +#### 请求类型 + +POST + +#### 接口路径 + +`/auth/refresh` + +#### 接口参数 + +```typescript +{ + refreshToken: string; // 刷新令牌 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "刷新成功", + "data": { + "token": "new-access-token", + "expiresIn": 7200 + } +} +``` + +#### 开发思路 + +1. 验证refreshToken有效性 +2. 检查用户状态 +3. 生成新的accessToken +4. 可选:轮转refreshToken +5. 记录刷新日志 + +--- + +### 2.4 账号激活 + +#### 需求描述 + +用户通过邮件中的链接或验证码激活账号。 + +#### 接口名称 + +账号激活接口 + +#### 设计原因 + +- 验证邮箱真实性 +- 防止垃圾注册 +- 提高用户质量 + +#### 请求类型 + +POST + +#### 接口路径 + +`/auth/activate` + +#### 接口参数 + +```typescript +{ + token: string; // 激活令牌(从邮件链接获取) + // 或 + email: string; // 邮箱 + code: string; // 6位数字激活码 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "账号激活成功", + "data": { + "userId": 1234567890, + "username": "john_doe", + "status": "active" + } +} +``` + +#### 开发思路 + +1. 验证激活token或激活码 +2. 检查是否过期 +3. 更新用户状态为active +4. 删除Redis中的激活信息 +5. 发送欢迎邮件 +6. 记录激活日志 + +--- + +### 2.5 退出登录 + +#### 需求描述 + +用户主动退出登录,清除token。 + +#### 接口名称 + +退出登录接口 + +#### 设计原因 + +- 用户主动结束会话 +- 清除服务端缓存 +- 增强安全性 + +#### 请求类型 + +POST + +#### 接口路径 + +`/auth/logout` + +#### 请求头 + +``` +Authorization: Bearer +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "退出成功", + "data": null +} +``` + +#### 开发思路 + +1. 获取当前token +2. 加入token黑名单(Redis) +3. 清除用户相关缓存 +4. 记录退出日志 + +--- + +## 3. 用户管理模块 (User Management) + +### 3.1 获取当前用户信息 + +#### 需求描述 + +获取当前登录用户的详细信息,包括基本信息、角色、权限、组织等。 + +#### 接口名称 + +获取当前用户信息接口 + +#### 设计原因 + +- 前端需要用户信息进行界面渲染 +- 权限控制需要用户角色和权限信息 +- 统一的用户信息获取入口 + +#### 请求类型 + +GET + +#### 接口路径 + +`/users/me` + +#### 请求头 + +``` +Authorization: Bearer +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1234567890, + "username": "john_doe", + "email": "john@example.com", + "mobile": "13800138000", + "nickname": "John", + "avatar": "https://...", + "gender": 1, + "birthday": "1990-01-01", + "bio": "Hello world", + "status": "active", + "lastLoginAt": "2024-01-01T12:00:00Z", + "roles": [ + { + "id": 1, + "code": "admin", + "name": "管理员" + } + ], + "permissions": [ + { + "code": "user:read", + "name": "查看用户", + "type": "api" + } + ], + "organizations": [ + { + "id": 1, + "name": "技术部", + "isPrimary": true, + "position": "工程师" + } + ], + "tags": [ + { + "id": 1, + "name": "VIP", + "color": "#ff4d4f" + } + ] + } +} +``` + +#### 开发思路 + +1. 从JWT token中获取用户ID +2. 查询用户基本信息 +3. 并行查询关联信息: + - 用户角色(包含权限快照) + - 用户组织 + - 用户标签 +4. 整合权限信息(角色权限去重) +5. 使用Redis缓存(5分钟过期) + +--- + +### 3.2 用户列表查询 + +#### 需求描述 + +分页查询用户列表,支持多条件筛选和排序。 + +#### 接口名称 + +用户列表查询接口 + +#### 设计原因 + +- 管理员需要查看和管理所有用户 +- 支持灵活的查询条件满足不同场景 +- 分页设计避免大数据量查询 + +#### 请求类型 + +GET + +#### 接口路径 + +`/users` + +#### 接口参数 + +```typescript +// Query Parameters +{ + page?: number; // 页码,默认1 + pageSize?: number; // 每页数量,默认20,最大100 + keyword?: string; // 搜索关键词(用户名/邮箱/手机号/昵称) + status?: string; // 用户状态 + roleId?: number; // 角色ID + organizationId?: number; // 组织ID + tagId?: number; // 标签ID + startDate?: string; // 注册开始时间 + endDate?: string; // 注册结束时间 + sortBy?: string; // 排序字段:createdAt,lastLoginAt + sortOrder?: string; // 排序方向:asc,desc +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { + "items": [ + { + "id": 1234567890, + "username": "john_doe", + "email": "john@example.com", + "mobile": "138****8000", + "nickname": "John", + "avatar": "https://...", + "status": "active", + "roles": ["admin", "user"], + "organizations": ["技术部"], + "tags": ["VIP"], + "loginCount": 100, + "lastLoginAt": "2024-01-01T12:00:00Z", + "createdAt": "2023-01-01T00:00:00Z" + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 100, + "totalPages": 5 + } + } +} +``` + +#### 开发思路 + +1. 参数验证和权限检查(需要user:read权限) +2. 构建查询条件: + - 关键词模糊查询 + - 状态精确匹配 + - 关联表过滤(角色、组织、标签) + - 时间范围查询 +3. 执行分页查询(使用索引优化) +4. 批量查询关联信息(减少N+1查询) +5. 数据脱敏(手机号中间4位) + +--- + +### 3.3 创建用户 + +#### 需求描述 + +管理员创建新用户,可直接设置角色、组织等信息。 + +#### 接口名称 + +创建用户接口 + +#### 设计原因 + +- 管理员需要批量创建用户 +- 可预设用户角色和组织 +- 支持直接激活状态 + +#### 请求类型 + +POST + +#### 接口路径 + +`/users` + +#### 接口参数 + +```typescript +{ + username: string; + email: string; + password: string; + mobile?: string; + nickname?: string; + status?: string; // 默认active + gender?: number; + birthday?: string; + bio?: string; + roleIds?: number[]; // 角色ID数组 + organizationIds?: Array<{ + id: number; + isPrimary: boolean; + position?: string; + }>; // 组织信息 + tagIds?: number[]; // 标签ID数组 + sendNotification?: boolean; // 是否发送通知邮件 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "创建成功", + "data": { + "id": 1234567890, + "username": "new_user", + "email": "new@example.com", + "status": "active" + } +} +``` + +#### 开发思路 + +1. 权限检查(需要user:create权限) +2. 参数验证(唯一性检查) +3. 使用事务处理: + - 创建用户记录 + - 分配角色 + - 分配组织 + - 分配标签 +4. 发送通知邮件(可选) +5. 记录操作日志 + +--- + +### 3.4 更新用户信息 + +#### 需求描述 + +更新用户基本信息和关联信息。 + +#### 接口名称 + +更新用户信息接口 + +#### 设计原因 + +- 支持用户自主修改个人信息 +- 管理员可修改用户所有信息 +- 分离基本信息和关联信息更新 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/users/{userId}` + +#### 接口参数 + +```typescript +{ + email?: string; + mobile?: string; + nickname?: string; + avatar?: string; + gender?: number; + birthday?: string; + bio?: string; + // 以下字段需要管理员权限 + status?: string; + roleIds?: number[]; + organizationIds?: Array<{ + id: number; + isPrimary: boolean; + position?: string; + }>; + tagIds?: number[]; +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "更新成功", + "data": { + "id": 1234567890, + "username": "john_doe", + "email": "john@example.com", + "updatedAt": "2024-01-01T12:00:00Z" + } +} +``` + +#### 开发思路 + +1. 权限检查: + - 用户可修改自己的基本信息 + - 管理员可修改所有信息 + - root用户信息限制修改 +2. 参数验证(邮箱唯一性等) +3. 使用乐观锁防止并发冲突 +4. 更新关联信息时先删后增 +5. 清除相关缓存 + +--- + +### 3.5 修改密码 + +#### 需求描述 + +用户修改自己的密码,需要验证原密码。 + +#### 接口名称 + +修改密码接口 + +#### 设计原因 + +- 用户自主管理密码 +- 增强账户安全性 +- 防止未授权修改 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/users/me/password` + +#### 接口参数 + +```typescript +{ + oldPassword: string; // 原密码 + newPassword: string; // 新密码 + confirmPassword: string; // 确认新密码 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "密码修改成功", + "data": null +} +``` + +#### 开发思路 + +1. 验证原密码正确性 +2. 验证新密码符合规则 +3. 更新密码(bcrypt加密) +4. 清除所有该用户的token +5. 发送密码修改通知邮件 + +--- + +### 3.6 重置密码 + +#### 需求描述 + +用户忘记密码时通过邮箱重置。 + +#### 接口名称 + +申请重置密码接口 + +#### 设计原因 + +- 用户忘记密码的补救措施 +- 通过邮箱验证身份 +- 防止恶意重置 + +#### 请求类型 + +POST + +#### 接口路径 + +`/users/password/reset-request` + +#### 接口参数 + +```typescript +{ + email: string; + captcha: string; + captchaId: string; +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "重置邮件已发送", + "data": null +} +``` + +#### 开发思路 + +1. 验证邮箱存在 +2. 验证图形验证码 +3. 生成重置token(6位数字或链接) +4. 存入Redis(30分钟过期) +5. 发送重置邮件 +6. 限制发送频率(1分钟内不可重复) + +--- + +### 3.7 确认重置密码 + +#### 需求描述 + +使用重置token设置新密码。 + +#### 接口名称 + +确认重置密码接口 + +#### 请求类型 + +POST + +#### 接口路径 + +`/users/password/reset-confirm` + +#### 接口参数 + +```typescript +{ + token: string; // 重置token + email: string; // 邮箱 + newPassword: string; // 新密码 + confirmPassword: string; // 确认密码 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "密码重置成功", + "data": null +} +``` + +#### 开发思路 + +1. 验证token有效性和匹配性 +2. 验证新密码规则 +3. 更新用户密码 +4. 删除重置token +5. 清除用户所有token +6. 发送重置成功通知 + +--- + +### 3.8 删除用户 + +#### 需求描述 + +软删除用户,保留数据用于审计。 + +#### 接口名称 + +删除用户接口 + +#### 设计原因 + +- 满足用户注销需求 +- 保留数据用于审计 +- 支持误删恢复 + +#### 请求类型 + +DELETE + +#### 接口路径 + +`/users/{userId}` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "删除成功", + "data": null +} +``` + +#### 开发思路 + +1. 权限检查(需要user:delete权限) +2. 检查是否为root用户(禁止删除) +3. 检查是否为自己(需要二次确认) +4. 更新deleted_at字段 +5. 清除用户所有会话 +6. 记录删除操作日志 + +--- + +### 3.9 批量操作用户 + +#### 需求描述 + +批量启用、禁用、删除用户。 + +#### 接口名称 + +批量操作用户接口 + +#### 设计原因 + +- 提高管理效率 +- 支持批量处理 +- 减少重复操作 + +#### 请求类型 + +POST + +#### 接口路径 + +`/users/batch` + +#### 接口参数 + +```typescript +{ + userIds: number[]; // 用户ID数组 + action: string; // 操作类型:enable,disable,delete +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "操作成功", + "data": { + "success": 10, + "failed": 0, + "errors": [] + } +} +``` + +#### 开发思路 + +1. 权限检查 +2. 过滤掉root用户 +3. 批量执行操作 +4. 记录操作结果 +5. 返回执行统计 + +--- + +## 4. 角色管理模块 (Role Management) + +### 4.1 角色树查询 + +#### 需求描述 + +查询角色树形结构,支持权限继承展示。 + +#### 接口名称 + +角色树查询接口 + +#### 设计原因 + +- 角色具有层级关系需要树形展示 +- 支持权限继承机制 +- 便于理解角色体系 + +#### 请求类型 + +GET + +#### 接口路径 + +`/roles/tree` + +#### 接口参数 + +```typescript +{ + status?: string; // 过滤状态 + withPermissions?: boolean; // 是否包含权限信息 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": [ + { + "id": 1, + "code": "super_admin", + "name": "超级管理员", + "description": "拥有所有权限", + "status": "active", + "isSystem": true, + "permissionCount": 50, + "children": [ + { + "id": 2, + "code": "admin", + "name": "管理员", + "pid": 1, + "children": [] + } + ] + } + ] +} +``` + +#### 开发思路 + +1. 查询所有角色(使用path字段优化) +2. 构建树形结构(递归或迭代) +3. 统计每个角色的权限数量 +4. 使用Redis缓存(角色变更时失效) + +--- + +### 4.2 创建角色 + +#### 需求描述 + +创建新角色并分配权限,支持角色继承。 + +#### 接口名称 + +创建角色接口 + +#### 设计原因 + +- 灵活的角色管理支持业务扩展 +- 权限继承减少重复配置 +- 下级角色权限不能超过上级 + +#### 请求类型 + +POST + +#### 接口路径 + +`/roles` + +#### 接口参数 + +```typescript +{ + code: string; // 角色代码,唯一 + name: string; // 角色名称 + description?: string; // 角色描述 + pid?: number; // 父角色ID + status?: string; // 状态,默认active + permissionIds: number[]; // 权限ID数组 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "创建成功", + "data": { + "id": 10, + "code": "editor", + "name": "编辑员", + "path": "/1/2/10/", + "level": 3 + } +} +``` + +#### 开发思路 + +1. 权限检查(需要role:create权限) +2. 验证角色代码唯一性 +3. 验证父角色存在性 +4. 检查权限分配合法性: + - 获取父角色所有权限 + - 确保新角色权限是父角色权限的子集 +5. 使用事务创建: + - 创建角色记录 + - 计算并设置path和level + - 分配权限 + - 生成权限快照 +6. 清除角色缓存 + +--- + +### 4.3 更新角色信息 + +#### 需求描述 + +更新角色基本信息,不包括权限。 + +#### 接口名称 + +更新角色信息接口 + +#### 设计原因 + +- 角色信息可能需要调整 +- 分离基本信息和权限更新 +- 防止误操作 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/roles/{roleId}` + +#### 接口参数 + +```typescript +{ + name?: string; + description?: string; + status?: string; + sortOrder?: number; +} +``` + +#### 开发思路 + +1. 权限检查(需要role:update权限) +2. 检查是否为系统角色(限制修改) +3. 更新角色信息 +4. 清除角色缓存 + +--- + +### 4.4 更新角色权限 + +#### 需求描述 + +动态调整角色权限,支持批量操作。 + +#### 接口名称 + +更新角色权限接口 + +#### 设计原因 + +- 业务变化需要调整权限 +- 支持灵活的权限管理 +- 保证权限继承一致性 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/roles/{roleId}/permissions` + +#### 接口参数 + +```typescript +{ + permissionIds: number[]; // 新的权限ID数组(全量) +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "权限更新成功", + "data": { + "roleId": 10, + "permissionCount": 20, + "addedCount": 5, + "removedCount": 3 + } +} +``` + +#### 开发思路 + +1. 检查角色是否为系统角色 +2. 验证权限合法性(不超过父角色) +3. 对比新旧权限,计算差异 +4. 使用事务更新: + - 删除旧权限关联 + - 创建新权限关联 + - 更新权限快照 +5. 级联更新子角色(移除超出的权限) +6. 清除相关用户的权限缓存 + +--- + +### 4.5 删除角色 + +#### 需求描述 + +删除角色,处理关联关系。 + +#### 接口名称 + +删除角色接口 + +#### 设计原因 + +- 清理无用角色 +- 保持系统整洁 +- 防止权限泄露 + +#### 请求类型 + +DELETE + +#### 接口路径 + +`/roles/{roleId}` + +#### 开发思路 + +1. 权限检查(需要role:delete权限) +2. 检查是否为系统角色(禁止删除) +3. 检查是否有用户使用该角色 +4. 检查是否有子角色 +5. 软删除角色 +6. 清除相关缓存 + +--- + +### 4.6 角色用户查询 + +#### 需求描述 + +查询某个角色下的所有用户。 + +#### 接口名称 + +角色用户查询接口 + +#### 设计原因 + +- 了解角色使用情况 +- 便于角色管理 +- 支持用户迁移 + +#### 请求类型 + +GET + +#### 接口路径 + +`/roles/{roleId}/users` + +#### 接口参数 + +```typescript +{ + page?: number; + pageSize?: number; + keyword?: string; +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { + "items": [ + { + "id": 123, + "username": "john_doe", + "email": "john@example.com", + "assignedAt": "2024-01-01T00:00:00Z" + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 50 + } + } +} +``` + +--- + +### 4.7 角色排序接口 + +#### 需求描述 + +支持手动调整角色的显示顺序,保证同级角色按指定顺序展示。 + +#### 接口名称 + +角色排序接口 + +#### 设计原因 + +- 角色列表需要按重要性排序 +- 下拉选择时按业务优先级展示 +- 管理员需要控制显示顺序 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/roles/sort` + +#### 接口参数 + +```typescript +{ + items: Array<{ + id: number; // 角色ID + sortOrder: number; // 新的排序号 + }>; +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "排序更新成功", + "data": { + "updated": 5 + } +} +``` + +#### 开发思路 + +1. 权限检查(需要role:sort权限) +2. 批量更新sortOrder字段 +3. 确保同级别角色排序唯一性 +4. 清除角色缓存 +5. 返回更新结果 + +--- + +### 4.8 拖拽排序接口 + +#### 需求描述 + +支持前端拖拽方式调整角色顺序,更直观的操作方式。 + +#### 接口名称 + +拖拽排序接口 + +#### 设计原因 + +- 提供更好的用户体验 +- 直观的排序操作 +- 减少误操作 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/roles/{roleId}/move` + +#### 接口参数 + +```typescript +{ + targetIndex: number; // 目标位置索引(0开始) + pid?: number; // 父级ID,如果跨级移动 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "移动成功", + "data": { + "roleId": 10, + "oldIndex": 2, + "newIndex": 0, + "affectedRoles": [8, 9, 10] + } +} +``` + +#### 开发思路 + +1. 计算目标位置的sortOrder值 +2. 更新被移动角色的sortOrder +3. 调整其他角色的sortOrder(避免冲突) +4. 如果跨级移动,同时更新pid +5. 清除相关缓存 + +--- + +## 5. 权限管理模块 (Permission Management) + +### 5.1 权限树查询 + +#### 需求描述 + +查询权限树形结构,按类型分组展示。 + +#### 接口名称 + +权限树查询接口 + +#### 设计原因 + +- 权限具有层级关系(菜单-页面-按钮) +- 便于权限分配时的选择 +- 支持不同类型权限的管理 + +#### 请求类型 + +GET + +#### 接口路径 + +`/permissions/tree` + +#### 接口参数 + +```typescript +{ + type?: string; // 权限类型:menu,button,api,data + status?: string; // 状态过滤 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": [ + { + "id": 1, + "code": "system", + "name": "系统管理", + "type": "menu", + "children": [ + { + "id": 2, + "code": "system:user", + "name": "用户管理", + "type": "menu", + "children": [ + { + "id": 3, + "code": "user:read", + "name": "查看用户", + "type": "api", + "resource": "user", + "action": "read" + } + ] + } + ] + } + ] +} +``` + +#### 开发思路 + +1. 查询权限列表 +2. 按类型分组 +3. 构建树形结构 +4. 使用Redis缓存 + +--- + +### 5.2 创建权限 + +#### 需求描述 + +创建新的权限节点。 + +#### 接口名称 + +创建权限接口 + +#### 设计原因 + +- 新功能需要新权限 +- 灵活的权限扩展 +- 支持细粒度控制 + +#### 请求类型 + +POST + +#### 接口路径 + +`/permissions` + +#### 接口参数 + +```typescript +{ + code: string; // 权限代码 + name: string; // 权限名称 + type: string; // 权限类型 + resource?: string; // 资源标识 + action?: string; // 操作标识 + pid?: number; // 父权限ID + description?: string; // 描述 + meta?: object; // 元数据(如图标、路由) +} +``` + +#### 开发思路 + +1. 权限检查(需要permission:create权限) +2. 验证权限代码唯一性 +3. 验证父权限存在性 +4. 创建权限记录 +5. 清除权限缓存 + +--- + +### 5.3 批量创建权限 + +#### 需求描述 + +支持批量导入权限配置。 + +#### 接口名称 + +批量创建权限接口 + +#### 设计原因 + +- 新模块上线需要批量添加权限 +- 提高配置效率 +- 支持权限模板导入 + +#### 请求类型 + +POST + +#### 接口路径 + +`/permissions/batch` + +#### 接口参数 + +```typescript +{ + permissions: Array<{ + code: string; + name: string; + type: string; + resource?: string; + action?: string; + pid?: number; + description?: string; + }>; +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "批量创建成功", + "data": { + "success": 10, + "failed": 0, + "errors": [] + } +} +``` + +#### 开发思路 + +1. 批量验证权限代码唯一性 +2. 验证父权限存在性 +3. 使用事务批量插入 +4. 返回执行结果统计 +5. 清除权限缓存 + +--- + +### 5.4 更新权限 + +#### 需求描述 + +更新权限信息。 + +#### 接口名称 + +更新权限接口 + +#### 设计原因 + +- 权限名称或描述可能需要调整 +- 元数据更新(如菜单图标) +- 状态变更 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/permissions/{permissionId}` + +#### 接口参数 + +```typescript +{ + name?: string; + description?: string; + status?: string; + meta?: object; + sortOrder?: number; +} +``` + +#### 开发思路 + +1. 权限检查 +2. 更新权限信息 +3. 清除权限缓存 +4. 通知相关角色缓存更新 + +--- + +### 5.5 删除权限 + +#### 需求描述 + +删除权限节点及其子权限。 + +#### 接口名称 + +删除权限接口 + +#### 设计原因 + +- 功能下线需要删除权限 +- 保持权限体系整洁 +- 防止权限泄露 + +#### 请求类型 + +DELETE + +#### 接口路径 + +`/permissions/{permissionId}` + +#### 开发思路 + +1. 权限检查(需要permission:delete权限) +2. 检查是否有角色使用该权限 +3. 递归删除子权限 +4. 清除相关缓存 + +--- + +### 5.6 权限检查接口 + +#### 需求描述 + +检查当前用户是否拥有特定权限。 + +#### 接口名称 + +权限检查接口 + +#### 设计原因 + +- 前端动态控制界面元素 +- 细粒度的功能控制 +- 实时权限验证 + +#### 请求类型 + +POST + +#### 接口路径 + +`/permissions/check` + +#### 接口参数 + +```typescript +{ + permissions: string[]; // 权限代码数组 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { + "user:read": true, + "user:create": true, + "user:delete": false + } +} +``` + +#### 开发思路 + +1. 获取当前用户所有权限 +2. 批量检查请求的权限 +3. 返回权限检查结果 + +--- + +### 5.7 权限排序接口 + +#### 需求描述 + +调整权限的显示顺序,特别是菜单权限的排序。 + +#### 接口名称 + +权限排序接口 + +#### 设计原因 + +- 菜单权限需要控制显示顺序 +- 影响前端菜单渲染顺序 +- 提升用户体验 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/permissions/sort` + +#### 接口参数 + +```typescript +{ + items: Array<{ + id: number; + sortOrder: number; + }>; +} +``` + +#### 开发思路 + +1. 权限检查(需要permission:sort权限) +2. 批量更新权限排序 +3. 确保同级权限排序唯一 +4. 清除权限缓存 +5. 通知前端刷新菜单 + +--- + +## 6. 组织架构模块 (Organization Management) + +### 6.1 组织树查询 + +#### 需求描述 + +查询组织架构树,支持人员统计。 + +#### 接口名称 + +组织架构树查询接口 + +#### 设计原因 + +- 直观展示组织层级关系 +- 便于用户分配和管理 +- 支持组织人员统计 + +#### 请求类型 + +GET + +#### 接口路径 + +`/organizations/tree` + +#### 接口参数 + +```typescript +{ + type?: string; // 组织类型 + status?: string; // 状态 + withStats?: boolean; // 是否包含统计信息 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": [ + { + "id": 1, + "code": "ROOT", + "name": "星撰集团", + "type": "company", + "userCount": 100, + "children": [ + { + "id": 2, + "code": "TECH", + "name": "技术部", + "type": "department", + "userCount": 30, + "leader": { + "id": 123, + "name": "张三" + }, + "children": [] + } + ] + } + ] +} +``` + +#### 开发思路 + +1. 查询组织列表 +2. 构建树形结构 +3. 如需统计: + - 批量查询用户组织关联 + - 聚合统计各组织人数 +4. 查询负责人信息 +5. 使用Redis缓存 + +--- + +### 6.2 创建组织 + +#### 需求描述 + +创建新的组织节点。 + +#### 接口名称 + +创建组织接口 + +#### 设计原因 + +- 组织架构需要动态调整 +- 支持业务扩展 +- 灵活的组织管理 + +#### 请求类型 + +POST + +#### 接口路径 + +`/organizations` + +#### 接口参数 + +```typescript +{ + code: string; // 组织代码 + name: string; // 组织名称 + fullName?: string; // 组织全称 + type: string; // 组织类型 + pid?: number; // 父组织ID + leaderId?: number; // 负责人ID + address?: string; // 地址 + phone?: string; // 电话 + description?: string; // 描述 +} +``` + +#### 开发思路 + +1. 权限检查(需要organization:create权限) +2. 验证组织代码唯一性 +3. 验证父组织存在性 +4. 创建组织记录 +5. 计算path和level +6. 清除组织缓存 + +--- + +### 6.3 调整组织架构 + +#### 需求描述 + +支持拖拽调整组织层级关系。 + +#### 接口名称 + +调整组织架构接口 + +#### 设计原因 + +- 组织架构会随业务发展调整 +- 支持灵活的组织变更 +- 保持数据完整性 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/organizations/{organizationId}/move` + +#### 接口参数 + +```typescript +{ + targetPid: number; // 目标父组织ID + targetIndex?: number; // 目标位置索引 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "调整成功", + "data": { + "id": 10, + "oldPath": "/1/2/10/", + "newPath": "/1/3/10/", + "affectedCount": 5 + } +} +``` + +#### 开发思路 + +1. 验证目标组织存在性 +2. 检查是否会形成循环 +3. 使用事务更新: + - 更新pid + - 重新计算path和level + - 更新所有子组织的path + - 调整排序 +4. 记录组织变更日志 +5. 清除相关缓存 + +--- + +### 6.4 组织人员管理 + +#### 需求描述 + +管理组织内的人员分配。 + +#### 接口名称 + +组织人员分配接口 + +#### 设计原因 + +- 灵活的人员调动 +- 支持批量操作 +- 记录调动历史 + +#### 请求类型 + +POST + +#### 接口路径 + +`/organizations/{organizationId}/users` + +#### 接口参数 + +```typescript +{ + action: string; // 操作:add,remove,move + userIds: number[]; // 用户ID数组 + position?: string; // 职位(添加时使用) + isPrimary?: boolean; // 是否主组织 + targetOrgId?: number; // 目标组织ID(移动时使用) +} +``` + +#### 开发思路 + +1. 权限检查 +2. 验证用户和组织存在性 +3. 执行批量操作: + - add: 添加用户到组织 + - remove: 从组织移除用户 + - move: 移动到其他组织 +4. 记录人员变动日志 + +--- + +### 6.5 删除组织 + +#### 需求描述 + +删除组织节点,处理人员安置。 + +#### 接口名称 + +删除组织接口 + +#### 设计原因 + +- 组织调整需要删除节点 +- 确保人员妥善安置 +- 保持数据完整性 + +#### 请求类型 + +DELETE + +#### 接口路径 + +`/organizations/{organizationId}` + +#### 接口参数 + +```typescript +{ + handleUsers?: string; // 人员处理方式:moveToParent,remove + targetOrgId?: number; // 目标组织ID(自定义迁移时使用) +} +``` + +#### 开发思路 + +1. 权限检查(需要organization:delete权限) +2. 检查是否有子组织 +3. 处理组织内人员 +4. 软删除组织 +5. 清除相关缓存 + +--- + +### 6.6 组织架构排序接口 + +#### 需求描述 + +调整同级组织的显示顺序。 + +#### 接口名称 + +组织排序接口 + +#### 设计原因 + +- 组织架构显示需要符合实际层级 +- 重要部门需要前置显示 +- 便于组织管理 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/organizations/{pid}/sort` + +#### 接口参数 + +```typescript +{ + items: Array<{ + id: number; + sortOrder: number; + }>; +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "组织排序更新成功", + "data": { + "parentId": 1, + "updated": 3 + } +} +``` + +#### 开发思路 + +1. 验证所有组织都属于同一父级 +2. 批量更新sortOrder +3. 清除组织架构缓存 +4. 返回更新结果 + +--- + +## 7. 字典管理模块 (Dictionary Management) + +### 7.1 字典类型树查询 + +#### 需求描述 + +查询字典类型的树形结构,支持分类管理。 + +#### 接口名称 + +字典类型树查询接口 + +#### 设计原因 + +- 字典类型支持分类管理 +- 便于字典的组织和查找 +- 区分系统字典和业务字典 + +#### 请求类型 + +GET + +#### 接口路径 + +`/dictionaries/types/tree` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": [ + { + "id": 1, + "code": "system", + "name": "系统字典", + "isSystem": true, + "children": [ + { + "id": 10, + "code": "user_status", + "name": "用户状态", + "itemCount": 4 + } + ] + } + ] +} +``` + +#### 开发思路 + +1. 查询所有字典类型 +2. 构建树形结构 +3. 统计每个类型的字典项数量 +4. 使用Redis缓存 + +--- + +### 7.2 获取字典项 + +#### 需求描述 + +根据字典类型获取所有字典项,支持树形字典。 + +#### 接口名称 + +获取字典项接口 + +#### 设计原因 + +- 前端下拉框等组件需要字典数据 +- 支持动态配置 +- 减少硬编码 + +#### 请求类型 + +GET + +#### 接口路径 + +`/dictionaries/items` + +#### 接口参数 + +```typescript +{ + typeCode: string; // 字典类型代码 + tree?: boolean; // 是否返回树形结构 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": [ + { + "key": "active", + "value": "active", + "label": "正常", + "labelEn": "Active", + "color": "#52c41a", + "sortOrder": 1, + "extra": {} + } + ] +} +``` + +#### 开发思路 + +1. 根据typeCode查询字典类型 +2. 查询该类型下的所有字典项 +3. 如果是树形字典,构建树结构 +4. 使用Redis缓存(字典变更少) + +--- + +### 7.3 创建字典类型 + +#### 需求描述 + +创建新的字典类型。 + +#### 接口名称 + +创建字典类型接口 + +#### 设计原因 + +- 业务扩展需要新字典 +- 灵活的配置管理 +- 支持分类组织 + +#### 请求类型 + +POST + +#### 接口路径 + +`/dictionaries/types` + +#### 接口参数 + +```typescript +{ + code: string; // 字典类型代码 + name: string; // 字典类型名称 + pid?: number; // 父类型ID + description?: string; // 描述 + sortOrder?: number; // 排序号 +} +``` + +#### 开发思路 + +1. 权限检查(需要dictionary:create权限) +2. 验证代码唯一性 +3. 验证父权限存在性 +4. 创建字典类型 +5. 清除字典缓存 + +--- + +### 7.4 批量更新字典项 + +#### 需求描述 + +支持批量更新字典项,保持顺序。 + +#### 接口名称 + +批量更新字典项接口 + +#### 设计原因 + +- 字典项通常需要批量维护 +- 保持显示顺序的一致性 +- 提高维护效率 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/dictionaries/types/{typeId}/items/batch` + +#### 接口参数 + +```typescript +{ + items: Array<{ + id?: number; // 有ID为更新,无ID为新增 + key: string; + value: string; + label: string; + labelEn?: string; + pid?: number; + sortOrder: number; + status?: string; + color?: string; + extra?: object; + }>; +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "批量更新成功", + "data": { + "created": 3, + "updated": 5, + "deleted": 2 + } +} +``` + +#### 开发思路 + +1. 验证字典类型存在 +2. 检查是否为系统字典 +3. 验证key在类型内唯一 +4. 使用事务处理: + - 标记删除不在列表中的项 + - 更新已存在的项 + - 创建新项 +5. 清除字典缓存 + +--- + +### 7.5 导出字典数据 + +#### 需求描述 + +导出字典配置用于备份或迁移。 + +#### 接口名称 + +导出字典数据接口 + +#### 设计原因 + +- 配置备份需求 +- 环境间迁移 +- 批量导入导出 + +#### 请求类型 + +GET + +#### 接口路径 + +`/dictionaries/export` + +#### 接口参数 + +```typescript +{ + typeIds?: number[]; // 指定类型ID,不传则导出全部 + format?: string; // 导出格式:json,excel +} +``` + +#### 响应格式 + +``` +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="dictionaries_20240101.json" +``` + +#### 开发思路 + +1. 查询指定的字典类型和项 +2. 构建导出数据结构 +3. 根据格式生成文件 +4. 返回文件流 + +--- + +### 7.6 字典项排序接口 + +#### 需求描述 + +调整字典项的显示顺序,影响下拉框等组件的选项顺序。 + +#### 接口名称 + +字典项排序接口 + +#### 设计原因 + +- 下拉框选项需要按业务逻辑排序 +- 常用选项前置显示 +- 提升用户操作效率 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/dictionaries/types/{typeId}/items/sort` + +#### 接口参数 + +```typescript +{ + items: Array<{ + id: number; + sortOrder: number; + }>; +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "字典项排序更新成功", + "data": { + "typeId": 1, + "updated": 4 + } +} +``` + +#### 开发思路 + +1. 验证所有字典项都属于指定类型 +2. 批量更新sortOrder +3. 清除字典缓存 +4. 返回更新结果 + +--- + +## 8. 标签管理模块 (Tag Management) + +### 8.1 标签列表查询 + +#### 需求描述 + +查询标签列表,支持按类型和使用频率排序。 + +#### 接口名称 + +标签列表查询接口 + +#### 设计原因 + +- 便于标签的统一管理 +- 支持热门标签展示 +- 不同类型标签分开管理 + +#### 请求类型 + +GET + +#### 接口路径 + +`/tags` + +#### 接口参数 + +```typescript +{ + type?: string; // 标签类型 + keyword?: string; // 搜索关键词 + sortBy?: string; // 排序:usageCount,createdAt + page?: number; + pageSize?: number; +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { + "items": [ + { + "id": 1, + "name": "VIP", + "type": "user", + "color": "#ff4d4f", + "usageCount": 100, + "description": "VIP用户" + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 50 + } + } +} +``` + +#### 开发思路 + +1. 构建查询条件 +2. 执行分页查询 +3. 根据sortBy排序 +4. 返回标签列表 + +--- + +### 8.2 创建标签 + +#### 需求描述 + +创建新的标签。 + +#### 接口名称 + +创建标签接口 + +#### 设计原因 + +- 灵活的标签扩展 +- 支持自定义标签 +- 统一标签管理 + +#### 请求类型 + +POST + +#### 接口路径 + +`/tags` + +#### 接口参数 + +```typescript +{ + name: string; // 标签名称 + type: string; // 标签类型 + color?: string; // 标签颜色 + description?: string; // 描述 +} +``` + +#### 开发思路 + +1. 权限检查(需要tag:create权限) +2. 验证标签名称唯一性(同类型内) +3. 创建标签记录 +4. 返回创建结果 + +--- + +### 8.3 标签使用统计 + +#### 需求描述 + +统计标签使用情况,生成报表。 + +#### 接口名称 + +标签使用统计接口 + +#### 设计原因 + +- 了解标签使用情况 +- 优化标签体系 +- 数据分析支持 + +#### 请求类型 + +GET + +#### 接口路径 + +`/tags/statistics` + +#### 接口参数 + +```typescript +{ + type?: string; // 标签类型 + top?: number; // 返回前N个 + startDate?: string; // 统计开始时间 + endDate?: string; // 统计结束时间 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { + "summary": { + "totalTags": 50, + "totalUsage": 1000, + "avgUsage": 20 + }, + "topTags": [ + { + "id": 1, + "name": "VIP", + "usageCount": 100, + "percentage": 10 + } + ], + "trend": [ + { + "date": "2024-01-01", + "count": 20 + } + ] + } +} +``` + +#### 开发思路 + +1. 统计基础数据 +2. 查询热门标签 +3. 计算使用趋势 +4. 整合返回结果 + +--- + +### 8.4 标签智能推荐 + +#### 需求描述 + +根据用户特征智能推荐标签。 + +#### 接口名称 + +标签推荐接口 + +#### 设计原因 + +- 提高标签使用的准确性 +- 基于规则引擎自动打标 +- 减少人工维护成本 + +#### 请求类型 + +POST + +#### 接口路径 + +`/tags/recommend` + +#### 接口参数 + +```typescript +{ + userId: number; // 用户ID + type: string; // 推荐类型 + limit?: number; // 推荐数量限制 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { + "recommendations": [ + { + "tagId": 1, + "tagName": "活跃用户", + "confidence": 0.95, + "reason": "最近30天登录20次" + } + ] + } +} +``` + +#### 开发思路 + +1. 获取用户特征数据 +2. 执行推荐规则: + - 活跃度规则(登录次数、最后登录时间) + - 贡献度规则(内容创作数量) + - 消费规则(VIP等级) +3. 计算置信度 +4. 返回推荐标签列表 +5. 记录推荐日志 + +--- + +### 8.5 批量打标签 + +#### 需求描述 + +为多个用户批量添加或移除标签。 + +#### 接口名称 + +批量打标签接口 + +#### 设计原因 + +- 提高运营效率 +- 支持批量操作 +- 灵活的标签管理 + +#### 请求类型 + +POST + +#### 接口路径 + +`/tags/batch-assign` + +#### 接口参数 + +```typescript +{ + userIds: number[]; // 用户ID数组 + tagIds: number[]; // 标签ID数组 + action: string; // 操作:add,remove +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "批量操作成功", + "data": { + "success": 50, + "failed": 0, + "errors": [] + } +} +``` + +#### 开发思路 + +1. 权限检查 +2. 验证用户和标签存在性 +3. 批量执行操作 +4. 更新标签使用统计 +5. 返回执行结果 + +--- + +### 8.6 标签排序接口 + +#### 需求描述 + +调整标签的显示顺序,支持热门标签前置。 + +#### 接口名称 + +标签排序接口 + +#### 设计原因 + +- 热门标签需要前置显示 +- 按业务重要性排序 +- 提升标签选择效率 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/tags/sort` + +#### 接口参数 + +```typescript +{ + type?: string; // 标签类型,为空则排序全部 + items: Array<{ + id: number; + sortOrder: number; + }> +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "标签排序更新成功", + "data": { + "type": "user", + "updated": 6 + } +} +``` + +#### 开发思路 + +1. 权限检查(需要tag:sort权限) +2. 验证标签类型一致性 +3. 批量更新sortOrder +4. 更新标签缓存 +5. 返回更新结果 + +--- + +## 9. 通用功能接口 + +### 9.1 图形验证码 + +#### 需求描述 + +生成图形验证码,用于注册、登录等场景。 + +#### 接口名称 + +获取图形验证码接口 + +#### 设计原因 + +- 防止机器人攻击 +- 增强系统安全性 +- 限制恶意请求 + +#### 请求类型 + +GET + +#### 接口路径 + +`/common/captcha` + +#### 接口参数 + +```typescript +{ + type?: string; // 验证码类型:image,math + width?: number; // 图片宽度,默认120 + height?: number; // 图片高度,默认40 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { + "captchaId": "xxxx-xxxx-xxxx", + "captchaImage": "data:image/svg+xml;base64,..." + } +} +``` + +#### 开发思路 + +1. 生成随机验证码文本 +2. 生成SVG或图片 +3. 存储到Redis(5分钟过期) +4. 返回Base64编码的图片 + +--- + +### 9.2 文件上传 + +#### 需求描述 + +通用文件上传接口,支持头像等文件上传。 + +#### 接口名称 + +文件上传接口 + +#### 设计原因 + +- 统一的文件上传入口 +- 支持多种文件类型 +- 安全的文件处理 + +#### 请求类型 + +POST + +#### 接口路径 + +`/common/upload` + +#### 请求格式 + +multipart/form-data + +#### 接口参数 + +```typescript +{ + file: File; // 文件 + type: string; // 文件类型:avatar,document,image + isPublic?: boolean; // 是否公开访问,默认false +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "上传成功", + "data": { + "fileId": "file123456", + "url": "https://oss.starzh.com/xxx.jpg", + "size": 102400, + "name": "avatar.jpg", + "type": "image/jpeg" + } +} +``` + +#### 开发思路 + +1. 验证文件类型和大小 +2. 生成唯一文件名 +3. 上传到OSS或本地存储 +4. 记录文件信息到数据库 +5. 返回文件访问URL + +--- + +### 9.3 邮件发送 + +#### 需求描述 + +统一的邮件发送接口,用于系统通知。 + +#### 接口名称 + +邮件发送接口(内部接口) + +#### 设计原因 + +- 统一邮件发送逻辑 +- 支持模板化 +- 便于监控和管理 + +#### 请求类型 + +POST + +#### 接口路径 + +`/internal/email/send` + +#### 接口参数 + +```typescript +{ + to: string[]; // 收件人邮箱数组 + template: string; // 邮件模板ID + params: object; // 模板参数 + attachments?: Array<{ // 附件 + filename: string; + content: string; + }>; +} +``` + +#### 开发思路 + +1. 加载邮件模板 +2. 渲染模板内容 +3. 调用邮件服务发送 +4. 记录发送日志 +5. 处理发送失败(重试机制) + +--- + +### 9.4 系统健康检查 + +#### 需求描述 + +提供系统健康状态检查接口。 + +#### 接口名称 + +健康检查接口 + +#### 设计原因 + +- 监控系统状态 +- 负载均衡健康检查 +- 快速发现问题 + +#### 请求类型 + +GET + +#### 接口路径 + +`/health` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "success", + "data": { + "status": "healthy", + "version": "1.0.0", + "uptime": 3600, + "services": { + "database": "ok", + "redis": "ok", + "elasticsearch": "ok" + } + } +} +``` + +#### 开发思路 + +1. 检查数据库连接 +2. 检查Redis连接 +3. 检查其他服务状态 +4. 返回综合健康状态 + +--- + +### 9.5 数据导入导出 + +#### 需求描述 + +支持批量数据的导入导出功能。 + +#### 接口名称 + +数据导出接口 + +#### 设计原因 + +- 数据备份需求 +- 批量数据处理 +- 数据迁移支持 + +#### 请求类型 + +POST + +#### 接口路径 + +`/common/export` + +#### 接口参数 + +```typescript +{ + module: string; // 模块:users,roles,organizations + format: string; // 格式:excel,csv,json + filters?: object; // 过滤条件 + fields?: string[]; // 导出字段 +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "导出任务已创建", + "data": { + "taskId": "export-123456", + "status": "processing" + } +} +``` + +#### 开发思路 + +1. 创建导出任务 +2. 异步处理数据查询和转换 +3. 生成导出文件 +4. 通过WebSocket或轮询通知完成 +5. 提供下载链接 + +--- + +### 9.6 通用排序接口 + +#### 需求描述 + +提供通用的排序接口,支持多种数据类型的排序。 + +#### 接口名称 + +通用排序接口 + +#### 设计原因 + +- 统一的排序逻辑 +- 减少代码重复 +- 便于维护和扩展 + +#### 请求类型 + +PUT + +#### 接口路径 + +`/common/sort` + +#### 接口参数 + +```typescript +{ + table: string; // 表名:roles,permissions,organizations,tags + scopeField?: string; // 范围字段:pid,typeId等 + scopeValue?: any; // 范围值 + items: Array<{ + id: number; + sortOrder: number; + }> +} +``` + +#### 响应格式 + +```json +{ + "code": 0, + "message": "排序更新成功", + "data": { + "table": "roles", + "scope": "pid=1", + "updated": 5 + } +} +``` + +#### 开发思路 + +1. 验证表名和字段的安全性 +2. 权限检查(根据表名动态判断) +3. 构建更新SQL +4. 批量更新排序字段 +5. 清除相关缓存 +6. 返回更新结果 + +--- + +## 10. 安全性设计 + +### 10.1 认证机制 + +- **JWT Token认证** + - AccessToken有效期2小时 + - RefreshToken有效期7天 + - Token黑名单机制 + - 支持多设备登录管理 + +### 10.2 权限控制 + +- **基于RBAC的权限模型** + - 接口级别的权限控制 + - 数据级别的权限过滤 + - 动态权限加载 + - 权限缓存优化 + +### 10.3 安全措施 + +- **传输安全** + - 所有接口强制HTTPS + - 敏感数据加密传输 + - 防重放攻击(时间戳+nonce) + +- **输入验证** + - 参数类型和格式验证 + - SQL注入防护(参数化查询) + - XSS防护(输入过滤和转义) + - 文件上传安全检查 + +- **访问控制** + - IP白名单(管理接口) + - 请求频率限制 + - 并发连接限制 + - 异常行为检测 + +### 10.4 日志审计 + +- **操作日志** + - 记录所有写操作 + - 包含用户、时间、IP、操作内容 + - 敏感操作实时告警 + +- **安全日志** + - 登录失败记录 + - 权限拒绝记录 + - 异常请求记录 + +--- + +## 11. 性能优化 + +### 11.1 缓存策略 + +| 缓存类型 | 缓存内容 | 过期时间 | 更新策略 | +| ------------ | -------------------- | -------- | ----------------- | +| 用户信息缓存 | 基本信息、角色、权限 | 5分钟 | 主动更新+过期刷新 | +| 角色权限缓存 | 角色树、权限快照 | 1小时 | 变更时删除 | +| 字典缓存 | 字典类型和项 | 24小时 | 变更时删除 | +| 验证码缓存 | 图形验证码 | 5分钟 | 使用后删除 | + +### 11.2 查询优化 + +- **索引优化** + - 基于查询模式设计索引 + - 定期分析慢查询日志 + - 避免全表扫描 + +- **查询优化** + - 使用预编译语句 + - 避免N+1查询 + - 合理使用JOIN和子查询 + - 大数据量分页优化 + +### 11.3 并发控制 + +- **限流策略** + - 接口级别限流(令牌桶算法) + - 用户级别限流 + - IP级别限流 + +- **资源控制** + - 数据库连接池管理 + - Redis连接池管理 + - 异步任务队列 + +### 11.4 异步处理 + +- **异步任务** + - 邮件发送 + - 文件处理 + - 数据导入导出 + - 统计计算 + +--- + +## 12. 错误处理 + +### 12.1 错误码规范 + +| 错误码范围 | 说明 | 示例 | +| ---------- | ---------- | ------------------ | +| 0 | 成功 | 操作成功 | +| 400xx | 客户端错误 | 40001-参数错误 | +| 401xx | 认证错误 | 40101-未登录 | +| 403xx | 权限错误 | 40301-无权限 | +| 404xx | 资源不存在 | 40401-用户不存在 | +| 409xx | 资源冲突 | 40901-用户名已存在 | +| 423xx | 资源锁定 | 42301-账号被锁定 | +| 429xx | 请求过多 | 42901-请求频率超限 | +| 500xx | 服务器错误 | 50001-内部错误 | +| 503xx | 服务不可用 | 50301-服务维护中 | + +### 12.2 错误响应格式 + +```json +{ + "code": 40001, + "message": "参数验证失败:用户名长度必须在3-50个字符之间", + "data": { + "field": "username", + "value": "ab", + "rule": "minLength" + } +} +``` + +### 12.3 错误处理最佳实践 + +- **友好的错误提示** + - 使用用户能理解的语言 + - 提供解决建议 + - 避免暴露技术细节 + +- **错误日志记录** + - 记录完整的错误堆栈 + - 包含请求上下文 + - 分级记录(info/warn/error) + +- **错误监控告警** + - 错误率监控 + - 关键错误实时告警 + - 错误趋势分析 + +--- + +## 13. 接口版本管理 + +### 13.1 版本策略 + +- **URL版本化** + - 格式:`/api/v{n}/...` + - 主版本号变更表示不兼容更新 + - 向后兼容原则 + +### 13.2 版本迁移 + +- **废弃通知** + - 提前3个月通知 + - 响应头添加废弃警告 + - 提供迁移文档 + +- **版本共存** + - 新旧版本并行运行 + - 逐步迁移用户 + - 设置旧版本下线时间 + +--- + +## 14. 开发规范总结 + +### 14.1 接口设计原则 + +- **RESTful规范** + - 使用标准HTTP方法 + - 资源导向的URL设计 + - 合理使用状态码 + +- **一致性原则** + - 统一的命名规范 + - 统一的响应格式 + - 统一的错误处理 + +### 14.2 最佳实践 + +- **幂等性设计** + - GET/PUT/DELETE操作幂等 + - POST操作防重复提交 + +- **分页设计** + - 统一分页参数 + - 限制最大返回条数 + - 提供总数统计 + +- **数据安全** + - 敏感数据脱敏 + - 防止信息泄露 + - 输入验证和过滤 + +### 14.3 文档规范 + +- **接口文档** + - 使用Swagger自动生成 + - 包含请求和响应示例 + - 说明业务场景 + +- **变更记录** + - 记录所有API变更 + - 说明变更原因 + - 提供升级指南 + +--- + +## 15. 附录 + +### 15.1 常用正则表达式 + +```javascript +// 用户名:3-50位字母数字下划线,字母开头 +const USERNAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_]{2,49}$/; + +// 邮箱 +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +// 手机号(中国大陆) +const MOBILE_REGEX = /^1[3-9]\d{9}$/; + +// 强密码:8-100位,包含大小写字母和数字 +const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,100}$/; +``` + +### 15.2 时间格式约定 + +- 统一使用ISO 8601格式 +- 示例:`2024-01-01T12:00:00Z` +- 时区:统一使用UTC时间 + +### 15.3 分页参数约定 + +- `page`: 页码,从1开始 +- `pageSize`: 每页条数,默认20,最大100 +- `total`: 总记录数 +- `totalPages`: 总页数 + +--- + +本接口设计文档为M2基础用户系统的完整API规范,后续开发应严格遵循本文档的设计要求。 diff --git a/prd/M2-基础用户系统-数据库设计.md b/prd/M2-基础用户系统-数据库设计.md index 805a816..acd8659 100644 --- a/prd/M2-基础用户系统-数据库设计.md +++ b/prd/M2-基础用户系统-数据库设计.md @@ -1,922 +1,925 @@ -# M2 - 基础用户系统 - 数据库设计文档 - -## 1. 概述 - -本文档为星撰个人综合平台的基础用户系统(M2阶段)提供详细的数据库设计方案。设计遵循以下原则: - -- **无外键约束**:所有关联关系通过应用层代码维护,提高性能和灵活性 -- **软删除机制**:所有业务表支持软删除,保留数据历史 -- **树形结构支持**:角色、组织、权限、字典等支持无限层级的树形结构 -- **审计追踪**:完整的创建、修改记录 -- **高性能**:合理的索引设计,支持大数据量查询 -- **扩展性**:预留扩展字段,支持未来功能演进 - -## 2. 数据库 ER 图 - -```mermaid -erDiagram - %% 用户相关 - sys_users { - bigint id PK "主键,雪花ID" - varchar(50) username UK "用户名" - varchar(100) email UK "邮箱" - varchar(20) mobile "手机号" - varchar(255) password_hash "密码哈希" - varchar(255) avatar "头像URL" - varchar(100) nickname "昵称" - varchar(20) status "状态:字典[user_status]" - tinyint gender "性别:0未知,1男,2女" - date birthday "生日" - varchar(500) bio "个人简介" - int login_count "登录次数" - datetime last_login_at "最后登录时间" - varchar(45) last_login_ip "最后登录IP" - int failed_attempts "失败尝试次数" - datetime locked_until "锁定截止时间" - boolean is_root "是否超级管理员" - json extra "扩展信息JSON" - bigint created_by "创建人" - datetime created_at "创建时间" - bigint updated_by "更新人" - datetime updated_at "更新时间" - datetime deleted_at "删除时间" - int version "乐观锁版本号" - } - - %% 角色相关 - sys_roles { - bigint id PK "主键" - varchar(50) code UK "角色代码" - varchar(100) name "角色名称" - text description "角色描述" - bigint pid "父角色ID" - varchar(500) path "层级路径,如:/1/2/3/" - int level "层级深度" - int sort_order "排序号" - varchar(20) status "状态:字典[role_status]" - boolean is_system "是否系统角色" - json permissions_snapshot "权限快照" - json extra "扩展信息" - bigint created_by "创建人" - datetime created_at "创建时间" - bigint updated_by "更新人" - datetime updated_at "更新时间" - datetime deleted_at "删除时间" - int version "版本号" - } - - %% 权限相关 - sys_permissions { - bigint id PK "主键" - varchar(100) code UK "权限代码" - varchar(100) name "权限名称" - varchar(20) type "类型:menu,button,api,data" - varchar(50) resource "资源标识" - varchar(50) action "操作:read,write,delete等" - text description "权限描述" - bigint pid "父权限ID" - varchar(500) path "层级路径" - int level "层级深度" - int sort_order "排序号" - varchar(20) status "状态" - json meta "元数据" - bigint created_by "创建人" - datetime created_at "创建时间" - bigint updated_by "更新人" - datetime updated_at "更新时间" - datetime deleted_at "删除时间" - } - - %% 组织架构 - sys_organizations { - bigint id PK "主键" - varchar(100) code UK "组织代码" - varchar(200) name "组织名称" - varchar(200) full_name "组织全称" - text description "组织描述" - bigint pid "父组织ID" - varchar(500) path "层级路径" - int level "层级深度" - varchar(20) type "类型:字典[org_type]" - varchar(20) status "状态:字典[org_status]" - int sort_order "排序号" - varchar(100) leader_id "负责人ID" - varchar(200) address "地址" - varchar(50) phone "电话" - json extra "扩展信息" - bigint created_by "创建人" - datetime created_at "创建时间" - bigint updated_by "更新人" - datetime updated_at "更新时间" - datetime deleted_at "删除时间" - int version "版本号" - } - - %% 字典管理 - sys_dict_types { - bigint id PK "主键" - varchar(50) code UK "字典类型代码" - varchar(100) name "字典类型名称" - text description "描述" - bigint pid "父字典类型ID" - varchar(500) path "层级路径" - int level "层级深度" - varchar(20) status "状态:active,inactive" - boolean is_system "是否系统字典" - int sort_order "排序号" - bigint created_by "创建人" - datetime created_at "创建时间" - bigint updated_by "更新人" - datetime updated_at "更新时间" - datetime deleted_at "删除时间" - } - - sys_dict_items { - bigint id PK "主键" - bigint type_id "字典类型ID" - varchar(50) item_key "字典项键" - varchar(200) item_value "字典项值" - varchar(100) label "显示标签" - varchar(200) label_en "英文标签" - text description "描述" - bigint pid "父字典项ID" - varchar(500) path "层级路径" - int level "层级深度" - int sort_order "排序号" - varchar(20) status "状态" - varchar(50) css_class "样式类" - varchar(50) color "颜色值" - json extra "扩展属性" - bigint created_by "创建人" - datetime created_at "创建时间" - bigint updated_by "更新人" - datetime updated_at "更新时间" - datetime deleted_at "删除时间" - } - - %% 标签管理 - sys_tags { - bigint id PK "主键" - varchar(50) name UK "标签名称" - varchar(50) type "标签类型:user,role,content" - varchar(50) color "标签颜色" - text description "描述" - int usage_count "使用次数" - bigint created_by "创建人" - datetime created_at "创建时间" - datetime deleted_at "删除时间" - } - - %% 关联表 - sys_user_roles { - bigint id PK "主键" - bigint user_id "用户ID" - bigint role_id "角色ID" - datetime expired_at "过期时间" - bigint created_by "创建人" - datetime created_at "创建时间" - } - - sys_role_permissions { - bigint id PK "主键" - bigint role_id "角色ID" - bigint permission_id "权限ID" - boolean is_half "是否半选(树形权限)" - bigint created_by "创建人" - datetime created_at "创建时间" - } - - sys_user_organizations { - bigint id PK "主键" - bigint user_id "用户ID" - bigint organization_id "组织ID" - boolean is_primary "是否主组织" - varchar(100) position "职位" - datetime joined_at "加入时间" - bigint created_by "创建人" - datetime created_at "创建时间" - } - - sys_user_tags { - bigint id PK "主键" - bigint user_id "用户ID" - bigint tag_id "标签ID" - bigint created_by "创建人" - datetime created_at "创建时间" - } - - %% 操作日志 - sys_operation_logs { - bigint id PK "主键" - bigint user_id "用户ID" - varchar(100) username "用户名" - varchar(50) module "模块" - varchar(50) action "操作" - varchar(200) target "操作对象" - bigint target_id "对象ID" - text request_data "请求数据" - text response_data "响应数据" - varchar(20) status "状态:success,fail" - varchar(45) ip "IP地址" - varchar(200) user_agent "用户代理" - bigint duration "耗时(ms)" - text error_msg "错误信息" - datetime created_at "创建时间" - } - - %% 关系定义 - sys_users ||--o{ sys_user_roles : "拥有" - sys_roles ||--o{ sys_user_roles : "被分配" - sys_roles ||--o{ sys_role_permissions : "拥有" - sys_permissions ||--o{ sys_role_permissions : "被分配" - sys_users ||--o{ sys_user_organizations : "属于" - sys_organizations ||--o{ sys_user_organizations : "包含" - sys_users ||--o{ sys_user_tags : "拥有" - sys_tags ||--o{ sys_user_tags : "被使用" - sys_dict_types ||--o{ sys_dict_items : "包含" - sys_roles ||--o| sys_roles : "继承自" - sys_organizations ||--o| sys_organizations : "隶属于" - sys_permissions ||--o| sys_permissions : "包含" - sys_dict_items ||--o| sys_dict_items : "子项" - sys_dict_types ||--o| sys_dict_types : "包含" -``` - -## 3. 表结构详细设计 - -### 3.1 用户表 (sys_users) - -用户系统的核心表,存储用户基本信息和认证信息。 - -```sql --- 表结构 -CREATE TABLE `sys_users` ( - `id` BIGINT NOT NULL COMMENT '主键,雪花ID', - `username` VARCHAR(50) NOT NULL COMMENT '用户名,唯一', - `email` VARCHAR(100) NOT NULL COMMENT '邮箱,唯一', - `mobile` VARCHAR(20) NULL COMMENT '手机号', - `password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希值', - `avatar` VARCHAR(255) NULL COMMENT '头像URL', - `nickname` VARCHAR(100) NULL COMMENT '昵称', - `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-正常,inactive-未激活,locked-锁定,disabled-禁用', - `gender` TINYINT NULL DEFAULT 0 COMMENT '性别:0-未知,1-男,2-女', - `birthday` DATE NULL COMMENT '生日', - `bio` VARCHAR(500) NULL COMMENT '个人简介', - `login_count` INT NOT NULL DEFAULT 0 COMMENT '登录次数', - `last_login_at` DATETIME NULL COMMENT '最后登录时间', - `last_login_ip` VARCHAR(45) NULL COMMENT '最后登录IP', - `failed_attempts` INT NOT NULL DEFAULT 0 COMMENT '连续失败尝试次数', - `locked_until` DATETIME NULL COMMENT '锁定截止时间', - `is_root` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否超级管理员', - `extra` JSON NULL COMMENT '扩展信息,JSON格式', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_by` BIGINT NULL COMMENT '更新人ID', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted_at` DATETIME NULL COMMENT '删除时间,软删除标记', - `version` INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_username` ON `sys_users` (`username`, `deleted_at`); -CREATE UNIQUE INDEX `uk_email` ON `sys_users` (`email`, `deleted_at`); -CREATE INDEX `idx_mobile` ON `sys_users` (`mobile`); -CREATE INDEX `idx_status` ON `sys_users` (`status`); -CREATE INDEX `idx_created_at` ON `sys_users` (`created_at`); -CREATE INDEX `idx_deleted_at` ON `sys_users` (`deleted_at`); -CREATE INDEX `idx_is_root` ON `sys_users` (`is_root`); -CREATE INDEX `idx_last_login` ON `sys_users` (`last_login_at`); -``` - -### 3.2 角色表 (sys_roles) - -支持树形结构的角色管理表,实现角色继承。 - -```sql --- 表结构 -CREATE TABLE `sys_roles` ( - `id` BIGINT NOT NULL COMMENT '主键', - `code` VARCHAR(50) NOT NULL COMMENT '角色代码,唯一', - `name` VARCHAR(100) NOT NULL COMMENT '角色名称', - `description` TEXT NULL COMMENT '角色描述', - `pid` BIGINT NULL DEFAULT 0 COMMENT '父角色ID,0表示顶级', - `path` VARCHAR(500) NULL COMMENT '层级路径,如:/1/2/3/', - `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', - `sort_order` INT NOT NULL DEFAULT 0 COMMENT '同级排序号', - `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-启用,inactive-禁用', - `is_system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否系统内置角色', - `permissions_snapshot` JSON NULL COMMENT '权限快照,用于优化查询', - `extra` JSON NULL COMMENT '扩展信息', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_by` BIGINT NULL COMMENT '更新人ID', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted_at` DATETIME NULL COMMENT '删除时间', - `version` INT NOT NULL DEFAULT 1 COMMENT '版本号', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_code` ON `sys_roles` (`code`, `deleted_at`); -CREATE INDEX `idx_name` ON `sys_roles` (`name`); -CREATE INDEX `idx_pid` ON `sys_roles` (`pid`); -CREATE INDEX `idx_path` ON `sys_roles` (`path`); -CREATE INDEX `idx_status` ON `sys_roles` (`status`); -CREATE INDEX `idx_deleted_at` ON `sys_roles` (`deleted_at`); -CREATE INDEX `idx_is_system` ON `sys_roles` (`is_system`); -CREATE INDEX `idx_sort` ON `sys_roles` (`pid`, `sort_order`); -``` - -### 3.3 权限表 (sys_permissions) - -细粒度的权限定义表,支持多种权限类型。 - -```sql --- 表结构 -CREATE TABLE `sys_permissions` ( - `id` BIGINT NOT NULL COMMENT '主键', - `code` VARCHAR(100) NOT NULL COMMENT '权限代码,唯一', - `name` VARCHAR(100) NOT NULL COMMENT '权限名称', - `type` VARCHAR(20) NOT NULL COMMENT '权限类型:menu-菜单,button-按钮,api-接口,data-数据', - `resource` VARCHAR(50) NULL COMMENT '资源标识,如:user,role,post', - `action` VARCHAR(50) NULL COMMENT '操作标识,如:read,create,update,delete', - `description` TEXT NULL COMMENT '权限描述', - `pid` BIGINT NULL DEFAULT 0 COMMENT '父权限ID', - `path` VARCHAR(500) NULL COMMENT '层级路径', - `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', - `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', - `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态', - `meta` JSON NULL COMMENT '元数据,如:图标、路由等', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_by` BIGINT NULL COMMENT '更新人ID', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted_at` DATETIME NULL COMMENT '删除时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_code` ON `sys_permissions` (`code`, `deleted_at`); -CREATE INDEX `idx_type` ON `sys_permissions` (`type`); -CREATE INDEX `idx_resource_action` ON `sys_permissions` (`resource`, `action`); -CREATE INDEX `idx_pid` ON `sys_permissions` (`pid`); -CREATE INDEX `idx_deleted_at` ON `sys_permissions` (`deleted_at`); -CREATE INDEX `idx_status` ON `sys_permissions` (`status`); -CREATE INDEX `idx_sort` ON `sys_permissions` (`pid`, `sort_order`); -``` - -### 3.4 组织架构表 (sys_organizations) - -支持多级组织结构,用于用户归属管理。 - -```sql --- 表结构 -CREATE TABLE `sys_organizations` ( - `id` BIGINT NOT NULL COMMENT '主键', - `code` VARCHAR(100) NOT NULL COMMENT '组织代码,唯一', - `name` VARCHAR(200) NOT NULL COMMENT '组织名称', - `full_name` VARCHAR(200) NULL COMMENT '组织全称', - `description` TEXT NULL COMMENT '组织描述', - `pid` BIGINT NULL DEFAULT 0 COMMENT '父组织ID', - `path` VARCHAR(500) NULL COMMENT '层级路径', - `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', - `type` VARCHAR(20) NULL COMMENT '组织类型:company,department,team', - `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态', - `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', - `leader_id` BIGINT NULL COMMENT '负责人ID', - `address` VARCHAR(200) NULL COMMENT '地址', - `phone` VARCHAR(50) NULL COMMENT '联系电话', - `extra` JSON NULL COMMENT '扩展信息', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_by` BIGINT NULL COMMENT '更新人ID', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted_at` DATETIME NULL COMMENT '删除时间', - `version` INT NOT NULL DEFAULT 1 COMMENT '版本号', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织架构表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_code` ON `sys_organizations` (`code`, `deleted_at`); -CREATE INDEX `idx_name` ON `sys_organizations` (`name`); -CREATE INDEX `idx_pid` ON `sys_organizations` (`pid`); -CREATE INDEX `idx_path` ON `sys_organizations` (`path`); -CREATE INDEX `idx_type` ON `sys_organizations` (`type`); -CREATE INDEX `idx_leader_id` ON `sys_organizations` (`leader_id`); -CREATE INDEX `idx_deleted_at` ON `sys_organizations` (`deleted_at`); -CREATE INDEX `idx_status` ON `sys_organizations` (`status`); -CREATE INDEX `idx_sort` ON `sys_organizations` (`pid`, `sort_order`); -``` - -### 3.5 字典类型表 (sys_dict_types) - -管理系统中的各类数据字典,支持树形结构。 - -```sql --- 表结构 -CREATE TABLE `sys_dict_types` ( - `id` BIGINT NOT NULL COMMENT '主键', - `code` VARCHAR(50) NOT NULL COMMENT '字典类型代码,唯一', - `name` VARCHAR(100) NOT NULL COMMENT '字典类型名称', - `description` TEXT NULL COMMENT '描述', - `pid` BIGINT NULL DEFAULT 0 COMMENT '父字典类型ID,支持字典分类', - `path` VARCHAR(500) NULL COMMENT '层级路径', - `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', - `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-启用,inactive-禁用', - `is_system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否系统内置', - `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_by` BIGINT NULL COMMENT '更新人ID', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted_at` DATETIME NULL COMMENT '删除时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典类型表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_code` ON `sys_dict_types` (`code`, `deleted_at`); -CREATE INDEX `idx_name` ON `sys_dict_types` (`name`); -CREATE INDEX `idx_pid` ON `sys_dict_types` (`pid`); -CREATE INDEX `idx_path` ON `sys_dict_types` (`path`); -CREATE INDEX `idx_status` ON `sys_dict_types` (`status`); -CREATE INDEX `idx_deleted_at` ON `sys_dict_types` (`deleted_at`); -CREATE INDEX `idx_is_system` ON `sys_dict_types` (`is_system`); -CREATE INDEX `idx_sort` ON `sys_dict_types` (`pid`, `sort_order`); -``` - -### 3.6 字典项表 (sys_dict_items) - -存储具体的字典数据,支持树形结构。 - -```sql --- 表结构 -CREATE TABLE `sys_dict_items` ( - `id` BIGINT NOT NULL COMMENT '主键', - `type_id` BIGINT NOT NULL COMMENT '字典类型ID', - `item_key` VARCHAR(50) NOT NULL COMMENT '字典项键', - `item_value` VARCHAR(200) NOT NULL COMMENT '字典项值', - `label` VARCHAR(100) NOT NULL COMMENT '显示标签', - `label_en` VARCHAR(200) NULL COMMENT '英文标签', - `description` TEXT NULL COMMENT '描述', - `pid` BIGINT NULL DEFAULT 0 COMMENT '父字典项ID,支持树形字典', - `path` VARCHAR(500) NULL COMMENT '层级路径', - `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', - `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', - `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态', - `css_class` VARCHAR(50) NULL COMMENT 'CSS样式类', - `color` VARCHAR(50) NULL COMMENT '颜色值,如:#FF0000', - `extra` JSON NULL COMMENT '扩展属性', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `updated_by` BIGINT NULL COMMENT '更新人ID', - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `deleted_at` DATETIME NULL COMMENT '删除时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典项表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_type_key` ON `sys_dict_items` (`type_id`, `item_key`, `deleted_at`); -CREATE INDEX `idx_type_id` ON `sys_dict_items` (`type_id`); -CREATE INDEX `idx_pid` ON `sys_dict_items` (`pid`); -CREATE INDEX `idx_status` ON `sys_dict_items` (`status`); -CREATE INDEX `idx_deleted_at` ON `sys_dict_items` (`deleted_at`); -CREATE INDEX `idx_sort` ON `sys_dict_items` (`type_id`, `sort_order`); -CREATE INDEX `idx_key` ON `sys_dict_items` (`item_key`); -``` - -### 3.7 标签表 (sys_tags) - -灵活的标签系统,可用于用户、内容等多种场景。 - -```sql --- 表结构 -CREATE TABLE `sys_tags` ( - `id` BIGINT NOT NULL COMMENT '主键', - `name` VARCHAR(50) NOT NULL COMMENT '标签名称', - `type` VARCHAR(50) NULL DEFAULT 'user' COMMENT '标签类型:user-用户标签,role-角色标签,content-内容标签', - `color` VARCHAR(50) NULL COMMENT '标签颜色,如:#FF0000', - `description` TEXT NULL COMMENT '描述', - `usage_count` INT NOT NULL DEFAULT 0 COMMENT '使用次数统计', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `deleted_at` DATETIME NULL COMMENT '删除时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_name_type` ON `sys_tags` (`name`, `type`, `deleted_at`); -CREATE INDEX `idx_type` ON `sys_tags` (`type`); -CREATE INDEX `idx_usage_count` ON `sys_tags` (`usage_count` DESC); -CREATE INDEX `idx_deleted_at` ON `sys_tags` (`deleted_at`); -CREATE INDEX `idx_name` ON `sys_tags` (`name`); -``` - -### 3.8 用户角色关联表 (sys_user_roles) - -```sql --- 表结构 -CREATE TABLE `sys_user_roles` ( - `id` BIGINT NOT NULL COMMENT '主键', - `user_id` BIGINT NOT NULL COMMENT '用户ID', - `role_id` BIGINT NOT NULL COMMENT '角色ID', - `expired_at` DATETIME NULL COMMENT '过期时间,NULL表示永久', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_user_role` ON `sys_user_roles` (`user_id`, `role_id`); -CREATE INDEX `idx_user_id` ON `sys_user_roles` (`user_id`); -CREATE INDEX `idx_role_id` ON `sys_user_roles` (`role_id`); -CREATE INDEX `idx_expired_at` ON `sys_user_roles` (`expired_at`); -CREATE INDEX `idx_created_at` ON `sys_user_roles` (`created_at`); -``` - -### 3.9 角色权限关联表 (sys_role_permissions) - -```sql --- 表结构 -CREATE TABLE `sys_role_permissions` ( - `id` BIGINT NOT NULL COMMENT '主键', - `role_id` BIGINT NOT NULL COMMENT '角色ID', - `permission_id` BIGINT NOT NULL COMMENT '权限ID', - `is_half` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否半选状态(树形权限)', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色权限关联表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_role_permission` ON `sys_role_permissions` (`role_id`, `permission_id`); -CREATE INDEX `idx_role_id` ON `sys_role_permissions` (`role_id`); -CREATE INDEX `idx_permission_id` ON `sys_role_permissions` (`permission_id`); -CREATE INDEX `idx_is_half` ON `sys_role_permissions` (`is_half`); -``` - -### 3.10 用户组织关联表 (sys_user_organizations) - -```sql --- 表结构 -CREATE TABLE `sys_user_organizations` ( - `id` BIGINT NOT NULL COMMENT '主键', - `user_id` BIGINT NOT NULL COMMENT '用户ID', - `organization_id` BIGINT NOT NULL COMMENT '组织ID', - `is_primary` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否主组织', - `position` VARCHAR(100) NULL COMMENT '职位', - `joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户组织关联表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_user_org` ON `sys_user_organizations` (`user_id`, `organization_id`); -CREATE INDEX `idx_user_id` ON `sys_user_organizations` (`user_id`); -CREATE INDEX `idx_organization_id` ON `sys_user_organizations` (`organization_id`); -CREATE INDEX `idx_is_primary` ON `sys_user_organizations` (`is_primary`); -CREATE INDEX `idx_joined_at` ON `sys_user_organizations` (`joined_at`); -``` - -### 3.11 用户标签关联表 (sys_user_tags) - -```sql --- 表结构 -CREATE TABLE `sys_user_tags` ( - `id` BIGINT NOT NULL COMMENT '主键', - `user_id` BIGINT NOT NULL COMMENT '用户ID', - `tag_id` BIGINT NOT NULL COMMENT '标签ID', - `created_by` BIGINT NULL COMMENT '创建人ID', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户标签关联表'; - --- 索引设计 -CREATE UNIQUE INDEX `uk_user_tag` ON `sys_user_tags` (`user_id`, `tag_id`); -CREATE INDEX `idx_user_id` ON `sys_user_tags` (`user_id`); -CREATE INDEX `idx_tag_id` ON `sys_user_tags` (`tag_id`); -CREATE INDEX `idx_created_at` ON `sys_user_tags` (`created_at`); -``` - -### 3.12 操作日志表 (sys_operation_logs) - -记录所有重要操作,用于审计和问题追踪。 - -```sql --- 表结构 -CREATE TABLE `sys_operation_logs` ( - `id` BIGINT NOT NULL COMMENT '主键', - `user_id` BIGINT NULL COMMENT '操作用户ID', - `username` VARCHAR(100) NULL COMMENT '操作用户名', - `module` VARCHAR(50) NOT NULL COMMENT '操作模块', - `action` VARCHAR(50) NOT NULL COMMENT '操作类型', - `target` VARCHAR(200) NULL COMMENT '操作对象描述', - `target_id` BIGINT NULL COMMENT '操作对象ID', - `request_data` TEXT NULL COMMENT '请求数据', - `response_data` TEXT NULL COMMENT '响应数据', - `status` VARCHAR(20) NOT NULL COMMENT '操作状态:success-成功,fail-失败', - `ip` VARCHAR(45) NULL COMMENT 'IP地址', - `user_agent` VARCHAR(200) NULL COMMENT '用户代理', - `duration` BIGINT NULL COMMENT '操作耗时(毫秒)', - `error_msg` TEXT NULL COMMENT '错误信息', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表'; - --- 索引设计 -CREATE INDEX `idx_user_id` ON `sys_operation_logs` (`user_id`); -CREATE INDEX `idx_module_action` ON `sys_operation_logs` (`module`, `action`); -CREATE INDEX `idx_target` ON `sys_operation_logs` (`target_id`); -CREATE INDEX `idx_status` ON `sys_operation_logs` (`status`); -CREATE INDEX `idx_created_at` ON `sys_operation_logs` (`created_at`); -CREATE INDEX `idx_ip` ON `sys_operation_logs` (`ip`); --- 分区建议:按月分区 -ALTER TABLE `sys_operation_logs` PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) ( - PARTITION p202401 VALUES LESS THAN (202402), - PARTITION p202402 VALUES LESS THAN (202403), - -- 继续添加更多分区... - PARTITION p_future VALUES LESS THAN MAXVALUE -); -``` - -## 4. 设计特点说明 - -### 4.1 软删除机制 - -- 所有业务表都包含 `deleted_at` 字段 -- 删除操作只更新 `deleted_at` 为当前时间 -- 唯一索引都包含 `deleted_at`,确保已删除数据不影响唯一性 -- 查询时默认过滤 `deleted_at IS NULL` -- 提供回收站功能,可恢复误删数据 - -### 4.2 树形结构支持 - -- 使用 `pid` 存储父节点ID,0或NULL表示顶级节点 -- `path` 字段存储完整路径(如:/1/2/3/),便于查询所有子节点 -- `level` 记录层级深度,优化层级查询 -- 支持无限层级嵌套 -- 提供递归查询的存储过程优化性能 - -### 4.3 审计追踪 - -- 所有表包含 `created_by`、`created_at`、`updated_by`、`updated_at` -- 重要操作记录到 `sys_operation_logs` 表 -- 支持数据变更历史追踪 -- 可配置敏感操作的详细日志记录 - -### 4.4 性能优化 - -- 合理的索引设计,覆盖常用查询场景 -- 使用JSON字段存储扩展信息,减少表结构变更 -- 权限快照机制,避免复杂的联表查询 -- 日志表按月分区,提高查询效率 -- 高频查询数据缓存到Redis - -### 4.5 扩展性设计 - -- 预留 `extra` JSON字段,支持灵活扩展 -- 版本号字段支持乐观锁 -- 标签系统支持多种业务场景 -- 字典系统支持动态配置 -- 支持插件化扩展新功能 - -### 4.6 数据完整性 - -- 虽然不使用外键,但通过应用层保证引用完整性 -- 删除操作需级联处理关联数据 -- 使用事务保证操作原子性 -- 定期数据一致性检查任务 - -### 4.7 安全性设计 - -- 密码使用bcrypt算法加密,成本因子不低于12 -- 登录失败次数限制,防止暴力破解 -- 操作日志记录IP和UserAgent,便于审计 -- 敏感数据(如手机号)部分存储时脱敏 -- SQL注入防护通过参数化查询实现 - -## 5. 初始化数据 - -### 5.1 系统初始用户 - -```sql --- 创建root超级管理员 -INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `is_root`, `status`) -VALUES (1, 'root', 'root@system.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYpfQaXUIkRrPJK', '超级管理员', TRUE, 'active'); - --- 创建系统管理员 -INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `status`) -VALUES (2, 'admin', 'admin@system.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYpfQaXUIkRrPJK', '系统管理员', 'active'); -``` - -### 5.2 系统初始角色 - -```sql --- 超级管理员角色 -INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) -VALUES (1, 'super_admin', '超级管理员', '拥有系统所有权限', TRUE, 'active'); - --- 系统管理员角色 -INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`, `pid`, `path`, `level`) -VALUES (2, 'admin', '系统管理员', '负责系统配置和用户管理', TRUE, 'active', 1, '/1/2/', 2); - --- 普通用户角色 -INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) -VALUES (3, 'user', '普通用户', '普通注册用户默认角色', TRUE, 'active'); - --- 分配角色 -INSERT INTO `sys_user_roles` (`user_id`, `role_id`) VALUES (1, 1); -INSERT INTO `sys_user_roles` (`user_id`, `role_id`) VALUES (2, 2); -``` - -### 5.3 基础字典数据 - -```sql --- 字典分类 -INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`) VALUES -(1, 'system', '系统字典', TRUE), -(2, 'business', '业务字典', TRUE); - --- 用户状态字典 -INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES -(10, 'user_status', '用户状态', TRUE, 1, '/1/10/'); - -INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `color`, `sort_order`) VALUES -(10, 'active', 'active', '正常', '#52c41a', 1), -(10, 'inactive', 'inactive', '未激活', '#faad14', 2), -(10, 'locked', 'locked', '锁定', '#ff4d4f', 3), -(10, 'disabled', 'disabled', '禁用', '#d9d9d9', 4); - --- 组织类型字典 -INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES -(11, 'org_type', '组织类型', TRUE, 1, '/1/11/'); - -INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES -(11, 'company', 'company', '公司', 1), -(11, 'department', 'department', '部门', 2), -(11, 'team', 'team', '团队', 3), -(11, 'group', 'group', '小组', 4); - --- 性别字典 -INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES -(12, 'gender', '性别', TRUE, 1, '/1/12/'); - -INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES -(12, '0', '0', '未知', 1), -(12, '1', '1', '男', 2), -(12, '2', '2', '女', 3); - --- 权限类型字典 -INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES -(13, 'permission_type', '权限类型', TRUE, 1, '/1/13/'); - -INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `color`, `sort_order`) VALUES -(13, 'menu', 'menu', '菜单', '#1890ff', 1), -(13, 'button', 'button', '按钮', '#52c41a', 2), -(13, 'api', 'api', '接口', '#fa8c16', 3), -(13, 'data', 'data', '数据', '#722ed1', 4); - --- 标签类型字典 -INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES -(14, 'tag_type', '标签类型', TRUE, 1, '/1/14/'); - -INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES -(14, 'user', 'user', '用户标签', 1), -(14, 'role', 'role', '角色标签', 2), -(14, 'content', 'content', '内容标签', 3); - --- 地区字典(示例) -INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES -(20, 'region', '地区', TRUE, 2, '/2/20/'); - -INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES -(20, 'CN', 'CN', '中国', 1); - -INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `pid`, `path`, `sort_order`) VALUES -(20, 'CN_BJ', 'CN_BJ', '北京', (SELECT id FROM sys_dict_items WHERE item_key = 'CN'), NULL, 1), -(20, 'CN_SH', 'CN_SH', '上海', (SELECT id FROM sys_dict_items WHERE item_key = 'CN'), NULL, 2); -``` - -### 5.4 基础权限数据 - -```sql --- 系统管理权限 -INSERT INTO `sys_permissions` (`id`, `code`, `name`, `type`, `resource`, `action`) VALUES -(1, 'system:manage', '系统管理', 'menu', 'system', 'manage'), -(2, 'user:read', '查看用户', 'api', 'user', 'read'), -(3, 'user:create', '创建用户', 'api', 'user', 'create'), -(4, 'user:update', '更新用户', 'api', 'user', 'update'), -(5, 'user:delete', '删除用户', 'api', 'user', 'delete'), -(6, 'role:read', '查看角色', 'api', 'role', 'read'), -(7, 'role:create', '创建角色', 'api', 'role', 'create'), -(8, 'role:update', '更新角色', 'api', 'role', 'update'), -(9, 'role:delete', '删除角色', 'api', 'role', 'delete'); - --- 为超级管理员角色分配所有权限 -INSERT INTO `sys_role_permissions` (`role_id`, `permission_id`) -SELECT 1, id FROM `sys_permissions`; -``` - -### 5.5 初始组织架构 - -```sql --- 创建顶级组织 -INSERT INTO `sys_organizations` (`id`, `code`, `name`, `type`, `status`) VALUES -(1, 'ROOT', '星撰集团', 'company', 'active'); - --- 创建部门 -INSERT INTO `sys_organizations` (`id`, `code`, `name`, `type`, `pid`, `path`, `level`, `status`) VALUES -(2, 'TECH', '技术部', 'department', 1, '/1/2/', 2, 'active'), -(3, 'PRODUCT', '产品部', 'department', 1, '/1/3/', 2, 'active'), -(4, 'OPERATE', '运营部', 'department', 1, '/1/4/', 2, 'active'); -``` - -### 5.6 示例标签数据 - -```sql --- 用户标签 -INSERT INTO `sys_tags` (`name`, `type`, `color`, `description`) VALUES -('VIP', 'user', '#ff4d4f', 'VIP用户'), -('活跃用户', 'user', '#52c41a', '经常登录的用户'), -('内容创作者', 'user', '#1890ff', '发布优质内容的用户'), -('新用户', 'user', '#faad14', '注册不满30天的用户'); - --- 角色标签 -INSERT INTO `sys_tags` (`name`, `type`, `color`, `description`) VALUES -('核心角色', 'role', '#ff4d4f', '系统核心角色'), -('业务角色', 'role', '#1890ff', '业务相关角色'); -``` - -## 6. 注意事项 - -1. **ID生成策略**:建议使用雪花算法生成分布式ID,避免自增ID的局限性 -2. **密码存储**:必须使用bcrypt或类似的强哈希算法,成本因子不低于12 -3. **并发控制**:使用version字段实现乐观锁,防止并发更新冲突 -4. **查询优化**:对于树形结构的查询,优先使用path字段而非递归查询 -5. **数据归档**:日志表建议定期归档,避免单表数据量过大 -6. **缓存策略**:权限、字典等数据变更不频繁,建议缓存到Redis -7. **批量操作**:大批量数据操作时使用批量SQL,避免循环单条执行 -8. **索引维护**:定期分析索引使用情况,删除无用索引,优化查询性能 - -## 7. 性能优化建议 - -### 7.1 查询优化 -- 使用覆盖索引减少回表查询 -- 合理使用联合索引,注意索引顺序 -- 避免在WHERE子句中对字段进行函数操作 -- 使用EXPLAIN分析SQL执行计划 - -### 7.2 树形结构查询优化 -```sql --- 查询所有子节点(使用path) -SELECT * FROM sys_roles WHERE path LIKE '/1/2/%' AND deleted_at IS NULL; - --- 查询直接子节点 -SELECT * FROM sys_roles WHERE pid = 2 AND deleted_at IS NULL ORDER BY sort_order; - --- 递归查询所有子节点(MySQL 8.0+) -WITH RECURSIVE role_tree AS ( - SELECT * FROM sys_roles WHERE id = 1 - UNION ALL - SELECT r.* FROM sys_roles r - INNER JOIN role_tree rt ON r.pid = rt.id -) -SELECT * FROM role_tree; -``` - -### 7.3 权限查询优化 -```sql --- 使用权限快照避免多表联查 -SELECT permissions_snapshot FROM sys_roles WHERE id = ?; - --- 获取用户所有权限(包括角色继承) -SELECT DISTINCT p.* -FROM sys_permissions p -JOIN sys_role_permissions rp ON p.id = rp.permission_id -JOIN sys_user_roles ur ON rp.role_id = ur.role_id -WHERE ur.user_id = ? - AND ur.expired_at IS NULL OR ur.expired_at > NOW() - AND p.deleted_at IS NULL; -``` - -## 8. 后续扩展建议 - -1. **多租户支持**:预留tenant_id字段,支持SaaS化部署 -2. **数据权限**:扩展权限表,支持行级数据权限控制 -3. **审批流程**:增加工作流相关表,支持复杂的审批场景 -4. **消息通知**:增加消息表,支持站内信、邮件、短信等通知 -5. **登录日志**:独立的登录日志表,记录所有登录尝试 -6. **配置管理**:系统配置表,支持动态配置管理 -7. **定时任务**:任务调度表,支持定时任务管理 -8. **数据字典缓存**:实现字典数据的多级缓存机制 - -## 9. 安全建议 - -1. **SQL注入防护**:使用参数化查询,避免拼接SQL -2. **XSS防护**:对用户输入进行转义处理 -3. **CSRF防护**:使用Token机制防止跨站请求伪造 -4. **敏感数据加密**:对敏感字段进行加密存储 -5. **访问控制**:实施最小权限原则 -6. **审计日志**:记录所有敏感操作 -7. **数据备份**:定期备份,异地容灾 -8. **安全扫描**:定期进行安全漏洞扫描 \ No newline at end of file +# M2 - 基础用户系统 - 数据库设计文档 + +## 1. 概述 + +本文档为星撰个人综合平台的基础用户系统(M2阶段)提供详细的数据库设计方案。设计遵循以下原则: + +- **无外键约束**:所有关联关系通过应用层代码维护,提高性能和灵活性 +- **软删除机制**:所有业务表支持软删除,保留数据历史 +- **树形结构支持**:角色、组织、权限、字典等支持无限层级的树形结构 +- **审计追踪**:完整的创建、修改记录 +- **高性能**:合理的索引设计,支持大数据量查询 +- **扩展性**:预留扩展字段,支持未来功能演进 + +## 2. 数据库 ER 图 + +```mermaid +erDiagram + %% 用户相关 + sys_users { + bigint id PK "主键,雪花ID" + varchar(50) username UK "用户名" + varchar(100) email UK "邮箱" + varchar(20) mobile "手机号" + varchar(255) password_hash "密码哈希" + varchar(255) avatar "头像URL" + varchar(100) nickname "昵称" + varchar(20) status "状态:字典[user_status]" + tinyint gender "性别:0未知,1男,2女" + date birthday "生日" + varchar(500) bio "个人简介" + int login_count "登录次数" + datetime last_login_at "最后登录时间" + varchar(45) last_login_ip "最后登录IP" + int failed_attempts "失败尝试次数" + datetime locked_until "锁定截止时间" + boolean is_root "是否超级管理员" + json extra "扩展信息JSON" + bigint created_by "创建人" + datetime created_at "创建时间" + bigint updated_by "更新人" + datetime updated_at "更新时间" + datetime deleted_at "删除时间" + int version "乐观锁版本号" + } + + %% 角色相关 + sys_roles { + bigint id PK "主键" + varchar(50) code UK "角色代码" + varchar(100) name "角色名称" + text description "角色描述" + bigint pid "父角色ID" + varchar(500) path "层级路径,如:/1/2/3/" + int level "层级深度" + int sort_order "排序号" + varchar(20) status "状态:字典[role_status]" + boolean is_system "是否系统角色" + json permissions_snapshot "权限快照" + json extra "扩展信息" + bigint created_by "创建人" + datetime created_at "创建时间" + bigint updated_by "更新人" + datetime updated_at "更新时间" + datetime deleted_at "删除时间" + int version "版本号" + } + + %% 权限相关 + sys_permissions { + bigint id PK "主键" + varchar(100) code UK "权限代码" + varchar(100) name "权限名称" + varchar(20) type "类型:menu,button,api,data" + varchar(50) resource "资源标识" + varchar(50) action "操作:read,write,delete等" + text description "权限描述" + bigint pid "父权限ID" + varchar(500) path "层级路径" + int level "层级深度" + int sort_order "排序号" + varchar(20) status "状态" + json meta "元数据" + bigint created_by "创建人" + datetime created_at "创建时间" + bigint updated_by "更新人" + datetime updated_at "更新时间" + datetime deleted_at "删除时间" + } + + %% 组织架构 + sys_organizations { + bigint id PK "主键" + varchar(100) code UK "组织代码" + varchar(200) name "组织名称" + varchar(200) full_name "组织全称" + text description "组织描述" + bigint pid "父组织ID" + varchar(500) path "层级路径" + int level "层级深度" + varchar(20) type "类型:字典[org_type]" + varchar(20) status "状态:字典[org_status]" + int sort_order "排序号" + varchar(100) leader_id "负责人ID" + varchar(200) address "地址" + varchar(50) phone "电话" + json extra "扩展信息" + bigint created_by "创建人" + datetime created_at "创建时间" + bigint updated_by "更新人" + datetime updated_at "更新时间" + datetime deleted_at "删除时间" + int version "版本号" + } + + %% 字典管理 + sys_dict_types { + bigint id PK "主键" + varchar(50) code UK "字典类型代码" + varchar(100) name "字典类型名称" + text description "描述" + bigint pid "父字典类型ID" + varchar(500) path "层级路径" + int level "层级深度" + varchar(20) status "状态:active,inactive" + boolean is_system "是否系统字典" + int sort_order "排序号" + bigint created_by "创建人" + datetime created_at "创建时间" + bigint updated_by "更新人" + datetime updated_at "更新时间" + datetime deleted_at "删除时间" + } + + sys_dict_items { + bigint id PK "主键" + bigint type_id "字典类型ID" + varchar(50) item_key "字典项键" + varchar(200) item_value "字典项值" + varchar(100) label "显示标签" + varchar(200) label_en "英文标签" + text description "描述" + bigint pid "父字典项ID" + varchar(500) path "层级路径" + int level "层级深度" + int sort_order "排序号" + varchar(20) status "状态" + varchar(50) css_class "样式类" + varchar(50) color "颜色值" + json extra "扩展属性" + bigint created_by "创建人" + datetime created_at "创建时间" + bigint updated_by "更新人" + datetime updated_at "更新时间" + datetime deleted_at "删除时间" + } + + %% 标签管理 + sys_tags { + bigint id PK "主键" + varchar(50) name UK "标签名称" + varchar(50) type "标签类型:user,role,content" + varchar(50) color "标签颜色" + text description "描述" + int usage_count "使用次数" + bigint created_by "创建人" + datetime created_at "创建时间" + datetime deleted_at "删除时间" + } + + %% 关联表 + sys_user_roles { + bigint id PK "主键" + bigint user_id "用户ID" + bigint role_id "角色ID" + datetime expired_at "过期时间" + bigint created_by "创建人" + datetime created_at "创建时间" + } + + sys_role_permissions { + bigint id PK "主键" + bigint role_id "角色ID" + bigint permission_id "权限ID" + boolean is_half "是否半选(树形权限)" + bigint created_by "创建人" + datetime created_at "创建时间" + } + + sys_user_organizations { + bigint id PK "主键" + bigint user_id "用户ID" + bigint organization_id "组织ID" + boolean is_primary "是否主组织" + varchar(100) position "职位" + datetime joined_at "加入时间" + bigint created_by "创建人" + datetime created_at "创建时间" + } + + sys_user_tags { + bigint id PK "主键" + bigint user_id "用户ID" + bigint tag_id "标签ID" + bigint created_by "创建人" + datetime created_at "创建时间" + } + + %% 操作日志 + sys_operation_logs { + bigint id PK "主键" + bigint user_id "用户ID" + varchar(100) username "用户名" + varchar(50) module "模块" + varchar(50) action "操作" + varchar(200) target "操作对象" + bigint target_id "对象ID" + text request_data "请求数据" + text response_data "响应数据" + varchar(20) status "状态:success,fail" + varchar(45) ip "IP地址" + varchar(200) user_agent "用户代理" + bigint duration "耗时(ms)" + text error_msg "错误信息" + datetime created_at "创建时间" + } + + %% 关系定义 + sys_users ||--o{ sys_user_roles : "拥有" + sys_roles ||--o{ sys_user_roles : "被分配" + sys_roles ||--o{ sys_role_permissions : "拥有" + sys_permissions ||--o{ sys_role_permissions : "被分配" + sys_users ||--o{ sys_user_organizations : "属于" + sys_organizations ||--o{ sys_user_organizations : "包含" + sys_users ||--o{ sys_user_tags : "拥有" + sys_tags ||--o{ sys_user_tags : "被使用" + sys_dict_types ||--o{ sys_dict_items : "包含" + sys_roles ||--o| sys_roles : "继承自" + sys_organizations ||--o| sys_organizations : "隶属于" + sys_permissions ||--o| sys_permissions : "包含" + sys_dict_items ||--o| sys_dict_items : "子项" + sys_dict_types ||--o| sys_dict_types : "包含" +``` + +## 3. 表结构详细设计 + +### 3.1 用户表 (sys_users) + +用户系统的核心表,存储用户基本信息和认证信息。 + +```sql +-- 表结构 +CREATE TABLE `sys_users` ( + `id` BIGINT NOT NULL COMMENT '主键,雪花ID', + `username` VARCHAR(50) NOT NULL COMMENT '用户名,唯一', + `email` VARCHAR(100) NOT NULL COMMENT '邮箱,唯一', + `mobile` VARCHAR(20) NULL COMMENT '手机号', + `password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希值', + `avatar` VARCHAR(255) NULL COMMENT '头像URL', + `nickname` VARCHAR(100) NULL COMMENT '昵称', + `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-正常,inactive-未激活,locked-锁定,disabled-禁用', + `gender` TINYINT NULL DEFAULT 0 COMMENT '性别:0-未知,1-男,2-女', + `birthday` DATE NULL COMMENT '生日', + `bio` VARCHAR(500) NULL COMMENT '个人简介', + `login_count` INT NOT NULL DEFAULT 0 COMMENT '登录次数', + `last_login_at` DATETIME NULL COMMENT '最后登录时间', + `last_login_ip` VARCHAR(45) NULL COMMENT '最后登录IP', + `failed_attempts` INT NOT NULL DEFAULT 0 COMMENT '连续失败尝试次数', + `locked_until` DATETIME NULL COMMENT '锁定截止时间', + `is_root` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否超级管理员', + `extra` JSON NULL COMMENT '扩展信息,JSON格式', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_by` BIGINT NULL COMMENT '更新人ID', + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` DATETIME NULL COMMENT '删除时间,软删除标记', + `version` INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_username` ON `sys_users` (`username`, `deleted_at`); +CREATE UNIQUE INDEX `uk_email` ON `sys_users` (`email`, `deleted_at`); +CREATE INDEX `idx_mobile` ON `sys_users` (`mobile`); +CREATE INDEX `idx_status` ON `sys_users` (`status`); +CREATE INDEX `idx_created_at` ON `sys_users` (`created_at`); +CREATE INDEX `idx_deleted_at` ON `sys_users` (`deleted_at`); +CREATE INDEX `idx_is_root` ON `sys_users` (`is_root`); +CREATE INDEX `idx_last_login` ON `sys_users` (`last_login_at`); +``` + +### 3.2 角色表 (sys_roles) + +支持树形结构的角色管理表,实现角色继承。 + +```sql +-- 表结构 +CREATE TABLE `sys_roles` ( + `id` BIGINT NOT NULL COMMENT '主键', + `code` VARCHAR(50) NOT NULL COMMENT '角色代码,唯一', + `name` VARCHAR(100) NOT NULL COMMENT '角色名称', + `description` TEXT NULL COMMENT '角色描述', + `pid` BIGINT NULL DEFAULT 0 COMMENT '父角色ID,0表示顶级', + `path` VARCHAR(500) NULL COMMENT '层级路径,如:/1/2/3/', + `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '同级排序号', + `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-启用,inactive-禁用', + `is_system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否系统内置角色', + `permissions_snapshot` JSON NULL COMMENT '权限快照,用于优化查询', + `extra` JSON NULL COMMENT '扩展信息', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_by` BIGINT NULL COMMENT '更新人ID', + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` DATETIME NULL COMMENT '删除时间', + `version` INT NOT NULL DEFAULT 1 COMMENT '版本号', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_code` ON `sys_roles` (`code`, `deleted_at`); +CREATE INDEX `idx_name` ON `sys_roles` (`name`); +CREATE INDEX `idx_pid` ON `sys_roles` (`pid`); +CREATE INDEX `idx_path` ON `sys_roles` (`path`); +CREATE INDEX `idx_status` ON `sys_roles` (`status`); +CREATE INDEX `idx_deleted_at` ON `sys_roles` (`deleted_at`); +CREATE INDEX `idx_is_system` ON `sys_roles` (`is_system`); +CREATE INDEX `idx_sort` ON `sys_roles` (`pid`, `sort_order`); +``` + +### 3.3 权限表 (sys_permissions) + +细粒度的权限定义表,支持多种权限类型。 + +```sql +-- 表结构 +CREATE TABLE `sys_permissions` ( + `id` BIGINT NOT NULL COMMENT '主键', + `code` VARCHAR(100) NOT NULL COMMENT '权限代码,唯一', + `name` VARCHAR(100) NOT NULL COMMENT '权限名称', + `type` VARCHAR(20) NOT NULL COMMENT '权限类型:menu-菜单,button-按钮,api-接口,data-数据', + `resource` VARCHAR(50) NULL COMMENT '资源标识,如:user,role,post', + `action` VARCHAR(50) NULL COMMENT '操作标识,如:read,create,update,delete', + `description` TEXT NULL COMMENT '权限描述', + `pid` BIGINT NULL DEFAULT 0 COMMENT '父权限ID', + `path` VARCHAR(500) NULL COMMENT '层级路径', + `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', + `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态', + `meta` JSON NULL COMMENT '元数据,如:图标、路由等', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_by` BIGINT NULL COMMENT '更新人ID', + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` DATETIME NULL COMMENT '删除时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_code` ON `sys_permissions` (`code`, `deleted_at`); +CREATE INDEX `idx_type` ON `sys_permissions` (`type`); +CREATE INDEX `idx_resource_action` ON `sys_permissions` (`resource`, `action`); +CREATE INDEX `idx_pid` ON `sys_permissions` (`pid`); +CREATE INDEX `idx_deleted_at` ON `sys_permissions` (`deleted_at`); +CREATE INDEX `idx_status` ON `sys_permissions` (`status`); +CREATE INDEX `idx_sort` ON `sys_permissions` (`pid`, `sort_order`); +``` + +### 3.4 组织架构表 (sys_organizations) + +支持多级组织结构,用于用户归属管理。 + +```sql +-- 表结构 +CREATE TABLE `sys_organizations` ( + `id` BIGINT NOT NULL COMMENT '主键', + `code` VARCHAR(100) NOT NULL COMMENT '组织代码,唯一', + `name` VARCHAR(200) NOT NULL COMMENT '组织名称', + `full_name` VARCHAR(200) NULL COMMENT '组织全称', + `description` TEXT NULL COMMENT '组织描述', + `pid` BIGINT NULL DEFAULT 0 COMMENT '父组织ID', + `path` VARCHAR(500) NULL COMMENT '层级路径', + `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', + `type` VARCHAR(20) NULL COMMENT '组织类型:company,department,team', + `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', + `leader_id` BIGINT NULL COMMENT '负责人ID', + `address` VARCHAR(200) NULL COMMENT '地址', + `phone` VARCHAR(50) NULL COMMENT '联系电话', + `extra` JSON NULL COMMENT '扩展信息', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_by` BIGINT NULL COMMENT '更新人ID', + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` DATETIME NULL COMMENT '删除时间', + `version` INT NOT NULL DEFAULT 1 COMMENT '版本号', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织架构表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_code` ON `sys_organizations` (`code`, `deleted_at`); +CREATE INDEX `idx_name` ON `sys_organizations` (`name`); +CREATE INDEX `idx_pid` ON `sys_organizations` (`pid`); +CREATE INDEX `idx_path` ON `sys_organizations` (`path`); +CREATE INDEX `idx_type` ON `sys_organizations` (`type`); +CREATE INDEX `idx_leader_id` ON `sys_organizations` (`leader_id`); +CREATE INDEX `idx_deleted_at` ON `sys_organizations` (`deleted_at`); +CREATE INDEX `idx_status` ON `sys_organizations` (`status`); +CREATE INDEX `idx_sort` ON `sys_organizations` (`pid`, `sort_order`); +``` + +### 3.5 字典类型表 (sys_dict_types) + +管理系统中的各类数据字典,支持树形结构。 + +```sql +-- 表结构 +CREATE TABLE `sys_dict_types` ( + `id` BIGINT NOT NULL COMMENT '主键', + `code` VARCHAR(50) NOT NULL COMMENT '字典类型代码,唯一', + `name` VARCHAR(100) NOT NULL COMMENT '字典类型名称', + `description` TEXT NULL COMMENT '描述', + `pid` BIGINT NULL DEFAULT 0 COMMENT '父字典类型ID,支持字典分类', + `path` VARCHAR(500) NULL COMMENT '层级路径', + `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', + `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-启用,inactive-禁用', + `is_system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否系统内置', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_by` BIGINT NULL COMMENT '更新人ID', + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` DATETIME NULL COMMENT '删除时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典类型表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_code` ON `sys_dict_types` (`code`, `deleted_at`); +CREATE INDEX `idx_name` ON `sys_dict_types` (`name`); +CREATE INDEX `idx_pid` ON `sys_dict_types` (`pid`); +CREATE INDEX `idx_path` ON `sys_dict_types` (`path`); +CREATE INDEX `idx_status` ON `sys_dict_types` (`status`); +CREATE INDEX `idx_deleted_at` ON `sys_dict_types` (`deleted_at`); +CREATE INDEX `idx_is_system` ON `sys_dict_types` (`is_system`); +CREATE INDEX `idx_sort` ON `sys_dict_types` (`pid`, `sort_order`); +``` + +### 3.6 字典项表 (sys_dict_items) + +存储具体的字典数据,支持树形结构。 + +```sql +-- 表结构 +CREATE TABLE `sys_dict_items` ( + `id` BIGINT NOT NULL COMMENT '主键', + `type_id` BIGINT NOT NULL COMMENT '字典类型ID', + `item_key` VARCHAR(50) NOT NULL COMMENT '字典项键', + `item_value` VARCHAR(200) NOT NULL COMMENT '字典项值', + `label` VARCHAR(100) NOT NULL COMMENT '显示标签', + `label_en` VARCHAR(200) NULL COMMENT '英文标签', + `description` TEXT NULL COMMENT '描述', + `pid` BIGINT NULL DEFAULT 0 COMMENT '父字典项ID,支持树形字典', + `path` VARCHAR(500) NULL COMMENT '层级路径', + `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', + `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态', + `css_class` VARCHAR(50) NULL COMMENT 'CSS样式类', + `color` VARCHAR(50) NULL COMMENT '颜色值,如:#FF0000', + `extra` JSON NULL COMMENT '扩展属性', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_by` BIGINT NULL COMMENT '更新人ID', + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` DATETIME NULL COMMENT '删除时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典项表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_type_key` ON `sys_dict_items` (`type_id`, `item_key`, `deleted_at`); +CREATE INDEX `idx_type_id` ON `sys_dict_items` (`type_id`); +CREATE INDEX `idx_pid` ON `sys_dict_items` (`pid`); +CREATE INDEX `idx_status` ON `sys_dict_items` (`status`); +CREATE INDEX `idx_deleted_at` ON `sys_dict_items` (`deleted_at`); +CREATE INDEX `idx_sort` ON `sys_dict_items` (`type_id`, `sort_order`); +CREATE INDEX `idx_key` ON `sys_dict_items` (`item_key`); +``` + +### 3.7 标签表 (sys_tags) + +灵活的标签系统,可用于用户、内容等多种场景。 + +```sql +-- 表结构 +CREATE TABLE `sys_tags` ( + `id` BIGINT NOT NULL COMMENT '主键', + `name` VARCHAR(50) NOT NULL COMMENT '标签名称', + `type` VARCHAR(50) NULL DEFAULT 'user' COMMENT '标签类型:user-用户标签,role-角色标签,content-内容标签', + `color` VARCHAR(50) NULL COMMENT '标签颜色,如:#FF0000', + `description` TEXT NULL COMMENT '描述', + `usage_count` INT NOT NULL DEFAULT 0 COMMENT '使用次数统计', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `deleted_at` DATETIME NULL COMMENT '删除时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_name_type` ON `sys_tags` (`name`, `type`, `deleted_at`); +CREATE INDEX `idx_type` ON `sys_tags` (`type`); +CREATE INDEX `idx_usage_count` ON `sys_tags` (`usage_count` DESC); +CREATE INDEX `idx_deleted_at` ON `sys_tags` (`deleted_at`); +CREATE INDEX `idx_name` ON `sys_tags` (`name`); +``` + +### 3.8 用户角色关联表 (sys_user_roles) + +```sql +-- 表结构 +CREATE TABLE `sys_user_roles` ( + `id` BIGINT NOT NULL COMMENT '主键', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `role_id` BIGINT NOT NULL COMMENT '角色ID', + `expired_at` DATETIME NULL COMMENT '过期时间,NULL表示永久', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_user_role` ON `sys_user_roles` (`user_id`, `role_id`); +CREATE INDEX `idx_user_id` ON `sys_user_roles` (`user_id`); +CREATE INDEX `idx_role_id` ON `sys_user_roles` (`role_id`); +CREATE INDEX `idx_expired_at` ON `sys_user_roles` (`expired_at`); +CREATE INDEX `idx_created_at` ON `sys_user_roles` (`created_at`); +``` + +### 3.9 角色权限关联表 (sys_role_permissions) + +```sql +-- 表结构 +CREATE TABLE `sys_role_permissions` ( + `id` BIGINT NOT NULL COMMENT '主键', + `role_id` BIGINT NOT NULL COMMENT '角色ID', + `permission_id` BIGINT NOT NULL COMMENT '权限ID', + `is_half` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否半选状态(树形权限)', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色权限关联表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_role_permission` ON `sys_role_permissions` (`role_id`, `permission_id`); +CREATE INDEX `idx_role_id` ON `sys_role_permissions` (`role_id`); +CREATE INDEX `idx_permission_id` ON `sys_role_permissions` (`permission_id`); +CREATE INDEX `idx_is_half` ON `sys_role_permissions` (`is_half`); +``` + +### 3.10 用户组织关联表 (sys_user_organizations) + +```sql +-- 表结构 +CREATE TABLE `sys_user_organizations` ( + `id` BIGINT NOT NULL COMMENT '主键', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `organization_id` BIGINT NOT NULL COMMENT '组织ID', + `is_primary` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否主组织', + `position` VARCHAR(100) NULL COMMENT '职位', + `joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户组织关联表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_user_org` ON `sys_user_organizations` (`user_id`, `organization_id`); +CREATE INDEX `idx_user_id` ON `sys_user_organizations` (`user_id`); +CREATE INDEX `idx_organization_id` ON `sys_user_organizations` (`organization_id`); +CREATE INDEX `idx_is_primary` ON `sys_user_organizations` (`is_primary`); +CREATE INDEX `idx_joined_at` ON `sys_user_organizations` (`joined_at`); +``` + +### 3.11 用户标签关联表 (sys_user_tags) + +```sql +-- 表结构 +CREATE TABLE `sys_user_tags` ( + `id` BIGINT NOT NULL COMMENT '主键', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `tag_id` BIGINT NOT NULL COMMENT '标签ID', + `created_by` BIGINT NULL COMMENT '创建人ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户标签关联表'; + +-- 索引设计 +CREATE UNIQUE INDEX `uk_user_tag` ON `sys_user_tags` (`user_id`, `tag_id`); +CREATE INDEX `idx_user_id` ON `sys_user_tags` (`user_id`); +CREATE INDEX `idx_tag_id` ON `sys_user_tags` (`tag_id`); +CREATE INDEX `idx_created_at` ON `sys_user_tags` (`created_at`); +``` + +### 3.12 操作日志表 (sys_operation_logs) + +记录所有重要操作,用于审计和问题追踪。 + +```sql +-- 表结构 +CREATE TABLE `sys_operation_logs` ( + `id` BIGINT NOT NULL COMMENT '主键', + `user_id` BIGINT NULL COMMENT '操作用户ID', + `username` VARCHAR(100) NULL COMMENT '操作用户名', + `module` VARCHAR(50) NOT NULL COMMENT '操作模块', + `action` VARCHAR(50) NOT NULL COMMENT '操作类型', + `target` VARCHAR(200) NULL COMMENT '操作对象描述', + `target_id` BIGINT NULL COMMENT '操作对象ID', + `request_data` TEXT NULL COMMENT '请求数据', + `response_data` TEXT NULL COMMENT '响应数据', + `status` VARCHAR(20) NOT NULL COMMENT '操作状态:success-成功,fail-失败', + `ip` VARCHAR(45) NULL COMMENT 'IP地址', + `user_agent` VARCHAR(200) NULL COMMENT '用户代理', + `duration` BIGINT NULL COMMENT '操作耗时(毫秒)', + `error_msg` TEXT NULL COMMENT '错误信息', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表'; + +-- 索引设计 +CREATE INDEX `idx_user_id` ON `sys_operation_logs` (`user_id`); +CREATE INDEX `idx_module_action` ON `sys_operation_logs` (`module`, `action`); +CREATE INDEX `idx_target` ON `sys_operation_logs` (`target_id`); +CREATE INDEX `idx_status` ON `sys_operation_logs` (`status`); +CREATE INDEX `idx_created_at` ON `sys_operation_logs` (`created_at`); +CREATE INDEX `idx_ip` ON `sys_operation_logs` (`ip`); +-- 分区建议:按月分区 +ALTER TABLE `sys_operation_logs` PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) ( + PARTITION p202401 VALUES LESS THAN (202402), + PARTITION p202402 VALUES LESS THAN (202403), + -- 继续添加更多分区... + PARTITION p_future VALUES LESS THAN MAXVALUE +); +``` + +## 4. 设计特点说明 + +### 4.1 软删除机制 + +- 所有业务表都包含 `deleted_at` 字段 +- 删除操作只更新 `deleted_at` 为当前时间 +- 唯一索引都包含 `deleted_at`,确保已删除数据不影响唯一性 +- 查询时默认过滤 `deleted_at IS NULL` +- 提供回收站功能,可恢复误删数据 + +### 4.2 树形结构支持 + +- 使用 `pid` 存储父节点ID,0或NULL表示顶级节点 +- `path` 字段存储完整路径(如:/1/2/3/),便于查询所有子节点 +- `level` 记录层级深度,优化层级查询 +- 支持无限层级嵌套 +- 提供递归查询的存储过程优化性能 + +### 4.3 审计追踪 + +- 所有表包含 `created_by`、`created_at`、`updated_by`、`updated_at` +- 重要操作记录到 `sys_operation_logs` 表 +- 支持数据变更历史追踪 +- 可配置敏感操作的详细日志记录 + +### 4.4 性能优化 + +- 合理的索引设计,覆盖常用查询场景 +- 使用JSON字段存储扩展信息,减少表结构变更 +- 权限快照机制,避免复杂的联表查询 +- 日志表按月分区,提高查询效率 +- 高频查询数据缓存到Redis + +### 4.5 扩展性设计 + +- 预留 `extra` JSON字段,支持灵活扩展 +- 版本号字段支持乐观锁 +- 标签系统支持多种业务场景 +- 字典系统支持动态配置 +- 支持插件化扩展新功能 + +### 4.6 数据完整性 + +- 虽然不使用外键,但通过应用层保证引用完整性 +- 删除操作需级联处理关联数据 +- 使用事务保证操作原子性 +- 定期数据一致性检查任务 + +### 4.7 安全性设计 + +- 密码使用bcrypt算法加密,成本因子不低于12 +- 登录失败次数限制,防止暴力破解 +- 操作日志记录IP和UserAgent,便于审计 +- 敏感数据(如手机号)部分存储时脱敏 +- SQL注入防护通过参数化查询实现 + +## 5. 初始化数据 + +### 5.1 系统初始用户 + +```sql +-- 创建root超级管理员 +INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `is_root`, `status`) +VALUES (1, 'root', 'root@system.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYpfQaXUIkRrPJK', '超级管理员', TRUE, 'active'); + +-- 创建系统管理员 +INSERT INTO `sys_users` (`id`, `username`, `email`, `password_hash`, `nickname`, `status`) +VALUES (2, 'admin', 'admin@system.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewYpfQaXUIkRrPJK', '系统管理员', 'active'); +``` + +### 5.2 系统初始角色 + +```sql +-- 超级管理员角色 +INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) +VALUES (1, 'super_admin', '超级管理员', '拥有系统所有权限', TRUE, 'active'); + +-- 系统管理员角色 +INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`, `pid`, `path`, `level`) +VALUES (2, 'admin', '系统管理员', '负责系统配置和用户管理', TRUE, 'active', 1, '/1/2/', 2); + +-- 普通用户角色 +INSERT INTO `sys_roles` (`id`, `code`, `name`, `description`, `is_system`, `status`) +VALUES (3, 'user', '普通用户', '普通注册用户默认角色', TRUE, 'active'); + +-- 分配角色 +INSERT INTO `sys_user_roles` (`user_id`, `role_id`) VALUES (1, 1); +INSERT INTO `sys_user_roles` (`user_id`, `role_id`) VALUES (2, 2); +``` + +### 5.3 基础字典数据 + +```sql +-- 字典分类 +INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`) VALUES +(1, 'system', '系统字典', TRUE), +(2, 'business', '业务字典', TRUE); + +-- 用户状态字典 +INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES +(10, 'user_status', '用户状态', TRUE, 1, '/1/10/'); + +INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `color`, `sort_order`) VALUES +(10, 'active', 'active', '正常', '#52c41a', 1), +(10, 'inactive', 'inactive', '未激活', '#faad14', 2), +(10, 'locked', 'locked', '锁定', '#ff4d4f', 3), +(10, 'disabled', 'disabled', '禁用', '#d9d9d9', 4); + +-- 组织类型字典 +INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES +(11, 'org_type', '组织类型', TRUE, 1, '/1/11/'); + +INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES +(11, 'company', 'company', '公司', 1), +(11, 'department', 'department', '部门', 2), +(11, 'team', 'team', '团队', 3), +(11, 'group', 'group', '小组', 4); + +-- 性别字典 +INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES +(12, 'gender', '性别', TRUE, 1, '/1/12/'); + +INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES +(12, '0', '0', '未知', 1), +(12, '1', '1', '男', 2), +(12, '2', '2', '女', 3); + +-- 权限类型字典 +INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES +(13, 'permission_type', '权限类型', TRUE, 1, '/1/13/'); + +INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `color`, `sort_order`) VALUES +(13, 'menu', 'menu', '菜单', '#1890ff', 1), +(13, 'button', 'button', '按钮', '#52c41a', 2), +(13, 'api', 'api', '接口', '#fa8c16', 3), +(13, 'data', 'data', '数据', '#722ed1', 4); + +-- 标签类型字典 +INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES +(14, 'tag_type', '标签类型', TRUE, 1, '/1/14/'); + +INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES +(14, 'user', 'user', '用户标签', 1), +(14, 'role', 'role', '角色标签', 2), +(14, 'content', 'content', '内容标签', 3); + +-- 地区字典(示例) +INSERT INTO `sys_dict_types` (`id`, `code`, `name`, `is_system`, `pid`, `path`) VALUES +(20, 'region', '地区', TRUE, 2, '/2/20/'); + +INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `sort_order`) VALUES +(20, 'CN', 'CN', '中国', 1); + +INSERT INTO `sys_dict_items` (`type_id`, `item_key`, `item_value`, `label`, `pid`, `path`, `sort_order`) VALUES +(20, 'CN_BJ', 'CN_BJ', '北京', (SELECT id FROM sys_dict_items WHERE item_key = 'CN'), NULL, 1), +(20, 'CN_SH', 'CN_SH', '上海', (SELECT id FROM sys_dict_items WHERE item_key = 'CN'), NULL, 2); +``` + +### 5.4 基础权限数据 + +```sql +-- 系统管理权限 +INSERT INTO `sys_permissions` (`id`, `code`, `name`, `type`, `resource`, `action`) VALUES +(1, 'system:manage', '系统管理', 'menu', 'system', 'manage'), +(2, 'user:read', '查看用户', 'api', 'user', 'read'), +(3, 'user:create', '创建用户', 'api', 'user', 'create'), +(4, 'user:update', '更新用户', 'api', 'user', 'update'), +(5, 'user:delete', '删除用户', 'api', 'user', 'delete'), +(6, 'role:read', '查看角色', 'api', 'role', 'read'), +(7, 'role:create', '创建角色', 'api', 'role', 'create'), +(8, 'role:update', '更新角色', 'api', 'role', 'update'), +(9, 'role:delete', '删除角色', 'api', 'role', 'delete'); + +-- 为超级管理员角色分配所有权限 +INSERT INTO `sys_role_permissions` (`role_id`, `permission_id`) +SELECT 1, id FROM `sys_permissions`; +``` + +### 5.5 初始组织架构 + +```sql +-- 创建顶级组织 +INSERT INTO `sys_organizations` (`id`, `code`, `name`, `type`, `status`) VALUES +(1, 'ROOT', '星撰集团', 'company', 'active'); + +-- 创建部门 +INSERT INTO `sys_organizations` (`id`, `code`, `name`, `type`, `pid`, `path`, `level`, `status`) VALUES +(2, 'TECH', '技术部', 'department', 1, '/1/2/', 2, 'active'), +(3, 'PRODUCT', '产品部', 'department', 1, '/1/3/', 2, 'active'), +(4, 'OPERATE', '运营部', 'department', 1, '/1/4/', 2, 'active'); +``` + +### 5.6 示例标签数据 + +```sql +-- 用户标签 +INSERT INTO `sys_tags` (`name`, `type`, `color`, `description`) VALUES +('VIP', 'user', '#ff4d4f', 'VIP用户'), +('活跃用户', 'user', '#52c41a', '经常登录的用户'), +('内容创作者', 'user', '#1890ff', '发布优质内容的用户'), +('新用户', 'user', '#faad14', '注册不满30天的用户'); + +-- 角色标签 +INSERT INTO `sys_tags` (`name`, `type`, `color`, `description`) VALUES +('核心角色', 'role', '#ff4d4f', '系统核心角色'), +('业务角色', 'role', '#1890ff', '业务相关角色'); +``` + +## 6. 注意事项 + +1. **ID生成策略**:建议使用雪花算法生成分布式ID,避免自增ID的局限性 +2. **密码存储**:必须使用bcrypt或类似的强哈希算法,成本因子不低于12 +3. **并发控制**:使用version字段实现乐观锁,防止并发更新冲突 +4. **查询优化**:对于树形结构的查询,优先使用path字段而非递归查询 +5. **数据归档**:日志表建议定期归档,避免单表数据量过大 +6. **缓存策略**:权限、字典等数据变更不频繁,建议缓存到Redis +7. **批量操作**:大批量数据操作时使用批量SQL,避免循环单条执行 +8. **索引维护**:定期分析索引使用情况,删除无用索引,优化查询性能 + +## 7. 性能优化建议 + +### 7.1 查询优化 + +- 使用覆盖索引减少回表查询 +- 合理使用联合索引,注意索引顺序 +- 避免在WHERE子句中对字段进行函数操作 +- 使用EXPLAIN分析SQL执行计划 + +### 7.2 树形结构查询优化 + +```sql +-- 查询所有子节点(使用path) +SELECT * FROM sys_roles WHERE path LIKE '/1/2/%' AND deleted_at IS NULL; + +-- 查询直接子节点 +SELECT * FROM sys_roles WHERE pid = 2 AND deleted_at IS NULL ORDER BY sort_order; + +-- 递归查询所有子节点(MySQL 8.0+) +WITH RECURSIVE role_tree AS ( + SELECT * FROM sys_roles WHERE id = 1 + UNION ALL + SELECT r.* FROM sys_roles r + INNER JOIN role_tree rt ON r.pid = rt.id +) +SELECT * FROM role_tree; +``` + +### 7.3 权限查询优化 + +```sql +-- 使用权限快照避免多表联查 +SELECT permissions_snapshot FROM sys_roles WHERE id = ?; + +-- 获取用户所有权限(包括角色继承) +SELECT DISTINCT p.* +FROM sys_permissions p +JOIN sys_role_permissions rp ON p.id = rp.permission_id +JOIN sys_user_roles ur ON rp.role_id = ur.role_id +WHERE ur.user_id = ? + AND ur.expired_at IS NULL OR ur.expired_at > NOW() + AND p.deleted_at IS NULL; +``` + +## 8. 后续扩展建议 + +1. **多租户支持**:预留tenant_id字段,支持SaaS化部署 +2. **数据权限**:扩展权限表,支持行级数据权限控制 +3. **审批流程**:增加工作流相关表,支持复杂的审批场景 +4. **消息通知**:增加消息表,支持站内信、邮件、短信等通知 +5. **登录日志**:独立的登录日志表,记录所有登录尝试 +6. **配置管理**:系统配置表,支持动态配置管理 +7. **定时任务**:任务调度表,支持定时任务管理 +8. **数据字典缓存**:实现字典数据的多级缓存机制 + +## 9. 安全建议 + +1. **SQL注入防护**:使用参数化查询,避免拼接SQL +2. **XSS防护**:对用户输入进行转义处理 +3. **CSRF防护**:使用Token机制防止跨站请求伪造 +4. **敏感数据加密**:对敏感字段进行加密存储 +5. **访问控制**:实施最小权限原则 +6. **审计日志**:记录所有敏感操作 +7. **数据备份**:定期备份,异地容灾 +8. **安全扫描**:定期进行安全漏洞扫描 diff --git a/prd/M2-基础用户系统-详细设计.back.md b/prd/M2-基础用户系统-详细设计.back.md index a2062b3..dab5cac 100644 --- a/prd/M2-基础用户系统-详细设计.back.md +++ b/prd/M2-基础用户系统-详细设计.back.md @@ -1,249 +1,278 @@ -# M2 - 基础用户系统 - 详细设计 (V2) - ---- - -## 1. 引言 - -### 1.1. 文档目的 -本文档是 "M2 - 基础用户系统" 阶段的 **V2 修订版**,旨在提供一份详尽、准确、可直接用于开发的技术设计方案。V2 版根据反馈进行了全面重构,重点增强了数据库设计的合理性、API定义的完整性,并补充了关键的 `root` 用户机制和标签功能。 - -### 1.2. 模块范围 -本设计覆盖功能点包括: -- 用户认证(注册、登录)及 `root` 超级管理员机制。 -- 用户、角色、权限、组织、字典的CRUD管理,支持层级结构。 -- 用户标签系统。 -- 基于角色的访问控制(RBAC)的实现。 - ---- - -## 2. 数据库设计 (Revised) - -### 2.1. E-R 图 (Entity-Relationship Diagram) -*已更新,包含标签和重构后的字典表* -```mermaid -erDiagram - users { - bigint id PK - varchar(50) username UK - varchar(255) email UK - varchar(255) password_hash - varchar(20) status - datetime created_at - datetime updated_at - } - - roles { - bigint id PK - varchar(50) name UK - text description - bigint parent_id FK "nullable, self-ref" - datetime created_at - datetime updated_at - } - - permissions { - bigint id PK - varchar(50) action UK - varchar(50) resource UK - text description - } - - organizations { - bigint id PK - varchar(100) name - text description - bigint parent_id FK "nullable, self-ref" - datetime created_at - datetime updated_at - } - - dictionary_types { - bigint id PK - varchar(50) type_name UK - text description - } - - dictionary_items { - bigint id PK - bigint type_id FK - varchar(50) `key` - varchar(255) value - int sort_order - } - - tags { - bigint id PK - varchar(50) name UK - } - - users ||--o{ user_roles : "has" - roles ||--o{ user_roles : "assigned to" - roles ||--o{ role_permissions : "has" - permissions ||--o{ role_permissions : "granted to" - users ||--o{ user_organizations : "belongs to" - organizations ||--o{ user_organizations : "has" - users ||--o{ user_tags : "has" - tags ||--o{ user_tags : "applied to" - dictionary_types ||--o{ dictionary_items : "has" - - roles }o..o| roles : "is child of" - organizations }o..o| organizations : "is child of" - - user_roles { bigint user_id PK,FK; bigint role_id PK,FK } - role_permissions { bigint role_id PK,FK; bigint permission_id PK,FK } - user_organizations { bigint user_id PK,FK; bigint organization_id PK,FK } - user_tags { bigint user_id PK,FK; bigint tag_id PK,FK } -``` - -### 2.2. 表结构定义 - -#### `roles` / `organizations` -- **`parent_id`**: `BIGINT NULL`, `FOREIGN KEY (parent_id) REFERENCES roles(id) ON DELETE SET NULL`。此字段用于实现树形层级结构。值为`NULL`表示为顶级角色/组织。 - -#### `dictionary_types` - 字典类型表 -| 字段名 | 类型 | 约束 | 描述 | -|---------------|---------------|---------------------|----------------------| -| `id` | `BIGINT` | `PK`, `AI` | 类型唯一ID | -| `type_name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL`| 字典类型名(如: `user_status`) | -| `description` | `TEXT` | | 类型描述 | - -#### `dictionary_items` - 字典条目表 -| 字段名 | 类型 | 约束 | 描述 | -|---------------|----------------|----------------------|----------------------| -| `id` | `BIGINT` | `PK`, `AI` | 条目唯一ID | -| `type_id` | `BIGINT` | `NOT NULL`, `FK` | 关联到`dictionary_types.id` | -| `key` | `VARCHAR(50)` | `NOT NULL` | 键 (程序中使用) | -| `value` | `VARCHAR(255)` | `NOT NULL` | 值 (UI中显示) | -| `sort_order` | `INT` | `DEFAULT 0` | 排序字段 | -*复合唯一键: `(type_id, key)`* - -#### `tags` - 标签表 -| 字段名 | 类型 | 约束 | 描述 | -|--------|---------------|---------------------|------------| -| `id` | `BIGINT` | `PK`, `AI` | 标签唯一ID | -| `name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL`| 标签名 | - -#### `user_tags` - 用户标签关联表 -| 字段名 | 类型 | 约束 | 描述 | -|-----------|----------|----------|------------| -| `user_id` | `BIGINT` | `PK`, `FK` | 关联用户ID | -| `tag_id` | `BIGINT` | `PK`, `FK` | 关联标签ID | - ---- - -## 3. Root 用户与系统初始化 - -- **`root` 用户**: - - `root` 是一个特殊的超级管理员账户,拥有系统中所有权限。 - - 此账户 **不可通过 API 创建或删除**。它应在系统首次部署时,通过数据库 **Seeding (种子填充) 脚本** 创建。 - - `root` 用户的角色是固定的(如 `super_admin`),此角色同样通过 Seeding 创建,并关联所有已定义的权限。 - - 任何权限校验逻辑都必须对 `root` 用户或 `super_admin` 角色开绿灯。任何API都不能降低 `root` 的权限或删除该用户。 - ---- - -## 4. API 接口设计 (Detailed) - -**Base URL**: `/api/v1` -**通用错误响应结构**: -```json -{ - "code": 40001, // 详细业务错误码 - "message": "Validation failed: username must be at least 3 characters.", - "data": null -} -``` - -### 3.1. Auth - 认证接口 (`/auth`) - -#### `POST /register` - 用户注册 -- **权限**: Public -- **请求体 (`application/json`)**: - | 名称 | 类型 | 必填 | 校验规则 | 描述 | - |------------|--------|------|----------------------------------------------------|----------------| - | `username` | string | 是 | min:3, max:50, 字母/数字/下划线组合 | 用户名 | - | `email` | string | 是 | valid email format | 邮箱地址 | - | `password` | string | 是 | min:8, max:100, 必须包含大小写字母和数字 | 密码 | -- **成功响应 (201 Created)**: - ```json - { "code": 0, "message": "User registered successfully.", "data": { "id": 1, "username": "newuser", "email": "..." } } - ``` -- **异常响应**: - | HTTP 码 | 业务码 | 原因 | - |---------|----------|----------------------------| - | 400 | 40001 | 请求参数不符合校验规则 | - | 409 | 40901 | 用户名或邮箱已被占用 | - -#### `POST /login` - 用户登录 -- **权限**: Public -- **请求体 (`application/json`)**: - | 名称 | 类型 | 必填 | 校验规则 | 描述 | - |------------|--------|------|------------------|------------------| - | `username` | string | 是 | - | 用户名或邮箱 | - | `password` | string | 是 | - | 密码 | -- **成功响应 (200 OK)**: - ```json - { "code": 0, "message": "Login successful.", "data": { "token": "ey..." } } - ``` -- **异常响应**: - | HTTP 码 | 业务码 | 原因 | - |---------|----------|--------------------------| - | 401 | 40101 | 用户名或密码错误 | - | 401 | 40102 | 账户被禁用或未激活 | - ---- -### 3.2. Users - 用户接口 (`/users`) -*除特殊说明外,均需认证* - -#### `GET /me` - 获取当前用户信息 -- **权限**: Authenticated -- **成功响应 (200 OK)**: `data` 包含当前用户详细信息、关联的角色和权限列表。 - -#### `GET /` - 获取用户列表 -- **权限**: `read:users` -- **查询参数**: - | 名称 | 类型 | 必填 | 描述 | - |-----------|--------|------|------------------------------------| - | `page` | number | 否 | 页码, default 1 | - | `pageSize`| number | 否 | 每页数量, default 10 | - | `keyword` | string | 否 | 按用户名或邮箱模糊搜索 | -- **成功响应 (200 OK)**: `data` 包含 `items` (用户列表) 和 `pagination` (分页信息)。 - -#### `PUT /{userId}/status` - 更新用户状态 -- **权限**: `update:user_status` -- **请求体**: `{ "status": "active" }` (status 必须是字典中 `user_status` 类型的值) -- **成功响应 (200 OK)**: 返回更新后的用户信息。 -- **异常响应**: - | HTTP 码 | 业务码 | 原因 | - |---------|----------|--------------------------| - | 403 | 40301 | 试图修改 `root` 用户状态 | - | 404 | 40401 | 指定用户不存在 | - -#### `POST /{userId}/tags` - 为用户打标签 -- **权限**: `update:user_tags` -- **请求体**: `{ "tagIds": [1, 2, 3] }` -- **成功响应 (204 No Content)**. - ---- -### 3.3. Roles - 角色接口 (`/roles`) -*所有接口均需 `manage:roles` 权限* - -#### `GET /` - 获取角色列表 -- **描述**: 以树形结构返回所有角色。 -- **成功响应 (200 OK)**: `data` 是一个包含 `children` 属性的树形数组。 - -#### `POST /` - 创建新角色 -- **请求体**: `{ "name": "editor", "description": "...", "parentId": 1, "permissionIds": [101, 102] }` -- **成功响应 (201 Created)**: `data` 包含新创建的角色信息。 -- **异常响应 (409 Conflict)**: 角色名已存在。 - ---- -*其他模块(Organizations, Dictionaries, Tags)的API将遵循类似的详细设计模式:提供完整的增删改查接口,明确定义权限、请求体、校验规则和所有可能的成功/异常响应。* - -# 补充说明 - -1. 你忽略了我在main.md中提到的root用户,所以缺少一类集中管理的接口和机制,请认真阅读 -2. 接口设计太简陋了,太粗略了,需要把异常情况考虑进去, -3. 数据库设计完全无法满足需求,没有pid如何实现层级结构 -4. 字典确实,无法完成拓展任务 -5. 标签功能呢 -6. 接口参数范围,校验 \ No newline at end of file +# M2 - 基础用户系统 - 详细设计 (V2) + +--- + +## 1. 引言 + +### 1.1. 文档目的 + +本文档是 "M2 - 基础用户系统" 阶段的 **V2 修订版**,旨在提供一份详尽、准确、可直接用于开发的技术设计方案。V2 版根据反馈进行了全面重构,重点增强了数据库设计的合理性、API定义的完整性,并补充了关键的 `root` 用户机制和标签功能。 + +### 1.2. 模块范围 + +本设计覆盖功能点包括: + +- 用户认证(注册、登录)及 `root` 超级管理员机制。 +- 用户、角色、权限、组织、字典的CRUD管理,支持层级结构。 +- 用户标签系统。 +- 基于角色的访问控制(RBAC)的实现。 + +--- + +## 2. 数据库设计 (Revised) + +### 2.1. E-R 图 (Entity-Relationship Diagram) + +_已更新,包含标签和重构后的字典表_ + +```mermaid +erDiagram + users { + bigint id PK + varchar(50) username UK + varchar(255) email UK + varchar(255) password_hash + varchar(20) status + datetime created_at + datetime updated_at + } + + roles { + bigint id PK + varchar(50) name UK + text description + bigint parent_id FK "nullable, self-ref" + datetime created_at + datetime updated_at + } + + permissions { + bigint id PK + varchar(50) action UK + varchar(50) resource UK + text description + } + + organizations { + bigint id PK + varchar(100) name + text description + bigint parent_id FK "nullable, self-ref" + datetime created_at + datetime updated_at + } + + dictionary_types { + bigint id PK + varchar(50) type_name UK + text description + } + + dictionary_items { + bigint id PK + bigint type_id FK + varchar(50) `key` + varchar(255) value + int sort_order + } + + tags { + bigint id PK + varchar(50) name UK + } + + users ||--o{ user_roles : "has" + roles ||--o{ user_roles : "assigned to" + roles ||--o{ role_permissions : "has" + permissions ||--o{ role_permissions : "granted to" + users ||--o{ user_organizations : "belongs to" + organizations ||--o{ user_organizations : "has" + users ||--o{ user_tags : "has" + tags ||--o{ user_tags : "applied to" + dictionary_types ||--o{ dictionary_items : "has" + + roles }o..o| roles : "is child of" + organizations }o..o| organizations : "is child of" + + user_roles { bigint user_id PK,FK; bigint role_id PK,FK } + role_permissions { bigint role_id PK,FK; bigint permission_id PK,FK } + user_organizations { bigint user_id PK,FK; bigint organization_id PK,FK } + user_tags { bigint user_id PK,FK; bigint tag_id PK,FK } +``` + +### 2.2. 表结构定义 + +#### `roles` / `organizations` + +- **`parent_id`**: `BIGINT NULL`, `FOREIGN KEY (parent_id) REFERENCES roles(id) ON DELETE SET NULL`。此字段用于实现树形层级结构。值为`NULL`表示为顶级角色/组织。 + +#### `dictionary_types` - 字典类型表 + +| 字段名 | 类型 | 约束 | 描述 | +| ------------- | ------------- | -------------------- | ------------------------------- | +| `id` | `BIGINT` | `PK`, `AI` | 类型唯一ID | +| `type_name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL` | 字典类型名(如: `user_status`) | +| `description` | `TEXT` | | 类型描述 | + +#### `dictionary_items` - 字典条目表 + +| 字段名 | 类型 | 约束 | 描述 | +| ------------ | -------------- | ---------------- | --------------------------- | +| `id` | `BIGINT` | `PK`, `AI` | 条目唯一ID | +| `type_id` | `BIGINT` | `NOT NULL`, `FK` | 关联到`dictionary_types.id` | +| `key` | `VARCHAR(50)` | `NOT NULL` | 键 (程序中使用) | +| `value` | `VARCHAR(255)` | `NOT NULL` | 值 (UI中显示) | +| `sort_order` | `INT` | `DEFAULT 0` | 排序字段 | + +_复合唯一键: `(type_id, key)`_ + +#### `tags` - 标签表 + +| 字段名 | 类型 | 约束 | 描述 | +| ------ | ------------- | -------------------- | ---------- | +| `id` | `BIGINT` | `PK`, `AI` | 标签唯一ID | +| `name` | `VARCHAR(50)` | `UNIQUE`, `NOT NULL` | 标签名 | + +#### `user_tags` - 用户标签关联表 + +| 字段名 | 类型 | 约束 | 描述 | +| --------- | -------- | ---------- | ---------- | +| `user_id` | `BIGINT` | `PK`, `FK` | 关联用户ID | +| `tag_id` | `BIGINT` | `PK`, `FK` | 关联标签ID | + +--- + +## 3. Root 用户与系统初始化 + +- **`root` 用户**: + - `root` 是一个特殊的超级管理员账户,拥有系统中所有权限。 + - 此账户 **不可通过 API 创建或删除**。它应在系统首次部署时,通过数据库 **Seeding (种子填充) 脚本** 创建。 + - `root` 用户的角色是固定的(如 `super_admin`),此角色同样通过 Seeding 创建,并关联所有已定义的权限。 + - 任何权限校验逻辑都必须对 `root` 用户或 `super_admin` 角色开绿灯。任何API都不能降低 `root` 的权限或删除该用户。 + +--- + +## 4. API 接口设计 (Detailed) + +**Base URL**: `/api/v1` +**通用错误响应结构**: + +```json +{ + "code": 40001, // 详细业务错误码 + "message": "Validation failed: username must be at least 3 characters.", + "data": null +} +``` + +### 3.1. Auth - 认证接口 (`/auth`) + +#### `POST /register` - 用户注册 + +- **权限**: Public +- **请求体 (`application/json`)**: + | 名称 | 类型 | 必填 | 校验规则 | 描述 | + |------------|--------|------|----------------------------------------------------|----------------| + | `username` | string | 是 | min:3, max:50, 字母/数字/下划线组合 | 用户名 | + | `email` | string | 是 | valid email format | 邮箱地址 | + | `password` | string | 是 | min:8, max:100, 必须包含大小写字母和数字 | 密码 | +- **成功响应 (201 Created)**: + ```json + { + "code": 0, + "message": "User registered successfully.", + "data": { "id": 1, "username": "newuser", "email": "..." } + } + ``` +- **异常响应**: + | HTTP 码 | 业务码 | 原因 | + |---------|----------|----------------------------| + | 400 | 40001 | 请求参数不符合校验规则 | + | 409 | 40901 | 用户名或邮箱已被占用 | + +#### `POST /login` - 用户登录 + +- **权限**: Public +- **请求体 (`application/json`)**: + | 名称 | 类型 | 必填 | 校验规则 | 描述 | + |------------|--------|------|------------------|------------------| + | `username` | string | 是 | - | 用户名或邮箱 | + | `password` | string | 是 | - | 密码 | +- **成功响应 (200 OK)**: + ```json + { "code": 0, "message": "Login successful.", "data": { "token": "ey..." } } + ``` +- **异常响应**: + | HTTP 码 | 业务码 | 原因 | + |---------|----------|--------------------------| + | 401 | 40101 | 用户名或密码错误 | + | 401 | 40102 | 账户被禁用或未激活 | + +--- + +### 3.2. Users - 用户接口 (`/users`) + +_除特殊说明外,均需认证_ + +#### `GET /me` - 获取当前用户信息 + +- **权限**: Authenticated +- **成功响应 (200 OK)**: `data` 包含当前用户详细信息、关联的角色和权限列表。 + +#### `GET /` - 获取用户列表 + +- **权限**: `read:users` +- **查询参数**: + | 名称 | 类型 | 必填 | 描述 | + |-----------|--------|------|------------------------------------| + | `page` | number | 否 | 页码, default 1 | + | `pageSize`| number | 否 | 每页数量, default 10 | + | `keyword` | string | 否 | 按用户名或邮箱模糊搜索 | +- **成功响应 (200 OK)**: `data` 包含 `items` (用户列表) 和 `pagination` (分页信息)。 + +#### `PUT /{userId}/status` - 更新用户状态 + +- **权限**: `update:user_status` +- **请求体**: `{ "status": "active" }` (status 必须是字典中 `user_status` 类型的值) +- **成功响应 (200 OK)**: 返回更新后的用户信息。 +- **异常响应**: + | HTTP 码 | 业务码 | 原因 | + |---------|----------|--------------------------| + | 403 | 40301 | 试图修改 `root` 用户状态 | + | 404 | 40401 | 指定用户不存在 | + +#### `POST /{userId}/tags` - 为用户打标签 + +- **权限**: `update:user_tags` +- **请求体**: `{ "tagIds": [1, 2, 3] }` +- **成功响应 (204 No Content)**. + +--- + +### 3.3. Roles - 角色接口 (`/roles`) + +_所有接口均需 `manage:roles` 权限_ + +#### `GET /` - 获取角色列表 + +- **描述**: 以树形结构返回所有角色。 +- **成功响应 (200 OK)**: `data` 是一个包含 `children` 属性的树形数组。 + +#### `POST /` - 创建新角色 + +- **请求体**: `{ "name": "editor", "description": "...", "parentId": 1, "permissionIds": [101, 102] }` +- **成功响应 (201 Created)**: `data` 包含新创建的角色信息。 +- **异常响应 (409 Conflict)**: 角色名已存在。 + +--- + +_其他模块(Organizations, Dictionaries, Tags)的API将遵循类似的详细设计模式:提供完整的增删改查接口,明确定义权限、请求体、校验规则和所有可能的成功/异常响应。_ + +# 补充说明 + +1. 你忽略了我在main.md中提到的root用户,所以缺少一类集中管理的接口和机制,请认真阅读 +2. 接口设计太简陋了,太粗略了,需要把异常情况考虑进去, +3. 数据库设计完全无法满足需求,没有pid如何实现层级结构 +4. 字典确实,无法完成拓展任务 +5. 标签功能呢 +6. 接口参数范围,校验 diff --git a/prd/main.md b/prd/main.md index e4a1b0a..8abde16 100644 --- a/prd/main.md +++ b/prd/main.md @@ -1,65 +1,65 @@ -# 个人网站策划 - -## 前言 - -我想做一个个人博客网站需要有一下功能 - -1. 基本用户系统 - -- 字典,维护一些常用字典:地区省份、菜单类别、组织架构类别等,为以后其他功能开发提供可拓展的属性 - -- 角色,角色是树形结构,可以有标签、排序等功能 - -- 组织架构/用户组,是树形结构,用户可以在多个组织 - -- 用户标签,字典中维护的一些用户属性 - -- 权限,包括页面权限,接口权限和数据权限等,下级角色只能从上级角色已有的权限中分配 - -- 用户,用户基础信息,用户拓展信息,用户创建,注册通过邮箱注册,可以限制错误登录次数,也可以重置,可以禁用用户,注销用户,找回密码,修改个人信息,最高管理员为root账户,他能做所有事情 - -2. 笔记/博客系统 - -- 笔记结构是树形的,可以增加标签 - -- 笔记可以发布为博客,即设置为公开内容,博客是单独的内容,修改笔记不影响博客能容,但是有绑定关系,可以更新和重新发布 - -- 博客可以点赞、评论、收藏,也是层级结构,可以增加标签分类 - -- 内容有markdown、富文本、思维导图、画板,结构通过关系型数据库存储,内容可以通过其他数据库存储,如mongodb、es等,增强搜索性能,可以嵌入文件等 - -- 对代码和编程友好 - -- 可以接入ai,mcp等,将笔记作为知识库,增强实用性 - -3. 个人空间 - -- 生活日志,分享照片、视频等 - -- 可以评论、收藏、分享、点赞 - -4. 阅读 - -- 可以上传自己下载的书,支持多种格式 - -- ai解读 - -- 评论、批注、分享 - -5. 工具 - -- 密码记录器(同步开发浏览器插件) - -- 书签记录同步器(同步开发浏览器插件) - -- bilibili收藏同步 - -- qq音乐、网易云音乐同步 - -- 文件管理:nas、支持在线音视频播放、媒体文件预览 - -- 文件对传:WEBRTC、P2P、阿里云OSS - -- 热点新闻捕获 - -- 网页离线存储,做成pdf \ No newline at end of file +# 个人网站策划 + +## 前言 + +我想做一个个人博客网站需要有一下功能 + +1. 基本用户系统 + +- 字典,维护一些常用字典:地区省份、菜单类别、组织架构类别等,为以后其他功能开发提供可拓展的属性 + +- 角色,角色是树形结构,可以有标签、排序等功能 + +- 组织架构/用户组,是树形结构,用户可以在多个组织 + +- 用户标签,字典中维护的一些用户属性 + +- 权限,包括页面权限,接口权限和数据权限等,下级角色只能从上级角色已有的权限中分配 + +- 用户,用户基础信息,用户拓展信息,用户创建,注册通过邮箱注册,可以限制错误登录次数,也可以重置,可以禁用用户,注销用户,找回密码,修改个人信息,最高管理员为root账户,他能做所有事情 + +2. 笔记/博客系统 + +- 笔记结构是树形的,可以增加标签 + +- 笔记可以发布为博客,即设置为公开内容,博客是单独的内容,修改笔记不影响博客能容,但是有绑定关系,可以更新和重新发布 + +- 博客可以点赞、评论、收藏,也是层级结构,可以增加标签分类 + +- 内容有markdown、富文本、思维导图、画板,结构通过关系型数据库存储,内容可以通过其他数据库存储,如mongodb、es等,增强搜索性能,可以嵌入文件等 + +- 对代码和编程友好 + +- 可以接入ai,mcp等,将笔记作为知识库,增强实用性 + +3. 个人空间 + +- 生活日志,分享照片、视频等 + +- 可以评论、收藏、分享、点赞 + +4. 阅读 + +- 可以上传自己下载的书,支持多种格式 + +- ai解读 + +- 评论、批注、分享 + +5. 工具 + +- 密码记录器(同步开发浏览器插件) + +- 书签记录同步器(同步开发浏览器插件) + +- bilibili收藏同步 + +- qq音乐、网易云音乐同步 + +- 文件管理:nas、支持在线音视频播放、媒体文件预览 + +- 文件对传:WEBRTC、P2P、阿里云OSS + +- 热点新闻捕获 + +- 网页离线存储,做成pdf diff --git a/prd/personal-website-comprehensive-prd.md b/prd/personal-website-comprehensive-prd.md index 320d2f4..212a6ca 100644 --- a/prd/personal-website-comprehensive-prd.md +++ b/prd/personal-website-comprehensive-prd.md @@ -1,455 +1,512 @@ -# 个人网站综合平台产品需求文档 (PRD) - -## 引言/概述 - -### 项目背景 -构建一个集成化的个人网站平台,涵盖内容创作、知识管理、生活记录、实用工具等多个维度,旨在打造一个功能完整、用户体验优秀的个人数字空间。 - -### 核心价值 -- **知识管理中心**:统一管理个人笔记、博客、阅读内容 -- **生活记录平台**:记录和分享个人生活点滴 -- **实用工具集**:集成常用的个人效率工具 -- **智能化体验**:通过AI增强内容创作和知识检索 - -## 目标 - -### 主要目标 -1. **建立完整的个人数字资产管理体系** -2. **提供便捷的内容创作和发布平台** -3. **构建实用的个人效率工具集** -4. **实现智能化的知识管理和检索** - -### 可衡量目标 -- 支持多种内容格式(Markdown、富文本、思维导图、画板) -- 实现秒级内容搜索响应 -- 支持10+种实用工具集成 -- 提供完整的权限管理体系 - -## 用户故事 (User Stories) - -- **作为一名创作者**, 我希望能够在一个地方写笔记、画思维导图,并且能将它们无缝地整理成树状结构,方便我构建自己的知识体系。 -- **作为一名博主**, 我希望可以一键将我精心撰写的笔记发布成一篇公开的博客,并且发布后对笔记的修改不会影响已发布的博客,除非我选择更新它。 -- **作为一名普通用户**, 我希望能用我的邮箱快速注册一个账号,并且可以管理我的个人信息、重置密码,确保我的账户安全。 -- **作为一名开发者**, 我希望有一个工具能帮我安全地记录各种网站的密码,并通过浏览器插件自动填充,还能同步我的书签,提升我的工作效率。 -- **作为一名生活记录者**, 我希望能方便地上传照片和视频,以时间线的形式记录我的生活点滴,并能设置只对特定的人开放。 - -## 功能模块详述 - -### 1. 基础用户系统模块 - -#### 1.1 字典管理子系统 -**功能描述**:维护系统中使用的各类数据字典 -- **地区字典**:省份、城市、区县三级联动 -- **分类字典**:菜单类别、标签类别、文件类型等 -- **组织字典**:部门类型、角色类型、权限类型 -- **状态字典**:用户状态、内容状态、审核状态等 -- **扩展字典**:支持自定义字典类型 -**验收标准**: -- 管理员可以增删改查字典类型和字典项。 -- 系统关键状态(如用户禁用、启用)应与字典数据关联。 -- 前端下拉框、选项等数据应能通过API动态获取字典。 - -#### 1.2 角色权限子系统 -**功能描述**:基于RBAC模型的权限管理体系 -- **角色管理**: - - 树形结构的角色层级 - - 角色继承机制 - - 角色标签和属性 - - 角色排序和分组 -- **权限管理**: - - 页面访问权限 - - 接口调用权限 - - 数据访问权限(行级、字段级) - - 操作权限(增删改查) -- **权限分配**: - - 下级角色只能分配上级已有权限 - - 批量权限分配 - - 权限模板机制 -**验收标准**: -- Root管理员拥有所有权限,且不可被修改。 -- 角色创建时,其可分配的权限不能超过创建者拥有的权限。 -- 用户访问未授权的页面或API时,应返回403 Forbidden状态。 -- 角色的树形结构应能正确地在前端展示和操作。 - -#### 1.3 组织架构子系统 -**功能描述**:管理用户组织关系 -- **组织结构**: - - 树形组织架构 - - 支持多级嵌套 - - 组织属性和标签 -- **用户归属**: - - 用户可属于多个组织 - - 主组织和辅助组织 - - 组织内角色分配 -**验收标准**: -- 用户可以被分配到多个组织节点下。 -- 组织架构支持拖拽调整层级和顺序。 -- 管理员可以根据组织架构筛选和管理用户。 - -#### 1.4 用户管理子系统 -**功能描述**:完整的用户生命周期管理 -- **用户注册**: - - 邮箱注册验证 - - 手机号注册(可选) - - 社交账号登录集成 -- **用户认证**: - - 密码登录 - - 登录错误次数限制 - - 账户锁定和解锁机制 - - 双因子认证(可选) -- **用户信息**: - - 基础信息:用户名、邮箱、手机、头像 - - 扩展信息:个人简介、兴趣标签、社交链接 - - 隐私设置:信息可见性控制 -- **账户管理**: - - 密码修改和重置 - - 找回密码(邮箱/手机) - - 账户禁用和注销 - - Root超级管理员 -**验收标准**: -- 新用户注册后,系统需发送验证邮件,点击链接后方可激活账户。 -- 用户连续5次输错密码后,账户应被锁定30分钟。 -- 用户可以随时修改自己的个人信息(除用户名外)。 -- 注销功能需要用户二次确认,并告知数据将被永久删除。 - -### 2. 内容管理系统模块 - -#### 2.1 笔记管理子系统 -**功能描述**:个人知识管理和笔记系统 -- **笔记结构**: - - 树形文件夹结构 - - 无限层级嵌套 - - 文件夹和笔记混合组织 -- **内容格式**: - - Markdown编辑器 - - 富文本编辑器 - - 思维导图工具 - - 在线画板工具 - - 代码片段高亮 -- **笔记功能**: - - 实时保存 - - 版本历史 - - 标签分类 - - 全文搜索 - - 笔记链接和引用 - - 附件管理(图片、文档、音视频) -- **协作功能**: - - 笔记分享(链接分享、权限控制) - - 协作编辑(可选) - - 评论和批注 -**验收标准**: -- 笔记内容在编辑时应每隔30秒自动保存一次。 -- 支持至少三级以上的树形目录结构。 -- 全文搜索功能应能在1秒内返回标题或内容包含关键词的结果。 -- 分享的笔记链接可以设置有效期和密码。 - -#### 2.2 博客发布子系统 -**功能描述**:将笔记转化为公开博客 -- **发布机制**: - - 笔记一键发布为博客 - - 博客独立存储(修改笔记不影响已发布博客) - - 绑定关系维护 - - 重新发布和更新机制 -- **博客管理**: - - 发布状态管理(草稿、已发布、下线) - - SEO优化(标题、描述、关键词) - - 分类和标签 - - 发布时间控制 -- **互动功能**: - - 点赞和收藏 - - 评论系统(层级评论) - - 阅读量统计 - - 分享功能 -- **展示功能**: - - 博客列表和详情页 - - 分类和标签筛选 - - 搜索功能 - - RSS订阅 -**验收标准**: -- 从笔记发布博客后,二者内容各自独立。 -- 博客可以关联多个分类和标签。 -- 未发布的博客(草稿)在公开列表不可见。 -- 评论支持层级回复,并有新评论通知。 - -#### 2.3 AI集成子系统 -**功能描述**:AI增强的内容创作和知识管理 -- **内容创作辅助**: - - AI写作建议 - - 文本润色和优化 - - 自动摘要生成 - - 关键词提取 -- **知识库功能**: - - 笔记内容向量化 - - 智能检索和推荐 - - 相关内容关联 - - 问答系统 -- **MCP集成**: - - 多模态内容处理 - - 代码理解和生成 - - 图像识别和描述 - -### 3. 个人空间模块 - -#### 3.1 生活日志子系统 -**功能描述**:记录和分享个人生活 -- **内容类型**: - - 文字日志 - - 图片分享(支持多图) - - 视频分享 - - 位置签到 - - 心情记录 -- **组织方式**: - - 时间轴展示 - - 分类管理 - - 标签系统 - - 重要程度标记 -- **隐私控制**: - - 公开/私密/好友可见 - - 精细化权限控制 - - 访客记录 - -#### 3.2 社交互动子系统 -**功能描述**:与访客的互动功能 -- **互动功能**: - - 点赞系统 - - 评论功能 - - 收藏功能 - - 分享功能 -- **通知系统**: - - 实时通知 - - 消息聚合 - - 邮件通知(可选) - -### 4. 阅读管理模块 - -#### 4.1 电子书管理子系统 -**功能描述**:个人电子书库管理 -- **格式支持**: - - PDF、EPUB、MOBI、TXT等 - - 在线阅读器 - - 进度同步 - -#### 4.2 阅读社区子系统 -**功能描述**:阅读交流和分享 -- **分享功能**: - - 读书笔记分享 - - 书评发布 - - 阅读进度分享 - -### 5. 实用工具模块 - -#### 5.1 密码管理子系统 -**功能描述**:个人密码安全管理 -- **密码存储**: - - 加密存储 - - 分类管理 - - 标签系统 -- **浏览器插件**: - - 自动填充 - - 密码生成 - - 同步功能 - -#### 5.2 书签同步子系统 -**功能描述**:跨设备书签管理 -- **同步功能**: - - 浏览器书签导入/导出 - - 实时同步 - - 冲突解决 - -#### 5.3 第三方平台同步子系统 -**功能描述**:整合各平台数据 -- **Bilibili收藏同步**: - - 收藏视频同步 - - 分类管理 - - 观看进度 - -#### 5.4 文件管理子系统 -**功能描述**:个人云存储和媒体中心 -- **存储功能**: - - NAS集成 - - 文件上传下载 - - 文件夹管理 - - 权限控制 -- **媒体功能**: - - 在线音视频播放 - - 图片预览 - - 文档在线查看 -- **传输功能**: - - WebRTC点对点传输 - - 阿里云OSS集成 - - 断点续传 - -#### 5.5 信息聚合子系统 -**功能描述**:热点信息和内容聚合 -- **新闻聚合**: - - 多源新闻抓取 - - 分类展示 - - 关键词过滤 - -## 高阶数据模型概览 (High-Level Data Model) - -此为概念模型,用于指导数据库设计,具体字段和关系将在详细设计阶段确定。 - -- **核心实体**: - - `User`: 存储用户信息 (id, username, email, password_hash) - - `Role`: 角色定义 (id, name, description) - - `Permission`: 权限定义 (id, action, resource) - - `Organization`: 组织架构 (id, name, parent_id) - - `Dictionary`: 数据字典 (id, type, key, value) - - `Note`: 笔记内容 (id, user_id, title, content, content_type, parent_id) - - `Post`: 博客文章 (id, user_id, source_note_id, title, content, status) - - `Tag`: 标签 (id, name) - - `Category`: 分类 (id, name) - - `Comment`: 评论 (id, post_id, user_id, content, parent_id) - - `File`: 文件管理 (id, user_id, file_name, path, type) - - `Bookmark`: 书签 (id, user_id, title, url) - - `Password`: 密码记录 (id, user_id, entry_name, username, encrypted_password) - -- **关系**: - - `User` (多) <-> (多) `Role` (通过 `UserRole` 中间表) - - `Role` (多) <-> (多) `Permission` (通过 `RolePermission` 中间表) - - `User` (多) <-> (多) `Organization` (通过 `UserOrganization` 中间表) - - `Note` (一) -> (多) `Post` - - `Post` (多) <-> (多) `Tag` (通过 `PostTag` 中间表) - - `Post` (多) <-> (多) `Category` (通过 `PostCategory` 中间表) - - `Post` (一) -> (多) `Comment` - - `User` (一) -> (多) `Note`, `Post`, `File`, `Bookmark`, `Password` - -## 关键用户交互流程 (Key User Flows) - -### 流程一:新用户注册与激活 -1. **访问首页**: 用户访问网站,点击"注册"按钮。 -2. **填写信息**: 跳转至注册页面,要求输入用户名、邮箱和密码(密码需二次确认)。 -3. **前端校验**: 实时校验用户名和邮箱是否已被占用,密码是否符合复杂度要求。 -4. **提交注册**: 用户点击"注册",前端校验通过后,将数据发送至后端。 -5. **后端处理**: 后端创建用户记录(状态为`inactive`),生成验证Token,并向用户邮箱发送一封包含激活链接(含Token)的邮件。 -6. **用户激活**: 用户打开邮箱,点击激活链接。 -7. **激活验证**: 后端验证Token有效性,将用户状态更新为`active`,并引导用户至登录页面,提示"激活成功"。 - -### 流程二:从笔记到博客的发布 -1. **创建笔记**: 用户在笔记系统中创建并编辑一篇笔记,内容可以是Markdown、富文本等。 -2. **发起发布**: 在笔记编辑页面,用户点击"发布为博客"按钮。 -3. **配置博客信息**: 弹窗或新页面要求用户填写博客的URL slug、选择分类、添加标签、设置封面图等。 -4. **确认发布**: 用户点击"确认发布"。 -5. **后端处理**: - - 后端在`Posts`表中创建一条新记录。 - - 复制当前版本的笔记内容到该`Post`记录中。 - - 记录该`Post`与源`Note`的关联关系 (`source_note_id`)。 - - 将博客状态设置为`published`。 -6. **发布成功**: 系统提示"发布成功",并提供查看博客文章的链接。 -7. **后续修改**: 用户后续在原笔记上的任何修改,都不会影响已发布的这篇博客文章。用户可在博客管理界面选择"从笔记更新内容"来同步最新修改。 - -## 技术架构需求 - -### 后端技术栈 -- **框架**:Elysia + Bun.js -- **数据库**:MySQL (主) + Redis (缓存) + Elasticsearch (搜索) -- **对象存储**:支持本地存储、阿里云OSS、自建NAS -- **消息队列**:Redis/RabbitMQ(处理异步任务) - -### 前端技术栈 -- **Web端**:Vue.js/React + TypeScript -- **移动端**:PWA或React Native -- **浏览器插件**:Manifest V3 - -### 基础设施 -- **部署**:Docker + Docker Compose -- **监控**:日志收集、性能监控、错误追踪 -- **安全**:HTTPS、数据加密、访问控制 - -### API设计原则 -- **RESTful风格**: 使用标准的HTTP方法 (GET, POST, PUT, DELETE, PATCH)。 -- **版本控制**: API URL中应包含版本号,如 `/api/v1/...`。 -- **统一响应格式**: 所有响应遵循 `{ "code": 0, "message": "success", "data": {} }` 结构。 -- **分页**: 对列表数据提供统一的分页参数,如 `page` 和 `pageSize`。 -- **排序与筛选**: 支持通过URL参数对结果进行排序和筛选。 -- **认证**: 受保护的API需通过`Authorization`头传递JWT。 - -## 非功能性需求 - -### 性能要求 -- **响应时间**:页面加载<2s,接口响应<500ms -- **并发支持**:支持1000+并发用户 -- **可用性**:99.9%系统可用性 - -### 安全要求 -- **数据加密**:敏感数据加密存储和传输 -- **访问控制**:完善的认证和授权机制 -- **审计日志**:关键操作日志记录 - -### 扩展性要求 -- **模块化设计**:支持功能模块独立部署 -- **API设计**:RESTful API,支持版本控制 -- **数据库**:支持读写分离和分库分表 - -## 开发优先级 - -### 第一期:核心基础(MVP) -1. 基础用户系统(用户管理、角色权限) -2. 笔记系统核心功能 -3. 基础博客发布 -4. 系统基础架构 - -### 第二期:内容增强 -1. 完整的博客系统 -2. 个人空间功能 -3. 基础工具(密码管理、书签) -4. 搜索优化 - -### 第三期:高级功能 -1. 阅读管理系统 -2. 文件管理系统 -3. AI集成功能 -4. 第三方平台同步 - -### 第四期:生态完善 -1. 移动端应用 -2. 浏览器插件 -3. 高级分析功能 -4. 性能优化 - -## 风险与挑战 - -### 技术风险 -- **数据迁移**:大量历史数据的迁移和同步 -- **性能优化**:大文件上传和处理 -- **第三方集成**:API变更和限制 - -### 产品风险 -- **功能复杂度**:避免功能过于复杂影响用户体验 -- **数据安全**:个人敏感数据的安全保护 -- **兼容性**:跨浏览器和设备兼容 - -## 成功指标 - -### 用户指标 -- 日活跃用户数 -- 用户留存率 -- 功能使用率 - -### 技术指标 -- 系统响应时间 -- 错误率控制 -- 数据备份完整性 - -### 业务指标 -- 内容创作量 -- 用户满意度 -- 功能完成度 - -## 后续规划 - -### 短期目标(6个月内) -- 完成第一期功能开发 -- 系统稳定性优化 -- 基础功能测试 - -### 中期目标(1年内) -- 完成主要功能模块 -- 移动端支持 -- 性能优化 - -### 长期目标(1年以上) -- AI功能深度集成 -- 开放API生态 -- 多语言支持 \ No newline at end of file +# 个人网站综合平台产品需求文档 (PRD) + +## 引言/概述 + +### 项目背景 + +构建一个集成化的个人网站平台,涵盖内容创作、知识管理、生活记录、实用工具等多个维度,旨在打造一个功能完整、用户体验优秀的个人数字空间。 + +### 核心价值 + +- **知识管理中心**:统一管理个人笔记、博客、阅读内容 +- **生活记录平台**:记录和分享个人生活点滴 +- **实用工具集**:集成常用的个人效率工具 +- **智能化体验**:通过AI增强内容创作和知识检索 + +## 目标 + +### 主要目标 + +1. **建立完整的个人数字资产管理体系** +2. **提供便捷的内容创作和发布平台** +3. **构建实用的个人效率工具集** +4. **实现智能化的知识管理和检索** + +### 可衡量目标 + +- 支持多种内容格式(Markdown、富文本、思维导图、画板) +- 实现秒级内容搜索响应 +- 支持10+种实用工具集成 +- 提供完整的权限管理体系 + +## 用户故事 (User Stories) + +- **作为一名创作者**, 我希望能够在一个地方写笔记、画思维导图,并且能将它们无缝地整理成树状结构,方便我构建自己的知识体系。 +- **作为一名博主**, 我希望可以一键将我精心撰写的笔记发布成一篇公开的博客,并且发布后对笔记的修改不会影响已发布的博客,除非我选择更新它。 +- **作为一名普通用户**, 我希望能用我的邮箱快速注册一个账号,并且可以管理我的个人信息、重置密码,确保我的账户安全。 +- **作为一名开发者**, 我希望有一个工具能帮我安全地记录各种网站的密码,并通过浏览器插件自动填充,还能同步我的书签,提升我的工作效率。 +- **作为一名生活记录者**, 我希望能方便地上传照片和视频,以时间线的形式记录我的生活点滴,并能设置只对特定的人开放。 + +## 功能模块详述 + +### 1. 基础用户系统模块 + +#### 1.1 字典管理子系统 + +**功能描述**:维护系统中使用的各类数据字典 + +- **地区字典**:省份、城市、区县三级联动 +- **分类字典**:菜单类别、标签类别、文件类型等 +- **组织字典**:部门类型、角色类型、权限类型 +- **状态字典**:用户状态、内容状态、审核状态等 +- **扩展字典**:支持自定义字典类型 + **验收标准**: +- 管理员可以增删改查字典类型和字典项。 +- 系统关键状态(如用户禁用、启用)应与字典数据关联。 +- 前端下拉框、选项等数据应能通过API动态获取字典。 + +#### 1.2 角色权限子系统 + +**功能描述**:基于RBAC模型的权限管理体系 + +- **角色管理**: + - 树形结构的角色层级 + - 角色继承机制 + - 角色标签和属性 + - 角色排序和分组 +- **权限管理**: + - 页面访问权限 + - 接口调用权限 + - 数据访问权限(行级、字段级) + - 操作权限(增删改查) +- **权限分配**: + - 下级角色只能分配上级已有权限 + - 批量权限分配 + - 权限模板机制 + **验收标准**: +- Root管理员拥有所有权限,且不可被修改。 +- 角色创建时,其可分配的权限不能超过创建者拥有的权限。 +- 用户访问未授权的页面或API时,应返回403 Forbidden状态。 +- 角色的树形结构应能正确地在前端展示和操作。 + +#### 1.3 组织架构子系统 + +**功能描述**:管理用户组织关系 + +- **组织结构**: + - 树形组织架构 + - 支持多级嵌套 + - 组织属性和标签 +- **用户归属**: + - 用户可属于多个组织 + - 主组织和辅助组织 + - 组织内角色分配 + **验收标准**: +- 用户可以被分配到多个组织节点下。 +- 组织架构支持拖拽调整层级和顺序。 +- 管理员可以根据组织架构筛选和管理用户。 + +#### 1.4 用户管理子系统 + +**功能描述**:完整的用户生命周期管理 + +- **用户注册**: + - 邮箱注册验证 + - 手机号注册(可选) + - 社交账号登录集成 +- **用户认证**: + - 密码登录 + - 登录错误次数限制 + - 账户锁定和解锁机制 + - 双因子认证(可选) +- **用户信息**: + - 基础信息:用户名、邮箱、手机、头像 + - 扩展信息:个人简介、兴趣标签、社交链接 + - 隐私设置:信息可见性控制 +- **账户管理**: + - 密码修改和重置 + - 找回密码(邮箱/手机) + - 账户禁用和注销 + - Root超级管理员 + **验收标准**: +- 新用户注册后,系统需发送验证邮件,点击链接后方可激活账户。 +- 用户连续5次输错密码后,账户应被锁定30分钟。 +- 用户可以随时修改自己的个人信息(除用户名外)。 +- 注销功能需要用户二次确认,并告知数据将被永久删除。 + +### 2. 内容管理系统模块 + +#### 2.1 笔记管理子系统 + +**功能描述**:个人知识管理和笔记系统 + +- **笔记结构**: + - 树形文件夹结构 + - 无限层级嵌套 + - 文件夹和笔记混合组织 +- **内容格式**: + - Markdown编辑器 + - 富文本编辑器 + - 思维导图工具 + - 在线画板工具 + - 代码片段高亮 +- **笔记功能**: + - 实时保存 + - 版本历史 + - 标签分类 + - 全文搜索 + - 笔记链接和引用 + - 附件管理(图片、文档、音视频) +- **协作功能**: + - 笔记分享(链接分享、权限控制) + - 协作编辑(可选) + - 评论和批注 + **验收标准**: +- 笔记内容在编辑时应每隔30秒自动保存一次。 +- 支持至少三级以上的树形目录结构。 +- 全文搜索功能应能在1秒内返回标题或内容包含关键词的结果。 +- 分享的笔记链接可以设置有效期和密码。 + +#### 2.2 博客发布子系统 + +**功能描述**:将笔记转化为公开博客 + +- **发布机制**: + - 笔记一键发布为博客 + - 博客独立存储(修改笔记不影响已发布博客) + - 绑定关系维护 + - 重新发布和更新机制 +- **博客管理**: + - 发布状态管理(草稿、已发布、下线) + - SEO优化(标题、描述、关键词) + - 分类和标签 + - 发布时间控制 +- **互动功能**: + - 点赞和收藏 + - 评论系统(层级评论) + - 阅读量统计 + - 分享功能 +- **展示功能**: + - 博客列表和详情页 + - 分类和标签筛选 + - 搜索功能 + - RSS订阅 + **验收标准**: +- 从笔记发布博客后,二者内容各自独立。 +- 博客可以关联多个分类和标签。 +- 未发布的博客(草稿)在公开列表不可见。 +- 评论支持层级回复,并有新评论通知。 + +#### 2.3 AI集成子系统 + +**功能描述**:AI增强的内容创作和知识管理 + +- **内容创作辅助**: + - AI写作建议 + - 文本润色和优化 + - 自动摘要生成 + - 关键词提取 +- **知识库功能**: + - 笔记内容向量化 + - 智能检索和推荐 + - 相关内容关联 + - 问答系统 +- **MCP集成**: + - 多模态内容处理 + - 代码理解和生成 + - 图像识别和描述 + +### 3. 个人空间模块 + +#### 3.1 生活日志子系统 + +**功能描述**:记录和分享个人生活 + +- **内容类型**: + - 文字日志 + - 图片分享(支持多图) + - 视频分享 + - 位置签到 + - 心情记录 +- **组织方式**: + - 时间轴展示 + - 分类管理 + - 标签系统 + - 重要程度标记 +- **隐私控制**: + - 公开/私密/好友可见 + - 精细化权限控制 + - 访客记录 + +#### 3.2 社交互动子系统 + +**功能描述**:与访客的互动功能 + +- **互动功能**: + - 点赞系统 + - 评论功能 + - 收藏功能 + - 分享功能 +- **通知系统**: + - 实时通知 + - 消息聚合 + - 邮件通知(可选) + +### 4. 阅读管理模块 + +#### 4.1 电子书管理子系统 + +**功能描述**:个人电子书库管理 + +- **格式支持**: + - PDF、EPUB、MOBI、TXT等 + - 在线阅读器 + - 进度同步 + +#### 4.2 阅读社区子系统 + +**功能描述**:阅读交流和分享 + +- **分享功能**: + - 读书笔记分享 + - 书评发布 + - 阅读进度分享 + +### 5. 实用工具模块 + +#### 5.1 密码管理子系统 + +**功能描述**:个人密码安全管理 + +- **密码存储**: + - 加密存储 + - 分类管理 + - 标签系统 +- **浏览器插件**: + - 自动填充 + - 密码生成 + - 同步功能 + +#### 5.2 书签同步子系统 + +**功能描述**:跨设备书签管理 + +- **同步功能**: + - 浏览器书签导入/导出 + - 实时同步 + - 冲突解决 + +#### 5.3 第三方平台同步子系统 + +**功能描述**:整合各平台数据 + +- **Bilibili收藏同步**: + - 收藏视频同步 + - 分类管理 + - 观看进度 + +#### 5.4 文件管理子系统 + +**功能描述**:个人云存储和媒体中心 + +- **存储功能**: + - NAS集成 + - 文件上传下载 + - 文件夹管理 + - 权限控制 +- **媒体功能**: + - 在线音视频播放 + - 图片预览 + - 文档在线查看 +- **传输功能**: + - WebRTC点对点传输 + - 阿里云OSS集成 + - 断点续传 + +#### 5.5 信息聚合子系统 + +**功能描述**:热点信息和内容聚合 + +- **新闻聚合**: + - 多源新闻抓取 + - 分类展示 + - 关键词过滤 + +## 高阶数据模型概览 (High-Level Data Model) + +此为概念模型,用于指导数据库设计,具体字段和关系将在详细设计阶段确定。 + +- **核心实体**: + - `User`: 存储用户信息 (id, username, email, password_hash) + - `Role`: 角色定义 (id, name, description) + - `Permission`: 权限定义 (id, action, resource) + - `Organization`: 组织架构 (id, name, parent_id) + - `Dictionary`: 数据字典 (id, type, key, value) + - `Note`: 笔记内容 (id, user_id, title, content, content_type, parent_id) + - `Post`: 博客文章 (id, user_id, source_note_id, title, content, status) + - `Tag`: 标签 (id, name) + - `Category`: 分类 (id, name) + - `Comment`: 评论 (id, post_id, user_id, content, parent_id) + - `File`: 文件管理 (id, user_id, file_name, path, type) + - `Bookmark`: 书签 (id, user_id, title, url) + - `Password`: 密码记录 (id, user_id, entry_name, username, encrypted_password) + +- **关系**: + - `User` (多) <-> (多) `Role` (通过 `UserRole` 中间表) + - `Role` (多) <-> (多) `Permission` (通过 `RolePermission` 中间表) + - `User` (多) <-> (多) `Organization` (通过 `UserOrganization` 中间表) + - `Note` (一) -> (多) `Post` + - `Post` (多) <-> (多) `Tag` (通过 `PostTag` 中间表) + - `Post` (多) <-> (多) `Category` (通过 `PostCategory` 中间表) + - `Post` (一) -> (多) `Comment` + - `User` (一) -> (多) `Note`, `Post`, `File`, `Bookmark`, `Password` + +## 关键用户交互流程 (Key User Flows) + +### 流程一:新用户注册与激活 + +1. **访问首页**: 用户访问网站,点击"注册"按钮。 +2. **填写信息**: 跳转至注册页面,要求输入用户名、邮箱和密码(密码需二次确认)。 +3. **前端校验**: 实时校验用户名和邮箱是否已被占用,密码是否符合复杂度要求。 +4. **提交注册**: 用户点击"注册",前端校验通过后,将数据发送至后端。 +5. **后端处理**: 后端创建用户记录(状态为`inactive`),生成验证Token,并向用户邮箱发送一封包含激活链接(含Token)的邮件。 +6. **用户激活**: 用户打开邮箱,点击激活链接。 +7. **激活验证**: 后端验证Token有效性,将用户状态更新为`active`,并引导用户至登录页面,提示"激活成功"。 + +### 流程二:从笔记到博客的发布 + +1. **创建笔记**: 用户在笔记系统中创建并编辑一篇笔记,内容可以是Markdown、富文本等。 +2. **发起发布**: 在笔记编辑页面,用户点击"发布为博客"按钮。 +3. **配置博客信息**: 弹窗或新页面要求用户填写博客的URL slug、选择分类、添加标签、设置封面图等。 +4. **确认发布**: 用户点击"确认发布"。 +5. **后端处理**: + - 后端在`Posts`表中创建一条新记录。 + - 复制当前版本的笔记内容到该`Post`记录中。 + - 记录该`Post`与源`Note`的关联关系 (`source_note_id`)。 + - 将博客状态设置为`published`。 +6. **发布成功**: 系统提示"发布成功",并提供查看博客文章的链接。 +7. **后续修改**: 用户后续在原笔记上的任何修改,都不会影响已发布的这篇博客文章。用户可在博客管理界面选择"从笔记更新内容"来同步最新修改。 + +## 技术架构需求 + +### 后端技术栈 + +- **框架**:Elysia + Bun.js +- **数据库**:MySQL (主) + Redis (缓存) + Elasticsearch (搜索) +- **对象存储**:支持本地存储、阿里云OSS、自建NAS +- **消息队列**:Redis/RabbitMQ(处理异步任务) + +### 前端技术栈 + +- **Web端**:Vue.js/React + TypeScript +- **移动端**:PWA或React Native +- **浏览器插件**:Manifest V3 + +### 基础设施 + +- **部署**:Docker + Docker Compose +- **监控**:日志收集、性能监控、错误追踪 +- **安全**:HTTPS、数据加密、访问控制 + +### API设计原则 + +- **RESTful风格**: 使用标准的HTTP方法 (GET, POST, PUT, DELETE, PATCH)。 +- **版本控制**: API URL中应包含版本号,如 `/api/v1/...`。 +- **统一响应格式**: 所有响应遵循 `{ "code": 0, "message": "success", "data": {} }` 结构。 +- **分页**: 对列表数据提供统一的分页参数,如 `page` 和 `pageSize`。 +- **排序与筛选**: 支持通过URL参数对结果进行排序和筛选。 +- **认证**: 受保护的API需通过`Authorization`头传递JWT。 + +## 非功能性需求 + +### 性能要求 + +- **响应时间**:页面加载<2s,接口响应<500ms +- **并发支持**:支持1000+并发用户 +- **可用性**:99.9%系统可用性 + +### 安全要求 + +- **数据加密**:敏感数据加密存储和传输 +- **访问控制**:完善的认证和授权机制 +- **审计日志**:关键操作日志记录 + +### 扩展性要求 + +- **模块化设计**:支持功能模块独立部署 +- **API设计**:RESTful API,支持版本控制 +- **数据库**:支持读写分离和分库分表 + +## 开发优先级 + +### 第一期:核心基础(MVP) + +1. 基础用户系统(用户管理、角色权限) +2. 笔记系统核心功能 +3. 基础博客发布 +4. 系统基础架构 + +### 第二期:内容增强 + +1. 完整的博客系统 +2. 个人空间功能 +3. 基础工具(密码管理、书签) +4. 搜索优化 + +### 第三期:高级功能 + +1. 阅读管理系统 +2. 文件管理系统 +3. AI集成功能 +4. 第三方平台同步 + +### 第四期:生态完善 + +1. 移动端应用 +2. 浏览器插件 +3. 高级分析功能 +4. 性能优化 + +## 风险与挑战 + +### 技术风险 + +- **数据迁移**:大量历史数据的迁移和同步 +- **性能优化**:大文件上传和处理 +- **第三方集成**:API变更和限制 + +### 产品风险 + +- **功能复杂度**:避免功能过于复杂影响用户体验 +- **数据安全**:个人敏感数据的安全保护 +- **兼容性**:跨浏览器和设备兼容 + +## 成功指标 + +### 用户指标 + +- 日活跃用户数 +- 用户留存率 +- 功能使用率 + +### 技术指标 + +- 系统响应时间 +- 错误率控制 +- 数据备份完整性 + +### 业务指标 + +- 内容创作量 +- 用户满意度 +- 功能完成度 + +## 后续规划 + +### 短期目标(6个月内) + +- 完成第一期功能开发 +- 系统稳定性优化 +- 基础功能测试 + +### 中期目标(1年内) + +- 完成主要功能模块 +- 移动端支持 +- 性能优化 + +### 长期目标(1年以上) + +- AI功能深度集成 +- 开放API生态 +- 多语言支持 diff --git a/prd/星撰-工程设计.md b/prd/星撰-工程设计.md index 998d58d..ab59ebd 100644 --- a/prd/星撰-工程设计.md +++ b/prd/星撰-工程设计.md @@ -1,128 +1,128 @@ -# 星撰个人综合平台 - 项目工程设计文档 - ---- - -## 0. 引言 - -本文档旨在为"星撰"个人综合平台项目提供全面的工程设计与规划。基于已确定的产品需求文档(PRD),本文档从软件工程和工程控制论的视角出发,深入分析项目的技术指标、可行性、技术选型、未来展望及各项核心系统属性,为项目的顺利开发、部署和长期演进提供一份结构化的工程蓝图。 - -**项目代号**: 星撰 (StarWriter) - -**核心愿景**: 构建一个高度集成、数据私有、体验优秀的个人数字化中枢,统一管理知识、生活与工具。 - ---- - -## 1. 技术指标 (Technical Specifications) - -为确保项目质量,我们设定以下可量化的关键技术指标: - -| 类别 | 指标项 | 目标值 | 备注 | -|--------------|------------------------------|------------------------------------------------|----------------------------------------------| -| **性能** | API 平均响应时间 | < 200ms (95th percentile) | 针对核心数据读写操作 | -| | 复杂查询/搜索响应时间 | < 800ms | 如全文搜索、聚合分析 | -| | 页面首次内容绘制 (FCP) | < 1.5s | 核心页面的加载性能 | -| **并发** | MVP 阶段并发用户数 | 100+ | 系统可稳定支持的并发会话数 | -| | 长期目标并发用户数 | 1000+ | | -| **可用性** | 系统年可用性 (Uptime) | 99.9% | 相当于每年停机时间不超过 8.76 小时 | -| **稳定性** | 核心 API 成功率 | > 99.95% | | -| | 数据备份恢复点目标 (RPO) | 24 小时 | 每日自动备份,最多丢失 24 小时数据 | -| | 数据备份恢复时间目标 (RTO) | < 2 小时 | 从灾难发生到恢复服务所需时间 | -| **安全性** | 安全漏洞响应 | 遵循 OWASP Top 10 防御原则,0 高危漏洞 | 使用自动化工具扫描并定期审查 | -| | 敏感数据处理 | 密码、密钥等使用非对称加密或强哈希算法存储 | | -| **代码质量** | 核心模块单元测试覆盖率 | > 90% | 确保业务逻辑的稳定可靠 | -| | CI/CD 流水线单次执行时间 | < 10 分钟 | 从代码提交到构建部署完成 | - ---- - -## 2. 可行性分析 (Feasibility Analysis) - -- **技术可行性**: - - **技术栈成熟度**: 项目选用的 Elysia.js, Bun, Vue/React, MySQL, Redis 等技术均为成熟或高速发展的开源技术,拥有活跃的社区和丰富的文档,技术风险可控。 - - **实现复杂度**: 项目功能虽多,但采用模块化、分阶段(MVP)的开发策略,可将复杂系统分解为多个可管理的小模块,降低了单次开发的复杂度。 - - **团队技能**: 核心开发人员具备所需的全栈技术能力,能够驾驭该技术栈。 - -- **经济可行性**: - - **初期成本**: 主要成本为服务器托管费用。初期可利用云服务商的免费套餐或低成本VPS,成本极低。所有核心软件均为开源,无授权费用。 - - **长期成本**: 随着用户量和数据量的增长,服务器和存储成本会线性增加,但成本模型清晰可控。 - - **开发成本**: 作为个人项目,主要为时间成本,无直接的人力资源开销。 - -- **操作与维护可行性**: - - **部署**: 采用 Docker 和 Docker Compose 进行容器化部署,实现了环境的标准化和一键部署,极大降低了操作和维护的复杂性。 - - **监控**: 规划了完善的日志和监控体系,能够主动发现和定位问题,提升了系统的可维护性。 - - **开发流程**: 明确的 PRD 和工程设计文档,结合 CI/CD 自动化流程,保障了开发过程的规范性和高效性。 - ---- - -## 3. 技术选型与论证 (Technology Stack Selection & Justification) - -| 领域 | 技术选型 | 选型论证 | -|----------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **后端运行时** | **Bun.js** | 极高的执行效率,内置打包器、测试器、包管理器,提供了现代、一体化的开发体验,与 Elysia.js 完美契合。 | -| **后端框架** | **Elysia.js** | 基于 Bun 设计,性能卓越。提供端到端的类型安全(Type-Safe),开发体验极佳,插件生态丰富,非常适合构建高性能、类型健壮的 API 服务。 | -| **前端框架** | **Vue.js / React** | 两者均为业界领先的声明式 UI 框架,拥有强大的生态系统和组件库。选择其一可快速构建复杂、交互丰富的单页应用(SPA)。最终选择可根据个人偏好决定。 | -| **数据库** | **MySQL (主) + Redis (缓存) + Elasticsearch (搜索)** | **MySQL**: 成熟可靠的关系型数据库,提供事务支持和数据一致性,适合存储结构化的核心业务数据。**Redis**: 高性能内存数据库,用于缓存、会话管理、消息队列等场景,能极大提升系统响应速度。**Elasticsearch**: 强大的全文搜索引擎,为笔记、博客等内容提供毫秒级的复杂搜索能力,是内容驱动型平台的关键组件。 | -| **部署方案** | **Docker + Docker Compose** | 实现开发、测试、生产环境的一致性,简化部署流程,便于服务的水平扩展和管理。 | - ---- - -## 4. 前沿技术展望 (Future Technology Outlook) - -本项目在设计上保持开放,为未来集成前沿技术预留了接口: - -- **深度 AI 集成 (LLM/RAG)**: 当前规划的 AI 功能是基础。未来可引入基于私有数据(笔记、文档)的检索增强生成(RAG)技术,将平台打造成一个真正个性化的智能问答和创作助手。 -- **去中心化身份与存储 (DID/IPFS)**: 探索使用去中心化身份标识(DID)进行用户认证,增强用户的自主权。核心的、不可变的笔记或博客可选择性地发布到 IPFS/Arweave 等去中心化存储网络,实现数据的永久保存和抗审查。 -- **边缘计算 (Edge Computing)**: 对于静态资源和一些低延迟要求的 API,可以利用边缘计算节点进行分发和计算,为全球用户提供更快的访问速度。 -- **WebAssembly (WASM)**: 对于前端的性能敏感模块,如富文本编辑器、思维导图、在线画板的核心引擎,未来可采用 Rust/C++ 编写并通过 WASM 在浏览器中运行,以获得接近原生的性能。 - ---- - -## 5. 实用性分析 (Utility Analysis) - -本项目的核心实用价值在于 **"终极整合"** 与 **"数据主权"**。 - -- **解决的核心痛点**: 现代人的数字信息散落在各个孤立的平台(笔记在A应用,书签在B浏览器,密码在C工具,博客在D平台)。"星撰"旨在通过一个统一的平台整合这些功能,消除切换成本,构建个人数据的内在联系。 -- **为用户创造的价值**: - 1. **数据所有权**: 用户数据完全存储在自己的服务器上,彻底摆脱对商业平台的依赖和隐私泄露的担忧。 - 2. **高度可定制**: 作为一个自建项目,可以根据个人需求无限扩展和定制功能,不受商业产品的限制。 - 3. **效率提升**: 将多种高频工具集成,减少了跨应用的操作,提升了日常工作和学习的效率。 - ---- - -## 6. 商用性分析 (Commercial Viability Analysis) - -尽管项目初衷为个人使用,但其架构和功能具备一定的商用潜力。 - -- **潜在市场**: 面向注重数据隐私、有极客精神、需要高度定制化解决方案的个人开发者、知识工作者、内容创作者。 -- **商业模式探索**: - 1. **SaaS 订阅服务**: 提供托管服务,用户无需自行部署。可通过免费增值模式(Freemium)吸引用户,高级功能(如无限AI调用、团队协作、更大存储空间)收费。 - 2. **开源+付费支持/高级版**: 项目核心开源,吸引社区贡献。提供付费的企业级支持、高级插件或一键部署的商业版。 - 3. **模板/插件市场**: 建立一个生态,允许开发者创建和销售主题模板、功能插件。 -- **核心竞争力**: 与 Notion, Obsidian 等产品的竞争中,其核心优势是 **开源、可自托管、高度集成**。这对于目标市场中的特定群体具有强大吸引力。 - ---- - -## 7. 稳定性与可靠性设计 (Stability & Reliability Design) - -- **冗余与备份**: 数据库采用主从复制(Read Replicas)实现读写分离和高可用。实施每日全量备份和增量备份策略,并定期进行恢复演练。 -- - **无状态服务**: 后端 API 设计为无状态,所有会话信息存储在 Redis 中。这使得服务可以轻松地进行水平扩展和故障切换。 -- **优雅降级**: 当 Elasticsearch 或 Redis 等非核心依赖出现故障时,系统应能自动降级服务(如暂时关闭搜索功能、使用数据库替代缓存),保证核心功能(如读写笔记)不受影响。 -- **健康检查**: 对外提供健康检查 API (`/health`),供负载均衡和监控系统调用,实现故障节点的自动摘除。 -- **配置中心**: 所有配置(数据库连接、密钥等)通过环境变量或配置中心管理,实现配置与代码分离,增强安全性与灵活性。 - ---- - -## 8. 可拓展性设计 (Scalability & Extensibility Design) - -- **水平扩展 (Scale Out)**: 无状态的后端服务设计,使其可以轻易地在负载均衡器后部署多个实例,以应对增长的流量。 -- **垂直扩展 (Scale Up)**: 系统也可以通过增加单个服务器的 CPU 和内存来提升处理能力。 -- **数据库扩展**: 初期采用读写分离。未来若数据量巨大,可根据业务模块进行垂直拆分(如用户库、内容库),或对超大表进行水平分片(Sharding)。 -- **功能扩展 (Extensibility)**: - - **模块化架构**: `src` 目录下的 `controllers`, `services`, `plugins` 结构清晰,新增功能模块不会影响现有逻辑。 - - **插件化设计**: 核心功能稳定后,可设计一套正式的插件机制,允许社区或自己以插件形式开发新功能(如新的编辑器、新的同步工具),实现真正的"热插拔"式扩展。 - - **事件驱动**: 通过引入消息队列,采用事件驱动模式解耦核心业务和附加业务(如发送邮件、生成通知、索引文档),提高系统的响应能力和可扩展性。 - ---- - -## 9. 结论 - -"星撰"项目不仅在产品功能上满足了现代数字生活的需求,其工程设计也遵循了高标准。项目技术上可行,经济上低成本,且在稳定性、可扩展性方面进行了深思熟虑的规划。本文档将作为指导,确保项目沿着一条稳健、高效、可持续的道路前进。 \ No newline at end of file +# 星撰个人综合平台 - 项目工程设计文档 + +--- + +## 0. 引言 + +本文档旨在为"星撰"个人综合平台项目提供全面的工程设计与规划。基于已确定的产品需求文档(PRD),本文档从软件工程和工程控制论的视角出发,深入分析项目的技术指标、可行性、技术选型、未来展望及各项核心系统属性,为项目的顺利开发、部署和长期演进提供一份结构化的工程蓝图。 + +**项目代号**: 星撰 (StarWriter) + +**核心愿景**: 构建一个高度集成、数据私有、体验优秀的个人数字化中枢,统一管理知识、生活与工具。 + +--- + +## 1. 技术指标 (Technical Specifications) + +为确保项目质量,我们设定以下可量化的关键技术指标: + +| 类别 | 指标项 | 目标值 | 备注 | +| ------------ | -------------------------- | ------------------------------------------ | ---------------------------------- | +| **性能** | API 平均响应时间 | < 200ms (95th percentile) | 针对核心数据读写操作 | +| | 复杂查询/搜索响应时间 | < 800ms | 如全文搜索、聚合分析 | +| | 页面首次内容绘制 (FCP) | < 1.5s | 核心页面的加载性能 | +| **并发** | MVP 阶段并发用户数 | 100+ | 系统可稳定支持的并发会话数 | +| | 长期目标并发用户数 | 1000+ | | +| **可用性** | 系统年可用性 (Uptime) | 99.9% | 相当于每年停机时间不超过 8.76 小时 | +| **稳定性** | 核心 API 成功率 | > 99.95% | | +| | 数据备份恢复点目标 (RPO) | 24 小时 | 每日自动备份,最多丢失 24 小时数据 | +| | 数据备份恢复时间目标 (RTO) | < 2 小时 | 从灾难发生到恢复服务所需时间 | +| **安全性** | 安全漏洞响应 | 遵循 OWASP Top 10 防御原则,0 高危漏洞 | 使用自动化工具扫描并定期审查 | +| | 敏感数据处理 | 密码、密钥等使用非对称加密或强哈希算法存储 | | +| **代码质量** | 核心模块单元测试覆盖率 | > 90% | 确保业务逻辑的稳定可靠 | +| | CI/CD 流水线单次执行时间 | < 10 分钟 | 从代码提交到构建部署完成 | + +--- + +## 2. 可行性分析 (Feasibility Analysis) + +- **技术可行性**: + - **技术栈成熟度**: 项目选用的 Elysia.js, Bun, Vue/React, MySQL, Redis 等技术均为成熟或高速发展的开源技术,拥有活跃的社区和丰富的文档,技术风险可控。 + - **实现复杂度**: 项目功能虽多,但采用模块化、分阶段(MVP)的开发策略,可将复杂系统分解为多个可管理的小模块,降低了单次开发的复杂度。 + - **团队技能**: 核心开发人员具备所需的全栈技术能力,能够驾驭该技术栈。 + +- **经济可行性**: + - **初期成本**: 主要成本为服务器托管费用。初期可利用云服务商的免费套餐或低成本VPS,成本极低。所有核心软件均为开源,无授权费用。 + - **长期成本**: 随着用户量和数据量的增长,服务器和存储成本会线性增加,但成本模型清晰可控。 + - **开发成本**: 作为个人项目,主要为时间成本,无直接的人力资源开销。 + +- **操作与维护可行性**: + - **部署**: 采用 Docker 和 Docker Compose 进行容器化部署,实现了环境的标准化和一键部署,极大降低了操作和维护的复杂性。 + - **监控**: 规划了完善的日志和监控体系,能够主动发现和定位问题,提升了系统的可维护性。 + - **开发流程**: 明确的 PRD 和工程设计文档,结合 CI/CD 自动化流程,保障了开发过程的规范性和高效性。 + +--- + +## 3. 技术选型与论证 (Technology Stack Selection & Justification) + +| 领域 | 技术选型 | 选型论证 | +| -------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **后端运行时** | **Bun.js** | 极高的执行效率,内置打包器、测试器、包管理器,提供了现代、一体化的开发体验,与 Elysia.js 完美契合。 | +| **后端框架** | **Elysia.js** | 基于 Bun 设计,性能卓越。提供端到端的类型安全(Type-Safe),开发体验极佳,插件生态丰富,非常适合构建高性能、类型健壮的 API 服务。 | +| **前端框架** | **Vue.js / React** | 两者均为业界领先的声明式 UI 框架,拥有强大的生态系统和组件库。选择其一可快速构建复杂、交互丰富的单页应用(SPA)。最终选择可根据个人偏好决定。 | +| **数据库** | **MySQL (主) + Redis (缓存) + Elasticsearch (搜索)** | **MySQL**: 成熟可靠的关系型数据库,提供事务支持和数据一致性,适合存储结构化的核心业务数据。**Redis**: 高性能内存数据库,用于缓存、会话管理、消息队列等场景,能极大提升系统响应速度。**Elasticsearch**: 强大的全文搜索引擎,为笔记、博客等内容提供毫秒级的复杂搜索能力,是内容驱动型平台的关键组件。 | +| **部署方案** | **Docker + Docker Compose** | 实现开发、测试、生产环境的一致性,简化部署流程,便于服务的水平扩展和管理。 | + +--- + +## 4. 前沿技术展望 (Future Technology Outlook) + +本项目在设计上保持开放,为未来集成前沿技术预留了接口: + +- **深度 AI 集成 (LLM/RAG)**: 当前规划的 AI 功能是基础。未来可引入基于私有数据(笔记、文档)的检索增强生成(RAG)技术,将平台打造成一个真正个性化的智能问答和创作助手。 +- **去中心化身份与存储 (DID/IPFS)**: 探索使用去中心化身份标识(DID)进行用户认证,增强用户的自主权。核心的、不可变的笔记或博客可选择性地发布到 IPFS/Arweave 等去中心化存储网络,实现数据的永久保存和抗审查。 +- **边缘计算 (Edge Computing)**: 对于静态资源和一些低延迟要求的 API,可以利用边缘计算节点进行分发和计算,为全球用户提供更快的访问速度。 +- **WebAssembly (WASM)**: 对于前端的性能敏感模块,如富文本编辑器、思维导图、在线画板的核心引擎,未来可采用 Rust/C++ 编写并通过 WASM 在浏览器中运行,以获得接近原生的性能。 + +--- + +## 5. 实用性分析 (Utility Analysis) + +本项目的核心实用价值在于 **"终极整合"** 与 **"数据主权"**。 + +- **解决的核心痛点**: 现代人的数字信息散落在各个孤立的平台(笔记在A应用,书签在B浏览器,密码在C工具,博客在D平台)。"星撰"旨在通过一个统一的平台整合这些功能,消除切换成本,构建个人数据的内在联系。 +- **为用户创造的价值**: + 1. **数据所有权**: 用户数据完全存储在自己的服务器上,彻底摆脱对商业平台的依赖和隐私泄露的担忧。 + 2. **高度可定制**: 作为一个自建项目,可以根据个人需求无限扩展和定制功能,不受商业产品的限制。 + 3. **效率提升**: 将多种高频工具集成,减少了跨应用的操作,提升了日常工作和学习的效率。 + +--- + +## 6. 商用性分析 (Commercial Viability Analysis) + +尽管项目初衷为个人使用,但其架构和功能具备一定的商用潜力。 + +- **潜在市场**: 面向注重数据隐私、有极客精神、需要高度定制化解决方案的个人开发者、知识工作者、内容创作者。 +- **商业模式探索**: + 1. **SaaS 订阅服务**: 提供托管服务,用户无需自行部署。可通过免费增值模式(Freemium)吸引用户,高级功能(如无限AI调用、团队协作、更大存储空间)收费。 + 2. **开源+付费支持/高级版**: 项目核心开源,吸引社区贡献。提供付费的企业级支持、高级插件或一键部署的商业版。 + 3. **模板/插件市场**: 建立一个生态,允许开发者创建和销售主题模板、功能插件。 +- **核心竞争力**: 与 Notion, Obsidian 等产品的竞争中,其核心优势是 **开源、可自托管、高度集成**。这对于目标市场中的特定群体具有强大吸引力。 + +--- + +## 7. 稳定性与可靠性设计 (Stability & Reliability Design) + +- **冗余与备份**: 数据库采用主从复制(Read Replicas)实现读写分离和高可用。实施每日全量备份和增量备份策略,并定期进行恢复演练。 +- - **无状态服务**: 后端 API 设计为无状态,所有会话信息存储在 Redis 中。这使得服务可以轻松地进行水平扩展和故障切换。 +- **优雅降级**: 当 Elasticsearch 或 Redis 等非核心依赖出现故障时,系统应能自动降级服务(如暂时关闭搜索功能、使用数据库替代缓存),保证核心功能(如读写笔记)不受影响。 +- **健康检查**: 对外提供健康检查 API (`/health`),供负载均衡和监控系统调用,实现故障节点的自动摘除。 +- **配置中心**: 所有配置(数据库连接、密钥等)通过环境变量或配置中心管理,实现配置与代码分离,增强安全性与灵活性。 + +--- + +## 8. 可拓展性设计 (Scalability & Extensibility Design) + +- **水平扩展 (Scale Out)**: 无状态的后端服务设计,使其可以轻易地在负载均衡器后部署多个实例,以应对增长的流量。 +- **垂直扩展 (Scale Up)**: 系统也可以通过增加单个服务器的 CPU 和内存来提升处理能力。 +- **数据库扩展**: 初期采用读写分离。未来若数据量巨大,可根据业务模块进行垂直拆分(如用户库、内容库),或对超大表进行水平分片(Sharding)。 +- **功能扩展 (Extensibility)**: + - **模块化架构**: `src` 目录下的 `controllers`, `services`, `plugins` 结构清晰,新增功能模块不会影响现有逻辑。 + - **插件化设计**: 核心功能稳定后,可设计一套正式的插件机制,允许社区或自己以插件形式开发新功能(如新的编辑器、新的同步工具),实现真正的"热插拔"式扩展。 + - **事件驱动**: 通过引入消息队列,采用事件驱动模式解耦核心业务和附加业务(如发送邮件、生成通知、索引文档),提高系统的响应能力和可扩展性。 + +--- + +## 9. 结论 + +"星撰"项目不仅在产品功能上满足了现代数字生活的需求,其工程设计也遵循了高标准。项目技术上可行,经济上低成本,且在稳定性、可扩展性方面进行了深思熟虑的规划。本文档将作为指导,确保项目沿着一条稳健、高效、可持续的道路前进。 diff --git a/prd/星撰-开发计划.md b/prd/星撰-开发计划.md index 907e408..a0c89b2 100644 --- a/prd/星撰-开发计划.md +++ b/prd/星撰-开发计划.md @@ -1,121 +1,137 @@ -# 星撰个人综合平台 - 详细开发计划 (按功能模块划分) - ---- - -## 1. 引言 - -### 1.1. 文档目的 -本文档基于《产品需求文档 (PRD)》和《概要设计》,旨在将项目需求拆分为具体、可执行的开发任务,并将其组织成一个以核心功能模块为导向、分阶段的开发计划。本文档是项目执行阶段的核心路线图。 - -### 1.2. 计划结构 -本计划遵循迭代式增量开发模型,将整个项目划分为多个主要阶段。**此版本已根据 `main.md` 的功能列表进行重组**,每个阶段聚焦于一个完整的功能模块。 - ---- - -## 阶段一: 项目初始化与基础设施 -**目标**: 搭建项目骨架,配置开发环境和规范,建立 CI/CD 基础流程。这是后续所有开发工作的基础。 -- [ ] **1.1. 项目脚手架**: 初始化 Elysia + Bun + TypeScript 项目,配置 `tsconfig.json` 和路径别名。 -- [ ] **1.2. 代码规范**: 配置 ESLint 和 Prettier,确保代码风格和质量一致。 -- [ ] **1.3. 配置文件管理**: 建立 `src/config` 目录,管理数据库、JWT、日志等配置,支持环境变量。 -- [ ] **1.4. 数据库连接**: 编写并测试 MySQL 和 Redis 的连接模块。 -- [ ] **1.5. 核心插件**: 实现全局错误处理、结构化日志 (`pino`) 和 Swagger API 文档插件。 -- [ ] **1.6. 容器化**: 编写 `Dockerfile` 和 `docker-compose.yml`,实现开发环境一键启动。 -- [ ] **1.7. CI/CD 基础**: 搭建基础的 GitHub Actions 工作流,实现提交代码时自动运行 lint 和测试。 - ---- - -## 阶段二: 基础用户系统 -**目标**: 实现一个完整的、支持RBAC的用户中心,包含用户、角色、权限、组织和字典等核心功能。 -- [ ] **2.1. 数据库设计**: 设计并创建 `users`, `roles`, `permissions`, `organizations`, `dictionaries` 及相关联的表。 -- [ ] **2.2. 用户认证 API**: - - [ ] `POST /api/v1/auth/register`: 实现用户注册(含数据校验、密码哈希)。 - - [ ] `POST /api/v1/auth/login`: 实现用户登录,成功后返回 JWT。 - - [ ] `GET /api/v1/users/me`: 创建受保护路由,获取当前登录用户信息。 -- [ ] **2.3. JWT与权限中间件**: - - [ ] 实现 JWT 校验中间件。 - - [ ] 实现一个基础的 RBAC 权限校验中间件。 -- [ ] **2.4. 字典管理 CRUD API**: 实现对数据字典类型和条目的增删改查。 -- [ ] **2.5. 角色管理 CRUD API**: 实现对角色的增删改查及权限分配。 -- [ ] **2.6. 组织架构 CRUD API**: 实现树形组织架构的增删改查。 -- [ ] **2.7. 用户管理 CRUD API**: 实现后台对用户的增删改查和角色分配。 - ---- - -## 阶段三: 笔记与博客系统 -**目标**: 构建平台的内容核心,支持从私有笔记到公开博客的完整流程,并集成搜索和社交功能。 -- [ ] **3.1. 数据库设计**: 设计 `notes`, `posts`, `tags`, `categories`, `comments` 及相关关联表。 -- [ ] **3.2. 笔记核心 API**: - - [ ] 实现笔记的 CRUD (创建/读取/更新/删除)。 - - [ ] `GET /api/v1/notes`: 获取用户的笔记树形结构列表。 - - [ ] 实现笔记的标签管理功能。 -- [ ] **3.3. 博客发布流程 API**: - - [ ] `POST /api/v1/notes/:id/publish`: 从笔记一键发布为博客。 - - [ ] 实现博客的 CRUD,包括分类管理。 -- [ ] **3.4. 公开访问 API**: - - [ ] `GET /api/v1/public/posts`: (公开) 获取博客列表(分页、分类、标签筛选)。 - - [ ] `GET /api/v1/public/posts/:slug`: (公开) 获取单篇博客内容。 -- [ ] **3.5. 互动功能 API**: - - [ ] 实现博客的点赞、收藏功能。 - - [ ] 实现层级评论的发表和查看功能。 -- [ ] **3.6. 搜索引擎集成**: - - [ ] 配置 Elasticsearch 服务。 - - [ ] 编写服务将 `posts` 数据索引到 ES。 - - [ ] `GET /api/v1/public/search`: 实现基于 ES 的博客全文搜索 API。 - ---- - -## 阶段四: 个人空间 -**目标**: 开发一个用于记录和分享个人生活点滴的模块。 -- [ ] **4.1. 数据库设计**: 设计 `life_logs` 表,支持文本、图片/视频链接、位置、心情等。 -- [ ] **4.2. 生活日志 CRUD API**: 实现生活日志的增删改查。 -- [ ] **4.3. 时间线 API**: `GET /api/v1/space/timeline` 按时间线获取日志列表。 -- [ ] **4.4. 隐私控制**: 为日志增加隐私设置(公开/私密/好友可见)。 - ---- - -## 阶段五: 阅读系统 -**目标**: 构建个人电子书库,支持在线阅读、批注和智能解读。 -- [ ] **5.1. 数据库设计**: 设计 `ebooks`, `annotations` (批注) 表。 -- [ ] **5.2. 电子书 API**: 实现电子书文件的上传、元数据解析和列表管理。 -- [ ] **5.3. 阅读功能 API**: 实现阅读时添加/查看批注、记录和同步阅读进度的功能。 -- [ ] **5.4. (选做) AI 解读 API**: `POST /api/v1/ebooks/:id/interpret` 对书籍内容进行摘要或问答。 - ---- - -## 阶段六: 综合工具集 -**目标**: 集成一系列实用工具,提升平台的附加价值和用户粘性。 -- [ ] **6.1. 密码与书签管理**: - - [ ] 数据库设计 `passwords`, `bookmarks` 表,关键字段加密。 - - [ ] 实现密码管理器的安全 CRUD API。 - - [ ] 实现书签管理器的 CRUD API。 -- [ ] **6.2. 文件管理系统**: - - [ ] 数据库设计 `files` 表,存储文件元数据。 - - [ ] 封装一个支持本地/OSS的对象存储服务。 - - [ ] 实现文件的上传、下载、删除、重命名和目录管理 API。 -- [ ] **6.3. 第三方平台同步**: - - [ ] 设计同步任务管理模块和第三方平台适配器。 - - [ ] 实现 Bilibili 收藏夹同步适配器。 - - [ ] 实现 QQ音乐/网易云音乐 歌单同步适配器。 -- [ ] **6.4. 信息聚合工具**: - - [ ] 实现热点新闻捕获和聚合的后台任务。 - - [ ] `POST /api/v1/tools/web-to-pdf`: 实现网页离线存储为 PDF 的 API。 - ---- - -## 阶段七: 高级功能与生态完善 -**目标**: 引入AI、实时通知等高级功能,并通过跨平台应用完善生态。最后对系统进行加固和优化,确保长期稳定。 -- [ ] **7.1. AI 功能深度集成**: - - [ ] `POST /api/v1/ai/summary`: 对给定文本生成摘要。 - - [ ] `POST /api/v1/ai/polish`: 对给定文本进行润色。 - - [ ] (高级) 探索将笔记向量化,实现基于个人知识库的智能问答。 -- [ ] **7.2. 高级功能与优化**: - - [ ] 使用 WebSocket 实现评论、点赞等事件的实时通知。 - - [ ] 开发基础的网站访问统计和内容分析功能。 - - [ ] 设计并实现基于 OAuth 2.0 的开放 API 体系。 -- [ ] **7.3. 跨平台生态**: - - [ ] (前端) 将 Web 应用打包为 PWA。 - - [ ] (前端) 开发用于快速保存书签和填充密码的浏览器插件。 -- [ ] **7.4. 系统加固与维护**: - - [ ] 引入 CDN 加速静态资源,分析并优化慢查询 SQL。 - - [ ] 定期进行依赖项安全扫描和代码审计。 - - [ ] 完善所有功能的开发者文档和用户手册。 \ No newline at end of file +# 星撰个人综合平台 - 详细开发计划 (按功能模块划分) + +--- + +## 1. 引言 + +### 1.1. 文档目的 + +本文档基于《产品需求文档 (PRD)》和《概要设计》,旨在将项目需求拆分为具体、可执行的开发任务,并将其组织成一个以核心功能模块为导向、分阶段的开发计划。本文档是项目执行阶段的核心路线图。 + +### 1.2. 计划结构 + +本计划遵循迭代式增量开发模型,将整个项目划分为多个主要阶段。**此版本已根据 `main.md` 的功能列表进行重组**,每个阶段聚焦于一个完整的功能模块。 + +--- + +## 阶段一: 项目初始化与基础设施 + +**目标**: 搭建项目骨架,配置开发环境和规范,建立 CI/CD 基础流程。这是后续所有开发工作的基础。 + +- [ ] **1.1. 项目脚手架**: 初始化 Elysia + Bun + TypeScript 项目,配置 `tsconfig.json` 和路径别名。 +- [ ] **1.2. 代码规范**: 配置 ESLint 和 Prettier,确保代码风格和质量一致。 +- [ ] **1.3. 配置文件管理**: 建立 `src/config` 目录,管理数据库、JWT、日志等配置,支持环境变量。 +- [ ] **1.4. 数据库连接**: 编写并测试 MySQL 和 Redis 的连接模块。 +- [ ] **1.5. 核心插件**: 实现全局错误处理、结构化日志 (`pino`) 和 Swagger API 文档插件。 +- [ ] **1.6. 容器化**: 编写 `Dockerfile` 和 `docker-compose.yml`,实现开发环境一键启动。 +- [ ] **1.7. CI/CD 基础**: 搭建基础的 GitHub Actions 工作流,实现提交代码时自动运行 lint 和测试。 + +--- + +## 阶段二: 基础用户系统 + +**目标**: 实现一个完整的、支持RBAC的用户中心,包含用户、角色、权限、组织和字典等核心功能。 + +- [ ] **2.1. 数据库设计**: 设计并创建 `users`, `roles`, `permissions`, `organizations`, `dictionaries` 及相关联的表。 +- [ ] **2.2. 用户认证 API**: + - [ ] `POST /api/v1/auth/register`: 实现用户注册(含数据校验、密码哈希)。 + - [ ] `POST /api/v1/auth/login`: 实现用户登录,成功后返回 JWT。 + - [ ] `GET /api/v1/users/me`: 创建受保护路由,获取当前登录用户信息。 +- [ ] **2.3. JWT与权限中间件**: + - [ ] 实现 JWT 校验中间件。 + - [ ] 实现一个基础的 RBAC 权限校验中间件。 +- [ ] **2.4. 字典管理 CRUD API**: 实现对数据字典类型和条目的增删改查。 +- [ ] **2.5. 角色管理 CRUD API**: 实现对角色的增删改查及权限分配。 +- [ ] **2.6. 组织架构 CRUD API**: 实现树形组织架构的增删改查。 +- [ ] **2.7. 用户管理 CRUD API**: 实现后台对用户的增删改查和角色分配。 + +--- + +## 阶段三: 笔记与博客系统 + +**目标**: 构建平台的内容核心,支持从私有笔记到公开博客的完整流程,并集成搜索和社交功能。 + +- [ ] **3.1. 数据库设计**: 设计 `notes`, `posts`, `tags`, `categories`, `comments` 及相关关联表。 +- [ ] **3.2. 笔记核心 API**: + - [ ] 实现笔记的 CRUD (创建/读取/更新/删除)。 + - [ ] `GET /api/v1/notes`: 获取用户的笔记树形结构列表。 + - [ ] 实现笔记的标签管理功能。 +- [ ] **3.3. 博客发布流程 API**: + - [ ] `POST /api/v1/notes/:id/publish`: 从笔记一键发布为博客。 + - [ ] 实现博客的 CRUD,包括分类管理。 +- [ ] **3.4. 公开访问 API**: + - [ ] `GET /api/v1/public/posts`: (公开) 获取博客列表(分页、分类、标签筛选)。 + - [ ] `GET /api/v1/public/posts/:slug`: (公开) 获取单篇博客内容。 +- [ ] **3.5. 互动功能 API**: + - [ ] 实现博客的点赞、收藏功能。 + - [ ] 实现层级评论的发表和查看功能。 +- [ ] **3.6. 搜索引擎集成**: + - [ ] 配置 Elasticsearch 服务。 + - [ ] 编写服务将 `posts` 数据索引到 ES。 + - [ ] `GET /api/v1/public/search`: 实现基于 ES 的博客全文搜索 API。 + +--- + +## 阶段四: 个人空间 + +**目标**: 开发一个用于记录和分享个人生活点滴的模块。 + +- [ ] **4.1. 数据库设计**: 设计 `life_logs` 表,支持文本、图片/视频链接、位置、心情等。 +- [ ] **4.2. 生活日志 CRUD API**: 实现生活日志的增删改查。 +- [ ] **4.3. 时间线 API**: `GET /api/v1/space/timeline` 按时间线获取日志列表。 +- [ ] **4.4. 隐私控制**: 为日志增加隐私设置(公开/私密/好友可见)。 + +--- + +## 阶段五: 阅读系统 + +**目标**: 构建个人电子书库,支持在线阅读、批注和智能解读。 + +- [ ] **5.1. 数据库设计**: 设计 `ebooks`, `annotations` (批注) 表。 +- [ ] **5.2. 电子书 API**: 实现电子书文件的上传、元数据解析和列表管理。 +- [ ] **5.3. 阅读功能 API**: 实现阅读时添加/查看批注、记录和同步阅读进度的功能。 +- [ ] **5.4. (选做) AI 解读 API**: `POST /api/v1/ebooks/:id/interpret` 对书籍内容进行摘要或问答。 + +--- + +## 阶段六: 综合工具集 + +**目标**: 集成一系列实用工具,提升平台的附加价值和用户粘性。 + +- [ ] **6.1. 密码与书签管理**: + - [ ] 数据库设计 `passwords`, `bookmarks` 表,关键字段加密。 + - [ ] 实现密码管理器的安全 CRUD API。 + - [ ] 实现书签管理器的 CRUD API。 +- [ ] **6.2. 文件管理系统**: + - [ ] 数据库设计 `files` 表,存储文件元数据。 + - [ ] 封装一个支持本地/OSS的对象存储服务。 + - [ ] 实现文件的上传、下载、删除、重命名和目录管理 API。 +- [ ] **6.3. 第三方平台同步**: + - [ ] 设计同步任务管理模块和第三方平台适配器。 + - [ ] 实现 Bilibili 收藏夹同步适配器。 + - [ ] 实现 QQ音乐/网易云音乐 歌单同步适配器。 +- [ ] **6.4. 信息聚合工具**: + - [ ] 实现热点新闻捕获和聚合的后台任务。 + - [ ] `POST /api/v1/tools/web-to-pdf`: 实现网页离线存储为 PDF 的 API。 + +--- + +## 阶段七: 高级功能与生态完善 + +**目标**: 引入AI、实时通知等高级功能,并通过跨平台应用完善生态。最后对系统进行加固和优化,确保长期稳定。 + +- [ ] **7.1. AI 功能深度集成**: + - [ ] `POST /api/v1/ai/summary`: 对给定文本生成摘要。 + - [ ] `POST /api/v1/ai/polish`: 对给定文本进行润色。 + - [ ] (高级) 探索将笔记向量化,实现基于个人知识库的智能问答。 +- [ ] **7.2. 高级功能与优化**: + - [ ] 使用 WebSocket 实现评论、点赞等事件的实时通知。 + - [ ] 开发基础的网站访问统计和内容分析功能。 + - [ ] 设计并实现基于 OAuth 2.0 的开放 API 体系。 +- [ ] **7.3. 跨平台生态**: + - [ ] (前端) 将 Web 应用打包为 PWA。 + - [ ] (前端) 开发用于快速保存书签和填充密码的浏览器插件。 +- [ ] **7.4. 系统加固与维护**: + - [ ] 引入 CDN 加速静态资源,分析并优化慢查询 SQL。 + - [ ] 定期进行依赖项安全扫描和代码审计。 + - [ ] 完善所有功能的开发者文档和用户手册。 diff --git a/prd/星撰-概要设计.md b/prd/星撰-概要设计.md index e87d885..3d9af4c 100644 --- a/prd/星撰-概要设计.md +++ b/prd/星撰-概要设计.md @@ -1,217 +1,241 @@ -# 星撰个人综合平台 - 软件开发概要设计 - ---- - -## 1. 引言 - -### 1.1. 文档目的 -本概要设计文档旨在为"星撰"个人综合平台项目定义一个完整的软件开发生命周期(SDLC)框架。它将作为项目从规划、设计、实现到部署和维护的最高指导性文件,确保所有开发活动遵循标准化的软件工程最佳实践。 - -### 1.2. 项目概述 -"星撰"是一个高度集成的个人数字化中枢,其核心功能、用户故事及非功能性需求已在《个人网站综合平台产品需求文档 (PRD)》中详细定义。本项目旨在将该PRD转化为一个稳定、可扩展、高质量的软件产品。 - -### 1.3. 范围 -本文档覆盖范围包括: -- 项目开发模型的选定。 -- 系统的高层体系结构设计。 -- 各阶段(设计、开发、测试、部署)的规划和规范。 -- 项目管理、风险控制和质量保证策略。 - ---- - -## 2. 项目规划与管理 - -### 2.1. 开发模型 -本项目采用 **迭代式增量开发模型 (Iterative and Incremental Development)**。 -- **迭代式**: 将开发过程划分为多个短周期的迭代,每个迭代都包含需求分析、设计、编码、测试的完整流程。 -- **增量式**: 每个迭代都会产出一个可测试、可交付的软件增量。产品功能将分阶段(见PRD中的优先级规划)逐步构建和完善。 - -**优势**: 此模型能够快速交付核心功能(MVP),及时获取反馈,灵活应对需求变化,并有效控制项目风险。 - -### 2.2. 项目里程碑 -项目将按照PRD中的优先级划分为四个主要里程碑(阶段): - -| 里程碑 | 名称 | 核心交付物 | 目标 | -|--------|--------------|------------------------------------------------------|------------------------------------| -| M1 | **核心基础 (MVP)** | 基础用户系统、笔记核心功能、基础博客发布、CI/CD流程建立 | 验证核心架构,跑通开发部署全流程 | -| M2 | **内容增强** | 完整博客系统、个人空间、基础工具、搜索引擎集成 | 完善内容生态,提升用户体验 | -| M3 | **高级功能** | 阅读管理、文件管理、AI功能集成、第三方平台同步 | 扩展平台能力,引入智能化 | -| M4 | **生态完善** | 移动端应用、浏览器插件、高级分析功能、性能深度优化 | 构建跨平台生态,实现长期稳定运营 | - -### 2.3. 风险管理 -| 风险类别 | 风险描述 | 可能性 | 影响 | 应对策略 | -|--------------|----------------------------------------|--------|------|------------------------------------------------------------------------| -| **技术风险** | 第三方API变更(如B站、QQ音乐) | 中 | 中 | 封装Adapter层,隔离变化;设计优雅降级机制。 | -| | 新技术引入(Bun/Elysia)的未知问题 | 低 | 中 | 保持对社区的关注,构建完善的测试体系以快速发现问题。 | -| **进度风险** | 个人项目时间投入不稳定 | 高 | 高 | 严格遵循任务清单,将大任务分解为小任务,利用碎片时间完成。 | -| **安全风险** | 个人敏感数据(密码、文件)泄露 | 中 | 高 | 强制加密存储;定期进行安全扫描;代码审查时关注安全漏洞。 | - ---- - -## 3. 系统设计 - -### 3.1. 系统架构 -系统采用分层、模块化的架构,实现前后端分离。 - -```mermaid -graph TD - subgraph "客户端 (Clients)" - WebApp[Web 应用 (Vue/React)] - BrowserExt[浏览器插件] - MobileApp[移动应用 (PWA/Native)] - end - - subgraph "服务端 (Backend - Elysia.js on Bun)" - API_Gateway[API 网关 / 路由层] - - subgraph "核心服务层 (Core Services)" - AuthService[认证与授权服务] - UserService[用户管理服务] - ContentService[内容管理服务 (笔记/博客)] - FileService[文件管理服务] - end - - subgraph "工具服务层 (Utility Services)" - ToolService[密码/书签/同步等工具服务] - end - - subgraph "插件与中间件 (Plugins & Middlewares)" - Logger[日志插件] - ErrorHandler[错误处理] - JWTMiddleware[JWT认证] - Swagger[Swagger文档] - end - end - - subgraph "数据与存储层 (Data & Storage)" - MySQL[(MySQL - 关系型数据)] - Redis[(Redis - 缓存/会话)] - Elasticsearch[(Elasticsearch - 全文搜索)] - ObjectStorage[对象存储 (OSS/NAS)] - end - - subgraph "第三方服务 (3rd Party Services)" - EmailService[邮件服务] - OAuth[OAuth 2.0 提供商] - AI_API[AI/LLM API] - end - - %% 连接关系 - WebApp --> API_Gateway - BrowserExt --> API_Gateway - MobileApp --> API_Gateway - - API_Gateway --> AuthService - API_Gateway --> UserService - API_Gateway --> ContentService - API_Gateway --> FileService - API_Gateway --> ToolService - - AuthService <--> MySQL - UserService <--> MySQL - ContentService <--> MySQL - ContentService <--> Elasticsearch - FileService <--> ObjectStorage - ToolService <--> MySQL - - AuthService --> JWTMiddleware - API_Gateway -- use --> Logger - API_Gateway -- use --> ErrorHandler - API_Gateway -- use --> JWTMiddleware - API_Gateway -- use --> Swagger - - subgraph "通用依赖" - AuthService <--> Redis - UserService <--> Redis - ContentService <--> Redis - end - - AuthService --> EmailService - AuthService --> OAuth - ContentService --> AI_API -``` -* **客户端层**: 负责用户交互和数据展示。 -* **服务端层**: 核心业务逻辑处理中心。通过API网关统一入口,内部按业务垂直划分为多个服务模块。 -* **数据与存储层**: 持久化存储数据,提供缓存和搜索能力。 -* **第三方服务**: 集成外部服务以完成特定功能。 - -### 3.2. 模块划分 -系统功能将按以下模块进行开发,每个模块对应`src/controllers`, `src/services`下的独立子目录。 -1. **`core`**: 核心模块,包括用户、角色、权限、组织、字典等。 -2. **`content`**: 内容模块,包括笔记、博客、评论、分类、标签等。 -3. **`space`**: 个人空间模块,包括生活日志、社交互动。 -4. **`reader`**: 阅读模块,包括电子书管理、批注。 -5. **`tools`**: 工具模块,包括密码、书签、文件、同步等。 -6. **`system`**: 系统模块,包括健康检查、监控、配置管理。 - -### 3.3. 详细设计规划 -本概要设计完成后,将对每个模块进行详细设计,产出物包括: -- **数据库设计文档**: E-R图、表结构定义(字段、类型、约束、索引)、数据字典。 -- **API接口设计文档**: 使用Swagger/OpenAPI规范,定义每个端点的URL、HTTP方法、请求参数、响应体结构及错误码。 -- **UI/UX设计稿**: 针对核心页面和流程,制作线框图或高保真原型。 - ---- - -## 4. 实现与编码 - -### 4.1. 开发环境与工具 -- **IDE**: Visual Studio Code -- **语言**: TypeScript -- **运行时**: Bun.js -- **包管理器**: Bun -- **版本控制**: Git / GitHub -- **代码检查与格式化**: ESLint / Prettier - -### 4.2. 编码规范 -- 严格遵守项目已定义的注释、命名、代码风格规范。 -- 所有业务逻辑必须在 `services` 层实现,`controllers` 层只负责路由和数据校验。 -- 禁止在代码中硬编码配置项(如密钥、端口),所有配置通过环境变量或配置文件注入。 - -### 4.3. 版本控制策略 -采用 **Gitflow工作流** 的简化版: -- `main`: 主分支,存放稳定、可发布的代码。每个里程碑完成后合并。 -- `develop`: 开发分支,集成了所有已完成的功能。 -- `feat/feature-name`: 功能分支,每个新功能或任务都在此分支开发,完成后合并到 `develop`。 -- `fix/bug-name`: Bug修复分支。 - ---- - -## 5. 测试策略 - -### 5.1. 测试层次 -- **单元测试**: 使用 `Vitest` 对 `services` 和 `utils` 中的核心函数进行测试,目标覆盖率 > 90%。 -- **接口测试 (集成测试)**: 使用 `supertest` 或Elysia的内置测试工具,对每个API端点进行测试,验证其与数据库、缓存的交互是否正确。 -- **端到端测试 (系统测试)**: 使用 `Playwright` 或 `Cypress` 对关键用户流程(如注册登录、发布博客)进行自动化测试。 -- **性能测试**: 使用 `autocannon` 对核心API进行压力测试,确保其满足技术指标。 - -### 5.2. 测试执行 -- 所有代码提交前,必须在本地通过相关单元测试和接口测试。 -- CI/CD流水线将在每次向 `develop` 或 `main` 分支合并代码时,自动运行全量测试用例。测试不通过则禁止合并。 - ---- - -## 6. 部署与运维 - -### 6.1. 部署流程 (CI/CD) -使用 **GitHub Actions** 自动化构建、测试和部署流程。 -1. **Push to `feat/*`**: 触发ESLint检查和单元测试。 -2. **Merge to `develop`**: 触发全量测试,构建Docker镜像并推送到镜像仓库,然后部署到 **Staging (预发布)** 环境。 -3. **Merge to `main`**: 触发全量测试,构建生产Docker镜像,并部署到 **Production (生产)** 环境。 - -### 6.2. 基础设施 -- **运行环境**: Docker容器。 -- **容器编排**: Docker Compose (初期) / Kubernetes (未来扩展)。 -- **数据库/缓存**: 使用云服务商提供的托管服务或在Docker中自建。 - -### 6.3. 运维监控 -- **日志**: 使用 `pino` 记录结构化日志,并聚合到统一的日志管理平台(如ELK Stack或Loki)。 -- **监控与告警**: 使用 `Prometheus` 收集应用指标,`Grafana` 进行可视化展示,并针对关键指标(如API错误率、延迟)设置告警。 - ---- - -## 7. 维护与演进 - -- **问题跟踪**: 使用GitHub Issues跟踪Bug和功能请求。 -- **文档同步**: 所有代码变更,若涉及架构、API或数据库设计,必须同步更新相关设计文档。 -- **定期审查**: 每个里程碑结束后,进行项目复盘,评估开发流程、代码质量和系统架构,并进行优化。 -- **依赖更新**: 定期(如每季度)审查并更新项目依赖,以修复潜在的安全漏洞和利用新特性。 \ No newline at end of file +# 星撰个人综合平台 - 软件开发概要设计 + +--- + +## 1. 引言 + +### 1.1. 文档目的 + +本概要设计文档旨在为"星撰"个人综合平台项目定义一个完整的软件开发生命周期(SDLC)框架。它将作为项目从规划、设计、实现到部署和维护的最高指导性文件,确保所有开发活动遵循标准化的软件工程最佳实践。 + +### 1.2. 项目概述 + +"星撰"是一个高度集成的个人数字化中枢,其核心功能、用户故事及非功能性需求已在《个人网站综合平台产品需求文档 (PRD)》中详细定义。本项目旨在将该PRD转化为一个稳定、可扩展、高质量的软件产品。 + +### 1.3. 范围 + +本文档覆盖范围包括: + +- 项目开发模型的选定。 +- 系统的高层体系结构设计。 +- 各阶段(设计、开发、测试、部署)的规划和规范。 +- 项目管理、风险控制和质量保证策略。 + +--- + +## 2. 项目规划与管理 + +### 2.1. 开发模型 + +本项目采用 **迭代式增量开发模型 (Iterative and Incremental Development)**。 + +- **迭代式**: 将开发过程划分为多个短周期的迭代,每个迭代都包含需求分析、设计、编码、测试的完整流程。 +- **增量式**: 每个迭代都会产出一个可测试、可交付的软件增量。产品功能将分阶段(见PRD中的优先级规划)逐步构建和完善。 + +**优势**: 此模型能够快速交付核心功能(MVP),及时获取反馈,灵活应对需求变化,并有效控制项目风险。 + +### 2.2. 项目里程碑 + +项目将按照PRD中的优先级划分为四个主要里程碑(阶段): + +| 里程碑 | 名称 | 核心交付物 | 目标 | +| ------ | ------------------ | ------------------------------------------------------- | -------------------------------- | +| M1 | **核心基础 (MVP)** | 基础用户系统、笔记核心功能、基础博客发布、CI/CD流程建立 | 验证核心架构,跑通开发部署全流程 | +| M2 | **内容增强** | 完整博客系统、个人空间、基础工具、搜索引擎集成 | 完善内容生态,提升用户体验 | +| M3 | **高级功能** | 阅读管理、文件管理、AI功能集成、第三方平台同步 | 扩展平台能力,引入智能化 | +| M4 | **生态完善** | 移动端应用、浏览器插件、高级分析功能、性能深度优化 | 构建跨平台生态,实现长期稳定运营 | + +### 2.3. 风险管理 + +| 风险类别 | 风险描述 | 可能性 | 影响 | 应对策略 | +| ------------ | ---------------------------------- | ------ | ---- | ---------------------------------------------------------- | +| **技术风险** | 第三方API变更(如B站、QQ音乐) | 中 | 中 | 封装Adapter层,隔离变化;设计优雅降级机制。 | +| | 新技术引入(Bun/Elysia)的未知问题 | 低 | 中 | 保持对社区的关注,构建完善的测试体系以快速发现问题。 | +| **进度风险** | 个人项目时间投入不稳定 | 高 | 高 | 严格遵循任务清单,将大任务分解为小任务,利用碎片时间完成。 | +| **安全风险** | 个人敏感数据(密码、文件)泄露 | 中 | 高 | 强制加密存储;定期进行安全扫描;代码审查时关注安全漏洞。 | + +--- + +## 3. 系统设计 + +### 3.1. 系统架构 + +系统采用分层、模块化的架构,实现前后端分离。 + +```mermaid +graph TD + subgraph "客户端 (Clients)" + WebApp[Web 应用 (Vue/React)] + BrowserExt[浏览器插件] + MobileApp[移动应用 (PWA/Native)] + end + + subgraph "服务端 (Backend - Elysia.js on Bun)" + API_Gateway[API 网关 / 路由层] + + subgraph "核心服务层 (Core Services)" + AuthService[认证与授权服务] + UserService[用户管理服务] + ContentService[内容管理服务 (笔记/博客)] + FileService[文件管理服务] + end + + subgraph "工具服务层 (Utility Services)" + ToolService[密码/书签/同步等工具服务] + end + + subgraph "插件与中间件 (Plugins & Middlewares)" + Logger[日志插件] + ErrorHandler[错误处理] + JWTMiddleware[JWT认证] + Swagger[Swagger文档] + end + end + + subgraph "数据与存储层 (Data & Storage)" + MySQL[(MySQL - 关系型数据)] + Redis[(Redis - 缓存/会话)] + Elasticsearch[(Elasticsearch - 全文搜索)] + ObjectStorage[对象存储 (OSS/NAS)] + end + + subgraph "第三方服务 (3rd Party Services)" + EmailService[邮件服务] + OAuth[OAuth 2.0 提供商] + AI_API[AI/LLM API] + end + + %% 连接关系 + WebApp --> API_Gateway + BrowserExt --> API_Gateway + MobileApp --> API_Gateway + + API_Gateway --> AuthService + API_Gateway --> UserService + API_Gateway --> ContentService + API_Gateway --> FileService + API_Gateway --> ToolService + + AuthService <--> MySQL + UserService <--> MySQL + ContentService <--> MySQL + ContentService <--> Elasticsearch + FileService <--> ObjectStorage + ToolService <--> MySQL + + AuthService --> JWTMiddleware + API_Gateway -- use --> Logger + API_Gateway -- use --> ErrorHandler + API_Gateway -- use --> JWTMiddleware + API_Gateway -- use --> Swagger + + subgraph "通用依赖" + AuthService <--> Redis + UserService <--> Redis + ContentService <--> Redis + end + + AuthService --> EmailService + AuthService --> OAuth + ContentService --> AI_API +``` + +- **客户端层**: 负责用户交互和数据展示。 +- **服务端层**: 核心业务逻辑处理中心。通过API网关统一入口,内部按业务垂直划分为多个服务模块。 +- **数据与存储层**: 持久化存储数据,提供缓存和搜索能力。 +- **第三方服务**: 集成外部服务以完成特定功能。 + +### 3.2. 模块划分 + +系统功能将按以下模块进行开发,每个模块对应`src/controllers`, `src/services`下的独立子目录。 + +1. **`core`**: 核心模块,包括用户、角色、权限、组织、字典等。 +2. **`content`**: 内容模块,包括笔记、博客、评论、分类、标签等。 +3. **`space`**: 个人空间模块,包括生活日志、社交互动。 +4. **`reader`**: 阅读模块,包括电子书管理、批注。 +5. **`tools`**: 工具模块,包括密码、书签、文件、同步等。 +6. **`system`**: 系统模块,包括健康检查、监控、配置管理。 + +### 3.3. 详细设计规划 + +本概要设计完成后,将对每个模块进行详细设计,产出物包括: + +- **数据库设计文档**: E-R图、表结构定义(字段、类型、约束、索引)、数据字典。 +- **API接口设计文档**: 使用Swagger/OpenAPI规范,定义每个端点的URL、HTTP方法、请求参数、响应体结构及错误码。 +- **UI/UX设计稿**: 针对核心页面和流程,制作线框图或高保真原型。 + +--- + +## 4. 实现与编码 + +### 4.1. 开发环境与工具 + +- **IDE**: Visual Studio Code +- **语言**: TypeScript +- **运行时**: Bun.js +- **包管理器**: Bun +- **版本控制**: Git / GitHub +- **代码检查与格式化**: ESLint / Prettier + +### 4.2. 编码规范 + +- 严格遵守项目已定义的注释、命名、代码风格规范。 +- 所有业务逻辑必须在 `services` 层实现,`controllers` 层只负责路由和数据校验。 +- 禁止在代码中硬编码配置项(如密钥、端口),所有配置通过环境变量或配置文件注入。 + +### 4.3. 版本控制策略 + +采用 **Gitflow工作流** 的简化版: + +- `main`: 主分支,存放稳定、可发布的代码。每个里程碑完成后合并。 +- `develop`: 开发分支,集成了所有已完成的功能。 +- `feat/feature-name`: 功能分支,每个新功能或任务都在此分支开发,完成后合并到 `develop`。 +- `fix/bug-name`: Bug修复分支。 + +--- + +## 5. 测试策略 + +### 5.1. 测试层次 + +- **单元测试**: 使用 `Vitest` 对 `services` 和 `utils` 中的核心函数进行测试,目标覆盖率 > 90%。 +- **接口测试 (集成测试)**: 使用 `supertest` 或Elysia的内置测试工具,对每个API端点进行测试,验证其与数据库、缓存的交互是否正确。 +- **端到端测试 (系统测试)**: 使用 `Playwright` 或 `Cypress` 对关键用户流程(如注册登录、发布博客)进行自动化测试。 +- **性能测试**: 使用 `autocannon` 对核心API进行压力测试,确保其满足技术指标。 + +### 5.2. 测试执行 + +- 所有代码提交前,必须在本地通过相关单元测试和接口测试。 +- CI/CD流水线将在每次向 `develop` 或 `main` 分支合并代码时,自动运行全量测试用例。测试不通过则禁止合并。 + +--- + +## 6. 部署与运维 + +### 6.1. 部署流程 (CI/CD) + +使用 **GitHub Actions** 自动化构建、测试和部署流程。 + +1. **Push to `feat/*`**: 触发ESLint检查和单元测试。 +2. **Merge to `develop`**: 触发全量测试,构建Docker镜像并推送到镜像仓库,然后部署到 **Staging (预发布)** 环境。 +3. **Merge to `main`**: 触发全量测试,构建生产Docker镜像,并部署到 **Production (生产)** 环境。 + +### 6.2. 基础设施 + +- **运行环境**: Docker容器。 +- **容器编排**: Docker Compose (初期) / Kubernetes (未来扩展)。 +- **数据库/缓存**: 使用云服务商提供的托管服务或在Docker中自建。 + +### 6.3. 运维监控 + +- **日志**: 使用 `pino` 记录结构化日志,并聚合到统一的日志管理平台(如ELK Stack或Loki)。 +- **监控与告警**: 使用 `Prometheus` 收集应用指标,`Grafana` 进行可视化展示,并针对关键指标(如API错误率、延迟)设置告警。 + +--- + +## 7. 维护与演进 + +- **问题跟踪**: 使用GitHub Issues跟踪Bug和功能请求。 +- **文档同步**: 所有代码变更,若涉及架构、API或数据库设计,必须同步更新相关设计文档。 +- **定期审查**: 每个里程碑结束后,进行项目复盘,评估开发流程、代码质量和系统架构,并进行优化。 +- **依赖更新**: 定期(如每季度)审查并更新项目依赖,以修复潜在的安全漏洞和利用新特性。 diff --git a/qq-email-setup.md b/qq-email-setup.md index c540ce5..88aae98 100644 --- a/qq-email-setup.md +++ b/qq-email-setup.md @@ -1,7 +1,9 @@ # QQ邮箱配置指南 ## 🚨 问题原因 + 错误 `550 The "From" header is missing or invalid` 是因为QQ邮箱要求: + 1. **From字段的邮箱地址必须与SMTP认证用户名完全一致** 2. **From字段格式必须符合RFC5322标准** @@ -41,6 +43,7 @@ EMAIL_REPLY_TO=your_qq_email@qq.com ### 第三步:替换为你的真实信息 **示例配置:** + ```env SMTP_HOST=smtp.qq.com SMTP_PORT=587 @@ -73,16 +76,21 @@ bun run src/tests/demo/emailDemo.ts your_receive_email@example.com ## 🔧 常见问题 ### Q: 为什么要用授权码而不是QQ密码? + A: QQ邮箱的安全策略,第三方应用必须使用授权码 ### Q: 授权码在哪里生成? + A: QQ邮箱设置 → 账户 → 开启IMAP/SMTP服务时生成 ### Q: 为什么From地址必须与SMTP_USER一致? + A: QQ邮箱的反欺诈机制,防止伪造发件人 ### Q: 可以使用其他邮箱服务吗? + A: 可以,修改SMTP_HOST即可: + - 163邮箱:`smtp.163.com` - Gmail:`smtp.gmail.com` - 企业邮箱:根据提供商配置 @@ -101,4 +109,4 @@ SMTP_PASS=替换为你的16位授权码 SMTP_FROM_EMAIL=替换为你的QQ邮箱@qq.com SMTP_FROM_NAME=星撰系统 EMAIL_REPLY_TO=替换为你的QQ邮箱@qq.com -``` \ No newline at end of file +``` diff --git a/quick-email-test.ts b/quick-email-test.ts index 3e6e982..6cc424b 100644 --- a/quick-email-test.ts +++ b/quick-email-test.ts @@ -3,15 +3,11 @@ * 运行方式: bun run quick-email-test.ts your@email.com */ -import { - initializeEmailService, - sendEmail, - closeEmailService -} from './src/plugins/email/email.service'; +import { initializeEmailService, sendEmail, closeEmailService } from './src/plugins/email/email.service'; async function quickTest() { const testEmail = process.argv[2]; - + if (!testEmail) { console.log('❌ 请提供邮箱地址'); console.log('💡 使用方法: bun run quick-email-test.ts your@email.com'); @@ -36,7 +32,7 @@ async function quickTest() {

🎉 邮件测试成功!

如果您收到这封邮件,说明邮件服务配置正确。

发送时间: ${new Date().toLocaleString('zh-CN')}

- ` + `, }); if (result.success) { @@ -47,7 +43,6 @@ async function quickTest() { console.log('❌ 邮件发送失败'); console.log(`💥 错误: ${result.error}`); } - } catch (error) { console.log('💥 执行失败:', error); } finally { @@ -56,4 +51,4 @@ async function quickTest() { } } -quickTest(); \ No newline at end of file +quickTest(); diff --git a/src/app.ts b/src/app.ts index 622a2ea..622a876 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,20 +18,6 @@ import { plugins } from '@/plugins/index'; // 格式化路由 import { formatRoute } from '@/utils/formatRoute'; -/** - * 自定义认证错误类 - * @description 用于处理认证相关的错误 - */ -class AuthenticationError extends Error { - constructor(message: string, code = 500) { - super(message); - this.name = 'AuthenticationError'; - if (code === 401) { - this.name = 'Unauthorized'; - } - } -} - /** * Elysia应用实例 * @description 主应用实例,集成所有插件和路由 diff --git a/src/config/email.config.ts b/src/config/email.config.ts index 445d6dd..5d361e1 100644 --- a/src/config/email.config.ts +++ b/src/config/email.config.ts @@ -127,4 +127,4 @@ export const getEmailConfig = () => ({ email: emailConfig, templates: emailTemplates, options: emailOptions, -}); \ No newline at end of file +}); diff --git a/src/config/jwt.config.ts b/src/config/jwt.config.ts index 1d576d4..84fe690 100644 --- a/src/config/jwt.config.ts +++ b/src/config/jwt.config.ts @@ -7,7 +7,6 @@ * @description 统一导出JWT密钥和过期时间,支持不同类型的token配置 */ - /** * JWT基础配置 * @property {string} secret - JWT签名密钥 @@ -21,4 +20,4 @@ export const jwtConfig = { issuer: process.env.JWT_ISSUER || 'elysia-api', /** JWT受众 */ audience: process.env.JWT_AUDIENCE || 'web-client', -}; \ No newline at end of file +}; diff --git a/src/config/redis.config.ts b/src/config/redis.config.ts index 2de2a04..e813a6f 100644 --- a/src/config/redis.config.ts +++ b/src/config/redis.config.ts @@ -1,45 +1,45 @@ -/** - * @file Redis数据库配置 - * @author hotok - * @date 2025-06-28 - * @lastEditor hotok - * @lastEditTime 2025-06-28 - * @description 读取环境变量并导出Redis连接配置 - */ - -/** - * Redis数据库连接配置 - * @property {string} connectName - Redis连接名称 - * @property {string} host - Redis服务器主机地址 - * @property {number} port - Redis服务器端口号 - * @property {string} username - Redis用户名 - * @property {string} password - Redis密码 - * @property {number} database - Redis数据库索引 - * @property {string} url - Redis连接URL - */ -export const redisConfig = { - /** Redis连接名称 */ - connectName: process.env.REDIS_CONNECT_NAME || 'cursor-init-redis', - /** Redis服务器主机地址 */ - host: process.env.REDIS_HOST || 'uair.cc', - /** Redis服务器端口号 */ - port: Number(process.env.REDIS_PORT) || 6379, - /** Redis用户名 */ - username: process.env.REDIS_USERNAME || 'default', - /** Redis密码 */ - password: process.env.REDIS_PASSWORD || 'nie', - /** Redis数据库索引 */ - database: Number(process.env.REDIS_DATABASE) || 0, -}; - -/** - * 构建Redis连接URL - * @returns Redis连接URL字符串 - */ -export const getRedisUrl = (): string => { - const { username, password, host, port, database } = redisConfig; - if (username && password) { - return `redis://${username}:${password}@${host}:${port}/${database}`; - } - return `redis://${host}:${port}/${database}`; -}; \ No newline at end of file +/** + * @file Redis数据库配置 + * @author hotok + * @date 2025-06-28 + * @lastEditor hotok + * @lastEditTime 2025-06-28 + * @description 读取环境变量并导出Redis连接配置 + */ + +/** + * Redis数据库连接配置 + * @property {string} connectName - Redis连接名称 + * @property {string} host - Redis服务器主机地址 + * @property {number} port - Redis服务器端口号 + * @property {string} username - Redis用户名 + * @property {string} password - Redis密码 + * @property {number} database - Redis数据库索引 + * @property {string} url - Redis连接URL + */ +export const redisConfig = { + /** Redis连接名称 */ + connectName: process.env.REDIS_CONNECT_NAME || 'cursor-init-redis', + /** Redis服务器主机地址 */ + host: process.env.REDIS_HOST || 'uair.cc', + /** Redis服务器端口号 */ + port: Number(process.env.REDIS_PORT) || 6379, + /** Redis用户名 */ + username: process.env.REDIS_USERNAME || 'default', + /** Redis密码 */ + password: process.env.REDIS_PASSWORD || 'nie', + /** Redis数据库索引 */ + database: Number(process.env.REDIS_DATABASE) || 0, +}; + +/** + * 构建Redis连接URL + * @returns Redis连接URL字符串 + */ +export const getRedisUrl = (): string => { + const { username, password, host, port, database } = redisConfig; + if (username && password) { + return `redis://${username}:${password}@${host}:${port}/${database}`; + } + return `redis://${host}:${port}/${database}`; +}; diff --git a/src/constants/swaggerTags.ts b/src/constants/swaggerTags.ts index bea4ae4..769fa49 100644 --- a/src/constants/swaggerTags.ts +++ b/src/constants/swaggerTags.ts @@ -28,9 +28,11 @@ export const tags = { permission: 'Permission', /** 验证码相关接口 */ captcha: 'Captcha', + /** 字典管理接口 */ + dict: 'Dict', } as const; /** * 标签类型定义 */ -export type ApiTag = typeof tags[keyof typeof tags]; \ No newline at end of file +export type ApiTag = (typeof tags)[keyof typeof tags]; diff --git a/src/eneities/customType.ts b/src/eneities/customType.ts index f0e9509..1da8e79 100644 --- a/src/eneities/customType.ts +++ b/src/eneities/customType.ts @@ -14,14 +14,16 @@ import { customType } from 'drizzle-orm/mysql-core'; // 写入读取是将bigint转化为string -export const bigintString = customType({ +export const bigintString = customType<{ data: string; driverData: bigint }>({ dataType() { return 'bigint'; }, - fromDriver(value) { // 数据库 -> JS - return value?.toString(); // 处理 null 值 + fromDriver(value) { + // 数据库 -> JS + return value?.toString(); }, - toDriver(value) { // JS -> 数据库 + toDriver(value) { + // JS -> 数据库 return BigInt(value as string); // 确保写入时为数字类型 - } -}); \ No newline at end of file + }, +}); diff --git a/src/eneities/index.ts b/src/eneities/index.ts index e2a85a0..8449f7c 100644 --- a/src/eneities/index.ts +++ b/src/eneities/index.ts @@ -1,317 +1,357 @@ -import { mysqlTable, mysqlSchema, AnyMySqlColumn, index, primaryKey, unique, varchar, text, int, json, datetime, tinyint, date } from "drizzle-orm/mysql-core" -import { sql } from "drizzle-orm" -import { bigintString as bigint } from "./customType" +import { + mysqlTable, + index, + primaryKey, + unique, + varchar, + int, + json, + timestamp, + text, + datetime, + tinyint, + date, +} from 'drizzle-orm/mysql-core'; +import { sql } from 'drizzle-orm'; +import { bigintString as bigint } from './customType'; -export const sysDictItems = mysqlTable("sys_dict_items", { - id: bigint({ mode: "number" }).notNull(), - typeId: bigint("type_id", { mode: "number" }).notNull(), - itemKey: varchar("item_key", { length: 50 }).notNull(), - itemValue: varchar("item_value", { length: 200 }).notNull(), - label: varchar({ length: 100 }).notNull(), - labelEn: varchar("label_en", { length: 200 }), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - sortOrder: int("sort_order").default(0).notNull(), - status: varchar({ length: 20 }).default('active').notNull(), - cssClass: varchar("css_class", { length: 50 }), - color: varchar({ length: 50 }), - extra: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_key").on(table.itemKey), - index("idx_pid").on(table.pid), - index("idx_sort").on(table.typeId, table.sortOrder), - index("idx_status").on(table.status), - index("idx_type_id").on(table.typeId), - primaryKey({ columns: [table.id], name: "sys_dict_items_id"}), - unique("uk_type_key").on(table.typeId, table.itemKey, table.deletedAt), -]); +export const sysDict = mysqlTable( + 'sys_dict', + { + id: bigint({ mode: 'number' }).notNull(), + code: varchar({ length: 50 }).notNull(), + name: varchar({ length: 100 }).notNull(), + value: varchar({ length: 200 }), + description: varchar({ length: 500 }), + icon: varchar({ length: 100 }), + pid: bigint({ mode: 'number' }), + level: int().default(1).notNull(), + sortOrder: int('sort_order').default(0).notNull(), + status: varchar({ length: 20 }).default('active').notNull(), + isSystem: tinyint('is_system').default(0).notNull(), + color: varchar({ length: 20 }), + extra: json(), + createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().onUpdateNow().notNull(), + }, + (table) => [ + index('idx_level').on(table.level), + index('idx_pid').on(table.pid), + index('idx_sort').on(table.sortOrder), + index('idx_status').on(table.status), + primaryKey({ columns: [table.id], name: 'sys_dict_id' }), + unique('uk_code').on(table.code), + ], +); -export const sysDictTypes = mysqlTable("sys_dict_types", { - id: bigint({ mode: "number" }).notNull(), - code: varchar({ length: 50 }).notNull(), - name: varchar({ length: 100 }).notNull(), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - status: varchar({ length: 20 }).default('active').notNull(), - isSystem: tinyint("is_system").default(0).notNull(), - sortOrder: int("sort_order").default(0).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_is_system").on(table.isSystem), - index("idx_name").on(table.name), - index("idx_path").on(table.path), - index("idx_pid").on(table.pid), - index("idx_sort").on(table.pid, table.sortOrder), - index("idx_status").on(table.status), - primaryKey({ columns: [table.id], name: "sys_dict_types_id"}), - unique("uk_code").on(table.code, table.deletedAt), -]); +export const sysOperationLogs = mysqlTable( + 'sys_operation_logs', + { + id: bigint({ mode: 'number' }).notNull(), + userId: bigint('user_id', { mode: 'number' }), + username: varchar({ length: 100 }), + module: varchar({ length: 50 }).notNull(), + action: varchar({ length: 50 }).notNull(), + target: varchar({ length: 200 }), + targetId: bigint('target_id', { mode: 'number' }), + requestData: text('request_data'), + responseData: text('response_data'), + status: varchar({ length: 20 }).notNull(), + ip: varchar({ length: 45 }), + userAgent: varchar('user_agent', { length: 200 }), + duration: bigint({ mode: 'number' }), + errorMsg: text('error_msg'), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_created_at').on(table.createdAt), + index('idx_ip').on(table.ip), + index('idx_module_action').on(table.module, table.action), + index('idx_status').on(table.status), + index('idx_target').on(table.targetId), + index('idx_user_id').on(table.userId), + primaryKey({ columns: [table.id], name: 'sys_operation_logs_id' }), + ], +); -export const sysOperationLogs = mysqlTable("sys_operation_logs", { - id: bigint({ mode: "number" }).notNull(), - userId: bigint("user_id", { mode: "number" }), - username: varchar({ length: 100 }), - module: varchar({ length: 50 }).notNull(), - action: varchar({ length: 50 }).notNull(), - target: varchar({ length: 200 }), - targetId: bigint("target_id", { mode: "number" }), - requestData: text("request_data"), - responseData: text("response_data"), - status: varchar({ length: 20 }).notNull(), - ip: varchar({ length: 45 }), - userAgent: varchar("user_agent", { length: 200 }), - duration: bigint({ mode: "number" }), - errorMsg: text("error_msg"), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_created_at").on(table.createdAt), - index("idx_ip").on(table.ip), - index("idx_module_action").on(table.module, table.action), - index("idx_status").on(table.status), - index("idx_target").on(table.targetId), - index("idx_user_id").on(table.userId), - primaryKey({ columns: [table.id], name: "sys_operation_logs_id"}), -]); +export const sysOrganizations = mysqlTable( + 'sys_organizations', + { + id: bigint({ mode: 'number' }).notNull(), + code: varchar({ length: 100 }).notNull(), + name: varchar({ length: 200 }).notNull(), + fullName: varchar('full_name', { length: 200 }), + description: text(), + pid: bigint({ mode: 'number' }), + path: varchar({ length: 500 }), + level: int().default(1).notNull(), + type: varchar({ length: 20 }), + status: varchar({ length: 20 }).default('active').notNull(), + sortOrder: int('sort_order').default(0).notNull(), + leaderId: bigint('leader_id', { mode: 'number' }), + address: varchar({ length: 200 }), + phone: varchar({ length: 50 }), + extra: json(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + updatedBy: bigint('updated_by', { mode: 'number' }), + updatedAt: datetime('updated_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + version: int().default(1).notNull(), + }, + (table) => [ + index('idx_deleted_at').on(table.deletedAt), + index('idx_leader_id').on(table.leaderId), + index('idx_name').on(table.name), + index('idx_path').on(table.path), + index('idx_pid').on(table.pid), + index('idx_sort').on(table.pid, table.sortOrder), + index('idx_status').on(table.status), + index('idx_type').on(table.type), + primaryKey({ columns: [table.id], name: 'sys_organizations_id' }), + unique('uk_code').on(table.code, table.deletedAt), + ], +); -export const sysOrganizations = mysqlTable("sys_organizations", { - id: bigint({ mode: "number" }).notNull(), - code: varchar({ length: 100 }).notNull(), - name: varchar({ length: 200 }).notNull(), - fullName: varchar("full_name", { length: 200 }), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - type: varchar({ length: 20 }), - status: varchar({ length: 20 }).default('active').notNull(), - sortOrder: int("sort_order").default(0).notNull(), - leaderId: bigint("leader_id", { mode: "number" }), - address: varchar({ length: 200 }), - phone: varchar({ length: 50 }), - extra: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), - version: int().default(1).notNull(), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_leader_id").on(table.leaderId), - index("idx_name").on(table.name), - index("idx_path").on(table.path), - index("idx_pid").on(table.pid), - index("idx_sort").on(table.pid, table.sortOrder), - index("idx_status").on(table.status), - index("idx_type").on(table.type), - primaryKey({ columns: [table.id], name: "sys_organizations_id"}), - unique("uk_code").on(table.code, table.deletedAt), -]); +export const sysPermissions = mysqlTable( + 'sys_permissions', + { + id: bigint({ mode: 'number' }).notNull(), + code: varchar({ length: 100 }).notNull(), + name: varchar({ length: 100 }).notNull(), + type: varchar({ length: 20 }).notNull(), + resource: varchar({ length: 50 }), + action: varchar({ length: 50 }), + description: text(), + pid: bigint({ mode: 'number' }), + path: varchar({ length: 500 }), + level: int().default(1).notNull(), + sortOrder: int('sort_order').default(0).notNull(), + status: varchar({ length: 20 }).default('active').notNull(), + meta: json(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + updatedBy: bigint('updated_by', { mode: 'number' }), + updatedAt: datetime('updated_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + }, + (table) => [ + index('idx_deleted_at').on(table.deletedAt), + index('idx_pid').on(table.pid), + index('idx_resource_action').on(table.resource, table.action), + index('idx_sort').on(table.pid, table.sortOrder), + index('idx_status').on(table.status), + index('idx_type').on(table.type), + primaryKey({ columns: [table.id], name: 'sys_permissions_id' }), + unique('uk_code').on(table.code, table.deletedAt), + ], +); -export const sysPermissions = mysqlTable("sys_permissions", { - id: bigint({ mode: "number" }).notNull(), - code: varchar({ length: 100 }).notNull(), - name: varchar({ length: 100 }).notNull(), - type: varchar({ length: 20 }).notNull(), - resource: varchar({ length: 50 }), - action: varchar({ length: 50 }), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - sortOrder: int("sort_order").default(0).notNull(), - status: varchar({ length: 20 }).default('active').notNull(), - meta: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_pid").on(table.pid), - index("idx_resource_action").on(table.resource, table.action), - index("idx_sort").on(table.pid, table.sortOrder), - index("idx_status").on(table.status), - index("idx_type").on(table.type), - primaryKey({ columns: [table.id], name: "sys_permissions_id"}), - unique("uk_code").on(table.code, table.deletedAt), -]); +export const sysRolePermissions = mysqlTable( + 'sys_role_permissions', + { + id: bigint({ mode: 'number' }).notNull(), + roleId: bigint('role_id', { mode: 'number' }).notNull(), + permissionId: bigint('permission_id', { mode: 'number' }).notNull(), + isHalf: tinyint('is_half').default(0).notNull(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_is_half').on(table.isHalf), + index('idx_permission_id').on(table.permissionId), + index('idx_role_id').on(table.roleId), + primaryKey({ columns: [table.id], name: 'sys_role_permissions_id' }), + unique('uk_role_permission').on(table.roleId, table.permissionId), + ], +); -export const sysRolePermissions = mysqlTable("sys_role_permissions", { - id: bigint({ mode: "number" }).notNull(), - roleId: bigint("role_id", { mode: "number" }).notNull(), - permissionId: bigint("permission_id", { mode: "number" }).notNull(), - isHalf: tinyint("is_half").default(0).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_is_half").on(table.isHalf), - index("idx_permission_id").on(table.permissionId), - index("idx_role_id").on(table.roleId), - primaryKey({ columns: [table.id], name: "sys_role_permissions_id"}), - unique("uk_role_permission").on(table.roleId, table.permissionId), -]); +export const sysRoles = mysqlTable( + 'sys_roles', + { + id: bigint({ mode: 'number' }).notNull(), + code: varchar({ length: 50 }).notNull(), + name: varchar({ length: 100 }).notNull(), + description: text(), + pid: bigint({ mode: 'number' }), + path: varchar({ length: 500 }), + level: int().default(1).notNull(), + sortOrder: int('sort_order').default(0).notNull(), + status: varchar({ length: 20 }).default('active').notNull(), + isSystem: tinyint('is_system').default(0).notNull(), + permissionsSnapshot: json('permissions_snapshot'), + extra: json(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + updatedBy: bigint('updated_by', { mode: 'number' }), + updatedAt: datetime('updated_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + version: int().default(1).notNull(), + }, + (table) => [ + index('idx_deleted_at').on(table.deletedAt), + index('idx_is_system').on(table.isSystem), + index('idx_name').on(table.name), + index('idx_path').on(table.path), + index('idx_pid').on(table.pid), + index('idx_sort').on(table.pid, table.sortOrder), + index('idx_status').on(table.status), + primaryKey({ columns: [table.id], name: 'sys_roles_id' }), + unique('uk_code').on(table.code, table.deletedAt), + ], +); -export const sysRoles = mysqlTable("sys_roles", { - id: bigint({ mode: "number" }).notNull(), - code: varchar({ length: 50 }).notNull(), - name: varchar({ length: 100 }).notNull(), - description: text(), - pid: bigint({ mode: "number" }), - path: varchar({ length: 500 }), - level: int().default(1).notNull(), - sortOrder: int("sort_order").default(0).notNull(), - status: varchar({ length: 20 }).default('active').notNull(), - isSystem: tinyint("is_system").default(0).notNull(), - permissionsSnapshot: json("permissions_snapshot"), - extra: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), - version: int().default(1).notNull(), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_is_system").on(table.isSystem), - index("idx_name").on(table.name), - index("idx_path").on(table.path), - index("idx_pid").on(table.pid), - index("idx_sort").on(table.pid, table.sortOrder), - index("idx_status").on(table.status), - primaryKey({ columns: [table.id], name: "sys_roles_id"}), - unique("uk_code").on(table.code, table.deletedAt), -]); +export const sysTags = mysqlTable( + 'sys_tags', + { + id: bigint({ mode: 'number' }).notNull(), + name: varchar({ length: 50 }).notNull(), + type: varchar({ length: 50 }).default('user'), + color: varchar({ length: 50 }), + description: text(), + usageCount: int('usage_count').default(0).notNull(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + }, + (table) => [ + index('idx_deleted_at').on(table.deletedAt), + index('idx_name').on(table.name), + index('idx_type').on(table.type), + index('idx_usage_count').on(table.usageCount), + primaryKey({ columns: [table.id], name: 'sys_tags_id' }), + unique('uk_name_type').on(table.name, table.type, table.deletedAt), + ], +); -export const sysTags = mysqlTable("sys_tags", { - id: bigint({ mode: "number" }).notNull(), - name: varchar({ length: 50 }).notNull(), - type: varchar({ length: 50 }).default('user'), - color: varchar({ length: 50 }), - description: text(), - usageCount: int("usage_count").default(0).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), -}, -(table) => [ - index("idx_deleted_at").on(table.deletedAt), - index("idx_name").on(table.name), - index("idx_type").on(table.type), - index("idx_usage_count").on(table.usageCount), - primaryKey({ columns: [table.id], name: "sys_tags_id"}), - unique("uk_name_type").on(table.name, table.type, table.deletedAt), -]); +export const sysUserOrganizations = mysqlTable( + 'sys_user_organizations', + { + id: bigint({ mode: 'number' }).notNull(), + userId: bigint('user_id', { mode: 'number' }).notNull(), + organizationId: bigint('organization_id', { mode: 'number' }).notNull(), + isPrimary: tinyint('is_primary').default(0).notNull(), + position: varchar({ length: 100 }), + joinedAt: datetime('joined_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_is_primary').on(table.isPrimary), + index('idx_joined_at').on(table.joinedAt), + index('idx_organization_id').on(table.organizationId), + index('idx_user_id').on(table.userId), + primaryKey({ columns: [table.id], name: 'sys_user_organizations_id' }), + unique('uk_user_org').on(table.userId, table.organizationId), + ], +); -export const sysUserOrganizations = mysqlTable("sys_user_organizations", { - id: bigint({ mode: "number" }).notNull(), - userId: bigint("user_id", { mode: "number" }).notNull(), - organizationId: bigint("organization_id", { mode: "number" }).notNull(), - isPrimary: tinyint("is_primary").default(0).notNull(), - position: varchar({ length: 100 }), - joinedAt: datetime("joined_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_is_primary").on(table.isPrimary), - index("idx_joined_at").on(table.joinedAt), - index("idx_organization_id").on(table.organizationId), - index("idx_user_id").on(table.userId), - primaryKey({ columns: [table.id], name: "sys_user_organizations_id"}), - unique("uk_user_org").on(table.userId, table.organizationId), -]); +export const sysUserRoles = mysqlTable( + 'sys_user_roles', + { + id: bigint({ mode: 'number' }).notNull(), + userId: bigint('user_id', { mode: 'number' }).notNull(), + roleId: bigint('role_id', { mode: 'number' }).notNull(), + expiredAt: datetime('expired_at', { mode: 'string' }), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_created_at').on(table.createdAt), + index('idx_expired_at').on(table.expiredAt), + index('idx_role_id').on(table.roleId), + index('idx_user_id').on(table.userId), + primaryKey({ columns: [table.id], name: 'sys_user_roles_id' }), + unique('uk_user_role').on(table.userId, table.roleId), + ], +); -export const sysUserRoles = mysqlTable("sys_user_roles", { - id: bigint({ mode: "number" }).notNull(), - userId: bigint("user_id", { mode: "number" }).notNull(), - roleId: bigint("role_id", { mode: "number" }).notNull(), - expiredAt: datetime("expired_at", { mode: 'string'}), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_created_at").on(table.createdAt), - index("idx_expired_at").on(table.expiredAt), - index("idx_role_id").on(table.roleId), - index("idx_user_id").on(table.userId), - primaryKey({ columns: [table.id], name: "sys_user_roles_id"}), - unique("uk_user_role").on(table.userId, table.roleId), -]); +export const sysUserTags = mysqlTable( + 'sys_user_tags', + { + id: bigint({ mode: 'number' }).notNull(), + userId: bigint('user_id', { mode: 'number' }).notNull(), + tagId: bigint('tag_id', { mode: 'number' }).notNull(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + }, + (table) => [ + index('idx_created_at').on(table.createdAt), + index('idx_tag_id').on(table.tagId), + index('idx_user_id').on(table.userId), + primaryKey({ columns: [table.id], name: 'sys_user_tags_id' }), + unique('uk_user_tag').on(table.userId, table.tagId), + ], +); -export const sysUserTags = mysqlTable("sys_user_tags", { - id: bigint({ mode: "number" }).notNull(), - userId: bigint("user_id", { mode: "number" }).notNull(), - tagId: bigint("tag_id", { mode: "number" }).notNull(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), -}, -(table) => [ - index("idx_created_at").on(table.createdAt), - index("idx_tag_id").on(table.tagId), - index("idx_user_id").on(table.userId), - primaryKey({ columns: [table.id], name: "sys_user_tags_id"}), - unique("uk_user_tag").on(table.userId, table.tagId), -]); - -export const sysUsers = mysqlTable("sys_users", { - id: bigint({ mode: "number" }).notNull(), - username: varchar({ length: 50 }).notNull(), - email: varchar({ length: 100 }).notNull(), - mobile: varchar({ length: 20 }), - passwordHash: varchar("password_hash", { length: 255 }).notNull(), - avatar: varchar({ length: 255 }), - nickname: varchar({ length: 100 }), - status: varchar({ length: 20 }).default('active').notNull(), - gender: tinyint().default(0), - // you can use { mode: 'date' }, if you want to have Date as type for this column - birthday: date({ mode: 'string' }), - bio: varchar({ length: 500 }), - loginCount: int("login_count").default(0).notNull(), - lastLoginAt: datetime("last_login_at", { mode: 'string'}), - lastLoginIp: varchar("last_login_ip", { length: 45 }), - failedAttempts: int("failed_attempts").default(0).notNull(), - lockedUntil: datetime("locked_until", { mode: 'string'}), - isRoot: tinyint("is_root").default(0).notNull(), - extra: json(), - createdBy: bigint("created_by", { mode: "number" }), - createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - updatedBy: bigint("updated_by", { mode: "number" }), - updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), - deletedAt: datetime("deleted_at", { mode: 'string'}), - version: int().default(1).notNull(), -}, -(table) => [ - index("idx_created_at").on(table.createdAt), - index("idx_deleted_at").on(table.deletedAt), - index("idx_is_root").on(table.isRoot), - index("idx_last_login").on(table.lastLoginAt), - index("idx_mobile").on(table.mobile), - index("idx_status").on(table.status), - primaryKey({ columns: [table.id], name: "sys_users_id"}), - unique("uk_email").on(table.email, table.deletedAt), - unique("uk_username").on(table.username, table.deletedAt), -]); +export const sysUsers = mysqlTable( + 'sys_users', + { + id: bigint({ mode: 'number' }).notNull(), + username: varchar({ length: 50 }).notNull(), + email: varchar({ length: 100 }).notNull(), + mobile: varchar({ length: 20 }), + passwordHash: varchar('password_hash', { length: 255 }).notNull(), + avatar: varchar({ length: 255 }), + nickname: varchar({ length: 100 }), + status: varchar({ length: 20 }).default('active').notNull(), + gender: tinyint().default(0), + // you can use { mode: 'date' }, if you want to have Date as type for this column + birthday: date({ mode: 'string' }), + bio: varchar({ length: 500 }), + loginCount: int('login_count').default(0).notNull(), + lastLoginAt: datetime('last_login_at', { mode: 'string' }), + lastLoginIp: varchar('last_login_ip', { length: 45 }), + failedAttempts: int('failed_attempts').default(0).notNull(), + lockedUntil: datetime('locked_until', { mode: 'string' }), + isRoot: tinyint('is_root').default(0).notNull(), + extra: json(), + createdBy: bigint('created_by', { mode: 'number' }), + createdAt: datetime('created_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + updatedBy: bigint('updated_by', { mode: 'number' }), + updatedAt: datetime('updated_at', { mode: 'string' }) + .default(sql`(CURRENT_TIMESTAMP)`) + .notNull(), + deletedAt: datetime('deleted_at', { mode: 'string' }), + version: int().default(1).notNull(), + }, + (table) => [ + index('idx_created_at').on(table.createdAt), + index('idx_deleted_at').on(table.deletedAt), + index('idx_is_root').on(table.isRoot), + index('idx_last_login').on(table.lastLoginAt), + index('idx_mobile').on(table.mobile), + index('idx_status').on(table.status), + primaryKey({ columns: [table.id], name: 'sys_users_id' }), + unique('uk_email').on(table.email, table.deletedAt), + unique('uk_username').on(table.username, table.deletedAt), + ], +); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 0ba99bb..cbef9a7 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,6 +1,6 @@ /** * @file 认证模块Controller层实现 - * @author AI Assistant + * @author AI Assistant * @date 2024-12-19 * @lastEditor AI Assistant * @lastEditTime 2025-01-07 @@ -8,8 +8,22 @@ */ import { Elysia } from 'elysia'; -import { RegisterSchema, ActivateSchema, LoginSchema, RefreshSchema, ResetPasswordRequestSchema, ResetPasswordConfirmSchema } from './auth.schema'; -import { RegisterResponsesSchema, ActivateResponsesSchema, LoginResponsesSchema, RefreshResponsesSchema, ResetPasswordRequestResponsesSchema, ResetPasswordConfirmResponsesSchema } from './auth.response'; +import { + RegisterSchema, + ActivateSchema, + LoginSchema, + RefreshSchema, + ResetPasswordRequestSchema, + ResetPasswordConfirmSchema, +} from './auth.schema'; +import { + RegisterResponsesSchema, + ActivateResponsesSchema, + LoginResponsesSchema, + RefreshResponsesSchema, + ResetPasswordRequestResponsesSchema, + ResetPasswordConfirmResponsesSchema, +} from './auth.response'; import { authService } from './auth.service'; import { tags } from '@/constants/swaggerTags'; @@ -25,21 +39,17 @@ export const authController = new Elysia() * @param body RegisterRequest 注册请求参数 * @returns RegisterSuccessResponse | RegisterErrorResponse */ - .post( - '/register', - ({ body, set }) => authService.register(body), - { - body: RegisterSchema, - detail: { - summary: '用户注册', - description: '用户注册接口,需要提供用户名、邮箱、密码和验证码', - tags: [tags.auth], - operationId: 'registerUser', - }, - response: RegisterResponsesSchema, - } - ) - + .post('/register', ({ body }) => authService.register(body), { + body: RegisterSchema, + detail: { + summary: '用户注册', + description: '用户注册接口,需要提供用户名、邮箱、密码和验证码', + tags: [tags.auth], + operationId: 'registerUser', + }, + response: RegisterResponsesSchema, + }) + /** * 邮箱激活接口 * @route POST /api/auth/activate @@ -47,21 +57,17 @@ export const authController = new Elysia() * @param body ActivateRequest 激活请求参数 * @returns ActivateSuccessResponse | ActivateErrorResponse */ - .post( - '/activate', - ({ body, set }) => authService.activate(body), - { - body: ActivateSchema, - detail: { - summary: '邮箱激活', - description: '通过激活Token激活用户邮箱,激活成功后用户状态将变为active', - tags: [tags.auth], - operationId: 'activateUser', - }, - response: ActivateResponsesSchema, - } - ) - + .post('/activate', ({ body }) => authService.activate(body), { + body: ActivateSchema, + detail: { + summary: '邮箱激活', + description: '通过激活Token激活用户邮箱,激活成功后用户状态将变为active', + tags: [tags.auth], + operationId: 'activateUser', + }, + response: ActivateResponsesSchema, + }) + /** * 用户登录接口 * @route POST /api/auth/login @@ -69,21 +75,17 @@ export const authController = new Elysia() * @param body LoginRequest 登录请求参数 * @returns LoginSuccessResponse | LoginErrorResponse */ - .post( - '/login', - ({ body, set }) => authService.login(body), - { - body: LoginSchema, - detail: { - summary: '用户登录', - description: '用户登录接口,支持用户名或邮箱登录,登录成功返回JWT访问令牌和刷新令牌', - tags: [tags.auth], - operationId: 'loginUser', - }, - response: LoginResponsesSchema, - } - ) - + .post('/login', ({ body }) => authService.login(body), { + body: LoginSchema, + detail: { + summary: '用户登录', + description: '用户登录接口,支持用户名或邮箱登录,登录成功返回JWT访问令牌和刷新令牌', + tags: [tags.auth], + operationId: 'loginUser', + }, + response: LoginResponsesSchema, + }) + /** * Token刷新接口 * @route POST /api/auth/refresh @@ -91,21 +93,17 @@ export const authController = new Elysia() * @param body RefreshRequest 刷新请求参数 * @returns RefreshSuccessResponse | RefreshErrorResponse */ - .post( - '/refresh', - ({ body, set }) => authService.refresh(body), - { - body: RefreshSchema, - detail: { - summary: 'Token刷新', - description: '使用刷新令牌获取新的访问令牌和刷新令牌,延长用户会话时间', - tags: [tags.auth], - operationId: 'refreshToken', - }, - response: RefreshResponsesSchema, - } - ) - + .post('/refresh', ({ body }) => authService.refresh(body), { + body: RefreshSchema, + detail: { + summary: 'Token刷新', + description: '使用刷新令牌获取新的访问令牌和刷新令牌,延长用户会话时间', + tags: [tags.auth], + operationId: 'refreshToken', + }, + response: RefreshResponsesSchema, + }) + /** * 找回密码接口 * @route POST /api/auth/password/reset-request @@ -113,21 +111,17 @@ export const authController = new Elysia() * @param body ResetPasswordRequestRequest 找回密码请求参数 * @returns ResetPasswordRequestSuccessResponse | ResetPasswordRequestErrorResponse */ - .post( - '/password/reset-request', - ({ body, set }) => authService.resetPasswordRequest(body), - { - body: ResetPasswordRequestSchema, - detail: { - summary: '找回密码', - description: '用户忘记密码时发送重置邮件到注册邮箱,邮件包含重置链接', - tags: [tags.auth], - operationId: 'resetPasswordRequest', - }, - response: ResetPasswordRequestResponsesSchema, - } - ) - + .post('/password/reset-request', ({ body }) => authService.resetPasswordRequest(body), { + body: ResetPasswordRequestSchema, + detail: { + summary: '找回密码', + description: '用户忘记密码时发送重置邮件到注册邮箱,邮件包含重置链接', + tags: [tags.auth], + operationId: 'resetPasswordRequest', + }, + response: ResetPasswordRequestResponsesSchema, + }) + /** * 重置密码接口 * @route POST /api/auth/password/reset-confirm @@ -135,17 +129,13 @@ export const authController = new Elysia() * @param body ResetPasswordConfirmRequest 重置密码请求参数 * @returns ResetPasswordConfirmSuccessResponse | ResetPasswordConfirmErrorResponse */ - .post( - '/password/reset-confirm', - ({ body, set }) => authService.resetPasswordConfirm(body), - { - body: ResetPasswordConfirmSchema, - detail: { - summary: '重置密码', - description: '用户通过重置令牌设置新密码,需要提供令牌、新密码和确认密码', - tags: [tags.auth], - operationId: 'resetPasswordConfirm', - }, - response: ResetPasswordConfirmResponsesSchema, - } - ); \ No newline at end of file + .post('/password/reset-confirm', ({ body }) => authService.resetPasswordConfirm(body), { + body: ResetPasswordConfirmSchema, + detail: { + summary: '重置密码', + description: '用户通过重置令牌设置新密码,需要提供令牌、新密码和确认密码', + tags: [tags.auth], + operationId: 'resetPasswordConfirm', + }, + response: ResetPasswordConfirmResponsesSchema, + }); diff --git a/src/modules/auth/auth.response.ts b/src/modules/auth/auth.response.ts index 857466f..1ca8f3a 100644 --- a/src/modules/auth/auth.response.ts +++ b/src/modules/auth/auth.response.ts @@ -17,36 +17,38 @@ import { responseWrapperSchema } from '@/utils/responseFormate'; * @description 用于Controller中定义所有可能的响应格式 */ export const RegisterResponsesSchema = { - 200: responseWrapperSchema(t.Object({ - /** 用户ID */ - id: t.String({ - description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', - examples: ['1', '2', '3'] + 200: responseWrapperSchema( + t.Object({ + /** 用户ID */ + id: t.String({ + description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', + examples: ['1', '2', '3'], + }), + /** 用户名 */ + username: t.String({ + description: '用户名', + examples: ['admin', 'testuser'], + }), + /** 邮箱地址 */ + email: t.String({ + description: '邮箱地址', + examples: ['user@example.com'], + }), + /** 账号状态 */ + status: t.String({ + description: '账号状态', + examples: ['pending', 'active'], + }), + /** 创建时间 */ + createdAt: t.String({ + description: '创建时间', + examples: ['2024-12-19T10:30:00Z'], + }), }), - /** 用户名 */ - username: t.String({ - description: '用户名', - examples: ['admin', 'testuser'] - }), - /** 邮箱地址 */ - email: t.String({ - description: '邮箱地址', - examples: ['user@example.com'] - }), - /** 账号状态 */ - status: t.String({ - description: '账号状态', - examples: ['pending', 'active'] - }), - /** 创建时间 */ - createdAt: t.String({ - description: '创建时间', - examples: ['2024-12-19T10:30:00Z'] - }) - })), + ), }; /** 用户注册成功响应数据类型 */ -export type RegisterResponsesType = Static; +export type RegisterResponsesType = Static<(typeof RegisterResponsesSchema)[200]>; // ========== 邮箱激活相关响应格式 ========== @@ -55,42 +57,44 @@ export type RegisterResponsesType = Static; * @description 用于Controller中定义所有可能的响应格式 */ export const ActivateResponsesSchema = { - 200: responseWrapperSchema(t.Object({ - /** 用户ID */ - id: t.String({ - description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', - examples: ['1', '2', '3'] + 200: responseWrapperSchema( + t.Object({ + /** 用户ID */ + id: t.String({ + description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', + examples: ['1', '2', '3'], + }), + /** 用户名 */ + username: t.String({ + description: '用户名', + examples: ['admin', 'testuser'], + }), + /** 邮箱地址 */ + email: t.String({ + description: '邮箱地址', + examples: ['user@example.com'], + }), + /** 账号状态 */ + status: t.String({ + description: '账号状态', + examples: ['active'], + }), + /** 激活时间 */ + updatedAt: t.String({ + description: '激活时间', + examples: ['2024-12-19T10:30:00Z'], + }), + /** 激活成功标识 */ + activated: t.Boolean({ + description: '是否已激活', + examples: [true], + }), }), - /** 用户名 */ - username: t.String({ - description: '用户名', - examples: ['admin', 'testuser'] - }), - /** 邮箱地址 */ - email: t.String({ - description: '邮箱地址', - examples: ['user@example.com'] - }), - /** 账号状态 */ - status: t.String({ - description: '账号状态', - examples: ['active'] - }), - /** 激活时间 */ - updatedAt: t.String({ - description: '激活时间', - examples: ['2024-12-19T10:30:00Z'] - }), - /** 激活成功标识 */ - activated: t.Boolean({ - description: '是否已激活', - examples: [true] - }) - })), + ), }; /** 邮箱激活成功响应数据类型 */ -export type ActivateSuccessType = Static; +export type ActivateSuccessType = Static<(typeof ActivateResponsesSchema)[200]>; // ========== 用户登录相关响应格式 ========== @@ -99,68 +103,70 @@ export type ActivateSuccessType = Static; * @description 用于Controller中定义所有可能的响应格式 */ export const LoginResponsesSchema = { - 200: responseWrapperSchema(t.Object({ - /** 用户基本信息 */ - user: t.Object({ - /** 用户ID */ - id: t.String({ - description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', - examples: ['1', '2', '3'] + 200: responseWrapperSchema( + t.Object({ + /** 用户基本信息 */ + user: t.Object({ + /** 用户ID */ + id: t.String({ + description: '用户ID(bigint类型以字符串形式返回防止精度丢失)', + examples: ['1', '2', '3'], + }), + /** 用户名 */ + username: t.String({ + description: '用户名', + examples: ['admin', 'testuser'], + }), + /** 邮箱地址 */ + email: t.String({ + description: '邮箱地址', + examples: ['user@example.com'], + }), + /** 账号状态 */ + status: t.String({ + description: '账号状态', + examples: ['active'], + }), + /** 最后登录时间 */ + lastLoginAt: t.Union([t.String(), t.Null()], { + description: '最后登录时间', + examples: ['2024-12-19T10:30:00Z', null], + }), }), - /** 用户名 */ - username: t.String({ - description: '用户名', - examples: ['admin', 'testuser'] + /** 认证令牌信息 */ + tokens: t.Object({ + /** 访问令牌 */ + accessToken: t.String({ + description: 'JWT访问令牌', + examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], + }), + /** 刷新令牌 */ + refreshToken: t.String({ + description: 'JWT刷新令牌', + examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], + }), + /** 令牌类型 */ + tokenType: t.String({ + description: '令牌类型', + examples: ['Bearer'], + }), + /** 过期时间(秒) */ + expiresIn: t.String({ + description: '访问令牌过期时间(秒)', + examples: [7200, 86400], + }), + /** 刷新令牌过期时间(秒) */ + refreshExpiresIn: t.String({ + description: '刷新令牌过期时间(秒)', + examples: [2592000], + }), }), - /** 邮箱地址 */ - email: t.String({ - description: '邮箱地址', - examples: ['user@example.com'] - }), - /** 账号状态 */ - status: t.String({ - description: '账号状态', - examples: ['active'] - }), - /** 最后登录时间 */ - lastLoginAt: t.Union([t.String(), t.Null()], { - description: '最后登录时间', - examples: ['2024-12-19T10:30:00Z', null] - }) }), - /** 认证令牌信息 */ - tokens: t.Object({ - /** 访问令牌 */ - accessToken: t.String({ - description: 'JWT访问令牌', - examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] - }), - /** 刷新令牌 */ - refreshToken: t.String({ - description: 'JWT刷新令牌', - examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] - }), - /** 令牌类型 */ - tokenType: t.String({ - description: '令牌类型', - examples: ['Bearer'] - }), - /** 过期时间(秒) */ - expiresIn: t.String({ - description: '访问令牌过期时间(秒)', - examples: [7200, 86400] - }), - /** 刷新令牌过期时间(秒) */ - refreshExpiresIn: t.String({ - description: '刷新令牌过期时间(秒)', - examples: [2592000] - }) - }) - })), + ), }; /** 用户登录成功响应数据类型 */ -export type LoginSuccessType = Static; +export type LoginSuccessType = Static<(typeof LoginResponsesSchema)[200]>; // ========== Token刷新相关响应格式 ========== @@ -169,45 +175,47 @@ export type LoginSuccessType = Static; * @description 用于Controller中定义所有可能的响应格式 */ export const RefreshResponsesSchema = { - 200: responseWrapperSchema(t.Object({ - /** 认证令牌信息 */ - tokens: t.Object({ - /** 访问令牌 */ - accessToken: t.String({ - description: 'JWT访问令牌', - examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] + 200: responseWrapperSchema( + t.Object({ + /** 认证令牌信息 */ + tokens: t.Object({ + /** 访问令牌 */ + accessToken: t.String({ + description: 'JWT访问令牌', + examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], + }), + /** 刷新令牌 */ + refreshToken: t.String({ + description: 'JWT刷新令牌', + examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], + }), + /** 令牌类型 */ + tokenType: t.String({ + description: '令牌类型', + examples: ['Bearer'], + }), + /** 过期时间(秒) */ + expiresIn: t.String({ + description: '访问令牌过期时间(秒)', + examples: [7200, 86400], + }), + /** 刷新令牌过期时间(秒) */ + refreshExpiresIn: t.String({ + description: '刷新令牌过期时间(秒)', + examples: [2592000], + }), }), - /** 刷新令牌 */ - refreshToken: t.String({ - description: 'JWT刷新令牌', - examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] + /** 刷新时间 */ + refreshedAt: t.String({ + description: '令牌刷新时间', + examples: ['2024-12-19T10:30:00Z'], }), - /** 令牌类型 */ - tokenType: t.String({ - description: '令牌类型', - examples: ['Bearer'] - }), - /** 过期时间(秒) */ - expiresIn: t.String({ - description: '访问令牌过期时间(秒)', - examples: [7200, 86400] - }), - /** 刷新令牌过期时间(秒) */ - refreshExpiresIn: t.String({ - description: '刷新令牌过期时间(秒)', - examples: [2592000] - }) }), - /** 刷新时间 */ - refreshedAt: t.String({ - description: '令牌刷新时间', - examples: ['2024-12-19T10:30:00Z'] - }) - })), + ), }; /** Token刷新成功响应数据类型 */ -export type RefreshSuccessType = Static; +export type RefreshSuccessType = Static<(typeof RefreshResponsesSchema)[200]>; // ========== 找回密码相关响应格式 ========== @@ -216,37 +224,39 @@ export type RefreshSuccessType = Static; * @description 用于Controller中定义所有可能的响应格式 */ export const ResetPasswordRequestResponsesSchema = { - 200: responseWrapperSchema(t.Object({ - /** 邮箱地址 */ - email: t.String({ - description: '发送重置邮件的邮箱地址', - examples: ['user@example.com', 'admin@company.com'] + 200: responseWrapperSchema( + t.Object({ + /** 邮箱地址 */ + email: t.String({ + description: '发送重置邮件的邮箱地址', + examples: ['user@example.com', 'admin@company.com'], + }), + /** 发送状态 */ + sent: t.Boolean({ + description: '邮件发送状态', + examples: [true], + }), + /** 发送时间 */ + sentAt: t.String({ + description: '邮件发送时间', + examples: ['2024-12-19T10:30:00Z'], + }), + /** 重置链接有效期(分钟) */ + expiresIn: t.Number({ + description: '重置链接有效期(分钟)', + examples: [30, 60], + }), + /** 提示信息 */ + message: t.String({ + description: '操作提示信息', + examples: ['重置邮件已发送,请查收邮箱'], + }), }), - /** 发送状态 */ - sent: t.Boolean({ - description: '邮件发送状态', - examples: [true] - }), - /** 发送时间 */ - sentAt: t.String({ - description: '邮件发送时间', - examples: ['2024-12-19T10:30:00Z'] - }), - /** 重置链接有效期(分钟) */ - expiresIn: t.Number({ - description: '重置链接有效期(分钟)', - examples: [30, 60] - }), - /** 提示信息 */ - message: t.String({ - description: '操作提示信息', - examples: ['重置邮件已发送,请查收邮箱'] - }) - })), + ), }; /** 找回密码成功响应数据类型 */ -export type ResetPasswordRequestSuccessType = Static; +export type ResetPasswordRequestSuccessType = Static<(typeof ResetPasswordRequestResponsesSchema)[200]>; // ========== 重置密码相关响应格式 ========== @@ -255,39 +265,41 @@ export type ResetPasswordRequestSuccessType = Static; \ No newline at end of file +export type ResetPasswordConfirmSuccessType = Static<(typeof ResetPasswordConfirmResponsesSchema)[200]>; diff --git a/src/modules/auth/auth.schema.ts b/src/modules/auth/auth.schema.ts index 8984b70..a880df5 100644 --- a/src/modules/auth/auth.schema.ts +++ b/src/modules/auth/auth.schema.ts @@ -19,34 +19,34 @@ export const RegisterSchema = t.Object({ minLength: 2, maxLength: 50, description: '用户名,2-50字符', - examples: ['root', 'testuser'] + examples: ['root', 'testuser'], }), /** 邮箱地址,对应sys_users.email */ email: t.String({ format: 'email', maxLength: 100, description: '邮箱地址', - examples: ['x71291@outlook.com'] + examples: ['x71291@outlook.com'], }), /** 密码,6-50字符 */ password: t.String({ minLength: 6, maxLength: 50, description: '密码,6-50字符', - examples: ['password123'] + examples: ['password123'], }), /** 图形验证码 */ captcha: t.String({ minLength: 4, maxLength: 6, description: '图形验证码', - examples: ['a1b2'] + examples: ['a1b2'], }), /** 验证码会话ID */ captchaId: t.String({ description: '验证码会话ID', - examples: ['cap'] - }) + examples: ['cap'], + }), }); /** @@ -59,8 +59,8 @@ export const ActivateSchema = t.Object({ minLength: 10, maxLength: 1000, description: '邮箱激活令牌,JWT格式,24小时有效', - examples: ['eyJhbGciOiJIUzI1NiI'] - }) + examples: ['eyJhbGciOiJIUzI1NiI'], + }), }); /** @@ -73,33 +73,39 @@ export const LoginSchema = t.Object({ minLength: 2, maxLength: 100, description: '用户名/邮箱地址,100字符', - examples: ['root', 'testuser', 'x71291@outlook.com'] + examples: ['root', 'testuser', 'x71291@outlook.com'], }), /** 图形验证码(可选) */ - captcha: t.Optional(t.String({ - minLength: 4, - maxLength: 6, - description: '图形验证码,登录失败次数过多时需要', - examples: ['a1b2'] - })), + captcha: t.Optional( + t.String({ + minLength: 4, + maxLength: 6, + description: '图形验证码,登录失败次数过多时需要', + examples: ['a1b2'], + }), + ), /** 密码,6-50字符 */ password: t.String({ minLength: 6, maxLength: 50, description: '密码,6-50字符', - examples: ['password123'] + examples: ['password123'], }), /** 验证码会话ID(可选) */ - captchaId: t.Optional(t.String({ - description: '验证码会话ID,与captcha配对使用', - examples: ['cap'] - })), + captchaId: t.Optional( + t.String({ + description: '验证码会话ID,与captcha配对使用', + examples: ['cap'], + }), + ), /** 是否记住登录状态 */ - rememberMe: t.Optional(t.Boolean({ - description: '是否记住登录状态,影响token过期时间', - examples: [true, false], - default: false - })) + rememberMe: t.Optional( + t.Boolean({ + description: '是否记住登录状态,影响token过期时间', + examples: [true, false], + default: false, + }), + ), }); /** @@ -112,8 +118,8 @@ export const RefreshSchema = t.Object({ minLength: 10, maxLength: 1000, description: '刷新令牌,JWT格式,用于获取新的访问令牌', - examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'] - }) + examples: ['eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'], + }), }); /** @@ -126,20 +132,20 @@ export const ResetPasswordRequestSchema = t.Object({ format: 'email', maxLength: 100, description: '注册时使用的邮箱地址', - examples: ['user@example.com', 'admin@company.com'] + examples: ['user@example.com', 'admin@company.com'], }), /** 图形验证码 */ captcha: t.String({ minLength: 4, maxLength: 6, description: '图形验证码', - examples: ['a1b2', '1234'] + examples: ['a1b2', '1234'], }), /** 验证码会话ID */ captchaId: t.String({ description: '验证码会话ID', - examples: ['cap_123', 'captcha_session'] - }) + examples: ['cap_123', 'captcha_session'], + }), }); /** @@ -152,22 +158,22 @@ export const ResetPasswordConfirmSchema = t.Object({ minLength: 10, maxLength: 1000, description: '重置密码令牌,JWT格式,30分钟有效', - examples: ['eyJhbGciOiJIUzI1NiI'] + examples: ['eyJhbGciOiJIUzI1NiI'], }), /** 新密码,6-50字符 */ newPassword: t.String({ minLength: 6, maxLength: 50, description: '新密码,6-50字符', - examples: ['newpassword123'] + examples: ['newpassword123'], }), /** 确认新密码,必须与新密码一致 */ confirmPassword: t.String({ minLength: 6, maxLength: 50, description: '确认新密码,必须与新密码一致', - examples: ['newpassword123'] - }) + examples: ['newpassword123'], + }), }); /** 用户注册请求类型 */ @@ -186,4 +192,4 @@ export type RefreshRequest = Static; export type ResetPasswordRequestRequest = Static; /** 重置密码请求类型 */ -export type ResetPasswordConfirmRequest = Static; \ No newline at end of file +export type ResetPasswordConfirmRequest = Static; diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 1bf4253..b173fab 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -5,7 +5,7 @@ * @lastEditor AI Assistant * @lastEditTime 2025-01-07 * @description 认证模块的业务逻辑实现,包括用户注册、邮箱激活、用户登录等 - * + * * 分布式锁使用策略: * 1. 短期操作(如登录、刷新token):使用短TTL,不开启自动续期 * 2. 长期操作(如注册、密码重置):使用较长TTL,开启自动续期 @@ -15,16 +15,30 @@ import bcrypt from 'bcrypt'; import { eq, sql } from 'drizzle-orm'; import { db } from '@/plugins/drizzle/drizzle.service'; -import { sysUsers } from '@/eneities'; +import { sysUsers } from '@/eneities/index'; import { captchaService } from '@/modules/captcha/captcha.service'; import { Logger } from '@/plugins/logger/logger.service'; import { nextId } from '@/utils/snowflake'; import { jwtService } from '@/plugins/jwt/jwt.service'; import { emailService } from '@/plugins/email/email.service'; import { DistributedLockService, LOCK_KEYS } from '@/utils/distributedLock'; -import type { RegisterRequest, ActivateRequest, LoginRequest, RefreshRequest, ResetPasswordRequestRequest, ResetPasswordConfirmRequest } from './auth.schema'; +import type { + RegisterRequest, + ActivateRequest, + LoginRequest, + RefreshRequest, + ResetPasswordRequestRequest, + ResetPasswordConfirmRequest, +} from './auth.schema'; import { successResponse, errorResponse, BusinessError } from '@/utils/responseFormate'; -import type { ActivateSuccessType, LoginSuccessType, RegisterResponsesType, RefreshSuccessType, ResetPasswordRequestSuccessType, ResetPasswordConfirmSuccessType } from './auth.response'; +import type { + ActivateSuccessType, + LoginSuccessType, + RegisterResponsesType, + RefreshSuccessType, + ResetPasswordRequestSuccessType, + ResetPasswordConfirmSuccessType, +} from './auth.response'; import { TOKEN_TYPES } from '@/type/jwt.type'; /** @@ -48,10 +62,10 @@ export class AuthService { // 获取分布式锁,防止并发注册(长期操作,开启自动续期) const lock = await DistributedLockService.acquire({ key: `${LOCK_KEYS.USER_REGISTER}:${username}:${email}`, - ttl: 60, // 注册可能需要较长时间(邮件发送等) + ttl: 60, // 注册可能需要较长时间(邮件发送等) timeout: 15000, autoRenew: true, - renewInterval: 20000 // 20秒续期一次 + renewInterval: 20000, // 20秒续期一次 }); try { @@ -71,7 +85,7 @@ export class AuthService { const newUser = await this.createUser({ username, email, - passwordHash + passwordHash, }); // 6. 发送激活邮件 @@ -79,14 +93,16 @@ export class AuthService { Logger.info(`用户注册成功:${newUser.id} - ${newUser.username}`); - return successResponse({ - id: newUser.id, - username: newUser.username, - email: newUser.email, - status: newUser.status, - createdAt: newUser.createdAt - }, '用户注册成功,请查收激活邮件'); - + return successResponse( + { + id: newUser.id, + username: newUser.username, + email: newUser.email, + status: newUser.status, + createdAt: newUser.createdAt, + }, + '用户注册成功,请查收激活邮件', + ); } finally { // 释放锁 await lock.release(); @@ -101,14 +117,11 @@ export class AuthService { private async validateCaptcha(captcha: string, captchaId: string): Promise { const result = await captchaService.verifyCaptcha({ captchaId, - captchaCode: captcha + captchaCode: captcha, }); if (!result.data?.valid) { - throw new BusinessError( - result.data?.message || '验证码验证失败', - 400 - ); + throw new BusinessError(result.data?.message || '验证码验证失败', 400); } } @@ -117,7 +130,8 @@ export class AuthService { * @param username 用户名 */ private async checkUsernameExists(username: string): Promise { - const existingUser = await db().select({ id: sysUsers.id }) + const existingUser = await db() + .select({ id: sysUsers.id }) .from(sysUsers) .where(eq(sysUsers.username, username)) .limit(1); @@ -132,7 +146,8 @@ export class AuthService { * @param email 邮箱地址 */ private async checkEmailExists(email: string): Promise { - const existingUser = await db().select({ id: sysUsers.id }) + const existingUser = await db() + .select({ id: sysUsers.id }) .from(sysUsers) .where(eq(sysUsers.email, email)) .limit(1); @@ -156,11 +171,7 @@ export class AuthService { * @param userData 用户数据 * @returns Promise 创建的用户信息 */ - private async createUser(userData: { - username: string; - email: string; - passwordHash: string; - }): Promise<{ + private async createUser(userData: { username: string; email: string; passwordHash: string }): Promise<{ id: string; username: string; email: string; @@ -177,17 +188,18 @@ export class AuthService { username, email, passwordHash, - status: 'pending' // 新注册用户状态为待激活 + status: 'pending', // 新注册用户状态为待激活 }); // 查询刚创建的用户信息 - const [newUser] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - status: sysUsers.status, - createdAt: sysUsers.createdAt - }) + const [newUser] = await db() + .select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + createdAt: sysUsers.createdAt, + }) .from(sysUsers) .where(eq(sysUsers.id, userId)) .limit(1); @@ -203,7 +215,7 @@ export class AuthService { username: newUser!.username, email: newUser!.email, status: newUser!.status, - createdAt: newUser!.createdAt + createdAt: newUser!.createdAt, }; } @@ -213,7 +225,7 @@ export class AuthService { * @returns Promise */ public async activate(request: ActivateRequest): Promise { - Logger.info(`邮箱激活请求开始处理`); + Logger.info('邮箱激活请求开始处理'); const { token } = request; @@ -228,7 +240,7 @@ export class AuthService { key: `${LOCK_KEYS.EMAIL_ACTIVATE}:${tokenPayload.userId}`, ttl: 30, timeout: 10000, - autoRenew: true + autoRenew: true, }); try { @@ -248,15 +260,17 @@ export class AuthService { Logger.info(`邮箱激活成功:${user.id} - ${user.username}`); - return successResponse({ - id: updatedUser.id, - username: updatedUser.username, - email: updatedUser.email, - status: updatedUser.status, - updatedAt: updatedUser.updatedAt, - activated: true - }, '邮箱激活成功'); - + return successResponse( + { + id: updatedUser.id, + username: updatedUser.username, + email: updatedUser.email, + status: updatedUser.status, + updatedAt: updatedUser.updatedAt, + activated: true, + }, + '邮箱激活成功', + ); } finally { // 释放锁 await lock.release(); @@ -276,14 +290,15 @@ export class AuthService { createdAt: string; updatedAt: string; }> { - const [user] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - status: sysUsers.status, - createdAt: sysUsers.createdAt, - updatedAt: sysUsers.updatedAt - }) + const [user] = await db() + .select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + createdAt: sysUsers.createdAt, + updatedAt: sysUsers.updatedAt, + }) .from(sysUsers) .where(eq(sysUsers.id, userId)) .limit(1); @@ -298,7 +313,7 @@ export class AuthService { email: user.email, status: user.status, createdAt: user.createdAt, - updatedAt: user.updatedAt + updatedAt: user.updatedAt, }; } @@ -308,7 +323,10 @@ export class AuthService { * @param status 新状态 * @returns Promise 更新后的用户信息 */ - private async updateUserStatus(userId: string, status: string): Promise<{ + private async updateUserStatus( + userId: string, + status: string, + ): Promise<{ id: string; username: string; email: string; @@ -316,20 +334,22 @@ export class AuthService { updatedAt: string; }> { // 更新用户状态 - await db().update(sysUsers) + await db() + .update(sysUsers) .set({ status: status, }) .where(eq(sysUsers.id, BigInt(userId))); // 查询更新后的用户信息 - const [updatedUser] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - status: sysUsers.status, - updatedAt: sysUsers.updatedAt - }) + const [updatedUser] = await db() + .select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + updatedAt: sysUsers.updatedAt, + }) .from(sysUsers) .where(eq(sysUsers.id, BigInt(userId))) .limit(1); @@ -343,7 +363,7 @@ export class AuthService { username: updatedUser!.username, email: updatedUser!.email, status: updatedUser!.status, - updatedAt: updatedUser!.updatedAt + updatedAt: updatedUser!.updatedAt, }; } @@ -361,9 +381,9 @@ export class AuthService { // 获取分布式锁,防止并发登录(短期操作,不开启自动续期) const lock = await DistributedLockService.acquire({ key: `${LOCK_KEYS.USER_LOGIN}:${identifier}`, - ttl: 15, // 登录操作通常很快 + ttl: 15, // 登录操作通常很快 timeout: 8000, - autoRenew: false // 短期操作不需要续期 + autoRenew: false, // 短期操作不需要续期 }); try { @@ -382,12 +402,15 @@ export class AuthService { this.checkAccountStatus(user); // 5. 生成JWT令牌 - const tokens = jwtService.generateTokens({ - id: user.id, - username: user.username, - email: user.email, - status: user.status - }, rememberMe); + const tokens = jwtService.generateTokens( + { + id: user.id, + username: user.username, + email: user.email, + status: user.status, + }, + rememberMe, + ); // 6. 更新最后登录时间 await this.updateLastLoginTime(user.id); @@ -397,17 +420,19 @@ export class AuthService { Logger.info(`用户登录成功:${user.id} - ${user.username}`); - return successResponse({ - user: { - id: user.id, - username: user.username, - email: user.email, - status: user.status, - lastLoginAt: user.lastLoginAt + return successResponse( + { + user: { + id: user.id, + username: user.username, + email: user.email, + status: user.status, + lastLoginAt: user.lastLoginAt, + }, + tokens, }, - tokens - }, '登录成功'); - + '登录成功', + ); } finally { // 释放锁 await lock.release(); @@ -431,18 +456,17 @@ export class AuthService { const isEmail = identifier.includes('@'); // 构建查询条件 - const whereCondition = isEmail - ? eq(sysUsers.email, identifier) - : eq(sysUsers.username, identifier); + const whereCondition = isEmail ? eq(sysUsers.email, identifier) : eq(sysUsers.username, identifier); - const [user] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - status: sysUsers.status, - passwordHash: sysUsers.passwordHash, - lastLoginAt: sysUsers.lastLoginAt - }) + const [user] = await db() + .select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + passwordHash: sysUsers.passwordHash, + lastLoginAt: sysUsers.lastLoginAt, + }) .from(sysUsers) .where(whereCondition) .limit(1); @@ -457,7 +481,7 @@ export class AuthService { email: user.email, status: user.status, passwordHash: user.passwordHash, - lastLoginAt: user.lastLoginAt + lastLoginAt: user.lastLoginAt, }; } @@ -496,10 +520,11 @@ export class AuthService { * @param userId 用户ID */ private async updateLastLoginTime(userId: string): Promise { - await db().update(sysUsers) + await db() + .update(sysUsers) .set({ lastLoginAt: sql`NOW()`, // 使用 MySQL 的 NOW() 函数 - loginCount: sql`${sysUsers.loginCount} + 1` + loginCount: sql`${sysUsers.loginCount} + 1`, }) .where(eq(sysUsers.id, BigInt(userId))); } @@ -529,10 +554,9 @@ export class AuthService {

恭喜您,${username}!

您的账号已成功激活,现在可以正常使用所有功能。

感谢您的使用!

- ` + `, }); // Logger.info(`激活成功邮件发送成功:${email}`); - } catch (error) { // 邮件发送失败不影响激活流程,只记录日志 Logger.warn(`激活成功邮件发送失败:${email}, 错误:${error}`); @@ -561,7 +585,7 @@ export class AuthService { 激活账号

此链接24小时内有效,请尽快完成激活。

如果您没有注册,请忽略此邮件。

- ` + `, }); Logger.info(`激活邮件发送成功:${email}`); } catch (error) { @@ -576,16 +600,16 @@ export class AuthService { * @returns Promise */ public async refresh(request: RefreshRequest): Promise { - Logger.info(`Token刷新请求开始处理`); + Logger.info('Token刷新请求开始处理'); const { refreshToken } = request; // 获取分布式锁,防止并发刷新(短期操作,不开启自动续期) const lock = await DistributedLockService.acquire({ key: `${LOCK_KEYS.TOKEN_REFRESH}:${refreshToken}`, - ttl: 10, // Token刷新操作很快 + ttl: 10, // Token刷新操作很快 timeout: 5000, - autoRenew: false // 短期操作不需要续期 + autoRenew: false, // 短期操作不需要续期 }); try { @@ -609,16 +633,18 @@ export class AuthService { id: user.id, username: user.username, email: user.email, - status: user.status + status: user.status, }); // 5. 记录刷新日志 await this.recordRefreshLog(user.id); - return successResponse({ - tokens - }, 'Token刷新成功'); - + return successResponse( + { + tokens, + }, + 'Token刷新成功', + ); } finally { // 释放锁 await lock.release(); @@ -651,7 +677,7 @@ export class AuthService { key: `${LOCK_KEYS.PASSWORD_RESET}:${email}`, ttl: 30, timeout: 10000, - autoRenew: true + autoRenew: true, }); try { @@ -672,14 +698,16 @@ export class AuthService { Logger.info(`找回密码邮件发送成功:${user.id} - ${user.email}`); - return successResponse({ - email: user.email, - sent: true, - sentAt: new Date().toISOString(), - expiresIn: 30, // 30分钟有效期 - message: '重置邮件已发送,请查收邮箱' - }, '重置邮件已发送,请查收邮箱'); - + return successResponse( + { + email: user.email, + sent: true, + sentAt: new Date().toISOString(), + expiresIn: 30, // 30分钟有效期 + message: '重置邮件已发送,请查收邮箱', + }, + '重置邮件已发送,请查收邮箱', + ); } finally { // 释放锁 await lock.release(); @@ -698,12 +726,13 @@ export class AuthService { email: string; status: string; }> { - const [user] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - status: sysUsers.status - }) + const [user] = await db() + .select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + status: sysUsers.status, + }) .from(sysUsers) .where(eq(sysUsers.email, email)) .limit(1); @@ -716,7 +745,7 @@ export class AuthService { id: user.id!.toString(), username: user.username, email: user.email, - status: user.status + status: user.status, }; } @@ -754,7 +783,7 @@ export class AuthService {

如果您有任何问题,请联系我们的客服团队。

谢谢!
星撰系统团队

- ` + `, }; await emailService.sendEmail(emailContent); @@ -787,7 +816,7 @@ export class AuthService { key: `${LOCK_KEYS.PASSWORD_RESET}:${tokenPayload.userId}`, ttl: 30, timeout: 10000, - autoRenew: true + autoRenew: true, }); try { @@ -808,15 +837,17 @@ export class AuthService { Logger.info(`密码重置成功:${user.id} - ${user.username}`); - return successResponse({ - id: updatedUser.id, - username: updatedUser.username, - email: updatedUser.email, - updatedAt: updatedUser.updatedAt, - reset: true, - message: '密码重置成功,请使用新密码登录' - }, '密码重置成功,请使用新密码登录'); - + return successResponse( + { + id: updatedUser.id, + username: updatedUser.username, + email: updatedUser.email, + updatedAt: updatedUser.updatedAt, + reset: true, + message: '密码重置成功,请使用新密码登录', + }, + '密码重置成功,请使用新密码登录', + ); } finally { // 释放锁 await lock.release(); @@ -829,25 +860,30 @@ export class AuthService { * @param newPasswordHash 新密码哈希 * @returns Promise<更新后的用户信息> */ - private async updateUserPassword(userId: string, newPasswordHash: string): Promise<{ + private async updateUserPassword( + userId: string, + newPasswordHash: string, + ): Promise<{ id: string; username: string; email: string; updatedAt: string; }> { - await db().update(sysUsers) + await db() + .update(sysUsers) .set({ passwordHash: newPasswordHash, }) .where(eq(sysUsers.id, BigInt(userId))); // 查询更新后的用户信息 - const [updatedUser] = await db().select({ - id: sysUsers.id, - username: sysUsers.username, - email: sysUsers.email, - updatedAt: sysUsers.updatedAt - }) + const [updatedUser] = await db() + .select({ + id: sysUsers.id, + username: sysUsers.username, + email: sysUsers.email, + updatedAt: sysUsers.updatedAt, + }) .from(sysUsers) .where(eq(sysUsers.id, BigInt(userId))) .limit(1); @@ -860,7 +896,7 @@ export class AuthService { id: updatedUser.id!.toString(), username: updatedUser.username, email: updatedUser.email, - updatedAt: updatedUser.updatedAt + updatedAt: updatedUser.updatedAt, }; } @@ -896,7 +932,7 @@ export class AuthService {

如果您有任何问题,请联系我们的客服团队。

谢谢!
星撰系统团队

- ` + `, }; await emailService.sendEmail(emailContent); @@ -905,4 +941,4 @@ export class AuthService { } // 导出单例实例 -export const authService = new AuthService(); \ No newline at end of file +export const authService = new AuthService(); diff --git a/src/modules/auth/auth.test.md b/src/modules/auth/auth.test.md index 242ec6f..8fa8dea 100644 --- a/src/modules/auth/auth.test.md +++ b/src/modules/auth/auth.test.md @@ -18,36 +18,40 @@ #### 1.1 正常流程测试 **测试用例**: 成功注册新用户 + - **请求参数**: - ```json - { - "username": "testuser", - "email": "test@example.com", - "password": "password123", - "captcha": "a1b2", - "captchaId": "test_captcha_id" - } - ``` + ```json + { + "username": "testuser", + "email": "test@example.com", + "password": "password123", + "captcha": "a1b2", + "captchaId": "test_captcha_id" + } + ``` - **预期响应**: 200 OK - **验证点**: - - 用户信息正确创建 - - 密码已加密存储 - - 激活邮件已发送 - - 用户状态为pending + - 用户信息正确创建 + - 密码已加密存储 + - 激活邮件已发送 + - 用户状态为pending #### 1.2 异常流程测试 **测试用例**: 用户名已存在 + - **请求参数**: 使用已存在的用户名 - **预期响应**: 400 Bad Request - **错误信息**: "用户名已存在" **测试用例**: 邮箱已被注册 + - **请求参数**: 使用已注册的邮箱 - **预期响应**: 400 Bad Request - **错误信息**: "邮箱已被注册" **测试用例**: 验证码错误 + - **请求参数**: 错误的验证码 - **预期响应**: 400 Bad Request - **错误信息**: "验证码验证失败" @@ -57,20 +61,22 @@ #### 2.1 正常流程测试 **测试用例**: 成功激活用户邮箱 + - **请求参数**: - ```json - { - "token": "valid_activation_token" - } - ``` + ```json + { + "token": "valid_activation_token" + } + ``` - **预期响应**: 200 OK - **验证点**: - - 用户状态更新为active - - 激活时间正确记录 + - 用户状态更新为active + - 激活时间正确记录 #### 2.2 异常流程测试 **测试用例**: 无效的激活令牌 + - **请求参数**: 无效或过期的令牌 - **预期响应**: 400 Bad Request - **错误信息**: "激活令牌无效或已过期" @@ -80,42 +86,47 @@ #### 3.1 正常流程测试 **测试用例**: 用户名登录成功 + - **请求参数**: - ```json - { - "identifier": "testuser", - "password": "password123" - } - ``` + ```json + { + "identifier": "testuser", + "password": "password123" + } + ``` - **预期响应**: 200 OK - **验证点**: - - 返回访问令牌和刷新令牌 - - 最后登录时间更新 - - 登录日志记录 + - 返回访问令牌和刷新令牌 + - 最后登录时间更新 + - 登录日志记录 **测试用例**: 邮箱登录成功 + - **请求参数**: - ```json - { - "identifier": "test@example.com", - "password": "password123" - } - ``` + ```json + { + "identifier": "test@example.com", + "password": "password123" + } + ``` - **预期响应**: 200 OK #### 3.2 异常流程测试 **测试用例**: 用户名不存在 + - **请求参数**: 不存在的用户名 - **预期响应**: 404 Not Found - **错误信息**: "用户不存在" **测试用例**: 密码错误 + - **请求参数**: 错误的密码 - **预期响应**: 401 Unauthorized - **错误信息**: "用户名或密码错误" **测试用例**: 账号未激活 + - **请求参数**: 未激活用户的凭据 - **预期响应**: 403 Forbidden - **错误信息**: "账号未激活,请先激活邮箱" @@ -125,20 +136,22 @@ #### 4.1 正常流程测试 **测试用例**: 成功刷新令牌 + - **请求参数**: - ```json - { - "refreshToken": "valid_refresh_token" - } - ``` + ```json + { + "refreshToken": "valid_refresh_token" + } + ``` - **预期响应**: 200 OK - **验证点**: - - 返回新的访问令牌和刷新令牌 - - 刷新日志记录 + - 返回新的访问令牌和刷新令牌 + - 刷新日志记录 #### 4.2 异常流程测试 **测试用例**: 无效的刷新令牌 + - **请求参数**: 无效或过期的刷新令牌 - **预期响应**: 401 Unauthorized - **错误信息**: "刷新令牌无效或已过期" @@ -148,33 +161,37 @@ #### 5.1 正常流程测试 **测试用例**: 成功发送重置邮件 + - **请求参数**: - ```json - { - "email": "test@example.com", - "captcha": "a1b2", - "captchaId": "test_captcha_id" - } - ``` + ```json + { + "email": "test@example.com", + "captcha": "a1b2", + "captchaId": "test_captcha_id" + } + ``` - **预期响应**: 200 OK - **验证点**: - - 重置邮件已发送 - - 重置令牌已生成 - - 返回发送状态和时间 + - 重置邮件已发送 + - 重置令牌已生成 + - 返回发送状态和时间 #### 5.2 异常流程测试 **测试用例**: 邮箱未注册 + - **请求参数**: 未注册的邮箱地址 - **预期响应**: 404 Not Found - **错误信息**: "该邮箱未注册" **测试用例**: 验证码错误 + - **请求参数**: 错误的验证码 - **预期响应**: 400 Bad Request - **错误信息**: "验证码验证失败" **测试用例**: 账号未激活 + - **请求参数**: 未激活用户的邮箱 - **预期响应**: 403 Forbidden - **错误信息**: "账号未激活,请先激活邮箱" @@ -184,39 +201,44 @@ #### 6.1 正常流程测试 **测试用例**: 成功重置密码 + - **请求参数**: - ```json - { - "token": "valid_reset_token", - "newPassword": "newpassword123", - "confirmPassword": "newpassword123" - } - ``` + ```json + { + "token": "valid_reset_token", + "newPassword": "newpassword123", + "confirmPassword": "newpassword123" + } + ``` - **预期响应**: 200 OK - **验证点**: - - 密码已更新 - - 重置令牌已失效 - - 成功邮件已发送 - - 返回用户基本信息 + - 密码已更新 + - 重置令牌已失效 + - 成功邮件已发送 + - 返回用户基本信息 #### 6.2 异常流程测试 **测试用例**: 重置令牌无效 + - **请求参数**: 无效或过期的重置令牌 - **预期响应**: 400 Bad Request - **错误信息**: "重置令牌无效或已过期" **测试用例**: 密码不一致 + - **请求参数**: 新密码和确认密码不一致 - **预期响应**: 400 Bad Request - **错误信息**: "两次输入的密码不一致" **测试用例**: 密码长度不足 + - **请求参数**: 新密码少于6字符 - **预期响应**: 400 Bad Request - **错误信息**: "密码长度不符合要求" **测试用例**: 账号未激活 + - **请求参数**: 未激活用户的重置令牌 - **预期响应**: 403 Forbidden - **错误信息**: "账号未激活,请先激活邮箱" @@ -228,15 +250,18 @@ ### 1. 输入验证边界 **测试用例**: 用户名长度边界 + - 最小长度: 2字符 - 最大长度: 50字符 - 超出范围应返回400错误 **测试用例**: 邮箱格式验证 + - 有效邮箱格式应通过验证 - 无效邮箱格式应返回400错误 **测试用例**: 密码强度要求 + - 最小长度: 6字符 - 最大长度: 50字符 - 超出范围应返回400错误 @@ -244,20 +269,24 @@ ### 2. 并发测试 **测试用例**: 并发注册 + - 同时使用相同用户名注册 - 应只有一个成功,其他失败 **测试用例**: 并发登录 + - 同一用户同时登录 - 应都能成功,但刷新令牌会失效 ### 3. 性能测试 **测试用例**: 大量用户注册 + - 测试系统在高并发下的表现 - 验证数据库连接池和缓存性能 **测试用例**: 邮件发送性能 + - 测试邮件服务的并发处理能力 - 验证邮件队列机制 @@ -266,30 +295,36 @@ ### 1. 密码安全 **测试用例**: 密码加密存储 + - 验证密码是否使用bcrypt加密 - 确认原始密码不在数据库中 **测试用例**: 密码强度验证 + - 测试弱密码的拒绝机制 - 验证密码复杂度要求 ### 2. 令牌安全 **测试用例**: JWT令牌验证 + - 验证令牌签名和过期时间 - 测试令牌篡改检测 **测试用例**: 令牌刷新安全 + - 验证刷新令牌的一次性使用 - 测试令牌泄露防护 ### 3. 输入安全 **测试用例**: SQL注入防护 + - 测试特殊字符输入 - 验证参数化查询 **测试用例**: XSS防护 + - 测试恶意脚本输入 - 验证输出转义 @@ -359,4 +394,4 @@ bun test src/modules/auth/auth.test.ts --reporter=verbose 2. **环境变量**: 测试环境应使用专门的配置 3. **异步操作**: 邮件发送等异步操作需要适当的等待时间 4. **资源清理**: 测试完成后应清理所有测试数据 -5. **错误处理**: 测试应覆盖各种错误情况 \ No newline at end of file +5. **错误处理**: 测试应覆盖各种错误情况 diff --git a/src/modules/auth/auth.test.ts b/src/modules/auth/auth.test.ts index eed2441..f4d7d2e 100644 --- a/src/modules/auth/auth.test.ts +++ b/src/modules/auth/auth.test.ts @@ -40,11 +40,9 @@ describe('认证模块测试', () => { // 清理测试数据和关闭连接 try { // 删除测试用户 - await drizzleService.db.delete(sysUsers) - .where(eq(sysUsers.username, 'testuser')); - await drizzleService.db.delete(sysUsers) - .where(eq(sysUsers.email, 'test@example.com')); - + await drizzleService.db.delete(sysUsers).where(eq(sysUsers.username, 'testuser')); + await drizzleService.db.delete(sysUsers).where(eq(sysUsers.email, 'test@example.com')); + await redisService.close(); await drizzleService.close(); } catch (error) { @@ -55,23 +53,24 @@ describe('认证模块测试', () => { beforeEach(async () => { // 每个测试前生成新的验证码 try { - const captchaResponse = await testApp - .handle(new Request('http://localhost/api/captcha/generate', { + const captchaResponse = await testApp.handle( + new Request('http://localhost/api/captcha/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'image', length: 4, - expireTime: 300 + expireTime: 300, }), - })); + }), + ); if (captchaResponse.status === 200) { - const captchaResult = await captchaResponse.json() as any; + const captchaResult = (await captchaResponse.json()) as any; captchaId = captchaResult.data.id; // 模拟已知验证码(在实际测试中可能需要直接从Redis获取) validCaptchaCode = 'TEST'; - + // 直接在Redis中设置已知的验证码用于测试 await redisService.setex( `captcha:${captchaId}`, @@ -82,8 +81,8 @@ describe('认证模块测试', () => { type: 'image', image: 'test-image-data', expireTime: Date.now() + 300000, - createdAt: Date.now() - }) + createdAt: Date.now(), + }), ); } } catch (error) { @@ -100,18 +99,19 @@ describe('认证模块测试', () => { email: 'test@example.com', password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(200); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('SUCCESS'); expect(result.message).toBe('用户注册成功'); expect(result.data).toBeDefined(); @@ -129,34 +129,36 @@ describe('认证模块测试', () => { email: 'existing@example.com', password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - await testApp - .handle(new Request('http://localhost/api/auth/register', { + await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(firstPayload), - })); + }), + ); // 重新生成验证码 try { - const captchaResponse = await testApp - .handle(new Request('http://localhost/api/captcha/generate', { + const captchaResponse = await testApp.handle( + new Request('http://localhost/api/captcha/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'image', length: 4, - expireTime: 300 + expireTime: 300, }), - })); + }), + ); if (captchaResponse.status === 200) { - const captchaResult = await captchaResponse.json() as any; + const captchaResult = (await captchaResponse.json()) as any; captchaId = captchaResult.data.id; validCaptchaCode = 'TEST'; - + // 直接在Redis中设置已知的验证码用于测试 await redisService.setex( `captcha:${captchaId}`, @@ -167,8 +169,8 @@ describe('认证模块测试', () => { type: 'image', image: 'test-image-data', expireTime: Date.now() + 300000, - createdAt: Date.now() - }) + createdAt: Date.now(), + }), ); } } catch (error) { @@ -182,18 +184,19 @@ describe('认证模块测试', () => { email: 'different@example.com', password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(duplicatePayload), - })); + }), + ); expect(response.status).toBe(400); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('USERNAME_EXISTS'); expect(result.message).toBe('用户名已存在'); expect(result.data).toBeNull(); @@ -205,18 +208,19 @@ describe('认证模块测试', () => { email: 'test@example.com', // 使用之前注册的邮箱 password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(duplicateEmailPayload), - })); + }), + ); expect(response.status).toBe(400); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('EMAIL_EXISTS'); expect(result.message).toBe('邮箱已被注册'); expect(result.data).toBeNull(); @@ -228,18 +232,19 @@ describe('认证模块测试', () => { email: 'newuser2@example.com', password: 'password123', captcha: 'WRONG', - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('CAPTCHA_ERROR'); expect(result.message).toContain('验证码'); expect(result.data).toBeNull(); @@ -251,18 +256,19 @@ describe('认证模块测试', () => { email: 'newuser3@example.com', password: 'password123', captcha: 'TEST', - captchaId: 'nonexistent-captcha-id' + captchaId: 'nonexistent-captcha-id', }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('CAPTCHA_ERROR'); expect(result.data).toBeNull(); }); @@ -275,15 +281,16 @@ describe('认证模块测试', () => { email: 'test@example.com', password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); @@ -294,15 +301,16 @@ describe('认证模块测试', () => { email: 'test@example.com', password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); @@ -313,15 +321,16 @@ describe('认证模块测试', () => { email: 'invalid-email', // 无效邮箱格式 password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); @@ -332,15 +341,16 @@ describe('认证模块测试', () => { email: 'test@example.com', password: '12345', // 5个字符,小于最小长度6 captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); @@ -351,15 +361,16 @@ describe('认证模块测试', () => { email: 'test@example.com', password: 'password123', captcha: '123', // 3个字符,小于最小长度4 - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); @@ -370,15 +381,16 @@ describe('认证模块测试', () => { // 缺少 email password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); @@ -391,15 +403,16 @@ describe('认证模块测试', () => { email: 'min2@example.com', password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(200); }); @@ -410,15 +423,16 @@ describe('认证模块测试', () => { email: 'max2@example.com', password: 'password123', captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(200); }); @@ -429,15 +443,16 @@ describe('认证模块测试', () => { email: 'minpass2@example.com', password: '123456', // 6个字符,正好等于最小长度 captcha: validCaptchaCode, - captchaId: captchaId + captchaId: captchaId, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/register', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(200); }); @@ -454,7 +469,7 @@ describe('认证模块测试', () => { testUserId = '1234567890123456789'; // 模拟bigint ID字符串 testUserEmail = 'activate@example.com'; testUsername = 'activateuser'; - + // 模拟有效的激活Token载荷(实际应该是JWT签名) validActivationToken = JSON.stringify({ userId: testUserId, @@ -469,15 +484,16 @@ describe('认证模块测试', () => { iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 86400, // 24小时后过期 }); - + // 创建一个pending状态的测试用户 try { // 先检查用户是否存在 - const existingUser = await drizzleService.db.select({ id: sysUsers.id }) + const existingUser = await drizzleService.db + .select({ id: sysUsers.id }) .from(sysUsers) .where(eq(sysUsers.id, BigInt(testUserId))) .limit(1); - + if (existingUser.length === 0) { await drizzleService.db.insert(sysUsers).values({ id: BigInt(testUserId), @@ -495,8 +511,7 @@ describe('认证模块测试', () => { afterEach(async () => { // 清理测试用户 try { - await drizzleService.db.delete(sysUsers) - .where(eq(sysUsers.id, BigInt(testUserId))); + await drizzleService.db.delete(sysUsers).where(eq(sysUsers.id, BigInt(testUserId))); } catch (error) { // 忽略清理错误 } @@ -504,18 +519,19 @@ describe('认证模块测试', () => { it('应该成功激活用户邮箱', async () => { const payload: ActivateRequest = { - token: validActivationToken + token: validActivationToken, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(200); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('SUCCESS'); expect(result.message).toBe('邮箱激活成功'); expect(result.data).toBeDefined(); @@ -529,18 +545,19 @@ describe('认证模块测试', () => { it('Token格式无效应返回400错误', async () => { const payload: ActivateRequest = { - token: 'invalid-token-format' + token: 'invalid-token-format', }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('INVALID_ACTIVATION_TOKEN'); expect(result.message).toContain('令牌'); expect(result.data).toBeNull(); @@ -548,18 +565,19 @@ describe('认证模块测试', () => { it('Token为空应返回400错误', async () => { const payload: ActivateRequest = { - token: '' + token: '', }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('INVALID_ACTIVATION_TOKEN'); expect(result.data).toBeNull(); }); @@ -568,22 +586,23 @@ describe('认证模块测试', () => { const invalidToken = JSON.stringify({ // 缺少必要字段 userId: testUserId, - tokenType: 'wrong-type' + tokenType: 'wrong-type', }); const payload: ActivateRequest = { - token: invalidToken + token: invalidToken, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('INVALID_ACTIVATION_TOKEN'); expect(result.data).toBeNull(); }); @@ -604,18 +623,19 @@ describe('认证模块测试', () => { }); const payload: ActivateRequest = { - token: expiredToken + token: expiredToken, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('INVALID_ACTIVATION_TOKEN'); expect(result.message).toContain('过期'); expect(result.data).toBeNull(); @@ -637,18 +657,19 @@ describe('认证模块测试', () => { }); const payload: ActivateRequest = { - token: nonExistentUserToken + token: nonExistentUserToken, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(404); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('USER_NOT_FOUND'); expect(result.message).toBe('用户不存在'); expect(result.data).toBeNull(); @@ -656,29 +677,32 @@ describe('认证模块测试', () => { it('账号已激活应返回409错误', async () => { // 先激活用户 - await drizzleService.db.update(sysUsers) + await drizzleService.db + .update(sysUsers) .set({ status: 'active' }) .where(eq(sysUsers.id, BigInt(testUserId))); const payload: ActivateRequest = { - token: validActivationToken + token: validActivationToken, }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(409); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('ALREADY_ACTIVATED'); expect(result.message).toBe('账号已经激活'); expect(result.data).toBeNull(); // 恢复为pending状态,便于其他测试 - await drizzleService.db.update(sysUsers) + await drizzleService.db + .update(sysUsers) .set({ status: 'pending' }) .where(eq(sysUsers.id, BigInt(testUserId))); }); @@ -686,44 +710,47 @@ describe('认证模块测试', () => { it('缺少Token参数应返回400错误', async () => { const payload = {}; // 缺少token字段 - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); it('Token长度过短应返回400错误', async () => { const payload: ActivateRequest = { - token: 'short' // 长度小于10 + token: 'short', // 长度小于10 }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); it('Token长度过长应返回400错误', async () => { const payload: ActivateRequest = { - token: 'a'.repeat(1001) // 长度超过1000 + token: 'a'.repeat(1001), // 长度超过1000 }; - const response = await testApp - .handle(new Request('http://localhost/api/auth/activate', { + const response = await testApp.handle( + new Request('http://localhost/api/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); }); -}); \ No newline at end of file +}); diff --git a/src/modules/captcha/captcha.controller.ts b/src/modules/captcha/captcha.controller.ts index 38797ad..b221ccb 100644 --- a/src/modules/captcha/captcha.controller.ts +++ b/src/modules/captcha/captcha.controller.ts @@ -16,51 +16,39 @@ export const captchaController = new Elysia() * 生成验证码 * @route POST /api/captcha/generate */ - .post( - '/generate', - ({ body }) => captchaService.generateCaptcha(body), - { - body: GenerateCaptchaSchema, - detail: { - summary: '生成验证码', - description: '生成图形验证码,支持自定义尺寸和过期时间', - tags: [tags.captcha], - }, - response: {200: responseWrapperSchema(t.Any())}, - } - ) - + .post('/generate', ({ body }) => captchaService.generateCaptcha(body), { + body: GenerateCaptchaSchema, + detail: { + summary: '生成验证码', + description: '生成图形验证码,支持自定义尺寸和过期时间', + tags: [tags.captcha], + }, + response: { 200: responseWrapperSchema(t.Any()) }, + }) + /** * 验证验证码 * @route POST /api/captcha/verify */ - .post( - '/verify', - ({ body }) => captchaService.verifyCaptcha(body), - { - body: VerifyCaptchaSchema, - detail: { - summary: '验证验证码', - description: '验证用户输入的验证码是否正确', - tags: [tags.captcha], - }, - response: {200: responseWrapperSchema(t.Any())}, - } - ) - + .post('/verify', ({ body }) => captchaService.verifyCaptcha(body), { + body: VerifyCaptchaSchema, + detail: { + summary: '验证验证码', + description: '验证用户输入的验证码是否正确', + tags: [tags.captcha], + }, + response: { 200: responseWrapperSchema(t.Any()) }, + }) + /** * 清理过期验证码(管理接口) * @route POST /api/captcha/cleanup */ - .post( - '/cleanup', - () => captchaService.cleanupExpiredCaptchas(), - { - detail: { - summary: '清理过期验证码', - description: '清理Redis中已过期的验证码数据', - tags: [tags.captcha], - }, - response: {200: responseWrapperSchema(t.Any())}, - } - ); \ No newline at end of file + .post('/cleanup', () => captchaService.cleanupExpiredCaptchas(), { + detail: { + summary: '清理过期验证码', + description: '清理Redis中已过期的验证码数据', + tags: [tags.captcha], + }, + response: { 200: responseWrapperSchema(t.Any()) }, + }); diff --git a/src/modules/captcha/captcha.schema.ts b/src/modules/captcha/captcha.schema.ts index b671e08..8aae080 100644 --- a/src/modules/captcha/captcha.schema.ts +++ b/src/modules/captcha/captcha.schema.ts @@ -11,64 +11,72 @@ import { t, type Static } from 'elysia'; * 生成验证码请求Schema */ export const GenerateCaptchaSchema = t.Object({ - type: t.Optional(t.Union([ - t.Literal('image'), - t.Literal('sms'), - t.Literal('email') - ], { - description: '验证码类型', - examples: ['image', 'sms', 'email'], - default: 'image' - })), - width: t.Optional(t.Number({ - minimum: 100, - maximum: 400, - description: '验证码图片宽度', - examples: [200], - default: 200 - })), - height: t.Optional(t.Number({ - minimum: 40, - maximum: 100, - description: '验证码图片高度', - examples: [60], - default: 60 - })), - length: t.Optional(t.Number({ - minimum: 4, - maximum: 8, - description: '验证码长度', - examples: [4], - default: 4 - })), - expireTime: t.Optional(t.Number({ - minimum: 60, - maximum: 1800, - description: '验证码过期时间(秒)', - examples: [300], - default: 300 - })) + type: t.Optional( + t.Union([t.Literal('image'), t.Literal('sms'), t.Literal('email')], { + description: '验证码类型', + examples: ['image', 'sms', 'email'], + default: 'image', + }), + ), + width: t.Optional( + t.Number({ + minimum: 100, + maximum: 400, + description: '验证码图片宽度', + examples: [200], + default: 200, + }), + ), + height: t.Optional( + t.Number({ + minimum: 40, + maximum: 100, + description: '验证码图片高度', + examples: [60], + default: 60, + }), + ), + length: t.Optional( + t.Number({ + minimum: 4, + maximum: 8, + description: '验证码长度', + examples: [4], + default: 4, + }), + ), + expireTime: t.Optional( + t.Number({ + minimum: 60, + maximum: 1800, + description: '验证码过期时间(秒)', + examples: [300], + default: 300, + }), + ), }); /** * 验证验证码请求Schema */ export const VerifyCaptchaSchema = t.Object({ - captchaId: t.String({ + captchaId: t.String({ minLength: 1, description: '验证码ID', - examples: ['captcha_1234567890'] + examples: ['captcha_1234567890'], }), - captchaCode: t.String({ + captchaCode: t.String({ minLength: 4, maxLength: 8, description: '用户输入的验证码', - examples: ['1234'] + examples: ['1234'], }), - scene: t.Optional(t.String({ - description: '验证场景', - examples: ['login', 'register', 'reset_password'] - })) + scene: t.Optional( + t.String({ + description: '验证场景', + examples: ['login', 'register', 'reset_password'], + }), + ), }); /** @@ -81,7 +89,7 @@ export const CaptchaDataSchema = t.Object({ image: t.Optional(t.String({ description: 'Base64图片数据' })), expireTime: t.Number({ description: '过期时间戳' }), scene: t.Optional(t.String({ description: '验证场景' })), - createdAt: t.Number({ description: '创建时间戳' }) + createdAt: t.Number({ description: '创建时间戳' }), }); /** @@ -91,11 +99,11 @@ export const CaptchaGenerateResponseSchema = t.Object({ id: t.String({ description: '验证码ID' }), image: t.String({ description: 'Base64编码的验证码图片' }), expireTime: t.Number({ description: '过期时间戳' }), - type: t.String({ description: '验证码类型' }) + type: t.String({ description: '验证码类型' }), }); // 导出TypeScript类型 export type GenerateCaptchaRequest = Static; export type VerifyCaptchaRequest = Static; export type CaptchaData = Static; -export type CaptchaGenerateResponse = Static; \ No newline at end of file +export type CaptchaGenerateResponse = Static; diff --git a/src/modules/captcha/captcha.service.ts b/src/modules/captcha/captcha.service.ts index 79c55c8..879594c 100644 --- a/src/modules/captcha/captcha.service.ts +++ b/src/modules/captcha/captcha.service.ts @@ -11,7 +11,7 @@ import type { GenerateCaptchaRequest, VerifyCaptchaRequest, CaptchaData, - CaptchaGenerateResponse + CaptchaGenerateResponse, } from './captcha.schema'; import { Logger } from '@/plugins/logger/logger.service'; import { redisService } from '@/plugins/redis/redis.service'; @@ -24,13 +24,7 @@ export class CaptchaService { * @returns Promise */ async generateCaptcha(body: GenerateCaptchaRequest) { - const { - type = 'image', - width = 200, - height = 60, - length = 4, - expireTime = 300 - } = body; + const { type = 'image', width = 200, height = 60, length = 4, expireTime = 300 } = body; // 生成验证码ID const captchaId = `captcha_${randomBytes(16).toString('hex')}`; @@ -39,7 +33,7 @@ export class CaptchaService { const code = this.generateRandomCode(length); // 计算过期时间 - const expireTimestamp = Date.now() + (expireTime * 1000); + const expireTimestamp = Date.now() + expireTime * 1000; let imageData: string | undefined; @@ -55,7 +49,7 @@ export class CaptchaService { type, image: imageData, expireTime: expireTimestamp, - createdAt: Date.now() + createdAt: Date.now(), }; // 存储到Redis @@ -69,7 +63,7 @@ export class CaptchaService { id: captchaId, image: imageData || '', expireTime: expireTimestamp, - type + type, }; return successResponse(responseData); @@ -106,9 +100,7 @@ export class CaptchaService { // 验证成功后删除验证码,防止重复使用 await redisService.del(redisKey); Logger.info(`验证码验证成功:${captchaId}`); - return successResponse( - { valid: true }, '验证码验证成功' - ); + return successResponse({ valid: true }, '验证码验证成功'); } else { throw new BusinessError('验证码错误', 400); } @@ -214,11 +206,9 @@ export class CaptchaService { } Logger.info(`清理过期验证码完成,共清理 ${cleanedCount} 个`); - return successResponse( - { cleanedCount }, '清理完成' - ); + return successResponse({ cleanedCount }, '清理完成'); } } // 导出单例实例 -export const captchaService = new CaptchaService(); \ No newline at end of file +export const captchaService = new CaptchaService(); diff --git a/src/modules/captcha/captcha.test.ts b/src/modules/captcha/captcha.test.ts index 045aa9c..e5b44a9 100644 --- a/src/modules/captcha/captcha.test.ts +++ b/src/modules/captcha/captcha.test.ts @@ -45,18 +45,19 @@ describe('Captcha API', () => { width: 200, height: 60, length: 4, - expireTime: 300 + expireTime: 300, }; - const response = await app - .handle(new Request('http://localhost/api/captcha/generate', { + const response = await app.handle( + new Request('http://localhost/api/captcha/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(200); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('SUCCESS'); expect(result.data.id).toBeDefined(); expect(result.data.image).toBeDefined(); @@ -71,15 +72,16 @@ describe('Captcha API', () => { it('应该使用默认参数生成验证码', async () => { const payload = {}; - const response = await app - .handle(new Request('http://localhost/api/captcha/generate', { + const response = await app.handle( + new Request('http://localhost/api/captcha/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(200); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('SUCCESS'); expect(result.data.type).toBe('image'); expect(result.data.image).toMatch(/^data:image\/png;base64,/); @@ -90,15 +92,16 @@ describe('Captcha API', () => { width: 50, // 小于最小值100 height: 20, // 小于最小值40 length: 2, // 小于最小值4 - expireTime: 30 // 小于最小值60 + expireTime: 30, // 小于最小值60 }; - const response = await app - .handle(new Request('http://localhost/api/captcha/generate', { + const response = await app.handle( + new Request('http://localhost/api/captcha/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); @@ -109,18 +112,19 @@ describe('Captcha API', () => { const payload: VerifyCaptchaRequest = { captchaId: 'nonexistent_captcha_id', captchaCode: '1234', - scene: 'login' + scene: 'login', }; - const response = await app - .handle(new Request('http://localhost/api/captcha/verify', { + const response = await app.handle( + new Request('http://localhost/api/captcha/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(200); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('SUCCESS'); expect(result.data.valid).toBe(false); expect(result.data.message).toContain('验证码不存在或已过期'); @@ -132,12 +136,13 @@ describe('Captcha API', () => { captchaCode: '123', // 长度小于4 }; - const response = await app - .handle(new Request('http://localhost/api/captcha/verify', { + const response = await app.handle( + new Request('http://localhost/api/captcha/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), - })); + }), + ); expect(response.status).toBe(400); }); @@ -145,14 +150,15 @@ describe('Captcha API', () => { describe('POST /api/captcha/cleanup', () => { it('应该成功清理过期验证码', async () => { - const response = await app - .handle(new Request('http://localhost/api/captcha/cleanup', { + const response = await app.handle( + new Request('http://localhost/api/captcha/cleanup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - })); + }), + ); expect(response.status).toBe(200); - const result = await response.json() as any; + const result = (await response.json()) as any; expect(result.code).toBe('SUCCESS'); expect(result.data.cleanedCount).toBeGreaterThanOrEqual(0); }); @@ -161,7 +167,7 @@ describe('Captcha API', () => { describe('验证码服务功能测试', () => { it('应该生成指定长度的随机验证码', async () => { const { captchaService } = await import('./captcha.service'); - + // 测试不同长度的验证码 const lengths = [4, 6, 8]; for (const length of lengths) { @@ -173,7 +179,7 @@ describe('Captcha API', () => { it('应该生成Base64格式的图片数据', async () => { const { captchaService } = await import('./captcha.service'); - + const imageData = await (captchaService as any).generateImageCaptcha('TEST', 200, 60); expect(imageData).toMatch(/^data:image\/png;base64,/); expect(imageData.length).toBeGreaterThan(100); // 确保有实际的图片数据 @@ -187,4 +193,4 @@ describe('Captcha API', () => { expect('ABC'.toLowerCase()).toBe('abc'.toLowerCase()); }); }); -}); \ No newline at end of file +}); diff --git a/src/modules/dict/dict.controller.ts b/src/modules/dict/dict.controller.ts new file mode 100644 index 0000000..9cc6161 --- /dev/null +++ b/src/modules/dict/dict.controller.ts @@ -0,0 +1,35 @@ +/** + * @file 字典模块Controller层实现 + * @author AI Assistant + * @date 2024-12-19 + * @lastEditor AI Assistant + * @lastEditTime 2025-01-07 + * @description 字典模块的路由控制器,仅实现创建字典项接口 + */ + +import { Elysia } from 'elysia'; +import { dictService } from './dict.service'; +import { CreateDictSchema } from './dict.schema'; +import { CreateDictResponsesSchema } from './dict.response'; +import { tags } from '@/constants/swaggerTags'; + +/** + * 字典控制器 + * @description 处理字典相关的HTTP请求 + */ +export const dictController = new Elysia() + /** + * 创建字典项接口 + * @route POST /api/dict + * @description 创建字典项,包含验证码验证、字典代码唯一性检查等 + */ + .post('/', ({ body }) => dictService.createDict(body), { + body: CreateDictSchema, + detail: { + summary: '创建字典项', + description: '创建字典项接口,需要提供字典代码、名称等基本信息', + tags: [tags.dict], + operationId: 'createDict', + }, + response: CreateDictResponsesSchema, + }); diff --git a/src/modules/dict/dict.docs.md b/src/modules/dict/dict.docs.md new file mode 100644 index 0000000..dc63e9e --- /dev/null +++ b/src/modules/dict/dict.docs.md @@ -0,0 +1,338 @@ +# 字典模块业务逻辑文档 + +## 模块概述 + +字典模块提供系统字典的管理功能,支持树形结构的字典数据CRUD操作。字典数据用于存储系统配置、状态选项、分类信息等静态数据。 + +## 数据库设计 + +### 表结构:sys_dict + +```sql +CREATE TABLE `sys_dict` ( + `id` BIGINT NOT NULL COMMENT '主键ID', + `code` VARCHAR(50) NOT NULL COMMENT '字典代码,唯一标识', + `name` VARCHAR(100) NOT NULL COMMENT '字典名称', + `value` VARCHAR(200) NULL COMMENT '字典值(叶子节点才有值)', + `description` VARCHAR(500) NULL COMMENT '字典描述', + `icon` VARCHAR(100) NULL COMMENT '图标(CSS类名或图标路径)', + `pid` BIGINT NULL DEFAULT 0 COMMENT '父级ID,0表示顶级', + `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度,1为顶级', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', + `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-启用,inactive-禁用', + `is_system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否系统字典', + `color` VARCHAR(20) NULL COMMENT '颜色标识', + `extra` JSON NULL COMMENT '扩展字段', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_pid` (`pid`), + KEY `idx_level` (`level`), + KEY `idx_status` (`status`), + KEY `idx_sort` (`sort_order`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统字典表'; +``` + +## 接口设计 + +### 1. 创建字典项接口 (POST /api/dict) + +#### 业务逻辑 + +1. **参数验证** + - 验证必填字段:code、name + - 验证code唯一性(全局唯一) + - 验证name在同级下的唯一性 + - 验证pid的有效性(如果指定了父级) + - 验证level深度不超过10层 + +2. **业务规则** + - 默认pid为0(顶级字典) + - 默认level为1(顶级) + - 默认status为'active' + - 默认is_system为false + - 默认sort_order为0 + - 如果有父级,level = 父级level + 1 + - 如果指定了父级,需要验证父级是否存在且状态为active + +3. **数据处理** + - code转换为小写并去除两端空格 + - name去除两端空格 + - description去除两端空格 + - 自动计算level(如果指定了pid) + - 自动生成sort_order(同级最大sort_order + 1) + +4. **错误处理** + - code已存在:409 Conflict + - name在同级下已存在:409 Conflict + - 父级不存在:404 Not Found + - 父级状态非active:400 Bad Request + - level超过10层:400 Bad Request + +#### 性能考虑 + +1. **数据库索引** + - code字段有唯一索引,查询快速 + - pid字段有索引,支持父子关系查询 + - level字段有索引,支持层级查询 + +2. **并发控制** + - 使用分布式锁防止code重复创建 + - 使用数据库事务确保数据一致性 + +#### 安全考虑 + +1. **输入验证** + - 防止SQL注入:使用参数化查询 + - 防止XSS:对输入进行转义 + - 限制字段长度:防止过长的输入 + +2. **权限控制** + - 需要管理员权限 + - 系统字典只能由超级管理员创建 + +#### 缓存策略 + +1. **缓存失效** + - 创建字典项后,清除相关缓存 + - 清除父级字典的缓存 + - 清除完整字典树缓存 + +## 2. 获取字典项内容接口 (GET /api/dict/:id) + +#### 业务逻辑 + +1. **参数验证** + - 验证id格式(bigint) + - 验证id是否存在 + +2. **查询逻辑** + - 根据id查询字典项 + - 只返回状态为active的字典项 + - 包含完整的字典项信息 + +3. **错误处理** + - 字典项不存在:404 Not Found + - 字典项状态非active:404 Not Found + +#### 缓存策略 + +1. **缓存读取** + - 优先从Redis缓存读取 + - 缓存未命中时从数据库查询 + - 查询结果写入缓存 + +## 3. 获取完整字典树接口 (GET /api/dict/tree) + +#### 业务逻辑 + +1. **查询参数** + - status:过滤状态(可选) + - is_system:是否系统字典(可选) + +2. **查询逻辑** + - 查询所有符合条件的字典项 + - 按level和sort_order排序 + - 构建树形结构 + +3. **树形构建** + - 使用递归算法构建树 + - 只包含状态为active的字典项 + - 按sort_order排序 + +#### 性能考虑 + +1. **缓存策略** + - 完整字典树缓存到Redis + - 缓存时间:1小时 + - 支持按条件过滤的缓存 + +2. **查询优化** + - 使用单次查询获取所有数据 + - 在应用层构建树形结构 + +## 4. 获取指定字典树接口 (GET /api/dict/tree/:code) + +#### 业务逻辑 + +1. **参数验证** + - 验证code格式 + - 验证code是否存在 + +2. **查询逻辑** + - 根据code查找字典项 + - 获取该字典项的所有子级 + - 构建子树结构 + +3. **错误处理** + - 字典项不存在:404 Not Found + - 字典项状态非active:404 Not Found + +#### 缓存策略 + +1. **缓存键设计** + - 使用code作为缓存键的一部分 + - 支持条件过滤的缓存 + +## 5. 更新字典项内容接口 (PUT /api/dict/:id) + +#### 业务逻辑 + +1. **参数验证** + - 验证id格式和存在性 + - 验证更新字段的有效性 + - 验证code唯一性(如果更新code) + - 验证name在同级下的唯一性(如果更新name) + +2. **业务规则** + - 系统字典只能由超级管理员更新 + - 不能更新已删除的字典项 + - 更新code时需要检查是否被其他字典项引用 + +3. **级联更新** + - 如果更新level,需要更新所有子级的level + - 如果更新status,可以选择是否级联更新子级 + +#### 缓存策略 + +1. **缓存失效** + - 更新后清除相关缓存 + - 清除父级和子级的缓存 + +## 6. 字典项排序接口 (PUT /api/dict/sort) + +#### 业务逻辑 + +1. **参数验证** + - 验证排序数据的完整性 + - 验证字典项的存在性 + +2. **排序逻辑** + - 支持同级内的排序 + - 支持跨级移动(改变父级) + - 自动重新计算level和sort_order + +3. **事务处理** + - 使用数据库事务确保排序的一致性 + - 支持批量排序操作 + +## 7. 删除字典项接口 (DELETE /api/dict/:id) + +#### 业务逻辑 + +1. **软删除** + - 不物理删除数据 + - 将status设置为'inactive' + - 保留数据完整性 + +2. **依赖检查** + - 检查是否有其他字典项引用 + - 检查是否有业务数据引用 + - 如果有依赖,不允许删除 + +3. **级联处理** + - 可以选择是否级联删除子级 + - 默认不级联删除 + +#### 缓存策略 + +1. **缓存失效** + - 删除后清除相关缓存 + - 清除父级和子级的缓存 + +## 缓存机制 + +### Redis缓存设计 + +1. **缓存键命名** + - 完整字典树:`dict:tree:all` + - 指定字典树:`dict:tree:{code}` + - 单个字典项:`dict:item:{id}` + - 字典项列表:`dict:list:{conditions}` + +2. **缓存时间** + - 字典树:1小时 + - 单个字典项:30分钟 + - 字典项列表:15分钟 + +3. **缓存失效策略** + - 写入操作后主动失效 + - 定时刷新机制 + - 内存不足时LRU淘汰 + +### 缓存一致性 + +1. **写入策略** + - 先更新数据库,再失效缓存 + - 使用分布式锁防止并发问题 + +2. **读取策略** + - 先读缓存,缓存未命中读数据库 + - 数据库查询结果写入缓存 + +## 错误码定义 + +| 错误码 | 说明 | HTTP状态码 | +| -------- | ---------------------- | ---------- | +| DICT_001 | 字典代码已存在 | 409 | +| DICT_002 | 字典名称在同级下已存在 | 409 | +| DICT_003 | 父级字典不存在 | 404 | +| DICT_004 | 父级字典状态非active | 400 | +| DICT_005 | 字典层级超过限制 | 400 | +| DICT_006 | 字典项不存在 | 404 | +| DICT_007 | 字典项状态非active | 404 | +| DICT_008 | 系统字典权限不足 | 403 | +| DICT_009 | 字典项被引用,无法删除 | 400 | +| DICT_010 | 排序数据无效 | 400 | + +## 性能优化建议 + +1. **数据库优化** + - 合理使用索引 + - 避免N+1查询问题 + - 使用批量操作 + +2. **缓存优化** + - 合理设置缓存时间 + - 使用缓存预热 + - 实现缓存穿透保护 + +3. **应用层优化** + - 使用连接池 + - 实现请求限流 + - 优化树形构建算法 + +## 测试策略 + +1. **单元测试** + - 测试所有业务逻辑方法 + - 测试边界条件和异常情况 + - 测试缓存机制 + +2. **集成测试** + - 测试完整的API接口 + - 测试数据库操作 + - 测试Redis缓存 + +3. **性能测试** + - 测试大量数据的处理能力 + - 测试并发访问性能 + - 测试缓存命中率 + +## 部署注意事项 + +1. **环境配置** + - 确保Redis服务可用 + - 配置数据库连接池 + - 设置合适的日志级别 + +2. **监控告警** + - 监控API响应时间 + - 监控缓存命中率 + - 监控数据库连接数 + +3. **备份策略** + - 定期备份字典数据 + - 备份Redis缓存数据 + - 制定数据恢复方案 diff --git a/src/modules/dict/dict.response.ts b/src/modules/dict/dict.response.ts new file mode 100644 index 0000000..bf981d5 --- /dev/null +++ b/src/modules/dict/dict.response.ts @@ -0,0 +1,130 @@ +/** + * @file 字典模块响应格式定义 + * @author AI Assistant + * @date 2024-12-19 + * @lastEditor AI Assistant + * @lastEditTime 2025-01-07 + * @description 定义字典模块的响应格式,包括创建字典项等 + */ + +import { t, type Static } from 'elysia'; +import { responseWrapperSchema } from '@/utils/responseFormate'; + +/** + * 创建字典项成功响应数据结构 + */ +export const CreateDictSuccessSchema = t.Object({ + id: t.String({ + description: '字典项ID(bigint类型以字符串返回防止精度丢失)', + examples: ['1', '2', '100'], + }), + code: t.String({ + description: '字典代码', + examples: ['user_status'], + }), + name: t.String({ + description: '字典名称', + examples: ['用户状态'], + }), + value: t.Optional( + t.String({ + description: '字典值', + examples: ['active'], + }), + ), + description: t.Optional( + t.String({ + description: '字典描述', + examples: ['用户状态字典'], + }), + ), + icon: t.Optional( + t.String({ + description: '图标', + examples: ['icon-user'], + }), + ), + pid: t.String({ + description: '父级ID(bigint类型以字符串返回)', + examples: ['0', '1'], + }), + level: t.Number({ + description: '层级深度', + examples: [1, 2], + }), + sortOrder: t.Number({ + description: '排序号', + examples: [0, 1, 10], + }), + status: t.String({ + description: '状态', + examples: ['active', 'inactive'], + }), + isSystem: t.Boolean({ + description: '是否系统字典', + examples: [true, false], + }), + color: t.Optional( + t.String({ + description: '颜色标识', + examples: ['#1890ff'], + }), + ), + extra: t.Optional( + t.Record(t.String(), t.Any(), { + description: '扩展字段', + examples: [{ key1: 'value1' }], + }), + ), + createdAt: t.String({ + description: '创建时间', + examples: ['2024-12-19T10:30:00Z'], + }), + updatedAt: t.String({ + description: '更新时间', + examples: ['2024-12-19T10:30:00Z'], + }), +}); + +/** + * 创建字典项接口响应组合 + * @description 用于Controller中定义所有可能的响应格式 + */ +export const CreateDictResponsesSchema = { + 200: responseWrapperSchema(CreateDictSuccessSchema), + 409: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '唯一性冲突', + examples: ['字典代码已存在', '字典名称在同级下已存在'], + }), + }), + ), + 400: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '参数错误', + examples: ['参数校验失败', '父级字典状态非active', '字典层级超过限制'], + }), + }), + ), + 404: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '资源不存在', + examples: ['父级字典不存在'], + }), + }), + ), + 500: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '服务器错误', + examples: ['内部服务器错误'], + }), + }), + ), +}; + +/** 创建字典项成功响应数据类型 */ +export type CreateDictSuccessType = Static<(typeof CreateDictResponsesSchema)[200]>; diff --git a/src/modules/dict/dict.schema.ts b/src/modules/dict/dict.schema.ts new file mode 100644 index 0000000..c06c957 --- /dev/null +++ b/src/modules/dict/dict.schema.ts @@ -0,0 +1,371 @@ +/** + * @file 字典模块Schema定义 + * @author AI Assistant + * @date 2024-12-19 + * @lastEditor AI Assistant + * @lastEditTime 2025-01-07 + * @description 定义字典模块的Schema,包括创建字典项、获取字典项、获取字典树等 + */ + +import { t, type Static } from 'elysia'; + +/** + * 创建字典项请求参数Schema + * @description 创建字典项的请求参数验证规则 + */ +export const CreateDictSchema = t.Object({ + /** 字典代码,唯一标识 */ + code: t.Transform(t.String({ + minLength: 1, + maxLength: 50, + description: '字典代码,全局唯一标识,自动转换为小写并去除两端空格', + examples: ['user_status', 'order_type', 'system_config'], + })) + .Decode((value: string) => value.trim().toLowerCase()) + .Encode((value: string) => value), + /** 字典名称 */ + name: t.Transform(t.String({ + minLength: 1, + maxLength: 100, + description: '字典名称,同级下唯一,自动去除两端空格', + examples: ['用户状态', '订单类型', '系统配置'], + })) + .Decode((value: string) => value.trim().toLowerCase()) + .Encode((value: string) => value), + /** 字典值(叶子节点才有值) */ + value: t.Optional( + t + .String({ + maxLength: 200, + description: '字典值,叶子节点才有值,自动去除两端空格', + examples: ['active', 'inactive', 'pending'], + }) + ), + /** 字典描述 */ + description: t.Optional( + t + .String({ + maxLength: 500, + description: '字典描述,自动去除两端空格', + examples: ['用户状态字典,包含激活、禁用、待审核等状态'], + }) + ), + /** 图标(CSS类名或图标路径) */ + icon: t.Optional( + t + .String({ + maxLength: 100, + description: '图标CSS类名或图标路径,自动去除两端空格', + examples: ['icon-user', 'icon-order', '/icons/config.png'], + }) + ), + /** 父级ID,0表示顶级 */ + pid: t.Optional( + t.Union( + [ + t.Literal('0'), + t.String({ + pattern: '^[1-9]\\d*$', + description: '父级字典ID,Bigint字符串形式', + }), + ], + { + description: '父级ID,0表示顶级字典', + examples: ['0', '1', '2'], + }, + ), + ), + /** 排序号 */ + sortOrder: t.Optional( + t.Number({ + minimum: 0, + maximum: 999999, + description: '排序号,同级内排序使用', + examples: [0, 1, 10, 100], + }), + ), + /** 状态:active-启用,inactive-禁用 */ + status: t.Optional( + t + .Union([t.Literal('active'), t.Literal('inactive')], { + description: '字典状态,默认active', + examples: ['active', 'inactive'], + default: 'active', + }) + ), + /** 是否系统字典 */ + isSystem: t.Optional( + t + .Boolean({ + description: '是否系统字典,系统字典只能由超级管理员创建,默认false', + examples: [true, false], + default: false, + }) + ), + /** 颜色标识 */ + color: t.Optional( + t + .String({ + maxLength: 20, + description: '颜色标识,用于前端显示,自动去除两端空格', + examples: ['#1890ff', '#52c41a', '#faad14', '#f5222d'], + }) + ), + /** 扩展字段 */ + extra: t.Optional( + t.Record(t.String(), t.Any(), { + description: '扩展字段,JSON格式', + examples: [{ key1: 'value1', key2: 'value2' }], + }), + ), +}); + +/** 创建字典项请求参数类型 */ +export type CreateDictRequest = Static; + +/** + * 获取字典项请求参数Schema + * @description 根据ID获取字典项的请求参数验证规则 + */ +export const GetDictByIdSchema = t.Object({ + /** 字典项ID */ + id: t.String({ + pattern: '^[1-9]\\d*$', + description: '字典项ID,必须是正整数', + examples: ['1', '2', '100'], + }), +}); + +/** 获取字典项请求参数类型 */ +export type GetDictByIdRequest = Static; + +/** + * 获取完整字典树查询参数Schema + * @description 获取完整字典树的查询参数验证规则 + */ +export const GetDictTreeQuerySchema = t.Object({ + /** 状态过滤 */ + status: t.Optional( + t.Union([t.Literal('active'), t.Literal('inactive'), t.Literal('all')], { + description: '状态过滤条件', + examples: ['active', 'inactive', 'all'], + }), + ), + /** 是否系统字典过滤 */ + isSystem: t.Optional( + t.Union([t.Literal('true'), t.Literal('false'), t.Literal('all')], { + description: '是否系统字典过滤条件', + examples: ['true', 'false', 'all'], + }), + ), +}); + +/** 获取完整字典树查询参数类型 */ +export type GetDictTreeQueryRequest = Static; + +/** + * 获取指定字典树请求参数Schema + * @description 根据code获取指定字典树的请求参数验证规则 + */ +export const GetDictTreeByCodeSchema = t.Object({ + /** 字典代码 */ + code: t.String({ + minLength: 1, + maxLength: 50, + description: '字典代码,用于查找指定的字典树', + examples: ['user_status', 'order_type', 'system_config'], + }), + /** 状态过滤 */ + status: t.Optional( + t.Union([t.Literal('active'), t.Literal('inactive'), t.Literal('all')], { + description: '状态过滤条件', + examples: ['active', 'inactive', 'all'], + }), + ), + /** 是否系统字典过滤 */ + isSystem: t.Optional( + t.Union([t.Literal('true'), t.Literal('false'), t.Literal('all')], { + description: '是否系统字典过滤条件', + examples: ['true', 'false', 'all'], + }), + ), +}); + +/** 获取指定字典树请求参数类型 */ +export type GetDictTreeByCodeRequest = Static; + +/** + * 更新字典项请求参数Schema + * @description 更新字典项的请求参数验证规则,所有字段都是可选的 + */ +export const UpdateDictSchema = t.Object({ + /** 字典代码,唯一标识 */ + code: t.Optional( + t.String({ + minLength: 1, + maxLength: 50, + description: '字典代码,全局唯一标识,自动转换为小写', + examples: ['user_status', 'order_type', 'system_config'], + }), + ), + /** 字典名称 */ + name: t.Optional( + t.String({ + minLength: 1, + maxLength: 100, + description: '字典名称,同级下唯一', + examples: ['用户状态', '订单类型', '系统配置'], + }), + ), + /** 字典值(叶子节点才有值) */ + value: t.Optional( + t.String({ + maxLength: 200, + description: '字典值,叶子节点才有值', + examples: ['active', 'inactive', 'pending'], + }), + ), + /** 字典描述 */ + description: t.Optional( + t.String({ + maxLength: 500, + description: '字典描述信息', + examples: ['用户状态字典,包含激活、禁用、待审核等状态'], + }), + ), + /** 图标(CSS类名或图标路径) */ + icon: t.Optional( + t.String({ + maxLength: 100, + description: '图标CSS类名或图标路径', + examples: ['icon-user', 'icon-order', '/icons/config.png'], + }), + ), + /** 父级ID,0表示顶级 */ + pid: t.Optional( + t.Union( + [ + t.Literal(0), + t.String({ + pattern: '^[1-9]\\d*$', + description: '父级字典ID,必须是正整数', + }), + ], + { + description: '父级ID,0表示顶级字典', + examples: [0, '1', '2'], + }, + ), + ), + /** 排序号 */ + sortOrder: t.Optional( + t.Number({ + minimum: 0, + maximum: 999999, + description: '排序号,同级内排序使用', + examples: [0, 1, 10, 100], + }), + ), + /** 状态:active-启用,inactive-禁用 */ + status: t.Optional( + t.Union([t.Literal('active'), t.Literal('inactive')], { + description: '字典状态', + examples: ['active', 'inactive'], + }), + ), + /** 是否系统字典 */ + isSystem: t.Optional( + t.Boolean({ + description: '是否系统字典,系统字典只能由超级管理员创建', + examples: [true, false], + }), + ), + /** 颜色标识 */ + color: t.Optional( + t.String({ + maxLength: 20, + description: '颜色标识,用于前端显示', + examples: ['#1890ff', '#52c41a', '#faad14', '#f5222d'], + }), + ), + /** 扩展字段 */ + extra: t.Optional( + t.Record(t.String(), t.Any(), { + description: '扩展字段,JSON格式', + examples: [{ key1: 'value1', key2: 'value2' }], + }), + ), +}); + +/** 更新字典项请求参数类型 */ +export type UpdateDictRequest = Static; + +/** + * 字典项排序请求参数Schema + * @description 字典项排序的请求参数验证规则 + */ +export const SortDictSchema = t.Object({ + /** 排序项列表 */ + items: t.Array( + t.Object({ + /** 字典项ID */ + id: t.String({ + pattern: '^[1-9]\\d*$', + description: '字典项ID', + examples: ['1', '2', '100'], + }), + /** 新的父级ID */ + pid: t.Union( + [ + t.Literal(0), + t.String({ + pattern: '^[1-9]\\d*$', + description: '父级字典ID', + }), + ], + { + description: '新的父级ID,0表示顶级', + examples: [0, '1', '2'], + }, + ), + /** 新的排序号 */ + sortOrder: t.Number({ + minimum: 0, + maximum: 999999, + description: '新的排序号', + examples: [0, 1, 10, 100], + }), + }), + { + minItems: 1, + maxItems: 100, + description: '排序项列表,至少包含一个项目,最多100个', + }, + ), +}); + +/** 字典项排序请求参数类型 */ +export type SortDictRequest = Static; + +/** + * 删除字典项请求参数Schema + * @description 删除字典项的请求参数验证规则 + */ +export const DeleteDictSchema = t.Object({ + /** 字典项ID */ + id: t.String({ + pattern: '^[1-9]\\d*$', + description: '字典项ID,必须是正整数', + examples: ['1', '2', '100'], + }), + /** 是否级联删除子级 */ + cascade: t.Optional( + t.Boolean({ + description: '是否级联删除子级字典项', + examples: [true, false], + }), + ), +}); + +/** 删除字典项请求参数类型 */ +export type DeleteDictRequest = Static; diff --git a/src/modules/dict/dict.service.ts b/src/modules/dict/dict.service.ts new file mode 100644 index 0000000..2e8ee3e --- /dev/null +++ b/src/modules/dict/dict.service.ts @@ -0,0 +1,127 @@ +/** + * @file 字典模块Service层实现 + * @author AI Assistant + * @date 2024-12-19 + * @lastEditor AI Assistant + * @lastEditTime 2025-01-07 + * @description 字典模块的业务逻辑实现,仅实现创建字典项 + */ + +import { Logger } from '@/plugins/logger/logger.service'; +import { db } from '@/plugins/drizzle/drizzle.service'; +import { sysDict } from '@/eneities'; +import { eq, and, max } from 'drizzle-orm'; +import { successResponse, BusinessError } from '@/utils/responseFormate'; +import { nextId } from '@/utils/snowflake'; +import type { CreateDictRequest } from './dict.schema'; +import type { CreateDictSuccessType } from './dict.response'; + +/** + * 字典服务类 + * @description 处理字典相关的业务逻辑 + */ +export class DictService { + /** + * 创建字典项 + * @param body 创建字典项请求参数 + * @returns Promise + * @throws BusinessError 业务逻辑错误 + */ + public async createDict(body: CreateDictRequest): Promise { + // 1. code唯一性校验 + const existCode = await db().select({id: sysDict.id}).from(sysDict).where(eq(sysDict.code, body.code)).limit(1); + if (existCode.length > 0) { + throw new BusinessError(`字典代码已存在: ${body.code}`, 409); + } + + // 2. name同级唯一性校验 + const pid = body.pid || '0'; + const existName = await db().select({id: sysDict.id}).from(sysDict).where(and(eq(sysDict.name, body.name), eq(sysDict.pid, pid))).limit(1); + if (existName.length > 0) { + throw new BusinessError(`字典名称已存在: ${body.name}`, 409); + } + + // 3. 父级校验与层级处理 + let level = 1; + if (pid !== '0') { + const parent = await db().select().from(sysDict).where(eq(sysDict.id, pid)).limit(1); + if (parent.length === 0) { + throw new BusinessError(`父级字典不存在: ${pid}`, 404); + } + if (parent[0]!.status !== 'active') { + throw new BusinessError(`父级字典状态非active: ${pid}`, 400); + } + level = parent[0]!.level + 1; + if (level > 10) { + throw new BusinessError(`字典层级超过限制: ${level}`, 400); + } + } + + // 4. sortOrder处理(同级最大+1) + let sortOrder = 0; + if (body.sortOrder !== undefined) { + sortOrder = body.sortOrder; + } else { + const maxSort = await db() + .select({ maxSort: max(sysDict.sortOrder) }) + .from(sysDict) + .where(eq(sysDict.pid, pid)); + sortOrder = (maxSort[0]?.maxSort ?? 0) + 1; + } + + // 5. 数据写入 + const dictId = nextId(); + + await db() + .insert(sysDict) + .values([ + { + id: dictId.toString(), + code: body.code, + name: body.name, + value: body.value ?? null, + description: body.description ?? null, + icon: body.icon ?? null, + pid: BigInt(pid), + level, + sortOrder, + status: body.status, + isSystem: body.isSystem ? 1 : 0, + color: body.color ?? null, + extra: body.extra ?? {}, + }, + ] as any); + + // 6. 查询刚插入的数据 + const insertedArr = await db().select().from(sysDict).where(eq(sysDict.id, dictId.toString())).limit(1); + if (!insertedArr || insertedArr.length === 0) { + throw new BusinessError('创建字典项失败', 500); + } + const inserted = insertedArr[0]!; + + // 7. 返回统一响应 + return successResponse( + { + id: String(inserted.id), + code: inserted.code, + name: inserted.name, + value: inserted.value, + description: inserted.description, + icon: inserted.icon, + pid: String(inserted.pid), + level: inserted.level, + sortOrder: inserted.sortOrder, + status: inserted.status, + isSystem: Boolean(inserted.isSystem), + color: inserted.color, + extra: inserted.extra, + createdAt: inserted.createdAt, + updatedAt: inserted.updatedAt, + }, + '创建字典项成功', + ); + } +} + +// 导出单例实例 +export const dictService = new DictService(); diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts index f192d17..cb3681c 100644 --- a/src/modules/health/health.controller.ts +++ b/src/modules/health/health.controller.ts @@ -28,4 +28,4 @@ export const healthController = new Elysia() summary: '获取详细健康状态', description: '获取系统详细健康状态,包括性能指标、资源使用情况等', }, - }); \ No newline at end of file + }); diff --git a/src/modules/health/health.service.ts b/src/modules/health/health.service.ts index 53e1600..455b870 100644 --- a/src/modules/health/health.service.ts +++ b/src/modules/health/health.service.ts @@ -1,300 +1,310 @@ -/** - * @file 健康检查服务 - * @author hotok - * @date 2025-06-28 - * @lastEditor hotok - * @lastEditTime 2025-06-28 - * @description 系统健康状态检查业务逻辑,包括数据库、Redis等依赖检查 - */ - -import type { Context } from 'elysia'; -import { Redis } from '@/utils/redis'; -import { pool } from '@/utils/mysql'; -import { Logger } from '@/plugins/logger/logger.service'; - -// 临时内联类型定义 -interface ComponentStatus { - status: 'healthy' | 'unhealthy' | 'degraded'; - responseTime?: number; - error?: string; - details?: Record; -} - -interface HealthStatus { - code: number; - message: string; - data: { - status: 'healthy' | 'unhealthy' | 'degraded'; - timestamp: string; - uptime: number; - responseTime: number; - version: string; - environment: string; - error?: string; - components: { - mysql?: ComponentStatus; - redis?: ComponentStatus; - [key: string]: ComponentStatus | undefined; - }; - }; -} - -interface DetailedHealthStatus extends HealthStatus { - data: HealthStatus['data'] & { - system?: { - platform: string; - arch: string; - nodeVersion: string; - runtime: string; - pid: number; - cwd: string; - }; - performance?: { - cpuUsage: { - user: number; - system: number; - }; - memoryUsage: { - rss: number; - heapTotal: number; - heapUsed: number; - external: number; - arrayBuffers: number; - }; - uptime: number; - }; - }; -} - -/** - * 健康检查服务类 - * 提供系统及依赖服务的健康状态检查 - */ -class HealthService { - /** - * Redis实例 - */ - private redis: Redis; - - constructor() { - this.redis = new Redis(); - } - - /** - * 获取基本健康状态 - * @param ctx Elysia上下文 - * @returns 健康状态信息 - */ - async getHealthStatus(ctx: Context): Promise { - const startTime = Date.now(); - const timestamp = new Date().toISOString(); - - try { - // 并行检查所有依赖 - const [mysqlStatus, redisStatus] = await Promise.allSettled([ - this.checkMysqlHealth(), - this.checkRedisHealth(), - ]); - - /** 系统整体状态 */ - const overallStatus = this.determineOverallStatus([ - mysqlStatus.status === 'fulfilled' ? mysqlStatus.value : { status: 'unhealthy', error: 'Connection failed' }, - redisStatus.status === 'fulfilled' ? redisStatus.value : { status: 'unhealthy', error: 'Connection failed' }, - ]); - - const responseTime = Date.now() - startTime; - - return { - code: overallStatus === 'healthy' ? 0 : 1, - message: overallStatus === 'healthy' ? '所有服务运行正常' : '部分服务异常', - data: { - status: overallStatus, - timestamp, - uptime: process.uptime(), - responseTime, - version: process.env.npm_package_version || '1.0.0', - environment: process.env.NODE_ENV || 'development', - components: { - mysql: mysqlStatus.status === 'fulfilled' ? mysqlStatus.value : { status: 'unhealthy', error: 'Connection failed' }, - redis: redisStatus.status === 'fulfilled' ? redisStatus.value : { status: 'unhealthy', error: 'Connection failed' }, - }, - }, - }; - } catch (error) { - Logger.error(error as Error); - return { - code: 1, - message: '健康检查异常', - data: { - status: 'unhealthy', - timestamp, - uptime: process.uptime(), - responseTime: Date.now() - startTime, - version: process.env.npm_package_version || '1.0.0', - environment: process.env.NODE_ENV || 'development', - error: 'Health check failed', - components: {}, - }, - }; - } - } - - /** - * 获取详细健康状态 - * @param ctx Elysia上下文 - * @returns 详细健康状态信息 - */ - async getDetailedHealthStatus(ctx: Context): Promise { - const startTime = Date.now(); - const timestamp = new Date().toISOString(); - - try { - // 获取基本健康状态 - const basicHealth = await this.getHealthStatus(ctx); - - // 获取系统资源信息 - const systemInfo = this.getSystemInfo(); - - return { - ...basicHealth, - data: { - ...basicHealth.data, - system: systemInfo, - performance: { - cpuUsage: process.cpuUsage(), - memoryUsage: process.memoryUsage(), - uptime: process.uptime(), - }, - }, - }; - } catch (error) { - Logger.error(error as Error); - return { - code: 1, - message: '详细健康检查异常', - data: { - status: 'unhealthy', - timestamp, - uptime: process.uptime(), - responseTime: Date.now() - startTime, - version: process.env.npm_package_version || '1.0.0', - environment: process.env.NODE_ENV || 'development', - error: 'Detailed health check failed', - components: {}, - }, - }; - } - } - - /** - * 检查MySQL健康状态 - * @returns MySQL组件状态 - */ - private async checkMysqlHealth(): Promise { - try { - const startTime = Date.now(); - await pool.execute('SELECT 1'); - const responseTime = Date.now() - startTime; - - return { - status: 'healthy', - responseTime, - details: { - connection: 'active', - host: process.env.DB_HOST || 'localhost', - port: process.env.DB_PORT || '3306', - }, - }; - } catch (error) { - Logger.error(error as Error); - return { - status: 'unhealthy', - error: (error as Error).message, - details: { - connection: 'failed', - host: process.env.DB_HOST || 'localhost', - port: process.env.DB_PORT || '3306', - }, - }; - } - } - - /** - * 检查Redis健康状态 - * @returns Redis组件状态 - */ - private async checkRedisHealth(): Promise { - try { - const startTime = Date.now(); - const isHealthy = await this.redis.checkRedisHealth(); - const responseTime = Date.now() - startTime; - - if (isHealthy) { - const redisStatus = this.redis.getRedisStatus(); - return { - status: 'healthy', - responseTime, - details: { - connection: 'active', - ...redisStatus.config, - }, - }; - } else { - return { - status: 'unhealthy', - error: 'Redis ping failed', - details: { - connection: 'failed', - }, - }; - } - } catch (error) { - Logger.error(error as Error); - return { - status: 'unhealthy', - error: (error as Error).message, - details: { - connection: 'failed', - }, - }; - } - } - - /** - * 确定整体状态 - * @param components 各组件状态 - * @returns 整体状态 - */ - private determineOverallStatus(components: ComponentStatus[]): 'healthy' | 'unhealthy' | 'degraded' { - const healthyCount = components.filter(c => c.status === 'healthy').length; - const totalCount = components.length; - - if (healthyCount === totalCount) { - return 'healthy'; - } else if (healthyCount === 0) { - return 'unhealthy'; - } else { - return 'degraded'; - } - } - - /** - * 获取系统信息 - * @returns 系统信息 - */ - private getSystemInfo() { - return { - platform: process.platform, - arch: process.arch, - nodeVersion: process.version, - runtime: 'Bun', - pid: process.pid, - cwd: process.cwd(), - }; - } -} - -/** - * 导出健康检查服务实例 - */ -export const healthService = new HealthService(); \ No newline at end of file +/** + * @file 健康检查服务 + * @author hotok + * @date 2025-06-28 + * @lastEditor hotok + * @lastEditTime 2025-06-28 + * @description 系统健康状态检查业务逻辑,包括数据库、Redis等依赖检查 + */ + +import type { Context } from 'elysia'; +import { Redis } from '@/utils/redis'; +import { pool } from '@/utils/mysql'; +import { Logger } from '@/plugins/logger/logger.service'; + +// 临时内联类型定义 +interface ComponentStatus { + status: 'healthy' | 'unhealthy' | 'degraded'; + responseTime?: number; + error?: string; + details?: Record; +} + +interface HealthStatus { + code: number; + message: string; + data: { + status: 'healthy' | 'unhealthy' | 'degraded'; + timestamp: string; + uptime: number; + responseTime: number; + version: string; + environment: string; + error?: string; + components: { + mysql?: ComponentStatus; + redis?: ComponentStatus; + [key: string]: ComponentStatus | undefined; + }; + }; +} + +interface DetailedHealthStatus extends HealthStatus { + data: HealthStatus['data'] & { + system?: { + platform: string; + arch: string; + nodeVersion: string; + runtime: string; + pid: number; + cwd: string; + }; + performance?: { + cpuUsage: { + user: number; + system: number; + }; + memoryUsage: { + rss: number; + heapTotal: number; + heapUsed: number; + external: number; + arrayBuffers: number; + }; + uptime: number; + }; + }; +} + +/** + * 健康检查服务类 + * 提供系统及依赖服务的健康状态检查 + */ +class HealthService { + /** + * Redis实例 + */ + private redis: Redis; + + constructor() { + this.redis = new Redis(); + } + + /** + * 获取基本健康状态 + * @param ctx Elysia上下文 + * @returns 健康状态信息 + */ + async getHealthStatus(ctx: Context): Promise { + const startTime = Date.now(); + const timestamp = new Date().toISOString(); + + try { + // 并行检查所有依赖 + const [mysqlStatus, redisStatus] = await Promise.allSettled([ + this.checkMysqlHealth(), + this.checkRedisHealth(), + ]); + + /** 系统整体状态 */ + const overallStatus = this.determineOverallStatus([ + mysqlStatus.status === 'fulfilled' + ? mysqlStatus.value + : { status: 'unhealthy', error: 'Connection failed' }, + redisStatus.status === 'fulfilled' + ? redisStatus.value + : { status: 'unhealthy', error: 'Connection failed' }, + ]); + + const responseTime = Date.now() - startTime; + + return { + code: overallStatus === 'healthy' ? 0 : 1, + message: overallStatus === 'healthy' ? '所有服务运行正常' : '部分服务异常', + data: { + status: overallStatus, + timestamp, + uptime: process.uptime(), + responseTime, + version: process.env.npm_package_version || '1.0.0', + environment: process.env.NODE_ENV || 'development', + components: { + mysql: + mysqlStatus.status === 'fulfilled' + ? mysqlStatus.value + : { status: 'unhealthy', error: 'Connection failed' }, + redis: + redisStatus.status === 'fulfilled' + ? redisStatus.value + : { status: 'unhealthy', error: 'Connection failed' }, + }, + }, + }; + } catch (error) { + Logger.error(error as Error); + return { + code: 1, + message: '健康检查异常', + data: { + status: 'unhealthy', + timestamp, + uptime: process.uptime(), + responseTime: Date.now() - startTime, + version: process.env.npm_package_version || '1.0.0', + environment: process.env.NODE_ENV || 'development', + error: 'Health check failed', + components: {}, + }, + }; + } + } + + /** + * 获取详细健康状态 + * @param ctx Elysia上下文 + * @returns 详细健康状态信息 + */ + async getDetailedHealthStatus(ctx: Context): Promise { + const startTime = Date.now(); + const timestamp = new Date().toISOString(); + + try { + // 获取基本健康状态 + const basicHealth = await this.getHealthStatus(ctx); + + // 获取系统资源信息 + const systemInfo = this.getSystemInfo(); + + return { + ...basicHealth, + data: { + ...basicHealth.data, + system: systemInfo, + performance: { + cpuUsage: process.cpuUsage(), + memoryUsage: process.memoryUsage(), + uptime: process.uptime(), + }, + }, + }; + } catch (error) { + Logger.error(error as Error); + return { + code: 1, + message: '详细健康检查异常', + data: { + status: 'unhealthy', + timestamp, + uptime: process.uptime(), + responseTime: Date.now() - startTime, + version: process.env.npm_package_version || '1.0.0', + environment: process.env.NODE_ENV || 'development', + error: 'Detailed health check failed', + components: {}, + }, + }; + } + } + + /** + * 检查MySQL健康状态 + * @returns MySQL组件状态 + */ + private async checkMysqlHealth(): Promise { + try { + const startTime = Date.now(); + await pool.execute('SELECT 1'); + const responseTime = Date.now() - startTime; + + return { + status: 'healthy', + responseTime, + details: { + connection: 'active', + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || '3306', + }, + }; + } catch (error) { + Logger.error(error as Error); + return { + status: 'unhealthy', + error: (error as Error).message, + details: { + connection: 'failed', + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || '3306', + }, + }; + } + } + + /** + * 检查Redis健康状态 + * @returns Redis组件状态 + */ + private async checkRedisHealth(): Promise { + try { + const startTime = Date.now(); + const isHealthy = await this.redis.checkRedisHealth(); + const responseTime = Date.now() - startTime; + + if (isHealthy) { + const redisStatus = this.redis.getRedisStatus(); + return { + status: 'healthy', + responseTime, + details: { + connection: 'active', + ...redisStatus.config, + }, + }; + } else { + return { + status: 'unhealthy', + error: 'Redis ping failed', + details: { + connection: 'failed', + }, + }; + } + } catch (error) { + Logger.error(error as Error); + return { + status: 'unhealthy', + error: (error as Error).message, + details: { + connection: 'failed', + }, + }; + } + } + + /** + * 确定整体状态 + * @param components 各组件状态 + * @returns 整体状态 + */ + private determineOverallStatus(components: ComponentStatus[]): 'healthy' | 'unhealthy' | 'degraded' { + const healthyCount = components.filter((c) => c.status === 'healthy').length; + const totalCount = components.length; + + if (healthyCount === totalCount) { + return 'healthy'; + } else if (healthyCount === 0) { + return 'unhealthy'; + } else { + return 'degraded'; + } + } + + /** + * 获取系统信息 + * @returns 系统信息 + */ + private getSystemInfo() { + return { + platform: process.platform, + arch: process.arch, + nodeVersion: process.version, + runtime: 'Bun', + pid: process.pid, + cwd: process.cwd(), + }; + } +} + +/** + * 导出健康检查服务实例 + */ +export const healthService = new HealthService(); diff --git a/src/modules/index.ts b/src/modules/index.ts index 4913167..fae3693 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -1,38 +1,41 @@ -/** - * @file API 路由总入口 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 所有 API 路由的统一入口,使用 group 进行模块化管理 - */ - -import { Elysia } from 'elysia'; -import { healthController } from './health/health.controller'; -import { userController } from './user/user.controller'; -import { testController } from './test/test.controller'; -import { captchaController } from './captcha/captcha.controller'; -import { authController } from './auth/auth.controller'; - -/** - * 主路由控制器 - API 路由总入口 - * @description 统一管理所有 API 路由,便于维护和扩展 - */ -export const controllers = new Elysia({ - prefix: '/api', - name: 'controller', -}) - // 版本信息 - .get('/version', () => ({ - version: '1.0.0', - })) - // 用户系统接口 - .group('/user', (app) => app.use(userController)) - // 验证性接口 - .group('/test', (app) => app.use(testController)) - // 健康检查接口 - .group('/health', (app) => app.use(healthController)) - // 认证接口 - .group('/auth', (app) => app.use(authController)) - // 验证码接口 - .group('/captcha', (app) => app.use(captchaController)); +/** + * @file API 路由总入口 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 所有 API 路由的统一入口,使用 group 进行模块化管理 + */ + +import { Elysia } from 'elysia'; +import { healthController } from './health/health.controller'; +import { userController } from './user/user.controller'; +import { testController } from './test/test.controller'; +import { captchaController } from './captcha/captcha.controller'; +import { authController } from './auth/auth.controller'; +import { dictController } from './dict/dict.controller'; + +/** + * 主路由控制器 - API 路由总入口 + * @description 统一管理所有 API 路由,便于维护和扩展 + */ +export const controllers = new Elysia({ + prefix: '/api', + name: 'controller', +}) + // 版本信息 + .get('/version', () => ({ + version: '1.0.0', + })) + // 用户系统接口 + .group('/user', (app) => app.use(userController)) + // 验证性接口 + .group('/test', (app) => app.use(testController)) + // 健康检查接口 + .group('/health', (app) => app.use(healthController)) + // 认证接口 + .group('/auth', (app) => app.use(authController)) + // 验证码接口 + .group('/captcha', (app) => app.use(captchaController)) + // 字典接口 + .group('/dict', (app) => app.use(dictController)); diff --git a/src/modules/test/test.controller.ts b/src/modules/test/test.controller.ts index 00419ff..6416690 100644 --- a/src/modules/test/test.controller.ts +++ b/src/modules/test/test.controller.ts @@ -1,5 +1,5 @@ -import { Elysia } from 'elysia'; - -export const testController = new Elysia({ name: 'testController' }) - .get('/', () => ({ message: '验证性接口' })) - .get('/hello', () => ({ message: 'hello' })); +import { Elysia } from 'elysia'; + +export const testController = new Elysia({ name: 'testController' }) + .get('/', () => ({ message: '验证性接口' })) + .get('/hello', () => ({ message: 'hello' })); diff --git a/src/modules/user/doc/create-user.doc.md b/src/modules/user/doc/create-user.doc.md new file mode 100644 index 0000000..50ded0e --- /dev/null +++ b/src/modules/user/doc/create-user.doc.md @@ -0,0 +1,267 @@ +# 创建用户接口业务逻辑文档 + +## 接口基本信息 + +- **接口名称**: 创建用户接口 +- **请求方法**: POST +- **接口路径**: `/users` +- **权限要求**: 需要 `user:create` 权限 +- **优先级**: P0 + +## 需求描述 + +管理员创建新用户,可直接设置角色、组织等信息。支持批量创建用户,可预设用户角色和组织,支持直接激活状态。 + +## 设计原因 + +- 管理员需要批量创建用户 +- 可预设用户角色和组织 +- 支持直接激活状态 +- 提供完整的用户信息设置 + +## 接口参数 + +```typescript +{ + username: string; // 用户名,必填,3-50字符,字母开头,仅包含字母数字下划线 + email: string; // 邮箱地址,必填,需符合邮箱格式 + password: string; // 密码,必填,8-100字符,必须包含大小写字母和数字 + mobile?: string; // 手机号,可选,11位数字 + nickname?: string; // 昵称,可选,1-50字符 + status?: string; // 用户状态,可选,默认active,可选值:active/inactive/locked + gender?: number; // 性别,可选,0-未知,1-男,2-女 + birthday?: string; // 生日,可选,ISO 8601格式 + bio?: string; // 个人简介,可选,最大500字符 + roleIds?: number[]; // 角色ID数组,可选 + organizationIds?: Array<{ + id: number; + isPrimary: boolean; // 是否主要组织 + position?: string; // 职位,可选 + }>; // 组织信息,可选 + tagIds?: number[]; // 标签ID数组,可选 + sendNotification?: boolean; // 是否发送通知邮件,可选,默认false +} +``` + +## 业务逻辑流程 + +### 1. 权限验证 + +- 检查当前用户是否具有 `user:create` 权限 +- 如果没有权限,返回403错误 + +### 2. 参数验证 + +- 验证必填字段:username、email +- 验证字段格式: + - username: 3-50字符,字母开头,仅包含字母数字下划线 + - email: 符合邮箱格式 + - mobile: 11位数字(如果提供) +- 验证唯一性: + - username 在系统中唯一 + - email 在系统中唯一 + - mobile 在系统中唯一(如果提供) +- password从配置文件获取,使用默认密码 + +### 3. 数据预处理 + +- username: 转换为小写并去除两端空格 +- email: 转换为小写并去除两端空格 +- mobile: 去除空格和特殊字符 +- nickname: 去除两端空格 +- bio: 去除两端空格 + +### 4. 密码处理 + +- 使用 bcrypt 加密密码,成本因子为12,从配置文件获取 +- 生成密码哈希值 + +### 5. 事务处理 + +使用数据库事务确保数据一致性: + +#### 5.1 创建用户记录 + +```sql +INSERT INTO sys_users ( + username, email, password_hash, mobile, nickname, + status, gender, birthday, bio, created_by, updated_by +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +``` + +#### 5.2 分配角色(如果提供roleIds) + +```sql +INSERT INTO sys_user_roles (user_id, role_id, created_by) VALUES (?, ?, ?) +``` + +#### 5.3 分配组织(如果提供organizationIds) + +```sql +INSERT INTO sys_user_organizations ( + user_id, organization_id, is_primary, position, created_by +) VALUES (?, ?, ?, ?, ?) +``` + +#### 5.4 分配标签(如果提供tagIds) + +```sql +INSERT INTO sys_user_tags (user_id, tag_id, created_by) VALUES (?, ?, ?) +``` + +### 6. 后续处理 + +- 如果 sendNotification 为 true,发送欢迎邮件(异步处理) +- 记录操作日志到 sys_operation_logs 表 +- 清除相关缓存(用户列表缓存等) + +## 响应格式 + +### 成功响应 (201 Created) + +```json +{ + "code": 0, + "message": "创建成功", + "data": { + "id": "1234567890", + "username": "new_user", + "email": "new@example.com", + "mobile": "13800138000", + "nickname": "新用户", + "status": "active", + "gender": 1, + "birthday": "1990-01-01", + "bio": "个人简介", + "createdAt": "2024-01-01T12:00:00Z", + "updatedAt": "2024-01-01T12:00:00Z" + } +} +``` + +### 错误响应 + +#### 400 Bad Request - 参数错误 + +```json +{ + "code": 400, + "message": "用户名格式不正确", + "data": null +} +``` + +#### 403 Forbidden - 权限不足 + +```json +{ + "code": 403, + "message": "权限不足", + "data": null +} +``` + +#### 409 Conflict - 资源冲突 + +```json +{ + "code": 409, + "message": "用户名已被占用", + "data": null +} +``` + +## 安全考虑 + +### 1. 权限控制 + +- 只有具有 `user:create` 权限的用户才能创建用户 +- 记录创建者信息,便于审计 + +### 2. 数据验证 + +- 严格的参数格式验证 +- 唯一性检查防止重复数据 +- 密码强度要求 + +### 3. 事务安全 + +- 使用数据库事务确保数据一致性 +- 如果任何步骤失败,回滚所有操作 + +### 4. 分布式锁 + +- 在检查用户名和邮箱唯一性时使用分布式锁 +- 防止并发创建导致的重复数据 + +## 性能考虑 + +### 1. 数据库优化 + +- 在 username、email、mobile 字段上建立唯一索引 +- 使用批量插入减少数据库交互次数 + +### 2. 缓存策略 + +- 创建成功后清除用户列表缓存 +- 清除相关角色和组织缓存 + +### 3. 异步处理 + +- 邮件发送使用异步队列处理 +- 日志记录使用异步处理 + +## 错误处理 + +### 1. 业务错误 + +- 参数验证失败:返回400错误 +- 权限不足:返回403错误 +- 资源冲突:返回409错误 + +### 2. 系统错误 + +- 数据库连接失败:返回500错误 +- 邮件服务失败:记录错误但不影响用户创建 +- 缓存服务失败:记录错误但不影响主流程 + +## 测试用例 + +### 1. 正常创建 + +- 提供完整的用户信息 +- 验证返回的用户数据正确 +- 验证数据库中数据正确保存 + +### 2. 权限测试 + +- 无权限用户尝试创建:应返回403错误 +- 有权限用户创建:应成功 + +### 3. 参数验证 + +- 无效的用户名格式:应返回400错误 +- 无效的邮箱格式:应返回400错误 +- 弱密码:应返回400错误 + +### 4. 唯一性测试 + +- 重复用户名:应返回409错误 +- 重复邮箱:应返回409错误 +- 重复手机号:应返回409错误 + +### 5. 事务测试 + +- 创建过程中数据库异常:应回滚所有操作 +- 部分数据插入失败:应回滚所有操作 + +## 注意事项 + +1. **分布式锁使用**:在检查唯一性时必须使用分布式锁 +2. **事务边界**:整个创建过程必须在同一个事务中 +3. **密码安全**:密码必须使用bcrypt加密,成本因子为12 +4. **日志记录**:必须记录操作日志,包含操作者和操作内容 +5. **缓存清理**:创建成功后必须清理相关缓存 +6. **异步处理**:邮件发送等非关键操作使用异步处理 +7. **错误处理**:必须处理所有可能的错误情况 +8. **数据一致性**:确保用户、角色、组织、标签数据的一致性 diff --git a/src/modules/user/user.schema.ts b/src/modules/user/user.schema.ts index cdaa20f..db2c791 100644 --- a/src/modules/user/user.schema.ts +++ b/src/modules/user/user.schema.ts @@ -10,47 +10,41 @@ import { t, type Static } from 'elysia'; import { createPaginationResponseSchema, createQuerySchema } from '@/utils/pagination'; - /** * 用户列表查询参数Schema * @description 用户列表查询的请求参数验证规则 */ -export const UserListQuerySchema = createQuerySchema(t.Object({ - // 用户特有参数 - keyword: t.Optional(t.String({ - minLength: 1, - maxLength: 100, - description: '搜索关键词,支持用户名、邮箱模糊搜索', - examples: ['admin', 'test@example.com'] - })), - status: t.Optional(t.Union([ - t.Literal('active'), - t.Literal('inactive'), - t.Literal('pending') - ], { - description: '用户状态筛选', - examples: ['active', 'inactive', 'pending'] - })), - gender: t.Optional(t.Union([ - t.Literal(0), - t.Literal(1), - t.Literal(2), - t.Literal('0'), - t.Literal('1'), - t.Literal('2'), - ], { - description: '性别筛选:0-未知,1-男,2-女', - examples: [0, 1, 2] - })), - isRoot: t.Optional(t.Boolean({ - description: '是否超级管理员筛选', - examples: [true, false] - })) -})); - - - +export const UserListQuerySchema = createQuerySchema( + t.Object({ + // 用户特有参数 + keyword: t.Optional( + t.String({ + minLength: 1, + maxLength: 100, + description: '搜索关键词,支持用户名、邮箱模糊搜索', + examples: ['admin', 'test@example.com'], + }), + ), + status: t.Optional( + t.Union([t.Literal('active'), t.Literal('inactive'), t.Literal('pending')], { + description: '用户状态筛选', + examples: ['active', 'inactive', 'pending'], + }), + ), + gender: t.Optional( + t.Union([t.Literal(0), t.Literal(1), t.Literal(2), t.Literal('0'), t.Literal('1'), t.Literal('2')], { + description: '性别筛选:0-未知,1-男,2-女', + examples: [0, 1, 2], + }), + ), + isRoot: t.Optional( + t.Boolean({ + description: '是否超级管理员筛选', + examples: [true, false], + }), + ), + }), +); /** 用户列表查询参数类型 */ export type UserListQueryRequest = Static; - diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index eec93dc..1dddbc5 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -44,7 +44,7 @@ export class UserService { status: sysUsers.status, lastLoginAt: sysUsers.lastLoginAt, createdAt: sysUsers.createdAt, - updatedAt: sysUsers.updatedAt + updatedAt: sysUsers.updatedAt, }) .from(sysUsers) .where(eq(sysUsers.id, BigInt(userId))) @@ -52,28 +52,28 @@ export class UserService { if (!user || user.length === 0) { Logger.warn(`用户不存在:${userId}`); - throw new BusinessError( - `用户不存在:${userId}`, - 404 - ); + throw new BusinessError(`用户不存在:${userId}`, 404); } const userData = user[0]!; Logger.info(`获取用户信息成功:${userId} - ${userData.username}`); - return successResponse({ - id: userId, // 使用传入的字符串ID,避免精度丢失 - username: userData.username, - email: userData.email, - nickname: userData.nickname, - avatar: userData.avatar, - phone: userData.mobile, - status: userData.status, - lastLoginAt: userData.lastLoginAt || null, - createdAt: userData.createdAt, - updatedAt: userData.updatedAt - }, '获取用户信息成功'); + return successResponse( + { + id: userId, // 使用传入的字符串ID,避免精度丢失 + username: userData.username, + email: userData.email, + nickname: userData.nickname, + avatar: userData.avatar, + phone: userData.mobile, + status: userData.status, + lastLoginAt: userData.lastLoginAt || null, + createdAt: userData.createdAt, + updatedAt: userData.updatedAt, + }, + '获取用户信息成功', + ); } /** @@ -94,7 +94,7 @@ export class UserService { // 关键词搜索(用户名、邮箱模糊搜索) if (keyword) { conditions.push( - sql`(${sysUsers.username} LIKE ${`%${keyword}%`} OR ${sysUsers.email} LIKE ${`%${keyword}%`})` + sql`(${sysUsers.username} LIKE ${`%${keyword}%`} OR ${sysUsers.email} LIKE ${`%${keyword}%`})`, ); } @@ -117,10 +117,14 @@ export class UserService { conditions.push(sql`${sysUsers.deletedAt} IS NULL`); // 构建排序 - const orderBy = sortBy === 'username' ? sysUsers.username : - sortBy === 'email' ? sysUsers.email : - sortBy === 'updatedAt' ? sysUsers.updatedAt : - sysUsers.createdAt; + const orderBy = + sortBy === 'username' + ? sysUsers.username + : sortBy === 'email' + ? sysUsers.email + : sortBy === 'updatedAt' + ? sysUsers.updatedAt + : sysUsers.createdAt; const orderDirection = sortOrder === 'asc' ? asc : desc; @@ -151,7 +155,7 @@ export class UserService { failedAttempts: sysUsers.failedAttempts, isRoot: sysUsers.isRoot, createdAt: sysUsers.createdAt, - updatedAt: sysUsers.updatedAt + updatedAt: sysUsers.updatedAt, }) .from(sysUsers) .where(and(...conditions)) @@ -160,7 +164,7 @@ export class UserService { .offset((page - 1) * pageSize); // 转换数据格式 - const userList: UserListItem[] = users.map(user => ({ + const userList: UserListItem[] = users.map((user) => ({ id: user.id!.toString(), // 确保ID以字符串形式返回 username: user.username, email: user.email, @@ -177,7 +181,7 @@ export class UserService { failedAttempts: user.failedAttempts, isRoot: user.isRoot === 1, createdAt: user.createdAt, - updatedAt: user.updatedAt + updatedAt: user.updatedAt, })); // 计算分页信息 @@ -185,12 +189,15 @@ export class UserService { Logger.info(`获取用户列表成功:总数${total},当前页${page},每页${pageSize}条`); - return successResponse({ - ...pagination, - data: userList - }, '获取用户列表成功'); + return successResponse( + { + ...pagination, + data: userList, + }, + '获取用户列表成功', + ); } } // 导出单例实例 -export const userService = new UserService(); \ No newline at end of file +export const userService = new UserService(); diff --git a/src/modules/user/user.test.md b/src/modules/user/user.test.md index 23a6965..9bccab0 100644 --- a/src/modules/user/user.test.md +++ b/src/modules/user/user.test.md @@ -18,122 +18,127 @@ #### 1.1 正常流程测试 **测试用例**: 成功获取当前用户信息 + - **前置条件**: 用户已登录,有有效的JWT Token - **测试步骤**: - 1. 发送GET请求到 `/api/users/me` - 2. 在Authorization header中携带有效的JWT Token + 1. 发送GET请求到 `/api/users/me` + 2. 在Authorization header中携带有效的JWT Token - **预期结果**: - - 状态码: 200 - - 响应格式: - ```json - { - "code": 200, - "message": "获取用户信息成功", - "data": { - "id": "1", - "username": "testuser", - "email": "test@example.com", - "nickname": "测试用户", - "avatar": "https://example.com/avatar.jpg", - "phone": "13800138000", - "status": "active", - "lastLoginAt": "2024-12-19T10:30:00Z", - "createdAt": "2024-12-19T10:30:00Z", - "updatedAt": "2024-12-19T10:30:00Z" - }, - "type": "SUCCESS", - "timestamp": "2024-12-19T10:30:00Z" - } - ``` + - 状态码: 200 + - 响应格式: + ```json + { + "code": 200, + "message": "获取用户信息成功", + "data": { + "id": "1", + "username": "testuser", + "email": "test@example.com", + "nickname": "测试用户", + "avatar": "https://example.com/avatar.jpg", + "phone": "13800138000", + "status": "active", + "lastLoginAt": "2024-12-19T10:30:00Z", + "createdAt": "2024-12-19T10:30:00Z", + "updatedAt": "2024-12-19T10:30:00Z" + }, + "type": "SUCCESS", + "timestamp": "2024-12-19T10:30:00Z" + } + ``` #### 1.2 认证失败测试 **测试用例**: 未提供JWT Token + - **前置条件**: 无 - **测试步骤**: - 1. 发送GET请求到 `/api/users/me` - 2. 不提供Authorization header + 1. 发送GET请求到 `/api/users/me` + 2. 不提供Authorization header - **预期结果**: - - 状态码: 401 - - 响应格式: - ```json - { - "code": 401, - "message": "未提供有效的认证令牌", - "data": null, - "type": "AUTH_ERROR", - "timestamp": "2024-12-19T10:30:00Z" - } - ``` + - 状态码: 401 + - 响应格式: + ```json + { + "code": 401, + "message": "未提供有效的认证令牌", + "data": null, + "type": "AUTH_ERROR", + "timestamp": "2024-12-19T10:30:00Z" + } + ``` **测试用例**: JWT Token无效 + - **前置条件**: 无 - **测试步骤**: - 1. 发送GET请求到 `/api/users/me` - 2. 在Authorization header中携带无效的JWT Token + 1. 发送GET请求到 `/api/users/me` + 2. 在Authorization header中携带无效的JWT Token - **预期结果**: - - 状态码: 401 - - 响应格式: - ```json - { - "code": 401, - "message": "令牌已过期", - "data": null, - "type": "AUTH_ERROR", - "timestamp": "2024-12-19T10:30:00Z" - } - ``` + - 状态码: 401 + - 响应格式: + ```json + { + "code": 401, + "message": "令牌已过期", + "data": null, + "type": "AUTH_ERROR", + "timestamp": "2024-12-19T10:30:00Z" + } + ``` #### 1.3 用户不存在测试 **测试用例**: 用户已被删除 + - **前置条件**: 用户已登录,但数据库中该用户已被删除 - **测试步骤**: - 1. 发送GET请求到 `/api/users/me` - 2. 在Authorization header中携带有效的JWT Token + 1. 发送GET请求到 `/api/users/me` + 2. 在Authorization header中携带有效的JWT Token - **预期结果**: - - 状态码: 404 - - 响应格式: - ```json - { - "code": 404, - "message": "用户不存在或已被删除", - "data": null, - "type": "NOT_FOUND", - "timestamp": "2024-12-19T10:30:00Z" - } - ``` + - 状态码: 404 + - 响应格式: + ```json + { + "code": 404, + "message": "用户不存在或已被删除", + "data": null, + "type": "NOT_FOUND", + "timestamp": "2024-12-19T10:30:00Z" + } + ``` #### 1.4 边界情况测试 **测试用例**: 用户信息字段为空 + - **前置条件**: 用户已登录,但用户信息中某些字段为空 - **测试步骤**: - 1. 发送GET请求到 `/api/users/me` - 2. 在Authorization header中携带有效的JWT Token + 1. 发送GET请求到 `/api/users/me` + 2. 在Authorization header中携带有效的JWT Token - **预期结果**: - - 状态码: 200 - - 响应中的空字段应该为null: - ```json - { - "code": 200, - "message": "获取用户信息成功", - "data": { - "id": "1", - "username": "testuser", - "email": "test@example.com", - "nickname": null, - "avatar": null, - "phone": null, - "status": "active", - "lastLoginAt": null, - "createdAt": "2024-12-19T10:30:00Z", - "updatedAt": "2024-12-19T10:30:00Z" - }, - "type": "SUCCESS", - "timestamp": "2024-12-19T10:30:00Z" - } - ``` + - 状态码: 200 + - 响应中的空字段应该为null: + ```json + { + "code": 200, + "message": "获取用户信息成功", + "data": { + "id": "1", + "username": "testuser", + "email": "test@example.com", + "nickname": null, + "avatar": null, + "phone": null, + "status": "active", + "lastLoginAt": null, + "createdAt": "2024-12-19T10:30:00Z", + "updatedAt": "2024-12-19T10:30:00Z" + }, + "type": "SUCCESS", + "timestamp": "2024-12-19T10:30:00Z" + } + ``` ## 测试数据准备 @@ -142,11 +147,11 @@ ```sql -- 插入测试用户 INSERT INTO sys_users ( - id, username, email, password_hash, salt, - nickname, avatar, phone, status, + id, username, email, password_hash, salt, + nickname, avatar, phone, status, last_login_at, created_at, updated_at ) VALUES ( - 1, 'testuser', 'test@example.com', + 1, 'testuser', 'test@example.com', 'hashed_password', 'salt_value', '测试用户', 'https://example.com/avatar.jpg', '13800138000', 'active', '2024-12-19T10:30:00Z', '2024-12-19T10:30:00Z', '2024-12-19T10:30:00Z' @@ -154,13 +159,13 @@ INSERT INTO sys_users ( -- 插入空字段测试用户 INSERT INTO sys_users ( - id, username, email, password_hash, salt, - nickname, avatar, phone, status, + id, username, email, password_hash, salt, + nickname, avatar, phone, status, last_login_at, created_at, updated_at ) VALUES ( - 2, 'emptyuser', 'empty@example.com', + 2, 'emptyuser', 'empty@example.com', 'hashed_password', 'salt_value', - NULL, NULL, NULL, 'active', + NULL, NULL, NULL, 'active', NULL, '2024-12-19T10:30:00Z', '2024-12-19T10:30:00Z' ); ``` @@ -169,11 +174,9 @@ INSERT INTO sys_users ( ```typescript // 生成测试用的JWT Token -const testToken = jwt.sign( - { userId: '1', username: 'testuser' }, - process.env.JWT_SECRET || 'test-secret', - { expiresIn: '1h' } -); +const testToken = jwt.sign({ userId: '1', username: 'testuser' }, process.env.JWT_SECRET || 'test-secret', { + expiresIn: '1h', +}); ``` ## 性能测试 @@ -242,13 +245,14 @@ describe('User API', () => { describe('GET /api/users/me', () => { it('应该成功获取当前用户信息', async () => { - const response = await app - .handle(new Request('http://localhost/api/users/me', { + const response = await app.handle( + new Request('http://localhost/api/users/me', { method: 'GET', - headers: { - 'Authorization': `Bearer ${testToken}` - } - })); + headers: { + Authorization: `Bearer ${testToken}`, + }, + }), + ); expect(response.status).toBe(200); const result = await response.json(); @@ -257,10 +261,11 @@ describe('User API', () => { }); it('应该处理认证失败', async () => { - const response = await app - .handle(new Request('http://localhost/api/users/me', { - method: 'GET' - })); + const response = await app.handle( + new Request('http://localhost/api/users/me', { + method: 'GET', + }), + ); expect(response.status).toBe(401); }); @@ -272,13 +277,13 @@ describe('User API', () => { ### 测试结果记录 -| 测试用例 | 状态 | 执行时间 | 备注 | -|---------|------|----------|------| -| 正常获取用户信息 | ✅ | 50ms | 通过 | -| 未提供Token | ✅ | 30ms | 通过 | -| Token无效 | ✅ | 35ms | 通过 | -| 用户不存在 | ✅ | 40ms | 通过 | -| 空字段处理 | ✅ | 45ms | 通过 | +| 测试用例 | 状态 | 执行时间 | 备注 | +| ---------------- | ---- | -------- | ---- | +| 正常获取用户信息 | ✅ | 50ms | 通过 | +| 未提供Token | ✅ | 30ms | 通过 | +| Token无效 | ✅ | 35ms | 通过 | +| 用户不存在 | ✅ | 40ms | 通过 | +| 空字段处理 | ✅ | 45ms | 通过 | ### 问题记录 @@ -288,4 +293,4 @@ describe('User API', () => { ## 总结 -用户模块的获取当前用户信息接口测试覆盖了正常流程、异常处理、边界情况等各个方面,确保接口的稳定性和安全性。所有测试用例均通过,可以投入生产使用。 \ No newline at end of file +用户模块的获取当前用户信息接口测试覆盖了正常流程、异常处理、边界情况等各个方面,确保接口的稳定性和安全性。所有测试用例均通过,可以投入生产使用。 diff --git a/src/plugins/drizzle/README.md b/src/plugins/drizzle/README.md index ae5c99a..9ce7480 100644 --- a/src/plugins/drizzle/README.md +++ b/src/plugins/drizzle/README.md @@ -1,121 +1,122 @@ -# Drizzle ORM 插件 - -这是一个集成到 Elysia 框架的 Drizzle ORM 插件,提供类型安全的数据库操作。 - -## 安装依赖 - -```bash -# 安装 Drizzle ORM 核心包 -bun add drizzle-orm - -# 安装 Drizzle Kit (开发工具) -bun add drizzle-kit -D -``` - -## 文件结构 - -``` -src/plugins/drizzle/ -├── drizzle.plugins.ts # 主插件文件 -├── drizzle.config.ts # Drizzle Kit 配置 -├── schema/ # 数据库表结构定义 -│ ├── index.ts # Schema 总入口 -│ └── users.ts # 用户表示例 -└── README.md # 使用说明 -``` - -## 使用方法 - -### 1. 在路由中使用数据库 - -```typescript -import { Elysia } from 'elysia'; -import { users } from '@/plugins/drizzle/schema'; - -const app = new Elysia() - .get('/users', async ({ db }) => { - // 查询所有用户 - const allUsers = await db.select().from(users); - return allUsers; - }) - .get('/users/:id', async ({ db, params }) => { - // 根据ID查询用户 - const user = await db.select() - .from(users) - .where(eq(users.id, parseInt(params.id))); - return user[0]; - }) - .post('/users', async ({ db, body }) => { - // 创建新用户 - const newUser = await db.insert(users).values(body); - return newUser; - }); -``` - -### 2. 定义新的表结构 - -在 `schema/` 目录下创建新的表文件: - -```typescript -// schema/posts.ts -import { mysqlTable, int, varchar, text, timestamp } from 'drizzle-orm/mysql-core'; -import { users } from './users'; - -export const posts = mysqlTable('posts', { - id: int('id').primaryKey().autoincrement(), - title: varchar('title', { length: 255 }).notNull(), - content: text('content'), - authorId: int('author_id').references(() => users.id), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), -}); - -export type Post = typeof posts.$inferSelect; -export type InsertPost = typeof posts.$inferInsert; -``` - -然后在 `schema/index.ts` 中导出: - -```typescript -export * from './posts'; -``` - -### 3. 生成和运行数据库迁移 - -```bash -# 生成迁移文件 -bun drizzle-kit generate - -# 推送迁移到数据库 -bun drizzle-kit push - -# 查看数据库状态 -bun drizzle-kit studio -``` - -## 配置说明 - -- **数据库连接**: 自动从 `@/config` 读取数据库配置 -- **连接池**: 默认最大连接数为 10 -- **日志**: 启用 SQL 查询日志 -- **迁移**: 迁移文件输出到 `./drizzle` 目录 - -## 类型支持 - -插件提供完整的 TypeScript 类型支持: - -- `DrizzleDB`: 数据库实例类型 -- `User`, `InsertUser`: 用户表相关类型 -- `DrizzleContext`: Elysia 上下文扩展类型 - -## 注意事项 - -1. 确保数据库配置正确且数据库服务已启动 -2. 生产环境建议使用环境变量管理数据库凭据 -3. 定期备份数据库,特别是在运行迁移之前 -4. 使用 Drizzle Studio 可视化管理数据库 - -## 相关链接 - -- [Drizzle ORM 官方文档](https://orm.drizzle.team/) -- [Drizzle Kit 文档](https://orm.drizzle.team/kit-docs/overview) \ No newline at end of file +# Drizzle ORM 插件 + +这是一个集成到 Elysia 框架的 Drizzle ORM 插件,提供类型安全的数据库操作。 + +## 安装依赖 + +```bash +# 安装 Drizzle ORM 核心包 +bun add drizzle-orm + +# 安装 Drizzle Kit (开发工具) +bun add drizzle-kit -D +``` + +## 文件结构 + +``` +src/plugins/drizzle/ +├── drizzle.plugins.ts # 主插件文件 +├── drizzle.config.ts # Drizzle Kit 配置 +├── schema/ # 数据库表结构定义 +│ ├── index.ts # Schema 总入口 +│ └── users.ts # 用户表示例 +└── README.md # 使用说明 +``` + +## 使用方法 + +### 1. 在路由中使用数据库 + +```typescript +import { Elysia } from 'elysia'; +import { users } from '@/plugins/drizzle/schema'; + +const app = new Elysia() + .get('/users', async ({ db }) => { + // 查询所有用户 + const allUsers = await db.select().from(users); + return allUsers; + }) + .get('/users/:id', async ({ db, params }) => { + // 根据ID查询用户 + const user = await db + .select() + .from(users) + .where(eq(users.id, parseInt(params.id))); + return user[0]; + }) + .post('/users', async ({ db, body }) => { + // 创建新用户 + const newUser = await db.insert(users).values(body); + return newUser; + }); +``` + +### 2. 定义新的表结构 + +在 `schema/` 目录下创建新的表文件: + +```typescript +// schema/posts.ts +import { mysqlTable, int, varchar, text, timestamp } from 'drizzle-orm/mysql-core'; +import { users } from './users'; + +export const posts = mysqlTable('posts', { + id: int('id').primaryKey().autoincrement(), + title: varchar('title', { length: 255 }).notNull(), + content: text('content'), + authorId: int('author_id').references(() => users.id), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), +}); + +export type Post = typeof posts.$inferSelect; +export type InsertPost = typeof posts.$inferInsert; +``` + +然后在 `schema/index.ts` 中导出: + +```typescript +export * from './posts'; +``` + +### 3. 生成和运行数据库迁移 + +```bash +# 生成迁移文件 +bun drizzle-kit generate + +# 推送迁移到数据库 +bun drizzle-kit push + +# 查看数据库状态 +bun drizzle-kit studio +``` + +## 配置说明 + +- **数据库连接**: 自动从 `@/config` 读取数据库配置 +- **连接池**: 默认最大连接数为 10 +- **日志**: 启用 SQL 查询日志 +- **迁移**: 迁移文件输出到 `./drizzle` 目录 + +## 类型支持 + +插件提供完整的 TypeScript 类型支持: + +- `DrizzleDB`: 数据库实例类型 +- `User`, `InsertUser`: 用户表相关类型 +- `DrizzleContext`: Elysia 上下文扩展类型 + +## 注意事项 + +1. 确保数据库配置正确且数据库服务已启动 +2. 生产环境建议使用环境变量管理数据库凭据 +3. 定期备份数据库,特别是在运行迁移之前 +4. 使用 Drizzle Studio 可视化管理数据库 + +## 相关链接 + +- [Drizzle ORM 官方文档](https://orm.drizzle.team/) +- [Drizzle Kit 文档](https://orm.drizzle.team/kit-docs/overview) diff --git a/src/plugins/drizzle/drizzle.plugins.ts b/src/plugins/drizzle/drizzle.plugins.ts index 9d1a1da..cdf2be1 100644 --- a/src/plugins/drizzle/drizzle.plugins.ts +++ b/src/plugins/drizzle/drizzle.plugins.ts @@ -1,24 +1,24 @@ -/** - * @file Drizzle ORM 数据库插件 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 集成Drizzle ORM到Elysia,提供类型安全的数据库操作 - */ - -import { Elysia } from 'elysia'; -import * as schema from '../../eneities'; -import { drizzleService } from './drizzle.service'; - -/** - * Drizzle ORM 插件 - * 提供类型安全的数据库操作接口 - */ -export const drizzlePlugin = new Elysia({ name: 'drizzle' }).onStart(async () => { - await drizzleService.initialize(); -}); - -/** 导出数据库类型,供其他模块使用 */ -export type DB = typeof schema; -export type DrizzleDB = Awaited>; +/** + * @file Drizzle ORM 数据库插件 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 集成Drizzle ORM到Elysia,提供类型安全的数据库操作 + */ + +import { Elysia } from 'elysia'; +import * as schema from '../../eneities'; +import { drizzleService } from './drizzle.service'; + +/** + * Drizzle ORM 插件 + * 提供类型安全的数据库操作接口 + */ +export const drizzlePlugin = new Elysia({ name: 'drizzle' }).onStart(async () => { + await drizzleService.initialize(); +}); + +/** 导出数据库类型,供其他模块使用 */ +export type DB = typeof schema; +export type DrizzleDB = Awaited>; diff --git a/src/plugins/drizzle/drizzle.service.ts b/src/plugins/drizzle/drizzle.service.ts index 0bf99f7..cc25894 100644 --- a/src/plugins/drizzle/drizzle.service.ts +++ b/src/plugins/drizzle/drizzle.service.ts @@ -1,459 +1,462 @@ -/** - * @file Drizzle ORM服务类 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 专业的Drizzle ORM数据库连接服务类,支持连接池管理、状态跟踪和优雅关闭 - */ - -import { drizzle } from 'drizzle-orm/mysql2'; -import mysql from 'mysql2/promise'; -import { dbConfig } from '@/config'; -import { Logger } from '@/plugins/logger/logger.service'; -import * as schema from '../../eneities'; -import type { DrizzleDB, ConnectionStatus, DatabaseConnectionInfo } from '@/type/drizzle.type'; - -/** - * Drizzle数据库服务类 - * 使用单例模式管理数据库连接 - */ -export class DrizzleService { - /** 单例实例 */ - private static instance: DrizzleService | null = null; - - /** 数据库实例 */ - private _db: DrizzleDB | null = null; - - /** 连接池实例 */ - private _connectionPool: mysql.Pool | null = null; - - /** 连接状态信息 */ - private _connectionInfo: DatabaseConnectionInfo; - - /** 初始化标志 */ - private _isInitialized = false; - - /** 连接池配置 */ - private readonly _poolConfig = { - /** 最大连接数 */ - connectionLimit: Number(process.env.DB_CONNECTION_LIMIT) || 10, - /** 队列限制 */ - queueLimit: Number(process.env.DB_QUEUE_LIMIT) || 0, - /** 等待连接 */ - waitForConnections: true, - // 启用此选项后,MySQL驱动程序将支持大数字(big numbers),这对于存储和处理 bigint 类型的数据尤为重要。 - // 如果不启用此选项,MySQL驱动程序可能无法正确处理超过 JavaScript 数字精度范围的大数值,导致数据精度丢失。 - supportBigNumbers: true, - // 启用此选项后,MySQL驱动程序将在接收 bigint 或其他大数值时,将其作为字符串返回,而不是作为JavaScript数字。 - // 这种处理方式可以避免JavaScript本身的数值精度限制问题,确保大数值在应用程序中保持精确。 - bigNumberStrings: true, - }; - - /** - * 私有构造函数,防止外部实例化 - */ - private constructor() { - this._connectionInfo = { - status: 'disconnected', - host: dbConfig.host, - port: dbConfig.port, - database: dbConfig.database, - }; - } - - /** - * 获取单例实例 - */ - public static getInstance(): DrizzleService { - if (!DrizzleService.instance) { - DrizzleService.instance = new DrizzleService(); - } - return DrizzleService.instance; - } - - /** - * 获取数据库实例 - */ - public get db(): DrizzleDB { - if (!this._db) { - throw new Error('数据库未初始化,请先调用 initialize() 方法'); - } - return this._db; - } - - /** - * 获取连接状态信息 - */ - public get connectionInfo(): DatabaseConnectionInfo { - return { ...this._connectionInfo }; - } - - /** - * 检查是否已初始化 - */ - public get isInitialized(): boolean { - return this._isInitialized; - } - - /** - * 验证数据库配置 - */ - private validateConfig(): void { - const requiredFields = ['host', 'port', 'user', 'password', 'database']; - - for (const field of requiredFields) { - if (!dbConfig[field as keyof typeof dbConfig]) { - throw new Error(`数据库配置缺少必需字段: ${field}`); - } - } - - if (dbConfig.port < 1 || dbConfig.port > 65535) { - throw new Error(`数据库端口号无效: ${dbConfig.port}`); - } - } - - /** - * 更新连接状态 - */ - private updateConnectionStatus(status: ConnectionStatus, error?: string): void { - this._connectionInfo.status = status; - this._connectionInfo.error = error; - - if (status === 'connected') { - this._connectionInfo.connectedAt = new Date(); - this._connectionInfo.error = undefined; - } - } - - /** - * 创建MySQL连接池 - */ - private async createConnection(): Promise { - try { - this.validateConfig(); - - this.updateConnectionStatus('connecting'); - - /** MySQL连接池配置 */ - const connection = mysql.createPool({ - host: dbConfig.host, - port: dbConfig.port, - user: dbConfig.user, - password: dbConfig.password, - database: dbConfig.database, - ...this._poolConfig, - }); - - // 测试连接 - const testConnection = await connection.getConnection(); - await testConnection.ping(); - testConnection.release(); - - this.updateConnectionStatus('connected'); - Logger.info({ - message: 'MySQL连接池创建成功', - host: dbConfig.host, - port: dbConfig.port, - database: dbConfig.database, - connectionLimit: this._poolConfig.connectionLimit, - }); - - return connection; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - this.updateConnectionStatus('error', errorMessage); - Logger.error(new Error(`MySQL连接池创建失败: ${errorMessage}`)); - throw new Error(`MySQL连接池创建失败: ${errorMessage}`); - } - } - - /** - * 初始化数据库连接 - */ - public async initialize(): Promise { - // 防止重复初始化 - if (this._isInitialized && this._db) { - Logger.info('Drizzle ORM 已初始化,返回现有实例'); - return this._db; - } - - try { - this._connectionPool = await this.createConnection(); - - console.log(process.env.NODE_ENV, process.env.NODE_ENV === 'development') - /** Drizzle数据库实例 */ - this._db = drizzle(this._connectionPool, { - schema, - mode: 'default', - logger: process.env.NODE_ENV === 'development' ? { - logQuery: (query, params) => { - Logger.debug({ - type: 'SQL_QUERY', - query: query.replace(/\s+/g, ' ').trim(), - params: params, - }); - }, - } : false, - }); - - this._isInitialized = true; - Logger.info({ - message: 'Drizzle ORM 初始化成功', - schema: Object.keys(schema).length > 0 ? Object.keys(schema) : ['无schema'], - loggerEnabled: process.env.NODE_ENV === 'development', - }); - - return this._db; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - this.updateConnectionStatus('error', errorMessage); - Logger.error(new Error(`Drizzle ORM 初始化失败: ${errorMessage}`)); - throw new Error(`Drizzle ORM 初始化失败: ${errorMessage}`); - } - } - - /** - * 检查数据库连接状态 - */ - public async checkConnection(): Promise { - try { - if (!this._connectionPool) { - return false; - } - - const connection = await this._connectionPool.getConnection(); - await connection.ping(); - connection.release(); - - return true; - } catch (error) { - Logger.error(error instanceof Error ? error : new Error('数据库连接检查失败')); - return false; - } - } - - /** - * 优雅关闭数据库连接 - */ - public async close(): Promise { - try { - if (this._connectionPool) { - await this._connectionPool.end(); - this._connectionPool = null; - this._db = null; - this.updateConnectionStatus('disconnected'); - this._isInitialized = false; - Logger.info('数据库连接已关闭'); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - Logger.error(new Error(`关闭数据库连接时出错: ${errorMessage}`)); - throw new Error(`关闭数据库连接失败: ${errorMessage}`); - } - } - - /** - * 重新连接数据库 - */ - public async reconnect(): Promise { - Logger.info('正在重新连接数据库...'); - - // 先关闭现有连接 - await this.close(); - - // 重新初始化连接 - return await this.initialize(); - } - - /** - * 获取连接池统计信息 - */ - public getPoolStats(): { - connectionLimit: number; - queueLimit: number; - waitForConnections: boolean; - } | null { - if (!this._connectionPool) { - return null; - } - - return { - connectionLimit: this._poolConfig.connectionLimit, - queueLimit: this._poolConfig.queueLimit, - waitForConnections: this._poolConfig.waitForConnections, - }; - } - - /** - * 执行健康检查 - */ - public async healthCheck(): Promise<{ - status: 'healthy' | 'unhealthy'; - connectionInfo: DatabaseConnectionInfo; - isConnected: boolean; - poolStats?: ReturnType; - }> { - const isConnected = await this.checkConnection(); - - return { - status: isConnected ? 'healthy' : 'unhealthy', - connectionInfo: this.connectionInfo, - isConnected, - poolStats: this.getPoolStats(), - }; - } -} - -/** - * ============================================== - * 主要导出 - 推荐使用的API - * ============================================== - */ - -/** - * Drizzle服务单例实例 - * - * @description 获取DrizzleService的单例实例,推荐的使用方式 - * @example - * ```typescript - * import { drizzleService } from '@/plugins/drizzle/drizzle.service'; - * - * // 初始化数据库 - * await drizzleService.initialize(); - * - * // 获取数据库实例 - * const database = drizzleService.db; - * - * // 检查连接状态 - * const isConnected = await drizzleService.checkConnection(); - * ``` - */ -export const drizzleService = DrizzleService.getInstance(); - -/** - * ============================================== - * 向后兼容导出 - 保持原有函数式API - * ============================================== - */ - -/** - * 创建并初始化Drizzle数据库连接 - * - * @description 向后兼容的初始化方法,内部调用drizzleService.initialize() - * @returns {Promise} 返回初始化后的Drizzle数据库实例 - * - * @example - * ```typescript - * import { createDrizzleDB } from '@/plugins/drizzle/drizzle.service'; - * - * const database = await createDrizzleDB(); - * ``` - * - * @deprecated 推荐使用 drizzleService.initialize() 替代 - */ -export const createDrizzleDB = () => drizzleService.initialize(); - -/** - * 获取数据库连接状态信息 - * - * @description 向后兼容的状态获取方法,内部调用drizzleService.connectionInfo - * @returns {DatabaseConnectionInfo} 返回数据库连接状态信息 - * - * @example - * ```typescript - * import { getConnectionInfo } from '@/plugins/drizzle/drizzle.service'; - * - * const info = getConnectionInfo(); - * console.log(`数据库状态: ${info.status}`); - * ``` - * - * @deprecated 推荐使用 drizzleService.connectionInfo 替代 - */ -export const getConnectionInfo = () => drizzleService.connectionInfo; - -/** - * 检查数据库连接状态 - * - * @description 向后兼容的连接检查方法,内部调用drizzleService.checkConnection() - * @returns {Promise} 返回连接是否正常 - * - * @example - * ```typescript - * import { checkConnection } from '@/plugins/drizzle/drizzle.service'; - * - * const isConnected = await checkConnection(); - * if (!isConnected) { - * console.log('数据库连接异常'); - * } - * ``` - * - * @deprecated 推荐使用 drizzleService.checkConnection() 替代 - */ -export const checkConnection = () => drizzleService.checkConnection(); - -/** - * 优雅关闭数据库连接 - * - * @description 向后兼容的连接关闭方法,内部调用drizzleService.close() - * @returns {Promise} 返回关闭操作的Promise - * - * @example - * ```typescript - * import { closeDrizzleDB } from '@/plugins/drizzle/drizzle.service'; - * - * // 应用关闭时清理资源 - * process.on('SIGTERM', async () => { - * await closeDrizzleDB(); - * process.exit(0); - * }); - * ``` - * - * @deprecated 推荐使用 drizzleService.close() 替代 - */ -export const closeDrizzleDB = () => drizzleService.close(); - -/** - * 重新连接数据库 - * - * @description 向后兼容的重连方法,内部调用drizzleService.reconnect() - * @returns {Promise} 返回重新连接后的数据库实例 - * - * @example - * ```typescript - * import { reconnectDB } from '@/plugins/drizzle/drizzle.service'; - * - * try { - * const database = await reconnectDB(); - * console.log('数据库重连成功'); - * } catch (error) { - * console.error('数据库重连失败:', error); - * } - * ``` - * - * @deprecated 推荐使用 drizzleService.reconnect() 替代 - */ -export const reconnectDB = () => drizzleService.reconnect(); - -/** - * 获取数据库实例 - * - * @description 向后兼容的数据库实例获取方法,内部调用drizzleService.db - * @returns {DrizzleDB} 返回Drizzle数据库实例 - * @throws {Error} 如果数据库未初始化则抛出错误 - * - * @example - * ```typescript - * import { db } from '@/plugins/drizzle/drizzle.service'; - * - * // 确保先初始化 - * await createDrizzleDB(); - * - * // 获取数据库实例 - * const database = db(); - * const users = await database.select().from(usersTable); - * ``` - * - * @deprecateds 推荐使用 drizzleService.db 替代 - */ -export const db = () => drizzleService.db; \ No newline at end of file +/** + * @file Drizzle ORM服务类 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 专业的Drizzle ORM数据库连接服务类,支持连接池管理、状态跟踪和优雅关闭 + */ + +import { drizzle } from 'drizzle-orm/mysql2'; +import mysql from 'mysql2/promise'; +import { dbConfig } from '@/config'; +import { Logger } from '@/plugins/logger/logger.service'; +import * as schema from '../../eneities'; +import type { DrizzleDB, ConnectionStatus, DatabaseConnectionInfo } from '@/type/drizzle.type'; + +/** + * Drizzle数据库服务类 + * 使用单例模式管理数据库连接 + */ +export class DrizzleService { + /** 单例实例 */ + private static instance: DrizzleService | null = null; + + /** 数据库实例 */ + private _db: DrizzleDB | null = null; + + /** 连接池实例 */ + private _connectionPool: mysql.Pool | null = null; + + /** 连接状态信息 */ + private _connectionInfo: DatabaseConnectionInfo; + + /** 初始化标志 */ + private _isInitialized = false; + + /** 连接池配置 */ + private readonly _poolConfig = { + /** 最大连接数 */ + connectionLimit: Number(process.env.DB_CONNECTION_LIMIT) || 10, + /** 队列限制 */ + queueLimit: Number(process.env.DB_QUEUE_LIMIT) || 0, + /** 等待连接 */ + waitForConnections: true, + // 启用此选项后,MySQL驱动程序将支持大数字(big numbers),这对于存储和处理 bigint 类型的数据尤为重要。 + // 如果不启用此选项,MySQL驱动程序可能无法正确处理超过 JavaScript 数字精度范围的大数值,导致数据精度丢失。 + supportBigNumbers: true, + // 启用此选项后,MySQL驱动程序将在接收 bigint 或其他大数值时,将其作为字符串返回,而不是作为JavaScript数字。 + // 这种处理方式可以避免JavaScript本身的数值精度限制问题,确保大数值在应用程序中保持精确。 + bigNumberStrings: true, + }; + + /** + * 私有构造函数,防止外部实例化 + */ + private constructor() { + this._connectionInfo = { + status: 'disconnected', + host: dbConfig.host, + port: dbConfig.port, + database: dbConfig.database, + }; + } + + /** + * 获取单例实例 + */ + public static getInstance(): DrizzleService { + if (!DrizzleService.instance) { + DrizzleService.instance = new DrizzleService(); + } + return DrizzleService.instance; + } + + /** + * 获取数据库实例 + */ + public get db(): DrizzleDB { + if (!this._db) { + throw new Error('数据库未初始化,请先调用 initialize() 方法'); + } + return this._db; + } + + /** + * 获取连接状态信息 + */ + public get connectionInfo(): DatabaseConnectionInfo { + return { ...this._connectionInfo }; + } + + /** + * 检查是否已初始化 + */ + public get isInitialized(): boolean { + return this._isInitialized; + } + + /** + * 验证数据库配置 + */ + private validateConfig(): void { + const requiredFields = ['host', 'port', 'user', 'password', 'database']; + + for (const field of requiredFields) { + if (!dbConfig[field as keyof typeof dbConfig]) { + throw new Error(`数据库配置缺少必需字段: ${field}`); + } + } + + if (dbConfig.port < 1 || dbConfig.port > 65535) { + throw new Error(`数据库端口号无效: ${dbConfig.port}`); + } + } + + /** + * 更新连接状态 + */ + private updateConnectionStatus(status: ConnectionStatus, error?: string): void { + this._connectionInfo.status = status; + this._connectionInfo.error = error; + + if (status === 'connected') { + this._connectionInfo.connectedAt = new Date(); + this._connectionInfo.error = undefined; + } + } + + /** + * 创建MySQL连接池 + */ + private async createConnection(): Promise { + try { + this.validateConfig(); + + this.updateConnectionStatus('connecting'); + + /** MySQL连接池配置 */ + const connection = mysql.createPool({ + host: dbConfig.host, + port: dbConfig.port, + user: dbConfig.user, + password: dbConfig.password, + database: dbConfig.database, + ...this._poolConfig, + }); + + // 测试连接 + const testConnection = await connection.getConnection(); + await testConnection.ping(); + testConnection.release(); + + this.updateConnectionStatus('connected'); + Logger.info({ + message: 'MySQL连接池创建成功', + host: dbConfig.host, + port: dbConfig.port, + database: dbConfig.database, + connectionLimit: this._poolConfig.connectionLimit, + }); + + return connection; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.updateConnectionStatus('error', errorMessage); + Logger.error(new Error(`MySQL连接池创建失败: ${errorMessage}`)); + throw new Error(`MySQL连接池创建失败: ${errorMessage}`); + } + } + + /** + * 初始化数据库连接 + */ + public async initialize(): Promise { + // 防止重复初始化 + if (this._isInitialized && this._db) { + Logger.info('Drizzle ORM 已初始化,返回现有实例'); + return this._db; + } + + try { + this._connectionPool = await this.createConnection(); + + console.log(process.env.NODE_ENV, process.env.NODE_ENV === 'development'); + /** Drizzle数据库实例 */ + this._db = drizzle(this._connectionPool, { + schema, + mode: 'default', + logger: + process.env.NODE_ENV === 'development' + ? { + logQuery: (query, params) => { + Logger.debug({ + type: 'SQL_QUERY', + query: query.replace(/\s+/g, ' ').trim(), + params: params, + }); + }, + } + : false, + }); + + this._isInitialized = true; + Logger.info({ + message: 'Drizzle ORM 初始化成功', + schema: Object.keys(schema).length > 0 ? Object.keys(schema) : ['无schema'], + loggerEnabled: process.env.NODE_ENV === 'development', + }); + + return this._db; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.updateConnectionStatus('error', errorMessage); + Logger.error(new Error(`Drizzle ORM 初始化失败: ${errorMessage}`)); + throw new Error(`Drizzle ORM 初始化失败: ${errorMessage}`); + } + } + + /** + * 检查数据库连接状态 + */ + public async checkConnection(): Promise { + try { + if (!this._connectionPool) { + return false; + } + + const connection = await this._connectionPool.getConnection(); + await connection.ping(); + connection.release(); + + return true; + } catch (error) { + Logger.error(error instanceof Error ? error : new Error('数据库连接检查失败')); + return false; + } + } + + /** + * 优雅关闭数据库连接 + */ + public async close(): Promise { + try { + if (this._connectionPool) { + await this._connectionPool.end(); + this._connectionPool = null; + this._db = null; + this.updateConnectionStatus('disconnected'); + this._isInitialized = false; + Logger.info('数据库连接已关闭'); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.error(new Error(`关闭数据库连接时出错: ${errorMessage}`)); + throw new Error(`关闭数据库连接失败: ${errorMessage}`); + } + } + + /** + * 重新连接数据库 + */ + public async reconnect(): Promise { + Logger.info('正在重新连接数据库...'); + + // 先关闭现有连接 + await this.close(); + + // 重新初始化连接 + return await this.initialize(); + } + + /** + * 获取连接池统计信息 + */ + public getPoolStats(): { + connectionLimit: number; + queueLimit: number; + waitForConnections: boolean; + } | null { + if (!this._connectionPool) { + return null; + } + + return { + connectionLimit: this._poolConfig.connectionLimit, + queueLimit: this._poolConfig.queueLimit, + waitForConnections: this._poolConfig.waitForConnections, + }; + } + + /** + * 执行健康检查 + */ + public async healthCheck(): Promise<{ + status: 'healthy' | 'unhealthy'; + connectionInfo: DatabaseConnectionInfo; + isConnected: boolean; + poolStats?: ReturnType; + }> { + const isConnected = await this.checkConnection(); + + return { + status: isConnected ? 'healthy' : 'unhealthy', + connectionInfo: this.connectionInfo, + isConnected, + poolStats: this.getPoolStats(), + }; + } +} + +/** + * ============================================== + * 主要导出 - 推荐使用的API + * ============================================== + */ + +/** + * Drizzle服务单例实例 + * + * @description 获取DrizzleService的单例实例,推荐的使用方式 + * @example + * ```typescript + * import { drizzleService } from '@/plugins/drizzle/drizzle.service'; + * + * // 初始化数据库 + * await drizzleService.initialize(); + * + * // 获取数据库实例 + * const database = drizzleService.db; + * + * // 检查连接状态 + * const isConnected = await drizzleService.checkConnection(); + * ``` + */ +export const drizzleService = DrizzleService.getInstance(); + +/** + * ============================================== + * 向后兼容导出 - 保持原有函数式API + * ============================================== + */ + +/** + * 创建并初始化Drizzle数据库连接 + * + * @description 向后兼容的初始化方法,内部调用drizzleService.initialize() + * @returns {Promise} 返回初始化后的Drizzle数据库实例 + * + * @example + * ```typescript + * import { createDrizzleDB } from '@/plugins/drizzle/drizzle.service'; + * + * const database = await createDrizzleDB(); + * ``` + * + * @deprecated 推荐使用 drizzleService.initialize() 替代 + */ +export const createDrizzleDB = () => drizzleService.initialize(); + +/** + * 获取数据库连接状态信息 + * + * @description 向后兼容的状态获取方法,内部调用drizzleService.connectionInfo + * @returns {DatabaseConnectionInfo} 返回数据库连接状态信息 + * + * @example + * ```typescript + * import { getConnectionInfo } from '@/plugins/drizzle/drizzle.service'; + * + * const info = getConnectionInfo(); + * console.log(`数据库状态: ${info.status}`); + * ``` + * + * @deprecated 推荐使用 drizzleService.connectionInfo 替代 + */ +export const getConnectionInfo = () => drizzleService.connectionInfo; + +/** + * 检查数据库连接状态 + * + * @description 向后兼容的连接检查方法,内部调用drizzleService.checkConnection() + * @returns {Promise} 返回连接是否正常 + * + * @example + * ```typescript + * import { checkConnection } from '@/plugins/drizzle/drizzle.service'; + * + * const isConnected = await checkConnection(); + * if (!isConnected) { + * console.log('数据库连接异常'); + * } + * ``` + * + * @deprecated 推荐使用 drizzleService.checkConnection() 替代 + */ +export const checkConnection = () => drizzleService.checkConnection(); + +/** + * 优雅关闭数据库连接 + * + * @description 向后兼容的连接关闭方法,内部调用drizzleService.close() + * @returns {Promise} 返回关闭操作的Promise + * + * @example + * ```typescript + * import { closeDrizzleDB } from '@/plugins/drizzle/drizzle.service'; + * + * // 应用关闭时清理资源 + * process.on('SIGTERM', async () => { + * await closeDrizzleDB(); + * process.exit(0); + * }); + * ``` + * + * @deprecated 推荐使用 drizzleService.close() 替代 + */ +export const closeDrizzleDB = () => drizzleService.close(); + +/** + * 重新连接数据库 + * + * @description 向后兼容的重连方法,内部调用drizzleService.reconnect() + * @returns {Promise} 返回重新连接后的数据库实例 + * + * @example + * ```typescript + * import { reconnectDB } from '@/plugins/drizzle/drizzle.service'; + * + * try { + * const database = await reconnectDB(); + * console.log('数据库重连成功'); + * } catch (error) { + * console.error('数据库重连失败:', error); + * } + * ``` + * + * @deprecated 推荐使用 drizzleService.reconnect() 替代 + */ +export const reconnectDB = () => drizzleService.reconnect(); + +/** + * 获取数据库实例 + * + * @description 向后兼容的数据库实例获取方法,内部调用drizzleService.db + * @returns {DrizzleDB} 返回Drizzle数据库实例 + * @throws {Error} 如果数据库未初始化则抛出错误 + * + * @example + * ```typescript + * import { db } from '@/plugins/drizzle/drizzle.service'; + * + * // 确保先初始化 + * await createDrizzleDB(); + * + * // 获取数据库实例 + * const database = db(); + * const users = await database.select().from(usersTable); + * ``` + * + * @deprecateds 推荐使用 drizzleService.db 替代 + */ +export const db = () => drizzleService.db; diff --git a/src/plugins/email/README.md b/src/plugins/email/README.md index a45fbe6..4695303 100644 --- a/src/plugins/email/README.md +++ b/src/plugins/email/README.md @@ -43,9 +43,8 @@ EMAIL_RETRY_DELAY=5000 import { Elysia } from 'elysia'; import { emailPlugin } from '@/plugins/email/email.plugins'; -const app = new Elysia() - .use(emailPlugin) - // ... 其他插件 +const app = new Elysia().use(emailPlugin); +// ... 其他插件 ``` ## 使用方法 @@ -156,6 +155,7 @@ app.get('/email/health', async ({ emailService }) => { 发送自定义邮件。 **参数:** + - `options.to`: 收件人邮箱(字符串或数组) - `options.subject`: 邮件主题 - `options.html`: HTML 内容 @@ -170,6 +170,7 @@ app.get('/email/health', async ({ emailService }) => { 发送模板邮件。 **参数:** + - `options.to`: 收件人邮箱 - `options.template`: 模板类型 - `options.params`: 模板参数 @@ -214,4 +215,4 @@ app.get('/email/health', async ({ emailService }) => { 2. **端口配置**: 根据邮件提供商配置正确的端口和 SSL 设置 3. **发送限制**: 注意邮件提供商的发送频率限制 4. **模板自定义**: 可以通过修改服务类来自定义邮件模板 -5. **安全性**: 避免在邮件内容中包含敏感信息 \ No newline at end of file +5. **安全性**: 避免在邮件内容中包含敏感信息 diff --git a/src/plugins/email/email.plugins.ts b/src/plugins/email/email.plugins.ts index 0d61a80..e9805be 100644 --- a/src/plugins/email/email.plugins.ts +++ b/src/plugins/email/email.plugins.ts @@ -129,11 +129,7 @@ export const emailPlugin = new Elysia({ name: 'email' }) /** * 发送欢迎邮件 */ - async sendWelcomeEmail(options: { - to: string; - username: string; - nickname?: string; - }): Promise { + async sendWelcomeEmail(options: { to: string; username: string; nickname?: string }): Promise { return await emailService.sendTemplateEmail({ to: options.to, template: 'welcome', @@ -221,11 +217,7 @@ export type EmailServiceDecorator = { resetCode: string; expireTime: string; }): Promise; - sendWelcomeEmail(options: { - to: string; - username: string; - nickname?: string; - }): Promise; + sendWelcomeEmail(options: { to: string; username: string; nickname?: string }): Promise; sendPasswordChangedEmail(options: { to: string; username: string; @@ -239,4 +231,4 @@ export type EmailServiceDecorator = { subject?: string; }): Promise; }; -}; \ No newline at end of file +}; diff --git a/src/plugins/email/email.service.ts b/src/plugins/email/email.service.ts index d029ff8..fbd3a4f 100644 --- a/src/plugins/email/email.service.ts +++ b/src/plugins/email/email.service.ts @@ -107,7 +107,7 @@ export class EmailService { private updateStatus( status: EmailServiceStatus['status'], transporterStatus: EmailServiceStatus['transporterStatus'], - error?: string + error?: string, ): void { this._status = { status, @@ -189,11 +189,9 @@ export class EmailService { if (!fromAddress) { throw new Error('发件人邮箱地址不能为空,请检查SMTP_USER或SMTP_FROM_EMAIL环境变量'); } - + const fromName = emailConfig.fromName || '星撰系统'; - const formattedFrom = fromAddress.includes('<') - ? fromAddress - : `"${fromName}" <${fromAddress}>`; + const formattedFrom = fromAddress.includes('<') ? fromAddress : `"${fromName}" <${fromAddress}>`; const mailOptions = { from: formattedFrom, @@ -263,7 +261,7 @@ export class EmailService { } // 等待重试延迟 - await new Promise(resolve => setTimeout(resolve, emailOptions.retryDelay)); + await new Promise((resolve) => setTimeout(resolve, emailOptions.retryDelay)); } } @@ -326,7 +324,7 @@ export class EmailService { */ private renderTemplate( templateType: EmailTemplateType, - params: EmailTemplateParams + params: EmailTemplateParams, ): { subject: string; html: string; text: string } { const template = emailTemplates[templateType]; const defaultParams = { @@ -617,4 +615,4 @@ export const checkEmailServiceHealth = () => emailService.healthCheck(); export const closeEmailService = () => emailService.close(); // 导出服务实例 -export { emailService }; \ No newline at end of file +export { emailService }; diff --git a/src/plugins/errorHandle/errorHandler.plugins.ts b/src/plugins/errorHandle/errorHandler.plugins.ts index bd18afd..a3317c0 100644 --- a/src/plugins/errorHandle/errorHandler.plugins.ts +++ b/src/plugins/errorHandle/errorHandler.plugins.ts @@ -83,6 +83,14 @@ export const errorHandlerPlugin = (app: Elysia) => errors: error.message, }; } + case 409: { + set.status = code; + return { + code: error.code, + message: '数据已存在,请勿重复操作', + errors: error.message, + }; + } default: { // 处理 ElysiaCustomStatusResponse status抛出的异常 if (error?.constructor?.name === 'ElysiaCustomStatusResponse') { @@ -94,7 +102,13 @@ export const errorHandlerPlugin = (app: Elysia) => }; } - console.log('error ==================== \n', error, `\n ==================== error \n`, code, 'code =============='); + console.log( + 'error ==================== \n', + error, + '\n ==================== error \n', + code, + 'code ==============', + ); set.status = 500; Logger.error(error as Error); return { diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 354f7ac..977bae8 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -1,31 +1,31 @@ -/** - * @file Plugins 插件总入口 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 所有插件的统一入口,使用 group 进行模块化管理 - */ - -import { Elysia } from 'elysia'; -import { loggerPlugin } from '@/plugins/logger/logger.plugins'; -import { errorHandlerPlugin } from '@/plugins/errorHandle/errorHandler.plugins'; -import { swaggerPlugin } from '@/plugins/swagger/swagger.plugins'; -import { drizzlePlugin } from '@/plugins/drizzle/drizzle.plugins'; -import { redisPlugin } from '@/plugins/redis/redis.plugins'; -import { emailPlugin } from '@/plugins/email/email.plugins'; - -export const plugins = (app: Elysia) => - app - // 日志插件 - .use(loggerPlugin) - // 错误处理插件 - .use(errorHandlerPlugin) - // 数据库插件 - .use(drizzlePlugin) - // Redis插件 - .use(redisPlugin) - // 邮件服务插件 - .use(emailPlugin) - // API 文档插件 - .use(swaggerPlugin); +/** + * @file Plugins 插件总入口 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 所有插件的统一入口,使用 group 进行模块化管理 + */ + +import { Elysia } from 'elysia'; +import { loggerPlugin } from '@/plugins/logger/logger.plugins'; +import { errorHandlerPlugin } from '@/plugins/errorHandle/errorHandler.plugins'; +import { swaggerPlugin } from '@/plugins/swagger/swagger.plugins'; +import { drizzlePlugin } from '@/plugins/drizzle/drizzle.plugins'; +import { redisPlugin } from '@/plugins/redis/redis.plugins'; +import { emailPlugin } from '@/plugins/email/email.plugins'; + +export const plugins = (app: Elysia) => + app + // 日志插件 + .use(loggerPlugin) + // 错误处理插件 + .use(errorHandlerPlugin) + // 数据库插件 + .use(drizzlePlugin) + // Redis插件 + .use(redisPlugin) + // 邮件服务插件 + .use(emailPlugin) + // API 文档插件 + .use(swaggerPlugin); diff --git a/src/plugins/jwt/jwt.plugins.ts b/src/plugins/jwt/jwt.plugins.ts index 1b9d095..d88353d 100644 --- a/src/plugins/jwt/jwt.plugins.ts +++ b/src/plugins/jwt/jwt.plugins.ts @@ -29,7 +29,7 @@ export const jwtAuthPlugin = (app: Elysia) => if (payload.type !== TOKEN_TYPES.ACCESS && ENV === 'production') { return status(401, 'Token无效'); } - + // 提取用户信息 const user: JwtUserType = { userId: payload.userId, diff --git a/src/plugins/jwt/jwt.service.ts b/src/plugins/jwt/jwt.service.ts index e9702fb..c9d6fc9 100644 --- a/src/plugins/jwt/jwt.service.ts +++ b/src/plugins/jwt/jwt.service.ts @@ -1,102 +1,105 @@ -/** - * @file JWT服务类 - 原生版 - * @author AI Assistant - * @date 2025-01-07 - * @description 使用原生jsonwebtoken库提供JWT功能 - */ - -import jwt from 'jsonwebtoken'; -import { jwtConfig } from '@/config'; -import { TOKEN_TYPES, type JwtPayloadType } from '@/type/jwt.type'; - -/** - * JWT服务类 - 原生版 - */ -export class JwtService { - /** - * 生成激活Token - */ - generateActivationToken(userId: string, email: string, username: string){ - return jwt.sign( - { - userId, - email, - username, - type: TOKEN_TYPES.ACTIVATION, - }, - jwtConfig.secret, - { expiresIn: '1D' } - ); - } - - /** - * 生成登录Token对 - */ - generateTokens(userInfo: { - id: string; - username: string; - email: string; - nickname?: string; - status: string; - }, rememberMe = false) { - const userPayload = { - userId: userInfo.id, - username: userInfo.username, - email: userInfo.email, - nickname: userInfo.nickname, - status: userInfo.status, - }; - const accessToken = jwt.sign( - { - ...userPayload, - type: TOKEN_TYPES.ACCESS, - }, - jwtConfig.secret, - { expiresIn: '20M' } - ) - - const refreshToken = jwt.sign( - { - ...userPayload, - type: TOKEN_TYPES.REFRESH, - }, - jwtConfig.secret, - { expiresIn: '14D' } - ) - - return { - accessToken, - refreshToken, - tokenType: 'Bearer', - expiresIn: '20M', - refreshExpiresIn: '14D', - }; - } - - /** - * 验证激活Token - */ - verifyToken(token: string) { - try { - return jwt.verify(token, jwtConfig.secret) as JwtPayloadType - } catch { - return { error: true } as JwtPayloadType; - } - } - - /** - * 生成重置密码Token - */ - generateResetToken(userId: string) { - return jwt.sign( - { - userId, - type: TOKEN_TYPES.PASSWORD_RESET, - }, - jwtConfig.secret, - { expiresIn: '30M' } - ); - } -} - -export const jwtService = new JwtService(); \ No newline at end of file +/** + * @file JWT服务类 - 原生版 + * @author AI Assistant + * @date 2025-01-07 + * @description 使用原生jsonwebtoken库提供JWT功能 + */ + +import jwt from 'jsonwebtoken'; +import { jwtConfig } from '@/config'; +import { TOKEN_TYPES, type JwtPayloadType } from '@/type/jwt.type'; + +/** + * JWT服务类 - 原生版 + */ +export class JwtService { + /** + * 生成激活Token + */ + generateActivationToken(userId: string, email: string, username: string) { + return jwt.sign( + { + userId, + email, + username, + type: TOKEN_TYPES.ACTIVATION, + }, + jwtConfig.secret, + { expiresIn: '1D' }, + ); + } + + /** + * 生成登录Token对 + */ + generateTokens( + userInfo: { + id: string; + username: string; + email: string; + nickname?: string; + status: string; + }, + rememberMe = false, + ) { + const userPayload = { + userId: userInfo.id, + username: userInfo.username, + email: userInfo.email, + nickname: userInfo.nickname, + status: userInfo.status, + }; + const accessToken = jwt.sign( + { + ...userPayload, + type: TOKEN_TYPES.ACCESS, + }, + jwtConfig.secret, + { expiresIn: '20M' }, + ); + + const refreshToken = jwt.sign( + { + ...userPayload, + type: TOKEN_TYPES.REFRESH, + }, + jwtConfig.secret, + { expiresIn: '14D' }, + ); + + return { + accessToken, + refreshToken, + tokenType: 'Bearer', + expiresIn: '20M', + refreshExpiresIn: '14D', + }; + } + + /** + * 验证激活Token + */ + verifyToken(token: string) { + try { + return jwt.verify(token, jwtConfig.secret) as JwtPayloadType; + } catch { + return { error: true } as JwtPayloadType; + } + } + + /** + * 生成重置密码Token + */ + generateResetToken(userId: string) { + return jwt.sign( + { + userId, + type: TOKEN_TYPES.PASSWORD_RESET, + }, + jwtConfig.secret, + { expiresIn: '30M' }, + ); + } +} + +export const jwtService = new JwtService(); diff --git a/src/plugins/logger/logger.plugins.ts b/src/plugins/logger/logger.plugins.ts index 5ea1cc3..b65e84c 100644 --- a/src/plugins/logger/logger.plugins.ts +++ b/src/plugins/logger/logger.plugins.ts @@ -26,7 +26,7 @@ export const loggerPlugin = (app: Elysia) => .state('requestStart', null as [number, number] | null) .state('color', null as string | null) /** 请求拦截器 - 记录请求信息 */ - .onRequest(({ store, request, server, path,}) => { + .onRequest(({ store, request, server, path }) => { /** 记录请求开始时间 */ store.requestStart = process.hrtime(); /** 获取客户端IP信息 */ diff --git a/src/plugins/logger/logger.service.ts b/src/plugins/logger/logger.service.ts index 8ee7ef0..2b2ecb5 100644 --- a/src/plugins/logger/logger.service.ts +++ b/src/plugins/logger/logger.service.ts @@ -1,260 +1,259 @@ -/** - * @file Winston日志器工具类 - * @author hotok - * @date 2025-06-28 - * @lastEditor hotok - * @lastEditTime 2025-06-28 - * @description 基于winston的高性能日志记录器,支持分环境输出、按日期轮转、彩色美化 - */ - -import winston, { log } from 'winston'; -import DailyRotateFile from 'winston-daily-rotate-file'; -import { loggerConfig } from '@/config/logger.config'; -import chalk from 'chalk'; -import { centerText } from '@/utils/text'; -import { formatFileSize } from '@/utils/formatFileSize'; - -/** - * 日志等级颜色格式化方法 - */ -const colorMethods = { - error: (msg: string) => chalk.bgRed.white(msg), - warn: (msg: string) => chalk.bgYellow.black(msg), - info: (msg: string) => chalk.bgGreen(msg), - http: (msg: string) => chalk.bgCyan(msg), - verbose: (msg: string) => chalk.bgGray(msg), - debug: (msg: string) => chalk.bgMagenta(msg), - silly: (msg: string) => chalk.bgGray(msg), -}; -const colorMethodsForStart = { - error: (msg: string) => chalk.red(msg), - warn: (msg: string) => chalk.yellow(msg), - info: (msg: string) => chalk.green(msg), - http: (msg: string) => chalk.cyan(msg), - verbose: (msg: string) => chalk.gray(msg), - debug: (msg: string) => chalk.magenta(msg), - silly: (msg: string) => chalk.gray(msg), -}; - -/** - * 格式化堆栈信息,每行第一个字符用红色背景 - * @param stack 堆栈字符串 - * @returns 格式化后的堆栈字符串 - */ -const formatStack = (stack: string): string => { - return ( - chalk.red('•••') + - '\n' + - stack - .split('\n') - .map((line, index) => { - if (index === 0) return line; // 第一行是错误消息,不处理 - if (line.trim() === '') return line; // 空行不处理 - - // 为每行第一个字符添加红色背景 - const firstChar = line.charAt(0); - const restOfLine = line.slice(1); - return chalk.bgRed(' ') + firstChar + restOfLine; - }) - .join('\n') - ); -}; - -/** - * 格式化JSON信息,每行第一个字符用对应日志颜色 - * @param str JSON字符串 - * @param level 日志等级 - * @returns 格式化后的JSON字符串 - */ -const formatJSON = (str: string, level: string): string => { - if (typeof str !== 'string') { - console.log('str', str); - return JSON.stringify(str, null, 2); - } - if (!str?.includes('\n')) { - return str; - } - const color = colorMethodsForStart[level as keyof typeof colorMethods]; - return ( - '\n' + - color('|') + - str - .split('\n') - .map((line, index) => { - if (index === 0) return line; // 第一行是错误消息,不处理 - if (line.trim() === '') return line; // 空行不处理 - - // 为每行第一个字符添加红色背景 - const firstChar = line.charAt(0); - const restOfLine = line.slice(1); - return color('|') + firstChar + restOfLine; - }) - .join('\n') - ); -}; - -/** - * 格式化JSON信息,每行第一个字符用对应日志颜色 - * @param str JSON字符串 - * @param level 日志等级 - * @returns 格式化后的JSON字符串 - */ -const formatHTTP = (obj: any): string => { - if (obj.type === 'request') { - return obj.color + `|< ${obj.method} ${obj.path} ${obj.ip} ${obj.browser}`; - } else if (obj.type === 'response') { - return ( - obj.color + - `| > ${obj.method} ${obj.path} ${obj.statusCode} ${chalk.bold(obj.requestTime)} ${chalk.bgGreen(obj.responseSize)}` - ); - } -}; -/** - * 控制台日志传输器 - */ - -const consoleTransport = new winston.transports.Console({ - format: winston.format.combine( - winston.format.timestamp({ format: 'YY/MM/DD HH:mm:ss - SSS' }), - winston.format.printf(({ timestamp, message, level, stack }) => { - // 使用居中对齐格式化日志级别 - const levelText = centerText(level.toUpperCase(), 7); - const levelFormatted = colorMethods[level as keyof typeof colorMethods](levelText); - - if (level === 'error' && stack && typeof stack === 'string') { - const formattedStack = formatStack(stack); - return `[${chalk.red.bold(timestamp)}] ${levelFormatted} ${message}\n${formattedStack}`; - } else if (level === 'error') { - return `[${chalk.red.bold(timestamp)}] ${levelFormatted} 未定义的异常 ${message}`; - } else if (level === 'http') { - return `[${chalk.red.bold(timestamp)}] ${levelFormatted} ${formatHTTP(message)}`; - } else if (level === 'debug' && (message as string).includes('"type": "SQL_QUERY"')) { - const sqlLevel = colorMethods[level as keyof typeof colorMethods](centerText('=SQL='.toUpperCase(), 7)); - console.log(message); - return `[${chalk.red.bold(timestamp)}] ${sqlLevel} ${formatJSON(message as string, level)}`; - } - - return `[${chalk.red.bold(timestamp)}] ${levelFormatted} ${formatJSON(message as string, level)}`; - }), - ), -}); - -/** - * 应用主日志文件传输器 - */ -const appFileTransport = new DailyRotateFile({ - filename: `${loggerConfig.directory}/app-%DATE%.log`, - datePattern: 'YYYY-MM-DD', - maxSize: loggerConfig.maxSize, - maxFiles: loggerConfig.maxFiles, - format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.json()), -}); - -/** - * 错误专用日志文件传输器 - */ -const errorFileTransport = new DailyRotateFile({ - filename: `${loggerConfig.directory}/error-%DATE%.log`, - datePattern: 'YYYY-MM-DD', - maxSize: loggerConfig.maxSize, - maxFiles: loggerConfig.maxFiles, - level: 'error', - format: winston.format.combine( - winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - winston.format.errors({ stack: true }), // 确保堆栈信息被记录 - winston.format.json(), - ), -}); - -/** - * Winston日志器实例 - */ -const logger = winston.createLogger({ - /** 日志级别 */ - level: loggerConfig.level, - - /** 传输器配置 */ - transports: [ - // 应用主日志文件 - appFileTransport, - - // 错误专用日志文件 - errorFileTransport, - - // 控制台日志(如果启用) - ...(loggerConfig.console ? [consoleTransport] : []), - ], -}); - -/** - * 格式化日志消息,支持字符串和对象 - * @param message 日志消息,可以是字符串或对象 - * @returns 格式化后的字符串 - */ -const formatMessage = (message: string | object): string => { - if (typeof message === 'string') { - return message; - } - - return JSON.stringify(message, (_, v) => - typeof v === 'bigint' ? v.toString() : v, 2); -}; - -/** - * 日志记录器类 - */ -export class Logger { - static debug(message: string | object): void { - logger.debug(formatMessage(message)); - } - static info(message: string | object): void { - logger.info(formatMessage(message)); - } - static warn(message: string | object): void { - logger.warn(formatMessage(message)); - } - static error(error: Error): void { - logger.error({ - message: error.message, - stack: error.stack, - name: error.name, - cause: error.cause, - }); - } - static http(message: string | object): void { - logger.http(message); - } - static verbose(message: string | object): void { - logger.verbose(formatMessage(message)); - } -} -// 获取响应体大小 -export function getResponseSize(response: unknown) { - let responseSize = 0; - - if (response instanceof Response) { - // 对于 Response 对象,可以通过 headers 获取 content-length - const contentLength = response.headers.get('content-length'); - if (contentLength) { - responseSize = parseInt(contentLength, 10); - } else if (response.body) { - // 如果没有 content-length,可以尝试读取 body 大小 - // 注意:这可能会消耗 stream,需要谨慎使用 - responseSize = new Blob([response.body]).size; - } - } else if (typeof response === 'string') { - // 对于字符串响应,计算字节大小 - responseSize = new TextEncoder().encode(response).length; - } else if (response && typeof response === 'object') { - // 对于对象响应,先序列化再计算大小 - responseSize = new TextEncoder().encode(JSON.stringify(response)).length; - } else if (response instanceof File || response instanceof Blob) { - // 对于文件响应,可以直接访问 size 属性 - responseSize = response.size; - } - - return formatFileSize(responseSize); -} - -// 导出默认实例 -export default Logger; +/** + * @file Winston日志器工具类 + * @author hotok + * @date 2025-06-28 + * @lastEditor hotok + * @lastEditTime 2025-06-28 + * @description 基于winston的高性能日志记录器,支持分环境输出、按日期轮转、彩色美化 + */ + +import winston, { log } from 'winston'; +import DailyRotateFile from 'winston-daily-rotate-file'; +import { loggerConfig } from '@/config/logger.config'; +import chalk from 'chalk'; +import { centerText } from '@/utils/text'; +import { formatFileSize } from '@/utils/formatFileSize'; + +/** + * 日志等级颜色格式化方法 + */ +const colorMethods = { + error: (msg: string) => chalk.bgRed.white(msg), + warn: (msg: string) => chalk.bgYellow.black(msg), + info: (msg: string) => chalk.bgGreen(msg), + http: (msg: string) => chalk.bgCyan(msg), + verbose: (msg: string) => chalk.bgGray(msg), + debug: (msg: string) => chalk.bgMagenta(msg), + silly: (msg: string) => chalk.bgGray(msg), +}; +const colorMethodsForStart = { + error: (msg: string) => chalk.red(msg), + warn: (msg: string) => chalk.yellow(msg), + info: (msg: string) => chalk.green(msg), + http: (msg: string) => chalk.cyan(msg), + verbose: (msg: string) => chalk.gray(msg), + debug: (msg: string) => chalk.magenta(msg), + silly: (msg: string) => chalk.gray(msg), +}; + +/** + * 格式化堆栈信息,每行第一个字符用红色背景 + * @param stack 堆栈字符串 + * @returns 格式化后的堆栈字符串 + */ +const formatStack = (stack: string): string => { + return ( + chalk.red('•••') + + '\n' + + stack + .split('\n') + .map((line, index) => { + if (index === 0) return line; // 第一行是错误消息,不处理 + if (line.trim() === '') return line; // 空行不处理 + + // 为每行第一个字符添加红色背景 + const firstChar = line.charAt(0); + const restOfLine = line.slice(1); + return chalk.bgRed(' ') + firstChar + restOfLine; + }) + .join('\n') + ); +}; + +/** + * 格式化JSON信息,每行第一个字符用对应日志颜色 + * @param str JSON字符串 + * @param level 日志等级 + * @returns 格式化后的JSON字符串 + */ +const formatJSON = (str: string, level: string): string => { + if (typeof str !== 'string') { + console.log('str', str); + return JSON.stringify(str, null, 2); + } + if (!str?.includes('\n')) { + return str; + } + const color = colorMethodsForStart[level as keyof typeof colorMethods]; + return ( + '\n' + + color('|') + + str + .split('\n') + .map((line, index) => { + if (index === 0) return line; // 第一行是错误消息,不处理 + if (line.trim() === '') return line; // 空行不处理 + + // 为每行第一个字符添加红色背景 + const firstChar = line.charAt(0); + const restOfLine = line.slice(1); + return color('|') + firstChar + restOfLine; + }) + .join('\n') + ); +}; + +/** + * 格式化JSON信息,每行第一个字符用对应日志颜色 + * @param str JSON字符串 + * @param level 日志等级 + * @returns 格式化后的JSON字符串 + */ +const formatHTTP = (obj: any): string => { + if (obj.type === 'request') { + return obj.color + `|< ${obj.method} ${obj.path} ${obj.ip} ${obj.browser}`; + } else if (obj.type === 'response') { + return ( + obj.color + + `| > ${obj.method} ${obj.path} ${obj.statusCode} ${chalk.bold(obj.requestTime)} ${chalk.bgGreen(obj.responseSize)}` + ); + } +}; +/** + * 控制台日志传输器 + */ + +const consoleTransport = new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp({ format: 'YY/MM/DD HH:mm:ss - SSS' }), + winston.format.printf(({ timestamp, message, level, stack }) => { + // 使用居中对齐格式化日志级别 + const levelText = centerText(level.toUpperCase(), 7); + const levelFormatted = colorMethods[level as keyof typeof colorMethods](levelText); + + if (level === 'error' && stack && typeof stack === 'string') { + const formattedStack = formatStack(stack); + return `[${chalk.red.bold(timestamp)}] ${levelFormatted} ${message}\n${formattedStack}`; + } else if (level === 'error') { + return `[${chalk.red.bold(timestamp)}] ${levelFormatted} 未定义的异常 ${message}`; + } else if (level === 'http') { + return `[${chalk.red.bold(timestamp)}] ${levelFormatted} ${formatHTTP(message)}`; + } else if (level === 'debug' && (message as string).includes('"type": "SQL_QUERY"')) { + const sqlLevel = colorMethods[level as keyof typeof colorMethods](centerText('=SQL='.toUpperCase(), 7)); + console.log(message); + return `[${chalk.red.bold(timestamp)}] ${sqlLevel} ${formatJSON(message as string, level)}`; + } + + return `[${chalk.red.bold(timestamp)}] ${levelFormatted} ${formatJSON(message as string, level)}`; + }), + ), +}); + +/** + * 应用主日志文件传输器 + */ +const appFileTransport = new DailyRotateFile({ + filename: `${loggerConfig.directory}/app-%DATE%.log`, + datePattern: 'YYYY-MM-DD', + maxSize: loggerConfig.maxSize, + maxFiles: loggerConfig.maxFiles, + format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.json()), +}); + +/** + * 错误专用日志文件传输器 + */ +const errorFileTransport = new DailyRotateFile({ + filename: `${loggerConfig.directory}/error-%DATE%.log`, + datePattern: 'YYYY-MM-DD', + maxSize: loggerConfig.maxSize, + maxFiles: loggerConfig.maxFiles, + level: 'error', + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), // 确保堆栈信息被记录 + winston.format.json(), + ), +}); + +/** + * Winston日志器实例 + */ +const logger = winston.createLogger({ + /** 日志级别 */ + level: loggerConfig.level, + + /** 传输器配置 */ + transports: [ + // 应用主日志文件 + appFileTransport, + + // 错误专用日志文件 + errorFileTransport, + + // 控制台日志(如果启用) + ...(loggerConfig.console ? [consoleTransport] : []), + ], +}); + +/** + * 格式化日志消息,支持字符串和对象 + * @param message 日志消息,可以是字符串或对象 + * @returns 格式化后的字符串 + */ +const formatMessage = (message: string | object): string => { + if (typeof message === 'string') { + return message; + } + + return JSON.stringify(message, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2); +}; + +/** + * 日志记录器类 + */ +export class Logger { + static debug(message: string | object): void { + logger.debug(formatMessage(message)); + } + static info(message: string | object): void { + logger.info(formatMessage(message)); + } + static warn(message: string | object): void { + logger.warn(formatMessage(message)); + } + static error(error: Error): void { + logger.error({ + message: error.message, + stack: error.stack, + name: error.name, + cause: error.cause, + }); + } + static http(message: string | object): void { + logger.http(message); + } + static verbose(message: string | object): void { + logger.verbose(formatMessage(message)); + } +} +// 获取响应体大小 +export function getResponseSize(response: unknown) { + let responseSize = 0; + + if (response instanceof Response) { + // 对于 Response 对象,可以通过 headers 获取 content-length + const contentLength = response.headers.get('content-length'); + if (contentLength) { + responseSize = parseInt(contentLength, 10); + } else if (response.body) { + // 如果没有 content-length,可以尝试读取 body 大小 + // 注意:这可能会消耗 stream,需要谨慎使用 + responseSize = new Blob([response.body]).size; + } + } else if (typeof response === 'string') { + // 对于字符串响应,计算字节大小 + responseSize = new TextEncoder().encode(response).length; + } else if (response && typeof response === 'object') { + // 对于对象响应,先序列化再计算大小 + responseSize = new TextEncoder().encode(JSON.stringify(response)).length; + } else if (response instanceof File || response instanceof Blob) { + // 对于文件响应,可以直接访问 size 属性 + responseSize = response.size; + } + + return formatFileSize(responseSize); +} + +// 导出默认实例 +export default Logger; diff --git a/src/plugins/redis/redis.plugins.ts b/src/plugins/redis/redis.plugins.ts index 0175a62..84ec10f 100644 --- a/src/plugins/redis/redis.plugins.ts +++ b/src/plugins/redis/redis.plugins.ts @@ -17,4 +17,3 @@ import { redisService } from './redis.service'; export const redisPlugin = new Elysia({ name: 'redis' }).onStart(async () => { await redisService.initialize(); }); - diff --git a/src/plugins/redis/redis.service.ts b/src/plugins/redis/redis.service.ts index 8393964..b5a03af 100644 --- a/src/plugins/redis/redis.service.ts +++ b/src/plugins/redis/redis.service.ts @@ -10,11 +10,7 @@ import { createClient } from 'redis'; import { redisConfig, getRedisUrl } from '@/config/redis.config'; import { Logger } from '@/plugins/logger/logger.service'; -import type { - RedisConnectionStatus, - RedisConnectionInfo, - RedisHealthCheckResult -} from '@/type/redis.type'; +import type { RedisConnectionStatus, RedisConnectionInfo, RedisHealthCheckResult } from '@/type/redis.type'; /** * Redis服务类 @@ -101,7 +97,7 @@ export class RedisService { this._connectionInfo.status = status; this._connectionInfo.error = error; this._connectionInfo.isConnected = status === 'connected'; - + if (status === 'connected') { this._connectionInfo.connectedAt = new Date(); this._connectionInfo.error = undefined; @@ -121,7 +117,7 @@ export class RedisService { try { this.validateConfig(); this.updateConnectionStatus('connecting'); - + // 创建Redis客户端 this._client = createClient({ name: redisConfig.connectName, @@ -139,7 +135,7 @@ export class RedisService { this._isInitialized = true; this.updateConnectionStatus('connected'); - + Logger.info({ message: 'Redis 初始化成功', host: redisConfig.host, @@ -147,7 +143,7 @@ export class RedisService { database: redisConfig.database, connectName: redisConfig.connectName, }); - + return this._client; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -165,7 +161,7 @@ export class RedisService { if (!this._client || !this._connectionInfo.isConnected) { return false; } - + await this._client.ping(); return true; } catch (error) { @@ -179,7 +175,7 @@ export class RedisService { */ public async healthCheck(): Promise { const startTime = Date.now(); - + try { if (!this._client) { return { @@ -202,7 +198,7 @@ export class RedisService { } catch (error) { const responseTime = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); - + return { status: 'unhealthy', responseTime, @@ -236,10 +232,10 @@ export class RedisService { */ public async reconnect(): Promise { Logger.info('正在重新连接Redis...'); - + // 先关闭现有连接 await this.close(); - + // 重新初始化连接 return await this.initialize(); } @@ -335,18 +331,18 @@ export class RedisService { /** * Redis服务单例实例 - * + * * @description 获取RedisService的单例实例,推荐的使用方式 * @example * ```typescript * import { redisService } from '@/plugins/redis/redis.service'; - * + * * // 初始化Redis * await redisService.initialize(); - * + * * // 获取客户端实例 * const client = redisService.client; - * + * * // 检查连接状态 * const isConnected = await redisService.checkConnection(); * ``` @@ -361,90 +357,90 @@ export const redisService = RedisService.getInstance(); /** * 创建并初始化Redis连接 - * + * * @description 向后兼容的初始化方法,内部调用redisService.initialize() * @returns {Promise} 返回初始化后的Redis客户端实例 - * + * * @example * ```typescript * import { createRedisClient } from '@/plugins/redis/redis.service'; - * + * * const client = await createRedisClient(); * ``` - * + * * @deprecated 推荐使用 redisService.initialize() 替代 */ export const createRedisClient = () => redisService.initialize(); /** * 获取Redis连接状态信息 - * + * * @description 向后兼容的状态获取方法,内部调用redisService.connectionInfo * @returns {RedisConnectionInfo} 返回Redis连接状态信息 - * + * * @example * ```typescript * import { getRedisConnectionInfo } from '@/plugins/redis/redis.service'; - * + * * const info = getRedisConnectionInfo(); * console.log(`Redis状态: ${info.status}`); * ``` - * + * * @deprecated 推荐使用 redisService.connectionInfo 替代 */ export const getRedisConnectionInfo = () => redisService.connectionInfo; /** * 检查Redis连接状态 - * + * * @description 向后兼容的连接检查方法,内部调用redisService.checkConnection() * @returns {Promise} 返回连接是否正常 - * + * * @example * ```typescript * import { checkRedisConnection } from '@/plugins/redis/redis.service'; - * + * * const isConnected = await checkRedisConnection(); * if (!isConnected) { * console.log('Redis连接异常'); * } * ``` - * + * * @deprecated 推荐使用 redisService.checkConnection() 替代 */ export const checkRedisConnection = () => redisService.checkConnection(); /** * 优雅关闭Redis连接 - * + * * @description 向后兼容的连接关闭方法,内部调用redisService.close() * @returns {Promise} 返回关闭操作的Promise - * + * * @example * ```typescript * import { closeRedisConnection } from '@/plugins/redis/redis.service'; - * + * * // 应用关闭时清理资源 * process.on('SIGTERM', async () => { * await closeRedisConnection(); * process.exit(0); * }); * ``` - * + * * @deprecated 推荐使用 redisService.close() 替代 */ export const closeRedisConnection = () => redisService.close(); /** * 重新连接Redis - * + * * @description 向后兼容的重连方法,内部调用redisService.reconnect() * @returns {Promise} 返回重新连接后的客户端实例 - * + * * @example * ```typescript * import { reconnectRedis } from '@/plugins/redis/redis.service'; - * + * * try { * const client = await reconnectRedis(); * console.log('Redis重连成功'); @@ -452,30 +448,30 @@ export const closeRedisConnection = () => redisService.close(); * console.error('Redis重连失败:', error); * } * ``` - * + * * @deprecated 推荐使用 redisService.reconnect() 替代 */ export const reconnectRedis = () => redisService.reconnect(); /** * 获取Redis客户端实例 - * + * * @description 向后兼容的客户端获取方法,内部调用redisService.client * @returns {any} 返回Redis客户端实例 * @throws {Error} 如果Redis未初始化则抛出错误 - * + * * @example * ```typescript * import { redis } from '@/plugins/redis/redis.service'; - * + * * // 确保先初始化 * await createRedisClient(); - * + * * // 获取客户端实例 * const client = redis(); * const result = await client.get('key'); * ``` - * + * * @deprecated 推荐使用 redisService.client 替代 */ -export const redis = () => redisService.client; \ No newline at end of file +export const redis = () => redisService.client; diff --git a/src/tests/demo/emailDemo.ts b/src/tests/demo/emailDemo.ts index 0955c79..e5d1d81 100644 --- a/src/tests/demo/emailDemo.ts +++ b/src/tests/demo/emailDemo.ts @@ -1,16 +1,16 @@ - /** +/** * @file 邮件发送Demo * @author hotok * @date 2025-06-29 * @description 简单的邮件发送测试demo,用于验证邮件服务是否正常工作 */ -import { +import { initializeEmailService, sendEmail, sendTemplateEmail, checkEmailServiceHealth, - closeEmailService + closeEmailService, } from '@/plugins/email/email.service'; import { validateEmailConfig } from '@/config/email.config'; import type { EmailSendOptions, EmailTemplateSendOptions } from '@/type/email.type'; @@ -25,7 +25,7 @@ class EmailDemo { async init(): Promise { try { console.log('🚀 正在初始化邮件服务...'); - + // 检查配置 const isConfigValid = validateEmailConfig(); if (!isConfigValid) { @@ -36,7 +36,7 @@ class EmailDemo { // 初始化服务 await initializeEmailService(); this.initialized = true; - + console.log('✅ 邮件服务初始化成功'); return true; } catch (error) { @@ -65,7 +65,7 @@ class EmailDemo { */ async sendSimpleEmail(to: string): Promise { console.log(`\n📧 发送简单测试邮件到: ${to}`); - + const emailOptions: EmailSendOptions = { to: to, subject: '邮件服务测试 - 简单邮件', @@ -89,7 +89,7 @@ class EmailDemo { try { const result = await sendEmail(emailOptions); - + if (result.success) { console.log('✅ 邮件发送成功!'); console.log(`📮 消息ID: ${result.messageId}`); @@ -111,7 +111,7 @@ class EmailDemo { */ async sendTemplateEmail(to: string): Promise { console.log(`\n📧 发送模板邮件到: ${to}`); - + const templateOptions: EmailTemplateSendOptions = { to: to, template: 'welcome', @@ -124,7 +124,7 @@ class EmailDemo { try { const result = await sendTemplateEmail(templateOptions); - + if (result.success) { console.log('✅ 模板邮件发送成功!'); console.log(`📮 消息ID: ${result.messageId}`); @@ -156,7 +156,7 @@ async function main() { console.log('='.repeat(50)); const demo = new EmailDemo(); - + try { // 初始化 const initSuccess = await demo.init(); @@ -177,7 +177,7 @@ async function main() { // 询问收件人邮箱 const testEmail = process.argv[2] || 'test@example.com'; console.log(`\n🎯 测试邮箱: ${testEmail}`); - + if (testEmail === 'test@example.com') { console.log('💡 提示: 可以通过参数指定邮箱地址'); console.log(' 示例: bun run src/tests/demo/emailDemo.ts your@email.com'); @@ -185,18 +185,17 @@ async function main() { // 发送测试邮件 await demo.sendSimpleEmail(testEmail); - + // 等待一下再发送模板邮件 console.log('\n⏳ 等待2秒后发送模板邮件...'); - await new Promise(resolve => setTimeout(resolve, 2000)); - + await new Promise((resolve) => setTimeout(resolve, 2000)); + await demo.sendTemplateEmail(testEmail); console.log('\n' + '='.repeat(50)); console.log('✨ Demo执行完成!请检查您的邮箱收件箱'); console.log('📬 如果没收到邮件,请检查垃圾邮件文件夹'); console.log('='.repeat(50)); - } catch (error) { console.log('💥 Demo执行过程中出现错误:', error); } finally { @@ -210,4 +209,4 @@ if (import.meta.main) { main().catch(console.error); } -export { EmailDemo }; \ No newline at end of file +export { EmailDemo }; diff --git a/src/tests/email.test.ts b/src/tests/email.test.ts index a1f38bb..51e001f 100644 --- a/src/tests/email.test.ts +++ b/src/tests/email.test.ts @@ -9,20 +9,20 @@ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { setTimeout } from 'node:timers'; -import { - emailService, +import { + emailService, initializeEmailService, sendEmail, sendTemplateEmail, getEmailServiceStatus, checkEmailServiceHealth, - closeEmailService + closeEmailService, } from '@/plugins/email/email.service'; import { validateEmailConfig } from '@/config/email.config'; import type { EmailSendOptions, EmailTemplateSendOptions } from '@/type/email.type'; // 简单的延时函数 -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // 测试用的邮箱地址(请根据实际情况修改) const TEST_EMAIL = 'x71291@outlook.com'; @@ -35,7 +35,7 @@ describe('邮件服务测试', () => { beforeAll(async () => { // 检查邮件配置是否有效 isEmailConfigValid = validateEmailConfig(); - + if (isEmailConfigValid) { try { // 初始化邮件服务 @@ -81,7 +81,7 @@ describe('邮件服务测试', () => { describe('邮件服务健康检查', () => { it('应该执行健康检查并返回结果', async () => { const healthResult = await checkEmailServiceHealth(); - + expect(healthResult).toHaveProperty('status'); expect(healthResult).toHaveProperty('responseTime'); expect(healthResult).toHaveProperty('serviceStatus'); @@ -95,41 +95,49 @@ describe('邮件服务测试', () => { // 只在邮件配置有效时运行实际发送测试 const runSendTest = isEmailConfigValid ? it : it.skip; - runSendTest('应该能够发送简单文本邮件', async () => { - const emailOptions: EmailSendOptions = { - to: TEST_EMAIL, - subject: '测试邮件 - 简单文本', - text: '这是一封测试邮件的纯文本内容。', - html: '

这是一封测试邮件的HTML内容

', - }; + runSendTest( + '应该能够发送简单文本邮件', + async () => { + const emailOptions: EmailSendOptions = { + to: TEST_EMAIL, + subject: '测试邮件 - 简单文本', + text: '这是一封测试邮件的纯文本内容。', + html: '

这是一封测试邮件的HTML内容

', + }; - const result = await sendEmail(emailOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - expect(result.sentAt).toBeInstanceOf(Date); - - if (result.success) { - expect(result).toHaveProperty('messageId'); - expect(result.accepted).toContain(TEST_EMAIL); - console.log('邮件发送成功:', result.messageId); - } else { - console.warn('邮件发送失败:', result.error); - } - }, 10000); // 设置较长的超时时间 + const result = await sendEmail(emailOptions); - runSendTest('应该能够发送带抄送的邮件', async () => { - const emailOptions: EmailSendOptions = { - to: TEST_EMAIL, - cc: TEST_EMAIL, // 测试环境下抄送给同一个邮箱 - subject: '测试邮件 - 带抄送', - html: '

测试抄送功能

这封邮件测试抄送功能。

', - }; + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + expect(result.sentAt).toBeInstanceOf(Date); - const result = await sendEmail(emailOptions); - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - }, 10000); + if (result.success) { + expect(result).toHaveProperty('messageId'); + expect(result.accepted).toContain(TEST_EMAIL); + console.log('邮件发送成功:', result.messageId); + } else { + console.warn('邮件发送失败:', result.error); + } + }, + 10000, + ); // 设置较长的超时时间 + + runSendTest( + '应该能够发送带抄送的邮件', + async () => { + const emailOptions: EmailSendOptions = { + to: TEST_EMAIL, + cc: TEST_EMAIL, // 测试环境下抄送给同一个邮箱 + subject: '测试邮件 - 带抄送', + html: '

测试抄送功能

这封邮件测试抄送功能。

', + }; + + const result = await sendEmail(emailOptions); + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + }, + 10000, + ); it('应该处理邮件发送错误', async () => { const invalidEmailOptions: EmailSendOptions = { @@ -139,11 +147,11 @@ describe('邮件服务测试', () => { }; const result = await sendEmail(invalidEmailOptions); - + // 无论SMTP是否配置,无效邮箱都应该返回失败 expect(result).toHaveProperty('success'); expect(result).toHaveProperty('sentAt'); - + if (!result.success) { expect(result).toHaveProperty('error'); expect(result.rejected).toContain('invalid-email'); @@ -154,118 +162,138 @@ describe('邮件服务测试', () => { describe('模板邮件功能', () => { const runTemplateTest = isEmailConfigValid ? it : it.skip; - runTemplateTest('应该能够发送用户激活邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'activation', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - email: TEST_EMAIL, - activationUrl: 'https://example.com/activate?token=test123', - activationCode: '123456', - expireTime: '24小时', - }, - }; + runTemplateTest( + '应该能够发送用户激活邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'activation', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + email: TEST_EMAIL, + activationUrl: 'https://example.com/activate?token=test123', + activationCode: '123456', + expireTime: '24小时', + }, + }; - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - expect(result).toHaveProperty('messageId'); - console.log('激活邮件发送成功:', result.messageId); - } - }, 10000); + const result = await sendTemplateEmail(templateOptions); - runTemplateTest('应该能够发送密码重置邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'passwordReset', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - email: TEST_EMAIL, - resetUrl: 'https://example.com/reset?token=test123', - resetCode: '654321', - expireTime: '30分钟', - }, - }; + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - console.log('密码重置邮件发送成功:', result.messageId); - } - }, 10000); + if (result.success) { + expect(result).toHaveProperty('messageId'); + console.log('激活邮件发送成功:', result.messageId); + } + }, + 10000, + ); - runTemplateTest('应该能够发送欢迎邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'welcome', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - email: TEST_EMAIL, - }, - }; + runTemplateTest( + '应该能够发送密码重置邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'passwordReset', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + email: TEST_EMAIL, + resetUrl: 'https://example.com/reset?token=test123', + resetCode: '654321', + expireTime: '30分钟', + }, + }; - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - console.log('欢迎邮件发送成功:', result.messageId); - } - }, 10000); + const result = await sendTemplateEmail(templateOptions); - runTemplateTest('应该能够发送密码修改通知邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'passwordChanged', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - email: TEST_EMAIL, - }, - }; + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - console.log('密码修改通知邮件发送成功:', result.messageId); - } - }, 10000); + if (result.success) { + console.log('密码重置邮件发送成功:', result.messageId); + } + }, + 10000, + ); - runTemplateTest('应该能够发送系统通知邮件', async () => { - const templateOptions: EmailTemplateSendOptions = { - to: TEST_EMAIL, - template: 'notification', - params: { - username: TEST_USERNAME, - nickname: TEST_NICKNAME, - message: '这是一条重要的系统通知,请及时查看您的账户状态。', - }, - subject: '重要系统通知', - }; + runTemplateTest( + '应该能够发送欢迎邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'welcome', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + email: TEST_EMAIL, + }, + }; - const result = await sendTemplateEmail(templateOptions); - - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - - if (result.success) { - console.log('系统通知邮件发送成功:', result.messageId); - } - }, 10000); + const result = await sendTemplateEmail(templateOptions); + + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + + if (result.success) { + console.log('欢迎邮件发送成功:', result.messageId); + } + }, + 10000, + ); + + runTemplateTest( + '应该能够发送密码修改通知邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'passwordChanged', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + email: TEST_EMAIL, + }, + }; + + const result = await sendTemplateEmail(templateOptions); + + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + + if (result.success) { + console.log('密码修改通知邮件发送成功:', result.messageId); + } + }, + 10000, + ); + + runTemplateTest( + '应该能够发送系统通知邮件', + async () => { + const templateOptions: EmailTemplateSendOptions = { + to: TEST_EMAIL, + template: 'notification', + params: { + username: TEST_USERNAME, + nickname: TEST_NICKNAME, + message: '这是一条重要的系统通知,请及时查看您的账户状态。', + }, + subject: '重要系统通知', + }; + + const result = await sendTemplateEmail(templateOptions); + + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + + if (result.success) { + console.log('系统通知邮件发送成功:', result.messageId); + } + }, + 10000, + ); it('应该处理无效模板类型', async () => { const invalidTemplateOptions = { @@ -277,7 +305,7 @@ describe('邮件服务测试', () => { }; const result = await sendTemplateEmail(invalidTemplateOptions); - + expect(result.success).toBe(false); expect(result).toHaveProperty('error'); expect(result.error).toContain('未找到邮件模板'); @@ -294,11 +322,11 @@ describe('邮件服务测试', () => { }; const result = await sendEmail(emailOptions); - + // 如果SMTP未配置或连接失败,应该有适当的错误处理 expect(result).toHaveProperty('success'); expect(result).toHaveProperty('sentAt'); - + if (!result.success) { expect(result).toHaveProperty('error'); expect(typeof result.error).toBe('string'); @@ -309,29 +337,33 @@ describe('邮件服务测试', () => { describe('邮件服务性能测试', () => { const runPerformanceTest = isEmailConfigValid ? it : it.skip; - runPerformanceTest('应该能够并发发送多封邮件', async () => { - const emailPromises = []; - const emailCount = 3; // 限制并发数量以避免SMTP限制 + runPerformanceTest( + '应该能够并发发送多封邮件', + async () => { + const emailPromises = []; + const emailCount = 3; // 限制并发数量以避免SMTP限制 - for (let i = 0; i < emailCount; i++) { - const emailOptions: EmailSendOptions = { - to: TEST_EMAIL, - subject: `并发测试邮件 ${i + 1}`, - text: `这是第 ${i + 1} 封并发测试邮件`, - }; - - emailPromises.push(sendEmail(emailOptions)); - } + for (let i = 0; i < emailCount; i++) { + const emailOptions: EmailSendOptions = { + to: TEST_EMAIL, + subject: `并发测试邮件 ${i + 1}`, + text: `这是第 ${i + 1} 封并发测试邮件`, + }; - const results = await Promise.all(emailPromises); - - expect(results).toHaveLength(emailCount); - results.forEach((result, index) => { - expect(result).toHaveProperty('success'); - expect(result).toHaveProperty('sentAt'); - console.log(`邮件 ${index + 1} 发送结果:`, result.success ? '成功' : '失败'); - }); - }, 30000); // 更长的超时时间用于并发测试 + emailPromises.push(sendEmail(emailOptions)); + } + + const results = await Promise.all(emailPromises); + + expect(results).toHaveLength(emailCount); + results.forEach((result, index) => { + expect(result).toHaveProperty('success'); + expect(result).toHaveProperty('sentAt'); + console.log(`邮件 ${index + 1} 发送结果:`, result.success ? '成功' : '失败'); + }); + }, + 30000, + ); // 更长的超时时间用于并发测试 }); describe('邮件模板渲染测试', () => { @@ -351,11 +383,11 @@ describe('邮件服务测试', () => { // 我们可以通过发送结果来验证模板是否正确处理 const result = await sendTemplateEmail(templateOptions); expect(result).toHaveProperty('success'); - + // 模板渲染错误会导致发送失败 if (!result.success && result.error) { expect(result.error).not.toContain('模板渲染失败'); } }); }); -}); \ No newline at end of file +}); diff --git a/src/tests/health.test.ts b/src/tests/health.test.ts index a291579..b37e21b 100644 --- a/src/tests/health.test.ts +++ b/src/tests/health.test.ts @@ -1,305 +1,305 @@ -/** - * @file 健康检查功能测试 - * @author hotok - * @date 2025-06-28 - * @lastEditor hotok - * @lastEditTime 2025-06-28 - * @description 测试健康检查接口和服务,包括系统状态、依赖检查、响应格式等 - */ - -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { setTimeout } from 'node:timers'; -import { app } from '@/app'; - -describe('健康检查接口测试', () => { - beforeAll(async () => { - // 等待应用启动 - await new Promise(resolve => setTimeout(resolve, 1000)); - }); - - describe('GET /api/health', () => { - it('应该返回基本健康状态', async () => { - const res = await app.fetch( - new Request('http://localhost/api/health', { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }), - ); - - const body = (await res.json()) as any; - - expect(res.status).toBe(200); - expect(body.code).toBeTypeOf('number'); - expect(body.message).toBeTypeOf('string'); - expect(body.data).toBeTypeOf('object'); - - // 检查基本数据结构 - expect(body.data.status).toMatch(/^(healthy|unhealthy|degraded)$/); - expect(body.data.timestamp).toBeTypeOf('string'); - expect(body.data.uptime).toBeTypeOf('number'); - expect(body.data.responseTime).toBeTypeOf('number'); - expect(body.data.version).toBeTypeOf('string'); - expect(body.data.environment).toBeTypeOf('string'); - expect(body.data.components).toBeTypeOf('object'); - - // 检查组件状态 - if (body.data.components.mysql) { - expect(body.data.components.mysql.status).toMatch(/^(healthy|unhealthy|degraded)$/); - } - if (body.data.components.redis) { - expect(body.data.components.redis.status).toMatch(/^(healthy|unhealthy|degraded)$/); - } - }); - - it('应该包含正确的时间戳格式', async () => { - const res = await app.fetch( - new Request('http://localhost/api/health', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - - // 验证ISO时间戳格式 - const timestamp = new Date(body.data.timestamp); - expect(timestamp.toISOString()).toBe(body.data.timestamp); - }); - - it('应该返回合理的响应时间', async () => { - const startTime = Date.now(); - const res = await app.fetch( - new Request('http://localhost/api/health', { - method: 'GET', - }), - ); - const endTime = Date.now(); - - const body = (await res.json()) as any; - - // 响应时间应该在合理范围内 - expect(body.data.responseTime).toBeGreaterThan(0); - expect(body.data.responseTime).toBeLessThan(endTime - startTime + 100); // 允许一定误差 - }); - - it('应该返回正确的环境信息', async () => { - const res = await app.fetch( - new Request('http://localhost/api/health', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - - expect(body.data.environment).toMatch(/^(development|production|test)$/); - expect(body.data.uptime).toBeGreaterThan(0); - }); - }); - - describe('GET /api/health/detailed', () => { - it('应该返回详细健康状态', async () => { - const res = await app.fetch( - new Request('http://localhost/api/health/detailed', { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }), - ); - - const body = (await res.json()) as any; - - expect(res.status).toBe(200); - expect(body.code).toBeTypeOf('number'); - expect(body.message).toBeTypeOf('string'); - expect(body.data).toBeTypeOf('object'); - - // 检查基本健康检查数据 - expect(body.data.status).toMatch(/^(healthy|unhealthy|degraded)$/); - expect(body.data.timestamp).toBeTypeOf('string'); - expect(body.data.uptime).toBeTypeOf('number'); - expect(body.data.responseTime).toBeTypeOf('number'); - expect(body.data.components).toBeTypeOf('object'); - - // 检查详细信息 - if (body.data.system) { - expect(body.data.system.platform).toBeTypeOf('string'); - expect(body.data.system.arch).toBeTypeOf('string'); - expect(body.data.system.nodeVersion).toBeTypeOf('string'); - expect(body.data.system.runtime).toBeTypeOf('string'); - expect(body.data.system.pid).toBeTypeOf('number'); - expect(body.data.system.cwd).toBeTypeOf('string'); - } - - if (body.data.performance) { - expect(body.data.performance.cpuUsage).toBeTypeOf('object'); - expect(body.data.performance.memoryUsage).toBeTypeOf('object'); - expect(body.data.performance.uptime).toBeTypeOf('number'); - - // 检查CPU使用情况 - expect(body.data.performance.cpuUsage.user).toBeTypeOf('number'); - expect(body.data.performance.cpuUsage.system).toBeTypeOf('number'); - - // 检查内存使用情况 - expect(body.data.performance.memoryUsage.rss).toBeTypeOf('number'); - expect(body.data.performance.memoryUsage.heapTotal).toBeTypeOf('number'); - expect(body.data.performance.memoryUsage.heapUsed).toBeTypeOf('number'); - expect(body.data.performance.memoryUsage.external).toBeTypeOf('number'); - expect(body.data.performance.memoryUsage.arrayBuffers).toBeTypeOf('number'); - } - }); - - it('详细健康检查应该包含系统信息', async () => { - const res = await app.fetch( - new Request('http://localhost/api/health/detailed', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - - if (body.data.system) { - expect(body.data.system.runtime).toBe('Bun'); - expect(body.data.system.pid).toBe(process.pid); - expect(body.data.system.platform).toBe(process.platform); - expect(body.data.system.arch).toBe(process.arch); - } - }); - }); - - describe('健康检查依赖服务测试', () => { - it('MySQL组件状态应该包含连接信息', async () => { - const res = await app.fetch( - new Request('http://localhost/api/health', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - - if (body.data.components.mysql) { - expect(body.data.components.mysql.status).toMatch(/^(healthy|unhealthy|degraded)$/); - - if (body.data.components.mysql.details) { - expect(body.data.components.mysql.details.connection).toMatch(/^(active|failed)$/); - expect(body.data.components.mysql.details.host).toBeTypeOf('string'); - expect(body.data.components.mysql.details.port).toBeTypeOf('string'); - } - - if (body.data.components.mysql.responseTime) { - expect(body.data.components.mysql.responseTime).toBeGreaterThan(0); - } - } - }); - - it('Redis组件状态应该包含连接信息', async () => { - const res = await app.fetch( - new Request('http://localhost/api/health', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - - if (body.data.components.redis) { - expect(body.data.components.redis.status).toMatch(/^(healthy|unhealthy|degraded)$/); - - if (body.data.components.redis.details) { - expect(body.data.components.redis.details.connection).toMatch(/^(active|failed)$/); - } - - if (body.data.components.redis.responseTime) { - expect(body.data.components.redis.responseTime).toBeGreaterThan(0); - } - } - }); - }); - - describe('健康检查错误处理', () => { - it('健康检查应该处理组件异常', async () => { - const res = await app.fetch( - new Request('http://localhost/api/health', { - method: 'GET', - }), - ); - - const body = (await res.json()) as any; - - // 即使有组件异常,也应该返回结构化的响应 - expect(res.status).toBe(200); - expect(body.code).toBeTypeOf('number'); - expect(body.message).toBeTypeOf('string'); - expect(body.data).toBeTypeOf('object'); - - // 如果有组件异常,整体状态可能是degraded或unhealthy - if (body.data.status === 'unhealthy' || body.data.status === 'degraded') { - // 应该有组件错误信息 - const components = body.data.components; - let hasUnhealthyComponent = false; - - Object.values(components).forEach((component: any) => { - if (component && component.status === 'unhealthy') { - hasUnhealthyComponent = true; - expect(component.error).toBeTypeOf('string'); - } - }); - - if (!hasUnhealthyComponent && body.data.error) { - expect(body.data.error).toBeTypeOf('string'); - } - } - }); - }); - - describe('健康检查性能测试', () => { - it('健康检查应该快速响应', async () => { - const startTime = Date.now(); - - const res = await app.fetch( - new Request('http://localhost/api/health', { - method: 'GET', - }), - ); - - const endTime = Date.now(); - const responseTime = endTime - startTime; - - expect(res.status).toBe(200); - expect(responseTime).toBeLessThan(2000); // 应该在2秒内完成 - }); - - it('详细健康检查应该在合理时间内完成', async () => { - const startTime = Date.now(); - - const res = await app.fetch( - new Request('http://localhost/api/health/detailed', { - method: 'GET', - }), - ); - - const endTime = Date.now(); - const responseTime = endTime - startTime; - - expect(res.status).toBe(200); - expect(responseTime).toBeLessThan(3000); // 详细检查可能稍慢,但应该在3秒内完成 - }); - - it('并发健康检查应该正常处理', async () => { - const promises = []; - const concurrentRequests = 5; - - for (let i = 0; i < concurrentRequests; i++) { - promises.push( - app.fetch( - new Request('http://localhost/api/health', { - method: 'GET', - }), - ), - ); - } - - const responses = await Promise.all(promises); - - responses.forEach(res => { - expect(res.status).toBe(200); - }); - }); - }); -}); \ No newline at end of file +/** + * @file 健康检查功能测试 + * @author hotok + * @date 2025-06-28 + * @lastEditor hotok + * @lastEditTime 2025-06-28 + * @description 测试健康检查接口和服务,包括系统状态、依赖检查、响应格式等 + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { setTimeout } from 'node:timers'; +import { app } from '@/app'; + +describe('健康检查接口测试', () => { + beforeAll(async () => { + // 等待应用启动 + await new Promise((resolve) => setTimeout(resolve, 1000)); + }); + + describe('GET /api/health', () => { + it('应该返回基本健康状态', async () => { + const res = await app.fetch( + new Request('http://localhost/api/health', { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }), + ); + + const body = (await res.json()) as any; + + expect(res.status).toBe(200); + expect(body.code).toBeTypeOf('number'); + expect(body.message).toBeTypeOf('string'); + expect(body.data).toBeTypeOf('object'); + + // 检查基本数据结构 + expect(body.data.status).toMatch(/^(healthy|unhealthy|degraded)$/); + expect(body.data.timestamp).toBeTypeOf('string'); + expect(body.data.uptime).toBeTypeOf('number'); + expect(body.data.responseTime).toBeTypeOf('number'); + expect(body.data.version).toBeTypeOf('string'); + expect(body.data.environment).toBeTypeOf('string'); + expect(body.data.components).toBeTypeOf('object'); + + // 检查组件状态 + if (body.data.components.mysql) { + expect(body.data.components.mysql.status).toMatch(/^(healthy|unhealthy|degraded)$/); + } + if (body.data.components.redis) { + expect(body.data.components.redis.status).toMatch(/^(healthy|unhealthy|degraded)$/); + } + }); + + it('应该包含正确的时间戳格式', async () => { + const res = await app.fetch( + new Request('http://localhost/api/health', { + method: 'GET', + }), + ); + + const body = (await res.json()) as any; + + // 验证ISO时间戳格式 + const timestamp = new Date(body.data.timestamp); + expect(timestamp.toISOString()).toBe(body.data.timestamp); + }); + + it('应该返回合理的响应时间', async () => { + const startTime = Date.now(); + const res = await app.fetch( + new Request('http://localhost/api/health', { + method: 'GET', + }), + ); + const endTime = Date.now(); + + const body = (await res.json()) as any; + + // 响应时间应该在合理范围内 + expect(body.data.responseTime).toBeGreaterThan(0); + expect(body.data.responseTime).toBeLessThan(endTime - startTime + 100); // 允许一定误差 + }); + + it('应该返回正确的环境信息', async () => { + const res = await app.fetch( + new Request('http://localhost/api/health', { + method: 'GET', + }), + ); + + const body = (await res.json()) as any; + + expect(body.data.environment).toMatch(/^(development|production|test)$/); + expect(body.data.uptime).toBeGreaterThan(0); + }); + }); + + describe('GET /api/health/detailed', () => { + it('应该返回详细健康状态', async () => { + const res = await app.fetch( + new Request('http://localhost/api/health/detailed', { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }), + ); + + const body = (await res.json()) as any; + + expect(res.status).toBe(200); + expect(body.code).toBeTypeOf('number'); + expect(body.message).toBeTypeOf('string'); + expect(body.data).toBeTypeOf('object'); + + // 检查基本健康检查数据 + expect(body.data.status).toMatch(/^(healthy|unhealthy|degraded)$/); + expect(body.data.timestamp).toBeTypeOf('string'); + expect(body.data.uptime).toBeTypeOf('number'); + expect(body.data.responseTime).toBeTypeOf('number'); + expect(body.data.components).toBeTypeOf('object'); + + // 检查详细信息 + if (body.data.system) { + expect(body.data.system.platform).toBeTypeOf('string'); + expect(body.data.system.arch).toBeTypeOf('string'); + expect(body.data.system.nodeVersion).toBeTypeOf('string'); + expect(body.data.system.runtime).toBeTypeOf('string'); + expect(body.data.system.pid).toBeTypeOf('number'); + expect(body.data.system.cwd).toBeTypeOf('string'); + } + + if (body.data.performance) { + expect(body.data.performance.cpuUsage).toBeTypeOf('object'); + expect(body.data.performance.memoryUsage).toBeTypeOf('object'); + expect(body.data.performance.uptime).toBeTypeOf('number'); + + // 检查CPU使用情况 + expect(body.data.performance.cpuUsage.user).toBeTypeOf('number'); + expect(body.data.performance.cpuUsage.system).toBeTypeOf('number'); + + // 检查内存使用情况 + expect(body.data.performance.memoryUsage.rss).toBeTypeOf('number'); + expect(body.data.performance.memoryUsage.heapTotal).toBeTypeOf('number'); + expect(body.data.performance.memoryUsage.heapUsed).toBeTypeOf('number'); + expect(body.data.performance.memoryUsage.external).toBeTypeOf('number'); + expect(body.data.performance.memoryUsage.arrayBuffers).toBeTypeOf('number'); + } + }); + + it('详细健康检查应该包含系统信息', async () => { + const res = await app.fetch( + new Request('http://localhost/api/health/detailed', { + method: 'GET', + }), + ); + + const body = (await res.json()) as any; + + if (body.data.system) { + expect(body.data.system.runtime).toBe('Bun'); + expect(body.data.system.pid).toBe(process.pid); + expect(body.data.system.platform).toBe(process.platform); + expect(body.data.system.arch).toBe(process.arch); + } + }); + }); + + describe('健康检查依赖服务测试', () => { + it('MySQL组件状态应该包含连接信息', async () => { + const res = await app.fetch( + new Request('http://localhost/api/health', { + method: 'GET', + }), + ); + + const body = (await res.json()) as any; + + if (body.data.components.mysql) { + expect(body.data.components.mysql.status).toMatch(/^(healthy|unhealthy|degraded)$/); + + if (body.data.components.mysql.details) { + expect(body.data.components.mysql.details.connection).toMatch(/^(active|failed)$/); + expect(body.data.components.mysql.details.host).toBeTypeOf('string'); + expect(body.data.components.mysql.details.port).toBeTypeOf('string'); + } + + if (body.data.components.mysql.responseTime) { + expect(body.data.components.mysql.responseTime).toBeGreaterThan(0); + } + } + }); + + it('Redis组件状态应该包含连接信息', async () => { + const res = await app.fetch( + new Request('http://localhost/api/health', { + method: 'GET', + }), + ); + + const body = (await res.json()) as any; + + if (body.data.components.redis) { + expect(body.data.components.redis.status).toMatch(/^(healthy|unhealthy|degraded)$/); + + if (body.data.components.redis.details) { + expect(body.data.components.redis.details.connection).toMatch(/^(active|failed)$/); + } + + if (body.data.components.redis.responseTime) { + expect(body.data.components.redis.responseTime).toBeGreaterThan(0); + } + } + }); + }); + + describe('健康检查错误处理', () => { + it('健康检查应该处理组件异常', async () => { + const res = await app.fetch( + new Request('http://localhost/api/health', { + method: 'GET', + }), + ); + + const body = (await res.json()) as any; + + // 即使有组件异常,也应该返回结构化的响应 + expect(res.status).toBe(200); + expect(body.code).toBeTypeOf('number'); + expect(body.message).toBeTypeOf('string'); + expect(body.data).toBeTypeOf('object'); + + // 如果有组件异常,整体状态可能是degraded或unhealthy + if (body.data.status === 'unhealthy' || body.data.status === 'degraded') { + // 应该有组件错误信息 + const components = body.data.components; + let hasUnhealthyComponent = false; + + Object.values(components).forEach((component: any) => { + if (component && component.status === 'unhealthy') { + hasUnhealthyComponent = true; + expect(component.error).toBeTypeOf('string'); + } + }); + + if (!hasUnhealthyComponent && body.data.error) { + expect(body.data.error).toBeTypeOf('string'); + } + } + }); + }); + + describe('健康检查性能测试', () => { + it('健康检查应该快速响应', async () => { + const startTime = Date.now(); + + const res = await app.fetch( + new Request('http://localhost/api/health', { + method: 'GET', + }), + ); + + const endTime = Date.now(); + const responseTime = endTime - startTime; + + expect(res.status).toBe(200); + expect(responseTime).toBeLessThan(2000); // 应该在2秒内完成 + }); + + it('详细健康检查应该在合理时间内完成', async () => { + const startTime = Date.now(); + + const res = await app.fetch( + new Request('http://localhost/api/health/detailed', { + method: 'GET', + }), + ); + + const endTime = Date.now(); + const responseTime = endTime - startTime; + + expect(res.status).toBe(200); + expect(responseTime).toBeLessThan(3000); // 详细检查可能稍慢,但应该在3秒内完成 + }); + + it('并发健康检查应该正常处理', async () => { + const promises = []; + const concurrentRequests = 5; + + for (let i = 0; i < concurrentRequests; i++) { + promises.push( + app.fetch( + new Request('http://localhost/api/health', { + method: 'GET', + }), + ), + ); + } + + const responses = await Promise.all(promises); + + responses.forEach((res) => { + expect(res.status).toBe(200); + }); + }); + }); +}); diff --git a/src/tests/redis.test.ts b/src/tests/redis.test.ts index 5aef8c3..a75cc10 100644 --- a/src/tests/redis.test.ts +++ b/src/tests/redis.test.ts @@ -1,238 +1,238 @@ -/** - * @file Redis连接测试 - * @author hotok - * @date 2025-06-28 - * @lastEditor hotok - * @lastEditTime 2025-06-28 - * @description 测试Redis连接、健康检查、基本操作和状态管理功能 - */ - -import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; -import { setTimeout } from 'node:timers'; -import { Redis } from '@/utils/redis'; -import { redisConfig } from '@/config/redis.config'; - -// 简单的延时函数 -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -describe('Redis连接测试', () => { - let redis: Redis; - - beforeAll(async () => { - redis = new Redis(); - // 等待连接建立 - await delay(1000); - }); - - afterAll(async () => { - if (redis) { - await redis.disconnectRedis(); - } - }); - - beforeEach(async () => { - // 确保每个测试前Redis连接正常 - if (!redis.redisClient.isOpen) { - await redis.connectRedis(); - } - }); - - describe('Redis连接管理', () => { - it('应该成功连接到Redis服务器', async () => { - const isHealthy = await redis.checkRedisHealth(); - expect(isHealthy).toBe(true); - }); - - it('应该正确返回Redis连接状态', () => { - const status = redis.getRedisStatus(); - expect(status).toEqual({ - isConnected: expect.any(Boolean), - config: { - host: redisConfig.host, - port: redisConfig.port, - database: redisConfig.database, - connectName: redisConfig.connectName, - }, - }); - }); - - it('应该能够执行ping命令', async () => { - const result = await redis.redisClient.ping(); - expect(result).toBe('PONG'); - }); - }); - - describe('Redis基本操作', () => { - const testKey = 'test_key'; - const testValue = 'test_value'; - - it('应该能够设置和获取字符串值', async () => { - // 设置值 - await redis.redisClient.set(testKey, testValue); - - // 获取值 - const result = await redis.redisClient.get(testKey); - expect(result).toBe(testValue); - - // 清理测试数据 - await redis.redisClient.del(testKey); - }); - - it('应该能够设置带过期时间的值', async () => { - const expiryTime = 2; // 2秒过期 - - // 设置带过期时间的值 - await redis.redisClient.setEx(testKey, expiryTime, testValue); - - // 立即获取应该有值 - const result1 = await redis.redisClient.get(testKey); - expect(result1).toBe(testValue); - - // 等待过期 - await delay(2100); - - // 过期后应该为null - const result2 = await redis.redisClient.get(testKey); - expect(result2).toBeNull(); - }); - - it('应该能够检查键是否存在', async () => { - // 设置测试键 - await redis.redisClient.set(testKey, testValue); - - // 检查存在 - const exists1 = await redis.redisClient.exists(testKey); - expect(exists1).toBe(1); - - // 删除键 - await redis.redisClient.del(testKey); - - // 检查不存在 - const exists2 = await redis.redisClient.exists(testKey); - expect(exists2).toBe(0); - }); - - it('应该能够删除键', async () => { - // 设置测试键 - await redis.redisClient.set(testKey, testValue); - - // 删除键 - const deleteCount = await redis.redisClient.del(testKey); - expect(deleteCount).toBe(1); - - // 验证键已被删除 - const result = await redis.redisClient.get(testKey); - expect(result).toBeNull(); - }); - }); - - describe('Redis Hash操作', () => { - const hashKey = 'test_hash'; - const field1 = 'field1'; - const value1 = 'value1'; - const field2 = 'field2'; - const value2 = 'value2'; - - it('应该能够设置和获取Hash字段', async () => { - // 设置Hash字段 - await redis.redisClient.hSet(hashKey, field1, value1); - - // 获取Hash字段 - const result = await redis.redisClient.hGet(hashKey, field1); - expect(result).toBe(value1); - - // 清理测试数据 - await redis.redisClient.del(hashKey); - }); - - it('应该能够设置和获取多个Hash字段', async () => { - // 设置多个Hash字段 - await redis.redisClient.hSet(hashKey, field1, value1); - await redis.redisClient.hSet(hashKey, field2, value2); - - // 获取所有Hash字段 - const result = await redis.redisClient.hGetAll(hashKey); - expect(result).toEqual({ - [field1]: value1, - [field2]: value2, - }); - - // 清理测试数据 - await redis.redisClient.del(hashKey); - }); - }); - - describe('Redis列表操作', () => { - const listKey = 'test_list'; - const value1 = 'item1'; - const value2 = 'item2'; - - it('应该能够推入和弹出列表元素', async () => { - // 推入元素 - await redis.redisClient.lPush(listKey, value1); - await redis.redisClient.lPush(listKey, value2); - - // 获取列表长度 - const length = await redis.redisClient.lLen(listKey); - expect(length).toBe(2); - - // 弹出元素 - const poppedValue = await redis.redisClient.lPop(listKey); - expect(poppedValue).toBe(value2); - - // 清理测试数据 - await redis.redisClient.del(listKey); - }); - }); - - describe('Redis连接错误处理', () => { - it('健康检查在连接断开时应返回false', async () => { - // 暂时断开连接 - await redis.disconnectRedis(); - - // 健康检查应该返回false - const isHealthy = await redis.checkRedisHealth(); - expect(isHealthy).toBe(false); - - // 重新连接 - await redis.connectRedis(); - }); - - it('应该能够重新连接Redis', async () => { - // 断开连接 - await redis.disconnectRedis(); - - // 重新连接 - await redis.connectRedis(); - - // 验证连接正常 - const isHealthy = await redis.checkRedisHealth(); - expect(isHealthy).toBe(true); - }); - }); - - describe('Redis性能测试', () => { - it('应该能够快速执行大量set操作', async () => { - const startTime = Date.now(); - const operations = []; - - // 执行100次set操作 - for (let i = 0; i < 1000; i++) { - operations.push(redis.redisClient.set(`perf_test_${i}`, `value_${i}`)); - } - - await Promise.all(operations); - const endTime = Date.now(); - - expect(endTime - startTime).toBeLessThan(1000); // 应该在1秒内完成 - console.log(endTime - startTime); - - // 清理测试数据 - const deleteOperations = []; - for (let i = 0; i < 100; i++) { - deleteOperations.push(redis.redisClient.del(`perf_test_${i}`)); - } - await Promise.all(deleteOperations); - }); - }); -}); \ No newline at end of file +/** + * @file Redis连接测试 + * @author hotok + * @date 2025-06-28 + * @lastEditor hotok + * @lastEditTime 2025-06-28 + * @description 测试Redis连接、健康检查、基本操作和状态管理功能 + */ + +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; +import { setTimeout } from 'node:timers'; +import { Redis } from '@/utils/redis'; +import { redisConfig } from '@/config/redis.config'; + +// 简单的延时函数 +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe('Redis连接测试', () => { + let redis: Redis; + + beforeAll(async () => { + redis = new Redis(); + // 等待连接建立 + await delay(1000); + }); + + afterAll(async () => { + if (redis) { + await redis.disconnectRedis(); + } + }); + + beforeEach(async () => { + // 确保每个测试前Redis连接正常 + if (!redis.redisClient.isOpen) { + await redis.connectRedis(); + } + }); + + describe('Redis连接管理', () => { + it('应该成功连接到Redis服务器', async () => { + const isHealthy = await redis.checkRedisHealth(); + expect(isHealthy).toBe(true); + }); + + it('应该正确返回Redis连接状态', () => { + const status = redis.getRedisStatus(); + expect(status).toEqual({ + isConnected: expect.any(Boolean), + config: { + host: redisConfig.host, + port: redisConfig.port, + database: redisConfig.database, + connectName: redisConfig.connectName, + }, + }); + }); + + it('应该能够执行ping命令', async () => { + const result = await redis.redisClient.ping(); + expect(result).toBe('PONG'); + }); + }); + + describe('Redis基本操作', () => { + const testKey = 'test_key'; + const testValue = 'test_value'; + + it('应该能够设置和获取字符串值', async () => { + // 设置值 + await redis.redisClient.set(testKey, testValue); + + // 获取值 + const result = await redis.redisClient.get(testKey); + expect(result).toBe(testValue); + + // 清理测试数据 + await redis.redisClient.del(testKey); + }); + + it('应该能够设置带过期时间的值', async () => { + const expiryTime = 2; // 2秒过期 + + // 设置带过期时间的值 + await redis.redisClient.setEx(testKey, expiryTime, testValue); + + // 立即获取应该有值 + const result1 = await redis.redisClient.get(testKey); + expect(result1).toBe(testValue); + + // 等待过期 + await delay(2100); + + // 过期后应该为null + const result2 = await redis.redisClient.get(testKey); + expect(result2).toBeNull(); + }); + + it('应该能够检查键是否存在', async () => { + // 设置测试键 + await redis.redisClient.set(testKey, testValue); + + // 检查存在 + const exists1 = await redis.redisClient.exists(testKey); + expect(exists1).toBe(1); + + // 删除键 + await redis.redisClient.del(testKey); + + // 检查不存在 + const exists2 = await redis.redisClient.exists(testKey); + expect(exists2).toBe(0); + }); + + it('应该能够删除键', async () => { + // 设置测试键 + await redis.redisClient.set(testKey, testValue); + + // 删除键 + const deleteCount = await redis.redisClient.del(testKey); + expect(deleteCount).toBe(1); + + // 验证键已被删除 + const result = await redis.redisClient.get(testKey); + expect(result).toBeNull(); + }); + }); + + describe('Redis Hash操作', () => { + const hashKey = 'test_hash'; + const field1 = 'field1'; + const value1 = 'value1'; + const field2 = 'field2'; + const value2 = 'value2'; + + it('应该能够设置和获取Hash字段', async () => { + // 设置Hash字段 + await redis.redisClient.hSet(hashKey, field1, value1); + + // 获取Hash字段 + const result = await redis.redisClient.hGet(hashKey, field1); + expect(result).toBe(value1); + + // 清理测试数据 + await redis.redisClient.del(hashKey); + }); + + it('应该能够设置和获取多个Hash字段', async () => { + // 设置多个Hash字段 + await redis.redisClient.hSet(hashKey, field1, value1); + await redis.redisClient.hSet(hashKey, field2, value2); + + // 获取所有Hash字段 + const result = await redis.redisClient.hGetAll(hashKey); + expect(result).toEqual({ + [field1]: value1, + [field2]: value2, + }); + + // 清理测试数据 + await redis.redisClient.del(hashKey); + }); + }); + + describe('Redis列表操作', () => { + const listKey = 'test_list'; + const value1 = 'item1'; + const value2 = 'item2'; + + it('应该能够推入和弹出列表元素', async () => { + // 推入元素 + await redis.redisClient.lPush(listKey, value1); + await redis.redisClient.lPush(listKey, value2); + + // 获取列表长度 + const length = await redis.redisClient.lLen(listKey); + expect(length).toBe(2); + + // 弹出元素 + const poppedValue = await redis.redisClient.lPop(listKey); + expect(poppedValue).toBe(value2); + + // 清理测试数据 + await redis.redisClient.del(listKey); + }); + }); + + describe('Redis连接错误处理', () => { + it('健康检查在连接断开时应返回false', async () => { + // 暂时断开连接 + await redis.disconnectRedis(); + + // 健康检查应该返回false + const isHealthy = await redis.checkRedisHealth(); + expect(isHealthy).toBe(false); + + // 重新连接 + await redis.connectRedis(); + }); + + it('应该能够重新连接Redis', async () => { + // 断开连接 + await redis.disconnectRedis(); + + // 重新连接 + await redis.connectRedis(); + + // 验证连接正常 + const isHealthy = await redis.checkRedisHealth(); + expect(isHealthy).toBe(true); + }); + }); + + describe('Redis性能测试', () => { + it('应该能够快速执行大量set操作', async () => { + const startTime = Date.now(); + const operations = []; + + // 执行100次set操作 + for (let i = 0; i < 1000; i++) { + operations.push(redis.redisClient.set(`perf_test_${i}`, `value_${i}`)); + } + + await Promise.all(operations); + const endTime = Date.now(); + + expect(endTime - startTime).toBeLessThan(1000); // 应该在1秒内完成 + console.log(endTime - startTime); + + // 清理测试数据 + const deleteOperations = []; + for (let i = 0; i < 100; i++) { + deleteOperations.push(redis.redisClient.del(`perf_test_${i}`)); + } + await Promise.all(deleteOperations); + }); + }); +}); diff --git a/src/tests/swagger.test.ts b/src/tests/swagger.test.ts index 72bed54..505b1d5 100644 --- a/src/tests/swagger.test.ts +++ b/src/tests/swagger.test.ts @@ -1,291 +1,290 @@ -/** - * @file Swagger文档功能测试 - * @author hotok - * @date 2025-06-28 - * @lastEditor hotok - * @lastEditTime 2025-06-28 - * @description 测试Swagger API文档功能,验证文档可访问性和内容完整性 - */ - -import { describe, it, expect } from 'vitest'; -import { setTimeout } from 'node:timers'; -import { app } from '@/app'; - -describe('Swagger API文档测试', () => { - describe('GET /docs', () => { - it('应该可以访问Swagger文档页面', async () => { - const res = await app.fetch( - new Request('http://localhost/docs', { - method: 'GET', - }), - ); - - expect(res.status).toBe(200); - - const contentType = res.headers.get('content-type'); - expect(contentType).toContain('text/html'); - }); - - it('Swagger文档应该包含基本配置信息', async () => { - const res = await app.fetch( - new Request('http://localhost/docs', { - method: 'GET', - }), - ); - - const html = await res.text(); - - // 检查基本配置 - expect(html).toContain('Cursor Init API服务'); - expect(html).toContain('swagger-ui'); - }); - }); - - describe('GET /docs/json', () => { - it('应该返回OpenAPI JSON文档', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - expect(res.status).toBe(200); - - const contentType = res.headers.get('content-type'); - expect(contentType).toContain('application/json'); - - const openApiDoc = await res.json(); - - // 验证OpenAPI文档结构 - expect(openApiDoc).toHaveProperty('openapi'); - expect(openApiDoc).toHaveProperty('info'); - expect(openApiDoc).toHaveProperty('paths'); - expect(openApiDoc).toHaveProperty('components'); - - // 验证基本信息 - expect(openApiDoc.info.title).toBe('Cursor Init API服务'); - expect(openApiDoc.info.version).toBe('1.0.0'); - expect(openApiDoc.info.description).toContain('Cursor Init API服务'); - }); - - it('OpenAPI文档应该包含安全配置', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const openApiDoc = await res.json(); - - // 验证安全配置 - expect(openApiDoc.components).toHaveProperty('securitySchemes'); - expect(openApiDoc.components.securitySchemes).toHaveProperty('bearerAuth'); - expect(openApiDoc.components.securitySchemes.bearerAuth.type).toBe('http'); - expect(openApiDoc.components.securitySchemes.bearerAuth.scheme).toBe('bearer'); - expect(openApiDoc.components.securitySchemes.bearerAuth.bearerFormat).toBe('JWT'); - }); - - it('OpenAPI文档应该包含全局组件定义', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const openApiDoc = await res.json(); - - // 验证全局组件 - expect(openApiDoc.components).toHaveProperty('schemas'); - expect(openApiDoc.components).toHaveProperty('responses'); - - // 验证响应组件 - const responses = openApiDoc.components.responses; - expect(responses).toHaveProperty('Success'); - expect(responses).toHaveProperty('BadRequest'); - expect(responses).toHaveProperty('Unauthorized'); - expect(responses).toHaveProperty('Forbidden'); - expect(responses).toHaveProperty('NotFound'); - expect(responses).toHaveProperty('ValidationError'); - expect(responses).toHaveProperty('InternalError'); - expect(responses).toHaveProperty('ServiceUnavailable'); - - // 验证Schema组件 - const schemas = openApiDoc.components.schemas; - expect(schemas).toHaveProperty('BaseResponse'); - expect(schemas).toHaveProperty('SuccessResponse'); - expect(schemas).toHaveProperty('ErrorResponse'); - expect(schemas).toHaveProperty('PaginationResponse'); - }); - - it('OpenAPI文档应该包含健康检查接口', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const openApiDoc = await res.json(); - - // 验证健康检查接口 - expect(openApiDoc.paths).toHaveProperty('/api/health'); - expect(openApiDoc.paths).toHaveProperty('/api/health/detailed'); - - const healthPath = openApiDoc.paths['/api/health']; - expect(healthPath).toHaveProperty('get'); - expect(healthPath.get).toHaveProperty('tags'); - expect(healthPath.get.tags).toContain('健康检查'); - expect(healthPath.get).toHaveProperty('summary'); - expect(healthPath.get).toHaveProperty('description'); - }); - - it('OpenAPI文档应该包含认证接口', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const openApiDoc = await res.json(); - - // 验证认证接口 - expect(openApiDoc.paths).toHaveProperty('/api/auth/login'); - - const loginPath = openApiDoc.paths['/api/auth/login']; - expect(loginPath).toHaveProperty('post'); - expect(loginPath.post).toHaveProperty('tags'); - expect(loginPath.post).toHaveProperty('requestBody'); - expect(loginPath.post).toHaveProperty('responses'); - }); - - it('OpenAPI文档应该包含标签分类', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const openApiDoc = await res.json(); - - // 验证标签 - expect(openApiDoc).toHaveProperty('tags'); - expect(Array.isArray(openApiDoc.tags)).toBe(true); - - const tagNames = openApiDoc.tags.map((tag: any) => tag.name); - expect(tagNames).toContain('认证管理'); - expect(tagNames).toContain('健康检查'); - - // 验证标签描述 - const healthTag = openApiDoc.tags.find((tag: any) => tag.name === '健康检查'); - expect(healthTag).toHaveProperty('description'); - expect(healthTag.description).toContain('系统健康状态'); - }); - }); - - describe('Swagger文档内容验证', () => { - it('应该包含错误码说明', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const openApiDoc = await res.json(); - - // 验证错误码描述在文档中 - expect(openApiDoc.info.description).toContain('错误码说明'); - expect(openApiDoc.info.description).toContain('| 错误码 | 说明 | 示例场景 |'); - expect(openApiDoc.info.description).toContain('| 0 | 操作成功 |'); - expect(openApiDoc.info.description).toContain('| 400 | 业务逻辑错误 |'); - expect(openApiDoc.info.description).toContain('| 401 | 身份认证失败 |'); - }); - - it('应该包含认证说明', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const openApiDoc = await res.json(); - - // 验证认证说明 - expect(openApiDoc.info.description).toContain('认证说明'); - expect(openApiDoc.info.description).toContain('Authorization: Bearer'); - expect(openApiDoc.components.securitySchemes.bearerAuth.description).toContain('JWT认证说明'); - expect(openApiDoc.components.securitySchemes.bearerAuth.description).toContain('Token有效期为24小时'); - }); - - it('应该包含响应格式说明', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const openApiDoc = await res.json(); - - // 验证响应格式说明 - expect(openApiDoc.info.description).toContain('响应格式'); - expect(openApiDoc.info.description).toContain('"code": 0'); - expect(openApiDoc.info.description).toContain('"message": "操作成功"'); - expect(openApiDoc.info.description).toContain('"data"'); - }); - - it('应该包含示例响应', async () => { - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const openApiDoc = await res.json(); - - // 验证示例响应 - const successResponse = openApiDoc.components.responses.Success; - expect(successResponse.content['application/json']).toHaveProperty('examples'); - - const errorResponse = openApiDoc.components.responses.BadRequest; - expect(errorResponse.content['application/json']).toHaveProperty('examples'); - - const unauthorizedResponse = openApiDoc.components.responses.Unauthorized; - expect(unauthorizedResponse.content['application/json']).toHaveProperty('examples'); - expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('tokenExpired'); - expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('tokenInvalid'); - expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('notLoggedIn'); - }); - }); - - describe('Swagger文档性能测试', () => { - it('文档页面应该快速加载', async () => { - const startTime = Date.now(); - - const res = await app.fetch( - new Request('http://localhost/docs', { - method: 'GET', - }), - ); - - const endTime = Date.now(); - const responseTime = endTime - startTime; - - expect(res.status).toBe(200); - expect(responseTime).toBeLessThan(1000); // 应该在1秒内完成 - }); - - it('JSON文档应该快速响应', async () => { - const startTime = Date.now(); - - const res = await app.fetch( - new Request('http://localhost/docs/json', { - method: 'GET', - }), - ); - - const endTime = Date.now(); - const responseTime = endTime - startTime; - - expect(res.status).toBe(200); - expect(responseTime).toBeLessThan(500); // JSON文档应该更快 - }); - }); -}); \ No newline at end of file +/** + * @file Swagger文档功能测试 + * @author hotok + * @date 2025-06-28 + * @lastEditor hotok + * @lastEditTime 2025-06-28 + * @description 测试Swagger API文档功能,验证文档可访问性和内容完整性 + */ + +import { describe, it, expect } from 'vitest'; +import { app } from '@/app'; + +describe('Swagger API文档测试', () => { + describe('GET /docs', () => { + it('应该可以访问Swagger文档页面', async () => { + const res = await app.fetch( + new Request('http://localhost/docs', { + method: 'GET', + }), + ); + + expect(res.status).toBe(200); + + const contentType = res.headers.get('content-type'); + expect(contentType).toContain('text/html'); + }); + + it('Swagger文档应该包含基本配置信息', async () => { + const res = await app.fetch( + new Request('http://localhost/docs', { + method: 'GET', + }), + ); + + const html = await res.text(); + + // 检查基本配置 + expect(html).toContain('Cursor Init API服务'); + expect(html).toContain('swagger-ui'); + }); + }); + + describe('GET /docs/json', () => { + it('应该返回OpenAPI JSON文档', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + expect(res.status).toBe(200); + + const contentType = res.headers.get('content-type'); + expect(contentType).toContain('application/json'); + + const openApiDoc = await res.json(); + + // 验证OpenAPI文档结构 + expect(openApiDoc).toHaveProperty('openapi'); + expect(openApiDoc).toHaveProperty('info'); + expect(openApiDoc).toHaveProperty('paths'); + expect(openApiDoc).toHaveProperty('components'); + + // 验证基本信息 + expect(openApiDoc.info.title).toBe('Cursor Init API服务'); + expect(openApiDoc.info.version).toBe('1.0.0'); + expect(openApiDoc.info.description).toContain('Cursor Init API服务'); + }); + + it('OpenAPI文档应该包含安全配置', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const openApiDoc = await res.json(); + + // 验证安全配置 + expect(openApiDoc.components).toHaveProperty('securitySchemes'); + expect(openApiDoc.components.securitySchemes).toHaveProperty('bearerAuth'); + expect(openApiDoc.components.securitySchemes.bearerAuth.type).toBe('http'); + expect(openApiDoc.components.securitySchemes.bearerAuth.scheme).toBe('bearer'); + expect(openApiDoc.components.securitySchemes.bearerAuth.bearerFormat).toBe('JWT'); + }); + + it('OpenAPI文档应该包含全局组件定义', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const openApiDoc = await res.json(); + + // 验证全局组件 + expect(openApiDoc.components).toHaveProperty('schemas'); + expect(openApiDoc.components).toHaveProperty('responses'); + + // 验证响应组件 + const responses = openApiDoc.components.responses; + expect(responses).toHaveProperty('Success'); + expect(responses).toHaveProperty('BadRequest'); + expect(responses).toHaveProperty('Unauthorized'); + expect(responses).toHaveProperty('Forbidden'); + expect(responses).toHaveProperty('NotFound'); + expect(responses).toHaveProperty('ValidationError'); + expect(responses).toHaveProperty('InternalError'); + expect(responses).toHaveProperty('ServiceUnavailable'); + + // 验证Schema组件 + const schemas = openApiDoc.components.schemas; + expect(schemas).toHaveProperty('BaseResponse'); + expect(schemas).toHaveProperty('SuccessResponse'); + expect(schemas).toHaveProperty('ErrorResponse'); + expect(schemas).toHaveProperty('PaginationResponse'); + }); + + it('OpenAPI文档应该包含健康检查接口', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const openApiDoc = await res.json(); + + // 验证健康检查接口 + expect(openApiDoc.paths).toHaveProperty('/api/health'); + expect(openApiDoc.paths).toHaveProperty('/api/health/detailed'); + + const healthPath = openApiDoc.paths['/api/health']; + expect(healthPath).toHaveProperty('get'); + expect(healthPath.get).toHaveProperty('tags'); + expect(healthPath.get.tags).toContain('健康检查'); + expect(healthPath.get).toHaveProperty('summary'); + expect(healthPath.get).toHaveProperty('description'); + }); + + it('OpenAPI文档应该包含认证接口', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const openApiDoc = await res.json(); + + // 验证认证接口 + expect(openApiDoc.paths).toHaveProperty('/api/auth/login'); + + const loginPath = openApiDoc.paths['/api/auth/login']; + expect(loginPath).toHaveProperty('post'); + expect(loginPath.post).toHaveProperty('tags'); + expect(loginPath.post).toHaveProperty('requestBody'); + expect(loginPath.post).toHaveProperty('responses'); + }); + + it('OpenAPI文档应该包含标签分类', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const openApiDoc = await res.json(); + + // 验证标签 + expect(openApiDoc).toHaveProperty('tags'); + expect(Array.isArray(openApiDoc.tags)).toBe(true); + + const tagNames = openApiDoc.tags.map((tag: any) => tag.name); + expect(tagNames).toContain('认证管理'); + expect(tagNames).toContain('健康检查'); + + // 验证标签描述 + const healthTag = openApiDoc.tags.find((tag: any) => tag.name === '健康检查'); + expect(healthTag).toHaveProperty('description'); + expect(healthTag.description).toContain('系统健康状态'); + }); + }); + + describe('Swagger文档内容验证', () => { + it('应该包含错误码说明', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const openApiDoc = await res.json(); + + // 验证错误码描述在文档中 + expect(openApiDoc.info.description).toContain('错误码说明'); + expect(openApiDoc.info.description).toContain('| 错误码 | 说明 | 示例场景 |'); + expect(openApiDoc.info.description).toContain('| 0 | 操作成功 |'); + expect(openApiDoc.info.description).toContain('| 400 | 业务逻辑错误 |'); + expect(openApiDoc.info.description).toContain('| 401 | 身份认证失败 |'); + }); + + it('应该包含认证说明', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const openApiDoc = await res.json(); + + // 验证认证说明 + expect(openApiDoc.info.description).toContain('认证说明'); + expect(openApiDoc.info.description).toContain('Authorization: Bearer'); + expect(openApiDoc.components.securitySchemes.bearerAuth.description).toContain('JWT认证说明'); + expect(openApiDoc.components.securitySchemes.bearerAuth.description).toContain('Token有效期为24小时'); + }); + + it('应该包含响应格式说明', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const openApiDoc = await res.json(); + + // 验证响应格式说明 + expect(openApiDoc.info.description).toContain('响应格式'); + expect(openApiDoc.info.description).toContain('"code": 0'); + expect(openApiDoc.info.description).toContain('"message": "操作成功"'); + expect(openApiDoc.info.description).toContain('"data"'); + }); + + it('应该包含示例响应', async () => { + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const openApiDoc = await res.json(); + + // 验证示例响应 + const successResponse = openApiDoc.components.responses.Success; + expect(successResponse.content['application/json']).toHaveProperty('examples'); + + const errorResponse = openApiDoc.components.responses.BadRequest; + expect(errorResponse.content['application/json']).toHaveProperty('examples'); + + const unauthorizedResponse = openApiDoc.components.responses.Unauthorized; + expect(unauthorizedResponse.content['application/json']).toHaveProperty('examples'); + expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('tokenExpired'); + expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('tokenInvalid'); + expect(unauthorizedResponse.content['application/json'].examples).toHaveProperty('notLoggedIn'); + }); + }); + + describe('Swagger文档性能测试', () => { + it('文档页面应该快速加载', async () => { + const startTime = Date.now(); + + const res = await app.fetch( + new Request('http://localhost/docs', { + method: 'GET', + }), + ); + + const endTime = Date.now(); + const responseTime = endTime - startTime; + + expect(res.status).toBe(200); + expect(responseTime).toBeLessThan(1000); // 应该在1秒内完成 + }); + + it('JSON文档应该快速响应', async () => { + const startTime = Date.now(); + + const res = await app.fetch( + new Request('http://localhost/docs/json', { + method: 'GET', + }), + ); + + const endTime = Date.now(); + const responseTime = endTime - startTime; + + expect(res.status).toBe(200); + expect(responseTime).toBeLessThan(500); // JSON文档应该更快 + }); + }); +}); diff --git a/src/type/drizzle.type.ts b/src/type/drizzle.type.ts index 0dbf46c..7c92046 100644 --- a/src/type/drizzle.type.ts +++ b/src/type/drizzle.type.ts @@ -49,4 +49,4 @@ export interface DatabaseConnectionInfo { connectedAt?: Date; /** 错误信息 */ error?: string; -} \ No newline at end of file +} diff --git a/src/type/email.type.ts b/src/type/email.type.ts index 50ec761..5328b90 100644 --- a/src/type/email.type.ts +++ b/src/type/email.type.ts @@ -253,4 +253,4 @@ export interface EmailHealthCheckResult { serviceStatus: EmailServiceStatus; /** 错误信息 */ error?: string; -} \ No newline at end of file +} diff --git a/src/type/jwt.type.ts b/src/type/jwt.type.ts index 887776b..6511e73 100644 --- a/src/type/jwt.type.ts +++ b/src/type/jwt.type.ts @@ -1,164 +1,164 @@ -/** - * @file JWT类型定义 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-07-06 - * @description JWT Token载荷和用户信息的TypeScript类型定义 - */ - -/** - * Token类型枚举 - */ -export const TOKEN_TYPES = { - ACCESS: 'access', - REFRESH: 'refresh', - ACTIVATION: 'activation', - PASSWORD_RESET: 'password_reset', -} as const; - -/** - * Token类型定义 - */ -export type TokenType = typeof TOKEN_TYPES[keyof typeof TOKEN_TYPES]; - -/** - * JWT Token中的用户信息类型 - * @description 存储在JWT Token中的用户基本信息,不包含敏感数据 - */ -export interface JwtUserType { - /** 用户ID(bigint类型以字符串形式存储防止精度丢失) */ - userId: string; - /** 用户名 */ - username: string; - /** 用户邮箱 */ - email: string; - /** 用户昵称 */ - nickname?: string; - /** 用户状态:0-禁用,1-启用 */ - status: number; - /** 用户角色(可选,用于权限控制) */ - role?: string; -} - -/** - * 完整的JWT载荷类型 - * @description JWT Token的完整载荷,包含用户信息和JWT标准字段 - */ -export interface JwtPayloadType extends JwtUserType { - /** Token发行时间(秒级时间戳) */ - iat: number; - /** Token过期时间(秒级时间戳) */ - exp: number; - /** Token主题,通常是用户ID */ - sub?: string; - /** Token发行者 */ - iss?: string; - /** Token受众 */ - aud?: string; - /** JWT ID */ - jti?: string; - /** Token生效时间(秒级时间戳) */ - nbf?: number; - error?: boolean; - type?: TokenType; -} - -/** - * JWT载荷基础类型(包含token类型和盐值) - * @description 所有特定类型token的基础载荷结构 - */ -export interface BaseJwtPayload { - /** 用户ID(bigint类型以字符串形式存储防止精度丢失) */ - userId: string; - /** 用户名 */ - username: string; - /** 邮箱 */ - email: string; - /** Token类型 */ - tokenType: TokenType; - /** 盐值哈希 */ - saltHash: string; - /** 签发者 */ - iss: string; - /** 受众 */ - aud: string; - /** 主题 */ - sub: string; - /** 签发时间 */ - iat: number; - /** 过期时间 */ - exp: number; -} - -/** - * 激活Token载荷类型 - * @description 邮箱激活token的载荷结构 - */ -export interface ActivationTokenPayload extends BaseJwtPayload { - tokenType: 'activation'; - /** 邮箱(用于激活验证) */ - email: string; - /** 用途说明 */ - purpose: 'email_activation'; -} - -/** - * 访问Token载荷类型 - * @description 访问token的载荷结构 - */ -export interface AccessTokenPayload extends BaseJwtPayload { - tokenType: 'access'; - /** 昵称 */ - nickname?: string; - /** 用户状态 */ - status: string; - /** 角色 */ - role?: string; -} - -/** - * 刷新Token载荷类型 - * @description 刷新token的载荷结构 - */ -export interface RefreshTokenPayload extends BaseJwtPayload { - tokenType: 'refresh'; - /** 原始访问token的ID(用于关联) */ - accessTokenId?: string; -} - -/** - * 密码重置Token载荷类型 - * @description 密码重置token的载荷结构 - */ -export interface PasswordResetTokenPayload extends BaseJwtPayload { - tokenType: 'password_reset'; - /** 用途说明 */ - purpose: 'password_reset'; -} - -/** - * JWT认证上下文类型 - * @description 在认证中间件中使用的用户上下文类型 - */ -export interface JwtContextType { - /** 当前认证用户信息 */ - user: JwtUserType; - /** 原始JWT载荷 */ - payload?: JwtPayloadType; -} - -/** - * JWT生成参数类型 - * @description 生成JWT Token时的参数类型 - */ -export interface JwtSignOptionsType { - /** 用户信息 */ - user: JwtUserType; - /** 自定义过期时间(可选) */ - expiresIn?: string; - /** 自定义发行者(可选) */ - issuer?: string; - /** 自定义受众(可选) */ - audience?: string; -} \ No newline at end of file +/** + * @file JWT类型定义 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-07-06 + * @description JWT Token载荷和用户信息的TypeScript类型定义 + */ + +/** + * Token类型枚举 + */ +export const TOKEN_TYPES = { + ACCESS: 'access', + REFRESH: 'refresh', + ACTIVATION: 'activation', + PASSWORD_RESET: 'password_reset', +} as const; + +/** + * Token类型定义 + */ +export type TokenType = (typeof TOKEN_TYPES)[keyof typeof TOKEN_TYPES]; + +/** + * JWT Token中的用户信息类型 + * @description 存储在JWT Token中的用户基本信息,不包含敏感数据 + */ +export interface JwtUserType { + /** 用户ID(bigint类型以字符串形式存储防止精度丢失) */ + userId: string; + /** 用户名 */ + username: string; + /** 用户邮箱 */ + email: string; + /** 用户昵称 */ + nickname?: string; + /** 用户状态:0-禁用,1-启用 */ + status: number; + /** 用户角色(可选,用于权限控制) */ + role?: string; +} + +/** + * 完整的JWT载荷类型 + * @description JWT Token的完整载荷,包含用户信息和JWT标准字段 + */ +export interface JwtPayloadType extends JwtUserType { + /** Token发行时间(秒级时间戳) */ + iat: number; + /** Token过期时间(秒级时间戳) */ + exp: number; + /** Token主题,通常是用户ID */ + sub?: string; + /** Token发行者 */ + iss?: string; + /** Token受众 */ + aud?: string; + /** JWT ID */ + jti?: string; + /** Token生效时间(秒级时间戳) */ + nbf?: number; + error?: boolean; + type?: TokenType; +} + +/** + * JWT载荷基础类型(包含token类型和盐值) + * @description 所有特定类型token的基础载荷结构 + */ +export interface BaseJwtPayload { + /** 用户ID(bigint类型以字符串形式存储防止精度丢失) */ + userId: string; + /** 用户名 */ + username: string; + /** 邮箱 */ + email: string; + /** Token类型 */ + tokenType: TokenType; + /** 盐值哈希 */ + saltHash: string; + /** 签发者 */ + iss: string; + /** 受众 */ + aud: string; + /** 主题 */ + sub: string; + /** 签发时间 */ + iat: number; + /** 过期时间 */ + exp: number; +} + +/** + * 激活Token载荷类型 + * @description 邮箱激活token的载荷结构 + */ +export interface ActivationTokenPayload extends BaseJwtPayload { + tokenType: 'activation'; + /** 邮箱(用于激活验证) */ + email: string; + /** 用途说明 */ + purpose: 'email_activation'; +} + +/** + * 访问Token载荷类型 + * @description 访问token的载荷结构 + */ +export interface AccessTokenPayload extends BaseJwtPayload { + tokenType: 'access'; + /** 昵称 */ + nickname?: string; + /** 用户状态 */ + status: string; + /** 角色 */ + role?: string; +} + +/** + * 刷新Token载荷类型 + * @description 刷新token的载荷结构 + */ +export interface RefreshTokenPayload extends BaseJwtPayload { + tokenType: 'refresh'; + /** 原始访问token的ID(用于关联) */ + accessTokenId?: string; +} + +/** + * 密码重置Token载荷类型 + * @description 密码重置token的载荷结构 + */ +export interface PasswordResetTokenPayload extends BaseJwtPayload { + tokenType: 'password_reset'; + /** 用途说明 */ + purpose: 'password_reset'; +} + +/** + * JWT认证上下文类型 + * @description 在认证中间件中使用的用户上下文类型 + */ +export interface JwtContextType { + /** 当前认证用户信息 */ + user: JwtUserType; + /** 原始JWT载荷 */ + payload?: JwtPayloadType; +} + +/** + * JWT生成参数类型 + * @description 生成JWT Token时的参数类型 + */ +export interface JwtSignOptionsType { + /** 用户信息 */ + user: JwtUserType; + /** 自定义过期时间(可选) */ + expiresIn?: string; + /** 自定义发行者(可选) */ + issuer?: string; + /** 自定义受众(可选) */ + audience?: string; +} diff --git a/src/type/logger.type.ts b/src/type/logger.type.ts index a4f6078..2f2af9b 100644 --- a/src/type/logger.type.ts +++ b/src/type/logger.type.ts @@ -1,4 +1,3 @@ - /** * Logger类的类型定义 */ @@ -15,4 +14,4 @@ export interface LoggerInstance { http(message: string | object): void; /** 详细级别日志 */ verbose(message: string | object): void; -} \ No newline at end of file +} diff --git a/src/type/redis.type.ts b/src/type/redis.type.ts index 28ea158..dfa808d 100644 --- a/src/type/redis.type.ts +++ b/src/type/redis.type.ts @@ -61,4 +61,4 @@ export interface RedisHealthCheckResult { connectionInfo: RedisConnectionInfo; /** 错误信息 */ error?: string; -} \ No newline at end of file +} diff --git a/src/utils/distributedLock.ts b/src/utils/distributedLock.ts index da78a6b..7f1440e 100644 --- a/src/utils/distributedLock.ts +++ b/src/utils/distributedLock.ts @@ -52,13 +52,13 @@ export interface DistributedLock { export class DistributedLockService { /** 锁前缀 */ private static readonly LOCK_PREFIX = 'distributed_lock:'; - + /** 默认TTL(秒) */ private static readonly DEFAULT_TTL = 30; - + /** 默认超时时间(毫秒) */ private static readonly DEFAULT_TIMEOUT = 5000; - + /** 默认续期间隔(毫秒) */ private static readonly DEFAULT_RENEW_INTERVAL = 10000; @@ -85,15 +85,15 @@ export class DistributedLockService { while (Date.now() - startTime < timeout) { // 使用 SET key value NX EX seconds 原子操作 const result = await redisService.client.set(lockKey, lockValue, { - NX: true, // 只有当 key 不存在时才设置 - EX: ttl // 设置过期时间(秒) + NX: true, // 只有当 key 不存在时才设置 + EX: ttl, // 设置过期时间(秒) }); - + if (result === 'OK') { acquired = true; break; } - + // 等待一段时间后重试 await this.sleep(100); } @@ -110,7 +110,7 @@ export class DistributedLockService { value: lockValue, acquired: true, acquiredAt: Date.now(), - + // 释放锁 release: async (): Promise => { // 清理定时器和事件监听器 @@ -124,7 +124,7 @@ export class DistributedLockService { process.removeListener('SIGTERM', processExitHandler); processExitHandler = null; } - + const released = await this.releaseLock(lockKey, lockValue); if (released) { lock.acquired = false; @@ -132,11 +132,11 @@ export class DistributedLockService { } return released; }, - + // 续期锁 renew: async (): Promise => { return await this.renewLock(lockKey, lockValue, ttl); - } + }, }; // 启动自动续期(仅在需要时) @@ -165,14 +165,13 @@ export class DistributedLockService { Logger.warn(`进程退出,强制释放分布式锁: ${lockKey}`); await this.forceRelease(config.key); }; - + process.on('exit', processExitHandler); process.on('SIGINT', processExitHandler); process.on('SIGTERM', processExitHandler); } return lock; - } catch (error) { // 清理已创建的定时器和事件监听器 if (renewTimer) { @@ -183,7 +182,7 @@ export class DistributedLockService { process.removeListener('SIGINT', processExitHandler); process.removeListener('SIGTERM', processExitHandler); } - + Logger.error(new Error(`获取分布式锁失败: ${lockKey}, error: ${error}`)); throw error; } @@ -205,12 +204,12 @@ export class DistributedLockService { return 0 end `; - + const result = await redisService.client.eval(luaScript, { keys: [lockKey], - arguments: [lockValue] + arguments: [lockValue], }); - + return result === 1; } catch (error) { Logger.error(new Error(`释放锁失败: ${lockKey}, error: ${error}`)); @@ -235,12 +234,12 @@ export class DistributedLockService { return 0 end `; - + const result = await redisService.client.eval(luaScript, { keys: [lockKey], - arguments: [lockValue, ttl.toString()] + arguments: [lockValue, ttl.toString()], }); - + return result === 1; } catch (error) { Logger.error(new Error(`续期锁失败: ${lockKey}, error: ${error}`)); @@ -291,7 +290,7 @@ export class DistributedLockService { * @returns Promise */ private static sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } } @@ -312,7 +311,7 @@ export const LOCK_KEYS = { // 验证码生成锁 CAPTCHA_GENERATE: 'captcha:generate', // 邮件发送锁 - EMAIL_SEND: 'email:send' + EMAIL_SEND: 'email:send', } as const; /** @@ -324,22 +323,22 @@ export const LOCK_KEYS = { export function withDistributedLock(lockKey: string, ttl: number = 30, timeout: number = 5000) { return function (target: any, propertyName: string, descriptor: PropertyDescriptor) { const method = descriptor.value; - + descriptor.value = async function (...args: any[]) { const lock = await DistributedLockService.acquire({ key: lockKey, ttl, timeout, - autoRenew: true + autoRenew: true, }); - + try { return await method.apply(this, args); } finally { await lock.release(); } }; - + return descriptor; }; -} \ No newline at end of file +} diff --git a/src/utils/formatRoute.ts b/src/utils/formatRoute.ts index 6eda223..1c24c32 100644 --- a/src/utils/formatRoute.ts +++ b/src/utils/formatRoute.ts @@ -1,5 +1,5 @@ -import chalk from 'chalk'; - -export const formatRoute = (router: any) => { - return router.map((route: any) => chalk.green.bold(route.method.padEnd(6, ' ')) + ' ' + route.path).join('\n'); -}; +import chalk from 'chalk'; + +export const formatRoute = (router: any) => { + return router.map((route: any) => chalk.green.bold(route.method.padEnd(6, ' ')) + ' ' + route.path).join('\n'); +}; diff --git a/src/utils/jwt.helper.ts b/src/utils/jwt.helper.ts index 191f1ec..8b14d51 100644 --- a/src/utils/jwt.helper.ts +++ b/src/utils/jwt.helper.ts @@ -1,138 +1,138 @@ -/** - * @file JWT辅助工具 - * @author hotok - * @date 2025-06-29 - * @lastEditor hotok - * @lastEditTime 2025-06-29 - * @description 提供JWT相关的辅助函数,用于Controller中的JWT操作 - */ - -import type { JwtUserType, JwtPayloadType } from '@/type/jwt.type'; -import type { UserInfoType } from '@/modules/example/example.schema'; - -/** - * 从完整用户信息创建JWT用户信息 - * @param userInfo 完整的用户信息 - * @param role 可选的用户角色 - * @returns JwtUserType JWT中的用户信息 - * @modification hotok 2025-06-29 创建JWT用户信息转换函数 - */ -export function createJwtUser(userInfo: UserInfoType, role?: string): JwtUserType { - return { - userId: userInfo.id, - username: userInfo.username, - email: userInfo.email, - nickname: userInfo.nickname, - status: userInfo.status, - role: role, - }; -} - -/** - * 创建JWT载荷 - * @param userInfo 完整的用户信息 - * @param options 可选配置 - * @returns JWT载荷对象(不包含iat、exp等自动生成字段) - * @modification hotok 2025-06-29 创建JWT载荷生成函数 - */ -export function createJwtPayload( - userInfo: UserInfoType, - options?: { - role?: string; - issuer?: string; - audience?: string; - subject?: string; - }, -): Omit { - const jwtUser = createJwtUser(userInfo, options?.role); - - return { - ...jwtUser, - sub: options?.subject || userInfo.id.toString(), - iss: options?.issuer || 'elysia-api', - aud: options?.audience || 'web-client', - }; -} - -/** - * 验证JWT载荷中的用户是否有效 - * @param payload JWT载荷 - * @returns boolean 用户是否有效 - * @modification hotok 2025-06-29 添加用户有效性验证 - */ -export function isValidJwtUser(payload: JwtPayloadType): boolean { - // 检查必需字段 - if (!payload.userId || !payload.username || !payload.email) { - return false; - } - - // 检查用户状态(1为启用) - if (payload.status !== 1) { - return false; - } - - return true; -} - -/** - * 检查JWT是否即将过期 - * @param payload JWT载荷 - * @param thresholdMinutes 阈值分钟数(默认30分钟) - * @returns boolean 是否即将过期 - * @modification hotok 2025-06-29 添加过期检查功能 - */ -export function isTokenExpiringSoon(payload: JwtPayloadType, thresholdMinutes: number = 30): boolean { - if (!payload.exp) return false; - - const now = Math.floor(Date.now() / 1000); - const threshold = thresholdMinutes * 60; - - return (payload.exp - now) <= threshold; -} - -/** - * 获取Token剩余有效时间 - * @param payload JWT载荷 - * @returns number 剩余秒数,-1表示已过期或无过期时间 - * @modification hotok 2025-06-29 添加时间计算功能 - */ -export function getTokenRemainingTime(payload: JwtPayloadType): number { - if (!payload.exp) return -1; - - const now = Math.floor(Date.now() / 1000); - const remaining = payload.exp - now; - - return remaining > 0 ? remaining : -1; -} - -/** - * 格式化Token剩余时间为可读字符串 - * @param seconds 剩余秒数 - * @returns string 格式化的时间字符串 - * @modification hotok 2025-06-29 添加时间格式化功能 - */ -export function formatRemainingTime(seconds: number): string { - if (seconds <= 0) return '已过期'; - - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const remainingSeconds = seconds % 60; - - if (hours > 0) { - return `${hours}小时${minutes}分钟`; - } else if (minutes > 0) { - return `${minutes}分钟${remainingSeconds}秒`; - } else { - return `${remainingSeconds}秒`; - } -} - -/** - * JWT用户信息的简化版本(用于日志记录) - * @param user JWT用户信息 - * @returns string 简化的用户信息字符串 - * @modification hotok 2025-06-29 添加用户信息格式化功能 - */ -export function formatUserForLog(user: JwtUserType): string { - return `用户ID:${user.userId} 用户名:${user.username} 状态:${user.status === 1 ? '启用' : '禁用'}`; -} \ No newline at end of file +/** + * @file JWT辅助工具 + * @author hotok + * @date 2025-06-29 + * @lastEditor hotok + * @lastEditTime 2025-06-29 + * @description 提供JWT相关的辅助函数,用于Controller中的JWT操作 + */ + +import type { JwtUserType, JwtPayloadType } from '@/type/jwt.type'; +import type { UserInfoType } from '@/modules/example/example.schema'; + +/** + * 从完整用户信息创建JWT用户信息 + * @param userInfo 完整的用户信息 + * @param role 可选的用户角色 + * @returns JwtUserType JWT中的用户信息 + * @modification hotok 2025-06-29 创建JWT用户信息转换函数 + */ +export function createJwtUser(userInfo: UserInfoType, role?: string): JwtUserType { + return { + userId: userInfo.id, + username: userInfo.username, + email: userInfo.email, + nickname: userInfo.nickname, + status: userInfo.status, + role: role, + }; +} + +/** + * 创建JWT载荷 + * @param userInfo 完整的用户信息 + * @param options 可选配置 + * @returns JWT载荷对象(不包含iat、exp等自动生成字段) + * @modification hotok 2025-06-29 创建JWT载荷生成函数 + */ +export function createJwtPayload( + userInfo: UserInfoType, + options?: { + role?: string; + issuer?: string; + audience?: string; + subject?: string; + }, +): Omit { + const jwtUser = createJwtUser(userInfo, options?.role); + + return { + ...jwtUser, + sub: options?.subject || userInfo.id.toString(), + iss: options?.issuer || 'elysia-api', + aud: options?.audience || 'web-client', + }; +} + +/** + * 验证JWT载荷中的用户是否有效 + * @param payload JWT载荷 + * @returns boolean 用户是否有效 + * @modification hotok 2025-06-29 添加用户有效性验证 + */ +export function isValidJwtUser(payload: JwtPayloadType): boolean { + // 检查必需字段 + if (!payload.userId || !payload.username || !payload.email) { + return false; + } + + // 检查用户状态(1为启用) + if (payload.status !== 1) { + return false; + } + + return true; +} + +/** + * 检查JWT是否即将过期 + * @param payload JWT载荷 + * @param thresholdMinutes 阈值分钟数(默认30分钟) + * @returns boolean 是否即将过期 + * @modification hotok 2025-06-29 添加过期检查功能 + */ +export function isTokenExpiringSoon(payload: JwtPayloadType, thresholdMinutes: number = 30): boolean { + if (!payload.exp) return false; + + const now = Math.floor(Date.now() / 1000); + const threshold = thresholdMinutes * 60; + + return payload.exp - now <= threshold; +} + +/** + * 获取Token剩余有效时间 + * @param payload JWT载荷 + * @returns number 剩余秒数,-1表示已过期或无过期时间 + * @modification hotok 2025-06-29 添加时间计算功能 + */ +export function getTokenRemainingTime(payload: JwtPayloadType): number { + if (!payload.exp) return -1; + + const now = Math.floor(Date.now() / 1000); + const remaining = payload.exp - now; + + return remaining > 0 ? remaining : -1; +} + +/** + * 格式化Token剩余时间为可读字符串 + * @param seconds 剩余秒数 + * @returns string 格式化的时间字符串 + * @modification hotok 2025-06-29 添加时间格式化功能 + */ +export function formatRemainingTime(seconds: number): string { + if (seconds <= 0) return '已过期'; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + if (hours > 0) { + return `${hours}小时${minutes}分钟`; + } else if (minutes > 0) { + return `${minutes}分钟${remainingSeconds}秒`; + } else { + return `${remainingSeconds}秒`; + } +} + +/** + * JWT用户信息的简化版本(用于日志记录) + * @param user JWT用户信息 + * @returns string 简化的用户信息字符串 + * @modification hotok 2025-06-29 添加用户信息格式化功能 + */ +export function formatUserForLog(user: JwtUserType): string { + return `用户ID:${user.userId} 用户名:${user.username} 状态:${user.status === 1 ? '启用' : '禁用'}`; +} diff --git a/src/utils/pagination.ts b/src/utils/pagination.ts index f8363af..9c1acd6 100644 --- a/src/utils/pagination.ts +++ b/src/utils/pagination.ts @@ -7,42 +7,47 @@ * @description 分页查询相关的Schema和工具函数 */ -import { t, type Static, type TSchema } from 'elysia'; +import { t, type Static } from 'elysia'; /** * 基础分页查询参数Schema */ export const BasePaginationSchema = t.Object({ /** 页码,从1开始 */ - page: t.Optional(t.Number({ - minimum: 1, - description: '页码,从1开始', - examples: [1, 2, 3], - default: 1 - })), + page: t.Optional( + t.Number({ + minimum: 1, + description: '页码,从1开始', + examples: [1, 2, 3], + default: 1, + }), + ), /** 每页大小,最大100 */ - pageSize: t.Optional(t.Number({ - minimum: 1, - maximum: 100, - description: '每页大小,最大100', - examples: [10, 20, 50], - default: 20 - })), + pageSize: t.Optional( + t.Number({ + minimum: 1, + maximum: 100, + description: '每页大小,最大100', + examples: [10, 20, 50], + default: 20, + }), + ), /** 排序字段 */ - sortBy: t.Optional(t.String({ - description: '排序字段', - examples: ['createdAt', 'updatedAt', 'username', 'email'], - default: 'createdAt' - })), + sortBy: t.Optional( + t.String({ + description: '排序字段', + examples: ['createdAt', 'updatedAt', 'username', 'email'], + default: 'createdAt', + }), + ), /** 排序方向 */ - sortOrder: t.Optional(t.Union([ - t.Literal('asc'), - t.Literal('desc') - ], { - description: '排序方向', - examples: ['asc', 'desc'], - default: 'desc' - })) + sortOrder: t.Optional( + t.Union([t.Literal('asc'), t.Literal('desc')], { + description: '排序方向', + examples: ['asc', 'desc'], + default: 'desc', + }), + ), }); /** @@ -54,9 +59,9 @@ export const BasePaginationSchema = t.Object({ export const createQuerySchema = (customSchema: any) => { return t.Object({ ...BasePaginationSchema.properties, - ...customSchema.properties + ...customSchema.properties, }); -} +}; /** * 基础分页查询参数类型 @@ -72,20 +77,20 @@ export const createPaginationResponseSchema = (dataSchema: T) => { /** 总记录数 */ total: t.Number({ description: '总记录数', - examples: [100, 250, 1000] + examples: [100, 250, 1000], }), /** 当前页码 */ page: t.Number({ description: '当前页码', - examples: [1, 2, 3] + examples: [1, 2, 3], }), /** 每页大小 */ pageSize: t.Number({ description: '每页大小', - examples: [10, 20, 50] + examples: [10, 20, 50], }), /** 数据列表 */ - data: t.Array(dataSchema) + data: t.Array(dataSchema), }); }; @@ -110,7 +115,7 @@ export const calculatePagination = (total: number, page: number, pageSize: numbe return { total, page, - pageSize + pageSize, }; }; @@ -124,6 +129,6 @@ export const normalizePaginationParams = (params: Partial page: Math.max(1, params.page || 1), pageSize: Math.min(100, Math.max(1, params.pageSize || 20)), sortBy: params.sortBy || 'createdAt', - sortOrder: params.sortOrder || 'desc' + sortOrder: params.sortOrder || 'desc', }; -}; \ No newline at end of file +}; diff --git a/src/utils/redis.ts b/src/utils/redis.ts index aa42b8e..1571ab9 100644 --- a/src/utils/redis.ts +++ b/src/utils/redis.ts @@ -1,127 +1,127 @@ -/** - * @file Redis数据库连接工具 - * @author hotok - * @date 2025-06-28 - * @lastEditor hotok - * @lastEditTime 2025-06-28 - * @description 提供Redis连接实例、连接池和健康检查功能,供全局复用 - */ - -import { createClient, type RedisClientType } from 'redis'; -import { redisConfig, getRedisUrl } from '@/config/redis.config'; -import { Logger } from '@/plugins/logger/logger.service'; - -/** - * Redis客户端实例 - */ -class Redis { - public redisClient: RedisClientType; - /** - * Redis连接状态 - */ - private isConnected = false; - constructor() { - this.redisClient = createClient({ - name: redisConfig.connectName, - username: redisConfig.username, - password: redisConfig.password, - database: redisConfig.database, - url: getRedisUrl(), - }); - - // 错误处理 - this.redisClient.on('error', (error) => { - Logger.error(error as Error); - this.isConnected = false; - }); - - this.redisClient.on('connect', () => { - Logger.info('Redis客户端连接建立'); - this.isConnected = true; - }); - - this.redisClient.on('ready', () => { - Logger.info('Redis客户端准备就绪'); - }); - - this.redisClient.on('end', () => { - Logger.info('Redis客户端连接结束'); - this.isConnected = false; - }); - - this.redisClient.on('reconnecting', () => { - Logger.warn('Redis客户端正在重连'); - }); - - // 初始化连接 - this.connectRedis().catch((error) => { - Logger.error(error as Error); - }); - } - - /** - * 连接Redis - * @returns Promise - */ - async connectRedis() { - try { - if (!this.isConnected) { - await this.redisClient.connect(); - this.isConnected = true; - } - } catch (error) { - Logger.error(error as Error); - throw error; - } - } - /** - * 断开Redis连接 - * @returns Promise - */ - async disconnectRedis() { - try { - if (this.isConnected) { - await this.redisClient.close(); - this.isConnected = false; - } - } catch (error) { - Logger.error(error as Error); - throw error; - } - } - - /** - * Redis健康检查 - * @returns Promise 返回连接状态 - */ - async checkRedisHealth() { - try { - await this.redisClient.ping(); - return true; - } catch (error) { - Logger.error(error as Error); - return false; - } - } - - /** - * 获取Redis连接状态 - * @returns 连接状态信息 - */ - getRedisStatus() { - return { - isConnected: this.isConnected, - config: { - host: redisConfig.host, - port: redisConfig.port, - database: redisConfig.database, - connectName: redisConfig.connectName, - }, - }; - } -} - -/** - * 导出Redis类,供创建新实例使用 - */ -export { Redis }; +/** + * @file Redis数据库连接工具 + * @author hotok + * @date 2025-06-28 + * @lastEditor hotok + * @lastEditTime 2025-06-28 + * @description 提供Redis连接实例、连接池和健康检查功能,供全局复用 + */ + +import { createClient, type RedisClientType } from 'redis'; +import { redisConfig, getRedisUrl } from '@/config/redis.config'; +import { Logger } from '@/plugins/logger/logger.service'; + +/** + * Redis客户端实例 + */ +class Redis { + public redisClient: RedisClientType; + /** + * Redis连接状态 + */ + private isConnected = false; + constructor() { + this.redisClient = createClient({ + name: redisConfig.connectName, + username: redisConfig.username, + password: redisConfig.password, + database: redisConfig.database, + url: getRedisUrl(), + }); + + // 错误处理 + this.redisClient.on('error', (error) => { + Logger.error(error as Error); + this.isConnected = false; + }); + + this.redisClient.on('connect', () => { + Logger.info('Redis客户端连接建立'); + this.isConnected = true; + }); + + this.redisClient.on('ready', () => { + Logger.info('Redis客户端准备就绪'); + }); + + this.redisClient.on('end', () => { + Logger.info('Redis客户端连接结束'); + this.isConnected = false; + }); + + this.redisClient.on('reconnecting', () => { + Logger.warn('Redis客户端正在重连'); + }); + + // 初始化连接 + this.connectRedis().catch((error) => { + Logger.error(error as Error); + }); + } + + /** + * 连接Redis + * @returns Promise + */ + async connectRedis() { + try { + if (!this.isConnected) { + await this.redisClient.connect(); + this.isConnected = true; + } + } catch (error) { + Logger.error(error as Error); + throw error; + } + } + /** + * 断开Redis连接 + * @returns Promise + */ + async disconnectRedis() { + try { + if (this.isConnected) { + await this.redisClient.close(); + this.isConnected = false; + } + } catch (error) { + Logger.error(error as Error); + throw error; + } + } + + /** + * Redis健康检查 + * @returns Promise 返回连接状态 + */ + async checkRedisHealth() { + try { + await this.redisClient.ping(); + return true; + } catch (error) { + Logger.error(error as Error); + return false; + } + } + + /** + * 获取Redis连接状态 + * @returns 连接状态信息 + */ + getRedisStatus() { + return { + isConnected: this.isConnected, + config: { + host: redisConfig.host, + port: redisConfig.port, + database: redisConfig.database, + connectName: redisConfig.connectName, + }, + }; + } +} + +/** + * 导出Redis类,供创建新实例使用 + */ +export { Redis }; diff --git a/src/utils/responseFormate.ts b/src/utils/responseFormate.ts index 6c42c00..8feaffa 100644 --- a/src/utils/responseFormate.ts +++ b/src/utils/responseFormate.ts @@ -7,7 +7,7 @@ * @description 统一响应格式 */ -import Logger from "@/plugins/logger/logger.service"; +import Logger from '@/plugins/logger/logger.service'; /** * 成功响应 @@ -21,8 +21,8 @@ export const successResponse = (data: any, message: string = 'success') => { message, data, timestamp: new Date().toISOString(), - } -} + }; +}; export const errorResponse = (code: number, message: string, type: string, data: any = null) => { const response = { @@ -31,10 +31,10 @@ export const errorResponse = (code: number, message: string, type: string, data: data, type, timestamp: new Date().toISOString(), - } + }; Logger.warn(response); - return response -} + return response; +}; export class BusinessError extends Error { public readonly code: number; @@ -64,4 +64,4 @@ export const responseWrapperSchema = (dataSchema: any) => examples: ['操作成功', '操作失败', '创建成功'], }), data: dataSchema, - }); \ No newline at end of file + }); diff --git a/src/utils/snowflake.test.ts b/src/utils/snowflake.test.ts index 3b8a3a9..f80ac94 100644 --- a/src/utils/snowflake.test.ts +++ b/src/utils/snowflake.test.ts @@ -7,314 +7,314 @@ * @description 雪花ID生成器的单元测试,验证ID生成、解析、配置等功能 */ -import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import { Snowflake, getSnowflake, nextId, parseId, createSnowflake, type SnowflakeConfig } from './snowflake'; describe('Snowflake ID Generator', () => { - let snowflake: Snowflake; + let snowflake: Snowflake; - beforeEach(() => { - // 创建新的雪花ID实例 - snowflake = new Snowflake({ workerId: 1, datacenterId: 1 }); - }); - - describe('Constructor', () => { - it('应该成功创建雪花ID生成器实例', () => { - const config: SnowflakeConfig = { - workerId: 1, - datacenterId: 1, - }; - const instance = new Snowflake(config); - expect(instance).toBeInstanceOf(Snowflake); + beforeEach(() => { + // 创建新的雪花ID实例 + snowflake = new Snowflake({ workerId: 1, datacenterId: 1 }); }); - it('应该使用默认配置创建实例', () => { - const config: SnowflakeConfig = { - workerId: 1, - datacenterId: 1, - }; - const instance = new Snowflake(config); - const instanceConfig = instance.getConfig(); - - expect(instanceConfig.workerId).toBe(1); - expect(instanceConfig.datacenterId).toBe(1); - expect(instanceConfig.sequence).toBe(0); - expect(instanceConfig.epoch).toBe(1577836800000); // 2020-01-01 00:00:00 UTC + describe('Constructor', () => { + it('应该成功创建雪花ID生成器实例', () => { + const config: SnowflakeConfig = { + workerId: 1, + datacenterId: 1, + }; + const instance = new Snowflake(config); + expect(instance).toBeInstanceOf(Snowflake); + }); + + it('应该使用默认配置创建实例', () => { + const config: SnowflakeConfig = { + workerId: 1, + datacenterId: 1, + }; + const instance = new Snowflake(config); + const instanceConfig = instance.getConfig(); + + expect(instanceConfig.workerId).toBe(1); + expect(instanceConfig.datacenterId).toBe(1); + expect(instanceConfig.sequence).toBe(0); + expect(instanceConfig.epoch).toBe(1577836800000); // 2020-01-01 00:00:00 UTC + }); + + it('应该使用自定义配置创建实例', () => { + const customEpoch = 1609459200000; // 2021-01-01 00:00:00 UTC + const config: SnowflakeConfig = { + workerId: 5, + datacenterId: 3, + sequence: 100, + epoch: customEpoch, + }; + const instance = new Snowflake(config); + const instanceConfig = instance.getConfig(); + + expect(instanceConfig.workerId).toBe(5); + expect(instanceConfig.datacenterId).toBe(3); + expect(instanceConfig.sequence).toBe(100); + expect(instanceConfig.epoch).toBe(customEpoch); + }); + + it('应该验证workerId范围', () => { + expect(() => { + new Snowflake({ workerId: -1, datacenterId: 1 }); + }).toThrow('Worker ID must be between 0 and 31'); + + expect(() => { + new Snowflake({ workerId: 32, datacenterId: 1 }); + }).toThrow('Worker ID must be between 0 and 31'); + }); + + it('应该验证datacenterId范围', () => { + expect(() => { + new Snowflake({ workerId: 1, datacenterId: -1 }); + }).toThrow('Datacenter ID must be between 0 and 31'); + + expect(() => { + new Snowflake({ workerId: 1, datacenterId: 32 }); + }).toThrow('Datacenter ID must be between 0 and 31'); + }); }); - it('应该使用自定义配置创建实例', () => { - const customEpoch = 1609459200000; // 2021-01-01 00:00:00 UTC - const config: SnowflakeConfig = { - workerId: 5, - datacenterId: 3, - sequence: 100, - epoch: customEpoch, - }; - const instance = new Snowflake(config); - const instanceConfig = instance.getConfig(); - - expect(instanceConfig.workerId).toBe(5); - expect(instanceConfig.datacenterId).toBe(3); - expect(instanceConfig.sequence).toBe(100); - expect(instanceConfig.epoch).toBe(customEpoch); + describe('ID Generation', () => { + it('应该生成唯一的ID', () => { + const id1 = snowflake.nextId(); + const id2 = snowflake.nextId(); + + expect(id1).not.toBe(id2); + expect(typeof id1).toBe('bigint'); + expect(typeof id2).toBe('bigint'); + }); + + it('应该生成递增的ID', () => { + const ids: bigint[] = []; + for (let i = 0; i < 10; i++) { + ids.push(snowflake.nextId()); + } + + for (let i = 1; i < ids.length; i++) { + expect(ids[i]).toBeGreaterThan(ids[i - 1]); + } + }); + + it('应该在同一毫秒内递增序列号', () => { + // 模拟同一毫秒 + const originalDateNow = Date.now; + Date.now = () => 1609459200000; + + const id1 = snowflake.nextId(); + const id2 = snowflake.nextId(); + + const parsed1 = Snowflake.parseId(id1); + const parsed2 = Snowflake.parseId(id2); + + expect(parsed1.timestamp).toBe(parsed2.timestamp); + expect(parsed2.sequence).toBe(parsed1.sequence + 1); + + // 恢复原始函数 + Date.now = originalDateNow; + }); + + it('应该在不同毫秒间重置序列号', () => { + let callCount = 0; + const originalDateNow = Date.now; + Date.now = () => { + callCount++; + return 1609459200000 + callCount; // 每次调用递增1毫秒 + }; + + const id1 = snowflake.nextId(); + const id2 = snowflake.nextId(); + + const parsed1 = Snowflake.parseId(id1); + const parsed2 = Snowflake.parseId(id2); + + expect(parsed1.timestamp).toBeLessThan(parsed2.timestamp); + expect(parsed2.sequence).toBe(0); // 序列号应该重置 + + // 恢复原始函数 + Date.now = originalDateNow; + }); }); - it('应该验证workerId范围', () => { - expect(() => { - new Snowflake({ workerId: -1, datacenterId: 1 }); - }).toThrow('Worker ID must be between 0 and 31'); + describe('ID Parsing', () => { + it('应该正确解析生成的ID', () => { + const id = snowflake.nextId(); + const parsed = Snowflake.parseId(id); - expect(() => { - new Snowflake({ workerId: 32, datacenterId: 1 }); - }).toThrow('Worker ID must be between 0 and 31'); + expect(parsed.workerId).toBe(1); + expect(parsed.datacenterId).toBe(1); + expect(parsed.sequence).toBeGreaterThanOrEqual(0); + expect(parsed.timestamp).toBeGreaterThan(0); + expect(parsed.createdAt).toBeInstanceOf(Date); + }); + + it('应该解析自定义配置生成的ID', () => { + const customSnowflake = new Snowflake({ + workerId: 10, + datacenterId: 20, + epoch: 1609459200000, + }); + + const id = customSnowflake.nextId(); + const parsed = Snowflake.parseId(id); + + expect(parsed.workerId).toBe(10); + expect(parsed.datacenterId).toBe(20); + }); + + it('应该正确计算创建时间', () => { + const id = snowflake.nextId(); + const parsed = Snowflake.parseId(id); + const now = new Date(); + + // 创建时间应该在合理范围内(前后1秒) + const timeDiff = Math.abs(parsed.createdAt.getTime() - now.getTime()); + expect(timeDiff).toBeLessThan(1000); + }); }); - it('应该验证datacenterId范围', () => { - expect(() => { - new Snowflake({ workerId: 1, datacenterId: -1 }); - }).toThrow('Datacenter ID must be between 0 and 31'); + describe('Singleton Pattern', () => { + it('应该返回相同的单例实例', () => { + const instance1 = getSnowflake(); + const instance2 = getSnowflake(); - expect(() => { - new Snowflake({ workerId: 1, datacenterId: 32 }); - }).toThrow('Datacenter ID must be between 0 and 31'); - }); - }); + expect(instance1).toBe(instance2); + }); - describe('ID Generation', () => { - it('应该生成唯一的ID', () => { - const id1 = snowflake.nextId(); - const id2 = snowflake.nextId(); - - expect(id1).not.toBe(id2); - expect(typeof id1).toBe('bigint'); - expect(typeof id2).toBe('bigint'); + it('应该使用默认配置创建单例', () => { + const instance = getSnowflake(); + const config = instance.getConfig(); + + expect(config.workerId).toBe(1); + expect(config.datacenterId).toBe(1); + }); + + it('应该使用自定义配置创建单例', () => { + const customInstance = getSnowflake({ workerId: 5, datacenterId: 5 }); + const config = customInstance.getConfig(); + + expect(config.workerId).toBe(5); + expect(config.datacenterId).toBe(5); + }); }); - it('应该生成递增的ID', () => { - const ids: bigint[] = []; - for (let i = 0; i < 10; i++) { - ids.push(snowflake.nextId()); - } - - for (let i = 1; i < ids.length; i++) { - expect(ids[i]).toBeGreaterThan(ids[i - 1]); - } + describe('Utility Functions', () => { + it('nextId函数应该生成ID', () => { + const id = nextId(); + expect(typeof id).toBe('bigint'); + expect(id).toBeGreaterThan(0n); + }); + + it('parseId函数应该解析ID', () => { + const id = nextId(); + const parsed = parseId(id); + + expect(parsed).toHaveProperty('timestamp'); + expect(parsed).toHaveProperty('datacenterId'); + expect(parsed).toHaveProperty('workerId'); + expect(parsed).toHaveProperty('sequence'); + expect(parsed).toHaveProperty('createdAt'); + }); + + it('createSnowflake函数应该创建新实例', () => { + const instance1 = createSnowflake({ workerId: 1, datacenterId: 1 }); + const instance2 = createSnowflake({ workerId: 2, datacenterId: 2 }); + + expect(instance1).not.toBe(instance2); + + const config1 = instance1.getConfig(); + const config2 = instance2.getConfig(); + + expect(config1.workerId).toBe(1); + expect(config2.workerId).toBe(2); + }); }); - it('应该在同一毫秒内递增序列号', () => { - // 模拟同一毫秒 - const originalDateNow = Date.now; - Date.now = () => 1609459200000; - - const id1 = snowflake.nextId(); - const id2 = snowflake.nextId(); - - const parsed1 = Snowflake.parseId(id1); - const parsed2 = Snowflake.parseId(id2); - - expect(parsed1.timestamp).toBe(parsed2.timestamp); - expect(parsed2.sequence).toBe(parsed1.sequence + 1); - - // 恢复原始函数 - Date.now = originalDateNow; + describe('Performance Tests', () => { + it('应该能够快速生成大量ID', () => { + const startTime = Date.now(); + const ids: bigint[] = []; + + // 生成1000个ID + for (let i = 0; i < 1000; i++) { + ids.push(snowflake.nextId()); + } + + const endTime = Date.now(); + const duration = endTime - startTime; + + expect(ids.length).toBe(1000); + expect(duration).toBeLessThan(100); // 应该在100ms内完成 + + // 验证所有ID都是唯一的 + const uniqueIds = new Set(ids); + expect(uniqueIds.size).toBe(1000); + }); + + it('应该能够处理序列号溢出', () => { + // 模拟快速生成ID,触发序列号溢出 + const originalDateNow = Date.now; + Date.now = () => 1609459200000; + + const ids: bigint[] = []; + for (let i = 0; i < 5000; i++) { + ids.push(snowflake.nextId()); + } + + // 验证所有ID都是唯一的 + const uniqueIds = new Set(ids); + expect(uniqueIds.size).toBe(5000); + + // 恢复原始函数 + Date.now = originalDateNow; + }); }); - it('应该在不同毫秒间重置序列号', () => { - let callCount = 0; - const originalDateNow = Date.now; - Date.now = () => { - callCount++; - return 1609459200000 + callCount; // 每次调用递增1毫秒 - }; - - const id1 = snowflake.nextId(); - const id2 = snowflake.nextId(); - - const parsed1 = Snowflake.parseId(id1); - const parsed2 = Snowflake.parseId(id2); - - expect(parsed1.timestamp).toBeLessThan(parsed2.timestamp); - expect(parsed2.sequence).toBe(0); // 序列号应该重置 - - // 恢复原始函数 - Date.now = originalDateNow; - }); - }); + describe('Edge Cases', () => { + it('应该处理最大配置值', () => { + const maxSnowflake = new Snowflake({ + workerId: 31, + datacenterId: 31, + }); - describe('ID Parsing', () => { - it('应该正确解析生成的ID', () => { - const id = snowflake.nextId(); - const parsed = Snowflake.parseId(id); - - expect(parsed.workerId).toBe(1); - expect(parsed.datacenterId).toBe(1); - expect(parsed.sequence).toBeGreaterThanOrEqual(0); - expect(parsed.timestamp).toBeGreaterThan(0); - expect(parsed.createdAt).toBeInstanceOf(Date); - }); + const id = maxSnowflake.nextId(); + const parsed = Snowflake.parseId(id); - it('应该解析自定义配置生成的ID', () => { - const customSnowflake = new Snowflake({ - workerId: 10, - datacenterId: 20, - epoch: 1609459200000, - }); - - const id = customSnowflake.nextId(); - const parsed = Snowflake.parseId(id); - - expect(parsed.workerId).toBe(10); - expect(parsed.datacenterId).toBe(20); - }); + expect(parsed.workerId).toBe(31); + expect(parsed.datacenterId).toBe(31); + }); - it('应该正确计算创建时间', () => { - const id = snowflake.nextId(); - const parsed = Snowflake.parseId(id); - const now = new Date(); - - // 创建时间应该在合理范围内(前后1秒) - const timeDiff = Math.abs(parsed.createdAt.getTime() - now.getTime()); - expect(timeDiff).toBeLessThan(1000); - }); - }); + it('应该处理最小配置值', () => { + const minSnowflake = new Snowflake({ + workerId: 0, + datacenterId: 0, + }); - describe('Singleton Pattern', () => { - it('应该返回相同的单例实例', () => { - const instance1 = getSnowflake(); - const instance2 = getSnowflake(); - - expect(instance1).toBe(instance2); - }); + const id = minSnowflake.nextId(); + const parsed = Snowflake.parseId(id); - it('应该使用默认配置创建单例', () => { - const instance = getSnowflake(); - const config = instance.getConfig(); - - expect(config.workerId).toBe(1); - expect(config.datacenterId).toBe(1); - }); + expect(parsed.workerId).toBe(0); + expect(parsed.datacenterId).toBe(0); + }); - it('应该使用自定义配置创建单例', () => { - const customInstance = getSnowflake({ workerId: 5, datacenterId: 5 }); - const config = customInstance.getConfig(); - - expect(config.workerId).toBe(5); - expect(config.datacenterId).toBe(5); - }); - }); + it('应该处理自定义epoch', () => { + const customEpoch = Date.now(); + const customSnowflake = new Snowflake({ + workerId: 1, + datacenterId: 1, + epoch: customEpoch, + }); - describe('Utility Functions', () => { - it('nextId函数应该生成ID', () => { - const id = nextId(); - expect(typeof id).toBe('bigint'); - expect(id).toBeGreaterThan(0n); - }); + const id = customSnowflake.nextId(); + const parsed = Snowflake.parseId(id); - it('parseId函数应该解析ID', () => { - const id = nextId(); - const parsed = parseId(id); - - expect(parsed).toHaveProperty('timestamp'); - expect(parsed).toHaveProperty('datacenterId'); - expect(parsed).toHaveProperty('workerId'); - expect(parsed).toHaveProperty('sequence'); - expect(parsed).toHaveProperty('createdAt'); + // 由于parseId使用默认epoch,时间戳会有差异 + expect(parsed.createdAt.getTime()).toBeLessThan(Date.now()); + }); }); - - it('createSnowflake函数应该创建新实例', () => { - const instance1 = createSnowflake({ workerId: 1, datacenterId: 1 }); - const instance2 = createSnowflake({ workerId: 2, datacenterId: 2 }); - - expect(instance1).not.toBe(instance2); - - const config1 = instance1.getConfig(); - const config2 = instance2.getConfig(); - - expect(config1.workerId).toBe(1); - expect(config2.workerId).toBe(2); - }); - }); - - describe('Performance Tests', () => { - it('应该能够快速生成大量ID', () => { - const startTime = Date.now(); - const ids: bigint[] = []; - - // 生成1000个ID - for (let i = 0; i < 1000; i++) { - ids.push(snowflake.nextId()); - } - - const endTime = Date.now(); - const duration = endTime - startTime; - - expect(ids.length).toBe(1000); - expect(duration).toBeLessThan(100); // 应该在100ms内完成 - - // 验证所有ID都是唯一的 - const uniqueIds = new Set(ids); - expect(uniqueIds.size).toBe(1000); - }); - - it('应该能够处理序列号溢出', () => { - // 模拟快速生成ID,触发序列号溢出 - const originalDateNow = Date.now; - Date.now = () => 1609459200000; - - const ids: bigint[] = []; - for (let i = 0; i < 5000; i++) { - ids.push(snowflake.nextId()); - } - - // 验证所有ID都是唯一的 - const uniqueIds = new Set(ids); - expect(uniqueIds.size).toBe(5000); - - // 恢复原始函数 - Date.now = originalDateNow; - }); - }); - - describe('Edge Cases', () => { - it('应该处理最大配置值', () => { - const maxSnowflake = new Snowflake({ - workerId: 31, - datacenterId: 31, - }); - - const id = maxSnowflake.nextId(); - const parsed = Snowflake.parseId(id); - - expect(parsed.workerId).toBe(31); - expect(parsed.datacenterId).toBe(31); - }); - - it('应该处理最小配置值', () => { - const minSnowflake = new Snowflake({ - workerId: 0, - datacenterId: 0, - }); - - const id = minSnowflake.nextId(); - const parsed = Snowflake.parseId(id); - - expect(parsed.workerId).toBe(0); - expect(parsed.datacenterId).toBe(0); - }); - - it('应该处理自定义epoch', () => { - const customEpoch = Date.now(); - const customSnowflake = new Snowflake({ - workerId: 1, - datacenterId: 1, - epoch: customEpoch, - }); - - const id = customSnowflake.nextId(); - const parsed = Snowflake.parseId(id); - - // 由于parseId使用默认epoch,时间戳会有差异 - expect(parsed.createdAt.getTime()).toBeLessThan(Date.now()); - }); - }); -}); \ No newline at end of file +}); diff --git a/src/utils/snowflake.ts b/src/utils/snowflake.ts index c67503a..03d2303 100644 --- a/src/utils/snowflake.ts +++ b/src/utils/snowflake.ts @@ -15,14 +15,14 @@ * @property {number} epoch - 起始时间戳 (毫秒) */ export interface SnowflakeConfig { - /** 工作机器ID,范围0-31 */ - workerId: number; - /** 数据中心ID,范围0-31 */ - datacenterId: number; - /** 序列号起始值,默认0 */ - sequence?: number; - /** 起始时间戳,默认2020-01-01 00:00:00 UTC */ - epoch?: number; + /** 工作机器ID,范围0-31 */ + workerId: number; + /** 数据中心ID,范围0-31 */ + datacenterId: number; + /** 序列号起始值,默认0 */ + sequence?: number; + /** 起始时间戳,默认2020-01-01 00:00:00 UTC */ + epoch?: number; } /** @@ -30,169 +30,173 @@ export interface SnowflakeConfig { * 生成64位长整型ID,格式:时间戳(41位) + 数据中心ID(5位) + 工作机器ID(5位) + 序列号(12位) */ export class Snowflake { - /** 工作机器ID位数 */ - private static readonly WORKER_ID_BITS = 5; - /** 数据中心ID位数 */ - private static readonly DATACENTER_ID_BITS = 5; - /** 序列号位数 */ - private static readonly SEQUENCE_BITS = 12; + /** 工作机器ID位数 */ + private static readonly WORKER_ID_BITS = 5; + /** 数据中心ID位数 */ + private static readonly DATACENTER_ID_BITS = 5; + /** 序列号位数 */ + private static readonly SEQUENCE_BITS = 12; - /** 最大工作机器ID */ - private static readonly MAX_WORKER_ID = (1 << Snowflake.WORKER_ID_BITS) - 1; - /** 最大数据中心ID */ - private static readonly MAX_DATACENTER_ID = (1 << Snowflake.DATACENTER_ID_BITS) - 1; - /** 最大序列号 */ - private static readonly MAX_SEQUENCE = (1 << Snowflake.SEQUENCE_BITS) - 1; + /** 最大工作机器ID */ + private static readonly MAX_WORKER_ID = (1 << Snowflake.WORKER_ID_BITS) - 1; + /** 最大数据中心ID */ + private static readonly MAX_DATACENTER_ID = (1 << Snowflake.DATACENTER_ID_BITS) - 1; + /** 最大序列号 */ + private static readonly MAX_SEQUENCE = (1 << Snowflake.SEQUENCE_BITS) - 1; - /** 工作机器ID左移位数 */ - private static readonly WORKER_ID_SHIFT = Snowflake.SEQUENCE_BITS; - /** 数据中心ID左移位数 */ - private static readonly DATACENTER_ID_SHIFT = Snowflake.SEQUENCE_BITS + Snowflake.WORKER_ID_BITS; - /** 时间戳左移位数 */ - private static readonly TIMESTAMP_LEFT_SHIFT = Snowflake.SEQUENCE_BITS + Snowflake.WORKER_ID_BITS + Snowflake.DATACENTER_ID_BITS; + /** 工作机器ID左移位数 */ + private static readonly WORKER_ID_SHIFT = Snowflake.SEQUENCE_BITS; + /** 数据中心ID左移位数 */ + private static readonly DATACENTER_ID_SHIFT = Snowflake.SEQUENCE_BITS + Snowflake.WORKER_ID_BITS; + /** 时间戳左移位数 */ + private static readonly TIMESTAMP_LEFT_SHIFT = + Snowflake.SEQUENCE_BITS + Snowflake.WORKER_ID_BITS + Snowflake.DATACENTER_ID_BITS; - /** 工作机器ID */ - private readonly workerId: number; - /** 数据中心ID */ - private readonly datacenterId: number; - /** 起始时间戳 */ - private readonly epoch: number; - /** 当前序列号 */ - private sequence: number; - /** 上次生成ID的时间戳 */ - private lastTimestamp: number; + /** 工作机器ID */ + private readonly workerId: number; + /** 数据中心ID */ + private readonly datacenterId: number; + /** 起始时间戳 */ + private readonly epoch: number; + /** 当前序列号 */ + private sequence: number; + /** 上次生成ID的时间戳 */ + private lastTimestamp: number; - /** - * 构造函数 - * @param config 雪花ID配置 - * @throws {Error} 当workerId或datacenterId超出范围时抛出错误 - */ - constructor(config: SnowflakeConfig) { - // 验证workerId范围 - if (config.workerId < 0 || config.workerId > Snowflake.MAX_WORKER_ID) { - throw new Error(`Worker ID must be between 0 and ${Snowflake.MAX_WORKER_ID}`); + /** + * 构造函数 + * @param config 雪花ID配置 + * @throws {Error} 当workerId或datacenterId超出范围时抛出错误 + */ + constructor(config: SnowflakeConfig) { + // 验证workerId范围 + if (config.workerId < 0 || config.workerId > Snowflake.MAX_WORKER_ID) { + throw new Error(`Worker ID must be between 0 and ${Snowflake.MAX_WORKER_ID}`); + } + + // 验证datacenterId范围 + if (config.datacenterId < 0 || config.datacenterId > Snowflake.MAX_DATACENTER_ID) { + throw new Error(`Datacenter ID must be between 0 and ${Snowflake.MAX_DATACENTER_ID}`); + } + + this.workerId = config.workerId; + this.datacenterId = config.datacenterId; + this.sequence = config.sequence || 0; + this.epoch = config.epoch || 1577836800000; // 2020-01-01 00:00:00 UTC + this.lastTimestamp = -1; } - // 验证datacenterId范围 - if (config.datacenterId < 0 || config.datacenterId > Snowflake.MAX_DATACENTER_ID) { - throw new Error(`Datacenter ID must be between 0 and ${Snowflake.MAX_DATACENTER_ID}`); + /** + * 生成下一个雪花ID + * @returns {bigint} 64位雪花ID + * @throws {Error} 当系统时钟回拨时抛出错误 + */ + public nextId(): bigint { + let timestamp = this.getCurrentTimestamp(); + + // 检查时钟回拨 + if (timestamp < this.lastTimestamp) { + const timeDiff = this.lastTimestamp - timestamp; + throw new Error(`Clock moved backwards. Refusing to generate id for ${timeDiff} milliseconds`); + } + + // 如果是同一毫秒内,递增序列号 + if (timestamp === this.lastTimestamp) { + this.sequence = (this.sequence + 1) & Snowflake.MAX_SEQUENCE; + + // 如果序列号溢出,等待下一毫秒 + if (this.sequence === 0) { + timestamp = this.waitNextMillis(this.lastTimestamp); + } + } else { + // 不同毫秒,重置序列号 + this.sequence = 0; + } + + this.lastTimestamp = timestamp; + + // 生成雪花ID + const id = + (BigInt(timestamp - this.epoch) << BigInt(Snowflake.TIMESTAMP_LEFT_SHIFT)) | + (BigInt(this.datacenterId) << BigInt(Snowflake.DATACENTER_ID_SHIFT)) | + (BigInt(this.workerId) << BigInt(Snowflake.WORKER_ID_SHIFT)) | + BigInt(this.sequence); + + return id; } - this.workerId = config.workerId; - this.datacenterId = config.datacenterId; - this.sequence = config.sequence || 0; - this.epoch = config.epoch || 1577836800000; // 2020-01-01 00:00:00 UTC - this.lastTimestamp = -1; - } + /** + * 解析雪花ID,返回各个组成部分 + * @param id 雪花ID + * @returns {object} 解析结果 + */ + public static parseId(id: bigint): { + timestamp: number; + datacenterId: number; + workerId: number; + sequence: number; + createdAt: Date; + } { + const timestamp = Number((id >> BigInt(Snowflake.TIMESTAMP_LEFT_SHIFT)) & BigInt((1 << 41) - 1)); + const datacenterId = Number( + (id >> BigInt(Snowflake.DATACENTER_ID_SHIFT)) & BigInt(Snowflake.MAX_DATACENTER_ID), + ); + const workerId = Number((id >> BigInt(Snowflake.WORKER_ID_SHIFT)) & BigInt(Snowflake.MAX_WORKER_ID)); + const sequence = Number(id & BigInt(Snowflake.MAX_SEQUENCE)); - /** - * 生成下一个雪花ID - * @returns {bigint} 64位雪花ID - * @throws {Error} 当系统时钟回拨时抛出错误 - */ - public nextId(): bigint { - let timestamp = this.getCurrentTimestamp(); + // 计算创建时间(使用默认epoch) + const epoch = 1577836800000; // 2020-01-01 00:00:00 UTC + const createdAt = new Date(epoch + timestamp); - // 检查时钟回拨 - if (timestamp < this.lastTimestamp) { - const timeDiff = this.lastTimestamp - timestamp; - throw new Error(`Clock moved backwards. Refusing to generate id for ${timeDiff} milliseconds`); + return { + timestamp, + datacenterId, + workerId, + sequence, + createdAt, + }; } - // 如果是同一毫秒内,递增序列号 - if (timestamp === this.lastTimestamp) { - this.sequence = (this.sequence + 1) & Snowflake.MAX_SEQUENCE; - - // 如果序列号溢出,等待下一毫秒 - if (this.sequence === 0) { - timestamp = this.waitNextMillis(this.lastTimestamp); - } - } else { - // 不同毫秒,重置序列号 - this.sequence = 0; + /** + * 获取当前时间戳(毫秒) + * @returns {number} 当前时间戳 + */ + private getCurrentTimestamp(): number { + return Date.now(); } - this.lastTimestamp = timestamp; - - // 生成雪花ID - const id = ((BigInt(timestamp - this.epoch) << BigInt(Snowflake.TIMESTAMP_LEFT_SHIFT)) | - (BigInt(this.datacenterId) << BigInt(Snowflake.DATACENTER_ID_SHIFT)) | - (BigInt(this.workerId) << BigInt(Snowflake.WORKER_ID_SHIFT)) | - BigInt(this.sequence)); - - return id; - } - - /** - * 解析雪花ID,返回各个组成部分 - * @param id 雪花ID - * @returns {object} 解析结果 - */ - public static parseId(id: bigint): { - timestamp: number; - datacenterId: number; - workerId: number; - sequence: number; - createdAt: Date; - } { - const timestamp = Number((id >> BigInt(Snowflake.TIMESTAMP_LEFT_SHIFT)) & BigInt((1 << 41) - 1)); - const datacenterId = Number((id >> BigInt(Snowflake.DATACENTER_ID_SHIFT)) & BigInt(Snowflake.MAX_DATACENTER_ID)); - const workerId = Number((id >> BigInt(Snowflake.WORKER_ID_SHIFT)) & BigInt(Snowflake.MAX_WORKER_ID)); - const sequence = Number(id & BigInt(Snowflake.MAX_SEQUENCE)); - - // 计算创建时间(使用默认epoch) - const epoch = 1577836800000; // 2020-01-01 00:00:00 UTC - const createdAt = new Date(epoch + timestamp); - - return { - timestamp, - datacenterId, - workerId, - sequence, - createdAt, - }; - } - - /** - * 获取当前时间戳(毫秒) - * @returns {number} 当前时间戳 - */ - private getCurrentTimestamp(): number { - return Date.now(); - } - - /** - * 等待下一毫秒 - * @param lastTimestamp 上次时间戳 - * @returns {number} 新的时间戳 - */ - private waitNextMillis(lastTimestamp: number): number { - let timestamp = this.getCurrentTimestamp(); - while (timestamp <= lastTimestamp) { - timestamp = this.getCurrentTimestamp(); + /** + * 等待下一毫秒 + * @param lastTimestamp 上次时间戳 + * @returns {number} 新的时间戳 + */ + private waitNextMillis(lastTimestamp: number): number { + let timestamp = this.getCurrentTimestamp(); + while (timestamp <= lastTimestamp) { + timestamp = this.getCurrentTimestamp(); + } + return timestamp; } - return timestamp; - } - /** - * 获取配置信息 - * @returns {object} 当前配置 - */ - public getConfig(): { - workerId: number; - datacenterId: number; - sequence: number; - epoch: number; - lastTimestamp: number; - } { - return { - workerId: this.workerId, - datacenterId: this.datacenterId, - sequence: this.sequence, - epoch: this.epoch, - lastTimestamp: this.lastTimestamp, - }; - } + /** + * 获取配置信息 + * @returns {object} 当前配置 + */ + public getConfig(): { + workerId: number; + datacenterId: number; + sequence: number; + epoch: number; + lastTimestamp: number; + } { + return { + workerId: this.workerId, + datacenterId: this.datacenterId, + sequence: this.sequence, + epoch: this.epoch, + lastTimestamp: this.lastTimestamp, + }; + } } /** @@ -207,15 +211,15 @@ let snowflakeInstance: Snowflake | null = null; * @returns {Snowflake} 雪花ID生成器实例 */ export function getSnowflake(config?: Partial): Snowflake { - if (!snowflakeInstance) { - const defaultConfig: SnowflakeConfig = { - workerId: 1, - datacenterId: 1, - ...config, - }; - snowflakeInstance = new Snowflake(defaultConfig); - } - return snowflakeInstance; + if (!snowflakeInstance) { + const defaultConfig: SnowflakeConfig = { + workerId: 1, + datacenterId: 1, + ...config, + }; + snowflakeInstance = new Snowflake(defaultConfig); + } + return snowflakeInstance; } /** @@ -223,7 +227,7 @@ export function getSnowflake(config?: Partial): Snowflake { * @returns {bigint} 64位雪花ID */ export function nextId(): bigint { - return getSnowflake().nextId(); + return getSnowflake().nextId(); } /** @@ -232,7 +236,7 @@ export function nextId(): bigint { * @returns {object} 解析结果 */ export function parseId(id: bigint) { - return Snowflake.parseId(id); + return Snowflake.parseId(id); } /** @@ -241,5 +245,5 @@ export function parseId(id: bigint) { * @returns {Snowflake} 新的雪花ID生成器实例 */ export function createSnowflake(config: SnowflakeConfig): Snowflake { - return new Snowflake(config); -} \ No newline at end of file + return new Snowflake(config); +} diff --git a/tasks/M2-基础用户系统-开发任务计划.md b/tasks/M2-基础用户系统-开发任务计划.md deleted file mode 100644 index 41a33d2..0000000 --- a/tasks/M2-基础用户系统-开发任务计划.md +++ /dev/null @@ -1,380 +0,0 @@ -# M2 - 基础用户系统 - 开发任务计划 - -## 相关文件 (Relevant Files) - -### 认证模块 (Auth) -- `src/modules/auth/auth.schema.ts` - 认证模块Schema定义 -- `src/modules/auth/auth.response.ts` - 认证模块响应格式定义 -- `src/modules/auth/auth.service.ts` - 认证模块Service层实现 -- `src/modules/auth/auth.controller.ts` - 认证模块Controller层实现 -- `src/modules/auth/auth.test.md` - 认证模块测试用例文档 - -### 用户管理模块 (User) -- `src/modules/user/user.schema.ts` - 用户模块Schema定义(已存在) -- `src/modules/user/user.response.ts` - 用户模块响应格式定义(已存在) -- `src/modules/user/user.service.ts` - 用户模块Service层实现(已存在) -- `src/modules/user/user.controller.ts` - 用户模块Controller层实现(需更新) -- `src/modules/user/user.test.md` - 用户模块测试用例文档 - -### 角色权限模块 (Role) -- `src/modules/role/role.schema.ts` - 角色模块Schema定义 -- `src/modules/role/role.response.ts` - 角色模块响应格式定义 -- `src/modules/role/role.service.ts` - 角色模块Service层实现 -- `src/modules/role/role.controller.ts` - 角色模块Controller层实现 -- `src/modules/role/role.test.md` - 角色模块测试用例文档 - -### 权限管理模块 (Permission) -- `src/modules/permission/permission.schema.ts` - 权限模块Schema定义 -- `src/modules/permission/permission.response.ts` - 权限模块响应格式定义 -- `src/modules/permission/permission.service.ts` - 权限模块Service层实现 -- `src/modules/permission/permission.controller.ts` - 权限模块Controller层实现 -- `src/modules/permission/permission.test.md` - 权限模块测试用例文档 - -### 组织架构模块 (Organization) -- `src/modules/organization/organization.schema.ts` - 组织模块Schema定义 -- `src/modules/organization/organization.response.ts` - 组织模块响应格式定义 -- `src/modules/organization/organization.service.ts` - 组织模块Service层实现 -- `src/modules/organization/organization.controller.ts` - 组织模块Controller层实现 -- `src/modules/organization/organization.test.md` - 组织模块测试用例文档 - -### 系统基础模块 (System) -- `src/modules/system/dict/dict.schema.ts` - 字典模块Schema定义 -- `src/modules/system/dict/dict.response.ts` - 字典模块响应格式定义 -- `src/modules/system/dict/dict.service.ts` - 字典模块Service层实现 -- `src/modules/system/dict/dict.controller.ts` - 字典模块Controller层实现 -- `src/modules/system/dict/dict.test.md` - 字典模块测试用例文档 -- `src/modules/system/tag/tag.schema.ts` - 标签模块Schema定义 -- `src/modules/system/tag/tag.response.ts` - 标签模块响应格式定义 -- `src/modules/system/tag/tag.service.ts` - 标签模块Service层实现 -- `src/modules/system/tag/tag.controller.ts` - 标签模块Controller层实现 -- `src/modules/system/tag/tag.test.md` - 标签模块测试用例文档 -- `src/modules/system/log/log.schema.ts` - 日志模块Schema定义 -- `src/modules/system/log/log.response.ts` - 日志模块响应格式定义 -- `src/modules/system/log/log.service.ts` - 日志模块Service层实现 -- `src/modules/system/log/log.controller.ts` - 日志模块Controller层实现 -- `src/modules/system/log/log.test.md` - 日志模块测试用例文档 - -### 备注 (Notes) -- 验证码功能已有captcha模块可直接集成 -- 遵循Elysia开发规范,每个接口都要有完整的5个文件 -- 按照PRD优先级:P0 > P1 > P2 顺序开发 - -## 任务 (Tasks) - -### 🔐 认证模块 (Auth Module) - P0优先级 - -- [x] 1.0 POST /auth/register - 用户注册接口 - - [x] 1.1 创建auth.schema.ts - 定义用户注册Schema - - [x] 1.2 创建auth.response.ts - 定义注册响应格式 - - [x] 1.3 创建auth.service.ts - 实现注册业务逻辑 - - [x] 1.4 创建auth.controller.ts - 实现注册路由 - - [x] 1.5 创建auth.test.md - 编写注册测试用例文档 - -- [x] 2.0 POST /auth/activate - 邮箱激活接口 - - [x] 2.1 扩展auth.schema.ts - 定义激活Schema - - [x] 2.2 扩展auth.response.ts - 定义激活响应格式 - - [x] 2.3 扩展auth.service.ts - 实现激活业务逻辑 - - [x] 2.4 扩展auth.controller.ts - 实现激活路由 - - [x] 2.5 扩展auth.test.md - 编写激活测试用例文档 - -- [x] 3.0 POST /auth/login - 用户登录接口 - - [x] 3.1 扩展auth.schema.ts - 定义登录Schema - - [x] 3.2 扩展auth.response.ts - 定义登录响应格式 - - [x] 3.3 扩展auth.service.ts - 实现登录业务逻辑 - - [x] 3.4 扩展auth.controller.ts - 实现登录路由 - - [x] 3.5 扩展auth.test.md - 编写登录测试用例文档 - -- [x] 4.0 POST /auth/refresh - Token刷新接口 - - [x] 4.1 扩展auth.schema.ts - 定义刷新Schema - - [x] 4.2 扩展auth.response.ts - 定义刷新响应格式 - - [x] 4.3 扩展auth.service.ts - 实现刷新业务逻辑 - - [x] 4.4 扩展auth.controller.ts - 实现刷新路由 - - [x] 4.5 扩展auth.test.md - 编写刷新测试用例文档 - -- ~~[ ] 5.0 POST /auth/logout - 退出登录接口~~ - - ~~[ ] 5.1 扩展auth.schema.ts - 定义退出Schema~~ - - ~~[ ] 5.2 扩展auth.response.ts - 定义退出响应格式~~ - - ~~[ ] 5.3 扩展auth.service.ts - 实现退出业务逻辑~~ - - ~~[ ] 5.4 扩展auth.controller.ts - 实现退出路由~~ - - ~~[ ] 5.5 扩展auth.test.md - 编写退出测试用例文档~~ - -- [x] 6.0 POST /auth/password/reset-request - 找回密码接口 - - [x] 6.1 扩展auth.schema.ts - 定义找回密码Schema - - [x] 6.2 扩展auth.response.ts - 定义找回密码响应格式 - - [x] 6.3 扩展auth.service.ts - 实现找回密码业务逻辑 - - [x] 6.4 扩展auth.controller.ts - 实现找回密码路由 - - [x] 6.5 扩展auth.test.md - 编写找回密码测试用例文档 - -- [x] 7.0 POST /auth/password/reset-confirm - 重置密码接口 - - [x] 7.1 扩展auth.schema.ts - 定义重置密码Schema - - [x] 7.2 扩展auth.response.ts - 定义重置密码响应格式 - - [x] 7.3 扩展auth.service.ts - 实现重置密码业务逻辑 - - [x] 7.4 扩展auth.controller.ts - 实现重置密码路由 - - [x] 7.5 扩展auth.test.md - 编写重置密码测试用例文档 - -- [x] 8.0 GET /auth/captcha - 图形验证码接口 - - [x] 8.1 扩展auth.schema.ts - 定义验证码Schema - - [x] 8.2 扩展auth.response.ts - 定义验证码响应格式 - - [x] 8.3 扩展auth.service.ts - 集成验证码服务 - - [x] 8.4 扩展auth.controller.ts - 实现验证码路由 - - [x] 8.5 扩展auth.test.md - 编写验证码测试用例文档 - -- [x] 9.0 检查认证模块 (Auth Module)那些接口需要加分布式锁,并加锁 - - -### 👤 用户管理模块 (User Module) - P0优先级 - -- [x] 9.0 GET /users/me - 获取当前用户信息接口 - - [x] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [x] 9.1 扩展user.schema.ts - 定义当前用户Schema - - [x] 9.2 扩展user.response.ts - 定义当前用户响应格式 - - [x] 9.3 扩展user.service.ts - 实现当前用户业务逻辑 - - [x] 9.4 更新user.controller.ts - 实现当前用户路由 - - [x] 9.5 创建user.test.md - 编写当前用户测试用例文档 - -- [x] 10.0 GET /users - 用户列表查询接口 - - [x] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [x] 10.1 扩展user.schema.ts - 定义用户列表Schema - - [x] 10.2 扩展user.response.ts - 定义用户列表响应格式 - - [x] 10.3 扩展user.service.ts - 实现用户列表业务逻辑 - - [x] 10.4 扩展user.controller.ts - 实现用户列表路由 - - [x] 10.5 扩展user.test.md - 编写用户列表测试用例文档 - -- [ ] 11.0 POST /users - 创建用户接口 - - [ ] 0.0 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 11.1 扩展user.schema.ts - 定义创建用户Schema - - [ ] 11.2 扩展user.response.ts - 定义创建用户响应格式 - - [ ] 11.3 扩展user.service.ts - 实现创建用户业务逻辑 - - [ ] 11.4 扩展user.controller.ts - 实现创建用户路由 - - [ ] 11.5 扩展user.test.md - 编写创建用户测试用例文档 - -- [ ] 12.0 PUT /users/{id} - 更新用户信息接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 12.1 扩展user.schema.ts - 定义更新用户Schema - - [ ] 12.2 扩展user.response.ts - 定义更新用户响应格式 - - [ ] 12.3 扩展user.service.ts - 实现更新用户业务逻辑 - - [ ] 12.4 扩展user.controller.ts - 实现更新用户路由 - - [ ] 12.5 扩展user.test.md - 编写更新用户测试用例文档 - -- [ ] 13.0 DELETE /users/{id} - 删除用户接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 13.1 扩展user.schema.ts - 定义删除用户Schema - - [ ] 13.2 扩展user.response.ts - 定义删除用户响应格式 - - [ ] 13.3 扩展user.service.ts - 实现删除用户业务逻辑 - - [ ] 13.4 扩展user.controller.ts - 实现删除用户路由 - - [ ] 13.5 扩展user.test.md - 编写删除用户测试用例文档 - -- [ ] 14.0 PUT /users/me/password - 修改密码接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 14.1 扩展user.schema.ts - 定义修改密码Schema - - [ ] 14.2 扩展user.response.ts - 定义修改密码响应格式 - - [ ] 14.3 扩展user.service.ts - 实现修改密码业务逻辑 - - [ ] 14.4 扩展user.controller.ts - 实现修改密码路由 - - [ ] 14.5 扩展user.test.md - 编写修改密码测试用例文档 - -- [ ] 15.0 GET /users/{id} - 用户详情接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 15.1 扩展user.schema.ts - 定义用户详情Schema - - [ ] 15.2 扩展user.response.ts - 定义用户详情响应格式 - - [ ] 15.3 扩展user.service.ts - 实现用户详情业务逻辑 - - [ ] 15.4 扩展user.controller.ts - 实现用户详情路由 - - [ ] 15.5 扩展user.test.md - 编写用户详情测试用例文档 - -- [ ] 16.0 POST /users/batch - 批量操作接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 16.1 扩展user.schema.ts - 定义批量操作Schema - - [ ] 16.2 扩展user.response.ts - 定义批量操作响应格式 - - [ ] 16.3 扩展user.service.ts - 实现批量操作业务逻辑 - - [ ] 16.4 扩展user.controller.ts - 实现批量操作路由 - - [ ] 16.5 扩展user.test.md - 编写批量操作测试用例文档 - -### 🎭 角色权限模块 (Role Module) - P0优先级 - -- [ ] 17.0 GET /roles - 角色列表接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 17.1 创建role.schema.ts - 定义角色Schema - - [ ] 17.2 创建role.response.ts - 定义角色响应格式 - - [ ] 17.3 创建role.service.ts - 实现角色业务逻辑 - - [ ] 17.4 创建role.controller.ts - 实现角色路由 - - [ ] 17.5 创建role.test.md - 编写角色测试用例文档 - -- [ ] 18.0 POST /roles - 创建角色接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 18.1 扩展role.schema.ts - 定义创建角色Schema - - [ ] 18.2 扩展role.response.ts - 定义创建角色响应格式 - - [ ] 18.3 扩展role.service.ts - 实现创建角色业务逻辑 - - [ ] 18.4 扩展role.controller.ts - 实现创建角色路由 - - [ ] 18.5 扩展role.test.md - 编写创建角色测试用例文档 - -- [ ] 19.0 PUT /roles/{id} - 更新角色接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 19.1 扩展role.schema.ts - 定义更新角色Schema - - [ ] 19.2 扩展role.response.ts - 定义更新角色响应格式 - - [ ] 19.3 扩展role.service.ts - 实现更新角色业务逻辑 - - [ ] 19.4 扩展role.controller.ts - 实现更新角色路由 - - [ ] 19.5 扩展role.test.md - 编写更新角色测试用例文档 - -- [ ] 20.0 DELETE /roles/{id} - 删除角色接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 20.1 扩展role.schema.ts - 定义删除角色Schema - - [ ] 20.2 扩展role.response.ts - 定义删除角色响应格式 - - [ ] 20.3 扩展role.service.ts - 实现删除角色业务逻辑 - - [ ] 20.4 扩展role.controller.ts - 实现删除角色路由 - - [ ] 20.5 扩展role.test.md - 编写删除角色测试用例文档 - -- [ ] 21.0 GET /permissions - 权限列表接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 21.1 创建permission.schema.ts - 定义权限Schema - - [ ] 21.2 创建permission.response.ts - 定义权限响应格式 - - [ ] 21.3 创建permission.service.ts - 实现权限业务逻辑 - - [ ] 21.4 创建permission.controller.ts - 实现权限路由 - - [ ] 21.5 创建permission.test.md - 编写权限测试用例文档 - -- [ ] 22.0 POST /roles/{id}/permissions - 权限分配接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 22.1 扩展role.schema.ts - 定义权限分配Schema - - [ ] 22.2 扩展role.response.ts - 定义权限分配响应格式 - - [ ] 22.3 扩展role.service.ts - 实现权限分配业务逻辑 - - [ ] 22.4 扩展role.controller.ts - 实现权限分配路由 - - [ ] 22.5 扩展role.test.md - 编写权限分配测试用例文档 - -- [ ] 23.0 POST /users/{id}/roles - 用户角色分配接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 23.1 扩展user.schema.ts - 定义用户角色分配Schema - - [ ] 23.2 扩展user.response.ts - 定义用户角色分配响应格式 - - [ ] 23.3 扩展user.service.ts - 实现用户角色分配业务逻辑 - - [ ] 23.4 扩展user.controller.ts - 实现用户角色分配路由 - - [ ] 23.5 扩展user.test.md - 编写用户角色分配测试用例文档 - -### 🏢 组织架构模块 (Organization Module) - P1优先级 - -- [ ] 24.0 GET /organizations - 组织列表接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 24.1 创建organization.schema.ts - 定义组织Schema - - [ ] 24.2 创建organization.response.ts - 定义组织响应格式 - - [ ] 24.3 创建organization.service.ts - 实现组织业务逻辑 - - [ ] 24.4 创建organization.controller.ts - 实现组织路由 - - [ ] 24.5 创建organization.test.md - 编写组织测试用例文档 - -- [ ] 25.0 POST /organizations - 创建组织接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 25.1 扩展organization.schema.ts - 定义创建组织Schema - - [ ] 25.2 扩展organization.response.ts - 定义创建组织响应格式 - - [ ] 25.3 扩展organization.service.ts - 实现创建组织业务逻辑 - - [ ] 25.4 扩展organization.controller.ts - 实现创建组织路由 - - [ ] 25.5 扩展organization.test.md - 编写创建组织测试用例文档 - -- [ ] 26.0 PUT /organizations/{id} - 更新组织接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 26.1 扩展organization.schema.ts - 定义更新组织Schema - - [ ] 26.2 扩展organization.response.ts - 定义更新组织响应格式 - - [ ] 26.3 扩展organization.service.ts - 实现更新组织业务逻辑 - - [ ] 26.4 扩展organization.controller.ts - 实现更新组织路由 - - [ ] 26.5 扩展organization.test.md - 编写更新组织测试用例文档 - -- [ ] 27.0 DELETE /organizations/{id} - 删除组织接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 27.1 扩展organization.schema.ts - 定义删除组织Schema - - [ ] 27.2 扩展organization.response.ts - 定义删除组织响应格式 - - [ ] 27.3 扩展organization.service.ts - 实现删除组织业务逻辑 - - [ ] 27.4 扩展organization.controller.ts - 实现删除组织路由 - - [ ] 27.5 扩展organization.test.md - 编写删除组织测试用例文档 - -- [ ] 28.0 POST /users/{id}/organizations - 用户组织关系接口 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 28.1 扩展user.schema.ts - 定义用户组织关系Schema - - [ ] 28.2 扩展user.response.ts - 定义用户组织关系响应格式 - - [ ] 28.3 扩展user.service.ts - 实现用户组织关系业务逻辑 - - [ ] 28.4 扩展user.controller.ts - 实现用户组织关系路由 - - [ ] 28.5 扩展user.test.md - 编写用户组织关系测试用例文档 - -### 🗂️ 系统基础模块 (System Module) - P1优先级 - -- [ ] 29.0 字典类型管理 - CRUD /dict-types - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 29.1 创建dict.schema.ts - 定义字典类型Schema - - [ ] 29.2 创建dict.response.ts - 定义字典类型响应格式 - - [ ] 29.3 创建dict.service.ts - 实现字典类型业务逻辑 - - [ ] 29.4 创建dict.controller.ts - 实现字典类型路由 - - [ ] 29.5 创建dict.test.md - 编写字典类型测试用例文档 - -- [ ] 30.0 字典项管理 - CRUD /dict-items - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 30.1 扩展dict.schema.ts - 定义字典项Schema - - [ ] 30.2 扩展dict.response.ts - 定义字典项响应格式 - - [ ] 30.3 扩展dict.service.ts - 实现字典项业务逻辑 - - [ ] 30.4 扩展dict.controller.ts - 实现字典项路由 - - [ ] 30.5 扩展dict.test.md - 编写字典项测试用例文档 - -- [ ] 31.0 标签管理 - CRUD /tags - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 31.1 创建tag.schema.ts - 定义标签Schema - - [ ] 31.2 创建tag.response.ts - 定义标签响应格式 - - [ ] 31.3 创建tag.service.ts - 实现标签业务逻辑 - - [ ] 31.4 创建tag.controller.ts - 实现标签路由 - - [ ] 31.5 创建tag.test.md - 编写标签测试用例文档 - -- [ ] 32.0 操作日志 - GET /logs/operations - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 32.1 创建log.schema.ts - 定义操作日志Schema - - [ ] 32.2 创建log.response.ts - 定义操作日志响应格式 - - [ ] 32.3 创建log.service.ts - 实现操作日志业务逻辑 - - [ ] 32.4 创建log.controller.ts - 实现操作日志路由 - - [ ] 32.5 创建log.test.md - 编写操作日志测试用例文档 - -- [ ] 33.0 登录日志 - GET /logs/logins - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 33.1 扩展log.schema.ts - 定义登录日志Schema - - [ ] 33.2 扩展log.response.ts - 定义登录日志响应格式 - - [ ] 33.3 扩展log.service.ts - 实现登录日志业务逻辑 - - [ ] 33.4 扩展log.controller.ts - 实现登录日志路由 - - [ ] 33.5 扩展log.test.md - 编写登录日志测试用例文档 - -### 🔧 基础设施完善 - -- [ ] 34.0 JWT认证中间件 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 34.1 创建JWT认证插件 - - [ ] 34.2 实现Token黑名单管理 - - [ ] 34.3 实现RefreshToken机制 - - [ ] 34.4 集成权限验证中间件 - - [ ] 34.5 编写认证中间件测试 - -- [ ] 35.0 路由模块集成 - - [ ] Before 整理输入此接口的逻辑,必须等待用户确认后进行,需要输入go才能进行下一步 - - [ ] 35.1 更新src/modules/index.ts - 集成所有模块 - - [ ] 35.2 更新src/app.ts - 注册所有路由 - - [ ] 35.3 更新Swagger标签定义 - - [ ] 35.4 完善API文档 - - [ ] 35.5 集成测试验证 - -## 开发优先级说明 - -### 第一阶段(P0):基础认证和用户管理 -- **认证模块**:用户注册、激活、登录、刷新、退出(任务1-5) -- **用户管理模块**:当前用户、用户列表、用户CRUD、密码管理(任务9-14) -- **完成目标**:具备基本的用户认证和管理功能 - -### 第二阶段(P0):角色权限系统 -- **角色管理**:角色CRUD、权限分配(任务17-20) -- **权限管理**:权限列表、权限分配、用户角色分配(任务21-23) -- **完成目标**:具备完整的RBAC权限控制体系 - -### 第三阶段(P1):扩展功能 -- **密码管理**:找回密码、重置密码(任务6-7) -- **验证码系统**:图形验证码(任务8) -- **用户扩展**:用户详情、批量操作(任务15-16) -- **组织架构**:组织管理、用户组织关系(任务24-28) - -### 第四阶段(P1-P2):系统完善 -- **系统基础**:字典、标签、日志管理(任务29-33) -- **基础设施**:JWT中间件、路由集成(任务34-35) -- **完成目标**:系统功能完整,可投入生产使用 - -## 备注说明 - -1. **已完成部分**:用户模块的Schema、Response、Service已基本完成,可直接使用 -2. **验证码集成**:现有captcha模块可直接集成到认证流程中 -3. **开发规范**:严格按照Elysia开发规范,每个接口都要有完整的5个子任务 -4. **测试要求**:每个模块都要有完整的测试用例文档,确保功能正确性 -5. **优先级管理**:按照P0 > P1 > P2的顺序开发,确保核心功能优先完成 \ No newline at end of file diff --git a/tasks/archive/20250629-计划.md b/tasks/archive/20250629-计划.md index 2197cc5..88601e2 100644 --- a/tasks/archive/20250629-计划.md +++ b/tasks/archive/20250629-计划.md @@ -1,3 +1,3 @@ 1. 协助ai完成接口 2. 协助ai完成测试用例 -3. 优化mdc关于drizzle和redis的使用 \ No newline at end of file +3. 优化mdc关于drizzle和redis的使用 diff --git a/tasks/M2-基础用户系统-开发PRD.md b/tasks/archive/M2-基础用户系统-开发PRD.md similarity index 50% rename from tasks/M2-基础用户系统-开发PRD.md rename to tasks/archive/M2-基础用户系统-开发PRD.md index e407d72..1407079 100644 --- a/tasks/M2-基础用户系统-开发PRD.md +++ b/tasks/archive/M2-基础用户系统-开发PRD.md @@ -3,9 +3,11 @@ ## 1. 项目概述 ### 1.1 项目背景 + 基于星撰个人综合平台的M2阶段需求,开发完整的基础用户系统。该系统将为后续的博客系统、个人空间、阅读系统等功能模块提供统一的用户管理和权限控制基础。 ### 1.2 项目目标 + - 构建安全可靠的用户认证和授权体系 - 实现灵活的角色和权限管理 - 提供完整的用户生命周期管理 @@ -13,7 +15,9 @@ - 为后续功能模块提供统一的用户服务 ### 1.3 项目范围 + 本次开发包含以下核心模块: + - **认证模块**:注册、登录、激活、密码管理 - **用户管理模块**:用户CRUD、信息管理、状态管理 - **角色权限模块**:角色管理、权限分配、权限验证 @@ -25,61 +29,67 @@ ### 2.1 核心功能清单 #### 2.1.1 认证模块 (Auth) -| 功能 | 优先级 | 状态 | 接口 | -|------|--------|------|------| -| 用户注册 | P0 | 待开发 | POST /auth/register | -| 邮箱激活 | P0 | 待开发 | POST /auth/activate | -| 用户登录 | P0 | 待开发 | POST /auth/login | -| Token刷新 | P0 | 待开发 | POST /auth/refresh | -| 退出登录 | P0 | 待开发 | POST /auth/logout | -| 找回密码 | P1 | 待开发 | POST /auth/password/reset-request | -| 重置密码 | P1 | 待开发 | POST /auth/password/reset-confirm | -| 图形验证码 | P1 | 待开发 | GET /auth/captcha | + +| 功能 | 优先级 | 状态 | 接口 | +| ---------- | ------ | ------ | --------------------------------- | +| 用户注册 | P0 | 待开发 | POST /auth/register | +| 邮箱激活 | P0 | 待开发 | POST /auth/activate | +| 用户登录 | P0 | 待开发 | POST /auth/login | +| Token刷新 | P0 | 待开发 | POST /auth/refresh | +| 退出登录 | P0 | 待开发 | POST /auth/logout | +| 找回密码 | P1 | 待开发 | POST /auth/password/reset-request | +| 重置密码 | P1 | 待开发 | POST /auth/password/reset-confirm | +| 图形验证码 | P1 | 待开发 | GET /auth/captcha | #### 2.1.2 用户管理模块 (User) -| 功能 | 优先级 | 状态 | 接口 | -|------|--------|------|------| -| 获取当前用户信息 | P0 | 待开发 | GET /users/me | -| 用户列表查询 | P0 | 待开发 | GET /users | -| 创建用户 | P0 | 待开发 | POST /users | -| 更新用户信息 | P0 | 待开发 | PUT /users/{id} | -| 删除用户 | P1 | 待开发 | DELETE /users/{id} | -| 修改密码 | P0 | 待开发 | PUT /users/me/password | -| 用户详情 | P1 | 待开发 | GET /users/{id} | -| 批量操作 | P2 | 待开发 | POST /users/batch | + +| 功能 | 优先级 | 状态 | 接口 | +| ---------------- | ------ | ------ | ---------------------- | +| 获取当前用户信息 | P0 | 待开发 | GET /users/me | +| 用户列表查询 | P0 | 待开发 | GET /users | +| 创建用户 | P0 | 待开发 | POST /users | +| 更新用户信息 | P0 | 待开发 | PUT /users/{id} | +| 删除用户 | P1 | 待开发 | DELETE /users/{id} | +| 修改密码 | P0 | 待开发 | PUT /users/me/password | +| 用户详情 | P1 | 待开发 | GET /users/{id} | +| 批量操作 | P2 | 待开发 | POST /users/batch | #### 2.1.3 角色权限模块 (Role & Permission) -| 功能 | 优先级 | 状态 | 接口 | -|------|--------|------|------| -| 角色列表 | P0 | 待开发 | GET /roles | -| 创建角色 | P0 | 待开发 | POST /roles | -| 更新角色 | P0 | 待开发 | PUT /roles/{id} | -| 删除角色 | P1 | 待开发 | DELETE /roles/{id} | -| 权限列表 | P0 | 待开发 | GET /permissions | -| 权限分配 | P0 | 待开发 | POST /roles/{id}/permissions | -| 用户角色分配 | P0 | 待开发 | POST /users/{id}/roles | + +| 功能 | 优先级 | 状态 | 接口 | +| ------------ | ------ | ------ | ---------------------------- | +| 角色列表 | P0 | 待开发 | GET /roles | +| 创建角色 | P0 | 待开发 | POST /roles | +| 更新角色 | P0 | 待开发 | PUT /roles/{id} | +| 删除角色 | P1 | 待开发 | DELETE /roles/{id} | +| 权限列表 | P0 | 待开发 | GET /permissions | +| 权限分配 | P0 | 待开发 | POST /roles/{id}/permissions | +| 用户角色分配 | P0 | 待开发 | POST /users/{id}/roles | #### 2.1.4 组织架构模块 (Organization) -| 功能 | 优先级 | 状态 | 接口 | -|------|--------|------|------| -| 组织列表 | P1 | 待开发 | GET /organizations | -| 创建组织 | P1 | 待开发 | POST /organizations | -| 更新组织 | P1 | 待开发 | PUT /organizations/{id} | -| 删除组织 | P2 | 待开发 | DELETE /organizations/{id} | -| 用户组织关系 | P1 | 待开发 | POST /users/{id}/organizations | + +| 功能 | 优先级 | 状态 | 接口 | +| ------------ | ------ | ------ | ------------------------------ | +| 组织列表 | P1 | 待开发 | GET /organizations | +| 创建组织 | P1 | 待开发 | POST /organizations | +| 更新组织 | P1 | 待开发 | PUT /organizations/{id} | +| 删除组织 | P2 | 待开发 | DELETE /organizations/{id} | +| 用户组织关系 | P1 | 待开发 | POST /users/{id}/organizations | #### 2.1.5 系统基础模块 (System) -| 功能 | 优先级 | 状态 | 接口 | -|------|--------|------|------| -| 字典类型管理 | P1 | 待开发 | CRUD /dict-types | -| 字典项管理 | P1 | 待开发 | CRUD /dict-items | -| 标签管理 | P1 | 待开发 | CRUD /tags | -| 操作日志 | P2 | 待开发 | GET /logs/operations | -| 登录日志 | P2 | 待开发 | GET /logs/logins | + +| 功能 | 优先级 | 状态 | 接口 | +| ------------ | ------ | ------ | -------------------- | +| 字典类型管理 | P1 | 待开发 | CRUD /dict-types | +| 字典项管理 | P1 | 待开发 | CRUD /dict-items | +| 标签管理 | P1 | 待开发 | CRUD /tags | +| 操作日志 | P2 | 待开发 | GET /logs/operations | +| 登录日志 | P2 | 待开发 | GET /logs/logins | ### 2.2 技术架构 #### 2.2.1 后端架构 + - **框架**:Elysia (Bun) - **数据库**:MySQL + Drizzle ORM - **缓存**:Redis @@ -89,11 +99,13 @@ - **API文档**:Swagger #### 2.2.2 数据存储策略 + - **主数据**:MySQL存储 - **缓存数据**:Redis存储用户会话、验证码、权限缓存 - **文件存储**:本地存储 + 后续可扩展OSS #### 2.2.3 安全策略 + - **密码加密**:bcrypt (cost=12) - **JWT策略**:AccessToken(2小时) + RefreshToken(7天) - **权限控制**:RBAC模型 @@ -105,9 +117,11 @@ ### 3.1 阶段划分 #### 第一阶段:基础认证系统 (1-2周) + **目标**:完成用户注册、登录、基础用户管理功能 **核心任务**: + - 数据库表结构完善 - 用户注册/登录接口 - JWT认证中间件 @@ -115,14 +129,17 @@ - 单元测试编写 **交付物**: + - 可工作的用户注册登录系统 - 完整的API文档 - 基础测试用例 #### 第二阶段:角色权限系统 (1-2周) + **目标**:完成RBAC权限控制体系 **核心任务**: + - 角色管理功能 - 权限管理功能 - 权限验证中间件 @@ -130,14 +147,17 @@ - 权限缓存机制 **交付物**: + - 完整的RBAC系统 - 权限控制中间件 - 管理员操作界面接口 #### 第三阶段:系统完善 (1周) + **目标**:完善系统功能,优化性能 **核心任务**: + - 组织架构管理 - 字典标签管理 - 操作日志记录 @@ -145,34 +165,38 @@ - 安全加固 **交付物**: + - 完整的用户系统 - 性能测试报告 - 部署文档 ### 3.2 里程碑计划 -| 里程碑 | 时间节点 | 主要交付物 | -|--------|----------|------------| -| M2.1 | 第1周末 | 基础认证功能完成 | -| M2.2 | 第2周末 | 用户管理功能完成 | -| M2.3 | 第3周末 | 角色权限功能完成 | -| M2.4 | 第4周末 | 系统完善,发布v1.0 | +| 里程碑 | 时间节点 | 主要交付物 | +| ------ | -------- | ------------------ | +| M2.1 | 第1周末 | 基础认证功能完成 | +| M2.2 | 第2周末 | 用户管理功能完成 | +| M2.3 | 第3周末 | 角色权限功能完成 | +| M2.4 | 第4周末 | 系统完善,发布v1.0 | ## 4. 质量保障 ### 4.1 测试策略 + - **单元测试**:覆盖率 > 80% - **集成测试**:关键业务流程测试 - **性能测试**:并发用户1000+ - **安全测试**:OWASP Top 10检查 ### 4.2 代码质量 + - **代码规范**:ESLint + Prettier - **类型安全**:TypeScript严格模式 - **代码审查**:Pull Request必须审查 - **文档完善**:接口文档、部署文档 ### 4.3 监控告警 + - **API监控**:响应时间、错误率 - **数据库监控**:连接数、慢查询 - **缓存监控**:Redis状态监控 @@ -181,34 +205,39 @@ ## 5. 风险评估 ### 5.1 技术风险 -| 风险 | 概率 | 影响 | 应对策略 | -|------|------|------|----------| -| 数据库性能瓶颈 | 中 | 高 | 索引优化、查询优化 | -| JWT安全性问题 | 低 | 高 | RefreshToken轮转、黑名单机制 | -| 并发数据一致性 | 中 | 中 | 乐观锁、Redis锁 | + +| 风险 | 概率 | 影响 | 应对策略 | +| -------------- | ---- | ---- | ---------------------------- | +| 数据库性能瓶颈 | 中 | 高 | 索引优化、查询优化 | +| JWT安全性问题 | 低 | 高 | RefreshToken轮转、黑名单机制 | +| 并发数据一致性 | 中 | 中 | 乐观锁、Redis锁 | ### 5.2 进度风险 -| 风险 | 概率 | 影响 | 应对策略 | -|------|------|------|----------| -| 需求变更 | 中 | 中 | 敏捷开发、版本控制 | -| 技术难点 | 低 | 中 | 技术预研、备选方案 | -| 测试时间不足 | 中 | 高 | 并行开发测试、自动化测试 | + +| 风险 | 概率 | 影响 | 应对策略 | +| ------------ | ---- | ---- | ------------------------ | +| 需求变更 | 中 | 中 | 敏捷开发、版本控制 | +| 技术难点 | 低 | 中 | 技术预研、备选方案 | +| 测试时间不足 | 中 | 高 | 并行开发测试、自动化测试 | ## 6. 成功标准 ### 6.1 功能指标 + - ✅ 用户注册成功率 > 95% - ✅ 登录响应时间 < 200ms - ✅ 权限验证准确率 = 100% - ✅ 密码安全强度符合规范 ### 6.2 技术指标 + - ✅ API可用性 > 99.9% - ✅ 单元测试覆盖率 > 80% - ✅ 代码质量评分 > 8.0 - ✅ 安全漏洞 = 0 ### 6.3 用户体验指标 + - ✅ 注册流程步骤 ≤ 3步 - ✅ 忘记密码找回成功率 > 90% - ✅ 用户反馈满意度 > 85% @@ -216,19 +245,22 @@ ## 7. 后续规划 ### 7.1 功能扩展 + - OAuth第三方登录(微信、GitHub等) - 两步验证(2FA) - 单点登录(SSO) - 用户行为分析 ### 7.2 性能优化 + - 分库分表策略 - 缓存策略优化 - CDN接入 - 数据库读写分离 ### 7.3 运维支持 + - 容器化部署 - 监控体系完善 - 自动化运维 -- 灾备方案 \ No newline at end of file +- 灾备方案 diff --git a/tasks/archive/M2-基础用户系统-开发任务计划.md b/tasks/archive/M2-基础用户系统-开发任务计划.md new file mode 100644 index 0000000..54e01cd --- /dev/null +++ b/tasks/archive/M2-基础用户系统-开发任务计划.md @@ -0,0 +1,390 @@ +# M2 - 基础用户系统 - 开发任务计划 + +## 相关文件 (Relevant Files) + +### 认证模块 (Auth) + +- `src/modules/auth/auth.schema.ts` - 认证模块Schema定义 +- `src/modules/auth/auth.response.ts` - 认证模块响应格式定义 +- `src/modules/auth/auth.service.ts` - 认证模块Service层实现 +- `src/modules/auth/auth.controller.ts` - 认证模块Controller层实现 +- `src/modules/auth/auth.test.md` - 认证模块测试用例文档 + +### 用户管理模块 (User) + +- `src/modules/user/user.schema.ts` - 用户模块Schema定义(已存在) +- `src/modules/user/user.response.ts` - 用户模块响应格式定义(已存在) +- `src/modules/user/user.service.ts` - 用户模块Service层实现(已存在) +- `src/modules/user/user.controller.ts` - 用户模块Controller层实现(需更新) +- `src/modules/user/user.test.md` - 用户模块测试用例文档 + +### 角色权限模块 (Role) + +- `src/modules/role/role.schema.ts` - 角色模块Schema定义 +- `src/modules/role/role.response.ts` - 角色模块响应格式定义 +- `src/modules/role/role.service.ts` - 角色模块Service层实现 +- `src/modules/role/role.controller.ts` - 角色模块Controller层实现 +- `src/modules/role/role.test.md` - 角色模块测试用例文档 + +### 权限管理模块 (Permission) + +- `src/modules/permission/permission.schema.ts` - 权限模块Schema定义 +- `src/modules/permission/permission.response.ts` - 权限模块响应格式定义 +- `src/modules/permission/permission.service.ts` - 权限模块Service层实现 +- `src/modules/permission/permission.controller.ts` - 权限模块Controller层实现 +- `src/modules/permission/permission.test.md` - 权限模块测试用例文档 + +### 组织架构模块 (Organization) + +- `src/modules/organization/organization.schema.ts` - 组织模块Schema定义 +- `src/modules/organization/organization.response.ts` - 组织模块响应格式定义 +- `src/modules/organization/organization.service.ts` - 组织模块Service层实现 +- `src/modules/organization/organization.controller.ts` - 组织模块Controller层实现 +- `src/modules/organization/organization.test.md` - 组织模块测试用例文档 + +### 系统基础模块 (System) + +- `src/modules/system/dict/dict.schema.ts` - 字典模块Schema定义 +- `src/modules/system/dict/dict.response.ts` - 字典模块响应格式定义 +- `src/modules/system/dict/dict.service.ts` - 字典模块Service层实现 +- `src/modules/system/dict/dict.controller.ts` - 字典模块Controller层实现 +- `src/modules/system/dict/dict.test.md` - 字典模块测试用例文档 +- `src/modules/system/tag/tag.schema.ts` - 标签模块Schema定义 +- `src/modules/system/tag/tag.response.ts` - 标签模块响应格式定义 +- `src/modules/system/tag/tag.service.ts` - 标签模块Service层实现 +- `src/modules/system/tag/tag.controller.ts` - 标签模块Controller层实现 +- `src/modules/system/tag/tag.test.md` - 标签模块测试用例文档 +- `src/modules/system/log/log.schema.ts` - 日志模块Schema定义 +- `src/modules/system/log/log.response.ts` - 日志模块响应格式定义 +- `src/modules/system/log/log.service.ts` - 日志模块Service层实现 +- `src/modules/system/log/log.controller.ts` - 日志模块Controller层实现 +- `src/modules/system/log/log.test.md` - 日志模块测试用例文档 + +### 备注 (Notes) + +- 验证码功能已有captcha模块可直接集成 +- 遵循Elysia开发规范,每个接口都要有完整的5个文件 +- 按照PRD优先级:P0 > P1 > P2 顺序开发 + +## 任务 (Tasks) + +### 🔐 认证模块 (Auth Module) - P0优先级 + +- [x] 1.0 POST /auth/register - 用户注册接口 + - [x] 1.1 创建auth.schema.ts - 定义用户注册Schema + - [x] 1.2 创建auth.response.ts - 定义注册响应格式 + - [x] 1.3 创建auth.service.ts - 实现注册业务逻辑 + - [x] 1.4 创建auth.controller.ts - 实现注册路由 + - [x] 1.5 创建auth.test.md - 编写注册测试用例文档 + +- [x] 2.0 POST /auth/activate - 邮箱激活接口 + - [x] 2.1 扩展auth.schema.ts - 定义激活Schema + - [x] 2.2 扩展auth.response.ts - 定义激活响应格式 + - [x] 2.3 扩展auth.service.ts - 实现激活业务逻辑 + - [x] 2.4 扩展auth.controller.ts - 实现激活路由 + - [x] 2.5 扩展auth.test.md - 编写激活测试用例文档 + +- [x] 3.0 POST /auth/login - 用户登录接口 + - [x] 3.1 扩展auth.schema.ts - 定义登录Schema + - [x] 3.2 扩展auth.response.ts - 定义登录响应格式 + - [x] 3.3 扩展auth.service.ts - 实现登录业务逻辑 + - [x] 3.4 扩展auth.controller.ts - 实现登录路由 + - [x] 3.5 扩展auth.test.md - 编写登录测试用例文档 + +- [x] 4.0 POST /auth/refresh - Token刷新接口 + - [x] 4.1 扩展auth.schema.ts - 定义刷新Schema + - [x] 4.2 扩展auth.response.ts - 定义刷新响应格式 + - [x] 4.3 扩展auth.service.ts - 实现刷新业务逻辑 + - [x] 4.4 扩展auth.controller.ts - 实现刷新路由 + - [x] 4.5 扩展auth.test.md - 编写刷新测试用例文档 + +- ~~[ ] 5.0 POST /auth/logout - 退出登录接口~~ + - ~~[ ] 5.1 扩展auth.schema.ts - 定义退出Schema~~ + - ~~[ ] 5.2 扩展auth.response.ts - 定义退出响应格式~~ + - ~~[ ] 5.3 扩展auth.service.ts - 实现退出业务逻辑~~ + - ~~[ ] 5.4 扩展auth.controller.ts - 实现退出路由~~ + - ~~[ ] 5.5 扩展auth.test.md - 编写退出测试用例文档~~ + +- [x] 6.0 POST /auth/password/reset-request - 找回密码接口 + - [x] 6.1 扩展auth.schema.ts - 定义找回密码Schema + - [x] 6.2 扩展auth.response.ts - 定义找回密码响应格式 + - [x] 6.3 扩展auth.service.ts - 实现找回密码业务逻辑 + - [x] 6.4 扩展auth.controller.ts - 实现找回密码路由 + - [x] 6.5 扩展auth.test.md - 编写找回密码测试用例文档 + +- [x] 7.0 POST /auth/password/reset-confirm - 重置密码接口 + - [x] 7.1 扩展auth.schema.ts - 定义重置密码Schema + - [x] 7.2 扩展auth.response.ts - 定义重置密码响应格式 + - [x] 7.3 扩展auth.service.ts - 实现重置密码业务逻辑 + - [x] 7.4 扩展auth.controller.ts - 实现重置密码路由 + - [x] 7.5 扩展auth.test.md - 编写重置密码测试用例文档 + +- [x] 8.0 GET /auth/captcha - 图形验证码接口 + - [x] 8.1 扩展auth.schema.ts - 定义验证码Schema + - [x] 8.2 扩展auth.response.ts - 定义验证码响应格式 + - [x] 8.3 扩展auth.service.ts - 集成验证码服务 + - [x] 8.4 扩展auth.controller.ts - 实现验证码路由 + - [x] 8.5 扩展auth.test.md - 编写验证码测试用例文档 + +- [x] 9.0 检查认证模块 (Auth Module)那些接口需要加分布式锁,并加锁 + +### 👤 用户管理模块 (User Module) - P0优先级 + +- [x] 9.0 GET /users/me - 获取当前用户信息接口 + - [x] Before 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [x] 9.1 扩展user.schema.ts - 定义当前用户Schema + - [x] 9.2 扩展user.response.ts - 定义当前用户响应格式 + - [x] 9.3 扩展user.service.ts - 实现当前用户业务逻辑 + - [x] 9.4 更新user.controller.ts - 实现当前用户路由 + - [x] 9.5 创建user.test.md - 编写当前用户测试用例文档 + +- [x] 10.0 GET /users - 用户列表查询接口 + - [x] 10.1 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [x] 10.2 扩展user.schema.ts - 定义用户列表Schema + - [x] 10.3 扩展user.response.ts - 定义用户列表响应格式 + - [x] 10.4 扩展user.service.ts - 实现用户列表业务逻辑 + - [x] 10.5 扩展user.controller.ts - 实现用户列表路由 + - [x] 10.6 扩展user.test.md - 编写用户列表测试用例文档 + +- [ ] 11.0 POST /users - 创建用户接口 + - [x] 11.1 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 11.2 扩展user.schema.ts - 定义创建用户Schema + - [ ] 11.3 扩展user.response.ts - 定义创建用户响应格式 + - [ ] 11.4 扩展user.service.ts - 根据当前单个接口doc.md实现创建用户业务逻辑 + - [ ] 11.5 扩展user.controller.ts - 实现创建用户路由 + - [ ] 11.6 扩展user.test.md - 编写创建用户测试用例文档 + +- [ ] 12.0 PUT /users/{id} - 更新用户信息接口 + - [ ] 12.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 12.1 扩展user.schema.ts - 定义更新用户Schema + - [ ] 12.2 扩展user.response.ts - 定义更新用户响应格式 + - [ ] 12.3 扩展user.service.ts - 根据当前单个接口doc.md实现更新用户业务逻辑 + - [ ] 12.4 扩展user.controller.ts - 实现更新用户路由 + - [ ] 12.5 扩展user.test.md - 编写更新用户测试用例文档 + +- [ ] 13.0 DELETE /users/{id} - 删除用户接口 + - [ ] 13.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 13.1 扩展user.schema.ts - 定义删除用户Schema + - [ ] 13.2 扩展user.response.ts - 定义删除用户响应格式 + - [ ] 13.3 扩展user.service.ts - 根据当前单个接口doc.md实现删除用户业务逻辑 + - [ ] 13.4 扩展user.controller.ts - 实现删除用户路由 + - [ ] 13.5 扩展user.test.md - 编写删除用户测试用例文档 + +- [ ] 14.0 PUT /users/me/password - 修改密码接口 + - [ ] 14.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 14.1 扩展user.schema.ts - 定义修改密码Schema + - [ ] 14.2 扩展user.response.ts - 定义修改密码响应格式 + - [ ] 14.3 扩展user.service.ts - 根据当前单个接口doc.md实现修改密码业务逻辑 + - [ ] 14.4 扩展user.controller.ts - 实现修改密码路由 + - [ ] 14.5 扩展user.test.md - 编写修改密码测试用例文档 + +- [ ] 15.0 GET /users/{id} - 用户详情接口 + - [ ] 15.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 15.1 扩展user.schema.ts - 定义用户详情Schema + - [ ] 15.2 扩展user.response.ts - 定义用户详情响应格式 + - [ ] 15.3 扩展user.service.ts - 根据当前单个接口doc.md实现用户详情业务逻辑 + - [ ] 15.4 扩展user.controller.ts - 实现用户详情路由 + - [ ] 15.5 扩展user.test.md - 编写用户详情测试用例文档 + +- [ ] 16.0 POST /users/batch - 批量操作接口 + - [ ] 16.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 16.1 扩展user.schema.ts - 定义批量操作Schema + - [ ] 16.2 扩展user.response.ts - 定义批量操作响应格式 + - [ ] 16.3 扩展user.service.ts - 根据当前单个接口doc.md实现批量操作业务逻辑 + - [ ] 16.4 扩展user.controller.ts - 实现批量操作路由 + - [ ] 16.5 扩展user.test.md - 编写批量操作测试用例文档 + +### 🎭 角色权限模块 (Role Module) - P0优先级 + +- [ ] 17.0 GET /roles - 角色列表接口 + - [ ] 17.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 17.1 创建role.schema.ts - 定义角色Schema + - [ ] 17.2 创建role.response.ts - 定义角色响应格式 + - [ ] 17.3 创建role.service.ts - 根据当前单个接口doc.md实现角色业务逻辑 + - [ ] 17.4 创建role.controller.ts - 实现角色路由 + - [ ] 17.5 创建role.test.md - 编写角色测试用例文档 + +- [ ] 18.0 POST /roles - 创建角色接口 + - [ ] 18.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 18.1 扩展role.schema.ts - 定义创建角色Schema + - [ ] 18.2 扩展role.response.ts - 定义创建角色响应格式 + - [ ] 18.3 扩展role.service.ts - 根据当前单个接口doc.md实现创建角色业务逻辑 + - [ ] 18.4 扩展role.controller.ts - 实现创建角色路由 + - [ ] 18.5 扩展role.test.md - 编写创建角色测试用例文档 + +- [ ] 19.0 PUT /roles/{id} - 更新角色接口 + - [ ] 19.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 19.1 扩展role.schema.ts - 定义更新角色Schema + - [ ] 19.2 扩展role.response.ts - 定义更新角色响应格式 + - [ ] 19.3 扩展role.service.ts - 根据当前单个接口doc.md实现更新角色业务逻辑 + - [ ] 19.4 扩展role.controller.ts - 实现更新角色路由 + - [ ] 19.5 扩展role.test.md - 编写更新角色测试用例文档 + +- [ ] 20.0 DELETE /roles/{id} - 删除角色接口 + - [ ] 20.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 20.1 扩展role.schema.ts - 定义删除角色Schema + - [ ] 20.2 扩展role.response.ts - 定义删除角色响应格式 + - [ ] 20.3 扩展role.service.ts - 根据当前单个接口doc.md实现删除角色业务逻辑 + - [ ] 20.4 扩展role.controller.ts - 实现删除角色路由 + - [ ] 20.5 扩展role.test.md - 编写删除角色测试用例文档 + +- [ ] 21.0 GET /permissions - 权限列表接口 + - [ ] 21.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 21.1 创建permission.schema.ts - 定义权限Schema + - [ ] 21.2 创建permission.response.ts - 定义权限响应格式 + - [ ] 21.3 创建permission.service.ts - 根据当前单个接口doc.md实现权限业务逻辑 + - [ ] 21.4 创建permission.controller.ts - 实现权限路由 + - [ ] 21.5 创建permission.test.md - 编写权限测试用例文档 + +- [ ] 22.0 POST /roles/{id}/permissions - 权限分配接口 + - [ ] 22.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 22.1 扩展role.schema.ts - 定义权限分配Schema + - [ ] 22.2 扩展role.response.ts - 定义权限分配响应格式 + - [ ] 22.3 扩展role.service.ts - 根据当前单个接口doc.md实现权限分配业务逻辑 + - [ ] 22.4 扩展role.controller.ts - 实现权限分配路由 + - [ ] 22.5 扩展role.test.md - 编写权限分配测试用例文档 + +- [ ] 23.0 POST /users/{id}/roles - 用户角色分配接口 + - [ ] 23.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 23.1 扩展user.schema.ts - 定义用户角色分配Schema + - [ ] 23.2 扩展user.response.ts - 定义用户角色分配响应格式 + - [ ] 23.3 扩展user.service.ts - 根据当前单个接口doc.md实现用户角色分配业务逻辑 + - [ ] 23.4 扩展user.controller.ts - 实现用户角色分配路由 + - [ ] 23.5 扩展user.test.md - 编写用户角色分配测试用例文档 + +### 🏢 组织架构模块 (Organization Module) - P1优先级 + +- [ ] 24.0 GET /organizations - 组织列表接口 + - [ ] 24.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 24.1 创建organization.schema.ts - 定义组织Schema + - [ ] 24.2 创建organization.response.ts - 定义组织响应格式 + - [ ] 24.3 创建organization.service.ts - 根据当前单个接口doc.md实现组织业务逻辑 + - [ ] 24.4 创建organization.controller.ts - 实现组织路由 + - [ ] 24.5 创建organization.test.md - 编写组织测试用例文档 + +- [ ] 25.0 POST /organizations - 创建组织接口 + - [ ] 25.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 25.1 扩展organization.schema.ts - 定义创建组织Schema + - [ ] 25.2 扩展organization.response.ts - 定义创建组织响应格式 + - [ ] 25.3 扩展organization.service.ts - 根据当前单个接口doc.md实现创建组织业务逻辑 + - [ ] 25.4 扩展organization.controller.ts - 实现创建组织路由 + - [ ] 25.5 扩展organization.test.md - 编写创建组织测试用例文档 + +- [ ] 26.0 PUT /organizations/{id} - 更新组织接口 + - [ ] 26.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 26.1 扩展organization.schema.ts - 定义更新组织Schema + - [ ] 26.2 扩展organization.response.ts - 定义更新组织响应格式 + - [ ] 26.3 扩展organization.service.ts - 根据当前单个接口doc.md实现更新组织业务逻辑 + - [ ] 26.4 扩展organization.controller.ts - 实现更新组织路由 + - [ ] 26.5 扩展organization.test.md - 编写更新组织测试用例文档 + +- [ ] 27.0 DELETE /organizations/{id} - 删除组织接口 + - [ ] 27.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 27.1 扩展organization.schema.ts - 定义删除组织Schema + - [ ] 27.2 扩展organization.response.ts - 定义删除组织响应格式 + - [ ] 27.3 扩展organization.service.ts - 根据当前单个接口doc.md实现删除组织业务逻辑 + - [ ] 27.4 扩展organization.controller.ts - 实现删除组织路由 + - [ ] 27.5 扩展organization.test.md - 编写删除组织测试用例文档 + +- [ ] 28.0 POST /users/{id}/organizations - 用户组织关系接口 + - [ ] 28.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 28.1 扩展user.schema.ts - 定义用户组织关系Schema + - [ ] 28.2 扩展user.response.ts - 定义用户组织关系响应格式 + - [ ] 28.3 扩展user.service.ts - 根据当前单个接口doc.md实现用户组织关系业务逻辑 + - [ ] 28.4 扩展user.controller.ts - 实现用户组织关系路由 + - [ ] 28.5 扩展user.test.md - 编写用户组织关系测试用例文档 + +### 🗂️ 系统基础模块 (System Module) - P1优先级 + +- [ ] 29.0 字典类型管理 - CRUD /dict-types + - [ ] 29.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 29.1 创建dict.schema.ts - 定义字典类型Schema + - [ ] 29.2 创建dict.response.ts - 定义字典类型响应格式 + - [ ] 29.3 创建dict.service.ts - 根据当前单个接口doc.md实现字典类型业务逻辑 + - [ ] 29.4 创建dict.controller.ts - 实现字典类型路由 + - [ ] 29.5 创建dict.test.md - 编写字典类型测试用例文档 + +- [ ] 30.0 字典项管理 - CRUD /dict-items + - [ ] 30.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 30.1 扩展dict.schema.ts - 定义字典项Schema + - [ ] 30.2 扩展dict.response.ts - 定义字典项响应格式 + - [ ] 30.3 扩展dict.service.ts - 根据当前单个接口doc.md实现字典项业务逻辑 + - [ ] 30.4 扩展dict.controller.ts - 实现字典项路由 + - [ ] 30.5 扩展dict.test.md - 编写字典项测试用例文档 + +- [ ] 31.0 标签管理 - CRUD /tags + - [ ] 31.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 31.1 创建tag.schema.ts - 定义标签Schema + - [ ] 31.2 创建tag.response.ts - 定义标签响应格式 + - [ ] 31.3 创建tag.service.ts - 根据当前单个接口doc.md实现标签业务逻辑 + - [ ] 31.4 创建tag.controller.ts - 实现标签路由 + - [ ] 31.5 创建tag.test.md - 编写标签测试用例文档 + +- [ ] 32.0 操作日志 - GET /logs/operations + - [ ] 32.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 32.1 创建log.schema.ts - 定义操作日志Schema + - [ ] 32.2 创建log.response.ts - 定义操作日志响应格式 + - [ ] 32.3 创建log.service.ts - 根据当前单个接口doc.md实现操作日志业务逻辑 + - [ ] 32.4 创建log.controller.ts - 实现操作日志路由 + - [ ] 32.5 创建log.test.md - 编写操作日志测试用例文档 + +- [ ] 33.0 登录日志 - GET /logs/logins + - [ ] 33.0 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 33.1 扩展log.schema.ts - 定义登录日志Schema + - [ ] 33.2 扩展log.response.ts - 定义登录日志响应格式 + - [ ] 33.3 扩展log.service.ts - 根据当前单个接口doc.md实现登录日志业务逻辑 + - [ ] 33.4 扩展log.controller.ts - 实现登录日志路由 + - [ ] 33.5 扩展log.test.md - 编写登录日志测试用例文档 + +### 🔧 基础设施完善 + +- [ ] 34.0 JWT认证中间件 + - [ ] Before 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 34.1 创建JWT认证插件 + - [ ] 34.2 实现Token黑名单管理 + - [ ] 34.3 实现RefreshToken机制 + - [ ] 34.4 集成权限验证中间件 + - [ ] 34.5 编写认证中间件测试 + +- [ ] 35.0 路由模块集成 + - [ ] Before 整理当前接口业务逻辑,写入当前单个接口doc.md,后续步骤按照此逻辑进行 + - [ ] 35.1 更新src/modules/index.ts - 集成所有模块 + - [ ] 35.2 更新src/app.ts - 注册所有路由 + - [ ] 35.3 更新Swagger标签定义 + - [ ] 35.4 完善API文档 + - [ ] 35.5 集成测试验证 + +## 开发优先级说明 + +### 第一阶段(P0):基础认证和用户管理 + +- **认证模块**:用户注册、激活、登录、刷新、退出(任务1-5) +- **用户管理模块**:当前用户、用户列表、用户CRUD、密码管理(任务9-14) +- **完成目标**:具备基本的用户认证和管理功能 + +### 第二阶段(P0):角色权限系统 + +- **角色管理**:角色CRUD、权限分配(任务17-20) +- **权限管理**:权限列表、权限分配、用户角色分配(任务21-23) +- **完成目标**:具备完整的RBAC权限控制体系 + +### 第三阶段(P1):扩展功能 + +- **密码管理**:找回密码、重置密码(任务6-7) +- **验证码系统**:图形验证码(任务8) +- **用户扩展**:用户详情、批量操作(任务15-16) +- **组织架构**:组织管理、用户组织关系(任务24-28) + +### 第四阶段(P1-P2):系统完善 + +- **系统基础**:字典、标签、日志管理(任务29-33) +- **基础设施**:JWT中间件、路由集成(任务34-35) +- **完成目标**:系统功能完整,可投入生产使用 + +## 备注说明 + +1. **已完成部分**:用户模块的Schema、Response、Service已基本完成,可直接使用 +2. **验证码集成**:现有captcha模块可直接集成到认证流程中 +3. **开发规范**:严格按照Elysia开发规范,每个接口都要有完整的5个子任务 +4. **测试要求**:每个模块都要有完整的测试用例文档,确保功能正确性 +5. **优先级管理**:按照P0 > P1 > P2的顺序开发,确保核心功能优先完成 diff --git a/tasks/字典模块开发计划.md b/tasks/字典模块开发计划.md new file mode 100644 index 0000000..1f0b2ac --- /dev/null +++ b/tasks/字典模块开发计划.md @@ -0,0 +1,196 @@ +# 字典模块开发计划 + +## 重新设计的字典表结构 + +**表名**: `sys_dict` + +**字段设计**: + +```sql +CREATE TABLE `sys_dict` ( + `id` BIGINT NOT NULL COMMENT '主键ID', + `code` VARCHAR(50) NOT NULL COMMENT '字典代码,唯一标识', + `name` VARCHAR(100) NOT NULL COMMENT '字典名称', + `value` VARCHAR(200) NULL COMMENT '字典值(叶子节点才有值)', + `description` VARCHAR(500) NULL COMMENT '字典描述', + `icon` VARCHAR(100) NULL COMMENT '图标(CSS类名或图标路径)', + `pid` BIGINT NULL DEFAULT 0 COMMENT '父级ID,0表示顶级', + `level` INT NOT NULL DEFAULT 1 COMMENT '层级深度,1为顶级', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序号', + `status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态:active-启用,inactive-禁用', + `is_system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否系统字典', + `color` VARCHAR(20) NULL COMMENT '颜色标识', + `extra` JSON NULL COMMENT '扩展字段', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_pid` (`pid`), + KEY `idx_level` (`level`), + KEY `idx_status` (`status`), + KEY `idx_sort` (`sort_order`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统字典表'; +``` + +## 字典管理模块接口清单 + +1. 创建字典项 + +- 默认pid为0 +- 字典项不允许同名 +- 等级深度不能超过10 +- 默认为系统字典 + +2. 获取字典项内容 + +- 根据code或id查找字典项 + +3. 获取完整字典树 + +- 获取完整的字典树 +- 可根据status、is_system等条件过滤 + +4. 获取指定字典树 + +- 通过code或id或pid查找字典树 +- 可根据status、is_system等条件过滤 + +5. 更新字典项内容 + +- 通过id修改字典内容,可选参数修改,不是传递全部参数,至少包括一个参数 + +6. 字典项排序 + +- 拖动排序 + +7. 删除字典项 + +- 软删除,防止依赖找不到 + +## 缓存机制 + +- 提供方法将整个字典写入redis缓存,后续接口查询先从缓存找 + +## 相关文件 (Relevant Files) + +- `src/eneities/sysDict.ts` - 字典表实体定义 +- `src/modules/dict/dict.docs.md` - 字典模块业务逻辑文档 +- `src/modules/dict/dict.schema.ts` - 字典模块Schema定义 +- `src/modules/dict/dict.response.ts` - 字典模块响应格式定义 +- `src/modules/dict/dict.service.ts` - 字典模块业务逻辑 +- `src/modules/dict/dict.controller.ts` - 字典模块路由控制器 +- `src/modules/dict/dict.test.md` - 字典模块测试用例文档 +- `src/modules/dict/dict.test.ts` - 字典模块单元测试 +- `src/constants/swaggerTags.ts` - 更新Swagger标签定义 + +### 备注 (Notes) + +- 单元测试通常应放置在与它们测试的代码文件相同的目录中 +- 字典模块需要支持树形结构的CRUD操作 +- 需要实现Redis缓存机制提高查询性能 +- 需要处理软删除逻辑,避免数据依赖问题 + +## 开发计划 + +### 阶段1:数据库实体和基础架构 + +- [x] 1.0 创建字典表实体 + - [x] 1.1 ~~创建 `src/eneities/sysDict.ts` 文件,定义字典表实体结构~~ + - [x] 1.2 更新 `src/eneities/index.ts` 导出字典实体 + - [x] 1.3 创建数据库迁移文件,执行字典表创建 + +### 阶段2:字典模块核心接口开发 + +- [ ] 2.0 创建字典项接口 (POST /api/dict) + - [ ] 2.1 生成接口业务逻辑文档,写入 `dict.docs.md` + - [ ] 2.2 创建 `dict.schema.ts` - 定义创建字典项Schema + - [ ] 2.3 创建 `dict.response.ts` - 定义创建字典项响应格式 + - [ ] 2.4 创建 `dict.service.ts` - 实现创建字典项业务逻辑 + - [ ] 2.5 创建 `dict.controller.ts` - 实现创建字典项路由 + - [ ] 2.6 创建 `dict.test.md` - 编写创建字典项测试用例 + +- [ ] 3.0 获取字典项内容接口 (GET /api/dict/:id) + - [ ] 3.1 更新 `dict.docs.md` - 添加获取字典项业务逻辑 + - [ ] 3.2 扩展 `dict.schema.ts` - 定义获取字典项Schema + - [ ] 3.3 扩展 `dict.response.ts` - 定义获取字典项响应格式 + - [ ] 3.4 扩展 `dict.service.ts` - 实现获取字典项业务逻辑 + - [ ] 3.5 扩展 `dict.controller.ts` - 实现获取字典项路由 + - [ ] 3.6 更新 `dict.test.md` - 添加获取字典项测试用例 + +- [ ] 4.0 获取完整字典树接口 (GET /api/dict/tree) + - [ ] 4.1 更新 `dict.docs.md` - 添加获取完整字典树业务逻辑 + - [ ] 4.2 扩展 `dict.schema.ts` - 定义获取字典树查询Schema + - [ ] 4.3 扩展 `dict.response.ts` - 定义字典树响应格式 + - [ ] 4.4 扩展 `dict.service.ts` - 实现获取完整字典树业务逻辑 + - [ ] 4.5 扩展 `dict.controller.ts` - 实现获取完整字典树路由 + - [ ] 4.6 更新 `dict.test.md` - 添加获取完整字典树测试用例 + +- [ ] 5.0 获取指定字典树接口 (GET /api/dict/tree/:code) + - [ ] 5.1 更新 `dict.docs.md` - 添加获取指定字典树业务逻辑 + - [ ] 5.2 扩展 `dict.schema.ts` - 定义获取指定字典树Schema + - [ ] 5.3 扩展 `dict.response.ts` - 定义指定字典树响应格式 + - [ ] 5.4 扩展 `dict.service.ts` - 实现获取指定字典树业务逻辑 + - [ ] 5.5 扩展 `dict.controller.ts` - 实现获取指定字典树路由 + - [ ] 5.6 更新 `dict.test.md` - 添加获取指定字典树测试用例 + +### 阶段3:字典管理接口开发 + +- [ ] 6.0 更新字典项内容接口 (PUT /api/dict/:id) + - [ ] 6.1 更新 `dict.docs.md` - 添加更新字典项业务逻辑 + - [ ] 6.2 扩展 `dict.schema.ts` - 定义更新字典项Schema + - [ ] 6.3 扩展 `dict.response.ts` - 定义更新字典项响应格式 + - [ ] 6.4 扩展 `dict.service.ts` - 实现更新字典项业务逻辑 + - [ ] 6.5 扩展 `dict.controller.ts` - 实现更新字典项路由 + - [ ] 6.6 更新 `dict.test.md` - 添加更新字典项测试用例 + +- [ ] 7.0 字典项排序接口 (PUT /api/dict/sort) + - [ ] 7.1 更新 `dict.docs.md` - 添加字典项排序业务逻辑 + - [ ] 7.2 扩展 `dict.schema.ts` - 定义字典项排序Schema + - [ ] 7.3 扩展 `dict.response.ts` - 定义字典项排序响应格式 + - [ ] 7.4 扩展 `dict.service.ts` - 实现字典项排序业务逻辑 + - [ ] 7.5 扩展 `dict.controller.ts` - 实现字典项排序路由 + - [ ] 7.6 更新 `dict.test.md` - 添加字典项排序测试用例 + +- [ ] 8.0 删除字典项接口 (DELETE /api/dict/:id) + - [ ] 8.1 更新 `dict.docs.md` - 添加删除字典项业务逻辑(软删除) + - [ ] 8.2 扩展 `dict.schema.ts` - 定义删除字典项Schema + - [ ] 8.3 扩展 `dict.response.ts` - 定义删除字典项响应格式 + - [ ] 8.4 扩展 `dict.service.ts` - 实现删除字典项业务逻辑 + - [ ] 8.5 扩展 `dict.controller.ts` - 实现删除字典项路由 + - [ ] 8.6 更新 `dict.test.md` - 添加删除字典项测试用例 + +### 阶段4:缓存机制和优化 + +- [ ] 9.0 Redis缓存机制实现 + - [ ] 9.1 更新 `dict.docs.md` - 添加缓存机制说明 + - [ ] 9.2 扩展 `dict.service.ts` - 实现Redis缓存写入方法 + - [ ] 9.3 扩展 `dict.service.ts` - 实现Redis缓存读取方法 + - [ ] 9.4 扩展 `dict.service.ts` - 实现缓存失效机制 + - [ ] 9.5 更新所有查询接口,优先从缓存读取 + - [ ] 9.6 更新 `dict.test.md` - 添加缓存机制测试用例 + +### 阶段5:集成和测试 + +- [ ] 10.0 模块集成和配置 + - [ ] 10.1 更新 `src/modules/index.ts` - 导出字典模块 + - [ ] 10.2 更新 `src/constants/swaggerTags.ts` - 添加字典模块标签 + - [ ] 10.3 更新主应用文件,注册字典模块路由 + - [ ] 10.4 创建 `dict.test.ts` - 实现单元测试 + - [ ] 10.5 进行端到端测试验证 + +### 阶段6:文档和部署 + +- [ ] 11.0 文档完善和部署准备 + - [ ] 11.1 完善API文档,确保Swagger正确显示 + - [ ] 11.2 创建字典模块使用说明文档 + - [ ] 11.3 进行性能测试和优化 + - [ ] 11.4 准备部署脚本和配置 + - [ ] 11.5 最终测试和验证 + +## 注意事项 + +- **每个接口的子任务执行完成后,需要停下来等待用户确认进行下一步输入,才能进入下一步,有可能需要对AI生成的内容作出修改** +- 所有接口都需要遵循项目的编码规范和错误处理机制 +- 字典模块需要支持树形结构的递归查询和操作 +- 缓存机制需要考虑数据一致性和失效策略 +- 软删除机制需要确保不影响现有业务逻辑 diff --git a/tsconfig.json b/tsconfig.json index c00a3a6..e676c27 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,7 @@ { "compilerOptions": { // Environment setup & latest features - "lib": [ - "ESNext" - ], + "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", @@ -31,22 +29,12 @@ "outDir": "dist", "rootDir": "src", "paths": { - "@/*": [ - "src/*" - ], - "@package.json": [ - "package.json" - ] + "@/*": ["src/*"], + "@package.json": ["package.json"] }, "resolveJsonModule": true, "allowSyntheticDefaultImports": true }, - "include": [ - "src", - "types/**/*.d.ts" -, "drizzle.config.ts" ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file + "include": ["src", "types/**/*.d.ts", "drizzle.config.ts"], + "exclude": ["node_modules", "dist"] +}