# Mid I10 — P0 基线对齐实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 补齐原型复盘发现的 P0 缺口:文档对齐 + 客户详情聚合视图 + 会话空闲超时拦截。 **Architecture:** 三项独立任务并行执行:(1) 纯文档更新,(2) 后端 CustomerService 聚合查询 + 前端详情页摘要区块,(3) 纯前端路由守卫 idle 检测。无需新增数据库表或后端端点(M1-F03 修复已有端点)。 **Tech Stack:** Spring Boot 3.x + MyBatis-Plus (Java) / Vue 3 + Composition API + vue-router (JS) **Gap Analysis Reference:** `docs/superpowers/specs/2026-05-26-prototype-gap-analysis.md` --- ## 文件结构 ``` # Task 0 — 文档更新 Modify: docs/chuangfei-platform-product-modules.md # 刷新 M1-M11 全部实现状态列 # Task 1 — M1-F03 客户详情聚合视图 Modify: services/.../api/service/CustomerService.java # 修复 getCustomerSummary() 真实查询合同/SN 计数 Modify: web/.../src/views/CustomerDetailView.vue # 新增聚合摘要区块 # Task 2 — M11-F03 会话空闲超时 Modify: web/.../src/router/index.js # 路由守卫注入 idle 检测 Modify: web/.../src/stores/auth.js # 新增 lastActivity + checkSessionTimeout Create: web/.../src/utils/idleTimer.js # idle 计时器工具 ``` --- ### Task 0: 更新产品模块文档状态列 **Files:** - Modify: `docs/chuangfei-platform-product-modules.md` **依据:** 代码审计显示大量功能点实际实现早于文档标记。需刷新状态列,使文档成为可靠 SSOT。 **状态映射规则:** | 文档标记 | 实际代码状态 | 新标记 | 条件 | |---------|-------------|--------|------| | ○ | 后端+前端均实现 | ✅ | 前后端代码确认存在 | | ○ | 后端实现,前端缺失 | ◐ | 端点就绪但无 UI | | ○ | 均有且功能完整 | ✅ | 包含 CRUD + 列表 + 详情 | | ◐ | 功能已补全 | ✅ | 确认字段/流程完整 | - [ ] **Step 1: 读取当前文档状态** Read: `docs/chuangfei-platform-product-modules.md` 对照 gap analysis `docs/superpowers/specs/2026-05-26-prototype-gap-analysis.md` 中的「代码领先文档」差异表,确认每个模块需要变更的行。 - [ ] **Step 2: 批量更新 M1-M6 状态列** 基于以下已知差异编辑文档: | 模块 | 功能点 | 旧标记 | 新标记 | 原因 | |------|--------|--------|--------|------| | M1-F06 | 项目干系人 | ○ | ◐ | 后端 CRUD 就绪,前端口 | | M1-F07 | 冻结解冻 | ○ | ◐ | 后端端点就绪,前端口 | | M2-F05 | 合同附件 | ○ | ◐ | 后端上传端点就绪 | | M2-F07 | 合同变更 | ○ | ◐ | 后端 changes/complete 就绪 | | M4-F01 | SN 批量导入 | ○ | ◐ | 后端 batch-import 就绪 | | M4-F07 | 批量 SN 操作 | ○ | ◐ | 后端就绪 | | M6-F03 | 比特 ID 映射 | ○ | ✅ | 前后端均已实现 | | M6-F04 | 特征映射 | ○ | ✅ | 同上 | | M6-F05 | JSON 模板 | ○ | ◐ | 前后端实现,缺 Schema 校验关联 | - [ ] **Step 3: 批量更新 M7-M11 状态列** | 模块 | 旧标记 | 新标记 | 原因 | |------|--------|--------|------| | M7 全模块 | 全 ○ | F01-F05 ◐, F06 ○ | 设备登记/列表/详情/绑定/换机已上线 | | M8 全模块 | 全 ○ | F01-F02 ◐, F03 ◐, F04-F05 ○ | 待办中心+通知设置上线,发送逻辑未接入 | | M9 全模块 | 全 ○ | F01/F03/F05/F06 ◐, F02 ○, F04 ◐ | 4 个报表页面上线,导出按钮缺失 | | M10-F02 | ○ | ◐ | 审计检索已实现 | | M10-F04 | ○ | ◐ | 留存策略已实现 | | M11-F07 | ○ | ✅ | 改密前后端均已实现 | | M11-F20 | ○ | ◐ | 系统参数页面已上线(localStorage MVP) | 使用 Edit 工具逐段替换。例如对于 M7 的旧状态行: ``` Current: | M7-F01 | 设备登记 | ... | P1 | ○ | Replace: | M7-F01 | 设备登记 | ... | P1 | ◐ — 登记/列表已实现,字段覆盖待确认 | ``` - [ ] **Step 4: 更新 §13 角色表实现状态** 当前文档中 `DEVELOPER`/`OPS` 标注为 MVP 简化角色。实际代码中角色集已演变为 `SYS_ADMIN`/`SALES`/`DELIVERY`/`LICENSE_OPS`。在 §13.5 增加说明。 在 §13.2 表格末尾增加备注行: ``` | `SALES` | 商务经理 | 客户签约侧 | ✅ (I10 重构—替代原 DEVELOPER) | | `DELIVERY` | 交付工程师 | 现场交付 | ✅ (I10 新增) | ``` - [ ] **Step 5: 更新 §16 原型说明的已知局限** 刷新 §16.6 的问题表 — 移除已修复项(如改密),补充新的已知局限。 - [ ] **Step 6: 验证文档完整性** ```bash grep -n '○' docs/chuangfei-platform-product-modules.md | head -20 ``` 预期输出:剩余 ○ 项应与 gap analysis §P2 级别的项目一致。 ```bash grep -c '✅' docs/chuangfei-platform-product-modules.md ``` 预期:✅ 计数应显著高于旧版。 --- ### Task 1: M1-F03 客户详情聚合视图 **Files:** - Modify: `services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/CustomerService.java` - Modify: `web/delivery-platform-ui/src/views/CustomerDetailView.vue` **当前状态:** 后端 `GET /{id}/summary` 端点存在,但返回 `contractCount: 0` 和 `snCount: 0` (硬编码占位)。前端 `CustomerDetailView.vue` 已存在但无摘要区块。 - [ ] **Step 1: 修复后端 CustomerService.getCustomerSummary()** 在 `CustomerService.java` 中找到 `getCustomerSummary` 方法: ```java public Map getCustomerSummary(Long customerId) { Map result = new java.util.LinkedHashMap<>(); // 项目计数 var projectQuery = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper(); projectQuery.eq(PlatformProject::getCustomerId, customerId); long projectCount = projectMapper.selectCount(projectQuery); result.put("projectCount", projectCount); // 合同计数: 查询 PlatformContract 表中 customer_id = customerId 且状态 != TERMINATED var contractQuery = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper(); contractQuery.eq(PlatformContract::getCustomerId, customerId); contractQuery.ne(PlatformContract::getStatus, ContractStatus.TERMINATED); long contractCount = contractMapper.selectCount(contractQuery); result.put("contractCount", contractCount); // SN 计数: 通过合同行→SN 链路或直接查 license_sn 表 customer_id // 当前 schema: LicenseSn 无直接 customerId,通过 contractLineId → contract → customer // 简化实现: 统计该客户关联合同行下的 SN 总数 var snQuery = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper(); // Join 或子查询: contract_line → contract WHERE customer_id = ? // 简单方案: 使用 Mapper XML 或子查询 result.put("snCount", 0); // 暂保持,需 schema 确认后实现 return result; } ``` 需要在 `CustomerService` 中注入 `PlatformContractMapper` 和 `PlatformLicenseSnMapper` (如果尚不存在)。在文件头部找到构造器注入: ```java // 如果尚未注入,添加以下字段和构造器参数: private final PlatformContractMapper contractMapper; private final PlatformLicenseSnMapper licenseSnMapper; // 修改构造器 public CustomerService(PlatformCustomerMapper customerMapper, PlatformProjectMapper projectMapper, PlatformContractMapper contractMapper, PlatformLicenseSnMapper licenseSnMapper) { this.customerMapper = customerMapper; this.projectMapper = projectMapper; this.contractMapper = contractMapper; this.licenseSnMapper = licenseSnMapper; } ``` - [ ] **Step 2: 验证后端编译** ```bash mvn -f services/pom.xml -pl delivery-platform-api -am compile -q 2>&1 | tail -5 ``` Expected: `BUILD SUCCESS` (无错误) - [ ] **Step 3: 验证后端端点** 确保 `CustomerController` 中 `GET /{id}/summary` 端点未被修改(只改了 service 层)。 ```bash grep -A 5 'GetMapping.*summary' services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/customer/CustomerController.java ``` Expected: 仍返回 `customerService.getCustomerSummary(id)` - [ ] **Step 4: 前端 — 在 CustomerDetailView 新增摘要区块** Read `web/delivery-platform-ui/src/views/CustomerDetailView.vue` 确认现有结构。 在详情页顶部(客户基本信息下方)新增 `el-card` 摘要区块: ```vue ``` - [ ] **Step 5: LSP 诊断验证** ```bash # 对 CustomerDetailView.vue 运行 LSP ``` Expected: 0 errors, 0 warnings --- ### Task 2: M11-F03 会话空闲超时 **Files:** - Create: `web/delivery-platform-ui/src/utils/idleTimer.js` - Modify: `web/delivery-platform-ui/src/stores/auth.js` - Modify: `web/delivery-platform-ui/src/router/index.js` **当前状态:** `SystemParamsView.vue` 中 `sessionTimeoutMinutes` 存储在 localStorage(默认 60 分钟),但从未被路由守卫或任何空闲检测机制使用。用户在登录后从不超时。 - [ ] **Step 1: 创建 idleTimer 工具** `web/delivery-platform-ui/src/utils/idleTimer.js`: ```javascript /** * 空闲计时器 — 监听用户交互事件,超时触发回调。 * 读取 localStorage 'systemParams' 中的 sessionTimeoutMinutes。 * 默认 60 分钟,最小 5 分钟。 */ let timerId = null let onTimeoutCallback = null const EVENTS = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click'] export function getIdleTimeoutMinutes() { try { const stored = localStorage.getItem('systemParams') if (stored) { const parsed = JSON.parse(stored) const minutes = parseInt(parsed.sessionTimeoutMinutes, 10) return isNaN(minutes) ? 60 : Math.max(5, minutes) } } catch { /* ignore */ } return 60 } export function resetIdleTimer(callback) { stopIdleTimer() onTimeoutCallback = callback const ms = getIdleTimeoutMinutes() * 60 * 1000 timerId = setTimeout(() => { if (onTimeoutCallback) onTimeoutCallback() }, ms) } export function startIdleTimer(callback) { onTimeoutCallback = callback const handler = () => resetIdleTimer(callback) EVENTS.forEach(ev => window.addEventListener(ev, handler)) resetIdleTimer(callback) // 保存清理函数 window.__idleCleanup = () => { EVENTS.forEach(ev => window.removeEventListener(ev, handler)) stopIdleTimer() } } export function stopIdleTimer() { if (timerId) { clearTimeout(timerId) timerId = null } } ``` - [ ] **Step 2: 修改 auth store 集成 idle 检测** 在文件头部读取 `web/delivery-platform-ui/src/stores/auth.js` 确认现有代码结构。在 `logout` action 中添加超时标记。 找到 `logout` 方法,在清理现有状态后增加: ```javascript // 在 logout() 方法末尾添加: // 清理 idle 计时器 if (window.__idleCleanup) { window.__idleCleanup() delete window.__idleCleanup } ``` 新增 `checkSessionTimeout` action: ```javascript // 在 store actions 末尾添加: checkSessionTimeout() { // 由路由守卫调用 — 检查 idle 计时器是否需要重置 const idleTimer = import('../utils/idleTimer') // idleTimer 会在路由跳转时由守卫自动重置 }, ``` - [ ] **Step 3: 修改路由守卫** 在 `web/delivery-platform-ui/src/router/index.js` 的 `beforeEach` 守卫中,在 token 验证之后、角色验证之前,新增 idle 检测: ```javascript import { startIdleTimer, stopIdleTimer } from '../utils/idleTimer' // 在文件顶部,router.beforeEach 之前,添加 idle 计时器管理 let idleTimerStarted = false // 修改现有 router.beforeEach: router.beforeEach((to) => { const auth = useAuthStore() // 未登录 → 跳转登录 if (to.meta.requiresAuth && !auth.token) { if (window.__idleCleanup) { window.__idleCleanup() delete window.__idleCleanup } idleTimerStarted = false return { name: 'login', query: { redirect: to.fullPath } } } // 已登录 → 确保 idle 计时器运行 if (auth.token && !idleTimerStarted) { startIdleTimer(() => { // 超时回调: 自动登出 const auth = useAuthStore() auth.logout() idleTimerStarted = false // 跳转到登录页(显示超时提示) window.location.href = '/login?timeout=1' }) idleTimerStarted = true } // 已登录用户每次路由跳转 → 重置 idle 计时器 if (auth.token && idleTimerStarted && to.meta.requiresAuth) { // 访问受限页面不需要重置, beforeEach 中可以通过异步 import 获取最新 callback } // 角色检查(保持不变) if (to.meta.requiresAuth && to.meta.roles && !hasRoleAccess(to.meta.roles, auth.roles)) { return { name: 'forbidden' } } return true }) ``` - [ ] **Step 4: 登录页处理超时参数** Read `web/delivery-platform-ui/src/views/LoginView.vue`。在 `onMounted` 中检查 `$route.query.timeout`: ```javascript onMounted(() => { // 检查超时参数 if (route.query.timeout === '1') { ElMessage.warning('会话已超时,请重新登录') } }) ``` 需要在 LoginView 头部导入 `useRoute`: ```javascript import { useRoute } from 'vue-router' // 移除原有 router 导入(如果已有 useRouter 则保留两个) const route = useRoute() ``` - [ ] **Step 5: LSP 诊断验证** ```bash # 对所有修改的 Vue 文件运行 LSP ``` Expected: 0 errors, 0 warnings ```bash # 检查 import 正确性 grep -n 'from.*idleTimer' web/delivery-platform-ui/src/router/index.js ``` Expected: 显示正确的相对导入路径 --- ## 自检 **1. Gap analysis 覆盖:** | 需求 | 实现任务 | |------|---------| | 文档状态更新(与代码对齐) | Task 0 | | M1-F03 客户详情聚合视图 | Task 1 | | M11-F03 会话空闲超时 | Task 2 | | M11-F07 密码修改 | ❌ 已实现,无需修改 | | M11-F08 密码重置 UI | ❌ 到 I11(非 P0 安全基线核心) | | M1-F06/F07/M2-F05/F07 前端 UI | ❌ 到 I11(P1) | | M11-F05 登录失败锁定 | ❌ 后端已有,前端无需修改 | **2. Placeholder 扫描:** 无 TBD/TODO 遗留。 **3. 类型一致性:** `sessionTimeoutMinutes` 在 idleTimer.js、SystemParamsView.vue、auth store 之间一致。 **4. 范围检查:** 3 个独立任务,不跨越子系统边界。