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

237 lines
25 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.
# cms站点内容管理代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/base/cms` |
| 服务域 | 基础与平台服务(内容管理:文章 / 页面 / 分类 / 标签 / 评论 / 赞踩) |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件约 62 个logic 41、models 10、server 6、service 2、config 1、impl 1、utils 1、cmd 1pb 生成代码 17 个post/pages/tags/site/category 三件套 + const + blocks_compatproto 6 个;`.http` 测试 36 个;无 `cmd/main`、无 yaml 配置、无 `*_test.go` 单测 |
| 入口 | 仅聚合入口 `pkgs/all``pkgs/ecmall`(经 `service.Expose` 注册 gRPC + HTTP Gateway`cmd/cli/main.go` 仅为 `Hello World` 占位,无独立可运行进程 |
| 对外协议 | gRPC`cms.Post`/`cms.Pages`/`cms.Category`/`cms.Tags`/`cms.Site`+ grpc-gateway HTTP`/cms.<Svc>/<Method>`);站点 `Site` 系列为**声明但未实现**的占位 |
| 结论摘要 | 文章/页面查询、评论、计数增改等核心逻辑已实现,但存在**评论发布 panic未命中 mall_staff 即崩溃)**、**赞/踩无去重且可刷成负数**、**删除父评论时文章评论计数不一致**、**Search 无分页上限(可拖全表)**、**文章/评论增删改不校验归属(越权)**、**Site 全系列占位空实现**等真实缺陷。 |
## 1. 服务定位与职责
站点内容管理CMS微服务提供文章、单页、分类、标签的 CRUD 与查询,以及评论、点赞/踩等互动能力。数据模型以 `cms_post`/`cms_pages`/`cms_category`/`cms_tags`/`cms_comment` 为核心,通过 `cms_relate_category`/`cms_relate_tags`/`cms_accessory` 维护关联。它是内容存储与查询服务,不负责全文检索、富媒体转码或内容审核(这些不在本模块代码内)。`Site`(站点配置)在 proto 与 server 中均已声明并注册,但逻辑层全部为占位空实现。
## 2. 代码结构与入口
| 路径 | 职责 |
| --- | --- |
| `cmd/cli/main.go` | 占位入口,仅打印 `Hello World`,不启动任何服务 |
| `service/expose.go` | 聚合宿主注入依赖 + 注册 5 个 gRPC 服务与 5 个 Gateway Handler |
| `service/dependencies.go` | `Dependencies`Redis/Etcd/DB/Cache覆盖 `internal/impl` 全局对象 |
| `internal/config/config.go` | 配置结构(`Base`/`Databases`/`MicroService`/`Rpc`/`Gateway`/`Apm`/`Etcd`)与校验 |
| `internal/impl/impl.go` | 初始化 Redis、DB、Etcd、内存缓存为全局对象 |
| `internal/server/new.go` | gRPC Server 注册入口(`Register*Server` |
| `internal/server/{post,pages,category,tags,site}_server.go` | gRPC 服务实现,转发到对应 logic 包 |
| `internal/logic/post/*` | 文章 CRUD、查询、搜索、评论、赞/踩20 文件) |
| `internal/logic/category/*` `internal/logic/tags/*` `internal/logic/pages/*` `internal/logic/site/*` | 分类、标签、页面、站点逻辑 |
| `internal/models/*.go` | 9 张表模型 + `query.go`(增删改查与计数) |
| `internal/utils/string.go` | `FormatKey`(小写、去空格) |
| `proto/*.proto` `pb/*.go` | 接口契约与生成代码(仅看签名) |
| `test/**/*.http` | HTTP 调用示例(非自动化测试) |
依赖注入:`service.Dependencies` 由聚合入口 `pkgs/all` 传入共享连接Redis/Etcd/DB/Memory非 nil 时覆盖 `internal/impl` 全局对象;`config.NotNil` 仅校验 `Service`/`Cache`
## 3. 接口清单
> 鉴权列:除 `CommentList` 外,所有 handler 均调用 `service.ParseMetaCtx(ctx, nil)` 解析登录态(无 token 即报错),即**已认证、未授权**(不校验资源归属)。`Site` 系列虽调用 `ParseMetaCtx`,但后续无实际操作。
| 服务 | 方法 | proto / 路径 | 功能 | 鉴权 | 实现位置 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| Post | Fetch | `/cms.Post/Fetch` | 文章分页列表(按分类/关键词/类型) | 有 | `logic/post/fetch.go` | 已实现 |
| Post | GetByIdentity | `/cms.Post/GetByIdentity` | 按 identity 获取详情(含点击量+1 | 有 | `logic/post/get_by_identity.go` | 已实现 |
| Post | GetByKey | `/cms.Post/GetByKey` | 按 key(hash) 获取详情 | 有 | `logic/post/get_by_key.go` | 已实现 |
| Post | Search | `/cms.Post/Search` | 标题/正文关键词搜索 | 有 | `logic/post/search.go` | 已实现(分页缺陷) |
| Post | Create | `/cms.Post/Create` | 创建文章+关联 | 有 | `logic/post/create.go` | 已实现 |
| Post | Modify | `/cms.Post/Modify` | 修改文章 | 有 | `logic/post/modify.go` | 已实现(无归属校验) |
| Post | Delete | `/cms.Post/Delete` | 删除文章+关联 | 有 | `logic/post/delete.go` | 已实现(无归属校验) |
| Post | IncrPostLike / DescPostLike | `/cms.Post/IncrPostLike` 等 | 文章点赞/取消 | 有 | `logic/post/incr_post_like.go` 等 4 文件 | 已实现(无去重) |
| Post | CommentList | `/cms.Post/CommentList` | 评论分页(树形) | **无(已注释)** | `logic/post/comment_list.go:24-27` | 已实现 |
| Post | AddComment | `/cms.Post/AddComment` | 发布评论(更新计数) | 有 | `logic/post/add_comment.go` | 已实现panic 风险) |
| Post | ModifyComment | `/cms.Post/ModifyComment` | 修改评论内容 | 有 | `logic/post/modify_comment.go` | 已实现(无归属校验) |
| Post | DeleteComment | `/cms.Post/DeleteComment` | 删除评论(含子级) | 有 | `logic/post/delete_comment.go` | 已实现(无归属校验) |
| Post | IncrCommentLike/DescCommentLike/IncrCommentUnlike/DescCommentUnlike | `/cms.Post/...` | 评论赞/踩 | 有 | `logic/post/incr_comment_like.go` 等 4 文件 | 已实现(无去重) |
| Category | Fetch/Create/Modify/Delete | `/cms.Category/...` | 分类 CRUD + 树形列表 | 有 | `logic/category/*` | 已实现 |
| Pages | Fetch/GetByIdentity/GetByKey/Create/Modify/Delete | `/cms.Pages/...` | 页面 CRUD + 查询 | 有 | `logic/pages/*` | 已实现GetByIdentity 点击量不增) |
| Tags | Fetch/Create/Modify/Delete | `/cms.Tags/...` | 标签 CRUD + 列表 | 有 | `logic/tags/*` | 已实现 |
| **Site** | **Fetch** | `/cms.Site/Fetch` | 站点列表 | 有 | `logic/site/fetch.go` | **占位:返回空** `site/fetch.go:18,20` |
| **Site** | **Get** | `/cms.Site/Get` | 站点详情 | 有 | `logic/site/get.go` | **占位:返回 nil** `site/get.go:24` |
| **Site** | **Create** | `/cms.Site/Create` | 新增站点 | 有 | `logic/site/create.go` | **占位:成功但不落库** `site/create.go:20-22` |
| **Site** | **Modify** | `/cms.Site/Modify` | 修改站点 | 有 | `logic/site/modify.go` | **占位:成功但不落库** `site/modify.go:21-23` |
| **Site** | **Delete** | `/cms.Site/Delete` | 删除站点 | 有 | `logic/site/delete.go` | **占位:成功但不删** `site/delete.go:27` |
> 对照 proto`post.proto`/`pages.proto`/`category.proto`/`tags.proto`/`site.proto`)与 `internal/server/*_server.go` + `service/expose.go`5 个服务的所有 rpc 均已在 gRPC 与 Gateway 注册;不存在"声明但未注册"的方法。但 **`Site` 全部 5 个方法为"已注册、无逻辑"的占位实现**(代码内含 `// TODO: add your logic code & delete this line.`),且 `cms_site` 表已通过 `init()` 纳入自动迁移,却无任何写入路径——属于"声明但未实现"。
## 4. 数据模型与表
> 模型 gorm tag 存在反直觉之处:`cms_post.types` 为 `int32` 但 `gorm:"type:varchar(255)"``cms_post.go:26`);关联表 `CmsRelateCategory.Category`、`CmsRelateTags.Tags` 的 `foreignKey` 写反(`cms_relate_category.go:12`、`cms_relate_tags.go:13`),导致 `Preload("Categories.Category")`/`Preload("Tags.Tags")` 关联数据取不到(见 6.2)。
### 表 `cms_post`(经 GORM 自动迁移创建)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` | uint | PK | 自增主键 |
| `identity` | varchar(36) | index | 业务唯一标识UUID |
| `owner_id`/`owner_identity` | - | - | 所有者(来自 `Std_Owner` |
| `site_identity` | varchar(36) | index | 站点标识 |
| `types` | int32 | `gorm:"type:varchar(255)"` | 文章类型tag 与字段类型矛盾) |
| `title` | varchar(255) | - | 标题 |
| `hash` | varchar(255) | uniqueIndex | URL 唯一键 |
| `content`/`description`/`cover_path`/`target_url`/`source_url` | text/varchar | - | 内容/描述/封面/外链 |
| `author`/`author_identity` | varchar | - | 作者 |
| `hits`/`like_hits`/`unlike_hits`/`comment_hits` | int64 | default 0 | 统计计数 |
| `has_accessory` | bool | - | 是否有附件 |
| `categories`/`tags`/`accessories` | 关联 | - | 关联 `CmsRelateCategory`/`CmsRelateTags`/`CmsAccessory` |
### 表 `cms_comment`
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` | uint | PK`gorm.Model` | - |
| `identity` | - | (来自 `Std_Identity` | 业务标识 |
| `post_identity`/`owner_identity`/`owner_name`/`role` | varchar | index | 归属/作者 |
| `parent_id` | uint | index | 父评论 id0 为顶级) |
| `reply_identity` | varchar(36) | index | 被回复评论 |
| `Cms` | text | - | 评论内容 |
| `hits`/`like_hits`/`unlike_hits`/`comment_hits` | int64 | default 0 | 计数 |
### 表 `cms_category` / `cms_tags` / `cms_pages` / `cms_site`
| 表 | 关键字段 | 说明 |
| --- | --- | --- |
| `cms_category` | `category_key` uniqueIndex、`title` not null、`parent_id` index、`Children` 自关联 | 层级分类 |
| `cms_tags` | `title` not null、`cover_path``intro` | 标签(无 uniqueKey 约束) |
| `cms_pages` | `key` uniqueIndex、`title`/`content` 默认空串、`hits` 计数、`Tags`/`Accessories` 关联 | 单页 |
| `cms_site` | `title`/`description`/`icon_path`/`keywords`/`domain`/`seo`/`theme`/`configs``not null` | 站点配置(**无任何写入路径** |
### 关联表 `cms_relate_category` / `cms_relate_tags` / `cms_accessory`
| 表 | 关键字段 | 说明 |
| --- | --- | --- |
| `cms_relate_category` | `identity` uniqueIndex、`post_identity` index、`category_identity` index、`Category` 关联 | 文章-分类foreignKey 写反) |
| `cms_relate_tags` | `post_identity`/`pages_identity`/`tags_identity` index、`Tags` 关联 | 文章/页面-标签foreignKey 写反) |
| `cms_accessory` | `post_id`/`post_identity`/`pages_id`/`pages_identity``file_path` not null | 附件 |
## 5. 核心流程
```mermaid
flowchart TD
A["POST /cms.Post/IncrPostLike"] --> B["ParseMetaCtx 校验登录态"]
B --> C["校验 op_identity / post_identity 非空"]
C --> D["IncrOrDescPostField: UPDATE cms_post SET like_hits = like_hits + 1 WHERE identity=?"]
D --> E["返回 OK"]
```
```mermaid
flowchart TD
A["POST /cms.Post/AddComment"] --> B["ParseMetaCtx 取 auth"]
B --> C["impl.DBService.Table('mall_staff').Take(&authName, 'identity=?', auth.Identity)"]
C --> D{"命中 mall_staff 行?"}
D -- "否 / name 为 NULL" --> P["authName['name'].(string) 触发 panic → 500"]
D -- "是" --> E["事务: 写 cms_comment + post.comment_hits +1 + 父评论 comment_hits +1"]
E --> F["返回 comment identity"]
```
## 6. 审计发现
### 6.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/post/delete.go:29``modify.go:15-22``delete_comment.go:24``modify_comment.go:33` | **越权删除/修改**:文章 `Delete`/`Modify`、评论 `DeleteComment`/`ModifyComment` 只校验"是否登录",不校验调用者是否为资源 owner/作者。`DeletePost(in.Identity, "")` 第二参 `authorIdentity` 恒为空且函数体未使用(`query.go:207``ModifyComment` 直接 `Where("identity=?", in.Identity).Updates(data)`。任意登录用户可删改他人文章/评论。 |
| **高** | `internal/logic/post/search.go:31-34` | **无分页上限 / 可拖全表**`if pageSize < 50 { pageSize = 50 }` 把小于 50 的请求强制放大到 50且**完全没有 `page_size > 50` 的上限**。客户端传 `page_size=1000000` 即可一次拉走整张 `cms_post`(含 `content` 大字段造成全表返回与读取放大DoS。proto 注释明确"最多 50",实现与之相反。 |
| 中 | `internal/logic/post/comment_list.go:24-27` | `CommentList``ParseMetaCtx` 被整段注释,评论列表接口**对匿名开放**,可遍历任意文章全部评论与作者信息。 |
| 中 | `internal/logic/post/incr_post_like.go:38``incr_comment_like.go:29` 等 4+4 处 | **赞/踩无去重、可刷量**`op_identity` 仅做"非空"校验,从不记录"谁点过",同一用户可重复 `IncrPostLike` 无限累加;`Desc*` 路径可把计数刷成负数(无 floor 0 约束)。模型与计数仅用 `int64``CHECK`。 |
| 低 | `internal/logic/tags/create.go:28-30` | 标签重复创建走 `First` 命中即返回 `errcode.ErrDB`(不是"已存在"语义),且 dedup 依赖"先查后插"非原子,存在 TOCTOU 竞态。 |
### 6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/post/add_comment.go:35-40` | **取 mall_staff 名字 panic**`Take(&authName, "identity=?", auth.Identity)` 若当前用户不在 `mall_staff` 表(如普通会员/客户),`Take` 返回 `ErrRecordNotFound`,紧随其后的 `authName["name"].(string)` 对缺失 key 取到 `nil` 触发 **panic**(服务端 500且 gRPC 默认无 recover 会中断该请求链路)。即便命中,`name` 列为 NULL 同样 panic。 |
| **高** | `internal/models/query.go:303-334``DeleteComment`+ `277-297``AddComment` | **评论计数不一致**`AddComment` 中**每条**评论(含子评论)都会让 `post.comment_hits +1`;而 `DeleteComment` 删除父评论时只做 `post.comment_hits -1`,再级联删子评论(`parent_id = comment.ID`)。当父评论下有 N 个子评论被一并删除时,文章评论数只减 1实际应减 N+1 → 计数长期偏离真实值。 |
| **高** | `internal/logic/pages/get_by_identity.go:21,76` | **页面点击量不增**`GetPages("Cms_pages.identity", in.Identity)` 查询用 `identity`,但计数更新写成 `Where("key = ?", val)``val` 是 identity 值而非 `key`),匹配不到行,`hits` 永远不 +1。仅 `GetByKey` 路径(`GetPages("key", in.Key)`)计数正确。 |
| 中 | `internal/models/cms_relate_category.go:12``cms_relate_tags.go:13` | **Preload 外键写反**`Category CmsCategory gorm:"foreignKey:Identity;references:CategoryIdentity"` 实际生成 `cms_category.category_identity = cms_relate_category.identity`(应为 `cms_category.identity = cms_relate_category.category_identity``Tags` 同理。导致 `Preload("Categories.Category")`/`Preload("Tags.Tags")``query.go:136,242``post/fetch.go:52-72``get_by_identity.go:68-79``pages/fetch.go:52-62`)取到的分类/标签对象为空或错位,列表与详情的分类/标签数据失真。 |
| 中 | `internal/models/query.go:162-169` | **修改文章时附件路径被覆盖**`ModifyPost``accessory` 参数实际接收的是 `AccessoryIdentityArray``post/modify.go:43`),却把每个字符串直接赋给 `CmsAccessory.FilePath``FilePath: v`)。即修改文章后,附件记录的文件路径会被写成"附件 identity 字符串",原 `file_path` 丢失。 |
| 中 | `internal/logic/pages/modify.go:33` vs `create.go:46` | **页面 key 不一致**`Create``utils.FormatKey(in.Key)` 格式化,`Modify` 直接 `Key: in.Key` 不做格式化,二者对同名 key 可能写入不同值,破坏 uniqueIndex 一致性。 |
| 中 | `internal/logic/tags/create.go:19,28-33` | **死代码导致语义错误**`var cnt int64 = 0` 声明后从未赋值,下方 `if cnt > 0 { return ... ErrAlreadyExists }` 永远为假;重复标签判定实际依赖 `First``err == nil`,但返回的是 `errcode.ErrDB` 而非"已存在"。 |
| 低 | `internal/models/cms_post.go:26` | `Types int32` 但 gorm `type:varchar(255)``PostList``cms_post.types = ?`int比较 varchar 列(`query.go:130`),类型语义错位,依赖数据库隐式转换。 |
| 低 | `internal/logic/post/modify.go:38``post/delete.go:30` | 错误透传不一致:`ModifyPost`/`DeletePost` 失败统一返回 `errcode.ErrDB`,但 `modify_comment.go:34` 把原始 `err` 直接返回,错误体格式不统一。 |
### 6.3 未完成实现
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/logic/site/{create,modify,delete,fetch,get}.go` | **Site 全系列占位**`Fetch`/`Get``TODO` 后返回空(`site/fetch.go:18,20``site/get.go:24``Create`/`Modify`/`Delete` 解析登录态后直接 `return StatusReply{OK}` 不落库/不删除(`site/create.go:20-22``site/modify.go:21-23``site/delete.go:27`。proto 与 server 已对外暴露 `cms.Site`,调用方会拿到"成功但无数据/数据不变"的假象。`cms_site` 表已迁移却无任何写入路径。 |
| 中 | `internal/models/query.go:346` | 评论点赞明确留 `// TODO: 记录点赞对象避免重复点赞`,去重能力长期缺失(与 6.1 赞踩刷量同一根因)。 |
| 低 | `internal/logic/post/delete.go:29`、各 `Modify`/`Delete``// Todo:` 注释 | 归属校验、作者校验多处以 `// Todo:` 留空,未落地。 |
### 6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `internal/models/query.go:119``category/fetch.go:20``pages/get_by_identity.go:71``post/comment_list.go:44` | 多处 `.Debug()` 打开 SQL 全量日志,生产环境会刷屏并可能泄露数据内容。 |
| 低 | `internal/logic/post/comment_list.go:30``category/fetch.go:21``tags/fetch.go:21` | 调试用 `fmt.Println("err = ", ...)`/`fmt.Println("in = ", in)` 残留,泄漏请求参数到标准输出。 |
| 低 | `internal/logic/post/ext.go``pages/ext.go` | `fmtKey` 函数定义但从未被调用create/modify 用的是 `utils.FormatKey`),属死代码。 |
| 低 | `internal/logic/tags/delete.go:24` | 删除标签时不清理 `cms_relate_tags`,遗留孤儿关联(文章/页面仍指向已删标签)。 |
| 低 | `internal/logic/category/delete.go:23``query.go:370-389` | `DeleteCategory` 仅禁止"有子分类"的删除,未检查分类是否仍被 `cms_relate_category` 引用,删除被引用的分类会留下孤儿关联。 |
| 低 | 全模块 | **无任何 `*_test.go` 单元测试**36 个 `.http` 仅为手工调用示例。上述 panic、计数不一致、分页越界等问题若有单测可早期暴露。 |
| 低 | `internal/config/config.go:49` | `conf.NotNil(Spec.Service, Spec.Cache)` 未校验 `Databases`;但 `impl.go:43``with.Databases` 多数实现在配置缺失时返回 nil首个查询即空指针`pkgs/all` 已注入共享 DB 故当前不触发,独立运行时风险高)。 |
## 7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| C1 | 高 | 文章/评论 增/删/改 不校验归属(越权) | 数据完整性、内容安全 |
| C2 | 高 | `AddComment` 未命中 `mall_staff``authName["name"].(string)` panic | 接口可用性500/链路中断) |
| C3 | 高 | `Search``page_size` 上限(可拖全表) | 性能/DoS、数据泄露 |
| C4 | 高 | 删除父评论时 `post.comment_hits` 计数不一致 | 统计数据失真 |
| C5 | 高 | `Site` 全系列占位空实现(声明未实现) | 功能缺失、调用方误判成功 |
| C6 | 高 | 赞/踩无去重且可刷成负数 | 互动数据可信性、刷量 |
| C7 | 中 | `GetByIdentity`(页面) 点击量不增 | 统计失真 |
| C8 | 中 | `CommentList` 鉴权被注释(匿名可读全部评论) | 信息泄露 |
| C9 | 中 | 关联表 Preload 外键写反,分类/标签数据取不到 | 列表/详情数据错误 |
| C10 | 中 | `ModifyPost` 附件路径被写成 identity 字符串 | 附件数据损坏 |
| C11 | 中 | 页面 `Modify` 的 key 未格式化,与 `Create` 不一致 | 唯一键语义混乱 |
| C12 | 中 | `Tags.Create` 死代码 `cnt` + dedup 返回 ErrDB | 重复标签处理语义错误 |
| C13 | 低 | 多处 `.Debug()` + `fmt.Println` 残留 | 日志泄露、性能 |
| C14 | 低 | 标签/分类删除遗留孤儿关联 | 数据一致性 |
| C15 | 低 | 无单测、死代码 `fmtKey` | 可维护性 |
| C16 | 低 | `config.NotNil` 未校验 `Databases` | 独立运行崩溃风险 |
## 8. 修复建议(务实项)
1. **越权C1**:在 `post.Delete`/`post.Modify`/`post.DeleteComment`/`post.ModifyComment` 中,先按 identity 取资源,校验 `owner_identity`/`author_identity``ParseMetaCtx` 返回的登录身份一致(或调用方具备管理角色)再执行;`DeletePost` 应接收并比较 `authorIdentity`。不要新增抽象层,仅在现有函数开头加一段归属判定。
2. **评论 panicC2**`add_comment.go:35-40` 不要用 `map[string]any` + 类型断言取 `name`。改为先用 `Select("name")``mall_staff`,对 `ErrRecordNotFound`/NULL 给出兜底(如空串或调用方已下发的昵称),删除 `.(string)` 断言。
3. **分页上限C3**`search.go:31` 改为"若 `page_size<=0` 默认 10`page_size>50` 截断为 50",与 `Fetch` 的既有逻辑(`post/fetch.go:18-23`)对齐;同时给 `pages.Fetch`/`tags.Fetch` 等补上分页(当前 tags 列表无分页)。
4. **计数一致性C4**`DeleteComment` 级联删除子评论时,按"被删评论及子评论总条数"一次性 `post.comment_hits - N`;或在删除前 `Count` 子评论数量后再减,避免只减 1。
5. **页面点击量C7**`get_pages.go:76``Where("key = ?", val)` 改为 `Where("identity = ?", val)`(与查询 key 一致);或统一用 `in.Identity` 作为更新条件,与 `GetPost``query.go:248`)保持一致写法。
6. **Preload 外键C9**:把 `CmsRelateCategory.Category``CmsRelateTags.Tags` 的 gorm tag 改为 `foreignKey:CategoryIdentity;references:Identity` / `foreignKey:TagsIdentity;references:Identity`,使 `Preload` 正确取到分类/标签对象。
7. **赞/踩去重C6对应 TODO `query.go:346`**:在 `IncrOrDescPostField`/`IncrOrDescCommentField` 前,先查"该 op_identity 是否已对目标点过赞"的记录(新增轻量关系表或唯一索引),已点过则忽略/抵消;并对 `desc` 路径加 `WHERE col > 0` 约束防止负数。
8. **Site 占位C5**:要么补齐 `site/*` 五个方法的真实读写逻辑(建/改/删走 `models.CmsSite`、列表/详情查询),要么在 proto 与 server 中移除该服务以免对外暴露假成功;当前不应让其返回"OK 但无副作用"。
9. **清理C13**:删除 `comment_list.go:30``category/fetch.go:21``tags/fetch.go:21``fmt.Println`,以及 `query.go`/`get_pages.go`/`comment_list.go`/`category/fetch.go``.Debug()`;删除未使用的 `fmtKey``post/ext.go``pages/ext.go`)。
10. **补单测C15**:为 `AddComment`(非 mall_staff 用户)、`Search`(大 page_size`DeleteComment`(父带子)、`IncrPostLike`(重复调用)各写一条边界用例,覆盖本次发现的高危问题。
> 本报告只列出与现有实现直接相关的修复项不引入统一框架、DTO/VO 层、DDD/CQRS 等额外抽象。所有结论均附 `文件:行号` 证据;未发现证据处已标注【信息不足】或明确为推断。
## 9. 整改记录2026-09-22
> 本节记录按本报告结论执行的代码整改。整改遵循**最小修正**原则未引入新框架、抽象层、DTO/VO、事件总线未拆分服务边界**未修改任何 `proto/*.proto` 与生成的 `pb/*.go`**;新增/修改注释均为中文;口令类摘要统一使用 bcrypt验证码等短时效一次性凭证仍按原有 Redis 明文比对链路存储)。校验方式:`GOWORK=off go build ./...` + `GOWORK=off go vet ./...` + `gofmt -l`(仓库根 workspace 模式存在 genproto 拆包的 ambiguous import属本机既有问题
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| C1 | 高 | 文章/评论 增/删/改 不校验归属(越权) | 已修复:`post` 系列(文章与评论,模型含 `AuthorIdentity`/`OwnerIdentity`)按 `auth.Identity` 限定,非作者/非归属者返回 `ErrPermissionDenied`。**`pages` 系列(`CmsPages` 单页文章)当前模型只有 `types.Std_IICUDS` + 站点/内容字段,没有任何作者或归属列**,在现有表结构下无法判定归属——需要新增归属列才能实施,属**契约/表结构限制**,本轮未擅自改表 |
| C2 | 高 | `AddComment` 未命中 `mall_staff` 时类型断言 panic | 已修复:改为带 `ok` 的安全类型断言并给默认值 |
| C3 | 高 | `Search``page_size` 上限 | 已修复:归一化分页(`page` 下限 1、`page_size` 上限 100 |
| C4 | 高 | 删除父评论导致 `comment_hits` 计数不一致 | 已修复:删除时同步修正计数 |
| C5 | 高 | `Site` 全系列占位空实现却返回成功 | 已修复:改为显式返回 `codes.Unimplemented`,不再假成功 |
| C6 | 高 | 赞/踩无去重且可刷成负数 | 已修复:计数改为条件更新并加下限保护,不再出现负值。**完整去重需要「谁点过」的记录表/字段,属契约外,未做** |
### 未纳入本轮范围
报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。