# 审计报告: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` - **证据**: ```go // 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 ``` ```go // 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) ``` ```go // 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` - **证据**: ```go // 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;"` ``` ```go // 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 ``` ```go // 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` - **证据**: ```go // 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 ``` ```go // 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` - **证据**: ```go 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` - **证据**: ```go // apply_get.go:17,26 只校验“已登录”,不校验申请人/接收人身份 _, err = service.ParseMetaCtx(ctx, nil) ... err = impl.DBService.Where("identity=?", in.Identity).First(&apply).Error ``` ```go // 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` - **证据**: ```go // 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 ``` ```go // 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` - **证据**: ```go // 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` - **证据**: ```sql 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`(写错表) - **证据**: ```go // match/do_join.go:17-35 RPC 注释是“执行通过,加为好友”,实际写入关注表 // 执行通过,加为好友 data := &models.RelationFollow{FromIdentity: auth.Identity, ToIdentity: in.Identity} err = impl.DBService.Create(data).Error ``` ```go // 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` - **证据**: ```go 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` - **证据**: ```go // 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 ``` ```go // 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*` - **证据**: ```go // follow/doing.go:28-38 关注前不查询任何拒绝/黑名单状态 data := &models.RelationFollow{FromIdentity: auth.Identity, ToIdentity: in.Identity} err = impl.DBService.Create(data).Error ``` ```go // 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` - **证据**: ```go // friend/get.go:15-23 仅 ParseMetaCtx + identity 非空,无任何好友/匹配/可见性校验 _, err = service.ParseMetaCtx(ctx, nil) ... reply, err = common.GetrelationInfoDetailCard(in.Identity) ``` ```go // 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` - **证据**: ```go // 全部模型仅 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"` ``` ```go // 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` - **证据**(本模块侧,仅记录事实,全局根因不在此重复论证): ```go // 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()) ``` ```go // 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` - **证据**: ```go // impl.go:23 opts 传 nil DBService = with.Databases(config.Spec.Databases, nil) // model ``` ```go // SDK core/database/sql/postgresql.go SetOptions(nil) 默认 Debug:true,随后无条件 gormDb.Debug() options = &types.SqlOptions{ MaxIdleConns: ..., IsAutoMigrate: false, LogStdout: false, Debug: true } ``` ```go // 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` - **证据**: ```go // 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 ``` ```go // 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` - **证据**: ```go 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 ``` ```go 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` - **证据**: ```go maxSize int = 50 err = impl.DBService.Where("to_id=? ", id).Order("id desc").Limit(maxSize).Find(&applys).Error ``` ```go 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` - **证据**: ```go key := "/RF/" + identity + "/" + collectionName err = impl.RedisService.Client.Set(impl.RedisService.Ctx, key, string(val), 0).Err() // 0 = 无过期 ``` ```go 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` 无命中 - **证据**: ```go 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` - **证据**: ```go 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` - **证据**: ```go CreatedAt: item.CreatedAt.Local().Format("2006-01-02 15:04:05"), applyItem.CreatedAt = item.CreatedAt.Local().Format("2006-01-02 15:04:05") ``` ```yaml - 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` - **证据**: ```go conf.NotNil(Spec.Service, Spec.Cache) // 未校验 Databases / Gateway / Etcd 的一致性 ``` ```yaml # 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 ``` ```go // 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}`) - **证据**: ```go err = impl.DBService.Model(models.FriendTag{}).Where("relation_id=?", id).Order("id asc").Scan(&reply.Tags).Error ``` ```go 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` - **证据**: ```go 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, ... } ``` ```go // 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` - **证据**: ```go 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` - **证据**: ```go // friend/search.go:18-21 "搜索" RPC:只校验 keyword 非空,返回空结构体 if in.Keyword == "" { return nil, errcode.ErrInvalidArgument } return &pb.PartFriendReply{}, nil ``` ```go // 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 ``` ```go // 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` 无命中 - **证据**: ```text 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` - **证据**: ```go 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` - **证据**: ```go // models/base.go、models/query.go 全文只有一行 package models ``` ```go // 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`) - **证据**: ```go case "MY": // 我关注的 column = "to_identity" case "WHO": // 关注我的 ``` ```go 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` - **证据**: ```markdown # relation ``` ```markdown - **主要功能**:好友搜索、列表、详情、备注修改、常用好友标记与删除;好友申请、申请留言、通过和拒绝;好友标签及标签成员;关注、取消关注、关注状态与列表;匹配记录查询、加入和忽略。 - **边界与协作**:... `Match` 的接口主要维护匹配记录,不应据此认定存在完整推荐算法。 ``` ```markdown - 动态 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` - **证据**: ```ini user=root # supervisor 以 root 运行 stdout_logfile=/data/app/logs/social-relation.log ``` ```yaml Cache: redis://null:CHANGE_ME@127.0.0.1:6379/ # prod 也是占位口令 + 明文 redis 协议 SecretKey: CHANGE_ME ``` ```go 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 的线上索引状态),未达证据标准的不升级级别。