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

660 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 审计报告module/base/cms
## 1. 模块概览
- **模块路径**`module/base/cms``bsm/full/module/base/cms`Go 1.26.5gRPC + 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 测试文件**)。
- **运行方式**:两种。
1. 独立进程:`cmd/main/main.go``config.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)``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|Cache|limiter'` | 仅 `internal/impl/impl.go` 初始化处命中,业务逻辑零使用(无缓存、无限流) |
| `grep 'WithContext|Begin()|Commit|Rollback|go func'`internal | 无命中 → DB 调用未绑定请求 ctx无显式事务 API/无 goroutine |
| 读 `$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` 列只能从模型推断)、前端是否为富文本渲染 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-53``pkgs/all/etc/default_dev.yaml:24-43`
- **证据**
```go
// internal/logic/post/delete.go:29
err = models.DeletePost(in.Identity, "") // Todo:
```
```go
// 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 {
```
```go
// 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 {
```
```go
// 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`
- **证据**
```go
// 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
```
```go
// internal/logic/site/get.go:24-26 —— 只做参数非空校验后返回零值
// TODO: add your logic code & delete this line.
return
```
```go
// 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.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`
- **证据**
```go
// search.go:37
tx := impl.DBService.Model(&models.CmsPost{}).Where("title like ? or Cms like ?", "%"+in.Keyword+"%", "%"+in.Keyword+"%")
```
```go
// 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`
- **证据**
```go
// 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`
- **证据**
```go
// 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: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`
- **证据**
```go
// 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})
}
```
```go
// 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)
```
```go
// 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` 恒为 0`update` 时 structure 没有 ID→ 附件与文章的整型外键断开,而 `CmsPost.Accessories` 的关联键是 `foreignkey:PostId``models/cms_post.go:59`),因此文章详情/列表再也 preload 不到这批附件;`FilePath` 被写成附件 identity`in.AccessoryIdentityArray` 是 identity 列表,不是路径),附件显示为随机 ID`Title` 丢失。
2. 关联重建是"先全删、后按入参重建":只要调用方只改标题、不带 `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`
- **证据**
```go
// 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 {
```
```go
// 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`
- **证据**
```go
// 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 {
```
```go
// 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`
- **证据**
```go
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` 的 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}`
- **证据**
```go
// 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))
```
```go
// 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`
- **证据**
```go
// 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-2Site 服务未实现),多站点隔离实际不可用。
- **建议**:所有查询签名透传 `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`、SDK `D:\work\bsm-sdk\core\service\service.go:49-51`
- **证据**
```go
// 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
```
```go
// 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`
- **证据**
```go
// 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 {
```
```go
// 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`
- **证据**
```go
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`
- **证据**
```go
// 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;"`
```
```go
// 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 处
- **证据**
```go
// 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`
- **证据**
```go
// 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`(全量)
- **证据**
```go
// 全模块检索 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`
- **证据**
```go
// 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)
```
```go
// 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`
- **证据**
```go
// 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=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-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-17``internal/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
- **证据**
```go
// 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`
- **证据**
```go
// 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`
- **证据**
```go
// 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`
- **证据**
```go
// 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;
```
```go
// 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`
- **证据**
```go
// 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 无命中)
- **证据**
```yaml
# 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`
- **证据**
```yaml
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`
- **证据**
```go
// 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` 不去重),导致 `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`
- **证据**
```markdown
- **⚡ 高性能**: 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.type` vs `types``key` vs `hash``cms` vs `content``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`
- **证据**
```go
// 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 张表)
- **证据**
```go
// 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`
- **证据**
```go
// 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 key`Anonymous` 列表不包含任何 `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**
- `Search``title/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/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 个动作:
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.go`32 个 `.http` 用例全部失效)。
- 未能覆盖/无法验证的部分:真实数据库 schema`cms_post` 是否遗留 `cms` 列 → 影响 P1-3 是"必然报错"还是"可能报错");前端是否以 HTML 渲染 `content`XSS 实际可利用性);独立部署模式(`cmd/main` + SDK `service.Start`)的网关/鉴权实际运行结果;`etc/cms_prod.yaml` 与线上投放配置的差异;`pb/` 生成代码内部逻辑(未逐行审阅)。