feat: ClientRealnameHandler 集成异步触发实名检查
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"go.uber.org/zap"
|
"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"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
assetService "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
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/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/constants"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
"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/break/junhong_cmp_fiber/pkg/response"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
)
|
)
|
||||||
@@ -30,6 +35,7 @@ type ClientRealnameHandler struct {
|
|||||||
carrierStore *postgres.CarrierStore
|
carrierStore *postgres.CarrierStore
|
||||||
gatewayClient *gateway.Client
|
gatewayClient *gateway.Client
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
|
manualTriggerSvc *pollingSvc.ManualTriggerService // 手动触发服务(可为nil,nil时跳过自动触发)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientRealnameHandler 创建 C 端实名认证处理器
|
// NewClientRealnameHandler 创建 C 端实名认证处理器
|
||||||
@@ -41,6 +47,7 @@ func NewClientRealnameHandler(
|
|||||||
carrierStore *postgres.CarrierStore,
|
carrierStore *postgres.CarrierStore,
|
||||||
gatewayClient *gateway.Client,
|
gatewayClient *gateway.Client,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
|
manualTriggerSvc *pollingSvc.ManualTriggerService, // 可为nil
|
||||||
) *ClientRealnameHandler {
|
) *ClientRealnameHandler {
|
||||||
return &ClientRealnameHandler{
|
return &ClientRealnameHandler{
|
||||||
assetService: assetSvc,
|
assetService: assetSvc,
|
||||||
@@ -50,6 +57,7 @@ func NewClientRealnameHandler(
|
|||||||
carrierStore: carrierStore,
|
carrierStore: carrierStore,
|
||||||
gatewayClient: gatewayClient,
|
gatewayClient: gatewayClient,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
manualTriggerSvc: manualTriggerSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,34 +102,9 @@ func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. 定位目标卡(3 条路径)
|
// 5. 定位目标卡(3 条路径)
|
||||||
var targetCard *model.IotCard
|
targetCard, err := h.resolveTargetCard(c, asset, req.ICCID)
|
||||||
switch {
|
if err != nil {
|
||||||
case asset.AssetType == "card":
|
return err
|
||||||
// 路径 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, "不支持的资产类型")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 检查实名状态
|
// 6. 检查实名状态
|
||||||
@@ -138,50 +121,83 @@ func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error {
|
|||||||
return errors.New(errors.CodeCarrierNotFound, "运营商信息查询失败")
|
return errors.New(errors.CodeCarrierNotFound, "运营商信息查询失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := &dto.RealnimeLinkResponse{
|
resp, err := h.buildRealnameResponse(ctx, targetCard, carrier)
|
||||||
CardInfo: dto.CardInfoBrief{
|
if err != nil {
|
||||||
ICCID: targetCard.ICCID,
|
return err
|
||||||
MSISDN: targetCard.MSISDN,
|
|
||||||
VirtualNo: targetCard.VirtualNo,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 异步触发实名检查,提升检测优先级;失败不影响主流程
|
||||||
|
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 {
|
switch carrier.RealnameLinkType {
|
||||||
case constants.RealnameLinkTypeNone:
|
case constants.RealnameLinkTypeNone:
|
||||||
// 该运营商不支持在线实名
|
// 该运营商不支持在线实名
|
||||||
return errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名")
|
return nil, errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名")
|
||||||
|
|
||||||
case constants.RealnameLinkTypeTemplate:
|
case constants.RealnameLinkTypeTemplate:
|
||||||
// 模板模式:替换占位符生成实名链接
|
// 模板模式:替换占位符生成实名链接
|
||||||
url := carrier.RealnameLinkTemplate
|
url := carrier.RealnameLinkTemplate
|
||||||
url = strings.ReplaceAll(url, "{iccid}", targetCard.ICCID)
|
url = strings.ReplaceAll(url, "{iccid}", card.ICCID)
|
||||||
url = strings.ReplaceAll(url, "{msisdn}", targetCard.MSISDN)
|
url = strings.ReplaceAll(url, "{msisdn}", card.MSISDN)
|
||||||
url = strings.ReplaceAll(url, "{virtual_no}", targetCard.VirtualNo)
|
url = strings.ReplaceAll(url, "{virtual_no}", card.VirtualNo)
|
||||||
resp.RealnameMode = constants.RealnameLinkTypeTemplate
|
resp.RealnameMode = constants.RealnameLinkTypeTemplate
|
||||||
resp.RealnameURL = url
|
resp.RealnameURL = url
|
||||||
|
|
||||||
case constants.RealnameLinkTypeGateway:
|
case constants.RealnameLinkTypeGateway:
|
||||||
// 网关模式:调用 Gateway 接口获取实名链接
|
// 网关模式:调用 Gateway 接口获取实名链接
|
||||||
linkResp, gwErr := h.gatewayClient.GetRealnameLink(ctx, &gateway.CardStatusReq{
|
linkResp, gwErr := h.gatewayClient.GetRealnameLink(ctx, &gateway.CardStatusReq{
|
||||||
CardNo: targetCard.ICCID,
|
CardNo: card.ICCID,
|
||||||
})
|
})
|
||||||
if gwErr != nil {
|
if gwErr != nil {
|
||||||
logger.GetAppLogger().Error("Gateway 获取实名链接失败",
|
logger.GetAppLogger().Error("Gateway 获取实名链接失败",
|
||||||
zap.String("iccid", targetCard.ICCID),
|
zap.String("iccid", card.ICCID),
|
||||||
zap.Error(gwErr))
|
zap.Error(gwErr))
|
||||||
return errors.Wrap(errors.CodeGatewayError, gwErr, "获取实名链接失败")
|
return nil, errors.Wrap(errors.CodeGatewayError, gwErr, "获取实名链接失败")
|
||||||
}
|
}
|
||||||
resp.RealnameMode = constants.RealnameLinkTypeGateway
|
resp.RealnameMode = constants.RealnameLinkTypeGateway
|
||||||
resp.RealnameURL = linkResp.URL
|
resp.RealnameURL = linkResp.URL
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.GetAppLogger().Warn("未知的实名链接类型",
|
logger.GetAppLogger().Warn("未知的实名链接类型",
|
||||||
zap.Uint("carrier_id", carrier.ID),
|
zap.Uint("carrier_id", carrier.ID),
|
||||||
zap.String("realname_link_type", carrier.RealnameLinkType))
|
zap.String("realname_link_type", carrier.RealnameLinkType))
|
||||||
return errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名")
|
return nil, errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名")
|
||||||
}
|
}
|
||||||
|
return resp, nil
|
||||||
return response.Success(c, resp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// findCardInDeviceBindings 在设备绑定中查找指定 ICCID 的卡
|
// findCardInDeviceBindings 在设备绑定中查找指定 ICCID 的卡
|
||||||
@@ -247,3 +263,33 @@ func (h *ClientRealnameHandler) findFirstBoundCard(c *fiber.Ctx, deviceID uint)
|
|||||||
|
|
||||||
return card, nil
|
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))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user