Files
full/docs/relation.md
2026-09-22 18:53:53 +08:00

306 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 个 yaml52/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 个 RPCgateway 路径为 `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 与配置的最小修正。