Files
junhong_cmp_fiber/internal/handler/admin/asset.go
huang cb75b5668b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m10s
操作日志
2026-04-27 12:16:38 +08:00

431 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package admin
import (
"strconv"
"strings"
"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
}
result, err := h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status)
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
}
result, err := h.assetService.GetCurrentPackage(c.UserContext(), asset.AssetType, asset.AssetID)
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,
})
}