feat: 初始化 WWJ Cloud 企业级框架项目

- 后端:基于 NestJS 的分层架构设计
- 前端:基于 VbenAdmin + Element Plus 的管理系统
- 支持 SaaS + 独立版双架构模式
- 完整的用户权限管理系统
- 系统设置、文件上传、通知等核心功能
- 多租户支持和插件化扩展架构
This commit is contained in:
万物街
2025-08-23 13:20:01 +08:00
commit f30d64e6cc
172 changed files with 10179 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
ParseIntPipe,
HttpStatus,
Req,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { AdminService } from './admin.service';
import { CreateAdminDto, UpdateAdminDto, QueryAdminDto } from './dto';
import { SysUser } from './entities/sys-user.entity';
import { Request } from 'express';
@ApiTags('管理员管理')
@Controller('admin')
export class AdminController {
constructor(private readonly adminService: AdminService) {}
@Post()
@ApiOperation({ summary: '创建管理员' })
@ApiResponse({ status: HttpStatus.CREATED, description: '创建成功', type: SysUser })
@ApiResponse({ status: HttpStatus.CONFLICT, description: '用户名或手机号已存在' })
async create(@Body() createAdminDto: CreateAdminDto) {
const admin = await this.adminService.create(createAdminDto);
return {
code: 200,
message: '创建成功',
data: admin,
};
}
@Get()
@ApiOperation({ summary: '获取管理员列表' })
@ApiResponse({ status: HttpStatus.OK, description: '获取成功' })
async findAll(@Query() queryDto: QueryAdminDto) {
const result = await this.adminService.findAll(queryDto);
return {
code: 200,
message: '获取成功',
data: result,
};
}
@Get(':id')
@ApiOperation({ summary: '获取管理员详情' })
@ApiResponse({ status: HttpStatus.OK, description: '获取成功', type: SysUser })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '管理员不存在' })
async findOne(@Param('id', ParseIntPipe) id: number) {
const admin = await this.adminService.findOne(id);
return {
code: 200,
message: '获取成功',
data: admin,
};
}
@Patch(':id')
@ApiOperation({ summary: '更新管理员信息' })
@ApiResponse({ status: HttpStatus.OK, description: '更新成功', type: SysUser })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '管理员不存在' })
@ApiResponse({ status: HttpStatus.CONFLICT, description: '用户名或手机号已存在' })
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateAdminDto: UpdateAdminDto,
) {
const admin = await this.adminService.update(id, updateAdminDto);
return {
code: 200,
message: '更新成功',
data: admin,
};
}
@Delete(':id')
@ApiOperation({ summary: '删除管理员' })
@ApiResponse({ status: HttpStatus.OK, description: '删除成功' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '管理员不存在' })
async remove(@Param('id', ParseIntPipe) id: number) {
await this.adminService.remove(id);
return {
code: 200,
message: '删除成功',
};
}
@Post('batch-delete')
@ApiOperation({ summary: '批量删除管理员' })
@ApiResponse({ status: HttpStatus.OK, description: '批量删除成功' })
async batchRemove(@Body('ids') ids: number[]) {
await this.adminService.batchRemove(ids);
return {
code: 200,
message: '批量删除成功',
};
}
@Post(':id/update-last-login')
@ApiOperation({ summary: '更新最后登录信息' })
@ApiResponse({ status: HttpStatus.OK, description: '更新成功' })
async updateLastLogin(
@Param('id', ParseIntPipe) id: number,
@Req() request: Request,
) {
const ip = request.ip || request.connection.remoteAddress || '';
await this.adminService.updateLastLogin(id, ip);
return {
code: 200,
message: '更新成功',
};
}
@Get('search/by-username/:username')
@ApiOperation({ summary: '根据用户名查询管理员' })
@ApiResponse({ status: HttpStatus.OK, description: '查询成功' })
async findByUsername(@Param('username') username: string) {
const admin = await this.adminService.findByUsername(username);
return {
code: 200,
message: '查询成功',
data: admin,
};
}
@Post(':id/assign-roles')
@ApiOperation({ summary: '分配角色' })
@ApiResponse({ status: HttpStatus.OK, description: '分配成功' })
async assignRoles(
@Param('id', ParseIntPipe) id: number,
@Body('roleIds') roleIds: number[],
@Body('siteId') siteId: number,
) {
await this.adminService.assignRoles(id, roleIds, siteId);
return {
code: 200,
message: '分配成功',
};
}
@Get(':id/roles')
@ApiOperation({ summary: '获取用户角色' })
@ApiResponse({ status: HttpStatus.OK, description: '获取成功' })
async getUserRoles(@Param('id', ParseIntPipe) id: number) {
const roleIds = await this.adminService.getUserRoles(id);
return {
code: 200,
message: '获取成功',
data: roleIds,
};
}
}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminService } from './admin.service';
import { AdminController } from './admin.controller';
import { SysUser } from './entities/sys-user.entity';
import { SysUserRole } from './entities/sys-user-role.entity';
@Module({
imports: [TypeOrmModule.forFeature([SysUser, SysUserRole])],
controllers: [AdminController],
providers: [AdminService],
exports: [AdminService, TypeOrmModule],
})
export class AdminModule {}

