Files
junhong_cmp_fiber/internal/service/iot_card/stop_resume_service.go
huang 18daeae65a
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m17s
feat: 钱包系统分离 - 代理钱包与卡钱包完全隔离
## 变更概述
将统一钱包系统拆分为代理钱包和卡钱包两个独立系统,实现数据表和代码层面的完全隔离。

## 数据库变更
- 新增 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>
2026-02-25 09:51:00 +08:00

236 lines
6.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 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
}