Files
full/docs/audit/module-base-cms.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

61 KiB
Raw Permalink Blame History

审计报告module/base/cms

1. 模块概览

  • 模块路径module/base/cmsbsm/full/module/base/cmsGo 1.26.5gRPC + grpc-gateway 微服务。
  • 服务清单proto/Post19 个 RPC文章 CRUD/搜索/评论 CRUD/文章与评论的赞踩)、Pages6Category4Tags4Site5
  • 关键目录cmd/{main,cli}etc/{cms_dev,cms_prod,cms_test}.yamlinternal/{config,impl,logic,models,server,utils}pb/生成代码17 个文件)、proto/6 个)、service/{dependencies,expose}.gotest/{category,comment,pages,post,tags}32 个 .http无任何 Go 测试文件)。
  • 运行方式:两种。
    1. 独立进程:cmd/main/main.goconfig.New("cms") + impl.NewImpl() + server.New(nil) + SDK service.New(...).Start()(聚合配置 services: [cms])。
    2. 聚合进程:pkgs/all/internal/service/cms.go:9 exposeCMS() 调用本模块 service.Expose(),把 5 个服务的 gRPC 与 gateway 处理器挂到聚合 Server 上(service/expose.go:20-38)。
  • 依赖设施internal/impl/impl.go 注入 DBService(*gorm.DB)RedisServiceMemoryServiceEtcdServiceservice/dependencies.go 由宿主注入。
  • 数据模型cms_postcms_pagescms_categorycms_tagscms_commentcms_accessorycms_relate_categorycms_relate_tagscms_siteinternal/models/*.go,均通过 database.MigrateTables 注册 AutoMigrate

2. 审计范围与方法

已读文件(全部非 pb 的 Go 文件 + proto + etc + README + test 抽样)

  • 入口/装配:cmd/main/main.gocmd/cli/main.gointernal/config/config.gointernal/impl/impl.gointernal/server/{new,post_server,category_server,pages_server,tags_server,site_server}.goservice/{expose,dependencies}.go
  • 模型:internal/models/{query,cms_post,cms_pages,cms_category,cms_tags,cms_comment,cms_site,cms_accessory,cms_relate_category,cms_relate_tags}.go
  • 逻辑:internal/logic/post/19 个文件全部)、internal/logic/pages/7 个)、internal/logic/category/4 个)、internal/logic/tags/4 个)、internal/logic/site/5 个,全部为空实现)、internal/utils/string.go
  • 契约:proto/{post,pages,category,tags,site,const}.protopb/*.pb.gw.go 的 URL pattern 抽样(post.pb.gw.go:1321-1322site.pb.gw.go:410-412)。
  • 配置:etc/cms_{dev,prod,test}.yamletc/supervisor.bsm-apps-cms.confREADME.md679 行)。
  • 宿主/相邻模块(用于判定鉴权边界):pkgs/all/internal/service/cms.gopkgs/all/internal/server/{server,authorization}.gopkgs/all/etc/default_dev.yamlpkgs/ecmall/internal/service/cms.go

执行的命令与结果

命令 结果
Get-ChildItem -Recurse -File 非 pb 文件 108 个pb 生成文件 17 个
gofmt -l .(模块目录) 无输出 → 全部文件已格式化
go vet ./...(模块目录,后台执行) exit code: 0,无任何告警
Get-ChildItem -Include *_test.go 0 个,模块无任何 Go 单元测试
`Select-String 'RedisService MemoryService
`grep 'WithContext Begin()
$GOMODCACHE/gorm.io/gorm@v1.31.2/clause/limit.go:16-19 确认 Limit(0) 会生成 LIMIT 0(用于判定评论列表缺陷)
D:\work\bsm-sdk\core\service\{meta,service}.go 确认 ParseMetaCtx 仅解析 JWT、service.New 不给 gRPC Server 加拦截器

未覆盖/无法验证

  • pb/*.go 生成代码细节(仅抽查 gateway patternswagger/scripts/DockerfileMakefile 在模块内不存在README 声称存在)。
  • 运行时行为、真实数据库 schemacms_post 是否存在遗留 cms 列只能从模型推断)、前端是否为富文本渲染 HTMLXSS 影响面为推测)。
  • etc/cms_prod.yaml 与线上实际投放配置的差异。

3. 问题清单

P0

1. 全模块写接口无归属/角色校验任意已认证用户可修改、删除他人文章与评论IDOR

  • 位置
    • module/base/cms/internal/logic/post/modify.go:16,43
    • module/base/cms/internal/logic/post/delete.go:29
    • module/base/cms/internal/models/query.go:207
    • module/base/cms/internal/logic/post/modify_comment.go:33
    • module/base/cms/internal/logic/post/delete_comment.go:24
    • module/base/cms/internal/models/query.go:306,312
    • module/base/cms/internal/logic/{category/modify.go:16,tags/modify.go:17,tags/delete.go:16,pages/modify.go:21,pages/delete.go:19}
    • 鉴权边界:pkgs/all/internal/server/authorization.go:42-53pkgs/all/etc/default_dev.yaml:24-43
  • 证据
// internal/logic/post/delete.go:29
err = models.DeletePost(in.Identity, "") // Todo:
// internal/models/query.go:207-211  —— authorIdentity 参数声明后从未使用
func DeletePost(identity, authorIdentity string) (err error) {
	return impl.DBService.Transaction(func(tx *gorm.DB) error {
		if err = tx.Where("identity = ?", identity).Delete(&CmsPost{}).Error; err != nil {
// internal/logic/post/modify_comment.go:17,33 —— 解析结果被丢弃,仅按 identity 更新
_, err := service.ParseMetaCtx(ctx, nil)
...
if err := impl.DBService.Where("identity = ?", data.Identity).Updates(data).Error; err != nil {
// pkgs/all/internal/server/authorization.go:42-51 —— 只校验 JWT 签名/有效期,无角色
func (a *authorization) unaryInterceptor(...) { if !a.isAnonymous(info.FullMethod) { ... a.validate(values[0]) ... } }
  • 影响:聚合模式下 /cms.* 全部方法只要求"任意一个有效 JWT"default_dev.yaml:24-43 的 Anonymous 白名单不含任何 cms. 项,但同一 HS256 密钥被 passport/mall 等所有模块共用)。因此任意 C 端用户 token 即可:改写/删除任意文章(含他人文章)、改写/删除任意评论(ModifyComment 甚至允许改 post_identity/parent_id 造成评论挂载错乱)、改删任意分类/标签/页面。模块内所有 service.ParseMetaCtx(ctx, nil)opts 均为 nil,从不做角色(RoleValue)校验;PostItem.owner_identity/author_identity 从不参与写条件。同类读方向问题:post.Fetch 声明了 author_identity 过滤参数却从不使用(internal/logic/post/fetch.go:24 未传该字段),调用方以为拿到"我的文章"实为全量。
  • 建议:在 models 写路径强制带上属主条件(WHERE identity = ? AND author_identity = ?ParseMetaCtx 传入 &service.ParseOptions{RoleValue: "admin"} 或新增管理端角色校验拦截器;为 CMS 的写方法单独引入 admin 网关前缀/独立 token 受众aud避免复用 C 端密钥;删除 DeletePost 的占位空参。

P1

2. Site 服务 5 个接口全部为空实现Create/Modify/Delete 返回"假成功"

  • 位置module/base/cms/internal/logic/site/{create.go:20-27,modify.go:21-28,delete.go:27-32,fetch.go:18-22,get.go:24-26};注册位置 module/base/cms/internal/server/new.go:36module/base/cms/service/expose.go:32module/base/cms/pb/site.pb.gw.go:410-412
  • 证据
// internal/logic/site/create.go:20-27 —— 无任何 DB 调用,直接返回成功
	// TODO: valid code
	// TODO: add your logic code & delete this line.
	return &pb.StatusReply{Details: vars.OK, Timeseq: time.Now().UnixMilli()}, nil
// internal/logic/site/get.go:24-26 —— 只做参数非空校验后返回零值
	// TODO: add your logic code & delete this line.
	return
// internal/logic/site/delete.go:23-31 —— 仅校验 id/identity 非空,未删除任何数据
	if in.Id == 0 && in.Identity == "" { return nil, errcode.ErrInvalidArgument }
  • 影响cms.Site/Create|Modify|Delete 已注册到 gRPC 与 gatewayURL /cms.Site/*),调用方收到 details=OK没有任何数据落库,是静默数据丢失(比报错更危险:前端会提示保存成功);Fetch 返回 nil(空响应体,无错误);Get 返回零值 SiteItem 且无 NotFound。同时 models.CmsSiteinternal/models/cms_site.go:24-26)已注册 AutoMigrate表会被建出来但永远无数据。site_identity 是本模块多站点隔离的唯一标识Site 不可用意味着站点元数据domain/seo/theme/configs无管理入口。
  • 建议:要么补齐 5 个方法的实现(含 site_identity 唯一性、默认站点保护、删除级联校验),要么从 proto/site.proto + expose.go + new.go 中移除该服务,避免暴露假成功接口;在补齐前先让 Create/Modify/Delete 返回 codes.Unimplemented

3. Post.Search 使用不存在的列 Cms,接口必然失败

  • 位置module/base/cms/internal/logic/post/search.go:37;模型定义 module/base/cms/internal/models/cms_post.go:20-69
  • 证据
// search.go:37
tx := impl.DBService.Model(&models.CmsPost{}).Where("title like ? or Cms like ?", "%"+in.Keyword+"%", "%"+in.Keyword+"%")
// models/cms_post.go:31 —— 正文列名为 content模型中不存在 Cms 列
	Content     string `gorm:"column:content;type:text;default:'';" json:"content"`
  • 影响CmsPost 的全部字段映射(cms_post.go:23-68)中没有 CmsAutoMigrate 建出的表只有 content。Postgres 中 Cms 折叠为 cms,查询报 column "cms" does not exist42703逻辑层统一吞成 errcode.ErrDB/cms.Post/Search 恒失败。(残留可能性:若线上表由更早版本模型迁移且遗留 cms 列则不会报错——此点为推测,需核对线上 \d cms_post。)
  • 建议:改为 title like ? or content like ?;补一条 Search 的集成测试;后续对 string 类型的关键字搜索考虑 pg_trgm/全文索引替代前后 % 模糊。

4. Post.Search 结果切片先 make(len)append,返回大量 null 且数量翻倍

  • 位置module/base/cms/internal/logic/post/search.go:43-45,70-73
  • 证据
// search.go:43-45
replyData := make([]*pb.PostItem, len(data))
for _, val := range data {
	replyData = append(replyData, &pb.PostItem{
  • 影响len(data)=N 时返回 2N 个元素,前 N 个是 nil 指针 → JSON 序列化为 null,前端渲染/遍历必然 NPE 或出现空行;Count 与实际数据条数不再对应。对比正确写法见 internal/logic/post/fetch.go:31make([]*pb.PostItem, 0))。
  • 建议:改为 make([]*pb.PostItem, 0, len(data));对返回切片做非空断言的单测。

5. 评论列表分页参数校验完全失效:缺省请求返回空列表

  • 位置module/base/cms/internal/logic/post/comment_list.go:18-19,34-46
  • 证据
// comment_list.go:18-19 —— 先把未校验的值拷进局部变量
page = in.GetPage()
size = in.GetSize()
// comment_list.go:34-42 —— 之后 clamp 的是 in.Page/in.Size局部变量不受影响
if in.GetPage() < 1 { in.Page = 1 }
if in.GetSize() < 10 { in.Size = 10 }
// comment_list.go:44-46 —— 查询用的是未 clamp 的 page/size
... .Count(&cnt).Limit(int(size)).Offset(int((page - 1) * size)).Find(&list)
  • 影响:客户端不传 page/sizeproto 默认 0size=0GORM 会生成 LIMIT 0(已核对 gorm@v1.31.2/clause/limit.go:16-19*Limit>=0 即写入),评论列表恒为空count 正常,前端表现为"有评论数但列表空白"page=0,size=10Offset 为 -10被 GORM 静默丢弃(limit.go:20 Offset > 0 才写入),分页回到第一页。第 34-42 行的三处 clamp 是死代码。
  • 建议:先 clamp 再赋值(page := in.GetPage(); if page < 1 { page = 1 }),或直接在 models 层统一做默认值;为 page/size 加边界单测0/负数/超大)。

6. ModifyPost 写入损坏的附件数据,并且未传关联数组时会清空全部关联(数据丢失)

  • 位置module/base/cms/internal/models/query.go:162-169,170-203;同模式:module/base/cms/internal/logic/pages/modify.go:68-75,76-101
  • 证据
// models/query.go:162-169 —— 附件在事务外构造post.ID 来自 logic 层新建的结构体(恒为 0FilePath 被写成 identity
	for _, v := range accessory {
		accessoryPath = append(accessoryPath, CmsAccessory{
			Std_Identity: types.Std_Identity{Identity: utils.UUID()},
			PostId:       post.ID,
			PostIdentity: identity,
			FilePath:     v})
	}
// internal/logic/post/modify.go:23-43 —— data 为新建结构体,未设置 IDaccessory 实参是 AccessoryIdentityArray
	var data = &models.CmsPost{ ... }
	err = models.ModifyPost(in.Identity, data, in.AccessoryIdentityArray, in.CategoryIdentityArray, in.TagsIdentityArray)
// models/query.go:175-201 —— 先删全部旧关联,仅当入参非空才重建
	if err := tx.Where("post_identity = ?", identity).Delete(&CmsAccessory{}).Error; err != nil { ... }
	if err := tx.Where("post_identity = ?", identity).Delete(&CmsRelateCategory{}).Error; err != nil { ... }
	...
	if len(category) != 0 { if err := tx.Create(&categoryData).Error; err != nil { ... } }
  • 影响:两个必然/高概率缺陷叠加:
    1. PostId 恒为 0update 时 structure 没有 ID→ 附件与文章的整型外键断开,而 CmsPost.Accessories 的关联键是 foreignkey:PostIdmodels/cms_post.go:59),因此文章详情/列表再也 preload 不到这批附件;FilePath 被写成附件 identityin.AccessoryIdentityArray 是 identity 列表,不是路径),附件显示为随机 IDTitle 丢失。
    2. 关联重建是"先全删、后按入参重建":只要调用方只改标题、不带 category_identity_array/tags_identity_array/accessory_identity_arrayproto 中均为可选字段),文章的全部分类、标签、附件关联被清空且不报错。 页面侧同源缺陷:internal/logic/pages/modify.go:71PagesId: pages.ID(同一 data 无 ID:81-99 同样先删后条件重建。
  • 建议ModifyPost/ModifyPagesidentity 反查真实记录 ID 后再构造附件;把 accessory 入参改为 []*pb.AccessoryItem(保留 file_path/title关联重建改为显式语义nil=不改动,空切片=清空),或使用"差集更新";补一条"仅改标题后分类/标签保持不变"的回归测试。

7. 文章详情 GetByIdentity 标签数据全为空、分类数据完全缺失

  • 位置module/base/cms/internal/logic/post/get_by_identity.go:57-80module/base/cms/internal/models/query.go:242
  • 证据
// models/query.go:242 —— 只 preload 一层Categories/Tags 是关联表,未加载其 Category/Tags 子对象
if err := tx.Model(&CmsPost{}).Preload("Accessories").Preload("Categories").Preload("Tags").
	Where(key+" = ?", val).First(post).Error; err != nil {
// get_by_identity.go:68-80 —— 用未加载的 val.Tags.Identity 填充,且未使用正确的 val.TagsIdentity
	for _, val := range data.Tags {
		res.TagsData = append(res.TagsData, &pb.TagsItem{Id: int64(val.ID), Identity: val.Tags.Identity, ...})
	}
	for _, val := range data.Tags {
		res.TagsIdentityArray = append(res.TagsIdentityArray, val.Tags.Identity)
	}
  • 影响val.Tags 是零值 CmsTagsTagsDatatitle/intro/cover_path 全为空字符串;tags_identity_array 被填充成 N 个空串,前端用它反查/编辑标签必然失败。同一函数 :57-80 完全没有遍历 data.Categoriescategory_data/category_identity_array 恒为空(虽然 Categories 已被 preloadGetByKeyget_by_key.go:25-48)更彻底:既不返回附件也不返回标签,与 GetByIdentity 响应不一致;反差对照 post/fetch.go:63-72 用的是 Preload("Tags.Tags") + tags.TagsIdentity(正确写法)。
  • 建议GetPost 改为 Preload("Categories.Category").Preload("Tags.Tags")identity 数组一律用关联表的 TagsIdentity/CategoryIdentity;为详情接口补字段级断言测试。

8. 页面详情:点击量更新条件用错字段;tags_identity_array 返回关联表 identity

  • 位置module/base/cms/internal/logic/pages/get_by_identity.go:21,76-77,62module/base/cms/internal/logic/pages/get_by_key.go:18,60
  • 证据
// pages/get_by_identity.go:21,76-77 —— 查询用参数 key但自增写死 key 列
data, err := GetPages("Cms_pages.identity", in.Identity)
...
if err := impl.DBService.Model(&models.CmsPages{}).Where("key = ?", val).
	UpdateColumn("hits", gorm.Expr("hits + 1")).Error; err != nil {
// pages/get_by_identity.go:61-62 —— val 是 CmsRelateTagsval.Identity 是关系表主键
for _, val := range data.Tags { res.TagsIdentityArray = append(res.TagsIdentityArray, val.Identity) }
  • 影响GetByIdentity 传入的 val 是页面 identity但自增条件恒为 key = <identity>:正常情况下匹配 0 行hits 永不增长);若某页面的 key 恰好等于另一页面的 identity则会误增他人页面的点击量tags_identity_array 返回的是 cms_relate_tags.identity 而非标签 identity导致按标签筛选/回显失败(get_by_key.go:60 同样问题,而 post/fetch.go:71 的写法是正确的 tags.TagsIdentity)。
  • 建议GetPages 的自增条件改为 Where(key+" = ?", val) 复用同一 key标签 identity 统一用 val.TagsIdentity;为 hits 增加"按 identity 查询后 hits+1"的单测。

9. AddComment 强依赖外部模块表 mall_staff,且存在未保护的 map 类型断言panic 风险)

  • 位置module/base/cms/internal/logic/post/add_comment.go:34-44
  • 证据
	authName := map[string]any{}
	err = impl.DBService.Table("mall_staff").Take(&authName, "identity=?", auth.Identity).Error
	if err != nil { printer.Error(err.Error()); return nil, errcode.ErrDB }
	data.OwnerName = authName["name"].(string)
  • 影响3 个问题。① 分层/耦合违规CMS 直接查商城模块的表 mall_staff(表名硬编码),跨模块 schema 耦合;② 任何非商城员工(例如 passport 普通用户,default_dev.yaml 的同一 JWT 密钥允许其通过鉴权)调用 AddComment 都会 Take 不到记录 → ErrDB,评论功能对 C 端不可用;③ 若该行 name 为 NULL 或列被改名,authName["name"].(string)nil 做类型断言会 panicgRPC 侧只有 pkgs/all 的 gin Recovery()pkgs/all/internal/server/server.go:35)作用于 HTTP 中间件gRPC 直连/内部调用链上无 recover 中间件panic 会终止进程。
  • 建议:删除跨模块表查询,OwnerName 改为由 token claim 或调用方传入;使用 if v, ok := authName["name"].(string); ok 兜底;为所有 gRPC handler 增加 recover 拦截器;models.AddComment(comment, authorIdentity)models/query.go:277的第二个参数从未被使用dead param

10. 文章/评论赞踩无去重、无下限保护(TODO 明确未实现),可无限刷量并出现负数

  • 位置module/base/cms/internal/models/query.go:346,266-275,347-356;调用点 internal/logic/post/{incr_post_like.go:38,desc_post_like.go:24,incr_post_unlike.go:23,desc_post_unlike.go:24,incr_comment_like.go:29,desc_comment_like.go:24,incr_comment_unlike.go:25,desc_comment_unlike.go:24}
  • 证据
// models/query.go:346-352 —— TODO 仍在,去重完全未实现
// TODO: 记录点赞对象避免重复点赞
func IncrOrDescCommentField(identity, column string, desc bool) (err error) {
	... UpdateColumn(column, gorm.Expr(fmt.Sprintf(expr, column), 1))
// incr_post_like.go:33-38 —— op_identity 只做非空校验,从不落库
	if in.GetOpIdentity() == "" || in.GetPostIdentity() == "" { return nil, errcode.ErrInvalidArgument }
	err = models.IncrOrDescPostField(in.PostIdentity, "like_hits", false)
  • 影响:同一 op_identity 可重复调用 IncrPostLike 无限刷高 like_hits13 个赞踩接口同理,IncrCommentLikeop_identity 都没有校验,见 incr_comment_like.go:25,与其他接口不一致);Desc*like_hits > 0 条件,可把计数减成负数(comment_hits 同理,见下条)。计数与真实互动记录无对账依据(无 cms_like 类记录表),数据不可修复。
  • 建议:新增互动记录表/Redis Setpost_identity+op_identity 唯一)做幂等,计数只在首次记录时增减;Desc*AND like_hits > 0;对同 IP/同用户加限流。

11. site_identity 只写不读:所有列表/详情/搜索查询都不过滤站点

  • 位置:写入 internal/logic/post/create.go:35,41internal/logic/category/create.go:36,42internal/logic/pages/create.go:38,44;查询 internal/models/query.go:117-139internal/logic/post/search.go:37internal/logic/category/fetch.go:20-26internal/logic/pages/fetch.go:88-96internal/logic/tags/fetch.go:20internal/logic/post/get_by_identity.go:19
  • 证据
// models/cms_post.go:23 —— 建了索引
SiteIdentity string `gorm:"column:site_identity;type:varchar(36);index;"`
// models/query.go:117-135 —— PostList 的查询条件里没有 site_identity
func PostList(page, size int64, categoryIdentity, keyword string, userType int64) (...) {
	tx := impl.DBService.Debug().Model(&CmsPost{})
	if categoryIdentity != "" { ... }
	if keyword != "" { ... }
  • 影响Create 强校验 site_identity 必填,但所有读路径都忽略它 → 多站点部署时 A 站接口会返回 B 站的文章/页面/分类/标签;cms_tags 甚至连写入都未设置 SiteIdentityinternal/logic/tags/create.go:34-39 无该字段),标签是全局共享的。配合 P1-2Site 服务未实现),多站点隔离实际不可用。
  • 建议:所有查询签名透传 siteIdentity 并在 where 中强制过滤;为 (site_identity, created_at) 建复合索引;cms_tagsSiteIdentity 写入。

12. 独立进程部署模式(cmd/main)无任何鉴权拦截器,且 gateway Mux 未初始化

  • 位置module/base/cms/internal/server/new.go:20-30module/base/cms/cmd/main/main.go:31-44、SDK D:\work\bsm-sdk\core\service\service.go:49-51
  • 证据
// internal/server/new.go:21-30 —— 自建 grpc.NewServer() 无拦截器Mux 字段从未赋值
	grpcServ = grpc.NewServer()
	srv := &Server{Ctx: context.Background(), Grpc: grpcServ, grpcConns: make(map[string]*grpc.ClientConn)}
// cmd/main/main.go:31,42
	s := server.New(nil)
	... GatewayMux:  s.Mux,   // nil
// SDK service.go:49-51 —— New 只做结构体组装,不加拦截器
func New(srv *grpc.Server, opts *Options) *Service { return &Service{GrpcSrv: srv, Opts: opts} }
  • 影响独立部署README "快速开始/make run" 描述的路径)下 gRPC Server 没有任何鉴权拦截器,全部 CMS 方法对内外网开放;同时 s.Mux 为 nilGatewayConf.Enable: trueetc/cms_dev.yaml:19-21)时网关注册依赖 nil mux。模块内 cmd/main/main.go 也从未调用 service.Expose(),即独立模式下 gateway 处理器是否注册取决于 SDK 行为(未验证)。相比之下聚合模式(pkgs/all)有全局拦截器。
  • 建议:独立模式改为复用宿主的鉴权装配(grpc.ChainUnaryInterceptor(auth...)),并在 server.New 中初始化 Mux: gwRuntime.NewServeMux()、显式调用 service.Expose 注册 handler若独立模式已废弃删除 cmd/mainetc/cms_*.yaml 避免误用。

P2

13. 模块内鉴权调用不一致:写接口 category.Modify 完全没有鉴权解析,CommentList 的校验被注释掉

  • 位置internal/logic/category/modify.go:16-36internal/logic/post/comment_list.go:24-27;对比 internal/logic/category/create.go:30internal/logic/category/delete.go:16
  • 证据
// category/modify.go:16-17 —— 整个函数没有 service.ParseMetaCtx
func Modify(ctx context.Context, in *pb.ModifyCategoryRequest) (reply *pb.StatusReply, err error) {
	if in.GetSiteIdentity() == "" || in.GetIdentity() == "" || len(in.GetIdentity()) > 255 {
// post/comment_list.go:24-27
	// _, err := service.ParseMetaCtx(ctx, nil)
	// if err != nil { return nil, err }
  • 影响:鉴权完全依赖宿主拦截器,模块自身不可独立保证安全(见 P1-12category.Modify 与同包 Create/Delete 不一致,是 CODEOWNERS 审查时最易漏的一类。
  • 建议:统一在模块内做 meta 解析并校验角色,或明确在 README 声明"鉴权由宿主负责"并删除散落的解析调用;删除被注释的校验代码。

14. tags.Create 重复校验逻辑失效(死代码),重复标签时返回错误码错误,且未写入 site_identity

  • 位置module/base/cms/internal/logic/tags/create.go:19,25-39
  • 证据
	var cnt int64 = 0
	...
	if err := impl.DBService.Model(&models.CmsTags{}).Where("title = ?", in.Title).First(&models.CmsTags{}).Error; err == nil {
		return nil, errcode.ErrDB          // 已存在却返回 ErrDB
	}
	if cnt > 0 {                            // cnt 从未被赋值,恒为 false
		return nil, errcode.ErrAlreadyExists
	}
  • 影响cnt 永远为 0ErrAlreadyExists 分支不可达dead code重复标题返回 ErrDB500 语义)而非 409CmsTags.SiteIdentity 永不写入(见 P1-11同时强制 cover_pathintro 非空(:25),这两个字段在 proto 中并无必填语义,且 DB 允许空串。
  • 建议:用 CountErrAlreadyExists 正确判定;补 SiteIdentity;放宽必填校验并交由 DB 约束/前端校验。

15. 唯一约束与软删除冲突:category_key/页面 key 唯一索引 + gorm.Model 软删除;tags.Modify 缺 identity 校验静默成功;删除不清理关联表

  • 位置internal/models/cms_category.go:20,25internal/models/cms_pages.go:16,21internal/logic/tags/modify.go:22-35internal/logic/tags/delete.go:21-26internal/models/query.go:370-388
  • 证据
// cms_category.go:20,25 —— gorm.Model 带 DeletedAt软删除category_key 却是 uniqueIndex
	gorm.Model
	CategoryKey string `gorm:"column:category_key;type:varchar(255);not null;uniqueIndex;"`
// tags/modify.go:22,35 —— 只校验 title/cover/intro不校验 identity
	if in.GetTitle() == "" || in.GetCoverPath() == "" || in.GetIntro() == "" { return nil, errcode.ErrInvalidArgument }
	if err := impl.DBService.Where("identity = ?", tagsData.Identity).Updates(tagsData).Error; err != nil {
  • 影响:① 删除分类/标签/页面后不能再用相同的 category_key/key/title 重建(唯一索引包含已软删除行)→ 表现为"莫名 ErrDB";② tags.Modify 传空 identity 时 WHERE identity = '' 匹配 0 行仍返回 OK静默成功③ 删除标签不清理 cms_relate_tagsPreload("Tags.Tags") 取不到已删标签 → 文章标签列表出现标题为空的占位项(脏数据累积);④ DeleteCategorymodels/query.go:375-388)用 Find 不校验 RowsAffected,删除不存在的 identity 返回成功,且不清理 cms_relate_category
  • 建议:唯一索引改为部分索引(WHERE deleted_at IS NULL或统一硬删除Modify 前校验 identity 存在(RowsAffected);删除时级联清理关联表并在同一事务内做计数校验。

16. 生产代码遗留 .Debug()fmt.Println 调试输出,日志无上下文

  • 位置.Debug()internal/models/query.go:119internal/logic/post/comment_list.go:44internal/logic/category/fetch.go:20internal/logic/pages/get_by_identity.go:71fmt.Printlninternal/logic/category/fetch.go:21(打印整个请求)、internal/logic/post/comment_list.go:30internal/logic/post/delete_comment.go:26internal/logic/tags/fetch.go:21printer.Error13 处
  • 证据
// models/query.go:119
tx := impl.DBService.Debug().Model(&CmsPost{})
// internal/logic/post/delete_comment.go:26 —— 空打印
	err = models.DeleteComment(in.Identity)
	if err != nil { fmt.Println(); return nil, errcode.ErrDB }
  • 影响Debug() 让 GORM 以 Info 级别打印全部 SQL 与参数含正文、评论内容、token 派生的 identity既污染日志又放大 I/O 开销;fmt.Println 绕过日志框架无级别、无采集、stdout 与 supervisor 的 stdout_logfile 混流);printer.Error(err.Error()) 丢失请求标识、方法名、参数与堆栈,无法定位。
  • 建议:删除所有 .Debug()fmt.Println;统一 printer.Error("cms.post.DeleteComment failed: identity=%s err=%v", ...) 形式;接入 request-id 与慢查询日志。

17. 数据库调用未绑定请求 ctx,无超时/重试/熔断/健康检查

  • 位置:全模块(grep WithContextinternal/ 无命中);internal/impl/impl.go:35-47internal/config/config.go:34-56
  • 证据
// internal/impl/impl.go:43 —— 只有启动期初始化,无健康检查/重试/连接池显式配置
	DBService = with.Databases(config.Spec.Databases, nil)
// internal/logic/post/get_by_identity.go:19 —— ctx 未传入数据访问层
	data, err := models.GetPost("identity", in.Identity)
  • 影响:客户端断开或上游超时后 SQL 仍继续执行,慢查询会持续占用连接池;config.New 只做端口/IP 与非空校验(config.go:39-49),不校验连接串可达性、不设置 MaxOpenConns/MaxIdleConns/ConnMaxLifetime;无 DB 健康检查接口,启动即对外服务。
  • 建议:数据访问统一 impl.DBService.WithContext(ctx);为 DB 调用设 statement timeout启动期做 sql.DB.PingContext 并失败即退出;暴露 /healthz 聚合 DB/Redis/Etcd 状态。

18. 缓存设施完全未使用;列表接口无分页、全表扫描

  • 位置internal/impl/impl.go:16-30,37-46(仅初始化);internal/models/query.go:359-367CategoryList 全量)、internal/logic/category/fetch.go:20-27(全量 + 递归 Preloadinternal/logic/tags/fetch.go:20(全量)
  • 证据
// 全模块检索 RedisService / MemoryService仅 impl.go 的声明与赋值命中
	MemoryService = with.Memory(nil)
	RedisService = with.RedisCache(config.Spec.Cache)
// internal/logic/tags/fetch.go:20 —— 无 Limit/Offset
	if err := impl.DBService.Model(&models.CmsTags{}).Order("created_at desc").Count(&cnt).Find(&tags).Error; err != nil {
  • 影响README 第 403-420 行声称的"文章列表 10 分钟 / 详情 30 分钟 / 分类 1 小时 / 标签 1 小时"缓存策略、以及"Redis 缓存 + 数据库优化"特性全部不存在,热点列表每次直查 DB分类与标签接口一次性全量加载分类还递归 Children),数据量增长后内存与 P99 双爆。
  • 建议:为分类/标签/站点配置加缓存(写操作失效)或至少加分页;删除 README 中不实的缓存表,或补齐实现。

19. CmsPost.Types 列类型与 Go 类型不一致Search 无排序、强制 page_size=50;分页边界未校验

  • 位置internal/models/cms_post.go:26internal/models/query.go:129-131internal/logic/post/search.go:27-34,38
  • 证据
// cms_post.go:26 —— int32 字段映射到 varchar(255)
	Types       int32  `gorm:"column:types;type:varchar(255);default:'';" json:"types"`
// models/query.go:130
		tx = tx.Where("cms_post.types = ?", userType)
// search.go:31-34 —— 期望"最大 50",实际是"强制 50"
	if pageSize < 50 { pageSize = 50; offset = (pageNo - 1) * pageSize }
  • 影响varchar 列与 int 参数比较依赖隐式转换Postgres 需按文本比较,语义脆弱;是否直接报 42703/42883 取决于部署 schema属推测SearchORDER BY → 分页结果不稳定(同页可能重复/漏项);客户端传 page_size=10 会被放大到 50传 0/负数也被放大到 50SearchFetch 的分页策略不一致。
  • 建议Types 改为 int32 + type:integer(或 smallint)并迁移存量数据;if pageSize <= 0 { pageSize = 10 }; if pageSize > 50 { pageSize = 50 };所有列表查询补 Order("created_at desc, id desc") 稳定排序。

20. Create/Modify 字段映射不一致、Updates(struct) 无法清空字段

  • 位置internal/logic/post/create.go:40-56 对比 internal/logic/post/modify.go:23-42internal/models/query.go:171internal/logic/category/modify.go:33internal/logic/tags/modify.go:35internal/logic/post/modify_comment.go:33
  • 证据
// create.go:40-56 —— 未映射 Lang/SourceOrigin/Rights/Extend*/Publishedproto 中存在这些字段)
	var data = &models.CmsPost{ SiteIdentity: ..., Title: ..., Content: ..., Types: in.PostType, ... }
