Compare commits

..

6 Commits

Author SHA1 Message Date
HeXiaoLong:Suanier
a8f20a70fb feat(permission): 新增获取权限详情接口(GET /api/permission/:id)相关文档与实现 2025-07-08 23:42:18 +08:00
HeXiaoLong:Suanier
64cf3415ab feat: 实现权限排序接口 (PUT /api/permission/sort)
- 新增权限排序业务逻辑文档到 permission.docs.md
- 定义权限排序请求参数 Schema (SortPermissionSchema)
- 定义权限排序响应格式 Schema (SortPermissionResponsesSchema)
- 实现权限排序业务逻辑 (sortPermissions 方法)
- 添加权限排序路由控制器

功能特性:
- 支持批量权限排序更新
- 验证权限同级别约束
- 验证排序值唯一性
- 使用分布式锁防止并发冲突
- 完整的错误处理和业务校验

接口路径: PUT /api/permission/sort
2025-07-08 22:22:12 +08:00
HeXiaoLong:Suanier
74fa306d7a feat(permission): 完成权限模块修改接口全链路实现(含分布式锁、同级重名、父子module一致、非法移动、状态可变更等校验) 2025-07-08 20:24:25 +08:00
HeXiaoLong:Suanier
24155039b6 feat(permission): 完成权限模块修改接口全链路实现(含分布式锁、同级重名、父子module一致、非法移动、状态可变更等校验) 2025-07-08 17:30:30 +08:00
HeXiaoLong:Suanier
a78824ab31 feat(permission): 完成权限模块修改接口全链路实现(含分布式锁、同级重名、父子module一致、非法移动、状态可变更等校验) 2025-07-08 17:02:49 +08:00
HeXiaoLong:Suanier
384213714c feat(permission): 新增权限模块新增/删除接口全链路实现(含分布式锁、唯一性/层级/引用校验、schema、响应、controller、docs) 2025-07-08 16:42:57 +08:00
12 changed files with 3110 additions and 707 deletions

View File

@ -1,2 +1,3 @@
import { relations } from 'drizzle-orm/relations';
import {} from './schema';
import { relations } from "drizzle-orm/relations";
import { } from "./schema";

View File

@ -1,325 +1,248 @@
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';
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 sysDict = mysqlTable(
'sys_dict',
{
id: bigint({ mode: 'number' }).autoincrement().notNull(),
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' }),
pid: bigint({ mode: "number" }),
level: int().default(1).notNull(),
sortOrder: int('sort_order').default(0).notNull(),
sortOrder: int("sort_order").default(0).notNull(),
status: varchar({ length: 20 }).default('active').notNull(),
isSystem: tinyint('is_system').default(0).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),
],
);
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 sysOperationLogs = mysqlTable(
'sys_operation_logs',
{
id: bigint({ mode: 'number' }).notNull(),
userId: bigint('user_id', { mode: 'number' }),
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'),
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' }),
],
);
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(),
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 }),
fullName: varchar("full_name", { length: 200 }),
description: text(),
pid: bigint({ mode: 'number' }),
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' }),
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' }),
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),
],
);
},
(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 sysPermission = mysqlTable("sys_permission", {
id: bigint({ mode: "number" }).notNull(),
pid: bigint({ mode: "number" }),
level: tinyint().default(1).notNull(),
permissionKey: varchar("permission_key", { length: 100 }).notNull(),
name: varchar({ length: 50 }).notNull(),
description: varchar({ length: 255 }).default(''),
type: tinyint().default(1).notNull(),
apiPathKey: varchar("api_path_key", { length: 200 }),
pagePathKey: varchar("page_path_key", { length: 200 }),
module: varchar({ length: 30 }).notNull(),
sort: bigint({ mode: "number" }),
icon: varchar({ length: 100 }),
status: tinyint().default(1),
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`),
updatedAt: datetime("updated_at", { mode: 'string'}),
},
(table) => [
index("idx_level").on(table.level),
index("idx_module").on(table.module),
index("idx_parent_sort").on(table.pid, table.sort),
index("idx_pid").on(table.pid),
index("idx_time").on(table.createdAt),
index("idx_type").on(table.type),
primaryKey({ columns: [table.id], name: "sys_permission_id"}),
unique("uniq_permission_key").on(table.permissionKey),
]);
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(),
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' }),
pid: bigint({ mode: "number" }),
path: varchar({ length: 500 }),
level: int().default(1).notNull(),
sortOrder: int('sort_order').default(0).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'),
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' }),
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),
],
);
},
(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(),
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),
],
);
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(),
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),
],
);
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(),
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(),
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
avatar: varchar({ length: 255 }),
nickname: varchar({ length: 100 }),
status: varchar({ length: 20 }).default('active').notNull(),
@ -327,33 +250,28 @@ export const sysUsers = mysqlTable(
// 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(),
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' }),
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),
],
);
},
(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),
]);

View File

@ -1,323 +1,249 @@
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';
import { mysqlTable, mysqlSchema, AnyMySqlColumn, index, primaryKey, unique, varchar, int, json, timestamp, text, datetime, tinyint, date, } from "drizzle-orm/mysql-core"
import { bigintString as bigint } from "./customType"
import { sql } from "drizzle-orm"
export const sysDict = mysqlTable(
'sys_dict',
{
id: bigint({ mode: 'number' }).notNull(),
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' }),
pid: bigint({ mode: "number" }),
level: int().default(1).notNull(),
sortOrder: int('sort_order').default(0).notNull(),
sortOrder: int("sort_order").default(0).notNull(),
status: varchar({ length: 20 }).default('active').notNull(),
isSystem: tinyint('is_system').default(0).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),
],
);
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 sysOperationLogs = mysqlTable(
'sys_operation_logs',
{
id: bigint({ mode: 'number' }).notNull(),
userId: bigint('user_id', { mode: 'number' }),
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'),
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' }),
],
);
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(),
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 }),
fullName: varchar("full_name", { length: 200 }),
description: text(),
pid: bigint({ mode: 'number' }),
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' }),
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' }),
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),
],
);
},
(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 sysPermission = mysqlTable("sys_permission", {
id: bigint({ mode: "number" }).notNull(),
pid: bigint({ mode: "number" }),
level: tinyint().default(1).notNull(),
permissionKey: varchar("permission_key", { length: 100 }).notNull(),
name: varchar({ length: 50 }).notNull(),
description: varchar({ length: 255 }).default(''),
type: tinyint().default(1).notNull(),
apiPathKey: varchar("api_path_key", { length: 200 }),
pagePathKey: varchar("page_path_key", { length: 200 }),
module: varchar({ length: 30 }).notNull(),
sort: bigint({ mode: "number" }),
icon: varchar({ length: 100 }),
status: tinyint().default(1),
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`),
updatedAt: datetime("updated_at", { mode: 'string'}),
},
(table) => [
index("idx_level").on(table.level),
index("idx_module").on(table.module),
index("idx_parent_sort").on(table.pid, table.sort),
index("idx_pid").on(table.pid),
index("idx_time").on(table.createdAt),
index("idx_type").on(table.type),
primaryKey({ columns: [table.id], name: "sys_permission_id"}),
unique("uniq_permission_key").on(table.permissionKey),
]);
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(),
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' }),
pid: bigint({ mode: "number" }),
path: varchar({ length: 500 }),
level: int().default(1).notNull(),
sortOrder: int('sort_order').default(0).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'),
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' }),
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),
],
);
},
(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(),
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),
],
);
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(),
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),
],
);
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(),
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(),
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
avatar: varchar({ length: 255 }),
nickname: varchar({ length: 100 }),
status: varchar({ length: 20 }).default('active').notNull(),
@ -325,33 +251,28 @@ export const sysUsers = mysqlTable(
// 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(),
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' }),
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),
],
);
},
(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),
]);

View File

