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 }