收口七月卡状态回调与系列授权兼容契约
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
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 联调延期
This commit is contained in:
79
internal/handler/callback/carrier_switch.go
Normal file
79
internal/handler/callback/carrier_switch.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package callback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"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"
|
||||
)
|
||||
|
||||
// SystemConfigReader 提供运营商回调运行时开关读取能力。
|
||||
type SystemConfigReader interface {
|
||||
Get(ctx context.Context, key string) (string, error)
|
||||
}
|
||||
|
||||
func carrierCallbackEnabled(ctx context.Context, reader SystemConfigReader, key string) (bool, error) {
|
||||
if reader == nil {
|
||||
return false, apperrors.New(apperrors.CodeInternalError, "运营商回调启停配置未装配")
|
||||
}
|
||||
value, err := reader.Get(ctx, key)
|
||||
if err != nil {
|
||||
return false, apperrors.Wrap(apperrors.CodeInternalError, err, "读取运营商回调启停配置失败")
|
||||
}
|
||||
enabled, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return false, apperrors.Wrap(apperrors.CodeInternalError, err, "运营商回调启停配置值无效")
|
||||
}
|
||||
return enabled, nil
|
||||
}
|
||||
|
||||
func recordDisabledCarrierCallback(
|
||||
ctx context.Context,
|
||||
repository *integrationlog.Repository,
|
||||
provider string,
|
||||
operation string,
|
||||
integrationPrefix string,
|
||||
body []byte,
|
||||
contentType string,
|
||||
) error {
|
||||
if repository == nil {
|
||||
return apperrors.New(apperrors.CodeInternalError, "运营商回调留痕能力未配置")
|
||||
}
|
||||
payloadHash := shortHashBytes(body)
|
||||
key := integrationPrefix + "-disabled-body:" + payloadHash
|
||||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||||
log, created, err := repository.RecordInbound(ctx, integrationlog.InboundAttempt{
|
||||
IntegrationID: integrationPrefix + "-disabled:" + shortHash(key),
|
||||
IdempotencyKey: key,
|
||||
Provider: provider,
|
||||
Operation: operation,
|
||||
ResourceType: constants.CardObservationResourceTypeCard,
|
||||
RawPayload: body,
|
||||
ContentType: contentType,
|
||||
RequestID: requestID,
|
||||
CorrelationID: requestID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
if log == nil || log.Result != constants.IntegrationResultPending {
|
||||
return nil
|
||||
}
|
||||
claimed, claimErr := repository.ClaimExpiredInboundPending(ctx, log.IntegrationID, constants.IntegrationInboundProcessingLease)
|
||||
if claimErr != nil || !claimed {
|
||||
return claimErr
|
||||
}
|
||||
}
|
||||
_, err = repository.Complete(ctx, log.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultIgnored,
|
||||
HTTPStatus: fiber.StatusOK,
|
||||
ResponseSummary: map[string]any{"reason": "后台配置已关闭该运营商回调"},
|
||||
})
|
||||
return err
|
||||
}
|
||||
171
internal/handler/callback/cmcc_realname.go
Normal file
171
internal/handler/callback/cmcc_realname.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package callback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/pkg/constants"
|
||||
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
// CMCCRealnameHandler 处理中国移动实名成功 JSON 回调。
|
||||
type CMCCRealnameHandler struct {
|
||||
translator *carriercallback.CMCCRealnameTranslator
|
||||
resolver *carriercallback.CMCCCardResolver
|
||||
integration *integrationlog.Repository
|
||||
observation *cardapp.Service
|
||||
series ResourceSeriesCompleter
|
||||
config SystemConfigReader
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCMCCRealnameHandler 创建中国移动实名回调 Handler。
|
||||
func NewCMCCRealnameHandler(translator *carriercallback.CMCCRealnameTranslator, resolver *carriercallback.CMCCCardResolver, integration *integrationlog.Repository, observation *cardapp.Service, series ResourceSeriesCompleter, config SystemConfigReader, logger *zap.Logger) *CMCCRealnameHandler {
|
||||
return &CMCCRealnameHandler{translator: translator, resolver: resolver, integration: integration, observation: observation, series: series, config: config, logger: logger}
|
||||
}
|
||||
|
||||
// Realname 接收中国移动实名结果并返回固定成功应答。
|
||||
// POST /api/callback/carriers/cmcc/realname
|
||||
func (h *CMCCRealnameHandler) Realname(c *fiber.Ctx) error {
|
||||
startedAt := time.Now()
|
||||
enabled, err := carrierCallbackEnabled(c.UserContext(), h.config, constants.SystemConfigCarrierCallbackCMCCRealnameEnabled)
|
||||
if err != nil {
|
||||
if h.logger != nil {
|
||||
h.logger.Error("读取移动实名回调启停配置失败,已按关闭处理并返回成功", zap.Error(err))
|
||||
}
|
||||
return sendCMCCSuccess(c, startedAt)
|
||||
}
|
||||
if !enabled {
|
||||
if err := recordDisabledCarrierCallback(c.UserContext(), h.integration, constants.IntegrationProviderCMCC, constants.IntegrationOperationCMCCRealnameCallback, "cmcc-realname", c.Body(), c.Get("Content-Type")); err != nil && h.logger != nil {
|
||||
h.logger.Error("移动实名回调已关闭,但留痕失败", zap.Error(err))
|
||||
}
|
||||
return sendCMCCSuccess(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 sendCMCCSuccess(c, startedAt)
|
||||
}
|
||||
|
||||
func (h *CMCCRealnameHandler) 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 := cmccIdempotencyKey(body, translated)
|
||||
integrationID := "cmcc-realname:" + shortHash(idempotencyKey)
|
||||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||||
log, created, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
||||
IntegrationID: integrationID, IdempotencyKey: idempotencyKey,
|
||||
Provider: constants.IntegrationProviderCMCC, Operation: constants.IntegrationOperationCMCCRealnameCallback,
|
||||
ResourceType: constants.CardObservationResourceTypeCard, ResourceKey: optionalHashedResourceKey(translated.ICCID),
|
||||
ExternalID: translated.BusiSeq, RawPayload: body, ContentType: contentType, RequestID: requestID, CorrelationID: requestID,
|
||||
})
|
||||
if err != nil {
|
||||
if isAppConflict(err) {
|
||||
return h.recordConflict(ctx, body, contentType, idempotencyKey, translated, requestID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
if log.Result != constants.IntegrationResultPending {
|
||||
return nil
|
||||
}
|
||||
claimed, claimErr := h.integration.ClaimExpiredInboundPending(ctx, log.IntegrationID, constants.IntegrationInboundProcessingLease)
|
||||
if claimErr != nil || !claimed {
|
||||
return claimErr
|
||||
}
|
||||
}
|
||||
if translateErr != nil {
|
||||
return h.complete(ctx, log.IntegrationID, constants.IntegrationResultInvalidPayload, false, "报文或业务结果无效")
|
||||
}
|
||||
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, false, "精确列未找到卡")
|
||||
}
|
||||
if len(cards) > 1 {
|
||||
return h.complete(ctx, log.IntegrationID, constants.IntegrationResultConflict, false, "精确列匹配多张卡")
|
||||
}
|
||||
card := cards[0]
|
||||
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: time.Now().UTC(),
|
||||
RequestID: optionalStringValue(requestID), CorrelationID: optionalStringValue(requestID), UpstreamSummary: "移动实名成功",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return h.fail(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 *CMCCRealnameHandler) recordConflict(ctx context.Context, body []byte, contentType, baseKey string, translated carriercallback.CMCCRealnameTranslation, requestID *string) error {
|
||||
key := baseKey + ":conflict:" + shortHashBytes(body)
|
||||
log, created, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{IntegrationID: "cmcc-realname-conflict:" + shortHash(key), IdempotencyKey: key, Provider: constants.IntegrationProviderCMCC, Operation: constants.IntegrationOperationCMCCRealnameCallback, ResourceType: constants.CardObservationResourceTypeCard, ResourceKey: optionalHashedResourceKey(translated.ICCID), ExternalID: translated.BusiSeq, RawPayload: body, ContentType: contentType, RequestID: requestID, CorrelationID: requestID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !created {
|
||||
if log.Result != constants.IntegrationResultPending {
|
||||
return nil
|
||||
}
|
||||
claimed, claimErr := h.integration.ClaimExpiredInboundPending(ctx, log.IntegrationID, constants.IntegrationInboundProcessingLease)
|
||||
if claimErr != nil || !claimed {
|
||||
return claimErr
|
||||
}
|
||||
}
|
||||
return h.complete(ctx, log.IntegrationID, constants.IntegrationResultConflict, false, "同一幂等语义对应不同载荷")
|
||||
}
|
||||
|
||||
func (h *CMCCRealnameHandler) complete(ctx context.Context, integrationID, result string, changed bool, reason string) error {
|
||||
_, err := h.integration.Complete(ctx, integrationID, integrationlog.Completion{Result: result, HTTPStatus: fiber.StatusOK, StateChanged: changed, ResponseSummary: map[string]any{"reason": reason}})
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CMCCRealnameHandler) fail(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 cmccIdempotencyKey(body []byte, translated carriercallback.CMCCRealnameTranslation) string {
|
||||
if translated.BusiSeq != "" {
|
||||
return "cmcc-external:" + shortHash(translated.BusiSeq)
|
||||
}
|
||||
return "cmcc-body:" + shortHashBytes(body)
|
||||
}
|
||||
|
||||
type cmccSuccessResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
func sendCMCCSuccess(c *fiber.Ctx, startedAt time.Time) error {
|
||||
body, err := sonic.Marshal(cmccSuccessResponse{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)
|
||||
}
|
||||
243
internal/handler/callback/ctcc_realname.go
Normal file
243
internal/handler/callback/ctcc_realname.go
Normal file
@@ -0,0 +1,243 @@
|
||||
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)
|
||||
}
|
||||
168
internal/handler/callback/cucc_realname.go
Normal file
168
internal/handler/callback/cucc_realname.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package callback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// CUCCRealnameHandler 处理中国联通实名成功回调。
|
||||
type CUCCRealnameHandler struct {
|
||||
translator *carriercallback.CUCCRealnameTranslator
|
||||
resolver *carriercallback.CUCCCardResolver
|
||||
integration *integrationlog.Repository
|
||||
observation *cardapp.Service
|
||||
series ResourceSeriesCompleter
|
||||
config SystemConfigReader
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCUCCRealnameHandler 创建中国联通实名成功回调 Handler。
|
||||
func NewCUCCRealnameHandler(translator *carriercallback.CUCCRealnameTranslator, resolver *carriercallback.CUCCCardResolver, integration *integrationlog.Repository, observation *cardapp.Service, series ResourceSeriesCompleter, config SystemConfigReader, logger *zap.Logger) *CUCCRealnameHandler {
|
||||
return &CUCCRealnameHandler{translator: translator, resolver: resolver, integration: integration, observation: observation, series: series, config: config, logger: logger}
|
||||
}
|
||||
|
||||
// Realname 接收中国联通实名成功结果并返回固定成功应答。
|
||||
// POST /api/callback/carriers/cucc/realname
|
||||
func (h *CUCCRealnameHandler) Realname(c *fiber.Ctx) error {
|
||||
startedAt := time.Now()
|
||||
enabled, err := carrierCallbackEnabled(c.UserContext(), h.config, constants.SystemConfigCarrierCallbackCUCCRealnameEnabled)
|
||||
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.IntegrationOperationCUCCRealnameCallback, "cucc-realname", 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 *CUCCRealnameHandler) 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)
|
||||
key := cuccRealnameIdempotencyKey(body, translated, translateErr == nil)
|
||||
integrationID := "cucc-realname:" + shortHash(key)
|
||||
requestID := middleware.GetRequestIDFromContext(ctx)
|
||||
log, created, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
||||
IntegrationID: integrationID, IdempotencyKey: key,
|
||||
Provider: constants.IntegrationProviderCUCC, Operation: constants.IntegrationOperationCUCCRealnameCallback,
|
||||
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, false, "报文、嵌套 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, false, "精确列未找到卡")
|
||||
}
|
||||
if len(cards) > 1 {
|
||||
return h.complete(ctx, log.IntegrationID, constants.IntegrationResultConflict, false, "精确列匹配多张卡")
|
||||
}
|
||||
card := cards[0]
|
||||
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: time.Now().UTC(),
|
||||
RequestID: optionalStringValue(requestID), CorrelationID: optionalStringValue(requestID),
|
||||
UpstreamSummary: "联通实名成功,变更时间已留痕",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return h.fail(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 *CUCCRealnameHandler) recordConflict(ctx context.Context, body []byte, contentType, baseKey string, translated carriercallback.CUCCRealnameTranslation, requestID *string) error {
|
||||
key := baseKey + ":conflict:" + shortHashBytes(body)
|
||||
log, created, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
||||
IntegrationID: "cucc-realname-conflict:" + shortHash(key), IdempotencyKey: key,
|
||||
Provider: constants.IntegrationProviderCUCC, Operation: constants.IntegrationOperationCUCCRealnameCallback,
|
||||
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, false, "同一实名语义对应不同载荷")
|
||||
}
|
||||
|
||||
func (h *CUCCRealnameHandler) 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 *CUCCRealnameHandler) complete(ctx context.Context, integrationID, result string, changed bool, reason string) error {
|
||||
_, err := h.integration.Complete(ctx, integrationID, integrationlog.Completion{
|
||||
Result: result, HTTPStatus: fiber.StatusOK, StateChanged: changed,
|
||||
ResponseSummary: map[string]any{"reason": reason},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CUCCRealnameHandler) fail(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 cuccRealnameIdempotencyKey(body []byte, translated carriercallback.CUCCRealnameTranslation, valid bool) string {
|
||||
if valid {
|
||||
semantic := strings.TrimSpace(translated.ICCID) + "\x00" + strings.TrimSpace(translated.DateChanged)
|
||||
return "cucc-realname-semantic:" + shortHash(semantic)
|
||||
}
|
||||
return "cucc-realname-body:" + shortHashBytes(body)
|
||||
}
|
||||
159
internal/handler/callback/cucc_realname_removal.go
Normal file
159
internal/handler/callback/cucc_realname_removal.go
Normal file
@@ -0,0 +1,159 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user