// modify.go:34-41 —— Modify 却映射了 Lang/SourceOrigin/Rights/Extend*
		Hash: utils.FormatKey(in.Key), Lang: in.Lang, SourceOrigin: in.SourceOrigin, Rights: in.Rights, ExtendUrl: in.ExtendUrl,
  • 影响:创建接口无法写入语言/来源/权限/扩展字段与 published,只能靠再调一次 ModifyModify 又要求 identity+site_identity所有 Updates(struct) 调用会跳过零值字段 → has_accessory=falseparent_id=0(分类移到顶级)、rights=""comment_hits=0 等都无法写入/清空,接口语义与 proto 注释不符。pubished 字段在详情中以 0001-01-01 00:00:00 返回(get_by_identity.go:55)。
  • 建议Create/Modify 共用同一个映射函数(避免漂移);需要清空语义的字段用 map[string]any + Select 显式更新,或用 proto3 optional/field_mask 表达"未设置"。

21. dead code 与未实现项集中存在

  • 位置
    • internal/models/query.go:23-38 InitData()(根分类初始化)在模块内无任何调用点 → 根分类永远不会被创建;
    • internal/models/query.go:358-367 CategoryList() 无调用点(internal/logic/category/fetch.go:15 只留注释 // list, cnt, err := logic.CategoryList()
    • internal/models/query.go:299-301 ModifyComment() 无调用点logic 内联 DB 操作);
    • internal/logic/post/ext.go:8-17internal/logic/pages/ext.go:8-17 fmtKey() 定义后无引用;
    • internal/models/query.go:207 DeletePost(identity, authorIdentity) 第二参数、internal/models/query.go:277 AddComment(comment, authorIdentity) 第二参数无使用;
    • internal/logic/tags/create.go:31-33 不可达分支(见 P2-14
  • 证据
// models/query.go:23-37 —— 无调用点;且忽略 Create 错误、无并发/唯一保护
func InitData() {
	var cnt int64 = 0
	err := impl.DBService.Model(&CmsCategory{}).Where("identity=?", "_RootCategory").Count(&cnt).Error
	if cnt == 0 || err != nil { ... impl.DBService.Create(data) }
}
  • 影响:根分类初始化与分类列表查询各有一份实现(fetch.go 重写了一遍),维护时容易改一处漏一处;InitData 忽略错误且多副本并发启动时会创建重复根分类(identity 无唯一索引,cms_category.go:21Std_Identity 无 unique 约束)。
  • 建议:删除或接线 dead codeInitData 应移入显式迁移步骤并加唯一约束/ON CONFLICT DO NOTHING);用 service.Expose 之外的显式初始化钩子。

