优化资金图谱主题节点检索
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` 通过。
|
||||
- 构建过程存在前端项目既有资源体积告警,不影响本次恢复外链图谱展示的打包结果。
|
||||
@@ -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'
|
||||
);
|
||||
```
|
||||
Reference in New Issue
Block a user