Files
server/app/console/commands/generate_test_payments.go
T
2026-01-16 15:49:34 +08:00

367 lines
11 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 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
}