新增流水明细对手方汇总查询

This commit is contained in:
wjj
2026-07-20 14:29:23 +08:00
parent 7fec8e21be
commit 175a50bf96
13 changed files with 521 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ 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.CcdiBankStatementCounterpartyVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
@@ -55,6 +56,19 @@ public class CcdiBankStatementController extends BaseController {
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 分页查询对手方聚合列表
*/
@GetMapping("/counterparty/list")
@Operation(summary = "分页查询对手方聚合列表")
public TableDataInfo counterpartyList(CcdiBankStatementQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiBankStatementCounterpartyVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiBankStatementCounterpartyVO> result = bankStatementService.selectCounterpartyPage(page, queryDTO);
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 查询项目级筛选项
*/

View File

@@ -0,0 +1,44 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 流水对手方聚合VO
*
* @author ruoyi
*/
@Data
public class CcdiBankStatementCounterpartyVO {
/** 对方名称 */
private String customerAccountName;
/** 对方名称是否为空 */
private Boolean nameEmpty;
/** 关联账号数 */
private Integer accountCount;
/** 流入合计 */
private BigDecimal inAmount;
/** 流入笔数 */
private Long inCount;
/** 流出合计 */
private BigDecimal outAmount;
/** 流出笔数 */
private Long outCount;
/** 总交易金额 */
private BigDecimal totalAmount;
/** 总笔数 */
private Long totalCount;
/** 最近交易时间 */
private String latestTrxDate;
}

View File

@@ -4,6 +4,7 @@ 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.CcdiBankStatementCounterpartyVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
@@ -38,6 +39,9 @@ public interface CcdiBankStatementMapper extends BaseMapper<CcdiBankStatement> {
Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
@Param("query") CcdiBankStatementQueryDTO query);
Page<CcdiBankStatementCounterpartyVO> selectCounterpartyPage(Page<CcdiBankStatementCounterpartyVO> page,
@Param("query") CcdiBankStatementQueryDTO query);
List<CcdiBankStatementListVO> selectStatementListForExport(@Param("query") CcdiBankStatementQueryDTO query);
CcdiBankStatementDetailVO selectStatementDetailById(@Param("bankStatementId") Long bankStatementId);

View File

@@ -3,6 +3,7 @@ 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.CcdiBankStatementCounterpartyVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
@@ -34,6 +35,16 @@ public interface ICcdiBankStatementService {
Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
CcdiBankStatementQueryDTO queryDTO);
/**
* 分页查询对手方聚合列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiBankStatementCounterpartyVO> selectCounterpartyPage(Page<CcdiBankStatementCounterpartyVO> page,
CcdiBankStatementQueryDTO queryDTO);
/**
* 查询导出列表
*

View File

@@ -3,6 +3,7 @@ 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.CcdiBankStatementCounterpartyVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO;
@@ -57,6 +58,16 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
return result;
}
@Override
public Page<CcdiBankStatementCounterpartyVO> selectCounterpartyPage(
Page<CcdiBankStatementCounterpartyVO> page,
CcdiBankStatementQueryDTO queryDTO
) {
CcdiBankStatementQueryDTO normalizedQuery = queryDTO == null ? new CcdiBankStatementQueryDTO() : queryDTO;
normalizeQuery(normalizedQuery);
return bankStatementMapper.selectCounterpartyPage(page, normalizedQuery);
}
@Override
public List<CcdiBankStatementExcel> selectStatementListForExport(CcdiBankStatementQueryDTO queryDTO) {
CcdiBankStatementQueryDTO normalizedQuery = queryDTO == null ? new CcdiBankStatementQueryDTO() : queryDTO;
@@ -153,6 +164,7 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
queryDTO.setCounterpartyAccount(normalizeText(queryDTO.getCounterpartyAccount()));
queryDTO.setTransactionType(normalizeText(queryDTO.getTransactionType()));
queryDTO.setOurSubjects(normalizeList(queryDTO.getOurSubjects()));
queryDTO.setOurCertNos(normalizeList(queryDTO.getOurCertNos()));
queryDTO.setOurBanks(normalizeList(queryDTO.getOurBanks()));
queryDTO.setOurAccounts(normalizeList(queryDTO.getOurAccounts()));
queryDTO.setCounterpartyNameEmpty(Boolean.TRUE.equals(queryDTO.getCounterpartyNameEmpty()));
@@ -180,6 +192,21 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
if ("trxDate".equalsIgnoreCase(normalized)) {
return "trxDate";
}
if ("latestTrxDate".equalsIgnoreCase(normalized)) {
return "latestTrxDate";
}
if ("inAmount".equalsIgnoreCase(normalized)) {
return "inAmount";
}
if ("outAmount".equalsIgnoreCase(normalized)) {
return "outAmount";
}
if ("totalAmount".equalsIgnoreCase(normalized)) {
return "totalAmount";
}
if ("totalCount".equalsIgnoreCase(normalized)) {
return "totalCount";
}
return "trxDate";
}

View File

@@ -72,6 +72,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="displayAmount" column="displayAmount"/>
</resultMap>
<resultMap id="CcdiBankStatementCounterpartyVOResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementCounterpartyVO">
<result property="customerAccountName" column="customerAccountName"/>
<result property="nameEmpty" column="nameEmpty"/>
<result property="accountCount" column="accountCount"/>
<result property="inAmount" column="inAmount"/>
<result property="inCount" column="inCount"/>
<result property="outAmount" column="outAmount"/>
<result property="outCount" column="outCount"/>
<result property="totalAmount" column="totalAmount"/>
<result property="totalCount" column="totalCount"/>
<result property="latestTrxDate" column="latestTrxDate"/>
</resultMap>
<resultMap id="CcdiBankStatementDetailVOResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO">
<id property="bankStatementId" column="bankStatementId"/>
<result property="projectId" column="projectId"/>
@@ -284,6 +298,45 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</choose>
</sql>
<sql id="counterpartyNameExpr">
TRIM(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''))
</sql>
<sql id="counterpartyOrderBy">
<choose>
<when test="query.orderBy == 'inAmount' and query.orderDirection == 'asc'">
ORDER BY inAmount ASC, totalCount DESC, customerAccountName ASC
</when>
<when test="query.orderBy == 'inAmount' and query.orderDirection == 'desc'">
ORDER BY inAmount DESC, totalCount DESC, customerAccountName ASC
</when>
<when test="query.orderBy == 'outAmount' and query.orderDirection == 'asc'">
ORDER BY outAmount ASC, totalCount DESC, customerAccountName ASC
</when>
<when test="query.orderBy == 'outAmount' and query.orderDirection == 'desc'">
ORDER BY outAmount DESC, totalCount DESC, customerAccountName ASC
</when>
<when test="query.orderBy == 'totalCount' and query.orderDirection == 'asc'">
ORDER BY totalCount ASC, totalAmount DESC, customerAccountName ASC
</when>
<when test="query.orderBy == 'totalCount' and query.orderDirection == 'desc'">
ORDER BY totalCount DESC, totalAmount DESC, customerAccountName ASC
</when>
<when test="query.orderBy == 'latestTrxDate' and query.orderDirection == 'asc'">
ORDER BY latestTrxDate ASC, totalAmount DESC, customerAccountName ASC
</when>
<when test="query.orderBy == 'latestTrxDate' and query.orderDirection == 'desc'">
ORDER BY latestTrxDate DESC, totalAmount DESC, customerAccountName ASC
</when>
<when test="query.orderBy == 'totalAmount' and query.orderDirection == 'asc'">
ORDER BY totalAmount ASC, totalCount DESC, customerAccountName ASC
</when>
<otherwise>
ORDER BY totalAmount DESC, totalCount DESC, customerAccountName ASC
</otherwise>
</choose>
</sql>
<select id="selectStatementPage" resultMap="CcdiBankStatementListVOResultMap">
SELECT
<include refid="statementListColumns"/>
@@ -294,6 +347,38 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="statementOrderBy"/>
</select>
<select id="selectCounterpartyPage" resultMap="CcdiBankStatementCounterpartyVOResultMap">
SELECT
grouped.customerAccountName,
CASE WHEN grouped.customerAccountName = '' THEN 1 ELSE 0 END AS nameEmpty,
grouped.accountCount,
grouped.inAmount,
grouped.inCount,
grouped.outAmount,
grouped.outCount,
grouped.totalAmount,
grouped.totalCount,
grouped.latestTrxDate
FROM (
SELECT
<include refid="counterpartyNameExpr"/> AS customerAccountName,
COUNT(DISTINCT NULLIF(TRIM(IFNULL(bs.CUSTOMER_ACCOUNT_NO, '')), '')) AS accountCount,
SUM(IFNULL(bs.AMOUNT_CR, 0)) AS inAmount,
SUM(CASE WHEN IFNULL(bs.AMOUNT_CR, 0) > 0 THEN 1 ELSE 0 END) AS inCount,
SUM(IFNULL(bs.AMOUNT_DR, 0)) AS outAmount,
SUM(CASE WHEN IFNULL(bs.AMOUNT_DR, 0) > 0 THEN 1 ELSE 0 END) AS outCount,
SUM(IFNULL(bs.AMOUNT_CR, 0) + IFNULL(bs.AMOUNT_DR, 0)) AS totalAmount,
COUNT(1) AS totalCount,
DATE_FORMAT(MAX(<include refid="parsedTrxDateExpr"/>), '%Y-%m-%d %H:%i:%s') AS latestTrxDate
FROM ccdi_bank_statement bs
<where>
<include refid="statementFilterWhere"/>
</where>
GROUP BY <include refid="counterpartyNameExpr"/>
) grouped
<include refid="counterpartyOrderBy"/>
</select>
<select id="selectStatementListForExport" resultMap="CcdiBankStatementListVOResultMap">
SELECT
<include refid="statementListColumns"/>

View File

@@ -3,6 +3,7 @@ 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.CcdiBankStatementCounterpartyVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
@@ -80,6 +81,31 @@ class CcdiBankStatementControllerTest {
assertEquals(1, result.getRows().size());
}
@Test
void counterpartyList_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);
CcdiBankStatementCounterpartyVO row = new CcdiBankStatementCounterpartyVO();
row.setCustomerAccountName("测试对手方");
row.setTotalCount(2L);
Page<CcdiBankStatementCounterpartyVO> page = new Page<>(1, 10);
page.setRecords(List.of(row));
page.setTotal(1);
when(bankStatementService.selectCounterpartyPage(any(), same(queryDTO))).thenReturn(page);
TableDataInfo result = controller.counterpartyList(queryDTO);
assertEquals(200, result.getCode());
assertEquals(1L, result.getTotal());
assertEquals(1, result.getRows().size());
verify(projectAccessService).assertCanRead(100L);
}
@Test
void detail_shouldReturnAjaxResultSuccess() {
CcdiBankStatementDetailVO detailVO = new CcdiBankStatementDetailVO();

View File

@@ -116,6 +116,27 @@ class CcdiBankStatementMapperXmlTest {
assertTrue(sql.contains("END ) <= ?"), sql);
}
@Test
void selectCounterpartyPage_shouldGroupByTrimmedCounterpartyNameAndReuseFilters() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectCounterpartyPage");
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
queryDTO.setProjectId(33L);
queryDTO.setTabType("out");
queryDTO.setCounterpartyName("杭州");
queryDTO.setOrderBy("totalAmount");
queryDTO.setOrderDirection("desc");
String sql = renderSql(mappedStatement, queryDTO);
assertTrue(sql.contains("TRIM(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')) AS customerAccountName"), sql);
assertTrue(sql.contains("GROUP BY TRIM(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''))"), sql);
assertTrue(sql.contains("(bs.project_id = ?)"), sql);
assertTrue(sql.contains("IFNULL(bs.AMOUNT_DR, 0) > 0"), sql);
assertTrue(sql.contains("bs.CUSTOMER_ACCOUNT_NAME LIKE CONCAT('%', TRIM(?), '%')"), sql);
assertTrue(sql.contains("ORDER BY totalAmount DESC, totalCount DESC, customerAccountName ASC"), sql);
}
@Test
void selectStatementDetailById_shouldJoinUploadRecordForOriginalFileMetadata() throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {