优化无外部人员涉疑交易查询
This commit is contained in:
@@ -22,4 +22,7 @@ public class CcdiProjectSuspiciousTransactionQueryDTO {
|
|||||||
|
|
||||||
/** 每页数量 */
|
/** 每页数量 */
|
||||||
private Integer pageSize;
|
private Integer pageSize;
|
||||||
|
|
||||||
|
/** 是否包含外部人员预警分支 */
|
||||||
|
private Boolean includeExternalPerson;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询外部人员预警模型卡片
|
* 查询外部人员预警模型卡片
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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