refactor: 统一代码缩进与引号风格,新增CORS配置

1.  修正所有文件的缩进为4空格,将单引号统一改为双引号
2.  新增CORS_ORIGIN环境变量配置并从env读取CORS允许源
3.  新增prettier配置文件统一代码格式
4.  为main.ts添加启动日志输出
This commit is contained in:
2026-07-02 16:03:42 +08:00
parent 0eeeea0c4d
commit f2b1043cbc
10 changed files with 388 additions and 435 deletions

3
.env
View File

@@ -12,4 +12,7 @@ DB_USERNAME=root
DB_PASSWORD=zhxiao1124.. DB_PASSWORD=zhxiao1124..
DB_DATABASE=onion_tiktok DB_DATABASE=onion_tiktok
# CORS settings
CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
DB_SYNCHRONIZE = true DB_SYNCHRONIZE = true

View File

@@ -12,4 +12,7 @@ DB_USERNAME=root
DB_PASSWORD= DB_PASSWORD=
DB_DATABASE=cth_tk_backend DB_DATABASE=cth_tk_backend
# CORS settings
CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
DB_SYNCHRONIZE = true DB_SYNCHRONIZE = true

9
.prettierrc Normal file
View File

@@ -0,0 +1,9 @@
{
"tabWidth": 4,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 390,
"eslintIntegration": true,
"bracketSpacing": true,
"stylelintIntegration": true
}

View File

@@ -1,31 +1,31 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from "@nestjs/config";
import { AdminModule } from './modules/admin/admin.module'; import { AdminModule } from "./modules/admin/admin.module";
import { AuthModule } from './modules/auth/auth.module'; import { AuthModule } from "./modules/auth/auth.module";
import { DataModule } from './modules/data/data.module'; import { DataModule } from "./modules/data/data.module";
import { DatabaseModule } from './modules/database/database.module'; import { DatabaseModule } from "./modules/database/database.module";
import { DramasModule } from './modules/dramas/dramas.module'; import { DramasModule } from "./modules/dramas/dramas.module";
import { HealthModule } from './modules/health/health.module'; import { HealthModule } from "./modules/health/health.module";
import { LogsModule } from './modules/logs/logs.module'; import { LogsModule } from "./modules/logs/logs.module";
import { MediaModule } from './modules/media/media.module'; import { MediaModule } from "./modules/media/media.module";
import { OrdersModule } from './modules/orders/orders.module'; import { OrdersModule } from "./modules/orders/orders.module";
import { PlayerModule } from './modules/player/player.module'; import { PlayerModule } from "./modules/player/player.module";
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
}), }),
DatabaseModule, DatabaseModule,
AdminModule, AdminModule,
HealthModule, HealthModule,
LogsModule, LogsModule,
DramasModule, DramasModule,
DataModule, DataModule,
MediaModule, MediaModule,
AuthModule, AuthModule,
OrdersModule, OrdersModule,
PlayerModule, PlayerModule,
], ],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -8,35 +8,27 @@ import { RequestIdMiddleware } from "./common/middleware/request-id.middleware";
import { LogsService } from "./modules/logs/logs.service"; import { LogsService } from "./modules/logs/logs.service";
export function bootstrapApp(app: INestApplication) { export function bootstrapApp(app: INestApplication) {
app.enableCors({ app.enableCors({
origin: process.env.CORS_ORIGIN?.split(',') ?? [ origin: process.env.CORS_ORIGIN?.split(","),
'http://localhost:5173', credentials: true,
'http://127.0.0.1:5173', });
], app.use(RequestIdMiddleware);
credentials: true, app.useGlobalPipes(
}); new ValidationPipe({
app.use(RequestIdMiddleware); whitelist: true,
app.useGlobalPipes( transform: true,
new ValidationPipe({ forbidNonWhitelisted: true,
whitelist: true, }),
transform: true, );
forbidNonWhitelisted: true,
}),
);
const logsService = app.get(LogsService); const logsService = app.get(LogsService);
app.useGlobalFilters(new HttpExceptionFilter(logsService));
app.useGlobalInterceptors(
new RequestLoggingInterceptor(logsService, app.get(Reflector)),
new ResponseInterceptor(),
);
const config = new DocumentBuilder() app.useGlobalFilters(new HttpExceptionFilter(logsService));
.setTitle("TikTok Short Drama Backend") app.useGlobalInterceptors(new RequestLoggingInterceptor(logsService, app.get(Reflector)), new ResponseInterceptor());
.setDescription("RESTful API for TikTok short drama backend services.")
.setVersion("1.0") const config = new DocumentBuilder().setTitle("TikTok Short Drama Backend").setDescription("RESTful API for TikTok short drama backend services.").setVersion("1.0").addBearerAuth().build();
.addBearerAuth()
.build(); const document = SwaggerModule.createDocument(app, config);
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("/api/docs", app, document); SwaggerModule.setup("/api/docs", app, document);
} }

