← 返回项目档案

项目复盘 · 2019

从产业链图谱到产融知识网络:产品、画布与数据治理复盘

从源码还原产业谱系的四向递归画布、节点探查和企业画像接口,并辨析产品路线图、数据准备与实际交付之间的边界。

产业链知识图谱企业数据数据治理

产业研究最常见的交付物是一张上下游关系图,但研究人员真正要完成的任务远不止“看图”:从某个材料或产品出发,需要知道它属于哪条产业链,链上有哪些企业,这些企业集中在哪里,资本和控制关系如何连接,又有哪些政策、新闻、研报和风险事件值得继续追踪。

产业链二代项目正是在这个问题上逐渐扩张。它先把产业链做成可搜索、可展开、可收藏的交互画布,随后加入成员列表、企业画像、区域分布、园区、协会、产业基金和资讯入口,最终形成“以产业链为索引组织企业与金融信息”的产品方向。这个方向有明确的前端实现和数据准备证据,但规划中的预测、自动报告和多类关系图谱,并不能全部按已交付能力处理。

本文聚焦产品逻辑和端到端流程;需要按模块查看恢复规格时,可继续阅读产业链知识网络技术规格,需要查看更集中的源码结论时,可阅读产业链关系画布与数据融合源码深读

先区分三层证据

本文把材料分成三个层次:

  • 源码事实:前端仓库存在路由、接口调用、数据变换和自研画布;采集仓库存在园区、概念股、新闻、政策和研报处理代码。
  • 项目记录:协作文档记录了版本排期、字段清单、数据规模快照和“完成、进行中、等待联调”等状态。
  • 产品规划:全息画像、控制关系、事件分析、预测和自动报告等功能被系统拆解过,但部分页面只有计划表或入口,缺少相应服务端代码与验收记录。

这一区分很重要。页面名称只能证明前端准备过某个入口;接口函数只能证明前端约定过调用契约;只有能与服务端、数据和测试记录相互印证,才适合写成完整交付。

从仓库恢复出的系统分层

主前端不是一张孤立的关系图。路由表实际包含产业谱系、产业链成员、企业画像、行业画像、产业云图、产业规模、基金匹配、基金全量检索、投资谱系、并购融资事件、资讯与行研、产业园区、产业集群、园区资讯、协会联盟和协会成员。由此可以把系统恢复为五层:来源适配负责采集,实体与关系层负责统一名称和标识,领域接口按产业、节点、公司、资本、区域与内容拆分,Vue 页面编排研究任务,Canvas 与图表组件负责呈现。

产业链平台的逻辑分层
产业结构与企业来源采集适配器原始记录与文件
原始记录与文件清洗和实体映射产业节点与企业主实体
产业节点与企业主实体关系和统计服务前端领域接口
前端领域接口产业谱系与节点探查企业和资本下钻
前端领域接口云图与规模分析区域和指标下钻
前端领域接口新闻、政策与研报原始证据阅读

这五层的证据完整度并不相同。前端页面、请求参数和画布算法可以由源码逐行确认;采集字段可以由多个 Python 与 Scrapy 仓库确认;但统一实体层和完整服务端没有以同版本代码保存下来。因此,本文会描述“前端要求服务端返回什么”,不会把缺失的图查询和消歧实现写成历史事实。

产品实际围绕哪些业务对象展开

从前端数据契约和需求文档可以还原出一组核心对象:

对象关键字段或关系在产品中的作用
产业链内部标识、名称、四个方向区域、节点树搜索和研究任务的主入口
产业节点名称、方向、子节点、是否可探查表示产品、原料、技术、服务或市场环节
企业基本信息、上市信息、地址、经营状态、股东和投资关系承接“节点上有哪些经营主体”
园区与协会地区、产业集群、成员企业、政策与资讯提供区域和组织视角
产业基金登记信息、资本、关联产业链、被投企业提供资金与产业匹配视角
内容与事件新闻、政策、研报、融资或风险事件为静态结构增加时间维度

