This commit is contained in:
expressgy 2025-05-06 10:28:07 +08:00
parent 81953dd44c
commit 7b4dffecac
19 changed files with 813 additions and 72 deletions

View File

@ -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 }),

View File

@ -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 }),

View File

@ -14,6 +14,8 @@ async function constData(fastify, options) {
CREATE_MODULE: 'CREATE_MODULE:',
// 编辑模块
UPDATE_MODULE: 'UPDATE_MODULE:',
// 新增字典
CREATE_DICT: 'CREATE_DICT:',
}
}

View File

@ -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]);

22
src/routes/dict.route.js Normal file
View File

@ -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);
}

View File

@ -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(() => {

View File

@ -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. 新增系统模块

View File

@ -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);
}

View File

@ -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: '上级ID0表示根节点',
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: '上级ID0表示根节点',
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(禁用)'
},
}
}

213
src/schema/dict.schema.js Normal file
View File

@ -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: '上级字典ID0表示根节点',
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: [] }]
};

View File

@ -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: {

View File

@ -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',

View File

@ -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;
}

View File

@ -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: '删除成功' };
}
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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
],
};

View File

@ -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添加自定义的参数校验规则

View File

@ -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 '图标',