feat: ClientRealnameHandler 集成异步触发实名检查

This commit is contained in:
2026-04-13 14:51:42 +08:00
parent 5196472d7e
commit 8569873690

View File

@@ -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 // 手动触发服务可为nilnil时跳过自动触发
}
// 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 而非 UserTypeSuperAdminSuperAdmin 不受日限制约束
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))
}