package callback import ( "context" "crypto/sha256" "encoding/hex" stderrors "errors" "strconv" "strings" "time" "github.com/bytedance/sonic" "github.com/gofiber/fiber/v2" "go.uber.org/zap" cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation" carddomain "github.com/break/junhong_cmp_fiber/internal/domain/cardobservation" "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" ) // ResourceSeriesCompleter 提前完成同资源未执行的观测序列。 type ResourceSeriesCompleter interface { CompleteResourceSeries(ctx context.Context, resourceType, resourceID, syncType string) error } // CTCCRealnameHandler 处理电信实名 XML 回调。 type CTCCRealnameHandler struct { translator *carriercallback.CTCCRealnameTranslator resolver *carriercallback.CTCCCardResolver integration *integrationlog.Repository observation *cardapp.Service series ResourceSeriesCompleter config SystemConfigReader logger *zap.Logger } // NewCTCCRealnameHandler 创建电信实名回调 Handler。 func NewCTCCRealnameHandler( translator *carriercallback.CTCCRealnameTranslator, resolver *carriercallback.CTCCCardResolver, integration *integrationlog.Repository, observation *cardapp.Service, series ResourceSeriesCompleter, config SystemConfigReader, logger *zap.Logger, ) *CTCCRealnameHandler { return &CTCCRealnameHandler{ translator: translator, resolver: resolver, integration: integration, observation: observation, series: series, config: config, logger: logger, } } // Realname 接收电信实名结果并返回固定成功应答。 // POST /api/callback/carriers/ctcc/realname func (h *CTCCRealnameHandler) Realname(c *fiber.Ctx) error { startedAt := time.Now() enabled, err := carrierCallbackEnabled(c.UserContext(), h.config, constants.SystemConfigCarrierCallbackCTCCRealnameEnabled) if err != nil { if h.logger != nil { h.logger.Error("读取电信实名回调启停配置失败,已按关闭处理并返回成功", zap.Error(err)) } return sendCTCCSuccess(c, startedAt) } if !enabled { if err := recordDisabledCarrierCallback(c.UserContext(), h.integration, constants.IntegrationProviderCTCC, constants.IntegrationOperationCTCCRealnameCallback, "ctcc-realname", c.Body(), c.Get("Content-Type")); err != nil && h.logger != nil { h.logger.Error("电信实名回调已关闭,但留痕失败", zap.Error(err)) } return sendCTCCSuccess(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 sendCTCCSuccess(c, startedAt) } func (h *CTCCRealnameHandler) process(ctx context.Context, body []byte, contentType string) error { if h == nil || h.translator == nil || h.resolver == nil || h.integration == nil || h.observation == nil { return apperrors.New(apperrors.CodeInternalError, "电信实名回调能力未完整配置") } translated, translateErr := h.translator.Translate(body) idempotencyKey := ctccIdempotencyKey(body, translated) integrationID := "ctcc-realname:" + shortHash(idempotencyKey) resourceKey := optionalHashedResourceKey(translated.ICCID) requestID := middleware.GetRequestIDFromContext(ctx) log, created, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{ IntegrationID: integrationID, IdempotencyKey: idempotencyKey, Provider: constants.IntegrationProviderCTCC, Operation: constants.IntegrationOperationCTCCRealnameCallback, ExternalID: translated.GroupTransactionID, ResourceType: constants.CardObservationResourceTypeCard, ResourceKey: resourceKey, RawPayload: body, ContentType: contentType, RequestID: requestID, CorrelationID: requestID, }) if err != nil { if isAppConflict(err) { return h.recordPayloadConflict(ctx, body, contentType, idempotencyKey, translated, requestID) } return err } if !created { claimed, claimErr := h.claimExistingPending(ctx, log) if claimErr != nil || !claimed { return claimErr } } if translateErr != nil { return h.complete(ctx, log.IntegrationID, constants.IntegrationResultInvalidPayload, false, "报文解析或 ICCID 校验失败") } if translated.Action != carriercallback.CTCCRealnameActionVerified { return h.complete(ctx, log.IntegrationID, constants.IntegrationResultIgnored, false, "合法业务结果无需更新实名事实") } cards, err := h.resolver.Resolve(ctx, translated.ICCID) if err != nil { return h.failPending(ctx, log.IntegrationID, err) } if len(cards) == 0 { return h.complete(ctx, log.IntegrationID, constants.IntegrationResultNotFound, false, "精确列未找到卡") } if len(cards) > 1 { return h.complete(ctx, log.IntegrationID, constants.IntegrationResultConflict, false, "精确列匹配多张卡") } card := cards[0] now := time.Now().UTC() decision, err := h.observation.ApplyCardObservation(ctx, carddomain.RealnameObservation{ CardID: card.ID, Verified: true, Metadata: carddomain.ObservationMetadata{ ObservationID: log.IntegrationID, Source: constants.CardObservationSourceCarrierCallback, Scene: constants.CardObservationSceneCarrierCallback, ObservedAt: now, RequestID: optionalStringValue(requestID), CorrelationID: optionalStringValue(requestID), UpstreamSummary: "电信实名补录成功", }, }) if err != nil { return h.failPending(ctx, log.IntegrationID, err) } if h.series != nil { if err := h.series.CompleteResourceSeries(ctx, constants.CardObservationResourceTypeCard, uintString(card.ID), constants.CardObservationSyncTypeRealname); err != nil && h.logger != nil { h.logger.Warn("电信实名事实已应用但提前完成观测序列失败", zap.Uint("card_id", card.ID), zap.Error(err)) } } return h.complete(ctx, log.IntegrationID, constants.IntegrationResultSuccess, decision.StatusChanged, "实名事实已幂等应用") } func (h *CTCCRealnameHandler) failPending(ctx context.Context, integrationID string, original error) error { if err := h.complete(ctx, integrationID, constants.IntegrationResultFailed, false, "内部处理失败"); err != nil && h.logger != nil { h.logger.Error("电信实名回调失败终态写入失败", zap.String("integration_id", integrationID), zap.Error(err)) } return original } func (h *CTCCRealnameHandler) recordPayloadConflict(ctx context.Context, body []byte, contentType, baseKey string, translated carriercallback.CTCCRealnameTranslation, requestID *string) error { conflictKey := baseKey + ":conflict:" + shortHashBytes(body) log, created, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{ IntegrationID: "ctcc-realname-conflict:" + shortHash(conflictKey), IdempotencyKey: conflictKey, Provider: constants.IntegrationProviderCTCC, Operation: constants.IntegrationOperationCTCCRealnameCallback, ExternalID: translated.GroupTransactionID, 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.claimExistingPending(ctx, log) if claimErr != nil || !claimed { return claimErr } } return h.complete(ctx, log.IntegrationID, constants.IntegrationResultConflict, false, "同一幂等语义对应不同载荷") } func (h *CTCCRealnameHandler) claimExistingPending(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 *CTCCRealnameHandler) complete(ctx context.Context, integrationID, result string, stateChanged bool, reason string) error { _, err := h.integration.Complete(ctx, integrationID, integrationlog.Completion{ Result: result, HTTPStatus: fiber.StatusOK, StateChanged: stateChanged, ResponseSummary: map[string]any{"reason": reason}, }) return err } func ctccIdempotencyKey(body []byte, translated carriercallback.CTCCRealnameTranslation) string { if strings.TrimSpace(translated.GroupTransactionID) != "" { return "ctcc-external:" + shortHash(strings.TrimSpace(translated.GroupTransactionID)) } return "ctcc-body:" + shortHashBytes(body) } func optionalHashedResourceKey(iccid string) *string { if strings.TrimSpace(iccid) == "" { return nil } value := "iccid-sha256:" + shortHash(strings.TrimSpace(iccid)) return &value } func shortHash(value string) string { return shortHashBytes([]byte(value)) } func shortHashBytes(value []byte) string { sum := sha256.Sum256(value) return hex.EncodeToString(sum[:])[:32] } func optionalStringValue(value *string) string { if value == nil { return "" } return *value } func isAppConflict(err error) bool { var appErr *apperrors.AppError return stderrors.As(err, &appErr) && appErr.Code == apperrors.CodeConflict } func uintString(value uint) string { return strconv.FormatUint(uint64(value), 10) } type ctccSuccessResponse struct { Code int `json:"code"` Message string `json:"msg"` Timestamp string `json:"timestamp"` } func sendCTCCSuccess(c *fiber.Ctx, startedAt time.Time) error { body, err := sonic.Marshal(ctccSuccessResponse{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) }