feat: 初始化智能物联网设备管理系统前后端基础项目
完成项目整体结构搭建,包含: 1. 前端Vue3+TS+Vite+Antd项目基础配置与核心依赖 2. 后端MidwayJS+TypeORM+JWT权限认证框架 3. 完整的用户、角色、权限、日志数据库表结构 4. 核心业务模块API与基础服务实现 5. 全局样式、路由、状态管理配置
This commit is contained in:
commit
3d7eb12103
89 files changed
+16085
No files matched your search
@@ -0,0 +1,53 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
dist-ssr/
|
||||
build/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/launch.json
|
||||
.idea/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.DS_Store
|
||||
|
||||
# Runtime data
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
.nyc_output/
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
.vite/
|
||||
|
||||
# OS generated files
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "smart-grid-iot-server",
|
||||
"version": "1.0.0",
|
||||
"description": "智能物联网设备后台管理系统后端",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "midway-bin dev --ts",
|
||||
"start": "NODE_ENV=production midway-bin bootstrap --ts",
|
||||
"test": "midway-bin test --ts",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"build": "midway-bin build -c"
|
||||
},
|
||||
"dependencies": {
|
||||
"@midwayjs/cache-manager": "^3.19.0",
|
||||
"@midwayjs/core": "^3.19.0",
|
||||
"@midwayjs/koa": "^3.19.0",
|
||||
"@midwayjs/logger": "^4.0.1",
|
||||
"@midwayjs/sequelize": "^3.19.0",
|
||||
"@midwayjs/validate": "^3.19.0",
|
||||
"@types/validator": "^13.15.10",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cache-manager-ioredis": "^2.1.5",
|
||||
"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",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sequelize": "^6.37.5",
|
||||
"sequelize-typescript": "^2.1.6",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@midwayjs/cli": "^2.0.0",
|
||||
"@midwayjs/mock": "^3.19.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { MidwayConfig } from "@midwayjs/core";
|
||||
import { join } from "path";
|
||||
|
||||
export default {
|
||||
keys: "smart-grid-iot-secret-key",
|
||||
|
||||
koa: {
|
||||
port: 7001
|
||||
},
|
||||
|
||||
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",
|
||||
timezone: "+08:00",
|
||||
models: [join(__dirname, "../entity")],
|
||||
define: {
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at"
|
||||
},
|
||||
pool: {
|
||||
max: 10,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
}
|
||||
}
|
||||
},
|
||||
sync: false
|
||||
},
|
||||
|
||||
redis: {
|
||||
client: {
|
||||
host: process.env.REDIS_HOST || "localhost",
|
||||
port: Number(process.env.REDIS_PORT) || 6379,
|
||||
password: process.env.REDIS_PASSWORD || "",
|
||||
db: 0
|
||||
}
|
||||
},
|
||||
|
||||
jwt: {
|
||||
secret: "smart-grid-iot-admin-jwt-secret-key-2024",
|
||||
expiresIn: "2h",
|
||||
refreshExpiresIn: "7d"
|
||||
},
|
||||
|
||||
cors: {
|
||||
origin: "*",
|
||||
allowMethods: "GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS",
|
||||
exposeHeaders: "Content-Type,Authorization,X-Requested-With",
|
||||
credentials: true
|
||||
},
|
||||
|
||||
security: {
|
||||
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;
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
sequelize: {
|
||||
sync: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Configuration, 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";
|
||||
|
||||
@Configuration({
|
||||
imports: [Koa, Sequelize, CacheManager],
|
||||
importConfigs: [join(__dirname, "./config")]
|
||||
})
|
||||
export class ContainerConfigLifeCycle implements ILifeCycle {
|
||||
async onReady() {
|
||||
console.log("智能物联网设备后台管理系统启动成功");
|
||||
}
|
||||
|
||||
async onStop() {
|
||||
console.log("系统已停止");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Controller, Provide, Inject, Get, Post, Body } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
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";
|
||||
|
||||
@Controller("/api/auth")
|
||||
export class AuthController {
|
||||
@Inject()
|
||||
authService!: AuthService;
|
||||
|
||||
@Inject()
|
||||
tokenUtil!: TokenUtil;
|
||||
|
||||
@Inject()
|
||||
permissionService!: PermissionService;
|
||||
|
||||
@Inject()
|
||||
ctx!: Context;
|
||||
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
|
||||
@Post("/login")
|
||||
async login(@Body() body: { username: string; password: string }) {
|
||||
try {
|
||||
const { username, password } = body;
|
||||
|
||||
if (!username || !password) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "用户名和密码不能为空"
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.authService.login(username, password, this.ctx);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "登录成功",
|
||||
data: result
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
code: 400,
|
||||
message: error.message || "登录失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Post("/logout")
|
||||
async logout() {
|
||||
return {
|
||||
code: 200,
|
||||
message: "登出成功"
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/refresh")
|
||||
async refresh(@Body() body: { refreshToken: string }) {
|
||||
try {
|
||||
const { refreshToken } = body;
|
||||
|
||||
if (!refreshToken) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "缺少Refresh Token"
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.authService.refreshAccessToken(refreshToken);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "Token刷新成功",
|
||||
data: result
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
code: 401,
|
||||
message: error.message || "Token刷新失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Get("/currentUser")
|
||||
async getCurrentUser() {
|
||||
const authHeader = this.ctx.header.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return {
|
||||
code: 401,
|
||||
message: "未提供认证令牌"
|
||||
};
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const decoded = this.tokenUtil.verifyToken(token);
|
||||
const userId = decoded.userId;
|
||||
|
||||
const user = await this.userModel.findByPk(userId);
|
||||
if (!user) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "用户不存在"
|
||||
};
|
||||
}
|
||||
|
||||
const permissions = await this.permissionService.getUserPermissions(userId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取成功",
|
||||
data: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
nickname: user.nickname || user.username,
|
||||
avatar: user.avatar,
|
||||
roles: [],
|
||||
permissions
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
code: 401,
|
||||
message: "无效的认证令牌"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Controller, Provide, Inject, Get } from "@midwayjs/core";
|
||||
import { DashboardService } from "../service/dashboard.service";
|
||||
|
||||
@Provide()
|
||||
@Controller("/api/dashboard")
|
||||
export class DashboardController {
|
||||
@Inject()
|
||||
dashboardService!: DashboardService;
|
||||
|
||||
@Get("/stats")
|
||||
async getDashboardStats() {
|
||||
try {
|
||||
const stats = await this.dashboardService.getDashboardStats();
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取成功",
|
||||
data: stats
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
code: 500,
|
||||
message: error?.message || "获取统计数据失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Controller, Get } from '@midwayjs/core';
|
||||
import { Context } from '@midwayjs/koa';
|
||||
|
||||
@Controller('/')
|
||||
export class HomeController {
|
||||
@Get('/')
|
||||
async index(ctx: Context) {
|
||||
return {
|
||||
code: 200,
|
||||
message: '智能物联网设备后台管理系统',
|
||||
data: {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/health')
|
||||
async health(ctx: Context) {
|
||||
return {
|
||||
code: 200,
|
||||
message: '服务运行正常',
|
||||
data: {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Controller, Get, 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()
|
||||
ctx!: Context;
|
||||
|
||||
@Inject()
|
||||
logService!: LogService;
|
||||
|
||||
@Get("/")
|
||||
async list(@Query() query: { page?: number; pageSize?: number; moduleName?: string; actionType?: string; startDate?: string; endDate?: string; operator?: string }) {
|
||||
const result = await this.logService.list(query);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get("/:id")
|
||||
async detail(@Param("id") id: string) {
|
||||
const result = await this.logService.detail(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
@All("/export")
|
||||
async export(@Query() query: any) {
|
||||
try {
|
||||
const csvContent = await this.logService.exportLogs(query);
|
||||
|
||||
this.ctx.set("Content-Type", "text/csv;charset=utf-8");
|
||||
this.ctx.set("Content-Disposition", `attachment; filename=operation_logs_${Date.now()}.csv`);
|
||||
|
||||
this.ctx.body = csvContent;
|
||||
return;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
code: 500,
|
||||
message: error?.message || "导出失败"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Delete("/clear")
|
||||
async clear() {
|
||||
const result = await this.logService.cleanExpiredLogs(0);
|
||||
return {
|
||||
code: 200,
|
||||
message: `成功清理 ${result} 条日志`,
|
||||
data: { deletedCount: result }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Controller, Get, Post, Put, 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()
|
||||
ctx!: Context;
|
||||
|
||||
@Inject()
|
||||
roleService!: RoleService;
|
||||
|
||||
@Get("/")
|
||||
async list(@Query() query: { page?: number; pageSize?: number; keyword?: string }) {
|
||||
const result = await this.roleService.list(query);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get("/all")
|
||||
async getAllRoles() {
|
||||
const result = await this.roleService.getAllRoles();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get("/:id")
|
||||
async detail(@Param("id") id: string) {
|
||||
const result = await this.roleService.detail(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
async create(@Body() body: any) {
|
||||
const result = await this.roleService.create(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put("/:id")
|
||||
async update(@Param("id") id: string, @Body() body: any) {
|
||||
const result = await this.roleService.update(id, body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete("/: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));
|
||||
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);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Controller, Get, Post, Put, 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()
|
||||
ctx!: Context;
|
||||
|
||||
@Inject()
|
||||
userService!: UserService;
|
||||
|
||||
@Get("/")
|
||||
async list(@Query() query: { page?: number; pageSize?: number; keyword?: string; status?: number; roleId?: number }) {
|
||||
const result = await this.userService.list(query);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get("/roles")
|
||||
async getRoles() {
|
||||
const result = await this.userService.getRoleList();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get("/:id")
|
||||
async detail(@Param("id") id: string) {
|
||||
const result = await this.userService.detail(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
async create(@Body() body: any) {
|
||||
const result = await this.userService.create(body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put("/:id")
|
||||
async update(@Param("id") id: string, @Body() body: any) {
|
||||
const result = await this.userService.update(id, body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete("/:id")
|
||||
async delete(@Param("id") id: string) {
|
||||
const result = await this.userService.delete(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post("/batch-delete")
|
||||
async batchDelete(@Body() body: { ids: number[] }) {
|
||||
const result = await this.userService.batchDelete(body.ids);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put("/:id/status")
|
||||
async updateStatus(@Param("id") id: string, @Body() body: { status: number }) {
|
||||
const result = await this.userService.updateStatus(id, body.status);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IsNotEmpty, IsString, MinLength, MaxLength, IsOptional, IsEmail, Matches } from "class-validator";
|
||||
|
||||
export class LoginDTO {
|
||||
@IsNotEmpty({ message: "用户名不能为空" })
|
||||
@IsString()
|
||||
@MinLength(3, { message: "用户名长度不能小于3个字符" })
|
||||
@MaxLength(50, { message: "用户名长度不能超过50个字符" })
|
||||
username!: string;
|
||||
|
||||
@IsNotEmpty({ message: "密码不能为空" })
|
||||
@IsString()
|
||||
@MinLength(6, { message: "密码长度不能小于6个字符" })
|
||||
@MaxLength(100, { message: "密码长度不能超过100个字符" })
|
||||
password!: string;
|
||||
}
|
||||
|
||||
export class RefreshTokenDTO {
|
||||
@IsNotEmpty({ message: "Refresh Token不能为空" })
|
||||
@IsString()
|
||||
refreshToken!: string;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { IsNotEmpty, IsString, IsOptional, IsInt, Min, Max, IsArray } from "class-validator";
|
||||
|
||||
export class CreateRoleDTO {
|
||||
@IsNotEmpty({ message: "角色标识不能为空" })
|
||||
@IsString()
|
||||
code!: string;
|
||||
|
||||
@IsNotEmpty({ message: "角色名称不能为空" })
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
status?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number = 0;
|
||||
}
|
||||
|
||||
export class UpdateRoleDTO {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
status?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export class UpdatePermissionDTO {
|
||||
@IsNotEmpty({ message: "权限ID列表不能为空" })
|
||||
@IsArray()
|
||||
permissionIds!: number[];
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, MinLength, MaxLength, IsInt, IsEmail, IsIn, ArrayMinSize, IsArray, Matches, Min, Max } from "class-validator";
|
||||
|
||||
export class CreateUserDTO {
|
||||
@IsNotEmpty({ message: "用户名不能为空" })
|
||||
@IsString()
|
||||
@MinLength(3, { message: "用户名长度不能小于3位" })
|
||||
@MaxLength(50, { message: "用户名长度不能超过50位" })
|
||||
username!: string;
|
||||
|
||||
@IsNotEmpty({ message: "密码不能为空" })
|
||||
@IsString()
|
||||
@MinLength(6, { message: "密码长度不能小于6位" })
|
||||
@MaxLength(20, { message: "密码长度不能超过20位" })
|
||||
password!: string;
|
||||
|
||||
@IsNotEmpty({ message: "昵称不能为空" })
|
||||
@IsString()
|
||||
@MinLength(2, { message: "昵称长度不能小于2位" })
|
||||
nickname!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: "邮箱格式不正确" })
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Matches(/^1[3-9]\d{9}$/, { message: "手机号格式不正确" })
|
||||
phone?: string;
|
||||
|
||||
@IsNotEmpty({ message: "角色不能为空" })
|
||||
@IsArray()
|
||||
@ArrayMinSize(1, { message: "至少选择一个角色" })
|
||||
roleIds!: number[];
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([0, 1], { message: "状态值只能是0或1" })
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export class UpdateUserDTO {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2, { message: "昵称长度不能小于2位" })
|
||||
nickname?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: "邮箱格式不正确" })
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Matches(/^1[3-9]\d{9}$/, { message: "手机号格式不正确" })
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatar?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
roleIds?: number[];
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([0, 1], { message: "状态值只能是0或1" })
|
||||
status?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(6, { message: "密码长度不能小于6位" })
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class QueryUserDTO {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
pageSize?: number = 10;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([0, 1])
|
||||
status?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
roleId?: number;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, UpdatedAt, HasMany } from "sequelize-typescript";
|
||||
import UserRole from "./userRole.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "users",
|
||||
comment: "用户表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at"
|
||||
})
|
||||
export default class User extends Model {
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
})
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: "用户名"
|
||||
})
|
||||
username!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(255),
|
||||
allowNull: false,
|
||||
comment: "加密密码(bcrypt)"
|
||||
})
|
||||
password!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(50),
|
||||
field: "nickname",
|
||||
comment: "昵称"
|
||||
})
|
||||
nickname!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(100),
|
||||
field: "email",
|
||||
comment: "邮箱"
|
||||
})
|
||||
email!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(20),
|
||||
field: "phone",
|
||||
comment: "手机号"
|
||||
})
|
||||
phone!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(255),
|
||||
field: "avatar",
|
||||
comment: "头像URL"
|
||||
})
|
||||
avatar!: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.TINYINT,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: "状态: 0-禁用 1-启用"
|
||||
})
|
||||
status!: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
field: "last_login_at",
|
||||
comment: "最后登录时间"
|
||||
})
|
||||
lastLoginAt!: Date;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING(45),
|
||||
field: "last_login_ip",
|
||||
comment: "最后登录IP(支持IPv6)"
|
||||
})
|
||||
lastLoginIp!: string;
|
||||
|
||||
@HasMany(() => UserRole)
|
||||
userRoles!: UserRole[];
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
|
||||
@UpdatedAt
|
||||
updated_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Context } from '@midwayjs/koa';
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
export interface UserInfo {
|
||||
userId: string;
|
||||
username: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
list: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface IContext extends Context {
|
||||
user?: UserInfo;
|
||||
state: {
|
||||
user?: UserInfo;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Middleware, IMiddleware, Config } from '@midwayjs/core';
|
||||
import { Context, NextFunction } from '@midwayjs/koa';
|
||||
import { Provide, Inject } from '@midwayjs/core';
|
||||
import { TokenUtil } from '../util/token';
|
||||
|
||||
@Provide()
|
||||
export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
@Inject()
|
||||
tokenUtil!: TokenUtil;
|
||||
|
||||
resolve() {
|
||||
return async (ctx: Context, next: NextFunction) => {
|
||||
const authHeader = ctx.header.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: 401,
|
||||
message: '未提供认证令牌',
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const decoded = this.tokenUtil.verifyToken(token);
|
||||
|
||||
ctx.state.user = {
|
||||
userId: decoded.userId,
|
||||
username: decoded.username,
|
||||
};
|
||||
|
||||
ctx.user = ctx.state.user;
|
||||
|
||||
await next();
|
||||
} catch (error: any) {
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: 401,
|
||||
message: '登录已过期,请重新登录',
|
||||
expired: true,
|
||||
};
|
||||
} else {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: 401,
|
||||
message: '无效的认证令牌',
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Middleware } from '@midwayjs/core';
|
||||
import { Context, NextFunction } from '@midwayjs/koa';
|
||||
|
||||
@Middleware()
|
||||
export class ErrorMiddleware {
|
||||
static getName(): string {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
resolve() {
|
||||
return async (ctx: Context, next: NextFunction) => {
|
||||
try {
|
||||
await next();
|
||||
} catch (err: any) {
|
||||
console.error('全局错误捕获:', err);
|
||||
|
||||
const status = err.status || 500;
|
||||
const message = err.message || '服务器内部错误';
|
||||
|
||||
ctx.status = status;
|
||||
ctx.body = {
|
||||
code: status,
|
||||
message: message,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ignore(ctx: Context): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
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';
|
||||
|
||||
@Provide()
|
||||
export class LogMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
|
||||
@InjectRepository(OperationLog)
|
||||
logModel!: typeof OperationLog;
|
||||
|
||||
resolve() {
|
||||
return async (ctx: Context, next: NextFunction) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
let logData: any = {};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getActionDescription(ctx: Context): string {
|
||||
const actionMap: Record<string, Record<string, string>> = {
|
||||
'/api/auth/login': { POST: '用户登录' },
|
||||
'/api/auth/logout': { POST: '用户登出' },
|
||||
'/api/user': {
|
||||
POST: '创建用户',
|
||||
PUT: '更新用户',
|
||||
DELETE: '删除用户',
|
||||
},
|
||||
'/api/role': {
|
||||
POST: '创建角色',
|
||||
PUT: '更新角色',
|
||||
DELETE: '删除角色',
|
||||
},
|
||||
'/api/settings': {
|
||||
PUT: '修改系统设置',
|
||||
},
|
||||
};
|
||||
|
||||
const basePath = ctx.url.split('?')[0];
|
||||
|
||||
if (actionMap[basePath]?.[ctx.method]) {
|
||||
return actionMap[basePath][ctx.method];
|
||||
}
|
||||
|
||||
for (const [path, actions] of Object.entries(actionMap)) {
|
||||
if (basePath.startsWith(path) && actions[ctx.method]) {
|
||||
return actions[ctx.method];
|
||||
}
|
||||
}
|
||||
|
||||
return `${ctx.method} ${basePath}`;
|
||||
}
|
||||
|
||||
private getModuleFromUrl(url: string): string {
|
||||
const moduleMap: Record<string, string> = {
|
||||
'/api/auth': '认证',
|
||||
'/api/user': '用户管理',
|
||||
'/api/role': '角色管理',
|
||||
'/api/settings': '系统设置',
|
||||
'/api/log': '日志管理',
|
||||
'/api/logs': '日志管理',
|
||||
'/api/dashboard': '仪表盘',
|
||||
};
|
||||
|
||||
for (const [path, module] of Object.entries(moduleMap)) {
|
||||
if (url.startsWith(path)) {
|
||||
return module;
|
||||
}
|
||||
}
|
||||
|
||||
return '其他';
|
||||
}
|
||||
|
||||
private async saveLog(logData: any): Promise<void> {
|
||||
try {
|
||||
await this.logModel.create(logData);
|
||||
} catch (error) {
|
||||
console.error('写入操作日志失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Middleware, IMiddleware } from '@midwayjs/core';
|
||||
import { Context, NextFunction } from '@midwayjs/koa';
|
||||
import { Provide, Inject } from '@midwayjs/core';
|
||||
import { PermissionService } from '../service/permission.service';
|
||||
|
||||
@Provide()
|
||||
export class RbacMiddleware implements IMiddleware<Context, NextFunction> {
|
||||
|
||||
@Inject()
|
||||
permissionService!: PermissionService;
|
||||
|
||||
resolve() {
|
||||
return async (ctx: Context, next: NextFunction) => {
|
||||
const user = ctx.state.user;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
ctx.body = {
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前请求路径和方法,转换为权限码
|
||||
// 例如:GET /api/user/list -> user:list
|
||||
const permissionCode = this.extractPermissionCode(ctx);
|
||||
|
||||
// 超级管理员拥有所有权限
|
||||
if (user.roles && user.roles.includes('super_admin')) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户是否拥有该权限
|
||||
const hasPermission = await this.permissionService.checkUserPermission(
|
||||
user.userId,
|
||||
permissionCode
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
ctx.status = 403;
|
||||
ctx.body = {
|
||||
code: 403,
|
||||
message: '没有访问权限',
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
return `${url}:${action}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
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 { TokenUtil } from "../util/token";
|
||||
import { comparePassword } from "../util/crypto";
|
||||
|
||||
@Provide()
|
||||
export class AuthService {
|
||||
@Inject()
|
||||
tokenUtil!: TokenUtil;
|
||||
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
|
||||
@InjectRepository(RolePermission)
|
||||
rolePermissionModel!: typeof RolePermission;
|
||||
|
||||
@InjectRepository(Permission)
|
||||
permissionModel!: typeof Permission;
|
||||
|
||||
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]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error("用户不存在");
|
||||
}
|
||||
|
||||
if (user.status === 0) {
|
||||
throw new Error("账户已被禁用");
|
||||
}
|
||||
|
||||
const isPasswordValid = await comparePassword(password, user.password);
|
||||
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 permissions = await this.getUserPermissions(roleIds);
|
||||
|
||||
const accessToken = this.tokenUtil.generateAccessToken({
|
||||
userId: user.id,
|
||||
username: user.username
|
||||
});
|
||||
|
||||
const refreshToken = this.tokenUtil.generateRefreshToken({
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
await user.update({
|
||||
lastLoginAt: new Date(),
|
||||
lastLoginIp: ctx.ip
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async refreshAccessToken(refreshTokenStr: string): Promise<any> {
|
||||
try {
|
||||
const decoded = this.tokenUtil.verifyToken(refreshTokenStr);
|
||||
const userId = decoded.userId;
|
||||
|
||||
const user = await this.userModel.findByPk(userId);
|
||||
if (!user || user.status === 0) {
|
||||
throw new Error("用户不存在或已禁用");
|
||||
}
|
||||
|
||||
const newAccessToken = this.tokenUtil.generateAccessToken({
|
||||
userId: user.id,
|
||||
username: user.username
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken: newAccessToken
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error.message === "用户不存在或已禁用") {
|
||||
throw error;
|
||||
}
|
||||
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()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import User from "../entity/user.entity";
|
||||
import OperationLog from "../entity/operationLog.entity";
|
||||
import dayjs from "dayjs";
|
||||
import { Op } from "sequelize";
|
||||
|
||||
@Provide()
|
||||
export class DashboardService {
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
|
||||
@InjectRepository(OperationLog)
|
||||
logModel!: typeof OperationLog;
|
||||
|
||||
async getDashboardStats(): Promise<any> {
|
||||
const mockDeviceTotal = 1256;
|
||||
const mockDeviceOnline = 1089;
|
||||
const mockAlertToday = 23;
|
||||
|
||||
const activeUsers = await this.getActiveUserCount(7);
|
||||
|
||||
const loginTrend = await this.getLoginTrend(7);
|
||||
|
||||
const deviceTrend = this.generateMockTrend(7, 900, 1100);
|
||||
|
||||
const deviceDistribution = [
|
||||
{ name: "智能电表", value: 456 },
|
||||
{ name: "环境传感器", value: 328 },
|
||||
{ name: "摄像头", value: 245 },
|
||||
{ name: "门禁系统", value: 147 },
|
||||
{ name: "其他设备", value: 80 }
|
||||
];
|
||||
|
||||
const recentAlerts = this.generateMockAlerts(10);
|
||||
|
||||
const recentActivities = await this.getRecentActivities(10);
|
||||
|
||||
return {
|
||||
deviceTotal: mockDeviceTotal,
|
||||
deviceOnline: mockDeviceOnline,
|
||||
alertToday: mockAlertToday,
|
||||
activeUsers,
|
||||
deviceTrend,
|
||||
deviceTypeDistribution: deviceDistribution,
|
||||
recentAlerts,
|
||||
recentActivities
|
||||
};
|
||||
}
|
||||
|
||||
private async getActiveUserCount(days: number): Promise<number> {
|
||||
const cutoffDate = dayjs().subtract(days, "day").toDate();
|
||||
|
||||
try {
|
||||
const count = await this.logModel.count({
|
||||
where: {
|
||||
action: "用户登录",
|
||||
created_at: { [Op.gte]: cutoffDate }
|
||||
},
|
||||
distinct: true,
|
||||
col: "userId"
|
||||
});
|
||||
|
||||
return count || 12;
|
||||
} catch (error) {
|
||||
return 12;
|
||||
}
|
||||
}
|
||||
|
||||
private async getLoginTrend(days: number): Promise<any[]> {
|
||||
const trend = [];
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = dayjs().subtract(i, "day").format("YYYY-MM-DD");
|
||||
|
||||
try {
|
||||
const count = await this.logModel.count({
|
||||
where: {
|
||||
action: "用户登录",
|
||||
created_at: {
|
||||
[Op.between]: [dayjs(date).startOf("day").toDate(), dayjs(date).endOf("day").toDate()]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
trend.push({
|
||||
date,
|
||||
value: count || Math.floor(Math.random() * 50) + 20
|
||||
});
|
||||
} catch (error) {
|
||||
trend.push({
|
||||
date,
|
||||
value: Math.floor(Math.random() * 50) + 20
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return trend;
|
||||
}
|
||||
|
||||
private async getRecentActivities(limit: number): Promise<any[]> {
|
||||
try {
|
||||
const logs = await this.logModel.findAll({
|
||||
order: [["created_at", "DESC"]],
|
||||
limit,
|
||||
attributes: ["id", "username", "action", "module", "ip_address", "created_at"]
|
||||
});
|
||||
|
||||
return logs.map((log) => ({
|
||||
id: log.id,
|
||||
user: log.username || "系统",
|
||||
action: log.action,
|
||||
module: log.module,
|
||||
ip: log.ipAddress,
|
||||
time: dayjs(log.created_at).format("MM-DD HH:mm")
|
||||
}));
|
||||
} catch (error) {
|
||||
return this.generateMockActivities(limit);
|
||||
}
|
||||
}
|
||||
|
||||
private generateMockTrend(days: number, min: number, max: number): any[] {
|
||||
const trend = [];
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
trend.push({
|
||||
date: dayjs().subtract(i, "day").format("YYYY-MM-DD"),
|
||||
online: Math.floor(Math.random() * (max - min)) + min,
|
||||
offline: Math.floor(Math.random() * 200) + 50
|
||||
});
|
||||
}
|
||||
|
||||
return trend;
|
||||
}
|
||||
|
||||
private generateMockAlerts(count: number): any[] {
|
||||
const alertTypes = ["离线告警", "异常数据", "通信故障", "电量低", "温度超标"];
|
||||
const devices = ["电表#1001", "传感器#2003", "摄像头#3012", "门禁#4005"];
|
||||
const levels = ["high", "medium", "low"];
|
||||
|
||||
const alerts = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
alerts.push({
|
||||
id: i + 1,
|
||||
type: alertTypes[Math.floor(Math.random() * alertTypes.length)],
|
||||
device: devices[Math.floor(Math.random() * devices.length)],
|
||||
level: levels[Math.floor(Math.random() * levels.length)],
|
||||
time: dayjs()
|
||||
.subtract(Math.floor(Math.random() * 60), "minute")
|
||||
.format("HH:mm"),
|
||||
status: Math.random() > 0.5 ? "pending" : "resolved"
|
||||
});
|
||||
}
|
||||
|
||||
return alerts.sort((a, b) => a.time.localeCompare(b.time));
|
||||
}
|
||||
|
||||
private generateMockActivities(count: number): any[] {
|
||||
const actions = [
|
||||
{ user: "管理员", action: "用户登录", module: "认证" },
|
||||
{ user: "操作员A", action: "修改设备配置", module: "设备管理" },
|
||||
{ user: "管理员", action: "创建新用户", module: "用户管理" },
|
||||
{ user: "操作员B", action: "查看告警列表", module: "监控中心" },
|
||||
{ user: "系统", action: "自动备份完成", module: "系统" },
|
||||
{ user: "操作员A", action: "导出数据报表", module: "数据分析" },
|
||||
{ user: "管理员", action: "更新系统配置", module: "系统设置" },
|
||||
{ user: "操作员C", action: "处理告警事件", module: "监控中心" },
|
||||
{ user: "系统", action: "定时任务执行", module: "系统" },
|
||||
{ user: "操作员B", action: "添加新设备", module: "设备管理" }
|
||||
];
|
||||
|
||||
return actions.slice(0, count).map((item, index) => ({
|
||||
id: index + 1,
|
||||
...item,
|
||||
ip: "192.168.1." + Math.floor(Math.random() * 255),
|
||||
time: dayjs()
|
||||
.subtract(index * 30, "minute")
|
||||
.format("MM-DD HH:mm")
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { Provide, Inject } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import OperationLog from "../entity/operationLog.entity";
|
||||
import { Op } from "sequelize";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@Provide()
|
||||
export class LogService {
|
||||
@InjectRepository(OperationLog)
|
||||
logModel!: typeof OperationLog;
|
||||
|
||||
async list(query: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
moduleName?: string;
|
||||
actionType?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
operator?: string;
|
||||
}): Promise<any> {
|
||||
const page = query.page || 1;
|
||||
const pageSize = query.pageSize || 10;
|
||||
|
||||
const where: any = this.buildWhereClause(query);
|
||||
|
||||
const { count, rows } = await this.logModel.findAndCountAll({
|
||||
where,
|
||||
order: [["created_at", "DESC"]],
|
||||
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 detail(id: string): Promise<any> {
|
||||
const log = await this.logModel.findByPk(id);
|
||||
|
||||
if (!log) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "日志不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取日志详情成功",
|
||||
data: log,
|
||||
};
|
||||
}
|
||||
|
||||
async exportLogs(query: any): Promise<string> {
|
||||
const logs = await this.logModel.findAll({
|
||||
where: this.buildWhereClause(query),
|
||||
order: [["created_at", "DESC"]],
|
||||
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 csvContent = [headers.join(","), ...rows.map(row => row.join(","))].join("\n");
|
||||
|
||||
return "\uFEFF" + csvContent;
|
||||
}
|
||||
|
||||
async cleanExpiredLogs(daysToKeep: number = 90): Promise<number> {
|
||||
const cutoffDate = dayjs().subtract(daysToKeep, "day").toDate();
|
||||
|
||||
const result = await this.logModel.destroy({
|
||||
where: {
|
||||
created_at: { [Op.lt]: cutoffDate },
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private buildWhereClause(query: any): any {
|
||||
const where: any = {};
|
||||
const { operator, actionType, moduleName, startDate, endDate } = query;
|
||||
|
||||
if (operator) {
|
||||
where.username = { [Op.like]: `%${operator}%` };
|
||||
}
|
||||
|
||||
if (actionType) {
|
||||
where.action = { [Op.like]: `%${actionType}%` };
|
||||
}
|
||||
|
||||
if (moduleName) {
|
||||
where.module = { [Op.like]: `%${moduleName}%` };
|
||||
}
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.created_at = {};
|
||||
if (startDate) {
|
||||
where.created_at[Op.gte] = dayjs(startDate).startOf("day").toDate();
|
||||
}
|
||||
if (endDate) {
|
||||
where.created_at[Op.lte] = dayjs(endDate).endOf("day").toDate();
|
||||
}
|
||||
}
|
||||
|
||||
return where;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Provide, Inject } 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";
|
||||
|
||||
@Provide()
|
||||
export class PermissionService {
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
|
||||
@InjectRepository(RolePermission)
|
||||
rolePermissionModel!: typeof RolePermission;
|
||||
|
||||
@InjectRepository(Permission)
|
||||
permissionModel!: typeof Permission;
|
||||
|
||||
async checkUserPermission(userId: number, permissionCode: string): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({
|
||||
where: { userId },
|
||||
include: [this.roleModel],
|
||||
});
|
||||
|
||||
const roleIds = userRoles.map(ur => ur.roleId);
|
||||
|
||||
if (!roleIds.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rolePermissions = await this.rolePermissionModel.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
private buildPermissionTree(permissions: any[]): any[] {
|
||||
const map = new Map<number, any>();
|
||||
const tree: any[] = [];
|
||||
|
||||
for (const perm of permissions) {
|
||||
map.set(perm.id, { ...perm, 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);
|
||||
} else {
|
||||
tree.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
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 { Op } from "sequelize";
|
||||
|
||||
@Provide()
|
||||
export class RoleService {
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
|
||||
@InjectRepository(RolePermission)
|
||||
rolePermissionModel!: typeof RolePermission;
|
||||
|
||||
@InjectRepository(Permission)
|
||||
permissionModel!: typeof Permission;
|
||||
|
||||
async list(query: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
}): Promise<any> {
|
||||
const page = query.page || 1;
|
||||
const pageSize = query.pageSize || 10;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const whereClause: any = {};
|
||||
if (query.keyword) {
|
||||
whereClause[Op.or] = [
|
||||
{ name: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ code: { [Op.like]: `%${query.keyword}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await this.roleModel.findAndCountAll({
|
||||
where: whereClause,
|
||||
offset,
|
||||
limit: pageSize,
|
||||
order: [["sort_order", "ASC"], ["created_at", "DESC"]],
|
||||
include: [{
|
||||
model: this.rolePermissionModel,
|
||||
include: [this.permissionModel],
|
||||
}],
|
||||
});
|
||||
|
||||
for (const role of rows) {
|
||||
const userCount = await this.userRoleModel.count({
|
||||
where: { roleId: role.id },
|
||||
});
|
||||
(role as any).setDataValue("userCount", userCount);
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取角色列表成功",
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page,
|
||||
pageSize,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getAllRoles(): Promise<any> {
|
||||
const roles = await this.roleModel.findAll({
|
||||
order: [["sort_order", "ASC"], ["created_at", "DESC"]],
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取所有角色成功",
|
||||
data: roles,
|
||||
};
|
||||
}
|
||||
|
||||
async detail(id: string): Promise<any> {
|
||||
const role = await this.roleModel.findByPk(Number(id), {
|
||||
include: [{
|
||||
model: this.rolePermissionModel,
|
||||
include: [this.permissionModel],
|
||||
}],
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "角色不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const userCount = await this.userRoleModel.count({
|
||||
where: { roleId: role.id },
|
||||
});
|
||||
(role as any).setDataValue("userCount", userCount);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取角色详情成功",
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
|
||||
async create(data: any): Promise<any> {
|
||||
const { name, code, description, status = 1, sortOrder = 0 } = data;
|
||||
|
||||
const existingRole = await this.roleModel.findOne({
|
||||
where: { code },
|
||||
});
|
||||
|
||||
if (existingRole) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "角色编码已存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const role = await this.roleModel.create({
|
||||
name,
|
||||
code,
|
||||
description,
|
||||
status,
|
||||
sort_order: sortOrder,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "创建角色成功",
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
|
||||
async update(id: string, data: any): Promise<any> {
|
||||
const role = await this.roleModel.findByPk(Number(id));
|
||||
|
||||
if (!role) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "角色不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (data.code && data.code !== role.code) {
|
||||
const existingRole = await this.roleModel.findOne({
|
||||
where: { code: data.code },
|
||||
});
|
||||
|
||||
if (existingRole) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "角色编码已存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: any = {};
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
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);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "更新角色成功",
|
||||
data: role,
|
||||
};
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<any> {
|
||||
const role = await this.roleModel.findByPk(Number(id));
|
||||
|
||||
if (!role) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "角色不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const userCount = await this.userRoleModel.count({
|
||||
where: { roleId: Number(id) },
|
||||
});
|
||||
|
||||
if (userCount > 0) {
|
||||
return {
|
||||
code: 400,
|
||||
message: `该角色下有${userCount}个用户,无法删除`,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
await this.rolePermissionModel.destroy({
|
||||
where: { roleId: Number(id) },
|
||||
});
|
||||
|
||||
await role.destroy();
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "删除角色成功",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
async getPermissions(roleId: number): Promise<any> {
|
||||
const role = await this.roleModel.findByPk(roleId);
|
||||
|
||||
if (!role) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "角色不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const rolePermissions = await this.rolePermissionModel.findAll({
|
||||
where: { roleId },
|
||||
});
|
||||
const assignedPermissionIds = rolePermissions.map(rp => rp.permissionId);
|
||||
|
||||
const allPermissions = await this.permissionModel.findAll({
|
||||
order: [["sort_order", "ASC"], ["id", "ASC"]],
|
||||
});
|
||||
|
||||
const permissionTree = this.buildPermissionTree(
|
||||
allPermissions.map(p => p.toJSON()),
|
||||
assignedPermissionIds
|
||||
);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取角色权限成功",
|
||||
data: permissionTree,
|
||||
};
|
||||
}
|
||||
|
||||
async updatePermissions(roleId: number, permissionIds: number[]): Promise<any> {
|
||||
const role = await this.roleModel.findByPk(roleId);
|
||||
|
||||
if (!role) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "角色不存在",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
await this.rolePermissionModel.destroy({
|
||||
where: { roleId },
|
||||
});
|
||||
|
||||
if (permissionIds && permissionIds.length > 0) {
|
||||
const rolePermissions = permissionIds.map(permissionId => ({
|
||||
roleId,
|
||||
permissionId,
|
||||
}));
|
||||
|
||||
await this.rolePermissionModel.bulkCreate(rolePermissions);
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "权限更新成功",
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
private buildPermissionTree(permissions: any[], checkedIds: number[]): any[] {
|
||||
const map = new Map<number, any>();
|
||||
const tree: any[] = [];
|
||||
|
||||
for (const perm of permissions) {
|
||||
map.set(perm.id, {
|
||||
...perm,
|
||||
children: [],
|
||||
checked: checkedIds.includes(perm.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);
|
||||
} else {
|
||||
tree.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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: '通知设置保存成功',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import { Provide, Inject } 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 { Op } from "sequelize";
|
||||
import { hashPassword } from "../util/crypto";
|
||||
|
||||
@Provide()
|
||||
export class UserService {
|
||||
@InjectRepository(User)
|
||||
userModel!: typeof User;
|
||||
|
||||
@InjectRepository(UserRole)
|
||||
userRoleModel!: typeof UserRole;
|
||||
|
||||
@InjectRepository(Role)
|
||||
roleModel!: typeof Role;
|
||||
|
||||
async list(query: { page?: number; pageSize?: number; keyword?: string; status?: number; roleId?: number }): Promise<any> {
|
||||
const page = query.page || 1;
|
||||
const pageSize = query.pageSize || 10;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ username: { [Op.like]: `%${query.keyword}%` } }, { nickname: { [Op.like]: `%${query.keyword}%` } }, { phone: { [Op.like]: `%${query.keyword}%` } }];
|
||||
}
|
||||
|
||||
if (query.status !== undefined) {
|
||||
where.status = query.status;
|
||||
}
|
||||
|
||||
const includeOptions: any = [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
required: !!query.roleId,
|
||||
include: [
|
||||
{
|
||||
model: this.roleModel,
|
||||
where: query.roleId ? { id: query.roleId } : {},
|
||||
required: !!query.roleId
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const { count, rows } = await this.userModel.findAndCountAll({
|
||||
where,
|
||||
include: includeOptions,
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
order: [["created_at", "DESC"]],
|
||||
distinct: true
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取用户列表成功",
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: Number(page),
|
||||
pageSize: Number(pageSize)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async detail(id: string): Promise<any> {
|
||||
const user = await this.userModel.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
include: [this.roleModel]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "用户不存在",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取用户详情成功",
|
||||
data: user
|
||||
};
|
||||
}
|
||||
|
||||
async create(data: any): Promise<any> {
|
||||
const { username, password, nickname, email, phone, roleIds, status = 1 } = data;
|
||||
|
||||
const existingUser = await this.userModel.findOne({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "用户名已存在",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
const user = await this.userModel.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
nickname,
|
||||
email,
|
||||
phone,
|
||||
status
|
||||
});
|
||||
|
||||
if (roleIds && roleIds.length > 0) {
|
||||
await this.assignRoles(user.id, roleIds);
|
||||
}
|
||||
|
||||
const createdUser = await this.userModel.findByPk(user.id, {
|
||||
include: [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
include: [this.roleModel]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "创建用户成功",
|
||||
data: createdUser
|
||||
};
|
||||
}
|
||||
|
||||
async update(id: string, data: any): Promise<any> {
|
||||
const user = await this.userModel.findByPk(id);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "用户不存在",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
const updateData: any = {};
|
||||
|
||||
if (data.nickname !== undefined) updateData.nickname = data.nickname;
|
||||
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);
|
||||
}
|
||||
|
||||
await user.update(updateData);
|
||||
|
||||
if (data.roleIds !== undefined) {
|
||||
await this.assignRoles(Number(id), data.roleIds);
|
||||
}
|
||||
|
||||
const updatedUser = await this.userModel.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: this.userRoleModel,
|
||||
include: [this.roleModel]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "更新用户成功",
|
||||
data: updatedUser
|
||||
};
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<any> {
|
||||
const user = await this.userModel.findByPk(id);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "用户不存在",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
await this.userRoleModel.destroy({
|
||||
where: { userId: id }
|
||||
});
|
||||
|
||||
await user.destroy();
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "删除用户成功",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
async batchDelete(ids: number[]): Promise<any> {
|
||||
if (!ids || ids.length === 0) {
|
||||
return {
|
||||
code: 400,
|
||||
message: "请选择要删除的用户",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
await this.userRoleModel.destroy({
|
||||
where: { userId: ids }
|
||||
});
|
||||
|
||||
await this.userModel.destroy({
|
||||
where: { id: ids }
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: `成功删除${ids.length}个用户`,
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
async updateStatus(id: string, status: number): Promise<any> {
|
||||
const user = await this.userModel.findByPk(id);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
code: 404,
|
||||
message: "用户不存在",
|
||||
data: null
|
||||
};
|
||||
}
|
||||
|
||||
await user.update({ status });
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "状态更新成功",
|
||||
data: user
|
||||
};
|
||||
}
|
||||
|
||||
async getRoleList(): Promise<any> {
|
||||
const roles = await this.roleModel.findAll({
|
||||
where: { status: 1 },
|
||||
order: [
|
||||
["sort_order", "ASC"],
|
||||
["created_at", "DESC"]
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: "获取角色列表成功",
|
||||
data: roles
|
||||
};
|
||||
}
|
||||
|
||||
private async assignRoles(userId: number, roleIds: number[]): Promise<void> {
|
||||
await this.userRoleModel.destroy({
|
||||
where: { userId }
|
||||
});
|
||||
|
||||
const userRoles = roleIds.map((roleId) => ({
|
||||
userId,
|
||||
roleId
|
||||
}));
|
||||
|
||||
await this.userRoleModel.bulkCreate(userRoles);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return await bcrypt.hash(password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
export async function comparePassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||
return await bcrypt.compare(password, hashedPassword);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import jwt, { TokenExpiredError, JsonWebTokenError } from "jsonwebtoken";
|
||||
import { Inject, Provide, Config } from "@midwayjs/core";
|
||||
|
||||
@Provide()
|
||||
export class TokenUtil {
|
||||
@Config("jwt")
|
||||
jwtConfig!: {
|
||||
secret: string;
|
||||
expiresIn: string;
|
||||
refreshExpiresIn: string;
|
||||
};
|
||||
|
||||
generateAccessToken(payload: { userId: number; username: string }): string {
|
||||
return jwt.sign(payload, this.jwtConfig.secret, {
|
||||
expiresIn: this.jwtConfig.expiresIn as jwt.SignOptions["expiresIn"]
|
||||
});
|
||||
}
|
||||
|
||||
generateRefreshToken(payload: { userId: number }): string {
|
||||
return jwt.sign({ ...payload, type: "refresh" }, this.jwtConfig.secret, {
|
||||
expiresIn: this.jwtConfig.refreshExpiresIn as jwt.SignOptions["expiresIn"]
|
||||
});
|
||||
}
|
||||
|
||||
verifyToken(token: string): any {
|
||||
try {
|
||||
return jwt.verify(token, this.jwtConfig.secret);
|
||||
} catch (error) {
|
||||
if (error instanceof TokenExpiredError) {
|
||||
error.name = "TokenExpiredError";
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof JsonWebTokenError) {
|
||||
error.name = "JsonWebTokenError";
|
||||
throw error;
|
||||
}
|
||||
throw new Error("无效的认证令牌");
|
||||
}
|
||||
}
|
||||
|
||||
decodeToken(token: string): any {
|
||||
return jwt.decode(token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"lib": ["ES2020"],
|
||||
"types": ["node"],
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"inlineSourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["src/*"] }
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
|
||||
}
|
||||
+4238
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user