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

40 KiB
Raw Blame History

relation好友、关注与匹配关系代码审计报告

内容
审计对象 module/social/relationmodule 路径 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.gogRPC + grpc-gateway 单进程dev Port: 12248 / prod 12426Gateway.Port: 12425未被 pkgs/allpkgs/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 还必然空指针 panicFollow.Fetch/Match.Fetch 分别因 Params 静默空返回与无效 SQL 不可用;Match 的两个写接口改的其实是关注表;缓存分支可返回 (nil, nil) 或 panicfollow.Do 是遗留占位死代码;网关未接线、无聚合入口,etcKycConf 与匿名清单均为死配置。

1. 服务定位与职责

维护三类社交关系与其辅助数据:

  • 好友relation_friend(好友条目、备注、置顶、会话 IDrelation_friend_apply(申请)、relation_friend_apply_message(申请留言)、relation_friend_tag + relation_friend_in_tag(好友标签分组);
  • 关注relation_followfrom/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.Newsrv.Start()srv.Use(models.InitData) 被注释(:37
cmd/cli/main.go 调试入口,只做 config.New("relation") + fmt.Println(config.Spec.Databases)
internal/config/config.go SrvConfigBase、Databases、MicroService、Rpc、Gateway、APM、Etcd、WeChatConf、Token、Kycconf.NotNil(Service, Cache) 校验
internal/impl/impl.go 初始化 Memory、Redis、DB、Etcd 四个全局实例
internal/server/new.go 创建 grpc.Server,注册 FollowFriendMatch 三个 serviceprotoc-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/*.go17 个) 好友基础操作、申请管理、标签管理
internal/logic/match/{fetch,get,do_join,do_ignore}.go 匹配列表/详情/通过/忽略
internal/models/*.go9 个) 7 张表模型 + query.gobase.go(后两者各 1 行,仅 package models
proto/{follow,friend,match,const}.proto 契约;公共消息放在 proto/const.protopackage 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:21scripts/api-docgen

3. 接口清单

HTTP 路径由生成代码固定为 /relation.{服务}/{方法}pb/follow.pb.gw.go:344-347pb/friend.pb.gw.go:1176-1192pb/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:16SQL 无效,见 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),要求有效 JWTopts 均为 nil,即不校验角色、不校验私有 IP。其中 Follow.UndoFriend.SearchFriend.ApplyGetMatch.GetMatch.DoIgnore 以及死代码 follow.Do_, err = ... 丢弃 claimsfollow/undo.go:18friend/search.go:14friend/apply_get.go:17match/get.go:16match/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.Doinggrep "follow.Do(" 全仓库无调用
只有参数校验、无查询 Friend.Search internal/logic/friend/search.go:18-21(返回空的 &pb.PartFriendReply{}
模型存在、无写入路径 RelationMatch(匹配记录) 全仓库对 RelationMatch 的唯一使用是 internal/logic/match/fetch.go:34Count没有任何 Create/Update → 匹配记录只能靠外部写入
空初始化被注释 models.InitDatamodels/query.go 只有 1 行 package modelscmd/main/main.go:37 的调用已被注释 internal/models/query.go:1internal/models/base.go:1cmd/main/main.go:37

4. 数据模型与表

模型未注册任何自动迁移:database.AppendMigrate / AutoMigratemodule/social/**无匹配grep AppendMigrate module/social 返回空7 张表依赖外部建表脚本,仓库内没有对应 SQL。公共字段来自 SDKbsm-sdk/core/types/db.gotypes.Std_IICUDS = id(PK) / identity varchar(36) uniqueIndex / created_at / updated_at / deleted_at(index) / status int8 default 0 indextypes.Std_Passport = passport_id uint index / passport_identity varchar(36) index

relation_friendinternal/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 会话 IDcommon.UniqueSessionIDapply_do_pass.go:33

表上没有 (passport_id, friend_relation_identity) 唯一约束,重复加好友只能靠代码判断(当前没有判断);也没有 relation_id,但 6 处查询在用它(见 6.2)。

relation_followinternal/models/relation_follow.go:8-17

字段 类型 键/约束 说明
id / identity uint / varchar(36) PK / uniqueIndex 关注条目标识(doing.go:32do_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_matchinternal/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_applyinternal/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) status1 通过(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 最后一条留言 IDapply_do.go:57apply_do_message.go:40 回写)

(from_identity, to_identity) 唯一约束 → 同一对用户可重复申请(当前无查重)。

relation_friend_apply_messageinternal/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_taginternal/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,而 TagFetchCollectionFriendData 都在用它过滤(见 6.2)。

relation_friend_in_taginternal/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-47tag_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-52tag_do_update.go:30-37)不校验标签归属与好友关系。

外部表 relation_extend(仓库内无模型)

internal/logic/common/const.go:20,26,166 三处查询 relation_extend,但仓库内既没有该表的模型也没有建表 SQL表结构无法从代码确证【信息不足】。可确证的是同一张表的三处查询列名互相矛盾Filedcommon/const.go:15)取 nickname/avatar/sex/province/city/area/sign,而 GetrelationInfoDetailCardByMatchcommon/const.go:158)取 pe.name,且 pb.RelationItemproto/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:17Where("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:40id=? 更新,无归属条件)→ 可污染他人申请会话,也可给不存在的 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_idrelation_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 可撤销他人关注关系。入口丢弃 claimsModel(&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:49Count)。
internal/logic/friend/get.go:15-29internal/logic/match/get.go:16-28internal/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-50internal/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_identityrelation_friend.go:10friend_tag.go:10friend_in_tag.go:10),全仓库没有任何 relation_id 的定义;按模型声明,这些语句会因列不存在全部失败 → 涉及 Friend 的 Fetch/Delete/ModifyNickname/DoPopular/UndoPopular/TagFetch/TagMemberFetch 共 7 个接口。(表结构为外部建表,若线上确有 relation_id,则说明模型与真实表不一致,同样需要修正其中一侧。)
internal/logic/common/const.go:120-146 好友列表恒为空:120Find 没有检查错误(列名问题被静默吞掉),relationFriend 始终为空 → ids 为空、:139GetrelationInfoDetailCardById 用空 ids 查询。即便把 relation_id 改成正确列名:123 收集的是 relation_friend.id(好友条目自增主键),而 GetrelationInfoDetailCardById:26)拿它去 relation_extend 按用户身份匹配,且列名写成 relation_id in ?(与同表 :20relation_identity = ? 自相矛盾,relation_extendrelation_id 列)→ 语义与列名双重不匹配,好友卡片无法装配;正确来源应是 item.FriendrelationIdentityrelation_friend.go:12)。此外 :141-142 回填的 RemarkName/Popular 不在 Filed 的查询列里(common/const.go:15),备注与置顶恒为空:134Scan(&reply.Tags) 直接把关系表扫进 TagItemTagItem.friend_total(标签成员数)从未统计,恒为 0(标签名 tag_name 也取不到 name 列)。
internal/logic/friend/tag_member_fetch.go:27 标签成员接口三处错误叠加:① 表选错——标签成员在 relation_friend_in_tag,却查 RelationFriend;② 列不存在——relation_idgroup_identitygroup_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 existOffset ? Limit ? Order by rm.id DescORDER BY 放在 LIMIT/OFFSET 之后,也是语法错误。因此 Match.Fetch 恒返回 ErrDBpe.name 与同表的 nicknamecommon/const.go:15)互相矛盾,pb.RelationItem 里也没有 name 字段。
internal/logic/match/do_join.go:29-35internal/logic/match/do_ignore.go:27 匹配的两个写接口操作的是错误的对象DoJoin"执行通过,加为好友"proto/match.proto:14-15)插入的是 RelationFollow(关注记录),不会产生好友关系;DoIgnore 删除的是 RelationFollowrelation_matchstatus 从未被更新(全仓库无 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,:57uint(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 → 对不存在、不属于自己、甚至已处理过的申请都会返回成功;已通过的申请还能再被置为 -1status 无流转校验)。
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-28internal/logic/follow/fetch.go:42-47 两处分页兜底重复实现,且把 int64 页码/页大小转成 int 后再算 Offsetint(in.PageNo-1)*int(in.PageSize)),无溢出与上限保护。
internal/logic/common/const.go:44 好友申请列表硬编码 maxSize = 50VersionRequest 也没有分页字段(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:26internal/server/follow_server.go:19-21
空实现(无 TODO 标注) Friend.Search internal/logic/friend/search.go:18-21
数据初始化缺失 models 包无 InitDatamodels/query.gomodels/base.go 各 1 行),cmd/main/main.go:37 的注册被注释 → 需要种子数据只能人工写库 internal/models/query.go:1cmd/main/main.go:37
匹配记录无写入路径 relation_match 只被 Count 读取,模块内无任何插入/更新 internal/logic/match/fetch.go:34grep RelationMatch 全仓库仅命中此处与模型定义)
gateway 未接线 pb.RegisterFollowHandlerServer / RegisterFriendHandlerServer / RegisterMatchHandlerServer 在模块内无调用点internal/server/new.go:20-25 未创建 ServeMux internal/server/new.go:20-33cmd/main/main.go:32
未接入聚合入口 pkgs/all/internal/service/service.go:18-3215 项)与 pkgs/ecmall/internal/service/service.go:18-3214 项)均无 social/*;本模块无 service/ 目录,没有 Expose/Dependencies pkgs/all/internal/service/service.go:18-33pkgs/ecmall/AGENT.md:26
无建表手段 database.AppendMigrate/AutoMigratemodule/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.Muxnew.go:16)声明后从未赋值,GatewayMux: s.Mux 传给 SDK 的是类型非空、值为 nil*runtime.ServeMuxservice.Start 用它直接 http.ListenAndServebsm-sdk/core/service/service.go:106-111,120-129),请求到达时 runtime.ServeMux.ServeHTTP 会读取自身字段(grpc-gateway/v2@v2.30.0/runtime/mux.go:407-437)→ nil 接收者 panicnet/http 按连接 recover客户端表现为连接中断Gateway.Enable: true 得到的 HTTP 侧完全不可用。
internal/config/config.go:56 conf.NotNil(Spec.Service, Spec.Cache) 未校验 Databases,而 with.Databasescfg == nilpanic("No Database Source Found !")bsm-sdk/core/with/databases.go:12-15)→ 配置漏配即启动 panic。当前 etc/relation_dev.yaml:4-7Databases 段,可正常初始化。
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)→ 匿名清单无效;配置中没有 Etcdwith.Etcd(nil) 直接返回 nilbsm-sdk/core/with/etcd.go:14-17),一旦把 MicroService.Enable 改成 trueservice.Start 会因 EtcdClient == nil 执行 os.Exit(1)bsm-sdk/core/service/service.go:66-70)。
etc/relation_prod.yaml:31-35 KycConf 是死配置:SrvConfig 只映射 Kycinternal/config/config.go:25KycConf 整块不会被读取dev/test 两份没有这一段,属复制粘贴残留)。
etc/relation_dev.yamletc/relation_prod.yamletc/relation_test.yaml 三份配置除 Port12248 / 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:29internal/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.gotest/ 目录不存在,无任何请求样例;cmd/cli/main.go 只打印配置。

7. 风险汇总

编号 级别 问题 影响面
R1 好友申请详情无归属校验,可读取他人申请与留言 隐私泄露
R2 可向任意申请追加留言(不校验归属与存在性) 数据污染、可伪造会话
R3 通过申请时不校验申请与好友参数,可单方面加任意人为好友 越权、关系数据失真
R4 好友删除/备注/置顶的归属条件列名与语义双错 功能不可用 + 越权风险
R5 Follow.UndoMatch.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 无效(别名 peOrder 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所有涉及"某个人的关系记录"的写/读接口,把 authParseMetaCtx 取回(不要写 _),并在 SQL 条件里加上归属列——关系记录用 passport_id = auth.IDrelation_friendrelation_friend_tagrelation_friend_in_tag),关注记录用 from_identity = auth.Identityrelation_followApplyGet/ApplyDoMessageto_identity = auth.Identity OR from_identity = auth.IdentityApplyDoPass 先用 apply_identity 查出申请,核对 to_identity == auth.Identityfrom_identity == in.FriendRelationIdentity 之后再建好友记录,并检查 RowsAffected
  2. 修列名R4/R9/R10把 7 处 relation_id=? 改成模型里真实存在的列——好友条目按 passport_id 归属、按 friend_relation_identity(或 identity,需统一语义)定位目标;TagFetch 的标签归属用 passport_idTagMemberFetch 改为 relation_friend_in_tagpassport_id + tag_identityfriend_identity 列表(Pluck("friend_identity", &ids)dest 必须取地址)。
  3. 修 panic 与返回R7/R8/R11TagFetchreply = &pb.FriendTagsReply{} 再用 &reply.TotalFriend.Fetch/ApplyFetchcacheErr 分支的打印改为 cacheErr.Error(),并在两个赋值分支都不成立时补一次真实查询(或在 Version == 0 缺缓存时直接查库),避免返回 nil。
  4. 修好友列表装配R10CollectionFriendData 里给 :120/:128Find 加错误检查;:123 收集的 id 改为 item.FriendrelationIdentity(好友的用户身份),并把 GetrelationInfoDetailCardByIdcommon/const.go:26)的 relation_id in ? 改成 relation_identity in ?(与 :20 统一);Filedcommon/const.go:15)补上 remark_namepopular;标签成员数改为按 tag_identity 聚合一次 count 再填 TagItem.friend_total
  5. 修匹配链路R12/R13GetrelationInfoDetailCardByMatch 的 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/R15Follow.Doing 先按 from_identity + to_identity 查一次(并拒绝 from == toApplyDo 前查是否已有同类申请或已是好友;ApplyDoPass 建好友前查重;ApplyDo 的三次写入与 ApplyDoPass 的"建好友 + 改申请状态"分别放进 impl.DBService.Transaction
  7. 修参数与分页R16Follow.Fetchdefault 分支返回 ErrInvalidArgument 而不是空列表;follow/match 的分页补上限(如 50CollectionFriendApplymaxSize=50 改为可传入的分页参数;Follow.State 的返回语义固定为一种(建议只返回"是否关注"与关注条目 identity不再用 vars.OK 表达未关注)。
  8. 清理与一致性R19删除 internal/logic/follow/do.go 的死代码 Do(或在其真正实现前改为返回 codes.UnimplementedFriend.Search 未实现前返回明确的未实现错误;nickname 空值清除备注的语义按 proto/friend.proto:19 实现;Friend.Delete/ApplyDoPass/ApplyDoReject 变更后删除对应缓存 keyKycConfetc/relation_prod.yaml:31-35)删除。
  9. 接线与配置R17/R18internal/server/new.goMux: gwRuntime.NewServeMux() 并调用 pb.RegisterFollowHandlerServer/pb.RegisterFriendHandlerServer/pb.RegisterMatchHandlerServer;或在 service/ 下补与 wallet 同构的 Expose 以便接入 pkgs/allconfig.Newconf.NotNil(Spec.Databases);三份 yaml 区分环境与口令来源,Anonymous 改为真实存在的方法或删除,若要启用微服务注册需补 Etcd 段。
  10. 测试:为 Follow.Doing/Undo(幂等与归属)、Friend.ApplyDo/ApplyGet(归属)、TagFetch(不再 panicFetch(缓存三条分支)各补一条正例 + 一条边界用例(当前 0 测试)。

本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。为 relation 引入"统一关系模型""好友/关注抽象层""DTO/VO 分层""缓存统一框架"一类改造不在建议范围内——上述 10 条都是对现有函数、SQL 与配置的最小修正。