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

如何在Node.js中为类Google Forms问卷应用设计Mongoose Schema

实现方案

核心设计思路

要实现自定义字段的需求,需要拆分两个独立的Mongoose模型,把问卷配置规则和用户提交数据解耦存储,适配不同创建者的差异化字段配置:

  • 「问卷模板模型」:存储创建者配置的所有自定义字段规则、下拉选项等元信息
  • 「问卷提交模型」:存储用户对应模板填写的实际内容

1. 问卷模板模型代码

用于存储创建者配置的自定义字段规则,下拉类字段的可选值会直接存在对应字段的options属性中:

const mongoose = require('mongoose')
const validator = require('validator')

// 单条自定义字段子Schema
const formFieldSchema = new mongoose.Schema({
  // 字段显示标题,比如「姓名」「性别」
  label: {
    type: String,
    required: [true, '字段标题不能为空']
  },
  // 字段类型,限定支持的输入类型
  type: {
    type: String,
    required: true,
    enum: ['text', 'number', 'email', 'phone', 'select', 'radio', 'checkbox']
  },
  // 字段是否为必填项
  required: {
    type: Boolean,
    default: false
  },
  // 仅下拉/单选/多选类字段需要配置该属性,存储可选值
  options: {
    type: [String],
    // 条件必填:仅当字段为选择类时需要传options
    required: function() {
      return ['select', 'radio', 'checkbox'].includes(this.type)
    },
    validate: {
      validator: function(val) {
        // 选择类字段至少要有2个可选值
        return !['select', 'radio', 'checkbox'].includes(this.type) || val.length >=2
      },
      message: '选择类字段至少需要2个可选值'
    }
  },
  // 字段排序值,用于调整字段展示顺序
  order: {
    type: Number,
    required: true
  }
})

// 问卷模板主Schema
const questionnaireTemplateSchema = new mongoose.Schema({
  author: {
    type: mongoose.Schema.ObjectId,
    ref: 'User',
    required: true
  },
  // 问卷整体标题
  title: {
    type: String,
    required: [true, '问卷标题不能为空']
  },
  // 所有自定义字段配置数组
  fields: [formFieldSchema],
  createdAt: {
    type: Date,
    default: Date.now
  }
})

const QuestionnaireTemplate = mongoose.model('QuestionnaireTemplate', questionnaireTemplateSchema)
module.exports = QuestionnaireTemplate

2. 用户提交结果模型代码

用于存储用户填写的实际内容,适配任意自定义字段结构,同时内置提交校验逻辑,确保值符合模板规则:

const mongoose = require('mongoose')
const validator = require('validator')
// 需引入上一步定义的问卷模板模型
const QuestionnaireTemplate = require('./questionnaireTemplate.model')

// 单条字段提交值子Schema
const submissionValueSchema = new mongoose.Schema({
  // 关联模板中对应字段的ID
  fieldId: {
    type: mongoose.Schema.ObjectId,
    required: true
  },
  // 存储用户提交的值,用Mixed类型适配不同输入类型的取值
  value: {
    type: mongoose.Schema.Types.Mixed,
    required: true
  }
})

// 提交结果主Schema
const questionnaireSubmissionSchema = new mongoose.Schema({
  // 关联对应的问卷模板ID
  templateId: {
    type: mongoose.Schema.ObjectId,
    ref: 'QuestionnaireTemplate',
    required: true
  },
  // 提交人ID,匿名提交可以不填
  submitter: {
    type: mongoose.Schema.ObjectId,
    ref: 'User'
  },
  // 所有字段的提交值数组
  values: [submissionValueSchema],
  submittedAt: {
    type: Date,
    default: Date.now
  }
})

// 预保存钩子:提交时自动校验值是否符合模板规则
questionnaireSubmissionSchema.pre('save', async function(next) {
  const template = await QuestionnaireTemplate.findById(this.templateId)
  const fieldMap = new Map(template.fields.map(f => [f._id.toString(), f]))

  for (const item of this.values) {
    const field = fieldMap.get(item.fieldId.toString())
    if (!field) throw new Error(`字段${item.fieldId}不存在`)
    // 必填校验
    if (field.required && (item.value === undefined || item.value === null || item.value === '')) {
      throw new Error(`字段${field.label}为必填项`)
    }
    // 类型/规则校验
    switch(field.type) {
      case 'email':
        if (!validator.isEmail(item.value)) throw new Error(`字段${field.label}请输入正确的邮箱格式`)
        break
      case 'number':
        if (isNaN(Number(item.value))) throw new Error(`字段${field.label}请输入数字`)
        break
      case 'select':
      case 'radio':
        if (!field.options.includes(item.value)) throw new Error(`字段${field.label}选项值不合法`)
        break
      case 'checkbox':
        if (!Array.isArray(item.value) || item.value.some(v => !field.options.includes(v))) {
          throw new Error(`字段${field.label}选项值不合法`)
        }
        break
    }
  }
  next()
})

const QuestionnaireSubmission = mongoose.model('QuestionnaireSubmission', questionnaireSubmissionSchema)
module.exports = QuestionnaireSubmission

使用示例

创建问卷模板

const newTemplate = await QuestionnaireTemplate.create({
  author: 'xxx用户ID',
  title: '员工信息收集问卷',
  fields: [
    { label: '姓名', type: 'text', required: true, order: 1 },
    { label: '年龄', type: 'number', required: true, order: 2 },
    { label: '性别', type: 'select', required: true, options: ['男', '女', '不愿透露'], order: 3 }
  ]
})

提交用户填写内容

const newSubmission = await QuestionnaireSubmission.create({
  templateId: newTemplate._id,
  values: [
    { fieldId: newTemplate.fields[0]._id, value: '张三' },
    { fieldId: newTemplate.fields[1]._id, value: 28 },
    { fieldId: newTemplate.fields[2]._id, value: '男' }
  ]
})

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 16:15:05