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

245 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 个 RPCgateway 路径为 `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/G12PASS 前查申请 `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 与配置的最小修正。