模块化架构与统一分层.md 27 KB

模块化架构与统一分层

本文引用的文件

  • ai-server/pom.xml
  • CallRecordController.java
  • CallRecordService.java
  • CallRecordMapper.java
  • CallRecord.java
  • CaseRoutingDataSource.java
  • CaseContextHolder.java
  • CaseAccessGuard.java

目录

  1. 引言
  2. 项目结构
  3. 核心组件
  4. 架构总览
  5. 详细模块分析
  6. 依赖关系分析
  7. 性能考虑
  8. 故障排查指南
  9. 结论
  10. 附录:新增业务模块标准步骤

引言

ai-server 是一个以案件为中心、面向多数据域的后端服务。代码主体位于 ai-server/src/main/java/com/zsjz/ai,其中业务域按 module/* 划分,例如通话、转账、轨迹、人员、外部数据、治理、智能体、知识检索等;公共模型、配置、上下文和数据源路由放在 common/*。

本文聚焦以下目标:

  • 说明每个业务模块通常包含的 controller、service、mapper、entity 的职责边界。
  • 明确跨模块调用应通过 Service 接口或共享 DTO,避免直接依赖实现类。
  • 解释模块间耦合原则:禁止循环依赖、网关只感知 ai-server 整体、不感知内部模块。
  • 给出新增业务模块的标准步骤:创建 module、定义 entity、Mapper、Service、Controller,注册路由,并维护 SQL 脚本。
  • 补充代码风格、命名约定和可测试性建议。

项目结构

ai-server 的核心源码路径是 ai-server/src/main/java/com/zsjz/ai。从当前仓库证据看,业务域集中在 module 下,同时存在大量共享模型、上下文、数据源配置和工具类。

graph TB
    Gateway["网关<br/>ai-gateway"] --> Server["ai-server 后端服务"]
    subgraph "ai-server"
        Common["公共层<br/>common/*"]
        ModuleCall["通话模块<br/>module/call"]
        ModuleTrans["转账模块<br/>module/trans"]
        ModuleTrack["轨迹模块<br/>module/track"]
        ModulePerson["人员模块<br/>module/person"]
        ModuleExternal["外部数据模块<br/>module/external"]
        ModuleGovern["治理模块<br/>module/govern"]
        ModuleAgent["智能体模块<br/>module/agent"]
        ModuleKnowledge["知识模块<br/>module/knowledge"]
        ModuleAiClean["AI 清洗模块<br/>module/aiclean"]
    end
    DB["数据库<br/>主库 + 案件数据源"]
    Server --> Common
    Server --> ModuleCall
    Server --> ModuleTrans
    Server --> ModuleTrack
    Server --> ModulePerson
    Server --> ModuleExternal
    Server --> ModuleGovern
    Server --> ModuleAgent
    Server --> ModuleKnowledge
    Server --> ModuleAiClean
    ModuleCall --> DB
    ModuleTrans --> DB
    ModuleTrack --> DB
    ModulePerson --> DB
    ModuleExternal --> DB
    ModuleGovern --> DB
    ModuleAgent --> DB
    ModuleKnowledge --> DB
    ModuleAiClean --> DB

该图体现的是概念结构:网关作为入口,ai-server 暴露统一能力;内部模块各自负责一个领域;所有业务查询最终落到数据库。

图表来源

  • CaseRoutingDataSource.java:1-68
  • CallRecordController.java:1-38

章节来源

  • ai-server/pom.xml

核心组件

在 ai-server 中,典型业务模块由四层组成:

层级 包位置 职责 对外契约
控制器 module/<domain>/controller 接收 HTTP 请求、校验入参、封装响应 REST 接口路径和参数
服务 module/<domain>/service 组合 Mapper、调用其他模块 Service、处理业务规则 Service 接口方法
数据访问 module/<domain>/mapper 绑定 MyBatis-Plus 或自定义 SQL Mapper 接口方法
实体 common/model/<domain>/entity 映射数据库表结构 实体字段和注解

以通话记录为例:

  • CallRecordController 暴露 /cr/getCallRecord 接口。
  • CallRecordService 继承 MyBatis-Plus 的 ServiceImpl,处理分页、动态表名、默认比较符等逻辑。
  • CallRecordMapper 定义分页查询、流式查询、统计和摘要查询。
  • CallRecord 是 call_record 表的实体映射。

    classDiagram
    class CallRecordController {
        +getCallRecord(query) Result
    }
    
    class CallRecordService {
        +getCallRecord(query) Page
    }
    
    class CallRecordMapper {
        +selectCallRecordPage(page, query) Page
        +selectForStream(resultHandler, tableName) void
        +statCall(personName, tableName) CallDataProfileStatDTO
        +callOftenTop10(page, query) Page
        +callStatStation(query) List
        +getCallRecordSummary(page, query) Page
        +callGraphDetail(page, query) Page
    }
    
    class CallRecord {
        +personName
        +personPhone
        +callDateTime
        +duration
        +callDirection
        +otherPhone
        +...
    }
    
    CallRecordController --> CallRecordService : "调用"
    CallRecordService --> CallRecordMapper : "调用"
    CallRecordMapper --> CallRecord : "映射"
    

图表来源

  • CallRecordController.java:1-38
  • CallRecordService.java:1-39
  • CallRecordMapper.java:1-92
  • CallRecord.java:1-200

章节来源

  • CallRecordController.java:1-38
  • CallRecordService.java:1-39
  • CallRecordMapper.java:1-92
  • CallRecord.java:1-200

架构总览

ai-server 的请求链路可以概括为:网关 → Spring MVC Controller → Service → Mapper → 动态数据源 → 数据库。

sequenceDiagram
    participant Client as "客户端"
    participant Gateway as "网关"
    participant Controller as "Controller"
    participant Service as "Service"
    participant Mapper as "Mapper"
    participant DataSource as "动态数据源"
    participant Database as "数据库"

    Client->>Gateway: "HTTP 请求"
    Gateway->>Controller: "转发到 ai-server 接口"
    Controller->>Service: "调用业务方法"
    Service->>Service: "校验、组合条件、调用其他模块 Service"
    Service->>Mapper: "执行数据访问"
    Mapper->>DataSource: "@DS 或动态路由"
    DataSource->>Database: "按案件数据源执行 SQL"
    Database-->>DataSource: "返回结果"
    DataSource-->>Mapper: "实体或 DTO"
    Mapper-->>Service: "业务数据结构"
    Service-->>Controller: "分页或聚合结果"
    Controller-->>Client: "统一 Result 响应"

该流程强调两个关键约束:

  1. Controller 只负责协议层:解析请求、调用 Service、包装 Result。
  2. Service 是模块内和模块间的唯一协作点:不要跨模块直接注入其他模块的 Controller、Mapper 或具体实现类。

图表来源

  • CallRecordController.java:1-38
  • CallRecordService.java:1-39
  • CallRecordMapper.java:1-92
  • CaseRoutingDataSource.java:1-68

详细模块分析

通话模块:module/call

通话模块围绕通话记录展开,包括通话记录、夜间通话、连续通话、敏感通话、开户信息等子能力。其典型结构如下:

  • Controller:CallRecordController、CallNightController、CallContinuousController、CallSensitiveController。
  • Service:CallRecordService、CallNightService、CallContinuousService、CallSensitiveService、CallOpenInfoService。
  • Mapper:CallRecordMapper、CallNightMapper、CallContinuousMapper、CallSensitiveMapper、CallOpenInfoMapper。
  • 实体:通话相关实体放在 common/model/call/entity,查询对象放在 common/model/call/query。

通话记录请求序列

sequenceDiagram
    participant Client as "客户端"
    participant Controller as "CallRecordController"
    participant Service as "CallRecordService"
    participant Governance as "GovernConfService"
    participant Mapper as "CallRecordMapper"

    Client->>Controller: "POST /cr/getCallRecord"
    Controller->>Service: "getCallRecord(CallRecordQuery)"
    Service->>Governance: "获取通话表名"
    Governance-->>Service: "返回表名"
    Service->>Service: "设置默认符号和未知号码处理"
    Service->>Mapper: "selectCallRecordPage(Page, Query)"
    Mapper-->>Service: "分页通话记录"
    Service-->>Controller: "分页结果"
    Controller-->>Client: "Result<Page<CallRecord>>"

图表来源

  • CallRecordController.java:1-38
  • CallRecordService.java:1-39
  • CallRecordMapper.java:1-92

通话记录处理流程

flowchart TD
    Start(["进入 getCallRecord"]) --> ReadQuery["读取查询条件"]
    ReadQuery --> GetTableName["从治理配置获取通话表名"]
    GetTableName --> SetSymbol["设置默认比较符号"]
    SetSymbol --> CheckUnknown{"是否包含“未知号码”"}
    CheckUnknown -->|是| ClearList["清空列表并设置单一未知号码字段"]
    CheckUnknown -->|否| Skip["跳过"]
    ClearList --> BuildPage["构造分页对象"]
    Skip --> BuildPage
    BuildPage --> QueryMapper["调用 Mapper 分页查询"]
    QueryMapper --> Return["返回分页结果"]

图表来源

  • CallRecordService.java:1-39

章节来源

  • CallRecordController.java:1-38
  • CallRecordService.java:1-39
  • CallRecordMapper.java:1-92

数据访问层:Mapper 与实体

Mapper 是模块对数据库的边界。它不应承载复杂业务判断,而应专注于:

  • 基础 CRUD。
  • 分页查询。
  • 流式查询。
  • 统计和汇总查询。
  • 动态表名或复杂 SQL。

CallRecordMapper 展示了这一模式:它继承 BaseMapper,并通过 @DS(StrConsts.DS_KEY_SLAVE) 指定数据源键。同时提供流式查询接口,用于大数据量场景。

实体放在 common/model/<domain>/entity,例如 CallRecord。它使用 MyBatis-Plus 注解映射表名和字段,并继承公共基类。实体是持久化模型,不是纯粹的接口传输对象;对外暴露时通常通过 DTO 或 VO 进行裁剪。

章节来源

  • CallRecordMapper.java:1-92
  • CallRecord.java:1-200

案件级数据源路由

ai-server 的一个重要特性是“按案件隔离数据”。数据源路由逻辑集中在 CaseRoutingDataSource 和 CaseContextHolder:

  • CaseContextHolder 使用 JDK ScopedValue 保存当前线程作用域中的 caseId 和 userId。
  • CaseRoutingDataSource 将逻辑数据源键 slave 映射为物理数据源键 case{caseId}。
  • 如果未打开案件,路由会返回明确的 400 错误,而不是静默回退。

    flowchart TD
    Request["收到数据访问请求"] --> CheckKey{"数据源键是否为 slave"}
    CheckKey -->|否| DefaultRoute["走默认数据源路由"]
    CheckKey -->|是| ReadCaseId["读取 CaseContextHolder.currentCaseId()"]
    ReadCaseId --> HasCaseId{"是否有 caseId"}
    HasCaseId -->|否| ThrowNoCase["抛出“请先打开案件”异常"]
    HasCaseId -->|是| BuildKey["生成物理 key:case{caseId}"]
    BuildKey --> FindDs["查找对应 DataSource"]
    FindDs --> Found{"是否找到"}
    Found -->|否| ThrowMissing["抛出“案件数据源未打开”异常"]
    Found -->|是| ReturnDs["返回目标数据源"]
    

图表来源

  • CaseRoutingDataSource.java:1-68
  • CaseContextHolder.java:1-184

章节来源

  • CaseRoutingDataSource.java:1-68
  • CaseContextHolder.java:1-184

案件访问安全

CaseAccessGuard 提供统一的案件访问校验,主要解决三类问题:

  1. 当前是否已打开案件。
  2. 请求参数中的 caseId 是否与当前案件一致。
  3. 当前登录用户是否拥有该案件。

    flowchart TD
    Enter["进入 requireOwnedCurrentCase"] --> RequireOpen["校验已打开案件"]
    RequireOpen --> CompareCaseId{"请求 caseId 与当前 caseId 是否一致"}
    CompareCaseId -->|不一致| RejectContext["拒绝:案件上下文不一致"]
    CompareCaseId -->|一致| LoadCase["加载案件信息"]
    LoadCase --> CheckOwner{"是否有 owner_id"}
    CheckOwner -->|无| LogWarn["记录警告日志并按历史数据放行"]
    CheckOwner -->|有| ValidateUser{"是否属于当前用户"}
    ValidateUser -->|否| RejectForbidden["拒绝:无权访问"]
    ValidateUser -->|是| ReturnCase["返回案件信息"]
    

图表来源

  • CaseAccessGuard.java:1-90

章节来源

  • CaseAccessGuard.java:1-90

其他业务模块概览

除通话模块外,仓库中还可见多个稳定形态的业务模块:

模块 典型职责 代表 Controller 代表 Service
trans 转账、资金流水、频繁交易、大额交易、固定存款等 TransRecordController、TransFrequencyController、TransBigController TransRecordService、TransFrequencyService、TransBigService
track 轨迹、同住、同乘、基站、快递、酒店等 TrackMeetController、TrackTogetherTravelController、ExpressInfoController TrackMeetService、TrackTogetherTravelService、TrackExpressInfoService
person 人员档案、亲密度、常住人口、人员分组 PersonBasicInfoController、IntimacyController PersonBasicInfoService、IntimacyService
external 外部数据映射 外部数据相关 Controller 外部数据相关 Service
govern 治理配置、树形结构 GovernController、GovernTreeController GovernService、GovernTreeService
agent 智能体、模型、工具包、聊天会话 AgentChatController、AgentModelController AgentChatService、AgentModelService
knowledge 知识库、文档、向量索引 KnowledgeBaseController KnowledgeBaseService、KnowledgeDocService
aiclean AI 清洗、模板、规则、批次任务 AiCleanController、AiSmartCleanController AiCleanService、AiSmartCleanService

这些模块共同遵循相同分层:Controller 暴露接口,Service 组织业务,Mapper 访问数据,Entity 映射表结构。

章节来源

  • CallRecordController.java:1-38
  • CallRecordService.java:1-39
  • CallRecordMapper.java:1-92

依赖关系分析

模块内依赖方向

在一个模块内部,依赖方向应是单向的:

flowchart LR
    Controller["Controller"] --> Service["Service"]
    Service --> Mapper["Mapper"]
    Service --> Entity["Entity"]
    Mapper --> Entity["Entity"]

Controller 不直接操作数据库;Service 不暴露给 Controller 以外的调用方作为实现类;Mapper 只关注数据访问。

跨模块依赖原则

跨模块调用应遵守以下规则:

  1. 通过 Service 接口调用:被调用方的 Service 接口是契约,调用方不应依赖其 impl 实现类。
  2. 通过共享 DTO 传递数据:不要在跨模块调用中直接传递复杂实体,尤其是带副作用或状态的对象。
  3. 禁止循环依赖:A 模块依赖 B 模块时,B 模块不能反过来依赖 A 模块。
  4. 网关只感知 ai-server 整体:网关层不应知道 module/call、module/trans 等内部模块名称;它只看到 ai-server 暴露的统一 API。

    graph TB
    SubA["模块 A<br/>module/trans"]
    SubB["模块 B<br/>module/call"]
    SharedDto["共享 DTO<br/>common/model/*/dto"]
    
    SubA -->|"调用 Service 接口"| SubB
    SubB -->|"返回"| SharedDto
    SubA -->|"使用"| SharedDto
    