前端接口层还约定了产业链检索、分类、单链详情、节点公司、节点分布、节点新闻、节点研报、节点政策、园区搜索、协会成员、基金搜索、上市公司画像等调用。它们显示产品曾尝试以统一导航承接多个数据域。需要强调的是,当前保留的是前端调用契约,并非这些接口的服务端完整实现。

前端接口契约到底细到什么程度

接口模块可以恢复的不只是资源名称,还包括筛选和分页字段。下表选择研究路径中最关键的契约;路径仅表示历史前端约定,不代表当前仍可访问。

接口域主要输入页面如何消费
产业链搜索关键词返回候选产业链,随后用稳定标识读取单链
单链详情chainId读取中心名称、四区节点树、收藏状态
节点探查nodeName、页码返回可继续进入的产业链及桥接节点列表
节点公司nodeName把整链成员表切换为当前节点统计
节点分布nodeName、区域类型在区域视角聚合关联企业
节点内容nodeName、页码、可选部门分别读取新闻、研报与政策
园区搜索地区、产业链、关键词、行政区、分页返回园区列表,再下钻产业集群、企业与资讯
协会联盟关键词、热门行业、分页返回协会,再读取成员与谱系
产业基金地区、资本区间、成立时间、产业链、分页支持全量筛选、投资谱系和产业匹配
上市公司companyId分拆读取基本信息、人物、股票、股东、管理层、股本和题材

这种拆法让每个页面可以延迟加载,但也造成一次企业画像要发出多次请求。前端没有聚合响应版本号;当各接口数据更新时间不同,用户看到的“同一家公司”可能混合多个批次。若重建,应让响应携带 asOf、来源版本和实体版本,或由后端提供一次性画像快照。

接口实现还有三项明确的工程语义。第一,请求超时统一设为两分钟,说明部分统计或图谱查询预期较慢。第二,请求拦截器会按问号前的路径取消已有请求;同一资源的不同查询参数也被视为重复请求。这适合搜索框的 latest-wins 场景,却可能误伤本应并行的分页或对比查询。第三,收藏新增和删除使用 GET 请求,会让预取、重放和缓存改变用户状态;重建时应改为幂等的 PUTDELETE

下面是按历史接口模块整理的等价代码(据源码契约重写),保留了节点探查、分布和内容查询的真实参数形态:

javascript
export function getExplorableChains({ name, page }) {
  return request.get("/node/explore/list", {
    params: { nodeName: name, page }
  })
}

export function getDistribution({ name, type }) {
  return request.get("/node/distribution/get", {
    params: { nodeName: name, areaType: type }
  })
}

export function getPolicyList({ name, page, department }) {
  return request.get("/node/policy/list", {
    params: { nodeName: name, page, department }
  })
}

原仓库使用字符串拼接生成查询串;这里改写为参数对象以去除历史实现中的注入与编码风险,因此不能把这段示例当成原文件的逐字符摘录。

一次查询在前端如何执行

源码中的主链路可以概括为:

  1. 用户输入产业或企业关键词,搜索接口返回候选产业链。
  2. 用户选中产业链后,页面按内部标识读取单链详情。
  3. 返回数据中的 chainBody 被交给自研 Canvas 组件,组件重新计算整张图的宽高、中心和全部节点坐标。
  4. 用户悬停节点时,画布只擦除并重绘该节点;点击具有探查标记的节点后,页面请求“可继续探查的产业链列表”。
  5. 选择探查结果会在新页面打开另一条产业链,同时携带节点名称作为上下文。
  6. 用户还可以缩放、拖动画布、收藏当前产业链,或把画布导出为图片。
产业链查询与节点探查
关键词候选产业链单链详情四向坐标计算Canvas 渲染
可探查节点候选关联链打开新产业链重新计算布局
当前产业链收藏或导出研究留存

节点探查由此不是简单的树展开。它允许用户把当前节点作为桥梁,跳到另一张以该概念为中心的产业图。例如研究者从“关键材料”节点进入与该材料相关的另一条产业链,再继续观察企业和区域信息。这种跨链导航,是产品从静态图走向知识网络的关键一步。

页面内部其实有两套查询状态

