# 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
建申请 + 建留言 + 回写 last_message_id
无事务 无重复校验"] --> B["relation_friend_apply"] B --> C["POST /relation.Friend/ApplyFetch
先读 Redis 缓存 再按 version 判断"] C --> D{"version 非 0 且与缓存相同"} D -->|"是"| E["reply 保持 nil 返回 nil,nil
网关序列化失败"] D -->|"否"| F["CollectionFriendApply
按 to_id 取最近 50 条"] B --> G["POST /relation.Friend/ApplyGet
只按 identity 查 不校验归属"] G --> H["relation_friend_apply_message 全量留言"] B --> I["POST /relation.Friend/ApplyDoPass
直接用请求的 friend_relation_id 建好友"] I --> J["relation_friend 只写接受方一侧"] I --> K["按 to_identity + apply_identity 置 status=1
不检查影响行数"] L["POST /relation.Follow/Doing
按 from/to 直接插入 无查重 无自关注校验"] --> M["relation_follow"] N["POST /relation.Follow/Undo"] --> O["Delete identity=?
丢弃 claims 无归属校验"] P["POST /relation.Match/DoIgnore"] --> O Q["POST /relation.Match/DoJoin"] --> M R["POST /relation.Match/Fetch"] --> S["GetrelationInfoDetailCardByMatch
别名 pe 无效 且 Order by 在 Limit 之后
SQL 必然报错"] T["POST /relation.Friend/Fetch"] --> U["CollectionFriendData
按 relation_id 过滤 列不存在
错误被忽略 好友列表恒空"] ``` ## 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 与配置的最小修正。