All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m37s
- 轮询配置新增套餐状态、资产状态、重置日前窗口与窗口内流量间隔(迁移 248),间隔三态:正数按间隔、0 不检查、空继承 - 配置按 priority, id 排序合并;在库与已销售无套餐未匹配时告警不入队 - 新增优先级整体重排、匹配预览、立即重算到期时间(异步只提前)、分层统计与重置日前加密统计接口 - 已切换日快照计量的卡流量按观测时间归属,激活前用量只记已结算 - 订单购买与实名激活补齐资产级激活锁,激活前对已切换卡结算一次流量,失败只告警不阻塞,结算不触发通道阈值判定
86 lines
3.2 KiB
Go
86 lines
3.2 KiB
Go
package order
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
"go.uber.org/zap"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
)
|
||
|
||
// PreActivationSettler 在资产从无生效主套餐变为有生效主套餐之前做一次流量结算(尽力而为,不阻塞)。
|
||
type PreActivationSettler interface {
|
||
SettleBeforeActivation(ctx context.Context, carrierType string, carrierID uint)
|
||
}
|
||
|
||
// activationLockWait 是订单路径等待资产级激活锁的最长时间;超时照常继续(行锁与流量按观测时间归属兜底正确性)。
|
||
const activationLockWait = 5 * time.Second
|
||
|
||
// SetPreActivationSettler 注入激活前流量结算器;未注入时不做结算。
|
||
func (s *Service) SetPreActivationSettler(settler PreActivationSettler) {
|
||
s.preActivationSettler = settler
|
||
}
|
||
|
||
// prepareActivation 在订单激活事务开始前执行:抢资产级激活锁(与排队、指定、队首、实名激活共用同一把锁),
|
||
// 再做激活前流量结算。返回的释放函数须在激活事务提交或回滚后调用。外部网络调用不得放进数据库事务(ENG-TX-001)。
|
||
func (s *Service) prepareActivation(ctx context.Context, order *model.Order) func() {
|
||
carrierType, carrierID, ok := orderCarrier(order)
|
||
if !ok {
|
||
return func() {}
|
||
}
|
||
release := s.acquireActivationLock(ctx, carrierType, carrierID)
|
||
if s.preActivationSettler != nil {
|
||
s.preActivationSettler.SettleBeforeActivation(ctx, carrierType, carrierID)
|
||
}
|
||
return release
|
||
}
|
||
|
||
func orderCarrier(order *model.Order) (string, uint, bool) {
|
||
if order == nil {
|
||
return "", 0, false
|
||
}
|
||
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
|
||
return constants.AssetTypeIotCard, *order.IotCardID, true
|
||
}
|
||
if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
|
||
return constants.AssetTypeDevice, *order.DeviceID, true
|
||
}
|
||
return "", 0, false
|
||
}
|
||
|
||
// acquireActivationLock 在最长 activationLockWait 内抢锁;抢不到时记日志并返回空释放函数。
|
||
func (s *Service) acquireActivationLock(ctx context.Context, carrierType string, carrierID uint) func() {
|
||
if s.redis == nil {
|
||
return func() {}
|
||
}
|
||
key := constants.RedisPackageActivationLockKey(carrierType, carrierID)
|
||
token := uuid.NewString()
|
||
deadline := time.Now().Add(activationLockWait)
|
||
for {
|
||
locked, err := s.redis.SetNX(ctx, key, token, constants.PackageActivationLockSeconds*time.Second).Result()
|
||
if err == nil && locked {
|
||
return func() {
|
||
releaseCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second)
|
||
defer cancel()
|
||
// 只释放自己持有的锁,避免锁过期后误删他人的锁。
|
||
if current, getErr := s.redis.Get(releaseCtx, key).Result(); getErr == nil && current == token {
|
||
s.redis.Del(releaseCtx, key)
|
||
}
|
||
}
|
||
}
|
||
if err != nil || time.Now().After(deadline) {
|
||
s.logger.Warn("订单激活未能获取资产级激活锁,继续执行(行锁与流量按观测时间归属兜底)",
|
||
zap.String("carrier_type", carrierType), zap.Uint("carrier_id", carrierID), zap.Error(err))
|
||
return func() {}
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
return func() {}
|
||
case <-time.After(200 * time.Millisecond):
|
||
}
|
||
}
|
||
}
|