NestJS结合Mongoose如何实现自定义格式自增序列字段?
NestJS + Mongoose 实现自定义格式自增序列方案
在NestJS搭配Mongoose的技术栈下,完全可以实现格式为XAT22000001这类带固定前缀、年份标识的自定义自增序列字段,不需要依赖第三方自增插件即可落地,核心逻辑是通过独立计数集合+原子更新保证序号唯一、连续。
核心实现思路
- 单独维护一个序列计数集合,按「考试前缀_年份后两位」的维度存储对应已使用的最大序号,避免不同考试、不同年份的注册号串号
- 生成注册号时,先拼接固定考试前缀+当前年份后两位,再从计数集合中获取对应维度的下一个序号,补零到指定位数后拼接为完整注册号
- 序号递增必须使用数据库原子操作,避免高并发场景下生成重复注册号
具体实现步骤
1. 定义序列计数Schema
首先创建专门存储序列值的集合结构,给序列标识字段加唯一索引:
// src/schemas/counter.schema.ts import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; export type CounterDocument = Counter & Document; @Schema({ versionKey: false }) export class Counter { // 序列唯一标识,格式示例:XAT_22 @Prop({ required: true, unique: true }) seqKey: string; // 当前已分配的最大序号 @Prop({ required: true, default: 0 }) seqValue: number; } export const CounterSchema = SchemaFactory.createForClass(Counter);
记得在NestJS的模块中引入该Model,和其他Mongoose模型注册方式一致。
2. 业务集合添加注册号字段
在需要存储自定义注册号的业务Schema(比如考试报名记录表)中添加对应字段,给字段加唯一索引做兜底校验:
// src/schemas/exam-registration.schema.ts import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; export type ExamRegistrationDocument = ExamRegistration & Document; @Schema({ versionKey: false, timestamps: true }) export class ExamRegistration { // 自定义格式自增注册号 @Prop({ required: true, unique: true }) registrationNo: string; // 其余业务字段按需定义,比如考生姓名、证件号、报考科目等 @Prop({ required: true }) candidateName: string; } export const ExamRegistrationSchema = SchemaFactory.createForClass(ExamRegistration);
3. 实现原子性序号生成逻辑
在对应的业务Service中实现注册号生成方法,通过Mongoose的findOneAndUpdate + $inc实现原子递增,避免并发冲突:
// src/exam-registration/exam-registration.service.ts import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Counter, CounterDocument } from '../schemas/counter.schema'; import { ExamRegistration, ExamRegistrationDocument } from '../schemas/exam-registration.schema'; @Injectable() export class ExamRegistrationService { constructor( @InjectModel(Counter.name) private counterModel: Model<CounterDocument>, @InjectModel(ExamRegistration.name) private registrationModel: Model<ExamRegistrationDocument>, ) {} /** * 生成指定考试的自定义格式注册号 * @param examPrefix 考试前缀标识,默认XAT */ async generateRegistrationNo(examPrefix: string = 'XAT'): Promise<string> { // 取当前年份后两位,例如2024年取'24' const yearSuffix = new Date().getFullYear().toString().slice(-2); const seqKey = `${examPrefix}_${yearSuffix}`; // 原子更新:存在对应序列则序号+1,不存在则初始化序列从1开始计数 const updatedCounter = await this.counterModel.findOneAndUpdate( { seqKey }, { $inc: { seqValue: 1 } }, { new: true, upsert: true, setDefaultsOnInsert: true }, ); // 序号补零到6位,拼接为最终注册号,格式示例:XAT22000001 const seqPart = updatedCounter.seqValue.toString().padStart(6, '0'); return `${examPrefix}${yearSuffix}${seqPart}`; } // 创建报名记录的业务方法 async createRegistration(candidateName: string) { const registrationNo = await this.generateRegistrationNo(); return this.registrationModel.create({ registrationNo, candidateName, }); } }
注意事项
- 不要用「先查询当前序号、再手动+1更新」的非原子逻辑,高并发场景下100%会出现重复注册号
- 计数集合的
seqKey、业务集合的registrationNo必须加唯一索引,作为数据一致性的兜底保障 - 该方案天然支持跨年自动重置序号:新年份会生成新的
seqKey,序号自动从1开始计数,不需要手动维护 - 如果需要支持多考试类型,只需要调用生成方法时传入对应考试前缀即可,不同考试的计数完全隔离
- 如果需要手动调整序号起始值,直接修改Counter集合中对应
seqKey的seqValue字段即可,后续生成会从修改后的值继续递增
内容的提问来源于stack exchange,提问作者Rinoj A.J.
相关产品推荐
相关产品推荐

