Files
craftlabs-authorization-sdk/docs/superpowers/plans/2026-05-26-i10-p0-baseline-alignment.md
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

16 KiB
Raw Permalink Blame History

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: 验证文档完整性
grep -n '○' docs/chuangfei-platform-product-modules.md | head -20

预期输出:剩余 ○ 项应与 gap analysis §P2 级别的项目一致。

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: 0snCount: 0 (硬编码占位)。前端 CustomerDetailView.vue 已存在但无摘要区块。

  • Step 1: 修复后端 CustomerService.getCustomerSummary()

CustomerService.java 中找到 getCustomerSummary 方法:

public Map<String, Object> getCustomerSummary(Long customerId) {
    Map<String, Object> result = new java.util.LinkedHashMap<>();

    // 项目计数
    var projectQuery = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<PlatformProject>();
    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<PlatformContract>();
    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<PlatformLicenseSn>();
    // Join 或子查询: contract_line → contract WHERE customer_id = ?
    // 简单方案: 使用 Mapper XML 或子查询
    result.put("snCount", 0);  // 暂保持,需 schema 确认后实现

    return result;
}

需要在 CustomerService 中注入 PlatformContractMapperPlatformLicenseSnMapper (如果尚不存在)。在文件头部找到构造器注入:

// 如果尚未注入,添加以下字段和构造器参数:
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: 验证后端编译
mvn -f services/pom.xml -pl delivery-platform-api -am compile -q 2>&1 | tail -5

Expected: BUILD SUCCESS (无错误)

  • Step 3: 验证后端端点

确保 CustomerControllerGET /{id}/summary 端点未被修改(只改了 service 层)。

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 摘要区块:

<script setup>
// 已有 imports 末尾添加
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'

const route = useRoute()
const summary = ref(null)
const summaryLoading = ref(false)

async function loadSummary() {
  summaryLoading.value = true
  try {
    const res = await axios.get(`/api/v1/customers/${route.params.id}/summary`)
    summary.value = res.data
  } catch { /* 静默失败 — 摘要为增强信息,不阻断页面 */ }
  finally { summaryLoading.value = false }
}

onMounted(() => {
  // 保留已有 onMounted 逻辑,新增:
  loadSummary()
})
</script>

<template>
  <!-- 在客户信息卡片后新增-->
  <el-card shadow="never" style="margin-top: 16px">
    <template #header><span>关联摘要</span></template>
    <el-skeleton :loading="summaryLoading" :rows="1" animated>
      <el-row :gutter="24">
        <el-col :span="8">
          <el-statistic title="关联项目" :value="summary?.projectCount ?? '-'" />
        </el-col>
        <el-col :span="8">
          <el-statistic title="在履约合同" :value="summary?.contractCount ?? '-'" />
        </el-col>
        <el-col :span="8">
          <el-statistic title="在途 SN" :value="summary?.snCount ?? '-'" />
        </el-col>
      </el-row>
    </el-skeleton>
  </el-card>
</template>
  • Step 5: LSP 诊断验证
# 对 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.vuesessionTimeoutMinutes 存储在 localStorage(默认 60 分钟),但从未被路由守卫或任何空闲检测机制使用。用户在登录后从不超时。

  • Step 1: 创建 idleTimer 工具

web/delivery-platform-ui/src/utils/idleTimer.js:

/**
 * 空闲计时器 — 监听用户交互事件,超时触发回调。
 * 读取 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 方法,在清理现有状态后增加:

// 在 logout() 方法末尾添加:
// 清理 idle 计时器
if (window.__idleCleanup) {
  window.__idleCleanup()
  delete window.__idleCleanup
}

新增 checkSessionTimeout action

// 在 store actions 末尾添加:
checkSessionTimeout() {
  // 由路由守卫调用 — 检查 idle 计时器是否需要重置
  const idleTimer = import('../utils/idleTimer')
  // idleTimer 会在路由跳转时由守卫自动重置
},
  • Step 3: 修改路由守卫

web/delivery-platform-ui/src/router/index.jsbeforeEach 守卫中,在 token 验证之后、角色验证之前,新增 idle 检测:

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

onMounted(() => {
  // 检查超时参数
  if (route.query.timeout === '1') {
    ElMessage.warning('会话已超时,请重新登录')
  }
})

需要在 LoginView 头部导入 useRoute:

import { useRoute } from 'vue-router'
// 移除原有 router 导入(如果已有 useRouter 则保留两个)
const route = useRoute()
  • Step 5: LSP 诊断验证
# 对所有修改的 Vue 文件运行 LSP

Expected: 0 errors, 0 warnings

# 检查 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 到 I11P1
M11-F05 登录失败锁定 后端已有,前端无需修改

2. Placeholder 扫描: 无 TBD/TODO 遗留。

3. 类型一致性: sessionTimeoutMinutes 在 idleTimer.js、SystemParamsView.vue、auth store 之间一致。

4. 范围检查: 3 个独立任务,不跨越子系统边界。