MongoDB/NestJS中俱乐部经验变更时自动更新等级字段的实现咨询
我在应用里用MongoDB的Club集合,每个文档都有experience字段,想基于它新增level和expToNextLvl两个字段,希望在experience变更时自动更新这两个字段(比如经验增加时同步更新)。另外我不太懂迁移脚本,想知道这是不是实现需求的最佳方案?
我写了一段示例脚本:
import { Injectable } from '@nestjs/common'; import { ClubRepository } from '@shared/lib/mongo/entities/club'; @Injectable() export class ClubMigrationService { constructor(private clubRepository: ClubRepository) {} async migrate(levels: number[]) { const clubs = await this.clubRepository.find(); for (const club of clubs) { // Calculate the club's level based on the amount of experience it has const experience = club.experience; let lvl = 1; for (let i = levels.length - 1; i >= 0; i--) { if (experience >= levels[i]) { lvl = i + 1; break; } } // Calculate the amount of experience required to reach the next level let expToNextLvl = 0; if (lvl < levels.length) { const expForCurrentLvl = levels[lvl - 1]; const expForNextLvl = levels[lvl]; const expToReachNextLvl = expForNextLvl - expForCurrentLvl; const expAtCurrentLvl = experience - expForCurrentLvl; expToNextLvl = expToReachNextLvl - expAtCurrentLvl; if (expToNextLvl < 0) { expToNextLvl = 0; } } // Update the club document with the new experience, expToNextLvl, and lvl fields await this.clubRepository.findOneAndUpdate(club._id, { $set: { experience, expToNextLvl, lvl, }, }); } } }
如果这段脚本逻辑没问题,请问怎么设置才能在experience实际变更时触发它?
一、迁移脚本的定位
迁移脚本不是实现「实时自动更新」的最佳方案,它的核心作用是一次性处理存量数据——比如你刚新增level和expToNextLvl字段时,跑一次迁移把所有已有Club文档的这两个字段计算补全。但它没法监听experience的实时变更,做不到数据更新时自动同步。
二、你的脚本逻辑调整
你的脚本计算level和expToNextLvl的逻辑是可行的,但有两个小优化点:
- 循环遍历
levels从后往前找等级的方式可以换成二分查找(如果levels是升序排列),提升大数量级下的效率; - 更新时不需要重复
$set: { experience },原文档的experience没变化,只需要更新计算后的lvl和expToNextLvl。
可以把计算逻辑抽成独立工具函数,方便复用:
// 抽离成工具函数,在迁移和实时更新场景都能用 export const calculateLevelAndExp = (experience: number, levels: number[]) => { let lvl = 1; // 二分查找(假设levels是升序排列,比如[100, 300, 500]) let left = 0; let right = levels.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); if (experience >= levels[mid]) { lvl = mid + 1; left = mid + 1; } else { right = mid - 1; } } let expToNextLvl = 0; if (lvl < levels.length) { expToNextLvl = levels[lvl] - experience; // 确保数值非负 expToNextLvl = Math.max(expToNextLvl, 0); } return { lvl, expToNextLvl }; };
三、实现实时自动更新的方案
要在experience变更时自动同步level和expToNextLvl,结合你用的NestJS,推荐两种方案:
方案1:业务逻辑层拦截更新操作
在修改experience的Service方法里,每次更新经验后调用工具函数计算新值,再同步写入数据库。这种方式逻辑可控,和业务代码紧密结合,适配NestJS的分层架构。
示例代码:
import { Injectable } from '@nestjs/common'; import { ClubRepository } from '@shared/lib/mongo/entities/club'; import { calculateLevelAndExp } from './utils'; // 等级配置可以存在数据库/配置文件,这里用常量示例 const LEVELS = [100, 300, 500, 800, 1200]; @Injectable() export class ClubService { constructor(private clubRepository: ClubRepository) {} async addExperience(clubId: string, addExp: number) { const club = await this.clubRepository.findOne({ _id: clubId }); if (!club) throw new Error('俱乐部不存在'); const newExperience = club.experience + addExp; const { lvl, expToNextLvl } = calculateLevelAndExp(newExperience, LEVELS); // 一次性更新所有字段 await this.clubRepository.findOneAndUpdate( { _id: clubId }, { $set: { experience: newExperience, lvl, expToNextLvl, }, } ); } }
方案2:MongoDB变更流(Change Streams)监听
如果不想在业务代码里耦合该逻辑,可以用MongoDB的Change Streams监听Club集合的更新事件,当检测到experience字段被修改时,自动触发计算并更新。
注意:该方案要求MongoDB部署在副本集或分片集群模式(单节点默认不支持Change Streams)。
NestJS中实现示例:
import { Injectable, OnModuleInit } from '@nestjs/common'; import { ClubRepository } from '@shared/lib/mongo/entities/club'; import { calculateLevelAndExp } from './utils'; const LEVELS = [100, 300, 500, 800, 1200]; @Injectable() export class ClubChangeStreamService implements OnModuleInit { constructor(private clubRepository: ClubRepository) {} async onModuleInit() { // 监听Club集合的更新事件,只关注experience字段变更 const changeStream = this.clubRepository.collection.watch([ { $match: { operationType: 'update', 'updateDescription.updatedFields.experience': { $exists: true }, }, }, ]); changeStream.on('change', async (change) => { const clubId = change.documentKey._id; const newExperience = change.updateDescription.updatedFields.experience; const { lvl, expToNextLvl } = calculateLevelAndExp(newExperience, LEVELS); await this.clubRepository.findOneAndUpdate( { _id: clubId }, { $set: { lvl, expToNextLvl } } ); }); } }
四、迁移脚本的正确用法
如果是第一次新增level和expToNextLvl字段,你原来的migrate方法可以用来补全存量数据,只需要在应用启动时手动触发一次,或者写一个CLI命令执行:
示例CLI命令(基于NestJS的CLI扩展):
import { Command } from '@nestjs-architects/typed-cqrs'; import { ClubMigrationService } from './club-migration.service'; @Command({ name: 'club:migrate' }) export class ClubMigrateCommand { constructor(private migrationService: ClubMigrationService) {} async run() { const LEVELS = [100, 300, 500, 800, 1200]; await this.migrationService.migrate(LEVELS); console.log('俱乐部数据迁移完成'); } }
部署后执行npx nest run club:migrate,即可补全所有已有Club文档的level和expToNextLvl字段。
内容的提问来源于stack exchange,提问作者BrzKrz

