merge: new product survey steps three and four

dev-QQq
chenyuan 3 weeks ago
commit 66cd95b13e

@ -0,0 +1,213 @@
# 新产品调查与分析第三步 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:** 前端将旧的三产品对比行转换成固定三层分析行,展示 AI 学习机参考并将第二步的产品名称作为分析列标题。学生输入以换行文本编辑、保存为 `rows[].points` 数组;后端提供无状态的完整度校验接口,答案本体继续由既有学生实训答案服务保存到 `step3Answer`
**Tech Stack:** Vue 3 Composition API、Element Plus、Vite、RuoYi request 封装、Spring Boot、JUnit 5、Mockito。
## Global Constraints
- 仅调整学生端 `new-product-survey` 的第三步与对应学生校验接口;不调整教师端、管理端、前两步或第四步。
- 保留当前任务配置的 `materialUrl` 下载入口和固定“下载案例资料”文案。
- `step3Answer` 必须继续使用已有学生答案表保存,不新增表。
- 核心层、有形层、延伸层每层至少两条非空、换行分隔的要点。
- 演示模式执行同一套本地校验,不调用新增后端接口。
---
### Task 1: 后端第三步完整度校验
**Files:**
- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepThreeValidationRequest.java`
- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepThreeValidationResult.java`
- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/service/NewProductSurveyStepThreeService.java`
- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepThreeServiceImpl.java`
- Modify: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java`
- Test: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/test/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepThreeServiceImplTest.java`
- Test: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/test/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerControllerTest.java`
**Interfaces:**
- Consumes: `POST /api/student-training-answers/new-product-survey/step-3/validate` with `rows: Array<{ dimension: String, points: List<String> }>` and `JwtUser`.
- Produces: `NewProductSurveyStepThreeService.validate(NewProductSurveyStepThreeValidationRequest, JwtUser)` that returns `{ valid: true, message: "校验通过" }` or a 4xx `ServiceException`.
- [ ] **Step 1: Write the failing service tests**
```java
@Test
void validatesThreeDimensionsWithTwoPointsEach() {
NewProductSurveyStepThreeValidationResult result = service.validate(completeRequest(), student());
assertTrue(result.isValid());
}
@Test
void rejectsAOnePointRowDuplicateDimensionAndNonStudent() {
assertThrows(ServiceException.class, () -> service.validate(request(row("核心层", "仅一条"), tangible(), extended()), student()));
assertThrows(ServiceException.class, () -> service.validate(request(core(), core(), extended()), student()));
assertThrows(ServiceException.class, () -> service.validate(completeRequest(), teacher()));
}
```
- [ ] **Step 2: Run the service test to verify it fails**
Run: `mvn -B '-Dtest=NewProductSurveyStepThreeServiceImplTest' test`
Expected: FAIL because the step-three request, result, and service do not exist.
- [ ] **Step 3: Implement the DTOs and service**
```java
public interface NewProductSurveyStepThreeService {
NewProductSurveyStepThreeValidationResult validate(NewProductSurveyStepThreeValidationRequest request, JwtUser user);
}
// Require roleId == 4; exactly one 核心层, 有形层, 延伸层 row;
// trim and discard blank points; each expected row needs at least two remaining points.
```
- [ ] **Step 4: Add the controller endpoint and controller test**
```java
@PostMapping("/new-product-survey/step-3/validate")
public ResultEntity<NewProductSurveyStepThreeValidationResult> validateNewProductSurveyStepThree(
@RequestBody NewProductSurveyStepThreeValidationRequest answer, HttpServletRequest request) {
return new ResultEntity<>(HttpStatus.OK, "校验通过", newProductSurveyStepThreeService.validate(answer, TokenProvider.getJWTUser(request)));
}
```
Mock the service only at the controller boundary; assert the real controller returns HTTP 200 with the validation result for an authenticated student request.
- [ ] **Step 5: Run focused backend tests**
Run: `mvn -B '-Dtest=NewProductSurveyStepThreeServiceImplTest,StudentTrainingAnswerControllerTest' test`
Expected: PASS with zero failures.
- [ ] **Step 6: Commit backend changes**
```bash
git add src/main/java src/test/java
git commit -m "feat: validate new product survey step three"
```
### Task 2: 学生端单产品分析表与持久化
**Files:**
- Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/new-product-survey-step3/src/api/studentTrainingAnswer.js`
- Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/new-product-survey-step3/src/views/foundation/new-product-survey.vue`
- Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/new-product-survey-step3/tests/new-product-survey-step-one.static.test.cjs`
**Interfaces:**
- Consumes: `checkNewProductSurveyStepThree({ rows })` and second-step `productName`.
- Produces: `step3Answer` JSON `{ rows: Array<{ dimension: String, points: String[] }> }`; advances to step four only after the third-step validation call resolves.
- [ ] **Step 1: Write failing frontend contract assertions**
```js
assert.match(page, /const STEP_THREE_REFERENCE_ROWS = \[/, "step three must retain the fixed AI learning-device reference");
assert.match(page, /step2Form\.productName \|\| '所选产品'/, "the analysis header must reflect the selected product");
assert.match(page, /countStepThreePoints\(row\.analysis\)/, "each row must show its entered-point count");
assert.match(page, /checkNewProductSurveyStepThree\(buildStepThreeValidationPayload\(\)\)/, "step three must validate before advancing");
assert.match(api, /new-product-survey\/step-3\/validate/, "the API must expose the step-three validation route");
```
- [ ] **Step 2: Run the frontend contract test to verify it fails**
Run: `node tests/new-product-survey-step-one.static.test.cjs`
Expected: FAIL because the page still uses `createStep3Rows()` as a three-product comparison table and the third-step API does not exist.
- [ ] **Step 3: Add the API client and demonstration-mode validator**
```js
export function checkNewProductSurveyStepThree(payload) {
if (isStudentDemo()) return Promise.resolve({ code: 200, data: validateNewProductSurveyStepThreeDemo(payload) });
return request({
url: '/api/student-training-answers/new-product-survey/step-3/validate',
method: 'post',
data: payload,
headers: { repeatSubmit: false },
});
}
```
The demo validator must require the same three dimensions and at least two trimmed points per row, rejecting invalid input with `Error`.
- [ ] **Step 4: Replace the comparison table with the prototype-aligned table**
```vue
<th>{{ step2Form.productName || '所选产品' }}分析</th>
<tr v-for="row in step3Rows" :key="row.dimension">
<td>{{ row.dimension }}<small>{{ row.definition }}</small></td>
<td><ol><li v-for="point in row.referencePoints" :key="point">{{ point }}</li></ol></td>
<td><textarea v-model="row.analysis" placeholder="每行填写一条要点,至少两条" /></td>
</tr>
```
Define `STEP_THREE_REFERENCE_ROWS` with the exact three dimensions, definitions, and AI 学习机 reference points from the approved spec. Add helpers: `splitStepThreePoints(value)`, `countStepThreePoints(value)`, `buildStepThreeValidationPayload()`, and a normalizer that converts old `product1`/`product2`/`product3` records into the new `analysis` text. Update local draft, answer restoration, reset, export outcome, and `buildAnswerPayload` to store the new points-array representation.
- [ ] **Step 5: Gate step-three navigation and show count guidance**
```js
if (currentStep.value === 3) {
await checkNewProductSurveyStepThree(buildStepThreeValidationPayload());
await saveAnswer('IN_PROGRESS', true);
advanceStep();
return;
}
```
Display “已填写 N / 至少 2 条” per row and leave the student on step three when the API or local validation rejects the data. Preserve the first- and second-step validation flows.
- [ ] **Step 6: Run frontend contract test and production build**
Run: `node tests/new-product-survey-step-one.static.test.cjs`
Expected: PASS.
Run: `node 'E:\workspace\dianshang\e-commerce-internet\node_modules\vite\bin\vite.js' build --config '.\vite.config.js'`
Expected: exit code 0, aside from existing repository warnings.
- [ ] **Step 7: Commit frontend changes**
```bash
git add src/api/studentTrainingAnswer.js src/views/foundation/new-product-survey.vue tests/new-product-survey-step-one.static.test.cjs
git commit -m "feat: analyze new product survey step three"
```
### Task 3: 完整验证与本地服务
**Files:**
- Modify: no source files expected.
**Interfaces:**
- Consumes: feature commits in both step-three worktrees.
- Produces: test, build, and runtime evidence for the student page and backend API.
- [ ] **Step 1: Run full backend tests**
Run: `mvn -B test`
Expected: all tests pass with zero failures.
- [ ] **Step 2: Run all standalone frontend tests**
Run:
```powershell
$tests = Get-ChildItem tests -File | Where-Object { $_.Name -like '*.test.cjs' -or $_.Name -like '*.test.mjs' }
foreach ($test in $tests) { node $test.FullName }
```
Expected: every test file exits 0.
- [ ] **Step 3: Start or refresh local services only on approval**
Run the frontend from its worktree using Vite on port 147. Build the backend JAR with `mvn -B package -DskipTests` and start it on port 7548 only if the user asks to start the backend; otherwise leave it stopped as requested previously.
- [ ] **Step 4: Report evidence and keep branches isolated**
Report test/build outcomes and commit hashes. Do not merge or stop/restart services unless the user explicitly chooses an integration or runtime action.

