Forráskód Böngészése

Merge branch 'feat/中文日志与PDF计时' of jyx/dcjxb.microservice into master

金逸霄 6 napja
szülő
commit
a2e4100092
12 módosított fájl, 191 hozzáadás és 93 törlés
  1. 23 23
      abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java
  2. 11 11
      abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationPipeline.java
  3. 4 4
      abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportWarmupRunner.java
  4. 15 15
      abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
  5. 5 5
      abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java
  6. 5 5
      abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportWarmupRunnerTest.java
  7. 34 1
      abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/DefaultPlaywrightPdfWorker.java
  8. 12 12
      abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/PooledPlaywrightExamSprintReportPdfGenerator.java
  9. 4 4
      abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorage.java
  10. 60 0
      abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/PlaywrightExamSprintReportPdfGeneratorTest.java
  11. 9 9
      abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/PooledPlaywrightExamSprintReportPdfGeneratorTest.java
  12. 9 4
      abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorageTest.java

+ 23 - 23
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java

@@ -129,7 +129,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
 
     private void logReceivedPayload(ReportType reportType, String mode, JsonNode payload) {
         log.info(
-                "exam_sprint_report_payload_received reportType={} mode={} payload={}",
+                "临考报告请求载荷已接收 reportType={} mode={} payload={}",
                 reportType,
                 mode,
                 summarizePayloadForLog(payload));
@@ -208,7 +208,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                 now.plus(properties.getRetention()));
         repository.save(report);
         log.info(
-                "exam_sprint_report_submitted reportId={} reportType={} generationStatus={} expiresAt={} mode=async",
+                "临考报告生成任务已提交 reportId={} reportType={} generationStatus={} expiresAt={} mode=async",
                 report.reportId(),
                 report.reportType(),
                 report.generationStatus(),
@@ -216,13 +216,13 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         try {
             dispatcher.dispatch(report.reportId());
             log.info(
-                    "exam_sprint_report_dispatched reportId={} reportType={} mode=async",
+                    "临考报告生成任务已派发 reportId={} reportType={} mode=async",
                     report.reportId(),
                     report.reportType());
         } catch (RuntimeException exception) {
             report = repository.save(report.failed(now, dispatchFailureReason(exception)));
             log.warn(
-                    "exam_sprint_report_dispatch_failed reportId={} reportType={} failureReason={} exceptionType={}",
+                    "临考报告生成任务派发失败 reportId={} reportType={} failureReason={} exceptionType={}",
                     report.reportId(),
                     report.reportType(),
                     report.failureReason(),
@@ -249,7 +249,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                 now.plus(properties.getRetention()));
         repository.save(report);
         log.info(
-                "exam_sprint_report_sync_generation_started reportId={} reportType={} generationStatus={} expiresAt={}",
+                "临考报告同步生成已开始 reportId={} reportType={} generationStatus={} expiresAt={}",
                 report.reportId(),
                 report.reportType(),
                 report.generationStatus(),
@@ -258,7 +258,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         Optional<ExamSprintReport> generatedReportOption = pipeline.generate(report.reportId());
         if (generatedReportOption.isEmpty()) {
             log.warn(
-                    "exam_sprint_report_sync_generation_unavailable reportId={} reportType={} reason=pipeline_empty durationMs={}",
+                    "临考报告同步生成不可用 reportId={} reportType={} reason=pipeline_empty durationMs={}",
                     report.reportId(),
                     report.reportType(),
                     elapsedMillis(startedNanos));
@@ -269,7 +269,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         if (generatedReport.generationStatus() != ReportGenerationStatus.SUCCESS
                 || generatedReport.storageObjectKey() == null) {
             log.warn(
-                    "exam_sprint_report_sync_generation_unavailable reportId={} reportType={} generationStatus={} storageObjectKeyPresent={} durationMs={}",
+                    "临考报告同步生成不可用 reportId={} reportType={} generationStatus={} storageObjectKeyPresent={} durationMs={}",
                     generatedReport.reportId(),
                     generatedReport.reportType(),
                     generatedReport.generationStatus(),
@@ -285,7 +285,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                     properties.getDownloadExpiry()).toString();
         } catch (RuntimeException exception) {
             log.warn(
-                    "exam_sprint_report_sync_generation_unavailable reportId={} reportType={} generationStatus={} reason=download_url_generation_failed exceptionType={} durationMs={}",
+                    "临考报告同步生成不可用 reportId={} reportType={} generationStatus={} reason=download_url_generation_failed exceptionType={} durationMs={}",
                     generatedReport.reportId(),
                     generatedReport.reportType(),
                     generatedReport.generationStatus(),
@@ -294,7 +294,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
             throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
         }
         log.info(
-                "exam_sprint_report_sync_generation_succeeded reportId={} reportType={} generationStatus={} durationMs={} storageObjectKey={}",
+                "临考报告同步生成成功 reportId={} reportType={} generationStatus={} durationMs={} storageObjectKey={}",
                 generatedReport.reportId(),
                 generatedReport.reportType(),
                 generatedReport.generationStatus(),
@@ -335,7 +335,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
 
         private void logCompleted() {
             log.info(
-                    "exam_sprint_report_sync_stage_completed reportType={} stage=prepare_content durationMs={} payloadSummaryDurationMs={} validationDurationMs={} contentMappingDurationMs={}",
+                    "临考报告同步准备阶段完成 reportType={} stage=prepare_content durationMs={} payloadSummaryDurationMs={} validationDurationMs={} contentMappingDurationMs={}",
                     reportType,
                     elapsedMillis(startedNanos),
                     payloadSummaryDurationMs,
@@ -351,7 +351,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         if (report.isExpiredAt(now) && report.generationStatus() != ReportGenerationStatus.EXPIRED) {
             report = repository.save(report.expired(now));
             log.info(
-                    "exam_sprint_report_marked_expired_on_query reportId={} reportType={} generationStatus={} expiresAt={}",
+                    "临考报告查询时已标记过期 reportId={} reportType={} generationStatus={} expiresAt={}",
                     report.reportId(),
                     report.reportType(),
                     report.generationStatus(),
@@ -366,7 +366,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                 downloadUrl = storage.generateDownloadUrl(report.storageObjectKey(), properties.getDownloadExpiry()).toString();
             } catch (RuntimeException exception) {
                 log.warn(
-                        "exam_sprint_report_download_url_generation_failed reportId={} reportType={} generationStatus={} storageObjectKey={} exceptionType={}",
+                        "临考报告下载地址生成失败 reportId={} reportType={} generationStatus={} storageObjectKey={} exceptionType={}",
                         report.reportId(),
                         report.reportType(),
                         report.generationStatus(),
@@ -377,7 +377,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
 
         boolean downloadUrlIncluded = downloadUrl != null;
         log.info(
-                "exam_sprint_report_query_completed reportId={} reportType={} generationStatus={} downloadUrlIncluded={} storageObjectKeyPresent={}",
+                "临考报告查询完成 reportId={} reportType={} generationStatus={} downloadUrlIncluded={} storageObjectKeyPresent={}",
                 report.reportId(),
                 report.reportType(),
                 report.generationStatus(),
@@ -400,7 +400,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         Instant now = clock.instant();
         ExamSprintReport report = requireReport(reportId);
         log.info(
-                "exam_sprint_report_download_started reportId={} reportType={} generationStatus={}",
+                "临考报告下载已开始 reportId={} reportType={} generationStatus={}",
                 report.reportId(),
                 report.reportType(),
                 report.generationStatus());
@@ -409,7 +409,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                 report = repository.save(report.expired(now));
             }
             log.warn(
-                    "exam_sprint_report_download_unavailable reportId={} reportType={} generationStatus={} reason=expired",
+                    "临考报告下载不可用 reportId={} reportType={} generationStatus={} reason=expired",
                     report.reportId(),
                     report.reportType(),
                     report.generationStatus());
@@ -417,7 +417,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         }
         if (report.generationStatus() != ReportGenerationStatus.SUCCESS) {
             log.warn(
-                    "exam_sprint_report_download_unavailable reportId={} reportType={} generationStatus={} reason=not_success",
+                    "临考报告下载不可用 reportId={} reportType={} generationStatus={} reason=not_success",
                     report.reportId(),
                     report.reportType(),
                     report.generationStatus());
@@ -425,7 +425,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         }
         if (report.storageObjectKey() == null) {
             log.warn(
-                    "exam_sprint_report_download_unavailable reportId={} reportType={} generationStatus={} reason=missing_storage_key",
+                    "临考报告下载不可用 reportId={} reportType={} generationStatus={} reason=missing_storage_key",
                     report.reportId(),
                     report.reportType(),
                     report.generationStatus());
@@ -437,7 +437,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
             content = storage.download(report.storageObjectKey());
         } catch (RuntimeException exception) {
             log.warn(
-                    "exam_sprint_report_download_unavailable reportId={} reportType={} generationStatus={} reason=storage_download_failed storageObjectKey={} exceptionType={}",
+                    "临考报告下载不可用 reportId={} reportType={} generationStatus={} reason=storage_download_failed storageObjectKey={} exceptionType={}",
                     report.reportId(),
                     report.reportType(),
                     report.generationStatus(),
@@ -447,7 +447,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
         }
         if (content.isEmpty()) {
             log.warn(
-                    "exam_sprint_report_download_missing_storage_content reportId={} reportType={} storageObjectKey={}",
+                    "临考报告下载缺少存储内容 reportId={} reportType={} storageObjectKey={}",
                     report.reportId(),
                     report.reportType(),
                     report.storageObjectKey());
@@ -460,7 +460,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                         storedContent.contentType()))
                 .orElseThrow();
         log.info(
-                "exam_sprint_report_download_succeeded reportId={} reportType={} storageObjectKey={} fileName={} byteLength={}",
+                "临考报告下载成功 reportId={} reportType={} storageObjectKey={} fileName={} byteLength={}",
                 report.reportId(),
                 report.reportType(),
                 report.storageObjectKey(),
@@ -488,7 +488,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
             } catch (RuntimeException exception) {
                 failedCount++;
                 log.warn(
-                        "exam_sprint_report_cleanup_item_failed reportId={} reportType={} generationStatus={} storageObjectKey={} exceptionType={}",
+                        "临考报告清理单项失败 reportId={} reportType={} generationStatus={} storageObjectKey={} exceptionType={}",
                         report.reportId(),
                         report.reportType(),
                         report.generationStatus(),
@@ -498,7 +498,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
             }
         }
         log.info(
-                "exam_sprint_report_cleanup_completed scannedCount={} storageClearedCount={} markedExpiredCount={} failedCount={}",
+                "临考报告清理完成 scannedCount={} storageClearedCount={} markedExpiredCount={} failedCount={}",
                 expiredReports.size(),
                 storageClearedCount,
                 markedExpiredCount,
@@ -539,7 +539,7 @@ public class DefaultExamSprintReportApplicationService implements ExamSprintRepo
                 - shapeDurationMs
                 - deserializationDurationMs;
         log.info(
-                "exam_sprint_report_validation_stage_completed reportType={} stage=validate_payload durationMs={} studentNameDurationMs={} shapeDurationMs={} deserializationDurationMs={} beanValidationDurationMs={}",
+                "临考报告参数校验阶段完成 reportType={} stage=validate_payload durationMs={} studentNameDurationMs={} shapeDurationMs={} deserializationDurationMs={} beanValidationDurationMs={}",
                 ReportType.OUTLOOK,
                 elapsedMillis(startedNanos),
                 studentNameDurationMs,

+ 11 - 11
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationPipeline.java

@@ -55,13 +55,13 @@ public class ExamSprintReportGenerationPipeline {
         long startedNanos = System.nanoTime();
         ExamSprintReport report = repository.findById(reportId).orElse(null);
         if (report == null) {
-            log.info("exam_sprint_report_generation_skipped reportId={} reason=not_found", reportId);
+            log.info("临考报告生成已跳过 reportId={} reason=not_found", reportId);
             return Optional.empty();
         }
 
         if (report.generationStatus() != ReportGenerationStatus.PENDING) {
             log.info(
-                    "exam_sprint_report_generation_skipped reportId={} reportType={} generationStatus={} reason=not_pending",
+                    "临考报告生成已跳过 reportId={} reportType={} generationStatus={} reason=not_pending",
                     reportId,
                     report.reportType(),
                     report.generationStatus());
@@ -70,7 +70,7 @@ public class ExamSprintReportGenerationPipeline {
 
         if (report.isExpiredAt(startedAt)) {
             log.info(
-                    "exam_sprint_report_generation_skipped reportId={} reportType={} generationStatus={} reason=expired expiresAt={}",
+                    "临考报告生成已跳过 reportId={} reportType={} generationStatus={} reason=expired expiresAt={}",
                     reportId,
                     report.reportType(),
                     report.generationStatus(),
@@ -79,7 +79,7 @@ public class ExamSprintReportGenerationPipeline {
         }
 
         log.info(
-                "exam_sprint_report_generation_started reportId={} reportType={} generationStatus={} startedAt={}",
+                "临考报告生成已开始 reportId={} reportType={} generationStatus={} startedAt={}",
                 reportId,
                 report.reportType(),
                 report.generationStatus(),
@@ -87,7 +87,7 @@ public class ExamSprintReportGenerationPipeline {
 
         ExamSprintReport processingReport = repository.save(report.processing(startedAt));
         log.info(
-                "exam_sprint_report_generation_processing reportId={} reportType={} generationStatus={}",
+                "临考报告生成处理中 reportId={} reportType={} generationStatus={}",
                 processingReport.reportId(),
                 processingReport.reportType(),
                 processingReport.generationStatus());
@@ -100,7 +100,7 @@ public class ExamSprintReportGenerationPipeline {
             long renderStartedNanos = System.nanoTime();
             String html = renderer.render(processingReport.content(), startedAt);
             log.info(
-                    "exam_sprint_report_generation_stage_completed reportId={} reportType={} stage=render_html durationMs={} htmlLength={}",
+                    "临考报告生成阶段完成 reportId={} reportType={} stage=render_html durationMs={} htmlLength={}",
                     processingReport.reportId(),
                     processingReport.reportType(),
                     elapsedMillis(renderStartedNanos),
@@ -110,7 +110,7 @@ public class ExamSprintReportGenerationPipeline {
             long pdfStartedNanos = System.nanoTime();
             byte[] pdfBytes = pdfGenerator.generate(html);
             log.info(
-                    "exam_sprint_report_generation_stage_completed reportId={} reportType={} stage=pdf_generation durationMs={} pdfByteLength={}",
+                    "临考报告生成阶段完成 reportId={} reportType={} stage=pdf_generation durationMs={} pdfByteLength={}",
                     processingReport.reportId(),
                     processingReport.reportType(),
                     elapsedMillis(pdfStartedNanos),
@@ -126,7 +126,7 @@ public class ExamSprintReportGenerationPipeline {
                     pdfBytes,
                     processingReport.expiresAt());
             log.info(
-                    "exam_sprint_report_generation_stage_completed reportId={} reportType={} stage=storage_upload durationMs={} storageObjectKey={} fileName={} pdfByteLength={}",
+                    "临考报告生成阶段完成 reportId={} reportType={} stage=storage_upload durationMs={} storageObjectKey={} fileName={} pdfByteLength={}",
                     processingReport.reportId(),
                     processingReport.reportType(),
                     elapsedMillis(uploadStartedNanos),
@@ -138,7 +138,7 @@ public class ExamSprintReportGenerationPipeline {
             Optional<ExamSprintReport> currentReport = repository.findById(reportId);
             if (currentReport.isEmpty()) {
                 log.info(
-                        "exam_sprint_report_generation_skipped reportId={} reportType={} stage=success_save reason=not_found_before_success_save",
+                        "临考报告生成已跳过 reportId={} reportType={} stage=success_save reason=not_found_before_success_save",
                         reportId,
                         processingReport.reportType());
                 return Optional.empty();
@@ -147,7 +147,7 @@ public class ExamSprintReportGenerationPipeline {
             ExamSprintReport successReport = repository.save(currentReport.orElseThrow()
                     .success(clock.instant(), storedFile.storageObjectKey(), storedFile.fileName()));
             log.info(
-                    "exam_sprint_report_generation_succeeded reportId={} reportType={} generationStatus={} durationMs={} storageObjectKey={} fileName={}",
+                    "临考报告生成成功 reportId={} reportType={} generationStatus={} durationMs={} storageObjectKey={} fileName={}",
                     successReport.reportId(),
                     successReport.reportType(),
                     successReport.generationStatus(),
@@ -157,7 +157,7 @@ public class ExamSprintReportGenerationPipeline {
             return Optional.of(successReport);
         } catch (Exception exception) {
             log.error(
-                    "exam_sprint_report_generation_failed reportId={} reportType={} stage={} failureReason={} exceptionType={} durationMs={}",
+                    "临考报告生成失败 reportId={} reportType={} stage={} failureReason={} exceptionType={} durationMs={}",
                     reportId,
                     processingReport.reportType(),
                     stage,

+ 4 - 4
abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportWarmupRunner.java

@@ -55,7 +55,7 @@ public class ExamSprintReportWarmupRunner {
             executor.execute(this::warmUpReports);
         } catch (RuntimeException exception) {
             log.warn(
-                    "exam_sprint_report_warmup_failed reportType={} stage={} failureReason={} exceptionType={} durationMs={}",
+                    "临考报告预热失败 reportType={} stage={} failureReason={} exceptionType={} durationMs={}",
                     "ALL",
                     "async_submit",
                     exception.getClass().getSimpleName(),
@@ -72,7 +72,7 @@ public class ExamSprintReportWarmupRunner {
     private void warmUpReport(ReportType reportType, String resource) {
         long startedNanos = System.nanoTime();
         String stage = "load_payload";
-        log.info("exam_sprint_report_warmup_started reportType={} resource={}", reportType, resource);
+        log.info("临考报告预热已开始 reportType={} resource={}", reportType, resource);
 
         try {
             JsonNode payload = loadPayload(resource);
@@ -95,7 +95,7 @@ public class ExamSprintReportWarmupRunner {
             long pdfDurationMs = elapsedMillis(pdfStartedNanos);
 
             log.info(
-                    "exam_sprint_report_warmup_succeeded reportType={} durationMs={} renderDurationMs={} pdfDurationMs={} htmlLength={} pdfByteLength={}",
+                    "临考报告预热成功 reportType={} durationMs={} renderDurationMs={} pdfDurationMs={} htmlLength={} pdfByteLength={}",
                     reportType,
                     elapsedMillis(startedNanos),
                     renderDurationMs,
@@ -104,7 +104,7 @@ public class ExamSprintReportWarmupRunner {
                     pdfBytes.length);
         } catch (Exception exception) {
             log.warn(
-                    "exam_sprint_report_warmup_failed reportType={} stage={} failureReason={} exceptionType={} durationMs={}",
+                    "临考报告预热失败 reportType={} stage={} failureReason={} exceptionType={} durationMs={}",
                     reportType,
                     stage,
                     exception.getClass().getSimpleName(),

+ 15 - 15
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java

@@ -117,7 +117,7 @@ class ExamSprintReportApplicationServiceTest {
         service.createOutlookReport(callerVocabularyPayload());
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_payload_received")
+                .contains("临考报告请求载荷已接收")
                 .contains("reportType=OUTLOOK")
                 .contains("mode=async")
                 .contains("StudentName")
@@ -145,7 +145,7 @@ class ExamSprintReportApplicationServiceTest {
                 .isEqualTo(ErrorCode.VALIDATION_ERROR);
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_payload_received")
+                .contains("临考报告请求载荷已接收")
                 .contains("reportType=OUTLOOK")
                 .contains("\"StudentWordsLatestSize\":10")
                 .doesNotContain("\"StudentWordsLatestSize\":\"forged-size\"");
@@ -402,7 +402,7 @@ class ExamSprintReportApplicationServiceTest {
         service.createAchievementReport(validAchievementPayload());
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_payload_received")
+                .contains("临考报告请求载荷已接收")
                 .contains("reportType=ACHIEVEMENT")
                 .contains("mode=async")
                 .contains("StudentName")
@@ -665,7 +665,7 @@ class ExamSprintReportApplicationServiceTest {
         service.createOutlookReportSync(validOutlookPayload());
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_sync_stage_completed")
+                .contains("临考报告同步准备阶段完成")
                 .contains("reportType=OUTLOOK")
                 .contains("stage=prepare_content")
                 .contains("durationMs=")
@@ -687,7 +687,7 @@ class ExamSprintReportApplicationServiceTest {
         service.createOutlookReportSync(validOutlookPayload());
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_validation_stage_completed")
+                .contains("临考报告参数校验阶段完成")
                 .contains("reportType=OUTLOOK")
                 .contains("stage=validate_payload")
                 .contains("durationMs=")
@@ -845,10 +845,10 @@ class ExamSprintReportApplicationServiceTest {
         assertThat(saved.failureReason()).doesNotContain("dispatcher unavailable");
         assertThat(repository.countByStatus(ReportGenerationStatus.PENDING)).isZero();
         assertThat(output.getAll())
-                .contains("exam_sprint_report_submitted")
+                .contains("临考报告生成任务已提交")
                 .contains("reportType=OUTLOOK")
                 .contains("mode=async")
-                .contains("exam_sprint_report_dispatch_failed")
+                .contains("临考报告生成任务派发失败")
                 .contains("failureReason=report_generation_dispatch_failed")
                 .contains("exceptionType=IllegalStateException")
                 .doesNotContain("task-executor-7 rejected")
@@ -901,7 +901,7 @@ class ExamSprintReportApplicationServiceTest {
         assertThat(storage.generatedKeys)
                 .containsExactly("exam-sprint-achievement-report-report-success.pdf");
         assertThat(output.getAll())
-                .contains("exam_sprint_report_query_completed")
+                .contains("临考报告查询完成")
                 .contains("reportId=report-success")
                 .contains("reportType=ACHIEVEMENT")
                 .contains("generationStatus=SUCCESS")
@@ -933,7 +933,7 @@ class ExamSprintReportApplicationServiceTest {
                 .isEqualTo(cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus.SUCCESS);
         assertThat(response.downloadUrl()).isNull();
         assertThat(output.getAll())
-                .contains("exam_sprint_report_download_url_generation_failed")
+                .contains("临考报告下载地址生成失败")
                 .contains("exceptionType=IllegalStateException")
                 .contains("storageObjectKey=exam-sprint-achievement-report-report-query-url-failure.pdf")
                 .doesNotContain("SENSITIVE_QUERY_URL_DO_NOT_LOG");
@@ -961,7 +961,7 @@ class ExamSprintReportApplicationServiceTest {
                 .isEqualTo(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_download_unavailable")
+                .contains("临考报告下载不可用")
                 .contains("reportId=report-expired")
                 .contains("generationStatus=EXPIRED")
                 .contains("reason=expired");
@@ -989,10 +989,10 @@ class ExamSprintReportApplicationServiceTest {
                 .isEqualTo(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_download_started")
+                .contains("临考报告下载已开始")
                 .contains("reportId=report-missing-content")
                 .contains("reportType=OUTLOOK")
-                .contains("exam_sprint_report_download_missing_storage_content")
+                .contains("临考报告下载缺少存储内容")
                 .contains("storageObjectKey=exam-sprint-outlook-report-report-missing-content.pdf");
     }
 
@@ -1019,7 +1019,7 @@ class ExamSprintReportApplicationServiceTest {
                 .isEqualTo(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_download_unavailable")
+                .contains("临考报告下载不可用")
                 .contains("reason=storage_download_failed")
                 .contains("exceptionType=IllegalStateException")
                 .doesNotContain("SENSITIVE_STORAGE_DOWNLOAD_DO_NOT_LOG");
@@ -1082,11 +1082,11 @@ class ExamSprintReportApplicationServiceTest {
                 .contains("first.pdf")
                 .contains("second.pdf");
         assertThat(output.getAll())
-                .contains("exam_sprint_report_cleanup_item_failed")
+                .contains("临考报告清理单项失败")
                 .contains("reportId=report-delete-fails")
                 .contains("storageObjectKey=first.pdf")
                 .contains("exceptionType=IllegalStateException")
-                .contains("exam_sprint_report_cleanup_completed")
+                .contains("临考报告清理完成")
                 .contains("scannedCount=2")
                 .contains("storageClearedCount=1")
                 .contains("markedExpiredCount=0")

+ 5 - 5
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java

@@ -75,7 +75,7 @@ class ExamSprintReportGenerationWorkerTest {
         worker.process("report-log-success");
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_generation_started")
+                .contains("临考报告生成已开始")
                 .contains("reportId=report-log-success")
                 .contains("reportType=OUTLOOK")
                 .contains("stage=render_html")
@@ -83,7 +83,7 @@ class ExamSprintReportGenerationWorkerTest {
                 .contains("pdfByteLength=")
                 .contains("stage=storage_upload")
                 .containsPattern("stage=storage_upload.*pdfByteLength=\\d+")
-                .contains("exam_sprint_report_generation_succeeded")
+                .contains("临考报告生成成功")
                 .contains("storageObjectKey=report-log-success-临考词汇突击潜力展望报告-20260101080000.pdf")
                 .doesNotContain("SENSITIVE_HTML_DO_NOT_LOG")
                 .doesNotContain("<html><body>SENSITIVE_HTML_DO_NOT_LOG</body></html>");
@@ -247,7 +247,7 @@ class ExamSprintReportGenerationWorkerTest {
         worker.process("report-log-failed");
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_generation_failed")
+                .contains("临考报告生成失败")
                 .contains("reportId=report-log-failed")
                 .contains("reportType=OUTLOOK")
                 .contains("stage=render_html")
@@ -277,7 +277,7 @@ class ExamSprintReportGenerationWorkerTest {
         assertThat(report.generationStatus()).isEqualTo(ReportGenerationStatus.FAILED);
         assertThat(report.failureReason()).isEqualTo("<html>SENSITIVE_FAILURE_DO_NOT_LOG</html>");
         assertThat(output.getAll())
-                .contains("exam_sprint_report_generation_failed")
+                .contains("临考报告生成失败")
                 .contains("failureReason=IllegalStateException")
                 .doesNotContain("SENSITIVE_FAILURE_DO_NOT_LOG")
                 .doesNotContain("<html>SENSITIVE_FAILURE_DO_NOT_LOG</html>");
@@ -301,7 +301,7 @@ class ExamSprintReportGenerationWorkerTest {
         assertThat(repository.findById("report-deleted")).isEmpty();
         assertThat(output.getAll())
                 .contains("reason=not_found_before_success_save")
-                .doesNotContain("exam_sprint_report_generation_failed");
+                .doesNotContain("临考报告生成失败");
     }
 
     private ExamSprintReportGenerationWorker createWorker(

+ 5 - 5
abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportWarmupRunnerTest.java

@@ -74,7 +74,7 @@ class ExamSprintReportWarmupRunnerTest {
 
         assertThat(events).isEmpty();
         assertThat(output.getAll())
-                .contains("exam_sprint_report_warmup_failed")
+                .contains("临考报告预热失败")
                 .contains("reportType=ALL")
                 .contains("stage=async_submit")
                 .contains("exceptionType=RejectedExecutionException")
@@ -118,8 +118,8 @@ class ExamSprintReportWarmupRunnerTest {
 
         assertThat(pdfGenerator.htmlInputs).containsExactly("outlook-html", "achievement-html");
         assertThat(output.getAll())
-                .contains("exam_sprint_report_warmup_started")
-                .contains("exam_sprint_report_warmup_succeeded")
+                .contains("临考报告预热已开始")
+                .contains("临考报告预热成功")
                 .contains("reportType=OUTLOOK")
                 .contains("reportType=ACHIEVEMENT")
                 .contains("durationMs=")
@@ -150,12 +150,12 @@ class ExamSprintReportWarmupRunnerTest {
         assertThat(achievementRenderer.renderedContents).singleElement().isInstanceOf(AchievementReportContent.class);
         assertThat(pdfGenerator.htmlInputs).containsExactly("achievement-html");
         assertThat(output.getAll())
-                .contains("exam_sprint_report_warmup_failed")
+                .contains("临考报告预热失败")
                 .contains("reportType=OUTLOOK")
                 .contains("stage=render_html")
                 .contains("exceptionType=IllegalStateException")
                 .contains("failureReason=IllegalStateException")
-                .contains("exam_sprint_report_warmup_succeeded")
+                .contains("临考报告预热成功")
                 .contains("reportType=ACHIEVEMENT")
                 .doesNotContain("SENSITIVE_WARMUP_FAILURE_DO_NOT_LOG")
                 .doesNotContain("StudentWordsLatest")

+ 34 - 1
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/DefaultPlaywrightPdfWorker.java

@@ -8,11 +8,16 @@ import com.microsoft.playwright.Playwright;
 import com.microsoft.playwright.options.Margin;
 import com.microsoft.playwright.options.Media;
 import com.microsoft.playwright.options.WaitUntilState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 
 final class DefaultPlaywrightPdfWorker implements PlaywrightPdfWorker {
 
+    private static final Logger log = LoggerFactory.getLogger(DefaultPlaywrightPdfWorker.class);
+
     private final Playwright playwright;
     private final Browser browser;
     private final double renderTimeoutMillis;
@@ -43,16 +48,32 @@ final class DefaultPlaywrightPdfWorker implements PlaywrightPdfWorker {
 
         BrowserContext context = null;
         try {
+            long stageStartedNanos = System.nanoTime();
             context = browser.newContext(new Browser.NewContextOptions().setLocale("zh-CN"));
+            logStageCompleted("创建浏览器上下文", stageStartedNanos);
             context.setDefaultTimeout(renderTimeoutMillis);
             context.setDefaultNavigationTimeout(renderTimeoutMillis);
+
+            stageStartedNanos = System.nanoTime();
             Page page = context.newPage();
+            logStageCompleted("创建页面", stageStartedNanos);
+
+            stageStartedNanos = System.nanoTime();
             page.emulateMedia(new Page.EmulateMediaOptions().setMedia(Media.PRINT));
+            logStageCompleted("设置打印媒体", stageStartedNanos);
+
+            stageStartedNanos = System.nanoTime();
             page.setContent(htmlContent, new Page.SetContentOptions()
                     .setWaitUntil(WaitUntilState.LOAD)
                     .setTimeout(renderTimeoutMillis));
+            logStageCompleted("设置HTML内容", stageStartedNanos);
+
+            stageStartedNanos = System.nanoTime();
             page.evaluate("() => document.fonts ? document.fonts.ready.then(() => true) : true");
-            return page.pdf(new Page.PdfOptions()
+            logStageCompleted("等待字体加载完成", stageStartedNanos);
+
+            stageStartedNanos = System.nanoTime();
+            byte[] pdfBytes = page.pdf(new Page.PdfOptions()
                     .setFormat("A4")
                     .setPrintBackground(true)
                     .setPreferCSSPageSize(true)
@@ -61,10 +82,14 @@ final class DefaultPlaywrightPdfWorker implements PlaywrightPdfWorker {
                             .setRight("0")
                             .setBottom("0")
                             .setLeft("0")));
+            logStageCompleted("生成PDF文件", stageStartedNanos);
+            return pdfBytes;
         } catch (Exception exception) {
             throw new IllegalStateException("Failed to generate PDF", exception);
         } finally {
+            long closeStartedNanos = System.nanoTime();
             closeQuietly(context);
+            logStageCompleted("关闭浏览器上下文", closeStartedNanos);
         }
     }
 
@@ -116,4 +141,12 @@ final class DefaultPlaywrightPdfWorker implements PlaywrightPdfWorker {
         } catch (Exception ignored) {
         }
     }
+
+    private void logStageCompleted(String stage, long startedNanos) {
+        log.info("PDF渲染阶段完成 stage={} durationMs={}", stage, elapsedMillis(startedNanos));
+    }
+
+    private long elapsedMillis(long startedNanos) {
+        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos);
+    }
 }

+ 12 - 12
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/PooledPlaywrightExamSprintReportPdfGenerator.java

@@ -47,7 +47,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
             }
             PoolState state = poolState();
             LOGGER.info(
-                    "exam_sprint_report_pdf_pool_initialized poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} borrowTimeoutMs={}",
+                    "PDF工作线程池已初始化 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} borrowTimeoutMs={}",
                     state.poolSize(),
                     state.availableWorkers(),
                     state.activeWorkers(),
@@ -72,7 +72,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
             success = true;
             PoolState state = poolState();
             LOGGER.info(
-                    "exam_sprint_report_pdf_worker_generation_completed poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} workerRenderMs={} pdfByteLength={}",
+                    "PDF工作线程生成完成 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} workerRenderMs={} pdfByteLength={}",
                     state.poolSize(),
                     state.availableWorkers(),
                     state.activeWorkers(),
@@ -83,7 +83,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
         } catch (RuntimeException | Error failure) {
             PoolState state = poolState();
             LOGGER.warn(
-                    "exam_sprint_report_pdf_worker_generation_failed poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} workerRenderMs={} exceptionType={}",
+                    "PDF工作线程生成失败 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} workerRenderMs={} exceptionType={}",
                     state.poolSize(),
                     state.availableWorkers(),
                     state.activeWorkers(),
@@ -114,7 +114,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
         closeWorkers(workersToClose);
         PoolState state = poolState();
         LOGGER.info(
-                "exam_sprint_report_pdf_pool_closed poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} closedWorkers={} remainingWorkers={}",
+                "PDF工作线程池已关闭 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} closedWorkers={} remainingWorkers={}",
                 state.poolSize(),
                 state.availableWorkers(),
                 state.activeWorkers(),
@@ -137,7 +137,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
                     long borrowWaitMs = elapsedMillis(borrowStartedNanos);
                     PoolState state = poolState();
                     LOGGER.warn(
-                            "exam_sprint_report_pdf_worker_borrow_timeout poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} borrowWaitMs={} borrowTimeoutMs={} exceptionType={}",
+                            "PDF工作线程借出超时 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} borrowWaitMs={} borrowTimeoutMs={} exceptionType={}",
                             state.poolSize(),
                             state.availableWorkers(),
                             state.activeWorkers(),
@@ -153,7 +153,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
                     ensureOpen();
                     PoolState state = poolState();
                     LOGGER.info(
-                            "exam_sprint_report_pdf_worker_borrowed poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} borrowWaitMs={}",
+                            "PDF工作线程已借出 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} borrowWaitMs={}",
                             state.poolSize(),
                             state.availableWorkers(),
                             state.activeWorkers(),
@@ -180,7 +180,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
             closeManagedWorker(worker);
             PoolState state = poolState();
             LOGGER.warn(
-                    "exam_sprint_report_pdf_worker_return_failed poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} reason=pool_full",
+                    "PDF工作线程归还失败 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} reason=pool_full",
                     state.poolSize(),
                     state.availableWorkers(),
                     state.activeWorkers(),
@@ -190,7 +190,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
         } else {
             PoolState state = poolState();
             LOGGER.info(
-                    "exam_sprint_report_pdf_worker_returned poolSize={} availableWorkers={} activeWorkers={} managedWorkers={}",
+                    "PDF工作线程已归还 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={}",
                     state.poolSize(),
                     state.availableWorkers(),
                     state.activeWorkers(),
@@ -212,7 +212,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
                     closeManagedWorker(replacementWorker);
                     PoolState state = poolState();
                     LOGGER.warn(
-                            "exam_sprint_report_pdf_worker_replacement_failed poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} exceptionType={} reason=pool_full",
+                            "PDF工作线程替换失败 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} exceptionType={} reason=pool_full",
                             state.poolSize(),
                             state.availableWorkers(),
                             state.activeWorkers(),
@@ -223,7 +223,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
                 } else {
                     PoolState state = poolState();
                     LOGGER.info(
-                            "exam_sprint_report_pdf_worker_replaced poolSize={} availableWorkers={} activeWorkers={} managedWorkers={}",
+                            "PDF工作线程已替换 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={}",
                             state.poolSize(),
                             state.availableWorkers(),
                             state.activeWorkers(),
@@ -235,7 +235,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
         } catch (RuntimeException | Error replacementFailure) {
             PoolState state = poolState();
             LOGGER.warn(
-                    "exam_sprint_report_pdf_worker_replacement_failed poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} exceptionType={}",
+                    "PDF工作线程替换失败 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} exceptionType={}",
                     state.poolSize(),
                     state.availableWorkers(),
                     state.activeWorkers(),
@@ -277,7 +277,7 @@ public class PooledPlaywrightExamSprintReportPdfGenerator implements ExamSprintR
         } catch (RuntimeException | Error exception) {
             PoolState state = poolState();
             LOGGER.warn(
-                    "exam_sprint_report_pdf_worker_close_failed poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} exceptionType={}",
+                    "PDF工作线程关闭失败 poolSize={} availableWorkers={} activeWorkers={} managedWorkers={} exceptionType={}",
                     state.poolSize(),
                     state.availableWorkers(),
                     state.activeWorkers(),

+ 4 - 4
abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorage.java

@@ -78,7 +78,7 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
         String blobName = normalizeSegment(fileName, "fileName");
         BlobClient blobClient = containerClient.getBlobClient(blobName);
         log.info(
-                "exam_sprint_report_azure_storage_upload_stage_completed reportId={} reportType={} stage=client_resolved storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
+                "临考报告Azure存储上传阶段完成 reportId={} reportType={} stage=client_resolved storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
                 reportId,
                 reportType,
                 blobName,
@@ -106,7 +106,7 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
             throw e;
         }
         log.info(
-                "exam_sprint_report_azure_storage_upload_stage_completed reportId={} reportType={} stage=upload_request_completed storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={} azureStatusCode={} azureRequestId={} azureClientRequestId={} azureETag={} azureLastModified={}",
+                "临考报告Azure存储上传阶段完成 reportId={} reportType={} stage=upload_request_completed storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={} azureStatusCode={} azureRequestId={} azureClientRequestId={} azureETag={} azureLastModified={}",
                 reportId,
                 reportType,
                 blobName,
@@ -120,7 +120,7 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
                 uploadResponse.getValue() == null ? null : uploadResponse.getValue().getETag(),
                 uploadResponse.getValue() == null ? null : uploadResponse.getValue().getLastModified());
         log.info(
-                "exam_sprint_report_azure_storage_upload_completed reportId={} reportType={} storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
+                "临考报告Azure存储上传完成 reportId={} reportType={} storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
                 reportId,
                 reportType,
                 blobName,
@@ -238,7 +238,7 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
             RuntimeException exception) {
         HttpHeaders headers = responseHeaders(exception);
         log.warn(
-                "exam_sprint_report_azure_storage_upload_failed reportId={} reportType={} storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={} exceptionType={} azureStatusCode={} azureRequestId={} azureClientRequestId={} azureErrorCode={}",
+                "临考报告Azure存储上传失败 reportId={} reportType={} storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={} exceptionType={} azureStatusCode={} azureRequestId={} azureClientRequestId={} azureErrorCode={}",
                 reportId,
                 reportType,
                 blobName,

+ 60 - 0
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/PlaywrightExamSprintReportPdfGeneratorTest.java

@@ -9,12 +9,16 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.microsoft.playwright.BrowserType;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.text.PDFTextStripper;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
+import org.slf4j.LoggerFactory;
 
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -107,6 +111,62 @@ class PlaywrightExamSprintReportPdfGeneratorTest {
         }
     }
 
+    @Test
+    void generateEmitsChineseStageTimingLogs() {
+        Logger logger = (Logger) LoggerFactory.getLogger(DefaultPlaywrightPdfWorker.class);
+        ListAppender<ILoggingEvent> appender = new ListAppender<>();
+        appender.start();
+        logger.addAppender(appender);
+
+        try {
+            byte[] pdfBytes = pdfGenerator.generate("""
+                    <html>
+                    <head><meta charset=\"UTF-8\"/></head>
+                    <body><p>PDF 阶段计时日志</p></body>
+                    </html>
+                    """);
+
+            assertPdfHeader(pdfBytes);
+        } finally {
+            logger.detachAppender(appender);
+            appender.stop();
+        }
+
+        List<String> messages = appender.list.stream()
+                .map(ILoggingEvent::getFormattedMessage)
+                .toList();
+
+        assertThat(messages).anySatisfy(message -> {
+            assertThat(message).contains("PDF渲染阶段完成");
+            assertThat(message).contains("stage=创建浏览器上下文");
+            assertThat(message).contains("durationMs=");
+        });
+        assertThat(messages).anySatisfy(message -> assertThat(message)
+                .contains("PDF渲染阶段完成")
+                .contains("stage=创建页面")
+                .contains("durationMs="));
+        assertThat(messages).anySatisfy(message -> assertThat(message)
+                .contains("PDF渲染阶段完成")
+                .contains("stage=设置打印媒体")
+                .contains("durationMs="));
+        assertThat(messages).anySatisfy(message -> assertThat(message)
+                .contains("PDF渲染阶段完成")
+                .contains("stage=设置HTML内容")
+                .contains("durationMs="));
+        assertThat(messages).anySatisfy(message -> assertThat(message)
+                .contains("PDF渲染阶段完成")
+                .contains("stage=等待字体加载完成")
+                .contains("durationMs="));
+        assertThat(messages).anySatisfy(message -> assertThat(message)
+                .contains("PDF渲染阶段完成")
+                .contains("stage=生成PDF文件")
+                .contains("durationMs="));
+        assertThat(messages).anySatisfy(message -> assertThat(message)
+                .contains("PDF渲染阶段完成")
+                .contains("stage=关闭浏览器上下文")
+                .contains("durationMs="));
+    }
+
     @Test
     void generateCreatesReadablePdfForOutlookReportTemplate() throws Exception {
         ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);

+ 9 - 9
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/pdf/PooledPlaywrightExamSprintReportPdfGeneratorTest.java

@@ -69,27 +69,27 @@ class PooledPlaywrightExamSprintReportPdfGeneratorTest {
                 .toList();
 
         assertThat(messages).anySatisfy(message -> {
-            assertThat(message).contains("exam_sprint_report_pdf_pool_initialized");
+            assertThat(message).contains("PDF工作线程池已初始化");
             assertThat(message).contains("poolSize=1");
             assertThat(message).contains("borrowTimeoutMs=100");
         });
         assertThat(messages).anySatisfy(message -> {
-            assertThat(message).contains("exam_sprint_report_pdf_worker_borrowed");
+            assertThat(message).contains("PDF工作线程已借出");
             assertThat(message).contains("borrowWaitMs=");
             assertThat(message).contains("availableWorkers=");
             assertThat(message).contains("activeWorkers=");
             assertThat(message).contains("managedWorkers=");
         });
         assertThat(messages).anySatisfy(message -> {
-            assertThat(message).contains("exam_sprint_report_pdf_worker_generation_completed");
+            assertThat(message).contains("PDF工作线程生成完成");
             assertThat(message).contains("workerRenderMs=");
             assertThat(message).contains("pdfByteLength=");
         });
         assertThat(messages).anySatisfy(message -> {
-            assertThat(message).contains("exam_sprint_report_pdf_worker_returned");
+            assertThat(message).contains("PDF工作线程已归还");
             assertThat(message).contains("availableWorkers=");
         });
-        assertThat(messages).anySatisfy(message -> assertThat(message).contains("exam_sprint_report_pdf_pool_closed"));
+        assertThat(messages).anySatisfy(message -> assertThat(message).contains("PDF工作线程池已关闭"));
     }
 
     @Test
@@ -192,7 +192,7 @@ class PooledPlaywrightExamSprintReportPdfGeneratorTest {
                 .toList();
 
         assertThat(messages).anySatisfy(message -> {
-            assertThat(message).contains("exam_sprint_report_pdf_worker_borrow_timeout");
+            assertThat(message).contains("PDF工作线程借出超时");
             assertThat(message).contains("poolSize=1");
             assertThat(message).contains("borrowTimeoutMs=50");
             assertThat(message).contains("borrowWaitMs=");
@@ -231,7 +231,7 @@ class PooledPlaywrightExamSprintReportPdfGeneratorTest {
                 .toList();
 
         assertThat(messages).anySatisfy(message -> {
-            assertThat(message).contains("exam_sprint_report_pdf_worker_generation_failed");
+            assertThat(message).contains("PDF工作线程生成失败");
             assertThat(message).contains("workerRenderMs=");
             assertThat(message).contains("exceptionType=RuntimeException");
             assertThat(message).contains("availableWorkers=");
@@ -239,7 +239,7 @@ class PooledPlaywrightExamSprintReportPdfGeneratorTest {
             assertThat(message).contains("managedWorkers=");
         });
         assertThat(messages).anySatisfy(message -> {
-            assertThat(message).contains("exam_sprint_report_pdf_worker_replaced");
+            assertThat(message).contains("PDF工作线程已替换");
             assertThat(message).contains("poolSize=1");
             assertThat(message).contains("availableWorkers=");
             assertThat(message).contains("activeWorkers=");
@@ -273,7 +273,7 @@ class PooledPlaywrightExamSprintReportPdfGeneratorTest {
                 .toList();
 
         assertThat(messages).anySatisfy(message -> {
-            assertThat(message).contains("exam_sprint_report_pdf_worker_replacement_failed");
+            assertThat(message).contains("PDF工作线程替换失败");
             assertThat(message).contains("exceptionType=RuntimeException");
             assertThat(message).contains("availableWorkers=");
             assertThat(message).contains("activeWorkers=");

+ 9 - 4
abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/AzureBlobExamSprintReportStorageTest.java

@@ -180,6 +180,8 @@ class AzureBlobExamSprintReportStorageTest {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobClient blobClient = mock(BlobClient.class);
         when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
+        when(blobClient.uploadWithResponse(argThat(options -> true), eq(Context.NONE)))
+                .thenReturn(successfulUploadResponse());
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 "report",
@@ -228,10 +230,10 @@ class AzureBlobExamSprintReportStorageTest {
                 Instant.parse("2026-01-10T00:00:00Z"));
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_azure_storage_upload_stage_completed")
+                .contains("临考报告Azure存储上传阶段完成")
                 .contains("stage=client_resolved")
                 .contains("stage=upload_request_completed")
-                .contains("exam_sprint_report_azure_storage_upload_completed")
+                .contains("临考报告Azure存储上传完成")
                 .contains("reportId=report-123")
                 .contains("reportType=OUTLOOK")
                 .contains("storageObjectKey=file.pdf")
@@ -254,8 +256,9 @@ class AzureBlobExamSprintReportStorageTest {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobClient blobClient = mock(BlobClient.class);
         when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
+        BlobStorageException uploadFailure = blobStorageException();
         when(blobClient.uploadWithResponse(argThat(options -> true), eq(Context.NONE)))
-                .thenThrow(blobStorageException());
+                .thenThrow(uploadFailure);
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 "report",
@@ -271,7 +274,7 @@ class AzureBlobExamSprintReportStorageTest {
                 .isInstanceOf(BlobStorageException.class);
 
         assertThat(output.getAll())
-                .contains("exam_sprint_report_azure_storage_upload_failed")
+                .contains("临考报告Azure存储上传失败")
                 .contains("reportId=report-123")
                 .contains("reportType=OUTLOOK")
                 .contains("storageObjectKey=file.pdf")
@@ -294,6 +297,8 @@ class AzureBlobExamSprintReportStorageTest {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobClient blobClient = mock(BlobClient.class);
         when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
+        when(blobClient.uploadWithResponse(argThat(options -> true), eq(Context.NONE)))
+                .thenReturn(successfulUploadResponse());
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 "report",