22. 点击量自增无防刷,且在"读操作"里写库并包在事务中

  • 位置internal/models/query.go:236-255internal/logic/pages/get_by_identity.go:67-81
  • 证据
// models/query.go:240-251 —— 查询 + 计数更新包在同一事务,且每次调用都 +1
	return post, impl.DBService.Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&CmsPost{}).Preload(...).Where(key+" = ?", val).First(post).Error; err != nil { return err }
		if err := tx.Model(&CmsPost{}).Where(key+" = ?", val).
			UpdateColumn("hits", gorm.Expr("hits + 1")).Error; err != nil {
  • 影响:任何调用方(含后台管理、爬虫、自身服务重试)查看详情都会使 hits 增长,热门文章行会被高频 UPDATE 造成行锁竞争Postgres 上每次详情 = 一次写事务 + 一次 WAL无按用户/IP 去重、无限流。
  • 建议:浏览量改为异步/Redis 计数 + 定期落库,或按 op_identity+时间段 去重;把读与写拆开(读事务内不写)。

23. 富文本无 XSS 处理;附件路径由客户端任意指定

  • 位置internal/logic/post/create.go:48internal/models/query.go:89internal/logic/pages/create.go:111
  • 证据
// post/create.go:48 —— 原样入库
		Content: in.Content,
// models/query.go:85-91 —— FilePath/Title 来自请求,无任何校验
			accessoryPath = append(accessoryPath, CmsAccessory{ ..., FilePath: v.FilePath, Title: v.Title })
  • 影响content 为 text 且无任何 sanitize/转义,若前端使用 v-html/innerHTML 渲染(推测,前端未在本次范围)就是存储型 XSS而写接口在 P0-1 下对任意登录用户开放;file_path 无协议/域名白名单,可写入 javascript:、内网地址或本地路径,被前端/下载服务消费时形成 XSS/SSRF 或路径穿越CMS 自身不做上传,实际危害取决于消费方)。
  • 建议:入库前用白名单 HTML sanitizer如 bluemonday过滤或统一按纯文本渲染附件路径校验为 https:// + 允许域名,服务端生成路径。