产业谱系页下方还有一张“成员分类统计”表。页面初次进入时按 chainId 查询整条产业链,各节点返回行业列表和三类企业数量;用户点击可探查节点后,Canvas 组件一方面请求候选关联链,另一方面向父页面发出节点名称,父页面随即切换为按 nodeName 查询单节点统计。一个布尔状态决定后续翻页和排序继续调用整链接口,还是节点接口。

谱系页的双查询状态
路由含 chainId加载整链绘制 Canvas请求整链成员统计
点击可探查节点记录 nodeName请求节点统计表格切换为单节点
点击可探查节点请求候选关联链选择候选新页面打开另一条链
翻页或排序判断当前查询模式重放整链查询或节点查询

表格排序没有直接把字段名传给后端,而是按固定数组映射为序号:上市/头部数量、核心数量、普通数量、总数依次映射为 1 到 4;升序、降序和无排序映射为 1、2、0。这里存在两层维护风险:前端列名与响应字段的语义并不完全直观,新增列还会改变位置协议。更稳定的契约应使用白名单枚举,例如 sort=totalCount&order=desc,并由契约测试确认每个展示列对应哪种企业口径。

下面的等价伪代码还原了页面状态转换,而不是复制 Vue 组件语法:

javascript
onRoute(chainId) {
  mode = "chain"
  query = { chainId, page: 1, pageSize: 5, sort: "none" }
  canvas.render(fetchChain(chainId))
  table.render(fetchChainStatistics(query))
}

onProbe(nodeName) {
  mode = "node"
  activeNode = nodeName
  table.render(fetchNodeStatistics(nodeName))
  probePanel.render(fetchExplorableChains(nodeName, 1))
}

onTableChange(page, sort) {
  return mode === "chain" ? fetchChainStatistics({ ...query, page, sort })
                          : fetchNodeStatistics(activeNode)
}

历史实现中节点统计接口没有携带分页和排序参数,但页面在节点模式下仍允许触发分页控件。这表明 UI 状态与服务契约没有完全闭合:要么节点结果本来只应是一行并禁用分页,要么接口需要支持与整链一致的分页排序。

四向递归画布的具体逻辑

产业链画布没有直接依赖通用关系图组件,而是自己实现了数据展开、坐标计算、绘制和命中检测。接口期望的数据可抽象为:中心产业链下有上、下、左、右四个区域;每个区域有名称和节点列表;节点包含内部标识、名称、方向、是否可探查以及下一层节点列表。

坐标计算分三步:

  1. 测量子树:递归遍历节点,统计每个方向的叶节点数 leafCount 和最大深度 depth。叶节点数决定同层需要预留多少横向或纵向槽位,深度决定离中心最远要延伸多少层。
  2. 深度优先展开:节点树被展平。每个节点保存层级、父节点、前面已经占用的叶槽数量和自身子树占用的叶槽数。
  3. 计算坐标:上、下区域把叶槽映射为横坐标,把层级映射为纵坐标;左、右区域反过来。父节点连接点由方向和层级决定,一级节点连接到中心区域边界,更深节点连接到父节点边缘。

因此,一个有多个后代的节点不会只占一个位置,而是占据其全部叶节点所需的跨度,并放在这段跨度的中间。这是一种适合规则树的确定性布局:同一份数据每次都能得到相同位置,也比力导向图更容易保持上下游方向。

接口中的 region 同时兼容两套命名:up/upstreamdown/downstreamleft/leftstreamright/rightstream,其他值直接抛错。节点被转换为内部对象后会增加 levelparentpreLlengthcanProbeisHoverisClick。其中 length 是子树叶节点数,preL 表示此前已经占用的叶槽;这两个量共同决定节点位于所属子树跨度的中心。

在缩放倍率为 1 时,普通节点基础尺寸为 24×120,横向与纵向间距分别为 12 和 24,中心节点为 120×30。画布并不紧贴内容:宽高计算结束后还各增加 600 像素留白,为拖动和弹层保留空间。上下区域使用叶节点数决定宽度、最大深度决定高度;左右区域正好互换。

