All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
核心变更: 1. Model层时间字段改为*time.Time并设为nullable - PackageUsage.ActivatedAt/ExpiresAt - PersonalCustomerDevice/ICCID/Phone.LastUsedAt/VerifiedAt 2. 数据库迁移: - activated_at/expires_at列移除NOT NULL约束 - 清洗零值记录(status=0且activated_at<'2000-01-01') 3. 新增ActivateSpecificPackage方法:精准激活指定套餐, 修复HandlePackageQueueActivation从"查找过期包"改为直接激活payload指定套餐 4. 新增孤儿套餐恢复扫描:Worker启动或每次套餐检查时, 自动发现并恢复无status=1主套餐的孤儿载体 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
4.1 KiB
Go
148 lines
4.1 KiB
Go
package packagepkg
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||
"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/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type CustomerViewService struct {
|
||
db *gorm.DB
|
||
redis *redis.Client
|
||
packageUsageStore *postgres.PackageUsageStore
|
||
logger *zap.Logger
|
||
}
|
||
|
||
func NewCustomerViewService(
|
||
db *gorm.DB,
|
||
redis *redis.Client,
|
||
packageUsageStore *postgres.PackageUsageStore,
|
||
logger *zap.Logger,
|
||
) *CustomerViewService {
|
||
return &CustomerViewService{
|
||
db: db,
|
||
redis: redis,
|
||
packageUsageStore: packageUsageStore,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
// GetMyUsage 任务 12.2-12.5: 获取客户套餐使用情况
|
||
// 根据载体ID和类型查询生效中的套餐,计算总流量使用情况
|
||
func (s *CustomerViewService) GetMyUsage(ctx context.Context, carrierType string, carrierID uint) (*dto.PackageUsageCustomerViewResponse, error) {
|
||
// 任务 12.3: 查询生效套餐(status IN (1,2))
|
||
var packages []*model.PackageUsage
|
||
query := s.db.WithContext(ctx).
|
||
Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted})
|
||
|
||
if carrierType == "iot_card" {
|
||
query = query.Where("iot_card_id = ?", carrierID)
|
||
} else if carrierType == "device" {
|
||
query = query.Where("device_id = ?", carrierID)
|
||
} else {
|
||
return nil, errors.New(errors.CodeInvalidParam, "无效的载体类型")
|
||
}
|
||
|
||
// 按优先级排序:主套餐在前,加油包在后
|
||
if err := query.Order("CASE WHEN master_usage_id IS NULL THEN 0 ELSE 1 END, priority ASC").
|
||
Find(&packages).Error; err != nil {
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败")
|
||
}
|
||
|
||
if len(packages) == 0 {
|
||
return nil, errors.New(errors.CodeNotFound, "未找到套餐使用记录")
|
||
}
|
||
|
||
// 任务 12.4: 区分主套餐和加油包,计算总流量
|
||
var mainPackage *dto.PackageUsageItemResponse
|
||
var addonPackages []dto.PackageUsageItemResponse
|
||
var totalUsedMB int64
|
||
var totalLimitMB int64
|
||
|
||
for _, pkg := range packages {
|
||
// 查询套餐信息
|
||
var packageInfo model.Package
|
||
if err := s.db.First(&packageInfo, pkg.PackageID).Error; err != nil {
|
||
s.logger.Warn("查询套餐信息失败",
|
||
zap.Uint("package_id", pkg.PackageID),
|
||
zap.Error(err))
|
||
continue
|
||
}
|
||
|
||
// 格式化状态文本
|
||
statusText := getStatusText(pkg.Status)
|
||
|
||
// 格式化时间
|
||
activatedAtStr := ""
|
||
if pkg.ActivatedAt != nil {
|
||
activatedAtStr = pkg.ActivatedAt.Format("2006-01-02 15:04:05")
|
||
}
|
||
|
||
expiresAtStr := ""
|
||
if pkg.ExpiresAt != nil {
|
||
expiresAtStr = pkg.ExpiresAt.Format("2006-01-02 15:04:05")
|
||
}
|
||
|
||
item := dto.PackageUsageItemResponse{
|
||
PackageUsageID: pkg.ID,
|
||
PackageID: pkg.PackageID,
|
||
PackageName: packageInfo.PackageName,
|
||
UsedMB: pkg.DataUsageMB,
|
||
TotalMB: pkg.DataLimitMB,
|
||
Status: pkg.Status,
|
||
StatusText: statusText,
|
||
ActivatedAt: activatedAtStr,
|
||
ExpiresAt: expiresAtStr,
|
||
Priority: pkg.Priority,
|
||
}
|
||
|
||
// 累计总流量
|
||
totalUsedMB += pkg.DataUsageMB
|
||
totalLimitMB += pkg.DataLimitMB
|
||
|
||
// 区分主套餐和加油包
|
||
if pkg.MasterUsageID == nil {
|
||
mainPackage = &item
|
||
} else {
|
||
addonPackages = append(addonPackages, item)
|
||
}
|
||
}
|
||
|
||
// 任务 12.5: 组装响应 DTO
|
||
response := &dto.PackageUsageCustomerViewResponse{
|
||
MainPackage: mainPackage,
|
||
AddonPackages: addonPackages,
|
||
Total: dto.PackageUsageTotalInfo{
|
||
UsedMB: totalUsedMB,
|
||
TotalMB: totalLimitMB,
|
||
},
|
||
}
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// getStatusText 获取状态文本
|
||
func getStatusText(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 "未知"
|
||
}
|
||
}
|