优化无外部人员涉疑交易查询

This commit is contained in:
wjj
2026-07-16 15:26:14 +08:00
parent 4a7cfde4af
commit 6d90bb4e58
8 changed files with 232 additions and 20 deletions

View File

@@ -22,4 +22,7 @@ public class CcdiProjectSuspiciousTransactionQueryDTO {
/** 每页数量 */ /** 每页数量 */
private Integer pageSize; private Integer pageSize;
/** 是否包含外部人员预警分支 */
private Boolean includeExternalPerson;
} }

View File

@@ -163,6 +163,14 @@ public interface CcdiProjectOverviewMapper {
*/ */
CcdiProjectExternalRiskSummaryVO selectExternalRiskSummaryByProjectId(@Param("projectId") Long projectId); CcdiProjectExternalRiskSummaryVO selectExternalRiskSummaryByProjectId(@Param("projectId") Long projectId);
/**
* 判断项目是否存在外部人员主体
*
* @param projectId 项目ID
* @return 存在时返回1否则返回空
*/
Integer selectExternalPersonSubjectExistsByProjectId(@Param("projectId") Long projectId);
/** /**
* 查询外部人员预警模型卡片 * 查询外部人员预警模型卡片
* *

View File

@@ -86,6 +86,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
private static final String ACTION_LABEL = "查看详情"; private static final String ACTION_LABEL = "查看详情";
private static final String SUSPICIOUS_TYPE_ALL = "ALL";
private static final String SUSPICIOUS_TYPE_MODEL_RULE = "MODEL_RULE";
private static final String SUSPICIOUS_TYPE_EXTERNAL_PERSON = "EXTERNAL_PERSON";
private static final String EXCLUSION_TYPE_STATEMENT = "STATEMENT"; private static final String EXCLUSION_TYPE_STATEMENT = "STATEMENT";
private static final String EXCLUSION_TYPE_OBJECT = "OBJECT"; private static final String EXCLUSION_TYPE_OBJECT = "OBJECT";
@@ -369,6 +375,11 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
) { ) {
ensureProjectExists(queryDTO.getProjectId()); ensureProjectExists(queryDTO.getProjectId());
normalizeSuspiciousTransactionQuery(queryDTO); normalizeSuspiciousTransactionQuery(queryDTO);
prepareSuspiciousTransactionExternalBranch(queryDTO);
if (isExternalPersonOnlyWithoutSubject(queryDTO)) {
return emptySuspiciousTransactionPage();
}
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>( Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(
defaultPageNum(queryDTO.getPageNum()), defaultPageNum(queryDTO.getPageNum()),
@@ -389,6 +400,11 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
) { ) {
ensureProjectExists(queryDTO.getProjectId()); ensureProjectExists(queryDTO.getProjectId());
normalizeSuspiciousTransactionQuery(queryDTO); normalizeSuspiciousTransactionQuery(queryDTO);
prepareSuspiciousTransactionExternalBranch(queryDTO);
if (isExternalPersonOnlyWithoutSubject(queryDTO)) {
return List.of();
}
return defaultList(overviewMapper.selectReportSuspiciousTransactionList(queryDTO)).stream() return defaultList(overviewMapper.selectReportSuspiciousTransactionList(queryDTO)).stream()
.map(this::buildSuspiciousTransactionExcelRow) .map(this::buildSuspiciousTransactionExcelRow)
@@ -719,12 +735,42 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
queryDTO.setModelCode(null); queryDTO.setModelCode(null);
} }
if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) { if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) {
queryDTO.setSuspiciousType("ALL"); queryDTO.setSuspiciousType(SUSPICIOUS_TYPE_ALL);
return; return;
} }
queryDTO.setSuspiciousType(queryDTO.getSuspiciousType().trim().toUpperCase()); queryDTO.setSuspiciousType(queryDTO.getSuspiciousType().trim().toUpperCase());
} }
private void prepareSuspiciousTransactionExternalBranch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
if (!shouldCheckExternalPersonBranch(queryDTO.getSuspiciousType())) {
queryDTO.setIncludeExternalPerson(false);
return;
}
queryDTO.setIncludeExternalPerson(hasExternalPersonSubject(queryDTO.getProjectId()));
}
private boolean shouldCheckExternalPersonBranch(String suspiciousType) {
return SUSPICIOUS_TYPE_ALL.equals(suspiciousType)
|| SUSPICIOUS_TYPE_MODEL_RULE.equals(suspiciousType)
|| SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(suspiciousType);
}
private boolean hasExternalPersonSubject(Long projectId) {
return overviewMapper.selectExternalPersonSubjectExistsByProjectId(projectId) != null;
}
private boolean isExternalPersonOnlyWithoutSubject(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
return SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType())
&& !Boolean.TRUE.equals(queryDTO.getIncludeExternalPerson());
}
private CcdiProjectSuspiciousTransactionPageVO emptySuspiciousTransactionPage() {
CcdiProjectSuspiciousTransactionPageVO result = new CcdiProjectSuspiciousTransactionPageVO();
result.setRows(List.of());
result.setTotal(0L);
return result;
}
private CcdiProjectOverviewStatVO buildStat(String key, String label, Integer value) { private CcdiProjectOverviewStatVO buildStat(String key, String label, Integer value) {
CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO(); CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
stat.setKey(key); stat.setKey(key);

View File

@@ -811,6 +811,22 @@
and risk.cert_no = subject.cert_no and risk.cert_no = subject.cert_no
</select> </select>
<select id="selectExternalPersonSubjectExistsByProjectId" resultType="java.lang.Integer">
select 1
from ccdi_bank_statement bs
left join ccdi_base_staff staff
on staff.id_card = bs.cret_no
left join ccdi_staff_fmy_relation relation
on relation.status = 1
and relation.relation_cert_no = bs.cret_no
where bs.project_id = #{projectId}
and bs.cret_no is not null
and trim(bs.cret_no) != ''
and staff.id_card is null
and relation.relation_cert_no is null
limit 1
</select>
<select id="selectExternalRiskModelCardsByProjectId" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO"> <select id="selectExternalRiskModelCardsByProjectId" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO">
<bind name="externalProjectId" value="projectId"/> <bind name="externalProjectId" value="projectId"/>
select select
@@ -1161,6 +1177,7 @@
<include refid="suspiciousTransactionNameHitSql"/> <include refid="suspiciousTransactionNameHitSql"/>
) name_hits on name_hits.bankStatementId = base.bankStatementId ) name_hits on name_hits.bankStatementId = base.bankStatementId
<if test="query.includeExternalPerson == true">
union all union all
select select
@@ -1181,6 +1198,7 @@
from ( from (
<include refid="externalSuspiciousTransactionSql"/> <include refid="externalSuspiciousTransactionSql"/>
) external_hits ) external_hits
</if>
</sql> </sql>
<sql id="suspiciousTransactionAggregatedSql"> <sql id="suspiciousTransactionAggregatedSql">

View File

@@ -91,7 +91,9 @@ class CcdiProjectOverviewMapperSqlTest {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml")); String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String suspiciousSql = extractSelect(xml, "selectSuspiciousTransactionPage"); String suspiciousSql = extractSelect(xml, "selectSuspiciousTransactionPage");
String modelHitSql = extractSqlFragment(xml, "suspiciousTransactionModelHitSql"); String modelHitSql = extractSqlFragment(xml, "suspiciousTransactionModelHitSql");
String mergedSql = extractSqlFragment(xml, "suspiciousTransactionMergedSql");
String aggregatedSql = extractSqlFragment(xml, "suspiciousTransactionAggregatedSql"); String aggregatedSql = extractSqlFragment(xml, "suspiciousTransactionAggregatedSql");
String externalSubjectExistsSql = extractSelect(xml, "selectExternalPersonSubjectExistsByProjectId");
assertTrue(modelHitSql.contains("from ccdi_bank_statement_tag_result tr"), modelHitSql); assertTrue(modelHitSql.contains("from ccdi_bank_statement_tag_result tr"), modelHitSql);
assertTrue(modelHitSql.contains("tr.bank_statement_id is not null"), modelHitSql); assertTrue(modelHitSql.contains("tr.bank_statement_id is not null"), modelHitSql);
@@ -104,6 +106,11 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql); assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasNameListHit"), suspiciousSql); assertTrue(suspiciousSql.contains("hasNameListHit"), suspiciousSql);
assertTrue(suspiciousSql.contains("final_result.nameListHitType"), suspiciousSql); assertTrue(suspiciousSql.contains("final_result.nameListHitType"), suspiciousSql);
assertTrue(mergedSql.contains("<if test=\"query.includeExternalPerson == true\">"), mergedSql);
assertTrue(mergedSql.contains("<include refid=\"externalSuspiciousTransactionSql\"/>"), mergedSql);
assertTrue(externalSubjectExistsSql.contains("limit 1"), externalSubjectExistsSql);
assertTrue(externalSubjectExistsSql.contains("staff.id_card is null"), externalSubjectExistsSql);
assertTrue(externalSubjectExistsSql.contains("relation.relation_cert_no is null"), externalSubjectExistsSql);
String reportSuspiciousSql = extractSelect(xml, "selectReportSuspiciousTransactionList"); String reportSuspiciousSql = extractSelect(xml, "selectReportSuspiciousTransactionList");
assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '中介'"), reportSuspiciousSql); assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '中介'"), reportSuspiciousSql);

View File

@@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -93,6 +94,70 @@ class CcdiProjectOverviewServiceSuspiciousTransactionTest {
); );
} }
@Test
void shouldIncludeExternalBranchWhenExternalSubjectExistsForAllSuspiciousTransactions() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
when(overviewMapper.selectExternalPersonSubjectExistsByProjectId(40L)).thenReturn(1);
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(1, 10);
page.setRecords(List.of());
page.setTotal(0);
when(overviewMapper.selectSuspiciousTransactionPage(any(Page.class), any(CcdiProjectSuspiciousTransactionQueryDTO.class)))
.thenReturn(page);
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
CcdiProjectSuspiciousTransactionPageVO result = service.getSuspiciousTransactions(queryDTO);
assertEquals(0L, result.getTotal());
verify(overviewMapper).selectSuspiciousTransactionPage(
any(Page.class),
argThat(query -> "ALL".equals(query.getSuspiciousType())
&& Boolean.TRUE.equals(query.getIncludeExternalPerson()))
);
}
@Test
void shouldReturnEmptyExternalSuspiciousTransactionsWhenExternalSubjectDoesNotExist() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
when(overviewMapper.selectExternalPersonSubjectExistsByProjectId(40L)).thenReturn(null);
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setSuspiciousType("external_person");
CcdiProjectSuspiciousTransactionPageVO result = service.getSuspiciousTransactions(queryDTO);
assertEquals(0L, result.getTotal());
assertTrue(result.getRows().isEmpty());
verify(overviewMapper, never()).selectSuspiciousTransactionPage(
any(Page.class),
any(CcdiProjectSuspiciousTransactionQueryDTO.class)
);
}
@Test
void shouldReturnEmptyExternalSuspiciousTransactionExportWhenExternalSubjectDoesNotExist() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
when(overviewMapper.selectExternalPersonSubjectExistsByProjectId(40L)).thenReturn(null);
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setSuspiciousType("EXTERNAL_PERSON");
List<CcdiProjectSuspiciousTransactionExcel> rows = service.exportSuspiciousTransactions(queryDTO);
assertTrue(rows.isEmpty());
verify(overviewMapper, never()).selectReportSuspiciousTransactionList(any(CcdiProjectSuspiciousTransactionQueryDTO.class));
}
@Test @Test
void shouldExportSuspiciousTransactionsWithCurrentFilter() { void shouldExportSuspiciousTransactionsWithCurrentFilter() {
CcdiProject project = new CcdiProject(); CcdiProject project = new CcdiProject();

View File

@@ -0,0 +1,25 @@
# 涉疑交易无外部人员短路后端实施计划
## 背景
生产结果总览首屏中,`/ccdi/project/overview/suspicious-transactions` 在无外部人员项目上可能超过前端 10 秒超时。该接口的汇总 SQL 无条件拼接外部人员预警分支,导致只上传员工数据的项目仍会执行外部人员复杂关联。
## 目标
- 保持涉疑交易原有口径不变。
- 当项目不存在外部人员主体时,跳过涉疑交易中的外部人员预警分支。
- 当查询类型为 `EXTERNAL_PERSON` 且项目不存在外部人员主体时,直接返回空结果。
## 实施步骤
1.`CcdiProjectSuspiciousTransactionQueryDTO` 增加内部开关 `includeExternalPerson`
2.`CcdiProjectOverviewMapper` 增加轻量外部人员主体存在性判断。
3.`CcdiProjectOverviewMapper.xml` 中将 `externalSuspiciousTransactionSql``union all` 改为按 `includeExternalPerson` 条件拼接。
4.`CcdiProjectOverviewServiceImpl` 中按 `suspiciousType` 设置外部人员分支开关;`EXTERNAL_PERSON` 无主体时直接返回空分页或空导出列表。
5. 补充 Mapper SQL 结构测试和 Service 行为测试。
## 验证
- 运行涉疑交易相关单测。
- 运行结果总览 Mapper SQL 结构测试。
- 验证无外部人员项目不再执行涉疑交易外部人员分支。

View File

@@ -0,0 +1,40 @@
# 涉疑交易无外部人员短路实施记录
## 修改内容
- 为涉疑交易查询 DTO 增加 `includeExternalPerson` 内部开关。
- 新增 `selectExternalPersonSubjectExistsByProjectId`,使用 `limit 1` 轻量判断项目是否存在外部人员主体。
- 将涉疑交易 SQL 中外部人员预警 `union all` 改为按 `includeExternalPerson` 条件拼接。
- 在涉疑交易分页和导出服务入口设置外部人员分支开关;`EXTERNAL_PERSON` 且无外部人员主体时直接返回空结果。
- 补充 Service 单测和 Mapper SQL 结构测试。
## 影响范围
- 影响接口:
- `GET /ccdi/project/overview/suspicious-transactions`
- 涉疑交易导出相关服务调用
- 不改变员工本人、员工亲属、模型规则、名单命中等涉疑交易原有查询口径。
- 不修改前端逻辑和数据库表结构。
## 验证情况
- 首次执行以下命令时,未带 `-am` 编译依赖模块,`ccdi-project` 使用本地旧版 `ccdi-lsfx` 依赖,导致编译阶段出现 LSFX 接口签名不一致报错,未进入测试阶段:
```bash
mvn -pl ccdi-project "-Dtest=CcdiProjectOverviewServiceSuspiciousTransactionTest,CcdiProjectOverviewMapperSqlTest" test
```
- 已使用 `-am` 重新构建依赖模块并执行定向单测:
```bash
mvn -pl ccdi-project -am "-Dtest=CcdiProjectOverviewServiceSuspiciousTransactionTest,CcdiProjectOverviewMapperSqlTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
```
- 结果:通过,`Tests run: 14, Failures: 0, Errors: 0, Skipped: 0`
- 覆盖测试:
- `CcdiProjectOverviewServiceSuspiciousTransactionTest`
- `CcdiProjectOverviewMapperSqlTest`
## 备注
本次修复针对无外部人员项目的性能短路,避免为了计算“没有外部人员”而执行外部人员预警复杂关联。