Procházet zdrojové kódy

fix(临考突击报告): 修正 Azure Blob 报告对象路径

金逸霄 před 6 dny
rodič
revize
aa05b78b50

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

@@ -118,7 +118,7 @@ public class ExamSprintReportProperties {
 
 
     public static class Storage {
     public static class Storage {
         private String type = "memory";
         private String type = "memory";
-        private String containerName = "reports";
+        private String containerName = "report";
         private String downloadUrlPrefix;
         private String downloadUrlPrefix;
         private String connectionString;
         private String connectionString;
         private String endpoint;
         private String endpoint;

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

@@ -46,7 +46,7 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
 
 
     @Autowired
     @Autowired
     public AzureBlobExamSprintReportStorage(
     public AzureBlobExamSprintReportStorage(
-            @Value("${ability.exam-sprint.report.storage.container-name:reports}") String containerName,
+            @Value("${ability.exam-sprint.report.storage.container-name:report}") String containerName,
             @Value("${ability.exam-sprint.report.storage.connection-string:}") String connectionString,
             @Value("${ability.exam-sprint.report.storage.connection-string:}") String connectionString,
             @Value("${ability.exam-sprint.report.storage.endpoint:}") String endpoint,
             @Value("${ability.exam-sprint.report.storage.endpoint:}") String endpoint,
             @Value("${ability.exam-sprint.report.storage.account-name:}") String accountName,
             @Value("${ability.exam-sprint.report.storage.account-name:}") String accountName,
@@ -75,7 +75,7 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
             byte[] pdfBytes,
             byte[] pdfBytes,
             Instant expiresAt) {
             Instant expiresAt) {
         long uploadStartedNanos = System.nanoTime();
         long uploadStartedNanos = System.nanoTime();
-        String blobName = normalizeSegment(fileName, "fileName");
+        String blobName = blobName(reportId, fileName);
         BlobClient blobClient = containerClient.getBlobClient(blobName);
         BlobClient blobClient = containerClient.getBlobClient(blobName);
         log.info(
         log.info(
                 "exam_sprint_report_azure_storage_upload_stage_completed reportId={} reportType={} stage=client_resolved storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
                 "exam_sprint_report_azure_storage_upload_stage_completed reportId={} reportType={} stage=client_resolved storageObjectKey={} blobName={} fileName={} pdfByteLength={} durationMs={}",
@@ -163,6 +163,12 @@ public class AzureBlobExamSprintReportStorage implements ExamSprintReportStorage
         return storageObjectKey.substring(storageObjectKey.lastIndexOf('/') + 1);
         return storageObjectKey.substring(storageObjectKey.lastIndexOf('/') + 1);
     }
     }
 
 
+    private String blobName(String reportId, String fileName) {
+        return normalizeSegment(reportId, "reportId")
+                + "/"
+                + normalizeSegment(fileName, "fileName");
+    }
+
     private static String normalizeDownloadUrlPrefix(String value) {
     private static String normalizeDownloadUrlPrefix(String value) {
         if (!hasText(value)) {
         if (!hasText(value)) {
             throw new IllegalStateException("Azure storage download-url-prefix is incomplete");
             throw new IllegalStateException("Azure storage download-url-prefix is incomplete");

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

@@ -70,7 +70,7 @@ class AzureBlobExamSprintReportStorageTest {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 FIXED_CLOCK);
                 FIXED_CLOCK);
 
 
@@ -79,7 +79,7 @@ class AzureBlobExamSprintReportStorageTest {
                 Duration.ofMinutes(15));
                 Duration.ofMinutes(15));
 
 
         assertThat(downloadUrl).isEqualTo(URI.create(
         assertThat(downloadUrl).isEqualTo(URI.create(
-                "https://dcjxbtest.blob.core.chinacloudapi.cn/reports/exam-sprint-outlook-report-report-123.pdf"));
+                "https://dcjxbtest.blob.core.chinacloudapi.cn/report/report-123/exam-sprint-outlook-report-report-123.pdf"));
         verifyNoInteractions(containerClient);
         verifyNoInteractions(containerClient);
     }
     }
 
 
@@ -88,7 +88,7 @@ class AzureBlobExamSprintReportStorageTest {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "/reports/",
+                "/report/",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn/",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn/",
                 FIXED_CLOCK);
                 FIXED_CLOCK);
 
 
@@ -97,26 +97,26 @@ class AzureBlobExamSprintReportStorageTest {
                 Duration.ofMinutes(15));
                 Duration.ofMinutes(15));
 
 
         assertThat(downloadUrl).isEqualTo(URI.create(
         assertThat(downloadUrl).isEqualTo(URI.create(
-                "https://dcjxbtest.blob.core.chinacloudapi.cn/reports/file.pdf"));
+                "https://dcjxbtest.blob.core.chinacloudapi.cn/report/report-123/file.pdf"));
     }
     }
 
 
     @Test
     @Test
-    void generateDownloadUrlEncodesFileNameWithoutReportScopedPath() {
+    void generateDownloadUrlEncodesReportScopedFileNameWithoutReportTypePath() {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 FIXED_CLOCK);
                 FIXED_CLOCK);
 
 
         URI downloadUrl = storage.generateDownloadUrl(
         URI downloadUrl = storage.generateDownloadUrl(
-                "John Doe-报告 #1?.pdf",
+                "report-123/John Doe-报告 #1?.pdf",
                 Duration.ofMinutes(15));
                 Duration.ofMinutes(15));
 
 
         assertThat(downloadUrl.toString()).isEqualTo(
         assertThat(downloadUrl.toString()).isEqualTo(
-                "https://dcjxbtest.blob.core.chinacloudapi.cn/reports/John%20Doe-报告%20%231%3F.pdf");
+                "https://dcjxbtest.blob.core.chinacloudapi.cn/report/report-123/John%20Doe-报告%20%231%3F.pdf");
         assertThat(downloadUrl.getRawPath()).isEqualTo(
         assertThat(downloadUrl.getRawPath()).isEqualTo(
-                "/reports/John%20Doe-报告%20%231%3F.pdf");
+                "/report/report-123/John%20Doe-报告%20%231%3F.pdf");
         assertThat(downloadUrl.getRawQuery()).isNull();
         assertThat(downloadUrl.getRawQuery()).isNull();
         assertThat(downloadUrl.getRawFragment()).isNull();
         assertThat(downloadUrl.getRawFragment()).isNull();
     }
     }
