weizong song 4 months ago
parent 2b7fe09f60
commit 9ea2faef21

@ -0,0 +1,327 @@
<?php
namespace App\Console\Commands;
use App\Models\Balance;
use App\Models\OrderItems;
use App\Models\Orders;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class FixOrderItemsPaidAt extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'order-items:fix-paid-at {date} {--dry-run : 试运行模式,只显示预览信息,不执行实际修复}';
/**
* The console command description.
*
* @var string
*/
protected $description = '修复指定日期 service_date 的 order_items 表中滞后于 service_date 的 paid_at 字段';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$date = $this->argument('date');
// 验证日期格式
try {
$serviceDate = Carbon::createFromFormat('Y-m-d', $date);
} catch (\Exception $e) {
$this->error("日期格式错误,请使用 Y-m-d 格式例如2025-12-01");
return 1;
}
$serviceDateStr = $serviceDate->format('Y-m-d');
$serviceDateEnd = $serviceDateStr . ' 23:59:59';
$isDryRun = $this->option('dry-run');
if ($isDryRun) {
$this->warn("=== 试运行模式:将只显示预览信息,不会执行实际修复 ===");
$this->newLine();
}
$this->info("开始" . ($isDryRun ? "预览" : "修复") . " {$serviceDateStr} 的订单项...");
$this->info("目标时间点:{$serviceDateEnd}");
// 查询需要修复的记录
$items = OrderItems::where('service_date', $serviceDateStr)
->whereNotNull('paid_at')
->whereRaw("DATE(`paid_at`) > '{$serviceDateStr}'")
->where('total', '>', 0)
->with(['order' => function ($query) {
$query->select('id', 'customer_id');
}])
->get();
$totalCount = $items->count();
$this->info("找到 {$totalCount} 条需要检查的记录");
if ($totalCount == 0) {
$this->info("没有需要修复的记录");
return 0;
}
$fixedCount = 0;
$skippedCount = 0;
$errorCount = 0;
$previewData = [];
if (!$isDryRun) {
$bar = $this->output->createProgressBar($totalCount);
$bar->start();
} else {
$this->newLine();
$this->info("预览将要修复的记录:");
$this->newLine();
}
foreach ($items as $item) {
try {
// 获取客户ID
$customerId = $item->order ? $item->order->customer_id : null;
if (!$customerId) {
if (!$isDryRun) {
$this->newLine();
$this->warn("订单项 {$item->id} 没有关联的客户,跳过");
$bar->advance();
}
$skippedCount++;
continue;
}
// 计算在 service_date 23:59:59 时的客户余额
$balanceAtTime = $this->calculateCustomerBalanceAtTime($customerId, $serviceDateEnd, $item->id);
// 判断是否满足修复条件
if ($balanceAtTime >= $item->total) {
// 满足修复条件
$originalPaidAt = $item->paid_at;
// 检查是否有关联的 balance 记录
$balanceRecord = Balance::where('belongs_type', OrderItems::class)
->where('belongs_id', $item->id)
->where('customer_id', $customerId)
->first();
if ($isDryRun) {
// 试运行模式:只收集预览信息
$previewData[] = [
'order_item_id' => $item->id,
'order_id' => $item->order_id,
'customer_id' => $customerId,
'service_date' => $item->service_date,
'total' => $item->total,
'balance_at_time' => $balanceAtTime,
'original_paid_at' => $originalPaidAt,
'new_paid_at' => $serviceDateEnd,
'has_balance_record' => $balanceRecord ? '是' : '否',
'balance_record_id' => $balanceRecord ? $balanceRecord->id : null,
];
$fixedCount++;
} else {
// 实际执行修复
DB::beginTransaction();
try {
// 更新 order_items
$item->update([
'paid_at' => $serviceDateEnd,
'paid_at_original' => $originalPaidAt
]);
// 更新关联的 balance 记录
if ($balanceRecord) {
$balanceRecord->update([
'created_at' => $serviceDateEnd
]);
} else {
$this->newLine();
$this->warn("订单项 {$item->id} 没有找到关联的 balance 记录");
}
DB::commit();
$fixedCount++;
Log::info("修复订单项 {$item->id}:原 paid_at {$originalPaidAt} -> {$serviceDateEnd}");
} catch (\Exception $e) {
DB::rollBack();
$errorCount++;
$this->newLine();
$this->error("修复订单项 {$item->id} 失败:" . $e->getMessage());
Log::error("修复订单项 {$item->id} 失败:" . $e->getMessage());
}
}
} else {
// 余额不足,不满足修复条件
if ($isDryRun) {
$previewData[] = [
'order_item_id' => $item->id,
'order_id' => $item->order_id,
'customer_id' => $customerId,
'service_date' => $item->service_date,
'total' => $item->total,
'balance_at_time' => $balanceAtTime,
'original_paid_at' => $item->paid_at,
'new_paid_at' => '不满足条件',
'has_balance_record' => '-',
'balance_record_id' => null,
'skip_reason' => '余额不足(余额:' . $balanceAtTime . ' < 金额:' . $item->total . '',
];
}
$skippedCount++;
}
} catch (\Exception $e) {
$errorCount++;
if (!$isDryRun) {
$this->newLine();
$this->error("处理订单项 {$item->id} 时出错:" . $e->getMessage());
}
Log::error("处理订单项 {$item->id} 时出错:" . $e->getMessage());
}
if (!$isDryRun) {
$bar->advance();
}
}
if (!$isDryRun) {
$bar->finish();
}
$this->newLine(2);
// 试运行模式:显示详细预览信息
if ($isDryRun && !empty($previewData)) {
$this->info("=== 预览详情 ===");
$this->newLine();
// 显示满足条件的记录
$willFix = array_filter($previewData, function($item) {
return !isset($item['skip_reason']);
});
if (!empty($willFix)) {
$this->info("满足条件将修复的记录(" . count($willFix) . " 条):");
$this->newLine();
$this->table(
['订单项ID', '订单ID', '客户ID', '服务日期', '金额', '当时余额', '原paid_at', '新paid_at', '有balance记录'],
array_map(function($item) {
return [
$item['order_item_id'],
$item['order_id'],
$item['customer_id'],
$item['service_date'],
$item['total'],
$item['balance_at_time'],
$item['original_paid_at'],
$item['new_paid_at'],
$item['has_balance_record'],
];
}, $willFix)
);
$this->newLine();
}
// 显示跳过的记录
$willSkip = array_filter($previewData, function($item) {
return isset($item['skip_reason']);
});
if (!empty($willSkip)) {
$this->warn("不满足条件将跳过的记录(" . count($willSkip) . " 条):");
$this->newLine();
$this->table(
['订单项ID', '订单ID', '客户ID', '服务日期', '金额', '当时余额', '原paid_at', '跳过原因'],
array_map(function($item) {
return [
$item['order_item_id'],
$item['order_id'],
$item['customer_id'],
$item['service_date'],
$item['total'],
$item['balance_at_time'],
$item['original_paid_at'],
$item['skip_reason'],
];
}, $willSkip)
);
$this->newLine();
}
}
// 输出统计信息
$this->info(($isDryRun ? "预览" : "修复") . "完成!");
$this->table(
['统计项', '数量'],
[
['总记录数', $totalCount],
['满足条件' . ($isDryRun ? '(将修复)' : '并修复'), $fixedCount],
['余额不足跳过', $skippedCount],
['处理失败', $errorCount],
]
);
if ($isDryRun) {
$this->newLine();
$this->comment("提示:这是试运行模式,没有执行实际修复。");
$this->comment("要执行实际修复,请运行命令时不加 --dry-run 参数。");
}
return 0;
}
/**
* 计算指定时间点的客户余额
*
* @param int $customerId
* @param string $datetime
* @param int $excludeOrderItemId 排除当前订单项(因为它在修复前的时间点还不存在)
* @return float
*/
private function calculateCustomerBalanceAtTime($customerId, $datetime, $excludeOrderItemId = null)
{
// 查询在该时间点之前的所有 balance 记录
$query = Balance::where('customer_id', $customerId)
->where('created_at', '<=', $datetime)
->whereNull('deleted_at');
// 排除当前订单项的 balance 记录(因为它在修复前的时间点还不存在)
// 需要排除 belongs_type 为 OrderItems 且 belongs_id 为当前订单项 ID 的记录
if ($excludeOrderItemId) {
$query->where(function ($q) use ($excludeOrderItemId) {
$q->where('belongs_type', '!=', OrderItems::class)
->orWhere(function ($subQ) use ($excludeOrderItemId) {
$subQ->where('belongs_type', OrderItems::class)
->where('belongs_id', '!=', $excludeOrderItemId);
});
});
}
// 累加所有 money 字段money 为正数表示充值/退款,负数表示扣款)
$balance = $query->sum('money');
return (float) $balance;
}
}

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateOrderItemsAddPaidAtOriginal extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table("order_items", function (Blueprint $table) {
$table->timestamp("paid_at_original")->nullable()->after("paid_at");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table("order_items", function (Blueprint $table) {
$table->dropColumn("paid_at_original");
});
}
}
Loading…
Cancel
Save