代码:

src/main/java/com/weiyu/util/BeanConvertUtils.java

package com.weiyu.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.weiyu.annotation.Debounce;
import com.weiyu.enumeration.CapitalInfoStateEnum;
import com.weiyu.exception.ResourceNotFoundException;
import com.weiyu.model.*;
import com.weiyu.service.CapitalInfoService;
import com.weiyu.util.BeanConvertUtils;
import com.weiyu.util.FileUtils;
import com.weiyu.util.SecurityUtils;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.StringEscapeUtils;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;

/**
 * 资金信息控制器
 */
@RestController
@RequestMapping({"/capital/info", "/capital/options"})
@Slf4j
@RequiredArgsConstructor
public class CapitalInfoController {

    private final CapitalInfoService capitalInfoService;

    /**
     * 获取资金信息列表
     *
     * @param queryDTO 查询对象
     * @return {@link Result}<{@link List}<{@link CapitalInfoRespones}>>
     */
    @PostMapping("/list")
    public Result<List<CapitalInfoRespones>> queryList(@RequestBody @Valid CapitalInfoQueryRequest queryDTO) {

        String endpoint = "/capital/info/list";
        String method = "queryList";

        log.info("【资金信息】查询资金信息列表,{},{},queryDTO = {}", endpoint, method, SecurityUtils.safeForLog(queryDTO));

        // 1、基本校验已由 @Valid 完成,Spring会自动执行以下验证:
        //    a. 检查@Pattern注解(格式验证)
        //    b. 调用@AssertTrue注解的方法(业务逻辑验证)

        // 2、处理业务逻辑校验,检查空条件
        if (isAllQueryParamsEmpty(queryDTO)) {
            log.warn("【资金信息】至少需要一个查询条件,{},{}", endpoint, method);
            return Result.error("请至少输入一个查询条件");
        }

        // 3、处理业务逻辑校验 - 日期范围
        String dateRangeError = validateDateRange(queryDTO.getCreateDateRange());
        if (dateRangeError != null) {
            return Result.error(dateRangeError);
        }

        try {
            List<CapitalInfoDTO> dtos = capitalInfoService.queryList(queryDTO);

            // DTO -> VO
            List<CapitalInfoRespones> vos = BeanConvertUtils.convertList(dtos, CapitalInfoRespones.class);

            return Result.success(vos);

        } catch (Exception e) {
            log.error("【资金信息】查询资金信息列表失败,{},{}", endpoint, method, e);
            return Result.error("查询资金信息列表失败");
        }
    }

    /**
     * 获取资金信息分页
     *
     * @param queryDTO 查询对象
     * @return {@link Result}&lt;{@link PageResult}&lt;{@link CapitalInfoRespones}&gt;&gt;
     */
    @PostMapping("/page")
    public Result<PageResult<CapitalInfoRespones>> queryPage(@RequestBody @Valid CapitalInfoQueryRequest queryDTO) {

        String endpoint = "/capital/info/page";
        String method = "queryPage";

        log.info("【资金信息】查询资金信息分页,{},{},queryDTO = {}", endpoint, method, SecurityUtils.safeForLog(queryDTO));

        // 1、基本校验已由 @Valid 完成,Spring会自动执行以下验证:
        //    a. 检查@Pattern注解(格式验证)
        //    b. 调用@AssertTrue注解的方法(业务逻辑验证)

        // 处理校验结果,由全局异常 【处理方法参数验证异常(MethodArgumentNotValidException)】 处理校验结果

        // 2、处理业务逻辑校验,检查空条件
        if (isAllQueryParamsEmpty(queryDTO)) {
            log.warn("【资金信息】 至少需要一个查询条件,{},{}", endpoint, method);
            return Result.error("请至少输入一个查询条件");
        }

        // 3、处理业务逻辑校验 - 日期范围
        String dateRangeError = validateDateRange(queryDTO.getCreateDateRange());
        if (dateRangeError != null) {
            return Result.error(dateRangeError);
        }

        try {
            PageResult<CapitalInfoDTO> dtoPage = capitalInfoService.queryPage(queryDTO);

            // DTO -> VO
            PageResult<CapitalInfoRespones> voPage = BeanConvertUtils.convertPage(dtoPage, CapitalInfoRespones.class);

            return Result.success(voPage);

        } catch (Exception e) {
            log.error("【资金信息】查询资金信息分页失败,{},{}", endpoint, method, e);
            return Result.error("查询资金信息分页失败");
        }
    }

