ASP.NET Core Razor Pages如何防止表单重复提交至PageModel/API
解决表单重复提交问题
问题背景
我的PageModel存在用户重复提交表单的问题,连续按Space键触发保存按钮就能复现。试过用遮罩层防止主动点击,但这种方式能被绕过;也尝试在表单提交时禁用保存按钮,但代码没生效:
<form id="questionnaireForm" method="post" onsubmit="disableSave(); toggleOverlay();"> <script> function disableSave() { document.querySelector("#saveSectionButton").disabled = true } function toggleOverlay() { const overlay = document.querySelector("#overlay") var isHidden = overlay.classList.contains("d-none") if (!isHidden) { overlay.classList.add("d-none") } else { overlay.classList.remove("d-none") } } </script>
原本以为模型绑定能避免这类问题,后来在OnPostAsync方法里加了检查,试图更新现有数据而非保存重复项:
public async Task<IActionResult> OnPostAsync() { // Work on the page modifies the public Answers BindProperty. if (this.Answers == null || this.Answers.Count == 0) { throw new ArgumentException("Answers cannot be null or empty."); } var answered = this.Answers.Where(x => x.Value is not null); foreach (var answer in answered) { // Basic validation removed for brevity. var question = await context.QuestionnaireQuestions .SingleOrDefaultAsync(x => x.Id == answer.QuestionId); // Handle potential updates to the answer. var existing = context.QuestionnaireAnswers .Where(x => x.Question.Id == answer.QuestionId && x.Booking.Id == this.BookingId); if (existing.Any()) { await UpdateAnswer(this.BookingId, answer, question, existing); continue; } // Continue to process and save the new answer to the database. } }
但这里有个明显缺陷:快速连续提交多个请求时,第一个请求还没完成保存,第二个请求就已经检查完现有答案,导致还是会插入重复数据。
解决方案
1. 修复前端按钮禁用逻辑
之前的onsubmit事件禁用逻辑可能因为表单提交流程问题没生效,改成按钮点击时立即禁用,同时添加提交标记防止重复触发:
<form id="questionnaireForm" method="post"> <button id="saveSectionButton" type="submit">保存</button> </form> <script> let isSubmitting = false; const form = document.querySelector("#questionnaireForm"); const saveBtn = document.querySelector("#saveSectionButton"); const overlay = document.querySelector("#overlay"); form.addEventListener("submit", function(e) { if (isSubmitting) { e.preventDefault(); return; } isSubmitting = true; saveBtn.disabled = true; overlay.classList.remove("d-none"); }); </script>
2. 后端添加并发控制
方式一:悲观锁控制(单实例场景)
在查询现有答案时,使用数据库悲观写锁锁定对应行,阻止其他请求同时插入:
public async Task<IActionResult> OnPostAsync() { if (this.Answers == null || this.Answers.Count == 0) { throw new ArgumentException("Answers cannot be null or empty."); } var answered = this.Answers.Where(x => x.Value is not null); foreach (var answer in answered) { var question = await context.QuestionnaireQuestions .SingleOrDefaultAsync(x => x.Id == answer.QuestionId); // 加悲观写锁,锁定匹配的行 var existing = await context.QuestionnaireAnswers .Where(x => x.Question.Id == answer.QuestionId && x.Booking.Id == this.BookingId) .WithLock(LockMode.PessimisticWrite) .FirstOrDefaultAsync(); if (existing != null) { await UpdateAnswer(this.BookingId, answer, question, existing); continue; } // 新增答案逻辑 var newAnswer = new QuestionnaireAnswer { BookingId = this.BookingId, QuestionId = answer.QuestionId, Value = answer.Value }; context.QuestionnaireAnswers.Add(newAnswer); } await context.SaveChangesAsync(); return RedirectToPage("./Success"); }
方式二:分布式锁(多实例部署场景)
如果是多服务器部署,用Redis实现分布式锁,确保同一BookingId下的提交同一时间只有一个在处理:
private readonly IDistributedLock _distributedLock; public async Task<IActionResult> OnPostAsync() { using (var lockHandle = await _distributedLock.TryAcquireLockAsync($"questionnaire:{this.BookingId}", TimeSpan.FromSeconds(10))) { if (lockHandle == null) { ModelState.AddModelError("", "操作过于频繁,请稍后再试"); return Page(); } // 原有处理逻辑(同悲观锁版本) if (this.Answers == null || this.Answers.Count == 0) { throw new ArgumentException("Answers cannot be null or empty."); } var answered = this.Answers.Where(x => x.Value is not null); foreach (var answer in answered) { var question = await context.QuestionnaireQuestions .SingleOrDefaultAsync(x => x.Id == answer.QuestionId); var existing = await context.QuestionnaireAnswers .FirstOrDefaultAsync(x => x.Question.Id == answer.QuestionId && x.Booking.Id == this.BookingId); if (existing != null) { await UpdateAnswer(this.BookingId, answer, question, existing); continue; } var newAnswer = new QuestionnaireAnswer { BookingId = this.BookingId, QuestionId = answer.QuestionId, Value = answer.Value }; context.QuestionnaireAnswers.Add(newAnswer); } await context.SaveChangesAsync(); } return RedirectToPage("./Success"); }
3. 数据库层面添加唯一约束(终极保障)
在QuestionnaireAnswers表上添加联合唯一约束,约束字段为QuestionId和BookingId,即使前后端控制失效,数据库也会直接拒绝重复插入。
用EF Core Fluent API配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<QuestionnaireAnswer>() .HasIndex(a => new { a.QuestionId, a.BookingId }) .IsUnique(); }
后端捕获唯一约束异常,返回友好提示:
try { await context.SaveChangesAsync(); } catch (DbUpdateException ex) { if (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2601) // SQL Server唯一约束冲突错误码 { ModelState.AddModelError("", "该问题已提交答案,无需重复提交"); return Page(); } throw; }
内容的提问来源于stack exchange,提问作者Ortund
相关产品推荐
相关产品推荐

