feat: 套餐系统升级 - Worker 重构、流量重置、文档与规范更新
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s

- 重构 Worker 启动流程,引入 bootstrap 模块统一管理依赖注入
- 实现套餐流量重置服务(日/月/年周期重置)
- 新增套餐激活排队、加油包绑定、囤货待实名激活逻辑
- 新增订单创建幂等性防重(Redis 业务键 + 分布式锁)
- 更新 AGENTS.md/CLAUDE.md:新增注释规范、幂等性规范,移除测试要求
- 添加套餐系统升级完整文档(API文档、使用指南、功能总结、运维指南)
- 归档 OpenSpec package-system-upgrade 变更,同步 specs 到主目录
- 新增 queue types 抽象和 Redis 常量定义
This commit is contained in:
2026-02-12 14:24:15 +08:00
parent 655c9ce7a6
commit c665f32976
51 changed files with 7289 additions and 424 deletions

View File

@@ -256,3 +256,21 @@ func RedisPollingInitProgressKey() string {
func RedisPackageActivationLockKey(carrierType string, carrierID uint) string {
return fmt.Sprintf("package:activation:lock:%s:%d", carrierType, carrierID)
}
// ========================================
// 订单幂等性相关 Redis Key
// ========================================
// RedisOrderIdempotencyKey 生成订单创建幂等性检测的 Redis 键
// 用途防止相同买家在短时间内对同一载体重复下单SETNX 快速拒绝)
// 过期时间3 分钟
func RedisOrderIdempotencyKey(businessKey string) string {
return fmt.Sprintf("order:idempotency:%s", businessKey)
}
// RedisOrderCreateLockKey 生成订单创建分布式锁的 Redis 键
// 用途:防止同一载体的订单创建并发执行
// 过期时间10 秒
func RedisOrderCreateLockKey(carrierType string, carrierID uint) string {
return fmt.Sprintf("order:create:lock:%s:%d", carrierType, carrierID)
}

View File

@@ -8,10 +8,6 @@ import (
"github.com/break/junhong_cmp_fiber/internal/gateway"
"github.com/break/junhong_cmp_fiber/internal/polling"
"github.com/break/junhong_cmp_fiber/internal/service/commission_calculation"
"github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/internal/task"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/storage"
@@ -25,9 +21,20 @@ type Handler struct {
storage *storage.Service
gatewayClient *gateway.Client
pollingCallback task.PollingCallback
workerResult *WorkerBootstrapResult
asynqClient *asynq.Client
}
func NewHandler(db *gorm.DB, redis *redis.Client, storageSvc *storage.Service, gatewayClient *gateway.Client, pollingCallback task.PollingCallback, logger *zap.Logger) *Handler {
func NewHandler(
db *gorm.DB,
redis *redis.Client,
storageSvc *storage.Service,
gatewayClient *gateway.Client,
pollingCallback task.PollingCallback,
workerResult *WorkerBootstrapResult,
asynqClient *asynq.Client,
logger *zap.Logger,
) *Handler {
return &Handler{
mux: asynq.NewServeMux(),
logger: logger,
@@ -36,6 +43,8 @@ func NewHandler(db *gorm.DB, redis *redis.Client, storageSvc *storage.Service, g
storage: storageSvc,
gatewayClient: gatewayClient,
pollingCallback: pollingCallback,
workerResult: workerResult,
asynqClient: asynqClient,
}
}
@@ -65,32 +74,55 @@ func (h *Handler) RegisterHandlers() *asynq.ServeMux {
}
func (h *Handler) registerIotCardImportHandler() {
importTaskStore := postgres.NewIotCardImportTaskStore(h.db, h.redis)
iotCardStore := postgres.NewIotCardStore(h.db, h.redis)
iotCardImportHandler := task.NewIotCardImportHandler(h.db, h.redis, importTaskStore, iotCardStore, h.storage, h.pollingCallback, h.logger)
iotCardImportHandler := task.NewIotCardImportHandler(
h.db,
h.redis,
h.workerResult.Stores.IotCardImportTask,
h.workerResult.Stores.IotCard,
h.storage,
h.pollingCallback,
h.logger,
)
h.mux.HandleFunc(constants.TaskTypeIotCardImport, iotCardImportHandler.HandleIotCardImport)
h.logger.Info("注册 IoT 卡导入任务处理器", zap.String("task_type", constants.TaskTypeIotCardImport))
}
func (h *Handler) registerDeviceImportHandler() {
importTaskStore := postgres.NewDeviceImportTaskStore(h.db, h.redis)
deviceStore := postgres.NewDeviceStore(h.db, h.redis)
bindingStore := postgres.NewDeviceSimBindingStore(h.db, h.redis)
iotCardStore := postgres.NewIotCardStore(h.db, h.redis)
deviceImportHandler := task.NewDeviceImportHandler(h.db, h.redis, importTaskStore, deviceStore, bindingStore, iotCardStore, h.storage, h.logger)
deviceImportHandler := task.NewDeviceImportHandler(
h.db,
h.redis,
h.workerResult.Stores.DeviceImportTask,
h.workerResult.Stores.Device,
h.workerResult.Stores.DeviceSimBinding,
h.workerResult.Stores.IotCard,
h.storage,
h.logger,
)
h.mux.HandleFunc(constants.TaskTypeDeviceImport, deviceImportHandler.HandleDeviceImport)
h.logger.Info("注册设备导入任务处理器", zap.String("task_type", constants.TaskTypeDeviceImport))
}
func (h *Handler) registerCommissionStatsHandlers() {
statsStore := postgres.NewShopSeriesCommissionStatsStore(h.db)
allocationStore := postgres.NewShopPackageAllocationStore(h.db)
updateHandler := task.NewCommissionStatsUpdateHandler(h.redis, statsStore, allocationStore, h.logger)
syncHandler := task.NewCommissionStatsSyncHandler(h.db, h.redis, statsStore, h.logger)
archiveHandler := task.NewCommissionStatsArchiveHandler(h.db, h.redis, statsStore, h.logger)
updateHandler := task.NewCommissionStatsUpdateHandler(
h.redis,
h.workerResult.Stores.ShopSeriesCommissionStats,
h.workerResult.Stores.ShopPackageAllocation,
h.logger,
)
syncHandler := task.NewCommissionStatsSyncHandler(
h.db,
h.redis,
h.workerResult.Stores.ShopSeriesCommissionStats,
h.logger,
)
archiveHandler := task.NewCommissionStatsArchiveHandler(
h.db,
h.redis,
h.workerResult.Stores.ShopSeriesCommissionStats,
h.logger,
)
h.mux.HandleFunc(constants.TaskTypeCommissionStatsUpdate, updateHandler.HandleCommissionStatsUpdate)
h.logger.Info("注册佣金统计更新任务处理器", zap.String("task_type", constants.TaskTypeCommissionStatsUpdate))
@@ -103,58 +135,23 @@ func (h *Handler) registerCommissionStatsHandlers() {
}
func (h *Handler) registerCommissionCalculationHandler() {
// 创建所有需要的 Store 实例
commissionRecordStore := postgres.NewCommissionRecordStore(h.db, h.redis)
shopStore := postgres.NewShopStore(h.db, h.redis)
shopPackageAllocationStore := postgres.NewShopPackageAllocationStore(h.db)
shopSeriesAllocationStore := postgres.NewShopSeriesAllocationStore(h.db)
packageSeriesStore := postgres.NewPackageSeriesStore(h.db)
iotCardStore := postgres.NewIotCardStore(h.db, h.redis)
deviceStore := postgres.NewDeviceStore(h.db, h.redis)
walletStore := postgres.NewWalletStore(h.db, h.redis)
walletTransactionStore := postgres.NewWalletTransactionStore(h.db, h.redis)
orderStore := postgres.NewOrderStore(h.db, h.redis)
orderItemStore := postgres.NewOrderItemStore(h.db, h.redis)
packageStore := postgres.NewPackageStore(h.db)
commissionStatsStore := postgres.NewShopSeriesCommissionStatsStore(h.db)
// 创建 commission_stats.Service
commissionStatsService := commission_stats.New(commissionStatsStore)
// 创建 commission_calculation.Service
commissionCalculationService := commission_calculation.New(
commissionCalculationHandler := task.NewCommissionCalculationHandler(
h.db,
commissionRecordStore,
shopStore,
shopPackageAllocationStore,
shopSeriesAllocationStore,
packageSeriesStore,
iotCardStore,
deviceStore,
walletStore,
walletTransactionStore,
orderStore,
orderItemStore,
packageStore,
commissionStatsStore,
commissionStatsService,
h.workerResult.Services.CommissionCalculation,
h.logger,
)
// 创建并注册 Handler
commissionCalculationHandler := task.NewCommissionCalculationHandler(h.db, commissionCalculationService, h.logger)
h.mux.HandleFunc(constants.TaskTypeCommission, commissionCalculationHandler.HandleCommissionCalculation)
h.logger.Info("注册佣金计算任务处理器", zap.String("task_type", constants.TaskTypeCommission))
}
// registerPollingHandlers 注册轮询任务处理器
func (h *Handler) registerPollingHandlers() {
// 创建套餐相关 Store 和 Service用于流量扣减
packageUsageStore := postgres.NewPackageUsageStore(h.db, h.redis)
packageUsageDailyRecordStore := postgres.NewPackageUsageDailyRecordStore(h.db, h.redis)
usageService := packagepkg.NewUsageService(h.db, h.redis, packageUsageStore, packageUsageDailyRecordStore, h.logger)
pollingHandler := task.NewPollingHandler(h.db, h.redis, h.gatewayClient, usageService, h.logger)
pollingHandler := task.NewPollingHandler(
h.db,
h.redis,
h.gatewayClient,
h.workerResult.Services.UsageService,
h.logger,
)
h.mux.HandleFunc(constants.TaskTypePollingRealname, pollingHandler.HandleRealnameCheck)
h.logger.Info("注册实名检查任务处理器", zap.String("task_type", constants.TaskTypePollingRealname))
@@ -166,45 +163,18 @@ func (h *Handler) registerPollingHandlers() {
h.logger.Info("注册套餐检查任务处理器", zap.String("task_type", constants.TaskTypePollingPackage))
}
// registerPackageActivationHandlers 注册套餐激活任务处理器
// 任务 22.6 和 23.6: 注册首次实名激活和排队激活任务 Handler
func (h *Handler) registerPackageActivationHandlers() {
// 创建套餐相关 Store 和 Service
packageUsageStore := postgres.NewPackageUsageStore(h.db, h.redis)
packageStore := postgres.NewPackageStore(h.db)
packageUsageDailyRecordStore := postgres.NewPackageUsageDailyRecordStore(h.db, h.redis)
activationService := packagepkg.NewActivationService(
h.db,
h.redis,
packageUsageStore,
packageStore,
packageUsageDailyRecordStore,
h.logger,
)
// 创建 Asynq 客户端用于任务提交
redisOpt := asynq.RedisClientOpt{
Addr: h.redis.Options().Addr,
Password: h.redis.Options().Password,
DB: h.redis.Options().DB,
}
queueClient := asynq.NewClient(redisOpt)
// 创建套餐激活处理器
packageActivationHandler := polling.NewPackageActivationHandler(
h.db,
h.redis,
queueClient,
activationService,
h.asynqClient,
h.workerResult.Services.ActivationService,
h.logger,
)
// 任务 22.6: 注册首次实名激活任务 Handler
h.mux.HandleFunc(constants.TaskTypePackageFirstActivation, packageActivationHandler.HandlePackageFirstActivation)
h.logger.Info("注册首次实名激活任务处理器", zap.String("task_type", constants.TaskTypePackageFirstActivation))
// 任务 23.6: 注册排队激活任务 Handler
h.mux.HandleFunc(constants.TaskTypePackageQueueActivation, packageActivationHandler.HandlePackageQueueActivation)
h.logger.Info("注册排队激活任务处理器", zap.String("task_type", constants.TaskTypePackageQueueActivation))
}

52
pkg/queue/types.go Normal file
View File

@@ -0,0 +1,52 @@
package queue
import (
"github.com/break/junhong_cmp_fiber/internal/service/commission_calculation"
"github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
)
// WorkerStores Worker 侧所有 Store 的集合
type WorkerStores struct {
IotCardImportTask *postgres.IotCardImportTaskStore
IotCard *postgres.IotCardStore
DeviceImportTask *postgres.DeviceImportTaskStore
Device *postgres.DeviceStore
DeviceSimBinding *postgres.DeviceSimBindingStore
ShopSeriesCommissionStats *postgres.ShopSeriesCommissionStatsStore
ShopPackageAllocation *postgres.ShopPackageAllocationStore
CommissionRecord *postgres.CommissionRecordStore
Shop *postgres.ShopStore
ShopSeriesAllocation *postgres.ShopSeriesAllocationStore
PackageSeries *postgres.PackageSeriesStore
Wallet *postgres.WalletStore
WalletTransaction *postgres.WalletTransactionStore
Order *postgres.OrderStore
OrderItem *postgres.OrderItemStore
Package *postgres.PackageStore
PackageUsage *postgres.PackageUsageStore
PackageUsageDailyRecord *postgres.PackageUsageDailyRecordStore
PollingAlertRule *postgres.PollingAlertRuleStore
PollingAlertHistory *postgres.PollingAlertHistoryStore
DataCleanupConfig *postgres.DataCleanupConfigStore
DataCleanupLog *postgres.DataCleanupLogStore
}
// WorkerServices Worker 侧所有 Service 的集合
type WorkerServices struct {
CommissionCalculation *commission_calculation.Service
CommissionStats *commission_stats.Service
UsageService *packagepkg.UsageService
ActivationService *packagepkg.ActivationService
ResetService *packagepkg.ResetService
AlertService *pollingSvc.AlertService
CleanupService *pollingSvc.CleanupService
}
// WorkerBootstrapResult Worker Bootstrap 结果
type WorkerBootstrapResult struct {
Stores *WorkerStores
Services *WorkerServices
}