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.
This commit is contained in:
403
docs/audit/module-base-ads.md
Normal file
403
docs/audit/module-base-ads.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# 审计报告: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`)。
|
||||
依赖外部组件:PostgreSQL(gorm)、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(非法/禁用),而读取侧只认 1;README: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 —— 唯一的建表 DDL:ads_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、无 metrics;README: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 时 panic,GRPC 为 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:218,main.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_package),ads 只保留 `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-25:1..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/Recovery,Gateway 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/Etcd;Redis 改为懒加载且失败降级(因当前未被使用,最简方案是直接移除初始化)。影响面:`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** 按代码实际重写 README(Go 版本、目录树、参数、健康检查、缓存/索引能力标注为未实现)|验收: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_package,ads 仅保留自身消息|验收: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-1);2) 为 `(pos_key,status)` 建索引并加 `Limit`/`Order`/`WithContext` 超时(P1-2);3) 为独立入口装配鉴权限流并关闭生产 reflection(P1-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 调用点相关处引用。
|
||||
Reference in New Issue
Block a user