All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m17s
## 变更概述 将统一钱包系统拆分为代理钱包和卡钱包两个独立系统,实现数据表和代码层面的完全隔离。 ## 数据库变更 - 新增 6 张表:tb_agent_wallet、tb_agent_wallet_transaction、tb_agent_recharge_record、tb_card_wallet、tb_card_wallet_transaction、tb_card_recharge_record - 删除 3 张旧表:tb_wallet、tb_wallet_transaction、tb_recharge_record - 代理钱包:按 (shop_id, wallet_type) 唯一标识,支持主钱包和分佣钱包 - 卡钱包:按 (resource_type, resource_id) 唯一标识,支持物联网卡和设备 ## 代码变更 - Model 层:新增 AgentWallet、AgentWalletTransaction、AgentRechargeRecord、CardWallet、CardWalletTransaction、CardRechargeRecord 模型 - Store 层:新增 6 个独立 Store,支持事务、乐观锁、Redis 缓存 - Service 层:重构 commission_calculation、commission_withdrawal、order、recharge 等 8 个服务 - Bootstrap 层:更新 Store 和 Service 依赖注入 - 常量层:按钱包类型重新组织常量和 Redis Key 生成函数 ## 技术特性 - 乐观锁:使用 version 字段防止并发冲突 - 多租户:支持 shop_id_tag 和 enterprise_id_tag 过滤 - 事务管理:所有余额变动使用事务保证 ACID - 缓存策略:Cache-Aside 模式,余额变动后删除缓存 ## 业务影响 - 代理钱包和卡钱包业务完全隔离,互不影响 - 为独立监控、优化、扩展打下基础 - 提升代理钱包的稳定性和独立性 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
236 lines
6.2 KiB
Go
236 lines
6.2 KiB
Go
package iot_card
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
)
|
||
|
||
// StopResumeService 停复机服务
|
||
// 任务 24.2: 处理 IoT 卡的自动停机和复机逻辑
|
||
type StopResumeService struct {
|
||
db *gorm.DB
|
||
redis *redis.Client
|
||
iotCardStore *postgres.IotCardStore
|
||
gatewayClient *gateway.Client
|
||
logger *zap.Logger
|
||
|
||
// 重试配置
|
||
maxRetries int
|
||
retryInterval time.Duration
|
||
}
|
||
|
||
// NewStopResumeService 创建停复机服务
|
||
func NewStopResumeService(
|
||
db *gorm.DB,
|
||
redis *redis.Client,
|
||
iotCardStore *postgres.IotCardStore,
|
||
gatewayClient *gateway.Client,
|
||
logger *zap.Logger,
|
||
) *StopResumeService {
|
||
return &StopResumeService{
|
||
db: db,
|
||
redis: redis,
|
||
iotCardStore: iotCardStore,
|
||
gatewayClient: gatewayClient,
|
||
logger: logger,
|
||
maxRetries: 3, // 默认最多重试 3 次
|
||
retryInterval: 2 * time.Second, // 默认重试间隔 2 秒
|
||
}
|
||
}
|
||
|
||
// CheckAndStopCard 任务 24.3: 检查流量耗尽并停机
|
||
// 当所有套餐流量用完时,调用运营商接口停机
|
||
func (s *StopResumeService) CheckAndStopCard(ctx context.Context, cardID uint) error {
|
||
// 查询卡信息
|
||
card, err := s.iotCardStore.GetByID(ctx, cardID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 如果已经是停机状态,跳过
|
||
if card.NetworkStatus == constants.NetworkStatusOffline {
|
||
s.logger.Debug("卡已处于停机状态,跳过",
|
||
zap.Uint("card_id", cardID))
|
||
return nil
|
||
}
|
||
|
||
// 检查是否有可用套餐(status=1 生效中 或 status=0 待生效)
|
||
hasAvailablePackage, err := s.hasAvailablePackage(ctx, cardID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 如果还有可用套餐,不停机
|
||
if hasAvailablePackage {
|
||
return nil
|
||
}
|
||
|
||
// 任务 24.5: 调用运营商停机接口(带重试机制)
|
||
if err := s.stopCardWithRetry(ctx, card); err != nil {
|
||
s.logger.Error("调用运营商停机接口失败",
|
||
zap.Uint("card_id", cardID),
|
||
zap.String("iccid", card.ICCID),
|
||
zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
// 更新卡状态
|
||
now := time.Now()
|
||
if err := s.db.WithContext(ctx).Model(card).Updates(map[string]any{
|
||
"network_status": constants.NetworkStatusOffline,
|
||
"stopped_at": now,
|
||
"stop_reason": constants.StopReasonTrafficExhausted,
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
s.logger.Info("卡因流量耗尽已停机",
|
||
zap.Uint("card_id", cardID),
|
||
zap.String("iccid", card.ICCID))
|
||
|
||
return nil
|
||
}
|
||
|
||
// ResumeCardIfStopped 任务 24.4: 购买套餐后自动复机
|
||
// 当购买新套餐且卡之前因流量耗尽停机时,自动复机
|
||
func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, cardID uint) error {
|
||
// 查询卡信息
|
||
card, err := s.iotCardStore.GetByID(ctx, cardID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 幂等性检查:如果已经是开机状态,跳过
|
||
if card.NetworkStatus == constants.NetworkStatusOnline {
|
||
s.logger.Debug("卡已处于开机状态,跳过",
|
||
zap.Uint("card_id", cardID))
|
||
return nil
|
||
}
|
||
|
||
// 只有因流量耗尽停机的卡才自动复机
|
||
if card.StopReason != constants.StopReasonTrafficExhausted {
|
||
s.logger.Debug("卡非流量耗尽停机,不自动复机",
|
||
zap.Uint("card_id", cardID),
|
||
zap.String("stop_reason", card.StopReason))
|
||
return nil
|
||
}
|
||
|
||
// 任务 24.5: 调用运营商复机接口(带重试机制)
|
||
if err := s.resumeCardWithRetry(ctx, card); err != nil {
|
||
s.logger.Error("调用运营商复机接口失败",
|
||
zap.Uint("card_id", cardID),
|
||
zap.String("iccid", card.ICCID),
|
||
zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
// 更新卡状态
|
||
now := time.Now()
|
||
if err := s.db.WithContext(ctx).Model(card).Updates(map[string]any{
|
||
"network_status": constants.NetworkStatusOnline,
|
||
"resumed_at": now,
|
||
"stop_reason": "", // 清空停机原因
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
s.logger.Info("卡购买套餐后已自动复机",
|
||
zap.Uint("card_id", cardID),
|
||
zap.String("iccid", card.ICCID))
|
||
|
||
return nil
|
||
}
|
||
|
||
// hasAvailablePackage 检查是否有可用套餐
|
||
func (s *StopResumeService) hasAvailablePackage(ctx context.Context, cardID uint) (bool, error) {
|
||
var count int64
|
||
err := s.db.WithContext(ctx).Model(&model.PackageUsage{}).
|
||
Where("iot_card_id = ?", cardID).
|
||
Where("status IN ?", []int{
|
||
constants.PackageUsageStatusPending, // 待生效
|
||
constants.PackageUsageStatusActive, // 生效中
|
||
}).
|
||
Count(&count).Error
|
||
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
return count > 0, nil
|
||
}
|
||
|
||
// stopCardWithRetry 任务 24.5: 调用运营商停机接口(带重试机制)
|
||
func (s *StopResumeService) stopCardWithRetry(ctx context.Context, card *model.IotCard) error {
|
||
if s.gatewayClient == nil {
|
||
s.logger.Warn("Gateway 客户端未配置,跳过调用运营商接口",
|
||
zap.Uint("card_id", card.ID))
|
||
return nil
|
||
}
|
||
|
||
var lastErr error
|
||
for i := 0; i < s.maxRetries; i++ {
|
||
if i > 0 {
|
||
s.logger.Debug("重试调用停机接口",
|
||
zap.Int("attempt", i+1),
|
||
zap.String("iccid", card.ICCID))
|
||
time.Sleep(s.retryInterval)
|
||
}
|
||
|
||
err := s.gatewayClient.StopCard(ctx, &gateway.CardOperationReq{
|
||
CardNo: card.ICCID,
|
||
})
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
|
||
lastErr = err
|
||
s.logger.Warn("调用停机接口失败,准备重试",
|
||
zap.Int("attempt", i+1),
|
||
zap.Error(err))
|
||
}
|
||
|
||
return lastErr
|
||
}
|
||
|
||
// resumeCardWithRetry 任务 24.5: 调用运营商复机接口(带重试机制)
|
||
func (s *StopResumeService) resumeCardWithRetry(ctx context.Context, card *model.IotCard) error {
|
||
if s.gatewayClient == nil {
|
||
s.logger.Warn("Gateway 客户端未配置,跳过调用运营商接口",
|
||
zap.Uint("card_id", card.ID))
|
||
return nil
|
||
}
|
||
|
||
var lastErr error
|
||
for i := 0; i < s.maxRetries; i++ {
|
||
if i > 0 {
|
||
s.logger.Debug("重试调用复机接口",
|
||
zap.Int("attempt", i+1),
|
||
zap.String("iccid", card.ICCID))
|
||
time.Sleep(s.retryInterval)
|
||
}
|
||
|
||
err := s.gatewayClient.StartCard(ctx, &gateway.CardOperationReq{
|
||
CardNo: card.ICCID,
|
||
})
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
|
||
lastErr = err
|
||
s.logger.Warn("调用复机接口失败,准备重试",
|
||
zap.Int("attempt", i+1),
|
||
zap.Error(err))
|
||
}
|
||
|
||
return lastErr
|
||
}
|