24. 评论:只加载一层子评论、顶层与回复混合分页、不校验文章存在、计数可为负

  • 位置internal/logic/post/comment_list.go:44-46internal/models/query.go:277-297,303-334
  • 证据
// comment_list.go:45-46 —— 只 Preload("Children") 一层,且分页未按 parent_id 区分顶层
		Order("created_at desc").Preload("Children").Count(&cnt).
		Limit(int(size)).Offset(int((page - 1) * size)).Find(&list).Error;
// models/query.go:283,315-316 —— 计数无下限,重复删除/并发可减为负
	tx.Model(&CmsPost{}).Where("identity = ?", comment.PostIdentity).UpdateColumn("comment_hits", gorm.Expr("comment_hits + ?", 1))
	... UpdateColumn("comment_hits", gorm.Expr("comment_hits - ?", 1))
  • 影响:三级及以上回复(Children.Children)永远不返回;分页集合包含顶层评论与各级回复,count 与"顶层评论数"不符,前端按页拼树会串位;AddComment 不校验 post_identity 是否真实存在(可给任意字符串写评论,comment_hits 自增 0 行静默成功);DeleteComment 只减一级父子计数,删除父评论时子评论被软删而子评论的计数不回滚,comment_hits 可长期为负;ModifyComment 允许改 post_identity 把评论搬到别的文章。
  • 建议:列表只查 parent_id = 0 分页、子评论按父 ID 批量二次查询并限制深度;AddComment 校验文章存在;计数改 GREATEST(comment_hits - 1, 0) 或按实际行数重算;ModifyComment 禁止修改 post_identity/parent_id