@@ -126,7 +126,7 @@ class AzureBlobExamSprintReportStorageTest {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 FIXED_CLOCK);
                 FIXED_CLOCK);
 
 
@@ -140,7 +140,7 @@ class AzureBlobExamSprintReportStorageTest {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 FIXED_CLOCK);
                 FIXED_CLOCK);
 
 
@@ -155,7 +155,7 @@ class AzureBlobExamSprintReportStorageTest {
 
 
         assertThatThrownBy(() -> new AzureBlobExamSprintReportStorage(
         assertThatThrownBy(() -> new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 "///",
                 "///",
                 FIXED_CLOCK))
                 FIXED_CLOCK))
                 .isInstanceOf(IllegalStateException.class)
                 .isInstanceOf(IllegalStateException.class)
@@ -168,7 +168,7 @@ class AzureBlobExamSprintReportStorageTest {
 
 
         assertThatThrownBy(() -> new AzureBlobExamSprintReportStorage(
         assertThatThrownBy(() -> new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 " / ",
                 " / ",
                 FIXED_CLOCK))
                 FIXED_CLOCK))
                 .isInstanceOf(IllegalStateException.class)
                 .isInstanceOf(IllegalStateException.class)
@@ -176,13 +176,13 @@ class AzureBlobExamSprintReportStorageTest {
     }
     }
 
 
     @Test
     @Test
-    void uploadUsesFileNameAsBlobNameAndSendsHeadersAndMetadataInUploadRequest() {
+    void uploadUsesReportIdAndFileNameAsBlobNameAndSendsHeadersAndMetadataInUploadRequest() {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobClient blobClient = mock(BlobClient.class);
         BlobClient blobClient = mock(BlobClient.class);
         when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
         when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 FIXED_CLOCK);
                 FIXED_CLOCK);
 
 
@@ -193,9 +193,9 @@ class AzureBlobExamSprintReportStorageTest {
                 new byte[]{1, 2, 3},
                 new byte[]{1, 2, 3},
                 Instant.parse("2026-01-10T00:00:00Z"));
                 Instant.parse("2026-01-10T00:00:00Z"));
 
 
-        assertThat(storedFile.storageObjectKey()).isEqualTo("file.pdf");
+        assertThat(storedFile.storageObjectKey()).isEqualTo("report-123/file.pdf");
         assertThat(storedFile.fileName()).isEqualTo("file.pdf");
         assertThat(storedFile.fileName()).isEqualTo("file.pdf");
