Files
junhong_cmp_fiber/internal/infrastructure/carriercallback/ctcc_realname.go
break 5c4d17e9fc
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
收口七月卡状态回调与系列授权兼容契约
完成运营商实名回调、业务事件观测序列与受控配置装配,同时恢复 UR43 已交付的 packages[].remove 字段及旧响应兼容,统一更新 OpenSpec、OpenAPI 和交付文档。

Constraint: 七月测试环境里程碑不新增或运行自动化测试

Rejected: 以必填 operation_type 替换 packages[].remove | 会破坏已交付前端契约

Confidence: high

Scope-risk: broad

Directive: 后续修改系列套餐管理接口必须保持 packages[].remove 和 ShopSeriesGrantResponse 兼容

Tested: go run ./cmd/gendocs;go build -buildvcs=false ./...;openspec validate complete-july-iteration-test-release --strict;git diff --check

Not-tested: 按本 Change 约定未运行 go test,真实运营商与 Gateway 联调延期
2026-07-24 19:59:24 +08:00

108 lines
3.6 KiB
Go

// Package carriercallback 提供运营商回调协议到内部观测模型的适配能力。
package carriercallback
import (
"bytes"
"context"
"encoding/xml"
"strings"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/errors"
pkgvalidator "github.com/break/junhong_cmp_fiber/pkg/validator"
)
const (
// CTCCRealnameActionVerified 表示电信实名补录已完成。
CTCCRealnameActionVerified = "verified"
// CTCCRealnameActionIgnored 表示报文合法但无需改变实名事实。
CTCCRealnameActionIgnored = "ignored"
)
// CTCCRealnameTranslation 是电信 XML 回调的受控内部语义。
type CTCCRealnameTranslation struct {
ICCID string
AcceptMessage string
ResultMessage string
GroupTransactionID string
Action string
}
type ctccContractRoot struct {
XMLName xml.Name `xml:"ContractRoot"`
ICCID string `xml:"ICCID"`
AcceptMessage string `xml:"ACCEPTMSG"`
ResultMessage string `xml:"RESULTMSG"`
GroupTransactionID string `xml:"GROUP_TRANSACTIONID"`
}
// CTCCRealnameTranslator 解析已知电信 XML 协议字段。
type CTCCRealnameTranslator struct{}
// NewCTCCRealnameTranslator 创建电信实名回调 Translator。
func NewCTCCRealnameTranslator() *CTCCRealnameTranslator {
return &CTCCRealnameTranslator{}
}
// Translate 将 XML 转为受控语义;未知业务结果保留为 ignored。
func (t *CTCCRealnameTranslator) Translate(body []byte) (CTCCRealnameTranslation, error) {
var result CTCCRealnameTranslation
if len(bytes.TrimSpace(body)) == 0 {
return result, errors.New(errors.CodeInvalidParam, "电信实名回调报文为空")
}
var payload ctccContractRoot
if err := xml.Unmarshal(body, &payload); err != nil {
return result, errors.Wrap(errors.CodeInvalidParam, err, "电信实名回调 XML 解析失败")
}
result.ICCID = strings.TrimSpace(payload.ICCID)
result.AcceptMessage = strings.TrimSpace(payload.AcceptMessage)
result.ResultMessage = strings.TrimSpace(payload.ResultMessage)
result.GroupTransactionID = strings.TrimSpace(payload.GroupTransactionID)
if err := t.validateICCID(result.ICCID); err != nil {
return result, err
}
result.Action = CTCCRealnameActionIgnored
if result.ResultMessage == "成功" && strings.Contains(result.AcceptMessage, "已完成实名信息补录") {
result.Action = CTCCRealnameActionVerified
}
return result, nil
}
func (t *CTCCRealnameTranslator) validateICCID(iccid string) error {
if result := pkgvalidator.ValidateICCIDWithoutCarrier(iccid); !result.Valid {
return errors.New(errors.CodeInvalidParam, "电信实名回调 ICCID 无效")
}
return nil
}
// CTCCCardResolver 使用系统级精确列查询定位电信回调卡。
type CTCCCardResolver struct {
db *gorm.DB
}
// NewCTCCCardResolver 创建电信回调卡定位器。
func NewCTCCCardResolver(db *gorm.DB) *CTCCCardResolver {
return &CTCCCardResolver{db: db}
}
// Resolve 按 ICCID 长度选择唯一列,禁止跨列降级。
func (r *CTCCCardResolver) Resolve(ctx context.Context, iccid string) ([]*model.IotCard, error) {
if r == nil || r.db == nil {
return nil, errors.New(errors.CodeInternalError, "电信实名回调卡查询能力未配置")
}
column := "iccid_19"
if len(iccid) == 20 {
column = "iccid_20"
}
var cards []*model.IotCard
if err := r.db.WithContext(ctx).
Where(column+" = ? AND deleted_at IS NULL", iccid).
Limit(2).
Find(&cards).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询电信实名回调卡失败")
}
return cards, nil
}