Files

301 lines
11 KiB
Go

package upstreamapi
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/go-sql-driver/mysql"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
)
type apnOperationStore interface {
Claim(context.Context, string, string, string) (bool, error)
Load(context.Context, string, string) (string, string, error)
Save(context.Context, string, string, string, string) error
}
type sqlApnOperationStore struct{ db gdb.DB }
func (s sqlApnOperationStore) database() gdb.DB {
if s.db != nil {
return s.db
}
return g.DB()
}
func validateApnOperationIdentity(id, channel, hash string) error {
for _, value := range []string{id, channel} {
if len(value) == 0 || len(value) > 128 || !utf8.ValidString(value) || strings.TrimSpace(value) != value || strings.ContainsRune(value, 0) {
return fmt.Errorf("operationId和chanKey必须为1至128字节且不含首尾空白或空字符")
}
}
decoded, err := hex.DecodeString(hash)
if err != nil || len(decoded) != sha256.Size || hash != strings.ToLower(hash) {
return fmt.Errorf("APN请求摘要必须为小写SHA-256")
}
return nil
}
func (s sqlApnOperationStore) Claim(ctx context.Context, id, channel, hash string) (bool, error) {
if err := validateApnOperationIdentity(id, channel, hash); err != nil {
return false, err
}
result, err := s.database().Exec(ctx, "INSERT INTO t_apn_operation (operation_id,chan_key,request_hash,response_json,created_at,updated_at) VALUES (?,?,?,'',NOW(),NOW())", id, channel, hash)
if err != nil {
var duplicate *mysql.MySQLError
if errors.As(err, &duplicate) && duplicate.Number == 1062 {
return false, nil
}
return false, err
}
count, err := result.RowsAffected()
if err != nil {
return false, err
}
if count != 1 {
return false, fmt.Errorf("APN提交意图插入行数异常:%d", count)
}
return true, nil
}
func (s sqlApnOperationStore) Load(ctx context.Context, id, channel string) (string, string, error) {
row, err := s.database().GetOne(ctx, "SELECT request_hash,response_json FROM t_apn_operation WHERE operation_id=? AND chan_key=? AND BINARY operation_id=BINARY ? AND BINARY chan_key=BINARY ?", id, channel, id, channel)
if err != nil {
return "", "", err
}
if len(row) == 0 {
return "", "", nil
}
return row["request_hash"].String(), row["response_json"].String(), nil
}
func (s sqlApnOperationStore) Save(ctx context.Context, id, channel, hash, response string) error {
if err := validateApnOperationIdentity(id, channel, hash); err != nil {
return err
}
return s.database().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
row, err := tx.GetOne("SELECT request_hash,response_json FROM t_apn_operation WHERE operation_id=? AND chan_key=? AND BINARY operation_id=BINARY ? AND BINARY chan_key=BINARY ? FOR UPDATE", id, channel, id, channel)
if err != nil {
return err
}
if len(row) == 0 {
return fmt.Errorf("APN受理映射不存在,禁止无意图保存")
}
if row["request_hash"].String() != hash {
return fmt.Errorf("operationId已绑定不同的办理请求")
}
merged, err := mergeApnOperationJSON(row["response_json"].String(), response)
if err != nil {
return err
}
result, err := tx.Exec("UPDATE t_apn_operation SET response_json=?,revision=revision+1,updated_at=NOW(3) WHERE operation_id=? AND chan_key=? AND request_hash=?", merged, id, channel, hash)
if err != nil {
return err
}
count, err := result.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return fmt.Errorf("APN受理映射保存行数异常:%d", count)
}
return nil
})
}
// 已确认逐卡结果不回退未知,已持久化jobId不能被后续空响应或错误抹掉。
func mergeApnOperationJSON(saved, incoming string) (string, error) {
var output, updates model.SimCardOperateChangeApnOutput
if saved != "" {
if err := json.Unmarshal([]byte(saved), &output); err != nil {
return "", fmt.Errorf("解析已存APN受理映射失败:%w", err)
}
}
if incoming != "" {
if err := json.Unmarshal([]byte(incoming), &updates); err != nil {
return "", fmt.Errorf("解析待存APN受理映射失败:%w", err)
}
}
positions := make(map[model.SimKey]int)
for i, item := range output {
if item == nil || item.SimKey == nil {
return "", fmt.Errorf("已存APN受理映射缺少卡关联")
}
positions[*item.SimKey] = i
}
for _, item := range updates {
if item == nil || item.SimKey == nil {
return "", fmt.Errorf("待存APN受理映射缺少卡关联")
}
i, exists := positions[*item.SimKey]
if !exists {
positions[*item.SimKey] = len(output)
output = append(output, item)
continue
}
old := output[i]
if old.ResData.OrderNum != "" && item.ResData.OrderNum != "" && old.ResData.OrderNum != item.ResData.OrderNum {
return "", fmt.Errorf("同卡APN受理映射出现不同jobId,禁止覆盖")
}
oldTerminal := !old.ResData.Pending && !old.ResData.Uncertain
newTerminal := !item.ResData.Pending && !item.ResData.Uncertain
if oldTerminal || (!newTerminal && old.ResData.OrderNum != "") {
continue
}
if item.ResData.OrderNum == "" {
item.ResData.OrderNum = old.ResData.OrderNum
}
output[i] = item
}
body, err := json.Marshal(output)
return string(body), err
}
func unknownApnOperation(input *model.SimCardOperateChangeApnInput, reason string) model.SimCardOperateChangeApnOutput {
output := make(model.SimCardOperateChangeApnOutput, 0, len(input.SimKeys))
for _, key := range input.SimKeys {
output = append(output, &model.SimBaseRes[model.SimCardOperateChangeApnItem]{SimKey: key, Code: consts.ApiStatusError, Message: reason, ResData: model.SimCardOperateChangeApnItem{Uncertain: true}})
}
return output
}
// durableApnOperation 先落提交意图,重复投递与重启恢复均不重新办理。
func durableApnOperation(ctx context.Context, store apnOperationStore, channel string, input *model.SimCardOperateChangeApnInput, submit func(context.Context) (model.SimCardOperateChangeApnOutput, error)) (model.SimCardOperateChangeApnOutput, error) {
id := input.ReqData.OperationID
if len(input.SimKeys) == 0 || len(input.SimKeys) > 100 {
return nil, fmt.Errorf("异步APN子任务必须包含1至100张卡")
}
normalized := *input
normalized.SimKeys = append([]*model.SimKey(nil), input.SimKeys...)
for _, key := range normalized.SimKeys {
if key == nil || key.ChanKey != channel || key.ICCID == "" {
return nil, fmt.Errorf("异步APN子任务卡标识或通道归属无效")
}
}
sort.Slice(normalized.SimKeys, func(i, j int) bool {
a, b := normalized.SimKeys[i], normalized.SimKeys[j]
return a.ICCID+"|"+a.MSISDN+"|"+a.IMSI < b.ICCID+"|"+b.MSISDN+"|"+b.IMSI
})
normalized.ReqData.QueryJobID = ""
normalized.ReqData.RecoverOnly = false
normalized.ReqData.SubmitOnly = true
payload, err := json.Marshal(normalized)
if err != nil {
return nil, err
}
digest := sha256.Sum256(payload)
hash := hex.EncodeToString(digest[:])
if err := validateApnOperationIdentity(id, channel, hash); err != nil {
return nil, err
}
claimed := false
if !input.ReqData.RecoverOnly {
claimed, err = store.Claim(ctx, id, channel, hash)
if err != nil {
return nil, fmt.Errorf("保存APN提交意图失败,未调用运营商:%w", err)
}
}
if !claimed {
savedHash, saved, err := store.Load(ctx, id, channel)
if err != nil {
return nil, fmt.Errorf("读取APN受理映射失败:%w", err)
}
if savedHash == "" {
return unknownApnOperation(input, "未找到提交意图,禁止自动重新办理,请核对"), nil
}
if savedHash != hash {
return nil, fmt.Errorf("operationId已绑定不同的办理请求")
}
if saved == "" {
return unknownApnOperation(input, "已记录提交意图但受理结果未确认,禁止重复办理,请核对"), nil
}
return completeApnOperation(input, saved)
}
var checkpointMu sync.Mutex
latest := ""
checkpoint := func(partial model.SimCardOperateChangeApnOutput) error {
checkpointMu.Lock()
defer checkpointMu.Unlock()
snapshot, err := json.Marshal(partial)
if err != nil {
return err
}
merged, err := mergeApnOperationJSON(latest, string(snapshot))
if err != nil {
return err
}
latest = merged
saveCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer cancel()
return store.Save(saveCtx, id, channel, hash, latest)
}
submitCtx := context.WithValue(ctx, model.ApnOperationCheckpointKey{}, checkpoint)
output, submitErr := submit(submitCtx)
if submitErr != nil {
output = unknownApnOperation(input, "提交结果未确认:"+submitErr.Error())
} else if len(output) == 0 {
output = unknownApnOperation(input, "提交未返回逐卡受理结果,请核对")
}
if err := checkpoint(output); err != nil {
g.Log().Errorf(context.WithoutCancel(ctx), "APN受理结果持久化失败 operationId=%s chanKey=%s error=%v", id, channel, err)
return nil, fmt.Errorf("上游受理结果持久化失败,禁止重新办理:%w", err)
}
return completeApnOperation(input, latest)
}
func (u *sUpstreamAPI) QuerySimApnCapability(ctx context.Context, input *model.SimCardOperateChangeApnInput) (model.SimApnCapabilityOutput, error) {
if len(input.SimKeys) == 0 || len(input.SimKeys) > 100 {
return nil, fmt.Errorf("APN能力探测必须包含1至100张卡")
}
output := make(model.SimApnCapabilityOutput, 0, len(input.SimKeys))
for _, key := range input.SimKeys {
if key == nil || key.ChanKey == "" || key.ICCID == "" {
return nil, fmt.Errorf("APN能力探测缺少冻结卡标识")
}
output = append(output, &model.SimBaseRes[model.SimApnCapabilityItem]{SimKey: key, ResData: model.SimApnCapabilityItem{ProtocolVersion: 1}})
}
// 已有 jobId 只读查询运营商,不依赖本地提交意图表;提交和 operationId 恢复仍需该表。
if strings.TrimSpace(input.ReqData.QueryJobID) == "" || input.ReqData.SubmitOnly || input.ReqData.RecoverOnly {
probeCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if _, err := g.DB().GetAll(probeCtx, "SELECT revision FROM t_apn_operation LIMIT 0"); err != nil {
return nil, fmt.Errorf("APN持久化迁移未就绪:%w", err)
}
}
return output, nil
}
func completeApnOperation(input *model.SimCardOperateChangeApnInput, saved string) (model.SimCardOperateChangeApnOutput, error) {
var output model.SimCardOperateChangeApnOutput
if err := json.Unmarshal([]byte(saved), &output); err != nil {
return nil, fmt.Errorf("解析已持久化受理映射失败:%w", err)
}
for _, key := range input.SimKeys {
found := false
for _, item := range output {
if item != nil && item.SimKey != nil && *item.SimKey == *key {
found = true
break
}
}
if !found {
missing := *input
missing.SimKeys = []*model.SimKey{key}
output = append(output, unknownApnOperation(&missing, "该卡未持久化受理结果,禁止重新办理,请核对")...)
}
}
return output, nil
}