简易问卷系统(一)背景和模型设计

分类: 开发笔记

背景

一位经管院的同学需要定制一份问卷,希望能够让用户填写一题,就能保存提交一题,后台按照题目来查看,并不管用户是否全部做完。而现有的问卷系统大多是把问卷作为基本单位——答完全部题目之后,才能提交问卷,一次性保存全部填写的结果。于是,这位同学找我希望能够定制这样一份问卷。

当时我在微软实习,大组内就有用ASP.NET + React的项目,想想我还没接触过,正有尝试的意思。于是,干脆答应了,而且不是只做一份问卷,打算直接做一个问卷系统(平台)。

经过对React的学习,初步了解了它组件式开发前端的思想。结合逐题提交的要求,我做了一个大胆的设想……

设计特点

市面上的问卷平台,像问卷星、腾讯问卷等,都是预设了诸多题型,可以加入到问卷中。然而,如果用户有独特的需求,需要定制组件,那可能只能走商业洽谈的途径了。

React为前端带来了组件式的解决方案,那么不妨就把问卷中的每一种题型,都抽象成一种React组件——听到这,好像理所应当;既然决定用React,自然要这样做呀——但我还想,用户是否也能自己上传React组件的代码,自定义组件呢?

此时并不确定这样可行性是否很高,也许会带来复杂度的急剧增加,有些得不偿失。而且,这种设计,对于一般用户群体而言,真是没有什么用处,可以说是优先级极低的考虑了。然而,就技术层面而言,如果能做到这个特点,应该可以让系统的设计更加丰富有趣,也能储备一些技术经验。

那么就干吧!

说的很笼统,该系列文章后面第(二)(三)部分的内容也很简略,可以先看第(四)部分的Demo演示,对功能设计有个大概的了解。

技术栈

React + ASP.NET SPA with RESTful API

  • C# 7
  • ASP.NET Core 2.0
  • EntityFramework Core
  • SQLite
  • React
  • TypeScript
  • Office UI Fabric React Component

SPA = Single-Page Application

设计细节

数据模型

数据库设计

PK = Primary Key, 主键

FK = Foreign Key, 外键

Questionaire
Name Type Flag Description
Id Int PK ID
Title String Unique Index 标题
Description String 描述文本
OwnerIP String 问卷创建者IP
Guid Guid Unique Index 问卷的唯一标志(创建者保存,用于获取编辑、查看报告等权限)
StartDate DateTime 问卷开始日期
EndDate DateTime 问卷结束日期
CreatedAt DateTimeOffset 创建于
UpdatedAt DateTimeOffset 修改于
Question
Name Type Flag Description
Id Int PK ID
QuestionaireId Int FK Questionaire.Id
QuestionTypeId Int FK QuestionType.Id
Order Int 问题序号
Content String 问题内容(序列化存储问题参数)
CreatedAt DateTimeOffset 创建于
UpdatedAt DateTimeOffset 修改于
QuestionType
Name Type Flag Description
Id Int PK ID
Name String Unique Index 题型名称
CreateFormTSX String FK 创建表单TSX代码(用于题型用户编辑题目信息)
ShowFormTSX String 题目展示表单TSX代码(用于用户填写问卷时展示该题型的题目)
CompiledCreateForm String 编译后创建表单代码(JavaScript)
CompiledShowForm String 编译后展示表单代码(JavaScript)
OwnerIP String 提醒创建者IP
Answer
Name Type Flag Description
Id Int PK ID
QuestionId Int FK Question.Id
Content String 回答内容(序列化存储答案参数)
IP String 回答者IP
CreatedAt DateTimeOffset 创建于
UpdatedAt DateTimeOffset 修改于

C# 模型类

Questionaire
public class Questionaire
{
    public int Id { get; set; }
    [Required] public string Title { get; set; }
    [Required] public string Description { get; set; }
    public string OwnerIP { get; set; }
    public Guid Guid { get; private set; } = Guid.NewGuid();
    [Required] public DateTime StartDate { get; set; }
    [Required] public DateTime EndDate { get; set; }

    // IsEnabled 用于判断问卷是否在启用期内
    [NotMapped]
    public bool IsEnabled
    {
        get
        {
            return DateTime.Compare(DateTime.Now, EndDate) <= 0
                && DateTime.Compare(StartDate, DateTime.Now) <= 0;
        }
    }
    public DateTimeOffset CreatedAt { get; set; }
    public DateTimeOffset UpdatedAt { get; set; }

    // EntityFramework Core 可以自动绑定一对多关系
    // Questionaire对应一系列Question
    [JsonIgnore]
    public ICollection<Question> Questions { get; set; } = new List<Question>();

