From 7e7ad3c1b1cd91a6d1a28864b3d4c3f3d5d16ba2 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Fri, 7 Aug 2026 10:34:14 +0800 Subject: [PATCH] docs: plan new product survey step one --- .../2026-08-07-new-product-survey-step1.md | 371 ++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-07-new-product-survey-step1.md diff --git a/docs/superpowers/plans/2026-08-07-new-product-survey-step1.md b/docs/superpowers/plans/2026-08-07-new-product-survey-step1.md new file mode 100644 index 0000000..a1c6b5b --- /dev/null +++ b/docs/superpowers/plans/2026-08-07-new-product-survey-step1.md @@ -0,0 +1,371 @@ +# 新产品调查与分析第一步 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 first step for new-product-survey: 11 product-selling-point classifications, server-side grading against persisted answers, feedback, and saved progress. + +**Architecture:** The Java service owns the canonical answer JSON in a new idempotently-created MySQL table and exposes a student-authenticated grading endpoint that never returns correct categories. The Vue page renders the fixed question list, sends only student selections for grading, saves the selections plus grading result through the existing answer API, and advances only after all entries are correct. + +**Tech Stack:** Vue 3 Composition API, Element Plus, Vite 3, Spring Boot 2.7, Java 8, MyBatis/JdbcTemplate, JUnit 5, Node assert static contract tests. + +## Global Constraints + +- Modify only the student new-product-survey page and its supporting student API/service/data; do not modify teacher or administration pages. +- Keep TrainingMaterialButton and its existing materialUrl download behavior unchanged. +- The correct category must be stored only in the backend standard-answer table and must never be returned by the grading endpoint. +- Keep the existing student answer and step-progress persistence model; store first-step selections and feedback JSON in step1Answer. +- In student demo mode, preserve the current session-storage behavior and grade with the same local question metadata. + +--- + +## File Structure + +- link_commerce/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepOneCheckRequest.java: request DTO containing 11 id/category selections. +- link_commerce/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepOneCheckResult.java: response DTO containing per-item correctness, aggregate counts, and completion flag without the correct category. +- link_commerce/src/main/java/com/sztzjy/linkCommerce/service/NewProductSurveyStepOneService.java: student grading service contract. +- link_commerce/src/main/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepOneServiceImpl.java: creates/seeds the standard-answer table and validates/grades requests. +- link_commerce/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java: hosts the new student-only grading route. +- link_commerce/src/test/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepOneServiceImplTest.java: service grading and initialization tests. +- e-commerce-internet/src/api/studentTrainingAnswer.js: client wrapper for the grading route and demo fallback. +- e-commerce-internet/src/views/foundation/new-product-survey.vue: first-step table, feedback, persistence, and guarded navigation. +- e-commerce-internet/tests/new-product-survey-step-one.static.test.cjs: source contract for the UI and client request. + +### Task 1: Persist and grade the canonical answers + +**Files:** + +- Create: link_commerce/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepOneCheckRequest.java +- Create: link_commerce/src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepOneCheckResult.java +- Create: link_commerce/src/main/java/com/sztzjy/linkCommerce/service/NewProductSurveyStepOneService.java +- Create: link_commerce/src/main/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepOneServiceImpl.java +- Test: link_commerce/src/test/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepOneServiceImplTest.java + +**Interfaces:** + +- Consumes: JwtUser, ServiceException, JdbcTemplate, and existing student-service conventions. +- Produces: NewProductSurveyStepOneService.check(NewProductSurveyStepOneCheckRequest request, JwtUser user), returning a result with items, correctCount, totalCount, and allCorrect. + +- [ ] **Step 1: Write failing service tests** + +~~~java +@Test +void checkMarksEachSelectionWithoutExposingCorrectCategory() { + NewProductSurveyStepOneCheckResult result = serviceWithStandardAnswers() + .check(request(item("selling-point-1", "核心层"), item("selling-point-2", "核心层")), student()); + + assertEquals(1, result.getCorrectCount()); + assertFalse(result.getAllCorrect()); + assertTrue(result.getItems().get(0).getCorrect()); + assertFalse(result.getItems().get(1).getCorrect()); + assertNull(result.getItems().get(1).getCorrectCategory()); +} + +@Test +void checkRejectsUnknownItemsInvalidCategoriesAndNonStudents() { + assertThrows(ServiceException.class, () -> service.check(request(item("unknown", "核心层")), student())); + assertThrows(ServiceException.class, () -> service.check(fullRequest("错误层级"), student())); + assertThrows(ServiceException.class, () -> service.check(fullRequest("核心层"), teacher())); +} + +@Test +void initializationUsesInsertIgnoreAndNeverOverwritesConfiguredAnswer() { + JdbcTemplate jdbc = mock(JdbcTemplate.class); + new NewProductSurveyStepOneServiceImpl(jdbc).initializeStandardAnswers(); + verify(jdbc).execute(contains("CREATE TABLE IF NOT EXISTS training_step_standard_answer")); + verify(jdbc).update(startsWith("INSERT IGNORE INTO training_step_standard_answer"), any(), any(), any(), any()); +} +~~~ + +- [ ] **Step 2: Verify the new test fails** + +Run: .\mvnw.cmd -Dtest=NewProductSurveyStepOneServiceImplTest test + +Expected: FAIL because the new service and DTO types do not exist. + +- [ ] **Step 3: Add the DTOs and service contract** + +~~~java +public class NewProductSurveyStepOneCheckRequest { + private List items; + public static class Item { private String id; private String category; } +} + +public class NewProductSurveyStepOneCheckResult { + private List items; + private int correctCount; + private int totalCount; + private boolean allCorrect; + public static class ItemResult { private String id; private boolean correct; private String message; } +} + +public interface NewProductSurveyStepOneService { + NewProductSurveyStepOneCheckResult check(NewProductSurveyStepOneCheckRequest request, JwtUser user); +} +~~~ + +- [ ] **Step 4: Implement table initialization, seed data, and grading** + +~~~java +private static final String TASK_KEY = "new-product-survey"; +private static final int STEP_NO = 1; + +@PostConstruct +public void initializeStandardAnswers() { + jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS training_step_standard_answer (... UNIQUE KEY uk_training_step_standard_answer (task_key, step_no)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + jdbcTemplate.update("INSERT IGNORE INTO training_step_standard_answer (id, task_key, step_no, answer_json, create_time, update_time) VALUES (?, ?, ?, ?, NOW(), NOW())", + "new-product-survey-step-1", TASK_KEY, STEP_NO, JSON.toJSONString(DEFAULT_ANSWERS)); +} + +public NewProductSurveyStepOneCheckResult check(NewProductSurveyStepOneCheckRequest request, JwtUser user) { + requireStudent(user); + Map submitted = validateCompleteSelections(request.getItems()); + return compare(submitted, loadStandardAnswers()); +} +~~~ + +Use the following IDs and categories in DEFAULT_ANSWERS: 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 有形层. Require exactly these 11 IDs once each and categories only from 核心层、有形层、延伸层. Return only id, correct, and message per item; use 分类正确 and 分类有误,请重新选择. + +- [ ] **Step 5: Verify the service test passes** + +Run: .\mvnw.cmd -Dtest=NewProductSurveyStepOneServiceImplTest test + +Expected: PASS. + +- [ ] **Step 6: Commit Task 1** + +~~~powershell +git add -- src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepOneCheckRequest.java src/main/java/com/sztzjy/linkCommerce/entity/dto/NewProductSurveyStepOneCheckResult.java src/main/java/com/sztzjy/linkCommerce/service/NewProductSurveyStepOneService.java src/main/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepOneServiceImpl.java src/test/java/com/sztzjy/linkCommerce/service/impl/NewProductSurveyStepOneServiceImplTest.java +git commit -m "feat: grade new product survey step one" +~~~ + +### Task 2: Expose the student-only grading route + +**Files:** + +- Modify: link_commerce/src/main/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerController.java +- Test: link_commerce/src/test/java/com/sztzjy/linkCommerce/controller/stu/StudentTrainingAnswerControllerTest.java + +**Interfaces:** + +- Consumes: NewProductSurveyStepOneService.check from Task 1. +- Produces: POST /api/student-training-answers/new-product-survey/step-1/check returning ResultEntity of NewProductSurveyStepOneCheckResult. + +- [ ] **Step 1: Write a failing controller test** + +~~~java +@Test +void checkNewProductSurveyStepOneDelegatesForTheAuthenticatedStudent() { + when(TokenProvider.getJWTUser(request)).thenReturn(student()); + when(stepOneService.check(any(), any())).thenReturn(result()); + + ResultEntity response = + controller.checkNewProductSurveyStepOne(requestBody(), request); + + assertEquals(HttpStatus.OK, response.getCode()); + verify(stepOneService).check(requestBody(), student()); +} +~~~ + +- [ ] **Step 2: Verify the controller test fails** + +Run: .\mvnw.cmd -Dtest=StudentTrainingAnswerControllerTest test + +Expected: FAIL because the endpoint and injected service do not exist. + +- [ ] **Step 3: Add the route and service-error mapping** + +~~~java +@PostMapping("/new-product-survey/step-1/check") +@ApiOperation("新产品调查与分析第一步判题") +public ResultEntity checkNewProductSurveyStepOne( + @RequestBody NewProductSurveyStepOneCheckRequest answer, HttpServletRequest request) { + try { + JwtUser user = TokenProvider.getJWTUser(request); + return new ResultEntity<>(HttpStatus.OK, "判题完成", newProductSurveyStepOneService.check(answer, user)); + } catch (ServiceException e) { + return new ResultEntity<>(e.getCode(), e.getMessage()); + } +} +~~~ + +- [ ] **Step 4: Verify both backend test classes pass** + +Run: .\mvnw.cmd -Dtest=NewProductSurveyStepOneServiceImplTest,StudentTrainingAnswerControllerTest test + +Expected: PASS. + +- [ ] **Step 5: Commit Task 2** + +~~~powershell +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 new product survey grading api" +~~~ + +### Task 3: Render and persist the student classification step + +**Files:** + +- Modify: e-commerce-internet/src/api/studentTrainingAnswer.js +- Modify: e-commerce-internet/src/views/foundation/new-product-survey.vue +- Test: e-commerce-internet/tests/new-product-survey-step-one.static.test.cjs + +**Interfaces:** + +- Consumes: the Task 2 grading route. +- Produces: checkNewProductSurveyStepOne(items) returning items, correctCount, totalCount, allCorrect; first-step JSON { classifications, checkResult } in existing student answer persistence. + +- [ ] **Step 1: Write the failing frontend contract test** + +~~~javascript +const source = fs.readFileSync(path.join(root, "src/views/foundation/new-product-survey.vue"), "utf8"); +const api = fs.readFileSync(path.join(root, "src/api/studentTrainingAnswer.js"), "utf8"); + +assert.match(source, /const STEP_ONE_SELLING_POINTS = \[/); +assert.equal((source.match(/selling-point-/g) || []).length >= 11, true); +assert.match(source, /核心层/); +assert.match(source, /有形层/); +assert.match(source, /延伸层/); +assert.match(source, /checkNewProductSurveyStepOne/); +assert.match(source, /checkResult/); +assert.match(api, /new-product-survey\/step-1\/check/); +~~~ + +- [ ] **Step 2: Verify the frontend test fails** + +Run: node tests/new-product-survey-step-one.static.test.cjs + +Expected: FAIL because the fixed classification data and API wrapper do not exist. + +- [ ] **Step 3: Add the API wrapper and demo-mode grader** + +~~~javascript +export function checkNewProductSurveyStepOne(items) { + if (isStudentDemo()) return Promise.resolve({ code: 200, data: gradeNewProductSurveyStepOne(items) }); + return request({ + url: "/api/student-training-answers/new-product-survey/step-1/check", + method: "post", + data: { items }, + headers: { repeatSubmit: false }, + }); +} +~~~ + +Keep the demo answers in the same fixed id/category map as the visible question metadata, return no correctCategory, and return the same feedback messages as the API. + +- [ ] **Step 4: Replace only the first-step form with the classification table** + +~~~vue + + + + + + + + + +
序号卖点三层次分类
{{ index + 1 }}{{ point.text }} + + + +

