修改过期时间,以及修改套餐已用量
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled

This commit is contained in:
Break
2026-06-12 10:41:24 +08:00
parent c437593a8c
commit 016e7bee79
8 changed files with 501 additions and 85 deletions

View File

@@ -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

View File

@@ -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已实名",