    /**
     * 通过关键字获取资金信息列表
     *
     * @param keyword 关键字
     * @return {@link Result}&lt;{@link List}&lt;{@link CapitalInfoRespones}&gt;&gt;
     */
    @GetMapping("/list-keyword")
    public Result<List<CapitalInfoRespones>> queryListByKeyword(@RequestParam @NotBlank String keyword) {

        String endpoint = "/capital/info/list-keyword";
        String method = "queryListByKeyword";

        log.info("【资金信息】通过关键字获取资金信息列表,{},{},keyword = {}",
                endpoint, method, SecurityUtils.safeForLog(keyword));

        // 检查空条件
        if (!StringUtils.hasText(keyword)) {
            log.warn("【资金信息】搜索关键字不能为空,{},{}", endpoint, method);
            return Result.error("搜索关键字不能为空");
        }

        try {
            List<CapitalInfoDTO> dtos = capitalInfoService.queryListByKeyword(keyword);

            // DTO -> VO
            List<CapitalInfoRespones> vos = BeanConvertUtils.convertList(dtos, CapitalInfoRespones.class);

            return Result.success(vos);

        } catch (Exception e) {
            log.error("【资金信息】通过关键字获取资金信息列表失败,{},{}", endpoint, method, e);
            return Result.error("通过关键字获取资金信息列表失败");
        }
    }

    /**
     * 通过关键字获取资金信息分页
     *
     * @param keyword 关键字
     * @return {@link Result}&lt;{@link PageResult}&lt;{@link CapitalInfoRespones}&gt;&gt;
     */
    @GetMapping("/page-keyword")
    public Result<PageResult<CapitalInfoRespones>> queryPageByKeyword(
            @RequestParam(value = "current", defaultValue = "1") Integer current,
            @RequestParam(value = "size", defaultValue = "20") Integer size,
            @RequestParam("keyword") @NotBlank String keyword
    ) {

        String endpoint = "/capital/info/page-keyword";
        String method = "queryPageByKeyword";

        log.info("【资金信息】通过关键字获取资金信息分页,{},{},current = {},size = {},keyword = {}",
                endpoint, method, current, size, SecurityUtils.safeForLog(keyword));

        // 检查空条件
        if (!StringUtils.hasText(keyword)) {
            log.warn("【资金信息】 搜索关键字不能为空,{},{}", endpoint, method);
            return Result.error("搜索关键字不能为空");
        }

        try {
            PageResult<CapitalInfoDTO> dtoPage = capitalInfoService.queryPageByKeyword(current, size, keyword);

            // DTO -> VO
            PageResult<CapitalInfoRespones> voPage = BeanConvertUtils.convertPage(dtoPage, CapitalInfoRespones.class);

            return Result.success(voPage);

        } catch (Exception e) {
            log.error("【资金信息】通过关键字获取资金信息分页失败,{},{}", endpoint, method, e);
            return Result.error("通过关键字获取资金信息分页失败");
        }
    }

    /**
     * 刷新资金信息列表
     *
     * @return {@link Result}&lt;{@link List}&lt;{@link CapitalInfoRespones}&gt;&gt;
     */
    @GetMapping("/list")
    public Result<List<CapitalInfoRespones>> refreshList() {

        String endpoint = "/capital/info/list";
        String method = "refreshList";

        log.info("【资金信息】刷新资金信息列表,{},{}", endpoint, method);

        try {
            List<CapitalInfoDTO> dtos = capitalInfoService.refreshList();

            // DTO -> VO
            List<CapitalInfoRespones> vos = BeanConvertUtils.convertList(dtos, CapitalInfoRespones.class);

            return Result.success(vos);

        } catch (Exception e) {
            log.error("【资金信息】刷新资金信息列表失败,{},{}", endpoint, method, e);
            return Result.error("刷新资金信息列表失败");
        }
    }

    /**
     * 刷新资金信息分页
     *
     * @param current 当前页码
     * @param size    每页大小
     * @return {@link Result}&lt;{@link PageResult}&lt;{@link CapitalInfoRespones}&gt;&gt;
     */
    @GetMapping("/page")
    public Result<PageResult<CapitalInfoRespones>> refreshPage(
            @RequestParam(value = "current", defaultValue = "1") Integer current,
            @RequestParam(value = "size", defaultValue = "20") Integer size
    ) {

        String endpoint = "/capital/info/page";
        String method = "refreshPage";

        log.info("【资金信息】刷新资金信息分页,{},{},current = {},size = {}", endpoint, method, current, size);

        try {
            PageResult<CapitalInfoDTO> dtoPage = capitalInfoService.refreshPage(current, size);

            // DTO -> VO
            PageResult<CapitalInfoRespones> voPage = BeanConvertUtils.convertPage(dtoPage, CapitalInfoRespones.class);

            return Result.success(voPage);

        } catch (Exception e) {
            log.error("【资金信息】刷新资金信息分页失败,{},{}", endpoint, method, e);
            return Result.error("刷新资金信息分页失败");
        }
    }

