Files
full/docs/relation.md

332 lines
43 KiB
Markdown
Raw Normal View History

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 个 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 与配置的最小修正。
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`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。