fix(content): planning 字段快照物理删除,修二次保存重复键 500

captureFieldSnapshot 的 deleteBySectionId 原走 MyBatis-Plus 逻辑删除(@TableLogic):
被删物理行(deleted=TRUE)仍占唯一约束 uk(tenant_id,section_id,field_key)(不含 deleted),
用户二次保存同 section 字段时 INSERT 撞键→DuplicateKeyException→规划保存 500。
改原生 @Delete 物理清行,根治幂等(快照是可重建派生数据,物理删除无审计损失)。

根因:全量 e2e 暴露 planning-edit save 500(work-schema-binding 为并发 flaky,单跑绿)。
验证:planning-edit e2e 真后端 1 passed;连续 2 次 save 同 section code=0(rev 3→4,旧 jar 第 2 次必 500)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-21 10:09:07 -07:00
parent 494f3b5fd6
commit a9323adb13

View File

@ -3,7 +3,9 @@ package cn.iocoder.muse.module.content.dal.mysql;
import cn.iocoder.muse.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.muse.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.muse.module.content.dal.dataobject.PlanningFieldSnapshotDO;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@ -17,11 +19,16 @@ import java.util.List;
@Mapper
public interface PlanningFieldSnapshotMapper extends BaseMapperX<PlanningFieldSnapshotDO> {
/** 删除某 section 的旧快照(保存 planning 时先删后写,保证重复保存幂等)。 */
default int deleteBySectionId(Long sectionId) {
return delete(new LambdaQueryWrapperX<PlanningFieldSnapshotDO>()
.eq(PlanningFieldSnapshotDO::getSectionId, sectionId));
}
/**
* 物理删除某 section 的旧快照保存 planning 时先删后写保证重复保存幂等
*
* <p>WHY 物理删除而非 MyBatis-Plus 逻辑删除本表是字段用量投影(可重建的派生数据无审计价值),
* 唯一约束 uk(tenant_id, section_id, field_key) 不含 deleted 若走 @TableLogic 逻辑删除,
* 被删的物理行(deleted=TRUE)仍占用 uk,再次保存同字段时 INSERT 必撞唯一键DuplicateKeyException(二次保存即 500)
* 故用原生 DELETE 清干净物理行;tenant 插件会对该 DELETE 追加 tenant_id 条件, section_id 全局唯一,双重隔离</p>
*/
@Delete("DELETE FROM muse_content_planning_field_snapshot WHERE section_id = #{sectionId}")
int deleteBySectionId(@Param("sectionId") Long sectionId);
/** 查某 section 现有快照(测试 / 校验用)。 */
default List<PlanningFieldSnapshotDO> selectListBySectionId(Long sectionId) {