Files
full/docs/audit/module-social-relation.md
yanweidong 63aeedc2fe docs: add per-module audit reports (18 modules)
Add static security/quality audit reports for all 18 Go service modules under module/, plus a consolidated index (docs/audit/README.md) with per-module statistics, top risks, cross-module systemic defects and a phased TODO list (T1-T19). No production code is modified.
2026-09-14 22:16:27 +08:00

76 KiB
Raw Blame History

审计报告module/social/relation

审计方式:只读代码审计(未修改任何 .go/.proto/.yaml/go.mod 文件)。 审计对象:D:\work\bsm-infra\full\module\social\relationgRPC + grpc-gateway 微服务Postgres + Redis。 证据行号均以当前工作区文件为准。

1. 模块概览

内容
服务名 / 端口 relationgRPC Port: 12248(dev) / 12426(prod,test)etc/relation_dev.yaml:2etc/relation_prod.yaml:2
对外协议 原生 gRPC /relation.{Service}/{Method};自带 grpc-gatewayGateway.Enable: true, Port: 12425
gRPC 服务 Follow(4 RPC)、Friend(17 RPC)、Match(4 RPC),共 25 个方法(internal/server/new.go:28-30proto/*.proto
存储 PostgresDatabases.Driver: postgres),本地模型表 relation_followrelation_friendrelation_friend_applyrelation_friend_apply_messagerelation_friend_tagrelation_friend_in_tagrelation_match;外部表 relation_extend(本模块直接 SQL 访问,仓库内无任何模型/迁移定义)
缓存 Redis/RF/{identity}/friends/RF/{identity}/applyinternal/logic/common/const.go:176-201
目录结构 cmd/{main,cli}etc/{dev,test,prod}.yamlinternal/{config,impl,logic/{common,follow,friend,match},models,server}pb/proto/test/lint(空目录)
关键依赖 git.apinb.com/bsm-sdk/core v0.2.1replace../../../../../bsm-sdk/corego.mod:69
README module/social/relation/README.md# relation 两行,无任何文档

业务语义:Follow 维护单向关注from→toFriend 维护双向好友(每个用户一行 relation_friend)、好友申请与留言、好友标签;Match 声称维护推荐/匹配记录并可通过/忽略。

2. 审计范围与方法

已读文件(非 pb 源码全覆盖52 个非 pb 文件中除 go.sum 外全部阅读)

  • 入口与配置:cmd/main/main.gocmd/cli/main.gointernal/config/config.goetc/relation_{dev,test,prod}.yamletc/supervisor.bsm-social-relation.confgo.modREADME.md
  • 契约:proto/{const,friend,follow,match}.protointernal/server/{new,follow_server,friend_server,match_server}.go
  • 逻辑24 个文件):internal/logic/common/const.gologic/follow/{do,doing,undo,state,fetch}.gologic/friend/{search,fetch,get,delete,modify_nickname,do_popular,undo_popular,apply_do,apply_do_message,apply_do_pass,apply_do_reject,apply_fetch,apply_get,tag_fetch,tag_member_fetch,tag_do_create,tag_do_update}.gologic/match/{fetch,get,do_join,do_ignore}.go
  • 模型:internal/models/*.go(含空的 base.goquery.go)、internal/impl/impl.go
  • 参考SDK用于确认语义非审计对象D:\work\bsm-sdk\core\types\db.goStd_IICUDS/Std_Passport 字段与索引)、core/service/{service,meta,register}.gocore/with/*.gocore/database/{new.go,sql/postgresql.go}core/vars/status.go
  • 生成代码 pb/*.pb.go*.pb.gw.go 仅做接口/路由/字段核对(TagItemFriendsReplyApplyItemFriendApplyGetReply、gw 路由)

方法read 全量阅读 + grep 交叉检索TODO/空实现、relation_* 列名、RelationMatch 写入方、事务、限流、黑名单、MQ、索引、panic/recover仓库级 grep 验证“唯一写入点”“表定义是否存在”等结论。证据等级:每条结论均附 file:line + 摘录;凡"返回 nil"的结论均按显式 return 分支逐条追踪(不使用"命名返回值未赋值即裸 return 返回 nil"的经验外推);无法从源码确认的标注推测且不据此升级级别(汇总见第 6 节末尾)。

静态检查(在模块目录执行):

  • gofmt -l . → 无输出exit 0格式合规
  • go vet ./... → exit 0无告警代码可编译、vet 干净;下文缺陷均为运行期/语义缺陷,编译器与 vet 无法发现)

未覆盖 / 无法验证:数据库物理 schema仓库内无建表 SQL/迁移脚本grep relation_follow|relation_friend|relation_match + *.sql 无结果)、线上 Redis/etcd 实际配置、上游 JWT 签发方与调用方客户端行为、relation_extend 表归属与真实列。

3. 问题清单

P0

1. 两个 RPC 必现 nil 指针 panic可直接打挂整个服务进程无 recover 拦截器)

  • 位置
    • module/social/relation/internal/logic/friend/tag_fetch.go:15reply *pb.FriendTagsReply 命名返回,全函数从未赋值)、:22Count(&reply.Total)
    • module/social/relation/internal/logic/friend/apply_get.go:16reply *pb.FriendApplyGetReply 命名返回,全函数从未赋值)、:32reply.Data = ...
    • 放大点:module/social/relation/internal/server/new.go:23
  • 证据
    // tag_fetch.go:15,22
    func TagFetch(ctx context.Context, in *pb.Empty) (reply *pb.FriendTagsReply, err error) {
    	...
    	err = impl.DBService.Model(models.FriendTag{}).Where("relation_id=?", auth.ID).Count(&reply.Total).Order("id asc").Scan(&reply.Data).Error
    
    // apply_get.go:16,32
    func ApplyGet(ctx context.Context, in *pb.IdentRequest) (reply *pb.FriendApplyGetReply, err error) {
    	...
    	reply.Data, err = common.GetrelationInfoDetailCard(apply.FromIdentity)
    
    // server/new.go:23
    Grpc:      grpc.NewServer(),   // 无 UnaryInterceptor无 recover / 无日志 / 无限流 / 无超时
    
  • 影响reply 为 nil 指针,&reply.Total / reply.Data = ... 都会解引用 nil两个 RPC 100% panic。grpc.NewServer() 未安装任何拦截器gRPC 不会 recover handler panic → 整个 relation 进程退出supervisorautorestart=true 会造成反复重启抖动,期间 Follow/Friend/Match 全部不可用)。任何持有有效 JWT 的账号调用一次 Friend.TagFetchFriend.ApplyGet 即可触发,属远程可用性攻击面。
  • 证据强度说明(已复核,非推断):两处都是编译期即可确定的 nil 指针字段解引用——:22 计算 &(*reply).Total:32 执行 (*reply).Data = ...panic 发生在到达文件末尾那个裸 returntag_fetch.go:27apply_get.go:43)之前,因此不属于"命名返回值未赋值 → 裸 return 返回 nil"那一类推断(那类模式见 module/ec/address/internal/logic/library/fetch.go:38-40);生成的 internal/server/friend_server.go:84-86 只是直接转发 return friend.TagFetch(ctx, in),命名返回值在入口处必为零值 nil。
  • 建议1立即在 new.go 安装 grpc.ChainUnaryInterceptor(recovery 拦截器, 日志, 限流, 超时) 作为兜底2两个函数内显式初始化 reply = &pb.FriendTagsReply{} / reply = &pb.FriendApplyGetReply{}3CI 增加 staticcheckSA5011 类)与 handler 级单元测试,禁止未初始化命名返回指针被使用。

P1

2. 好友/标签表的读写列名系统性不一致(模型 passport_id vs 查询 relation_id vs group_identity

  • 位置internal/models/relation_friend.go:10internal/models/friend_in_tag.go:10internal/models/friend_tag.go:10 vs internal/logic/common/const.go:120,128,134internal/logic/friend/delete.go:27do_popular.go:27undo_popular.go:27tag_fetch.go:22tag_member_fetch.go:27
  • 证据
    // models/relation_friend.go:10-12  归属列由 SDK Std_Passport 提供passport_id / passport_identity
    	types.Std_Passport
    	FriendrelationID       uint   `gorm:"column:friend_relation_id;not null;"`
    
    // common/const.go:120,128,134
    	impl.DBService.Where("relation_id=?", id).Order("id desc").Find(&relationFriend)
    	impl.DBService.Where("relation_id=?", id).Find(&friendInTag)
    	err = impl.DBService.Model(models.FriendTag{}).Where("relation_id=?", id).Order("id asc").Scan(&reply.Tags).Error
    
    // tag_member_fetch.go:27  relation_id + group_identity 两个列在任何模型中都不存在
    	err = impl.DBService.Model(models.RelationFriend{}).Where("relation_id=? and group_identity=?", auth.ID, in.Identity).Pluck("relation_id", ids).Error
    
  • 影响:写入侧(apply_do_pass.go:31-32PassportID/PassportIdentitypassport_id/passport_identity)与读取侧(relation_id/relation_identity/group_identity)永远对不上:若物理表按模型建(无 relation_id 列),则好友列表、好友标签列表、标签成员、删除/置顶好友全部报 column "relation_id" does not exist 并以 ErrDB 返回;若物理表恰有 relation_id 列,则该列从未被写入,好友列表恒为空。无论哪种情况,"好友/标签"功能都不可用。group_identity 属第三种命名,几乎必然不存在。
  • 建议:统一以模型为准(passport_id/passport_identity 或显式改名 relation_id/relation_identity 并同步模型标签),把 friend_in_tag.friend_identity/tag_identity 的查询改为关联 relation_friend_in_tag 表;补一个集成测试用真实表跑通 Fetch/Delete/TagMemberFetch。

3. 好友行写操作把“目标用户 identity”当作“好友行主键 identity”且写了不存在的列 → 删除好友/备注/置顶全部静默失效

  • 位置internal/logic/friend/delete.go:27modify_nickname.go:26do_popular.go:27undo_popular.go:27
  • 证据
    // delete.go:27
    	err = impl.DBService.Model(models.RelationFriend{}).Delete("relation_id=? and identity=?", auth.ID, in.Identity).Error
    // modify_nickname.go:26   nickname 列在模型中不存在(模型是 remark_name
    	err = impl.DBService.Model(models.RelationFriend{}).Where("relation_id=? and identity=?", auth.ID, in.Identity).UpdateColumn("nickname", in.Nickname).Error
    
    // apply_do_pass.go:29-33  证明 identity = 本行 UUID好友目标在 friend_relation_identity
    	friend := &models.RelationFriend{FriendrelationID: uint(in.FriendRelationId), FriendrelationIdentity: in.FriendRelationIdentity}
    	friend.Identity = utils.UUID()
    
  • 影响IdentRequest.identityfollow/state.go:29friend/get.go:23 中被明确当作“目标用户 identity”relation_friend.identity 是行 UUIDStd_IICUDS.IdentityuniqueIndex),两者永不可能相等 → 4 个写操作全部匹配 0 行,但都返回 Data:"OK"(假成功):Delete 删不掉好友、备注与置顶不生效。ModifyNickname 额外写 nickname 列(表中为 remark_name),会直接 SQL 报错。DB 侧软删除(DeletedAt gorm.DeletedAt)使这些行永久残留。
  • 建议Where 条件改为 passport_id=? and friend_relation_identity=?(与写入侧一致),nickname 改为 remark_name;对 RowsAffected == 0 返回明确错误码而非 OK

4. Follow.Undo 定位列错误且不校验归属 → 取消关注永远无效,并可越权删除他人关注关系

  • 位置internal/logic/follow/undo.go:18,26
  • 证据
    func Undo(ctx context.Context, in *pb.IdentRequest) (reply *pb.DataStatusReply, err error) {
    	_, err = service.ParseMetaCtx(ctx, nil)     // auth 被丢弃,后续无归属校验
    	...
    	err = impl.DBService.Model(&models.RelationFollow{}).Delete("identity=?", in.Identity).Error
    
  • 影响arelation_follow.identity 是行 UUIDin.Identity 是目标用户 identity → 取消关注永远删不到行(配合 Doing 无去重重复关注会不断累积bSQL 完全没有 from_identity = auth.Identity 约束,若调用方传入某条关注行的 UUID该 UUID 会通过 follow/state.go:42-45Data: data.Identity 泄漏给客户端),即可删除任意用户的关注边IDOR + 软删除),造成他人关注关系被破坏。
  • 建议:改为 Where("from_identity=? and to_identity=?", auth.Identity, in.Identity).Delete(&models.RelationFollow{}),并使用 auth,对 0 行受影响返回明确错误;State 只返回布尔/状态码,不返回行 UUID。

5. 好友申请详情与留言可被任意登录用户越权读写IDOR

  • 位置internal/logic/friend/apply_get.go:17,26internal/logic/friend/apply_do_message.go:18,27,40
  • 证据
    // apply_get.go:17,26  只校验“已登录”,不校验申请人/接收人身份
    	_, err = service.ParseMetaCtx(ctx, nil)
    	...
    	err = impl.DBService.Where("identity=?", in.Identity).First(&apply).Error
    
    // apply_do_message.go:26-40  任意 apply_id 都能追加留言并改写其 last_message_id
    	msg := models.FriendApplyMessage{ApplyID: uint(in.ApplyId), Body: in.Body}
    	...
    	err = impl.DBService.Model(models.FriendApply{}).Where("id=?", msg.ApplyID).UpdateColumn("last_message_id", msg.ID).Error
    
  • 影响ApplyGet 返回申请详情(对端资料卡片 GetrelationInfoDetailCard(apply.FromIdentity) + 全部聊天记录),任何账号只要猜到/枚举到 apply.identityUUID即可读取两个陌生人之间的申请与私聊内容ApplyDoMessage 可向任意申请会话注入留言并篡改 last_message_id,破坏他人会话(且 Body 会进入对方申请列表展示,可用于骚扰/钓鱼)。同文件 apply_do_reject.go:26apply_do_pass.go:43 都已按 to_identity = auth.Identity 归属过滤,说明这里有遗漏而非设计意图。
  • 建议ApplyGet/ApplyDoMessage 增加 Where("identity=? and (from_identity=? or to_identity=?)", in.Identity, auth.Identity, auth.Identity)(或复用同一查询):先取 apply 再校验 apply.FromIdentity == auth.Identity || apply.ToIdentity == auth.Identity,越权返回 ErrPermissionDenied

6. 标签成员可被越权增删(跨用户写)

  • 位置internal/logic/friend/tag_do_update.go:28-39internal/logic/friend/tag_do_create.go:40-52
  • 证据
    // tag_do_update.go:38-39  DEL 分支没有任何 owner 过滤
    	case "DEL":
    		err = impl.DBService.Model(models.FriendInTag{}).Delete("friend_identity=? and tag_identity=?", in.FriendIdentity, in.TagIdentity).Error
    
    // tag_do_create.go:41-49  任意 friend_identity 直接入标签,未校验是否为本人好友,也未校验 tag 归属
    	for _, identity := range in.FriendIdentity {
    		temp := &models.FriendInTag{FriendIdentity: identity, TagIdentity: data.Identity}
    
  • 影响DEL 是无归属条件的全局删除——任何账号可删除其他用户标签下的任意成员IDOR破坏他人数据ADD 分支同样不校验 tag_identity 是否属于调用者,可向他人标签塞入成员(数据污染),且重复调用无唯一约束会不断产生重复行。注意 RelationFriend/FriendInTag 均无 (owner, friend, tag) 唯一约束(models/friend_in_tag.go:11-12)。
  • 建议:操作前先校验 FriendTag.identity = in.TagIdentity AND passport_id = auth.IDADD 校验目标 friend_identity 是否存在于调用者的 relation_friendDEL 追加 passport_id = auth.ID 条件;为 (passport_id, friend_identity, tag_identity) 建唯一索引并改用 upsert。

7. Friend.Fetch / Friend.ApplyFetch 缓存命中时返回 nil 响应,且 Redis 写失败时 nil 接口调用 panic

  • 位置internal/logic/friend/fetch.go:21-51internal/logic/friend/apply_fetch.go:21-51
  • 证据
    // fetch.go:29-51apply_fetch.go 同构)
    	cache, err := common.GetCache(auth.Identity, "friends")
    	...
    	if in.Version != cache.Version {
    		reply, err = common.CollectionFriendData(auth.ID)
    		...
    	}
    	if err != nil { ... }
    	if cacheErr != nil {
    		printer.Error(err.Error())      // 此处 err 保证为 nil → 对 nil error 接口调用 Error() → panic
    		return nil, errcode.ErrRedis
    	}
    	return reply, nil                    // 版本命中缓存时 reply 从未赋值,返回 typed nil
    
  • 影响两个缺陷。a当客户端按协议带回 version == 缓存版本(稳态调用)时,reply 保持 nil函数返回 (nil, nil),客户端拿到空的 FriendsReply/ApplyFetchReplytotal=0、无好友/无申请),表现为“好友列表突然清空”;同时 GetCache 的结果被读入 cache 却从未返回,缓存形同虚设(in.Version == 0 时还会重复执行一次全量 CollectionFriendData,见 fetch.go:21-40bRedis 写失败(SetCache 出错)时 cacheErr != nil,而第 48 行打印的是恒为 nil 的 err,触发 nil 接口方法调用 panic与 P0-1 同样无 recoverRedis 抖动会升级为进程崩溃
  • 证据强度说明(已复核):本条 (a) 的 nil 返回是按显式 return reply, nilfetch.go:51apply_fetch.go:51)逐分支追踪得出的,不是"裸 return"推断:in.Version != 0 时第 21-27 行的赋值分支被整体跳过,第 35 行的重算分支仅在 in.Version != cache.Version 时执行,两条路径都不成立时 reply 必为 nil。common.GetCache 侧不是 nilcommon/const.go:196json.Unmarshal(..., &reply) 会对 **T 分配新对象),因此 cache.Version 可安全访问、panic 只可能来自第 48 行。
  • 建议:版本命中时 return cache, nilprinter.Error(cacheErr.Error());把缓存读写显式建模(命中/未命中/降级),缓存不可用时回源 DB 而不是报 ErrRedis

8. Match.Fetch 的原生 SQL 语法不成立(重复别名 + ORDER BY 位置错误)→ 匹配列表恒失败

  • 位置internal/logic/common/const.go:155-172
  • 证据
    			From relation_match as rm,relation_extend as pe 
    			Left join pe on pe.relation_identity=rm.recommend_identity
    			Where rm.relation_identity=?
    			Offset ?
    			Limit ?
    			Order by rm.id Desc
    
  • 影响relation_extend as pe 已在 FROM 中声明别名,随后又 Left join pePostgres 报 table name "pe" specified more than once;即使修掉别名,Order by 出现在 Limit 之后也是语法错误(syntax error at or near "Order")。因此 Match.Fetchmatch/fetch.go:41)必然返回 ErrDB匹配列表功能完全不可用。附带问题SELECT 用 pe.name,而本模块统一的卡片字段是 nicknameconst.go:15),字段名存疑(推测该列不存在)。
  • 建议:改写为 FROM relation_match rm JOIN relation_extend pe ON pe.relation_identity = rm.recommend_identity WHERE rm.relation_identity = $1 ORDER BY rm.id DESC OFFSET $2 LIMIT $3,并补一条针对该函数的集成测试(当前无测试,见 P2-29

9. 匹配记录没有任何写入方;DoJoin/DoIgnore 操作的是关注表,语义完全错位

  • 位置internal/logic/match/fetch.go:34internal/logic/common/const.go:166(只读);internal/logic/match/do_join.go:29-35internal/logic/match/do_ignore.go:27(写错表)
  • 证据
    // match/do_join.go:17-35  RPC 注释是“执行通过,加为好友”,实际写入关注表
    // 执行通过,加为好友
    	data := &models.RelationFollow{FromIdentity: auth.Identity, ToIdentity: in.Identity}
    	err = impl.DBService.Create(data).Error
    
    // match/do_ignore.go:27  从关注表按行 UUID 删除
    	err = impl.DBService.Model(new(models.RelationFollow)).Delete("identity=?", in.Identity).Error
    
  • 影响a全仓库 grep RelationMatch 只有 match/fetch.go:34 的 COUNT 与 common/const.go:166 的 SELECT没有任何 INSERT/写入路径(无推荐算法、无定时任务、无 MQ 消费者),因此即使修好 SQL匹配列表也永远为空——README 第 201 行“Match 的接口主要维护匹配记录”与实现不符。bDoJoin 只是“关注”了对方(且无去重,见 P1-11并未建立好友关系与 RPC 契约“加为好友”不符也没有把匹配记录置为已通过重复推荐无法消除cDoIgnore 删除的是 relation_follow 且按 identity=in.Identity(行 UUID 列)定位,既删不掉“忽略”的对象(匹配记录未被标记),又提供了无归属约束的跨用户删除入口(同 P1-4
  • 建议:明确匹配数据写入方(推荐服务/离线任务),DoJoin 改为写 relation_match.status=1 + 建好友关系,DoIgnore 改为 relation_match.status=-2(忽略) 并加 relation_identity = auth.Identity 约束;若匹配能力短期不做,应在 proto/实现中显式返回 codes.Unimplemented 而不是“假成功 + 关注表”。

10. ApplyDoPass 只写入单向好友关系、无事务、无状态校验 → 好友关系永久不对称 + 重复通过产生重复行

  • 位置internal/logic/friend/apply_do_pass.go:24-47
  • 证据
    	friend := &models.RelationFriend{FriendrelationID: uint(in.FriendRelationId), FriendrelationIdentity: in.FriendRelationIdentity}
    	friend.Identity = utils.UUID()
    	friend.PassportID = auth.ID          // 只给“通过者”自己建好友行
    	friend.PassportIdentity = auth.Identity
    	...
    	err = impl.DBService.Model(models.FriendApply{}).Where("to_identity=? and identity=?", auth.Identity, in.ApplyIdentity).UpdateColumn("status", 1).Error
    
  • 影响a全仓库唯一写入 relation_friend 的位置就是本行grep &models.RelationFriend{ 仅此一处),该行归属 auth.Identity(申请人申请的接收方),申请人一侧永远没有好友行Friend.Fetch(按 relation_id=当前用户 查询)也永远不会返回对方 → “A 是 B 的好友但 B 不是 A 的好友”双向好友语义被破坏。b三次写库建好友 → 改申请状态不在事务中第二步失败则好友已建立而申请仍为待处理重试即产生第二条好友行无唯一约束c先建好友、后校验未检查申请是否存在/是否处于待处理状态:传入任意 apply_identity 都会先插入好友行,再把 0 行更新为 status=1最终返回 OK(假成功 + 数据污染)。
  • 建议:用 db.Transaction 包裹“校验申请(to_identity=auth.Identitystatus 待处理)→ 双向写好友行(双方各一行,session_idcommon.UniqueSessionID 一致)→ 更新申请状态”,失败整体回滚;对 (passport_id, friend_relation_identity) 建唯一索引;RowsAffected==0 返回 ErrNotFound/ErrInvalidArgument

11. 关注/申请既无唯一约束也无幂等校验,且全模块无限流 → 重复关注、重复申请与刷量

  • 位置internal/logic/follow/doing.go:24-38internal/logic/friend/apply_do.go:23-40internal/models/relation_follow.go:8-12internal/server/new.go:23
  • 证据
    // follow/doing.go:28-38  无“是否已关注/是否自己”检查,直接 Insert
    	data := &models.RelationFollow{FromIdentity: auth.Identity, ToIdentity: in.Identity}
    	data.Identity = utils.UUID()
    	err = impl.DBService.Create(data).Error
    
    // models/relation_follow.go:10-11  只有 identity 有唯一索引from/to 无约束、无索引
    	FromIdentity string `gorm:"column:from_identity;type:varchar(36);not null;"`
    	ToIdentity   string `gorm:"column:to_identity;type:varchar(255);not null;"`
    
  • 影响:同一用户可无限次 Doing 同一个人(每次新 UUID 都插入成功)→ 关注表膨胀、follow.Fetch 关注列表出现重复项、Count 计数虚高;同理 ApplyDo 可无限次提交申请与留言(每次 1 条 relation_friend_apply + 1 条 relation_friend_apply_message),对方申请列表被刷屏。没有任何限流/防刷(grpc.NewServer() 无拦截器、Redis 无计数 SetNX),配合 P1-13 的任意资料查询可脚本化批量操作。
  • 建议(from_identity,to_identity)(from_identity,to_identity)(apply) 建唯一索引,使用 ON CONFLICT DO NOTHING(或先查后插 + 冲突捕获)实现幂等;禁止关注/申请自己;接入 SDK 中间件做按用户+方法的令牌桶限流,并对同一对用户设置申请冷却时间。

12. 拉黑/黑名单能力在本模块完全不存在 → 拉黑后仍可关注、申请、查看资料

  • 位置:全模块检索 black|block|拉黑|shield 无任何命中(internal/**/*.go);模型仅 relation_followrelation_friendrelation_matchfriend_apply*friend_tag*
  • 证据
    // follow/doing.go:28-38  关注前不查询任何拒绝/黑名单状态
    	data := &models.RelationFollow{FromIdentity: auth.Identity, ToIdentity: in.Identity}
    	err = impl.DBService.Create(data).Error
    
    // common/const.go:19-22  资料卡片查询也不带任何可见性/黑名单条件
    	err = impl.DBService.Table("relation_extend").Select(Filed).Where("relation_identity = ?", identity).First(&card).Error
    
  • 影响:没有黑名单表/字段、没有 DoBlock/UndoBlock RPC、没有在 Doing/ApplyDo/Get/Fetch 查询路径上做屏蔽判断,因此即使上层产品有“拉黑”入口,本服务的关注、好友申请、资料卡片与好友列表都不会过滤 → 被拉黑用户仍可继续关注、发申请、查看资料,拉黑形同虚设(同时 relation_friend_apply.status=-1 仅表示“拒绝”,不是黑名单,且被拒绝后仍可反复申请,见 P1-11
  • 建议:新增 relation_block(owner_identity, target_identity, status) 表与 Follow/Friend 前置校验钩子,在 DoingApplyDo(双向)、GetMatch.GetApplyFetchFriend.Fetch 上统一过滤;拉黑时同时撤销关注边与在途申请。

13. 任意用户资料卡片可被任意登录账号按 identity 拉取(隐私泄露 + 批量枚举)

  • 位置internal/logic/friend/get.go:14-29internal/logic/match/get.go:14-29
  • 证据
    // friend/get.go:15-23  仅 ParseMetaCtx + identity 非空,无任何好友/匹配/可见性校验
    	_, err = service.ParseMetaCtx(ctx, nil)
    	...
    	reply, err = common.GetrelationInfoDetailCard(in.Identity)
    
    // common/const.go:15  返回的字段
    	Filed string = "relation_identity as identity,nickname,avatar,sex,province,city,area,sign"
    
  • 影响Friend.Get/Match.Get 对任意 identity 返回昵称、头像、性别、生日(RelationItem.birthday 在 match/get 路径由 SQL 选取)、省市地区、签名,且无好友关系、无匹配关系、无隐私开关、无黑名单校验。结合 P1-11 的无限流,可对 identity 空间做批量爬取/用户画像收集(用户枚举风险)。ApplyGetP1-5TagMemberFetch 也会放大该数据出口。
  • 建议Friend.Get 限制为“我的好友或与我存在申请关系”,Match.Get 限制为“我的匹配记录中的对象”;引入可见性字段(谁看得到地区/生日)与黑名单过滤;对外暴露的批量/单条资料接口加限流与审计日志。

14. 关系热点列没有任何索引声明,仓库内也没有建表/迁移脚本

  • 位置internal/models/relation_follow.go:10-11relation_friend.go:11-12friend_in_tag.go:11-12friend_apply.go:10-14relation_match.go:8-9
  • 证据
    // 全部模型仅 identity/deleted_at/status/passport_* 带索引标签;业务查询列无索引
    	FriendrelationIdentity string `gorm:"column:friend_relation_identity;not null;" json:"friend_relation_identity"`
    	RecommendIdentity string `gorm:"column:recommend_identity;type:varchar(255);not null;" json:"recommend_identity"`
    
    // follow/fetch.go:49 / follow/state.go:29 / match/fetch.go:34 都在无索引列上过滤
    	err = model.Count(&total).Order("id desc").Offset(...).Pluck(column, &ids).Error   // where from_identity=? / to_identity=?
    
  • 影响from_identityto_identity(关注/粉丝列表、Statefriend_relation_identityfriend_identitytag_identityrecommend_identity(匹配计数)上的查询全部走全表扫描;关注表是社交产品最大的表之一,大 V 账号的粉丝列表/状态查询会成为慢查询与连接池占用源(MaxOpenConns 默认 64database/sql/postgresql.go 通过 SDK 默认值)。仓库内 grep 无任何 .sql/迁移文件(go.mod 无 migration 依赖,cmd/main/main.go:37models.InitData 已被注释),模型索引是否落到线上表无法自证——推测线上表由外部 DBA 手工维护,必须人工核对。
  • 建议:为 (from_identity,to_identity)(to_identity)(passport_id,friend_relation_identity)(passport_id)(relation_identity)(tag_identity,friend_identity) 建索引(唯一索引与查询索引合并);把建表/索引纳入版本化迁移脚本并在 CI 校验,而不是依赖外部手工操作。

15. Gateway.Enable: true 但本模块既无 service/expose.goMux 也恒为 nil → 12425 端口每个请求 panic全局模板缺陷的本模块表现

  • 位置module/social/relation/service/空目录0 文件)、internal/server/new.go:20-34cmd/main/main.go:29-32etc/relation_prod.yaml:38-40
  • 证据(本模块侧,仅记录事实,全局根因不在此重复论证):
    // server/new.go:20-29  Mux 字段从未初始化;只注册 gRPC无任何 Register*HandlerServer
    	srv := &Server{ Ctx: context.Background(), Grpc: grpc.NewServer(), grpcConns: make(map[string]*grpc.ClientConn) }
    	pb.RegisterFollowServer(srv.Grpc, NewFollowServer())
    
    // cmd/main/main.go:29-32  把恒为 nil 的 s.Mux 交给 SDK
    			GatewayConf: config.Spec.Gateway,
    			GatewayMux:  s.Mux,
    
    对比其它模块的约定文件 module/ec/address/service/expose.go:18-26pb.RegisterLibraryHandlerServer(ctx, options.Gateway, server.NewLibraryServer())),本模块 service/ 目录为空、无 Expose 入口,因此 pkgs/all/internal/service/(无 relation/social 文件)也不可能聚合暴露它(与 README.md:194“社交域未接入 all 的服务注册表”一致)。
  • 影响:三份配置都开启网关(etc/relation_prod.yaml:38-40SDK 会在此端口 http.ListenAndServe(addr, s.Opts.GatewayMux)core/service/service.go:106-108,126handler 为 typed-nil *runtime.ServeMux → 每个 HTTP 请求在 ServeMux.ServeHTTP 内解引用 nil receiver panic被 net/http 逐连接 recover表现为 5xx/连接重置与日志刷屏)。结论:该模块当前只能通过原生 gRPC或被其它网关按 /rpc/... 动态转发)访问,自带 HTTP 端口不可用。与第 3 节其余条目不同,这是仓库级的模板缺陷(同批审计的其它模块报告会有一致结论),本报告只标注本模块表现,不单独展开修复论证。
  • 建议:补齐 module/social/relation/service/expose.go(与其它模块同构,内部初始化 Mux 并注册 Register*HandlerServer),或在未接入前把三份配置的 Gateway.Enable 置为 false,避免暴露不可用端口。

