367 lines
11 KiB
Go
367 lines
11 KiB
Go
package commands
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"math/rand"
|
||
"strconv"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/goravel/framework/contracts/console"
|
||
"github.com/goravel/framework/contracts/console/command"
|
||
"github.com/goravel/framework/facades"
|
||
"github.com/oklog/ulid/v2"
|
||
|
||
"goravel/app/models"
|
||
"goravel/app/services"
|
||
"goravel/app/utils"
|
||
"goravel/app/utils/errorlog"
|
||
"goravel/database/migrations"
|
||
)
|
||
|
||
// GenerateTestPayments 生成测试支付记录数据
|
||
type GenerateTestPayments struct {
|
||
paymentService services.PaymentService
|
||
}
|
||
|
||
// Signature The name and signature of the console command.
|
||
func (receiver *GenerateTestPayments) Signature() string {
|
||
return "payment:generate-test-data"
|
||
}
|
||
|
||
// # 使用默认10个并发协程
|
||
// go run . artisan payment:generate-test-data --count=1000000
|
||
|
||
// # 使用20个并发协程(更快)
|
||
// go run . artisan payment:generate-test-data --count=1000000 --workers=20
|
||
|
||
// # 根据服务器性能调整并发数
|
||
// go run . artisan payment:generate-test-data --count=1000000 --workers=50 --batch-size=2000
|
||
|
||
// Description The console command description.
|
||
func (receiver *GenerateTestPayments) Description() string {
|
||
return "生成支付记录测试数据(用于测试支付记录导出等功能)"
|
||
}
|
||
|
||
// Extend The console command extend.
|
||
func (receiver *GenerateTestPayments) Extend() command.Extend {
|
||
return command.Extend{
|
||
Category: "payment",
|
||
Flags: []command.Flag{
|
||
&command.IntFlag{
|
||
Name: "count",
|
||
Aliases: []string{"c"},
|
||
Value: 10000,
|
||
Usage: "生成的记录数量",
|
||
},
|
||
&command.IntFlag{
|
||
Name: "workers",
|
||
Aliases: []string{"w"},
|
||
Value: 10,
|
||
Usage: "并发协程数",
|
||
},
|
||
&command.IntFlag{
|
||
Name: "batch-size",
|
||
Aliases: []string{"b"},
|
||
Value: 1000,
|
||
Usage: "每个批次插入的记录数",
|
||
},
|
||
&command.StringFlag{
|
||
Name: "start-date",
|
||
Aliases: []string{"s"},
|
||
Value: "",
|
||
Usage: "开始日期(格式:2006-01-02),默认为3个月前",
|
||
},
|
||
&command.StringFlag{
|
||
Name: "end-date",
|
||
Aliases: []string{"e"},
|
||
Value: "",
|
||
Usage: "结束日期(格式:2006-01-02),默认为当前时间",
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
// Handle Execute the console command.
|
||
func (receiver *GenerateTestPayments) Handle(ctx console.Context) error {
|
||
count := ctx.OptionInt("count")
|
||
workers := ctx.OptionInt("workers")
|
||
batchSize := ctx.OptionInt("batch-size")
|
||
startDateStr := ctx.Option("start-date")
|
||
endDateStr := ctx.Option("end-date")
|
||
|
||
// 参数验证
|
||
if count <= 0 {
|
||
return fmt.Errorf("记录数量必须大于0")
|
||
}
|
||
if workers <= 0 {
|
||
return fmt.Errorf("并发协程数必须大于0")
|
||
}
|
||
if batchSize <= 0 {
|
||
return fmt.Errorf("批次大小必须大于0")
|
||
}
|
||
|
||
// 解析日期范围
|
||
var startDate, endDate time.Time
|
||
var err error
|
||
|
||
if startDateStr != "" {
|
||
startDate, err = time.ParseInLocation("2006-01-02", startDateStr, time.UTC)
|
||
if err != nil {
|
||
return fmt.Errorf("开始日期格式错误,应为 YYYY-MM-DD 格式: %v", err)
|
||
}
|
||
} else {
|
||
startDate = time.Now().UTC().AddDate(0, -3, 0) // 默认3个月前
|
||
}
|
||
|
||
if endDateStr != "" {
|
||
endDate, err = time.ParseInLocation("2006-01-02", endDateStr, time.UTC)
|
||
if err != nil {
|
||
return fmt.Errorf("结束日期格式错误,应为 YYYY-MM-DD 格式: %v", err)
|
||
}
|
||
} else {
|
||
endDate = time.Now().UTC() // 默认当前时间
|
||
}
|
||
|
||
if startDate.After(endDate) {
|
||
return fmt.Errorf("开始日期不能晚于结束日期")
|
||
}
|
||
|
||
ctx.Info(fmt.Sprintf("开始生成 %d 条支付记录测试数据", count))
|
||
ctx.Info(fmt.Sprintf("时间范围: %s 至 %s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02")))
|
||
ctx.Info(fmt.Sprintf("并发协程数: %d, 批次大小: %d", workers, batchSize))
|
||
|
||
// 获取支付方式列表
|
||
paymentMethods, err := receiver.getPaymentMethods()
|
||
if err != nil {
|
||
return fmt.Errorf("获取支付方式失败: %v", err)
|
||
}
|
||
|
||
if len(paymentMethods) == 0 {
|
||
return fmt.Errorf("没有找到支付方式,请先创建支付方式")
|
||
}
|
||
|
||
// 计算每个协程的工作量
|
||
totalBatches := (count + batchSize - 1) / batchSize
|
||
batchesPerWorker := totalBatches / workers
|
||
remainder := totalBatches % workers
|
||
|
||
startTime := time.Now()
|
||
var wg sync.WaitGroup
|
||
var mu sync.Mutex
|
||
var successCount int64
|
||
var errorCount int64
|
||
|
||
// 启动工作协程
|
||
for i := 0; i < workers; i++ {
|
||
wg.Add(1)
|
||
go func(workerID int) {
|
||
defer wg.Done()
|
||
|
||
// 计算当前协程的批次范围
|
||
batchStart := i * batchesPerWorker
|
||
batchEnd := batchStart + batchesPerWorker
|
||
if i < remainder {
|
||
batchEnd++
|
||
}
|
||
|
||
for batchID := batchStart; batchID < batchEnd; batchID++ {
|
||
// 计算当前批次的记录数
|
||
remainingCount := count - batchID*batchSize
|
||
currentBatchSize := batchSize
|
||
if remainingCount < batchSize {
|
||
currentBatchSize = remainingCount
|
||
}
|
||
|
||
if currentBatchSize <= 0 {
|
||
break
|
||
}
|
||
|
||
// 生成批次数据
|
||
err := receiver.generateBatch(currentBatchSize, startDate, endDate, paymentMethods)
|
||
mu.Lock()
|
||
if err != nil {
|
||
errorCount++
|
||
ctx.Error(fmt.Sprintf("Worker %d 批次 %d 失败: %v", workerID, batchID, err))
|
||
} else {
|
||
successCount += int64(currentBatchSize)
|
||
ctx.Info(fmt.Sprintf("Worker %d 批次 %d 完成,生成 %d 条记录", workerID, batchID, currentBatchSize))
|
||
}
|
||
mu.Unlock()
|
||
}
|
||
}(i)
|
||
}
|
||
|
||
wg.Wait()
|
||
|
||
// 输出结果
|
||
duration := time.Since(startTime)
|
||
ctx.Info(fmt.Sprintf("\n✅ 完成!"))
|
||
ctx.Info(fmt.Sprintf("总耗时: %v", duration))
|
||
ctx.Info(fmt.Sprintf("成功生成: %d 条记录", successCount))
|
||
ctx.Info(fmt.Sprintf("失败批次: %d", errorCount))
|
||
if successCount > 0 {
|
||
ctx.Info(fmt.Sprintf("平均速度: %.0f 条/秒", float64(successCount)/duration.Seconds()))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// getPaymentMethods 获取支付方式列表
|
||
func (receiver *GenerateTestPayments) getPaymentMethods() ([]models.PaymentMethod, error) {
|
||
var paymentMethods []models.PaymentMethod
|
||
err := facades.Orm().Query().Model(&models.PaymentMethod{}).Where("is_active", true).Find(&paymentMethods)
|
||
return paymentMethods, err
|
||
}
|
||
|
||
// generateBatch 生成一批支付记录
|
||
func (receiver *GenerateTestPayments) generateBatch(count int, startDate, endDate time.Time, paymentMethods []models.PaymentMethod) error {
|
||
payments := make([]models.Payment, 0, count)
|
||
|
||
for i := 0; i < count; i++ {
|
||
// 随机生成时间
|
||
randomTime := randomTimeInRange(startDate, endDate)
|
||
|
||
// 随机选择支付方式
|
||
paymentMethod := paymentMethods[rand.Intn(len(paymentMethods))]
|
||
|
||
// 随机生成订单号(模拟已存在的订单)
|
||
orderNo := fmt.Sprintf("ORD%s%s", randomTime.Format("20060102"), strconv.Itoa(rand.Intn(999999)))
|
||
|
||
// 随机生成用户ID
|
||
userID := uint(rand.Intn(10000) + 1)
|
||
|
||
// 随机生成金额(0.01-10000.00)
|
||
amount := float64(rand.Intn(1000000)+1) / 100
|
||
|
||
// 随机生成状态
|
||
statuses := []string{"pending", "paid", "failed", "cancelled"}
|
||
status := statuses[rand.Intn(len(statuses))]
|
||
|
||
// 生成支付单号
|
||
paymentNo := fmt.Sprintf("PAY%s%s", randomTime.Format("20060102"), ulid.Make().String())
|
||
|
||
payment := models.Payment{
|
||
PaymentNo: paymentNo,
|
||
OrderNo: orderNo,
|
||
PaymentMethodID: paymentMethod.ID,
|
||
UserID: userID,
|
||
Amount: amount,
|
||
Status: status,
|
||
}
|
||
|
||
// 根据状态设置其他字段
|
||
switch status {
|
||
case "paid":
|
||
payment.PayTime = &randomTime
|
||
payment.ThirdPartyNo = fmt.Sprintf("TXN%s%d", randomTime.Format("20060102150405"), rand.Intn(999999))
|
||
case "failed":
|
||
payment.FailReason = "支付超时"
|
||
}
|
||
|
||
payments = append(payments, payment)
|
||
}
|
||
|
||
// 确保分表存在(使用最早的时间)
|
||
tableName := utils.GetShardingTableName("payments", startDate)
|
||
if !facades.Schema().HasTable(tableName) {
|
||
if err := receiver.ensurePaymentShardingTableExists(startDate); err != nil {
|
||
return fmt.Errorf("创建分表失败: %v", err)
|
||
}
|
||
}
|
||
|
||
// 批量插入(按分表分组)
|
||
tableGroups := make(map[string][]models.Payment)
|
||
for _, payment := range payments {
|
||
// 从支付单号提取日期
|
||
if len(payment.PaymentNo) >= 11 {
|
||
dateStr := payment.PaymentNo[3:11] // 提取日期部分 YYYYMMDD
|
||
if parsedTime, err := time.Parse("20060102", dateStr); err == nil {
|
||
tableName := utils.GetShardingTableName("payments", parsedTime)
|
||
tableGroups[tableName] = append(tableGroups[tableName], payment)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分表插入
|
||
for tableName, tablePayments := range tableGroups {
|
||
// 确保分表存在
|
||
if !facades.Schema().HasTable(tableName) {
|
||
// 从表名解析月份
|
||
if len(tableName) > 8 {
|
||
monthStr := tableName[len(tableName)-6:] // 提取 YYYYMM
|
||
if parsedTime, err := time.Parse("200601", monthStr); err == nil {
|
||
if err := receiver.ensurePaymentShardingTableExists(parsedTime); err != nil {
|
||
return fmt.Errorf("创建分表 %s 失败: %v", tableName, err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 转换为 map 以便批量插入
|
||
paymentMaps := make([]map[string]any, len(tablePayments))
|
||
for i, payment := range tablePayments {
|
||
// 从支付单号解析创建时间
|
||
var createdAt time.Time
|
||
if len(payment.PaymentNo) >= 11 {
|
||
dateStr := payment.PaymentNo[3:11] // 提取日期部分 YYYYMMDD
|
||
if parsedTime, err := time.Parse("20060102", dateStr); err == nil {
|
||
// 添加随机时分秒
|
||
createdAt = parsedTime.Add(time.Duration(rand.Intn(86400)) * time.Second)
|
||
}
|
||
}
|
||
if createdAt.IsZero() {
|
||
createdAt = time.Now().UTC()
|
||
}
|
||
|
||
paymentMaps[i] = map[string]any{
|
||
"payment_no": payment.PaymentNo,
|
||
"order_no": payment.OrderNo,
|
||
"payment_method_id": payment.PaymentMethodID,
|
||
"user_id": payment.UserID,
|
||
"amount": payment.Amount,
|
||
"status": payment.Status,
|
||
"third_party_no": payment.ThirdPartyNo,
|
||
"pay_time": payment.PayTime,
|
||
"fail_reason": payment.FailReason,
|
||
"remark": payment.Remark,
|
||
"created_at": createdAt,
|
||
"updated_at": createdAt,
|
||
}
|
||
}
|
||
|
||
if err := facades.Orm().Query().Table(tableName).Create(paymentMaps); err != nil {
|
||
errorlog.Record(context.Background(), "payment", "批量插入支付记录失败", map[string]any{
|
||
"table_name": tableName,
|
||
"count": len(paymentMaps),
|
||
"error": err.Error(),
|
||
}, "批量插入支付记录失败: %v", err)
|
||
return fmt.Errorf("插入分表 %s 失败: %v", tableName, err)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// randomTimeInRange 生成指定时间范围内的随机时间
|
||
func randomTimeInRange(start, end time.Time) time.Time {
|
||
delta := end.Sub(start)
|
||
sec := rand.Int63n(int64(delta.Seconds()))
|
||
return start.Add(time.Duration(sec) * time.Second)
|
||
}
|
||
|
||
// ensurePaymentShardingTableExists 确保支付记录分表存在
|
||
func (receiver *GenerateTestPayments) ensurePaymentShardingTableExists(paymentTime time.Time) error {
|
||
tableName := utils.GetShardingTableName("payments", paymentTime)
|
||
|
||
if !facades.Schema().HasTable(tableName) {
|
||
// 使用迁移函数创建分表
|
||
if err := migrations.CreatePaymentsShardingTable(tableName); err != nil {
|
||
return fmt.Errorf("创建支付记录分表失败: %v", err)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|