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:
@@ -177,7 +177,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
PollingAlert: pollingSvc.NewAlertService(s.PollingAlertRule, s.PollingAlertHistory, deps.Redis, deps.Logger),
|
PollingAlert: pollingSvc.NewAlertService(s.PollingAlertRule, s.PollingAlertHistory, deps.Redis, deps.Logger),
|
||||||
PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger),
|
PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger),
|
||||||
PollingManualTrigger: pollingSvc.NewManualTriggerService(s.PollingManualTriggerLog, s.IotCard, deps.Redis, 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),
|
AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device),
|
||||||
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
|
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
|
||||||
StopResumeService: iotCardSvc.NewStopResumeService(deps.DB, deps.Redis, s.IotCard, s.DeviceSimBinding, deps.GatewayClient, deps.Logger),
|
StopResumeService: iotCardSvc.NewStopResumeService(deps.DB, deps.Redis, s.IotCard, s.DeviceSimBinding, deps.GatewayClient, deps.Logger),
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ type CommissionWithdrawalRequest struct {
|
|||||||
PaymentType string `gorm:"column:payment_type;type:varchar(20);default:'manual';comment:放款类型(manual=人工打款)" json:"payment_type"`
|
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"`
|
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"`
|
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"`
|
ProcessorID uint `gorm:"column:processor_id;index;comment:处理人ID" json:"processor_id"`
|
||||||
ProcessedAt *time.Time `gorm:"column:processed_at;comment:处理时间" json:"processed_at"`
|
ProcessedAt *time.Time `gorm:"column:processed_at;comment:处理时间" json:"processed_at"`
|
||||||
PaidAt *time.Time `gorm:"column:paid_at;comment:到账时间" json:"paid_at"`
|
PaidAt *time.Time `gorm:"column:paid_at;comment:到账时间" json:"paid_at"`
|
||||||
|
|||||||
@@ -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{
|
Register(assets, doc, groupPath, "GET", "/:asset_type/:id/packages", handler.Packages, RouteSpec{
|
||||||
Summary: "资产套餐列表",
|
Summary: "资产套餐列表",
|
||||||
Description: "查询该资产所有套餐记录,含虚流量换算结果。",
|
Description: "查询该资产所有套餐记录,含虚流量换算结果。支持分页(默认 page=1, page_size=50, 最大100)。",
|
||||||
Tags: []string{"资产管理"},
|
Tags: []string{"资产管理"},
|
||||||
Input: new(dto.AssetTypeIDRequest),
|
Input: new(dto.AssetPackagesRequest),
|
||||||
Output: new([]dto.AssetPackageResponse),
|
Output: new(dto.AssetPackagesResult),
|
||||||
Auth: true,
|
Auth: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"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"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
@@ -37,6 +38,7 @@ type Service struct {
|
|||||||
shopStore *postgres.ShopStore
|
shopStore *postgres.ShopStore
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
iotCardService IotCardRefresher
|
iotCardService IotCardRefresher
|
||||||
|
gatewayClient *gateway.Client // 用于调用 sync-info 同步设备信息(可为 nil,失败时仅记录日志)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New 创建资产服务实例
|
// New 创建资产服务实例
|
||||||
@@ -51,6 +53,7 @@ func New(
|
|||||||
shopStore *postgres.ShopStore,
|
shopStore *postgres.ShopStore,
|
||||||
redisClient *redis.Client,
|
redisClient *redis.Client,
|
||||||
iotCardService IotCardRefresher,
|
iotCardService IotCardRefresher,
|
||||||
|
gatewayClient *gateway.Client,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
db: db,
|
db: db,
|
||||||
@@ -63,6 +66,7 @@ func New(
|
|||||||
shopStore: shopStore,
|
shopStore: shopStore,
|
||||||
redis: redisClient,
|
redis: redisClient,
|
||||||
iotCardService: iotCardService,
|
iotCardService: iotCardService,
|
||||||
|
gatewayClient: gatewayClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +340,29 @@ func (s *Service) Refresh(ctx context.Context, assetType string, id uint) (*dto.
|
|||||||
// 设置冷却 Key
|
// 设置冷却 Key
|
||||||
s.redis.Set(ctx, cooldownKey, 1, constants.DeviceRefreshCooldownDuration)
|
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 优先,无则用 SN(per 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)
|
return s.GetRealtimeStatus(ctx, "device", id)
|
||||||
|
|
||||||
default:
|
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()
|
||||||
|
|
||||||
|
// 解析 LastOnlineTime(ISO 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 获取资产的所有套餐列表(支持分页)
|
// GetPackages 获取资产的所有套餐列表(支持分页)
|
||||||
// page 默认 1,pageSize 默认 50,pageSize 最大 100
|
// page 默认 1,pageSize 默认 50,pageSize 最大 100
|
||||||
func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, page, pageSize int) (*dto.AssetPackagesResult, error) {
|
func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, page, pageSize int) (*dto.AssetPackagesResult, error) {
|
||||||
|
|||||||
@@ -206,8 +206,6 @@ func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveWithdraw
|
|||||||
"processed_at": now,
|
"processed_at": now,
|
||||||
"payment_type": req.PaymentType,
|
"payment_type": req.PaymentType,
|
||||||
"remark": req.Remark,
|
"remark": req.Remark,
|
||||||
"approved_by": currentUserID,
|
|
||||||
"approved_at": now,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Amount != nil {
|
if req.Amount != nil {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE tb_commission_withdrawal_request
|
||||||
|
ADD COLUMN IF NOT EXISTS approved_by BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- 删除 tb_commission_withdrawal_request 表中的冗余审批人字段
|
||||||
|
-- approved_by / approved_at 与 processor_id / processed_at 完全等价:
|
||||||
|
-- Approve 时两组字段写同一个值,Reject 时只写 processor_id。
|
||||||
|
-- processor_id / processed_at 已覆盖全部场景,approved_* 为历史遗留字段。
|
||||||
|
ALTER TABLE tb_commission_withdrawal_request
|
||||||
|
DROP COLUMN IF EXISTS approved_by,
|
||||||
|
DROP COLUMN IF EXISTS approved_at;
|
||||||
Reference in New Issue
Block a user