16. 生产环境 GORM Debug 日志默认开启 → 全量 SQL 与好友申请留言正文写入 stdout 日志

  • 位置internal/impl/impl.go:23cmd/cli/main.go:11etc/supervisor.bsm-social-relation.conf:8
  • 证据
    // impl.go:23  opts 传 nil
    	DBService = with.Databases(config.Spec.Databases, nil) // model
    
    // SDK core/database/sql/postgresql.go SetOptions(nil) 默认 Debug:true随后无条件 gormDb.Debug()
    		options = &types.SqlOptions{ MaxIdleConns: ..., IsAutoMigrate: false, LogStdout: false, Debug: true }
    
    // cmd/cli/main.go:11  直接打印含账号口令的 DSN
    	fmt.Println(config.Spec.Databases)
    
  • 影响GORM Debug 会打印每条 SQL 及其参数,包括 relation_friend_apply_message.body(好友申请留言正文,属用户私聊内容)、用户 identity、申请/标签等记录;printer 输出经 supervisor 重定向进入 /data/app/logs/social-relation.log,形成长期明文 PII 存储与日志膨胀(性能亦有损耗)。cmd/cli 在开发/运维执行时会直接输出 host=... user=postgres password=CHANGE_ME ...
  • 建议:按环境传显式 SqlOptions{Debug:false}(或从配置读取 Debug 开关),生产禁用 SQL stdoutcmd/cli 移除 DSN 整体打印,日志统一走结构化 logger 并对 body 等字段脱敏。