@ -0,0 +1,232 @@
# New Product Survey Step Four 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:** Deliver the student-only fourth step that collects three AI application-value analyses and blocks final task submission until all are filled.
**Architecture:** Keep the existing `StudentTrainingAnswer.step4Answer` JSON persistence and add a stateless student-only validation endpoint. The Vue page retains a local draft and calls the validation endpoint only on final submission; the demo client enforces the same non-empty rule locally.
**Tech Stack:** Vue 3 Composition API, Element Plus, Vite, Spring Boot, Java 8, JUnit 5, Mockito.
## Global Constraints
- Only change the student `new-product-survey` page and its supporting student validation API; do not change teacher, admin, table schema, prior steps, or material download behavior.
- Persist `{ coreValue, tangibleValue, extendedValue }` in the existing `step4Answer` field.
- A value is valid only when `trim()` leaves non-empty text; validation does not score analysis quality.
- Saving a draft remains allowed with empty fields; final `SUBMIT` is guarded by validation.
- Keep legacy fourth-step JSON restoration compatible.
- Do not start the backend automatically; the user starts it manually.
---
### Task 1: Add server-side fourth-step validation
**Files:**
- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepFourValidationRequest.java`
- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepFourValidationResult.java`
- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/service/NewProductSurveyStepFourService.java`
- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepFourServiceImpl.java`
- Modify: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java`
- Test: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/test/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepFourServiceImplTest.java`
- Test: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step3/src/test/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerControllerTest.java`
**Interfaces:**
- Consumes: authenticated `JwtUser` and `NewProductSurveyStepFourValidationRequest { String coreValue, String tangibleValue, String extendedValue }`.
- Produces: `NewProductSurveyStepFourValidationResult { boolean valid, String message }` from `POST /api/student-training-answers/new-product-survey/step-4/validate`.
- [ ] **Step 1: Write the failing service tests**
```java
@Test
void acceptsStudentWithAllThreeNonBlankValues() {
NewProductSurveyStepFourValidationRequest request = request("核心分析", "有形分析", "延伸分析");
assertTrue(service.validate(request, student()).isValid());
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t"})
void rejectsBlankCoreValue(String value) {
assertThrows(ServiceException.class, () -> service.validate(request(value, "有形", "延伸"), student()));
}
@Test
void rejectsNonStudent() {
assertThrows(ServiceException.class, () -> service.validate(request("核心", "有形", "延伸"), teacher()));
}
```
- [ ] **Step 2: Run the service test to verify it fails**
Run: `mvn -B '-Dtest=NewProductSurveyStepFourServiceImplTest' test`
Expected: FAIL because fourth-step request and service types do not exist.
- [ ] **Step 3: Implement DTOs and service**
```java
public interface NewProductSurveyStepFourService {
NewProductSurveyStepFourValidationResult validate(NewProductSurveyStepFourValidationRequest request, JwtUser user);
}
private void requireNonBlank(String value, String message) {
if (StringUtils.isBlank(value)) {
throw new ServiceException(HttpStatus.BAD_REQUEST, message);
}
}
```
Require the existing student role and user id, then require `coreValue`, `tangibleValue`, and `extendedValue` after trimming. Return `valid=true` and `校验通过` on success.
- [ ] **Step 4: Run the service test to verify it passes**
Run: `mvn -B '-Dtest=NewProductSurveyStepFourServiceImplTest' test`
Expected: PASS with nonblank, blank, and authorization behavior covered.
- [ ] **Step 5: Write the failing controller test**
```java
@Test
void validateStepFourUsesAuthenticatedStudentAndReturnsValidationResult() {
NewProductSurveyStepFourValidationResult result = new NewProductSurveyStepFourValidationResult(true, "校验通过");
when(newProductSurveyStepFourService.validate(any(), any())).thenReturn(result);
ResultEntity<NewProductSurveyStepFourValidationResult> response = controller
.validateNewProductSurveyStepFour(request("核心", "有形", "延伸"), authenticatedRequest());
assertEquals(HttpStatus.OK, response.getCode());
assertSame(result, response.getData());
}
```
- [ ] **Step 6: Run the controller test to verify it fails**
Run: `mvn -B '-Dtest=StudentTrainingAnswerControllerTest' test`
Expected: FAIL because `validateNewProductSurveyStepFour` does not exist.
- [ ] **Step 7: Add the controller endpoint**
```java
@PostMapping("/new-product-survey/step-4/validate")
public ResultEntity<NewProductSurveyStepFourValidationResult> validateNewProductSurveyStepFour(
@RequestBody NewProductSurveyStepFourValidationRequest answer, HttpServletRequest request) {
try {
return new ResultEntity<>(HttpStatus.OK, "校验通过",
newProductSurveyStepFourService.validate(answer, TokenProvider.getJWTUser(request)));
} catch (ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage());
}
}
```
- [ ] **Step 8: Run focused server tests**
Run: `mvn -B '-Dtest=NewProductSurveyStepFourServiceImplTest,StudentTrainingAnswerControllerTest' test`
Expected: PASS with fourth-step service behavior and endpoint delegation covered.
- [ ] **Step 9: Commit server validation**
```bash
git add src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepFourValidationRequest.java src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepFourValidationResult.java src/main/java/com/sztzjy/linkCommerce/service/NewProductSurveyStepFourService.java src/main/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepFourServiceImpl.java src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java src/test/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepFourServiceImplTest.java src/test/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerControllerTest.java
git commit -m "feat: validate new product survey step four"
```
### Task 2: Update the fourth-step student UI and submission flow
**Files:**
- Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/new-product-survey-step3/src/api/studentTrainingAnswer.js`
- Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/new-product-survey-step3/src/views/foundation/new-product-survey.vue`
- Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/new-product-survey-step3/tests/new-product-survey-step-one.static.test.cjs`
**Interfaces:**
- Consumes: `checkNewProductSurveyStepFour(payload)` returning `{ code: 200, data: { valid: true, message: "校验通过" } }`.
- Produces: guarded `submitTask()` that saves `step4Answer` only after successful validation.
- [ ] **Step 1: Add failing static source assertions**
```js
assert.match(page, /AI技术是否实现产品功能突破是否形成差异化卖点/, "step four must show the core-layer prototype question");
assert.match(page, /AI技术是否影响产品形态如产品外形包装、电商主图设计、参数配置、价格带定位。/, "step four must show the tangible-layer prototype question");
assert.match(page, /AI技术如何通过服务升级、内容更新提升用户粘性推动付费服务/, "step four must show the extended-layer prototype question");
assert.match(page, /checkNewProductSurveyStepFour\(step4Form\.value\)/, "step four must validate before submission");
assert.match(api, /new-product-survey\/step-4\/validate/, "the API must expose the step-four validation route");
```
- [ ] **Step 2: Run the static test to verify it fails**
Run: `& 'E:\Program Files\nodejs\node.exe' tests\new-product-survey-step-one.static.test.cjs`
Expected: FAIL because the fourth-step question copy and validation call are absent.
- [ ] **Step 3: Implement client validation and prototype-aligned form**
```js
function validateNewProductSurveyStepFourDemo(form) {
const value = form || {};
for (const [field, label] of [["coreValue", "核心层"], ["tangibleValue", "有形层"], ["extendedValue", "延伸层"]]) {
if (!String(value[field] || "").trim()) throw new Error(`请填写${label}AI应用价值分析`);
}
return { valid: true, message: "校验通过" };
}
async function submitTask() {
checking.value = true;
try {
await checkNewProductSurveyStepFour(step4Form.value);
await saveAnswer("SUBMITTED");
} catch (error) {
proxy?.$modal?.msgWarning(error?.message || "请完成三层次AI应用价值分析");
} finally {
checking.value = false;
}
}
```
Replace the existing generic field labels with the three prototype questions, keep their bindings as `coreValue`, `tangibleValue`, and `extendedValue`, and preserve draft saving and restoration.
- [ ] **Step 4: Run the static test to verify it passes**
Run: `& 'E:\Program Files\nodejs\node.exe' tests\new-product-survey-step-one.static.test.cjs`
Expected: PASS with prior step contracts still intact.
- [ ] **Step 5: Commit student UI and client validation**
```bash
git add src/api/studentTrainingAnswer.js src/views/foundation/new-product-survey.vue tests/new-product-survey-step-one.static.test.cjs
git commit -m "feat: add new product survey step four"
```
### Task 3: Run full verification
**Files:**
- Modify: none.
**Interfaces:**
- Consumes: both committed fourth-step implementations.
- Produces: verification evidence before integration; no application process is started.
- [ ] **Step 1: Run complete backend tests**
Run: `mvn -B test`
Expected: Maven reports `BUILD SUCCESS` and zero failures.
- [ ] **Step 2: Run the frontend source contract**
Run: `& 'E:\Program Files\nodejs\node.exe' tests\new-product-survey-step-one.static.test.cjs`
Expected: `new product survey step-one contract passed`.
- [ ] **Step 3: Run the production frontend build**
Run: `& 'E:\Program Files\nodejs\node.exe' 'E:\workspace\dianshang\e-commerce-internet\node_modules\vite\bin\vite.js' build`
Expected: Vite exits with code 0. Existing `::v-deep`, unresolved `Navbar.vue` top1 asset, and large-chunk warnings may remain.
- [ ] **Step 4: Inspect repository state**
Run: `git status --short; git log --oneline --max-count=5`
Expected: only the intended fourth-step commits are present; do not start the backend.