View File

@@ -1,12 +1,13 @@
import 'dotenv/config'; import "dotenv/config";
import { NestFactory } from '@nestjs/core'; import { NestFactory } from "@nestjs/core";
import { AppModule } from './app.module'; import { AppModule } from "./app.module";
import { bootstrapApp } from './bootstrap'; import { bootstrapApp } from "./bootstrap";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
bootstrapApp(app); bootstrapApp(app);
await app.listen(process.env.PORT ?? 3000); await app.listen(process.env.PORT ?? 3000);
console.log(`Application is running on: ${await app.getUrl()}`);
} }
void bootstrap(); void bootstrap();

View File

@@ -1,172 +1,134 @@
import { import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
Body, import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
Controller, import { RequirePermissions } from "../admin/decorators/require-permissions.decorator";
Get, import { AdminJwtAuthGuard } from "../admin/guards/admin-jwt-auth.guard";
Param, import { PermissionsGuard } from "../admin/guards/permissions.guard";
Patch, import { ConfigureEpisodesDto } from "./dto/configure-episodes.dto";
Post, import { CreateDramaDto } from "./dto/create-drama.dto";
Query, import { CreateEpisodeDto } from "./dto/create-episode.dto";
UseGuards, import { UpdateDramaDto } from "./dto/update-drama.dto";
} from '@nestjs/common'; import { UpdateEpisodeDto } from "./dto/update-episode.dto";
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { DramasService } from "./dramas.service";
import { RequirePermissions } from '../admin/decorators/require-permissions.decorator'; import { PublishStatus } from "./drama-status.enum";
import { AdminJwtAuthGuard } from '../admin/guards/admin-jwt-auth.guard';
import { PermissionsGuard } from '../admin/guards/permissions.guard';
import { ConfigureEpisodesDto } from './dto/configure-episodes.dto';
import { CreateDramaDto } from './dto/create-drama.dto';
import { CreateEpisodeDto } from './dto/create-episode.dto';
import { UpdateDramaDto } from './dto/update-drama.dto';
import { UpdateEpisodeDto } from './dto/update-episode.dto';
import { DramasService } from './dramas.service';
import { PublishStatus } from './drama-status.enum';
@ApiTags('Dramas') @ApiTags("Dramas")
@Controller() @Controller()
export class DramasController { export class DramasController {
constructor(private readonly dramasService: DramasService) {} constructor(private readonly dramasService: DramasService) {}
@Post('/api/admin/v1/dramas') @Post("/api/admin/v1/dramas")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('drama:create') @RequirePermissions("drama:create")
createDrama(@Body() dto: CreateDramaDto) { createDrama(@Body() dto: CreateDramaDto) {
return this.dramasService.createDrama(dto); return this.dramasService.createDrama(dto);
} }
@Get('/api/admin/v1/dramas') @Get("/api/admin/v1/dramas")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('drama:read') @RequirePermissions("drama:read")
listAdminDramas(@Query('page') page?: string, @Query('pageSize') pageSize?: string) { listAdminDramas(@Query("page") page?: string, @Query("pageSize") pageSize?: string) {
return this.dramasService.listAdminDramas({ return this.dramasService.listAdminDramas({
page: Number(page) || 1, page: Number(page) || 1,
pageSize: Number(pageSize) || 20, pageSize: Number(pageSize) || 20,
}); });
} }
@Get('/api/admin/v1/dramas/:id') @Get("/api/admin/v1/dramas/:id")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('drama:read') @RequirePermissions("drama:read")
getDrama(@Param('id') id: string) { getDrama(@Param("id") id: string) {
return this.dramasService.getDramaOrThrow(Number(id)); return this.dramasService.getDramaOrThrow(Number(id));
} }
@Patch('/api/admin/v1/dramas/:id') @Patch("/api/admin/v1/dramas/:id")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('drama:update') @RequirePermissions("drama:update")
updateDrama(@Param('id') id: string, @Body() dto: UpdateDramaDto) { updateDrama(@Param("id") id: string, @Body() dto: UpdateDramaDto) {
return this.dramasService.updateDrama(Number(id), dto); return this.dramasService.updateDrama(Number(id), dto);
} }
@Post('/api/admin/v1/dramas/:id/episodes/batch') @Post("/api/admin/v1/dramas/:id/episodes/batch")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('episode:create') @RequirePermissions("episode:create")
configureEpisodes( configureEpisodes(@Param("id") id: string, @Body() dto: ConfigureEpisodesDto) {
@Param('id') id: string, return this.dramasService.configureEpisodes(Number(id), dto);
@Body() dto: ConfigureEpisodesDto, }
) {
return this.dramasService.configureEpisodes(Number(id), dto);
}
@Get('/api/admin/v1/dramas/:id/episodes') @Get("/api/admin/v1/dramas/:id/episodes")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('episode:update') @RequirePermissions("episode:update")
listAdminEpisodes( listAdminEpisodes(@Param("id") id: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string) {
@Param('id') id: string, return this.dramasService.listAdminEpisodes(Number(id), {
@Query('page') page?: string, page: Number(page) || 1,
@Query('pageSize') pageSize?: string, pageSize: Number(pageSize) || 20,
) { });
return this.dramasService.listAdminEpisodes(Number(id), { }
page: Number(page) || 1,
pageSize: Number(pageSize) || 20,
});
}
@Post('/api/admin/v1/episodes') @Post("/api/admin/v1/episodes")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('episode:create') @RequirePermissions("episode:create")
createEpisode(@Body() dto: CreateEpisodeDto) { createEpisode(@Body() dto: CreateEpisodeDto) {
return this.dramasService.createEpisode(dto); return this.dramasService.createEpisode(dto);
} }
@Patch('/api/admin/v1/episodes/:id') @Patch("/api/admin/v1/episodes/:id")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('episode:update') @RequirePermissions("episode:update")
updateEpisode(@Param('id') id: string, @Body() dto: UpdateEpisodeDto) { updateEpisode(@Param("id") id: string, @Body() dto: UpdateEpisodeDto) {
return this.dramasService.updateEpisode(Number(id), dto); return this.dramasService.updateEpisode(Number(id), dto);
} }
@Patch('/api/admin/v1/dramas/:dramaId/episodes/:episodeId') @Patch("/api/admin/v1/dramas/:dramaId/episodes/:episodeId")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('episode:update') @RequirePermissions("episode:update")
updateDramaEpisode( updateDramaEpisode(@Param("dramaId") dramaId: string, @Param("episodeId") episodeId: string, @Body() dto: UpdateEpisodeDto) {
@Param('dramaId') dramaId: string, return this.dramasService.updateDramaEpisode(Number(dramaId), Number(episodeId), dto);
@Param('episodeId') episodeId: string, }
@Body() dto: UpdateEpisodeDto,
) {
return this.dramasService.updateDramaEpisode(
Number(dramaId),
Number(episodeId),
dto,
);
}
@Post('/api/admin/v1/dramas/:id/submit-review') @Post("/api/admin/v1/dramas/:id/submit-review")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('drama:update') @RequirePermissions("drama:update")
submitReview(@Param('id') id: string) { submitReview(@Param("id") id: string) {
return this.dramasService.submitReview(Number(id)); return this.dramasService.submitReview(Number(id));
} }
@Patch('/api/admin/v1/episodes/:id/publish-status') @Patch("/api/admin/v1/episodes/:id/publish-status")
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
@RequirePermissions('episode:update') @RequirePermissions("episode:update")
updatePublishStatus( updatePublishStatus(@Param("id") id: string, @Body() dto: { publishStatus: PublishStatus }) {
@Param('id') id: string, return this.dramasService.updateEpisodePublishStatus(Number(id), dto.publishStatus);
@Body() dto: { publishStatus: PublishStatus }, }
) {
return this.dramasService.updateEpisodePublishStatus(
Number(id),
dto.publishStatus,
);
}
@Get('/api/app/v1/dramas') @Get("/api/app/v1/dramas")
listPublishedDramas( listPublishedDramas(@Query("page") page?: string, @Query("pageSize") pageSize?: string, @Query("title") title?: string) {
@Query('page') page?: string, return this.dramasService.listPublishedDramas({
@Query('pageSize') pageSize?: string, page: Number(page) || 1,
@Query('title') title?: string, pageSize: Number(pageSize) || 10,
) { title,
return this.dramasService.listPublishedDramas({ });
page: Number(page) || 1, }
pageSize: Number(pageSize) || 10,
title,
});
}
@Get('/api/app/v1/dramas/:id/episodes') @Get("/api/app/v1/dramas/:id/episodes")
listPublishedEpisodes( listPublishedEpisodes(@Param("id") id: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string) {
@Param('id') id: string, return this.dramasService.listPublishedEpisodes(Number(id), {
@Query('page') page?: string, page: Number(page) || 1,
@Query('pageSize') pageSize?: string, pageSize: Number(pageSize) || 20,
) { });
return this.dramasService.listPublishedEpisodes(Number(id), { }
page: Number(page) || 1,
pageSize: Number(pageSize) || 20,
});
}
@Get('/api/app/v1/dramas/:id') @Get("/api/app/v1/dramas/:id")
getPublishedDrama(@Param('id') id: string) { getPublishedDrama(@Param("id") id: string) {
return this.dramasService.getPublishedDramaOrThrow(Number(id)); return this.dramasService.getPublishedDramaOrThrow(Number(id));
} }
} }