@ -331,14 +331,14 @@ export type UpdateDictSuccessType = Static<(typeof UpdateDictResponsesSchema)[20
* @description Controller中定义所有可能的响应格式
*/
export const SortDictResponsesSchema = {
200: responseWrapperSchema(t.Null(), {
200: responseWrapperSchema(t.Null({
description: '排序成功',
examples: {
code: 0,
message: '字典项排序成功',
data: null,
},
}),
})),
400: responseWrapperSchema(
t.Object({
error: t.String({
@ -373,14 +373,16 @@ export type SortDictSuccessType = Static<(typeof SortDictResponsesSchema)[200]>;
* @description Controller中定义所有可能的响应格式
*/
export const DeleteDictResponsesSchema = {
200: responseWrapperSchema(t.Null(), {
200: responseWrapperSchema(t.Null(
{
description: '删除成功',
examples: {
code: 0,
message: '字典项删除成功',
data: null,
},
}),
}
)),
400: responseWrapperSchema(
t.Object({
error: t.String({

View File

@ -14,6 +14,7 @@ import { testController } from './test/test.controller';
import { captchaController } from './captcha/captcha.controller';
import { authController } from './auth/auth.controller';
import { dictController } from './dict/dict.controller';
import { permissionController } from './permission/permission.controller';
/**
* - API
@ -38,4 +39,6 @@ export const controllers = new Elysia({
// 验证码接口
.group('/captcha', (app) => app.use(captchaController))
// 字典接口
.group('/dict', (app) => app.use(dictController));
.group('/dict', (app) => app.use(dictController))
// 权限接口
.group('/permission', (app) => app.use(permissionController));

View File

@ -0,0 +1,139 @@
/**
* @file Controller
* @author AI Assistant
* @date 2024-12-19
* @lastEditor AI Assistant
* @lastEditTime 2025-01-07
* @description
*/
import { Elysia } from 'elysia';
import { CreatePermissionSchema, GetPermissionTreeByPidParamsSchema } from './permission.schema';
import { CreatePermissionResponsesSchema } from './permission.response';
import { DeletePermissionParamsSchema } from './permission.schema';
import { DeletePermissionResponsesSchema } from './permission.response';
import { UpdatePermissionParamsSchema, UpdatePermissionBodySchema } from './permission.schema';
import { UpdatePermissionResponsesSchema } from './permission.response';
import { permissionService } from './permission.service';
import { tags } from '@/constants/swaggerTags';
import { GetPermissionTreeQuerySchema, SortPermissionSchema } from './permission.schema';
import { GetPermissionTreeResponsesSchema, SortPermissionResponsesSchema } from './permission.response';
import { GetPermissionDetailParamsSchema } from './permission.schema';
import { GetPermissionDetailResponsesSchema } from './permission.response';
export const permissionController = new Elysia()
/**
*
* @route POST /api/permission
* @description
*/
.post(
'/',
async ({ body }) => permissionService.createPermission(body),
{
body: CreatePermissionSchema,
detail: {
summary: '新增权限',
description: '创建新的权限项支持树形结构需校验唯一性、层级、父子module一致性等',
tags: [tags.permission],
operationId: 'createPermission',
},
response: CreatePermissionResponsesSchema,
}
)
/**
*
* @route DELETE /api/permission/:id
* @description
*/
.delete(
'/:id',
async ({ params }) => permissionService.deletePermission(params.id),
{
params: DeletePermissionParamsSchema,
detail: {
summary: '删除权限',
description: '删除指定权限,会检查子权限和角色引用关系,采用逻辑删除',
tags: [tags.permission],
operationId: 'deletePermission',
},
response: DeletePermissionResponsesSchema,
}
)
/**
*
* @route PUT /api/permission/:id
* @description module一致
*/
.put(
'/:id',
async ({ params, body }) => permissionService.updatePermission(params.id, body),
{
params: UpdatePermissionParamsSchema,
body: UpdatePermissionBodySchema,
detail: {
summary: '修改权限',
description: '修改指定权限支持字段变更、同级重名、父子module一致、非法移动、状态可变更等校验',
tags: [tags.permission],
operationId: 'updatePermission',
},
response: UpdatePermissionResponsesSchema,
}
)
/**
*
* @route GET /api/permission/tree
* @description
*/
.get(
'/tree',
async ({ query }) => permissionService.getPermissionTree(query),
{
query: GetPermissionTreeQuerySchema,
detail: {
summary: '查看权限完整树',
description: '查询权限完整树支持module、status、type多条件筛选',
tags: [tags.permission],
operationId: 'getPermissionTree',
},
response: GetPermissionTreeResponsesSchema,
}
)
/**
*
* @route PUT /api/permission/sort
* @description
*/
.put(
'/sort',
async ({ body }) => permissionService.sortPermissions(body),
{
body: SortPermissionSchema,
detail: {
summary: '权限排序',
description: '批量更新权限排序,要求所有权限必须在同一级别,排序值不能重复',
tags: [tags.permission],
operationId: 'sortPermissions',
},
response: SortPermissionResponsesSchema,
}
)
/**
*
* @route GET /api/permission/:id
* @description
*/
.get(
'/:id',
async ({ params }) => permissionService.getPermissionDetail(params.id),
{
params: GetPermissionDetailParamsSchema,
detail: {
summary: '获取权限详情',
description: '查询指定权限的详细信息包括权限名称、类型、状态、module、排序等',
tags: [tags.permission],
operationId: 'getPermissionDetail',
},
response: GetPermissionDetailResponsesSchema,
}
);

View File

@ -0,0 +1,800 @@
# 权限模块业务逻辑文档
## 模块概述
权限模块提供基于RBAC基于角色的访问控制的权限管理功能支持树形结构的权限数据CRUD操作。权限数据用于控制用户对系统资源的访问权限包括菜单、按钮、接口和数据权限。
## 数据库设计
### 表结构sys_permission
```sql
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY COMMENT '主键ID',
pid BIGINT DEFAULT 0 COMMENT '父权限ID',
level TINYINT NOT NULL DEFAULT 1 COMMENT '权限层级',
permission_key VARCHAR(100) NOT NULL COMMENT '权限标识',
name VARCHAR(50) NOT NULL COMMENT '权限名称',
description VARCHAR(255) DEFAULT '' COMMENT '权限描述',
type TINYINT NOT NULL DEFAULT 1 COMMENT '权限类型(1=菜单 2=按钮 3=接口 4=数据)',
api_path_key VARCHAR(200) DEFAULT NULL COMMENT '接口路径标识',
page_path_key VARCHAR(200) DEFAULT NULL COMMENT '前端路由标识',
module VARCHAR(30) NOT NULL COMMENT '所属模块',
sort BIGINT DEFAULT 0 COMMENT '排序值',
icon VARCHAR(100) DEFAULT NULL COMMENT '图标标识',
status TINYINT DEFAULT 1 COMMENT '状态(1=启用, 0禁用)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
/* 唯一约束 */
UNIQUE KEY uniq_permission_key (permission_key),
/* 单列索引 */
KEY idx_pid (pid),
KEY idx_level (level),
KEY idx_type (type),
KEY idx_module (module),
KEY idx_time (created_at),
/* 复合索引 */
KEY idx_parent_sort (pid, sort)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='RBAC权限表';
```
## 接口设计
### 1. 新增权限接口 (POST /api/permission)
#### 业务逻辑
1. **参数验证**
- 验证必填字段permission_key、name、type、module
- 验证permission_key唯一性全局唯一
- 验证name在同级下的唯一性
- 验证pid的有效性如果指定了父级
- 验证level深度不超过10层
- 验证type的有效性1=菜单 2=按钮 3=接口 4=数据)
- 验证module格式字母数字下划线长度1-30
2. **业务规则**
- 默认pid为0顶级权限
- 默认level为1顶级
- 默认status为1启用
- 默认sort_order为0
- 如果有父级level = 父级level + 1
- 如果指定了父级需要验证父级是否存在且状态为1
- 如果存在父权限module必须与父权限一致
- 同级权限允许重名但permission_key必须唯一
3. **数据处理**
- permission_key转换为小写并去除两端空格
- name去除两端空格
- description去除两端空格
- api_path_key去除两端空格如果提供
- page_path_key去除两端空格如果提供
- module转换为小写并去除两端空格
- icon去除两端空格如果提供
- 自动计算level如果指定了pid
- 自动生成sort_order同级最大sort_order + 1
4. **错误处理**
- permission_key已存在409 Conflict
- name在同级下已存在409 Conflict
- 父级不存在404 Not Found
- 父级状态非启用400 Bad Request
- level超过10层400 Bad Request
- type值无效400 Bad Request
- module格式错误400 Bad Request
- 父权限module不一致400 Bad Request
#### 性能考虑
1. **数据库索引**
- permission_key字段有唯一索引查询快速
- pid字段有索引支持父子关系查询
- level字段有索引支持层级查询
- module字段有索引支持模块筛选
2. **并发控制**
- 使用分布式锁防止permission_key重复创建
- 使用数据库事务确保数据一致性
- 使用乐观锁防止并发更新冲突
#### 业务流程图
```
开始
验证必填参数
检查permission_key唯一性
验证父权限(如果指定)
检查module一致性
计算level和sort_order
数据预处理
保存到数据库
返回成功响应
```
#### 数据验证规则
1. **permission_key**
- 必填
- 长度1-100字符
- 格式:字母、数字、下划线
- 全局唯一
2. **name**
- 必填
- 长度1-50字符
- 同级下唯一
3. **type**
- 必填
- 值1菜单、2按钮、3接口、4数据
4. **module**
- 必填
- 长度1-30字符
- 格式:字母、数字、下划线
- 与父权限一致(如果存在父权限)
5. **pid**
- 可选
- 默认0
- 必须指向存在的权限
6. **api_path_key**
- 可选
- 长度0-200字符
- type为3接口时建议填写
7. **page_path_key**
- 可选
- 长度0-200字符
- type为1菜单时建议填写
#### 响应示例
**成功响应 (200)**
```json
{
"code": 200,
"message": "权限创建成功",
"data": {
"id": "123456789",
"permission_key": "user:create",
"name": "创建用户",
"description": "创建新用户的权限",
"type": 3,
"module": "user",
"pid": "0",
"level": 1,
"sort_order": 1,
"status": 1,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
```
**错误响应 (409)**
```json
{
"code": 409,
"message": "权限标识已存在",
"data": null
}
```
#### 测试用例
1. **正常创建权限**
- 输入:有效的权限数据
- 预期:创建成功,返回权限信息
2. **重复permission_key**
- 输入已存在的permission_key
- 预期返回409错误
3. **无效父权限**
- 输入不存在的pid
- 预期返回404错误
4. **模块不一致**
- 输入父权限module与子权限不一致
- 预期返回400错误
5. **层级超限**
- 输入level超过10层
- 预期返回400错误
#### 注意事项
1. **性能优化**
- 使用批量操作减少数据库访问
- 合理使用缓存减少查询压力
- 优化树形结构构建算法
2. **数据一致性**
- 使用事务确保数据完整性
- 及时更新相关缓存
- 处理并发冲突
3. **安全性**
- 严格验证输入参数
- 记录所有操作日志
- 防止权限提升攻击
4. **可维护性**
- 代码结构清晰
- 错误处理完善
- 文档详细完整
---
### 2. 删除权限接口 (DELETE /api/permission/:id)
#### 业务逻辑
1. **参数验证**
- 验证id格式bigint字符串
- 验证id是否存在
2. **业务规则**
- 只允许删除状态为启用status=1的权限
- 检查是否有子权限pid=当前id有则不允许删除
- 检查是否被角色、用户等引用(如有,需提示或禁止删除,具体视业务需求)
- 逻辑删除将status设为0禁用不物理删除
- 记录删除操作日志
3. **错误处理**
- 权限不存在404 Not Found
- 权限状态非启用400 Bad Request
- 存在子权限400 Bad Request
- 被引用不可删除400 Bad Request
#### 性能与安全考虑
- 子权限、引用检查需加索引优化
- 删除操作需加分布式锁防止并发冲突
- 需管理员权限
- 防止越权删除
#### 缓存策略
- 删除后清除相关权限缓存、权限树缓存、模块权限缓存
#### 响应示例
**成功响应 (200)**
```json
{
"code": 200,
"message": "权限删除成功",
"data": null
}
```
**错误响应 (400/404)**
```json
{
"code": 400,
"message": "存在子权限,无法删除",
"data": null
}
```
#### 测试用例
1. **正常删除**
- 输入无子权限、未被引用的权限id
- 预期:删除成功
2. **有子权限**
- 输入有子权限的权限id
- 预期返回400错误
3. **权限不存在**
- 输入不存在的id
- 预期返回404错误
4. **权限已禁用**
- 输入status=0的权限id
- 预期返回400错误
5. **被引用**
- 输入:被角色/用户引用的权限id
- 预期返回400错误
---
### 3. 修改权限接口 (PUT /api/permission/:id)
#### 业务逻辑
1. **参数验证**
- 验证id格式bigint字符串
- 验证id是否存在
- 校验可修改字段如name、description、type、apiPathKey、pagePathKey、module、sort、icon、status等
- name同级唯一性校验如有修改
- type、module等字段格式校验
2. **业务规则**
- 只允许修改状态为启用status=1的权限
- 存在父权限时module必须与父权限一致
- 同级允许重名但permissionKey必须唯一
- 不允许修改permissionKey如需支持请说明
- 不允许将权限移动到自身或其子节点下
- 修改type、module等需考虑子权限影响
- 记录修改操作日志
3. **错误处理**
- 权限不存在404 Not Found
- 权限状态非启用400 Bad Request
- name同级下已存在409 Conflict
- 父权限module不一致400 Bad Request
- 非法移动400 Bad Request
#### 性能与安全考虑
- name、module、type等字段加索引优化
- 修改操作需加分布式锁防止并发冲突
- 需管理员权限
- 防止越权修改
#### 缓存策略
- 修改后清除相关权限缓存、权限树缓存、模块权限缓存
#### 响应示例
**成功响应 (200)**
```json
{
"code": 200,
"message": "权限修改成功",
"data": {
"id": "123456789",
"permission_key": "user:create",
"name": "创建用户",
"description": "创建新用户的权限",
"type": 3,
"module": "user",
"pid": "0",
"level": 1,
"sort": 1,
"icon": null,
"status": 1,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:40:00Z"
}
}
```
**错误响应 (400/404/409)**
```json
{
"code": 409,
"message": "同级下已存在该名称",
"data": null
}
```
#### 测试用例
1. **正常修改**
- 输入有效的权限id和修改字段
- 预期:修改成功,返回新数据
2. **同级重名**
- 输入同级下已存在的name
- 预期返回409错误
3. **权限不存在**
- 输入不存在的id
- 预期返回404错误
4. **状态非启用**
- 输入status=0的权限id
- 预期返回400错误
5. **父权限module不一致**
- 输入module与父权限不一致
- 预期返回400错误
6. **非法移动**
- 输入pid为自身或子节点
- 预期返回400错误
---
### 4. 查看权限完整树接口 (GET /api/permission/tree)
#### 业务逻辑
1. **参数校验**
- 支持可选参数module模块、status状态、type权限类型
- module字符串筛选所属模块
- status数字或字符串1=启用0=禁用,默认全部
- type数字或字符串1=菜单2=按钮3=接口4=数据,默认全部
2. **业务规则**
- 查询所有符合条件的权限数据
- 按level和sort排序
- 构建树形结构(递归或循环)
- 只返回未被禁用或根据status参数权限
- 支持多模块、多类型、多状态筛选
3. **错误处理**
- 参数格式错误400 Bad Request
- 查询异常500 Internal Server Error
#### 性能与安全考虑
- 查询前加索引优化module、status、type、pid、sort
- 构建树结构时避免N+1查询建议一次性查全量后内存组装
- 支持缓存如Redis提升高频查询性能
- 需管理员或有权限用户访问
#### 缓存策略
- 支持按module、status、type等条件缓存完整树结构
- 查询后写入缓存,更新/删除/新增权限时清理相关缓存
#### 响应示例
**成功响应 (200)**
```json
{
"code": 200,
"message": "查询成功",
"data": [
{
"id": "1",
"permission_key": "system",
"name": "系统管理",
"type": 1,
"module": "system",
"pid": "0",
"level": 1,
"sort": 1,
"status": 1,
"children": [
{
"id": "2",
"permission_key": "user",
"name": "用户管理",
"type": 1,
"module": "system",
"pid": "1",
"level": 2,
"sort": 1,
"status": 1,
"children": []
}
]
}
]
}
```
#### 测试用例
1. **正常查询**
- 输入:无参数
- 预期:返回完整权限树
2. **按模块筛选**
- 输入module=system
- 预期只返回system模块的权限树
3. **按状态筛选**
- 输入status=1
- 预期:只返回启用权限
4. **按类型筛选**
- 输入type=1
- 预期:只返回菜单类型权限
5. **参数错误**
- 输入非法status/type/module
- 预期返回400错误
---
### 5. 权限排序接口 (PUT /api/permission/sort)
#### 业务逻辑
1. **参数验证**
- 接收权限ID和排序值的数组格式`[{id: string, sort: string}]`
- 验证ID格式bigint字符串
- 验证sort格式数字字符串
- 验证数组不为空最多处理100条记录
- 验证所有权限ID的唯一性数组内不能重复
2. **业务规则**
- 权限ID必须存在且状态为启用
- 只能对同级权限进行排序所有权限必须有相同的pid
- 同级权限的sort值不能重复
- 排序值必须为非负整数
- 支持批量更新多个权限的排序值
- 事务中执行,要么全部成功要么全部失败
3. **数据处理**
- 验证所有权限是否存在且状态正常
- 检查所有权限是否在同一级别相同pid
- 检查sort值的有效性和唯一性
- 批量更新权限的sort字段
- 记录操作日志
4. **错误处理**
- 权限不存在404 Not Found
- 权限状态非启用400 Bad Request
- 权限不在同一级别400 Bad Request
- sort值重复409 Conflict
- sort值格式错误400 Bad Request
- 数组为空或超过限制400 Bad Request
- 权限ID重复400 Bad Request
#### 性能考虑
1. **数据库优化**
- 批量查询权限信息减少数据库访问
- 使用事务确保数据一致性
- 利用pid+sort复合索引提升查询性能
- 批量更新减少数据库交互次数
2. **并发控制**
- 使用分布式锁防止并发排序冲突
- 锁定范围相同pid下的所有权限
- 锁定时间10秒TTL
#### 安全考虑
- 需要管理员权限或特定的权限排序权限
- 防止越权操作其他模块的权限
- 记录操作日志便于审计
#### 缓存策略
- 排序后清除权限树缓存
- 清除相关模块的权限缓存
- 更新权限变更时间戳
#### 业务流程
```
开始
验证参数格式和数量
获取分布式锁
查询所有权限是否存在
验证权限状态和级别
检查sort值唯一性
开启数据库事务
批量更新sort值
提交事务
释放分布式锁
清除缓存
返回成功响应
```
#### 请求示例
```json
{
"permissions": [
{"id": "123", "sort": "1"},
{"id": "124", "sort": "2"},
{"id": "125", "sort": "3"}
]
}
```
#### 响应示例
**成功响应 (200)**
```json
{
"code": 200,
"message": "权限排序更新成功",
"data": {
"updated_count": 3,
"updated_permissions": [
{
"id": "123",
"name": "用户管理",
"sort": "1"
},
{
"id": "124",
"name": "角色管理",
"sort": "2"
},
{
"id": "125",
"name": "权限管理",
"sort": "3"
}
]
}
}
```
**错误响应 (400)**
```json
{
"code": 400,
"message": "权限不在同一级别",
"data": null
}
```
**错误响应 (409)**
```json
{
"code": 409,
"message": "排序值重复",
"data": {
"duplicate_sort": "2",
"duplicate_ids": ["124", "125"]
}
}
```
#### 测试用例
1. **正常排序**
- 输入:同级权限的有效排序数据
- 预期:排序更新成功
2. **权限不存在**
- 输入包含不存在权限ID的数据
- 预期返回404错误
3. **权限状态非启用**
- 输入:包含禁用权限的数据
- 预期返回400错误
4. **权限不在同一级别**
- 输入不同pid的权限混合
- 预期返回400错误
5. **sort值重复**
- 输入相同sort值的权限
- 预期返回409错误
6. **数组为空**
- 输入:空数组
- 预期返回400错误
7. **超过限制**
- 输入超过100条的数据
- 预期返回400错误
8. **权限ID重复**
- 输入重复的权限ID
- 预期返回400错误
#### 注意事项
1. **排序逻辑**
- 前端通常通过拖拽操作触发排序
- 需要考虑拖拽后相邻权限的sort值调整
- 建议前端计算好新的排序值再提交
2. **性能优化**
- 避免频繁的排序操作
- 考虑防抖处理,用户操作完成后统一提交
- 大批量权限排序时考虑分页处理
3. **用户体验**
- 排序操作应该有loading状态
- 失败时回滚前端显示状态
- 成功后实时更新权限树显示
---
### 7. 获取权限详情接口 (GET /api/permission/:id)
#### 业务逻辑
1. **接口用途**
- 根据权限ID获取单个权限的详细信息。
- 用于权限管理页面的详情查看、编辑前数据回显等场景。
2. **参数验证**
- 验证id格式bigint字符串前端需以字符串传递防止精度丢失
- 验证id是否存在。
3. **业务规则**
- 仅查询单条权限数据,不包含子权限。
- 返回所有主要字段(见响应结构)。
- 若权限不存在返回404。
- 若权限被禁用status=0仍可查询详情。
4. **数据处理**
- 查询sys_permission表按id精确查找。
- 字段类型转换bigint字段以字符串返回。
- 返回完整的权限信息包括id、pid、level、permission_key、name、description、type、api_path_key、page_path_key、module、sort、icon、status、created_at、updated_at。
5. **错误处理**
- 权限不存在404 Not Found
- id格式错误400 Bad Request
6. **安全性**
- 需具备权限管理相关权限方可访问。
- 记录操作日志。
7. **性能考虑**
- id字段为主键查询效率高。
- 单条数据查询,无性能瓶颈。
#### 响应结构
**成功响应 (200)**
```json
{
"code": 200,
"message": "获取权限详情成功",
"data": {
"id": "123456789",
"pid": "0",
"level": 1,
"permission_key": "user:create",
"name": "创建用户",
"description": "创建新用户的权限",
"type": 3,
"api_path_key": "/api/user/create",
"page_path_key": null,
"module": "user",
"sort": 1,
"icon": null,
"status": 1,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
```
**错误响应 (404)**
```json
{
"code": 404,
"message": "权限不存在",
"data": null
}
```
**错误响应 (400)**
```json
{
"code": 400,
"message": "参数格式错误",
"data": null
}
```
#### 测试用例
1. **正常获取权限详情**
- 输入存在的权限ID
- 预期:返回对应权限的详细信息
2. **权限不存在**
- 输入不存在的权限ID
- 预期返回404错误
3. **ID格式错误**
- 输入非bigint格式的ID
- 预期返回400错误
#### 注意事项
- 前端传递ID需为字符串防止大数精度丢失。
- 查询结果不包含子权限,仅为单条详情。
- 可用于详情页、编辑页的数据回显。
- 需做好权限校验,防止越权访问。

View File

@ -0,0 +1,366 @@
/**
* @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 CreatePermissionSuccessSchema = t.Object({
id: t.String({
description: '权限IDbigint类型以字符串返回防止精度丢失',
examples: ['1', '2', '100'],
}),
permissionKey: t.String({
description: '权限标识',
examples: ['user:create', 'user:read', 'user:update'],
}),
name: t.String({
description: '权限名称',
examples: ['创建用户', '查看用户', '修改用户'],
}),
description: t.Optional(
t.String({
description: '权限描述',
examples: ['创建新用户的权限', '查看用户信息的权限'],
}),
),
type: t.Number({
description: '权限类型1=菜单2=按钮3=接口4=数据',
examples: [1, 2, 3, 4],
}),
apiPathKey: t.Optional(
t.String({
description: '接口路径标识',
examples: ['/api/user', '/api/user/create', 'POST:/api/user'],
}),
),
pagePathKey: t.Optional(
t.String({
description: '前端路由标识',
examples: ['/user', '/user/list', '/user/detail'],
}),
),
module: t.String({
description: '所属模块',
examples: ['user', 'role', 'permission', 'system'],
}),
pid: t.String({
description: '父权限IDbigint类型以字符串返回',
examples: ['0', '1', '2'],
}),
level: t.Number({
description: '权限层级',
examples: [1, 2, 3],
}),
sort: t.String({
description: '排序值,同级内排序使用',
examples: ['0','1', '10', '100'],
}),
icon: t.Optional(
t.String({
description: '图标标识',
examples: ['icon-user', 'icon-add', '/icons/user.png'],
}),
),
status: t.Number({
description: '权限状态1=启用0=禁用',
examples: [1, 0],
}),
createdAt: t.String({
description: '创建时间',
examples: ['2024-12-19T10:30:00Z'],
}),
updatedAt: t.String({
description: '更新时间',
examples: ['2024-12-19T10:30:00Z'],
}),
});
/**
*
* @description Controller中定义所有可能的响应格式
*/
export const CreatePermissionResponsesSchema = {
200: responseWrapperSchema(CreatePermissionSuccessSchema),
409: responseWrapperSchema(
t.Object({
error: t.String({
description: '唯一性冲突',
examples: ['权限标识已存在', '权限名称在同级下已存在'],
}),
}),
),
400: responseWrapperSchema(
t.Object({
error: t.String({
description: '参数错误',
examples: ['参数校验失败', '父级权限状态非启用', '权限层级超过限制', '权限类型无效', '模块格式错误', '父权限模块不一致'],
}),
}),
),
404: responseWrapperSchema(
t.Object({
error: t.String({
description: '资源不存在',
examples: ['父级权限不存在'],
}),
}),
),
403: responseWrapperSchema(
t.Object({
error: t.String({
description: '权限不足',
examples: ['权限不足', '需要管理员权限'],
}),
}),
),
500: responseWrapperSchema(
t.Object({
error: t.String({
description: '服务器错误',
examples: ['内部服务器错误'],
}),
}),
),
};
/** 新增权限成功响应数据类型 */
export type CreatePermissionSuccessType = Static<(typeof CreatePermissionResponsesSchema)[200]>;
/**
*
* @description Controller中定义所有可能的响应格式
*/
export const DeletePermissionResponsesSchema = {
200: responseWrapperSchema(t.Object({})),
400: responseWrapperSchema(
t.Object({
error: t.String({
description: '请求错误',
examples: ['存在子权限,无法删除', '权限状态非启用', '被引用不可删除'],
}),
}),
),
404: responseWrapperSchema(
t.Object({
error: t.String({
description: '资源不存在',
examples: ['权限不存在'],
}),
}),
),
};
/** 删除权限成功响应类型 */
export type DeletePermissionSuccessType = Static<(typeof DeletePermissionResponsesSchema)[200]>;
/**
*
* @description Controller中定义所有可能的响应格式
*/
export const UpdatePermissionResponsesSchema = {
200: responseWrapperSchema(CreatePermissionSuccessSchema),
400: responseWrapperSchema(
t.Object({
error: t.String({
description: '请求错误',
examples: ['权限状态非启用', '父权限module不一致', '非法移动'],
}),
}),
),
404: responseWrapperSchema(
t.Object({
error: t.String({
description: '资源不存在',
examples: ['权限不存在'],
}),
}),
),
409: responseWrapperSchema(
t.Object({
error: t.String({
description: '唯一性冲突',
examples: ['同级下已存在该名称'],
}),
}),
),
};
/** 修改权限成功响应类型 */
export type UpdatePermissionSuccessType = Static<(typeof UpdatePermissionResponsesSchema)[200]>;
/**
* Schema
* @description children为子权限数组
*/
export const PermissionTreeItemSchema = t.Recursive((self) =>
t.Object({
id: t.String({ description: '权限IDbigint字符串', examples: ['1', '2', '100'] }),
permissionKey: t.String({ description: '权限标识', examples: ['user:create'] }),
name: t.String({ description: '权限名称', examples: ['创建用户'] }),
description: t.Optional(t.String({ description: '权限描述', examples: ['创建新用户的权限'] })),
type: t.Number({ description: '权限类型1=菜单2=按钮3=接口4=数据', examples: [1, 2, 3, 4] }),
apiPathKey: t.Optional(t.String({ description: '接口路径标识', examples: ['/api/user'] })),
pagePathKey: t.Optional(t.String({ description: '前端路由标识', examples: ['/user'] })),
module: t.String({ description: '所属模块', examples: ['user'] }),
pid: t.String({ description: '父权限IDbigint字符串', examples: ['0', '1'] }),
level: t.Number({ description: '权限层级', examples: [1, 2, 3] }),
sort: t.String({ description: '排序值', examples: ['0', '1', '10'] }),
icon: t.Optional(t.String({ description: '图标标识', examples: ['icon-user'] })),
status: t.Number({ description: '权限状态1=启用0=禁用', examples: [1, 0] }),
createdAt: t.String({ description: '创建时间', examples: ['2024-12-19T10:30:00Z'] }),
updatedAt: t.String({ description: '更新时间', examples: ['2024-12-19T10:30:00Z'] }),
children: t.Optional(t.Array(self)),
})
);
/**
*
* @description Controller中定义所有可能的响应格式
*/
export const GetPermissionTreeResponsesSchema = {
200: responseWrapperSchema(t.Array(PermissionTreeItemSchema)),
400: responseWrapperSchema(t.Object({ error: t.String({ description: '请求错误', examples: ['参数错误'] }) })),
500: responseWrapperSchema(t.Object({ error: t.String({ description: '服务器错误', examples: ['内部服务器错误'] }) })),
};
/** 查看权限完整树成功响应类型 */
export type GetPermissionTreeSuccessType = Static<typeof GetPermissionTreeResponsesSchema[200]>;
/**
* Schema
* @description
*/
export const SortedPermissionItemSchema = t.Object({
/** 权限ID */
id: t.String({
description: '权限IDbigint字符串',
examples: ['123', '124', '125'],
}),
/** 权限名称 */
name: t.String({
description: '权限名称',
examples: ['用户管理', '角色管理', '权限管理'],
}),
/** 排序值 */
sort: t.String({
description: '更新后的排序值',
examples: ['1', '2', '3'],
}),
});
/**
* Schema
* @description
*/
export const SortPermissionSuccessSchema = t.Object({
/** 更新的权限数量 */
updated_count: t.Number({
description: '成功更新的权限数量',
examples: [3, 5, 10],
}),
/** 更新的权限列表 */
updated_permissions: t.Array(SortedPermissionItemSchema, {
description: '成功更新的权限列表',
}),
});
/**
*
* @description Controller中定义所有可能的响应格式
*/
export const SortPermissionResponsesSchema = {
200: responseWrapperSchema(SortPermissionSuccessSchema),
400: responseWrapperSchema(
t.Object({
error: t.String({
description: '请求错误',
examples: [
'权限不在同一级别',
'sort值格式错误',
'数组为空或超过限制',
'权限ID重复',
'权限状态非启用'
],
}),
}),
),
404: responseWrapperSchema(
t.Object({
error: t.String({
description: '权限不存在',
examples: ['权限不存在'],
}),
}),
),
409: responseWrapperSchema(
t.Object({
error: t.String({
description: '排序值冲突',
examples: ['排序值重复'],
}),
duplicate_sort: t.Optional(t.String({
description: '重复的排序值',
examples: ['2'],
})),
duplicate_ids: t.Optional(t.Array(t.String(), {
description: '使用重复排序值的权限ID列表',
examples: [['124', '125']],
})),
}),
),
403: responseWrapperSchema(
t.Object({
error: t.String({
description: '权限不足',
examples: ['权限不足', '需要管理员权限'],
}),
}),
),
500: responseWrapperSchema(
t.Object({
error: t.String({
description: '服务器错误',
examples: ['内部服务器错误', '数据库操作失败'],
}),
}),
),
};
/** 权限排序成功响应数据类型 */
export type SortPermissionSuccessType = Static<typeof SortPermissionResponsesSchema[200]>;
/**
*
* @description Controller中定义所有可能的响应格式
*/
export const GetPermissionDetailResponsesSchema = {
200: responseWrapperSchema(CreatePermissionSuccessSchema),
404: responseWrapperSchema(
t.Object({
error: t.String({
description: '资源不存在',
examples: ['权限不存在'],
}),
}),
),
400: responseWrapperSchema(
t.Object({
error: t.String({
description: '参数错误',
examples: ['参数格式错误'],
}),
}),
),
};
/** 获取权限详情成功响应数据类型 */
export type GetPermissionDetailSuccessType = Static<typeof GetPermissionDetailResponsesSchema[200]>;

View File

@ -0,0 +1,403 @@
/**
* @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 CreatePermissionSchema = t.Object({
/** 权限标识,全局唯一 */
permissionKey: t
.Transform(
t.String({
minLength: 1,
maxLength: 100,
pattern: '^[a-zA-Z0-9_]+$',
description: '权限标识,全局唯一,只允许字母数字下划线,自动转换为小写并去除两端空格',
examples: ['user:create', 'user:read', 'user:update', 'user:delete'],
}),
)
.Decode((value: string) => value.trim().toLowerCase())
.Encode((value: string) => value),
/** 权限名称 */
name: t
.Transform(
t.String({
minLength: 1,
maxLength: 50,
description: '权限名称,同级下唯一,自动去除两端空格',
examples: ['创建用户', '查看用户', '修改用户', '删除用户'],
}),
)
.Decode((value: string) => value.trim())
.Encode((value: string) => value),
/** 权限描述 */
description: t.Optional(
t.Transform(
t.String({
maxLength: 255,
description: '权限描述,自动去除两端空格',
examples: ['创建新用户的权限', '查看用户信息的权限'],
}),
)
.Decode((value: string) => value.trim())
.Encode((value: string) => value),
),
/** 权限类型(1=菜单 2=按钮 3=接口 4=数据) */
type: t.Union([
t.Literal(1),
t.Literal(2),
t.Literal(3),
t.Literal(4),
t.Literal('1'),
t.Literal('2'),
t.Literal('3'),
t.Literal('4'),
], {
description: '权限类型1=菜单2=按钮3=接口4=数据',
examples: [1, 2, 3, 4],
}),
/** 接口路径标识 */
apiPathKey: t.Optional(
t.Transform(
t.String({
maxLength: 200,
description: '接口路径标识type为3接口时建议填写自动去除两端空格',
examples: ['/api/user', '/api/user/create', 'POST:/api/user'],
}),
)
.Decode((value: string) => value.trim())
.Encode((value: string) => value),
),
/** 前端路由标识 */
pagePathKey: t.Optional(
t.Transform(
t.String({
maxLength: 200,
description: '前端路由标识type为1菜单时建议填写自动去除两端空格',
examples: ['/user', '/user/list', '/user/detail'],
}),
)
.Decode((value: string) => value.trim())
.Encode((value: string) => value),
),
/** 所属模块 */
module: t
.Transform(
t.String({
minLength: 1,
maxLength: 30,
pattern: '^[a-zA-Z0-9_]+$',
description: '所属模块,只允许字母数字下划线,自动转换为小写并去除两端空格',
examples: ['user', 'role', 'permission', 'system'],
}),
)
.Decode((value: string) => value.trim().toLowerCase())
.Encode((value: string) => value),
/** 父权限ID0表示顶级 */
pid: t.Optional(
t.Union([
t.Literal('0'),
t.String({
pattern: '^[1-9]\\d*$',
description: '父权限IDBigint字符串形式',
}),
], {
description: '父权限ID0表示顶级权限',
examples: ['0', '1', '2'],
default: '0',
}),
),
/** 排序值 */
sort: t.Optional(
t.String({
maxLength: 20,
description: '排序值,同级内排序使用',
examples: ['0','1', '10', '100'],
})
),
/** 图标标识 */
icon: t.Optional(
t.Transform(
t.String({
maxLength: 100,
description: '图标标识CSS类名或图标路径自动去除两端空格',
examples: ['icon-user', 'icon-add', '/icons/user.png'],
}),
)
.Decode((value: string) => value.trim())
.Encode((value: string) => value),
),
/** 状态(1=启用, 0=禁用) */
status: t.Optional(
t.Union([
t.Literal(1),
t.Literal(0),
t.Literal('1'),
t.Literal('0'),
], {
description: '权限状态1=启用0=禁用',
examples: [1, 0],
default: 1,
}),
),
});
/** 新增权限请求参数类型 */
export type CreatePermissionRequest = Static<typeof CreatePermissionSchema>;
/**
* Schema
* @description DELETE /api/permission/:id
*/
export const DeletePermissionParamsSchema = t.Object({
id: t.String({
pattern: '^[1-9]\\d*$',
description: '权限IDbigint字符串必须为正整数',
examples: ['1', '100', '123456789'],
}),
});
/** 删除权限参数类型 */
export type DeletePermissionParams = Static<typeof DeletePermissionParamsSchema>;
/**
* Schema
* @description GET /api/permission/:id
*/
export const GetPermissionDetailParamsSchema = t.Object({
id: t.String({
pattern: '^[1-9]\\d*$',
description: '权限IDbigint字符串必须为正整数',
examples: ['1', '100', '123456789'],
}),
});
/** 获取权限详情参数类型 */
export type GetPermissionDetailParams = Static<typeof GetPermissionDetailParamsSchema>;
/**
* Schema
*/
export const UpdatePermissionParamsSchema = t.Object({
id: t.String({
pattern: '^[1-9]\\d*$',
description: '权限IDbigint字符串必须为正整数',
examples: ['1', '100', '123456789'],
}),
});
/**
* Schema
*/
export const UpdatePermissionBodySchema = t.Object({
name: t.Optional(
t.String({
minLength: 1,
maxLength: 50,
description: '权限名称,同级下唯一',
examples: ['创建用户', '查看用户'],
})
),
description: t.Optional(
t.String({
maxLength: 255,
description: '权限描述',
examples: ['创建新用户的权限'],
})
),
type: t.Optional(
t.Union([
t.Literal(1), t.Literal(2), t.Literal(3), t.Literal(4),
t.Literal('1'), t.Literal('2'), t.Literal('3'), t.Literal('4'),
], {
description: '权限类型1=菜单2=按钮3=接口4=数据',
examples: [1, 2, 3, 4],
})
),
apiPathKey: t.Optional(
t.String({
maxLength: 200,
description: '接口路径标识',
examples: ['/api/user'],
})
),
pagePathKey: t.Optional(
t.String({
maxLength: 200,
description: '前端路由标识',
examples: ['/user'],
})
),
module: t.Optional(
t.String({
minLength: 1,
maxLength: 30,
pattern: '^[a-zA-Z0-9_]+$',
description: '所属模块,只允许字母数字下划线',
examples: ['user', 'role'],
})
),
pid: t.Optional(
t.Union([
t.Literal('0'),
t.String({
pattern: '^[1-9]\\d*$',
description: '父权限IDBigint字符串',
}),
], {
description: '父权限ID0表示顶级权限',
examples: ['0', '1', '2'],
})
),
sort: t.Optional(
t.Number({
minimum: 0,
maximum: 999999,
description: '排序值',
examples: [0, 1, 10],
})
),
icon: t.Optional(
t.String({
maxLength: 100,
description: '图标标识',
examples: ['icon-user'],
})
),
status: t.Optional(
t.Union([
t.Literal(1), t.Literal(0), t.Literal('1'), t.Literal('0')
], {
description: '权限状态1=启用0=禁用',
examples: [1, 0],
})
),
});
export type UpdatePermissionParams = Static<typeof UpdatePermissionParamsSchema>;
export type UpdatePermissionBody = Static<typeof UpdatePermissionBodySchema>;
/**
*
*/
export const PermissionType = {
MENU: 1, // 菜单
BUTTON: 2, // 按钮
API: 3, // 接口
DATA: 4, // 数据
} as const;
/**
*
*/
export const PermissionStatus = {
ENABLED: 1, // 启用
DISABLED: 0, // 禁用
} as const;
/**
* Schema
*/
export const GetPermissionTreeQuerySchema = t.Object({
module: t.Optional(
t.String({
minLength: 1,
maxLength: 30,
description: '所属模块,筛选权限所属模块',
examples: ['system', 'user'],
})
),
status: t.Optional(
t.Union([
t.Literal(1), t.Literal(0), t.Literal('1'), t.Literal('0'), t.Literal('all')
], {
description: '权限状态1=启用0=禁用all=全部',
examples: [1, 0, 'all'],
})
),
type: t.Optional(
t.Union([
t.Literal(1), t.Literal(2), t.Literal(3), t.Literal(4),
t.Literal('1'), t.Literal('2'), t.Literal('3'), t.Literal('4'), t.Literal('all')
], {
description: '权限类型1=菜单2=按钮3=接口4=数据all=全部',
examples: [1, 2, 3, 4, 'all'],
})
),
});
export type GetPermissionTreeQuery = Static<typeof GetPermissionTreeQuerySchema>;
/**
* Schema
* @description GET /api/permission/tree/:pid
*/
export const GetPermissionTreeByPidParamsSchema = t.Object({
pid: t.Union([
t.Literal('0'),
t.String({
pattern: '^[1-9]\\d*$',
description: '父权限IDbigint字符串必须为正整数',
}),
], {
description: '父权限ID0表示获取根权限其他值表示获取指定父权限下的子权限树',
examples: ['0', '1', '100', '123456789'],
}),
});
/** 查看指定父权限下的子权限树参数类型 */
export type GetPermissionTreeByPidParams = Static<typeof GetPermissionTreeByPidParamsSchema>;
/**
* Schema
* @description
*/
export const PermissionSortItemSchema = t.Object({
/** 权限ID */
id: t.String({
pattern: '^[1-9]\\d*$',
description: '权限IDbigint字符串必须为正整数',
examples: ['1', '123', '456789'],
}),
/** 排序值 */
sort: t.String({
pattern: '^\\d+$',
description: '排序值,必须为非负整数字符串',
examples: ['0', '1', '10', '100'],
}),
});
/**
* Schema
* @description
*/
export const SortPermissionSchema = t.Object({
/** 权限排序数组 */
permissions: t.Array(PermissionSortItemSchema, {
minItems: 1,
maxItems: 100,
description: '权限排序数组最少1项最多100项',
examples: [
[
{ id: '123', sort: '1' },
{ id: '124', sort: '2' },
{ id: '125', sort: '3' }
]
],
}),
});
/** 权限排序项类型 */
export type PermissionSortItem = Static<typeof PermissionSortItemSchema>;
/** 权限排序请求参数类型 */
export type SortPermissionRequest = Static<typeof SortPermissionSchema>;

View File

@ -0,0 +1,583 @@
/**
* @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 { sysPermission, sysRolePermissions } from '@/eneities';
import { eq, and, max, ne, sql, asc, inArray, notInArray } from 'drizzle-orm';
import { alias, bigint, int, mysqlTable } from 'drizzle-orm/mysql-core';
import { successResponse, BusinessError } from '@/utils/responseFormate';
import { nextId } from '@/utils/snowflake';
import { DistributedLockService } from '@/utils/distributedLock';
import type { CreatePermissionRequest } from './permission.schema';
import type { CreatePermissionSuccessType } from './permission.response';
export class PermissionService {
/**
*
* @param body
* @returns Promise<CreatePermissionSuccessType>
* @throws BusinessError
*/
public async createPermission(body: CreatePermissionRequest): Promise<CreatePermissionSuccessType> {
const pid = body.pid || '0';
const lockKey = `permission:create:key:${body.permissionKey}:name:${body.name}:pid:${pid}`;
const lock = await DistributedLockService.acquire({ key: lockKey, ttl: 10 });
try {
// 1. permissionKey唯一性校验
const existKey = await db()
.select({ id: sysPermission.id })
.from(sysPermission)
.where(eq(sysPermission.permissionKey, body.permissionKey))
.limit(1);
if (existKey.length > 0) {
throw new BusinessError(`权限标识已存在: ${body.permissionKey}`, 409);
}
// 2. name同级唯一性校验
const existName = await db()
.select({ id: sysPermission.id })
.from(sysPermission)
.where(and(eq(sysPermission.name, body.name), eq(sysPermission.pid, pid)))
.limit(1);
if (existName.length > 0) {
throw new BusinessError(`权限名称在同级下已存在: ${body.name}`, 409);
}
// 3. 父级校验与层级处理
let level = 1;
let parentModule = body.module;
if (pid !== '0') {
const parent = await db().select().from(sysPermission).where(eq(sysPermission.id, pid)).limit(1);
if (parent.length === 0) {
throw new BusinessError(`父级权限不存在: ${pid}`, 404);
}
if (parent[0]!.status !== 1) {
throw new BusinessError(`父级权限状态非启用: ${pid}`, 400);
}
level = parent[0]!.level + 1;
if (level > 10) {
throw new BusinessError(`权限层级超过限制: ${level}`, 400);
}
// module必须与父权限一致
if (parent[0]!.module !== body.module) {
throw new BusinessError('父权限module不一致', 400);
}
parentModule = parent[0]!.module;
}
// 4. sort处理同级最大+1
let sort = '0';
if (body.sort !== undefined) {
sort = body.sort;
} else {
const maxSort = await db()
.select({ maxSort: max(sysPermission.sort) })
.from(sysPermission)
.where(eq(sysPermission.pid, pid));
sort = String(Number(maxSort[0]?.maxSort ?? 0) + 1);
}
// 5. 数据写入
const permissionId = nextId();
await db()
.insert(sysPermission)
.values([
{
id: permissionId.toString(),
pid: BigInt(pid),
level,
permissionKey: body.permissionKey,
name: body.name,
description: body.description ?? '',
type: Number(body.type),
apiPathKey: body.apiPathKey ?? null,
pagePathKey: body.pagePathKey ?? null,
module: parentModule,
sort,
icon: body.icon ?? null,
status: body.status !== undefined ? Number(body.status) : 1,
},
] as any);
// 6. 查询刚插入的数据
const insertedArr = await db().select().from(sysPermission).where(eq(sysPermission.id, permissionId.toString())).limit(1);
if (!insertedArr || insertedArr.length === 0) {
throw new BusinessError('创建权限失败', 500);
}
const inserted = insertedArr[0]!;
Logger.info(inserted);
// 7. 返回统一响应
return successResponse(
{
id: String(inserted.id),
permissionKey: inserted.permissionKey,
name: inserted.name,
description: inserted.description,
type: inserted.type,
apiPathKey: inserted.apiPathKey,
pagePathKey: inserted.pagePathKey,
module: inserted.module,
pid: String(inserted.pid),
level: inserted.level,
sort: inserted.sort,
icon: inserted.icon,
status: inserted.status,
createdAt: inserted.createdAt,
updatedAt: inserted.updatedAt,
},
'创建权限成功',
);
} finally {
await lock.release();
}
}
/**
*
* @param id IDbigint字符串
* @returns Promise<DeletePermissionSuccessType>
* @throws BusinessError
*/
public async deletePermission(id: string): Promise<import('./permission.response').DeletePermissionSuccessType> {
const lockKey = `permission:delete:id:${id}`;
const lock = await DistributedLockService.acquire({ key: lockKey, ttl: 10 });
try {
const permArr = await db().select().from(sysPermission).where(eq(sysPermission.id, id)).limit(1);
if (!permArr || permArr.length === 0) {
throw new BusinessError('权限不存在', 404);
}
const perm = permArr[0]!;
// if (perm.status !== 1) {
// throw new BusinessError('权限状态非启用', 400);
// }
// 2. 子权限检查
const childArr = await db().select({ id: sysPermission.id }).from(sysPermission).where(eq(sysPermission.pid, id)).limit(1);
if (childArr.length > 0) {
throw new BusinessError('存在子权限,无法删除', 400);
}
// 3. 被角色引用检查
const refArr = await db().select({ id: sysRolePermissions.id }).from(sysRolePermissions).where(eq(sysRolePermissions.permissionId, id)).limit(1);
if (refArr.length > 0) {
throw new BusinessError('权限被角色引用,无法删除', 400);
}
// 4. 逻辑删除
await db().delete(sysPermission).where(eq(sysPermission.id, id));
Logger.info(`权限删除成功:${id}`);
return successResponse({}, '权限删除成功');
} finally {
await lock.release();
}
}
/**
*
* @param id ID
* @param body
* @returns Promise<UpdatePermissionSuccessType>
* @throws BusinessError
*/
public async updatePermission(id: string, body: import('./permission.schema').UpdatePermissionBody): Promise<import('./permission.response').UpdatePermissionSuccessType> {
const lockKey = `permission:update:id:${id}`;
const lock = await DistributedLockService.acquire({ key: lockKey, ttl: 10 });
try {
// 1. 校验存在性
const permArr = await db().select().from(sysPermission).where(eq(sysPermission.id, id)).limit(1);
if (!permArr || permArr.length === 0) {
throw new BusinessError('权限不存在', 404);
}
const perm = permArr[0]!;
if (perm.status !== 1) {
throw new BusinessError('权限状态非启用', 400);
}
// 2. 校验同级重名如有修改name
if (body.name && body.name !== perm.name) {
const existName = await db()
.select({ id: sysPermission.id })
.from(sysPermission)
.where(and(eq(sysPermission.name, body.name), eq(sysPermission.pid, perm.pid as string), ne(sysPermission.id, id)))
.limit(1);
if (existName.length > 0) {
throw new BusinessError('同级下已存在该名称', 409);
}
}
// 3. 校验父权限module一致、非法移动
let newPid = perm.pid;
if (body.pid && body.pid !== String(perm.pid)) {
if (body.pid === id) {
throw new BusinessError('不能将权限移动到自身下', 400);
}
// 检查是否移动到子节点下(递归查)
const allPerms = await db().select().from(sysPermission);
const findChildren = (parentId: string, list: any[]): string[] => {
const children = list.filter(item => String(item.pid) === parentId).map(item => String(item.id));
return children.concat(children.flatMap(cid => findChildren(cid, list)));
};
const childrenIds = findChildren(id, allPerms);
if (childrenIds.includes(body.pid)) {
throw new BusinessError('不能将权限移动到自身子节点下', 400);
}
// 检查父权限存在且module一致
const parentArr = await db().select().from(sysPermission).where(eq(sysPermission.id, body.pid)).limit(1);
if (!parentArr || parentArr.length === 0) {
throw new BusinessError('父权限不存在', 400);
}
if (body.module && parentArr[0]!.module !== body.module) {
throw new BusinessError('父权限module不一致', 400);
}
newPid = body.pid;
}
// 4. 组装更新字段
const updateData: any = {};
if (body.name) updateData.name = body.name;
if (body.description !== undefined) updateData.description = body.description;
if (body.type !== undefined) updateData.type = Number(body.type);
if (body.apiPathKey !== undefined) updateData.apiPathKey = body.apiPathKey;
if (body.pagePathKey !== undefined) updateData.pagePathKey = body.pagePathKey;
if (body.module !== undefined) updateData.module = body.module;
if (body.icon !== undefined) updateData.icon = body.icon;
if (body.status !== undefined) updateData.status = Number(body.status);
if (body.sort !== undefined) updateData.sort = Number(body.sort);
if (body.pid !== undefined) updateData.pid = newPid;
updateData.updatedAt = new Date().toISOString();
// 5. 执行更新
await db().update(sysPermission).set(updateData).where(eq(sysPermission.id, id));
// 6. 查询最新数据
const updatedArr = await db().select().from(sysPermission).where(eq(sysPermission.id, id)).limit(1);
if (!updatedArr || updatedArr.length === 0) {
throw new BusinessError('权限修改失败', 500);
}
const updated = updatedArr[0]!;
// 7. 返回统一响应
return successResponse(
{
id: String(updated.id),
permission_key: updated.permissionKey,
name: updated.name,
description: updated.description,
type: updated.type,
api_path_key: updated.apiPathKey,
page_path_key: updated.pagePathKey,
module: updated.module,
pid: String(updated.pid),
level: updated.level,
sort: updated.sort,
icon: updated.icon,
status: updated.status,
created_at: updated.createdAt,
updated_at: updated.updatedAt,
},
'权限修改成功',
);
} finally {
await lock.release();
}
}
/**
*
* @param query modulestatustype
* @param pid ID'0'
* @returns Promise<GetPermissionTreeSuccessType>
*/
public async getPermissionTree(query: import('./permission.schema').GetPermissionTreeQuery, pid: string = '0'): Promise<import('./permission.response').GetPermissionTreeSuccessType> {
// 1. 定义表别名
const ctetTableName = 'ctet'
// 锚点表
const pointTable = alias(sysPermission, 'pt');
// CTE表
const cteTable = alias(sysPermission, ctetTableName);
const cteAlias = mysqlTable(ctetTableName, { id: int() })
// 递归表
const recursiveTable = alias(sysPermission, 'rt');
// 2. 构建基础查询条件
const baseConditions: any[] = [];
if (query.module) baseConditions.push(eq(sysPermission.module, query.module));
if (query.status && query.status !== 'all') baseConditions.push(eq(sysPermission.status, Number(query.status)));
if (query.type && query.type !== 'all') baseConditions.push(eq(sysPermission.type, Number(query.type)));
// 找根节点
if(pid !== '0'){
baseConditions.push(eq(pointTable.id, pid));
}else{
baseConditions.push(eq(pointTable.pid, pid));
}
// 3. 定义主查询
const mainQuery = db().select({
id: pointTable.id,
permissionKey: pointTable.permissionKey,
name: pointTable.name,
description: pointTable.description,
type: pointTable.type,
apiPathKey: pointTable.apiPathKey,
pagePathKey: pointTable.pagePathKey,
module: pointTable.module,
pid: pointTable.pid,
level: pointTable.level,
sort: pointTable.sort,
icon: pointTable.icon,
status: pointTable.status,
createdAt: pointTable.createdAt,
updatedAt: pointTable.updatedAt,
})
.from(pointTable)
.where(and(...baseConditions))
.union(
db().select({
id: recursiveTable.id,
permissionKey: recursiveTable.permissionKey,
name: recursiveTable.name,
description: recursiveTable.description,
type: recursiveTable.type,
apiPathKey: recursiveTable.apiPathKey,
pagePathKey: recursiveTable.pagePathKey,
module: recursiveTable.module,
pid: recursiveTable.pid,
level: recursiveTable.level,
sort: recursiveTable.sort,
icon: recursiveTable.icon,
status: recursiveTable.status,
createdAt: recursiveTable.createdAt,
updatedAt: recursiveTable.updatedAt,
})
.from(recursiveTable)
.innerJoin(cteAlias, eq(recursiveTable.pid, cteTable.id))
)
// 4. 构建最终查询
const finalQuery = db()
.select({
id: cteTable.id,
permissionKey: cteTable.permissionKey,
name: cteTable.name,
description: cteTable.description,
type: cteTable.type,
apiPathKey: cteTable.apiPathKey,
pagePathKey: cteTable.pagePathKey,
module: cteTable.module,
pid: cteTable.pid,
level: cteTable.level,
sort: cteTable.sort,
icon: cteTable.icon,
status: cteTable.status,
createdAt: cteTable.createdAt,
updatedAt: cteTable.updatedAt,
})
.from(cteAlias)
.orderBy(asc(cteTable.level), asc(cteTable.sort));
// 5. 将union查询放入WITH RECURSIVE中并构建最终查询
const recursiveSql = sql` WITH RECURSIVE ${cteTable} AS ( ${mainQuery} ) ${finalQuery} `;
// 6. 执行递归查询
const [rows] = await db().execute(recursiveSql);
const list = Array.isArray(rows) ? rows : (rows as any)[0];
// 7. 构建树结构
const nodeMap: Record<string, any> = {};
const roots: any[] = [];
for (const item of list as any[]) {
const node = {
id: String(item.id),
permissionKey: item.permissionKey || item.permission_key,
name: item.name,
description: item.description,
type: item.type,
apiPathKey: item.apiPathKey || item.api_path_key,
pagePathKey: item.pagePathKey || item.page_path_key,
module: item.module,
pid: String(item.pid),
level: item.level,
sort: String(item.sort || '0'),
icon: item.icon,
status: item.status,
createdAt: item.createdAt || item.created_at,
updatedAt: item.updatedAt || item.updated_at,
children: [],
};
nodeMap[node.id] = node;
}
// 8. 组装父子关系
for (const node of Object.values(nodeMap)) {
if (node.pid && node.pid !== '0' && nodeMap[node.pid]) {
nodeMap[node.pid].children.push(node);
} else {
roots.push(node);
}
}
return successResponse(roots, '查询权限树成功');
}
/**
*
* @param body
* @returns Promise<SortPermissionSuccessType>
* @throws BusinessError
* @type API =====================================================================
*/
public async sortPermissions(body: import('./permission.schema').SortPermissionRequest): Promise<import('./permission.response').SortPermissionSuccessType> {
// 1. 参数验证 - 检查权限ID的唯一性
const permissionIds = body.permissions.map(p => p.id);
const uniqueIds = new Set(permissionIds);
if (uniqueIds.size !== permissionIds.length) {
const duplicates = permissionIds.filter((id, index) => permissionIds.indexOf(id) !== index);
throw new BusinessError(`权限ID重复: ${duplicates.join(', ')}`, 400);
}
// 2. 检查sort值的唯一性
const sortValues = body.permissions.map(p => p.sort);
const uniqueSorts = new Set(sortValues);
if (uniqueSorts.size !== sortValues.length) {
const duplicateSorts = sortValues.filter((sort, index) => sortValues.indexOf(sort) !== index);
const duplicateIds = body.permissions
.filter(p => duplicateSorts.includes(p.sort))
.map(p => p.id);
throw new BusinessError(`排序值重复:${duplicateIds.join(', ')}`, 409);
}
// 3. 获取分布式锁 - 使用第一个权限的ID作为锁的一部分
const lockKey = `permission:sort:ids:${permissionIds.join(':')}`;
const lock = await DistributedLockService.acquire({ key: lockKey, ttl: 10 });
try {
// 4. 查询所有权限是否存在且状态正常
const permissions = await db()
.select({
id: sysPermission.id,
name: sysPermission.name,
pid: sysPermission.pid,
status: sysPermission.status,
sort: sysPermission.sort,
})
.from(sysPermission)
.where(inArray(sysPermission.id, permissionIds));
// 5. 检查权限是否都存在
if (permissions.length !== permissionIds.length) {
const foundIds = permissions.map(p => String(p.id));
const missingIds = permissionIds.filter(id => !foundIds.includes(id));
throw new BusinessError(`权限不存在: ${missingIds.join(', ')}`, 404);
}
// 6. 检查权限状态
const disabledPermissions = permissions.filter(p => p.status !== 1);
if (disabledPermissions.length > 0) {
const disabledIds = disabledPermissions.map(p => String(p.id));
throw new BusinessError(`权限状态非启用: ${disabledIds.join(', ')}`, 400);
}
// 7. 检查所有权限是否在同一级别相同pid
const pids = permissions.map(p => String(p.pid));
const uniquePids = new Set(pids);
if (uniquePids.size !== 1) {
throw new BusinessError('权限不在同一级别', 400);
}
// 8. 检查同级别下是否有其他权限使用了相同的sort值
const pid = pids[0]!;
const existingSorts = await db()
.select({ id: sysPermission.id, sort: sysPermission.sort })
.from(sysPermission)
.where(
and(
eq(sysPermission.pid, pid),
notInArray(sysPermission.id, permissionIds)
)
);
const existingSortValues = existingSorts.map(p => p.sort);
const conflictingSorts = sortValues.filter(sort => existingSortValues.includes(sort));
if (conflictingSorts.length > 0) {
throw new BusinessError(`排序值与其他权限冲突: ${conflictingSorts.join(', ')}`, 409);
}
// 9. 准备更新数据
const updatePromises = body.permissions.map(async (permData) => {
return db()
.update(sysPermission)
.set({
sort: permData.sort,
})
.where(eq(sysPermission.id, permData.id));
});
// 10. 批量执行更新
await Promise.all(updatePromises);
// 11. 查询更新后的权限信息
const updatedPermissions = await db()
.select({
id: sysPermission.id,
name: sysPermission.name,
sort: sysPermission.sort,
})
.from(sysPermission)
.where(inArray(sysPermission.id, permissionIds))
.orderBy(asc(sysPermission.sort));
Logger.info(`权限排序成功,更新了 ${updatedPermissions.length} 个权限`);
// 12. 返回成功响应
return successResponse(
{
updated_count: updatedPermissions.length,
updated_permissions: updatedPermissions.map(p => ({
id: String(p.id),
name: p.name,
sort: p.sort,
})),
},
'权限排序更新成功'
);
} finally {
await lock.release();
}
}
/**
*
* @param id IDbigint字符串
* @returns Promise<CreatePermissionSuccessType>
* @throws BusinessError
*/
public async getPermissionDetail(id: string): Promise<CreatePermissionSuccessType> {
if (!/^[1-9]\d*$/.test(id)) {
throw new BusinessError('参数格式错误', 400);
}
const permArr = await db().select().from(sysPermission).where(eq(sysPermission.id, id)).limit(1);
if (!permArr || permArr.length === 0) {
throw new BusinessError('权限不存在', 404);
}
const perm = permArr[0]!;
Logger.info(`获取权限详情:${id}`);
return successResponse({
id: String(perm.id),
pid: String(perm.pid),
level: perm.level,
permissionKey: perm.permissionKey,
name: perm.name,
description: perm.description,
type: perm.type,
apiPathKey: perm.apiPathKey,
pagePathKey: perm.pagePathKey,
module: perm.module,
sort: perm.sort,
icon: perm.icon,
status: perm.status,
createdAt: perm.createdAt,
updatedAt: perm.updatedAt,
}, '获取权限详情成功');
}
}
export const permissionService = new PermissionService();

View File

@ -0,0 +1,131 @@
/**
* @file Winston日志器工具类
* @author hotok
* @date 2025-06-28
* @lastEditor hotok
* @lastEditTime 2025-06-28
* @description winston的高性能日志记录器
*/
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
const loggerConfig = {
directory: 'logs',
maxSize: '20m',
maxFiles: '14d',
level: 'info',
console: true,
};
/**
*
*/
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 }) => {
return `[${timestamp}] ${level} ${message} ${stack}`;
}),
),
});
/**
*
*/
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 default Logger;
Logger.info('test');
Logger.error(new Error('test'));
Logger.http('test');
Logger.verbose('test');
Logger.debug('test');
Logger.warn('test');

View File

@ -0,0 +1,136 @@
# 权限模块开发计划
## 1. 权限表设计
```sql
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY COMMENT '主键ID',
pid BIGINT DEFAULT 0 COMMENT '父权限ID',
level TINYINT NOT NULL DEFAULT 1 COMMENT '权限层级',
permission_key VARCHAR(100) NOT NULL COMMENT '权限标识',
name VARCHAR(50) NOT NULL COMMENT '权限名称',
description VARCHAR(255) DEFAULT '' COMMENT '权限描述',
type TINYINT NOT NULL DEFAULT 1 COMMENT '权限类型(1=菜单 2=按钮 3=接口 4=数据)',
api_path_key VARCHAR(200) DEFAULT NULL COMMENT '接口路径标识',
page_path_key VARCHAR(200) DEFAULT NULL COMMENT '前端路由标识',
module VARCHAR(30) NOT NULL COMMENT '所属模块',
sort BIGINT DEFAULT 0 COMMENT '排序值',
icon VARCHAR(100) DEFAULT NULL COMMENT '图标标识',
status TINYINT DEFAULT 1 COMMENT '状态(1=启用, 0禁用)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
/* 唯一约束 */
UNIQUE KEY uniq_permission_key (permission_key),
/* 单列索引 */
KEY idx_pid (pid),
KEY idx_level (level),
KEY idx_type (type),
KEY idx_module (module),
KEY idx_time (created_at),
/* 复合索引 */
KEY idx_parent_sort (pid, sort)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='RBAC权限表';
```
## 2. 接口功能描述
2.1 新增权限
- 沿用父模块的module
2.2 删除权限
2.3 修改权限
- 存在父权限时module必须一致
- 同级允许重名
2.4 查看权限完整树
- 模块、状态、类型筛选
2.4 查看指定权限树
- 根据id查其下级树
2.5 排序
## 3. 接口开发计划
### 3.1 新增权限接口
- [x] 1.0 创建新增权限接口 (POST /api/permission)
- [x] 1.1 生成当前接口业务逻辑文档,写入 `permission.docs.md`
- [x] 1.2 创建 `permission.schema.ts` - 定义新增权限Schema
- [x] 1.3 创建 `permission.response.ts` - 定义新增权限响应格式
- [x] 1.4 创建 `permission.service.ts` - 实现新增权限业务逻辑
- [x] 1.5 创建 `permission.controller.ts` - 实现新增权限路由
### 3.2 删除权限接口
- [x] 2.0 创建删除权限接口 (DELETE /api/permission/:id)
- [x] 2.1 更新 `permission.docs.md` - 添加删除权限业务逻辑文档
- [x] 2.2 更新 `permission.schema.ts` - 定义删除权限Schema
- [x] 2.3 更新 `permission.response.ts` - 定义删除权限响应格式
- [x] 2.4 更新 `permission.service.ts` - 实现删除权限业务逻辑
- [x] 2.5 更新 `permission.controller.ts` - 实现删除权限路由
### 3.3 修改权限接口
- [x] 3.0 创建修改权限接口 (PUT /api/permission/:id)
- [x] 3.1 更新 `permission.docs.md` - 添加修改权限业务逻辑文档
- [x] 3.2 更新 `permission.schema.ts` - 定义修改权限Schema
- [x] 3.3 更新 `permission.response.ts` - 定义修改权限响应格式
- [x] 3.4 更新 `permission.service.ts` - 实现修改权限业务逻辑
- [x] 3.5 更新 `permission.controller.ts` - 实现修改权限路由
### 3.4 查看权限完整树接口
- [x] 4.0 创建查看权限完整树接口 (GET /api/permission/tree)
- [x] 4.1 更新 `permission.docs.md` - 添加查看权限完整树业务逻辑文档
- [x] 4.2 更新 `permission.schema.ts` - 定义查看权限完整树Schema
- [x] 4.3 更新 `permission.response.ts` - 定义查看权限完整树响应格式
- [x] 4.4 更新 `permission.service.ts` - 实现查看权限完整树业务逻辑
- [x] 4.5 更新 `permission.controller.ts` - 实现查看权限完整树路由
### 3.5 查看指定权限树接口
- ~~[ ] 5.0 创建查看指定权限树接口 (GET /api/permission/:id/tree)~~
- ~~[ ] 5.1 更新 `permission.docs.md` - 添加查看指定权限树业务逻辑文档~~
- ~~[ ] 5.2 更新 `permission.schema.ts` - 定义查看指定权限树Schema~~
- ~~[ ] 5.3 更新 `permission.response.ts` - 定义查看指定权限树响应格式~~
- ~~[ ] 5.4 更新 `permission.service.ts` - 实现查看指定权限树业务逻辑~~
- ~~[ ] 5.5 更新 `permission.controller.ts` - 实现查看指定权限树路由~~
### 3.6 权限排序接口
- [x] 6.0 创建权限排序接口 (PUT /api/permission/sort)
- [x] 6.1 更新 `permission.docs.md` - 添加权限排序业务逻辑文档
- [x] 6.2 更新 `permission.schema.ts` - 定义权限排序Schema
- [x] 6.3 更新 `permission.response.ts` - 定义权限排序响应格式
- [x] 6.4 更新 `permission.service.ts` - 实现权限排序业务逻辑
- [x] 6.5 更新 `permission.controller.ts` - 实现权限排序路由
### 3.7 获取权限详情接口
- [x] 7.0 创建获取权限详情接口 (GET /api/permission/:id)
- [x] 7.1 更新 `permission.docs.md` - 添加获取权限详情业务逻辑文档
- [x] 7.2 更新 `permission.schema.ts` - 定义获取权限详情Schema
- [x] 7.3 更新 `permission.response.ts` - 定义获取权限详情响应格式
- [x] 7.4 更新 `permission.service.ts` - 实现获取权限详情业务逻辑
- [x] 7.5 更新 `permission.controller.ts` - 实现获取权限详情路由
## 4. 相关文件
- `src/modules/permission/permission.docs.md` - 权限模块业务逻辑文档
- `src/modules/permission/permission.schema.ts` - 权限模块Schema定义
- `src/modules/permission/permission.response.ts` - 权限模块响应格式定义
- `src/modules/permission/permission.service.ts` - 权限模块业务逻辑实现
- `src/modules/permission/permission.controller.ts` - 权限模块路由控制器
- `src/modules/permission/permission.test.md` - 权限模块测试用例文档
- `src/eneities/sysPermission.ts` - 权限表实体定义
- `src/eneities/index.ts` - 实体导出文件更新
### 备注
- 权限模块需要支持树形结构,注意父子关系的处理
- 权限key必须唯一需要做好唯一性验证
- 删除权限时需要检查是否有子权限,有则不允许删除
- 修改权限时如果存在父权限module必须与父权限一致
- 同级权限允许重名但权限key必须唯一
- 排序功能需要支持拖拽排序更新多个权限的sort_order值