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) }