以上游节点为例,横坐标的实际结构是:

x = x0 + preL × (nodeWidth + gapX) + length × (nodeWidth + gapX) ÷ 2 - nodeWidth ÷ 2

纵坐标则按层级逐层远离中心:

y = y0 - (level - 1) × (nodeHeight + gapY)

左、右区域交换横纵轴,并根据方向改变正负号。一级节点的父连接点落在中心区域边界,深层节点的父连接点取父节点边缘。这比“按数组下标排布”多做了一步:父节点始终位于其所有叶后代占用区域的视觉中心。

下面是依据画布源码整理的等价伪代码。它保留“先计算叶槽,再按方向放置”的算法骨架,但省略了样式与框架代码,并非原文件逐行摘录:

javascript
function layout(nodes, direction) {
  const flat = []
  let usedLeaves = 0
  for (const node of nodes) {
    const span = countLeaves(node.children)
    flat.push({ node, direction, span, usedLeaves })
    if (node.children.length > 0) {
      flat.push(...layout(node.children, direction))
    }
    usedLeaves += span
  }
  return flat.map(item => placeByLeafSpanAndDepth(item))
}

下列是 getItemXY 中上游分支的源码节选(已脱敏)。它验证了前面的坐标公式,同时保留一级节点与深层节点采用不同父连接点的处理:

javascript
if (data.type === "up") {
  const { w, h, x0, y0 } = this.getFirstXY("up")
  return {
    x:
      x0 +
      data.preL * (w + this.alteX) +
      (data.length * (w + this.alteX)) / 2 -
      w / 2,
    y: y0 - (data.level - 1) * (h + this.alteY),
    parentX: data.level === 1 ? this.x : data.props.x + w / 2,
    parentY:
      data.level === 1
        ? this.y - this.centerH / 2 - this.alteY - this.typeH
        : data.props.y
  }
}

连线同样按方向处理。若父子节点不在同一水平线或垂直线上,代码以两点中线构造两个控制点,画出带拐弯效果的贝塞尔虚线;中心节点、方向标签和普通节点由不同绘制器负责。长于八个字符的节点名称在悬停时以三百毫秒为步长滚动显示。

画布还考虑了高分屏:用设备像素比与 Canvas 后备像素比计算倍率,内部画布按倍率放大,再通过样式变换还原视觉尺寸。缩放不是对已有位图做简单拉伸,而是修改比例、重算坐标并重绘全部节点。这个实现思路保证了文字和线条在不同缩放级别下仍较清晰。

缩放滑块范围是 -80 到 100,步长 10,转换后对应 20% 到 200%。每次缩放都会重新创建坐标器、重设画布尺寸、重绘并重新绑定鼠标事件。重绘保持清晰,但重复绑定的生命周期依赖旧处理器被属性覆盖,长文本滚动定时器和窗口监听仍缺少统一销毁入口。更合理的组件协议应提供 setScaleresizedestroy,由 destroy 负责清理计时器、事件和临时对象地址。

命中检测、拖动画布与节点选择

鼠标移动时,代码在四个方向的展平节点中执行点位检测。命中可探查节点才改变指针并局部重绘 hover 状态;命中普通节点不会触发探查。文本超过八个字符时,每 300 毫秒截取连续八字,尾部以全角空格补齐,形成横向滚动效果。离开节点后停止计时器并重绘原节点。

鼠标按下时先判断命中对象:可探查节点会清除其他节点的点击态、设置当前点击态并执行回调;中心节点走另一分支;空白区域才进入拖动画布。这个顺序避免“点击节点同时拖动画布”,但只围绕鼠标设计,没有键盘焦点、ARIA 替代文本和触控手势。Canvas 也没有原生 DOM 节点,因此屏幕阅读器无法知道图里有哪些产业环节。

导出使用 canvas.toBlob 生成图片并通过临时对象地址下载。它适合汇报留图,却只保存像素,不包含节点标识、关系、来源和当前缩放参数。若图片进入研究报告,应同时生成一个伴随清单,至少记录产业链版本、节点版本、导出时间和数据快照时间。

