35 KiB
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 发布动态 → 落库 → 列表读取
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 评论与互动计数
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 占位 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_<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. 修复建议(务实项)
- 补归属校验(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定位。 - 修可见性过滤(F3):
PostList增加is_open = true与作者本人可见的OR条件,并按需过滤status <> -1;Fetch不要丢弃 claims,用调用者身份决定是否可见私密动态。 - 修分页(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 则取默认值。 - 修删除与计数(F6/F8):
DeleteComment补实现(软删除 + 同事务把对应feed_post.cnt_comment减 1,并做下限保护);LikeAction的第 50 行改为action_op == "unlike",action_type用白名单(post/comment)校验并显式设置Model,column为空时直接返回参数错误。 - 修标签与附件读写(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。 - 配置(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中的口令改为环境变量注入并轮换已泄露的生产口令。 - 接线与启动(F11):在
internal/server/new.go中给Mux赋gwRuntime.NewServeMux()并注册 4 组 handler,或在service/下补一份与 wallet 同构的expose.go/dependencies.go以便接入pkgs/all;否则Gateway.Enable=true只会得到一个 404 网关。 - 补初始化与占位反馈(F16):
models.InitData要么实现必要的基础数据,要么在cmd/main/main.go:38去掉srv.Use(models.InitData);9 个 TODO RPC 在实现前应返回明确的Unimplemented错误,而不是(nil, nil)。 - 清理(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至少补一段服务说明。 - 测试:为
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,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。