手动实名
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m24s

This commit is contained in:
2026-04-23 17:35:38 +08:00
parent f177c26016
commit a44c88005d
5 changed files with 221 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ package iot_card
import (
"context"
stderrors "errors"
"time"
"github.com/break/junhong_cmp_fiber/internal/gateway"
@@ -12,6 +13,7 @@ import (
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
)
@@ -38,6 +40,13 @@ type DataDeductor interface {
DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB float64) error
}
// RealnameActivator 实名激活回调接口
// 用于在手动实名后触发待实名套餐激活,避免循环依赖
type RealnameActivator interface {
// ActivateByRealname 根据载体触发待实名套餐激活
ActivateByRealname(ctx context.Context, carrierType string, carrierID uint) error
}
type Service struct {
db *gorm.DB
iotCardStore *postgres.IotCardStore
@@ -50,6 +59,10 @@ type Service struct {
logger *zap.Logger
pollingCallback PollingCallback
dataDeductor DataDeductor
realnameActivator RealnameActivator
stopResumeService StopResumeServiceInterface
deviceSimBindingStore *postgres.DeviceSimBindingStore
redis *redis.Client
assetIdentifierStore *postgres.AssetIdentifierStore
}
@@ -93,6 +106,30 @@ func (s *Service) SetDataDeductor(deductor DataDeductor) {
s.dataDeductor = deductor
}
// SetRealnameActivator 设置实名激活回调
// 在应用启动时由 bootstrap 注入套餐激活服务
func (s *Service) SetRealnameActivator(activator RealnameActivator) {
s.realnameActivator = activator
}
// SetStopResumeService 设置停复机服务
// 在应用启动时由 bootstrap 注入,确保手动实名后可立即评估停复机
func (s *Service) SetStopResumeService(stopResumeService StopResumeServiceInterface) {
s.stopResumeService = stopResumeService
}
// SetDeviceSimBindingStore 设置设备绑定存储
// 在手动实名成功后用于补触发设备维度待实名套餐激活
func (s *Service) SetDeviceSimBindingStore(deviceSimBindingStore *postgres.DeviceSimBindingStore) {
s.deviceSimBindingStore = deviceSimBindingStore
}
// SetRedisClient 设置 Redis 客户端
// 用于清理实名逆转计数器,避免历史计数干扰手动实名后的判定
func (s *Service) SetRedisClient(redisClient *redis.Client) {
s.redis = redisClient
}
func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIotCardRequest) (*dto.ListStandaloneIotCardResponse, error) {
page := req.Page
pageSize := req.PageSize
@@ -1156,3 +1193,119 @@ func (s *Service) UpdateRealnamePolicy(ctx context.Context, cardID uint, realnam
return nil
}
// ManualUpdateRealnameStatus 手动更新卡实名状态
// 用于人工纠偏实名状态,并同步触发实名相关业务(套餐激活、停复机评估、轮询缓存失效)
func (s *Service) ManualUpdateRealnameStatus(ctx context.Context, cardID uint, realNameStatus int) (*model.IotCard, error) {
if realNameStatus != constants.RealNameStatusNotVerified &&
realNameStatus != constants.RealNameStatusVerified {
return nil, errors.New(errors.CodeInvalidParam, "无效的实名状态")
}
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "IoT卡不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
}
oldStatus := card.RealNameStatus
statusChanged := oldStatus != realNameStatus
now := time.Now()
fields := map[string]any{
"last_real_name_check_at": now,
}
if statusChanged {
fields["real_name_status"] = realNameStatus
}
if statusChanged &&
oldStatus != constants.RealNameStatusVerified &&
realNameStatus == constants.RealNameStatusVerified {
fields["first_realname_at"] = now
}
if err := s.iotCardStore.UpdateFields(ctx, cardID, fields); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新卡实名状态失败")
}
freshCard, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询更新后的IoT卡失败")
}
if statusChanged {
s.handleManualRealnameStatusChanged(ctx, oldStatus, freshCard)
}
if s.pollingCallback != nil {
s.pollingCallback.OnCardStatusChanged(ctx, cardID)
}
s.logger.Info("手动更新卡实名状态",
zap.Uint("card_id", cardID),
zap.Int("old_status", oldStatus),
zap.Int("new_status", realNameStatus),
zap.Uint("operator_id", middleware.GetUserIDFromContext(ctx)))
return freshCard, nil
}
// handleManualRealnameStatusChanged 处理手动实名状态变更后的联动逻辑
func (s *Service) handleManualRealnameStatusChanged(ctx context.Context, oldStatus int, card *model.IotCard) {
if card == nil {
return
}
s.clearRealnameReversalCounter(ctx, card.ID)
if oldStatus != constants.RealNameStatusVerified &&
card.RealNameStatus == constants.RealNameStatusVerified {
s.triggerRealnameActivation(ctx, constants.AssetTypeIotCard, card.ID)
if s.deviceSimBindingStore != nil {
binding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
if err == nil && binding != nil {
s.triggerRealnameActivation(ctx, constants.AssetTypeDevice, binding.DeviceID)
} else if err != nil && !stderrors.Is(err, gorm.ErrRecordNotFound) {
s.logger.Warn("手动实名后查询绑定设备失败",
zap.Uint("card_id", card.ID),
zap.Error(err))
}
}
}
if s.stopResumeService != nil {
if err := s.stopResumeService.EvaluateAndAct(ctx, card); err != nil {
s.logger.Warn("手动实名后触发停复机评估失败",
zap.Uint("card_id", card.ID),
zap.Error(err))
}
}
}
// triggerRealnameActivation 触发待实名套餐激活
func (s *Service) triggerRealnameActivation(ctx context.Context, carrierType string, carrierID uint) {
if s.realnameActivator == nil {
return
}
if err := s.realnameActivator.ActivateByRealname(ctx, carrierType, carrierID); err != nil {
s.logger.Warn("手动实名后触发待实名套餐激活失败",
zap.String("carrier_type", carrierType),
zap.Uint("carrier_id", carrierID),
zap.Error(err))
}
}
// clearRealnameReversalCounter 清理实名逆转连续计数器
func (s *Service) clearRealnameReversalCounter(ctx context.Context, cardID uint) {
if s.redis == nil {
return
}
if err := s.redis.Del(ctx, constants.RedisPollingRealnameReversalCountKey(cardID)).Err(); err != nil {
s.logger.Warn("清理实名逆转计数器失败",
zap.Uint("card_id", cardID),
zap.Error(err))
}
}