@ -0,0 +1,41 @@
# 新产品调查与分析:第三步 AI 产品三层次分析
## 范围
仅调整学生端“新产品调查与分析”(`new-product-survey`)的第三步,以及支持该步骤提交校验的后端接口。保留前两步、案例资料下载、教师端、管理端和第四步既有行为。
## 页面与交互
第三步标题为“AI 产品三层次分析”。页面说明学生应基于第二步采集的真实数据,运用产品三层次理论进行深度分析。
页面使用三列表格:
1. 分析维度固定三行分别为核心层AI 技术解决的核心需求/功能)、有形层(外观/材质/参数/价格策略)、延伸层(软件升级/专属服务/社群运营)。
2. 实例AI 学习机:固定显示原型中的参考要点:核心层为“大模型作业批改、 自适应学习路径规划”;有形层为“护眼屏+铝合金机身、搭载 NPU 芯片、全平台统一定价 2999 元/直播间专属价 2699 元”延伸层为“终身免费题库更新、VIP 学科规划师 1 对 1、家长互助社群及资料共享微信群”。
3. 产品分析:针对学生在第二步选择的产品填写。表头显示第二步产品名称,未填写时显示“所选产品”。每行均为多行输入框,按换行录入要点。
核心层、有形层、延伸层各自至少需要两条非空要点。页面显示该行已填写的要点数量和“至少 2 条”的提示;不满足时保持当前页并显示可理解的错误信息。草稿保存不强制完成;点击“下一步”校验通过后保存并进入第四步。
## 数据与接口
第三步继续写入既有 `StudentTrainingAnswer.step3Answer`,采用以下 JSON 结构:
```json
{
"rows": [
{ "dimension": "核心层", "points": ["要点一", "要点二"] },
{ "dimension": "有形层", "points": ["要点一", "要点二"] },
{ "dimension": "延伸层", "points": ["要点一", "要点二"] }
]
}
```
前端输入仍以每行的多行文本编辑,保存时由换行规范化为 `points`;回填时将要点重新换行展示。对于此前版本保存的 `product1`、`product2`、`product3` 表格答案,页面只取其中第一个非空产品列并转换为当前行文本,保证已保存草稿可继续编辑。
后端新增仅学生可调用的 `POST /api/student-training-answers/new-product-survey/step-3/validate`。请求传入三行 `{ dimension, points }`。接口校验学生身份、三层次行标识完整且无重复、每行至少两条去空白后的要点、每条要点非空;校验成功返回 `{ valid: true, message: "校验通过" }`。该接口只校验填写完整度,不判定学生对其自主选择产品的分析内容是否正确,不新增答案表。
演示模式不调用后端,使用同样的本地校验规则;图片和第二步答案继续沿用既有会话存储与保存逻辑。
## 验证
后端测试覆盖合法三层分析、少于两条要点、维度缺失或重复、非学生权限和控制器调用。前端测试覆盖固定参考内容、产品名称回显、多行要点数量提示、步骤答案序列化与第 3 步校验调用;完成后执行全量后端测试、前端测试和 Vite 构建。

