# 审计报告:module/social/feed ## 1. 模块概览 - **路径**:`D:\work\bsm-infra\full\module\social\feed`(Go module `bsm/full/module/social/feed`,`go.mod:1`,Go 1.26.5)。 - **定位**:社交域「用户动态内容」服务。README(`README.md:212-218`)描述其承载动态发布/查询/修改/删除、评论、互动计数、标签,以及 4 个时间线接口;服务接口为 `Post`、`Tag`、`Timeline`、`Setting`。 - **技术形态**:gRPC(`google.golang.org/grpc v1.83.2`)+ grpc-gateway(`grpc-gateway/v2 v2.30.0`)+ GORM(`gorm.io/gorm v1.31.2`)+ PostgreSQL/Redis/etcd(`go.mod:5-13`),依赖 SDK `git.apinb.com/bsm-sdk/core`(`replace` 指向 `../../../../../bsm-sdk/core`,即 `D:\work\bsm-sdk\core`)。 - **规模**:全模块 59 个文件;非生成源码 26 个 Go 文件 + 5 个 `.proto` + 6 个 `etc/*.yaml` + 1 个 supervisor conf。`pb/` 下为生成代码(`const.pb.go` 单文件约 99KB)。 - **入口**:`cmd/main/main.go`(`config.New("Feed")` → `impl.NewImpl()` → `server.New` → `service.New(...).Start()`);`cmd/cli/main.go` 为调试用 CLI。 - **持久化模型 5 张表**:`feed_post`、`feed_comment`、`feed_relate_attach`、`feed_relate_tags`、`feed_tags`(`internal/models/*.go`)。 - **目录状态**:`service/` 与 `test/lint/` 均为**空目录**(其它模块普遍有 `service/expose.go`);无任何 `*_test.go`。 - **实现完成度**:8 个已声明 RPC 完全未实现(仅鉴权+分页校验后裸 `return`,见 P1-13),删除评论为空实现(P1-11),发帖/评论/点赞三条主链路存在确定性失败(P1-5/P1-6/P1-7)。 ## 2. 审计范围与方法 **范围**:`module/social/feed` 下全部非 `pb/` 生成代码(`cmd/`、`etc/`、`internal/{config,impl,logic,models,server}`)、`proto/*.proto`、`README.md`;按需检索 `pb/*.pb.gw.go`(路由 pattern、网关注册函数)与 `pb/post_grpc.pb.go`(Unimplemented 行为)。为验证运行时语义,另行读取 SDK 依赖(`D:\work\bsm-sdk\core`:`service/meta.go`、`service/service.go`、`conf/new.go`、`with/databases.go`、`database/*`、`env/env.go`、`crypto/token/jwt.go`、`types/db.go`、`printer/print.go`)与第三方库源码(`C:\Users\david\go\pkg\mod\gorm.io\gorm@v1.31.2`、`google.golang.org\protobuf@v1.36.12`、`grpc@v1.83.2`),用于确认 GORM 默认值/空切片/缺表模型的行为。 **方法**:全量逐文件阅读(26 个非生成 Go 文件全部读完)+ 关键字检索(`TODO`、`Preload`、`_ =`、`UpdateColumn`、`Migrate`、`SecretKey`、`Register*Handler`、`Expose`)+ 与同域兄弟模块 `module/social/group`、`module/social/relation` 及聚合入口 `pkgs/all` 交叉对照 + 仓库既有审计基线 `wiki/audit-2026-08-10.md`、`docs/audit/README.md`、`docs/audit/module-base-*.md` 对齐。 **证据标准**:每条结论给出 `file:line` + 代码摘录;依赖库行为单独标注来源文件与行号;无法从源码确认的判断显式标注「推测」;「命名返回值未赋值 → 恒返回 nil」一类结论必须见到裸 `return` 才成立(本报告 P1-11、P1-13 均已核对裸 `return`)。 **只读约束遵守情况**:未修改/新建/删除任何 `.go`、`.proto`、`.yaml`、`go.mod` 文件;`git status` 中本模块无改动。唯一写入为本文件。 **静态检查**: - `gofmt -l .` → 无输出(格式合规)。 - `GOFLAGS=-mod=readonly; go vet ./...` → 无输出、退出码 0(类型与编译层面无问题)。 - 结论:本报告全部问题均为**语义/运行时/设计**问题,不是编译或格式问题。 **未做**:未启动服务、未连接真实数据库/Redis/etcd、未做黑盒或压测(约束禁止改动代码,且仓库内无可用基础设施),所有运行期行为结论为源码 + 依赖库源码静态推导,涉及线上表结构与生产环境变量的判断已单独标注「推测」。 ## 3. 问题清单 ### P0 #### 1. 越权删除:任意登录用户可删除任意用户的动态 - **位置**:`module/social/feed/internal/logic/post/remove.go:16-29`、`module/social/feed/internal/models/query.go:34-37` - **证据**: ```go // remove.go:17-25 —— 只校验「是否登录」,claims 被丢弃,无 owner 比对 _, err = service.ParseMetaCtx(ctx, nil) ... err = models.DeletePost(in.Identity) // query.go:34-36 —— WHERE 只有 identity,没有 passport_identity func DeletePost(identity string) error { err := impl.DBService.Where("identity = ?", identity).Delete(&FeedPost{}).Error ``` - **影响**:任何持有合法 token 的普通用户,只要拿到(或从列表接口获得)他人动态的 `identity`,即可删除他人动态;且是软删除(`types.Std_IICUDS.DeletedAt`,`bsm-sdk/core/types/db.go:33`),受害者与运营侧都较难察觉。`Fetch` 会返回全站动态的 `identity`(见 P0-3),攻击成本极低。这是本模块可利用性最高的风险。 - **建议**:`Remove` 必须取 `auth = service.ParseMetaCtx(ctx, nil)`,并在 `DeletePost` 中增加 `passport_identity = ?`(或 `passport_id = ?`)条件 + 影响行数校验(`RowsAffected == 0` 时返回 `NotFound/PermissionDenied`);管理员删除走独立的、显式 `RoleValue` 的管理接口。 #### 2. 越权修改:任意登录用户可覆写任意动态的正文与可见性 - **位置**:`module/social/feed/internal/logic/post/change.go:17-49`、`module/social/feed/internal/models/query.go:30-33` - **证据**: ```go // change.go:18-28 —— claims 丢弃;Content/IsOpen 直接取自请求体 _, err = service.ParseMetaCtx(ctx, nil) ... var postData = models.FeedPost{ Std_IICUDS: types.Std_IICUDS{Identity: in.Identity}, Content: in.Content, IsOpen: in.IsOpen, } // query.go:31 —— 仅按 identity 更新 err := impl.DBService.Where("identity = ?", data.Identity).Updates(&data).Error ``` - **影响**:任何登录用户可把任意动态正文改成任意内容(含违规内容),并可尝试改写可见性;与 P0-1 组合即「任意用户可改写与删除任意动态」。此外 `Updates(&struct)` 会忽略零值字段,`is_open=false` 永远写不进去(详见 P0-3)。 - **建议**:与 P0-1 同源处理:把 owner 条件放进 `WHERE`(`identity = ? AND passport_identity = ?`),并对 `RowsAffected` 做校验;修改可见性建议使用显式 `Select("is_open")` 或 map 更新以规避零值忽略语义。 #### 3. 动态可见性控制完全失效:私密动态写不进去,列表也不做任何可见性过滤 - **位置**:`module/social/feed/internal/logic/post/create.go:29`、`module/social/feed/internal/models/feed_post.go:12`、`module/social/feed/internal/models/query.go:60-68` - **证据**: ```go // create.go:29 —— 直接把 proto 的 bool 传给模型 IsOpen: in.IsOpen, // feed_post.go:12 —— 模型声明了 DB 默认值 true IsOpen bool `gorm:"column:is_open;default:true;" json:"is_open"` // 默认为true-公开,false-不公开 // query.go:61-68 —— 列表查询无 is_open / status / 用户维度条件 tx := impl.DBService.Order("created_at desc") if tag != "" { ... } tx.Limit(int(page)).Offset(int((page - 1) * size)) err = tx.Find(&list).Count(&cnt).Error ``` - **依赖库证据(GORM v1.31.2,确定性行为)**:`schema/field.go:236-239` 会把 `default:true` 解析为 `DefaultValueInterface = true`;`callbacks/create.go:298-301` 在字段为零值(`false`)时用默认值替换写入值并回写结构体: ```go // gorm@v1.31.2/callbacks/create.go:298-301 if values.Values[i][idx], isZero = field.ValueOf(stmt.Context, rv); isZero { if field.DefaultValueInterface != nil { values.Values[i][idx] = field.DefaultValueInterface stmt.AddError(field.Set(stmt.Context, rv, field.DefaultValueInterface)) ``` - **影响**: 1. **写入侧**:请求 `is_open=false`(protobuf bool 默认值即 false,客户端不显式传 `true` 时也会是 false)会被 GORM 静默改写为 `true` 落库 —— **私密动态永远无法创建**,用户以为已设为私密、实际是公开;`Change` 走 `Updates(&struct)` 同样忽略零值,也无法把动态改回私密。当前**所有动态事实上都是公开的**。 2. **读取侧**:`PostList` 完全没有 `is_open` 过滤,任何登录用户调用 `Post.Fetch` 都能读到全站动态(含将来任何渠道写入的 `is_open=false` 记录),并同时获得其 `identity` 作为 P0-1/P0-2 的攻击输入。 - **建议**:模型去掉 `default:true`(或改为 `*bool` + 应用层显式赋值),把可见性语义收敛到服务端常量而不依赖 DB 默认;列表查询显式加可见性过滤(`is_open = true OR passport_identity = 当前用户`),并补 `status` 过滤(见 P2-26);为可见性/所有权规则补集成测试。 ### P1 #### 4. JWT 校验密钥可退化为公开硬编码默认值 → 未注入环境变量时可伪造任意身份令牌 - **位置**:`module/social/feed/internal/config/config.go:56-59`、`module/social/feed/etc/supervisor.bsm-apps-passport.conf:1-8` - **证据**: ```go // config.go:56-59 —— 只校验 Service/Cache,未注入也未校验 JWT 密钥 conf.NotNil(Spec.Service, Spec.Cache) // 初始化加密SecretKey encipher.New(env.Runtime.JwtSecretKey) // bsm-sdk/core/env/env.go:19 —— 未设置 BSM_JwtSecretKey 时回退到源码公开常量 JwtSecretKey: GetEnvDefault("BSM_JwtSecretKey", "Cblocksmesh2022C"), // bsm-sdk/core/service/meta.go:26-34 —— 模块唯一的鉴权实现就用这个密钥验签 var Authorizations []string = md.Get("authorization") claims, err := token.New(env.Runtime.JwtSecretKey).ParseJwt(Authorizations[0]) ``` `etc/supervisor.bsm-apps-passport.conf` 全文无 `environment=` 行,即容器/supervisor 启动时不注入 `BSM_JwtSecretKey`;`config.Spec.SecretKey` 在本模块零引用(仅 `config.go` 结构体定义与 `etc/passport_*.yaml:43` 的赋值)。 - **影响(条件性)**:若按仓库内 supervisor 独立部署且环境未注入该变量,JWT 的 HMAC 密钥就是仓库与 SDK 源码中公开可读的 `Cblocksmesh2022C`,攻击者可离线自签任意 `identity/id` 的 token,绕过本模块**全部**接口鉴权;进一步可伪造 `passport_identity` 发帖/评论(嫁祸他人)并配合 P0-1/P0-2 删除、改写全站动态。本模块没有 `pkgs/all` 那层保护(`pkgs/all/internal/config/config.go:73` 会显式覆盖密钥),且 README(`README.md:194`)明确 social 三模块尚未接入聚合入口,因此独立部署路径是现实路径。附带(SDK 级):`crypto/token/jwt.go` 的 `ParseJwt` 未限制签名算法(无 `jwt.WithValidMethods`),存在算法混淆风险面。 - **建议**:在 `config.New` 中显式要求并校验密钥(非空、长度 16/24/32、非默认值,否则 `panic` 拒绝启动,可对照 `pkgs/all/internal/config/config.go:63-73`);supervisor 配置补 `environment=BSM_RuntimeMode="prod",BSM_Workspace="...",BSM_JwtSecretKey="..."`;SDK 侧补签名算法白名单(跨模块事项)。 - **定为 P1 而非 P0 的理由**:密钥被实际使用的**前提**(生产未注入环境变量)无法从仓库确认,属推测;同一机制在 `docs/audit/module-base-fts.md:180-195` 被定为 P1-6。若部署确认未注入密钥,本条应按 P0 处理(其时影响等同于 P0-1/P0-2 且不需要任何账号)。 #### 5. 发帖主链路确定性失败:`CreatePost` 对可空切片调用 `Create`,GORM 返回 `ErrEmptySlice` 并回滚整个事务 - **位置**:`module/social/feed/internal/models/query.go:14-29` - **证据**: ```go // query.go:15-24 —— 两个 Create 无条件执行,未判空 err := impl.DBService.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&data).Error; err != nil { return err } if err := tx.Create(&data.Attachs).Error; err != nil { return err } if err := tx.Create(&data.Tags).Error; err != nil { return err } // logic/post/create.go:32,42 —— 说明 Attachs/Tags 本来就可以为空 if len(in.GetAttachs()) > 0 { ... } if len(in.GetTags()) > 0 { ... } ``` - **依赖库证据(GORM v1.31.2)**:`callbacks/create.go:275-281` 对空切片直接返回 `gorm.ErrEmptySlice`(`errors.go:36-37`): ```go case reflect.Slice, reflect.Array: rValLen := stmt.ReflectValue.Len() if rValLen == 0 { stmt.AddError(gorm.ErrEmptySlice) ``` - **影响**:**纯文本动态(无附件)必然发帖失败**;有附件但无标签的动态同样失败(第二步或第三步返回 `empty slice found`),事务回滚,`logic/post/create.go:51-55` 统一返回 `errcode.ErrDB`。即核心功能「发动态」在绝大多数正常输入下不可用,且错误只有一个笼统的 DB Fatal Error,排障困难。 - **建议**:`if len(data.Attachs) > 0 { ... }` / `if len(data.Tags) > 0 { ... }` 分别包裹;并补「无附件纯文本发帖成功」的集成测试。 #### 6. 回复评论必然失败:事务内父评论计数更新缺少 Model/Table,导致整个评论事务回滚 - **位置**:`module/social/feed/internal/models/query.go:103-120`(重点是 `:112`) - **证据**: ```go // query.go:106 —— 这一句显式指定了表 err := tx.Create(comment).Table("feed_post").Where("identity = ?", comment.PostIdentity).UpdateColumn("cnt_comment", gorm.Expr("cnt_comment + ?", 1)).Error // query.go:112 —— 这一句既无 Model 也无 Table(同一个 tx 上) err = tx.Where("identity = ?", comment.ParentIdentity).UpdateColumn("cnt_comment", gorm.Expr("cnt_comment + ?", 1)).Error ``` - **依赖库证据(GORM v1.31.2)**:更新语句若既无 `Model` 也无 `Table`,`callbacks.go:110-117` 会写入错误 `"...Table not set, please set it like: db.Model(&user) or db.Table(\"users\")"`: ```go if err := stmt.Parse(stmt.Model); err != nil && (!errors.Is(err, schema.ErrUnsupportedDataType) || (stmt.Table == "" && stmt.TableExpr == nil && stmt.SQL.Len() == 0)) { ``` - **影响**:只要带 `parent_identity`(回复某条评论),第 112 行即报错 → 事务回滚 → **回复评论 100% 失败且一条评论都写不进去**(连第 106 行已执行的评论插入也被回滚);父评论 `cnt_comment` 永远不增长。`logic/post/add_comment.go:31-34` 把该错误统一转成 `errcode.ErrDB`。 - **建议**:改为 `tx.Model(&models.FeedComment{}).Where("identity = ?", comment.ParentIdentity).UpdateColumn(...)`;并补「回复评论成功 + 两级计数各 +1」的测试。 - **附带说明**:第 106 行 `tx.Create(comment).Table("feed_post")...` 这种「Create 链式接 UpdateColumn」的写法依赖 GORM「执行后重置 `Statement.SQL`」(`callbacks.go:149-152`)才恰好可行,属高危写法,建议拆成两条独立语句。 #### 7. 计数接口:点踩永久失败(判断写错列),点赞无用户维度/幂等/上限,可无限刷 - **位置**:`module/social/feed/internal/models/query.go:40-57`、`module/social/feed/internal/logic/post/action.go:17-33` - **证据**: ```go // query.go:41-53 —— 第二个分支误用 action_type 判断(应为 action_op),column 保持空串 var column string tx := impl.DBService if action_type == "post" { tx = tx.Model(&FeedPost{}) } else if action_type == "comment" { tx = tx.Model(&FeedComment{}) } if action_op == "ilike" { column = "cnt_like" } else if action_type == "unlike" { // ← 应为 action_op == "unlike" column = "cnt_unlike" } if err = tx.Where("identity = ?", identity).UpdateColumn(column, gorm.Expr(fmt.Sprintf("%s + ?", column), 1)).Error; err != nil { // action.go:22-28 —— 只校验非空,无频率/次数限制 if in.GetActionOp() == "" || in.GetActionType() == "" || in.GetIdentity() == "" { return nil, errcode.ErrInvalidArgument } if err := models.LikeAction(in.ActionOp, in.ActionType, in.Identity); err != nil { ``` - **影响**: 1. `action_op="unlike"` 时 `column==""`,生成非法 SQL(`SET = + 1` 形态)→ 点踩/取消点赞功能不可用(返回 DB 错误)。 2. 点赞没有任何「谁赞过」的记录表、没有幂等键、没有频率限制:同一用户重复调用 `Post.Action{ActionOp:"ilike"}` 即可把 `cnt_like` 刷到任意大。计数被用于展示与(未来的)热门排序,属可被利用的数据污染/排序操纵,而且**无法取消点赞**(`unlike` 分支不可用,计数只增不减)。 3. `action_type` 非 `post/comment` 时 `tx` 退化为无 Model 的 `impl.DBService`,同样命中「Table not set」错误。 - **建议**:引入 `feed_action`(user×target 唯一索引)记录并以其驱动计数(或至少「先插入动作记录成功再 `+1`」的幂等事务);修正 `unlike` 判断;对 `action_op/action_type` 做白名单枚举校验;对点赞/评论/发帖加限流(见 P2-18)。 #### 8. `feed_relate_tags.identity` 从未赋值,标签关联写入存在唯一键冲突(推测) - **位置**:`module/social/feed/internal/logic/post/create.go:42-50`、`module/social/feed/internal/models/feed_relate_tags.go:6-9` - **证据**: ```go // create.go:44-48 —— 只设置 PostIdentity/Content/Key,未设置 Identity postData.Tags = append(postData.Tags, models.FeedRelateTags{ PostIdentity: postData.Std_IICUDS.Identity, Content: val.Content, Key: val.Key, }) // feed_relate_tags.go:6-8 —— 内嵌 Std_IDIdentity,Identity 是 uniqueIndex(varchar(36)) type FeedRelateTags struct { types.Std_IDIdentity // bsm-sdk/core/types/db.go:24 Identity string `gorm:"column:identity;type:varchar(36);uniqueIndex;" json:"identity"` ``` - **影响**:每行 `identity` 都写空串。若线上表按模型声明建了唯一索引,第二条标签行(同一动态多标签,或第二条带标签的动态)即触发唯一键冲突 → 事务回滚 → 发帖失败;`feed_relate_tags` 实际只能存一行。对照同一路径的附件行显式赋值 `Identity: utils.UUID()`(`create.go:36`),说明这是遗漏而非设计。 - **建议**:补 `Identity: utils.UUID()`;同时确认线上 DDL 中该唯一索引是否存在并对历史空值数据做清理。 - **推测部分**:仓库内**没有任何建表 DDL / 迁移**(见 P2-27),无法验证线上是否真的存在该唯一索引;若线上无索引则本条降级为数据质量问题(所有标签行 `identity` 为空,无法按行定位/删除)。 #### 9. 分页 `LIMIT` 用错变量:每页返回条数等于页码 - **位置**:`module/social/feed/internal/models/query.go:67` - **证据**: ```go // query.go:67 —— Limit 应为 int(size),写成了 int(page) tx.Limit(int(page)).Offset(int((page - 1) * size)) // logic/post/fetch.go:19-24 —— 上游默认值 page_no<=0→1、page_size<=0→10 if in.GetPageNo() <= 0 { in.PageNo = 1 } if in.GetPageSize() <= 0 { in.PageSize = 10 } ``` - **影响**:`page=1,size=10` → `LIMIT 1 OFFSET 0`,只返回 1 条;`page=2` → 返回 2 条且 `OFFSET 10`。分页语义完全错乱:首页只看到 1 条动态、翻页出现空档与数量漂移。配合 P1-12(附件/标签恒空),列表接口对外基本不可用。 - **建议**:改为 `Limit(int(size)).Offset(int((page - 1) * size))`,补 `size` 上限(如 ≤100);加分页边界单测(首页/末页/越界页/相同 `created_at` 的翻页稳定性,后者建议改用游标分页)。 #### 10. `Tag.List` 丢弃查询结果,永远返回空标签列表 - **位置**:`module/social/feed/internal/logic/tag/list.go:20-32` - **证据**: ```go // list.go:25-32 —— 组装了 res,最后却返回新建的空对象 var res = &pb.TagListReply{Total: cnt} for _, v := range tags { res.Tags = append(res.Tags, &pb.TagItem{Key: v.Key, Content: v.Content}) } return &pb.TagListReply{}, nil ``` - **影响**:`Tag.List` 恒返回 `total=0`、`tags=[]`。客户端据此认为「系统没有任何标签」,话题入口/筛选功能整体失效;`tags` 与 `err` 的处理成为死代码,掩盖了 `TagsList` 的真实错误表现。 - **建议**:`return res, nil`;补「有标签时返回条数与内容」的测试。 - **附带**:`models.TagsList`(`query.go:96-101`)无分页、无过滤,全表返回 `feed_tags`,见 P2-19。 #### 11. `DeleteComment` 是空实现却返回成功:评论删不掉,且只传 `id` 也返回成功 - **位置**:`module/social/feed/internal/models/query.go:122-125`、`module/social/feed/internal/logic/post/delete_comment.go:16-33` - **证据**: ```go // query.go:122-125 —— 第 124 行是裸 return,命名返回值 err 从未被赋值,恒为 nil func DeleteComment(identity string) (err error) { return } // delete_comment.go:21-24 —— 校验允许「只传 id」,但实现只用了 identity if in.GetId() == 0 && in.GetIdentity() == "" { return nil, errcode.ErrInvalidArgument } err = models.DeleteComment(in.Identity) ``` - **影响**:接口对外一律返回 `Data:"OK"`,实际数据零变化。用户以为评论已删除,内容仍在;若客户端按 proto 语义只传 `int64 id`(`proto/const.proto:16-19` 明确提供该字段),依然返回成功——静默失败最难被发现。同时该接口没有任何属主/帖子作者/管理员判定,一旦将来补上实现,会把越权删除评论一并引入。 - **建议**:实现时按 `identity` 软删除 + 属主/动态作者/管理员校验 + `RowsAffected` 校验 + 子评论与计数回退;`id` 与 `identity` 二选一的入参约束要在文档与校验中统一(当前校验与实现不一致)。 #### 12. 动态列表永远不返回附件与标签(`gorm:"-"` + 全模块无 `Preload`) - **位置**:`module/social/feed/internal/models/feed_post.go:16-17`、`module/social/feed/internal/logic/post/fetch.go:39-66` - **证据**: ```go // feed_post.go:16-17 —— 两个关联字段被 GORM 完全忽略 Attachs []FeedRelateAttach `gorm:"-"` Tags []FeedRelateTags `gorm:"-"` // fetch.go:51-63 —— 组装逻辑存在,但循环体永远不执行(切片恒空) for _, tag := range val.Tags { post.Tags = append(post.Tags, &pb.TagItem{...}) } for _, attachs := range val.Attachs { post.Attachs = append(post.Attachs, &pb.AttachItem{...}) } ``` 检索全模块:没有任何 `Preload` 调用;`Attachs`/`Tags` 仅在写入路径(`create.go:34/44`)被赋值。 - **影响**:`Post.Fetch` 返回的 `attachs`、`tags` 恒为空数组 —— **发帖时上传的图片/附件、关联的话题标签永远读不出来**(写进去、读不出)。这与 P1-5(必须有附件+标签才能发帖)叠加后体验最差:用户被迫传附件,却看不到任何图片。即便将来加载了关联行,`FeedRelateTags.Content` 是 `gorm:"-"`(`feed_relate_tags.go:10`)且从未赋值,`tags[].content` 依然是空串。 - **建议**:给关联字段加真实关联标签并用 `Preload` 加载(或一次性批量查询 `post_identity IN (...)` 组装,避免 N+1);`Tags[].content` 通过 `feed_tags` 关联补齐;补「发布带图带话题 → 列表返回附件与标签」的测试。 #### 13. 8 个 RPC 未实现,却对外返回「成功 + 空响应」而非 `Unimplemented` - **位置**:`internal/logic/timeline/{recommend,friend,follow,hot}.go:11-28`、`internal/logic/tag/post_list.go:11-28`、`internal/logic/post/comment_list.go:11-28`、`internal/logic/setting/{info,rights}.go:11-22` - **证据**(逐条列出方法名、行号与实际行为): | RPC 方法 | 文件:行 | 实际行为 | | --- | --- | --- | | `Timeline.Recommend` | `internal/logic/timeline/recommend.go:11`,TODO `:26`,裸 `return` `:28` | 鉴权 + 分页校验后直接返回 typed-nil `*pb.FeedPostListReply` | | `Timeline.Friend` | `internal/logic/timeline/friend.go:11`,TODO `:26`,裸 `return` `:28` | 同上 | | `Timeline.Follow` | `internal/logic/timeline/follow.go:11`,TODO `:26`,裸 `return` `:28` | 同上 | | `Timeline.Hot` | `internal/logic/timeline/hot.go:11`,TODO `:26`,裸 `return` `:28` | 同上 | | `Tag.PostList` | `internal/logic/tag/post_list.go:11`,TODO `:26`,裸 `return` `:28` | 同上 | | `Post.CommentList` | `internal/logic/post/comment_list.go:11`,TODO `:26`,裸 `return` `:28` | 同上 | | `Setting.Info` | `internal/logic/setting/info.go:11`,TODO `:18/:20`,裸 `return` `:22` | typed-nil `*pb.Empty`,不做任何修改 | | `Setting.Rights` | `internal/logic/setting/rights.go:11`,TODO `:18/:20`,裸 `return` `:22` | 同上 | ```go // timeline/recommend.go:26-28(其余 5 个同构) // TODO: add your logic code & delete this line. return ``` - **影响(对外暴露后的行为)**:这些方法**被显式实现**(`internal/server/*.go` 全部转调 logic),因此内嵌的 `pb.Unimplemented*Server`(如 `pb/post_grpc.pb.go:190-194` 返回 `codes.Unimplemented`)永远不会被命中,客户端**拿不到 `Unimplemented`**。Go 语义下裸 `return` 返回 typed-nil 指针 + `nil` error;protobuf 对 nil/无效消息编码为空字节(`google.golang.org/protobuf@v1.36.12/proto/encode.go:120-146`),gRPC 以 `code=OK` 返回字段全零的响应。后果:① 客户端无法区分「未实现」与「暂无数据」,`Timeline.*` 被当成「没有动态」、`CommentList` 被当成「没有评论」;② `Setting.Info/Rights` 被当成「修改成功」(实际未改任何东西),是最典型的静默失败;③ 监控无法通过错误率发现这些接口未实现。README(`README.md:217`)已正确记载它们是占位,但代码层没有任何防护。 - **建议**:未实现方法统一 `return nil, status.Error(codes.Unimplemented, "not implemented")`,并在 proto/网关层决定是否暂时下线路由;把「未实现即 Unimplemented」写入团队规范(`wiki/audit-2026-08-10.md:35` 已有同样要求)。 #### 14. `etc` 配置与配置结构不匹配:按仓库内配置启动会直接失败 - **位置**:`module/social/feed/etc/feed_prod.yaml:1-8`(dev/test 同构)、`module/social/feed/internal/config/config.go:46-62` - **证据**: ```yaml # etc/feed_prod.yaml:1-4 —— 旧版布局:Name/ListenOn/Dsn;没有 Service/Databases/Cache Name: {ServiceKey} ListenOn: 0.0.0.0:12212 Dsn: postgres://prod:...@192.168.0.224:5432/scf?sslmode=disable&TimeZone=Asia/Shanghai ``` ```go // config.go:56 —— 要求 Service/Cache 非空;impl.go:23 用 Databases 建连 conf.NotNil(Spec.Service, Spec.Cache) DBService = with.Databases(config.Spec.Databases, nil) // model ``` - **依赖库证据**:`conf.New` 以 `etc/{ServiceKey小写}_{mode}.yaml` 为第一候选(`bsm-sdk/core/conf/new.go:35-36`),文件存在即不再回退 workspace 公共配置;随后 `new.go:58-60` 要求内容含 `Service:` 否则 `log.Fatalln`;`with.Databases(nil, nil)` 直接 `panic("No Database Source Found !")`(`bsm-sdk/core/with/databases.go:13-15`)。 - **影响**:`cmd/main` 以 `config.New("Feed")` 启动时读取 `feed_{dev|test|prod}.yaml`,其中没有 `Service:` → 进程在配置阶段 `Fatal`;即便绕过这一步,`Databases` 为空 → `panic`。**本模块按仓库内提供的配置无法启动**。真正符合结构(`Service: feed` + `Databases` + `Gateway` + `Token`)的三个文件被命名为 `etc/passport_*.yaml`,按服务键 `Feed` 永远不会被加载;对照同域兄弟模块 `module/social/group/etc/group_dev.yaml:1-10` 才是现行约定。此外 `{ServiceKey}`/`{Workspace}` 占位符不会被执行(`conf.New` 只做 `os.ExpandEnv`,不识别 `{}`),会以字面量留在配置里。 - **建议**:把 `passport_*.yaml` 的内容改为 `feed_{dev,test,prod}.yaml`(并删除误导性文件名),补齐 `Service/Port/Databases/Cache/Gateway`;`config.New` 增加 `NotNil(Spec.Databases, Spec.Gateway)` 等必填校验,让缺失在启动期以明确信息失败;在 `README.md`/部署文档中写明配置来源与必需环境变量。 #### 15. `etc` 中留存明文数据库口令与公网地址(与既有审计结论不一致) - **位置**:`module/social/feed/etc/feed_prod.yaml:4`、`module/social/feed/etc/feed_dev.yaml:4`、`module/social/feed/etc/feed_test.yaml:4`、`module/social/feed/etc/passport_prod.yaml:7` - **证据**: ```yaml # etc/feed_prod.yaml:4 —— 内网生产库 + 疑似真实口令,明文入库 Dsn: postgres://prod:MakeW2023~PROD@192.168.0.224:5432/scf?sslmode=disable&TimeZone=Asia/Shanghai # etc/feed_dev.yaml:4 / feed_test.yaml:4 —— 公网 IP 上的数据库 Dsn: postgres://postgres:CHANGE_ME@47.108.57.74:5432/milu?sslmode=disable&TimeZone=Asia/Shanghai ``` - **影响**:仓库即凭据,任何有读权限的人(含 CI、镜像、外包)都可拿到生产库账号与内网拓扑;`sslmode=disable` 使 DB 链路明文。`wiki/audit-2026-08-10.md:12` 声称「样例含公网地址、固定 secret … 改为 localhost 或 CHANGE_ME」已修复,本模块明显未覆盖到(属回归/漏改)。 - **建议**:立刻轮换该口令并从历史提交中清除;配置改为环境变量注入(`os.ExpandEnv` 已支持 `${VAR}`),仓库只保留 `${DB_DSN}` 形态;生产 `sslmode=require` 及以上;CI 增加凭据扫描(既有审计路线第 5 条已提及)。 #### 16. 管理类接口缺角色校验:任意登录用户可写全局标签库 - **位置**:`module/social/feed/internal/logic/tag/create.go:15-37` - **证据**: ```go // tag/create.go:16-19 —— opts 传 nil,未要求角色 _, err = service.ParseMetaCtx(ctx, nil) if err != nil { return nil, err } if len(in.GetTags()) == 0 { return nil, errcode.ErrInvalidArgument } // bsm-sdk/core/service/meta.go:36-39 —— SDK 本已提供角色校验能力,本模块从未使用 if opts != nil { if !checkRole(claims, "role", opts.RoleValue) { ``` - **影响**:`Tag.Create` 写的是全站共享的 `feed_tags`,任何普通用户可以批量创建任意标签(无长度/数量/去重限制),污染全体用户可见的话题列表(`Tag.List` 修好后即可见),且当前 `Tag.List` 无分页(P2-19),可被用来灌入海量数据。同类风险适用于将来的 `Setting.Rights`(权限设置类接口)。 - **建议**:`Tag.Create` 传 `&service.ParseOptions{RoleValue: "admin"}`(或按业务定义独立管理接口/内部 IP 限制 `MustPrivateAllow`);增加批量条数上限、标签名长度与查重。 ### P2 #### 17. 评论作者信息未落库:评论无法归属,proto 的 `feed_identity` 被忽略 - **位置**:`module/social/feed/internal/logic/post/add_comment.go:18-31`、`module/social/feed/internal/models/query.go:103`、`module/social/feed/internal/models/feed_comment.go:6-16` - **证据**: ```go // add_comment.go:23-27 —— 注释掉了作者字段:只填 Identity/PostIdentity/Content,未填 Std_Passport var data = &models.FeedComment{ Std_IICUDS: types.Std_IICUDS{Identity: utils.UUID()}, PostIdentity: in.PostIdentity, Content: in.Content, } // query.go:103 —— authorIdentity 参数在函数体内从未使用(签名与行为不一致) func AddComment(comment *FeedComment, authorIdentity string) (err error) { // proto/post.proto:39 —— 请求里带了评论人标识,服务端直接忽略 string feed_identity=3; // 评论人唯一标识 ``` - **影响**:`feed_comment.passport_id/passport_identity` 永远为 0/空 → 无法展示评论作者、无法实现「仅作者可删评论」(P1-11 的校验前提不存在)、无法按用户做审计与风控;忽略客户端传入的 `feed_identity` 是正确做法,但替代它的 token 身份也没有落库,等于作者信息彻底丢失。评论列表实现时必须重新设计作者回填。 - **建议**:写入 `PassportID: auth.ID, PassportIdentity: auth.Identity`;删除误导性的 `authorIdentity` 参数;评论列表按批查询 passport 信息回填作者(避免 N+1)。 #### 18. 全模块无缓存、无限流、无防刷:Redis/内存缓存初始化后零引用 - **位置**:`module/social/feed/internal/impl/impl.go:12-24`、`module/social/feed/internal/server/new.go:20-33` - **证据**: ```go // impl.go:13-22 —— RedisService/MemorySerice 建好之后全模块再无任何引用 RedisService *redis.RedisClient MemorySerice *cache.Cache ... MemorySerice = with.Memory(nil) RedisService = with.RedisCache(config.Spec.Cache) // redis cache // server/new.go:23 —— 无任何 unary interceptor(无鉴权限流、无 recover、无超时) Grpc: grpc.NewServer(), ``` 检索 `RedisService|MemorySerice` 的命中仅在 `impl.go` 自身(`EtcdService` 只用于 etcd 注册,`grpcConns` 未使用,见 P3-32)。 - **影响**:① 发帖/评论/点赞/列表全部直打数据库,无任何缓存(热帖、标签列表每次全查,见 P2-19);② `cnt_like`/`cnt_comment` 是单行热点,高并发下同行锁竞争明显(配合 P1-7 的无限刷可被放大);③ 没有任何频控/防刷,配合 P1-7 可低成本刷计数,并可灌水发帖/评论。 - **建议**:点赞/计数先写 Redis 再异步落库或批量合并;列表/标签加短 TTL 缓存并定义失效策略;对 `Create/AddComment/Action` 增加按用户+接口的令牌桶限流(可放 gRPC 拦截器或网关),并对单用户单位时间动作次数设上限。 #### 19. 列表查询无可用索引、OFFSET 深分页、`page_size` 无上限、`Find`+`Count` 双查询 - **位置**:`module/social/feed/internal/models/query.go:60-71,96-101`、`module/social/feed/internal/logic/post/fetch.go:19-26` - **证据**: ```go // query.go:61,67-69 —— 固定按 created_at 排序,OFFSET 分页,每次都额外 Count 并打印 tx := impl.DBService.Order("created_at desc") tx.Limit(int(page)).Offset(int((page - 1) * size)) err = tx.Find(&list).Count(&cnt).Error fmt.Print("posts", list) // fetch.go:22-24 —— 只设下界,page_size 可以传 1e9 if in.GetPageSize() <= 0 { in.PageSize = 10 } ``` - **影响**:`feed_post` 模型只在 `identity`(唯一索引)与 `passport_id/passport_identity` 上有索引(`feed_post.go:8-18` 内嵌 `types.Std_IICUDS`/`Std_Passport`,`bsm-sdk/core/types/db.go:28-35,52-55`),`created_at`、`is_open`、`status` 均无索引 → 列表需全表扫描 + 排序;深分页 `OFFSET` 随页码线性劣化;`page_size` 无上限时单请求可拉取极大结果集(叠加 `fmt.Print` 打印开销);`Find(...).Count(...)` 每条请求两条 SQL(`TagsList` 同样)。相同 `created_at` 的记录在 OFFSET 翻页下还可能重复/漏出。 - **建议**:为 `feed_post(created_at desc)`(或 `(is_open, created_at desc)`)建索引;`page_size` 设上限(如 50);`Count` 与查询合并或改为「先 count 再查」;深分页改游标(`created_at,id`);`TagsList` 加分页与上限。 #### 20. 敏感与全量内容日志:打印整页动态内容 + GORM 生产 Debug 打印全部 SQL 及参数 - **位置**:`module/social/feed/internal/models/query.go:69`、`module/social/feed/internal/impl/impl.go:23` - **证据**: ```go // query.go:69 —— 每次列表请求把整页动态(含 content)打到 stdout fmt.Print("posts", list) // impl.go:23 —— opts 传 nil DBService = with.Databases(config.Spec.Databases, nil) // model ``` - **依赖库证据**:`with.Databases(cfg, nil)` → `database.NewDatabase(driver, dsn, nil)` → `dbsql.SetOptions(nil)` 构造的默认 `SqlOptions{Debug: true}`(`bsm-sdk/core/database/sql/postgresql.go:11-24`),随后 `gormDb.Debug()`(同文件 `:46-48`)**无条件开启** SQL 日志;`printer` 是 `log.New(os.Stdout, ...)` 的裸 logger(`bsm-sdk/core/printer/print.go:11-17`),无字段、无级别、无 trace/request-id、带 ANSI 颜色。 - **影响**:生产日志中持续出现全量动态正文(可能含用户隐私/违规内容)与带参数的 SQL(含 `identity`、`content`、`passport_identity`),既造成日志膨胀,也构成数据泄漏面;缺少结构化字段与请求 ID,问题定位只能靠时间戳。 - **建议**:删除 `fmt.Print`,改用带 level/field 的结构化日志;显式传 `SqlOptions{Debug: false}`(或按环境区分);日志中避免打印正文,改为打印 `identity`/长度;透传 trace-id。 #### 21. 软删除与计数一致性缺失:删除动态不清理关联数据,计数仍可更新且无回退 - **位置**:`module/social/feed/internal/models/query.go:34-37,40-57,122-125` - **证据**: ```go // query.go:35 —— 软删除(gorm.DeletedAt),但只动 feed_post 一行 err := impl.DBService.Where("identity = ?", identity).Delete(&FeedPost{}).Error // query.go:53 —— 点赞/评论计数按 identity 直接更新,未加 deleted_at 条件 if err = tx.Where("identity = ?", identity).UpdateColumn(column, gorm.Expr(fmt.Sprintf("%s + ?", column), 1)).Error; err != nil { ``` - **影响**:动态被软删除后,`feed_relate_attach`、`feed_relate_tags`、`feed_comment` 行全部保留(无级联、无清理任务),已删除动态仍可被点赞/评论(计数行继续变化),计数也没有「评论删除后 -1」的回退路径(`DeleteComment` 还是空实现)。数据持续膨胀,且「已删除内容仍有互动」的一致性无法自证。 - **建议**:删除动态时在事务内软删除评论与关联行(并记录审计);计数更新统一加 `deleted_at IS NULL` 条件;提供对账/回填任务(`cnt_comment` 与 `feed_comment` 实际条数比对)。 #### 22. 幂等性与唯一约束缺失:重复提交即重复数据,标签无去重 - **位置**:`internal/logic/post/create.go:25-30`、`internal/logic/post/add_comment.go:23-31`、`internal/logic/tag/create.go:23-33`、`internal/models/feed_tags.go:8-12` - **证据**: ```go // create.go:26 —— 每次请求生成新 identity,无幂等键(无 request-id/客户端去重) Std_IICUDS: types.Std_IICUDS{Identity: utils.UUID()}, // tag/create.go:25-28 —— 直接批量插入,无查重、无唯一约束 *tags = append(*tags, models.FeedTags{ Key: utils.UUID(), Content: val }) // feed_tags.go:10-11 —— key/content 均无 uniqueIndex Key string `gorm:"column:key;type:varchar(36);not null" json:"key"` Content string `gorm:"column:content;type:varchar(255);not null" json:"content"` ``` - **影响**:客户端超时重试/双击会创建多条重复动态与重复评论(评论无去重);`Tag.Create` 每次生成新 UUID,同一名称标签可无限重复且无唯一约束,`Tag.List` 会出现大量同名标签。全模块没有任何幂等键机制。 - **建议**:`Post.Create`/`AddComment` 接受客户端幂等键(或复用 gRPC metadata 的 request-id)并加唯一索引;`feed_tags.content` 加唯一索引 + 冲突忽略/先查后插;发布「同一用户 N 秒内相同内容」频控。 #### 23. 内容与附件 URL 无任何校验:长度/富文本/协议白名单全部缺失 - **位置**:`internal/logic/post/create.go:22-40`、`internal/models/feed_post.go:11`、`internal/models/feed_relate_attachs.go:11` - **证据**: ```go // create.go:22-24 —— 唯一的校验是「非空」 if in.GetContent() == "" { return nil, errcode.ErrInvalidArgument } // create.go:36-38 —— 客户端传入的 URL 与类型原样入库,无协议/域名校验 AttachType: val.AttachType, Url: val.Url, // feed_post.go:11 —— content 为 text 类型,无长度限制 Content string `gorm:"column:content;type:text;default:'';" json:"content"` ``` - **影响**:正文可提交任意长度(gRPC 默认 4MB 上限,单行可写入 MB 级文本)与任意标记(若未来渲染 HTML 则为**存储型 XSS**:本服务不转义、不净化,proto 也无长度约束);`url` 字段可写入 `javascript:`/`data:` 等协议或任意外链(若前端直接渲染为链接/图片,则为外链注入与钓鱼跳转面;`url` 仅 `varchar(255)`,也无格式校验)。 - **建议**:服务端做长度上限(正文/附件数/标签数)+ 富文本白名单净化(或明确只接受纯文本并在文档中声明)+ 附件 URL 协议与域名白名单(对齐 OSS/CDN 域名)+ 图片真实性与尺寸校验;在 proto 中补充字段约束或接入校验中间件。 #### 24. 健壮性与可观测性:无拦截器(无 recover/超时/指标)、reflection 无条件开启、无健康检查、退出与网关无超时 - **位置**:`internal/server/new.go:20-35`、`cmd/main/main.go:19-45` - **证据**: ```go // new.go:23,33 —— 裸 grpc.Server + 无条件 reflection Grpc: grpc.NewServer(), reflection.Register(srv.Grpc) // bsm-sdk/core/service/service.go:142-144 —— 优雅退出无超时 func (s *Service) Stop() { s.GrpcSrv.GracefulStop() } // bsm-sdk/core/service/service.go:126 —— 网关无 ReadTimeout/WriteTimeout if err := http.ListenAndServe(httpAddr, s.Opts.GatewayMux); err != nil { ``` - **影响**:① `grpc.NewServer()` 未挂任何 `UnaryInterceptor`:任何 panic(如 `impl.DBService` 未初始化时的 nil 解引用、未来逻辑中的切片越界)无人 recover,会直接终止进程;也没有统一超时(慢 SQL 会把请求挂满)与指标/日志拦截器。② `reflection.Register` 无条件开启,向任何能访问 gRPC 端口的方暴露完整服务与消息定义(`wiki/audit-2026-08-10.md:29-31` 已列为 P1,本模块未做开关)。③ 没有 gRPC health service / 就绪探针,容器编排无法判断可用性。④ `GracefulStop()` 无超时,长连接未关闭时进程可能永远不退出(`defer srv.Stop()` 在 `main.go:41`);HTTP 网关无读写超时。⑤ 全模块日志无 trace/request-id(见 P2-20)。 - **建议**:加 unary interceptor 链(recover → 超时 → 限流 → 日志/指标)、`reflection` 改为配置开关且生产默认关闭、注册 `grpc_health_v1`、`GracefulStop` 包一层带超时的强制 `Stop`、网关加 `ReadHeaderTimeout/ReadTimeout/WriteTimeout`。 #### 25. 配置校验不足与不安全默认值:随机端口/本机 IP、必填项只校验两项、`Databases/Etcd` 为空即 panic、4 个配置块为死配置 - **位置**:`internal/config/config.go:15-62` - **证据**: ```go // config.go:51-56 —— 端口/IP 为空则随机分配/取本机 IP;只校验 Service/Cache Spec.Port = conf.CheckPort(Spec.Port) Spec.BindIP = conf.CheckIP(Spec.BindIP) conf.NotNil(Spec.Service, Spec.Cache) // config.go:19-25 —— WeChat/Kyc/Token/Rpc 定义后模块内零引用 Rpc map[string]conf.RpcConf `yaml:"Rpc"` WeChat *WeChatConf `yaml:"WeChatConf"` Kyc *KycConf `yaml:"Kyc"` ``` - **影响**:`Port` 缺失时随机端口(`conf.CheckPort`,`bsm-sdk/core/conf/new.go:90-97`)会让实例注册地址漂移、网关/etcd 路由不稳定;`BindIP` 缺失时绑定本机探测 IP,多网卡机器上可能绑到错误网卡;`NotNil` 只覆盖 `Service/Cache`,`Databases` 为空会在 `with.Databases` 里 panic(`with/databases.go:13-15`);`WeChatConf/Kyc/KycConf/Token/Rpc` 五个结构体字段在本模块无任何读取处(与动态业务无关,明显是模板拷贝),其中 `Kyc.ApiSecret/ApiToken` 属敏感命名却无人使用,容易误判「已配置生效」。 - **建议**:必填项校验扩展到 `Databases`(Driver/Source 非空)、`Gateway.Port`、etcd endpoints;缺失或为 `CHANGE_ME` 时 fail-fast 并输出明确字段名;禁止生产环境随机端口;删除与业务无关的 `WeChat/Kyc/KycConf` 配置。 #### 26. 模型 `Status` 字段从未使用:无法封禁/下架动态与评论 - **位置**:`internal/models/feed_post.go:8-18`、`internal/models/query.go:60-68` - **证据**: ```go // bsm-sdk/core/types/db.go:34 —— Std_IICUDS 带 Status 语义(-1 禁止,1 正常) Status int8 `gorm:"column:status;default:0;index;" json:"status"` // query.go:61-68 —— 列表无任何 status 条件;写入路径也从不设置 status tx := impl.DBService.Order("created_at desc") ``` - **影响**:`status` 有索引、有明确语义,但全模块既不写入也不过滤:运营把动态/评论置为 `-1`(禁止)后,`Post.Fetch` 依旧照常返回该内容,审核封禁实际不生效;评论表也没有可用于审核的查询路径。 - **建议**:列表/详情统一加 `status >= 0`(或 `status = 1`)条件;创建时显式写默认状态;提供管理侧封禁接口(含角色校验)与缓存失效。 #### 27. 表结构无来源:无迁移、无 DDL、`InitData` 空实现却挂在启动钩子 - **位置**:`internal/models/query.go:10-12`、`cmd/main/main.go:37-38`、`internal/impl/impl.go:23` - **证据**: ```go // query.go:10-12 —— 裸 return,启动钩子的实现是空的 func InitData() error { return nil } // main.go:37-38 —— 却仍被注册为启动初始化 // 注册:初始化数据 srv.Use(models.InitData) ``` - **依赖库证据**:`with.Databases(cfg, nil)` → `IsAutoMigrate=false`(`database/sql/postgresql.go:17`),且 `database.MigrateTables` 需要显式 `database.AppendMigrate`(`database/new.go:112-120`)——全模块无任何 `AppendMigrate/AutoMigrate` 调用;全仓库检索 `feed_post|feed_comment|feed_relate_tags|feed_tags` 在 `*.sql/*.md/*.ps1/*.sh/*.yaml` 中**零命中**,即没有任何建表脚本或索引 DDL。 - **影响**:5 张表的结构与索引(含 `feed_relate_tags.identity` 唯一索引、`created_at` 索引)在仓库内没有唯一可信来源,模型 tag 与线上表是否一致无人能验证(这也是 P1-8 只能标注「推测」的原因);新环境无法自助建库,`InitData` 作为启动钩子给人「会初始化」的错觉。`wiki/audit-2026-08-10.md:41-43` 已指出「迁移与启动耦合」,本模块属「既无迁移也无 seed」的另一极端。 - **建议**:把 DDL 纳入仓库(版本化 SQL 迁移或在显式开关下 `AppendMigrate`),并在 CI 校验模型与 DDL 一致;删除空的 `InitData` 注册或实现真正的 seed。 #### 28. 测试为零:`test/lint` 为空目录,模块内 0 个测试文件 - **位置**:`module/social/feed/test/lint/`(空目录)、`module/social/feed/service/`(空目录) - **证据**:`glob module/social/feed/**/*_test.go` → 无匹配;`test/lint/` 与 `service/` 目录存在但无任何文件(兄弟模块 `social/group`、`social/relation` 同构,属模板遗留空目录)。可用的静态质量门只有 `gofmt`/`go vet`(本次均通过)。 - **影响**:本报告中的 P0/P1 问题(越权删除/修改、私密可见性反转、发帖必失败、回复评论必失败、点赞可无限刷、分页 LIMIT 错变量、`Tag.List` 丢结果、空实现返回成功)**没有一条会被现有 CI 拦住**,模块处于「编译通过即视为可用」的状态,与 `wiki/audit-2026-08-10.md:37-39` 的判断一致。 - **关键缺失用例清单(建议优先补齐)**: 1. 可见性越权:用户 A 发 `is_open=false`,B 调用 `Fetch` 不得看到;B 调用 `Remove/Change` 改删 A 的动态必须 `PermissionDenied`(对应 P0-1/P0-2/P0-3)。 2. 计数并发:N 个 goroutine 并发 `Action`/`AddComment`,断言 `cnt_like/cnt_comment` 与实际动作记录数一致(覆盖 P1-7 与未来的去重表)。 3. 发帖矩阵:无附件无标签 / 仅附件 / 仅标签 / 附件+标签 四种输入均须成功并能读回(P1-5、P1-12)。 4. 评论树:顶级评论、回复评论均成功,`feed_post.cnt_comment` 与父评论 `cnt_comment` 各 +1(P1-6)。 5. 分页边界:`page_size=1/10/100`、越界页、相同 `created_at` 翻页不重复不漏(P1-9)。 6. 幂等:同请求重试只产生一条动态/评论/标签(P2-22)。 7. 空实现接口契约:未实现 RPC 必须返回 `codes.Unimplemented`(P1-13)。 8. 启动配置校验:缺 `Databases`/`Service`/JWT 密钥时必须明确失败(P1-14、P1-4、P2-25)。 #### 29. 标签 key 体系不闭环:服务端生成 UUID 与客户端自造 key 混用 - **位置**:`internal/logic/tag/create.go:24-29`、`internal/logic/post/create.go:42-50`、`internal/models/query.go:62-65` - **证据**: ```go // tag/create.go:26 —— 标签主数据 key 由服务端生成 Key: utils.UUID(), // create.go:47 —— 动态关联标签的 key 直接采用客户端传入值 Key: val.Key, // query.go:64-65 —— 列表按 params["key"] 过滤关联表 tx = tx.Where("key = ?", tag) ``` - **影响**:`feed_relate_tags.key` 与 `feed_tags.key` 之间没有任何外键、校验或映射:客户端无法用 `Tag.List` 返回的 key 过滤动态(`Tag.Create` 生成的 UUID 不会出现在动态关联里),只能自己造 key 并保证全端一致;同一话题在数据层可能出现多个不同 key,`PostList` 的话题过滤结果不可预期;关联表也没有去重约束(重复关联行会让 `PostList` 返回重复动态)。此外 `FeedRelateTags.Content` 声明为 `gorm:"-"` 且从未赋值(`feed_relate_tags.go:10`),关联表里连标签显示名都没有。 - **建议**:明确「标签主数据」唯一来源:`Post.Create` 接收 `feed_tags.key`(校验存在性)或改为接收标签名并在服务端查找/创建;`feed_relate_tags` 增加 `(post_identity,key)` 唯一索引;`key` 列长度与格式(UUID/短码)统一并写入约束。 #### 30. HTTP 网关未接线(跨模块模板缺陷,本模块一句话引用) - **位置**:`module/social/feed/internal/server/new.go:20-35`、`module/social/feed/cmd/main/main.go:33`、`module/social/feed/service/`(空目录) - **证据**: ```go // server/new.go:20-25 —— Mux 字段声明后从未初始化,全模块也无 Register*HandlerServer 调用(检索确认) func New(addr string) *Server { srv := &Server{ Ctx: context.Background(), Grpc: grpc.NewServer(), grpcConns: make(map[string]*grpc.ClientConn) } // cmd/main/main.go:33 —— 把 nil mux 交给服务层;SDK 侧 Gateway.Enable 时 http.ListenAndServe(addr, mux) GatewayMux: s.Mux, ``` - **说明**:其它模块由 `service/expose.go` 把 `options.Gateway` 传给生成的 `pb.Register*HandlerServer`(`module/base/ads/service/expose.go:12-25`),生成的 handler 首行即 `mux.Handle(...)`(无 nil 防御);feed(及 `group`、`relation`)连 `service/expose.go` 都不存在,`cmd/main` 也从不调用 `Expose`。主流程已确认这是全局模板缺陷,故此处不展开论证,仅记录本模块现状:HTTP 面不可用(`http.DefaultServeMux` 无路由 → 404),`pb/*.pb.gw.go`(约 80KB 生成代码)为死代码,与 `README.md:212-218` 的 gRPC-gateway 描述不符。 - **建议**:与其它模块对齐,补 `service/expose.go` 并在 `server.New` 中初始化 `gwRuntime.NewServeMux()` + 注册 4 个 handler(或接入仓储/网关统一的 HTTP 暴露层),并加 HTTP 冒烟测试。 ### P3 #### 31. 模块 README 只有一行且标题错误 - **位置**:`module/social/feed/README.md:1-2` - **证据**: ```markdown # content ``` - **影响**:文件内容为 `# content`(与模块名 `feed` 都不一致),全模块没有任何使用说明、配置说明、接口边界说明;与仓库根 `README.md:212-218` 和 `wiki/` 文档体系脱节,新人无法从模块内判断哪些 RPC 可用(P1-13 的 8 个占位接口只能靠读代码发现)。 - **建议**:补最小 README:模块职责、已实现/未实现 RPC 清单、配置项与必需环境变量、表结构来源、gRPC 与 HTTP 调用示例。 #### 32. 死代码与永不生效的函数 - **位置**:`internal/models/query.go:80-94`、`internal/server/new.go:17`、`internal/models/feed_comment.go:12` - **证据**: ```go // query.go:80-94 —— ModifyTags/DeleteTags 无任何调用方,且 Where 用的列与写入用的列不一致(写入是 key 列,这里按 identity) func ModifyTags(data *FeedTags) (err error) { err = impl.DBService.Where("identity = ?", data.Key).Save(data).Error ... } func DeleteTags(key string) (err error) { err = impl.DBService.Where("identity = ?", key).Delete(&FeedTags{}).Error ... } // new.go:17 —— grpcConns 声明后从未使用 grpcConns map[string]*grpc.ClientConn // 连接池 // feed_comment.go:12 —— SubComment 关联从未被加载 SubComment []FeedComment `gorm:"foreignkey:parent_identity;"` ``` - **影响**:`AddTags`/`TagsList` 之外的两个标签写入函数无调用点且查询条件本身是错的(`FeedTags` 无 `identity` 列,实际列是 `key`),一旦被后人复用必然「无报错、无效果」;`grpcConns`、`SubComment`、空的 `service/` 目录与未注册的 `pb/*.pb.gw.go`(P2-30)共同构成模板残留。 - **建议**:删除无调用方且实现错误的函数(或修正为按 `key` 操作并接入接口);清理未使用字段与空目录。 #### 33. 命名与分层问题:`models` 包承担业务规则、拼写错误、函数名与语义不符 - **位置**:`internal/impl/impl.go:16`、`internal/models/query.go:30-33,106-116` - **证据**: ```go // impl.go:16 —— 拼写错误(Serice) MemorySerice *cache.Cache // query.go:30 —— ChargePost 实际语义是 UpdatePost(更新动态) func ChargePost(data *FeedPost) error { // query.go:106-116 —— 评论计数 +1、父子评论关系判定等业务规则写在数据访问层 err := tx.Create(comment).Table("feed_post").Where(...).UpdateColumn("cnt_comment", gorm.Expr("cnt_comment + ?", 1)).Error if comment.ParentIdentity != "" { ... } ``` - **影响**:`logic → models` 的分层里,`models` 既做 CRUD 又做计数业务规则,逻辑层无法独立测试;`ChargePost`/`MemorySerice` 这类命名会误导调用方;`AddComment(comment, authorIdentity)` 的参数被忽略(P2-17),签名与行为不一致。 - **建议**:统一命名(`UpdatePost`、`MemoryService`);把计数/回复规则上移到 logic(或抽 `service` 层),`models` 只保留数据访问;修正或删除未使用参数。 #### 34. 响应与分页语义不一致,魔法数字散落 - **位置**:`internal/logic/post/create.go:57-60`、`internal/logic/post/action.go:29-32`、`internal/logic/post/remove.go:31-34`、`internal/logic/tag/create.go:35`、`internal/logic/post/fetch.go:19-24`、`internal/logic/post/comment_list.go:19-24` - **证据**: ```go // 同一个 reply.Data 字段承载三种含义:identity / "OK" / 空串 return &pb.DataStatusReply{ Data: postData.Identity, ... } // create.go:58 return &pb.DataStatusReply{ Data: vars.OK, ... } // action.go:30(remove.go 同) return &pb.DataStatusReply{}, nil // tag/create.go:35(Data 为空) // 分页默认值语义不统一 if in.GetPageSize() <= 0 { in.PageSize = 10 } // fetch.go:22-24 if in.GetPageSize() < 10 { in.PageSize = 50 } // comment_list.go:22-24(9 会被改成 50) ``` - **影响**:客户端无法从 `Data` 稳定解析出「新对象 identity」还是「状态字符串」;`Tag.Create` 的响应既无 identity 也无 timeseq,与其它写接口不一致;分页默认值在同类接口间不一致,`<10 → 50` 会让 `page_size=9` 意外变成 50(配合 P1-9 更混乱)。这些魔法数字(10/50/1)散落在 6 个文件里。 - **建议**:`DataStatusReply.Data` 语义文档化(或在 proto 注释中明确),写接口统一返回新建 identity;分页默认值/上限抽成常量并在所有接口统一;对 8 个占位接口同步修正后再实现。 #### 35. 6 处重复的「鉴权 + 分页校验」样板;评论列表接口无法指定动态 - **位置**:`internal/logic/timeline/{recommend,friend,follow,hot}.go:11-28`、`internal/logic/tag/post_list.go:11-28`、`internal/logic/post/comment_list.go:11-28`、`proto/post.proto:19`、`proto/const.proto:10-14` - **证据**: ```go // 6 个文件逐字重复同一段(仅函数名/包不同) _, err = service.ParseMetaCtx(ctx, nil) if err != nil { return nil, err } if in.GetPageNo() < 1 { in.PageNo = 1 } if in.GetPageSize() < 10 { in.PageSize = 50 } // proto/post.proto:19 —— CommentList 复用 blocks.FetchRequest,后者只有 page_no/page_size/params rpc CommentList (blocks.FetchRequest) returns (CommentListReply){}; ``` - **影响**:样板重复使后续统一改造(加限流、加可见性过滤、改分页语义)需要改 6 处,极易漏改;`CommentList` 没有 `post_identity` 参数(`proto/const.proto:10-14` 只有 `page_no/page_size/params`),语义上只能靠 `params["post_identity"]` 这种无契约约定——实现时极易出现「参数缺失即全表返回评论」的越权读,值得在实现前先修 proto。 - **建议**:抽公共 `parsePage(in)`/`authMeta(ctx)` helper 或统一用拦截器;`CommentList` 增加明确的 `post_identity` 字段(并做必填校验),避免用 `params` map 承担关键参数。 #### 36. `cmd/cli` 直接把含凭据的配置打印到 stdout - **位置**:`module/social/feed/cmd/cli/main.go:9-12` - **证据**: ```go // cli/main.go:10-11 —— 加载与主程序相同的配置并整体打印 config.New("feed") fmt.Println(config.Spec.Databases) ``` - **影响**:CLI 打印的 `Databases` 含 DSN(用户名/口令/内网地址,见 P1-15),在本地调试、CI 日志或共享终端中会直接泄漏;且它复用了 `config.New("feed")`,在缺少 `etc/feed_.yaml` 时会 `log.Fatalln`,作为工具不具可用性。 - **建议**:删除该调试 CLI 或改为打印脱敏后的配置摘要(仅驱动/主机/库名,隐去口令);如确需诊断工具,走独立的、只读的子命令。 #### 37. `etc` 残留与命名不一致:`passport_*` 为其它服务拷贝、supervisor 文件名与 program 名不符、端口冲突 - **位置**:`module/social/feed/etc/passport_dev.yaml:1-2`、`etc/passport_test.yaml:2`、`etc/passport_prod.yaml:1-2`、`etc/supervisor.bsm-apps-passport.conf:1-2`、`etc/feed_prod.yaml:2` - **证据**: ```yaml # etc/passport_prod.yaml:1-2 —— 文件名叫 passport,内容 Service 却是 feed,端口 12426 Service: feed Port: 12426 # etc/feed_prod.yaml:2 —— 另一套布局里端口是 12212(同一个服务两个端口) ListenOn: 0.0.0.0:12212 ``` ```ini ; etc/supervisor.bsm-apps-passport.conf:1 —— 文件名 passport,program 名 feed [program:bsm-apps-feed] ``` - **影响**:同一模块并存两套互不兼容的配置布局与两个端口号(12212/12426),文件名、program 名、服务名三者不一致,运维极易加载错误文件或启动错进程;`passport_*` 明显是从其它模块拷贝而来(含 `WeChatConf`、`Kyc`、`KycConf` 等无关块,`etc/passport_prod.yaml:19-35`)。 - **建议**:只保留一套 `feed_{dev,test,prod}.yaml` + `supervisor.bsm-apps-feed.conf`,统一端口并删除无关配置块;在部署文档中固定「配置文件名 = 服务键小写 + 模式」这一约定(`bsm-sdk/core/conf/new.go:35`)。 ## 4. 推荐优化方案 按「先保安全与可用,再补工程能力」的顺序推进,前三项建议作为独立发布批次: 1. **所有权与可见性收敛(对应 P0-1/2/3、P1-16)** - 抽象统一的数据访问约束:所有写操作(`Remove`、`Change`、`DeleteComment` 及未来的封禁)强制 `identity + passport_identity` 双条件 + `RowsAffected` 校验,缺失即 `PermissionDenied`; - 可见性规则只在服务端一处定义(`is_open` + `status` + 是否本人/好友),列表查询统一走该处;模型去掉 `default:true`,杜绝 GORM 静默改写; - 管理类接口(标签创建、内容封禁)统一走 `ParseOptions{RoleValue}` 或独立管理面。 2. **修复三条主链路的确定性故障(P1-5/P1-6/P1-7/P1-9/P1-10/P1-11/P1-12)** - 空切片保护、`UpdateColumn` 补 `Model`、修正 `unlike` 判断、修正 `Limit(size)`、`Tag.List` 返回 `res`; - 点赞引入「用户×目标」动作表作为计数唯一来源(同时解决幂等与取消点赞); - 附件/标签用批量 `IN` 查询或 `Preload` 加载并补齐 `content`、评论补作者字段(P2-17); - 落地第 28 节列出的 8 类必测用例,把这些问题变成回归测试。 3. **鉴权与配置治理(P1-4、P1-14、P1-15、P2-25、P3-37)** - JWT 密钥显式化并 fail-fast(禁止回退默认值),supervisor 注入环境变量; - 统一 `etc` 布局(改名/补必填项),生产凭据移出仓库并轮换,`sslmode>=require`; - 启动校验覆盖 `Databases/Gateway/Etcd`,拒绝 `CHANGE_ME` 与随机端口。 4. **能力补齐与工程化(P1-13、P2-17~P2-30、P2-27、P3-31~P3-37)** - 8 个占位 RPC 先统一返回 `Unimplemented`(或直接下线路由),再逐个实现(时间线/评论列表先补 proto 参数,见 P3-35); - 按其它模块模式补齐 `service/expose.go` 并初始化 `Mux`(P2-30); - 补索引(`feed_post(created_at desc)` 等)、分页上限与游标分页、Redis 计数缓冲、限流与结构化日志; - 迁移/DDL 入仓并与模型一致性校验;interceptor 链(recover/超时/指标)+ health + 有超时的优雅退出。 ## 5. TODO 清单 - [ ] **P0-1** 为 `Post.Remove` 增加属主条件与 `RowsAffected` 校验|验收:用户 B 删除用户 A 的动态返回 `PermissionDenied`,A 的记录仍存在|涉及:`module/social/feed/internal/logic/post/remove.go:16`, `module/social/feed/internal/models/query.go:34` - [ ] **P0-2** 为 `Post.Change` 增加属主条件并把 `identity` 从唯一 WHERE 条件改为附加条件|验收:B 修改 A 的动态不生效且返回 `PermissionDenied`|涉及:`module/social/feed/internal/logic/post/change.go:40`, `module/social/feed/internal/models/query.go:30` - [ ] **P0-3** 修复可见性:模型去掉 `default:true`、按请求显式写 `is_open`、列表加可见性过滤|验收:A 创建 `is_open=false` 后 B 的 `Fetch` 看不到,A 自己能查到|涉及:`module/social/feed/internal/models/feed_post.go:12`, `module/social/feed/internal/logic/post/create.go:29`, `module/social/feed/internal/models/query.go:61` - [ ] **P1-4** JWT 密钥显式化与启动校验(禁止回退 SDK 默认值),supervisor 注入环境变量|验收:未设置 `BSM_JwtSecretKey` 或为默认值时进程启动失败;默认密钥签发的 token 被拒|涉及:`module/social/feed/internal/config/config.go:56`, `module/social/feed/etc/supervisor.bsm-apps-passport.conf:1` - [ ] **P1-5** `CreatePost` 对空 `Attachs`/`Tags` 做判空,事务不再因 `ErrEmptySlice` 回滚|验收:无附件纯文本发帖成功并在列表可读|涉及:`module/social/feed/internal/models/query.go:19` - [ ] **P1-6** 父评论计数更新补 `Model(&FeedComment{})`,并把链式 `Create+UpdateColumn` 拆为独立语句|验收:回复评论成功,动态与父评论 `cnt_comment` 各 +1|涉及:`module/social/feed/internal/models/query.go:106`, `module/social/feed/internal/models/query.go:112` - [ ] **P1-7** 修正 `unlike` 判断、引入用户维度动作表并做幂等 + 限流|验收:重复点赞只计一次、取消点赞可用、单用户刷计数被限流|涉及:`module/social/feed/internal/models/query.go:48`, `module/social/feed/internal/logic/post/action.go:25` - [ ] **P1-8** 为 `FeedRelateTags` 补 `Identity: utils.UUID()` 并核对线上唯一索引与历史空值|验收:一条动态挂多标签、多条动态挂标签均成功写入|涉及:`module/social/feed/internal/logic/post/create.go:44` - [ ] **P1-9** 分页 `Limit` 改用 `size` 并加 `page_size` 上限|验收:`size=10` 时首页返回 ≤10 条,翻页无空档|涉及:`module/social/feed/internal/models/query.go:67` - [ ] **P1-10** `Tag.List` 返回组装结果并补测试|验收:存在 N 个标签时返回 N 条且 `total=N`|涉及:`module/social/feed/internal/logic/tag/list.go:32` - [ ] **P1-11** 实现 `DeleteComment`(属主校验 + 软删除 + 计数回退 + 子评论处理)并统一 `id/identity` 入参契约|验收:本人可删、他人不可删、删除后列表与计数同步变化|涉及:`module/social/feed/internal/models/query.go:122`, `module/social/feed/internal/logic/post/delete_comment.go:21` - [ ] **P1-12** 列表加载附件与标签并补齐 `content`|验收:带图带话题发帖后 `Fetch` 返回相同附件与标签|涉及:`module/social/feed/internal/models/feed_post.go:16`, `module/social/feed/internal/logic/post/fetch.go:51` - [ ] **P1-13** 8 个未实现 RPC 统一返回 `codes.Unimplemented`(或临时下线路由)|验收:调用 `Timeline.*/Tag.PostList/Post.CommentList/Setting.*` 得到 `Unimplemented` 而非 OK+空响应|涉及:`module/social/feed/internal/logic/timeline/recommend.go:26`, `module/social/feed/internal/logic/setting/rights.go:20` - [ ] **P1-14** 统一 `etc` 配置:以现行结构重写 `feed_{dev,test,prod}.yaml`,启动校验必填项|验收:按仓库配置可启动且缺失字段时启动失败并指明字段|涉及:`module/social/feed/etc/feed_prod.yaml:1`, `module/social/feed/internal/config/config.go:56` - [ ] **P1-15** 轮换并移除仓库内明文凭据,配置改为环境变量注入、生产启用 `sslmode=require`|验收:仓库内无任何明文口令,凭据扫描通过|涉及:`module/social/feed/etc/feed_prod.yaml:4`, `module/social/feed/etc/feed_dev.yaml:4` - [ ] **P1-16** `Tag.Create` 增加角色校验与条数/长度/查重限制|验收:普通用户调用返回 `PermissionDenied`,管理员可创建且无重复标签|涉及:`module/social/feed/internal/logic/tag/create.go:16` - [ ] **P2-17** 评论落库作者(`PassportID/PassportIdentity`)并删除未使用的 `authorIdentity` 参数|验收:评论列表可显示作者;作者字段非空|涉及:`module/social/feed/internal/logic/post/add_comment.go:23`, `module/social/feed/internal/models/query.go:103` - [ ] **P2-18** 为计数加 Redis 缓冲、为写接口加限流|验收:点赞峰值不直接压 DB 单行;单用户超频请求被拒|涉及:`module/social/feed/internal/impl/impl.go:13`, `module/social/feed/internal/server/new.go:23` - [ ] **P2-19** 补 `created_at`/`is_open` 索引、`page_size` 上限、游标分页与 Count 合并|验收:万级数据下列表 P95 达标且深分页不劣化|涉及:`module/social/feed/internal/models/query.go:61`, `module/social/feed/internal/logic/post/fetch.go:22` - [ ] **P2-20** 移除 `fmt.Print`、生产关闭 GORM Debug、改结构化日志|验收:日志无正文与带参 SQL、含 trace-id 与 level|涉及:`module/social/feed/internal/models/query.go:69`, `module/social/feed/internal/impl/impl.go:23` - [ ] **P2-21** 删除动态时清理关联与评论、计数更新加 `deleted_at IS NULL`、提供对账回填|验收:删除后关联查询为空、计数与实际一致|涉及:`module/social/feed/internal/models/query.go:35`, `module/social/feed/internal/models/query.go:53` - [ ] **P2-22** 引入幂等键与标签唯一约束|验收:同请求重试只落一条数据,重复标签名只存一条|涉及:`module/social/feed/internal/logic/post/create.go:26`, `module/social/feed/internal/models/feed_tags.go:10` - [ ] **P2-23** 正文长度/富文本净化与附件 URL 白名单|验收:超长与非法协议 URL 被拒,HTML 被净化|涉及:`module/social/feed/internal/logic/post/create.go:22`, `module/social/feed/internal/logic/post/create.go:36` - [ ] **P2-24** 加 interceptor 链(recover/超时/日志/指标)、reflection 开关、health、带超时退出与网关超时|验收:panic 不再终止进程,探针可用,停止请求在超时内完成|涉及:`module/social/feed/internal/server/new.go:23`, `module/social/feed/cmd/main/main.go:41` - [ ] **P2-25** 扩展配置校验并清理死配置|验收:`Databases/Gateway/Etcd` 缺失即 fail-fast;删除 `WeChat/Kyc/Token` 无关块|涉及:`module/social/feed/internal/config/config.go:56` - [ ] **P2-26** 列表与详情统一加 `status` 过滤、创建时显式写状态|验收:被置为 `-1` 的动态不再出现在列表|涉及:`module/social/feed/internal/models/query.go:61` - [ ] **P2-27** DDL/迁移入仓并与模型一致性校验,删除空的 `InitData` 注册|验收:空库可一键建表;模型与 DDL 差异在 CI 报错|涉及:`module/social/feed/internal/models/query.go:10`, `module/social/feed/cmd/main/main.go:38` - [ ] **P2-28** 建立测试基线:第 28 节 8 类用例全部落地并对 P0/P1 回归设门禁|验收:`go test ./...` 覆盖越权/计数/发帖/评论/分页/幂等/未实现/配置|涉及:`module/social/feed/test/lint/` - [ ] **P2-29** 统一标签 key 体系(服务端主数据 + `(post_identity,key)` 唯一索引)|验收:`Tag.List` 的 key 可直接用于 `PostList` 过滤且无重复标签|涉及:`module/social/feed/internal/logic/tag/create.go:26`, `module/social/feed/internal/logic/post/create.go:47` - [ ] **P2-30** 按其它模块模式补 `service/expose.go` 并初始化 `Mux`、注册 4 个 handler|验收:HTTP `POST /feed.Post/Fetch` 返回 200 与正确 JSON|涉及:`module/social/feed/internal/server/new.go:20`, `module/social/feed/cmd/main/main.go:33` - [ ] **P3-31** 补模块 README(职责/已实现与未实现接口/配置/表结构来源)|验收:文档与代码一致,未实现接口在文档中列明|涉及:`module/social/feed/README.md:1` - [ ] **P3-32** 清理死代码(`ModifyTags/DeleteTags`、`grpcConns`、未注册的 gw 代码、空目录)|验收:静态与人工核查无未使用导出与死路径|涉及:`module/social/feed/internal/models/query.go:80`, `module/social/feed/internal/server/new.go:17` - [ ] **P3-33** 统一命名与分层(`MemorySerice`→`MemoryService`、`ChargePost`→`UpdatePost`、业务规则上移 logic)|验收:`models` 仅数据访问,命名无歧义|涉及:`module/social/feed/internal/impl/impl.go:16`, `module/social/feed/internal/models/query.go:30` - [ ] **P3-34** 统一 `DataStatusReply.Data` 语义与分页默认值常量|验收:所有写接口返回同一语义;`page_size` 默认/上限一致|涉及:`module/social/feed/internal/logic/tag/create.go:35`, `module/social/feed/internal/logic/post/fetch.go:22` - [ ] **P3-35** 抽公共鉴权/分页样板,`CommentList` 增加 `post_identity` 字段|验收:6 处样板收敛为 1 处;评论列表可显式指定动态且必填校验生效|涉及:`module/social/feed/internal/logic/post/comment_list.go:12`, `module/social/feed/proto/post.proto:19` - [ ] **P3-36** 移除或脱敏 `cmd/cli` 的配置打印|验收:CLI 不再输出含口令的 DSN|涉及:`module/social/feed/cmd/cli/main.go:11` - [ ] **P3-37** 统一 `etc` 文件命名/端口,删除 `passport_*` 与无关配置块,修正 supervisor 文件名|验收:单一配置布局、单一端口、文件名与服务名一致|涉及:`module/social/feed/etc/passport_prod.yaml:1`, `module/social/feed/etc/supervisor.bsm-apps-passport.conf:1` ## 6. 审计摘要(供汇总使用) - **问题数**:P0=3 P1=13 P2=14 P3=7(合计 37) - **最高风险(一句话)**:动态的删除与修改完全没有属主校验(IDOR),叠加可见性控制被 GORM 默认值与缺失过滤双重击穿,任何登录用户即可读取、改写、删除全站动态(若生产未注入 `BSM_JwtSecretKey`,连账号都不需要)。 - **最优先 3 个动作**: 1) 给 `Remove`/`Change` 加上 `passport_identity` 属主条件与 `RowsAffected` 校验,并补齐列表可见性过滤(P0-1/P0-2/P0-3)。 2) 修复三条主链路的确定性故障并加回归测试:空切片发帖必失败、回复评论事务必败、点赞 `unlike`/无限刷(P1-5/P1-6/P1-7,测试见 P2-28)。 3) 显式配置并校验 JWT 密钥、禁止回退 SDK 公开默认值 `Cblocksmesh2022C`,supervisor 注入环境变量(P1-4)。 - **未能覆盖/无法验证的部分**: - **真实数据库表结构**:仓库内无任何 DDL/迁移(P2-27),故 `feed_relate_tags.identity` 唯一索引是否真实存在、`created_at/is_open` 索引现状、`is_open` 的 DB 默认值是否与模型一致,均无法验证(相关判断已在 P1-8 标注「推测」)。 - **生产环境变量与部署产物**:是否注入 `BSM_JwtSecretKey`、实际加载哪份配置(`etc/feed_*.yaml` 还是运维另放的 workspace 公共配置)、是否有 `.builds/` 部署产物覆盖 etc,仓库内无法确认(P1-4 已给出降级/升级条件)。 - **运行期实测**:约束禁止改动代码,未启动服务、未连库、未做压测与黑盒验证;所有 GORM/gRPC 行为结论来自依赖库源码静态推导(GORM v1.31.2 `callbacks/create.go:275-281`、`callbacks.go:110-117`、`schema/field.go:236-239`、`callbacks/create.go:298-301`;protobuf v1.36.12 `proto/encode.go:120-146`),已在各条目中标注来源。 - **生成代码逐行审阅**:`pb/const.pb.go`(约 99KB)、`*.pb.gw.go` 等生成文件仅按需检索(路由注册、Unimplemented、路由 pattern),未逐行审阅;`wiki/README.md:21`、`wiki/api/00-overview.md:80` 提到的 social 三模块 protobuf descriptor 同名冲突未在本报告展开(属跨模块事项,`pb/blocks_compat.go` 是本地规避措施)。 - **性能**:无实测数据,P2-18/P2-19 的性能影响为静态推断(无索引、OFFSET 深分页、无缓存、热点行更新)。 - **HTTP 网关缺陷的跨模块定论**:本报告仅记录 feed 侧现状(`Mux` 恒为 nil、无 `service/expose.go`、`cmd/main` 不调用 `Expose`;其它模块由 `service/expose.go` 传入 `options.Gateway` 且生成代码无 nil 防御),系统性结论以主流程/聚合审计为准,未在本报告内独立论证。 - **横向对比**:仅抽查 `module/social/group`(配置、query 模式)与 `pkgs/all`(网关暴露、密钥注入)用于确认约定差异,未审计 relation 及聚合路径对 feed 的实际调用(README 表明 social 尚未接入聚合入口)。