View File

@@ -0,0 +1,311 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { SysUser } from './entities/sys-user.entity';
import { SysUserRole } from './entities/sys-user-role.entity';
import { CreateAdminDto, UpdateAdminDto, QueryAdminDto } from './dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AdminService {
constructor(
@InjectRepository(SysUser)
private readonly sysUserRepository: Repository<SysUser>,
@InjectRepository(SysUserRole)
private readonly sysUserRoleRepository: Repository<SysUserRole>,
) {}
/**
* 创建管理员
*/
async create(createAdminDto: CreateAdminDto): Promise<SysUser> {
// 检查用户名是否已存在
const existingByUsername = await this.sysUserRepository.findOne({
where: { username: createAdminDto.username, deleteTime: 0 },
});
if (existingByUsername) {
throw new ConflictException('用户名已存在');
}
// 检查手机号是否已存在
if (createAdminDto.mobile) {
const existingByMobile = await this.sysUserRepository.findOne({
where: { mobile: createAdminDto.mobile, deleteTime: 0 },
});
if (existingByMobile) {
throw new ConflictException('手机号已存在');
}
}
// 密码加密
const hashedPassword = await bcrypt.hash(createAdminDto.password, 10);
const { roleIds, ...adminData } = createAdminDto;
const admin = this.sysUserRepository.create({
...adminData,
password: hashedPassword,
createTime: Math.floor(Date.now() / 1000),
updateTime: Math.floor(Date.now() / 1000),
});
const savedAdmin = await this.sysUserRepository.save(admin);
// 分配角色
if (roleIds && roleIds.length > 0) {
await this.assignRoles(savedAdmin.uid, roleIds, createAdminDto.siteId);
}
return await this.findOne(savedAdmin.uid);
}
/**
* 分页查询管理员列表
*/
async findAll(queryDto: QueryAdminDto) {
const { page = 1, limit = 10, keyword, siteId, sex, status, isAdmin, roleId, startTime, endTime } = queryDto;
const skip = (page - 1) * limit;
const queryBuilder = this.sysUserRepository.createQueryBuilder('admin')
.leftJoinAndSelect('admin.userRoles', 'userRole')
.where('admin.deleteTime = :deleteTime', { deleteTime: 0 });
// 关键词搜索
if (keyword) {
queryBuilder.andWhere(
'(admin.username LIKE :keyword OR admin.realName LIKE :keyword OR admin.mobile LIKE :keyword)',
{ keyword: `%${keyword}%` }
);
}
// 站点ID筛选
if (siteId !== undefined) {
queryBuilder.andWhere('admin.siteId = :siteId', { siteId });
}
// 性别筛选
if (sex !== undefined) {
queryBuilder.andWhere('admin.sex = :sex', { sex });
}
// 状态筛选
if (status !== undefined) {
queryBuilder.andWhere('admin.status = :status', { status });
}
// 超级管理员筛选
if (isAdmin !== undefined) {
queryBuilder.andWhere('admin.isAdmin = :isAdmin', { isAdmin });
}
// 角色筛选
if (roleId !== undefined) {
queryBuilder.andWhere('userRole.roleId = :roleId', { roleId });
}
// 时间范围筛选
if (startTime && endTime) {
queryBuilder.andWhere('admin.createTime BETWEEN :startTime AND :endTime', {
startTime,
endTime,
});
} else if (startTime) {
queryBuilder.andWhere('admin.createTime >= :startTime', { startTime });
} else if (endTime) {
queryBuilder.andWhere('admin.createTime <= :endTime', { endTime });
}
// 排序
queryBuilder.orderBy('admin.createTime', 'DESC');
// 分页
const [list, total] = await queryBuilder
.skip(skip)
.take(limit)
.getManyAndCount();
// 移除密码字段
const safeList = list.map(admin => {
const { password, ...safeAdmin } = admin;
return {
...safeAdmin,
roleIds: admin.userRoles?.map(ur => ur.roleId) || [],
};
});
return {
list: safeList,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
/**
* 根据ID查询管理员详情
*/
async findOne(id: number): Promise<SysUser> {
const admin = await this.sysUserRepository.findOne({
where: { uid: id, deleteTime: 0 },
relations: ['userRoles'],
});
if (!admin) {
throw new NotFoundException('管理员不存在');
}
// 移除密码字段
const { password, ...safeAdmin } = admin;
return {
...safeAdmin,
roleIds: admin.userRoles?.map(ur => ur.roleId) || [],
} as any;
}
/**
* 根据用户名查询管理员
*/
async findByUsername(username: string): Promise<SysUser | null> {
return await this.sysUserRepository.findOne({
where: { username, deleteTime: 0 },
relations: ['userRoles'],
});
}
/**
* 更新管理员信息
*/
async update(id: number, updateAdminDto: UpdateAdminDto): Promise<SysUser> {
const admin = await this.findOne(id);
// 检查用户名是否已被其他用户使用
if (updateAdminDto.username && updateAdminDto.username !== admin.username) {
const existingByUsername = await this.sysUserRepository.findOne({
where: { username: updateAdminDto.username, deleteTime: 0 },
});
if (existingByUsername && existingByUsername.uid !== id) {
throw new ConflictException('用户名已存在');
}
}
// 检查手机号是否已被其他用户使用
if (updateAdminDto.mobile && updateAdminDto.mobile !== admin.mobile) {
const existingByMobile = await this.sysUserRepository.findOne({
where: { mobile: updateAdminDto.mobile, deleteTime: 0 },
});
if (existingByMobile && existingByMobile.uid !== id) {
throw new ConflictException('手机号已存在');
}
}
const { roleIds, ...adminData } = updateAdminDto;
// 如果更新密码,需要加密
if (adminData.password) {
adminData.password = await bcrypt.hash(adminData.password, 10);
}
await this.sysUserRepository.update(id, {
...adminData,
updateTime: Math.floor(Date.now() / 1000),
});
// 更新角色分配
if (roleIds !== undefined) {
await this.updateRoles(id, roleIds, admin.siteId);
}
return await this.findOne(id);
}
/**
* 软删除管理员
*/
async remove(id: number): Promise<void> {
const admin = await this.findOne(id);
await this.sysUserRepository.update(id, {
deleteTime: Math.floor(Date.now() / 1000),
updateTime: Math.floor(Date.now() / 1000),
});
// 删除用户角色关联
await this.sysUserRoleRepository.delete({ uid: id });
}
/**
* 批量软删除管理员
*/
async batchRemove(ids: number[]): Promise<void> {
const deleteTime = Math.floor(Date.now() / 1000);
await this.sysUserRepository.update(
{ uid: In(ids) },
{
deleteTime,
updateTime: deleteTime,
}
);
// 删除用户角色关联
await this.sysUserRoleRepository.delete({ uid: In(ids) });
}
/**
* 更新最后登录信息
*/
async updateLastLogin(id: number, ip: string): Promise<void> {
const now = Math.floor(Date.now() / 1000);
await this.sysUserRepository.update(id, {
lastTime: now,
lastIp: ip,
updateTime: now,
});
}
/**
* 验证密码
*/
async validatePassword(admin: SysUser, password: string): Promise<boolean> {
return await bcrypt.compare(password, admin.password);
}
/**
* 分配角色
*/
async assignRoles(uid: number, roleIds: number[], siteId: number): Promise<void> {
const userRoles = roleIds.map(roleId =>
this.sysUserRoleRepository.create({
uid,
roleId,
siteId,
})
);
await this.sysUserRoleRepository.save(userRoles);
}
/**
* 更新用户角色
*/
async updateRoles(uid: number, roleIds: number[], siteId: number): Promise<void> {
// 删除现有角色
await this.sysUserRoleRepository.delete({ uid });
// 分配新角色
if (roleIds.length > 0) {
await this.assignRoles(uid, roleIds, siteId);
}
}
/**
* 获取用户角色
*/
async getUserRoles(uid: number): Promise<number[]> {
const userRoles = await this.sysUserRoleRepository.find({
where: { uid },
});
return userRoles.map(ur => ur.roleId);
}
}

View File

@@ -0,0 +1,94 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsInt, IsEmail, IsIn, Length, IsArray } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateAdminDto {
@ApiProperty({ description: '站点ID' })
@IsInt()
@Transform(({ value }) => parseInt(value))
siteId: number;
@ApiProperty({ description: '用户名' })
@IsString()
@Length(1, 255)
username: string;
@ApiProperty({ description: '密码' })
@IsString()
@Length(6, 255)
password: string;
@ApiProperty({ description: '真实姓名' })
@IsString()
@Length(1, 255)
realName: string;
@ApiPropertyOptional({ description: '头像' })
@IsOptional()
@IsString()
headImg?: string;
@ApiPropertyOptional({ description: '手机号' })
@IsOptional()
@IsString()
@Length(11, 11)
mobile?: string;
@ApiPropertyOptional({ description: '邮箱' })
@IsOptional()
@IsEmail()
email?: string;
@ApiPropertyOptional({ description: '性别1男 2女 0保密' })
@IsOptional()
@IsIn([0, 1, 2])
@Transform(({ value }) => parseInt(value))
sex?: number;
@ApiPropertyOptional({ description: '生日' })
@IsOptional()
@IsString()
birthday?: string;
@ApiPropertyOptional({ description: '省份ID' })
@IsOptional()
@IsInt()
@Transform(({ value }) => parseInt(value))
pid?: number;
@ApiPropertyOptional({ description: '城市ID' })
@IsOptional()
@IsInt()
@Transform(({ value }) => parseInt(value))
cid?: number;
@ApiPropertyOptional({ description: '区县ID' })
@IsOptional()
@IsInt()
@Transform(({ value }) => parseInt(value))
did?: number;
@ApiPropertyOptional({ description: '详细地址' })
@IsOptional()
@IsString()
address?: string;
@ApiPropertyOptional({ description: '状态1正常 0禁用', default: 1 })
@IsOptional()
@IsIn([0, 1])
@Transform(({ value }) => parseInt(value))
status?: number;
@ApiPropertyOptional({ description: '是否超级管理员1是 0否', default: 0 })
@IsOptional()
@IsIn([0, 1])
@Transform(({ value }) => parseInt(value))
isAdmin?: number;
@ApiPropertyOptional({ description: '角色ID数组' })
@IsOptional()
@IsArray()
@IsInt({ each: true })
@Transform(({ value }) => Array.isArray(value) ? value.map(v => parseInt(v)) : [])
roleIds?: number[];
}

