后端架构与模块依赖
适用端:后端 | 类型:参考
后端是一个 Maven 多模块单体(不是微服务):enlev-server 是唯一启动模块,业务能力按域拆成 5 个 enlev-module-*,横切能力沉在 enlev-framework 的 14 个 starter 里。本页讲清依赖方向、请求生命周期与扩展位置。
前置阅读:仓库结构与改名约定。
它解决什么问题
(依赖关系实测自各模块 pom.xml 的 cn.enlev.ainb 依赖声明。)
三句话结论:
- 依赖是单向的:
agent → {ai, infra, bpm, system},没有任何模块反向依赖 agent。给 agent 加工具可以直接调别的模块的 service;反过来别的模块要用 agent 能力,只能通过接口或事件解耦。 - system 是地基:所有业务模块都依赖它(用户、租户、权限、字典)。
- infra 被 system 依赖,也依赖 system:
system的 pom 里有enlev-module-infra(文件、日志),infra不依赖任何业务模块——两者不构成环,是因为 system 只调 infra 的 api 包。
🔴 enlev-module-im 已被整体删除,不只是「注释出 reactor」
本手册曾有 10 处写着「它在根 pom.xml 第 19 行被注释掉、目录还在、不要顺手修好」。这个描述已过期,而且那条行号现在是误导——pom.xml:19 当前是 <module>enlev-module-ai</module>。
实测(2026-10-25,在 ainb/ 下):
| 检查 | 结果 |
|---|---|
ls -d enlev-module-* | 只剩 agent ai bpm infra system 5 个,没有 im |
grep -n "module-im" pom.xml | 0 命中,连注释掉的那行也没了 |
git show --numstat --format='' 12347efcd1 | grep -c '^enlev-module-im/' | 305(该提交共删 311 个文件) |
删除发生在 12347efcd1 refactor(系统):清除未用上的模块。上游的支付 / 商城 / CRM / ERP / 会员 / IoT / 公众号等模块同批清掉。
reactor 现在是 24 个模块 = 根 pom + enlev-dependencies + enlev-framework 聚合 pom + 它的 15 个子模块 + enlev-server + 5 个业务模块。两条独立口径都对得上:mvn package 输出 [24/24],find . -name pom.xml -not -path '*/target/*' | wc -l → 24。
所以 -pl enlev-module-im 必然失败,不是因为被注释掉,是因为不存在。别按上游 yudao 的模块清单猜这里有什么。
移动端仓里那 11 个 pages-{crm,erp,im,iot,mall,…} 分包编译进包但用户点不到,正是因为后端这些模块已删。
代码在哪
三层结构
| 层 | 目录 | 职责 | 能不能被业务模块改 |
|---|---|---|---|
| 启动层 | enlev-server | 主类、5 个 yaml、logback | 只改配置 |
| 业务层 | enlev-module-{system,infra,bpm,ai,agent} | Controller / Service / Mapper / DO | 主战场 |
| 框架层 | enlev-framework/enlev-spring-boot-starter-* | 横切能力,全部走 Spring Boot 自动配置 | 改这里等于改全项目,慎重 |
14 个 starter + 1 个公共包
| 构件 | 关键自动配置类 | 负责什么 |
|---|---|---|
enlev-common | —(非 starter) | pojo(CommonResult / PageResult / PageParam)、exception(ErrorCode / ServiceException)、enums、util、validation |
starter-web | EnlevWebAutoConfiguration、EnlevJacksonAutoConfiguration、EnlevSwaggerAutoConfiguration、EnlevApiLogAutoConfiguration、EnlevXssAutoConfiguration、EnlevApiEncryptAutoConfiguration、EnlevBannerAutoConfiguration | URL 前缀、CORS、全局异常、统一响应、JSON 序列化、knife4j 分组、XSS、接口加密 |
starter-security | EnlevSecurityAutoConfiguration、EnlevWebSecurityConfigurerAdapter、EnlevOperateLogConfiguration | Token 认证过滤器、@ss bean、@PreAuthorize 支撑、操作日志 |
starter-mybatis | EnlevMybatisAutoConfiguration、EnlevDataSourceAutoConfiguration、EnlevTranslateAutoConfiguration | BaseMapperX / LambdaQueryWrapperX、BaseDO、分页与乐观锁拦截器、字段自动填充、Easy-Trans 翻译 |
starter-redis | EnlevRedisAutoConfiguration、EnlevCacheAutoConfiguration | RedisTemplate 序列化、Spring Cache |
starter-biz-tenant | EnlevTenantAutoConfiguration | 多租户:TenantBaseDO、TenantLineInnerInterceptor、@TenantIgnore、租户上下文过滤器 |
starter-biz-data-permission | EnlevDataPermissionAutoConfiguration、EnlevDeptDataPermissionAutoConfiguration | 上游的数据权限(@DataPermission、按部门过滤)。⚠️ 本项目定制的授权决策引擎不在这里,在 enlev-module-system/.../framework/permission/ |
starter-protection | EnlevIdempotentConfiguration、EnlevLock4jConfiguration、EnlevRateLimiterConfiguration、EnlevApiSignatureAutoConfiguration | @Idempotent、@Lock4j、@RateLimiter、API 签名 |
starter-job | EnlevQuartzAutoConfiguration、EnlevAsyncAutoConfiguration | 定时任务用 Quartz(不是 XXL-Job)+ @Async 线程池 |
starter-mq | EnlevRedisMQProducer/ConsumerAutoConfiguration、EnlevRabbitMQAutoConfiguration | Redis Stream / RabbitMQ 消息 |
starter-websocket | EnlevWebSocketAutoConfiguration | WebSocket,sender-type: local 时不需要 MQ |
starter-excel | EnlevDictAutoConfiguration | EasyExcel(cn.idev.excel)导入导出 + 字典转换 |
starter-monitor | EnlevTracerAutoConfiguration、EnlevMetricsAutoConfiguration | SkyWalking 链路 + Actuator 指标 |
starter-biz-ip | —(纯工具) | IP / 地区库(Area) |
starter-test | —(无 imports) | 测试基类:BaseDbUnitTest(H2)、BaseMockitoUnitTest、BaseRedisUnitTest |
定制功能:自研代码不在 framework 层
enlev-framework 下没有 agent 或新权限体系的 starter。自研部分全在模块层:
enlev-module-system/.../framework/permission/——子包config / core / dataset / decision / rule / scan / subject / perf / runner,核心类GrantDecisionService,对应配置enlev.permission.grant-enabled/collapse-enabled(yaml 注释原文标为「权限体系(S1~S5 重构)」);enlev-module-agent——整模块自研;enlev.data-model.*——低代码建模规范,硬约束在SqlSafeGuard里。
核心机制
一个请求怎么从 URL 走到 SQL
过滤器顺序不是靠 @Order 随手写的,而是集中在 enlev-common 的 WebFilterOrderEnum(注释原文:「考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下」):
int CORS_FILTER = Integer.MIN_VALUE;
int TRACE_FILTER = CORS_FILTER + 1;
int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1;
int TENANT_CONTEXT_FILTER = -104; // 需要保证在 ApiAccessLogFilter 前面
int API_ACCESS_LOG_FILTER = -103;
int XSS_FILTER = -102;
int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器(-100)后面
int FLOWABLE_FILTER = -98;新增过滤器时必须在这个枚举里占一个位,不要写魔法数字。
/admin-api 前缀是包名决定的,不是注解写的
Controller 上只有 @RequestMapping("/system/post"),URL 里的 /admin-api 由 EnlevWebAutoConfiguration 通过 WebMvcRegistrations 在 HandlerMapping 实例化时统一加:
// WebProperties.java
private Api appApi = new Api("/app-api", "**.controller.app.**");
private Api adminApi = new Api("/admin-api", "**.controller.admin.**");
// EnlevWebAutoConfiguration#putPathPrefix
pathPrefixes.put(api.getPrefix(),
clazz -> clazz.isAnnotationPresent(RestController.class)
&& matcher.match(api.getController(), clazz.getPackage().getName()));匹配用的是 AntPathMatcher(".") 对包名做 **.controller.admin.** 匹配。推论:
- 把 Controller 直接放在
controller/下(不进admin/app子包)→ 不加前缀 → 前端按/admin-api/...调必然 404; - 管理后台接口进
controller/admin/,用户端接口进controller/app/,这是硬约定。
统一响应与异常
GlobalResponseBodyHandler + GlobalExceptionHandler(都在 starter-web)保证任何返回都是 CommonResult{code, data, msg}。AccessDeniedException → 403 没有该操作权限,ServiceException → 原样带出业务码与消息,其余 Exception 兜底 500。详见接口约定与 CRUD 骨架。
配置与开关
application.yaml 里 enlev.* 的主要开关(都能在不改代码的前提下改变行为,动之前先读它旁边的注释——本项目的 yaml 注释写得比文档细):
| 配置 | 默认 | 作用与后果 |
|---|---|---|
enlev.info.base-package | cn.enlev.ainb | 组件扫描与 @MapperScan 的根,改名时唯一需要动的地方 |
enlev.security.mock-enable | 三个 profile 均 false(476b4b2637 起) | 打开则 Bearer test{userId} 免登录,且 mock-secret 默认就是字面量 test。曾经 local/prod 都是 true,现已全关;代码路径未删,开关一开就复活。见环境准备与启动与坑位索引 A1 |
enlev.permission.grant-enabled | true | 关掉则 @ss.hasPermission 回落到「按权限串在 system_menu 里找菜单」,数据集动作(infra:form-design-run:*)只存在于 sys_resource,非超管会寸步难行 |
enlev.permission.collapse-enabled | true | 「资源身份 × 动作」塌缩读路径。🔴 必须与数据迁移同批切换:数据没迁就打开 = 静默判无权(全 200、无异常、按钮全消失) |
enlev.permission.subject.dept-enabled | true | 部门作为授权主体。关掉则流程设计器里配的「某部门可发起」静默失效 |
enlev.data-model.table-prefix-enforced | true | 建模规范(命名/前缀/注释/配额)只作用于新建路径;importTables 导入存量表一律放行。硬约束(保留字、标识符白名单、框架表黑名单)在 SqlSafeGuard 里,不可关 |
enlev.tenant.enable | true | 多租户总开关 |
enlev.api-encrypt.enable | false | 请求/响应 AES 加密,按请求头 opt-in(前端 headers.isEncrypt) |
enlev.xss.enable | false | XSS 过滤 |
enlev.websocket.enable | true | sender-type: local,不需要 MQ |
enlev.agent.webhook.rate-limit-* | 开 | agent 模块唯一匿名面的限流:IP 维度 60/60s AND path 维度 300/60s(注释说明 IP 取自客户端可控头,真正承重的是 path 维度) |
enlev.agent.sanitize.*-max-length | 五档 | 不可信输入进 state 前按污点源分档截断(chat 4000 / webhook body 32768 / form-event 8192 / retrieval 4000 / tool-result 16384),max-depth: 16 防 StackOverflowError |
接口清单
按域统计(由 tools/gen-api.mjs 扫描 Controller 注解生成,勿手抄):
| 域 | Controller 数 | 接口数 |
|---|---|---|
| system | 45 | 269 |
| infra(含低代码) | 23 | 219 |
| bpm | 15 | 82 |
| ai | 13 | 101 |
| agent(自研) | 15 | 101 |
| 合计 | 111 | 772 |
这张表会随后端提交漂移,别把它当常量引用
本页上一版记的是 110 / 761。三处差额都能点名到具体提交与端点,说明差异来自后端新增、 不是生成脚本口径变了:
| 域 | 变化 | 来源 |
|---|---|---|
| system | 44 → 45 Controller、261 → 269 端点 | ★ NavNodeController(/system/nav-node,8 个端点),bf82d570b2「新增分端导航树 system_nav_node」 |
| infra | 218 → 219 端点 | POST /infra/form-design/bind-app-nav,cb41f251c5「表单设计支持发布到 App 端」 |
| agent | 99 → 101 端点 | GET /agent/diagnostic/doc-sync-status、GET /agent/diagnostic/doc-sync-verify,1b1ac7b1e9「AI 助手内置知识库改成可溯源」 |
要引用具体数字时,以 docs/.generated/api-index.md 的当次生成结果为准(跑 node tools/gen-api.mjs 刷新),本表只作量级参考。
这张表曾经整列偏小,原因是脚本丢了多行注解
gen-api.mjs 原来在遇到不以 @ 开头的续行时会清空缓冲,把上面已经攒到的 @PostMapping 一起丢掉:
@Operation(summary = "…",
description = "…") // ← 这一行不以 @ 开头
@PostMapping("/xxx")全站因此漏掉 4 个 Controller / 81 个端点,其中包括匿名可达的 POST /agent/webhook/{path} ——攻击面最大的那个端点恰好不在清单里。已改用「括号是否配平」判据修复。
⚠️ 由此得到一条通用口径:数 Controller 与端点不要用裸 grep。 grep -rl "@RestController" 会把只在注释里提到该注解的 PermissionAnnotationScanner 也算进去 (system 域因此多数 1 个:裸 grep 46,真实 45);grep -o '@GetMapping' 不加行首锚定会命中 注释掉的代码(infra 域因此多数 4 个端点:裸 grep 223,真实 219,多的是 DataModelDataController.java:72,81,93,104 四行被 // 注掉的 mapping)。 详见 system 域开发 · 接口清单。
完整清单在文档工程的 docs/.generated/ 下(api-index.md + api-{system,infra,bpm,ai,agent}.md)——本手册各域章节的「接口清单」小节直接引用它们,不手抄。
怎么扩展
给现有模块加一个功能(最常见):照接口约定与 CRUD 骨架的五件套走,或者直接用代码生成器(infra/codegen 菜单)生成后改。
新增一个业务模块 enlev-module-xxx:
- 建目录,
pom.xml的<parent>指向enlev,artifactId=enlev-module-xxx,依赖enlev-module-system+ 需要的 starter(照抄enlev-module-ai/pom.xml最省事); - 根
pom.xml的<modules>加一行;enlev-server/pom.xml加依赖; - 包结构按
cn.enlev.ainb.module.xxx.{api,controller,dal,enums,framework,service}建,Controller 必须落在controller/admin/或controller/app/下(否则没有 URL 前缀); - 若要独立 Swagger 分组,写一个
XxxWebConfiguration:
@Configuration(proxyBeanMethods = false)
public class AgentWebConfiguration {
@Bean
public GroupedOpenApi agentGroupedOpenApi() {
return EnlevSwaggerAutoConfiguration.buildGroupedOpenApi("agent");
}
}- 错误码在
enums/ErrorCodeConstants.java里按ServiceErrorCodeRange申请的区间写,别撞号。
新增一个 starter:在 enlev-framework/ 下建 enlev-spring-boot-starter-xxx,自动配置类用 @AutoConfiguration,并在 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 里登记类全名——本项目全部 starter 都是这个套路(Spring Boot 3 已不用 spring.factories)。
坑与注意
- agent 模块没有 Swagger 分组:缺
AgentWebConfiguration(实测四个*WebConfiguration只有 ai / bpm / infra / system),101 个接口只能在all组看到(实测/v3/api-docs/swagger-config只返回 ai/all/bpm/infra/system)。 - agent 模块在 pom 里本地钉住了
spring-ai-bom版本,用来覆盖spring-ai-alibaba-graph-core的传递依赖——是刻意为之,不要「修正」。 - agent 模块没有
src/test/resources/application-unit-test.yaml:它的 161 个测试文件 / 2225 个@Test绝大多数是纯 Mockito。要在那里加 DB 测试,得先补这份配置(BaseDbUnitTest依赖 profileunit-test与/sql/clean.sql)。 - ai 模块连
src/test/resources/目录都不存在:43 个测试文件 / 187 个@Test里有 86 个@Disabled(全部不带原因,分布在 32 个文件:29 个*Tests.java占 81 个,另 5 个在 3 个厂商客户端*Test.java),是全仓测试网最薄的模块,而它管着所有厂商模型调用。详见 ai 域开发 · 测试。🔴 数这个 86 要加行首锚定:grep -ro '@Disabled'会多数 2 个——framework/ai/core/VendorApiKeys.java:9,23的 javadoc 里用{@code @Disabled}提到了它。文件数同理会多数 1 个(33 vs 32),多的就是VendorApiKeys.java自己。 - 测试 SQL 只在 3 个模块里有:
enlev-module-{bpm,infra,system}/src/test/resources/sql/{clean.sql,create_tables.sql}(H2 用),ai / agent 没有。(旧版本这里写的是 4 个、含enlev-module-im——那个模块已随12347efcd1整体删除,目录都不在了。) @author 芋道源码标签在很多文件里保留着,属于归属声明,不要批量替换成自己的名字。- 不要把业务逻辑写进 starter:framework 层的类被所有模块共享,改一处影响全站;业务定制请放模块内。
- 依赖是单向的,别为了让 system 用 agent 的能力而加反向依赖——会立刻造成 Maven 循环依赖编译失败。用接口下沉到
api包或走事件。
→ 本节是贴着上下文的提醒;按症状查的全域清单见坑位索引。
相关页面
- 接口约定与 CRUD 骨架:Controller / VO / Service / Mapper / DO 五件套;
- 数据库约定与表总览:
BaseDO、租户与逻辑删除、拦截器; - 后端架构总览(导读):模块依赖图的简版与阅读路线。
