|
|
@@ -0,0 +1,331 @@
|
|
|
+# Exam Sprint Report Payload Summary And Efficiency 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:** Log incoming `OUTLOOK` and `ACHIEVEMENT` report payloads while replacing every array field with `<fieldName>Size`, and derive achievement learning efficiency from mastery hit rate divided by `0.04`.
|
|
|
+
|
|
|
+**Architecture:** Keep changes inside the existing report application boundary. `DefaultExamSprintReportApplicationService` owns payload logging because it receives raw `JsonNode` request payloads; `AchievementReportContentMapper` owns presentation-field derivation for achievement reports.
|
|
|
+
|
|
|
+**Tech Stack:** Java 17, Spring Boot, Jackson `JsonNode`/`ObjectNode`, SLF4J, JUnit 5, AssertJ, Spring `OutputCaptureExtension`, Maven.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File Structure
|
|
|
+
|
|
|
+- Modify `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java`
|
|
|
+ - Add payload-received logs in the four report creation entry points.
|
|
|
+ - Add recursive Jackson helper methods that preserve scalar/object fields and replace arrays with `<fieldName>Size` numeric fields.
|
|
|
+- Modify `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapper.java`
|
|
|
+ - Compute learning efficiency as `(testPaperImprovedWordCount / studentImproveWordCount) / 0.04`.
|
|
|
+ - Return `0` when `studentImproveWordCount` is zero.
|
|
|
+- Modify `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
|
|
|
+ - Add log-capture tests for `OUTLOOK` and `ACHIEVEMENT` payload summaries.
|
|
|
+ - Update achievement learning-efficiency assertions.
|
|
|
+
|
|
|
+## Task 1: Payload Summary Logging Tests
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Test: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
|
|
|
+
|
|
|
+- [ ] **Step 1: Add an OUTLOOK payload logging test**
|
|
|
+
|
|
|
+Insert this test near existing OUTLOOK creation tests after `createOutlookReportAcceptsCallerVocabularyPayloadAndDispatches`:
|
|
|
+
|
|
|
+```java
|
|
|
+ /** 覆盖展望报告原始报文日志场景,当 payload 包含数组时,应只记录数组大小而不记录数组内容。 */
|
|
|
+ @Test
|
|
|
+ void createOutlookReportLogsPayloadWithArraySizesOnly(CapturedOutput output) throws Exception {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ DefaultExamSprintReportApplicationService service = service(repository, reportId -> { }, new TestStorage());
|
|
|
+
|
|
|
+ service.createOutlookReport(callerVocabularyPayload());
|
|
|
+
|
|
|
+ assertThat(output.getAll())
|
|
|
+ .contains("exam_sprint_report_payload_received")
|
|
|
+ .contains("reportType=OUTLOOK")
|
|
|
+ .contains("mode=async")
|
|
|
+ .contains("StudentName")
|
|
|
+ .contains("20260318测试")
|
|
|
+ .contains("TestPaperWordIdArraySize")
|
|
|
+ .contains("StudentWordsLatestSize")
|
|
|
+ .contains("TestPaperUnMasterWordsSize")
|
|
|
+ .contains("TestPaperMastedWordsSize")
|
|
|
+ .doesNotContain("TestPaperWordIdArray\":")
|
|
|
+ .doesNotContain("WordSpell\":\"w1")
|
|
|
+ .doesNotContain("lot")
|
|
|
+ .doesNotContain("father")
|
|
|
+ .doesNotContain("catch");
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Add an ACHIEVEMENT payload logging test**
|
|
|
+
|
|
|
+Insert this test near existing ACHIEVEMENT creation tests after `createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent`:
|
|
|
+
|
|
|
+```java
|
|
|
+ /** 覆盖成果报告原始报文日志场景,当 payload 包含数组时,应只记录数组大小而不记录数组内容。 */
|
|
|
+ @Test
|
|
|
+ void createAchievementReportLogsPayloadWithArraySizesOnly(CapturedOutput output) {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ DefaultExamSprintReportApplicationService service = service(repository, reportId -> { }, new TestStorage());
|
|
|
+
|
|
|
+ service.createAchievementReport(validAchievementPayload());
|
|
|
+
|
|
|
+ assertThat(output.getAll())
|
|
|
+ .contains("exam_sprint_report_payload_received")
|
|
|
+ .contains("reportType=ACHIEVEMENT")
|
|
|
+ .contains("mode=async")
|
|
|
+ .contains("StudentName")
|
|
|
+ .contains("吴泓妤")
|
|
|
+ .contains("TestPaperImprovedWordsSize")
|
|
|
+ .contains("StudentWordsLatestSize")
|
|
|
+ .contains("StudentWordsFirstPreExamAssaultAfterSize")
|
|
|
+ .doesNotContain("TestPaperImprovedWords\":")
|
|
|
+ .doesNotContain("number")
|
|
|
+ .doesNotContain("ignored-large-array");
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Run tests and verify the new tests fail before implementation**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createOutlookReportLogsPayloadWithArraySizesOnly,ExamSprintReportApplicationServiceTest#createAchievementReportLogsPayloadWithArraySizesOnly test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL because `exam_sprint_report_payload_received`, `TestPaperWordIdArraySize`, and `TestPaperImprovedWordsSize` are not logged yet.
|
|
|
+
|
|
|
+## Task 2: Payload Summary Logging Implementation
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java`
|
|
|
+- Test: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
|
|
|
+
|
|
|
+- [ ] **Step 1: Add Jackson node imports**
|
|
|
+
|
|
|
+Update imports in `DefaultExamSprintReportApplicationService.java`:
|
|
|
+
|
|
|
+```java
|
|
|
+import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
|
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
|
|
+import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
|
+```
|
|
|
+
|
|
|
+Keep the existing `ObjectNode` import and add `ArrayNode` plus `JsonNodeFactory` beside it.
|
|
|
+
|
|
|
+- [ ] **Step 2: Log payload summaries in all report creation entry points**
|
|
|
+
|
|
|
+Replace the four public creation methods with:
|
|
|
+
|
|
|
+```java
|
|
|
+ @Override
|
|
|
+ public CreateExamSprintReportResponse createOutlookReport(JsonNode payload) {
|
|
|
+ logReceivedPayload(ReportType.OUTLOOK, "async", payload);
|
|
|
+ validateOutlookPayload(payload);
|
|
|
+ JsonNode normalizedPayload = normalizeOutlookPayload(payload);
|
|
|
+ return submitReportGeneration(ReportType.OUTLOOK, new UnmodeledReportContent(ReportType.OUTLOOK, normalizedPayload));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public CreateExamSprintReportResponse createAchievementReport(JsonNode payload) {
|
|
|
+ logReceivedPayload(ReportType.ACHIEVEMENT, "async", payload);
|
|
|
+ AchievementReportContent content = validateAchievementPayload(payload);
|
|
|
+ return submitReportGeneration(ReportType.ACHIEVEMENT, content);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public CreateExamSprintReportWithUrlResponse createOutlookReportSync(JsonNode payload) {
|
|
|
+ logReceivedPayload(ReportType.OUTLOOK, "sync", payload);
|
|
|
+ validateOutlookPayload(payload);
|
|
|
+ JsonNode normalizedPayload = normalizeOutlookPayload(payload);
|
|
|
+ return submitReportGenerationSync(ReportType.OUTLOOK, new UnmodeledReportContent(ReportType.OUTLOOK, normalizedPayload));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public CreateExamSprintReportWithUrlResponse createAchievementReportSync(JsonNode payload) {
|
|
|
+ logReceivedPayload(ReportType.ACHIEVEMENT, "sync", payload);
|
|
|
+ AchievementReportContent content = validateAchievementPayload(payload);
|
|
|
+ return submitReportGenerationSync(ReportType.ACHIEVEMENT, content);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Add recursive payload summary helpers**
|
|
|
+
|
|
|
+Add these private methods before `submitReportGeneration`:
|
|
|
+
|
|
|
+```java
|
|
|
+ private void logReceivedPayload(ReportType reportType, String mode, JsonNode payload) {
|
|
|
+ log.info(
|
|
|
+ "exam_sprint_report_payload_received reportType={} mode={} payload={}",
|
|
|
+ reportType,
|
|
|
+ mode,
|
|
|
+ summarizePayloadForLog(payload));
|
|
|
+ }
|
|
|
+
|
|
|
+ private JsonNode summarizePayloadForLog(JsonNode payload) {
|
|
|
+ if (payload == null || payload.isNull() || payload.isMissingNode()) {
|
|
|
+ return payload;
|
|
|
+ }
|
|
|
+ if (payload.isObject()) {
|
|
|
+ ObjectNode summary = objectMapper.createObjectNode();
|
|
|
+ payload.fields().forEachRemaining(entry -> appendSummarizedField(summary, entry.getKey(), entry.getValue()));
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+ if (payload.isArray()) {
|
|
|
+ return JsonNodeFactory.instance.numberNode(payload.size());
|
|
|
+ }
|
|
|
+ return payload.deepCopy();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void appendSummarizedField(ObjectNode summary, String fieldName, JsonNode value) {
|
|
|
+ if (value != null && value.isArray()) {
|
|
|
+ summary.put(fieldName + "Size", value.size());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ summary.set(fieldName, summarizePayloadForLog(value));
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Run payload logging tests and verify pass**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createOutlookReportLogsPayloadWithArraySizesOnly,ExamSprintReportApplicationServiceTest#createAchievementReportLogsPayloadWithArraySizesOnly test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+## Task 3: Achievement Learning Efficiency Tests
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Test: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
|
|
|
+
|
|
|
+- [ ] **Step 1: Update existing learning-efficiency assertion**
|
|
|
+
|
|
|
+In `createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent`, replace:
|
|
|
+
|
|
|
+```java
|
|
|
+ assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("0.48");
|
|
|
+```
|
|
|
+
|
|
|
+with:
|
|
|
+
|
|
|
+```java
|
|
|
+ assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("5.263158");
|
|
|
+```
|
|
|
+
|
|
|
+This expectation comes from `(4 / 19) / 0.04`, rounded to 6 decimal places.
|
|
|
+
|
|
|
+- [ ] **Step 2: Add zero-denominator assertion**
|
|
|
+
|
|
|
+In `createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero`, add:
|
|
|
+
|
|
|
+```java
|
|
|
+ assertThat(content.examUnknownWordsHitStatus().learningEfficiencyText()).isEqualTo("0");
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Run tests and verify they fail before implementation**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent,ExamSprintReportApplicationServiceTest#createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL because implementation still uses upstream `ImproveStudyEfficiency`.
|
|
|
+
|
|
|
+## Task 4: Achievement Learning Efficiency Implementation
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapper.java`
|
|
|
+- Test: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
|
|
|
+
|
|
|
+- [ ] **Step 1: Introduce derived learning efficiency text**
|
|
|
+
|
|
|
+In `toDomainContent`, after `masteryHitRateText` add:
|
|
|
+
|
|
|
+```java
|
|
|
+ String learningEfficiencyText = learningEfficiencyText(payload.testPaperImprovedWordCount(), payload.studentImproveWordCount());
|
|
|
+```
|
|
|
+
|
|
|
+Then replace both occurrences of:
|
|
|
+
|
|
|
+```java
|
|
|
+format(payload.improveStudyEfficiency())
|
|
|
+```
|
|
|
+
|
|
|
+with:
|
|
|
+
|
|
|
+```java
|
|
|
+learningEfficiencyText
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Add the calculation helper**
|
|
|
+
|
|
|
+Add this method after `masteryHitRateText`:
|
|
|
+
|
|
|
+```java
|
|
|
+ private static String learningEfficiencyText(BigDecimal improvedWordCount, BigDecimal studentImproveWordCount) {
|
|
|
+ if (studentImproveWordCount.signum() == 0) {
|
|
|
+ return "0";
|
|
|
+ }
|
|
|
+ BigDecimal efficiency = improvedWordCount
|
|
|
+ .divide(studentImproveWordCount, 8, RoundingMode.HALF_UP)
|
|
|
+ .divide(new BigDecimal("0.04"), 6, RoundingMode.HALF_UP);
|
|
|
+ return format(efficiency);
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Run learning-efficiency tests and verify pass**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest#createAchievementReportAcceptsCallerPascalCasePayloadAndStoresAchievementContent,ExamSprintReportApplicationServiceTest#createAchievementReportStoresZeroPercentWhenStudentImproveWordCountIsZero test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+## Task 5: Regression Verification
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Test: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
|
|
|
+- Verify: `pom.xml`, `abilities/exam-sprint/application/pom.xml`
|
|
|
+
|
|
|
+- [ ] **Step 1: Run full application-module test class**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+- [ ] **Step 2: Run application module tests**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+- [ ] **Step 3: Inspect working tree**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+git status --short
|
|
|
+```
|
|
|
+
|
|
|
+Expected: modified Java source/test files and this plan file only, unless local build outputs already existed.
|
|
|
+
|
|
|
+## Self-Review
|
|
|
+
|
|
|
+- Spec coverage: payload logging for `OUTLOOK` and `ACHIEVEMENT`, array-size-only behavior, and derived achievement learning efficiency are each covered by tests and implementation tasks.
|
|
|
+- Placeholder scan: no `TBD`, `TODO`, or unspecified implementation steps remain.
|
|
|
+- Type consistency: helper methods use existing `JsonNode`, `ObjectNode`, `ObjectMapper`, `ReportType`, `BigDecimal`, and `RoundingMode` types already present in the target classes.
|