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 联调延期
160 lines
7.3 KiB
Go
160 lines
7.3 KiB
Go
package callback
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bytedance/sonic"
|
|
"github.com/gofiber/fiber/v2"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/infrastructure/carriercallback"
|
|
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
|
)
|
|
|
|
// CUCCRealnameRemovalHandler 处理中国联通解除实名留痕回调。
|
|
type CUCCRealnameRemovalHandler struct {
|
|
translator *carriercallback.CUCCRealnameRemovalTranslator
|
|
resolver *carriercallback.CUCCCardResolver
|
|
integration *integrationlog.Repository
|
|
config SystemConfigReader
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewCUCCRealnameRemovalHandler 创建中国联通解除实名回调 Handler。
|
|
func NewCUCCRealnameRemovalHandler(translator *carriercallback.CUCCRealnameRemovalTranslator, resolver *carriercallback.CUCCCardResolver, integration *integrationlog.Repository, config SystemConfigReader, logger *zap.Logger) *CUCCRealnameRemovalHandler {
|
|
return &CUCCRealnameRemovalHandler{translator: translator, resolver: resolver, integration: integration, config: config, logger: logger}
|
|
}
|
|
|
|
// Remove 接收联通解除实名结果,仅精确识别资源并留痕忽略。
|
|
// POST /api/callback/carriers/cucc/realname/remove
|
|
func (h *CUCCRealnameRemovalHandler) Remove(c *fiber.Ctx) error {
|
|
startedAt := time.Now()
|
|
enabled, err := carrierCallbackEnabled(c.UserContext(), h.config, constants.SystemConfigCarrierCallbackCUCCRealnameRemovalEnabled)
|
|
if err != nil {
|
|
if h.logger != nil {
|
|
h.logger.Error("读取联通解除实名回调启停配置失败,已按关闭处理并返回成功", zap.Error(err))
|
|
}
|
|
return sendCUCCSuccess(c, startedAt)
|
|
}
|
|
if !enabled {
|
|
if err := recordDisabledCarrierCallback(c.UserContext(), h.integration, constants.IntegrationProviderCUCC, constants.IntegrationOperationCUCCRealnameRemovalCallback, "cucc-realname-remove", c.Body(), c.Get("Content-Type")); err != nil && h.logger != nil {
|
|
h.logger.Error("联通解除实名回调已关闭,但留痕失败", zap.Error(err))
|
|
}
|
|
return sendCUCCSuccess(c, startedAt)
|
|
}
|
|
if err := h.process(c.UserContext(), c.Body(), c.Get("Content-Type")); err != nil && h.logger != nil {
|
|
h.logger.Error("处理联通解除实名回调失败,已按运营商约定返回成功", zap.Error(err))
|
|
}
|
|
return sendCUCCSuccess(c, startedAt)
|
|
}
|
|
|
|
func (h *CUCCRealnameRemovalHandler) process(ctx context.Context, body []byte, contentType string) error {
|
|
if h == nil || h.translator == nil || h.resolver == nil || h.integration == nil {
|
|
return apperrors.New(apperrors.CodeInternalError, "联通解除实名回调能力未完整配置")
|
|
}
|
|
translated, translateErr := h.translator.Translate(body)
|
|
key := cuccRemovalIdempotencyKey(body, translated, translateErr == nil)
|
|
integrationID := "cucc-realname-remove:" + shortHash(key)
|
|
requestID := middleware.GetRequestIDFromContext(ctx)
|
|
log, created, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
|
IntegrationID: integrationID, IdempotencyKey: key,
|
|
Provider: constants.IntegrationProviderCUCC, Operation: constants.IntegrationOperationCUCCRealnameRemovalCallback,
|
|
ResourceType: constants.CardObservationResourceTypeCard, ResourceKey: optionalHashedResourceKey(translated.ICCID),
|
|
RawPayload: body, ContentType: contentType, RequestID: requestID, CorrelationID: requestID,
|
|
})
|
|
if err != nil {
|
|
if isAppConflict(err) {
|
|
return h.recordConflict(ctx, body, contentType, key, translated, requestID)
|
|
}
|
|
return err
|
|
}
|
|
if !created {
|
|
claimed, claimErr := h.claimPending(ctx, log)
|
|
if claimErr != nil || !claimed {
|
|
return claimErr
|
|
}
|
|
}
|
|
if translateErr != nil {
|
|
return h.complete(ctx, log.IntegrationID, constants.IntegrationResultInvalidPayload, "报文、嵌套 data 或字段校验失败")
|
|
}
|
|
cards, err := h.resolver.Resolve(ctx, translated.ICCID)
|
|
if err != nil {
|
|
return h.fail(ctx, log.IntegrationID, err)
|
|
}
|
|
if len(cards) == 0 {
|
|
return h.complete(ctx, log.IntegrationID, constants.IntegrationResultNotFound, "精确列未找到卡")
|
|
}
|
|
if len(cards) > 1 {
|
|
return h.complete(ctx, log.IntegrationID, constants.IntegrationResultConflict, "精确列匹配多张卡")
|
|
}
|
|
return h.complete(ctx, log.IntegrationID, constants.IntegrationResultIgnored, "解除实名仅留痕,不修改本地实名事实")
|
|
}
|
|
|
|
func (h *CUCCRealnameRemovalHandler) recordConflict(ctx context.Context, body []byte, contentType, baseKey string, translated carriercallback.CUCCRealnameRemovalTranslation, requestID *string) error {
|
|
key := baseKey + ":conflict:" + shortHashBytes(body)
|
|
log, created, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
|
IntegrationID: "cucc-realname-remove-conflict:" + shortHash(key), IdempotencyKey: key,
|
|
Provider: constants.IntegrationProviderCUCC, Operation: constants.IntegrationOperationCUCCRealnameRemovalCallback,
|
|
ResourceType: constants.CardObservationResourceTypeCard, ResourceKey: optionalHashedResourceKey(translated.ICCID),
|
|
RawPayload: body, ContentType: contentType, RequestID: requestID, CorrelationID: requestID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !created {
|
|
claimed, claimErr := h.claimPending(ctx, log)
|
|
if claimErr != nil || !claimed {
|
|
return claimErr
|
|
}
|
|
}
|
|
return h.complete(ctx, log.IntegrationID, constants.IntegrationResultConflict, "同一解除实名语义对应不同载荷")
|
|
}
|
|
|
|
func (h *CUCCRealnameRemovalHandler) claimPending(ctx context.Context, log *model.IntegrationLog) (bool, error) {
|
|
if log == nil || log.Result != constants.IntegrationResultPending {
|
|
return false, nil
|
|
}
|
|
return h.integration.ClaimExpiredInboundPending(ctx, log.IntegrationID, constants.IntegrationInboundProcessingLease)
|
|
}
|
|
|
|
func (h *CUCCRealnameRemovalHandler) complete(ctx context.Context, integrationID, result, reason string) error {
|
|
_, err := h.integration.Complete(ctx, integrationID, integrationlog.Completion{Result: result, HTTPStatus: fiber.StatusOK, ResponseSummary: map[string]any{"reason": reason}})
|
|
return err
|
|
}
|
|
|
|
func (h *CUCCRealnameRemovalHandler) fail(ctx context.Context, integrationID string, original error) error {
|
|
if err := h.complete(ctx, integrationID, constants.IntegrationResultFailed, "内部处理失败"); err != nil && h.logger != nil {
|
|
h.logger.Error("联通解除实名回调失败终态写入失败", zap.String("integration_id", integrationID), zap.Error(err))
|
|
}
|
|
return original
|
|
}
|
|
|
|
func cuccRemovalIdempotencyKey(body []byte, translated carriercallback.CUCCRealnameRemovalTranslation, valid bool) string {
|
|
if valid {
|
|
semantic := strings.TrimSpace(translated.ICCID) + "\x00" + strings.TrimSpace(translated.DateChanged)
|
|
return "cucc-semantic:" + shortHash(semantic)
|
|
}
|
|
return "cucc-body:" + shortHashBytes(body)
|
|
}
|
|
|
|
type cuccSuccessResponse struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"msg"`
|
|
Timestamp string `json:"timestamp"`
|
|
}
|
|
|
|
func sendCUCCSuccess(c *fiber.Ctx, startedAt time.Time) error {
|
|
body, err := sonic.Marshal(cuccSuccessResponse{Code: 200, Message: "success", Timestamp: startedAt.Format("2006-01-02 15:04:05")})
|
|
if err != nil {
|
|
return apperrors.Wrap(apperrors.CodeInternalError, err, "生成联通回调应答失败")
|
|
}
|
|
c.Type("json", "utf-8")
|
|
return c.Status(fiber.StatusOK).Send(body)
|
|
}
|