diff --git a/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/DictController.java b/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/DictController.java index 97a0b7c6..562f34b9 100644 --- a/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/DictController.java +++ b/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/DictController.java @@ -16,7 +16,7 @@ import java.util.List; */ @RequiredArgsConstructor @RestController -@RequestMapping("/app") +@RequestMapping("/common/dict") public class DictController { private final ISysDictDataService dictDataService; diff --git a/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/QrCodeController.java b/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/QrCodeController.java new file mode 100644 index 00000000..392e8008 --- /dev/null +++ b/ruoyi-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/QrCodeController.java @@ -0,0 +1,190 @@ +package com.zdxt.enforcementcode.controller; + +import com.zdxt.enforcementcode.domain.vo.QrCodeGenerateHistoryVo; +import com.zdxt.enforcementcode.util.QrCodeUtil; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.R; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.common.web.core.BaseController; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * 二维码生成通用接口 + *
+ * 提供二维码生成(标准黑白 / 蓝色带 LOGO)、Base64 编码、二维码解析等通用能力。 + * 生成后自动上传到 OSS 并保存 zdxt_file 和 qr_code_generate_history 记录。 + *
+ * + * @author zdxt + */ +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/common/qrcode") +public class QrCodeController extends BaseController { + + /** + * 生成标准黑白二维码(默认尺寸 300px),上传 OSS 并保存记录 + * + * @param content 二维码内容(通常为 URL) + * @param enterpriseNumber 企业纳税识别号 + * @param businessId 关联的业务ID + * @return 二维码生成历史记录 + */ + @Log(title = "二维码生成", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/generate") + public R+ * 封装文件上传(本地存储)、查询、删除等通用能力, + * 基于 {@link ZdxtFileUtil} 工具类,文件保存到本地磁盘并记录到 zdxt_file 表。 + *
+ * + * @author zdxt + */ +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/common/file") +public class ZdxtFileUtilController extends BaseController { + + // ==================== 上传接口 ==================== + + /** + * 上传单个文件(自动获取当前登录用户信息) + * + * @param file 上传的文件 + * @param businessId 业务ID(关联的业务主键) + * @return 附件视图对象 + */ + @Log(title = "附件上传", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/upload") + public R+ * 分为三类: + * 1. 参数校验测试(不依赖 OSS/DB) + * 2. Base64 / 解析测试(纯计算,不依赖 OSS/DB) + * 3. 生成+保存集成测试(需要 Spring 上下文、OSS 和数据库) + *
+ * + * @author zdxt + */ +@SpringBootTest(classes = ZdxtAdminApplication.class) +@AutoConfigureMockMvc +@DisplayName("QrCodeController 二维码通用接口测试") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class QrCodeControllerTest { + + @Autowired + private MockMvc mockMvc; + + private static final String BASE_URL = "/common/qrcode"; + private static final String TEST_CONTENT = "https://www.example.com/qr?id=ctrl_test"; + private static final String TEST_ENTERPRISE_NUMBER = "91110000CTRL001"; + private static final String TEST_BUSINESS_ID = "TEST_CTRL_QR_001"; + private static final String TEST_CLIENT_ID = "e5cd7e4891bf95d1d19206ce24a7b32e"; + + /** 登录后获取的 token */ + private String token; + + @BeforeEach + public void setUp() { + // 初始化 Sa-Token 上下文(非 Web 环境下 StpUtil.login 需要) + SaTokenContextMockUtil.setMockContext(); + + // 设置模拟请求上下文(UserActionListener 需要从 Request 中获取 User-Agent) + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockRequest)); + + // 构造测试用登录用户 + LoginUser loginUser = new LoginUser(); + loginUser.setUserId(1L); + loginUser.setUserType("sys_user"); + loginUser.setTenantId("000000"); + loginUser.setUsername("test_admin"); + loginUser.setNickname("测试管理员"); + loginUser.setDeptId("103"); + loginUser.setDeptName("测试部门"); + loginUser.setRolePermission(new HashSet<>()); + loginUser.setMenuPermission(new HashSet<>()); + + // 模拟登录并获取 token + SaLoginParameter param = new SaLoginParameter(); + param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); + LoginHelper.login(loginUser, param); + token = StpUtil.getTokenValue(); + } + + @AfterEach + public void tearDown() { + try { + StpUtil.logout(); + } catch (Exception ignored) { + } + RequestContextHolder.resetRequestAttributes(); + SaTokenContextMockUtil.clearContext(); + } + + // ==================== 参数校验测试 ==================== + + @DisplayName("生成二维码 - 缺少 content 参数应返回失败") + @Test + @Order(1) + public void testGenerateMissingContent() throws Exception { + mockMvc.perform(post(BASE_URL + "/generate") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("enterpriseNumber", TEST_ENTERPRISE_NUMBER) + .param("businessId", TEST_BUSINESS_ID) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("生成二维码 - 缺少 enterpriseNumber 参数应返回失败") + @Test + @Order(1) + public void testGenerateMissingEnterpriseNumber() throws Exception { + mockMvc.perform(post(BASE_URL + "/generate") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("businessId", TEST_BUSINESS_ID) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("生成二维码 - 缺少 businessId 参数应返回失败") + @Test + @Order(1) + public void testGenerateMissingBusinessId() throws Exception { + mockMvc.perform(post(BASE_URL + "/generate") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("enterpriseNumber", TEST_ENTERPRISE_NUMBER) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("自定义尺寸生成 - 缺少 size 参数应返回失败") + @Test + @Order(1) + public void testGenerateWithSizeMissingSize() throws Exception { + mockMvc.perform(post(BASE_URL + "/generateWithSize") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("enterpriseNumber", TEST_ENTERPRISE_NUMBER) + .param("businessId", TEST_BUSINESS_ID) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("蓝色二维码生成 - 缺少 size 参数应返回失败") + @Test + @Order(1) + public void testGenerateBlueWithLogoMissingSize() throws Exception { + mockMvc.perform(post(BASE_URL + "/generateBlueWithLogo") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("enterpriseNumber", TEST_ENTERPRISE_NUMBER) + .param("businessId", TEST_BUSINESS_ID) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + // ==================== Base64 接口测试 ==================== + + @DisplayName("Base64 接口 - 缺少 content 参数应返回失败") + @Test + @Order(2) + public void testBase64MissingContent() throws Exception { + mockMvc.perform(get(BASE_URL + "/base64") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("size", "300")) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("Base64 接口 - 缺少 size 参数应返回失败") + @Test + @Order(2) + public void testBase64MissingSize() throws Exception { + mockMvc.perform(get(BASE_URL + "/base64") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("Base64 接口 - 正常生成标准二维码 Base64") + @Test + @Order(2) + public void testBase64Success() throws Exception { + MvcResult result = mockMvc.perform(get(BASE_URL + "/base64") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("size", "300")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isNotEmpty()) + .andReturn(); + + String responseBody = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + Assertions.assertTrue(responseBody.contains("data:image/jpeg;base64,"), + "返回数据应包含 Base64 前缀"); + System.out.println("Base64 接口测试成功,响应长度: " + responseBody.length()); + } + + @DisplayName("蓝色 Base64 接口 - 正常生成蓝色二维码 Base64") + @Test + @Order(2) + public void testBlueBase64WithLogoSuccess() throws Exception { + MvcResult result = mockMvc.perform(get(BASE_URL + "/blueBase64WithLogo") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("size", "300")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isNotEmpty()) + .andReturn(); + + String responseBody = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + Assertions.assertTrue(responseBody.contains("data:image/jpeg;base64,"), + "返回数据应包含 Base64 前缀"); + System.out.println("蓝色 Base64 接口测试成功"); + } + + // ==================== 二维码解析接口测试 ==================== + + @DisplayName("解析接口 - 上传空文件应返回失败") + @Test + @Order(3) + public void testDecodeEmptyFile() throws Exception { + MockMultipartFile emptyFile = new MockMultipartFile( + "file", "empty.jpg", "image/jpeg", new byte[0] + ); + + mockMvc.perform(multipart(BASE_URL + "/decode").file(emptyFile) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(500)); + } + + @DisplayName("解析接口 - 正常解析二维码图片") + @Test + @Order(3) + public void testDecodeSuccess() throws Exception { + // 通过 ZXing 生成一张真实的二维码图片作为测试输入 + com.google.zxing.common.BitMatrix bitMatrix = new com.google.zxing.MultiFormatWriter() + .encode(TEST_CONTENT, com.google.zxing.BarcodeFormat.QR_CODE, 300, 300); + BufferedImage image = new BufferedImage(300, 300, BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < 300; x++) { + for (int y = 0; y < 300; y++) { + image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); + } + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, "jpg", baos); + byte[] imageBytes = baos.toByteArray(); + + MockMultipartFile qrFile = new MockMultipartFile( + "file", "qrcode.jpg", "image/jpeg", imageBytes + ); + + mockMvc.perform(multipart(BASE_URL + "/decode").file(qrFile) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").value(TEST_CONTENT)); + + System.out.println("二维码解析接口测试成功"); + } + + @DisplayName("解析接口 - 上传非二维码图片应返回失败") + @Test + @Order(3) + public void testDecodeNonQrImage() throws Exception { + // 生成一张纯白色图片(非二维码) + BufferedImage whiteImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < 100; x++) { + for (int y = 0; y < 100; y++) { + whiteImage.setRGB(x, y, 0xFFFFFFFF); + } + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(whiteImage, "jpg", baos); + + MockMultipartFile notQrFile = new MockMultipartFile( + "file", "not_qr.jpg", "image/jpeg", baos.toByteArray() + ); + + mockMvc.perform(multipart(BASE_URL + "/decode").file(notQrFile) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(500)); + + System.out.println("非二维码图片解析正确返回失败"); + } + + // ==================== 生成+保存集成测试(需 OSS + DB) ==================== + + @DisplayName("生成标准二维码并保存 - 参数完整") + @Test + @Order(5) + public void testGenerateAndSave() throws Exception { + mockMvc.perform(post(BASE_URL + "/generate") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("enterpriseNumber", TEST_ENTERPRISE_NUMBER) + .param("businessId", TEST_BUSINESS_ID) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isNotEmpty()) + .andExpect(jsonPath("$.data.id").isNotEmpty()) + .andExpect(jsonPath("$.data.qrCodeUrl").isNotEmpty()) + .andExpect(jsonPath("$.data.enterpriseNumber").value(TEST_ENTERPRISE_NUMBER)); + + System.out.println("标准二维码生成并保存接口测试成功"); + } + + @DisplayName("生成自定义尺寸二维码并保存 - 带分类代码") + @Test + @Order(5) + public void testGenerateWithSizeAndSave() throws Exception { + mockMvc.perform(post(BASE_URL + "/generateWithSize") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("enterpriseNumber", TEST_ENTERPRISE_NUMBER) + .param("businessId", TEST_BUSINESS_ID) + .param("sortCode", "QR_SORT_CTRL") + .param("size", "400") + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isNotEmpty()); + + System.out.println("自定义尺寸二维码生成接口测试成功"); + } + + @DisplayName("生成蓝色二维码并保存") + @Test + @Order(5) + public void testGenerateBlueWithLogoAndSave() throws Exception { + mockMvc.perform(post(BASE_URL + "/generateBlueWithLogo") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("enterpriseNumber", TEST_ENTERPRISE_NUMBER) + .param("businessId", TEST_BUSINESS_ID) + .param("size", "300") + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.qrCodeUrl").isNotEmpty()); + + System.out.println("蓝色二维码生成并保存接口测试成功"); + } + + @DisplayName("生成蓝色二维码(含分类代码)并保存") + @Test + @Order(5) + public void testGenerateBlueWithLogoAndSortAndSave() throws Exception { + mockMvc.perform(post(BASE_URL + "/generateBlueWithLogoAndSort") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("content", TEST_CONTENT) + .param("enterpriseNumber", TEST_ENTERPRISE_NUMBER) + .param("businessId", TEST_BUSINESS_ID) + .param("sortCode", "QR_BLUE_CTRL") + .param("size", "500") + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isNotEmpty()); + + System.out.println("蓝色二维码(含分类)生成接口测试成功"); + } +} diff --git a/zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/ZdxtFileUtilControllerTest.java b/zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/ZdxtFileUtilControllerTest.java new file mode 100644 index 00000000..7cdf5157 --- /dev/null +++ b/zdxt-web-server/zdxt-admin-web-server/src/test/java/com/zdxt/rest/test/ZdxtFileUtilControllerTest.java @@ -0,0 +1,482 @@ +package com.zdxt.rest.test; + +import cn.dev33.satoken.context.mock.SaTokenContextMockUtil; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.StpUtil; +import com.zdxt.enforcementcode.domain.vo.ZdxtFileVo; +import com.zdxt.enforcementcode.util.ZdxtFileUtil; +import org.dromara.ZdxtAdminApplication; +import org.dromara.common.core.domain.model.LoginUser; +import org.dromara.common.satoken.utils.LoginHelper; +import org.junit.jupiter.api.*; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * ZdxtFileUtilController 附件管理通用接口测试 + *+ * 分为四类: + * 1. 上传接口测试(单文件 / 批量 / 带分类代码) + * 2. 查询接口测试(按 guid / 按 businessId / 按 businessId + sortCode) + * 3. 删除接口测试(单个 / 批量 / 按 businessId) + * 4. 参数校验测试 + *
+ * + * @author zdxt + */ +@SpringBootTest(classes = ZdxtAdminApplication.class) +@AutoConfigureMockMvc +@DisplayName("ZdxtFileUtilController 附件管理通用接口测试") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ZdxtFileUtilControllerTest { + + @Autowired + private MockMvc mockMvc; + + private static final String BASE_URL = "/common/file"; + private static final String TEST_BUSINESS_ID = "TEST_CTRL_FILE_001"; + private static final String TEST_SORT_CODE = "CTRL_SORT"; + private static final String TEST_CLIENT_ID = "e5cd7e4891bf95d1d19206ce24a7b32e"; + + /** 登录后获取的 token */ + private String token; + + /** 记录上传后的 guid,供后续查询/删除使用 */ + private static String uploadedGuid; + + @BeforeEach + public void setUp() { + // 初始化 Sa-Token 上下文(非 Web 环境下 StpUtil.login 需要) + SaTokenContextMockUtil.setMockContext(); + + // 设置模拟请求上下文(UserActionListener 需要从 Request 中获取 User-Agent) + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockRequest)); + + // 构造测试用登录用户 + LoginUser loginUser = new LoginUser(); + loginUser.setUserId(1L); + loginUser.setUserType("sys_user"); + loginUser.setTenantId("000000"); + loginUser.setUsername("test_admin"); + loginUser.setNickname("测试管理员"); + loginUser.setDeptId("103"); + loginUser.setDeptName("测试部门"); + loginUser.setRolePermission(new HashSet<>()); + loginUser.setMenuPermission(new HashSet<>()); + + // 模拟登录并获取 token + SaLoginParameter param = new SaLoginParameter(); + param.setExtra(LoginHelper.CLIENT_KEY, TEST_CLIENT_ID); + LoginHelper.login(loginUser, param); + token = StpUtil.getTokenValue(); + } + + @AfterEach + public void tearDown() { + try { + StpUtil.logout(); + } catch (Exception ignored) { + } + RequestContextHolder.resetRequestAttributes(); + SaTokenContextMockUtil.clearContext(); + } + + // ==================== 上传接口测试 ==================== + + @DisplayName("上传单个文件 - 正常上传") + @Test + @Order(1) + public void testUploadFile() throws Exception { + MockMultipartFile mockFile = new MockMultipartFile( + "file", "controller-test.txt", "text/plain", + "Controller upload test content.".getBytes(StandardCharsets.UTF_8) + ); + + MvcResult result = mockMvc.perform(multipart(BASE_URL + "/upload") + .file(mockFile) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isNotEmpty()) + .andExpect(jsonPath("$.data.guid").isNotEmpty()) + .andExpect(jsonPath("$.data.businessId").value(TEST_BUSINESS_ID)) + .andExpect(jsonPath("$.data.fileName").value("controller-test.txt")) + .andExpect(jsonPath("$.data.fileSuffix").value(".txt")) + .andExpect(jsonPath("$.data.fileType").value("txt")) + .andExpect(jsonPath("$.data.uploadPath").isNotEmpty()) + .andReturn(); + + // 从响应中提取 guid 供后续测试使用 + String responseBody = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + // 简单解析 guid(集成测试中直接使用工具查询更可靠) + if (responseBody.contains("\"guid\"")) { + int start = responseBody.indexOf("\"guid\":\"") + 8; + int end = responseBody.indexOf("\"", start); + uploadedGuid = responseBody.substring(start, end); + } + System.out.println("单文件上传成功: guid=" + uploadedGuid); + } + + @DisplayName("上传单个文件(带分类代码)- 正常上传") + @Test + @Order(1) + public void testUploadFileWithSort() throws Exception { + MockMultipartFile mockFile = new MockMultipartFile( + "file", "sorted-file.pdf", "application/pdf", + "PDF content simulation".getBytes(StandardCharsets.UTF_8) + ); + + mockMvc.perform(multipart(BASE_URL + "/uploadWithSort") + .file(mockFile) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID) + .param("sortCode", TEST_SORT_CODE)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.guid").isNotEmpty()) + .andExpect(jsonPath("$.data.fileSuffix").value(".pdf")); + + System.out.println("带分类代码的文件上传成功"); + } + + @DisplayName("批量上传文件 - 正常上传") + @Test + @Order(1) + public void testUploadBatch() throws Exception { + MockMultipartFile file1 = new MockMultipartFile( + "files", "batch1.txt", "text/plain", "batch content 1".getBytes() + ); + MockMultipartFile file2 = new MockMultipartFile( + "files", "batch2.txt", "text/plain", "batch content 2".getBytes() + ); + + mockMvc.perform(multipart(BASE_URL + "/uploadBatch") + .file(file1) + .file(file2) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(2)); + + System.out.println("批量上传成功"); + } + + @DisplayName("批量上传文件(带分类代码)- 正常上传") + @Test + @Order(1) + public void testUploadBatchWithSort() throws Exception { + MockMultipartFile file1 = new MockMultipartFile( + "files", "batch_sort1.txt", "text/plain", "sort batch 1".getBytes() + ); + MockMultipartFile file2 = new MockMultipartFile( + "files", "batch_sort2.txt", "text/plain", "sort batch 2".getBytes() + ); + + mockMvc.perform(multipart(BASE_URL + "/uploadBatchWithSort") + .file(file1) + .file(file2) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID) + .param("sortCode", TEST_SORT_CODE)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isArray()); + + System.out.println("带分类代码的批量上传成功"); + } + + // ==================== 参数校验测试 ==================== + + @DisplayName("上传文件 - 缺少 businessId 应返回失败") + @Test + @Order(1) + public void testUploadMissingBusinessId() throws Exception { + MockMultipartFile mockFile = new MockMultipartFile( + "file", "test.txt", "text/plain", "test".getBytes() + ); + + mockMvc.perform(multipart(BASE_URL + "/upload") + .file(mockFile) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("上传文件 - 空文件应返回失败") + @Test + @Order(1) + public void testUploadEmptyFile() throws Exception { + MockMultipartFile emptyFile = new MockMultipartFile( + "file", "empty.txt", "text/plain", new byte[0] + ); + + mockMvc.perform(multipart(BASE_URL + "/upload") + .file(emptyFile) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(500)); + + System.out.println("空文件上传正确返回失败"); + } + + @DisplayName("上传文件 - 无后缀文件名应返回失败") + @Test + @Order(1) + public void testUploadFileWithoutSuffix() throws Exception { + MockMultipartFile noSuffixFile = new MockMultipartFile( + "file", "noextension", "application/octet-stream", "data".getBytes() + ); + + mockMvc.perform(multipart(BASE_URL + "/upload") + .file(noSuffixFile) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(500)); + + System.out.println("无后缀文件上传正确返回失败"); + } + + // ==================== 查询接口测试 ==================== + + @DisplayName("根据 guid 查询附件信息") + @Test + @Order(2) + public void testGetByGuid() throws Exception { + if (uploadedGuid == null) { + System.out.println("跳过: 依赖上传测试先执行"); + return; + } + + mockMvc.perform(get(BASE_URL + "/{guid}", uploadedGuid) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.guid").value(uploadedGuid)); + + System.out.println("按 guid 查询成功: " + uploadedGuid); + } + + @DisplayName("根据 businessId 查询附件列表") + @Test + @Order(2) + public void testListByBusinessId() throws Exception { + mockMvc.perform(get(BASE_URL + "/listByBusinessId") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isArray()); + + System.out.println("按 businessId 查询成功"); + } + + @DisplayName("根据 businessId 和 sortCode 查询附件列表") + @Test + @Order(2) + public void testListByBusinessIdAndSort() throws Exception { + mockMvc.perform(get(BASE_URL + "/listByBusinessIdAndSort") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID) + .param("sortCode", TEST_SORT_CODE)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isArray()); + + System.out.println("按 businessId + sortCode 查询成功"); + } + + @DisplayName("查询附件列表 - 缺少 businessId 应返回失败") + @Test + @Order(2) + public void testListByBusinessIdMissingParam() throws Exception { + mockMvc.perform(get(BASE_URL + "/listByBusinessId") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @DisplayName("查询附件列表 - 缺少 sortCode 应返回失败") + @Test + @Order(2) + public void testListByBusinessIdAndSortMissingSortCode() throws Exception { + mockMvc.perform(get(BASE_URL + "/listByBusinessIdAndSort") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + // ==================== 删除接口测试 ==================== + + @DisplayName("根据 guid 删除附件") + @Test + @Order(3) + public void testDeleteByGuid() throws Exception { + // 先上传一个文件用于删除 + MockMultipartFile mockFile = new MockMultipartFile( + "file", "to-delete.txt", "text/plain", "delete me".getBytes() + ); + MvcResult uploadResult = mockMvc.perform(multipart(BASE_URL + "/upload") + .file(mockFile) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", "TEST_DEL_BIZ")) + .andExpect(status().isOk()) + .andReturn(); + + String body = uploadResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + String guidToDelete = null; + if (body.contains("\"guid\"")) { + int start = body.indexOf("\"guid\":\"") + 8; + int end = body.indexOf("\"", start); + guidToDelete = body.substring(start, end); + } + Assertions.assertNotNull(guidToDelete, "上传后应有 guid"); + + // 执行删除 + mockMvc.perform(delete(BASE_URL + "/{guid}", guidToDelete) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + System.out.println("按 guid 删除成功: " + guidToDelete); + } + + @DisplayName("批量删除附件") + @Test + @Order(3) + public void testDeleteByGuids() throws Exception { + // 先上传两个文件 + MockMultipartFile file1 = new MockMultipartFile( + "file", "del-batch1.txt", "text/plain", "batch del 1".getBytes() + ); + MockMultipartFile file2 = new MockMultipartFile( + "file", "del-batch2.txt", "text/plain", "batch del 2".getBytes() + ); + + MvcResult r1 = mockMvc.perform(multipart(BASE_URL + "/upload") + .file(file1) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", "TEST_BATCH_DEL")) + .andExpect(status().isOk()).andReturn(); + MvcResult r2 = mockMvc.perform(multipart(BASE_URL + "/upload") + .file(file2) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", "TEST_BATCH_DEL")) + .andExpect(status().isOk()).andReturn(); + + String body1 = r1.getResponse().getContentAsString(StandardCharsets.UTF_8); + String body2 = r2.getResponse().getContentAsString(StandardCharsets.UTF_8); + String guid1 = extractGuid(body1); + String guid2 = extractGuid(body2); + + Assertions.assertNotNull(guid1); + Assertions.assertNotNull(guid2); + + // 批量删除 + mockMvc.perform(delete(BASE_URL + "/batch/{guids}", guid1 + "," + guid2) + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + System.out.println("批量删除成功: " + guid1 + ", " + guid2); + } + + @DisplayName("按 businessId 删除所有关联附件") + @Test + @Order(4) + public void testDeleteByBusinessId() throws Exception { + mockMvc.perform(delete(BASE_URL + "/deleteByBusinessId") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + // 验证删除后查询为空 + mockMvc.perform(get(BASE_URL + "/listByBusinessId") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID) + .param("businessId", TEST_BUSINESS_ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(0)); + + System.out.println("按 businessId 删除所有附件成功"); + } + + @DisplayName("按 businessId 删除 - 缺少参数应返回失败") + @Test + @Order(4) + public void testDeleteByBusinessIdMissingParam() throws Exception { + mockMvc.perform(delete(BASE_URL + "/deleteByBusinessId") + .header("Authorization", "Bearer " + token) + .header("clientid", TEST_CLIENT_ID)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + // ==================== 工具方法 ==================== + + /** + * 从 JSON 响应中简单提取 guid 值 + */ + private String extractGuid(String responseBody) { + if (responseBody != null && responseBody.contains("\"guid\"")) { + int start = responseBody.indexOf("\"guid\":\"") + 8; + int end = responseBody.indexOf("\"", start); + return responseBody.substring(start, end); + } + return null; + } +}