现有跨模块调用示例

当前可见的典型跨模块依赖包括:

  • CallRecordService 依赖 GovernConfService,用于获取通话表名。
  • CaseAccessGuard 依赖 CaseInfoMapper,用于校验案件归属。
  • 数据源路由依赖 CaseContextHolder,用于读取当前案件上下文。

这说明跨模块依赖已经存在,但应继续收敛到 Service 层和共享 DTO,而不是让 Controller 或 Mapper 互相感知。

章节来源

  • CallRecordService.java:1-39
  • CaseAccessGuard.java:1-90
  • CaseRoutingDataSource.java:1-68

性能考虑

当前代码中存在一些值得注意的性能特征:

  1. 流式查询:CallRecordMapper.selectForStream 使用向前游标和 fetchSize=0,适合大数据量扫描,避免一次性加载全部结果。
  2. 分页查询:多数业务接口使用 MyBatis-Plus 的 Page,避免全表拉取。
  3. 动态数据源路由:CaseRoutingDataSource 把逻辑 key 映射为物理 key,避免硬编码连接,但路由失败时会直接抛异常,需要确保案件已打开。
  4. 异步上下文传播:CaseContextHolder 使用 ScopedValue,比 ThreadLocal 更安全,也更适合虚拟线程和异步链路。

