Files
junhong_cmp_fiber/internal/service/agent_open_api/service.go
break b3fb8c7a82
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
资产详情新增两个字段
2026-07-02 16:49:50 +09:00

688 lines
24 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 agent_open_api
import (
"context"
"fmt"
"math/rand"
"strings"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset"
deviceSvc "github.com/break/junhong_cmp_fiber/internal/service/device"
iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
orderSvc "github.com/break/junhong_cmp_fiber/internal/service/order"
packageSvc "github.com/break/junhong_cmp_fiber/internal/service/package"
shopCommissionSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_commission"
"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/middleware"
"gorm.io/gorm"
)
// Service 代理开放接口业务编排服务
type Service struct {
assetService *assetSvc.Service
packageService *packageSvc.Service
orderService *orderSvc.Service
shopCommissionService *shopCommissionSvc.Service
stopResumeService *iotCardSvc.StopResumeService
deviceService *deviceSvc.Service
iotCardStore *postgres.IotCardStore
packageUsageStore *postgres.PackageUsageStore
packageStore *postgres.PackageStore
packageSeriesStore *postgres.PackageSeriesStore
agentWalletStore *postgres.AgentWalletStore
deviceSimBindingStore *postgres.DeviceSimBindingStore
deviceStore *postgres.DeviceStore
}
// New 创建代理开放接口业务编排服务
func New(
assetService *assetSvc.Service,
packageService *packageSvc.Service,
orderService *orderSvc.Service,
shopCommissionService *shopCommissionSvc.Service,
stopResumeService *iotCardSvc.StopResumeService,
deviceService *deviceSvc.Service,
iotCardStore *postgres.IotCardStore,
packageUsageStore *postgres.PackageUsageStore,
packageStore *postgres.PackageStore,
packageSeriesStore *postgres.PackageSeriesStore,
agentWalletStore *postgres.AgentWalletStore,
deviceSimBindingStore *postgres.DeviceSimBindingStore,
deviceStore *postgres.DeviceStore,
) *Service {
return &Service{
assetService: assetService,
packageService: packageService,
orderService: orderService,
shopCommissionService: shopCommissionService,
stopResumeService: stopResumeService,
deviceService: deviceService,
iotCardStore: iotCardStore,
packageUsageStore: packageUsageStore,
packageStore: packageStore,
packageSeriesStore: packageSeriesStore,
agentWalletStore: agentWalletStore,
deviceSimBindingStore: deviceSimBindingStore,
deviceStore: deviceStore,
}
}
// GetCardTraffic 查询开放接口流量
// 支持三种解析路径:
// 1. card_no 解析为独立卡 → 查卡维度流量
// 2. card_no 解析为已绑定设备的卡 → 反查绑定设备 → 查设备维度流量
// 3. card_no 无法解析为卡 → 尝试作为设备标识IMEI/虚拟号)→ 查设备维度流量
func (s *Service) GetCardTraffic(ctx context.Context, req *dto.AgentOpenAPICardQueryRequest) (*dto.AgentOpenAPICardTrafficResponse, error) {
card, cardErr := s.resolveOpenAPICard(ctx, req.CardNo)
if cardErr == nil {
// 独立卡:直接查卡维度流量
if card.IsStandalone {
return s.buildCardTrafficResponse(ctx, req.CardNo, "iot_card", card.ID, "")
}
// 已绑定设备的卡:反查设备后查设备维度流量
binding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡绑定设备失败")
}
device, err := s.deviceStore.GetByID(ctx, binding.DeviceID)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询绑定设备信息失败")
}
return s.buildCardTrafficResponse(ctx, req.CardNo, "device", device.ID, device.VirtualNo)
}
// 兜底:尝试解析为设备标识(支持 IMEI/虚拟号)
device, err := s.resolveOpenAPIDevice(ctx, req.CardNo)
if err != nil {
// 两种解析都失败,返回原始卡解析错误(语义更贴近入参)
return nil, cardErr
}
return s.buildCardTrafficResponse(ctx, req.CardNo, "device", device.ID, device.VirtualNo)
}
// buildCardTrafficResponse 统一构造卡流量响应,支持卡和设备两种载体维度
func (s *Service) buildCardTrafficResponse(ctx context.Context, cardNo, carrierType string, carrierID uint, deviceNo string) (*dto.AgentOpenAPICardTrafficResponse, error) {
activeStatus := constants.PackageUsageStatusActive
depletedStatus := constants.PackageUsageStatusDepleted
pendingStatus := constants.PackageUsageStatusPending
activeUsages, err := s.packageUsageStore.ListByCarrier(ctx, carrierType, carrierID, &activeStatus)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询生效套餐失败")
}
// 已用完的套餐仍在有效期内,与生效中套餐一并展示
depletedUsages, err := s.packageUsageStore.ListByCarrier(ctx, carrierType, carrierID, &depletedStatus)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询已用完套餐失败")
}
activeUsages = append(activeUsages, depletedUsages...)
pendingUsages, err := s.packageUsageStore.ListByCarrier(ctx, carrierType, carrierID, &pendingStatus)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询待生效套餐失败")
}
packageMap, seriesMap, err := s.loadUsagePackageContext(ctx, append(activeUsages, pendingUsages...))
if err != nil {
return nil, err
}
resp := &dto.AgentOpenAPICardTrafficResponse{
CardNo: cardNo,
DeviceNo: deviceNo,
ActivePackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(activeUsages)),
PendingPackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(pendingUsages)),
}
for _, usage := range activeUsages {
item := s.buildTrafficItem(usage, packageMap, seriesMap, true)
resp.ActivePackages = append(resp.ActivePackages, item)
resp.ActiveTotalFlowMB += item.TotalFlowMB
resp.ActiveUsedFlowMB += item.UsedFlowMB
resp.ActiveRemainingFlowMB += item.RemainingFlowMB
if item.ExpiresAt != nil && (resp.ActiveExpiresAt == nil || item.ExpiresAt.After(*resp.ActiveExpiresAt)) {
resp.ActiveExpiresAt = item.ExpiresAt
}
}
for _, usage := range pendingUsages {
resp.PendingPackages = append(resp.PendingPackages, s.buildTrafficItem(usage, packageMap, seriesMap, false))
}
return resp, nil
}
// GetCardStatus 查询开放接口单卡状态
func (s *Service) GetCardStatus(ctx context.Context, req *dto.AgentOpenAPICardQueryRequest) (*dto.AgentOpenAPICardStatusResponse, error) {
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
if err != nil {
return nil, err
}
cardStatus := constants.AgentOpenAPICardStatusStopped
stopReason := card.StopReason
if card.NetworkStatus == constants.NetworkStatusOnline {
cardStatus = constants.AgentOpenAPICardStatusNormal
stopReason = ""
}
return &dto.AgentOpenAPICardStatusResponse{
CardNo: req.CardNo,
CardStatus: cardStatus,
CardStatusName: constants.AgentOpenAPICardStatusName(cardStatus),
GatewayExtend: card.GatewayExtend,
StopReason: stopReason,
StopReasonName: constants.AgentOpenAPIStopReasonName(stopReason),
}, nil
}
// GetRealnameStatus 查询开放接口单卡实名状态
func (s *Service) GetRealnameStatus(ctx context.Context, req *dto.AgentOpenAPICardQueryRequest) (*dto.AgentOpenAPIRealnameStatusResponse, error) {
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
if err != nil {
return nil, err
}
return &dto.AgentOpenAPIRealnameStatusResponse{
CardNo: req.CardNo,
IsRealnamed: card.RealNameStatus == constants.RealNameStatusVerified,
}, nil
}
// ResumeCard 调用上游复机机卡分离停机卡
func (s *Service) ResumeCard(ctx context.Context, req *dto.AgentOpenAPICardResumeRequest) (*dto.AgentOpenAPICardResumeResponse, error) {
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
if err != nil {
return nil, err
}
if s.stopResumeService == nil {
return nil, errors.New(errors.CodeInternalError, "开放接口复机依赖未配置")
}
if err := s.stopResumeService.StartMachineSeparatedCard(ctx, card); err != nil {
return nil, err
}
return &dto.AgentOpenAPICardResumeResponse{
CardNo: req.CardNo,
CardStatus: constants.AgentOpenAPICardStatusNormal,
CardStatusName: constants.AgentOpenAPICardStatusName(constants.AgentOpenAPICardStatusNormal),
GatewayExtend: "",
}, nil
}
// ListPackages 查询开放接口代理可购买套餐列表
func (s *Service) ListPackages(ctx context.Context, req *dto.AgentOpenAPIPackageListRequest) ([]*dto.AgentOpenAPIPackageItem, int64, int, int, error) {
status := constants.StatusEnabled
shelfStatus := constants.ShelfStatusOn
packageReq := &dto.PackageListRequest{
Page: req.Page,
PageSize: req.PageSize,
PackageName: req.PackageName,
SeriesID: req.SeriesID,
Status: &status,
ShelfStatus: &shelfStatus,
PackageType: req.PackageType,
}
packages, total, err := s.packageService.List(ctx, packageReq)
if err != nil {
return nil, 0, 0, 0, err
}
items := make([]*dto.AgentOpenAPIPackageItem, 0, len(packages))
for _, pkg := range packages {
items = append(items, &dto.AgentOpenAPIPackageItem{
PackageID: pkg.ID,
PackageCode: pkg.PackageCode,
PackageName: pkg.PackageName,
SeriesName: stringValue(pkg.SeriesName),
PackageType: pkg.PackageType,
PackageTypeName: constants.AgentOpenAPIPackageTypeName(pkg.PackageType),
CostPrice: pkg.CostPrice,
RetailPrice: int64Value(pkg.EffectiveRetailPrice),
TotalFlowMB: pkg.RealDataMB,
ValidDays: packageValidDays(pkg.DurationDays, pkg.DurationMonths),
})
}
page, pageSize := normalizePage(req.Page, req.PageSize)
return items, total, page, pageSize, nil
}
// GetWalletBalance 查询开放接口预充值主钱包余额
func (s *Service) GetWalletBalance(ctx context.Context) (*dto.AgentOpenAPIWalletBalanceResponse, error) {
shopID := middleware.GetShopIDFromContext(ctx)
if shopID == 0 {
return nil, errors.New(errors.CodeUnauthorized)
}
wallet, err := s.agentWalletStore.GetMainWallet(ctx, shopID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return &dto.AgentOpenAPIWalletBalanceResponse{
Balance: 0,
FrozenBalance: 0,
AvailableBalance: 0,
Currency: constants.AgentOpenAPIWalletCurrencyCNY,
}, nil
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询主钱包余额失败")
}
return &dto.AgentOpenAPIWalletBalanceResponse{
Balance: wallet.Balance,
FrozenBalance: wallet.FrozenBalance,
AvailableBalance: wallet.GetAvailableBalance(),
Currency: walletCurrency(wallet.Currency),
}, nil
}
// ListWalletTransactions 查询开放接口预充值主钱包流水
func (s *Service) ListWalletTransactions(ctx context.Context, req *dto.AgentOpenAPIWalletTransactionListRequest) (*dto.MainWalletTransactionListResponse, error) {
shopID := middleware.GetShopIDFromContext(ctx)
if shopID == 0 {
return nil, errors.New(errors.CodeUnauthorized)
}
return s.shopCommissionService.ListMainWalletTransactions(ctx, shopID, &dto.MainWalletTransactionListRequest{
Page: req.Page,
PageSize: req.PageSize,
TransactionType: req.TransactionType,
StartDate: req.StartDate,
EndDate: req.EndDate,
})
}
// CreateWalletPackageOrders 使用代理预充值钱包按卡购买套餐
func (s *Service) CreateWalletPackageOrders(ctx context.Context, req *dto.AgentOpenAPIWalletPackageOrderRequest) (*dto.AgentOpenAPIWalletPackageOrderResponse, error) {
shopID := middleware.GetShopIDFromContext(ctx)
if shopID == 0 {
return nil, errors.New(errors.CodeUnauthorized)
}
pkg, err := s.packageStore.GetByCode(ctx, strings.TrimSpace(req.PackageCode))
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "套餐不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询套餐失败")
}
resp := &dto.AgentOpenAPIWalletPackageOrderResponse{
BatchNo: generateAgentOpenAPIBatchNo(),
PackageCode: pkg.PackageCode,
Orders: []dto.AgentOpenAPIWalletPackageOrderItem{},
FailedCards: []dto.AgentOpenAPIWalletPackageOrderFailure{},
}
for _, rawCardNo := range req.CardNos {
cardNo := strings.TrimSpace(rawCardNo)
if cardNo == "" {
resp.FailedCards = append(resp.FailedCards, buildFailedCard(rawCardNo, errors.New(errors.CodeInvalidParam, "卡标识不能为空")))
continue
}
var identifier string
card, cardErr := s.resolveOpenAPICard(ctx, cardNo)
if cardErr != nil {
// 兜底:尝试解析为设备标识(支持 IMEI/虚拟号)
device, devErr := s.resolveOpenAPIDevice(ctx, cardNo)
if devErr != nil {
resp.FailedCards = append(resp.FailedCards, buildFailedCard(cardNo, cardErr))
continue
}
identifier = device.VirtualNo
} else if !card.IsStandalone {
// 卡已绑定设备时,自动转为设备维度购买
binding, bindErr := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
if bindErr != nil {
resp.FailedCards = append(resp.FailedCards, buildFailedCard(cardNo, errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定设备失败")))
continue
}
device, devErr := s.deviceStore.GetByID(ctx, binding.DeviceID)
if devErr != nil {
resp.FailedCards = append(resp.FailedCards, buildFailedCard(cardNo, errors.Wrap(errors.CodeInternalError, devErr, "查询绑定设备信息失败")))
continue
}
identifier = device.VirtualNo
} else {
identifier = card.ICCID
}
orderResp, err := s.orderService.CreateAdminOrder(ctx, &dto.CreateAdminOrderRequest{
Identifier: identifier,
PackageIDs: []uint{pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}, model.BuyerTypeAgent, shopID)
if err != nil {
resp.FailedCards = append(resp.FailedCards, buildFailedCard(cardNo, err))
continue
}
resp.Orders = append(resp.Orders, dto.AgentOpenAPIWalletPackageOrderItem{
CardNo: cardNo,
OrderNo: orderResp.OrderNo,
})
}
resp.SuccessCount = len(resp.Orders)
resp.FailedCount = len(resp.FailedCards)
return resp, nil
}
// resolveOpenAPIDevice 将开放接口设备标识解析为当前代理可见的设备
// 设备不存在或不属于代理管辖店铺时统一返回 CodeForbidden避免信息泄露
func (s *Service) resolveOpenAPIDevice(ctx context.Context, deviceNo string) (*model.Device, error) {
identifier := strings.TrimSpace(deviceNo)
if identifier == "" {
return nil, errors.New(errors.CodeInvalidParam)
}
if s.deviceService == nil {
return nil, errors.New(errors.CodeInternalError, "开放接口设备解析依赖未配置")
}
device, err := s.deviceService.GetDeviceByIdentifier(ctx, identifier)
if err != nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
if device.ShopID == nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
subordinateIDs := middleware.GetSubordinateShopIDs(ctx)
found := false
for _, id := range subordinateIDs {
if id == *device.ShopID {
found = true
break
}
}
if !found {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return device, nil
}
// GetDeviceTraffic 查询开放接口设备套餐内流量
func (s *Service) GetDeviceTraffic(ctx context.Context, req *dto.AgentOpenAPIDeviceQueryRequest) (*dto.AgentOpenAPIDeviceTrafficResponse, error) {
device, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo)
if err != nil {
return nil, err
}
activeStatus := constants.PackageUsageStatusActive
pendingStatus := constants.PackageUsageStatusPending
activeUsages, err := s.packageUsageStore.ListByCarrier(ctx, "device", device.ID, &activeStatus)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询生效套餐失败")
}
pendingUsages, err := s.packageUsageStore.ListByCarrier(ctx, "device", device.ID, &pendingStatus)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询待生效套餐失败")
}
packageMap, seriesMap, err := s.loadUsagePackageContext(ctx, append(activeUsages, pendingUsages...))
if err != nil {
return nil, err
}
resp := &dto.AgentOpenAPIDeviceTrafficResponse{
DeviceNo: req.DeviceNo,
ActivePackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(activeUsages)),
PendingPackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(pendingUsages)),
}
for _, usage := range activeUsages {
item := s.buildTrafficItem(usage, packageMap, seriesMap, true)
resp.ActivePackages = append(resp.ActivePackages, item)
resp.ActiveTotalFlowMB += item.TotalFlowMB
resp.ActiveUsedFlowMB += item.UsedFlowMB
resp.ActiveRemainingFlowMB += item.RemainingFlowMB
if item.ExpiresAt != nil && (resp.ActiveExpiresAt == nil || item.ExpiresAt.After(*resp.ActiveExpiresAt)) {
resp.ActiveExpiresAt = item.ExpiresAt
}
}
for _, usage := range pendingUsages {
resp.PendingPackages = append(resp.PendingPackages, s.buildTrafficItem(usage, packageMap, seriesMap, false))
}
return resp, nil
}
// SwitchDeviceCard 开放接口切网(多卡设备切换到指定 ICCID
func (s *Service) SwitchDeviceCard(ctx context.Context, req *dto.AgentOpenAPIDeviceSwitchCardRequest) error {
if _, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo); err != nil {
return err
}
return s.deviceService.GatewaySwitchCard(ctx, req.DeviceNo, &dto.SwitchCardRequest{
TargetICCID: req.ICCID,
})
}
// RebootDevice 开放接口重启设备
func (s *Service) RebootDevice(ctx context.Context, req *dto.AgentOpenAPIDeviceOperationRequest) error {
if _, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo); err != nil {
return err
}
return s.deviceService.GatewayRebootDevice(ctx, req.DeviceNo)
}
// ResetDevice 开放接口恢复出厂设置
func (s *Service) ResetDevice(ctx context.Context, req *dto.AgentOpenAPIDeviceOperationRequest) error {
if _, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo); err != nil {
return err
}
return s.deviceService.GatewayResetDevice(ctx, req.DeviceNo)
}
// resolveOpenAPICard 将开放接口卡标识解析为当前代理可见的 IoT 卡
func (s *Service) resolveOpenAPICard(ctx context.Context, cardNo string) (*model.IotCard, error) {
identifier := strings.TrimSpace(cardNo)
if identifier == "" {
return nil, errors.New(errors.CodeInvalidParam)
}
if s.assetService == nil || s.iotCardStore == nil {
return nil, errors.New(errors.CodeInternalError, "开放接口资产解析依赖未配置")
}
resolved, err := s.assetService.Resolve(ctx, identifier, false)
if err != nil {
if appErr, ok := err.(*errors.AppError); ok && appErr.Code == errors.CodeNotFound {
return s.resolveOpenAPICardByIdentifier(ctx, identifier)
}
return nil, err
}
if resolved.AssetType != "card" {
return nil, errors.New(errors.CodeInvalidParam, "开放接口只支持单卡标识")
}
card, err := s.iotCardStore.GetByID(ctx, resolved.AssetID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
}
return card, nil
}
// resolveOpenAPICardByIdentifier 兜底支持 ICCID 双列、虚拟号和 MSISDN 精确解析
func (s *Service) resolveOpenAPICardByIdentifier(ctx context.Context, identifier string) (*model.IotCard, error) {
card, err := s.iotCardStore.GetByIdentifier(ctx, identifier)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
}
return card, nil
}
// loadUsagePackageContext 批量加载套餐和系列信息,避免单卡流量响应出现 N+1 查询
func (s *Service) loadUsagePackageContext(ctx context.Context, usages []*model.PackageUsage) (map[uint]*model.Package, map[uint]string, error) {
packageIDs := make([]uint, 0)
packageIDSeen := make(map[uint]struct{})
for _, usage := range usages {
if usage.PackageID == 0 {
continue
}
if _, ok := packageIDSeen[usage.PackageID]; !ok {
packageIDSeen[usage.PackageID] = struct{}{}
packageIDs = append(packageIDs, usage.PackageID)
}
}
packages, err := s.packageStore.GetByIDsUnscoped(ctx, packageIDs)
if err != nil {
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "批量查询套餐失败")
}
packageMap := make(map[uint]*model.Package, len(packages))
seriesIDSeen := make(map[uint]struct{})
seriesIDs := make([]uint, 0)
for _, pkg := range packages {
packageMap[pkg.ID] = pkg
if pkg.SeriesID > 0 {
if _, ok := seriesIDSeen[pkg.SeriesID]; !ok {
seriesIDSeen[pkg.SeriesID] = struct{}{}
seriesIDs = append(seriesIDs, pkg.SeriesID)
}
}
}
seriesList, err := s.packageSeriesStore.GetByIDs(ctx, seriesIDs)
if err != nil {
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "批量查询套餐系列失败")
}
seriesMap := make(map[uint]string, len(seriesList))
for _, series := range seriesList {
seriesMap[series.ID] = series.SeriesName
}
return packageMap, seriesMap, nil
}
// buildTrafficItem 按开放接口口径构造套餐流量项,主动隐藏虚流量内部字段
func (s *Service) buildTrafficItem(usage *model.PackageUsage, packageMap map[uint]*model.Package, seriesMap map[uint]string, active bool) dto.AgentOpenAPIPackageTrafficItem {
pkg := packageMap[usage.PackageID]
packageCode := ""
packageName := usage.PackageName
packageType := ""
validDays := 0
seriesName := ""
if pkg != nil {
packageCode = pkg.PackageCode
if packageName == "" {
packageName = pkg.PackageName
}
packageType = pkg.PackageType
validDays = packageValidDays(nil, pkg.DurationMonths)
if pkg.DurationDays > 0 {
validDays = pkg.DurationDays
}
seriesName = seriesMap[pkg.SeriesID]
}
usedFlow := int64(0)
remainingFlow := int64(0)
if active {
usedFlow = visibleUsedFlowMB(usage)
remainingFlow = usage.DataLimitMB - usedFlow
if remainingFlow < 0 {
remainingFlow = 0
}
}
return dto.AgentOpenAPIPackageTrafficItem{
PackageCode: packageCode,
PackageName: packageName,
SeriesName: seriesName,
PackageType: packageType,
PackageTypeName: constants.AgentOpenAPIPackageTypeName(packageType),
TotalFlowMB: usage.DataLimitMB,
UsedFlowMB: usedFlow,
RemainingFlowMB: remainingFlow,
StartAt: usage.ActivatedAt,
ExpiresAt: usage.ExpiresAt,
ValidDays: validDays,
Priority: usage.Priority,
}
}
// visibleUsedFlowMB 计算对代理展示的已用流量,并限制不超过真实总量
func visibleUsedFlowMB(usage *model.PackageUsage) int64 {
ratio := usage.DisplayGainRatioSnapshot
if ratio <= 0 {
ratio = 1
}
used := int64(float64(usage.DataUsageMB) * ratio)
if used > usage.DataLimitMB {
return usage.DataLimitMB
}
if used < 0 {
return 0
}
return used
}
// packageValidDays 将套餐有效期统一折算为开放接口展示天数
func packageValidDays(durationDays *int, durationMonths int) int {
if durationDays != nil && *durationDays > 0 {
return *durationDays
}
if durationMonths > 0 {
return durationMonths * 30
}
return 0
}
func normalizePage(page, pageSize int) (int, int) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = constants.DefaultPageSize
}
if pageSize > constants.MaxPageSize {
pageSize = constants.MaxPageSize
}
return page, pageSize
}
func stringValue(value *string) string {
if value == nil {
return ""
}
return *value
}
func int64Value(value *int64) int64 {
if value == nil {
return 0
}
return *value
}
func walletCurrency(currency string) string {
if currency == "" {
return constants.AgentOpenAPIWalletCurrencyCNY
}
return currency
}
func buildFailedCard(cardNo string, err error) dto.AgentOpenAPIWalletPackageOrderFailure {
code := errors.CodeInternalError
message := errors.GetMessage(code, "zh-CN")
if appErr, ok := err.(*errors.AppError); ok {
code = appErr.Code
message = appErr.Message
}
return dto.AgentOpenAPIWalletPackageOrderFailure{
CardNo: cardNo,
Code: code,
Message: message,
}
}
func generateAgentOpenAPIBatchNo() string {
return fmt.Sprintf("%s%s%06d", constants.AgentOpenAPIBatchNoPrefix, time.Now().Format("20060102150405"), rand.Intn(1000000))
}