@ -0,0 +1,35 @@
# 新产品调查与分析:第四步 AI 应用价值分析
## 范围
仅调整学生端“新产品调查与分析”(`new-product-survey`)的第四步,以及支持最终提交完整度校验的学生接口。保留前 1 至 3 步、案例资料下载、教师端、管理端和既有答案表结构。
## 页面与交互
第四步标题为“AI应用价值分析”。页面按原型展示说明“结合上面实训针对AI技术在产品中的应用价值进行分析回答以下问题。”随后依次提供三个多行文本框
1. 核心层AI技术是否实现产品功能突破是否形成差异化卖点
2. 有形层AI技术是否影响产品形态如产品外形包装、电商主图设计、参数配置、价格带定位。
3. 延伸层AI技术如何通过服务升级、内容更新提升用户粘性推动付费服务
学生可随时保存草稿,草稿不强制填写。点击“提交任务”时,前端去除首尾空白后检查三项均非空;任一为空则阻止提交并提示缺少的层级。通过本地校验后调用后端校验,后端确认通过才复用既有答案保存接口以 `SUBMIT` 方式提交。
## 数据与接口
答案继续保存在既有 `StudentTrainingAnswer.step4Answer`,结构保持兼容:
```json
{
"coreValue": "核心层分析",
"tangibleValue": "有形层分析",
"extendedValue": "延伸层分析"
}
```
后端新增仅学生可调用的 `POST /api/student-training-answers/new-product-survey/step-4/validate`。请求使用同一结构,校验学生身份及三项文本去首尾空白后均非空;成功返回 `{ "valid": true, "message": "校验通过" }`。该接口不评判分析内容,不新增表或标准答案。
演示模式不调用后端,执行同样的本地规则。旧版已保存的第 4 步 JSON 继续由现有兼容逻辑读取。
## 验证
后端测试覆盖三项均填写、任一缺失和非学生访问,以及控制器透传。前端测试覆盖原型问题文案、提交前调用第四步校验接口与 `step4Answer` 的持久化;完成后运行完整 Maven 测试、前端静态测试和 Vite 生产构建。

