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

47 KiB
Raw Permalink Blame History

审计报告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.goRun()config.Newimpl.NewImplserver.New(nil) → SDK service.New/StartgRPC 端口 12216、HTTP 网关端口 12102etc/ads_dev.yaml:2,19-21)。 对外接口形式gRPC + grpc-gatewaypb/ads.pb.gw.go:152POST /ads.Fetch/ByPos);另有聚合入口的动态 HTTP /rpc/ads/Fetch/ByPoswiki/api/01-ads.md:25)。 依赖外部组件PostgreSQLgorm、Redisimpl.go:24、Etcdimpl.go:28,配置未提供故为 nil、SDK bsm-sdk/core本地 replace。 聚合接入:同时被 pkgs/all/internal/service/ads.go:9-20pkgs/ecmall/internal/service/ads.go:9-20 通过 service.Expose 注册进聚合进程(配置中 Services: - ads);独立进程入口与聚合入口的注册路径不同,是本模块多个缺陷的根源。

2. 审计范围与方法

已读文件(全部非生成代码 + 关键配置):README.mdcmd/main/main.gocmd/cli/main.gointernal/config/config.gointernal/impl/impl.gointernal/logic/fetch/by_pos.gointernal/models/ads_item.gointernal/models/ads_pos.gointernal/server/{fetch_server.go,new.go}service/{dependencies.go,expose.go}test/rpc/rpc.goproto/{ads.proto,const.proto}etc/*.yamletc/supervisor.bsm-apps-ads.confgo.mod.builds/etc/ads_prod.yaml。 生成代码(pb/*.pb.gopb/*.pb.gw.go)只做接口一致性检查:pb.FetchServer.ByPos(ctx, *ByPosRequest) (*ByPosReply, error)internal/server/fetch_server.go:19 一致proto 字段id/title/content/type/toUrl/createdmodels.AdsItem 映射一致;pattern_Fetch_ByPos_0 = /ads.Fetch/ByPospb/ads.pb.gw.go:152)与 README/wiki 一致;未发现生成代码与手写代码的签名冲突。 为判定跨模块调用链,另外读取了 SDK 与聚合服务的确证位置:bsm-sdk/core/service/service.goconf/{new.go,types.go}with/{databases.go,redis.go,etcd.go}database/{new.go,sql/postgresql.go}types/db.goenv/env.gocrypto/encipher/encipher.gocache/redis/redis.gopkgs/all/internal/server/{server.go,authorization.go}pkgs/all/etc/default_dev.yaml。 执行命令与结果:go version → go1.26.5 windows/amd64gofmt -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:55SDK/聚合服务代码仅按需读取,未做完整审计。

3. 问题清单

P0

无。本模块无写接口、无金额/事务路径SQL 全部参数化(by_pos.go:21 使用 ? 占位符),未发现可被利用的注入点;聚合入口对 /ads.Fetch/ByPos 强制 JWTpkgs/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-30module/base/ads/cmd/main/main.go:27,37module/base/ads/service/expose.go:20-23
  • 证据
// 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.Exposeexpose.go:23),而 cmd/main 从不调用它,只调用 SDK 的 service.New。反向地,若有人按 deps.GRPC 路径把 server.New() 返回值的 Mux 直接交给 Exposeruntime.ServeMux.Handle 在 nil 接收者上先访问 s.middlewaresgrpc-gateway v2.30.0 runtime/mux.goHandle)会 panic。即“网关要么 404要么崩”。
  • 建议:在 internal/server/new.goNew() 中初始化 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:30module/base/ads/internal/logic/fetch/by_pos.go:21module/base/ads/README.md:319-326
  • 证据
// 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 = ? 无法走索引 → 每次请求全表扫描;FindLimit,命中行数无上限,全部加载进内存并序列化返回(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 由请求参数或配置上限控制,默认如 50context 超时;按 pos_key 做短 TTL 缓存并在管理端写路径失效(若后续新增写接口)。

3. 独立入口的 ByPos 完全无鉴权、无限流,并默认开启 gRPC 反射

  • 位置module/base/ads/etc/ads_dev.yaml:15-16internal/server/new.go:23,36internal/logic/fetch/by_pos.go:15-17
  • 证据
# etc/ads_dev.yaml:13-16  —— 模块自己声明该 RPC 免鉴权(三份配置相同)
MicroService:
  Enable: false
  Anonymous:
    - ads.Fetch.ByPos
// 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-17key 空间可被无限枚举,配合问题 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-16pkgs/all/etc/default_dev.yaml:24-43pkgs/all/internal/server/authorization.go:43-51
  • 证据
# 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.goisAnonymous 逐条精确匹配,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,24internal/config/config.go:37,40bsm-sdk/core/env/env.go:19
  • 证据
# 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
// 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-fastSecretKey 真正用起来或删除,避免“看着像已配置”;密钥统一走环境变量/密钥管理,不在仓库中保留任何形式的字符串。

6. 状态默认值与查询条件冲突:新建广告默认不可见

  • 位置module/base/ads/internal/models/ads_item.go:34bsm-sdk/core/types/db.go:75internal/logic/fetch/by_pos.go:21README.md:269
  • 证据
// 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 的广告记录都不会被任何入口返回,表现为“后台已创建但前端看不到”的静默数据不一致;同时 10-1 缺乏命名常量,语义靠注释维护。
  • 建议:把默认值统一为 1gorm:"...;default:1")或改为枚举 + CHECK(status IN (-1,0,1));在 models 中定义 StatusNormal/StatusDisabled 常量并替换 by_pos.go:21 的魔法数字 1README 的表结构与模型标签需自动校验一致。

7. 模型含软删除列但文档建表 DDL 没有该列,且模块内没有任何可执行的迁移入口

  • 位置module/base/ads/internal/models/ads_item.go:28,37-39README.md:261-271bsm-sdk/core/database/sql/postgresql.go:17internal/impl/impl.go:26README.md:218
  • 证据
// 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.AppendMigrateads_item.go:37-39)因 IsAutoMigrate=false 不生效README:218 记载的 go run cmd/main/main.go --init-dbcmd/main/main.go 中不存在(无 flag 解析)。
  • 建议:仓库内提供版本化迁移(独立 migrations/ + 显式命令或 IsAutoMigrate 配置开关),修正 README 的 DDL 使其与模型一致(补 deleted_at TIMESTAMPpos_key/status 索引),并删除文档中不存在的 --init-db 用法或真正实现它。

8. context 未向下传递,读路径无超时/无重试/无熔断

  • 位置module/base/ads/internal/logic/fetch/by_pos.go:13,21
  • 证据
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=64bsm-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-24internal/config/config.go:22README.md:16,331-339
  • 证据
// internal/logic/fetch/by_pos.go:22-24  —— 底层错误与上下文全部丢弃
if err != nil {
    return nil, errcode.ErrDB
}
// 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 只注册 Fetchgrpc_health_v1)。
  • 建议:错误返回前 printer.Error/log 记录 errpos_keyrequest_id(从 metadata 取),并把底层错误包装进可观测字段;注册 gRPC 健康检查服务并暴露 /health;把 Apm 真正接入或删除该配置项;为慢查询(>100ms打点。

10. 启动强依赖未被使用的 Redis且 GORM Debug 默认开启打印全部 SQL 与参数

  • 位置module/base/ads/internal/impl/impl.go:24,26bsm-sdk/core/with/redis.go:10-14bsm-sdk/core/cache/redis/redis.go:27-33bsm-sdk/core/database/sql/postgresql.go:17-19,46-48
  • 证据
// 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 导致服务起不来——但本模块没有任何一处读/写 RedisRedisService 除赋值外无引用),即“无用依赖决定可用性”;同理 MemorySerice 也未被使用。GORM 以 Debug() 模式运行会打印每条 SQL 及其参数(含 pos_key 等业务数据)到标准输出/日志,既影响吞吐又造成数据面信息落日志。
  • 建议:删除启动时的 Redis/内存缓存初始化,或仅在真正使用后按需懒加载并对连接失败降级告警而非 panicwith.Databases 传入显式的 SqlOptions{Debug: <由配置控制>, IsAutoMigrate: ...},生产环境默认关闭 SQL 明细日志。

11. 优雅退出不可达,无信号处理,外部资源不关闭

  • 位置module/base/ads/cmd/main/main.go:42-45bsm-sdk/core/service/service.go:113-115,142-144
  • 证据
// 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-25module/base/ads/service/dependencies.go:26-28
  • 证据
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 == nilRegisterFetchHandlerServer 会在 *runtime.ServeMux 的 nil 接收者上 panicHandle 首行即访问 s.middlewaresgrpc-gateway v2.30.0 runtime/mux.go),而调用方 pkgs/all/internal/service/ads.go:10-19pkgs/ecmall/.../ads.go 未做 nil 检查,聚合进程会直接崩溃;(b) options.GRPC == nilserver.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.GRPCoptions.Gatewayoptions.Dependencies.DB 非 nil 并返回明确错误;使用 server.New 的返回值(保留 *Server 供后续使用或断言一致性);Expose 返回的错误在聚合启动处必须 panic/退出而不是忽略。

13. 返回内容原样透出,无白名单/转义,contentto_url 可承载任意字符串

  • 位置module/base/ads/internal/logic/fetch/by_pos.go:29-36internal/models/ads_item.go:31,33proto/ads.proto:22-23
  • 证据
        result = append(result, &pb.AdsItem{
            Id:      int64(item.ID),
            Title:   item.Title,
            Content: item.Content,
            Type:    int32(item.Type),
            ToUrl:   item.ToUrl,
  • 影响contentvarchar(255) 文本/图片/视频 URLto_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
  • 证据
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 且不 panicstatus 默认值写入后是否可被读取(对应问题 6⑤表缺 deleted_at 时的失败模式(对应问题 7⑥网关路由 POST /ads.Fetch/ByPos 的 200 与 JSON 字段映射(对应问题 1、4ExposeGateway/GRPC/DB 为 nil 时的行为(对应问题 12ctx 取消能否中断查询(对应问题 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
  • 证据
- **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.modcmd/main 实际支持的参数对齐;把缓存/索引等“应然”描述移到“优化建议”小节并显式标注“未实现”。

16. 死代码与未使用资源集中(含已声明但无实现的“广告位管理”)

  • 位置cmd/cli/main.go:6-8test/rpc/rpc.go:3-23internal/models/ads_pos.go:15-28internal/server/new.go:17,29internal/config/config.go:22internal/impl/impl.go:13,16,24
  • 证据
// 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 并发写会 panicApmRedisServiceMemorySerice 均为“配置/依赖存在但无人使用”的噪声,让人误判能力边界。
  • 建议:删除确无用途的 cmd/clitest/rpcgrpcConnsApm 配置项与缓存初始化;若 ads_pos 是规划中的功能,则在 README 标注“规划中”并保留 TODO issue否则一并删除模型与迁移注册。

17. proto/const.proto 把大量他域消息塞进 ads 的 go_package生成代码严重膨胀

  • 位置proto/const.proto:1-326pb/const.pb.go(约 3000+ 行)
  • 证据
// 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_blocksoption 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,35internal/models/ads_item.go:19-32proto/ads.proto:22
  • 证据
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 无语义常量(问题 6Type 默认 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,69README.md:35
  • 证据
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-8bsm-sdk/core/conf/types.go:18-21bsm-sdk/core/vars/sql.go:7-9
  • 证据
; etc/supervisor.bsm-apps-ads.conf:6-8
user=root
redirect_stderr=true
stdout_logfile=/data/app/logs/apps-ads.log
// 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 等环境变量,配置与环境强耦合在主进程 shellconf.New 依赖 BSM_RuntimeMode 选择 ads_<mode>.yaml,见 bsm-sdk/core/conf/new.go:35supervisor 重启后可能读到非预期配置DB 连接池与超时无法通过 yaml 调整,ConnMaxLifetime=60s 会造成周期性重连。
  • 建议user 改为专用非特权账号;配置 stdout_logfile_maxbytes=100MBstdout_logfile_backups=5environment=BSM_RuntimeMode="prod",BSM_Workspace="...",BSM_JwtSecretKey="...";把连接池/超时参数提升为 DBConf 字段(或模块自己构造 SqlOptions)并按库容量调优。

21. CORS 未装配(跨域浏览器调用会被拦),且仅存在未使用的 indirect 依赖

  • 位置pkgs/all/go.mod:75pkgs/all/internal/server/server.go:34-38
  • 证据
// 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:75pkgs/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/mainservice.Expose 都只调用同一函数;Expose 增加 nil 校验并返回错误。影响面:internal/server/new.goservice/expose.gocmd/main/main.gopkgs/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.gointernal/logic/fetch/by_pos.go、迁移脚本。风险:中;缓存需处理失效与空值穿透(空结果也缓存短 TTL
  3. 鉴权与限流对齐(目标:无论哪种部署都有一致的身份策略):做法:以聚合 Authorization.Anonymous 为唯一白名单来源,明确 ads 是否公开;独立入口装配相同拦截器并加限流与 reflection 开关。影响面:etc/*.yamlinternal/server/new.go。风险:中;若把 ByPos 改为需登录,需同步前端与文档。
  4. 密钥与配置治理(目标:杜绝占位/默认密钥进入生产)做法prod 配置独立化DSN/库/sslmode、启动时拒绝 CHANGE_ME 与默认 JWT 密钥、要求显式设置 BSM_JwtSecretKey(长度与复杂度校验)、删除未使用的 SecretKey 或真正使用。影响面:internal/config/config.goetc/*.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.gointernal/impl/impl.gointernal/server/new.go。风险:低。
  7. 启停与资源生命周期(目标:可优雅重启、无强依赖):做法:信号处理 + 关停时按序关闭 gRPC/DB/Redis/EtcdRedis 改为懒加载且失败降级(因当前未被使用,最简方案是直接移除初始化)。影响面:cmd/main/main.gointernal/impl/impl.go。风险:低。
  8. 测试与 CI 门禁(目标:让上述修复可回归):做法:按问题 14 的清单补单测/网关集成测试CI 跑 go test ./...go vetgofmt -l(当前两者已通过,可作为基线门禁)。影响面:test/、模块 CI 配置。风险:低。
  9. 代码与 proto 瘦身(目标:消除误导性声明与生成代码膨胀):做法:删除 cmd/clitest/rpcgrpcConnsads_pos 的“僵尸”定义;把 const.proto 的他域消息迁出。影响面:多个文件与 codegen 流程(scripts/api-docgenwiki/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-1internal/server.New() 中初始化 Mux 并注册 RegisterFetchHandlerServer,让 cmd/mainservice.Expose 走同一注册路径|验收:独立启动后 curl -X POST localhost:12102/ads.Fetch/ByPos -d '{"key":"x"}' 返回 200 而非 404go 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-2ads_item 增加 (pos_key,status) 复合索引,并给 FindOrder+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-13to_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/clitest/rpcgrpcConnsApmRedisServiceMemorySericeads_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-17proto/const.proto 中他域消息迁出为独立 proto/go_packageads 仅保留自身消息验收ads 的 pb 包不再包含 Cms/Order/Social/Market 消息,scripts/api-docgenwiki/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 从未初始化/路由从未注册而整体 404internal/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 覆盖SDKbsm-sdk/core)与聚合服务(pkgs/allpkgs/ecmall)仅按需读取相关片段,其中记录的 SDK 侧问题(硬编码默认 JWT 密钥 Cblocksmesh2022CGrpcSrv.Serve/Listen 失败即 panic、Start()select {} 阻塞)归属 SDK 仓库,本报告仅在与 ads 调用点相关处引用。