Files
full/docs/audit/module-social-group.md
yanweidong 63aeedc2fe docs: add per-module audit reports (18 modules)
Add static security/quality audit reports for all 18 Go service modules under module/, plus a consolidated index (docs/audit/README.md) with per-module statistics, top risks, cross-module systemic defects and a phased TODO list (T1-T19). No production code is modified.
2026-09-14 22:16:27 +08:00

704 lines
72 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.
# 审计报告module/social/group
## 1. 模块概览
- **路径**`D:\work\bsm-infra\full\module\social\group`Go module `bsm/full/module/social/group``go.work` 成员,见 `go.work``./module/social/group`)。
- **形态**gRPC + gRPC-gateway 微服务,独立进程入口 `cmd/main/main.go`proto 定义 `Basic`6 个 RPC`Member`7 个 RPC两个 service**13 个 RPC**`proto/group.proto:7-25``proto/member.proto:7-30`)。
- **规模**27 个非 pb Go 文件 / **805 行**;另有 3 个 proto`group/member/const`+ 7 个生成文件(`const.pb.go``group.pb.go``group.pb.gw.go``group_grpc.pb.go``member.pb.go``member.pb.gw.go``member_grpc.pb.go`+ 4 个 etc 配置dev/test/prod/supervisor
- **数据模型**`GroupBasic``group_basic``internal/models/group_basic.go:28-30`)、`GroupMember``relation_group_member``group_member.go:18-20`)、`GroupApply``relation_group_apply``group_apply.go:19-21`)、`GroupRes``relation_group_res``group_res.go:19-21`**全模块零引用**)。三者均内嵌 `types.Std_IICUDS`(软删除 `DeletedAt`)与 `types.Std_Passport``passport_id`/`passport_identity`**没有任何 `creator_id` 字段**`D:\work\bsm-sdk\core\types\db.go:28,52`)。
- **路由/鉴权模型**:每个 handler 都调用 `service.ParseMetaCtx(ctx, nil)` 校验 `authorization` 元数据中的 JWT`D:\work\bsm-sdk\core\service\meta.go:19-49``opts==nil` 因此不做角色限制;鉴权只区分"是否登录"**群内角色(群主/管理员/成员)由各 logic 自行判断**,这正是本模块问题最集中之处。
- **接入状态**`MicroService.Enable: false`(不注册 etcd 路由),`Gateway.Enable: true``service/` 目录**存在但为空**,未按 `README.md:313` 约定提供 `service.Expose`;顶层 `README.md:45,231` 明确"社交服务不接入 `all`/`ecmall`"。
- **一句话结论**13 个 RPC 中 **12 个存在确定性缺陷**SQL 表名/列名错误、结果被丢弃、权限查询恒假、模板 TODO 空实现),唯一"能落库"的 `Create` 仍缺群号、事务与校验;同时"成员上限""免验证入群""搜索开关"等开关全部未生效。
## 2. 审计范围与方法
**已读完100%**
- 全部 27 个非 pb Go 文件、805 行:`cmd/main``cmd/cli``internal/{config,impl,server,models,logic/basic,logic/member}`
- `proto/group.proto``proto/member.proto``proto/const.proto``pb/blocks_compat.go`;生成代码按需阅读(`group.pb.gw.go``*_grpc.pb.go``group.pb.go`)。
- `etc/group_{dev,test,prod}.yaml``etc/supervisor.bsm-social-group.conf``go.mod``README.md`(模块)、顶层 `README.md``wiki/api/14-group.md``docs/audit/README.md`
**外部交叉验证(用于判定"是模块缺陷还是平台约定"**
- SDK`D:\work\bsm-sdk\core\{service,conf,with,database,errcode,types,crypto/token,utils,env}`
- 平台网关:`D:\work\bsm-infra\gateway\internal\rpc\rpc.go:130-207`(路由与 JWT 转发)、`internal/http/http.go:154-176`
- SQL 生成语义GORM `v1.31.2` 源码(`clause/expression.go``clause/set.go``statement.go:175-269``scan.go:126-357``finisher_api.go``schema/schema.go``callbacks/update.go``grpc-gateway/v2@v2.30.0/runtime/mux.go`
- 同构模块对照:`module/finance/wallet/service/expose.go`(有网关注册)、`module/base/passport|social/relation|social/feed/internal/server/new.go`(同为单模板产物)。
**静态检查(在 `module/social/group` 下执行)**
| 命令 | 结果 |
| --- | --- |
| `gofmt -l .` | 无输出exit=0格式干净 |
| `go vet ./...` | 无输出exit=0 |
| `go test ./...` | 9 个包全部 `[no test files]`exit=0`_test.go` 数量 = **0** |
**方法**:以"证据优先"逐方法核对 13 个 RPC 的鉴权、SQL、事务、返回值与错误处理对无法运行验证的结论SQL 渲染、驱动报错、DDL 索引)在正文中显式标注「推测」。**本次审计未修改/新建任何代码文件**,唯一新增文件即本报告。
**返回值证据口径(避免泛化)**:本模块 27 个非 pb 文件中**只有 2 处裸 `return`**——`internal/models/query.go:21``:36`,且两者上一行都显式赋值了命名返回值(`err = impl.DBService.Raw(...).Find(&groups).Error`**不存在"命名返回值未赋值 → 恒返回 nil 且 err 为 nil"的路径**。报告中所有"某字段恒为空/结果被丢弃"的判断都来自**显式结构体字面量返回**并已附原文摘录,例如 `return &pb.GroupsReply{Total: int32(len(groups))}, nil``internal/logic/basic/fetch.go:27-29`,未设置的 `Groups` 为零值 nil`return &pb.GroupItem{}, nil``internal/logic/basic/get.go:25`)、`return &pb.GroupMemberReply{Total: int32(len(members))}, nil``internal/logic/member/fetch.go:31-33`)。
**无法覆盖/无法验证**
1. **真实表结构**:仓库内**没有**任何 DDL/SQL 迁移文件,`IsAutoMigrate` 默认 `false``D:\work\bsm-sdk\core\database\sql\postgresql.go:17,46`)且本模块未调用 `AutoMigrate`(对照 `module/ec/order/internal/models/impl.go:41`),因此 `group_basic` 是否真有 `creator_id``number` 是否有唯一索引、`relation_group_member` 是否有唯一约束**均无法从仓库确认**——相关结论已写成"分支 A/分支 B"。
2. **运行时行为**:未连接真实 PostgreSQL 执行 SQL也未启动服务所有"SQL 报错/panic"结论是基于所用库源码的**推演**(已标注)。
3. **生产配置注入方式**`conf.New` 只做 `os.ExpandEnv`(不支持 `$VAR` 之外的覆盖,`etc/*.yaml` 中也没有变量),但发布系统是否外部覆盖 `etc/` 无法验证。
4. **前端渲染方式**(决定群名/公告是否构成存储型 XSS不在本仓库。
## 3. 问题清单
### P0
**无。**
判定说明(逐条排除,避免误报):本次未发现"当前可被利用的 P0 级漏洞"。最接近的两个候选都被其他缺陷挡住,因而降级为 P1
- `Basic.Modify` 的破坏性写入(改写群 `identity`、清零 `passport_id`/`member_limit`/`member_total`)本身足以构成数据损坏,但其 `WHERE` 引用了模型中不存在的 `creator_id``internal/logic/basic/modify.go:35`),使该 `UPDATE` 在当前代码下要么直接报 `ErrDB`、要么 0 行受影响后静默返回 OK —— 破坏性 SET 属于**潜在**而非必然触发(见 P1-1
- 越权面最大的 `Member.JoinDoHandle`(完全无权限校验)需要攻击者拿到入群申请的 `identity`,而该值从不出现在任何响应中(`DoJoin` 返回的是另一个随机 UUID见 P1-12当前不可达故列为 P1见 P1-8
### P1
#### 1. `Basic.Modify` 用 `UpdateColumns` 提交整个结构体:会连带写入零值字段并把群 `identity` 改成随机 UUID且所有权条件引用不存在的列
- **位置**`module/social/group/internal/logic/basic/modify.go:24-35``internal/models/group_basic.go:9-11``D:\work\bsm-sdk\core\types\db.go:52`
- **证据**
```go
24 record := &models.GroupBasic{
25 Name: in.GetName(),
30 EnableSearchByNumber: in.GetEnableSearchByNumber(),
32 }
33 record.Identity = utils.UUID() // ← 重新生成群 identity
35 err = impl.DBService.Where("identity=? and creator_id=?", in.Identity, auth.ID).UpdateColumns(record).Error
```
`GroupBasic` 只有 `types.Std_Passport``passport_id`/`passport_identity`),全仓库(含 proto、其它模块模型检索不到 `creator_id` 列的定义(`grep -r creator_id module/` 仅命中本模块 3 处逻辑代码 + 1 处 proto
- **影响**
1. `UpdateColumns(struct)` 的语义是"更新**全部**字段(含零值)"——依据本模块实际使用的 GORM `v1.31.2``finisher_api.go``UpdateColumns` 仅设置 `SkipHooks=true`,而 `callbacks/update.go``ConvertToAssignments``!restricted`(未调用 `Select`)时对每个非主键字段都执行赋值(`callbacks/update.go:278-279`),因此 `Number/MemberLimit/MemberTotal/PassportID/PassportIdentity/Status/Province/City/Area/UpdatedAt` 都会以 record 的零值写库;`Identity` 更是被替换成新的 UUIDv7。成员`relation_group_member.group_identity`)、申请(`relation_group_apply.group_identity`)都以 `group_identity` 关联,**identity 一旦变化,该群的全部成员与申请关系立即断裂**,且不可恢复。
2. 所有权条件 `creator_id` 在模型/迁移中不存在:分支 A库里无此列→ PostgreSQL `column "creator_id" does not exist`,接口恒定返回 `errcode.ErrDB`;分支 B库里有此列但由本模块 `Create` 从不写入,`create.go:33` 写的是 `PassportID`)→ 条件恒不成立,`RowsAffected=0``.Error==nil`**接口返回 `Data: vars.OK` 的假成功,什么也没改**。
- **建议**:改为 `Model(&models.GroupBasic{}).Where("identity=? AND passport_id=?", in.Identity, auth.ID).Updates(map[string]any{...白名单字段...})`;删除 `record.Identity = utils.UUID()`;用 `RowsAffected==0` 显式返回 `ErrRecordNotFound`/`ErrPermissionDenied`;统一所有权字段为 `passport_id`(或补 `CreatorID` 字段并让 `Create` 写入)。
#### 2. `Basic.Fetch`(群列表)原始 SQL 表名/关键字/列歧义三重错误,且查询结果被丢弃
- **位置**`module/social/group/internal/models/query.go:14-22``internal/logic/basic/fetch.go:20-29`
- **证据**
```go
16 sql := `Select group.*,gm.nickname,gm.remark_name,gm.role
17 From group_member as gm
18 Left join group on group.id=gm.group_id
19 Where passport_id=? and deleted_at is null`
20 err = impl.DBService.Raw(sql, passport_id).Find(&groups).Error
```
```go
27 return &pb.GroupsReply{
28 Total: int32(len(groups)), // groups 未被赋值给 Groups
29 }, nil
```
- **影响**
1. `group_member` 不是模型声明的表名(模型是 `relation_group_member``group_member.go:19``group` 也不是(模型是 `group_basic`),且 `group` 是 PostgreSQL 保留字,`FROM group`/`Select group.*` 无法解析;`passport_id`/`deleted_at` 在 join 后同时存在于成员表与群表,属于**歧义列**42702。推测该接口对所有用户恒返回 `ErrDB(1501)`(表不存在/语法错误),未连库无法给出确切 SQLSTATE。
2. 即便 SQL 修好,返回值也丢掉了数据:`Total``len(groups)` 表示,`Groups` 字段始终为 nil —— 客户端"获取群组列表"永远拿到空列表(`groups` 变量在函数内被完全浪费)。
- **建议**:用 GORM 模型 + `Joins` 写查询(让表名/软删条件由 schema 决定),或至少把表名修正为 `relation_group_member`/`group_basic` 并给列加表别名前缀;返回时补 `Groups: groups``Total` 改为独立 `Count`
#### 3. `Basic.Get`(群详情)把 pb 结构体当查询模型:表名推导为 `group_item`,且最终丢弃结果返回空对象
- **位置**`module/social/group/internal/logic/basic/get.go:20-25`
- **证据**
```go
20 err = impl.DBService.Where("identity=?", in.Identity).First(&reply) // reply 是 *pb.GroupItem无 TableName
21 if err != nil {
23 return nil, errcode.ErrDB
24 }
25 return &pb.GroupItem{}, nil // 查询结果被丢弃
```
- **影响**`First(&reply)``pb.GroupItem` 作为 Dest/ModelSDK 的 NamingStrategy 是 `SingularTable: true``D:\work\bsm-sdk\core\database\sql\postgresql.go:39-41`),因此表名被推导为 `group_item` 而真实表是 `group_basic` → 推测该接口对任意入参恒返回 `ErrDB`;若表名恰好存在,代码也会丢弃已扫描到的 `reply` 并返回全零 `GroupItem`id=0、identity=""、name=""**任何调用者都拿不到群资料**。另外 `in.Id` 被完全忽略。
- **建议**`var g models.GroupBasic; ...First(&g)` 后显式映射到 pb`Id`/`Identity` 二者至少校验其一或都支持。
#### 4. `Member.Fetch`(成员列表)表名错误 + 结果被丢弃,且无成员校验、无分页
- **位置**`module/social/group/internal/models/query.go:30-37``internal/logic/member/fetch.go:20-33`
- **证据**
```go
31 sql := `Select pe.identity,pe.nickname,pe.avatar,pe.sex,gm.remark_name,gm.role
32 From group_member as gm
33 Left join passport_extend as pe on pe.passport_id=gm.passport_id
34 Where group_identity=? and deleted_at is null`
```
```go
31 return &pb.GroupMemberReply{
32 Total: int32(len(members)), // Members 未赋值
33 }, nil
```
- **影响**:表名 `group_member` 不存在 → 推测恒 `ErrDB`;即便修好,`Members` 仍恒为空。同时该接口只校验"已登录"**不校验调用者是否属于该群**,一旦 SQL 修正即成为"知道群 identity 就能拉全群成员档案(昵称/头像/性别/群内备注)"的横向越权(群 identity 为 UUIDv7不可枚举故计为 P1 而非 P0且查询无 `LIMIT`/分页,大群会把全部成员一次加载进内存。
- **建议**:先查调用者的成员行(`group_identity=? AND passport_id=?`,未命中返回 `ErrPermissionDenied`加分页参数proto 目前只有 `blocks.IdentRequest`,需扩参),修正表名并补 `deleted_at` 表别名前缀,返回 `Members`
#### 5. `Member.JoinFetch`入群申请列表SQL 缺少占位符、循环内 N+1 查询、复用同一指针导致所有申请卡片被最后一条覆盖
- **位置**`module/social/group/internal/logic/member/join_fetch.go:22-37``internal/models/query.go:24-28`
- **证据**
```go
24 err = impl.DBService.Model(&models.GroupApply{}).Where("group_id in ", group_ids).Find(&fetch).Error
...
29 var card = &pb.PassportInfoDetailCard{} // ← 循环外,指针被复用
30 for idx, item := range fetch {
31 err = impl.DBService.Table("passport_extend").Select(filed).Where("passport_identity = ?", item.From.Identity).First(card).Error
...
36 fetch[idx].From = card
37 }
```
- **影响**
1. `Where("group_id in ", group_ids)` 的 SQL 串**不含 `?`**GORM 会把 `group_ids` 作为"多余变量"追加(`clause/expression.go``Expr.Build` 尾部 → `statement.go:182-183``sql.NamedArg` 只入参不写占位符)→ 渲染成 `... WHERE group_id in AND "relation_group_apply"."deleted_at" IS NULL`。推测:只要用户管理着 ≥1 个群,该接口必报 `ErrDB`(语法错误/参数个数不符)。正确写法是 `Where("group_id IN ?", group_ids)`
2. 每条申请一次 `passport_extend` 查询 = N+1。
3. `var card` 定义在循环外,每次 `First(card)` 都写同一块内存,`fetch[idx].From = card` 让**所有申请项的 `from` 指向同一个对象**,其内容恒为最后一条申请人的资料 → 申请列表里除最后一项外的申请人资料全部错乱(展示的是他人资料),既是正确性问题也是隐私问题。
- **建议**`Where("group_id IN ?", ids).Find(&fetch)`;申请人资料用一次 `passport_identity IN ?` 批量取回并建 map每项分配独立对象。
#### 6. `Member.DoKick`(踢人)权限查询列名与参数类型双重错误 → 恒定拒绝;目标标识语义与对外数据不一致
- **位置**`module/social/group/internal/logic/member/do_kick.go:24-39``internal/models/query.go:31`(对外成员身份的来源)
- **证据**
```go
24 if err = impl.DBService.Model(&models.GroupMember{}).Where("identity=? and role !=0 and passport_id=?", in.GroupIdentity, auth.Identity).Count(&tc).Error; err != nil {
...
28 if tc == 0 {
29 return nil, errcode.ErrPermissionDenied
30 }
...
32 err = impl.DBService.Model(&models.GroupMember{}).Delete("group_identity=? and identity=?", in.GroupIdentity, in.Identity).Error
```
`auth.Identity`**string**`D:\work\bsm-sdk\core\token``Claims{ID uint; Identity string}`),而 `passport_id` 是整型列;`GroupMember``identity` 列是**成员行自身的 UUID**,群标识在 `group_identity` 列。
- **影响**:该权限查询把群 identity 比到了成员行的 `identity` 列上,同时把字符串 `auth.Identity` 比到 `passport_id`PostgreSQL 无 `bigint = text` 运算符)→ 推测恒定报错并返回 `ErrDB`**群主与管理员都无法踢人**fail-closed因此暂不构成越权。此外 `Delete``identity=in.Identity`(成员行 UUID作为目标而对外唯一暴露成员身份的地方返回的是 **passport identity**`query.go:31` `pe.identity AS identity`)——即便权限查询修好,调用方也拿不到可用于踢人的 `identity`API 契约断裂。删除影响 0 行时仍继续扣减群成员数(见 P1-11
- **建议**:权限校验改为 `Where("group_identity=? AND passport_id=? AND role<>0", in.GroupIdentity, auth.ID)`;踢人目标改为 `passport_identity`(或 proto 明确该字段语义并统一);`Delete(...).RowsAffected==0` 时返回 `ErrRecordNotFound` 且不更新计数。
#### 7. `Member.DoSetManager`:目标成员未按群限定(跨群越权),`UpdateColumn` 错误被吞,非法 direction 静默成功
- **位置**`module/social/group/internal/logic/member/do_set_manager.go:24-45`
- **证据**
```go
30 if auth.ID != creator_id {
31 return nil, errcode.ErrPermissionDenied
32 }
34 switch strings.ToUpper(in.Direction) {
35 case "SET":
37 impl.DBService.Model(&models.GroupMember{}).Where("identity=?", in.Identity).UpdateColumn("role", ROLE_MASTER)
38 case "CANCEL":
40 impl.DBService.Model(&models.GroupMember{}).Where("identity=?", in.Identity).UpdateColumn("role", ROLE_MEMBER)
41 }
42 return &pb.DataStatusReply{Data: vars.OK, ...
```
- **影响**
1. `Where("identity=?", in.Identity)` **完全没有 `group_identity` 限定**:只要 `creator_id` 分支可用,群创建者就能把**任意群**的成员行改成管理员(经典 IDOR。实际可达性受两点限制成员行 identity 无对外出口、`creator_id` 列缺失),故计 P1 而非 P0。
2. 两次 `UpdateColumn``.Error` 从未检查,改角色失败仍返回 OK配合上一行的 `creator_id` 缺失问题,可能出现"校验报错/静默无效"两种极端。
3. `Direction` 非 SET/CANCEL 时直接返回 OK`switch` 无 default非法状态流转被静默接受。
- **建议**`Where("group_identity=? AND identity=?", in.GroupIdentity, in.Identity)`;检查 `.Error``RowsAffected``default:` 返回 `ErrInvalidArgument`;把 direction 从字符串改成 proto enum见 P3-26
#### 8. `Member.JoinDoHandle`(审核入群申请)完全缺少群权限校验,且非幂等、非申请状态机
- **位置**`module/social/group/internal/logic/member/join_do_handle.go:18-56`
- **证据**
```go
19 _, err = service.ParseMetaCtx(ctx, nil) // 只校验"已登录"auth 被丢弃
...
27 if err = impl.DBService.Where("identity=?", in.Identity).First(&apply).Error; err != nil {
...
43 if err = impl.DBService.Create(&member).Error; err != nil {
...
49 impl.DBService.Model(&models.GroupApply{}).Where("identity=?", in.Identity).UpdateColumn("status", 1)
52 models.UpsetGroupMemberTotal(apply.GroupIdentity, "+")
```
- **影响**
1. **没有 `in.GroupIdentity` 相关的任何角色校验**(对比 `DoSetManager`/`DoQuit` 至少查了 `creator_id`):任何已登录用户只要掌握某条申请的 `identity`,就能替任意群通过/拒绝该申请。当前可达性受"申请 identity 从不出现在任何响应中"限制(`DoJoin` 返回的是另一个随机 UUID见 P1-12因此判定为 P1 越权缺口而非 P0但一旦有任何接口含未来的管理端/日志/通知)泄露申请 identity即直接升级为 P0普通用户自批入群
2. **非幂等**:对同一条申请重复 PASS 会重复 `Create` 成员行并重复 `member_total+1`;申请的 `status` 是否已处理完全不检查(模型也没有"pending/approved"状态机约束)。
3. `apply.GroupID`/`apply.GroupIdentity` 直接取自申请行,而申请行是客户端可控的(`DoJoin` 用请求里的 `id``identity`,见 P1-12审核动作因此可能给"另一个群"建成员、给"另一个群"加计数。
4. `direction` 非法时静默"成功"`switch strings.ToUpper(in.Direction)`(第 24 行)只有 `PASS`/`REJECT` 两个 case、**没有 default**,非法值会跳过整个 switch 落到第 58 行的 `if err != nil`(此时 `err` 仍是第 19 行 `ParseMetaCtx` 的 nil最终返回 `Data: identity`(空字符串)的 `OK`
- **建议**:入参以 `group_identity` 为准,校验调用者在该群 `role<>0`;查到申请后校验其 `status==0`(未处理)再处理;整段(建成员 + 改申请 status + 计数)放进同一事务并加 `ON CONFLICT DO NOTHING`/唯一约束保证幂等;`switch``default` 返回 `ErrInvalidArgument`
#### 9. `Member.DoQuit`(退群):依赖不存在的 `creator_id`,群主校验失效且没有转让机制,删除 0 行仍扣减计数
- **位置**`module/social/group/internal/logic/member/do_quit.go:23-41`
- **证据**
```go
23 var creator_id uint
24 if err = impl.DBService.Model(&models.GroupBasic{}).Select("creator_id").Where("identity=?", in.GroupIdentity).Scan(&creator_id).Error; err != nil {
26 return nil, errcode.ErrDB
27 }
29 if auth.ID == creator_id {
30 return nil, errcode.ErrPermissionDenied
31 }
33 err = impl.DBService.Model(&models.GroupMember{}).Delete("group_identity=? and passport_id=?", in.GroupIdentity, auth.ID).Error
39 models.UpsetGroupMemberTotal(in.GroupIdentity, "-")
```
- **影响**:分支 A`creator_id` 列)→ 推测恒 `ErrDB`**用户永远无法退出群组**;分支 B列存在但从未被写入、值为 0/NULL`Scan` 不报错且 `creator_id=0``auth.ID==0` 恒假,**群主可以直接退群**,而模块内**没有任何转让群主/继任者机制**proto 与 logic 均无),群将永久失去创建者:后续 `DoSetManager``Modify``Disband`(若有实现)全部无人有权执行。另外 `Delete` 影响 0 行(非成员调用)仍会扣减 `member_total`
- **建议**:统一为 `passport_id`;群主退群必须走"先转让"(新增 `Transfer` RPC 或在退群入参中要求继承人并校验其为成员);用 `RowsAffected` 决定是否更新计数。
#### 10. `Basic.Disband`(解散群组)是模板 TODO 占位:校验后直接返回 OK不解散任何数据
- **位置**`module/social/group/internal/logic/basic/disband.go:16-31`
- **证据**
```go
16 _, err = service.ParseMetaCtx(ctx, nil)
22 if in.Id == 0 && in.Identity == "" {
23 return nil, errcode.ErrInvalidArgument
24 }
26 // TODO: add your logic code & delete this line.
28 return &pb.DataStatusReply{
29 Data: vars.OK,
```
- **影响**:这是本模块唯一的模板 TODO顶层 `README.md:209` 也确认"`Basic.Disband` 解散群组仍为占位")。行为上:调用者(任意登录用户,**该方法没有任何所有权校验**)收到 `OK`,客户端有充分理由把群从 UI 移除,而服务端群、成员、申请记录**全部保留**——"假成功"造成的状态分歧比直接报错更危险;同时该方法也没有软删除/级联清理成员与申请的语义。一旦有人"顺手补实现"而漏掉群主校验(当前文件里就没有这个校验骨架),即成为 P0 越权解散。
- **建议**:实现为事务:校验 `passport_id == auth.ID`(群主)→ 软删 `group_basic` + 级联软删 `relation_group_member`/`relation_group_apply` → 清理缓存;未实现前应返回 `ErrUnimplemented` 而不是 `OK`
#### 11. 群成员计数 `member_total` 链路整体失效SQL 非法、错误被吞、直接入群不计数、0 行删除仍扣减
- **位置**`module/social/group/internal/models/query.go:45-46`;调用点 `internal/logic/member/do_kick.go:32-39``do_quit.go:33-41``join_do_handle.go:52``do_join.go:28-37`;写入点 `internal/logic/basic/create.go:30`
- **证据**
```go
45 func UpsetGroupMemberTotal(group_identity string, op string) {
46 impl.DBService.Model(&GroupBasic{}).Where("identity=?", group_identity).UpdateColumn("member_total", gorm.Expr("member_total ? ?", op, 1))
47 }
```
`op` 是被当作**绑定参数**传入的运算符GORM 会把 `Expr` 中每个 `?` 渲染成占位符(`clause/expression.go` + `statement.go:263-265`),实际 SQL 形如 `SET "member_total"=member_total $1 $2`(参数 `"-", 1`),运算符位置是绑定变量 → 推测 PostgreSQL 直接报语法/运算符错误;而该函数**没有返回值、也不检查 `.Error`**,调用方(`do_kick.go:38``do_quit.go:39`)也无从得知失败。
- **影响**
1. 计数从未被正确更新(错误 100% 被吞),并且 `do_join.go:28-37` 的"免验证直接入群"分支**根本不调用**计数更新 → 计数与实际成员数**永久不一致**`Create` 写死的 `MemberTotal: 1``create.go:30`)是唯一可靠值。
2. 即使 SQL 修好,协议也是错的:`Delete` 影响 0 行(踢不存在的成员、非成员退群)时仍会 `-1`,可把计数刷成负数(无 `member_total > 0` 守卫)。
3. 计数器是**反规范化字段**,却没有与成员增删放在同一事务内(见问题 17并发下必然丢失更新。
- **建议**`gorm.Expr("member_total + ?", 1)` / `-1` 并把 `ok` 暴露给调用方;用 `RowsAffected` 门控;或直接删除该冗余计数,改为 `COUNT(*)``MemberFetchPage` 场景加索引即可)。
#### 12. `Member.DoJoin`:返回值不是落库 identity无重复申请/入群去重;不校验群存在、(id,identity) 匹配与成员上限;免验证开关恒失效
- **位置**`module/social/group/internal/logic/member/do_join.go:22-60``internal/models/query.go:39-43`
- **证据**
```go
23 identity := utils.UUID() // ← 返回给客户端
26 if models.GetGroupEnableAnyJoin(auth.ID, in.Identity) { // 参数用「申请人」当群主
28 apply := &models.GroupMember{GroupID: uint(in.Id), GroupIdentity: in.Identity, ...}
33 apply.Identity = utils.UUID() // ← 落库 identity 与返回值不同
...
40 apply := &models.GroupApply{FromID: uint(auth.ID), GroupID: uint(in.Id), GroupIdentity: in.Identity, Message: in.Message}
47 apply.Identity = utils.UUID() // ← 落库 identity 与返回值不同
```
调查询开关的实现:
```go
41 impl.DBService.Model(&GroupBasic{}).Select("enable_any_join").Where("createor_id=? and identity=?", passport_id, group_identity).Scan(&is)
```
- **影响**
1. **契约断裂**:接口返回的 `Data` 是一个从未入库的 UUIDv7客户端无法用它做后续 `JoinDoHandle`(会 `ErrRecordNotFound``data` 字段实际是"无效凭据"。
2. **无去重**:同一用户可对同一群无限次 `DoJoin`(申请表中无数一约束、代码无存在性检查),形成申请刷量/骚扰;若走直接入群分支则产生重复成员行,而 `DoQuit`/`DoKick` 的软删除 `Delete` 会一次删掉所有匹配行,计数却只减 1。
3. **无群存在性校验、无 (id, identity) 一致性校验**`GroupID``GroupIdentity` 都直接来自请求,二者可任意组合,构造出的申请会在审核通过后生成"`group_id` 属 A、`group_identity` 属 B"的错配成员行,并把计数加到 B 群头上(`join_do_handle.go:35-36,52`)。属于 IDOR 型数据污染。
4. **成员上限完全未实现**`member_limit` 只在 `create.go:28` 被写成固定 500任何路径都没有 `member_total < member_limit` 判断proto 注释"0代表不限制"`group.proto:35`)与模型默认 100`group_basic.go:15`)也不一致。
5. **`enable_any_join` 开关恒失效**`GetGroupEnableAnyJoin` 的列名拼错为 `createor_id`(正确是 `creator_id`,且模型根本没有该列),`Scan` 错误被忽略,且语义错位(把**申请人**当作群主去匹配)——该函数**必然**返回 `false``do_join.go:28-37` 的免验证直接入群分支是死代码,"免验证入群"这一产品能力从未生效(`basic/const.go:10``DefaultEnableAnyJoin = true` 也从未被读取)。
- **建议**`identity` 与落库值统一(`apply.Identity = identity`,或返回落库对象);入参改为只接受 `group_identity` 并据此查群取 `id`;加 `(group_identity, passport_id)` 唯一约束 + `FirstOrCreate`;加 `member_total < member_limit` 校验与入群频率限制;把开关查询改为 `Where("identity=?", group_identity)` 并检查错误。
#### 13. `Basic.Create`:无事务、群号从不生成、忽略请求中的 `member_limit`、零输入校验
- **位置**`module/social/group/internal/logic/basic/create.go:23-55``internal/models/group_basic.go:11,15`
- **证据**
```go
23 record := &models.GroupBasic{
24 Name: in.GetName(),
28 MemberLimit: DefaultMemberLimit, // 请求里的 member_limit 被忽略
29 EnableAnyJoin: DefaultEnableAnyJoin,
30 MemberTotal: 1, // 第 23-31 行整体未给 Number群号赋值
31 }
32 record.Identity = utils.UUID()
33 record.PassportID = auth.ID
36 err = impl.DBService.Create(&record).Error
...
42 member := &models.GroupMember{GroupID: record.ID, GroupIdentity: record.Identity, Role: ROLE_CREATEOR}
51 err = impl.DBService.Create(&member).Error
```
- **影响**
1. **无事务**:群记录成功、创建者成员行失败时留下"无创建者成员的孤儿群"(群主自己都不在成员表里),而接口返回错误;反之成员行成功而群失败亦不可回滚。
2. **群号恒为 0**`Number` 声明为 `column:number;not null``group_basic.go:11`却从不赋值GORM 会把零值写库 → 所有群的 `number=0`。若库上对该列有唯一索引,**第一个群之后所有创建请求都会失败**;若无唯一索引,则"群号"完全失去标识意义(`enable_search_by_number` 也因此无意义)。分支取决于库外 DDL无法验证
3. `member_limit` 固定 500 与模型默认 100 不一致,客户端无法设置;`DefaultEnableAnyJoin=true` 也只写进了库(写入后无人读取,见 P1-12
4. 零校验:`Name` 允许为空串(`not null` 不拦空串)或超 255 字符DB 报错 → `ErrDB``Introduce`/`Notice`text`Avatar`/`Background`varchar 255同样无长度/内容校验;`Master`/`Tags`/`Number` 等请求字段被静默丢弃(`GroupItem.cretor_identity` 拼写错误,见 P3-28
5. 无频率限制:任意登录用户可无限建群。
- **建议**`DBService.Transaction(...)` 包住两步写入(或用 `Association`/同事务 `Create`);生成唯一群号(序列或随机不重复号 + 唯一索引 + 冲突重试);白名单读取 `member_limit`(限制上界)并统一默认值;校验 `name` 非空与长度、限制公告/简介长度并做内容过滤。
### P2
#### 14. 配置声明开启 HTTP gateway但 `Mux` 从未初始化、也没有任何 gateway handler 注册 → 请求必定 panic
- **位置**`module/social/group/internal/server/new.go:13-33``cmd/main/main.go:24-35``etc/group_{dev,test,prod}.yaml:19-21``D:\work\bsm-sdk\core\service\service.go:106-111,120-129`
- **证据**
```go
13 type Server struct {
16 Mux *gwRuntime.ServeMux // 从未赋值
20 func New(addr string) *Server {
21 srv := &Server{Ctx: context.Background(), Grpc: grpc.NewServer(), grpcConns: make(...)} // 结构体无 Mux 赋值
28 pb.RegisterBasicServer(srv.Grpc, NewBasicServer())
```
```yaml
Gateway:
Enable: true
Port: 12425
```
```go
108 go s.Gateway(s.Opts.Addr, addr)
126 if err := http.ListenAndServe(httpAddr, s.Opts.GatewayMux); err != nil {
```
- **跨模块系统缺陷(本报告不展开论证,也不作为本模块独立 P0**:内部 `internal/server/new.go` 只注册 gRPC`Mux` 恒 nil、从不调用 `gwRuntime.NewServeMux()``service/expose.go`(本模块缺失,对照 `module/finance/wallet/service/expose.go:30-41`)把 `options.Gateway` 直接交给生成的 `pb.Register*HandlerServer`,而生成代码首行即 `mux.Handle(...)``pb/group.pb.gw.go:206``pb/member.pb.gw.go:233`)无 nil 防御;独立部署入口 `cmd/main` 又从不调用 `Expose`。因此"独立部署时 HTTP 网关不可用"是**全仓模板缺陷**,本模块只需决策"是否启用 Gateway",修复应在模板/聚合层统一进行。
- **影响**`Mux` 恒为 nil且全模块含生成的 `pb/group.pb.gw.go:205``pb/member.pb.gw.go:232`**从未调用** `RegisterBasicHandlerServer`/`RegisterMemberHandlerServer`(对照 `module/finance/wallet/service/expose.go:30-41` 的标准接入方式)。因此 `http.ListenAndServe("0.0.0.0:12425", (*gwRuntime.ServeMux)(nil))` 收到任何请求都会在 `ServeMux.ServeHTTP` 解引用 nil推测`grpc-gateway/v2@v2.30.0/runtime/mux.go``s.unescapingMode` 字段读取 → nil pointer panic`net/http` 会按连接 recover 并打印 64KB 栈(`http: panic serving ...`),即**每个请求都会产生一次 panic 日志放大**,客户端拿到空响应。`wiki/api/14-group.md:11` 承诺的 `POST /group.Basic/*` 路由实际不存在。
- **建议**:按 `module/finance/wallet/service/expose.go``service/expose.go`(创建 `runtime.NewServeMux()` 并注册 `RegisterBasicHandlerServer`/`RegisterMemberHandlerServer`),或在未接入前把 `Gateway.Enable` 置 false同时确认 `Gateway.Port` 的唯一性(见 P2-15
#### 15. 三份环境配置完全相同prod 指向本地 `bsm_dev` 与 `CHANGE_ME`,端口与其它模块冲突,且未注册 etcd
- **位置**`etc/group_dev.yaml``etc/group_test.yaml``etc/group_prod.yaml`(三文件 MD5 均为 `1D0D05F67F310F43608020899215E982`)、`etc/supervisor.bsm-social-group.conf:1-8`
- **证据**
```yaml
Databases:
Driver: postgres
Source:
- host=127.0.0.1 user=postgres password=CHANGE_ME dbname=bsm_dev port=5432 sslmode=disable TimeZone=Asia/Shanghai
Cache: redis://null:CHANGE_ME@127.0.0.1:6379/
MicroService:
Enable: false
Gateway:
Enable: true
Port: 12425
```
- **影响**
1. `group_prod.yaml` 与 dev 完全一致:生产指向 `127.0.0.1/bsm_dev`、凭据为 `CHANGE_ME`。顶层 `README.md:286-288` 明确"YAML 仅为结构示例,部署时必须注入真实值",但本模块连环境区分都没做(`conf.New` 只支持 `os.ExpandEnv`,而文件里没有 `$VAR`),一旦发布流程未覆盖 `etc/`,生产会连到开发库。
2. `Gateway.Port: 12425``module/base/passport/etc/passport_prod.yaml``module/social/relation/etc/relation_prod.yaml` 相同(同一主机启动必然有一个绑定失败),而绑定失败在 SDK 里**只打一条 `printer.Error` 不退出**`service.go:126-128`)→ 网关静默缺失。
3. `MicroService.Enable: false``service.go:66-90`)→ 不向 etcd 注册路由表,平台网关(`gateway/internal/rpc/rpc.go:202``RpcPool.RandomConn`)无法发现该服务;配合 P2-14本模块实际只有"直连 gRPC 端口"一种可达方式。
4. `supervisor``user=root` 运行、只 `redirect_stderr`、无日志轮转(`stdout_logfile=/data/app/logs/social-group.log``Etcd` 段在 YAML 中缺失(`config.Spec.Etcd == nil``Token`/`Kyc`/`WeChatConf`/`Rpc`/`APM` 段或缺失或从不被使用(见 P3-27
- **建议**:为 prod/test 提供真实(或至少明确的占位+注入说明)配置并区分端口;开启 `MicroService.Enable` 或明确文档化"仅直连 gRPC"supervisor 降权、加 `stdout_logfile_maxbytes`/轮转。
#### 16. 数据库连接常开 Debug 日志:生产环境全量打印 SQL 与参数
- **位置**`module/social/group/internal/impl/impl.go:19-25``D:\work\bsm-sdk\core\with\databases.go:12-26``D:\work\bsm-sdk\core\database\sql\postgresql.go:11-24,46-48`
- **证据**
```go
23 DBService = with.Databases(config.Spec.Databases, nil) // opts = nil
```
```go
13-21 options = &types.SqlOptions{ ..., IsAutoMigrate: false, ..., Debug: true } // SetOptions(nil) 默认值Debug 在第 19 行
46 if options.Debug {
47 gormDb = gormDb.Debug()
48 }
```
- **影响**`Debug: true` 使 GORM 的 default logger 以 Info 级别把**每条 SQL 及其参数**输出到标准输出,再由 supervisor 落盘;生产环境日志量成倍放大,且慢查询/全表扫描会被记录为普通日志(无指标、无阈值)。参数里包含 `passport_id`/群 identity 等可关联到个人的标识(无密码类字段,故未按"敏感信息泄露"计 P1
- **建议**:改用 `with.Databases(cfg, &types.SqlOptions{Debug: env.Runtime.Mode != "prod", ...})`,或显式传入配好 `Logger``LogLevel`SlowThreshold的 options。
#### 17. 多写路径全部没有事务,任一步失败即产生不一致数据
- **位置**`internal/logic/basic/create.go:36,51``internal/logic/member/join_do_handle.go:43,49,52``internal/logic/member/do_kick.go:32,38``internal/logic/member/do_quit.go:33,39`(全模块 `grep Transaction` 无匹配)
- **证据**
```go
36 err = impl.DBService.Create(&record).Error // 群
51 err = impl.DBService.Create(&member).Error // + 创建者成员(两步之间无事务)
```
- **影响**:群 + 成员 + 申请状态 + 计数分属 2~4 条独立语句:`Create` 可留下无群主的群;`JoinDoHandle` 可留下"成员已建但申请仍为 pending / 计数未加"的状态;踢人/退群可出现"成员已删但计数未减"。GORM 的隐式事务只覆盖**单条**语句,跨语句必须显式开启,而全模块没有任何 `Transaction`/`Begin` 调用(`grep -rn "Transaction\|Begin(" internal/` 无匹配SDK 的 MySQL 分支还显式设置了 `SkipDefaultTransaction: true``D:\work\bsm-sdk\core\database\new.go``NewMysql`),说明该团队默认依赖调用方自行控制事务边界。
- **建议**:所有涉及 ≥2 表写入的用例用 `impl.DBService.Transaction(func(tx *gorm.DB) error {...})`,并把计数更新纳入事务。
#### 18. 错误被静默吞掉4 处),失败与成功对外不可区分
- **位置**`internal/models/query.go:26``Pluck` 的 error 被丢弃)、`internal/models/query.go:41``Scan` 的 error 被丢弃)、`internal/models/query.go:46``UpdateColumn` 无返回值/未检查)、`internal/logic/member/do_set_manager.go:37,40``UpdateColumn` 未检查)
- **证据**
```go
26 impl.DBService.Model(GroupMember{}).Select("group_id").Where("passport_id=? and role!=0", passport_id).Pluck("group_id", &ids) // 无 .Error 检查,失败返回空切片
41 impl.DBService.Model(&GroupBasic{}).Select("enable_any_join").Where("createor_id=? and identity=?", ...).Scan(&is) // 同上
```
- **影响**:数据库故障/列名错误被伪装成业务结果——`GetMasterGroup` 失败会让 `JoinFetch` 返回"空申请列表"`GetGroupEnableAnyJoin` 失败会静默改变入群分支,`DoSetManager` 失败仍返回 `Data: vars.OK`。与 P1-11 的错误吞没叠加后,运维无法通过日志区分"没有数据"和"查询挂了"。
- **建议**:把这些函数签名改为返回 `error`,调用方统一映射为 `ErrDB`;对 `UpdateColumn` 一律检查 `.Error``RowsAffected`
#### 19. 无 context 传递、无超时/重试,客户端取消无法中断数据库查询
- **位置**:全模块(`grep WithContext module/social/group` 无匹配);`internal/impl/impl.go:23`
- **证据**
```go
20 groups, err := models.GetMyGroup(auth.ID) // fetch.go:20未传 ctx
35 err = impl.DBService.Where("identity=? and creator_id=?", in.Identity, auth.ID).UpdateColumns(record).Error // modify.go:35
```
- **影响**:所有查询/更新都跑在 `*gorm.DB` 的默认上下文上gRPC 请求被取消或超时后 SQL 仍会执行到底(连接被占用),无 `context.WithTimeout`、无重试策略、无熔断;在慢查询(例如无索引的成员列表)下会放大连接池压力。`service.ParseMetaCtx` 解析出的 claims 也未放入 ctx 传递(后续日志/审计无法关联请求)。
- **建议**`impl.DBService.WithContext(ctx)` 统一注入;对外部依赖设置超时;在 logic 入口加 `context.WithTimeout`
#### 20. 无分页、无索引声明、无迁移:列表接口是全量扫描,索引只能依赖库外 DDL
- **位置**`internal/models/query.go:14-22,30-37``internal/logic/basic/fetch.go``internal/logic/member/fetch.go``internal/logic/member/join_fetch.go`(均无 LIMIT/OFFSET`internal/models/group_member.go:11``group_apply.go:11-14`
- **证据**
```go
11 GroupID uint `gorm:"group_id;not null;" json:"group_id"` // 未用 column: 前缀,也没有 index/uniqueIndex 标签
```
- **影响**`proto``Fetch` 只接受 `blocks.Empty`/`blocks.IdentRequest`,没有 `page_no/page_size`(对照 `const.proto:10-14` 已有的 `FetchRequest` 未被使用)→ 成员列表/申请列表/群列表都是"一次性全量",大群会把全部行读进内存(`[]*pb.PassportInfoSimpleCard`)再整体序列化。同时模型层没有声明任何索引,`group_id`/`passport_id`/`group_identity` 是否建索引完全取决于库外 DDL`IsAutoMigrate: false`,本模块无 `AutoMigrate` 调用)。按条件列推断(`WHERE group_identity=?``WHERE passport_id=? AND role<>0``WHERE group_id IN ?`**缺少 `(group_identity, passport_id)` 组合索引即会全表扫描**——实际索引存在性无法验证(推测)。
- **建议**:列表 RPC 增加分页参数并强制上界;模型补 `index`/`uniqueIndex` 标签并补一份 DDL 迁移脚本纳入仓库;至少为 `relation_group_member(group_identity, passport_id)``relation_group_member(passport_id, role)``relation_group_apply(group_id, status)` 建索引。
#### 21. 权限/可见性校验缺口汇总(除 P1-6/7/8 外)
- **位置**`internal/logic/member/fetch.go:14-33`(只校验登录,不校验调用者是群成员)、`internal/logic/basic/get.go:16-25`(无可见性/成员校验,也不读 `enable_search_by_name/number`)、`internal/logic/basic/search.go:17-21`
- **证据**
```go
15 _, err = service.ParseMetaCtx(ctx, nil) // Member.Fetchclaims 被丢弃
20 if in.Identity == "" { return nil, errcode.ErrInvalidArgument }
24 members, err := models.GetGroupMember(in.Identity)
```
- **影响**:群成员列表与群资料对所有"已登录用户"开放,只要知道群 identity 即可读取identity 为 UUIDv7不可枚举故计 P2`enable_search_by_name`/`enable_search_by_number` 两个开关(`group_basic.go:22-23``group.proto:41-42`)在整个模块中**没有任何读取点**`Basic.Search` 又只做空返回,所以"按名称/群号搜索"这一产品能力实际不存在,开关形同虚设。
- **建议**:为读接口补成员校验(或明确"群资料公开"并在 proto/文档写清);实现 `Search` 时把两个开关作为查询条件,并加分页。
#### 22. 无输入长度/内容校验,群名与公告存在内容注入面
- **位置**`internal/logic/basic/create.go:24-27``internal/logic/member/do_join.go:40-46``internal/models/group_basic.go:13-20``internal/models/group_apply.go:15`
- **证据**
```go
24 Name: in.GetName(), // varchar(255) not null无长度/非空校验
26 Notice: in.GetNotice(), // text无任何过滤
```
```go
45 Message: in.Message, // varchar(255),无长度校验
```
- **影响**:超长入参会直接触发 PostgreSQL `value too long` 并被统一包装成 `ErrDB(1501)`(错误语义掩盖真实原因);`Name`/`Notice`/`Introduce`/`RemarkName` 原样入库、无转义无长度上限,若客户端以 HTML 渲染即为**存储型 XSS / 内容注入**面(本仓库无前端,标记为推测)。`Message` 也没有敏感词或频率限制(见 P2-23
- **建议**:在 logic 入口做 `utf8.RuneCount` 长度校验与 trim/非空校验,返回 `ErrInvalidArgument`;对 `Notice`/`Name`/`Introduce` 做 HTML 转义或按纯文本协议在前端渲染;统一超长错误的语义。
#### 23. 无任何限流/防刷与幂等键,已初始化的 Redis/内存缓存完全未使用
- **位置**`internal/impl/impl.go:12-25``RedisService`/`MemorySerice` 只被赋值,全模块无引用)、`internal/logic/member/do_join.go``internal/logic/basic/create.go`
- **证据**
```go
13 RedisService *redis.RedisClient
16 MemorySerice *cache.Cache
22 RedisService = with.RedisCache(config.Spec.Cache) // 之后无任何读取
```
- **影响**`Create`(建群)、`DoJoin`(申请加群)都没有频次限制、验证码或幂等键,恶意用户可无限建群/刷申请(写放大 + 存储膨胀);同时模块没有任何缓存,`Basic.Get`(群资料)这类热点读也无法缓存,纯靠数据库。
- **建议**:用已注入的 `RedisService``SETNX` 幂等键与滑动窗口限流(按 `passport_id` + 接口维度);群资料/成员数等热点数据加短 TTL 缓存并明确失效点成员增删、Modify 时删除缓存)。
#### 24. 软删除与唯一约束的冲突风险:踢人/退群是软删除,重新加入会新增一行
- **位置**`internal/models/group_member.go:9,19`(内嵌 `Std_IICUDS`,含 `gorm.DeletedAt`)、`internal/logic/member/do_kick.go:32``do_quit.go:33``do_join.go:37`
- **证据**
```go
32 err = impl.DBService.Model(&models.GroupMember{}).Delete("group_identity=? and identity=?", in.GroupIdentity, in.Identity).Error // 软删除UPDATE deleted_at
```
- **影响**:成员行不会被物理删除,`(group_id, passport_id)` / `(group_identity, identity)` 上的唯一索引(若 DDL 建了)会与重新入群的新行冲突 → **退群/被踢后无法再次加入**(返回 `ErrDB`);若没有唯一索引,则同一用户会在表中留下多行历史记录,成员列表(若修好)会出现重复项。此外 `query.go:34``deleted_at is null` 在多表 join 下是歧义列(见 P1-4。DDL 不可见,冲突必然性标注为推测。
- **建议**:明确"退出"的语义——要么物理删除(`Unscoped()` + 唯一约束),要么改为状态字段(`status=-1`)并在重新入群时"复活"原行(`OnConflict` 更新 `deleted_at=NULL`join 查询统一使用带表别名的 `gm.deleted_at IS NULL`
#### 25. 可观测性与生命周期缺口:日志无上下文、无健康检查、`cmd/cli` 空入口、优雅退出不完整
- **位置**`cmd/cli/main.go:1-4`(空 `main`)、`cmd/main/main.go:37-44``internal/server/new.go:22-31``reflection.Register`)、全部 logic 的 `printer.Error(err.Error())`
- **证据**
```go
3 func main() {
4 }
```
```go
31 reflection.Register(srv.Grpc)
```
- **影响**`cmd/cli` 是空实现(顶层 README 也把它当作"命令行辅助入口");日志只有裸 `err.Error()`,没有 request_id/操作者 `passport_id`/群标识等结构化字段,而这些问题恰恰发生在权限判定路径上,线上无法复盘"谁对哪个群做了什么";无 `Health`/`Ready` 探针(无法区分进程存活与 DB/Redis 可用gRPC `reflection` 在生产开放(顶层 `README.md:288` 建议评估关闭),配合无 TLS 的明文 gRPC`grpc.NewServer()` 无凭据)会暴露完整接口面并让 JWT 走明文(`BindIP` 取自本机地址,`conf.CheckIP`)。`service.Stop()` 只做 `GracefulStop`,没有先从 etcd 摘除注册(`service.go:142-144`)。
- **建议**:日志改用带 request_id/操作者的结构化输出SDK `printer` 支持格式串);增加 `Health/Ready` RPC按环境关闭 reflection 并启用 TLS/mTLS`Stop()``Revoke` 租约再 `GracefulStop`;删除空 `cmd/cli` 或补齐子命令。
### P3
#### 26. 角色/状态枚举与常量重复定义、魔法数字散落,`direction` 用字符串承载枚举
- **位置**`internal/logic/basic/const.go:3-14``internal/logic/member/const.go:3-14`(两文件 15 行**逐行重复**)、`internal/models/group_member.go:14``proto/member.proto:87`
- **证据**
```go
4 ROLE_MEMBER = 0 // 普通会员
5 ROLE_MASTER = 1 // 管理员
7 ROLE_CREATEOR = 10 // 创建者
```
```proto
87 string direction=3; //操作方式Pass通过,Reject拒绝,Set设置,cancel取消
```
- **影响**:同一套角色常量在两个包各定义一份(`basic.DefaultMemberLimit``member.DefaultMemberLimit` 也重复),跨包使用同一含义的魔法数字 `0``join_do_handle.go:37` 直接写 `Role: 0`,而 `do_set_manager.go:40` 用常量),常量名 `ROLE_CREATEOR` 拼写错误角色语义0/1/2/10其中 `ROLE_SUPER_MASTER=2` 无任何使用点)没有单一事实来源。`direction` 用大小写不敏感的字符串承载 4 种枚举,非法值被 `switch` 静默吞掉P1-7、P1-8且与 `Basic` 服务的 `IdentRequest` 风格不统一。
- **建议**:把角色/状态抽到一个共享包(或 `internal/models`)并定义 `Role` 类型常量+`Valid()``direction` 改为 proto enum`OP_PASS/OP_REJECT/OP_SET/OP_CANCEL/OP_UNSPECIFIED``OP_UNSPECIFIED` 一律返回 `ErrInvalidArgument`
#### 27. 死代码与被静默丢弃的字段/配置
- **位置**`internal/models/group_res.go:1-21``relation_group_res` 模型全模块零引用)、`internal/logic/basic/const.go:14``filed` 仅在 member 包使用)、`internal/server/new.go:17,24``grpcConns` 连接池只创建不使用)、`internal/impl/impl.go:21-22``MemorySerice`/`RedisService` 零引用,且 `MemorySerice` 拼写错误)、`internal/config/config.go:15-25``WeChat`/`Kyc`/`Rpc`/`Apm`/`Token` 配置段无任何读取点)、`proto/group.proto:36-39``master`/`background`/`tags` 字段无落库逻辑)
- **证据**
```go
14 filed string = "passport_identity as identity,nickname,avatar,sex,province,city,area,sign"
```
- **影响**:这些残留物让"能力"看起来比实现多(群文件/群主列表/背景/标签、第三方配置、连接池),会持续误导接入方与后续审计(顶层 `README.md:276``service` 描述为聚合接入层,而本模块该目录为空,也属此类)。
- **建议**:删除或接线:`GroupRes``relation_group_res` 要么补 RPC 要么删;`filed` 移入 member 包或删除 basic 包中的重复定义;未使用的配置段从 `SrvConfig` 移除或在 etc 中注释说明。
#### 28. 命名/文档与实现不一致proto、README、wiki
- **位置**`proto/group.proto:34``cretor_identity` 拼写错误)、`:33``creator_id` 在模型中实为 `passport_id`)、`:35`"0代表不限制" vs 代码固定 500 / 模型默认 100`wiki/api/14-group.md:11`(承诺 grpc-gateway 路由 `POST /group.Basic/*`,实际无 handler见 P2-14`README.md:207-209`(把搜索/列表/修改列为"主要功能",实际 Search 空实现、Fetch/Get/Modify 不可用)、顶层 `README.md:313`(要求 `service.Expose`
- **证据**
```go
12 func Search(...) (reply *pb.GroupsReply, err error) {
21 return &pb.GroupsReply{}, nil // 空实现
```
- **影响**:接入方按 wiki/README 编写客户端会得到空列表/错误码;`cretor_identity` 一旦被前端依赖就形成永久兼容负担。
- **建议**:统一为 `creator_identity`proto 改字段名需保留 `json_name` 兼容);把各接口的真实实现状态写进模块 README现在只有一行 `# group``service/` 补空目录说明或补 `expose.go`
#### 29. 错误码语义不当与"总数"语义错误
- **位置**`internal/logic/basic/get.go:23``internal/logic/member/join_do_handle.go:29`(记录不存在返回 `ErrDB`)、`internal/logic/member/do_quit.go:30`"群主不能退群"用 `ErrPermissionDenied`)、`internal/logic/basic/fetch.go:28``internal/logic/member/fetch.go:32``internal/logic/member/join_fetch.go:41``Total` = 本页/本次返回条数)
- **证据**
```go
28 Total: int32(len(groups)), // 语义上应为全量总数
```
- **影响**:调用方无法区分"参数不存在"(应为 `ErrRecordNotFound`/`ErrInvalidArgument`)与"数据库故障"`ErrDB`);分页引入后 `Total` 若继续用 `len()` 会直接算错总页数。
- **建议**`gorm.ErrRecordNotFound` 映射为 `ErrRecordNotFound``Total` 用独立 `Count(*)`
#### 30. 群 identity 兼作加入凭证,且缺少 `id`/`identity` 一致性校验UUIDv7 时间有序
- **位置**`internal/logic/member/do_join.go:28-46`(请求同时携带 `id``identity`,二者不校验)、`D:\work\bsm-sdk\core\utils\identity.go:8-10``utils.UUID()` = UUIDv7
- **证据**
```go
8 func UUID() string {
9 return uuid.Must(uuid.NewV7()).String()
10 }
```
- **影响**:模块没有独立的邀请码/邀请链接机制,`group.identity` 既是公开标识又是加入凭证持有即可申请入群一旦泄露无法吊销无过期、无重置接口UUIDv7 前 48 位是毫秒时间戳,可推断群/成员的创建时间与相对顺序(信息泄露面很小,但不符合"不可预测凭证"的最佳实践)。请求中的 `id``identity` 从不互相校验P1-12 的错配来源)。
- **建议**:入参只保留 `identity`(或强校验二者匹配);需要邀请制时引入独立、可吊销、可设过期的邀请码;对外标识与加入凭证分离。
#### 31. 生成代码与脚手架残留的一致性提示
- **位置**`internal/server/{basic_server.go,member_server.go,new.go}:1``// Code generated by protoc-gen-slc. DO NOT EDIT.`)、`internal/server/new.go:20``New(addr string)``addr` 参数未使用)、`internal/server/basic_server.go:11``member_server.go:11`(内嵌 `Unimplemented*Server`,但所有 13 个方法都被覆写为"假实现",因此 `codes.Unimplemented` 永远不会返回)
- **影响**`pb.Unimplemented*Server` 的语义是"未实现即报 `Unimplemented`",而本模块 13 个方法全部被覆写成"返回成功/返回空对象"**库层面的"未实现保护"被绕过**——这正是 `Disband`/`Search` 等空实现能对外返回 OK 的机制原因。`addr` 形参未使用、`Mux` 未初始化P2-14说明脚手架版本与控制器模板不一致。
- **建议**:未实现的方法不要覆写(保留 `Unimplemented*Server` 默认行为直接返回 `codes.Unimplemented`),或显式返回 `ErrUnimplemented`;同步更新 `protoc-gen-slc` 模板(生成 `Mux` 初始化 + gateway 注册),三处一起改,避免只修一个模块。
## 4. 推荐优化方案
按"先止血、再补能力、后清理"三步推进;由于本模块目前**几乎不可用**13 个 RPC 中 12 个有确定性缺陷),工作量集中在第 1 步。
### 第 1 步:让已有接口"要么正确、要么明确报错"P0/P1 级1~2 人日)
1. **统一数据访问层**:删掉全部 raw SQL`internal/models/query.go:14-22,30-37`),改用 GORM 模型 + `Joins`/`Where("... IN ?")`,让表名(`group_basic`/`relation_group_member`/`relation_group_apply`)与软删条件由 schema 产生;所有查询 `WithContext(ctx)`、所有写操作检查 `.Error``RowsAffected`
2. **统一"所有权/角色"判定**:新增一个内部函数 `membership(tx, groupIdentity, passportID) (role, err)`,把 `creator_id` 全部替换为 `passport_id`(或补 `CreatorID` 字段并让 `Create` 写入),并按"角色矩阵"落地:解散/改资料=群主;设置管理员/踢人=群主或管理员(管理员不可操作群主与同级);退群=本人(群主须先转让);审核申请=群主或管理员。
3. **修返回值与契约**`Fetch`/`Get`/`Member.Fetch` 补回数据;`DoJoin` 返回落库 identity`Disband`/`Search` 未实现前返回 `ErrUnimplemented`;非法 `direction` 返回 `ErrInvalidArgument`
4. **修计数**`gorm.Expr("member_total + ?", 1)` + `RowsAffected` 门控,并把成员变更与计数放进同一事务;`do_join.go` 的直接入群分支补计数与上限校验。
5. **`Modify` 白名单更新**`Updates(map)` + 删除 `record.Identity = utils.UUID()`
### 第 2 步补齐产品能力与安全护栏P1/P2 级2~3 人日)
6. **入群链路完整化**`DoJoin` 只接受 `group_identity`(服务端查 `id``(group_identity, passport_id)` 加唯一约束 + `FirstOrCreate`;申请去重与限流;`JoinDoHandle` 加权限校验 + 申请状态机(仅 `status=0` 可处理)+ 事务;实现 `enable_any_join`(修正 `createor_id` 拼写与语义)与 `member_limit` 上限校验。
7. **群生命周期**`Create` 生成唯一群号(唯一索引 + 冲突重试)、事务写入群与创建者成员、校验名称非空/长度;新增群主转让 RPC 后再允许群主退群;实现 `Disband`(事务级联软删)。
8. **列表与检索**`Fetch`/`Member.Fetch`/`JoinFetch`/`Search` 全部加分页(复用 `const.proto:10-14``FetchRequest` 或新增分页消息)并实现 `Search`(按 `enable_search_by_name/number` 过滤)。
9. **接入与配置**:补 `service/expose.go`(对齐 `module/finance/wallet/service/expose.go`),或明确"仅直连 gRPC"并把 `Gateway.Enable` 关掉;区分 dev/test/prod 配置与端口;`Debug` 日志按环境关闭;按 `README.md:288` 打开 TLS、关闭 reflection。
10. **测试**:为权限矩阵(群主/管理员/成员 × 13 个 RPC、重复入群/重复审核的幂等、并发计数、软删除后重新入群建立表驱动测试(详见 §5
### 第 3 步可维护性与治理P3 级1 人日)
11. 角色/状态收敛到单一包 + proto enum清理重复常量与魔法数字。
12. 删除死代码(`GroupRes``filed`(basic)、`grpcConns`、未用配置段、`cmd/cli`),或在 README 标注 roadmap。
13. 用代码现状重写模块 README当前仅 `# group`)并同步 `wiki/api/14-group.md`,明确每个 RPC 的实现状态;把 `docs/audit/README.md` 的索引与汇总 TODO 回填。
## 5. TODO 清单
- [ ] **P1-1问题1** 重写 `Basic.Modify`:白名单 `Updates`、去掉 `identity` 重写、所有权条件改 `passport_id`、校验 `RowsAffected`|验收:改群资料后 `identity`/`passport_id`/`member_limit`/`member_total` 均不变,非群主调用返回 `ErrPermissionDenied`0 行受影响时返回 `ErrRecordNotFound`|涉及:`module/social/group/internal/logic/basic/modify.go:24`, `:33`, `:35`
- [ ] **P1-2问题2** 重写 `GetMyGroup`:表名改 `relation_group_member`/`group_basic`(或改 GORM 查询)、列加表别名、回填 `Groups`|验收:任一用户调用 `Basic.Fetch` 返回其全部群且 `Groups` 非空、`Total` 与条数一致|涉及:`module/social/group/internal/models/query.go:16`, `internal/logic/basic/fetch.go:27`
- [ ] **P1-3问题3** `Basic.Get` 改查 `models.GroupBasic` 并映射 pb不再丢弃结果验收按 identity 查询返回真实群资料name/avatar/notice/member_total 与库一致),不存在的 identity 返回 `ErrRecordNotFound`|涉及:`module/social/group/internal/logic/basic/get.go:20`, `:25`
- [ ] **P1-4问题4** `Member.Fetch` 修正表名、回填 `Members`、加"调用者须为该群成员"校验与分页|验收:非成员调用返回 `ErrPermissionDenied`;成员调用返回分页数据且 `Total` 为真实总数;单次查询有 LIMIT涉及`module/social/group/internal/models/query.go:31`, `internal/logic/member/fetch.go:31`
- [ ] **P1-5问题5** `JoinFetch``Where("group_id IN ?", ids)`、批量查申请人资料、每项独立对象|验收:管理 1 个及以上群的用户可获得正确申请列表,每个 `from` 卡片对应各自申请人(无覆盖/错乱SQL 条数不随申请数增长|涉及:`module/social/group/internal/logic/member/join_fetch.go:24`, `:29`, `:36`
- [ ] **P1-6问题6** `DoKick` 权限查询改 `group_identity` + `passport_id`(整型),踢人目标语义统一并检查 `RowsAffected`|验收:群主/管理员可踢普通成员,普通成员调用返回 `ErrPermissionDenied`,踢不存在的成员返回 `ErrRecordNotFound` 且不改计数|涉及:`module/social/group/internal/logic/member/do_kick.go:24`, `:32`
- [ ] **P1-7问题7** `DoSetManager` 目标成员加 `group_identity` 限定、检查错误与 `RowsAffected`、非法 direction 报错|验收:群主只能设置本群成员;跨群 identity 调用返回 `ErrPermissionDenied`/`ErrRecordNotFound``direction=foo` 返回 `ErrInvalidArgument`|涉及:`module/social/group/internal/logic/member/do_set_manager.go:37`, `:40`
- [ ] **P1-8问题8** `JoinDoHandle` 加"调用者在申请所属群 role<>0"校验、申请状态机与事务、幂等(唯一约束/`OnConflict DoNothing`)|验收:非管理员调用返回 `ErrPermissionDenied`;重复 PASS 不产生第二个成员行、计数只加一次;非法 direction 返回 `ErrInvalidArgument`|涉及:`module/social/group/internal/logic/member/join_do_handle.go:19`, `:43`, `:49`, `:52`
- [ ] **P1-9问题9** `DoQuit` 改用 `passport_id` 判群主、补群主转让前置条件、`RowsAffected` 门控计数|验收:群主未转让时退群返回明确错误;普通成员退群成功且计数减 1非成员调用不改计数涉及`module/social/group/internal/logic/member/do_quit.go:24`, `:33`, `:39`
- [ ] **P1-10问题10** 实现 `Basic.Disband`(群主校验 + 事务级联软删群/成员/申请);在实现前返回 `ErrUnimplemented`|验收:非群主调用返回 `ErrPermissionDenied`;群主调用后群、成员、申请全部不可见(软删)且不再出现在列表中|涉及:`module/social/group/internal/logic/basic/disband.go:26`, `:28`
- [ ] **P1-11问题11**`UpsetGroupMemberTotal``gorm.Expr("member_total + ?", 1)`、返回 error、按 `RowsAffected` 门控)并把成员增删与计数纳入同一事务|验收:直接入群/审核通过/踢人/退群后 `member_total``COUNT(*)` 一致;并发 50 次入群后计数误差为 0涉及`module/social/group/internal/models/query.go:45`, `internal/logic/member/do_kick.go:38`, `do_quit.go:39`, `join_do_handle.go:52`, `do_join.go:28`
- [ ] **P1-12问题12** `DoJoin` 返回值与落库 identity 统一;只接受 `group_identity` 并由服务端取 `id`;加去重、群存在性、`member_limit` 校验;修 `GetGroupEnableAnyJoin``identity` 条件 + 错误检查 + 上界)|验收:返回值可直接用于 `JoinDoHandle`;同一用户重复入群不产生第二行;超上限返回 `Errcode` 类业务错误;`enable_any_join=true` 的群为直接入群|涉及:`module/social/group/internal/logic/member/do_join.go:23`, `:26`, `:33`, `:47`, `internal/models/query.go:41`
- [ ] **P1-13问题13** `Create` 加事务、生成唯一群号、按请求解析 `member_limit`(带上界)、校验 `name` 非空/长度|验收:群与创建者成员同事务提交(失败全回滚);每个群 `number<>0` 且唯一;空名/超长名返回 `ErrInvalidArgument``member_limit` 为请求值与默认值的一致结果|涉及:`module/social/group/internal/logic/basic/create.go:23`, `:28`, `:36`, `:51`
- [ ] **P2-14问题14**`service/expose.go` 创建并注册 grpc-gateway handler或关闭 `Gateway.Enable`)|验收:`curl -XPOST :12425/group.Basic/Fetch` 返回业务响应而非 panic/空响应;进程日志无 `http: panic serving`|涉及:`module/social/group/internal/server/new.go:13`, `cmd/main/main.go:33`, `etc/group_prod.yaml:19`
- [ ] **P2-15问题15** 拆分 dev/test/prod 配置DB/Redis/端口/密钥),把 `Gateway.Port` 换成未被占用的值,明确 `MicroService.Enable` 取舍验收三份配置差异化且互不冲突prod 不再指向 `bsm_dev`/`CHANGE_ME`;同机启动 group/passport/relation 无端口绑定失败|涉及:`module/social/group/etc/group_prod.yaml:7`, `:10`, `:14`, `:21`
- [ ] **P2-16问题16** 生产关闭 GORM Debug 并配置慢查询阈值验收prod 日志不再出现成功 SQL 明细,仅保留 ≥ 阈值的慢查询;`Debug` 由环境变量控制|涉及:`module/social/group/internal/impl/impl.go:23`
- [ ] **P2-17问题17** 为所有多表写入加事务|验收:`Create``JoinDoHandle``Disband` 注入中途失败后数据完整回滚|涉及:`module/social/group/internal/logic/basic/create.go:36`, `internal/logic/member/join_do_handle.go:43`
- [ ] **P2-18问题18** `GetMasterGroup`/`GetGroupEnableAnyJoin`/`UpsetGroupMemberTotal` 返回 error所有 `UpdateColumn` 检查错误|验收:注入 DB 错误后调用方返回 `ErrDB` 而非空结果/OK`grep -n "_ = impl.DBService"` 无匹配|涉及:`module/social/group/internal/models/query.go:26`, `:41`, `:46`, `internal/logic/member/do_set_manager.go:37`
- [ ] **P2-19问题19** 全部 DB 调用注入 `ctx` 并加超时|验收:取消 gRPC 请求后数据库查询随之中断(可在 pg_stat_activity 观察);`grep WithoutContext` 无非 `WithContext` 的业务查询|涉及:`module/social/group/internal/logic/basic/fetch.go:20`, `internal/logic/basic/modify.go:35`
- [ ] **P2-20问题20** 列表接口加分页;模型补索引标签与 DDL 迁移脚本|验收:成员/申请/群列表均支持 page_no/page_size 且有上界;迁移脚本包含 `(group_identity, passport_id)` 等索引;`EXPLAIN` 显示索引扫描而非全表扫描|涉及:`module/social/group/internal/logic/member/fetch.go:24`, `internal/models/group_member.go:11`, `internal/models/group_apply.go:11`
- [ ] **P2-21问题21** 读接口补成员/可见性校验,并把 `enable_search_by_name/number` 用于检索条件|验收:非成员调用成员列表被拒;关闭开关的群不出现于搜索结果|涉及:`module/social/group/internal/logic/member/fetch.go:15`, `internal/logic/basic/search.go:21`
- [ ] **P2-22问题22** 入参加长度/非空/内容校验|验收:超长 `name`/`message`/`notice` 返回 `ErrInvalidArgument` 而非 `ErrDB`;输入被 trim 且拒绝空名|涉及:`module/social/group/internal/logic/basic/create.go:24`, `internal/logic/member/do_join.go:45`
- [ ] **P2-23问题23** 用 Redis 做入群/建群限流与幂等键|验收:同一用户 1 分钟内 `DoJoin` 超过阈值被拒;重复提交(同幂等键)只落一条申请|涉及:`module/social/group/internal/impl/impl.go:22`, `internal/logic/member/do_join.go:22`
- [ ] **P2-24问题24** 明确退出/踢出与唯一约束的语义物理删除或状态位复活join 查询统一 `gm.deleted_at IS NULL`|验收:退出后可再次加入且不产生重复行;成员列表无重复|涉及:`module/social/group/internal/logic/member/do_kick.go:32`, `do_quit.go:33`, `internal/models/query.go:34`
- [ ] **P2-25问题25** 结构化日志 + `Health/Ready` + 关闭 reflection/TLS + 优雅退出|验收:日志含 request_id 与操作者 identity`grpc.health.v1.Health/Check` 可反映 DB/Redis 状态SIGTERM 时先摘除 etcd 注册再 `GracefulStop`|涉及:`module/social/group/cmd/main/main.go:37`, `internal/server/new.go:31`, `internal/logic/member/do_kick.go:25`
- [ ] **P3-26问题26** 角色/状态常量单一来源 + `direction` 改 proto enum验收全模块只有一个 `Role` 定义;`OP_UNSPECIFIED` 一律报错;`grep -rn "ROLE_MEMBER" ` 只命中定义处|涉及:`module/social/group/internal/logic/basic/const.go:4`, `internal/logic/member/const.go:4`, `proto/member.proto:87`
- [ ] **P3-27问题27** 清理死代码与未用配置/字段|验收:`GroupRes`、basic 包 `filed``grpcConns``WeChat/Kyc/Rpc/Apm/Token` 配置、`master/tags/background` 字段各有"删除或接线"结论|涉及:`module/social/group/internal/models/group_res.go:8`, `internal/logic/basic/const.go:14`, `internal/server/new.go:17`, `internal/config/config.go:15`
- [ ] **P3-28问题28** 修正 proto 命名并同步 README/wiki验收`cretor_identity` 改为 `creator_identity`(保留 `json_name` 兼容);模块 README 逐 RPC 标注实现状态wiki 路由与实际网关一致|涉及:`module/social/group/proto/group.proto:34`, `README.md:1`, `wiki/api/14-group.md:11`
- [ ] **P3-29问题29** 修正错误码与 `Total` 语义|验收:记录不存在返回 `ErrRecordNotFound``Total` 为全量总数(分页下与 `Count(*)` 一致)|涉及:`module/social/group/internal/logic/basic/get.go:23`, `internal/logic/basic/fetch.go:28`
- [ ] **P3-30问题30** 加入凭证与对外标识分离(可选邀请码/可吊销),并强校验 `id`/`identity` 一致性|验收:不再存在"持有 identity 即可加入"的隐式凭证;`id``identity` 不匹配时返回 `ErrInvalidArgument`|涉及:`module/social/group/internal/logic/member/do_join.go:28`, `D:\work\bsm-sdk\core\utils\identity.go:8`
- [ ] **P3-31问题31** 未实现方法不覆写(保留 `Unimplemented` 默认行为)并同步 `protoc-gen-slc` 模板Mux 初始化 + gateway 注册)|验收:未实现 RPC 返回 `codes.Unimplemented`;新生成模块自带 `Mux` 初始化与 handler 注册|涉及:`module/social/group/internal/server/basic_server.go:19`, `internal/server/member_server.go:19`, `internal/server/new.go:20`
**测试用例清单(`test/` 目前只有空 `lint/` 目录0 个测试)**
- [ ] **P2-T1** 权限矩阵表驱动测试13 个 RPC × {群主, 管理员, 普通成员, 非成员, 未登录}|验收:每个组合断言预期错误码(`ErrPermissionDenied`/成功),覆盖 P1-6/7/8/9涉及`module/social/group/internal/logic/member/`
- [ ] **P2-T2** 幂等/去重测试:重复 `DoJoin`、重复 PASS 同一申请、重复 `Create` 同名群|验收:成员表与申请表不产生重复行,计数只加一次|涉及:`module/social/group/internal/logic/member/do_join.go:37`, `join_do_handle.go:43`
- [ ] **P2-T3** 并发计数测试:并行 N 次入群/退群后 `member_total == COUNT(*)`|验收:无丢失更新(配合 P1-11 的事务修复)|涉及:`module/social/group/internal/models/query.go:45`
- [ ] **P2-T4** 契约测试:`DoJoin` 返回值可用于 `JoinDoHandle``Basic.Get`/`Fetch`/`Member.Fetch` 返回真实数据|验收:端到端流程(建群→申请→审核→列表→踢人→退群)断言成功|涉及:`module/social/group/internal/logic/`
- [ ] **P2-T5** 软删除/唯一约束回归:退群或被踢后重新入群|验收:不报唯一键冲突且成员列表无重复|涉及:`module/social/group/internal/logic/member/do_join.go:37`
- [ ] **P3-T6** 配置/网关冒烟:`gofmt -l``go vet ./...`、启动后 `curl` gateway 端口与 gRPC reflection 检查|验收:无 panic 日志、gateway 路由按预期可用或已明确关闭|涉及:`module/social/group/internal/server/new.go:13`
## 6. 审计摘要(供汇总使用)
- **问题数P0=0 P1=13 P2=12 P3=6**(合计 31另有 6 条测试 TODO
- **最高风险(一句话)**13 个 RPC 中 12 个存在确定性缺陷——群列表/群详情/成员列表因表名与列名错误 + 结果被丢弃而恒失败或恒空、`Disband` 是返回 OK 的模板 TODO、踢人/设管理员/退群/审核的权限与 SQL 全错,唯一能落库的 `Create` 还不写群号且无事务;同时 `Modify``UpdateColumns(整个结构体)` 一旦所有权列被修正就会随机重写群 `identity`(并清零 `passport_id`/`member_total`),使全群成员与申请关系断裂。
- **最优先 3 个动作**
1. 修权限与 SQL 的正确性基线:`DoKick`/`DoSetManager`/`DoQuit`/`JoinDoHandle` 的角色校验改为 `group_identity + passport_id` 并检查 `RowsAffected`,同时把 `GetMyGroup`/`GetGroupMember`/`JoinFetch` 的 raw SQL 全部换成带表名与 `IN ?` 的 GORM 查询(`internal/models/query.go``internal/logic/member/*`),让每个接口"要么正确、要么明确报错"。
2.`Basic.Modify` 的破坏性更新与 `Create` 的事务/群号,并把 `member_total` 的计数更新放进同一事务、不再吞错误(`modify.go:33-35``create.go:36,51``query.go:45`)。
3.`service/expose.go`(或关闭 `Gateway.Enable`)并统一 `creator_id``passport_id`,避免"配置声明开启但网关必 panic、代码引用不存在的列"这两类系统性误导(`internal/server/new.go`、三份 etc
- **未能覆盖/无法验证的部分**
1. 真实数据库表结构与索引/唯一约束(仓库内无 DDL、无迁移`IsAutoMigrate=false``creator_id` 列是否存在、`number` 是否唯一、成员表是否有唯一索引均按"分支 A/B"表述,未连接生产库验证;
2. 运行时行为:未真正执行 SQL/启动服务,"SQL 报错、`http: panic serving`"等结论基于 GORM v1.31.2 与 grpc-gateway v2.30.0 源码推演(正文已标注「推测」);
3. 生产发布流程是否外部覆盖 `etc/`、平台网关的实际鉴权/匿名白名单配置(`D:\work\bsm-infra\gateway`)未纳入验证;
4. 前端渲染方式未知,群名/公告/留言的存储型 XSS 仅为风险面提示(未证实);
5. `gofmt -l`(无输出)与 `go vet ./...`exit=0通过但两者均无法发现本报告中任何一类缺陷。