优化建议:

  • 对高频查询增加缓存,如 Redis 或本地缓存,但要注意案件维度隔离。
  • 对大结果集优先使用流式查询或分批导出。
  • 避免在 Service 中做大量字符串拼接或重复 SQL 构建。
  • 对跨模块调用引入超时和重试策略,防止下游慢查询拖垮上游。

[本节为通用性能建议,不直接分析具体文件]

故障排查指南

常见问题一:提示“请先打开案件后再操作数据”

可能原因:

  • 前端没有先调用开案接口。
  • 当前线程没有绑定 caseId。
  • 异步任务中没有用 CaseContextHolder.runWith 或 callWith 包裹业务逻辑。

排查步骤:

  1. 检查开案接口是否成功写入 CaseContextHolder。
  2. 检查异步任务是否显式传递 caseId 和 userId。
  3. 查看 CaseRoutingDataSource 路由日志,确认是否拿到物理数据源 key。

章节来源

  • CaseRoutingDataSource.java:1-68
  • CaseContextHolder.java:1-184

常见问题二:提示“当前案件数据源未打开或已释放”

可能原因:

  • 用户关闭了案件。
  • 案件数据源尚未注册。
  • 多租户或多案件并发时,上下文串线。

排查步骤:

  1. 确认案件生命周期管理是否正确释放和注册数据源。
  2. 检查是否存在旧线程复用导致上下文污染。
  3. 核对物理数据源 key 是否符合 case{caseId} 且不含下划线。

