# 数据流与模块交互详图
> 本文档补充系统各模块之间的数据流转细节和交互时序。
---
## 一、资源管理全链路数据流
### 1.1 资源 CRUD 完整数据流
```mermaid
sequenceDiagram
participant U as 用户
participant V as ResourceList.vue
participant A as api/resource.js
participant C as ResourceController
participant S as ResourceServiceImpl
participant M as ResourceMapper
participant DB as MySQL
Note over U,DB: ── 分页查询 ──
U->>V: 输入筛选条件 / 翻页
V->>A: resourcePageApi(params)
A->>C: GET /resource/page?keyword=&typeId=&status=&page=&size=
C->>S: page(query)
S->>M: selectPage + 条件构造
M->>DB: SELECT * FROM resource WHERE ...
DB-->>M: 分页结果
S->>S: 批量组装 VO
(关联类型名、服务商名、负责人名、关联项目)
S-->>C: IPage
C-->>A: Result.ok(data)
A-->>V: 渲染表格
Note over U,DB: ── 新增资源 ──
U->>V: 点击"新增",填写表单
V->>A: resourceSaveApi(dto)
A->>C: POST /resource {name, typeId, providerId, ...}
C->>S: save(dto)
S->>S: DTO → Entity 转换
S->>S: 计算初始状态
(根据 expire_date)
S->>M: insert(resource)
M->>DB: INSERT INTO resource
S->>S: 关联项目 + 设置分摊比例
S->>M: batch insert project_resource
S->>S: 记录操作日志
S-->>C: 成功
C-->>A: Result.ok()
A-->>V: 关闭弹窗,刷新列表
Note over U,DB: ── 编辑资源 ──
U->>V: 点击"编辑"
V->>A: resourceDetailApi(id)
A->>C: GET /resource/{id}
C->>S: getDetail(id)
S->>M: selectById + 关联查询
M->>DB: SELECT ...
S-->>C: ResourceVO
C-->>A: Result.ok(data)
A-->>V: 回填表单
U->>V: 修改后提交
V->>A: resourceSaveApi(dto)
A->>C: POST /resource {id, name, ...}
C->>S: save(dto)
S->>M: updateById
M->>DB: UPDATE resource SET ...
S->>S: 更新项目关联 + 分摊比例
S-->>C: 成功
C-->>A: Result.ok()
Note over U,DB: ── 删除资源 ──
U->>V: 点击"删除",确认
V->>A: resourceDeleteApi(id)
A->>C: DELETE /resource/{id}
C->>S: delete(id)
S->>M: deleteById (逻辑删除)
M->>DB: UPDATE resource SET deleted=1
S->>M: 删除关联的 project_resource
S->>S: 记录操作日志
S-->>C: 成功
C-->>A: Result.ok()
```
### 1.2 资源导出 Excel 数据流
```mermaid
sequenceDiagram
participant U as 用户
participant V as ResourceList.vue
participant A as api/resource.js
participant C as ResourceController
participant S as ResourceServiceImpl
participant EE as EasyExcel
participant DB as MySQL
U->>V: 点击"导出 Excel"
V->>A: resourceExportApi(params)
A->>C: GET /resource/export?keyword=&typeId=&status=
C->>S: list(query)
S->>DB: 查询全量数据(不分页)
DB-->>S: List
S->>S: Entity → ResourceExportVO
(状态转文字、类型名、服务商名)
S-->>C: List
C->>EE: EasyExcel.write(response, ResourceExportVO.class)
EE->>C: 生成 .xlsx 字节流
C-->>A: Content-Type: application/octet-stream
Content-Disposition: attachment
A-->>V: Blob 响应
V->>V: 创建下载链接
触发浏览器下载
V-->>U: 文件保存对话框
```
---
## 二、项目管理数据流
### 2.1 项目树形查询
```mermaid
sequenceDiagram
participant V as ProjectList.vue
participant A as api/project.js
participant C as ProjectController
participant S as ProjectServiceImpl
participant DB as MySQL
V->>A: projectTreeApi()
A->>C: GET /project/tree
C->>S: tree()
S->>DB: SELECT * FROM project WHERE deleted=0
DB-->>S: List
S->>S: 分离顶层/子项目
S->>S: 构建树形结构:
顶层项目 → children[]
loop 每个顶层项目
S->>DB: 查询关联资源数
S->>DB: 查询子项目列表
loop 每个子项目
S->>DB: 查询子项目关联资源数
end
end
S-->>C: List (树形)
C-->>A: Result.ok(data)
A-->>V: el-table tree 模式渲染
```
### 2.2 项目详情与资源关联
```mermaid
sequenceDiagram
participant U as 用户
participant V as ProjectDetail.vue
participant A as api/project.js
participant RA as api/resource.js
participant C as ProjectController
participant S as ProjectServiceImpl
participant DB as MySQL
U->>V: 进入项目详情页
V->>A: projectDetailApi(id)
A->>C: GET /project/{id}
C->>S: getDetail(id)
S->>DB: 查询项目 + 子项目 + 关联资源
DB-->>S: 聚合数据
S-->>C: ProjectVO
C-->>A: Result.ok(data)
A-->>V: 渲染基本信息 + 资源列表
U->>V: 点击"关联资源"
V->>RA: resourcePageApi(未关联筛选)
RA-->>V: 可选资源列表
U->>V: 多选资源 + 设置分摊比例
V->>A: POST /resource/relate
Note over V,A: {resourceIds: [1,2,3],
projectId: 5,
allocations: [{resourceId:1, percent:50},...]}
A->>C: POST /resource/relate
C->>S: 批量插入 project_resource
S->>DB: INSERT INTO project_resource
DB-->>S: 成功
S-->>C: 成功
C-->>A: Result.ok()
A-->>V: 刷新资源列表
```
---
## 三、费用统计数据流
### 3.1 按项目维度统计
```mermaid
flowchart LR
subgraph 查询
Q["GET /resource/cost-stat/project
?days=30"]
end
subgraph SQL["ResourceMapper.xml"]
SQL1["SELECT
p.name AS projectName,
COUNT(DISTINCT r.id) AS resourceCount,
SUM(r.cost * pr.allocation_percent / 100) AS totalCost
FROM project p
LEFT JOIN project_resource pr ON ...
LEFT JOIN resource r ON ...
GROUP BY p.id"]
end
subgraph 结果
R1["CostStatVO
{projectName, resourceCount, totalCost}"]
end
Q --> SQL1
SQL1 --> R1
```
### 3.2 采购费用台账数据流
```mermaid
flowchart TD
subgraph 数据来源
R["resource 表
初始采购记录"]
RR["resource_renewal 表
续费记录"]
end
subgraph 合并逻辑["ResourceServiceImpl.mergeProcurementCosts()"]
Q["ProcurementCostQuery
名称/类型/服务商/费用类型/月份"]
Merge["合并初始采购 + 续费记录
UNION ALL"]
Filter["按条件筛选"]
Page["分页"]
end
subgraph 输出
VO["ProcurementCostVO
资源名、类型、服务商、项目、占比、
分摊费用、计费周期、购买时长、
类型标签(初始采购/续费)、
发生日期、到期日期"]
end
R --> Merge
RR --> Merge
Q --> Filter
Merge --> Filter
Filter --> Page
Page --> VO
```
---
## 四、认证授权数据流
### 4.1 JWT Token 生命周期
```mermaid
sequenceDiagram
participant C as 客户端
participant F as JwtAuthenticationFilter
participant J as JwtUtil
participant US as UserDetailsServiceImpl
participant DB as MySQL
Note over C,DB: ── Token 生成 ──
C->>F: POST /auth/login
F->>US: loadUserByUsername()
US->>DB: SELECT * FROM sys_user WHERE username=?
DB-->>US: SysUser
US-->>F: LoginUser (UserDetails)
F->>J: generateToken(userId, username)
J->>J: HS256 签名
payload: {userId, username, iat, exp}
J-->>F: JWT String
F-->>C: {token, userId, username, role}
Note over C,DB: ── Token 校验 (每次请求) ──
C->>F: GET /resource/page
Authorization: Bearer eyJhbG...
F->>F: 提取 Header 中的 Token
F->>J: parseToken(token)
J->>J: 验签 + 检查过期
alt Token 有效
J-->>F: Claims {userId, username}
F->>US: loadUserByUsername()
US->>DB: 查询用户
DB-->>US: SysUser
US-->>F: LoginUser
F->>F: SecurityContextHolder.setContext()
F-->>F: chain.doFilter() → Controller
else Token 过期
J-->>F: ExpiredJwtException
F-->>C: 401 Unauthorized
else Token 无效
J-->>F: SignatureException
F-->>C: 401 Unauthorized
end
```
### 4.2 登录安全防护
```mermaid
flowchart TD
Login["登录请求"] --> CheckUser{"用户存在?"}
CheckUser -->|否| Fail1["返回: 账号或密码错误"]
CheckUser -->|是| CheckStatus{"账号已启用?"}
CheckStatus -->|否| Fail2["返回: 账号已禁用"]
CheckStatus -->|是| CheckLock{"15 分钟内失败 ≥ 5 次?"}
CheckLock -->|是| Fail3["返回: 账号已锁定"]
CheckLock -->|否| CheckPwd{"密码正确?"}
CheckPwd -->|否| IncFail["失败计数 + 1"]
IncFail --> Fail4["返回: 密码错误"]
CheckPwd -->|是| ResetFail["清除失败计数"]
ResetFail --> GenToken["生成 JWT Token"]
GenToken --> Log["记录登录日志"]
Log --> Success["返回 token + 用户信息"]
```
---
## 五、定时任务数据流
### 5.1 资源状态刷新 + 到期提醒
```mermaid
flowchart TD
subgraph 定时触发
Cron["cron: 0 0 8 * * ?
每天 08:00"]
end
subgraph 阶段一["阶段一:状态刷新"]
Cron --> Refresh["refreshExpireStatus()"]
Refresh --> Q1["查询所有未停用资源"]
Q1 --> Loop1{"遍历每个资源"}
Loop1 --> Calc["计算 daysUntilExpiry
= expire_date - today"]
Calc --> S1{"expire_date < today?"}
S1 -->|是| Set1["status = 3 (已过期)"]
S1 -->|否| S2{"daysUntilExpiry ≤ 30?"}
S2 -->|是| Set2["status = 2 (即将到期)"]
S2 -->|否| Set3["status = 1 (使用中)"]
Set1 --> Update["UPDATE resource SET status"]
Set2 --> Update
Set3 --> Update
Update --> Loop1
end
subgraph 阶段二["阶段二:邮件提醒"]
Update --> Scan["扫描需要提醒的资源"]
Scan --> Q2["查询 status IN (2, 3) 的资源"]
Q2 --> CheckLog{"查 expire_notify_log
是否已发过?"}
CheckLog -->|已发| Skip["跳过"]
CheckLog -->|未发| Stage{"判断阶段"}
Stage -->|≤30天 >7天| Far["stage=far
普通提醒"]
Stage -->|≤7天 >0天| Near["stage=near
紧急提醒"]
Stage -->|已过期| Expired["stage=expired
过期通知"]
Far --> Send["发送邮件给负责人"]
Near --> Send
Expired --> Send
Send --> LogNotify["INSERT expire_notify_log"]
end
```
---
## 六、前端状态管理数据流
### 6.1 用户状态管理 (Pinia)
```mermaid
flowchart TD
subgraph 登录流程
Input["输入账号密码"] --> CallLogin["userStore.login()"]
CallLogin --> API["loginApi() → POST /auth/login"]
API --> Resp["响应: {token, userId, role}"]
Resp --> SetToken["setToken(token)"]
SetToken --> LS["localStorage.setItem('token', ...)"]
SetToken --> State["state.token = token"]
State --> FetchMe["fetchMe() → GET /auth/me"]
FetchMe --> SetInfo["state.userInfo = {...}"]
SetInfo --> Redirect["router.push(redirect || '/dashboard')"]
end
subgraph 请求拦截
Req["Axios 请求"] --> GetToken["localStorage.getItem('token')"]
GetToken --> AddHeader["headers.Authorization = 'Bearer ' + token"]
AddHeader --> Send["发送请求"]
end
subgraph 响应拦截
Res["Axios 响应"] --> CheckCode{"code === 200?"}
CheckCode -->|否| Error["ElMessage.error(msg)"]
CheckCode -->|是| Return["返回 data"]
Error --> Check401{"code === 401?"}
Check401 -->|是| ClearToken["clearToken()"]
ClearToken --> GoLogin["router.push('/login')"]
end
```
### 6.2 路由守卫流程
```mermaid
flowchart TD
Nav["用户导航"] --> Guard["router.beforeEach()"]
Guard --> HasToken{"localStorage
有 token?"}
HasToken -->|是| IsLogin{"目标是 /login?"}
IsLogin -->|是| Redirect["→ /dashboard
(已登录不显示登录页)"]
IsLogin -->|否| Allow["放行"]
HasToken -->|否| IsPublic{"是公开页?
(/login)"}
IsPublic -->|是| Allow
IsPublic -->|否| ToLogin["→ /login?redirect=当前路径"]
```
---
## 七、模块间交互矩阵
```mermaid
graph TD
subgraph 资源模块
Res["Resource"]
Renew["ResourceRenewal"]
Expire["ExpireNotify"]
end
subgraph 项目模块
Proj["Project"]
ProjRes["ProjectResource"]
end
subgraph 用户模块
User["SysUser"]
Auth["Auth/JWT"]
end
subgraph 系统模块
Log["OperationLog"]
Dict["ResourceType/Provider"]
Dash["Dashboard"]
end
Res -->|owner_id| User
Res -->|type_id| Dict
Res -->|provider_id| Dict
Res -->|续费| Renew
Res -->|关联| ProjRes
Res -->|到期提醒| Expire
Res -->|操作日志| Log
Proj -->|owner_id| User
Proj -->|父子| Proj
Proj -->|关联| ProjRes
ProjRes -->|project_id| Proj
ProjRes -->|resource_id| Res
Renew -->|resource_id| Res
Renew -->|operator_id| User
Expire -->|resource_id| Res
Log -->|user_id| User
Dash -->|统计| Res
Dash -->|统计| Proj
Auth -->|认证| User
```
---
## 八、关键设计决策
| 决策 | 方案 | 理由 |
|------|------|------|
| 认证方式 | JWT (无状态) | 前后端分离,无需服务端 session |
| 密码存储 | BCrypt 哈希 | Spring Security 内置支持,安全性高 |
| 登录锁定 | 内存计数 + 15 分钟过期 | 简单有效,防暴力破解 |
| 项目结构 | 两层(顶层 + 子项目) | 满足业务需求,避免过深嵌套 |
| 费用分摊 | project_resource 表存储百分比 | 灵活支持均分/自定义,尾差修正 |
| 续费记录 | 独立表,不回写主表金额 | 保留完整历史,支持删除回滚 |
| 到期提醒 | 三阶段 + 日志表去重 | 避免重复发送,提醒有梯度 |
| 资源状态 | 定时任务刷新 + 操作时即时刷新 | 双保险,数据一致性 |
| 逻辑删除 | MyBatis-Plus @TableLogic | 数据可恢复,审计友好 |
| 配置分离 | dev/prod profile | 开发便利 + 生产安全 |
| 敏感信息 | 环境变量注入 | 不进代码仓库 |
| Excel 导出 | EasyExcel | 内存友好,大数据量不 OOM |
| 前端图表 | ECharts + useChart 封装 | 主题切换支持,代码复用 |
| 前端状态 | Pinia (Composition API) | Vue 3 官方推荐,类型安全 |