merge: product development factors steps three and four

dev-QQq
chenyuan 3 weeks ago
commit d15cfccaba

@ -0,0 +1,221 @@
# 产品开发关键因素第 3 步 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:** 为学生端产品开发关键因素实训实现第 3 步商业化及市场反馈分析、服务端校验与只保存进入第 4 步的流程。
**Architecture:** 前端在现有 `product-development-factors.vue` 中增加固定三行的第 3 步表格,并把数据独立写入 `step3Answer`。后端按前两步的分层模式新增 DTO、服务及控制器入口严格验证学生身份、三项固定维度及六个非空分析字段。
**Tech Stack:** Vue 3 Composition API、Element Plus、现有 request APISpring Boot、JUnit 5、Mockito、Maven。
## Global Constraints
- 仅修改学生端页面及学生答案校验接口;不调整教师端或任务配置。
- 不新增数据库表或字段,使用既有答案记录的 `step3Answer``currentStep`
- 第 3 步保存固定使用 `SAVE`、`submitted: false`、`currentStep: 4`,不得提交任务。
- 保存前必须通过正式或演示模式的一致校验:三个固定维度、六个分析字段全部非空。
---
### Task 1: 第 3 步后端校验服务
**Files:**
- Create: `src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDevelopmentFactorsStepThreeValidationRequest.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDevelopmentFactorsStepThreeValidationResult.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/service/ProductDevelopmentFactorsStepThreeService.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/service/impl/ProductDevelopmentFactorsStepThreeServiceImpl.java`
- Test: `src/test/java/com/sztzjy/linkCommerce/service/impl/ProductDevelopmentFactorsStepThreeServiceImplTest.java`
**Interfaces:**
- Consumes: `JwtUser`、`ServiceException`、Spring HTTP status conventions。
- Produces: `ProductDevelopmentFactorsStepThreeService#validate(ProductDevelopmentFactorsStepThreeValidationRequest, JwtUser)`,返回 `{ valid, message }`
- [ ] **Step 1: 写失败服务测试**
```java
@Test void acceptsCompleteStudentAnalyses() {
assertTrue(new ProductDevelopmentFactorsStepThreeServiceImpl().validate(valid(), student()).isValid());
}
@Test void rejectsBlankOrForgedAnalyses() {
ProductDevelopmentFactorsStepThreeValidationRequest blank = valid();
blank.getFactors().get(2).setFailedAnalysis(" ");
assertThrows(ServiceException.class, () -> service.validate(blank, student()));
ProductDevelopmentFactorsStepThreeValidationRequest forged = valid();
forged.getFactors().get(2).setDimension("伪造维度");
assertThrows(ServiceException.class, () -> service.validate(forged, student()));
}
```
- [ ] **Step 2: 运行测试确认失败**
Run: `mvn -B -Dtest=ProductDevelopmentFactorsStepThreeServiceImplTest test`
Expected: FAIL因为 DTO 和服务尚不存在。
- [ ] **Step 3: 实现最小服务**
```java
private static final Set<String> DIMENSIONS = new LinkedHashSet<>(Arrays.asList(
"供应链与成本控制", "商业模式", "市场反馈"));
if (user == null || StringUtils.isBlank(user.getUserId())) throw new ServiceException(HttpStatus.UNAUTHORIZED, "请先登录后再提交");
if (user.getRoleId() != 4) throw new ServiceException(HttpStatus.FORBIDDEN, "仅学生可提交实训答案");
```
校验 `factors` 数量为 3、每个维度只出现一次且属于 `DIMENSIONS`,并要求每个 `successAnalysis`、`failedAnalysis` 均非空;合法时返回 `valid=true`、`message="校验通过"`。
- [ ] **Step 4: 运行服务测试确认通过**
Run: `mvn -B -Dtest=ProductDevelopmentFactorsStepThreeServiceImplTest test`
Expected: PASS覆盖合法、空值、伪造维度和非学生身份。
- [ ] **Step 5: 提交后端服务**
```bash
git add src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDevelopmentFactorsStepThreeValidationRequest.java src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDevelopmentFactorsStepThreeValidationResult.java src/main/java/com/sztzjy/linkCommerce/service/ProductDevelopmentFactorsStepThreeService.java src/main/java/com/sztzjy/linkCommerce/service/impl/ProductDevelopmentFactorsStepThreeServiceImpl.java src/test/java/com/sztzjy/linkCommerce/service/impl/ProductDevelopmentFactorsStepThreeServiceImplTest.java
git commit -m "feat: validate product factors step three"
```
### Task 2: 第 3 步校验控制器入口
**Files:**
- Modify: `src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java`
- Modify: `src/test/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerControllerTest.java`
**Interfaces:**
- Consumes: Task 1 的 `ProductDevelopmentFactorsStepThreeService`
- Produces: `POST /api/student-training-answers/product-development-factors/step-3/validate`
- [ ] **Step 1: 写失败控制器测试**
```java
@Test void validateProductDevelopmentFactorsStepThreeUsesTheAuthenticatedStudentAndReturnsTheValidationResult() {
ProductDevelopmentFactorsStepThreeService service = mock(ProductDevelopmentFactorsStepThreeService.class);
ReflectionTestUtils.setField(controller, "productDevelopmentFactorsStepThreeService", service);
when(service.validate(any(ProductDevelopmentFactorsStepThreeValidationRequest.class), any(JwtUser.class))).thenReturn(validation);
ResultEntity<ProductDevelopmentFactorsStepThreeValidationResult> result = controller.validateProductDevelopmentFactorsStepThree(new ProductDevelopmentFactorsStepThreeValidationRequest(), authenticatedStudentRequest());
assertEquals(HttpStatus.OK, result.getStatusCode());
}
```
- [ ] **Step 2: 运行测试确认失败**
Run: `mvn -B -Dtest=StudentTrainingAnswerControllerTest test`
Expected: FAIL因为第 3 步服务注入字段和控制器方法尚不存在。
- [ ] **Step 3: 增加路由与转发**
```java
@PostMapping("/product-development-factors/step-3/validate")
public ResultEntity<ProductDevelopmentFactorsStepThreeValidationResult> validateProductDevelopmentFactorsStepThree(
@RequestBody ProductDevelopmentFactorsStepThreeValidationRequest request,
HttpServletRequest servletRequest) {
return ResultEntity.ok(productDevelopmentFactorsStepThreeService.validate(request, getJwtUser(servletRequest)));
}
```
- [ ] **Step 4: 运行控制器测试确认通过**
Run: `mvn -B -Dtest=StudentTrainingAnswerControllerTest,ProductDevelopmentFactorsStepThreeServiceImplTest test`
Expected: PASS。
- [ ] **Step 5: 提交控制器**
```bash
git add src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java src/test/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerControllerTest.java
git commit -m "feat: expose product factors step three validation"
```
### Task 3: 第 3 步学生页面、草稿和 API
**Files:**
- Modify: `src/views/foundation/product-development-factors.vue`
- Modify: `src/api/studentTrainingAnswer.js`
- Modify: `tests/product-development-factors-step-one.static.test.cjs`
**Interfaces:**
- Consumes: Task 2 的正式校验 URL、既有 `saveStudentTrainingAnswer`、`getStudentTrainingAnswer`。
- Produces: 第 3 步固定三行分析页面;`step3Answer`、`currentStep: 4`、`checkProductDevelopmentFactorsStepThree(payload)`。
- [ ] **Step 1: 先扩展前端静态契约测试**
```js
assert.match(pageSource, /STEP_THREE_FACTOR_ROWS/);
assert.match(pageSource, /供应链与成本控制/);
assert.match(pageSource, /商业模式/);
assert.match(pageSource, /市场反馈/);
assert.match(pageSource, /step3Answer/);
assert.match(pageSource, /saveAndGoToStepFour/);
assert.match(apiSource, /product-development-factors\/step-3\/validate/);
```
- [ ] **Step 2: 运行静态测试确认失败**
Run: `node tests/product-development-factors-step-one.static.test.cjs`
Expected: FAIL因为第 3 步常量、保存函数和 API 尚不存在。
- [ ] **Step 3: 实现前端最小改动**
`currentStep === 2` 增加表格;建立 `STEP_THREE_FACTOR_ROWS`、`createStepThreeRows`、`stepThreeRows`,并扩展草稿、答案恢复、导出与清空。
```js
function buildStepThreeAnswerPayload(saveAction = "SAVE", persistedStep = 4) {
return {
step3Answer: JSON.stringify(buildStepThreeOutcome()),
currentStep: persistedStep,
submitted: false,
saveAction,
};
}
```
实现正式 API 和演示模式的一致校验;`saveAndGoToStepFour` 成功后清理草稿并在第 4 步存在时将 `currentStep` 设为 `3`,否则显示已保存但未配置第 4 步的提示。
- [ ] **Step 4: 运行前端测试与生产构建**
Run: `node tests/product-development-factors-step-one.static.test.cjs; npm run build:prod`
Expected: 静态契约测试与生产构建均 PASS。
- [ ] **Step 5: 提交前端改动**
```bash
git add src/views/foundation/product-development-factors.vue src/api/studentTrainingAnswer.js tests/product-development-factors-step-one.static.test.cjs
git commit -m "feat: add product factors step three analysis"
```
### Task 4: 全量回归与交付检查
**Files:**
- Verify only: 前后端工作树的已提交变更。
**Interfaces:**
- Consumes: Tasks 13。
- Produces: 经过完整回归验证的前后端功能分支。
- [ ] **Step 1: 运行后端全量测试**
Run: `mvn -B test`
Expected: BUILD SUCCESS所有测试通过。
- [ ] **Step 2: 运行前端完整验证**
Run: `node tests/product-development-factors-step-one.static.test.cjs; npm run build:prod`
Expected: 静态契约测试和生产构建成功。
- [ ] **Step 3: 审查工作树**
Run: `git status --short; git log --oneline -3`
Expected: 仅本功能的预期提交;不纳入主工作区已有未跟踪文件。
- [ ] **Step 4: 汇报并等待集成指令**
报告功能、提交和验证结果,等待用户明确指示合并、推送或发布。

