| 档案 | |
|---|---|
module |
contacts |
summary |
联系人:好友是私聊 / 通话 / 信标的授权关系(私聊闸对 bot 账号放行、平台分身接主人直接建好友这两处例外;陌生人闸和动态 network 档另看六度),分类不是标签而是羁绊握手推导出来的;通讯录只上传手机号的无盐 sha256、服务器只比对不落库;六度没有度数表,全靠好友边现跑 BFS(四份每层后 > 5000 才停、两份只限层数;无缓存);介绍卡每次推进一站、拒绝一次整卡作废、走完不自动成为好友;动态的分类与排除名单「跟着羁绊走、发布时定格」,但只在部分读路径生效(单条 canView 仅 friends 分支查;流里按各自 SQL);状态只到一度、24 小时后不可读(清库待核)。 |
updated |
2026-09-08 |
verified_ref |
95b0676ac542e03532d87c9f7453126e808449ad |
source_ref |
[backend/internal/domain/friend, backend/internal/domain/invite, backend/internal/domain/bond, backend/internal/domain/beacon, backend/internal/domain/post, backend/internal/domain/status, backend/internal/domain/user(搜索), app/lib/screens/friends_screen.dart, app/lib/screens/contacts_priming_screen.dart, app/lib/screens/sixlogy_*.dart, app/lib/screens/dunbar_rings_screen.dart, app/lib/screens/bond_screen.dart, app/lib/screens/intro_letter_screen.dart, app/lib/screens/my_world_screen.dart, app/lib/data/friends_feed.dart, app/lib/data/six_network.dart, app/lib/widgets/ignite_sheet.dart, app/lib/services/deep_links.dart] |
status |
current |
review_after |
2026-12-07 |
owner |
nova |
supersedes |
[CONTACTS_SIXLOGY.md, CONTACTS_INVITE_STRATEGY.md] |
partially_supersedes |
[BOND.md(握手 / 配对表 / 温度三节;共享空间归城堡线), sixlogy_play.html(三根轴 / 度数路径 / 引荐→介绍卡 / 两条红线 / 性能判据;挑战与星图归 growth), FIRST_HOUR.md(§3 通讯录匹配两行), ONBOARDING_TUTORIAL.md(§3.2 第 5 条 hint_contacts_empty)] |
这份怎么读:每一节先讲「它解决什么、用户看到什么」,再讲「为什么这样做、数据在哪、哪一步会断、断了留下什么、有什么保障和缺口」;代码位置、参数、行号收在每段末尾的「出处」里。 出处标记:
[code@95b0676a 文件:行]= 那棵树上的实现;[prod@日期,谁核]= 生产机当天核过;[decision@日期,谁]= 拍板过的决定。代码证明「那一版怎么做的」,证明不了「线上是这样」和「应该是这样」。 边界:群 / 社群的邀请链接归 community;温度(warmth)的数值本体归 castle;新人激活策略归 growth;这里只写「人和人之间的关系」本身。
0. 给接手的人:三分钟读懂联系人
先认五个词:好友 = 双方都点过头的一条关系,私聊、通话、信标的授权都看它(只有私聊闸对 bot 账号例外);六度另有两处也当授权用 —— 陌生人闸(六度内才能向关了陌生人申请的人发请求)和动态 network 档的可达集;羁绊(bond) = 好友之上再各自声明一个角色(爸爸 / 死党 / 同事…),配成一对才成立,成立后决定这个人进哪个分类、加多少温度;六度(sixlogy) = 从我出发沿好友边走几步能到他,没有存表,每次现算;登巴环 = 我的 150 人按温度分四环的那张图,外面再长出二到六度;介绍卡 = 想认识一个不是好友的人,写一封信沿六度那条线一站一站传,每站可以帮或不帮。
它保证到什么程度:
1. 通讯录不出手机:手机只把号码做 sha256(无盐)后的 hex 送上去,服务器现算自己库里号码的 hash 比对,不落任何表、用完即丢;被你 vault 进暗影的人整条从匹配结果里消失。
2. 普通用户之间没人能自动成为你的好友:通讯录命中也只是「可加」,要发请求、对方接受。两处例外:平台分身(你的 AI 助手)接到主人时直接写双向好友、不经申请;人和 bot 账号之间的私聊闸双向放行(只这一道,通话和信标照旧看好友)。陌生人默认能发请求;关了「接受陌生人」的人,只有六度内的才能发 —— 三道检查(拉黑、设置、六度)按顺序判断,每一道只在查得到时拦:某道查询报错,该道不拒绝、继续后面的检查和写库,所以存在绕过那一道的可能;走到写库也可能失败,不保证发成功(§1.1 第 5 步)。
3. 分类没有手工标签:五个抽屉(家人 / 死党 / 同学 / 同事 / 圈子)全部由羁绊握手推导;没握手的人自成「Friends」一档。这是 Jeff 08-05 定的三判据之一。
4. 分类与排除名单跟着羁绊走、发布时定格,但生效范围要看读路径:单条读取只在 friends 分支查分类与排除,network 拿到 id 就能看;流里 Feed / ByUser 只取 friends 档并套分类与排除,ByUserVisibility 按传入档位取、分类与排除无条件套用(§3.4);温度这类会漂移的信号只展示、永不授权(Jeff 砍掉了「只给核心圈看」)。
当前明确没做的(§4 全表,先说三条):六度 BFS 每次现算没有缓存,量大会慢;hoop://invite 深链没人接,邀请落地页指的是官网首页;介绍卡走完 accepted 后不会自动成为好友,怎么衔接好友申请本文没找到(老的点火 ignite 只有一跳、不接力;介绍卡是每同意一次推进一站的接力)。
出了问题先查哪:§5 一条路 —— 「加不了好友」先看是拉黑、陌生人闸还是 pending 撞键;「看不到他的动态」先看 audience_category 和 post_excludes;「六度里没有他」先看是哪一份 BFS、有没有在 5000 那道阈值后停掉(§3.3 表)。
1. 两段关系的一生
1.1 从通讯录到好友:授权 → 哈希上传 → 比对 → 发请求 → 接受
你看到的:第一次登录先看到一页「为什么要通讯录、不拿什么」,点允许才弹系统框(跳过的人落在聊天页,允许的人落在联系人页,这页一辈子只出现一次)。联系人页里,通讯录命中的 HOOP 用户排在前面、可以一键加;没命中的可以发短信 / WhatsApp 邀请;发了请求的三天后可以「敲一下」催;对方接受后两人互为好友,也互相关注。
背后发生的:
1. 权限静默查、永不裸弹:进联系人页时查 flutter_contacts 的权限;已授权直接同步,没决定过显示预热卡,拒绝过显示「打开设置」卡。
2. 手机侧哈希:每个号码剥掉非数字、少于 7 位丢弃;0 开头的再补一条 60 + 去首零的马来西亚写法;每个变体各算一条 sha256 hex,本机保留 hash → 姓名的映射,只把 hash 列表上传。为什么不加盐:服务器要用同一算法现算自己库里的号码来比,盐会让两边对不上(实现推断;没查到决定记录)。代价是彩虹表可逆,见 §4。
3. 服务器只比对:收 hashes[],超过 5000 条截断;对每个 hash 现算 sha256(users.phone) 比对,只匹配手机号,邮箱哈希没有;结果回五种关系(shadow / friend / outgoing / incoming / none)+ 能不能敲门(未敲过且满 72 小时);不落任何表。
4. 手机侧收拾:shadow 的整条删掉(既不显示也不进邀请);未命中的算「可邀请」;按 none → incoming → outgoing → 其余排序。
5. 发请求:先查拉黑互查;再查陌生人闸 —— 对方关了「接受陌生人请求」且我不在他六度内 → 403 stranger_blocked。三道检查按顺序判断,每一道只在查得到时拦:某道查询报错,该道不拒绝、继续后面的检查,最后走到写库(拉黑查询报错后,陌生人设置和六度仍可能正常返回并拒绝);写库 SendRequest 也可能失败。所以存在绕过某一道限制的可能,也不保证请求发成功 —— 这是现状,不是设计承诺。已是好友静默幂等返回空;撞上 pending 唯一键就返回已存在那条。通讯录命中的人加之前先弹一句可改的招呼语。
6. 接受:一个事务里改状态、双向写 friendships、双向写 follows。删好友只删 friendships,不撤 follows(故意的)。
7. 同步到屏上:好友表在本机 Hive friends 盒,唯一取法是 friends_feed(先给缓存那帧、再拉后端整表替换);WS 收到 friend.* 事件或重连就重拉,没有轮询;角标 = 好友申请数 + 待确认羁绊数。
哪一步会断、留下什么:第 1 步拒绝 = 什么都不传,只能手动加;第 3 步截断 = 第 5001 条起的号码永远匹配不到,而且没有提示;第 5 步 403 = 手机上「对方不接受陌生人」,第 5 步某道查询报错 = 那一道不拦、后面照常,写库失败 = 没发出去;第 6 步事务失败 = 关系不变,两边都还是 pending;第 7 步 WS 断 = 屏上陈,重连重拉。
出处:预热页与首次路由
[code@95b0676a app/lib/screens/contacts_priming_screen.dart:14-16,29-40,59-119]、[code@95b0676a app/lib/screens/login_screen.dart:275-277];权限静默查[code@95b0676a app/lib/screens/friends_screen.dart:269,292-310,1405,1441];号码变体与哈希:434-442,只传 hash:483-489,shadow 删掉:495,排序:497-502,重跑时机:522,716;上传接口[code@95b0676a app/lib/api/api_client.dart:1556-1564];服务器截断 5000[code@95b0676a backend/internal/domain/friend/handler.go:308-314],现算 sha256 只比手机、不落库[code@95b0676a backend/internal/domain/friend/repository.go:212-217,231],五种关系与 72 小时:247-262;发请求幂等与撞键:82-123,拉黑互查handler.go:217-220,陌生人闸:221-227(三处都err == nil才拦)+repository.go:26-35,38-74;bot 双向放行[code@95b0676a backend/internal/domain/message/service.go:267-288],平台分身直插双向好友[code@95b0676a backend/internal/domain/provision/repository.go:371-382];接受双向写:282-315(follows:304-313),删除不撤 follows:280-282,501-506,敲门 3 天:163-169;手机发 / 接 / 催 / 撤[code@95b0676a app/lib/api/api_client.dart:1302-1316,1566-1572],页面friends_screen.dart:519-529,653,671,713;好友盒与唯一取法[code@95b0676a app/lib/data/friends_feed.dart:5-22,31,44]、local_store.dart:38,777,780;WS 触发重拉friends_screen.dart:271-283,角标:401-402。
1.2 一张介绍卡:想认识朋友的朋友的朋友
你看到的:在「我的世界」里挑一个二度以上的人,写一封信;屏上先画一条线「我 → 经过谁 → 他」(Jeff:那条线很重要);信从第一站开始传,每一站的人在自己的「我的世界」收件箱看到橙条,可以写一两句话「帮他传」或「这次不帮」;到终点的人同意,你们就能认识。反过来,别人也能通过你找关系 —— 同一个收件箱。
背后发生的:
1. 链由后端算:手机只说「我要认识谁」,服务器沿好友边 BFS(最深 6)算出一条路;没路 → 404 no_path。留言截断 200 字。
2. 一次只推一站:建卡时只有第一站 pending、其余 waiting;只有球在他脚下的人才动得了(不是他的回合 → ErrNotYourTurn);卡已了结再表态 → ErrCardSettled。
3. 拒绝 = 整张卡作废;同意 = 把下一站从 waiting 推成 pending(这就是接力,每次只推进一站),没有下一站就整卡 accepted。accepted 只改卡的状态,不写 friendships,接口也只是读卡返回 —— 走完不会自动成为好友,之后怎么衔接好友申请 / 私聊本文没找到(§7 待核)。
4. 双向:同一个接口既给发起者也给路上的人;GET /v1/intro-cards 回两组 —— 等我表态的、我发起的(各最多 50)。
5. 和老的「点火」的关系:ignite(迁移 000080)是一跳:只对二度成立,三度以上不给按钮只给那条链,后端不接力;介绍卡(000304)是整条路,每同意一次推进一站。手机端「寻找关系」那条竖线写着「同意了自动传下一个」,对点火是画的,对介绍卡是真的。
哪一步会断:没路 = 404,用户看到「找不到路」;中间任何一站拒绝 = 整卡 declined,发起者收件箱看到;中间人不表态 = 卡一直 pending;introcard.go 和迁移 000304 里没有过期字段或过期逻辑(全仓别处未核,§7)。
出处:表
[code@95b0676a backend/migrations/000304_intro_cards.up.sql:14-54];链 BFS[code@95b0676a backend/internal/domain/friend/introcard.go:261-311],无路 404[code@95b0676a backend/internal/domain/friend/introcard_handler.go:52-57],留言 200 字:60-62;第一站 pendingintrocard.go:87-99;表态:137-204(已了结:152-154,不是你的回合:159-164,拒绝整卡:179-187,同意推下一站 waiting → pending:190-202),接口 accepted 后只读卡返回、不建好友introcard_handler.go:87-122,双向:133-136,两组收件箱:209,233;路由introcard_handler.go:12-21,忘注册不报错的守卫introcard_test.go:27;手机接口[code@95b0676a app/lib/api/api_client.dart:1462-1509],写信页[code@95b0676a app/lib/screens/intro_letter_screen.dart:78-86,115,144,402,425],收件箱[code@95b0676a app/lib/screens/my_world_screen.dart:427-457,600-607,646];点火只一跳[code@95b0676a app/lib/widgets/ignite_sheet.dart:51-55,60]、[code@95b0676a app/lib/screens/relation_chain_screen.dart:16-22][decision@2026-08-27,Jeff 语音:介绍卡沿六度走、每站可写一两句、双向]。
2. 用户能看到的能力(怎么用 / 限制 / 入口)
| 能力 | 怎么用 / 限制 | 后端入口 | App 落点 |
|---|---|---|---|
| 通讯录匹配 | 首次登录预热页;只传号码 hash;≤ 5000 条;暗影的人不出现 | POST /v1/contacts/match |
contacts_priming_screen、friends_screen |
| 搜人 | 手机号 / 邮箱 / @用户名精确匹配,默认 20 上限 50 | GET /v1/users/search |
add_friend_screen |
| 加好友 / 接受 / 拒绝 / 撤回 / 敲门 | 拉黑互查;陌生人闸;3 天后可敲一次 | /v1/friends/requests* |
friends_screen、new_contact_screen |
| 删好友 / 拉黑 | 在私聊设置里,不在联系人页;拉黑单独有黑名单页 | DELETE /v1/friends/{id}、/v1/blocks |
direct_chat_settings_screen、blocked_users_screen |
| 备注名 | 只自己看 | PATCH /v1/friends/{id}/remark |
new_contact_screen |
| 分类胶囊 | 家人 / 死党 / 同学 / 同事 / 圈子 + Friends;有人才显示;分类消失自动回 all | 由 bonds.category 推导 |
CategoryChips |
| 羁绊 | 只能从头像进;各自声明角色,配对才握手;自定义角色不加温度;解除是隐藏 | /v1/bonds/{uid}、GET /v1/bond/roles |
bond_screen |
| 六度主屏 | 宇宙 / 照片墙 / 数据 / 距离 / 圆桌 / 信标六个 tab | /v1/sixlogy* |
sixlogy_screen(唯一开法 openSixlogy()) |
| 登巴环 | 我的 150 四环;按一下长出 2..6 度;脸最多 16 张 | /v1/sixlogy/people |
dunbar_rings_screen |
| 我的世界 | Jeff 亲手交的关系宇宙;介绍卡收件箱在这 | — | my_world_screen、relationship_universe |
| 点火 | 只对二度;挑和我最热的那位牵线;一跳 | /v1/ignite* |
ignite_sheet |
| 介绍卡 | 写信沿六度走,每同意一次推进一站;拒绝整卡作废;走完不自动成为好友 | /v1/intro-cards* |
intro_letter_screen |
| 信标接力 | 点亮给一个非好友,每手只能传给真好友,不能成环,只有持有人能转 | /v1/beacon* |
beacon_screen |
| 动态 | 唯一入口开相册 → 配文 → 可见范围(自己 / 好友 / 某抽屉 / 六度网);分类与排除在哪些读路径生效见 §3.4;改范围不能变宽;转发不能比原帖宽 | /v1/posts*、PATCH …/visibility |
moment_compose_screen、post_sheets |
| 状态 | 24 小时后读不到(清库待核);只有一度好友看得到;谁看过只作者可见;≤ 50 MB | /v1/status* |
— |
| 邀请码(Gmail 式) | 一码一人、默认 3 个名额、等候名单;总闸默认关 | /v1/invites*、/v1/waitlist |
my_invites_screen、invite_gate_sheet |
| 邀请没上 HOOP 的人 | 短信 / WhatsApp / 微信 / 复制,落地页 = 官网首页 | — | friends_screen _invite |
| 群 / 社群邀请链接 | 归 community;这里只有共用页 | 000239 / 000288 | invite_link_screen |
出处:路由
[code@95b0676a backend/internal/domain/friend/handler.go:43-66]、[code@95b0676a backend/internal/domain/invite/handler.go:29-42]、[code@95b0676a backend/internal/domain/bond/handler.go:20-27]、[code@95b0676a backend/internal/domain/beacon/handler.go:24-31]、[code@95b0676a backend/internal/domain/status/handler.go:24-29]、[code@95b0676a backend/internal/domain/post/handler.go:32-100](六度:85-91)、搜人[code@95b0676a backend/internal/domain/user/handler.go:40-41](service.go:103-117);删好友 / 拉黑在私聊设置[code@95b0676a app/lib/screens/direct_chat_settings_screen.dart:204-217,261-274];六度唯一开法[code@95b0676a app/lib/screens/sixlogy_screen.dart:34,230-243];登巴环[code@95b0676a app/lib/screens/dunbar_rings_screen.dart:21-25,76,85];发帖唯一入口[code@95b0676a app/lib/screens/moment_compose_screen.dart:27-29,38]。
3. 内部怎么运作
3.1 好友是私聊 / 通话 / 信标的授权关系;六度用于陌生人闸与可达范围
用户看到的:不是好友发不了私聊、打不了电话、传不了信标;群通话拉人也看好友。
怎么做的:friendships 一张表存双向两行;私聊建会话前查、1v1 通话 AuthorizeCall 查、群通话 / 临机房注入 AreFriends、信标每一手只能传给真好友。删好友只删 friendships 不撤 follows(关注和好友是两件事,故意分开)。例外两处:私聊闸 directNotFriend 对 bot 账号双向放行(发起方或对方是 bot 都不查好友;只这一道,通话 AuthorizeCall 和信标没有这个例外);平台分身接到主人时用 befriendSQL 直接插双向 friendships,不走申请。六度也当授权用的两处:陌生人闸(对方关了陌生人申请时,六度内才能发)和动态 network 档的可达集(流里筛)。为什么通话 / 私聊只认好友:Jeff 06-29 定的底线「陌生人和被拉黑者不能呼叫」。
出处:表
[code@95b0676a backend/migrations/000001_init.up.sql:165-199];私聊闸[code@95b0676a backend/internal/domain/message/service.go:284]+[code@95b0676a backend/internal/domain/conversation/repository.go:1570-1577];通话message/service.go:3047-3064;群通话[code@95b0676a backend/internal/domain/calls/group.go:237-241];信标beacon/handler.go:52-55,89-92;不撤 followsfriend/repository.go:280-282;bot 放行message/service.go:267-288;平台分身直插provision/repository.go:371-382;陌生人闸用六度friend/handler.go:221-227。
3.2 分类 = 羁绊握手推导,零手工标签
用户看到的:联系人页顶上的胶囊(家人 / 死党 / 同学 / 同事 / 圈子 / Friends),不能手动拖人进抽屉;和某人在羁绊页配成「爸爸 ↔ 女儿」,他自动进「家人」,温度 +300。
怎么做的:bonds 一行存一对(pair_key = min:max,role_a / role_b);发起方写自己那半 → pending;确认方写另一半,服务器校验两个角色互补(pairs()),不匹配 ErrMismatch 且不改库 → active;解除 = hidden 不删,重新握手能回来。角色 → 抽屉只有一个出处(角色表那行的 Cat),握手时落进 bonds.category;好友列表一趟 SQL LEFT JOIN bonds + conversation_warmth 把 bond_role / bond_category / warmth 带出来。温度加成:亲人 13 个 + 挚爱 3 个角色 +300、挚友 +100,握手成功后调 warmth 域加;自定义角色一律 0 分且整对 0 —— 否则「custom:战友 ↔ 爸爸」就能白拿 +300(model 里写明了)。亲人和挚爱同是 +300 档所以并成一个 family 抽屉(Jeff 08-05 拍板)。手机端角色目录从后端拉,故意不内置兜底清单。
改它之前:加角色要同时给归属抽屉(测试逼);自定义永远 0 分(测试逼);$1 在 SQL 里每处显式 cast,漏了当场 42883(2026-08-05 生产炸过好友列表)。
出处:表
[code@95b0676a backend/migrations/000040_bonds.up.sql:2-12],category 列[code@95b0676a backend/migrations/000191_post_audience.up.sql:8];握手[code@95b0676a backend/internal/domain/bond/service.go:30-35,76-160](mismatch:142-144,加温:153-158),解除:162-178;角色与加成[code@95b0676a backend/internal/domain/bond/model.go:29-30,42-51,56-74,118,186-197,217-222];落 category[code@95b0676a backend/internal/domain/bond/repository.go:49-55];好友列表 JOIN[code@95b0676a backend/internal/domain/friend/repository.go:414-447,479](cast 红线:427-428,列序:460-462),模型friend/model.go:31-44;手机胶囊[code@95b0676a app/lib/screens/friends_screen.dart:2097-2172],角色目录不内置[code@95b0676a app/lib/data/bond_catalog.dart:19-20,41,59],羁绊页[code@95b0676a app/lib/screens/bond_screen.dart:66,110,705,790,808,858-952];守卫bond/model_test.go:12-183[decision@2026-08-05,Jeff:三判据 / 亲人挚爱并成 family][decision@2026-08-27,李敏:羁绊页 ❤️→🔗、高亮互认、加回改关系(v1.294)]。
3.3 六度:没有度数表,全靠好友边现算
用户看到的:六度主屏六个 tab;登巴环按一下长出二到六度;二度以上的人优先显示他设的「分身脸」,没设就还是基础昵称 / 头像;温度只在一度有值。
怎么做的:没有任何度数 / 环的表,所有「几度」都是从 friendships 现跑 BFS;六个地方各有一份,参数不一样:
| 函数 | 深度 | 节点阈值 | 检查时机 | 暗影过滤 |
|---|---|---|---|---|
WithinSixDegrees(陌生人闸) |
6 | visited > 5000 停 | 每层跑完后查,所以可超 5000 | 未见(待核) |
ReachableSet(动态可达集) |
6 | visited > 5000 停 | 同上 | 只在第一跳 NOT is_shadowed,后续层不排除暗影用户 |
reachableDegrees(度数) |
6 | visited > maxNodes 停 | 同上 | 同上,只第一跳 |
PathPeople(路径) |
6 | parent > 5000 停 | 同上 | 函数本身不查暗影;调用方 Path 先查 IsShadowed,命中就传 skipDirect=true,函数只抹 from → to 那条直连边、仍可经别人到达;查询失败 skip 保持 false |
IntroChain(介绍卡链) |
maxDepth(调用传 6) | 无 | 只限层数 | 未见(待核) |
beacon.Shortest(信标最短路) |
6 | 无 | 只限层数;返回 0 = 够不到或数据库错误 | 未见(待核) |
没有缓存:代码里明写「量大时可缓存 Redis,MVP 先现算」—— 这是本模组唯一明写的欠账。二度以上看到什么:广场 scope=network 只放六度内、public 放六度外;照片墙上限 300 人;脸按度数换 —— 一度用基础资料,二度以上优先 personas(scope='network') 的分身脸,分身昵称空回基础昵称、分身头像空回基础头像地址(两项各自回退,所以「二度以上看不到真头像」不是保证);温度只一度有值、二度以上恒 0;FriendsOf 会露出目标的全部好友,但目标必须在我六度内否则 403(Jeff 知情后选的);中心度排名只在全站 2..200 人时才算。统计接口回 friends / second_degree / active_30 / total_users / total_links(长度 / 宽度 / 深度三根轴)。
手机侧:数据唯一取处 six_network(内存缓存 + 可取消退避重试最多 3 次);星系按最近联系天数定轨道和亮度、五档色环;登巴环脸最多 16 张、其余撒密度点;关系宇宙用 Ticker 替代 AnimationController.repeat 消掉 36 秒一次的角度跳变,整体 RepaintBoundary。两条红线(sixlogy_play,仍有效):路径暴露的是别人的关系,默认只给度数不给名字;不做人脉排行榜(一排名就会去刷握手,三根轴同时脏)。
出处:统计
[code@95b0676a backend/internal/domain/friend/repository.go:176-210];六份 BFS(表里的行号)friend/repository.go:38-74、[code@95b0676a backend/internal/domain/post/repository.go:417-450](第一跳暗影:428-429)、Path传 skipDirect[code@95b0676a backend/internal/domain/post/service.go:753-758]、只抹直连边post/sixlogy.go:240-241、[code@95b0676a backend/internal/domain/post/sixlogy.go:160-197,221-287]、friend/introcard.go:261-311、[code@95b0676a backend/internal/domain/beacon/repository.go:56-92];无缓存post/repository.go:418;广场[code@95b0676a backend/internal/domain/post/service.go:230-241],300 人:709-711,FriendsOf403:716-741+sixlogy.go:338-350;分身脸与回退sixlogy.go:289-336,温度只一度:16,88-92,中心度:446-504;手机[code@95b0676a app/lib/data/six_network.dart:14-25,52,60-64],星系[code@95b0676a app/lib/screens/sixlogy_universe.dart:41-53,338,358,657,834-838,1623],登巴环dunbar_rings_screen.dart:21-25,108,113,关系宇宙[code@95b0676a app/lib/widgets/relationship_universe.dart:14-16,75-85,122-123,186,685](Jeff 08-24 亲手交的,结构参数别按自己口味改);接口api_client.dart:1323-1341,1399,1414,1454;红线sixlogy_play.html ⑦[decision@2026-08-05,Jeff:温度只展示不授权]。
3.4 可见范围:跟着羁绊走,排除名单发布时定格
用户看到的:发动态时选给谁看:自己 / 好友 / 只给某个抽屉 / 六度网;已发的可以改范围但不能变宽;转发不能比原帖宽;换档会先提示「会清掉分类限制」。
怎么做的:posts.visibility(private / friends / network)+ posts.audience_category + post_excludes(发布那一刻定格)。读有两层,别混:① 流里筛(Feed / ByUser / ByUserVisibility)按档位 + 分类 + 排除名单筛;② 拿到单条能不能看(canView):作者自己 → 能;network 档 → 直接能,不查六度、不查分类、不查排除;friends 档 → 查好友、查暗影、再 AudienceAllows(分类 + 排除);private → 不能。流的真实条件各自不同,别外推:Feed 取 friends 档中自己的帖,以及非暗影好友的帖(作者是自己 OR 作者是好友且非暗影);分类谓词豁免作者本人,排除名单的 NOT EXISTS 仍按 SQL 检查;ByUser 只取 friends 档,分类同样豁免作者本人,排除仍查;ByUserVisibility 按传入的档位取,分类(豁免作者本人)+ 排除无条件套用(不限 friends)。这三条说的是仓储 SQL,上层调用权限不由此外推;单条 canView 只有 friends 分支查分类 + 排除,network 直接能。写入时各档位会不会把分类 / 排除字段清空是另一层,未核(§7)。@ 提及只有看得见这条帖的人才收到,非好友的 @ 发布时就丢掉;原声页(BySong)故意不做六度过滤、只放 network 档。分页硬编码 21 / 11 / 21。状态是另一套:expires_at 默认 now()+24h,Feed / 媒体 / 作者查询都按 expires_at > now() 过滤,所以 24 小时后读不到;到点有没有物理删行、删对象,status 包里只看到用户主动删除,清理链待核。只到一度(Feed = 我 + 直连好友),谁看过只作者可读。手机侧 feed 只缓存第一页 20 条、整页替换(有意的,和会话列表的墓碑法不同)。
出处:迁移
[code@95b0676a backend/migrations/000191_post_audience.up.sql:21-31]、[code@95b0676a backend/migrations/000022_posts.up.sql:3-13];流的 SQL:Feedpost/repository.go:139-152(自己 OR 好友非暗影:140-143,分类豁免作者:146,排除:152),ByUser:462-473(分类豁免作者:469),ByUserVisibility:485-499(分类豁免作者:495,分类 + 排除无条件),单条canView按档位post/service.go:397-425(network 直接 true:406-407,friends 查好友 / 暗影 /AudienceAllows:408-421),AudienceAllowspost/repository.go:1292-1304;分页post/service.go:221,236,250;BySongpost/repository.go:369-397(选择service.go:278-280);状态[code@95b0676a backend/migrations/000010_status.up.sql:12-22]、[code@95b0676a backend/internal/domain/status/service.go:62-68,97-111,149-157],按expires_at过滤[code@95b0676a backend/internal/domain/status/repository.go:35-43,66-84],只有主动删除:111-113,50 MBstatus/handler.go:21,34;手机三档moment_compose_screen.dart:27-29,355-383,改范围[code@95b0676a app/lib/widgets/post_sheets.dart:113,1555,1620-1639,1720],feed 缓存[code@95b0676a app/lib/data/local_store.dart:687-711];守卫post/visibility_db_test.go:56,131、mention_db_test.go:122,190、bysong_db_test.go:52、repost_db_test.go:267[decision@2026-08-05,Jeff:可见范围跟羁绊 / 排除定格 / 砍掉「只给核心圈看」]。
3.5 邀请:两套不要混
用户看到的:① 「我的邀请」里有名额和邀请码(Gmail 式,默认 3 个,总闸没开时用不上);② 通讯录里没上 HOOP 的人可以短信 / WhatsApp 邀,落地页是官网首页;③ 群 / 社群的邀请链接是另一回事(community)。
怎么做的:邀请码 HOOP-XXXX-XXXX(字母表避开 0 / O / 1 / I / L),配额 users.invite_quota 覆盖默认;Reserve 原子核销、建号后 AttachUser 补 used_by —— issuer_user_id + used_by 就是邀请树;注册 / 登录时把码随请求带上。总闸 gate_enabled 默认关,读设置失败一律放行(fail-open,故意:宁可漏进一个人);等候名单按 IP 限 24 小时 20 次(本模组唯一的 Redis 键)。旧提案里的 invite_links / contacts_imports / discoverable_by_phone / group_invites 全仓零命中,没做。
出处:码
[code@95b0676a backend/internal/domain/invite/service.go:24-41,52-75,78-94],配额[code@95b0676a backend/internal/domain/invite/repository.go:102-109]+[code@95b0676a backend/migrations/000190_invite_gate.up.sql:4,9-39],核销 / 归因repository.go:140-173,fail-openservice.go:99-110,等候名单与限频:135-151;手机[code@95b0676a app/lib/screens/my_invites_screen.dart:63,86]、[code@95b0676a app/lib/widgets/invite_gate_sheet.dart:65],归因[code@95b0676a app/lib/screens/login_screen.dart:115,199,232,257];通讯录邀请friends_screen.dart:66,636,742,753;守卫:invite 包没有测试。
3.6 信标:米尔格拉姆实验
用户看到的:点亮一个信标给一个不是好友的人,只能传给真好友,每一手上链,看它几手到达;全站平均手数。
怎么做的:目标不能是我的直连好友;每手只能传给真好友;不能传给已在链上的人(防成环);只有当前持有人能转;创建时记 BFS 理论最短(6 跳内,够不到 = 0)。证据范围:后端、迁移、页面都在;2026-08-20 之后 CHANGELOG 没有记录 —— 这只说明「没写进日志」,是不是停更本文不下结论,待 Jeff 定(§4)。
出处:
[code@95b0676a backend/migrations/000120_beacon.up.sql:4-26];规则[code@95b0676a backend/internal/domain/beacon/handler.go:47-55,83-103],最短路beacon/repository.go:1-3,56-92,统计:319-323;手机[code@95b0676a app/lib/screens/beacon_screen.dart:268,287];守卫:beacon 包没有测试。
4. 还没做的(不是现状)
| 项 | 状态 | 出处 |
|---|---|---|
| 六度 BFS 结果缓存 | 未做,每次现算(阈值见 §3.3 表) | post/repository.go:418 |
| 通讯录 hash 加盐 / 邮箱匹配 | 未做(无盐 sha256、只匹配手机) | friend/repository.go:217,231 |
| 5000 条截断没有提示 | 现状,用户不知道 | friend/handler.go:312-314 |
hoop://invite 深链 |
无人接(只分叉 call / group / community);邀请落地页 = 官网首页 | deep_links.dart:5,111-113;friends_screen.dart:61-66 |
| 介绍卡过期 / 走完后衔接好友 | introcard.go 与 000304 无过期字段与逻辑(别处未核);accepted 不建好友,衔接未找到 |
introcard.go:190-202、introcard_handler.go:87-122 |
| 点火(ignite)接力 | 后端只一跳,不接力(介绍卡才接力);RelationChainScreen 只在测试里被引用 |
relation_chain_screen.dart:16-22;ignite_sheet.dart:51-55 |
| 羁绊共享空间(相册 / 文档 / 暗号 + 4 位密码 / Face ID) | 未开工 | BOND.md §四;bond/ 零命中 |
邀请提案里的 pending circle / group_invites / discoverable_by_phone / contacts_imports |
未开工,全仓零命中 | CONTACTS_INVITE_STRATEGY §4.7 / §6 |
| Discover 新人种子流 | 标最高优先,08-20 后零动静 | FIRST_HOUR §4 |
| 信标去留 | 08-20 后 CHANGELOG 无记录(不等于停更),待 Jeff 定 | CHANGELOG |
MomentComposeScreen 类无处 push、MomentsScreen 无构造点 |
疑似断路,要不要删得问 Jeff | moment_compose_screen.dart:36-37 |
| 状态到期后的物理清除(删行 / 删对象) | 待核,status 包只见按 expires_at 过滤和主动删除 | status/repository.go:35-113 |
| 好友申请三道闸:某道查询报错时该道不拦、继续后续检查与写库 | 现状(err == nil 才拦),要不要 fail-closed 待拍 |
friend/handler.go:217-231 |
| invite / beacon / status 三个包的测试 | 没有 | — |
| 社群加入 202 待审批态 | 后端没做(P2),非 open 社群吃 403 | invite_link_screen.dart:26-36(归 community) |
| 待 Jeff 拍(sixlogy_play ⑩):路径显名默认、引荐入口摆哪、挑战用不用绿能量、度数显示到几度 | 未决 | sixlogy_play.html ⑩ |
AI_SCORING 温度引擎与 conversation_warmth 两套 |
未接通(它今天服务假面舞会) | AI_SCORING §5 / §8 |
5. 出问题先查哪里 · 动它之前
排障一条路:
1. 加不了好友:看返回 —— 403 stranger_blocked = 对方关了陌生人且我不在他六度内;403 blocked = 拉黑;空串 = 已是好友(幂等);拿到旧 id = pending 撞键。反过来「不该发出去却发出去了」先看那三道查询当时有没有报错(该项报错不拦,后续检查与写库仍可能失败)。
2. 通讯录匹配不到:先看手机侧号码变体(剥非数字、0 → 60),再看是不是第 5001 条之后;服务器只比 users.phone,对方用邮箱注册就永远匹配不到;对方在我暗影里会整条消失。
3. 看不到他的动态:先分是流里没出现还是单条打不开 —— 单条 network 拿到 id 就能看;单条 friends 看好友、暗影、audience_category 对不对上我的 bonds.category、post_excludes 有没有我;流里按 §3.4 那条路径的 SQL 查(ByUserVisibility 任何档位都套分类与排除);原声页不看六度。
4. 六度里没有他:先看是哪一份 BFS(§3.3 表:四份有 5000 阈值且层后才查,两份只限层数);信标最短路 0 可能是数据库错误;FriendsOf 403 = 目标不在我六度内。
5. 温度不对:握手是不是 active、角色是不是自定义(恒 0)、亲人 / 挚爱 +300 只在握手那一刻加一次;数值本体在 warmth 域(castle)。
改它之前:
- 改好友 SQL:$1 每处显式 cast;Scan 列序和 SELECT 一字不差(错位是静默的)。
- 加角色:归属抽屉必填;自定义永远 0 分(bond/model_test.go 逼)。
- 加介绍卡 / 好友端点:必须写进 Register,忘了 go build 不报错(introcard_test.go:27 逼)。
- 动可见范围:四条读路径一起改;改窄不改宽;转发继承原帖。
- 动六度:六份 BFS 参数各不同(§3.3 表),改阈值别只改一份;阈值是层后才查,别把它当硬上限;别加排行榜。
- 动关系宇宙:结构参数(四圈半径 / 转速 / 呼吸 / 粒子)是 Jeff 亲手给的,别改。
- 别信的旧图纸:CONTACTS_INVITE_STRATEGY 的 checkbox 全空且命名撞车;BOND §五 羁绊页 UI 已被 v1.294 改;sixlogy_play 的 /v1/social/* 当天撤掉、「请求引荐」已被介绍卡取代;ONBOARDING_TUTORIAL 的联系人页现状被 08-31 起七次改动推翻。
6. 不能碰的数(当前值 / 实现出处 / 守卫或未覆盖)
🔒 = Jeff / 评审明确锁定的;其余是普通配置值,改之前把守卫一起改。
| 数 | 当前值 | 实现出处(95b0676a) | 守卫 / 未覆盖 |
|---|---|---|---|
| 羁绊温度加成 🔒 | 亲人 / 挚爱 +300,挚友 +100,自定义 0 | bond/model.go:29-30,118,186-197 |
bond/model_test.go:167 |
| 六度深度 / BFS 阈值 | 6 / 四份 > 5000 层后停、两份无阈值 | §3.3 表六处 | 未覆盖 |
| 通讯录一次上限 | 5000 条 | friend/handler.go:312-314 |
未覆盖 |
| 敲门冷却 | 3 天(72 小时) | friend/repository.go:166-169,247-262 |
未覆盖 |
| 介绍卡留言 / 收件箱 | 200 字 / 各 50 | introcard_handler.go:60-62、introcard.go:209-233 |
未覆盖 |
| 邀请码默认名额 / 批量上限 / 等候名单限频 | 3 / 1000 / 24 小时 20 次 | 000190:35、invite/handler.go:174-177、service.go:140-151 |
未覆盖 |
| 动态分页 / feed 缓存 | 21 · 11 · 21 / 第一页 20 条 | post/service.go:221,236,250、local_store.dart:711 |
未覆盖 |
| 状态 | 24 小时(到期不可读)/ 50 MB | 000010:12、status/repository.go:35-43、status/handler.go:21 |
未覆盖 |
| 照片墙 / 登巴环脸数 | 300 人 / 16 张 | post/service.go:709-711、dunbar_rings_screen.dart:21-25 |
dunbar_second_ring_test(部分) |
| 关系宇宙结构参数 🔒 | 四圈半径 / 转速 / 呼吸 / 粒子 | relationship_universe.dart:14-16 |
未覆盖(Jeff 亲手给的) |
| 号码变体 | < 7 位丢弃;0 → 60 |
friends_screen.dart:434-440 |
未覆盖 |
7. 出处与旧图纸去向
- 代码(全部
[code@95b0676a]):backend/internal/domain/friend/{handler,repository,model,introcard,introcard_handler}.go、invite/{handler,service,repository,dashboard}.go、bond/{handler,service,repository,model}.go、beacon/{handler,repository}.go、post/{handler,service,repository,sixlogy}.go、status/{handler,service,repository}.go、user/{handler,service,repository}.go;Applib/screens/{friends_screen,add_friend_screen,new_contact_screen,contacts_priming_screen,invite_link_screen,my_invites_screen,sixlogy_screen,sixlogy_universe,sixlogy_story_screen,dunbar_rings_screen,bond_screen,intro_letter_screen,beacon_screen,moments_screen,moment_compose_screen,my_world_screen,relation_chain_screen,login_screen,direct_chat_settings_screen}.dart、lib/data/{friends_feed,six_network,bond_catalog,local_store}.dart、lib/widgets/{ignite_sheet,relationship_universe,dunbar_rings_view,post_sheets,post_card,invite_gate_sheet}.dart、lib/services/deep_links.dart、lib/api/api_client.dart。 - 迁移:000001(
friend_requests/friendships)、000010(statuses/status_views)、000021(敲门)、000022 / 000023 / 000025 / 000026 / 000269-000291(posts 增列)、000040(bonds)、000080(ignite_requests)、000120(beacons/beacon_hops)、000190(邀请门四表)、000191(bonds.category/posts.audience_category/post_excludes)、000300 / 000302(评论社交、举报)、000304(intro_cards/intro_card_hops,friend 最新)、000311(post_notify_subs,本模组整体最新)。六度 / 度数没有表。 - Redis:只有
invite:wl:<ip>(24 小时、20 次);friend / bond / beacon / post / status 五个包不用 Redis。 - 生产:本文没有在生产机核任何项(通讯录匹配不落库、邀请门开关
gate_enabled现状、personas分身脸数据)→[prod@ 待核]。 - 决定:2026-06-29 Jeff 陌生人 / 被拉黑不能呼叫(好友是授权);2026-08-05 Jeff 六度三判据、亲人 + 挚爱并成 family、砍掉「只给核心圈看」;2026-08-24 Jeff 亲手交关系宇宙组件;2026-08-27 Jeff 介绍卡沿六度走、双向、一次一站;2026-08-27 李敏 羁绊页 ❤️ → 🔗、高亮互认、加回改关系;2026-08-31 到 09-04 Jeff 通讯录字母圆牌四连改;2026-09-04 Mike Ng 隐私说明改深灰;2026-09-04 Jeff 「You」下面列你的 AI 助手(000341);as-built 哈希匹配只匹配不落库(出处是代码注释,不是决定记录)。
- 未核清单:① 生产未核(见上);② 介绍卡过期:
introcard.go与 000304 无字段无逻辑,全仓别处未核;介绍卡 accepted 后怎么衔接好友申请 / 私聊,未找到;③MomentsScreen/RelationChainScreen是否真断路(源码没说明);④ 信标:08-20 后 CHANGELOG 无记录,是否停更不下结论;⑤ 「不加盐是为了服务器现算比对」是实现推断;⑥ 状态到期的物理清除链;⑦WithinSixDegrees/IntroChain/Shortest三份 BFS 有没有暗影过滤,未核(PathPeople靠调用方 skipDirect、两份可达集只第一跳,已核);⑧ 发布 / 改档位时会不会清空分类与排除字段,未核。 - 旧图纸去向:
CONTACTS_SIXLOGY.md整份被本文替代(三判据、登巴环、SQL 红线都在 §3.2 / §3.3 / §5)→_archive/(等 maintainer 挪);CONTACTS_INVITE_STRATEGY.md整份替代(只留三条原则:哈希匹配、不存原始通讯录、不自动加好友,已在 §0 / §3.5);BOND.md握手 / 配对表 / 温度三节 → 本文 §3.2,共享空间 → 城堡线(两人共建与群共建同构),与暗影的呼应 → SHADOW;sixlogy_play.html三根轴 / 度数路径 / 引荐(→ 介绍卡)/ 两条红线 / 性能判据 → 本文 §3.3,挑战与星图 → growth;FIRST_HOUR.md§3 通讯录两行 → 本文,其余留作激活策略;ONBOARDING_TUTORIAL.md§3.2 第 5 条 → 本文,其余 → growth;AI_SCORING.md不被替代,归 castle(warmth)线;群 / 社群邀请链接(000239 / 000288)→ community。