{{ checkResult.itemsById[point.id].message }}

+
+~~~ + +Define the 11 selling-point strings exactly as in the approved prototype and the IDs from Task 1. Leave steps two through four, task configuration, TrainingMaterialButton, and its download action intact. Replace obsolete step1Form inputs in load/reset/draft/export helpers with classifications and checkResult. + +- [ ] **Step 5: Guard first-step navigation and save feedback** + +~~~javascript +async function nextStep() { + if (currentStep.value !== 1) return advanceStep(); + if (Object.keys(classifications.value).length !== STEP_ONE_SELLING_POINTS.length) { + proxy?.$modal?.msgWarning("请完成全部卖点分类后再进入下一步"); + return; + } + checking.value = true; + try { + const res = await checkNewProductSurveyStepOne(toCheckItems()); + checkResult.value = normalizeCheckResult(res?.data); + await saveAnswer("IN_PROGRESS", true); + if (checkResult.value.allCorrect) advanceStep(); + } finally { + checking.value = false; + } +} +~~~ + +Persist step1Answer as JSON containing classifications and checkResult. On failed grading, missing selections, network failure, or save failure, retain current selections and do not advance. Saving current progress remains an ungraded draft save. Hydrate classifications and checkResult after refresh without auto-advancing. + +- [ ] **Step 6: Add response and feedback styles** + +~~~scss +.classification-table .classification-row--correct { background: rgba(19, 180, 118, 0.12); } +.classification-table .classification-row--incorrect { background: rgba(239, 68, 68, 0.12); } +.classification-feedback { margin: 6px 0 0; font-size: 12px; } +@media (max-width: 860px) { .classification-table { min-width: 720px; } } +~~~ + +Match the existing student theme; do not render the prototype’s red standard answers. + +- [ ] **Step 7: Verify the frontend test and production build pass** + +Run: node tests/new-product-survey-step-one.static.test.cjs; npm run build:prod + +Expected: the static contract passes and Vite exits 0. + +- [ ] **Step 8: Commit Task 3** + +~~~powershell +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 classification step" +~~~ + +### Task 4: Verify the integrated student flow and launch services + +**Files:** + +- Modify only if verification identifies a defect in files from Tasks 1–3. + +**Interfaces:** + +- Consumes: compiled backend route and compiled frontend page from Tasks 1–3. +- Produces: locally running backend and frontend processes, with their endpoint and page reachable. + +- [ ] **Step 1: Run the focused backend suite** + +Run: .\mvnw.cmd -Dtest=NewProductSurveyStepOneServiceImplTest,StudentTrainingAnswerServiceImplTest,StudentTrainingAnswerControllerTest test + +Expected: PASS. + +- [ ] **Step 2: Run frontend tests and production build** + +Run: node tests/new-product-survey-step-one.static.test.cjs; node tests/student-training-navigation-esm.test.mjs; npm run build:prod + +Expected: all tests print their pass message and build exits 0. + +- [ ] **Step 3: Start and verify the backend** + +Run from link_commerce: + +~~~powershell +Start-Process -FilePath .\mvnw.cmd -ArgumentList 'spring-boot:run' -WindowStyle Hidden -RedirectStandardOutput .\logs\new-product-survey-backend.out.log -RedirectStandardError .\logs\new-product-survey-backend.err.log +~~~ + +Expected: inspect the log until Spring Boot reports that it has started. + +- [ ] **Step 4: Start and verify the frontend** + +Run from e-commerce-internet: + +~~~powershell +Start-Process -FilePath npm.cmd -ArgumentList 'run','dev','--','--host','127.0.0.1' -WindowStyle Hidden -RedirectStandardOutput .\logs\new-product-survey-frontend.out.log -RedirectStandardError .\logs\new-product-survey-frontend.err.log +~~~ + +Expected: Vite reports the local URL; request it and confirm HTTP 200. + +## Self-Review + +- Spec coverage: Task 1 seeds non-overwriting persistent standard answers; Task 2 exposes a student-only route; Task 3 creates the approved page, safe feedback, persistence, guarded advance, and demo behavior while retaining download behavior; Task 4 verifies and launches both applications. +- Placeholder scan: no undecided schema, endpoint, question IDs, categories, or commands remain. +- Type consistency: backend and client use request.items; response consistently uses items, correctCount, totalCount, and allCorrect; persisted first-step data is consistently classifications and checkResult.