@ -0,0 +1,150 @@
# 产品开发关键因素第 4 步 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:** 实现学生端产品开发关键因素第 4 步总结与最终任务提交,并提供后端非空校验。
**Architecture:** 前端在现有四步页面中增加两个总结文本框,把数据独立持久化到 `step4Answer`。后端延续前三步校验模式,新增 DTO、服务和控制器路由校验通过后由前端以 `SUBMIT` 终结任务。
**Tech Stack:** Vue 3 Composition API、Element Plus、Spring Boot、JUnit 5、Mockito、Maven。
## Global Constraints
- 仅修改学生端和学生答案校验接口,不修改教师端或任务配置。
- 沿用当前隔离分支,最终分支同时包含第 3、4 步。
- 不新增数据库字段,使用 `step4Answer``currentStep: 4`
- 最终保存使用 `saveAction: "SUBMIT"`、`submitted: true`,两个因素字段必须非空。
---
### Task 1: 第 4 步后端校验服务与路由
**Files:**
- Create: `src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDevelopmentFactorsStepFourValidationRequest.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDevelopmentFactorsStepFourValidationResult.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/service/ProductDevelopmentFactorsStepFourService.java`
- Create: `src/main/java/com/sztzjy/linkCommerce/service/impl/ProductDevelopmentFactorsStepFourServiceImpl.java`
- Modify: `src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java`
- Test: `src/test/java/com/sztzjy/linkCommerce/service/impl/ProductDevelopmentFactorsStepFourServiceImplTest.java`
- Test: `src/test/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerControllerTest.java`
**Interfaces:**
- Produces: `POST /api/student-training-answers/product-development-factors/step-4/validate`
- Request: `{ successFactors: String, failedFactors: String }`
- Response: `{ valid: boolean, message: String }`
- [ ] **Step 1: 写失败服务测试并验证红灯**
```java
@Test void acceptsCompleteStudentSummary() {
assertTrue(new ProductDevelopmentFactorsStepFourServiceImpl().validate(valid(), student()).isValid());
}
@Test void rejectsBlankSummaryOrNonStudent() {
ProductDevelopmentFactorsStepFourValidationRequest blank = valid();
blank.setFailedFactors(" ");
assertThrows(ServiceException.class, () -> service.validate(blank, student()));
assertThrows(ServiceException.class, () -> service.validate(valid(), teacher()));
}
```
Run: `mvn -B -Dtest=ProductDevelopmentFactorsStepFourServiceImplTest test`
Expected: FAIL因为第 4 步类型与服务尚未创建。
- [ ] **Step 2: 实现最小 DTO 与服务**
```java
if (user == null || StringUtils.isBlank(user.getUserId())) throw new ServiceException(HttpStatus.UNAUTHORIZED, "请先登录后再提交");
if (user.getRoleId() != 4) throw new ServiceException(HttpStatus.FORBIDDEN, "仅学生可提交实训答案");
if (!StringUtils.isNotBlank(request.getSuccessFactors())) throw badRequest("请填写产品成功的关键因素");
if (!StringUtils.isNotBlank(request.getFailedFactors())) throw badRequest("请填写产品失败的关键因素");
```
- [ ] **Step 3: 写失败控制器测试、实现路由并验证绿灯**
```java
@PostMapping("/product-development-factors/step-4/validate")
public ResultEntity<ProductDevelopmentFactorsStepFourValidationResult> validateProductDevelopmentFactorsStepFour(
@RequestBody ProductDevelopmentFactorsStepFourValidationRequest answer, HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "校验通过", productDevelopmentFactorsStepFourService.validate(answer, TokenProvider.getJWTUser(request)));
}
```
Run: `mvn -B "-Dtest=StudentTrainingAnswerControllerTest,ProductDevelopmentFactorsStepFourServiceImplTest" test`
Expected: PASS。
- [ ] **Step 4: 提交后端改动**
```bash
git add src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDevelopmentFactorsStepFourValidationRequest.java src/main/java/com/sztzjy/linkCommerce/entity/dto/ProductDevelopmentFactorsStepFourValidationResult.java src/main/java/com/sztzjy/linkCommerce/service/ProductDevelopmentFactorsStepFourService.java src/main/java/com/sztzjy/linkCommerce/service/impl/ProductDevelopmentFactorsStepFourServiceImpl.java src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java src/test/java/com/sztzjy/linkCommerce/service/impl/ProductDevelopmentFactorsStepFourServiceImplTest.java src/test/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerControllerTest.java
git commit -m "feat: validate product factors step four"
```
### Task 2: 第 4 步学生页面与最终提交
**Files:**
- Modify: `src/views/foundation/product-development-factors.vue`
- Modify: `src/api/studentTrainingAnswer.js`
- Modify: `tests/product-development-factors-step-one.static.test.cjs`
**Interfaces:**
- Consumes: `checkProductDevelopmentFactorsStepFour(payload)`
- Produces: `step4Answer`、`submitTraining()` 和最终 `SUBMIT` 保存。
- [ ] **Step 1: 写失败前端契约测试并验证红灯**
```js
assert.match(page, /v-else-if="currentStep === 3"/);
assert.match(page, /产品成功的关键因素/);
assert.match(page, /产品失败的关键因素/);
assert.match(page, /step4Answer/);
assert.match(page, /await checkProductDevelopmentFactorsStepFour\(buildStepFourValidationPayload\(\)\)/);
assert.match(page, /buildStepFourAnswerPayload\("SUBMIT", 4\)/);
assert.match(api, /product-development-factors\/step-4\/validate/);
```
Run: `node tests/product-development-factors-step-one.static.test.cjs`
Expected: FAIL因为第 4 步表单、校验和提交动作尚不存在。
- [ ] **Step 2: 实现页面、恢复与演示校验**
```js
function buildStepFourAnswerPayload(saveAction = "SUBMIT", persistedStep = 4) {
return { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: persistedStep, submitted: true, saveAction };
}
```
增加 `stepFourForm`、草稿恢复、导出和清空;演示模式与正式接口都要求 `successFactors`、`failedFactors` 非空。`submitTraining` 校验成功后保存、清理草稿并提示“任务提交成功”。
- [ ] **Step 3: 运行前端验证并提交**
Run: `node tests/product-development-factors-step-one.static.test.cjs; npm run build:prod`
Expected: PASS。
```bash
git add src/views/foundation/product-development-factors.vue src/api/studentTrainingAnswer.js tests/product-development-factors-step-one.static.test.cjs
git commit -m "feat: add product factors step four summary"
```
### Task 3: 全量回归与交付
- [ ] **Step 1: 运行后端全量测试**
Run: `mvn -B test`
Expected: BUILD SUCCESS。
- [ ] **Step 2: 运行前端测试和生产构建**
Run: `node tests/product-development-factors-step-one.static.test.cjs; npm run build:prod`
Expected: PASS。
- [ ] **Step 3: 核对提交范围并等待集成指令**
Run: `git status --short; git log --oneline -6`
Expected: 仅第 3、4 步的预期提交;不修改主工作区的已有未跟踪文件。

