package app import ( "github.com/break/junhong_cmp_fiber/internal/gateway" "github.com/break/junhong_cmp_fiber/internal/middleware" "github.com/break/junhong_cmp_fiber/internal/model/dto" assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/logger" "github.com/break/junhong_cmp_fiber/pkg/response" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "go.uber.org/zap" ) var clientDeviceValidator = validator.New() // deviceAssetInfo validateDeviceAsset 解析后的设备资产信息 type deviceAssetInfo struct { DeviceID uint // 设备数据库 ID IMEI string // 设备 IMEI(用于 Gateway API 调用) VirtualNo string // 设备虚拟号(用于所有权校验) } // ClientDeviceHandler C 端设备能力处理器 // 提供设备卡列表、重启、恢复出厂、WiFi 配置、切卡等操作 type ClientDeviceHandler struct { assetService *assetSvc.Service personalDeviceStore *postgres.PersonalCustomerDeviceStore deviceStore *postgres.DeviceStore deviceSimBindingStore *postgres.DeviceSimBindingStore iotCardStore *postgres.IotCardStore gatewayClient *gateway.Client logger *zap.Logger } // NewClientDeviceHandler 创建 C 端设备能力处理器 func NewClientDeviceHandler( assetService *assetSvc.Service, personalDeviceStore *postgres.PersonalCustomerDeviceStore, deviceStore *postgres.DeviceStore, deviceSimBindingStore *postgres.DeviceSimBindingStore, iotCardStore *postgres.IotCardStore, gatewayClient *gateway.Client, logger *zap.Logger, ) *ClientDeviceHandler { return &ClientDeviceHandler{ assetService: assetService, personalDeviceStore: personalDeviceStore, deviceStore: deviceStore, deviceSimBindingStore: deviceSimBindingStore, iotCardStore: iotCardStore, gatewayClient: gatewayClient, logger: logger, } } // validateDeviceAsset 校验设备资产的所有权和有效性 // 流程:认证 → 资产解析 → 类型校验(仅设备)→ 所有权校验 → IMEI 校验 func (h *ClientDeviceHandler) validateDeviceAsset(c *fiber.Ctx, identifier string) (*deviceAssetInfo, error) { // 获取当前登录的个人客户 ID customerID, ok := middleware.GetCustomerID(c) if !ok || customerID == 0 { return nil, errors.New(errors.CodeUnauthorized) } ctx := c.UserContext() // 通过标识符解析资产 asset, err := h.assetService.Resolve(ctx, identifier) if err != nil { return nil, err } // 仅设备资产支持设备能力操作 if asset.AssetType != "device" { return nil, errors.New(errors.CodeInvalidParam, "仅设备资产支持该操作") } // 校验个人客户对该设备的所有权 owns, err := h.personalDeviceStore.ExistsByCustomerAndDevice(ctx, customerID, asset.VirtualNo) if err != nil { h.logger.Error("校验设备所有权失败", zap.Uint("customer_id", customerID), zap.String("virtual_no", asset.VirtualNo), zap.Error(err)) return nil, errors.New(errors.CodeInternalError) } if !owns { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } // 校验设备 IMEI 是否存在(Gateway API 调用必需) if asset.IMEI == "" { return nil, errors.New(errors.CodeInvalidParam, "设备IMEI缺失") } return &deviceAssetInfo{ DeviceID: asset.AssetID, IMEI: asset.IMEI, VirtualNo: asset.VirtualNo, }, nil } // GetDeviceCards F1 获取设备卡列表 // GET /api/c/v1/device/cards func (h *ClientDeviceHandler) GetDeviceCards(c *fiber.Ctx) error { var req dto.DeviceCardListRequest if err := c.QueryParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } if err := clientDeviceValidator.Struct(&req); err != nil { logger.GetAppLogger().Warn("设备卡列表参数校验失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } info, err := h.validateDeviceAsset(c, req.Identifier) if err != nil { return err } ctx := c.UserContext() // 查询设备绑定的所有 SIM 卡 bindings, err := h.deviceSimBindingStore.ListByDeviceID(ctx, info.DeviceID) if err != nil { h.logger.Error("查询设备SIM绑定失败", zap.Uint("device_id", info.DeviceID), zap.Error(err)) return errors.New(errors.CodeInternalError) } // 无绑定卡时返回空列表 if len(bindings) == 0 { return response.Success(c, &dto.DeviceCardListResponse{Cards: []dto.DeviceCardItem{}}) } // 收集卡 ID 并记录插槽位置映射 cardIDs := make([]uint, 0, len(bindings)) slotMap := make(map[uint]int, len(bindings)) for _, b := range bindings { cardIDs = append(cardIDs, b.IotCardID) slotMap[b.IotCardID] = b.SlotPosition } // 批量查询卡详情 cards, err := h.iotCardStore.GetByIDs(ctx, cardIDs) if err != nil { h.logger.Error("批量查询IoT卡失败", zap.Uints("card_ids", cardIDs), zap.Error(err)) return errors.New(errors.CodeInternalError) } // 组装响应,slot_position == 1 视为当前激活卡 items := make([]dto.DeviceCardItem, 0, len(cards)) for _, card := range cards { slot := slotMap[card.ID] items = append(items, dto.DeviceCardItem{ CardID: card.ID, ICCID: card.ICCID, MSISDN: card.MSISDN, CarrierName: card.CarrierName, NetworkStatus: networkStatusText(card.NetworkStatus), RealNameStatus: card.RealNameStatus, SlotPosition: slot, IsActive: slot == 1, }) } return response.Success(c, &dto.DeviceCardListResponse{Cards: items}) } // RebootDevice F2 设备重启 // POST /api/c/v1/device/reboot func (h *ClientDeviceHandler) RebootDevice(c *fiber.Ctx) error { var req dto.DeviceRebootRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } if err := clientDeviceValidator.Struct(&req); err != nil { logger.GetAppLogger().Warn("设备重启参数校验失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } info, err := h.validateDeviceAsset(c, req.Identifier) if err != nil { return err } // 调用 Gateway 重启设备 if err := h.gatewayClient.RebootDevice(c.UserContext(), &gateway.DeviceOperationReq{ DeviceID: info.IMEI, }); err != nil { h.logger.Error("Gateway重启设备失败", zap.String("imei", info.IMEI), zap.Error(err)) return errors.Wrap(errors.CodeGatewayError, err, "设备重启失败") } return response.Success(c, &dto.DeviceOperationResponse{Accepted: true}) } // FactoryResetDevice F3 恢复出厂设置 // POST /api/c/v1/device/factory-reset func (h *ClientDeviceHandler) FactoryResetDevice(c *fiber.Ctx) error { var req dto.DeviceFactoryResetRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } if err := clientDeviceValidator.Struct(&req); err != nil { logger.GetAppLogger().Warn("恢复出厂设置参数校验失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } info, err := h.validateDeviceAsset(c, req.Identifier) if err != nil { return err } // 调用 Gateway 恢复出厂设置 if err := h.gatewayClient.ResetDevice(c.UserContext(), &gateway.DeviceOperationReq{ DeviceID: info.IMEI, }); err != nil { h.logger.Error("Gateway恢复出厂设置失败", zap.String("imei", info.IMEI), zap.Error(err)) return errors.Wrap(errors.CodeGatewayError, err, "恢复出厂设置失败") } return response.Success(c, &dto.DeviceOperationResponse{Accepted: true}) } // SetWiFi F4 设备WiFi配置 // POST /api/c/v1/device/wifi // 注意:WiFiReq.CardNo 字段名具有误导性,实际传入的是设备 IMEI,而非卡号 func (h *ClientDeviceHandler) SetWiFi(c *fiber.Ctx) error { var req dto.DeviceWifiRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } if err := clientDeviceValidator.Struct(&req); err != nil { logger.GetAppLogger().Warn("WiFi配置参数校验失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } info, err := h.validateDeviceAsset(c, req.Identifier) if err != nil { return err } // 调用 Gateway 配置 WiFi // CardNo 字段虽名为"卡号",但 Gateway 实际要求传入设备 IMEI if err := h.gatewayClient.SetWiFi(c.UserContext(), &gateway.WiFiReq{ CardNo: info.IMEI, DeviceID: info.IMEI, SSID: req.SSID, Password: req.Password, Enabled: req.Enabled, }); err != nil { h.logger.Error("Gateway配置WiFi失败", zap.String("imei", info.IMEI), zap.String("ssid", req.SSID), zap.Error(err)) return errors.Wrap(errors.CodeGatewayError, err, "WiFi配置失败") } return response.Success(c, &dto.DeviceOperationResponse{Accepted: true}) } // SwitchCard F5 设备切卡 // POST /api/c/v1/device/switch-card func (h *ClientDeviceHandler) SwitchCard(c *fiber.Ctx) error { var req dto.DeviceSwitchCardRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam) } if err := clientDeviceValidator.Struct(&req); err != nil { logger.GetAppLogger().Warn("设备切卡参数校验失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } info, err := h.validateDeviceAsset(c, req.Identifier) if err != nil { return err } // 调用 Gateway 切卡,CardNo 传设备 IMEI if err := h.gatewayClient.SwitchCard(c.UserContext(), &gateway.SwitchCardReq{ CardNo: info.IMEI, ICCID: req.TargetICCID, }); err != nil { h.logger.Error("Gateway切卡失败", zap.String("imei", info.IMEI), zap.String("target_iccid", req.TargetICCID), zap.Error(err)) return errors.Wrap(errors.CodeGatewayError, err, "设备切卡失败") } return response.Success(c, &dto.DeviceSwitchCardResponse{ Accepted: true, TargetICCID: req.TargetICCID, }) } // networkStatusText 将网络状态码转为文本描述 func networkStatusText(status int) string { switch status { case 0: return "停机" case 1: return "开机" default: return "未知" } }