Update 临考词汇突击 PDF report filenames so generated files use the student's name, the business report title, and an Asia/Shanghai timestamp.
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:
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.
Generated PDF filenames must use one of these formats:
{StudentName}-临考词汇突击成果报告-{currentTime}.pdf
{StudentName}-临考词汇突击潜力展望报告-{currentTime}.pdf
Examples:
吴泓妤-临考词汇突击成果报告-20250627133544.pdf
冯亿豪-临考词汇突击潜力展望报告-20250627120841.pdf
studentName from the report payload root for filename generation.studentName is missing, JSON null, non-textual, empty, or all whitespace, use reportId as the filename prefix.studentName.StudentName field and may also accept lowercase studentName as an alias, without letting the filename-only lowercase field weaken unrelated validation.ReportType.ACHIEVEMENT maps to 临考词汇突击成果报告.ReportType.OUTLOOK maps to 临考词汇突击潜力展望报告.Clock.Asia/Shanghai.yyyyMMddHHmmss..pdf extension.ExamSprintReportGenerationPipelineUpdate 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.
AchievementExamSprintReportPayloadAdd optional root field studentName without @NotBlank, because missing or blank names are valid and must fall back to reportId.
OutlookExamSprintReportPayloadKeep 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 AchievementReportContentMapperAdd optional studentName to AchievementReportContent and map it from the contract payload. Existing validation should continue to require the current report content fields only.
InMemoryExamSprintReportStorageStop 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(...).
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.Clock.storage.upload(...) receives the new filename.ExamSprintReport as storageObjectKey and fileName.studentName must not fail generation.Clock.Follow TDD:
Required coverage:
studentName stores 冯亿豪-临考词汇突击潜力展望报告-20260101080000.pdf when Clock is fixed at 2026-01-01T00:00:00Z.studentName stores 吴泓妤-临考词汇突击成果报告-20260101080000.pdf for the same clock instant.studentName falls back to {reportId}-临考词汇突击潜力展望报告-20260101080000.pdf or the corresponding achievement title.fileName, generated download key tracking, and log assertions are updated to the same new filename.{StudentNameOrReportId}-临考词汇突击潜力展望报告-{yyyyMMddHHmmss}.pdf.{StudentNameOrReportId}-临考词汇突击成果报告-{yyyyMMddHHmmss}.pdf.currentTime is formatted in Asia/Shanghai.studentName uses reportId.