@ -0,0 +1,52 @@
# 产品开发关键因素第 3 步设计
## 目标
为学生端“产品开发关键因素”实训补齐第 3 步“商业化及市场反馈”分析页。学生对成功产品与失败产品分别完成三项固定维度分析,保存后进入第 4 步,但不提交整个任务。
## 范围与约束
- 仅修改学生端页面及其学生答案校验接口;不调整教师端、任务配置端或其他模块。
- 延续现有 `product-development-factors` 单页多步骤页面、答案存储和本地草稿机制。
- 不新增数据库表或字段:第 3 步内容持久化到现有答案记录的 `step3Answer`,进度写入 `currentStep`
- 成功保存第 3 步后,保存动作固定为 `SAVE`、`submitted: false`、`currentStep: 4`;不得调用任务提交动作。
## 学生页面
`currentStep === 2` 时展示标题“商业化及市场反馈角度分析,填写下表:”,表头为“维度 / 评估项 / 成功产品 / 失败产品”。
固定三行及评估项:
| 维度 | 评估项 |
| --- | --- |
| 供应链与成本控制 | 产品成本是否可控是否支持规模化是否具备备选供应链关键元器件如芯片、传感器有≥2家可替换供应商 |
| 商业模式 | 盈利模式是否清晰硬件销售毛利率≥50%或有订阅服务如云存储、AI 功能包)。是否构建“数据 - 反馈 - 迭代”闭环?产品使用数据可回传,驱动功能优化与新版本开发 |
| 市场反馈 | 是否处于需求上升期?功能、设计、服务、品牌是否有差异化壁垒?用户是否高留存、高互动、低退货? |
每行的成功产品与失败产品列各提供多行文本输入。六个输入均为必填;保存按钮为“保存并进入第 4 步”,清空按钮沿用现有行为。页面恢复已保存答案和本地草稿;导出内容与清空逻辑覆盖前三步。
若任务配置少于四步,保存仍成功,提示“已保存,当前任务尚未配置第 4 步”,并留在当前页;否则切换到第 4 步占位页。
## 前端数据流
- 定义 `STEP_THREE_FACTOR_ROWS``stepThreeRows`,结构与前两步相同:`dimension`、`criteria`、`successAnalysis`、`failedAnalysis`。
- 在 `buildDraft` 中新增 `stepThree`;在加载答案与草稿时恢复第 3 步。
- 仅第 3 步保存时组装 `{ factors: [{ dimension, successAnalysis, failedAnalysis }] }` 发往校验接口;校验通过后,保存 `step3Answer`
- 演示模式使用与正式接口相同的固定维度和六项非空规则,保证离线体验一致。
## 后端校验接口
新增 `POST /api/student-training-answers/product-development-factors/step-3/validate`
- 仅允许已登录、`roleId == 4` 的学生调用;未登录返回 401非学生返回 403。
- 请求体为 `factors` 数组,每项含 `dimension`、`successAnalysis`、`failedAnalysis`。
- 必须且只能提交一次下列三个维度:供应链与成本控制、商业模式、市场反馈。
- 每个维度的成功产品与失败产品分析去除空白后都必须有内容。
- 合法请求返回 `{ valid: true, message: "校验通过" }`;缺项、重复、伪造维度或空内容返回明确的 400 错误信息。
## 测试
- 后端服务测试覆盖:完整合法数据、空内容、伪造或重复维度、非学生身份。
- 控制器测试覆盖:接口把认证学生与请求参数传给第 3 步校验服务并返回结果。
- 前端静态契约测试覆盖:三项固定维度、`step3Answer` 持久化、校验 API、六项非空演示校验以及只保存进入第 4 步、不提交任务的行为。
- 最终运行后端 `mvn -B test` 和前端静态契约测试、生产构建。

