Files
full/docs/audit/module-social-feed.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

73 KiB
Raw Blame History

审计报告module/social/feed

1. 模块概览

  • 路径D:\work\bsm-infra\full\module\social\feedGo module bsm/full/module/social/feedgo.mod:1Go 1.26.5)。
  • 定位社交域「用户动态内容」服务。READMEREADME.md:212-218)描述其承载动态发布/查询/修改/删除、评论、互动计数、标签,以及 4 个时间线接口;服务接口为 PostTagTimelineSetting
  • 技术形态gRPCgoogle.golang.org/grpc v1.83.2+ grpc-gatewaygrpc-gateway/v2 v2.30.0+ GORMgorm.io/gorm v1.31.2+ PostgreSQL/Redis/etcdgo.mod:5-13),依赖 SDK git.apinb.com/bsm-sdk/corereplace 指向 ../../../../../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.goconfig.New("Feed")impl.NewImpl()server.Newservice.New(...).Start()cmd/cli/main.go 为调试用 CLI。
  • 持久化模型 5 张表feed_postfeed_commentfeed_relate_attachfeed_relate_tagsfeed_tagsinternal/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/*.protoREADME.md;按需检索 pb/*.pb.gw.go(路由 pattern、网关注册函数pb/post_grpc.pb.goUnimplemented 行为)。为验证运行时语义,另行读取 SDK 依赖(D:\work\bsm-sdk\coreservice/meta.goservice/service.goconf/new.gowith/databases.godatabase/*env/env.gocrypto/token/jwt.gotypes/db.goprinter/print.go)与第三方库源码(C:\Users\david\go\pkg\mod\gorm.io\gorm@v1.31.2google.golang.org\protobuf@v1.36.12grpc@v1.83.2),用于确认 GORM 默认值/空切片/缺表模型的行为。

方法全量逐文件阅读26 个非生成 Go 文件全部读完)+ 关键字检索(TODOPreload_ =UpdateColumnMigrateSecretKeyRegister*HandlerExpose+ 与同域兄弟模块 module/social/groupmodule/social/relation 及聚合入口 pkgs/all 交叉对照 + 仓库既有审计基线 wiki/audit-2026-08-10.mddocs/audit/README.mddocs/audit/module-base-*.md 对齐。

证据标准:每条结论给出 file:line + 代码摘录;依赖库行为单独标注来源文件与行号;无法从源码确认的判断显式标注「推测」;「命名返回值未赋值 → 恒返回 nil」一类结论必须见到裸 return 才成立(本报告 P1-11、P1-13 均已核对裸 return)。

只读约束遵守情况:未修改/新建/删除任何 .go.proto.yamlgo.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-29module/social/feed/internal/models/query.go:34-37
  • 证据
// 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.DeletedAtbsm-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-49module/social/feed/internal/models/query.go:30-33
  • 证据
// 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 条件放进 WHEREidentity = ? AND passport_identity = ?),并对 RowsAffected 做校验;修改可见性建议使用显式 Select("is_open") 或 map 更新以规避零值忽略语义。

3. 动态可见性控制完全失效:私密动态写不进去,列表也不做任何可见性过滤

  • 位置module/social/feed/internal/logic/post/create.go:29module/social/feed/internal/models/feed_post.go:12module/social/feed/internal/models/query.go:60-68
  • 证据
// 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 = truecallbacks/create.go:298-301 在字段为零值(false)时用默认值替换写入值并回写结构体:
// 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=falseprotobuf bool 默认值即 false客户端不显式传 true 时也会是 false会被 GORM 静默改写为 true 落库 —— 私密动态永远无法创建,用户以为已设为私密、实际是公开;ChangeUpdates(&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-59module/social/feed/etc/supervisor.bsm-apps-passport.conf:1-8
  • 证据
// 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_JwtSecretKeyconfig.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 会显式覆盖密钥),且 READMEREADME.md:194)明确 social 三模块尚未接入聚合入口因此独立部署路径是现实路径。附带SDK 级):crypto/token/jwt.goParseJwt 未限制签名算法(无 jwt.WithValidMethods),存在算法混淆风险面。
  • 建议:在 config.New 中显式要求并校验密钥(非空、长度 16/24/32、非默认值否则 panic 拒绝启动,可对照 pkgs/all/internal/config/config.go:63-73supervisor 配置补 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 对可空切片调用 CreateGORM 返回 ErrEmptySlice 并回滚整个事务

  • 位置module/social/feed/internal/models/query.go:14-29
  • 证据
// 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.2callbacks/create.go:275-281 对空切片直接返回 gorm.ErrEmptySliceerrors.go:36-37
			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
  • 证据
// 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 也无 Tablecallbacks.go:110-117 会写入错误 "...Table not set, please set it like: db.Model(&user) or db.Table(\"users\")"
			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-57module/social/feed/internal/logic/post/action.go:17-33
  • 证据
// query.go:41-53 —— 第二个分支误用 action_type 判断(应为 action_opcolumn 保持空串
	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=="",生成非法 SQLSET = + 1 形态)→ 点踩/取消点赞功能不可用(返回 DB 错误)。
    2. 点赞没有任何「谁赞过」的记录表、没有幂等键、没有频率限制:同一用户重复调用 Post.Action{ActionOp:"ilike"} 即可把 cnt_like 刷到任意大。计数被用于展示与(未来的)热门排序,属可被利用的数据污染/排序操纵,而且无法取消点赞unlike 分支不可用,计数只增不减)。
    3. action_typepost/commenttx 退化为无 Model 的 impl.DBService同样命中「Table not set」错误。
  • 建议:引入 feed_actionuser×target 唯一索引)记录并以其驱动计数(或至少「先插入动作记录成功再 +1」的幂等事务);修正 unlike 判断;对 action_op/action_type 做白名单枚举校验;对点赞/评论/发帖加限流(见 P2-18

8. feed_relate_tags.identity 从未赋值,标签关联写入存在唯一键冲突(推测)

  • 位置module/social/feed/internal/logic/post/create.go:42-50module/social/feed/internal/models/feed_relate_tags.go:6-9
  • 证据
// 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_IDIdentityIdentity 是 uniqueIndexvarchar(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
  • 证据
// 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=10LIMIT 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
  • 证据
// 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=0tags=[]。客户端据此认为「系统没有任何标签」,话题入口/筛选功能整体失效;tagserr 的处理成为死代码,掩盖了 TagsList 的真实错误表现。
  • 建议return res, nil;补「有标签时返回条数与内容」的测试。
  • 附带models.TagsListquery.go:96-101)无分页、无过滤,全表返回 feed_tags,见 P2-19。

11. DeleteComment 是空实现却返回成功:评论删不掉,且只传 id 也返回成功

  • 位置module/social/feed/internal/models/query.go:122-125module/social/feed/internal/logic/post/delete_comment.go:16-33
  • 证据
// 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 idproto/const.proto:16-19 明确提供该字段),依然返回成功——静默失败最难被发现。同时该接口没有任何属主/帖子作者/管理员判定,一旦将来补上实现,会把越权删除评论一并引入。
  • 建议:实现时按 identity 软删除 + 属主/动态作者/管理员校验 + RowsAffected 校验 + 子评论与计数回退;ididentity 二选一的入参约束要在文档与校验中统一(当前校验与实现不一致)。

12. 动态列表永远不返回附件与标签(gorm:"-" + 全模块无 Preload

  • 位置module/social/feed/internal/models/feed_post.go:16-17module/social/feed/internal/logic/post/fetch.go:39-66
  • 证据
// 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 返回的 attachstags 恒为空数组 —— 发帖时上传的图片/附件、关联的话题标签永远读不出来(写进去、读不出)。这与 P1-5必须有附件+标签才能发帖)叠加后体验最差:用户被迫传附件,却看不到任何图片。即便将来加载了关联行,FeedRelateTags.Contentgorm:"-"feed_relate_tags.go:10)且从未赋值,tags[].content 依然是空串。
  • 建议:给关联字段加真实关联标签并用 Preload 加载(或一次性批量查询 post_identity IN (...) 组装,避免 N+1Tags[].content 通过 feed_tags 关联补齐;补「发布带图带话题 → 列表返回附件与标签」的测试。

13. 8 个 RPC 未实现,却对外返回「成功 + 空响应」而非 Unimplemented

  • 位置internal/logic/timeline/{recommend,friend,follow,hot}.go:11-28internal/logic/tag/post_list.go:11-28internal/logic/post/comment_list.go:11-28internal/logic/setting/{info,rights}.go:11-22
  • 证据(逐条列出方法名、行号与实际行为):
RPC 方法 文件:行 实际行为
Timeline.Recommend internal/logic/timeline/recommend.go:11TODO :26,裸 return :28 鉴权 + 分页校验后直接返回 typed-nil *pb.FeedPostListReply
Timeline.Friend internal/logic/timeline/friend.go:11TODO :26,裸 return :28 同上
Timeline.Follow internal/logic/timeline/follow.go:11TODO :26,裸 return :28 同上
Timeline.Hot internal/logic/timeline/hot.go:11TODO :26,裸 return :28 同上
Tag.PostList internal/logic/tag/post_list.go:11TODO :26,裸 return :28 同上
Post.CommentList internal/logic/post/comment_list.go:11TODO :26,裸 return :28 同上
Setting.Info internal/logic/setting/info.go:11TODO :18/:20,裸 return :22 typed-nil *pb.Empty,不做任何修改
Setting.Rights internal/logic/setting/rights.go:11TODO :18/:20,裸 return :22 同上
// 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 errorprotobuf 对 nil/无效消息编码为空字节(google.golang.org/protobuf@v1.36.12/proto/encode.go:120-146gRPC 以 code=OK 返回字段全零的响应。后果:① 客户端无法区分「未实现」与「暂无数据」,Timeline.* 被当成「没有动态」、CommentList 被当成「没有评论」;② Setting.Info/Rights 被当成「修改成功」(实际未改任何东西),是最典型的静默失败;③ 监控无法通过错误率发现这些接口未实现。READMEREADME.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-8dev/test 同构)、module/social/feed/internal/config/config.go:46-62
  • 证据
# 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
// config.go:56 —— 要求 Service/Cache 非空impl.go:23 用 Databases 建连
	conf.NotNil(Spec.Service, Spec.Cache)
	DBService = with.Databases(config.Spec.Databases, nil) // model
  • 依赖库证据conf.Newetc/{ServiceKey小写}_{mode}.yaml 为第一候选(bsm-sdk/core/conf/new.go:35-36),文件存在即不再回退 workspace 公共配置;随后 new.go:58-60 要求内容含 Service: 否则 log.Fatallnwith.Databases(nil, nil) 直接 panic("No Database Source Found !")bsm-sdk/core/with/databases.go:13-15)。
  • 影响cmd/mainconfig.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/Gatewayconfig.New 增加 NotNil(Spec.Databases, Spec.Gateway) 等必填校验,让缺失在启动期以明确信息失败;在 README.md/部署文档中写明配置来源与必需环境变量。

15. etc 中留存明文数据库口令与公网地址(与既有审计结论不一致)

  • 位置module/social/feed/etc/feed_prod.yaml:4module/social/feed/etc/feed_dev.yaml:4module/social/feed/etc/feed_test.yaml:4module/social/feed/etc/passport_prod.yaml:7
  • 证据
# 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
  • 证据
// 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-31module/social/feed/internal/models/query.go:103module/social/feed/internal/models/feed_comment.go:6-16
  • 证据
// 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-24module/social/feed/internal/server/new.go:20-33
  • 证据
// 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-19cnt_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-101module/social/feed/internal/logic/post/fetch.go:19-26
  • 证据
// 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_Passportbsm-sdk/core/types/db.go:28-35,52-55created_atis_openstatus 均无索引 → 列表需全表扫描 + 排序;深分页 OFFSET 随页码线性劣化;page_size 无上限时单请求可拉取极大结果集(叠加 fmt.Print 打印开销);Find(...).Count(...) 每条请求两条 SQLTagsList 同样)。相同 created_at 的记录在 OFFSET 翻页下还可能重复/漏出。
  • 建议:为 feed_post(created_at desc)(或 (is_open, created_at desc))建索引;page_size 设上限(如 50Count 与查询合并或改为「先 count 再查」;深分页改游标(created_at,idTagsList 加分页与上限。

20. 敏感与全量内容日志:打印整页动态内容 + GORM 生产 Debug 打印全部 SQL 及参数

  • 位置module/social/feed/internal/models/query.go:69module/social/feed/internal/impl/impl.go:23
  • 证据
// 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 日志;printerlog.New(os.Stdout, ...) 的裸 loggerbsm-sdk/core/printer/print.go:11-17),无字段、无级别、无 trace/request-id、带 ANSI 颜色。
  • 影响:生产日志中持续出现全量动态正文(可能含用户隐私/违规内容)与带参数的 SQLidentitycontentpassport_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
  • 证据
// 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_attachfeed_relate_tagsfeed_comment 行全部保留(无级联、无清理任务),已删除动态仍可被点赞/评论(计数行继续变化),计数也没有「评论删除后 -1」的回退路径DeleteComment 还是空实现)。数据持续膨胀,且「已删除内容仍有互动」的一致性无法自证。
  • 建议:删除动态时在事务内软删除评论与关联行(并记录审计);计数更新统一加 deleted_at IS NULL 条件;提供对账/回填任务(cnt_commentfeed_comment 实际条数比对)。

22. 幂等性与唯一约束缺失:重复提交即重复数据,标签无去重

  • 位置internal/logic/post/create.go:25-30internal/logic/post/add_comment.go:23-31internal/logic/tag/create.go:23-33internal/models/feed_tags.go:8-12
  • 证据
// 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-40internal/models/feed_post.go:11internal/models/feed_relate_attachs.go:11
  • 证据
// 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: 等协议或任意外链(若前端直接渲染为链接/图片,则为外链注入与钓鱼跳转面;urlvarchar(255),也无格式校验)。
  • 建议:服务端做长度上限(正文/附件数/标签数)+ 富文本白名单净化(或明确只接受纯文本并在文档中声明)+ 附件 URL 协议与域名白名单(对齐 OSS/CDN 域名)+ 图片真实性与尺寸校验;在 proto 中补充字段约束或接入校验中间件。

24. 健壮性与可观测性:无拦截器(无 recover/超时/指标、reflection 无条件开启、无健康检查、退出与网关无超时

  • 位置internal/server/new.go:20-35cmd/main/main.go:19-45
  • 证据
// 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:任何 panicimpl.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:41HTTP 网关无读写超时。⑤ 全模块日志无 trace/request-id见 P2-20
  • 建议:加 unary interceptor 链recover → 超时 → 限流 → 日志/指标)、reflection 改为配置开关且生产默认关闭、注册 grpc_health_v1GracefulStop 包一层带超时的强制 Stop、网关加 ReadHeaderTimeout/ReadTimeout/WriteTimeout

25. 配置校验不足与不安全默认值:随机端口/本机 IP、必填项只校验两项、Databases/Etcd 为空即 panic、4 个配置块为死配置

  • 位置internal/config/config.go:15-62
  • 证据
// 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.CheckPortbsm-sdk/core/conf/new.go:90-97)会让实例注册地址漂移、网关/etcd 路由不稳定;BindIP 缺失时绑定本机探测 IP多网卡机器上可能绑到错误网卡NotNil 只覆盖 Service/CacheDatabases 为空会在 with.Databases 里 panicwith/databases.go:13-15WeChatConf/Kyc/KycConf/Token/Rpc 五个结构体字段在本模块无任何读取处(与动态业务无关,明显是模板拷贝),其中 Kyc.ApiSecret/ApiToken 属敏感命名却无人使用,容易误判「已配置生效」。
  • 建议:必填项校验扩展到 DatabasesDriver/Source 非空)、Gateway.Port、etcd endpoints缺失或为 CHANGE_ME 时 fail-fast 并输出明确字段名;禁止生产环境随机端口;删除与业务无关的 WeChat/Kyc/KycConf 配置。

26. 模型 Status 字段从未使用:无法封禁/下架动态与评论

  • 位置internal/models/feed_post.go:8-18internal/models/query.go:60-68
  • 证据
// 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-12cmd/main/main.go:37-38internal/impl/impl.go:23
  • 证据
// query.go:10-12 —— 裸 return启动钩子的实现是空的
func InitData() error {
	return nil
}
// main.go:37-38 —— 却仍被注册为启动初始化
	// 注册:初始化数据
	srv.Use(models.InitData)
  • 依赖库证据with.Databases(cfg, nil)IsAutoMigrate=falsedatabase/sql/postgresql.go:17),且 database.MigrateTables 需要显式 database.AppendMigratedatabase/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/groupsocial/relation 同构,属模板遗留空目录)。可用的静态质量门只有 gofmt/go vet(本次均通过)。
  • 影响:本报告中的 P0/P1 问题(越权删除/修改、私密可见性反转、发帖必失败、回复评论必失败、点赞可无限刷、分页 LIMIT 错变量、Tag.List 丢结果、空实现返回成功)没有一条会被现有 CI 拦住,模块处于「编译通过即视为可用」的状态,与 wiki/audit-2026-08-10.md:37-39 的判断一致。
  • 关键缺失用例清单(建议优先补齐)
    1. 可见性越权:用户 A 发 is_open=falseB 调用 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 各 +1P1-6
    5. 分页边界:page_size=1/10/100、越界页、相同 created_at 翻页不重复不漏P1-9
    6. 幂等:同请求重试只产生一条动态/评论/标签P2-22
    7. 空实现接口契约:未实现 RPC 必须返回 codes.UnimplementedP1-13
    8. 启动配置校验:缺 Databases/Service/JWT 密钥时必须明确失败P1-14、P1-4、P2-25

29. 标签 key 体系不闭环:服务端生成 UUID 与客户端自造 key 混用

  • 位置internal/logic/tag/create.go:24-29internal/logic/post/create.go:42-50internal/models/query.go:62-65
  • 证据
// 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.keyfeed_tags.key 之间没有任何外键、校验或映射:客户端无法用 Tag.List 返回的 key 过滤动态(Tag.Create 生成的 UUID 不会出现在动态关联里),只能自己造 key 并保证全端一致;同一话题在数据层可能出现多个不同 keyPostList 的话题过滤结果不可预期;关联表也没有去重约束(重复关联行会让 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-35module/social/feed/cmd/main/main.go:33module/social/feed/service/(空目录)
  • 证据
// 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.gooptions.Gateway 传给生成的 pb.Register*HandlerServermodule/base/ads/service/expose.go:12-25),生成的 handler 首行即 mux.Handle(...)(无 nil 防御feedgrouprelation)连 service/expose.go 都不存在,cmd/main 也从不调用 Expose。主流程已确认这是全局模板缺陷故此处不展开论证仅记录本模块现状HTTP 面不可用(http.DefaultServeMux 无路由 → 404pb/*.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
  • 证据
# content
  • 影响:文件内容为 # content(与模块名 feed 都不一致),全模块没有任何使用说明、配置说明、接口边界说明;与仓库根 README.md:212-218wiki/ 文档体系脱节,新人无法从模块内判断哪些 RPC 可用P1-13 的 8 个占位接口只能靠读代码发现)。
  • 建议:补最小 README模块职责、已实现/未实现 RPC 清单、配置项与必需环境变量、表结构来源、gRPC 与 HTTP 调用示例。

32. 死代码与永不生效的函数

  • 位置internal/models/query.go:80-94internal/server/new.go:17internal/models/feed_comment.go:12
  • 证据
// 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 之外的两个标签写入函数无调用点且查询条件本身是错的(FeedTagsidentity 列,实际列是 key),一旦被后人复用必然「无报错、无效果」;grpcConnsSubComment、空的 service/ 目录与未注册的 pb/*.pb.gw.goP2-30共同构成模板残留。
  • 建议:删除无调用方且实现错误的函数(或修正为按 key 操作并接入接口);清理未使用字段与空目录。

33. 命名与分层问题:models 包承担业务规则、拼写错误、函数名与语义不符

  • 位置internal/impl/impl.go:16internal/models/query.go:30-33,106-116
  • 证据
// 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签名与行为不一致。
  • 建议:统一命名(UpdatePostMemoryService);把计数/回复规则上移到 logic或抽 service 层),models 只保留数据访问;修正或删除未使用参数。

34. 响应与分页语义不一致,魔法数字散落

  • 位置internal/logic/post/create.go:57-60internal/logic/post/action.go:29-32internal/logic/post/remove.go:31-34internal/logic/tag/create.go:35internal/logic/post/fetch.go:19-24internal/logic/post/comment_list.go:19-24
  • 证据
// 同一个 reply.Data 字段承载三种含义identity / "OK" / 空串
	return &pb.DataStatusReply{ Data: postData.Identity, ... }        // create.go:58
	return &pb.DataStatusReply{ Data: vars.OK, ... }                  // action.go:30remove.go 同)
	return &pb.DataStatusReply{}, nil                                // tag/create.go:35Data 为空)
// 分页默认值语义不统一
	if in.GetPageSize() <= 0 { in.PageSize = 10 }                    // fetch.go:22-24
	if in.GetPageSize() < 10 { in.PageSize = 50 }                    // comment_list.go:22-249 会被改成 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-28internal/logic/tag/post_list.go:11-28internal/logic/post/comment_list.go:11-28proto/post.proto:19proto/const.proto:10-14
  • 证据
// 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
  • 证据
// cli/main.go:10-11 —— 加载与主程序相同的配置并整体打印
	config.New("feed")
	fmt.Println(config.Spec.Databases)
  • 影响CLI 打印的 Databases 含 DSN用户名/口令/内网地址,见 P1-15在本地调试、CI 日志或共享终端中会直接泄漏;且它复用了 config.New("feed"),在缺少 etc/feed_<mode>.yaml 时会 log.Fatalln,作为工具不具可用性。
  • 建议:删除该调试 CLI 或改为打印脱敏后的配置摘要(仅驱动/主机/库名,隐去口令);如确需诊断工具,走独立的、只读的子命令。

37. etc 残留与命名不一致:passport_* 为其它服务拷贝、supervisor 文件名与 program 名不符、端口冲突

  • 位置module/social/feed/etc/passport_dev.yaml:1-2etc/passport_test.yaml:2etc/passport_prod.yaml:1-2etc/supervisor.bsm-apps-passport.conf:1-2etc/feed_prod.yaml:2
  • 证据
# 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
; etc/supervisor.bsm-apps-passport.conf:1 —— 文件名 passportprogram 名 feed
[program:bsm-apps-feed]
  • 影响同一模块并存两套互不兼容的配置布局与两个端口号12212/12426文件名、program 名、服务名三者不一致,运维极易加载错误文件或启动错进程;passport_* 明显是从其它模块拷贝而来(含 WeChatConfKycKycConf 等无关块,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
    • 抽象统一的数据访问约束:所有写操作(RemoveChangeDeleteComment 及未来的封禁)强制 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
    • 空切片保护、UpdateColumnModel、修正 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-17P2-30、P2-27、P3-31P3-37
    • 8 个占位 RPC 先统一返回 Unimplemented(或直接下线路由),再逐个实现(时间线/评论列表先补 proto 参数,见 P3-35
    • 按其它模块模式补齐 service/expose.go 并初始化 MuxP2-30
    • 补索引(feed_post(created_at desc)、分页上限与游标分页、Redis 计数缓冲、限流与结构化日志;
    • 迁移/DDL 入仓并与模型一致性校验interceptor 链recover/超时/指标)+ health + 有超时的优雅退出。

5. TODO 清单

  • P0-1Post.Remove 增加属主条件与 RowsAffected 校验|验收:用户 B 删除用户 A 的动态返回 PermissionDeniedA 的记录仍存在|涉及:module/social/feed/internal/logic/post/remove.go:16, module/social/feed/internal/models/query.go:34
  • P0-2Post.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-8FeedRelateTagsIdentity: 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-19created_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/DeleteTagsgrpcConns、未注册的 gw 代码、空目录)|验收:静态与人工核查无未使用导出与死路径|涉及:module/social/feed/internal/models/query.go:80, module/social/feed/internal/server/new.go:17
  • P3-33 统一命名与分层(MemorySericeMemoryServiceChargePostUpdatePost、业务规则上移 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 公开默认值 Cblocksmesh2022Csupervisor 注入环境变量P1-4
  • 未能覆盖/无法验证的部分
    • 真实数据库表结构:仓库内无任何 DDL/迁移P2-27feed_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-281callbacks.go:110-117schema/field.go:236-239callbacks/create.go:298-301protobuf v1.36.12 proto/encode.go:120-146),已在各条目中标注来源。
    • 生成代码逐行审阅pb/const.pb.go(约 99KB*.pb.gw.go 等生成文件仅按需检索路由注册、Unimplemented、路由 pattern未逐行审阅wiki/README.md:21wiki/api/00-overview.md:80 提到的 social 三模块 protobuf descriptor 同名冲突未在本报告展开(属跨模块事项,pb/blocks_compat.go 是本地规避措施)。
    • 性能无实测数据P2-18/P2-19 的性能影响为静态推断无索引、OFFSET 深分页、无缓存、热点行更新)。
    • HTTP 网关缺陷的跨模块定论:本报告仅记录 feed 侧现状(Mux 恒为 nil、无 service/expose.gocmd/main 不调用 Expose;其它模块由 service/expose.go 传入 options.Gateway 且生成代码无 nil 防御),系统性结论以主流程/聚合审计为准,未在本报告内独立论证。
    • 横向对比:仅抽查 module/social/group配置、query 模式)与 pkgs/all(网关暴露、密钥注入)用于确认约定差异,未审计 relation 及聚合路径对 feed 的实际调用README 表明 social 尚未接入聚合入口)。