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.
61 KiB
61 KiB
审计报告:module/base/cms
1. 模块概览
- 模块路径:
module/base/cms(bsm/full/module/base/cms),Go 1.26.5,gRPC + grpc-gateway 微服务。 - 服务清单(
proto/):Post(19 个 RPC:文章 CRUD/搜索/评论 CRUD/文章与评论的赞踩)、Pages(6)、Category(4)、Tags(4)、Site(5)。 - 关键目录:
cmd/{main,cli}、etc/{cms_dev,cms_prod,cms_test}.yaml、internal/{config,impl,logic,models,server,utils}、pb/(生成代码,17 个文件)、proto/(6 个)、service/{dependencies,expose}.go、test/{category,comment,pages,post,tags}(32 个.http,无任何 Go 测试文件)。 - 运行方式:两种。
- 独立进程:
cmd/main/main.go→config.New("cms")+impl.NewImpl()+server.New(nil)+ SDKservice.New(...).Start()(聚合配置services: [cms])。 - 聚合进程:
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)、RedisService、MemoryService、EtcdService;service/dependencies.go由宿主注入。 - 数据模型:
cms_post、cms_pages、cms_category、cms_tags、cms_comment、cms_accessory、cms_relate_category、cms_relate_tags、cms_site(internal/models/*.go,均通过database.MigrateTables注册 AutoMigrate)。
2. 审计范围与方法
已读文件(全部非 pb 的 Go 文件 + proto + etc + README + test 抽样)
- 入口/装配:
cmd/main/main.go、cmd/cli/main.go、internal/config/config.go、internal/impl/impl.go、internal/server/{new,post_server,category_server,pages_server,tags_server,site_server}.go、service/{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}.proto;pb/*.pb.gw.go的 URL pattern 抽样(post.pb.gw.go:1321-1322、site.pb.gw.go:410-412)。 - 配置:
etc/cms_{dev,prod,test}.yaml、etc/supervisor.bsm-apps-cms.conf、README.md(679 行)。 - 宿主/相邻模块(用于判定鉴权边界):
pkgs/all/internal/service/cms.go、pkgs/all/internal/server/{server,authorization}.go、pkgs/all/etc/default_dev.yaml、pkgs/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 pattern);swagger/、scripts/、Dockerfile、Makefile在模块内不存在(README 声称存在)。- 运行时行为、真实数据库 schema(
cms_post是否存在遗留cms列只能从模型推断)、前端是否为富文本渲染 HTML(XSS 影响面为推测)。 etc/cms_prod.yaml与线上实际投放配置的差异。
3. 问题清单
P0
1. 全模块写接口无归属/角色校验,任意已认证用户可修改、删除他人文章与评论(IDOR)
- 位置:
module/base/cms/internal/logic/post/modify.go:16,43module/base/cms/internal/logic/post/delete.go:29module/base/cms/internal/models/query.go:207module/base/cms/internal/logic/post/modify_comment.go:33module/base/cms/internal/logic/post/delete_comment.go:24module/base/cms/internal/models/query.go:306,312module/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-53、pkgs/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:36、module/base/cms/service/expose.go:32、module/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 与 gateway(URL/cms.Site/*),调用方收到details=OK但没有任何数据落库,是静默数据丢失(比报错更危险:前端会提示保存成功);Fetch返回nil(空响应体,无错误);Get返回零值SiteItem且无 NotFound。同时models.CmsSite(internal/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)中没有Cms,AutoMigrate 建出的表只有content。Postgres 中Cms折叠为cms,查询报column "cms" does not exist(42703),逻辑层统一吞成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:31(make([]*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/size(proto 默认 0)时size=0,GORM 会生成LIMIT 0(已核对gorm@v1.31.2/clause/limit.go:16-19,*Limit>=0即写入),评论列表恒为空而count正常,前端表现为"有评论数但列表空白";page=0,size=10时Offset为 -10,被 GORM 静默丢弃(limit.go:20Offset > 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 层新建的结构体(恒为 0),FilePath 被写成 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 为新建结构体,未设置 ID;accessory 实参是 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 { ... } }
- 影响:两个必然/高概率缺陷叠加:
PostId恒为 0(update时 structure 没有 ID)→ 附件与文章的整型外键断开,而CmsPost.Accessories的关联键是foreignkey:PostId(models/cms_post.go:59),因此文章详情/列表再也 preload 不到这批附件;FilePath被写成附件 identity(in.AccessoryIdentityArray是 identity 列表,不是路径),附件显示为随机 ID,Title丢失。- 关联重建是"先全删、后按入参重建":只要调用方只改标题、不带
category_identity_array/tags_identity_array/accessory_identity_array(proto 中均为可选字段),文章的全部分类、标签、附件关联被清空且不报错。 页面侧同源缺陷:internal/logic/pages/modify.go:71写PagesId: pages.ID(同一data无 ID),:81-99同样先删后条件重建。
- 建议:
ModifyPost/ModifyPages用identity反查真实记录 ID 后再构造附件;把accessory入参改为[]*pb.AccessoryItem(保留file_path/title);关联重建改为显式语义(nil=不改动,空切片=清空),或使用"差集更新";补一条"仅改标题后分类/标签保持不变"的回归测试。
7. 文章详情 GetByIdentity 标签数据全为空、分类数据完全缺失
- 位置:
module/base/cms/internal/logic/post/get_by_identity.go:57-80、module/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是零值CmsTags→TagsData的title/intro/cover_path全为空字符串;tags_identity_array被填充成 N 个空串,前端用它反查/编辑标签必然失败。同一函数:57-80完全没有遍历data.Categories,category_data/category_identity_array恒为空(虽然Categories已被 preload)。GetByKey(get_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,62、module/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 是 CmsRelateTags,val.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做类型断言会 panic;gRPC 侧只有pkgs/all的 ginRecovery()(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_hits(13 个赞踩接口同理,IncrCommentLike连op_identity都没有校验,见incr_comment_like.go:25,与其他接口不一致);Desc*无like_hits > 0条件,可把计数减成负数(comment_hits同理,见下条)。计数与真实互动记录无对账依据(无cms_like类记录表),数据不可修复。 - 建议:新增互动记录表/Redis Set(
post_identity+op_identity唯一)做幂等,计数只在首次记录时增减;Desc*加AND like_hits > 0;对同 IP/同用户加限流。
11. site_identity 只写不读:所有列表/详情/搜索查询都不过滤站点
- 位置:写入
internal/logic/post/create.go:35,41、internal/logic/category/create.go:36,42、internal/logic/pages/create.go:38,44;查询internal/models/query.go:117-139、internal/logic/post/search.go:37、internal/logic/category/fetch.go:20-26、internal/logic/pages/fetch.go:88-96、internal/logic/tags/fetch.go:20、internal/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甚至连写入都未设置SiteIdentity(internal/logic/tags/create.go:34-39无该字段),标签是全局共享的。配合 P1-2(Site 服务未实现),多站点隔离实际不可用。 - 建议:所有查询签名透传
siteIdentity并在 where 中强制过滤;为(site_identity, created_at)建复合索引;cms_tags补SiteIdentity写入。
12. 独立进程部署模式(cmd/main)无任何鉴权拦截器,且 gateway Mux 未初始化
- 位置:
module/base/cms/internal/server/new.go:20-30、module/base/cms/cmd/main/main.go:31-44、SDKD:\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为 nil,GatewayConf.Enable: true(etc/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/main与etc/cms_*.yaml避免误用。
P2
13. 模块内鉴权调用不一致:写接口 category.Modify 完全没有鉴权解析,CommentList 的校验被注释掉
- 位置:
internal/logic/category/modify.go:16-36、internal/logic/post/comment_list.go:24-27;对比internal/logic/category/create.go:30、internal/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-12);
category.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永远为 0,ErrAlreadyExists分支不可达(dead code);重复标题返回ErrDB(500 语义)而非 409;CmsTags.SiteIdentity永不写入(见 P1-11);同时强制cover_path与intro非空(:25),这两个字段在 proto 中并无必填语义,且 DB 允许空串。 - 建议:用
Count或ErrAlreadyExists正确判定;补SiteIdentity;放宽必填校验并交由 DB 约束/前端校验。
15. 唯一约束与软删除冲突:category_key/页面 key 唯一索引 + gorm.Model 软删除;tags.Modify 缺 identity 校验静默成功;删除不清理关联表
- 位置:
internal/models/cms_category.go:20,25、internal/models/cms_pages.go:16,21、internal/logic/tags/modify.go:22-35、internal/logic/tags/delete.go:21-26、internal/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_tags,Preload("Tags.Tags")取不到已删标签 → 文章标签列表出现标题为空的占位项(脏数据累积);④DeleteCategory(models/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:119、internal/logic/post/comment_list.go:44、internal/logic/category/fetch.go:20、internal/logic/pages/get_by_identity.go:71;fmt.Println:internal/logic/category/fetch.go:21(打印整个请求)、internal/logic/post/comment_list.go:30、internal/logic/post/delete_comment.go:26、internal/logic/tags/fetch.go:21;printer.Error:13 处 - 证据:
// 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 WithContext在internal/无命中);internal/impl/impl.go:35-47;internal/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-367(CategoryList全量)、internal/logic/category/fetch.go:20-27(全量 + 递归 Preload)、internal/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:26、internal/models/query.go:129-131、internal/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,属推测);Search无ORDER BY→ 分页结果不稳定(同页可能重复/漏项);客户端传page_size=10会被放大到 50,传 0/负数也被放大到 50;Search与Fetch的分页策略不一致。 - 建议:
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-42;internal/models/query.go:171、internal/logic/category/modify.go:33、internal/logic/tags/modify.go:35、internal/logic/post/modify_comment.go:33 - 证据:
// create.go:40-56 —— 未映射 Lang/SourceOrigin/Rights/Extend*/Published(proto 中存在这些字段)
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,只能靠再调一次 Modify(Modify 又要求 identity+site_identity);所有Updates(struct)调用会跳过零值字段 →has_accessory=false、parent_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-38InitData()(根分类初始化)在模块内无任何调用点 → 根分类永远不会被创建;internal/models/query.go:358-367CategoryList()无调用点(internal/logic/category/fetch.go:15只留注释// list, cnt, err := logic.CategoryList());internal/models/query.go:299-301ModifyComment()无调用点(logic 内联 DB 操作);internal/logic/post/ext.go:8-17、internal/logic/pages/ext.go:8-17fmtKey()定义后无引用;internal/models/query.go:207DeletePost(identity, authorIdentity)第二参数、internal/models/query.go:277AddComment(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:21的Std_Identity无 unique 约束)。 - 建议:删除或接线 dead code(
InitData应移入显式迁移步骤并加唯一约束/ON CONFLICT DO NOTHING);用service.Expose之外的显式初始化钩子。
22. 点击量自增无防刷,且在"读操作"里写库并包在事务中
- 位置:
internal/models/query.go:236-255、internal/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:48、internal/models/query.go:89、internal/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-46、internal/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-73、internal/logic/post/modify.go:48-52、internal/logic/tags/delete.go:28-32、internal/logic/post/modify_comment.go:36、internal/logic/post/get_by_identity.go:39-40、internal/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:39用time.Time.String()(2021-12-08 15:59:00 +0000 UTC)而fetch.go:87用Format(time.DateTime),同一字段两种格式。 - 建议:统一响应构造器(code/message/details 语义固定)、统一
timeseq单位与时间格式(RFC3339 或2006-01-02 15:04:05)。
26. 可观测性与运维缺口:无健康检查/指标、无结构化日志、APM 全部注释
- 位置:
etc/cms_{dev,prod,test}.yaml:32-35(APM 被注释)、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/*.yaml的Gateway.Port: 12409不一致),无健康检查意味着编排层无法判定实例可用性;日志为printer文本 +fmt.Println混流,缺少结构化字段。 - 建议:注册 gRPC health service 并透出
/healthz;启用 APM/trace(ParseMetaCtx已有 claim 可作 trace 维度);日志 JSON 化并带 request-id。
27. 配置缺陷:注释中残留真实密钥、全环境占位符、无效 Anonymous 项、模块 yaml 在聚合模式下不被加载
- 位置:
etc/cms_dev.yaml:24-29、etc/cms_prod.yaml:24-29、etc/cms_test.yaml:24-29、etc/cms_dev.yaml:15-16、pkgs/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/Cache,config.go:49);③Anonymous: cms.ping.hello(prod/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-10、internal/models/cms_relate_tags.go:8-11、internal/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:45Order("created_at desc"))缺(post_identity, created_at desc)复合索引,数据量大后排序走磁盘;cms_relate_category/cms_relate_tags允许同一文章重复关联同一分类/标签(models/query.go:58-73不去重),导致PostList的left join计数放大(query.go:121-133);cms_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/**/*.http(32 个文件) - 证据:
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/Create(pb/post.pb.gw.go:1321-1322、pb/site.pb.gw.go:410-412),且路由大小写敏感;Cms.Post/DeletePost、Cms.Category/ModifyCategory、Cms.Tags/{AddTags,TagsList,ModifyTags,DeleteTags}、Cms.PostPages/*这些方法名在 proto 中都不存在(应为cms.Post/Delete、cms.Category/Modify、cms.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/、Dockerfile、docker-compose.yml、Makefile、.githooks、build/、logs/均存在(实际模块内不存在,已用目录清单核对);声称"完善的错误处理和输入验证""监控:健康检查和APM集成"与实现不符;DDL 章节(README:531-617)与模型不一致(cms_post.typevstypes、keyvshash、cmsvscontent;cms_tags缺site_identity);API 表格未记录已注册的Site服务;Port: 12101/12102与实际12410/12409不一致。 - 建议:以代码为准重写 README(或标注"部分为规划");至少修正端口、DDL、缓存章节与服务清单,避免误导使用者按
make run部署出一个无鉴权的独立实例(见 P1-12)。
31. 注释/命名错误与陈旧路径
- 位置:
internal/models/cms_site.go:28、internal/models/cms_pages.go:6,15、internal/models/cms_tags.go:6、internal/models/cms_comment.go:6、internal/models/cms_accessory.go:25、internal/logic/post/modify.go:18(// 修改文章出现在 pages)、internal/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:6、cms_pages.go:6、cms_tags.go:6),检索与新人理解成本上升;cms_accessory.go:25注释"分类表"实为附件表。 - 建议:清理陈旧路径与错误注释,
golangci-lint的godot/revive纳入 CI。
32. 硬编码表名(跨模块)与魔法数字
- 位置:
internal/logic/post/add_comment.go:35(Table("mall_staff"))、internal/logic/pages/fetch.go:91,95,98、internal/logic/pages/get_by_identity.go:21、internal/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为跨模块表名硬编码;50(pageSize 上限)、255(长度校验)、10(默认 size)散落在各 logic 文件中重复定义。 - 建议:统一用
models的TableName()/常量,跨模块数据改走对方 service 接口;把分页/长度常量集中到internal/config或const块。
33. 无任何 Go 单元测试;cmd/cli 为空壳
- 位置:模块内
*_test.go= 0 个(Get-ChildItem -Include *_test.go | Measure-Object→ 0);cmd/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-29),CI 无从回归。 - 建议:优先用
sqlmock/sqlite或 dockertest 建 5 个最小用例(文章 CRUD 关联、评论分页、赞踩幂等、Search、分类唯一性),并把.http用例换成可执行的集成测试。
4. 推荐优化方案
- 鉴权与授权分层(对应 P0-1、P1-12、P2-13)
- 宿主层:为 CMS 写方法建立独立 admin 受众(aud)/密钥,不再与 C 端共用 HS256 key;
Anonymous列表不包含任何cms.方法。 - 模块层:
service.ParseMetaCtx(ctx, &service.ParseOptions{RoleValue: "admin"})统一入口;数据访问强制属主条件(identity + author_identity/owner_identity),评论删除/修改仅允许作者或管理员。 - 独立模式(
cmd/main)补齐grpc.ChainUnaryInterceptor(auth, recovery, timeout, logging)与Mux初始化,否则删除该入口。
- 宿主层:为 CMS 写方法建立独立 admin 受众(aud)/密钥,不再与 C 端共用 HS256 key;
- 数据一致性(对应 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,或分类/标签改硬删除并级联清理关联表。
- 查询正确性(对应 P1-3、P1-4、P1-5、P1-11、P2-19、P2-28)
Search用title/content并加稳定排序;修正make(len)+append;分页 clamp 前置;所有列表强制site_identity过滤;补 4 个复合索引。- 引入 repository 层封装(禁止 logic 直接拼 SQL/表名),统一
WithContext+LIMIT/OFFSET+ 排序约定。
- 契约与文档(对应 P1-2、P2-20、P2-25、P3-29~33)
- Site 服务:实现或下架;
StatusReply统一构造器;Create/Modify 复用同一映射;README 按代码重写;.http用例按 gateway pattern 校正并纳入 CI。
- Site 服务:实现或下架;
- 可观测性与健壮性(对应 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/Delete、cms.Post/ModifyComment改删 B 用户数据返回PermissionDenied,且ParseMetaCtx传入角色选项|涉及:module/base/cms/internal/logic/post/modify.go:16、module/base/cms/internal/logic/post/delete.go:29、module/base/cms/internal/models/query.go:207、module/base/cms/internal/logic/post/delete_comment.go:24 - P1-2 补齐或下架
Site五个方法(不得返回假成功)|验收:cms.Site/Create后Get/Fetch能查到记录;未实现前返回Unimplemented|涉及:module/base/cms/internal/logic/site/create.go:20、module/base/cms/internal/logic/site/get.go:24 - P1-3 修正 Search 的列名|验收:带
keyword调cms.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-201、module/base/cms/internal/logic/pages/modify.go:68-99 - P1-7 文章详情返回标签与分类数据|验收:
GetByIdentity的tags_data[].title非空、tags_identity_array为真实标签 identity、category_data非空|涉及:module/base/cms/internal/models/query.go:242、module/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:76、module/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 次IncrPostLike后like_hits=1;Desc*不会低于 0|涉及:module/base/cms/internal/models/query.go:346、module/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-139、module/base/cms/internal/logic/category/fetch.go:20、module/base/cms/internal/logic/tags/create.go:34 - P1-12 独立模式补鉴权拦截器并初始化
Mux|验收:独立进程启动后无 token 调cms.Post/Delete返回 Unauthenticated|涉及:module/base/cms/internal/server/new.go:20-30、module/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-25、module/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:119、module/base/cms/internal/logic/post/comment_list.go:30 - P2-17 DB 访问绑定 ctx 并设超时|验收:请求取消后 SQL 终止;启动期 Ping 失败即退出|涉及:
module/base/cms/internal/impl/impl.go:43、module/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:20、module/base/cms/internal/logic/category/fetch.go:20 - P2-19 统一
Types列类型并修正分页/排序边界|验收:type=1过滤有效、所有列表有稳定ORDER BY、page_size>50被夹到 50|涉及:module/base/cms/internal/models/cms_post.go:26、module/base/cms/internal/logic/post/search.go:31-38 - P2-20 Create/Modify 字段映射对齐并支持清空语义|验收:Create 可写
lang/source_origin/rights/extend_*/published;has_accessory可置 false|涉及:module/base/cms/internal/logic/post/create.go:40-56、module/base/cms/internal/models/query.go:171 - P2-21 清理/接线 dead code(
InitData、CategoryList、models.ModifyComment、fmtKey、无用参数)|验收:go vet+deadcode无告警,根分类由确定性的迁移步骤创建|涉及:module/base/cms/internal/models/query.go:23、module/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:48、module/base/cms/internal/models/query.go:89 - P2-24 评论树分层分页、校验文章存在、计数下限|验收:三层回复可返回;
comment_hits不低于 0;不存在的post_identity返回 NotFound|涉及:module/base/cms/internal/logic/post/comment_list.go:45、module/base/cms/internal/models/query.go:277-334 - P2-25 统一响应构造与时间格式|验收:所有接口
code/message/details/timeseq语义一致,时间为 RFC3339|涉及:module/base/cms/internal/logic/post/create.go:68、module/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-24、module/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:20、module/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:1、module/base/cms/test/pages/search.http:1 - P3-30 按代码重写 README(端口、DDL、缓存章节、Site 服务、目录结构)|验收:README 与实际端口/表字段逐一核对通过|涉及:
module/base/cms/README.md:403-430、module/base/cms/README.md:531-617 - P3-31 清理错误注释与陈旧路径|验收:无
/src/git.buka.tv残留、结构体注释与类型一致|涉及:module/base/cms/internal/models/cms_site.go:28、module/base/cms/internal/models/cms_pages.go:6 - P3-32 去硬编码表名/魔法数字|验收:无
Table("mall_staff")与字符串表名,分页常量集中定义|涉及:module/base/cms/internal/logic/post/add_comment.go:35、module/base/cms/internal/logic/pages/fetch.go:91 - P3-33 建立最小单测集并接线 CI|验收:
go test ./...覆盖文章关联写入、评论分页、赞踩幂等、Search、分类唯一性,CI 必过|涉及:module/base/cms/internal/logic/post/search.go:37、module/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 个动作:
- 给所有写路径补属主/角色校验(模块内
ParseMetaCtx带角色 + SQL 带author_identity/owner_identity),并为 CMS 使用独立管理端密钥/受众。 - 修复必然/高概率数据损坏:
Search列名与切片构造(search.go:37,43)、CommentList分页恒空(comment_list.go:18-46)、ModifyPost附件post_id=0/file_path=identity与"空数组清空关联"(models/query.go:162-201)。 - 明确
Site服务去留(实现或下架,禁止假成功),并补齐 5 个最小单测用例接入 CI(当前模块 0 个*_test.go,32 个.http用例全部失效)。
- 给所有写路径补属主/角色校验(模块内
- 未能覆盖/无法验证的部分:真实数据库 schema(
cms_post是否遗留cms列 → 影响 P1-3 是"必然报错"还是"可能报错");前端是否以 HTML 渲染content(XSS 实际可利用性);独立部署模式(cmd/main+ SDKservice.Start)的网关/鉴权实际运行结果;etc/cms_prod.yaml与线上投放配置的差异;pb/生成代码内部逻辑(未逐行审阅)。