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

设计MongoDB Schema的最优方式?工位预订系统Mongoose Schema求优化建议

现有Schema的核心问题

  • 缺失核心关联字段:User表未关联所属公司,Booking表未关联工位、办公室、公司,完全无法满足业务权限控制和多维度查询需求
  • 工位预订字段设计错误:Office.desk.bookings为单值ObjectId,一个工位无法存储多时间段的预订记录
  • 双向引用冗余且易出错:同时在User.bookings、Office.desk.bookings存储预订引用,新增/删除预订时需要同时更新3张表,极易出现数据不一致
  • 基础约束缺失:大量业务唯一字段未加唯一索引、必填校验,也未做敏感字段的访问限制
  • 缺乏常用扩展字段:没有预订状态、用户角色这类高频使用的属性,后续业务迭代会非常痛苦

优化后的Schema设计方案

核心思路是以Booking为主表存储全量关联信息,取消冗余双向引用,增加必要约束和索引,减少数据维护成本,提升查询效率

const mongoose = require('mongoose')
const Schema = mongoose.Schema

// 公司表
const CompanySchema = new Schema({
    companyName: {
        type: String,
        required: true
    },
    companyLogo: String,
    uniqueCompanyCode: {
        type: String,
        required: true,
        unique: true // 公司编码全局唯一
    },
    companyPasscode: {
        type: String,
        required: true,
        select: false // 禁止普通查询返回通行码,避免泄露
    },
    companyAdmins: [{
        type: Schema.Types.ObjectId,
        ref: "User"
    }],
    offices: [{
        type: Schema.Types.ObjectId,
        ref: 'Office'
    }]
}, { timestamps: true }) // 自动生成创建/更新时间,后续排查问题更方便

// 用户表
const UserSchema = new Schema({
    email: {
        type: String,
        required: true,
        unique: true
    },
    companyId: {
        type: Schema.Types.ObjectId,
        ref: 'Company',
        required: true // 所有用户必须关联所属公司
    },
    role: {
        type: String,
        enum: ['admin', 'employee'],
        default: 'employee',
        required: true
    }
}, { timestamps: true })

// passportLocalMongoose会自动生成username、hash、salt字段,无需手动添加
UserSchema.plugin(passportLocalMongoose);

// 办公室表
const OfficeSchema = new Schema({
    companyId: {
        type: Schema.Types.ObjectId,
        ref: 'Company',
        required: true
    },
    officeAddress:{
        streetAddress: String,
        town: String,
        county: String,
        postcode: String
    },
    floorPlan: String,
    desks: [{
        deskNumber: {
            type: Number,
            required: true
        },
        description: String // 可选,可存储靠窗、带显示器等工位描述
    }]
}, { timestamps: true })

// 同办公室工位号唯一的复合索引,避免重复工位号
OfficeSchema.index({ companyId: 1, 'desks.deskNumber': 1 }, { unique: true })

// 预订表(核心主表)
const BookingSchema = new Schema({
    companyId: {
        type: Schema.Types.ObjectId,
        ref: 'Company',
        required: true
    },
    officeId: {
        type: Schema.Types.ObjectId,
        ref: 'Office',
        required: true
    },
    deskId: {
        type: Schema.Types.ObjectId,
        required: true // 对应Office.desks数组里的_id
    },
    deskNumber: {
        type: Number,
        required: true // 冗余存储工位号,查询预订时无需关联Office表直接展示
    },
    bookedFrom: {
        type: Date,
        required: true
    },
    bookedTo: {
        type: Date,
        required: true
    },
    bookedBy: {
        type: Schema.Types.ObjectId,
        ref: "User",
        required: true
    },
    status: {
        type: String,
        enum: ['active', 'cancelled', 'completed'],
        default: 'active',
        required: true
    }
}, { timestamps: true })

// 高频查询索引,大幅提升查询速度
BookingSchema.index({ deskId: 1, bookedFrom: 1, bookedTo: 1 }) // 预订冲突校验专用索引
BookingSchema.index({ bookedBy: 1, bookedFrom: -1 }) // 用户查个人预订按时间倒序
BookingSchema.index({ officeId: 1, bookedFrom: -1 }) // 管理员查办公室预订按时间倒序

// 预订时间校验中间件,保存前自动判断结束时间晚于开始时间
BookingSchema.pre('save', function(next) {
    if (this.bookedTo <= this.bookedFrom) {
        next(new Error('预订结束时间必须晚于开始时间'))
    }
    next()
})

对应查询实现(完全满足多维度查询需求)

  • 员工查自己的所有有效预订:Booking.find({ bookedBy: userId, status: 'active' }).sort({ bookedFrom: -1 })
  • 管理员查指定办公场地的所有有效预订:Booking.find({ officeId: officeId, status: 'active' }).sort({ bookedFrom: -1 })
  • 预订前校验时间段冲突:Booking.find({ deskId: deskId, status: 'active', $or: [{ bookedFrom: { $lt: newBookedTo } }, { bookedTo: { $gt: newBookedFrom } }] }),返回结果不为空则代表时间段冲突
  • 无需维护多表引用关联,新增/取消预订仅需操作Booking单表即可,完全避免数据不一致问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 11:45:07