feat(03-02): Asset Service 接入 Gateway sync-info(DEVICE-04)

- Service 结构体新增 gatewayClient 字段,New() 追加 gatewayClient 参数
- Refresh device 分支在绑定卡刷新后调用 SyncDeviceInfo,nil guard + Warn 日志不阻断主流程
- 新增 updateDeviceFromSyncInfo 私有函数:更新 5 个 device 字段 + 调用 UpdateIsCurrentByDeviceID
- bootstrap/services.go asset.New() 调用追加 deps.GatewayClient
- go build ./... 零新增编译错误
This commit is contained in:
2026-03-28 11:38:14 +08:00
parent ba1886c314
commit 15dbf8dc66
7 changed files with 83 additions and 8 deletions

View File

@@ -177,7 +177,7 @@ func initServices(s *stores, deps *Dependencies) *services {
PollingAlert: pollingSvc.NewAlertService(s.PollingAlertRule, s.PollingAlertHistory, deps.Redis, deps.Logger),
PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger),
PollingManualTrigger: pollingSvc.NewManualTriggerService(s.PollingManualTriggerLog, s.IotCard, deps.Redis, deps.Logger),
Asset: assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard),
Asset: assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient),
AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device),
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
StopResumeService: iotCardSvc.NewStopResumeService(deps.DB, deps.Redis, s.IotCard, s.DeviceSimBinding, deps.GatewayClient, deps.Logger),

View File

@@ -24,8 +24,6 @@ type CommissionWithdrawalRequest struct {
PaymentType string `gorm:"column:payment_type;type:varchar(20);default:'manual';comment:放款类型manual=人工打款)" json:"payment_type"`
AccountInfo datatypes.JSON `gorm:"column:account_info;type:jsonb;comment:收款账户信息(姓名、账号等)" json:"account_info"`
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-待审核 2-已通过 3-已拒绝 4-已到账" json:"status"`
ApprovedBy uint `gorm:"column:approved_by;index;comment:审批人用户ID" json:"approved_by"`
ApprovedAt *time.Time `gorm:"column:approved_at;comment:审批时间" json:"approved_at"`
ProcessorID uint `gorm:"column:processor_id;index;comment:处理人ID" json:"processor_id"`
ProcessedAt *time.Time `gorm:"column:processed_at;comment:处理时间" json:"processed_at"`
PaidAt *time.Time `gorm:"column:paid_at;comment:到账时间" json:"paid_at"`

View File

@@ -41,10 +41,10 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
Register(assets, doc, groupPath, "GET", "/:asset_type/:id/packages", handler.Packages, RouteSpec{
Summary: "资产套餐列表",
Description: "查询该资产所有套餐记录,含虚流量换算结果。",
Description: "查询该资产所有套餐记录,含虚流量换算结果。支持分页(默认 page=1, page_size=50, 最大100",
Tags: []string{"资产管理"},
Input: new(dto.AssetTypeIDRequest),
Output: new([]dto.AssetPackageResponse),
Input: new(dto.AssetPackagesRequest),
Output: new(dto.AssetPackagesResult),
Auth: true,
})

View File

@@ -8,6 +8,7 @@ import (
"sort"
"time"
"github.com/break/junhong_cmp_fiber/internal/gateway"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
@@ -37,6 +38,7 @@ type Service struct {
shopStore *postgres.ShopStore
redis *redis.Client
iotCardService IotCardRefresher
gatewayClient *gateway.Client // 用于调用 sync-info 同步设备信息(可为 nil失败时仅记录日志
}
// New 创建资产服务实例
@@ -51,6 +53,7 @@ func New(
shopStore *postgres.ShopStore,
redisClient *redis.Client,
iotCardService IotCardRefresher,
gatewayClient *gateway.Client,
) *Service {
return &Service{
db: db,
@@ -63,6 +66,7 @@ func New(
shopStore: shopStore,
redis: redisClient,
iotCardService: iotCardService,
gatewayClient: gatewayClient,
}
}
@@ -336,6 +340,29 @@ func (s *Service) Refresh(ctx context.Context, assetType string, id uint) (*dto.
// 设置冷却 Key
s.redis.Set(ctx, cooldownKey, 1, constants.DeviceRefreshCooldownDuration)
// 刷新设备自身信息在线状态、当前卡、固件版本等失败不阻断刷新流程per D-15
if s.gatewayClient != nil {
device, devErr := s.deviceStore.GetByID(ctx, id)
if devErr == nil {
// IMEI 优先,无则用 SNper D-3 注释说明)
cardNo := device.IMEI
if cardNo == "" {
cardNo = device.SN
}
if cardNo != "" {
if syncResp, syncErr := s.gatewayClient.SyncDeviceInfo(ctx, &gateway.SyncDeviceInfoReq{
CardNo: cardNo,
}); syncErr == nil {
s.updateDeviceFromSyncInfo(ctx, id, syncResp)
} else {
logger.GetAppLogger().Warn("sync-info 调用失败",
zap.Uint("device_id", id),
zap.Error(syncErr))
}
}
}
}
return s.GetRealtimeStatus(ctx, "device", id)
default:
@@ -343,6 +370,48 @@ func (s *Service) Refresh(ctx context.Context, assetType string, id uint) (*dto.
}
}
// updateDeviceFromSyncInfo 根据 Gateway sync-info 响应更新设备状态字段和当前卡标识
// 失败时只记录日志,不返回错误(非关键步骤)
func (s *Service) updateDeviceFromSyncInfo(ctx context.Context, deviceID uint, resp *gateway.SyncDeviceInfoResp) {
now := time.Now()
// 解析 LastOnlineTimeISO 8601 字符串 → *time.Time
var lastOnlineTime *time.Time
if resp.LastOnlineTime != "" {
if t, err := time.Parse(time.RFC3339, resp.LastOnlineTime); err == nil {
lastOnlineTime = &t
}
}
// 更新设备 5 个存储字段
updates := map[string]any{
"online_status": resp.OnlineStatus,
"software_version": resp.SoftwareVersion,
"switch_mode": resp.SwitchMode,
"last_gateway_sync_at": now,
}
if lastOnlineTime != nil {
updates["last_online_time"] = lastOnlineTime
}
if err := s.db.WithContext(ctx).
Model(&model.Device{}).
Where("id = ?", deviceID).
Updates(updates).Error; err != nil {
logger.GetAppLogger().Warn("sync-info 更新设备字段失败",
zap.Uint("device_id", deviceID),
zap.Error(err))
return
}
// 更新 is_current 标识事务原子操作per D-14 决策)
if err := s.deviceSimBindingStore.UpdateIsCurrentByDeviceID(ctx, deviceID, resp.CurrentIccid); err != nil {
logger.GetAppLogger().Warn("sync-info 更新 is_current 失败",
zap.Uint("device_id", deviceID),
zap.Error(err))
}
}
// GetPackages 获取资产的所有套餐列表(支持分页)
// page 默认 1pageSize 默认 50pageSize 最大 100
func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, page, pageSize int) (*dto.AssetPackagesResult, error) {

View File

@@ -206,8 +206,6 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveWithdraw
"processed_at": now,
"payment_type": req.PaymentType,
"remark": req.Remark,
"approved_by": currentUserID,
"approved_at": now,
}
if req.Amount != nil {