選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

165 行
39 KiB

  1. <!doctype html>
  2. <html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>权限与角色切换验收 · 2026-09-06</title>
  3. <style>
  4. :root{color-scheme:light;--bg:#f3f6fa;--ink:#192b3f;--muted:#66788b;--line:#dce5ed;--blue:#2459ae;--green:#087452;--red:#b42636;--amber:#875600}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.75 system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif}a{color:var(--blue);text-underline-offset:3px}a:hover{text-decoration-thickness:2px}button{font:inherit}header{background:#112d48;color:#fff;padding:44px max(24px,calc((100vw - 1320px)/2)) 38px}.eyebrow{color:#a8c9e8;font-size:13px;letter-spacing:.12em}h1{margin:8px 0 12px;font-size:clamp(27px,3vw,40px);line-height:1.3}header p{max-width:950px;color:#d0dfed}header .meta{font-size:13px;color:#a9bfd2}.nav{position:sticky;top:0;z-index:3;background:rgba(255,255,255,.96);border-bottom:1px solid var(--line);display:flex;justify-content:center;gap:30px;padding:12px 20px;backdrop-filter:blur(10px)}.nav a{font-weight:650;text-decoration:none;color:var(--ink)}main{max-width:1368px;padding:26px 24px 60px;margin:auto}section{scroll-margin-top:72px;margin:32px 0}.section-label{color:var(--blue);font-size:12px;font-weight:750;letter-spacing:.1em}h2{font-size:25px;margin:0 0 16px}h3{font-size:19px;margin:0 0 10px}h4{font-size:17px;margin:8px 0 2px}p{margin:8px 0 14px}.panel,.stage{background:#fff;border:1px solid var(--line);border-radius:12px;padding:22px;margin:16px 0}.notice{border:1px solid var(--line);border-left:4px solid #c28b2e;border-radius:8px;padding:14px 18px;margin:16px 0;background:#fffaf0}.notice.pass{background:#edf8f2;border-color:#79b99b}.notice.fail{background:#fff0f1;border-color:#d57681}.notice strong{display:block;font-size:18px}.notice .badge{margin-right:7px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:20px 0}.stat{background:#fff;border:1px solid var(--line);border-radius:10px;padding:17px 20px}.stat b{display:block;font-size:28px;line-height:1.4}.stat span,.muted,.filename{color:var(--muted);font-size:13px}.stat small{display:block;color:var(--muted)}.badge{display:inline-block;border-radius:6px;padding:2px 8px;font-size:12px;line-height:1.65;white-space:nowrap;font-weight:650;background:#edf1f5;color:#556a7c}.badge.pass{background:#dcf4e9;color:var(--green)}.badge.fail,.badge.error{background:#ffe1e4;color:var(--red)}.badge.pending,.badge.warn,.badge.skip{background:#fff0cb;color:var(--amber)}.grid-two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.grid-two .panel{margin:0}.table-wrap{overflow:auto;margin:12px 0}table{width:100%;border-collapse:collapse;font-size:14px}th,td{text-align:left;vertical-align:top;padding:12px 14px;border-bottom:1px solid var(--line)}td code,th{white-space:nowrap}th{background:#f0f5fa;color:#435b70;font-weight:650}td:first-child{min-width:155px}tbody tr:last-child td{border-bottom:0}code{font:12px/1.65 Consolas,monospace;overflow-wrap:anywhere;background:#eef3f7;padding:2px 4px;border-radius:3px}pre{margin:12px 0;max-height:480px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;background:#f3f6fa;border:1px solid var(--line);border-radius:6px;padding:16px;font:12px/1.65 Consolas,"Microsoft YaHei",monospace}details{margin-top:12px}summary{cursor:pointer;font-weight:650;color:var(--blue);padding:4px 0}ul,ol{padding-left:24px}li{margin:6px 0}.gallery{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}figure{margin:0;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:#fff}.shot{display:block;background:#e7eef5;padding:8px;cursor:zoom-in}.shot img{display:block;width:100%;height:auto;aspect-ratio:16/9;object-fit:contain;background:#e7eef5}figcaption{padding:15px 17px}figcaption p{font-size:14px;color:#4d6478;margin-bottom:0}.filename{display:block;margin-top:5px;overflow-wrap:anywhere}.links{display:flex;flex-wrap:wrap;gap:10px 24px}.links a{overflow-wrap:anywhere}footer{border-top:1px solid var(--line);margin-top:35px;padding-top:18px;color:var(--muted);font-size:13px}dialog{border:0;border-radius:10px;padding:0;width:min(96vw,1800px);max-width:none;max-height:96vh;background:#102238;color:white}dialog::backdrop{background:rgba(4,13,25,.88)}.dialog-bar{display:flex;justify-content:space-between;align-items:center;padding:10px 18px;gap:20px}dialog button{border:1px solid #7b93ab;border-radius:5px;background:transparent;color:white;padding:3px 14px;cursor:pointer}dialog img{display:block;width:100%;max-height:84vh;object-fit:contain}dialog a{color:#c4e4ff}.dialog-caption{font-size:14px}@media(max-width:850px){.stats{grid-template-columns:repeat(2,1fr)}.grid-two,.gallery{grid-template-columns:1fr}.nav{gap:16px;justify-content:flex-start;overflow:auto;white-space:nowrap;font-size:13px}header{padding:30px 20px}main{padding:18px 14px 40px}.stage,.panel{padding:18px}h2{font-size:22px}}@media print{.nav,dialog{display:none}.panel,.stage,figure{break-inside:avoid}header{background:white;color:var(--ink);padding:20px}header p,header .meta{color:var(--muted)}main{max-width:none;padding:0}details{display:block}a{color:inherit}.gallery{grid-template-columns:repeat(2,1fr)}}
  5. </style></head><body>
  6. <header><div class="eyebrow">装备数字车间 · 权限治理与工作身份</div><h1>权限与角色切换验收报告</h1><p>按资料、产品原型、设计、开发、测试梳理本轮调整。页面与操作使用真实权限;管理员采用固定授权;内部维护与普通业务隔离;工作身份切换保护未保存内容。</p><div class="meta">日期 2026-09-06 · 生成时间 2026年9月6日 12:11:22(北京时间) · 证据来自当前报告目录</div></header>
  7. <nav class="nav" aria-label="报告章节"><a href="#sources">01 资料</a><a href="#prototype">02 产品原型</a><a href="#design">03 设计</a><a href="#development">04 开发</a><a href="#testing">05 测试</a><a href="#artifacts">证据文件</a></nav>
  8. <main><div class="notice pass" role="status"><span class="badge pass">通过</span><strong>本轮记录的验收项已通过</strong><p>真实接口、浏览器操作与构建/测试记录分别列示;测试覆盖范围和 OFD 本地入口边界仍需结合下文阅读。</p></div>
  9. <div class="stats"><div class="stat"><span>真实 API · 权限主流程</span><b>32 / 32</b><small>接口断言,不计作浏览器用例</small></div><div class="stat"><span>真实 API · 维护隔离边界</span><b>39 / 39</b><small>目录、审计、角色与写入保护</small></div><div class="stat"><span>浏览器 E2E · 当前结果</span><b>9 / 9</b><small>已标记为最终完整运行</small></div><div class="stat"><span>构建 / 单测 / DDL</span><b>通过</b><small>独立统计,详见验证记录</small></div></div>
  10. <section id="sources"><div class="section-label">01 · 资料</div><h2>以旧系统实际能力为迁移边界</h2><div class="stage"><p>旧环境采用 <code>SINGLE_ACTIVE</code>。管理员曾动态获得全部叶子权限,但内容写又被教员身份门槛阻止;本轮把这种不一致改为明确的页面权限与操作权限。</p><div class="table-wrap"><table><thead><tr><th>旧角色</th><th>旧授权</th><th>数据范围</th><th>迁移依据</th></tr></thead><tbody><tr><td>管理员 · admin</td><td>动态全部叶子权限</td><td><code>ALL</code></td><td>旧权限集合全选,但内容写仍被教员身份门槛阻止。</td></tr><tr><td>教员 · teacher</td><td>33 项显式权限</td><td><code>SYSTEM</code></td><td>保留原有制作、教学能力;未获授予的资源管理不自动新增。</td></tr><tr><td>学员 · student</td><td>14 项显式权限</td><td><code>SELF</code></td><td>保留本人或本人队组的学习与执行能力。</td></tr></tbody></table></div><p>动作合同记录 87 个新增动作和 14 个停用共用动作码。旧角色仅按原已持有权限和所属页面展开,自定义角色不自动补齐其他业务能力。</p><div class="links"><a href="data/role-permissions-audit-20260906.json">脱敏旧角色快照</a><a href="data/granular-permissions-contract.json">细分动作合同</a><a href="authorization-design.md">完整中文设计与验收约定</a></div></div></section>
  11. <section id="prototype"><div class="section-label">02 · 产品原型</div><h2>保持入口,把“能查看”和“能操作”说明白</h2><div class="grid-two"><article class="panel"><h3>页面与工作区</h3><p>三个登录身份入口保留。列表的新建、复制、删除、发布按各类内容权限显示;没有修改权限的三维工作区仍可查看,编辑、导入、自动保存和恢复写入受到控制。</p><p>管理员授权界面展示固定清单,不能在普通角色编辑中改动固定模板。勾选动作自动补齐所属页面;取消页面先确认依赖动作。</p></article><article class="panel"><h3>头像与个人中心</h3><p>只列出当前账号已分配且有效的工作身份。单角色生效模式下,切换前确认各工作区未保存内容;成功后重新加载身份、菜单和页签。</p><p>切换请求失败保留原页面。切换已提交但新身份加载失败时,不继续使用旧身份业务缓存。</p></article></div></section>
  12. <section id="design"><div class="section-label">03 · 设计</div><h2>页面、动作、身份和数据范围分别约束</h2><div class="stage"><div class="table-wrap"><table><thead><tr><th>内容类型</th><th>新建 / 复制</th><th>编辑 / 上传 / 封面</th><th>删除工程</th><th>发布 / 撤回发布</th><th>批准 / 驳回</th></tr></thead><tbody><tr><td>模型</td><td><code>content.model.create</code></td><td><code>content.model.update</code></td><td><code>content.model.delete</code></td><td><code>content.model.publish</code></td><td>不新增审批功能</td></tr><tr><td>场景</td><td><code>content.scene.create</code></td><td><code>content.scene.update</code></td><td><code>content.scene.delete</code></td><td><code>content.scene.publish</code></td><td>不新增审批功能</td></tr><tr><td>训练编排</td><td><code>content.training.create</code></td><td><code>content.training.update</code></td><td><code>content.training.delete</code></td><td><code>content.training.publish</code></td><td>不新增审批功能</td></tr><tr><td>作业指导书</td><td><code>content.ofd.create</code></td><td><code>content.ofd.update</code></td><td><code>content.ofd.delete</code></td><td><code>content.ofd.publish</code></td><td><code>content.ofd.review</code></td></tr></tbody></table></div><p>保存后发布需要修改与发布两项权限。只有发布权限时,提交已保存的服务端版本;OFD 还须复用与当前内容指纹一致的已保存交付物,缺失或过期时由编辑者处理。</p><div class="table-wrap"><table><thead><tr><th>迁移后角色</th><th>默认保留能力</th><th>明确边界</th></tr></thead><tbody><tr><td>管理员</td><td>固定普通管理授权、系统配置与既有教学/资源管理</td><td>不自动拥有内容写、学员执行/提交或内部维护权限。</td></tr><tr><td>教员</td><td>原有四类内容制作、教学管理与运行能力</td><td>原无 manage 的资源/知识管理不因拆码获得 CRUD。</td></tr><tr><td>学员</td><td>本人/本人队组的领取、执行、提交、指导书学习与反馈</td><td>SELF 范围、参与人、队伍、岗位和正式考试投影继续生效。</td></tr><tr><td>内部维护</td><td>受保护账号与有效内部角色共同确认后获得维护能力</td><td>普通人员/角色目录隐藏;内部审计与业务审计分别读取。</td></tr></tbody></table></div><p>任务中的教员、学员、观察员成员身份与平台角色名称解耦;成员身份不授予平台动作权限。业务参与范围、固定版本、乐观锁、发布前置条件继续由服务端校验。</p></div></section>
  13. <section id="development"><div class="section-label">04 · 开发</div><h2>实现范围与需要保留的边界</h2><div class="stage"><ul><li>内容四类动作、教学各渠道、资源管理真实操作分别授权;停用旧共用动作码,不设置永久兼容放行。</li><li>模型同源宿主桥接、场景与训练运行时、正式工作台及 OFD 本地入口同步只读和操作门控;模型静态生成包未在本轮重做。</li><li>所有工作区参与角色切换确认。场景加载或资源上传先阻止离开;已开始的截图上传、保存、发布等待完整结束再核查未保存状态。</li><li>前一工作区选择放弃、后一工作区取消时仍保留草稿。角色接口成功后整页进入总览,新启动重新取身份和菜单,页签按用户与角色隔离。</li><li>036 迁移固定管理员模板、细分已有角色能力并增加审计范围;维护凭据只经私有初始化流程设置,报告和 DDL 不包含凭据。</li></ul><div class="notice warn"><strong>OFD 本地入口的边界</strong><p>默认 OFD 原型仍使用本地存储。本轮接入真实会话和本地写权限保护,未将整个原型业务迁移至 API;不能将前端限制描述成完整服务端隔离、跨设备协作或新增审批功能。</p></div></div></section>
  14. <section id="testing"><div class="section-label">05 · 测试</div><h2>真实接口、浏览器、构建与单测分别验收</h2>
  15. <article class="panel"><h3>A. 真实 API:权限配置与多角色主流程</h3><p><span class="badge pass">通过</span> <strong>32 / 32</strong> 项通过 · <a href="api-checks.json">JSON 证据</a></p>
  16. <details><summary>逐项检查结果</summary><div class="table-wrap"><table><thead><tr><th>检查项目</th><th>结果</th><th>补充证据</th></tr></thead><tbody><tr><td>root 使用管理员登录入口进入内部维护角色</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通角色目录不显示内部维护角色</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>管理员角色采用固定的受限授权</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>管理员授权已持久化,不动态获得全权限</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>admin 无法通过接口修改固定管理员授权</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>root 无法通过接口修改固定管理员授权</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>admin 账号已取消 is_protected</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>用户目录不显示 root 维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通账号按 ID 查询 root 受保护</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通账号按 ID 停用 root 受保护</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通账号按 ID 重置密码 root 受保护</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通账号按 ID 删除 root 受保护</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>管理员无法访问维护审计</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通审计不混入 root 登录与操作记录</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>root 活动保留在可追溯的内部审计</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>目录提供模型独立动作与查看依赖</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>已移除旧的共用内容写权限</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>创建可配置的reader测试角色</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>保存reader细分授权</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>创建可配置的editor测试角色</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>保存editor细分授权</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>建立隔离的多角色测试账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>多角色测试账号正常登录</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>接口切换到reader角色</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>reader当前身份独立生效</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>reader拥有模型查看权限</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>只有查看权限时,直接新建接口返回403</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>接口切换到editor角色</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>editor当前身份独立生效</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>editor拥有模型查看权限</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>自定义非教员角色获得新增权限即可创建模型</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通管理员模型查看与创建权限相互独立</td><td><span class="badge pass">通过</span></td><td></td></tr></tbody></table></div></details>
  17. </article>
  18. <article class="panel"><h3>B. 真实 API:内部维护与审计隔离边界</h3><p><span class="badge pass">通过</span> <strong>39 / 39</strong> 项通过 · <a href="maintenance-boundaries.json">JSON 证据</a></p>
  19. <details><summary>逐项检查结果</summary><div class="table-wrap"><table><thead><tr><th>检查项目</th><th>结果</th><th>补充证据</th></tr></thead><tbody><tr><td>维护账号使用已分配的内部角色,普通管理员不带维护标记</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>admin 的普通角色目录和分页都隐藏内部角色</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>admin 不能按已知内部角色 ID 读取/详情</td><td><span class="badge pass">通过</span></td><td>HTTP 404</td></tr><tr><td>admin 不能按已知内部角色 ID 读取/users</td><td><span class="badge pass">通过</span></td><td>HTTP 404</td></tr><tr><td>admin 不能按已知内部角色 ID 读取/permissions</td><td><span class="badge pass">通过</span></td><td>HTTP 404</td></tr><tr><td>root 的普通角色目录和分页都隐藏内部角色</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>root 不能按已知内部角色 ID 读取/详情</td><td><span class="badge pass">通过</span></td><td>HTTP 404</td></tr><tr><td>root 不能按已知内部角色 ID 读取/users</td><td><span class="badge pass">通过</span></td><td>HTTP 404</td></tr><tr><td>root 不能按已知内部角色 ID 读取/permissions</td><td><span class="badge pass">通过</span></td><td>HTTP 404</td></tr><tr><td>角色 admin 的关联用户排除维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>角色 administrative 的关联用户排除维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>角色 teacher 的关联用户排除维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>角色 student 的关联用户排除维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>角色 group_leader 的关联用户排除维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>角色 rv_reader_1e43e04e 的关联用户排除维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>角色 rv_editor_1e43e04e 的关联用户排除维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>教学人员搜索不暴露维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>教学人员按 ID 解析拒绝维护账号</td><td><span class="badge pass">通过</span></td><td>HTTP 400</td></tr><tr><td>公开登录候选列表不暴露维护账号</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>admin 无法通过普通用户接口给测试账号授予内部角色</td><td><span class="badge pass">通过</span></td><td>HTTP 400</td></tr><tr><td>root 无法通过普通用户接口给测试账号授予内部角色</td><td><span class="badge pass">通过</span></td><td>HTTP 400</td></tr><tr><td>拒绝内部角色分配后测试账号原角色和版本保持不变</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>admin 无法通过普通编辑更改固定管理员数据范围</td><td><span class="badge pass">通过</span></td><td>HTTP 403</td></tr><tr><td>admin 无法停用固定管理员角色</td><td><span class="badge pass">通过</span></td><td>HTTP 403</td></tr><tr><td>root 无法通过普通编辑更改固定管理员数据范围</td><td><span class="badge pass">通过</span></td><td>HTTP 403</td></tr><tr><td>root 无法停用固定管理员角色</td><td><span class="badge pass">通过</span></td><td>HTTP 403</td></tr><tr><td>固定管理员角色状态、范围、版本未被边界请求改变</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通管理员不能读取维护审计/列表</td><td><span class="badge pass">通过</span></td><td>HTTP 403</td></tr><tr><td>普通管理员不能读取维护审计/stats</td><td><span class="badge pass">通过</span></td><td>HTTP 403</td></tr><tr><td>普通管理员不能读取维护审计/export</td><td><span class="badge pass">通过</span></td><td>HTTP 403</td></tr><tr><td>已知维护账号错误密码和未知账号均返回统一认证失败</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>维护登录失败在普通列表不可见且内部审计保留真实 USER 目标</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>未知身份失败仍可在普通安全审计中查到</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通审计导出不包含维护记录,维护导出保留该记录</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通审计统计只计算 BUSINESS,维护统计只计算 INTERNAL</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>普通用户和角色统计排除内部账号及内部角色</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>首页人数不计维护账号,最近动态不显示维护失败日志</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>数据库只读核验:已知维护失败 INTERNAL,未知失败 BUSINESS</td><td><span class="badge pass">通过</span></td><td></td></tr><tr><td>维护再次登录后普通 admin 仍未被重新保护</td><td><span class="badge pass">通过</span></td><td></td></tr></tbody></table></div></details>
  20. </article>
  21. <article class="panel"><h3>C. 浏览器 E2E:可见交互与异常路径</h3>
  22. <p><span class="badge pass">通过</span> <strong>9 / 9</strong> 项通过,0 项失败,0 项待完成/跳过/需复核 · <a href="results.json">完整 JSON</a> · 开始于 2026-09-06T04:01:43.840Z</p>
  23. <div class="table-wrap"><table><thead><tr><th>浏览器用例</th><th>结果</th><th>耗时</th><th>失败证据</th></tr></thead><tbody><tr><td>管理员授权固定,权限树细分到页面操作</td><td><span class="badge pass">通过</span></td><td>4.9 秒</td><td>—</td></tr><tr><td>权限依赖:勾选操作自动补齐查看,取消查看需要确认</td><td><span class="badge pass">通过</span></td><td>11.4 秒</td><td>—</td></tr><tr><td>头像弹窗切换自定义角色,清理旧缓存并改变操作能力</td><td><span class="badge pass">通过</span></td><td>6.2 秒</td><td>—</td></tr><tr><td>角色切换接口失败保留原身份与页面</td><td><span class="badge pass">通过</span></td><td>3.7 秒</td><td>—</td></tr><tr><td>切换已提交但身份加载失败,不展示旧角色业务页面</td><td><span class="badge pass">通过</span></td><td>5.1 秒</td><td>—</td></tr><tr><td>训练编排未保存时,取消切换保留当前内容与身份</td><td><span class="badge pass">通过</span></td><td>7.9 秒</td><td>—</td></tr><tr><td>未保存训练的保存请求失败时,阻止提交角色切换</td><td><span class="badge pass">通过</span></td><td>6.6 秒</td><td>—</td></tr><tr><td>权限加载乱序:关闭 A 后打开 B,A 的迟到响应不能覆盖 B</td><td><span class="badge pass">通过</span></td><td>6.2 秒</td><td>—</td></tr><tr><td>保存授权期间禁止恢复,PUT 成功但列表刷新失败仍保留提交快照</td><td><span class="badge pass">通过</span></td><td>6.5 秒</td><td>—</td></tr></tbody></table></div>
  24. <p class="muted">角色切换/身份获取/保存失败等异常路径使用浏览器定向响应模拟。它们验证前端保留状态与阻断行为,不表示真实服务发生过这些故障。</p></article>
  25. <article class="panel"><h3>D. 构建、聚焦单测与 DDL</h3><p><a href="validation.json">完整验证记录</a>。下方按实际执行的命令与结果展示,不把单测数量并入 E2E 或真实 API 数量。</p><pre>{
  26. &quot;time&quot;: &quot;2026-09-06T04:08:03.624366+00:00&quot;,
  27. &quot;overallStatus&quot;: &quot;PASS&quot;,
  28. &quot;e2eComplete&quot;: true,
  29. &quot;build&quot;: {
  30. &quot;web&quot;: &quot;PASS: pnpm build; Vite production build and postbuild 10 iframe payload checks&quot;,
  31. &quot;auth&quot;: &quot;PASS: Maven test/package&quot;,
  32. &quot;tran&quot;: &quot;PASS: Maven test/package&quot;
  33. },
  34. &quot;backendTests&quot;: {
  35. &quot;ut_auth&quot;: {
  36. &quot;tests&quot;: 112,
  37. &quot;failures&quot;: 0,
  38. &quot;errors&quot;: 0,
  39. &quot;skipped&quot;: 0
  40. },
  41. &quot;ut_tran&quot;: {
  42. &quot;tests&quot;: 340,
  43. &quot;failures&quot;: 0,
  44. &quot;errors&quot;: 0,
  45. &quot;skipped&quot;: 0
  46. }
  47. },
  48. &quot;nodeTests&quot;: {
  49. &quot;contentCoverPermissions&quot;: {
  50. &quot;tests&quot;: 57,
  51. &quot;failed&quot;: 0,
  52. &quot;command&quot;: &quot;node --test tools/content-permissions.test.mjs tools/training-cover.test.mjs tools/scene-cover-api.test.mjs&quot;
  53. },
  54. &quot;uiPermissionContracts&quot;: {
  55. &quot;tests&quot;: 12,
  56. &quot;failed&quot;: 0,
  57. &quot;command&quot;: &quot;node --test scripts/permission-selection.contract.test.mjs scripts/teaching-permissions.test.mjs&quot;
  58. }
  59. },
  60. &quot;realApi&quot;: {
  61. &quot;permissions&quot;: 32,
  62. &quot;maintenanceBoundaries&quot;: 39,
  63. &quot;failed&quot;: 0
  64. },
  65. &quot;e2e&quot;: {
  66. &quot;complete&quot;: true,
  67. &quot;passed&quot;: 9,
  68. &quot;failed&quot;: 0,
  69. &quot;flaky&quot;: 0,
  70. &quot;skipped&quot;: 0,
  71. &quot;screenshots&quot;: 12,
  72. &quot;command&quot;: &quot;pnpm exec playwright test --config role-permissions-live.config.ts&quot;
  73. },
  74. &quot;ddl&quot;: {
  75. &quot;status&quot;: &quot;PASS&quot;,
  76. &quot;file&quot;: &quot;20260906_036_role_permissions_maintenance.sql&quot;,
  77. &quot;sha256&quot;: &quot;66d6c32edef315c8fcd9264352dda3444ddcdf1ca2d50d2af6f138fc7f4d4bd1&quot;,
  78. &quot;environment&quot;: &quot;Development: unreal_tran / public, KingBase MySQL compatibility&quot;,
  79. &quot;execution&quot;: &quot;200 statements committed; same SHA rerun committed successfully&quot;,
  80. &quot;catalogLeafCount&quot;: 138,
  81. &quot;adminFixedLeafCount&quot;: 106,
  82. &quot;fullDdlUpdated&quot;: true,
  83. &quot;executionRecord&quot;: &quot;unreal_tran_api/database/迁移执行记录.md&quot;
  84. },
  85. &quot;runtime&quot;: {
  86. &quot;gateway&quot;: &quot;http://127.0.0.1:6180&quot;,
  87. &quot;authPort&quot;: 6101,
  88. &quot;tranPort&quot;: 6102,
  89. &quot;deployedArtifacts&quot;: {
  90. &quot;auth&quot;: {
  91. &quot;sha256&quot;: &quot;98aaeedfe29c8e75e99ef9421a3982ae52ab357eb8592c68a30dfc54e633ff85&quot;,
  92. &quot;runningCopyMatches&quot;: true
  93. },
  94. &quot;tran&quot;: {
  95. &quot;sha256&quot;: &quot;d66a38602e32c0db0c2680905f6a6461d9cd09ab12f4ba1481c87f99f583e828&quot;,
  96. &quot;runningCopyMatches&quot;: true
  97. }
  98. },
  99. &quot;rootBootstrap&quot;: &quot;Initialized from the local account file; one-time bootstrap disabled on restart; root login verified afterward&quot;,
  100. &quot;adminProtected&quot;: false
  101. },
  102. &quot;cleanup&quot;: {
  103. &quot;time&quot;: &quot;2026-09-06T04:07:09.247Z&quot;,
  104. &quot;projects&quot;: [
  105. &quot;165&quot;
  106. ],
  107. &quot;users&quot;: [
  108. &quot;100&quot;
  109. ],
  110. &quot;roles&quot;: [
  111. &quot;20&quot;,
  112. &quot;21&quot;
  113. ],
  114. &quot;status&quot;: &quot;PASS&quot;,
  115. &quot;postDeleteReads&quot;: &quot;All 4 isolated fixture records return HTTP 404&quot;
  116. },
  117. &quot;artifactChecks&quot;: {
  118. &quot;status&quot;: &quot;PASS&quot;,
  119. &quot;file&quot;: &quot;artifact-checks.json&quot;,
  120. &quot;diffWhitespace&quot;: {
  121. &quot;unreal_tran_api&quot;: &quot;PASS&quot;,
  122. &quot;unreal_tran_web&quot;: &quot;PASS&quot;,
  123. &quot;ute2e&quot;: &quot;PASS&quot;
  124. }
  125. },
  126. &quot;scopeNotes&quot;: [
  127. &quot;Root login and operation audits are retained as INTERNAL; ordinary lists, statistics and exports exclude them.&quot;,
  128. &quot;Admin has fixed persisted grants; both admin and root cannot edit that built-in role permission set through the application.&quot;,
  129. &quot;OFD legacy editor continues to store its existing local browser data; session permission checks were added, not a new server persistence workflow.&quot;,
  130. &quot;Permission and role-switch tests are the focus; this run does not claim complete regression of every teaching or 3D business scenario.&quot;,
  131. &quot;Temporary test fixtures were deleted; existing training used for dirty-state checks was not saved.&quot;,
  132. &quot;Development environment only; no production deployment, Git commit or push performed.&quot;
  133. ]
  134. }</pre></article>
  135. <article class="panel"><h3>E. 临时测试数据与清理</h3><p><a href="cleanup.json">清理证据</a>。业务模型、场景与训练的保留/恢复情况以记录为准。</p><pre>{
  136. &quot;time&quot;: &quot;2026-09-06T04:07:09.247Z&quot;,
  137. &quot;projects&quot;: [
  138. &quot;165&quot;
  139. ],
  140. &quot;users&quot;: [
  141. &quot;100&quot;
  142. ],
  143. &quot;roles&quot;: [
  144. &quot;20&quot;,
  145. &quot;21&quot;
  146. ],
  147. &quot;status&quot;: &quot;PASS&quot;,
  148. &quot;postDeleteReads&quot;: &quot;All 4 isolated fixture records return HTTP 404&quot;
  149. }</pre></article>
  150. <h3>截图证据 · 点击放大</h3><p class="muted">截图只证明相应画面。标记为“过程截图”的文件未关联当前结果,不计入当前通过数量。</p><div class="gallery"><figure><a class="shot" href="screenshots/01-admin-fixed-permissions.png" data-caption="管理员固定授权"><img src="screenshots/01-admin-fixed-permissions.png" alt="管理员固定授权" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">01-admin-fixed-permissions.png</span></div><h4>管理员固定授权</h4><p>验收后于 12:10:50(北京时间)等待完整加载后补拍:管理员权限面板展示固定授权,模型操作节点与保存授权被禁用。补拍仅执行读取,原始 Playwright 附件保持原记录。</p></figcaption></figure><figure><a class="shot" href="screenshots/02-action-adds-page-permission.png" data-caption="操作自动补齐查看权限"><img src="screenshots/02-action-adds-page-permission.png" alt="操作自动补齐查看权限" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">02-action-adds-page-permission.png</span></div><h4>操作自动补齐查看权限</h4><p>勾选模型新建时,同步勾选模型页面,避免产生不可达的操作权限。</p></figcaption></figure><figure><a class="shot" href="screenshots/03-removing-page-confirms-dependent-actions.png" data-caption="取消页面权限需确认"><img src="screenshots/03-removing-page-confirms-dependent-actions.png" alt="取消页面权限需确认" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">03-removing-page-confirms-dependent-actions.png</span></div><h4>取消页面权限需确认</h4><p>移除模型查看时提示其依赖操作会一起移除,取消后保留原勾选。</p></figcaption></figure><figure><a class="shot" href="screenshots/04-custom-reader-model-list.png" data-caption="自定义只读角色·首次加载"><img src="screenshots/04-custom-reader-model-list.png" alt="自定义只读角色·首次加载" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">04-custom-reader-model-list.png</span></div><h4>自定义只读角色·首次加载</h4><p>截图捕获了只读状态,但列表仍有加载遮罩,不能单独证明完整列表已加载。模型查看返回 200、新建返回 403 的结论见真实 API 断言。</p></figcaption></figure><figure><a class="shot" href="screenshots/05-avatar-role-switch-dialog.png" data-caption="头像菜单切换角色"><img src="screenshots/05-avatar-role-switch-dialog.png" alt="头像菜单切换角色" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">05-avatar-role-switch-dialog.png</span></div><h4>头像菜单切换角色</h4><p>仅列出当前账号已分配的有效工作身份,并标识当前身份。</p></figcaption></figure><figure><a class="shot" href="screenshots/06-custom-editor-model-list.png" data-caption="切换后按新授权显示操作"><img src="screenshots/06-custom-editor-model-list.png" alt="切换后按新授权显示操作" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">06-custom-editor-model-list.png</span></div><h4>切换后按新授权显示操作</h4><p>切换至获得模型新增权限的自定义角色后,新建入口可见。</p></figcaption></figure><figure><a class="shot" href="screenshots/07-failed-switch-keeps-current-role.png" data-caption="切换接口失败保留原身份"><img src="screenshots/07-failed-switch-keeps-current-role.png" alt="切换接口失败保留原身份" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">07-failed-switch-keeps-current-role.png</span></div><h4>切换接口失败保留原身份</h4><p>注入角色接口 503 响应后显示短消息,原身份和当前页面保留。</p></figcaption></figure><figure><a class="shot" href="screenshots/08-role-refresh-failure-clears-old-view.png" data-caption="身份重取失败不回放旧页面"><img src="screenshots/08-role-refresh-failure-clears-old-view.png" alt="身份重取失败不回放旧页面" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">08-role-refresh-failure-clears-old-view.png</span></div><h4>身份重取失败不回放旧页面</h4><p>角色切换已提交但身份接口失败时回到登录入口,不继续显示旧身份业务页面。</p></figcaption></figure><figure><a class="shot" href="screenshots/09-unsaved-training-blocks-role-switch.png" data-caption="未保存训练阻止切换"><img src="screenshots/09-unsaved-training-blocks-role-switch.png" alt="未保存训练阻止切换" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">09-unsaved-training-blocks-role-switch.png</span></div><h4>未保存训练阻止切换</h4><p>修改训练名称后切换角色触发离开确认;取消时不提交角色切换请求,修改仍保留。</p></figcaption></figure><figure><a class="shot" href="screenshots/09b-save-failure-blocks-role-switch.png" data-caption="保存失败阻止角色切换"><img src="screenshots/09b-save-failure-blocks-role-switch.png" alt="保存失败阻止角色切换" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">09b-save-failure-blocks-role-switch.png</span></div><h4>保存失败阻止角色切换</h4><p>模拟训练保存接口 503 后保留原身份与输入,角色切换请求未提交。</p></figcaption></figure><figure><a class="shot" href="screenshots/10-stale-permission-response-is-ignored.png" data-caption="忽略过期权限响应"><img src="screenshots/10-stale-permission-response-is-ignored.png" alt="忽略过期权限响应" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">10-stale-permission-response-is-ignored.png</span></div><h4>忽略过期权限响应</h4><p>权限编辑切换上下文后的延迟响应不应覆盖当前角色。此截图的通过状态以关联测试记录为准。</p></figcaption></figure><figure><a class="shot" href="screenshots/11-permission-save-survives-list-refresh-failure.png" data-caption="保存成功后列表刷新失败"><img src="screenshots/11-permission-save-survives-list-refresh-failure.png" alt="保存成功后列表刷新失败" loading="lazy" width="1280" height="720"></a><figcaption><div><span class="badge pass">当前 E2E 通过</span><span class="filename">11-permission-save-survives-list-refresh-failure.png</span></div><h4>保存成功后列表刷新失败</h4><p>权限写入成功与随后列表刷新失败分别处理,避免误报授权未保存。此截图的通过状态以关联测试记录为准。</p></figcaption></figure></div></section>
  151. <section id="artifacts"><div class="section-label">证据文件</div><h2>可随 Git 交付的材料</h2><div class="panel"><div class="links"><a href="authorization-design.md">authorization-design.md</a><a href="data/role-permissions-audit-20260906.json">data/role-permissions-audit-20260906.json</a><a href="data/granular-permissions-contract.json">data/granular-permissions-contract.json</a><a href="api-checks.json">api-checks.json</a><a href="maintenance-boundaries.json">maintenance-boundaries.json</a><a href="results.json">results.json</a><a href="validation.json">validation.json</a><a href="cleanup.json">cleanup.json</a></div><p class="muted">data/ 仅包含脱敏角色/权限快照与动作合同。本生成器不读取或复制账号配置、私有测试 fixtures、数据库备份、服务日志或会话凭据。</p></div></section>
  152. <footer>结果只反映本报告目录当前证据。重新执行 <code>node tools/build-role-permissions-report.mjs</code> 可刷新报告;新增文件不会自动被当成通过。</footer></main>
  153. <dialog id="image-dialog" aria-label="放大截图"><div class="dialog-bar"><span class="dialog-caption"></span><div><a id="original-image" target="_blank" rel="noopener">打开原图</a> <button id="close-image" type="button">关闭 ×</button></div></div><img alt=""></dialog>
  154. <script>
  155. const viewer=document.getElementById('image-dialog'), large=viewer.querySelector('img');
  156. document.querySelectorAll('a.shot').forEach(link=>link.addEventListener('click',event=>{event.preventDefault();large.src=link.href;large.alt=link.dataset.caption;viewer.querySelector('.dialog-caption').textContent=link.dataset.caption;document.getElementById('original-image').href=link.href;viewer.showModal()}));
  157. document.getElementById('close-image').addEventListener('click',()=>viewer.close());
  158. viewer.addEventListener('click',event=>{if(event.target===viewer)viewer.close()});
  159. </script></body></html>