区域分析不是一张地图

产品在旧版产业统计基础上,将区域能力重组为“产业云图”。规划给出的统计口径是:先筛选经营状态有效、能够映射到产业链的企业,再按注册地址聚合到省级区域或大区。帕累托视角同时展示企业数量、单一区域占比和累计占比;地图视角则与区域列表联动。

这里反映出一次合理的产品收敛:把分散的统计入口合并到一个可切换视角的分析任务中。其关键并不在地图着色,而在口径统一——企业是否存续、采用注册地址还是办公地址、一个企业能否属于多条产业链、跨地区集团如何计算,都必须在聚合前固定下来。

源码可确认前端存在云图页面和 ECharts 依赖,文档可确认上述计算定义;但缺少完整服务端聚合代码,因此本文不把所有统计口径描述为已在生产环境严格执行。

前端本身并不计算帕累托值。接口直接返回 barGraphtableCountshotCountstotalCount;每个区域已经带有 countpercentaccuPercent。柱状图使用成员数,折线使用累计占比,右轴固定为 0–100;前端只负责格式化。这意味着占比的分母、排序后再累计还是累计后再排序、未知地区是否进入分母,都属于缺失后端的证据边界。

产业云图的数据消费逻辑
节点名称区域分布接口省级数量和累计占比
省级数量和累计占比柱状图与帕累托折线热点区域表
省级数量和累计占比省级地图单省悬浮信息
大区汇总大区统计表大区内所有省份联动高亮

大区地图还有一个容易误读的实现细节:前端把某大区的汇总数量复制到该大区的每个省级图形上,用于整组着色和联动高亮。因此,大区模式表达的是“这些省属于同一大区,大区总量为多少”,并不表示每个省各自拥有相同数量。图例和悬浮提示若不明确标注,使用者会把大区总量误认为省级值。

下面是这一映射的源码节选(已脱敏)

javascript
for (const area of areaCounts) {
  for (const province of provinceMap) {
    if (province.area === area.base) {
      mapData.push({
        name: province.name,
        areaName: area.base,
        value: Number(area.count),
        scale: Number(area.percent)
      })
    }
  }
}

同一组件在大区模式下为地图注册悬停监听,在切回省级模式时解除。若数据在大区模式下多次刷新,初始化函数可能重复注册监听;ECharts 实例也没有在组件销毁时调用 dispose。重建时应把“数据更新”“模式切换”“组件销毁”作为三种独立生命周期测试。

数据管线留下了什么

历史仓库中存在多组 Scrapy 与 Python 程序,处理过以下数据:

  • 概念板块及其股票列表,包括代码、名称、入选原因和其他概念;
  • 产业园区名称、地区、产业集群和园区企业;
  • 新闻标题、时间、关键词、正文与页面内容;
  • 公司简介、主营业务、主营产品和市值;
  • 行业研报的标题、摘要、来源、发布时间、文件状态与正文文件;
  • 产业统计指标树,以及指标对应的周期数据。

数据跟踪表还记录过若干阶段性规模:行业研报约二十九万条、产业统计约二百二十万条、新闻约六百八十万条。它们是项目文档中的快照,并非本次通过数据库重新计数的结果,所以只能用来说明采集规模已经超出手工整理范围,不能作为最终上线量或有效量。

来源记录进入研究页面
来源列表调度任务列表页采集详情或文件下载
详情或文件下载原始内容留存字段清洗重复检查
字段清洗企业与产业候选映射人工或规则核验可发布关系
可发布关系统计与检索索引节点公司、云图和资讯接口
原始内容留存研报文件与正文PDF 阅读和来源下钻

行业研报快照至少保存地址摘要、标题、来源、上传时间、下载状态、文件位置和摘要。这个模型已经意识到“发现一条报告”和“文件真正可用”不是同一状态,但一个整数下载状态无法覆盖完整生命周期。更可恢复的状态机应区分 discoveredmetadata_saveddownloadedparsedindexedrejected,并为每次失败保存原因、重试次数和处理器版本。

