Files
full/docs/audit/module-base-ads.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

404 lines
47 KiB
Markdown
Raw Permalink 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/ads
## 1. 模块概览
`base/ads` 是广告内容读取微服务:仅提供一个 RPC `ads.Fetch/ByPos`,按广告位 key 从 `ads_item` 表读取 `status=1` 的广告并转成 protobuf 返回(`internal/logic/fetch/by_pos.go:13-39`)。
代码规模Go 文件 16 个,其中 `pb/` 生成代码 4 个;非生成业务代码 12 个文件、约 301 行(`cmd/cli/main.go` 5 行、`test/rpc/rpc.go` 21 行均为空壳)。入口为 `cmd/main/main.go``Run()``config.New``impl.NewImpl``server.New(nil)` → SDK `service.New/Start`gRPC 端口 12216、HTTP 网关端口 12102`etc/ads_dev.yaml:2,19-21`)。
对外接口形式gRPC + grpc-gateway`pb/ads.pb.gw.go:152``POST /ads.Fetch/ByPos`);另有聚合入口的动态 HTTP `/rpc/ads/Fetch/ByPos``wiki/api/01-ads.md:25`)。
依赖外部组件PostgreSQLgorm、Redis`impl.go:24`、Etcd`impl.go:28`,配置未提供故为 nil、SDK bsm-sdk/core本地 replace
聚合接入:同时被 `pkgs/all/internal/service/ads.go:9-20``pkgs/ecmall/internal/service/ads.go:9-20` 通过 `service.Expose` 注册进聚合进程(配置中 `Services: - ads`);独立进程入口与聚合入口的注册路径不同,是本模块多个缺陷的根源。
## 2. 审计范围与方法
已读文件(全部非生成代码 + 关键配置):`README.md``cmd/main/main.go``cmd/cli/main.go``internal/config/config.go``internal/impl/impl.go``internal/logic/fetch/by_pos.go``internal/models/ads_item.go``internal/models/ads_pos.go``internal/server/{fetch_server.go,new.go}``service/{dependencies.go,expose.go}``test/rpc/rpc.go``proto/{ads.proto,const.proto}``etc/*.yaml``etc/supervisor.bsm-apps-ads.conf``go.mod``.builds/etc/ads_prod.yaml`
生成代码(`pb/*.pb.go``pb/*.pb.gw.go`)只做接口一致性检查:`pb.FetchServer.ByPos(ctx, *ByPosRequest) (*ByPosReply, error)``internal/server/fetch_server.go:19` 一致proto 字段id/title/content/type/toUrl/created`models.AdsItem` 映射一致;`pattern_Fetch_ByPos_0 = /ads.Fetch/ByPos``pb/ads.pb.gw.go:152`)与 README/wiki 一致;未发现生成代码与手写代码的签名冲突。
为判定跨模块调用链,另外读取了 SDK 与聚合服务的确证位置:`bsm-sdk/core/service/service.go``conf/{new.go,types.go}``with/{databases.go,redis.go,etcd.go}``database/{new.go,sql/postgresql.go}``types/db.go``env/env.go``crypto/encipher/encipher.go``cache/redis/redis.go``pkgs/all/internal/server/{server.go,authorization.go}``pkgs/all/etc/default_dev.yaml`
执行命令与结果:`go version` → go1.26.5 windows/amd64`gofmt -l .`(模块目录内)→ 无输出,全部文件已格式化;`go vet ./...``vet_exit=0`,无告警;三份 `etc/ads_*.yaml` 做 SHA256 比对 → 哈希完全相同(`DB300DC1…`)。
未覆盖/无法验证:未运行服务、未连接真实数据库/Redis/Etcd因此运行时行为HTTP 404、panic、SQL 是否报列不存在)为静态推断并逐条标注依据;未读取 `pb/*.pb.go` 逐行内容3000+ 行,仅做接口比对);未覆盖 CI/发布脚本与线上真实配置(配置可能被 `${VAR}` 环境变量或 etcd 覆盖,`conf.New` 使用 `os.ExpandEnv`,见 `bsm-sdk/core/conf/new.go:55`SDK/聚合服务代码仅按需读取,未做完整审计。
## 3. 问题清单
### P0
无。本模块无写接口、无金额/事务路径SQL 全部参数化(`by_pos.go:21` 使用 `?` 占位符),未发现可被利用的注入点;聚合入口对 `/ads.Fetch/ByPos` 强制 JWT`pkgs/all/etc/default_dev.yaml:24-43` 白名单未包含 ads + `authorization.go:43-51,58-63`),未发现可直接绕过的越权写操作。
### P1
#### 1. 独立部署入口cmd/main的 HTTP 网关完全不可用,并且一旦补注册即 panic
- **位置**`module/base/ads/internal/server/new.go:26-30``module/base/ads/cmd/main/main.go:27,37``module/base/ads/service/expose.go:20-23`
- **证据**
```go
// internal/server/new.go:26-30 —— Mux 字段从未初始化
srv := &Server{
Ctx: context.Background(),
Grpc: grpcServ,
grpcConns: make(map[string]*grpc.ClientConn),
}
// cmd/main/main.go:37 —— 把 nil 传给 SDK 作为网关 handler
GatewayMux: s.Mux, // 网关路由
// bsm-sdk/core/service/service.go:126 —— nil handler 落到 DefaultServeMux而本服务从未向其注册任何路由
if err := http.ListenAndServe(httpAddr, s.Opts.GatewayMux); err != nil {
```
- **影响**`cmd/main` 独立部署时 `etc/ads_*.yaml:19-21` 打开 `Gateway.Enable: true`,但网关 handler 是 `nil`,全部 HTTP 请求(含 README:171 与 `wiki/api/01-ads.md:11` 文档化的 `POST /ads.Fetch/ByPos`)返回 404注册网关路由的唯一代码在 `service.Expose``expose.go:23`),而 `cmd/main` 从不调用它,只调用 SDK 的 `service.New`。反向地,若有人按 `deps.GRPC` 路径把 `server.New()` 返回值的 `Mux` 直接交给 `Expose``runtime.ServeMux.Handle` 在 nil 接收者上先访问 `s.middlewares`grpc-gateway v2.30.0 `runtime/mux.go``Handle`)会 panic。即“网关要么 404要么崩”。
- **建议**:在 `internal/server/new.go``New()` 中初始化 `Mux: gwRuntime.NewServeMux()` 并调用 `pb.RegisterFetchHandlerServer(srv.Ctx, srv.Mux, NewFetchServer())`;或在 `cmd/main/main.go` 中改为调用模块自身的 `service.Expose(service.ExposeOptions{GRPC: s.Grpc, Gateway: s.Mux, ...})`,消除两条注册路径的差异。补一条集成测试断言 `POST /ads.Fetch/ByPos` 在独立进程下返回 200。
#### 2. 读路径无索引、无分页、无缓存:每次请求都是一次全表扫描 + 全量返回
- **位置**`module/base/ads/internal/models/ads_item.go:30``module/base/ads/internal/logic/fetch/by_pos.go:21``module/base/ads/README.md:319-326`
- **证据**
```go
// internal/models/ads_item.go:30 —— pos_key 无 index 标签(同文件 status 由 Std_Status 带 index
PosKey string `gorm:"column:pos_key;type:varchar(255);not null;" json:"pos_key"` // 广告位key
// internal/logic/fetch/by_pos.go:21 —— 无 LIMIT / 无 ORDER BY
err = impl.DBService.Where("pos_key = ? AND status = ?", in.Key, 1).Find(&adsItems).Error
```
- **影响**`ads_item` 上唯一被 WHERE 使用的 `pos_key` 没有索引(模型无 `index` 标签,且 `database.AppendMigrate` 的自动迁移默认关闭,见问题 7条件 `pos_key = ? AND status = ?` 无法走索引 → 每次请求全表扫描;`Find``Limit`,命中行数无上限,全部加载进内存并序列化返回(`by_pos.go:27-37`大广告位会造成内存与响应体膨胀。README:326 声称“在 pos_key 和 status 字段建立复合索引”、README:14/110/319-323 声称“Redis 缓存 10 分钟、按广告位缓存”,代码中均无实现(见问题 16。叠加匿名访问问题 3构成可被外部放大的资源耗尽路径。
- **建议**:迁移中为 `ads_item(pos_key, status)` 建复合索引(`gorm:"index:idx_pos_status,priority:1"` + `priority:2`),并让迁移可执行;查询加 `Order("id ASC").Limit(N)`N 由请求参数或配置上限控制,默认如 50`context` 超时;按 `pos_key` 做短 TTL 缓存并在管理端写路径失效(若后续新增写接口)。
#### 3. 独立入口的 ByPos 完全无鉴权、无限流,并默认开启 gRPC 反射
- **位置**`module/base/ads/etc/ads_dev.yaml:15-16``internal/server/new.go:23,36``internal/logic/fetch/by_pos.go:15-17`
- **证据**
```yaml
# etc/ads_dev.yaml:13-16 —— 模块自己声明该 RPC 免鉴权(三份配置相同)
MicroService:
Enable: false
Anonymous:
- ads.Fetch.ByPos
```
```go
// internal/server/new.go:23,36 —— 无拦截器、开启反射
grpcServ = grpc.NewServer()
reflection.Register(srv.Grpc)
```
- **影响**:独立部署时 `grpc.NewServer()` 未装配任何 unary/stream 拦截器(无鉴权、无 recover、无日志、无限流`MicroService.Enable: false` 又意味着 `Anonymous` 声明在该入口下完全失效——即该接口对任何能连到 12216/12102 的调用方都是无条件开放,且只校验 `in.Key == ""``by_pos.go:15-17`key 空间可被无限枚举,配合问题 2 的全表扫描形成刷接口即拖库压测的路径。同时 `reflection.Register` 使服务与消息结构可被匿名枚举,属信息泄露。
- **建议**:独立入口也接入与聚合一致的鉴权拦截器(`grpc.UnaryInterceptor`)与 `grpc_recovery`;引入按 IP/调用方的限流(如令牌桶)与并发上限;仅在 dev 打开 reflection用配置开关控制默认关闭`key` 加长度与字符集校验(如 `^[a-zA-Z0-9_\-]{1,64}$`)。
### P2
#### 4. 同一接口在两种部署入口的鉴权语义相反,且模块内部没有任何鉴权代码
- **位置**`module/base/ads/etc/ads_prod.yaml:15-16``pkgs/all/etc/default_dev.yaml:24-43``pkgs/all/internal/server/authorization.go:43-51`
- **证据**
```yaml
# module/base/ads/etc/ads_prod.yaml:14-16 —— 声明匿名
Anonymous:
- ads.Fetch.ByPos
# pkgs/all/etc/default_dev.yaml:24-43 —— 聚合白名单只含 passport/market/mall 登录等,不含 ads
Authorization:
Anonymous:
- /passport.Login/Pwd
```
- **影响**:按模块配置,`ads.Fetch.ByPos` 是匿名接口;按聚合服务配置(它与 `authorization.go``isAnonymous` 逐条精确匹配,`authorization.go:96-102``/ads.Fetch/ByPos``/rpc/ads/Fetch/ByPos` 都需要 JWT。同一份代码在“独立进程”下完全公开、在“聚合进程”下要求登录公开广告位如首页 banner走聚合入口会 401而走独立入口则无需登录行为随部署方式漂移运维/前端无法从文档确定预期。`wiki/api/01-ads.md:12` 只说“需要登录的接口通过 Authorization 传递 JWT”未标明本接口属于哪类。
- **建议**:把 ByPos 是否匿名的结论固化为唯一来源(建议在聚合白名单中显式加入 `/ads.Fetch/ByPos`,并同步删除模块 `Anonymous` 中的误导性声明或标注其仅对 MicroService 注册生效),然后在 `wiki/api/01-ads.md` 的接口表增加“鉴权”列;用测试固定 `/ads.Fetch/ByPos` 的 200/401 期望值。
#### 5. 配置与密钥管理三份环境配置完全相同prod = dev占位密钥可通过校验且模块直接使用 SDK 硬编码兜底密钥
- **位置**`module/base/ads/etc/ads_prod.yaml:7,10,24``internal/config/config.go:37,40``bsm-sdk/core/env/env.go:19`
- **证据**
```yaml
# etc/ads_prod.yaml:7,10,24与 ads_dev.yaml / ads_test.yaml 字节级相同SHA256 均为 DB300DC1…
- host=127.0.0.1 user=postgres password=CHANGE_ME dbname=rst_dev port=5432 sslmode=disable TimeZone=Asia/Shanghai
Cache: redis://null:CHANGE_ME@127.0.0.1:6379/
SecretKey: CHANGE_ME
```
```go
// internal/config/config.go:37,40 —— 只校验非空;"CHANGE_ME" 视为合法;加密密钥取自环境(含硬编码兜底)
conf.NotNil(Spec.Service, Spec.Cache)
encipher.New(env.Runtime.JwtSecretKey)
// bsm-sdk/core/env/env.go:19
JwtSecretKey: GetEnvDefault("BSM_JwtSecretKey", "Cblocksmesh2022C"),
```
- **影响**:生产配置与开发/测试配置一致(本地回环地址、`rst_dev` 库、`sslmode=disable` 明文数据库链路、占位口令),部署产物 `.builds/etc/ads_prod.yaml:7,10,24` 亦相同;`conf.NotNil` 只判空,`CHANGE_ME` 能通过启动校验(配置错误在运行时才以连接失败暴露)。若未设置 `BSM_JwtSecretKey`,模块的加密初始化会使用公开可读的硬编码密钥 `Cblocksmesh2022C`(本模块未调用 `GenerateTokenAes/ParseTokenAes`,故对 ads 的直接可利用性为**推测**:一旦后续复用该密钥签发/校验凭证即可离线伪造)。此外 `SecretKey: CHANGE_ME` 被解析进 `Spec.SecretKey` 但全模块无任何读取处(仅 `config.go` 结构体定义),属失效配置。
- **建议**prod 配置必须与 dev 分离(独立 DSN/库名/账号,`sslmode=require` 或 verify-full启动时对配置做白名单校验拒绝 `CHANGE_ME`、拒绝空 `BindIP` 下的公网绑定、要求 `BSM_JwtSecretKey` 长度 ≥32 且非默认值,缺失即 fail-fast`SecretKey` 真正用起来或删除,避免“看着像已配置”;密钥统一走环境变量/密钥管理,不在仓库中保留任何形式的字符串。
#### 6. 状态默认值与查询条件冲突:新建广告默认不可见
- **位置**`module/base/ads/internal/models/ads_item.go:34``bsm-sdk/core/types/db.go:75``internal/logic/fetch/by_pos.go:21``README.md:269`
- **证据**
```go
// internal/models/ads_item.go:34 —— 嵌入 Std_Status
types.Std_Status
// bsm-sdk/core/types/db.go:75 —— 该字段默认值 0
Status int64 `gorm:"column:status;default:0;index;" json:"status"` // 状态默认为0-1禁止1为正常
// internal/logic/fetch/by_pos.go:21 —— 只取 status = 1
err = impl.DBService.Where("pos_key = ? AND status = ?", in.Key, 1).Find(&adsItems).Error
```
- **影响**:模型/迁移给出的 `status` 默认值是 0非法/禁用),而读取侧只认 1README:269 又写 `status INTEGER DEFAULT 1`。任何按模型默认值插入、或漏填 status 的广告记录都不会被任何入口返回,表现为“后台已创建但前端看不到”的静默数据不一致;同时 `1``0``-1` 缺乏命名常量,语义靠注释维护。
- **建议**:把默认值统一为 1`gorm:"...;default:1"`)或改为枚举 + `CHECK(status IN (-1,0,1))`;在 `models` 中定义 `StatusNormal/StatusDisabled` 常量并替换 `by_pos.go:21` 的魔法数字 `1`README 的表结构与模型标签需自动校验一致。
#### 7. 模型含软删除列但文档建表 DDL 没有该列,且模块内没有任何可执行的迁移入口
- **位置**`module/base/ads/internal/models/ads_item.go:28,37-39``README.md:261-271``bsm-sdk/core/database/sql/postgresql.go:17``internal/impl/impl.go:26``README.md:218`
- **证据**
```go
// internal/models/ads_item.go:28 —— 嵌入 gorm.Model含 DeletedAt
type AdsItem struct {
gorm.Model
// README.md:261-271 —— 唯一的建表 DDLads_item 无 deleted_at 列
CREATE TABLE IF NOT EXISTS ads_item (
id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, pos_key VARCHAR(255) NOT NULL,
...
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
// bsm-sdk/core/database/sql/postgresql.go:17 —— 默认不自动迁移
IsAutoMigrate: false,
// internal/impl/impl.go:26 —— 模块传 nil沿用默认不迁移
DBService = with.Databases(config.Spec.Databases, nil)
```
- **影响**GORM 对含 `DeletedAt` 的模型会自动在查询上追加 `deleted_at IS NULL`。若按 README 的 DDL 建表(或线上表缺该列),`ByPos` 的查询会直接报 `column ads_item.deleted_at does not exist` 并被吞成 `ErrDB`(问题 9接口 100% 失败。而模块自身没有任何可用迁移路径:`init()` 里的 `database.AppendMigrate``ads_item.go:37-39`)因 `IsAutoMigrate=false` 不生效README:218 记载的 `go run cmd/main/main.go --init-db``cmd/main/main.go` 中不存在(无 `flag` 解析)。
- **建议**:仓库内提供版本化迁移(独立 `migrations/` + 显式命令或 `IsAutoMigrate` 配置开关),修正 README 的 DDL 使其与模型一致(补 `deleted_at TIMESTAMP``pos_key/status` 索引),并删除文档中不存在的 `--init-db` 用法或真正实现它。
#### 8. context 未向下传递,读路径无超时/无重试/无熔断
- **位置**`module/base/ads/internal/logic/fetch/by_pos.go:13,21`
- **证据**
```go
func ByPos(ctx context.Context, in *pb.ByPosRequest) (reply *pb.ByPosReply, err error) {
...
err = impl.DBService.Where("pos_key = ? AND status = ?", in.Key, 1).Find(&adsItems).Error
```
- **影响**`ctx` 形参在函数体内从未使用(也未见 `WithContext`),客户端断连/网关超时都无法取消这条 SQL配合问题 2 的全表扫描,慢查询会持续占用连接,`MaxOpenConns=64``bsm-sdk/core/vars/sql.go:8`)被占满后整个服务(含聚合进程内的其它模块)一起排队。模块内无任何超时、重试、熔断配置。
- **建议**:改为 `impl.DBService.WithContext(ctx).Where(...)`;为 handler 增加 `ctx, cancel := context.WithTimeout(ctx, 3*time.Second)`;在 gRPC server 侧加超时拦截器,并按需对可重试错误配置有限重试。
#### 9. 原始错误被吞掉,且业务路径没有任何日志/指标/健康检查APM 配置解析后未使用)
- **位置**`module/base/ads/internal/logic/fetch/by_pos.go:22-24``internal/config/config.go:22``README.md:16,331-339`
- **证据**
```go
// internal/logic/fetch/by_pos.go:22-24 —— 底层错误与上下文全部丢弃
if err != nil {
return nil, errcode.ErrDB
}
```
```go
// internal/config/config.go:22 —— APM 配置被解析但全模块无引用grep "Apm" 仅此一处)
Apm *conf.ApmConf `yaml:"APM"` // APM监控配置
```
- **影响**DB 报错如表不存在、连接池耗尽、SQL 语法)在日志中没有任何痕迹,`errcode.ErrDB` 也无法区分“无数据”和“查询失败”,线上只能靠猜;`internal/` 下无 `printer`/`log` 调用(全模块 grep 仅 `cmd/cli/main.go:7` 与生成代码),无结构化日志、无 trace id、无 metricsREADME:335/338 记载的 `/health``/metrics` 端点不存在(`new.go:32-37` 只注册 Fetch`grpc_health_v1`)。
- **建议**:错误返回前 `printer.Error`/`log` 记录 `err``pos_key``request_id`(从 metadata 取),并把底层错误包装进可观测字段;注册 gRPC 健康检查服务并暴露 `/health`;把 `Apm` 真正接入或删除该配置项;为慢查询(>100ms打点。
#### 10. 启动强依赖未被使用的 Redis且 GORM Debug 默认开启打印全部 SQL 与参数
- **位置**`module/base/ads/internal/impl/impl.go:24,26``bsm-sdk/core/with/redis.go:10-14``bsm-sdk/core/cache/redis/redis.go:27-33``bsm-sdk/core/database/sql/postgresql.go:17-19,46-48`
- **证据**
```go
// internal/impl/impl.go:24,26
RedisService = with.RedisCache(config.Spec.Cache)
DBService = with.Databases(config.Spec.Databases, nil) // opts=nil → 使用 SDK 默认Debug: true
// bsm-sdk/core/cache/redis/redis.go:27-33 —— Ping 失败即 panic
func New(dsn string, hashRadix string) *RedisClient {
client, err := NewWithContext(context.Background(), dsn, hashRadix)
if err != nil { panic(err) }
// bsm-sdk/core/database/sql/postgresql.go:17-19,46-48
IsAutoMigrate: false, LogStdout: false, Debug: true,
if options.Debug { gormDb = gormDb.Debug() }
```
- **影响**Redis 客户端在启动时创建并 `Ping`,失败即 panic 导致服务起不来——但本模块没有任何一处读/写 Redis`RedisService` 除赋值外无引用),即“无用依赖决定可用性”;同理 `MemorySerice` 也未被使用。GORM 以 `Debug()` 模式运行会打印每条 SQL 及其参数(含 `pos_key` 等业务数据)到标准输出/日志,既影响吞吐又造成数据面信息落日志。
- **建议**:删除启动时的 Redis/内存缓存初始化,或仅在真正使用后按需懒加载并对连接失败降级告警而非 panic`with.Databases` 传入显式的 `SqlOptions{Debug: <由配置控制>, IsAutoMigrate: ...}`,生产环境默认关闭 SQL 明细日志。
#### 11. 优雅退出不可达,无信号处理,外部资源不关闭
- **位置**`module/base/ads/cmd/main/main.go:42-45``bsm-sdk/core/service/service.go:113-115,142-144`
- **证据**
```go
// cmd/main/main.go:42-45
defer srv.Stop()
srv.Start()
// bsm-sdk/core/service/service.go:113-115
// 阻塞主线程
select {}
```
- **影响**`Start()` 内部以 `select {}` 永久阻塞且永不返回,`defer srv.Stop()` 是死代码;进程未监听 SIGTERM/SIGINT全模块无 `signal.Notify`),容器/`supervisor` 停止时只能被强杀 → 在途 gRPC 请求被截断,且数据库/Redis/Etcd 连接不做关闭。`Service.Stop()` 本身也只做 `GrpcSrv.GracefulStop()``service.go:142-144`),未关闭 DB/Redis/Etcd。
- **建议**:在 `cmd/main` 中实现信号处理(`signal.NotifyContext`),收到信号后按序 `srv.Stop()``sqlDB.Close()``RedisService.Close()``EtcdService.Close()`,并为关停设置超时;向 SDK 反馈 `Start()` 阻塞不可退出的问题或改用可返回的启动接口。
#### 12. `service.Expose` 无入参校验且丢弃 `server.New` 返回值Gateway 为 nil 时 panicGRPC 为 nil 时静默失效
- **位置**`module/base/ads/service/expose.go:19-25``module/base/ads/service/dependencies.go:26-28`
- **证据**
```go
func Expose(options ExposeOptions) error {
applyDependencies(options.Dependencies)
server.New(options.GRPC) // 返回值被丢弃
ctx := context.Background()
if err := pb.RegisterFetchHandlerServer(ctx, options.Gateway, server.NewFetchServer()); err != nil {
```
- **影响**(a) `options.Gateway == nil``RegisterFetchHandlerServer` 会在 `*runtime.ServeMux` 的 nil 接收者上 panic`Handle` 首行即访问 `s.middlewares`grpc-gateway v2.30.0 `runtime/mux.go`),而调用方 `pkgs/all/internal/service/ads.go:10-19``pkgs/ecmall/.../ads.go` 未做 nil 检查,聚合进程会直接崩溃;(b) `options.GRPC == nil``server.New` 会自建一个 gRPC server 并注册 Fetch但该 server 没有监听者gRPC 调用方得到 "unknown service"故障被静默吞掉HTTP 路径因使用本地 handler 仍可用,问题更隐蔽);(c) `dependencies.go:26-28` 只在非 nil 时覆盖全局 `impl.DBService`,若 DB 缺失,`by_pos.go:21` 会在请求时对 nil `*gorm.DB` 解引用 panic 而不是启动时 fail-fast。
- **建议**`Expose` 入口校验 `options.GRPC``options.Gateway``options.Dependencies.DB` 非 nil 并返回明确错误;使用 `server.New` 的返回值(保留 `*Server` 供后续使用或断言一致性);`Expose` 返回的错误在聚合启动处必须 panic/退出而不是忽略。
#### 13. 返回内容原样透出,无白名单/转义,`content` 与 `to_url` 可承载任意字符串
- **位置**`module/base/ads/internal/logic/fetch/by_pos.go:29-36``internal/models/ads_item.go:31,33``proto/ads.proto:22-23`
- **证据**
```go
result = append(result, &pb.AdsItem{
Id: int64(item.ID),
Title: item.Title,
Content: item.Content,
Type: int32(item.Type),
ToUrl: item.ToUrl,
```
- **影响**`content`varchar(255) 文本/图片/视频 URL`to_url` 无任何格式校验,直接以 JSON 返回给前端。广告位内容通常由后台运营录入,若消费端按 HTML 渲染 `content` 或在 `<a href>`/跳转中使用 `to_url`,即构成存储型 XSS 或开放重定向/钓鱼跳转。是否存在可利用的消费端渲染属**推测**(本仓库未找到 ads 的前端消费代码);另 `content varchar(255)` 对视频/附件类 URL 易截断(`ads_item.go:31`),而 `proto/ads.proto:22` 注释只声明了 1-3 三种类型,与 `models/ads_item.go:19-24` 的 6 类枚举不一致,消费端对 4/5/6 的分支处理无据可依。
- **建议**:在写侧(后续的管理接口)做 `type` 与内容形态校验:`to_url` 强制 `http/https` 且域名白名单(拒绝 `javascript:`/`data:`),文本内容做 HTML 转义或明确以纯文本下发;把 `content` 改为 `text`/`varchar(1024)` 并按类型区分字段;同步修正 `proto/ads.proto` 的类型注释与枚举。
#### 14. 零测试:`test/` 目录没有任何可运行测试
- **位置**`module/base/ads/test/rpc/rpc.go:1-23`(全文件为注释)、模块内 `*_test.go` 数量 = 0
- **证据**
```go
func main() {
/*
md := metadata.New(map[string]string{"request_id": utils.UUID(), "workspace": "scf"})
ctx := metadata.NewOutgoingContext(context.Background(), md)
```
- **影响**:唯一的 `test/` 文件是整段注释(连 `pb.NewFetchClient` 都不存在,`pb` 中只有 `NewFetchClient` 对应的 Fetch 服务,文件里写的是 `NewMethodClient`),无编译价值;`go test ./...` 无任何用例,问题 1/6/7/12 这类“会在运行时才炸”的缺陷没有回归网。缺失的关键路径测试清单:①`ByPos` 空 key 返回 `ErrInvalidArgument`;②`pos_key` 命中/未命中/仅含禁用状态的行为③DB 报错时返回 `ErrDB` 且不 panic`status` 默认值写入后是否可被读取(对应问题 6⑤表缺 `deleted_at` 时的失败模式(对应问题 7⑥网关路由 `POST /ads.Fetch/ByPos` 的 200 与 JSON 字段映射(对应问题 1、4`Expose``Gateway/GRPC/DB` 为 nil 时的行为(对应问题 12`ctx` 取消能否中断查询(对应问题 8
- **建议**:把 `test/rpc/rpc.go` 改写为可编译的集成冒烟测试(或删除),补 `by_pos_test.go`(用 `sqlmock`/测试库覆盖上述 ①-③)、`expose_test.go`(④⑤⑦)与 gateway 层 `httptest`(⑥);在 CI 中对 `module/base/ads` 执行 `go test ./...` 并设覆盖率门限。
### P3
#### 15. README 大面积与代码/仓库实际不符
- **位置**`README.md:12,14,35,91-95,110,218,319-326,331-339,394`
- **证据**
```markdown
- **Go**: 1.25.1+ # README.md:35而 go.mod:3 为 go 1.26.5
│ ├── 📁 swagger/ # README.md:91仓库中不存在 swagger/ 与 scripts/、Dockerfile、Makefile
- **📍 广告位管理**: 灵活的广告位配置和分类管理 # README.md:12模块内无任何广告位AdsPos读写实现
go run cmd/main/main.go --init-db # README.md:218main.go 无 flag 解析
curl http://localhost:12102/health # README.md:335无该路由
```
- **影响**:文档声称的 Dockerfile/Makefile/swagger/scripts 目录、`--init-db`/`--check-config` 参数、`/health``/metrics`、APM 集成、Redis 10 分钟缓存与 `pos_key/status` 复合索引、广告位管理功能,在本仓库中均不存在(`module/base/ads` 下仅 `cmd/ etc/ internal/ pb/ proto/ service/ test/`)。新人按 README 操作会直接失败,审计/排障也会被误导。
- **建议**:以“代码为准”重写 README 的部署与性能章节(删除不存在的能力,或补齐实现),把 Go 版本、目录树、示例命令与 `go.mod``cmd/main` 实际支持的参数对齐;把缓存/索引等“应然”描述移到“优化建议”小节并显式标注“未实现”。
#### 16. 死代码与未使用资源集中(含已声明但无实现的“广告位管理”)
- **位置**`cmd/cli/main.go:6-8``test/rpc/rpc.go:3-23``internal/models/ads_pos.go:15-28``internal/server/new.go:17,29``internal/config/config.go:22``internal/impl/impl.go:13,16,24`
- **证据**
```go
// cmd/cli/main.go:6-8 —— 空壳 CLI
func main() { log.Println("广告服务命令行工具") }
// internal/server/new.go:17,29 —— 声明并初始化,但全模块再无引用(无锁,若启用即数据竞争)
grpcConns map[string]*grpc.ClientConn // 连接池
grpcConns: make(map[string]*grpc.ClientConn),
// internal/models/ads_pos.go:15-28 —— ads_pos 表被迁移注册,但无任何查询/写入代码
```
- **影响**`AdsPos` 模型与 `ads_pos` 表只被 `database.AppendMigrate` 注册(`ads_pos.go:21-23`),业务侧零引用,与 README:12 的“广告位管理”承诺矛盾;`cmd/cli` 无功能;`grpcConns` 无使用者(若未来启用,无 mutex 的 map 并发写会 panic`Apm``RedisService``MemorySerice` 均为“配置/依赖存在但无人使用”的噪声,让人误判能力边界。
- **建议**:删除确无用途的 `cmd/cli``test/rpc``grpcConns``Apm` 配置项与缓存初始化;若 `ads_pos` 是规划中的功能,则在 README 标注“规划中”并保留 TODO issue否则一并删除模型与迁移注册。
#### 17. `proto/const.proto` 把大量他域消息塞进 ads 的 go_package生成代码严重膨胀
- **位置**`proto/const.proto:1-326``pb/const.pb.go`(约 3000+ 行)
- **证据**
```protobuf
// proto/const.proto:132-326 —— cms/market/order/social/passport 等与本模块无关的消息
// CMS messages.
message CmsCategoryItem { ... }
message OrderSummaryItem { ... repeated OrderDetails details = 33; }
```
- **影响**ads 模块的 pb 包里包含了订单、CMS、社交、钱包等域的消息`package base_ads_blocks``option go_package = "bsm/full/module/base/ads/pb;ads"` 共用),导致 ads 的 gRPC 反射与文档(`scripts/api-docgen/main.go:12` 匿名导入 `base/ads/pb`暴露的“schema”远超本模块职责也使得任何共享消息变更都要重新生成 ads 的 3000+ 行代码;`AdsItem` 等 6 个消息真正被使用,其余为复制粘贴产物。
- **建议**:把共享消息抽到独立的 `blocks` 模块/单独的 `.proto`(独立 go_packageads 只保留 `ads.proto` 的 6 个消息;确认 `const.proto` 在本模块是否真的被 codegen 流程依赖,若否直接删除。
#### 18. 魔法数字、枚举与文档不一致、排序不确定、时间格式硬编码
- **位置**`internal/logic/fetch/by_pos.go:21,35``internal/models/ads_item.go:19-32``proto/ads.proto:22`
- **证据**
```go
err = impl.DBService.Where("pos_key = ? AND status = ?", in.Key, 1).Find(&adsItems).Error // 魔法数字 1
Created: item.CreatedAt.Format("2006-01-02 15:04:05"), // 硬编码布局、无时区信息
// internal/models/ads_item.go:19-251..6 六类proto/ads.proto:22 注释只写 "1.文本 2.图片 3.视频"
```
- **影响**`status=1` 无语义常量(问题 6`Type` 默认 0 落在枚举 1..6 之外,返回给消费端是未定义值;查询无 `ORDER BY`,同广告位多条的返回顺序依赖数据库执行计划(可能随索引/统计信息变化),文案轮播顺序不可复现;`created` 为无时区标记的字符串,跨端解析依赖客户端约定。
- **建议**:定义 `StatusEnabled=1` 等常量;`Type` 增加 `CONTENT_TYPE_UNKNOWN=0` 并在查询侧过滤非法类型;加显式 `Order("id ASC")`(或加权重列排序);`created` 改为 `time.RFC3339` 或 proto `google.protobuf.Timestamp`,避免自定义布局。
#### 19. `go.mod` 使用指向仓库外的相对 replace工具依赖被标为 indirect
- **位置**`go.mod:3,26-67,69``README.md:35`
- **证据**
```go
go 1.26.5
git.apinb.com/bsm-sdk/core v0.2.1
replace git.apinb.com/bsm-sdk/core => ../../../../../bsm-sdk/core
```
- **影响**`../../../../../bsm-sdk/core` 解析到仓库根(`D:\work\bsm-infra\full`)之外的 `D:\work\bsm-sdk\core`,在 CI/容器中未同步该目录即无法构建;同时 `protoc-gen-*` 工具链被列在 `require` 的 indirect 区块(`go.mod:26-67`),与实际 `tool` 区块(`go.mod:5-14`)语义重叠,容易误判依赖来源。
- **建议**CI 中用 `go.work` 或脚本显式同步 SDK 到固定路径,避免相对路径穿透仓库;把工具依赖只保留在 `tool` 块并清理 indirect 段;在 README 中说明构建前置条件。
#### 20. 部署与运行配置粗糙supervisor 以 root 运行、无健康检查/日志轮转/环境变量,连接池不可配置
- **位置**`etc/supervisor.bsm-apps-ads.conf:1-8``bsm-sdk/core/conf/types.go:18-21``bsm-sdk/core/vars/sql.go:7-9`
- **证据**
```ini
; etc/supervisor.bsm-apps-ads.conf:6-8
user=root
redirect_stderr=true
stdout_logfile=/data/app/logs/apps-ads.log
```
```go
// bsm-sdk/core/conf/types.go:18-21 —— DBConf 无任何连接池字段
type DBConf struct {
Driver string `yaml:"Driver"`
Source []string `yaml:"Source"`
}
// bsm-sdk/core/vars/sql.go:7-9 —— 池参数写死Idle=64/Open=64/Lifetime=60s
SqlOptionConnMaxLifetime time.Duration = 60 * time.Second
```
- **影响**:进程以 root 运行且 `redirect_stderr=true` 混流,日志无轮转(无 `stdout_logfile_maxbytes`/`backupCount`),长期运行可能写满磁盘;未设置 `BSM_RuntimeMode`/`BSM_Workspace`/`BSM_JwtSecretKey`/`TZ` 等环境变量,配置与环境强耦合在主进程 shell`conf.New` 依赖 `BSM_RuntimeMode` 选择 `ads_<mode>.yaml`,见 `bsm-sdk/core/conf/new.go:35`supervisor 重启后可能读到非预期配置DB 连接池与超时无法通过 yaml 调整,`ConnMaxLifetime=60s` 会造成周期性重连。
- **建议**`user` 改为专用非特权账号;配置 `stdout_logfile_maxbytes=100MB``stdout_logfile_backups=5``environment=BSM_RuntimeMode="prod",BSM_Workspace="...",BSM_JwtSecretKey="..."`;把连接池/超时参数提升为 `DBConf` 字段(或模块自己构造 `SqlOptions`)并按库容量调优。
#### 21. CORS 未装配(跨域浏览器调用会被拦),且仅存在未使用的 indirect 依赖
- **位置**`pkgs/all/go.mod:75``pkgs/all/internal/server/server.go:34-38`
- **证据**
```go
// pkgs/all/internal/server/server.go:34-38 —— 聚合 HTTP 只用 gin.Logger/RecoveryGateway mux 也无 CORS 中间件
engine := gin.New()
engine.Use(gin.Logger(), gin.Recovery())
...
Gateway: gwRuntime.NewServeMux(),
```
- **影响**`github.com/gin-contrib/cors` 仅作为 indirect 依赖出现在 `pkgs/all/go.mod:75``pkgs/ecmall/go.mod:51` 同),代码中无任何装配点,`gwRuntime.NewServeMux()` 也未使用 `WithMiddlewares` 注入 CORS。所有浏览器直连的 ads 接口(`POST /ads.Fetch/ByPos``/rpc/ads/Fetch/ByPos`)都缺少 `Access-Control-Allow-Origin`,跨域前端会失败;是否有前置网关/Nginx 统一注入 CORS 属**推测**(仓库内未见相关配置)。
- **建议**:明确广告位接口的调用方(浏览器/服务端):若需浏览器直连,在聚合 HTTP 层装配白名单化 CORS限定来源与方法避免 `*` 与凭证同用);若仅服务端调用则从依赖中移除 cors 包并在文档中注明。
## 4. 推荐优化方案
1. **统一入口与注册路径(目标:消除“独立进程 404 / 聚合进程 401”的双轨分歧**:做法:让 `internal/server.New()` 自行创建并填充 `Mux` 与路由,`cmd/main``service.Expose` 都只调用同一函数;`Expose` 增加 nil 校验并返回错误。影响面:`internal/server/new.go``service/expose.go``cmd/main/main.go``pkgs/all|ecmall/internal/service/ads.go`(调用方签名不变)。风险:低;需回归两条部署路径的启动自检。
2. **读路径性能兜底(目标:把每次请求的代价从 O(全表) 降到 O(log n) + 固定上限)**:做法:加 `(pos_key,status)` 复合索引 + `Limit` + `ORDER BY` + `WithContext` 超时,并按 `pos_key` 引入 TTL 缓存Redis 或内存),写路径失效。影响面:`internal/models/ads_item.go``internal/logic/fetch/by_pos.go`、迁移脚本。风险:中;缓存需处理失效与空值穿透(空结果也缓存短 TTL
3. **鉴权与限流对齐(目标:无论哪种部署都有一致的身份策略)**:做法:以聚合 `Authorization.Anonymous` 为唯一白名单来源,明确 ads 是否公开;独立入口装配相同拦截器并加限流与 reflection 开关。影响面:`etc/*.yaml``internal/server/new.go`。风险:中;若把 ByPos 改为需登录,需同步前端与文档。
4. **密钥与配置治理(目标:杜绝占位/默认密钥进入生产)**做法prod 配置独立化DSN/库/sslmode、启动时拒绝 `CHANGE_ME` 与默认 JWT 密钥、要求显式设置 `BSM_JwtSecretKey`(长度与复杂度校验)、删除未使用的 `SecretKey` 或真正使用。影响面:`internal/config/config.go``etc/*``.builds/etc/ads_prod.yaml`。风险:中;需与运维确认密钥注入方式,避免上线即 fail-fast 造成不可用。
5. **数据模型与迁移一致化目标模型、DDL、查询三者不再互相打脸**:做法:提供版本化迁移(含索引与 `deleted_at`),统一 `status` 默认值为 1 并定义常量,重写 README 的建表语句。影响面:`internal/models/*`、新增 `migrations/``README.md`。风险:中;已有线上表需用非破坏性 `ALTER TABLE` 并核对现存数据 status 分布。
6. **可观测性与错误治理(目标:任何失败都能定位)**:做法:错误包装并上报原始 err + `pos_key` + request_id接入 `Apm` 或删除该配置;注册 gRPC 健康检查与 `/health`;生产关闭 GORM `Debug()`。影响面:`internal/logic/fetch/by_pos.go``internal/impl/impl.go``internal/server/new.go`。风险:低。
7. **启停与资源生命周期(目标:可优雅重启、无强依赖)**:做法:信号处理 + 关停时按序关闭 gRPC/DB/Redis/EtcdRedis 改为懒加载且失败降级(因当前未被使用,最简方案是直接移除初始化)。影响面:`cmd/main/main.go``internal/impl/impl.go`。风险:低。
8. **测试与 CI 门禁(目标:让上述修复可回归)**:做法:按问题 14 的清单补单测/网关集成测试CI 跑 `go test ./...``go vet``gofmt -l`(当前两者已通过,可作为基线门禁)。影响面:`test/`、模块 CI 配置。风险:低。
9. **代码与 proto 瘦身(目标:消除误导性声明与生成代码膨胀)**:做法:删除 `cmd/cli``test/rpc``grpcConns``ads_pos` 的“僵尸”定义;把 `const.proto` 的他域消息迁出。影响面:多个文件与 codegen 流程(`scripts/api-docgen``wiki/api/01-ads.md` 生成)。风险:中;迁移 proto 会影响所有引用 `base_ads_blocks` 的模块,需一次性同步生成与文档。
10. **部署配置加固(目标:可运维、不写满磁盘、不越权)**做法supervisor 改非 root、加日志轮转与 `environment=`、显式设置 `BSM_RuntimeMode`;把连接池/超时提升为可配置。影响面:`etc/supervisor.bsm-apps-ads.conf`、SDK `conf.DBConf`。风险:低-中SDK 改动影响全部模块,需单独评审。
## 5. TODO 清单
- [ ] **P1-1**`internal/server.New()` 中初始化 `Mux` 并注册 `RegisterFetchHandlerServer`,让 `cmd/main``service.Expose` 走同一注册路径|验收:独立启动后 `curl -X POST localhost:12102/ads.Fetch/ByPos -d '{"key":"x"}'` 返回 200 而非 404`go vet ./...` 通过|涉及:`module/base/ads/internal/server/new.go:26`, `module/base/ads/cmd/main/main.go:27`, `module/base/ads/service/expose.go:20`
- [ ] **P1-2**`ads_item` 增加 `(pos_key,status)` 复合索引,并给 `Find``Order`+`Limit` 上限|验收:`EXPLAIN` 显示走索引扫描,返回条数不超过配置上限,迁移脚本可重复执行|涉及:`module/base/ads/internal/models/ads_item.go:30`, `module/base/ads/internal/logic/fetch/by_pos.go:21`
- [ ] **P1-3** 为独立入口装配鉴权/限流拦截器并把 gRPC reflection 改为配置开关(生产默认关闭)|验收:未携带凭证调用 `ads.Fetch/ByPos` 得到明确拒绝或按白名单放行,`grpcurl list` 在生产配置下失败|涉及:`module/base/ads/internal/server/new.go:23`, `module/base/ads/internal/server/new.go:36`, `module/base/ads/etc/ads_prod.yaml:15`
- [ ] **P2-4** 统一 `ads.Fetch.ByPos` 的匿名策略(建议在聚合白名单中显式登记)并在 `wiki/api/01-ads.md` 增加鉴权列|验收:独立入口与聚合入口对同一请求返回一致的 200/401文档与实现一致涉及`module/base/ads/etc/ads_prod.yaml:15`, `pkgs/all/etc/default_dev.yaml:24`, `wiki/api/01-ads.md:12`
- [ ] **P2-5** prod 配置与 dev 分离,启动时拒绝占位口令与默认 JWT 密钥|验收:`password=CHANGE_ME` 或未设置 `BSM_JwtSecretKey` 时进程启动失败并打印明确错误prod DSN 使用 `sslmode=require` 与独立库名|涉及:`module/base/ads/etc/ads_prod.yaml:7`, `module/base/ads/internal/config/config.go:37`, `module/base/ads/internal/config/config.go:40`
- [ ] **P2-6** 统一 `status` 默认值为 1 并定义状态常量替换魔法数字|验收:不带 status 插入的记录可被 `ByPos` 查到;`grep -n "status = ?\", in.Key, 1"` 处使用常量|涉及:`module/base/ads/internal/models/ads_item.go:34`, `module/base/ads/internal/logic/fetch/by_pos.go:21`, `module/base/ads/README.md:269`
- [ ] **P2-7** 提供可执行的迁移(含 `deleted_at`、索引)并修正 README 建表 DDL删除不存在的 `--init-db`|验收:按 README 从空库执行后 `ByPos` 不报列缺失;迁移命令存在且有版本记录|涉及:`module/base/ads/internal/models/ads_item.go:37`, `module/base/ads/internal/impl/impl.go:26`, `module/base/ads/README.md:261`, `module/base/ads/README.md:218`
- [ ] **P2-8** 查询改为 `WithContext(ctx)` 并设置查询超时|验收:客户端取消后 DB 查询在超时内被中断(测试用 20s 慢查询验证)|涉及:`module/base/ads/internal/logic/fetch/by_pos.go:13`, `module/base/ads/internal/logic/fetch/by_pos.go:21`
- [ ] **P2-9** 错误返回前记录原始 err 与 `pos_key`/request_id接入或删除 APM 配置,注册健康检查|验收:注入 DB 故障后日志中有可定位记录;`/health` 或 gRPC health 返回 SERVING涉及`module/base/ads/internal/logic/fetch/by_pos.go:22`, `module/base/ads/internal/config/config.go:22`, `module/base/ads/internal/server/new.go:32`
- [ ] **P2-10** 移除未使用的 Redis/内存缓存初始化(或改为懒加载降级),并向 `with.Databases` 传入生产可控的 `SqlOptions{Debug:false}`验收Redis 不可达时服务仍能启动并提供 ByPos生产日志中无逐条 SQL 及其参数|涉及:`module/base/ads/internal/impl/impl.go:24`, `module/base/ads/internal/impl/impl.go:26`
- [ ] **P2-11** 实现信号处理与真正的优雅退出,按序关闭 gRPC/DB/Redis/Etcd验收`SIGTERM` 后在途请求完成、进程 5s 内退出,无强杀|涉及:`module/base/ads/cmd/main/main.go:42`, `module/base/ads/cmd/main/main.go:45`
- [ ] **P2-12** `Expose` 校验 `GRPC/Gateway/DB` 非 nil 并返回错误,不再丢弃 `server.New` 返回值|验收:任一依赖为 nil 时 `Expose` 返回明确错误而非 panic聚合启动即失败并打印原因涉及`module/base/ads/service/expose.go:20`, `module/base/ads/service/expose.go:23`, `module/base/ads/service/dependencies.go:26`
- [ ] **P2-13**`to_url` 做协议/域名白名单校验,对 `content` 明确纯文本或按类型校验,并扩展字段长度|验收:写入 `javascript:` 链接被拒绝;长 URL>255可正常存储涉及`module/base/ads/internal/models/ads_item.go:31`, `module/base/ads/internal/models/ads_item.go:33`, `module/base/ads/proto/ads.proto:23`
- [ ] **P2-14** 补齐测试:`ByPos` 空 key/命中/禁用/DB 错误、status 默认值、网关路由与字段映射、`Expose` nil 依赖、ctx 取消,并接入 CI验收`go test ./...` 覆盖上述用例且全部通过;覆盖率纳入门禁|涉及:`module/base/ads/test/rpc/rpc.go:3`, `module/base/ads/internal/logic/fetch/by_pos.go:13`
- [ ] **P3-15** 按代码实际重写 READMEGo 版本、目录树、参数、健康检查、缓存/索引能力标注为未实现验收README 中出现的每条命令/路径/目录都能在仓库中找到对应实现|涉及:`module/base/ads/README.md:35`, `module/base/ads/README.md:91`, `module/base/ads/README.md:218`, `module/base/ads/README.md:335`
- [ ] **P3-16** 清理死代码与未使用依赖(`cmd/cli``test/rpc``grpcConns``Apm``RedisService``MemorySerice``ads_pos` 若无实现则删除或标注规划中|验收:`grep -rn "grpcConns\|MemorySerice\|Apm" module/base/ads` 无残留业务引用|涉及:`module/base/ads/cmd/cli/main.go:6`, `module/base/ads/internal/server/new.go:17`, `module/base/ads/internal/models/ads_pos.go:15`
- [ ] **P3-17**`proto/const.proto` 中他域消息迁出为独立 proto/go_packageads 仅保留自身消息验收ads 的 pb 包不再包含 Cms/Order/Social/Market 消息,`scripts/api-docgen``wiki/api/01-ads.md` 重新生成后内容不变|涉及:`module/base/ads/proto/const.proto:132`, `module/base/ads/pb/const.pb.go:5`
- [ ] **P3-18** 消除魔法数字与排序/时间格式隐患:状态常量、`Order("id ASC")``created` 用 RFC3339、`Type` 增加 UNKNOWN=0并同步 proto 注释|验收:`by_pos.go` 无字面量状态值;同广告位多条记录顺序稳定;时间字符串带时区|涉及:`module/base/ads/internal/logic/fetch/by_pos.go:21`, `module/base/ads/internal/logic/fetch/by_pos.go:35`, `module/base/ads/proto/ads.proto:22`
- [ ] **P3-19** 收敛 `go.mod`:去掉穿透仓库的本地 `replace` 依赖方式或改为 `go.work`,清理工具依赖的 indirect 标注|验收:在干净环境中仅凭仓库内步骤可完成构建,`go.mod` 无仓库外相对路径|涉及:`module/base/ads/go.mod:69`, `module/base/ads/go.mod:26`
- [ ] **P3-20** 加固 supervisor 配置(非 root、日志轮转、显式环境变量并把连接池/超时提升为可配置|验收:`supervisorctl restart` 后进程以非 root 运行、日志按大小轮转、`BSM_RuntimeMode=prod` 生效|涉及:`module/base/ads/etc/supervisor.bsm-apps-ads.conf:6`, `module/base/ads/etc/supervisor.bsm-apps-ads.conf:8`
- [ ] **P3-21** 明确并按需装配 CORS浏览器直连则白名单化否则移除 indirect 依赖并在文档注明)|验收:跨域前端请求带正确 `Access-Control-Allow-Origin`,或文档明确该接口不支持跨域直连|涉及:`pkgs/all/go.mod:75`, `pkgs/all/internal/server/server.go:34`
## 6. 审计摘要(供汇总使用)
- 问题数P0=0 P1=3 P2=11 P3=7
- 最高风险(一句话):独立部署入口(`cmd/main`)的 HTTP 网关因 `Mux` 从未初始化/路由从未注册而整体 404`internal/server/new.go:26-30` + `cmd/main/main.go:37`),而唯一业务接口 `ads.Fetch/ByPos` 在该入口既无鉴权无限流、其查询又因 `pos_key` 无索引而无分页全表扫描,形成“文档宣称可用但实际不通、若修通则裸奔”的双重缺口。
- 最优先 3 个动作1) 统一入口注册路径并让网关真正生效P1-12) 为 `(pos_key,status)` 建索引并加 `Limit`/`Order`/`WithContext` 超时P1-23) 为独立入口装配鉴权限流并关闭生产 reflectionP1-3
- 未能覆盖/无法验证的部分:未运行服务、未连接真实 PostgreSQL/Redis/Etcd故 404、panic、`deleted_at` 列缺失导致 `ErrDB` 等为静态推断(已在对应条目给出代码依据);未读取 3000+ 行 `pb/const.pb.go` 逐行内容(仅做接口与消息一致性比对);未审计 CI/发布脚本、未确认线上真实库表结构与生产配置是否被环境变量/etcd 覆盖SDK`bsm-sdk/core`)与聚合服务(`pkgs/all``pkgs/ecmall`)仅按需读取相关片段,其中记录的 SDK 侧问题(硬编码默认 JWT 密钥 `Cblocksmesh2022C``GrpcSrv.Serve/Listen` 失败即 panic、`Start()``select {}` 阻塞)归属 SDK 仓库,本报告仅在与 ads 调用点相关处引用。