Files
plates-server/src/users/users.controller.ts

345 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Controller,
Get,
Post,
Body,
Param,
HttpCode,
HttpStatus,
Put,
Delete,
Query,
Logger,
UseGuards,
Inject,
Req,
NotFoundException,
UseInterceptors,
UploadedFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Request } from 'express';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger as WinstonLogger } from 'winston';
import { UsersService } from './users.service';
import { UploadImageResponseDto } from './dto/upload-image.dto';
import { CreateUserDto } from './dto/create-user.dto';
import { UserResponseDto } from './dto/user-response.dto';
import { ApiOperation, ApiBody, ApiResponse, ApiTags, ApiQuery } from '@nestjs/swagger';
import { UpdateUserDto, UpdateUserResponseDto } from './dto/update-user.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';
import { AppStoreServerNotificationDto, ProcessNotificationResponseDto } from './dto/app-store-notification.dto';
import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-purchase.dto';
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
import { UpdateWeightRecordDto, WeightRecordResponseDto, DeleteWeightRecordResponseDto } from './dto/weight-record.dto';
import { Public } from '../common/decorators/public.decorator';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import { AccessTokenPayload } from './services/apple-auth.service';
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
import { ResponseCode } from 'src/base.dto';
import { CosService } from './cos.service';
@ApiTags('users')
@Controller('users')
export class UsersController {
private readonly logger = new Logger(UsersController.name);
constructor(
private readonly usersService: UsersService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly winstonLogger: WinstonLogger,
private readonly cosService: CosService,
) { }
@UseGuards(JwtAuthGuard)
@Get('/info')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '创建用户' })
@ApiBody({ type: CreateUserDto })
@ApiResponse({ type: UserResponseDto })
async getProfile(@CurrentUser() user: AccessTokenPayload): Promise<UserResponseDto> {
this.logger.log(`get profile: ${JSON.stringify(user)}`);
return this.usersService.getProfile(user);
}
// 获取历史体重记录
@UseGuards(JwtAuthGuard)
@Get('/weight-history')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取历史体重记录(按时间倒序)' })
@ApiQuery({ name: 'limit', required: false, description: '返回条数默认200最大1000' })
async getWeightHistory(
@CurrentUser() user: AccessTokenPayload,
) {
const data = await this.usersService.getWeightHistory(user.sub, {});
return { code: ResponseCode.SUCCESS, message: 'success', data };
}
// 更新体重记录
@UseGuards(JwtAuthGuard)
@Put('/weight-records/:id')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '更新体重记录' })
@ApiBody({ type: UpdateWeightRecordDto })
@ApiResponse({ status: 200, description: '成功更新体重记录', type: WeightRecordResponseDto })
async updateWeightRecord(
@Param('id') recordId: string,
@Body() updateDto: UpdateWeightRecordDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<WeightRecordResponseDto> {
this.logger.log(`更新体重记录 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
return this.usersService.updateWeightRecord(user.sub, parseInt(recordId), updateDto);
}
// 删除体重记录
@UseGuards(JwtAuthGuard)
@Delete('/weight-records/:id')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '删除体重记录' })
@ApiResponse({ status: 200, description: '成功删除体重记录', type: DeleteWeightRecordResponseDto })
async deleteWeightRecord(
@Param('id') recordId: string,
@CurrentUser() user: AccessTokenPayload,
): Promise<DeleteWeightRecordResponseDto> {
this.logger.log(`删除体重记录 - 用户ID: ${user.sub}, 记录ID: ${recordId}`);
const success = await this.usersService.deleteWeightRecord(user.sub, parseInt(recordId));
if (!success) {
throw new NotFoundException('体重记录不存在');
}
return { code: ResponseCode.SUCCESS, message: 'success' };
}
// 更新用户昵称、头像
@UseGuards(JwtAuthGuard)
@Put('update')
async updateUser(@Body() updateUserDto: UpdateUserDto, @CurrentUser() user: AccessTokenPayload): Promise<UpdateUserResponseDto> {
return this.usersService.updateUser(updateUserDto, user.sub);
}
// Apple 登录
@Public()
@Post('auth/apple/login')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Apple 登录验证' })
@ApiBody({ type: AppleLoginDto })
@ApiResponse({ type: AppleLoginResponseDto })
async appleLogin(@Body() appleLoginDto: AppleLoginDto): Promise<AppleLoginResponseDto> {
return this.usersService.appleLogin(appleLoginDto);
}
// 刷新访问令牌
@Public()
@Post('auth/refresh')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '刷新访问令牌' })
@ApiBody({ type: RefreshTokenDto })
@ApiResponse({ type: RefreshTokenResponseDto })
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto): Promise<RefreshTokenResponseDto> {
return this.usersService.refreshToken(refreshTokenDto);
}
// 删除用户账号
@UseGuards(JwtAuthGuard)
@Post('delete-account')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '删除用户账号' })
@ApiBody({ type: DeleteAccountDto })
@ApiResponse({ type: DeleteAccountResponseDto })
async deleteAccount(@CurrentUser() user: AccessTokenPayload): Promise<DeleteAccountResponseDto> {
const deleteAccountDto = {
userId: user.sub,
};
return this.usersService.deleteAccount(deleteAccountDto);
}
// 游客登录
@Public()
@Post('auth/guest/login')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '游客登录' })
@ApiBody({ type: GuestLoginDto })
@ApiResponse({ type: GuestLoginResponseDto })
async guestLogin(@Body() guestLoginDto: GuestLoginDto): Promise<GuestLoginResponseDto> {
return this.usersService.guestLogin(guestLoginDto);
}
// 刷新游客令牌
@Public()
@Post('auth/guest/refresh')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '刷新游客访问令牌' })
@ApiBody({ type: RefreshGuestTokenDto })
@ApiResponse({ type: RefreshGuestTokenResponseDto })
async refreshGuestToken(@Body() refreshGuestTokenDto: RefreshGuestTokenDto): Promise<RefreshGuestTokenResponseDto> {
return this.usersService.refreshGuestToken(refreshGuestTokenDto);
}
// 获取COS上传临时密钥
@UseGuards(JwtAuthGuard)
@Get('cos/upload-token')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取COS上传临时密钥' })
async getCosUploadToken(@CurrentUser() user: AccessTokenPayload) {
try {
const data = await this.cosService.getUploadToken(user.sub);
return { code: ResponseCode.SUCCESS, message: 'success', data };
} catch (error) {
this.winstonLogger.error('获取COS上传临时密钥失败', {
context: 'UsersController',
userId: user?.sub,
error: (error as Error).message,
stack: (error as Error).stack,
});
return { code: ResponseCode.ERROR, message: (error as Error).message };
}
}
// 上传图片到COS
@UseGuards(JwtAuthGuard)
@Post('cos/upload-image')
@HttpCode(HttpStatus.OK)
@UseInterceptors(FileInterceptor('file'))
@ApiOperation({ summary: '上传图片到COS' })
@ApiResponse({
status: 200,
description: '图片上传成功',
type: UploadImageResponseDto,
})
@ApiResponse({
status: 400,
description: '上传失败:文件格式不支持或文件过大',
})
async uploadImageToCos(
@CurrentUser() user: AccessTokenPayload,
@UploadedFile() file: Express.Multer.File,
) {
try {
if (!file) {
return { code: ResponseCode.ERROR, message: '请选择要上传的图片文件' };
}
this.winstonLogger.info(`receive file, fileSize: ${file.size}`, {
context: 'UsersController',
userId: user?.sub,
file,
})
const data = await this.cosService.uploadImage(user.sub, file);
return data;
} catch (error) {
this.winstonLogger.error('上传图片失败', {
context: 'UsersController',
userId: user?.sub,
error: (error as Error).message,
stack: (error as Error).stack,
});
return { code: ResponseCode.ERROR, message: (error as Error).message };
}
}
// App Store 服务器通知接收接口
@Public()
@Post('app-store-notifications')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '接收App Store服务器通知' })
@ApiBody({ type: AppStoreServerNotificationDto })
@ApiResponse({ type: ProcessNotificationResponseDto })
async handleAppStoreNotification(@Body() notificationDto: AppStoreServerNotificationDto): Promise<ProcessNotificationResponseDto> {
this.logger.log(`收到App Store服务器通知`);
return this.usersService.processAppStoreNotification(notificationDto);
}
// RevenueCat Webhook
@Public()
@Post('revenuecat-webhook')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '接收 RevenueCat webhook' })
async handleRevenueCatWebhook(@Body() webhook) {
// 使用结构化日志记录webhook接收
this.winstonLogger.info('RevenueCat webhook received', {
context: 'UsersController',
eventType: webhook.event?.type,
eventId: webhook.event?.id,
appUserId: webhook.event?.app_user_id,
timestamp: new Date().toISOString(),
webhookData: {
apiVersion: webhook.api_version,
eventTimestamp: webhook.event?.event_timestamp_ms
}
});
try {
await this.usersService.handleRevenueCatWebhook(webhook);
this.winstonLogger.info('RevenueCat webhook processed successfully', {
context: 'UsersController',
eventType: webhook.event?.type,
eventId: webhook.event?.id,
appUserId: webhook.event?.app_user_id
});
return { code: ResponseCode.SUCCESS, message: 'success' };
} catch (error) {
this.winstonLogger.error('RevenueCat webhook processing failed', {
context: 'UsersController',
eventType: webhook.event?.type,
eventId: webhook.event?.id,
appUserId: webhook.event?.app_user_id,
error: error.message,
stack: error.stack
});
return { code: ResponseCode.ERROR, message: error.message };
}
}
// 恢复购买
@UseGuards(JwtAuthGuard)
@Post('restore-purchase')
async restorePurchase(
@Body() restorePurchaseDto: RestorePurchaseDto,
@CurrentUser() user: AccessTokenPayload,
@Req() request: Request
): Promise<RestorePurchaseResponseDto> {
const clientIp = request.ip || request.connection.remoteAddress || 'unknown';
const userAgent = request.get('User-Agent') || 'unknown';
this.logger.log(`恢复购买请求 - 用户ID: ${user.sub}, IP: ${clientIp}`);
// 记录安全相关信息
this.winstonLogger.info('Purchase restore request', {
context: 'UsersController',
userId: user.sub,
clientIp,
userAgent,
originalAppUserId: restorePurchaseDto.customerInfo?.originalAppUserId,
entitlementsCount: Object.keys(restorePurchaseDto.customerInfo?.activeEntitlements || {}).length,
nonSubTransactionsCount: restorePurchaseDto.customerInfo?.nonSubscriptionTransactions?.length || 0
});
return this.usersService.restorePurchase(restorePurchaseDto, user.sub, clientIp, userAgent);
}
// ==================== 用户活跃记录相关接口 ====================
/**
* 获取用户最近六个月的活跃情况
*/
@UseGuards(JwtAuthGuard)
@Get('activity-history')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取用户最近六个月的活跃情况' })
@ApiResponse({ status: 200, description: '成功获取用户活跃历史', type: GetUserActivityHistoryResponseDto })
async getUserActivityHistory(
@CurrentUser() user: AccessTokenPayload,
): Promise<GetUserActivityHistoryResponseDto> {
this.logger.log(`获取用户活跃历史 - 用户ID: ${user.sub}`);
return this.usersService.getUserActivityHistory(user.sub);
}
}