refactor: 重构项目目录与代码结构,清理冗余文件
本次提交进行了大规模的项目结构调整: 1. 调整文件目录归属,将模块代码按功能域重新组织 2. 删除多处冗余的DTO、实体类和模块文件 3. 统一了常量、装饰器、工具类的存放位置 4. 修复了`.gitignore`的日志目录匹配规则 5. 新增了健康检查、认证、用户、剧集、订单等核心业务模块
This commit is contained in:
37
src/users/user.entity.ts
Normal file
37
src/users/user.entity.ts
Normal 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;
|
||||
}
|
||||
36
src/users/users.controller.ts
Normal file
36
src/users/users.controller.ts
Normal 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
8
src/users/users.dto.ts
Normal 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
12
src/users/users.module.ts
Normal 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
172
src/users/users.service.ts
Normal 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
10
src/users/users.types.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user