40 KiB
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. 核心流程
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. 修复建议(务实项)
- 补归属校验(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。 - 修列名(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 必须取地址)。 - 修 panic 与返回(R7/R8/R11):
TagFetch先reply = &pb.FriendTagsReply{}再用&reply.Total;Friend.Fetch/ApplyFetch把cacheErr分支的打印改为cacheErr.Error(),并在两个赋值分支都不成立时补一次真实查询(或在Version == 0缺缓存时直接查库),避免返回 nil。 - 修好友列表装配(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。 - 修匹配链路(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校验。 - 补幂等与事务(R3/R14/R15):
Follow.Doing先按from_identity + to_identity查一次(并拒绝from == to);ApplyDo前查是否已有同类申请或已是好友;ApplyDoPass建好友前查重;ApplyDo的三次写入与ApplyDoPass的"建好友 + 改申请状态"分别放进impl.DBService.Transaction。 - 修参数与分页(R16):
Follow.Fetch的default分支返回ErrInvalidArgument而不是空列表;follow/match的分页补上限(如 50),CollectionFriendApply的maxSize=50改为可传入的分页参数;Follow.State的返回语义固定为一种(建议只返回"是否关注"与关注条目 identity,不再用vars.OK表达未关注)。 - 清理与一致性(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)删除。 - 接线与配置(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段。 - 测试:为
Follow.Doing/Undo(幂等与归属)、Friend.ApplyDo/ApplyGet(归属)、TagFetch(不再 panic)、Fetch(缓存三条分支)各补一条正例 + 一条边界用例(当前 0 测试)。
本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。为 relation 引入"统一关系模型""好友/关注抽象层""DTO/VO 分层""缓存统一框架"一类改造不在建议范围内——上述 10 条都是对现有函数、SQL 与配置的最小修正。