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

165 lines
15 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.
# ads公共广告位内容分发代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/base/ads` |
| 服务域 | 基础与平台服务 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件 12 个、385 行(含 `test/rpc/rpc.go` 23 行空壳);`proto` 2 个(`ads.proto``const.proto``pb/` 生成文件 4 个 |
| 入口 | `cmd/main`gRPC + HTTP Gateway 单进程)、聚合入口 `pkgs/all``pkgs/all/internal/service/ads.go` |
| 对外协议 | gRPC + REST GatewayREST 路径 `/ads.Fetch/ByPos` |
| 结论摘要 | 全模块只有 1 个只读接口 `ByPos`,落库查询用 `pos_key + status=1` 过滤,能正确屏蔽未启用广告;但 README 宣称的"Redis 缓存 10 分钟 / 广告位 30 分钟缓存"**从未实现**`RedisService`/`MemorySerice` 初始化后无任何引用),因此不存在缓存一致性问题;鉴权策略在"模块 yaml 声明匿名"与"聚合部署未放行"之间自相矛盾;`conf.NotNil` 未校验 `Databases`,缺失时启动直接 panic。 |
## 1. 服务定位与职责
按广告位 `key` 查询该广告位下**已启用**的广告内容(文本/图片/视频/音频/链接/附件),供客户端页面渲染。它只做"读取与分发",不提供广告位/广告的增删改,也不负责投放计费、定向或素材处理。
## 2. 代码结构与入口
| 路径 | 职责 |
| --- | --- |
| `cmd/main/main.go` | 独立进程入口:`config.New``impl.NewImpl``server.New(nil)``service.New(...).Start()``Run()``main()` 分离供聚合入口复用 |
| `cmd/cli/main.go` | 命令行入口,仅 `log.Println("广告服务命令行工具")`8 行,占位) |
| `internal/config/config.go` | 配置结构(`Base`/`Databases`/`MicroService`/`Rpc`/`Gateway`/`APM`/`Etcd`)与校验 |
| `internal/impl/impl.go` | 初始化 DB / Redis / Etcd / 内存缓存实例 |
| `internal/logic/fetch/by_pos.go` | `ByPos` 唯一业务逻辑:按 `pos_key + status=1` 查库并转 pb |
| `internal/models/ads_item.go` | `ads_item` 表模型 + `database.AppendMigrate` 注册 |
| `internal/models/ads_pos.go` | `ads_pos`(广告位)表模型 + 迁移注册,全模块无读写 |
| `internal/server/fetch_server.go` | `FetchServer.ByPos` 转发到 `logic/fetch` |
| `internal/server/new.go` | 注册 `Fetch` 到 gRPC独立运行时开 `reflection` |
| `internal/routers` | **不存在**(本模块走 gRPC + HTTP Gateway无 Gin 路由) |
| `service/{expose,dependencies}.go` | 聚合宿主注入:`Expose` 注册 Gateway handler`applyDependencies` 覆盖 Redis/Etcd/DB/Cache |
| `proto/{ads,const}.proto` | `ads.proto` 定义 `Fetch.ByPos``const.proto` 与本模块无关 |
| `etc/{ads_dev,ads_prod,ads_test}.yaml` | 三份内容一致Port 12216、Gateway 12102 |
| `test/rpc/rpc.go` | 整个 `main` 被注释23 行空壳) |
## 3. 接口清单
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
| --- | --- | --- | --- | --- |
| POST | `/ads.Fetch/ByPos` | 按广告位 `key` 返回已启用广告列表 | 无(模块 yaml 登记匿名,`etc/ads_dev.yaml:15-16` | `internal/logic/fetch/by_pos.go` |
| POST | gRPC `/ads.Fetch/ByPos` | 同上Gateway 反向代理同一 handler | 同上 | `pb/ads.pb.gw.go:71,77``internal/server/fetch_server.go:19` |
| HEAD | `/` | 独立入口健康检查 | 无 | 【信息不足】由 `git.apinb.com/bsm-sdk/core/service` 提供,本模块未声明 |
**声明但未实现/占位的接口**:无。`proto/ads.proto:6-9` 只声明 `Fetch.ByPos` 一个 rpc`pb/ads_grpc.pb.go``FetchServer` 接口也只有 `ByPos`,二者一致;不存在"proto 声明了但 server 未实现"的方法。
> 鉴权补充:`etc/ads_dev.yaml:14` 为 `MicroService.Enable: false`,而 `service.Start` 只在 `MsConf.Enable == true` 时才注册服务并发布匿名清单(`D:\work\bsm-sdk\core\service\service.go:66-90`),因此独立部署时模块 yaml 的匿名清单**不会生效**,接口是否免鉴权完全取决于前置网关。
## 4. 数据模型与表
### 表 `ads_item``internal/models/ads_item.go`GORM 自动迁移)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` | uint | PK | 自增主键(来自 `gorm.Model` |
| `created_at`/`updated_at`/`deleted_at` | timestamp | - | `gorm.Model` 提供,带软删 |
| `title` | varchar(255) | not null | 广告名称 |
| `pos_key` | varchar(255) | not null | 广告位 key无索引声明 |
| `content` | varchar(255) | default '' | 广告内容 |
| `type` | int | default 0 | 广告类型,`ContentType` 常量从 1 起1 文本…6 附件) |
| `to_url` | varchar(255) | default '' | 跳转链接 |
| `status` | int64 | default 0, index | 来自内嵌 `types.Std_Status``D:\work\bsm-sdk\core\types\db.go:74-76` |
### 表 `ads_pos``internal/models/ads_pos.go`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | uint | PK来自 `types.Std_IdCreated` |
| `created_at` | timestamp | 创建时间 |
| `key` | varchar(255) | 广告位标识 |
| `name` | varchar(255) | 广告位名称 |
> 该表模型在本模块内仅注册迁移(`ads_pos.go:21-23`**全模块无任何读写**`ByPos` 也不校验 `pos_key` 是否存在于 `ads_pos`。
## 5. 核心流程
```mermaid
flowchart TD
A["POST /ads.Fetch/ByPos"] --> B["校验 in.Key 非空,否则 ErrInvalidArgument"]
B --> C["SELECT * FROM ads_item WHERE pos_key = ? AND status = 1"]
C --> D["逐条映射为 pb.AdsItem含 CreatedAt 格式化)"]
D --> E["返回 ByPosReply{data: [...]}"]
C --> F["查询失败返回 errcode.ErrDB"]
```
## 6. 审计发现
### 6.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `etc/ads_dev.yaml:15-16``etc/ads_prod.yaml:15-16``etc/ads_test.yaml:15-16` | `- ads.Fetch.ByPos` 被列入 `MicroService.Anonymous`,属**无鉴权读取**的设计声明;同时 `MicroService.Enable: false``ads_dev.yaml:14`)使该清单在独立部署时根本不会发布到 etcd等于"声明了但不会生效",容易让部署方误判防护已就位。 |
| 中 | `pkgs/all/etc/default_dev.yaml:24-43` | 聚合入口的 `Authorization.Anonymous` 白名单里**没有** `/ads.Fetch/ByPos`(只有 passport/mall/market 登录、`/rest/fts/ping|config``/rest/logs/ping``/rest/mgt/*`)。与模块 yaml 的匿名声明直接矛盾:按模块配置匿名调用会被聚合网关按 `pkgs/all/internal/server/authorization.go:42-66` 拒绝,"开发可用、上线 401"。 |
| 低 | `internal/logic/fetch/by_pos.go:21` | 广告查询只以 `pos_key` 为唯一凭据,无 app / 店铺 / 租户维度的归属过滤,`ads_item` 也没有归属字段。任何能调用该接口的客户端,只要知道 `pos_key`,就取到完全相同的一批广告;无法支持"按端/按租户下发不同广告"。 |
### 6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| — | `internal/logic/fetch/by_pos.go:21` | `Where("pos_key = ? AND status = ?", in.Key, 1)`**已正确过滤启用状态,不存在"可查未启用广告"的问题**(按任务要求核实,特此说明)。 |
| 低 | `internal/logic/fetch/by_pos.go:21` | `Find``LIMIT`,单个 `pos_key` 下条目数不受约束,接口全量返回;大广告位会一次性吐出全部记录。 |
| 低 | `internal/models/ads_item.go:32``:18-25` | `type``default:0`,但 `ContentType` 常量从 1 开始(`CONTENT_TYPE_TEXT iota + 1`)。`0` 无对应类型,历史数据或未显式赋值的记录会返回前端无法识别的 `type`。 |
| 低 | `internal/models/ads_item.go:31` | `content``varchar(255)`,而图片/视频/链接类广告的 `content` 实际承载 URL255 上限偏紧超长时不同数据库行为不同PostgreSQL 报错、MySQL 截断),仓库内 yaml 为 postgres【具体行为取决于驱动信息不足】。 |
| 低 | `internal/logic/fetch/by_pos.go:13,21` | 入参 `ctx` 未被使用,请求上下文(超时/取消)不会传导到 DB 查询,无法对慢查询做超时控制。 |
### 6.3 未完成实现
- **缓存完全未落地README 属不实描述**`README.md:13-14`"Redis缓存""智能缓存10分钟缓存策略")、`README.md:110`"Redis缓存加速缓存时间10分钟")、`README.md:319-322`"广告数据 10 分钟 / 广告位信息 30 分钟"缓存策略表)均宣称有缓存;但 `internal/logic/fetch/by_pos.go:21` 直接查库,`internal/impl/impl.go:13,16,22,24` 初始化的 `RedisService``MemorySerice` 在本模块**没有任何引用**(全仓检索仅命中 `internal/impl/impl.go``service/dependencies.go:21,30` 的赋值点)。结论:**不存在缓存,也就不存在缓存一致性问题**`README.md:315-339` 的"性能优化/监控指标"章节同样与实现不符。
- `proto/const.proto:1-326` 定义了 `OrderSummaryItem``FeedPostItem``GroupPostItem``RelationItem``MarketLoginReply` 等大量与广告无关的共享消息,本模块未引用任何一条 → 死定义,且被 `pb/const.pb.go` 一并编译进本模块。
- `internal/models/ads_pos.go:15-28``ads_pos` 模型仅注册迁移,无任何查询/写入 → 预留实现(表由其它模块维护)。
- `cmd/cli/main.go:1-8` 仅打印一行;`test/rpc/rpc.go:3-23` 整个函数体被注释,且引用的是不存在的 `pb.NewMethodClient`/`pb.Crc` → 占位/死代码。
- `etc/ads_dev.yaml:26-29``Rpc` 段被注释,`internal/config/config.go:20``SrvConfig.Rpc` 字段全模块无使用点 → 死配置。
### 6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `internal/config/config.go:37` | `conf.NotNil(Spec.Service, Spec.Cache)` 未校验 `Databases`。而 `with.Databases``D:\work\bsm-sdk\core\with\databases.go:13-15`)在 `cfg == nil \|\| len(cfg.Source) == 0` 时直接 `panic("No Database Source Found !")`,因此缺 `Databases` 段时 `impl.NewImpl()``internal/impl/impl.go:26`)在**启动阶段就 panic**,且报错文本不含配置项名,排查成本高。 |
| 低 | `internal/impl/impl.go:16` | 变量名拼写错误 `MemorySerice`(应为 `MemoryService`)。同一 workspace 的 `pkgs/all/internal/impl` 用的是正确拼写 `MemoryService`(见 `pkgs/all/internal/service/ads.go:15`),跨模块阅读容易误判为两个不同变量。 |
| 低 | `internal/impl/impl.go:13,16,22,24` | `RedisService``MemorySerice` 初始化后在本模块无使用点(`EtcdService` 仅透传给 `service.Options.EtcdClient``cmd/main/main.go:34`)→ 死代码。 |
| 低 | `README.md:88-96` | README 声明的 `swagger/``scripts/``Dockerfile``Makefile` 在仓库中均不存在(与本模块实际文件清单不符),属虚构文档。 |
| 低 | 全模块 | **无任何 `*_test.go`**`test/rpc/rpc.go` 为注释空壳,无法覆盖 `ByPos` 的过滤语义。 |
| 低 | `internal/logic/fetch/by_pos.go:15-17` | 只校验 `in.Key == ""`,无长度/字符白名单与上限约束。 |
## 7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| A1 | 中 | 鉴权策略两处矛盾:模块 yaml 声明 `ByPos` 匿名,聚合部署 `Anonymous` 未放行 | 上线 401 / 误判防护已生效 |
| A2 | 中 | 广告读取仅凭 `pos_key`,无租户/端隔离 | 内容越权读取、无法按租户差异化投放 |
| A3 | 中 | `Databases` 未校验,缺失即启动 panic 且报错不明确 | 部署可用性、排障成本 |
| A4 | 低 | README 宣称的缓存/性能/监控能力全部未实现 | 文档误导、容量评估失真 |
| A5 | 低 | `ByPos` 无返回条数上限 | 大广告位全量返回、响应体膨胀 |
| A6 | 低 | 死代码与死配置(`const.proto``ads_pos``cmd/cli``test/rpc``Rpc` 段)+ 无自动化测试 | 可维护性 |
## 8. 修复建议(务实项)
1. **鉴权策略对齐(二选一,必须选)**:若 `ByPos` 应当匿名,则把 `/ads.Fetch/ByPos` 加入 `pkgs/all/etc/default_dev.yaml:24-43``Authorization.Anonymous`(并同步生产配置);若应当鉴权,则从 `etc/ads_{dev,prod,test}.yaml:15-16` 的匿名清单中删除该条。注意 `MicroService.Enable: false` 时模块匿名清单不会生效,不要把它当作生效依据。
2. **配置校验**`internal/config/config.go:37``conf.NotNil` 补上 `Databases`;或在 `impl.NewImpl` 前显式判空并打印缺失的配置项名。
3. **返回值上限**`internal/logic/fetch/by_pos.go:21``Find` 增加 `.Limit(...)`(如 100避免单广告位无界返回。
4. **类型默认值**`internal/models/ads_item.go:32``type` 默认值改为 `1`(文本),或在 `ByPos` 中跳过 `type == 0` 的记录,避免下发未定义类型。
5. **上下文传导**`by_pos.go` 改用 `impl.DBService.WithContext(ctx)` 发起查询,使上游超时/取消能生效。
6. **文档与死代码清理**:删除/改写 `README.md:13-14,110,319-339` 中"Redis 缓存 10 分钟""广告位 30 分钟缓存""swagger/scripts/Dockerfile/Makefile"等不实内容;删除 `proto/const.proto` 与本模块无关的消息、`test/rpc/rpc.go``etc/ads_dev.yaml:26-29``Rpc` 注释段;变量名 `MemorySerice``MemoryService`,并删除未使用的 `RedisService`/`MemorySerice` 初始化(或按其文档用途落地)。
7. **补测试**:为 `ByPos` 增加 2~3 条最小用例——命中启用广告、同 `pos_key` 下含 `status != 1` 的记录(须被过滤)、`key` 为空。
> 本报告只列出与现有实现直接相关的修复项,不引入新的分层、抽象封装或 DTO/VO 改造。
## 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属本机既有问题
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| A1 | 中 | 鉴权策略两处矛盾(模块 yaml 匿名 vs 聚合未放行) | 未处理:本模块无「高」级问题,本轮未改动 `module/base/ads` 任何文件 |
| A2 | 中 | 广告读取仅凭 `pos_key`,无租户/端隔离 | 未处理(保持现状) |
| A3 | 中 | `Databases` 未校验,缺失即启动 panic | 未处理:本轮未改动本模块,其余服务已按同一模式补上非空校验,可后续对齐 |
| A4 | 低 | README 宣称的缓存/性能/监控能力未实现 | 未处理(文档问题) |
| A5 | 低 | `ByPos` 无返回条数上限 | 未处理 |
| A6 | 低 | 死代码与死配置 + 无自动化测试 | 未处理 |
### 未纳入本轮范围
报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。