From 856987369002a4fcf7e0a672bb84ef1559a26e48 Mon Sep 17 00:00:00 2001 From: huang Date: Mon, 13 Apr 2026 14:51:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ClientRealnameHandler=20=E9=9B=86?= =?UTF-8?q?=E6=88=90=E5=BC=82=E6=AD=A5=E8=A7=A6=E5=8F=91=E5=AE=9E=E5=90=8D?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/app/client_realname.go | 140 ++++++++++++++++-------- 1 file changed, 93 insertions(+), 47 deletions(-) diff --git a/internal/handler/app/client_realname.go b/internal/handler/app/client_realname.go index 24d19b6..eb9fa41 100644 --- a/internal/handler/app/client_realname.go +++ b/internal/handler/app/client_realname.go @@ -1,7 +1,9 @@ package app import ( + "context" "strings" + "time" "github.com/gofiber/fiber/v2" "go.uber.org/zap" @@ -11,10 +13,13 @@ import ( "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" assetService "github.com/break/junhong_cmp_fiber/internal/service/asset" + pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling" "github.com/break/junhong_cmp_fiber/internal/store/postgres" + "github.com/break/junhong_cmp_fiber/pkg/config" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/logger" + pkgMiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/break/junhong_cmp_fiber/pkg/response" "github.com/go-playground/validator/v10" ) @@ -30,6 +35,7 @@ type ClientRealnameHandler struct { carrierStore *postgres.CarrierStore gatewayClient *gateway.Client logger *zap.Logger + manualTriggerSvc *pollingSvc.ManualTriggerService // 手动触发服务(可为nil,nil时跳过自动触发) } // NewClientRealnameHandler 创建 C 端实名认证处理器 @@ -41,6 +47,7 @@ func NewClientRealnameHandler( carrierStore *postgres.CarrierStore, gatewayClient *gateway.Client, logger *zap.Logger, + manualTriggerSvc *pollingSvc.ManualTriggerService, // 可为nil ) *ClientRealnameHandler { return &ClientRealnameHandler{ assetService: assetSvc, @@ -50,6 +57,7 @@ func NewClientRealnameHandler( carrierStore: carrierStore, gatewayClient: gatewayClient, logger: logger, + manualTriggerSvc: manualTriggerSvc, } } @@ -94,34 +102,9 @@ func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error { } // 5. 定位目标卡(3 条路径) - var targetCard *model.IotCard - switch { - case asset.AssetType == "card": - // 路径 1:资产本身就是卡,直接使用 - card, cardErr := h.iotCardStore.GetByID(ctx, asset.AssetID) - if cardErr != nil { - return errors.New(errors.CodeIotCardNotFound, "卡信息查询失败") - } - targetCard = card - - case asset.AssetType == "device" && req.ICCID != "": - // 路径 2:资产是设备,指定了 ICCID,从设备绑定中查找该卡 - card, cardErr := h.findCardInDeviceBindings(c, asset.AssetID, req.ICCID) - if cardErr != nil { - return cardErr - } - targetCard = card - - case asset.AssetType == "device": - // 路径 3:资产是设备,未指定 ICCID,取第一张绑定卡(按插槽位置排序) - card, cardErr := h.findFirstBoundCard(c, asset.AssetID) - if cardErr != nil { - return cardErr - } - targetCard = card - - default: - return errors.New(errors.CodeInvalidParam, "不支持的资产类型") + targetCard, err := h.resolveTargetCard(c, asset, req.ICCID) + if err != nil { + return err } // 6. 检查实名状态 @@ -138,50 +121,83 @@ func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error { return errors.New(errors.CodeCarrierNotFound, "运营商信息查询失败") } - resp := &dto.RealnimeLinkResponse{ - CardInfo: dto.CardInfoBrief{ - ICCID: targetCard.ICCID, - MSISDN: targetCard.MSISDN, - VirtualNo: targetCard.VirtualNo, - }, + resp, err := h.buildRealnameResponse(ctx, targetCard, carrier) + if err != nil { + return err } + // 异步触发实名检查,提升检测优先级;失败不影响主流程 + if h.manualTriggerSvc != nil && config.Get().PollingAutoTrigger.EnableAutoTrigger { + systemUserID := uint(config.Get().PollingAutoTrigger.AutoTriggerSystemUserID) + go h.triggerRealnameCheck(targetCard.ID, customerID, targetCard.ICCID, systemUserID) + } + + return response.Success(c, resp) +} + +// resolveTargetCard 根据资产类型和ICCID定位目标卡 +// 支持三条路径:直接卡资产、设备+指定ICCID、设备取第一张绑定卡 +func (h *ClientRealnameHandler) resolveTargetCard(c *fiber.Ctx, asset *dto.AssetResolveResponse, iccid string) (*model.IotCard, error) { + switch { + case asset.AssetType == "card": + // 路径 1:资产本身就是卡,直接使用 + card, cardErr := h.iotCardStore.GetByID(c.UserContext(), asset.AssetID) + if cardErr != nil { + return nil, errors.New(errors.CodeIotCardNotFound, "卡信息查询失败") + } + return card, nil + case asset.AssetType == "device" && iccid != "": + // 路径 2:资产是设备,指定了 ICCID,从设备绑定中查找该卡 + return h.findCardInDeviceBindings(c, asset.AssetID, iccid) + case asset.AssetType == "device": + // 路径 3:资产是设备,未指定 ICCID,取第一张绑定卡(按插槽位置排序) + return h.findFirstBoundCard(c, asset.AssetID) + default: + return nil, errors.New(errors.CodeInvalidParam, "不支持的资产类型") + } +} + +// buildRealnameResponse 根据运营商实名链接类型构建响应 +func (h *ClientRealnameHandler) buildRealnameResponse(ctx context.Context, card *model.IotCard, carrier *model.Carrier) (*dto.RealnimeLinkResponse, error) { + resp := &dto.RealnimeLinkResponse{ + CardInfo: dto.CardInfoBrief{ + ICCID: card.ICCID, + MSISDN: card.MSISDN, + VirtualNo: card.VirtualNo, + }, + } switch carrier.RealnameLinkType { case constants.RealnameLinkTypeNone: // 该运营商不支持在线实名 - return errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名") - + return nil, errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名") case constants.RealnameLinkTypeTemplate: // 模板模式:替换占位符生成实名链接 url := carrier.RealnameLinkTemplate - url = strings.ReplaceAll(url, "{iccid}", targetCard.ICCID) - url = strings.ReplaceAll(url, "{msisdn}", targetCard.MSISDN) - url = strings.ReplaceAll(url, "{virtual_no}", targetCard.VirtualNo) + url = strings.ReplaceAll(url, "{iccid}", card.ICCID) + url = strings.ReplaceAll(url, "{msisdn}", card.MSISDN) + url = strings.ReplaceAll(url, "{virtual_no}", card.VirtualNo) resp.RealnameMode = constants.RealnameLinkTypeTemplate resp.RealnameURL = url - case constants.RealnameLinkTypeGateway: // 网关模式:调用 Gateway 接口获取实名链接 linkResp, gwErr := h.gatewayClient.GetRealnameLink(ctx, &gateway.CardStatusReq{ - CardNo: targetCard.ICCID, + CardNo: card.ICCID, }) if gwErr != nil { logger.GetAppLogger().Error("Gateway 获取实名链接失败", - zap.String("iccid", targetCard.ICCID), + zap.String("iccid", card.ICCID), zap.Error(gwErr)) - return errors.Wrap(errors.CodeGatewayError, gwErr, "获取实名链接失败") + return nil, errors.Wrap(errors.CodeGatewayError, gwErr, "获取实名链接失败") } resp.RealnameMode = constants.RealnameLinkTypeGateway resp.RealnameURL = linkResp.URL - default: logger.GetAppLogger().Warn("未知的实名链接类型", zap.Uint("carrier_id", carrier.ID), zap.String("realname_link_type", carrier.RealnameLinkType)) - return errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名") + return nil, errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名") } - - return response.Success(c, resp) + return resp, nil } // findCardInDeviceBindings 在设备绑定中查找指定 ICCID 的卡 @@ -247,3 +263,33 @@ func (h *ClientRealnameHandler) findFirstBoundCard(c *fiber.Ctx, deviceID uint) return card, nil } + +// triggerRealnameCheck 异步触发单卡实名检查 +// 在独立 goroutine 中调用,使用独立 context 避免 Fiber 请求 context 失效问题 +// 参数全部为值类型,不捕获请求相关指针 +func (h *ClientRealnameHandler) triggerRealnameCheck(cardID, customerID uint, iccid string, systemUserID uint) { + // 必须使用独立 context,禁止复用 Fiber 请求 context(请求返回后即失效) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + // 使用平台用户身份构建 context,绕过卡归属权限检查 + // 注意:使用 UserTypePlatform 而非 UserTypeSuperAdmin,SuperAdmin 不受日限制约束 + sysCtx := pkgMiddleware.SetUserContext(ctx, &pkgMiddleware.UserContextInfo{ + UserID: systemUserID, + UserType: constants.UserTypePlatform, + }) + + err := h.manualTriggerSvc.TriggerSingle(sysCtx, cardID, constants.TaskTypePollingRealname, systemUserID) + if err != nil { + h.logger.Warn("自动触发实名检查失败", + zap.Uint("customer_id", customerID), + zap.String("iccid", iccid), + zap.Uint("card_id", cardID), + zap.Error(err)) + return + } + h.logger.Info("自动触发实名检查成功", + zap.Uint("customer_id", customerID), + zap.String("iccid", iccid), + zap.Uint("card_id", cardID)) +}