    // SafeContent用于非创建者读取Questionaire信息,屏蔽IP、Guid等非公开内容
    [NotMapped] [JsonIgnore] 
    public object SafeContent
    {
        get
        {
            var ip = "*.*.*.*";
            var ipParts = OwnerIP.Split('.');
            if (ipParts.Length == 4)
            {
                ip = ipParts[0] + ".*.*." + ipParts[3];
            }

            return new
            {
                Id = Id,
                Title = Title,
                Description = Description,
                OwnerIP = ip,
                StartDate = StartDate,
                EndDate = EndDate,
                IsEnabled = IsEnabled,
                CreatedAt = CreatedAt,
                UpdatedAt = UpdatedAt,
            };
        }
    }
}
Question
public class Question 
{
    public int Id { get; set; }
    [Required] public int QuestionaireId { get; set; }
    // EntityFramework Core自动映射Foreign Key ID到对应对象
    [JsonIgnore] public Questionaire Questionaire { get; set; } 

    [Required] public int TypeId { get; set; } 
    // EntityFramework Core自动映射Foreign Key ID到对应对象
    [JsonIgnore] public QuestionType Type { get; set; } 


    [Required] public int Order { get; set; }
    public string Content { get; set; }

    // EntityFramework Core 可以自动绑定一对多关系
    // Question对应一系列Answer
    [JsonIgnore] public ICollection<Answer> Answers { get; set; } = new List<Answer>();

    public DateTimeOffset CreatedAt { get; set; }
    public DateTimeOffset UpdatedAt { get; set; }
}
QuestionType
public class QuestionType 
{
    public int Id { get; set; } 
    [Required] public string Name { get; set; }
    [Required] public string CreateFormTSX { get; set; }
    [Required] public string ShowFormTSX { get; set; }
    [Required] public string CompiledCreateForm { get; set; }
    [Required] public string CompiledShowForm { get; set; }
    public string OwnerIP { get; set; } 
    public DateTimeOffset CreatedAt { get; set; } 
    public DateTimeOffset UpdatedAt { get; set; }

    // SafeContent用于非创建者读取QuestionType信息,屏蔽IP等非公开内容
    [NotMapped] [JsonIgnore]
    public object SafeContent
    {
        get
        {
            var ip = "*.*.*.*";
            var ipParts = OwnerIP.Split('.');
            if (ipParts.Length == 4)
            {
                ip = ipParts[0] + ".*.*." + ipParts[3];
            }
            return new
            {
                Id = Id,
                Name = Name,
                CreateFormTSX = CreateFormTSX,
                ShowFormTSX = ShowFormTSX,
                CompiledCreateForm = CompiledCreateForm,
                CompiledShowForm = CompiledShowForm,
                OwnerIP = ip,
                CreatedAt = CreatedAt,
                UpdatedAt = UpdatedAt,
            };
        }
    }
}
Answer
public class Answer
{
    public int Id { get; set; }
    [Required] public string Content { get; set; }
    public string OwnerIP { get; set; }
    public string SessionId { get; set; }
    [Required] public int TimeSpent { get; set; } // By Second

    [Required] public int QuestionId { get; set; }
    // EntityFramework Core自动映射Foreign Key ID到对应对象
    [JsonIgnore] public Question Question { get; set; } 

    public DateTimeOffset CreatedAt { get; set; }
    public DateTimeOffset UpdatedAt { get; set; }

    // SafeContent用于非创建者读取Answer信息,屏蔽IP等非公开内容
    [NotMapped] [JsonIgnore]
    public object SafeContent
    {
        get
        {
            var ip = "*.*.*.*";
            var ipParts = OwnerIP.Split('.');
            if (ipParts.Length == 4)
            {
                ip = ipParts[0] + ".*.*." + ipParts[3];
            }

            return new
            {
                Id = Id,
                Content = Content,
                OwnerIP = ip,
                TimeSpent = TimeSpent,
                QuestionId = QuestionId,
                Question = Question,
                CreatedAt = CreatedAt,
                UpdatedAt = UpdatedAt,
            };
        }
    }
}

TypeScript 模型接口

Questionaire
export interface IQuestionaireModel {
    [key: string]: any,
    id: number,
    title: string,
    description: string,
    startDate: Date,
    endDate: Date,
    isEnabled: boolean,
    ownerIP: string,
    guid?: string,
    createdAt: Date,
    updatedAt: Date,
}
Question
export interface IQuestionModel {
    [key: string]: any,
    id: number,
    questionaireId: number,
    typeId: number,
    order: number,
    content: string,
    createdAt: Date,
    updatedAt: Date,
}
QuestionType
export interface IQuestionTypeModel {
    [key: string]: any,
    id: number,
    name: string,
    createFormTSX: string,
    showFormTSX: string,
    compiledCreateForm: string,
    compiledShowForm: string,
    ownerIP: string,
    createdAt: Date,
    updatedAt: Date,
}
Answer
export interface IAnswerModel {
    [key: string]: any,
    id: number,
    content: string,
    sessionId: string,
    timeSpent: number,
    questionId: number,
    ownerIP: string,
    createdAt: Date,
    updatedAt: Date,
}

TypeScript接口中的[key: string]: any可以让你以foo['PropertyName']的形式访问成员属性。