修复流水异常标签展示与导出

This commit is contained in:
wkc
2026-03-19 10:20:58 +08:00
parent e058cec78e
commit 144897237b
17 changed files with 530 additions and 5 deletions

View File

@@ -41,6 +41,10 @@ public class CcdiBankStatementExcel {
@Excel(name = "交易类型")
private String cashType;
/** 异常标签 */
@Excel(name = "异常标签")
private String hitTags;
/** 交易金额 */
@Excel(name = "交易金额")
private BigDecimal displayAmount;

View File

@@ -3,7 +3,9 @@ package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 流水明细详情VO
@@ -96,4 +98,7 @@ public class CcdiBankStatementDetailVO {
/** 原始文件上传时间 */
private Date uploadTime;
/** 命中异常标签 */
private List<CcdiBankStatementHitTagVO> hitTags = new ArrayList<>();
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 流水命中异常标签VO
*/
@Data
public class CcdiBankStatementHitTagVO {
/** 规则编码 */
private String ruleCode;
/** 规则名称 */
private String ruleName;
/** 风险等级 */
private String riskLevel;
/** 命中原因 */
private String reasonDetail;
/** 流水ID */
private Long bankStatementId;
}

View File

@@ -3,6 +3,8 @@ package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 流水明细列表VO
@@ -38,4 +40,7 @@ public class CcdiBankStatementListVO {
/** 页面展示金额 */
private BigDecimal displayAmount;
/** 命中异常标签 */
private List<CcdiBankStatementHitTagVO> hitTags = new ArrayList<>();
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagResult;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -20,6 +21,18 @@ public interface CcdiBankTagResultMapper extends BaseMapper<CcdiBankTagResult> {
*/
int deleteByProjectAndModel(@Param("projectId") Long projectId, @Param("modelCode") String modelCode);
/**
* 按项目和流水ID批量查询命中的异常标签
*
* @param projectId 项目ID
* @param bankStatementIds 流水ID列表
* @return 命中的异常标签列表
*/
List<CcdiBankStatementHitTagVO> selectStatementTagsByProjectAndStatementIds(
@Param("projectId") Long projectId,
@Param("bankStatementIds") List<Long> bankStatementIds
);
/**
* 批量插入结果
*

View File

@@ -5,16 +5,21 @@ 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.CcdiBankStatementHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.service.ICcdiBankStatementService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -31,6 +36,9 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
@Resource
private CcdiBankStatementMapper bankStatementMapper;
@Resource
private CcdiBankTagResultMapper bankTagResultMapper;
@Override
public CcdiBankStatementFilterOptionsVO getFilterOptions(Long projectId) {
CcdiBankStatementFilterOptionsVO options = bankStatementMapper.selectFilterOptions(projectId);
@@ -42,7 +50,9 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
CcdiBankStatementQueryDTO queryDTO) {
CcdiBankStatementQueryDTO normalizedQuery = queryDTO == null ? new CcdiBankStatementQueryDTO() : queryDTO;
normalizeQuery(normalizedQuery);
return bankStatementMapper.selectStatementPage(page, normalizedQuery);
Page<CcdiBankStatementListVO> result = bankStatementMapper.selectStatementPage(page, normalizedQuery);
attachHitTags(result == null ? Collections.emptyList() : result.getRecords(), normalizedQuery.getProjectId());
return result;
}
@Override
@@ -53,12 +63,58 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
if (rows == null || rows.isEmpty()) {
return Collections.emptyList();
}
attachHitTags(rows, normalizedQuery.getProjectId());
return rows.stream().map(this::toExcel).collect(Collectors.toList());
}
@Override
public CcdiBankStatementDetailVO getStatementDetail(Long bankStatementId) {
return bankStatementMapper.selectStatementDetailById(bankStatementId);
CcdiBankStatementDetailVO detail = bankStatementMapper.selectStatementDetailById(bankStatementId);
if (detail == null || detail.getProjectId() == null || detail.getBankStatementId() == null) {
return detail;
}
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = loadHitTagMap(
detail.getProjectId(),
List.of(detail.getBankStatementId())
);
detail.setHitTags(new ArrayList<>(hitTagMap.getOrDefault(detail.getBankStatementId(), Collections.emptyList())));
return detail;
}
private void attachHitTags(List<CcdiBankStatementListVO> rows, Long projectId) {
if (rows == null || rows.isEmpty() || projectId == null) {
return;
}
List<Long> bankStatementIds = rows.stream()
.map(CcdiBankStatementListVO::getBankStatementId)
.filter(item -> item != null)
.distinct()
.collect(Collectors.toList());
if (bankStatementIds.isEmpty()) {
return;
}
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = loadHitTagMap(projectId, bankStatementIds);
rows.forEach(row -> row.setHitTags(new ArrayList<>(
hitTagMap.getOrDefault(row.getBankStatementId(), Collections.emptyList())
)));
}
private Map<Long, List<CcdiBankStatementHitTagVO>> loadHitTagMap(Long projectId, List<Long> bankStatementIds) {
if (projectId == null || bankStatementIds == null || bankStatementIds.isEmpty()) {
return Collections.emptyMap();
}
List<CcdiBankStatementHitTagVO> hitTags =
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(projectId, bankStatementIds);
if (hitTags == null || hitTags.isEmpty()) {
return Collections.emptyMap();
}
return hitTags.stream()
.filter(item -> item.getBankStatementId() != null)
.collect(Collectors.groupingBy(
CcdiBankStatementHitTagVO::getBankStatementId,
LinkedHashMap::new,
Collectors.toList()
));
}
private void normalizeQuery(CcdiBankStatementQueryDTO queryDTO) {
@@ -138,7 +194,19 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
excel.setCustomerAccountNo(row.getCustomerAccountNo());
excel.setUserMemo(row.getUserMemo());
excel.setCashType(row.getCashType());
excel.setHitTags(formatHitTags(row.getHitTags()));
excel.setDisplayAmount(row.getDisplayAmount());
return excel;
}
private String formatHitTags(List<CcdiBankStatementHitTagVO> hitTags) {
if (hitTags == null || hitTags.isEmpty()) {
return "";
}
return hitTags.stream()
.map(CcdiBankStatementHitTagVO::getRuleName)
.filter(item -> item != null && !item.isBlank())
.distinct()
.collect(Collectors.joining(""));
}
}

View File

@@ -29,6 +29,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="remark" column="remark"/>
</resultMap>
<resultMap id="CcdiBankStatementHitTagVOResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO">
<result property="ruleCode" column="rule_code"/>
<result property="ruleName" column="rule_name"/>
<result property="riskLevel" column="risk_level"/>
<result property="reasonDetail" column="reason_detail"/>
<result property="bankStatementId" column="bank_statement_id"/>
</resultMap>
<delete id="deleteByProjectAndModel">
delete from ccdi_bank_statement_tag_result
where project_id = #{projectId}
@@ -37,6 +45,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if>
</delete>
<select id="selectStatementTagsByProjectAndStatementIds" resultMap="CcdiBankStatementHitTagVOResultMap">
select
rule_code,
rule_name,
risk_level,
reason_detail,
bank_statement_id
from ccdi_bank_statement_tag_result
where project_id = #{projectId}
and bank_statement_id is not null
and bank_statement_id IN
<foreach collection="bankStatementIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
order by bank_statement_id asc, id asc
</select>
<insert id="insertBatch" parameterType="java.util.List">
insert into ccdi_bank_statement_tag_result (
project_id, model_code, model_name, rule_code, rule_name, indicator_code,

View File

@@ -0,0 +1,31 @@
package com.ruoyi.ccdi.project.domain.vo;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class CcdiBankStatementHitTagsContractTest {
@Test
void listVo_shouldExposeHitTagsField() throws Exception {
Field field = CcdiBankStatementListVO.class.getDeclaredField("hitTags");
assertNotNull(field, "流水列表VO应返回命中异常标签");
}
@Test
void detailVo_shouldExposeHitTagsField() throws Exception {
Field field = CcdiBankStatementDetailVO.class.getDeclaredField("hitTags");
assertNotNull(field, "流水详情VO应返回命中异常标签");
}
@Test
void excelVo_shouldExposeHitTagsField() throws Exception {
Field field = com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel.class.getDeclaredField("hitTags");
assertNotNull(field, "流水导出对象应返回异常标签文本列");
}
}

View File

@@ -2,9 +2,12 @@ package com.ruoyi.ccdi.project.mapper;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiBankTagResultMapperXmlTest {
@@ -20,4 +23,25 @@ class CcdiBankTagResultMapperXmlTest {
assertTrue(xml.contains("model_code = #{modelCode}"));
}
}
@Test
void mapper_shouldExposeStatementTagQueryForBankStatementDetails() {
Method method = Arrays.stream(CcdiBankTagResultMapper.class.getDeclaredMethods())
.filter(item -> "selectStatementTagsByProjectAndStatementIds".equals(item.getName()))
.findFirst()
.orElse(null);
assertNotNull(method, "应提供按项目和流水ID批量查询异常标签的方法");
}
@Test
void xml_shouldDefineStatementTagQueryForBankStatementDetails() throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("selectStatementTagsByProjectAndStatementIds"), xml);
assertTrue(xml.contains("bank_statement_id IN"), xml);
assertTrue(xml.contains("project_id = #{projectId}"), xml);
}
}
}

View File

@@ -5,9 +5,11 @@ 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.CcdiBankStatementHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementOptionVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -36,6 +38,9 @@ class CcdiBankStatementServiceImplTest {
@Mock
private CcdiBankStatementMapper bankStatementMapper;
@Mock
private CcdiBankTagResultMapper bankTagResultMapper;
@Test
void getFilterOptions_shouldReturnProjectWideOptions() {
CcdiBankStatementFilterOptionsVO options = new CcdiBankStatementFilterOptionsVO();
@@ -54,6 +59,7 @@ class CcdiBankStatementServiceImplTest {
queryDTO.setProjectId(100L);
queryDTO.setOrderBy("amount");
queryDTO.setOrderDirection("desc");
page.setRecords(List.of(new CcdiBankStatementListVO()));
doReturn(page).when(bankStatementMapper).selectStatementPage(eq(page), same(queryDTO));
service.selectStatementPage(page, queryDTO);
@@ -111,14 +117,66 @@ class CcdiBankStatementServiceImplTest {
assertEquals("6222", result.get(0).getLeAccountNo());
}
@Test
void selectStatementListForExport_shouldIncludeHitTagNames() {
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
queryDTO.setProjectId(43L);
CcdiBankStatementListVO row = new CcdiBankStatementListVO();
row.setBankStatementId(51274L);
row.setLeAccountNo("6222");
when(bankStatementMapper.selectStatementListForExport(same(queryDTO))).thenReturn(List.of(row));
CcdiBankStatementHitTagVO hitTag = new CcdiBankStatementHitTagVO();
hitTag.setBankStatementId(51274L);
hitTag.setRuleName("大额存现交易");
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(43L, List.of(51274L)))
.thenReturn(List.of(hitTag));
List<CcdiBankStatementExcel> result = service.selectStatementListForExport(queryDTO);
assertEquals(1, result.size());
assertEquals("大额存现交易", result.get(0).getHitTags());
}
@Test
void selectStatementPage_shouldAttachHitTags() {
Page<CcdiBankStatementListVO> page = new Page<>(1, 10);
CcdiBankStatementQueryDTO queryDTO = new CcdiBankStatementQueryDTO();
queryDTO.setProjectId(43L);
CcdiBankStatementListVO row = new CcdiBankStatementListVO();
row.setBankStatementId(51274L);
page.setRecords(List.of(row));
doReturn(page).when(bankStatementMapper).selectStatementPage(eq(page), same(queryDTO));
CcdiBankStatementHitTagVO hitTag = new CcdiBankStatementHitTagVO();
hitTag.setBankStatementId(51274L);
hitTag.setRuleCode("LARGE_CASH_DEPOSIT");
hitTag.setRuleName("大额存现交易");
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(43L, List.of(51274L)))
.thenReturn(List.of(hitTag));
Page<CcdiBankStatementListVO> result = service.selectStatementPage(page, queryDTO);
assertEquals(1, result.getRecords().get(0).getHitTags().size());
assertEquals("LARGE_CASH_DEPOSIT", result.getRecords().get(0).getHitTags().get(0).getRuleCode());
}
@Test
void getStatementDetail_shouldDelegateToMapper() {
CcdiBankStatementDetailVO detailVO = new CcdiBankStatementDetailVO();
detailVO.setBankStatementId(200L);
detailVO.setProjectId(43L);
when(bankStatementMapper.selectStatementDetailById(200L)).thenReturn(detailVO);
CcdiBankStatementHitTagVO hitTag = new CcdiBankStatementHitTagVO();
hitTag.setBankStatementId(200L);
hitTag.setRuleName("大额存现交易");
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(43L, List.of(200L)))
.thenReturn(List.of(hitTag));
CcdiBankStatementDetailVO result = service.getStatementDetail(200L);
assertSame(detailVO, result);
assertEquals(1, result.getHitTags().size());
assertEquals("大额存现交易", result.getHitTags().get(0).getRuleName());
}
}