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

37
src/users/user.entity.ts Normal file
View File

@@ -0,0 +1,37 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Index({ unique: true })
@Column({ type: 'varchar', length: 128 })
tiktokOpenId: string;
@Index()
@Column({ type: 'varchar', length: 128, nullable: true })
tiktokUnionId?: string;
@Column({ type: 'varchar', length: 128, nullable: true })
nickname?: string;
@Column({ type: 'varchar', length: 1024, nullable: true })
avatarUrl?: string;
@Column({ type: 'varchar', length: 32, default: 'active' })
status: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@@ -0,0 +1,36 @@
import { Body, Controller, Get, Param, Patch, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { RequirePermissions } from '../admin/decorators/require-permissions.decorator';
import { AdminJwtAuthGuard } from '../admin/guards/admin-jwt-auth.guard';
import { PermissionsGuard } from '../admin/guards/permissions.guard';
import { UpdateUserStatusDto } from './dto/update-user-status.dto';
import { UsersService } from './users.service';
@ApiTags('Users')
@Controller('/api/admin/v1/users')
@ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
@RequirePermissions('user:read')
listUsers(@Query('page') page?: string, @Query('pageSize') pageSize?: string) {
return this.usersService.listUsers({
page: Number(page) || 1,
pageSize: Number(pageSize) || 20,
});
}
@Get('/:id')
@RequirePermissions('user:read')
getUser(@Param('id') id: string) {
return this.usersService.getUserOrThrow(Number(id));
}
@Patch('/:id/status')
@RequirePermissions('user:update')
updateStatus(@Param('id') id: string, @Body() dto: UpdateUserStatusDto) {
return this.usersService.updateStatus(Number(id), dto.status);
}
}

8
src/users/users.dto.ts Normal file
View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsIn } from 'class-validator';
export class UpdateUserStatusDto {
@ApiProperty({ enum: ['active', 'disabled'] })
@IsIn(['active', 'disabled'])
status: 'active' | 'disabled';
}

12
src/users/users.module.ts Normal file
View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AdminModule } from '../admin/admin.module';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [AdminModule],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

172
src/users/users.service.ts Normal file
View File

@@ -0,0 +1,172 @@
import { Injectable, NotFoundException, Optional } from "@nestjs/common";
import { InjectDataSource } from "@nestjs/typeorm";
import { DataSource } from "typeorm";
import { PaginatedResponse } from "../../common/dto/paginated-response.dto";
import { User } from "./entities/user.entity";
import { UserRecord } from "./interfaces/user-record.interface";
interface UpsertTikTokUserInput {
tiktokOpenId: string;
tiktokUnionId?: string;
nickname?: string;
avatarUrl?: string;
}
interface PaginationInput {
page: number;
pageSize: number;
}
@Injectable()
export class UsersService {
private userSequence = 1;
private readonly users: UserRecord[] = [];
constructor(
@Optional()
@InjectDataSource()
readonly dataSource?: DataSource,
) {}
async upsertTikTokUser(input: UpsertTikTokUserInput) {
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(User);
const existing = await repository.findOne({
where: { tiktokOpenId: input.tiktokOpenId },
});
if (existing) {
existing.tiktokUnionId = input.tiktokUnionId ?? existing.tiktokUnionId;
existing.nickname = input.nickname ?? existing.nickname;
existing.avatarUrl = input.avatarUrl ?? existing.avatarUrl;
const saved = await repository.save(existing);
return { user: this.toRecord(saved), isNewUser: false };
}
const user = repository.create({
tiktokOpenId: input.tiktokOpenId,
tiktokUnionId: input.tiktokUnionId,
nickname: input.nickname,
avatarUrl: input.avatarUrl,
status: "active",
});
const saved = await repository.save(user);
return { user: this.toRecord(saved), isNewUser: true };
}
const existing = this.users.find((user) => user.tiktokOpenId === input.tiktokOpenId);
const now = new Date().toISOString();
if (existing) {
existing.tiktokUnionId = input.tiktokUnionId ?? existing.tiktokUnionId;
existing.nickname = input.nickname ?? existing.nickname;
existing.avatarUrl = input.avatarUrl ?? existing.avatarUrl;
existing.updatedAt = now;
return { user: existing, isNewUser: false };
}
const user: UserRecord = {
id: this.userSequence++,
tiktokOpenId: input.tiktokOpenId,
tiktokUnionId: input.tiktokUnionId,
nickname: input.nickname,
avatarUrl: input.avatarUrl,
status: "active",
createdAt: now,
updatedAt: now,
};
this.users.push(user);
return { user, isNewUser: true };
}
async findById(id: number) {
if (this.hasDatabase()) {
const user = await this.dataSource.getRepository(User).findOne({
where: { id },
});
return user ? this.toRecord(user) : undefined;
}
return this.users.find((user) => user.id === id);
}
async listUsers(pagination: PaginationInput): Promise<PaginatedResponse<UserRecord>> {
if (this.hasDatabase()) {
const page = Math.max(pagination.page, 1);
const pageSize = Math.min(Math.max(pagination.pageSize, 1), 100);
const repository = this.dataSource.getRepository(User);
const [records, total] = await repository.findAndCount({
order: { id: "DESC" },
skip: (page - 1) * pageSize,
take: pageSize,
});
return {
list: records.map((user) => this.toRecord(user)),
pagination: { page, pageSize, total },
};
}
return this.paginate([...this.users].reverse(), pagination);
}
async getUserOrThrow(id: number) {
const user = await this.findById(id);
if (!user) {
throw new NotFoundException("User not found");
}
return user;
}
async updateStatus(id: number, status: "active" | "disabled") {
if (this.hasDatabase()) {
const repository = this.dataSource.getRepository(User);
const user = await repository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException("User not found");
}
user.status = status;
return this.toRecord(await repository.save(user));
}
const user = await this.getUserOrThrow(id);
user.status = status;
user.updatedAt = new Date().toISOString();
return user;
}
private hasDatabase(): this is this & { dataSource: DataSource } {
return Boolean(this.dataSource?.isInitialized);
}
private toRecord(user: User): UserRecord {
return {
id: Number(user.id),
tiktokOpenId: user.tiktokOpenId,
tiktokUnionId: user.tiktokUnionId,
nickname: user.nickname,
avatarUrl: user.avatarUrl,
status: user.status as "active" | "disabled",
createdAt: user.createdAt?.toISOString?.() ?? new Date().toISOString(),
updatedAt: user.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,
},
};
}
}

10
src/users/users.types.ts Normal file
View File

@@ -0,0 +1,10 @@
export interface UserRecord {
id: number;
tiktokOpenId: string;
tiktokUnionId?: string;
nickname?: string;
avatarUrl?: string;
status: 'active' | 'disabled';
createdAt: string;
updatedAt: string;
}