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>
225 lines
6.1 KiB
Go
225 lines
6.1 KiB
Go
//go:build ignore
|
||
// +build ignore
|
||
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"math/rand"
|
||
"os"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"gorm.io/driver/postgres"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/logger"
|
||
)
|
||
|
||
// IotCard 简化的卡模型
|
||
type IotCard struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
ICCID string `gorm:"column:iccid;uniqueIndex:idx_iot_card_iccid,where:deleted_at IS NULL"`
|
||
CardCategory string `gorm:"column:card_category;default:normal"`
|
||
CarrierID uint `gorm:"column:carrier_id"`
|
||
Status int `gorm:"column:status;default:1"`
|
||
ActivationStatus int `gorm:"column:activation_status;default:0"`
|
||
RealNameStatus int `gorm:"column:real_name_status;default:0"`
|
||
NetworkStatus int `gorm:"column:network_status;default:0"`
|
||
EnablePolling bool `gorm:"column:enable_polling;default:true"`
|
||
Creator uint `gorm:"column:creator"`
|
||
Updater uint `gorm:"column:updater"`
|
||
CreatedAt time.Time
|
||
UpdatedAt time.Time
|
||
DeletedAt *time.Time `gorm:"index"`
|
||
}
|
||
|
||
func (IotCard) TableName() string {
|
||
return "tb_iot_card"
|
||
}
|
||
|
||
var (
|
||
totalCards = flag.Int("total", 10000000, "要生成的卡数量")
|
||
batchSize = flag.Int("batch", 10000, "每批插入数量")
|
||
workers = flag.Int("workers", 10, "并行 worker 数量")
|
||
startICCID = flag.String("start", "898600000", "起始 ICCID 前缀(9位,总长度不超过20位)")
|
||
clearOld = flag.Bool("clear", false, "是否清空现有测试卡")
|
||
|
||
insertedCount int64
|
||
startTime time.Time
|
||
)
|
||
|
||
func main() {
|
||
flag.Parse()
|
||
|
||
fmt.Println("=== 生成测试卡数据 ===")
|
||
fmt.Printf("目标数量: %d 张\n", *totalCards)
|
||
fmt.Printf("批次大小: %d\n", *batchSize)
|
||
fmt.Printf("并行数: %d\n", *workers)
|
||
fmt.Println("")
|
||
|
||
// 连接数据库
|
||
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||
os.Getenv("JUNHONG_DATABASE_HOST"),
|
||
os.Getenv("JUNHONG_DATABASE_PORT"),
|
||
os.Getenv("JUNHONG_DATABASE_USER"),
|
||
os.Getenv("JUNHONG_DATABASE_PASSWORD"),
|
||
os.Getenv("JUNHONG_DATABASE_DBNAME"),
|
||
)
|
||
|
||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||
Logger: logger.Default.LogMode(logger.Silent),
|
||
})
|
||
if err != nil {
|
||
log.Fatalf("连接数据库失败: %v", err)
|
||
}
|
||
|
||
// 配置连接池
|
||
sqlDB, _ := db.DB()
|
||
sqlDB.SetMaxOpenConns(50)
|
||
sqlDB.SetMaxIdleConns(25)
|
||
|
||
fmt.Println("✓ 数据库连接成功")
|
||
|
||
// 检查现有卡数量
|
||
var existingCount int64
|
||
db.Model(&IotCard{}).Count(&existingCount)
|
||
fmt.Printf("现有卡数量: %d\n", existingCount)
|
||
|
||
if *clearOld {
|
||
fmt.Println("清空现有测试卡...")
|
||
// 只删除 ICCID 以 898600000 开头的测试卡
|
||
db.Exec("DELETE FROM tb_iot_card WHERE iccid LIKE '898600000%'")
|
||
fmt.Println("✓ 清空完成")
|
||
}
|
||
|
||
// 开始生成
|
||
startTime = time.Now()
|
||
ctx := context.Background()
|
||
|
||
// 创建任务通道
|
||
taskCh := make(chan int, *workers*2)
|
||
var wg sync.WaitGroup
|
||
|
||
// 启动 worker
|
||
for i := 0; i < *workers; i++ {
|
||
wg.Add(1)
|
||
go func(workerID int) {
|
||
defer wg.Done()
|
||
worker(ctx, db, workerID, taskCh)
|
||
}(i)
|
||
}
|
||
|
||
// 分发任务
|
||
batches := *totalCards / *batchSize
|
||
for i := 0; i < batches; i++ {
|
||
taskCh <- i
|
||
}
|
||
close(taskCh)
|
||
|
||
// 等待完成
|
||
wg.Wait()
|
||
|
||
elapsed := time.Since(startTime)
|
||
fmt.Println("")
|
||
fmt.Println("=== 生成完成 ===")
|
||
fmt.Printf("总插入: %d 张\n", atomic.LoadInt64(&insertedCount))
|
||
fmt.Printf("耗时: %v\n", elapsed)
|
||
fmt.Printf("速度: %.0f 张/秒\n", float64(atomic.LoadInt64(&insertedCount))/elapsed.Seconds())
|
||
|
||
// 验证
|
||
var finalCount int64
|
||
db.Model(&IotCard{}).Count(&finalCount)
|
||
fmt.Printf("数据库总卡数: %d\n", finalCount)
|
||
}
|
||
|
||
func worker(ctx context.Context, db *gorm.DB, workerID int, taskCh <-chan int) {
|
||
rng := rand.New(rand.NewSource(time.Now().UnixNano() + int64(workerID)))
|
||
|
||
for batchIndex := range taskCh {
|
||
cards := generateBatch(rng, *startICCID, batchIndex, *batchSize)
|
||
|
||
// 批量插入
|
||
err := db.WithContext(ctx).CreateInBatches(cards, 1000).Error
|
||
if err != nil {
|
||
log.Printf("Worker %d 插入失败: %v", workerID, err)
|
||
continue
|
||
}
|
||
|
||
count := atomic.AddInt64(&insertedCount, int64(len(cards)))
|
||
|
||
// 进度报告
|
||
if count%100000 == 0 {
|
||
elapsed := time.Since(startTime).Seconds()
|
||
speed := float64(count) / elapsed
|
||
eta := float64(*totalCards-int(count)) / speed
|
||
fmt.Printf("进度: %d/%d (%.1f%%) | 速度: %.0f/秒 | ETA: %.0f秒\n",
|
||
count, *totalCards, float64(count)*100/float64(*totalCards), speed, eta)
|
||
}
|
||
}
|
||
}
|
||
|
||
func generateBatch(rng *rand.Rand, iccidPrefix string, batchIndex int, size int) []IotCard {
|
||
cards := make([]IotCard, size)
|
||
now := time.Now()
|
||
|
||
for i := 0; i < size; i++ {
|
||
// 使用前缀 + 序号生成 ICCID(总长度 20 位)
|
||
// 例如: 898600000 (9位) + 00000000001 (11位) = 20 位
|
||
cardIndex := batchIndex*size + i
|
||
iccid := fmt.Sprintf("%s%011d", iccidPrefix, cardIndex)
|
||
|
||
// 随机分配状态(匹配轮询配置条件)
|
||
// 实名状态: 0=未实名, 1=实名中, 2=已实名
|
||
// 网络状态: 0=停机, 1=正常
|
||
// 配置匹配逻辑:
|
||
// - not_real_name: RealNameStatus == 0 或 1
|
||
// - real_name: RealNameStatus == 2 && NetworkStatus != 1
|
||
// - activated: RealNameStatus == 2 && NetworkStatus == 1
|
||
r := rng.Float64()
|
||
var realNameStatus, activationStatus, networkStatus int
|
||
if r < 0.10 {
|
||
// 10% 未实名 -> 匹配 not_real_name 配置
|
||
realNameStatus = 0
|
||
activationStatus = 0
|
||
networkStatus = 0
|
||
} else if r < 0.30 {
|
||
// 20% 已实名未激活 -> 匹配 real_name 配置
|
||
realNameStatus = 2
|
||
activationStatus = 0
|
||
networkStatus = 0
|
||
} else {
|
||
// 70% 已激活 -> 匹配 activated 配置(流量+套餐检查)
|
||
realNameStatus = 2
|
||
activationStatus = 1
|
||
networkStatus = 1
|
||
}
|
||
|
||
// 随机卡类型
|
||
cardCategory := "normal"
|
||
if rng.Float64() < 0.05 {
|
||
cardCategory = "industry"
|
||
}
|
||
|
||
cards[i] = IotCard{
|
||
ICCID: iccid,
|
||
CardCategory: cardCategory,
|
||
CarrierID: uint(rng.Intn(3) + 1), // 1-3 运营商
|
||
Status: 1,
|
||
ActivationStatus: activationStatus,
|
||
RealNameStatus: realNameStatus,
|
||
NetworkStatus: networkStatus,
|
||
EnablePolling: true,
|
||
Creator: 1,
|
||
Updater: 1,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
}
|
||
|
||
return cards
|
||
}
|