View File

@@ -0,0 +1,3 @@
export { CreateAdminDto } from './create-admin.dto';
export { UpdateAdminDto } from './update-admin.dto';
export { QueryAdminDto } from './query-admin.dto';

View File

@@ -0,0 +1,64 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, IsInt, IsIn } from 'class-validator';
import { Transform } from 'class-transformer';
export class QueryAdminDto {
@ApiPropertyOptional({ description: '页码', default: 1 })
@IsOptional()
@IsInt()
@Transform(({ value }) => parseInt(value) || 1)
page?: number = 1;
@ApiPropertyOptional({ description: '每页数量', default: 10 })
@IsOptional()
@IsInt()
@Transform(({ value }) => parseInt(value) || 10)
limit?: number = 10;
@ApiPropertyOptional({ description: '关键词搜索(用户名/真实姓名/手机号)' })
@IsOptional()
@IsString()
keyword?: string;
@ApiPropertyOptional({ description: '站点ID' })
@IsOptional()
@IsInt()
@Transform(({ value }) => parseInt(value))
siteId?: number;
@ApiPropertyOptional({ description: '性别1男 2女 0保密' })
@IsOptional()
@IsIn([0, 1, 2])
@Transform(({ value }) => parseInt(value))
sex?: number;
@ApiPropertyOptional({ description: '状态1正常 0禁用' })
@IsOptional()
@IsIn([0, 1])
@Transform(({ value }) => parseInt(value))
status?: number;
@ApiPropertyOptional({ description: '是否超级管理员1是 0否' })
@IsOptional()
@IsIn([0, 1])
@Transform(({ value }) => parseInt(value))
isAdmin?: number;
@ApiPropertyOptional({ description: '角色ID' })
@IsOptional()
@IsInt()
@Transform(({ value }) => parseInt(value))
roleId?: number;
@ApiPropertyOptional({ description: '开始时间(时间戳)' })
@IsOptional()
@IsInt()
@Transform(({ value }) => parseInt(value))
startTime?: number;
@ApiPropertyOptional({ description: '结束时间(时间戳)' })
@IsOptional()
@IsInt()
@Transform(({ value }) => parseInt(value))
endTime?: number;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateAdminDto } from './create-admin.dto';
export class UpdateAdminDto extends PartialType(CreateAdminDto) {}