25. 响应结构不统一(StatusReply 语义混乱、时间单位不一致)

  • 位置internal/logic/post/create.go:68-73internal/logic/post/modify.go:48-52internal/logic/tags/delete.go:28-32internal/logic/post/modify_comment.go:36internal/logic/post/get_by_identity.go:39-40internal/logic/site/create.go:24-27
  • 证据
// post/create.go:68-72 vs tags/delete.go:28-32 vs site/create.go:24-27
	&pb.StatusReply{Code: 0, Message: vars.OK, Details: data.Identity, Timeseq: time.Now().Unix()}
	&pb.StatusReply{Code: 0, Message: "OK", Timeseq: time.Now().Unix()}
	&pb.StatusReply{Details: vars.OK, Timeseq: time.Now().UnixMilli()}
  • 影响timeseq 有的毫秒有的秒;Message/Details 混用(Details 时而承载 identity 时而承载 "OK"ModifyComment/DeleteComment/Desc*Like 返回空 StatusReply{}code=0 但无任何信息,且 code=0 是否代表成功取决于客户端约定);get_by_identity.go:39time.Time.String()2021-12-08 15:59:00 +0000 UTC)而 fetch.go:87Format(time.DateTime),同一字段两种格式。
  • 建议统一响应构造器code/message/details 语义固定)、统一 timeseq 单位与时间格式RFC3339 或 2006-01-02 15:04:05)。

