1. 新增频道模块,包含类型定义、实体、DTO、控制器和服务 2. 为短剧添加频道ID和频道名字段,支持关联频道 3. 新增短剧删除API和权限配置 4. 调整tsconfig和依赖配置,更新权限列表 5. 新增相关测试用例验证频道与短剧关联逻辑
604 lines
18 KiB
TypeScript
604 lines
18 KiB
TypeScript
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 { ADMIN_AUTH_CONFIG } from '@config/app.config';
|
|
import { PaginatedResponse } from '@app/common/paginated-response.dto';
|
|
import { AdminLoginDto, CreateAdminUserDto, CreatePermissionDto, CreateRoleDto, UpdateAdminProfileDto } from './admin.dto';
|
|
import { AdminUser } from './admin-user.entity';
|
|
import { Permission } from './permission.entity';
|
|
import { Role } from './role.entity';
|
|
import {
|
|
AdminUserRecord,
|
|
AdminUserView,
|
|
PermissionRecord,
|
|
RoleRecord,
|
|
} from './admin.types';
|
|
|
|
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', '更新用户'],
|
|
['channel:read', '查看频道'],
|
|
['channel:create', '创建频道'],
|
|
['channel:update', '更新频道'],
|
|
['channel:delete', '删除频道'],
|
|
['drama:read', '查看短剧'],
|
|
['drama:create', '创建短剧'],
|
|
['drama:update', '更新短剧'],
|
|
['drama:delete', '删除短剧'],
|
|
['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: ADMIN_AUTH_CONFIG.username,
|
|
passwordHash: bcrypt.hashSync(ADMIN_AUTH_CONFIG.password, 10),
|
|
nickname: 'Super Admin',
|
|
email: ADMIN_AUTH_CONFIG.email,
|
|
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 = ADMIN_AUTH_CONFIG.username;
|
|
const existingAdmin = await adminRepository.findOne({ where: { username } });
|
|
if (!existingAdmin) {
|
|
await adminRepository.save(
|
|
adminRepository.create({
|
|
username,
|
|
passwordHash: bcrypt.hashSync(ADMIN_AUTH_CONFIG.password, 10),
|
|
nickname: 'Super Admin',
|
|
email: ADMIN_AUTH_CONFIG.email,
|
|
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,
|
|
},
|
|
};
|
|
}
|
|
}
|