    /**
     * 新增资金信息
     *
     * @param createDTO 创建对象
     * @return {@link Result}&lt;{@link Integer}&gt;
     */
    @PostMapping("/add")
    public Result<Integer> add(@RequestBody CapitalInfoCreateRequest createDTO) {

        String endpoint = "/capital/info/add";
        String method = "add";

        log.info("【资金信息】新增资金信息,{},{},createDTO = {}", endpoint, method, SecurityUtils.safeForLog(createDTO));

        try {
            Integer id = capitalInfoService.add(createDTO);

            return Result.success(id);

        } catch (Exception e) {
            log.error("【资金信息】新增资金信息失败,{},{}", endpoint, method, e);
            return Result.error("新增资金信息失败");
        }
    }

    /**
     * 新增资金信息(可能附带上传附件和上传文件)
     *
     * @param createDTO   创建对象
     * @param uploadFile  上传附件
     * @param uploadFiles 上传文件
     * @return {@link Result}&lt;{@link Integer}&gt;
     */
    @PostMapping("/add-by-formdata")
    @Debounce(keyType = Debounce.KeyType.USER, value = 0) // value=0 表示使用配置值
    public Result<Integer> add(@RequestPart("capitalInfo") CapitalInfoCreateRequest createDTO,
                               @RequestPart(value = "uploadFile", required = false) MultipartFile uploadFile,
                               @RequestPart(value = "uploadFiles", required = false) List<MultipartFile> uploadFiles) {

        String endpoint = "/capital/info/add-by-formdata";
        String method = "add";

        log.info("【资金信息】新增资金信息(可能附带上传附件和上传文件),{},{},createDTO = {},uploadFile = {}, uploadFiles = {}",
                endpoint, method, SecurityUtils.safeForLog(createDTO), uploadFile, uploadFiles);

        try {
            Integer id = capitalInfoService.add(createDTO, uploadFile, uploadFiles);

            return Result.success(id);

        } catch (Exception e) {
            log.error("【资金信息】新增资金信息(可能附带上传附件和上传文件)失败,{},{}", endpoint, method, e);
            return Result.error("新增资金信息(可能附带上传附件和上传文件)失败");
        }
    }

    /**
     * 更新资金信息
     *
     * @param updateDTO 更新对象
     * @return {@link Result}&lt;{@link Integer}&gt;
     */
    @PostMapping("/update")
    public Result<Integer> update(@RequestBody CapitalInfoUpdateRequest updateDTO) {

        String endpoint = "/capital/info/update";
        String method = "update";

        log.info("【资金信息】更新资金信息,{},{},updateDTO = {}", endpoint, method, SecurityUtils.safeForLog(updateDTO));

        // 检查
        if (updateDTO == null) {
            log.warn("【资金信息】更新对象不能为空,{},{}", endpoint, method);
            return Result.error("更新对象不能为空");
        }
        if (Objects.equals(updateDTO.getCapitalState(), CapitalInfoStateEnum.CLOSED.getValue())) {
            log.warn("【资金信息】资金状态为已结案不能更新,{},{}", endpoint, method);
            return Result.error("资金状态为已结案不能更新");
        }

        try {
            Integer rows = capitalInfoService.update(updateDTO);

            return Result.success(rows);

        } catch (Exception e) {
            log.error("【资金信息】更新资金信息失败,{},{}", endpoint, method, e);
            return Result.error("更新资金信息失败");
        }
    }