-        verify(containerClient).getBlobClient("file.pdf");
+        verify(containerClient).getBlobClient("report-123/file.pdf");
         verify(blobClient).uploadWithResponse(argThat(options ->
         verify(blobClient).uploadWithResponse(argThat(options ->
                         options.getLength() == 3
                         options.getLength() == 3
                                 && options.getHeaders() != null
                                 && options.getHeaders() != null
@@ -216,7 +216,7 @@ class AzureBlobExamSprintReportStorageTest {
                 .thenReturn(successfulUploadResponse());
                 .thenReturn(successfulUploadResponse());
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 FIXED_CLOCK);
                 FIXED_CLOCK);
 
 
@@ -234,8 +234,8 @@ class AzureBlobExamSprintReportStorageTest {
                 .contains("exam_sprint_report_azure_storage_upload_completed")
                 .contains("exam_sprint_report_azure_storage_upload_completed")
                 .contains("reportId=report-123")
                 .contains("reportId=report-123")
                 .contains("reportType=OUTLOOK")
                 .contains("reportType=OUTLOOK")
-                .contains("storageObjectKey=file.pdf")
-                .contains("blobName=file.pdf")
+                .contains("storageObjectKey=report-123/file.pdf")
+                .contains("blobName=report-123/file.pdf")
                 .contains("fileName=file.pdf")
                 .contains("fileName=file.pdf")
                 .contains("pdfByteLength=3")
                 .contains("pdfByteLength=3")
                 .contains("durationMs=")
                 .contains("durationMs=")
@@ -258,7 +258,7 @@ class AzureBlobExamSprintReportStorageTest {
                 .thenThrow(blobStorageException());
                 .thenThrow(blobStorageException());
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 FIXED_CLOCK);
                 FIXED_CLOCK);
 
 
@@ -274,8 +274,8 @@ class AzureBlobExamSprintReportStorageTest {
                 .contains("exam_sprint_report_azure_storage_upload_failed")
                 .contains("exam_sprint_report_azure_storage_upload_failed")
                 .contains("reportId=report-123")
                 .contains("reportId=report-123")
                 .contains("reportType=OUTLOOK")
                 .contains("reportType=OUTLOOK")
-                .contains("storageObjectKey=file.pdf")
-                .contains("blobName=file.pdf")
+                .contains("storageObjectKey=report-123/file.pdf")
+                .contains("blobName=report-123/file.pdf")
                 .contains("fileName=file.pdf")
                 .contains("fileName=file.pdf")
                 .contains("pdfByteLength=3")
                 .contains("pdfByteLength=3")
                 .contains("durationMs=")
                 .contains("durationMs=")
@@ -290,13 +290,13 @@ class AzureBlobExamSprintReportStorageTest {
     }
     }
 
 
     @Test
     @Test
-    void uploadUsesDisplayFileNameAsTheStorageObjectKey() {
+    void uploadUsesReportIdAndDisplayFileNameAsTheStorageObjectKey() {
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobContainerClient containerClient = mock(BlobContainerClient.class);
         BlobClient blobClient = mock(BlobClient.class);
         BlobClient blobClient = mock(BlobClient.class);
         when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
         when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
         AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
                 containerClient,
                 containerClient,
-                "reports",
+                "report",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 "https://dcjxbtest.blob.core.chinacloudapi.cn",
                 FIXED_CLOCK);
                 FIXED_CLOCK);
 
 
@@ -313,8 +313,9 @@ class AzureBlobExamSprintReportStorageTest {
                 new byte[]{2},
                 new byte[]{2},
                 Instant.parse("2026-01-10T00:00:00Z"));
                 Instant.parse("2026-01-10T00:00:00Z"));
 
 
-        assertThat(firstStoredFile.storageObjectKey()).isEqualTo("same-display-name.pdf");
-        assertThat(secondStoredFile.storageObjectKey()).isEqualTo("same-display-name.pdf");
+        assertThat(firstStoredFile.storageObjectKey()).isEqualTo("report-123/same-display-name.pdf");
+        assertThat(secondStoredFile.storageObjectKey()).isEqualTo("report-456/same-display-name.pdf");
+        assertThat(firstStoredFile.storageObjectKey()).isNotEqualTo(secondStoredFile.storageObjectKey());
         assertThat(firstStoredFile.fileName()).isEqualTo("same-display-name.pdf");
         assertThat(firstStoredFile.fileName()).isEqualTo("same-display-name.pdf");
         assertThat(secondStoredFile.fileName()).isEqualTo("same-display-name.pdf");
         assertThat(secondStoredFile.fileName()).isEqualTo("same-display-name.pdf");
     }
     }
@@ -330,7 +331,7 @@ class AzureBlobExamSprintReportStorageTest {
                 true,
                 true,
                 null);
                 null);
         return new SimpleResponse<>(
         return new SimpleResponse<>(
-                new HttpRequest(HttpMethod.PUT, "https://example.test/reports/file.pdf"),
+                new HttpRequest(HttpMethod.PUT, "https://example.test/report/report-123/file.pdf"),
                 201,
                 201,
                 headers,
                 headers,
                 blockBlobItem);
                 blockBlobItem);
@@ -345,7 +346,7 @@ class AzureBlobExamSprintReportStorageTest {
                 .set("x-ms-error-code", "ServerBusy"));
                 .set("x-ms-error-code", "ServerBusy"));
         when(response.getHeaderValue(HttpHeaderName.X_MS_REQUEST_ID)).thenReturn("azure-request-failed");
         when(response.getHeaderValue(HttpHeaderName.X_MS_REQUEST_ID)).thenReturn("azure-request-failed");
         when(response.getHeaderValue(HttpHeaderName.X_MS_CLIENT_REQUEST_ID)).thenReturn("client-request-failed");
         when(response.getHeaderValue(HttpHeaderName.X_MS_CLIENT_REQUEST_ID)).thenReturn("client-request-failed");
-        when(response.getRequest()).thenReturn(new HttpRequest(HttpMethod.PUT, new URL("https://example.test/reports/file.pdf")));
+        when(response.getRequest()).thenReturn(new HttpRequest(HttpMethod.PUT, new URL("https://example.test/report/report-123/file.pdf")));
         return new BlobStorageException("ServerBusy", response, null);
         return new BlobStorageException("ServerBusy", response, null);
     }
     }
 }
 }

+ 1 - 1
ability-center-runtime/src/main/resources/application.yml

@@ -6,7 +6,7 @@ ability:
       cleanup-interval-ms: 600000
       cleanup-interval-ms: 600000
       storage:
       storage:
         type: memory
         type: memory
-        container-name: reports
+        container-name: report
         connection-string:
         connection-string:
         endpoint:
         endpoint:
         account-name:
         account-name:

+ 6 - 6
ability-center-runtime/src/test/java/cn/yunzhixue/ability/center/examsprint/adapter/http/ExamSprintReportControllerWebMvcTest.java

@@ -124,7 +124,7 @@ class ExamSprintReportControllerWebMvcTest {
                 Instant.parse("2026-01-01T00:00:00Z"),
                 Instant.parse("2026-01-01T00:00:00Z"),
                 Instant.parse("2026-01-01T00:01:00Z"),
                 Instant.parse("2026-01-01T00:01:00Z"),
                 Instant.parse("2026-01-02T00:00:00Z"),
                 Instant.parse("2026-01-02T00:00:00Z"),
-                "https://dcjxbtest.blob.core.chinacloudapi.cn/reports/exam-sprint-outlook-report-report-sync-001.pdf"));
+                "https://dcjxbtest.blob.core.chinacloudapi.cn/report/report-sync-001/exam-sprint-outlook-report-report-sync-001.pdf"));
 
 
         String requestJson = requestJson("requests/exam-sprint-outlook-report-request.json");
         String requestJson = requestJson("requests/exam-sprint-outlook-report-request.json");
 
 
@@ -134,7 +134,7 @@ class ExamSprintReportControllerWebMvcTest {
                 .andExpect(status().isOk())
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.reportType").value("OUTLOOK"))
                 .andExpect(jsonPath("$.data.reportType").value("OUTLOOK"))
                 .andExpect(jsonPath("$.data.generationStatus").value("SUCCESS"))
                 .andExpect(jsonPath("$.data.generationStatus").value("SUCCESS"))
-                .andExpect(jsonPath("$.data.downloadUrl").value("https://dcjxbtest.blob.core.chinacloudapi.cn/reports/exam-sprint-outlook-report-report-sync-001.pdf"));
+                .andExpect(jsonPath("$.data.downloadUrl").value("https://dcjxbtest.blob.core.chinacloudapi.cn/report/report-sync-001/exam-sprint-outlook-report-report-sync-001.pdf"));
 
 
         verify(applicationService).createOutlookReportSync(any());
         verify(applicationService).createOutlookReportSync(any());
     }
     }