P2

17. 列表接口无分页/分页无上限,深分页与负 offset 可触发 SQL 错误或大结果集

  • 位置internal/logic/friend/fetch.go:21VersionRequest 仅有 version无分页internal/logic/common/const.go:120,139internal/logic/follow/fetch.go:42-49internal/logic/match/fetch.go:23-41
  • 证据
    // follow/fetch.go:42-49  page_size<=1 被强制改 20无上限校验offset 由客户端数值直接相乘
    	if in.PageSize <= 1 { in.PageSize = 20 }
    	err = model.Count(&total).Order("id desc").Offset(int(in.PageNo-1)*int(in.PageSize)).Limit(int(in.PageSize)).Pluck(column, &ids).Error
    
    // common/const.go:120,139  好友列表:无 Limit全量取行后再 IN 查询卡片
    	impl.DBService.Where("relation_id=?", id).Order("id desc").Find(&relationFriend)
    	reply.Friends, err = GetrelationInfoDetailCardById(ids)
    
  • 影响Friend.Fetch 无分页参数,好友数上千的账号会一次拉全量(并生成巨型 IN (...)Postgres 参数上限 65535超限直接报错Follow.Fetch/Match.FetchPageSize 无上限,客户端传 page_size=9223372036854775807int(PageNo-1)*int(PageSize) 溢出为负 → OFFSET -N SQL 报错,或正数时形成深分页全表扫描;PageSize=1 语义被改写为 20边界错误
  • 建议:统一分页参数并加 MaxPageSize(如 100PageNo 上限校验,归还 total 用于前端分页;对 Friend.Fetch 增加分页或改为游标(id < last_id)增量同步;IN 查询按 1000 分片。

18. CollectionFriendData 丢失备注名/置顶状态(死代码 mapRelationFrend)、顺序不可控、Total 未设置

  • 位置internal/logic/common/const.go:114,122-126,139-148
  • 证据
    		mapRelationFrend = make(map[uint]models.RelationFriend)   // 声明后仅写入,从未读取(死代码)
    	...
    	for idx, item := range reply.Friends {
    		reply.Friends[idx].RemarkName = item.RemarkName      // item 即 reply.Friends[idx],自赋值,永远为空
    		reply.Friends[idx].Popular = item.Popular
    
    		ids = append(ids, item.ID)      // 用的是 relation_friend 行 ID而不是好友的 friend_relation_id
    
  • 影响aids 取自 relation_friend.id(行主键),而卡片表 relation_extendrelation_id(会员 ID匹配——对比 CollectionFriendApply:59-64 用的是 item.FromID(会员 ID可见此处取错字段 → 好友列表返回的卡片与好友关系不对应(错人/空卡片b备注名与置顶位被自赋值抹掉且 DB 中 remark_name 从未被写入(见 P1-3备注功能整链路失效cGetrelationInfoDetailCardByIdIN 查询无 ORDER BY,好友列表顺序随机,Order("id desc") 形同虚设“置顶”无法体现dFriendsReply.total 从未赋值proto 有该字段),前端总数为 0。
  • 建议ids 改为 item.FriendrelationID,用 mapRelationFrend[item.FriendrelationID] 合并 RemarkName/Popular 并按 popular desc, id desc 重排;reply.Total = int32(len(reply.Friends));删除死代码或补上真正的合并逻辑。

19. CollectionFriendApply 硬编码截断 50 条且 Total 缺失

  • 位置internal/logic/common/const.go:44,54,102-104
  • 证据
    		maxSize      int = 50
    	err = impl.DBService.Where("to_id=? ", id).Order("id desc").Limit(maxSize).Find(&applys).Error
    
    	reply.Applys = replyItems
    	reply.Version = version      // total 从未赋值
    
  • 影响:申请列表静默截断为最近 50 条(魔法数字,无分页参数),第 51 条起用户永远看不到;ApplyFetchReply.total 恒为 0前端无法提示“还有更多申请”也无法分页。
  • 建议:引入分页参数与 total 统计;maxSize 提升为常量/配置并显式返回“已截断”标志;列表保留未读/待处理过滤能力。

20. Redis 缓存 TTL=0永不过期、key 硬编码、无失效与降级,版本号无法反映留言更新

  • 位置internal/logic/common/const.go:176-201internal/logic/friend/fetch.go:35apply_fetch.go:35
  • 证据
    	key := "/RF/" + identity + "/" + collectionName
    	err = impl.RedisService.Client.Set(impl.RedisService.Ctx, key, string(val), 0).Err()   // 0 = 无过期
    
    		version = int64(item.ID)     // 版本 = 最后一条申请/好友行的 ID
    
  • 影响a/RF/... 每个用户 key 永不过期且写入侧Doing/Delete/ApplyDo…没有任何删除/失效逻辑,用户数增长即 Redis 常驻内存无限增长b版本号只取“新建行 ID 最大值”,对已有申请的追加留言(ApplyDoMessage 只更新 last_message_id,行 ID 不变)不会改变 version客户端拿到的是过期会话摘要cGetCache 失败即返回 ErrRedisfetch.go:30-33Redis 抖动会导致接口整体不可用,属缓存与数据源强耦合(应降级回源)。
  • 建议:设置明确 TTL如 10~30 分钟并加随机抖动key 前缀纳入服务/环境命名空间;所有写路径删除对应 key版本号改为业务版本updated_at/消息自增 ID缓存不可用时降级直查 DB 并告警。

21. 多步写操作无事务(申请+留言+回写、好友+状态、标签+成员)

  • 位置internal/logic/friend/apply_do.go:36-61apply_do_pass.go:35-47apply_do_message.go:33-44tag_do_create.go:34-56;全模块 grep Transaction|Begin()|Commit|Rollback 无命中
  • 证据
    	err = impl.DBService.Create(&apply).Error        // 步骤 1建申请
    	...
    	err = impl.DBService.Create(&msg).Error          // 步骤 2建留言
    	...
    	err = impl.DBService.Model(models.FriendApply{}).Where("id=?", apply.ID).UpdateColumn("last_message_id", msg.ID).Error  // 步骤 3
    
  • 影响:任一步失败即留下半成品状态且接口返回 ErrDB(客户端重试会再插一条申请/留言,造成重复数据);例如“留言已创建但 last_message_id 未回写”,列表展示的消息永远停留在旧的一条;tag_do_create 中标签建好而成员批量插入失败会留下空标签。仓库无事务代码,也没有补偿/幂等设计。
  • 建议:用 impl.DBService.Transaction(func(tx *gorm.DB) error {...}) 包裹多步写;跨表更新使用 tx 传递;辅以唯一约束把重试变为幂等。

22. relation_extend 跨模块直连且无状态/软删过滤,字段名与本模块约定不一致

  • 位置internal/logic/common/const.go:20,26,166
  • 证据
    	err = impl.DBService.Table("relation_extend").Select(Filed).Where("relation_identity = ?", identity).First(&card).Error
    	err = impl.DBService.Table("relation_extend").Select(Filed).Where("relation_id in ?", ids).Find(&cards).Error
    
  • 影响relation_extend 在本模块无模型、无迁移、无归属声明(全仓库 grep 仅这 3 处引用),字段含义只能靠 SQL 字符串推断,任何上游表结构调整都会静默破坏本模块(且 Filed 里的 nickname 与 match SQL 里的 pe.name 互不一致);查询不带 status/deleted_at 条件,推测已注销/封禁用户的资料卡片仍会被返回(status 语义见 SDK types.Std_IICUDS:“-1禁止1正常”。分层上这是“关系服务直接读他人领域表”违反服务边界。
  • 建议:通过上游服务 RPC 或明确的只读视图获取资料卡片;至少补齐 status=1 AND deleted_at IS NULL 过滤,把列名约定集中为常量并与上游对齐;为跨模块读取加契约测试。

23. 时间格式化依赖进程时区,与 DSN 中 TimeZone=Asia/Shanghai 可能不一致

  • 位置internal/logic/common/const.go:80,87etc/relation_prod.yaml:7
  • 证据
    			CreatedAt:        item.CreatedAt.Local().Format("2006-01-02 15:04:05"),
    		applyItem.CreatedAt = item.CreatedAt.Local().Format("2006-01-02 15:04:05")
    
        - host=127.0.0.1 user=postgres password=CHANGE_ME dbname=bsm_dev port=5432 sslmode=disable TimeZone=Asia/Shanghai
    
  • 影响CreatedAt 由驱动按 DSN 的 Asia/Shanghai 解析,随后 .Local() 再按容器进程 TZ 转换;容器通常为 UTC会导致申请/好友时间整体偏移 8 小时(跨时区部署时更不可控),且接口契约是字符串时间(ApplyItem.created_at),客户端无法自行纠正。
  • 建议:统一使用 time.UTC 或显式 time.LoadLocation("Asia/Shanghai"),或在 proto 中改用 google.protobuf.Timestamp/Unix 时间戳;容器与 DSN 时区纳入部署校验。

24. 配置校验与默认值不安全:关键配置缺失即 panic/退出、无效配置项、无健康检查、无优雅退出

  • 位置internal/config/config.go:46-61etc/relation_prod.yaml:31-35,43etc/relation_dev.yaml:12-16cmd/main/main.go:37-43internal/server/new.go:32
  • 证据
    	conf.NotNil(Spec.Service, Spec.Cache)   // 未校验 Databases / Gateway / Etcd 的一致性
    
    # relation_prod.yaml:31-35   SrvConfig 中没有 yaml:"KycConf" 字段,该段被静默忽略
    KycConf:
      BaseUrl: 123
    # relation_dev.yaml:12-16    匿名白名单指向不存在的服务方法;本模块只注册了 Follow/Friend/Match
    MicroService:
      Enable: false
      Anonymous:
        - relation.ping.hello
    
    // cmd/main/main.go:40-43  defer 在 select{} 阻塞下永不执行
    	defer srv.Stop()
    	srv.Start()
    
  • 影响aDatabases 未配置时 with.Databases(nil,...)impl.NewImpl() 阶段 panic("No Database Source Found !"),而 NotNil 没有覆盖它错误信息不指向配置键bEtcd 段在 dev/test/prod 全部缺失,一旦有人打开 MicroService.Enable 就会在 service.Start()os.Exit(1)SDK core/service/service.go:66-70故障定位成本高cprod 的 KycConf 段完全无效(无对应结构字段),Kyc/WeChatConf 残留 CHANGE_ME 占位与 AppID: 123dAnonymous 白名单里的 relation.ping.hello 不存在(模块未注册任何 Ping/Health 服务,server/new.go:28-32),即没有健康检查 RPCesrv.Start()select{} 永久阻塞且无 signal.Notifydefer srv.Stop() 永远不会执行 → 无优雅退出,GracefulStop 形同虚设supervisor 停止时直接断连。
  • 建议:为所有必需配置项补 NotNil/显式校验并在启动期输出清晰错误;删除 prod 无效段与占位密钥;实现 grpc_health_v1 健康检查并在三份配置里登记为匿名方法;在 main 中监听 SIGINT/SIGTERM 调用 srv.Stop()(把 select{} 换成信号阻塞)。

25. 标签名称与成员统计的字段映射缺陷(TagName 恒空、FriendTotal 恒 0

  • 位置internal/logic/friend/tag_fetch.go:22internal/logic/common/const.go:134;字段定义 pb/friend.pb.goTagItem{Id, Identity, FriendTotal, TagName}
  • 证据
    	err = impl.DBService.Model(models.FriendTag{}).Where("relation_id=?", id).Order("id asc").Scan(&reply.Tags).Error
    
    	types.Std_IICUDS
    	types.Std_Passport        //标签归属
    	Name               string `gorm:"column:name;type:varchar(255);not null;" json:"name"`
    
  • 影响relation_friend_tag.name 需要映射到 pb 字段 TagNameGORM 默认列名为 tag_name),当前 Scan 未做别名映射,推测标签名会扫描为空;friend_total 没有任何统计 SQLrelation_friend_in_tag 计数从未查询),恒为 0前端标签成员数永远显示 0。注意 TagFetch 目前会先在 P0-1 处 panic此问题在修复 P0-1 后才对外可见。
  • 建议:改用明确投影(Select("id,identity,name as tag_name"))或引入 DTO用一条 GROUP BY tag_identity 的聚合查询填充 friend_total;补测试固定该字段契约。

26. 申请接口参数可任意组合、无自申请/好友校验,且对不存在记录返回“假成功”

  • 位置internal/logic/friend/apply_do.go:23-34apply_do_pass.go:43-47apply_do_reject.go:26-30
  • 证据
    	if in.ToIdentity == "" || in.Body == "" || in.ToId == 0 { return nil, errcode.ErrInvalidArgument }
    	apply := models.FriendApply{ FromID: uint(auth.ID), FromIdentity: auth.Identity, ToID: uint(in.ToId), ToIdentity: in.ToIdentity, ... }
    
    // apply_do_pass.go:43  Update 不校验影响行数0 行也返回 OK
    	err = impl.DBService.Model(models.FriendApply{}).Where("to_identity=? and identity=?", auth.Identity, in.ApplyIdentity).UpdateColumn("status", 1).Error
    
  • 影响ToIdToIdentity 由客户端分别提供且不做一致性校验(可构造 to_id 属于 A、to_identity 属于 B 的申请,污染双方数据,后续按 to_id 查列表、按 to_identity 审批会出现跨人错配);不校验“是否已是好友/是否申请自己” → 可给自己发申请、对已是好友的人反复申请;ApplyDoPass/ApplyDoReject 对不存在的 apply_identity 返回 Data:"OK",掩盖失败。
  • 建议:以 to_identity 为准在服务端解析 to_id(或直接去掉 to_id 入参),校验 to_identity != auth.Identity、目标存在、双方非好友;写操作检查 RowsAffected0 行返回 ErrNotFound

27. Follow.State 返回语义混用("OK" 与关注行 UUID 混在同一字段),并泄漏内部主键

  • 位置internal/logic/follow/state.go:28-45
  • 证据
    	if err == gorm.ErrRecordNotFound {
    		return &pb.DataStatusReply{ Data: vars.OK, ... }, nil    // 未关注 → "OK"vars.OK == "OK"
    	}
    	return &pb.DataStatusReply{ Data: data.Identity, ... }, nil  // 已关注 → 关注行 UUID
    
  • 影响:同一 data 字段既承载状态常量又承载数据库行 UUID客户端必须字符串比较 "OK" 才能判断状态,契约脆弱;行 UUID 外泄后可用于构造 P1-4 的越权删除。DataStatusReply 被本模块所有写接口复用,语义不统一已成系统性问题。
  • 建议:为关注状态定义明确的枚举(或返回布尔/status 字段),内部行 UUID 不出接口;需要幂等键时单独定义字段并做归属校验。

28. 明确的空实现/模板残留与“注释承诺但未实现”的能力

  • 位置internal/logic/friend/search.go:12-21internal/logic/follow/do.go:14-33internal/logic/friend/tag_member_fetch.go:26-41apply_do.go:63apply_do_message.go:46apply_do_pass.go:41apply_do_reject.go:32
  • 证据
    // friend/search.go:18-21  "搜索" RPC只校验 keyword 非空,返回空结构体
    	if in.Keyword == "" { return nil, errcode.ErrInvalidArgument }
    	return &pb.PartFriendReply{}, nil
    
    // follow/do.go:26-31  模板 TODO返回假成功该函数未被 server 注册server/follow_server.go 只注册 Doing/Undo/State/Fetch
    	// TODO: add your logic code & delete this line.
    	return &pb.DataStatusReply{ Data: vars.OK, ... }, nil
    
    // apply_do.go:63 等 4 处:注释声明要发 MQ 推送与更新 Cache函数体无任何实现全模块 grep EventMQ|queue|MQ|Publish 仅命中这些注释)
    	//EventMQ向mesh MQ中心发送报文请求推送消息以及更新Cache.
    
  • 影响Friend.SearchREADME.md:199 列为“主要功能:好友搜索”)实际返回空列表,前端会呈现“搜索无结果”而非“功能不可用”,问题被掩盖;follow.Do 是死代码且返回假成功(proto/follow.proto:7-19 未声明 Do,仅有同名文件);TagMemberFetch 因列名错误P1-2Pluck 目标错误(见下)根本无法返回成员;好友申请的“推送 + 缓存更新”完全缺失,导致无实时通知、通过/拒绝后缓存不刷新(同款模板 // TODO: add your logic code & delete this line. 与裸 return 亦见于 module/ec/address/internal/logic/library/fetch.go:38-40,属模板级共性问题)。
    • TagMemberFetch 附加证据(tag_member_fetch.go:26-33Pluck("relation_id", ids)ids 是值传递的 nil 切片而非指针GORM 的 dest 需为指针),且应取 friend_identity、应查 relation_friend_in_tag;随后 GetrelationInfoDetailCardById(ids) 必然以空 IN 查询返回空列表。
  • 逐条对照README 声称的能力 vs 方法名 / 文件行号 / 实际行为)
README:199 声称能力 方法 文件:行 实际行为(证据)
好友搜索 Friend.Search internal/logic/friend/search.go:18-21 只校验 keyword 非空后 return &pb.PartFriendReply{}, nil——空实现,永不返回结果
好友列表 Friend.Fetch internal/logic/friend/fetch.go:35,51 有实现,但版本命中缓存时返回 nil备注/置顶丢失、total 未赋值P2-18
好友详情 Friend.Get internal/logic/friend/get.go:23 有实现,无关系/隐私校验P1-13
备注修改 Friend.ModifyNickname internal/logic/friend/modify_nickname.go:26 identity=?(行 UUID 列)定位 + 更新不存在的 nickname 列 → 必然失败仍返回 OKP1-3
常用好友标记 Friend.DoPopular / UndoPopular do_popular.go:27 / undo_popular.go:27 定位条件错误 → 匹配 0 行置顶永不生效P1-3
删除好友 Friend.Delete internal/logic/friend/delete.go:27 同上0 行受影响仍返回 OKP1-3
好友申请 / 申请留言 Friend.ApplyDo / ApplyDoMessage apply_do.go:23-61 / apply_do_message.go:33-44 有实现无幂等、无限流P1-11留言可越权写任意申请P1-5
申请通过 / 拒绝 Friend.ApplyDoPass / ApplyDoReject apply_do_pass.go:29-47 / apply_do_reject.go:26 通过只写单向好友行、无事务、对不存在申请假成功P1-10、P2-26
好友标签 / 标签成员 Friend.TagFetch / TagMemberFetch / TagDoCreate / TagDoUpdate tag_fetch.go:22tag_member_fetch.go:27tag_do_create.go:41tag_do_update.go:39 TagFetch 必 panicP0-1TagMemberFetch 恒返回空;创建/更新可越权写他人标签P1-6
关注 / 取消关注 Follow.Doing / Follow.Undo doing.go:28-38 / undo.go:26 Doing 可重复关注;Undo 定位列错误 + 无归属校验 → 取消关注永不生效P1-4
关注状态 / 关注列表 Follow.State / Follow.Fetch state.go:31-45 / fetch.go:37-49 有实现;State 语义混用 "OK"/UUIDP2-27非法 direction 静默返回空
匹配记录查询 Match.Fetch common/const.go:155-172 原生 SQL 语法不成立 → 恒 ErrDBP1-8relation_match 无写入方P1-9
加入 / 忽略 Match.DoJoin / Match.DoIgnore do_join.go:29 / do_ignore.go:27 写错表:分别向 relation_follow 插入/删除从未维护匹配记录P1-9
proto 未声明,模板残留) follow.Do internal/logic/follow/do.go:26-31 // TODO: add your logic code & delete this line. 后直接返回 Data: vars.OK——死代码 + 假成功;server/follow_server.go:19-35 未注册该方法
(注释承诺的副作用) ApplyDo / ApplyDoMessage / ApplyDoPass / ApplyDoReject 均含 apply_do.go:63apply_do_message.go:46apply_do_pass.go:41apply_do_reject.go:32 //EventMQ向mesh MQ中心发送报文请求推送消息以及更新Cache. 之后无任何代码;全模块 grep EventMQ|queue|MQ|Publish 只命中这 4 条注释 → 推送与缓存更新均未实现
  • 建议:未实现能力一律返回 codes.Unimplemented/ErrNotImplemented,不要返回空成功;删除或补全 follow.Do实现申请通知MQ/推送)与缓存失效;按上表把每个 RPC 的真实状态写入模块 README。

29. 测试完全缺失(无任何 _test.gotest/lint 为空目录)

  • 位置module/social/relation/test/lint/0 个文件);全仓库 module/social/**/*_test.go 无命中
  • 证据
    Get-ChildItem module/social/relation/test -Recurse -Force → 仅空目录 test\lint
    glob module/social/**/*_test.go → No files found
    
  • 影响P0/P1 这类必现缺陷nil panic、列名不匹配、单向好友、缓存命中返回 nil本可由最简单的单测/集成测试拦下,当前零覆盖;go vetgofmt 均通过(见第 2 节),说明常规静态检查无法覆盖这些运行期与语义缺陷。最小缺失用例清单:① TagFetch/ApplyGet 非 panic 且返回非 nil 响应;② Doing 重复调用只产生一条关注边、UndoState 回到未关注;③ 好友对称性(ApplyDoPass 后双方 Friend.Fetch 均含对方);④ 越权矩阵(ApplyGet/ApplyDoMessage/TagDoUpdate(DEL) 用他人 identity 必须 PermissionDenied);⑤ 缓存命中路径(version 相同必须返回与首次一致的数据);⑥ 并发 Doing 不产生重复行;⑦ CollectionFriendData 返回的 remark_name/popular 与 DB 一致、total 正确;⑧ Match.Fetch 的 SQL 能真实执行(可用 sqlmock/真实库);⑨ 时区固定用例。
  • 建议:为每个 RPC 建立 handler 级测试(含 JWT metadata 构造)与关键 SQL 的集成测试;把上述 9 类用例纳入 CI 门禁,并新增“未实现 RPC 必须返回 Unimplemented”的契约测试。

30. cmd/cli 将含口令的数据库 DSN 打印到终端

  • 位置cmd/cli/main.go:9-12
  • 证据
    	config.New("relation")
    	fmt.Println(config.Spec.Databases)
    
  • 影响DSN 内含 user/password/host/dbnameetc/relation_prod.yaml:7),任何执行该 CLI 的环境(含 CI、跳板机、共享终端都会把连接串写入终端记录/日志;结合 conf.PrintInfo 与 GORM DebugP1-16共同构成敏感信息外泄面。
  • 建议:移除该打印,或仅输出 Driverlen(Source);确需诊断时输出脱敏 DSN。

P3

31. 空文件与死代码/笔误

  • 位置internal/models/base.go:1internal/models/query.go:1internal/logic/follow/do.go:14internal/impl/impl.go:16,21
  • 证据
    // models/base.go、models/query.go 全文只有一行
    package models
    
    // impl.go:16,21  MemorySerice 拼写错误,且该内存缓存全模块从未被使用(依赖 github.com/patrickmn/go-cache 仅为它引入)
    	MemorySerice *cache.Cache
    	MemorySerice = with.Memory(nil)
    
  • 影响:空文件与未使用依赖增加维护噪声;follow.Do 是无调用者的模板残留(含 TODO容易被误当作入口README 已提示需与 follow.Doing 区分);MemorySerice 拼写错误一旦被外部引用将无法修复兼容。
  • 建议:删除空文件与 follow/do.go,或将其改为调用 Doing 的显式别名;修正拼写并移除未使用依赖。

32. 魔法数字与字符串枚举散落各处

  • 位置internal/logic/common/const.go:44maxSize = 50)、follow/fetch.go:31,35"MY"/"WHO")、tag_do_update.go:29,38"ADD"/"DEL")、apply_do_pass.go:43status=1)、apply_do_reject.go:26status=-1)、do_popular.go:27/undo_popular.go:27popular=1/0)、follow/fetch.go:46PageSize=20
  • 证据
    	case "MY": // 我关注的
    		column = "to_identity"
    	case "WHO": // 关注我的
    
    	switch strings.ToUpper(in.Direction) {
    	case "ADD":
    	case "DEL":
    
  • 影响:关系类型/方向/状态以裸字符串与裸数字表达,客户端与服务端只能靠约定同步,拼写错误(如 "my" 之外的空值)会静默返回空结果(follow/fetch.go:37-39 直接返回空 reply 且无错误),状态值 1/-1 与 SDK vars.NormalStatus/DisabledStatus 重复定义。proto 中也没有相应枚举。
  • 建议:在 proto 中定义 Direction/Status 枚举(或 common 包集中定义常量并做参数校验),非法值返回 ErrInvalidArgument 而不是空成功。

33. README/wiki 与实现不一致,模块文档为空白模板

  • 位置module/social/relation/README.md:1-2README.md:196-202wiki/api/15-relation.md:9-11
  • 证据
    # relation
    
    - **主要功能**:好友搜索、列表、详情、备注修改、常用好友标记与删除;好友申请、申请留言、通过和拒绝;好友标签及标签成员;关注、取消关注、关注状态与列表;匹配记录查询、加入和忽略。
    - **边界与协作**... `Match` 的接口主要维护匹配记录,不应据此认定存在完整推荐算法。
    
    - 动态 HTTP`POST /rpc/relation/{Service}/{Method}`
    - grpc-gateway`POST /relation.{Service}/{Method}`
    
  • 影响:仓库 README 把“好友搜索”“备注修改”“删除好友”“取消关注”“匹配记录维护”列为已实现能力,实际分别为空实现/列名错误/定位错误/无写入方(逐条对照见 P2-28 表格与 P1-2/3/4/9wiki 只描述动态 HTTP 路径,未说明本模块自带 grpc-gateway 不可用(属全局模板缺陷,见 P1-15 一句话说明);模块 README 为空,运维与新人无落地依据。
  • 建议:在 README 中逐 RPC 标注实现状态(可用/部分可用/占位)与已知缺陷编号;补充数据表清单、列名约定、依赖的上游表与调用鉴权要求。

34. 工程化与运维配置缺陷root 运行、占位密钥、无 CI、依赖 replace 相对路径、命名返回未初始化的统一模式)

  • 位置etc/supervisor.bsm-social-relation.conf:6etc/relation_prod.yaml:7,10,21,27,28,43go.mod:69internal/logic/friend/{tag_fetch,apply_get,fetch,apply_fetch}.go
  • 证据
    user=root          # supervisor 以 root 运行
    stdout_logfile=/data/app/logs/social-relation.log
    
    Cache: redis://null:CHANGE_ME@127.0.0.1:6379/     # prod 也是占位口令 + 明文 redis 协议
    SecretKey: CHANGE_ME
    
    replace git.apinb.com/bsm-sdk/core => ../../../../../bsm-sdk/core
    
  • 影响:进程以 root 运行、日志重定向文件(配合 P1-16 的 SQL Debug 形成 PII 落盘prod 配置为占位值(Cache: redis://null 主从/哨兵缺失、无 TLSgo.mod 用相对路径 replace 使模块无法脱离本仓库路径独立构建(跨机/CI 构建易失败);reply 命名返回“声明即使用”的模式在 4 个文件中重复出现P0-1 的共同根因),缺少统一约定与 lint 规则。
  • 建议:改为专用低权限账号运行、日志接入集中式采集并对 body 脱敏;密钥从环境/密钥管理注入Redis 启用认证与 TLSSDK 依赖改为版本化(去掉相对路径 replace并固定 CI在 golangci-lint 中启用 nilerr/staticcheck 等规则,禁止未初始化命名返回指针被成员访问。

4. 推荐优化方案

按“先止血、再修数据面、后补工程能力”的顺序推进:

  1. 可用性止血(当天)
    • internal/server/new.gogrpc.NewServer() 上安装 grpc.ChainUnaryInterceptor(recovery, 结构化日志, 超时, 限流),确保任何 handler panic 不再终止进程;同时修复 P0-1 的两处 nil 响应初始化。
    • 修复 fetch.go:48/apply_fetch.go:48err/cacheErr 混淆与缓存命中返回 nilP1-7
    • 关闭生产 GORM Debug 并移除 CLI 的 DSN 打印P1-16、P2-30
  2. 数据面一致性与权限1~2 周)
    • 统一列名约定(passport_idrelation_id,二选一并全量替换),修复 P1-2/P1-3/P1-4 的定位条件;上线前用真实库跑通 Follow/Friend/Tag 全链路。
    • 为所有写接口补归属校验(ApplyGet/ApplyDoMessage/TagDoUpdate/Undo与黑名单前置过滤P1-4/5/6/12
    • 用事务重写 ApplyDo/ApplyDoPass/TagDoCreate并补齐双向好友行、幂等与唯一索引P1-10/11、P2-21
    • 撤销/忽略关注与匹配记录按业务重新建模:relation_match 明确写入方,DoJoin 建好友、DoIgnore 标记忽略P1-9
  3. 性能与容量(与数据面并行)
    • 建索引P1-14、给列表接口加统一分页与上限P2-17、修正 CollectionFriendData 的字段与合并逻辑并补 totalP2-18/19
    • 缓存改为带 TTL + 写路径失效 + 降级回源版本号改用业务版本P2-20
  4. 工程化(持续)
    • 用迁移脚本管理表结构与索引,接入上游资料服务替代直连 relation_extendP2-22
    • 按第 3 节 P2-29 的 9 类用例建立 CI 门禁:越权矩阵、对称性、幂等/并发、缓存命中、SQL 可执行性、时区、未实现 RPC 契约。
    • 配置校验前置(必需项缺失时输出键名并退出)、补健康检查 RPC 与信号优雅退出P2-24、README 逐 RPC 标注实现状态P3-33

5. TODO 清单

  • P0-1 修复 TagFetch/ApplyGet 的 nil 指针 panic初始化 reply),并在 grpc.NewServer() 安装 recover 等拦截器|验收:两个 RPC 用真实库调用返回非空响应且进程不退出;故意 panic 的注入测试不会杀死进程|涉及:module/social/relation/internal/logic/friend/tag_fetch.go:15module/social/relation/internal/logic/friend/apply_get.go:16module/social/relation/internal/server/new.go:23
  • P1-2 统一 relation_friend/relation_friend_in_tag/relation_friend_tag 的归属列命名(模型 passport_id 与查询 relation_id/group_identity 二选一)|验收:好友列表、删除好友、标签列表、标签成员在真实库上跑通且返回预期数据|涉及:module/social/relation/internal/models/relation_friend.go:10module/social/relation/internal/logic/common/const.go:120module/social/relation/internal/logic/friend/tag_member_fetch.go:27
  • P1-3 好友行写操作改用 friend_relation_identity(与 passport_id)定位,nickname 改为 remark_name,并对 0 行受影响返回错误|验收:删除好友/改备注/置顶后重新查询结果符合预期,且对非好友返回明确错误|涉及:module/social/relation/internal/logic/friend/delete.go:27module/social/relation/internal/logic/friend/modify_nickname.go:26module/social/relation/internal/logic/friend/do_popular.go:27
  • P1-4 Follow.Undo 改为按 from_identity=auth.Identity AND to_identity=in.Identity 删除,并停止在 State 中返回行 UUID验收取消关注后 State 返回未关注、他人关注边不受影响(越权用例返回 PermissionDenied涉及module/social/relation/internal/logic/follow/undo.go:26module/social/relation/internal/logic/follow/state.go:43
  • P1-5 ApplyGetApplyDoMessage 增加申请参与方校验|验收:非参与方读取/留言返回 PermissionDenied参与方行为不变涉及module/social/relation/internal/logic/friend/apply_get.go:26module/social/relation/internal/logic/friend/apply_do_message.go:40
  • P1-6 TagDoUpdate(DEL) 追加 passport_id=auth.ID 归属条件,TagDoCreate 校验成员是否为本人好友与标签归属,并加唯一索引|验收:无法删除他人标签成员、重复 ADD 不产生重复行|涉及:module/social/relation/internal/logic/friend/tag_do_update.go:39module/social/relation/internal/logic/friend/tag_do_create.go:41
  • P1-7 修正 Fetch/ApplyFetch 缓存逻辑:版本命中返回缓存、失败降级回源、cacheErr 打印正确变量|验收:version 命中时返回与首次一致的数据Redis 写失败不 panic 且接口仍可用|涉及:module/social/relation/internal/logic/friend/fetch.go:35module/social/relation/internal/logic/friend/fetch.go:48module/social/relation/internal/logic/friend/apply_fetch.go:48
  • P1-8 重写 GetrelationInfoDetailCardByMatch 的 SQL合法 join、ORDER BY 前置、字段名与 Filed 对齐)|验收:Match.Fetch 对预置数据返回匹配对象且无 SQL 错误|涉及:module/social/relation/internal/logic/common/const.go:155
  • P1-9 明确 relation_match 写入方;DoJoin 建好友关系、DoIgnore 标记忽略并加归属条件|验收:DoJoin 后双方好友列表含对方,DoIgnore 后该匹配不再出现在列表|涉及:module/social/relation/internal/logic/match/do_join.go:29module/social/relation/internal/logic/match/do_ignore.go:27
  • P1-10 ApplyDoPass 改为事务内“校验申请状态 → 双向写好友行 → 更新状态”,对重复/不存在申请返回错误|验收:通过后双方 Friend.Fetch 均含对方;同一申请重复调用不产生第二条好友行|涉及:module/social/relation/internal/logic/friend/apply_do_pass.go:29
  • P1-11(from_identity,to_identity) 等关系对建唯一索引/约束,Doing/ApplyDo 幂等化并禁止自关注|验收:并发/重复调用只产生一条记录;同一用户对同一目标的申请有冷却限制|涉及:module/social/relation/internal/models/relation_follow.go:10module/social/relation/internal/logic/follow/doing.go:34
  • P1-12 新增拉黑模型与前置校验,覆盖 Doing/ApplyDo/Get/Fetch验收A 拉黑 B 后 B 无法关注、无法申请、无法获取 A 的资料卡片|涉及:module/social/relation/internal/logic/follow/doing.go:28module/social/relation/internal/logic/common/const.go:20
  • P1-13Friend.Get/Match.Get 增加关系与可见性校验、黑名单过滤与限流|验收:非好友/非匹配对象查询返回 PermissionDenied单位时间内的资料查询有配额限制涉及module/social/relation/internal/logic/friend/get.go:23module/social/relation/internal/logic/match/get.go:24
  • P1-14 为关系热点列补齐索引并纳入版本化迁移脚本|验收:EXPLAIN 显示关注/粉丝/好友/标签/匹配查询走索引CI 中有可重复执行的迁移脚本|涉及:module/social/relation/internal/models/relation_follow.go:10module/social/relation/internal/models/relation_friend.go:12
  • P1-15 修复或关闭自带 grpc-gateway全局模板缺陷的本模块表现补齐 module/social/relation/service/expose.go(内部初始化 Mux 并调用 pb.Register*HandlerServer,参照 module/ec/address/service/expose.go:18-26),或把三份配置的 Gateway.Enable 置为 false|验收:配置开启时 HTTP 路由可用,或端口不再监听且无 ServeMux panic 日志|涉及:module/social/relation/service/(空目录)、module/social/relation/internal/server/new.go:20module/social/relation/cmd/main/main.go:32module/social/relation/etc/relation_prod.yaml:38
  • P1-16 生产关闭 GORM Debug、移除 CLI 的 DSN 打印、对申请正文脱敏|验收:生产日志中不含 SQL 参数与 DSNcmd/cli 输出无口令|涉及:module/social/relation/internal/impl/impl.go:23module/social/relation/cmd/cli/main.go:11
  • P2-17 为所有列表接口统一分页与上限校验(含 Friend.Fetch、深分页、IN 分片)|验收:page_size 超限被拒绝或截断;超大 offset 不报错;好友列表可分页获取|涉及:module/social/relation/internal/logic/follow/fetch.go:45module/social/relation/internal/logic/common/const.go:120
  • P2-18 修正 CollectionFriendDataids 字段与备注/置顶合并逻辑、补 total、稳定排序|验收:好友列表卡片与关系一一对应,remark_name/popular/total 与 DB 一致且顺序稳定|涉及:module/social/relation/internal/logic/common/const.go:123module/social/relation/internal/logic/common/const.go:141
  • P2-19 申请列表去硬编码并返回 total|验收:超过 50 条申请可分页查看,total 准确|涉及:module/social/relation/internal/logic/common/const.go:44module/social/relation/internal/logic/common/const.go:103
  • P2-20 缓存加 TTL/随机抖动、写路径失效、失败降级回源、版本号改用业务版本验收key 有过期时间新增留言会驱动客户端刷新Redis 不可用时接口仍可返回 DB 数据|涉及:module/social/relation/internal/logic/common/const.go:182module/social/relation/internal/logic/friend/apply_fetch.go:30
  • P2-21 多步写操作纳入事务|验收:注入中途失败后数据库无半成品状态(申请/留言/状态一致)|涉及:module/social/relation/internal/logic/friend/apply_do.go:36module/social/relation/internal/logic/friend/tag_do_create.go:34
  • P2-22 用上游服务/只读视图替代直连 relation_extend,并补状态与软删过滤|验收:资料卡片查询带 status=1 AND deleted_at IS NULL,且不依赖未登记的物理表|涉及:module/social/relation/internal/logic/common/const.go:20
  • P2-23 统一时间语义UTC 或固定时区,或改用 Timestamp验收容器时区改为 UTC 时接口返回时间不变|涉及:module/social/relation/internal/logic/common/const.go:80
  • P2-24 配置校验前置、清理无效/占位配置、补健康检查 RPC 与信号优雅退出|验收:缺少 Databases/Etcd 时启动即报明确键名;存在可匿名调用的健康检查;SIGTERM 触发 GracefulStop|涉及:module/social/relation/internal/config/config.go:56module/social/relation/cmd/main/main.go:40module/social/relation/etc/relation_prod.yaml:32
  • P2-25 修正标签名称/成员统计映射|验收:TagFetch 返回非空 tag_namefriend_total 与实际成员数一致|涉及:module/social/relation/internal/logic/friend/tag_fetch.go:22module/social/relation/internal/logic/common/const.go:134
  • P2-26 申请接口参数一致性与存在性校验、去除假成功|验收:to_id/to_identity 不一致被拒绝;申请自己/已是好友被拒绝;操作不存在记录返回 NotFound涉及module/social/relation/internal/logic/friend/apply_do.go:23module/social/relation/internal/logic/friend/apply_do_pass.go:43
  • P2-27 Follow.State 定义明确状态枚举,不外泄内部行 UUID验收未关注/已关注以稳定枚举区分,响应中不含关系表主键|涉及:module/social/relation/internal/logic/follow/state.go:31
  • P2-28 未实现 RPC 明确返回 Unimplemented删除模板残留补申请通知与缓存失效逐条对照表见第 3 节 P2-28验收Friend.Search 返回 Unimplemented或真正实现follow.Do 被移除4 处 EventMQ 注释对应的推送与缓存更新有实现;模块 README 的真实状态表与代码一致|涉及:module/social/relation/internal/logic/friend/search.go:21module/social/relation/internal/logic/follow/do.go:26module/social/relation/internal/logic/friend/tag_member_fetch.go:27module/social/relation/internal/logic/friend/apply_do.go:63
  • P2-29 建立测试与 CI 门禁(第 3 节 P2-29 的 9 类用例验收CI 中单元+集成测试全绿含越权矩阵、好友对称性、缓存命中、并发幂等、SQL 可执行性用例|涉及:module/social/relation/test/lint/(当前为空)、module/social/relation/internal/logic/friend/apply_do_pass.go:29
  • P3-31 清理空文件与死代码、修正 MemorySerice 拼写与未使用依赖|验收:internal/models/base.go/query.go 删除或补齐,follow/do.go 移除,go.mod 无未使用依赖|涉及:module/social/relation/internal/models/query.go:1module/social/relation/internal/impl/impl.go:16
  • P3-32 用枚举/常量替换魔法数字与方向字符串,非法值返回参数错误|验收:方向/状态/分页常量集中定义,非法 direction 返回 InvalidArgument 而非空结果|涉及:module/social/relation/internal/logic/follow/fetch.go:37module/social/relation/internal/logic/friend/tag_do_update.go:29
  • P3-33 补全模块 README 并同步仓库 README/wiki 的实现边界|验收:模块 README 含逐 RPC 实现状态、表与列约定、依赖的上游表与鉴权说明|涉及:module/social/relation/README.md:1README.md:199
  • P3-34 运维与依赖治理(非 root 运行、密钥外置、去掉相对路径 replace、lint 规则验收supervisor 使用低权限账号;配置无 CHANGE_ME模块可脱离本地路径构建lint 禁止未初始化命名返回指针被访问|涉及:module/social/relation/etc/supervisor.bsm-social-relation.conf:6module/social/relation/go.mod:69module/social/relation/etc/relation_prod.yaml:43

6. 审计摘要(供汇总使用)

  • 问题数P0=1 P1=15 P2=14 P3=4合计 34
  • 最高风险(一句话)Friend.TagFetchFriend.ApplyGet 的命名返回指针从未初始化即被字段解引用(tag_fetch.go:22&reply.Totalapply_get.go:32reply.Data = ...,编译期即可确定的 nil 解引用)→ 必然 panicgrpc.NewServer() 未装 recover 拦截器,任何持有有效 JWT 的账号调用一次即可让整个 relation 服务进程退出(远程可用性攻击);同时好友/关注/标签的读写列名与归属条件系统性错配,导致"删除好友、取消关注、备注置顶"等操作静默假成功、好友关系永久单向。
  • 最优先 3 个动作
    1. grpc.NewServer() 安装 recovery/日志/限流拦截器,并修复 tag_fetch.go:22apply_get.go:32 的 nil 响应初始化P0-1
    2. 统一 relation_friend* 系列的归属列命名并修正写操作定位条件(friend_relation_identity/remark_name)与 Undo/DoIgnore 的删除条件P1-2/3/4
    3. ApplyGet/ApplyDoMessage/TagDoUpdate(DEL) 补归属校验(越权读写),并把 ApplyDoPass 改为事务内双向写好友关系、relation_match 明确写入方P1-5/6/9/10
  • 未能覆盖 / 无法验证的部分
    1. 数据库物理 schema仓库内无建表 SQL 或迁移脚本(*.sqlrelation_* 命中、models.InitData 被注释、IsAutoMigrate=false),因此 P1-2 中"列不存在 vs 列存在但从未写入"的最终形态、以及 P1-14 的线上索引实际状态只能人工核对生产库。
    2. relation_extend 表归属、真实列名(nickname vs name)、是否含 status/deleted_at本模块无模型、全仓库无定义P2-22、P1-8 的相关结论含有推测成分(已在正文标注)。
    3. 未运行任何写操作或数据库连接:所有 SQL 层结论基于代码与 Postgres 语法规则推导;go vet/gofmt 通过,说明这些缺陷不在编译期可见范围。
    4. 上游调用方行为未验证:无法确认客户端是否会在 ApplyDoPass 后自行补写反向好友行、是否会带回 version 命中缓存P1-7/P1-10 的影响面取决于客户端)。
    5. 未审计 pb 生成代码的正确性(仅核对字段与路由),未审计 SDK 内部实现(仅作参考读取,其自身问题如 ListenLeaseRespChan 空转、默认 Debug:true 在正文中作为放大因素提及)。
    6. HTTP 暴露路径未端到端验证:本模块 service/ 为空目录、未接入 pkgs/all 聚合网关(README.md:194),因此无法确认线上是否存在其它网关按 /rpc/relation/{Service}/{Method} 动态转发(该路径的真实可用性取决于网关侧配置,本报告只记录本模块侧事实)。
  • 证据等级约定:① 每条结论均给 file:line + 摘录;② 涉及"返回 nil"的结论一律按显式 return 分支追踪(如 P1-7 的 return reply, nil),不套用"命名返回值未赋值 → 裸 return 返回 nil"的经验判断;③ 编译期即可确定的解引用P0-1与"是否 panic"分开表述,不与 nil 返回混淆;④ 无法从源码确认的一律标注推测P1-8 的 pe.name、P2-22 的 relation_extend 列与状态过滤、P2-25 的标签名映射、P1-14 的线上索引状态),未达证据标准的不升级级别。