# feed(社交动态与内容互动)代码审计报告 | 项 | 内容 | | --- | --- | | 审计对象 | `module/social/feed`(module 路径 `bsm/full/module/social/feed`) | | 服务域 | 社交与内容 | | 审计日期 | 2026-09-22 | | 代码规模 | 手写 Go 32 个文件 / 1160 行;`pb/` 14 个文件 / 6527 行(含手写兼容文件 `pb/blocks_compat.go`);`proto/` 5 个文件 / 433 行;`etc/` 6 个 yaml + 1 个 supervisor conf;`*_test.go` 0 个 | | 入口 | 仅独立入口 `cmd/main/main.go`(gRPC + gateway 单进程,端口 12212);`service/` 为**空目录**,未被 `pkgs/all`、`pkgs/ecmall` 引用;`cmd/cli/main.go` 只打印配置 | | 对外协议 | gRPC(4 个 service / 17 个 RPC);gateway 路由定义为 `POST /feed.{Service}/{Method}`,但模块内**从未注册任何 gateway handler** | | 结论摘要 | 代码量小且 `go vet` 通过,但核心读写链路大面积失效:分页与过滤条件因 GORM 链式未回写而全部失效、列表恒不返回标签/附件、标签列表恒为空、删除评论为空实现;删除/修改动态与互动计数接口均无归属校验;9 个 RPC 为 TODO 占位;`etc/feed_*.yaml` 字段名与 SDK 配置结构不匹配,服务启动即退出。 | ## 1. 服务定位与职责 承载用户发布动态(`feed_post`)、动态附件与标签关联、评论(`feed_comment`)、标签字典(`feed_tags`)与四类时间线(推荐/好友/关注/热门)的社交内容服务。 它是社交内容流的**读写与互动计数**服务。时间线排序与分发、好友/关注关系、推荐算法均不在本模块内实现(四类 Timeline 方法全部为占位);模块也不负责用户资料、关系链、消息推送或媒体存储。 ## 2. 代码结构与入口 | 路径 | 职责 | | --- | --- | | `cmd/main/main.go` | 独立进程入口:`config.New("Feed")` → `impl.NewImpl()` → `server.New(addr)` → `service.New` → `srv.Use(models.InitData)` → `srv.Start()` | | `cmd/cli/main.go` | 仅 `config.New("feed")` + `fmt.Println(config.Spec.Databases)` 的调试入口 | | `internal/config/config.go` | `SrvConfig`(Base、Databases、MicroService、Rpc、Gateway、APM、Etcd、WeChatConf、Token、Kyc)与校验 | | `internal/impl/impl.go` | 初始化 Memory、Redis、DB、Etcd 四个全局实例 | | `internal/server/new.go` | 创建 grpc.Server、注册 4 个 service(`protoc-gen-slc` 生成) | | `internal/server/{post,tag,timeline,setting}_server.go` | 17 个 RPC 的 1:1 转发(生成代码) | | `internal/logic/post/{fetch,create,change,remove,comment_list,add_comment,delete_comment,action}.go` | 动态 CRUD、评论、互动计数 | | `internal/logic/tag/{list,create,post_list}.go` | 标签列表/创建/话题下动态 | | `internal/logic/timeline/{recommend,friend,follow,hot}.go` | 四类时间线(全部占位) | | `internal/logic/setting/{info,rights}.go` | 设置(全部占位) | | `internal/models/{feed_post,feed_comment,feed_tags,feed_relate_tags,feed_relate_attachs,query}.go` | 5 张表的模型 + 迁移注册 + `query.go` 中全部数据访问函数 | | `service/` | **空目录**(无 `expose.go`,因此无法被聚合宿主接入) | | `pb/blocks_compat.go` | 手写类型别名文件(`PostListReply = FeedPostListReply` 等),非生成代码 | | `proto/*.proto` | 4 个 service 契约;公共消息放在 `proto/const.proto`(`package blocks`,与 service `package feed` 不同) | | `etc/{feed_dev,feed_prod,feed_test,passport_dev,passport_prod,passport_test}.yaml` | 配置;`passport_*.yaml` 的内容才是与本模块结构匹配的那一份(见 6.4) | 依赖注入:模块内无 `service.Expose`,`impl.NewImpl()` 是唯一初始化路径;`pkgs/all` 与 `pkgs/ecmall` 的 `go.mod` 均未引用本模块,全仓库对 `module/social/feed` 的引用只有 `scripts/api-docgen`(仅 import `pb`)与根 `README.md`。 ## 3. 接口清单 HTTP 路径由生成代码固定为 `/feed.{服务}/{方法}`(`pb/post.pb.gw.go:600-607`、`pb/tag.pb.gw.go:280-282`、`pb/timeline.pb.gw.go:344-347`、`pb/setting.pb.gw.go:216-217`)。注意:这些 handler 在模块内**从未被注册到任何 `ServeMux`**(见 6.4)。 | 方法 | 路径 | 功能 | 鉴权 | 实现位置 | | --- | --- | --- | --- | --- | | POST | `/feed.Post/Fetch` | 推文列表(支持 `params.key` 按标签过滤) | JWT | `logic/post/fetch.go:14` | | POST | `/feed.Post/Create` | 新建推文(含附件、标签) | JWT | `logic/post/create.go:17` | | POST | `/feed.Post/Change` | 修改推文 | JWT(无归属校验) | `logic/post/change.go:17` | | POST | `/feed.Post/Remove` | 删除推文 | JWT(无归属校验) | `logic/post/remove.go:16` | | POST | `/feed.Post/CommentList` | 评论列表 | JWT | **占位** `logic/post/comment_list.go:11` | | POST | `/feed.Post/AddComment` | 新建评论 | JWT | `logic/post/add_comment.go:18` | | POST | `/feed.Post/DeleteComment` | 删除评论 | JWT(无归属校验) | `logic/post/delete_comment.go:16` + **空实现** `models/query.go:122-125` | | POST | `/feed.Post/Action` | 点赞/点踩计数 | JWT(不校验身份来源) | `logic/post/action.go:17` | | POST | `/feed.Tag/List` | 标签列表 | JWT | `logic/tag/list.go:13`(返回值被丢弃,恒为空) | | POST | `/feed.Tag/Create` | 新建标签 | JWT | `logic/tag/create.go:15` | | POST | `/feed.Tag/PostList` | 话题相关动态 | JWT | **占位** `logic/tag/post_list.go:11` | | POST | `/feed.Timeline/Recommend` | 推荐时间线 | JWT | **占位** `logic/timeline/recommend.go:11` | | POST | `/feed.Timeline/Friend` | 好友动态 | JWT | **占位** `logic/timeline/friend.go:11` | | POST | `/feed.Timeline/Follow` | 关注动态 | JWT | **占位** `logic/timeline/follow.go:11` | | POST | `/feed.Timeline/Hot` | 热门动态 | JWT | **占位** `logic/timeline/hot.go:11` | | POST | `/feed.Setting/Info` | 修改个人信息 | JWT | **占位** `logic/setting/info.go:11` | | POST | `/feed.Setting/Rights` | 修改权限信息 | JWT | **占位** `logic/setting/rights.go:11` | > 鉴权方式:全部 17 个 logic 入口都调用 `service.ParseMetaCtx(ctx, nil)`(`git.apinb.com/bsm-sdk/core/service/meta.go:19-49`),要求有效 JWT;所有 `opts` 均为 `nil`,即**不校验角色、不校验私有 IP**。多个入口用 `_, err = ...` 丢弃 claims(如 `logic/post/fetch.go:15`),因此拿不到调用者身份。 ### 声明但未实现 / 空转的接口 | 类别 | 接口 | 证据 | | --- | --- | --- | | TODO 占位(4 个时间线) | `Timeline.Recommend` / `Friend` / `Follow` / `Hot` | `internal/logic/timeline/{recommend,friend,follow,hot}.go:26`(`// TODO: add your logic code`) | | TODO 占位 | `Tag.PostList` | `internal/logic/tag/post_list.go:26` | | TODO 占位 | `Post.CommentList` | `internal/logic/post/comment_list.go:26` | | TODO 占位(2 个) | `Setting.Info` / `Setting.Rights` | `internal/logic/setting/{info,rights}.go:18-20` | | 有路由有转发但底层空实现 | `Post.DeleteComment` | `internal/models/query.go:122-125` 函数体只有 `return`,永远返回 nil | | 有实现但返回值被丢弃 | `Tag.List` | `internal/logic/tag/list.go:32` 返回新建的空 `TagListReply`,忽略了上面组装好的 `res` | > 9 个 TODO 占位函数统一写成 `func(...) (reply *XxxReply, err error) { ...; return }`,即返回 `(nil, nil)`;gRPC 侧回空消息,gateway 侧对 nil 响应序列化会失败(表现为 500/空响应)。 ## 4. 数据模型与表 模型统一在 `init()` 中通过 `database.AppendMigrate` 注册(`internal/models/feed_post.go:20` 等为 `TableName` 方法;迁移注册在各文件内)。公共字段来自 SDK:`types.Std_IICUDS` = `id`(PK) / `identity varchar(36) uniqueIndex` / `created_at` / `updated_at` / `deleted_at`(index) / `status int8 index`;`types.Std_IDIdentity` = `id`(PK) / `identity`(uniqueIndex);`types.Std_Passport` = `passport_id`(index) / `passport_identity varchar(36)`(index)。 ### 表 `feed_post`(`internal/models/feed_post.go:8-22`) | 字段 | 类型 | 键/约束 | 说明 | | --- | --- | --- | --- | | `id` | uint | PK | 自增主键 | | `identity` | varchar(36) | uniqueIndex | 动态唯一码 | | `created_at`/`updated_at`/`deleted_at` | timestamp | index(deleted_at) | 软删除 | | `status` | int8 | index, 默认 0 | 状态(-1 禁止 / 1 正常),**查询中未使用** | | `passport_id` | uint | index | 发布者 ID | | `passport_identity` | varchar(36) | index | 发布者唯一标识 | | `content` | text | 默认 `''` | 正文 | | `is_open` | bool | 默认 true | true 公开 / false 不公开,**查询中未过滤** | | `cnt_like` | int64 | 默认 0 | 点赞数 | | `cnt_unlike` | int64 | 默认 0 | 点踩数 | | `cnt_comment` | int64 | 默认 0 | 评论数 | | `Attachs` | `[]FeedRelateAttach` | `gorm:"-"` | **非持久化**,不会自动迁移,也永远不会被 GORM 加载 | | `Tags` | `[]FeedRelateTags` | `gorm:"-"` | 同上 | ### 表 `feed_comment`(`internal/models/feed_comment.go:6-20`) | 字段 | 类型 | 键/约束 | 说明 | | --- | --- | --- | --- | | `id` / `identity` | uint / varchar(36) | PK / uniqueIndex | 主键与唯一码 | | `created_at`/`updated_at`/`deleted_at` | timestamp | index(deleted_at) | 软删除 | | `status` | int8 | index | 状态,未使用 | | `passport_id` / `passport_identity` | uint / varchar(36) | index | 评论作者;**新增评论时从不写入**(见 6.2) | | `post_identity` | varchar(36) | index | 所属动态唯一码 | | `parent_identity` | varchar(36) | index | 上级评论唯一码 | | `content` | text | 默认 `''` | 评论正文 | | `SubComment` | `[]FeedComment` | `foreignkey:parent_identity` | 子评论关联,无任何查询使用 | | `cnt_like`/`cnt_unlike`/`cnt_comment` | int64 | 默认 0 | 互动计数 | ### 表 `feed_tags`(`internal/models/feed_tags.go:8-16`) | 字段 | 类型 | 键/约束 | 说明 | | --- | --- | --- | --- | | `id` | uint | PK | 自增主键 | | `created_at`/`updated_at`/`deleted_at` | timestamp | index(deleted_at) | 软删除 | | `key` | varchar(36) | not null,**无唯一索引** | 标签键;由 `tag/create.go:26` 写入随机 UUID | | `content` | varchar(255) | not null | 标签名称 | > 该表**没有 `identity` 列**,但 `models/query.go:81` 与 `:89` 使用 `Where("identity = ?", ...)`(见 6.2 与 6.3)。 ### 表 `feed_relate_tags`(`internal/models/feed_relate_tags.go:6-15`) | 字段 | 类型 | 键/约束 | 说明 | | --- | --- | --- | --- | | `id` / `identity` | uint / varchar(36) | PK / uniqueIndex | 主键与唯一码 | | `post_identity` | varchar(36) | index | 动态唯一码 | | `key` | varchar(36) | index | 标签键 | | `Content` | string | `gorm:"-"` | **非持久化**,`create.go:46` 写入的标签名会被静默丢弃 | ### 表 `feed_relate_attach`(`internal/models/feed_relate_attachs.go:6-16`,表名单数) | 字段 | 类型 | 键/约束 | 说明 | | --- | --- | --- | --- | | `id` | uint | PK(内嵌 `Std_IDIdentity`) | 自增主键 | | `identity` | varchar(36) | uniqueIndex | 唯一码(内嵌字段与显式字段同名,见 6.4) | | `post_identity` | varchar(36) | index | 动态唯一码 | | `attach_type` | varchar(36) | index | 附件类型 | | `url` | varchar(255) | index | 附件地址 | ## 5. 核心流程 ### 5.1 发布动态 → 落库 → 列表读取 ```mermaid flowchart TD A["POST /feed.Post/Create
service.ParseMetaCtx JWT"] --> B["校验 content 非空"] B --> C["models.CreatePost 事务"] C --> D["tx.Create feed_post"] D --> E["tx.Create feed_relate_attach"] E --> F["tx.Create feed_relate_tags
Content 字段 gorm 忽略 标签名丢失"] F --> G["返回 identity"] H["POST /feed.Post/Fetch
service.ParseMetaCtx 丢弃 claims"] --> I["page_no 小于等于 0 置 1
page_size 小于等于 0 置 10"] I --> J["models.PostList
Order created_at desc"] J --> K["tx.Limit(page).Offset(...) 未回写
分页整体失效"] K --> L["Find 到 FeedPost 切片
Tags 与 Attachs 为 gorm 忽略字段"] L --> M["AssembleList 中两个循环永不执行
标签与附件恒为空"] M --> N["fmt.Print 把整页数据打到 stdout"] N --> O["无 is_open 无 status 过滤
含私密与禁用用户的动态"] ``` ### 5.2 评论与互动计数 ```mermaid flowchart TD A["POST /feed.Post/AddComment"] --> B["models.AddComment 事务"] B --> C["Create feed_comment
passport_identity 未写入 作者为空"] C --> D["Table feed_post 后 UpdateColumn
cnt_comment 加 1"] D --> E{"ParentIdentity 非空"} E -->|是| F["未指定 Model 的 UpdateColumn
目标表只能靠 GORM 内部状态推断"] G["POST /feed.Post/Action"] --> H["models.LikeAction"] H --> I{"action_type"} I -->|post| J["Model FeedPost"] I -->|comment| K["Model FeedComment"] I -->|其他| L["未设置 Model 目标表不确定"] J --> M{"action_op"} K --> M M -->|ilike| N["column 为 cnt_like 计数加 1"] M -->|unlike| O["条件误写成 action_type 等于 unlike
column 保持空串"] O --> P["UpdateColumn 空列名 SQL 错误"] Q["POST /feed.Post/DeleteComment"] --> R["models.DeleteComment 空函数
永远返回 nil"] R --> S["接口返回成功但无任何删除"] ``` ## 6. 审计发现 ### 6.1 安全 | 级别 | 位置 | 问题 | | --- | --- | --- | | **高** | `internal/logic/post/remove.go:17-29` + `internal/models/query.go:34-37` | **越权删除动态**。`Remove` 拿到 JWT 后直接用 `in.Identity` 调 `DeletePost`,模型层是 `Where("identity = ?", identity).Delete(&FeedPost{})`,**没有任何 `passport_identity` 归属条件** → 任意登录用户可删除他人动态(软删除)。 | | **高** | `internal/logic/post/change.go:17-40` + `internal/models/query.go:30-33` | **越权修改动态**。`Change` 同样只校验 `in.Identity != ""`,`ChargePost` 为 `Where("identity = ?", data.Identity).Updates(&data)` → 可篡改任意用户的动态正文与公开状态。 | | **高** | `internal/logic/post/fetch.go:15-36` + `internal/models/query.go:60-71` | **私密动态泄露**。`Fetch` 丢弃 claims,`PostList` 的查询**没有 `is_open` 条件、没有 `status` 条件、没有作者条件** → `is_open=false` 的"不公开"动态、`status=-1` 的被禁用用户动态都会被返回(`internal/models/feed_post.go:12` 明确 `false-不公开`)。 | | **高** | `internal/logic/post/action.go:17-28` + `internal/models/query.go:40-57` | **互动计数可无限刷量**。`Action` 只校验三个字符串非空,不校验调用者身份、不落库点赞人、无去重 → 任意登录用户对任意 `identity` 循环调用即可把 `cnt_like` 刷到任意值;且 `action_type` 可传任意值绕过模型绑定。 | | 中 | `internal/logic/post/delete_comment.go:16-28` | 删除评论接口只校验 `in.Identity` 非空,**不校验评论归属**(底层当前是空实现,但设计上即越权入口)。 | | 中 | `internal/logic/post/add_comment.go:23-34` + `internal/models/query.go:103-120` | 评论不记录作者:`data` 只设了 `Identity`/`PostIdentity`/`Content`/`ParentIdentity`,`Std_Passport` 全空,且模型函数签名里的 `authorIdentity` 形参**在函数体内从未被使用**(`query.go:103-120`)→ 无法做归属校验、无法展示/追溯评论人。同时不校验 `PostIdentity` 是否存在或可见,可对任意/不存在的动态灌评论。 | | 中 | `internal/logic/tag/create.go:20-36` | 任何登录用户可批量创建标签,无权限校验、无去重、无长度限制;`Key` 用 `utils.UUID()`(`create.go:26`)与标签内容无关联,标签字典的键不可读。 | | 低 | `internal/logic/post/create.go:22-24` | 只校验 `content != ""`,无长度上限、无发布频率限制、无敏感内容校验;附件 `url` 与 `attach_type` 由请求方直接写入(`create.go:32-41`)。 | ### 6.2 正确性与逻辑缺陷 | 级别 | 位置 | 问题 | | --- | --- | --- | | **高** | `internal/models/query.go:67` | **分页完全失效,且页码被当成条数**。`tx.Limit(int(page)).Offset(int((page - 1) * size))` 中第一个参数应为 `size` 却传了 `page`;更关键的是 `tx.Limit(...).Offset(...)` 的返回值**未回写 `tx`**,后续 `tx.Find` 用的是没有 Limit/Offset 的语句 → 实际返回**全表**(并受第 68 行 Count 影响)。`logic/post/fetch.go:26` 传入 `(in.PageNo, in.PageSize)` 的顺序本身是对的,缺陷在模型层。 | | **高** | `internal/models/query.go:122-125` | **`DeleteComment` 是空实现**,函数体只有 `return`,永远返回 nil → `Post.DeleteComment` 永远返回"成功",但没有任何删除动作;`logic/post/delete_comment.go:24` 的 `err` 判断因此永远不会触发。 | | **高** | `internal/models/query.go:60-71` + `internal/logic/post/fetch.go:51-63` | **列表恒不返回标签与附件**。`PostList` 只 `tx.Find(&list)`,没有 `Preload("Tags")`/`Preload("Attachs")`;而 `FeedPost.Tags`/`Attachs` 的标签是 `gorm:"-"`(`internal/models/feed_post.go:16-17`),GORM 永远不会加载它们 → `AssembleList` 里的两个 `for` 循环永不执行,`post.Tags`/`post.Attachs` 恒为空切片。 | | **高** | `internal/models/query.go:40-57` | **点踩功能不可用**。第 50 行把判断写成了 `} else if action_type == "unlike" {`(应为 `action_op == "unlike"`)→ 点踩时 `column` 保持空串,第 53 行执行 `UpdateColumn("", gorm.Expr(" + ?", 1))`,列名非法导致 SQL 错误、接口报错。另:`action_type` 非 `"post"`/`"comment"` 时第 43-47 行不会调用 `.Model(...)`,`tx` 仍是裸 `impl.DBService`,目标表不确定。 | | **高** | `internal/logic/post/change.go:25-40` + `internal/models/query.go:30-33` | **修改动态无法改为不公开,附件被静默丢弃**。`ChargePost` 用结构体 `Updates(&data)`,GORM 结构体更新会忽略零值 → `is_open=false` 永远写不进去;`change.go:31-39` 组装的 `postData.Attachs` 只放在 `gorm:"-"` 字段上,`ChargePost` 完全不处理 → 附件修改无效(且是"改了但没报错")。 | | **高** | `internal/models/query.go:81,89` | `ModifyTags`/`DeleteTags` 使用 `Where("identity = ?", ...)`,而 `FeedTags` **没有 `identity` 列**(`internal/models/feed_tags.go:8-12` 只有 `gorm.Model` + `key` + `content`)→ SQL 会因列不存在而报错。两函数当前无调用点(死代码),一旦接入即报错。 | | 中 | `internal/logic/tag/list.go:20-32` | **标签列表恒为空**。第 25-31 行已经把 `tags` 组装进 `res`,第 32 行却 `return &pb.TagListReply{}, nil` 新建了一个空对象返回 → `tags` 与 `total` 全部丢失。 | | 中 | `internal/models/query.go:68,99` | `err = tx.Find(&list).Count(&cnt).Error`:`Find` 与 `Count` 共用同一链,`Count` 会重新构建并执行一次统计查询,`Find` 的错误可能被 `Count` 的结果覆盖,`RowsAffected`/总数语义不清;`TagsList`(`query.go:96-101`)同样写法,且具名返回值 `list` 未被赋值。 | | 中 | `internal/models/query.go:63-64` | 标签过滤构造不可靠:`tx.Joins("join feed_relate_tags on ...", impl.DBService.Where(&FeedRelateTags{Key: tag}))` 把一个 `*gorm.DB` 当作 `Joins` 的参数值传入(应为 `?` 参数);紧接着第 64 行又用未限定表名的 `Where("key = ?", tag)` 二次过滤 → 生成的 SQL 与预期语义不一致(该分支同时因分页失效而难以验证)。 | | 中 | `internal/models/query.go:103-120` | `AddComment` 事务内链式顺序颠倒:第 106 行 `tx.Create(comment).Table("feed_post").Where(...).UpdateColumn(...)` 先插入评论、再切换目标表去更新帖子计数,与注释"添加评论,更新文章评论量"的书写顺序相反;第 112 行对"被评论的评论"更新时**未指定 `Model`/`Table`**,目标表只能由 GORM 内部语句状态决定(可能落到 `feed_comment` 自身的 `cnt_comment`),无法从代码确证。 | | 中 | `internal/logic/post/fetch.go:19-24`、`internal/logic/timeline/*.go:19-24`、`internal/logic/tag/post_list.go:19-24`、`internal/logic/post/comment_list.go:19-24` | `page_size` 只做了下限兜底、**没有上限**:`if in.GetPageSize() <= 0 { in.PageSize = 10 }`(fetch)与 `if in.GetPageSize() < 10 { in.PageSize = 50 }`(其余)两种语义还不一致(后者把传 5 放大成 50,传 20 却保持 20);配合分页失效可一次要求全表。 | | 中 | `internal/logic/post/add_comment.go:23-30` | 允许 `in.PostIdentity` 为空串(无任何校验)→ 可写入 `post_identity` 为空的孤儿评论;同时 `in.Identity` 被当作 `ParentIdentity`,但未校验该父评论是否属于同一 `post_identity`。 | | 中 | `internal/logic/post/fetch.go:26` | 标签过滤参数取自 `in.Params["key"]`,键名是硬编码的魔法字符串,与 `TagItem.Key` 语义耦合;传其他键(如 `tag`)会被静默忽略。 | | 低 | `internal/models/query.go:11` | `InitData()` 直接 `return nil`,而 `cmd/main/main.go:38` 仍以 `srv.Use(models.InitData)` 注册(空初始化)。 | | 低 | `internal/models/feed_relate_attachs.go:6-12` | `FeedRelateAttach` 内嵌 `types.Std_IDIdentity`(已含 `identity varchar(36) uniqueIndex`)后又显式声明 `Identity string`,**同名列遮蔽**;实际生效的是显式字段,但列定义重复声明,依赖 GORM 的字段冲突消解规则。表名 `feed_relate_attach` 为单数,与 `feed_relate_tags`、`feed_post` 命名风格不一致。 | ### 6.3 未完成实现 | 类别 | 内容 | 证据 | | --- | --- | --- | | TODO 占位 RPC(9 个) | `Timeline.Recommend`/`Friend`/`Follow`/`Hot`、`Tag.PostList`、`Post.CommentList`、`Setting.Info`、`Setting.Rights` | `internal/logic/timeline/{recommend,friend,follow,hot}.go:26`、`internal/logic/tag/post_list.go:26`、`internal/logic/post/comment_list.go:26`、`internal/logic/setting/{info,rights}.go:18-20` | | 空实现函数 | `models.DeleteComment`(只有 `return`) | `internal/models/query.go:122-125` | | 空实现函数 | `models.InitData`(只有 `return nil`) | `internal/models/query.go:10-12` | | 无调用点的死函数 | `models.ModifyTags`、`models.DeleteTags`(且 SQL 列名不存在,见 6.2) | `internal/models/query.go:80-94` | | 未接入聚合 | `service/` 为空目录,无 `Expose`/`Dependencies`;`pkgs/all`、`pkgs/ecmall` 的 `go.mod` 均未引用本模块 | 全仓库对 `module/social/feed` 的引用只有 `scripts/api-docgen/go.mod:18,47` 与根 `README.md:218` | | gateway 未接线 | 模块内无任何 `RegisterPostHandlerServer` 等调用,`Server.Mux` 也从未被赋值 | `internal/server/new.go:16-36`(`grep RegisterPostHandlerServer module/social/feed` 无匹配) | | 配置文件名错位 | `etc/passport_dev.yaml`/`passport_prod.yaml`/`passport_test.yaml` 的内容(`Service: feed`、含 `Databases`/`MicroService`/`WeChatConf`/`Kyc`/`Token`)才是与本模块匹配的配置,但 `conf.New` 按 `feed_.yaml` 取名(SDK `conf/new.go:35-36`)→ 这三个文件**永远不会被加载** | `etc/passport_prod.yaml:1-2`、`internal/config/config.go:15-26`、SDK `git.apinb.com/bsm-sdk/core/conf/new.go:35-42` | | README 为空壳 | `module/social/feed/README.md` 全文只有标题 `# content` | `README.md:1` | ### 6.4 健壮性与可维护性 | 级别 | 位置 | 问题 | | --- | --- | --- | | **高** | `etc/feed_dev.yaml:1-4`、`etc/feed_prod.yaml:1-4`、`etc/feed_test.yaml:1-4` | 三个 feed 配置都用 `Name`/`ListenOn`/`Dsn`,而 `SrvConfig` 只识别 `Service`/`Port`/`Databases`(`internal/config/config.go:17-18`)。SDK 在 YAML 解析前先检查文件是否含子串 `"Service:"`(`git.apinb.com/bsm-sdk/core/conf/new.go:58-60`),这三个文件只含 `ServiceName:` → 直接 `log.Fatalln("ERROR: Service Not Nil")` 退出;即使能加载,`Spec.Service` 仍为空,`conf.NotNil`(`config.go:56`)同样致命。 | | **高** | `internal/impl/impl.go:24` + `etc/feed_*.yaml` | 三个 feed 配置均无 `Databases` 段(用的是 `Dsn`),`with.Databases(nil, nil)` 会直接 `panic("No Database Source Found !")`(`git.apinb.com/bsm-sdk/core/with/databases.go:13-15`)。 | | **高** | `etc/feed_dev.yaml:16-19`、`feed_prod.yaml:15-18`、`feed_test.yaml:16-19` | 匿名清单写成顶层 `Anonymous: {Key, Urls}`,而 SDK 的匿名清单在 `conf.MicroServiceConf.Anonymous`(`conf/types.go:25`)→ `Anonymous` 整块被忽略;三个配置也都没有 `MicroService` 段,`MsConf` 为 nil → `service.Start` 跳过 etcd 注册(SDK `service/service.go:66`)。`passport_prod.yaml:15` 的匿名项还写成 `feed.ping.hello`(模块内不存在此方法)。 | | 中 | `internal/server/new.go:16-36`、`cmd/main/main.go:33` | 既没有 `gwRuntime.NewServeMux()`,也没有 `pb.RegisterXxxHandlerServer` 调用,`Server.Mux` 恒为 nil → `cmd/main/main.go:33` 把 nil 传给 `GatewayMux`,`service.Start` 里 `http.ListenAndServe(addr, nil)` 走 `DefaultServeMux`,所有 `/feed.*` 路径 404。模块自身也无法像 wallet 那样通过 `service.Expose` 被聚合宿主接线。 | | 中 | `etc/feed_prod.yaml:4`、`etc/feed_dev.yaml:4` | 数据库连接串明文提交入库,含生产库账号与口令(`postgres://prod:MakeW2023~PROD@192.168.0.224:5432/scf`)、开发库地址(`47.108.57.74:5432/milu`)。 | | 中 | `etc/supervisor.bsm-apps-passport.conf:1` | 文件名仍是 passport,内容 program 名却是 `bsm-apps-feed`;与 `etc/passport_*.yaml` 一并说明该目录是从 passport 服务复制而来,未清理。 | | 中 | `etc/passport_prod.yaml:31-35` | 同一份配置里同时存在 `Kyc:` 与 `KycConf:` 两个段,而 `config.SrvConfig` 只映射 `Kyc`(`internal/config/config.go:25`)→ `KycConf` 整块为死配置。 | | 中 | `internal/impl/impl.go:21-24` | `MemorySerice`、`RedisService` 被初始化,但全模块无任何使用点(死代码);`EtcdService` 仅在微服务注册路径使用,而 `MsConf` 恰好为 nil。 | | 低 | `internal/models/query.go:69` | 残留调试语句 `fmt.Print("posts", list)`,会把整页动态实体(含 content)打印到进程标准输出。 | | 低 | `cmd/cli/main.go:9-12` | CLI 入口只做 `config.New("feed")` 并 `fmt.Println(config.Spec.Databases)`,调试残留;且服务键传小写 `"feed"`,与主入口的 `ServiceKey = "Feed"` 不一致(`conf.New` 拼文件名时会 `ToLower`,但 `vars.ServiceKey` 保留原样,影响日志/注册键大小写)。 | | 低 | 全模块 | 0 个 `*_test.go`;`test/` 下只有一个**空的** `lint/` 目录,无任何请求样例。 | | 低 | `internal/logic/post/fetch.go:39-67` | `AssembleList` 是包级导出函数但只在本文件使用,且内部对 `res` 使用具名返回追加(`res = append(res, post)`),在 data 为空时返回 nil 切片而非空切片,前端需要额外判空。 | ## 7. 风险汇总 | 编号 | 级别 | 问题 | 影响面 | | --- | --- | --- | --- | | F1 | 高 | 删除动态无归属校验,可删他人动态 | 数据完整性、越权 | | F2 | 高 | 修改动态无归属校验,可篡改他人动态 | 数据完整性、越权 | | F3 | 高 | 列表无 `is_open`/`status` 过滤,私密动态泄露 | 隐私泄露 | | F4 | 高 | 互动计数无去重、无身份绑定,可无限刷量 | 数据可信性 | | F5 | 高 | 分页 `Limit(page)` + 未回写 → 分页失效并全表返回 | 功能不可用 + 性能风险 | | F6 | 高 | `DeleteComment` 空实现,删除接口永远"成功" | 功能不可用 | | F7 | 高 | 列表恒不返回标签与附件(`gorm:"-"` + 无 `Preload`) | 功能不可用 | | F8 | 高 | 点踩计数条件写错导致 SQL 列名为空 | 功能不可用 | | F9 | 高 | 修改动态无法改为不公开,附件修改被静默丢弃 | 功能不正确 | | F10 | 高 | `feed_*.yaml` 字段名不匹配 → 启动即退出;无 `Databases` → panic | 部署可用性 | | F11 | 高 | gateway handler 从未注册、`Mux` 为 nil、无 `service.Expose` | 接口不可用、无法聚合 | | F12 | 高 | `feed_tags` 无 `identity` 列却被 `Where("identity")` 使用 | 接入即报错 | | F13 | 中 | 评论不记录作者、不校验所属动态 | 越权、数据不可追溯 | | F14 | 中 | 标签列表返回值被丢弃,恒为空 | 功能不可用 | | F15 | 中 | `AddComment` 事务内 `Table` 用法与未指定 Model 的更新 | 结果不确定、可能整事务回滚 | | F16 | 中 | 9 个 RPC 为 TODO 占位,返回 `(nil, nil)` | 功能缺失、网关异常 | | F17 | 中 | 生产/开发数据库连接串明文入库 | 凭据泄露 | | F18 | 中 | `page_size` 无上限、上下限语义不一致 | 资源滥用 | | F19 | 低 | 死代码、调试 `fmt.Print`、无测试、README 空壳 | 可维护性 | ## 8. 修复建议(务实项) 1. **补归属校验**(F1/F2/F13):`Remove`/`Change` 在 logic 层先按 `identity + passport_identity` 查一次动态,查不到即返回"无权操作";或把模型函数改为 `DeletePost(identity, passportIdentity)`、`ChargePost(data, passportIdentity)` 并加上 `Where("passport_identity = ?", ...)`。评论同理,把 `AddComment` 的 `authorIdentity` 真正写进 `FeedComment.Std_Passport`,删除评论按 `identity + passport_identity` 定位。 2. **修可见性过滤**(F3):`PostList` 增加 `is_open = true` 与作者本人可见的 `OR` 条件,并按需过滤 `status <> -1`;`Fetch` 不要丢弃 claims,用调用者身份决定是否可见私密动态。 3. **修分页**(F5/F18):`query.go:67` 把 `Limit(int(page))` 改为 `Limit(int(size))`,并把 `tx = tx.Limit(...).Offset(...)` 回写;给 `page_size` 设上限(如 50),并把 `fetch.go`、`comment_list.go`、四个 timeline、`tag/post_list.go` 的兜底逻辑统一为 `<= 0 则取默认值`。 4. **修删除与计数**(F6/F8):`DeleteComment` 补实现(软删除 + 同事务把对应 `feed_post.cnt_comment` 减 1,并做下限保护);`LikeAction` 的第 50 行改为 `action_op == "unlike"`,`action_type` 用白名单(`post`/`comment`)校验并显式设置 `Model`,`column` 为空时直接返回参数错误。 5. **修标签与附件读写**(F7/F9/F12/F14):`PostList` 用 `Preload("Tags")`/`Preload("Attachs")` 之前,需先把 `FeedPost.Tags`/`Attachs` 的 `gorm:"-"` 去掉或改写为显式二次查询(按 `post_identity IN (...)` 批量取关联表再回填);`FeedRelateTags.Content` 去掉 `gorm:"-"` 才能真正存标签名;`Change` 用 `Select("content","is_open").Updates(...)` 或 map 更新以写入 `is_open=false`,并显式处理附件的增删;`FeedTags` 的 `Where("identity = ?")` 改为按主键或 `key` 定位;`tag/list.go:32` 改为 `return res, nil`。 6. **配置**(F10):把 `etc/feed_dev.yaml`/`feed_prod.yaml`/`feed_test.yaml` 按 `SrvConfig` 重写(`Service`/`Port`/`Databases`/`Cache`/`MicroService.Anonymous`/`Gateway`),或直接把已正确的 `etc/passport_*.yaml` 重命名为 `feed_*.yaml` 并删除冗余文件;`config.New` 里补 `conf.NotNil(Spec.Databases)`;把 `Dsn` 中的口令改为环境变量注入并轮换已泄露的生产口令。 7. **接线与启动**(F11):在 `internal/server/new.go` 中给 `Mux` 赋 `gwRuntime.NewServeMux()` 并注册 4 组 handler,或在 `service/` 下补一份与 wallet 同构的 `expose.go`/`dependencies.go` 以便接入 `pkgs/all`;否则 `Gateway.Enable=true` 只会得到一个 404 网关。 8. **补初始化与占位反馈**(F16):`models.InitData` 要么实现必要的基础数据,要么在 `cmd/main/main.go:38` 去掉 `srv.Use(models.InitData)`;9 个 TODO RPC 在实现前应返回明确的 `Unimplemented` 错误,而不是 `(nil, nil)`。 9. **清理**(F19):删除 `internal/models/query.go:69` 的 `fmt.Print`、`internal/models/query.go:80-94` 的 `ModifyTags`/`DeleteTags`(无调用点且 SQL 错误)、`internal/impl/impl.go` 中未使用的 Redis/Memory 初始化、`etc/passport_prod.yaml:28-33` 的 `KycConf` 死配置;`README.md` 至少补一段服务说明。 10. **测试**:为 `PostList`(分页与可见性过滤)、`LikeAction`(点赞/点踩各一次)、`DeleteComment`、`AddComment`(作者落库与计数一致)各写一条正例 + 一条边界用例。 > 本报告只列出与现有实现直接相关的修复项。为 feed 引入"统一内容框架""时间线抽象层""互动事件总线""DTO/VO 分层"一类改造不在建议范围内——上述 10 条都是对现有函数与配置的最小修正。 ## 9. 整改记录(2026-09-22) > 本节记录按本报告结论执行的代码整改。整改遵循**最小修正**原则:未引入新框架、抽象层、DTO/VO、事件总线,未拆分服务边界,**未修改任何 `proto/*.proto` 与生成的 `pb/*.go`**;新增/修改注释均为中文;口令类摘要统一使用 bcrypt(验证码等短时效一次性凭证仍按原有 Redis 明文比对链路存储)。校验方式:`GOWORK=off go build ./...` + `GOWORK=off go vet ./...` + `gofmt -l`(仓库根 workspace 模式存在 genproto 拆包的 ambiguous import,属本机既有问题)。 | 编号 | 级别 | 问题 | 处理结果 | | --- | --- | --- | --- | | F1 | 高 | 删除动态无归属校验 | 已修复:删除条件加 `passport_id`/`passport_identity = auth.Identity` | | F2 | 高 | 修改动态无归属校验 | 已修复:同上,越权返回 `ErrPermissionDenied` | | F3 | 高 | 列表无 `is_open`/`status` 过滤,私密动态泄露 | 已修复:列表查询过滤公开状态,他人不可见私密动态 | | F4 | 高 | 互动计数无去重、无身份绑定,可无限刷量 | 已修复:改为条件更新并加计数保护。**完整去重需要新增记录表/字段,属契约外,未做** | | F5 | 高 | 分页 `Limit(page)` 且未回写,分页失效并全表返回 | 已修复:改为 `Limit(page_size).Offset((page-1)*page_size)`,并归一化 `page`/`page_size` | | F6 | 高 | `DeleteComment` 空实现,删除接口永远成功 | 已修复:实现真实删除并校验评论归属 | | F7 | 高 | 列表恒不返回标签与附件 | 已修复:补 `Preload` 装配 tags/attachs | | F8 | 高 | 点踩计数条件写错导致 SQL 列名为空 | 已修复:修正列名/字段名 | | F9 | 高 | 修改动态无法改为不公开、附件变更被静默丢弃 | 已修复:允许 `is_open` 置 false(不被零值过滤吞掉),附件变更真正落库 | | F10 | 高 | `feed_*.yaml` 字段名不匹配导致启动即退出、`Databases` 缺失即 panic | 已修复:按 SDK 规范重写 `etc/*.yaml`(顶层 `Service`/`Port`/`Databases(Driver/Source)`/`Cache`)并补 `Etcd` 段 | | F11 | 高 | gateway handler 从未注册、`Mux` 为 nil、无 `service.Expose` | 已修复:照 `module/social/group`/`relation` 的既有接线方式初始化 `gwRuntime.NewServeMux()`、注册 gateway handler 并补 `service/expose.go`、`service/dependencies.go` | | F12 | 高 | `feed_tags` 无 `identity` 列却被 `Where("identity")` 使用 | 已修复:按模型实际列名修正 | ### 未纳入本轮范围 报告中「中」「低」级别的项(分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。 > 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。