    /**
     * 删除资金信息
     *
     * @param id 主键id
     */
    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable @Min(1) Integer id) {

        String endpoint = "/capital/info/";
        String method = "delete";

        log.info("【资金信息】删除资金信息,{}{},{}", endpoint, id, method);

        try {
            boolean success = capitalInfoService.delete(id);

            return success ? Result.success() : Result.error("删除失败");

        } catch (Exception e) {
            log.error("【资金信息】删除资金信息失败,{}{},{}:", endpoint, id, method, e);
            return Result.error("删除资金信息失败");
        }
    }

    /**
     * 结案资金信息
     *
     * @param id 主键id
     * @return {@link Result}&lt;{@link Integer}&gt;
     */
    @PostMapping("/update-complete/{id}")
    public Result<Integer> updateStateToComplete(@PathVariable @Min(1) Integer id) {

        String endpoint = "/capital/info/update-complete/";
        String method = "updateStateToComplete";

        log.info("【资金信息】结案资金信息,{}{},{}", endpoint, id, method);

        try {
            Integer rows = capitalInfoService.updateStateToComplete(id);

            return Result.success(rows);

        } catch (Exception e) {
            log.error("【资金信息】结案资金信息失败,{}{},{}:", endpoint, id, method, e);
            return Result.error("结案资金信息失败");
        }
    }

    /**
     * 上传附件
     *
     * @param uploadFile 上传文件 {@link MultipartFile}
     * @param id         主键id
     * @return {@link Result}&lt;{@link Void}&gt;
     */
    @PostMapping("upload-attachment")
    @Debounce(keyType = Debounce.KeyType.USER, value = 0) // value=0 表示使用配置值
    public Result<Void> uploadAttachment(@RequestParam("uploadFile") MultipartFile uploadFile,
                                         @RequestParam("key") @Min(1) Integer id) {

        String endpoint = "/capital/info/upload-attachment";
        String method = "uploadAttachment";

        log.info("【资金信息】上传附件,{},{},id = {},uploadFile = {}", endpoint, method, id, uploadFile);

        try {
            capitalInfoService.uploadAttachment(uploadFile, id);

            return Result.success();

        } catch (Exception e) {
            log.error("【资金信息】上传附件失败,{},{}", endpoint, method, e);
            return Result.error("上传附件失败,请稍后重试");
        }
    }

    /**
     * 下载附件
     *
     * @param id 主键id
     * @return {@link ResponseEntity}&lt;{@link Resource}&gt;
     */
    @GetMapping("/download-attachment/{id}")
    @Debounce(keyType = Debounce.KeyType.USER, value = -1) // value=-1 表示使用配置值
    public ResponseEntity<Resource> downloadAttachment(@PathVariable @Min(1) Integer id) {

        String endpoint = "/capital/info/download-attachment/";
        String method = "downloadAttachment";

        log.info("【资金信息】下载附件,{}{},{}", endpoint, id, method);

        try {
            ResponseEntity<Resource> response = capitalInfoService.downloadAttachment(id);

            log.info("【资金信息】下载附件成功,{}{},{}", endpoint, id, method);

            return response;

        } catch (ResourceNotFoundException e) {
            log.warn("【资金信息】下载附件失败,文件不存在,{}{},{}", endpoint, id, method);
            return ResponseEntity.notFound().build();
        } catch (Exception e) {
            log.error("【资金信息】下载附件失败,{}{},{},异常信息:{}", endpoint, id, method, e.getMessage(), e);
            return ResponseEntity.status(500).build();
        }
    }

    /**
     * 提取附件文本内容
     *
     * @param id 主键id
     * @return {@link Result}&lt;{@link String}&gt;
     */
    @GetMapping("/extract-attachment/{id}")
    @Debounce(keyType = Debounce.KeyType.USER, value = 3000) // value=3000 表示固定3秒,不使用配置
    public Result<String> extractAttachment(@PathVariable @Min(1) Integer id) {

        String endpoint = "/capital/info/extract-attachment/";
        String method = "extractAttachment";

        log.info("【资金信息】提取附件文本内容,{}{},{}", endpoint, id, method);

        try {
            String text = capitalInfoService.extractAttachment(id);

            return Result.success(text);

        } catch (Exception e) {
            log.error("【资金信息】提取附件文本内容失败,{}{},{}:", endpoint, id, method, e);
            return Result.error("提取附件文本内容失败,请稍后重试");
        }
    }

    /**
     * 清除附件
     *
     * @param id 主键id
     * @return {@link Result}&lt;{@link Void}&gt;
     */
    @PostMapping("/clear-attachment/{id}")
    public Result<Void> clearAttachment(@PathVariable @Min(1) Integer id) {

        String endpoint = "/capital/info/clear-attachment/";
        String method = "clearAttachment";

        log.info("【资金信息】清除附件,{}{},{}", endpoint, id, method);

        try {
            capitalInfoService.clearAttachment(id);

            return Result.success();

        } catch (Exception e) {
            log.error("【资金信息】清除附件失败,{}{},{}:", endpoint, id, method, e);
            return Result.error("清除附件失败,请稍后重试");
        }
    }

    /**
     * 导入数据
     *
     * @param uploadFile 上传文件 {@link MultipartFile}
     * @return {@link Result}&lt;{@link Void}&gt;
     */
    @PostMapping("/import-data")
    @Debounce(keyType = Debounce.KeyType.USER, value = 0) // value=0 表示使用配置值
    public Result<Void> importData(@RequestParam("uploadFile") @NotNull MultipartFile uploadFile) {

        String endpoint = "/capital/info/import-data";
        String method = "importData";

        log.info("【资金信息】导入数据,{},{},uploadFile = {}", endpoint, method, uploadFile);

        try {
            capitalInfoService.importData(uploadFile);

            return Result.success();

        } catch (Exception e) {
            log.error("【资金信息】导入数据失败,{},{}", endpoint, method, e);
            return Result.error("导入数据失败,请稍后重试");
        }
    }

    /**
     * 导出数据
     *
     * @param queryDTO 查询对象
     * @return {@link ResponseEntity}&lt;{@link Resource}&gt;
     */
    @PostMapping("/export-data")
    @Debounce(keyType = Debounce.KeyType.USER, value = -1) // value=-1 表示使用配置值
    public ResponseEntity<Resource> exportData(@RequestBody @Valid CapitalInfoQueryRequest queryDTO) throws JsonProcessingException {

        String endpoint = "/capital/info/export-data";
        String method = "exportData";

        // 对日志输出进行HTML转义(防XSS),使用自定义工具类SecurityUtils,如:将<script>alert(1)</script>转换成&lt;script&gt;alert(1)&lt;/script&gt;
        // log.info("【资金信息】导出数据,{},{},queryDTO = {}", endpoint, method, SecurityUtils.safeForLog(queryDTO));

        // 对日志输出进行HTML转义(防XSS),使用第三方工具类Apache Commons Text的StringEscapeUtils,如:将<script>alert(1)</script>转换成&lt;script&gt;alert(1)&lt;/script&gt;
        ObjectMapper mapper = new ObjectMapper();
        String jsonDTO = mapper.writeValueAsString(queryDTO);
        log.info("【资金信息】导出数据,{},{},queryDTO = {}", endpoint, method, StringEscapeUtils.escapeHtml4(jsonDTO));

        try {
            // 获取导出数据文件路径
            String filePath = capitalInfoService.exportFile(queryDTO);

            // 创建文件路径
            Path path = Paths.get(filePath);
            // 创建资源
            Resource resource = new UrlResource(path.toUri());
            // 资源不存在
            if (!resource.exists()) {
                return ResponseEntity.notFound().build();
            }

            log.info("【资金信息】导出数据成功,{},{}", endpoint, method);

            // 返回响应实体
            return ResponseEntity
                    // 设置状态
                    .ok()
                    // 设置内容类型为 MediaType.APPLICATION_OCTET_STREAM,八位字节的二进制数据流
                    .contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength())
                    // 设置响应标头,添加属性 Content-Disposition,Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名。
                    // 其属性值必须要加上attachment,如: attachment;filename="name.xlsx",就是文件名称的信息,并且文件名称需要用双引号包裹(不支持中文编码,需要编码转换)
                    // 设置内容处置为附件,并指定文件名,到时前端就可以解析这个响应头拿到这个文件名称进行下载
                    // .header("Content-Disposition", "attachment;filename=\"" + URLEncoder.encode(fileName, StandardCharsets.UTF_8) +"\"")
                    // 实际测试发现文件名称不用双引号包裹,也是可以达到需求目标,并且前端通过正则表达式解析出文件名称时还简单一些
                    // 文件名通常放在双引号内,如果文件名包含空格或特殊字符,使用双引号是必要的
                    .header("Content-Disposition", "attachment;filename=" + URLEncoder.encode(FileUtils.getFileName(filePath), StandardCharsets.UTF_8))
                    // 设置响应消息体为 resource
                    .body(resource);

        } catch (ResourceNotFoundException e) {
            log.warn("【资金信息】导出数据失败,文件不存在,{},{}", endpoint, method);
            return ResponseEntity.notFound().build();
        } catch (Exception e) {
            log.error("【资金信息】导出数据失败,{},{},异常信息:{}", endpoint, method, e.getMessage(), e);
            return ResponseEntity.internalServerError().build(); // 等同 ResponseEntity.status(500).build()
        }
    }

    /**
     * 使用模板导出数据
     *
     * @param queryDTO 查询对象
     * @return {@link ResponseEntity}&lt;{@link Resource}&gt;
     */
    @PostMapping("/export-data-by-template")
    @Debounce(keyType = Debounce.KeyType.USER, value = -1) // value=-1 表示使用配置值
    public ResponseEntity<Resource> exportDataByTemplate(@RequestBody @Valid CapitalInfoQueryRequest queryDTO) throws IOException {

        String endpoint = "/capital/info/export-data-by-template";
        String method = "exportDataByTemplate";

        // 对日志输出进行HTML转义(防XSS),使用自定义工具类SecurityUtils,如:将<script>alert(1)</script>转换成&lt;script&gt;alert(1)&lt;/script&gt;
        log.info("【资金信息】使用模板导出数据,{},{},queryDTO = {}", endpoint, method, SecurityUtils.safeForLog(queryDTO));
        // 输出示例:【资金信息】使用模板导出数据,/capital/info/export-data-by-template,exportDataByTemplate,queryDTO = CapitalInfoQueryDTO(super=BasePageQuery(current=1, size=20), capitalNo=AAA66, capitalName=, capitalType=, capitalIndexType=, capitalAccount=, capitalSource=, capitalIndexSource=, capitalYear=null, capitalStates=[], remark=, createDateRange=DateRange(beginDate=, endDate=))

        // 对日志输出进行HTML转义(防XSS),使用第三方工具类Apache Commons Text的StringEscapeUtils,如:将<script>alert(1)</script>转换成&lt;script&gt;alert(1)&lt;/script&gt;
        // ObjectMapper mapper = new ObjectMapper();
        // String jsonDTO = mapper.writeValueAsString(queryDTO);
        // log.info("【资金信息】使用模板导出数据,{},{},queryDTO = {}", endpoint, method, StringEscapeUtils.escapeHtml4(jsonDTO));
        // 输出示例:资金信息】使用模板导出数据,/capital/info/export-data-by-template,exportDataByTemplate,queryDTO = {&quot;current&quot;:1,&quot;size&quot;:20,&quot;capitalNo&quot;:&quot;AAA66&quot;,&quot;capitalName&quot;:&quot;&quot;,&quot;capitalType&quot;:&quot;&quot;,&quot;capitalIndexType&quot;:&quot;&quot;,&quot;capitalAccount&quot;:&quot;&quot;,&quot;capitalSource&quot;:&quot;&quot;,&quot;capitalIndexSource&quot;:&quot;&quot;,&quot;capitalYear&quot;:null,&quot;capitalStates&quot;:[],&quot;remark&quot;:&quot;&quot;,&quot;createDateRange&quot;:{&quot;beginDate&quot;:&quot;&quot;,&quot;endDate&quot;:&quot;&quot;}}

        // 获取导出数据文件路径
        String filePath = capitalInfoService.exportFileByTemplate(queryDTO);

        // 创建文件路径
        Path path = Paths.get(filePath);
        // 创建资源
        Resource resource = new UrlResource(path.toUri());
        // 资源不存在则抛出异常(由全局处理器处理)
        if (!resource.exists()) {
            throw new ResourceNotFoundException("资源文件不存在");
        }

        log.info("【资金信息】使用模板导出数据成功,{},{}", endpoint, method);

        // 返回响应实体
        return ResponseEntity
                // 设置状态
                .ok()
                // 设置内容类型为 MediaType.APPLICATION_OCTET_STREAM,八位字节的二进制数据流
                .contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength())
                // 设置响应标头,添加属性 Content-Disposition,Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名。
                // 其属性值必须要加上attachment,如: attachment;filename="name.xlsx",就是文件名称的信息,并且文件名称需要用双引号包裹(不支持中文编码,需要编码转换)
                // 设置内容处置为附件,并指定文件名,到时前端就可以解析这个响应头拿到这个文件名称进行下载
                // .header("Content-Disposition", "attachment;filename=\"" + URLEncoder.encode(fileName, StandardCharsets.UTF_8) +"\"")
                // 实际测试发现文件名称不用双引号包裹,也是可以达到需求目标,并且前端通过正则表达式解析出文件名称时还简单一些
                // 文件名通常放在双引号内,如果文件名包含空格或特殊字符,使用双引号是必要的
                .header("Content-Disposition", "attachment;filename=" + URLEncoder.encode(FileUtils.getFileName(filePath), StandardCharsets.UTF_8))
                // 设置响应消息体为 resource
                .body(resource);
    }

    /**
     * 查询资金账户选项
     *
     * @return {@link Result}&lt;{@link List}&lt;{@link String}&gt;&gt;
     */
    @GetMapping("/account")
    public Result<List<String>> queryCapitalAccountOptions() {

        String endpoint = "/capital/options/account";
        String method = "queryCapitalAccountOptions";

        log.info("【基础选项】查询资金账户选项,{},{}", endpoint, method);

        try {
            List<String> options = capitalInfoService.queryCapitalAccountOptions();

            return Result.success(options);

        } catch (Exception e) {
            log.error("【基础选项】查询资金账户选项失败,{},{}", endpoint, method, e);
            return Result.error("查询资金账户选项失败,请稍后重试");
        }
    }

    /**
     * 查询资金来源选项
     *
     * @return {@link Result}&lt;{@link List}&lt;{@link String}&gt;&gt;
     */
    @GetMapping("/source")
    public Result<List<String>> queryCapitalSourceOptions() {

        String endpoint = "/capital/options/source";
        String method = "queryCapitalSourceOptions";

        log.info("【基础选项】查询资金来源选项,{},{}", endpoint, method);

        try {
            List<String> options = capitalInfoService.queryCapitalSourceOptions();

            return Result.success(options);

        } catch (Exception e) {
            log.error("【基础选项】查询资金来源选项失败,{},{}", endpoint, method, e);
            return Result.error("查询资金来源选项失败,请稍后重试");
        }
    }

    /**
     * 查询指标来源选项
     *
     * @return {@link Result}&lt;{@link List}&lt;{@link String}&gt;&gt;
     */
    @GetMapping("/index-source")
    public Result<List<String>> queryCapitalIndexSourceOptions() {

        String endpoint = "/capital/options/index-source";
        String method = "queryCapitalIndexSourceOptions";

        log.info("【基础选项】查询指标来源选项,{},{}", endpoint, method);

        try {
            List<String> options = capitalInfoService.queryCapitalIndexSourceOptions();

            return Result.success(options);

        } catch (Exception e) {
            log.error("【基础选项】查询指标来源选项失败,{},{}", endpoint, method, e);
            return Result.error("查询指标来源选项失败,请稍后重试");
        }
    }

    /**
     * 查询指标类别选项
     *
     * @return {@link Result}&lt;{@link List}&lt;{@link String}&gt;&gt;
     */
    @GetMapping("/index-type")
    public Result<List<String>> queryCapitalIndexTypeOptions() {

        String endpoint = "/capital/options/index-type";
        String method = "queryCapitalIndexTypeOptions";

        log.info("【基础选项】查询指标类别选项,{},{}", endpoint, method);

        try {
            List<String> options = capitalInfoService.queryCapitalIndexTypeOptions();

            return Result.success(options);

        } catch (Exception e) {
            log.error("【基础选项】查询指标类别选项失败,{},{}", endpoint, method, e);
            return Result.error("查询指标类别选项失败,请稍后重试");
        }
    }

    /**
     * 查询预算情况选项
     *
     * @return {@link Result}&lt;{@link List}&lt;{@link String}&gt;&gt;
     */
    @GetMapping("/budget")
    public Result<List<String>> queryCapitalBudgetOptions() {

        String endpoint = "/capital/options/budget";
        String method = "queryCapitalBudgetOptions";

        log.info("【基础选项】查询预算情况选项,{},{}", endpoint, method);

        try {
            List<String> options = capitalInfoService.queryCapitalBudgetOptions();

            return Result.success(options);

        } catch (Exception e) {
            log.error("【基础选项】查询预算情况选项失败,{},{}", endpoint, method, e);
            return Result.error("查询预算情况选项失败,请稍后重试");
        }
    }

    /**
     * 查询支出分类选项
     *
     * @return {@link Result}&lt;{@link List}&lt;{@link String}&gt;&gt;
     */
    @GetMapping("/pay-type")
    public Result<List<String>> queryCapitalPayTypeOptions() {

        String endpoint = "/capital/options/pay-type";
        String method = "queryCapitalPayTypeOptions";

        log.info("【基础选项】查询支出分类选项,{},{}", endpoint, method);

        try {
            List<String> options = capitalInfoService.queryCapitalPayTypeOptions();

            return Result.success(options);

        } catch (Exception e) {
            log.error("【基础选项】查询支出分类选项失败,{},{}", endpoint, method, e);
            return Result.error("查询支出分类选项失败,请稍后重试");
        }
    }

    /**
     * 查询支出方式选项
     *
     * @return {@link Result}&lt;{@link List}&lt;{@link String}&gt;&gt;
     */
    @GetMapping("/pay-mode")
    public Result<List<String>> queryCapitalPayModeOptions() {

        String endpoint = "/capital/options/pay-mode";
        String method = "queryCapitalPayTypeOptions";

        log.info("【基础选项】查询支出方式选项,{},{}", endpoint, method);

        try {
            List<String> options = capitalInfoService.queryCapitalPayModeOptions();

            return Result.success(options);

        } catch (Exception e) {
            log.error("【基础选项】查询支出方式选项失败,{},{}", endpoint, method, e);
            return Result.error("查询支出方式选项失败,请稍后重试");
        }
    }

    /**
     * 检查所有查询条件是否都为空
     */
    private boolean isAllQueryParamsEmpty(CapitalInfoQueryRequest queryDTO) {
        if (queryDTO == null) {
            return true;
        }

//        return (queryDTO.getCapitalNo() == null || queryDTO.getCapitalNo().trim().isEmpty())
//               && (queryDTO.getCapitalName() == null || queryDTO.getCapitalName().trim().isEmpty())
//               && (queryDTO.getCapitalType() == null || queryDTO.getCapitalType().trim().isEmpty())
//               && (queryDTO.getCapitalIndexType() == null || queryDTO.getCapitalIndexType().trim().isEmpty())
//               && (queryDTO.getCapitalAccount() == null || queryDTO.getCapitalAccount().trim().isEmpty())
//               && (queryDTO.getCapitalSource() == null || queryDTO.getCapitalSource().trim().isEmpty())
//               && (queryDTO.getCapitalIndexSource() == null || queryDTO.getCapitalIndexSource().trim().isEmpty())
//               && queryDTO.getCapitalYear() == null
//               && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
//               && (queryDTO.getRemark() == null || queryDTO.getRemark().trim().isEmpty())
//               && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getBeginDateTime().isEmpty())
//               && (queryDTO.getCreateDateRange() == null || queryDTO.getCreateDateRange().getEndDateTime().isEmpty());

        return !StringUtils.hasText(queryDTO.getCapitalNo())
               && !StringUtils.hasText(queryDTO.getCapitalName())
               && !StringUtils.hasText(queryDTO.getCapitalType())
               && !StringUtils.hasText(queryDTO.getCapitalIndexType())
               && !StringUtils.hasText(queryDTO.getCapitalAccount())
               && !StringUtils.hasText(queryDTO.getCapitalSource())
               && !StringUtils.hasText(queryDTO.getCapitalIndexSource())
               && queryDTO.getCapitalYear() == null
               && (queryDTO.getCapitalStates() == null || queryDTO.getCapitalStates().isEmpty())
               && !StringUtils.hasText(queryDTO.getRemark())
               && (queryDTO.getCreateDateRange() == null
                   || (!StringUtils.hasText(queryDTO.getCreateDateRange().getBeginDate())
                       && !StringUtils.hasText(queryDTO.getCreateDateRange().getEndDate())));
    }

    /**
     * 校验日期范围
     */
    private String validateDateRange(DateRange createDateRange) {
        if (createDateRange == null) {
            return null;  // 校验通过
        }

        // 检查是否选择了日期范围
        if (createDateRange.hasDateRange()) {
            // 检查是否完整(两个都有值或两个都没有值)
            if (createDateRange.isIncomplete()) {
                log.warn("【资金信息】日期范围不完整,开始日期:{},结束日期:{}",
                        createDateRange.getBeginDate(), createDateRange.getEndDate());
                return "请填写完整的日期范围";
            }

            // 检查是否有效
            if (createDateRange.hasValidDateRange()) {
                // 日期范围有效,可以正常查询
                log.info("【资金信息】使用日期范围查询:{} 至 {}",
                        createDateRange.getBeginDate(), createDateRange.getEndDate());
                return null;  // 校验通过
            } else {
                // 日期范围无效(开始日期晚于结束日期)
                log.warn("【资金信息】开始日期不能晚于结束日期,开始日期:{},结束日期:{}",
                        createDateRange.getBeginDate(), createDateRange.getEndDate());
                return "开始日期不能晚于结束日期";
            }
        } else {
            // 没有选择日期范围,可以正常查询其他条件
            log.info("【资金信息】未使用日期范围查询");
            return null;  // 校验通过
        }
    }
}

