This commit is contained in:
Joe
2026-01-16 15:49:34 +08:00
commit 550d3e1f42
380 changed files with 62024 additions and 0 deletions
@@ -0,0 +1,366 @@
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
}