From 7b4dffecac9171d9b7c0d31ac59df195c29c571d Mon Sep 17 00:00:00 2001 From: expressgy Date: Tue, 6 May 2025 10:28:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=97=E5=85=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SQL/schema.ts | 20 +-- src/entities/schema.js | 8 +- src/plugins/const/index.js | 2 + src/plugins/database/index.js | 8 +- src/routes/dict.route.js | 22 +++ src/routes/index.js | 42 +---- src/routes/module.route.js | 2 +- src/routes/user.route.js | 6 +- src/schema/atomSchema.js | 82 ++++++++- src/schema/dict.schema.js | 213 ++++++++++++++++++++++ src/schema/module.schema.js | 4 + src/schema/user.schema.js | 6 +- src/services/dict/dict.db.js | 249 ++++++++++++++++++++++++++ src/services/dict/dict.service.js | 162 +++++++++++++++++ src/services/module/module.db.js | 9 +- src/services/module/module.service.js | 5 +- src/utils/ajv/index.js | 3 +- src/utils/ajv/method.js | 35 +++- yuheng.sql | 7 +- 19 files changed, 813 insertions(+), 72 deletions(-) create mode 100644 src/routes/dict.route.js create mode 100644 src/schema/dict.schema.js create mode 100644 src/services/dict/dict.db.js create mode 100644 src/services/dict/dict.service.js diff --git a/SQL/schema.ts b/SQL/schema.ts index 5d8f415..03da5c5 100644 --- a/SQL/schema.ts +++ b/SQL/schema.ts @@ -1,11 +1,11 @@ -import { mysqlTable, mysqlSchema, AnyMySqlColumn, primaryKey, unique, bigint, int, tinyint, varchar, datetime } from "drizzle-orm/mysql-core" +import { mysqlTable, mysqlSchema, AnyMySqlColumn, primaryKey, unique, int, varchar, bigint, datetime, tinyint } from "drizzle-orm/mysql-core" import { sql } from "drizzle-orm" export const sysDict = mysqlTable("sys_dict", { - id: bigint({ mode: "number" }).notNull(), + id: int().autoincrement().notNull(), version: int().default(0).notNull(), - pid: bigint({ mode: "number" }).notNull(), - module: tinyint(), + pid: int().notNull(), + moduleId: int("module_Id"), dictKey: varchar("dict_key", { length: 255 }), value: varchar({ length: 255 }), description: varchar({ length: 255 }), @@ -36,7 +36,6 @@ export const sysModule = mysqlTable("sys_module", { }, (table) => [ primaryKey({ columns: [table.id], name: "sys_module_id"}), - unique("uniq_name").on(table.name), unique("uniq_module_key").on(table.moduleKey), ]); @@ -98,8 +97,8 @@ export const sysPermission = mysqlTable("sys_permission", { }, (table) => [ primaryKey({ columns: [table.permId], name: "sys_permission_perm_id"}), - unique("uniq_pid_name").on(table.permName, table.pid), unique("uniq_perm_key").on(table.permKey), + unique("uniq_pid_name").on(table.permName, table.pid), ]); export const sysProfile = mysqlTable("sys_profile", { @@ -184,6 +183,7 @@ export const sysRole = mysqlTable("sys_role", { export const sysUser = mysqlTable("sys_user", { userId: bigint("user_id", { mode: "number" }).notNull(), pid: bigint({ mode: "number" }).notNull(), + nickname: varchar({ length: 255 }).notNull(), username: varchar({ length: 255 }).notNull(), email: varchar({ length: 255 }).notNull(), phone: varchar({ length: 255 }), @@ -197,8 +197,8 @@ export const sysUser = mysqlTable("sys_user", { }, (table) => [ primaryKey({ columns: [table.userId], name: "sys_user_user_id"}), - unique("uniq_username").on(table.username), unique("uniq_email").on(table.email), + unique("uniq_username").on(table.username), ]); export const sysUserAuth = mysqlTable("sys_user_auth", { @@ -212,7 +212,7 @@ export const sysUserAuth = mysqlTable("sys_user_auth", { ]); export const sysUserAuthHistory = mysqlTable("sys_user_auth_history", { - id: bigint({ mode: "number" }).autoincrement().notNull(), + id: bigint({ mode: "number" }).notNull(), userId: bigint("user_id", { mode: "number" }).notNull(), passwordHash: varchar("password_hash", { length: 255 }).notNull(), modifiedAt: datetime("modified_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(), @@ -242,12 +242,12 @@ export const sysUserFieldDefinition = mysqlTable("sys_user_field_definition", { }, (table) => [ primaryKey({ columns: [table.fieldId], name: "sys_user_field_definition_field_id"}), - unique("uniq_field_name").on(table.fieldName), unique("uniq_field_key").on(table.fieldKey), + unique("uniq_field_name").on(table.fieldName), ]); export const sysUserFieldValue = mysqlTable("sys_user_field_value", { - id: bigint({ mode: "number" }).autoincrement().notNull(), + id: bigint({ mode: "number" }).notNull(), userId: bigint("user_id", { mode: "number" }).notNull(), fieldId: int("field_id").notNull(), value: varchar({ length: 4096 }), diff --git a/src/entities/schema.js b/src/entities/schema.js index f20feff..54068b2 100644 --- a/src/entities/schema.js +++ b/src/entities/schema.js @@ -5,10 +5,10 @@ import { mysqlTable, primaryKey, unique, int, tinyint, varchar, datetime } from import { sql } from "drizzle-orm" export const sysDict = mysqlTable("sys_dict", { - id: bigint({ mode: "number" }).notNull(), + id: int().autoincrement().notNull(), version: int().default(0).notNull(), - pid: bigint({ mode: "number" }).notNull(), - module: tinyint(), + pid: int().notNull(), + moduleId: int('module_id', { mode: "number" }).notNull(), dictKey: varchar("dict_key", { length: 255 }), value: varchar({ length: 255 }), description: varchar({ length: 255 }), @@ -39,7 +39,6 @@ export const sysModule = mysqlTable("sys_module", { }, (table) => [ primaryKey({ columns: [table.id], name: "sys_module_id"}), - unique("uniq_name").on(table.name), unique("uniq_module_key").on(table.moduleKey), ]); @@ -187,6 +186,7 @@ export const sysRole = mysqlTable("sys_role", { export const sysUser = mysqlTable("sys_user", { userId: bigint("user_id", { mode: "number" }).notNull(), pid: bigint({ mode: "number" }).notNull(), + nickname: varchar({ length: 255 }).notNull(), username: varchar({ length: 255 }).notNull(), email: varchar({ length: 255 }).notNull(), phone: varchar({ length: 255 }), diff --git a/src/plugins/const/index.js b/src/plugins/const/index.js index 604c497..649b915 100644 --- a/src/plugins/const/index.js +++ b/src/plugins/const/index.js @@ -14,6 +14,8 @@ async function constData(fastify, options) { CREATE_MODULE: 'CREATE_MODULE:', // 编辑模块 UPDATE_MODULE: 'UPDATE_MODULE:', + // 新增字典 + CREATE_DICT: 'CREATE_DICT:', } } diff --git a/src/plugins/database/index.js b/src/plugins/database/index.js index a5bf813..128d112 100644 --- a/src/plugins/database/index.js +++ b/src/plugins/database/index.js @@ -32,7 +32,13 @@ async function database(fastify, options) { // } }); // 暴露数据库 - const db = drizzle(pool); + const db = drizzle(pool, { + logger: { + logQuery: (query, params) => { + fastify.log.info(`SQL: ${query} - Params: ${JSON.stringify(params)}`); + } + } + }); // 新增获取所有表名方法 const [result] = await db.execute(sql`SHOW TABLES`); const tableList = result.map(row => Object.values(row)[0]); diff --git a/src/routes/dict.route.js b/src/routes/dict.route.js new file mode 100644 index 0000000..9804d52 --- /dev/null +++ b/src/routes/dict.route.js @@ -0,0 +1,22 @@ +import { dictListSchema, dictInfoSchema, dictTreeSchema, dictCreateSchema, dictUpdateSchema, dictDeleteSchema } from "#schema/dict.schema"; +import { dictListService, dictInfoService, dictTreeService, dictCreateService, dictUpdateService, dictDeleteService } from "#services/dict/dict.service"; +export default async function moduleRoute(fastify, options) { + // 1. 通过PID获取字典列表 25-03/27 + fastify.get('/', { schema: dictListSchema }, dictListService); + + // 2. 获取字典详情 25-03/27 + fastify.get('/:id', { schema: dictInfoSchema }, dictInfoService); + + // 3. 获取字典树 25-03/27 + fastify.get('/tree/:id', dictTreeService); + + // 4. 添加字典 25-03/27 + fastify.post('/', { schema: dictCreateSchema }, dictCreateService); + + // 5. 修改字典 25-03/27 + fastify.patch('/:id', { schema: dictUpdateSchema }, dictUpdateService); + + // 6. 删除字典 25-03/27 + fastify.delete('/:id', { schema: dictDeleteSchema }, dictDeleteService); + +} \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index 80e6fc6..4c3dbe1 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -2,6 +2,7 @@ import { testSchema } from '#src/schema/test.schema'; import { testService } from '#src/services/test.service'; import userRoute from '#routes/user.route'; import moduleRoute from '#routes/module.route'; +import dictRoute from '#routes/dict.route'; export default async function routes(fastify, options) { // 定义一个GET请求的路由,路径为根路径 @@ -19,45 +20,8 @@ export default async function routes(fastify, options) { // 注册子路由 -------- 角色 - fastify.route({ - method: 'POST', - url: '/login', - schema: { - tags: ['认证体系'], // 接口分组 - summary: '用户登录', - description: '使用用户名密码进行身份认证', - security: [{}], - body: { - type: 'object', - required: ['username', 'password'], - properties: { - username: { type: 'string', default: 'user1' }, - password: { type: 'string', default: '123456' }, - }, - }, - response: { - 200: { - description: '登录成功', - type: 'object', - properties: { - token: { type: 'string' }, - expire: { type: 'number' }, - }, - }, - 401: { - description: '认证失败', - type: 'object', - properties: { - code: { type: 'number', enum: [401] }, - error: { type: 'string' }, - }, - }, - }, - }, - handler: async (req, reply) => { - // ...业务逻辑... - }, - }); + // 注册子路由 -------- 字典 + fastify.register(dictRoute, { prefix: '/dict' }); // 输出路由树 fastify.ready(() => { diff --git a/src/routes/module.route.js b/src/routes/module.route.js index c5528b0..3c9b73d 100644 --- a/src/routes/module.route.js +++ b/src/routes/module.route.js @@ -1,7 +1,7 @@ import { moduleListSchema, moduleCreateSchema, moduleUpdateSchema } from '#schema/module.schema'; import { moduleListService, moduleCreateService, moduleUpdateService } from '#services/module/module.service'; export default async function moduleRoute(fastify, options) { - // 1. 获取系统模块列表 23-04/23 + // 1. 获取系统模块列表 25-03/26 fastify.get('/', { schema: moduleListSchema }, moduleListService); // 2. 新增系统模块 diff --git a/src/routes/user.route.js b/src/routes/user.route.js index 1e407fd..6c78e6a 100644 --- a/src/routes/user.route.js +++ b/src/routes/user.route.js @@ -1,11 +1,11 @@ -import { userDefaultSchema, userRegisterSchema, userLoginSchema, userRefreshTokenSchema } from '#src/schema/user.schema'; -import { userRegisterService, userLoginService, refreshTokenService } from '#src/services/user/user.service'; +import { userDefaultSchema, userRegisterSchema, userLoginSchema, userRefreshTokenSchema } from '#schema/user.schema'; +import { userRegisterService, userLoginService, refreshTokenService } from '#services/user/user.service'; export default async function userRoute(fastify, options) { // 1. 新用户注册 25-03/25 fastify.post('/register', { schema: userRegisterSchema }, userRegisterService); // 2. 用户登录 25-03/26 fastify.post('/login', { schema: userLoginSchema }, userLoginService); - // 3. 刷新token 25-03/27 + // 3. 刷新token 25-03/26 fastify.post('/refreshToken', { schema: userRefreshTokenSchema }, refreshTokenService); } diff --git a/src/schema/atomSchema.js b/src/schema/atomSchema.js index ba05d88..a096f59 100644 --- a/src/schema/atomSchema.js +++ b/src/schema/atomSchema.js @@ -40,6 +40,17 @@ export const password = { pattern: '必须包含至少一个小写字母、一个大写字母、一个数字和一个特殊字符', }, }; +export const nickname = { + type: 'string', + minLength: 1, + maxLength: 32, + isTrim: true, + description: '昵称', + errorMessage: { + minLength: '昵称至少需要1个字符', + maxLength: '昵称不能超过32个字符', + }, +}; export const page = { type: 'integer', @@ -99,6 +110,15 @@ export const description = { default: null, description: '描述信息' } +export const pid = { + type: 'integer', + default: 0, + description: '上级ID(0表示根节点)', + errorMessage: { + type: '上级ID必须是整数', + } +} + export const module = { name: { type: 'string', @@ -118,7 +138,7 @@ export const module = { pattern: '^[a-z][a-z0-9_]*$', description: '模块唯一标识Key(英文小写)', errorMessage: { - maxLength: '模块名称不能超过255个字符', + maxLength: '模块标识不能超过255个字符', pattern: '必须小写字母开头,只能包含字母、数字和下划线' } }, @@ -131,4 +151,64 @@ export const module = { enum: '状态值只能是0(正常)或1(禁用)' } } +} + +export const dict = { + dictKey: { + type:'string', + maxLength: 255, + isLowerCase: true, + isTrim: true, + pattern: '^[a-z][a-z0-9_]*$', + description: '字典唯一标识Key(英文小写)', + errorMessage: { + maxLength: '模块标识不能超过255个字符', + pattern: '必须小写字母开头,只能包含字母、数字和下划线' + } + }, + value: { + type:'string', + maxLength: 255, + description: '字典值/名', + } +} + +export const edit = { + + pid: { + type: 'integer', + description: '上级ID(0表示根节点)', + errorMessage: { + type: '上级ID必须是整数', + }, + nullable: true + }, + moduleId: { + type: 'integer', examples: [0], default: 0, description: '模块', + errorMessage: { + type: '模块必须是整数', + } + }, + description: { + type: ['string', 'null'], + maxLength: 255, + description: '描述信息', nullable: true + }, + sort: { + + type: 'integer', + minimum: 0, + maximum: 9999, + errorMessage: '排序值范围0-9999', + nullable: true + }, + status: { + type: 'integer', + enum: [0, 1, null], + nullable: true, + errorMessage: { + type: '状态值必须是整数', + enum: '状态值只能是0(正常)或1(禁用)' + }, + } } \ No newline at end of file diff --git a/src/schema/dict.schema.js b/src/schema/dict.schema.js new file mode 100644 index 0000000..72b31da --- /dev/null +++ b/src/schema/dict.schema.js @@ -0,0 +1,213 @@ +import { sortOrder, module, sortBy, description, sort, pid, dict, edit } from "#schema/atomSchema"; +import errorAtomSchema from "#schema/error.atomSchema"; + +const dictProperties = { + id: { type: 'integer', examples: [1] }, + pid: { type: 'integer', examples: [0] }, + dictKey: { type: 'string', examples: ['gender'] }, + value: { type: 'string', examples: ['男'] }, + description: { type: ['string', 'null'] }, + sort: { type: 'integer', examples: [10] }, + status: { type: 'integer', enum: [0, 1] }, + moduleKey: { type: 'string', examples: [''] }, + moduleName: { type: 'string', examples: [''] }, + createdUser: { type: 'string', examples: [1001] }, + updatedUser: { type: 'string', examples: [1001] }, + createdBy: { type: 'string', examples: [1001] }, + updatedBy: { type: ['string', 'null'], examples: [1002] }, + createdAt: { + type: 'string', + format: 'date-time', + examples: ['2023-07-15T08:23:45.000Z'] + }, + updatedAt: { + type: 'string', + format: 'date-time', + examples: ['2023-07-16T09:34:12.000Z'] + } +} +// 通过PID获取字典列表 +export const dictListSchema = { + tags: ['系统管理'], + summary: '获取字典列表', + description: '通过PID获取字典列表(非分页)', + querystring: { + type: 'object', + required: ['pid'], + properties: { + pid: { + ...pid, + nullable: true, + }, + moduleKey: { + type: 'string', + maxLength: 255, + default: null, + description: '模块过滤', + toNull: true, + isLowerCase: true, + isTrim: true, + errorMessage: { + maxLength: '模块标识不能超过255个字符', + }, + }, + sortBy: { + ...sortBy, + default: 'sort' + }, + sortOrder + } + }, + response: { + 200: { + type: 'object', + properties: { + code: { type: 'number', enum: [200] }, + data: { + type: 'array', + items: { + type: 'object', + properties: dictProperties + } + } + } + }, + 400: errorAtomSchema['400'] + }, + security: [{ apiKey: [] }] +}; + +// 字典详情 +export const dictInfoSchema = { + tags: ['系统管理'], + summary: '获取字典详情', + response: { + 200: { + type: 'object', + properties: { + code: { type: 'number', enum: [200] }, + data: { + type: 'object', + properties: dictProperties + }, + message: { type: 'string' } + } + } + }, + security: [{ apiKey: [] }] +}; + +// 字典树schema +export const dictTreeSchema = { + tags: ['系统管理'], + summary: '获取字典树', + response: { + 200: { + ...dictListSchema.response[200], + properties: { + code: { type: 'number', enum: [200] }, + data: { + type: 'object', + properties: { + ...dictProperties, + children: { + type: 'array', + items: { $ref: '#' } + } + } + }, + message: { type: 'string' } + } + } + }, + security: [{ apiKey: [] }] +}; + +// 添加字典 +export const dictCreateSchema = { + tags: ['系统管理'], + summary: '创建字典项', + body: { + type: 'object', + required: ['pid', 'moduleId', 'dictKey', 'value'], + properties: { + pid: { + type: 'integer', examples: [0], default: 0, description: '上级字典ID(0表示根节点)', + errorMessage: { + type: '上级ID必须是整数', + } + }, + moduleId: { + type: 'integer', examples: [0], default: 0, description: '模块', + errorMessage: { + type: '模块必须是整数', + } + }, + dictKey: { ...dict.dictKey }, + value: { ...dict.value }, + description, + sort, + status: { ...module.status } + } + }, + response: { + 201: { + type: 'object', + properties: { + code: { type: 'number', enum: [201] }, + data: { + type: 'object', + properties: dictProperties + }, + message: { type: 'string' } + }, + }, + 409: errorAtomSchema['409'] + }, + security: [{ apiKey: [] }] +}; + +// 修改字典 +export const dictUpdateSchema = { + tags: ['系统管理'], + summary: '修改字典项', + body: { + type: 'object', + properties: { + dictKey: { ...dict.dictKey, nullable: true }, + value: { ...dict.value, nullable: true }, + moduleId: edit.moduleId, + description: edit.description, + sort: edit.sort, + status: edit.status, + }, + anyOf: [ + { required: ['dictKey'] }, + { required: ['value'] }, + { required: ['moduleId'] }, + { required: ['description'] }, + { required: ['sort'] }, + { required: ['status'] } + ], + additionalProperties: false + }, + // response: dictCreateSchema.response, + security: [{ apiKey: [] }] +}; + +// 删除字典 +export const dictDeleteSchema = { + tags: ['系统管理'], + summary: '删除字典项', + response: { + 204: { + type: 'object', + properties: { + code: { type: 'number', enum: [204] }, + message: { type: 'string' } + } + }, + 404: errorAtomSchema['404'] + }, + security: [{ apiKey: [] }] +}; \ No newline at end of file diff --git a/src/schema/module.schema.js b/src/schema/module.schema.js index d95e02c..0b15d44 100644 --- a/src/schema/module.schema.js +++ b/src/schema/module.schema.js @@ -75,6 +75,8 @@ export const moduleListSchema = { description: { type: ['string', 'null'], examples: ['用户权限管理模块'] }, sort: { type: 'integer', examples: [10] }, status: { type: 'integer', enum: [0, 1] }, + createdUser: { type: 'string', examples: [1001] }, + updatedUser: { type: 'string', examples: [1001] }, createdBy: { type: 'string', examples: [1001] }, updatedBy: { type: ['string', 'null'], examples: [1002] }, createdAt: { @@ -209,6 +211,8 @@ export const moduleUpdateSchema = { description: { type: ['string', 'null'], examples: ['用户权限管理模块'] }, sort: { type: 'integer', examples: [10] }, status: { type: 'integer', enum: [0, 1] }, + createdUser: { type: 'string', examples: [1001] }, + updatedUser: { type: 'string', examples: [1001] }, createdBy: { type: 'string', examples: [1001] }, updatedBy: { type: ['string', 'null'], examples: [1002] }, createdAt: { diff --git a/src/schema/user.schema.js b/src/schema/user.schema.js index 2b82c15..746c321 100644 --- a/src/schema/user.schema.js +++ b/src/schema/user.schema.js @@ -1,4 +1,4 @@ -import { email, username, password } from '#schema/atomSchema'; +import { email, username, password, nickname } from '#schema/atomSchema'; import errorAtomSchema from '#schema/error.atomSchema'; // 默认schema export const userDefaultSchema = {}; @@ -10,12 +10,13 @@ export const userRegisterSchema = { description: '创建新用户账号(需验证手机/邮箱)', body: { type: 'object', - required: ['username', 'email', 'password'], + required: ['username', 'email', 'password', 'nickname'], errorMessage: { required: { username: '缺少用户名', email: '缺少邮箱', password: '缺少密码', + nickname: '缺少昵称', }, }, properties: { @@ -33,6 +34,7 @@ export const userRegisterSchema = { type: 'object', properties: { username: { type: 'string' }, + nickname: { type: 'string' }, email: { type: 'string' }, userId: { type: 'string', diff --git a/src/services/dict/dict.db.js b/src/services/dict/dict.db.js new file mode 100644 index 0000000..2955ae0 --- /dev/null +++ b/src/services/dict/dict.db.js @@ -0,0 +1,249 @@ +import { sysUser, sysUserAuth, sysUserAuthHistory, sysModule, sysDict } from '#entity'; +import { or, eq, and, asc, desc, count, like, ne, sql } from 'drizzle-orm'; +import { alias, QueryBuilder } from 'drizzle-orm/mysql-core'; + +// 获取字典列表 +export async function getDictList(queryData) { + const { pid = 0, moduleKey, sortBy = 'sort', sortOrder = 'asc' } = queryData; + + // 联表查询构造器 + const sysCreatedUser = alias(sysUser, 'sysCreatedUser'); + const sysUpdatedUser = alias(sysUser, 'sysUpdatedUser'); + const query = this.db + .select({ + id: sysDict.id, + pid: sysDict.pid, + dictKey: sysDict.dictKey, + value: sysDict.value, + description: sysDict.description, + sort: sysDict.sort, + status: sysDict.status, + moduleKey: sysModule.moduleKey, // 新增模块Key字段 + moduledName: sysModule.name, // 新增模块名称字段 + createdUser: sysCreatedUser.nickname, + updatedUser: sysUpdatedUser.nickname, + createdBy: sysModule.createdBy, + updatedBy: sysModule.updatedBy, + createdAt: sysModule.createdAt, + updatedAt: sysModule.updatedAt + }) + .from(sysDict) + .leftJoin(sysModule, eq(sysDict.moduleId, sysModule.id)) // 通过moduleKey关联模块表 + .leftJoin(sysCreatedUser, eq(sysCreatedUser.userId, sysModule.createdBy)) + .leftJoin(sysUpdatedUser, eq(sysUpdatedUser.userId, sysModule.updatedBy)) + .$dynamic(); + + // 构建过滤条件 + const conditions = [eq(sysDict.pid, pid === null ? 0 : pid)]; + if (moduleKey) { + conditions.push(eq(sysModule.moduleKey, moduleKey)); // 根据模块Key过滤 + } + + // 应用排序规则 + const orderBy = sortOrder === 'desc' + ? desc(sysDict[sortBy]) + : asc(sysDict[sortBy]); + + return await query + .where(and(...conditions)) + .orderBy(orderBy); +} + +// 新增字典详情查询方法 +export async function getDictDetail(dictId) { + const sysCreatedUser = alias(sysUser, 'sysCreatedUser'); + const sysUpdatedUser = alias(sysUser, 'sysUpdatedUser'); + + const result = await this.db + .select({ + id: sysDict.id, + pid: sysDict.pid, + dictKey: sysDict.dictKey, + value: sysDict.value, + description: sysDict.description, + sort: sysDict.sort, + status: sysDict.status, + moduleKey: sysModule.moduleKey, + moduleName: sysModule.name, + createdUser: sysCreatedUser.nickname, + updatedUser: sysUpdatedUser.nickname, + createdAt: sysDict.createdAt, // 修正为字典表字段 + updatedAt: sysDict.updatedAt // 修正为字典表字段 + }) + .from(sysDict) + .leftJoin(sysModule, eq(sysDict.moduleId, sysModule.id)) + .leftJoin(sysCreatedUser, eq(sysCreatedUser.userId, sysDict.createdBy)) + .leftJoin(sysUpdatedUser, eq(sysUpdatedUser.userId, sysDict.updatedBy)) + .where(eq(sysDict.id, dictId)) + .limit(1); + + return result[0] || null; +} + +// 检查字典Key冲突 +export async function checkDictKeyConflict(pid, dictKey) { + const result = await this.db + .select({ count: count() }) + .from(sysDict) + .where(and( + eq(sysDict.pid, pid), + eq(sysDict.dictKey, dictKey) + )); + + return result[0].count > 0; +} +// 检查字典Key冲突(排除当前记录) +export async function checkDictKeyConflictNotMe(pid, dictKey, excludeId) { + const [result] = await this.db.select({ id: sysDict.id }) + .from(sysDict) + .where(and( + eq(sysDict.pid, pid), + eq(sysDict.dictKey, dictKey), + ne(sysDict.id, excludeId) + )); + return !!result; +} + +// 通过pid和dictKey获取字典详情 +export async function getDictByPidAndDictKey(pid, dictKey) { + const sysCreatedUser = alias(sysUser, 'sysCreatedUser'); + const sysUpdatedUser = alias(sysUser, 'sysUpdatedUser'); + + const result = await this.db + .select({ + id: sysDict.id, + pid: sysDict.pid, + dictKey: sysDict.dictKey, + value: sysDict.value, + description: sysDict.description, + sort: sysDict.sort, + status: sysDict.status, + moduleKey: sysModule.moduleKey, + moduleName: sysModule.name, + createdUser: sysCreatedUser.nickname, + updatedUser: sysUpdatedUser.nickname, + createdAt: sysDict.createdAt, // 修正为字典表字段 + updatedAt: sysDict.updatedAt // 修正为字典表字段 + }) + .from(sysDict) + .leftJoin(sysModule, eq(sysDict.moduleId, sysModule.id)) + .leftJoin(sysCreatedUser, eq(sysCreatedUser.userId, sysDict.createdBy)) + .leftJoin(sysUpdatedUser, eq(sysUpdatedUser.userId, sysDict.updatedBy)) + .where(and(eq(sysDict.pid, pid), eq(sysDict.dictKey, dictKey))) + .limit(1); + + return result[0] || null; +} + +// 新增字典 +export async function createDict(dictData) { + await this.db.insert(sysDict).values({ + pid: dictData.pid, + moduleId: dictData.moduleId, // 关联模块ID + dictKey: dictData.dictKey, + value: dictData.value, + description: dictData.description, + sort: dictData.sort || 0, + status: dictData.status || 0, + createdBy: dictData.createdBy, + }).execute(); + return await getDictByPidAndDictKey.call(this, dictData.pid, dictData.dictKey); +} + +// 更新字典数据 +export async function updateDict(id, updateData) { + await this.db.update(sysDict) + .set({ + ...Object.entries(updateData).reduce((acc, [key, value]) => { + if (value !== null && value !== undefined) { + acc[key] = value; + } + return acc; + }, {}), + updatedAt: new Date() + }) + .where(and( + eq(sysDict.id, id), + )); + + return await getDictDetail.call(this, id); +} + + +// 删除字典项 +export async function deleteDict(id) { + const [result] = await this.db.delete(sysDict) + .where(eq(sysDict.id, id)) + .execute(); + return result.affectedRows; +} + +// 获取字典树 +export async function getDictTree(dictId) { + console.log(dictId); + const sysCreatedUser = alias(sysUser, 'sysCreatedUser'); + const sysUpdatedUser = alias(sysUser, 'sysUpdatedUser'); + const dictList = { + id: sysDict.id, + pid: sysDict.pid, + dictKey: sysDict.dictKey, + value: sysDict.value, + description: sysDict.description, + sort: sysDict.sort, + status: sysDict.status, + moduleKey: sysModule.moduleKey, + moduleName: sysModule.name, + createdUser: sysCreatedUser.nickname, + updatedUser: sysUpdatedUser.nickname, + createdAt: sysDict.createdAt, // 修正为字典表字段 + updatedAt: sysDict.updatedAt // 修正为字典表字段 + } + // ! 基础层级 + const baseQueryBuilder = new QueryBuilder(); + const baseQuery = baseQueryBuilder + .select({ + ...dictList, + level: sql`0`.as('level'), + }) + .from(sysDict) + .leftJoin(sysModule, eq(sysDict.moduleId, sysModule.id)) + .leftJoin(sysCreatedUser, eq(sysCreatedUser.userId, sysDict.createdBy)) + .leftJoin(sysUpdatedUser, eq(sysUpdatedUser.userId, sysDict.updatedBy)) + .where(eq(sysDict.id, dictId)); + + // ! 递归层级 + const recursiveQueryBuilder = new QueryBuilder(); + const recursiveQuery = recursiveQueryBuilder + .select({ + ...dictList, + level: sql`dictHierarchy.level + 1`.as('level'), + }) + .from(sysDict) + .leftJoin(sysModule, eq(sysDict.moduleId, sysModule.id)) + .leftJoin(sysCreatedUser, eq(sysCreatedUser.userId, sysDict.createdBy)) + .leftJoin(sysUpdatedUser, eq(sysUpdatedUser.userId, sysDict.updatedBy)) + .innerJoin(sql`dictHierarchy`, sql`dictHierarchy.id = ${sysDict.pid}`); + + const rowName = customDrizzleRowWithRecursive(dictList); + + // ! 执行原始SQL查询 + return this.db.execute( + sql`WITH RECURSIVE dictHierarchy(${rowName}) AS(${baseQuery} UNION ALL ${recursiveQuery}) SELECT * FROM dictHierarchy`, + ); +} + +export function customDrizzleRowWithRecursive(obj) { + // ! 获取所有的列别名 + const rowNameList = [...Object.keys(obj), 'level']; + + // ! 制造drizzle专属的列名称 + const rowName = sql.empty(); + rowNameList.forEach((i, index) => { + rowName.append(sql`${sql.raw(i)}`); + if (index < rowNameList.length - 1) { + rowName.append(sql`, `); + } + }); + + return rowName; +} \ No newline at end of file diff --git a/src/services/dict/dict.service.js b/src/services/dict/dict.service.js new file mode 100644 index 0000000..95889e9 --- /dev/null +++ b/src/services/dict/dict.service.js @@ -0,0 +1,162 @@ +import { + getDictList, + getDictDetail, + checkDictKeyConflict, + createDict, + deleteDict, + checkDictKeyConflictNotMe, + updateDict, + getDictTree +} from './dict.db.js'; + +// 字典列表服务 +export async function dictListService(request) { + const data = await getDictList.call(this, request.query); + console.log('查询结果:', data); // 打印查询结果,用于调试和验证查询逻辑是否正确 + return { code: 200, data, message: '查询成功' }; +} + +// 字典详情服务 +export async function dictInfoService(request) { + const { id } = request.params; + const data = await getDictDetail.call(this, id); + + if (!data) { + return { code: 404, error: '字典不存在' }; + } + + return { + code: 200, + data: data, + message: '查询成功' + }; +} + +// 字典树服务 +export async function dictTreeService(request) { + const data = await getDictTree.call(this, request.params.id); + return { code: 200, data: data[0], message: '查询成功' }; +} + +// 创建字典服务(带分布式锁) +export async function dictCreateService(request, reply) { + const { pid, moduleId, dictKey, value, description, sort, status } = request.body; + const userId = request.user.userId; // 从JWT获取用户ID + const lockKey = `${this.const.DISTRIBUTED_LOCK_PREFIX.CREATE_DICT}_${pid}_${dictKey}`; + const lockIdentifier = this.snowflake(); + let renewInterval; + + try { + // 获取分布式锁 + const locked = await this.redis.SET(lockKey, lockIdentifier, { NX: true, EX: 5 }); + if (!locked) throw this.httpErrors.tooManyRequests('操作正在进行,请稍后重试'); + + // 启动锁续期 + renewInterval = setInterval(async () => { + if (await this.redis.GET(lockKey) === lockIdentifier) { + await this.redis.EXPIRE(lockKey, 5); + } + }, 3000); + + // 检查字典Key冲突 + const exists = await checkDictKeyConflict.call(this, pid, dictKey); + if (exists) throw this.httpErrors.conflict('当前集合下字典标识已存在'); + + // 创建新字典项 + const newDict = await createDict.call(this, { pid, moduleId, dictKey, value, description, sort, status, createdBy: userId }); // 确保 userId 正确传递给 createDict 方法; + + reply.code(201).send({ + code: 201, + data: newDict, + message: '字典创建成功' + }); + } catch (err) { + if (err.statusCode === 409) { + throw this.httpErrors.conflict(err.message); + } + throw err; + } finally { + clearInterval(renewInterval); + if (await this.redis.GET(lockKey) === lockIdentifier) { + await this.redis.DEL(lockKey); + } + } +} + + +// 更新字典服务 +export async function dictUpdateService(request) { + const { id } = request.params; + const updateData = request.body; + const userId = request.user.userId; + console.log('updateData:', updateData); // 打印更新数据,用于调试和验证更新逻辑是否正确 + + let lockKey = `${this.const.DISTRIBUTED_LOCK_PREFIX.UPDATE_DICT}${id}`; + // 检查字典是否存在 + const existing = await getDictDetail.call(this, id); + if (!existing) throw this.httpErrors.notFound('字典不存在'); + // 如果更新了字典标识,需要改变锁key + if (updateData.dictKey) { + lockKey = `${this.const.DISTRIBUTED_LOCK_PREFIX.CREATE_DICT}_${existing.pid}_${updateData.dictKey}`; + } + + const lockIdentifier = this.snowflake(); + let renewInterval; + + try { + // 获取分布式锁 + const locked = await this.redis.SET(lockKey, lockIdentifier, { NX: true, EX: 5 }); + if (!locked) throw this.httpErrors.tooManyRequests('操作正在进行,请稍后重试'); + + // 启动锁续期 + renewInterval = setInterval(async () => { + if (await this.redis.GET(lockKey) === lockIdentifier) { + await this.redis.EXPIRE(lockKey, 5); + } + }, 3000); + + // 检查字典Key冲突(如果更新了dictKey) + if (updateData.dictKey && updateData.dictKey !== existing.dictKey) { + const keyExists = await checkDictKeyConflictNotMe.call( + this, + existing.pid, + updateData.dictKey, + id + ); + if (keyExists) throw this.httpErrors.conflict('字典标识已被占用'); + } + + // 执行更新 + const updated = await updateDict.call(this, id, { + ...updateData, + updatedBy: userId, + }); + + return { + code: 200, + data: updated, + message: '字典更新成功' + }; + } catch (err) { + if (err.statusCode === 409) { + throw this.httpErrors.conflict(err.message); + } + throw err; + } finally { + clearInterval(renewInterval); + if (await this.redis.GET(lockKey) === lockIdentifier) { + await this.redis.DEL(lockKey); + } + } +} + +// 删除字典服务 +export async function dictDeleteService(request) { + const { id } = request.params; + const data = await deleteDict.call(this, id); + if(data === 0){ + return { code: 404, error: '字典不存在' }; + }else{ + return { code: 200, message: '删除成功' }; + } +} \ No newline at end of file diff --git a/src/services/module/module.db.js b/src/services/module/module.db.js index a8e311d..2bc611a 100644 --- a/src/services/module/module.db.js +++ b/src/services/module/module.db.js @@ -1,5 +1,6 @@ import { sysUser, sysUserAuth, sysUserAuthHistory, sysModule } from '#entity'; import { or, eq, and, asc, desc, count, like, ne } from 'drizzle-orm'; +import { alias } from 'drizzle-orm/mysql-core'; // 获取模块列表和分页 export async function getModuleList(queryData) { @@ -13,6 +14,8 @@ export async function getModuleList(queryData) { sortOrder = 'desc' } = queryData; this.log.info(queryData); + const sysCreatedUser = alias(sysUser, 'sysCreatedUser'); + const sysUpdatedUser = alias(sysUser, 'sysUpdatedUser'); const isGetPage = page && pageSize; const listSelect = { id: sysModule.id, @@ -25,6 +28,8 @@ export async function getModuleList(queryData) { version: sysModule.version, sort: sysModule.sort, status: sysModule.status, + createdUser: sysCreatedUser.nickname, + updatedUser: sysUpdatedUser.nickname, createdBy: sysModule.createdBy, updatedBy: sysModule.updatedBy, createdAt: sysModule.createdAt, @@ -35,6 +40,8 @@ export async function getModuleList(queryData) { let query = db .select(isGetPage ? pageSelect : listSelect) .from(sysModule) + .leftJoin(sysCreatedUser, eq(sysCreatedUser.userId, sysModule.createdBy)) + .leftJoin(sysUpdatedUser, eq(sysUpdatedUser.userId, sysModule.updatedBy)) .$dynamic(); // 应用过滤条件 const conditions = []; @@ -100,8 +107,6 @@ export async function insertModule(data) { description: data.description, sort: data.sort || 0, status: data.status || 0, - createdAt: new Date(), - updatedAt: new Date(), createdBy: data.createdBy, }).execute(); } diff --git a/src/services/module/module.service.js b/src/services/module/module.service.js index f130a16..a35f54d 100644 --- a/src/services/module/module.service.js +++ b/src/services/module/module.service.js @@ -40,7 +40,7 @@ export async function moduleCreateService(request, reply) { if (exists) throw this.httpErrors.conflict('模块标识已存在'); // 创建模块 - const newModule = await insertModule.call(this, { + await insertModule.call(this, { ...request.body, createdBy: request.user?.userId || 0 // 从JWT获取用户ID }); @@ -48,7 +48,6 @@ export async function moduleCreateService(request, reply) { reply.code(201).send({ code: 201, data: { - id: newModule.insertId, moduleKey, name }, @@ -67,7 +66,7 @@ export async function moduleCreateService(request, reply) { } } -// 新增模块修改服务 +// 模块修改服务 export async function moduleUpdateService(request, reply) { const { id } = request.params; const updateData = request.body; diff --git a/src/utils/ajv/index.js b/src/utils/ajv/index.js index 9afbbc8..d000119 100644 --- a/src/utils/ajv/index.js +++ b/src/utils/ajv/index.js @@ -1,5 +1,5 @@ import ajvErrors from 'ajv-errors'; -import { isLowerCase, isTrim } from '#utils/ajv/method'; +import { isLowerCase, isTrim, toNull } from '#utils/ajv/method'; export const ajvConfig = { customOptions: { removeAdditional: true, // 自动删除schema未定义的额外属性 @@ -12,5 +12,6 @@ export const ajvConfig = { ajvErrors, // 支持自定义错误消息 isLowerCase, // 自定义验证:强制小写(如用户名) isTrim, // 自定义验证:自动trim字符串 + toNull, // 自定义验证:如果为空字符串将其设置为null ], }; diff --git a/src/utils/ajv/method.js b/src/utils/ajv/method.js index 4796c25..252d0c9 100644 --- a/src/utils/ajv/method.js +++ b/src/utils/ajv/method.js @@ -22,7 +22,11 @@ export function isLowerCase(ajv){ errors: true, modifying: true, validate: function validateIsEven(schema, data, parentSchema, dataCxt) { - if(typeof data == 'string'){ + if(typeof data == 'string' || data == null){ + if(data == null){ + dataCxt.parentData[dataCxt.parentDataProperty] = null; + return true; + } dataCxt.parentData[dataCxt.parentDataProperty] = dataCxt.parentData[dataCxt.parentDataProperty].trim().toLowerCase(); return true; }else{ @@ -44,7 +48,11 @@ export function isTrim(ajv){ errors: true, modifying: true, validate: function validateIsEven(schema, data, parentSchema, dataCxt) { - if(typeof data == 'string'){ + if(typeof data == 'string' || data == null){ + if(data == null){ + dataCxt.parentData[dataCxt.parentDataProperty] = null; + return true; + } dataCxt.parentData[dataCxt.parentDataProperty] = dataCxt.parentData[dataCxt.parentDataProperty].trim(); return true; }else{ @@ -56,6 +64,29 @@ export function isTrim(ajv){ } }); } +// 如果为空字符串将其设置为null +export function toNull(ajv){ + ajv.addKeyword({ + keyword: 'toNull', + type: 'string', + errors: true, + modifying: true, + validate: function validateIsEven(schema, data, parentSchema, dataCxt) { + if(typeof data == 'string' || data == null){ + if(data.trim() ==''){ + dataCxt.parentData[dataCxt.parentDataProperty] = null; + return true; + } + return true; + }else{ + validateIsEven.errors = [ { + message: '参数不是字符串格式' + } ]; + return false; + } + } + }); +} // 给fastify添加自定义的参数校验规则 diff --git a/yuheng.sql b/yuheng.sql index a0a991a..518c6f0 100644 --- a/yuheng.sql +++ b/yuheng.sql @@ -1,8 +1,8 @@ CREATE TABLE `sys_dict` ( - `id` BIGINT NOT NULL COMMENT 'ID', + `id` INT NOT NULL AUTO_INCREMENT COMMENT 'ID', `version` INT NOT NULL DEFAULT 0, - `pid` BIGINT NOT NULL COMMENT '上级ID', - `module` tinyint NULL COMMENT '模块', + `pid` INT NOT NULL COMMENT '上级ID', + `module_id` tinyint NULL COMMENT '模块', `dict_key` varchar(255) NULL COMMENT '字典标识', `value` varchar(255) NULL COMMENT '字典值', `description` varchar(255) NULL COMMENT '描述', @@ -129,6 +129,7 @@ CREATE TABLE `sys_user` ( `user_id` bigint NOT NULL COMMENT '用户ID', `pid` bigint NOT NULL COMMENT '上级ID', `username` varchar(255) NOT NULL COMMENT '用户名,全小写', + `nickname` varchar(255) NOT NULL COMMENT '用户昵称', `email` varchar(255) NOT NULL COMMENT '邮箱', `phone` varchar(255) NULL COMMENT '手机号', `avatar_url` varchar(255) NULL COMMENT '图标',