You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/docs/superpowers/plans/2026-07-30-score-reference-...

155 lines
8.7 KiB
Markdown

# 成绩中心参考成绩与实训进度 Implementation Plan
> **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:** 在教师端成绩中心展示不参与评分的参考成绩和基于教学班任务分配的实训进度。
**Architecture:** 后端在查询和导出学生排名记录后,按教学班可用任务及学生已提交模块对每条记录补充进度;不修改 `stu_rank`、`stu_grade` 或权限规则。前端仅消费新增的只读进度字段,并将原成绩展示名称调整为参考成绩。
**Tech Stack:** Spring Boot、MyBatis、JUnit 5/Mockito、Vue 3、Element Plus、Vite。
## Global Constraints
- `score` 仍是现有排名和计算的输入,本功能只能修改其展示名称。
- 分母使用当前教学班可访问且未禁用的任务;班级没有任务时显示 `--`
- 已完成数按任务模块去重,且只能计入该班已分配模块。
- 不新增数据库字段或迁移脚本,不改变教师、学校管理员、学生的权限判断。
---
### Task 1: 为成绩列表和导出补充实训进度
**Files:**
- Modify: `E:/workspace/dianshang/link_commerce/.worktrees/optional-administrative-class/src/main/java/com/sztzjy/linkCommerce/entity/StuRank.java`
- Modify: `E:/workspace/dianshang/link_commerce/.worktrees/optional-administrative-class/src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java`
- Create: `E:/workspace/dianshang/link_commerce/.worktrees/optional-administrative-class/src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerProgressTest.java`
**Interfaces:**
- Consumes: `TaskAllocationMapper.selectByExample(TaskAllocationExample)`、`StuGradeMapper.selectByExample(StuGradeExample)`、`StuRankMapper.selectByExample(StuRankExample)`。
- Produces: `StuRank.trainingProgress``BigDecimal`,范围 0100 或 `null`)、`completedTaskCount``Integer`)、`totalTaskCount``Integer`),由 `selectStuRankAndScore``exportStuScore` 一致返回/导出。
- [ ] **Step 1: Write the failing backend tests**
Create `TeaScoreControllerProgressTest` with a mocked class task list containing active `任务A`、active `任务B` 和 disabled `任务C`,以及同一学生的 two duplicate `任务A` 成绩记录和 one `任务C` 成绩记录. Assert the returned `StuRank` has `completedTaskCount == 1`, `totalTaskCount == 2`, and `trainingProgress == new BigDecimal("50.00")`. Add a second test with an empty class task list and an empty default task list that asserts `trainingProgress == null` and `totalTaskCount == 0`.
```java
assertEquals(1, row.getCompletedTaskCount());
assertEquals(2, row.getTotalTaskCount());
assertEquals(new BigDecimal("50.00"), row.getTrainingProgress());
assertNull(rowWithoutTasks.getTrainingProgress());
```
- [ ] **Step 2: Run the backend test to verify it fails**
Run: `mvn -q -Dtest=TeaScoreControllerProgressTest test`
Expected: compilation failure because `StuRank` does not yet expose the progress fields.
- [ ] **Step 3: Implement the minimal progress enrichment**
Add `trainingProgress`, `completedTaskCount`, and `totalTaskCount` fields with getters/setters to `StuRank`; they remain transient API/export values and are not added to any mapper XML. Inject `TaskAllocationMapper` into `TeaScoreController`. Add a private `enrichTrainingProgress(List<StuRank> ranks, String requestedClassId)` method that resolves each row's class as `requestedClassId` when supplied, otherwise `rank.getSchoolClassId()`; uses that class's task allocations, falls back to `999999999` only when the class list is empty, ignores `disabledStatus == 1`, and counts distinct `StuGrade.module` values for the same student/class that belong to the active task-module set. Set `trainingProgress` to `null` when the active set is empty; otherwise use `completed * 100 / total` rounded to two decimal places.
Call this helper immediately after both rank-list mapper reads. Change export headers and properties to:
```java
String[] headers = {"学号", "学生姓名", "班级名称", "排名", "参考成绩", "实训进度"};
List<String> listColumn = Arrays.asList("username", "name", "className", "stuRank", "score", "trainingProgress");
```
- [ ] **Step 4: Run the backend tests to verify they pass**
Run: `mvn -q -Dtest=TeaScoreControllerProgressTest,TeaScoreControllerTeachingClassTest test`
Expected: both progress edge cases and existing teaching-class member filtering pass.
- [ ] **Step 5: Commit backend changes**
```bash
git -C E:/workspace/dianshang/link_commerce/.worktrees/optional-administrative-class add src/main/java/com/sztzjy/linkCommerce/entity/StuRank.java src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerProgressTest.java
git -C E:/workspace/dianshang/link_commerce/.worktrees/optional-administrative-class commit -m "feat: add training progress to score center"
```
### Task 2: 在教师端展示参考成绩和实训进度
**Files:**
- Modify: `src/views/teacherEnd/score/index.vue:45-65`
- Create: `tests/score-reference-progress.static.test.cjs`
**Interfaces:**
- Consumes: `getScoreList` 返回的每行 `score`、`trainingProgress`、`completedTaskCount` 和 `totalTaskCount`
- Produces: 主表“参考成绩”和“实训进度”列;进度为 `--`、`0%` 或两位小数百分比,提示已完成/总任务数。
- [ ] **Step 1: Write the failing frontend regression test**
Create `tests/score-reference-progress.static.test.cjs` that loads `src/views/teacherEnd/score/index.vue` and asserts it defines `formatTrainingProgress`, renders `label="参考成绩"` with `prop="score"`, renders `label="实训进度"`, and renders the `--` fallback for null progress.
```js
assert.match(source, /label="参考成绩"\s+prop="score"/);
assert.match(source, /label="实训进度"/);
assert.match(source, /formatTrainingProgress/);
assert.match(source, /return "--"/);
```
- [ ] **Step 2: Run the frontend test to verify it fails**
Run: `node tests/score-reference-progress.static.test.cjs`
Expected: assertion failure because the page still labels score as “实训成绩” and has no formatter.
- [ ] **Step 3: Implement the display-only table changes**
In `src/views/teacherEnd/score/index.vue`, replace the `实训成绩` table-column label with `参考成绩`. Insert an `实训进度` column after it whose cell invokes `formatTrainingProgress(row)`. Add:
```js
const formatTrainingProgress = (row) => {
if (row.trainingProgress === null || row.trainingProgress === undefined) return "--";
const percentage = Number(row.trainingProgress);
if (!Number.isFinite(percentage)) return "--";
return `${percentage.toFixed(2).replace(/\.00$/, "")}%`;
};
```
Render `已完成 ${row.completedTaskCount}/${row.totalTaskCount} 项` as the column tooltip/title when `trainingProgress` is present. Do not add a form control, request parameter, or ranking calculation to the frontend.
- [ ] **Step 4: Run the frontend checks to verify they pass**
Run: `node tests/score-reference-progress.static.test.cjs && npm run build:prod`
Expected: regression assertions pass and Vite production build completes with exit code 0.
- [ ] **Step 5: Commit frontend changes**
```bash
git add src/views/teacherEnd/score/index.vue tests/score-reference-progress.static.test.cjs
git commit -m "feat: show reference score and training progress"
```
### Task 3: End-to-end verification and development-service refresh
**Files:**
- Verify only: backend and frontend files changed in Tasks 12.
**Interfaces:**
- Consumes: the score-center API output and teacher score table changes.
- Produces: a verified local backend/frontend service without uncommitted changes.
- [ ] **Step 1: Run complete backend verification**
Run: `mvn -q -DforkCount=0 test`
Expected: all backend unit tests pass, including `TeaScoreControllerProgressTest`.
- [ ] **Step 2: Run relevant frontend regressions and inspect the worktree**
Run: `node tests/score-reference-progress.static.test.cjs && node tests/school-product-config.static.test.cjs && git diff --check && git status --short`
Expected: both tests pass, diff check prints no errors, and frontend worktree is clean.
- [ ] **Step 3: Refresh and probe local services**
Restart only the backend if it changed, restart the Vite server at `127.0.0.1:147`, then request `http://127.0.0.1:147/index.html` and the backend root endpoint. Expect HTTP 200 from both.
- [ ] **Step 4: Report the fixed data contract**
State that the list and Excel export call the existing score field “参考成绩”, progress is read-only, `--` means no enabled task, and historical scoring/ranking and permission behavior remain unchanged.