feat: 实现客户端核心业务接口(client-core-business-api)
新增客户端资产、钱包、订单、实名、设备管理等核心业务 Handler 与 DTO: - 客户端资产信息查询、套餐列表、套餐历史、资产刷新 - 客户端钱包详情、流水、充值校验、充值订单、充值记录 - 客户端订单创建、列表、详情 - 客户端实名认证链接获取 - 客户端设备卡列表、重启、恢复出厂、WiFi配置、切卡 - 客户端订单服务(含微信/支付宝支付流程) - 强充自动代购异步任务处理 - 数据库迁移 000084:充值记录增加自动代购状态字段
This commit is contained in:
556
internal/handler/app/client_asset.go
Normal file
556
internal/handler/app/client_asset.go
Normal file
@@ -0,0 +1,556 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"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/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ClientAssetHandler C 端资产信息处理器
|
||||
// 提供 B1~B4 资产信息、可购套餐、套餐历史、手动刷新接口
|
||||
type ClientAssetHandler struct {
|
||||
assetService *asset.Service
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
assetWalletStore *postgres.AssetWalletStore
|
||||
packageStore *postgres.PackageStore
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewClientAssetHandler 创建 C 端资产信息处理器
|
||||
func NewClientAssetHandler(
|
||||
assetService *asset.Service,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
assetWalletStore *postgres.AssetWalletStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
db *gorm.DB,
|
||||
logger *zap.Logger,
|
||||
) *ClientAssetHandler {
|
||||
return &ClientAssetHandler{
|
||||
assetService: assetService,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
assetWalletStore: assetWalletStore,
|
||||
packageStore: packageStore,
|
||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type resolvedAssetContext struct {
|
||||
CustomerID uint
|
||||
Identifier string
|
||||
Asset *dto.AssetResolveResponse
|
||||
Generation int
|
||||
WalletBalance int64
|
||||
SkipPermissionCtx context.Context
|
||||
IsAgentChannel bool
|
||||
SellerShopID uint
|
||||
MainPackageActived bool
|
||||
}
|
||||
|
||||
// resolveAssetFromIdentifier 统一执行资产解析与归属校验
|
||||
// 处理流程:客户鉴权 -> 标识符解析 -> 资产解析 -> 归属校验 -> 世代与钱包信息补齐
|
||||
func (h *ClientAssetHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifier string) (*resolvedAssetContext, error) {
|
||||
customerID, ok := middleware.GetCustomerID(c)
|
||||
if !ok || customerID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
identifier = strings.TrimSpace(identifier)
|
||||
if identifier == "" {
|
||||
identifier = strings.TrimSpace(c.Query("identifier"))
|
||||
}
|
||||
if identifier == "" {
|
||||
var req dto.AssetRefreshRequest
|
||||
if err := c.BodyParser(&req); err == nil {
|
||||
identifier = strings.TrimSpace(req.Identifier)
|
||||
}
|
||||
}
|
||||
if identifier == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
skipPermissionCtx := context.WithValue(c.UserContext(), constants.ContextKeySubordinateShopIDs, []uint{})
|
||||
assetInfo, err := h.assetService.Resolve(skipPermissionCtx, identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
owned, ownErr := h.isCustomerOwnAsset(skipPermissionCtx, customerID, assetInfo.VirtualNo)
|
||||
if ownErr != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||
}
|
||||
if !owned {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||
}
|
||||
|
||||
generation, genErr := h.getAssetGeneration(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
|
||||
if genErr != nil {
|
||||
return nil, genErr
|
||||
}
|
||||
|
||||
walletBalance, walletErr := h.getAssetWalletBalance(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
|
||||
if walletErr != nil {
|
||||
return nil, walletErr
|
||||
}
|
||||
|
||||
ctxInfo := &resolvedAssetContext{
|
||||
CustomerID: customerID,
|
||||
Identifier: identifier,
|
||||
Asset: assetInfo,
|
||||
Generation: generation,
|
||||
WalletBalance: walletBalance,
|
||||
SkipPermissionCtx: skipPermissionCtx,
|
||||
}
|
||||
|
||||
if assetInfo.ShopID != nil && *assetInfo.ShopID > 0 {
|
||||
ctxInfo.IsAgentChannel = true
|
||||
ctxInfo.SellerShopID = *assetInfo.ShopID
|
||||
}
|
||||
|
||||
return ctxInfo, nil
|
||||
}
|
||||
|
||||
// GetAssetInfo B1 资产信息
|
||||
// GET /api/c/v1/asset/info
|
||||
func (h *ClientAssetHandler) GetAssetInfo(c *fiber.Ctx) error {
|
||||
var req dto.AssetInfoRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := &dto.AssetInfoResponse{
|
||||
AssetType: resolved.Asset.AssetType,
|
||||
AssetID: resolved.Asset.AssetID,
|
||||
Identifier: resolved.Identifier,
|
||||
VirtualNo: resolved.Asset.VirtualNo,
|
||||
Status: resolved.Asset.Status,
|
||||
RealNameStatus: resolved.Asset.RealNameStatus,
|
||||
CarrierName: resolved.Asset.CarrierName,
|
||||
Generation: strconv.Itoa(resolved.Generation),
|
||||
WalletBalance: resolved.WalletBalance,
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// GetAvailablePackages B2 资产可购套餐列表
|
||||
// GET /api/c/v1/asset/packages
|
||||
func (h *ClientAssetHandler) GetAvailablePackages(c *fiber.Ctx) error {
|
||||
var req dto.AssetPackageListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resolved.Asset.SeriesID == nil || *resolved.Asset.SeriesID == 0 {
|
||||
return errors.New(errors.CodeNoAvailablePackage, "当前无可购买套餐")
|
||||
}
|
||||
|
||||
allUsages, err := h.assetService.GetPackages(resolved.SkipPermissionCtx, resolved.Asset.AssetType, resolved.Asset.AssetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolved.MainPackageActived = hasActiveMainPackage(allUsages)
|
||||
|
||||
listCtx := resolved.SkipPermissionCtx
|
||||
if resolved.IsAgentChannel {
|
||||
listCtx = context.WithValue(listCtx, constants.ContextKeyUserType, constants.UserTypeAgent)
|
||||
listCtx = context.WithValue(listCtx, constants.ContextKeyShopID, resolved.SellerShopID)
|
||||
}
|
||||
|
||||
pkgs, _, err := h.packageStore.List(listCtx, &store.QueryOptions{
|
||||
Page: 1,
|
||||
PageSize: constants.MaxPageSize,
|
||||
OrderBy: "id DESC",
|
||||
}, map[string]any{
|
||||
"series_id": *resolved.Asset.SeriesID,
|
||||
"status": constants.StatusEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询可购套餐失败")
|
||||
}
|
||||
|
||||
allocationMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
if resolved.IsAgentChannel {
|
||||
packageIDs := collectPackageIDs(pkgs)
|
||||
allocations, allocErr := h.shopPackageAllocationStore.GetByShopAndPackages(
|
||||
resolved.SkipPermissionCtx,
|
||||
resolved.SellerShopID,
|
||||
packageIDs,
|
||||
)
|
||||
if allocErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, allocErr, "查询套餐分配记录失败")
|
||||
}
|
||||
for _, allocation := range allocations {
|
||||
allocationMap[allocation.PackageID] = allocation
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]dto.ClientPackageItem, 0, len(pkgs))
|
||||
for _, pkg := range pkgs {
|
||||
item, ok := buildClientPackageItem(pkg, resolved, allocationMap)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return errors.New(errors.CodeNoAvailablePackage, "当前无可购买套餐")
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].RetailPrice < items[j].RetailPrice
|
||||
})
|
||||
|
||||
return response.Success(c, &dto.AssetPackageListResponse{Packages: items})
|
||||
}
|
||||
|
||||
// GetPackageHistory B3 资产套餐历史
|
||||
// GET /api/c/v1/asset/package-history
|
||||
func (h *ClientAssetHandler) GetPackageHistory(c *fiber.Ctx) error {
|
||||
var req dto.AssetPackageHistoryRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
if req.PageSize > constants.MaxPageSize {
|
||||
req.PageSize = constants.MaxPageSize
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := h.db.WithContext(resolved.SkipPermissionCtx).Model(&model.PackageUsage{}).
|
||||
Where("generation = ?", resolved.Generation)
|
||||
if resolved.Asset.AssetType == "card" {
|
||||
query = query.Where("iot_card_id = ?", resolved.Asset.AssetID)
|
||||
} else {
|
||||
query = query.Where("device_id = ?", resolved.Asset.AssetID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐历史总数失败")
|
||||
}
|
||||
|
||||
var usages []*model.PackageUsage
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&usages).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐历史失败")
|
||||
}
|
||||
|
||||
packageMap, err := h.loadPackageMap(resolved.SkipPermissionCtx, usages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list := make([]dto.AssetPackageResponse, 0, len(usages))
|
||||
for _, usage := range usages {
|
||||
pkg := packageMap[usage.PackageID]
|
||||
ratio := 1.0
|
||||
pkgName := ""
|
||||
pkgType := ""
|
||||
if pkg != nil {
|
||||
ratio = safeVirtualRatio(pkg.VirtualRatio)
|
||||
pkgName = pkg.PackageName
|
||||
pkgType = pkg.PackageType
|
||||
}
|
||||
|
||||
list = append(list, dto.AssetPackageResponse{
|
||||
PackageUsageID: usage.ID,
|
||||
PackageID: usage.PackageID,
|
||||
PackageName: pkgName,
|
||||
PackageType: pkgType,
|
||||
UsageType: usage.UsageType,
|
||||
Status: usage.Status,
|
||||
StatusName: packageStatusName(usage.Status),
|
||||
DataLimitMB: usage.DataLimitMB,
|
||||
VirtualLimitMB: int64(float64(usage.DataLimitMB) / ratio),
|
||||
DataUsageMB: usage.DataUsageMB,
|
||||
VirtualUsedMB: float64(usage.DataUsageMB) / ratio,
|
||||
VirtualRemainMB: float64(usage.DataLimitMB-usage.DataUsageMB) / ratio,
|
||||
VirtualRatio: ratio,
|
||||
ActivatedAt: nonZeroTimePtr(usage.ActivatedAt),
|
||||
ExpiresAt: nonZeroTimePtr(usage.ExpiresAt),
|
||||
MasterUsageID: usage.MasterUsageID,
|
||||
Priority: usage.Priority,
|
||||
CreatedAt: usage.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, list, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
// RefreshAsset B4 资产刷新
|
||||
// POST /api/c/v1/asset/refresh
|
||||
func (h *ClientAssetHandler) RefreshAsset(c *fiber.Ctx) error {
|
||||
var req dto.AssetRefreshRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := h.assetService.Refresh(
|
||||
resolved.SkipPermissionCtx,
|
||||
resolved.Asset.AssetType,
|
||||
resolved.Asset.AssetID,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := &dto.AssetRefreshResponse{
|
||||
RefreshType: resolved.Asset.AssetType,
|
||||
Accepted: true,
|
||||
CooldownSeconds: 0,
|
||||
}
|
||||
if resolved.Asset.AssetType == constants.ResourceTypeDevice {
|
||||
resp.CooldownSeconds = int(constants.DeviceRefreshCooldownDuration / time.Second)
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *ClientAssetHandler) isCustomerOwnAsset(ctx context.Context, customerID uint, virtualNo string) (bool, error) {
|
||||
records, err := h.personalDeviceStore.GetByCustomerID(ctx, customerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (h *ClientAssetHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
||||
switch assetType {
|
||||
case "card":
|
||||
card, err := h.iotCardStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败")
|
||||
}
|
||||
return card.Generation, nil
|
||||
case "device":
|
||||
device, err := h.deviceStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败")
|
||||
}
|
||||
return device.Generation, nil
|
||||
default:
|
||||
return 0, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ClientAssetHandler) getAssetWalletBalance(ctx context.Context, assetType string, assetID uint) (int64, error) {
|
||||
resourceType := constants.AssetWalletResourceTypeIotCard
|
||||
if assetType == constants.ResourceTypeDevice {
|
||||
resourceType = constants.AssetWalletResourceTypeDevice
|
||||
}
|
||||
|
||||
wallet, err := h.assetWalletStore.GetByResourceTypeAndID(ctx, resourceType, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败")
|
||||
}
|
||||
|
||||
return wallet.Balance, nil
|
||||
}
|
||||
|
||||
func (h *ClientAssetHandler) loadPackageMap(ctx context.Context, usages []*model.PackageUsage) (map[uint]*model.Package, error) {
|
||||
ids := make([]uint, 0, len(usages))
|
||||
seen := make(map[uint]struct{}, len(usages))
|
||||
for _, usage := range usages {
|
||||
if usage == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[usage.PackageID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[usage.PackageID] = struct{}{}
|
||||
ids = append(ids, usage.PackageID)
|
||||
}
|
||||
|
||||
packages, err := h.packageStore.GetByIDsUnscoped(ctx, ids)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
result := make(map[uint]*model.Package, len(packages))
|
||||
for _, pkg := range packages {
|
||||
result[pkg.ID] = pkg
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func collectPackageIDs(pkgs []*model.Package) []uint {
|
||||
ids := make([]uint, 0, len(pkgs))
|
||||
for _, pkg := range pkgs {
|
||||
if pkg == nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, pkg.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func hasActiveMainPackage(usages []*dto.AssetPackageResponse) bool {
|
||||
for _, usage := range usages {
|
||||
if usage == nil {
|
||||
continue
|
||||
}
|
||||
if usage.PackageType == constants.PackageTypeFormal && usage.Status == constants.PackageUsageStatusActive {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildClientPackageItem(
|
||||
pkg *model.Package,
|
||||
resolved *resolvedAssetContext,
|
||||
allocationMap map[uint]*model.ShopPackageAllocation,
|
||||
) (dto.ClientPackageItem, bool) {
|
||||
if pkg == nil || pkg.Status != constants.StatusEnabled {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
isAddon := pkg.PackageType == constants.PackageTypeAddon
|
||||
if isAddon && !resolved.MainPackageActived {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
retailPrice := pkg.SuggestedRetailPrice
|
||||
costPrice := pkg.CostPrice
|
||||
|
||||
if resolved.IsAgentChannel {
|
||||
allocation, ok := allocationMap[pkg.ID]
|
||||
if !ok || allocation == nil {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
if allocation.ShelfStatus != constants.ShelfStatusOn || allocation.Status != constants.StatusEnabled {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
retailPrice = allocation.RetailPrice
|
||||
costPrice = allocation.CostPrice
|
||||
} else if pkg.ShelfStatus != constants.ShelfStatusOn {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
if retailPrice < costPrice {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
validityDays := pkg.DurationDays
|
||||
if validityDays <= 0 && pkg.DurationMonths > 0 {
|
||||
validityDays = pkg.DurationMonths * 30
|
||||
}
|
||||
|
||||
dataAllowance := pkg.VirtualDataMB
|
||||
if dataAllowance <= 0 {
|
||||
dataAllowance = pkg.RealDataMB
|
||||
}
|
||||
|
||||
return dto.ClientPackageItem{
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageType: pkg.PackageType,
|
||||
RetailPrice: retailPrice,
|
||||
CostPrice: costPrice,
|
||||
ValidityDays: validityDays,
|
||||
IsAddon: isAddon,
|
||||
DataAllowance: dataAllowance,
|
||||
DataUnit: "MB",
|
||||
Description: pkg.PackageCode,
|
||||
}, true
|
||||
}
|
||||
|
||||
func nonZeroTimePtr(t time.Time) *time.Time {
|
||||
if t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &t
|
||||
}
|
||||
|
||||
func safeVirtualRatio(ratio float64) float64 {
|
||||
if ratio <= 0 {
|
||||
return 1.0
|
||||
}
|
||||
return ratio
|
||||
}
|
||||
|
||||
func packageStatusName(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 "未知"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user