更新服务器地址和项目名称,移除不必要的客户端日志相关代码,添加阻止交易模型,调整端口号及相关文档内容
This commit is contained in:
@@ -1,99 +0,0 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, IsEnum, IsDateString, IsNumber, Min } from 'class-validator';
|
||||
import { LogLevel } from '../models/client-log.model';
|
||||
import { BaseResponseDto } from '../../base.dto';
|
||||
|
||||
// 创建客户端日志请求DTO
|
||||
export class CreateClientLogDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
logContent: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LogLevel)
|
||||
logLevel?: LogLevel;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
clientVersion?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
deviceModel?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
iosVersion?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
clientTimestamp?: string;
|
||||
}
|
||||
|
||||
// 批量创建客户端日志请求DTO
|
||||
export class CreateBatchClientLogDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
logs: Omit<CreateClientLogDto, 'userId'>[];
|
||||
}
|
||||
|
||||
// 查询客户端日志请求DTO
|
||||
export class GetClientLogsDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
pageSize?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LogLevel)
|
||||
logLevel?: LogLevel;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
// 客户端日志响应DTO
|
||||
export class ClientLogResponseDto {
|
||||
id: number;
|
||||
userId: string;
|
||||
logContent: string;
|
||||
logLevel: LogLevel;
|
||||
clientVersion?: string;
|
||||
deviceModel?: string;
|
||||
iosVersion?: string;
|
||||
clientTimestamp?: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// 创建客户端日志响应DTO
|
||||
export interface CreateClientLogResponseDto extends BaseResponseDto<ClientLogResponseDto> {}
|
||||
|
||||
// 批量创建客户端日志响应DTO
|
||||
export interface CreateBatchClientLogResponseDto extends BaseResponseDto<ClientLogResponseDto[]> {}
|
||||
|
||||
// 获取客户端日志列表响应DTO
|
||||
export interface GetClientLogsResponseDto extends BaseResponseDto<{
|
||||
total: number;
|
||||
list: ClientLogResponseDto[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}> {}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class EncryptedCreateUserDto {
|
||||
@IsString({ message: '加密数据必须是字符串' })
|
||||
@IsNotEmpty({ message: '加密数据不能为空' })
|
||||
@ApiProperty({
|
||||
description: '加密的用户数据',
|
||||
example: 'eyJpdiI6IjEyMzQ1Njc4OTAiLCJ0YWciOiJhYmNkZWZnaCIsImRhdGEiOiIuLi4ifQ=='
|
||||
})
|
||||
encryptedData: string;
|
||||
}
|
||||
|
||||
export class EncryptedResponseDto {
|
||||
@ApiProperty({ description: '是否成功', example: true })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '响应消息', example: '操作成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '加密的响应数据', required: false })
|
||||
encryptedData?: string;
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { IsNumber, IsBoolean } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ResponseCode } from 'src/base.dto';
|
||||
|
||||
// 收藏话题请求DTO
|
||||
export class FavoriteTopicDto {
|
||||
@ApiProperty({ description: '话题ID' })
|
||||
@IsNumber()
|
||||
topicId: number;
|
||||
}
|
||||
|
||||
// 取消收藏话题请求DTO
|
||||
export class UnfavoriteTopicDto {
|
||||
@ApiProperty({ description: '话题ID' })
|
||||
@IsNumber()
|
||||
topicId: number;
|
||||
}
|
||||
|
||||
// 收藏操作响应DTO
|
||||
export class FavoriteResponseDto {
|
||||
@ApiProperty({ description: '响应代码' })
|
||||
code: ResponseCode;
|
||||
|
||||
@ApiProperty({ description: '响应消息' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '操作结果' })
|
||||
data: {
|
||||
success: boolean;
|
||||
isFavorited: boolean;
|
||||
topicId: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 扩展的话题响应DTO(包含收藏状态)
|
||||
export class TopicWithFavoriteDto {
|
||||
@ApiProperty({ description: '话题ID' })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '话题标题' })
|
||||
topic: string;
|
||||
|
||||
@ApiProperty({ description: '开场白' })
|
||||
opening: string | object;
|
||||
|
||||
@ApiProperty({ description: '脚本类型' })
|
||||
scriptType: string;
|
||||
|
||||
@ApiProperty({ description: '脚本话题' })
|
||||
scriptTopic: string;
|
||||
|
||||
@ApiProperty({ description: '关键词' })
|
||||
keywords: string;
|
||||
|
||||
@ApiProperty({ description: '是否已收藏' })
|
||||
@IsBoolean()
|
||||
isFavorited: boolean;
|
||||
|
||||
@ApiProperty({ description: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ description: '更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// 话题列表响应DTO(包含收藏状态)
|
||||
export class TopicListWithFavoriteResponseDto {
|
||||
@ApiProperty({ description: '响应代码' })
|
||||
code: ResponseCode;
|
||||
|
||||
@ApiProperty({ description: '响应消息' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '话题列表数据' })
|
||||
data: {
|
||||
list: TopicWithFavoriteDto[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { IsNumber, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { ResponseCode } from 'src/base.dto';
|
||||
import { TopicLibrary } from '../models/topic-library.model';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { TopicCategory } from '../models/topic-category.model';
|
||||
export class TopicLibraryResponseDto {
|
||||
code: ResponseCode;
|
||||
message: string;
|
||||
data: TopicLibrary | TopicLibrary[] | null;
|
||||
}
|
||||
|
||||
export class GetTopicLibraryRequestDto {
|
||||
// 分页相关
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@ApiProperty({ description: '页码', example: 1 })
|
||||
page?: number;
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@ApiProperty({ description: '每页条数', example: 10 })
|
||||
pageSize?: number;
|
||||
// 话题筛选
|
||||
@IsString()
|
||||
@ApiProperty({ description: '话题', example: '话题' })
|
||||
topic: string;
|
||||
|
||||
// 用户ID,用于查询用户自己的话题
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty({ description: '用户ID', example: '123' })
|
||||
userId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ description: '加密参数', example: '加密参数' })
|
||||
encryptedParameters?: string;
|
||||
}
|
||||
|
||||
export class GetTopicLibraryResponseDto {
|
||||
code: ResponseCode;
|
||||
message: string;
|
||||
data: TopicLibrary | TopicLibrary[] | null;
|
||||
}
|
||||
|
||||
export class GenerateTopicRequestDto {
|
||||
@ApiProperty({
|
||||
description: '话题',
|
||||
example: '话题',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
topic: string;
|
||||
}
|
||||
|
||||
export class GenerateTopicResponseDto {
|
||||
code: ResponseCode;
|
||||
message: string;
|
||||
data: TopicLibrary | TopicLibrary[] | null;
|
||||
}
|
||||
|
||||
export class GetTopicCategoryResponseDto {
|
||||
code: ResponseCode;
|
||||
message: string;
|
||||
data: TopicCategory | TopicCategory[] | null;
|
||||
}
|
||||
|
||||
export class DeleteTopicRequestDto {
|
||||
@ApiProperty({ description: '话题ID', example: 1 })
|
||||
@IsNumber()
|
||||
topicId: number;
|
||||
}
|
||||
|
||||
export class DeleteTopicResponseDto {
|
||||
@ApiProperty({ description: '响应码', example: 200 })
|
||||
code: ResponseCode;
|
||||
|
||||
@ApiProperty({ description: '响应消息', example: '删除成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true }
|
||||
}
|
||||
})
|
||||
data: { success: boolean };
|
||||
}
|
||||
|
||||
export class DislikeTopicRequestDto {
|
||||
@ApiProperty({ description: '话题ID', example: 1 })
|
||||
@IsNumber()
|
||||
topicId: number;
|
||||
}
|
||||
|
||||
export class DislikeTopicResponseDto {
|
||||
@ApiProperty({ description: '响应码', example: 200 })
|
||||
code: ResponseCode;
|
||||
|
||||
@ApiProperty({ description: '响应消息', example: '不喜欢成功' })
|
||||
message: string;
|
||||
|
||||
@ApiProperty({ description: '响应数据', example: true })
|
||||
data: boolean;
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { IsString, IsBoolean, IsOptional, IsNumber } from 'class-validator';
|
||||
import { BaseResponseDto, ResponseCode } from 'src/base.dto';
|
||||
import { UserRelationInfo } from '../models/user-relation-info.model';
|
||||
|
||||
export class UserRelationInfoDto {
|
||||
@IsString()
|
||||
userId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
myOccupation?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
myInterests?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
myCity?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
myCharacteristics?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
theirName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
theirOccupation?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
theirBirthday?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
theirInterests?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
theirCity?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
currentStage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isLongDistance?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
additionalDescription?: string;
|
||||
}
|
||||
|
||||
export class UserRelationInfoResponseDto implements BaseResponseDto<UserRelationInfo> {
|
||||
code: ResponseCode;
|
||||
message: string;
|
||||
data: UserRelationInfo;
|
||||
}
|
||||
97
src/users/models/blocked-transaction.model.ts
Normal file
97
src/users/models/blocked-transaction.model.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Column, Model, Table, DataType } from 'sequelize-typescript';
|
||||
|
||||
export enum BlockReason {
|
||||
FRAUD_DETECTED = 'FRAUD_DETECTED',
|
||||
DUPLICATE_USAGE = 'DUPLICATE_USAGE',
|
||||
SUSPICIOUS_ACTIVITY = 'SUSPICIOUS_ACTIVITY',
|
||||
MANUAL_BLOCK = 'MANUAL_BLOCK',
|
||||
SYSTEM_SECURITY = 'SYSTEM_SECURITY'
|
||||
}
|
||||
|
||||
@Table({
|
||||
tableName: 't_blocked_transactions',
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['transaction_id']
|
||||
},
|
||||
{
|
||||
fields: ['created_at']
|
||||
},
|
||||
{
|
||||
fields: ['block_reason']
|
||||
}
|
||||
]
|
||||
})
|
||||
export class BlockedTransaction extends Model {
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
defaultValue: DataType.UUIDV4,
|
||||
primaryKey: true,
|
||||
})
|
||||
declare id: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '被阻止的交易ID'
|
||||
})
|
||||
transactionId: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: '产品ID'
|
||||
})
|
||||
productId: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
comment: '阻止原因'
|
||||
})
|
||||
blockReason: BlockReason;
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
allowNull: true,
|
||||
comment: '详细说明'
|
||||
})
|
||||
description: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: '操作员ID(如果是手动阻止)'
|
||||
})
|
||||
operatorId: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
allowNull: true,
|
||||
comment: '阻止到期时间(null表示永久阻止)'
|
||||
})
|
||||
expiresAt: Date | null;
|
||||
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '是否激活'
|
||||
})
|
||||
isActive: boolean;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
defaultValue: DataType.NOW,
|
||||
})
|
||||
declare createdAt: Date;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
defaultValue: DataType.NOW,
|
||||
})
|
||||
declare updatedAt: Date;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Column, Model, Table, DataType, ForeignKey, BelongsTo } from 'sequelize-typescript';
|
||||
import { User } from './user.model';
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 'debug',
|
||||
INFO = 'info',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
@Table({
|
||||
tableName: 't_client_logs',
|
||||
underscored: true,
|
||||
})
|
||||
export class ClientLog extends Model {
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
})
|
||||
declare id: number;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
comment: '用户ID',
|
||||
})
|
||||
declare userId: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
allowNull: false,
|
||||
comment: '日志内容',
|
||||
})
|
||||
declare logContent: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: LogLevel.INFO,
|
||||
comment: '日志级别',
|
||||
})
|
||||
declare logLevel: LogLevel;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: '客户端版本',
|
||||
})
|
||||
declare clientVersion: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: '设备型号',
|
||||
})
|
||||
declare deviceModel: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: 'iOS版本',
|
||||
})
|
||||
declare iosVersion: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
allowNull: true,
|
||||
comment: '客户端时间戳',
|
||||
})
|
||||
declare clientTimestamp: Date;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
defaultValue: DataType.NOW,
|
||||
comment: '服务器接收时间',
|
||||
})
|
||||
declare createdAt: Date;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
defaultValue: DataType.NOW,
|
||||
})
|
||||
declare updatedAt: Date;
|
||||
|
||||
@BelongsTo(() => User)
|
||||
user: User;
|
||||
}
|
||||
@@ -110,15 +110,6 @@ export class User extends Model {
|
||||
})
|
||||
declare deviceName: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: '用户过去生成的话题 id 列表',
|
||||
})
|
||||
declare lastTopicIds: string;
|
||||
|
||||
declare isNew?: boolean;
|
||||
|
||||
get isVip(): boolean {
|
||||
return this.membershipExpiration ? dayjs(this.membershipExpiration).isAfter(dayjs()) : false;
|
||||
}
|
||||
|
||||
@@ -18,15 +18,8 @@ import { Logger as WinstonLogger } from 'winston';
|
||||
import { UsersService } from './users.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UserResponseDto } from './dto/user-response.dto';
|
||||
import { UserRelationInfoDto, UserRelationInfoResponseDto } from './dto/user-relation-info.dto';
|
||||
import { ApiOperation, ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { UpdateUserDto, UpdateUserResponseDto } from './dto/update-user.dto';
|
||||
import {
|
||||
CreateClientLogDto,
|
||||
CreateBatchClientLogDto,
|
||||
CreateClientLogResponseDto,
|
||||
CreateBatchClientLogResponseDto,
|
||||
} from './dto/client-log.dto';
|
||||
import { AppleLoginDto, AppleLoginResponseDto, RefreshTokenDto, RefreshTokenResponseDto } from './dto/apple-login.dto';
|
||||
import { DeleteAccountDto, DeleteAccountResponseDto } from './dto/delete-account.dto';
|
||||
import { GuestLoginDto, GuestLoginResponseDto, RefreshGuestTokenDto, RefreshGuestTokenResponseDto } from './dto/guest-login.dto';
|
||||
@@ -49,7 +42,7 @@ export class UsersController {
|
||||
) { }
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
@Get('/info')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '创建用户' })
|
||||
@ApiBody({ type: CreateUserDto })
|
||||
@@ -67,40 +60,6 @@ export class UsersController {
|
||||
return this.usersService.updateUser(updateUserDto);
|
||||
}
|
||||
|
||||
|
||||
// 获取用户关系
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put('relations')
|
||||
async updateOrCreateRelationInfo(@Body() relationInfoDto: UserRelationInfoDto) {
|
||||
return this.usersService.updateOrCreateRelationInfo(relationInfoDto);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('relations/:userId')
|
||||
async getRelationInfo(@Param('userId') userId: string): Promise<UserRelationInfoResponseDto> {
|
||||
return this.usersService.getRelationInfo(userId);
|
||||
}
|
||||
|
||||
// 创建客户端日志
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('logs')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '创建客户端日志' })
|
||||
@ApiBody({ type: CreateClientLogDto })
|
||||
async createClientLog(@Body() createClientLogDto: CreateClientLogDto): Promise<CreateClientLogResponseDto> {
|
||||
return this.usersService.createClientLog(createClientLogDto);
|
||||
}
|
||||
|
||||
// 批量创建客户端日志
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('logs/batch')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '批量创建客户端日志' })
|
||||
@ApiBody({ type: CreateBatchClientLogDto })
|
||||
async createBatchClientLog(@Body() createBatchClientLogDto: CreateBatchClientLogDto): Promise<CreateBatchClientLogResponseDto> {
|
||||
return this.usersService.createBatchClientLog(createBatchClientLogDto);
|
||||
}
|
||||
|
||||
// Apple 登录
|
||||
@Public()
|
||||
@Post('auth/apple/login')
|
||||
|
||||
@@ -7,9 +7,20 @@ import { ApplePurchaseService } from "./services/apple-purchase.service";
|
||||
import { EncryptionService } from "../common/encryption.service";
|
||||
import { AppleAuthService } from "./services/apple-auth.service";
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { BlockedTransaction } from "./models/blocked-transaction.model";
|
||||
import { UserPurchase } from "./models/user-purchase.model";
|
||||
import { PurchaseRestoreLog } from "./models/purchase-restore-log.model";
|
||||
import { RevenueCatEvent } from "./models/revenue-cat-event.model";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SequelizeModule.forFeature([User]),
|
||||
SequelizeModule.forFeature([
|
||||
User,
|
||||
BlockedTransaction,
|
||||
UserPurchase,
|
||||
PurchaseRestoreLog,
|
||||
RevenueCatEvent,
|
||||
]),
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_ACCESS_SECRET || 'your-access-token-secret-key',
|
||||
signOptions: { expiresIn: '30d' },
|
||||
|
||||
@@ -12,32 +12,18 @@ import { InjectModel, InjectConnection } from '@nestjs/sequelize';
|
||||
import { Gender, User } from './models/user.model';
|
||||
import { UserResponseDto } from './dto/user-response.dto';
|
||||
import { ResponseCode } from 'src/base.dto';
|
||||
import { UserRelationInfo } from './models/user-relation-info.model';
|
||||
import { UserRelationInfoDto, UserRelationInfoResponseDto } from './dto/user-relation-info.dto';
|
||||
import { TopicLibrary } from './models/topic-library.model';
|
||||
import { Transaction, Op } from 'sequelize';
|
||||
import { Sequelize } from 'sequelize-typescript';
|
||||
import { UpdateUserDto, UpdateUserResponseDto } from './dto/update-user.dto';
|
||||
|
||||
import { TopicCategory } from './models/topic-category.model';
|
||||
import { UserPurchase, PurchaseType, PurchaseStatus, PurchasePlatform } from './models/user-purchase.model';
|
||||
import { ApplePurchaseService } from './services/apple-purchase.service';
|
||||
import * as dayjs from 'dayjs';
|
||||
import { UpdateMembershipDto, UpdateMembershipResponseDto } from './dto/membership.dto';
|
||||
import { ClientLog } from './models/client-log.model';
|
||||
import {
|
||||
CreateClientLogDto,
|
||||
CreateBatchClientLogDto,
|
||||
CreateClientLogResponseDto,
|
||||
CreateBatchClientLogResponseDto,
|
||||
} from './dto/client-log.dto';
|
||||
import { EncryptionService } from 'src/common/encryption.service';
|
||||
import { AccessTokenPayload, AppleAuthService, AppleTokenPayload } from './services/apple-auth.service';
|
||||
import { AppleLoginDto, AppleLoginResponseDto, RefreshTokenDto, RefreshTokenResponseDto } from './dto/apple-login.dto';
|
||||
import { DeleteAccountDto, DeleteAccountResponseDto } from './dto/delete-account.dto';
|
||||
import { GuestLoginDto, GuestLoginResponseDto, RefreshGuestTokenDto, RefreshGuestTokenResponseDto } from './dto/guest-login.dto';
|
||||
import { AppStoreServerNotificationDto, ProcessNotificationResponseDto, NotificationType } from './dto/app-store-notification.dto';
|
||||
import { TopicService } from './topic.service';
|
||||
import { RevenueCatEvent } from './models/revenue-cat-event.model';
|
||||
import { RevenueCatWebhookDto, RevenueCatEventType } from './dto/revenue-cat-webhook.dto';
|
||||
import { RestorePurchaseDto, RestorePurchaseResponseDto, RestoredPurchaseInfo, ActiveEntitlement, NonSubscriptionTransaction } from './dto/restore-purchase.dto';
|
||||
@@ -53,26 +39,16 @@ export class UsersService {
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly winstonLogger: WinstonLogger,
|
||||
@InjectModel(User)
|
||||
private userModel: typeof User,
|
||||
@InjectModel(UserRelationInfo)
|
||||
private userRelationInfoModel: typeof UserRelationInfo,
|
||||
@InjectModel(TopicLibrary)
|
||||
private topicLibraryModel: typeof TopicLibrary,
|
||||
@InjectModel(TopicCategory)
|
||||
private topicCategoryModel: typeof TopicCategory,
|
||||
@InjectModel(UserPurchase)
|
||||
private userPurchaseModel: typeof UserPurchase,
|
||||
@InjectModel(ClientLog)
|
||||
private clientLogModel: typeof ClientLog,
|
||||
@InjectModel(RevenueCatEvent)
|
||||
private revenueCatEventModel: typeof RevenueCatEvent,
|
||||
@InjectModel(PurchaseRestoreLog)
|
||||
private purchaseRestoreLogModel: typeof PurchaseRestoreLog,
|
||||
@InjectModel(BlockedTransaction)
|
||||
private blockedTransactionModel: typeof BlockedTransaction,
|
||||
private encryptionService: EncryptionService,
|
||||
private appleAuthService: AppleAuthService,
|
||||
private applePurchaseService: ApplePurchaseService,
|
||||
private topicService: TopicService,
|
||||
@InjectModel(BlockedTransaction)
|
||||
private blockedTransactionModel: typeof BlockedTransaction,
|
||||
@InjectConnection()
|
||||
private sequelize: Sequelize,
|
||||
) { }
|
||||
@@ -101,13 +77,9 @@ export class UsersService {
|
||||
};
|
||||
}
|
||||
|
||||
// 查询已收藏的话题数量
|
||||
const favoriteTopicCount = await this.topicService.getFavoriteTopicCount(existingUser.id);
|
||||
|
||||
const returnData = {
|
||||
...existingUser.toJSON(),
|
||||
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
|
||||
favoriteTopicCount,
|
||||
isVip: existingUser.isVip,
|
||||
}
|
||||
|
||||
@@ -176,192 +148,6 @@ export class UsersService {
|
||||
};
|
||||
}
|
||||
|
||||
async updateOrCreateRelationInfo(relationInfoDto: UserRelationInfoDto): Promise<UserRelationInfoResponseDto> {
|
||||
try {
|
||||
this.logger.log(`updateOrCreateRelationInfo: ${JSON.stringify(relationInfoDto, null, 2)}`);
|
||||
// 检查userId是否存在于用户表中
|
||||
const user = await this.userModel.findByPk(relationInfoDto.userId);
|
||||
if (!user) {
|
||||
return {
|
||||
code: ResponseCode.ERROR,
|
||||
message: `ID为${relationInfoDto.userId}的用户不存在`,
|
||||
data: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
// 查找是否已存在关系信息
|
||||
const existingInfo = await this.userRelationInfoModel.findOne({
|
||||
where: { userId: relationInfoDto.userId },
|
||||
});
|
||||
|
||||
if (existingInfo) {
|
||||
// 存在则更新
|
||||
await existingInfo.update(relationInfoDto, {
|
||||
where: { userId: relationInfoDto.userId },
|
||||
});
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: 'success',
|
||||
data: existingInfo,
|
||||
};
|
||||
} else {
|
||||
// 不存在则创建
|
||||
const newRelationInfo = await this.userRelationInfoModel.create({
|
||||
...relationInfoDto,
|
||||
});
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: 'success',
|
||||
data: newRelationInfo,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`更新或创建关系信息失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
code: ResponseCode.ERROR,
|
||||
message: `更新或创建关系信息失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
data: null as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getRelationInfo(userId: string): Promise<UserRelationInfoResponseDto> {
|
||||
try {
|
||||
const relationInfos = await this.userRelationInfoModel.findAll({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!relationInfos.length) {
|
||||
const newRelationInfo = await this.userRelationInfoModel.create({
|
||||
userId,
|
||||
});
|
||||
return {
|
||||
code: ResponseCode.ERROR,
|
||||
message: '关系信息不存在',
|
||||
data: newRelationInfo,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: 'success',
|
||||
data: relationInfos[0],
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`获取关系信息失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
return {
|
||||
code: ResponseCode.ERROR,
|
||||
message: `获取关系信息失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
data: null as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 内部方法:计算订阅过期时间, 根据会员当前的有效期累加新的有效期
|
||||
private calculateExpirationDate(purchaseType: PurchaseType, currentExpiration: Date): Date {
|
||||
this.logger.log(`calculateExpirationDate purchaseType: ${purchaseType}, currentExpiration: ${currentExpiration}`);
|
||||
switch (purchaseType) {
|
||||
case PurchaseType.WEEKLY:
|
||||
return dayjs(currentExpiration ?? new Date()).add(7, 'day').toDate();
|
||||
case PurchaseType.QUARTERLY:
|
||||
return dayjs(currentExpiration ?? new Date()).add(90, 'day').toDate();
|
||||
case PurchaseType.LIFETIME:
|
||||
return dayjs(currentExpiration ?? new Date()).add(365 * 100, 'day').toDate();
|
||||
default:
|
||||
throw new BadRequestException('无效的购买类型');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建客户端日志
|
||||
async createClientLog(createClientLogDto: CreateClientLogDto): Promise<CreateClientLogResponseDto> {
|
||||
try {
|
||||
this.logger.log(`createClientLog: ${JSON.stringify(createClientLogDto, null, 2)}`);
|
||||
|
||||
const { userId, logContent, logLevel, clientVersion, deviceModel, iosVersion, clientTimestamp } = createClientLogDto;
|
||||
|
||||
// 检查用户是否存在
|
||||
const user = await this.userModel.findByPk(userId);
|
||||
if (!user) {
|
||||
return {
|
||||
code: ResponseCode.ERROR,
|
||||
message: `ID为${userId}的用户不存在`,
|
||||
data: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
// 创建日志记录
|
||||
const clientLog = await this.clientLogModel.create({
|
||||
userId,
|
||||
logContent,
|
||||
logLevel,
|
||||
clientVersion,
|
||||
deviceModel,
|
||||
iosVersion,
|
||||
clientTimestamp: clientTimestamp ? new Date(clientTimestamp) : null,
|
||||
});
|
||||
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: 'success',
|
||||
data: clientLog.toJSON(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`创建客户端日志失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
return {
|
||||
code: ResponseCode.ERROR,
|
||||
message: `创建客户端日志失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
data: null as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 批量创建客户端日志
|
||||
async createBatchClientLog(createBatchClientLogDto: CreateBatchClientLogDto): Promise<CreateBatchClientLogResponseDto> {
|
||||
try {
|
||||
this.logger.log(`createBatchClientLog: ${JSON.stringify(createBatchClientLogDto, null, 2)}`);
|
||||
|
||||
const { userId, logs } = createBatchClientLogDto;
|
||||
|
||||
// 检查用户是否存在
|
||||
const user = await this.userModel.findByPk(userId);
|
||||
if (!user) {
|
||||
return {
|
||||
code: ResponseCode.ERROR,
|
||||
message: `ID为${userId}的用户不存在`,
|
||||
data: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
// 批量创建日志记录
|
||||
const clientLogs = await this.clientLogModel.bulkCreate(
|
||||
logs.map(log => ({
|
||||
userId,
|
||||
logContent: log.logContent,
|
||||
logLevel: log.logLevel,
|
||||
clientVersion: log.clientVersion,
|
||||
deviceModel: log.deviceModel,
|
||||
iosVersion: log.iosVersion,
|
||||
clientTimestamp: log.clientTimestamp ? new Date(log.clientTimestamp) : null,
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
code: ResponseCode.SUCCESS,
|
||||
message: 'success',
|
||||
data: clientLogs.map(log => log.toJSON()),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`批量创建客户端日志失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
return {
|
||||
code: ResponseCode.ERROR,
|
||||
message: `批量创建客户端日志失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
data: null as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apple 登录
|
||||
@@ -406,8 +192,6 @@ export class UsersService {
|
||||
isNewUser = true;
|
||||
this.logger.log(`创建新的Apple用户: ${userId}`);
|
||||
|
||||
// 创建三条话题,不消耗次数
|
||||
await this.topicService.generateTopic('初识破冰', userId, false, true, 3);
|
||||
} else {
|
||||
// 更新现有用户的登录时间
|
||||
user.lastLogin = new Date();
|
||||
@@ -415,8 +199,6 @@ export class UsersService {
|
||||
this.logger.log(`Apple用户登录: ${userId}`);
|
||||
}
|
||||
|
||||
const favoriteTopicCount = await this.topicService.getFavoriteTopicCount(userId);
|
||||
|
||||
// 生成访问令牌和刷新令牌
|
||||
const accessToken = this.appleAuthService.generateAccessToken(userId, user.mail);
|
||||
const refreshToken = this.appleAuthService.generateRefreshToken(userId);
|
||||
@@ -427,7 +209,6 @@ export class UsersService {
|
||||
isNew: isNewUser,
|
||||
isVip: user.isVip,
|
||||
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
|
||||
favoriteTopicCount,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -508,41 +289,19 @@ export class UsersService {
|
||||
}
|
||||
|
||||
// 开始删除用户相关数据(使用事务确保数据一致性)
|
||||
// 1. 删除用户关系信息
|
||||
await this.userRelationInfoModel.destroy({
|
||||
where: { userId },
|
||||
transaction,
|
||||
});
|
||||
|
||||
// 2. 删除用户购买记录
|
||||
// 1. 删除用户购买记录
|
||||
await this.userPurchaseModel.destroy({
|
||||
where: { userId },
|
||||
transaction,
|
||||
});
|
||||
|
||||
// 3. 删除用户客户端日志
|
||||
await this.clientLogModel.destroy({
|
||||
where: { userId },
|
||||
transaction,
|
||||
});
|
||||
|
||||
// 4. 删除用户的个人话题
|
||||
// 删除收藏
|
||||
await this.topicService.deleteFavoriteTopics(userId, transaction);
|
||||
|
||||
await this.topicLibraryModel.destroy({
|
||||
where: { userId },
|
||||
transaction,
|
||||
});
|
||||
|
||||
// 5. 最后删除用户本身
|
||||
// 最后删除用户本身
|
||||
await this.userModel.destroy({
|
||||
where: { id: userId },
|
||||
transaction,
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 提交事务
|
||||
await transaction.commit();
|
||||
|
||||
@@ -605,8 +364,6 @@ export class UsersService {
|
||||
isNewUser = true;
|
||||
this.logger.log(`创建新的游客用户: ${guestUserId}`);
|
||||
|
||||
// 创建三条话题,不消耗次数
|
||||
await this.topicService.generateTopic('初识破冰', guestUserId, false, true, 3);
|
||||
} else {
|
||||
// 更新现有游客用户的登录时间和设备信息
|
||||
user.lastLogin = new Date();
|
||||
@@ -627,7 +384,6 @@ export class UsersService {
|
||||
isVip: user.membershipExpiration ? dayjs(user.membershipExpiration).isAfter(dayjs()) : false,
|
||||
isGuest: true,
|
||||
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
|
||||
favoriteTopicCount: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user