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.
47 KiB
审计报告: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 - 证据:
// 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.0runtime/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 - 证据:
// 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 - 证据:
# 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-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 - 证据:
# 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 - 证据:
# 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-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 - 证据:
// 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 - 证据:
// 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 - 证据:
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 - 证据:
// 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、无 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 - 证据:
// 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 - 证据:
// 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 - 证据:
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.0runtime/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 - 证据:
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 - 证据:
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 - 证据:
- **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 - 证据:
// 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+ 行) - 证据:
// 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 - 证据:
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或 protogoogle.protobuf.Timestamp,避免自定义布局。
19. go.mod 使用指向仓库外的相对 replace,工具依赖被标为 indirect
- 位置:
go.mod:3,26-67,69、README.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-8、bsm-sdk/core/conf/types.go:18-21、bsm-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等环境变量,配置与环境强耦合在主进程 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 - 证据:
// 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. 推荐优化方案
- 统一入口与注册路径(目标:消除“独立进程 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(调用方签名不变)。风险:低;需回归两条部署路径的启动自检。 - 读路径性能兜底(目标:把每次请求的代价从 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)。 - 鉴权与限流对齐(目标:无论哪种部署都有一致的身份策略):做法:以聚合
Authorization.Anonymous为唯一白名单来源,明确 ads 是否公开;独立入口装配相同拦截器并加限流与 reflection 开关。影响面:etc/*.yaml、internal/server/new.go。风险:中;若把 ByPos 改为需登录,需同步前端与文档。 - 密钥与配置治理(目标:杜绝占位/默认密钥进入生产):做法:prod 配置独立化(DSN/库/sslmode)、启动时拒绝
CHANGE_ME与默认 JWT 密钥、要求显式设置BSM_JwtSecretKey(长度与复杂度校验)、删除未使用的SecretKey或真正使用。影响面:internal/config/config.go、etc/*、.builds/etc/ads_prod.yaml。风险:中;需与运维确认密钥注入方式,避免上线即 fail-fast 造成不可用。 - 数据模型与迁移一致化(目标:模型、DDL、查询三者不再互相打脸):做法:提供版本化迁移(含索引与
deleted_at),统一status默认值为 1 并定义常量,重写 README 的建表语句。影响面:internal/models/*、新增migrations/、README.md。风险:中;已有线上表需用非破坏性ALTER TABLE并核对现存数据 status 分布。 - 可观测性与错误治理(目标:任何失败都能定位):做法:错误包装并上报原始 err +
pos_key+ request_id;接入Apm或删除该配置;注册 gRPC 健康检查与/health;生产关闭 GORMDebug()。影响面:internal/logic/fetch/by_pos.go、internal/impl/impl.go、internal/server/new.go。风险:低。 - 启停与资源生命周期(目标:可优雅重启、无强依赖):做法:信号处理 + 关停时按序关闭 gRPC/DB/Redis/Etcd;Redis 改为懒加载且失败降级(因当前未被使用,最简方案是直接移除初始化)。影响面:
cmd/main/main.go、internal/impl/impl.go。风险:低。 - 测试与 CI 门禁(目标:让上述修复可回归):做法:按问题 14 的清单补单测/网关集成测试,CI 跑
go test ./...、go vet、gofmt -l(当前两者已通过,可作为基线门禁)。影响面:test/、模块 CI 配置。风险:低。 - 代码与 proto 瘦身(目标:消除误导性声明与生成代码膨胀):做法:删除
cmd/cli、test/rpc、grpcConns、ads_pos的“僵尸”定义;把const.proto的他域消息迁出。影响面:多个文件与 codegen 流程(scripts/api-docgen、wiki/api/01-ads.md生成)。风险:中;迁移 proto 会影响所有引用base_ads_blocks的模块,需一次性同步生成与文档。 - 部署配置加固(目标:可运维、不写满磁盘、不越权):做法:supervisor 改非 root、加日志轮转与
environment=、显式设置BSM_RuntimeMode;把连接池/超时提升为可配置。影响面:etc/supervisor.bsm-apps-ads.conf、SDKconf.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 默认值、网关路由与字段映射、Exposenil 依赖、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 调用点相关处引用。