随着企业数字化转型深入,团队在日常运营中会使用大量云资源和服务套餐(云服务器、云数据库、对象存储、域名、SSL 证书、CDN、短信包、邮件包等),这些资源具有以下特点:
| 角色 | 描述 | 核心诉求 |
|---|---|---|
| 管理员 | 系统管理员,拥有全部权限 | 统一管理平台所有资源和用户 |
| 普通用户 | 项目负责人/运维人员/开发人员 | 登记和管理自己负责的资源,接收到期提醒 |
| 类别 | 要求 |
|---|---|
| 安全性 | 密码 BCrypt 加密存储;JWT 无状态认证;接口权限控制;CORS 跨域配置 |
| 性能 | 列表查询避免 N+1 问题,关联数据批量查询;分页查询减少单次数据量 |
| 可用性 | 统一响应格式和错误处理;前端统一拦截提示;表单参数校验 |
| 可扩展性 | 资源类型和服务商通过字典表管理,用户可自行扩展,无需修改代码 |
| 可维护性 | 前后端分离;分层架构清晰;Lombok 减少样板代码;MyBatis-Plus 简化数据访问 |
| 数据完整性 | 关键操作使用事务保证一致性;逻辑删除保留历史数据;唯一约束防止重复 |
采用经典的 前后端分离 + 分层架构 设计:
┌─────────────────────────────────────────┐
│ 表现层 (Frontend) │
│ Vue 3 SPA + Element Plus + Pinia │
└─────────────────┬───────────────────────┘
│ REST API (JSON)
┌─────────────────▼───────────────────────┐
│ 接口层 (Controller) │
│ 接收请求 → 参数校验 → 调用 Service │
├─────────────────────────────────────────┤
│ 业务层 (Service) │
│ 核心业务逻辑、事务管理、VO 组装 │
├─────────────────────────────────────────┤
│ 持久层 (Mapper) │
│ MyBatis-Plus BaseMapper 自动 CRUD │
├─────────────────────────────────────────┤
│ 数据层 (MySQL) │
│ 8 张业务表 + 逻辑删除 + 唯一约束 │
└─────────────────────────────────────────┘
横切关注点:
登录流程:
用户 → POST /auth/login {username, password}
→ Spring Security AuthenticationManager 校验
→ 校验通过 → JwtUtil.generateToken(userId, username) → 返回 Token
→ 校验失败 → 抛出 BadCredentialsException → 全局异常处理返回错误
请求鉴权流程:
请求 → JwtAuthenticationFilter
→ 从 Authorization 头提取 Bearer Token
→ JwtUtil.parseToken() 验证有效性
→ 从数据库加载用户信息(含角色)
→ 设置 SecurityContext
→ SecurityConfig 根据路径规则判断权限
- /auth/login → permitAll
- /user/** → hasRole("ADMIN")
- 其他 → authenticated
Token 设计:
Project ──M:N──> Resource
│
ProjectResource (中间表)
Resource ──N:1──> ResourceType (类型字典)
Resource ──N:1──> Provider (服务商字典)
Resource ──N:1──> SysUser (负责人)
Project ──N:1──> SysUser (负责人)
Resource ──1:N──> ExpireNotifyLog (提醒记录)
SysUser ──1:N──> OperationLog (操作日志)
| 设计决策 | 选择 | 原因 |
|---|---|---|
| 资源状态存储 | 冗余 status 字段 | 避免每次查询都计算到期状态,支持直接按状态筛选和索引 |
| 项目-资源关系 | 独立中间表 | 多对多关系的标准实现,支持双向查询 |
| 到期提醒防重 | 唯一约束+代码查重 | 数据库层面兜底 + 应用层面先查后插,双重保障 |
| 逻辑删除 | deleted 字段 | 保留历史数据,支持数据恢复,MyBatis-Plus @TableLogic 自动过滤 |
| 密码存储 | BCrypt | Spring Security 默认方案,自带盐值,安全性高 |
| 费用字段 | DECIMAL(12,2) | 精确的货币计算,避免浮点精度问题 |
用户提交表单
→ Controller 接收 ResourceDTO(含 projectIds 列表)
→ Service.saveOrUpdate()
→ 判断是新增还是编辑
→ 保存 Resource 基本信息
→ 维护项目关联:
1. 删除该资源的所有旧关联 (DELETE FROM project_resource WHERE resource_id = ?)
2. 遍历 projectIds,逐条插入新关联
→ 记录操作日志
→ 事务提交
设计考量:采用"先删后插"策略维护关联关系,逻辑简单、不易出现脏数据,适合数据量不大的场景。
@Scheduled(cron = "0 0 8 * * ?")
→ Step 1: refreshExpireStatus()
→ 查询所有非停用资源
→ 根据到期日期计算新状态
→ 状态变化时更新数据库
→ Step 2: 邮件提醒扫描
→ 查询所有有到期日期且非停用的资源
→ 遍历每个资源:
→ 计算距到期天数
→ 判断所处阶段(>30天:不提醒 / ≤30天:stage1 / ≤7天:stage2 / <0:stage3)
→ 查询 expire_notify_log 是否已发送该阶段提醒
→ 未发送 → 查询负责人邮箱 → 发送邮件 → 写入日志记录
→ 输出发送统计
邮件策略: | 阶段 | 条件 | 邮件标题样式 | 发送次数 | | --------- | -------------- | ------------------------------------- | -------- | | stage=1 | 距到期≤30天 | 【资源到期提醒】xxx 将在N天后到期 | 1次 | | stage=2 | 距到期≤7天 | 【紧急】资源 xxx 即将到期,仅剩N天 | 1次 | | stage=3 | 已过期(天数<0) | 【已过期】资源 xxx 已到期,请尽快处理 | 1次 |
Controller 接收查询参数
→ Service 构建 LambdaQueryWrapper
→ Mapper.selectPage() 分页查询 Resource 实体
→ assembleVOList() 批量组装 VO:
1. 收集所有 typeId、providerId、ownerId、resourceId
2. 批量查询 ResourceType → Map<id, name>
3. 批量查询 Provider → Map<id, name>
4. 批量查询 SysUser → Map<id, realName>
5. 批量查询 ProjectResource → Map<resourceId, List<projectId>>
6. 批量查询 Project → Map<id, name>
7. 遍历 Resource 列表,填充 VO 各字段
→ 返回 Page<ResourceVO>
性能考量:将 N+1 查询转化为 5 次批量查询,极大减少数据库交互次数。
frontend/src/
├── api/ # API 接口封装(按业务模块拆分)
│ ├── request.js # Axios 实例 + 拦截器
│ ├── auth.js # 登录、用户信息、密码修改
│ ├── dashboard.js
│ ├── resource.js
│ ├── project.js
│ ├── dict.js
│ ├── user.js
│ └── log.js
├── layout/ # 全局布局组件(侧边栏 + 顶部栏)
├── router/ # 路由配置 + 导航守卫
├── store/ # Pinia 状态管理(用户状态)
├── views/ # 页面组件(按业务模块组织)
│ ├── Dashboard.vue
│ ├── Login.vue
│ ├── CostStat.vue
│ ├── resource/ # 资源管理
│ ├── project/ # 项目管理
│ ├── dict/ # 字典管理
│ └── system/ # 系统管理(用户 + 日志)
├── styles/ # 全局样式变量
└── components/ # 公共组件
.vue 文件,使用 <script setup> 语法ResourceForm.vue),通过 v-model:visible 控制显隐,@saved 事件通知父组件刷新仅使用 Pinia 管理用户登录状态(Token + 用户信息),其他页面数据通过组件内部 ref/reactive + API 调用管理,避免过度使用全局状态。
| 安全措施 | 实现方式 |
|---|---|
| 密码安全 | BCrypt 加密存储,不存储明文/可逆密文 |
| 身份认证 | JWT 无状态 Token,每次请求验证有效性 |
| 接口权限 | Spring Security 路径规则 + 角色校验 |
| 跨域安全 | CORS 配置允许指定来源,支持携带凭证 |
| CSRF 防护 | 无状态 API(JWT)禁用 CSRF(不依赖 Cookie) |
| 敏感信息 | 资源表不存储密码,仅存储账号/控制台链接 |
| 参数校验 | Jakarta Validation 注解校验 + 全局异常处理 |
| 扩展点 | 设计方案 |
|---|---|
| 资源类型 | 字典表管理,用户可通过界面增删,无需改代码 |
| 服务商 | 字典表管理,同上 |
| 提醒天数 | application.yml 配置项,修改后重启生效 |
| 定时任务频率 | cron 表达式可配置 |
| Token 有效期 | application.yml 配置项 |
| 筛选条件 | 基于 MyBatis-Plus LambdaQueryWrapper 条件组装,易于扩展 |
| 优先级 | 改进项 | 方案 |
|---|---|---|
| 高 | 细粒度数据权限 | 增加"只查看自己负责的"筛选逻辑,基于 ownerId 过滤 |
| 高 | 敏感配置外部化 | 使用环境变量或配置中心(如 Nacos)管理密码和密钥 |
| 中 | 远程搜索下拉 | 将全量加载改为远程搜索 + 分页加载 |
| 中 | 接口自动化测试 | 使用 JUnit 5 + MockMvc 对核心接口编写集成测试 |
| 中 | 状态实时刷新 | 在资源列表查询时实时计算状态,或缩短定时任务间隔 |
| 低 | 操作日志增强 | 使用 AOP 切面自动记录日志,减少 Service 中的手动调用 |
| 低 | 文件上传管理 | 支持上传合同/发票等附件,与资源关联 |
| 低 | 数据备份与恢复 | 定期数据库备份,支持导出全量数据为 JSON/CSV |
| 低 | 多租户支持 | 如需对外提供服务,可增加租户隔离机制 |