package upstream import ( "context" "errors" "fmt" "sort" "strings" "sync" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" ) // apnRequiredSchema 是异步APN提交依赖的表与关键列,对应 20261015 的四份迁移脚本。 // 缺表或缺列意味着迁移没有执行完,此时提交必然写不进去,应在发请求前就拒绝。 var apnRequiredSchema = map[string][]string{ "t_apn_operation": {"operation_id", "chan_key", "request_hash", "response_json", "revision"}, "data_operation_parent": {"batch_no", "ready", "finalize_token", "finalize_until"}, "data_operation_sub_batch": {"operation_id", "batch_no", "chan_key", "request_data", "status", "poll_count", "lease_token"}, "data_operation_sub_detail": {"detail_id", "operation_id", "batch_no", "iccid"}, "data_operation_card_claim": {"tenant_id", "iccid", "batch_id", "active_batch_id"}, } // apnReadinessTTL 是依赖检查结果的缓存时间:迁移完成后最迟一分钟内自动放行,无需重启。 const apnReadinessTTL = time.Minute // apnReadinessVerifyTimeout 是单次结构校验的独立超时,与调用方上下文解耦。 const apnReadinessVerifyTimeout = 5 * time.Second type apnReadinessCache struct { ttl time.Duration mu sync.Mutex checkedAt time.Time err error // verify 默认指向 verifyAPNSchema;独立出来是为了让缓存行为可测。 verify func(context.Context) error } var defaultAPNReadiness = &apnReadinessCache{ttl: apnReadinessTTL, verify: verifyAPNSchema} // check 返回异步APN提交的依赖状态,结果按 TTL 缓存,避免每次提交都查库。 func (c *apnReadinessCache) check(ctx context.Context) error { c.mu.Lock() defer c.mu.Unlock() if !c.checkedAt.IsZero() && time.Since(c.checkedAt) < c.ttl { return c.err } verify := c.verify if verify == nil { verify = verifyAPNSchema } // 校验使用独立超时,不受调用方取消影响;否则一次取消就会把失败结果缓存给全进程。 verifyCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), apnReadinessVerifyTimeout) defer cancel() c.err = verify(verifyCtx) if c.err != nil && (errors.Is(c.err, context.Canceled) || errors.Is(c.err, context.DeadlineExceeded)) { // 只是本次校验没跑完,不代表依赖缺失,不写入缓存。 c.checkedAt = time.Time{} return c.err } c.checkedAt = time.Now() return c.err } // verifyAPNSchema 直接查询库里的实际表结构,而不是依赖人工开开关确认「迁移已执行」。 func verifyAPNSchema(ctx context.Context) (err error) { if !databaseConfigured(ctx) { // 未配置数据库连接(测试桩、纯传输层用例)时跳过;生产 worker 不存在该情况。 return nil } // g.DB() 在缺配置时会 panic,这里统一转成可读错误,避免拖垮提交链路。 defer func() { if recovered := recover(); recovered != nil { err = gerror.Newf("无法获取数据库连接,不能确认异步APN依赖是否就绪:%v", recovered) } }() db := g.DB() if db == nil { return gerror.New("无法获取数据库连接,不能确认异步APN依赖是否就绪") } config := db.GetConfig() if strings.EqualFold(config.Type, "sqlite") { // 本地/单元测试使用 SQLite,没有 information_schema,跳过结构检查。 return nil } names := make([]string, 0, len(apnRequiredSchema)) for name := range apnRequiredSchema { names = append(names, name) } sort.Strings(names) placeholders := make([]string, 0, len(names)) args := make([]interface{}, 0, len(names)) for _, name := range names { placeholders = append(placeholders, "?") args = append(args, name) } records, err := db.GetAll(ctx, fmt.Sprintf( "SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name FROM information_schema.columns WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (%s)", strings.Join(placeholders, ",")), args...) if err != nil { return gerror.Wrap(err, "查询异步APN依赖表结构失败") } existing := make(map[string]map[string]bool, len(records)) for _, record := range records { table := record["table_name"].String() if existing[table] == nil { existing[table] = make(map[string]bool) } existing[table][strings.ToLower(record["column_name"].String())] = true } return validateAPNSchema(existing) } // databaseConfigured 判断当前配置里是否声明了数据库,用于区分测试桩与真实运行环境。 func databaseConfigured(ctx context.Context) bool { value := g.Cfg().MustGet(ctx, "database") return value != nil && !value.IsNil() && !value.IsEmpty() } // validateAPNSchema 比对必需的表与列,返回可直接据此执行迁移的错误信息。 func validateAPNSchema(existing map[string]map[string]bool) error { tables := make([]string, 0, len(apnRequiredSchema)) for table := range apnRequiredSchema { tables = append(tables, table) } sort.Strings(tables) var missingTables []string var missingColumns []string for _, table := range tables { columns, ok := existing[table] if !ok { missingTables = append(missingTables, table) continue } required := append([]string(nil), apnRequiredSchema[table]...) sort.Strings(required) for _, column := range required { if !columns[column] { missingColumns = append(missingColumns, table+"."+column) } } } switch { case len(missingTables) > 0: return gerror.Newf("异步APN依赖未就绪:缺少数据表 %s,请先执行 20261015 迁移脚本", strings.Join(missingTables, "、")) case len(missingColumns) > 0: return gerror.Newf("异步APN依赖未就绪:缺少字段 %s,请先执行 20261015 迁移脚本", strings.Join(missingColumns, "、")) } return nil }