diff --git a/docs/superpowers/plans/2026-08-07-new-product-survey-step2.md b/docs/superpowers/plans/2026-08-07-new-product-survey-step2.md new file mode 100644 index 0000000..5e03093 --- /dev/null +++ b/docs/superpowers/plans/2026-08-07-new-product-survey-step2.md @@ -0,0 +1,214 @@ +# 新产品调查与分析第二步 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:** 让学生完成新产品调查与分析的第二步,并在提交前由后端验证平台、场景、关键词、产品名和 1–5 张产品图片。 + +**Architecture:** 将 `new-product-survey.vue` 中旧的三行平台表格替换为单个调查表单,并将其 JSON 继续保存在现有 `StudentTrainingAnswer.step2Answer`。图片先沿用 `/common/upload` 上传,答案中只保存 `{ name, url }`;新增一个无状态的第二步校验服务与控制器入口,前端通过校验后才推进到第三步。 + +**Tech Stack:** Vue 3 Composition API、Element Plus、Vite、现有 RuoYi request 封装、Spring Boot、JUnit 5、Mockito。 + +## Global Constraints + +- 仅调整学生端 `new-product-survey` 与对应学生答案接口;不改教师端、管理端或其余步骤。 +- 案例资料按钮继续读取当前任务的 `materialUrl` 并下载,显示固定文案“下载案例资料”。 +- `step2Answer` 必须兼容既有答案存储与本地草稿,不新建图片表。 +- 第二步提交必须要求 1 至 5 张图片;选择“其他场景”时必须填写自定义场景。 +- 演示模式只做本地校验与会话保存,不调用上传或新增后端校验接口。 + +--- + +### Task 1: 后端第二步提交校验 + +**Files:** +- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step1/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepTwoValidationRequest.java` +- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step1/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepTwoValidationResult.java` +- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step1/src/main/java/com/sztzjy/linkCommerce/service/NewProductSurveyStepTwoService.java` +- Create: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step1/src/main/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepTwoServiceImpl.java` +- Modify: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step1/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java` +- Test: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step1/src/test/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepTwoServiceImplTest.java` +- Test: `E:/workspace/dianshang/link_commerce/.worktrees/new-product-survey-step1/src/test/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerControllerTest.java` + +**Interfaces:** +- Consumes: `JwtUser` and `POST /api/student-training-answers/new-product-survey/step-2/validate` request JSON. +- Produces: `NewProductSurveyStepTwoService.validate(NewProductSurveyStepTwoValidationRequest, JwtUser)` returning `{ valid: true, message: "校验通过" }` or a 4xx `ServiceException`. + +- [ ] **Step 1: Write failing service tests** + +```java +@Test +void validatesACompleteStudentSubmission() { + NewProductSurveyStepTwoValidationResult result = service.validate(request("京东", "AI儿童玩具", "", twoImages()), student()); + assertTrue(result.isValid()); +} + +@Test +void rejectsOtherSceneWithoutCustomSceneAndMoreThanFiveImages() { + assertThrows(ServiceException.class, () -> service.validate(request("京东", "其他场景", "", oneImage()), student())); + assertThrows(ServiceException.class, () -> service.validate(request("京东", "AI儿童玩具", "", sixImages()), student())); +} +``` + +- [ ] **Step 2: Run the service test to verify it fails** + +Run: `mvn -B '-Dtest=NewProductSurveyStepTwoServiceImplTest' test` + +Expected: FAIL because the step-two DTO and service do not exist. + +- [ ] **Step 3: Implement the DTO, service, and validation rules** + +```java +public interface NewProductSurveyStepTwoService { + NewProductSurveyStepTwoValidationResult validate(NewProductSurveyStepTwoValidationRequest request, JwtUser user); +} + +// validate roleId == 4; platform in 京东/天猫/抖音电商; scene is nonblank; +// customScene required only for 其他场景; keyword/productName nonblank; +// imageUrls contains 1..5 nonblank URLs. Throw ServiceException(BAD_REQUEST, message) on failure. +``` + +- [ ] **Step 4: Expose the controller endpoint and its test** + +```java +@PostMapping("/new-product-survey/step-2/validate") +public ResultEntity validateNewProductSurveyStepTwo( + @RequestBody NewProductSurveyStepTwoValidationRequest answer, HttpServletRequest request) { + return new ResultEntity<>(HttpStatus.OK, "校验通过", newProductSurveyStepTwoService.validate(answer, TokenProvider.getJWTUser(request))); +} +``` + +Mock the new service in `StudentTrainingAnswerControllerTest` and assert the controller passes the request and JWT user through, returning HTTP 200. + +- [ ] **Step 5: Run focused backend tests** + +Run: `mvn -B '-Dtest=NewProductSurveyStepTwoServiceImplTest,StudentTrainingAnswerControllerTest' test` + +Expected: PASS with zero failures. + +- [ ] **Step 6: Commit the backend implementation** + +```bash +git add src/main/java src/test/java +git commit -m "feat: validate new product survey step two" +``` + +### Task 2: 前端第二步表单、上传与答案回填 + +**Files:** +- Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/new-product-survey-step1/src/api/studentTrainingAnswer.js` +- Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/new-product-survey-step1/src/views/foundation/new-product-survey.vue` +- Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/new-product-survey-step1/tests/new-product-survey-step-one.static.test.cjs` + +**Interfaces:** +- Consumes: `uploadStudentTrainingFile(FormData)`, `checkNewProductSurveyStepTwo(form)` and the backend validation response. +- Produces: `step2Answer` JSON `{ platform, scene, customScene, keyword, productName, images: Array<{name, url}> }` and only advances the wizard after validation succeeds. + +- [ ] **Step 1: Write failing frontend static assertions** + +```js +assert.match(source, /AI智能家居[\s\S]*AI美妆辅助[\s\S]*其他场景/); +assert.match(source, /v-if="step2Form\.scene === '其他场景'"/); +assert.match(source, /MAX_STEP_TWO_IMAGES = 5/); +assert.match(source, /checkNewProductSurveyStepTwo/); +assert.match(source, /step2Answer: JSON\.stringify\(step2Form\.value\)/); +``` + +- [ ] **Step 2: Run the static test to verify it fails** + +Run: `node tests/new-product-survey-step-one.static.test.cjs` + +Expected: FAIL because the page still renders `step2Rows` and has no step-two validation client. + +- [ ] **Step 3: Add the API client and local demo validator** + +```js +export function checkNewProductSurveyStepTwo(form) { + if (isStudentDemo()) return Promise.resolve({ code: 200, data: validateStepTwoDemo(form) }); + return request({ + url: "/api/student-training-answers/new-product-survey/step-2/validate", + method: "post", + data: { ...form, imageUrls: form.images.map((image) => image.url) }, + headers: { repeatSubmit: false }, + }); +} +``` + +`validateStepTwoDemo` must apply the same required-field, other-scene, and 1–5 image count rules and reject invalid data with an `Error`. + +- [ ] **Step 4: Replace the old table with the second-step form** + +```vue + + {{ option.label }} + + + +``` + +Use constants for the three platform options and six scene options. Implement `uploadStepTwoImages`, `removeStepTwoImage`, and `previewStepTwoImage`: reject non-images and selections that would exceed five files; upload files with `FormData`; store only name and resolved URL; show thumbnails in a dialog-capable preview. Replace all `step2Rows` watch, reset, draft, save, outcome, and restoration paths with `step2Form`, while retaining safe fallback behavior for old row-array answers. + +- [ ] **Step 5: Gate next-step navigation on step-two validation** + +```js +if (currentStep.value === 2) { + await checkNewProductSurveyStepTwo(step2Form.value); + await saveAnswer("IN_PROGRESS", true); + advanceStep(); + return; +} +``` + +Show a human-readable validation/upload error and leave the user on step two on failure. Keep step-one grading and steps three/four navigation behavior unchanged. + +- [ ] **Step 6: Run frontend static 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: build exits with code 0; retain only the repository’s pre-existing warnings. + +- [ ] **Step 7: Commit the frontend implementation** + +```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: collect new product survey step two" +``` + +### Task 3: 全量验证与运行中的服务检查 + +**Files:** +- Modify: no source files expected. + +**Interfaces:** +- Consumes: committed frontend and backend worktree changes. +- Produces: evidence that the new APIs compile and the updated student page is available on the already-started local services. + +- [ ] **Step 1: Run the complete backend suite** + +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: Verify the running services** + +Run: `Invoke-WebRequest http://127.0.0.1:147/index.html -UseBasicParsing` and `Invoke-WebRequest http://127.0.0.1:7548/doc.html -UseBasicParsing`. + +Expected: both return HTTP 200. If the backend process predates the new commit, rebuild the backend JAR with `mvn -B package -DskipTests` and restart only the project JAR on port 7548. + +- [ ] **Step 4: Report verification and commits** + +Provide test/build outcomes, local URLs, and the two feature commit hashes. Do not merge the isolated worktree branches without an explicit user instruction. diff --git a/docs/superpowers/specs/2026-08-07-new-product-survey-step2-design.md b/docs/superpowers/specs/2026-08-07-new-product-survey-step2-design.md new file mode 100644 index 0000000..f7589db --- /dev/null +++ b/docs/superpowers/specs/2026-08-07-new-product-survey-step2-design.md @@ -0,0 +1,44 @@ +# 新产品调查与分析:第二步选定调查平台与产品 + +## 范围 + +仅调整学生端“新产品调查与分析”(`new-product-survey`)的第二步,以及支持该步骤提交校验的后端接口。保留当前任务配置的案例资料下载入口和下载行为;不调整教师端、管理端及其余实训步骤。 + +## 页面与交互 + +第二步标题为“选定调查平台与产品”。学生以消费者身份,通过搜索、浏览和对比采集一个真实 AI 产品的数据,并完成以下字段: + +1. 调查平台:必选单选项为京东、天猫、抖音电商,并展示原型中的平台说明。 +2. 调查场景:必选单选项为 AI 智能家居、AI 智能办公、AI 儿童玩具、AI 学习设备、AI 美妆辅助、其他场景;每项展示示例。选择“其他场景”后显示必填文本框,填写自定义场景名称。 +3. 搜索关键词:必填单行输入框,提示学生按“核心词 + 属性词 + 场景词”组织关键词,并展示原型示例。 +4. 产品名称:必填单行输入框。 +5. 产品图片:必传 1 至 5 张图片。上传完成后以缩略图显示;点击缩略图查看大图,学生可删除已上传的图片。超过 5 张、非图片文件或上传失败均显示明确提示,且不写入答案。 + +步骤导航继续使用既有四步名称。点击“下一步”时,先调用第二步校验接口;校验通过后保存答案并进入第三步,未通过时停留当前页并显示校验提示。普通“保存”仍可保存草稿,不强制所有字段完整。 + +## 数据持久化与接口 + +页面将一个对象序列化到既有学生答案的 `step2Answer` 字段: + +```json +{ + "platform": "京东", + "scene": "AI儿童玩具", + "customScene": "", + "keyword": "AI智能对话机器人 3-6岁 玩具", + "productName": "示例产品", + "images": [ + { "name": "product-main.png", "url": "/profile/upload/202608/...png" } + ] +} +``` + +图片继续上传到现有 `/common/upload`,只在答案 JSON 中保存原始文件名和服务端返回的 URL,不新增图片表。刷新或再次进入页面时回填表单和缩略图;旧版第二步表格 JSON 可以安全忽略,不阻止学生填写新版内容。 + +后端新增仅学生可调用的 `POST /api/student-training-answers/new-product-survey/step-2/validate`。请求包括平台、场景、自定义场景、关键词、产品名称和图片 URL 列表。接口校验:学生身份、平台与场景已选择、选择“其他场景”时自定义场景非空、关键词与产品名称非空、图片 URL 数量在 1 至 5 范围且每项非空。校验成功后返回成功结果;前端随后复用现有学生实训答案保存接口持久化 `step2Answer`。不新增标准答案或判题逻辑,因为本步骤采集的是学生自行选择的真实产品资料。 + +## 兼容性与验证 + +现有 `student_training_answer` 与步骤答案明细表继续承载 JSON,因此成绩、进度和导出链路保持兼容。演示模式沿用浏览器会话存储,并在本地执行同等的必填与图片数量校验,不发起上传或后端校验请求。 + +验证包括:后端服务与控制器的合法请求、缺失字段、其他场景未填写、图片数量越界和非学生权限测试;前端静态测试覆盖选择项、自定义场景条件渲染、上传数量限制、缩略图预览、数据保存与校验调用;再执行前端构建与后端测试。 diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js index 45d34bc..311f1ed 100644 --- a/src/api/studentTrainingAnswer.js +++ b/src/api/studentTrainingAnswer.js @@ -1,6 +1,20 @@ import request from "@/utils/request"; import { getDemoAnswerKey, getStudentDemoSession, isStudentDemo } from "@/utils/studentDemo"; +const newProductSurveyStepOneAnswers = { + "selling-point-1": "核心层", + "selling-point-2": "有形层", + "selling-point-3": "核心层", + "selling-point-4": "核心层", + "selling-point-5": "有形层", + "selling-point-6": "有形层", + "selling-point-7": "有形层", + "selling-point-8": "延伸层", + "selling-point-9": "延伸层", + "selling-point-10": "延伸层", + "selling-point-11": "有形层", +}; + function demoResult(taskKey) { try { return JSON.parse(sessionStorage.getItem(getDemoAnswerKey(taskKey)) || "null"); } catch (error) { return null; } } @@ -49,3 +63,74 @@ export function uploadStudentTrainingFile(data) { }, }); } + +export function checkNewProductSurveyStepOne(items) { + if (isStudentDemo()) { + const results = Array.isArray(items) + ? items.map((item) => { + const correct = newProductSurveyStepOneAnswers[item?.id] === item?.category; + return { + id: item?.id, + correct, + message: correct ? "分类正确" : "分类有误,请重新选择", + }; + }) + : []; + const correctCount = results.filter((item) => item.correct).length; + return Promise.resolve({ + code: 200, + data: { + items: results, + correctCount, + totalCount: results.length, + allCorrect: results.length === Object.keys(newProductSurveyStepOneAnswers).length && correctCount === results.length, + }, + }); + } + return request({ + url: "/api/student-training-answers/new-product-survey/step-1/check", + method: "post", + data: { items }, + headers: { repeatSubmit: false }, + }); +} + +function validateNewProductSurveyStepTwoDemo(form) { + const value = form || {}; + const images = Array.isArray(value.images) ? value.images : []; + if (!['京东', '天猫', '抖音电商'].includes(String(value.platform || '').trim())) { + throw new Error('请选择调查平台'); + } + if (!String(value.scene || '').trim()) { + throw new Error('请选择调查场景'); + } + if (value.scene === '其他场景' && !String(value.customScene || '').trim()) { + throw new Error('请填写自定义调查场景'); + } + if (!String(value.keyword || '').trim()) { + throw new Error('请填写搜索关键词'); + } + if (!String(value.productName || '').trim()) { + throw new Error('请填写产品名称'); + } + if (images.length < 1 || images.length > 5 || images.some((image) => !String(image?.url || '').trim())) { + throw new Error('请上传 1 至 5 张产品图片'); + } + return { valid: true, message: '校验通过' }; +} + +export function checkNewProductSurveyStepTwo(form) { + if (isStudentDemo()) { + try { + return Promise.resolve({ code: 200, data: validateNewProductSurveyStepTwoDemo(form) }); + } catch (error) { + return Promise.reject(error); + } + } + return request({ + url: '/api/student-training-answers/new-product-survey/step-2/validate', + method: 'post', + data: { ...form, imageUrls: (form?.images || []).map((image) => image?.url) }, + headers: { repeatSubmit: false }, + }); +} diff --git a/src/views/components/TrainingMaterialButton.vue b/src/views/components/TrainingMaterialButton.vue index 0f38c6a..99b4fb0 100644 --- a/src/views/components/TrainingMaterialButton.vue +++ b/src/views/components/TrainingMaterialButton.vue @@ -1,7 +1,7 @@ diff --git a/src/views/foundation/new-product-survey.vue b/src/views/foundation/new-product-survey.vue index 7f8e381..aa52aa4 100644 --- a/src/views/foundation/new-product-survey.vue +++ b/src/views/foundation/new-product-survey.vue @@ -50,60 +50,38 @@

Step 01

{{ getStepName(1) }}

- 案例信息提取 + 卖点分类练习 -

请根据实训要求中的案例一信息,分别从核心层、有形层、延伸层提取信息,并填写对应信息;

+

查看案例产品详情,仔细阅读主图及详情页信息,对下列产品卖点进行三层次分类。

- - -