feat(admin): 时间配置加入值级乐观锁 + 并发冲突返回 4090(B②)
场景:两个管理员同时打开「时间设置」,甲先保存,乙手里的表单已过期,
乙再保存就会静默覆盖甲的结果 —— 此前 setTimes 直接 upsert,无任何检测。
- service/admin.js:setTimes(times, { expected }) 先**全量校验**、全部通过后才写
(边校验边 upsert 会留下"部分生效"的半成品)。判据选值级比对而非时间戳:
competition_times.updated_at 没有 ON UPDATE CURRENT_TIMESTAMP,其刷新依赖
Sequelize 对 default 字面量的处理,语义不确定 ⇒ 建在它上面等于建在流沙上。
比对前归一到秒级,避免毫秒尾差造成恒定假冲突。
- controller/admin.js:冲突返回 **200 + code=4090**(⛔ 5xx —— 按 500 返回会让前端
拦截器提示"服务器错误",把"你刷新一下"掩盖成"系统挂了")。
- 不传 expectedTimes ⇒ 退回旧行为,不破坏既有调用方。
This commit is contained in:
@@ -591,9 +591,19 @@ class AdminController extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
await service.admin.setTimes(times);
|
||||
// B② 乐观锁:客户端可回传它读到的旧值(expectedTimes);与库里现值不一致
|
||||
// 即判冲突,避免后保存者静默覆盖先保存者的结果。不传 ⇒ 退回旧行为。
|
||||
const expectedTimes = (ctx.request.body && ctx.request.body.expectedTimes) || null;
|
||||
await service.admin.setTimes(times, { expected: expectedTimes });
|
||||
ctx.body = ctx.helper.success(null, '比赛时间设置成功');
|
||||
} catch (err) {
|
||||
// 🔴 并发冲突是业务性保护,不是 500:必须「200 + 业务码」。
|
||||
// 按 500 返回会让前端拦截器提示「服务器错误」,掩盖真实原因。
|
||||
if (err && err.name === 'TimesConflictError') {
|
||||
ctx.logger.warn('[setTimes] 时间配置并发冲突: %j', err.conflicts);
|
||||
ctx.body = ctx.helper.error(4090, '比赛时间已被他人修改,请刷新后重试');
|
||||
return;
|
||||
}
|
||||
ctx.logger.error('设置比赛时间失败:', err);
|
||||
ctx.body = ctx.helper.error(500, '设置比赛时间失败');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user