refactor: 重构项目目录与代码结构,清理冗余文件

本次提交进行了大规模的项目结构调整:
1. 调整文件目录归属,将模块代码按功能域重新组织
2. 删除多处冗余的DTO、实体类和模块文件
3. 统一了常量、装饰器、工具类的存放位置
4. 修复了`.gitignore`的日志目录匹配规则
5. 新增了健康检查、认证、用户、剧集、订单等核心业务模块
This commit is contained in:
2026-07-02 19:17:43 +08:00
parent 55e20f9f16
commit e84351e8ed
99 changed files with 522 additions and 441 deletions

601
src/admin/admin.service.ts Normal file
View 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,
},
};
}
}