如何在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
相关产品推荐
相关产品推荐

