修复错误购买报错的问题,修复接口返回虚流量启动错误的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
This commit is contained in:
@@ -72,7 +72,6 @@ type resolvedAssetContext struct {
|
|||||||
IsAgentChannel bool
|
IsAgentChannel bool
|
||||||
SellerShopID uint
|
SellerShopID uint
|
||||||
HasAddonMainPackage bool
|
HasAddonMainPackage bool
|
||||||
HasFormalMainPackage bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveAssetFromIdentifier 统一执行资产解析与归属校验
|
// resolveAssetFromIdentifier 统一执行资产解析与归属校验
|
||||||
@@ -243,16 +242,6 @@ func (h *ClientAssetHandler) GetAvailablePackages(c *fiber.Ctx) error {
|
|||||||
} else if err != gorm.ErrRecordNotFound {
|
} else if err != gorm.ErrRecordNotFound {
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
|
||||||
}
|
}
|
||||||
if hasBlockingMain, err := packagepkg.HasBlockingMainPackageForFormal(
|
|
||||||
h.db.WithContext(resolved.SkipPermissionCtx),
|
|
||||||
resolved.Asset.AssetType,
|
|
||||||
resolved.Asset.AssetID,
|
|
||||||
time.Now(),
|
|
||||||
); err != nil {
|
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
|
|
||||||
} else {
|
|
||||||
resolved.HasFormalMainPackage = hasBlockingMain
|
|
||||||
}
|
|
||||||
|
|
||||||
listCtx := resolved.SkipPermissionCtx
|
listCtx := resolved.SkipPermissionCtx
|
||||||
if resolved.IsAgentChannel {
|
if resolved.IsAgentChannel {
|
||||||
@@ -388,6 +377,7 @@ func (h *ClientAssetHandler) GetPackageHistory(c *fiber.Ctx) error {
|
|||||||
VirtualTotalMB: metrics.VirtualTotalMB,
|
VirtualTotalMB: metrics.VirtualTotalMB,
|
||||||
VirtualUsedMB: metrics.VirtualUsedMB,
|
VirtualUsedMB: metrics.VirtualUsedMB,
|
||||||
ReductionPct: metrics.ReductionPct,
|
ReductionPct: metrics.ReductionPct,
|
||||||
|
EnableVirtualData: usage.EnableVirtualDataSnapshot,
|
||||||
ActivatedAt: usage.ActivatedAt,
|
ActivatedAt: usage.ActivatedAt,
|
||||||
ExpiresAt: usage.ExpiresAt,
|
ExpiresAt: usage.ExpiresAt,
|
||||||
MasterUsageID: usage.MasterUsageID,
|
MasterUsageID: usage.MasterUsageID,
|
||||||
@@ -541,9 +531,6 @@ func buildClientPackageItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
isAddon := pkg.PackageType == constants.PackageTypeAddon
|
isAddon := pkg.PackageType == constants.PackageTypeAddon
|
||||||
if pkg.PackageType == constants.PackageTypeFormal && resolved.HasFormalMainPackage {
|
|
||||||
return dto.ClientPackageItem{}, false
|
|
||||||
}
|
|
||||||
if isAddon && !resolved.HasAddonMainPackage {
|
if isAddon && !resolved.HasAddonMainPackage {
|
||||||
return dto.ClientPackageItem{}, false
|
return dto.ClientPackageItem{}, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,10 @@ func (h *Handler) CreateWalletPackageOrders(c *fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if resp.FailedCount > 0 {
|
||||||
|
failure := resp.FailedCards[0]
|
||||||
|
return response.Business(c, failure.Code, resp, failure.Message)
|
||||||
|
}
|
||||||
return response.Success(c, resp)
|
return response.Success(c, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||||
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
|
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/alipay"
|
"github.com/break/junhong_cmp_fiber/pkg/alipay"
|
||||||
@@ -1248,10 +1247,6 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
|
|||||||
return &dto.ClientPayOrderResponse{PaymentMethod: model.PaymentMethodWallet}, nil
|
return &dto.ClientPayOrderResponse{PaymentMethod: model.PaymentMethodWallet}, nil
|
||||||
|
|
||||||
case model.PaymentMethodWechat:
|
case model.PaymentMethodWechat:
|
||||||
if err := s.ensureOrderFormalPackagePayable(skipCtx, order); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType)
|
activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1308,10 +1303,6 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
|
|||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case model.PaymentMethodAlipay:
|
case model.PaymentMethodAlipay:
|
||||||
if err := s.ensureOrderFormalPackagePayable(skipCtx, order); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 支付宝支付:不需要 app_type / OpenID
|
// 支付宝支付:不需要 app_type / OpenID
|
||||||
activeConfig, err := s.wechatConfigService.GetActiveConfig(skipCtx)
|
activeConfig, err := s.wechatConfigService.GetActiveConfig(skipCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1346,58 +1337,6 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureOrderFormalPackagePayable(ctx context.Context, order *model.Order) error {
|
|
||||||
if order == nil {
|
|
||||||
return errors.New(errors.CodeInvalidParam)
|
|
||||||
}
|
|
||||||
|
|
||||||
var items []*model.OrderItem
|
|
||||||
if err := s.db.WithContext(ctx).Where("order_id = ?", order.ID).Find(&items).Error; err != nil {
|
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单明细失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
hasFormal := false
|
|
||||||
for _, item := range items {
|
|
||||||
if item == nil || item.PackageType == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if *item.PackageType == constants.PackageTypeFormal {
|
|
||||||
hasFormal = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasFormal {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
carrierType := ""
|
|
||||||
carrierID := uint(0)
|
|
||||||
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
|
|
||||||
carrierType = constants.AssetWalletResourceTypeIotCard
|
|
||||||
carrierID = *order.IotCardID
|
|
||||||
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
|
|
||||||
carrierType = constants.AssetWalletResourceTypeDevice
|
|
||||||
carrierID = *order.DeviceID
|
|
||||||
}
|
|
||||||
if carrierID == 0 {
|
|
||||||
return errors.New(errors.CodeInvalidParam, "订单缺少套餐载体")
|
|
||||||
}
|
|
||||||
|
|
||||||
hasBlockingMain, err := packagepkg.HasBlockingMainPackageForFormal(
|
|
||||||
s.db.WithContext(ctx),
|
|
||||||
carrierType,
|
|
||||||
carrierID,
|
|
||||||
time.Now(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
|
|
||||||
}
|
|
||||||
if hasBlockingMain {
|
|
||||||
return errors.New(errors.CodeMainPackageExists)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
func (s *Service) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
||||||
switch assetType {
|
switch assetType {
|
||||||
case "card":
|
case "card":
|
||||||
|
|||||||
@@ -2227,13 +2227,10 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hasBlockingMain, err := packagepkg.HasBlockingMainPackageForFormal(tx.WithContext(ctx), carrierType, carrierID, now)
|
hasCurrentMain, err := packagepkg.HasCurrentMainPackageForQueue(tx.WithContext(ctx), carrierType, carrierID, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
|
||||||
}
|
}
|
||||||
if hasBlockingMain {
|
|
||||||
return errors.New(errors.CodeMainPackageExists)
|
|
||||||
}
|
|
||||||
|
|
||||||
var status int
|
var status int
|
||||||
var priority int
|
var priority int
|
||||||
@@ -2242,6 +2239,16 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
|||||||
var nextResetAt *time.Time
|
var nextResetAt *time.Time
|
||||||
var pendingRealnameActivation bool
|
var pendingRealnameActivation bool
|
||||||
|
|
||||||
|
if hasCurrentMain {
|
||||||
|
// 当前仍有在用主套餐时,新主套餐进入待生效队列,后续由过期/退款等流程接续激活。
|
||||||
|
status = constants.PackageUsageStatusPending
|
||||||
|
var maxPriority int
|
||||||
|
tx.Model(&model.PackageUsage{}).
|
||||||
|
Where(carrierType+"_id = ?", carrierID).
|
||||||
|
Select("COALESCE(MAX(priority), 0)").
|
||||||
|
Scan(&maxPriority)
|
||||||
|
priority = maxPriority + 1
|
||||||
|
} else {
|
||||||
status = constants.PackageUsageStatusActive
|
status = constants.PackageUsageStatusActive
|
||||||
priority = 1
|
priority = 1
|
||||||
activatedAt = now
|
activatedAt = now
|
||||||
@@ -2298,6 +2305,7 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
|||||||
// from_purchase:立即激活,status 已为 Active,保持不变
|
// from_purchase:立即激活,status 已为 Active,保持不变
|
||||||
}
|
}
|
||||||
// 行业卡或 C 端购买:直接激活,status 已为 Active,保持不变
|
// 行业卡或 C 端购买:直接激活,status 已为 Active,保持不变
|
||||||
|
}
|
||||||
|
|
||||||
// 创建套餐使用记录
|
// 创建套餐使用记录
|
||||||
virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg)
|
virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg)
|
||||||
|
|||||||
@@ -40,21 +40,17 @@ func FindAttachableMainPackageForAddon(tx *gorm.DB, carrierType string, carrierI
|
|||||||
return &mainPackage, nil
|
return &mainPackage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindBlockingMainPackageForFormal 查询阻止购买新主套餐的现有主套餐。
|
// FindCurrentMainPackageForQueue 查询应让新主套餐排队的现有主套餐。
|
||||||
// 待生效、生效中、未过期的已用完主套餐都仍占用主套餐资格;只有过期或失效后才允许再次购买主套餐。
|
// 未过期的生效中、已用完主套餐仍占用当前周期;待生效只是队列记录,不阻止继续排队购买。
|
||||||
func FindBlockingMainPackageForFormal(tx *gorm.DB, carrierType string, carrierID uint, now time.Time) (*model.PackageUsage, error) {
|
func FindCurrentMainPackageForQueue(tx *gorm.DB, carrierType string, carrierID uint, now time.Time) (*model.PackageUsage, error) {
|
||||||
var mainPackage model.PackageUsage
|
var mainPackage model.PackageUsage
|
||||||
query := tx.Model(&model.PackageUsage{}).
|
query := tx.Model(&model.PackageUsage{}).
|
||||||
Where("master_usage_id IS NULL").
|
Where("master_usage_id IS NULL").
|
||||||
Where(
|
Where("status IN ?", []int{
|
||||||
"(status IN ? OR (status = ? AND (expires_at IS NULL OR expires_at > ?)))",
|
|
||||||
[]int{
|
|
||||||
constants.PackageUsageStatusPending,
|
|
||||||
constants.PackageUsageStatusActive,
|
constants.PackageUsageStatusActive,
|
||||||
},
|
|
||||||
constants.PackageUsageStatusDepleted,
|
constants.PackageUsageStatusDepleted,
|
||||||
now,
|
}).
|
||||||
).
|
Where("expires_at IS NULL OR expires_at > ?", now).
|
||||||
Order("priority ASC, created_at ASC")
|
Order("priority ASC, created_at ASC")
|
||||||
|
|
||||||
switch carrierType {
|
switch carrierType {
|
||||||
@@ -72,9 +68,9 @@ func FindBlockingMainPackageForFormal(tx *gorm.DB, carrierType string, carrierID
|
|||||||
return &mainPackage, nil
|
return &mainPackage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasBlockingMainPackageForFormal 判断载体是否已有阻止新主套餐购买的主套餐。
|
// HasCurrentMainPackageForQueue 判断载体是否已有应让新主套餐排队的主套餐。
|
||||||
func HasBlockingMainPackageForFormal(tx *gorm.DB, carrierType string, carrierID uint, now time.Time) (bool, error) {
|
func HasCurrentMainPackageForQueue(tx *gorm.DB, carrierType string, carrierID uint, now time.Time) (bool, error) {
|
||||||
_, err := FindBlockingMainPackageForFormal(tx, carrierType, carrierID, now)
|
_, err := FindCurrentMainPackageForQueue(tx, carrierType, carrierID, now)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,21 +212,6 @@ func (s *Service) validatePackageUsageRules(ctx context.Context, carrierType str
|
|||||||
return errors.New(errors.CodeMainPackageExists, "一次只能购买一个主套餐")
|
return errors.New(errors.CodeMainPackageExists, "一次只能购买一个主套餐")
|
||||||
}
|
}
|
||||||
|
|
||||||
if formalCount == 1 {
|
|
||||||
hasBlockingMain, err := packagepkg.HasBlockingMainPackageForFormal(
|
|
||||||
s.db.WithContext(ctx),
|
|
||||||
carrierType,
|
|
||||||
carrierID,
|
|
||||||
time.Now(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
|
|
||||||
}
|
|
||||||
if hasBlockingMain {
|
|
||||||
return errors.New(errors.CodeMainPackageExists)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasAddon {
|
if hasAddon {
|
||||||
if _, err := packagepkg.FindAttachableMainPackageForAddon(
|
if _, err := packagepkg.FindAttachableMainPackageForAddon(
|
||||||
s.db.WithContext(ctx),
|
s.db.WithContext(ctx),
|
||||||
|
|||||||
@@ -501,13 +501,10 @@ func (h *AutoPurchaseHandler) activateMainPackage(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hasBlockingMain, err := packagepkg.HasBlockingMainPackageForFormal(tx.WithContext(ctx), carrierType, carrierID, now)
|
hasCurrentMain, err := packagepkg.HasCurrentMainPackageForQueue(tx.WithContext(ctx), carrierType, carrierID, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if hasBlockingMain {
|
|
||||||
return errors.New("已有主套餐,不能重复购买主套餐")
|
|
||||||
}
|
|
||||||
|
|
||||||
var status int
|
var status int
|
||||||
var priority int
|
var priority int
|
||||||
@@ -516,11 +513,21 @@ func (h *AutoPurchaseHandler) activateMainPackage(
|
|||||||
var nextResetAt *time.Time
|
var nextResetAt *time.Time
|
||||||
var pendingRealnameActivation bool
|
var pendingRealnameActivation bool
|
||||||
|
|
||||||
|
if hasCurrentMain {
|
||||||
|
status = constants.PackageUsageStatusPending
|
||||||
|
var maxPriority int
|
||||||
|
tx.Model(&model.PackageUsage{}).
|
||||||
|
Where(carrierType+"_id = ?", carrierID).
|
||||||
|
Select("COALESCE(MAX(priority), 0)").
|
||||||
|
Scan(&maxPriority)
|
||||||
|
priority = maxPriority + 1
|
||||||
|
} else {
|
||||||
status = constants.PackageUsageStatusActive
|
status = constants.PackageUsageStatusActive
|
||||||
priority = 1
|
priority = 1
|
||||||
activatedAt = now
|
activatedAt = now
|
||||||
expiresAt = packagepkg.CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
expiresAt = packagepkg.CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
||||||
nextResetAt = packagepkg.CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
|
nextResetAt = packagepkg.CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
// REALNAME-02: 自动购包属于 C 端充值触发,不需要等实名(C 端已做前置实名检查)
|
// REALNAME-02: 自动购包属于 C 端充值触发,不需要等实名(C 端已做前置实名检查)
|
||||||
// ExpiryBase 仅影响后台囤货路径,此处无需判断
|
// ExpiryBase 仅影响后台囤货路径,此处无需判断
|
||||||
|
|||||||
@@ -184,10 +184,17 @@ account
|
|||||||
|
|
||||||
响应 data MUST 包含:`batch_no`、`package_code`、`success_count`、`failed_count`、`orders`、`failed_cards`。`orders` 每项 MUST 包含 `card_no`、`order_no`。`failed_cards` 每项 MUST 包含 `card_no`、`code`、`message`。
|
响应 data MUST 包含:`batch_no`、`package_code`、`success_count`、`failed_count`、`orders`、`failed_cards`。`orders` 每项 MUST 包含 `card_no`、`order_no`。`failed_cards` 每项 MUST 包含 `card_no`、`code`、`message`。
|
||||||
|
|
||||||
|
当任一卡购买失败时,外层 `code`/`msg` MUST 返回首条失败卡的错误码和原因,`data` MUST 保留批次明细;调用方 MUST 以 `success_count`、`failed_count`、`orders`、`failed_cards` 判断每张卡的处理结果,避免整批重试导致重复下单。
|
||||||
|
|
||||||
#### Scenario: 多卡部分成功
|
#### Scenario: 多卡部分成功
|
||||||
|
|
||||||
- **WHEN** 代理提交 3 张卡购买同一个套餐编码,其中 2 张成功、1 张失败
|
- **WHEN** 代理提交 3 张卡购买同一个套餐编码,其中 2 张成功、1 张失败
|
||||||
- **THEN** 系统返回 `success_count=2`、`failed_count=1`、2 条订单号和 1 条失败卡原因
|
- **THEN** 系统外层 `code` 和 `msg` 返回首条失败卡的错误码和原因,`data` 返回 `success_count=2`、`failed_count=1`、2 条订单号和 1 条失败卡原因
|
||||||
|
|
||||||
|
#### Scenario: 全部购买失败
|
||||||
|
|
||||||
|
- **WHEN** 代理提交的卡全部购买失败
|
||||||
|
- **THEN** 系统外层 `code` 和 `msg` 返回首条失败卡的错误码和原因,`data.failed_cards` 返回全部失败明细
|
||||||
|
|
||||||
#### Scenario: 单套餐编码限制
|
#### Scenario: 单套餐编码限制
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,22 @@ func SuccessWithMessage(c *fiber.Ctx, data any, message string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Business 返回带业务码的统一响应,并保留业务数据。
|
||||||
|
// 适用于批量接口需要返回失败明细但外层 code 不能为 0 的场景。
|
||||||
|
func Business(c *fiber.Ctx, code int, data any, message string) error {
|
||||||
|
if message == "" {
|
||||||
|
message = errors.GetMessage(code, "zh-CN")
|
||||||
|
}
|
||||||
|
err := c.JSON(Response{
|
||||||
|
Code: code,
|
||||||
|
Data: data,
|
||||||
|
Message: message,
|
||||||
|
Timestamp: time.Now().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
c.Set(fiber.HeaderContentType, "application/json; charset=utf-8")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// PaginationData 分页数据结构
|
// PaginationData 分页数据结构
|
||||||
type PaginationData struct {
|
type PaginationData struct {
|
||||||
Items any `json:"items"` // 数据列表
|
Items any `json:"items"` // 数据列表
|
||||||
|
|||||||
Reference in New Issue
Block a user