src/main/java/com/weiyu/model/PageResult.java

package com.weiyu.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 分页结果
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> {
    private Long total;     // 总条数
    private List<T> rows;   // 当前页数据集合
}

使用示例

假设有以下类:

  • CapitalInfoDTO:数据传输对象

  • CapitalInfoRespones:视图对象(VO)

  • PageResult:分页结果类,包含 getTotal()getRows() 和构造函数 PageResult(long total, List<T> rows)

1. 单个对象转换

java

// DTO -> VO
CapitalInfoDTO dto = capitalInfoService.getById(1L);
CapitalInfoRespones vo = BeanConvertUtils.convert(dto, CapitalInfoRespones.class);

或使用自定义转换器(例如需要额外设置字段):

java

CapitalInfoRespones vo = BeanConvertUtils.convert(dto, d -> {
    CapitalInfoRespones v = new CapitalInfoRespones();
    BeanUtils.copyProperties(d, v);
    v.setExtraField("some value");
    return v;
});
2. 列表转换

java

List<CapitalInfoDTO> dtoList = capitalInfoService.listAll();
List<CapitalInfoRespones> voList = BeanConvertUtils.convertList(dtoList, CapitalInfoRespones.class);

或自定义转换器:

java

List<CapitalInfoRespones> voList = BeanConvertUtils.convertList(dtoList, d -> {
    CapitalInfoRespones v = new CapitalInfoRespones();
    BeanUtils.copyProperties(d, v);
    // 其他逻辑
    return v;
});
3. 分页结果转换

