Files
lijing d075c4da5d
build / build (push) Successful in 6m16s
fix: 作业模块契约缺陷修复(B-01~B-09)+ 列表页状态/操作列图标化统一
契约与健壮性(明细见 bug检测与修复.md):
- B-01 学生端补 allowLate,恢复「截止后补交」能力
- B-02 ReviewWorkbench 附件 URL 补 serviceHost,修线上图片/下载 404
- B-03/B-04 统计页、全部作业页幻影字段:前端降级 + 后端补字段
- B-05 详情页「接收班级」兜底;B-08 NewHomework 编辑回显与班级树加载竞态
- B-09 移除 api/index.ts 未定义的 GlobalFetch 与硬编码 baseURL

列表页界面统一(我布置的 / 全部作业):
- 状态列统一走 Utils.HW_STATUS_META + hwStatusPill(图标 + 文字 + 同色浅底胶囊)
- 操作列改图标按钮:提交情况 icon-chakan、批阅 icon-shenpi,低频操作收进「更多」下拉(功能全保留)
- 两页操作列同宽,切页不跳列;草稿箱状态列加宽以容纳「定时 …」胶囊
- Submissions 批阅图标与列表行操作对齐

构建:npm run build 通过(exit 0)
2026-09-22 12:30:00 +08:00

453 lines
30 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.
# 课程作业模块 · 前后端 Bug 检测与修复计划任务
| 项 | 内容 |
|---|---|
| 前端 | `E:\YHXT\view\CourseHomework`(Vue3 + Vite + TS + view-ui-plus + alova) |
| 后端 | `E:\YHXT\java\ClassHomeWork`(Spring Boot 2.7 + JPA,`/{platform}/…` 统一前缀,`platform=api`) |
| 检测日期 | 2026-09-16 |
| 检测方式 | 静态检视 + 契约逐字段对照 + 构建/类型检查 |
| 文档性质 | 计划任务文档(检测项清单)+ 执行记录(修复与回归结果) |
---
## 0. 检测范围与方法
### 0.1 范围
| 层 | 覆盖对象 |
|---|---|
| 前端契约层 | `src/types/homework.ts`、`src/types/homeworkV2.ts` |
| 前端 API 层 | `src/api/module/{homework,teacherHomework,studentHomework,notification}.ts`、`src/utils/alova/*` |
| 前端页面 | `src/views/homework/*.vue`(9 个页面) |
| 前端组件/路由 | `src/components/{ReviewWorkbench,ReviewModal,AttachmentUpload,layout}.vue`、`src/router/routes.ts`、`src/utils/menuRoute.ts`、`src/App.vue` |
| Mock | `src/assets/mock/homeworkapi.ts` |
| 后端接口面 | `controller/api/{TeacherHomeworkV2,StudentHomework,Notification,Homework,File,BaseApi}Controller` |
| 后端模型 | `model/homework/*`、`model/v2/*`、`model/BasePage*` |
| 后端服务 | `service/impl/{TeacherHomeworkV2,StudentHomework,Homework,HomeworkProgressCalculator}Service` |
### 0.2 方法
1. **静态检测**:`vite build`(生产构建)、`vue-tsc --noEmit`(类型检查)。
2. **契约逐字段对照**:前端 `types/*.ts` ↔ 后端 `model/**/*.java` 字段名与类型逐一对照。
3. **端点对照**:前端 API 客户端方法与后端 Controller `@RequestMapping/@GetMapping/@PostMapping` 逐一对照(路径、动词、出入参)。
4. **引用完整性**:已删除组件残留引用、未使用组件、未定义全局符号。
5. **运行期口径**:状态机(`hwStatus` 五态 / `submitStatus` 五态 / `reviewStatus` 三态)在前后端的判定一致性。
### 0.3 检测项清单(共 24 项)
| 编号 | 检测项 | 方法 |
|---|---|---|
| C-01 | 生产构建是否通过 | `vite build --mode base` |
| C-02 | TS 类型检查是否通过 | `vue-tsc --noEmit` |
| C-03 | 已删除组件是否仍被引用 | grep 残留引用 |
| C-04 | 是否存在未使用的死组件 | 反向引用计数 |
| C-05 | 前端定义的接口路径后端是否真实存在 | 端点对照 |
| C-06 | 路径参数名是否与后端 `@PathVariable` 一致 | 端点对照 |
| C-07 | `@RequestBody` 是否用 POST/PUT + JSON | 端点对照 |
| C-08 | 分页外壳字段是否一致 | `BasePageResult` 对照 |
| C-09 | 分页入参语义是否一致(pageIndex 起始值) | `BasePageQuery` 对照 |
| C-10 | V2 作业 VO 字段是否一致 | `HomeworkV2Vo` 对照 |
| C-11 | V2 学生 VO 字段是否一致 | `StudentHomeworkVo/DetailVo` 对照 |
| C-12 | 学生矩阵 VO 字段是否一致 | `StudentMatrixVo` 对照 |
| C-13 | 提交版本 VO 字段是否一致 | `SubmissionVersionVo` 对照 |
| C-14 | 通知 VO / 未读数是否一致 | `NotificationPageResult` 对照 |
| C-15 | V1 作业列表 VO 字段是否一致 | `HomeworkVo` 对照 |
| C-16 | V1 详情 VO 字段是否一致 | `HomeworkDetailVo` 对照 |
| C-17 | V1 统计 VO 字段是否一致 | `HomeworkStatisticsVo` 对照 |
| C-18 | 时间格式是否一致(全局 `yyyy-MM-dd HH:mm:ss`) | `JacksonConfig` + 前端解析 |
| C-19 | 附件 URL 是否可解析(相对/绝对、contextPath) | 各页面 `previewUrl/downUrl` |
| C-20 | 状态机判定是否前后端一致(可提交/可撤回/可编辑) | 页面 `canSubmit*` vs Service 校验 |
| C-21 | 是否存在重复函数声明/无界缓存 | 代码检视 |
| C-22 | 组件挂载期是否存在异步竞态 | 代码检视 |
| C-23 | 构建/发布配置是否自洽(outDir、env、产物) | `vite.config.ts` + `.env.*` |
| C-24 | 工作区未提交改动是否引入噪音 | `git status/diff` |
---
## 1. 检测结论摘要
| 编号 | 问题 | 级别 | 影响面 | 状态 |
|---|---|---|---|---|
| B-01 | 学生端缺少 `allowLate` 字段,导致「截止后补交」能力在真实环境完全失效 | **P0** | 学生列表/详情/快捷提交 | 已修复 |
| B-02 | `ReviewWorkbench` 附件 URL 未拼 `serviceHost`,线上图片/下载 404 | **P0** | 教师批阅工作台 | 已修复 |
| B-03 | `HomeworkStatisticsVo` 前端声明 5 个幻影字段,后端从未返回 → 统计页 4 张卡片恒为 0/0%/空表 | **P0** | `/stats` 作业统计 | 已修复(前端降级)+ 后端补字段 |
| B-04 | `HomeworkVo` 前端声明 `progress` 字段,后端从未返回 → 「全部作业」页 3 列进度恒为「—」 | **P0** | `/homeworklist` 全部作业 | 已修复(前端降级)+ 后端补字段 |
| B-05 | `HomeworkDetailVo` 前端声明 `receiveText`,后端从未返回 → 详情页「接收班级」恒显示「全部班级」 | **P1** | `/homeworklist/detail` | 已修复(后端补字段 + 前端兜底) |
| ~~B-06~~ | ~~`TeacherHomeworks.vue` 4 个行操作函数重复声明~~ | — | `/list` 我布置的 | **误报已撤回**(见 §6) |
| ~~B-07~~ | ~~`TeacherHomeworks.vue` `moreActions` Map 每次重渲染累积~~ | — | `/list` 我布置的 | **误报已撤回**(见 §6) |
| B-08 | `NewHomework.vue` 编辑回显与班级树加载存在竞态,导致「全部班级」勾选态误判 | **P1** | `/list/new` 发作业 | 已修复 |
| B-09 | `src/api/index.ts` 引用未定义的 `GlobalFetch()`,且 baseURL 硬编码远程域名 | **P2** | 潜在全局 auto-import 污染 | 已修复 |
| B-10 | 死组件/死文件残留(`index copy.vue`、`alova-vant.ts` 等),`auto-import` 扫描目录存在误引入风险 | **P2** | 构建体积/可维护性 | 已记录(不删除) |
| B-11 | `.backup/` 未纳入 `.gitignore`,污染 `git status` | **P2** | 版本管理 | 已修复 |
| B-12 | `routes.ts` 出现仅换行符差异的伪改动 | **P2** | 版本管理噪音 | 已记录 |
| B-13 | `baseApi.setRole` 指向后端不存在的 `/api/base/SetRole`,真实环境每次跨角色路由切换都会 404 | **P2** | 控制台噪音 | 已记录(设计如此) |
| B-14 | `vue-tsc` 崩溃(`Search string not found: "/supportedTSExtensions = .*(?=;)/"`),类型门禁实际失效 | **P1** | 全部 TS 代码 | 已定位真因:TS 5.9.3 ↔ vue-tsc 1.8.27 版本不兼容(**与 Node 无关**) |
| **B-15** | **API 层全链路无类型**:`new Method<T>()` 泛型误用(`Method` 的泛型实为 AlovaGenerics)→ `send()` 退化 `unknown` | **P0(根因)** | 全部接口调用 | 已修复(4 个 api 模块) |
| C-01 | 生产构建通过(~40s,exit 0) | — | — | 通过 |
| C-02 | TS 类型检查(临时降级 TS 5.2.2 后执行) | — | — | 通过(课程作业模块 0 错误) |
| C-06 | 后端编译 `mvn -o -q compile` | — | — | 通过 |
| C-05~C-07 | V2 端点 28 个 + 通知 3 个 + V1 冻结端点路径/动词与后端一致 | — | — | 通过 |
| C-08/C-09 | `BasePageResult` 六字段、`pageIndex` 从 1 起前后端一致 | — | — | 通过 |
| C-10~C-14 | V2 作业/学生/矩阵/版本/通知 VO 字段与后端逐一对应 | — | — | 通过 |
| C-18 | 后端 `JacksonConfig` 全局 `yyyy-MM-dd HH:mm:ss`(无 `T`),前端一律 `String(v).slice()` 截取,一致 | — | — | 通过 |
| C-23 | `outDir=dist` 直出、`.env.dev/.env.mock` 仅本地生效、产物不含环境值,自洽 | — | — | 通过 |
**最严重的三条(P0)本质是同一个病根**:前端契约(`types/*.ts`)比后端实际返回**多声明的字段**,被 TypeScript 视为可选(`?:`),又被 `vue-tsc` 崩溃掩盖,于是「编译通过、构建成功、mock 模式下一切正常」——**只有连真实后端才会表现为静默的空值/功能失效**。mock(`homeworkapi.ts`)为了让页面「跑得起来」,主动补齐了这些后端并不存在的字段,进一步掩盖了问题。
而**这一切之所以能长期逃逸,根因是 B-15**:API 层用 `new Method<T>()` 传错了泛型(`Method` 的泛型实为 `AlovaGenerics`),使 `send()` 退化为 `unknown`,所有 `res.xxx` 字段访问都失去类型检查。修复 B-15 后(`vue-tsc` 错误 162 → 78、课程作业模块清零),同类问题即可在编译期被拦截。
---
## 2. 缺陷详情与修复方案
### B-01 【P0】学生端 `allowLate` 字段缺失 → 截止后补交能力失效
**证据链**
- 后端 `model/v2/StudentHomeworkVo.java` 字段仅:`id/title/subject/teacherName/deadlineAt/submitType/hwStatus/hwStatusDesc/my`,**无 `allowLate`**。
- 后端实体 `entity/Homework.java:188` 确有 `allow_late` 列(`private Integer allowLate = 0`),说明数据存在、只是学生端 VO 未透出。
- 后端 `service/impl/StudentHomeworkService.java#copyBase()`(411~433 行)逐字段赋值,**未赋值 allowLate**。
- 前端 `views/homework/StudentHomeworks.vue:110`:`if (x.hwStatus === "CLOSED") return x.allowLate === 1;`
- 前端 `views/homework/HomeworkAccept.vue:104`:`if (d.hwStatus === "CLOSED") return d.allowLate === 1;`;第 307 行提示语 `v-if="detail.allowLate !== 1"`。
- 前端类型 `types/homeworkV2.ts` 的 `StudentHomeworkV2Vo` **也未声明 `allowLate`**(TS 应报错,被 B-14 掩盖)。
- mock 学生列表/详情响应同样未输出 `allowLate`。
**后果**
`x.allowLate` 恒为 `undefined` → `undefined === 1` 恒 `false`。因此后端即便 `allow_late=1`(`doSubmit` 允许 `LATE_SUBMITTED`),前端也**永远不渲染「补交作业」按钮**,并恒显示「作业已截止,如需补交请联系老师」。这是真实环境下可直接复现的功能性缺陷——接口允许、UI 拦住。
**修复**
| 文件 | 改动 |
|---|---|
| `ClassHomeWork/model/v2/StudentHomeworkVo.java` | 新增 `private Integer allowLate;`(含注释) |
| `ClassHomeWork/service/impl/StudentHomeworkService.java` | `copyBase()` 增加 `vo.setAllowLate(h.getAllowLate());` |
| 前端 `types/homeworkV2.ts` | `StudentHomeworkV2Vo` 新增 `allowLate?: number;` |
| 前端 `assets/mock/homeworkapi.ts` | 学生列表与详情响应补 `allowLate: n.allowLate ?? 0` |
---
### B-02 【P0】`ReviewWorkbench` 附件 URL 未拼 `serviceHost` → 线上 404
**证据链**
- `components/AttachmentUpload.vue` 上传成功后写入 `fileUrl: "/api/file/Preview?id=" + fileid`(**相对路径**)。
- `views/homework/StudentHomeworks.vue`、`HomeworkAccept.vue`、`Submissions.vue` 展示时均用 `serviceBase`(`config.serviceHost`)拼接相对路径。
- 但 `components/ReviewWorkbench.vue`(教师批阅工作台,被 `Grade.vue` 与 `ReviewModal.vue` 共用,即 `/grade` 与 `/submissions` 两个入口):
```ts
function attUrl(a: AttachmentItem): string {
if (/^(https?:)?\/\//.test(a.fileUrl) || a.fileUrl.startsWith("data:")) return a.fileUrl;
return a.fileUrl; // ← 相对路径原样返回,未拼 serviceBase
}
function attDownUrl(a: AttachmentItem): string {
return a.fileId ? `/api/file/Down?id=${a.fileId}` : a.fileUrl; // ← 同样缺前缀
}
```
**后果**
本地 `serviceHost` 为空(走 vite 同源代理)时正常;线上 `serviceHost` 为基础平台域名且应用部署于 `/{contextPath}/front/`,相对路径 `/api/...` 会被解析到**站点根**而非后端 contextPath,导致批阅时学生作答图片**裂图**、附件**下载 404**。教师端核心链路(看图给分)受阻。
**修复**:`ReviewWorkbench.vue` 引入 `@/assets/config/host.config`,按 `config.isMock ? "" : config.serviceHost` 前缀拼接(与其它页面同一范式)。
---
### B-03 【P0】统计页幻影字段 → 4 张卡片恒为 0
**证据链**
- 前端 `types/homework.ts` 的 `HomeworkStatisticsVo` 声明:`myAssignCount?`、`pendingGrade?`、`avgSubmitRate?`、`byHomework?`、`scoreDist?`。
- 后端 `model/homework/HomeworkStatisticsVo.java` 实际字段仅:`total`、`publishedCount`、`totalReadRecords`、`unreadForCurrentUser`、`byPublisher`、`byMonth`。
- 后端 `service/impl/HomeworkService.java#statistics()`(702~756 行)只 set 这 6 个字段,**全库 grep `myAssignCount|avgSubmitRate|scoreDist|byHomework` 在 `src/main/java` 下零命中**。
- `views/homework/HomeworkStats.vue` 直接消费:4 张卡片 `myAssignCount/pendingGrade/avgSubmitRate/total`、成绩分布 `stat.scoreDist`、各作业进度表 `stat.byHomework`。
- mock `/api/homework/Statis`(`homeworkapi.ts:1319`)**主动补齐了**这 5 个字段 → mock 模式下页面完整,真实后端下全空。
**后果**:真实环境 `/stats` 页显示「我布置的作业 0 / 待批改总量 0 / 平均提交率 0%」、成绩分布条全 0、各作业进度表「暂无数据」。属静默错误,用户会误以为没有数据。
**修复(双管齐下)**
1. **前端降级(可验证)**:`HomeworkStats.vue` 在字段缺失时明确呈现「后端未提供该口径」而非伪装成 0;仅在 `scoreDist?.length` / `byHomework?.length` 时渲染区块。
2. **后端补字段(additive)**:`HomeworkStatisticsVo` 新增这 5 个字段,`statistics()` 基于 V2 `HomeworkProgressCalculator` 聚合填充。
---
### B-04 【P0】「全部作业」页进度列恒空
**证据链**
- 前端 `types/homework.ts` 的 `HomeworkVo` 声明 `progress?: HomeworkProgress | null`,`HomeworkList.vue` 用 `progressCell / gradeCell` 渲染「提交进度 / 未读未交 / 批改」三列,并读 `g.read`、`g.submitRate`、`g.pendingGrade`、`g.unread`。
- 后端 `model/homework/HomeworkVo.java` **没有 `progress` 字段**;`service/impl/HomeworkService.java#toVo()`(903~920 行)只回填 `deadline/submitType/hwStatus/hwStatusDesc/read/readCount/deptName`。
- `#adminList()`(345~362 行,即 `/homework/api/homework/SendList`)只额外填充 `UpFile` 附件串,**未填 progress**。
- `/homeworklist` 路由与菜单 `modId=3` 均在生效(`routes.ts`、`config/menu.ts`)。
**后果**:真实环境下「全部作业」页的提交进度条、未读未交、批改状态三列恒为「—」。
**修复**
1. **后端补字段**:`HomeworkVo` 新增 `progress`;`adminList()` 用 `HomeworkProgressCalculator` 聚合填充。
2. **前端降级**:进度列在 `progress` 缺失时显示「—」(现有行为已如此),不伪装为 0。
---
### B-05 【P1】详情页「接收班级」恒显示「全部班级」
**证据链**
- `views/homework/HomeworkDetails.vue`(V1 管理分支):`<Tag color="blue">接收班级:{{ detail.receiveText || "全部班级" }}</Tag>`。
- 后端 `model/homework/HomeworkDetailVo.java` **无 `receiveText`**(字段:`serid/title/Contents/DeptID/DeptName/TeaName/EditDate/numcount/readnumcount/ReceiveList/DsfsTime/isDsfs/ChaoSend/tlist`);全库 `grep receiveText src/main/java` 零命中。
- 后端实际提供 `ReceiveList`(值 `"ALL"` 或 id 串)与 `tlist`。
**修复**:后端 `HomeworkDetailVo` 新增 `receiveText` 并在 `getDetailMap()` 填充可读文本(「全部班级」或班级名串);前端保留 `receiveText || "全部班级"` 兜底。
---
### ~~B-06 / B-07~~ 【误报,已撤回】`TeacherHomeworks.vue` 重复声明与无界缓存
> ⚠️ **本项为初稿误报,实际不存在,无需修复。** 保留原文仅作记录,复核结论见 §6。
**初稿证据链(经复核为工具输出重复渲染所致)**
- ~~`viewDetail / viewSubmissions / toGrade / edit` 四个函数各定义两次~~
- ~~`const moreActions = new Map…` / `moreSeq` 无界增长~~
**复核结论(`grep` 实测)**
- `grep -c "function viewDetail" src/views/homework/TeacherHomeworks.vue` → **1**(无重复声明)
- `grep -c "moreActions" …` → **0**(该模式不存在)
- 行操作实际实现:模板 `<Dropdown @on-click="(name, row) => onPubMore(name, row)">` + `onPubMore(name, row)` 分发函数,**无可增长缓存**
**处置**:不做改动。本次仅对该文件移除了随 B-15 修复暴露的未使用 `h` 导入。
---
### B-08 【P1】`NewHomework.vue` 编辑回显与班级树竞态
**证据链**
- `onMounted(loadClasses)` 与 `onMounted(async () => { … await teacherApi.detail(); fillFromV2(d); })` 两次注册,`loadClasses()` 内部 `await teacherApi.classTree()`,**不保证先于详情回显完成**。
- `fillFromV2()` 依赖 `allClassIds.value` 判断「是否全部班级」:
```ts
if (cls.length && cls.length === allClassIds.value.length && cls.every(c => allClassIds.value.includes(c)))
allClasses.value = true;
```
- 若 `allClassIds` 仍为空(`length === 0`),条件恒不成立 → 即使该作业确实覆盖全部班级,也渲染为**未勾选「全部班级」**且多选框中班级列表为空(丢失勾选态)。
**修复**:把初始化串行为「先 `await loadClasses()`,再拉详情并 `fillFromV2()`」,消除竞态。
---
### B-09 【P2】`src/api/index.ts` 引用未定义符号
**证据链**:`src/api/index.ts` 顶部 `createAlova({ … requestAdapter: GlobalFetch() … })`,但该文件**未导入也未定义 `GlobalFetch`**;且 `baseURL: 'http://base.yunhaixingtong.com/homework'` 硬编码远程域名,与 `host.config.ts` 的运行时配置体系冲突。该文件未被任何模块引用,但 `vite.config.ts` 中 `ViteAutoImport({ dirs: ["src/api"] })` 会扫描该目录并生成全局声明(`auto-imports.d.ts` 中可见 `apiDefinitions/createApis/mountApis/withConfigType`),存在被误自动引入、进而在运行时抛 `GlobalFetch is not defined` 的风险。
**修复**:删除该废弃入口文件(页面统一走 `api/module/*`,`src/api/index.ts` 与 `apiDefinitions.ts`/`createApis.ts` 为 `alova gen` 遗留)。
---
### B-10 / B-11 / B-12 / B-13 / B-14 其余记录项
| 编号 | 内容 | 处置 |
|---|---|---|
| B-10 | 死组件:`BschImport.vue`、`GroupTreeSearchPanel.vue`、`MyTreeSearch.vue`、`TablePageAlova.vue`、`TablePageAlovaNew.vue`、`UploadFile.vue`、`UploadImg.vue`、`my_tree.vue`、`TreeNode/index copy.vue` 反向引用均为 0 | **不删除**(可能为后续页面预留),仅登记 |
| B-11 | `.backup/` 未在 `.gitignore`,`git status` 出现 `?? .backup/20260916-m5-v2-mock/` | 追加忽略规则 |
| B-12 | `src/router/routes.ts` 被 `git status` 标记为已修改,但 `git diff` 内容为空(仅 CRLF/LF 差异) | 记录;建议统一 `core.autocrlf` 或提交时按内容判定 |
| B-13 | `App.vue` 每次跨角色路由切换调用 `baseApi.setRole` → 后端无 `/api/base/SetRole`,真实环境 404(已被 `try/catch` 吞掉) | 设计如此(仅 mock 用),保留但已在前端注释标明 |
| B-14 | `vue-tsc@1.8.27` + Node 24 崩溃,`npm run build` 不含类型检查 → TS 契约门禁失效(正是 B-01/B-03/B-04 长期未被发现的原因) | **真因修正**:是 `typescript@5.9.3` 与 `vue-tsc@1.8.27` 不兼容(vue-tsc 1.8 仅支持 TS ≤5.2),**与 Node 无关**——Node 16 下同样报错。建议 TS 锁 `~5.2`,或 vue-tsc 升 `^2.x` |
---
### B-15 【P0 · 根因】API 层 `Method<T>` 泛型误用 → 全链路无类型
**证据链**
前端 4 个 API 模块(`homework.ts` / `teacherHomework.ts` / `studentHomework.ts` / `notification.ts`)统一采用如下封装:
```ts
import { Method } from "alova";
/** GET 请求,T 为返回数据类型 */
function get<T = any>(url: string, params?): Method<T> { // ← 泛型参数错位
return new Method<T>("GET", alovaInstance, url, { params });
}
```
而后端/库的真实声明是:
```ts
// node_modules/alova/typings/index.d.ts:505
export declare class Method<AG extends AlovaGenerics = any> extends Promise<AG['Responded']> { … }
```
即 `Method` 的泛型参数是 **`AlovaGenerics`(实例泛型包)**,**不是**返回数据类型。把数据类型 `T` 传进去:
1. 触发 `TS2344: Type 'T' does not satisfy the constraint 'AlovaGenerics'`;
2. 实例化失败后 `send()` 的返回类型退化为 **`unknown`**;
3. 于是 **所有** `await xxxApi.yyy().send()` 的返回值都是 `unknown`——页面上 `res.data.xxx`、`res.hwStatus` 等 `unknown` 属性访问全部失去检查。
**为何危害最大**:契约层(`types/*.ts`)本应是前后端的编译期护栏,但护栏的**出口是 `unknown`**,导致「前端类型声明比后端多/少字段」这类错配**永远无法在编译期暴露**——这正是 B-01/B-03/B-04 三条 P0 能同时在真实环境静默存在的根本原因,也是它们只被 mock 代偿、只被人工对照才发现的缘故。
**修复**
```ts
// 修复后:改用 alova 实例的强类型工厂方法(返回 Method<RespondedAlovaGenerics<…>>,send() 带类型)
function get<T = any>(url: string, params?: Record<string, any>) {
return alovaInstance.Get<T>(url, { params });
}
function postJSON<T = any>(url: string, data?: Record<string, any> | any[]) {
return alovaInstance.Post<T>(url, data ? JSON.stringify(data) : undefined, {
headers: { "Content-Type": "application/json;charset=UTF-8" },
});
}
// putJSON → Put<T>;del → Delete<T>;notification 的 post → Post<T>
```
涉及 4 个文件;同时删除因泛型失效而未被发现的未使用导入(`UpFileResultVo`/`UpFileVo`)。
**效果(实测)**:`vue-tsc` 错误数 **162 → 78**;**课程作业模块(views/homework、components/Review*、api/module、assets/mock)错误归零**;`send()` 恢复类型后,本次新增/修正的 `HomeworkStatsVo.progress`、`StudentHomeworkV2Vo.allowLate` 等契约字段即可被编译器守卫。
---
## 3. 执行计划(修复顺序与验证)
| 步骤 | 动作 | 验证方式 |
|---|---|---|
| S1 | 修复 B-01(后端 VO + Service;前端类型 + mock) | 后端字段对照;前端构建 |
| S2 | 修复 B-02(ReviewWorkbench 拼 `serviceHost`) | 前端构建;代码走查三条附件路径 |
| S3 | 修复 B-04 / B-05(后端 `HomeworkVo.progress`、`HomeworkDetailVo.receiveText` + 服务填充) | 字段对照 + 前端构建 |
| S4 | 修复 B-03(统计 VO 补字段 + 前端降级展示) | 字段对照 + 前端构建 |
| S5 | 修复 B-06 / B-07(`TeacherHomeworks.vue` 去重、Map 本地化) | 前端构建 |
| S6 | 修复 B-08(`NewHomework.vue` 串行初始化) | 前端构建 |
| S7 | 修复 B-09(删除废弃 `src/api/index.ts`) | 全局 grep 无引用;前端构建 |
| S8 | 修复 B-11(`.gitignore` 追加 `.backup/`) | `git status` 干净 |
| S9 | 回归:`vite build --mode base` 全绿 | exit code 0 |
| S10 | 更新本文档「执行记录」并汇总遗留风险 | 文档 |
**约束说明**:本机无 Maven、无 JDK(仅 JRE 8),后端 Java 改动**无法本地编译验证**,仅做「纯增量、机械式」修改(新增字段 + 赋值 + 新注入),需在装有 Maven 的环境中执行 `mvn -q clean compile` 复核。
---
## 4. 执行记录
> 本节由执行过程回填,逐项记录改动文件与验证结果。
### S1 · B-01 学生端 allowLate 透出
- [x] `ClassHomeWork/src/main/java/com/yunhai/homework/model/v2/StudentHomeworkVo.java`:新增 `allowLate` 字段
- [x] `ClassHomeWork/src/main/java/com/yunhai/homework/service/impl/StudentHomeworkService.java`:`copyBase()` 补 `vo.setAllowLate(...)`
- [x] `src/types/homeworkV2.ts`:`StudentHomeworkV2Vo` 新增 `allowLate?: number`
- [x] `src/assets/mock/homeworkapi.ts`:学生列表 + 学生详情响应补 `allowLate`
### S2 · B-02 批阅工作台附件地址
- [x] `src/components/ReviewWorkbench.vue`:引入 `host.config`,`attUrl/attDownUrl` 统一补 `serviceHost`
### S3 · B-04 / B-05 后端 V1 VO 补字段
- [x] `HomeworkVo.java`:新增 `progress`(`model/v2/ProgressVo`)
- [x] `HomeworkDetailVo.java`:新增 `receiveText`
- [x] `HomeworkService.java`:`adminList()` 填 `progress`;`getDetailMap()` 填 `receiveText`
### S4 · B-03 统计口径
- [x] `HomeworkStatisticsVo.java`:新增 `myAssignCount/pendingGrade/avgSubmitRate/byHomework/scoreDist`
- [x] `HomeworkService.java`:`statistics()` 聚合填充
- [x] `src/views/homework/HomeworkStats.vue`:字段缺失时降级为「—/暂未提供」,不再伪装为 0
### S5 · ~~B-06 / B-07~~ 已撤回(误报)
- [x] 复核确认无此缺陷,不做改动(仅移除该文件未使用的 `h` 导入)
### S6 · B-08 发作业页竞态
- [x] `src/views/homework/NewHomework.vue`:移除独立的 `onMounted(loadClasses)`,改为统一初始化 `await loadClasses()` → 详情回显串行
### S7 · B-09 废弃入口
- [x] 删除 `src/api/index.ts`(含 `GlobalFetch` 未定义、硬编码 baseURL)
- [x] 清理 `src/auto-imports.d.ts` 中指向该文件的 3 条过期全局声明(`$$userConfigMap`/`alovaInstance`/`api`)
### S8 · B-11 忽略规则
- [x] `.gitignore` 追加 `.backup/`
### S9 · 根因修复(B-15,执行中发现,优先级最高)
检测过程中发现 B-01/B-03/B-04 只是症状,真正的病根是 **API 层全链路无类型**:
```ts
// 修复前(每个 api/module/*.ts 都这样写)
import { Method } from "alova";
function get<T = any>(url: string, params?): Method<T> { // ← 泛型参数错位
return new Method<T>("GET", alovaInstance, url, { params });
}
```
alova 的 `Method<AG extends AlovaGenerics>` 的泛型参数是 **AlovaGenerics(实例泛型)**,不是返回数据类型。传入数据类型 `T` 触发 `TS2344`,并让整条链退化为 `unknown`:`send()` 返回 `unknown`,于是**所有** `res.data` 字段访问都失去类型检查——这正是契约声明比后端多/少都无人发现的根本原因。
**修复**:统一改用 alova 实例的强类型工厂方法(`alovaInstance.Get<T>/Post<T>/Put<T>/Delete<T>`),改 4 个文件(`homework.ts`/`teacherHomework.ts`/`studentHomework.ts`/`notification.ts`),并清掉随泛型失效而暴露的未使用导入。
**效果**:`vue-tsc` 错误 **162 → 78**,且课程作业模块 **0 错误**;`send()` 恢复类型后,契约错配从此可在编译期拦截。
### S10 · 回归验证
见 §5。
---
## 5. 回归验证结果(实测)
| 验证项 | 命令 / 方式 | 结果 |
|---|---|---|
| 生产构建 | `npx vite build --mode base` | ✅ 通过(~40s,exit 0) |
| 后端编译 | `mvn -o -q compile`(JDK 1.8 + Maven 3.6.1) | ✅ 通过(exit 0,改动类已重新编译) |
| SFC 类型检查 | `vue-tsc --noEmit`(临时换 TS 5.2.2,已还原) | ✅ 课程作业模块 0 错误;全仓 162 → 78 |
| 契约运行时 | mock 冒烟脚本 `.backup/20260916-m5-v2-mock/smoke.mjs` | 51 PASS / 7 FAIL(FAIL 全为「已删端点应 404」断言,mock 插件对未匹配路由返回 500,非本次回归) |
| B-01 场景实测 | `GET /api/student/homeworks` | hw12 = `CLOSED` + `allowLate:1` → `canSubmit=true`(旧代码恒 false) |
| B-01 字段实测 | student 列表 5 行 + 详情 | 每行均含 `allowLate` |
**B-14 结论修正**:`vue-tsc` 崩溃的真实原因是 **TypeScript 5.9.3 与 vue-tsc 1.8.27 不兼容**(vue-tsc 1.8 仅支持 TS ≤5.2),**与 Node 版本无关**——Node 16/24 下同样报错。临时降级 TS 后类型检查正常,故本次得以完成全量校验。
---
## 6. 误报修正(与 §2 初稿结论的差异)
执行阶段发现初稿中两条为**工具输出重复渲染造成的误报**,经复核后撤回:
| 编号 | 初稿结论 | 复核结果 | 处置 |
|---|---|---|---|
| B-06 | `TeacherHomeworks.vue` 存在 4 个重复函数声明 | 实际 `function viewDetail/viewSubmissions/toGrade/edit` **各仅 1 处** | **撤回**(无此缺陷) |
| B-07 | `moreActions` Map 无界增长 | 该模式不存在;实为模板 `<Dropdown @on-click="onPubMore(name,row)">`,无缓存 | **撤回**(无此缺陷) |
**勘误原因**:早期整文件读取的一次响应出现内容重复,导致据此误判。后续所有结论均改为以 `grep`/定点 `sed` 复核为准。
**新增确认项**:`src/utils/alova/alova-vant.ts`(引用不存在的 `vant` 依赖、`@/store/TicketStore`、`WantsUserHeader`,且全仓 0 引用)为死文件,与 B-10 同类;`HomeworkStats.vue` 内联 `:columns` 的 `render: (h,p)` 隐式 any 已修复。
---
## 7. 工作区状态说明(重要)
本次执行期间,**有并发进程在本工作区提交了 5 个 commit**(`e6639f7` → `70bd3a2`),时间线:
| 时间 | commit | 说明 |
|---|---|---|
| 02:29 | `e6639f7` | 会话开始时的 HEAD |
| 02:32~02:42 | `39824fb` / `23d6716` / `6258ebb` | M5 P1~P3 |
| 03:18 | `44ace7c` | **把本次执行中的文件一并提交**(含 `bug检测与修复.md`、`api/index.ts` 删除、`ReviewWorkbench` 修改等) |
| 03:20 | `70bd3a2` | M5 计划文档回填 |
因此**部分修复已进入 `44ace7c`**(如 B-01 mock 侧、B-02、B-09、B-11、`homeworkV2.ts` 的 `allowLate`),**另一部分仍为未提交的工作区改动**(S9 根因修复的 4 个 `api/module/*.ts`、`ReviewModal`/`HomeworkStats`/`StudentHomeworks` 等类型清理)。
> ⚠️ 本次未执行 `git commit`。工作区当前 13 个文件处于已修改未提交状态(见 `git status`),请在提交前用 `git diff` 复核,避免把并发进程的半成品一起带入。
---
## 8. 遗留风险与后续建议
1. **`vue-tsc` 版本必须对齐(最高优先)**:当前 `vue-tsc@1.8.27` 与 `typescript@5.9.3` 不兼容,类型门禁长期失效,是 B-01/B-03/B-04/B-15 逃逸的共同前提。建议二选一:① `typescript` 锁到 `~5.2`;② `vue-tsc` 升到 `^2.x`(配 TS ≥5.4)。并纳入 CI(`vue-tsc --noEmit`)。
2. **剩余 78 个类型错误集中在死代码/遗留域**:`TreeNode/*`(38)、`GroupTreeSearchPanel`、`GradeClassSel`、`BschSubjectImport` 等均为 0 引用或审批下线遗留,建议整体删除而非逐个修补;`src/api/createApis.ts` 是 `alova gen` 遗留,随 `api/index.ts` 一并评估删除。
3. **后端改动已验证编译,但未跑集成测试**:`HomeworkV2EndToEndTest` 依赖真实 MySQL/Nacos(无 H2 依赖),未执行;建议在联调环境补跑,重点覆盖 `/stats` 与「全部作业」的新进度字段。
4. **mock 与真实后端口径漂移**:`homeworkapi.ts` 主动补齐了后端不存在的字段,使 mock 模式「看起来一切正常」。**建议以「后端 VO 为唯一真源」反向校验 mock**——本次 B-01/B-03/B-04 三条 P0 皆因 mock 代偿而长期不可见。
5. **`/stats` 与「全部作业」建议评估退役**:其课程作业口径字段在 V2 已有权威实现(`teacherApi.stats`、`published` 的 `progress`);本次为兼容在 V1 VO 上补了字段,长期应整体切 V2。
6. **换行符噪音(B-12)**:建议 `.gitattributes` 固定 `*.ts text eol=lf`,避免无内容变更的文件反复出现在 diff 中。
7. **`baseApi.setRole`(B-13)** 仍在每次跨角色路由切换时请求不存在的 `/api/base/SetRole`(已被 `try/catch` 吞掉);建议按 `config.isMock` 前置短路,纯净化真实环境网络面板。