docs: add per-module audit reports (18 modules)

Add static security/quality audit reports for all 18 Go service modules under module/, plus a consolidated index (docs/audit/README.md) with per-module statistics, top risks, cross-module systemic defects and a phased TODO list (T1-T19). No production code is modified.
This commit is contained in:
2026-09-14 22:16:27 +08:00
parent fc1a64f9dd
commit 63aeedc2fe
19 changed files with 12724 additions and 0 deletions

View File

@@ -0,0 +1,717 @@
# 审计报告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{}`3CI 增加 `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` 无去重重复关注会不断累积bSQL 完全没有 `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-51apply_fetch.go 同构)
cache, err := common.GetCache(auth.Identity, "friends")
...
if in.Version != cache.Version {
reply, err = common.CollectionFriendData(auth.ID)
...
}
if err != nil { ... }
if cacheErr != nil {
printer.Error(err.Error()) // 此处 err 保证为 nil → 对 nil error 接口调用 Error() → panic
return nil, errcode.ErrRedis
}
return reply, nil // 版本命中缓存时 reply 从未赋值,返回 typed nil
```
- **影响**两个缺陷。a当客户端**按协议**带回 `version == 缓存版本`(稳态调用)时,`reply` 保持 nil函数返回 `(nil, nil)`,客户端拿到空的 `FriendsReply/ApplyFetchReply``total=0`、无好友/无申请),表现为“好友列表突然清空”;同时 `GetCache` 的结果被读入 `cache` 却从未返回,缓存形同虚设(`in.Version == 0` 时还会重复执行一次全量 `CollectionFriendData`,见 `fetch.go:21-40`bRedis 写失败(`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`故障定位成本高cprod 的 `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` 列 → 必然失败仍返回 OKP1-3 |
| 常用好友标记 | `Friend.DoPopular` / `UndoPopular` | `do_popular.go:27` / `undo_popular.go:27` | 定位条件错误 → 匹配 0 行置顶永不生效P1-3 |
| 删除好友 | `Friend.Delete` | `internal/logic/friend/delete.go:27` | 同上0 行受影响仍返回 OKP1-3 |
| 好友申请 / 申请留言 | `Friend.ApplyDo` / `ApplyDoMessage` | `apply_do.go:23-61` / `apply_do_message.go:33-44` | 有实现无幂等、无限流P1-11留言可越权写任意申请P1-5 |
| 申请通过 / 拒绝 | `Friend.ApplyDoPass` / `ApplyDoReject` | `apply_do_pass.go:29-47` / `apply_do_reject.go:26` | 通过只写单向好友行、无事务、对不存在申请假成功P1-10、P2-26 |
| 好友标签 / 标签成员 | `Friend.TagFetch` / `TagMemberFetch` / `TagDoCreate` / `TagDoUpdate` | `tag_fetch.go:22``tag_member_fetch.go:27``tag_do_create.go:41``tag_do_update.go:39` | `TagFetch` 必 panicP0-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"`/UUIDP2-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 DebugP1-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/9wiki 只描述动态 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 启用认证与 TLSSDK 依赖改为版本化(去掉相对路径 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` 混淆与缓存命中返回 nilP1-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 的线上索引状态),未达证据标准的不升级级别。