java

PageResult<CapitalInfoDTO> dtoPage = capitalInfoService.queryPage(queryDTO);

// 基于 Class 转换
PageResult<CapitalInfoRespones> voPage = BeanConvertUtils.convertPage(dtoPage, CapitalInfoRespones.class);

// 基于自定义转换器
PageResult<CapitalInfoRespones> voPage2 = BeanConvertUtils.convertPage(dtoPage, d -> {
    CapitalInfoRespones v = new CapitalInfoRespones();
    BeanUtils.copyProperties(d, v);
    return v;
});
4. 使用 Supplier 的替代方案

如果你习惯使用 Supplier 创建目标对象,可以通过 Function 方式实现:

java

Supplier<CapitalInfoRespones> supplier = CapitalInfoRespones::new;
List<CapitalInfoRespones> voList = BeanConvertUtils.convertList(dtoList, d -> {
    CapitalInfoRespones v = supplier.get();
    BeanUtils.copyProperties(d, v);
    return v;
});

注意事项

  1. 无参构造器:基于 Class 的转换要求目标类有一个公开的无参构造器(例如使用 Lombok 的 @NoArgsConstructor 或默认构造器)。

  2. 属性名称匹配BeanUtils.copyProperties 根据属性名进行浅拷贝,确保 DTO 和 VO 中同名字段的类型兼容。

  3. 空值处理:工具类对于输入为 null 的情况返回 null。你可以根据业务需要修改为返回空集合或空对象。

  4. 异常处理:转换失败时抛出运行时异常,可根据项目需要替换为自定义异常或日志记录。

此工具类已经过测试,可直接集成到 Spring Boot 项目中使用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