init local code

This commit is contained in:
chenxing
2026-09-07 09:09:39 +08:00
commit 99cd26b3d2
259 changed files with 20554 additions and 0 deletions
@@ -0,0 +1,174 @@
package com.yunhai.cms.service.impl;
import com.yunhai.cms.entity.CmsArticle;
import com.yunhai.cms.entity.CmsBanner;
import com.yunhai.cms.enums.ArticleStatusEnum;
import com.yunhai.cms.enums.CategoryTypeEnum;
import com.yunhai.cms.model.dto.ArticleSaveRequest;
import com.yunhai.cms.model.dto.BannerSaveRequest;
import com.yunhai.cms.model.dto.CategorySaveRequest;
import com.yunhai.cms.model.dto.DeleteRequest;
import com.yunhai.cms.support.CmsIntegrationTestBase;
import com.yunhai.cms.support.TestUsers;
import com.yunhai.platform.apistd.BizException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* 脏数据健壮性回归(第 2 步 bug 清单 D-3/D-4/D-5/D-6):
* 非法枚举码显式拒绝、定时时间窗校验、删除时轮播引用拦截(统一"被引用即拦"规则)。
*/
@DisplayName("脏数据校验 - 枚举/时间窗/删除引用拦截")
@Transactional
class DirtyValidationIT extends CmsIntegrationTestBase {
@Autowired
private ArticleService articleService;
@Autowired
private CategoryService categoryService;
@Autowired
private BannerService bannerService;
private Integer siteId;
private Integer categoryId;
@BeforeEach
void seed() {
siteId = persistSite("dirty1", 1, "脏数据演示站").getId();
categoryId = persistCategory(siteId, 0, "新闻", CategoryTypeEnum.LIST).getId();
}
private CategorySaveRequest catReq(Integer type) {
CategorySaveRequest r = new CategorySaveRequest();
r.setSiteId(siteId);
r.setParentId(0);
r.setName("新栏目");
r.setType(type);
return r;
}
// ===== D-4 非法枚举码 =====
@Test
@DisplayName("栏目 type=9 显式拒绝(不再静默降级为列表栏目)")
void categorySave_illegalType_rejected() {
CategorySaveRequest r = catReq(9);
assertThatThrownBy(() -> categoryService.save(TestUsers.superAdmin(), r))
.isInstanceOf(BizException.class)
.hasMessageContaining("栏目类型非法");
}
@Test
@DisplayName("轮播 linkType=9 显式拒绝(不再误报\"跳转目标必须是数字 ID\")")
void bannerSave_illegalLinkType_rejected() {
BannerSaveRequest r = new BannerSaveRequest();
r.setSiteId(siteId);
r.setPosition("home");
r.setTitle("t");
r.setImageUrl("/upload/x.png");
r.setLinkType(9);
r.setLinkValue("https://example.cn");
assertThatThrownBy(() -> bannerService.save(TestUsers.superAdmin(), r))
.isInstanceOf(BizException.class)
.hasMessageContaining("跳转类型非法");
}
// ===== D-5 定时时间窗 =====
@Test
@DisplayName("保存:offlineTime 早于 publishTime 显式拒绝(状态机自洽)")
void articleSave_offlineBeforePublish_rejected() {
ArticleSaveRequest r = new ArticleSaveRequest();
r.setSiteId(siteId);
r.setCategoryId(categoryId);
r.setTitle("时间矛盾");
r.setPublishTime(LocalDateTime.of(2026, 12, 1, 0, 0));
r.setOfflineTime(LocalDateTime.of(2026, 11, 1, 0, 0));
assertThatThrownBy(() -> articleService.save(TestUsers.superAdmin(), r))
.isInstanceOf(BizException.class)
.hasMessageContaining("定时下架时间必须晚于发布时间");
}
@Test
@DisplayName("发布:过期 offlineTime 自愈清空(重发=新生命周期,不再被调度器立即下架)")
void publish_staleOfflineTime_cleared() {
Integer id = articleService.save(TestUsers.superAdmin(), draftReq());
// 直接落库一个矛盾历史数据(模拟脏数据):offlineTime 已过
articleRepository.findById(id).ifPresent(a -> {
a.setOfflineTime(LocalDateTime.now().minusDays(1));
articleRepository.saveAndFlush(a);
});
articleService.publish(TestUsers.superAdmin(), siteId, id);
CmsArticle after = articleRepository.findById(id).orElseThrow(IllegalStateException::new);
assertThat(after.getStatus()).isEqualTo(ArticleStatusEnum.PUBLISHED.getCode());
assertThat(after.getOfflineTime()).isNull();
}
private ArticleSaveRequest draftReq() {
ArticleSaveRequest r = new ArticleSaveRequest();
r.setSiteId(siteId);
r.setCategoryId(categoryId);
r.setTitle("普通草稿");
r.setContent("<p>正文</p>");
return r;
}
// ===== D-6 删除时轮播引用拦截 =====
@Test
@DisplayName("删除文章:被轮播引用时拦截并提示")
void articleDelete_blockedByBannerReference() {
Integer articleId = articleService.save(TestUsers.superAdmin(), draftReq());
persistBannerRef(1, String.valueOf(articleId));
DeleteRequest req = new DeleteRequest();
req.setSiteId(siteId);
req.setId(articleId);
assertThatThrownBy(() -> articleService.delete(TestUsers.superAdmin(), req))
.isInstanceOf(BizException.class)
.hasMessageContaining("轮播");
}
@Test
@DisplayName("删除栏目:被轮播引用时拦截并提示(与子栏目/文章拦截同一规则)")
void categoryDelete_blockedByBannerReference() {
persistBannerRef(2, String.valueOf(categoryId));
DeleteRequest req = new DeleteRequest();
req.setSiteId(siteId);
req.setId(categoryId);
assertThatThrownBy(() -> categoryService.delete(TestUsers.superAdmin(), req))
.isInstanceOf(BizException.class)
.hasMessageContaining("轮播");
}
@Test
@DisplayName("删除文章:无轮播引用时正常删除(不误拦)")
void articleDelete_withoutReference_succeeds() {
Integer articleId = articleService.save(TestUsers.superAdmin(), draftReq());
DeleteRequest req = new DeleteRequest();
req.setSiteId(siteId);
req.setId(articleId);
articleService.delete(TestUsers.superAdmin(), req);
assertThat(articleRepository.findById(articleId)).isEmpty();
}
private void persistBannerRef(int linkType, String linkValue) {
CmsBanner b = new CmsBanner();
b.setSiteId(siteId);
b.setPosition("home");
b.setTitle("引用型轮播");
b.setImageUrl("/upload/banner.png");
b.setLinkType(linkType);
b.setLinkValue(linkValue);
b.setSortNo(1);
b.setStatus(1);
bannerRepository.saveAndFlush(b);
}
}