优化无外部人员涉疑交易查询
This commit is contained in:
@@ -22,4 +22,7 @@ public class CcdiProjectSuspiciousTransactionQueryDTO {
|
||||
|
||||
/** 每页数量 */
|
||||
private Integer pageSize;
|
||||
|
||||
/** 是否包含外部人员预警分支 */
|
||||
private Boolean includeExternalPerson;
|
||||
}
|
||||
|
||||
@@ -163,6 +163,14 @@ public interface CcdiProjectOverviewMapper {
|
||||
*/
|
||||
CcdiProjectExternalRiskSummaryVO selectExternalRiskSummaryByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 判断项目是否存在外部人员主体
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 存在时返回1,否则返回空
|
||||
*/
|
||||
Integer selectExternalPersonSubjectExistsByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 查询外部人员预警模型卡片
|
||||
*
|
||||
|
||||
@@ -86,6 +86,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
|
||||
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_OBJECT = "OBJECT";
|
||||
@@ -369,6 +375,11 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
) {
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
normalizeSuspiciousTransactionQuery(queryDTO);
|
||||
prepareSuspiciousTransactionExternalBranch(queryDTO);
|
||||
|
||||
if (isExternalPersonOnlyWithoutSubject(queryDTO)) {
|
||||
return emptySuspiciousTransactionPage();
|
||||
}
|
||||
|
||||
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(
|
||||
defaultPageNum(queryDTO.getPageNum()),
|
||||
@@ -389,6 +400,11 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
) {
|
||||
ensureProjectExists(queryDTO.getProjectId());
|
||||
normalizeSuspiciousTransactionQuery(queryDTO);
|
||||
prepareSuspiciousTransactionExternalBranch(queryDTO);
|
||||
|
||||
if (isExternalPersonOnlyWithoutSubject(queryDTO)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return defaultList(overviewMapper.selectReportSuspiciousTransactionList(queryDTO)).stream()
|
||||
.map(this::buildSuspiciousTransactionExcelRow)
|
||||
@@ -719,12 +735,42 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
queryDTO.setModelCode(null);
|
||||
}
|
||||
if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) {
|
||||
queryDTO.setSuspiciousType("ALL");
|
||||
queryDTO.setSuspiciousType(SUSPICIOUS_TYPE_ALL);
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
|
||||
stat.setKey(key);
|
||||
|
||||
@@ -811,6 +811,22 @@
|
||||
and risk.cert_no = subject.cert_no
|
||||
</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">
|
||||
<bind name="externalProjectId" value="projectId"/>
|
||||
select
|
||||
@@ -1161,6 +1177,7 @@
|
||||
<include refid="suspiciousTransactionNameHitSql"/>
|
||||
) name_hits on name_hits.bankStatementId = base.bankStatementId
|
||||
|
||||
<if test="query.includeExternalPerson == true">
|
||||
union all
|
||||
|
||||
select
|
||||
@@ -1181,6 +1198,7 @@
|
||||
from (
|
||||
<include refid="externalSuspiciousTransactionSql"/>
|
||||
) external_hits
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<sql id="suspiciousTransactionAggregatedSql">
|
||||
|
||||
@@ -91,7 +91,9 @@ class CcdiProjectOverviewMapperSqlTest {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
|
||||
String suspiciousSql = extractSelect(xml, "selectSuspiciousTransactionPage");
|
||||
String modelHitSql = extractSqlFragment(xml, "suspiciousTransactionModelHitSql");
|
||||
String mergedSql = extractSqlFragment(xml, "suspiciousTransactionMergedSql");
|
||||
String aggregatedSql = extractSqlFragment(xml, "suspiciousTransactionAggregatedSql");
|
||||
String externalSubjectExistsSql = extractSelect(xml, "selectExternalPersonSubjectExistsByProjectId");
|
||||
|
||||
assertTrue(modelHitSql.contains("from ccdi_bank_statement_tag_result tr"), 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("hasNameListHit"), 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");
|
||||
assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '中介'"), reportSuspiciousSql);
|
||||
|
||||
@@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
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
|
||||
void shouldExportSuspiciousTransactionsWithCurrentFilter() {
|
||||
CcdiProject project = new CcdiProject();
|
||||
|
||||
@@ -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 结构测试。
|
||||
- 验证无外部人员项目不再执行涉疑交易外部人员分支。
|
||||
@@ -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`
|
||||
|
||||
## 备注
|
||||
|
||||
本次修复针对无外部人员项目的性能短路,避免为了计算“没有外部人员”而执行外部人员预警复杂关联。
|
||||
Reference in New Issue
Block a user