You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用MongoDB存储用户体重历史记录的最佳实现方案是什么?

体重历史记录需求的NestJS + Mongoose最佳实现方案

架构选型

根据你的业务场景,有两种成熟的实现方案,可根据实际使用情况选择:

  • 方案1:用户文档内嵌体重历史数组(推荐90%以上普通场景使用)
    优势:单文档查询无需联表,写入性能高,普通用户一生的体重记录也不会超过万级,完全不会触及MongoDB单文档16MB的上限
    劣势:仅不适用于分钟级高频上报体重的特殊场景(如医疗监测设备自动上报)
  • 方案2:独立集合存储体重历史记录
    优势:文档体积可控,适合超高频率的体重上报场景
    劣势:需要事务保证数据一致性,查询用户数据需要跨表操作

方案1(内嵌数组)实现步骤

1. 定义Mongoose Schema

将原有的单体重字段拆分为最新体重冗余字段+历史记录数组,冗余字段用于直接展示最新体重,无需遍历历史数组:

// user.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

// 体重记录子文档,不需要单独_id
@Schema({ _id: false })
class WeightRecord {
  @Prop({ required: true, type: Number })
  value: number; // 体重数值

  @Prop({ required: true, type: Date, default: Date.now })
  recordedAt: Date; // 记录时间

  @Prop({ type: String })
  note?: string; // 可选备注,如“空腹”“餐后”
}

@Schema({ timestamps: true })
export class User extends Document {
  // 原有用户字段(如昵称、手机号等自行保留)
  @Prop({ required: true, unique: true })
  username: string;

  // 最新体重冗余字段,提升查询效率
  @Prop({ type: Number })
  currentWeight?: number;

  // 历史体重数组,默认按插入顺序倒序存储,最新记录在最前
  @Prop({ type: [WeightRecord], default: [] })
  weightHistory: WeightRecord[];
}

export const UserSchema = SchemaFactory.createForClass(User);

// 可选:加复合索引避免同一用户同一时间重复提交记录
UserSchema.index({ _id: 1, 'weightHistory.recordedAt': 1 }, { unique: true });

2. 编写体重更新与查询逻辑

用MongoDB原子操作符完成单请求更新,避免并发问题,无需先查后改:

// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from './user.schema';
import { UpdateWeightDto } from './dto/update-weight.dto';

@Injectable()
export class UserService {
  constructor(@InjectModel(User.name) private userModel: Model<User>) {}

  // 更新体重,同时写入历史记录
  async updateWeight(userId: string, updateWeightDto: UpdateWeightDto) {
    const { value, recordedAt, note } = updateWeightDto;
    return this.userModel.findByIdAndUpdate(
      userId,
      {
        // 更新最新体重
        $set: { currentWeight: value },
        // 插入历史记录到数组头部
        $push: {
          weightHistory: {
            $each: [{ value, recordedAt: recordedAt || new Date(), note }],
            $position: 0
          }
        }
      },
      { new: true } // 返回更新后的文档
    );
  }

  // 分页查询体重历史
  async getWeightHistory(userId: string, page = 1, limit = 30) {
    const skip = (page - 1) * limit;
    const user = await this.userModel.findById(userId, {
      weightHistory: { $slice: [skip, limit] },
      currentWeight: 1
    });
    return user?.weightHistory || [];
  }
}

对应DTO定义参考:

// dto/update-weight.dto.ts
import { IsNumber, IsOptional, IsDateString, IsString } from 'class-validator';

export class UpdateWeightDto {
  @IsNumber()
  value: number;

  @IsOptional()
  @IsDateString()
  recordedAt?: string;

  @IsOptional()
  @IsString()
  note?: string;
}

方案2(独立集合)实现步骤(仅适合高频上报场景)

如果是医疗设备等分钟级上报体重的场景,单用户年记录量超过5万条,可采用该方案:

1. 定义独立集合Schema

// weight-history.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';

@Schema({ timestamps: true })
export class WeightHistory extends Document {
  @Prop({ required: true, type: Types.ObjectId, ref: 'User' })
  userId: Types.ObjectId;

  @Prop({ required: true, type: Number })
  value: number;

  @Prop({ required: true, type: Date, default: Date.now })
  recordedAt: Date;

  @Prop({ type: String })
  note?: string;
}

export const WeightHistorySchema = SchemaFactory.createForClass(WeightHistory);
// 加复合索引大幅提升用户历史查询速度
WeightHistorySchema.index({ userId: 1, recordedAt: -1 });

2. 写入逻辑(用事务保证数据一致性)

// weight-history.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, ClientSession } from 'mongoose';
import { WeightHistory } from './weight-history.schema';
import { User } from '../user/user.schema';
import { UpdateWeightDto } from './dto/update-weight.dto';

@Injectable()
export class WeightHistoryService {
  constructor(
    @InjectModel(WeightHistory.name) private weightHistoryModel: Model<WeightHistory>,
    @InjectModel(User.name) private userModel: Model<User>
  ) {}

  async addWeightRecord(userId: string, dto: UpdateWeightDto) {
    const session: ClientSession = await this.weightHistoryModel.startSession();
    session.startTransaction();
    try {
      // 新增历史记录
      const [record] = await this.weightHistoryModel.create(
        [{ userId, ...dto, recordedAt: dto.recordedAt || new Date() }],
        { session }
      );
      // 更新用户最新体重
      await this.userModel.findByIdAndUpdate(userId, { currentWeight: dto.value }, { session });
      await session.commitTransaction();
      return record;
    } catch (err) {
      await session.abortTransaction();
      throw err;
    } finally {
      session.endSession();
    }
  }
}

内容的提问来源于stack exchange,提问作者BrunoFred

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 13:36:02