From f5d44100635c093de3f22b4f74f13f6e21e595df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com>
Date: Sat, 5 Sep 2026 00:18:16 +0800
Subject: [PATCH] =?UTF-8?q?task-211:=20ArchUnit=20Controller=20=E8=BE=B9?=
=?UTF-8?q?=E7=95=8C=E8=A7=84=E5=88=99=EF=BC=88=E4=B8=8D=E7=9B=B4=E6=8E=A5?=
=?UTF-8?q?=E8=AE=BF=E9=97=AE=20Mapper/=E5=A4=96=E9=83=A8=20Client?=
=?UTF-8?q?=EF=BC=8C=E9=9B=B6=E8=BF=9D=E8=A7=84=EF=BC=89+=20task=E2=86=92?=
=?UTF-8?q?=E4=B8=9A=E5=8A=A1=20=E4=BE=9D=E8=B5=96=E9=98=B2=E6=81=B6?=
=?UTF-8?q?=E5=8C=96=E5=9F=BA=E7=BA=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- archunit-junit5(test) 依赖引入一次到位
- Controller 不访问 ..mapper.. / ..client.. 两条规则全绿(探针确认存量零违规)
- task 模块经 *ResultFileJobHandler 等依赖业务 128 处(存量)→ 冻结上限防增长;module 切片存在 collectdata→dedupe→task 环,清零需 Handler 迁移等重构,另行处理
---
backend-java/pom.xml | 6 +
.../aiimage/ArchitectureBoundaryTest.java | 108 ++++++++++++++++++
2 files changed, 114 insertions(+)
create mode 100644 backend-java/src/test/java/com/nanri/aiimage/ArchitectureBoundaryTest.java
diff --git a/backend-java/pom.xml b/backend-java/pom.xml
index fb3ebbc7..f897b13e 100644
--- a/backend-java/pom.xml
+++ b/backend-java/pom.xml
@@ -150,6 +150,12 @@
spring-boot-starter-test
test
+
+ com.tngtech.archunit
+ archunit-junit5
+ 1.3.0
+ test
+
diff --git a/backend-java/src/test/java/com/nanri/aiimage/ArchitectureBoundaryTest.java b/backend-java/src/test/java/com/nanri/aiimage/ArchitectureBoundaryTest.java
new file mode 100644
index 00000000..423ef498
--- /dev/null
+++ b/backend-java/src/test/java/com/nanri/aiimage/ArchitectureBoundaryTest.java
@@ -0,0 +1,108 @@
+package com.nanri.aiimage;
+
+import com.tngtech.archunit.core.domain.JavaClasses;
+import com.tngtech.archunit.core.importer.ClassFileImporter;
+import com.tngtech.archunit.core.importer.ImportOption;
+import org.junit.jupiter.api.Test;
+
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * task-210/211/212:模块边界 ArchUnit 规则(plan 13)。
+ *
+ * 已通过(零违规):任何 Controller 不得直接访问 Mapper 或外部 Client(211)。
+ * 债务防恶化(210/212,探针 2026-09-05):task 模块经 *ResultFileJobHandler 等依赖具体业务
+ * Service/Mapper 共 128 处、modules 切片存在 collectdata→dedupe→task 环;清零需把 Handler 实现
+ * 迁回业务模块等大重构。本文件冻结边界(不允许新增违规;存量≤128),消除债务留待专门重构。
+ */
+class ArchitectureBoundaryTest {
+
+ private static final int TASK_TO_BUSINESS_BASELINE = 128;
+
+ private static volatile JavaClasses cached;
+
+ private static JavaClasses app() {
+ if (cached == null) {
+ cached = new ClassFileImporter()
+ .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
+ .importPackages("com.nanri.aiimage.modules");
+ }
+ return cached;
+ }
+
+ @Test
+ void controllersDoNotAccessMapper() {
+ noClasses().that().resideInAPackage("..controller..")
+ .should().dependOnClassesThat().resideInAPackage("..mapper..")
+ .because("Controller 只经 Service 访问 Mapper(单向分层)")
+ .check(app());
+ }
+
+ @Test
+ void controllersDoNotDependOnExternalClient() {
+ noClasses().that().resideInAPackage("..controller..")
+ .should().dependOnClassesThat().resideInAPackage("..client..")
+ .because("外部 Client 细节由 Service 层收敛,Controller 不应感知")
+ .check(app());
+ }
+
+ @Test
+ void boundaryRulesCoverAllControllers() {
+ long controllers = app().stream()
+ .filter(c -> c.getPackageName().endsWith(".controller")).count();
+ assertTrue(controllers >= 10, "应覆盖全部 Controller,实际 " + controllers);
+ }
+
+ @Test
+ void taskToBusinessDependencyDoesNotGrow() {
+ // 冻结存量(探针 128):不允许 task→业务 依赖继续增加
+ int violations = countViolations(taskRule());
+ assertTrue(violations >= 1, "存在 task→业务 依赖(Handler SPI 承接点)");
+ assertTrue(violations <= TASK_TO_BUSINESS_BASELINE,
+ "task→业务 依赖不得超过基线 " + TASK_TO_BUSINESS_BASELINE + ",当前 " + violations
+ + ";新增依赖走 Handler SPI 或先消除存量");
+ }
+
+ @Test
+ void ruleFailuresCarryClearReason() {
+ // 规则附 because 说明,失败信息可定位(构造性校验:规则对象能生成可读描述)
+ assertTrue(taskRule().getDescription().contains("no classes that reside in a package"),
+ "规则描述应可读");
+ }
+
+ @Test
+ void spiHandlersAreTheSanctionedBridge() {
+ // 允许方向:task 模块仅 *ResultFileJobHandler(04 SPI 实现)可依赖业务 Service;
+ // 其余 task 类若依赖业务即属增长(由 taskToBusinessDependencyDoesNotGrow 守卫上限)。
+ JavaClasses classes = app();
+ long handlers = classes.stream()
+ .filter(c -> c.getSimpleName().endsWith("ResultFileJobHandler"))
+ .count();
+ assertTrue(handlers >= 10, "Handler SPI 实现应在 task 模块注册,实际 " + handlers);
+ }
+
+ private static com.tngtech.archunit.lang.ArchRule taskRule() {
+ return noClasses().that().resideInAPackage("..modules.task..")
+ .should().dependOnClassesThat().resideInAPackage(
+ "..modules.(similarasin|appearancepatent|deletebrand|publish|brand|collectdata|"
+ + "shopdatacrawl|imagevideo|shopmatch|productrisk|queryasin|withdraw|patroldelete)..")
+ .because("公共 task 模块不应依赖具体业务模块");
+ }
+
+ private static int countViolations(com.tngtech.archunit.lang.ArchRule rule) {
+ try {
+ rule.check(app());
+ return 0;
+ } catch (AssertionError e) {
+ String msg = e.getMessage();
+ long count = msg.lines()
+ .filter(l -> l.trim().startsWith("Constructor <")
+ || l.trim().startsWith("Field <")
+ || l.trim().startsWith("Method <")
+ || l.trim().startsWith("Class <"))
+ .count();
+ return (int) count;
+ }
+ }
+}