Files
huangping 1333cb38d6 docs: add AGENTS.md, code audit reports, and implementation plans
Added hierarchical AGENTS.md files for root, java, native, services, web modules. Added comprehensive audit reports covering PRD progress, UI audit, full version gap analysis, code audit findings, and ONLYOFFICE status.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-27 08:37:24 +08:00

319 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 代码实现审计报告 — PRD vs 实际实现
**审计日期:** 2026-05-26
**审计范围:**
- PRD 文档: `chuangfei-platform-product-modules.md` (M1-M11), `FRONTEND_UI_SPECIFICATION.md`, `chuangfei-platform-bpm-and-roadmap.md`
- 后端: `services/delivery-platform-api/` (153 Java 文件) + `services/license-webhook-ingress/`
- 前端: `web/delivery-platform-ui/src/` (47 源文件)
---
## 1. 严重缺陷 (Critical)
### CR-01: 认证系统硬编码用户凭据
**位置:** `AuthController.java:54-89`
**PRD 对照:** M11-F14 要求「用户与账号生命周期:创建、启用/禁用、离职归档」
**实际实现:** 4 个用户硬编码在 Java switch 语句中:
```java
case "admin" role=SYS_ADMIN
case "sales" role=SALES
case "delivery" role=DELIVERY
case "ops" role=LICENSE_OPS
```
**缺陷:**
- 密码 = 小写用户名 (`pass.equals(user.toLowerCase())`) — admin/admin, sales/sales
- 无数据库用户表 — 无法 CRUD、禁用、归档
- 注入的 `PasswordEncoder` (BCrypt) 仅用于 `changePassword` 端点,登录完全不使用
- `changePassword` 验证旧密码时硬编码 `passwordEncoder.encode("admin")`,对非 admin 用户永远失败
**影响:** 无法管理用户、密码与用户名相同、安全基线不达标
---
### CR-02: JWT Token 存储在 localStorage
**位置:** `stores/auth.js:4,8,29,37`
**PRD 对照:** §16.6 已知局限明确标注「前端 Token 存 localStorage(非 HttpOnly Cookie)」为已知安全缺陷,计划 Mid 迁移
**实际实现:** Token 通过 `localStorage.setItem(TOKEN_KEY)` 持久化
```javascript
// auth.js:29
localStorage.setItem(TOKEN_KEY, this.token);
axios.defaults.headers.common.Authorization = `Bearer ${this.token}`;
```
**缺陷:** XSS 攻击可窃取 localStorage 中的 JWT,获得完整 API 访问权限
**影响:** 安全 — XSS 窃取 → 权限丢失
**缓解:** 当前通过 CSP + 前端无富文本渲染降低风险,但未根本解决
---
### CR-03: Error Message 泄露
**位置:**
- `LicenseController.java:25``ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()))`
- `ContractController.java:136``ResponseEntity.status(500).body(Map.of("error", e.getMessage()))`
**PRD 对照:** 无明确的错误消息规范,但全局 `ApiExceptionHandler` 已返回泛化消息 "服务器内部错误"
**缺陷:** 这两个 Controller 使用 try-catch 捕获 `Exception` 并将异常消息原文 (`e.getMessage()`) 返回给客户端,绕过了全局异常处理器。可能泄露实现细节(表名、SQL、文件路径)。
**影响:** 信息安全 — 生产环境可能泄露堆栈或内部路径信息
---
### CR-04: resetPassword 和 forceLogout 是空操作
**位置:** `AuthController.java:131-148`
**PRD 对照:** M11-F08「密码重置: 管理员重置密码或邮件/短信重置链接」、M11-F12「管理员强制下线」
**实际实现:** 两个端点均仅有参数校验,无实际逻辑
```java
// resetPassword — 校验参数后直接返回 200 OK,未更新任何密码
@PostMapping("/admin/reset-password")
public ResponseEntity<Void> resetPassword(@RequestBody Map<String, String> body) {
String username = body.get("username");
String newPassword = body.get("newPassword");
if (username == null || newPassword == null || newPassword.length() < 6) {
throw new ResponseStatusException(...);
}
return ResponseEntity.ok().build(); // 没有实际更新密码!
}
// forceLogout — 同上,无会话失效逻辑
@PostMapping("/admin/force-logout")
public ResponseEntity<Void> forceLogout(@RequestBody Map<String, String> body) {
String username = body.get("username");
if (username == null) throw new ResponseStatusException(...);
return ResponseEntity.ok().build(); // 没有实际使会话失效!
}
```
**影响:** 功能完全不可用 — 前端调用后显示成功,实际无效果
---
## 2. 高危缺陷 (High)
### HI-01: 无用户管理数据库表
**位置:** `AuthController.java` (全部)
**PRD 对照:** M11-F14「用户与账号生命周期:创建、启用/禁用、离职归档」— P0
**实际:**`platform_user` 表或类似实体。4 个用户硬编码。Flyway 迁移 V15 `seed_product_roles.sql` 仅涉及角色种子数据。
**影响:** M11-F14 完全未实现,无法添加/禁用/管理用户
---
### HI-02: 权限模型硬编码
**位置:** `AuthController.java:91-114`
**PRD 对照:** §13.4 要求权限码命名规范(如 `customer:project:rw``contract:order:export`
**实际:** 权限字符串在 Java switch 中硬编码,非数据库驱动、不可配置
```java
case "SALES":
permissions.add("customer:*");
permissions.add("project:*");
permissions.add("contract:*");
permissions.add("delivery:read");
break;
```
**影响:** 新增角色或调整权限需改代码重启;权限码 `v-permission` 指令在前端存在但后端无对应校验
---
### HI-03: 会话管理后端无状态
**位置:** `SecurityConfig.java:55-56``SessionCreationPolicy.STATELESS`
**PRD 对照:** M11-F03「空闲超时自动登出」、M11-F11「并发会话策略」
**实际:** JWT 无状态设计意味着后端无法主动使会话失效(无会话存储)。空闲超时仅在前端实现(`idleTimer.js`),后端无法强制登出。`forceLogout` API 为空操作。
**影响:** 并发会话、强制下线、空闲超时功能均无法在后端层面实现
---
### HI-04: LicenseController 异常处理绕过全局 Handler
**位置:** `LicenseController.java:19-27`
**PRD 对照:** 全局 `ApiExceptionHandler` 已提供统一错误格式 `{status, message}`
**实际:** `create` 方法手动 try-catch,返回非标准错误格式
```java
try {
return ResponseEntity.ok(licenseService.create(request));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
}
```
返回格式为 `{"error": "..."}` 而非全局标准的 `{"status": 500, "message": "..."}`
**影响:** API 响应格式不一致,前端 `apiErrorMessage.js` 可能无法解析
---
## 3. 中危缺陷 (Medium)
### ME-01: 合同附件上传无校验
**位置:** `ContractController.java:118-137`
**PRD 对照:** M2-F05「合同附件:上传扫描件/电子签输出(存储与权限受控)」
**实际:** 上传端点无文件大小限制、无文件类型白名单、无病毒扫描
```java
@PostMapping("/{id}/attachments")
public ResponseEntity<Map<String, Object>> uploadAttachment(@PathVariable Long id, @RequestParam("file") MultipartFile file) {
// 无 file.getSize() 校验
// 无 file.getContentType() 白名单
// 直接将文件写入本地磁盘
```
**影响:** 可能被用于上传恶意文件;磁盘可能被大文件填满
---
### ME-02: 部分 Controller 返回格式不统一
**位置:** 多文件
**PRD 对照:** 无明确 API 响应规范
**实际:** 存在三种返回风格:
1. 全局 `ApiExceptionHandler``{status, message}` (标准)
2. `LicenseController``{error, ...}` (非标准)
3. `ContractController``{error, ...}` (非标准)
4. 部分端点直接返回实体对象(非 Map)
**影响:** 前端 `apiErrorMessage.js` 兼容多种格式但无法覆盖所有情况
---
### ME-03: 系统参数仅存于 localStorage
**位置:** `SystemParamsView.vue:14-33`
**PRD 对照:** M11-F20「系统参数」— 期望持久化到后端数据库
**实际:**
```javascript
// 直接保存到浏览器 localStorage
localStorage.setItem('systemParams', JSON.stringify(params.value))
ElMessage.success('参数已保存(MVP: 存储于浏览器本地)')
```
**影响:** 参数仅对当前浏览器有效,不同用户/设备参数不一致;清除浏览器数据后丢失
---
### ME-04: 后端 changePassword 验证逻辑错误
**位置:** `AuthController.java:151-168`
**PRD 对照:** M11-F07「已登录用户修改本人密码;校验旧密码强度与新密码策略」
**实际:**
```java
String currentPasswordHash = passwordEncoder.encode("admin"); // 始终比较 admin 的密码!
if (!passwordEncoder.matches(oldPassword, currentPasswordHash)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "旧密码错误");
}
```
`passwordEncoder.encode("admin")` 硬编码为 "admin",导致:
- admin 用户可以改密(旧密码 = admin 通过)
- 其他用户(sales/delivery/ops)永远无法通过旧密码验证
---
### ME-05: SN 批量导入缺少事务回滚
**位置:** `LicenseSnService.java:134-155`
**PRD 对照:** M4-F01/F07 批量操作
**实际:** 逐条插入,失败项跳过继续,但方法未标注 `@Transactional`
```java
public Map<String, Object> batchImport(List<LicenseSnCreateRequest> requests) {
// for 循环逐条 insert,无事务保护
// 部分成功 = 部分写入无法回滚
```
**影响:** 批量导入 100 条中第 50 条失败时,前 49 条已写入无法撤销
---
## 4. PRD 与实现偏离 (Misalignment)
### MA-01: M11 角色模型偏离产品定义
| 产品定义角色 | 实际实现 | 状态 |
|------------|---------|------|
| SYS_ADMIN | ✅ | 产品定义包含 |
| SALES | ✅ I10 新增 | 产品定义包含 |
| DELIVERY | ✅ I10 新增 | 产品定义包含 |
| LICENSE_OPS | ✅ I10 新增 | 产品定义包含 |
| ORDER_SUPPORT | ○ | 产品定义但未实现 |
| FINANCE_VIEW | ○ | 产品定义但未实现 |
| COMPLIANCE | ○ | 产品定义但未实现 |
| EXEC_VIEW | ○ | 产品定义但未实现 |
| SECURITY_ADMIN | ○ | 产品定义但未实现 |
| DEVELOPER | ✅ (应废弃) | MVP 遗留非标角色 |
| OPS | ✅ (应废弃) | MVP 遗留非标角色 |
前端路由角色标记(`router/index.js`)仍广泛使用 `SYS_ADMIN``SALES`,但 `DEVELOPER` 已从路由角色列表中移除,而 `LICENSE_OPS``DELIVERY` 已加入。
### MA-02: M1-F07 客户冻结后端就绪前端缺 UI
**位置:** 后端 `CustomerController.java``PATCH /{id}/freeze``/unfreeze` 端点,前端 `CustomersView.vue` 无冻结操作入口
### MA-03: M11-F07 密码修改已实现但产品文档未标记
`ProfileView.vue` 已包含完整的改密弹窗,`AuthController` 有对应端点,但文档标注为 ○。
---
## 5. 代码质量问题 (Code Quality)
### CQ-01: SecurityConfig 重复 import (已修复)
**位置:** `SecurityConfig.java:5,20` — 两次 `import org.springframework.context.annotation.Bean`
**状态:** ✅ 本次审计已修复
### CQ-02: 个别 Controller 使用 `@PreAuthorize` 而非 JWT Filter 角色
**位置:** `LicenseController.java:20,30,40` — 使用 `@PreAuthorize("hasRole('LICENSE_OPS') or hasRole('ADMIN')")`
**问题:** 与 JWT Filter 的双重验证增加了 role 前缀处理的复杂性(JwtAuthFilter 添加 `ROLE_` 前缀,`@PreAuthorize` 期望 `ROLE_` 格式)
### CQ-03: 前端 API 层中个别函数含 query 参数拼接
**位置:** `platform.js:460`
```javascript
export function createSkuMapping(contractLineId, body) {
return axios.post(`/api/v1/integration/sku-mappings?contractLineId=${contractLineId}`, body);
}
```
**影响:** 非紧急,但建议统一使用 `{ params: { contractLineId } }` 方式
---
## 6. 未被 PRD 覆盖但代码已实现的模块(超前实现)
| 模块 | 功能 | 建议 |
|------|------|------|
| M7 设备管理 | 登记/列表/详情/绑定/换机申请 | 核对 PRD 需求后决定是否纳入正式范围 |
| M8 通知待办 | 待办中心 + 通知通道配置 UI | 需补充实际发送逻辑 |
| M9 报表对账 | 4 个报表页面均已上线 | 补充导出按钮和推送逻辑 |
| M6 ID/特征/SKU 映射 | 前后端均已实现 | 更新产品文档状态 |
---
## 7. 汇总统计
| 严重级别 | 数量 | 编号 |
|---------|------|------|
| 🔴 Critical | 4 | CR-01~CR-04 |
| 🟠 High | 4 | HI-01~HI-04 |
| 🟡 Medium | 5 | ME-01~ME-05 |
| 🔵 Misalignment | 3 | MA-01~MA-03 |
| ⚪ Code Quality | 3 | CQ-01~CQ-03 |
| **合计** | **19** | |
---
## 8. 修复建议优先级
### P0 — 立即修复(安全基线)
1. **CR-01** (硬编码用户): 创建 `platform_user` 表 + Flyway 迁移 + AuthController 改用数据库查询 + BCrypt 密码校验
2. **CR-04** (空操作端点): `resetPassword``forceLogout` 补充实际逻辑 — resetPassword 更新用户密码,forceLogout 增加黑名单机制
3. **CR-03** (错误泄露): `LicenseController``ContractController` 移除 try-catch,让全局 `ApiExceptionHandler` 接管
4. **ME-04** (改密逻辑错误): `changePassword` 从 SecurityContext 获取当前用户名,从数据库查询对应用户的密码哈希
### P1 — 短期修复
5. **HI-01** (用户管理): 在 P0 用户表基础上实现用户 CRUD API + 前端管理页面
6. **ME-01** (附件校验): ContractController upload 增加 `@Size` 注解和文件类型白名单
7. **ME-05** (事务缺失): `batchImport` 方法添加 `@Transactional`
### P2 — 中长期
8. **CR-02** (Token 存储): 迁移至 HttpOnly Cookie(需后端配合返回 Set-Cookie header
9. **HI-02** (权限模型): 权限码持久化到数据库,实现可配置 RBAC
10. **HI-03** (会话管理): 引入 Token 黑名单/白名单机制或 Redis 会话存储