This commit is contained in:
@@ -141,6 +141,10 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
)
|
)
|
||||||
|
|
||||||
stopResumeService := iotCardSvc.NewStopResumeService(deps.Redis, s.IotCard, s.PackageUsage, s.DeviceSimBinding, deps.GatewayClient, deps.Logger)
|
stopResumeService := iotCardSvc.NewStopResumeService(deps.Redis, s.IotCard, s.PackageUsage, s.DeviceSimBinding, deps.GatewayClient, deps.Logger)
|
||||||
|
iotCard.SetRealnameActivator(packageActivation)
|
||||||
|
iotCard.SetStopResumeService(stopResumeService)
|
||||||
|
iotCard.SetDeviceSimBindingStore(s.DeviceSimBinding)
|
||||||
|
iotCard.SetRedisClient(deps.Redis)
|
||||||
device := deviceSvc.New(deps.DB, deps.Redis, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.PackageSeries, deps.GatewayClient, s.AssetIdentifier)
|
device := deviceSvc.New(deps.DB, deps.Redis, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.PackageSeries, deps.GatewayClient, s.AssetIdentifier)
|
||||||
operationPassword := operationPasswordSvc.New(deps.Redis)
|
operationPassword := operationPasswordSvc.New(deps.Redis)
|
||||||
|
|
||||||
|
|||||||
@@ -341,3 +341,43 @@ func (h *AssetHandler) UpdateRealnamePolicy(c *fiber.Ctx) error {
|
|||||||
RealnamePolicy: req.RealnamePolicy,
|
RealnamePolicy: req.RealnamePolicy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateRealnameStatus 手动更新资产实名状态
|
||||||
|
// PATCH /api/admin/assets/:identifier/realname-status
|
||||||
|
func (h *AssetHandler) UpdateRealnameStatus(c *fiber.Ctx) error {
|
||||||
|
identifier := c.Params("identifier")
|
||||||
|
if identifier == "" {
|
||||||
|
return errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.UpdateAssetRealnameStatusRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RealNameStatus != constants.RealNameStatusNotVerified &&
|
||||||
|
req.RealNameStatus != constants.RealNameStatusVerified {
|
||||||
|
return errors.New(errors.CodeInvalidParam, "无效的实名状态")
|
||||||
|
}
|
||||||
|
|
||||||
|
asset, err := h.assetService.Resolve(c.UserContext(), identifier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if asset.AssetType != "card" {
|
||||||
|
return errors.New(errors.CodeInvalidParam, "仅支持IoT卡资产手动修改实名状态")
|
||||||
|
}
|
||||||
|
|
||||||
|
card, err := h.iotCardService.ManualUpdateRealnameStatus(c.UserContext(), asset.AssetID, req.RealNameStatus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, &dto.UpdateAssetRealnameStatusResponse{
|
||||||
|
AssetType: asset.AssetType,
|
||||||
|
AssetID: asset.AssetID,
|
||||||
|
RealNameStatus: card.RealNameStatus,
|
||||||
|
RealNameStatusName: constants.GetRealNameStatusName(card.RealNameStatus),
|
||||||
|
FirstRealnameAt: card.FirstRealnameAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -301,3 +301,18 @@ type UpdateAssetRealnamePolicyResponse struct {
|
|||||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||||
RealnamePolicy string `json:"realname_policy" description:"更新后的实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
RealnamePolicy string `json:"realname_policy" description:"更新后的实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAssetRealnameStatusRequest 手动更新资产实名状态请求
|
||||||
|
type UpdateAssetRealnameStatusRequest struct {
|
||||||
|
Identifier string `path:"identifier" description:"资产标识符(ICCID 或 VirtualNo)" required:"true"`
|
||||||
|
RealNameStatus int `json:"real_name_status" description:"实名状态 (0:未实名, 1:已实名)" required:"true" enum:"0,1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAssetRealnameStatusResponse 手动更新资产实名状态响应
|
||||||
|
type UpdateAssetRealnameStatusResponse struct {
|
||||||
|
AssetType string `json:"asset_type" description:"资产类型 (card:IoT卡)"`
|
||||||
|
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||||
|
RealNameStatus int `json:"real_name_status" description:"更新后的实名状态 (0:未实名, 1:已实名)"`
|
||||||
|
RealNameStatusName string `json:"real_name_status_name" description:"实名状态名称(中文)"`
|
||||||
|
FirstRealnameAt *time.Time `json:"first_realname_at" description:"首次实名时间(仅已实名时可能有值)"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,4 +127,13 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
|
|||||||
Output: new(dto.UpdateAssetRealnamePolicyResponse),
|
Output: new(dto.UpdateAssetRealnamePolicyResponse),
|
||||||
Auth: true,
|
Auth: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Register(assets, doc, groupPath, "PATCH", "/:identifier/realname-status", handler.UpdateRealnameStatus, RouteSpec{
|
||||||
|
Summary: "手动更新卡实名状态",
|
||||||
|
Description: "手动将指定卡资产标记为已实名或未实名。状态更新后会触发停复机评估,并在标记为已实名时触发待实名套餐激活。",
|
||||||
|
Tags: []string{"资产管理"},
|
||||||
|
Input: new(dto.UpdateAssetRealnameStatusRequest),
|
||||||
|
Output: new(dto.UpdateAssetRealnameStatusResponse),
|
||||||
|
Auth: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package iot_card
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
stderrors "errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
"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/constants"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -38,6 +40,13 @@ type DataDeductor interface {
|
|||||||
DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB float64) error
|
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 {
|
type Service struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
iotCardStore *postgres.IotCardStore
|
iotCardStore *postgres.IotCardStore
|
||||||
@@ -50,6 +59,10 @@ type Service struct {
|
|||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
pollingCallback PollingCallback
|
pollingCallback PollingCallback
|
||||||
dataDeductor DataDeductor
|
dataDeductor DataDeductor
|
||||||
|
realnameActivator RealnameActivator
|
||||||
|
stopResumeService StopResumeServiceInterface
|
||||||
|
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||||
|
redis *redis.Client
|
||||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +106,30 @@ func (s *Service) SetDataDeductor(deductor DataDeductor) {
|
|||||||
s.dataDeductor = deductor
|
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) {
|
func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIotCardRequest) (*dto.ListStandaloneIotCardResponse, error) {
|
||||||
page := req.Page
|
page := req.Page
|
||||||
pageSize := req.PageSize
|
pageSize := req.PageSize
|
||||||
@@ -1156,3 +1193,119 @@ func (s *Service) UpdateRealnamePolicy(ctx context.Context, cardID uint, realnam
|
|||||||
|
|
||||||
return nil
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user