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.

291 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# AI 助学与 AI 助评 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:** 在每个学生实训任务中提供一次 AI 助学和一次 AI 助评,保存答案快照与报告,并将助评分数回写至用户-任务作答记录。
**Architecture:** 新增一个按学生、教学班和任务唯一的 `ai_training_evaluation` 聚合记录同时容纳助学和助评两组状态、快照和结果。Spring Boot 服务端读取任务与作答、通过千问 OpenAI 兼容 Chat Completions 接口取得 JSON再做本地校验和持久化Vue 公共实训页通过独立组件展示状态、确认调用和报告。
**Tech Stack:** Java 8/Spring Boot、MyBatis XML、MySQL、Jackson、OkHttp 4、Vue 3、Element Plus、Axios。
## Global Constraints
- API Key 仅保存在未跟踪的 `src/main/resources/application-local.yml`;不得写入前端、测试、日志或提交的 YAML。
- 服务端从 JWT 取得学生身份POST 请求不得接收用户 ID、任务内容或作答。
- 每项功能仅在成功后锁定;`FAILED` 可重试;`PROCESSING` 不触发第二次模型调用。
- 助评必须是 0100 的整数,且 35 个维度的满分和得分分别都等于 100。
- 生成报告时必须保存任务与四步答案快照;之后修改答案不得影响已保存报告。
- 不使用 `v-html` 渲染模型内容。
---
## File structure
- Create backend: `config/ai/QwenProperties.java`, `ai/QwenChatClient.java`, `ai/DashScopeQwenChatClient.java`, `entity/AiTrainingEvaluation.java`, `entity/dto/AiTrainingEvaluationView.java`, `mapper/AiTrainingEvaluationMapper.java`, `service/AiTrainingEvaluationService.java`, `service/impl/AiTrainingEvaluationServiceImpl.java`, `controller/stu/AiTrainingEvaluationController.java` and matching mapper XML/tests.
- Modify backend: `StudentTrainingAnswer.java`, `StudentTrainingAnswerMapper.java`, `StudentTrainingAnswerMapper.xml`, `StudentTrainingAnswerServiceImpl.java`, `application.yml`.
- Create frontend: `E:/workspace/dianshang/e-commerce-internet/src/api/aiTrainingEvaluation.js`, `E:/workspace/dianshang/e-commerce-internet/src/views/components/TrainingAiEvaluation.vue`, `E:/workspace/dianshang/e-commerce-internet/tests/ai-training-evaluation.static.test.cjs`.
- Modify frontend: `E:/workspace/dianshang/e-commerce-internet/src/views/training/GenericTrainingPage.vue`.
- Create migration: `docs/sql/2026-07-16-ai-training-evaluation.sql`.
## Task 1: 建立持久化模型和数据库迁移
**Files:**
- Create: `docs/sql/2026-07-16-ai-training-evaluation.sql`
- Create: `src/main/java/com/sztzjy/linkCommerce/entity/AiTrainingEvaluation.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/mapper/AiTrainingEvaluationMapper.java`
- Create: `src/main/resources/mappers/AiTrainingEvaluationMapper.xml`
- Modify: `src/main/java/com/sztzjy/linkCommerce/entity/StudentTrainingAnswer.java`
- Modify: `src/main/java/com/sztzjy/linkCommerce/mapper/StudentTrainingAnswerMapper.java`
- Modify: `src/main/resources/mappers/StudentTrainingAnswerMapper.xml`
- Modify: `src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java`
- Test: `src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java`
**Interfaces:** Produces `findByStudentClassAndTask(studentUserId, teachingClassId, taskId)`, atomic `claimHelp/claimAssessment`, complete/fail mapper methods, and `updateAiAssessmentScore(answerId, score, updateTime)`.
- [ ] **Step 1: Write the failing model test and migration.**
Create the migration with exactly this core DDL:
```sql
CREATE TABLE IF NOT EXISTS ai_training_evaluation (
id varchar(64) NOT NULL, student_user_id varchar(64) NOT NULL,
teaching_class_id varchar(64) NOT NULL, task_id varchar(64) NOT NULL, task_key varchar(128) NOT NULL,
help_status varchar(16) NOT NULL DEFAULT 'NOT_STARTED', help_task_snapshot longtext NULL,
help_answer_snapshot longtext NULL, help_report_json longtext NULL, help_raw_response longtext NULL,
help_model varchar(128) NULL, help_completed_at datetime NULL, help_error_message varchar(1000) NULL,
assessment_status varchar(16) NOT NULL DEFAULT 'NOT_STARTED', assessment_task_snapshot longtext NULL,
assessment_answer_snapshot longtext NULL, assessment_score int NULL, assessment_report_json longtext NULL,
assessment_raw_response longtext NULL, assessment_model varchar(128) NULL, assessment_completed_at datetime NULL,
assessment_error_message varchar(1000) NULL, create_time datetime NOT NULL, update_time datetime NOT NULL,
PRIMARY KEY (id), UNIQUE KEY uk_ai_training_evaluation (student_user_id, teaching_class_id, task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE student_training_answer ADD COLUMN IF NOT EXISTS ai_assessment_score int NULL;
```
Write a test that references the new entity and calls `answer.setAiAssessmentScore(86)`.
- [ ] **Step 2: Run the test to verify it fails.**
Run: `mvn -Dtest=AiTrainingEvaluationServiceImplTest test`
Expected: FAIL because the entity and score property do not exist.
- [ ] **Step 3: Implement the mappings.**
Give `AiTrainingEvaluation` String fields for both status groups, snapshots/reports/raw responses/models/errors, Integer `assessmentScore`, and Date timestamps. Add all fields to its XML `resultMap`.
Add these mapper signatures:
```java
AiTrainingEvaluation findByStudentClassAndTask(String studentUserId, String teachingClassId, String taskId);
int insertIgnore(AiTrainingEvaluation record);
int claimHelp(String id, Date updateTime);
int claimAssessment(String id, Date updateTime);
int completeHelp(AiTrainingEvaluation record);
int completeAssessment(AiTrainingEvaluation record);
int failHelp(String id, String errorMessage, Date updateTime);
int failAssessment(String id, String errorMessage, Date updateTime);
int updateAiAssessmentScore(String id, Integer score, Date updateTime);
```
`insertIgnore` must be `INSERT IGNORE`; each claim is `UPDATE ... SET <status>='PROCESSING' WHERE id=#{id} AND <status> IN ('NOT_STARTED','FAILED')`. Add `ai_assessment_score` to the existing answer entity, result map, column list and dynamic insert/update. Add the score column to `ensureStudentTrainingAnswerTable()` for fresh databases.
- [ ] **Step 4: Run the test to verify it passes.**
Run: `mvn -Dtest=AiTrainingEvaluationServiceImplTest test`
Expected: PASS.
- [ ] **Step 5: Commit the persistence task.**
```powershell
git add docs/sql/2026-07-16-ai-training-evaluation.sql src/main/java/com/sztzjy/linkCommerce/entity/AiTrainingEvaluation.java src/main/java/com/sztzjy/linkCommerce/entity/StudentTrainingAnswer.java src/main/java/com/sztzjy/linkCommerce/mapper/AiTrainingEvaluationMapper.java src/main/java/com/sztzjy/linkCommerce/mapper/StudentTrainingAnswerMapper.java src/main/resources/mappers/AiTrainingEvaluationMapper.xml src/main/resources/mappers/StudentTrainingAnswerMapper.xml src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java
git commit -m "feat: persist AI training evaluations"
```
## Task 2: 封装千问 JSON 调用与安全配置
**Files:**
- Create: `src/main/java/com/sztzjy/linkCommerce/config/ai/QwenProperties.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/ai/QwenChatClient.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/ai/DashScopeQwenChatClient.java`
- Modify: `src/main/resources/application.yml`
- Test: `src/test/java/com/sztzjy/linkCommerce/ai/DashScopeQwenChatClientTest.java`
**Interfaces:** Produces `String QwenChatClient.completeJson(String systemPrompt, String userPrompt)`, used only by the service in Task 3.
- [ ] **Step 1: Write the failing HTTP contract test.**
Use an OkHttp mock web server response of `{"choices":[{"message":{"content":"{\"score\":86}"}}]}`. Assert `POST /compatible-mode/v1/chat/completions`, `Authorization: Bearer test-key`, and request body containing `"response_format":{"type":"json_object"}`. Add a missing-content test expecting `ServiceException` with `BAD_GATEWAY`.
- [ ] **Step 2: Run the client test to verify it fails.**
Run: `mvn -Dtest=DashScopeQwenChatClientTest test`
Expected: FAIL because the Qwen client is absent.
- [ ] **Step 3: Implement the configuration and client.**
Use `@Component` and `@ConfigurationProperties(prefix = "ai.qwen")` on `QwenProperties` with `baseUrl`, `apiKey`, `model`, and `timeoutMillis`. Add only this committed configuration:
```yaml
ai:
qwen:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: ${DASHSCOPE_API_KEY:}
model: qwen-plus
timeout-millis: 60000
```
Build the OpenAI-compatible body with two messages, `temperature: 0.2`, and JSON mode. Request `baseUrl + "/chat/completions"`. Reject blank keys, non-2xx responses, missing choices or blank message content, and never include the key or full prompt in thrown messages.
- [ ] **Step 4: Run the client test to verify it passes.**
Run: `mvn -Dtest=DashScopeQwenChatClientTest test`
Expected: PASS.
- [ ] **Step 5: Commit the client task.**
```powershell
git add src/main/java/com/sztzjy/linkCommerce/config/ai/QwenProperties.java src/main/java/com/sztzjy/linkCommerce/ai/QwenChatClient.java src/main/java/com/sztzjy/linkCommerce/ai/DashScopeQwenChatClient.java src/main/resources/application.yml src/test/java/com/sztzjy/linkCommerce/ai/DashScopeQwenChatClientTest.java
git commit -m "feat: add Qwen JSON chat client"
```
## Task 3: 实现状态机、提示词和学生 API
**Files:**
- Create: `src/main/java/com/sztzjy/linkCommerce/entity/dto/AiTrainingEvaluationView.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/service/AiTrainingEvaluationService.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationController.java`
- Modify: `src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java`
- Create: `src/test/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationControllerTest.java`
**Interfaces:**
```java
AiTrainingEvaluationView get(String taskKey, JwtUser user);
AiTrainingEvaluationView generateHelp(String taskKey, JwtUser user);
AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user);
```
- [ ] **Step 1: Write failing service tests.**
Mock `TrainingTaskMapper`, `StudentTrainingAnswerMapper`, `TeachingClassStudentMapper`, `AiTrainingEvaluationMapper`, and `QwenChatClient`. Add tests named `rejectsBlankAllStepAnswers`, `helpSuccessStoresSnapshotAndReport`, `assessmentSuccessWritesScoreToAnswer`, `assessmentRejectsInvalidCriteriaTotal`, `succeededHelpReturnsStoredReportWithoutCallingQwenAgain`, `failedHelpCanBeClaimedAgain`, `processingAssessmentDoesNotCallQwenAgain`, and `rejectsNonStudentJwtUser`.
Use a valid assessment response with 3 dimensions at maximum scores 40/35/25 and earned 34/30/22, total 86. Verify the snapshot has task background/objectives/requirements plus all four answer values, and verify `updateAiAssessmentScore(answerId, 86, any(Date.class))`.
- [ ] **Step 2: Run the service test to verify it fails.**
Run: `mvn -Dtest=AiTrainingEvaluationServiceImplTest test`
Expected: FAIL because the service is absent.
- [ ] **Step 3: Implement the service and controller.**
Reuse existing student role, task and teaching-class checks. Reject empty `step1Answer``step4Answer`. Insert-ignore then read the aggregate; if the requested state is `SUCCEEDED`, return its stored view; if `PROCESSING`, return its stored view without calling Qwen; otherwise claim atomically.
Create JSON snapshots with `background`, `objectives`, `requirements`, `step1Answer`, `step2Answer`, `step3Answer`, `step4Answer`. Call Qwen outside any transaction. Parse with `ObjectMapper.readTree`; help requires `overallDiagnosis`, `strengths`, `improvementAreas`, `recommendedActions`. Assessment requires an integer 0100 `score`, 35 criteria, integer `maxScore`/`score` in every criteria, and separate maximum/earned totals of 100. On parse or provider failure write only the target status as `FAILED` and a 1000-character error; on success write report/raw response/model/completed time and, in a short transactional method, update the answer score.
Expose `GET`, `POST /help`, and `POST /assessment` under `@RequestMapping("api/student/training-tasks/{taskKey}/ai-evaluation")`; get the user from `TokenProvider.getJWTUser(request)` and return `ResultEntity` exactly as current student controllers do. The view must exclude snapshots, raw response and detailed errors.
- [ ] **Step 4: Run service and controller tests.**
Run: `mvn -Dtest=AiTrainingEvaluationServiceImplTest,AiTrainingEvaluationControllerTest test`
Expected: PASS.
- [ ] **Step 5: Commit the API task.**
```powershell
git add src/main/java/com/sztzjy/linkCommerce/entity/dto/AiTrainingEvaluationView.java src/main/java/com/sztzjy/linkCommerce/service/AiTrainingEvaluationService.java src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java src/main/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationController.java src/test/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImplTest.java src/test/java/com/sztzjy/linkCommerce/controller/stu/AiTrainingEvaluationControllerTest.java
git commit -m "feat: add AI help and assessment APIs"
```
## Task 4: 在公共实训页展示和调用 AI 评价
**Files:**
- Create: `E:/workspace/dianshang/e-commerce-internet/src/api/aiTrainingEvaluation.js`
- Create: `E:/workspace/dianshang/e-commerce-internet/src/views/components/TrainingAiEvaluation.vue`
- Modify: `E:/workspace/dianshang/e-commerce-internet/src/views/training/GenericTrainingPage.vue`
- Create: `E:/workspace/dianshang/e-commerce-internet/tests/ai-training-evaluation.static.test.cjs`
**Interfaces:** `getAiTrainingEvaluation(taskKey)`, `generateAiTrainingHelp(taskKey)`, and `generateAiTrainingAssessment(taskKey)`; component props are `taskKey` and `hasAnswer`.
- [ ] **Step 1: Write the failing static frontend test.**
Read the three files and assert the three endpoint URLs, `<TrainingAiEvaluation :task-key="pageKey" :has-answer="hasAnyAnswer" />`, strings `AI助学`/`AI助评`, `$modal.confirm`, both status names, no `v-html`, and disable expressions covering `!hasAnswer`, `PROCESSING`, and `SUCCEEDED`.
- [ ] **Step 2: Run the static test to verify it fails.**
Run: `node tests/ai-training-evaluation.static.test.cjs`
Expected: FAIL because API and component files are absent.
- [ ] **Step 3: Implement API, component, and parent integration.**
API methods are:
```js
export const getAiTrainingEvaluation = (taskKey) => request({ url: `/api/student/training-tasks/${taskKey}/ai-evaluation`, method: "get" });
export const generateAiTrainingHelp = (taskKey) => request({ url: `/api/student/training-tasks/${taskKey}/ai-evaluation/help`, method: "post", headers: { repeatSubmit: false } });
export const generateAiTrainingAssessment = (taskKey) => request({ url: `/api/student/training-tasks/${taskKey}/ai-evaluation/assessment`, method: "post", headers: { repeatSubmit: false } });
```
`TrainingAiEvaluation` loads on mount and `taskKey` change; asks `await proxy.$modal.confirm("将基于当前答案创建不可修改的快照,确认继续吗?")`; keeps help and assessment loading/status independent; reloads state after errors; and renders report properties using interpolation, `v-for`, and `el-descriptions` only. In `GenericTrainingPage.vue`, define:
```js
const hasAnyAnswer = computed(() => [form.value, workRows.value, processRows.value]
.some((value) => JSON.stringify(value).replace(/[\[\]{}\",:\s]/g, "").length > 0));
```
Import/render the component immediately before `TrainingAiSidebar`. No answer means both component buttons are disabled; each succeeded feature remains disabled while the other can still run.
- [ ] **Step 4: Run frontend verification.**
Run: `node tests/ai-training-evaluation.static.test.cjs`
Expected: PASS.
Run: `npm run build:prod`
Expected: Vite exits with code 0.
- [ ] **Step 5: Commit the frontend task.**
```powershell
git add src/api/aiTrainingEvaluation.js src/views/components/TrainingAiEvaluation.vue src/views/training/GenericTrainingPage.vue tests/ai-training-evaluation.static.test.cjs
git commit -m "feat: add AI evaluation controls to training page"
```
## Task 5: 全量验证与部署准备
**Files:**
- Modify only if needed: `docs/superpowers/specs/2026-07-16-ai-training-help-assessment-design.md`
- [ ] **Step 1: Run final backend verification.**
Run: `mvn -Dtest=AiTrainingEvaluationServiceImplTest,DashScopeQwenChatClientTest,AiTrainingEvaluationControllerTest,StudentTrainingAnswerServiceImplTest test`
Expected: PASS.
Run: `mvn -DskipTests compile`
Expected: BUILD SUCCESS.
- [ ] **Step 2: Verify migration and secrets.**
Execute `docs/sql/2026-07-16-ai-training-evaluation.sql` twice against the target MySQL schema; the second run must have no structural error. Confirm `application-local.yml` is ignored and run `rg -n "sk-[A-Za-z0-9]" src docs tests`; expected output is empty.
- [ ] **Step 3: Manually accept the feature.**
As a student, save at least one answer, run both functions, refresh and see the persisted reports and score, edit the answer and confirm old reports stay unchanged, repeat both successful calls and confirm no new model call occurs, then verify empty-answer and non-student calls are rejected.
- [ ] **Step 4: Commit documentation only after the worktree is clean or its scope is explicitly staged.**
```powershell
git add docs/superpowers/specs/2026-07-16-ai-training-help-assessment-design.md docs/superpowers/plans/2026-07-16-ai-training-help-assessment.md
git commit -m "docs: add AI training evaluation design and plan"
```