View File

@@ -1,13 +1,4 @@
import { import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
Body,
Controller,
Get,
Param,
Patch,
Post,
Query,
UseGuards,
} from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { RequirePermissions } from "../admin/decorators/require-permissions.decorator"; import { RequirePermissions } from "../admin/decorators/require-permissions.decorator";
import { AdminJwtAuthGuard } from "../admin/guards/admin-jwt-auth.guard"; import { AdminJwtAuthGuard } from "../admin/guards/admin-jwt-auth.guard";
@@ -22,59 +13,56 @@ import { MediaService } from "./media.service";
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminJwtAuthGuard, PermissionsGuard) @UseGuards(AdminJwtAuthGuard, PermissionsGuard)
export class MediaController { export class MediaController {
constructor(private readonly mediaService: MediaService) {} constructor(private readonly mediaService: MediaService) {}
@Post("/covers") @Post("/covers")
@RequirePermissions("media:create") @RequirePermissions("media:create")
createCover(@Body() dto: CreateCoverDto) { createCover(@Body() dto: CreateCoverDto) {
return this.mediaService.createCover(dto); return this.mediaService.createCover(dto);
} }
@Post("/upload-jobs") @Post("/upload-jobs")
@RequirePermissions("media:create") @RequirePermissions("media:create")
createUploadJob(@Body() dto: CreateUploadJobDto) { createUploadJob(@Body() dto: CreateUploadJobDto) {
return this.mediaService.createUploadJob(dto); return this.mediaService.createUploadJob(dto);
} }
@Post("/upload-tasks") @Post("/upload-tasks")
@RequirePermissions("media:create") @RequirePermissions("media:create")
createDirectUploadTask(@Body() dto: CreateDirectUploadTaskDto) { createDirectUploadTask(@Body() dto: CreateDirectUploadTaskDto) {
return this.mediaService.createDirectUploadTask(dto); return this.mediaService.createDirectUploadTask(dto);
} }
@Get("/upload-tasks/:jobId") @Get("/upload-tasks/:jobId")
@RequirePermissions("media:read") @RequirePermissions("media:read")
getUploadTask(@Param("jobId") jobId: string) { getUploadTask(@Param("jobId") jobId: string) {
return this.mediaService.getUploadTask(jobId); return this.mediaService.getUploadTask(jobId);
} }
@Patch("/upload-tasks/:jobId/complete") @Patch("/upload-tasks/:jobId/complete")
@RequirePermissions("media:create") @RequirePermissions("media:create")
completeDirectUploadTask(@Param("jobId") jobId: string) { completeDirectUploadTask(@Param("jobId") jobId: string) {
return this.mediaService.completeDirectUploadTask(jobId); return this.mediaService.completeDirectUploadTask(jobId);
} }
@Post("/upload-tasks/:jobId/review/retry") @Post("/upload-tasks/:jobId/review/retry")
@RequirePermissions("media:create") @RequirePermissions("media:create")
retryReview(@Param("jobId") jobId: string) { retryReview(@Param("jobId") jobId: string) {
return this.mediaService.retryReview(jobId); return this.mediaService.retryReview(jobId);
} }
@Post("/review-status/sync") @Post("/review-status/sync")
@RequirePermissions("media:create") @RequirePermissions("media:create")
syncReviewStatuses() { syncReviewStatuses() {
return this.mediaService.syncReviewStatuses(); return this.mediaService.syncReviewStatuses();
} }
@Get("/upload-jobs") @Get("/upload-jobs")
@RequirePermissions("media:read") @RequirePermissions("media:read")
listUploadJobs( listUploadJobs(@Query("page") page?: string, @Query("pageSize") pageSize?: string) {
@Query("page") page?: string, return this.mediaService.listUploadJobs({
@Query("pageSize") pageSize?: string, page: Number(page) || 1,
) { pageSize: Number(pageSize) || 20,
return this.mediaService.listUploadJobs({ });
page: Number(page) || 1, }
pageSize: Number(pageSize) || 20,
});
}
} }

View File

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

View File

@@ -1,19 +1,19 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"target": "ES2023", "target": "ES2023",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"baseUrl": "./", "baseUrl": "./",
"incremental": true, "incremental": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"noImplicitAny": false "noImplicitAny": false
} }
} }