From 384213714c0fe6dc95b21a55b3d1bca0fd2597f3 Mon Sep 17 00:00:00 2001 From: "HeXiaoLong:Suanier" Date: Tue, 8 Jul 2025 16:42:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(permission):=20=E6=96=B0=E5=A2=9E=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=A8=A1=E5=9D=97=E6=96=B0=E5=A2=9E/=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=8E=A5=E5=8F=A3=E5=85=A8=E9=93=BE=E8=B7=AF=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=EF=BC=88=E5=90=AB=E5=88=86=E5=B8=83=E5=BC=8F=E9=94=81?= =?UTF-8?q?=E3=80=81=E5=94=AF=E4=B8=80=E6=80=A7/=E5=B1=82=E7=BA=A7/?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E6=A0=A1=E9=AA=8C=E3=80=81schema=E3=80=81?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E3=80=81controller=E3=80=81docs=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drizzle/relations.ts | 5 +- drizzle/schema.ts | 614 ++++++++---------- src/eneities/index.ts | 613 ++++++++--------- src/modules/dict/dict.response.ts | 22 +- src/modules/index.ts | 5 +- .../permission/permission.controller.ts | 56 ++ src/modules/permission/permission.docs.md | 310 +++++++++ src/modules/permission/permission.response.ts | 163 +++++ src/modules/permission/permission.schema.ts | 188 ++++++ src/modules/permission/permission.service.ts | 181 ++++++ src/tests/demo/sampleLogger.ts | 131 ++++ tasks/权限模块开发计划.md | 135 ++++ 12 files changed, 1716 insertions(+), 707 deletions(-) create mode 100644 src/modules/permission/permission.controller.ts create mode 100644 src/modules/permission/permission.docs.md create mode 100644 src/modules/permission/permission.response.ts create mode 100644 src/modules/permission/permission.schema.ts create mode 100644 src/modules/permission/permission.service.ts create mode 100644 src/tests/demo/sampleLogger.ts create mode 100644 tasks/权限模块开发计划.md diff --git a/drizzle/relations.ts b/drizzle/relations.ts index 34c5368..80768e2 100644 --- a/drizzle/relations.ts +++ b/drizzle/relations.ts @@ -1,2 +1,3 @@ -import { relations } from 'drizzle-orm/relations'; -import {} from './schema'; +import { relations } from "drizzle-orm/relations"; +import { } from "./schema"; + diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 4f8045f..dcff651 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -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), +]); diff --git a/src/eneities/index.ts b/src/eneities/index.ts index 8449f7c..d0943eb 100644 --- a/src/eneities/index.ts +++ b/src/eneities/index.ts @@ -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), +]); diff --git a/src/modules/dict/dict.response.ts b/src/modules/dict/dict.response.ts index bb530b7..b099f33 100644 --- a/src/modules/dict/dict.response.ts +++ b/src/modules/dict/dict.response.ts @@ -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({ diff --git a/src/modules/index.ts b/src/modules/index.ts index fae3693..c338066 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -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)); diff --git a/src/modules/permission/permission.controller.ts b/src/modules/permission/permission.controller.ts new file mode 100644 index 0000000..6530d9c --- /dev/null +++ b/src/modules/permission/permission.controller.ts @@ -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, + } + ); \ No newline at end of file diff --git a/src/modules/permission/permission.docs.md b/src/modules/permission/permission.docs.md new file mode 100644 index 0000000..7316b6d --- /dev/null +++ b/src/modules/permission/permission.docs.md @@ -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错误 \ No newline at end of file diff --git a/src/modules/permission/permission.response.ts b/src/modules/permission/permission.response.ts new file mode 100644 index 0000000..add3256 --- /dev/null +++ b/src/modules/permission/permission.response.ts @@ -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]>; \ No newline at end of file diff --git a/src/modules/permission/permission.schema.ts b/src/modules/permission/permission.schema.ts new file mode 100644 index 0000000..c47b2ac --- /dev/null +++ b/src/modules/permission/permission.schema.ts @@ -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; + +/** + * 删除权限接口参数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; + +/** + * 权限类型枚举 + */ +export const PermissionType = { + MENU: 1, // 菜单 + BUTTON: 2, // 按钮 + API: 3, // 接口 + DATA: 4, // 数据 +} as const; + +/** + * 权限状态枚举 + */ +export const PermissionStatus = { + ENABLED: 1, // 启用 + DISABLED: 0, // 禁用 +} as const; \ No newline at end of file diff --git a/src/modules/permission/permission.service.ts b/src/modules/permission/permission.service.ts new file mode 100644 index 0000000..d59af78 --- /dev/null +++ b/src/modules/permission/permission.service.ts @@ -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 + * @throws BusinessError 业务逻辑错误 + */ + public async createPermission(body: CreatePermissionRequest): Promise { + 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 + * @throws BusinessError 业务逻辑错误 + */ + public async deletePermission(id: string): Promise { + 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(); \ No newline at end of file diff --git a/src/tests/demo/sampleLogger.ts b/src/tests/demo/sampleLogger.ts new file mode 100644 index 0000000..5e03273 --- /dev/null +++ b/src/tests/demo/sampleLogger.ts @@ -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'); \ No newline at end of file diff --git a/tasks/权限模块开发计划.md b/tasks/权限模块开发计划.md new file mode 100644 index 0000000..1c0e02f --- /dev/null +++ b/tasks/权限模块开发计划.md @@ -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值 \ No newline at end of file