package admin import ( "strconv" "strings" "time" "github.com/gofiber/fiber/v2" dto "github.com/break/junhong_cmp_fiber/internal/model/dto" assetService "github.com/break/junhong_cmp_fiber/internal/service/asset" assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" deviceService "github.com/break/junhong_cmp_fiber/internal/service/device" iotCardService "github.com/break/junhong_cmp_fiber/internal/service/iot_card" pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling" "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/break/junhong_cmp_fiber/pkg/response" ) // AssetHandler 资产管理处理器 // 提供统一的资产解析、实时状态、套餐查询、停复机、停用、轮询管控等接口 type AssetHandler struct { assetService *assetService.Service assetAuditService *assetAuditSvc.Service deviceService *deviceService.Service iotCardService *iotCardService.Service iotCardStopResume *iotCardService.StopResumeService assetPolling *pollingSvc.AssetPollingService assetLifecycleService AssetLifecycleService } // NewAssetHandler 创建资产管理处理器 func NewAssetHandler( assetSvc *assetService.Service, assetAuditService *assetAuditSvc.Service, deviceSvc *deviceService.Service, iotCardSvc *iotCardService.Service, iotCardStopResume *iotCardService.StopResumeService, assetPolling *pollingSvc.AssetPollingService, ) *AssetHandler { return &AssetHandler{ assetService: assetSvc, assetAuditService: assetAuditService, deviceService: deviceSvc, iotCardService: iotCardSvc, iotCardStopResume: iotCardStopResume, assetPolling: assetPolling, } } // SetLifecycleService 注入生命周期服务(用于 Deactivate 接口) func (h *AssetHandler) SetLifecycleService(svc AssetLifecycleService) { h.assetLifecycleService = svc } // resolveByIdentifier 通过标识符解析资产,用于各处理器的公共逻辑 func (h *AssetHandler) resolveByIdentifier(c *fiber.Ctx) (*dto.AssetResolveResponse, error) { identifier := c.Params("identifier") if identifier == "" { return nil, errors.New(errors.CodeInvalidParam, "标识符不能为空") } return h.assetService.Resolve(c.UserContext(), identifier) } // Resolve 通过任意标识符解析资产(设备或卡) // GET /api/admin/assets/resolve/:identifier func (h *AssetHandler) Resolve(c *fiber.Ctx) error { identifier := c.Params("identifier") if identifier == "" { return errors.New(errors.CodeInvalidParam, "标识符不能为空") } result, err := h.assetService.Resolve(c.UserContext(), identifier) if err != nil { return err } return response.Success(c, result) } // RealtimeStatus 获取资产实时状态 // GET /api/admin/assets/:identifier/realtime-status func (h *AssetHandler) RealtimeStatus(c *fiber.Ctx) error { asset, err := h.resolveByIdentifier(c) if err != nil { return err } result, err := h.assetService.GetRealtimeStatus(c.UserContext(), asset.AssetType, asset.AssetID) if err != nil { return err } return response.Success(c, result) } // Refresh 刷新资产状态(调网关同步) // POST /api/admin/assets/:identifier/refresh func (h *AssetHandler) Refresh(c *fiber.Ctx) error { userType := middleware.GetUserTypeFromContext(c.UserContext()) if userType == constants.UserTypeEnterprise { return errors.New(errors.CodeForbidden, "企业账号无权主动刷新资产状态") } asset, err := h.resolveByIdentifier(c) if err != nil { return err } result, err := h.assetService.Refresh(c.UserContext(), asset.AssetType, asset.AssetID) if err != nil { return err } return response.Success(c, result) } // Packages 获取资产所有套餐列表(支持分页,默认 page=1 pageSize=50) // GET /api/admin/assets/:identifier/packages func (h *AssetHandler) Packages(c *fiber.Ctx) error { asset, err := h.resolveByIdentifier(c) if err != nil { return err } page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 50) statusRaw := c.Query("status") var status *int if statusRaw != "" { parsedStatus, parseErr := strconv.Atoi(statusRaw) if parseErr != nil || parsedStatus < constants.PackageUsageStatusPending || parsedStatus > constants.PackageUsageStatusInvalidated { return errors.New(errors.CodeInvalidParam) } status = &parsedStatus } callerAccountType := resolveCallerAccountType(c) result, err := h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType) if err != nil { return err } return response.Success(c, result) } // CurrentPackage 获取资产当前生效套餐 // GET /api/admin/assets/:identifier/current-package func (h *AssetHandler) CurrentPackage(c *fiber.Ctx) error { asset, err := h.resolveByIdentifier(c) if err != nil { return err } callerAccountType := resolveCallerAccountType(c) result, err := h.assetService.GetCurrentPackage(c.UserContext(), asset.AssetType, asset.AssetID, callerAccountType) if err != nil { return err } return response.Success(c, result) } // UpdatePackageExpiresAt 修改资产套餐过期时间 // PATCH /api/admin/assets/:identifier/packages/:package_usage_id/expires-at func (h *AssetHandler) UpdatePackageExpiresAt(c *fiber.Ctx) error { if err := ensureAssetPackageAdjuster(c); err != nil { return err } asset, packageUsageID, err := h.resolveAssetPackageUsagePath(c) if err != nil { return err } var req dto.UpdateAssetPackageExpiresAtRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } expiresAt, err := parseAssetPackageTime(req.ExpiresAt) if err != nil { return errors.New(errors.CodeInvalidParam, "过期时间格式无效") } result, err := h.assetService.UpdatePackageExpiresAt(c.UserContext(), asset.AssetType, asset.AssetID, assetIdentifierForAudit(asset), packageUsageID, expiresAt) if err != nil { return err } return response.Success(c, result) } // UpdatePackageUsage 修改资产套餐已用量 // PATCH /api/admin/assets/:identifier/packages/:package_usage_id/used-data func (h *AssetHandler) UpdatePackageUsage(c *fiber.Ctx) error { if err := ensureAssetPackageAdjuster(c); err != nil { return err } asset, packageUsageID, err := h.resolveAssetPackageUsagePath(c) if err != nil { return err } var req dto.UpdateAssetPackageUsageRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } if req.DataUsageMB == nil || *req.DataUsageMB < 0 { return errors.New(errors.CodeInvalidParam) } result, err := h.assetService.UpdatePackageUsage(c.UserContext(), asset.AssetType, asset.AssetID, assetIdentifierForAudit(asset), packageUsageID, *req.DataUsageMB) if err != nil { return err } return response.Success(c, result) } // Stop 停机(设备批量停机,单卡停机) // POST /api/admin/assets/:identifier/stop func (h *AssetHandler) Stop(c *fiber.Ctx) error { asset, err := h.resolveByIdentifier(c) if err != nil { return err } switch asset.AssetType { case "device": result, devErr := h.deviceService.StopDevice(c.UserContext(), asset.AssetID) if devErr != nil { return devErr } return response.Success(c, result) case "card": if asset.ICCID == "" { return errors.New(errors.CodeInternalError, "卡 ICCID 为空,无法停机") } if cardErr := h.iotCardStopResume.ManualStopCard(c.UserContext(), asset.ICCID); cardErr != nil { return cardErr } return response.Success(c, nil) default: return errors.New(errors.CodeInvalidParam, "不支持的资产类型") } } // Start 复机(设备批量复机,单卡复机) // POST /api/admin/assets/:identifier/start func (h *AssetHandler) Start(c *fiber.Ctx) error { asset, err := h.resolveByIdentifier(c) if err != nil { return err } switch asset.AssetType { case "device": if devErr := h.deviceService.StartDevice(c.UserContext(), asset.AssetID); devErr != nil { return devErr } return response.Success(c, nil) case "card": if asset.ICCID == "" { return errors.New(errors.CodeInternalError, "卡 ICCID 为空,无法复机") } if cardErr := h.iotCardStopResume.ManualStartCard(c.UserContext(), asset.ICCID); cardErr != nil { return cardErr } return response.Success(c, nil) default: return errors.New(errors.CodeInvalidParam, "不支持的资产类型") } } // Deactivate 停用资产 // PATCH /api/admin/assets/:identifier/deactivate func (h *AssetHandler) Deactivate(c *fiber.Ctx) error { if h.assetLifecycleService == nil { return errors.New(errors.CodeInternalError, "资产生命周期服务未配置") } asset, err := h.resolveByIdentifier(c) if err != nil { return err } switch asset.AssetType { case "device": if devErr := h.assetLifecycleService.DeactivateDevice(c.UserContext(), asset.AssetID); devErr != nil { return devErr } case "card": if cardErr := h.assetLifecycleService.DeactivateIotCard(c.UserContext(), asset.AssetID); cardErr != nil { return cardErr } default: return errors.New(errors.CodeInvalidParam, "不支持的资产类型") } return response.Success(c, nil) } // Orders 查询资产历史订单(支持跨代追溯) // GET /api/admin/assets/:identifier/orders func (h *AssetHandler) Orders(c *fiber.Ctx) error { identifier := c.Params("identifier") if identifier == "" { return errors.New(errors.CodeInvalidParam, "标识符不能为空") } page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 20) includePrevious := c.QueryBool("include_previous", false) result, err := h.assetService.GetOrders(c.UserContext(), identifier, page, pageSize, includePrevious) if err != nil { return err } return response.Success(c, result) } // OperationLogs 查询资产操作审计日志 // GET /api/admin/assets/:identifier/operation-logs func (h *AssetHandler) OperationLogs(c *fiber.Ctx) error { if h.assetAuditService == nil { return errors.New(errors.CodeInternalError, "资产审计服务未配置") } asset, err := h.resolveByIdentifier(c) if err != nil { return err } page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 20) if page <= 0 || pageSize <= 0 || pageSize > 100 { return errors.New(errors.CodeInvalidParam, "分页参数无效") } operationType := strings.TrimSpace(c.Query("operation_type")) resultStatus := strings.TrimSpace(c.Query("result_status")) if resultStatus != "" && resultStatus != constants.AssetAuditResultSuccess && resultStatus != constants.AssetAuditResultFailed && resultStatus != constants.AssetAuditResultDenied { return errors.New(errors.CodeInvalidParam, "无效的结果状态") } result, err := h.assetAuditService.ListByAsset(c.UserContext(), assetAuditSvc.ListByAssetParams{ AssetType: asset.AssetType, AssetID: asset.AssetID, Page: page, PageSize: pageSize, OperationType: operationType, ResultStatus: resultStatus, }) if err != nil { return err } return response.Success(c, result) } // UpdatePollingStatus 更新资产轮询状态 // PATCH /api/admin/assets/:identifier/polling-status func (h *AssetHandler) UpdatePollingStatus(c *fiber.Ctx) error { identifier := c.Params("identifier") if identifier == "" { return errors.New(errors.CodeInvalidParam) } var req dto.UpdateAssetPollingStatusRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } asset, err := h.assetService.Resolve(c.UserContext(), identifier) if err != nil { return err } _ = middleware.GetUserIDFromContext(c.UserContext()) if h.assetPolling == nil { return errors.New(errors.CodeInternalError, "轮询管控服务未配置") } pollingAssetType := asset.AssetType if pollingAssetType == "card" { pollingAssetType = "iot_card" } if err := h.assetPolling.UpdatePollingStatus(c.UserContext(), pollingAssetType, asset.AssetID, req.EnablePolling); err != nil { return err } return response.Success(c, &dto.UpdateAssetPollingStatusResponse{ AssetType: asset.AssetType, AssetID: asset.AssetID, EnablePolling: req.EnablePolling, }) } // UpdateRealnamePolicy 更新资产实名认证策略 // PATCH /api/admin/assets/:identifier/realname-mode func (h *AssetHandler) UpdateRealnamePolicy(c *fiber.Ctx) error { identifier := c.Params("identifier") if identifier == "" { return errors.New(errors.CodeInvalidParam) } var req dto.UpdateAssetRealnamePolicyRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } if req.RealnamePolicy != constants.RealnamePolicyNone && req.RealnamePolicy != constants.RealnamePolicyBeforeOrder && req.RealnamePolicy != constants.RealnamePolicyAfterOrder { return errors.New(errors.CodeInvalidParam, "无效的实名认证策略") } asset, err := h.assetService.Resolve(c.UserContext(), identifier) if err != nil { return err } switch asset.AssetType { case "card": if err := h.iotCardService.UpdateRealnamePolicy(c.UserContext(), asset.AssetID, req.RealnamePolicy); err != nil { return err } case "device": if err := h.deviceService.UpdateRealnamePolicy(c.UserContext(), asset.AssetID, req.RealnamePolicy); err != nil { return err } default: return errors.New(errors.CodeInvalidParam, "不支持的资产类型") } return response.Success(c, &dto.UpdateAssetRealnamePolicyResponse{ AssetType: asset.AssetType, AssetID: asset.AssetID, 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, }) } // resolveCallerAccountType 从 middleware 上下文中提取调用方账号类型 // 平台账号(SuperAdmin/Platform)返回 "platform",其他返回 "agent" func resolveCallerAccountType(c *fiber.Ctx) string { userType := middleware.GetUserTypeFromContext(c.UserContext()) if userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform { return constants.OwnerTypePlatform } return "agent" } func (h *AssetHandler) resolveAssetPackageUsagePath(c *fiber.Ctx) (*dto.AssetResolveResponse, uint, error) { asset, err := h.resolveByIdentifier(c) if err != nil { return nil, 0, err } packageUsageID, parseErr := strconv.ParseUint(c.Params("package_usage_id"), 10, 64) if parseErr != nil || packageUsageID == 0 { return nil, 0, errors.New(errors.CodeInvalidParam) } return asset, uint(packageUsageID), nil } func ensureAssetPackageAdjuster(c *fiber.Ctx) error { if middleware.GetUserIDFromContext(c.UserContext()) != 2 { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } return nil } func parseAssetPackageTime(value string) (time.Time, error) { raw := strings.TrimSpace(value) if raw == "" { return time.Time{}, errors.New(errors.CodeInvalidParam) } layouts := []string{ time.RFC3339, "2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02", } for _, layout := range layouts { if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil { return t, nil } } return time.Time{}, errors.New(errors.CodeInvalidParam) } func assetIdentifierForAudit(asset *dto.AssetResolveResponse) string { if asset == nil { return "" } if asset.AssetType == "card" && strings.TrimSpace(asset.ICCID) != "" { return asset.ICCID } if strings.TrimSpace(asset.VirtualNo) != "" { return asset.VirtualNo } return asset.Identifier }