资讯任务则把按日和按月运行的入口分开,并按来源维护 Spider;它能够降低单一解析器变化对其他来源的影响。但关键词命中只是候选关联。如果一篇报道同时出现企业名和产业词,并不能直接证明企业属于该产业节点。正确模型应至少保存 mention(文本提及)、classified_as(规则或模型分类)和 member_of(经过证据确认的产业成员)三种不同关系。

采集链路也暴露出明显的不完整性。例如园区爬虫的启动函数在单个样例请求后立即返回,使后面的九百余页批量逻辑不可达;股票写入管线在函数开头直接返回,后续数据库代码成为死代码;几份仓库保留了近乎相同的目录和提交历史。这说明“代码存在”与“稳定定时运行”之间仍有距离。

数据脚本还有更基础的安全与可复现问题:历史连接信息直接出现在源码中,建表工具会先删除同名库或表,部分删除与更新语句通过字符串格式化构造。本文不复述任何连接值;这些配置应按泄露处理并轮换。重建时,迁移脚本必须默认非破坏、使用参数绑定、输出变更计划,并要求显式的目标环境与二次确认。

真正的瓶颈是实体统一

项目记录曾出现典型的数据融合故障:企业更名后,来源系统给出新的外部标识;企业主表仍保留旧标识,投资关系却已经引用新标识,最终导致关系无法查到。高管、职位等字段在不同批次重复出现,也会让简单覆盖式导入产生冲突。

这类问题需要独立的统一实体层,而不是继续给业务接口打补丁:

  • 为公司、产业、园区、协会和基金生成稳定的内部标识;
  • 把来源标识、曾用名、标准名和时间范围保存为别名记录;
  • 关系边保存来源、抓取时间、适用时间和可信等级;
  • 合并操作保留审计记录,允许撤销错误合并;
  • 将“同名”“更名”“疑似同一主体”拆成不同状态,不自动等同。

只有这样,股东、投资、园区成员、协会成员、新闻主体和产业链节点才能长期指向同一个实体。否则每增加一种来源,都会放大错连和断链。

一条关系需要保存哪些信息

如果只保存 (企业)-[属于]->(产业节点),就无法回答关系何时成立、为什么成立、是否仍有效。恢复后的最小关系记录应包含:

字段作用
subjectId / objectId指向稳定内部实体,不使用展示名称连接
relationType区分产业成员、投资、股权、园区入驻、协会成员和文本提及
validFrom / validTo表示业务事实的有效时间,而非数据库更新时间
sourceId / sourceRecordId回到原始页面、文件或来源记录
observedAt记录本系统何时观察到这项事实
confidence / reviewState区分自动候选、人工确认、驳回和待复核
ruleVersion说明是哪一版映射或分类规则生成了关系

产业本体本身也要版本化。节点改名不应生成全新产业,节点拆分和合并则不能仅覆盖旧记录;需要保存 replaced_bysplit_intomerged_into 等演进关系。这样,旧报告仍能按当时口径复现,新查询也能显式选择“历史本体”或“当前本体”。

下面是建议重建的等价伪代码,不是历史后端实现:

python
def resolve_company(record, source, observed_at):
    candidates = by_source_id(source, record.external_id)
    candidates += by_normalized_name(record.names)
    candidates += by_registration_fingerprint(record.registration)

    ranked = score_candidates(candidates, record)
    if not ranked or ranked[0].score < AUTO_LINK_THRESHOLD:
        return create_review_case(record, ranked)
    if len(ranked) > 1 and ranked[0].score - ranked[1].score < SAFE_MARGIN:
        return create_review_case(record, ranked[:5])

    entity = ranked[0].entity
    save_alias(entity.id, record.name, source, observed_at)
    return entity

这段流程刻意保留“无法自动决定”的分支。实体解析最危险的失败不是少连一条边,而是把两个不同主体永久合并;疑似命中应进入审核队列,自动合并需要高阈值和足够领先优势,且每次合并都应能够撤销。

公司分类与统计口径

成员统计页面把企业分成三类并展示总数,另有独立接口返回上市公司、头部公司和成长公司列表。代码能确认页面和参数存在,却不能恢复分类计算方式。分类可能来自人工标签、上市状态、规模规则或综合评分,现有前端无法判断。

