155 lines
5.6 KiB
Go
155 lines
5.6 KiB
Go
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
|
|
}
|