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

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.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 18:16:00