优化资金图谱主题节点检索

This commit is contained in:
wkc
2026-06-03 17:11:09 +08:00
parent d45e9410ef
commit c5b2033a3d
5 changed files with 240 additions and 29 deletions

View File

@@ -20,6 +20,10 @@ public interface CcdiFundGraphMapper {
List<CcdiFundGraphNodeVO> selectFundGraphSubjects(@Param("query") CcdiFundGraphQueryDTO query);
List<CcdiFundGraphNodeVO> selectFundGraphSubjectsByExactKeyword(@Param("query") CcdiFundGraphQueryDTO query);
List<CcdiFundGraphNodeVO> selectFundGraphSubjectsByName(@Param("query") CcdiFundGraphQueryDTO query);
List<CcdiFundGraphEdgeVO> selectFundGraphEdges(@Param("query") CcdiFundGraphQueryDTO query);
List<CcdiFundGraphEdgeVO> selectFundGraphManualEdges(@Param("query") CcdiFundGraphQueryDTO query);

View File

@@ -48,7 +48,7 @@ public class CcdiFundGraphServiceImpl implements ICcdiFundGraphService {
if (isBlank(query.getKeyword()) && isBlank(query.getObjectKey())) {
return Collections.emptyList();
}
return fundGraphMapper.selectFundGraphSubjects(query);
return selectSubjects(query);
}
@Override
@@ -134,13 +134,30 @@ public class CcdiFundGraphServiceImpl implements ICcdiFundGraphService {
if (isBlank(query.getObjectKey()) && isBlank(query.getKeyword())) {
return null;
}
List<CcdiFundGraphNodeVO> subjects = fundGraphMapper.selectFundGraphSubjects(query);
List<CcdiFundGraphNodeVO> subjects = selectSubjects(query);
if (subjects == null || subjects.isEmpty()) {
return null;
}
return subjects.get(0);
}
private List<CcdiFundGraphNodeVO> selectSubjects(CcdiFundGraphQueryDTO query) {
if (query == null) {
return Collections.emptyList();
}
if (!isBlank(query.getObjectKey())) {
return fundGraphMapper.selectFundGraphSubjects(query);
}
if (isBlank(query.getKeyword())) {
return Collections.emptyList();
}
List<CcdiFundGraphNodeVO> exactSubjects = fundGraphMapper.selectFundGraphSubjectsByExactKeyword(query);
if (exactSubjects != null && !exactSubjects.isEmpty()) {
return exactSubjects;
}
return fundGraphMapper.selectFundGraphSubjectsByName(query);
}
private List<CcdiFundGraphNodeVO> buildNodes(CcdiFundGraphNodeVO centerNode, List<CcdiFundGraphEdgeVO> edges) {
Map<String, CcdiFundGraphNodeVO> nodeMap = new LinkedHashMap<>();
Map<String, CcdiFundGraphNodeVO> subjectCache = new LinkedHashMap<>();

View File

@@ -116,17 +116,79 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
INNER JOIN lx_fund_flow_subject_node to_subject
ON CONCAT('idno_node/', to_subject.object_key) = to_own.from_key
WHERE 1 = 1
<if test="query.objectKey != null and query.objectKey != ''">
AND (
from_subject.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
OR to_subject.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
)
</if>
<include refid="detailFilter"/>
</sql>
<select id="selectFundGraphSubjects" resultMap="FundGraphNodeResultMap">
<sql id="subjectJoinRowsByCenter">
SELECT
d.object_key AS detailObjectKey,
d.bank_statement_id AS bankStatementId,
d.trx_date AS trxDate,
d.le_account_no AS leAccountNo,
d.le_account_name AS leAccountName,
d.customer_account_name AS customerAccountName,
d.customer_account_no AS customerAccountNo,
d.cash_type AS cashType,
d.user_memo AS userMemo,
d.amount,
d.flag AS direction,
d.family_relation_type AS familyRelationType,
center_subject.object_key AS fromObjectKey,
to_subject.object_key AS toObjectKey,
CONCAT('idno_node/', center_subject.object_key) AS fromKey,
CONCAT('idno_node/', to_subject.object_key) AS toKey,
center_subject.name AS fromName,
to_subject.name AS toName,
center_subject.idnocfno AS fromIdNo,
to_subject.idnocfno AS toIdNo
FROM lx_fund_flow_subject_node center_subject
INNER JOIN lx_fund_flow_own_account_edge from_own
ON from_own.from_key = CONCAT('idno_node/', center_subject.object_key)
INNER JOIN lx_fund_flow_detail_edge d
ON d.from_key = from_own.to_key
INNER JOIN lx_fund_flow_own_account_edge to_own
ON to_own.to_key = d.to_key
INNER JOIN lx_fund_flow_subject_node to_subject
ON to_subject.object_key = SUBSTRING(to_own.from_key, 11)
WHERE center_subject.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
<include refid="detailFilter"/>
UNION ALL
SELECT
d.object_key AS detailObjectKey,
d.bank_statement_id AS bankStatementId,
d.trx_date AS trxDate,
d.le_account_no AS leAccountNo,
d.le_account_name AS leAccountName,
d.customer_account_name AS customerAccountName,
d.customer_account_no AS customerAccountNo,
d.cash_type AS cashType,
d.user_memo AS userMemo,
d.amount,
d.flag AS direction,
d.family_relation_type AS familyRelationType,
from_subject.object_key AS fromObjectKey,
center_subject.object_key AS toObjectKey,
CONCAT('idno_node/', from_subject.object_key) AS fromKey,
CONCAT('idno_node/', center_subject.object_key) AS toKey,
from_subject.name AS fromName,
center_subject.name AS toName,
from_subject.idnocfno AS fromIdNo,
center_subject.idnocfno AS toIdNo
FROM lx_fund_flow_subject_node center_subject
INNER JOIN lx_fund_flow_own_account_edge to_own
ON to_own.from_key = CONCAT('idno_node/', center_subject.object_key)
INNER JOIN lx_fund_flow_detail_edge d
ON d.to_key = to_own.to_key
INNER JOIN lx_fund_flow_own_account_edge from_own
ON from_own.to_key = d.from_key
INNER JOIN lx_fund_flow_subject_node from_subject
ON from_subject.object_key = SUBSTRING(from_own.from_key, 11)
WHERE center_subject.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
AND from_subject.object_key != center_subject.object_key
<include refid="detailFilter"/>
</sql>
<sql id="fundGraphSubjectColumns">
n.object_key AS objectKey,
CONCAT('idno_node/', n.object_key) AS nodeKey,
n.name AS nodeName,
@@ -165,28 +227,57 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
0 AS depth,
0 AS totalAmount,
0 AS transactionCount
</sql>
<select id="selectFundGraphSubjects" resultMap="FundGraphNodeResultMap">
SELECT
<include refid="fundGraphSubjectColumns"/>
FROM lx_fund_flow_subject_node n
WHERE 1 = 1
<if test="query.objectKey != null and query.objectKey != ''">
AND n.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
</if>
<if test="query.objectKey == null or query.objectKey == ''">
<if test="query.keyword != null and query.keyword != ''">
AND (
n.idnocfno = (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
OR n.name LIKE (CONCAT('%', TRIM(#{query.keyword}), '%') COLLATE utf8mb4_general_ci)
OR n.object_key = (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
)
</if>
</if>
WHERE n.object_key = (#{query.objectKey} COLLATE utf8mb4_general_ci)
</select>
<select id="selectFundGraphSubjectsByExactKeyword" resultMap="FundGraphNodeResultMap">
SELECT exact_rows.*
FROM (
SELECT
<include refid="fundGraphSubjectColumns"/>,
0 AS matchOrder
FROM lx_fund_flow_subject_node n
WHERE n.idnocfno = (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
UNION ALL
SELECT
<include refid="fundGraphSubjectColumns"/>,
1 AS matchOrder
FROM lx_fund_flow_subject_node n
WHERE n.object_key = (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
AND (
n.idnocfno IS NULL
OR n.idnocfno != (TRIM(#{query.keyword}) COLLATE utf8mb4_general_ci)
)
) exact_rows
ORDER BY exact_rows.matchOrder, exact_rows.nodeName
LIMIT
<choose>
<when test="query.limit != null and query.limit > 0">
#{query.limit}
</when>
<otherwise>
20
</otherwise>
</choose>
</select>
<select id="selectFundGraphSubjectsByName" resultMap="FundGraphNodeResultMap">
SELECT
<include refid="fundGraphSubjectColumns"/>
FROM lx_fund_flow_subject_node n
WHERE n.name LIKE (CONCAT('%', TRIM(#{query.keyword}), '%') COLLATE utf8mb4_general_ci)
ORDER BY
CASE
WHEN n.idnocfno = (TRIM(IFNULL(#{query.keyword}, '')) COLLATE utf8mb4_general_ci) THEN 0
WHEN n.object_key = (TRIM(IFNULL(#{query.keyword}, '')) COLLATE utf8mb4_general_ci) THEN 1
WHEN n.staff_id IS NOT NULL AND TRIM(n.staff_id) != '' THEN 2
WHEN UPPER(IFNULL(n.source_type, '')) LIKE '%EMPLOYEE%' THEN 2
WHEN n.source_type LIKE '%员工%' THEN 2
ELSE 3
WHEN n.staff_id IS NOT NULL AND TRIM(n.staff_id) != '' THEN 0
WHEN UPPER(IFNULL(n.source_type, '')) LIKE '%EMPLOYEE%' THEN 0
WHEN n.source_type LIKE '%员工%' THEN 0
ELSE 1
END,
n.name
LIMIT
@@ -247,7 +338,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ELSE 0
END AS canTrace
FROM (
<include refid="subjectJoinRows"/>
<include refid="subjectJoinRowsByCenter"/>
) graph_rows
WHERE 1 = 1
GROUP BY

View File

@@ -0,0 +1,20 @@
# 图谱展示恢复外链版本实施记录
## 基本信息
- 实施日期2026-06-01
- 实施分支dev
- 恢复前版本保存分支codex/save-dev-current-graph
- 影响范围:项目详情专项排查页图谱展示区域
## 修改内容
- 将专项排查页 `SpecialCheck.vue` 的图谱展示组件从内置资金图谱 `FundGraphSection` 恢复为外链图谱组件 `GraphAtlasSection`
- 保留员工家庭资产负债数据为空时的空状态展示,同时在空状态下继续展示外链图谱查询入口。
- 保持扩展查询区域仍位于专项排查主体内容之后。
## 验证情况
- 已执行 `git diff --check`,未发现空白字符问题。
- 已通过 `nvm use` 切换到 Node `v14.21.3`,执行 `npm run build:prod` 通过。
- 构建过程存在前端项目既有资源体积告警,不影响本次恢复外链图谱展示的打包结果。

View File

@@ -0,0 +1,79 @@
# 资金流图谱接口超时优化实施记录
## 背景
- 生产反馈:资金流图谱页面请求 `/ccdi/project/fund-graph/graph` 出现接口超时。
- 截图中的请求参数为 `keyword=330781199401056317`,属于身份证号精确查询场景。
- 生产数据量约 61 万主体时,原主体定位 SQL 将身份证精确匹配、姓名模糊匹配、`object_key` 精确匹配混在同一个 `OR` 条件内,并叠加 `ORDER BY CASE`,容易导致优化器选择低效执行计划。
## 修改内容
1. 拆分主体定位查询:
- 文件:`ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiFundGraphMapper.java`
- 文件:`ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFundGraphMapper.xml`
- 新增 `selectFundGraphSubjectsByExactKeyword`,只查询 `idnocfno = keyword``object_key = keyword`
- 新增 `selectFundGraphSubjectsByName`,仅在精确查询无结果后再执行 `name LIKE '%keyword%'`
- `selectFundGraphSubjects` 保留为 `object_key` 主键查询,不再执行额外排序。
2. 调整 Service 调用顺序:
- 文件:`ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFundGraphServiceImpl.java`
- `/search``/graph` 的中心节点解析统一走 `selectSubjects`
-`objectKey` 时直接主键查询。
-`objectKey` 且有 `keyword` 时,先精确查询;精确命中后直接返回,不再执行姓名模糊查询。
3. 优化图谱边查询入口:
- 文件:`ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFundGraphMapper.xml`
- 新增 `subjectJoinRowsByCenter`,将“中心主体作为起点”和“中心主体作为终点”拆为两段 `UNION ALL`
- `selectFundGraphEdges` 改为引用 `subjectJoinRowsByCenter`,避免在主体表两侧使用 `from_subject.object_key OR to_subject.object_key` 作为过滤入口。
## 影响范围
- 资金流图谱主体搜索:`GET /ccdi/project/fund-graph/search`
- 资金流图谱查询:`GET /ccdi/project/fund-graph/graph`
- 边明细查询 `GET /ccdi/project/fund-graph/edge-detail` 仍使用原明细 SQL 片段,未改变入参和返回结构。
- 本次未新增数据库字段或索引脚本;现有 DDL 中已包含 `lx_fund_flow_subject_node.idnocfno``name``object_key` 相关索引。
## 验证情况
- 已通过静态断言确认:
- Mapper 与 XML 已新增精确查询和姓名查询方法。
- XML 中已移除 `OR n.name LIKE` 的精确/模糊混用形态。
- `selectFundGraphEdges` 已引用 `subjectJoinRowsByCenter` 且包含 `UNION ALL`
- 已通过 XML 格式检查:
- `xmllint --noout ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFundGraphMapper.xml`
- 已通过 MyBatis XML 解析和 SQL 渲染临时检查:
- `selectFundGraphSubjectsByExactKeyword`
- `selectFundGraphSubjectsByName`
- `selectFundGraphEdges`
- 已通过源码编译:
- `mvn -pl ccdi-project -am -DskipTests compile`
## 验证阻断说明
- 直接执行 `mvn -pl ccdi-project -DskipTests compile` 会使用本地仓库中的旧依赖模块,出现既有签名不一致错误;使用 `-am` 联编依赖模块后通过。
- 直接执行 `mvn -pl ccdi-project -Dtest=CcdiFundGraphTimeoutOptimizationTest test` 时,测试编译阶段被既有无关测试源码阻断,未进入本次临时测试断言:
- `CcdiBankStatementTest` 仍引用 `BankStatementItem#setCustomerCertNo``setCustomerSocialCreditCode`
- `CcdiFileUploadServiceImplTest` 仍按旧的 `LsfxAnalysisClient.uploadFile(Integer, Object, String)` 签名编写。
## 生产核对建议
- 发布后用生产超时样例身份证号重新请求 `/ccdi/project/fund-graph/graph`,核对接口耗时与返回图谱中心节点。
- 如生产库是历史库,先确认以下索引存在:
```sql
SHOW INDEX FROM lx_fund_flow_subject_node WHERE Key_name IN (
'PRIMARY',
'idx_lx_fund_flow_subject_idnocfno',
'idx_lx_fund_flow_subject_name'
);
SHOW INDEX FROM lx_fund_flow_own_account_edge WHERE Key_name IN (
'idx_lx_fund_flow_own_from_key',
'idx_lx_fund_flow_own_to_key'
);
SHOW INDEX FROM lx_fund_flow_detail_edge WHERE Key_name IN (
'idx_lx_fund_flow_detail_from_date',
'idx_lx_fund_flow_detail_to_date'
);
```