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.
76 KiB
审计报告: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:10vsinternal/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/UndoBlockRPC、没有在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 -NSQL 报错,或正数时形成深分页全表扫描;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.Popularids = 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).Errorreply.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;全模块 grepTransaction|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语义见 SDKtypes.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)(SDKcore/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).Errortypes.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与 SDKvars.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.logCache: redis://null:CHANGE_ME@127.0.0.1:6379/ # prod 也是占位口令 + 明文 redis 协议 SecretKey: CHANGE_MEreplace 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. 推荐优化方案
按“先止血、再修数据面、后补工程能力”的顺序推进:
- 可用性止血(当天)
- 在
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)。
- 在
- 数据面一致性与权限(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)。
- 统一列名约定(
- 性能与容量(与数据面并行)
- 建索引(P1-14)、给列表接口加统一分页与上限(P2-17)、修正
CollectionFriendData的字段与合并逻辑并补total(P2-18/19)。 - 缓存改为带 TTL + 写路径失效 + 降级回源,版本号改用业务版本(P2-20)。
- 建索引(P1-14)、给列表接口加统一分页与上限(P2-17)、修正
- 工程化(持续)
- 用迁移脚本管理表结构与索引,接入上游资料服务替代直连
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 个动作:
- 给
grpc.NewServer()安装 recovery/日志/限流拦截器,并修复tag_fetch.go:22、apply_get.go:32的 nil 响应初始化(P0-1)。 - 统一
relation_friend*系列的归属列命名并修正写操作定位条件(friend_relation_identity/remark_name)与Undo/DoIgnore的删除条件(P1-2/3/4)。 - 为
ApplyGet/ApplyDoMessage/TagDoUpdate(DEL)补归属校验(越权读写),并把ApplyDoPass改为事务内双向写好友关系、relation_match明确写入方(P1-5/6/9/10)。
- 给
- 未能覆盖 / 无法验证的部分:
- 数据库物理 schema:仓库内无建表 SQL 或迁移脚本(
*.sql无relation_*命中、models.InitData被注释、IsAutoMigrate=false),因此 P1-2 中"列不存在 vs 列存在但从未写入"的最终形态、以及 P1-14 的线上索引实际状态只能人工核对生产库。 relation_extend表归属、真实列名(nicknamevsname)、是否含status/deleted_at:本模块无模型、全仓库无定义,P2-22、P1-8 的相关结论含有推测成分(已在正文标注)。- 未运行任何写操作或数据库连接:所有 SQL 层结论基于代码与 Postgres 语法规则推导;
go vet/gofmt通过,说明这些缺陷不在编译期可见范围。 - 上游调用方行为未验证:无法确认客户端是否会在
ApplyDoPass后自行补写反向好友行、是否会带回version命中缓存(P1-7/P1-10 的影响面取决于客户端)。 - 未审计 pb 生成代码的正确性(仅核对字段与路由),未审计 SDK 内部实现(仅作参考读取,其自身问题如
ListenLeaseRespChan空转、默认Debug:true在正文中作为放大因素提及)。 - HTTP 暴露路径未端到端验证:本模块
service/为空目录、未接入pkgs/all聚合网关(README.md:194),因此无法确认线上是否存在其它网关按/rpc/relation/{Service}/{Method}动态转发(该路径的真实可用性取决于网关侧配置,本报告只记录本模块侧事实)。
- 数据库物理 schema:仓库内无建表 SQL 或迁移脚本(
- 证据等级约定:① 每条结论均给
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 的线上索引状态),未达证据标准的不升级级别。