This commit is contained in:
@@ -192,7 +192,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
shopCommission := shopCommissionSvc.New(s.Shop, s.Account, s.AgentWallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.AgentWalletTransaction, deps.DB, deps.Logger)
|
||||
packageService := packageSvc.New(s.Package, s.PackageSeries, s.ShopPackageAllocation, s.ShopSeriesAllocation)
|
||||
orderService := orderSvc.New(deps.DB, deps.Redis, s.Order, s.OrderItem, s.AgentWallet, s.AssetWallet, s.Payment, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, s.PackageUsage, s.Package, wechatConfig, deps.WechatPayment, paymentLoader, deps.QueueClient, deps.Logger, s.AssetIdentifier, s.PersonalCustomer, s.PersonalCustomerPhone)
|
||||
assetService := assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient, s.AssetIdentifier, s.Order, s.OrderItem, s.ExchangeOrder)
|
||||
assetService := assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient, s.AssetIdentifier, s.Order, s.OrderItem, s.ExchangeOrder, assetAudit)
|
||||
agentOpenAPI := agentOpenAPISvc.New(assetService, packageService, orderService, shopCommission, stopResumeService, device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.AgentWallet, s.DeviceSimBinding, s.Device)
|
||||
|
||||
return &services{
|
||||
|
||||
@@ -3,6 +3,7 @@ package admin
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
@@ -162,6 +163,63 @@ func (h *AssetHandler) CurrentPackage(c *fiber.Ctx) error {
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// UpdatePackageExpiresAt 修改资产套餐过期时间
|
||||
// PATCH /api/admin/assets/:identifier/packages/:package_usage_id/expires-at
|
||||
func (h *AssetHandler) UpdatePackageExpiresAt(c *fiber.Ctx) error {
|
||||
if err := ensureAssetPackageAdjuster(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asset, packageUsageID, err := h.resolveAssetPackageUsagePath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req dto.UpdateAssetPackageExpiresAtRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
expiresAt, err := parseAssetPackageTime(req.ExpiresAt)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "过期时间格式无效")
|
||||
}
|
||||
|
||||
result, err := h.assetService.UpdatePackageExpiresAt(c.UserContext(), asset.AssetType, asset.AssetID, assetIdentifierForAudit(asset), packageUsageID, expiresAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// UpdatePackageUsage 修改资产套餐已用量
|
||||
// PATCH /api/admin/assets/:identifier/packages/:package_usage_id/used-data
|
||||
func (h *AssetHandler) UpdatePackageUsage(c *fiber.Ctx) error {
|
||||
if err := ensureAssetPackageAdjuster(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asset, packageUsageID, err := h.resolveAssetPackageUsagePath(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req dto.UpdateAssetPackageUsageRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if req.DataUsageMB == nil || *req.DataUsageMB < 0 {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
result, err := h.assetService.UpdatePackageUsage(c.UserContext(), asset.AssetType, asset.AssetID, assetIdentifierForAudit(asset), packageUsageID, *req.DataUsageMB)
|
||||
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 {
|
||||
@@ -440,3 +498,57 @@ func resolveCallerAccountType(c *fiber.Ctx) string {
|
||||
}
|
||||
return "agent"
|
||||
}
|
||||
|
||||
func (h *AssetHandler) resolveAssetPackageUsagePath(c *fiber.Ctx) (*dto.AssetResolveResponse, uint, error) {
|
||||
asset, err := h.resolveByIdentifier(c)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
packageUsageID, parseErr := strconv.ParseUint(c.Params("package_usage_id"), 10, 64)
|
||||
if parseErr != nil || packageUsageID == 0 {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
return asset, uint(packageUsageID), nil
|
||||
}
|
||||
|
||||
func ensureAssetPackageAdjuster(c *fiber.Ctx) error {
|
||||
if middleware.GetUserIDFromContext(c.UserContext()) != 2 {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAssetPackageTime(value string) (time.Time, error) {
|
||||
raw := strings.TrimSpace(value)
|
||||
if raw == "" {
|
||||
return time.Time{}, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
layouts := []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02",
|
||||
}
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
func assetIdentifierForAudit(asset *dto.AssetResolveResponse) string {
|
||||
if asset == nil {
|
||||
return ""
|
||||
}
|
||||
if asset.AssetType == "card" && strings.TrimSpace(asset.ICCID) != "" {
|
||||
return asset.ICCID
|
||||
}
|
||||
if strings.TrimSpace(asset.VirtualNo) != "" {
|
||||
return asset.VirtualNo
|
||||
}
|
||||
return asset.Identifier
|
||||
}
|
||||
|
||||
@@ -57,14 +57,14 @@ type AssetResolveResponse struct {
|
||||
SwitchMode string `json:"switch_mode,omitempty" description:"切卡模式:0=自动,1=手动(设备类型时有效)"`
|
||||
LastGatewaySyncAt *time.Time `json:"last_gateway_sync_at,omitempty" description:"最后 sync-info 同步时间(设备类型时有效)"`
|
||||
// 卡专属字段(device类型时为零值)
|
||||
CarrierID uint `json:"carrier_id,omitempty" description:"运营商ID"`
|
||||
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型"`
|
||||
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
|
||||
MSISDN string `json:"msisdn,omitempty" description:"手机号"`
|
||||
IMSI string `json:"imsi,omitempty" description:"IMSI"`
|
||||
CardCategory string `json:"card_category,omitempty" description:"卡业务类型"`
|
||||
Supplier string `json:"supplier,omitempty" description:"供应商"`
|
||||
EnablePolling bool `json:"enable_polling" description:"是否参与轮询"`
|
||||
CarrierID uint `json:"carrier_id,omitempty" description:"运营商ID"`
|
||||
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型"`
|
||||
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
|
||||
MSISDN string `json:"msisdn,omitempty" description:"手机号"`
|
||||
IMSI string `json:"imsi,omitempty" description:"IMSI"`
|
||||
CardCategory string `json:"card_category,omitempty" description:"卡业务类型"`
|
||||
Supplier string `json:"supplier,omitempty" description:"供应商"`
|
||||
EnablePolling bool `json:"enable_polling" description:"是否参与轮询"`
|
||||
NetworkStatus int `json:"network_status" description:"网络状态:0停机 1开机(asset_type=card时有效)"`
|
||||
GatewayExtend string `json:"gateway_extend" description:"Gateway 卡状态扩展字段,原样返回上游 extend,用于展示运营商侧实际停机原因"`
|
||||
GatewayCardIMEI string `json:"gateway_card_imei" description:"插拔卡业务 IMEI(由 Gateway 卡状态接口同步,非设备自身 IMEI,无业务含义,仅供查看)"`
|
||||
@@ -172,6 +172,20 @@ type AssetPackagesRequest struct {
|
||||
Status *int `query:"status" validate:"omitempty,min=0,max=4" minimum:"0" maximum:"4" description:"套餐状态 (0:待生效, 1:生效中, 2:已用完, 3:已过期, 4:已失效)"`
|
||||
}
|
||||
|
||||
// UpdateAssetPackageExpiresAtRequest 修改资产套餐过期时间请求
|
||||
type UpdateAssetPackageExpiresAtRequest struct {
|
||||
Identifier string `path:"identifier" description:"资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN)" required:"true"`
|
||||
PackageUsageID uint `path:"package_usage_id" description:"套餐使用记录ID" required:"true"`
|
||||
ExpiresAt string `json:"expires_at" validate:"required" required:"true" description:"新的套餐过期时间,格式:YYYY-MM-DD HH:MM:SS 或 RFC3339"`
|
||||
}
|
||||
|
||||
// UpdateAssetPackageUsageRequest 修改资产套餐已用量请求
|
||||
type UpdateAssetPackageUsageRequest struct {
|
||||
Identifier string `path:"identifier" description:"资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN)" required:"true"`
|
||||
PackageUsageID uint `path:"package_usage_id" description:"套餐使用记录ID" required:"true"`
|
||||
DataUsageMB *int64 `json:"data_usage_mb" validate:"required,min=0" required:"true" minimum:"0" description:"新的套餐真实已用量(MB)"`
|
||||
}
|
||||
|
||||
// DeviceIDRequest 设备ID请求(路径参数)
|
||||
type DeviceIDRequest struct {
|
||||
DeviceID uint `path:"device_id" description:"设备ID" required:"true"`
|
||||
|
||||
@@ -56,6 +56,24 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "PATCH", "/:identifier/packages/:package_usage_id/expires-at", handler.UpdatePackageExpiresAt, RouteSpec{
|
||||
Summary: "修改资产套餐过期时间",
|
||||
Description: "修改指定资产下某条套餐使用记录的过期时间。仅账号ID为2的账号可调用。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.UpdateAssetPackageExpiresAtRequest),
|
||||
Output: new(dto.AssetPackageResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "PATCH", "/:identifier/packages/:package_usage_id/used-data", handler.UpdatePackageUsage, RouteSpec{
|
||||
Summary: "修改资产套餐已用量",
|
||||
Description: "修改指定资产下某条套餐使用记录的真实已用量(MB)。仅账号ID为2的账号可调用。",
|
||||
Tags: []string{"资产管理"},
|
||||
Input: new(dto.UpdateAssetPackageUsageRequest),
|
||||
Output: new(dto.AssetPackageResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(assets, doc, groupPath, "POST", "/:identifier/stop", handler.Stop, RouteSpec{
|
||||
Summary: "停机",
|
||||
Description: "设备批量停机设备下所有已实名卡;单卡手动停机。",
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"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"
|
||||
@@ -45,6 +46,7 @@ type Service struct {
|
||||
iotCardService IotCardRefresher
|
||||
gatewayClient *gateway.Client
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
assetAuditService assetAuditSvc.OperationLogger
|
||||
}
|
||||
|
||||
// New 创建资产服务实例
|
||||
@@ -64,6 +66,7 @@ func New(
|
||||
orderStore *postgres.OrderStore,
|
||||
orderItemStore *postgres.OrderItemStore,
|
||||
exchangeOrderStore *postgres.ExchangeOrderStore,
|
||||
assetAuditService assetAuditSvc.OperationLogger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
@@ -81,6 +84,7 @@ func New(
|
||||
iotCardService: iotCardService,
|
||||
gatewayClient: gatewayClient,
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
assetAuditService: assetAuditService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,49 +838,102 @@ func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id ui
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询当前生效套餐失败")
|
||||
}
|
||||
|
||||
pkg, pkgErr := s.packageStore.GetByID(ctx, usage.PackageID)
|
||||
metrics := usage.BuildTrafficMetrics()
|
||||
pkgName := usage.PackageName
|
||||
pkgType := ""
|
||||
expiryBase := ""
|
||||
if pkgErr == nil && pkg != nil {
|
||||
if pkgName == "" {
|
||||
pkgName = pkg.PackageName
|
||||
}
|
||||
pkgType = pkg.PackageType
|
||||
expiryBase = pkg.ExpiryBase
|
||||
return s.buildAssetPackageResponse(ctx, usage, callerAccountType), nil
|
||||
}
|
||||
|
||||
// UpdatePackageExpiresAt 修改资产套餐过期时间。
|
||||
func (s *Service) UpdatePackageExpiresAt(ctx context.Context, assetType string, assetID uint, assetIdentifier string, packageUsageID uint, expiresAt time.Time) (*dto.AssetPackageResponse, error) {
|
||||
carrierType, err := normalizeAssetCarrierType(assetType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var paidAmount *int64
|
||||
if callerAccountType == constants.OwnerTypePlatform {
|
||||
paidAmount = usage.PaidAmount
|
||||
|
||||
before, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
|
||||
if err != nil {
|
||||
return nil, wrapPackageUsageNotFound(err)
|
||||
}
|
||||
return &dto.AssetPackageResponse{
|
||||
PackageUsageID: usage.ID,
|
||||
PackageID: usage.PackageID,
|
||||
PackageName: pkgName,
|
||||
PackageType: pkgType,
|
||||
ExpiryBase: expiryBase,
|
||||
OrderID: usage.OrderID,
|
||||
OrderNo: usage.OrderNo,
|
||||
RefundID: usage.RefundID,
|
||||
RefundNo: usage.RefundNo,
|
||||
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,
|
||||
PaidAmount: paidAmount,
|
||||
RetailAmount: usage.RetailAmount,
|
||||
CreatedAt: usage.CreatedAt,
|
||||
}, nil
|
||||
|
||||
beforeData := packageUsageExpiresAtAuditData(before)
|
||||
rows, err := s.packageUsageStore.UpdateExpiresAtForCarrier(ctx, packageUsageID, carrierType, assetID, expiresAt)
|
||||
if err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "修改套餐过期时间失败")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"expires_at": expiresAt,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
if rows == 0 {
|
||||
appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"expires_at": expiresAt,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
updated, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
|
||||
if err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"expires_at": expiresAt,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间", constants.AssetAuditResultSuccess, assetType, assetID, assetIdentifier, beforeData, packageUsageExpiresAtAuditData(updated), nil)
|
||||
return s.buildAssetPackageResponse(ctx, updated, constants.OwnerTypePlatform), nil
|
||||
}
|
||||
|
||||
// UpdatePackageUsage 修改资产套餐已用量。
|
||||
func (s *Service) UpdatePackageUsage(ctx context.Context, assetType string, assetID uint, assetIdentifier string, packageUsageID uint, dataUsageMB int64) (*dto.AssetPackageResponse, error) {
|
||||
if dataUsageMB < 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
carrierType, err := normalizeAssetCarrierType(assetType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
before, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
|
||||
if err != nil {
|
||||
return nil, wrapPackageUsageNotFound(err)
|
||||
}
|
||||
|
||||
beforeData := packageUsageTrafficAuditData(before)
|
||||
nextStatus := statusForManualDataUsage(before, dataUsageMB)
|
||||
rows, err := s.packageUsageStore.UpdateDataUsageForCarrier(ctx, packageUsageID, carrierType, assetID, dataUsageMB, nextStatus)
|
||||
if err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "修改套餐已用量失败")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"data_usage_mb": dataUsageMB,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
if rows == 0 {
|
||||
appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"data_usage_mb": dataUsageMB,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
updated, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID)
|
||||
if err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{
|
||||
"package_usage_id": packageUsageID,
|
||||
"data_usage_mb": dataUsageMB,
|
||||
}, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量", constants.AssetAuditResultSuccess, assetType, assetID, assetIdentifier, beforeData, packageUsageTrafficAuditData(updated), nil)
|
||||
return s.buildAssetPackageResponse(ctx, updated, constants.OwnerTypePlatform), nil
|
||||
}
|
||||
|
||||
// getDeviceProtectStatus 查询设备保护期状态
|
||||
@@ -973,6 +1030,153 @@ func (s *Service) resolveAssetGeneration(ctx context.Context, assetType string,
|
||||
return 1
|
||||
}
|
||||
|
||||
func (s *Service) buildAssetPackageResponse(ctx context.Context, usage *model.PackageUsage, callerAccountType string) *dto.AssetPackageResponse {
|
||||
if usage == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkg, pkgErr := s.packageStore.GetByID(ctx, usage.PackageID)
|
||||
metrics := usage.BuildTrafficMetrics()
|
||||
pkgName := usage.PackageName
|
||||
pkgType := ""
|
||||
expiryBase := ""
|
||||
if pkgErr == nil && pkg != nil {
|
||||
if pkgName == "" {
|
||||
pkgName = pkg.PackageName
|
||||
}
|
||||
pkgType = pkg.PackageType
|
||||
expiryBase = pkg.ExpiryBase
|
||||
}
|
||||
var paidAmount *int64
|
||||
if callerAccountType == constants.OwnerTypePlatform {
|
||||
paidAmount = usage.PaidAmount
|
||||
}
|
||||
|
||||
return &dto.AssetPackageResponse{
|
||||
PackageUsageID: usage.ID,
|
||||
PackageID: usage.PackageID,
|
||||
PackageName: pkgName,
|
||||
PackageType: pkgType,
|
||||
ExpiryBase: expiryBase,
|
||||
OrderID: usage.OrderID,
|
||||
OrderNo: usage.OrderNo,
|
||||
RefundID: usage.RefundID,
|
||||
RefundNo: usage.RefundNo,
|
||||
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,
|
||||
PaidAmount: paidAmount,
|
||||
RetailAmount: usage.RetailAmount,
|
||||
CreatedAt: usage.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAssetCarrierType(assetType string) (string, error) {
|
||||
switch assetType {
|
||||
case "card", constants.AssetTypeIotCard:
|
||||
return constants.AssetTypeIotCard, nil
|
||||
case constants.AssetTypeDevice:
|
||||
return constants.AssetTypeDevice, nil
|
||||
default:
|
||||
return "", errors.New(errors.CodeInvalidParam, "不支持的资产类型,仅支持 card 或 device")
|
||||
}
|
||||
}
|
||||
|
||||
func wrapPackageUsageNotFound(err error) error {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||||
}
|
||||
|
||||
func statusForManualDataUsage(usage *model.PackageUsage, dataUsageMB int64) *int {
|
||||
if usage == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
threshold := usage.EffectiveThresholdMB()
|
||||
if threshold <= 0 {
|
||||
return nil
|
||||
}
|
||||
switch usage.Status {
|
||||
case constants.PackageUsageStatusActive:
|
||||
if dataUsageMB >= threshold {
|
||||
status := constants.PackageUsageStatusDepleted
|
||||
return &status
|
||||
}
|
||||
case constants.PackageUsageStatusDepleted:
|
||||
if dataUsageMB < threshold {
|
||||
status := constants.PackageUsageStatusActive
|
||||
return &status
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func packageUsageExpiresAtAuditData(usage *model.PackageUsage) map[string]any {
|
||||
if usage == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"package_usage_id": usage.ID,
|
||||
"expires_at": usage.ExpiresAt,
|
||||
"status": usage.Status,
|
||||
"status_name": packageStatusName(usage.Status),
|
||||
}
|
||||
}
|
||||
|
||||
func packageUsageTrafficAuditData(usage *model.PackageUsage) map[string]any {
|
||||
if usage == nil {
|
||||
return nil
|
||||
}
|
||||
metrics := usage.BuildTrafficMetrics()
|
||||
return map[string]any{
|
||||
"package_usage_id": usage.ID,
|
||||
"data_usage_mb": usage.DataUsageMB,
|
||||
"virtual_used_mb": metrics.VirtualUsedMB,
|
||||
"real_total_mb": metrics.RealTotalMB,
|
||||
"virtual_total_mb": metrics.VirtualTotalMB,
|
||||
"status": usage.Status,
|
||||
"status_name": packageStatusName(usage.Status),
|
||||
"enable_virtual_data": usage.EnableVirtualDataSnapshot,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) logPackageAdjustmentAudit(ctx context.Context, operationType, operationDesc, resultStatus, assetType string, assetID uint, assetIdentifier string, beforeData, afterData map[string]any, err error) {
|
||||
if s == nil || s.assetAuditService == nil {
|
||||
return
|
||||
}
|
||||
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
|
||||
wrappedBefore, wrappedAfter := assetAuditSvc.WrapOperationContent(beforeData, afterData, map[string]any{
|
||||
"asset_type": assetAuditSvc.NormalizeAssetType(assetType),
|
||||
"asset_id": assetID,
|
||||
"asset_identifier": assetIdentifier,
|
||||
})
|
||||
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, assetAuditSvc.BuildLogParams{
|
||||
Operator: assetAuditSvc.OperatorFromContext(ctx),
|
||||
AssetType: assetType,
|
||||
AssetID: assetID,
|
||||
AssetIdentifier: assetIdentifier,
|
||||
OperationType: operationType,
|
||||
OperationDesc: operationDesc,
|
||||
BeforeData: wrappedBefore,
|
||||
AfterData: wrappedAfter,
|
||||
ResultStatus: resultStatus,
|
||||
ErrorCode: errorCode,
|
||||
ErrorMsg: errorMsg,
|
||||
}))
|
||||
}
|
||||
|
||||
// tracePreviousGenerations 通过换货链逆向追溯前代资产的订单,最多追溯10代
|
||||
func (s *Service) tracePreviousGenerations(ctx context.Context, assetType string, assetID uint) ([]*dto.PreviousGenerationOrders, bool) {
|
||||
const maxDepth = 10
|
||||
|
||||
@@ -339,6 +339,24 @@ func getOperationFieldDesc(operationType string) map[string]string {
|
||||
return map[string]string{
|
||||
"realname_policy": "实名策略(none/before_order/after_order)",
|
||||
}
|
||||
case "asset_package_expires_at":
|
||||
return map[string]string{
|
||||
"package_usage_id": "套餐使用记录ID",
|
||||
"expires_at": "套餐过期时间",
|
||||
"status": "套餐状态",
|
||||
"status_name": "套餐状态名称",
|
||||
}
|
||||
case "asset_package_usage":
|
||||
return map[string]string{
|
||||
"package_usage_id": "套餐使用记录ID",
|
||||
"data_usage_mb": "套餐真实已用量(MB)",
|
||||
"virtual_used_mb": "套餐展示已用量(MB)",
|
||||
"real_total_mb": "套餐真实总量(MB)",
|
||||
"virtual_total_mb": "套餐业务停机阈值(MB)",
|
||||
"status": "套餐状态",
|
||||
"status_name": "套餐状态名称",
|
||||
"enable_virtual_data": "是否启用虚流量",
|
||||
}
|
||||
case "card_realname_status":
|
||||
return map[string]string{
|
||||
"real_name_status": "实名状态(0未实名/1已实名)",
|
||||
|
||||
@@ -42,6 +42,17 @@ func (s *PackageUsageStore) GetByID(ctx context.Context, id uint) (*model.Packag
|
||||
return &usage, nil
|
||||
}
|
||||
|
||||
// GetByIDForCarrier 根据载体查询套餐使用记录。
|
||||
func (s *PackageUsageStore) GetByIDForCarrier(ctx context.Context, id uint, carrierType string, carrierID uint) (*model.PackageUsage, error) {
|
||||
var usage model.PackageUsage
|
||||
query := s.db.WithContext(ctx).Where("id = ?", id)
|
||||
query = applyPackageUsageCarrierFilter(query, carrierType, carrierID)
|
||||
if err := query.First(&usage).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &usage, nil
|
||||
}
|
||||
|
||||
// Update 更新套餐使用记录
|
||||
func (s *PackageUsageStore) Update(ctx context.Context, usage *model.PackageUsage) error {
|
||||
return s.db.WithContext(ctx).Save(usage).Error
|
||||
@@ -140,6 +151,37 @@ func (s *PackageUsageStore) UpdateDataUsage(ctx context.Context, id uint, increm
|
||||
Update("data_usage_mb", gorm.Expr("data_usage_mb + ?", incrementMB)).Error
|
||||
}
|
||||
|
||||
// UpdateExpiresAtForCarrier 按载体条件更新套餐过期时间。
|
||||
func (s *PackageUsageStore) UpdateExpiresAtForCarrier(ctx context.Context, id uint, carrierType string, carrierID uint, expiresAt time.Time) (int64, error) {
|
||||
query := s.db.WithContext(ctx).
|
||||
Model(&model.PackageUsage{}).
|
||||
Where("id = ?", id)
|
||||
query = applyPackageUsageCarrierFilter(query, carrierType, carrierID)
|
||||
|
||||
result := query.Updates(map[string]any{
|
||||
"expires_at": expiresAt,
|
||||
})
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// UpdateDataUsageForCarrier 按载体条件更新套餐已用量。
|
||||
func (s *PackageUsageStore) UpdateDataUsageForCarrier(ctx context.Context, id uint, carrierType string, carrierID uint, dataUsageMB int64, status *int) (int64, error) {
|
||||
query := s.db.WithContext(ctx).
|
||||
Model(&model.PackageUsage{}).
|
||||
Where("id = ?", id)
|
||||
query = applyPackageUsageCarrierFilter(query, carrierType, carrierID)
|
||||
|
||||
updates := map[string]any{
|
||||
"data_usage_mb": dataUsageMB,
|
||||
}
|
||||
if status != nil {
|
||||
updates["status"] = *status
|
||||
}
|
||||
|
||||
result := query.Updates(updates)
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// GetPackagesForReset 查询需要重置的套餐(WHERE next_reset_at <= NOW)
|
||||
func (s *PackageUsageStore) GetPackagesForReset(ctx context.Context, limit int) ([]*model.PackageUsage, error) {
|
||||
var usages []*model.PackageUsage
|
||||
@@ -239,11 +281,7 @@ func (s *PackageUsageStore) ListByCarrier(ctx context.Context, carrierType strin
|
||||
var usages []*model.PackageUsage
|
||||
query := s.db.WithContext(ctx).Order("priority ASC, created_at DESC")
|
||||
|
||||
if carrierType == "iot_card" {
|
||||
query = query.Where("iot_card_id = ?", carrierID)
|
||||
} else if carrierType == "device" {
|
||||
query = query.Where("device_id = ?", carrierID)
|
||||
}
|
||||
query = applyPackageUsageCarrierFilter(query, carrierType, carrierID)
|
||||
if status != nil {
|
||||
query = query.Where("status = ?", *status)
|
||||
}
|
||||
@@ -253,3 +291,13 @@ func (s *PackageUsageStore) ListByCarrier(ctx context.Context, carrierType strin
|
||||
}
|
||||
return usages, nil
|
||||
}
|
||||
|
||||
func applyPackageUsageCarrierFilter(query *gorm.DB, carrierType string, carrierID uint) *gorm.DB {
|
||||
if carrierType == "iot_card" {
|
||||
return query.Where("iot_card_id = ?", carrierID)
|
||||
}
|
||||
if carrierType == "device" {
|
||||
return query.Where("device_id = ?", carrierID)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user