本文引用的文件
ai-server 是一个以案件为中心、面向多数据域的后端服务。代码主体位于 ai-server/src/main/java/com/zsjz/ai,其中业务域按 module/* 划分,例如通话、转账、轨迹、人员、外部数据、治理、智能体、知识检索等;公共模型、配置、上下文和数据源路由放在 common/*。
本文聚焦以下目标:
controller、service、mapper、entity 的职责边界。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 暴露统一能力;内部模块各自负责一个领域;所有业务查询最终落到数据库。
图表来源
章节来源
在 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 : "映射"
图表来源
章节来源
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 响应"
该流程强调两个关键约束:
Result。图表来源
module/call通话模块围绕通话记录展开,包括通话记录、夜间通话、连续通话、敏感通话、开户信息等子能力。其典型结构如下:
CallRecordController、CallNightController、CallContinuousController、CallSensitiveController。CallRecordService、CallNightService、CallContinuousService、CallSensitiveService、CallOpenInfoService。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>>"
图表来源
flowchart TD
Start(["进入 getCallRecord"]) --> ReadQuery["读取查询条件"]
ReadQuery --> GetTableName["从治理配置获取通话表名"]
GetTableName --> SetSymbol["设置默认比较符号"]
SetSymbol --> CheckUnknown{"是否包含“未知号码”"}
CheckUnknown -->|是| ClearList["清空列表并设置单一未知号码字段"]
CheckUnknown -->|否| Skip["跳过"]
ClearList --> BuildPage["构造分页对象"]
Skip --> BuildPage
BuildPage --> QueryMapper["调用 Mapper 分页查询"]
QueryMapper --> Return["返回分页结果"]
图表来源
章节来源
Mapper 是模块对数据库的边界。它不应承载复杂业务判断,而应专注于:
CallRecordMapper 展示了这一模式:它继承 BaseMapper,并通过 @DS(StrConsts.DS_KEY_SLAVE) 指定数据源键。同时提供流式查询接口,用于大数据量场景。
实体放在 common/model/<domain>/entity,例如 CallRecord。它使用 MyBatis-Plus 注解映射表名和字段,并继承公共基类。实体是持久化模型,不是纯粹的接口传输对象;对外暴露时通常通过 DTO 或 VO 进行裁剪。
章节来源
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["返回目标数据源"]
图表来源
章节来源
CaseAccessGuard 提供统一的案件访问校验,主要解决三类问题:
caseId 是否与当前案件一致。当前登录用户是否拥有该案件。
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["返回案件信息"]
图表来源
章节来源
除通话模块外,仓库中还可见多个稳定形态的业务模块:
| 模块 | 典型职责 | 代表 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 映射表结构。
章节来源
在一个模块内部,依赖方向应是单向的:
flowchart LR
Controller["Controller"] --> Service["Service"]
Service --> Mapper["Mapper"]
Service --> Entity["Entity"]
Mapper --> Entity["Entity"]
Controller 不直接操作数据库;Service 不暴露给 Controller 以外的调用方作为实现类;Mapper 只关注数据访问。
跨模块调用应遵守以下规则:
impl 实现类。网关只感知 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 互相感知。
章节来源
当前代码中存在一些值得注意的性能特征:
CallRecordMapper.selectForStream 使用向前游标和 fetchSize=0,适合大数据量扫描,避免一次性加载全部结果。Page,避免全表拉取。CaseRoutingDataSource 把逻辑 key 映射为物理 key,避免硬编码连接,但路由失败时会直接抛异常,需要确保案件已打开。CaseContextHolder 使用 ScopedValue,比 ThreadLocal 更安全,也更适合虚拟线程和异步链路。优化建议:
[本节为通用性能建议,不直接分析具体文件]
可能原因:
caseId。CaseContextHolder.runWith 或 callWith 包裹业务逻辑。排查步骤:
CaseContextHolder。caseId 和 userId。CaseRoutingDataSource 路由日志,确认是否拿到物理数据源 key。章节来源
可能原因:
排查步骤:
case{caseId} 且不含下划线。章节来源
可能原因:
caseId 不属于当前登录用户。owner_id,但后续系统收紧权限后仍可能被拦截。排查步骤:
CaseAccessGuard.requireOwnedCurrentCase 定位权限校验失败点。章节来源
ai-server 的模块化架构以 module/* 为业务边界,以 controller → service → mapper → entity 为统一分层。通话模块是最清晰的参考实现:Controller 暴露接口,Service 处理业务规则和跨模块协作,Mapper 负责数据访问,Entity 映射表结构。
关键设计要点包括:
CaseRoutingDataSource 和 CaseContextHolder 保证。在创建代码前,先回答:
common/model 中作为共享模型?建议在 ai-server/src/main/java/com/zsjz/ai/module/<domain> 下创建:
controllerserviceservice/impl(如需接口与实现分离)mapperconverter(可选)config(可选)vo(可选)dto(仅当该模块需要跨模块输入输出时使用,否则优先使用 common/model)实体放在 common/model/<domain>/entity,使用 MyBatis-Plus 注解映射表名和字段。实体应与数据库表结构保持强对应关系,但不宜直接作为对外接口模型。
在 module/<domain>/mapper 中定义 Mapper 接口,继承 MyBatis-Plus 的 BaseMapper 或自定义查询接口。对需要按案件数据源访问的 Mapper,使用 @DS(StrConsts.DS_KEY_SLAVE)。
在 module/<domain>/service 中定义 Service 接口和实现类。Service 应:
在 module/<domain>/controller 中定义 Controller,使用 @RestController 和 @RequestMapping。Controller 只做:
Result。根据项目现有路由策略注册 Controller 路径。若项目使用集中式路由注册,则更新对应注册类;若使用 Spring MVC 自动扫描,则确保 Controller 能被扫描到。
在 sql/ 目录下新增或更新对应 SQL 脚本,例如:
sql/<domain>_tables.sqlsql/<domain>_init_data.sqlsql/<domain>_migration_vX.sqlSQL 脚本应具备版本标识、幂等性和可回滚说明。
建议覆盖:
代码评审应检查:
CaseContextHolder 和 CaseAccessGuard。章节来源