refactor: 重构项目目录与代码结构,清理冗余文件
本次提交进行了大规模的项目结构调整: 1. 调整文件目录归属,将模块代码按功能域重新组织 2. 删除多处冗余的DTO、实体类和模块文件 3. 统一了常量、装饰器、工具类的存放位置 4. 修复了`.gitignore`的日志目录匹配规则 5. 新增了健康检查、认证、用户、剧集、订单等核心业务模块
This commit is contained in:
601
src/admin/admin.service.ts
Normal file
601
src/admin/admin.service.ts
Normal file
@@ -0,0 +1,601 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
OnModuleInit,
|
||||
Optional,
|
||||
UnauthorizedException,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
|
||||
import { AdminLoginDto } from './dto/admin-login.dto';
|
||||
import { CreateAdminUserDto } from './dto/create-admin-user.dto';
|
||||
import { CreatePermissionDto } from './dto/create-permission.dto';
|
||||
import { CreateRoleDto } from './dto/create-role.dto';
|
||||
import { UpdateAdminProfileDto } from './dto/update-admin-profile.dto';
|
||||
import { AdminUser } from './entities/admin-user.entity';
|
||||
import { Permission } from './entities/permission.entity';
|
||||
import { Role } from './entities/role.entity';
|
||||
import {
|
||||
AdminUserRecord,
|
||||
AdminUserView,
|
||||
PermissionRecord,
|
||||
RoleRecord,
|
||||
} from './interfaces/admin-records.interface';
|
||||
|
||||
interface PaginationInput {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminService implements OnModuleInit {
|
||||
private permissionSequence = 1;
|
||||
private roleSequence = 1;
|
||||
private adminSequence = 1;
|
||||
private readonly permissions: PermissionRecord[] = [];
|
||||
private readonly roles: RoleRecord[] = [];
|
||||
private readonly admins: AdminUserRecord[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
@Optional()
|
||||
@InjectDataSource()
|
||||
readonly dataSource?: DataSource,
|
||||
) {
|
||||
if (!this.hasDatabase()) {
|
||||
this.seedMemory();
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
if (this.hasDatabase()) {
|
||||
await this.seedDatabase();
|
||||
await this.loadDatabaseRecords();
|
||||
}
|
||||
}
|
||||
|
||||
async login(dto: AdminLoginDto) {
|
||||
const admin = this.admins.find((item) => item.username === dto.username);
|
||||
if (
|
||||
!admin ||
|
||||
admin.status !== 'active' ||
|
||||
!bcrypt.compareSync(dto.password, admin.passwordHash)
|
||||
) {
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: await this.jwtService.signAsync({
|
||||
sub: admin.id,
|
||||
username: admin.username,
|
||||
type: 'admin',
|
||||
}),
|
||||
admin: this.toAdminView(admin),
|
||||
};
|
||||
}
|
||||
|
||||
async createPermission(dto: CreatePermissionDto): Promise<PermissionRecord> {
|
||||
if (this.hasDatabase()) {
|
||||
const repository = this.dataSource.getRepository(Permission);
|
||||
if (await repository.findOne({ where: { code: dto.code } })) {
|
||||
throw new ConflictException('Permission code already exists');
|
||||
}
|
||||
const saved = await repository.save(repository.create(dto));
|
||||
await this.loadDatabaseRecords();
|
||||
return this.toPermissionRecord(saved);
|
||||
}
|
||||
|
||||
if (this.permissions.some((item) => item.code === dto.code)) {
|
||||
throw new ConflictException('Permission code already exists');
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const permission: PermissionRecord = {
|
||||
id: this.permissionSequence++,
|
||||
code: dto.code,
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.permissions.push(permission);
|
||||
return permission;
|
||||
}
|
||||
|
||||
async listPermissions(
|
||||
pagination: PaginationInput,
|
||||
): Promise<PaginatedResponse<PermissionRecord>> {
|
||||
if (this.hasDatabase()) {
|
||||
await this.loadDatabaseRecords();
|
||||
}
|
||||
|
||||
return this.paginate([...this.permissions], pagination);
|
||||
}
|
||||
|
||||
async createRole(dto: CreateRoleDto): Promise<RoleRecord> {
|
||||
if (this.hasDatabase()) {
|
||||
const repository = this.dataSource.getRepository(Role);
|
||||
if (await repository.findOne({ where: { code: dto.code } })) {
|
||||
throw new ConflictException('Role code already exists');
|
||||
}
|
||||
const saved = await repository.save(
|
||||
repository.create({
|
||||
code: dto.code,
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
permissionIds: dto.permissionIds ?? [],
|
||||
}),
|
||||
);
|
||||
await this.loadDatabaseRecords();
|
||||
return this.roles.find((role) => role.id === Number(saved.id)) as RoleRecord;
|
||||
}
|
||||
|
||||
if (this.roles.some((item) => item.code === dto.code)) {
|
||||
throw new ConflictException('Role code already exists');
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const permissionIds = dto.permissionIds ?? [];
|
||||
const role: RoleRecord = {
|
||||
id: this.roleSequence++,
|
||||
code: dto.code,
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
permissionIds,
|
||||
permissions: this.permissions.filter((permission) =>
|
||||
permissionIds.includes(permission.id),
|
||||
),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.roles.push(role);
|
||||
return role;
|
||||
}
|
||||
|
||||
async listRoles(pagination: PaginationInput): Promise<PaginatedResponse<RoleRecord>> {
|
||||
if (this.hasDatabase()) {
|
||||
await this.loadDatabaseRecords();
|
||||
}
|
||||
|
||||
return this.paginate([...this.roles], pagination);
|
||||
}
|
||||
|
||||
async updateRolePermissions(id: number, permissionIds: number[]): Promise<RoleRecord> {
|
||||
if (this.hasDatabase()) {
|
||||
const repository = this.dataSource.getRepository(Role);
|
||||
const role = await repository.findOne({ where: { id } });
|
||||
if (!role) {
|
||||
throw new NotFoundException('Role not found');
|
||||
}
|
||||
role.permissionIds = [...new Set(permissionIds)];
|
||||
await repository.save(role);
|
||||
await this.loadDatabaseRecords();
|
||||
return this.roles.find((item) => item.id === id) as RoleRecord;
|
||||
}
|
||||
|
||||
const role = this.roles.find((item) => item.id === id);
|
||||
if (!role) {
|
||||
throw new NotFoundException('Role not found');
|
||||
}
|
||||
|
||||
const uniquePermissionIds = [...new Set(permissionIds)];
|
||||
role.permissionIds = uniquePermissionIds;
|
||||
role.permissions = this.permissions.filter((permission) =>
|
||||
uniquePermissionIds.includes(permission.id),
|
||||
);
|
||||
role.updatedAt = new Date().toISOString();
|
||||
return role;
|
||||
}
|
||||
|
||||
async createAdminUser(dto: CreateAdminUserDto): Promise<AdminUserView> {
|
||||
if (this.hasDatabase()) {
|
||||
const repository = this.dataSource.getRepository(AdminUser);
|
||||
if (await repository.findOne({ where: { username: dto.username } })) {
|
||||
throw new ConflictException('Admin username already exists');
|
||||
}
|
||||
const saved = await repository.save(
|
||||
repository.create({
|
||||
username: dto.username,
|
||||
passwordHash: bcrypt.hashSync(dto.password, 10),
|
||||
nickname: dto.nickname,
|
||||
email: dto.email,
|
||||
status: 'active',
|
||||
isSuperAdmin: false,
|
||||
roleIds: dto.roleIds ?? [],
|
||||
}),
|
||||
);
|
||||
await this.loadDatabaseRecords();
|
||||
return this.toAdminView(
|
||||
this.admins.find((admin) => admin.id === Number(saved.id)) as AdminUserRecord,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.admins.some((item) => item.username === dto.username)) {
|
||||
throw new ConflictException('Admin username already exists');
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const admin: AdminUserRecord = {
|
||||
id: this.adminSequence++,
|
||||
username: dto.username,
|
||||
passwordHash: bcrypt.hashSync(dto.password, 10),
|
||||
nickname: dto.nickname,
|
||||
email: dto.email,
|
||||
status: 'active',
|
||||
isSuperAdmin: false,
|
||||
roleIds: dto.roleIds ?? [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.admins.push(admin);
|
||||
return this.toAdminView(admin);
|
||||
}
|
||||
|
||||
async listAdminUsers(
|
||||
pagination: PaginationInput,
|
||||
): Promise<PaginatedResponse<AdminUserView>> {
|
||||
if (this.hasDatabase()) {
|
||||
await this.loadDatabaseRecords();
|
||||
}
|
||||
|
||||
return this.paginate(
|
||||
this.admins.map((admin) => this.toAdminView(admin)),
|
||||
pagination,
|
||||
);
|
||||
}
|
||||
|
||||
async updateAdminStatus(
|
||||
id: number,
|
||||
status: 'active' | 'disabled',
|
||||
): Promise<AdminUserView> {
|
||||
if (this.hasDatabase()) {
|
||||
const repository = this.dataSource.getRepository(AdminUser);
|
||||
const admin = await repository.findOne({ where: { id } });
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin user not found');
|
||||
}
|
||||
admin.status = status;
|
||||
await repository.save(admin);
|
||||
await this.loadDatabaseRecords();
|
||||
return this.toAdminView(this.findAdminById(id) as AdminUserRecord);
|
||||
}
|
||||
|
||||
const admin = this.findAdminById(id);
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin user not found');
|
||||
}
|
||||
|
||||
admin.status = status;
|
||||
admin.updatedAt = new Date().toISOString();
|
||||
return this.toAdminView(admin);
|
||||
}
|
||||
|
||||
async updateAdminRoles(id: number, roleIds: number[]): Promise<AdminUserView> {
|
||||
if (this.hasDatabase()) {
|
||||
const repository = this.dataSource.getRepository(AdminUser);
|
||||
const admin = await repository.findOne({ where: { id } });
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin user not found');
|
||||
}
|
||||
admin.roleIds = [...new Set(roleIds)];
|
||||
await repository.save(admin);
|
||||
await this.loadDatabaseRecords();
|
||||
return this.toAdminView(this.findAdminById(id) as AdminUserRecord);
|
||||
}
|
||||
|
||||
const admin = this.findAdminById(id);
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin user not found');
|
||||
}
|
||||
|
||||
admin.roleIds = [...new Set(roleIds)];
|
||||
admin.updatedAt = new Date().toISOString();
|
||||
return this.toAdminView(admin);
|
||||
}
|
||||
|
||||
async updateProfile(
|
||||
adminId: number,
|
||||
dto: UpdateAdminProfileDto,
|
||||
): Promise<AdminUserView> {
|
||||
if (this.hasDatabase()) {
|
||||
const repository = this.dataSource.getRepository(AdminUser);
|
||||
const admin = await repository.findOne({ where: { id: adminId } });
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin user not found');
|
||||
}
|
||||
admin.nickname = dto.nickname;
|
||||
admin.email = dto.email;
|
||||
await repository.save(admin);
|
||||
await this.loadDatabaseRecords();
|
||||
return this.toAdminView(this.findAdminById(adminId) as AdminUserRecord);
|
||||
}
|
||||
|
||||
const admin = this.findAdminById(adminId);
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin user not found');
|
||||
}
|
||||
|
||||
admin.nickname = dto.nickname;
|
||||
admin.email = dto.email;
|
||||
admin.updatedAt = new Date().toISOString();
|
||||
return this.toAdminView(admin);
|
||||
}
|
||||
|
||||
async changePassword(
|
||||
adminId: number,
|
||||
oldPassword: string,
|
||||
newPassword: string,
|
||||
): Promise<Record<string, never>> {
|
||||
if (this.hasDatabase()) {
|
||||
const repository = this.dataSource.getRepository(AdminUser);
|
||||
const admin = await repository.findOne({ where: { id: adminId } });
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin user not found');
|
||||
}
|
||||
if (!bcrypt.compareSync(oldPassword, admin.passwordHash)) {
|
||||
throw new UnprocessableEntityException('Old password is incorrect');
|
||||
}
|
||||
admin.passwordHash = bcrypt.hashSync(newPassword, 10);
|
||||
await repository.save(admin);
|
||||
await this.loadDatabaseRecords();
|
||||
return {};
|
||||
}
|
||||
|
||||
const admin = this.findAdminById(adminId);
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin user not found');
|
||||
}
|
||||
|
||||
if (!bcrypt.compareSync(oldPassword, admin.passwordHash)) {
|
||||
throw new UnprocessableEntityException('Old password is incorrect');
|
||||
}
|
||||
|
||||
admin.passwordHash = bcrypt.hashSync(newPassword, 10);
|
||||
admin.updatedAt = new Date().toISOString();
|
||||
return {};
|
||||
}
|
||||
|
||||
findAdminById(id: number) {
|
||||
return this.admins.find((admin) => admin.id === id);
|
||||
}
|
||||
|
||||
getPermissionCodesForAdmin(adminId: number) {
|
||||
const admin = this.findAdminById(adminId);
|
||||
if (!admin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (admin.isSuperAdmin) {
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
return this.roles
|
||||
.filter((role) => admin.roleIds.includes(role.id))
|
||||
.flatMap((role) => role.permissions.map((permission) => permission.code));
|
||||
}
|
||||
|
||||
toAdminView(admin: AdminUserRecord): AdminUserView {
|
||||
const roles = this.roles.filter((role) => admin.roleIds.includes(role.id));
|
||||
const permissions: PermissionRecord[] | ['*'] = admin.isSuperAdmin
|
||||
? ['*']
|
||||
: roles.flatMap((role) => role.permissions);
|
||||
|
||||
return {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
nickname: admin.nickname,
|
||||
email: admin.email,
|
||||
status: admin.status,
|
||||
isSuperAdmin: admin.isSuperAdmin,
|
||||
roles,
|
||||
permissions,
|
||||
createdAt: admin.createdAt,
|
||||
updatedAt: admin.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private seedMemory() {
|
||||
const codes = [
|
||||
['permission:read', '查看权限'],
|
||||
['permission:create', '创建权限'],
|
||||
['role:read', '查看角色'],
|
||||
['role:create', '创建角色'],
|
||||
['role:update', '更新角色'],
|
||||
['admin-user:read', '查看人员'],
|
||||
['admin-user:create', '创建人员'],
|
||||
['admin-user:update', '更新人员'],
|
||||
['user:read', '查看用户'],
|
||||
['user:update', '更新用户'],
|
||||
['drama:read', '查看短剧'],
|
||||
['drama:create', '创建短剧'],
|
||||
['drama:update', '更新短剧'],
|
||||
['episode:create', '创建剧集'],
|
||||
['episode:update', '更新剧集'],
|
||||
['media:read', '查看媒体'],
|
||||
['media:create', '创建媒体'],
|
||||
['data:read', '查看数据'],
|
||||
['data:sync', '同步数据'],
|
||||
['payment:read', '查看支付记录'],
|
||||
['log:read', '查看日志'],
|
||||
];
|
||||
|
||||
for (const [code, name] of codes) {
|
||||
this.createPermission({ code, name });
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
this.admins.push({
|
||||
id: this.adminSequence++,
|
||||
username: process.env.ADMIN_USERNAME ?? 'admin',
|
||||
passwordHash: bcrypt.hashSync(process.env.ADMIN_PASSWORD ?? 'admin123', 10),
|
||||
nickname: 'Super Admin',
|
||||
email: process.env.ADMIN_EMAIL ?? 'admin@example.com',
|
||||
status: 'active',
|
||||
isSuperAdmin: true,
|
||||
roleIds: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
private hasDatabase(): this is this & { dataSource: DataSource } {
|
||||
return Boolean(this.dataSource?.isInitialized);
|
||||
}
|
||||
|
||||
private async seedDatabase() {
|
||||
const dataSource = this.dataSource as DataSource;
|
||||
const permissionRepository = dataSource.getRepository(Permission);
|
||||
for (const [code, name] of this.permissionSeeds()) {
|
||||
const existing = await permissionRepository.findOne({ where: { code } });
|
||||
if (!existing) {
|
||||
await permissionRepository.save(
|
||||
permissionRepository.create({ code, name }),
|
||||
);
|
||||
} else if (existing.name !== name) {
|
||||
existing.name = name;
|
||||
await permissionRepository.save(existing);
|
||||
}
|
||||
}
|
||||
|
||||
const adminRepository = dataSource.getRepository(AdminUser);
|
||||
const username = process.env.ADMIN_USERNAME ?? 'admin';
|
||||
const existingAdmin = await adminRepository.findOne({ where: { username } });
|
||||
if (!existingAdmin) {
|
||||
await adminRepository.save(
|
||||
adminRepository.create({
|
||||
username,
|
||||
passwordHash: bcrypt.hashSync(process.env.ADMIN_PASSWORD ?? 'admin123', 10),
|
||||
nickname: 'Super Admin',
|
||||
email: process.env.ADMIN_EMAIL ?? 'admin@example.com',
|
||||
status: 'active',
|
||||
isSuperAdmin: true,
|
||||
roleIds: [],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadDatabaseRecords() {
|
||||
const dataSource = this.dataSource as DataSource;
|
||||
const permissions = await dataSource.getRepository(Permission).find({
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
this.permissions.splice(
|
||||
0,
|
||||
this.permissions.length,
|
||||
...permissions.map((permission) => this.toPermissionRecord(permission)),
|
||||
);
|
||||
|
||||
const roles = await dataSource.getRepository(Role).find({
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
this.roles.splice(
|
||||
0,
|
||||
this.roles.length,
|
||||
...roles.map((role) => this.toRoleRecord(role)),
|
||||
);
|
||||
|
||||
const admins = await dataSource.getRepository(AdminUser).find({
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
this.admins.splice(
|
||||
0,
|
||||
this.admins.length,
|
||||
...admins.map((admin) => this.toAdminRecord(admin)),
|
||||
);
|
||||
}
|
||||
|
||||
private permissionSeeds() {
|
||||
return [
|
||||
['permission:read', '查看权限'],
|
||||
['permission:create', '创建权限'],
|
||||
['role:read', '查看角色'],
|
||||
['role:create', '创建角色'],
|
||||
['role:update', '更新角色'],
|
||||
['admin-user:read', '查看人员'],
|
||||
['admin-user:create', '创建人员'],
|
||||
['admin-user:update', '更新人员'],
|
||||
['user:read', '查看用户'],
|
||||
['user:update', '更新用户'],
|
||||
['drama:read', '查看短剧'],
|
||||
['drama:create', '创建短剧'],
|
||||
['drama:update', '更新短剧'],
|
||||
['episode:create', '创建剧集'],
|
||||
['episode:update', '更新剧集'],
|
||||
['media:read', '查看媒体'],
|
||||
['media:create', '创建媒体'],
|
||||
['data:read', '查看数据'],
|
||||
['data:sync', '同步数据'],
|
||||
['payment:read', '查看支付记录'],
|
||||
['log:read', '查看日志'],
|
||||
] as const;
|
||||
}
|
||||
|
||||
private toPermissionRecord(permission: Permission): PermissionRecord {
|
||||
return {
|
||||
id: Number(permission.id),
|
||||
code: permission.code,
|
||||
name: permission.name,
|
||||
description: permission.description,
|
||||
createdAt: permission.createdAt?.toISOString?.() ?? new Date().toISOString(),
|
||||
updatedAt: permission.updatedAt?.toISOString?.() ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
private toRoleRecord(role: Role): RoleRecord {
|
||||
const permissionIds = role.permissionIds ?? [];
|
||||
return {
|
||||
id: Number(role.id),
|
||||
code: role.code,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
permissionIds,
|
||||
permissions: this.permissions.filter((permission) =>
|
||||
permissionIds.includes(permission.id),
|
||||
),
|
||||
createdAt: role.createdAt?.toISOString?.() ?? new Date().toISOString(),
|
||||
updatedAt: role.updatedAt?.toISOString?.() ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
private toAdminRecord(admin: AdminUser): AdminUserRecord {
|
||||
return {
|
||||
id: Number(admin.id),
|
||||
username: admin.username,
|
||||
passwordHash: admin.passwordHash,
|
||||
nickname: admin.nickname,
|
||||
email: admin.email,
|
||||
status: admin.status as 'active' | 'disabled',
|
||||
isSuperAdmin: admin.isSuperAdmin,
|
||||
roleIds: admin.roleIds ?? [],
|
||||
createdAt: admin.createdAt?.toISOString?.() ?? new Date().toISOString(),
|
||||
updatedAt: admin.updatedAt?.toISOString?.() ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
private paginate<T>(
|
||||
records: T[],
|
||||
input: PaginationInput,
|
||||
): PaginatedResponse<T> {
|
||||
const page = Math.max(input.page, 1);
|
||||
const pageSize = Math.min(Math.max(input.pageSize, 1), 100);
|
||||
const start = (page - 1) * pageSize;
|
||||
|
||||
return {
|
||||
list: records.slice(start, start + pageSize),
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total: records.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user