因此,不能把 companyCategory=1/2/3 直接解释成算法结论。重建时每个分类结果要带 categoryRuleruleVersion、输入指标和生效日期;总企业数还要明确是企业主实体去重数、来源记录数,还是“企业 × 产业节点”关系数。一个企业属于多个节点时,整链汇总若直接相加会重复计数。

产品决策如何演进

提交历史显示,V2 前端在五个月内从框架迁移逐步加入园区、协会、产业基金、上市信息、成员列表和节点探查。项目记录还提供了更细的状态:功能迁移阶段需要重新设计数据库和接口;园区、协会、基金与上市信息先后进入开发或联调;企业基本情况因全量导入和校验问题延期;后续多个关系图谱页面的计划表中,只有需求文档完成,数据、服务端、测试和上线栏仍为空。

时间与版本可以从提交和代码确认的变化不能据此确认的事项
2019 年 4 月,V2.0新前端框架、产业谱系与领域路由进入仓库新数据库和接口是否完成全量迁移
2019 年 5 月,V2.01园区检索、园区企业和排序测试园区来源覆盖率与持续更新
2019 年 5 月,V2.02协会联盟、成员、谱系及分级成员关系是否经过实体去重
2019 年 5 月,V2.03产业基金筛选、投资谱系、匹配分布间接关系被删除后的完整业务口径
2019 年 6 月,V2.04上市公司画像接口、成员统计与节点探查各画像接口是否共享同一数据快照
2019 年 6–7 月,V2.05搜索逻辑调整、界面反馈、隐藏未开发页签被隐藏模块是否后来完成
2019 年 8 月,V2.20 标记仓库存在版本提交缺少正式发布说明,不能推断全部规划上线

版本号在提交历史中不是严格线性:多条版本分支反复合并到开发分支,同一天也可能出现多个近似标签。上表因此表达“某阶段能看到什么”,而不是把每个提交等同于生产发布。若要恢复线上版本,需要部署产物摘要、构建编号或发布清单,单靠分支名无法完成对应。

源码中的一个提交尤其能说明团队开始管理范围:它明确隐藏了尚未开发的页签。相比把所有规划入口都展示给用户,这种做法能降低“页面存在即能力存在”的误解。产业基金阶段还删除过“间接实体”功能,说明图谱展示也经历了从关系越多越好,到控制复杂度和可解释性的调整。

路由还支持从查询参数接收登录态并写入浏览器本地存储,两小时后由客户端自行过期。这方便跨系统免登录跳转,却会让认证信息进入浏览历史、访问日志和分享链接;客户端时钟也不是可靠的会话失效依据。重建应使用一次性授权码在后端交换安全 Cookie,并由服务端控制撤销与过期。

源码暴露的工程缺口

当前实现有几类值得在重建时优先处理的问题:

  • 接口层把搜索词直接拼入查询字符串,缺少统一编码和参数序列化,特殊字符可能改变请求含义。
  • 登录态值可以从页面查询串读取后写入本地存储,容易出现在浏览器历史、日志或分享链接中。
  • 画布注册了窗口变化监听,但真正的重排调用被注释,容器尺寸改变后只能依赖用户重新操作。
  • 布局用递归遍历整棵树,缺少深度、节点数和环检测;异常关系若形成环,不能安全终止。
  • 坐标、字号和边距主要是固定数值,长名称依赖滚动显示,缺少文本测量、换行和无障碍替代内容。
  • 采集程序保存固定连接配置,且多个近似仓库各自维护解析逻辑;这既有安全风险,也容易造成字段口径分叉。
  • 单元测试目录仍接近脚手架示例,没有覆盖布局不重叠、点击命中、缩放后坐标或空区域等核心不变量。
  • 请求去重只比较问号前路径;相同接口的不同参数请求会互相取消,适用范围没有被显式约束。
  • 多个列表把排序字段编码成位置数字,前后端字段顺序一旦分叉就会产生静默错误。
  • 云图依赖后端预先计算比例,前端没有校验数量、占比和累计占比是否守恒。

