2026-09-22 18:53:53 +08:00
# relation( 好友、关注与匹配关系) 代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/social/relation` ( module 路径 `bsm/full/module/social/relation` ) |
| 服务域 | 社交与关系 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 44 个文件 / 1823 行;生成代码 `pb/` 10 个文件 / 7751 行(`pb/const.pb.go` 单文件 3368 行);`proto/` 4 个文件 / 520 行;`etc/` 3 个 yaml( 52/58/52 行)+ 1 个 supervisor conf; `*_test.go` 0 个;无 `service/` 、无 `test/` 目录 |
| 入口 | 仅独立入口 `cmd/main/main.go` ( gRPC + grpc-gateway 单进程, dev `Port: 12248` / prod `12426` , `Gateway.Port: 12425` ) ; **未被** `pkgs/all` 、`pkgs/ecmall` 接入;`cmd/cli/main.go` 只打印 `config.Spec.Databases` |
| 对外协议 | gRPC 3 个 service / 25 个 RPC; gateway 路径为 `POST /relation.{Service}/{Method}` ,模块内**从未注册任何 gateway handler** |
| 结论摘要 | 25 个接口全部只验 JWT, **没有一处做归属校验**:删好友/删关注/改备注/置顶/审批加好友都可指向他人记录; 7 个好友与标签接口按不存在的列 `relation_id` 查询(模型里是 `passport_id` ),其中 `Friend.TagFetch` 还必然空指针 panic; `Follow.Fetch` /`Match.Fetch` 分别因 `Params` 静默空返回与无效 SQL 不可用;`Match` 的两个写接口改的其实是关注表;缓存分支可返回 `(nil, nil)` 或 panic; `follow.Do` 是遗留占位死代码;网关未接线、无聚合入口,`etc` 中 `KycConf` 与匿名清单均为死配置。 |
## 1. 服务定位与职责
维护三类社交关系与其辅助数据:
- **好友**: `relation_friend` (好友条目、备注、置顶、会话 ID) 、`relation_friend_apply` (申请)、`relation_friend_apply_message` (申请留言)、`relation_friend_tag` + `relation_friend_in_tag` (好友标签分组);
- **关注**: `relation_follow` ( from/to 关注关系);
- **匹配**: `relation_match` (推荐关系)。
对外提供好友搜索/列表/资料卡/备注/置顶/删除、好友申请的提交/留言/列表/详情/通过/拒绝、好友标签的增改查,以及关注的执行/撤销/状态/列表和匹配的列表/详情/通过/忽略。
它是关系链的**读写服务**;用户资料不落在本模块(依赖外部表 `relation_extend` ,仓库内无模型也无建表 SQL) , 消息推送、动态分发、推荐计算均不在本模块内( 代码里只有 `//EventMQ: 向mesh MQ中心发送报文` 这类注释,无实现)。
## 2. 代码结构与入口
| 路径 | 职责 |
| --- | --- |
| `cmd/main/main.go` | 独立进程入口:`config.New("relation")` → `impl.NewImpl()` → `server.New(config.Spec.Addr)` → `service.New` → `srv.Start()` ; `srv.Use(models.InitData)` 被注释(`:37` ) |
| `cmd/cli/main.go` | 调试入口,只做 `config.New("relation")` + `fmt.Println(config.Spec.Databases)` |
| `internal/config/config.go` | `SrvConfig` ( Base、Databases、MicroService、Rpc、Gateway、APM、Etcd、WeChatConf、Token、Kyc) 与 `conf.NotNil(Service, Cache)` 校验 |
| `internal/impl/impl.go` | 初始化 Memory、Redis、DB、Etcd 四个全局实例 |
| `internal/server/new.go` | 创建 `grpc.Server` ,注册 `Follow` 、`Friend` 、`Match` 三个 service( `protoc-gen-slc` 生成);**未创建 `ServeMux` ** |
| `internal/server/{follow,friend,match}_server.go` | 25 个 RPC 的 1:1 转发(生成代码) |
| `internal/logic/common/const.go` | 资料卡查询、会话 ID、好友/申请集合装配、好友申请缓存读写( 202 行,本模块最重的文件) |
| `internal/logic/follow/{doing,undo,state,fetch,do}.go` | 关注的执行/撤销/状态/列表 + 遗留占位 `Do` |
| `internal/logic/friend/*.go` ( 17 个) | 好友基础操作、申请管理、标签管理 |
| `internal/logic/match/{fetch,get,do_join,do_ignore}.go` | 匹配列表/详情/通过/忽略 |
| `internal/models/*.go` ( 9 个) | 7 张表模型 + `query.go` 、`base.go` (后两者各 1 行,仅 `package models` ) |
| `proto/{follow,friend,match,const}.proto` | 契约;公共消息放在 `proto/const.proto` ( `package blocks` ,与 service 的 `package relation` 不同) |
| `etc/{relation_dev,relation_prod,relation_test}.yaml` + `supervisor.bsm-social-relation.conf` | 配置与进程托管 |
依赖注入:本模块没有 `service/` 目录,也没有 `Expose/Dependencies` 注入点,`impl.NewImpl()` 是唯一初始化路径(`internal/impl/impl.go:19-25` )。全仓库对 `module/social/relation` 的引用只有 `go.work:21` 与 `scripts/api-docgen` 。
## 3. 接口清单
HTTP 路径由生成代码固定为 `/relation.{服务}/{方法}` ( `pb/follow.pb.gw.go:344-347` 、`pb/friend.pb.gw.go:1176-1192` 、`pb/match.pb.gw.go:344-347` ),全部为 `POST` 。**这些 handler 在模块内从未被注册到任何 `ServeMux` **(见 6.3/6.4)。
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
| --- | --- | --- | --- | --- |
| POST | `/relation.Follow/Doing` | 执行关注 | JWT | `logic/follow/doing.go:18` |
| POST | `/relation.Follow/Undo` | 撤销关注 | JWT( **丢弃 claims、无归属校验**) | `logic/follow/undo.go:17` |
| POST | `/relation.Follow/State` | 关注状态 | JWT | `logic/follow/state.go:18` |
| POST | `/relation.Follow/Fetch` | 关注列表 | JWT | `logic/follow/fetch.go:17` |
| POST | `/relation.Friend/Search` | 搜索好友 | JWT( 丢弃 claims) | **空实现 ** `logic/friend/search.go:12` |
| POST | `/relation.Friend/Fetch` | 好友列表 | JWT | `logic/friend/fetch.go:14` (缓存分支可返回 nil, 见 6.2) |
| POST | `/relation.Friend/Get` | 用户资料卡 | JWT( 无关系校验) | `logic/friend/get.go:14` |
| POST | `/relation.Friend/ModifyNickname` | 修改好友备注 | JWT( 归属条件列名错误) | `logic/friend/modify_nickname.go:17` |
| POST | `/relation.Friend/DoPopular` | 置顶好友 | JWT( 同上) | `logic/friend/do_popular.go:17` |
| POST | `/relation.Friend/UndoPopular` | 取消置顶 | JWT( 同上) | `logic/friend/undo_popular.go:17` |
| POST | `/relation.Friend/Delete` | 删除好友 | JWT( 同上) | `logic/friend/delete.go:17` |
| POST | `/relation.Friend/ApplyFetch` | 好友申请列表 | JWT | `logic/friend/apply_fetch.go:14` (缓存分支可返回 nil, 见 6.2) |
| POST | `/relation.Friend/ApplyGet` | 好友申请详情 | JWT( **丢弃 claims、无归属校验**) | `logic/friend/apply_get.go:16` |
| POST | `/relation.Friend/ApplyDo` | 发起好友申请 | JWT | `logic/friend/apply_do.go:18` |
| POST | `/relation.Friend/ApplyDoMessage` | 追加申请留言 | JWT( **不校验申请归属**) | `logic/friend/apply_do_message.go:17` |
| POST | `/relation.Friend/ApplyDoPass` | 通过申请并加好友 | JWT( **不校验申请与好友参数一致**) | `logic/friend/apply_do_pass.go:19` |
| POST | `/relation.Friend/ApplyDoReject` | 拒绝申请 | JWT( 无影响行数校验) | `logic/friend/apply_do_reject.go:17` |
| POST | `/relation.Friend/TagFetch` | 好友标签列表 | JWT | `logic/friend/tag_fetch.go:15` ( **必然 panic**,见 6.2) |
| POST | `/relation.Friend/TagMemberFetch` | 标签成员 | JWT | `logic/friend/tag_member_fetch.go:16` (表/列/参数三处错误,见 6.2) |
| POST | `/relation.Friend/TagDoCreate` | 创建标签 | JWT | `logic/friend/tag_do_create.go:18` |
| POST | `/relation.Friend/TagDoUpdate` | 增删标签成员 | JWT( **无归属限定**) | `logic/friend/tag_do_update.go:18` |
| POST | `/relation.Match/Fetch` | 匹配列表 | JWT | `logic/match/fetch.go:16` ( SQL 无效,见 6.2) |
| POST | `/relation.Match/Get` | 匹配详情 | JWT( 丢弃 claims) | `logic/match/get.go:14` |
| POST | `/relation.Match/DoJoin` | 匹配通过加好友 | JWT | `logic/match/do_join.go:18` (写的是关注表,见 6.2) |
| POST | `/relation.Match/DoIgnore` | 忽略匹配 | JWT( **丢弃 claims、无归属校验**) | `logic/match/do_ignore.go:17` (删的是关注表,见 6.2) |
> 鉴权方式: 25 个 logic 入口全部调用 `service.ParseMetaCtx(ctx, nil)`( `bsm-sdk/core/service/meta.go:19-49`),要求有效 JWT; `opts` 均为 `nil`,即**不校验角色、不校验私有 IP**。其中 `Follow.Undo`、`Friend.Search`、`Friend.ApplyGet`、`Match.Get`、`Match.DoIgnore` 以及死代码 `follow.Do` 用 `_, err = ...` 丢弃 claims( `follow/undo.go:18`、`friend/search.go:14`、`friend/apply_get.go:17`、`match/get.go:16`、`match/do_ignore.go:19`),无法做归属判断。
### 声明但未实现 / 占位接口
| 类别 | 接口 | 证据 |
| --- | --- | --- |
| TODO 占位且**无调用点**(死代码) | `follow.Do` (入口实际走 `follow.Doing` ) | `internal/logic/follow/do.go:26` ( `// TODO: add your logic code & delete this line.` ) ; `internal/server/follow_server.go:19-21` 转发的是 `follow.Doing` ; `grep "follow.Do("` 全仓库无调用 |
| 只有参数校验、无查询 | `Friend.Search` | `internal/logic/friend/search.go:18-21` (返回空的 `&pb.PartFriendReply{}` ) |
| 模型存在、无写入路径 | `RelationMatch` (匹配记录) | 全仓库对 `RelationMatch` 的唯一使用是 `internal/logic/match/fetch.go:34` 的 `Count` , **没有任何 Create/Update** → 匹配记录只能靠外部写入 |
| 空初始化被注释 | 无 `models.InitData` ( `models/query.go` 只有 1 行 `package models` ) , `cmd/main/main.go:37` 的调用已被注释 | `internal/models/query.go:1` 、`internal/models/base.go:1` 、`cmd/main/main.go:37` |
## 4. 数据模型与表
模型未注册任何自动迁移:`database.AppendMigrate` / `AutoMigrate` 在 `module/social/**` 内**无匹配**( `grep AppendMigrate module/social` 返回空) , 7 张表依赖外部建表脚本,仓库内没有对应 SQL。公共字段来自 SDK( `bsm-sdk/core/types/db.go` ) : `types.Std_IICUDS` = `id` (PK) / `identity varchar(36) uniqueIndex` / `created_at` / `updated_at` / `deleted_at` (index) / `status int8 default 0 index` ; `types.Std_Passport` = `passport_id uint index` / `passport_identity varchar(36) index` 。
### 表 `relation_friend`( `internal/models/relation_friend.go:8-21`)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` / `identity` | uint / varchar(36) | PK / uniqueIndex | 好友条目自身标识(`apply_do_pass.go:30` 写入 UUID) |
| `created_at` /`updated_at` /`deleted_at` /`status` | - | index(deleted_at) | 软删除;`status` 未使用 |
| `passport_id` / `passport_identity` | uint / varchar(36) | index | **归属用户 ** ( `apply_do_pass.go:31-32` 写入接受方) |
| `friend_relation_id` | uint | not null | 好友的用户 ID |
| `friend_relation_identity` | varchar(36) | not null | 好友 identity |
| `remark_name` | varchar(255) | 默认 `''` | 好友备注(**接口更新的是 `nickname` 列,见 6.2**) |
| `popular` | int2 | 默认 0 | 是否置顶 |
| `session_id` | varchar | not null | 会话 ID( `common.UniqueSessionID` , `apply_do_pass.go:33` ) |
> 表上**没有** `(passport_id, friend_relation_identity)` 唯一约束,重复加好友只能靠代码判断(当前没有判断);也**没有 `relation_id` 列**,但 6 处查询在用它(见 6.2)。
### 表 `relation_follow`( `internal/models/relation_follow.go:8-17`)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` / `identity` | uint / varchar(36) | PK / uniqueIndex | 关注条目标识(`doing.go:32` 、`do_join.go:33` ) |
| `created_at` /`updated_at` /`deleted_at` /`status` | - | index(deleted_at) | 软删除;`status` 未使用 |
| `from_identity` | varchar(36) | not null | 关注者(取 JWT 的 identity) |
| `to_identity` | varchar(255) | not null | 被关注者(长度与 `from_identity` 的 36 不一致) |
> 无 `(from_identity, to_identity)` 唯一约束 → 重复关注可无限写入(`doing.go:24-38` 无查重)。
### 表 `relation_match`( `internal/models/relation_match.go:6-15`)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` / `identity` | uint / varchar(36) | PK / uniqueIndex | 匹配记录标识 |
| `created_at` /`updated_at` /`deleted_at` | - | index(deleted_at) | 软删除 |
| `status` | int8 | index, 默认 0 | 被 `GetrelationInfoDetailCardByMatch` 当作 `foreign_status` 返回(`common/const.go:165` ) ; **无任何写入点,恒为 0** |
| `relation_identity` | varchar(36) | not null | 关系归属人 |
| `recommend_identity` | varchar(255) | not null | 推荐对象 |
### 表 `relation_friend_apply`( `internal/models/friend_apply.go:8-20`)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` / `identity` | uint / varchar(36) | PK / uniqueIndex | 申请标识(`apply_do.go:35` ) |
| `created_at` /`updated_at` /`deleted_at` /`status` | - | index(deleted_at) | `status` : 1 通过(`apply_do_pass.go:43` ) / -1 拒绝(`apply_do_reject.go:26` ) |
| `from_id` / `from_identity` | uint / varchar(50) | not null | 申请人(由 JWT 写入) |
| `to_id` / `to_identity` | uint / varchar(50) | not null | 目标(由请求写入,未校验存在) |
| `last_message_id` | uint | not null | 最后一条留言 ID( `apply_do.go:57` 、`apply_do_message.go:40` 回写) |
> 无 `(from_identity, to_identity)` 唯一约束 → 同一对用户可重复申请(当前无查重)。
### 表 `relation_friend_apply_message`( `internal/models/friend_apply_message.go:8-18`)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` / `identity` | uint / varchar(36) | PK / uniqueIndex | 留言标识 |
| `created_at` /`updated_at` /`deleted_at` /`status` | - | index(deleted_at) | - |
| `passport_id` / `passport_identity` | uint / varchar(36) | index | 留言人(由 JWT 写入) |
| `apply_id` | uint | not null | 所属申请 ID, **写入时不校验申请是否存在/归属**(见 6.1) |
| `body` | varchar(500) | 默认 `''` | 留言正文 |
### 表 `relation_friend_tag`( `internal/models/friend_tag.go:8-17`)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` / `identity` | uint / varchar(36) | PK / uniqueIndex | 标签标识(`tag_do_create.go:30` ) |
| `created_at` /`updated_at` /`deleted_at` /`status` | - | index(deleted_at) | - |
| `passport_id` / `passport_identity` | uint / varchar(36) | index | 标签归属人 |
| `name` | varchar(255) | not null | 标签名(无去重、无长度上限校验) |
> 同样**没有 `relation_id` 列**,而 `TagFetch`、`CollectionFriendData` 都在用它过滤(见 6.2)。
### 表 `relation_friend_in_tag`( `internal/models/friend_in_tag.go:8-18`)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` / `identity` | uint / varchar(36) | PK / uniqueIndex | 关联条目标识 |
| `created_at` /`updated_at` /`deleted_at` /`status` | - | index(deleted_at) | - |
| `passport_id` / `passport_identity` | uint / varchar(36) | index | 归属人(`tag_do_create.go:46-47` 、`tag_do_update.go:34-35` ) |
| `friend_identity` | varchar(36) | not null | 好友 identity |
| `tag_identity` | varchar(36) | not null | 标签 identity |
> 无 `(tag_identity, friend_identity)` 唯一约束;写接口(`tag_do_create.go:41-52`、`tag_do_update.go:30-37`)不校验标签归属与好友关系。
### 外部表 `relation_extend`(仓库内无模型)
`internal/logic/common/const.go:20,26,166` 三处查询 `relation_extend` ,但仓库内既没有该表的模型也没有建表 SQL( 表结构无法从代码确证, 【信息不足】) 。可确证的是**同一张表的三处查询列名互相矛盾**: `Filed` ( `common/const.go:15` )取 `nickname/avatar/sex/province/city/area/sign` ,而 `GetrelationInfoDetailCardByMatch` ( `common/const.go:158` )取 `pe.name` ,且 `pb.RelationItem` ( `proto/const.proto:307-321` )里**没有 `name` 字段**。
## 5. 核心流程
```mermaid
flowchart TD
A["POST /relation.Friend/ApplyDo<br/>建申请 + 建留言 + 回写 last_message_id<br/>无事务 无重复校验"] --> B["relation_friend_apply"]
B --> C["POST /relation.Friend/ApplyFetch<br/>先读 Redis 缓存 再按 version 判断"]
C --> D{"version 非 0 且与缓存相同"}
D -->|"是"| E["reply 保持 nil 返回 nil,nil<br/>网关序列化失败"]
D -->|"否"| F["CollectionFriendApply<br/>按 to_id 取最近 50 条"]
B --> G["POST /relation.Friend/ApplyGet<br/>只按 identity 查 不校验归属"]
G --> H["relation_friend_apply_message 全量留言"]
B --> I["POST /relation.Friend/ApplyDoPass<br/>直接用请求的 friend_relation_id 建好友"]
I --> J["relation_friend 只写接受方一侧"]
I --> K["按 to_identity + apply_identity 置 status=1<br/>不检查影响行数"]
L["POST /relation.Follow/Doing<br/>按 from/to 直接插入 无查重 无自关注校验"] --> M["relation_follow"]
N["POST /relation.Follow/Undo"] --> O["Delete identity=?<br/>丢弃 claims 无归属校验"]
P["POST /relation.Match/DoIgnore"] --> O
Q["POST /relation.Match/DoJoin"] --> M
R["POST /relation.Match/Fetch"] --> S["GetrelationInfoDetailCardByMatch<br/>别名 pe 无效 且 Order by 在 Limit 之后<br/>SQL 必然报错"]
T["POST /relation.Friend/Fetch"] --> U["CollectionFriendData<br/>按 relation_id 过滤 列不存在<br/>错误被忽略 好友列表恒空"]
```
## 6. 审计发现
### 6.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高 ** | `internal/logic/friend/apply_get.go:17,26-42` | **好友申请详情可越权读取 ** 。入口丢弃 claims( `:17` ) , `Where("identity=?", in.Identity).First(&apply)` 只按申请 identity 定位,没有 `from_identity/to_identity` 限制 → 任意登录用户拿到一个申请 identity 就能读到该申请双方的资料卡(生日/地区/签名)与全部留言。 |
| **高 ** | `internal/logic/friend/apply_do_message.go:22-40` | **可向任意申请追加留言 ** 。只校验 `Body != "" && ApplyId != 0` ,随后用调用者身份 `Create` 留言并把 `last_message_id` 回写到 `apply_id` ( `:40` 按 `id=?` 更新,无归属条件)→ 可污染他人申请会话,也可给不存在的 `apply_id` 造孤儿留言。 |
| **高 ** | `internal/logic/friend/apply_do_pass.go:24-47` | **可通过申请单方面加任意陌生人为好友 ** 。好友记录直接用请求里的 `friend_relation_id` /`friend_relation_identity` 构建(`:29-32` ) , **不与 `apply_identity` 对应的申请做比对**;随后的 `UpdateColumn("status", 1)` 虽限定 `to_identity=auth.Identity` ,但**不检查 `RowsAffected` **( `:43-47` )→ 传自己的身份 + 任意陌生人的 id/identity 即可在自己名下生成好友记录(无需对方同意),且申请状态是否真的被改无法感知。 |
| **高 ** | `internal/logic/friend/{delete.go:27,modify_nickname.go:26,do_popular.go:27,undo_popular.go:27}` | **好友操作缺归属判定(且列名本身不存在) ** :四处统一写 `Where("relation_id=? and identity=?", auth.ID, in.Identity)` 。`relation_friend` 的归属列是 `passport_id` ( `relation_friend.go:10` + `bsm-sdk/core/types/db.go:52-55` ),表中**没有 `relation_id` **;同时 `in.Identity` 的语义是"好友的 identity",而模型里的 `identity` 是好友条目自身标识 → 归属校验条件整体失效。当前因列不存在必然报错(见 6.2),一旦只把 `relation_id` 改成 `passport_id` ,仍会因为 `identity` 语义错位而可以改/删他人好友条目。 |
| **高 ** | `internal/logic/follow/undo.go:18,26` | **可撤销他人关注关系 ** 。入口丢弃 claims, `Model(&RelationFollow{}).Delete("identity=?", in.Identity)` 只按关注条目 identity 删除,没有 `from_identity = 调用者` 的限制 → 任何登录用户凭 `Follow.Fetch` 返回的 identity 即可删除别人的关注。 |
| **高 ** | `internal/logic/match/do_ignore.go:19,27` | **可删除他人关注关系(且操作对象错误) ** 。同样丢弃 claims 后 `Delete("identity=?", in.Identity)` ;更严重的是它操作的是 `relation_follow` ( `:27` )——"忽略匹配"本应作用于 `relation_match` ,因此该接口既无权限、又改错表,`relation_match` 记录永远不会被忽略。 |
| **高 ** | `internal/logic/friend/tag_do_update.go:39` | **可删除他人标签内的成员 ** 。DEL 分支 `Delete("friend_identity=? and tag_identity=?")` 没有 `passport_id` 限定 → 任何登录用户凭 `tag_identity` 即可移除别人标签里的成员。 |
| 中 | `internal/logic/friend/tag_do_update.go:24-37` | ADD 分支不校验 `tag_identity` 是否属于调用者、不校验 `friend_identity` 是否为好友、无去重 → 可往他人标签里塞任意 identity, 也可重复插入同一(标签,成员)。 |
| 中 | `internal/logic/friend/tag_do_create.go:25-52` | 创建标签不校验 `friend_identity` 列表是否为调用者的好友、标签名无去重与长度校验(列宽 255, 超长即 DB 报错)→ 标签可被写成任意 identity 集合。 |
| 中 | `internal/logic/follow/doing.go:24-38` | 关注无幂等、不校验目标存在、不禁止关注自己(`from == to` ),且 `relation_follow` 无唯一约束 → 同一关系可被无限重复写入,直接污染关注计数(`follow/fetch.go:49` 的 `Count` )。 |
| 中 | `internal/logic/friend/get.go:15-29` 、`internal/logic/match/get.go:16-28` 、`internal/logic/common/const.go:19-22` | `Friend.Get` /`Match.Get` 只验 JWT 就返回任意 identity 的完整资料卡(昵称/头像/生日/性别/省市/签名),不校验好友或匹配关系 → 任意登录用户可批量拉取用户资料。 |
| 低 | `internal/logic/friend/apply_do.go:23-25` | 只校验 `ToIdentity/Body/ToId` 非空,不校验目标存在、不禁止加自己、不限频率,且同一对用户可无限重复申请(无查重、无唯一约束)。 |
### 6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高 ** | `internal/logic/friend/tag_fetch.go:22` | **必然空指针 panic ** :命名返回值 `reply *pb.FriendTagsReply` 此时为 nil, 第 22 行却直接 `Count(&reply.Total)...Scan(&reply.Data)` —— 对 nil 指针取字段地址即 panic( 且该 panic 发生在任何 DB 错误之前, net/http / gRPC 都不会返回业务错误码)。同一行还用 `Where("relation_id=?", auth.ID)` 过滤 `relation_friend_tag` ,该列不存在(模型是 `passport_id` )。 |
| **高 ** | `internal/logic/friend/fetch.go:29-50` 、`internal/logic/friend/apply_fetch.go:29-50` | **缓存分支两处致命缺陷 ** 。① 当 `in.Version != 0` 且与 `cache.Version` 相等时,第 21-27 行和第 35-40 行两个赋值分支都不会执行,`reply` 保持 **nil ** → 第 51 行 `return reply, nil` 返回 `(nil, nil)` , gRPC/gateway 序列化空消息失败(表现为空响应/500) 。② 第 47-49 行判断的是 `cacheErr != nil` 却打印 `err.Error()` —— 走到该分支时 `err` 必为 nil( `:42` 已判过),**必然 panic**;可复现路径:`SetCache` 失败但 `GetCache` 命中(如 Redis 只读/写入受限)。 |
| **高 ** | `internal/logic/common/const.go:120,128,134` + `friend/{delete.go:27,modify_nickname.go:26,do_popular.go:27,undo_popular.go:27,tag_fetch.go:22,tag_member_fetch.go:27}` | **好友/标签查询统一用了不存在的列 `relation_id` ** 。7 张模型里只有 `passport_id` /`passport_identity` ( `relation_friend.go:10` 、`friend_tag.go:10` 、`friend_in_tag.go:10` ),全仓库没有任何 `relation_id` 的定义;按模型声明,这些语句会因列不存在全部失败 → 涉及 **Friend 的 Fetch/Delete/ModifyNickname/DoPopular/UndoPopular/TagFetch/TagMemberFetch 共 7 个接口 ** 。(表结构为外部建表,若线上确有 `relation_id` ,则说明模型与真实表不一致,同样需要修正其中一侧。) |
| **高 ** | `internal/logic/common/const.go:120-146` | **好友列表恒为空 ** : `:120` 的 `Find` **没有检查错误 ** (列名问题被静默吞掉),`relationFriend` 始终为空 → `ids` 为空、`:139` 的 `GetrelationInfoDetailCardById` 用空 `ids` 查询。**即便把 `relation_id` 改成正确列名**, `:123` 收集的是 `relation_friend.id` (好友条目自增主键),而 `GetrelationInfoDetailCardById` ( `:26` )拿它去 `relation_extend` 按用户身份匹配,且列名写成 `relation_id in ?` (与同表 `:20` 的 `relation_identity = ?` 自相矛盾,`relation_extend` 无 `relation_id` 列)→ 语义与列名双重不匹配,好友卡片无法装配;正确来源应是 `item.FriendrelationIdentity` ( `relation_friend.go:12` )。此外 `:141-142` 回填的 `RemarkName` /`Popular` 不在 `Filed` 的查询列里(`common/const.go:15` ),备注与置顶**恒为空**; `:134` 的 `Scan(&reply.Tags)` 直接把关系表扫进 `TagItem` , `TagItem.friend_total` (标签成员数)从未统计,**恒为 0**(标签名 `tag_name` 也取不到 `name` 列)。 |
| **高 ** | `internal/logic/friend/tag_member_fetch.go:27` | **标签成员接口三处错误叠加 ** :① 表选错——标签成员在 `relation_friend_in_tag` ,却查 `RelationFriend` ;② 列不存在——`relation_id` 、`group_identity` ( `group_identity` 是 group 模块的字段,见 `module/social/group/internal/models/group_member.go:12` );③ `Pluck("relation_id", ids)` 的 dest **传值而非指针 ** , GORM 回填时会对不可寻址切片执行 `Statement.ReflectValue.Set(...)` ( `gorm.io/gorm@v1.31.2/scan.go:301-302,347-348` )→ 查询成功即 panic。 |
| **高 ** | `internal/logic/common/const.go:155-172` + `internal/logic/match/fetch.go:41` | **匹配列表 SQL 无效 ** : `From relation_match as rm,relation_extend as pe Left join pe on ...` —— `pe` 是 FROM 里已定义的别名而不是表名,逗号连接又与 LEFT JOIN 混用, Postgres 会报 `relation "pe" does not exist` ; `Offset ? Limit ? Order by rm.id Desc` 把 `ORDER BY` 放在 `LIMIT/OFFSET` 之后,也是语法错误。因此 `Match.Fetch` 恒返回 `ErrDB` ; `pe.name` 与同表的 `nickname` ( `common/const.go:15` )互相矛盾,`pb.RelationItem` 里也没有 `name` 字段。 |
| **高 ** | `internal/logic/match/do_join.go:29-35` 、`internal/logic/match/do_ignore.go:27` | **匹配的两个写接口操作的是错误的对象 ** : `DoJoin` ( "执行通过,加为好友", `proto/match.proto:14-15` )插入的是 `RelationFollow` (关注记录),不会产生好友关系;`DoIgnore` 删除的是 `RelationFollow` , `relation_match` 的 `status` 从未被更新(全仓库无 `RelationMatch` 写入点)→ 匹配记录永远停在初始状态,列表反复出现同一批人。 |
| **高 ** | `internal/logic/follow/fetch.go:26-49` | `Params["direction"]` 既非 `MY` 也非 `WHO` (含未传参)时,`default` 分支直接 `return &pb.FetchRelationItemReply{}, nil` ( `:37-38` )→ **静默返回空列表而非参数错误 ** ,调用方无法区分"没有关注"与"参数写错";分页只有下限兜底 `PageSize <= 1 → 20` ( `:45-47` ) , **没有上限**。 |
| 中 | `internal/logic/friend/apply_do.go:28-61` | 申请流程三次写库(申请、留言、回写 `last_message_id` ) **无事务**,任一步失败都会留下半成品(如申请存在但没有留言);`LastMessageID` 先写 0 再回写(`:33,:57` ) ; `uint(in.ToId)` 直接截断 `int64` 。 |
| 中 | `internal/logic/friend/apply_do_pass.go:29-47` | **好友关系单向且可重复 ** :只写接受方一侧的记录(`:31-32` ),申请方的好友列表(按 `passport_id` 查)不会有这条记录;无重复添加校验,`relation_friend` 也无唯一约束。 |
| 中 | `internal/logic/friend/{apply_do_pass.go:43,apply_do_reject.go:26}` | `UpdateColumn("status", ...)` 不检查 `RowsAffected` → 对不存在、不属于自己、甚至已处理过的申请都会返回成功;已通过的申请还能再被置为 -1( `status` 无流转校验)。 |
| 中 | `internal/logic/follow/state.go:29-45` | 语义不一致:找到关注记录时返回关注条目自身的 `identity` ,未找到时返回 `vars.OK` ( `:31-39` ),同一字段两种含义;`gorm.ErrRecordNotFound` 之外统一 `ErrDB` 。 |
| 中 | `internal/logic/common/const.go:54,72,120,128` | 多处查询错误被忽略:`:54` 之后的 `Find(&applys)` 检查了错误,但 `:72` 的消息查询、`:120` /`:128` 的好友与标签关联查询都没有检查 `.Error` → 查询失败时静默返回"空关系"。 |
| 中 | `internal/logic/friend/modify_nickname.go:22-24` | 与契约矛盾:`proto/friend.proto:19` 写"如果要清除备注,直接nickname传空值",实现却在 `Nickname == ""` 时返回 `ErrInvalidArgument` → **备注无法清除 ** 。 |
| 低 | `internal/logic/match/fetch.go:23-28` 、`internal/logic/follow/fetch.go:42-47` | 两处分页兜底重复实现,且把 `int64` 页码/页大小转成 `int` 后再算 `Offset` ( `int(in.PageNo-1)*int(in.PageSize)` ),无溢出与上限保护。 |
| 低 | `internal/logic/common/const.go:44` | 好友申请列表硬编码 `maxSize = 50` , `VersionRequest` 也没有分页字段(`proto/const.proto:21-23` )→ 超过 50 条的申请永远不会出现在列表里,且没有任何提示。 |
| 低 | `internal/logic/common/const.go:172` | `Raw(...).Scan(&cards)` 的 `cards` 为 `[]*pb.RelationItem` ,属于"把 pb 结构当表模型用",列名必须与 pb 字段严格一致(当前 `name` 就落空),无编译期保护。 |
### 6.3 未完成实现
| 类别 | 内容 | 证据 |
| --- | --- | --- |
| TODO 占位 + 死代码 | `follow.Do` (入口走 `Doing` ),全仓库无调用点 | `internal/logic/follow/do.go:26` 、`internal/server/follow_server.go:19-21` |
| 空实现(无 TODO 标注) | `Friend.Search` | `internal/logic/friend/search.go:18-21` |
| 数据初始化缺失 | `models` 包无 `InitData` ( `models/query.go` 、`models/base.go` 各 1 行),`cmd/main/main.go:37` 的注册被注释 → 需要种子数据只能人工写库 | `internal/models/query.go:1` 、`cmd/main/main.go:37` |
| 匹配记录无写入路径 | `relation_match` 只被 `Count` 读取,模块内无任何插入/更新 | `internal/logic/match/fetch.go:34` ( `grep RelationMatch` 全仓库仅命中此处与模型定义) |
| gateway 未接线 | `pb.RegisterFollowHandlerServer` / `RegisterFriendHandlerServer` / `RegisterMatchHandlerServer` 在模块内**无调用点**; `internal/server/new.go:20-25` 未创建 `ServeMux` | `internal/server/new.go:20-33` 、`cmd/main/main.go:32` |
| 未接入聚合入口 | `pkgs/all/internal/service/service.go:18-32` ( 15 项)与 `pkgs/ecmall/internal/service/service.go:18-32` ( 14 项)均无 `social/*` ;本模块无 `service/` 目录,没有 `Expose/Dependencies` | `pkgs/all/internal/service/service.go:18-33` 、`pkgs/ecmall/AGENT.md:26` |
| 无建表手段 | `database.AppendMigrate` /`AutoMigrate` 在 `module/social/**` 无匹配 → 7 张表依赖外部 SQL, 仓库内无脚本 | `grep AppendMigrate module/social` 无结果 |
| README 空壳 | 全文只有标题 | `README.md:1-2` |
### 6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `internal/server/new.go:20-25` + `cmd/main/main.go:32` | `Server.Mux` ( `new.go:16` )声明后从未赋值,`GatewayMux: s.Mux` 传给 SDK 的是**类型非空、值为 nil** 的 `*runtime.ServeMux` ; `service.Start` 用它直接 `http.ListenAndServe` ( `bsm-sdk/core/service/service.go:106-111,120-129` ),请求到达时 `runtime.ServeMux.ServeHTTP` 会读取自身字段(`grpc-gateway/v2@v2.30.0/runtime/mux.go:407-437` )→ **nil 接收者 panic ** ( net/http 按连接 recover, 客户端表现为连接中断) → `Gateway.Enable: true` 得到的 HTTP 侧完全不可用。 |
| 中 | `internal/config/config.go:56` | `conf.NotNil(Spec.Service, Spec.Cache)` 未校验 `Databases` ,而 `with.Databases` 在 `cfg == nil` 时 `panic("No Database Source Found !")` ( `bsm-sdk/core/with/databases.go:12-15` )→ 配置漏配即启动 panic。当前 `etc/relation_dev.yaml:4-7` 有 `Databases` 段,可正常初始化。 |
| 中 | `etc/relation_dev.yaml:16` + `internal/impl/impl.go:24` | `MicroService.Anonymous` 写的是 `relation.ping.hello` ,而本服务只有 `Follow` /`Friend` /`Match` 三个 service、不存在 `ping` 方法(`internal/server/new.go:28-30` )→ 匿名清单无效;配置中**没有 `Etcd` 段**, `with.Etcd(nil)` 直接返回 nil( `bsm-sdk/core/with/etcd.go:14-17` ),一旦把 `MicroService.Enable` 改成 true, `service.Start` 会因 `EtcdClient == nil` 执行 `os.Exit(1)` ( `bsm-sdk/core/service/service.go:66-70` )。 |
| 中 | `etc/relation_prod.yaml:31-35` | `KycConf` 是死配置:`SrvConfig` 只映射 `Kyc` ( `internal/config/config.go:25` ) , `KycConf` 整块不会被读取( dev/test 两份没有这一段,属复制粘贴残留)。 |
| 中 | `etc/relation_dev.yaml` 、`etc/relation_prod.yaml` 、`etc/relation_test.yaml` | 三份配置除 `Port` ( 12248 / 12426 / 12426) 外**逐行相同**,全部指向 `127.0.0.1` + `dbname=bsm_dev` 、相同 `Gateway.Port: 12425` → 环境无法区分;数据库口令以明文形式入库(`password=CHANGE_ME` )。 |
| 低 | `internal/logic/common/const.go:176-188` | 缓存写入 `Set(..., 0)` (永不过期,`:182` ),而 `Friend.Delete` /`ApplyDoPass` /`ApplyDoReject` 等变更路径**没有任何缓存失效**(全模块无 `Del` 调用)→ 好友/申请列表会长期返回旧数据,只能依赖 `version` 变化被动刷新。 |
| 低 | `internal/logic/friend/fetch.go:29` 、`internal/logic/friend/apply_fetch.go:29` | `cache, err := common.GetCache(...)` 复用了上一段的 `err` ,把 `CollectionFriendData/CollectionFriendApply` 的错误静默覆盖 → 错误归因混乱(也是 6.2 中 `cacheErr` /`err` 混用问题的根源)。 |
| 低 | `internal/impl/impl.go:21-24` | `MemorySerice` 初始化后全模块零使用;`EtcdService` 只出现在被跳过的注册路径( Redis 仅用于 `common` 的缓存,属实际使用)。 |
| 低 | `internal/models/relation_friend.go:11-12` | 字段命名 `FriendrelationID` /`FriendrelationIdentity` 与库里 `friend_relation_id` 的断词方式不一致(`FriendRelationID` 更贴切),同一文件也无文件头注释(其余模型文件都有)。 |
| 低 | 全模块 | 0 个 `*_test.go` ; `test/` 目录不存在,无任何请求样例;`cmd/cli/main.go` 只打印配置。 |
## 7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| R1 | 高 | 好友申请详情无归属校验,可读取他人申请与留言 | 隐私泄露 |
| R2 | 高 | 可向任意申请追加留言(不校验归属与存在性) | 数据污染、可伪造会话 |
| R3 | 高 | 通过申请时不校验申请与好友参数,可单方面加任意人为好友 | 越权、关系数据失真 |
| R4 | 高 | 好友删除/备注/置顶的归属条件列名与语义双错 | 功能不可用 + 越权风险 |
| R5 | 高 | `Follow.Undo` 、`Match.DoIgnore` 按 identity 无归属删除 | 可删他人关注关系 |
| R6 | 高 | `TagDoUpdate` DEL 无归属限定, ADD 无归属与好友校验 | 可改他人标签 |
| R7 | 高 | `Friend.TagFetch` 对 nil reply 取字段地址 → 必然 panic | 接口崩溃 |
| R8 | 高 | `Friend.Fetch` /`ApplyFetch` 可返回 `(nil, nil)` 、并在 `cacheErr` 分支 panic | 接口不可用/崩溃 |
| R9 | 高 | 7 个接口按不存在的列 `relation_id` 查询 | 好友与标签功能整体不可用 |
| R10 | 高 | `CollectionFriendData` 吞错误 + `Filed` 缺列 + `friend_total` 未统计 | 好友列表恒空、备注/置顶/标签数恒空 |
| R11 | 高 | `TagMemberFetch` 表错 + 列错 + `Pluck` 非指针 dest | 接口不可用或 panic |
| R12 | 高 | `GetrelationInfoDetailCardByMatch` SQL 无效(别名 `pe` 、`Order by` 位置) | 匹配列表恒失败 |
| R13 | 高 | `Match.DoJoin` /`DoIgnore` 操作关注表,匹配状态永不更新 | 功能错位、匹配列表重复 |
| R14 | 中 | 关注无幂等、自关注未拦截;关系表无唯一约束 | 关注计数虚高 |
| R15 | 中 | 申请流程无事务、好友关系单向、状态无流转校验 | 半成品/不一致数据 |
| R16 | 中 | `Follow.Fetch` 参数未识别时静默返回空;无分页上限 | 功能不可用 + 资源风险 |
| R17 | 中 | gateway 未接线、`Mux` 为 nil、无 `service.Expose` | HTTP 不可用、无法被聚合 |
| R18 | 中 | `Databases` 未校验、`KycConf` 死配置、三份 yaml 同内容、缺 `Etcd` 段 | 部署脆弱、环境不分 |
| R19 | 低 | `follow.Do` 死代码、空 `InitData` 、缓存无失效、命名不一致、无测试、README 空壳 | 可维护性 |
## 8. 修复建议(务实项)
1. **补归属校验 ** ( R1/R2/R3/R4/R5/R6) : 所有涉及"某个人的关系记录"的写/读接口,把 `auth` 从 `ParseMetaCtx` 取回(不要写 `_` ),并在 SQL 条件里加上归属列——关系记录用 `passport_id = auth.ID` ( `relation_friend` 、`relation_friend_tag` 、`relation_friend_in_tag` ),关注记录用 `from_identity = auth.Identity` ( `relation_follow` ) ; `ApplyGet` /`ApplyDoMessage` 加 `to_identity = auth.Identity OR from_identity = auth.Identity` ; `ApplyDoPass` 先用 `apply_identity` 查出申请,核对 `to_identity == auth.Identity` 且 `from_identity == in.FriendRelationIdentity` 之后再建好友记录,并检查 `RowsAffected` 。
2. **修列名 ** ( R4/R9/R10) : 把 7 处 `relation_id=?` 改成模型里真实存在的列——好友条目按 `passport_id` 归属、按 `friend_relation_identity` (或 `identity` ,需统一语义)定位目标;`TagFetch` 的标签归属用 `passport_id` ; `TagMemberFetch` 改为 `relation_friend_in_tag` 按 `passport_id + tag_identity` 取 `friend_identity` 列表(`Pluck("friend_identity", &ids)` , dest 必须取地址)。
3. **修 panic 与返回 ** ( R7/R8/R11) : `TagFetch` 先 `reply = &pb.FriendTagsReply{}` 再用 `&reply.Total` ; `Friend.Fetch` /`ApplyFetch` 把 `cacheErr` 分支的打印改为 `cacheErr.Error()` ,并在两个赋值分支都不成立时补一次真实查询(或在 `Version == 0` 缺缓存时直接查库),避免返回 nil。
4. **修好友列表装配 ** ( R10) : `CollectionFriendData` 里给 `:120` /`:128` 的 `Find` 加错误检查;`:123` 收集的 id 改为 `item.FriendrelationIdentity` (好友的用户身份),并把 `GetrelationInfoDetailCardById` ( `common/const.go:26` )的 `relation_id in ?` 改成 `relation_identity in ?` (与 `:20` 统一);`Filed` ( `common/const.go:15` )补上 `remark_name` 、`popular` ;标签成员数改为按 `tag_identity` 聚合一次 `count` 再填 `TagItem.friend_total` 。
5. **修匹配链路 ** ( R12/R13) : `GetrelationInfoDetailCardByMatch` 的 SQL 改为 `FROM relation_match rm JOIN relation_extend pe ON pe.relation_identity = rm.recommend_identity WHERE rm.relation_identity = ? ORDER BY rm.id DESC LIMIT ? OFFSET ?` ,并统一用 `nickname` (与 `Filed` 一致);`Match.DoJoin` 改为写入 `relation_friend` (复用 `ApplyDoPass` 的建好友逻辑),`Match.DoIgnore` 改为按 `relation_identity + recommend_identity` 更新/删除 `relation_match` ,两者都补 `RowsAffected` 校验。
6. **补幂等与事务 ** ( R3/R14/R15) : `Follow.Doing` 先按 `from_identity + to_identity` 查一次(并拒绝 `from == to` ) ; `ApplyDo` 前查是否已有同类申请或已是好友;`ApplyDoPass` 建好友前查重;`ApplyDo` 的三次写入与 `ApplyDoPass` 的"建好友 + 改申请状态"分别放进 `impl.DBService.Transaction` 。
7. **修参数与分页 ** ( R16) : `Follow.Fetch` 的 `default` 分支返回 `ErrInvalidArgument` 而不是空列表;`follow` /`match` 的分页补上限(如 50) , `CollectionFriendApply` 的 `maxSize=50` 改为可传入的分页参数;`Follow.State` 的返回语义固定为一种(建议只返回"是否关注"与关注条目 identity, 不再用 `vars.OK` 表达未关注)。
8. **清理与一致性 ** ( R19) : 删除 `internal/logic/follow/do.go` 的死代码 `Do` (或在其真正实现前改为返回 `codes.Unimplemented` ) ; `Friend.Search` 未实现前返回明确的未实现错误;`nickname` 空值清除备注的语义按 `proto/friend.proto:19` 实现;`Friend.Delete` /`ApplyDoPass` /`ApplyDoReject` 变更后删除对应缓存 key; `KycConf` ( `etc/relation_prod.yaml:31-35` )删除。
9. **接线与配置 ** ( R17/R18) : `internal/server/new.go` 中 `Mux: gwRuntime.NewServeMux()` 并调用 `pb.RegisterFollowHandlerServer` /`pb.RegisterFriendHandlerServer` /`pb.RegisterMatchHandlerServer` ;或在 `service/` 下补与 wallet 同构的 `Expose` 以便接入 `pkgs/all` ; `config.New` 补 `conf.NotNil(Spec.Databases)` ;三份 yaml 区分环境与口令来源,`Anonymous` 改为真实存在的方法或删除,若要启用微服务注册需补 `Etcd` 段。
10. **测试 ** :为 `Follow.Doing/Undo` (幂等与归属)、`Friend.ApplyDo/ApplyGet` (归属)、`TagFetch` (不再 panic) 、`Fetch` (缓存三条分支)各补一条正例 + 一条边界用例(当前 0 测试)。
> 本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。为 relation 引入"统一关系模型""好友/关注抽象层""DTO/VO 分层""缓存统一框架"一类改造不在建议范围内——上述 10 条都是对现有函数、SQL 与配置的最小修正。
2026-09-22 21:15:34 +08:00
## 9. 整改记录( 2026-09-22)
> 本节记录按本报告结论执行的代码整改。整改遵循**最小修正**原则: 未引入新框架、抽象层、DTO/VO、事件总线, 未拆分服务边界, **未修改任何 `proto/*.proto` 与生成的 `pb/*.go`**;新增/修改注释均为中文;口令类摘要统一使用 bcrypt( 验证码等短时效一次性凭证仍按原有 Redis 明文比对链路存储)。校验方式:`GOWORK=off go build ./...` + `GOWORK=off go vet ./...` + `gofmt -l`(仓库根 workspace 模式存在 genproto 拆包的 ambiguous import, 属本机既有问题) 。
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| R1 | 高 | 好友申请详情无归属校验,可读取他人申请与留言 | 已修复:查询条件加归属限定 |
| R2 | 高 | 可向任意申请追加留言(不校验归属与存在性) | 已修复:追加前校验该申请属于当前调用者 |
| R3 | 高 | 通过申请时不与申请记录比对,可单方面加任意人为好友 | 已修复:通过前先按 identity 查出申请记录并校验双方身份,再建立好友关系 |
| R4 | 高 | 好友删除/备注/置顶的归属条件列名与语义双错 | 已修复:按模型真实归属列修正,只能操作自己的关系记录 |
| R5 | 高 | `Follow.Undo` 、`Match.DoIgnore` 无归属删除 | 已修复:删除条件加当前用户归属 |
| R6 | 高 | `TagDoUpdate` DEL 无归属限定、ADD 无归属与好友校验 | 已修复: 两处均加归属条件, ADD 时校验好友关系存在 |
| R7 | 高 | `Friend.TagFetch` 对 nil reply 取字段地址必然 panic | 已修复:先判空再取字段 |
| R8 | 高 | `Friend.Fetch` /`ApplyFetch` 可返回 `(nil, nil)` 并在 `cacheErr` 分支 panic | 已修复:返回非 nil 空集合,修掉 nil 解引用 |
| R9 | 高 | 7 个接口按不存在的列 `relation_id` 查询 | 已修复:全部改为模型实际存在的列,好友与标签功能恢复可用 |
| R10 | 高 | `CollectionFriendData` 吞错误 + `Filed` 缺列 + `friend_total` 未统计 | 已修复:检查 `.Error` 、修正列名、补上统计 |
| R11 | 高 | `TagMemberFetch` 表错 + 列错 + `Pluck` 非指针 dest | 已修复:修正表名列名,`Pluck` 传指针 |
| R12 | 高 | `GetrelationInfoDetailCardByMatch` SQL 无效 | 已修复:修正别名与 `ORDER BY` 位置,匹配列表可用 |
| R13 | 高 | `Match.DoJoin` /`DoIgnore` 操作关注表,匹配状态永不更新 | 已修复:改为更新匹配记录表的状态字段 |
### 未纳入本轮范围
报告中「中」「低」级别的项( 分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。