基于项目源码自动生成,所有图表均使用 Mermaid 语法,可在支持 Mermaid 的 Markdown 编辑器中渲染。
graph TB
subgraph 用户层
Browser["浏览器<br/>(Chrome / Edge)"]
end
subgraph 前端["前端 · Vue 3 + Vite"]
Router["Vue Router<br/>路由守卫 / 权限控制"]
Views["页面组件<br/>Dashboard / Resource / Project ..."]
Store["Pinia Store<br/>用户状态 / Token"]
API["Axios API 层<br/>request.js 统一拦截"]
end
subgraph 反向代理["Nginx 反向代理"]
Static["静态资源托管<br/>dist/"]
Proxy["/api/ → :8080<br/>API 反代"]
end
subgraph 后端["后端 · Spring Boot 3"]
Controller["Controller 层<br/>REST 接口"]
Security["Spring Security<br/>JWT 认证 + 权限"]
Service["Service 层<br/>核心业务逻辑"]
Mapper["Mapper 层<br/>MyBatis-Plus"]
Task["定时任务<br/>ResourceExpireTask"]
Mail["邮件服务<br/>EmailService"]
end
subgraph 数据层
MySQL["MySQL 8.0+<br/>resource_platform"]
SMTP["SMTP 邮件服务器"]
end
Browser -->|HTTP| 反向代理
反向代理 -->|静态文件| Static
反向代理 -->|/api/*| Controller
Router --> Views
Views --> Store
Views --> API
API -->|Bearer Token| Controller
Controller --> Security
Security --> Service
Service --> Mapper
Mapper --> MySQL
Task --> Service
Service --> Mail
Mail --> SMTP
graph LR
subgraph 接入层
A["AuthController"]
RC["ResourceController"]
PC["ProjectController"]
DC["DashboardController"]
UC["UserController"]
RTC["ResourceTypeController"]
PRC["ProviderController"]
LC["OperationLogController"]
end
subgraph 安全层
JWT["JwtAuthenticationFilter"]
SC["SecurityConfig<br/>CORS / 权限规则"]
SU["SecurityUtils"]
end
subgraph 业务层
RS["ResourceServiceImpl"]
PS["ProjectServiceImpl"]
US["SysUserServiceImpl"]
RRS["ResourceRenewalServiceImpl"]
DS["DashboardServiceImpl"]
OLS["OperationLogServiceImpl"]
ES["EmailServiceImpl"]
end
subgraph 数据层
RM["ResourceMapper"]
PM["ProjectMapper"]
UM["SysUserMapper"]
RRM["ResourceRenewalMapper"]
PRM["ProjectResourceMapper"]
OLM["OperationLogMapper"]
ENLM["ExpireNotifyLogMapper"]
end
A --> US
RC --> RS
RC --> RRS
PC --> PS
DC --> DS
UC --> US
RTC --> RS
PRC --> RS
LC --> OLS
RS --> RM
RS --> PRM
PS --> PM
PS --> PRM
US --> UM
RRS --> RRM
RRS --> RM
DS --> RM
DS --> PM
OLS --> OLM
RS --> OLS
ES --> SMTP["SMTP"]
JWT --> SC
erDiagram
sys_user ||--o{ resource : "负责"
sys_user ||--o{ project : "负责"
sys_user ||--o{ operation_log : "操作"
project ||--o{ project : "父子(两层)"
project ||--o{ project_resource : "关联"
resource ||--o{ project_resource : "关联"
resource ||--o{ resource_renewal : "续费"
resource ||--o{ expire_notify_log : "提醒"
resource }o--|| resource_type : "类型"
resource }o--|| provider : "服务商"
sys_user {
int id PK
varchar username
varchar password
varchar real_name
varchar email
int role "1=管理员 2=普通"
int status "0=禁用 1=启用"
}
resource {
int id PK
varchar name
int type_id FK
int provider_id FK
int owner_id FK
date start_date
date expire_date
decimal cost
int cost_cycle "1=月 2=年 3=一次性"
int status "0停用 1使用中 2即将到期 3已过期"
int purchase_duration
varchar account_info
varchar remark
}
project {
int id PK
varchar name
int parent_id FK "NULL=顶层"
int owner_id FK
int status "0=下线 1=进行中"
varchar remark
}
project_resource {
int id PK
int project_id FK
int resource_id FK
decimal allocation_percent "分摊占比"
}
resource_type {
int id PK
varchar name
int sort
varchar remark
}
provider {
int id PK
varchar name
varchar remark
}
resource_renewal {
int id PK
int resource_id FK
date old_expire_date
date new_expire_date
int duration_code
decimal cost
varchar remark
int operator_id FK
}
expire_notify_log {
int id PK
int resource_id FK
varchar stage "far/near/expired"
date notify_date
varchar send_to
}
operation_log {
int id PK
int user_id FK
varchar module
varchar operation
varchar detail
}
| 关系 | 类型 | 说明 |
|---|---|---|
| resource ↔ resource_type | 多对一 | 每个资源属于一个类型 |
| resource ↔ provider | 多对一 | 每个资源属于一个服务商 |
| resource ↔ sys_user (owner) | 多对一 | 每个资源有一个负责人 |
| resource ↔ project | 多对多 | 通过 project_resource 关联,支持费用分摊 |
| project_resource.allocation_percent | 分摊占比 | 同一资源在不同项目间按比例分摊费用 |
| project ↔ project (parent) | 自关联 | 两层结构:顶层项目 + 子项目 |
| resource ↔ resource_renewal | 一对多 | 一个资源可有多条续费记录 |
| resource ↔ expire_notify_log | 一对多 | 三阶段提醒记录(far/near/expired) |
sequenceDiagram
participant U as 浏览器
participant F as 前端 (Vue)
participant S as 后端 (Spring Boot)
participant DB as MySQL
U->>F: 输入用户名/密码
F->>S: POST /auth/login {username, password}
S->>DB: 查询用户信息
DB-->>S: sys_user 记录
alt 账号不存在 / 已禁用
S-->>F: {code: 401, message: "账号不存在或已禁用"}
end
alt 连续失败 ≥ 5 次(15 分钟内)
S-->>F: {code: 401, message: "账号已锁定,请 15 分钟后重试"}
end
alt 密码错误
S->>DB: 记录失败次数
S-->>F: {code: 401, message: "密码错误"}
end
alt 认证成功
S->>S: 生成 JWT Token (HS256, 24h)
S->>DB: 清除失败计数
S-->>F: {code: 200, data: {token, userId, username, role}}
F->>F: 存储 token 到 localStorage
F->>F: Pinia store 更新用户状态
F-->>U: 跳转至 Dashboard
end
Note over U,DB: 后续请求
U->>F: 操作页面
F->>S: 请求头 Authorization: Bearer <token>
S->>S: JwtAuthenticationFilter 校验 Token
alt Token 有效
S->>S: 设置 SecurityContext
S-->>F: 返回业务数据
else Token 过期/无效
S-->>F: 401 Unauthorized
F->>F: 清除 token,跳转登录页
end
graph TD
Login["用户登录"] --> JWT["获取 JWT Token"]
JWT --> Filter["JwtAuthenticationFilter"]
Filter -->|每次请求| Check{"Token 有效?"}
Check -->|否| Reject["401 → 跳转登录"]
Check -->|是| Load["加载 LoginUser<br/>(role, status)"]
Load --> SecConfig["SecurityConfig 规则匹配"]
SecConfig --> R1["/auth/login → 公开"]
SecConfig --> R2["/user/enabled-list → 登录即可"]
SecConfig --> R3["/user/** → ROLE_ADMIN"]
SecConfig --> R4["POST/DELETE /resource-type/** → ROLE_ADMIN"]
SecConfig --> R5["POST/DELETE /provider/** → ROLE_ADMIN"]
SecConfig --> R6["其他接口 → 登录即可"]
R1 --> Pass["放行"]
R2 --> Pass
R3 --> Admin{"isAdmin?"}
R4 --> Admin
R5 --> Admin
R6 --> Pass
Admin -->|是| Pass
Admin -->|否| Deny["403 Forbidden"]
stateDiagram-v2
[*] --> 使用中: 创建资源<br/>(expire_date > 今天)
[*] --> 已过期: 创建资源<br/>(expire_date ≤ 今天)
使用中 --> 即将到期: 定时任务扫描<br/>(距到期 ≤ 30 天)
即将到期 --> 即将到期: 定时任务扫描<br/>(距到期 ≤ 7 天)
即将到期 --> 已过期: 超过到期日
使用中 --> 已过期: 超过到期日
已过期 --> 使用中: 续费<br/>(新到期日 > 今天)
已过期 --> 即将到期: 续费<br/>(新到期日在 30 天内)
使用中 --> 已停用: 手动停用
即将到期 --> 已停用: 手动停用
已停用 --> 使用中: 重新启用<br/>(更新到期日)
使用中 --> [*]: 删除资源
即将到期 --> [*]: 删除资源
已过期 --> [*]: 删除资源
已停用 --> [*]: 删除资源
sequenceDiagram
participant U as 用户
participant F as 前端
participant C as ResourceController
participant RRS as ResourceRenewalService
participant RS as ResourceService
participant DB as MySQL
U->>F: 点击"续费",填写续费表单
Note over U,F: 续费日期、新到期日、时长、金额、备注
F->>C: POST /resource/{id}/renew
C->>RRS: renewResource(id, dto)
RRS->>DB: 查询当前资源信息
DB-->>RRS: Resource 记录
RRS->>RRS: 校验:新到期日 > 当前到期日
alt 校验失败
RRS-->>C: 抛出 BusinessException
C-->>F: {code: 400, message: "新到期日必须晚于当前到期日"}
end
RRS->>DB: INSERT resource_renewal
RRS->>DB: UPDATE resource.expire_date = 新到期日
RRS->>RS: 刷新资源状态
RS->>DB: 根据新到期日计算 status
Note over RS,DB: 使用中 / 即将到期
RRS-->>C: 续费成功
C->>C: 记录操作日志
C-->>F: {code: 200}
F->>F: 刷新详情页 + 续费历史列表
flowchart TD
Start["定时触发<br/>每天 08:00<br/>cron: 0 0 8 * * ?"] --> Refresh["刷新所有资源状态<br/>refreshExpireStatus()"]
Refresh --> Scan["扫描需要提醒的资源"]
Scan --> Check30{"距到期 ≤ 30 天<br/>且 > 7 天?"}
Scan --> Check7{"距到期 ≤ 7 天<br/>且 > 0 天?"}
Scan --> CheckExpired{"已过期<br/>expire_date < 今天?"}
Check30 -->|是| Log30{"已发过 far 提醒?"}
Check30 -->|否| Skip1["跳过"]
Check7 -->|是| Log7{"已发过 near 提醒?"}
Check7 -->|否| Skip2["跳过"]
CheckExpired -->|是| LogExp{"已发过 expired 提醒?"}
CheckExpired -->|否| Skip3["跳过"]
Log30 -->|未发| Send30["发送邮件<br/>stage=far<br/>普通提醒"]
Log30 -->|已发| Skip1
Log7 -->|未发| Send7["发送邮件<br/>stage=near<br/>紧急提醒"]
Log7 -->|已发| Skip2
LogExp -->|未发| SendExp["发送邮件<br/>stage=expired<br/>过期通知"]
LogExp -->|已发| Skip3
Send30 --> Record30["INSERT expire_notify_log<br/>stage=far"]
Send7 --> Record7["INSERT expire_notify_log<br/>stage=near"]
SendExp --> RecordExp["INSERT expire_notify_log<br/>stage=expired"]
Record30 --> End["完成"]
Record7 --> End
RecordExp --> End
Skip1 --> End
Skip2 --> End
Skip3 --> End
flowchart TD
Start["资源关联多个项目"] --> Input["用户选择关联项目"]
Input --> Mode{"分摊模式"}
Mode -->|均分| Calc1["每个项目 = 100% / N<br/>尾差加到最后一个"]
Mode -->|自定义| Calc2["用户手动输入各占比<br/>合计必须 = 100%"]
Calc1 --> Save["保存到 project_resource<br/>allocation_percent"]
Calc2 --> Validate{"合计 = 100%?"}
Validate -->|否| Error["提示:分摊占比合计必须为 100%"]
Validate -->|是| Save
Save --> Stat["费用统计时"]
Stat --> Formula["分摊费用 = 资源总费用 ×<br/>allocation_percent / 100"]
Formula --> Report["生成费用报表<br/>按项目 / 按类型"]
graph TD
subgraph 两层项目模型
P1["顶层项目 A"]
P1 --> C1["子项目 A-1"]
P1 --> C2["子项目 A-2"]
P2["顶层项目 B"]
P2 --> C3["子项目 B-1"]
P3["顶层项目 C<br/>(无子项目)"]
end
C1 --> R1["资源 1"]
C1 --> R2["资源 2"]
C2 --> R3["资源 3"]
C3 --> R4["资源 4"]
P3 --> R5["资源 5"]
style P1 fill:#409eff,color:#fff
style P2 fill:#409eff,color:#fff
style P3 fill:#409eff,color:#fff
style C1 fill:#67c23a,color:#fff
style C2 fill:#67c23a,color:#fff
style C3 fill:#67c23a,color:#fff
规则:资源只挂在叶子节点(子项目或无子项目的顶层项目)。父项目的资源统计 = 所有子项目资源的聚合。
flowchart TD
Start["用户点击新增/编辑"] --> Form["弹窗表单"]
Form --> Fields["名称 / 父项目 / 负责人 / 状态 / 备注"]
Fields --> Submit["提交"]
Submit --> Check{"有父项目?"}
Check -->|是| Leaf["标记为子项目<br/>parent_id = 选中值"]
Check -->|否| Root["标记为顶层项目<br/>parent_id = NULL"]
Leaf --> Save["保存到 project 表"]
Root --> Save
Save --> Edit{"编辑模式?"}
Edit -->|是| Update["UPDATE + 保留子项目关系"]
Edit -->|否| Insert["INSERT"]
Update --> Done["刷新列表"]
Insert --> Done
graph TD
App["App.vue"] --> Login["/login<br/>Login.vue"]
App --> Layout["Layout.vue<br/>侧边栏 + 顶栏"]
Layout --> Dashboard["/dashboard<br/>工作台"]
Layout --> Resource["/resource<br/>资源台账"]
Layout --> ResourceDetail["/resource/:id<br/>资源详情"]
Layout --> Project["/project<br/>项目管理"]
Layout --> ProjectDetail["/project/:id<br/>项目详情"]
Layout --> CostStat["/cost-stat<br/>费用统计"]
Layout --> ProcurementCost["/procurement-cost<br/>采购费用"]
Layout --> Dict["/dict<br/>字典管理"]
Layout --> User["/user<br/>用户管理 🔒"]
Layout --> Log["/log<br/>操作日志"]
Dashboard --> KPI["6 个 KPI 卡片"]
Dashboard --> Chart1["项目状态环形图"]
Dashboard --> Chart2["资源类型分布条形图"]
Dashboard --> Table["风险清单表格"]
CostStat --> Chart3["资源类型费用环形图"]
CostStat --> Chart4["项目预算消耗排行"]
CostStat --> Chart5["各类型费用条形图"]
CostStat --> Chart6["项目资源数对比"]
Dict --> Tab1["资源类型 Tab"]
Dict --> Tab2["服务商 Tab"]
style User fill:#e6a23c,color:#fff
graph LR
subgraph 页面组件
RL["ResourceList"]
RF["ResourceForm"]
RD["ResourceDetail"]
PL["ProjectList"]
PD["ProjectDetail"]
DSH["Dashboard"]
CS["CostStat"]
PC["ProcurementCost"]
end
subgraph 组合式函数
UC["useChart()<br/>ECharts 封装"]
USR["useScrollRestore()<br/>滚动恢复"]
end
subgraph 工具
CSV["exportCsv()"]
CC["getChartColors()"]
SC["statusClass()"]
end
subgraph API
RA["api/resource"]
PA["api/project"]
DA["api/dashboard"]
end
subgraph Store
ST["store/user"]
end
RL --> RF
RL --> RA
RL --> SC
RD --> RA
PL --> PA
PD --> PA
DSH --> DA
DSH --> UC
DSH --> CSV
CS --> RA
CS --> UC
CS --> CSV
PC --> RA
PC --> CSV
UC --> CC
RL --> USR
graph TB
subgraph 客户端
Browser2["浏览器"]
end
subgraph Nginx["Nginx (resource.mizentech.com:80)"]
Frontend["前端静态资源<br/>/opt/resource-platform/frontend/dist/"]
APIProxy["/api/ 反代<br/>→ 127.0.0.1:8080"]
end
subgraph Backend["Spring Boot (127.0.0.1:8080)"]
App["resource-platform.jar<br/>--spring.profiles.active=prod"]
end
subgraph Data["数据层"]
RDS["MySQL 8.0+<br/>resource_platform"]
Mail2["SMTP 邮件服务器<br/>smtp.qiye.aliyun.com:465"]
end
subgraph 配置
Env[".env (chmod 600)<br/>DB_URL / JWT_SECRET<br/>MAIL_* / CORS_ORIGINS"]
Systemd["systemd 服务<br/>自动重启 / 开机自启"]
end
Browser2 -->|HTTP| Nginx
Nginx -->|/| Frontend
Nginx -->|/api/*| APIProxy
APIProxy --> App
App --> RDS
App --> Mail2
Env --> App
Systemd --> App
graph LR
subgraph 开发环境
Dev["application-dev.yml"]
DevDB["阿里云 RDS<br/>明文密码"]
DevMail["smtp.example.com<br/>占位配置"]
DevLog["日志级别: DEBUG"]
DevCORS["CORS: localhost:5173"]
end
subgraph 生产环境
Prod["application-prod.yml"]
ProdDB["环境变量注入<br/>DB_URL / DB_PASSWORD"]
ProdMail["smtp.qiye.aliyun.com<br/>企业邮箱"]
ProdLog["日志级别: INFO"]
ProdCORS["CORS: resource.mizentech.com"]
end
Dev --> DevDB
Dev --> DevMail
Dev --> DevLog
Dev --> DevCORS
Prod --> ProdDB
Prod --> ProdMail
Prod --> ProdLog
Prod --> ProdCORS
graph TD
subgraph 认证模块
A1["POST /auth/login"]
A2["GET /auth/me"]
A3["PUT /auth/password"]
end
subgraph 资源管理
R1["GET /resource/page"]
R2["GET /resource/{id}"]
R3["POST /resource"]
R4["DELETE /resource/{id}"]
R5["POST /resource/relate"]
R6["DELETE /resource/unrelate"]
R7["POST /resource/{id}/renew"]
R8["GET /resource/{id}/renewals"]
R9["DELETE /resource/{id}/renewals/{rid}"]
R10["GET /resource/export"]
end
subgraph 费用统计
C1["GET /resource/cost-stat/project"]
C2["GET /resource/cost-stat/type"]
C3["GET /resource/procurement-cost"]
C4["GET /resource/procurement-cost/export"]
end
subgraph 项目管理
P1["GET /project/page"]
P2["GET /project/tree"]
P3["GET /project/{id}"]
P4["POST /project"]
P5["DELETE /project/{id}"]
P6["GET /project/active-list"]
P7["GET /project/parent-options"]
end
subgraph 工作台
D1["GET /dashboard"]
end
subgraph 系统管理 🔒
U1["GET /user/list"]
U2["GET /user/enabled-list"]
U3["POST /user"]
U4["DELETE /user/{id}"]
T1["GET /resource-type/list"]
T2["POST /resource-type"]
T3["DELETE /resource-type/{id}"]
V1["GET /provider/list"]
V2["POST /provider"]
V3["DELETE /provider/{id}"]
L1["GET /operation-log/page"]
end
graph TD
subgraph 前端依赖
Vue["Vue 3.4"] --> VRouter["Vue Router 4.3"]
Vue --> Pinia["Pinia 2.1"]
Vue --> EP["Element Plus 2.6"]
Vue --> ECharts["ECharts 6.1"]
Vue --> Axios["Axios 1.6"]
Vue --> DayJS["Day.js 1.11"]
end
subgraph 后端依赖
SB["Spring Boot 3.2.5"] --> SS["Spring Security 6.2"]
SB --> MBP["MyBatis-Plus 3.5.5"]
SB --> SM["Spring Mail"]
SB --> SV["Spring Validation"]
SS --> JWT["JJWT 0.11.5"]
MBP --> MySQL["MySQL Connector 8.3"]
SB --> EE["EasyExcel 3.3.2"]
SB --> Lombok["Lombok 1.18.32"]
end
subgraph 构建工具
Vite["Vite 5.1"]
Maven["Maven"]
Vitest["Vitest 4.1"]
JUnit["JUnit 5 + Mockito"]
end