章节来源

  • CaseRoutingDataSource.java:1-68

常见问题三:提示“无权访问该案件”

可能原因:

  • 请求传入的 caseId 不属于当前登录用户。
  • 历史脏数据缺少 owner_id,但后续系统收紧权限后仍可能被拦截。

排查步骤:

  1. 使用 CaseAccessGuard.requireOwnedCurrentCase 定位权限校验失败点。
  2. 检查案件归属字段是否迁移完成。
  3. 审计缺失归属的案件 ID。

章节来源

  • CaseAccessGuard.java:1-90

结论

ai-server 的模块化架构以 module/* 为业务边界,以 controller → service → mapper → entity 为统一分层。通话模块是最清晰的参考实现:Controller 暴露接口,Service 处理业务规则和跨模块协作,Mapper 负责数据访问,Entity 映射表结构。

关键设计要点包括:

  • 模块内单向依赖:Controller 不碰 Mapper,Service 不直接暴露实现。
  • 跨模块通过 Service 接口和共享 DTO 通信。
  • 网关只感知 ai-server 整体,不感知内部模块。
  • 数据访问按案件隔离,由 CaseRoutingDataSource 和 CaseContextHolder 保证。
  • 新增模块必须遵循统一分层、命名约定、SQL 脚本管理和可测试性要求。

附录:新增业务模块标准步骤

第一步:确定业务域和边界

在创建代码前,先回答:

  • 这个模块负责哪个领域?
  • 它依赖哪些已有模块?
  • 哪些数据应该放在 common/model 中作为共享模型?
  • 是否需要新增案件级数据源表?

第二步:创建模块目录

建议在 ai-server/src/main/java/com/zsjz/ai/module/<domain> 下创建:

  • controller
  • service
  • service/impl(如需接口与实现分离)
  • mapper
  • converter(可选)
  • config(可选)
  • vo(可选)
  • dto(仅当该模块需要跨模块输入输出时使用,否则优先使用 common/model)

第三步:定义实体

实体放在 common/model/<domain>/entity,使用 MyBatis-Plus 注解映射表名和字段。实体应与数据库表结构保持强对应关系,但不宜直接作为对外接口模型。

第四步:定义 Mapper

在 module/<domain>/mapper 中定义 Mapper 接口,继承 MyBatis-Plus 的 BaseMapper 或自定义查询接口。对需要按案件数据源访问的 Mapper,使用 @DS(StrConsts.DS_KEY_SLAVE)。

第五步:定义 Service

在 module/<domain>/service 中定义 Service 接口和实现类。Service 应:

  • 处理业务规则。
  • 组合多个 Mapper。
  • 调用其他模块的 Service 接口。
  • 返回 DTO 或领域结果对象。
  • 不在 Service 中直接写 HTTP 相关逻辑。

第六步:定义 Controller

在 module/<domain>/controller 中定义 Controller,使用 @RestController 和 @RequestMapping。Controller 只做:

  • 参数解析。
  • 简单校验。
  • 调用 Service。
  • 返回统一 Result。

第七步:注册路由

根据项目现有路由策略注册 Controller 路径。若项目使用集中式路由注册,则更新对应注册类;若使用 Spring MVC 自动扫描,则确保 Controller 能被扫描到。

第八步:维护 SQL 脚本

在 sql/ 目录下新增或更新对应 SQL 脚本,例如:

  • sql/<domain>_tables.sql
  • sql/<domain>_init_data.sql
  • sql/<domain>_migration_vX.sql

SQL 脚本应具备版本标识、幂等性和可回滚说明。

第九步:编写单元测试和集成测试

建议覆盖:

  • Controller 接口用例。
  • Service 业务逻辑用例。
  • Mapper SQL 用例。
  • 跨模块调用用例。
  • 案件上下文和权限用例。

第十步:评审重点

代码评审应检查:

  • 是否违反 Controller → Service → Mapper 的分层。
  • 是否存在跨模块直接依赖实现类。
  • 是否存在循环依赖。
  • 是否使用共享 DTO 而非实体跨模块传输。
  • 是否正确使用 CaseContextHolder 和 CaseAccessGuard。
  • SQL 脚本是否纳入版本管理。

章节来源

  • CallRecordController.java:1-38
  • CallRecordService.java:1-39
  • CallRecordMapper.java:1-92
  • CallRecord.java:1-200
  • CaseRoutingDataSource.java:1-68
  • CaseContextHolder.java:1-184
  • CaseAccessGuard.java:1-90