Files
junhong_cmp_fiber/internal/service/order/pre_activation.go
邹进 98017ac053
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m37s
feat: 分层轮询、重置日前加密与激活前流量结算
- 轮询配置新增套餐状态、资产状态、重置日前窗口与窗口内流量间隔(迁移 248),间隔三态:正数按间隔、0 不检查、空继承
- 配置按 priority, id 排序合并;在库与已销售无套餐未匹配时告警不入队
- 新增优先级整体重排、匹配预览、立即重算到期时间(异步只提前)、分层统计与重置日前加密统计接口
- 已切换日快照计量的卡流量按观测时间归属,激活前用量只记已结算
- 订单购买与实名激活补齐资产级激活锁,激活前对已切换卡结算一次流量,失败只告警不阻塞,结算不触发通道阈值判定
2026-10-24 17:06:38 +09:00

86 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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):
}
}
}