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

ASP.NET Core 6 MVC表单部分保存与自动保存方案咨询

ASP.NET Core 6 MVC 表单部分保存与自动保存实现方案

核心思路

针对原模型/数据库要求必填项完整才能入库的限制,通过草稿存储+Ajax异步交互实现未完成表单的保存与恢复,同时支持自动保存防止数据丢失。


一、调整数据存储方案

方案1:原表新增状态字段(推荐,无需额外表)

在现有表单主表中添加以下字段:

  • IsDraft:bit类型,标记是否为草稿(true=未完成,false=已提交)
  • LastSavedAt:datetime类型,记录最后保存时间
  • 保留原表的UserId、ParticipantId、ParticipantActivityId外键,确保草稿与用户、参与者、活动的关联唯一性

方案2:独立草稿表

若不想修改原表结构,新建FormDrafts表,结构与原表单表一致,但允许原必填字段为null,同时包含UserId、ParticipantId、ParticipantActivityId外键及LastSavedAt字段。提交时将草稿数据迁移至正式表并删除草稿。


二、后端接口实现

在MVC控制器中添加异步Action,处理草稿的保存、读取与提交逻辑:

1. 保存草稿接口

[HttpPost]
public async Task<IActionResult> SaveDraft([FromBody] FormDraftViewModel model)
{
    // 验证用户操作权限(确保当前用户可操作该参与者/活动)
    var draft = await _context.FormSubmissions.FirstOrDefaultAsync(d => 
        d.UserId == model.UserId && 
        d.ParticipantId == model.ParticipantId && 
        d.ParticipantActivityId == model.ParticipantActivityId &&
        d.IsDraft == true);

    if (draft == null)
    {
        draft = new FormSubmission
        {
            UserId = model.UserId,
            ParticipantId = model.ParticipantId,
            ParticipantActivityId = model.ParticipantActivityId,
            IsDraft = true,
            CreatedAt = DateTime.UtcNow
        };
        _context.FormSubmissions.Add(draft);
    }

    // 仅更新已填写的字段(无需校验必填项)
    draft.Section1Field1 = model.Section1Field1;
    draft.Section1Field2 = model.Section1Field2;
    draft.Section2Field1 = model.Section2Field1;
    // ...映射其他小节字段
    draft.LastSavedAt = DateTime.UtcNow;

    await _context.SaveChangesAsync();
    return Ok(new { Success = true, DraftId = draft.Id });
}

2. 获取草稿接口

[HttpGet]
public async Task<IActionResult> GetDraft(int userId, int participantId, int activityId)
{
    var draft = await _context.FormSubmissions.FirstOrDefaultAsync(d => 
        d.UserId == userId && 
        d.ParticipantId == participantId && 
        d.ParticipantActivityId == activityId &&
        d.IsDraft == true);

    if (draft == null)
        return NotFound();

    var viewModel = new FormDraftViewModel
    {
        UserId = draft.UserId,
        ParticipantId = draft.ParticipantId,
        ParticipantActivityId = draft.ParticipantActivityId,
        Section1Field1 = draft.Section1Field1,
        Section1Field2 = draft.Section1Field2,
        // ...映射其他字段
    };
    return Ok(viewModel);
}

3. 正式提交接口

[HttpPost]
public async Task<IActionResult> SubmitForm([FromBody] FormSubmissionViewModel model)
{
    // 校验所有必填项
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    var draft = await _context.FormSubmissions.FirstOrDefaultAsync(d => 
        d.UserId == model.UserId && 
        d.ParticipantId == model.ParticipantId && 
        d.ParticipantActivityId == model.ParticipantActivityId &&
        d.IsDraft == true);

    if (draft != null)
    {
        // 将草稿转为正式提交
        draft.IsDraft = false;
        draft.SubmittedAt = DateTime.UtcNow;
        // 补全剩余必填项(若前端已提交则无需额外处理)
        draft.RequiredField1 = model.RequiredField1;
    }
    else
    {
        // 直接创建正式提交
        var submission = new FormSubmission
        {
            UserId = model.UserId,
            ParticipantId = model.ParticipantId,
            ParticipantActivityId = model.ParticipantActivityId,
            IsDraft = false,
            SubmittedAt = DateTime.UtcNow,
            // ...填充所有必填项
        };
        _context.FormSubmissions.Add(submission);
    }

    await _context.SaveChangesAsync();
    return Ok(new { Success = true, Message = "表单提交成功" });
}

三、前端Ajax实现

1. 手动保存草稿

添加“保存草稿”按钮,点击时收集表单数据并提交至后端:

// 收集当前表单数据
function getFormData() {
    return {
        UserId: $('#userId').val(),
        ParticipantId: $('#participantId').val(),
        ParticipantActivityId: $('#activityId').val(),
        Section1Field1: $('#section1-field1').val(),
        Section1Field2: $('#section1-field2').val(),
        Section2Field1: $('#section2-field1').val()
        // ...其他字段
    };
}

// 手动保存按钮绑定
$('#save-draft-btn').click(function() {
    const formData = getFormData();
    $.ajax({
        url: '/Form/SaveDraft',
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify(formData),
        success: function(res) {
            if (res.Success) {
                $('.save-status').text('草稿已保存').fadeIn().delay(2000).fadeOut();
            }
        },
        error: function() {
            $('.save-status').text('保存失败,请重试').fadeIn().delay(2000).fadeOut();
        }
    });
});

2. 自动保存逻辑

设置定时器,定期检测表单变化并自动保存:

let lastSavedData = {};

// 对比数据是否有变化
function hasFormChanged() {
    const currentData = getFormData();
    return JSON.stringify(currentData) !== JSON.stringify(lastSavedData);
}

// 自动保存定时器(每30秒执行一次)
setInterval(function() {
    if (hasFormChanged()) {
        const formData = getFormData();
        $.ajax({
            url: '/Form/SaveDraft',
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(formData),
            success: function(res) {
                if (res.Success) {
                    lastSavedData = JSON.parse(JSON.stringify(formData));
                    $('.auto-save-status').text('已自动保存').fadeIn().delay(1500).fadeOut();
                }
            }
        });
    }
}, 30000);

3. 页面加载时恢复草稿

$(document).ready(function() {
    const userId = $('#userId').val();
    const participantId = $('#participantId').val();
    const activityId = $('#activityId').val();

    $.ajax({
        url: `/Form/GetDraft?userId=${userId}&participantId=${participantId}&activityId=${activityId}`,
        type: 'GET',
        success: function(res) {
            // 填充表单字段
            $('#section1-field1').val(res.Section1Field1);
            $('#section1-field2').val(res.Section1Field2);
            $('#section2-field1').val(res.Section2Field1);
            // ...其他字段
            lastSavedData = res;
        }
    });
});

四、多参与者/多活动处理建议

  • 切换参与者或活动时,清空当前表单并调用GetDraft接口加载对应草稿(若存在)
  • 在草稿存储时,严格通过UserId+ParticipantId+ParticipantActivityId的组合作为唯一标识,避免不同活动的草稿混淆
  • 前端可添加“草稿列表”入口,展示当前用户所有未完成的草稿,支持快速切换

五、优化建议

  • 给草稿查询字段(UserId、ParticipantId、ParticipantActivityId、IsDraft)添加联合索引,提升查询效率
  • 后端添加乐观锁(如RowVersion字段),避免多标签页编辑同一草稿导致的数据覆盖
  • 定期清理过期草稿(如超过30天未更新的草稿),减少数据库冗余

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 01:58:21