@@ -149,7 +149,7 @@ class ExamSprintReportControllerWebMvcTest {
                 Instant.parse("2026-01-01T00:00:00Z"),
                 Instant.parse("2026-01-01T00:00:00Z"),
                 Instant.parse("2026-01-01T00:01:00Z"),
                 Instant.parse("2026-01-01T00:01:00Z"),
                 Instant.parse("2026-01-02T00:00:00Z"),
                 Instant.parse("2026-01-02T00:00:00Z"),
-                "https://dcjxbtest.blob.core.chinacloudapi.cn/reports/exam-sprint-achievement-report-report-sync-002.pdf"));
+                "https://dcjxbtest.blob.core.chinacloudapi.cn/report/report-sync-002/exam-sprint-achievement-report-report-sync-002.pdf"));
 
 
         String requestJson = requestJson("requests/exam-sprint-achievement-report-request.json");
         String requestJson = requestJson("requests/exam-sprint-achievement-report-request.json");
 
 
@@ -159,7 +159,7 @@ class ExamSprintReportControllerWebMvcTest {
                 .andExpect(status().isOk())
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.reportType").value("ACHIEVEMENT"))
                 .andExpect(jsonPath("$.data.reportType").value("ACHIEVEMENT"))
                 .andExpect(jsonPath("$.data.generationStatus").value("SUCCESS"))
                 .andExpect(jsonPath("$.data.generationStatus").value("SUCCESS"))
-                .andExpect(jsonPath("$.data.downloadUrl").value("https://dcjxbtest.blob.core.chinacloudapi.cn/reports/exam-sprint-achievement-report-report-sync-002.pdf"));
+                .andExpect(jsonPath("$.data.downloadUrl").value("https://dcjxbtest.blob.core.chinacloudapi.cn/report/report-sync-002/exam-sprint-achievement-report-report-sync-002.pdf"));
 
 
         verify(applicationService).createAchievementReportSync(any());
         verify(applicationService).createAchievementReportSync(any());
     }
     }
@@ -214,7 +214,7 @@ class ExamSprintReportControllerWebMvcTest {
                 Instant.parse("2026-01-01T00:00:00Z"),
                 Instant.parse("2026-01-01T00:00:00Z"),
                 Instant.parse("2026-01-01T00:05:00Z"),
                 Instant.parse("2026-01-01T00:05:00Z"),
                 Instant.parse("2026-01-02T00:00:00Z"),
                 Instant.parse("2026-01-02T00:00:00Z"),
-                "https://dcjxbtest.blob.core.chinacloudapi.cn/reports/exam-sprint-outlook-report-report-001.pdf",
+                "https://dcjxbtest.blob.core.chinacloudapi.cn/report/report-001/exam-sprint-outlook-report-report-001.pdf",
                 null));
                 null));
         given(applicationService.downloadReport("report-001")).willReturn(new ReportDownloadContent(
         given(applicationService.downloadReport("report-001")).willReturn(new ReportDownloadContent(
                 "exam-sprint-outlook-report-report-001.pdf",
                 "exam-sprint-outlook-report-report-001.pdf",
@@ -224,7 +224,7 @@ class ExamSprintReportControllerWebMvcTest {
         mockMvc.perform(get("/api/exam-sprint/reports/{reportId}", "report-001"))
         mockMvc.perform(get("/api/exam-sprint/reports/{reportId}", "report-001"))
                 .andExpect(status().isOk())
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.data.reportId").value("report-001"))
                 .andExpect(jsonPath("$.data.reportId").value("report-001"))
-                .andExpect(jsonPath("$.data.downloadUrl").value("https://dcjxbtest.blob.core.chinacloudapi.cn/reports/exam-sprint-outlook-report-report-001.pdf"));
+                .andExpect(jsonPath("$.data.downloadUrl").value("https://dcjxbtest.blob.core.chinacloudapi.cn/report/report-001/exam-sprint-outlook-report-report-001.pdf"));
 
 
         mockMvc.perform(get("/api/exam-sprint/reports/{reportId}/download", "report-001"))
         mockMvc.perform(get("/api/exam-sprint/reports/{reportId}/download", "report-001"))
                 .andExpect(status().isOk())
                 .andExpect(status().isOk())