feat(B2): 附件引用模型-cms_attachment 增 ref_type/ref_id,消灭孤儿误报

- 实体/双 SQL 加列+idx_att_ref;upload 增 scene 白名单(banner/link/site)
- AttachmentService 新增 markUsed/release/markSiteConfigUsed(webUrl 反查,幂等)
- 未使用=articleId 与 refType 均空;page 返回 unusedCount(前后端同步改名无兼容别名)
- Banner/Link 保存先解引用再回写(换图旧图回未使用);Site 配置 logo/favicon 引用回写(@Lazy 解循环依赖)
- 新增13例(Attachment6/Banner3/Link2/Site2)+既有语义断言更新;mvn -o test 全绿
This commit is contained in:
chenxing
2026-09-07 19:40:14 +08:00
parent 52676f592a
commit b4822a7f38
15 changed files with 466 additions and 42 deletions
@@ -136,4 +136,72 @@ class LinkServiceIT extends CmsIntegrationTestBase {
.isInstanceOf(BizException.class)
.hasMessageContaining("不存在");
}
// ===== B2 附件引用模型(友链 logo 引用回写)=====
@Autowired
private AttachmentService attachmentService;
/** 1x1 真实 png(上传需过魔数校验) */
private org.springframework.mock.web.MockMultipartFile pngFile() {
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(1, 1, java.awt.image.BufferedImage.TYPE_INT_RGB);
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
try {
javax.imageio.ImageIO.write(img, "png", out);
} catch (java.io.IOException e) {
throw new IllegalStateException(e);
}
return new org.springframework.mock.web.MockMultipartFile("file", "logo.png", "image/png", out.toByteArray());
}
@Test
@DisplayName("B2: 保存友链回写 attachment.ref_type=link, ref_id=linkId;删除解引用")
void save_writesAttachmentRef_deleteReleases() {
com.yunhai.cms.model.vo.AttachmentVo logo = attachmentService.upload(
TestUsers.superAdmin(), siteId, null, "link", pngFile());
LinkSaveRequest r = req("带Logo友链", "https://www.example.com");
r.setLogoUrl(logo.getUrl());
LinkVo vo = linkService.save(TestUsers.superAdmin(), r);
com.yunhai.cms.entity.CmsAttachment att = attachmentRepository.findById(logo.getId())
.orElseThrow(IllegalStateException::new);
assertThat(att.getRefType()).isEqualTo("link");
assertThat(att.getRefId()).isEqualTo(vo.getId());
DeleteRequest del = new DeleteRequest();
del.setSiteId(siteId);
del.setId(vo.getId());
linkService.delete(TestUsers.superAdmin(), del);
com.yunhai.cms.entity.CmsAttachment released = attachmentRepository.findById(logo.getId())
.orElseThrow(IllegalStateException::new);
assertThat(released.getRefType()).isNull();
assertThat(released.getRefId()).isNull();
}
@Test
@DisplayName("B2: 友链无 logo 不产生附件引用;logo 清空后引用解除")
void save_withoutLogo_noRef() {
LinkSaveRequest r = req("纯文字友链", "https://www.example.com");
LinkVo vo = linkService.save(TestUsers.superAdmin(), r);
assertThat(attachmentRepository.findByRefTypeAndRefId("link", vo.getId())).isEmpty();
com.yunhai.cms.model.vo.AttachmentVo logo = attachmentService.upload(
TestUsers.superAdmin(), siteId, null, "link", pngFile());
LinkSaveRequest withLogo = req("先有Logo", "https://www.example.com");
withLogo.setId(vo.getId());
withLogo.setLogoUrl(logo.getUrl());
linkService.save(TestUsers.superAdmin(), withLogo);
assertThat(attachmentRepository.findByRefTypeAndRefId("link", vo.getId()))
.extracting(com.yunhai.cms.entity.CmsAttachment::getId)
.containsExactly(logo.getId());
// logo 清空(改回纯文字)→ 引用解除
LinkSaveRequest clear = req("再去Logo", "https://www.example.com");
clear.setId(vo.getId());
clear.setLogoUrl("");
linkService.save(TestUsers.superAdmin(), clear);
assertThat(attachmentRepository.findByRefTypeAndRefId("link", vo.getId())).isEmpty();
}
}