26. 可观测性与运维缺口:无健康检查/指标、无结构化日志、APM 全部注释

  • 位置etc/cms_{dev,prod,test}.yaml:32-35APM 被注释、README:422-430声称 /health/metrics)、模块内无 health/metrics 相关代码grep 无命中)
  • 证据
# APM:
#   Platform: elasticAPM
#   Endpoint: http://127.0.0.1:14268/api/traces
  • 影响README 声称的 curl http://localhost:12102/health/metrics、APM 集成在模块内均不存在(端口 12102 也与 etc/*.yamlGateway.Port: 12409 不一致),无健康检查意味着编排层无法判定实例可用性;日志为 printer 文本 + fmt.Println 混流,缺少结构化字段。
  • 建议:注册 gRPC health service 并透出 /healthz;启用 APM/traceParseMetaCtx 已有 claim 可作 trace 维度);日志 JSON 化并带 request-id。

27. 配置缺陷:注释中残留真实密钥、全环境占位符、无效 Anonymous 项、模块 yaml 在聚合模式下不被加载

  • 位置etc/cms_dev.yaml:24-29etc/cms_prod.yaml:24-29etc/cms_test.yaml:24-29etc/cms_dev.yaml:15-16pkgs/all/internal/service/cms.go:9-20
  • 证据
SecretKey: CHANGE_ME
# Rpc:
#   fts:
#     Endpoint: https://api-v2.traingo.cn/fts/v2
#     SecretKey: 4ef05311358cd1c8f787281f08b38b1c     # 真实形态的密钥留在仓库里
MicroService:
  Anonymous:
    - cms.ping.hello                                    # 无 cms.ping 服务/方法
  • 影响:① 仓库内保留 4ef05311358cd1c8f787281f08b38b1c 形态的密钥(即使被注释也属泄露面,需确认是否仍有效);② 三套环境 DB/Cache/SecretKey 全为 CHANGE_ME,无配置校验拒绝启动(config.NotNil 只校验 Service/Cacheconfig.go:49);③ Anonymous: cms.ping.helloprod/test 写成 Cms.ping.hello,大小写不一致)指向不存在的服务,给人"已配置白名单"的错觉;④ 聚合模式下 etc/cms_*.yaml 根本不被读取(exposeCMS 只注入依赖),其中的 SecretKey/Anonymous/Gateway 均为误导性配置。
  • 建议:轮换并移除示例密钥;生产配置走环境变量/密管(不落库);删除失效的 Anonymous;明确"模块 yaml 仅独立模式有效"或删除。

28. 索引与约束缺口

  • 位置internal/models/cms_comment.go:20-30(仅单列索引)、internal/models/cms_relate_category.go:8-10internal/models/cms_relate_tags.go:8-11internal/models/query.go:58-73
  • 证据
// cms_comment.go:20 —— 只有 post_identity 单列索引,而查询按 post_identity + created_at 排序分页
	PostIdentity  string `gorm:"column:post_identity;type:varchar(36);index;"`
// cms_relate_tags.go:8-10 —— identity 唯一,但 (post_identity, tags_identity) 无唯一约束
	Identity      string `gorm:"column:identity;type:varchar(36);uniqueIndex;"`
  • 影响:评论列表(comment_list.go:45 Order("created_at desc"))缺 (post_identity, created_at desc) 复合索引,数据量大后排序走磁盘;cms_relate_category/cms_relate_tags 允许同一文章重复关联同一分类/标签(models/query.go:58-73 不去重),导致 PostListleft join 计数放大(query.go:121-133cms_post(site_identity, created_at)(与 P1-11 联动)。
  • 建议:补复合索引与 UNIQUE(post_identity, category_identity) / UNIQUE(post_identity, tags_identity);入参数组去重。

P3

29. test/*.http 全部过期:服务名前缀、方法名、域名、请求头均不匹配当前契约

  • 位置module/base/cms/test/**/*.http32 个文件)
  • 证据
