Files
full/docs/feed.md
2026-09-22 21:15:34 +08:00

318 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 只打印配置 |
| 对外协议 | gRPC4 个 service / 17 个 RPCgateway 路由定义为 `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<br/>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<br/>Content 字段 gorm 忽略 标签名丢失"]
F --> G["返回 identity"]
H["POST /feed.Post/Fetch<br/>service.ParseMetaCtx 丢弃 claims"] --> I["page_no 小于等于 0 置 1<br/>page_size 小于等于 0 置 10"]
I --> J["models.PostList<br/>Order created_at desc"]
J --> K["tx.Limit(page).Offset(...) 未回写<br/>分页整体失效"]
K --> L["Find 到 FeedPost 切片<br/>Tags 与 Attachs 为 gorm 忽略字段"]
L --> M["AssembleList 中两个循环永不执行<br/>标签与附件恒为空"]
M --> N["fmt.Print 把整页数据打到 stdout"]
N --> O["无 is_open 无 status 过滤<br/>含私密与禁用用户的动态"]
```
### 5.2 评论与互动计数
```mermaid
flowchart TD
A["POST /feed.Post/AddComment"] --> B["models.AddComment 事务"]
B --> C["Create feed_comment<br/>passport_identity 未写入 作者为空"]
C --> D["Table feed_post 后 UpdateColumn<br/>cnt_comment 加 1"]
D --> E{"ParentIdentity 非空"}
E -->|是| F["未指定 Model 的 UpdateColumn<br/>目标表只能靠 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<br/>column 保持空串"]
O --> P["UpdateColumn 空列名 SQL 错误"]
Q["POST /feed.Post/DeleteComment"] --> R["models.DeleteComment 空函数<br/>永远返回 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 占位 RPC9 个) | `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_<mode>.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`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。