技术架构
云族记是一个跨端前端 + Serverless 后端 + 文档型数据库的三层应用。产品上分为人际关系(族谱关系网络)与家族事务(图文纪事、收支)两大模块,本文从工程视角讲清楚:数据怎么存、关系怎么算、服务怎么拆。
技术栈一览
| 层面 | 选型 | 为什么是它 |
|---|---|---|
| 跨端框架 | uni-app(Vue 3 Composition API + Vite) | 一套代码编译到微信小程序 / App / H5,自家人在哪都能用 |
| 后端 | uniCloud 阿里云(云函数 + 云数据库) | Serverless,免运维,免费额度足够一个家族使用 |
| 数据库 | 云数据库(MongoDB 风格的文档库,JQL 访问) | 关系结构灵活,适合字段会随成员增长的族谱数据 |
| 状态管理 | Pinia(+ 本地持久化插件) | 登录态与用户资料缓存 |
| 关系画布 | Canvas 2D 全自绘 | 族谱布局/连线/命中测试高度定制,现成图库表达不了网状关系 |
| UI 组件 | uv-ui、uni-ui、TDesign 多套按需取用 | 表单/弹窗等标准组件不重复造轮子 |
整体代码约 3 万行(前端 2.7 万 + 云端 0.4 万),配套 50 个无头回归脚本守护核心算法。
三层架构
┌─────────────────────────────────────────────────────────┐
│ 前端(uni-app 编译到三端) │
│ │
│ 页面 pages/ 族谱画布(Canvas 自绘) │
│ │ │ │
│ │ 称呼引擎(纯 JS,本地实时算) │
│ │ │ │
│ └───────── 统一云调用封装 callCloud ─────────┘ │
└───────────────────────────┬─────────────────────────────┘
│ uniCloud.callFunction(带 token)
┌───────────────────────────▼─────────────────────────────┐
│ 云端(云函数,按业务域拆分) │
│ │
│ api-family 家族/成员/治理 api-tree 族谱树读取 │
│ api-identity 认领/分身 api-transfer 导入导出 │
│ api-record 家族记录 api-user 用户资料 │
│ │ 公共业务模块 yz-family-core │ │
│ │ (权限断言 / 匿名净化 / 合并 / 归档 共用) │
└───────────────────────────┬─────────────────────────────┘
│ JQL(受 schema 权限约束)
┌───────────────────────────▼─────────────────────────────┐
│ 云数据库(文档集合) │
│ 用户即成员行 / 家族 / 分身可见性 / 主分身 / 特批名单 / │
│ 归档留痕 / 导入批次 / 家族记录 … │
└─────────────────────────────────────────────────────────┘云函数按业务域拆分
最初所有逻辑挤在一个云函数里,随着功能变多,按业务域拆成了多个,并抽出一个公共模块:
| 云函数 | 职责 |
|---|---|
api-family | 家族与成员的增删改、治理、搜索、默认家族偏好 |
api-tree | 族谱树的读取、外族访问裁剪、匿名净化 |
api-identity | 真人认领、身份合并、分身匿名、主分身 |
api-transfer | CSV 导入导出、GEDCOM 转换、批次撤销 |
api-record | 家族内部的图文记录与收支 |
api-user | 成员登记、头像、账号注销 |
yz-family-core(公共模块) | 上面多个函数共用的核心:权限断言、匿名净化、认领合并、归档恢复 |
拆分的好处是每个函数职责单一、改一处不牵连其他;公共逻辑沉到 yz-family-core,避免同一段权限/合并规则在各处写出不一致的版本。
数据模型:一个人,为什么有三种 ID
这是整个系统最关键的设计。传统族谱里"一个人 = 一行数据",但现实中一个人会同时出现在多个家族:嫁过来的配偶、随母出现的外亲子女……
云族记的做法是:真人只有一行数据,但在不同家族的树上会生成不同的"呈现节点"。前端用一个叫 cloneId 的结构字段来定位每个节点,它有三种形态:
| 节点类型 | cloneId 形态 | 含义 |
|---|---|---|
| 本家血亲 | 就是本人数据行的 id | 他在自己家族的"本体" |
| 配偶分身 | 配偶id__host__宿主节点 | 他作为配偶,出现在伴侣家族的那个节点 |
| 外亲子女 | 孩子id__outsider__母亲节点 | 他随母亲(外亲)出现在另一个家族的节点 |
这样设计带来的好处:
- 数据不重复:一个真人无论出现在几个家族,资料只存一份,改一处处处生效。
- 结构可表达:每个"分身"都是树上一个可定位、可连线、可算称呼的节点。
- 隐私可控制:可以单独把某个家族里的分身匿名掉,而不影响他在自己家族的实名身份。
数据行里存的始终是真人之间的原始关系(父 id、母 id、配偶 id 数组);cloneId 这类结构字段只在读取族谱树时实时组装,不落库。这样原始数据保持干净,展示层怎么变都不污染事实源。
为什么不用图数据库
族谱听起来很像"图",但最终没有引入图数据库(如 Neo4j),原因是:
- 父系血亲部分本质是一棵严格的树——每个人有确定的父、母,辈分、排行沿树推导,路径唯一,文档库 + 树组装就够了。
- 真正"网状"的只有配偶和外亲这些挂接点,数量少、规则明确,用约定好的分身结构表达,比引入一整套图查询更可控。
- Serverless 场景下图数据库运维和成本都不划算,而文档库是 uniCloud 自带的,零额外基础设施。
换句话说:用结构化的约定,把"图"的复杂度收敛到了少数几类连边上,而不是为了这部分复杂度背上一个图数据库。
工程保障
- 无头回归测试:50 个 Node 脚本覆盖称呼样例、布局方向、路径 id、匿名净化、特批名单、导入组装等,一条命令全跑,核心算法改动有兜底。
- 数据真相唯一:服务端数据库行是唯一事实源,前端缓存只做展示加速,绝不反向定义关系语义。
- 危险操作可逆:删除、认领合并、批量导入都写归档快照,而不是物理删除,详见归档可逆设计。
下一步可以读:
