# 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 Gateway,REST 路径 `/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` 实际承载 URL,255 上限偏紧;超长时不同数据库行为不同(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`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。