feat(permission): 新增权限模块新增/删除接口全链路实现(含分布式锁、唯一性/层级/引用校验、schema、响应、controller、docs)

This commit is contained in:
HeXiaoLong:Suanier 2025-07-08 16:42:57 +08:00
parent e8e352b6b6
commit 384213714c
12 changed files with 1716 additions and 707 deletions

View File

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

View File

@ -1,359 +1,277 @@
import { import { mysqlTable, mysqlSchema, AnyMySqlColumn, index, primaryKey, unique, bigint, varchar, int, json, timestamp, text, datetime, tinyint, date } from "drizzle-orm/mysql-core"
mysqlTable, import { sql } from "drizzle-orm"
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( export const sysDict = mysqlTable("sys_dict", {
'sys_dict', id: bigint({ mode: "number" }).autoincrement().notNull(),
{ code: varchar({ length: 50 }).notNull(),
id: bigint({ mode: 'number' }).autoincrement().notNull(), name: varchar({ length: 100 }).notNull(),
code: varchar({ length: 50 }).notNull(), value: varchar({ length: 200 }),
name: varchar({ length: 100 }).notNull(), description: varchar({ length: 500 }),
value: varchar({ length: 200 }), icon: varchar({ length: 100 }),
description: varchar({ length: 500 }), pid: bigint({ mode: "number" }),
icon: varchar({ length: 100 }), level: int().default(1).notNull(),
pid: bigint({ mode: 'number' }), sortOrder: int("sort_order").default(0).notNull(),
level: int().default(1).notNull(), status: varchar({ length: 20 }).default('active').notNull(),
sortOrder: int('sort_order').default(0).notNull(), isSystem: tinyint("is_system").default(0).notNull(),
status: varchar({ length: 20 }).default('active').notNull(), color: varchar({ length: 20 }),
isSystem: tinyint('is_system').default(0).notNull(), extra: json(),
color: varchar({ length: 20 }), createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
extra: json(), updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().onUpdateNow().notNull(),
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), },
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().onUpdateNow().notNull(), (table) => [
}, index("idx_level").on(table.level),
(table) => [ index("idx_pid").on(table.pid),
index('idx_level').on(table.level), index("idx_sort").on(table.sortOrder),
index('idx_pid').on(table.pid), index("idx_status").on(table.status),
index('idx_sort').on(table.sortOrder), primaryKey({ columns: [table.id], name: "sys_dict_id"}),
index('idx_status').on(table.status), unique("uk_code").on(table.code),
primaryKey({ columns: [table.id], name: 'sys_dict_id' }), ]);
unique('uk_code').on(table.code),
],
);
export const sysOperationLogs = mysqlTable( export const sysOperationLogs = mysqlTable("sys_operation_logs", {
'sys_operation_logs', id: bigint({ mode: "number" }).notNull(),
{ userId: bigint("user_id", { mode: "number" }),
id: bigint({ mode: 'number' }).notNull(), username: varchar({ length: 100 }),
userId: bigint('user_id', { mode: 'number' }), module: varchar({ length: 50 }).notNull(),
username: varchar({ length: 100 }), action: varchar({ length: 50 }).notNull(),
module: varchar({ length: 50 }).notNull(), target: varchar({ length: 200 }),
action: varchar({ length: 50 }).notNull(), targetId: bigint("target_id", { mode: "number" }),
target: varchar({ length: 200 }), requestData: text("request_data"),
targetId: bigint('target_id', { mode: 'number' }), responseData: text("response_data"),
requestData: text('request_data'), status: varchar({ length: 20 }).notNull(),
responseData: text('response_data'), ip: varchar({ length: 45 }),
status: varchar({ length: 20 }).notNull(), userAgent: varchar("user_agent", { length: 200 }),
ip: varchar({ length: 45 }), duration: bigint({ mode: "number" }),
userAgent: varchar('user_agent', { length: 200 }), errorMsg: text("error_msg"),
duration: bigint({ mode: 'number' }), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
errorMsg: text('error_msg'), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_created_at").on(table.createdAt),
.notNull(), index("idx_ip").on(table.ip),
}, index("idx_module_action").on(table.module, table.action),
(table) => [ index("idx_status").on(table.status),
index('idx_created_at').on(table.createdAt), index("idx_target").on(table.targetId),
index('idx_ip').on(table.ip), index("idx_user_id").on(table.userId),
index('idx_module_action').on(table.module, table.action), primaryKey({ columns: [table.id], name: "sys_operation_logs_id"}),
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( export const sysOrganizations = mysqlTable("sys_organizations", {
'sys_organizations', id: bigint({ mode: "number" }).notNull(),
{ code: varchar({ length: 100 }).notNull(),
id: bigint({ mode: 'number' }).notNull(), name: varchar({ length: 200 }).notNull(),
code: varchar({ length: 100 }).notNull(), fullName: varchar("full_name", { length: 200 }),
name: varchar({ length: 200 }).notNull(), description: text(),
fullName: varchar('full_name', { length: 200 }), pid: bigint({ mode: "number" }),
description: text(), path: varchar({ length: 500 }),
pid: bigint({ mode: 'number' }), level: int().default(1).notNull(),
path: varchar({ length: 500 }), type: varchar({ length: 20 }),
level: int().default(1).notNull(), status: varchar({ length: 20 }).default('active').notNull(),
type: varchar({ length: 20 }), sortOrder: int("sort_order").default(0).notNull(),
status: varchar({ length: 20 }).default('active').notNull(), leaderId: bigint("leader_id", { mode: "number" }),
sortOrder: int('sort_order').default(0).notNull(), address: varchar({ length: 200 }),
leaderId: bigint('leader_id', { mode: 'number' }), phone: varchar({ length: 50 }),
address: varchar({ length: 200 }), extra: json(),
phone: varchar({ length: 50 }), createdBy: bigint("created_by", { mode: "number" }),
extra: json(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), updatedBy: bigint("updated_by", { mode: "number" }),
createdAt: datetime('created_at', { mode: 'string' }) updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
.default(sql`(CURRENT_TIMESTAMP)`) deletedAt: datetime("deleted_at", { mode: 'string'}),
.notNull(), version: int().default(1).notNull(),
updatedBy: bigint('updated_by', { mode: 'number' }), },
updatedAt: datetime('updated_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_deleted_at").on(table.deletedAt),
.notNull(), index("idx_leader_id").on(table.leaderId),
deletedAt: datetime('deleted_at', { mode: 'string' }), index("idx_name").on(table.name),
version: int().default(1).notNull(), index("idx_path").on(table.path),
}, index("idx_pid").on(table.pid),
(table) => [ index("idx_sort").on(table.pid, table.sortOrder),
index('idx_deleted_at').on(table.deletedAt), index("idx_status").on(table.status),
index('idx_leader_id').on(table.leaderId), index("idx_type").on(table.type),
index('idx_name').on(table.name), primaryKey({ columns: [table.id], name: "sys_organizations_id"}),
index('idx_path').on(table.path), unique("uk_code").on(table.code, table.deletedAt),
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( export const sysPermission = mysqlTable("sys_permission", {
'sys_permissions', id: bigint({ mode: "number" }).notNull(),
{ pid: bigint({ mode: "number" }),
id: bigint({ mode: 'number' }).notNull(), level: tinyint().default(1).notNull(),
code: varchar({ length: 100 }).notNull(), permissionKey: varchar("permission_key", { length: 100 }).notNull(),
name: varchar({ length: 100 }).notNull(), name: varchar({ length: 50 }).notNull(),
type: varchar({ length: 20 }).notNull(), description: varchar({ length: 255 }).default(''),
resource: varchar({ length: 50 }), type: tinyint().default(1).notNull(),
action: varchar({ length: 50 }), apiPathKey: varchar("api_path_key", { length: 200 }),
description: text(), pagePathKey: varchar("page_path_key", { length: 200 }),
pid: bigint({ mode: 'number' }), module: varchar({ length: 30 }).notNull(),
path: varchar({ length: 500 }), sort: bigint({ mode: "number" }),
level: int().default(1).notNull(), icon: varchar({ length: 100 }),
sortOrder: int('sort_order').default(0).notNull(), status: tinyint().default(1),
status: varchar({ length: 20 }).default('active').notNull(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`),
meta: json(), updatedAt: datetime("updated_at", { mode: 'string'}),
createdBy: bigint('created_by', { mode: 'number' }), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_level").on(table.level),
.notNull(), index("idx_module").on(table.module),
updatedBy: bigint('updated_by', { mode: 'number' }), index("idx_parent_sort").on(table.pid, table.sort),
updatedAt: datetime('updated_at', { mode: 'string' }) index("idx_pid").on(table.pid),
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_time").on(table.createdAt),
.notNull(), index("idx_type").on(table.type),
deletedAt: datetime('deleted_at', { mode: 'string' }), primaryKey({ columns: [table.id], name: "sys_permission_id"}),
}, unique("uniq_permission_key").on(table.permissionKey),
(table) => [ ]);
index('idx_deleted_at').on(table.deletedAt),
index('idx_pid').on(table.pid),
index('idx_resource_action').on(table.resource, table.action),
index('idx_sort').on(table.pid, table.sortOrder),
index('idx_status').on(table.status),
index('idx_type').on(table.type),
primaryKey({ columns: [table.id], name: 'sys_permissions_id' }),
unique('uk_code').on(table.code, table.deletedAt),
],
);
export const sysRolePermissions = mysqlTable( export const sysRolePermissions = mysqlTable("sys_role_permissions", {
'sys_role_permissions', id: bigint({ mode: "number" }).notNull(),
{ roleId: bigint("role_id", { mode: "number" }).notNull(),
id: bigint({ mode: 'number' }).notNull(), permissionId: bigint("permission_id", { mode: "number" }).notNull(),
roleId: bigint('role_id', { mode: 'number' }).notNull(), isHalf: tinyint("is_half").default(0).notNull(),
permissionId: bigint('permission_id', { mode: 'number' }).notNull(), createdBy: bigint("created_by", { mode: "number" }),
isHalf: tinyint('is_half').default(0).notNull(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_is_half").on(table.isHalf),
.notNull(), index("idx_permission_id").on(table.permissionId),
}, index("idx_role_id").on(table.roleId),
(table) => [ primaryKey({ columns: [table.id], name: "sys_role_permissions_id"}),
index('idx_is_half').on(table.isHalf), unique("uk_role_permission").on(table.roleId, table.permissionId),
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( export const sysRoles = mysqlTable("sys_roles", {
'sys_roles', id: bigint({ mode: "number" }).notNull(),
{ code: varchar({ length: 50 }).notNull(),
id: bigint({ mode: 'number' }).notNull(), name: varchar({ length: 100 }).notNull(),
code: varchar({ length: 50 }).notNull(), description: text(),
name: varchar({ length: 100 }).notNull(), pid: bigint({ mode: "number" }),
description: text(), path: varchar({ length: 500 }),
pid: bigint({ mode: 'number' }), level: int().default(1).notNull(),
path: varchar({ length: 500 }), sortOrder: int("sort_order").default(0).notNull(),
level: int().default(1).notNull(), status: varchar({ length: 20 }).default('active').notNull(),
sortOrder: int('sort_order').default(0).notNull(), isSystem: tinyint("is_system").default(0).notNull(),
status: varchar({ length: 20 }).default('active').notNull(), permissionsSnapshot: json("permissions_snapshot"),
isSystem: tinyint('is_system').default(0).notNull(), extra: json(),
permissionsSnapshot: json('permissions_snapshot'), createdBy: bigint("created_by", { mode: "number" }),
extra: json(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), updatedBy: bigint("updated_by", { mode: "number" }),
createdAt: datetime('created_at', { mode: 'string' }) updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
.default(sql`(CURRENT_TIMESTAMP)`) deletedAt: datetime("deleted_at", { mode: 'string'}),
.notNull(), version: int().default(1).notNull(),
updatedBy: bigint('updated_by', { mode: 'number' }), },
updatedAt: datetime('updated_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_deleted_at").on(table.deletedAt),
.notNull(), index("idx_is_system").on(table.isSystem),
deletedAt: datetime('deleted_at', { mode: 'string' }), index("idx_name").on(table.name),
version: int().default(1).notNull(), index("idx_path").on(table.path),
}, index("idx_pid").on(table.pid),
(table) => [ index("idx_sort").on(table.pid, table.sortOrder),
index('idx_deleted_at').on(table.deletedAt), index("idx_status").on(table.status),
index('idx_is_system').on(table.isSystem), primaryKey({ columns: [table.id], name: "sys_roles_id"}),
index('idx_name').on(table.name), unique("uk_code").on(table.code, table.deletedAt),
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( export const sysTags = mysqlTable("sys_tags", {
'sys_tags', id: bigint({ mode: "number" }).notNull(),
{ name: varchar({ length: 50 }).notNull(),
id: bigint({ mode: 'number' }).notNull(), type: varchar({ length: 50 }).default('user'),
name: varchar({ length: 50 }).notNull(), color: varchar({ length: 50 }),
type: varchar({ length: 50 }).default('user'), description: text(),
color: varchar({ length: 50 }), usageCount: int("usage_count").default(0).notNull(),
description: text(), createdBy: bigint("created_by", { mode: "number" }),
usageCount: int('usage_count').default(0).notNull(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), deletedAt: datetime("deleted_at", { mode: 'string'}),
createdAt: datetime('created_at', { mode: 'string' }) },
.default(sql`(CURRENT_TIMESTAMP)`) (table) => [
.notNull(), index("idx_deleted_at").on(table.deletedAt),
deletedAt: datetime('deleted_at', { mode: 'string' }), index("idx_name").on(table.name),
}, index("idx_type").on(table.type),
(table) => [ index("idx_usage_count").on(table.usageCount),
index('idx_deleted_at').on(table.deletedAt), primaryKey({ columns: [table.id], name: "sys_tags_id"}),
index('idx_name').on(table.name), unique("uk_name_type").on(table.name, table.type, table.deletedAt),
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( export const sysUserOrganizations = mysqlTable("sys_user_organizations", {
'sys_user_organizations', id: bigint({ mode: "number" }).notNull(),
{ userId: bigint("user_id", { mode: "number" }).notNull(),
id: bigint({ mode: 'number' }).notNull(), organizationId: bigint("organization_id", { mode: "number" }).notNull(),
userId: bigint('user_id', { mode: 'number' }).notNull(), isPrimary: tinyint("is_primary").default(0).notNull(),
organizationId: bigint('organization_id', { mode: 'number' }).notNull(), position: varchar({ length: 100 }),
isPrimary: tinyint('is_primary').default(0).notNull(), joinedAt: datetime("joined_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
position: varchar({ length: 100 }), createdBy: bigint("created_by", { mode: "number" }),
joinedAt: datetime('joined_at', { mode: 'string' }) createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
.default(sql`(CURRENT_TIMESTAMP)`) },
.notNull(), (table) => [
createdBy: bigint('created_by', { mode: 'number' }), index("idx_is_primary").on(table.isPrimary),
createdAt: datetime('created_at', { mode: 'string' }) index("idx_joined_at").on(table.joinedAt),
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_organization_id").on(table.organizationId),
.notNull(), index("idx_user_id").on(table.userId),
}, primaryKey({ columns: [table.id], name: "sys_user_organizations_id"}),
(table) => [ unique("uk_user_org").on(table.userId, table.organizationId),
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( export const sysUserRoles = mysqlTable("sys_user_roles", {
'sys_user_roles', id: bigint({ mode: "number" }).notNull(),
{ userId: bigint("user_id", { mode: "number" }).notNull(),
id: bigint({ mode: 'number' }).notNull(), roleId: bigint("role_id", { mode: "number" }).notNull(),
userId: bigint('user_id', { mode: 'number' }).notNull(), expiredAt: datetime("expired_at", { mode: 'string'}),
roleId: bigint('role_id', { mode: 'number' }).notNull(), createdBy: bigint("created_by", { mode: "number" }),
expiredAt: datetime('expired_at', { mode: 'string' }), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_created_at").on(table.createdAt),
.notNull(), index("idx_expired_at").on(table.expiredAt),
}, index("idx_role_id").on(table.roleId),
(table) => [ index("idx_user_id").on(table.userId),
index('idx_created_at').on(table.createdAt), primaryKey({ columns: [table.id], name: "sys_user_roles_id"}),
index('idx_expired_at').on(table.expiredAt), unique("uk_user_role").on(table.userId, table.roleId),
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( export const sysUserTags = mysqlTable("sys_user_tags", {
'sys_user_tags', id: bigint({ mode: "number" }).notNull(),
{ userId: bigint("user_id", { mode: "number" }).notNull(),
id: bigint({ mode: 'number' }).notNull(), tagId: bigint("tag_id", { mode: "number" }).notNull(),
userId: bigint('user_id', { mode: 'number' }).notNull(), createdBy: bigint("created_by", { mode: "number" }),
tagId: bigint('tag_id', { mode: 'number' }).notNull(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_created_at").on(table.createdAt),
.notNull(), index("idx_tag_id").on(table.tagId),
}, index("idx_user_id").on(table.userId),
(table) => [ primaryKey({ columns: [table.id], name: "sys_user_tags_id"}),
index('idx_created_at').on(table.createdAt), unique("uk_user_tag").on(table.userId, table.tagId),
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( export const sysUsers = mysqlTable("sys_users", {
'sys_users', id: bigint({ mode: "number" }).notNull(),
{ username: varchar({ length: 50 }).notNull(),
id: bigint({ mode: 'number' }).notNull(), email: varchar({ length: 100 }).notNull(),
username: varchar({ length: 50 }).notNull(), mobile: varchar({ length: 20 }),
email: varchar({ length: 100 }).notNull(), passwordHash: varchar("password_hash", { length: 255 }).notNull(),
mobile: varchar({ length: 20 }), avatar: varchar({ length: 255 }),
passwordHash: varchar('password_hash', { length: 255 }).notNull(), nickname: varchar({ length: 100 }),
avatar: varchar({ length: 255 }), status: varchar({ length: 20 }).default('active').notNull(),
nickname: varchar({ length: 100 }), gender: tinyint().default(0),
status: varchar({ length: 20 }).default('active').notNull(), // you can use { mode: 'date' }, if you want to have Date as type for this column
gender: tinyint().default(0), birthday: date({ mode: 'string' }),
// you can use { mode: 'date' }, if you want to have Date as type for this column bio: varchar({ length: 500 }),
birthday: date({ mode: 'string' }), loginCount: int("login_count").default(0).notNull(),
bio: varchar({ length: 500 }), lastLoginAt: datetime("last_login_at", { mode: 'string'}),
loginCount: int('login_count').default(0).notNull(), lastLoginIp: varchar("last_login_ip", { length: 45 }),
lastLoginAt: datetime('last_login_at', { mode: 'string' }), failedAttempts: int("failed_attempts").default(0).notNull(),
lastLoginIp: varchar('last_login_ip', { length: 45 }), lockedUntil: datetime("locked_until", { mode: 'string'}),
failedAttempts: int('failed_attempts').default(0).notNull(), isRoot: tinyint("is_root").default(0).notNull(),
lockedUntil: datetime('locked_until', { mode: 'string' }), extra: json(),
isRoot: tinyint('is_root').default(0).notNull(), createdBy: bigint("created_by", { mode: "number" }),
extra: json(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), updatedBy: bigint("updated_by", { mode: "number" }),
createdAt: datetime('created_at', { mode: 'string' }) updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
.default(sql`(CURRENT_TIMESTAMP)`) deletedAt: datetime("deleted_at", { mode: 'string'}),
.notNull(), version: int().default(1).notNull(),
updatedBy: bigint('updated_by', { mode: 'number' }), },
updatedAt: datetime('updated_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_created_at").on(table.createdAt),
.notNull(), index("idx_deleted_at").on(table.deletedAt),
deletedAt: datetime('deleted_at', { mode: 'string' }), index("idx_is_root").on(table.isRoot),
version: int().default(1).notNull(), index("idx_last_login").on(table.lastLoginAt),
}, index("idx_mobile").on(table.mobile),
(table) => [ index("idx_status").on(table.status),
index('idx_created_at').on(table.createdAt), primaryKey({ columns: [table.id], name: "sys_users_id"}),
index('idx_deleted_at').on(table.deletedAt), unique("uk_email").on(table.email, table.deletedAt),
index('idx_is_root').on(table.isRoot), unique("uk_username").on(table.username, table.deletedAt),
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,357 +1,278 @@
import { import { mysqlTable, mysqlSchema, AnyMySqlColumn, index, primaryKey, unique, varchar, int, json, timestamp, text, datetime, tinyint, date, } from "drizzle-orm/mysql-core"
mysqlTable, import { bigintString as bigint } from "./customType"
index, import { sql } from "drizzle-orm"
primaryKey,
unique,
varchar,
int,
json,
timestamp,
text,
datetime,
tinyint,
date,
} from 'drizzle-orm/mysql-core';
import { sql } from 'drizzle-orm';
import { bigintString as bigint } from './customType';
export const sysDict = mysqlTable( export const sysDict = mysqlTable("sys_dict", {
'sys_dict', id: bigint({ mode: "number" }).notNull(),
{ code: varchar({ length: 50 }).notNull(),
id: bigint({ mode: 'number' }).notNull(), name: varchar({ length: 100 }).notNull(),
code: varchar({ length: 50 }).notNull(), value: varchar({ length: 200 }),
name: varchar({ length: 100 }).notNull(), description: varchar({ length: 500 }),
value: varchar({ length: 200 }), icon: varchar({ length: 100 }),
description: varchar({ length: 500 }), pid: bigint({ mode: "number" }),
icon: varchar({ length: 100 }), level: int().default(1).notNull(),
pid: bigint({ mode: 'number' }), sortOrder: int("sort_order").default(0).notNull(),
level: int().default(1).notNull(), status: varchar({ length: 20 }).default('active').notNull(),
sortOrder: int('sort_order').default(0).notNull(), isSystem: tinyint("is_system").default(0).notNull(),
status: varchar({ length: 20 }).default('active').notNull(), color: varchar({ length: 20 }),
isSystem: tinyint('is_system').default(0).notNull(), extra: json(),
color: varchar({ length: 20 }), createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
extra: json(), updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().onUpdateNow().notNull(),
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), },
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().onUpdateNow().notNull(), (table) => [
}, index("idx_level").on(table.level),
(table) => [ index("idx_pid").on(table.pid),
index('idx_level').on(table.level), index("idx_sort").on(table.sortOrder),
index('idx_pid').on(table.pid), index("idx_status").on(table.status),
index('idx_sort').on(table.sortOrder), primaryKey({ columns: [table.id], name: "sys_dict_id"}),
index('idx_status').on(table.status), unique("uk_code").on(table.code),
primaryKey({ columns: [table.id], name: 'sys_dict_id' }), ]);
unique('uk_code').on(table.code),
],
);
export const sysOperationLogs = mysqlTable( export const sysOperationLogs = mysqlTable("sys_operation_logs", {
'sys_operation_logs', id: bigint({ mode: "number" }).notNull(),
{ userId: bigint("user_id", { mode: "number" }),
id: bigint({ mode: 'number' }).notNull(), username: varchar({ length: 100 }),
userId: bigint('user_id', { mode: 'number' }), module: varchar({ length: 50 }).notNull(),
username: varchar({ length: 100 }), action: varchar({ length: 50 }).notNull(),
module: varchar({ length: 50 }).notNull(), target: varchar({ length: 200 }),
action: varchar({ length: 50 }).notNull(), targetId: bigint("target_id", { mode: "number" }),
target: varchar({ length: 200 }), requestData: text("request_data"),
targetId: bigint('target_id', { mode: 'number' }), responseData: text("response_data"),
requestData: text('request_data'), status: varchar({ length: 20 }).notNull(),
responseData: text('response_data'), ip: varchar({ length: 45 }),
status: varchar({ length: 20 }).notNull(), userAgent: varchar("user_agent", { length: 200 }),
ip: varchar({ length: 45 }), duration: bigint({ mode: "number" }),
userAgent: varchar('user_agent', { length: 200 }), errorMsg: text("error_msg"),
duration: bigint({ mode: 'number' }), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
errorMsg: text('error_msg'), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_created_at").on(table.createdAt),
.notNull(), index("idx_ip").on(table.ip),
}, index("idx_module_action").on(table.module, table.action),
(table) => [ index("idx_status").on(table.status),
index('idx_created_at').on(table.createdAt), index("idx_target").on(table.targetId),
index('idx_ip').on(table.ip), index("idx_user_id").on(table.userId),
index('idx_module_action').on(table.module, table.action), primaryKey({ columns: [table.id], name: "sys_operation_logs_id"}),
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( export const sysOrganizations = mysqlTable("sys_organizations", {
'sys_organizations', id: bigint({ mode: "number" }).notNull(),
{ code: varchar({ length: 100 }).notNull(),
id: bigint({ mode: 'number' }).notNull(), name: varchar({ length: 200 }).notNull(),
code: varchar({ length: 100 }).notNull(), fullName: varchar("full_name", { length: 200 }),
name: varchar({ length: 200 }).notNull(), description: text(),
fullName: varchar('full_name', { length: 200 }), pid: bigint({ mode: "number" }),
description: text(), path: varchar({ length: 500 }),
pid: bigint({ mode: 'number' }), level: int().default(1).notNull(),
path: varchar({ length: 500 }), type: varchar({ length: 20 }),
level: int().default(1).notNull(), status: varchar({ length: 20 }).default('active').notNull(),
type: varchar({ length: 20 }), sortOrder: int("sort_order").default(0).notNull(),
status: varchar({ length: 20 }).default('active').notNull(), leaderId: bigint("leader_id", { mode: "number" }),
sortOrder: int('sort_order').default(0).notNull(), address: varchar({ length: 200 }),
leaderId: bigint('leader_id', { mode: 'number' }), phone: varchar({ length: 50 }),
address: varchar({ length: 200 }), extra: json(),
phone: varchar({ length: 50 }), createdBy: bigint("created_by", { mode: "number" }),
extra: json(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), updatedBy: bigint("updated_by", { mode: "number" }),
createdAt: datetime('created_at', { mode: 'string' }) updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
.default(sql`(CURRENT_TIMESTAMP)`) deletedAt: datetime("deleted_at", { mode: 'string'}),
.notNull(), version: int().default(1).notNull(),
updatedBy: bigint('updated_by', { mode: 'number' }), },
updatedAt: datetime('updated_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_deleted_at").on(table.deletedAt),
.notNull(), index("idx_leader_id").on(table.leaderId),
deletedAt: datetime('deleted_at', { mode: 'string' }), index("idx_name").on(table.name),
version: int().default(1).notNull(), index("idx_path").on(table.path),
}, index("idx_pid").on(table.pid),
(table) => [ index("idx_sort").on(table.pid, table.sortOrder),
index('idx_deleted_at').on(table.deletedAt), index("idx_status").on(table.status),
index('idx_leader_id').on(table.leaderId), index("idx_type").on(table.type),
index('idx_name').on(table.name), primaryKey({ columns: [table.id], name: "sys_organizations_id"}),
index('idx_path').on(table.path), unique("uk_code").on(table.code, table.deletedAt),
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( export const sysPermission = mysqlTable("sys_permission", {
'sys_permissions', id: bigint({ mode: "number" }).notNull(),
{ pid: bigint({ mode: "number" }),
id: bigint({ mode: 'number' }).notNull(), level: tinyint().default(1).notNull(),
code: varchar({ length: 100 }).notNull(), permissionKey: varchar("permission_key", { length: 100 }).notNull(),
name: varchar({ length: 100 }).notNull(), name: varchar({ length: 50 }).notNull(),
type: varchar({ length: 20 }).notNull(), description: varchar({ length: 255 }).default(''),
resource: varchar({ length: 50 }), type: tinyint().default(1).notNull(),
action: varchar({ length: 50 }), apiPathKey: varchar("api_path_key", { length: 200 }),
description: text(), pagePathKey: varchar("page_path_key", { length: 200 }),
pid: bigint({ mode: 'number' }), module: varchar({ length: 30 }).notNull(),
path: varchar({ length: 500 }), sort: bigint({ mode: "number" }),
level: int().default(1).notNull(), icon: varchar({ length: 100 }),
sortOrder: int('sort_order').default(0).notNull(), status: tinyint().default(1),
status: varchar({ length: 20 }).default('active').notNull(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`),
meta: json(), updatedAt: datetime("updated_at", { mode: 'string'}),
createdBy: bigint('created_by', { mode: 'number' }), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_level").on(table.level),
.notNull(), index("idx_module").on(table.module),
updatedBy: bigint('updated_by', { mode: 'number' }), index("idx_parent_sort").on(table.pid, table.sort),
updatedAt: datetime('updated_at', { mode: 'string' }) index("idx_pid").on(table.pid),
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_time").on(table.createdAt),
.notNull(), index("idx_type").on(table.type),
deletedAt: datetime('deleted_at', { mode: 'string' }), primaryKey({ columns: [table.id], name: "sys_permission_id"}),
}, unique("uniq_permission_key").on(table.permissionKey),
(table) => [ ]);
index('idx_deleted_at').on(table.deletedAt),
index('idx_pid').on(table.pid),
index('idx_resource_action').on(table.resource, table.action),
index('idx_sort').on(table.pid, table.sortOrder),
index('idx_status').on(table.status),
index('idx_type').on(table.type),
primaryKey({ columns: [table.id], name: 'sys_permissions_id' }),
unique('uk_code').on(table.code, table.deletedAt),
],
);
export const sysRolePermissions = mysqlTable( export const sysRolePermissions = mysqlTable("sys_role_permissions", {
'sys_role_permissions', id: bigint({ mode: "number" }).notNull(),
{ roleId: bigint("role_id", { mode: "number" }).notNull(),
id: bigint({ mode: 'number' }).notNull(), permissionId: bigint("permission_id", { mode: "number" }).notNull(),
roleId: bigint('role_id', { mode: 'number' }).notNull(), isHalf: tinyint("is_half").default(0).notNull(),
permissionId: bigint('permission_id', { mode: 'number' }).notNull(), createdBy: bigint("created_by", { mode: "number" }),
isHalf: tinyint('is_half').default(0).notNull(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_is_half").on(table.isHalf),
.notNull(), index("idx_permission_id").on(table.permissionId),
}, index("idx_role_id").on(table.roleId),
(table) => [ primaryKey({ columns: [table.id], name: "sys_role_permissions_id"}),
index('idx_is_half').on(table.isHalf), unique("uk_role_permission").on(table.roleId, table.permissionId),
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( export const sysRoles = mysqlTable("sys_roles", {
'sys_roles', id: bigint({ mode: "number" }).notNull(),
{ code: varchar({ length: 50 }).notNull(),
id: bigint({ mode: 'number' }).notNull(), name: varchar({ length: 100 }).notNull(),
code: varchar({ length: 50 }).notNull(), description: text(),
name: varchar({ length: 100 }).notNull(), pid: bigint({ mode: "number" }),
description: text(), path: varchar({ length: 500 }),
pid: bigint({ mode: 'number' }), level: int().default(1).notNull(),
path: varchar({ length: 500 }), sortOrder: int("sort_order").default(0).notNull(),
level: int().default(1).notNull(), status: varchar({ length: 20 }).default('active').notNull(),
sortOrder: int('sort_order').default(0).notNull(), isSystem: tinyint("is_system").default(0).notNull(),
status: varchar({ length: 20 }).default('active').notNull(), permissionsSnapshot: json("permissions_snapshot"),
isSystem: tinyint('is_system').default(0).notNull(), extra: json(),
permissionsSnapshot: json('permissions_snapshot'), createdBy: bigint("created_by", { mode: "number" }),
extra: json(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), updatedBy: bigint("updated_by", { mode: "number" }),
createdAt: datetime('created_at', { mode: 'string' }) updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
.default(sql`(CURRENT_TIMESTAMP)`) deletedAt: datetime("deleted_at", { mode: 'string'}),
.notNull(), version: int().default(1).notNull(),
updatedBy: bigint('updated_by', { mode: 'number' }), },
updatedAt: datetime('updated_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_deleted_at").on(table.deletedAt),
.notNull(), index("idx_is_system").on(table.isSystem),
deletedAt: datetime('deleted_at', { mode: 'string' }), index("idx_name").on(table.name),
version: int().default(1).notNull(), index("idx_path").on(table.path),
}, index("idx_pid").on(table.pid),
(table) => [ index("idx_sort").on(table.pid, table.sortOrder),
index('idx_deleted_at').on(table.deletedAt), index("idx_status").on(table.status),
index('idx_is_system').on(table.isSystem), primaryKey({ columns: [table.id], name: "sys_roles_id"}),
index('idx_name').on(table.name), unique("uk_code").on(table.code, table.deletedAt),
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( export const sysTags = mysqlTable("sys_tags", {
'sys_tags', id: bigint({ mode: "number" }).notNull(),
{ name: varchar({ length: 50 }).notNull(),
id: bigint({ mode: 'number' }).notNull(), type: varchar({ length: 50 }).default('user'),
name: varchar({ length: 50 }).notNull(), color: varchar({ length: 50 }),
type: varchar({ length: 50 }).default('user'), description: text(),
color: varchar({ length: 50 }), usageCount: int("usage_count").default(0).notNull(),
description: text(), createdBy: bigint("created_by", { mode: "number" }),
usageCount: int('usage_count').default(0).notNull(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), deletedAt: datetime("deleted_at", { mode: 'string'}),
createdAt: datetime('created_at', { mode: 'string' }) },
.default(sql`(CURRENT_TIMESTAMP)`) (table) => [
.notNull(), index("idx_deleted_at").on(table.deletedAt),
deletedAt: datetime('deleted_at', { mode: 'string' }), index("idx_name").on(table.name),
}, index("idx_type").on(table.type),
(table) => [ index("idx_usage_count").on(table.usageCount),
index('idx_deleted_at').on(table.deletedAt), primaryKey({ columns: [table.id], name: "sys_tags_id"}),
index('idx_name').on(table.name), unique("uk_name_type").on(table.name, table.type, table.deletedAt),
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( export const sysUserOrganizations = mysqlTable("sys_user_organizations", {
'sys_user_organizations', id: bigint({ mode: "number" }).notNull(),
{ userId: bigint("user_id", { mode: "number" }).notNull(),
id: bigint({ mode: 'number' }).notNull(), organizationId: bigint("organization_id", { mode: "number" }).notNull(),
userId: bigint('user_id', { mode: 'number' }).notNull(), isPrimary: tinyint("is_primary").default(0).notNull(),
organizationId: bigint('organization_id', { mode: 'number' }).notNull(), position: varchar({ length: 100 }),
isPrimary: tinyint('is_primary').default(0).notNull(), joinedAt: datetime("joined_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
position: varchar({ length: 100 }), createdBy: bigint("created_by", { mode: "number" }),
joinedAt: datetime('joined_at', { mode: 'string' }) createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
.default(sql`(CURRENT_TIMESTAMP)`) },
.notNull(), (table) => [
createdBy: bigint('created_by', { mode: 'number' }), index("idx_is_primary").on(table.isPrimary),
createdAt: datetime('created_at', { mode: 'string' }) index("idx_joined_at").on(table.joinedAt),
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_organization_id").on(table.organizationId),
.notNull(), index("idx_user_id").on(table.userId),
}, primaryKey({ columns: [table.id], name: "sys_user_organizations_id"}),
(table) => [ unique("uk_user_org").on(table.userId, table.organizationId),
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( export const sysUserRoles = mysqlTable("sys_user_roles", {
'sys_user_roles', id: bigint({ mode: "number" }).notNull(),
{ userId: bigint("user_id", { mode: "number" }).notNull(),
id: bigint({ mode: 'number' }).notNull(), roleId: bigint("role_id", { mode: "number" }).notNull(),
userId: bigint('user_id', { mode: 'number' }).notNull(), expiredAt: datetime("expired_at", { mode: 'string'}),
roleId: bigint('role_id', { mode: 'number' }).notNull(), createdBy: bigint("created_by", { mode: "number" }),
expiredAt: datetime('expired_at', { mode: 'string' }), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_created_at").on(table.createdAt),
.notNull(), index("idx_expired_at").on(table.expiredAt),
}, index("idx_role_id").on(table.roleId),
(table) => [ index("idx_user_id").on(table.userId),
index('idx_created_at').on(table.createdAt), primaryKey({ columns: [table.id], name: "sys_user_roles_id"}),
index('idx_expired_at').on(table.expiredAt), unique("uk_user_role").on(table.userId, table.roleId),
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( export const sysUserTags = mysqlTable("sys_user_tags", {
'sys_user_tags', id: bigint({ mode: "number" }).notNull(),
{ userId: bigint("user_id", { mode: "number" }).notNull(),
id: bigint({ mode: 'number' }).notNull(), tagId: bigint("tag_id", { mode: "number" }).notNull(),
userId: bigint('user_id', { mode: 'number' }).notNull(), createdBy: bigint("created_by", { mode: "number" }),
tagId: bigint('tag_id', { mode: 'number' }).notNull(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), },
createdAt: datetime('created_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_created_at").on(table.createdAt),
.notNull(), index("idx_tag_id").on(table.tagId),
}, index("idx_user_id").on(table.userId),
(table) => [ primaryKey({ columns: [table.id], name: "sys_user_tags_id"}),
index('idx_created_at').on(table.createdAt), unique("uk_user_tag").on(table.userId, table.tagId),
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( export const sysUsers = mysqlTable("sys_users", {
'sys_users', id: bigint({ mode: "number" }).notNull(),
{ username: varchar({ length: 50 }).notNull(),
id: bigint({ mode: 'number' }).notNull(), email: varchar({ length: 100 }).notNull(),
username: varchar({ length: 50 }).notNull(), mobile: varchar({ length: 20 }),
email: varchar({ length: 100 }).notNull(), passwordHash: varchar("password_hash", { length: 255 }).notNull(),
mobile: varchar({ length: 20 }), avatar: varchar({ length: 255 }),
passwordHash: varchar('password_hash', { length: 255 }).notNull(), nickname: varchar({ length: 100 }),
avatar: varchar({ length: 255 }), status: varchar({ length: 20 }).default('active').notNull(),
nickname: varchar({ length: 100 }), gender: tinyint().default(0),
status: varchar({ length: 20 }).default('active').notNull(), // you can use { mode: 'date' }, if you want to have Date as type for this column
gender: tinyint().default(0), birthday: date({ mode: 'string' }),
// you can use { mode: 'date' }, if you want to have Date as type for this column bio: varchar({ length: 500 }),
birthday: date({ mode: 'string' }), loginCount: int("login_count").default(0).notNull(),
bio: varchar({ length: 500 }), lastLoginAt: datetime("last_login_at", { mode: 'string'}),
loginCount: int('login_count').default(0).notNull(), lastLoginIp: varchar("last_login_ip", { length: 45 }),
lastLoginAt: datetime('last_login_at', { mode: 'string' }), failedAttempts: int("failed_attempts").default(0).notNull(),
lastLoginIp: varchar('last_login_ip', { length: 45 }), lockedUntil: datetime("locked_until", { mode: 'string'}),
failedAttempts: int('failed_attempts').default(0).notNull(), isRoot: tinyint("is_root").default(0).notNull(),
lockedUntil: datetime('locked_until', { mode: 'string' }), extra: json(),
isRoot: tinyint('is_root').default(0).notNull(), createdBy: bigint("created_by", { mode: "number" }),
extra: json(), createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
createdBy: bigint('created_by', { mode: 'number' }), updatedBy: bigint("updated_by", { mode: "number" }),
createdAt: datetime('created_at', { mode: 'string' }) updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
.default(sql`(CURRENT_TIMESTAMP)`) deletedAt: datetime("deleted_at", { mode: 'string'}),
.notNull(), version: int().default(1).notNull(),
updatedBy: bigint('updated_by', { mode: 'number' }), },
updatedAt: datetime('updated_at', { mode: 'string' }) (table) => [
.default(sql`(CURRENT_TIMESTAMP)`) index("idx_created_at").on(table.createdAt),
.notNull(), index("idx_deleted_at").on(table.deletedAt),
deletedAt: datetime('deleted_at', { mode: 'string' }), index("idx_is_root").on(table.isRoot),
version: int().default(1).notNull(), index("idx_last_login").on(table.lastLoginAt),
}, index("idx_mobile").on(table.mobile),
(table) => [ index("idx_status").on(table.status),
index('idx_created_at').on(table.createdAt), primaryKey({ columns: [table.id], name: "sys_users_id"}),
index('idx_deleted_at').on(table.deletedAt), unique("uk_email").on(table.email, table.deletedAt),
index('idx_is_root').on(table.isRoot), unique("uk_username").on(table.username, table.deletedAt),
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中定义所有可能的响应格式 * @description Controller中定义所有可能的响应格式
*/ */
export const SortDictResponsesSchema = { export const SortDictResponsesSchema = {
200: responseWrapperSchema(t.Null(), { 200: responseWrapperSchema(t.Null({
description: '排序成功', description: '排序成功',
examples: { examples: {
code: 0, code: 0,
message: '字典项排序成功', message: '字典项排序成功',
data: null, data: null,
}, },
}), })),
400: responseWrapperSchema( 400: responseWrapperSchema(
t.Object({ t.Object({
error: t.String({ error: t.String({
@ -373,14 +373,16 @@ export type SortDictSuccessType = Static<(typeof SortDictResponsesSchema)[200]>;
* @description Controller中定义所有可能的响应格式 * @description Controller中定义所有可能的响应格式
*/ */
export const DeleteDictResponsesSchema = { export const DeleteDictResponsesSchema = {
200: responseWrapperSchema(t.Null(), { 200: responseWrapperSchema(t.Null(
description: '删除成功', {
examples: { description: '删除成功',
code: 0, examples: {
message: '字典项删除成功', code: 0,
data: null, message: '字典项删除成功',
}, data: null,
}), },
}
)),
400: responseWrapperSchema( 400: responseWrapperSchema(
t.Object({ t.Object({
error: t.String({ error: t.String({

View File

@ -14,6 +14,7 @@ import { testController } from './test/test.controller';
import { captchaController } from './captcha/captcha.controller'; import { captchaController } from './captcha/captcha.controller';
import { authController } from './auth/auth.controller'; import { authController } from './auth/auth.controller';
import { dictController } from './dict/dict.controller'; import { dictController } from './dict/dict.controller';
import { permissionController } from './permission/permission.controller';
/** /**
* - API * - API
@ -38,4 +39,6 @@ export const controllers = new Elysia({
// 验证码接口 // 验证码接口
.group('/captcha', (app) => app.use(captchaController)) .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,56 @@
/**
* @file Controller
* @author AI Assistant
* @date 2024-12-19
* @lastEditor AI Assistant
* @lastEditTime 2025-01-07
* @description
*/
import { Elysia } from 'elysia';
import { CreatePermissionSchema } from './permission.schema';
import { CreatePermissionResponsesSchema } from './permission.response';
import { DeletePermissionParamsSchema } from './permission.schema';
import { DeletePermissionResponsesSchema } from './permission.response';
import { permissionService } from './permission.service';
import { tags } from '@/constants/swaggerTags';
export const permissionController = new Elysia()
/**
*
* @route POST /api/permission
* @description
*/
.post(
'/api/permission',
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(
'/api/permission/:id',
async ({ params }) => permissionService.deletePermission(params.id),
{
params: DeletePermissionParamsSchema,
detail: {
summary: '删除权限',
description: '删除指定权限,会检查子权限和角色引用关系,采用逻辑删除',
tags: [tags.permission],
operationId: 'deletePermission',
},
response: DeletePermissionResponsesSchema,
}
);

View File

@ -0,0 +1,310 @@
# 权限模块业务逻辑文档
## 模块概述
权限模块提供基于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错误

View File

@ -0,0 +1,163 @@
/**
* @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]>;

View File

@ -0,0 +1,188 @@
/**
* @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>;
/**
*
*/
export const PermissionType = {
MENU: 1, // 菜单
BUTTON: 2, // 按钮
API: 3, // 接口
DATA: 4, // 数据
} as const;
/**
*
*/
export const PermissionStatus = {
ENABLED: 1, // 启用
DISABLED: 0, // 禁用
} as const;

View File

@ -0,0 +1,181 @@
/**
* @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 } from 'drizzle-orm';
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();
}
}
}
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,135 @@
# 权限模块开发计划
## 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 新增权限接口
- [ ] 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 删除权限接口
- [ ] 2.0 创建删除权限接口 (DELETE /api/permission/:id)
- [ ] 2.1 更新 `permission.docs.md` - 添加删除权限业务逻辑文档
- [ ] 2.2 更新 `permission.schema.ts` - 定义删除权限Schema
- [ ] 2.3 更新 `permission.response.ts` - 定义删除权限响应格式
- [ ] 2.4 更新 `permission.service.ts` - 实现删除权限业务逻辑
- [ ] 2.5 更新 `permission.controller.ts` - 实现删除权限路由
### 3.3 修改权限接口
- [ ] 3.0 创建修改权限接口 (PUT /api/permission/:id)
- [ ] 3.1 更新 `permission.docs.md` - 添加修改权限业务逻辑文档
- [ ] 3.2 更新 `permission.schema.ts` - 定义修改权限Schema
- [ ] 3.3 更新 `permission.response.ts` - 定义修改权限响应格式
- [ ] 3.4 更新 `permission.service.ts` - 实现修改权限业务逻辑
- [ ] 3.5 更新 `permission.controller.ts` - 实现修改权限路由
### 3.4 查看权限完整树接口
- [ ] 4.0 创建查看权限完整树接口 (GET /api/permission/tree)
- [ ] 4.1 更新 `permission.docs.md` - 添加查看权限完整树业务逻辑文档
- [ ] 4.2 更新 `permission.schema.ts` - 定义查看权限完整树Schema
- [ ] 4.3 更新 `permission.response.ts` - 定义查看权限完整树响应格式
- [ ] 4.4 更新 `permission.service.ts` - 实现查看权限完整树业务逻辑
- [ ] 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 权限排序接口
- [ ] 6.0 创建权限排序接口 (PUT /api/permission/sort)
- [ ] 6.1 更新 `permission.docs.md` - 添加权限排序业务逻辑文档
- [ ] 6.2 更新 `permission.schema.ts` - 定义权限排序Schema
- [ ] 6.3 更新 `permission.response.ts` - 定义权限排序响应格式
- [ ] 6.4 更新 `permission.service.ts` - 实现权限排序业务逻辑
- [ ] 6.5 更新 `permission.controller.ts` - 实现权限排序路由
### 3.7 获取权限详情接口
- [ ] 7.0 创建获取权限详情接口 (GET /api/permission/:id)
- [ ] 7.1 更新 `permission.docs.md` - 添加获取权限详情业务逻辑文档
- [ ] 7.2 更新 `permission.schema.ts` - 定义获取权限详情Schema
- [ ] 7.3 更新 `permission.response.ts` - 定义获取权限详情响应格式
- [ ] 7.4 更新 `permission.service.ts` - 实现获取权限详情业务逻辑
- [ ] 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值