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\relation(gRPC + grpc-gateway 微服务,Postgres + Redis)。 证据行号均以当前工作区文件为准。

1. 模块概览

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

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

2. 审计范围与方法

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

  • 入口与配置:cmd/main/main.go、cmd/cli/main.go、internal/config/config.go、etc/relation_{dev,test,prod}.yaml、etc/supervisor.bsm-social-relation.conf、go.mod、README.md
  • 契约:proto/{const,friend,follow,match}.proto;internal/server/{new,follow_server,friend_server,match_server}.go
  • 逻辑(24 个文件):internal/logic/common/const.go;logic/follow/{do,doing,undo,state,fetch}.go;logic/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}.go;logic/match/{fetch,get,do_join,do_ignore}.go
  • 模型:internal/models/*.go(含空的 base.go、query.go)、internal/impl/impl.go
  • 参考(SDK,用于确认语义,非审计对象):D:\work\bsm-sdk\core\types\db.go(Std_IICUDS/Std_Passport 字段与索引)、core/service/{service,meta,register}.go、core/with/*.go、core/database/{new.go,sql/postgresql.go}、core/vars/status.go
  • 生成代码 pb/*.pb.go、*.pb.gw.go 仅做接口/路由/字段核对(TagItem、FriendsReply、ApplyItem、FriendApplyGetReply、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:15(reply *pb.FriendTagsReply 命名返回,全函数从未赋值)、:22(Count(&reply.Total))
    • module/social/relation/internal/logic/friend/apply_get.go:16(reply *pb.FriendApplyGetReply 命名返回,全函数从未赋值)、:32(reply.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 进程退出(supervisor 的 autorestart=true 会造成反复重启抖动,期间 Follow/Friend/Match 全部不可用)。任何持有有效 JWT 的账号调用一次 Friend.TagFetch 或 Friend.ApplyGet 即可触发,属远程可用性攻击面。
  • 证据强度说明(已复核,非推断):两处都是编译期即可确定的 nil 指针字段解引用——:22 计算 &(*reply).Total、:32 执行 (*reply).Data = ...,panic 发生在到达文件末尾那个裸 return(tag_fetch.go:27、apply_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{};(3)CI 增加 staticcheck(SA5011 类)与 handler 级单元测试,禁止未初始化命名返回指针被使用。

P1

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

  • 位置:internal/models/relation_friend.go:10、internal/models/friend_in_tag.go:10、internal/models/friend_tag.go:10 vs internal/logic/common/const.go:120,128,134、internal/logic/friend/delete.go:27、do_popular.go:27、undo_popular.go:27、tag_fetch.go:22、tag_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-32 写 PassportID/PassportIdentity → passport_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:27、modify_nickname.go:26、do_popular.go:27、undo_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.identity 在 follow/state.go:29、friend/get.go:23 中被明确当作“目标用户 identity”,而 relation_friend.identity 是行 UUID(Std_IICUDS.Identity,uniqueIndex),两者永不可能相等 → 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
    
  • 影响:(a)relation_follow.identity 是行 UUID,而 in.Identity 是目标用户 identity → 取消关注永远删不到行(配合 Doing 无去重,重复关注会不断累积);(b)SQL 完全没有 from_identity = auth.Identity 约束,若调用方传入某条关注行的 UUID(该 UUID 会通过 follow/state.go:42-45 的 Data: 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,26、internal/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.identity(UUID)即可读取两个陌生人之间的申请与私聊内容;ApplyDoMessage 可向任意申请会话注入留言并篡改 last_message_id,破坏他人会话(且 Body 会进入对方申请列表展示,可用于骚扰/钓鱼)。同文件 apply_do_reject.go:26、apply_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-39、internal/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.ID;ADD 校验目标 friend_identity 是否存在于调用者的 relation_friend;DEL 追加 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-51、internal/logic/friend/apply_fetch.go:21-51
  • 证据:
    // fetch.go:29-51(apply_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/ApplyFetchReply(total=0、无好友/无申请),表现为“好友列表突然清空”;同时 GetCache 的结果被读入 cache 却从未返回,缓存形同虚设(in.Version == 0 时还会重复执行一次全量 CollectionFriendData,见 fetch.go:21-40)。(b)Redis 写失败(SetCache 出错)时 cacheErr != nil,而第 48 行打印的是恒为 nil 的 err,触发 nil 接口方法调用 panic(与 P0-1 同样无 recover),即 Redis 抖动会升级为进程崩溃。
  • 证据强度说明(已复核):本条 (a) 的 nil 返回是按显式 return reply, nil(fetch.go:51、apply_fetch.go:51)逐分支追踪得出的,不是"裸 return"推断:in.Version != 0 时第 21-27 行的赋值分支被整体跳过,第 35 行的重算分支仅在 in.Version != cache.Version 时执行,两条路径都不成立时 reply 必为 nil。common.GetCache 侧不是 nil(common/const.go:196 的 json.Unmarshal(..., &reply) 会对 **T 分配新对象),因此 cache.Version 可安全访问、panic 只可能来自第 48 行。
  • 建议:版本命中时 return cache, nil;printer.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 pe,Postgres 报 table name "pe" specified more than once;即使修掉别名,Order by 出现在 Limit 之后也是语法错误(syntax error at or near "Order")。因此 Match.Fetch(match/fetch.go:41)必然返回 ErrDB,匹配列表功能完全不可用。附带问题:SELECT 用 pe.name,而本模块统一的卡片字段是 nickname(const.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:34、internal/logic/common/const.go:166(只读);internal/logic/match/do_join.go:29-35、internal/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 的接口主要维护匹配记录”与实现不符。(b)DoJoin 只是“关注”了对方(且无去重,见 P1-11),并未建立好友关系,与 RPC 契约“加为好友”不符,也没有把匹配记录置为已通过(重复推荐无法消除)。(c)DoIgnore 删除的是 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.Identity、status 待处理)→ 双向写好友行(双方各一行,session_id 用 common.UniqueSessionID 一致)→ 更新申请状态”,失败整体回滚;对 (passport_id, friend_relation_identity) 建唯一索引;RowsAffected==0 返回 ErrNotFound/ErrInvalidArgument。

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

  • 位置:internal/logic/follow/doing.go:24-38、internal/logic/friend/apply_do.go:23-40、internal/models/relation_follow.go:8-12、internal/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_follow、relation_friend、relation_match、friend_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 前置校验钩子,在 Doing、ApplyDo(双向)、Get、Match.Get、ApplyFetch、Friend.Fetch 上统一过滤;拉黑时同时撤销关注边与在途申请。

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

  • 位置:internal/logic/friend/get.go:14-29、internal/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 空间做批量爬取/用户画像收集(用户枚举风险)。ApplyGet(P1-5)与 TagMemberFetch 也会放大该数据出口。
  • 建议:Friend.Get 限制为“我的好友或与我存在申请关系”,Match.Get 限制为“我的匹配记录中的对象”;引入可见性字段(谁看得到地区/生日)与黑名单过滤;对外暴露的批量/单条资料接口加限流与审计日志。

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

  • 位置:internal/models/relation_follow.go:10-11、relation_friend.go:11-12、friend_in_tag.go:11-12、friend_apply.go:10-14、relation_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_identity、to_identity(关注/粉丝列表、State)、friend_relation_identity、friend_identity、tag_identity、recommend_identity(匹配计数)上的查询全部走全表扫描;关注表是社交产品最大的表之一,大 V 账号的粉丝列表/状态查询会成为慢查询与连接池占用源(MaxOpenConns 默认 64,database/sql/postgresql.go 通过 SDK 默认值)。仓库内 grep 无任何 .sql/迁移文件(go.mod 无 migration 依赖,cmd/main/main.go:37 的 models.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.go、Mux 也恒为 nil → 12425 端口每个请求 panic(全局模板缺陷的本模块表现)

  • 位置:module/social/relation/service/(空目录,0 文件)、internal/server/new.go:20-34、cmd/main/main.go:29-32、etc/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-26(pb.RegisterLibraryHandlerServer(ctx, options.Gateway, server.NewLibraryServer())),本模块 service/ 目录为空、无 Expose 入口,因此 pkgs/all/internal/service/(无 relation/social 文件)也不可能聚合暴露它(与 README.md:194“社交域未接入 all 的服务注册表”一致)。
  • 影响:三份配置都开启网关(etc/relation_prod.yaml:38-40),SDK 会在此端口 http.ListenAndServe(addr, s.Opts.GatewayMux)(core/service/service.go:106-108,126),handler 为 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:23;cmd/cli/main.go:11;etc/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 stdout;cmd/cli 移除 DSN 整体打印,日志统一走结构化 logger 并对 body 等字段脱敏。

P2

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

  • 位置:internal/logic/friend/fetch.go:21(VersionRequest 仅有 version,无分页)、internal/logic/common/const.go:120,139、internal/logic/follow/fetch.go:42-49、internal/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.Fetch 的 PageSize 无上限,客户端传 page_size=9223372036854775807 时 int(PageNo-1)*int(PageSize) 溢出为负 → OFFSET -N SQL 报错,或正数时形成深分页全表扫描;PageSize=1 语义被改写为 20(边界错误)。
  • 建议:统一分页参数并加 MaxPageSize(如 100)与 PageNo 上限校验,归还 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
    
  • 影响:(a)ids 取自 relation_friend.id(行主键),而卡片表 relation_extend 按 relation_id(会员 ID)匹配——对比 CollectionFriendApply:59-64 用的是 item.FromID(会员 ID),可见此处取错字段 → 好友列表返回的卡片与好友关系不对应(错人/空卡片);(b)备注名与置顶位被自赋值抹掉,且 DB 中 remark_name 从未被写入(见 P1-3),备注功能整链路失效;(c)GetrelationInfoDetailCardById 的 IN 查询无 ORDER BY,好友列表顺序随机,Order("id desc") 形同虚设,“置顶”无法体现;(d)FriendsReply.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-201、internal/logic/friend/fetch.go:35、apply_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,客户端拿到的是过期会话摘要;(c)GetCache 失败即返回 ErrRedis(fetch.go:30-33),Redis 抖动会导致接口整体不可用,属缓存与数据源强耦合(应降级回源)。
  • 建议:设置明确 TTL(如 10~30 分钟并加随机抖动),key 前缀纳入服务/环境命名空间;所有写路径删除对应 key;版本号改为业务版本(updated_at/消息自增 ID);缓存不可用时降级直查 DB 并告警。

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

  • 位置:internal/logic/friend/apply_do.go:36-61、apply_do_pass.go:35-47、apply_do_message.go:33-44、tag_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,87;etc/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-61、etc/relation_prod.yaml:31-35,43、etc/relation_dev.yaml:12-16、cmd/main/main.go:37-43、internal/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()
    
  • 影响:(a)Databases 未配置时 with.Databases(nil,...) 在 impl.NewImpl() 阶段 panic("No Database Source Found !"),而 NotNil 没有覆盖它,错误信息不指向配置键;(b)Etcd 段在 dev/test/prod 全部缺失,一旦有人打开 MicroService.Enable 就会在 service.Start() 中 os.Exit(1)(SDK core/service/service.go:66-70),故障定位成本高;(c)prod 的 KycConf 段完全无效(无对应结构字段),Kyc/WeChatConf 残留 CHANGE_ME 占位与 AppID: 123;(d)Anonymous 白名单里的 relation.ping.hello 不存在(模块未注册任何 Ping/Health 服务,server/new.go:28-32),即没有健康检查 RPC;(e)srv.Start() 以 select{} 永久阻塞且无 signal.Notify,defer 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:22、internal/logic/common/const.go:134;字段定义 pb/friend.pb.go(TagItem{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 字段 TagName(GORM 默认列名为 tag_name),当前 Scan 未做别名映射,推测标签名会扫描为空;friend_total 没有任何统计 SQL(relation_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-34、apply_do_pass.go:43-47、apply_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
    
  • 影响:ToId 与 ToIdentity 由客户端分别提供且不做一致性校验(可构造 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、目标存在、双方非好友;写操作检查 RowsAffected,0 行返回 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-21、internal/logic/follow/do.go:14-33、internal/logic/friend/tag_member_fetch.go:26-41、apply_do.go:63、apply_do_message.go:46、apply_do_pass.go:41、apply_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.Search(README.md:199 列为“主要功能:好友搜索”)实际返回空列表,前端会呈现“搜索无结果”而非“功能不可用”,问题被掩盖;follow.Do 是死代码且返回假成功(proto/follow.proto:7-19 未声明 Do,仅有同名文件);TagMemberFetch 因列名错误(P1-2)与 Pluck 目标错误(见下)根本无法返回成员;好友申请的“推送 + 缓存更新”完全缺失,导致无实时通知、通过/拒绝后缓存不刷新(同款模板 // 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-33):Pluck("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 列 → 必然失败仍返回 OK(P1-3)
常用好友标记 Friend.DoPopular / UndoPopular do_popular.go:27 / undo_popular.go:27 定位条件错误 → 匹配 0 行,置顶永不生效(P1-3)
删除好友 Friend.Delete internal/logic/friend/delete.go:27 同上,0 行受影响仍返回 OK(P1-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:22;tag_member_fetch.go:27;tag_do_create.go:41;tag_do_update.go:39 TagFetch 必 panic(P0-1);TagMemberFetch 恒返回空;创建/更新可越权写他人标签(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"/UUID(P2-27);非法 direction 静默返回空
匹配记录查询 Match.Fetch common/const.go:155-172 原生 SQL 语法不成立 → 恒 ErrDB(P1-8),且 relation_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:63、apply_do_message.go:46、apply_do_pass.go:41、apply_do_reject.go:32 //EventMQ:向mesh MQ中心发送报文,请求推送消息以及更新Cache. 之后无任何代码;全模块 grep EventMQ|queue|MQ|Publish 只命中这 4 条注释 → 推送与缓存更新均未实现
  • 建议:未实现能力一律返回 codes.Unimplemented/ErrNotImplemented,不要返回空成功;删除或补全 follow.Do;实现申请通知(MQ/推送)与缓存失效;按上表把每个 RPC 的真实状态写入模块 README。

29. 测试完全缺失(无任何 _test.go,test/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 vet 与 gofmt 均通过(见第 2 节),说明常规静态检查无法覆盖这些运行期与语义缺陷。最小缺失用例清单:① TagFetch/ApplyGet 非 panic 且返回非 nil 响应;② Doing 重复调用只产生一条关注边、Undo 后 State 回到未关注;③ 好友对称性(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/dbname(etc/relation_prod.yaml:7),任何执行该 CLI 的环境(含 CI、跳板机、共享终端)都会把连接串写入终端记录/日志;结合 conf.PrintInfo 与 GORM Debug(P1-16)共同构成敏感信息外泄面。
  • 建议:移除该打印,或仅输出 Driver 与 len(Source);确需诊断时输出脱敏 DSN。

P3

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

  • 位置:internal/models/base.go:1、internal/models/query.go:1、internal/logic/follow/do.go:14、internal/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:44(maxSize = 50)、follow/fetch.go:31,35("MY"/"WHO")、tag_do_update.go:29,38("ADD"/"DEL")、apply_do_pass.go:43(status=1)、apply_do_reject.go:26(status=-1)、do_popular.go:27/undo_popular.go:27(popular=1/0)、follow/fetch.go:46(PageSize=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-2;README.md:196-202;wiki/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/9);wiki 只描述动态 HTTP 路径,未说明本模块自带 grpc-gateway 不可用(属全局模板缺陷,见 P1-15 一句话说明);模块 README 为空,运维与新人无落地依据。
  • 建议:在 README 中逐 RPC 标注实现状态(可用/部分可用/占位)与已知缺陷编号;补充数据表清单、列名约定、依赖的上游表与调用鉴权要求。

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

  • 位置:etc/supervisor.bsm-social-relation.conf:6、etc/relation_prod.yaml:7,10,21,27,28,43、go.mod:69、internal/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 主从/哨兵缺失、无 TLS),go.mod 用相对路径 replace 使模块无法脱离本仓库路径独立构建(跨机/CI 构建易失败);reply 命名返回“声明即使用”的模式在 4 个文件中重复出现(P0-1 的共同根因),缺少统一约定与 lint 规则。
  • 建议:改为专用低权限账号运行、日志接入集中式采集并对 body 脱敏;密钥从环境/密钥管理注入,Redis 启用认证与 TLS;SDK 依赖改为版本化(去掉相对路径 replace)并固定 CI;在 golangci-lint 中启用 nilerr/staticcheck 等规则,禁止未初始化命名返回指针被成员访问。

4. 推荐优化方案

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

  1. 可用性止血(当天)
    • 在 internal/server/new.go 的 grpc.NewServer() 上安装 grpc.ChainUnaryInterceptor(recovery, 结构化日志, 超时, 限流),确保任何 handler panic 不再终止进程;同时修复 P0-1 的两处 nil 响应初始化。
    • 修复 fetch.go:48/apply_fetch.go:48 的 err/cacheErr 混淆与缓存命中返回 nil(P1-7)。
    • 关闭生产 GORM Debug 并移除 CLI 的 DSN 打印(P1-16、P2-30)。
  2. 数据面一致性与权限(1~2 周)
    • 统一列名约定(passport_id 或 relation_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 的字段与合并逻辑并补 total(P2-18/19)。
    • 缓存改为带 TTL + 写路径失效 + 降级回源,版本号改用业务版本(P2-20)。
  4. 工程化(持续)
    • 用迁移脚本管理表结构与索引,接入上游资料服务替代直连 relation_extend(P2-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:15、module/social/relation/internal/logic/friend/apply_get.go:16、module/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:10、module/social/relation/internal/logic/common/const.go:120、module/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:27、module/social/relation/internal/logic/friend/modify_nickname.go:26、module/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:26、module/social/relation/internal/logic/follow/state.go:43
  • P1-5 ApplyGet、ApplyDoMessage 增加申请参与方校验|验收:非参与方读取/留言返回 PermissionDenied,参与方行为不变|涉及:module/social/relation/internal/logic/friend/apply_get.go:26、module/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:39、module/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:35、module/social/relation/internal/logic/friend/fetch.go:48、module/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:29、module/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:10、module/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:28、module/social/relation/internal/logic/common/const.go:20
  • P1-13 为 Friend.Get/Match.Get 增加关系与可见性校验、黑名单过滤与限流|验收:非好友/非匹配对象查询返回 PermissionDenied;单位时间内的资料查询有配额限制|涉及:module/social/relation/internal/logic/friend/get.go:23、module/social/relation/internal/logic/match/get.go:24
  • P1-14 为关系热点列补齐索引并纳入版本化迁移脚本|验收:EXPLAIN 显示关注/粉丝/好友/标签/匹配查询走索引;CI 中有可重复执行的迁移脚本|涉及:module/social/relation/internal/models/relation_follow.go:10、module/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:20、module/social/relation/cmd/main/main.go:32、module/social/relation/etc/relation_prod.yaml:38
  • P1-16 生产关闭 GORM Debug、移除 CLI 的 DSN 打印、对申请正文脱敏|验收:生产日志中不含 SQL 参数与 DSN;cmd/cli 输出无口令|涉及:module/social/relation/internal/impl/impl.go:23、module/social/relation/cmd/cli/main.go:11
  • P2-17 为所有列表接口统一分页与上限校验(含 Friend.Fetch、深分页、IN 分片)|验收:page_size 超限被拒绝或截断;超大 offset 不报错;好友列表可分页获取|涉及:module/social/relation/internal/logic/follow/fetch.go:45、module/social/relation/internal/logic/common/const.go:120
  • P2-18 修正 CollectionFriendData 的 ids 字段与备注/置顶合并逻辑、补 total、稳定排序|验收:好友列表卡片与关系一一对应,remark_name/popular/total 与 DB 一致且顺序稳定|涉及:module/social/relation/internal/logic/common/const.go:123、module/social/relation/internal/logic/common/const.go:141
  • P2-19 申请列表去硬编码并返回 total|验收:超过 50 条申请可分页查看,total 准确|涉及:module/social/relation/internal/logic/common/const.go:44、module/social/relation/internal/logic/common/const.go:103
  • P2-20 缓存加 TTL/随机抖动、写路径失效、失败降级回源、版本号改用业务版本|验收:key 有过期时间;新增留言会驱动客户端刷新;Redis 不可用时接口仍可返回 DB 数据|涉及:module/social/relation/internal/logic/common/const.go:182、module/social/relation/internal/logic/friend/apply_fetch.go:30
  • P2-21 多步写操作纳入事务|验收:注入中途失败后数据库无半成品状态(申请/留言/状态一致)|涉及:module/social/relation/internal/logic/friend/apply_do.go:36、module/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:56、module/social/relation/cmd/main/main.go:40、module/social/relation/etc/relation_prod.yaml:32
  • P2-25 修正标签名称/成员统计映射|验收:TagFetch 返回非空 tag_name,friend_total 与实际成员数一致|涉及:module/social/relation/internal/logic/friend/tag_fetch.go:22、module/social/relation/internal/logic/common/const.go:134
  • P2-26 申请接口参数一致性与存在性校验、去除假成功|验收:to_id/to_identity 不一致被拒绝;申请自己/已是好友被拒绝;操作不存在记录返回 NotFound|涉及:module/social/relation/internal/logic/friend/apply_do.go:23、module/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:21、module/social/relation/internal/logic/follow/do.go:26、module/social/relation/internal/logic/friend/tag_member_fetch.go:27、module/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:1、module/social/relation/internal/impl/impl.go:16
  • P3-32 用枚举/常量替换魔法数字与方向字符串,非法值返回参数错误|验收:方向/状态/分页常量集中定义,非法 direction 返回 InvalidArgument 而非空结果|涉及:module/social/relation/internal/logic/follow/fetch.go:37、module/social/relation/internal/logic/friend/tag_do_update.go:29
  • P3-33 补全模块 README 并同步仓库 README/wiki 的实现边界|验收:模块 README 含逐 RPC 实现状态、表与列约定、依赖的上游表与鉴权说明|涉及:module/social/relation/README.md:1、README.md:199
  • P3-34 运维与依赖治理(非 root 运行、密钥外置、去掉相对路径 replace、lint 规则)|验收:supervisor 使用低权限账号;配置无 CHANGE_ME;模块可脱离本地路径构建;lint 禁止未初始化命名返回指针被访问|涉及:module/social/relation/etc/supervisor.bsm-social-relation.conf:6、module/social/relation/go.mod:69、module/social/relation/etc/relation_prod.yaml:43

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

  • 问题数:P0=1 P1=15 P2=14 P3=4(合计 34)
  • 最高风险(一句话):Friend.TagFetch 与 Friend.ApplyGet 的命名返回指针从未初始化即被字段解引用(tag_fetch.go:22 的 &reply.Total、apply_get.go:32 的 reply.Data = ...,编译期即可确定的 nil 解引用)→ 必然 panic,而 grpc.NewServer() 未装 recover 拦截器,任何持有有效 JWT 的账号调用一次即可让整个 relation 服务进程退出(远程可用性攻击);同时好友/关注/标签的读写列名与归属条件系统性错配,导致"删除好友、取消关注、备注置顶"等操作静默假成功、好友关系永久单向。
  • 最优先 3 个动作:
    1. 给 grpc.NewServer() 安装 recovery/日志/限流拦截器,并修复 tag_fetch.go:22、apply_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 或迁移脚本(*.sql 无 relation_* 命中、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 的线上索引状态),未达证据标准的不升级级别。