本文主题:解释模块为什么能互相使用,以及 Maven、Spring 和公共接口分别承担什么职责
适合读者:刚接触 Maven 多模块、Spring 依赖注入和模块化单体架构的 Java 开发者
代码基线:当前学习分支源码快照
上一篇:从零读懂 AI 智能客服后端架构:模块职责与 SSE 聊天链路下一篇:AI 智能客服为什么不能照搬传统三层架构:CRUD 与 AI 编排分层对比
🐟 这里是yurenpai
27届开发者,主要学习 Java 后端与 AI 应用开发。
这里记录真实项目中的代码调用链、Agent/RAG 工程化、问题排查和开发复盘。
个人理念:
模块之间能否协作,不能只看目录位置,必须同时追踪构建依赖和运行时装配。
写在前面
先说结论:两个模块放在同一个仓库里,并不代表它们可以直接互相调用。真正决定模块协作的是三层机制:Maven 负责建立编译依赖,Spring 负责发现并注入运行时 Bean,公共接口负责控制依赖方向、避免业务模块形成循环依赖。
本文会以"聊天模块调用工作流、工作流反向使用聊天能力"为贯穿案例,重点讲清:
- 父子关系、聚合关系和依赖关系有什么区别;
admin为什么能够把多个业务模块装进同一个应用;- 接口放在公共模块中,为什么能避免
chat ↔ aiflow的循环依赖; - 如何用 POM、
import、接口实现和启动模块反向验证一次跨模块调用。
说明:本文以当前项目源码为例,重点解释模块协作方法;不同项目的模块名称可能不同,但分析步骤可以复用。
代码说明:除明确标注为完整源码外,文中的 POM、Java 代码和目录片段均为根据当前源码整理的简化示意;文中的"已核对"表示完成静态源码核对,不等同于启动或接口测试通过。
一、为什么要拆成这么多模块
假设不拆模块,把所有代码都放在一个项目中:
text
src/main/java/com/tst/pharma/
├─ 用户管理
├─ 权限管理
├─ 聊天
├─ 知识库
├─ 工作流
├─ AI 流程
├─ Redis
├─ SSE
├─ 短信
├─ 文件上传
├─ Excel
└─ 定时任务
时间长了会出现几个问题:
text
1. 所有代码混在一起,不知道属于哪个业务;
2. 一个聊天模块也能随便依赖用户、短信、工作流内部实现;
3. 公共工具被复制多份;
4. 修改一个模块容易影响其他模块;
5. Maven 依赖越来越混乱;
6. 新人很难判断代码应该放在哪里;
7. 无法控制模块之间的依赖方向。
所以项目进行了两次拆分。
第一次:按大职责拆分
text
tst-pharma-admin
├─ 应用启动和组装
tst-pharma-common
├─ 公共基础能力
tst-pharma-modules
├─ 具体业务模块
tst-pharma-extend
└─ 独立扩展服务
第二次:在每个大模块内部继续拆分
例如公共能力拆成:
text
tst-pharma-common-core
tst-pharma-common-web
tst-pharma-common-redis
tst-pharma-common-sse
tst-pharma-common-mybatis
tst-pharma-common-satoken
业务能力拆成:
text
tst-pharma-system
tst-pharma-chat
tst-pharma-workflow
tst-pharma-aiflow
tst-pharma-generator
这样就可以做到:
text
聊天模块需要 SSE
→ 只依赖 common-sse
聊天模块需要 Web 能力
→ 只依赖 common-web
聊天模块不需要短信
→ 就不必直接依赖 common-sms
这就是"模块化"的主要意义:
不是为了把目录弄多,而是为了划分职责、复用能力并控制依赖。
二、先区分三个容易混淆的概念
项目中有三种关系,一定要区分。
1. 父子关系
例如子模块的 pom.xml 中:
xml
<parent>
<groupId>com.tst.pharma</groupId>
<artifactId>tst-pharma-main-backend</artifactId>
<version>${revision}</version>
</parent>
它表示子模块继承父工程的:
text
版本号
依赖版本管理
Maven 插件
Java 版本
构建配置
但是:
继承父 POM,不代表父模块能够直接使用子模块的 Java 类。
2. 聚合关系
根 pom.xml 中:
xml
<modules>
<module>tst-pharma-admin</module>
<module>tst-pharma-common</module>
<module>tst-pharma-extend</module>
<module>tst-pharma-modules</module>
</modules>
它表示:
text
在根目录执行 Maven 构建时
→ Maven 会一起构建这些模块
但是:
被根 POM 聚合,也不等于模块之间能够互相调用。
例如:
text
tst-pharma-chat
tst-pharma-workflow
虽然都被根项目聚合,但如果 chat/pom.xml 没有依赖 workflow,那么 chat 不能直接随意导入 workflow 的类。
3. 依赖关系
真正决定一个模块能否使用另一个模块 Java 类的是:
xml
<dependency>
<groupId>com.tst.pharma</groupId>
<artifactId>被依赖模块</artifactId>
</dependency>
例如 tst-pharma-chat/pom.xml 中有:
xml
<dependency>
<groupId>com.tst.pharma</groupId>
<artifactId>tst-pharma-common-chat</artifactId>
</dependency>
<dependency>
<groupId>com.tst.pharma</groupId>
<artifactId>tst-pharma-common-sse</artifactId>
</dependency>
所以聊天模块才能使用:
java
import com.tst.pharma.common.chat.domain.dto.request.ChatRequest;
import com.tst.pharma.common.sse.core.SseEmitterManager;
可以记成:
text
聚合关系
└─ 决定一起构建
父子关系
└─ 决定继承统一配置
依赖关系
└─ 决定是否能使用另一个模块的代码
小鱼点睛
父子关系统一配置,聚合关系决定一起构建,依赖关系决定能否使用对方的 Java 类。两个目录即使紧挨在一起,也不会自动产生代码依赖。
三、项目真实的模块依赖方向
整个项目可以简化成:
text
tst-pharma-admin
主应用启动和组装
│
┌───────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
tst-pharma-system tst-pharma-chat tst-pharma-workflow
系统管理业务 AI聊天与知识库 传统业务流程
│
▼
tst-pharma-aiflow
AI流程编排
上述业务模块继续依赖:
tst-pharma-common-*
公共基础组件
更准确地说,admin 直接依赖:
text
tst-pharma-system
tst-pharma-generator
tst-pharma-chat
tst-pharma-workflow
tst-pharma-aiflow
因此最终启动 admin 时,这些业务模块都会进入主应用。
四、admin 是怎么把所有业务模块装进来的
文件:
text
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-admin\pom.xml
它声明了:
text
admin
├─ 依赖 system
├─ 依赖 generator
├─ 依赖 chat
├─ 依赖 workflow
└─ 依赖 aiflow
构建主应用时,Maven 会把这些模块的编译产物和依赖一起放进最终应用。
可以将最终运行的应用理解成:
text
tst-pharma-admin.jar
├─ admin 自己的类
├─ system 模块的类
├─ chat 模块的类
├─ workflow 模块的类
├─ aiflow 模块的类
├─ generator 模块的类
└─ 它们所依赖的 common 类
因此运行时主要是:
text
一个 JVM 进程
一个 Spring 容器
一个后端端口 6039
而不是:
text
system 启一个端口
chat 启一个端口
workflow 启一个端口
aiflow 再启一个端口
所以当前主体架构属于:
模块化单体应用,不是微服务架构。
五、公共模块是怎么被业务模块使用的
例子一:聊天模块使用 SSE
ChatServiceFacade 中:
java
private final SseEmitterManager sseEmitterManager;
SseEmitterManager 不在聊天模块,而在:
text
tst-pharma-common-sse
聊天模块的 pom.xml 声明了:
xml
<dependency>
<groupId>com.tst.pharma</groupId>
<artifactId>tst-pharma-common-sse</artifactId>
</dependency>
完整关系是:
text
tst-pharma-chat
│
│ Maven 依赖
▼
tst-pharma-common-sse
│
├─ SseEmitterManager
├─ SseMessageUtils
├─ SseEventDto
└─ SSE 自动配置
于是聊天模块才能:
text
建立 SSE
发送 content
发送 done
发送 error
关闭 SSE
例子二:聊天模块使用登录认证
聊天代码中使用:
text
LoginHelper.getUserId();
StpUtil.getTokenValue();
这涉及:
text
common-satoken
common-redis
Sa-Token
依赖链不一定都由聊天模块直接声明,也可以通过依赖传递获得。
简化理解:
text
tst-pharma-chat
↓
common-chat / common-sse / common-web
↓
common-satoken
↓
common-redis
↓
Redis
所以一个模块不一定需要把所有底层依赖都重新声明一遍。
例子三:系统模块使用 MyBatis
SysConfigMapper:
java
public interface SysConfigMapper
extends BaseMapperPlus<SysConfig, SysConfigVo> {
}
其中 BaseMapperPlus 位于:
text
tst-pharma-common-mybatis
关系为:
text
tst-pharma-system
│
▼
tst-pharma-common-mybatis
│
▼
MyBatis-Plus
│
▼
MySQL
因此 system 模块不需要自己再实现一套分页、Mapper 基类和数据库配置。
六、模块之间不仅靠 Maven,还靠 Spring 连接
Maven 解决的是:
text
编译时能不能看到另一个模块的类
Spring 解决的是:
text
程序启动后,具体使用哪个实现对象
启动类:
text
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-admin\src\main\java\com\tst\pharma\TstPharmaApplication.java
位于根包:
java
package com.tst.pharma;
并且有:
java
@SpringBootApplication
Spring 默认会从启动类所在包向下扫描:
text
com.tst.pharma
├─ controller
├─ service
├─ config
├─ factory
├─ workflow
└─ ...
即使类分别位于不同 Maven 模块中,只要最后都被 admin 引入,并且包名属于:
text
com.tst.pharma...
Spring 就能发现它们。
例如:
text
chat 模块
└─ ChatController
└─ @Controller
chat 模块
└─ ChatServiceFacade
└─ @Service
aiflow 模块
└─ WorkflowStarter
└─ Spring Bean
common-sse 模块
└─ SSE 自动配置
└─ 注册 SseEmitterManager
它们最后都在同一个 Spring 容器中。
七、Spring 依赖注入是怎么跨模块工作的
例如 ChatController 中:
java
private final ChatServiceFacade chatService;
因为类上有:
java
@RequiredArgsConstructor
Lombok 会生成类似构造器:
java
public ChatController(ChatServiceFacade chatService) {
this.chatService = chatService;
}
Spring 启动时发现:
text
ChatController 需要 ChatServiceFacade
然后又发现:
java
@Service
public class ChatServiceFacade {
}
于是完成注入:
text
Spring 容器
├─ 创建 ChatServiceFacade
├─ 创建 ChatController
└─ 把 ChatServiceFacade 放进 ChatController
虽然它们处在不同目录甚至不同 Maven 模块,也不影响运行时注入,只要满足:
text
1. admin 的 Maven 依赖把两个模块装进来了;
2. Spring 扫描到了对应类;
3. 类注册成了 Bean;
4. 依赖类型能够匹配。
小鱼点睛
Maven 只解决"编译时能不能看见接口和类",真正把接口字段连接到实现对象的是 Spring 容器。只有编译依赖、Bean 扫描和类型匹配同时成立,跨模块注入才会成功。
八、项目中最典型的跨模块设计:common-chat 接口桥梁
这一部分很重要,因为它解释了:
chat和aiflow为什么可以互相协作,但没有在 POM 中直接互相依赖。
先看结构:
text
tst-pharma-common-chat
├─ IChatService
├─ IChatModelService
└─ IWorkFlowStarterService
它们只是接口和公共协议,不包含具体业务实现。
聊天服务接口
位于:
text
tst-pharma-common-chat
└─ IChatService
具体实现位于聊天模块:
java
@Service
public class ChatServiceFacade implements IChatService {
}
关系:
text
common-chat
└─ 定义 IChatService
chat
└─ ChatServiceFacade 实现 IChatService
工作流启动接口
位于:
text
tst-pharma-common-chat
└─ IWorkFlowStarterService
具体实现位于 AI 流程模块:
java
public class WorkflowStarter
implements IWorkFlowStarterService {
}
关系:
text
common-chat
└─ 定义 IWorkFlowStarterService
aiflow
└─ WorkflowStarter 实现 IWorkFlowStarterService
chat 调用 aiflow
ChatServiceFacade 中不是直接依赖某个 aiflow 内部类,而是:
java
private final IWorkFlowStarterService workFlowStarterService;
流程是:
text
ChatServiceFacade
│
│ 只认识公共接口
▼
IWorkFlowStarterService
▲
│ 由 Spring 在运行时寻找实现
│
WorkflowStarter
这样 chat 模块在编译时不需要直接依赖 aiflow。
aiflow 调用 chat
aiflow 的 WorkflowUtil 中使用:
java
private IChatService chatService;
private IChatModelService chatModelService;
流程:
text
WorkflowUtil
│
│ 只依赖公共接口
▼
IChatService
▲
│ Spring 注入实现
│
ChatServiceFacade
所以整体是:
text
common-chat
公共接口和公共请求对象
▲ ▲
│ │
chat aiflow
实现聊天接口 实现流程接口
这叫:
通过公共契约解耦业务模块。
小鱼点睛把接口下沉到双方都能依赖的公共模块,目的不只是"集中存放公共类",而是反转依赖方向:业务模块面向稳定契约协作,避免
chat和aiflow在 POM 中相互咬住。
九、为什么接口要放在 common-chat,而不是直接放在 chat
如果 IChatService 放在:
text
tst-pharma-chat
那么 aiflow 为了调用它,就必须:
text
aiflow → 依赖 chat
同时聊天模块为了启动 AI 工作流,可能又需要:
text
chat → 依赖 aiflow
最后形成:
text
chat → aiflow
↑ ↓
└───────┘
这就是 Maven 循环依赖。
Maven 无法正常处理这样的模块关系。
当前项目把接口放进公共模块后,变成:
text
chat ──────→ common-chat
aiflow ────→ common-chat
然后由 admin 同时引入:
text
admin
├─ chat
└─ aiflow
运行时再由 Spring 把实现连接起来。
这个设计可以理解为插座:
text
common-chat
└─ 规定插座形状
chat
└─ 提供一种插头
aiflow
└─ 提供或使用另一种插头
Spring
└─ 启动时把兼容的插头插到插座上
十、admin 是最终的组装者
这个项目中,真正知道"我要同时加载哪些业务模块"的是:
text
tst-pharma-admin
它的 pom.xml 相当于组装清单:
text
主应用需要:
├─ system
├─ generator
├─ chat
├─ workflow
└─ aiflow
因此可以这样理解:
text
common
└─ 制造公共零件
modules
├─ 制造用户系统
├─ 制造聊天系统
├─ 制造业务工作流
└─ 制造 AI 工作流
admin
└─ 把所有零件和业务模块装成完整应用
十一、数据库 Mapper 又是怎么跨模块加载的
配置位于:
text
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-admin\src\main\resources\application.yml
其中配置了:
yaml
mybatis-plus:
mapperPackage: com.tst.pharma.**.mapper
mapperLocations: classpath*:mapper/**/*Mapper.xml
typeAliasesPackage: com.tst.pharma.**.domain
MyBatis 配置类使用:
java
@MapperScan("${mybatis-plus.mapperPackage}")
意思是扫描所有模块中符合下面规则的 Mapper:
text
com.tst.pharma.任意内容.mapper
例如:
text
system 模块
└─ com.tst.pharma.system.mapper.SysConfigMapper
chat 模块
└─ com.tst.pharma.mapper.ChatMessageMapper
workflow 模块
└─ 对应的 mapper
classpath*: 也表示:
不只查当前模块,还会查依赖 JAR 中的 Mapper XML。
所以启动一个 admin,能够把多个业务模块中的 Mapper 全部注册到同一个 MyBatis/Spring 容器。
十二、配置文件如何影响所有模块
主要运行配置集中在:
text
tst-pharma-admin\src\main\resources\application.yml
tst-pharma-admin\src\main\resources\application-dev.yml
这里配置:
text
端口
数据库
Redis
MyBatis
Sa-Token
多租户
日志
SSE
上传目录
虽然配置文件在 admin,但 Spring 启动后创建的是一个统一环境。
因此:
text
system 模块可以使用数据源
chat 模块可以使用数据源
workflow 模块可以使用 Redis
common-sse 可以读取 SSE 配置
common-satoken 可以读取认证配置
它们不需要各自维护一套 application.yml。
可以理解成:
text
admin application.yml
│
▼
Spring Environment
├─ system 使用
├─ chat 使用
├─ workflow 使用
├─ aiflow 使用
└─ common 组件使用
十三、数据库和 Redis 也是模块之间的公共基础设施
当前主要业务模块运行在同一个应用中,并共享:
text
MySQL
Redis
登录状态
租户上下文
事务管理器
SSE 连接管理
例如一次聊天请求可能发生:
text
chat 模块
├─ 从 chat_model 表查询模型配置
├─ 从 chat_message 表查询历史消息
├─ 从 knowledge_info 表查询知识库配置
├─ 使用 Redis/登录上下文获取认证信息
└─ 使用 SSE 向当前用户发送结果
但要注意:
共享同一个数据库,不代表模块应该随意直接修改其他模块的表。
正常情况下应优先:
text
模块 A
→ 调用公共接口
→ 模块 B 的 Service
→ 模块 B 的 Mapper
→ 模块 B 负责自己的表
而不是:
text
模块 A
→ 直接拿模块 B 的 Mapper
→ 随意修改模块 B 的表
否则模块边界会再次被破坏。
十四、用聊天请求看模块之间如何真正协作
一次:
http
POST /chat/send
背后会经过很多模块。
text
客户端请求
│
▼
common-web / common-satoken
├─ Web 请求处理
├─ 登录认证
└─ 用户上下文
│
▼
tst-pharma-chat
├─ ChatController
├─ ChatServiceFacade
├─ 场景分类
└─ 模型调用
│
├──────────────┐
▼ ▼
common-sse common-chat
SSE连接和事件 公共请求对象和接口
│ │
│ ├─────────────┐
│ ▼ ▼
│ aiflow chat实现
│ AI流程启动 模型聊天实现
│
▼
浏览器逐段接收回复
如果是普通聊天:
text
ChatController
→ ChatServiceFacade
→ ChatModelService
→ ChatServiceFactory
→ 模型供应商实现
→ LangChain4j
→ 外部模型接口
→ common-sse
→ 前端
如果是 AI 工作流:
text
ChatController
→ ChatServiceFacade
→ IWorkFlowStarterService
→ aiflow 中的 WorkflowStarter
→ AI 工作流节点执行
→ 需要模型时调用 IChatService
→ chat 中的 ChatServiceFacade
→ 模型
这就是多个模块之间的真实协作。
十五、extend 为什么没有被直接装进 admin
根 POM 聚合了:
text
tst-pharma-extend
所以执行根 Maven 构建时,会一起构建:
text
monitor-admin
snailjob-server
但是 admin/pom.xml 没有把它们作为主业务依赖引入。
这说明:
text
根 POM 聚合它们
≠ admin 运行时包含它们
它们更可能是独立启动的扩展服务:
text
主后端
└─ tst-pharma-admin
监控服务
└─ tst-pharma-monitor-admin
任务调度服务
└─ tst-pharma-snailjob-server
所以再次强调:
text
聚合
└─ 一起构建
依赖
└─ 编译和运行时使用
十六、当前项目的核心模块关系图
可以先保存这张图:
text
tst-pharma-main-backend
│
├─ tst-pharma-common
│ ├─ common-core
│ │ └─ 最底层公共对象、异常、工具
│ │
│ ├─ common-redis
│ │ └─ 依赖 common-core
│ │
│ ├─ common-satoken
│ │ └─ 依赖 common-core、common-redis
│ │
│ ├─ common-mybatis
│ │ └─ 依赖 common-core、common-satoken
│ │
│ ├─ common-sse
│ │ └─ 依赖 core、redis、satoken、json
│ │
│ ├─ common-web
│ │ └─ Web 公共能力
│ │
│ └─ common-chat
│ ├─ 依赖 core
│ ├─ 依赖 sse
│ ├─ 依赖 mybatis
│ └─ 定义聊天、模型、工作流公共接口
│
├─ tst-pharma-modules
│ ├─ system
│ │ └─ 依赖 MyBatis、Web、认证、租户等公共模块
│ │
│ ├─ chat
│ │ ├─ 依赖 common-chat
│ │ ├─ 依赖 common-sse
│ │ ├─ 依赖 common-web
│ │ └─ 实现 IChatService、IChatModelService
│ │
│ ├─ aiflow
│ │ ├─ 依赖 common-chat
│ │ ├─ 使用 IChatService
│ │ └─ 实现 IWorkFlowStarterService
│ │
│ ├─ workflow
│ │ └─ 依赖 MyBatis、Web、租户、认证等公共模块
│ │
│ └─ generator
│ └─ 依赖 MyBatis、Web、文档、日志等公共模块
│
├─ tst-pharma-admin
│ ├─ 引入 system
│ ├─ 引入 chat
│ ├─ 引入 workflow
│ ├─ 引入 aiflow
│ ├─ 引入 generator
│ └─ 启动一个完整 Spring Boot 应用
│
└─ tst-pharma-extend
├─ monitor-admin
└─ snailjob-server
十七、判断"两个模块怎么关联"的固定方法
以后看到两个模块,不要凭目录名猜,可以按下面五步确认。
第一步:看调用模块的 pom.xml
例如想知道 chat 能不能使用 common-sse:
text
打开 tst-pharma-chat/pom.xml
搜索 tst-pharma-common-sse
如果有依赖,说明编译时可见。
第二步:看 Java import
例如:
java
import com.tst.pharma.common.sse.core.SseEmitterManager;
说明代码确实使用了该模块的类。
第三步:看字段注入类型
例如:
java
private final IWorkFlowStarterService workFlowStarterService;
说明当前类依赖的是接口。
第四步:查谁实现接口
搜索:
text
implements IWorkFlowStarterService
找到:
text
aiflow → WorkflowStarter
第五步:确认最终启动模块是否同时引入双方
查看:
text
tst-pharma-admin/pom.xml
确认同时依赖:
text
chat
aiflow
这样 Spring 运行时才有机会把它们连接起来。
十八、核心结论
结论一
text
目录相邻
≠ 有依赖关系
真正的编译依赖看:
text
pom.xml 中的 dependency
结论二
text
根 POM 中有 module
≠ 该模块被装进主应用
它可能只是一起构建。
结论三
text
admin 是主应用组装者
它把:
text
system、chat、workflow、aiflow、generator
放进同一个 Spring Boot 应用。
结论四
text
common 提供公共能力和公共接口
modules 提供具体业务实现
结论五
模块之间推荐通过:
text
公共接口
Spring 依赖注入
进行协作,而不是互相直接依赖内部实现。
结论六
当前主体不是微服务,而是:
text
模块化单体
├─ 编译时分模块
├─ 代码职责分模块
└─ 运行时主要在同一个 Spring Boot 进程
最典型的真实关系就是:
text
ChatServiceFacade
│
│ 实现
▼
IChatService(common-chat)
▲
│ 使用
│
WorkflowUtil(aiflow)
以及反方向:
text
WorkflowStarter(aiflow)
│
│ 实现
▼
IWorkFlowStarterService(common-chat)
▲
│ 使用
│
ChatServiceFacade(chat)
它们由 admin 同时组装,再由 Spring 在运行时连接。这个例子基本涵盖了整个项目模块化设计的核心。
总结
本文围绕"解释模块为什么能互相使用,以及 Maven、Spring 和公共接口分别承担什么职责",主要分析了:
- 父子、聚合和依赖三种 Maven 关系的边界;
- 启动模块如何完成业务模块的最终组装;
- Spring 如何跨 JAR 扫描 Bean 并按接口注入实现;
- 公共接口桥梁如何控制依赖方向并避免循环依赖;
整个过程可以概括为:
text
根 POM 聚合 → 子模块声明依赖 → Java 使用公共接口 → Spring 扫描实现类 → admin 统一装配 → 运行时完成调用
Maven 解决"编译时能不能看见",Spring 解决"运行时由谁来实现",公共接口解决"依赖应该朝哪个方向"。三者缺一不可。
当前进度
OK 已经完成
已完成主要模块 POM 依赖方向的静态核对;
已通过接口定义、实现类和启动模块还原聊天模块与工作流模块的协作关系;
TODO 后续继续下一篇对比普通 CRUD 分层与 AI 对话编排分层;
继续结合真实请求理解模块协作在运行时如何落到方法调用;
小鱼点睛Maven 解决"编译时能不能看见",Spring 解决"运行时由谁来实现",公共接口解决"依赖应该朝哪个方向"。三者缺一不可。
下一篇
下一篇将继续分析"AI 智能客服为什么不能照搬传统三层架构:CRUD 与 AI 编排分层对比",把本文建立的结构认知继续落到具体代码和对象流上。
这篇文章是我在真实项目学习过程中的阶段性记录。不同项目的命名和目录可能不同,但判断职责边界、依赖方向和数据生命周期的方法可以复用。如果内容中还有遗漏,欢迎一起交流。
