sync: 同步 v1.0.4 开源代码并脱敏
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user