6 changed files with 1273 additions and 1 deletions
@ -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; |
||||
|
|
||||
|
/** |
||||
|
* 二维码生成通用接口 |
||||
|
* <p> |
||||
|
* 提供二维码生成(标准黑白 / 蓝色带 LOGO)、Base64 编码、二维码解析等通用能力。 |
||||
|
* 生成后自动上传到 OSS 并保存 zdxt_file 和 qr_code_generate_history 记录。 |
||||
|
* </p> |
||||
|
* |
||||
|
* @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<QrCodeGenerateHistoryVo> generate( |
||||
|
@NotBlank(message = "二维码内容不能为空") @RequestParam String content, |
||||
|
@NotBlank(message = "企业纳税识别号不能为空") @RequestParam String enterpriseNumber, |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId) { |
||||
|
QrCodeGenerateHistoryVo vo = QrCodeUtil.generateAndSave(content, enterpriseNumber, businessId); |
||||
|
return R.ok(vo); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成标准黑白二维码(可指定分类代码和尺寸),上传 OSS 并保存记录 |
||||
|
* |
||||
|
* @param content 二维码内容 |
||||
|
* @param enterpriseNumber 企业纳税识别号 |
||||
|
* @param businessId 关联的业务ID |
||||
|
* @param sortCode 分类代码(可为空) |
||||
|
* @param size 二维码尺寸(像素) |
||||
|
* @return 二维码生成历史记录 |
||||
|
*/ |
||||
|
@Log(title = "二维码生成", businessType = BusinessType.INSERT) |
||||
|
@RepeatSubmit() |
||||
|
@PostMapping("/generateWithSize") |
||||
|
public R<QrCodeGenerateHistoryVo> generateWithSize( |
||||
|
@NotBlank(message = "二维码内容不能为空") @RequestParam String content, |
||||
|
@NotBlank(message = "企业纳税识别号不能为空") @RequestParam String enterpriseNumber, |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId, |
||||
|
@RequestParam(required = false) String sortCode, |
||||
|
@NotNull(message = "二维码尺寸不能为空") @RequestParam Integer size) { |
||||
|
QrCodeGenerateHistoryVo vo = QrCodeUtil.generateAndSave(content, enterpriseNumber, businessId, sortCode, size); |
||||
|
return R.ok(vo); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成蓝色二维码(含默认 LOGO),上传 OSS 并保存记录 |
||||
|
* |
||||
|
* @param content 二维码内容 |
||||
|
* @param enterpriseNumber 企业纳税识别号 |
||||
|
* @param businessId 关联的业务ID |
||||
|
* @param size 二维码尺寸(像素) |
||||
|
* @return 二维码生成历史记录 |
||||
|
*/ |
||||
|
@Log(title = "蓝色二维码生成", businessType = BusinessType.INSERT) |
||||
|
@RepeatSubmit() |
||||
|
@PostMapping("/generateBlueWithLogo") |
||||
|
public R<QrCodeGenerateHistoryVo> generateBlueWithLogo( |
||||
|
@NotBlank(message = "二维码内容不能为空") @RequestParam String content, |
||||
|
@NotBlank(message = "企业纳税识别号不能为空") @RequestParam String enterpriseNumber, |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId, |
||||
|
@NotNull(message = "二维码尺寸不能为空") @RequestParam Integer size) { |
||||
|
QrCodeGenerateHistoryVo vo = QrCodeUtil.generateBlueWithLogoAndSave(content, enterpriseNumber, businessId, size); |
||||
|
return R.ok(vo); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成蓝色二维码(含默认 LOGO、可指定分类代码),上传 OSS 并保存记录 |
||||
|
* |
||||
|
* @param content 二维码内容 |
||||
|
* @param enterpriseNumber 企业纳税识别号 |
||||
|
* @param businessId 关联的业务ID |
||||
|
* @param sortCode 分类代码(可为空) |
||||
|
* @param size 二维码尺寸(像素) |
||||
|
* @return 二维码生成历史记录 |
||||
|
*/ |
||||
|
@Log(title = "蓝色二维码生成", businessType = BusinessType.INSERT) |
||||
|
@RepeatSubmit() |
||||
|
@PostMapping("/generateBlueWithLogoAndSort") |
||||
|
public R<QrCodeGenerateHistoryVo> generateBlueWithLogoAndSort( |
||||
|
@NotBlank(message = "二维码内容不能为空") @RequestParam String content, |
||||
|
@NotBlank(message = "企业纳税识别号不能为空") @RequestParam String enterpriseNumber, |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId, |
||||
|
@RequestParam(required = false) String sortCode, |
||||
|
@NotNull(message = "二维码尺寸不能为空") @RequestParam Integer size) { |
||||
|
QrCodeGenerateHistoryVo vo = QrCodeUtil.generateBlueWithLogoAndSave(content, enterpriseNumber, businessId, sortCode, size); |
||||
|
return R.ok(vo); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成标准黑白二维码的 Base64 编码字符串(含 data:image 前缀) |
||||
|
* |
||||
|
* @param content 二维码内容 |
||||
|
* @param size 二维码尺寸(像素) |
||||
|
* @return Base64 编码字符串 |
||||
|
*/ |
||||
|
@GetMapping("/base64") |
||||
|
public R<String> toBase64( |
||||
|
@NotBlank(message = "二维码内容不能为空") @RequestParam String content, |
||||
|
@NotNull(message = "二维码尺寸不能为空") @RequestParam Integer size) { |
||||
|
String base64 = QrCodeUtil.toBase64(content, size); |
||||
|
return R.ok(base64); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成蓝色二维码(含 LOGO)的 Base64 编码字符串 |
||||
|
* |
||||
|
* @param content 二维码内容 |
||||
|
* @param size 二维码尺寸(像素) |
||||
|
* @return Base64 编码字符串 |
||||
|
*/ |
||||
|
@GetMapping("/blueBase64WithLogo") |
||||
|
public R<String> toBlueBase64WithLogo( |
||||
|
@NotBlank(message = "二维码内容不能为空") @RequestParam String content, |
||||
|
@NotNull(message = "二维码尺寸不能为空") @RequestParam Integer size) { |
||||
|
String base64 = QrCodeUtil.toBlueBase64WithLogo(content, size); |
||||
|
return R.ok(base64); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解析上传的二维码图片,返回二维码内容 |
||||
|
* |
||||
|
* @param file 二维码图片文件 |
||||
|
* @return 解析出的文本内容 |
||||
|
*/ |
||||
|
@PostMapping("/decode") |
||||
|
public R<String> decode(@RequestParam("file") MultipartFile file) { |
||||
|
if (file == null || file.isEmpty()) { |
||||
|
return R.fail("上传的二维码图片不能为空"); |
||||
|
} |
||||
|
Path tempFile = null; |
||||
|
try { |
||||
|
// 将上传文件写入临时文件以供解析
|
||||
|
String originalFilename = file.getOriginalFilename(); |
||||
|
String suffix = (originalFilename != null && originalFilename.contains(".")) |
||||
|
? originalFilename.substring(originalFilename.lastIndexOf(".")) |
||||
|
: ".jpg"; |
||||
|
tempFile = Files.createTempFile("qr_decode_", suffix); |
||||
|
file.transferTo(tempFile.toFile()); |
||||
|
String result = QrCodeUtil.decodeQrCode(tempFile.toFile()); |
||||
|
return R.ok("解析成功", result); |
||||
|
} catch (IOException e) { |
||||
|
log.error("解析二维码失败: {}", e.getMessage(), e); |
||||
|
return R.fail("解析二维码失败: " + e.getMessage()); |
||||
|
} finally { |
||||
|
// 清理临时文件
|
||||
|
if (tempFile != null) { |
||||
|
try { |
||||
|
Files.deleteIfExists(tempFile); |
||||
|
} catch (IOException ignored) { |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,193 @@ |
|||||
|
package com.zdxt.enforcementcode.controller; |
||||
|
|
||||
|
import com.zdxt.enforcementcode.domain.vo.ZdxtFileVo; |
||||
|
import com.zdxt.enforcementcode.util.ZdxtFileUtil; |
||||
|
import jakarta.validation.constraints.NotBlank; |
||||
|
import jakarta.validation.constraints.NotEmpty; |
||||
|
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.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 附件管理通用接口 |
||||
|
* <p> |
||||
|
* 封装文件上传(本地存储)、查询、删除等通用能力, |
||||
|
* 基于 {@link ZdxtFileUtil} 工具类,文件保存到本地磁盘并记录到 zdxt_file 表。 |
||||
|
* </p> |
||||
|
* |
||||
|
* @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<ZdxtFileVo> upload( |
||||
|
@RequestParam("file") MultipartFile file, |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId) { |
||||
|
ZdxtFileVo vo = ZdxtFileUtil.uploadFile(file, businessId); |
||||
|
return R.ok(vo); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 上传单个文件,带分类代码(自动获取当前登录用户信息) |
||||
|
* |
||||
|
* @param file 上传的文件 |
||||
|
* @param businessId 业务ID |
||||
|
* @param sortCode 分类代码 |
||||
|
* @return 附件视图对象 |
||||
|
*/ |
||||
|
@Log(title = "附件上传", businessType = BusinessType.INSERT) |
||||
|
@RepeatSubmit() |
||||
|
@PostMapping("/uploadWithSort") |
||||
|
public R<ZdxtFileVo> uploadWithSort( |
||||
|
@RequestParam("file") MultipartFile file, |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId, |
||||
|
@RequestParam(required = false) String sortCode) { |
||||
|
ZdxtFileVo vo = ZdxtFileUtil.uploadFile(file, businessId, sortCode); |
||||
|
return R.ok(vo); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 批量上传文件 |
||||
|
* |
||||
|
* @param files 文件数组 |
||||
|
* @param businessId 业务ID |
||||
|
* @return 附件视图对象列表 |
||||
|
*/ |
||||
|
@Log(title = "附件批量上传", businessType = BusinessType.INSERT) |
||||
|
@RepeatSubmit() |
||||
|
@PostMapping("/uploadBatch") |
||||
|
public R<List<ZdxtFileVo>> uploadBatch( |
||||
|
@RequestParam("files") MultipartFile[] files, |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId) { |
||||
|
List<ZdxtFileVo> list = ZdxtFileUtil.uploadFiles(files, businessId); |
||||
|
return R.ok(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 批量上传文件,带分类代码 |
||||
|
* |
||||
|
* @param files 文件数组 |
||||
|
* @param businessId 业务ID |
||||
|
* @param sortCode 分类代码 |
||||
|
* @return 附件视图对象列表 |
||||
|
*/ |
||||
|
@Log(title = "附件批量上传", businessType = BusinessType.INSERT) |
||||
|
@RepeatSubmit() |
||||
|
@PostMapping("/uploadBatchWithSort") |
||||
|
public R<List<ZdxtFileVo>> uploadBatchWithSort( |
||||
|
@RequestParam("files") MultipartFile[] files, |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId, |
||||
|
@RequestParam(required = false) String sortCode) { |
||||
|
List<ZdxtFileVo> list = ZdxtFileUtil.uploadFiles(files, businessId, sortCode); |
||||
|
return R.ok(list); |
||||
|
} |
||||
|
|
||||
|
// ==================== 查询接口 ====================
|
||||
|
|
||||
|
/** |
||||
|
* 根据附件ID查询附件信息 |
||||
|
* |
||||
|
* @param guid 附件主键ID |
||||
|
* @return 附件视图对象 |
||||
|
*/ |
||||
|
@GetMapping("/{guid}") |
||||
|
public R<ZdxtFileVo> getByGuid( |
||||
|
@NotBlank(message = "附件ID不能为空") @PathVariable String guid) { |
||||
|
ZdxtFileVo vo = ZdxtFileUtil.queryByGuid(guid); |
||||
|
return R.ok(vo); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据业务ID查询附件列表 |
||||
|
* |
||||
|
* @param businessId 业务ID |
||||
|
* @return 附件列表 |
||||
|
*/ |
||||
|
@GetMapping("/listByBusinessId") |
||||
|
public R<List<ZdxtFileVo>> listByBusinessId( |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId) { |
||||
|
List<ZdxtFileVo> list = ZdxtFileUtil.queryByBusinessId(businessId); |
||||
|
return R.ok(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据业务ID和分类代码查询附件列表 |
||||
|
* |
||||
|
* @param businessId 业务ID |
||||
|
* @param sortCode 分类代码 |
||||
|
* @return 附件列表 |
||||
|
*/ |
||||
|
@GetMapping("/listByBusinessIdAndSort") |
||||
|
public R<List<ZdxtFileVo>> listByBusinessIdAndSort( |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId, |
||||
|
@NotBlank(message = "分类代码不能为空") @RequestParam String sortCode) { |
||||
|
List<ZdxtFileVo> list = ZdxtFileUtil.queryByBusinessIdAndSortCode(businessId, sortCode); |
||||
|
return R.ok(list); |
||||
|
} |
||||
|
|
||||
|
// ==================== 删除接口 ====================
|
||||
|
|
||||
|
/** |
||||
|
* 根据附件ID删除单个附件(同时删除本地文件) |
||||
|
* |
||||
|
* @param guid 附件主键ID |
||||
|
* @return 是否删除成功 |
||||
|
*/ |
||||
|
@Log(title = "附件删除", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{guid}") |
||||
|
public R<Void> deleteByGuid( |
||||
|
@NotBlank(message = "附件ID不能为空") @PathVariable String guid) { |
||||
|
return toAjax(ZdxtFileUtil.deleteByGuid(guid)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据附件ID批量删除附件(同时删除本地文件) |
||||
|
* |
||||
|
* @param guids 附件主键ID数组 |
||||
|
* @return 是否删除成功 |
||||
|
*/ |
||||
|
@Log(title = "附件批量删除", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/batch/{guids}") |
||||
|
public R<Void> deleteByGuids( |
||||
|
@NotEmpty(message = "附件ID不能为空") @PathVariable String[] guids) { |
||||
|
return toAjax(ZdxtFileUtil.deleteByGuids(List.of(guids))); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据业务ID删除所有关联附件(同时删除本地文件) |
||||
|
* |
||||
|
* @param businessId 业务ID |
||||
|
* @return 是否删除成功 |
||||
|
*/ |
||||
|
@Log(title = "按业务ID删除附件", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/deleteByBusinessId") |
||||
|
public R<Void> deleteByBusinessId( |
||||
|
@NotBlank(message = "业务ID不能为空") @RequestParam String businessId) { |
||||
|
return toAjax(ZdxtFileUtil.deleteByBusinessId(businessId)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,404 @@ |
|||||
|
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 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 javax.imageio.ImageIO; |
||||
|
import java.awt.image.BufferedImage; |
||||
|
import java.io.ByteArrayOutputStream; |
||||
|
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.*; |
||||
|
|
||||
|
/** |
||||
|
* QrCodeController 二维码通用接口测试 |
||||
|
* <p> |
||||
|
* 分为三类: |
||||
|
* 1. 参数校验测试(不依赖 OSS/DB) |
||||
|
* 2. Base64 / 解析测试(纯计算,不依赖 OSS/DB) |
||||
|
* 3. 生成+保存集成测试(需要 Spring 上下文、OSS 和数据库) |
||||
|
* </p> |
||||
|
* |
||||
|
* @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("蓝色二维码(含分类)生成接口测试成功"); |
||||
|
} |
||||
|
} |
||||
@ -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 附件管理通用接口测试 |
||||
|
* <p> |
||||
|
* 分为四类: |
||||
|
* 1. 上传接口测试(单文件 / 批量 / 带分类代码) |
||||
|
* 2. 查询接口测试(按 guid / 按 businessId / 按 businessId + sortCode) |
||||
|
* 3. 删除接口测试(单个 / 批量 / 按 businessId) |
||||
|
* 4. 参数校验测试 |
||||
|
* </p> |
||||
|
* |
||||
|
* @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; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue