20260823210241

This commit is contained in:
oneao committed 2026-08-23 21:02:41 +08:00
1 parent 5ff4583aeb
commit f4c7413dc6
2766 files changed
+1836409 -94689

No files matched your search

@@ -0,0 +1,395 @@
# 数据服务
<cite>
**本文引用的文件**
- [DataService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java)
- [DataSaveService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java)
- [DbUtils.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java)
- [ParamUtils.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ParamUtils.java)
- [BusinessException.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/exception/BusinessException.java)
- [OrgDataSourceManager.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录:API使用示例与最佳实践](#附录api使用示例与最佳实践)
## 简介
本文件面向数据服务的后端实现,围绕 DataService 与 DataSaveService 的数据操作能力进行系统化说明。内容涵盖:
- CRUD 与批量数据处理
- 事务管理与一致性保障
- 元数据驱动的数据模型、动态 SQL 生成与查询优化
- 数据验证规则、业务逻辑封装与异常处理机制
- 多租户数据隔离、数据权限控制(只读校验)与缓存策略
- API 使用示例(复杂查询、导入导出思路)与性能调优建议
## 项目结构
数据服务位于 fms-api 模块的 service 与 utils 包中,核心由以下部分组成:
- 服务层:DataService(读取)、DataSaveService(写入与自动编号)
- 工具层:DbUtils(SQL 构建、CRUD、分页、元数据读取、安全校验)、ParamUtils(参数解析)
- 多租户:OrgDataSourceManager(按组织维度管理数据源连接)
- 异常:BusinessException(业务异常统一类型)
```mermaid
graph TB
subgraph "服务层"
DS["DataService"]
DSS["DataSaveService"]
end
subgraph "工具层"
DU["DbUtils"]
PU["ParamUtils"]
end
subgraph "多租户"
ODM["OrgDataSourceManager"]
end
DS --> DU
DSS --> DU
DSS --> ODM
DS --> ODM
```
图表来源
- [DataService.java:17-92](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java#L17-L92)
- [DataSaveService.java:25-494](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L494)
- [DbUtils.java:27-665](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L27-L665)
- [OrgDataSourceManager.java:13-62](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java#L13-L62)
章节来源
- [DataService.java:17-92](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java#L17-L92)
- [DataSaveService.java:25-494](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L494)
- [DbUtils.java:27-665](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L27-L665)
- [OrgDataSourceManager.java:13-62](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java#L13-L62)
## 核心组件
- DataService:提供视图/表数据的描述、加载、分页与自定义只读 SQL 执行能力。所有查询通过 DbUtils 的安全校验与动态 SQL 构建。
- DataSaveService:提供批量保存(删除/更新/插入)与自动编码生成;内部维护事务边界,确保原子性。
- DbUtils:封装数据库访问细节,包括元数据读取、列级可写性判断、SQL 注入防护、分页、读写分离式方法命名与安全校验。
- ParamUtils:统一的参数提取与类型校验工具。
- OrgDataSourceManager:按组织 ID 获取/复用数据源连接,支撑多租户数据隔离。
章节来源
- [DataService.java:17-92](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java#L17-L92)
- [DataSaveService.java:25-494](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L494)
- [DbUtils.java:27-665](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L27-L665)
- [ParamUtils.java:8-60](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ParamUtils.java#L8-L60)
- [OrgDataSourceManager.java:13-62](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java#L13-L62)
## 架构总览
数据服务采用“服务层 + 工具层”的分层设计:
- 服务层负责业务编排与事务边界
- 工具层负责 SQL 构建、元数据驱动、安全校验与数据库交互
- 多租户通过 OrgDataSourceManager 在需要时切换数据源
```mermaid
sequenceDiagram
participant C as "调用方"
participant S as "DataService"
participant U as "DbUtils"
participant DB as "数据库"
C->>S : 请求分页/加载数据
S->>U : page()/loadData()
U->>DB : 构建并执行只读SQL
DB-->>U : 结果集
U-->>S : 行列表/分页结果
S-->>C : 响应数据
```
图表来源
- [DataService.java:50-68](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java#L50-L68)
- [DbUtils.java:110-162](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L110-L162)
## 详细组件分析
### DataService:读取与分页
- 功能要点
- describe(table):返回表的列元信息(名称、类型、是否可空、长度),用于前端字段同步等场景。
- loadData(params):根据视图名、搜索条件、排序与指定列集合加载数据。
- loadDataBySql(sql):执行用户提供的只读 SQL,受严格白名单与结构校验保护。
- page(params):分页查询,返回 rows、total、page_no、page_size。
- 安全与校验
- 所有查询均经 DbUtils.validateReadOnlySql 校验,禁止非只读关键字、分号与注释。
- 标识符通过 quoteQualifiedIdentifier 安全转义,防止注入。
- 分页实现
- 使用 OFFSET/FETCH 分页,并通过 COUNT_BIG OVER() 计算总数;当首页无数据时回退为单独 COUNT 查询。
```mermaid
flowchart TD
Start(["进入 page"]) --> Validate["校验 page_no/page_size"]
Validate --> BuildSQL["构建查询SQL(过滤/排序)"]
BuildSQL --> PageSQL["包装为分页SQL<br/>包含COUNT_BIG OVER()"]
PageSQL --> Exec["执行分页查询"]
Exec --> HasRows{"是否有数据或第一页?"}
HasRows -- 是 --> Return["返回分页结果"]
HasRows -- 否 --> Count["单独COUNT查询"]
Count --> Return
```
图表来源
- [DataService.java:50-68](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java#L50-L68)
- [DbUtils.java:110-162](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L110-L162)
章节来源
- [DataService.java:26-90](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java#L26-L90)
- [DbUtils.java:44-96](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L44-L96)
- [DbUtils.java:110-162](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L110-L162)
- [DbUtils.java:440-459](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L440-L459)
### DataSaveService:批量保存与自动编号
- 批量保存
- save(requests):在一个事务内依次执行 deletes、updates、inserts;任一失败整体回滚。
- 元数据缓存:同一事务内对同一表的元数据仅加载一次,减少反射/元数据开销。
- 主键处理:新增时若主键为空或负数则自动生成雪花ID;更新/删除需显式提供主键。
- 自动编号 nextCode(moduleId, count)
- 从 s_module_auto_code 读取启用规则,校验目标字段类型与唯一索引。
- 支持前缀、日期片段、分隔符、流水宽度与重置周期(年/月/日/不重置)。
- 并发安全:使用 UPDLOCK/HOLDLOCK/ROWLOCK 锁定规则行,避免重复分配。
- 冲突检测:循环探测可用编号,直至找到未占用值或达到上限。
- 持久化:将当前周期与最新流水值写回 s_module_auto_code。
```mermaid
sequenceDiagram
participant Client as "调用方"
participant Save as "DataSaveService.save"
participant Conn as "Connection"
participant DU as "DbUtils"
Client->>Save : 提交批量保存请求
Save->>Conn : 开启事务(关闭自动提交)
loop 逐条请求
Save->>DU : delete/update/insert(带表元数据)
DU-->>Save : 影响行数
end
Save->>Conn : 提交事务
Save-->>Client : 成功
Note over Save,Conn : 任意异常触发回滚
```
图表来源
- [DataSaveService.java:34-58](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L34-L58)
- [DataSaveService.java:381-464](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L381-L464)
- [DbUtils.java:264-330](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L264-L330)
```mermaid
flowchart TD
A["nextCode入口"] --> B["加载自动编码规则(加锁)"]
B --> C{"规则有效?"}
C -- 否 --> E["抛出业务异常"]
C -- 是 --> D["校验目标字段类型与唯一索引"]
D --> F["构造格式(前缀/日期/分隔符/宽度)"]
F --> G["计算周期键(年/月/日/不重置)"]
G --> H{"是否需要重置?"}
H -- 是 --> I["起始值"]
H -- 否 --> J["递增当前值"]
I --> K["生成候选编号并查重"]
J --> K
K --> L{"是否可用?"}
L -- 否 --> M["递增并继续尝试"]
M --> K
L -- 是 --> N["记录最后一个值并更新周期"]
N --> O["返回编号列表"]
```
图表来源
- [DataSaveService.java:60-143](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L60-L143)
- [DataSaveService.java:145-181](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L145-L181)
- [DataSaveService.java:183-250](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L183-L250)
- [DataSaveService.java:252-348](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L252-L348)
章节来源
- [DataSaveService.java:34-143](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L34-L143)
- [DataSaveService.java:145-348](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L145-L348)
- [DataSaveService.java:381-494](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L381-L494)
- [DbUtils.java:264-330](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L264-L330)
### 元数据驱动的数据模型
- 表/列元数据
- loadTableMetadata:通过 SELECT * FROM table WHERE 1=0 获取列信息,推断是否可写、是否自增/时间戳、是否可空、精度/长度。
- ColumnMetadata/TableMetadata:封装列与表的结构信息,供写入时动态生成 SQL。
- 动态 SQL 生成
- insert/update/delete:基于元数据选择可写字段,拼接列名与占位符,保证参数化执行。
- 排序与列名:buildOrderBy/buildColumns 对列名进行安全转义,支持 CASE 表达式排序与 ASC/DESC。
章节来源
- [DbUtils.java:180-245](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L180-L245)
- [DbUtils.java:264-330](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L264-L330)
- [DbUtils.java:488-538](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L488-L538)
### 查询优化
- 分页优化
- 使用 COUNT_BIG OVER() 一次性获取总数,避免额外 COUNT 查询;仅在首页无数据时回退到独立 COUNT。
- 排序安全与灵活
- 支持多字段排序与 CASE 表达式排序,同时限制方向为 ASC/DESC,防止注入。
- 列裁剪
- 支持 search_columns 指定返回列,减少网络传输与内存占用。
章节来源
- [DbUtils.java:110-162](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L110-L162)
- [DbUtils.java:488-538](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L488-L538)
### 数据验证规则与业务逻辑封装
- 参数校验
- ParamUtils 提供 getString/getRequiredString/getList 等强类型提取,缺失或类型不符抛业务异常。
- DataService.page 对 page_no/page_size 进行正整数校验。
- 业务约束
- DataSaveService 对自动编号规则、字段类型、唯一索引、长度等进行前置校验。
- DbUtils 对 SQL 进行只读校验、标识符校验、字符串字面量剥离与注释检查。
章节来源
- [ParamUtils.java:13-46](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ParamUtils.java#L13-L46)
- [DataService.java:50-90](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java#L50-L90)
- [DataSaveService.java:60-143](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L60-L143)
- [DbUtils.java:434-459](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L434-L459)
### 异常处理机制
- 统一业务异常:BusinessException 贯穿参数校验、规则校验与运行时错误。
- 事务回滚:DataSaveService 在捕获异常时执行 rollback,并将回滚异常以 suppressed 形式附加,便于定位根因。
- 写入一致性:executeWrite 校验影响行数必须为 1,否则抛出异常。
章节来源
- [BusinessException.java:3-8](file://fms-api/src/main/java/cn/g3soft/fmsapi/exception/BusinessException.java#L3-L8)
- [DataSaveService.java:486-492](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L486-L492)
- [DbUtils.java:356-372](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L356-L372)
### 多租户数据隔离
- 数据源管理:OrgDataSourceManager 按 orgId 缓存 DruidDataSource,并提供 getConnection(orgId) 与 getConnection()(基于上下文)。
- 使用场景:认证等跨库查询可通过 getDataSource(orgId) 获取对应租户数据源。
- 资源清理:@PreDestroy 关闭所有数据源,避免连接泄漏。
章节来源
- [OrgDataSourceManager.java:13-62](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java#L13-L62)
### 数据权限控制(只读校验)
- 只读 SQL 白名单:validateReadOnlySql 仅允许 select/with 开头,禁止分号、注释与非只读关键字。
- 标识符安全:quoteQualifiedIdentifier 对表/列名进行严格校验与转义。
- 目的:防止通过 loadDataBySql 执行写入或破坏性语句。
章节来源
- [DbUtils.java:440-459](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L440-L459)
- [DbUtils.java:540-549](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L540-L549)
### 缓存策略
- 元数据缓存:DataSaveService 在同一事务内缓存 TableMetadata,避免重复元数据查询。
- 数据源缓存:OrgDataSourceManager 缓存各租户数据源实例,减少创建成本。
- 注意:当前未实现通用查询结果缓存;如需提升热点查询性能,可在上层引入应用级缓存(如 Redis),并结合失效策略。
章节来源
- [DataSaveService.java:44-47](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L44-L47)
- [DataSaveService.java:425-443](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L425-L443)
- [OrgDataSourceManager.java:16-27](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java#L16-L27)
## 依赖关系分析
```mermaid
classDiagram
class DataService {
+describe(table)
+loadData(params)
+loadDataBySql(params)
+page(params)
}
class DataSaveService {
+save(requests)
+nextCode(moduleId,count)
}
class DbUtils {
+loadData(...)
+loadDataBySql(...)
+page(...)
+insert/update/delete(...)
+loadTableMetadata(...)
+validateReadOnlySql(...)
}
class OrgDataSourceManager {
+getDataSource(orgId)
+getConnection(orgId)
}
class ParamUtils {
+getString/getRequiredString/getList(...)
}
DataService --> DbUtils : "使用"
DataSaveService --> DbUtils : "使用"
DataSaveService --> OrgDataSourceManager : "可选使用"
DataService --> ParamUtils : "参数解析"
DataSaveService --> ParamUtils : "参数解析"
```
图表来源
- [DataService.java:17-92](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java#L17-L92)
- [DataSaveService.java:25-494](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L494)
- [DbUtils.java:27-665](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L27-L665)
- [ParamUtils.java:8-60](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ParamUtils.java#L8-L60)
- [OrgDataSourceManager.java:13-62](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java#L13-L62)
章节来源
- [DataService.java:17-92](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataService.java#L17-L92)
- [DataSaveService.java:25-494](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L494)
- [DbUtils.java:27-665](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L27-L665)
- [ParamUtils.java:8-60](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ParamUtils.java#L8-L60)
- [OrgDataSourceManager.java:13-62](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java#L13-L62)
## 性能考虑
- 分页查询
- 优先使用 page 接口,利用 COUNT_BIG OVER() 降低额外 COUNT 开销。
- 合理设置 page_size,避免单页过大导致内存与网络压力。
- 列裁剪
- 通过 search_columns 指定返回列,减少数据传输。
- 排序
- 尽量使用索引列排序;CASE 表达式排序可能无法利用索引,需谨慎使用。
- 批量写入
- 使用 save 接口批量提交,减少事务次数;保持每批大小适中(例如数百条)。
- 元数据缓存
- 同一事务内元数据已缓存,避免重复反射开销。
- 只读 SQL 校验
- validateReadOnlySql 会做正则扫描,避免在高频路径传入超长 SQL。
[本节为通用指导,不直接分析具体文件]
## 故障排查指南
- 常见错误与定位
- 参数缺失或类型错误:检查 ParamUtils 抛出的业务异常信息,确认入参键名与类型。
- 只读 SQL 被拒绝:确认 SQL 以 select/with 开头,且不含分号、注释与写入关键字。
- 自动编号失败:检查 s_module_auto_code 配置、目标字段类型与唯一索引是否存在。
- 事务回滚:查看 DataSaveService 中的原始异常与 suppressed 异常,定位具体失败的行与操作。
- 调试建议
- 打印关键参数(view_name、search_condition、order_by、table、key_field)。
- 对于 loadDataBySql,先在后端控制台输出最终 SQL 再执行。
- 针对分页问题,分别测试不带排序与带排序的查询,逐步缩小范围。
章节来源
- [ParamUtils.java:13-46](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ParamUtils.java#L13-L46)
- [DbUtils.java:440-459](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L440-L459)
- [DataSaveService.java:60-143](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L60-L143)
- [DataSaveService.java:486-492](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L486-L492)
## 结论
该数据服务以元数据驱动为核心,结合严格的只读校验与安全的标识符处理,提供了稳定可靠的读取与写入能力。DataSaveService 在事务内完成批量操作,并内置自动编号生成器,满足常见业务需求。通过分页优化、列裁剪与元数据缓存等手段,系统在性能与安全性之间取得平衡。后续可按需在更高层引入应用级缓存与审计日志,进一步提升可扩展性与可观测性。
[本节为总结,不直接分析具体文件]
## 附录:API使用示例与最佳实践
- 读取数据
- 描述表结构:调用 describe(table),用于前端表单/表格字段渲染。
- 加载数据:调用 loadData({ view_name, search_condition?, order_by?, search_columns? })。
- 分页查询:调用 page({ view_name, search_columns?, search_condition?, order_by, page_no, page_size })。
- 自定义只读查询:调用 loadDataBySql({ sql }),确保 SQL 符合只读规范。
- 写入数据
- 批量保存:调用 save([{ table, key_field, deletes[], updates[], inserts[] }])。
- 自动编号:调用 nextCode({ moduleId, count }),返回生成的编号列表。
- 导入导出
- 导出:通过 loadData/loadDataBySql 拉取数据,结合服务端流式响应或分批下载。
- 导入:将外部数据转换为 [{ table, key_field, inserts[] }] 后调用 save。
- 性能调优建议
- 分页:合理设置 page_size,避免超大页。
- 排序:优先使用索引列;谨慎使用 CASE 表达式排序。
- 列裁剪:明确指定 search_columns 减少带宽。
- 批量写入:控制批次大小,避免单次事务过大。
- 只读 SQL:避免过长 SQL 与复杂子查询,必要时拆分为视图或物化视图。
[本节为使用指引,不直接分析具体文件]
@@ -0,0 +1,409 @@
# 文件服务
<cite>
**本文引用的文件**
- [FileController.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java)
- [FileService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java)
- [FileStorageProperties.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java)
- [application.yaml](file://fms-api/src/main/resources/application.yaml)
- [017_create_contract_file_tables.sql](file://fms-api/config/migrations/017_create_contract_file_tables.sql)
- [fileService.js](file://fms-vue/src/services/fileService.js)
- [FmsFile.vue](file://fms-vue/src/components/fms-file/FmsFile.vue)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件服务提供统一的文件上传、下载、预览与管理能力,支持本地存储与对象存储(OSS)两种模式。后端通过控制器暴露 REST API,服务层封装存储策略、数据库记录与外部 OSS 交互;前端通过统一的服务模块调用接口,并与文件组件集成实现上传、预览与管理。
## 项目结构
- 后端(Spring Boot)
- 控制器:/file/* 路由,处理配置查询、上传、凭证获取、保存记录、删除与静态资源访问
- 服务:FileService 实现本地/OSS 上传、删除、DB 记录写入等核心逻辑
- 配置:FileStorageProperties 绑定 fms.file.* 配置项,区分 local/oss
- 资源:application.yaml 提供运行时配置
- 前端(Vue)
- 服务层:fileService.js 封装 HTTP 调用
- 组件:FmsFile.vue 等文件组件负责 UI 与交互
- 数据模型
- 迁移脚本定义 bf_files 表结构,用于持久化文件元数据
```mermaid
graph TB
FE["前端 Vue<br/>fileService.js / FmsFile.vue"] --> API["后端控制器<br/>FileController"]
API --> SVC["服务层<br/>FileService"]
SVC --> CFG["配置<br/>FileStorageProperties"]
SVC --> DB["数据库<br/>bf_files"]
SVC --> OSS["对象存储网关<br/>g3oss"]
API --> FS["本地文件系统<br/>storageDir"]
```
**图表来源**
- [FileController.java:28-112](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L28-L112)
- [FileService.java:34-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L268)
- [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69)
- [application.yaml](file://fms-api/src/main/resources/application.yaml)
**章节来源**
- [FileController.java:28-112](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L28-L112)
- [FileService.java:34-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L268)
- [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69)
- [application.yaml](file://fms-api/src/main/resources/application.yaml)
## 核心组件
- FileController:对外暴露 /file 系列接口,包括:
- GET /file/config:返回当前存储类型
- POST /file/upload:本地多文件上传(FormData key: files),参数 father/moduleId/cateId
- POST /file/upload-credential:获取 OSS 直传凭证(bucket=机构码)
- POST /file/save-record:OSS 直传后回调保存文件记录
- POST /file/delete:删除文件(按 subid)
- GET /file/static/{fileName}:本地文件访问(路径含 orgId/yyyy/MM/dd/file)
- FileService:核心业务逻辑
- 本地上传:写磁盘 → 写 bf_files → 返回元数据列表
- OSS 凭证:向 g3oss 请求上传凭证
- 保存记录:将 OSS 直传后的元数据持久化
- 删除:根据存储类型删除物理文件并清理 DB 记录
- FileStorageProperties:配置映射
- fms.file.storageType:local 或 aliyun-oss
- fms.file.local.storageDir:本地存储根目录
- fms.file.local.urlPrefix:本地访问 URL 前缀
- fms.file.oss.g3ossUrl:OSS 网关地址
**章节来源**
- [FileController.java:28-112](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L28-L112)
- [FileService.java:51-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L51-L268)
- [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69)
## 架构总览
系统采用前后端分离的 REST 架构。前端通过 fileService.js 调用后端 /file 接口;后端控制器将请求委派给 FileService,后者根据配置选择本地或 OSS 存储策略,并在 bf_files 表中维护文件元数据。本地文件通过 /file/static 路由直接由控制器输出流返回。
```mermaid
sequenceDiagram
participant FE as "前端"
participant CTRL as "FileController"
participant SVC as "FileService"
participant DB as "数据库(bf_files)"
participant FS as "本地文件系统"
participant OSS as "g3oss"
FE->>CTRL : POST /file/upload (files, father, moduleId, cateId)
CTRL->>SVC : localUpload(files, father, moduleId, cateId)
loop 逐个文件
SVC->>FS : 写入 storageDir/orgId/yyyy/MM/dd/文件名
SVC->>DB : INSERT bf_files(元数据)
end
SVC-->>CTRL : 返回元数据列表
CTRL-->>FE : 成功响应
FE->>CTRL : GET /file/static/{path}
CTRL->>FS : 读取文件并输出流
FS-->>CTRL : 文件字节
CTRL-->>FE : 文件内容
```
**图表来源**
- [FileController.java:48-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L48-L111)
- [FileService.java:51-96](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L51-L96)
- [FileService.java:120-148](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L120-L148)
**章节来源**
- [FileController.java:48-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L48-L111)
- [FileService.java:51-96](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L51-L96)
- [FileService.java:120-148](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L120-L148)
## 详细组件分析
### 文件上传(本地)
- 流程要点
- 接收多文件(key: files),父级 father、模块 moduleId、分类 cateId
- 生成唯一 subid 与文件名,组织目录 orgId/yyyy/MM/dd
- 写入本地存储目录,构造 mapFilename(urlPrefix + 相对路径)
- 插入 bf_files 记录,返回包含 subid、原始名、大小、扩展名、mapFilename 等的元数据
- 安全与校验
- 使用 OrgContext.requireOrgId() 确保机构隔离
- 路径规范化与目录创建,避免非法路径
- 错误处理
- IO 异常与 SQL 异常向上抛出,由全局异常处理器统一处理
```mermaid
flowchart TD
Start(["开始"]) --> ReadFiles["读取表单 files/father/moduleId/cateId"]
ReadFiles --> ForEach{"遍历每个文件"}
ForEach --> |是| GenId["生成 subid 与文件名"]
GenId --> BuildPath["构建 orgId/yyyy/MM/dd 路径"]
BuildPath --> WriteDisk["写入本地存储目录"]
WriteDisk --> InsertDB["插入 bf_files 记录"]
InsertDB --> CollectMeta["收集元数据"]
CollectMeta --> Next{"是否还有文件"}
Next --> |是| ForEach
Next --> |否| Return["返回元数据列表"]
ForEach --> |否| Return
```
**图表来源**
- [FileController.java:48-59](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L48-L59)
- [FileService.java:51-96](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L51-L96)
- [FileService.java:120-148](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L120-L148)
**章节来源**
- [FileController.java:48-59](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L48-L59)
- [FileService.java:51-96](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L51-L96)
- [FileService.java:120-148](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L120-L148)
### 文件上传(OSS 直传)
- 流程要点
- 前端调用 /file/upload-credential 获取 g3oss 上传凭证(bucket=机构码)
- 前端直传到 g3oss,成功后回调 /file/save-record 保存记录
- 安全与校验
- 通过 OrgContext.requireOrgId() 限定 bucket
- 对 g3oss 响应状态码进行校验,失败抛业务异常
```mermaid
sequenceDiagram
participant FE as "前端"
participant CTRL as "FileController"
participant SVC as "FileService"
participant OSS as "g3oss"
FE->>CTRL : POST /file/upload-credential
CTRL->>SVC : ossCredential()
SVC->>OSS : GET /upload?orgid=&bucket=
OSS-->>SVC : 凭证信息
SVC-->>CTRL : 凭证
CTRL-->>FE : 凭证
FE->>OSS : 直传文件
FE->>CTRL : POST /file/save-record(元数据)
CTRL->>SVC : saveRecord(meta)
SVC->>DB : INSERT bf_files
SVC-->>CTRL : 保存结果
CTRL-->>FE : 成功
```
**图表来源**
- [FileController.java:61-76](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L61-L76)
- [FileService.java:159-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L159-L179)
- [FileService.java:101-118](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L101-L118)
**章节来源**
- [FileController.java:61-76](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L61-L76)
- [FileService.java:101-118](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L101-L118)
- [FileService.java:159-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L159-L179)
### 文件下载与预览
- 本地文件访问
- GET /file/static/{fileName} 直接输出文件流,设置 inline 与 UTF-8 文件名编码
- 路径穿越防护:规范化路径并校验是否在允许目录下
- 预览策略
- 浏览器可直接预览图片/PDF 等媒体类型;其他类型以附件形式下载
- 可通过前端组件控制预览行为(如弹窗预览)
```mermaid
sequenceDiagram
participant FE as "前端"
participant CTRL as "FileController"
participant FS as "本地文件系统"
FE->>CTRL : GET /file/static/{orgId/yyyy/MM/dd/file}
CTRL->>CTRL : 路径规范化与安全检查
CTRL->>FS : 读取文件
FS-->>CTRL : 文件字节
CTRL-->>FE : 文件内容inline
```
**图表来源**
- [FileController.java:89-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L89-L111)
**章节来源**
- [FileController.java:89-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L89-L111)
### 文件删除
- 流程要点
- 根据 subid 查询 bf_files 获取 mapfilename
- 依据 storageType 决定删除本地文件或调用 g3oss 删除
- 删除完成后清理 DB 记录
- 安全与校验
- 本地删除时基于 urlPrefix 解析相对路径,避免越权访问
- OSS 删除通过 g3oss 网关完成,服务端仅转发 objectName
```mermaid
flowchart TD
Start(["开始"]) --> Query["按 subid 查询 bf_files"]
Query --> HasRow{"是否存在记录"}
HasRow --> |否| End(["结束"])
HasRow --> |是| GetMap["获取 mapfilename"]
GetMap --> Type{"存储类型"}
Type --> |本地| DelLocal["删除本地文件"]
Type --> |OSS| DelOss["调用 g3oss 删除"]
DelLocal --> DelDB["删除 DB 行"]
DelOss --> DelDB
DelDB --> End
```
**图表来源**
- [FileController.java:78-87](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L78-L87)
- [FileService.java:181-259](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L181-L259)
**章节来源**
- [FileController.java:78-87](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L78-L87)
- [FileService.java:181-259](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L181-L259)
### 文件分类与元数据管理
- 元数据字段(bf_files)
- subid:主键标识
- father:父级 ID(可空)
- mx_filename:原始文件名
- mx_filesize:文件大小
- mx_fileext:扩展名
- mx_mapfilename:映射路径或 URL
- mx_moduleid:所属模块
- mx_cate_id:分类 ID
- mx_xh/mx_sh:排序与审核标记
- mx_inputuser_id/mx_inputdatetime:录入人与时间
- 分类与归属
- 通过 moduleId 与 cateId 实现文件与业务模块及分类的关联
- 通过 father 实现层级组织
**章节来源**
- [FileService.java:120-148](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L120-L148)
- [017_create_contract_file_tables.sql](file://fms-api/config/migrations/017_create_contract_file_tables.sql)
### 文件访问控制与安全机制
- 机构隔离
- 所有涉及存储的操作均基于 OrgContext.requireOrgId() 获取当前机构上下文,确保数据与存储隔离
- 路径安全
- 本地文件访问时对路径进行规范化与白名单校验,防止路径穿越
- 权限验证
- 控制器未直接实现鉴权,建议结合全局认证过滤器(如 JwtAuthFilter)在更上层进行权限校验
- 错误处理
- 网络或存储异常转换为业务异常,便于统一处理与日志记录
**章节来源**
- [FileController.java:89-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L89-L111)
- [FileService.java:159-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L159-L179)
- [FileService.java:181-259](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L181-L259)
### 大文件处理策略、断点续传与并发控制
- 当前实现
- 本地上传为逐文件写入,未实现分片与断点续传
- OSS 直传由前端直连 g3oss,服务端仅负责凭证与记录
- 建议方案
- 大文件分片上传:前端切分文件,服务端合并并校验完整性
- 断点续传:记录已上传分片索引,支持中断恢复
- 并发控制:限制单用户/单模块并发上传数,避免资源争用
- 限流与队列:引入消息队列异步处理元数据写入与后续任务
[本节为通用建议,不直接分析具体文件]
### 与前端文件组件的集成与数据流转
- 前端服务
- fileService.js 封装 /file 接口调用,统一错误处理与重试策略
- 组件集成
- FmsFile.vue 等组件负责选择文件、进度展示、预览与删除操作
- 上传流程:选择文件 → 调用 upload-credential(OSS 模式)或直接 upload(本地)→ 回调保存记录 → 刷新列表
- 数据流转
- 上传成功后返回元数据列表,组件渲染文件卡片与预览面板
- 删除操作通过 subid 触发后端删除并更新前端状态
**章节来源**
- [fileService.js](file://fms-vue/src/services/fileService.js)
- [FmsFile.vue](file://fms-vue/src/components/fms-file/FmsFile.vue)
## 依赖关系分析
- 控制器依赖服务与配置
- 服务依赖配置、数据库工具、雪花ID生成器与外部 g3oss
- 前端依赖后端 API 与文件组件
```mermaid
graph LR
CTRL["FileController"] --> SVC["FileService"]
SVC --> CFG["FileStorageProperties"]
SVC --> DBU["DbUtils/DataSource"]
SVC --> IDG["IdGenerator"]
SVC --> G3OSS["g3oss"]
FE["前端 fileService.js / FmsFile.vue"] --> CTRL
```
**图表来源**
- [FileController.java:28-112](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L28-L112)
- [FileService.java:34-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L268)
- [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69)
**章节来源**
- [FileController.java:28-112](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L28-L112)
- [FileService.java:34-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L268)
- [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69)
## 性能考虑
- 本地存储
- 合理设置 storageDir 到高性能磁盘或 SSD
- 使用 NIO 流式读写,避免一次性加载大文件到内存
- OSS 直传
- 减少服务端带宽占用,提升上传吞吐
- 合理设置超时与重试策略
- 数据库
- 批量写入与索引优化(subid、moduleId、cateId)
- 连接池与事务边界控制
- 缓存与CDN
- 对静态文件启用 CDN 加速
- 热点文件元数据缓存
[本节为通用建议,不直接分析具体文件]
## 故障排查指南
- 常见问题
- 上传失败:检查表单字段 files/father/moduleId/cateId 是否正确
- 路径访问 404:确认路径规范化与存储目录存在
- OSS 凭证失败:检查 g3ossUrl 与网络连通性
- 定位步骤
- 查看后端日志中的异常堆栈与状态码
- 核对 application.yaml 中 fms.file.* 配置
- 检查 bf_files 表记录是否完整
- 修复建议
- 修正路径与权限问题
- 调整超时与重试参数
- 增加必要索引与监控告警
**章节来源**
- [FileController.java:89-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L89-L111)
- [FileService.java:159-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L159-L179)
- [FileService.java:181-259](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L181-L259)
## 结论
该文件服务提供了简洁可靠的本地与 OSS 双模式文件管理能力,覆盖上传、下载、预览与删除等核心场景。通过配置驱动与机构隔离,满足多租户与模块化需求。建议在后续版本中增强大文件分片与断点续传、完善鉴权与审计、优化并发与性能监控,以提升用户体验与系统稳定性。
## 附录
### 配置项说明(application.yaml)
- fms.file.storageType:存储类型,取值 local 或 aliyun-oss
- fms.file.local.storageDir:本地存储根目录
- fms.file.local.urlPrefix:本地文件访问 URL 前缀
- fms.file.oss.g3ossUrl:OSS 网关地址
**章节来源**
- [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69)
- [application.yaml](file://fms-api/src/main/resources/application.yaml)
### 数据库表结构(bf_files)
- 主要字段:subid、father、mx_filename、mx_filesize、mx_fileext、mx_mapfilename、mx_moduleid、mx_cate_id、mx_xh、mx_sh、mx_inputuser_id、mx_inputdatetime
- 用途:持久化文件元数据,支撑分类、模块归属与审计
**章节来源**
- [017_create_contract_file_tables.sql](file://fms-api/config/migrations/017_create_contract_file_tables.sql)
### API 使用示例(前端)
- 获取配置:GET /file/config
- 本地上传:POST /file/upload(FormData: files, father, moduleId, cateId)
- 获取 OSS 凭证:POST /file/upload-credential
- 保存记录:POST /file/save-record(JSON: subid, father, mx_moduleid, mx_filename, mx_filesize, mx_fileext, mx_mapfilename, mx_cate_id)
- 删除文件:POST /file/delete(JSON: subid)
- 访问文件:GET /file/static/{orgId/yyyy/MM/dd/file}
**章节来源**
- [FileController.java:38-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L38-L111)
- [fileService.js](file://fms-vue/src/services/fileService.js)
@@ -0,0 +1,356 @@
# 服务层
<cite>
**本文引用的文件**
- [AuthService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java)
- [FileService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java)
- [LoginLogService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java)
- [DataSaveService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java)
- [ActiveSessionRegistry.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java)
- [FileStorageProperties.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java)
- [AuthProperties.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/AuthProperties.java)
- [application.yaml](file://fms-api/src/main/resources/application.yaml)
- [pom.xml](file://fms-api/pom.xml)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件聚焦 FMS 后端服务层的架构与实现,围绕三大业务服务展开:认证服务(AuthService)、文件服务(FileService)和登录日志服务(LoginLogService)。文档说明各服务的职责边界、设计模式、事务管理、错误处理策略、服务间依赖与调用模式,以及性能优化策略(缓存、异步、连接池),并提供扩展开发指南与最佳实践。
## 项目结构
FMS API 采用 Spring Boot 分层架构,服务层位于 service 包,配置集中在 config 包,数据访问通过 DbUtils 与 DataSource 完成,外部存储通过本地磁盘或 OSS 网关进行交互。关键入口包括:
- 认证流程:AuthService 负责用户校验、会话激活与令牌生成,并联动 LoginLogService 记录登录结果。
- 文件流程:FileService 支持本地上传与 OSS 凭证获取、删除,并通过 DataSaveService 统一持久化文件元数据。
- 通用保存:DataSaveService 提供多表批量写入、自动编号生成等能力,封装事务与异常。
```mermaid
graph TB
subgraph "服务层"
A["AuthService"]
B["FileService"]
C["LoginLogService"]
D["DataSaveService"]
E["ActiveSessionRegistry"]
end
subgraph "配置"
F["FileStorageProperties"]
G["AuthProperties"]
end
subgraph "基础设施"
H["DataSource / JDBC"]
I["HTTP 客户端(OSS 网关)"]
end
A --> E
A --> C
B --> D
B --> F
B --> I
C --> D
D --> H
A --> H
```
图表来源
- [AuthService.java:19-83](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L83)
- [FileService.java:34-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L179)
- [LoginLogService.java:21-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L21-L84)
- [DataSaveService.java:25-58](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L58)
- [FileStorageProperties.java:5-67](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L67)
- [AuthProperties.java:7-27](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/AuthProperties.java#L7-L27)
章节来源
- [application.yaml:10-33](file://fms-api/src/main/resources/application.yaml#L10-L33)
- [pom.xml:32-96](file://fms-api/pom.xml#L32-L96)
## 核心组件
- AuthService:认证与鉴权前置,负责加载用户、校验密码、签发 JWT、激活会话,并记录登录日志。
- FileService:文件上传(本地直传)、OSS 凭证获取、文件删除(本地/OSS 双路径),并将文件元数据持久化到数据库。
- LoginLogService:采集客户端信息(IP、浏览器、操作系统等),将登录事件写入 s_log_login 表,失败不影响主流程。
- DataSaveService:通用数据保存服务,提供多表批量插入/更新/删除、自动编号生成,统一事务管理与异常包装。
- ActiveSessionRegistry:内存中的活跃会话注册表,用于会话有效性校验。
- 配置类:FileStorageProperties、AuthProperties 分别承载文件存储与认证相关配置。
章节来源
- [AuthService.java:19-125](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L125)
- [FileService.java:34-267](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L267)
- [LoginLogService.java:21-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L21-L84)
- [DataSaveService.java:25-494](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L494)
- [ActiveSessionRegistry.java:8-30](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L8-L30)
- [FileStorageProperties.java:5-67](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L67)
- [AuthProperties.java:7-27](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/AuthProperties.java#L7-L27)
## 架构总览
服务层以“领域服务 + 通用基础服务”的方式组织:
- 领域服务:AuthService、FileService、LoginLogService 各自专注单一业务能力。
- 通用基础服务:DataSaveService 提供跨模块的持久化能力;DbUtils 提供表结构与列信息的元数据访问;JDBC/DataSource 提供连接与执行。
- 配置与外部依赖:通过 @ConfigurationProperties 注入配置;HTTP 客户端访问 OSS 网关;文件系统操作本地存储。
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Auth as "AuthService"
participant DB as "DbUtils/JDBC"
participant Log as "LoginLogService"
participant DS as "DataSaveService"
participant FS as "FileService"
participant OSS as "OSS 网关"
Client->>Auth : "提交登录(orgId, userId, password)"
Auth->>DB : "查询用户信息"
DB-->>Auth : "返回用户行"
Auth->>Auth : "校验密码/生成JWT/激活会话"
Auth->>Log : "记录登录成功/失败"
Log->>DS : "保存登录日志(事务)"
DS-->>Log : "完成"
Auth-->>Client : "返回 token/orgid/user"
Client->>FS : "本地上传文件"
FS->>FS : "写磁盘/构造映射路径"
FS->>DS : "保存文件元数据(事务)"
DS-->>FS : "完成"
FS-->>Client : "返回文件元数据列表"
Client->>FS : "获取 OSS 凭证"
FS->>OSS : "GET /upload?orgid=...&bucket=..."
OSS-->>FS : "返回凭证"
FS-->>Client : "返回凭证"
```
图表来源
- [AuthService.java:34-83](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L83)
- [LoginLogService.java:37-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L37-L84)
- [DataSaveService.java:34-58](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L34-L58)
- [FileService.java:54-118](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L54-L118)
- [FileService.java:163-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L163-L179)
## 详细组件分析
### 认证服务(AuthService)
- 职责:按 orgId+userId 加载用户,校验密码,生成 JWT,激活会话,记录登录日志,返回 token、orgid、user 信息。
- 设计要点:
- 使用 DbUtils 读取用户数据,避免在控制器中直接访问数据库。
- 使用 JwtUtils 生成令牌,ActiveSessionRegistry 维护活跃会话。
- 登录成功/失败均通过 LoginLogService 记录,保证审计可追溯。
- 错误处理:账号不存在、密码不匹配时返回空结果;内部字段缺失抛出非法状态异常。
- 事务:认证本身不涉及多表事务,但会触发日志写入(由 LoginLogService/DataSaveService 管理)。
```mermaid
flowchart TD
Start(["开始"]) --> Load["加载用户信息"]
Load --> Found{"找到用户?"}
Found -- 否 --> LogFail["记录登录失败(账号不存在)"]
LogFail --> ReturnEmpty["返回空"]
Found -- 是 --> CheckPwd["校验密码"]
CheckPwd --> PwdOk{"密码正确?"}
PwdOk -- 否 --> LogFail2["记录登录失败(密码错误)"]
LogFail2 --> ReturnEmpty
PwdOk -- 是 --> GenToken["生成JWT/激活会话"]
GenToken --> LogSuccess["记录登录成功"]
LogSuccess --> ReturnOK["返回 token/orgid/user"]
```
图表来源
- [AuthService.java:34-83](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L83)
- [AuthService.java:115-124](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L115-L124)
章节来源
- [AuthService.java:19-125](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L125)
- [ActiveSessionRegistry.java:8-30](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L8-L30)
- [AuthProperties.java:7-27](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/AuthProperties.java#L7-L27)
### 文件服务(FileService)
- 职责:本地上传、OSS 凭证获取、文件删除(本地/OSS),并将文件元数据持久化到 bf_files。
- 设计要点:
- 本地上传:按机构码/年/月/日组织目录,生成唯一文件名,写入磁盘后插入数据库。
- OSS 模式:通过 HTTP 客户端调用 g3oss 获取上传凭证;删除时根据 URL 解析 objectName 并调用删除接口。
- 持久化:使用 DataSourceUtils 获取连接,手动释放;或通过 DataSaveService 统一保存。
- 错误处理:OSS 调用失败抛业务异常;文件不存在时删除逻辑安全跳过。
- 事务:insertFileRecord/deleteDbRow 使用独立连接;若需跨多个表一致性,建议通过 DataSaveService 的事务包裹。
```mermaid
classDiagram
class FileService {
+localUpload(files, father, moduleId, cateId) Map[]
+saveRecord(meta) Map
+ossCredential() Map
+delete(subid) void
-insertFileRecord(...)
-deleteLocalFile(mapfilename)
-deleteOssFile(mapfilename)
-deleteDbRow(subid)
}
class FileStorageProperties {
+storageType
+local
+oss
}
class DataSaveService {
+save(requests) void
}
class DataSource
FileService --> FileStorageProperties : "读取配置"
FileService --> DataSaveService : "可选地保存元数据"
FileService --> DataSource : "获取连接"
```
图表来源
- [FileService.java:34-267](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L267)
- [FileStorageProperties.java:5-67](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L67)
- [DataSaveService.java:25-58](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L58)
章节来源
- [FileService.java:34-267](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L267)
- [FileStorageProperties.java:5-67](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L67)
### 登录日志服务(LoginLogService)
- 职责:采集客户端信息(IP、浏览器、操作系统、设备类型等),将登录事件写入 s_log_login 表。
- 设计要点:
- 通过 ClientInfoExtractor 提取请求上下文信息。
- 使用 IdGenerator 生成主键,组装 row 并通过 DataSaveService.save 写入。
- 使用 OrgContext 临时设置机构上下文,确保在多租户环境下写入正确的库。
- 错误处理:写入失败仅记录日志,不影响登录主流程,体现“非关键路径降级”。
```mermaid
sequenceDiagram
participant Auth as "AuthService"
participant Log as "LoginLogService"
participant Ext as "ClientInfoExtractor"
participant DS as "DataSaveService"
Auth->>Log : "recordLogin(orgId, account, success, ...)"
Log->>Ext : "extract(request)"
Ext-->>Log : "ClientInfo"
Log->>DS : "save([{table : s_log_login, inserts : [row]}])"
DS-->>Log : "完成"
Log-->>Auth : "完成(忽略异常)"
```
图表来源
- [LoginLogService.java:37-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L37-L84)
- [DataSaveService.java:34-58](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L34-L58)
章节来源
- [LoginLogService.java:21-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L21-L84)
### 通用数据保存服务(DataSaveService)
- 职责:提供统一的 save 方法,支持多表批量插入/更新/删除;提供 nextCode 自动生成编码。
- 事务管理:
- save 方法内开启事务,逐表执行 delete/update/insert,任一失败回滚。
- nextCode 方法内对规则表加锁读取并更新当前周期值,保证并发安全。
- 元数据缓存:在同一事务内缓存表元数据,减少重复查询。
- 错误处理:将 SQL 异常包装为 SaveObjectException,便于上层定位具体表与操作。
```mermaid
flowchart TD
S["进入 save(requests)"] --> T["获取连接并关闭自动提交"]
T --> Loop{"遍历每个表请求"}
Loop --> |是| Exec["执行 delete/update/insert"]
Exec --> Next{"是否还有表"}
Next --> |是| Loop
Next --> |否| Commit["提交事务"]
Commit --> End["结束"]
Loop --> |异常| Rollback["回滚并抛出异常"]
```
图表来源
- [DataSaveService.java:34-58](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L34-L58)
- [DataSaveService.java:381-464](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L381-L464)
章节来源
- [DataSaveService.java:25-494](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L494)
## 依赖关系分析
- 服务耦合:
- AuthService 依赖 DbUtils、JwtUtils、ActiveSessionRegistry、LoginLogService。
- FileService 依赖 FileStorageProperties、DbUtils、DataSource、HTTP 客户端。
- LoginLogService 依赖 DataSaveService、ClientInfoExtractor。
- 外部依赖:
- 数据库:通过 DataSource/JDBC 访问,连接池参数在 application.yaml 中配置。
- OSS:通过 HTTP 客户端调用 g3oss 网关,超时与错误处理在 FileService 中实现。
- 工具库:JWT、UA Parser、Snowflake ID 等。
```mermaid
graph LR
AuthService --> DbUtils
AuthService --> JwtUtils
AuthService --> ActiveSessionRegistry
AuthService --> LoginLogService
FileService --> FileStorageProperties
FileService --> DbUtils
FileService --> DataSource
FileService --> HttpClient
LoginLogService --> DataSaveService
DataSaveService --> DataSource
```
图表来源
- [AuthService.java:19-32](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L32)
- [FileService.java:34-49](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L49)
- [LoginLogService.java:21-35](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L21-L35)
- [DataSaveService.java:25-33](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L25-L33)
章节来源
- [pom.xml:32-96](file://fms-api/pom.xml#L32-L96)
- [application.yaml:10-33](file://fms-api/src/main/resources/application.yaml#L10-L33)
## 性能考虑
- 连接池管理:
- 使用 Druid 连接池,初始大小与最小空闲为 0,最大活动连接 10,等待超时 5s,启用空闲检测与借出检查,适合低并发场景快速启动与资源回收。
- 建议在更高并发下提升 max-active 并根据压测调整 min-idle 与验证策略。
- 缓存机制:
- DataSaveService 在同一事务内缓存表元数据,避免重复查询系统表。
- ActiveSessionRegistry 使用 ConcurrentHashMap 维护活跃会话,O(1) 查找。
- 异步处理:
- 登录日志写入失败不影响主流程,具备天然降级;可进一步引入消息队列异步落盘以提升吞吐。
- 外部调用:
- HTTP 客户端设置连接超时 10s,避免阻塞;OSS 删除/凭证获取失败抛业务异常,便于上层重试或告警。
- 文件存储:
- 本地存储按日期分目录,降低单目录文件数量;OSS 模式通过网关解耦存储细节。
[本节为通用指导,无需特定文件引用]
## 故障排查指南
- 登录失败:
- 检查账号是否存在、密码是否正确;查看 LoginLogService 记录的失败原因。
- 确认 JwtUtils 配置(密钥、过期时间)与 ActiveSessionRegistry 状态。
- 文件上传失败:
- 本地模式:检查 storage-dir 权限与磁盘空间;确认 url-prefix 与静态资源映射一致。
- OSS 模式:检查 g3oss-url 可达性与凭证返回;关注 HTTP 状态码与异常信息。
- 数据保存异常:
- 查看 DataSaveService 抛出的 SaveObjectException,定位具体表与操作索引。
- 检查自动编号规则与唯一索引配置。
- 连接池问题:
- 监控连接占用与等待时间;必要时调整 pool 参数。
章节来源
- [LoginLogService.java:75-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L75-L84)
- [FileService.java:163-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L163-L179)
- [DataSaveService.java:486-494](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/DataSaveService.java#L486-L494)
- [application.yaml:14-26](file://fms-api/src/main/resources/application.yaml#L14-L26)
## 结论
FMS 服务层以清晰的分层与职责划分实现了认证、文件与日志的核心能力。通过 DataSaveService 统一事务与异常处理,提升了数据一致性与可维护性;通过 ActiveSessionRegistry 与配置化存储策略,兼顾了性能与扩展性。建议在高并发场景下进一步优化连接池、引入异步日志与缓存策略,并完善对外部依赖的重试与熔断机制。
[本节为总结,无需特定文件引用]
## 附录
- 配置项说明:
- fms.auth.jwt-secret:JWT 签名密钥,默认开发值,生产环境务必替换。
- fms.auth.jwt-expiration:JWT 过期时间,默认 24h。
- fms.database.pool.*:Druid 连接池参数,可按压测结果调优。
- fms.file.storage-type:存储类型 local 或 oss。
- fms.file.local.storage-dir:本地文件存储根目录。
- fms.file.local.url-prefix:静态资源访问前缀。
- fms.file.oss.g3oss-url:OSS 网关地址。
章节来源
- [application.yaml:10-33](file://fms-api/src/main/resources/application.yaml#L10-L33)
- [AuthProperties.java:7-27](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/AuthProperties.java#L7-L27)
- [FileStorageProperties.java:5-67](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L67)
@@ -0,0 +1,349 @@
# 登录日志服务
<cite>
**本文引用的文件**
- [LoginLogService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java)
- [ClientInfoExtractor.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfoExtractor.java)
- [ClientInfo.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfo.java)
- [IpRegionSearcher.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/IpRegionSearcher.java)
- [003_create_login_log.sql](file://fms-api/config/migrations/003_create_login_log.sql)
- [LoginLogWriteTest.java](file://.tmp-fkcheck/LoginLogWriteTest.java)
- [LogRebuild.java](file://.tmp-fkcheck/LogRebuild.java)
- [index.vue(旧前端占位)](file://fms-vue-old2/src/views/system/login-log/index.vue)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录:查询与使用示例](#附录:查询与使用示例)
## 简介
本文件面向登录审计能力,围绕 LoginLogService 的登录日志记录流程进行系统化说明。内容涵盖:
- 登录成功与失败的捕获机制
- IP 地址解析与地理位置识别
- 客户端信息提取(浏览器、操作系统、设备类型、User-Agent)
- 用户行为追踪与安全事件监控
- 日志数据模型、存储结构与索引设计
- 日志清理策略、性能优化与数据隐私保护建议
- 查询接口与使用示例(安全审计、故障排查)
## 项目结构
登录日志功能主要位于后端 API 模块中,包含服务层、工具层与数据库迁移脚本;前端提供占位页面用于后续扩展。
```mermaid
graph TB
subgraph "后端"
A["LoginLogService<br/>记录登录日志"]
B["ClientInfoExtractor<br/>提取客户端环境"]
C["IpRegionSearcher<br/>IP归属地查询"]
D["DataSaveService<br/>通用写入接口"]
E["s_log_login 表<br/>迁移脚本定义"]
end
subgraph "前端"
F["login-log 页面占位"]
end
A --> B
B --> C
A --> D
D --> E
F -.->|"待对接"| A
```
图表来源
- [LoginLogService.java:17-85](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L17-L85)
- [ClientInfoExtractor.java:10-61](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfoExtractor.java#L10-L61)
- [IpRegionSearcher.java:15-64](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/IpRegionSearcher.java#L15-L64)
- [003_create_login_log.sql:17-43](file://fms-api/config/migrations/003_create_login_log.sql#L17-L43)
章节来源
- [LoginLogService.java:17-85](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L17-L85)
- [003_create_login_log.sql:1-46](file://fms-api/config/migrations/003_create_login_log.sql#L1-L46)
## 核心组件
- 登录日志服务:负责在登录成功或失败时采集并持久化日志,写失败不影响主流程。
- 客户端信息提取器:从请求中提取 IP、User-Agent,解析浏览器/系统/设备类型,并查询 IP 归属地。
- IP 归属地查询器:基于本地 ip2region xdb 内存化查询,快速返回国家/省/市/运营商或“内网”“未知”。
- 数据保存服务:通过通用 saveobjt 机制将日志写入 s_log_login 表。
- 数据模型:s_log_login 表定义了完整的登录审计字段及索引。
章节来源
- [LoginLogService.java:17-85](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L17-L85)
- [ClientInfoExtractor.java:10-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfoExtractor.java#L10-L111)
- [IpRegionSearcher.java:15-115](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/IpRegionSearcher.java#L15-L115)
- [003_create_login_log.sql:17-43](file://fms-api/config/migrations/003_create_login_log.sql#L17-L43)
## 架构总览
登录日志写入的关键调用链如下:
```mermaid
sequenceDiagram
participant Auth as "认证流程"
participant Svc as "LoginLogService"
participant Ext as "ClientInfoExtractor"
participant Geo as "IpRegionSearcher"
participant Save as "DataSaveService"
participant DB as "s_log_login"
Auth->>Svc : recordLogin(orgId, account, userId, success, failReason, sessionId, request)
Svc->>Ext : extract(request)
Ext->>Geo : search(ip)
Geo-->>Ext : 归属地
Ext-->>Svc : ClientInfo
Svc->>Save : save([{table : "s_log_login", key_field : "b_id", inserts : [row]}])
Save->>DB : INSERT INTO s_log_login (...)
DB-->>Save : 成功/失败
Save-->>Svc : 结果
Note over Svc,DB : 写入失败仅记录错误日志,不中断登录主流程
```
图表来源
- [LoginLogService.java:37-83](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L37-L83)
- [ClientInfoExtractor.java:27-61](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfoExtractor.java#L27-L61)
- [IpRegionSearcher.java:47-64](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/IpRegionSearcher.java#L47-L64)
- [003_create_login_log.sql:17-43](file://fms-api/config/migrations/003_create_login_log.sql#L17-L43)
## 详细组件分析
### 登录日志服务(LoginLogService)
- 职责
- 在登录成功或失败时统一记录日志。
- 设置机构上下文后执行写入,确保多租户隔离。
- 异常捕获与降级:写入失败仅记录错误日志,不影响登录主流程。
- 关键逻辑
- 参数校验:无有效 orgId 直接返回。
- 客户端信息采集:调用 ClientInfoExtractor 获取 IP、浏览器、系统、设备类型等。
- 行数据组装:生成唯一 ID、时间戳、登录结果、失败原因、会话标识等。
- 通用写入:通过 DataSaveService 以 table/key_field/inserts 形式插入 s_log_login。
- 上下文管理:finally 中清理 OrgContext,避免上下文泄漏。
```mermaid
flowchart TD
Start(["进入 recordLogin"]) --> CheckOrg{"orgId 是否有效?"}
CheckOrg -- 否 --> ExitNoop["直接返回"]
CheckOrg -- 是 --> Extract["提取客户端信息"]
Extract --> BuildRow["组装日志行数据"]
BuildRow --> SetOrg["设置机构上下文"]
SetOrg --> Save["调用 DataSaveService 写入 s_log_login"]
Save --> Finally["清理上下文"]
Finally --> End(["结束"])
Save --> |异常| LogErr["记录错误日志"] --> Finally
```
图表来源
- [LoginLogService.java:37-83](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L37-L83)
章节来源
- [LoginLogService.java:17-85](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L17-L85)
### 客户端信息提取器(ClientInfoExtractor)
- 职责
- 从 HttpServletRequest 提取真实 IP、User-Agent。
- 解析浏览器名称与版本、操作系统名称与版本、设备类型。
- 调用 IpRegionSearcher 获取 IP 归属地。
- 关键点
- 空请求降级:返回默认值,保证健壮性。
- UA 解析容错:解析异常仅记录警告,不影响整体流程。
- 设备类型推导:根据设备族与 UA 关键字推断 Mobile/Tablet/Desktop/Bot。
```mermaid
classDiagram
class ClientInfo {
+String ip
+String ipLocation
+String browser
+String browserVersion
+String os
+String osVersion
+String deviceType
+String userAgent
}
class ClientInfoExtractor {
-Parser uaParser
-IpRegionSearcher ipRegionSearcher
+extract(request) ClientInfo
-deriveDeviceType(family, ua) String
-buildVersion(major, minor, patch) String
}
ClientInfoExtractor --> ClientInfo : "返回"
```
图表来源
- [ClientInfoExtractor.java:10-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfoExtractor.java#L10-L111)
- [ClientInfo.java:1-15](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfo.java#L1-L15)
章节来源
- [ClientInfoExtractor.java:10-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfoExtractor.java#L10-L111)
- [ClientInfo.java:1-15](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfo.java#L1-L15)
### IP 归属地查询器(IpRegionSearcher)
- 职责
- 启动时加载 ip2region.xdb 到内存,避免每次 IO。
- 查询 IP 归属地,支持 IPv4/IPv6 内网判断,返回“内网”或“未知”作为降级。
- 关键点
- 初始化失败降级:记录错误日志并将 searcher 置空,后续查询返回“未知”。
- 内网判定:覆盖常见私有段与回环地址,减少误判。
```mermaid
flowchart TD
Init(["启动 init"]) --> Load["读取 ip2region.xdb 到内存"]
Load --> Ready{"初始化成功?"}
Ready -- 否 --> Fallback["searcher=null"]
Ready -- 是 --> Done["就绪"]
Done --> Query["search(ip)"]
Query --> CheckNull{"ip 为空?"}
CheckNull -- 是 --> RetUnknown["返回'未知'"]
CheckNull -- 否 --> CheckInternal{"是否内网?"}
CheckInternal -- 是 --> RetIntranet["返回'内网'"]
CheckInternal -- 否 --> DoSearch["调用 searcher.search(ip)"]
DoSearch --> RetResult{"结果非空?"}
RetResult -- 是 --> RetRegion["返回区域字符串"]
RetResult -- 否 --> RetUnknown
```
图表来源
- [IpRegionSearcher.java:26-64](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/IpRegionSearcher.java#L26-L64)
- [IpRegionSearcher.java:66-113](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/IpRegionSearcher.java#L66-L113)
章节来源
- [IpRegionSearcher.java:15-115](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/IpRegionSearcher.java#L15-L115)
### 数据模型与存储结构(s_log_login)
- 表名与用途
- 表名:s_log_login(系统日志表,区别于业务表 b_)。
- 用途:记录每次登录尝试(成功/失败)的客户端环境与结果,用于登录审计。
- 关键字段
- 主键:b_id(雪花ID)
- 用户与账号:b_user_id、b_account
- 登录类型:b_login_type(如 password)
- 结果与原因:b_login_result(1/0)、b_fail_reason
- 网络与环境:b_ip、b_ip_location、b_browser、b_browser_version、b_os、b_os_version、b_device_type、b_user_agent
- 会话与链路:b_session_id、b_request_id、b_trace_id
- 时间:b_occurdatetime(UTC)
- 索引设计
- IX_s_log_login_user:按用户+时间倒序,便于用户登录历史查询
- IX_s_log_login_account:按账号+时间倒序,便于账号维度统计
- IX_s_log_login_result:按结果+时间倒序,便于失败率统计
```mermaid
erDiagram
S_LOG_LOGIN {
bigint b_id PK
bigint b_user_id
varchar b_account
varchar b_login_type
int b_login_result
nvarchar b_fail_reason
varchar b_ip
nvarchar b_ip_location
varchar b_browser
varchar b_browser_version
varchar b_os
varchar b_os_version
varchar b_device_type
nvarchar b_user_agent
varchar b_session_id
varchar b_request_id
varchar b_trace_id
datetime2 b_occurdatetime
}
```
图表来源
- [003_create_login_log.sql:17-43](file://fms-api/config/migrations/003_create_login_log.sql#L17-L43)
章节来源
- [003_create_login_log.sql:1-46](file://fms-api/config/migrations/003_create_login_log.sql#L1-L46)
## 依赖关系分析
- 组件耦合
- LoginLogService 依赖 DataSaveService 与 ClientInfoExtractor,低耦合、高内聚。
- ClientInfoExtractor 依赖 IpRegionSearcher 与 UA 解析库,职责单一。
- IpRegionSearcher 依赖 ip2region 库,启动时加载资源,运行时零 IO。
- 外部依赖
- 通用数据保存接口(saveobjt),屏蔽底层 SQL 细节。
- 雪花 ID 生成器,保证全局唯一主键。
- 潜在循环依赖
- 当前为单向依赖,未发现循环引用。
```mermaid
graph LR
LoginLogService --> ClientInfoExtractor
ClientInfoExtractor --> IpRegionSearcher
LoginLogService --> DataSaveService
DataSaveService --> s_log_login
```
图表来源
- [LoginLogService.java:29-35](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L29-L35)
- [ClientInfoExtractor.java:18-25](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfoExtractor.java#L18-L25)
- [003_create_login_log.sql:17-43](file://fms-api/config/migrations/003_create_login_log.sql#L17-L43)
章节来源
- [LoginLogService.java:17-85](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L17-L85)
- [ClientInfoExtractor.java:10-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/ClientInfoExtractor.java#L10-L111)
- [IpRegionSearcher.java:15-115](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/IpRegionSearcher.java#L15-L115)
## 性能考虑
- 写入路径
- 日志写入采用异步友好的通用接口,且异常被吞掉,避免阻塞登录主流程。
- 建议在高频场景下对 DataSaveService 做批处理或消息队列落盘(可选优化)。
- 客户端信息解析
- UA 解析器复用单例 Parser,避免重复编译正则带来的开销。
- 解析异常降级为警告,不影响主流程。
- IP 归属地查询
- 启动时加载 xdb 至内存,查询为 O(1)~O(logN) 级别,极低延迟。
- 内网地址直接短路返回,减少不必要查询。
- 索引与查询
- 已针对用户、账号、结果+时间建立复合索引,利于分页与统计。
- 建议对 b_occurdatetime 单独建索引以支持大范围时间范围扫描(视实际查询而定)。
- 存储与清理
- 建议按时间分区或归档策略定期清理历史日志,控制表体积。
- 可结合冷热分层(热数据 SSD、冷数据对象存储)降低成本。
[本节为通用指导,无需特定文件来源]
## 故障排查指南
- 常见问题定位
- 未写入日志:检查 orgId 是否为空;查看 DataSaveService 异常日志。
- IP 归属地为“未知”:确认 ip2region.xdb 是否存在于 classpath;查看初始化日志。
- UA 解析失败:检查 User-Agent 是否为空或畸形;查看警告日志。
- 验证与测试
- 可使用独立测试类模拟写入与回读,验证列结构与索引效果。
- 重建脚本可用于恢复表结构与索引,确保一致性。
章节来源
- [LoginLogWriteTest.java:1-55](file://.tmp-fkcheck/LoginLogWriteTest.java#L1-L55)
- [LogRebuild.java:25-50](file://.tmp-fkcheck/LogRebuild.java#L25-L50)
## 结论
登录日志服务以最小侵入方式实现登录审计,具备以下特点:
- 稳定可靠:写失败不阻断登录主流程,异常降级完善。
- 信息丰富:覆盖 IP、归属地、浏览器/系统/设备类型、UA、会话与链路标识。
- 易于查询:完善的索引设计与清晰的字段语义,支撑安全审计与故障排查。
- 可扩展:通用数据保存接口便于接入更多审计场景。
[本节为总结,无需特定文件来源]
## 附录:查询与使用示例
- 安全审计场景
- 统计某用户最近 7 天登录失败次数与失败原因分布。
- 统计某账号在不同 IP/地点的登录情况,识别异常异地登录。
- 按设备类型与浏览器维度分析登录来源,辅助风控策略。
- 故障排查场景
- 定位某次登录失败的具体时间与客户端环境,复现问题。
- 对比同一账号在不同设备的登录结果,排查兼容性问题。
- 查询建议
- 使用 b_user_id/b_account + b_occurdatetime 组合条件进行分页查询。
- 使用 b_login_result 过滤成功/失败记录,结合时间窗口分析趋势。
- 前端展示
- 当前前端 login-log 页面为占位视图,后续可对接通用数据加载接口进行只读展示。
章节来源
- [index.vue(旧前端占位):1-5](file://fms-vue-old2/src/views/system/login-log/index.vue#L1-L5)
- [003_create_login_log.sql:41-43](file://fms-api/config/migrations/003_create_login_log.sql#L41-L43)
@@ -0,0 +1,351 @@
# 认证服务
<cite>
**本文引用的文件**
- [AuthService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java)
- [JwtUtils.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java)
- [ActiveSessionRegistry.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java)
- [DbUtils.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java)
- [LoginLogService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向认证服务的后端实现,聚焦 AuthService 的核心能力:用户登录验证、JWT 令牌生成与管理、会话状态管理;并说明密码校验机制、多租户(orgId)处理、用户信息获取流程。同时描述与 JwtUtils、ActiveSessionRegistry 的集成方式,涵盖令牌生命周期管理与活跃会话监控。文末提供完整的登录流程示例(含错误处理与日志记录)、安全最佳实践与性能优化建议。
## 项目结构
认证相关代码位于 fms-api 模块中,采用分层组织:
- service 层:AuthService(登录编排)、ActiveSessionRegistry(活跃会话注册表)、LoginLogService(登录日志)
- utils 层:JwtUtils(JWT 签发与解析)、DbUtils(数据库访问封装)
- 配置与上下文:AuthProperties(由 JwtUtils 注入,用于密钥与过期时间)、OrgContext(由 LoginLogService 使用以设置多租户上下文)
```mermaid
graph TB
subgraph "服务层"
A["AuthService"]
B["ActiveSessionRegistry"]
C["LoginLogService"]
end
subgraph "工具层"
D["JwtUtils"]
E["DbUtils"]
end
A --> D
A --> B
A --> C
A --> E
```
图表来源
- [AuthService.java:19-33](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L33)
- [JwtUtils.java:18-26](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L26)
- [ActiveSessionRegistry.java:8-15](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L8-L15)
- [DbUtils.java:27-42](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L27-L42)
- [LoginLogService.java:21-35](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L21-L35)
章节来源
- [AuthService.java:19-33](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L33)
- [JwtUtils.java:18-26](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L26)
- [ActiveSessionRegistry.java:8-15](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L8-L15)
- [DbUtils.java:27-42](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L27-L42)
- [LoginLogService.java:21-35](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L21-L35)
## 核心组件
- AuthService:登录入口,负责参数归一化、用户查询、密码校验、令牌签发、会话激活与日志记录。
- JwtUtils:基于配置的 HMAC-SHA 密钥签发 JWT,包含 orgid、sessionId 等声明,并提供解析方法。
- ActiveSessionRegistry:内存中的活跃会话注册表,按 orgId+userId 维度维护当前有效 sessionId。
- DbUtils:通过 OrgDataSourceManager 按 orgId 获取数据源,执行只读查询,提供 loadUserForAuthentication。
- LoginLogService:采集客户端信息并写入 s_log_login 表,失败不影响主流程。
章节来源
- [AuthService.java:34-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L84)
- [JwtUtils.java:45-70](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L45-L70)
- [ActiveSessionRegistry.java:13-26](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L13-L26)
- [DbUtils.java:164-178](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L164-L178)
- [LoginLogService.java:37-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L37-L84)
## 架构总览
下图展示一次完整登录请求在系统内的调用链路与数据流向。
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Auth as "AuthService"
participant DB as "DbUtils"
participant JWT as "JwtUtils"
participant Sess as "ActiveSessionRegistry"
participant Log as "LoginLogService"
Client->>Auth : "提交登录(orgId, userId, password, request)"
Auth->>DB : "loadUserForAuthentication(orgId, userId)"
DB-->>Auth : "用户记录(可能为空)"
alt "未找到用户"
Auth->>Log : "recordLogin(失败 : 账号不存在)"
Auth-->>Client : "空结果"
else "找到用户"
Auth->>Auth : "passwordMatches(比对存储密码)"
alt "密码不匹配"
Auth->>Log : "recordLogin(失败 : 密码错误)"
Auth-->>Client : "空结果"
else "密码匹配"
Auth->>JWT : "generateToken(userId, orgId, sessionId)"
JWT-->>Auth : "token"
Auth->>Sess : "activate(orgId, userId, sessionId)"
Auth->>Log : "recordLogin(成功, sessionId)"
Auth-->>Client : "{token, orgid, user}"
end
end
```
图表来源
- [AuthService.java:34-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L84)
- [DbUtils.java:164-178](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L164-L178)
- [JwtUtils.java:45-57](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L45-L57)
- [ActiveSessionRegistry.java:13-15](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L13-L15)
- [LoginLogService.java:37-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L37-L84)
## 详细组件分析
### AuthService 登录流程与多租户支持
- 输入归一化:orgId 去空白并转大写;userId 去空白。
- 多租户数据源:通过 DbUtils.loadUserForAuthentication(orgId, account) 从指定租户库查询 b_user 表,仅启用用户(b_canuse=1)。
- 密码校验:使用 MessageDigest.isEqual 进行常量时间比较,避免时序攻击。
- 令牌签发:调用 JwtUtils.generateToken 生成包含 userId、orgId、sessionId 的 JWT。
- 会话激活:将 orgId+userId -> sessionId 映射写入 ActiveSessionRegistry。
- 日志记录:无论成功或失败,均调用 LoginLogService.recordLogin 记录登录结果与原因。
- 返回内容:成功时返回 token、orgid 与精简用户信息(id、account、name)。
```mermaid
flowchart TD
Start(["开始"]) --> Normalize["归一化 orgId/userId"]
Normalize --> Query["查询用户(DbUtils)"]
Query --> Found{"是否找到用户?"}
Found -- 否 --> LogFail1["记录失败: 账号不存在"] --> ReturnEmpty["返回空"]
Found -- 是 --> CheckPwd["密码比对(MessageDigest.isEqual)"]
CheckPwd --> PwdOk{"密码正确?"}
PwdOk -- 否 --> LogFail2["记录失败: 密码错误"] --> ReturnEmpty
PwdOk -- 是 --> GenToken["生成JWT(JwtUtils)"]
GenToken --> Activate["激活会话(ActiveSessionRegistry)"]
Activate --> LogSuccess["记录成功(含sessionId)"]
LogSuccess --> BuildResp["组装响应(token, orgid, user)"]
BuildResp --> End(["结束"])
```
图表来源
- [AuthService.java:34-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L84)
- [DbUtils.java:164-178](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L164-L178)
- [JwtUtils.java:45-57](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L45-L57)
- [ActiveSessionRegistry.java:13-15](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L13-L15)
- [LoginLogService.java:37-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L37-L84)
章节来源
- [AuthService.java:34-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L84)
- [AuthService.java:115-124](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L115-L124)
- [DbUtils.java:164-178](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L164-L178)
### JwtUtils 令牌生命周期管理
- 初始化:从配置读取 jwtSecret 与 jwtExpiration,校验长度与有效性后构建签名密钥与解析器。
- 签发:生成 issuedAt、expiresAt,携带 subject(userId)、id(sessionId)、claim(orgid),并使用 HMAC-SHA 签名。
- 解析:解析出 userId、orgId、sessionId,缺失则抛出异常。
- 提取 orgId:便捷方法直接返回 claim.orgid。
```mermaid
classDiagram
class JwtUtils {
-SecretKey signingKey
-Duration expiration
-JwtParser jwtParser
+init() void
+generateToken(userId, orgId, sessionId) String
+parseToken(token) TokenClaims
+getOrgId(token) String
}
class TokenClaims {
+String userId
+String orgId
+String sessionId
}
JwtUtils --> TokenClaims : "返回"
```
图表来源
- [JwtUtils.java:18-43](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L43)
- [JwtUtils.java:45-80](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L45-L80)
章节来源
- [JwtUtils.java:18-43](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L43)
- [JwtUtils.java:45-80](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L45-L80)
### ActiveSessionRegistry 活跃会话监控
- 数据结构:ConcurrentHashMap 保存 AccountKey(orgId, userId) -> sessionId。
- activate:覆盖式更新当前有效会话。
- isActive:校验传入 sessionId 是否与当前有效会话一致。
- hasActiveSession:判断是否存在任何活跃会话。
```mermaid
classDiagram
class ActiveSessionRegistry {
-Map~AccountKey,String~ activeSessions
+activate(orgId, userId, sessionId) void
+isActive(orgId, userId, sessionId) boolean
+hasActiveSession(orgId, userId) boolean
}
class AccountKey {
+String orgId
+String userId
}
ActiveSessionRegistry --> AccountKey : "使用"
```
图表来源
- [ActiveSessionRegistry.java:8-29](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L8-L29)
章节来源
- [ActiveSessionRegistry.java:8-29](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L8-L29)
### DbUtils 多租户数据访问
- 多租户连接:通过 OrgDataSourceManager.getConnection(orgId) 获取租户专属连接。
- 认证查询:loadUserForAuthentication 使用预编译语句查询 b_user,过滤 b_canuse=1,防止 SQL 注入。
- 安全策略:对动态 SQL 进行只读校验与标识符白名单校验,确保查询安全。
章节来源
- [DbUtils.java:164-178](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L164-L178)
- [DbUtils.java:440-459](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L440-L459)
### LoginLogService 登录日志
- 数据采集:通过 ClientInfoExtractor 提取 IP、浏览器、操作系统等信息。
- 写入表:构造行数据并通过 DataSaveService.saveobjt 机制写入 s_log_login。
- 多租户上下文:临时设置 OrgContext.setOrgId(orgId),finally 清理,保证线程安全。
- 容错设计:写日志失败不影响登录主流程,仅记录错误日志。
章节来源
- [LoginLogService.java:37-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L37-L84)
## 依赖关系分析
- AuthService 依赖 DbUtils(用户查询)、JwtUtils(令牌签发)、ActiveSessionRegistry(会话管理)、LoginLogService(审计日志)。
- JwtUtils 依赖配置 AuthProperties(密钥与有效期),使用 JJWT 库进行签名与解析。
- DbUtils 依赖 OrgDataSourceManager(多租户数据源)与 Spring JDBC。
- LoginLogService 依赖 DataSaveService(通用写入)与 ClientInfoExtractor(客户端信息)。
```mermaid
graph LR
Auth["AuthService"] --> DB["DbUtils"]
Auth --> JWT["JwtUtils"]
Auth --> REG["ActiveSessionRegistry"]
Auth --> LOG["LoginLogService"]
JWT --> CFG["AuthProperties"]
DB --> ODSM["OrgDataSourceManager"]
LOG --> DS["DataSaveService"]
LOG --> CIE["ClientInfoExtractor"]
```
图表来源
- [AuthService.java:19-33](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L33)
- [JwtUtils.java:18-26](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L26)
- [DbUtils.java:27-42](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L27-L42)
- [LoginLogService.java:21-35](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L21-L35)
章节来源
- [AuthService.java:19-33](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L19-L33)
- [JwtUtils.java:18-26](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L18-L26)
- [DbUtils.java:27-42](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L27-L42)
- [LoginLogService.java:21-35](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L21-L35)
## 性能考虑
- 数据库访问
- 使用预编译语句与只读查询,减少 SQL 注入风险与解析开销。
- 多租户数据源隔离,避免跨租户干扰。
- 密码校验
- 使用常量时间比较避免时序攻击,代价为固定耗时,适合小量字符串。
- 令牌签发
- 每次登录生成新 sessionId,便于单点登出与会话追踪;注意合理设置过期时间。
- 会话注册表
- 基于 ConcurrentHashMap 的内存表,读写高效;但重启会丢失,需结合持久化或网关侧缓存扩展。
- 日志写入
- 异步或失败降级策略已内置(异常捕获),避免阻塞登录主路径。
[本节为通用性能讨论,不直接分析具体文件]
## 故障排查指南
- 账号不存在
- 现象:登录返回空结果。
- 检查:DbUtils.loadUserForAuthentication 是否返回空;确认 orgId 大小写与空格归一化是否正确。
- 日志:LoginLogService 记录“账号不存在”。
- 密码错误
- 现象:登录返回空结果。
- 检查:passwordMatches 比对逻辑;确认数据库中存储的密码格式与前端提交一致。
- 日志:LoginLogService 记录“密码错误”。
- JWT 配置错误
- 现象:应用启动失败或令牌解析异常。
- 检查:AuthProperties.jwtSecret 长度不少于 32 字节;jwtExpiration 必须大于 0。
- 异常:JwtUtils 初始化或解析时抛出非法参数异常。
- 会话不一致
- 现象:后续请求被拒绝或需要重新登录。
- 检查:ActiveSessionRegistry.isActive 是否返回 false;确认 sessionId 与 activate 时一致。
- 日志写入失败
- 现象:登录成功但无登录日志。
- 检查:DataSaveService 与 OrgContext 设置/清理是否正常;关注错误日志输出。
章节来源
- [AuthService.java:47-61](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L47-L61)
- [JwtUtils.java:28-43](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L28-L43)
- [JwtUtils.java:59-77](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L59-L77)
- [ActiveSessionRegistry.java:17-22](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/ActiveSessionRegistry.java#L17-L22)
- [LoginLogService.java:75-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L75-L84)
## 结论
AuthService 提供了清晰、安全的登录流程:多租户数据隔离、常量时间密码比对、JWT 令牌签发与会话注册、完善的登录审计。JwtUtils 与 ActiveSessionRegistry 分别承担令牌生命周期与会话状态管理职责,DbUtils 保障只读与安全的数据库访问,LoginLogService 提供可观测性。整体设计兼顾安全性与可维护性,建议在部署时严格配置 JWT 密钥与有效期,并结合网关或中间件实现令牌校验与会话续期策略。
[本节为总结性内容,不直接分析具体文件]
## 附录
### 完整登录流程示例(含错误处理与日志)
- 正常流程
- 客户端提交 orgId、userId、password 与 HTTP 请求头。
- 服务端归一化参数,查询用户,校验密码,签发 JWT,激活会话,记录成功日志,返回 token、orgid、user。
- 异常分支
- 账号不存在:记录失败日志并返回空。
- 密码错误:记录失败日志并返回空。
- JWT 配置错误:启动阶段抛异常,阻止运行。
- 日志写入失败:不影响登录,仅记录错误日志。
章节来源
- [AuthService.java:34-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L34-L84)
- [JwtUtils.java:28-43](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L28-L43)
- [LoginLogService.java:37-84](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/LoginLogService.java#L37-L84)
### 安全最佳实践
- 使用强密钥与合理过期时间:jwtSecret 长度不少于 32 字节;jwtExpiration 设置为合理值。
- 常量时间密码比较:避免时序攻击。
- 多租户隔离:通过 orgId 选择数据源,避免越权访问。
- 只读查询与标识符校验:防止 SQL 注入与恶意操作。
- 会话唯一性:每次登录生成新 sessionId,便于单点登出与审计。
章节来源
- [JwtUtils.java:28-43](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/JwtUtils.java#L28-L43)
- [AuthService.java:115-124](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/AuthService.java#L115-L124)
- [DbUtils.java:440-459](file://fms-api/src/main/java/cn/g3soft/fmsapi/utils/DbUtils.java#L440-L459)
### 性能优化策略
- 数据库层面
- 使用预编译语句与最小字段集查询。
- 合理索引 b_account、b_canuse 以提升查询效率。
- 令牌与会话
- 根据业务需求调整 JWT 过期时间,平衡安全与用户体验。
- 若需跨节点共享会话,可将 ActiveSessionRegistry 替换为分布式缓存(如 Redis)。
- 日志与可观测性
- 保持日志写入非阻塞,必要时引入异步队列。
- 对高频登录场景进行限流与防刷策略。
[本节为通用优化建议,不直接分析具体文件]