修改过期时间,以及修改套餐已用量
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

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

View File

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

View File

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

View File

@@ -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: "设备批量停机设备下所有已实名卡;单卡手动停机。",

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

View File

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