Compare commits
17 Commits
17d39a0208
...
0de248a039
| Author | SHA1 | Date | |
|---|---|---|---|
| 0de248a039 | |||
| b69064b68d | |||
| 68325518d7 | |||
| 120255fcd5 | |||
| 40b7e5bb1b | |||
| df15307288 | |||
| 879580ffe5 | |||
| ab1c06e631 | |||
| d95de8a692 | |||
| a3a890a2f1 | |||
| 4384c7a4ff | |||
| 1c607c0b2d | |||
| cfc3545fc7 | |||
| b498137206 | |||
| 80337e33b1 | |||
| ebc2d2c3d2 | |||
| 0921e76781 |
@@ -0,0 +1,88 @@
|
||||
package com.ruoyi.ccdi.project.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiBankStatementService;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.PageDomain;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.page.TableSupport;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流水明细查询Controller
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/ccdi/project/bank-statement")
|
||||
@Tag(name = "流水明细查询")
|
||||
public class CcdiBankStatementController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private ICcdiBankStatementService bankStatementService;
|
||||
|
||||
/**
|
||||
* 分页查询流水明细
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "分页查询流水明细")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public TableDataInfo list(CcdiBankStatementQueryDTO queryDTO) {
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
Page<CcdiBankStatementListVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
|
||||
Page<CcdiBankStatementListVO> result = bankStatementService.selectStatementPage(page, queryDTO);
|
||||
return getDataTable(result.getRecords(), result.getTotal());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询项目级筛选项
|
||||
*/
|
||||
@GetMapping("/options")
|
||||
@Operation(summary = "查询项目级筛选项")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public AjaxResult getOptions(Long projectId) {
|
||||
CcdiBankStatementFilterOptionsVO options = bankStatementService.getFilterOptions(projectId);
|
||||
return AjaxResult.success(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询流水详情
|
||||
*/
|
||||
@GetMapping("/detail/{bankStatementId}")
|
||||
@Operation(summary = "查询流水详情")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public AjaxResult getDetail(@PathVariable Long bankStatementId) {
|
||||
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(bankStatementId);
|
||||
return AjaxResult.success(detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出流水明细
|
||||
*/
|
||||
@PostMapping("/export")
|
||||
@Operation(summary = "导出流水明细")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:export')")
|
||||
public void export(HttpServletResponse response, CcdiBankStatementQueryDTO queryDTO) {
|
||||
List<CcdiBankStatementExcel> list = bankStatementService.selectStatementListForExport(queryDTO);
|
||||
ExcelUtil<CcdiBankStatementExcel> util = new ExcelUtil<>(CcdiBankStatementExcel.class);
|
||||
util.exportExcel(response, list, "流水明细");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ruoyi.ccdi.project.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流水明细查询DTO
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
public class CcdiBankStatementQueryDTO {
|
||||
|
||||
/** 项目ID */
|
||||
private Long projectId;
|
||||
|
||||
/** 页签类型 */
|
||||
private String tabType;
|
||||
|
||||
/** 交易开始时间 */
|
||||
private String transactionStartTime;
|
||||
|
||||
/** 交易结束时间 */
|
||||
private String transactionEndTime;
|
||||
|
||||
/** 对方名称 */
|
||||
private String counterpartyName;
|
||||
|
||||
/** 对方名称是否匹配空值 */
|
||||
private Boolean counterpartyNameEmpty;
|
||||
|
||||
/** 摘要 */
|
||||
private String userMemo;
|
||||
|
||||
/** 摘要是否匹配空值 */
|
||||
private Boolean userMemoEmpty;
|
||||
|
||||
/** 本方主体 */
|
||||
private List<String> ourSubjects;
|
||||
|
||||
/** 本方银行 */
|
||||
private List<String> ourBanks;
|
||||
|
||||
/** 本方账户 */
|
||||
private List<String> ourAccounts;
|
||||
|
||||
/** 最小金额 */
|
||||
private BigDecimal amountMin;
|
||||
|
||||
/** 最大金额 */
|
||||
private BigDecimal amountMax;
|
||||
|
||||
/** 对方账户 */
|
||||
private String counterpartyAccount;
|
||||
|
||||
/** 对方账户是否匹配空值 */
|
||||
private Boolean counterpartyAccountEmpty;
|
||||
|
||||
/** 交易类型 */
|
||||
private String transactionType;
|
||||
|
||||
/** 交易类型是否匹配空值 */
|
||||
private Boolean transactionTypeEmpty;
|
||||
|
||||
/** 排序字段 */
|
||||
private String orderBy;
|
||||
|
||||
/** 排序方向 */
|
||||
private String orderDirection;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.ruoyi.ccdi.project.domain.excel;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 流水明细导出对象
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
public class CcdiBankStatementExcel {
|
||||
|
||||
/** 交易时间 */
|
||||
@Excel(name = "交易时间")
|
||||
private String trxDate;
|
||||
|
||||
/** 本方账户 */
|
||||
@Excel(name = "本方账户")
|
||||
private String leAccountNo;
|
||||
|
||||
/** 本方主体 */
|
||||
@Excel(name = "本方主体")
|
||||
private String leAccountName;
|
||||
|
||||
/** 对方名称 */
|
||||
@Excel(name = "对方名称")
|
||||
private String customerAccountName;
|
||||
|
||||
/** 对方账户 */
|
||||
@Excel(name = "对方账户")
|
||||
private String customerAccountNo;
|
||||
|
||||
/** 摘要 */
|
||||
@Excel(name = "摘要")
|
||||
private String userMemo;
|
||||
|
||||
/** 交易类型 */
|
||||
@Excel(name = "交易类型")
|
||||
private String cashType;
|
||||
|
||||
/** 交易金额 */
|
||||
@Excel(name = "交易金额")
|
||||
private BigDecimal displayAmount;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 流水明细详情VO
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
public class CcdiBankStatementDetailVO {
|
||||
|
||||
/** 流水ID */
|
||||
private Long bankStatementId;
|
||||
|
||||
/** 项目ID */
|
||||
private Long projectId;
|
||||
|
||||
/** 交易时间 */
|
||||
private String trxDate;
|
||||
|
||||
/** 币种 */
|
||||
private String currency;
|
||||
|
||||
/** 本方账户 */
|
||||
private String leAccountNo;
|
||||
|
||||
/** 本方主体 */
|
||||
private String leAccountName;
|
||||
|
||||
/** 对方名称 */
|
||||
private String customerAccountName;
|
||||
|
||||
/** 对方账户 */
|
||||
private String customerAccountNo;
|
||||
|
||||
/** 对方银行 */
|
||||
private String customerBank;
|
||||
|
||||
/** 对方备注 */
|
||||
private String customerReference;
|
||||
|
||||
/** 摘要 */
|
||||
private String userMemo;
|
||||
|
||||
/** 银行摘要 */
|
||||
private String bankComments;
|
||||
|
||||
/** 银行交易号 */
|
||||
private String bankTrxNumber;
|
||||
|
||||
/** 本方银行 */
|
||||
private String bank;
|
||||
|
||||
/** 交易类型 */
|
||||
private String cashType;
|
||||
|
||||
/** 借方金额 */
|
||||
private BigDecimal amountDr;
|
||||
|
||||
/** 贷方金额 */
|
||||
private BigDecimal amountCr;
|
||||
|
||||
/** 余额 */
|
||||
private BigDecimal amountBalance;
|
||||
|
||||
/** 页面展示金额 */
|
||||
private BigDecimal displayAmount;
|
||||
|
||||
/** 交易标志 */
|
||||
private String trxFlag;
|
||||
|
||||
/** 分类ID */
|
||||
private Integer trxType;
|
||||
|
||||
/** 异常类型 */
|
||||
private String exceptionType;
|
||||
|
||||
/** 是否内部交易 */
|
||||
private Integer internalFlag;
|
||||
|
||||
/** 交易方式 */
|
||||
private String paymentMethod;
|
||||
|
||||
/** 身份证号 */
|
||||
private String cretNo;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createDate;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流水明细筛选项集合VO
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
public class CcdiBankStatementFilterOptionsVO {
|
||||
|
||||
/** 本方主体选项 */
|
||||
private List<CcdiBankStatementOptionVO> ourSubjectOptions = new ArrayList<>();
|
||||
|
||||
/** 本方银行选项 */
|
||||
private List<CcdiBankStatementOptionVO> ourBankOptions = new ArrayList<>();
|
||||
|
||||
/** 本方账户选项 */
|
||||
private List<CcdiBankStatementOptionVO> ourAccountOptions = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 流水明细列表VO
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
public class CcdiBankStatementListVO {
|
||||
|
||||
/** 流水ID */
|
||||
private Long bankStatementId;
|
||||
|
||||
/** 交易时间 */
|
||||
private String trxDate;
|
||||
|
||||
/** 本方账户 */
|
||||
private String leAccountNo;
|
||||
|
||||
/** 本方主体 */
|
||||
private String leAccountName;
|
||||
|
||||
/** 对方名称 */
|
||||
private String customerAccountName;
|
||||
|
||||
/** 对方账户 */
|
||||
private String customerAccountNo;
|
||||
|
||||
/** 摘要 */
|
||||
private String userMemo;
|
||||
|
||||
/** 交易类型 */
|
||||
private String cashType;
|
||||
|
||||
/** 页面展示金额 */
|
||||
private BigDecimal displayAmount;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 流水明细筛选项VO
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CcdiBankStatementOptionVO {
|
||||
|
||||
/** 展示文案 */
|
||||
private String label;
|
||||
|
||||
/** 实际值 */
|
||||
private String value;
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
@@ -24,4 +29,13 @@ public interface CcdiBankStatementMapper extends BaseMapper<CcdiBankStatement> {
|
||||
|
||||
int deleteByProjectIdAndBatchId(@Param("projectId") Long projectId,
|
||||
@Param("batchId") Integer batchId);
|
||||
|
||||
Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
|
||||
@Param("query") CcdiBankStatementQueryDTO query);
|
||||
|
||||
List<CcdiBankStatementListVO> selectStatementListForExport(@Param("query") CcdiBankStatementQueryDTO query);
|
||||
|
||||
CcdiBankStatementDetailVO selectStatementDetailById(@Param("bankStatementId") Long bankStatementId);
|
||||
|
||||
CcdiBankStatementFilterOptionsVO selectFilterOptions(@Param("projectId") Long projectId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.ruoyi.ccdi.project.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流水明细查询Service接口
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface ICcdiBankStatementService {
|
||||
|
||||
/**
|
||||
* 查询项目级筛选项
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 筛选项
|
||||
*/
|
||||
CcdiBankStatementFilterOptionsVO getFilterOptions(Long projectId);
|
||||
|
||||
/**
|
||||
* 分页查询流水明细
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param queryDTO 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
|
||||
CcdiBankStatementQueryDTO queryDTO);
|
||||
|
||||
/**
|
||||
* 查询导出列表
|
||||
*
|
||||
* @param queryDTO 查询条件
|
||||
* @return 导出列表
|
||||
*/
|
||||
List<CcdiBankStatementExcel> selectStatementListForExport(CcdiBankStatementQueryDTO queryDTO);
|
||||
|
||||
/**
|
||||
* 查询流水详情
|
||||
*
|
||||
* @param bankStatementId 流水ID
|
||||
* @return 详情
|
||||
*/
|
||||
CcdiBankStatementDetailVO getStatementDetail(Long bankStatementId);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.ruoyi.ccdi.project.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiBankStatementService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 流水明细查询Service实现
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
|
||||
|
||||
private static final Set<String> ALLOWED_TAB_TYPES = Set.of("all", "in", "out");
|
||||
private static final Set<String> ALLOWED_ORDER_DIRECTIONS = Set.of("asc", "desc");
|
||||
|
||||
@Resource
|
||||
private CcdiBankStatementMapper bankStatementMapper;
|
||||
|
||||
@Override
|
||||
public CcdiBankStatementFilterOptionsVO getFilterOptions(Long projectId) {
|
||||
CcdiBankStatementFilterOptionsVO options = bankStatementMapper.selectFilterOptions(projectId);
|
||||
return options == null ? new CcdiBankStatementFilterOptionsVO() : options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
|
||||
CcdiBankStatementQueryDTO queryDTO) {
|
||||
CcdiBankStatementQueryDTO normalizedQuery = queryDTO == null ? new CcdiBankStatementQueryDTO() : queryDTO;
|
||||
normalizeQuery(normalizedQuery);
|
||||
return bankStatementMapper.selectStatementPage(page, normalizedQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CcdiBankStatementExcel> selectStatementListForExport(CcdiBankStatementQueryDTO queryDTO) {
|
||||
CcdiBankStatementQueryDTO normalizedQuery = queryDTO == null ? new CcdiBankStatementQueryDTO() : queryDTO;
|
||||
normalizeQuery(normalizedQuery);
|
||||
List<CcdiBankStatementListVO> rows = bankStatementMapper.selectStatementListForExport(normalizedQuery);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return rows.stream().map(this::toExcel).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiBankStatementDetailVO getStatementDetail(Long bankStatementId) {
|
||||
return bankStatementMapper.selectStatementDetailById(bankStatementId);
|
||||
}
|
||||
|
||||
private void normalizeQuery(CcdiBankStatementQueryDTO queryDTO) {
|
||||
queryDTO.setTransactionStartTime(normalizeText(queryDTO.getTransactionStartTime()));
|
||||
queryDTO.setTransactionEndTime(normalizeText(queryDTO.getTransactionEndTime()));
|
||||
queryDTO.setCounterpartyName(normalizeText(queryDTO.getCounterpartyName()));
|
||||
queryDTO.setUserMemo(normalizeText(queryDTO.getUserMemo()));
|
||||
queryDTO.setCounterpartyAccount(normalizeText(queryDTO.getCounterpartyAccount()));
|
||||
queryDTO.setTransactionType(normalizeText(queryDTO.getTransactionType()));
|
||||
queryDTO.setOurSubjects(normalizeList(queryDTO.getOurSubjects()));
|
||||
queryDTO.setOurBanks(normalizeList(queryDTO.getOurBanks()));
|
||||
queryDTO.setOurAccounts(normalizeList(queryDTO.getOurAccounts()));
|
||||
queryDTO.setCounterpartyNameEmpty(Boolean.TRUE.equals(queryDTO.getCounterpartyNameEmpty()));
|
||||
queryDTO.setUserMemoEmpty(Boolean.TRUE.equals(queryDTO.getUserMemoEmpty()));
|
||||
queryDTO.setCounterpartyAccountEmpty(Boolean.TRUE.equals(queryDTO.getCounterpartyAccountEmpty()));
|
||||
queryDTO.setTransactionTypeEmpty(Boolean.TRUE.equals(queryDTO.getTransactionTypeEmpty()));
|
||||
queryDTO.setTabType(normalizeTabType(queryDTO.getTabType()));
|
||||
queryDTO.setOrderBy(normalizeOrderBy(queryDTO.getOrderBy()));
|
||||
queryDTO.setOrderDirection(normalizeOrderDirection(queryDTO.getOrderDirection()));
|
||||
}
|
||||
|
||||
private String normalizeTabType(String tabType) {
|
||||
String normalized = normalizeLowerCase(tabType);
|
||||
return normalized != null && ALLOWED_TAB_TYPES.contains(normalized) ? normalized : "all";
|
||||
}
|
||||
|
||||
private String normalizeOrderBy(String orderBy) {
|
||||
String normalized = normalizeText(orderBy);
|
||||
if (normalized == null) {
|
||||
return "trxDate";
|
||||
}
|
||||
if ("amount".equalsIgnoreCase(normalized)) {
|
||||
return "amount";
|
||||
}
|
||||
if ("trxDate".equalsIgnoreCase(normalized)) {
|
||||
return "trxDate";
|
||||
}
|
||||
return "trxDate";
|
||||
}
|
||||
|
||||
private String normalizeOrderDirection(String orderDirection) {
|
||||
String normalized = normalizeLowerCase(orderDirection);
|
||||
return normalized != null && ALLOWED_ORDER_DIRECTIONS.contains(normalized) ? normalized : "desc";
|
||||
}
|
||||
|
||||
private String normalizeLowerCase(String value) {
|
||||
String normalized = normalizeText(value);
|
||||
return normalized == null ? null : normalized.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String normalizeText(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private List<String> normalizeList(List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<String> normalized = values.stream()
|
||||
.map(this::normalizeText)
|
||||
.filter(item -> item != null)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
private CcdiBankStatementExcel toExcel(CcdiBankStatementListVO row) {
|
||||
CcdiBankStatementExcel excel = new CcdiBankStatementExcel();
|
||||
excel.setTrxDate(row.getTrxDate());
|
||||
excel.setLeAccountNo(row.getLeAccountNo());
|
||||
excel.setLeAccountName(row.getLeAccountName());
|
||||
excel.setCustomerAccountName(row.getCustomerAccountName());
|
||||
excel.setCustomerAccountNo(row.getCustomerAccountNo());
|
||||
excel.setUserMemo(row.getUserMemo());
|
||||
excel.setCashType(row.getCashType());
|
||||
excel.setDisplayAmount(row.getDisplayAmount());
|
||||
return excel;
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,302 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
from ccdi_bank_statement
|
||||
</sql>
|
||||
|
||||
<resultMap id="CcdiBankStatementListVOResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO">
|
||||
<id property="bankStatementId" column="bankStatementId"/>
|
||||
<result property="trxDate" column="trxDate"/>
|
||||
<result property="leAccountNo" column="leAccountNo"/>
|
||||
<result property="leAccountName" column="leAccountName"/>
|
||||
<result property="customerAccountName" column="customerAccountName"/>
|
||||
<result property="customerAccountNo" column="customerAccountNo"/>
|
||||
<result property="userMemo" column="userMemo"/>
|
||||
<result property="cashType" column="cashType"/>
|
||||
<result property="displayAmount" column="displayAmount"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="CcdiBankStatementDetailVOResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO">
|
||||
<id property="bankStatementId" column="bankStatementId"/>
|
||||
<result property="projectId" column="projectId"/>
|
||||
<result property="trxDate" column="trxDate"/>
|
||||
<result property="currency" column="currency"/>
|
||||
<result property="leAccountNo" column="leAccountNo"/>
|
||||
<result property="leAccountName" column="leAccountName"/>
|
||||
<result property="customerAccountName" column="customerAccountName"/>
|
||||
<result property="customerAccountNo" column="customerAccountNo"/>
|
||||
<result property="customerBank" column="customerBank"/>
|
||||
<result property="customerReference" column="customerReference"/>
|
||||
<result property="userMemo" column="userMemo"/>
|
||||
<result property="bankComments" column="bankComments"/>
|
||||
<result property="bankTrxNumber" column="bankTrxNumber"/>
|
||||
<result property="bank" column="bank"/>
|
||||
<result property="cashType" column="cashType"/>
|
||||
<result property="amountDr" column="amountDr"/>
|
||||
<result property="amountCr" column="amountCr"/>
|
||||
<result property="amountBalance" column="amountBalance"/>
|
||||
<result property="displayAmount" column="displayAmount"/>
|
||||
<result property="trxFlag" column="trxFlag"/>
|
||||
<result property="trxType" column="trxType"/>
|
||||
<result property="exceptionType" column="exceptionType"/>
|
||||
<result property="internalFlag" column="internalFlag"/>
|
||||
<result property="paymentMethod" column="paymentMethod"/>
|
||||
<result property="cretNo" column="cretNo"/>
|
||||
<result property="createDate" column="createDate"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="CcdiBankStatementFilterOptionsVOResultMap"
|
||||
type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO">
|
||||
<collection property="ourSubjectOptions"
|
||||
column="{projectId=project_id}"
|
||||
select="selectOurSubjectOptions"/>
|
||||
<collection property="ourBankOptions"
|
||||
column="{projectId=project_id}"
|
||||
select="selectOurBankOptions"/>
|
||||
<collection property="ourAccountOptions"
|
||||
column="{projectId=project_id}"
|
||||
select="selectOurAccountOptions"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="parsedTrxDateExpr">
|
||||
CASE
|
||||
WHEN bs.TRX_DATE IS NULL OR TRIM(bs.TRX_DATE) = '' THEN NULL
|
||||
WHEN LENGTH(TRIM(bs.TRX_DATE)) = 10 THEN STR_TO_DATE(CONCAT(TRIM(bs.TRX_DATE), ' 00:00:00'), '%Y-%m-%d %H:%i:%s')
|
||||
ELSE STR_TO_DATE(TRIM(bs.TRX_DATE), '%Y-%m-%d %H:%i:%s')
|
||||
END
|
||||
</sql>
|
||||
|
||||
<sql id="displayAmountExpr">
|
||||
CASE
|
||||
WHEN IFNULL(bs.AMOUNT_CR, 0) > 0 THEN IFNULL(bs.AMOUNT_CR, 0)
|
||||
ELSE 0 - IFNULL(bs.AMOUNT_DR, 0)
|
||||
END
|
||||
</sql>
|
||||
|
||||
<sql id="absoluteAmountExpr">
|
||||
CASE
|
||||
WHEN IFNULL(bs.AMOUNT_CR, 0) > 0 THEN IFNULL(bs.AMOUNT_CR, 0)
|
||||
ELSE IFNULL(bs.AMOUNT_DR, 0)
|
||||
END
|
||||
</sql>
|
||||
|
||||
<sql id="statementListColumns">
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.TRX_DATE AS trxDate,
|
||||
bs.LE_ACCOUNT_NO AS leAccountNo,
|
||||
bs.LE_ACCOUNT_NAME AS leAccountName,
|
||||
bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName,
|
||||
bs.CUSTOMER_ACCOUNT_NO AS customerAccountNo,
|
||||
bs.USER_MEMO AS userMemo,
|
||||
bs.CASH_TYPE AS cashType,
|
||||
<include refid="displayAmountExpr"/> AS displayAmount
|
||||
</sql>
|
||||
|
||||
<sql id="statementFilterWhere">
|
||||
AND (bs.project_id = #{query.projectId})
|
||||
<if test="query.tabType == 'in'">
|
||||
AND IFNULL(bs.AMOUNT_CR, 0) > 0
|
||||
</if>
|
||||
<if test="query.tabType == 'out'">
|
||||
AND IFNULL(bs.AMOUNT_DR, 0) > 0
|
||||
</if>
|
||||
<if test="query.transactionStartTime != null and query.transactionStartTime != ''">
|
||||
AND (<include refid="parsedTrxDateExpr"/>) <![CDATA[ >= ]]>
|
||||
CASE
|
||||
WHEN LENGTH(TRIM(#{query.transactionStartTime})) = 10
|
||||
THEN STR_TO_DATE(CONCAT(TRIM(#{query.transactionStartTime}), ' 00:00:00'), '%Y-%m-%d %H:%i:%s')
|
||||
ELSE STR_TO_DATE(TRIM(#{query.transactionStartTime}), '%Y-%m-%d %H:%i:%s')
|
||||
END
|
||||
</if>
|
||||
<if test="query.transactionEndTime != null and query.transactionEndTime != ''">
|
||||
AND (<include refid="parsedTrxDateExpr"/>) <![CDATA[ <= ]]>
|
||||
CASE
|
||||
WHEN LENGTH(TRIM(#{query.transactionEndTime})) = 10
|
||||
THEN STR_TO_DATE(CONCAT(TRIM(#{query.transactionEndTime}), ' 23:59:59'), '%Y-%m-%d %H:%i:%s')
|
||||
ELSE STR_TO_DATE(TRIM(#{query.transactionEndTime}), '%Y-%m-%d %H:%i:%s')
|
||||
END
|
||||
</if>
|
||||
<if test="(query.counterpartyName != null and query.counterpartyName != '') or query.counterpartyNameEmpty">
|
||||
AND (
|
||||
<if test="query.counterpartyName != null and query.counterpartyName != ''">
|
||||
bs.CUSTOMER_ACCOUNT_NAME LIKE CONCAT('%', TRIM(#{query.counterpartyName}), '%')
|
||||
</if>
|
||||
<if test="query.counterpartyName != null and query.counterpartyName != '' and query.counterpartyNameEmpty">
|
||||
OR
|
||||
</if>
|
||||
<if test="query.counterpartyNameEmpty">
|
||||
bs.CUSTOMER_ACCOUNT_NAME IS NULL OR TRIM(bs.CUSTOMER_ACCOUNT_NAME) = ''
|
||||
</if>
|
||||
)
|
||||
</if>
|
||||
<if test="(query.userMemo != null and query.userMemo != '') or query.userMemoEmpty">
|
||||
AND (
|
||||
<if test="query.userMemo != null and query.userMemo != ''">
|
||||
bs.USER_MEMO LIKE CONCAT('%', TRIM(#{query.userMemo}), '%')
|
||||
</if>
|
||||
<if test="query.userMemo != null and query.userMemo != '' and query.userMemoEmpty">
|
||||
OR
|
||||
</if>
|
||||
<if test="query.userMemoEmpty">
|
||||
bs.USER_MEMO IS NULL OR TRIM(bs.USER_MEMO) = ''
|
||||
</if>
|
||||
)
|
||||
</if>
|
||||
<if test="query.ourSubjects != null and query.ourSubjects.size() > 0">
|
||||
AND bs.LE_ACCOUNT_NAME IN
|
||||
<foreach collection="query.ourSubjects" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="query.ourBanks != null and query.ourBanks.size() > 0">
|
||||
AND bs.BANK IN
|
||||
<foreach collection="query.ourBanks" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="query.ourAccounts != null and query.ourAccounts.size() > 0">
|
||||
AND bs.LE_ACCOUNT_NO IN
|
||||
<foreach collection="query.ourAccounts" item="item" open="(" separator="," close=")">
|
||||
#{item}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="query.amountMin != null">
|
||||
AND (<include refid="absoluteAmountExpr"/>) <![CDATA[ >= ]]> #{query.amountMin}
|
||||
</if>
|
||||
<if test="query.amountMax != null">
|
||||
AND (<include refid="absoluteAmountExpr"/>) <![CDATA[ <= ]]> #{query.amountMax}
|
||||
</if>
|
||||
<if test="(query.counterpartyAccount != null and query.counterpartyAccount != '') or query.counterpartyAccountEmpty">
|
||||
AND (
|
||||
<if test="query.counterpartyAccount != null and query.counterpartyAccount != ''">
|
||||
bs.CUSTOMER_ACCOUNT_NO LIKE CONCAT('%', TRIM(#{query.counterpartyAccount}), '%')
|
||||
</if>
|
||||
<if test="query.counterpartyAccount != null and query.counterpartyAccount != '' and query.counterpartyAccountEmpty">
|
||||
OR
|
||||
</if>
|
||||
<if test="query.counterpartyAccountEmpty">
|
||||
bs.CUSTOMER_ACCOUNT_NO IS NULL OR TRIM(bs.CUSTOMER_ACCOUNT_NO) = ''
|
||||
</if>
|
||||
)
|
||||
</if>
|
||||
<if test="(query.transactionType != null and query.transactionType != '') or query.transactionTypeEmpty">
|
||||
AND (
|
||||
<if test="query.transactionType != null and query.transactionType != ''">
|
||||
bs.CASH_TYPE LIKE CONCAT('%', TRIM(#{query.transactionType}), '%')
|
||||
</if>
|
||||
<if test="query.transactionType != null and query.transactionType != '' and query.transactionTypeEmpty">
|
||||
OR
|
||||
</if>
|
||||
<if test="query.transactionTypeEmpty">
|
||||
bs.CASH_TYPE IS NULL OR TRIM(bs.CASH_TYPE) = ''
|
||||
</if>
|
||||
)
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<sql id="statementOrderBy">
|
||||
<choose>
|
||||
<when test="query.orderBy == 'amount' and query.orderDirection == 'asc'">
|
||||
ORDER BY <include refid="absoluteAmountExpr"/> ASC, bs.bank_statement_id ASC
|
||||
</when>
|
||||
<when test="query.orderBy == 'amount' and query.orderDirection == 'desc'">
|
||||
ORDER BY <include refid="absoluteAmountExpr"/> DESC, bs.bank_statement_id DESC
|
||||
</when>
|
||||
<when test="query.orderDirection == 'asc'">
|
||||
ORDER BY <include refid="parsedTrxDateExpr"/> ASC, bs.bank_statement_id ASC
|
||||
</when>
|
||||
<otherwise>
|
||||
ORDER BY <include refid="parsedTrxDateExpr"/> DESC, bs.bank_statement_id DESC
|
||||
</otherwise>
|
||||
</choose>
|
||||
</sql>
|
||||
|
||||
<select id="selectStatementPage" resultMap="CcdiBankStatementListVOResultMap">
|
||||
SELECT
|
||||
<include refid="statementListColumns"/>
|
||||
FROM ccdi_bank_statement bs
|
||||
<where>
|
||||
<include refid="statementFilterWhere"/>
|
||||
</where>
|
||||
<include refid="statementOrderBy"/>
|
||||
</select>
|
||||
|
||||
<select id="selectStatementListForExport" resultMap="CcdiBankStatementListVOResultMap">
|
||||
SELECT
|
||||
<include refid="statementListColumns"/>
|
||||
FROM ccdi_bank_statement bs
|
||||
<where>
|
||||
<include refid="statementFilterWhere"/>
|
||||
</where>
|
||||
<include refid="statementOrderBy"/>
|
||||
</select>
|
||||
|
||||
<select id="selectStatementDetailById" resultMap="CcdiBankStatementDetailVOResultMap">
|
||||
SELECT
|
||||
bs.bank_statement_id AS bankStatementId,
|
||||
bs.project_id AS projectId,
|
||||
bs.TRX_DATE AS trxDate,
|
||||
bs.CURRENCY AS currency,
|
||||
bs.LE_ACCOUNT_NO AS leAccountNo,
|
||||
bs.LE_ACCOUNT_NAME AS leAccountName,
|
||||
bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName,
|
||||
bs.CUSTOMER_ACCOUNT_NO AS customerAccountNo,
|
||||
bs.customer_bank AS customerBank,
|
||||
bs.customer_reference AS customerReference,
|
||||
bs.USER_MEMO AS userMemo,
|
||||
bs.BANK_COMMENTS AS bankComments,
|
||||
bs.BANK_TRX_NUMBER AS bankTrxNumber,
|
||||
bs.BANK AS bank,
|
||||
bs.CASH_TYPE AS cashType,
|
||||
bs.AMOUNT_DR AS amountDr,
|
||||
bs.AMOUNT_CR AS amountCr,
|
||||
bs.AMOUNT_BALANCE AS amountBalance,
|
||||
<include refid="displayAmountExpr"/> AS displayAmount,
|
||||
bs.TRX_FLAG AS trxFlag,
|
||||
bs.TRX_TYPE AS trxType,
|
||||
bs.EXCEPTION_TYPE AS exceptionType,
|
||||
bs.internal_flag AS internalFlag,
|
||||
bs.payment_method AS paymentMethod,
|
||||
bs.cret_no AS cretNo,
|
||||
bs.CREATE_DATE AS createDate
|
||||
FROM ccdi_bank_statement bs
|
||||
WHERE bs.bank_statement_id = #{bankStatementId}
|
||||
</select>
|
||||
|
||||
<select id="selectFilterOptions" resultMap="CcdiBankStatementFilterOptionsVOResultMap">
|
||||
SELECT #{projectId} AS project_id
|
||||
</select>
|
||||
|
||||
<select id="selectOurSubjectOptions" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementOptionVO">
|
||||
SELECT DISTINCT
|
||||
TRIM(bs.LE_ACCOUNT_NAME) AS label,
|
||||
TRIM(bs.LE_ACCOUNT_NAME) AS value
|
||||
FROM ccdi_bank_statement bs
|
||||
WHERE bs.project_id = #{projectId}
|
||||
AND bs.LE_ACCOUNT_NAME IS NOT NULL
|
||||
AND TRIM(bs.LE_ACCOUNT_NAME) != ''
|
||||
ORDER BY TRIM(bs.LE_ACCOUNT_NAME)
|
||||
</select>
|
||||
|
||||
<select id="selectOurBankOptions" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementOptionVO">
|
||||
SELECT DISTINCT
|
||||
TRIM(bs.BANK) AS label,
|
||||
TRIM(bs.BANK) AS value
|
||||
FROM ccdi_bank_statement bs
|
||||
WHERE bs.project_id = #{projectId}
|
||||
AND bs.BANK IS NOT NULL
|
||||
AND TRIM(bs.BANK) != ''
|
||||
ORDER BY TRIM(bs.BANK)
|
||||
</select>
|
||||
|
||||
<select id="selectOurAccountOptions" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementOptionVO">
|
||||
SELECT DISTINCT
|
||||
TRIM(bs.LE_ACCOUNT_NO) AS label,
|
||||
TRIM(bs.LE_ACCOUNT_NO) AS value
|
||||
FROM ccdi_bank_statement bs
|
||||
WHERE bs.project_id = #{projectId}
|
||||
AND bs.LE_ACCOUNT_NO IS NOT NULL
|
||||
AND TRIM(bs.LE_ACCOUNT_NO) != ''
|
||||
ORDER BY TRIM(bs.LE_ACCOUNT_NO)
|
||||
</select>
|
||||
|
||||
<insert id="insertBatch" parameterType="java.util.List">
|
||||
insert into ccdi_bank_statement (
|
||||
project_id, LE_ID, ACCOUNT_ID, group_id,
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.ruoyi.ccdi.project.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiBankStatementService;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiBankStatementControllerTest {
|
||||
|
||||
@InjectMocks
|
||||
private CcdiBankStatementController controller;
|
||||
|
||||
@Mock
|
||||
private ICcdiBankStatementService bankStatementService;
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
|
||||
@Test
|
||||
void options_shouldReturnAjaxResultSuccess() {
|
||||
when(bankStatementService.getFilterOptions(100L)).thenReturn(new CcdiBankStatementFilterOptionsVO());
|
||||
|
||||
AjaxResult result = controller.getOptions(100L);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void list_shouldReturnTableData() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter("pageNum", "1");
|
||||
request.setParameter("pageSize", "10");
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
|
||||
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
queryDTO.setProjectId(100L);
|
||||
CcdiBankStatementListVO row = new CcdiBankStatementListVO();
|
||||
row.setBankStatementId(1L);
|
||||
Page<CcdiBankStatementListVO> page = new Page<>(1, 10);
|
||||
page.setRecords(List.of(row));
|
||||
page.setTotal(1);
|
||||
when(bankStatementService.selectStatementPage(any(), same(queryDTO))).thenReturn(page);
|
||||
|
||||
TableDataInfo result = controller.list(queryDTO);
|
||||
|
||||
assertEquals(200, result.getCode());
|
||||
assertEquals(1L, result.getTotal());
|
||||
assertEquals(1, result.getRows().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detail_shouldReturnAjaxResultSuccess() {
|
||||
CcdiBankStatementDetailVO detailVO = new CcdiBankStatementDetailVO();
|
||||
detailVO.setBankStatementId(1000L);
|
||||
when(bankStatementService.getStatementDetail(1000L)).thenReturn(detailVO);
|
||||
|
||||
AjaxResult result = controller.getDetail(1000L);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals(detailVO, result.get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void export_shouldWriteExcelResponse() {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
CcdiBankStatementExcel row = new CcdiBankStatementExcel();
|
||||
row.setLeAccountNo("6222");
|
||||
row.setDisplayAmount(new BigDecimal("10.00"));
|
||||
when(bankStatementService.selectStatementListForExport(same(queryDTO))).thenReturn(List.of(row));
|
||||
|
||||
controller.export(response, queryDTO);
|
||||
|
||||
verify(bankStatementService).selectStatementListForExport(same(queryDTO));
|
||||
assertTrue(response.getContentType().startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
|
||||
assertTrue(response.getContentAsByteArray().length > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
|
||||
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import org.apache.ibatis.type.TypeAliasRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiBankStatementMapperXmlTest {
|
||||
|
||||
private static final String RESOURCE = "mapper/ccdi/project/CcdiBankStatementMapper.xml";
|
||||
|
||||
@Test
|
||||
void selectStatementPage_shouldKeepWhitespaceBeforeDynamicAndConditions() throws Exception {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));
|
||||
registerTypeAliases(configuration.getTypeAliasRegistry());
|
||||
configuration.getLanguageRegistry().register(XMLLanguageDriver.class);
|
||||
configuration.addMapper(CcdiBankStatementMapper.class);
|
||||
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||
XMLMapperBuilder xmlMapperBuilder =
|
||||
new XMLMapperBuilder(inputStream, configuration, RESOURCE, configuration.getSqlFragments());
|
||||
xmlMapperBuilder.parse();
|
||||
}
|
||||
|
||||
MappedStatement mappedStatement = configuration.getMappedStatement(
|
||||
"com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectStatementPage");
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
queryDTO.setProjectId(33L);
|
||||
queryDTO.setTabType("in");
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("query", queryDTO);
|
||||
|
||||
BoundSql boundSql = mappedStatement.getBoundSql(params);
|
||||
String sql = boundSql.getSql().replaceAll("\\s+", " ").trim();
|
||||
|
||||
assertFalse(sql.contains("?AND"), sql);
|
||||
assertTrue(sql.contains("(bs.project_id = ?) AND IFNULL"), sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void statementFilterWhere_shouldWrapBaseConditionToAvoidParameterAndCollision() throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
String projectIdLine = Arrays.stream(xml.split("\\R"))
|
||||
.filter(line -> line.contains("bs.project_id = #{query.projectId}"))
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
|
||||
assertTrue(xml.contains("AND (bs.project_id = #{query.projectId})"), xml);
|
||||
assertTrue(projectIdLine.contains("AND (bs.project_id = #{query.projectId})"), projectIdLine);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void statementFilterWhere_shouldWrapIncludedCaseExpressionsToAvoidAndCaseCollision() throws Exception {
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(xml.contains("AND (<include refid=\"parsedTrxDateExpr\"/>) <![CDATA[ >= ]]>"), xml);
|
||||
assertTrue(xml.contains("AND (<include refid=\"parsedTrxDateExpr\"/>) <![CDATA[ <= ]]>"), xml);
|
||||
assertTrue(xml.contains("AND (<include refid=\"absoluteAmountExpr\"/>) <![CDATA[ >= ]]>"), xml);
|
||||
assertTrue(xml.contains("AND (<include refid=\"absoluteAmountExpr\"/>) <![CDATA[ <= ]]>"), xml);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectStatementPage_shouldKeepWhitespaceBeforeDateRangeExpressions() throws Exception {
|
||||
MappedStatement mappedStatement = loadMappedStatement(
|
||||
"com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectStatementPage");
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
queryDTO.setProjectId(33L);
|
||||
queryDTO.setTransactionStartTime("2024-01-01 00:00:00");
|
||||
queryDTO.setTransactionEndTime("2024-01-31 23:59:59");
|
||||
|
||||
String sql = renderSql(mappedStatement, queryDTO);
|
||||
|
||||
assertFalse(sql.contains("ANDCASE"), sql);
|
||||
assertTrue(sql.contains("AND ( CASE WHEN bs.TRX_DATE"), sql);
|
||||
assertTrue(sql.contains("END ) >="), sql);
|
||||
assertTrue(sql.contains("END ) <="), sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectStatementPage_shouldKeepWhitespaceBeforeAmountRangeExpressions() throws Exception {
|
||||
MappedStatement mappedStatement = loadMappedStatement(
|
||||
"com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectStatementPage");
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
queryDTO.setProjectId(33L);
|
||||
queryDTO.setAmountMin(java.math.BigDecimal.ONE);
|
||||
queryDTO.setAmountMax(new java.math.BigDecimal("100"));
|
||||
|
||||
String sql = renderSql(mappedStatement, queryDTO);
|
||||
|
||||
assertFalse(sql.contains("ANDCASE"), sql);
|
||||
assertTrue(sql.contains("AND ( CASE WHEN IFNULL(bs.AMOUNT_CR, 0) > 0"), sql);
|
||||
assertTrue(sql.contains("END ) >= ?"), sql);
|
||||
assertTrue(sql.contains("END ) <= ?"), sql);
|
||||
}
|
||||
|
||||
private MappedStatement loadMappedStatement(String statementId) throws Exception {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));
|
||||
registerTypeAliases(configuration.getTypeAliasRegistry());
|
||||
configuration.getLanguageRegistry().register(XMLLanguageDriver.class);
|
||||
configuration.addMapper(CcdiBankStatementMapper.class);
|
||||
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
|
||||
XMLMapperBuilder xmlMapperBuilder =
|
||||
new XMLMapperBuilder(inputStream, configuration, RESOURCE, configuration.getSqlFragments());
|
||||
xmlMapperBuilder.parse();
|
||||
}
|
||||
return configuration.getMappedStatement(statementId);
|
||||
}
|
||||
|
||||
private String renderSql(MappedStatement mappedStatement, CcdiBankStatementQueryDTO queryDTO) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("query", queryDTO);
|
||||
BoundSql boundSql = mappedStatement.getBoundSql(params);
|
||||
return boundSql.getSql().replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
|
||||
private void registerTypeAliases(TypeAliasRegistry typeAliasRegistry) {
|
||||
typeAliasRegistry.registerAlias("map", Map.class);
|
||||
}
|
||||
|
||||
private static class NoOpDataSource implements DataSource {
|
||||
|
||||
@Override
|
||||
public java.sql.Connection getConnection() {
|
||||
throw new UnsupportedOperationException("Not required for SQL rendering tests");
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.sql.Connection getConnection(String username, String password) {
|
||||
throw new UnsupportedOperationException("Not required for SQL rendering tests");
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.PrintWriter getLogWriter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLogWriter(java.io.PrintWriter out) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoginTimeout(int seconds) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLoginTimeout() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.logging.Logger getParentLogger() {
|
||||
return java.util.logging.Logger.getGlobal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unwrap(Class<T> iface) {
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWrapperFor(Class<?> iface) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.ruoyi.ccdi.project.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementOptionVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiBankStatementServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private CcdiBankStatementServiceImpl service;
|
||||
|
||||
@Mock
|
||||
private CcdiBankStatementMapper bankStatementMapper;
|
||||
|
||||
@Test
|
||||
void getFilterOptions_shouldReturnProjectWideOptions() {
|
||||
CcdiBankStatementFilterOptionsVO options = new CcdiBankStatementFilterOptionsVO();
|
||||
options.setOurSubjectOptions(List.of(new CcdiBankStatementOptionVO("主体A", "主体A")));
|
||||
when(bankStatementMapper.selectFilterOptions(100L)).thenReturn(options);
|
||||
|
||||
CcdiBankStatementFilterOptionsVO result = service.getFilterOptions(100L);
|
||||
|
||||
assertEquals(1, result.getOurSubjectOptions().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageQuery_shouldNormalizeSortFieldAndDirection() {
|
||||
Page<CcdiBankStatementListVO> page = new Page<>(1, 10);
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
queryDTO.setProjectId(100L);
|
||||
queryDTO.setOrderBy("amount");
|
||||
queryDTO.setOrderDirection("desc");
|
||||
doReturn(page).when(bankStatementMapper).selectStatementPage(eq(page), same(queryDTO));
|
||||
|
||||
service.selectStatementPage(page, queryDTO);
|
||||
|
||||
verify(bankStatementMapper).selectStatementPage(eq(page), same(queryDTO));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeQuery_shouldFallbackToSafeDefaults() {
|
||||
Page<CcdiBankStatementListVO> page = new Page<>(1, 10);
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
queryDTO.setProjectId(100L);
|
||||
queryDTO.setOrderBy("drop table");
|
||||
queryDTO.setOrderDirection("sideways");
|
||||
doReturn(page).when(bankStatementMapper).selectStatementPage(eq(page), same(queryDTO));
|
||||
|
||||
service.selectStatementPage(page, queryDTO);
|
||||
|
||||
assertEquals("trxDate", queryDTO.getOrderBy());
|
||||
assertEquals("desc", queryDTO.getOrderDirection());
|
||||
assertEquals("all", queryDTO.getTabType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeQuery_shouldTrimBlankStringsToNull() {
|
||||
Page<CcdiBankStatementListVO> page = new Page<>(1, 10);
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
queryDTO.setProjectId(100L);
|
||||
queryDTO.setCounterpartyName(" ");
|
||||
queryDTO.setUserMemo(" ");
|
||||
queryDTO.setCounterpartyAccount(" ");
|
||||
queryDTO.setTransactionType(" ");
|
||||
doReturn(page).when(bankStatementMapper).selectStatementPage(eq(page), same(queryDTO));
|
||||
|
||||
service.selectStatementPage(page, queryDTO);
|
||||
|
||||
assertNull(queryDTO.getCounterpartyName());
|
||||
assertNull(queryDTO.getUserMemo());
|
||||
assertNull(queryDTO.getCounterpartyAccount());
|
||||
assertNull(queryDTO.getTransactionType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectStatementListForExport_shouldMapDisplayColumns() {
|
||||
CcdiBankStatementListVO row = new CcdiBankStatementListVO();
|
||||
row.setTrxDate("2024-02-01 10:33:44");
|
||||
row.setLeAccountNo("6222");
|
||||
row.setLeAccountName("张三");
|
||||
row.setDisplayAmount(new BigDecimal("-8.00"));
|
||||
when(bankStatementMapper.selectStatementListForExport(any())).thenReturn(List.of(row));
|
||||
|
||||
List<CcdiBankStatementExcel> result = service.selectStatementListForExport(new CcdiBankStatementQueryDTO());
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("6222", result.get(0).getLeAccountNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStatementDetail_shouldDelegateToMapper() {
|
||||
CcdiBankStatementDetailVO detailVO = new CcdiBankStatementDetailVO();
|
||||
detailVO.setBankStatementId(200L);
|
||||
when(bankStatementMapper.selectStatementDetailById(200L)).thenReturn(detailVO);
|
||||
|
||||
CcdiBankStatementDetailVO result = service.getStatementDetail(200L);
|
||||
|
||||
assertSame(detailVO, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
# Project Detail Transaction Query Backend Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build the backend query, filter options, detail, and export endpoints for the project detail bank statement page using local `ccdi_bank_statement` data.
|
||||
|
||||
**Architecture:** Extend the existing `ccdi-project` module with a dedicated bank statement controller and service, while reusing the current `CcdiBankStatementMapper` plus XML dynamic SQL. Keep DTO/VO layering strict, compute direction and display amount in the query layer, and export through `ruoyi-common` `ExcelUtil` so the page and export share one query contract.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MyBatis XML, Mockito + JUnit 5, RuoYi `ExcelUtil`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Scaffold DTO/VO contracts and the first failing service test
|
||||
|
||||
**Files:**
|
||||
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiBankStatementQueryDTO.java`
|
||||
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementOptionVO.java`
|
||||
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementFilterOptionsVO.java`
|
||||
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementListVO.java`
|
||||
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementDetailVO.java`
|
||||
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiBankStatementService.java`
|
||||
- Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiBankStatementServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private CcdiBankStatementServiceImpl service;
|
||||
|
||||
@Mock
|
||||
private CcdiBankStatementMapper bankStatementMapper;
|
||||
|
||||
@Test
|
||||
void getFilterOptions_shouldReturnProjectWideOptions() {
|
||||
CcdiBankStatementFilterOptionsVO options = new CcdiBankStatementFilterOptionsVO();
|
||||
options.setOurSubjectOptions(List.of(new CcdiBankStatementOptionVO("主体A", "主体A")));
|
||||
when(bankStatementMapper.selectFilterOptions(100L)).thenReturn(options);
|
||||
|
||||
CcdiBankStatementFilterOptionsVO result = service.getFilterOptions(100L);
|
||||
|
||||
assertEquals(1, result.getOurSubjectOptions().size());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest`
|
||||
|
||||
Expected: FAIL with missing `CcdiBankStatementServiceImpl`, DTO, or mapper methods.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```java
|
||||
public interface ICcdiBankStatementService {
|
||||
CcdiBankStatementFilterOptionsVO getFilterOptions(Long projectId);
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Data
|
||||
public class CcdiBankStatementOptionVO {
|
||||
private String label;
|
||||
private String value;
|
||||
|
||||
public CcdiBankStatementOptionVO(String label, String value) {
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add the remaining DTO/VO classes with only the fields already confirmed in the design.
|
||||
|
||||
**Step 4: Run test to verify it still fails only on the missing service implementation**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest`
|
||||
|
||||
Expected: FAIL with `CcdiBankStatementServiceImpl` not found or method not implemented.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiBankStatementQueryDTO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementOptionVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementFilterOptionsVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementListVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementDetailVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiBankStatementService.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java
|
||||
git commit -m "新增流水明细查询后端契约与测试骨架"
|
||||
```
|
||||
|
||||
### Task 2: Implement mapper query methods for options, list, detail, and export
|
||||
|
||||
**Files:**
|
||||
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java`
|
||||
- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml`
|
||||
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add tests that assert the service delegates to mapper methods with normalized arguments:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void pageQuery_shouldNormalizeSortFieldAndDirection() {
|
||||
Page<CcdiBankStatementListVO> page = new Page<>(1, 10);
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
queryDTO.setProjectId(100L);
|
||||
queryDTO.setOrderBy("amount");
|
||||
queryDTO.setOrderDirection("desc");
|
||||
|
||||
service.selectStatementPage(page, queryDTO);
|
||||
|
||||
verify(bankStatementMapper).selectStatementPage(eq(page), same(queryDTO));
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest`
|
||||
|
||||
Expected: FAIL because mapper methods `selectStatementPage`, `selectStatementListForExport`, `selectStatementDetailById`, `selectFilterOptions` are missing.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
In `CcdiBankStatementMapper.java`, add:
|
||||
|
||||
```java
|
||||
Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
|
||||
@Param("query") CcdiBankStatementQueryDTO query);
|
||||
|
||||
List<CcdiBankStatementListVO> selectStatementListForExport(@Param("query") CcdiBankStatementQueryDTO query);
|
||||
|
||||
CcdiBankStatementDetailVO selectStatementDetailById(@Param("bankStatementId") Long bankStatementId);
|
||||
|
||||
CcdiBankStatementFilterOptionsVO selectFilterOptions(@Param("projectId") Long projectId);
|
||||
```
|
||||
|
||||
In `CcdiBankStatementMapper.xml`, add:
|
||||
|
||||
- a reusable SQL fragment for common filters
|
||||
- a reusable expression for parsed transaction time
|
||||
- a reusable expression for signed display amount
|
||||
- separate selects for page, export, detail, and distinct project options
|
||||
|
||||
**Step 4: Run test to verify it passes or fails only on service logic**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest`
|
||||
|
||||
Expected: mapper-signature errors gone; remaining failures only relate to unimplemented service methods.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java
|
||||
git commit -m "补充流水明细查询Mapper与动态SQL"
|
||||
```
|
||||
|
||||
### Task 3: Implement service normalization, page query, and project-wide filter options
|
||||
|
||||
**Files:**
|
||||
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java`
|
||||
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add focused service tests for:
|
||||
|
||||
- default `tabType=all`
|
||||
- invalid `orderBy` falling back to `trxDate`
|
||||
- invalid `orderDirection` falling back to `desc`
|
||||
- blank string fields trimming to `null`
|
||||
|
||||
```java
|
||||
@Test
|
||||
void normalizeQuery_shouldFallbackToSafeDefaults() {
|
||||
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
|
||||
queryDTO.setProjectId(100L);
|
||||
queryDTO.setOrderBy("drop table");
|
||||
queryDTO.setOrderDirection("sideways");
|
||||
|
||||
service.selectStatementPage(new Page<>(1, 10), queryDTO);
|
||||
|
||||
assertEquals("trxDate", queryDTO.getOrderBy());
|
||||
assertEquals("desc", queryDTO.getOrderDirection());
|
||||
assertEquals("all", queryDTO.getTabType());
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest`
|
||||
|
||||
Expected: FAIL because service normalization logic is missing.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
In `CcdiBankStatementServiceImpl.java`:
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
|
||||
|
||||
@Resource
|
||||
private CcdiBankStatementMapper bankStatementMapper;
|
||||
|
||||
@Override
|
||||
public Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
|
||||
CcdiBankStatementQueryDTO queryDTO) {
|
||||
normalizeQuery(queryDTO);
|
||||
return bankStatementMapper.selectStatementPage(page, queryDTO);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Implement `normalizeQuery(queryDTO)` to enforce the white-listed sort fields and normalize blank strings and booleans before delegating to mapper.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java
|
||||
git commit -m "实现流水明细查询服务层规范化逻辑"
|
||||
```
|
||||
|
||||
### Task 4: Add detail lookup and export model with a failing export test
|
||||
|
||||
**Files:**
|
||||
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiBankStatementExcel.java`
|
||||
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiBankStatementService.java`
|
||||
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java`
|
||||
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add a service test for export mapping:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void selectStatementListForExport_shouldMapDisplayColumns() {
|
||||
CcdiBankStatementListVO row = new CcdiBankStatementListVO();
|
||||
row.setTrxDate("2024-02-01 10:33:44");
|
||||
row.setLeAccountNo("6222");
|
||||
row.setLeAccountName("张三");
|
||||
row.setDisplayAmount(new BigDecimal("-8.00"));
|
||||
when(bankStatementMapper.selectStatementListForExport(any())).thenReturn(List.of(row));
|
||||
|
||||
List<CcdiBankStatementExcel> result = service.selectStatementListForExport(new CcdiBankStatementQueryDTO());
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("6222", result.get(0).getLeAccountNo());
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest`
|
||||
|
||||
Expected: FAIL because export model and export service method do not exist.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Create `CcdiBankStatementExcel.java` with `@Excel` annotations for:
|
||||
|
||||
- `trxDate`
|
||||
- `leAccountNo`
|
||||
- `leAccountName`
|
||||
- `customerAccountName`
|
||||
- `customerAccountNo`
|
||||
- `userMemo`
|
||||
- `cashType`
|
||||
- `displayAmount`
|
||||
|
||||
Add service methods:
|
||||
|
||||
```java
|
||||
List<CcdiBankStatementExcel> selectStatementListForExport(CcdiBankStatementQueryDTO queryDTO);
|
||||
|
||||
CcdiBankStatementDetailVO getStatementDetail(Long bankStatementId);
|
||||
```
|
||||
|
||||
Map export rows from `CcdiBankStatementListVO` to `CcdiBankStatementExcel`.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiBankStatementExcel.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiBankStatementService.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java
|
||||
git commit -m "实现流水明细导出模型与详情查询"
|
||||
```
|
||||
|
||||
### Task 5: Add controller endpoints and the first failing controller test
|
||||
|
||||
**Files:**
|
||||
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java`
|
||||
- Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementControllerTest.java`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiBankStatementControllerTest {
|
||||
|
||||
@InjectMocks
|
||||
private CcdiBankStatementController controller;
|
||||
|
||||
@Mock
|
||||
private ICcdiBankStatementService bankStatementService;
|
||||
|
||||
@Test
|
||||
void options_shouldReturnAjaxResultSuccess() {
|
||||
when(bankStatementService.getFilterOptions(100L)).thenReturn(new CcdiBankStatementFilterOptionsVO());
|
||||
|
||||
AjaxResult result = controller.getOptions(100L);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementControllerTest`
|
||||
|
||||
Expected: FAIL because controller class and methods do not exist.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Add endpoints:
|
||||
|
||||
- `GET /ccdi/project/bank-statement/list`
|
||||
- `GET /ccdi/project/bank-statement/options`
|
||||
- `GET /ccdi/project/bank-statement/detail/{bankStatementId}`
|
||||
- `POST /ccdi/project/bank-statement/export`
|
||||
|
||||
Use `TableSupport.buildPageRequest()` for paging and `ExcelUtil<CcdiBankStatementExcel>` for export:
|
||||
|
||||
```java
|
||||
ExcelUtil<CcdiBankStatementExcel> util = new ExcelUtil<>(CcdiBankStatementExcel.class);
|
||||
util.exportExcel(response, list, "流水明细");
|
||||
```
|
||||
|
||||
Guard permissions with:
|
||||
|
||||
- query/list/detail/options: `ccdi:project:query`
|
||||
- export: `ccdi:project:export`
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementControllerTest`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementControllerTest.java
|
||||
git commit -m "新增流水明细查询控制器接口"
|
||||
```
|
||||
|
||||
### Task 6: Verify the backend end-to-end inside the module
|
||||
|
||||
**Files:**
|
||||
- Modify if needed after failures: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java`
|
||||
- Modify if needed after failures: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java`
|
||||
- Modify if needed after failures: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml`
|
||||
|
||||
**Step 1: Run the focused backend tests**
|
||||
|
||||
Run: `mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest,CcdiBankStatementControllerTest`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run the module compile**
|
||||
|
||||
Run: `mvn clean compile -pl ccdi-project -am`
|
||||
|
||||
Expected: BUILD SUCCESS
|
||||
|
||||
**Step 3: Smoke-check the mapper XML and endpoint contracts**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn test -pl ccdi-project -Dtest=CcdiBankStatementServiceImplTest -q
|
||||
```
|
||||
|
||||
Expected: PASS without XML binding errors or missing mapper statements.
|
||||
|
||||
**Step 4: Fix any compile or binding failures with the smallest possible patch**
|
||||
|
||||
Typical fixes:
|
||||
|
||||
- wrong `@Param("query")` names
|
||||
- missing VO field aliases in SQL
|
||||
- `Page<>` generic mismatch
|
||||
- `ExcelUtil` export field annotation issues
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementControllerTest.java
|
||||
git commit -m "完成流水明细查询后端实现与校验"
|
||||
```
|
||||
371
docs/plans/2026-03-10-project-detail-transaction-query-design.md
Normal file
371
docs/plans/2026-03-10-project-detail-transaction-query-design.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# 项目详情流水明细查询设计
|
||||
|
||||
## 概述
|
||||
|
||||
本次设计面向项目详情页中的“流水明细查询”菜单,按原型图实现“查询 + 筛选 + 导出”能力,不包含旧需求中的“加入分析”“二次分析”等扩展功能。
|
||||
|
||||
页面默认查询当前项目下全部已入库流水,数据源仅使用本地表 `ccdi_bank_statement`,不再依赖上传记录的“查看流水”跳转。上传数据页同步移除上传记录表的操作列,后续不再从上传记录进入流水明细页。
|
||||
|
||||
## 已确认范围
|
||||
|
||||
- 页面入口保留在项目详情页的 `detail` 菜单
|
||||
- 默认查询范围为当前 `projectId` 下全部流水
|
||||
- 上传页移除“查看流水”入口
|
||||
- “导出流水”需要实现真实功能
|
||||
- 导出范围为当前页面筛选后的全部结果
|
||||
- 本次只做“查询 + 筛选 + 导出”
|
||||
- 摘要筛选仅使用 `userMemo`
|
||||
- 筛选栏中的多选项需要基于整个项目维度单独查询
|
||||
- 进入页面时即并行加载列表数据和项目级多选项
|
||||
|
||||
## 方案对比
|
||||
|
||||
### 方案一:本地库专用查询模块
|
||||
|
||||
- 在 `ccdi-project` 模块新增银行流水查询 Controller、Service、Mapper 查询方法
|
||||
- 页面直接查询本地 `ccdi_bank_statement`
|
||||
- 导出使用本地查询条件复用
|
||||
|
||||
优点:
|
||||
|
||||
- 与现有“上传后落本地库”架构一致
|
||||
- 查询与导出结果稳定,可复现
|
||||
- 后续可继续扩展更多项目级分析能力
|
||||
|
||||
缺点:
|
||||
|
||||
- 需要补充 DTO、VO、Mapper SQL、导出模型
|
||||
|
||||
### 方案二:大而全聚合接口
|
||||
|
||||
- 用一个接口同时返回列表、多选项、统计信息
|
||||
|
||||
优点:
|
||||
|
||||
- 前端调用少
|
||||
|
||||
缺点:
|
||||
|
||||
- 接口职责混杂
|
||||
- 后续增加筛选维度时维护成本高
|
||||
- 导出仍需要单独处理
|
||||
|
||||
### 方案三:直接回源 LSFX 接口
|
||||
|
||||
- 页面查询与导出实时调用流水分析平台接口
|
||||
|
||||
优点:
|
||||
|
||||
- 本地查询层改动少
|
||||
|
||||
缺点:
|
||||
|
||||
- 与现有本地入库方案冲突
|
||||
- 项目级全量查询稳定性差
|
||||
- 页面与导出口径难统一
|
||||
|
||||
## 选型
|
||||
|
||||
采用方案一:本地库专用查询模块。
|
||||
|
||||
该方案与当前 `ccdi_bank_statement`、`CcdiFileUploadServiceImpl` 的入库逻辑一致,最符合“项目维度统一查询 + 当前筛选导出”的实现要求。
|
||||
|
||||
## 页面结构
|
||||
|
||||
页面仍由 `ruoyi-ui/src/views/ccdiProject/detail.vue` 动态加载 `DetailQuery.vue`。
|
||||
|
||||
`DetailQuery.vue` 从占位组件升级为正式页面,整体布局分为左右两栏:
|
||||
|
||||
- 左侧:固定筛选栏
|
||||
- 右侧:结果区域
|
||||
|
||||
右侧结果区域包含:
|
||||
|
||||
- 标题“流水明细查询”
|
||||
- 顶部页签:`全部` / `流入` / `流出`
|
||||
- 导出按钮:`导出流水`
|
||||
- 列表表格
|
||||
- 分页器
|
||||
|
||||
列表中的操作位保留只读详情能力,用于打开详情抽屉或弹窗查看单条流水完整字段。
|
||||
|
||||
## 筛选项设计
|
||||
|
||||
左侧筛选栏按原型提供以下条件:
|
||||
|
||||
1. 交易时间
|
||||
- 对应字段:`trxDate`
|
||||
- 支持起止范围查询
|
||||
- 后端兼容 `yyyy-MM-dd HH:mm:ss` 和 `yyyy-MM-dd`
|
||||
|
||||
2. 对方名称 + 空值
|
||||
- 对应字段:`customerAccountName`
|
||||
- 输入框为模糊匹配
|
||||
- 勾选空值时匹配 `null`、空串、全空白字符串
|
||||
|
||||
3. 摘要 + 空值
|
||||
- 对应字段:`userMemo`
|
||||
- 输入框为模糊匹配
|
||||
- 勾选空值时匹配 `null`、空串、全空白字符串
|
||||
|
||||
4. 本方主体
|
||||
- 对应字段:`leAccountName`
|
||||
- 多选
|
||||
|
||||
5. 本方银行
|
||||
- 对应字段:`bank`
|
||||
- 多选
|
||||
|
||||
6. 本方账户
|
||||
- 对应字段:`leAccountNo`
|
||||
- 多选
|
||||
|
||||
7. 交易金额
|
||||
- 使用绝对值范围筛选
|
||||
- 统一适配 `全部 / 流入 / 流出` 三个页签
|
||||
|
||||
8. 对方账户 + 空值
|
||||
- 对应字段:`customerAccountNo`
|
||||
- 输入框为模糊匹配
|
||||
|
||||
9. 交易类型 + 空值
|
||||
- 对应字段:`cashType`
|
||||
- 输入框为模糊匹配
|
||||
|
||||
## 多选项加载规则
|
||||
|
||||
本方主体、本方银行、本方账户三类多选项不从当前列表结果派生,而是通过单独接口按整个项目维度去重查询。
|
||||
|
||||
进入页面时前端并行加载:
|
||||
|
||||
- 列表接口:默认查询当前项目全部流水
|
||||
- 多选项接口:返回整个项目维度的全部主体、银行、账户选项
|
||||
|
||||
多选项接口返回值:
|
||||
|
||||
- `ourSubjectOptions`
|
||||
- `ourBankOptions`
|
||||
- `ourAccountOptions`
|
||||
|
||||
前端多选框内部搜索仅在已加载的项目级选项上做本地过滤,不再触发额外远程请求。
|
||||
|
||||
## 列表列设计
|
||||
|
||||
表格列与原型语义对齐:
|
||||
|
||||
1. 交易时间
|
||||
- 字段:`trxDate`
|
||||
|
||||
2. 本行账户/主体
|
||||
- 主行:`leAccountNo`
|
||||
- 副行:`leAccountName`
|
||||
|
||||
3. 对方名称/账户
|
||||
- 主行:`customerAccountName`
|
||||
- 副行:`customerAccountNo`
|
||||
|
||||
4. 摘要/交易类型
|
||||
- 主行:`userMemo`
|
||||
- 副行:`cashType`
|
||||
|
||||
5. 交易金额
|
||||
- 流入显示 `+amountCr`
|
||||
- 流出显示 `-amountDr`
|
||||
- 页面展示统一输出计算后的 `displayAmount`
|
||||
|
||||
6. 操作
|
||||
- 只读详情
|
||||
|
||||
## 页签与排序规则
|
||||
|
||||
### 页签规则
|
||||
|
||||
- `全部`:不过滤金额方向
|
||||
- `流入`:仅查询 `amount_cr > 0`
|
||||
- `流出`:仅查询 `amount_dr > 0`
|
||||
|
||||
### 排序规则
|
||||
|
||||
支持两个排序字段:
|
||||
|
||||
- 交易时间
|
||||
- 交易金额
|
||||
|
||||
排序方向支持:
|
||||
|
||||
- 升序
|
||||
- 降序
|
||||
|
||||
交易金额排序按绝对值排序,和金额范围筛选保持一致。
|
||||
|
||||
排序字段必须由后端白名单控制,禁止前端任意传值直接拼接 SQL。
|
||||
|
||||
## 后端接口设计
|
||||
|
||||
建议新增专用 Controller:`CcdiBankStatementController`
|
||||
|
||||
### 1. 列表分页查询
|
||||
|
||||
- 路径:`GET /ccdi/project/bank-statement/list`
|
||||
- 入参:`CcdiBankStatementQueryDTO`
|
||||
- 返回:`TableDataInfo`
|
||||
|
||||
入参字段包括:
|
||||
|
||||
- `projectId`
|
||||
- `tabType`
|
||||
- `transactionStartTime`
|
||||
- `transactionEndTime`
|
||||
- `counterpartyName`
|
||||
- `counterpartyNameEmpty`
|
||||
- `userMemo`
|
||||
- `userMemoEmpty`
|
||||
- `ourSubjects`
|
||||
- `ourBanks`
|
||||
- `ourAccounts`
|
||||
- `amountMin`
|
||||
- `amountMax`
|
||||
- `counterpartyAccount`
|
||||
- `counterpartyAccountEmpty`
|
||||
- `transactionType`
|
||||
- `transactionTypeEmpty`
|
||||
- `orderBy`
|
||||
- `orderDirection`
|
||||
|
||||
### 2. 多选项查询
|
||||
|
||||
- 路径:`GET /ccdi/project/bank-statement/options`
|
||||
- 入参:`projectId`
|
||||
- 返回:`CcdiBankStatementFilterOptionsVO`
|
||||
|
||||
### 3. 单条详情
|
||||
|
||||
- 路径:`GET /ccdi/project/bank-statement/detail/{bankStatementId}`
|
||||
- 返回:`CcdiBankStatementDetailVO`
|
||||
|
||||
### 4. 导出
|
||||
|
||||
- 路径:`POST /ccdi/project/bank-statement/export`
|
||||
- 入参:复用 `CcdiBankStatementQueryDTO`
|
||||
- 返回:Excel 文件流
|
||||
|
||||
## 服务层设计
|
||||
|
||||
建议新增:
|
||||
|
||||
- `ICcdiBankStatementService`
|
||||
- `CcdiBankStatementServiceImpl`
|
||||
|
||||
职责拆分:
|
||||
|
||||
- 列表查询参数校验与标准化
|
||||
- 排序字段白名单处理
|
||||
- 时间范围解析与兼容
|
||||
- 多选项去重查询
|
||||
- 单条详情查询
|
||||
- 导出列表查询与导出模型组装
|
||||
|
||||
## Mapper 设计
|
||||
|
||||
保留现有 `CcdiBankStatementMapper`,在接口和 XML 中新增查询方法,不新增独立 Mapper。
|
||||
|
||||
建议补充的方法:
|
||||
|
||||
- `selectStatementPage`
|
||||
- `selectStatementListForExport`
|
||||
- `selectFilterOptions`
|
||||
- `selectStatementDetailById`
|
||||
|
||||
SQL 规则:
|
||||
|
||||
- 基于 `project_id` 做主过滤
|
||||
- 使用动态 SQL 处理输入框、空值、多选数组
|
||||
- 用统一表达式计算排序时间字段
|
||||
- 用统一表达式计算展示金额与金额排序值
|
||||
|
||||
## 前端设计
|
||||
|
||||
建议新增 API 文件:
|
||||
|
||||
- `ruoyi-ui/src/api/ccdiProjectBankStatement.js`
|
||||
|
||||
建议改造文件:
|
||||
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
|
||||
|
||||
页面初始化流程:
|
||||
|
||||
1. 读取 `projectId`
|
||||
2. 并行调用:
|
||||
- `list`
|
||||
- `options`
|
||||
3. 渲染左侧筛选和右侧列表
|
||||
|
||||
交互规则:
|
||||
|
||||
- 查询、页签切换、排序切换、分页切换时重置到第一页并刷新列表
|
||||
- 重置时恢复默认筛选并回到 `全部`
|
||||
- 导出时使用当前筛选条件导出全部结果
|
||||
|
||||
## 导出规则
|
||||
|
||||
- 导出范围:当前页面筛选后的全部结果
|
||||
- 不受当前分页限制
|
||||
- 导出文件名:`项目名称_流水明细_时间戳.xlsx`
|
||||
- 导出列与页面列表保持一致:
|
||||
- 交易时间
|
||||
- 本方账户
|
||||
- 本方主体
|
||||
- 对方名称
|
||||
- 对方账户
|
||||
- 摘要
|
||||
- 交易类型
|
||||
- 交易金额
|
||||
- 当前筛选结果为空时,不生成空文件,直接提示“当前条件下无可导出数据”
|
||||
|
||||
## 异常处理
|
||||
|
||||
- 多选项接口失败:列表仍可查询,筛选项置空并提示可刷新重试
|
||||
- 列表接口失败:显示错误态并保留当前筛选值
|
||||
- 详情接口失败:仅阻断详情展示,不影响主页面
|
||||
- 导出失败:保留筛选条件并提示失败原因
|
||||
- 项目下无流水数据:显示空态,导出按钮禁用
|
||||
|
||||
## 验收标准
|
||||
|
||||
### 功能验收
|
||||
|
||||
- 进入页面时并行加载列表与项目级多选项
|
||||
- 默认展示当前项目下全部流水
|
||||
- 三个页签切换结果正确
|
||||
- 所有筛选项单独与组合查询均正确
|
||||
- 空值筛选逻辑正确
|
||||
- 交易时间兼容两种时间格式
|
||||
- 金额范围按绝对值筛选正确
|
||||
- 排序仅支持交易时间与交易金额
|
||||
- 导出结果与页面筛选口径一致
|
||||
- 上传页操作列已移除,不再支持查看流水跳转
|
||||
|
||||
### 技术验收
|
||||
|
||||
- 后端遵循项目 DTO / VO 分层规范
|
||||
- Controller 使用 Swagger 注释
|
||||
- 简单查询由 MyBatis Plus + XML 动态 SQL 实现
|
||||
- 前端 API 独立封装到 `src/api`
|
||||
- 页面在桌面端和移动端均可正常展示
|
||||
|
||||
## 风险与约束
|
||||
|
||||
1. `TRX_DATE` 为字符串字段
|
||||
- 需要在 SQL 或服务层统一解析,保证筛选与排序稳定
|
||||
|
||||
2. 历史数据可能存在空字符串与空白字符串混用
|
||||
- 空值筛选必须统一兼容
|
||||
|
||||
3. 项目维度多选项可能数量较大
|
||||
- 首版先按项目维度一次性加载
|
||||
- 若实际数据量过大,再考虑远程搜索优化
|
||||
|
||||
4. 当前页面不引入旧需求中的二次分析能力
|
||||
- 后续若扩展,需要新增业务表与关联逻辑
|
||||
@@ -0,0 +1,411 @@
|
||||
# Project Detail Transaction Query Frontend Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Replace the placeholder `DetailQuery.vue` with the full project detail bank statement query page and remove the obsolete upload-record jump entry.
|
||||
|
||||
**Architecture:** Keep the page inside the existing `ccdiProject/detail.vue` dynamic component system. Use one dedicated API module for list, options, detail, and export calls; preload project-wide select options on page entry; keep the entire interaction inside `DetailQuery.vue` plus a minimal cleanup in `UploadData.vue`.
|
||||
|
||||
**Tech Stack:** Vue 2.6, Element UI 2.15, Axios request wrapper, existing global `this.download`, `npm run build:prod` + manual smoke validation
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Replace the placeholder with a typed page shell and make the build fail first
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
|
||||
- Create: `ruoyi-ui/src/api/ccdiProjectBankStatement.js`
|
||||
|
||||
**Step 1: Write the failing verification**
|
||||
|
||||
In `DetailQuery.vue`, replace the placeholder text with the final top-level data structure and import the new API module before creating it:
|
||||
|
||||
```javascript
|
||||
import {
|
||||
listBankStatement,
|
||||
getBankStatementOptions,
|
||||
getBankStatementDetail
|
||||
} from "@/api/ccdiProjectBankStatement";
|
||||
```
|
||||
|
||||
Also add empty methods `getList`, `getOptions`, and `handleExport`.
|
||||
|
||||
**Step 2: Run build to verify it fails**
|
||||
|
||||
Run: `cd ruoyi-ui && npm run build:prod`
|
||||
|
||||
Expected: FAIL because `@/api/ccdiProjectBankStatement` does not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Create `ruoyi-ui/src/api/ccdiProjectBankStatement.js` with:
|
||||
|
||||
```javascript
|
||||
import request from "@/utils/request";
|
||||
|
||||
export function listBankStatement(query) {
|
||||
return request({
|
||||
url: "/ccdi/project/bank-statement/list",
|
||||
method: "get",
|
||||
params: query
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Add stubs for:
|
||||
|
||||
- `getBankStatementOptions(projectId)`
|
||||
- `getBankStatementDetail(bankStatementId)`
|
||||
|
||||
Do not add export wrapper here; use `this.download` directly in the component.
|
||||
|
||||
**Step 4: Run build to verify it passes**
|
||||
|
||||
Run: `cd ruoyi-ui && npm run build:prod`
|
||||
|
||||
Expected: PASS or fail only on unfinished component template bindings.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue ruoyi-ui/src/api/ccdiProjectBankStatement.js
|
||||
git commit -m "搭建流水明细查询前端页面骨架"
|
||||
```
|
||||
|
||||
### Task 2: Implement page state, preload list and project-wide select options
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
|
||||
|
||||
**Step 1: Write the failing verification**
|
||||
|
||||
Add the real component state before wiring the template:
|
||||
|
||||
```javascript
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
optionsLoading: false,
|
||||
activeTab: "all",
|
||||
queryParams: {
|
||||
projectId: this.projectId,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
tabType: "all",
|
||||
transactionStartTime: "",
|
||||
transactionEndTime: "",
|
||||
counterpartyName: "",
|
||||
counterpartyNameEmpty: false,
|
||||
userMemo: "",
|
||||
userMemoEmpty: false,
|
||||
ourSubjects: [],
|
||||
ourBanks: [],
|
||||
ourAccounts: [],
|
||||
amountMin: "",
|
||||
amountMax: "",
|
||||
counterpartyAccount: "",
|
||||
counterpartyAccountEmpty: false,
|
||||
transactionType: "",
|
||||
transactionTypeEmpty: false,
|
||||
orderBy: "trxDate",
|
||||
orderDirection: "desc"
|
||||
},
|
||||
optionData: {
|
||||
ourSubjectOptions: [],
|
||||
ourBankOptions: [],
|
||||
ourAccountOptions: []
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Call both `getList()` and `getOptions()` in `created()`.
|
||||
|
||||
**Step 2: Run build to verify it fails**
|
||||
|
||||
Run: `cd ruoyi-ui && npm run build:prod`
|
||||
|
||||
Expected: FAIL because the template has not been updated to use the new reactive state yet, or methods are incomplete.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement:
|
||||
|
||||
- `getList()`
|
||||
- `getOptions()`
|
||||
- `syncProjectId()`
|
||||
- `handleQuery()`
|
||||
- `resetQuery()`
|
||||
|
||||
Ensure `created()` and the `projectId` watcher both call:
|
||||
|
||||
```javascript
|
||||
this.queryParams.projectId = this.projectId;
|
||||
this.getOptions();
|
||||
this.getList();
|
||||
```
|
||||
|
||||
**Step 4: Run build to verify it passes**
|
||||
|
||||
Run: `cd ruoyi-ui && npm run build:prod`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue
|
||||
git commit -m "实现流水明细查询页面初始加载逻辑"
|
||||
```
|
||||
|
||||
### Task 3: Build the left filter panel and local-search multi-selects
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
|
||||
|
||||
**Step 1: Write the failing verification**
|
||||
|
||||
Replace the old placeholder template with the final left column filter markup using Element UI controls:
|
||||
|
||||
- `el-date-picker` for transaction time range
|
||||
- `el-input` + `el-checkbox` for counterparty name, summary, counterparty account, transaction type
|
||||
- `el-select multiple filterable collapse-tags` for subject, bank, account
|
||||
- amount range inputs
|
||||
|
||||
Do not wire the search/reset buttons yet.
|
||||
|
||||
**Step 2: Run build to verify it fails**
|
||||
|
||||
Run: `cd ruoyi-ui && npm run build:prod`
|
||||
|
||||
Expected: FAIL because methods or bound state used in the template are incomplete.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Wire the filter controls to `queryParams` and `dateRange`:
|
||||
|
||||
```javascript
|
||||
watch: {
|
||||
dateRange(value) {
|
||||
this.queryParams.transactionStartTime = value && value[0] ? value[0] : "";
|
||||
this.queryParams.transactionEndTime = value && value[1] ? value[1] : "";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add the left action buttons:
|
||||
|
||||
- `查询`
|
||||
- `重置`
|
||||
|
||||
Add local filter support inside each `el-select` using Element UI `filterable`.
|
||||
|
||||
**Step 4: Run build to verify it passes**
|
||||
|
||||
Run: `cd ruoyi-ui && npm run build:prod`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue
|
||||
git commit -m "完成流水明细查询筛选栏布局"
|
||||
```
|
||||
|
||||
### Task 4: Build the right result area, tabs, table, pagination, and detail drawer
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
|
||||
|
||||
**Step 1: Write the failing verification**
|
||||
|
||||
Add the right-side structure:
|
||||
|
||||
- top tabs `全部 / 流入 / 流出`
|
||||
- export button
|
||||
- `el-table`
|
||||
- `pagination`
|
||||
- detail drawer or dialog
|
||||
|
||||
Reference the final columns:
|
||||
|
||||
- `trxDate`
|
||||
- `leAccountNo / leAccountName`
|
||||
- `customerAccountName / customerAccountNo`
|
||||
- `userMemo / cashType`
|
||||
- `displayAmount`
|
||||
- `详情`
|
||||
|
||||
**Step 2: Run build to verify it fails**
|
||||
|
||||
Run: `cd ruoyi-ui && npm run build:prod`
|
||||
|
||||
Expected: FAIL because event handlers like `handleTabChange`, `handleSortChange`, `handleViewDetail`, or `handlePageChange` are missing.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement:
|
||||
|
||||
- `handleTabChange(tab)`
|
||||
- `handleSortChange({ prop, order })`
|
||||
- `handlePageChange(pageNum)`
|
||||
- `handleViewDetail(row)`
|
||||
- `closeDetailDialog()`
|
||||
|
||||
Sort mapping:
|
||||
|
||||
```javascript
|
||||
const sortMap = {
|
||||
trxDate: "trxDate",
|
||||
displayAmount: "amount"
|
||||
};
|
||||
```
|
||||
|
||||
Tab mapping:
|
||||
|
||||
```javascript
|
||||
const tabMap = {
|
||||
all: "all",
|
||||
in: "in",
|
||||
out: "out"
|
||||
};
|
||||
```
|
||||
|
||||
**Step 4: Run build to verify it passes**
|
||||
|
||||
Run: `cd ruoyi-ui && npm run build:prod`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue
|
||||
git commit -m "完成流水明细查询结果区与详情展示"
|
||||
```
|
||||
|
||||
### Task 5: Wire export, empty/error states, and responsive styles
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
|
||||
|
||||
**Step 1: Write the failing verification**
|
||||
|
||||
Add the `导出流水` button click handler without implementation:
|
||||
|
||||
```javascript
|
||||
handleExport() {
|
||||
// TODO
|
||||
}
|
||||
```
|
||||
|
||||
Show the button in the UI but keep it clickable.
|
||||
|
||||
**Step 2: Run build to verify it fails functionally in manual smoke**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
Expected: BUILD PASS, but manual smoke in browser should show export button with no behavior yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement export with the existing global helper:
|
||||
|
||||
```javascript
|
||||
this.download(
|
||||
"ccdi/project/bank-statement/export",
|
||||
{ ...this.queryParams },
|
||||
`${this.projectInfo.projectName || "项目"}_流水明细_${Date.now()}.xlsx`
|
||||
);
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
- empty state when `list.length === 0`
|
||||
- inline error tip when list load fails
|
||||
- disabled export when total is `0`
|
||||
- responsive layout styles for mobile widths
|
||||
|
||||
**Step 4: Run build and manual smoke verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
Expected: BUILD PASS
|
||||
|
||||
Manual smoke:
|
||||
|
||||
1. Open project detail page
|
||||
2. Switch to `流水明细查询`
|
||||
3. Confirm left filters render
|
||||
4. Confirm export button is disabled on empty data and enabled on non-empty data
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue
|
||||
git commit -m "补齐流水明细查询导出与状态反馈"
|
||||
```
|
||||
|
||||
### Task 6: Remove obsolete upload-page operation column and finish smoke verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
|
||||
- Modify if needed: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
|
||||
|
||||
**Step 1: Write the failing verification**
|
||||
|
||||
Delete only the old upload-table operation column usage in the template, but do not clean methods yet.
|
||||
|
||||
**Step 2: Run build to verify it fails**
|
||||
|
||||
Run: `cd ruoyi-ui && npm run build:prod`
|
||||
|
||||
Expected: FAIL because `handleViewFlow` or `handleViewError` remains unused or referenced.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
In `UploadData.vue`:
|
||||
|
||||
- remove the upload-record operation column entirely
|
||||
- remove methods:
|
||||
- `handleViewFlow`
|
||||
- `handleViewError`
|
||||
- remove any event payload that tried to change menu to `detail`
|
||||
|
||||
Keep upload, polling, and record-list refresh behavior unchanged.
|
||||
|
||||
**Step 4: Run final verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
Expected: BUILD PASS
|
||||
|
||||
Manual smoke:
|
||||
|
||||
1. Open one project detail page
|
||||
2. Confirm `上传数据` 页不再出现“查看流水”入口
|
||||
3. Confirm `流水明细查询` 可独立查询、筛选、分页、导出
|
||||
4. Confirm手机宽度下左右布局能够折叠为上下布局
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue ruoyi-ui/src/api/ccdiProjectBankStatement.js
|
||||
git commit -m "完成流水明细查询前端实现并移除旧跳转入口"
|
||||
```
|
||||
26
ruoyi-ui/src/api/ccdiProjectBankStatement.js
Normal file
26
ruoyi-ui/src/api/ccdiProjectBankStatement.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
export function listBankStatement(query) {
|
||||
return request({
|
||||
url: "/ccdi/project/bank-statement/list",
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
export function getBankStatementOptions(projectId) {
|
||||
return request({
|
||||
url: "/ccdi/project/bank-statement/options",
|
||||
method: "get",
|
||||
params: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getBankStatementDetail(bankStatementId) {
|
||||
return request({
|
||||
url: `/ccdi/project/bank-statement/detail/${bankStatementId}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,438 @@
|
||||
<template>
|
||||
<div class="detail-query-container">
|
||||
<div class="placeholder-content">
|
||||
<i class="el-icon-document"></i>
|
||||
<p>流水明细查询功能开发中...</p>
|
||||
<div class="query-page-shell">
|
||||
<div class="shell-sidebar">
|
||||
<div class="shell-panel-title">筛选条件</div>
|
||||
<el-form label-position="top" class="filter-form">
|
||||
<el-form-item label="交易时间">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
class="filter-control"
|
||||
type="datetimerange"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
unlink-panels
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="本方主体">
|
||||
<el-select
|
||||
v-model="queryParams.ourSubjects"
|
||||
class="filter-control"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
clearable
|
||||
:loading="optionsLoading"
|
||||
placeholder="请选择本方主体"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in optionData.ourSubjectOptions"
|
||||
:key="`subject-${item.value}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="本方银行">
|
||||
<el-select
|
||||
v-model="queryParams.ourBanks"
|
||||
class="filter-control"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
clearable
|
||||
:loading="optionsLoading"
|
||||
placeholder="请选择本方银行"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in optionData.ourBankOptions"
|
||||
:key="`bank-${item.value}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="本方账号">
|
||||
<el-select
|
||||
v-model="queryParams.ourAccounts"
|
||||
class="filter-control"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
clearable
|
||||
:loading="optionsLoading"
|
||||
placeholder="请选择本方账号"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in optionData.ourAccountOptions"
|
||||
:key="`account-${item.value}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="金额区间">
|
||||
<div class="amount-range">
|
||||
<el-input
|
||||
v-model="queryParams.amountMin"
|
||||
placeholder="最小金额"
|
||||
clearable
|
||||
/>
|
||||
<span class="amount-separator">-</span>
|
||||
<el-input
|
||||
v-model="queryParams.amountMax"
|
||||
placeholder="最大金额"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="filter-item-header">
|
||||
<span class="filter-item-label">对方名称</span>
|
||||
<el-checkbox
|
||||
v-model="queryParams.counterpartyNameEmpty"
|
||||
class="empty-checkbox"
|
||||
>
|
||||
匹配空值
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="queryParams.counterpartyName"
|
||||
placeholder="请输入对方名称"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="filter-item-header">
|
||||
<span class="filter-item-label">对方账户</span>
|
||||
<el-checkbox
|
||||
v-model="queryParams.counterpartyAccountEmpty"
|
||||
class="empty-checkbox"
|
||||
>
|
||||
匹配空值
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="queryParams.counterpartyAccount"
|
||||
placeholder="请输入对方账户"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="filter-item-header">
|
||||
<span class="filter-item-label">摘要</span>
|
||||
<el-checkbox v-model="queryParams.userMemoEmpty" class="empty-checkbox">
|
||||
匹配空值
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="queryParams.userMemo"
|
||||
placeholder="请输入摘要关键字"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="filter-actions">
|
||||
<el-button size="small" type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button size="small" plain @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="shell-main">
|
||||
<div class="shell-header">
|
||||
<div class="shell-title-group">
|
||||
<span class="shell-title">流水明细查询</span>
|
||||
<span class="shell-subtitle">按项目范围查询交易明细并查看详情</span>
|
||||
</div>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="total === 0"
|
||||
@click="handleExport"
|
||||
>
|
||||
导出流水
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="result-tabs" @tab-click="handleTabChange">
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
<el-tab-pane label="流入" name="in" />
|
||||
<el-tab-pane label="流出" name="out" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="result-card">
|
||||
<el-alert
|
||||
v-if="listError"
|
||||
:closable="false"
|
||||
class="result-alert"
|
||||
show-icon
|
||||
title="流水明细加载失败,请稍后重试"
|
||||
type="error"
|
||||
/>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
border
|
||||
stripe
|
||||
class="result-table"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<template slot="empty">
|
||||
<el-empty
|
||||
:image-size="96"
|
||||
description="当前筛选条件下暂无流水明细"
|
||||
/>
|
||||
</template>
|
||||
<el-table-column
|
||||
label="交易时间"
|
||||
prop="trxDate"
|
||||
min-width="180"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column label="本方账户" min-width="220">
|
||||
<template slot-scope="scope">
|
||||
<div class="multi-line-cell">
|
||||
<div class="primary-text">{{ formatField(scope.row.leAccountNo) }}</div>
|
||||
<div class="secondary-text">{{ formatField(scope.row.leAccountName) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="对方账户" min-width="220">
|
||||
<template slot-scope="scope">
|
||||
<div class="multi-line-cell">
|
||||
<div class="primary-text">
|
||||
{{ formatField(scope.row.customerAccountName) }}
|
||||
</div>
|
||||
<div class="secondary-text">
|
||||
{{ formatField(scope.row.customerAccountNo) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="摘要 / 交易类型" min-width="240">
|
||||
<template slot-scope="scope">
|
||||
<div class="multi-line-cell">
|
||||
<div class="primary-text">{{ formatField(scope.row.userMemo) }}</div>
|
||||
<div class="secondary-text">{{ formatField(scope.row.cashType) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="交易金额"
|
||||
prop="displayAmount"
|
||||
min-width="140"
|
||||
align="right"
|
||||
sortable="custom"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
class="amount-text"
|
||||
:class="scope.row.displayAmount >= 0 ? 'amount-in' : 'amount-out'"
|
||||
>
|
||||
{{ formatAmount(scope.row.displayAmount) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="详情" width="100" fixed="right" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="small" @click="handleViewDetail(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
:visible.sync="detailVisible"
|
||||
append-to-body
|
||||
custom-class="detail-drawer"
|
||||
size="520px"
|
||||
title="流水详情"
|
||||
@close="closeDetailDialog"
|
||||
>
|
||||
<div v-loading="detailLoading" class="detail-drawer-body">
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">基础信息</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">流水ID</span>
|
||||
<span class="detail-value">{{ formatField(detailData.bankStatementId) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">交易时间</span>
|
||||
<span class="detail-value">{{ formatField(detailData.trxDate) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">币种</span>
|
||||
<span class="detail-value">{{ formatField(detailData.currency) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">交易类型</span>
|
||||
<span class="detail-value">{{ formatField(detailData.cashType) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">显示金额</span>
|
||||
<span class="detail-value">{{ formatAmount(detailData.displayAmount) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">余额</span>
|
||||
<span class="detail-value">{{ formatAmount(detailData.amountBalance) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">账户信息</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item detail-item-full">
|
||||
<span class="detail-label">本方账户</span>
|
||||
<span class="detail-value">
|
||||
{{ formatField(detailData.leAccountName) }} / {{ formatField(detailData.leAccountNo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item detail-item-full">
|
||||
<span class="detail-label">对方账户</span>
|
||||
<span class="detail-value">
|
||||
{{ formatField(detailData.customerAccountName) }} /
|
||||
{{ formatField(detailData.customerAccountNo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">本方银行</span>
|
||||
<span class="detail-value">{{ formatField(detailData.bank) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">对方银行</span>
|
||||
<span class="detail-value">{{ formatField(detailData.customerBank) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-section-title">补充信息</div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item detail-item-full">
|
||||
<span class="detail-label">摘要</span>
|
||||
<span class="detail-value">{{ formatField(detailData.userMemo) }}</span>
|
||||
</div>
|
||||
<div class="detail-item detail-item-full">
|
||||
<span class="detail-label">银行摘要</span>
|
||||
<span class="detail-value">{{ formatField(detailData.bankComments) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">银行交易号</span>
|
||||
<span class="detail-value">{{ formatField(detailData.bankTrxNumber) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">交易方式</span>
|
||||
<span class="detail-value">{{ formatField(detailData.paymentMethod) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">异常类型</span>
|
||||
<span class="detail-value">{{ formatField(detailData.exceptionType) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">创建时间</span>
|
||||
<span class="detail-value">{{ formatDate(detailData.createDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { parseTime } from "@/utils/ruoyi";
|
||||
import {
|
||||
listBankStatement,
|
||||
getBankStatementOptions,
|
||||
getBankStatementDetail,
|
||||
} from "@/api/ccdiProjectBankStatement";
|
||||
|
||||
const TAB_MAP = {
|
||||
all: "all",
|
||||
in: "in",
|
||||
out: "out",
|
||||
};
|
||||
|
||||
const SORT_MAP = {
|
||||
trxDate: "trxDate",
|
||||
displayAmount: "amount",
|
||||
};
|
||||
|
||||
const createDefaultQueryParams = (projectId) => ({
|
||||
projectId,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
tabType: "all",
|
||||
transactionStartTime: "",
|
||||
transactionEndTime: "",
|
||||
counterpartyName: "",
|
||||
counterpartyNameEmpty: false,
|
||||
userMemo: "",
|
||||
userMemoEmpty: false,
|
||||
ourSubjects: [],
|
||||
ourBanks: [],
|
||||
ourAccounts: [],
|
||||
amountMin: "",
|
||||
amountMax: "",
|
||||
counterpartyAccount: "",
|
||||
counterpartyAccountEmpty: false,
|
||||
orderBy: "trxDate",
|
||||
orderDirection: "desc",
|
||||
});
|
||||
|
||||
const createEmptyOptionData = () => ({
|
||||
ourSubjectOptions: [],
|
||||
ourBankOptions: [],
|
||||
ourAccountOptions: [],
|
||||
});
|
||||
|
||||
const createEmptyDetailData = () => ({
|
||||
bankStatementId: "",
|
||||
trxDate: "",
|
||||
currency: "",
|
||||
leAccountNo: "",
|
||||
leAccountName: "",
|
||||
customerAccountName: "",
|
||||
customerAccountNo: "",
|
||||
customerBank: "",
|
||||
userMemo: "",
|
||||
bankComments: "",
|
||||
bankTrxNumber: "",
|
||||
bank: "",
|
||||
cashType: "",
|
||||
amountBalance: "",
|
||||
displayAmount: "",
|
||||
paymentMethod: "",
|
||||
exceptionType: "",
|
||||
createDate: "",
|
||||
});
|
||||
|
||||
export default {
|
||||
name: "DetailQuery",
|
||||
props: {
|
||||
@@ -24,28 +449,421 @@ export default {
|
||||
}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
optionsLoading: false,
|
||||
detailLoading: false,
|
||||
detailVisible: false,
|
||||
activeTab: "all",
|
||||
dateRange: [],
|
||||
list: [],
|
||||
total: 0,
|
||||
listError: "",
|
||||
detailData: createEmptyDetailData(),
|
||||
queryParams: createDefaultQueryParams(this.projectId),
|
||||
optionData: createEmptyOptionData(),
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
this.getOptions();
|
||||
},
|
||||
watch: {
|
||||
dateRange(value) {
|
||||
this.queryParams.transactionStartTime = value && value[0] ? value[0] : "";
|
||||
this.queryParams.transactionEndTime = value && value[1] ? value[1] : "";
|
||||
},
|
||||
projectId() {
|
||||
this.syncProjectId();
|
||||
this.getOptions();
|
||||
this.getList();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getList() {
|
||||
this.syncProjectId();
|
||||
if (!this.queryParams.projectId) {
|
||||
this.list = [];
|
||||
this.total = 0;
|
||||
this.listError = "";
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await listBankStatement(this.queryParams);
|
||||
this.list = res.rows || [];
|
||||
this.total = res.total || 0;
|
||||
this.listError = "";
|
||||
} catch (error) {
|
||||
this.list = [];
|
||||
this.total = 0;
|
||||
this.listError = "加载流水明细失败";
|
||||
console.error("加载流水明细失败", error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async getOptions() {
|
||||
this.syncProjectId();
|
||||
if (!this.queryParams.projectId) {
|
||||
this.optionData = createEmptyOptionData();
|
||||
return;
|
||||
}
|
||||
|
||||
this.optionsLoading = true;
|
||||
try {
|
||||
const res = await getBankStatementOptions(this.queryParams.projectId);
|
||||
const data = res.data || {};
|
||||
this.optionData = {
|
||||
ourSubjectOptions: data.ourSubjectOptions || [],
|
||||
ourBankOptions: data.ourBankOptions || [],
|
||||
ourAccountOptions: data.ourAccountOptions || [],
|
||||
};
|
||||
} catch (error) {
|
||||
this.optionData = createEmptyOptionData();
|
||||
console.error("加载流水筛选项失败", error);
|
||||
} finally {
|
||||
this.optionsLoading = false;
|
||||
}
|
||||
},
|
||||
syncProjectId() {
|
||||
this.queryParams.projectId = this.projectId;
|
||||
this.queryParams.tabType = TAB_MAP[this.activeTab] || "all";
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.syncProjectId();
|
||||
this.getList();
|
||||
},
|
||||
resetQuery() {
|
||||
this.activeTab = "all";
|
||||
this.dateRange = [];
|
||||
this.queryParams = createDefaultQueryParams(this.projectId);
|
||||
this.syncProjectId();
|
||||
this.getOptions();
|
||||
this.getList();
|
||||
},
|
||||
handleTabChange(tab) {
|
||||
const tabName = tab && tab.name ? tab.name : this.activeTab;
|
||||
this.activeTab = tabName;
|
||||
this.queryParams.pageNum = 1;
|
||||
this.queryParams.tabType = TAB_MAP[tabName] || "all";
|
||||
this.getList();
|
||||
},
|
||||
handleSortChange({ prop, order }) {
|
||||
this.queryParams.orderBy = SORT_MAP[prop] || "trxDate";
|
||||
this.queryParams.orderDirection = order === "ascending" ? "asc" : "desc";
|
||||
if (!order) {
|
||||
this.queryParams.orderBy = "trxDate";
|
||||
this.queryParams.orderDirection = "desc";
|
||||
}
|
||||
this.getList();
|
||||
},
|
||||
handlePageChange(pageInfo) {
|
||||
if (typeof pageInfo === "number") {
|
||||
this.queryParams.pageNum = pageInfo;
|
||||
} else {
|
||||
this.queryParams.pageNum = pageInfo.page;
|
||||
this.queryParams.pageSize = pageInfo.limit;
|
||||
}
|
||||
this.getList();
|
||||
},
|
||||
async handleViewDetail(row) {
|
||||
if (!row || !row.bankStatementId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.detailVisible = true;
|
||||
this.detailLoading = true;
|
||||
try {
|
||||
const res = await getBankStatementDetail(row.bankStatementId);
|
||||
this.detailData = {
|
||||
...createEmptyDetailData(),
|
||||
...(res.data || {}),
|
||||
};
|
||||
} catch (error) {
|
||||
this.detailData = createEmptyDetailData();
|
||||
this.$message.error("加载流水详情失败");
|
||||
console.error("加载流水详情失败", error);
|
||||
} finally {
|
||||
this.detailLoading = false;
|
||||
}
|
||||
},
|
||||
closeDetailDialog() {
|
||||
this.detailVisible = false;
|
||||
this.detailData = createEmptyDetailData();
|
||||
},
|
||||
handleExport() {
|
||||
if (this.total === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.download(
|
||||
"ccdi/project/bank-statement/export",
|
||||
{ ...this.queryParams },
|
||||
`${this.projectInfo.projectName || "项目"}_流水明细_${Date.now()}.xlsx`
|
||||
);
|
||||
},
|
||||
formatField(value) {
|
||||
return value || "-";
|
||||
},
|
||||
formatDate(value) {
|
||||
return value ? parseTime(value, "{y}-{m}-{d} {h}:{i}:{s}") : "-";
|
||||
},
|
||||
formatAmount(value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "-";
|
||||
}
|
||||
return Number(value).toLocaleString("zh-CN", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-query-container {
|
||||
padding: 40px 20px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
min-height: 400px;
|
||||
min-height: 480px;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
.query-page-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 300px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
.shell-sidebar,
|
||||
.shell-main {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.shell-sidebar {
|
||||
padding: 16px 14px;
|
||||
}
|
||||
|
||||
.shell-main {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.shell-panel-title,
|
||||
.shell-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.shell-title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.shell-subtitle {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.shell-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
margin-top: 12px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
:deep(.el-form-item__label) {
|
||||
padding-bottom: 6px;
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.filter-item-label {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.empty-checkbox {
|
||||
margin-top: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.empty-checkbox .el-checkbox__label) {
|
||||
padding-left: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.amount-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.amount-separator {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.el-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.result-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-alert {
|
||||
margin: 16px 16px 0;
|
||||
}
|
||||
|
||||
.result-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.multi-line-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.primary-text {
|
||||
color: #303133;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.amount-in {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.amount-out {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.detail-drawer-body {
|
||||
padding: 0 4px 24px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
margin-bottom: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px 16px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.detail-item-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #303133;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.query-page-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail-query-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.shell-sidebar,
|
||||
.shell-main {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.shell-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.result-alert {
|
||||
margin: 12px 12px 0;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,7 +27,12 @@
|
||||
<!-- 上传模块 -->
|
||||
<div class="upload-section">
|
||||
<div class="upload-cards">
|
||||
<div v-for="card in uploadCards" :key="card.key" class="upload-card">
|
||||
<div
|
||||
v-for="card in uploadCards"
|
||||
:key="card.key"
|
||||
class="upload-card"
|
||||
:class="{ 'is-disabled': card.disabled }"
|
||||
>
|
||||
<div class="card-icon">
|
||||
<i :class="card.icon"></i>
|
||||
</div>
|
||||
@@ -38,6 +43,7 @@
|
||||
:type="card.uploaded ? 'primary' : ''"
|
||||
:icon="card.uploaded ? 'el-icon-view' : 'el-icon-upload2'"
|
||||
:plain="!card.uploaded"
|
||||
:disabled="card.disabled"
|
||||
@click="handleUploadClick(card.key)"
|
||||
>
|
||||
{{ card.btnText }}
|
||||
@@ -49,21 +55,6 @@
|
||||
<!-- 文件上传记录列表 -->
|
||||
<div class="file-list-section">
|
||||
<div class="list-toolbar">
|
||||
<div class="filter-group">
|
||||
<el-select
|
||||
v-model="queryParams.fileStatus"
|
||||
placeholder="文件状态"
|
||||
clearable
|
||||
@change="loadFileList"
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="上传中" value="uploading"></el-option>
|
||||
<el-option label="解析中" value="parsing"></el-option>
|
||||
<el-option label="解析成功" value="parsed_success"></el-option>
|
||||
<el-option label="解析失败" value="parsed_failed"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<el-button icon="el-icon-refresh" @click="handleManualRefresh">刷新</el-button>
|
||||
</div>
|
||||
|
||||
@@ -92,29 +83,6 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUser" label="上传人" width="100"></el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-if="scope.row.fileStatus === 'parsed_success'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleViewFlow(scope.row)"
|
||||
>
|
||||
查看流水
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.fileStatus === 'parsed_failed'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleViewError(scope.row)"
|
||||
>
|
||||
查看错误
|
||||
</el-button>
|
||||
<span v-if="scope.row.fileStatus === 'uploading' || scope.row.fileStatus === 'parsing'">
|
||||
-
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
@@ -365,6 +333,7 @@ export default {
|
||||
icon: "el-icon-document",
|
||||
btnText: "上传流水",
|
||||
uploaded: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
key: "credit",
|
||||
@@ -373,6 +342,7 @@ export default {
|
||||
icon: "el-icon-s-data",
|
||||
btnText: "上传征信",
|
||||
uploaded: false,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: "namelist",
|
||||
@@ -381,6 +351,7 @@ export default {
|
||||
icon: "el-icon-s-order",
|
||||
btnText: "选择名单",
|
||||
uploaded: false,
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
// 质量指标
|
||||
@@ -422,9 +393,8 @@ export default {
|
||||
listLoading: false,
|
||||
queryParams: {
|
||||
projectId: null,
|
||||
fileStatus: null,
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
pageSize: 10,
|
||||
},
|
||||
total: 0,
|
||||
|
||||
@@ -544,7 +514,7 @@ export default {
|
||||
/** 上传卡片点击 */
|
||||
handleUploadClick(key) {
|
||||
const card = this.uploadCards.find((c) => c.key === key);
|
||||
if (!card) return;
|
||||
if (!card || card.disabled) return;
|
||||
|
||||
if (key === "transaction") {
|
||||
// 流水导入 - 打开批量上传弹窗
|
||||
@@ -881,7 +851,6 @@ export default {
|
||||
try {
|
||||
const params = {
|
||||
projectId: this.projectId,
|
||||
fileStatus: this.queryParams.fileStatus,
|
||||
pageNum: this.queryParams.pageNum,
|
||||
pageSize: this.queryParams.pageSize,
|
||||
};
|
||||
@@ -955,23 +924,6 @@ export default {
|
||||
this.loadFileList();
|
||||
},
|
||||
|
||||
/** 查看流水 */
|
||||
handleViewFlow(record) {
|
||||
this.$emit("menu-change", {
|
||||
key: "detail",
|
||||
route: "detail",
|
||||
params: { logId: record.logId },
|
||||
});
|
||||
},
|
||||
|
||||
/** 查看错误 */
|
||||
handleViewError(record) {
|
||||
this.$alert(record.errorMessage || "未知错误", "错误信息", {
|
||||
confirmButtonText: "确定",
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
|
||||
/** 状态文本映射 */
|
||||
getStatusText(status) {
|
||||
const map = {
|
||||
@@ -1160,6 +1112,23 @@ export default {
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
background-color: #fafafa;
|
||||
border-color: #ebeef5;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
border-color: #ebeef5;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card-icon,
|
||||
.card-title,
|
||||
.card-desc {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 32px;
|
||||
color: #1890ff;
|
||||
@@ -1291,14 +1260,9 @@ export default {
|
||||
|
||||
.list-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
78
ruoyi-ui/tests/unit/detail-query-filter-layout.test.js
Normal file
78
ruoyi-ui/tests/unit/detail-query-filter-layout.test.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const componentPath = path.resolve(
|
||||
__dirname,
|
||||
"../../src/views/ccdiProject/components/detail/DetailQuery.vue"
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, "utf8");
|
||||
|
||||
[
|
||||
["对方名称", "queryParams.counterpartyNameEmpty"],
|
||||
["摘要", "queryParams.userMemoEmpty"],
|
||||
["对方账户", "queryParams.counterpartyAccountEmpty"],
|
||||
].forEach(([label, model]) => {
|
||||
const inlineTogglePattern = new RegExp(
|
||||
`<el-form-item>[\\s\\S]*?<div class="filter-item-header">[\\s\\S]*?<span class="filter-item-label">${label}</span>[\\s\\S]*?<el-checkbox[\\s\\S]*?v-model="${model}"[\\s\\S]*?class="empty-checkbox"[\\s\\S]*?>[\\s\\S]*?匹配空值[\\s\\S]*?</el-checkbox>[\\s\\S]*?</div>`,
|
||||
"m"
|
||||
);
|
||||
|
||||
assert(
|
||||
inlineTogglePattern.test(source),
|
||||
`${label}筛选项应将匹配空值放到标题同一行`
|
||||
);
|
||||
});
|
||||
|
||||
const filterOrder = [
|
||||
'label="交易时间"',
|
||||
'label="本方主体"',
|
||||
'label="本方银行"',
|
||||
'label="本方账号"',
|
||||
'label="金额区间"',
|
||||
'<span class="filter-item-label">对方名称</span>',
|
||||
'<span class="filter-item-label">对方账户</span>',
|
||||
'<span class="filter-item-label">摘要</span>',
|
||||
];
|
||||
|
||||
let lastIndex = -1;
|
||||
filterOrder.forEach((token) => {
|
||||
const currentIndex = source.indexOf(token);
|
||||
assert.notStrictEqual(currentIndex, -1, `未找到筛选项标识: ${token}`);
|
||||
assert(
|
||||
currentIndex > lastIndex,
|
||||
`筛选项顺序不正确,${token} 应出现在前一项之后`
|
||||
);
|
||||
lastIndex = currentIndex;
|
||||
});
|
||||
|
||||
assert(
|
||||
!source.includes('placeholder="请输入交易类型"'),
|
||||
"筛选栏不应再显示交易类型输入框"
|
||||
);
|
||||
|
||||
assert(
|
||||
!source.includes("queryParams.transactionType"),
|
||||
"筛选逻辑不应再保留交易类型参数"
|
||||
);
|
||||
|
||||
assert(
|
||||
!source.includes("queryParams.transactionTypeEmpty"),
|
||||
"筛选逻辑不应再保留交易类型空值匹配参数"
|
||||
);
|
||||
|
||||
assert(
|
||||
/\.filter-item-header\s*\{[\s\S]*display:\s*flex;[\s\S]*justify-content:\s*space-between;[\s\S]*margin-bottom:\s*6px;/m.test(
|
||||
source
|
||||
),
|
||||
"筛选项标题行应使用紧凑的横向布局"
|
||||
);
|
||||
|
||||
assert(
|
||||
/\.filter-form\s*\{[\s\S]*margin-top:\s*12px;[\s\S]*:deep\(.el-form-item\)\s*\{[\s\S]*margin-bottom:\s*14px;/m.test(
|
||||
source
|
||||
),
|
||||
"筛选表单应压缩顶部和表单项间距"
|
||||
);
|
||||
|
||||
console.log("detail-query-filter-layout test passed");
|
||||
35
ruoyi-ui/tests/unit/upload-data-disabled-cards.test.js
Normal file
35
ruoyi-ui/tests/unit/upload-data-disabled-cards.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const componentPath = path.resolve(
|
||||
__dirname,
|
||||
"../../src/views/ccdiProject/components/detail/UploadData.vue"
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, "utf8");
|
||||
|
||||
assert(
|
||||
/<el-button[\s\S]*?:disabled="card\.disabled"[\s\S]*?@click="handleUploadClick\(card\.key\)"/.test(
|
||||
source
|
||||
),
|
||||
"上传卡片按钮应绑定禁用状态"
|
||||
);
|
||||
|
||||
assert(
|
||||
/key:\s*"credit"[\s\S]*?disabled:\s*true/.test(source),
|
||||
"征信导入卡片应配置为禁用"
|
||||
);
|
||||
|
||||
assert(
|
||||
/key:\s*"namelist"[\s\S]*?disabled:\s*true/.test(source),
|
||||
"名单库选择卡片应配置为禁用"
|
||||
);
|
||||
|
||||
assert(
|
||||
/handleUploadClick\(key\)\s*\{[\s\S]*?if\s*\(!card\s*\|\|\s*card\.disabled\)\s*return;/.test(
|
||||
source
|
||||
),
|
||||
"禁用卡片点击后不应继续执行上传逻辑"
|
||||
);
|
||||
|
||||
console.log("upload-data-disabled-cards test passed");
|
||||
26
ruoyi-ui/tests/unit/upload-data-file-list-settings.test.js
Normal file
26
ruoyi-ui/tests/unit/upload-data-file-list-settings.test.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const componentPath = path.resolve(
|
||||
__dirname,
|
||||
"../../src/views/ccdiProject/components/detail/UploadData.vue"
|
||||
);
|
||||
const source = fs.readFileSync(componentPath, "utf8");
|
||||
|
||||
const fileListSectionMatch = source.match(
|
||||
/<div class="file-list-section">([\s\S]*?)<el-table/
|
||||
);
|
||||
assert(fileListSectionMatch, "未找到上传文件列表区域");
|
||||
|
||||
assert(
|
||||
!/<el-select[\s\S]*?queryParams\.fileStatus/.test(fileListSectionMatch[1]),
|
||||
"上传文件列表工具栏不应再显示上传状态筛选框"
|
||||
);
|
||||
|
||||
assert(
|
||||
/queryParams:\s*\{[\s\S]*?pageSize:\s*10\b/.test(source),
|
||||
"上传文件列表分页默认每页应为 10 条"
|
||||
);
|
||||
|
||||
console.log("upload-data-file-list-settings test passed");
|
||||
Reference in New Issue
Block a user