@ -134,3 +134,70 @@ export function checkNewProductSurveyStepTwo(form) {
headers: { repeatSubmit: false },
});
}
function validateNewProductSurveyStepThreeDemo(payload) {
const expectedDimensions = ["核心层", "有形层", "延伸层"];
const rows = Array.isArray(payload?.rows) ? payload.rows : [];
if (rows.length !== expectedDimensions.length) {
throw new Error("请完成核心层、有形层、延伸层分析");
}
const dimensions = rows.map((row) => String(row?.dimension || "").trim());
if (new Set(dimensions).size !== expectedDimensions.length || expectedDimensions.some((dimension) => !dimensions.includes(dimension))) {
throw new Error("分析维度不完整或重复");
}
const invalidRow = rows.find((row) => {
const points = Array.isArray(row?.points) ? row.points : [];
return points.map((point) => String(point || "").trim()).filter(Boolean).length < 2;
});
if (invalidRow) {
throw new Error(`${invalidRow.dimension || "该层"}至少填写 2 条分析要点`);
}
return { valid: true, message: "校验通过" };
}
export function checkNewProductSurveyStepThree(payload) {
if (isStudentDemo()) {
try {
return Promise.resolve({ code: 200, data: validateNewProductSurveyStepThreeDemo(payload) });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/new-product-survey/step-3/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
function validateNewProductSurveyStepFourDemo(form) {
const value = form || {};
const fields = [
["coreValue", "核心层"],
["tangibleValue", "有形层"],
["extendedValue", "延伸层"],
];
for (const [field, label] of fields) {
if (!String(value[field] || "").trim()) {
throw new Error(`请填写${label}AI应用价值分析`);
}
}
return { valid: true, message: "校验通过" };
}
export function checkNewProductSurveyStepFour(form) {
if (isStudentDemo()) {
try {
return Promise.resolve({ code: 200, data: validateNewProductSurveyStepFourDemo(form) });
} catch (error) {
return Promise.reject(error);
}
}
return request({
url: "/api/student-training-answers/new-product-survey/step-4/validate",
method: "post",
data: form,
headers: { repeatSubmit: false },
});
}

@ -154,10 +154,10 @@
<p class="section-eyebrow">Step 03</p>
<h3>{{ getStepName(3) }}</h3>
</div>
<span class="step-chip">产品1 / 产品2 / 产品3</span>
<span class="step-chip">三层次对照分析</span>
</div>
<p class="step-desc">
根据查找的产品从核心层有形层延伸层开展三层次分析对比不同产品如何创造用户价值
基于上一步采集的真实数据运用三层次理论进行深度分析参考示例填写下表每层至少填写 2 条要点并按换行分条
</p>
<div class="training-table-wrap">
@ -165,21 +165,22 @@
<thead>
<tr>
<th>分析维度</th>
<th>内容要求</th>
<th>实例AI学习机</th>
<th>产品1</th>
<th>产品2</th>
<th>产品3</th>
<th>{{ step2Form.productName || '所选产品' }}分析</th>
</tr>
</thead>
<tbody>
<tr v-for="row in step3Rows" :key="row.dimension">
<td class="dimension-cell">{{ row.dimension }}</td>
<td class="requirement-cell">{{ row.requirement }}</td>
<td class="example-cell">{{ row.example }}</td>
<td><textarea v-model="row.product1" class="cell-textarea" /></td>
<td><textarea v-model="row.product2" class="cell-textarea" /></td>
<td><textarea v-model="row.product3" class="cell-textarea" /></td>
<td class="dimension-cell"><strong>{{ row.dimension }}</strong><span>{{ row.definition }}</span></td>
<td class="example-cell">
<ol class="reference-points">
<li v-for="point in row.referencePoints" :key="point">{{ point }}</li>
</ol>
</td>
<td class="analysis-input-cell">
<textarea v-model="row.analysis" class="cell-textarea" rows="7" placeholder="每行填写一条分析要点" />
<p class="analysis-count" :class="{ 'is-ready': countStepThreePoints(row.analysis) >= 2 }">已填写 {{ countStepThreePoints(row.analysis) }} / 至少 2 </p>
</td>
</tr>
</tbody>
</table>
@ -196,19 +197,19 @@
</div>
<span class="step-chip">价值判断</span>
</div>
<p class="step-desc">根据上面分析围绕核心层有形层延伸层回答以下问题并形成产品开发启发</p>
<p class="step-desc">结合上面实训针对AI技术在产品中的应用价值进行分析回答以下问题</p>
<label class="field-block">
<span>核心层价值AI 技术解决了用户什么真实问题</span>
<textarea v-model="step4Form.coreValue" class="input-field" rows="5" />
<span><b>1核心层</b>AI技术是否实现产品功能突破是否形成差异化卖点</span>
<textarea v-model="step4Form.coreValue" class="input-field step4-input" rows="7" placeholder="请填写核心层AI应用价值分析" />
</label>
<label class="field-block">
<span>有形层价值哪些功能设计或参数最能体现差异化</span>
<textarea v-model="step4Form.tangibleValue" class="input-field" rows="5" />
<span><b>2有形层</b>AI技术是否影响产品形态如产品外形包装电商主图设计参数配置价格带定位</span>
<textarea v-model="step4Form.tangibleValue" class="input-field step4-input" rows="7" placeholder="请填写有形层AI应用价值分析" />
</label>
<label class="field-block">
<span>延伸层价值哪些服务内容生态或售后能增强竞争优势</span>
<textarea v-model="step4Form.extendedValue" class="input-field" rows="5" />
<span><b>3延伸层</b>AI技术如何通过服务升级内容更新提升用户粘性推动付费服务</span>
<textarea v-model="step4Form.extendedValue" class="input-field step4-input" rows="7" placeholder="请填写延伸层AI应用价值分析" />
</label>
</div>
</section>
@ -229,9 +230,9 @@
</div>
<div class="task-actions">
<button type="button" class="btn-nav btn-submit" :disabled="saving" @click="submitTask">
<button type="button" class="btn-nav btn-submit" :disabled="saving || checking" @click="submitTask">
<el-icon><CircleCheck /></el-icon>
提交任务
{{ checking ? "正在提交" : "提交任务" }}
</button>
<button type="button" class="btn-nav" :disabled="saving" @click="resetExample">
<el-icon><RefreshLeft /></el-icon>
@ -269,7 +270,7 @@ import {
TrendCharts,
} from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { checkNewProductSurveyStepOne, checkNewProductSurveyStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { checkNewProductSurveyStepFour, checkNewProductSurveyStepOne, checkNewProductSurveyStepThree, checkNewProductSurveyStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import useUserStore from "@/store/modules/user";
import { extractTrainingStepNames } from "@/views/training/taskKeyMap";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
@ -357,33 +358,14 @@ const STEP_ONE_SELLING_POINTS = [
const createStep2Form = () => ({ platform: "", scene: "", customScene: "", keyword: "", productName: "", images: [] });
const createStep3Rows = () => [
{
dimension: "核心层",
requirement: "说明产品解决的核心痛点、用户需求和核心利益。",
example: "解决学生学习效率低、家长辅导时间不足等痛点,提供个性化学习支持。",
product1: "",
product2: "",
product3: "",
},
{
dimension: "有形层",
requirement: "记录功能、设计、品牌、参数、包装、价格等可见要素。",
example: "护眼屏、AI语音互动、拍照搜题、错题本、课程资源、学习报告。",
product1: "",
product2: "",
product3: "",
},
{
dimension: "延伸层",
requirement: "分析售后、会员、内容更新、物流、生态兼容和数据安全等附加服务。",
example: "课程持续更新、家长端管理、云端错题同步、售后保修、会员内容权益。",
product1: "",
product2: "",
product3: "",
},
const STEP_THREE_REFERENCE_ROWS = [
{ dimension: "核心层", definition: "AI技术解决的核心需求/功能", referencePoints: ["大模型作业批改", "自适应学习路径规划"] },
{ dimension: "有形层", definition: "外观/材质/参数/价格策略", referencePoints: ["护眼屏+铝合金机身", "搭载NPU芯片", "全平台统一定价2999元某网红直播间专属价2699元"] },
{ dimension: "延伸层", definition: "软件升级/专属服务/社群运营", referencePoints: ["终身免费题库更新", "VIP学科规划师1对1", "家长互助社群及资料共享微信群"] },
];
const createStep3Rows = () => STEP_THREE_REFERENCE_ROWS.map((row) => ({ ...row, analysis: "" }));
const createStep4Form = () => ({
coreValue: "",
tangibleValue: "",
@ -406,7 +388,7 @@ const currentStudyTip = computed(() => {
const tips = {
1: "当前步骤建议:先从案例或电商详情页中拆出核心层、有形层、延伸层,不要把功能和用户利益混在一起。",
2: "当前步骤建议:选择一个主流电商平台与真实 AI 产品,用“核心词 + 属性词 + 场景词”搜索并保留主图、详情页截图。",
3: "当前步骤建议:同一维度横向比较 3 个产品,重点找出 AI 技术在哪一层创造了差异化。",
3: "当前步骤建议:将上一步选定产品的卖点按核心层、有形层、延伸层拆分,每层至少记录两条可核对的分析要点。",
4: "当前步骤建议:把前面三步的发现收束为产品开发启发,说明哪些价值值得继续强化。",
};
return tips[currentStep.value];
@ -623,7 +605,19 @@ function applyStep3Answer(value) {
const parsed = safeParse(value);
const rows = Array.isArray(parsed) ? parsed : parsed?.step3Rows || parsed?.rows || parsed?.layerRows;
if (!Array.isArray(rows) || !rows.length) return false;
step3Rows.value = normalizeRows(rows, createStep3Rows(), "dimension");
const savedByDimension = rows.reduce((result, row) => {
if (row?.dimension) result[row.dimension] = row;
return result;
}, {});
step3Rows.value = createStep3Rows().map((row, index) => {
const saved = savedByDimension[row.dimension] || rows[index] || {};
const points = Array.isArray(saved.points) ? saved.points : [];
const legacyValue = [saved.analysis, saved.product1, saved.product2, saved.product3].find((item) => String(item || "").trim());
return {
...row,
analysis: points.length ? points.map((item) => String(item || "").trim()).filter(Boolean).join("\n") : String(legacyValue || ""),
};
});
return true;
}
@ -667,6 +661,26 @@ function normalizeRows(rows, fallback, matchKey = "") {
}));
}
function splitStepThreePoints(value) {
return String(value || "")
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
}
function countStepThreePoints(value) {
return splitStepThreePoints(value).length;
}
function buildStepThreeValidationPayload() {
return {
rows: step3Rows.value.map((row) => ({
dimension: row.dimension,
points: splitStepThreePoints(row.analysis),
})),
};
}
function buildAnswerPayload(status) {
const saveAction = status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE";
return {
@ -674,7 +688,7 @@ function buildAnswerPayload(status) {
currentStep: currentStep.value,
step1Answer: JSON.stringify({ classifications: classifications.value, checkResult: checkResult.value }),
step2Answer: JSON.stringify(step2Form.value),
step3Answer: JSON.stringify(step3Rows.value),
step3Answer: JSON.stringify(buildStepThreeValidationPayload()),
step4Answer: JSON.stringify(step4Form.value),
};
}
@ -695,8 +709,16 @@ function saveCurrentProgress() {
return saveAnswer("IN_PROGRESS");
}
function submitTask() {
return saveAnswer("SUBMITTED");
async function submitTask() {
checking.value = true;
try {
await checkNewProductSurveyStepFour(step4Form.value);
await saveAnswer("SUBMITTED");
} catch (error) {
proxy?.$modal?.msgWarning(error?.message || "请完成三层次AI应用价值分析");
} finally {
checking.value = false;
}
}
async function nextStep() {
@ -714,6 +736,20 @@ async function nextStep() {
}
return;
}
if (currentStep.value === 3) {
checking.value = true;
try {
await checkNewProductSurveyStepThree(buildStepThreeValidationPayload());
await saveAnswer("IN_PROGRESS", true);
proxy?.$modal?.msgSuccess("第三步分析已校验并保存");
advanceStep();
} catch (error) {
proxy?.$modal?.msgWarning(error?.message || "请确保每个层次至少填写 2 条分析要点");
} finally {
checking.value = false;
}
return;
}
if (currentStep.value !== 1) {
advanceStep();
return;
@ -1243,6 +1279,10 @@ function exportOutcome() {
line-height: 1.65;
}
.step4-input {
min-height: 168px;
}
.input-single {
min-height: 42px;
padding: 0 14px;
@ -1513,9 +1553,26 @@ function exportOutcome() {
}
.dimension-cell {
min-width: 170px;
color: #eafaff;
font-weight: 900;
text-align: center;
text-align: left;
strong,
span {
display: block;
}
strong {
margin-bottom: 7px;
font-weight: 900;
}
span {
color: #9fc6da;
font-size: 12px;
font-weight: 700;
line-height: 1.55;
}
}
.requirement-cell,
@ -1529,6 +1586,28 @@ function exportOutcome() {
color: #f5d99d;
}
.reference-points {
display: grid;
gap: 8px;
margin: 0;
padding-left: 20px;
}
.analysis-input-cell {
min-width: 280px;
}
.analysis-count {
margin: 8px 2px 0;
color: #ffb1b1;
font-size: 12px;
font-weight: 700;
&.is-ready {
color: #86e7bd;
}
}
.example-hint {
display: flex;
gap: 8px;

@ -24,6 +24,16 @@ assert.match(page, /@click="previewStepTwoImage\(image\.url\)"/, "uploaded produ
assert.match(page, /step2Answer: JSON\.stringify\(step2Form\.value\)/, "step two must persist the survey form instead of table rows");
assert.match(page, /checkNewProductSurveyStepTwo\(step2Form\.value\)/, "step two must request server-side validation before advancing");
assert.match(api, /new-product-survey\/step-2\/validate/, "the student API must call the step-two validation endpoint");
assert.match(page, /const STEP_THREE_REFERENCE_ROWS = \[/, "step three must define the fixed AI learning-machine reference rows");
assert.match(page, /step2Form\.productName \|\| '所选产品'/, "step three must use the surveyed product name as its analysis-column heading");
assert.match(page, /countStepThreePoints\(row\.analysis\)/, "step three must show the number of entered analysis points");
assert.match(page, /checkNewProductSurveyStepThree\(buildStepThreeValidationPayload\(\)\)/, "step three must validate its rows before advancing");
assert.match(api, /new-product-survey\/step-3\/validate/, "the student API must call the step-three validation endpoint");
assert.match(page, /AI技术是否实现产品功能突破是否形成差异化卖点/, "step four must show the core-layer prototype question");
assert.match(page, /AI技术是否影响产品形态如产品外形包装、电商主图设计、参数配置、价格带定位。/, "step four must show the tangible-layer prototype question");
assert.match(page, /AI技术如何通过服务升级、内容更新提升用户粘性推动付费服务/, "step four must show the extended-layer prototype question");
assert.match(page, /checkNewProductSurveyStepFour\(step4Form\.value\)/, "step four must validate before submission");
assert.match(api, /new-product-survey\/step-4\/validate/, "the student API must call the step-four validation endpoint");
assert.match(materialButton, /下载案例资料/, "the download button must use a fixed student-facing label");
assert.doesNotMatch(materialButton, /\{\{ material\?\.name \|\| "案例文档" \}\}/, "the upload file name must not become button text");

Loading…
Cancel
Save