feat(users): 添加身体围度测量功能
新增用户身体围度的完整功能模块,包括数据库迁移、模型定义、API接口和历史记录追踪。 支持胸围、腰围、上臀围、臂围、大腿围、小腿围六项身体围度指标的管理。 - 添加数据库迁移脚本,扩展用户档案表字段 - 创建围度历史记录表用于数据追踪 - 实现围度数据的更新和历史查询API - 添加数据验证和错误处理机制
This commit is contained in:
63
sql-scripts/body-measurements-migration.sql
Normal file
63
sql-scripts/body-measurements-migration.sql
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
-- 身体围度功能数据库迁移脚本
|
||||||
|
-- 执行日期: 2024年
|
||||||
|
-- 功能: 为用户档案表新增围度字段,创建围度历史记录表
|
||||||
|
|
||||||
|
-- 禁用外键检查(执行时)
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- 1. 为用户档案表新增围度字段
|
||||||
|
ALTER TABLE `t_user_profile`
|
||||||
|
ADD COLUMN `chest_circumference` FLOAT NULL COMMENT '胸围(厘米)' AFTER `daily_water_goal`,
|
||||||
|
ADD COLUMN `waist_circumference` FLOAT NULL COMMENT '腰围(厘米)' AFTER `chest_circumference`,
|
||||||
|
ADD COLUMN `upper_hip_circumference` FLOAT NULL COMMENT '上臀围(厘米)' AFTER `waist_circumference`,
|
||||||
|
ADD COLUMN `arm_circumference` FLOAT NULL COMMENT '臂围(厘米)' AFTER `upper_hip_circumference`,
|
||||||
|
ADD COLUMN `thigh_circumference` FLOAT NULL COMMENT '大腿围(厘米)' AFTER `arm_circumference`,
|
||||||
|
ADD COLUMN `calf_circumference` FLOAT NULL COMMENT '小腿围(厘米)' AFTER `thigh_circumference`;
|
||||||
|
|
||||||
|
-- 2. 创建用户身体围度历史记录表
|
||||||
|
CREATE TABLE `t_user_body_measurement_history` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`user_id` VARCHAR(255) NOT NULL COMMENT '用户ID',
|
||||||
|
`measurement_type` ENUM(
|
||||||
|
'chest_circumference',
|
||||||
|
'waist_circumference',
|
||||||
|
'upper_hip_circumference',
|
||||||
|
'arm_circumference',
|
||||||
|
'thigh_circumference',
|
||||||
|
'calf_circumference'
|
||||||
|
) NOT NULL COMMENT '围度类型',
|
||||||
|
`value` FLOAT NOT NULL COMMENT '围度值(厘米)',
|
||||||
|
`source` ENUM('manual', 'other') NOT NULL DEFAULT 'manual' COMMENT '更新来源',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_measurement_type` (`measurement_type`),
|
||||||
|
KEY `idx_created_at` (`created_at`),
|
||||||
|
KEY `idx_user_measurement_time` (`user_id`, `measurement_type`, `created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户身体围度历史记录表';
|
||||||
|
|
||||||
|
-- 重新启用外键检查
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- 验证表结构
|
||||||
|
SHOW CREATE TABLE `t_user_profile`;
|
||||||
|
SHOW CREATE TABLE `t_user_body_measurement_history`;
|
||||||
|
|
||||||
|
-- 验证新增字段
|
||||||
|
SELECT
|
||||||
|
COLUMN_NAME,
|
||||||
|
DATA_TYPE,
|
||||||
|
IS_NULLABLE,
|
||||||
|
COLUMN_COMMENT
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 't_user_profile'
|
||||||
|
AND COLUMN_NAME IN (
|
||||||
|
'chest_circumference',
|
||||||
|
'waist_circumference',
|
||||||
|
'upper_hip_circumference',
|
||||||
|
'arm_circumference',
|
||||||
|
'thigh_circumference',
|
||||||
|
'calf_circumference'
|
||||||
|
);
|
||||||
77
src/users/dto/body-measurement.dto.ts
Normal file
77
src/users/dto/body-measurement.dto.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNumber, IsOptional, IsEnum, Min } from 'class-validator';
|
||||||
|
import { ResponseCode } from 'src/base.dto';
|
||||||
|
import { BodyMeasurementType } from '../models/user-body-measurement-history.model';
|
||||||
|
|
||||||
|
export class UpdateBodyMeasurementDto {
|
||||||
|
@IsNumber({}, { message: '胸围必须是数字' })
|
||||||
|
@Min(0, { message: '胸围不能为负数' })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: '胸围(厘米)', example: 90.5, required: false })
|
||||||
|
chestCircumference?: number;
|
||||||
|
|
||||||
|
@IsNumber({}, { message: '腰围必须是数字' })
|
||||||
|
@Min(0, { message: '腰围不能为负数' })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: '腰围(厘米)', example: 70.5, required: false })
|
||||||
|
waistCircumference?: number;
|
||||||
|
|
||||||
|
@IsNumber({}, { message: '上臀围必须是数字' })
|
||||||
|
@Min(0, { message: '上臀围不能为负数' })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: '上臀围(厘米)', example: 95.0, required: false })
|
||||||
|
upperHipCircumference?: number;
|
||||||
|
|
||||||
|
@IsNumber({}, { message: '臂围必须是数字' })
|
||||||
|
@Min(0, { message: '臂围不能为负数' })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: '臂围(厘米)', example: 28.5, required: false })
|
||||||
|
armCircumference?: number;
|
||||||
|
|
||||||
|
@IsNumber({}, { message: '大腿围必须是数字' })
|
||||||
|
@Min(0, { message: '大腿围不能为负数' })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: '大腿围(厘米)', example: 55.0, required: false })
|
||||||
|
thighCircumference?: number;
|
||||||
|
|
||||||
|
@IsNumber({}, { message: '小腿围必须是数字' })
|
||||||
|
@Min(0, { message: '小腿围不能为负数' })
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({ description: '小腿围(厘米)', example: 35.0, required: false })
|
||||||
|
calfCircumference?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateBodyMeasurementResponseDto {
|
||||||
|
@ApiProperty({ description: '状态码', example: ResponseCode.SUCCESS })
|
||||||
|
code: ResponseCode;
|
||||||
|
@ApiProperty({ description: '消息', example: 'success' })
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetBodyMeasurementHistoryResponseDto {
|
||||||
|
@ApiProperty({ description: '状态码', example: ResponseCode.SUCCESS })
|
||||||
|
code: ResponseCode;
|
||||||
|
@ApiProperty({ description: '消息', example: 'success' })
|
||||||
|
message: string;
|
||||||
|
@ApiProperty({
|
||||||
|
description: '围度历史记录',
|
||||||
|
example: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
userId: 'user123',
|
||||||
|
measurementType: 'chest_circumference',
|
||||||
|
value: 90.5,
|
||||||
|
source: 'manual',
|
||||||
|
createdAt: '2024-01-01T00:00:00.000Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
data: Array<{
|
||||||
|
id: number;
|
||||||
|
userId: string;
|
||||||
|
measurementType: BodyMeasurementType;
|
||||||
|
value: number;
|
||||||
|
source: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
69
src/users/models/user-body-measurement-history.model.ts
Normal file
69
src/users/models/user-body-measurement-history.model.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Column, DataType, Index, Model, PrimaryKey, Table } from 'sequelize-typescript';
|
||||||
|
|
||||||
|
export enum BodyMeasurementType {
|
||||||
|
ChestCircumference = 'chest_circumference',
|
||||||
|
WaistCircumference = 'waist_circumference',
|
||||||
|
UpperHipCircumference = 'upper_hip_circumference',
|
||||||
|
ArmCircumference = 'arm_circumference',
|
||||||
|
ThighCircumference = 'thigh_circumference',
|
||||||
|
CalfCircumference = 'calf_circumference',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MeasurementUpdateSource {
|
||||||
|
Manual = 'manual',
|
||||||
|
Other = 'other',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: 't_user_body_measurement_history',
|
||||||
|
underscored: true,
|
||||||
|
})
|
||||||
|
export class UserBodyMeasurementHistory extends Model {
|
||||||
|
@PrimaryKey
|
||||||
|
@Column({
|
||||||
|
type: DataType.BIGINT,
|
||||||
|
autoIncrement: true,
|
||||||
|
})
|
||||||
|
declare id: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
comment: '用户ID',
|
||||||
|
})
|
||||||
|
declare userId: string;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.ENUM('chest_circumference', 'waist_circumference', 'upper_hip_circumference', 'arm_circumference', 'thigh_circumference', 'calf_circumference'),
|
||||||
|
allowNull: false,
|
||||||
|
comment: '围度类型',
|
||||||
|
})
|
||||||
|
declare measurementType: BodyMeasurementType;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.FLOAT,
|
||||||
|
allowNull: false,
|
||||||
|
comment: '围度值(厘米)',
|
||||||
|
})
|
||||||
|
declare value: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.ENUM('manual', 'other'),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'manual',
|
||||||
|
comment: '更新来源',
|
||||||
|
})
|
||||||
|
declare source: MeasurementUpdateSource;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.DATE,
|
||||||
|
defaultValue: DataType.NOW,
|
||||||
|
})
|
||||||
|
declare createdAt: Date;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.DATE,
|
||||||
|
defaultValue: DataType.NOW,
|
||||||
|
})
|
||||||
|
declare updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -93,6 +93,48 @@ export class UserProfile extends Model {
|
|||||||
})
|
})
|
||||||
declare dailyWaterGoal: number;
|
declare dailyWaterGoal: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.FLOAT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '胸围(厘米)',
|
||||||
|
})
|
||||||
|
declare chestCircumference: number | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.FLOAT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '腰围(厘米)',
|
||||||
|
})
|
||||||
|
declare waistCircumference: number | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.FLOAT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '上臀围(厘米)',
|
||||||
|
})
|
||||||
|
declare upperHipCircumference: number | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.FLOAT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '臂围(厘米)',
|
||||||
|
})
|
||||||
|
declare armCircumference: number | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.FLOAT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '大腿围(厘米)',
|
||||||
|
})
|
||||||
|
declare thighCircumference: number | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: DataType.FLOAT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '小腿围(厘米)',
|
||||||
|
})
|
||||||
|
declare calfCircumference: number | null;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: DataType.DATE,
|
type: DataType.DATE,
|
||||||
defaultValue: DataType.NOW,
|
defaultValue: DataType.NOW,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { AppStoreServerNotificationDto, ProcessNotificationResponseDto } from '.
|
|||||||
import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-purchase.dto';
|
import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-purchase.dto';
|
||||||
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
|
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
|
||||||
import { UpdateWeightRecordDto, WeightRecordResponseDto, DeleteWeightRecordResponseDto } from './dto/weight-record.dto';
|
import { UpdateWeightRecordDto, WeightRecordResponseDto, DeleteWeightRecordResponseDto } from './dto/weight-record.dto';
|
||||||
|
import { UpdateBodyMeasurementDto, UpdateBodyMeasurementResponseDto, GetBodyMeasurementHistoryResponseDto } from './dto/body-measurement.dto';
|
||||||
|
|
||||||
import { Public } from '../common/decorators/public.decorator';
|
import { Public } from '../common/decorators/public.decorator';
|
||||||
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
@@ -341,4 +342,40 @@ export class UsersController {
|
|||||||
return this.usersService.getUserActivityHistory(user.sub);
|
return this.usersService.getUserActivityHistory(user.sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 围度相关接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户围度信息
|
||||||
|
*/
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Put('body-measurements')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '更新用户围度信息' })
|
||||||
|
@ApiBody({ type: UpdateBodyMeasurementDto })
|
||||||
|
@ApiResponse({ status: 200, description: '成功更新围度信息', type: UpdateBodyMeasurementResponseDto })
|
||||||
|
async updateBodyMeasurements(
|
||||||
|
@Body() updateBodyMeasurementDto: UpdateBodyMeasurementDto,
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
): Promise<UpdateBodyMeasurementResponseDto> {
|
||||||
|
this.logger.log(`更新用户围度信息 - 用户ID: ${user.sub}, 数据: ${JSON.stringify(updateBodyMeasurementDto)}`);
|
||||||
|
return this.usersService.updateBodyMeasurements(user.sub, updateBodyMeasurementDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户围度历史记录
|
||||||
|
*/
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('body-measurements/history')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '获取用户围度历史记录' })
|
||||||
|
@ApiQuery({ name: 'measurementType', required: false, description: '围度类型筛选' })
|
||||||
|
@ApiResponse({ status: 200, description: '成功获取围度历史记录', type: GetBodyMeasurementHistoryResponseDto })
|
||||||
|
async getBodyMeasurementHistory(
|
||||||
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
|
@Query('measurementType') measurementType?: string,
|
||||||
|
): Promise<GetBodyMeasurementHistoryResponseDto> {
|
||||||
|
this.logger.log(`获取用户围度历史记录 - 用户ID: ${user.sub}, 围度类型: ${measurementType || '全部'}`);
|
||||||
|
return this.usersService.getBodyMeasurementHistory(user.sub, measurementType as any);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { UsersService } from "./users.service";
|
|||||||
import { User } from "./models/user.model";
|
import { User } from "./models/user.model";
|
||||||
import { UserProfile } from "./models/user-profile.model";
|
import { UserProfile } from "./models/user-profile.model";
|
||||||
import { UserWeightHistory } from "./models/user-weight-history.model";
|
import { UserWeightHistory } from "./models/user-weight-history.model";
|
||||||
|
import { UserBodyMeasurementHistory } from "./models/user-body-measurement-history.model";
|
||||||
|
|
||||||
import { UserDietHistory } from "./models/user-diet-history.model";
|
import { UserDietHistory } from "./models/user-diet-history.model";
|
||||||
import { ApplePurchaseService } from "./services/apple-purchase.service";
|
import { ApplePurchaseService } from "./services/apple-purchase.service";
|
||||||
@@ -31,6 +32,7 @@ import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
|
|||||||
RevenueCatEvent,
|
RevenueCatEvent,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
UserWeightHistory,
|
UserWeightHistory,
|
||||||
|
UserBodyMeasurementHistory,
|
||||||
|
|
||||||
UserDietHistory,
|
UserDietHistory,
|
||||||
UserActivity,
|
UserActivity,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { RestorePurchaseDto, RestorePurchaseResponseDto, RestoredPurchaseInfo, A
|
|||||||
import { PurchaseRestoreLog, RestoreStatus, RestoreSource } from './models/purchase-restore-log.model';
|
import { PurchaseRestoreLog, RestoreStatus, RestoreSource } from './models/purchase-restore-log.model';
|
||||||
import { BlockedTransaction, BlockReason } from './models/blocked-transaction.model';
|
import { BlockedTransaction, BlockReason } from './models/blocked-transaction.model';
|
||||||
import { UserWeightHistory, WeightUpdateSource } from './models/user-weight-history.model';
|
import { UserWeightHistory, WeightUpdateSource } from './models/user-weight-history.model';
|
||||||
|
import { UserBodyMeasurementHistory, BodyMeasurementType, MeasurementUpdateSource } from './models/user-body-measurement-history.model';
|
||||||
|
|
||||||
import { ActivityLogsService } from '../activity-logs/activity-logs.service';
|
import { ActivityLogsService } from '../activity-logs/activity-logs.service';
|
||||||
import { UserActivityService } from './services/user-activity.service';
|
import { UserActivityService } from './services/user-activity.service';
|
||||||
@@ -63,6 +64,8 @@ export class UsersService {
|
|||||||
private userProfileModel: typeof UserProfile,
|
private userProfileModel: typeof UserProfile,
|
||||||
@InjectModel(UserWeightHistory)
|
@InjectModel(UserWeightHistory)
|
||||||
private userWeightHistoryModel: typeof UserWeightHistory,
|
private userWeightHistoryModel: typeof UserWeightHistory,
|
||||||
|
@InjectModel(UserBodyMeasurementHistory)
|
||||||
|
private userBodyMeasurementHistoryModel: typeof UserBodyMeasurementHistory,
|
||||||
|
|
||||||
@InjectConnection()
|
@InjectConnection()
|
||||||
private sequelize: Sequelize,
|
private sequelize: Sequelize,
|
||||||
@@ -114,6 +117,12 @@ export class UsersService {
|
|||||||
height: profile?.height,
|
height: profile?.height,
|
||||||
activityLevel: profile?.activityLevel,
|
activityLevel: profile?.activityLevel,
|
||||||
dailyWaterGoal: profile?.dailyWaterGoal,
|
dailyWaterGoal: profile?.dailyWaterGoal,
|
||||||
|
chestCircumference: profile?.chestCircumference,
|
||||||
|
waistCircumference: profile?.waistCircumference,
|
||||||
|
upperHipCircumference: profile?.upperHipCircumference,
|
||||||
|
armCircumference: profile?.armCircumference,
|
||||||
|
thighCircumference: profile?.thighCircumference,
|
||||||
|
calfCircumference: profile?.calfCircumference,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`getProfile returnData: ${JSON.stringify(returnData, null, 2)}`);
|
this.logger.log(`getProfile returnData: ${JSON.stringify(returnData, null, 2)}`);
|
||||||
@@ -2278,4 +2287,103 @@ export class UsersService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户围度信息
|
||||||
|
*/
|
||||||
|
async updateBodyMeasurements(userId: string, measurements: any): Promise<{ code: any; message: string }> {
|
||||||
|
const transaction = await this.sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取或创建用户档案
|
||||||
|
const [profile] = await this.userProfileModel.findOrCreate({
|
||||||
|
where: { userId },
|
||||||
|
defaults: { userId },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateFields: any = {};
|
||||||
|
const historyRecords: any[] = [];
|
||||||
|
|
||||||
|
// 映射字段名到围度类型
|
||||||
|
const fieldMappings = {
|
||||||
|
chestCircumference: BodyMeasurementType.ChestCircumference,
|
||||||
|
waistCircumference: BodyMeasurementType.WaistCircumference,
|
||||||
|
upperHipCircumference: BodyMeasurementType.UpperHipCircumference,
|
||||||
|
armCircumference: BodyMeasurementType.ArmCircumference,
|
||||||
|
thighCircumference: BodyMeasurementType.ThighCircumference,
|
||||||
|
calfCircumference: BodyMeasurementType.CalfCircumference,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理每个传入的围度字段
|
||||||
|
for (const [fieldName, measurementType] of Object.entries(fieldMappings)) {
|
||||||
|
if (measurements[fieldName] !== undefined) {
|
||||||
|
const value = measurements[fieldName];
|
||||||
|
updateFields[fieldName] = value;
|
||||||
|
|
||||||
|
// 准备历史记录
|
||||||
|
historyRecords.push({
|
||||||
|
userId,
|
||||||
|
measurementType,
|
||||||
|
value,
|
||||||
|
source: MeasurementUpdateSource.Manual,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户档案
|
||||||
|
if (Object.keys(updateFields).length > 0) {
|
||||||
|
await profile.update(updateFields, { transaction });
|
||||||
|
|
||||||
|
// 批量创建历史记录
|
||||||
|
if (historyRecords.length > 0) {
|
||||||
|
await this.userBodyMeasurementHistoryModel.bulkCreate(historyRecords, { transaction });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
this.logger.log(`用户 ${userId} 围度更新成功: ${JSON.stringify(updateFields)}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: ResponseCode.SUCCESS,
|
||||||
|
message: 'success',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
this.logger.error(`更新用户围度失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||||
|
throw new BadRequestException(`更新围度信息失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户围度历史记录
|
||||||
|
*/
|
||||||
|
async getBodyMeasurementHistory(userId: string, measurementType?: BodyMeasurementType): Promise<any> {
|
||||||
|
try {
|
||||||
|
const whereCondition: any = { userId };
|
||||||
|
if (measurementType) {
|
||||||
|
whereCondition.measurementType = measurementType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = await this.userBodyMeasurementHistoryModel.findAll({
|
||||||
|
where: whereCondition,
|
||||||
|
order: [['createdAt', 'DESC']],
|
||||||
|
limit: 100, // 限制返回最近100条记录
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: ResponseCode.SUCCESS,
|
||||||
|
message: 'success',
|
||||||
|
data: history,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`获取围度历史记录失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||||
|
return {
|
||||||
|
code: ResponseCode.ERROR,
|
||||||
|
message: `获取围度历史记录失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user