test/post/post_add.http:1        POST http://127.0.0.1:12409/Cms.Post/Create
test/pages/search.http:1         POST http://127.0.0.1:12409/Cms.PostPages/Search
test/tags/tags_add.http:1        POST http://127.0.0.1:12409/Cms.Tags/AddTags
test/category/category_list.http:1 POST http://api.apinb.com/Cms.Category/Fetch
test/post/post_add.http:2        Cms-Type: application/json
  • 影响gateway 注册的 pattern 是小写 cms.Post/Createpb/post.pb.gw.go:1321-1322pb/site.pb.gw.go:410-412),且路由大小写敏感;Cms.Post/DeletePostCms.Category/ModifyCategoryCms.Tags/{AddTags,TagsList,ModifyTags,DeleteTags}Cms.PostPages/* 这些方法名在 proto 中都不存在(应为 cms.Post/Deletecms.Category/Modifycms.Tags/{Create,Fetch,Modify,Delete}cms.Pages/*Cms.PostPages/Search 更是无此 RPC请求头应为 Content-Type。整套手工用例照抄执行会 404/405等于没有可用验证手段。
  • 建议:按 pb/*.pb.gw.go 生成/校正用例,或改用自动化集成测试(见 P3-33删除指向公网域名 api.apinb.com 的残留用例。

30. README 与实现严重不符

  • 位置module/base/cms/README.md:5-17,75-100,403-430,527-617
  • 证据
- **⚡ 高性能**: Redis缓存 + 数据库优化          (README:14 —— 实际零缓存使用)
| 文章列表 | 10分钟 | 按分类+关键词缓存 |      (README:409 —— 无任何缓存代码)
curl http://localhost:12102/health              (README:426 —— 模块无 /health端口应为 12409)
  • 影响README 声称 swagger/scripts/Dockerfiledocker-compose.ymlMakefile.githooksbuild/logs/ 均存在(实际模块内不存在,已用目录清单核对);声称"完善的错误处理和输入验证""监控健康检查和APM集成"与实现不符DDL 章节README:531-617与模型不一致cms_post.type vs typeskey vs hashcms vs contentcms_tagssite_identityAPI 表格未记录已注册的 Site 服务;Port: 12101/12102 与实际 12410/12409 不一致。
  • 建议:以代码为准重写 README或标注"部分为规划"至少修正端口、DDL、缓存章节与服务清单避免误导使用者按 make run 部署出一个无鉴权的独立实例(见 P1-12

31. 注释/命名错误与陈旧路径

  • 位置internal/models/cms_site.go:28internal/models/cms_pages.go:6,15internal/models/cms_tags.go:6internal/models/cms_comment.go:6internal/models/cms_accessory.go:25internal/logic/post/modify.go:18// 修改文章 出现在 pagesinternal/logic/pages/modify.go:18
  • 证据
// cms_site.go:28-30 —— 注释写成 CmsAccessory
// TableName .CmsAccessory 分类表
func (c *CmsSite) TableName() string { return "cms_site" }
// cms_pages.go:15,6 —— 类型注释写成 CmsPost文件头保留 /src/git.buka.tv/Cms/internal/models/post.go
// CmsPost 单页文章
  • 影响CmsSite/CmsPages 的结构体注释与实际类型不符,FilePath 头注释指向旧仓库路径 /src/git.buka.tv/...cms_comment.go:6cms_pages.go:6cms_tags.go:6),检索与新人理解成本上升;cms_accessory.go:25 注释"分类表"实为附件表。
  • 建议:清理陈旧路径与错误注释,golangci-lintgodot/revive 纳入 CI。

32. 硬编码表名(跨模块)与魔法数字

  • 位置internal/logic/post/add_comment.go:35Table("mall_staff"))、internal/logic/pages/fetch.go:91,95,98internal/logic/pages/get_by_identity.go:21internal/models/query.go:18-21(表名常量只覆盖 3/9 张表)
  • 证据
// pages/fetch.go:91,95 —— 表名硬编码进 SQL 字符串,未用模型 TableName
tx = tx.Where("Cms_pages.title like ?", "%"+keyword+"%")
tx = tx.Where("Cms_pages.type = ?", userType)
  • 影响:表名/大小写依赖 Postgres 折叠语义,改表名或换库(如 MySQL 大小写敏感)即失效;mall_staff 为跨模块表名硬编码;50pageSize 上限)、255(长度校验)、10(默认 size散落在各 logic 文件中重复定义。
  • 建议:统一用 modelsTableName()/常量,跨模块数据改走对方 service 接口;把分页/长度常量集中到 internal/configconst 块。

33. 无任何 Go 单元测试;cmd/cli 为空壳

  • 位置:模块内 *_test.go = 0 个(Get-ChildItem -Include *_test.go | Measure-Object → 0cmd/cli/main.go:5-7
  • 证据
// cmd/cli/main.go —— README 声称"命令行工具"
func main() { log.Println("Hello World!") }
  • 影响本报告中的多数缺陷Search 列名、make(len)+append、CommentList 分页、ModifyPost 附件、GetByIdentity 标签)都是单测可秒级捕获的类型;当前唯一的"测试"是 32 个失效的 .http 文件(见 P3-29CI 无从回归。
  • 建议:优先用 sqlmock/sqlite 或 dockertest 建 5 个最小用例(文章 CRUD 关联、评论分页、赞踩幂等、Search、分类唯一性并把 .http 用例换成可执行的集成测试。

4. 推荐优化方案

  1. 鉴权与授权分层(对应 P0-1、P1-12、P2-13
    • 宿主层:为 CMS 写方法建立独立 admin 受众aud/密钥,不再与 C 端共用 HS256 keyAnonymous 列表不包含任何 cms. 方法。
    • 模块层:service.ParseMetaCtx(ctx, &service.ParseOptions{RoleValue: "admin"}) 统一入口;数据访问强制属主条件(identity + author_identity/owner_identity),评论删除/修改仅允许作者或管理员。
    • 独立模式(cmd/main)补齐 grpc.ChainUnaryInterceptor(auth, recovery, timeout, logging)Mux 初始化,否则删除该入口。
  2. 数据一致性(对应 P1-6、P1-7、P1-8、P1-10、P2-15、P2-24
    • 写路径统一"先按 identity 查真实记录 → 组装附件(真实 ID/路径)→ 用显式字段集更新 → 差集重建关联",禁止"先删后按需重建"。
    • 互动计数引入幂等记录(cms_interaction(post_identity, comment_identity, op_identity, type) 唯一索引)+ Redis 计数,Desc 加下限保护;comment_hits 定期按实际评论数对账。
    • 唯一约束与软删除二选一:唯一索引加 WHERE deleted_at IS NULL,或分类/标签改硬删除并级联清理关联表。
  3. 查询正确性(对应 P1-3、P1-4、P1-5、P1-11、P2-19、P2-28
    • Searchtitle/content 并加稳定排序;修正 make(len)+append分页 clamp 前置;所有列表强制 site_identity 过滤;补 4 个复合索引。
    • 引入 repository 层封装(禁止 logic 直接拼 SQL/表名),统一 WithContext + LIMIT/OFFSET + 排序约定。
  4. 契约与文档(对应 P1-2、P2-20、P2-25、P3-29~33
    • Site 服务:实现或下架;StatusReply 统一构造器Create/Modify 复用同一映射README 按代码重写;.http 用例按 gateway pattern 校正并纳入 CI。
  5. 可观测性与健壮性(对应 P2-16、P2-17、P2-26、P2-27
    • 删除 .Debug()/fmt.Println输出结构化日志method、identity、site_identity、request-id、耗时DB statement timeout + 连接池显式配置;/healthz 聚合依赖状态;配置启动强校验(拒绝 CHANGE_ME、校验 DSN 可达);移除仓库内示例密钥。

5. TODO 清单

  • P0-1 为 CMS 全部写接口加属主/角色校验(WHERE identity=? AND author_identity=?,评论仅作者/管理员可改删验收A 用户 token 调 cms.Post/Deletecms.Post/ModifyComment 改删 B 用户数据返回 PermissionDenied,且 ParseMetaCtx 传入角色选项|涉及:module/base/cms/internal/logic/post/modify.go:16module/base/cms/internal/logic/post/delete.go:29module/base/cms/internal/models/query.go:207module/base/cms/internal/logic/post/delete_comment.go:24
  • P1-2 补齐或下架 Site 五个方法(不得返回假成功)|验收:cms.Site/CreateGet/Fetch 能查到记录;未实现前返回 Unimplemented|涉及:module/base/cms/internal/logic/site/create.go:20module/base/cms/internal/logic/site/get.go:24
  • P1-3 修正 Search 的列名|验收:带 keywordcms.Post/Search 返回 200 且命中 content|涉及:module/base/cms/internal/logic/post/search.go:37
  • P1-4 修正 Search 结果切片构造|验收:返回元素数等于实际行数且无 null|涉及:module/base/cms/internal/logic/post/search.go:43
  • P1-5 修正评论列表分页 clamp 顺序|验收:page/size 缺省时返回前 10 条、count 与数据一致|涉及:module/base/cms/internal/logic/post/comment_list.go:18-46
  • P1-6 修正 ModifyPost/ModifyPages 附件字段与关联重建语义验收modify 后 post_id 为真实 ID、file_path 为路径;仅改标题时分类/标签/附件不变|涉及:module/base/cms/internal/models/query.go:162-201module/base/cms/internal/logic/pages/modify.go:68-99
  • P1-7 文章详情返回标签与分类数据|验收:GetByIdentitytags_data[].title 非空、tags_identity_array 为真实标签 identity、category_data 非空|涉及:module/base/cms/internal/models/query.go:242module/base/cms/internal/logic/post/get_by_identity.go:68-80
  • P1-8 修正页面详情 hits 更新条件与 tag identity验收GetByIdentity 使该页 hits+1 且不误增他页;tags_identity_array 为标签 identity涉及module/base/cms/internal/logic/pages/get_by_identity.go:76module/base/cms/internal/logic/pages/get_by_identity.go:62
  • P1-9 移除 mall_staff 依赖并修复类型断言 panic验收非商城员工用户可发评论构造 name 为 NULL 的数据不再 panic涉及module/base/cms/internal/logic/post/add_comment.go:35-40
  • P1-10 实现赞踩幂等与计数下限|验收:同一 op_identity 连续 100 次 IncrPostLikelike_hits=1Desc* 不会低于 0涉及module/base/cms/internal/models/query.go:346module/base/cms/internal/logic/post/incr_post_like.go:38
  • P1-11 全部查询按 site_identity 过滤验收A 站 token 查询不到 B 站文章/页面/分类/标签|涉及:module/base/cms/internal/models/query.go:117-139module/base/cms/internal/logic/category/fetch.go:20module/base/cms/internal/logic/tags/create.go:34
  • P1-12 独立模式补鉴权拦截器并初始化 Mux|验收:独立进程启动后无 token 调 cms.Post/Delete 返回 Unauthenticated涉及module/base/cms/internal/server/new.go:20-30module/base/cms/cmd/main/main.go:31-42
  • P2-14 修正 tags.Create 重复校验与 site_identity|验收:重复标题返回 ErrAlreadyExists,新标签带 site_identity|涉及:module/base/cms/internal/logic/tags/create.go:19-39
  • P2-15 解决软删除与唯一约束冲突、Modify 静默成功、删除残留关联|验收:删除后可用同名 key 重建;空 identity 的 Modify 返回 InvalidArgument;删除标签后无残留关联行|涉及:module/base/cms/internal/models/cms_category.go:20-25module/base/cms/internal/logic/tags/modify.go:22-35
  • P2-16 清理 .Debug()/fmt.Println,日志结构化|验收:grep -r 'Debug()\|fmt.Print' internal/ 无命中,日志含 method/identity/request-id涉及module/base/cms/internal/models/query.go:119module/base/cms/internal/logic/post/comment_list.go:30
  • P2-17 DB 访问绑定 ctx 并设超时|验收:请求取消后 SQL 终止;启动期 Ping 失败即退出|涉及:module/base/cms/internal/impl/impl.go:43module/base/cms/internal/logic/post/get_by_identity.go:19
  • P2-18 为分类/标签加缓存或分页|验收:cms.Tags/Fetch 支持 page/size 且缓存命中率可观测|涉及:module/base/cms/internal/logic/tags/fetch.go:20module/base/cms/internal/logic/category/fetch.go:20
  • P2-19 统一 Types 列类型并修正分页/排序边界|验收:type=1 过滤有效、所有列表有稳定 ORDER BYpage_size>50 被夹到 50涉及module/base/cms/internal/models/cms_post.go:26module/base/cms/internal/logic/post/search.go:31-38
  • P2-20 Create/Modify 字段映射对齐并支持清空语义验收Create 可写 lang/source_origin/rights/extend_*/publishedhas_accessory 可置 false涉及module/base/cms/internal/logic/post/create.go:40-56module/base/cms/internal/models/query.go:171
  • P2-21 清理/接线 dead codeInitDataCategoryListmodels.ModifyCommentfmtKey、无用参数)|验收:go vet + deadcode 无告警,根分类由确定性的迁移步骤创建|涉及:module/base/cms/internal/models/query.go:23module/base/cms/internal/logic/post/ext.go:8
  • P2-22 浏览量异步化/去重|验收:详情接口无同步 UPDATE同用户短时间重复刷新不重复计数涉及module/base/cms/internal/models/query.go:248
  • P2-23 富文本 sanitize + 附件路径白名单|验收:<script> 入库被过滤;非 https 白名单域名的 file_path 被拒|涉及:module/base/cms/internal/logic/post/create.go:48module/base/cms/internal/models/query.go:89
  • P2-24 评论树分层分页、校验文章存在、计数下限|验收:三层回复可返回;comment_hits 不低于 0不存在的 post_identity 返回 NotFound涉及module/base/cms/internal/logic/post/comment_list.go:45module/base/cms/internal/models/query.go:277-334
  • P2-25 统一响应构造与时间格式|验收:所有接口 code/message/details/timeseq 语义一致,时间为 RFC3339涉及module/base/cms/internal/logic/post/create.go:68module/base/cms/internal/logic/tags/delete.go:28
  • P2-26 增加健康检查与 APM/结构化日志|验收:/healthz 返回依赖状态APM 配置生效|涉及:module/base/cms/etc/cms_prod.yaml:33
  • P2-27 配置加固(拒绝 CHANGE_ME、清理示例密钥、删除失效 Anonymous)|验收:占位配置启动失败并给出明确错误|涉及:module/base/cms/etc/cms_prod.yaml:7-24module/base/cms/internal/config/config.go:49
  • P2-28 补复合索引与关联唯一约束|验收:(post_identity, created_at desc)(site_identity, created_at)UNIQUE(post_identity, tags_identity) 存在且 EXPLAIN 无全表排序|涉及:module/base/cms/internal/models/cms_comment.go:20module/base/cms/internal/models/cms_relate_tags.go:8
  • P3-29 校正/重写 test/*.http(服务名小写 cms.、方法名与 proto 一致、Content-Type)|验收:全部用例可执行通过|涉及:module/base/cms/test/post/post_add.http:1module/base/cms/test/pages/search.http:1
  • P3-30 按代码重写 README端口、DDL、缓存章节、Site 服务、目录结构验收README 与实际端口/表字段逐一核对通过|涉及:module/base/cms/README.md:403-430module/base/cms/README.md:531-617
  • P3-31 清理错误注释与陈旧路径|验收:无 /src/git.buka.tv 残留、结构体注释与类型一致|涉及:module/base/cms/internal/models/cms_site.go:28module/base/cms/internal/models/cms_pages.go:6
  • P3-32 去硬编码表名/魔法数字|验收:无 Table("mall_staff") 与字符串表名,分页常量集中定义|涉及:module/base/cms/internal/logic/post/add_comment.go:35module/base/cms/internal/logic/pages/fetch.go:91
  • P3-33 建立最小单测集并接线 CI验收go test ./... 覆盖文章关联写入、评论分页、赞踩幂等、Search、分类唯一性CI 必过|涉及:module/base/cms/internal/logic/post/search.go:37module/base/cms/internal/logic/post/comment_list.go:44

6. 审计摘要(供汇总使用)

  • 问题数P0=1 P1=11 P2=16 P3=5合计 33
  • 最高风险一句话CMS 全部写接口只校验"任意有效 JWT"而无角色/归属校验,任意 C 端登录用户可通过 /cms.Post/Delete/cms.Post/ModifyComment 等改删他人文章与评论(logic/post/delete.go:29 甚至显式传空 authorIdentity 并留 // Todo:),且 Site 服务五个接口全为空实现却返回"成功"。
  • 最优先 3 个动作:
    1. 给所有写路径补属主/角色校验(模块内 ParseMetaCtx 带角色 + SQL 带 author_identity/owner_identity),并为 CMS 使用独立管理端密钥/受众。
    2. 修复必然/高概率数据损坏:Search 列名与切片构造(search.go:37,43)、CommentList 分页恒空(comment_list.go:18-46)、ModifyPost 附件 post_id=0/file_path=identity 与"空数组清空关联"models/query.go:162-201)。
    3. 明确 Site 服务去留(实现或下架,禁止假成功),并补齐 5 个最小单测用例接入 CI当前模块 0 个 *_test.go32 个 .http 用例全部失效)。
  • 未能覆盖/无法验证的部分:真实数据库 schemacms_post 是否遗留 cms 列 → 影响 P1-3 是"必然报错"还是"可能报错");前端是否以 HTML 渲染 contentXSS 实际可利用性);独立部署模式(cmd/main + SDK service.Start)的网关/鉴权实际运行结果;etc/cms_prod.yaml 与线上投放配置的差异;pb/ 生成代码内部逻辑(未逐行审阅)。