新增客户端资产、钱包、订单、实名、设备管理等核心业务 Handler 与 DTO: - 客户端资产信息查询、套餐列表、套餐历史、资产刷新 - 客户端钱包详情、流水、充值校验、充值订单、充值记录 - 客户端订单创建、列表、详情 - 客户端实名认证链接获取 - 客户端设备卡列表、重启、恢复出厂、WiFi配置、切卡 - 客户端订单服务(含微信/支付宝支付流程) - 强充自动代购异步任务处理 - 数据库迁移 000084:充值记录增加自动代购状态字段
318 lines
9.9 KiB
Go
318 lines
9.9 KiB
Go
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 "未知"
|
||
}
|
||
}
|