2026-04-29-exam-sprint-report-filename-design.md 7.3 KB

Exam Sprint Report Filename Design

Goal

Update 临考词汇突击 PDF report filenames so generated files use the student's name, the business report title, and an Asia/Shanghai timestamp.

Scope

This change is limited to the exam-sprint report generation filename path and the minimal payload/domain/storage-test propagation needed to make studentName available for both report types and keep generated download URLs working with localized filenames.

It changes the fileName passed to ExamSprintReportStorage.upload(...). Because the existing storage implementations return the same value as storageObjectKey in current tests and Azure uses fileName as the blob name, the visible storage key and downloaded filename will also change.

This change does not alter:

  • API response schemas
  • report rendering content
  • PDF generation
  • storage interfaces and Azure storage behavior
  • download URL generation semantics
  • report state transitions

Recommended Approach

Keep filename construction centralized in ExamSprintReportGenerationPipeline.fileNameOf(...).

The pipeline already owns the upload filename and has access to ExamSprintReport, ReportType, report content, and the injected Clock. This is the smallest safe change and keeps Azure storage, in-memory storage, logs, and persisted fileName/storageObjectKey values consistent. For achievement reports, add optional studentName propagation from the request contract into AchievementReportContent so the pipeline can use the same root field rule as outlook reports.

Business Rules

Filename format

Generated PDF filenames must use one of these formats:

{StudentName}-临考词汇突击成果报告-{currentTime}.pdf
{StudentName}-临考词汇突击潜力展望报告-{currentTime}.pdf

Examples:

吴泓妤-临考词汇突击成果报告-20250627133544.pdf
冯亿豪-临考词汇突击潜力展望报告-20250627120841.pdf

Student name source

  • Read lowercase studentName from the report payload root for filename generation.
  • If studentName is missing, JSON null, non-textual, empty, or all whitespace, use reportId as the filename prefix.
  • Trim leading and trailing whitespace before using studentName.
  • Outlook validation must continue to accept the existing upstream StudentName field and may also accept lowercase studentName as an alias, without letting the filename-only lowercase field weaken unrelated validation.

Report title mapping

  • ReportType.ACHIEVEMENT maps to 临考词汇突击成果报告.
  • ReportType.OUTLOOK maps to 临考词汇突击潜力展望报告.

Timestamp

  • Use the pipeline's injected Clock.
  • Format the current instant in Asia/Shanghai.
  • Use pattern yyyyMMddHHmmss.
  • Keep the .pdf extension.

Components

ExamSprintReportGenerationPipeline

Update fileNameOf(ExamSprintReport report) to build the business filename instead of exam-sprint-{type}-report-{reportId}.pdf.

Add focused private helpers if needed:

  • studentNameOrReportId(ExamSprintReport report)
  • studentNameFromContent(ReportContent content)
  • reportTitle(ReportType reportType)
  • formattedShanghaiTimestamp()

The implementation should support both current content shapes:

  • UnmodeledReportContent wrapping a JsonNode source, used by outlook reports.
  • AchievementReportContent, used by achievement reports after payload validation and mapping, with optional studentName propagated from AchievementExamSprintReportPayload.studentName.

If an achievement payload omits studentName, the implementation should still fall back to reportId rather than failing.

AchievementExamSprintReportPayload

Add optional root field studentName without @NotBlank, because missing or blank names are valid and must fall back to reportId.

OutlookExamSprintReportPayload

Keep the existing @JsonProperty("StudentName") contract and add lowercase studentName as a Jackson alias for validation compatibility. When both StudentName and studentName appear, validation should avoid treating the filename-only alias as a replacement for the canonical upstream name, while the persisted raw payload still keeps lowercase studentName for filename generation.

AchievementReportContent and AchievementReportContentMapper

Add optional studentName to AchievementReportContent and map it from the contract payload. Existing validation should continue to require the current report content fields only.

InMemoryExamSprintReportStorage

Stop deriving reportId from storageObjectKey filename prefixes. The new localized filenames are not parseable by the old exam-sprint-*-report-{reportId}.pdf convention. The in-memory storage should retain the reportId supplied to upload(...) with the stored file and use that value in generateDownloadUrl(...).

Data Flow

  1. The application service validates and saves the submitted report.
  2. The generation pipeline renders HTML and generates PDF bytes.
  3. For achievement reports, AchievementReportContentMapper preserves root studentName on the domain content. For outlook reports, the unmodeled JSON content preserves root lowercase studentName while validation remains compatible with canonical StudentName.
  4. Before uploading, the pipeline builds the filename from report content, report type, report id, and Clock.
  5. storage.upload(...) receives the new filename.
  6. The stored file result is persisted on ExamSprintReport as storageObjectKey and fileName.
  7. Existing query, download, cleanup, and logging flows continue to use the persisted values.

Error Handling

  • Missing or invalid studentName must not fail generation.
  • Unknown report type should fail fast through the existing pipeline exception handling rather than producing a misleading filename.
  • Time formatting should be deterministic under tests by using the injected Clock.

Testing Strategy

Follow TDD:

  1. Update application/generation tests to expect the new filename format and verify they fail against current code.
  2. Implement the minimal filename-generation changes.
  3. Run the focused application tests.
  4. Run the full Maven test suite.

Required coverage:

  • Outlook report with lowercase studentName stores 冯亿豪-临考词汇突击潜力展望报告-20260101080000.pdf when Clock is fixed at 2026-01-01T00:00:00Z.
  • Achievement report with lowercase studentName stores 吴泓妤-临考词汇突击成果报告-20260101080000.pdf for the same clock instant.
  • Missing or blank studentName falls back to {reportId}-临考词汇突击潜力展望报告-20260101080000.pdf or the corresponding achievement title.
  • Existing storage key, persisted fileName, generated download key tracking, and log assertions are updated to the same new filename.
  • Default in-memory storage can generate download URLs after localized filename upload without parsing report id from the filename.

Acceptance Criteria

  • Generated outlook PDF filenames follow {StudentNameOrReportId}-临考词汇突击潜力展望报告-{yyyyMMddHHmmss}.pdf.
  • Generated achievement PDF filenames follow {StudentNameOrReportId}-临考词汇突击成果报告-{yyyyMMddHHmmss}.pdf.
  • currentTime is formatted in Asia/Shanghai.
  • Empty, missing, null, or non-textual studentName uses reportId.
  • In-memory storage download URL generation remains compatible with localized filenames.
  • All affected tests pass.