34 KiB
group(群组与成员管理)代码审计报告
| 项 | 内容 |
|---|---|
| 审计对象 | module/social/group(module 路径 bsm/full/module/social/group) |
| 服务域 | 社交与关系 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 28 个文件 / 1011 行(含手写别名文件 pb/blocks_compat.go 6 行);生成代码 pb/ 7 个文件 / 6157 行;proto/ 3 个文件 / 465 行;etc/ 3 个 yaml + 1 个 supervisor conf;*_test.go 0 个;无 service/、无 test/ 目录 |
| 入口 | 仅独立入口 cmd/main/main.go(gRPC + grpc-gateway 单进程,Port: 12247);未被 pkgs/all、pkgs/ecmall 接入;cmd/cli/main.go 全文 4 行、main() 为空 |
| 对外协议 | gRPC 2 个 service / 13 个 RPC;gateway 路径为 POST /group.{Service}/{Method},但模块内从未注册任何 gateway handler |
| 结论摘要 | 13 个接口全部只验 JWT、全部缺群归属/角色校验;3 个核心接口用了不存在的列 creator_id 导致必然失败;群信息/群列表/成员列表/入群申请四类查询都是"查了不返回"或"SQL 报错";免验证入群开关恒为 false;Basic.Disband 是返回成功的 TODO 占位;成员计数、成员上限、群号、分页全部未落地;网关未接线(Mux 恒 nil)、未接入聚合入口,InitData 与 GroupRes 为死代码。 |
1. 服务定位与职责
维护群组基本信息(group_basic)、群成员(relation_group_member)、入群申请(relation_group_apply)与群资源模型(relation_group_res,无逻辑),对外提供建群、改群、解散、群列表/搜索、群成员列表,以及入群申请与审批(PASS/REJECT)、踢人、退群、设置管理员。
它是群组与成员关系的读写服务。群内消息、群文件、群动态(proto/const.proto:277-304 里定义了 GroupPostItem 等消息但无 service 使用)、好友/关注关系、用户资料(依赖外部表 passport_extend)均不在本模块内实现。
2. 代码结构与入口
| 路径 | 职责 |
|---|---|
cmd/main/main.go |
独立进程入口:config.New("group") → impl.NewImpl() → server.New(config.Spec.Addr) → service.New → srv.Use(models.InitData) → srv.Start() |
cmd/cli/main.go |
调试入口,全文 4 行且 main() 为空 |
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,注册 Basic、Member 两个 service(protoc-gen-slc 生成);未创建 ServeMux |
internal/server/{basic_server,member_server}.go |
13 个 RPC 的 1:1 转发(生成代码) |
internal/logic/basic/{search,fetch,get,create,modify,disband,const}.go |
群组搜索/列表/详情/创建/修改/解散 |
internal/logic/member/{fetch,do_join,join_fetch,join_do_handle,do_kick,do_quit,do_set_manager,const}.go |
成员列表、入群申请、审批、踢人、退群、管理员设置 |
internal/models/{group_basic,group_member,group_apply,group_res,query}.go |
4 张表模型 + query.go 中全部数据访问(含 4 段裸 SQL) |
pb/*.go |
生成代码;pb/blocks_compat.go 为手写类型别名(PostItem = GroupPostItem 等) |
proto/{group,member,const}.proto |
契约;公共消息放在 proto/const.proto(package blocks,与 service 的 package group 不同) |
etc/{group_dev,group_prod,group_test}.yaml + supervisor.bsm-social-group.conf |
配置与进程托管;三份 yaml 内容完全相同 |
依赖注入:本模块没有 service/ 目录,也没有 Expose/Dependencies 注入点,impl.NewImpl() 是唯一初始化路径(internal/impl/impl.go:19-25)。全仓库对 module/social/group 的引用只有 go.work:20 与 scripts/api-docgen。
3. 接口清单
HTTP 路径由生成代码固定为 /group.{服务}/{方法}(pb/group.pb.gw.go:472-477、pb/member.pb.gw.go:536-542),全部为 POST。这些 handler 在模块内从未被注册到任何 ServeMux(见 6.4)。
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
|---|---|---|---|---|
| POST | /group.Basic/Search |
搜索群组 | JWT(丢弃 claims) | 空实现 logic/basic/search.go:12 |
| POST | /group.Basic/Fetch |
我的群组列表 | JWT | logic/basic/fetch.go:14(只返回总数,见 6.2) |
| POST | /group.Basic/Get |
群组信息 | JWT(丢弃 claims) | logic/basic/get.go:14(返回值被丢弃,见 6.2) |
| POST | /group.Basic/Create |
创建群组 | JWT | logic/basic/create.go:17 |
| POST | /group.Basic/Modify |
修改群组信息 | JWT(按创建者,列名错误) | logic/basic/modify.go:18 |
| POST | /group.Basic/Disband |
解散群组 | JWT(丢弃 claims) | TODO 占位但返回成功 logic/basic/disband.go:14 |
| POST | /group.Member/Fetch |
群成员列表 | JWT(丢弃 claims) | logic/member/fetch.go:14(只返回总数,见 6.2) |
| POST | /group.Member/DoJoin |
申请加群 | JWT | logic/member/do_join.go:17 |
| POST | /group.Member/JoinFetch |
申请加群列表 | JWT | logic/member/join_fetch.go:15(SQL 不可用,见 6.2) |
| POST | /group.Member/JoinDoHandle |
申请加群处理 | JWT(无角色校验、丢弃 claims) | logic/member/join_do_handle.go:18 |
| POST | /group.Member/DoSetManager |
设置/取消管理员 | JWT(按创建者,列名错误) | logic/member/do_set_manager.go:18 |
| POST | /group.Member/DoKick |
踢人 | JWT(权限条件错误) | logic/member/do_kick.go:17 |
| POST | /group.Member/DoQuit |
退群 | JWT(按创建者,列名错误) | logic/member/do_quit.go:17 |
鉴权方式:13 个 logic 入口全部调用
service.ParseMetaCtx(ctx, nil)(bsm-sdk/core/service/meta.go:19-49),要求有效 JWT;opts均为nil,即不校验角色、不校验私有 IP。Search、Get、Disband、Member.Fetch、JoinDoHandle用_, err = ...丢弃 claims(search.go:13、get.go:16、disband.go:16、member/fetch.go:15、join_do_handle.go:19),后续无法做任何归属判断。
声明但未实现 / 占位接口
| 类别 | 接口 | 证据 |
|---|---|---|
TODO 占位但返回 vars.OK |
Basic.Disband |
internal/logic/basic/disband.go:26-31(// TODO: add your logic code & delete this line.) |
| 只有参数校验、无任何查询 | Basic.Search |
internal/logic/basic/search.go:17-21(校验 keyword 后直接 return &pb.GroupsReply{}, nil) |
| 有实现但结果被丢弃 | Basic.Get |
internal/logic/basic/get.go:20-25(第 25 行返回新的空 &pb.GroupItem{}) |
| 有实现但结果未装配 | Basic.Fetch |
internal/logic/basic/fetch.go:20-29(Groups 字段从未赋值) |
| 有实现但结果未装配 | Member.Fetch |
internal/logic/member/fetch.go:24-33(Members 字段从未赋值) |
| 模型存在、全模块零引用 | GroupRes(群文件/资源) |
internal/models/group_res.go:8-21(全仓库无 GroupRes 读写调用) |
| 空初始化被注册 | models.InitData |
internal/models/query.go:9-12 + cmd/main/main.go:38 |
4. 数据模型与表
模型未注册任何自动迁移:database.AppendMigrate / AutoMigrate 在 module/social/** 内无匹配(grep AppendMigrate module/social 返回空),4 张表依赖外部建表脚本,仓库内没有对应 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。
表 group_basic(internal/models/group_basic.go:8-30)
| 字段 | 类型 | 键/约束 | 说明 |
|---|---|---|---|
id |
uint | PK | 自增主键 |
identity |
varchar(36) | uniqueIndex | 群组唯一码(utils.UUID() 生成,UUIDv7 字符串) |
created_at/updated_at/deleted_at |
TIMESTAMP | index(deleted_at) | 软删除 |
status |
int8 | index,默认 0 | 状态,全模块查询均未过滤 |
passport_id / passport_identity |
uint / varchar(36) | index | 创建者;注意:modify.go:35、do_quit.go:24、do_set_manager.go:25 用的是不存在的 creator_id |
number |
int64 | not null | 群号,create.go 从未生成(恒 0) |
avatar / name / introduce / notice / background |
varchar(255)/text | 默认 '',name not null |
群资料 |
member_limit |
smallint | 默认 100 | 最大成员数,0 表示不限;仅创建时写死 500,从未参与校验 |
province/city/area |
int32 | 默认 0 | 地区,无逻辑写入 |
enable_any_join |
bool | - | 免验证入群;创建时置 true,判定逻辑恒为 false(见 6.2) |
enable_search_by_number / enable_search_by_name |
bool | - | 搜索开关,创建时未写入(恒 false) |
member_total |
int32 | 默认 0 | 成员数;创建时写 1,之后靠 UpsetGroupMemberTotal 加减 |
表 relation_group_member(internal/models/group_member.go:8-20)
| 字段 | 类型 | 键/约束 | 说明 |
|---|---|---|---|
id / identity |
uint / varchar(36) | PK / uniqueIndex | 成员记录自身标识;member/do_join.go:33、member/join_do_handle.go:39 用 utils.UUID() 写入,但 basic/create.go:42-51 建群时的创建者成员记录没有赋值(见 6.2) |
created_at/updated_at/deleted_at/status |
- | index(deleted_at) | 软删除;status 未使用 |
passport_id / passport_identity |
uint / varchar(36) | index | 成员 |
group_id |
uint | not null | 群组主键(标签写作裸名 group_id,依赖 GORM 默认蛇形命名,结果与显式声明一致) |
group_identity |
varchar(36) | not null | 群组唯一码(权限查询里被误用为 identity,见 6.2) |
remark_name |
varchar(255) | 默认 '' |
群内昵称,无任何写入点 |
role |
smallint | 默认 0 | 0 普通 / 1 管理员 / 2 超管 / 10 创建者(logic/member/const.go:3-11) |
表上没有
(group_identity, passport_id)唯一约束 → 重复入群无法由数据库兜底。
表 relation_group_apply(internal/models/group_apply.go:8-21)
| 字段 | 类型 | 键/约束 | 说明 |
|---|---|---|---|
id / identity |
uint / varchar(36) | PK / uniqueIndex | 申请记录标识(do_join.go:47) |
created_at/updated_at/deleted_at/status |
- | index(deleted_at) | status:1 通过 / -1 拒绝(join_do_handle.go:49,55),查询未按状态过滤 |
from_id / from_identity |
uint / varchar(36) | not null | 申请人 |
group_id / group_identity |
uint / varchar(36) | not null | 目标群(group_id 来自请求 in.Id,未校验与 identity 是否对应) |
message |
varchar(255) | 默认 '' |
加群留言 |
表 relation_group_res(internal/models/group_res.go:8-21)
| 字段 | 类型 | 键/约束 | 说明 |
|---|---|---|---|
id / identity |
uint / varchar(36) | PK / uniqueIndex | 主键与唯一码 |
group_id |
varchar(50) | not null | 注意这里是字符串,与 relation_group_member.group_id(uint)类型不一致 |
name / introduce / url / download_num |
- | - | 群文件信息 |
该表无任何读写逻辑,属未启用模型。
5. 核心流程
flowchart TD
A["POST /group.Member/DoJoin<br/>ParseMetaCtx 取 auth"] --> B["GetGroupEnableAnyJoin<br/>列名 createor_id 不存在 且错误被丢弃<br/>恒返回 false"]
B -->|"恒走此分支"| C["写 relation_group_apply<br/>无重复校验 无 group 存在性校验"]
D["POST /group.Member/JoinDoHandle<br/>ParseMetaCtx 丢弃 claims"] --> E{"direction"}
E -->|"PASS"| F["按 identity 查申请表"]
F --> G["Create relation_group_member<br/>无幂等 无 MemberLimit 判断"]
G --> H["UpdateColumn status=1<br/>不检查影响行数"]
H --> I["UpsetGroupMemberTotal +1<br/>不检查影响行数"]
E -->|"REJECT"| J["UpdateColumn status=-1<br/>错误未赋给 err 失败也返回成功"]
K["POST /group.Member/DoKick"] --> L["权限查询 identity=群组identity<br/>且 passport_id 传字符串<br/>必然报错"]
M["POST /group.Member/DoQuit"] --> N["Select creator_id<br/>列不存在 必然报错"]
O["POST /group.Basic/Fetch"] --> P["GetMyGroup 裸 SQL<br/>表名 group / group_member 不存在"]
Q["POST /group.Basic/Get"] --> R["First into reply 后<br/>返回新的空 GroupItem"]
6. 审计发现
6.1 安全
| 级别 | 位置 | 问题 |
|---|---|---|
| 高 | internal/logic/member/join_do_handle.go:19,24-52 |
入群审批无任何角色校验。入口 _, err = service.ParseMetaCtx(ctx, nil) 丢弃调用者身份,PASS/REJECT 分支不判断"调用者是否该群管理员/创建者"(对比 do_set_manager.go:30 的创建者校验、do_kick.go:22-30 的意图)→ 任意登录用户提交 apply_identity 就能把任意申请人拉进任意群(PASS)、或拒绝任意申请(REJECT),并可借重复 PASS 膨胀 member_total。 |
| 高 | internal/logic/member/do_kick.go:24 |
踢人权限判定双重错误:Where("identity=? and role !=0 and passport_id=?", in.GroupIdentity, auth.Identity)。① identity 是成员记录自身 ULID(member/do_join.go:33、member/join_do_handle.go:39 写入;basic/create.go:42-51 漏写),群标识列是 group_identity(group_member.go:11-12),条件语义错误;② passport_id 是整型列(bsm-sdk/core/types/db.go:52),却传入 36 位 UUID 字符串(bsm-sdk/core/utils/identity.go:8-10)→ Postgres 类型错误。当前表现为接口恒失败(见 6.2);一旦按列名"修好",该条件退化为"任意管理员可踢任意人",且代码中没有禁止踢创建者/超管的保护。 |
| 中 | internal/logic/member/do_set_manager.go:37,40 |
设置/取消管理员只用 Where("identity=?", in.Identity) 定位成员,未限定 group_identity,也不校验目标是否属于该群 → 修正列名后即成为跨群改角色入口;同时没有拦截"把创建者 ROLE_CREATEOR=10 改写为 1"。 |
| 中 | internal/logic/member/fetch.go:15-24 + internal/models/query.go:30-37 |
群成员列表只校验 JWT,不校验调用者是否为该群成员,按 group_identity 返回全量成员资料(identity/昵称/头像/性别/省市/签名)→ 任意登录用户可枚举任意群成员。 |
| 低 | internal/logic/member/do_join.go:26-50 |
入群申请无频率/数量限制,且不校验 in.Id/in.Identity 是否对应真实存在的群 → 可批量灌入 relation_group_apply 垃圾申请(当前因 JoinFetch 不可用而无法被管理员看到)。 |
6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
|---|---|---|
| 高 | internal/logic/basic/disband.go:26-31 |
解散群组是 TODO 占位却返回成功:// TODO: add your logic code & delete this line. 之后直接 return &pb.DataStatusReply{Data: vars.OK, ...} → 调用方收到成功,群与成员数据原样保留。 |
| 高 | internal/logic/basic/get.go:20-25 |
群信息恒为空:First(&reply) 已把记录读入命名返回值,第 25 行又 return &pb.GroupItem{}, nil 丢弃;且"记录不存在"(gorm.ErrRecordNotFound)被当成 ErrDB。 |
| 高 | internal/logic/basic/fetch.go:20-29 |
我的群列表恒为空:GetMyGroup 的结果 groups 只用于 Total: int32(len(groups)),Groups 字段从未赋值 → 列表空,总数也恒为 0。 |
| 高 | internal/logic/basic/modify.go:33-35 |
改群信息会重写群 identity:record.Identity = utils.UUID() 之后 UpdateColumns(record)。UpdateColumns 对结构体不做零值过滤、只跳过主键,因此 identity 会被换成新的 UUID,未传的 name/introduce/avatar/background/notice/enable_search_* 一律写空/false;relation_group_member.group_identity、relation_group_apply.group_identity 立刻与新 identity 脱钩(群成员、申请全部"消失")。 |
| 高 | internal/logic/basic/modify.go:35、internal/logic/member/do_quit.go:24、internal/logic/member/do_set_manager.go:25 |
使用不存在的列 creator_id:GroupBasic 的创建者列是 passport_id(group_basic.go:10 内嵌 Std_Passport,bsm-sdk/core/types/db.go:52-55),模型内无 creator_id → WHERE creator_id=? / SELECT creator_id 在 Postgres 直接报错 → 修改群信息、退群、设置管理员三个接口恒返回 ErrDB。 |
| 高 | internal/models/query.go:39-43 + internal/logic/member/do_join.go:26 |
"免验证入群"判定恒为 false:GetGroupEnableAnyJoin 写作 Where("createor_id=? and identity=?", passport_id, group_identity) —— createor_id 列不存在,条件语义也从"群是否开放"变成"申请人是否为群主",且第 41 行没有检查 .Scan(&is) 的错误、直接返回零值 false → DoJoin 永远走"写申请表"分支,GroupBasic.EnableAnyJoin(创建时置 true)完全失效。 |
| 高 | internal/logic/member/join_do_handle.go:24-56 |
审批无幂等、错误被吞、方向未识别即成功:同一 apply_identity 可反复 PASS,每次都 Create 新的 relation_group_member 并把 member_total 再 +1(:43-52),申请表没有任何"已处理"判断;REJECT 分支第 55 行 UpdateColumn 的返回值未赋给 err,第 58 行的 if err != nil 检查的是旧值 → 失败也返回成功;direction 既非 PASS 也非 REJECT 时直接落到函数末尾返回成功(:56)。 |
| 高 | internal/logic/member/do_join.go:26-50 |
入群无任何幂等与上限校验:不查成员表是否已存在、不查申请表中是否已有待处理申请,同一用户可生成多条成员/申请记录;MemberLimit/MemberTotal 从未参与判断(MemberLimit 只在创建时写死 500)。 |
| 高 | internal/models/query.go:16-20、:30-37 |
裸 SQL 表名与模型不一致:模型声明 group_basic(group_basic.go:29)与 relation_group_member(group_member.go:19),SQL 却写 From group_member as gm Left join group on ... 和 From group_member as gm Left join passport_extend ... → 表不存在;且 group 是 Postgres 保留字,即使存在也必须引号化。两个列表接口(Basic.Fetch、Member.Fetch)必然 SQL 报错。 |
| 高 | internal/logic/member/join_fetch.go:24 |
Where("group_id in ", group_ids) 缺少占位符(正确写法 group_id in ?)。该表达式不含 ?,GORM 只能把整个 []uint 当作单个参数追加(gorm.io/gorm@v1.31.2/clause/expression.go:53-57)→ 无法构成 IN 列表,条件无效或直接报错。 |
| 高 | internal/logic/member/join_fetch.go:21-38 |
申请列表的 dest 类型与结果集不匹配:Find(&fetch) 的目标是 []*pb.ApplyJoinGroupItem(字段 identity/group_id/from/message/created_at/status,proto/member.proto:74-81),而 relation_group_apply 只有 from_id/from_identity,GORM 会拼出 SELECT ... "from" ... → 列不存在报错。即便查询成功,第 31 行还用 item.From.Identity(恒为空串,因为 from 列从未映射)去查 passport_extend,ErrRecordNotFound 被当成 ErrDB(:32-35)→ 只要有一条申请,整个列表就失败。 |
| 中 | internal/logic/member/join_fetch.go:29-36 |
card 在 for 循环外只分配一次,fetch[idx].From = card 让所有元素共用同一指针 → 即使查询成功,全部申请都会显示"最后一个被查到的人"的资料卡。 |
| 中 | internal/models/query.go:45-47 + do_quit.go:37-41、do_kick.go:36-39 |
群成员计数没有一致性保障:UpsetGroupMemberTotal 不返回错误、不检查影响行数;删除成员(软删除)时 RowsAffected=0 也照样 -1(非成员调用 DoQuit 会让计数长期偏小甚至为负),而 PASS 分支重复审批会重复 +1。 |
| 中 | internal/logic/member/fetch.go:31-33 |
群成员列表只填 Total: int32(len(members)),Members 字段从未赋值 → 成员列表恒为空。 |
| 高 | internal/logic/basic/create.go:42-51 |
创建者成员记录漏写 identity。member 只赋了 GroupID/GroupIdentity/Role 与 PassportID/PassportIdentity,没有 member.Identity = utils.UUID()(对比 member/do_join.go:33、member/join_do_handle.go:39)。而 GroupMember.Identity 来自 types.Std_IICUDS、标签是 uniqueIndex(bsm-sdk/core/types/db.go:28-35)→ 若表结构按模型建立,第一条成员记录以 identity='' 落库后,任何人再建群都会因唯一键冲突失败(Create 返回 ErrDB);若线上表没有该索引,则这些成员记录的 identity 全是空串,DoKick 里按 identity 定位成员的逻辑永远无法正确命中。 |
| 中 | internal/logic/basic/create.go:36-55 |
建群无事务:群记录写入成功而成员记录失败时,群已存在且 MemberTotal=1 却没有任何成员;Number(群号)与 EnableSearchByNumber/Name 从未从请求写入(恒 0/false),in.MemberLimit 被忽略(第 28 行写死 DefaultMemberLimit)。 |
| 中 | 全模块 | 没有任何分页与上限:Basic.Fetch 拉全部群、Member.Fetch 拉全部成员、JoinFetch 拉全部申请,proto/const.proto:10-14 的 FetchRequest.page_no/page_size 从未被任何 logic 使用 → 大群/多申请一次性全量返回。 |
| 低 | internal/logic/basic/fetch.go:28 |
Total: int32(len(groups)) 与真实总数脱钩(len 恒 0),且未做 int→int32 溢出保护。 |
| 低 | internal/logic/basic/create.go:59 |
时间戳用 UnixNano,其余接口统一 UnixMilli(如 modify.go:43)→ 同一字段单位不一致。 |
| 低 | internal/logic/member/do_join.go:23,58 |
返回的 Data 是第 23 行新生成的 UUID,既没落库也不代表任何记录,调用方拿到的"标识"无效。 |
6.3 未完成实现
| 类别 | 内容 | 证据 |
|---|---|---|
| TODO 占位(且返回成功) | Basic.Disband |
internal/logic/basic/disband.go:26-31 |
| 空实现(无 TODO 标注) | Basic.Search |
internal/logic/basic/search.go:17-21 |
| 空实现 | models.InitData(只有 return nil),却被 srv.Use 注册 |
internal/models/query.go:9-12 + cmd/main/main.go:38 |
| 模型存在、零逻辑 | GroupRes 群文件表 |
internal/models/group_res.go:8-21 |
| gateway 未接线 | Server.Mux 声明后从未赋值(internal/server/new.go:20-25 只建 gRPC),pb.RegisterBasicHandlerServer / pb.RegisterMemberHandlerServer 在仓库内无调用点 |
internal/server/new.go:20-33、cmd/main/main.go:33;grep RegisterBasicHandlerServer module/social/group 仅命中生成文件 |
| 未接入聚合入口 | 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 |
| 无建表手段 | database.AppendMigrate/AutoMigrate 在 module/social/** 无匹配 → 4 张表依赖外部 SQL,仓库内无脚本 |
grep AppendMigrate module/social 无结果 |
| CLI 空实现 | cmd/cli/main.go 全文 4 行,main() 为空 |
cmd/cli/main.go:1-4 |
| README 空壳 | 全文只有标题 | README.md:1-2 |
6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
|---|---|---|
| 中 | internal/server/new.go:20-25 + cmd/main/main.go:33 |
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/group_dev.yaml:4-7 有 Databases 段,可正常初始化。 |
| 中 | etc/group_dev.yaml、etc/group_prod.yaml、etc/group_test.yaml |
三份配置逐行相同(Service: group、Port: 12247、同一个 127.0.0.1 + dbname=bsm_dev 连接串、相同 Gateway.Port: 12425)→ dev/prod/test 无法区分;数据库口令以明文形式入库(password=CHANGE_ME)。 |
| 中 | etc/group_dev.yaml:13-16 + internal/impl/impl.go:24 |
MicroService.Anonymous 写的是 passport.ping.hello,而本服务只有 Basic/Member 两个 service、不存在 ping 方法(internal/server/new.go:28-29)→ 匿名清单无效;配置中没有 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)。 |
| 低 | internal/impl/impl.go:21-22 |
MemorySerice、RedisService 初始化后全模块零使用(死代码);EtcdService 只出现在被跳过的注册路径。 |
| 低 | internal/logic/basic/const.go:13-15、internal/logic/member/const.go:3-15 |
两个包重复定义同一组 ROLE_*、DefaultMemberLimit、DefaultEnableAnyJoin;basic 包的 filed 变量没有任何使用点(member 包的同名变量用于 join_fetch.go:31)。 |
| 低 | internal/logic/basic/get.go:20 |
把 **pb.GroupItem(&reply)交给 GORM 当 dest,依赖 SDK 对双重指针的隐式分配(gorm.io/gorm@v1.31.2/callbacks.go:121-133),语义正确但极不直观,也正是第 25 行改写返回值后不易察觉的原因。 |
| 低 | 全模块 | 0 个 *_test.go;test/ 目录不存在,无任何请求样例;README 只有标题。 |
7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
|---|---|---|---|
| G1 | 高 | 入群审批无角色校验,任意登录用户可拉人进群/拒绝申请 | 越权、群成员被污染 |
| G2 | 高 | Disband 为 TODO 占位却返回成功 |
功能不可用、误导调用方 |
| G3 | 高 | Basic.Get/Basic.Fetch 丢弃查询结果 |
功能不可用 |
| G4 | 高 | Modify 重写群 identity + UpdateColumns 零值覆盖 |
关联数据断裂、群资料被清空 |
| G5 | 高 | creator_id 列不存在 → Modify/DoQuit/DoSetManager 恒失败 |
功能不可用 |
| G6 | 高 | GetGroupEnableAnyJoin 列名错+语义错+吞错误 |
免验证入群永久失效 |
| G7 | 高 | JoinDoHandle 无幂等、错误被吞、未知 direction 返回成功 |
重复成员、计数膨胀、假成功 |
| G8 | 高 | 入群无幂等与成员上限校验 | 数据污染、绕过群容量 |
| G9 | 高 | 裸 SQL 表名(group/group_member)与模型不一致 |
两个列表接口必然报错 |
| G10 | 高 | join_fetch IN 缺占位符 + dest 结构不匹配 + From 指针复用 |
申请列表不可用 |
| G11 | 高 | DoKick 权限条件列名/类型双错(修正后无角色保护) |
功能不可用、权限模型缺失 |
| G12 | 中 | 成员计数无一致性保障(不检查影响行数) | 群成员数长期失真 |
| G13 | 中 | 无任何分页与上限 | 资源滥用、超大响应 |
| G14 | 中 | 建群无事务,群号/搜索开关/成员上限未落库 | 半成品数据 |
| G15 | 中 | gateway 未接线、Mux 为 nil、无 service.Expose |
HTTP 不可用、无法被聚合 |
| G16 | 中 | Databases 未校验、三份配置同内容、缺 Etcd 段 |
部署脆弱、环境不分 |
| G17 | 低 | 死代码(GroupRes、InitData、filed、Redis/Memory)、重复常量、无测试、README 空壳 |
可维护性 |
8. 修复建议(务实项)
- 补权限判定(G1/G11):
JoinDoHandle、DoKick、DoSetManager先取回 claims(不要写成_),再按group_identity + passport_id查一条relation_group_member拿到role,据此判断"是否管理员/创建者",并禁止对role >= 操作者 role的成员执行踢人/改角色;DoQuit保留"创建者不能退群"。 - 修列名与 SQL(G3/G5/G6/G9/G10):
creator_id/createor_id统一改为passport_id;GetMyGroup/GetGroupMember的group/group_member改为group_basic/relation_group_member;GetGroupEnableAnyJoin改为Where("identity=?", group_identity).Select("enable_any_join")并检查错误;join_fetch.go:24改为Where("group_id in ?", group_ids)。 - 修返回值装配(G3):
Get直接return reply, nil(ErrRecordNotFound单独返回"群不存在"错误码);Basic.Fetch/Member.Fetch把查到的切片填进Groups/Members并同步Total。 - 修
Modify(G4):删除record.Identity = utils.UUID();改用Updates(map[string]any{...})或Select("name","introduce",...)只更新请求中实际提供的字段;Where改为identity=? and passport_id=?。 - 补幂等与上限(G7/G8/G12):PASS 前查申请
status与成员表是否已存在,已处理直接返回;入群前用MemberTotal < MemberLimit(0 视为不限)拦截;成员增删后按Delete/Create的RowsAffected决定是否±1,并把UpsetGroupMemberTotal改为返回error由调用方检查。 - 修申请列表(G10):改为先
Find(&[]models.GroupApply{})再组装 pb;card在循环内new;申请人资料查不到时跳过该条而不是整单失败;如只展示待处理,补status = 0条件。 - 补事务与字段落库(G14):建群的两条
Create放进impl.DBService.Transaction;把Number(群号生成规则)、EnableSearchByNumber/Name、MemberLimit从请求写入。 - 补分页(G13):为
Member.Fetch/JoinFetch接上FetchRequest的page_no/page_size(并设默认值与上限),Basic.Fetch按产品需要决定是否分页。 - 接线与配置(G15/G16):
internal/server/new.go中Mux: gwRuntime.NewServeMux()并调用pb.RegisterBasicHandlerServer/pb.RegisterMemberHandlerServer;或在service/下补与 wallet 同构的Expose以便接入pkgs/all;config.New补conf.NotNil(Spec.Databases);三份 yaml 区分环境与口令来源,Anonymous改为真实存在的方法或删除,若要启用微服务注册需补Etcd段。 - 清理与测试(G17):删除
basic/const.go未使用的filed、impl.go中未使用的 Redis/Memory 初始化、models/query.go:9-12的空InitData(或从cmd/main/main.go:38移除注册);为DoJoin/JoinDoHandle/DoQuit/Modify各补一条正例 + 一条边界用例(当前 0 测试)。
本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。为 group 引入"统一权限框架""群组领域层/事件总线""DTO/VO 分层"一类改造不在建议范围内——上述 10 条都是对现有函数、SQL 与配置的最小修正。
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,属本机既有问题)。
| 编号 | 级别 | 问题 | 处理结果 |
|---|---|---|---|
| G1 | 高 | 入群审批无角色校验 | 已修复:审批前校验调用者为该群群主/管理员,否则 ErrPermissionDenied |
| G2 | 高 | Disband 为 TODO 占位却返回成功 |
已修复:实现真实解散——校验群组存在且调用者为群主(group.PassportID != auth.ID 即 ErrPermissionDenied),并在事务内软删除群组与其成员记录,不再假成功 |
| G3 | 高 | Basic.Get/Basic.Fetch 丢弃查询结果 |
已修复:修正返回值装配,接口真实返回群数据 |
| G4 | 高 | Modify 重写群 identity + UpdateColumns 零值覆盖 |
已修复:不再改写 identity;零值字段不再覆盖已有值 |
| G5 | 高 | creator_id 列不存在导致 Modify/DoQuit/DoSetManager 恒失败 |
已修复:按模型真实列名修正(含 createor_id 拼写错误) |
| G6 | 高 | GetGroupEnableAnyJoin 列名错+语义错+吞错误 |
已修复:修正列名与免验证入群语义,并检查 .Error |
| G7 | 高 | JoinDoHandle 无幂等、错误被吞、未知 direction 返回成功 |
已修复:加幂等判断、检查全部 .Error、未知 direction 返回 ErrInvalidArgument |
| G8 | 高 | 入群无幂等与成员上限校验 | 已修复:加重复入群防护与成员上限校验 |
| G9 | 高 | 裸 SQL 表名(group/group_member)与模型不一致 |
已修复:表名与模型 TableName() 对齐,两个列表接口可用 |
| G10 | 高 | join_fetch IN 缺占位符 + dest 结构不匹配 + From 指针复用 |
已修复:修正占位符数量、目标结构映射与指针复用 |
| G11 | 高 | DoKick 权限条件列名/类型双错 |
已修复:修正列名与类型,并补「仅群主/管理员可踢人」的角色校验 |
未纳入本轮范围
报告中「中」「低」级别的项(分页上限、死代码、README 与实现不符、单测缺失、可维护性等)本轮未处理;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
本轮整改未修改任何
proto/*.proto与pb/*.go,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。