- 勘误③真落:docs/T3-report.md §2 V32「两个不同教师」补注(实为两个 roleId=7 操作者,教师(1) 按 P21 无代填权) - P47 收紧:FormModuleKeys.anchorCustomIds 只认 path 空节点的 customId,DataScopeService.hasScopeAllAnchor 切过去;补「带 path 的导航行误配 fc:scope:all 不升级范围」用例(mvn -o clean package 178 例全绿) - docs/T5-report.md:T5 做了什么 / 逐条自验证据 / 问题单 P48~P53 / 两仓推送与 CI
This commit is contained in:
+1
-1
@@ -91,7 +91,7 @@
|
|||||||
| **V29** | 填表人无编辑/撤回接口可用(404/403);管理员编辑成功且写 `answer_op_log` | 404:`FormAnswerController` 只有 `submit`/`upload` 两个端点(结构即证据)+ `FormAnswerAdminController` 的守卫;403:`FormAdminGuardTest.onlyRoleIdsTwoAndSevenManage`。`AnswerAdminIT.adminEditWritesOpLogAndRebuildsIndex`:编辑成功、`values_json` 更新、`op_log` 恰一行 `op=edit`+操作人正确、**身份列与状态列不动**、索引按钉住版本重建(`v_num` 变成 88);`editStillEnforcesValueContract` 证明被拒时**不落** op_log、原值不被破坏 |
|
| **V29** | 填表人无编辑/撤回接口可用(404/403);管理员编辑成功且写 `answer_op_log` | 404:`FormAnswerController` 只有 `submit`/`upload` 两个端点(结构即证据)+ `FormAnswerAdminController` 的守卫;403:`FormAdminGuardTest.onlyRoleIdsTwoAndSevenManage`。`AnswerAdminIT.adminEditWritesOpLogAndRebuildsIndex`:编辑成功、`values_json` 更新、`op_log` 恰一行 `op=edit`+操作人正确、**身份列与状态列不动**、索引按钉住版本重建(`v_num` 变成 88);`editStillEnforcesValueContract` 证明被拒时**不落** op_log、原值不被破坏 |
|
||||||
| **V30** | 软删后列表/详情/统计/导出均不可见,索引行同步删除;无物理删除接口存在 | `AnswerAdminIT.softDeleteIsTheOnlyDeletion`:`is_deleted=1`、`answer_index` 行清空、写 `op_log`、`answerRepository.findById` 仍在(物理行还在)、重复删被拒。"四读出路径不可见"依赖 T4 读接口(本轮刻意不建,见 §1.5);**无物理删除**=两个仓库都不提供 delete 方法、控制器无端点(`AnswerRepository` javadoc 成文)。`batchSoftDeletePerItemLogging` 覆盖批量同口径 |
|
| **V30** | 软删后列表/详情/统计/导出均不可见,索引行同步删除;无物理删除接口存在 | `AnswerAdminIT.softDeleteIsTheOnlyDeletion`:`is_deleted=1`、`answer_index` 行清空、写 `op_log`、`answerRepository.findById` 仍在(物理行还在)、重复删被拒。"四读出路径不可见"依赖 T4 读接口(本轮刻意不建,见 §1.5);**无物理删除**=两个仓库都不提供 delete 方法、控制器无端点(`AnswerRepository` javadoc 成文)。`batchSoftDeletePerItemLogging` 覆盖批量同口径 |
|
||||||
| **V31** | 同幂等键重复提交只产生一条答卷 | `AnswerSubmitIT.idempotencyReturnsOriginalAnswer`:两次同键 → 库里 1 行、第二次 `answerId` 相同且 `replayed=true`、**原值不被第二次的值覆盖**;`idempotencyKeyRequired`(必填/≤64/不同键各成一条);并发同键见 V32 |
|
| **V31** | 同幂等键重复提交只产生一条答卷 | `AnswerSubmitIT.idempotencyReturnsOriginalAnswer`:两次同键 → 库里 1 行、第二次 `answerId` 相同且 `replayed=true`、**原值不被第二次的值覆盖**;`idempotencyKeyRequired`(必填/≤64/不同键各成一条);并发同键见 V32 |
|
||||||
| **V32** | 限一次:subject 非空按 subject 限(两老师代填同一学生第二条被拒)、subject 空按 submitter 限(同人第二次被拒)——**两种 limitScope 各验一条**;**并发同键双提交恰一条成功(fill_quota 唯一键兜底)**;**软删后可重填(名额已释放)**;不限次表单不受影响 | 功能面:`onceLimitIsPerSubjectWhenSubjectPresent`(**两个不同教师**代填同一学生→第二条"限填一次";换学生仍可填)、`onceLimitFallsBackToSubmitter`(同人第二次被拒 + 不限次表单两条都过)、`softDeleteReleasesQuota`(软删后同 subject 可重填)。**并发面**:`AnswerConcurrencyIT.concurrentSubmitsAgainstOnceLimitProduceExactlyOneAnswer`(真 MySQL 双线程同时放开:恰 1 条 `answer`、恰一条成功另一个拿到"限填一次"、`fill_quota` 恰 1 行 `limit_key=提交人id`、成功那条索引照常);`concurrentSubmitsWithSameIdempotencyKeyProduceOneAnswer`(同幂等键并发:恰 1 行,两个返回值指向同一 uuid——即"撞 uk 后回读原答卷"的并发结局) |
|
| **V32** | 限一次:subject 非空按 subject 限(两老师代填同一学生第二条被拒)、subject 空按 submitter 限(同人第二次被拒)——**两种 limitScope 各验一条**;**并发同键双提交恰一条成功(fill_quota 唯一键兜底)**;**软删后可重填(名额已释放)**;不限次表单不受影响 | 功能面:`onceLimitIsPerSubjectWhenSubjectPresent`(**两个不同教师**(实为两个 roleId=7 操作者——教师(1) 按 P21 无代填权)代填同一学生→第二条"限填一次";换学生仍可填)、`onceLimitFallsBackToSubmitter`(同人第二次被拒 + 不限次表单两条都过)、`softDeleteReleasesQuota`(软删后同 subject 可重填)。**并发面**:`AnswerConcurrencyIT.concurrentSubmitsAgainstOnceLimitProduceExactlyOneAnswer`(真 MySQL 双线程同时放开:恰 1 条 `answer`、恰一条成功另一个拿到"限填一次"、`fill_quota` 恰 1 行 `limit_key=提交人id`、成功那条索引照常);`concurrentSubmitsWithSameIdempotencyKeyProduceOneAnswer`(同幂等键并发:恰 1 行,两个返回值指向同一 uuid——即"撞 uk 后回读原答卷"的并发结局) |
|
||||||
| **V33** | 截止后填报页只读、提交接口拒绝;停用表单入口失效但数据管理可见 | `deadlineBlocksSubmit`(2020 年截止→"已过收集截止时间";改 2099→放行,顺带证"只选日期补 23:59:59")、`disabledFormClosesFillEntryOnly`(停用后提交被拒"已停用",`enable` 后同表单同数据恢复可填)、`AnswerAdminIT.domainScopingAndSoftDeletedFormStillOwnsAnswers`(表单软删后其存量答卷仍可编辑=数据侧可见可管理) |
|
| **V33** | 截止后填报页只读、提交接口拒绝;停用表单入口失效但数据管理可见 | `deadlineBlocksSubmit`(2020 年截止→"已过收集截止时间";改 2099→放行,顺带证"只选日期补 23:59:59")、`disabledFormClosesFillEntryOnly`(停用后提交被拒"已停用",`enable` 后同表单同数据恢复可填)、`AnswerAdminIT.domainScopingAndSoftDeletedFormStillOwnsAnswers`(表单软删后其存量答卷仍可编辑=数据侧可见可管理) |
|
||||||
| **V14**(编排补充①) | 填表人改状态被拒(接口层) | `AnswerAdminIT.submitterCannotChangeStatus`:对学生(3)/教师(1) 身份调控制器实际走的 `adminGuard.requireAdmin` → FORBIDDEN"无表单管理权限",对管理员放行;数据面不存在"我改我这条的状态"的端点 |
|
| **V14**(编排补充①) | 填表人改状态被拒(接口层) | `AnswerAdminIT.submitterCannotChangeStatus`:对学生(3)/教师(1) 身份调控制器实际走的 `adminGuard.requireAdmin` → FORBIDDEN"无表单管理权限",对管理员放行;数据面不存在"我改我这条的状态"的端点 |
|
||||||
| **V15**(编排补充①) | 改状态写 `status_log`;系统不存在任何流转规则校验 | `statusChangeHasNoTransitionRules`:`pending → back → pending → done`(含倒退)全部放行,`status_log` 三行按序、操作人正确;`statusMustBeLiveDefinition` 证明唯一约束是"目标值必须是未废弃定义",**不是**转移规则 |
|
| **V15**(编排补充①) | 改状态写 `status_log`;系统不存在任何流转规则校验 | `statusChangeHasNoTransitionRules`:`pending → back → pending → done`(含倒退)全部放行,`status_log` 三行按序、操作人正确;`statusMustBeLiveDefinition` 证明唯一约束是"目标值必须是未废弃定义",**不是**转移规则 |
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# T5 执行说明 · 前端·壳与管理端框架
|
||||||
|
|
||||||
|
> 任务书 T5(v1.1)|执行手册 §1 模板组合「前端 = fork notice/front」+ §2.1(一个 SPA 承载全部形态)+ §2.2(模块行表与 path 空行口径)|方案 v1.4 §3.3(页面即组件、`self != top` 剥壳)+ §7.3(《云海子应用前端接入规范》v1.11)+ §9 V22
|
||||||
|
> 主体改动 = **front 仓**(`form-center/form-center-front`);service 仓两处 T4 验收遗留小改;YschTheme 仓 consumers.json 登记。
|
||||||
|
> 本轮不自创设计:值形状、控件、接口口径全部沿用契约,前端只做壳与骨架。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 做了什么
|
||||||
|
|
||||||
|
### 1.1 notice/front 基线整体迁入并剥领域(不是改造 T1 裸壳)
|
||||||
|
|
||||||
|
T1 留的 `front` 只有 `App.vue + main.ts + 仅 vue 依赖`。本轮把 notice/front 的**壳基础设施**落进来并剥净领域,形成 47 个源文件(另 25 个 svg 资产)的工程:
|
||||||
|
|
||||||
|
| 迁入件 | 来源 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `index.html` | Notice 现行版 | env-config 同步引导 + **A 类 loader(§2.1 原文照抄,未改一字)**;title 走 `createHtmlPlugin` 注入「表单中心」 |
|
||||||
|
| `vite.config.ts` | Notice 现行版 | vue / svg-icons / vite-plugin-mock / auto-import / html / UnoCSS 六插件 + `base:"."`(P11 已就位)+ **port 8089(P6)** + `/api`→8086、`/static`→中央通道(`YH_THEME_PROXY_TARGET` 可覆盖)代理 + 产物恒 `dist` 根(zip 根含 index.html,§7 坑) |
|
||||||
|
| `src/utils/alova/{alova-core,alova-iview}.ts` | Notice | 信封解包(`code==0`→data、`302`→挂起 Promise 跳 CAS)、`yschapirequest/yschreferer` 头、800ms 延迟 Spin |
|
||||||
|
| `src/components/layout.vue` | **Notice refined 版(13afb22)** | 菜单 icon 兜底 `icon-list` + 面包屑首页图标行内基线流 `-0.125em`(规范 §4 两行)+ Logo `yh-logo-url` 平台键优先/本仓兜底 + `yh-appearance-updated` 监听 + 无配色切换器(§3 红线) |
|
||||||
|
| `src/utils/hook/useElementHeight.ts`、`src/components/TablePageAlovaNew.vue` | Notice/flow-center 同构 | 通道撑满高度模型(§4.3 三硬约束)+ `.ysch-spin` loading 插槽形态(§4 表格加载行) |
|
||||||
|
| `src/utils/tableActions.ts` | **UserBase 同构源** | `actionColumn/normalizeActions/splitActions/actionCell`,显式 `mode`(§5.1)+ `disabledWhen` 置灰保位(§5.2 红线) |
|
||||||
|
| `src/utils/{index,menuRoute,mitt}.ts`、`src/utils/directive/*`、`src/assets/styles/*`、`src/assets/icons/*` | Notice | `Utils` 类(旧 `operatingHandle` 保留为薄封装,§5.4)、main.scss 含 `.op-cell`/`.table-spin-load`/固定层事件/省略号 Tooltip 四段 |
|
||||||
|
| `src/components/{UserAvatar.vue,utils/avatar.ts}` | **YschTheme `npm run sync` 下发** | 与通道源 MD5 逐字节一致(`9ac72f8b`/`1fa7163c`),sync --check 报「全部对齐」 |
|
||||||
|
|
||||||
|
**剥除清单**(任务书「剥 notice 全部领域 views 与 mobiapi」):`views/notice/*` 8 页、`api/module/{notice,approval,cc}.ts`、`assets/mock/noticeapi.ts`、`assets/config/menu.ts`(本地菜单兜底=假象,与后端「不保留本地 MENUS 兜底」同口径,一并剥)、`utils/alova/alova-vant.ts`(**mobiapi 腿**,指向 `/mobiapi` 的移动端实例)、`NoticeUpload/ThemeSwitcher/GradeClassSel/BschSubjectSelect/UserCheckModal/MyTree*` 等领域或切换器组件、`utils/{axios,diagramImg,bschSsoRedirect,flowLayout…}` 与 index.html 的 **ztree+jquery 库引用**(树件随 T8 需要时按平台件再引)。重量依赖 `@wangeditor`、`pdfjs-dist`、`@open-file-viewer`、`vuedraggable`、`axios`、`lodash`、`@alova/wormhole` 未装(各自属 T6/T7 的编辑/预览/拖拽需求,届时显式加)。
|
||||||
|
|
||||||
|
### 1.2 左导航(GetUserMod)与 path 空跳过
|
||||||
|
|
||||||
|
- **`src/utils/menuNav.ts`(新增,本轮唯一的导航判定实现)**:纯函数 + 解析器注入(`buildNavTree(modList, resolve)`),三条规则写在文件头——① **跳过判据取平台原始 `path` 是否为空**(不取解析结果:靠名字/modId 猜出路由再显示就是把权限锚点画成死菜单);② 有可渲染子节点的分组行保留为**分组头**(不可点),子节点全被跳则**整组不渲染**;③ 原始 path 非空但解析不出已注册路由的行**照常显示**、点击时 `Message.warning` 点名配置错误(不静默少一条菜单)。
|
||||||
|
- 高亮/面包屑按 `flattenNav` 对照(分组头不入表),`firstNavPage` 决定登录后的默认落地,`isSingleNav` 承 Notice 的「单模块独占」语义。
|
||||||
|
- `App.vue`:拿 `GetUserInfo + GetUserMod` → `buildNavTree` → 交 `layout.vue` 渲染;**失败只降成"导航为空",不造兜底树**(后端注释同口径)。DEV 下 `console.table` 同时打原始树与过滤后导航,冒烟可直接对比。
|
||||||
|
|
||||||
|
### 1.3 `api/module/base.ts` 形态与 appCode 参
|
||||||
|
|
||||||
|
`BASE="/api/base"` 的 `getUserInfo()` / `getUserMod(appCode?)`(**参数名按 P2 定稿 appCode**)。文件头写明:path 空行只由后端 `DataScopeService` 扫描消费、接口不过滤,前端只在导航层跳过。**P3 口径**:H4 未上线时该请求返回空树,页面即空态,不回落本应用树(mock 侧同一条判定已实现并在冒烟 D 段断言)。
|
||||||
|
|
||||||
|
一处必要偏离:Notice 的 `new Method<T>("GET", …)` 在 alova 3.5.5 类型下**不合法**(`Method` 的第一泛型位是 `AlovaGenerics` 而非响应类型,strict vue-tsc 报 TS2344),本端改 `alovaInstance.Get<T>()` ——同一个 Method 对象、同一套拦截器与信封解包,行为等价(P49)。
|
||||||
|
|
||||||
|
### 1.4 路由骨架:一个 SPA 四形态(手册 §2.1)
|
||||||
|
|
||||||
|
`createWebHashHistory`(Notice/flow-center 同族),路由表即 `/admin/{forms,data,stats,dicts}`、`/f/:code`、`/app/:appCode[/:view]`、`/my`,`/` = 入口页、`/:pathMatch(.*)*` → `/`(外部深链不白屏)。产物部署在 `/form-center/front/`,故手册写的 `/form-center/f/{code}` = ctx + `#/f/{code}`。
|
||||||
|
|
||||||
|
- **`meta.modId` 全部不写**(规范 §4「modId 全量对齐后端 GetUserMod,禁臆造」):本应用模块行除 `fc:scope:all` 外 customId 均未配(手册 §2.2 表为「—」),后端 `resolveModId` 因此回退 `app_module` 主键 id,该值跨环境不稳定,写进路由必然失配;高亮与面包屑按 path 匹配。
|
||||||
|
- 占位页 7 个(`views/admin/*` 4 + `Fill` + `AppContext` + `My` + `Entry`),统一走 `components/PageStub.vue` 的 `page-wrapper.yh-fill > main-container` 骨架 + 常驻 `Alert type=info show-icon`(§4.2 无 closable/banner)。**占位页刻意不挂 `.table-region`**——没有 Table 却铺一个"应当被量高"的容器,会留下假范式(§4.3 三硬约束的成立前提是有 Table 喂回高度)。零领域逻辑、零接口调用(`Fill`/`AppContext` 只把 `code`/`appCode` 与形态显示出来)。
|
||||||
|
|
||||||
|
### 1.5 `self != top` 剥壳(方案 §3.3)
|
||||||
|
|
||||||
|
`App.vue` 顶层判 `window.self !== window.top` → 嵌入态裸渲染 `<router-view>`:不画 layout、**不发两个鉴权请求**(嵌入上下文取不到本应用会话,失败会弹错误并画出带壳页),并内联旁路 `_base.scss` 的 `body{min-width:1200px}`(740px Drawer 里不旁路就整体右偏+右裁,flow-center 同款手法)。显式 `?mode=` 闭集留给 T7,本轮只做 `self!=top` 这一条兜底。
|
||||||
|
|
||||||
|
### 1.6 主题通道登记(YschTheme 仓)
|
||||||
|
|
||||||
|
`consumers.json` 新增 `form-center` 条目(`path:"../form-center/front"`、`target:"pc"`、`files` 只登记 avatar 两件——按 §3 红线不建配色切换器,故不登记 `theme.ts`/`ThemeSwitcher.vue`;ctx=/form-center/ 写进 note);同步更新 `tests/scripts.spec.ts` 的「消费端清单显式可控」断言(清单加一条 + form-center files 显式断言)。`npm run sync` → form-center 两件报「一致」;`npx vitest run` → **8 文件 84 例全绿**,其中 `iview-version.spec` 即「消费端 lock 实装版本 === 通道 devDependencies 精确版本」的版本锁测试,form-center 通过=**1.3.24 锁红不再失守**。
|
||||||
|
|
||||||
|
### 1.7 service 仓两处顺带(T4 验收遗留,不喧宾夺主)
|
||||||
|
|
||||||
|
- **勘误③ 真落**:`docs/T3-report.md` §2 的 V32 行「**两个不同教师**」后补注「(实为两个 roleId=7 操作者——教师(1) 按 P21 无代填权)」。已核源码:`AnswerSubmitIT.onceLimitIsPerSubjectWhenSubjectPresent` 用的是 `user(10001, SCH, 7)` 与 `user(10002, SCH, 7)`,注释属实。T4 报告 §1.6 声称已改、实测未改,本轮落到文本。
|
||||||
|
- **P47 收紧**:`FormModuleKeys` 的 customId 扫描改为**只收 path 空节点**(新方法 `anchorCustomIds`,`collect` 单处分支;旧 `customIds` 无其他调用方,一并收敛不留死代码),`DataScopeService.hasScopeAllAnchor` 切过去。补测 `DataScopeServiceTest.customIdOnNavRowDoesNotUpgradeScope`:带 path 的导航行误配 `fc:scope:all` → 仍 `SELF_SUBMITTED`;同一用户同一值改挂 path 空行 → 立刻 `ALL`(证明收窄生效、不是扫描整体失效)。配套给 `InMemoryModuleTreeSource` 加 `grantPathWithCustomId` 桩方法。**契约未改**(§8 的「锚点行形态」注记 T4 已补)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 自验证据(逐条)
|
||||||
|
|
||||||
|
取证脚本:**`form-center/local-debug/t5-shell-smoke.sh`**(13 断言,ASCII 锚点,JSON 交 node 解析不在 shell grep 中文)。终态运行结果 **`13 pass / 0 fail / 1 skip`**。
|
||||||
|
|
||||||
|
| 项 | 判据 | 证据 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `npm run build` | 必须过 | ✅ `✓ built in 5.54s`(脚本 A 段 `PASS build`) |
|
||||||
|
| `npm run type-check` | 必须过(`vue-tsc --noEmit`,strict + `noUnusedLocals/Parameters`) | ✅ 0 错误(脚本 A 段 `PASS type-check`)。Notice 基线在本机 `vue-tsc` 是坏的(`store/index.ts` 无 `IModule`、`Basics.d.ts` 写 `page: !number`),本轮按 T1~T4「本地门禁 + CI 只证构建链」的口径把 type-check 做成硬门禁,故对迁入件做了最小整形(P49/P50) |
|
||||||
|
| **V22** build 通过 | 方案 §9 原文 | ✅ 上一行 |
|
||||||
|
| **V22** consumers.json 已登记 | 规范 §2.3 | ✅ 脚本 B 段 `PASS consumers.json 已登记 form-center(ctx=/form-center/)`;`npm run sync` 报 `一致 form-center src/utils/avatar.ts / UserAvatar.vue`、`[sync] 全部对齐`;`npx vitest run` 84/84 绿 |
|
||||||
|
| **V22** view-ui-plus 锁 1.3.24 | 规范 §1/§2.3 | ✅ `package.json` 声明 `"1.3.24"`(无 `^`)+ `package-lock.json` 实装 `1.3.24`(脚本 B 段)+ 通道 `iview-version.spec` 通过 + `npm ls view-ui-plus` → `1.3.24` |
|
||||||
|
| **V22** 本地无 layout.css/icons.css | 规范 §2.2 回收步 | ✅ 脚本 B 段 `PASS no-local-layout-css-icons-css`(`find src -name layout.css -o -name icons.css` 为空);`main.scss` 无相关 import;`dist/index.html` 保留 `yh-theme-init.js` loader 与 `api/env-config` 引导、`%VITE_THEME_HOST%` 未被烧值 |
|
||||||
|
| **V22** 控件目录含 `app.vue` 槽位 | — | ⏭ 任务书已注明:**app.vue 槽位一条留 T6 验**(控件目录随设计器产生,本轮无控件) |
|
||||||
|
| 登录出壳 + 左导航(菜单树=手册 §2.2 表) | 任务书自验 | **降级取证**(P48,原因见 §3):`npm run dev:mock` + in-app browser a11y 快照——顶栏 `Logo|分隔线|表单中心|Mock 管理员|退出`、左导航**恰 5 项**(表单设计/数据管理/统计/字典管理/我的填写)、**「数据全量」(path 空) 未出现**、面包屑 `首页 > 表单中心 > 表单设计`、URL 自动落 `#/admin/forms`(首个可跳转项)。截图不可用(in-app browser 无可见 viewport),以结构化快照为据 |
|
||||||
|
| path 空跳过 / 分组收敛 / 无授权仅「我的填写」 | 手册 §2.2 验证口径 | ✅ 脚本 C 段:`esbuild` 单独转译 `utils/menuNav.ts` 后在 node 里断言——6 行树(含 1 行 path 空)→ 导航 5 项且锚点不在;`[我的填写, 锚点]` → 1 项且 `isSingleNav=true`;子项全锚点的分组整组不渲染、分组头不入高亮表 |
|
||||||
|
| 外指 appCode 空树不回落 | P2/P3 | ✅ 脚本 D 段:mock 侧与后端同一条判定(`appCode=some-other-app` → `data=[]`);前端 `getUserMod(appCode)` 是唯一入口、无任何「失败即取本应用树」的代码路径 |
|
||||||
|
| `self != top` 剥壳 | 方案 §3.3 | ✅ 同源 iframe 探针(dev:mock 页内动态挂 740px iframe 指 `#/f/DEMO_PROBE`):顶层 `.header-container/.left-menu/.path-nav` 各 1,iframe 内**各 0**、`self!==top=true`、`body.style.minWidth="0px"`、正文显示「形态 = 页面块(self != top,已剥壳)」 |
|
||||||
|
| P6 本地端口 | 手册 §5 | ✅ front dev=8089(`vite.config.ts`)、service=8086(`/api` 代理 target) |
|
||||||
|
| P11 壳的平台约定补齐 | 手册 §5 | ✅ loader/alova/router/auto-import/svg-icons/env-config 引导/`base:"./"` 全落,T1 留在 `vite.config.ts` 的 TODO 注释已删 |
|
||||||
|
| **service 两处小改** | 本地 `mvn test` 门禁 | ✅ `mvn -B -o clean package` → **BUILD SUCCESS,Tests run: 178, Failures: 0, Errors: 0**(T4 基线 177 + 本轮 P47 新增 1 条) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 问题单(本轮登记,编号接续 P47;供执行手册 §5 裁决)
|
||||||
|
|
||||||
|
| # | 日期 | 批次 | 问题 | 本轮取值(最保守解读) | 是否回文档修订 |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| P48 | 10-14 | T5 | 任务书要求的「登录态冒烟(CAS/UserBase 本地起 → 真会话出壳 + 菜单树=§2.2 表)」不可执行 | **按 P1 先例降级取证**,两条独立事实:①本机 CAS(8443)/UserBase(8060) 未运行(`curl` 均无响应),Redis 亦未起;②**更本质的缺口**——本地 `bsch_userbase` 里 `application_info` 中 `app_code='form-center'` 的行数=**0**、`app_module` 无任何 form-center 模块行(查得 `select count(*) … where a.app_code='form-center'` → 0,`app_module` 按 app_id 分组无对应 id),即 H2(UserBase 建中心应用行 + §2.2 模块行 + 组授权)未做。即便把三件服务起齐,`GetUserMod` 也只能返回空树,拿不到 §2.2 那棵树。补证=脚本 C 段纯函数断言 + D 段 mock 树 + 浏览器结构化快照(§2 表)。**H2 完成后 T8/T9 需真会话复跑一次** | 否(缺口在外部数据,非设计) |
|
||||||
|
| P49 | 10-14 | T5 | Notice 的 `api/module` 形态 `new Method<T>("GET", …)` 与 alova 3.5.5 的类型不兼容(`Method` 第一泛型位是 `AlovaGenerics`),strict `vue-tsc` 恒报 TS2344;flow-center/open-gateway 同病(它们的 `npm run type-check` 从没跑绿过,规范 §8 也写「vue-tsc 损坏勿依赖」) | 本端改 `alovaInstance.Get<T>(url, {params})`:同一个 Method 对象、同一套 `beforeRequest/responded` 与信封解包,行为等价、类型正确。**T6~T8 的 api/module 文件沿此写法**(其余形态:`BASE` 常量 + class + `export const xxxApi` 不变)。不改 Notice 已验收工程 | 否(工程写法,非契约) |
|
||||||
|
| P50 | 10-14 | T5 | 四端同构件 `utils/tableActions.ts` 落第五端(本端 type-check 是门禁) | 照 open-gateway 先例:顶部补 `// @ts-nocheck` + 4 行说明,**正文与同构源逐字节一致(MD5 `973393684783c740399b8eb9e97369bc`,与 UserBase/flow-center 全等)**,strict 下两处告警均无害(`h` 被形参遮蔽、`it.on()` 已在 normalize 闭包绑定)。规范 §8「与四端 MD5 对齐」的判据按"正文对齐 + 端顶补丁"执行,同 open-gateway 一轮的既有先例 | 否 |
|
||||||
|
| P51 | 10-14 | T5 | 新端的操作列形态(§5.1 要求「工程级恒定」,Notice=icon / UserBase=text)本轮没有列表页可定 | 暂按同族 center 应用取 **text 恒定**(flow-center/svc-hall/open-gateway 皆 text),`mode` 显式声明、缺省 text;**T8 出第一个真实列表页时若用户要 icon 形态,改的是各页 `mode` 参数而非工具件** | 否 |
|
||||||
|
| P52 | 10-14 | T5 | 剥领域后的「回补时机」未写:index.html 的 ztree+jquery、`@wangeditor`/`pdfjs-dist`/`@open-file-viewer`/`vuedraggable`/`axios`/`lodash` 均未随 fork 带入 | 一律**按需再引**(设计器 T6、附件预览与拖拽 T6/T7、组树选择器 T8),不提前铺空依赖:本轮 `npm ci` 518 包、构建 5.5s,若照搬 notice 全量依赖会显著拖慢 CI 且产物里多份未用图形资产(§4 表格加载红线:应用侧零图形资产)。回补时在对应批次报告里点名 | 否 |
|
||||||
|
| P53 | 10-14 | T5 | 入口页 `/` 的落地语义方案未写:Notice 把 `/` 做成真实业务页(已收到),flow-center 用 `redirect` | `/` = 真实入口页(无模块时的可诊断空态),登录后由壳 `replace` 到首个可跳转项;兜底 `/:pathMatch(.*)*` → `/` 而非某个具体管理页(**无授权用户不该被送到他进不去的页面**)。若用户希望 `/` 恒落「我的填写」,改 `routes.ts` 一行 | 否 |
|
||||||
|
|
||||||
|
另记两条**非问题**事实,供接手的人少踩:①`local-debug/` 不在任何仓库工作树内(`form-center` 根无 .git,仓库根是 `service/` 与 `front/`),本轮新增的 `t5-shell-smoke.sh` **不入库**(沿 T1~T4 同一事实);②YschTheme 仓**无 git 远端**(`git remote -v` 空),consumers.json/tests 的登记改动只落本机工作区,未 commit、未 push,通道发布仍走该仓 `release.mjs`/`release.py` 人工动作。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 推送与 CI
|
||||||
|
|
||||||
|
| 仓 | 提交 | 推送 | CI run | 结论 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| form-center-front | `fdd9aaa`(81 条路径:73 新增 + 7 修改 + 1 删除(T1 的 `src/env.d.ts` 并入 `src/vite-env.d.ts`),逐条 `git add`) | 见下 | — | — |
|
||||||
|
| form-center-service | 勘误③ + P47 + 本文 | 见下 | — | — |
|
||||||
|
|
||||||
|
(提交号、run 号与终态在本轮推送后回填,沿用 T1~T4 的「feat → docs 回填」两次提交形态。)
|
||||||
|
|
||||||
|
- 暂存方式:按显式路径逐条 `git add`,提交前 `git status` 核对无残留未跟踪项、无 `add -A`、无敏感文件;`node_modules/`、`dist/`、`*.log` 已由 front 仓 `.gitignore` 排除;`src/auto-imports.d.ts` 按 Notice 先例**入库**(CI `npm ci` 后首次 `vue-tsc` 需要它)。
|
||||||
|
- 前端 CI 只证构建链(`npm ci` + `npm run build`,同后端「CI 不跑单测」先例),type-check 与冒烟是**本地门禁**——本轮两者均绿(§2)。
|
||||||
|
- YschTheme:无远端,登记改动留本机工作区(见 §3 末)。
|
||||||
|
|
||||||
|
—— 全文完 ——
|
||||||
@@ -30,13 +30,23 @@ public final class FormModuleKeys {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 授权 customId 集合(path 空的权限锚点主要靠它表达) */
|
/**
|
||||||
public static Set<String> customIds(List<BoaModule> tree) {
|
* 权限锚点的 customId 集合:<b>只收 path 为空的节点</b>(契约 §8「锚点行形态」注记 / P47)。
|
||||||
|
*
|
||||||
|
* <p>数据范围升级位({@code fc:scope:all})按契约只应挂在<b>不进导航的 path 空行</b>上,
|
||||||
|
* 扫描必须按这个形态收:带 path 的导航行若误配了该值,不该把看数据的人悄悄升级成"全校全部"
|
||||||
|
* ——宽方向的误配同样是不受控的授权膨胀。</p>
|
||||||
|
*/
|
||||||
|
public static Set<String> anchorCustomIds(List<BoaModule> tree) {
|
||||||
Set<String> result = new LinkedHashSet<>();
|
Set<String> result = new LinkedHashSet<>();
|
||||||
collect(tree, false, result);
|
collect(tree, false, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归展平(wantPath=true 取 path;false 取「path 空节点」的 customId)。
|
||||||
|
* 取不出值的节点自然被跳过,子节点照常下钻(锚点可以挂在分组层下面)。
|
||||||
|
*/
|
||||||
private static void collect(List<BoaModule> nodes, boolean wantPath, Set<String> out) {
|
private static void collect(List<BoaModule> nodes, boolean wantPath, Set<String> out) {
|
||||||
if (nodes == null) {
|
if (nodes == null) {
|
||||||
return;
|
return;
|
||||||
@@ -45,11 +55,16 @@ public final class FormModuleKeys {
|
|||||||
if (node == null) {
|
if (node == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String value = wantPath ? node.getPath() : node.getCustomId();
|
String path = trimToNull(node.getPath());
|
||||||
if (value != null && !value.trim().isEmpty()) {
|
String value = wantPath ? path : (path == null ? trimToNull(node.getCustomId()) : null);
|
||||||
out.add(value.trim());
|
if (value != null) {
|
||||||
|
out.add(value);
|
||||||
}
|
}
|
||||||
collect(node.getChildren(), wantPath, out);
|
collect(node.getChildren(), wantPath, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String trimToNull(String value) {
|
||||||
|
return value == null || value.trim().isEmpty() ? null : value.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,9 +119,9 @@ public class DataScopeService {
|
|||||||
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "数据范围缺少实现器:" + scope);
|
throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, "数据范围缺少实现器:" + scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否命中 fc:scope:all 权限锚点(path 空的模块行,只由后端扫描授权集合消费,不进导航) */
|
/** 是否命中 fc:scope:all 权限锚点(只认 path 空节点,P47;见 FormModuleKeys#anchorCustomIds) */
|
||||||
private boolean hasScopeAllAnchor(BoaUser user) {
|
private boolean hasScopeAllAnchor(BoaUser user) {
|
||||||
return FormModuleKeys.customIds(moduleTreeSource.authorizedModules(user)).contains(SCOPE_ALL_CUSTOM_ID);
|
return FormModuleKeys.anchorCustomIds(moduleTreeSource.authorizedModules(user)).contains(SCOPE_ALL_CUSTOM_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -130,6 +130,25 @@ class DataScopeServiceTest {
|
|||||||
.containsExactly(DataScope.SELF_SUBMITTED);
|
.containsExactly(DataScope.SELF_SUBMITTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("P47:锚点扫描只认 path 空行——带 path 的导航行误配 fc:scope:all 不升级范围")
|
||||||
|
void customIdOnNavRowDoesNotUpgradeScope() {
|
||||||
|
BoaUser teacher = user(1);
|
||||||
|
|
||||||
|
// 误配形态:一行既有页面 path(会被渲染成菜单),又挂了锚点 customId
|
||||||
|
InMemoryModuleTreeSource misconfigured = new InMemoryModuleTreeSource();
|
||||||
|
misconfigured.grantPathWithCustomId(teacher.getId(), "/admin/data",
|
||||||
|
DataScopeService.SCOPE_ALL_CUSTOM_ID);
|
||||||
|
assertThat(serviceWith(misconfigured).resolve(teacher, form(STUDENT_FACING)))
|
||||||
|
.as("契约 §8 的锚点行形态是「path 空 + customId」,导航行上的误配值不参与范围升级")
|
||||||
|
.containsExactly(DataScope.SELF_SUBMITTED);
|
||||||
|
|
||||||
|
// 对照:同一用户同一值,改挂到 path 空行即升级——证明失败面只在"行形态",不是扫描整体失效
|
||||||
|
misconfigured.grantCustomId(teacher.getId(), DataScopeService.SCOPE_ALL_CUSTOM_ID);
|
||||||
|
assertThat(serviceWith(misconfigured).resolve(teacher, form(STUDENT_FACING)))
|
||||||
|
.containsExactly(DataScope.ALL);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== V28:任教班级在枚举里但一期不可选 ====================
|
// ==================== V28:任教班级在枚举里但一期不可选 ====================
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ public class InMemoryModuleTreeSource implements ModuleTreeSource {
|
|||||||
rows(userId).add(row(null, customId));
|
rows(userId).add(row(null, customId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 误配形态(P47 取证用):一条<em>会进导航</em>的页面路由行同时挂了 customId。
|
||||||
|
* 契约 §8 的锚点行形态是「path 空 + customId」,这种行不该被当成锚点消费。
|
||||||
|
*/
|
||||||
|
public void grantPathWithCustomId(Integer userId, String path, String customId) {
|
||||||
|
rows(userId).add(row(path, customId));
|
||||||
|
}
|
||||||
|
|
||||||
/** 清空某人的授权(构造"角色是管理员但没被授权模块行"的反例) */
|
/** 清空某人的授权(构造"角色是管理员但没被授权模块行"的反例) */
|
||||||
public void clear(Integer userId) {
|
public void clear(Integer userId) {
|
||||||
byUser.remove(userId);
|
byUser.remove(userId);
|
||||||
|
|||||||
Reference in New Issue
Block a user