refactor: 系统权限模块重构,迁移至PostgreSQL并对齐新设计

- 切换数据库为PostgreSQL,更新依赖与配置
- 重构权限体系,使用sys_user/sys_role/sys_menu等新实体替代旧模型
- 移除旧的用户角色/权限/系统设置相关代码与实体
- 新增字典、菜单管理模块与基础CRUD接口
- 优化鉴权逻辑,基于菜单权限标识进行接口校验
- 前端同步更新API、路由与权限控制逻辑
- 删除冗余的v-permission指令与相关代码
This commit is contained in:
andyesfly committed 2026-08-19 14:11:34 +08:00
1 parent 98c22f6afb
commit 0a8103f33d
67 files changed
+3263 -1924

No files matched your search

+3 -2
View File
@@ -19,13 +19,14 @@
"@midwayjs/validate": "^3.19.0",
"@types/validator": "^13.15.10",
"bcryptjs": "^2.4.3",
"cache-manager-ioredis": "^2.1.5",
"cache-manager-ioredis": "^2.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dayjs": "^1.11.13",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.12.0",
"pg": "^8.11.0",
"pg-hstore": "^2.3.4",
"reflect-metadata": "^0.2.2",
"sequelize": "^6.37.5",
"sequelize-typescript": "^2.1.6",
+6 -23
View File
@@ -11,12 +11,12 @@ export default {
sequelize: {
dataSource: {
default: {
dialect: "mysql",
host: process.env.MYSQL_HOST || "localhost",
port: Number(process.env.MYSQL_PORT) || 3306,
database: process.env.MYSQL_DATABASE || "smart_grid_iot",
username: process.env.MYSQL_USERNAME || "root",
password: process.env.MYSQL_PASSWORD || "123",
dialect: "postgres",
host: process.env.PG_HOST || "localhost",
port: Number(process.env.PG_PORT) || 5432,
database: process.env.PG_DATABASE || "xintong",
username: process.env.PG_USERNAME || "postgres",
password: process.env.PG_PASSWORD || "postgres",
timezone: "+08:00",
models: [join(__dirname, "../entity")],
define: {
@@ -62,22 +62,5 @@ export default {
csrf: {
enable: false
}
},
middleware: ["authMiddleware", "rbacMiddleware", "logMiddleware"],
middlewareConfig: {
authMiddleware: {
match: "/api/**",
ignore: ["/api/auth/**", "/api/health"]
},
rbacMiddleware: {
match: "/api/**",
ignore: ["/api/auth/**", "/api/health", "/api/dashboard/**"]
},
logMiddleware: {
match: "/api/**",
ignore: ["/api/logs/**", "/api/health", "/api/auth/login"]
}
}
} as MidwayConfig;
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
sequelize: {
sync: true,
sync: false,
},
};
+9 -1
View File
@@ -1,15 +1,23 @@
import { Configuration, ILifeCycle } from "@midwayjs/core";
import { Configuration, App, ILifeCycle } from "@midwayjs/core";
import * as Koa from "@midwayjs/koa";
import * as Sequelize from "@midwayjs/sequelize";
import * as CacheManager from "@midwayjs/cache-manager";
import { join } from "path";
import { AuthMiddleware } from "./middleware/auth.middleware";
import { RbacMiddleware } from "./middleware/rbac.middleware";
import { LogMiddleware } from "./middleware/log.middleware";
@Configuration({
imports: [Koa, Sequelize, CacheManager],
importConfigs: [join(__dirname, "./config")]
})
export class ContainerConfigLifeCycle implements ILifeCycle {
@App()
app!: Koa.Application;
async onReady() {
// 注册全局中间件(顺序:鉴权 → 权限 → 日志)
this.app.useMiddleware([AuthMiddleware, RbacMiddleware, LogMiddleware]);
console.log("智能物联网设备后台管理系统启动成功");
}
+19 -22
View File
@@ -4,9 +4,9 @@ import { AuthService } from "../service/auth.service";
import { TokenUtil } from "../util/token";
import { PermissionService } from "../service/permission.service";
import { InjectRepository } from "@midwayjs/sequelize";
import User from "../entity/user.entity";
import UserRole from "../entity/userRole.entity";
import Role from "../entity/role.entity";
import SysUser from "../entity/sysUser.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
@Controller("/api/auth")
export class AuthController {
@@ -22,14 +22,14 @@ export class AuthController {
@Inject()
ctx!: Context;
@InjectRepository(User)
userModel!: typeof User;
@InjectRepository(SysUser)
userModel!: typeof SysUser;
@InjectRepository(UserRole)
userRoleModel!: typeof UserRole;
@InjectRepository(SysUserRole)
userRoleModel!: typeof SysUserRole;
@InjectRepository(Role)
roleModel!: typeof Role;
@InjectRepository(SysRole)
roleModel!: typeof SysRole;
@Post("/login")
async login(@Body() body: { username: string; password: string }) {
@@ -110,14 +110,7 @@ export class AuthController {
const decoded = this.tokenUtil.verifyToken(token);
const userId = decoded.userId;
const user = await this.userModel.findByPk(userId, {
include: [
{
model: this.userRoleModel,
include: [this.roleModel]
}
]
});
const user = await this.userModel.findByPk(userId);
if (!user) {
return {
code: 404,
@@ -125,8 +118,13 @@ export class AuthController {
};
}
const permissions = await this.permissionService.getUserPermissions(userId);
const roles = (user as any).userRoles?.map((ur: any) => ur.role?.code).filter(Boolean) || [];
const userRoles = await this.userRoleModel.findAll({
where: { userId },
include: [this.roleModel]
});
const roles = userRoles.map(ur => ur.role?.roleCode).filter(Boolean) || [];
const menus = await this.permissionService.getUserMenus(userId);
return {
code: 200,
@@ -134,10 +132,9 @@ export class AuthController {
data: {
id: user.id,
username: user.username,
nickname: user.nickname || user.username,
avatar: user.avatar,
realName: user.realName,
roles,
permissions
menus
}
};
} catch (error: any) {
+42
View File
@@ -0,0 +1,42 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { DictService } from "../service/dict.service";
@Controller("/api/dicts")
export class DictController {
@Inject()
ctx!: Context;
@Inject()
dictService!: DictService;
@Get("/")
async list(@Query() query: { page?: number; pageSize?: number; dictType?: string; keyword?: string; status?: number }) {
const result = await this.dictService.list(query);
return result;
}
@Get("/types")
async types() {
const result = await this.dictService.types();
return result;
}
@Post("/")
async create(@Body() body: any) {
const result = await this.dictService.create(body);
return result;
}
@Put("/:id")
async update(@Param("id") id: string, @Body() body: any) {
const result = await this.dictService.update(id, body);
return result;
}
@Del("/:id")
async remove(@Param("id") id: string) {
const result = await this.dictService.remove(id);
return result;
}
}
+3 -9
View File
@@ -1,13 +1,7 @@
import { Controller, Get, Inject, Query, Param, All } from "@midwayjs/core";
import { Controller, Get, Del, Inject, Query, Param, All } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { LogService } from "../service/log.service";
const Delete = (path?: string) => {
return (target: any, key: string, descriptor: PropertyDescriptor) => {
return descriptor;
};
};
@Controller("/api/logs")
export class LogController {
@Inject()
@@ -17,7 +11,7 @@ export class LogController {
logService!: LogService;
@Get("/")
async list(@Query() query: { page?: number; pageSize?: number; moduleName?: string; actionType?: string; startDate?: string; endDate?: string; operator?: string }) {
async list(@Query() query: { page?: number; pageSize?: number; userId?: string; module?: string; action?: string; startDate?: string; endDate?: string }) {
const result = await this.logService.list(query);
return result;
}
@@ -46,7 +40,7 @@ export class LogController {
}
}
@Delete("/clear")
@Del("/clear")
async clear() {
const result = await this.logService.cleanExpiredLogs(0);
return {
+36
View File
@@ -0,0 +1,36 @@
import { Controller, Get, Post, Put, Del, Inject, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { MenuService } from "../service/menu.service";
@Controller("/api/menus")
export class MenuController {
@Inject()
ctx!: Context;
@Inject()
menuService!: MenuService;
@Get("/tree")
async getMenuTree() {
const result = await this.menuService.getMenuTree();
return result;
}
@Post("/")
async create(@Body() body: any) {
const result = await this.menuService.create(body);
return result;
}
@Put("/:id")
async update(@Param("id") id: string, @Body() body: any) {
const result = await this.menuService.update(id, body);
return result;
}
@Del("/:id")
async remove(@Param("id") id: string) {
const result = await this.menuService.remove(id);
return result;
}
}
+8 -14
View File
@@ -1,13 +1,7 @@
import { Controller, Get, Post, Put, Inject, Query, Body, Param } from "@midwayjs/core";
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { RoleService } from "../service/role.service";
const Delete = (path?: string) => {
return (target: any, key: string, descriptor: PropertyDescriptor) => {
return descriptor;
};
};
@Controller("/api/roles")
export class RoleController {
@Inject()
@@ -46,21 +40,21 @@ export class RoleController {
return result;
}
@Delete("/:id")
@Del("/:id")
async delete(@Param("id") id: string) {
const result = await this.roleService.delete(id);
return result;
}
@Get("/:id/permissions")
async getPermissions(@Param("id") id: string) {
const result = await this.roleService.getPermissions(Number(id));
@Get("/:id/menus")
async getMenus(@Param("id") id: string) {
const result = await this.roleService.getMenus(Number(id));
return result;
}
@Put("/:id/permissions")
async updatePermissions(@Param("id") id: string, @Body() body: { permissionIds: number[] }) {
const result = await this.roleService.updatePermissions(Number(id), body.permissionIds);
@Put("/:id/menus")
async updateMenus(@Param("id") id: string, @Body() body: { menuIds: number[] }) {
const result = await this.roleService.updateMenus(Number(id), body.menuIds);
return result;
}
}
@@ -1,32 +0,0 @@
import { Controller, Get, Put, Inject, Body } from "@midwayjs/core";
import { SystemService } from "../service/system.service";
@Controller("/api/settings")
export class SystemController {
@Inject()
systemService!: SystemService;
@Get("/")
async getSettings() {
const result = await this.systemService.getSettings();
return result;
}
@Put("/basic")
async updateBasicSettings(@Body() body: any) {
const result = await this.systemService.updateBasicSettings(body);
return result;
}
@Put("/security")
async updateSecuritySettings(@Body() body: any) {
const result = await this.systemService.updateSecuritySettings(body);
return result;
}
@Put("/notification")
async updateNotificationSettings(@Body() body: any) {
const result = await this.systemService.updateNotificationSettings(body);
return result;
}
}
+6 -10
View File
@@ -1,13 +1,7 @@
import { Controller, Get, Post, Put, Inject, Query, Body, Param } from "@midwayjs/core";
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { UserService } from "../service/user.service";
const Delete = (path?: string) => {
return (target: any, key: string, descriptor: PropertyDescriptor) => {
return descriptor;
};
};
@Controller("/api/users")
export class UserController {
@Inject()
@@ -46,15 +40,17 @@ export class UserController {
return result;
}
@Delete("/:id")
@Del("/:id")
async delete(@Param("id") id: string) {
const result = await this.userService.delete(id);
const currentUserId = this.ctx.state.user?.userId;
const result = await this.userService.delete(id, currentUserId);
return result;
}
@Post("/batch-delete")
async batchDelete(@Body() body: { ids: number[] }) {
const result = await this.userService.batchDelete(body.ids);
const currentUserId = this.ctx.state.user?.userId;
const result = await this.userService.batchDelete(body.ids, currentUserId);
return result;
}
+67
View File
@@ -0,0 +1,67 @@
import { IsNotEmpty, IsString, IsOptional, IsInt, IsIn } from "class-validator";
export class CreateDictDTO {
@IsNotEmpty({ message: "字典类型不能为空" })
@IsString()
dictType!: string;
@IsNotEmpty({ message: "字典标签不能为空" })
@IsString()
dictLabel!: string;
@IsNotEmpty({ message: "字典键值不能为空" })
@IsString()
dictValue!: string;
@IsOptional()
@IsInt()
sortOrder?: number = 0;
@IsOptional()
@IsIn([0, 1], { message: "状态只能为 0 或 1" })
status?: number = 1;
}
export class UpdateDictDTO {
@IsOptional()
@IsString()
dictType?: string;
@IsOptional()
@IsString()
dictLabel?: string;
@IsOptional()
@IsString()
dictValue?: string;
@IsOptional()
@IsInt()
sortOrder?: number;
@IsOptional()
@IsIn([0, 1], { message: "状态只能为 0 或 1" })
status?: number;
}
export class QueryDictDTO {
@IsOptional()
@IsInt()
page?: number;
@IsOptional()
@IsInt()
pageSize?: number;
@IsOptional()
@IsString()
dictType?: string;
@IsOptional()
@IsString()
keyword?: string;
@IsOptional()
@IsIn([0, 1])
status?: number;
}
+77
View File
@@ -0,0 +1,77 @@
import { IsNotEmpty, IsString, IsOptional, IsInt, IsIn } from "class-validator";
export class CreateMenuDTO {
@IsOptional()
@IsInt()
parentId?: number;
@IsNotEmpty({ message: "菜单名称不能为空" })
@IsString()
menuName!: string;
@IsOptional()
@IsString()
menuPath?: string;
@IsOptional()
@IsString()
component?: string;
@IsOptional()
@IsString()
icon?: string;
@IsOptional()
@IsInt()
sortOrder?: number = 0;
@IsOptional()
@IsIn([1, 2])
menuType?: number = 2;
@IsOptional()
@IsString()
permission?: string;
@IsOptional()
@IsIn([0, 1])
visible?: number = 1;
}
export class UpdateMenuDTO {
@IsOptional()
@IsInt()
parentId?: number;
@IsOptional()
@IsString()
menuName?: string;
@IsOptional()
@IsString()
menuPath?: string;
@IsOptional()
@IsString()
component?: string;
@IsOptional()
@IsString()
icon?: string;
@IsOptional()
@IsInt()
sortOrder?: number;
@IsOptional()
@IsIn([1, 2])
menuType?: number;
@IsOptional()
@IsString()
permission?: string;
@IsOptional()
@IsIn([0, 1])
visible?: number;
}
+12 -20
View File
@@ -1,24 +1,18 @@
import { IsNotEmpty, IsString, IsOptional, IsInt, Min, Max, IsArray } from "class-validator";
import { IsNotEmpty, IsString, IsOptional, IsInt, IsArray } from "class-validator";
export class CreateRoleDTO {
@IsNotEmpty({ message: "角色标识不能为空" })
@IsNotEmpty({ message: "角色编码不能为空" })
@IsString()
code!: string;
roleCode!: string;
@IsNotEmpty({ message: "角色名称不能为空" })
@IsString()
name!: string;
roleName!: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsInt()
@Min(0)
@Max(1)
status?: number = 1;
@IsOptional()
@IsInt()
sortOrder?: number = 0;
@@ -27,25 +21,23 @@ export class CreateRoleDTO {
export class UpdateRoleDTO {
@IsOptional()
@IsString()
name?: string;
roleCode?: string;
@IsOptional()
@IsString()
roleName?: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsInt()
@Min(0)
@Max(1)
status?: number;
@IsOptional()
@IsInt()
sortOrder?: number;
}
export class UpdatePermissionDTO {
@IsNotEmpty({ message: "权限ID列表不能为空" })
export class UpdateMenusDTO {
@IsNotEmpty({ message: "菜单ID列表不能为空" })
@IsArray()
permissionIds!: number[];
menuIds!: number[];
}
+7 -9
View File
@@ -13,10 +13,11 @@ export class CreateUserDTO {
@MaxLength(20, { message: "密码长度不能超过20位" })
password!: string;
@IsNotEmpty({ message: "昵称不能为空" })
@IsNotEmpty({ message: "真实姓名不能为空" })
@IsString()
@MinLength(2, { message: "昵称长度不能小于2位" })
nickname!: string;
@MinLength(2, { message: "真实姓名长度不能小于2位" })
@MaxLength(64, { message: "真实姓名长度不能超过64位" })
realName!: string;
@IsOptional()
@IsEmail({}, { message: "邮箱格式不正确" })
@@ -39,8 +40,9 @@ export class CreateUserDTO {
export class UpdateUserDTO {
@IsOptional()
@IsString()
@MinLength(2, { message: "昵称长度不能小于2位" })
nickname?: string;
@MinLength(2, { message: "真实姓名长度不能小于2位" })
@MaxLength(64, { message: "真实姓名长度不能超过64位" })
realName?: string;
@IsOptional()
@IsEmail({}, { message: "邮箱格式不正确" })
@@ -50,10 +52,6 @@ export class UpdateUserDTO {
@Matches(/^1[3-9]\d{9}$/, { message: "手机号格式不正确" })
phone?: string;
@IsOptional()
@IsString()
avatar?: string;
@IsOptional()
@IsArray()
roleIds?: number[];
-112
View File
@@ -1,112 +0,0 @@
import { Column, Model, Table, DataType, CreatedAt } from "sequelize-typescript";
@Table({
tableName: "operation_logs",
comment: "操作日志表",
timestamps: true,
createdAt: "created_at",
updatedAt: false
})
export default class OperationLog extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@Column({
type: DataType.BIGINT,
field: "user_id",
comment: "操作用户ID"
})
userId!: number;
@Column({
type: DataType.STRING(50),
field: "username",
comment: "操作人用户名"
})
username!: string;
@Column({
type: DataType.STRING(255),
allowNull: false,
field: "action",
comment: "操作描述"
})
action!: string;
@Column({
type: DataType.STRING(50),
field: "module",
comment: "操作模块"
})
module!: string;
@Column({
type: DataType.STRING(10),
field: "method",
comment: "请求方法(GET/POST/PUT/DELETE等)"
})
method!: string;
@Column({
type: DataType.STRING(500),
field: "url",
comment: "请求URL"
})
url!: string;
@Column({
type: DataType.STRING(45),
field: "ip_address",
comment: "IP地址(支持IPv6)"
})
ipAddress!: string;
@Column({
type: DataType.STRING(500),
field: "user_agent",
comment: "浏览器UA"
})
userAgent!: string;
@Column({
type: DataType.TEXT,
field: "request_params",
comment: "请求参数(JSON格式)"
})
requestParams!: string;
@Column({
type: DataType.TEXT,
field: "response_data",
comment: "响应数据(JSON格式)"
})
responseData!: string;
@Column({
type: DataType.INTEGER,
field: "status_code",
comment: "HTTP响应状态码"
})
statusCode!: number;
@Column({
type: DataType.INTEGER,
field: "duration",
comment: "请求耗时(毫秒)"
})
duration!: number;
@Column({
type: DataType.TEXT,
field: "error_message",
comment: "错误信息(仅失败时记录)"
})
errorMessage!: string;
@CreatedAt
created_at!: Date;
}
-95
View File
@@ -1,95 +0,0 @@
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, HasMany } from "sequelize-typescript";
import RolePermission from "./rolePermission.entity";
@Table({
tableName: "permissions",
comment: "权限表",
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at"
})
export default class Permission extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@Column({
type: DataType.STRING(50),
allowNull: false,
comment: "权限名称"
})
name!: string;
@Column({
type: DataType.STRING(100),
allowNull: false,
unique: true,
comment: "权限编码(唯一标识)"
})
code!: string;
@Column({
type: DataType.ENUM("menu", "button", "api"),
allowNull: false,
defaultValue: "menu",
comment: "权限类型: menu-菜单 button-按钮 api-接口"
})
type!: string;
@Column({
type: DataType.BIGINT,
field: "parent_id",
comment: "父权限ID(NULL表示一级菜单)"
})
parentId!: number;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
field: "sort_order",
comment: "排序(数值越小越靠前)"
})
sortOrder!: number;
@Column({
type: DataType.STRING(50),
field: "icon",
comment: "图标(菜单类型使用)"
})
icon!: string;
@Column({
type: DataType.STRING(255),
field: "route",
comment: "路由路径(菜单类型使用)"
})
route!: string;
@Column({
type: DataType.STRING(255),
field: "component",
comment: "组件路径(菜单类型使用)"
})
component!: string;
@Column({
type: DataType.TINYINT,
allowNull: false,
defaultValue: 1,
comment: "状态: 0-禁用 1-启用"
})
status!: number;
@HasMany(() => RolePermission)
rolePermissions!: RolePermission[];
@CreatedAt
created_at!: Date;
@UpdatedAt
updated_at!: Date;
}
-70
View File
@@ -1,70 +0,0 @@
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, HasMany } from "sequelize-typescript";
import UserRole from "./userRole.entity";
import RolePermission from "./rolePermission.entity";
@Table({
tableName: "roles",
comment: "角色表",
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at"
})
export default class Role extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@Column({
type: DataType.STRING(50),
allowNull: false,
comment: "角色名称"
})
name!: string;
@Column({
type: DataType.STRING(50),
allowNull: false,
unique: true,
comment: "角色编码(唯一标识)"
})
code!: string;
@Column({
type: DataType.STRING(255),
field: "description",
comment: "描述"
})
description!: string;
@Column({
type: DataType.TINYINT,
allowNull: false,
defaultValue: 1,
comment: "状态: 0-禁用 1-启用"
})
status!: number;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
field: "sort_order",
comment: "排序(数值越小越靠前)"
})
sortOrder!: number;
@HasMany(() => UserRole)
userRoles!: UserRole[];
@HasMany(() => RolePermission)
rolePermissions!: RolePermission[];
@CreatedAt
created_at!: Date;
@UpdatedAt
updated_at!: Date;
}
@@ -1,46 +0,0 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Role from "./role.entity";
import Permission from "./permission.entity";
@Table({
tableName: "role_permissions",
comment: "角色权限关联表",
timestamps: true,
createdAt: "created_at",
updatedAt: false
})
export default class RolePermission extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@ForeignKey(() => Role)
@Column({
type: DataType.BIGINT,
allowNull: false,
field: "role_id",
comment: "角色ID"
})
roleId!: number;
@ForeignKey(() => Permission)
@Column({
type: DataType.BIGINT,
allowNull: false,
field: "permission_id",
comment: "权限ID"
})
permissionId!: number;
@BelongsTo(() => Role)
role!: Role;
@BelongsTo(() => Permission)
permission!: Permission;
@CreatedAt
created_at!: Date;
}
+61
View File
@@ -0,0 +1,61 @@
import { Column, Model, Table, DataType, CreatedAt } from "sequelize-typescript";
@Table({
tableName: "sys_dict",
comment: "系统字典表",
timestamps: true,
createdAt: "created_at",
updatedAt: false
})
export default class SysDict extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@Column({
type: DataType.STRING(64),
allowNull: false,
field: "dict_type",
comment: "字典类型"
})
dictType!: string;
@Column({
type: DataType.STRING(128),
allowNull: false,
field: "dict_label",
comment: "字典标签"
})
dictLabel!: string;
@Column({
type: DataType.STRING(128),
allowNull: false,
field: "dict_value",
comment: "字典键值"
})
dictValue!: string;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
field: "sort_order",
comment: "排序(数值越小越靠前)"
})
sortOrder!: number;
@Column({
type: DataType.SMALLINT,
allowNull: false,
defaultValue: 1,
comment: "状态: 0-禁用 1-启用"
})
status!: number;
@CreatedAt
created_at!: Date;
}
+93
View File
@@ -0,0 +1,93 @@
import { Column, Model, Table, DataType, CreatedAt, HasMany } from "sequelize-typescript";
import SysRoleMenu from "./sysRoleMenu.entity";
@Table({
tableName: "sys_menu",
comment: "系统菜单表",
timestamps: true,
createdAt: "created_at",
updatedAt: false
})
export default class SysMenu extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@Column({
type: DataType.BIGINT,
field: "parent_id",
comment: "父菜单ID(NULL表示一级菜单)"
})
parentId!: number;
@Column({
type: DataType.STRING(64),
allowNull: false,
field: "menu_name",
comment: "菜单名称"
})
menuName!: string;
@Column({
type: DataType.STRING(256),
field: "menu_path",
comment: "菜单路由路径"
})
menuPath!: string;
@Column({
type: DataType.STRING(256),
field: "component",
comment: "组件路径"
})
component!: string;
@Column({
type: DataType.STRING(64),
field: "icon",
comment: "菜单图标"
})
icon!: string;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
field: "sort_order",
comment: "排序(数值越小越靠前)"
})
sortOrder!: number;
@Column({
type: DataType.SMALLINT,
allowNull: false,
defaultValue: 1,
field: "menu_type",
comment: "菜单类型: 1-目录 2-菜单 3-按钮"
})
menuType!: number;
@Column({
type: DataType.STRING(128),
field: "permission",
comment: "权限标识(按钮类型使用)"
})
permission!: string;
@Column({
type: DataType.SMALLINT,
allowNull: false,
defaultValue: 1,
comment: "是否可见: 0-隐藏 1-显示"
})
visible!: number;
@HasMany(() => SysRoleMenu)
sysRoleMenus!: SysRoleMenu[];
@CreatedAt
created_at!: Date;
}
@@ -0,0 +1,62 @@
import { Column, Model, Table, DataType, CreatedAt } from "sequelize-typescript";
@Table({
tableName: "sys_operation_log",
comment: "系统操作日志表",
timestamps: true,
createdAt: "created_at",
updatedAt: false
})
export default class SysOperationLog extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@Column({
type: DataType.BIGINT,
field: "user_id",
comment: "操作用户ID"
})
userId!: number;
@Column({
type: DataType.STRING(64),
field: "module",
comment: "操作模块"
})
module!: string;
@Column({
type: DataType.STRING(64),
field: "action",
comment: "操作动作"
})
action!: string;
@Column({
type: DataType.STRING(256),
field: "method",
comment: "请求方法(GET/POST/PUT/DELETE等)"
})
method!: string;
@Column({
type: DataType.TEXT,
field: "params",
comment: "请求参数(JSON格式)"
})
params!: string;
@Column({
type: DataType.STRING(64),
field: "ip",
comment: "操作IP(支持IPv6)"
})
ip!: string;
@CreatedAt
created_at!: Date;
}
+61
View File
@@ -0,0 +1,61 @@
import { Column, Model, Table, DataType, CreatedAt, HasMany } from "sequelize-typescript";
import SysUserRole from "./sysUserRole.entity";
import SysRoleMenu from "./sysRoleMenu.entity";
@Table({
tableName: "sys_role",
comment: "系统角色表",
timestamps: true,
createdAt: "created_at",
updatedAt: false
})
export default class SysRole extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@Column({
type: DataType.STRING(32),
allowNull: false,
unique: true,
field: "role_code",
comment: "角色编码(唯一标识)"
})
roleCode!: string;
@Column({
type: DataType.STRING(64),
allowNull: false,
field: "role_name",
comment: "角色名称"
})
roleName!: string;
@Column({
type: DataType.TEXT,
field: "description",
comment: "角色描述"
})
description!: string;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
field: "sort_order",
comment: "排序(数值越小越靠前)"
})
sortOrder!: number;
@HasMany(() => SysUserRole)
sysUserRoles!: SysUserRole[];
@HasMany(() => SysRoleMenu)
sysRoleMenus!: SysRoleMenu[];
@CreatedAt
created_at!: Date;
}
+36
View File
@@ -0,0 +1,36 @@
import { Column, Model, Table, DataType, ForeignKey, BelongsTo } from "sequelize-typescript";
import SysRole from "./sysRole.entity";
import SysMenu from "./sysMenu.entity";
@Table({
tableName: "sys_role_menu",
comment: "系统角色菜单关联表",
timestamps: false
})
export default class SysRoleMenu extends Model {
@ForeignKey(() => SysRole)
@Column({
type: DataType.BIGINT,
allowNull: false,
primaryKey: true,
field: "role_id",
comment: "角色ID"
})
roleId!: number;
@ForeignKey(() => SysMenu)
@Column({
type: DataType.BIGINT,
allowNull: false,
primaryKey: true,
field: "menu_id",
comment: "菜单ID"
})
menuId!: number;
@BelongsTo(() => SysRole)
role!: SysRole;
@BelongsTo(() => SysMenu)
menu!: SysMenu;
}
@@ -1,14 +1,14 @@
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, HasMany } from "sequelize-typescript";
import UserRole from "./userRole.entity";
import SysUserRole from "./sysUserRole.entity";
@Table({
tableName: "users",
comment: "用户表",
tableName: "sys_user",
comment: "系统用户表",
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at"
})
export default class User extends Model {
export default class SysUser extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
@@ -17,7 +17,7 @@ export default class User extends Model {
id!: number;
@Column({
type: DataType.STRING(50),
type: DataType.STRING(64),
allowNull: false,
unique: true,
comment: "用户名"
@@ -25,25 +25,20 @@ export default class User extends Model {
username!: string;
@Column({
type: DataType.STRING(255),
type: DataType.STRING(256),
allowNull: false,
field: "password_hash",
comment: "加密密码(bcrypt)"
})
password!: string;
passwordHash!: string;
@Column({
type: DataType.STRING(50),
field: "nickname",
comment: "昵称"
type: DataType.STRING(64),
allowNull: false,
field: "real_name",
comment: "真实姓名"
})
nickname!: string;
@Column({
type: DataType.STRING(100),
field: "email",
comment: "邮箱"
})
email!: string;
realName!: string;
@Column({
type: DataType.STRING(20),
@@ -53,14 +48,14 @@ export default class User extends Model {
phone!: string;
@Column({
type: DataType.STRING(255),
field: "avatar",
comment: "头像URL"
type: DataType.STRING(128),
field: "email",
comment: "邮箱"
})
avatar!: string;
email!: string;
@Column({
type: DataType.TINYINT,
type: DataType.SMALLINT,
allowNull: false,
defaultValue: 1,
comment: "状态: 0-禁用 1-启用"
@@ -74,15 +69,8 @@ export default class User extends Model {
})
lastLoginAt!: Date;
@Column({
type: DataType.STRING(45),
field: "last_login_ip",
comment: "最后登录IP(支持IPv6)"
})
lastLoginIp!: string;
@HasMany(() => UserRole)
userRoles!: UserRole[];
@HasMany(() => SysUserRole)
sysUserRoles!: SysUserRole[];
@CreatedAt
created_at!: Date;
+36
View File
@@ -0,0 +1,36 @@
import { Column, Model, Table, DataType, ForeignKey, BelongsTo } from "sequelize-typescript";
import SysUser from "./sysUser.entity";
import SysRole from "./sysRole.entity";
@Table({
tableName: "sys_user_role",
comment: "系统用户角色关联表",
timestamps: false
})
export default class SysUserRole extends Model {
@ForeignKey(() => SysUser)
@Column({
type: DataType.BIGINT,
allowNull: false,
primaryKey: true,
field: "user_id",
comment: "用户ID"
})
userId!: number;
@ForeignKey(() => SysRole)
@Column({
type: DataType.BIGINT,
allowNull: false,
primaryKey: true,
field: "role_id",
comment: "角色ID"
})
roleId!: number;
@BelongsTo(() => SysUser)
user!: SysUser;
@BelongsTo(() => SysRole)
role!: SysRole;
}
-46
View File
@@ -1,46 +0,0 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import User from "./user.entity";
import Role from "./role.entity";
@Table({
tableName: "user_roles",
comment: "用户角色关联表",
timestamps: true,
createdAt: "created_at",
updatedAt: false
})
export default class UserRole extends Model {
@Column({
type: DataType.BIGINT,
autoIncrement: true,
primaryKey: true
})
id!: number;
@ForeignKey(() => User)
@Column({
type: DataType.BIGINT,
allowNull: false,
field: "user_id",
comment: "用户ID"
})
userId!: number;
@ForeignKey(() => Role)
@Column({
type: DataType.BIGINT,
allowNull: false,
field: "role_id",
comment: "角色ID"
})
roleId!: number;
@BelongsTo(() => User)
user!: User;
@BelongsTo(() => Role)
role!: Role;
@CreatedAt
created_at!: Date;
}
+3
View File
@@ -5,6 +5,9 @@ import { TokenUtil } from '../util/token';
@Provide()
export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
// 中间件匹配范围:全局生效,但跳过公开接口
ignore = ['/api/auth/*', '/api/health'];
@Inject()
tokenUtil!: TokenUtil;
+42 -50
View File
@@ -2,56 +2,41 @@ import { Middleware, IMiddleware } from '@midwayjs/core';
import { Context, NextFunction } from '@midwayjs/koa';
import { Provide, Inject } from '@midwayjs/core';
import { InjectRepository } from '@midwayjs/sequelize';
import OperationLog from '../entity/operationLog.entity';
import SysOperationLog from '../entity/sysOperationLog.entity';
@Provide()
export class LogMiddleware implements IMiddleware<Context, NextFunction> {
// 中间件匹配范围:全局生效,跳过日志查询、健康检查与登录(登录由内部记录)
ignore = ['/api/logs/*', '/api/health', '/api/auth/login'];
@InjectRepository(OperationLog)
logModel!: typeof OperationLog;
@InjectRepository(SysOperationLog)
logModel!: typeof SysOperationLog;
resolve() {
return async (ctx: Context, next: NextFunction) => {
const startTime = Date.now();
let logData: any = {};
await next();
try {
logData = {
method: ctx.method,
url: ctx.url,
ipAddress: ctx.ip,
userAgent: ctx.header['user-agent'],
requestParams: JSON.stringify(ctx.request.body || {}),
};
if (ctx.state.user) {
logData.userId = ctx.state.user.userId;
logData.username = ctx.state.user.username;
}
await next();
logData.statusCode = ctx.status;
logData.duration = Date.now() - startTime;
if (['POST', 'PUT', 'DELETE'].includes(ctx.method)) {
logData.action = this.getActionDescription(ctx);
logData.module = this.getModuleFromUrl(ctx.url);
this.saveLog(logData).catch(err => {
console.error('保存操作日志失败:', err);
});
}
} catch (error: any) {
logData.statusCode = 500;
logData.duration = Date.now() - startTime;
logData.errorMessage = error.message;
this.saveLog(logData).catch(() => {});
throw error;
// 仅记录写操作,且要求登录用户存在
if (!['POST', 'PUT', 'DELETE'].includes(ctx.method)) {
return;
}
if (!ctx.state.user) {
return;
}
const logData = {
userId: ctx.state.user.userId,
module: this.getModuleFromUrl(ctx.url),
action: this.getActionDescription(ctx),
method: ctx.method,
params: JSON.stringify(ctx.request.body || {}),
ip: ctx.ip,
};
this.saveLog(logData).catch(err => {
console.error('保存操作日志失败:', err);
});
};
}
@@ -59,23 +44,30 @@ export class LogMiddleware implements IMiddleware<Context, NextFunction> {
const actionMap: Record<string, Record<string, string>> = {
'/api/auth/login': { POST: '用户登录' },
'/api/auth/logout': { POST: '用户登出' },
'/api/user': {
'/api/users': {
POST: '创建用户',
PUT: '更新用户',
DELETE: '删除用户',
},
'/api/role': {
'/api/roles': {
POST: '创建角色',
PUT: '更新角色',
DELETE: '删除角色',
},
'/api/settings': {
PUT: '修改系统设置',
'/api/menus': {
POST: '创建菜单',
PUT: '更新菜单',
DELETE: '删除菜单',
},
'/api/dicts': {
POST: '创建字典',
PUT: '更新字典',
DELETE: '删除字典',
},
};
const basePath = ctx.url.split('?')[0];
if (actionMap[basePath]?.[ctx.method]) {
return actionMap[basePath][ctx.method];
}
@@ -92,10 +84,10 @@ export class LogMiddleware implements IMiddleware<Context, NextFunction> {
private getModuleFromUrl(url: string): string {
const moduleMap: Record<string, string> = {
'/api/auth': '认证',
'/api/user': '用户管理',
'/api/role': '角色管理',
'/api/settings': '系统设置',
'/api/log': '日志管理',
'/api/users': '用户管理',
'/api/roles': '角色管理',
'/api/menus': '菜单管理',
'/api/dicts': '数据字典',
'/api/logs': '日志管理',
'/api/dashboard': '仪表盘',
};
+48 -24
View File
@@ -1,14 +1,37 @@
import { Middleware, IMiddleware } from '@midwayjs/core';
import { Context, NextFunction } from '@midwayjs/koa';
import { Provide, Inject } from '@midwayjs/core';
import { InjectRepository } from '@midwayjs/sequelize';
import { PermissionService } from '../service/permission.service';
import SysUserRole from '../entity/sysUserRole.entity';
import SysRole from '../entity/sysRole.entity';
// 请求路径前缀 → 菜单权限标识映射
const PERMISSION_MAP: Record<string, string> = {
'/api/users': 'system:user',
'/api/roles': 'system:role',
'/api/menus': 'system:menu',
'/api/dicts': 'system:dict',
'/api/logs': 'system:log',
};
// 超级管理员角色编码,拥有全部权限
const ADMIN_ROLE_CODE = 'ADMIN';
@Provide()
export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
// 中间件匹配范围:全局生效,跳过公开接口与仪表盘
ignore = ['/api/auth/*', '/api/health', '/api/dashboard/*'];
@Inject()
permissionService!: PermissionService;
@InjectRepository(SysUserRole)
userRoleModel!: typeof SysUserRole;
@InjectRepository(SysRole)
roleModel!: typeof SysRole;
resolve() {
return async (ctx: Context, next: NextFunction) => {
const user = ctx.state.user;
@@ -22,17 +45,21 @@ export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
return;
}
// 获取当前请求路径和方法,转换为权限码
// 例如:GET /api/user/list -> user:list
const permissionCode = this.extractPermissionCode(ctx);
// 超级管理员拥有所有权限
if (user.roles && user.roles.includes('super_admin')) {
// 路径前缀匹配,未匹配到任何权限映射的请求直接放行
const permissionCode = this.matchPermissionCode(ctx.path);
if (!permissionCode) {
await next();
return;
}
// 检查用户是否拥有该权限
// 查询用户角色编码,超级管理员直接放行
const roleCodes = await this.getUserRoleCodes(user.userId);
if (roleCodes.includes(ADMIN_ROLE_CODE)) {
await next();
return;
}
// 检查用户菜单中是否包含该权限标识
const hasPermission = await this.permissionService.checkUserPermission(
user.userId,
permissionCode
@@ -51,24 +78,21 @@ export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
};
}
private extractPermissionCode(ctx: Context): string {
const url = ctx.path.replace('/api/', '').replace(/^\//, '');
const method = ctx.method.toLowerCase();
const methodMap: Record<string, string> = {
post: 'create',
put: 'update',
delete: 'delete',
get: 'list',
};
const action = methodMap[method] || method;
const parts = url.split('/');
if (parts.length >= 2) {
return `${parts[0]}:${action}`;
private matchPermissionCode(path: string): string {
for (const [prefix, code] of Object.entries(PERMISSION_MAP)) {
if (path === prefix || path.startsWith(prefix + '/')) {
return code;
}
}
return '';
}
return `${url}:${action}`;
private async getUserRoleCodes(userId: number): Promise<string[]> {
const userRoles = await this.userRoleModel.findAll({
where: { userId },
include: [this.roleModel],
});
return userRoles.map(ur => ur.role?.roleCode).filter(Boolean) as string[];
}
}
+27 -47
View File
@@ -1,43 +1,33 @@
import { Provide, Inject } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { InjectRepository } from "@midwayjs/sequelize";
import User from "../entity/user.entity";
import UserRole from "../entity/userRole.entity";
import Role from "../entity/role.entity";
import RolePermission from "../entity/rolePermission.entity";
import Permission from "../entity/permission.entity";
import SysUser from "../entity/sysUser.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { TokenUtil } from "../util/token";
import { comparePassword } from "../util/crypto";
import { PermissionService } from "./permission.service";
@Provide()
export class AuthService {
@Inject()
tokenUtil!: TokenUtil;
@InjectRepository(User)
userModel!: typeof User;
@Inject()
permissionService!: PermissionService;
@InjectRepository(UserRole)
userRoleModel!: typeof UserRole;
@InjectRepository(SysUser)
userModel!: typeof SysUser;
@InjectRepository(Role)
roleModel!: typeof Role;
@InjectRepository(SysUserRole)
userRoleModel!: typeof SysUserRole;
@InjectRepository(RolePermission)
rolePermissionModel!: typeof RolePermission;
@InjectRepository(Permission)
permissionModel!: typeof Permission;
@InjectRepository(SysRole)
roleModel!: typeof SysRole;
async login(username: string, password: string, ctx: Context): Promise<any> {
const user = await this.userModel.findOne({
where: { username },
include: [
{
model: this.userRoleModel,
include: [this.roleModel]
}
]
where: { username }
});
if (!user) {
@@ -48,15 +38,20 @@ export class AuthService {
throw new Error("账户已被禁用");
}
const isPasswordValid = await comparePassword(password, user.password);
const isPasswordValid = await comparePassword(password, user.passwordHash);
if (!isPasswordValid) {
throw new Error("密码错误");
}
const roles = (user as any).userRoles?.map((ur: any) => ur.role) || [];
const roleIds = roles.map((r: Role) => r.id);
// 获取用户角色编码
const userRoles = await this.userRoleModel.findAll({
where: { userId: user.id },
include: [this.roleModel]
});
const roleCodes = userRoles.map(ur => ur.role?.roleCode).filter(Boolean) as string[];
const permissions = await this.getUserPermissions(roleIds);
// 获取用户菜单树
const menus = await this.permissionService.getUserMenus(user.id);
const accessToken = this.tokenUtil.generateAccessToken({
userId: user.id,
@@ -68,8 +63,7 @@ export class AuthService {
});
await user.update({
lastLoginAt: new Date(),
lastLoginIp: ctx.ip
lastLoginAt: new Date()
});
return {
@@ -78,11 +72,10 @@ export class AuthService {
userInfo: {
id: user.id,
username: user.username,
nickname: user.nickname,
avatar: user.avatar,
roles: roles.map((r: Role) => r.code),
permissions: permissions.map((p: Permission) => p.code)
}
realName: user.realName
},
roles: roleCodes,
menus
};
}
@@ -111,17 +104,4 @@ export class AuthService {
throw new Error("Refresh Token无效或已过期");
}
}
private async getUserPermissions(roleIds: number[]): Promise<Permission[]> {
if (!roleIds || roleIds.length === 0) {
return [];
}
const rolePermissions = await this.rolePermissionModel.findAll({
where: { roleId: roleIds },
include: [this.permissionModel]
});
return [...new Map(rolePermissions.map((rp: any) => [rp.permission.id, rp.permission])).values()];
}
}
+7 -10
View File
@@ -1,17 +1,14 @@
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import User from "../entity/user.entity";
import OperationLog from "../entity/operationLog.entity";
import SysUser from "../entity/sysUser.entity";
import SysOperationLog from "../entity/sysOperationLog.entity";
import dayjs from "dayjs";
import { Op } from "sequelize";
@Provide()
export class DashboardService {
@InjectRepository(User)
userModel!: typeof User;
@InjectRepository(OperationLog)
logModel!: typeof OperationLog;
@InjectRepository(SysOperationLog)
logModel!: typeof SysOperationLog;
async getDashboardStats(): Promise<any> {
const mockDeviceTotal = 1256;
@@ -103,15 +100,15 @@ export class DashboardService {
const logs = await this.logModel.findAll({
order: [["created_at", "DESC"]],
limit,
attributes: ["id", "username", "action", "module", "ip_address", "created_at"]
attributes: ["id", "action", "module", "ip", "created_at"]
});
return logs.map((log) => ({
id: log.id,
user: log.username || "系统",
user: String(log.userId || "系统"),
action: log.action,
module: log.module,
ip: log.ipAddress,
ip: log.ip,
time: dayjs(log.created_at).format("MM-DD HH:mm")
}));
} catch (error) {
+145
View File
@@ -0,0 +1,145 @@
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import SysDict from "../entity/sysDict.entity";
import { Op } from "sequelize";
@Provide()
export class DictService {
@InjectRepository(SysDict)
dictModel!: typeof SysDict;
async list(query: {
page?: number;
pageSize?: number;
dictType?: string;
keyword?: string;
status?: number;
}): Promise<any> {
const page = query.page || 1;
const pageSize = query.pageSize || 10;
const where: any = {};
if (query.dictType) {
where.dictType = query.dictType;
}
if (query.keyword) {
where[Op.or] = [
{ dictLabel: { [Op.like]: `%${query.keyword}%` } },
{ dictValue: { [Op.like]: `%${query.keyword}%` } },
];
}
if (query.status !== undefined && query.status !== null) {
where.status = query.status;
}
const { count, rows } = await this.dictModel.findAndCountAll({
where,
order: [["sort_order", "ASC"], ["id", "ASC"]],
limit: Number(pageSize),
offset: (Number(page) - 1) * Number(pageSize),
});
return {
code: 200,
message: "获取字典列表成功",
data: {
list: rows,
total: count,
page: Number(page),
pageSize: Number(pageSize),
},
};
}
async types(): Promise<any> {
const rows = await this.dictModel.findAll({
attributes: ["dictType"],
group: ["dictType"],
order: [["dictType", "ASC"]],
});
const types = rows.map(row => row.dictType);
return {
code: 200,
message: "获取字典类型成功",
data: types,
};
}
async create(data: any): Promise<any> {
const { dictType, dictLabel, dictValue, sortOrder = 0, status = 1 } = data;
if (!dictType || !dictLabel || !dictValue) {
return {
code: 400,
message: "字典类型、字典标签、字典键值不能为空",
data: null,
};
}
const dict = await this.dictModel.create({
dict_type: dictType,
dict_label: dictLabel,
dict_value: dictValue,
sort_order: sortOrder,
status,
});
return {
code: 200,
message: "创建字典成功",
data: dict,
};
}
async update(id: string, data: any): Promise<any> {
const dict = await this.dictModel.findByPk(Number(id));
if (!dict) {
return {
code: 404,
message: "字典不存在",
data: null,
};
}
const updateData: any = {};
if (data.dictType !== undefined) updateData.dict_type = data.dictType;
if (data.dictLabel !== undefined) updateData.dict_label = data.dictLabel;
if (data.dictValue !== undefined) updateData.dict_value = data.dictValue;
if (data.sortOrder !== undefined) updateData.sort_order = data.sortOrder;
if (data.status !== undefined) updateData.status = data.status;
await dict.update(updateData);
return {
code: 200,
message: "更新字典成功",
data: dict,
};
}
async remove(id: string): Promise<any> {
const dict = await this.dictModel.findByPk(Number(id));
if (!dict) {
return {
code: 404,
message: "字典不存在",
data: null,
};
}
await dict.destroy();
return {
code: 200,
message: "删除字典成功",
data: null,
};
}
}
+29 -26
View File
@@ -1,22 +1,22 @@
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import OperationLog from "../entity/operationLog.entity";
import SysOperationLog from "../entity/sysOperationLog.entity";
import { Op } from "sequelize";
import dayjs from "dayjs";
@Provide()
export class LogService {
@InjectRepository(OperationLog)
logModel!: typeof OperationLog;
@InjectRepository(SysOperationLog)
logModel!: typeof SysOperationLog;
async list(query: {
page?: number;
pageSize?: number;
moduleName?: string;
actionType?: string;
userId?: string | number;
module?: string;
action?: string;
startDate?: string;
endDate?: string;
operator?: string;
}): Promise<any> {
const page = query.page || 1;
const pageSize = query.pageSize || 10;
@@ -67,19 +67,22 @@ export class LogService {
limit: 10000,
});
const headers = ["序号", "操作人", "操作类型", "操作模块", "请求方法", "请求URL", "IP地址", "状态码", "耗时(ms)", "操作时间"];
const rows = logs.map((log: any, index: number) => [
index + 1,
log.username || "-",
log.action || "-",
log.module || "-",
log.method || "-",
(log.url || "").replace(/,/g, ","),
log.ipAddress || "-",
log.statusCode || "-",
log.duration || 0,
dayjs(log.created_at).format("YYYY-MM-DD HH:mm:ss"),
]);
const headers = ["序号", "操作人ID", "操作模块", "操作动作", "请求方法", "请求参数", "IP地址", "操作时间"];
const rows = logs.map((log: any, index: number) => {
const paramsText = (log.params || "").replace(/,/g, ",");
const paramsCell = paramsText.length > 200 ? paramsText.substring(0, 200) + "..." : paramsText;
return [
index + 1,
log.userId || "-",
log.module || "-",
log.action || "-",
log.method || "-",
paramsCell,
log.ip || "-",
dayjs(log.created_at).format("YYYY-MM-DD HH:mm:ss"),
];
});
const csvContent = [headers.join(","), ...rows.map(row => row.join(","))].join("\n");
@@ -100,18 +103,18 @@ export class LogService {
private buildWhereClause(query: any): any {
const where: any = {};
const { operator, actionType, moduleName, startDate, endDate } = query;
const { userId, module, action, startDate, endDate } = query;
if (operator) {
where.username = { [Op.like]: `%${operator}%` };
if (userId) {
where.userId = userId;
}
if (actionType) {
where.action = { [Op.like]: `%${actionType}%` };
if (module) {
where.module = { [Op.like]: `%${module}%` };
}
if (moduleName) {
where.module = { [Op.like]: `%${moduleName}%` };
if (action) {
where.action = { [Op.like]: `%${action}%` };
}
if (startDate || endDate) {
+158
View File
@@ -0,0 +1,158 @@
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import SysMenu from "../entity/sysMenu.entity";
@Provide()
export class MenuService {
@InjectRepository(SysMenu)
menuModel!: typeof SysMenu;
async getMenuTree(): Promise<any> {
const menus = await this.menuModel.findAll({
order: [["sort_order", "ASC"], ["id", "ASC"]],
});
const tree = this.buildMenuTree(menus.map(menu => menu.toJSON()));
return {
code: 200,
message: "获取菜单树成功",
data: tree,
};
}
async create(data: any): Promise<any> {
const {
parentId,
menuName,
menuPath,
component,
icon,
sortOrder = 0,
menuType = 2,
permission,
visible = 1,
} = data;
if (!menuName) {
return {
code: 400,
message: "菜单名称不能为空",
data: null,
};
}
const menu = await this.menuModel.create({
parent_id: parentId,
menu_name: menuName,
menu_path: menuPath,
component,
icon,
sort_order: sortOrder,
menu_type: menuType,
permission,
visible,
});
return {
code: 200,
message: "创建菜单成功",
data: menu,
};
}
async update(id: string, data: any): Promise<any> {
const menu = await this.menuModel.findByPk(Number(id));
if (!menu) {
return {
code: 404,
message: "菜单不存在",
data: null,
};
}
const updateData: any = {};
if (data.parentId !== undefined) updateData.parent_id = data.parentId;
if (data.menuName !== undefined) updateData.menu_name = data.menuName;
if (data.menuPath !== undefined) updateData.menu_path = data.menuPath;
if (data.component !== undefined) updateData.component = data.component;
if (data.icon !== undefined) updateData.icon = data.icon;
if (data.sortOrder !== undefined) updateData.sort_order = data.sortOrder;
if (data.menuType !== undefined) updateData.menu_type = data.menuType;
if (data.permission !== undefined) updateData.permission = data.permission;
if (data.visible !== undefined) updateData.visible = data.visible;
await menu.update(updateData);
return {
code: 200,
message: "更新菜单成功",
data: menu,
};
}
async remove(id: string): Promise<any> {
const menu = await this.menuModel.findByPk(Number(id));
if (!menu) {
return {
code: 404,
message: "菜单不存在",
data: null,
};
}
const childCount = await this.menuModel.count({
where: { parentId: Number(id) },
});
if (childCount > 0) {
return {
code: 400,
message: "存在子菜单,无法删除",
data: null,
};
}
await menu.destroy();
return {
code: 200,
message: "删除菜单成功",
data: null,
};
}
private buildMenuTree(menus: any[]): any[] {
const map = new Map<number, any>();
const tree: any[] = [];
for (const menu of menus) {
map.set(menu.id, {
id: menu.id,
parentId: menu.parentId,
menuName: menu.menuName,
menuPath: menu.menuPath,
component: menu.component,
icon: menu.icon,
sortOrder: menu.sortOrder,
menuType: menu.menuType,
permission: menu.permission,
visible: menu.visible,
children: [],
});
}
for (const menu of menus) {
const node = map.get(menu.id)!;
if (menu.parentId && map.has(menu.parentId)) {
map.get(menu.parentId)!.children.push(node);
} else {
tree.push(node);
}
}
return tree;
}
}
+79 -91
View File
@@ -1,110 +1,111 @@
import { Provide, Inject } from "@midwayjs/core";
import { Provide } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import User from "../entity/user.entity";
import UserRole from "../entity/userRole.entity";
import Role from "../entity/role.entity";
import RolePermission from "../entity/rolePermission.entity";
import Permission from "../entity/permission.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRoleMenu from "../entity/sysRoleMenu.entity";
import SysMenu from "../entity/sysMenu.entity";
@Provide()
export class PermissionService {
@InjectRepository(User)
userModel!: typeof User;
@InjectRepository(SysUserRole)
userRoleModel!: typeof SysUserRole;
@InjectRepository(UserRole)
userRoleModel!: typeof UserRole;
@InjectRepository(SysRoleMenu)
roleMenuModel!: typeof SysRoleMenu;
@InjectRepository(Role)
roleModel!: typeof Role;
@InjectRepository(SysMenu)
menuModel!: typeof SysMenu;
@InjectRepository(RolePermission)
rolePermissionModel!: typeof RolePermission;
@InjectRepository(Permission)
permissionModel!: typeof Permission;
async checkUserPermission(userId: number, permissionCode: string): Promise<boolean> {
/**
* 获取用户可见菜单树(用户角色 → 角色菜单 → 可见菜单,按 sort_order/id 升序去重)
*/
async getUserMenus(userId: number): Promise<any[]> {
const userRoles = await this.userRoleModel.findAll({
where: { userId },
include: [this.roleModel],
where: { userId }
});
const roleIds = userRoles.map(ur => ur.roleId);
if (!roleIds.length) {
return [];
}
const roleMenus = await this.roleMenuModel.findAll({
where: { roleId: roleIds },
include: [
{
model: this.menuModel,
where: { visible: 1 }
}
]
});
const menuMap = new Map<number, any>();
for (const rm of roleMenus) {
const menu = rm.menu;
if (menu && !menuMap.has(menu.id)) {
menuMap.set(menu.id, this.toMenuNode(menu));
}
}
const menus = Array.from(menuMap.values());
menus.sort((a, b) => a.sortOrder - b.sortOrder || a.id - b.id);
return this.buildMenuTree(menus);
}
/**
* 检查用户角色关联的菜单中是否存在指定权限标识的菜单
*/
async checkUserPermission(userId: number, permissionCode: string): Promise<boolean> {
const userRoles = await this.userRoleModel.findAll({
where: { userId }
});
const roleIds = userRoles.map(ur => ur.roleId);
if (!roleIds.length) {
return false;
}
const rolePermissions = await this.rolePermissionModel.findAll({
const roleMenus = await this.roleMenuModel.findAll({
where: { roleId: roleIds },
include: [
{
model: this.permissionModel,
where: { code: permissionCode },
},
],
});
return rolePermissions.some(rp => rp.permission?.code === permissionCode);
}
async getUserPermissions(userId: number): Promise<string[]> {
const permissions = await this.getUserPermissionTree(userId);
return this.flattenPermissions(permissions);
}
async getUserPermissionTree(userId: number): Promise<any[]> {
const userRoles = await this.userRoleModel.findAll({
where: { userId },
include: [
{
model: this.roleModel,
include: [
{
model: this.rolePermissionModel,
include: [this.permissionModel],
},
],
},
],
});
const allPermissions = new Map<number, any>();
for (const ur of userRoles) {
const role: any = ur.role;
if (role && role.rolePermissions) {
for (const rp of role.rolePermissions) {
if (rp.permission && !allPermissions.has(rp.permission.id)) {
allPermissions.set(rp.permission.id, rp.permission.toJSON());
}
model: this.menuModel,
where: { permission: permissionCode }
}
}
}
return this.buildPermissionTree(Array.from(allPermissions.values()));
}
async getAllPermissions(): Promise<any[]> {
const permissions = await this.permissionModel.findAll({
order: [["sort_order", "ASC"]],
]
});
return this.buildPermissionTree(permissions.map(p => p.toJSON()));
return roleMenus.some(rm => rm.menu?.permission === permissionCode);
}
private buildPermissionTree(permissions: any[]): any[] {
private toMenuNode(menu: SysMenu): any {
return {
id: menu.id,
parentId: menu.parentId,
menuName: menu.menuName,
menuPath: menu.menuPath,
component: menu.component,
icon: menu.icon,
sortOrder: menu.sortOrder,
menuType: menu.menuType,
permission: menu.permission,
visible: menu.visible,
children: []
};
}
private buildMenuTree(menus: any[]): any[] {
const map = new Map<number, any>();
const tree: any[] = [];
for (const perm of permissions) {
map.set(perm.id, { ...perm, children: [] });
for (const menu of menus) {
map.set(menu.id, { ...menu, children: [] });
}
for (const perm of permissions) {
const node = map.get(perm.id)!;
if (perm.parentId && map.has(perm.parentId)) {
map.get(perm.parentId)!.children.push(node);
for (const menu of menus) {
const node = map.get(menu.id)!;
if (menu.parentId && map.has(menu.parentId)) {
map.get(menu.parentId)!.children.push(node);
} else {
tree.push(node);
}
@@ -112,17 +113,4 @@ export class PermissionService {
return tree;
}
private flattenPermissions(tree: any[]): string[] {
const codes: string[] = [];
for (const node of tree) {
codes.push(node.code);
if (node.children?.length) {
codes.push(...this.flattenPermissions(node.children));
}
}
return codes;
}
}
+65 -60
View File
@@ -1,24 +1,24 @@
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import Role from "../entity/role.entity";
import UserRole from "../entity/userRole.entity";
import RolePermission from "../entity/rolePermission.entity";
import Permission from "../entity/permission.entity";
import SysRole from "../entity/sysRole.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRoleMenu from "../entity/sysRoleMenu.entity";
import SysMenu from "../entity/sysMenu.entity";
import { Op } from "sequelize";
@Provide()
export class RoleService {
@InjectRepository(Role)
roleModel!: typeof Role;
@InjectRepository(SysRole)
roleModel!: typeof SysRole;
@InjectRepository(UserRole)
userRoleModel!: typeof UserRole;
@InjectRepository(SysUserRole)
userRoleModel!: typeof SysUserRole;
@InjectRepository(RolePermission)
rolePermissionModel!: typeof RolePermission;
@InjectRepository(SysRoleMenu)
roleMenuModel!: typeof SysRoleMenu;
@InjectRepository(Permission)
permissionModel!: typeof Permission;
@InjectRepository(SysMenu)
menuModel!: typeof SysMenu;
async list(query: {
page?: number;
@@ -32,20 +32,17 @@ export class RoleService {
const whereClause: any = {};
if (query.keyword) {
whereClause[Op.or] = [
{ name: { [Op.like]: `%${query.keyword}%` } },
{ code: { [Op.like]: `%${query.keyword}%` } },
{ roleCode: { [Op.like]: `%${query.keyword}%` } },
{ roleName: { [Op.like]: `%${query.keyword}%` } },
];
}
const { count, rows } = await this.roleModel.findAndCountAll({
where: whereClause,
attributes: ["id", "roleCode", "roleName", "description", "sortOrder"],
offset,
limit: pageSize,
order: [["sort_order", "ASC"], ["created_at", "DESC"]],
include: [{
model: this.rolePermissionModel,
include: [this.permissionModel],
}],
});
for (const role of rows) {
@@ -80,12 +77,7 @@ export class RoleService {
}
async detail(id: string): Promise<any> {
const role = await this.roleModel.findByPk(Number(id), {
include: [{
model: this.rolePermissionModel,
include: [this.permissionModel],
}],
});
const role = await this.roleModel.findByPk(Number(id));
if (!role) {
return {
@@ -108,10 +100,10 @@ export class RoleService {
}
async create(data: any): Promise<any> {
const { name, code, description, status = 1, sortOrder = 0 } = data;
const { roleCode, roleName, description, sortOrder = 0 } = data;
const existingRole = await this.roleModel.findOne({
where: { code },
where: { roleCode },
});
if (existingRole) {
@@ -123,10 +115,9 @@ export class RoleService {
}
const role = await this.roleModel.create({
name,
code,
roleCode,
roleName,
description,
status,
sort_order: sortOrder,
});
@@ -148,9 +139,9 @@ export class RoleService {
};
}
if (data.code && data.code !== role.code) {
if (data.roleCode && data.roleCode !== role.roleCode) {
const existingRole = await this.roleModel.findOne({
where: { code: data.code },
where: { roleCode: data.roleCode },
});
if (existingRole) {
@@ -163,9 +154,9 @@ export class RoleService {
}
const updateData: any = {};
if (data.name !== undefined) updateData.name = data.name;
if (data.roleCode !== undefined) updateData.roleCode = data.roleCode;
if (data.roleName !== undefined) updateData.roleName = data.roleName;
if (data.description !== undefined) updateData.description = data.description;
if (data.status !== undefined) updateData.status = data.status;
if (data.sortOrder !== undefined) updateData.sort_order = data.sortOrder;
await role.update(updateData);
@@ -200,7 +191,7 @@ export class RoleService {
};
}
await this.rolePermissionModel.destroy({
await this.roleMenuModel.destroy({
where: { roleId: Number(id) },
});
@@ -213,7 +204,7 @@ export class RoleService {
};
}
async getPermissions(roleId: number): Promise<any> {
async getMenus(roleId: number): Promise<any> {
const role = await this.roleModel.findByPk(roleId);
if (!role) {
@@ -224,28 +215,32 @@ export class RoleService {
};
}
const rolePermissions = await this.rolePermissionModel.findAll({
const roleMenus = await this.roleMenuModel.findAll({
where: { roleId },
});
const assignedPermissionIds = rolePermissions.map(rp => rp.permissionId);
const assignedMenuIds = roleMenus.map(rm => rm.menuId);
const allPermissions = await this.permissionModel.findAll({
const menus = await this.menuModel.findAll({
where: { menuType: { [Op.in]: [1, 2] } },
order: [["sort_order", "ASC"], ["id", "ASC"]],
});
const permissionTree = this.buildPermissionTree(
allPermissions.map(p => p.toJSON()),
assignedPermissionIds
const menuTree = this.buildMenuTree(
menus.map(m => m.toJSON()),
assignedMenuIds
);
return {
code: 200,
message: "获取角色权限成功",
data: permissionTree,
message: "获取角色菜单成功",
data: {
menuTree,
menuIds: assignedMenuIds,
},
};
}
async updatePermissions(roleId: number, permissionIds: number[]): Promise<any> {
async updateMenus(roleId: number, menuIds: number[]): Promise<any> {
const role = await this.roleModel.findByPk(roleId);
if (!role) {
@@ -256,47 +251,57 @@ export class RoleService {
};
}
await this.rolePermissionModel.destroy({
await this.roleMenuModel.destroy({
where: { roleId },
});
if (permissionIds && permissionIds.length > 0) {
const rolePermissions = permissionIds.map(permissionId => ({
if (menuIds && menuIds.length > 0) {
const roleMenus = menuIds.map(menuId => ({
roleId,
permissionId,
menuId,
}));
await this.rolePermissionModel.bulkCreate(rolePermissions);
await this.roleMenuModel.bulkCreate(roleMenus);
}
return {
code: 200,
message: "权限更新成功",
message: "菜单更新成功",
data: null,
};
}
private buildPermissionTree(permissions: any[], checkedIds: number[]): any[] {
const map = new Map<number, any>();
private buildMenuTree(menus: any[], checkedIds: number[]): any[] {
const nodeMap = new Map<number, any>();
const menuIdSet = new Set(menus.map(m => m.id));
const tree: any[] = [];
for (const perm of permissions) {
map.set(perm.id, {
...perm,
for (const menu of menus) {
nodeMap.set(menu.id, {
...menu,
children: [],
checked: checkedIds.includes(perm.id),
checked: checkedIds.includes(menu.id),
});
}
for (const perm of permissions) {
const node = map.get(perm.id)!;
if (perm.parent_id && map.has(perm.parent_id)) {
map.get(perm.parent_id)!.children.push(node);
for (const menu of menus) {
const node = nodeMap.get(menu.id)!;
const parent = menu.parentId ? nodeMap.get(menu.parentId) : undefined;
// 仅当父节点为一级节点时挂为其子节点,保证输出两级树;
// 父节点不在列表或父节点本身是二级节点时,提升为一级节点
if (parent && !menuIdSet.has(parent.parentId)) {
parent.children.push(node);
} else {
tree.push(node);
}
}
// 一级节点已在查询时按 sort_order 排序,此处对子节点排序
for (const node of tree) {
node.children.sort((a: any, b: any) => a.sortOrder - b.sortOrder);
}
return tree;
}
}
-69
View File
@@ -1,69 +0,0 @@
import { Provide } from '@midwayjs/core';
@Provide()
export class SystemService {
private settingsCache: Map<string, any> = new Map();
constructor() {
this.initDefaultSettings();
}
private initDefaultSettings() {
this.settingsCache.set('basic', {
siteName: '智能物联网设备管理系统',
logo: '',
sessionTimeout: 120,
});
this.settingsCache.set('security', {
passwordMinLength: 6,
maxLoginAttempts: 5,
lockoutDuration: 30,
ipWhitelist: [] as string[],
});
this.settingsCache.set('notification', {
enableEmail: false,
emailSmtpConfig: null as any,
});
}
async getSettings(): Promise<any> {
return {
code: 200,
message: '获取系统设置成功',
data: {
basic: this.settingsCache.get('basic'),
security: this.settingsCache.get('security'),
notification: this.settingsCache.get('notification'),
},
};
}
async updateBasicSettings(data: any): Promise<any> {
const current = this.settingsCache.get('basic') || {};
this.settingsCache.set('basic', { ...current, ...data });
return {
code: 200,
message: '基本设置保存成功',
};
}
async updateSecuritySettings(data: any): Promise<any> {
const current = this.settingsCache.get('security') || {};
this.settingsCache.set('security', { ...current, ...data });
return {
code: 200,
message: '安全设置保存成功',
};
}
async updateNotificationSettings(data: any): Promise<any> {
const current = this.settingsCache.get('notification') || {};
this.settingsCache.set('notification', { ...current, ...data });
return {
code: 200,
message: '通知设置保存成功',
};
}
}
+71 -36
View File
@@ -1,22 +1,22 @@
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import User from "../entity/user.entity";
import UserRole from "../entity/userRole.entity";
import Role from "../entity/role.entity";
import SysUser from "../entity/sysUser.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { Op } from "sequelize";
import { hashPassword } from "../util/crypto";
@Provide()
export class UserService {
@InjectRepository(User)
userModel!: Repository<User>;
@InjectRepository(SysUser)
userModel!: Repository<SysUser>;
@InjectRepository(UserRole)
userRoleModel!: Repository<UserRole>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(Role)
roleModel!: Repository<Role>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
async list(query: { page?: any; pageSize?: any; keyword?: string; status?: number; roleId?: number }): Promise<any> {
const page = Number(query.page) || 1;
@@ -24,7 +24,7 @@ export class UserService {
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ username: { [Op.like]: `%${query.keyword}%` } }, { nickname: { [Op.like]: `%${query.keyword}%` } }, { phone: { [Op.like]: `%${query.keyword}%` } }];
where[Op.or] = [{ username: { [Op.like]: `%${query.keyword}%` } }, { realName: { [Op.like]: `%${query.keyword}%` } }, { phone: { [Op.like]: `%${query.keyword}%` } }];
}
if (query.status !== undefined) {
@@ -33,11 +33,11 @@ export class UserService {
const includeOptions: any = [
{
model: UserRole,
model: SysUserRole,
required: !!query.roleId,
include: [
{
model: Role,
model: SysRole,
where: query.roleId ? { id: query.roleId } : {},
required: !!query.roleId
}
@@ -57,7 +57,8 @@ export class UserService {
const list = rows.map((user) => {
const u = user.toJSON();
delete u.password;
delete u.passwordHash;
delete u.password_hash;
return u;
});
@@ -77,8 +78,8 @@ export class UserService {
const user = await this.userModel.findByPk(id, {
include: [
{
model: UserRole,
include: [Role]
model: SysUserRole,
include: [SysRole]
}
]
});
@@ -92,7 +93,8 @@ export class UserService {
}
const userData = user.toJSON();
delete userData.password;
delete userData.passwordHash;
delete userData.password_hash;
return {
code: 200,
@@ -102,7 +104,7 @@ export class UserService {
}
async create(data: any): Promise<any> {
const { username, password, nickname, email, phone, roleIds, status = 1 } = data;
const { username, password, realName, email, phone, roleIds, status = 1 } = data;
const existingUser = await this.userModel.findOne({
where: { username }
@@ -120,8 +122,8 @@ export class UserService {
const user = await this.userModel.create({
username,
password: hashedPassword,
nickname,
passwordHash: hashedPassword,
realName,
email,
phone,
status
@@ -134,14 +136,15 @@ export class UserService {
const createdUser = await this.userModel.findByPk(user.id, {
include: [
{
model: UserRole,
include: [Role]
model: SysUserRole,
include: [SysRole]
}
]
});
const userData = createdUser!.toJSON();
delete userData.password;
delete userData.passwordHash;
delete userData.password_hash;
return {
code: 200,
@@ -163,14 +166,13 @@ export class UserService {
const updateData: any = {};
if (data.nickname !== undefined) updateData.nickname = data.nickname;
if (data.realName !== undefined) updateData.realName = data.realName;
if (data.email !== undefined) updateData.email = data.email;
if (data.phone !== undefined) updateData.phone = data.phone;
if (data.avatar !== undefined) updateData.avatar = data.avatar;
if (data.status !== undefined) updateData.status = data.status;
if (data.password) {
updateData.password = await hashPassword(data.password);
updateData.passwordHash = await hashPassword(data.password);
}
await user.update(updateData);
@@ -182,14 +184,15 @@ export class UserService {
const updatedUser = await this.userModel.findByPk(id, {
include: [
{
model: UserRole,
include: [Role]
model: SysUserRole,
include: [SysRole]
}
]
});
const userData = updatedUser!.toJSON();
delete userData.password;
delete userData.passwordHash;
delete userData.password_hash;
return {
code: 200,
@@ -198,7 +201,7 @@ export class UserService {
};
}
async delete(id: string): Promise<any> {
async delete(id: string, currentUserId?: number): Promise<any> {
const user = await this.userModel.findByPk(id);
if (!user) {
@@ -209,6 +212,22 @@ export class UserService {
};
}
if (Number(id) === 1) {
return {
code: 400,
message: "内置管理员账号不允许删除",
data: null
};
}
if (currentUserId && Number(id) === Number(currentUserId)) {
return {
code: 400,
message: "不能删除当前登录账号",
data: null
};
}
await this.userRoleModel.destroy({
where: { userId: id }
});
@@ -222,7 +241,7 @@ export class UserService {
};
}
async batchDelete(ids: number[]): Promise<any> {
async batchDelete(ids: number[], currentUserId?: number): Promise<any> {
if (!ids || ids.length === 0) {
return {
code: 400,
@@ -231,6 +250,22 @@ export class UserService {
};
}
if (ids.includes(1)) {
return {
code: 400,
message: "内置管理员账号不允许删除",
data: null
};
}
if (currentUserId && ids.includes(Number(currentUserId))) {
return {
code: 400,
message: "不能删除当前登录账号",
data: null
};
}
await this.userRoleModel.destroy({
where: { userId: ids }
});
@@ -260,7 +295,8 @@ export class UserService {
await user.update({ status });
const userData = user.toJSON();
delete userData.password;
delete userData.passwordHash;
delete userData.password_hash;
return {
code: 200,
@@ -271,18 +307,17 @@ export class UserService {
async getRoleList(): Promise<any> {
const roles = await this.roleModel.findAll({
where: { status: 1 },
attributes: ["id", "roleCode", "roleName"],
order: [
["sort_order", "ASC"],
["created_at", "DESC"]
],
raw: true
]
});
return {
code: 200,
message: "获取角色列表成功",
data: roles
data: roles.map((role) => role.toJSON())
};
}
@@ -298,4 +333,4 @@ export class UserService {
await this.userRoleModel.bulkCreate(userRoles);
}
}
}
+103 -63
View File
@@ -1316,11 +1316,6 @@ asynckit@^0.4.0:
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
aws-ssl-profiles@^1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641"
integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==
babel-jest@^29.7.0:
version "29.7.0"
resolved "https://registry.npmmirror.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5"
@@ -1486,7 +1481,7 @@ cache-content-type@^1.0.0:
mime-types "^2.1.18"
ylru "^1.2.0"
cache-manager-ioredis@^2.1.5:
cache-manager-ioredis@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/cache-manager-ioredis/-/cache-manager-ioredis-2.1.0.tgz#69c9ab4cf72531f9015adf44d3822f9e2dcadc01"
integrity sha512-TCxbp9ceuFveTKWuNaCX8QjoC41rAlHen4s63u9Yd+iXlw3efYmimc/u935PKPxSdhkXpnMes4mxtK3/yb0L4g==
@@ -1764,7 +1759,7 @@ delegates@^1.0.0:
resolved "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
denque@2.1.0, denque@^2.1.0:
denque@2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
@@ -2085,13 +2080,6 @@ function-bind@^1.1.2:
resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
generate-function@^2.3.1:
version "2.3.1"
resolved "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
dependencies:
is-property "^1.0.2"
generator-function@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2"
@@ -2290,13 +2278,6 @@ iconv-lite@0.4.24, iconv-lite@~0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
iconv-lite@^0.7.2:
version "0.7.2"
resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e"
integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
ignore@^5.1.1:
version "5.3.2"
resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
@@ -2430,11 +2411,6 @@ is-number@^7.0.0:
resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-property@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==
is-regex@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22"
@@ -3130,11 +3106,6 @@ lodash@^4.17.21:
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==
long@^5.3.2:
version "5.3.2"
resolved "https://registry.npmmirror.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83"
integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==
lru-cache@7.18.3:
version "7.18.3"
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
@@ -3147,11 +3118,6 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
lru.min@^1.1.0, lru.min@^1.1.4:
version "1.1.4"
resolved "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz#6ea1737a8c1ba2300cc87ad46910a4bdffa0117b"
integrity sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==
make-dir@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
@@ -3298,27 +3264,6 @@ ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
mysql2@^3.12.0:
version "3.22.4"
resolved "https://registry.npmmirror.com/mysql2/-/mysql2-3.22.4.tgz#2cfe5f6125d285755b90cbad9d7d35ceb526c417"
integrity sha512-CtXYlmL7ZamiYKbmqkamQHWJROUHSfm+f3kByzGfknw7kW51mcB2ouMUqYq1XfYxbXmnWo6RhPydx6OCqdgcmQ==
dependencies:
aws-ssl-profiles "^1.1.2"
denque "^2.1.0"
generate-function "^2.3.1"
iconv-lite "^0.7.2"
long "^5.3.2"
lru.min "^1.1.4"
named-placeholders "^1.1.6"
sql-escaper "^1.3.3"
named-placeholders@^1.1.6:
version "1.1.6"
resolved "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz#c50c6920b43f258f59c16add1e56654f5cc02bb5"
integrity sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==
dependencies:
lru.min "^1.1.0"
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -3470,11 +3415,74 @@ path-type@^4.0.0:
resolved "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pg-cloudflare@^1.4.0:
version "1.4.0"
resolved "https://registry.npmmirror.com/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz#4b4c20e6d8ae531d400730f4804571a8d62f1497"
integrity sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==
pg-connection-string@^2.14.0:
version "2.14.0"
resolved "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.14.0.tgz#abc26ee4f37c56c0f3ae0fcf0b0653cc4e1c0fd9"
integrity sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==
pg-connection-string@^2.6.1:
version "2.13.0"
resolved "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.13.0.tgz#8678113465a5af3cc977dcb51eadc847b27aa2de"
integrity sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==
pg-hstore@^2.3.4:
version "2.3.4"
resolved "https://registry.npmmirror.com/pg-hstore/-/pg-hstore-2.3.4.tgz#4425e3e2a3e15d2a334c35581186c27cf2e9b8dd"
integrity sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==
dependencies:
underscore "^1.13.1"
pg-int8@1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
pg-pool@^3.14.0:
version "3.14.0"
resolved "https://registry.npmmirror.com/pg-pool/-/pg-pool-3.14.0.tgz#f35ae4eb846780cad71af24099b3edfa9781ad90"
integrity sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==
pg-protocol@^1.16.0:
version "1.16.0"
resolved "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.16.0.tgz#cffb008826561ee9770a8a15dc21f269d731b305"
integrity sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==
pg-types@2.2.0:
version "2.2.0"
resolved "https://registry.npmmirror.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3"
integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
dependencies:
pg-int8 "1.0.1"
postgres-array "~2.0.0"
postgres-bytea "~1.0.0"
postgres-date "~1.0.4"
postgres-interval "^1.1.0"
pg@^8.11.0:
version "8.23.0"
resolved "https://registry.npmmirror.com/pg/-/pg-8.23.0.tgz#5c2026d32bd0cb4fbd9196bac1ecf5ae66607180"
integrity sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==
dependencies:
pg-connection-string "^2.14.0"
pg-pool "^3.14.0"
pg-protocol "^1.16.0"
pg-types "2.2.0"
pgpass "1.0.5"
optionalDependencies:
pg-cloudflare "^1.4.0"
pgpass@1.0.5:
version "1.0.5"
resolved "https://registry.npmmirror.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d"
integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==
dependencies:
split2 "^4.1.0"
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
@@ -3502,6 +3510,28 @@ pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
postgres-array@~2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
postgres-bytea@~1.0.0:
version "1.0.1"
resolved "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz#c40b3da0222c500ff1e51c5d7014b60b79697c7a"
integrity sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==
postgres-date@~1.0.4:
version "1.0.7"
resolved "https://registry.npmmirror.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8"
integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==
postgres-interval@^1.1.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695"
integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==
dependencies:
xtend "^4.0.0"
pretty-format@^29.0.0, pretty-format@^29.7.0:
version "29.7.0"
resolved "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"
@@ -3667,7 +3697,7 @@ safe-stable-stringify@^2.5.0:
resolved "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -3819,16 +3849,16 @@ source-map@^0.6.0, source-map@^0.6.1:
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
split2@^4.1.0:
version "4.2.0"
resolved "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
sql-escaper@^1.3.3:
version "1.3.3"
resolved "https://registry.npmmirror.com/sql-escaper/-/sql-escaper-1.3.3.tgz#65faf89f048d26bb9a75566b82b5990ddf8a5b7f"
integrity sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==
stack-utils@^2.0.3:
version "2.0.6"
resolved "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f"
@@ -4066,6 +4096,11 @@ uglify-js@^3.1.4:
resolved "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f"
integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==
underscore@^1.13.1:
version "1.13.8"
resolved "https://registry.npmmirror.com/underscore/-/underscore-1.13.8.tgz#a93a21186c049dbf0e847496dba72b7bd8c1e92b"
integrity sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==
"undici-types@>=7.24.0 <7.24.7":
version "7.24.6"
resolved "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91"
@@ -4194,6 +4229,11 @@ ws@^7.2.3:
resolved "https://registry.npmmirror.com/ws/-/ws-7.5.11.tgz#9460daf1812bb81a423c5b9eac746941a86310fa"
integrity sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==
xtend@^4.0.0:
version "4.0.2"
resolved "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"