@ -0,0 +1,48 @@
# 产品开发关键因素第 4 步设计
## 目标
为学生端“产品开发关键因素”实训补齐第 4 步总结页。学生结合案例资料及前三步分析,填写成功产品与失败产品的关键因素;两个输入都完成后提交整个任务。
## 范围与约束
- 仅修改学生端页面及学生答案校验接口;不调整教师端、任务配置端或其他模块。
- 在当前 `codex/product-development-factors-step3` 隔离分支继续开发,因此最终功能分支包含第 3、4 步,后续一次性合并。
- 不新增数据库表或字段:第 4 步内容使用现有答案记录的 `step4Answer`,进度保持为 `currentStep: 4`
- 最终提交动作固定使用 `saveAction: "SUBMIT"``submitted: true`;不得在提交成功后跳转至不存在的下一步。
## 学生页面
`currentStep === 3` 时,显示说明:“结合案例信息及前述步骤,分析案例资料中产品一成功的关键因素是什么,产品二失败的关键因素是什么。”
展示两个大文本框:
1. 产品成功的关键因素
2. 产品失败的关键因素
两个输入去除首尾空白后都必须有内容。提交按钮为“提交任务”,并提供既有“清空填写”按钮。草稿、保存答案加载和导出内容均包含第 4 步;清空操作同时清除前四步。
提交成功后保留在第 4 步,显示“任务提交成功”。重复点击时沿用现有 `saving` 防抖;接口校验失败时显示服务端错误信息。
## 前端数据流
- 使用 `stepFourForm` 保存 `successFactors`、`failedFactors`。
- `buildDraft` 添加 `stepFour`;加载 `step4Answer` 与本地草稿时恢复该表单。
- 点击提交先请求第 4 步校验接口,再保存 `{ step4Answer, currentStep: 4, submitted: true, saveAction: "SUBMIT" }`
- 演示模式和正式模式都必须执行两个字段非空的同一规则。
## 后端校验接口
新增 `POST /api/student-training-answers/product-development-factors/step-4/validate`
- 仅允许已登录、`roleId == 4` 的学生调用;未登录返回 401非学生返回 403。
- 请求体包含 `successFactors``failedFactors`
- 两个字段去除空白后都必须非空。
- 合法请求返回 `{ valid: true, message: "校验通过" }`;空字段返回明确的 400 错误信息。
## 测试
- 后端服务测试覆盖:合法提交、成功/失败关键因素为空、非学生身份。
- 控制器测试覆盖:接口把认证学生与请求参数传给第 4 步校验服务并返回结果。
- 前端静态契约测试覆盖:第 4 步文本框、`step4Answer`、校验 API、最终 `SUBMIT` 保存动作和演示校验。
- 最终运行后端 `mvn -B test` 和前端静态契约测试、生产构建。

