init
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user