import { Injectable, NotFoundException, Optional } from "@nestjs/common"; import { InjectDataSource } from "@nestjs/typeorm"; import { DataSource } from "typeorm"; import { PaginatedResponse } from "@app/common/paginated-response.dto"; import { User } from "./user.entity"; import { UserRecord } from "./users.types"; 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> { 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(records: T[], input: PaginationInput): PaginatedResponse { 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, }, }; } }