@ -251,3 +251,37 @@ export function checkProductDevelopmentFactorsStepTwo(payload) {
if (isStudentDemo()) { try { return Promise.resolve({ code: 200, data: validateProductDevelopmentFactorsStepTwoDemo(payload) }); } catch (error) { return Promise.reject(error); } }
return request({ url: "/api/student-training-answers/product-development-factors/step-2/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
}
const productDevelopmentFactorsStepThreeDimensions = ["供应链与成本控制", "商业模式", "市场反馈"];
function validateProductDevelopmentFactorsStepThreeDemo(payload) {
const factors = Array.isArray(payload?.factors) ? payload.factors : [];
const dimensions = factors.map((factor) => String(factor?.dimension || "").trim());
if (factors.length !== productDevelopmentFactorsStepThreeDimensions.length
|| new Set(dimensions).size !== productDevelopmentFactorsStepThreeDimensions.length
|| productDevelopmentFactorsStepThreeDimensions.some((dimension) => !dimensions.includes(dimension))) {
throw new Error("请完成供应链与成本控制、商业模式和市场反馈分析");
}
const incomplete = factors.find((factor) => !String(factor?.successAnalysis || "").trim()
|| !String(factor?.failedAnalysis || "").trim());
if (incomplete) throw new Error(`请完成${incomplete.dimension || "该项"}的成功产品和失败产品分析`);
return { valid: true, message: "校验通过" };
}
export function checkProductDevelopmentFactorsStepThree(payload) {
if (isStudentDemo()) {
try { return Promise.resolve({ code: 200, data: validateProductDevelopmentFactorsStepThreeDemo(payload) }); }
catch (error) { return Promise.reject(error); }
}
return request({ url: "/api/student-training-answers/product-development-factors/step-3/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
}
export function checkProductDevelopmentFactorsStepFour(payload) {
const value = payload || {};
if (isStudentDemo()) {
if (!String(value.successFactors || "").trim()) return Promise.reject(new Error("请填写产品成功的关键因素"));
if (!String(value.failedFactors || "").trim()) return Promise.reject(new Error("请填写产品失败的关键因素"));
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/product-development-factors/step-4/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
}

@ -104,6 +104,20 @@
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepThree"> 3 </button><button type="button" class="btn-nav" :disabled="saving" @click="resetTraining"><el-icon><RefreshLeft /></el-icon></button></div>
</template>
<template v-else-if="currentStep === 2">
<p class="factor-intro">商业化及市场反馈角度分析填写下表</p>
<div class="training-table-wrap"><table class="factor-table"><thead><tr><th class="dimension-col">维度</th><th class="criteria-col">评估项</th><th>成功产品</th><th>失败产品</th></tr></thead><tbody>
<tr v-for="row in stepThreeRows" :key="row.dimension"><td class="dimension-cell">{{ row.dimension }}</td><td class="criteria-cell">{{ row.criteria }}</td><td><textarea v-model="row.successAnalysis" class="table-textarea" placeholder="填写成功产品在该维度的分析" /></td><td><textarea v-model="row.failedAnalysis" class="table-textarea" placeholder="填写失败产品在该维度的分析" /></td></tr>
</tbody></table></div>
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="saveAndGoToStepFour"> 4 </button><button type="button" class="btn-nav" :disabled="saving" @click="resetTraining"><el-icon><RefreshLeft /></el-icon></button></div>
</template>
<template v-else-if="currentStep === 3">
<p class="factor-intro">结合案例信息及前述步骤分析案例资料中产品一成功的关键因素是什么产品二失败的关键因素是什么</p>
<label class="factor-intro">产品成功的关键因素</label><textarea v-model="stepFourForm.successFactors" class="table-textarea summary-textarea" />
<label class="factor-intro">产品失败的关键因素</label><textarea v-model="stepFourForm.failedFactors" class="table-textarea summary-textarea" />
<div class="task-actions"><button type="button" class="btn-nav btn-submit" :disabled="saving" @click="submitTraining"></button><button type="button" class="btn-nav" :disabled="saving" @click="resetTraining"></button></div>
</template>
<div v-else class="step-placeholder">
<p> {{ currentStep + 1 }} 步内容待配置</p>
<span>已保存的分析可从上方步骤栏返回查看</span>
@ -125,7 +139,7 @@
<script setup>
import { Download, Operation, RefreshLeft } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { checkProductDevelopmentFactorsStepOne, checkProductDevelopmentFactorsStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { checkProductDevelopmentFactorsStepOne, checkProductDevelopmentFactorsStepTwo, checkProductDevelopmentFactorsStepThree, checkProductDevelopmentFactorsStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import useUserStore from "@/store/modules/user";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
@ -182,6 +196,11 @@ const STEP_TWO_FACTOR_ROWS = [
{ dimension: "技术应用", criteria: "核心技术是否已成熟、可量产?避免使用实验室级原型(如未验证的 AI 模型)" },
{ dimension: "合规与认证", criteria: "是否通过目标市场强制认证目标市场要求的产品资质、检测报告等中国CCC美国FCC欧盟CE出口需符合 RoHS、REACH。是否规避专利与知识产权风险" },
];
const STEP_THREE_FACTOR_ROWS = [
{ dimension: "供应链与成本控制", criteria: "产品成本是否可控是否支持规模化是否具备备选供应链关键元器件如芯片、传感器有≥2家可替换供应商" },
{ dimension: "商业模式", criteria: "盈利模式是否清晰硬件销售毛利率≥50%或有订阅服务如云存储、AI 功能包)。是否构建“数据 - 反馈 - 迭代”闭环?产品使用数据可回传,驱动功能优化与新版本开发" },
{ dimension: "市场反馈", criteria: "是否处于需求上升期?功能、设计、服务、品牌是否有差异化壁垒?用户是否高留存、高互动、低退货?" },
];
const createStepOneRows = () => STEP_ONE_FACTOR_ROWS.map((row) => ({
...row,
@ -189,8 +208,11 @@ const createStepOneRows = () => STEP_ONE_FACTOR_ROWS.map((row) => ({
failedAnalysis: "",
}));
const createStepTwoRows = () => STEP_TWO_FACTOR_ROWS.map((row) => ({ ...row, successAnalysis: "", failedAnalysis: "" }));
const createStepThreeRows = () => STEP_THREE_FACTOR_ROWS.map((row) => ({ ...row, successAnalysis: "", failedAnalysis: "" }));
const stepOneRows = ref(createStepOneRows());
const stepTwoRows = ref(createStepTwoRows());
const stepThreeRows = ref(createStepThreeRows());
const stepFourForm = ref({ successFactors: "", failedFactors: "" });
const progressItems = [
{ text: "进行中:成功/失败产品对比", status: "doing" },
@ -230,6 +252,8 @@ async function loadSavedAnswer() {
clearDraft();
}
if (answer?.step2Answer) stepTwoRows.value = restoreRows(answer.step2Answer, createStepTwoRows);
if (answer?.step3Answer) stepThreeRows.value = restoreRows(answer.step3Answer, createStepThreeRows);
if (answer?.step4Answer) stepFourForm.value = { ...stepFourForm.value, ...parseSavedAnswer(answer.step4Answer) };
restoreCurrentStep(answer?.currentStep);
} catch (error) {
// Keep local draft when the backend answer cannot be loaded.
@ -310,6 +334,8 @@ function loadDraft() {
if (draft?.stepOne) stepOneRows.value = restoreRows(draft.stepOne, createStepOneRows);
else stepOneRows.value = restoreRows(raw, createStepOneRows);
if (draft?.stepTwo) stepTwoRows.value = restoreRows(draft.stepTwo, createStepTwoRows);
if (draft?.stepThree) stepThreeRows.value = restoreRows(draft.stepThree, createStepThreeRows);
if (draft?.stepFour) stepFourForm.value = { ...stepFourForm.value, ...draft.stepFour };
}
} catch (error) {
clearDraft();
@ -336,7 +362,9 @@ function buildStepOneOutcome() {
};
}
function buildStepTwoOutcome() { return { title: taskTitle.value, factors: stepTwoRows.value.map((row) => ({ dimension: row.dimension, criteria: row.criteria, successAnalysis: row.successAnalysis, failedAnalysis: row.failedAnalysis })) }; }
function buildDraft() { return { stepOne: buildStepOneOutcome(), stepTwo: buildStepTwoOutcome(), currentStep: currentStep.value + 1 }; }
function buildStepThreeOutcome() { return { title: taskTitle.value, factors: stepThreeRows.value.map((row) => ({ dimension: row.dimension, criteria: row.criteria, successAnalysis: row.successAnalysis, failedAnalysis: row.failedAnalysis })) }; }
function buildStepFourOutcome() { return { successFactors: stepFourForm.value.successFactors, failedFactors: stepFourForm.value.failedFactors }; }
function buildDraft() { return { stepOne: buildStepOneOutcome(), stepTwo: buildStepTwoOutcome(), stepThree: buildStepThreeOutcome(), stepFour: buildStepFourOutcome(), currentStep: currentStep.value + 1 }; }
function buildStepOneValidationPayload() {
return {
@ -358,6 +386,10 @@ function buildStepOneAnswerPayload(saveAction = "SAVE", persistedStep = currentS
}
function buildStepTwoValidationPayload() { return { factors: stepTwoRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({ dimension, successAnalysis, failedAnalysis })) }; }
function buildStepTwoAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) { return { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: persistedStep, submitted: false, saveAction }; }
function buildStepThreeValidationPayload() { return { factors: stepThreeRows.value.map(({ dimension, successAnalysis, failedAnalysis }) => ({ dimension, successAnalysis, failedAnalysis })) }; }
function buildStepThreeAnswerPayload(saveAction = "SAVE", persistedStep = currentStep.value + 1) { return { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: persistedStep, submitted: false, saveAction }; }
function buildStepFourValidationPayload() { return buildStepFourOutcome(); }
function buildStepFourAnswerPayload(saveAction = "SUBMIT", persistedStep = 4) { return { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: persistedStep, submitted: true, saveAction }; }
async function saveAndGoToStepTwo() {
if (saving.value) return;
@ -392,9 +424,25 @@ async function saveAndGoToStepThree() {
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成技术应用和合规与认证分析"); } finally { saving.value = false; }
}
async function saveAndGoToStepFour() {
if (saving.value) return;
saving.value = true;
try {
await checkProductDevelopmentFactorsStepThree(buildStepThreeValidationPayload());
await saveStudentTrainingAnswer(TASK_KEY, buildStepThreeAnswerPayload("SAVE", 4));
clearDraft();
if (trainingSteps.value.length < 4) { proxy?.$modal?.msgSuccess("已保存,当前任务尚未配置第 4 步"); return; }
currentStep.value = 3;
proxy?.$modal?.msgSuccess("已保存,已进入第 4 步");
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成供应链与成本控制、商业模式和市场反馈分析"); } finally { saving.value = false; }
}
async function submitTraining() { if (saving.value) return; saving.value = true; try { await checkProductDevelopmentFactorsStepFour(buildStepFourValidationPayload()); await saveStudentTrainingAnswer(TASK_KEY, buildStepFourAnswerPayload("SUBMIT", 4)); clearDraft(); proxy?.$modal?.msgSuccess("任务提交成功"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成关键因素分析"); } finally { saving.value = false; } }
async function resetTraining() {
stepOneRows.value = createStepOneRows();
stepTwoRows.value = createStepTwoRows();
stepThreeRows.value = createStepThreeRows();
stepFourForm.value = { successFactors: "", failedFactors: "" };
currentStep.value = 0;
clearDraft();
try {

@ -29,5 +29,23 @@ assert.match(page, /buildStepTwoAnswerPayload\("SAVE", 3\)/, "step two must save
assert.doesNotMatch(page, /buildStepTwoAnswerPayload\("SUBMIT"/, "step two must not submit the task");
assert.match(api, /export function checkProductDevelopmentFactorsStepTwo\(/, "the student API must expose the step-two validator");
assert.match(api, /product-development-factors\/step-2\/validate/, "the student API must call the step-two validation route");
assert.match(page, /const STEP_THREE_FACTOR_ROWS = \[/, "step three must define its fixed rows");
assert.match(page, /供应链与成本控制/, "step three must include supply-chain and cost control");
assert.match(page, /商业模式/, "step three must include business model");
assert.match(page, /市场反馈/, "step three must include market feedback");
assert.match(page, /v-else-if="currentStep === 2"/, "the third table must render only on step three");
assert.match(page, /await checkProductDevelopmentFactorsStepThree\(buildStepThreeValidationPayload\(\)\)/, "step three must validate before saving");
assert.match(page, /step3Answer: JSON\.stringify\(buildStepThreeOutcome\(\)\)/, "step three must save its own answer field");
assert.match(page, /buildStepThreeAnswerPayload\("SAVE", 4\)/, "step three must save progress for step four");
assert.doesNotMatch(page, /buildStepThreeAnswerPayload\("SUBMIT"/, "step three must not submit the task");
assert.match(api, /export function checkProductDevelopmentFactorsStepThree\(/, "the student API must expose the step-three validator");
assert.match(api, /product-development-factors\/step-3\/validate/, "the student API must call the step-three validation route");
assert.match(page, /v-else-if="currentStep === 3"/, "the fourth form must render only on step four");
assert.match(page, /产品成功的关键因素/, "step four must include success factors");
assert.match(page, /产品失败的关键因素/, "step four must include failure factors");
assert.match(page, /step4Answer/, "step four must save its own answer field");
assert.match(page, /await checkProductDevelopmentFactorsStepFour\(buildStepFourValidationPayload\(\)\)/, "step four must validate before submit");
assert.match(page, /buildStepFourAnswerPayload\("SUBMIT", 4\)/, "step four must submit the task");
assert.match(api, /product-development-factors\/step-4\/validate/, "the student API must call the step-four validation route");
console.log("product development factors step-one contract passed");

Loading…
Cancel
Save