View File

@@ -0,0 +1,27 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { SysUser } from './sys-user.entity';
@Entity('sys_user_role')
export class SysUserRole {
@ApiProperty({ description: '主键ID' })
@PrimaryGeneratedColumn({ name: 'id', type: 'int', unsigned: true })
id: number;
@ApiProperty({ description: '用户ID' })
@Column({ name: 'uid', type: 'int', default: 0 })
uid: number;
@ApiProperty({ description: '角色ID' })
@Column({ name: 'role_id', type: 'int', default: 0 })
roleId: number;
@ApiProperty({ description: '站点ID' })
@Column({ name: 'site_id', type: 'int', default: 0 })
siteId: number;
// 关联用户
@ManyToOne(() => SysUser, user => user.userRoles)
@JoinColumn({ name: 'uid' })
user: SysUser;
}

View File

@@ -0,0 +1,94 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { SysUserRole } from './sys-user-role.entity';
@Entity('sys_user')
export class SysUser {
@ApiProperty({ description: '用户ID' })
@PrimaryGeneratedColumn({ name: 'uid', type: 'int', unsigned: true })
uid: number;
@ApiProperty({ description: '站点ID' })
@Column({ name: 'site_id', type: 'int', default: 0 })
siteId: number;
@ApiProperty({ description: '用户名' })
@Column({ name: 'username', type: 'varchar', length: 255, default: '' })
username: string;
@ApiProperty({ description: '密码' })
@Column({ name: 'password', type: 'varchar', length: 255, default: '' })
password: string;
@ApiProperty({ description: '真实姓名' })
@Column({ name: 'real_name', type: 'varchar', length: 255, default: '' })
realName: string;
@ApiProperty({ description: '头像' })
@Column({ name: 'head_img', type: 'varchar', length: 255, default: '' })
headImg: string;
@ApiProperty({ description: '手机号' })
@Column({ name: 'mobile', type: 'varchar', length: 20, default: '' })
mobile: string;
@ApiProperty({ description: '邮箱' })
@Column({ name: 'email', type: 'varchar', length: 255, default: '' })
email: string;
@ApiProperty({ description: '性别1男 2女 0保密' })
@Column({ name: 'sex', type: 'tinyint', default: 0 })
sex: number;
@ApiProperty({ description: '生日' })
@Column({ name: 'birthday', type: 'varchar', length: 255, default: '' })
birthday: string;
@ApiProperty({ description: '省份ID' })
@Column({ name: 'pid', type: 'int', default: 0 })
pid: number;
@ApiProperty({ description: '城市ID' })
@Column({ name: 'cid', type: 'int', default: 0 })
cid: number;
@ApiProperty({ description: '区县ID' })
@Column({ name: 'did', type: 'int', default: 0 })
did: number;
@ApiProperty({ description: '详细地址' })
@Column({ name: 'address', type: 'varchar', length: 255, default: '' })
address: string;
@ApiProperty({ description: '状态1正常 0禁用' })
@Column({ name: 'status', type: 'tinyint', default: 1 })
status: number;
@ApiProperty({ description: '是否超级管理员1是 0否' })
@Column({ name: 'is_admin', type: 'tinyint', default: 0 })
isAdmin: number;
@ApiProperty({ description: '最后登录时间' })
@Column({ name: 'last_time', type: 'int', default: 0 })
lastTime: number;
@ApiProperty({ description: '最后登录IP' })
@Column({ name: 'last_ip', type: 'varchar', length: 255, default: '' })
lastIp: string;
@ApiProperty({ description: '创建时间' })
@CreateDateColumn({ name: 'create_time', type: 'int' })
createTime: number;
@ApiProperty({ description: '更新时间' })
@UpdateDateColumn({ name: 'update_time', type: 'int' })
updateTime: number;
@ApiProperty({ description: '删除时间' })
@Column({ name: 'delete_time', type: 'int', default: 0 })
deleteTime: number;
// 关联用户角色
@OneToMany(() => SysUserRole, userRole => userRole.user)
userRoles: SysUserRole[];
}

View File

@@ -0,0 +1,6 @@
export { AdminModule } from './admin.module';
export { AdminService } from './admin.service';
export { AdminController } from './admin.controller';
export { SysUser } from './entities/sys-user.entity';
export { SysUserRole } from './entities/sys-user-role.entity';
export * from './dto';