如果今天重建

重建不应从恢复所有页面开始,而应先交付一条可验证的研究闭环:

  1. 建立版本化领域模型:产业链、节点、实体、关系、证据和时间范围分表管理。
  2. 采集层只输出来源记录;标准化、实体解析和关系生成分别执行,并记录每一步输入输出版本。
  3. 服务端提供稳定的产业链详情契约,并给节点数量、深度和探查候选设置上限。
  4. 前端继续保留确定性四向布局,但加入环检测、虚拟化、文本测量、键盘操作和截图前的完整性检查。
  5. 为实体合并、区域聚合和节点探查建立黄金样本;布局测试至少验证无重叠、父子方向正确和缩放命中一致。
  6. 所有规划能力使用“数据就绪、接口就绪、前端就绪、已验收”四段状态,禁止只凭路由或原型宣布完成。

可执行的验收样例

“图能打开”不足以验收这类系统。至少要固定以下黄金样例,并在本体、解析器或接口版本变化时重放:

场景输入样例必须验证的不变量
四向布局四个方向、三层深度、不同叶节点数节点不重叠,父节点居中,关系方向正确
异常结构空区域、未知方向、过深节点、关系环明确报错或截断,不能无限递归和白屏
节点探查同名节点对应多条产业链候选链有稳定标识、路径说明和确定排序
企业更名同一主体的新旧名称与两个来源标识仍连接同一内部实体,并保留别名时间范围
企业重名同名但登记信息不同的两个主体不得自动合并,进入人工复核
区域统计一家公司属于多个节点、一个集团跨地区去重分母明确,数量与占比守恒
内容关联只提及产业词但没有成员证据的新闻只能成为提及关系,不能提升为产业成员
版本回放使用旧本体与旧规则打开历史报告能得到当时结果,并展示版本和数据时间

画布测试可以直接围绕坐标器而不是截图。下面是建议新增的等价测试代码,用于表达验收方式,不属于历史仓库:

javascript
const result = new Coordinate({ data: fixture }, 1).init()
const nodes = [
  ...result.up.data,
  ...result.down.data,
  ...result.left.data,
  ...result.right.data
]

assert.equal(new Set(nodes.map(node => node.id)).size, nodes.length)
assert.ok(nodes.every(node => Number.isFinite(node.x) && Number.isFinite(node.y)))
assert.ok(noRectanglesOverlap(nodes))
assert.ok(childrenStayInParentDirection(nodes))
assert.ok(result.width > 0 && result.height > 0)

服务端验收还要加入守恒式:接受记录数 + 拒绝记录数 = 输入记录数;区域数量汇总要与去重企业集合相等;所有展示关系必须能找到来源记录;每个自动合并必须能生成反向撤销操作。这样才能把“关系图展示”升级为可审计的数据产品。

这个项目最有价值的遗产,不只是某张产业图,而是把产业研究拆成了“结构导航—实体连接—区域分析—内容证据—持续探查”的任务链。它也留下了同样清晰的反面经验:知识图谱能否成为决策工具,最终取决于实体和证据是否稳定,而不是页面上能画出多少种关系。

证据边界

产业链协作空间保存了一百余篇页面,覆盖 2018 年末至 2020 年初;V2 前端仓库保留 93 次提交,集中于 2019 年春夏;另有多组园区、资讯、企业和研报采集仓库。本文逐项核对了路由、领域接口、页面状态、坐标器、Canvas 交互、区域图表和部分采集/建表代码,足以确认四向产业链画布、节点探查、双模式成员统计、收藏、图片导出以及多个业务页面和前端接口契约。

本文没有找到与该前端同版本的完整服务端仓库,也没有生产图模式、统一实体解析实现、连续运行指标、正式验收清单和用户效果数据。因此,自动报告、产业预测、完整控制关系与事件图谱仍被标记为规划或待确认;接口返回的占比计算和企业分类规则也不能由前端反推。文档中的数据量只作为历史快照,不代表去重后的有效记录或生产现状。