新增流水明细对手方汇总查询
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询项目级筛选项
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 查询导出列表
|
||||
*
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# 流水明细对手方聚合查询后端实施计划
|
||||
|
||||
## 背景
|
||||
|
||||
明细查询需要增加类似 lsfx 对手方页签的聚合视图,按流水 `CUSTOMER_ACCOUNT_NAME` 归集同一项目、同一筛选条件下的交易数据,供前端展示对手方汇总。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- `ccdi-project` 后端模块。
|
||||
- 新增对手方聚合分页接口,不新增业务表,不修改已有流水明细查询入参结构。
|
||||
- 复用 `CcdiBankStatementQueryDTO` 的项目、交易方向、交易时间、本方主体、本方银行、本方账号、金额区间、对方名称、对方账户、摘要和空值匹配筛选。
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 新增 `CcdiBankStatementCounterpartyVO`,返回对手方名称、空值标识、关联账号数、流入/流出金额与笔数、总交易额、总笔数、最近交易时间。
|
||||
2. 在 `CcdiBankStatementController` 增加 `GET /ccdi/project/bank-statement/counterparty/list`,沿用项目读权限校验和 RuoYi 分页返回结构。
|
||||
3. 在 `ICcdiBankStatementService` 与 `CcdiBankStatementServiceImpl` 增加聚合查询服务方法,统一复用明细查询参数归一化和排序白名单。
|
||||
4. 在 `CcdiBankStatementMapper` 与 XML 中新增聚合 SQL:
|
||||
- 对 `CUSTOMER_ACCOUNT_NAME` 做 `TRIM(IFNULL(..., ''))` 后分组。
|
||||
- 用 `CASE WHEN AMOUNT >= 0` 统计流入,用 `< 0` 统计流出。
|
||||
- 用 `ABS(AMOUNT)` 计算流出合计和总交易额,便于前端展示正数合计。
|
||||
- 最近交易时间复用既有交易时间解析表达式。
|
||||
5. 补充 Controller 与 Mapper XML 单元测试,覆盖接口分页返回、SQL 聚合字段、筛选条件和排序。
|
||||
|
||||
## 验证计划
|
||||
|
||||
- 运行后端定向单测:
|
||||
`mvn -pl ccdi-project -am "-Dtest=CcdiBankStatementMapperXmlTest,CcdiBankStatementControllerTest" "-Dsurefire.failIfNoSpecifiedTests=false" test`
|
||||
- 打包并启动真实后端后,用真实页面请求新接口,确认 `counterparty/list` 返回 200。
|
||||
@@ -0,0 +1,32 @@
|
||||
# 流水明细对手方聚合查询前端实施计划
|
||||
|
||||
## 背景
|
||||
|
||||
用户希望在项目详情的“流水明细查询”中增加“对手方”视图,效果参考 lsfx 对手方列表:在同一筛选条件下按对手方汇总,查看流入、流出和总交易规模。
|
||||
|
||||
## 实施范围
|
||||
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
|
||||
- `ruoyi-ui/src/api/ccdiProjectBankStatement.js`
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 在流水明细结果区标题旁增加 `el-radio-group`,提供“流水明细 / 对手方”切换。
|
||||
2. 新增 `listBankStatementCounterparties` API 方法,请求后端 `/ccdi/project/bank-statement/counterparty/list`。
|
||||
3. 对手方模式下复用左侧筛选区和分页组件,隐藏“导出流水”按钮。
|
||||
4. 对手方表格展示对手方名称、流入合计、流出合计、总交易额和最近交易时间。
|
||||
5. 对手方模式默认按总交易额倒序;表格列排序映射到后端白名单字段。
|
||||
|
||||
## 验证计划
|
||||
|
||||
- 前端构建:
|
||||
`npm run build:prod`
|
||||
- 真实页面验证:
|
||||
1. 打开项目详情 `http://127.0.0.1:8081/ccdiProject/detail/42?tab=detail`。
|
||||
2. 进入“流水明细查询”,切换到“对手方”。
|
||||
3. 确认列表展示对手方名称、流入/流出/总交易额与分页总数。
|
||||
4. 确认对手方表格不展示“操作”列和“查看流水”按钮。
|
||||
|
||||
## 本机 Node 说明
|
||||
|
||||
`.nvmrc` 指向 `14.21.3`,但本机该版本环境不可稳定提供 npm。按仓库约定,本次使用已验证可用的 Node `22.22.3` 与 npm `10.9.8` 完成构建和页面验证。
|
||||
@@ -0,0 +1,32 @@
|
||||
# 流水明细对手方聚合查询实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 后端新增 `GET /ccdi/project/bank-statement/counterparty/list`,按对方名称聚合流水,返回流入合计、流出合计、总交易额、笔数和最近交易时间。
|
||||
- 后端新增 `CcdiBankStatementCounterpartyVO`,并在 Mapper XML 中复用明细查询筛选条件和排序白名单。
|
||||
- 前端在项目详情“流水明细查询”中增加“流水明细 / 对手方”切换。
|
||||
- 前端新增对手方聚合表格,支持按聚合指标排序;按用户反馈移除“查看流水”入口,避免从汇总视图跳到逐笔流水后缺少明确返回路径。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 涉及 `ccdi-project` 的银行流水查询 Controller、Service、Mapper 与 XML。
|
||||
- 涉及 `ruoyi-ui` 的流水明细查询组件与银行流水 API 文件。
|
||||
- 不涉及数据库表结构、菜单、权限标识或初始化数据变更。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 后端单测通过:
|
||||
`mvn -pl ccdi-project -am "-Dtest=CcdiBankStatementMapperXmlTest,CcdiBankStatementControllerTest" "-Dsurefire.failIfNoSpecifiedTests=false" test`
|
||||
- 前端构建通过:
|
||||
`npm run build:prod`
|
||||
- 真实页面验证通过:
|
||||
- 打开真实项目详情页 `http://127.0.0.1:8081/ccdiProject/detail/42?tab=detail`。
|
||||
- 切换“对手方”后,接口 `/ccdi/project/bank-statement/counterparty/list` 返回 200,页面展示“美团、微信支付、银行转账”等聚合行和 `共 8 条`。
|
||||
- 对手方表格已移除“操作”列和“查看流水”按钮,只保留汇总信息展示与表格排序。
|
||||
- 浏览器验证说明:`browser-use` 对应的 Chrome DevTools MCP 被本机 profile 锁占用,本次按 Playwright 技能要求使用 `@playwright/cli` 在真实页面完成验证。
|
||||
|
||||
## 环境说明
|
||||
|
||||
- `.nvmrc` 为 `14.21.3`,但本机 nvm 的 Node 14 环境无法稳定提供 npm。
|
||||
- 本次前端构建与页面验证使用 Node `22.22.3`、npm `10.9.8`;部分 PowerShell 会话中 nvm 切换后 PATH 不刷新,因此构建使用已确认存在的 `C:\Users\20696\AppData\Local\nvm-nodejs\npm.cmd`。
|
||||
- 前端构建存在项目既有体积告警,包含 `login-background`、`chunk-elementUI`、`chunk-libs`、`0uS5znL.docx` 等资产超限;未发现由本次代码变更引入的构建失败。
|
||||
@@ -8,6 +8,14 @@ export function listBankStatement(query) {
|
||||
});
|
||||
}
|
||||
|
||||
export function listBankStatementCounterparties(query) {
|
||||
return request({
|
||||
url: "/ccdi/project/bank-statement/counterparty/list",
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
export function getBankStatementOptions(projectId) {
|
||||
return request({
|
||||
url: "/ccdi/project/bank-statement/options",
|
||||
|
||||
@@ -152,8 +152,18 @@
|
||||
<div class="shell-header">
|
||||
<div class="shell-title-group">
|
||||
<span class="shell-title">流水明细查询</span>
|
||||
<el-radio-group
|
||||
v-model="resultMode"
|
||||
class="result-mode-switch"
|
||||
size="small"
|
||||
@change="handleResultModeChange"
|
||||
>
|
||||
<el-radio-button label="statement">流水明细</el-radio-button>
|
||||
<el-radio-button label="counterparty">对手方</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="resultMode === 'statement'"
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
@@ -164,7 +174,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="prefillTip"
|
||||
v-if="resultMode === 'statement' && prefillTip"
|
||||
:closable="false"
|
||||
class="prefill-alert"
|
||||
show-icon
|
||||
@@ -180,15 +190,24 @@
|
||||
|
||||
<div class="result-card">
|
||||
<el-alert
|
||||
v-if="listError"
|
||||
v-if="resultMode === 'statement' && listError"
|
||||
:closable="false"
|
||||
class="result-alert"
|
||||
show-icon
|
||||
title="流水明细加载失败,请稍后重试"
|
||||
type="error"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="resultMode === 'counterparty' && counterpartyError"
|
||||
:closable="false"
|
||||
class="result-alert"
|
||||
show-icon
|
||||
title="对手方加载失败,请稍后重试"
|
||||
type="error"
|
||||
/>
|
||||
|
||||
<el-table
|
||||
v-if="resultMode === 'statement'"
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
class="result-table"
|
||||
@@ -275,9 +294,82 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
v-loading="loading"
|
||||
:data="counterpartyList"
|
||||
class="result-table counterparty-table"
|
||||
@sort-change="handleCounterpartySortChange"
|
||||
>
|
||||
<template slot="empty">
|
||||
<el-empty
|
||||
:image-size="96"
|
||||
description="当前筛选条件下暂无对手方"
|
||||
/>
|
||||
</template>
|
||||
<el-table-column
|
||||
label="对手方名称"
|
||||
prop="customerAccountName"
|
||||
min-width="220"
|
||||
:formatter="formatCounterpartyNameColumn"
|
||||
/>
|
||||
<el-table-column
|
||||
label="流入合计"
|
||||
prop="inAmount"
|
||||
width="150"
|
||||
align="right"
|
||||
sortable="custom"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<div class="amount-stack">
|
||||
<span class="amount-text amount-in">{{ formatAmount(scope.row.inAmount) }}</span>
|
||||
<span class="secondary-text">{{ formatCount(scope.row.inCount) }} 笔</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="流出合计"
|
||||
prop="outAmount"
|
||||
width="150"
|
||||
align="right"
|
||||
sortable="custom"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<div class="amount-stack">
|
||||
<span class="amount-text amount-out">{{ formatAmount(scope.row.outAmount) }}</span>
|
||||
<span class="secondary-text">{{ formatCount(scope.row.outCount) }} 笔</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="总交易额"
|
||||
prop="totalAmount"
|
||||
width="150"
|
||||
align="right"
|
||||
sortable="custom"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<div class="amount-stack">
|
||||
<span class="amount-text">{{ formatAmount(scope.row.totalAmount) }}</span>
|
||||
<span class="secondary-text">{{ formatCount(scope.row.totalCount) }} 笔</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="最近交易时间"
|
||||
prop="latestTrxDate"
|
||||
min-width="170"
|
||||
sortable="custom"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
{{ formatField(scope.row.latestTrxDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-show="currentTotal > 0"
|
||||
:total="currentTotal"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="handlePageChange"
|
||||
@@ -411,6 +503,7 @@
|
||||
import { parseTime } from "@/utils/ruoyi";
|
||||
import {
|
||||
listBankStatement,
|
||||
listBankStatementCounterparties,
|
||||
getBankStatementOptions,
|
||||
getBankStatementDetail,
|
||||
} from "@/api/ccdiProjectBankStatement";
|
||||
@@ -427,6 +520,14 @@ const SORT_MAP = {
|
||||
displayAmount: "amount",
|
||||
};
|
||||
|
||||
const COUNTERPARTY_SORT_MAP = {
|
||||
inAmount: "inAmount",
|
||||
outAmount: "outAmount",
|
||||
totalAmount: "totalAmount",
|
||||
totalCount: "totalCount",
|
||||
latestTrxDate: "latestTrxDate",
|
||||
};
|
||||
|
||||
const createDefaultQueryParams = (projectId) => ({
|
||||
projectId,
|
||||
pageNum: 1,
|
||||
@@ -517,10 +618,14 @@ export default {
|
||||
optionsLoading: false,
|
||||
detailLoading: false,
|
||||
detailVisible: false,
|
||||
resultMode: "statement",
|
||||
activeTab: "all",
|
||||
dateRange: [],
|
||||
list: [],
|
||||
total: 0,
|
||||
counterpartyList: [],
|
||||
counterpartyTotal: 0,
|
||||
counterpartyError: "",
|
||||
listError: "",
|
||||
prefillTip: "",
|
||||
detailData: createEmptyDetailData(),
|
||||
@@ -532,6 +637,11 @@ export default {
|
||||
this.getList();
|
||||
this.getOptions();
|
||||
},
|
||||
computed: {
|
||||
currentTotal() {
|
||||
return this.resultMode === "counterparty" ? this.counterpartyTotal : this.total;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dateRange(value) {
|
||||
this.queryParams.transactionStartTime = value && value[0] ? value[0] : "";
|
||||
@@ -557,12 +667,19 @@ export default {
|
||||
if (!this.queryParams.projectId) {
|
||||
this.list = [];
|
||||
this.total = 0;
|
||||
this.counterpartyList = [];
|
||||
this.counterpartyTotal = 0;
|
||||
this.listError = "";
|
||||
this.counterpartyError = "";
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
if (this.resultMode === "counterparty") {
|
||||
await this.getCounterpartyList();
|
||||
return;
|
||||
}
|
||||
const res = await listBankStatement(this.queryParams);
|
||||
this.list = (res.rows || []).map((item) => ({
|
||||
...item,
|
||||
@@ -579,6 +696,19 @@ export default {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async getCounterpartyList() {
|
||||
try {
|
||||
const res = await listBankStatementCounterparties(this.queryParams);
|
||||
this.counterpartyList = res.rows || [];
|
||||
this.counterpartyTotal = res.total || 0;
|
||||
this.counterpartyError = "";
|
||||
} catch (error) {
|
||||
this.counterpartyList = [];
|
||||
this.counterpartyTotal = 0;
|
||||
this.counterpartyError = "加载对手方失败";
|
||||
console.error("加载对手方失败", error);
|
||||
}
|
||||
},
|
||||
async getOptions() {
|
||||
this.syncProjectId();
|
||||
if (!this.queryParams.projectId) {
|
||||
@@ -624,6 +754,7 @@ export default {
|
||||
if (!value || !Array.isArray(value.ourCertNos) || !value.ourCertNos.length) {
|
||||
return;
|
||||
}
|
||||
this.resultMode = "statement";
|
||||
this.activeTab = "all";
|
||||
this.dateRange = [];
|
||||
this.queryParams = {
|
||||
@@ -634,6 +765,17 @@ export default {
|
||||
this.syncProjectId();
|
||||
this.getList();
|
||||
},
|
||||
handleResultModeChange(mode) {
|
||||
this.queryParams.pageNum = 1;
|
||||
if (mode === "counterparty") {
|
||||
this.queryParams.orderBy = "totalAmount";
|
||||
this.queryParams.orderDirection = "desc";
|
||||
} else {
|
||||
this.queryParams.orderBy = "trxDate";
|
||||
this.queryParams.orderDirection = "desc";
|
||||
}
|
||||
this.getList();
|
||||
},
|
||||
handleTabChange(tab) {
|
||||
const tabName = tab && tab.name ? tab.name : this.activeTab;
|
||||
this.activeTab = tabName;
|
||||
@@ -650,6 +792,15 @@ export default {
|
||||
}
|
||||
this.getList();
|
||||
},
|
||||
handleCounterpartySortChange({ prop, order }) {
|
||||
this.queryParams.orderBy = COUNTERPARTY_SORT_MAP[prop] || "totalAmount";
|
||||
this.queryParams.orderDirection = order === "ascending" ? "asc" : "desc";
|
||||
if (!order) {
|
||||
this.queryParams.orderBy = "totalAmount";
|
||||
this.queryParams.orderDirection = "desc";
|
||||
}
|
||||
this.getList();
|
||||
},
|
||||
handlePageChange(pageInfo) {
|
||||
if (typeof pageInfo === "number") {
|
||||
this.queryParams.pageNum = pageInfo;
|
||||
@@ -743,6 +894,13 @@ export default {
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
},
|
||||
formatCount(value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return 0;
|
||||
}
|
||||
const count = Number(value);
|
||||
return Number.isNaN(count) ? 0 : count;
|
||||
},
|
||||
formatSignedAmount(value) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "-";
|
||||
@@ -766,6 +924,15 @@ export default {
|
||||
}
|
||||
return this.formatField(detail.customerAccountName);
|
||||
},
|
||||
formatCounterpartyRowName(row) {
|
||||
if (!row || row.nameEmpty || !row.customerAccountName) {
|
||||
return "为空";
|
||||
}
|
||||
return this.formatField(row.customerAccountName);
|
||||
},
|
||||
formatCounterpartyNameColumn(row) {
|
||||
return this.formatCounterpartyRowName(row);
|
||||
},
|
||||
formatOriginalFileName(detail) {
|
||||
if (!detail) {
|
||||
return "暂无原始文件";
|
||||
@@ -876,6 +1043,14 @@ export default {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.result-mode-switch {
|
||||
margin-top: 8px;
|
||||
|
||||
:deep(.el-radio-button__inner) {
|
||||
min-width: 82px;
|
||||
}
|
||||
}
|
||||
|
||||
.shell-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -1011,6 +1186,15 @@ export default {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.amount-stack {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.amount-in {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user