245 lines
31 KiB
Markdown
245 lines
31 KiB
Markdown
|
|
# 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. 核心流程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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. 修复建议(务实项)
|
|||
|
|
|
|||
|
|
1. **补权限判定**(G1/G11):`JoinDoHandle`、`DoKick`、`DoSetManager` 先取回 claims(不要写成 `_`),再按 `group_identity + passport_id` 查一条 `relation_group_member` 拿到 `role`,据此判断"是否管理员/创建者",并禁止对 `role >= 操作者 role` 的成员执行踢人/改角色;`DoQuit` 保留"创建者不能退群"。
|
|||
|
|
2. **修列名与 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)`。
|
|||
|
|
3. **修返回值装配**(G3):`Get` 直接 `return reply, nil`(`ErrRecordNotFound` 单独返回"群不存在"错误码);`Basic.Fetch`/`Member.Fetch` 把查到的切片填进 `Groups`/`Members` 并同步 `Total`。
|
|||
|
|
4. **修 `Modify`**(G4):删除 `record.Identity = utils.UUID()`;改用 `Updates(map[string]any{...})` 或 `Select("name","introduce",...)` 只更新请求中实际提供的字段;`Where` 改为 `identity=? and passport_id=?`。
|
|||
|
|
5. **补幂等与上限**(G7/G8/G12):PASS 前查申请 `status` 与成员表是否已存在,已处理直接返回;入群前用 `MemberTotal < MemberLimit`(0 视为不限)拦截;成员增删后按 `Delete/Create` 的 `RowsAffected` 决定是否 `±1`,并把 `UpsetGroupMemberTotal` 改为返回 `error` 由调用方检查。
|
|||
|
|
6. **修申请列表**(G10):改为先 `Find(&[]models.GroupApply{})` 再组装 pb;`card` 在循环内 `new`;申请人资料查不到时跳过该条而不是整单失败;如只展示待处理,补 `status = 0` 条件。
|
|||
|
|
7. **补事务与字段落库**(G14):建群的两条 `Create` 放进 `impl.DBService.Transaction`;把 `Number`(群号生成规则)、`EnableSearchByNumber/Name`、`MemberLimit` 从请求写入。
|
|||
|
|
8. **补分页**(G13):为 `Member.Fetch`/`JoinFetch` 接上 `FetchRequest` 的 `page_no/page_size`(并设默认值与上限),`Basic.Fetch` 按产品需要决定是否分页。
|
|||
|
|
9. **接线与配置**(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` 段。
|
|||
|
|
10. **清理与测试**(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 与配置的最小修正。
|