feat(permission): 新增权限模块新增/删除接口全链路实现(含分布式锁、唯一性/层级/引用校验、schema、响应、controller、docs)
This commit is contained in:
parent
e8e352b6b6
commit
384213714c
@ -1,2 +1,3 @@
|
||||
import { relations } from 'drizzle-orm/relations';
|
||||
import {} from './schema';
|
||||
import { relations } from "drizzle-orm/relations";
|
||||
import { } from "./schema";
|
||||
|
||||
|
@ -1,359 +1,277 @@
|
||||
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(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
value: varchar({ length: 200 }),
|
||||
description: varchar({ length: 500 }),
|
||||
icon: varchar({ length: 100 }),
|
||||
pid: bigint({ mode: 'number' }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int('sort_order').default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint('is_system').default(0).notNull(),
|
||||
color: varchar({ length: 20 }),
|
||||
extra: json(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().onUpdateNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_level').on(table.level),
|
||||
index('idx_pid').on(table.pid),
|
||||
index('idx_sort').on(table.sortOrder),
|
||||
index('idx_status').on(table.status),
|
||||
primaryKey({ columns: [table.id], name: 'sys_dict_id' }),
|
||||
unique('uk_code').on(table.code),
|
||||
],
|
||||
);
|
||||
export const sysDict = mysqlTable("sys_dict", {
|
||||
id: bigint({ mode: "number" }).autoincrement().notNull(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
value: varchar({ length: 200 }),
|
||||
description: varchar({ length: 500 }),
|
||||
icon: varchar({ length: 100 }),
|
||||
pid: bigint({ mode: "number" }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint("is_system").default(0).notNull(),
|
||||
color: varchar({ length: 20 }),
|
||||
extra: json(),
|
||||
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().onUpdateNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_level").on(table.level),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_dict_id"}),
|
||||
unique("uk_code").on(table.code),
|
||||
]);
|
||||
|
||||
export const sysOperationLogs = mysqlTable(
|
||||
'sys_operation_logs',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
userId: bigint('user_id', { mode: 'number' }),
|
||||
username: varchar({ length: 100 }),
|
||||
module: varchar({ length: 50 }).notNull(),
|
||||
action: varchar({ length: 50 }).notNull(),
|
||||
target: varchar({ length: 200 }),
|
||||
targetId: bigint('target_id', { mode: 'number' }),
|
||||
requestData: text('request_data'),
|
||||
responseData: text('response_data'),
|
||||
status: varchar({ length: 20 }).notNull(),
|
||||
ip: varchar({ length: 45 }),
|
||||
userAgent: varchar('user_agent', { length: 200 }),
|
||||
duration: bigint({ mode: 'number' }),
|
||||
errorMsg: text('error_msg'),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_created_at').on(table.createdAt),
|
||||
index('idx_ip').on(table.ip),
|
||||
index('idx_module_action').on(table.module, table.action),
|
||||
index('idx_status').on(table.status),
|
||||
index('idx_target').on(table.targetId),
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: 'sys_operation_logs_id' }),
|
||||
],
|
||||
);
|
||||
export const sysOperationLogs = mysqlTable("sys_operation_logs", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }),
|
||||
username: varchar({ length: 100 }),
|
||||
module: varchar({ length: 50 }).notNull(),
|
||||
action: varchar({ length: 50 }).notNull(),
|
||||
target: varchar({ length: 200 }),
|
||||
targetId: bigint("target_id", { mode: "number" }),
|
||||
requestData: text("request_data"),
|
||||
responseData: text("response_data"),
|
||||
status: varchar({ length: 20 }).notNull(),
|
||||
ip: varchar({ length: 45 }),
|
||||
userAgent: varchar("user_agent", { length: 200 }),
|
||||
duration: bigint({ mode: "number" }),
|
||||
errorMsg: text("error_msg"),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_ip").on(table.ip),
|
||||
index("idx_module_action").on(table.module, table.action),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_target").on(table.targetId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_operation_logs_id"}),
|
||||
]);
|
||||
|
||||
export const sysOrganizations = mysqlTable(
|
||||
'sys_organizations',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 200 }).notNull(),
|
||||
fullName: varchar('full_name', { length: 200 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: 'number' }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
type: varchar({ length: 20 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
sortOrder: int('sort_order').default(0).notNull(),
|
||||
leaderId: bigint('leader_id', { mode: 'number' }),
|
||||
address: varchar({ length: 200 }),
|
||||
phone: varchar({ length: 50 }),
|
||||
extra: json(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
updatedBy: bigint('updated_by', { mode: 'number' }),
|
||||
updatedAt: datetime('updated_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_leader_id').on(table.leaderId),
|
||||
index('idx_name').on(table.name),
|
||||
index('idx_path').on(table.path),
|
||||
index('idx_pid').on(table.pid),
|
||||
index('idx_sort').on(table.pid, table.sortOrder),
|
||||
index('idx_status').on(table.status),
|
||||
index('idx_type').on(table.type),
|
||||
primaryKey({ columns: [table.id], name: 'sys_organizations_id' }),
|
||||
unique('uk_code').on(table.code, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const sysOrganizations = mysqlTable("sys_organizations", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 200 }).notNull(),
|
||||
fullName: varchar("full_name", { length: 200 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
type: varchar({ length: 20 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
leaderId: bigint("leader_id", { mode: "number" }),
|
||||
address: varchar({ length: 200 }),
|
||||
phone: varchar({ length: 50 }),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_leader_id").on(table.leaderId),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_type").on(table.type),
|
||||
primaryKey({ columns: [table.id], name: "sys_organizations_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysPermissions = mysqlTable(
|
||||
'sys_permissions',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
type: varchar({ length: 20 }).notNull(),
|
||||
resource: varchar({ length: 50 }),
|
||||
action: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: 'number' }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int('sort_order').default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
meta: json(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
updatedBy: bigint('updated_by', { mode: 'number' }),
|
||||
updatedAt: datetime('updated_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_pid').on(table.pid),
|
||||
index('idx_resource_action').on(table.resource, table.action),
|
||||
index('idx_sort').on(table.pid, table.sortOrder),
|
||||
index('idx_status').on(table.status),
|
||||
index('idx_type').on(table.type),
|
||||
primaryKey({ columns: [table.id], name: 'sys_permissions_id' }),
|
||||
unique('uk_code').on(table.code, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const 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(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
description: text(),
|
||||
pid: bigint({ mode: 'number' }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int('sort_order').default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint('is_system').default(0).notNull(),
|
||||
permissionsSnapshot: json('permissions_snapshot'),
|
||||
extra: json(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
updatedBy: bigint('updated_by', { mode: 'number' }),
|
||||
updatedAt: datetime('updated_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_is_system').on(table.isSystem),
|
||||
index('idx_name').on(table.name),
|
||||
index('idx_path').on(table.path),
|
||||
index('idx_pid').on(table.pid),
|
||||
index('idx_sort').on(table.pid, table.sortOrder),
|
||||
index('idx_status').on(table.status),
|
||||
primaryKey({ columns: [table.id], name: 'sys_roles_id' }),
|
||||
unique('uk_code').on(table.code, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const sysRoles = mysqlTable("sys_roles", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint("is_system").default(0).notNull(),
|
||||
permissionsSnapshot: json("permissions_snapshot"),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_system").on(table.isSystem),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_roles_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysTags = mysqlTable(
|
||||
'sys_tags',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
name: varchar({ length: 50 }).notNull(),
|
||||
type: varchar({ length: 50 }).default('user'),
|
||||
color: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
usageCount: int('usage_count').default(0).notNull(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_name').on(table.name),
|
||||
index('idx_type').on(table.type),
|
||||
index('idx_usage_count').on(table.usageCount),
|
||||
primaryKey({ columns: [table.id], name: 'sys_tags_id' }),
|
||||
unique('uk_name_type').on(table.name, table.type, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const sysTags = mysqlTable("sys_tags", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
name: varchar({ length: 50 }).notNull(),
|
||||
type: varchar({ length: 50 }).default('user'),
|
||||
color: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
usageCount: int("usage_count").default(0).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_type").on(table.type),
|
||||
index("idx_usage_count").on(table.usageCount),
|
||||
primaryKey({ columns: [table.id], name: "sys_tags_id"}),
|
||||
unique("uk_name_type").on(table.name, table.type, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysUserOrganizations = mysqlTable(
|
||||
'sys_user_organizations',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
userId: bigint('user_id', { mode: 'number' }).notNull(),
|
||||
organizationId: bigint('organization_id', { mode: 'number' }).notNull(),
|
||||
isPrimary: tinyint('is_primary').default(0).notNull(),
|
||||
position: varchar({ length: 100 }),
|
||||
joinedAt: datetime('joined_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_is_primary').on(table.isPrimary),
|
||||
index('idx_joined_at').on(table.joinedAt),
|
||||
index('idx_organization_id').on(table.organizationId),
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: 'sys_user_organizations_id' }),
|
||||
unique('uk_user_org').on(table.userId, table.organizationId),
|
||||
],
|
||||
);
|
||||
export const sysUserOrganizations = mysqlTable("sys_user_organizations", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
organizationId: bigint("organization_id", { mode: "number" }).notNull(),
|
||||
isPrimary: tinyint("is_primary").default(0).notNull(),
|
||||
position: varchar({ length: 100 }),
|
||||
joinedAt: datetime("joined_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_is_primary").on(table.isPrimary),
|
||||
index("idx_joined_at").on(table.joinedAt),
|
||||
index("idx_organization_id").on(table.organizationId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_organizations_id"}),
|
||||
unique("uk_user_org").on(table.userId, table.organizationId),
|
||||
]);
|
||||
|
||||
export const sysUserRoles = mysqlTable(
|
||||
'sys_user_roles',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
userId: bigint('user_id', { mode: 'number' }).notNull(),
|
||||
roleId: bigint('role_id', { mode: 'number' }).notNull(),
|
||||
expiredAt: datetime('expired_at', { mode: 'string' }),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_created_at').on(table.createdAt),
|
||||
index('idx_expired_at').on(table.expiredAt),
|
||||
index('idx_role_id').on(table.roleId),
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: 'sys_user_roles_id' }),
|
||||
unique('uk_user_role').on(table.userId, table.roleId),
|
||||
],
|
||||
);
|
||||
export const sysUserRoles = mysqlTable("sys_user_roles", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
roleId: bigint("role_id", { mode: "number" }).notNull(),
|
||||
expiredAt: datetime("expired_at", { mode: 'string'}),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_expired_at").on(table.expiredAt),
|
||||
index("idx_role_id").on(table.roleId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_roles_id"}),
|
||||
unique("uk_user_role").on(table.userId, table.roleId),
|
||||
]);
|
||||
|
||||
export const sysUserTags = mysqlTable(
|
||||
'sys_user_tags',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
userId: bigint('user_id', { mode: 'number' }).notNull(),
|
||||
tagId: bigint('tag_id', { mode: 'number' }).notNull(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_created_at').on(table.createdAt),
|
||||
index('idx_tag_id').on(table.tagId),
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: 'sys_user_tags_id' }),
|
||||
unique('uk_user_tag').on(table.userId, table.tagId),
|
||||
],
|
||||
);
|
||||
export const sysUserTags = mysqlTable("sys_user_tags", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
tagId: bigint("tag_id", { mode: "number" }).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_tag_id").on(table.tagId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_tags_id"}),
|
||||
unique("uk_user_tag").on(table.userId, table.tagId),
|
||||
]);
|
||||
|
||||
export const sysUsers = mysqlTable(
|
||||
'sys_users',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
username: varchar({ length: 50 }).notNull(),
|
||||
email: varchar({ length: 100 }).notNull(),
|
||||
mobile: varchar({ length: 20 }),
|
||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||
avatar: varchar({ length: 255 }),
|
||||
nickname: varchar({ length: 100 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
gender: tinyint().default(0),
|
||||
// you can use { mode: 'date' }, if you want to have Date as type for this column
|
||||
birthday: date({ mode: 'string' }),
|
||||
bio: varchar({ length: 500 }),
|
||||
loginCount: int('login_count').default(0).notNull(),
|
||||
lastLoginAt: datetime('last_login_at', { mode: 'string' }),
|
||||
lastLoginIp: varchar('last_login_ip', { length: 45 }),
|
||||
failedAttempts: int('failed_attempts').default(0).notNull(),
|
||||
lockedUntil: datetime('locked_until', { mode: 'string' }),
|
||||
isRoot: tinyint('is_root').default(0).notNull(),
|
||||
extra: json(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
updatedBy: bigint('updated_by', { mode: 'number' }),
|
||||
updatedAt: datetime('updated_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_created_at').on(table.createdAt),
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_is_root').on(table.isRoot),
|
||||
index('idx_last_login').on(table.lastLoginAt),
|
||||
index('idx_mobile').on(table.mobile),
|
||||
index('idx_status').on(table.status),
|
||||
primaryKey({ columns: [table.id], name: 'sys_users_id' }),
|
||||
unique('uk_email').on(table.email, table.deletedAt),
|
||||
unique('uk_username').on(table.username, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const sysUsers = mysqlTable("sys_users", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
username: varchar({ length: 50 }).notNull(),
|
||||
email: varchar({ length: 100 }).notNull(),
|
||||
mobile: varchar({ length: 20 }),
|
||||
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
|
||||
avatar: varchar({ length: 255 }),
|
||||
nickname: varchar({ length: 100 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
gender: tinyint().default(0),
|
||||
// you can use { mode: 'date' }, if you want to have Date as type for this column
|
||||
birthday: date({ mode: 'string' }),
|
||||
bio: varchar({ length: 500 }),
|
||||
loginCount: int("login_count").default(0).notNull(),
|
||||
lastLoginAt: datetime("last_login_at", { mode: 'string'}),
|
||||
lastLoginIp: varchar("last_login_ip", { length: 45 }),
|
||||
failedAttempts: int("failed_attempts").default(0).notNull(),
|
||||
lockedUntil: datetime("locked_until", { mode: 'string'}),
|
||||
isRoot: tinyint("is_root").default(0).notNull(),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_root").on(table.isRoot),
|
||||
index("idx_last_login").on(table.lastLoginAt),
|
||||
index("idx_mobile").on(table.mobile),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_users_id"}),
|
||||
unique("uk_email").on(table.email, table.deletedAt),
|
||||
unique("uk_username").on(table.username, table.deletedAt),
|
||||
]);
|
||||
|
@ -1,357 +1,278 @@
|
||||
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(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
value: varchar({ length: 200 }),
|
||||
description: varchar({ length: 500 }),
|
||||
icon: varchar({ length: 100 }),
|
||||
pid: bigint({ mode: 'number' }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int('sort_order').default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint('is_system').default(0).notNull(),
|
||||
color: varchar({ length: 20 }),
|
||||
extra: json(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().onUpdateNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_level').on(table.level),
|
||||
index('idx_pid').on(table.pid),
|
||||
index('idx_sort').on(table.sortOrder),
|
||||
index('idx_status').on(table.status),
|
||||
primaryKey({ columns: [table.id], name: 'sys_dict_id' }),
|
||||
unique('uk_code').on(table.code),
|
||||
],
|
||||
);
|
||||
export const sysDict = mysqlTable("sys_dict", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
value: varchar({ length: 200 }),
|
||||
description: varchar({ length: 500 }),
|
||||
icon: varchar({ length: 100 }),
|
||||
pid: bigint({ mode: "number" }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint("is_system").default(0).notNull(),
|
||||
color: varchar({ length: 20 }),
|
||||
extra: json(),
|
||||
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().onUpdateNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_level").on(table.level),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_dict_id"}),
|
||||
unique("uk_code").on(table.code),
|
||||
]);
|
||||
|
||||
export const sysOperationLogs = mysqlTable(
|
||||
'sys_operation_logs',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
userId: bigint('user_id', { mode: 'number' }),
|
||||
username: varchar({ length: 100 }),
|
||||
module: varchar({ length: 50 }).notNull(),
|
||||
action: varchar({ length: 50 }).notNull(),
|
||||
target: varchar({ length: 200 }),
|
||||
targetId: bigint('target_id', { mode: 'number' }),
|
||||
requestData: text('request_data'),
|
||||
responseData: text('response_data'),
|
||||
status: varchar({ length: 20 }).notNull(),
|
||||
ip: varchar({ length: 45 }),
|
||||
userAgent: varchar('user_agent', { length: 200 }),
|
||||
duration: bigint({ mode: 'number' }),
|
||||
errorMsg: text('error_msg'),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_created_at').on(table.createdAt),
|
||||
index('idx_ip').on(table.ip),
|
||||
index('idx_module_action').on(table.module, table.action),
|
||||
index('idx_status').on(table.status),
|
||||
index('idx_target').on(table.targetId),
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: 'sys_operation_logs_id' }),
|
||||
],
|
||||
);
|
||||
export const sysOperationLogs = mysqlTable("sys_operation_logs", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }),
|
||||
username: varchar({ length: 100 }),
|
||||
module: varchar({ length: 50 }).notNull(),
|
||||
action: varchar({ length: 50 }).notNull(),
|
||||
target: varchar({ length: 200 }),
|
||||
targetId: bigint("target_id", { mode: "number" }),
|
||||
requestData: text("request_data"),
|
||||
responseData: text("response_data"),
|
||||
status: varchar({ length: 20 }).notNull(),
|
||||
ip: varchar({ length: 45 }),
|
||||
userAgent: varchar("user_agent", { length: 200 }),
|
||||
duration: bigint({ mode: "number" }),
|
||||
errorMsg: text("error_msg"),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_ip").on(table.ip),
|
||||
index("idx_module_action").on(table.module, table.action),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_target").on(table.targetId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_operation_logs_id"}),
|
||||
]);
|
||||
|
||||
export const sysOrganizations = mysqlTable(
|
||||
'sys_organizations',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 200 }).notNull(),
|
||||
fullName: varchar('full_name', { length: 200 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: 'number' }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
type: varchar({ length: 20 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
sortOrder: int('sort_order').default(0).notNull(),
|
||||
leaderId: bigint('leader_id', { mode: 'number' }),
|
||||
address: varchar({ length: 200 }),
|
||||
phone: varchar({ length: 50 }),
|
||||
extra: json(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
updatedBy: bigint('updated_by', { mode: 'number' }),
|
||||
updatedAt: datetime('updated_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_leader_id').on(table.leaderId),
|
||||
index('idx_name').on(table.name),
|
||||
index('idx_path').on(table.path),
|
||||
index('idx_pid').on(table.pid),
|
||||
index('idx_sort').on(table.pid, table.sortOrder),
|
||||
index('idx_status').on(table.status),
|
||||
index('idx_type').on(table.type),
|
||||
primaryKey({ columns: [table.id], name: 'sys_organizations_id' }),
|
||||
unique('uk_code').on(table.code, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const sysOrganizations = mysqlTable("sys_organizations", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 200 }).notNull(),
|
||||
fullName: varchar("full_name", { length: 200 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
type: varchar({ length: 20 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
leaderId: bigint("leader_id", { mode: "number" }),
|
||||
address: varchar({ length: 200 }),
|
||||
phone: varchar({ length: 50 }),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_leader_id").on(table.leaderId),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
index("idx_type").on(table.type),
|
||||
primaryKey({ columns: [table.id], name: "sys_organizations_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysPermissions = mysqlTable(
|
||||
'sys_permissions',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
code: varchar({ length: 100 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
type: varchar({ length: 20 }).notNull(),
|
||||
resource: varchar({ length: 50 }),
|
||||
action: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
pid: bigint({ mode: 'number' }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int('sort_order').default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
meta: json(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
updatedBy: bigint('updated_by', { mode: 'number' }),
|
||||
updatedAt: datetime('updated_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_pid').on(table.pid),
|
||||
index('idx_resource_action').on(table.resource, table.action),
|
||||
index('idx_sort').on(table.pid, table.sortOrder),
|
||||
index('idx_status').on(table.status),
|
||||
index('idx_type').on(table.type),
|
||||
primaryKey({ columns: [table.id], name: 'sys_permissions_id' }),
|
||||
unique('uk_code').on(table.code, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const 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(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
description: text(),
|
||||
pid: bigint({ mode: 'number' }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int('sort_order').default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint('is_system').default(0).notNull(),
|
||||
permissionsSnapshot: json('permissions_snapshot'),
|
||||
extra: json(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
updatedBy: bigint('updated_by', { mode: 'number' }),
|
||||
updatedAt: datetime('updated_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_is_system').on(table.isSystem),
|
||||
index('idx_name').on(table.name),
|
||||
index('idx_path').on(table.path),
|
||||
index('idx_pid').on(table.pid),
|
||||
index('idx_sort').on(table.pid, table.sortOrder),
|
||||
index('idx_status').on(table.status),
|
||||
primaryKey({ columns: [table.id], name: 'sys_roles_id' }),
|
||||
unique('uk_code').on(table.code, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const sysRoles = mysqlTable("sys_roles", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
code: varchar({ length: 50 }).notNull(),
|
||||
name: varchar({ length: 100 }).notNull(),
|
||||
description: text(),
|
||||
pid: bigint({ mode: "number" }),
|
||||
path: varchar({ length: 500 }),
|
||||
level: int().default(1).notNull(),
|
||||
sortOrder: int("sort_order").default(0).notNull(),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
isSystem: tinyint("is_system").default(0).notNull(),
|
||||
permissionsSnapshot: json("permissions_snapshot"),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_system").on(table.isSystem),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_path").on(table.path),
|
||||
index("idx_pid").on(table.pid),
|
||||
index("idx_sort").on(table.pid, table.sortOrder),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_roles_id"}),
|
||||
unique("uk_code").on(table.code, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysTags = mysqlTable(
|
||||
'sys_tags',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
name: varchar({ length: 50 }).notNull(),
|
||||
type: varchar({ length: 50 }).default('user'),
|
||||
color: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
usageCount: int('usage_count').default(0).notNull(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_name').on(table.name),
|
||||
index('idx_type').on(table.type),
|
||||
index('idx_usage_count').on(table.usageCount),
|
||||
primaryKey({ columns: [table.id], name: 'sys_tags_id' }),
|
||||
unique('uk_name_type').on(table.name, table.type, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const sysTags = mysqlTable("sys_tags", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
name: varchar({ length: 50 }).notNull(),
|
||||
type: varchar({ length: 50 }).default('user'),
|
||||
color: varchar({ length: 50 }),
|
||||
description: text(),
|
||||
usageCount: int("usage_count").default(0).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_name").on(table.name),
|
||||
index("idx_type").on(table.type),
|
||||
index("idx_usage_count").on(table.usageCount),
|
||||
primaryKey({ columns: [table.id], name: "sys_tags_id"}),
|
||||
unique("uk_name_type").on(table.name, table.type, table.deletedAt),
|
||||
]);
|
||||
|
||||
export const sysUserOrganizations = mysqlTable(
|
||||
'sys_user_organizations',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
userId: bigint('user_id', { mode: 'number' }).notNull(),
|
||||
organizationId: bigint('organization_id', { mode: 'number' }).notNull(),
|
||||
isPrimary: tinyint('is_primary').default(0).notNull(),
|
||||
position: varchar({ length: 100 }),
|
||||
joinedAt: datetime('joined_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_is_primary').on(table.isPrimary),
|
||||
index('idx_joined_at').on(table.joinedAt),
|
||||
index('idx_organization_id').on(table.organizationId),
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: 'sys_user_organizations_id' }),
|
||||
unique('uk_user_org').on(table.userId, table.organizationId),
|
||||
],
|
||||
);
|
||||
export const sysUserOrganizations = mysqlTable("sys_user_organizations", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
organizationId: bigint("organization_id", { mode: "number" }).notNull(),
|
||||
isPrimary: tinyint("is_primary").default(0).notNull(),
|
||||
position: varchar({ length: 100 }),
|
||||
joinedAt: datetime("joined_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_is_primary").on(table.isPrimary),
|
||||
index("idx_joined_at").on(table.joinedAt),
|
||||
index("idx_organization_id").on(table.organizationId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_organizations_id"}),
|
||||
unique("uk_user_org").on(table.userId, table.organizationId),
|
||||
]);
|
||||
|
||||
export const sysUserRoles = mysqlTable(
|
||||
'sys_user_roles',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
userId: bigint('user_id', { mode: 'number' }).notNull(),
|
||||
roleId: bigint('role_id', { mode: 'number' }).notNull(),
|
||||
expiredAt: datetime('expired_at', { mode: 'string' }),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_created_at').on(table.createdAt),
|
||||
index('idx_expired_at').on(table.expiredAt),
|
||||
index('idx_role_id').on(table.roleId),
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: 'sys_user_roles_id' }),
|
||||
unique('uk_user_role').on(table.userId, table.roleId),
|
||||
],
|
||||
);
|
||||
export const sysUserRoles = mysqlTable("sys_user_roles", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
roleId: bigint("role_id", { mode: "number" }).notNull(),
|
||||
expiredAt: datetime("expired_at", { mode: 'string'}),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_expired_at").on(table.expiredAt),
|
||||
index("idx_role_id").on(table.roleId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_roles_id"}),
|
||||
unique("uk_user_role").on(table.userId, table.roleId),
|
||||
]);
|
||||
|
||||
export const sysUserTags = mysqlTable(
|
||||
'sys_user_tags',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
userId: bigint('user_id', { mode: 'number' }).notNull(),
|
||||
tagId: bigint('tag_id', { mode: 'number' }).notNull(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_created_at').on(table.createdAt),
|
||||
index('idx_tag_id').on(table.tagId),
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: 'sys_user_tags_id' }),
|
||||
unique('uk_user_tag').on(table.userId, table.tagId),
|
||||
],
|
||||
);
|
||||
export const sysUserTags = mysqlTable("sys_user_tags", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
userId: bigint("user_id", { mode: "number" }).notNull(),
|
||||
tagId: bigint("tag_id", { mode: "number" }).notNull(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_tag_id").on(table.tagId),
|
||||
index("idx_user_id").on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: "sys_user_tags_id"}),
|
||||
unique("uk_user_tag").on(table.userId, table.tagId),
|
||||
]);
|
||||
|
||||
export const sysUsers = mysqlTable(
|
||||
'sys_users',
|
||||
{
|
||||
id: bigint({ mode: 'number' }).notNull(),
|
||||
username: varchar({ length: 50 }).notNull(),
|
||||
email: varchar({ length: 100 }).notNull(),
|
||||
mobile: varchar({ length: 20 }),
|
||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||
avatar: varchar({ length: 255 }),
|
||||
nickname: varchar({ length: 100 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
gender: tinyint().default(0),
|
||||
// you can use { mode: 'date' }, if you want to have Date as type for this column
|
||||
birthday: date({ mode: 'string' }),
|
||||
bio: varchar({ length: 500 }),
|
||||
loginCount: int('login_count').default(0).notNull(),
|
||||
lastLoginAt: datetime('last_login_at', { mode: 'string' }),
|
||||
lastLoginIp: varchar('last_login_ip', { length: 45 }),
|
||||
failedAttempts: int('failed_attempts').default(0).notNull(),
|
||||
lockedUntil: datetime('locked_until', { mode: 'string' }),
|
||||
isRoot: tinyint('is_root').default(0).notNull(),
|
||||
extra: json(),
|
||||
createdBy: bigint('created_by', { mode: 'number' }),
|
||||
createdAt: datetime('created_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
updatedBy: bigint('updated_by', { mode: 'number' }),
|
||||
updatedAt: datetime('updated_at', { mode: 'string' })
|
||||
.default(sql`(CURRENT_TIMESTAMP)`)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string' }),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_created_at').on(table.createdAt),
|
||||
index('idx_deleted_at').on(table.deletedAt),
|
||||
index('idx_is_root').on(table.isRoot),
|
||||
index('idx_last_login').on(table.lastLoginAt),
|
||||
index('idx_mobile').on(table.mobile),
|
||||
index('idx_status').on(table.status),
|
||||
primaryKey({ columns: [table.id], name: 'sys_users_id' }),
|
||||
unique('uk_email').on(table.email, table.deletedAt),
|
||||
unique('uk_username').on(table.username, table.deletedAt),
|
||||
],
|
||||
);
|
||||
export const sysUsers = mysqlTable("sys_users", {
|
||||
id: bigint({ mode: "number" }).notNull(),
|
||||
username: varchar({ length: 50 }).notNull(),
|
||||
email: varchar({ length: 100 }).notNull(),
|
||||
mobile: varchar({ length: 20 }),
|
||||
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
|
||||
avatar: varchar({ length: 255 }),
|
||||
nickname: varchar({ length: 100 }),
|
||||
status: varchar({ length: 20 }).default('active').notNull(),
|
||||
gender: tinyint().default(0),
|
||||
// you can use { mode: 'date' }, if you want to have Date as type for this column
|
||||
birthday: date({ mode: 'string' }),
|
||||
bio: varchar({ length: 500 }),
|
||||
loginCount: int("login_count").default(0).notNull(),
|
||||
lastLoginAt: datetime("last_login_at", { mode: 'string'}),
|
||||
lastLoginIp: varchar("last_login_ip", { length: 45 }),
|
||||
failedAttempts: int("failed_attempts").default(0).notNull(),
|
||||
lockedUntil: datetime("locked_until", { mode: 'string'}),
|
||||
isRoot: tinyint("is_root").default(0).notNull(),
|
||||
extra: json(),
|
||||
createdBy: bigint("created_by", { mode: "number" }),
|
||||
createdAt: datetime("created_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
updatedBy: bigint("updated_by", { mode: "number" }),
|
||||
updatedAt: datetime("updated_at", { mode: 'string'}).default(sql`(CURRENT_TIMESTAMP)`).notNull(),
|
||||
deletedAt: datetime("deleted_at", { mode: 'string'}),
|
||||
version: int().default(1).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_created_at").on(table.createdAt),
|
||||
index("idx_deleted_at").on(table.deletedAt),
|
||||
index("idx_is_root").on(table.isRoot),
|
||||
index("idx_last_login").on(table.lastLoginAt),
|
||||
index("idx_mobile").on(table.mobile),
|
||||
index("idx_status").on(table.status),
|
||||
primaryKey({ columns: [table.id], name: "sys_users_id"}),
|
||||
unique("uk_email").on(table.email, table.deletedAt),
|
||||
unique("uk_username").on(table.username, table.deletedAt),
|
||||
]);
|
||||
|
@ -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(), {
|
||||
description: '删除成功',
|
||||
examples: {
|
||||
code: 0,
|
||||
message: '字典项删除成功',
|
||||
data: null,
|
||||
},
|
||||
}),
|
||||
200: responseWrapperSchema(t.Null(
|
||||
{
|
||||
description: '删除成功',
|
||||
examples: {
|
||||
code: 0,
|
||||
message: '字典项删除成功',
|
||||
data: null,
|
||||
},
|
||||
}
|
||||
)),
|
||||
400: responseWrapperSchema(
|
||||
t.Object({
|
||||
error: t.String({
|
||||
|
@ -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));
|
||||
|
56
src/modules/permission/permission.controller.ts
Normal file
56
src/modules/permission/permission.controller.ts
Normal 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,
|
||||
}
|
||||
);
|
310
src/modules/permission/permission.docs.md
Normal file
310
src/modules/permission/permission.docs.md
Normal 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错误
|
163
src/modules/permission/permission.response.ts
Normal file
163
src/modules/permission/permission.response.ts
Normal 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: '权限ID(bigint类型以字符串返回防止精度丢失)',
|
||||
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: '父权限ID(bigint类型以字符串返回)',
|
||||
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]>;
|
188
src/modules/permission/permission.schema.ts
Normal file
188
src/modules/permission/permission.schema.ts
Normal 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),
|
||||
/** 父权限ID,0表示顶级 */
|
||||
pid: t.Optional(
|
||||
t.Union([
|
||||
t.Literal('0'),
|
||||
t.String({
|
||||
pattern: '^[1-9]\\d*$',
|
||||
description: '父权限ID,Bigint字符串形式',
|
||||
}),
|
||||
], {
|
||||
description: '父权限ID,0表示顶级权限',
|
||||
examples: ['0', '1', '2'],
|
||||
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: '权限ID,bigint字符串,必须为正整数',
|
||||
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;
|
181
src/modules/permission/permission.service.ts
Normal file
181
src/modules/permission/permission.service.ts
Normal 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 权限ID(bigint字符串)
|
||||
* @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();
|
131
src/tests/demo/sampleLogger.ts
Normal file
131
src/tests/demo/sampleLogger.ts
Normal 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');
|
135
tasks/权限模块开发计划.md
Normal file
135
tasks/权限模块开发计划.md
Normal 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值
|
Loading…
Reference in New Issue
Block a user