| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992 |
- // C2 防护:边类型黑名单(模块级常量,参数化传入,勿拼字符串)
- // 普通模式:HAS_QUESTION + 能力画像全部边类型(支撑能力/考查能力/支撑维度/属于层次/对应模块)
- const HIDDEN_REL_TYPES = ['HAS_QUESTION', '支撑能力', '考查能力', '支撑维度', '属于层次', '对应模块'];
- // 能力画像模式:支撑能力仅由抽屉"展开支撑子图"专用查询(Q3a)消费,通用展开路径同样屏蔽
- const HIDDEN_REL_TYPES_ABILITY = ['HAS_QUESTION', '考查能力', '支撑维度', '属于层次', '对应模块'];
- // 边显示名:能力画像的"支撑能力/考查能力"边附加置信度(confidence 是过滤的第一口径)
- const formatEdgeLabel = (r_info) => {
- const type = r_info.type || r_info.props?.label || '相关';
- const c = r_info.props?.confidence;
- return c ? `${type}·${c}` : type;
- };
- // 节点 data 组装:能力画像属性(ability_id/level/definition 等)一并带上,
- // 供画布层次上色与能力项抽屉使用
- const toNodeData = (info) => {
- const p = info.props || {};
- return {
- text: p.name || '',
- elementId: info.elementId,
- labels: info.labels,
- ...(p.ability_id !== undefined && { ability_id: p.ability_id }),
- ...(p.level !== undefined && { level: p.level }),
- ...(p.definition !== undefined && { definition: p.definition }),
- ...(p.knowledge_support !== undefined && { knowledge_support: p.knowledge_support }),
- ...(p.star !== undefined && { star: p.star })
- };
- };
- export const graphService = {
- /**
- * 获取图谱的根节点
- * 能力画像模式:以 6 个维度(AbilityDimension)为根(维度有"支撑维度"入边,
- * 不能用 NOT ()-[]->(n) 判根,必须按标签直查)
- */
- async findRootNodes(neo4jInstance, includeAbility = false) {
- // 能力画像模式:6 个维度为根,节点大小按支撑知识点总数编码(如 877/818/672/653/637/419);
- // 不走 NOT ()-[]->(n) 判根(维度有"支撑维度"入边),按标签直查
- const cypher = includeAbility ? `
- MATCH (n:AbilityDimension)
- OPTIONAL MATCH ()-[r:支撑能力]->( )-[:包含]->(n)
- WITH n, count(r) AS supportCount
- RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels,
- properties(n) AS props, supportCount
- ` : `
- MATCH (n)
- WHERE NOT ()-[]->(n) AND NOT n:Question
- AND NOT n:AbilityDimension AND NOT n:Ability AND NOT n:AbilityLevel
- RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
- `;
- const records = await neo4jInstance.execute(cypher);
- // 将结果整理为清晰的数组格式
- return records.map(record => ({
- id: record.id,
- elementId: record.elementId,
- labels: record.labels,
- supportCount: record.supportCount ?? 0,
- ...record.props // 展开所有属性,比如 name, group 等
- }));
- },
- /**
- * 根据节点ID获取节点信息 WITH n,r,m LIMIT 50
- * */
- async findNodeById(neo4jInstance, nodeId, includeAbility = false) {
- // 能力画像模式:双向展开(Entity─支撑能力→Ability 等入边仅由 Q3a 专用查询消费,
- // 通用展开路径屏蔽全部能力画像边,画布只出 维度─包含→能力项);
- // AbilityLevel 不进画布(层次降维为 Ability 颜色属性)
- const cypher = includeAbility ? `
- MATCH (n)
- WHERE id(n) = ${nodeId}
- OPTIONAL MATCH (n)-[r1]->(m1)
- WHERE NOT type(r1) IN $hidden AND NOT m1:AbilityLevel
- AND (NOT n:Entity OR m1:Entity)
- OPTIONAL MATCH (m2)-[r2]->(n)
- WHERE NOT type(r2) IN $hidden AND NOT m2:AbilityLevel
- AND (NOT n:Entity OR m2:Entity)
- WITH n,
- CASE WHEN r1 IS NOT NULL AND m1 IS NOT NULL THEN {rel: r1, other: m1} ELSE null END AS outRel,
- CASE WHEN r2 IS NOT NULL AND m2 IS NOT NULL THEN {rel: r2, other: m2} ELSE null END AS inRel
- WITH n, [x IN [outRel, inRel] WHERE x IS NOT NULL] AS rels
- UNWIND rels AS rel
- WITH n, rel.rel AS r, rel.other AS m
- RETURN
- {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
- {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
- {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
- ` : `
- MATCH (n)-[r]->(m)
- WHERE id(n) = ${nodeId}
- AND NOT type(r) IN $hidden
- AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel
-
- RETURN
- {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
- {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
- {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
- `;
- const records = await neo4jInstance.execute(cypher, {
- hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
- });
- // 将 records 转换为 G6 需要的 {nodes:[], edges:[]} 格式
- const nodeMap = new Map();
- const edgeMap = new Map();
- records.forEach(record => {
- const {
- n_info,
- r_info,
- m_info
- } = record;
- // 处理起始节点 n
- if (n_info && !nodeMap.has(n_info.id)) {
- // console.log(n_info.props.sources)
- nodeMap.set(n_info.id, {
- id: String(n_info.id),
- data: toNodeData(n_info)
- });
- }
- // 处理目标节点 m
- if (m_info && !nodeMap.has(m_info.id)) {
- nodeMap.set(m_info.id, {
- id: String(m_info.id),
- data: toNodeData(m_info)
- });
- }
- // 处理关系 r
- if (r_info && !edgeMap.has(r_info.id)) {
- edgeMap.set(r_info.id, {
- id: String(r_info.elementId),
- source: String(n_info.id),
- target: String(m_info.id),
- label: formatEdgeLabel(r_info),
- // sources: r_info.props?.sources?r_info.props?.sources:"[]",
- });
- }
- });
- // console.log("根据节点ID获取节点信息:",nodeMap.values(), edgeMap.values())
- const result = {
- nodes: Array.from(nodeMap.values()),
- edges: Array.from(edgeMap.values())
- };
- // console.log('转换后的数据:', result);
- return result;
- },
- /**
- * 根据节点ID查询节点的双向关系(网状结构)
- * 包括入边和出边,展示节点的完整关联路径
- */
- async findNodeRelations(neo4jInstance, nodeId, includeAbility = false) {
- // 普通模式剔除能力画像三类节点;能力画像模式保留但排除 AbilityLevel(层次不进画布);
- // 能力画像模式下展开实体时仅保留实体邻居(过滤 Mu/Section 等结构节点)
- const filterM = includeAbility ? ' AND NOT m:AbilityLevel AND (NOT n:Entity OR m:Entity)'
- : ' AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel';
- const filterP = includeAbility ? ' AND NOT p:AbilityLevel AND (NOT n:Entity OR p:Entity)'
- : ' AND NOT p:AbilityDimension AND NOT p:Ability AND NOT p:AbilityLevel';
- const cypher = `
- MATCH (n)
- WHERE elementId(n) = $nodeId OR id(n) = toInteger($nodeId)
-
- OPTIONAL MATCH (n)-[r1]->(m)
- WHERE NOT type(r1) IN $hidden${filterM}
-
- OPTIONAL MATCH (p)-[r2]->(n)
- WHERE NOT type(r2) IN $hidden${filterP}
-
- WITH n, m, r1, p, r2
-
- WITH collect(DISTINCT n) + collect(DISTINCT m) + collect(DISTINCT p) AS allNodes,
- collect(DISTINCT r1) + collect(DISTINCT r2) AS allRelations
-
- UNWIND allNodes AS n1
- UNWIND allNodes AS n2
- MATCH (n1)-[r]->(n2)
- WHERE NOT type(r) IN $hidden AND r IS NOT NULL
-
- RETURN
- {id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
- {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
- {id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
- `;
-
- const records = await neo4jInstance.execute(cypher, {
- nodeId,
- hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
- });
- const nodeMap = new Map();
- const edgeMap = new Map();
- records.forEach(record => {
- const { n_info, r_info, m_info } = record;
- if (n_info && !nodeMap.has(n_info.id)) {
- nodeMap.set(n_info.id, {
- id: String(n_info.id),
- data: toNodeData(n_info)
- });
- }
- if (m_info && !nodeMap.has(m_info.id)) {
- nodeMap.set(m_info.id, {
- id: String(m_info.id),
- data: toNodeData(m_info)
- });
- }
- if (r_info && !edgeMap.has(r_info.elementId)) {
- edgeMap.set(r_info.elementId, {
- id: String(r_info.elementId),
- source: String(n_info.id),
- target: String(m_info.id),
- label: formatEdgeLabel(r_info),
- });
- }
- });
- if (nodeMap.size === 0) {
- const fallbackCypher = `
- MATCH (n) WHERE elementId(n) = $nodeId OR id(n) = toInteger($nodeId)
- RETURN {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info
- `;
- const fallbackRecords = await neo4jInstance.execute(fallbackCypher, { nodeId });
- if (fallbackRecords.length > 0) {
- const { n_info } = fallbackRecords[0];
- nodeMap.set(n_info.id, {
- id: String(n_info.id),
- data: {
- text: n_info.props.name || '',
- elementId: n_info.elementId,
- labels: n_info.labels,
- ...(n_info.props.star !== undefined && { star: n_info.props.star })
- }
- });
- }
- }
- return {
- nodes: Array.from(nodeMap.values()),
- edges: Array.from(edgeMap.values())
- };
- },
- /**
- * 根据父节点ID,获取其下属的子图
- * (包含父节点、所有子节点,以及这个家族圈子内部的所有交叉关系)
- */
- async findSubgraphByParentId(neo4jInstance, parentNodeId, includeAbility = false) {
- // 1. MATCH 找到指定的父节点 n
- // 2. OPTIONAL MATCH (n)-[r1]->(m) 顺着箭头找出所有的子节点 m
- // 3. 限制数量,并将父节点与所有子节点合并为 allNodes 节点池
- // 4. 在节点池内部,寻找任意两点之间的所有连线 r2(从而把子节点之间的关系也查出来)
- // 能力画像模式排除 AbilityLevel(层次降维为颜色属性,不进画布);
- // 展开实体时仅保留实体邻居(过滤 Mu/Section 等结构节点及其他类型)
- const abilityFilter = includeAbility ? ' AND NOT m:AbilityLevel AND (NOT n:Entity OR m:Entity)'
- : ' AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel';
- const cypher = `
- MATCH (n)
- WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
-
- // 明确单向箭头,只找父节点指向的子节点
- OPTIONAL MATCH (n)-[r1]->(m)
- WHERE NOT type(r1) IN $hidden${abilityFilter}
-
- WITH n, m LIMIT 60
-
- // 将父节点和它所有的子节点打包成一个“家族圈子”
- WITH collect(distinct n) + collect(distinct m) AS allNodes
-
- // 展开圈子,寻找圈子内部成员之间的所有关系(包括子节点与子节点之间的关系)
- UNWIND allNodes AS n1
- UNWIND allNodes AS n2
- MATCH (n1)-[r2]->(n2)
- WHERE NOT type(r2) IN $hidden
-
- RETURN
- {id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
- {id: id(r2), elementId: elementId(r2), type: type(r2), props: properties(r2)} AS r_info,
- {id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
- `;
-
- const records = await neo4jInstance.execute(cypher, {
- parentNodeId: parentNodeId,
- hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
- });
- const nodeMap = new Map();
- const edgeMap = new Map();
- records.forEach(record => {
- const { n_info, r_info, m_info } = record;
- // 1. 处理源节点
- if (n_info && !nodeMap.has(n_info.elementId)) {
- nodeMap.set(n_info.elementId, {
- id: String(n_info.elementId),
- data: {
- text: n_info.props.name || '',
- labels: n_info.labels,
- ...(n_info.props.star !== undefined && { star: n_info.props.star })
- }
- });
- }
- // 2. 处理目标节点
- if (m_info && !nodeMap.has(m_info.elementId)) {
- nodeMap.set(m_info.elementId, {
- id: String(m_info.elementId),
- data: {
- text: m_info.props.name || '',
- labels: m_info.labels,
- ...(m_info.props.star !== undefined && { star: m_info.props.star })
- }
- });
- }
- // 3. 处理关系(此时 r_info 包含了:父->子、子->子 的所有关系)
- if (r_info && !edgeMap.has(r_info.elementId)) {
- edgeMap.set(r_info.elementId, {
- id: String(r_info.elementId),
- source: String(n_info.elementId),
- target: String(m_info.elementId),
- label: formatEdgeLabel(r_info),
- });
- }
- });
- // 兜底机制:如果该父节点没有任何子节点,至少把父节点自身返回,避免前端画布空白
- if (nodeMap.size === 0) {
- const fallbackCypher = `
- MATCH (n) WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
- RETURN {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info
- `;
- const fallbackRecords = await neo4jInstance.execute(fallbackCypher, { parentNodeId: parentNodeId });
- if (fallbackRecords.length > 0) {
- const { n_info } = fallbackRecords[0];
- nodeMap.set(n_info.elementId, {
- id: String(n_info.elementId),
- data: {
- text: n_info.props.name || '',
- labels: n_info.labels,
- ...(n_info.props.star !== undefined && { star: n_info.props.star })
- }
- });
- }
- }
- return {
- nodes: Array.from(nodeMap.values()),
- edges: Array.from(edgeMap.values())
- };
- },
- /**
- * 查询某个父节点的直接子节点总数
- */
- async countChildren(neo4jInstance, parentNodeId, includeAbility = false) {
- // 能力画像模式:双向计数(出边邻居 + 入边邻居),排除 AbilityLevel;
- // 支撑能力等边在黑名单内,此计数反映"画布可见邻居数"
- const cypher = includeAbility ? `
- MATCH (n)
- WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
- OPTIONAL MATCH (n)-[r1]->(m1)
- WHERE NOT type(r1) IN $hidden AND NOT m1:AbilityLevel
- AND (NOT n:Entity OR m1:Entity)
- OPTIONAL MATCH (m2)-[r2]->(n)
- WHERE NOT type(r2) IN $hidden AND NOT m2:AbilityLevel
- AND (NOT n:Entity OR m2:Entity)
- WITH collect(DISTINCT m1) + collect(DISTINCT m2) AS allM
- RETURN size([x IN allM WHERE x IS NOT NULL]) AS total
- ` : `
- MATCH (n)-[r]->(m)
- WHERE (elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId))
- AND NOT type(r) IN $hidden
- AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel
- RETURN count(DISTINCT m) AS total
- `;
- const records = await neo4jInstance.execute(cypher, {
- parentNodeId,
- hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
- });
- return records[0]?.total || 0;
- },
- /**
- * 能力画像:查询能力项的层次详情(唯一需要查 AbilityLevel 节点的场景)
- * 取 bloom 区间映射与 definition,用于抽屉展示
- */
- async findAbilityLevelDetail(neo4jInstance, abilityId) {
- const cypher = `
- MATCH (a:Ability {ability_id: $abilityId})-[:属于层次]->(l:AbilityLevel)
- RETURN properties(l) AS levelProps
- `;
- const records = await neo4jInstance.execute(cypher, { abilityId });
- return records[0]?.levelProps || null;
- },
- /**
- * 能力画像:本维度的能力层次分布(聚合统计小字,如"会 3 · 通 2")
- */
- async findDimensionLevelDistribution(neo4jInstance, dimensionId) {
- const cypher = `
- MATCH (d:AbilityDimension {dimension_id: $dimensionId})-[:包含]->(a:Ability)
- RETURN a.level AS level, count(a) AS cnt
- ORDER BY cnt DESC
- `;
- const records = await neo4jInstance.execute(cypher, { dimensionId });
- return records.map(r => ({ level: r.level, count: r.cnt }));
- },
- /**
- * Q1 能力全景(首页态 + 一级展开)
- * 每行一个能力项,带支撑统计(total / high_cnt);
- * 首页态按维度聚合 supportCount(节点大小编码),展开态取该维度的能力项行
- */
- async findAbilityOverview(neo4jInstance) {
- const cypher = `
- MATCH (d:AbilityDimension)-[:包含]->(a:Ability)-[:属于层次]->(l:AbilityLevel)
- OPTIONAL MATCH (e:Entity)-[s:支撑能力]->(a)
- RETURN id(d) AS d_id, elementId(d) AS d_elementId,
- d.dimension_id AS dimension_id, d.name AS dimension_name,
- id(a) AS a_id, elementId(a) AS a_elementId,
- a.ability_id AS ability_id, a.name AS ability_name,
- a.level AS level, a.definition AS definition, a.knowledge_support AS knowledge_support,
- l.code AS level_code, l.bloom AS level_bloom, l.definition AS level_definition,
- count(s) AS total,
- sum(CASE WHEN s.confidence='高' THEN 1 ELSE 0 END) AS high_cnt
- ORDER BY d.dimension_id, a.ability_id
- `;
- return await neo4jInstance.execute(cypher);
- },
- /**
- * Q2 能力详情:支撑知识点列表(置信度分组排序:高→中→低,star 优先)
- * 供能力项抽屉展示(name/type/star/confidence/reasoning)
- */
- async findAbilitySupportList(neo4jInstance, abilityId) {
- const cypher = `
- MATCH (e:Entity)-[s:支撑能力]->(a:Ability {ability_id: $abilityId})
- RETURN e.name AS name, e.type AS type, e.star AS star,
- s.confidence AS confidence, s.reasoning AS reasoning
- ORDER BY CASE s.confidence WHEN '高' THEN 1 WHEN '中' THEN 2 ELSE 3 END,
- e.star DESC
- `;
- return await neo4jInstance.execute(cypher, { abilityId });
- },
- /**
- * Q3a 支撑子图·节点:Entity─支撑能力→Ability,高置信优先 + star 优先,LIMIT 默认 200
- */
- async findAbilitySupportSubgraph(neo4jInstance, abilityId, limit = 200) {
- const cypher = `
- MATCH (e:Entity)-[r:支撑能力]->(a:Ability {ability_id: $abilityId})
- WITH e, r, a
- ORDER BY CASE r.confidence WHEN '高' THEN 1 WHEN '中' THEN 2 WHEN '低' THEN 3 ELSE 4 END, e.star DESC
- LIMIT toInteger($limit)
- RETURN
- {id: id(a), elementId: elementId(a), labels: labels(a), props: properties(a)} AS n_info,
- {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
- {id: id(e), elementId: elementId(e), labels: labels(e), props: properties(e)} AS m_info
- `;
- const records = await neo4jInstance.execute(cypher, { abilityId, limit });
- const nodeMap = new Map();
- const edgeMap = new Map();
- records.forEach(record => {
- const { n_info, r_info, m_info } = record;
- if (n_info && !nodeMap.has(n_info.id)) {
- nodeMap.set(n_info.id, {
- id: String(n_info.id),
- data: toNodeData(n_info)
- });
- }
- if (m_info && !nodeMap.has(m_info.id)) {
- nodeMap.set(m_info.id, {
- id: String(m_info.id),
- data: toNodeData(m_info)
- });
- }
- if (r_info && !edgeMap.has(r_info.id)) {
- edgeMap.set(r_info.id, {
- id: String(r_info.elementId),
- source: String(m_info.id),
- target: String(n_info.id),
- label: formatEdgeLabel(r_info),
- });
- }
- });
- return {
- nodes: Array.from(nodeMap.values()),
- edges: Array.from(edgeMap.values())
- };
- },
- /**
- * Q3b 支撑子图·实体间语义边:仅在 Q3a 返回的实体名列表内,
- * 找 前序/关联/因果/对比 四类语义关系($names 为实体名列表)
- */
- async findEntitySemanticEdges(neo4jInstance, names) {
- if (!names || names.length < 2) return [];
- const cypher = `
- UNWIND $names AS n1
- UNWIND $names AS n2
- MATCH (a:Entity {name: n1})-[r]->(b:Entity {name: n2})
- WHERE type(r) IN ['前序', '关联', '因果', '对比']
- RETURN n1, n2, type(r) AS rel
- `;
- return await neo4jInstance.execute(cypher, { names });
- },
- /**
- * Q4 能力可选题量:考查该能力项的题目数(本阶段仅数字展示,不做跳转)
- */
- async findAbilityQuestionCount(neo4jInstance, abilityId) {
- const cypher = `
- MATCH (a:Ability {ability_id: $abilityId})<-[:考查能力]-(q:Question)
- RETURN count(q) AS q_cnt
- `;
- const records = await neo4jInstance.execute(cypher, { abilityId });
- return records[0]?.q_cnt || 0;
- },
- /**
- * 分页查询父节点的子图(用于子节点超过阈值时分批加载)
- * 行为与 findNodeById 一致(单向父子边,数字id),仅加分页
- */
- async findSubgraphByParentIdPaged(neo4jInstance, parentNodeId, skip, limit, includeAbility = false) {
- // 分页查询(普通模式单向父子边);能力画像模式为双向(入边邻居)
- const cypher = includeAbility ? `
- MATCH (n)
- WHERE (elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId))
- OPTIONAL MATCH (n)-[r1]->(m1)
- WHERE NOT type(r1) IN $hidden AND NOT m1:AbilityLevel
- AND (NOT n:Entity OR m1:Entity)
- OPTIONAL MATCH (m2)-[r2]->(n)
- WHERE NOT type(r2) IN $hidden AND NOT m2:AbilityLevel
- AND (NOT n:Entity OR m2:Entity)
- WITH n,
- CASE WHEN r1 IS NOT NULL AND m1 IS NOT NULL THEN {rel: r1, other: m1} ELSE null END AS outRel,
- CASE WHEN r2 IS NOT NULL AND m2 IS NOT NULL THEN {rel: r2, other: m2} ELSE null END AS inRel
- WITH n, [x IN [outRel, inRel] WHERE x IS NOT NULL] AS rels
- UNWIND rels AS rel
- WITH n, rel.rel AS r, rel.other AS m
- ORDER BY elementId(m), elementId(r)
- SKIP toInteger($skip) LIMIT toInteger($limit)
- RETURN
- {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
- {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
- {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
- ` : `
- MATCH (n)-[r]->(m)
- WHERE (elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId))
- AND NOT type(r) IN $hidden
- AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel
- WITH n, r, m
- ORDER BY elementId(m)
- SKIP toInteger($skip) LIMIT toInteger($limit)
- RETURN
- {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
- {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
- {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
- `;
- const records = await neo4jInstance.execute(cypher, {
- parentNodeId, skip, limit,
- hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
- });
- const nodeMap = new Map();
- const edgeMap = new Map();
- records.forEach(record => {
- const { n_info, r_info, m_info } = record;
- // 起始节点 n
- if (n_info && !nodeMap.has(n_info.id)) {
- nodeMap.set(n_info.id, {
- id: String(n_info.id),
- data: toNodeData(n_info)
- });
- }
- // 目标节点 m
- if (m_info && !nodeMap.has(m_info.id)) {
- nodeMap.set(m_info.id, {
- id: String(m_info.id),
- data: toNodeData(m_info)
- });
- }
- // 关系 r
- if (r_info && !edgeMap.has(r_info.id)) {
- edgeMap.set(r_info.id, {
- id: String(r_info.elementId),
- source: String(n_info.id),
- target: String(m_info.id),
- label: formatEdgeLabel(r_info),
- });
- }
- });
- return {
- nodes: Array.from(nodeMap.values()),
- edges: Array.from(edgeMap.values())
- };
- },
- /**
- * 根据节点名称模糊查询节点信息
- * 按模式区分检索范围:
- * - 普通模式(智能分析/智能组卷):Book/Part/Chapter/Section/Mu 结构节点 + 全部实体节点
- * - 能力画像模式:维度 + 能力项(懂/会/通/创)+ 全部实体节点
- */
- async searchNodesByName(neo4jInstance, keyword, includeAbility = false) {
- const typeFilter = includeAbility
- ? `(node:AbilityDimension OR node:Ability OR node:Entity)`
- : `(node:Book OR node:Part OR node:Chapter OR node:Section OR node:Mu OR node:Entity)`;
- // 1. 同款 Cypher 改造:在数据库直接把数据组装成干净的 JSON 对象
- const cypher = `
- MATCH (node)
- WHERE ${typeFilter}
- AND node.name CONTAINS $keyword
- WITH collect(node) AS items
- WITH items[0..100] AS nodesList
- UNWIND nodesList AS n
-
- // 寻找这些节点之间的内部上下级/横向关系
- OPTIONAL MATCH (n)-[r]->(m)
- WHERE m IN nodesList AND NOT type(r) IN $hidden
-
- // 同款 RETURN 打包语法糖,把所有需要的字段一口气全部暴露出来
- RETURN
- {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
- CASE WHEN r IS NOT NULL
- THEN {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)}
- ELSE null
- END AS r_info,
- CASE WHEN m IS NOT NULL
- THEN {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)}
- ELSE null
- END AS m_info
- `;
- // 2. 使用你的自定义注入插件执行查询
- const records = await neo4jInstance.execute(cypher, {
- keyword,
- hidden: HIDDEN_REL_TYPES
- });
- // console.log("模糊搜索原始 records:", records);
- // 3. 采用跟你一模一样的 Map 去重组装逻辑
- const nodeMap = new Map();
- const edgeMap = new Map();
- records.forEach(record => {
- // 解构出每一行的 JSON 数据
- const {
- n_info,
- r_info,
- m_info
- } = record;
- // ================= 处理主节点 n (匹配到名字的医院等节点) =================
- if (n_info && !nodeMap.has(n_info.id)) {
- nodeMap.set(n_info.id, {
- id: String(n_info.id),
- data: {
- text: n_info.props.name || '',
- elementId: n_info.elementId,
- labels: n_info.labels,
- ...(n_info.props.star !== undefined && { star: n_info.props.star })
- }
- });
- }
- // ================= 处理目标节点 m (存在内部关系的另一个医院节点) =================
- if (m_info && !nodeMap.has(m_info.id)) {
- nodeMap.set(m_info.id, {
- id: String(m_info.id),
- data: {
- text: m_info.props.name || '',
- elementId: m_info.elementId,
- labels: m_info.labels,
- ...(m_info.props.star !== undefined && { star: m_info.props.star })
- }
- });
- }
- // ================= 处理关系连线 r =================
- if (r_info && !edgeMap.has(r_info.id)) {
- edgeMap.set(r_info.elementId, {
- id: String(r_info.elementId),
- source: String(n_info.id), // 连线起点
- target: String(m_info.id), // 连线终点
- label: formatEdgeLabel(r_info),
- // sources: r_info.props?.sources ? r_info.props.sources : "[]",
- });
- }
- });
- const result = {
- nodes: Array.from(nodeMap.values()),
- edges: Array.from(edgeMap.values())
- };
- // console.log("模糊搜索内连最终结果:", result);
- return result;
- },
- /**
- * 根据节点id查询节点中的sources,返回新的结构格式
- * */
- async searchNodeSources(neo4jInstance, nodeId) {
- const cypher = `
- MATCH (node)
- WHERE id(node) = ${nodeId}
- RETURN properties(node) AS props
- `;
- const records = await neo4jInstance.execute(cypher);
- const props = records[0]?.props || {};
- // 能力画像节点(维度/能力项/层次)用 definition/knowledge_support 等属性展示
- const result = [{
- mu_id: props.mu_id || props.dimension_id || props.ability_id || props.code || '',
- name: props.name || '',
- path: props.path
- || [props.core_position, props.knowledge_module, props.domain_tag].filter(Boolean).join(' · ')
- || '',
- text: props.description || props.text || props.definition
- || [props.level && `层次:${props.level}`, props.bloom && `Bloom:${props.bloom}`, props.knowledge_support].filter(Boolean).join('\n')
- || '',
- menuVisible: false,
- menuStyle: {}
- }];
- return JSON.stringify(result);
- },
- /**
- * 根据关系id查询关系中的sources
- * */
- async searchEdgeSources(neo4jInstance, edgeId) {
- const cypher = `
- MATCH (n)-[r]->(m)
- WHERE elementId(r) = $edgeId
- RETURN r.source_sentence as source_sentence, r.slice_id as slice_id, r.chapter as chapter, r.slice_text as slice_text,
- properties(r) AS props, type(r) AS rel_type
- `;
- const sources = []
- const records = await neo4jInstance.execute(cypher, {
- edgeId
- });
- const rec = records[0];
- if (!rec) return "[]";
- // 知识图谱边:有原文切片属性,直接返回
- if (rec.source_sentence || rec.slice_id || rec.chapter || rec.slice_text) {
- sources.push(rec);
- return JSON.stringify(sources);
- }
- // 能力画像边(支撑能力/考查能力等):展示边属性
- // 样例: {confidence:"高", level:"会", bloom_level:"L3", knowledge_domain:"质量", reasoning:"..."}
- const p = rec.props || {};
- if (Object.keys(p).length > 0) {
- sources.push({
- mu_id: rec.rel_type,
- name: p.confidence ? `置信度:${p.confidence}` : '关系属性',
- path: [
- p.level && `层次:${p.level}`,
- p.bloom_level && `Bloom:${p.bloom_level}`,
- p.knowledge_domain && `领域:${p.knowledge_domain}`,
- p.derived === true ? 'derived' : null,
- p.via_entity && `经由实体:${p.via_entity}`
- ].filter(Boolean).join(' · '),
- text: p.reasoning || JSON.stringify(p)
- });
- return JSON.stringify(sources);
- }
- return "[]";
- },
- /**
- * 根据实体id查询MU中所有信息
- * */
- async searchMuByEntitySources(neo4jInstance, nodId) {
- const cypher = `
- MATCH (mu)-[:包含实体]->(node)
- WHERE id(node) = ${nodId}
- RETURN properties(mu) AS muProps
- `;
- const records = await neo4jInstance.execute(cypher, {
- nodId
- });
-
- const result = records.map(record => {
- const muProps = record.muProps || {};
- return {
- mu_id: muProps.mu_id || '',
- name: muProps.name || '',
- path: muProps.path || '',
- text: muProps.description || muProps.text || '',
- menuVisible: false,
- menuStyle: {}
- };
- });
-
- return JSON.stringify(result);
- },
- /**
- * 根据标签查询所有label的节点
- * */
- async findNodesByLabel(neo4jInstance, label) {
- const cypher = `
- MATCH(n:${label})
- RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
- `;
- const records = await neo4jInstance.execute(cypher);
- // console.log("根据标签查询节点11:", records);
- // 将结果整理为清晰的数组格式
- return records.map(record => ({
- id: record.id,
- elementId: record.elementId,
- labels: record.labels,
- ...record.props // 展开所有属性,比如 name, group 等
- }));
- },
- /**
- * 根据权重查询节点
- * */
- async findNodesByWeight(neo4jInstance, weight) {
- const cypher = `
- MATCH(n)
- WHERE n.weight = $weight
- RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
- `;
- const records = await neo4jInstance.execute(cypher, { weight });
- return records.map(record => ({
- id: record.id,
- elementId: record.elementId,
- labels: record.labels,
- ...record.props
- }));
- },
- /**
- * 查询所有的Question节点
- * */
- async findQuestionsNodes(neo4jInstance) {
- const cypher = `
- MATCH (n)
- WHERE NOT ()-[]->(n) OR n:Question
- RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
- `;
- const records = await neo4jInstance.execute(cypher);
- // 将结果整理为清晰的数组格式
- return records.map(record => ({
- id: record.id,
- elementId: record.elementId,
- labels: record.labels,
- ...record.props // 展开所有属性
- }));
- },
- async findNodesByQuestionType(neo4jInstance, questionType) {
- // 1. 修改 Cypher 语句,直接比对节点的属性 n.questionType
- const cypher = `
- MATCH (n)
- WHERE n.question_type = $targetType
- RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
- `;
- // 2. 将动态的 questionType 作为参数传入,防止注入并提高性能
- const records = await neo4jInstance.execute(cypher, {
- targetType: questionType
- });
- // 3. 保持原有的数据格式化逻辑
- return records.map(record => ({
- id: record.id,
- elementId: record.elementId,
- labels: record.labels,
- ...record.props // 展开后,questionType 也会平铺在这个对象的顶层
- }));
- },
- /**
- * 根据entity_name和question_type查询Question节点
- * @param {Object} neo4jInstance Neo4j 驱动实例
- * @param {string[]} entity_names 要查询的实体名称数组
- * @param {string[]} question_types 要查询的题目类型数组
- * @returns {Promise<QuestionNode[]>} 符合条件的Question节点数组
- * */
- async findQuestionsNodesByNameAndType(neo4jInstance, entity_names, question_types) {
- const cypher = `
- MATCH (q:Question)
- WHERE q.entity_name IN $entity_names
- AND ($question_types = [] OR q.question_type IN $question_types)
- RETURN
- id(q) AS node_db_id, // Neo4j数据库内置节点ID
- q.elementId AS elementId, // 节点自身属性elementId
- q AS node
- ORDER BY q.chapter ASC, q.question_type ASC
- `.trim();
- const records = await neo4jInstance.execute(cypher, {
- entity_names,
- question_types: question_types || [] // 确保如果是 null/undefined 时传空数组
- });
- console.log(records, 111)
- return records.map(record => {
- return {
- id: record.node_db_id,
- elementId: record.elementId,
- ...record.node
- }
- // 替换 record.get('q') 为普通的 JS 对象取值
- const node = record.q;
- if (!node) return null; // 增强健壮性,防止查空
- // console.log(node);
- // return {
- // id: nodeDbId,
- // elementId: elementId,
- // ...node
- // };
- }) // 过滤掉可能为 null 的数据
- },
- /**
- * 根据单个节点名称,获取其自身及所有子节点的名称列表
- * @param {Object} neo4jInstance Neo4j 驱动实例
- * @param {string} nodeName 当前选中的节点名称
- * @param {boolean} includeChildren 是否勾选了包含子节点
- * @returns {Promise<string[]>} 节点名称数组
- */
- async getEntityNamesWithChildren(neo4jInstance, nodeName, includeChildren = false) {
- if (!nodeName) return [];
- // 如果没有勾选包含子节点,直接返回当前节点名称
- if (!includeChildren) {
- return [nodeName];
- }
- // 如果勾选了,去图谱中查询自身及所有后代节点
- const cypher = `
- MATCH (p{name: $nodeName})-[*1..]->(descendant)
- RETURN collect(DISTINCT descendant.name) AS name_list
- `
- const records = await neo4jInstance.execute(cypher, {
- nodeName
- });
- // console.log(records)
- if (records.length) {
- return records[0].name_list
- } else {
- return []
- }
- },
- /**
- * 查询库内可展开节点总数
- * 按模式区分统计口径:
- * - 普通模式(智能分析/智能组卷):Book/Part/Chapter/Section/Mu 结构节点 + 全部实体节点
- * - 能力画像模式:维度 + 能力项(懂/会/通/创)+ 全部实体节点
- * @param {Object} neo4jInstance Neo4j 驱动实例
- * @param {boolean} includeAbility 是否能力画像模式
- * @returns {Promise<number>} 节点总数
- */
- async countAllNodes(neo4jInstance, includeAbility = false) {
- const cypher = includeAbility
- ? `MATCH (n)
- WHERE (n:AbilityDimension OR n:Ability OR n:Entity)
- RETURN count(n) AS total`
- : `MATCH (n)
- WHERE (n:Book OR n:Part OR n:Chapter OR n:Section OR n:Mu OR n:Entity)
- RETURN count(n) AS total`;
- const records = await neo4jInstance.execute(cypher);
- return records[0]?.total || 0;
- }
- }
|