670 lines
21 KiB
Go
670 lines
21 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
|
||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||
"github.com/gofiber/fiber/v2"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ClientAssetHandler C 端资产信息处理器
|
||
// 提供 B1~B4 资产信息、可购套餐、套餐历史、手动刷新接口
|
||
type ClientAssetHandler struct {
|
||
assetService *asset.Service
|
||
customerBinding *customerBinding.Service
|
||
assetWalletStore *postgres.AssetWalletStore
|
||
packageStore *postgres.PackageStore
|
||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||
iotCardStore *postgres.IotCardStore
|
||
deviceStore *postgres.DeviceStore
|
||
db *gorm.DB
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// NewClientAssetHandler 创建 C 端资产信息处理器
|
||
func NewClientAssetHandler(
|
||
assetService *asset.Service,
|
||
binding *customerBinding.Service,
|
||
assetWalletStore *postgres.AssetWalletStore,
|
||
packageStore *postgres.PackageStore,
|
||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||
iotCardStore *postgres.IotCardStore,
|
||
deviceStore *postgres.DeviceStore,
|
||
db *gorm.DB,
|
||
logger *zap.Logger,
|
||
) *ClientAssetHandler {
|
||
return &ClientAssetHandler{
|
||
assetService: assetService,
|
||
customerBinding: binding,
|
||
assetWalletStore: assetWalletStore,
|
||
packageStore: packageStore,
|
||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||
iotCardStore: iotCardStore,
|
||
deviceStore: deviceStore,
|
||
db: db,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
type resolvedAssetContext struct {
|
||
CustomerID uint
|
||
Identifier string
|
||
Asset *dto.AssetResolveResponse
|
||
Generation int
|
||
WalletBalance int64
|
||
SkipPermissionCtx context.Context
|
||
IsAgentChannel bool
|
||
SellerShopID uint
|
||
HasAddonMainPackage bool
|
||
}
|
||
|
||
// resolveAssetFromIdentifier 统一执行资产解析与归属校验
|
||
// 处理流程:客户鉴权 -> 标识符解析 -> 资产解析 -> 归属校验 -> 世代与钱包信息补齐
|
||
func (h *ClientAssetHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifier string) (*resolvedAssetContext, error) {
|
||
customerID, ok := middleware.GetCustomerID(c)
|
||
if !ok || customerID == 0 {
|
||
return nil, errors.New(errors.CodeUnauthorized)
|
||
}
|
||
|
||
identifier = strings.TrimSpace(identifier)
|
||
if identifier == "" {
|
||
identifier = strings.TrimSpace(c.Query("identifier"))
|
||
}
|
||
if identifier == "" {
|
||
var req dto.AssetRefreshRequest
|
||
if err := c.BodyParser(&req); err == nil {
|
||
identifier = strings.TrimSpace(req.Identifier)
|
||
}
|
||
}
|
||
if identifier == "" {
|
||
return nil, errors.New(errors.CodeInvalidParam)
|
||
}
|
||
|
||
skipPermissionCtx := context.WithValue(c.UserContext(), constants.ContextKeySubordinateShopIDs, []uint{})
|
||
assetInfo, err := h.assetService.Resolve(skipPermissionCtx, identifier)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
owned, ownErr := h.customerBinding.OwnsAsset(skipPermissionCtx, customerID, assetInfo.AssetType, assetInfo.AssetID)
|
||
if ownErr != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||
}
|
||
if !owned {
|
||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||
}
|
||
|
||
generation, genErr := h.getAssetGeneration(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
|
||
if genErr != nil {
|
||
return nil, genErr
|
||
}
|
||
|
||
walletBalance, walletErr := h.getAssetWalletBalance(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
|
||
if walletErr != nil {
|
||
return nil, walletErr
|
||
}
|
||
|
||
ctxInfo := &resolvedAssetContext{
|
||
CustomerID: customerID,
|
||
Identifier: identifier,
|
||
Asset: assetInfo,
|
||
Generation: generation,
|
||
WalletBalance: walletBalance,
|
||
SkipPermissionCtx: skipPermissionCtx,
|
||
}
|
||
|
||
if assetInfo.ShopID != nil && *assetInfo.ShopID > 0 {
|
||
ctxInfo.IsAgentChannel = true
|
||
ctxInfo.SellerShopID = *assetInfo.ShopID
|
||
}
|
||
|
||
return ctxInfo, nil
|
||
}
|
||
|
||
// GetAssetInfo B1 资产信息
|
||
// GET /api/c/v1/asset/info
|
||
func (h *ClientAssetHandler) GetAssetInfo(c *fiber.Ctx) error {
|
||
var req dto.AssetInfoRequest
|
||
if err := c.QueryParser(&req); err != nil {
|
||
return errors.New(errors.CodeInvalidParam)
|
||
}
|
||
|
||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
phone, _ := middleware.GetCustomerPhone(c)
|
||
|
||
resp := &dto.AssetInfoResponse{
|
||
BoundPhone: phone,
|
||
AssetType: resolved.Asset.AssetType,
|
||
AssetID: resolved.Asset.AssetID,
|
||
Identifier: resolved.Identifier,
|
||
VirtualNo: resolved.Asset.VirtualNo,
|
||
Status: resolved.Asset.Status,
|
||
StatusName: resolved.Asset.StatusName,
|
||
RealNameStatus: resolved.Asset.RealNameStatus,
|
||
RealNameStatusName: constants.GetRealNameStatusName(resolved.Asset.RealNameStatus),
|
||
CarrierName: resolved.Asset.CarrierName,
|
||
Generation: strconv.Itoa(resolved.Generation),
|
||
WalletBalance: resolved.WalletBalance,
|
||
ActivatedAt: resolved.Asset.ActivatedAt,
|
||
CurrentPackage: resolved.Asset.CurrentPackage,
|
||
CurrentPackageUsageID: resolved.Asset.CurrentPackageUsageID,
|
||
CurrentPackageActivatedAt: resolved.Asset.CurrentPackageActivatedAt,
|
||
CurrentPackageExpiresAt: resolved.Asset.CurrentPackageExpiresAt,
|
||
RealTotalMB: resolved.Asset.RealTotalMB,
|
||
RealUsedMB: resolved.Asset.RealUsedMB,
|
||
VirtualTotalMB: resolved.Asset.VirtualTotalMB,
|
||
VirtualUsedMB: resolved.Asset.VirtualUsedMB,
|
||
ReductionPct: resolved.Asset.ReductionPct,
|
||
EnableVirtualData: resolved.Asset.EnableVirtualData,
|
||
DeviceName: resolved.Asset.DeviceName,
|
||
IMEI: resolved.Asset.IMEI,
|
||
SN: resolved.Asset.SN,
|
||
DeviceModel: resolved.Asset.DeviceModel,
|
||
DeviceType: resolved.Asset.DeviceType,
|
||
Manufacturer: resolved.Asset.Manufacturer,
|
||
MaxSimSlots: resolved.Asset.MaxSimSlots,
|
||
BoundCardCount: resolved.Asset.BoundCardCount,
|
||
Cards: resolved.Asset.Cards,
|
||
DeviceProtectStatus: resolved.Asset.DeviceProtectStatus,
|
||
ICCID: resolved.Asset.ICCID,
|
||
MSISDN: resolved.Asset.MSISDN,
|
||
CarrierID: resolved.Asset.CarrierID,
|
||
CarrierType: resolved.Asset.CarrierType,
|
||
NetworkStatus: resolved.Asset.NetworkStatus,
|
||
NetworkStatusName: constants.GetNetworkStatusName(resolved.Asset.NetworkStatus),
|
||
GatewayExtend: resolved.Asset.GatewayExtend,
|
||
ActivationStatus: resolved.Asset.ActivationStatus,
|
||
ActivationStatusName: constants.GetActivationStatusName(resolved.Asset.ActivationStatus),
|
||
CardCategory: resolved.Asset.CardCategory,
|
||
BoundDeviceID: resolved.Asset.BoundDeviceID,
|
||
BoundDeviceNo: resolved.Asset.BoundDeviceNo,
|
||
BoundDeviceName: resolved.Asset.BoundDeviceName,
|
||
}
|
||
|
||
// 调用服务层获取设备实时 Gateway 数据(per D-11)
|
||
// Gateway 失败时 DeviceRealtime 仅含 gateway_msg,不阻断主流程(per D-06)
|
||
if resp.AssetType == "device" {
|
||
realtimeResp, realtimeErr := h.assetService.GetRealtimeStatus(
|
||
c.UserContext(), "device", resp.AssetID,
|
||
)
|
||
if realtimeErr == nil && realtimeResp.DeviceRealtime != nil {
|
||
resp.DeviceRealtime = mapDeviceGatewayInfoToClientInfo(realtimeResp.DeviceRealtime)
|
||
}
|
||
}
|
||
|
||
return response.Success(c, resp)
|
||
}
|
||
|
||
// GetAvailablePackages B2 资产可购套餐列表
|
||
// GET /api/c/v1/asset/packages
|
||
func (h *ClientAssetHandler) GetAvailablePackages(c *fiber.Ctx) error {
|
||
var req dto.AssetPackageListRequest
|
||
if err := c.QueryParser(&req); err != nil {
|
||
return errors.New(errors.CodeInvalidParam)
|
||
}
|
||
|
||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if resolved.Asset.SeriesID == nil || *resolved.Asset.SeriesID == 0 {
|
||
return errors.New(errors.CodeNoAvailablePackage, "当前无可购买套餐")
|
||
}
|
||
|
||
if _, err := packagepkg.FindAttachableMainPackageForAddon(
|
||
h.db.WithContext(resolved.SkipPermissionCtx),
|
||
resolved.Asset.AssetType,
|
||
resolved.Asset.AssetID,
|
||
time.Now(),
|
||
); err == nil {
|
||
resolved.HasAddonMainPackage = true
|
||
} else if err != gorm.ErrRecordNotFound {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
|
||
}
|
||
|
||
listCtx := resolved.SkipPermissionCtx
|
||
if resolved.IsAgentChannel {
|
||
listCtx = context.WithValue(listCtx, constants.ContextKeyUserType, constants.UserTypeAgent)
|
||
listCtx = context.WithValue(listCtx, constants.ContextKeyShopID, resolved.SellerShopID)
|
||
}
|
||
|
||
pkgs, _, err := h.packageStore.List(listCtx, &store.QueryOptions{
|
||
Page: 1,
|
||
PageSize: constants.MaxPageSize,
|
||
OrderBy: "id DESC",
|
||
}, map[string]any{
|
||
"series_id": *resolved.Asset.SeriesID,
|
||
"status": constants.StatusEnabled,
|
||
})
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询可购套餐失败")
|
||
}
|
||
|
||
allocationMap := make(map[uint]*model.ShopPackageAllocation)
|
||
if resolved.IsAgentChannel {
|
||
packageIDs := collectPackageIDs(pkgs)
|
||
allocations, allocErr := h.shopPackageAllocationStore.GetByShopAndPackages(
|
||
resolved.SkipPermissionCtx,
|
||
resolved.SellerShopID,
|
||
packageIDs,
|
||
)
|
||
if allocErr != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, allocErr, "查询套餐分配记录失败")
|
||
}
|
||
for _, allocation := range allocations {
|
||
allocationMap[allocation.PackageID] = allocation
|
||
}
|
||
}
|
||
|
||
items := make([]dto.ClientPackageItem, 0, len(pkgs))
|
||
for _, pkg := range pkgs {
|
||
item, ok := buildClientPackageItem(pkg, resolved, allocationMap)
|
||
if !ok {
|
||
continue
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
|
||
if len(items) == 0 {
|
||
return errors.New(errors.CodeNoAvailablePackage, "当前无可购买套餐")
|
||
}
|
||
|
||
sort.Slice(items, func(i, j int) bool {
|
||
return items[i].RetailPrice < items[j].RetailPrice
|
||
})
|
||
|
||
return response.Success(c, &dto.AssetPackageListResponse{Packages: items})
|
||
}
|
||
|
||
// GetPackageHistory B3 资产套餐历史
|
||
// GET /api/c/v1/asset/package-history
|
||
func (h *ClientAssetHandler) GetPackageHistory(c *fiber.Ctx) error {
|
||
var req dto.AssetPackageHistoryRequest
|
||
if err := c.QueryParser(&req); err != nil {
|
||
return errors.New(errors.CodeInvalidParam)
|
||
}
|
||
|
||
if req.Page < 1 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize < 1 {
|
||
req.PageSize = constants.DefaultPageSize
|
||
}
|
||
if req.PageSize > constants.MaxPageSize {
|
||
req.PageSize = constants.MaxPageSize
|
||
}
|
||
|
||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
query := h.db.WithContext(resolved.SkipPermissionCtx).Model(&model.PackageUsage{}).
|
||
Where("generation = ?", resolved.Generation)
|
||
if resolved.Asset.AssetType == "card" {
|
||
query = query.Where("iot_card_id = ?", resolved.Asset.AssetID)
|
||
} else {
|
||
query = query.Where("device_id = ?", resolved.Asset.AssetID)
|
||
}
|
||
if req.Status != nil {
|
||
query = query.Where("status = ?", *req.Status)
|
||
}
|
||
if req.PackageType != nil {
|
||
query = query.Where("package_id IN (?)",
|
||
h.db.Model(&model.Package{}).Select("id").Where("package_type = ?", *req.PackageType))
|
||
}
|
||
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐历史总数失败")
|
||
}
|
||
|
||
var usages []*model.PackageUsage
|
||
offset := (req.Page - 1) * req.PageSize
|
||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&usages).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐历史失败")
|
||
}
|
||
|
||
packageMap, err := h.loadPackageMap(resolved.SkipPermissionCtx, usages)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
list := make([]dto.AssetPackageResponse, 0, len(usages))
|
||
for _, usage := range usages {
|
||
pkg := packageMap[usage.PackageID]
|
||
metrics := usage.BuildTrafficMetrics()
|
||
pkgName := usage.PackageName
|
||
pkgType := ""
|
||
if pkg != nil {
|
||
if pkgName == "" {
|
||
pkgName = pkg.PackageName
|
||
}
|
||
pkgType = pkg.PackageType
|
||
}
|
||
|
||
list = append(list, dto.AssetPackageResponse{
|
||
PackageUsageID: usage.ID,
|
||
PackageID: usage.PackageID,
|
||
PackageName: pkgName,
|
||
PackageType: pkgType,
|
||
UsageType: usage.UsageType,
|
||
Status: usage.Status,
|
||
StatusName: packageStatusName(usage.Status),
|
||
RealTotalMB: metrics.RealTotalMB,
|
||
RealUsedMB: metrics.RealUsedMB,
|
||
VirtualTotalMB: metrics.VirtualTotalMB,
|
||
VirtualUsedMB: metrics.VirtualUsedMB,
|
||
ReductionPct: metrics.ReductionPct,
|
||
EnableVirtualData: usage.EnableVirtualDataSnapshot,
|
||
ActivatedAt: usage.ActivatedAt,
|
||
ExpiresAt: usage.ExpiresAt,
|
||
MasterUsageID: usage.MasterUsageID,
|
||
Priority: usage.Priority,
|
||
CreatedAt: usage.CreatedAt,
|
||
})
|
||
}
|
||
|
||
return response.SuccessWithPagination(c, list, total, req.Page, req.PageSize)
|
||
}
|
||
|
||
// RefreshAsset B4 资产刷新
|
||
// POST /api/c/v1/asset/refresh
|
||
func (h *ClientAssetHandler) RefreshAsset(c *fiber.Ctx) error {
|
||
var req dto.AssetRefreshRequest
|
||
if err := c.BodyParser(&req); err != nil {
|
||
return errors.New(errors.CodeInvalidParam)
|
||
}
|
||
|
||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if _, err := h.assetService.Refresh(
|
||
resolved.SkipPermissionCtx,
|
||
resolved.Asset.AssetType,
|
||
resolved.Asset.AssetID,
|
||
); err != nil {
|
||
return err
|
||
}
|
||
|
||
resp := &dto.AssetRefreshResponse{
|
||
RefreshType: resolved.Asset.AssetType,
|
||
Accepted: true,
|
||
CooldownSeconds: 0,
|
||
}
|
||
if resolved.Asset.AssetType == constants.ResourceTypeDevice {
|
||
resp.CooldownSeconds = int(constants.DeviceRefreshCooldownDuration / time.Second)
|
||
}
|
||
|
||
return response.Success(c, resp)
|
||
}
|
||
|
||
|
||
func (h *ClientAssetHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
||
switch assetType {
|
||
case "card":
|
||
card, err := h.iotCardStore.GetByID(ctx, assetID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return 0, errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败")
|
||
}
|
||
return card.Generation, nil
|
||
case "device":
|
||
device, err := h.deviceStore.GetByID(ctx, assetID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return 0, errors.New(errors.CodeAssetNotFound)
|
||
}
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败")
|
||
}
|
||
return device.Generation, nil
|
||
default:
|
||
return 0, errors.New(errors.CodeInvalidParam)
|
||
}
|
||
}
|
||
|
||
func (h *ClientAssetHandler) getAssetWalletBalance(ctx context.Context, assetType string, assetID uint) (int64, error) {
|
||
resourceType := constants.AssetWalletResourceTypeIotCard
|
||
if assetType == constants.ResourceTypeDevice {
|
||
resourceType = constants.AssetWalletResourceTypeDevice
|
||
}
|
||
|
||
wallet, err := h.assetWalletStore.GetByResourceTypeAndID(ctx, resourceType, assetID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return 0, nil
|
||
}
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败")
|
||
}
|
||
|
||
return wallet.Balance, nil
|
||
}
|
||
|
||
func (h *ClientAssetHandler) loadPackageMap(ctx context.Context, usages []*model.PackageUsage) (map[uint]*model.Package, error) {
|
||
ids := make([]uint, 0, len(usages))
|
||
seen := make(map[uint]struct{}, len(usages))
|
||
for _, usage := range usages {
|
||
if usage == nil {
|
||
continue
|
||
}
|
||
if _, ok := seen[usage.PackageID]; ok {
|
||
continue
|
||
}
|
||
seen[usage.PackageID] = struct{}{}
|
||
ids = append(ids, usage.PackageID)
|
||
}
|
||
|
||
packages, err := h.packageStore.GetByIDsUnscoped(ctx, ids)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||
}
|
||
|
||
result := make(map[uint]*model.Package, len(packages))
|
||
for _, pkg := range packages {
|
||
result[pkg.ID] = pkg
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
func collectPackageIDs(pkgs []*model.Package) []uint {
|
||
ids := make([]uint, 0, len(pkgs))
|
||
for _, pkg := range pkgs {
|
||
if pkg == nil {
|
||
continue
|
||
}
|
||
ids = append(ids, pkg.ID)
|
||
}
|
||
return ids
|
||
}
|
||
|
||
func buildClientPackageItem(
|
||
pkg *model.Package,
|
||
resolved *resolvedAssetContext,
|
||
allocationMap map[uint]*model.ShopPackageAllocation,
|
||
) (dto.ClientPackageItem, bool) {
|
||
if pkg == nil || pkg.Status != constants.StatusEnabled {
|
||
return dto.ClientPackageItem{}, false
|
||
}
|
||
if pkg.IsGift {
|
||
return dto.ClientPackageItem{}, false
|
||
}
|
||
|
||
isAddon := pkg.PackageType == constants.PackageTypeAddon
|
||
if isAddon && !resolved.HasAddonMainPackage {
|
||
return dto.ClientPackageItem{}, false
|
||
}
|
||
|
||
retailPrice := packageprice.PackageEffectiveRetailPrice(pkg)
|
||
costPrice := pkg.CostPrice
|
||
|
||
if resolved.IsAgentChannel {
|
||
allocation, ok := allocationMap[pkg.ID]
|
||
if !ok || allocation == nil {
|
||
return dto.ClientPackageItem{}, false
|
||
}
|
||
if allocation.ShelfStatus != constants.ShelfStatusOn || allocation.Status != constants.StatusEnabled {
|
||
return dto.ClientPackageItem{}, false
|
||
}
|
||
retailPrice = packageprice.AllocationEffectiveRetailPrice(allocation)
|
||
costPrice = allocation.CostPrice
|
||
} else if pkg.ShelfStatus != constants.ShelfStatusOn {
|
||
return dto.ClientPackageItem{}, false
|
||
}
|
||
|
||
if retailPrice < costPrice {
|
||
return dto.ClientPackageItem{}, false
|
||
}
|
||
|
||
validityDays := packagepkg.CalculateValidityDays(pkg.CalendarType, time.Now(), pkg.DurationMonths, pkg.DurationDays)
|
||
|
||
dataAllowance := pkg.RealDataMB
|
||
if dataAllowance <= 0 {
|
||
dataAllowance = pkg.RealDataMB
|
||
}
|
||
|
||
return dto.ClientPackageItem{
|
||
PackageID: pkg.ID,
|
||
PackageName: pkg.PackageName,
|
||
PackageType: pkg.PackageType,
|
||
RetailPrice: retailPrice,
|
||
CostPrice: costPrice,
|
||
ValidityDays: validityDays,
|
||
IsAddon: isAddon,
|
||
DataAllowance: dataAllowance,
|
||
DataUnit: "MB",
|
||
Description: pkg.PackageCode,
|
||
}, true
|
||
}
|
||
|
||
func nonZeroTimePtr(t time.Time) *time.Time {
|
||
if t.IsZero() {
|
||
return nil
|
||
}
|
||
return &t
|
||
}
|
||
|
||
func packageStatusName(status int) string {
|
||
switch status {
|
||
case constants.PackageUsageStatusPending:
|
||
return "待生效"
|
||
case constants.PackageUsageStatusActive:
|
||
return "生效中"
|
||
case constants.PackageUsageStatusDepleted:
|
||
return "已用完"
|
||
case constants.PackageUsageStatusExpired:
|
||
return "已过期"
|
||
case constants.PackageUsageStatusInvalidated:
|
||
return "已失效"
|
||
default:
|
||
return "未知"
|
||
}
|
||
}
|
||
|
||
// mapDeviceGatewayInfoToClientInfo 将 B 端 DeviceGatewayInfo 映射为 C 端 DeviceRealtimeInfo
|
||
// B 端和 C 端结构独立,通过映射函数转换(per D-08)
|
||
func mapDeviceGatewayInfoToClientInfo(g *dto.DeviceGatewayInfo) *dto.DeviceRealtimeInfo {
|
||
if g == nil {
|
||
return nil
|
||
}
|
||
info := &dto.DeviceRealtimeInfo{}
|
||
if g.OnlineStatus != nil {
|
||
v := int64(*g.OnlineStatus)
|
||
info.OnlineStatus = &v
|
||
}
|
||
if g.BatteryLevel != nil {
|
||
v := int64(*g.BatteryLevel)
|
||
info.BatteryLevel = &v
|
||
}
|
||
if g.Status != nil {
|
||
v := int64(*g.Status)
|
||
info.Status = &v
|
||
}
|
||
info.RunTime = g.RunTime
|
||
info.ConnectTime = g.ConnectTime
|
||
info.LastOnlineTime = g.LastOnlineTime
|
||
info.LastUpdateTime = g.LastUpdateTime
|
||
if g.Rsrp != nil {
|
||
v := int64(*g.Rsrp)
|
||
info.Rsrp = &v
|
||
}
|
||
if g.Rsrq != nil {
|
||
v := int64(*g.Rsrq)
|
||
info.Rsrq = &v
|
||
}
|
||
info.Rssi = g.Rssi
|
||
if g.Sinr != nil {
|
||
v := int64(*g.Sinr)
|
||
info.Sinr = &v
|
||
}
|
||
info.SignalQuality = g.SignalQuality
|
||
info.SignalBadReason = g.SignalBadReason
|
||
info.SSID = g.SSID
|
||
info.WifiEnabled = g.WifiEnabled
|
||
info.WifiPassword = g.WifiPassword
|
||
info.IPAddress = g.IPAddress
|
||
info.WANIP = g.WANIP
|
||
info.LANIP = g.LANIP
|
||
info.MACAddress = g.MACAddress
|
||
info.DailyUsage = g.DailyUsage
|
||
info.DLStats = g.DLStats
|
||
info.ULStats = g.ULStats
|
||
if g.LimitSpeed != nil {
|
||
v := int64(*g.LimitSpeed)
|
||
info.LimitSpeed = &v
|
||
}
|
||
info.CurrentIccid = g.CurrentIccid
|
||
if g.MaxClients != nil {
|
||
v := int64(*g.MaxClients)
|
||
info.MaxClients = &v
|
||
}
|
||
if g.ClientNumber != nil {
|
||
v := int64(*g.ClientNumber)
|
||
info.ClientNumber = &v
|
||
}
|
||
info.SoftwareVersion = g.SoftwareVersion
|
||
if g.SwitchMode != nil {
|
||
// SwitchMode 在 B 端为字符串("0"/"1"),C 端为整型(0/1),需解析转换
|
||
if n, convErr := strconv.Atoi(*g.SwitchMode); convErr == nil {
|
||
info.SwitchMode = &n
|
||
}
|
||
}
|
||
if g.SyncInterval != nil {
|
||
v := int64(*g.SyncInterval)
|
||
info.SyncInterval = &v
|
||
}
|
||
info.DeviceID = g.DeviceID
|
||
info.DeviceName = g.DeviceName
|
||
info.DeviceType = g.DeviceType
|
||
info.Imei = g.IMEI
|
||
info.Imsi = g.IMSI
|
||
info.GatewayMsg = g.GatewayMsg
|
||
return info
|
||
}
|