|
|
<?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->line('');
|
|
|
}
|
|
|
|
|
|
$this->info("开始" . ($isDryRun ? "预览" : "修复") . " {$serviceDateStr} 的订单项...");
|
|
|
$this->info("目标时间点:{$serviceDateEnd}");
|
|
|
|
|
|
// 查询需要修复的记录
|
|
|
// 条件:paid_at 的日期晚于 service_date,且不在同一月份
|
|
|
$items = OrderItems::where('service_date', $serviceDateStr)
|
|
|
->whereNotNull('paid_at')
|
|
|
->whereRaw("DATE(`paid_at`) > '{$serviceDateStr}'")
|
|
|
->whereRaw("DATE_FORMAT(`paid_at`, '%Y-%m') > DATE_FORMAT('{$serviceDateStr}', '%Y-%m')")
|
|
|
->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->line('');
|
|
|
$this->info("预览将要修复的记录:");
|
|
|
$this->line('');
|
|
|
}
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
try {
|
|
|
// 获取客户ID
|
|
|
$customerId = $item->order ? $item->order->customer_id : null;
|
|
|
|
|
|
if (!$customerId) {
|
|
|
if (!$isDryRun) {
|
|
|
$this->line('');
|
|
|
$this->warn("订单项 {$item->id} 没有关联的客户,跳过");
|
|
|
$bar->advance();
|
|
|
}
|
|
|
$skippedCount++;
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
// 计算在 service_date 时的客户余额
|
|
|
$balanceAtTime = $this->calculateCustomerBalanceAtTime($customerId, $serviceDateStr, $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->line('');
|
|
|
$this->warn("订单项 {$item->id} 没有找到关联的 balance 记录");
|
|
|
}
|
|
|
|
|
|
DB::commit();
|
|
|
$fixedCount++;
|
|
|
|
|
|
Log::info("修复订单项 {$item->id}:原 paid_at {$originalPaidAt} -> {$serviceDateEnd}");
|
|
|
} catch (\Exception $e) {
|
|
|
DB::rollBack();
|
|
|
$errorCount++;
|
|
|
$this->line('');
|
|
|
$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->line('');
|
|
|
$this->error("处理订单项 {$item->id} 时出错:" . $e->getMessage());
|
|
|
}
|
|
|
Log::error("处理订单项 {$item->id} 时出错:" . $e->getMessage());
|
|
|
}
|
|
|
|
|
|
if (!$isDryRun) {
|
|
|
$bar->advance();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!$isDryRun) {
|
|
|
$bar->finish();
|
|
|
}
|
|
|
$this->line('');
|
|
|
$this->line('');
|
|
|
|
|
|
// 试运行模式:显示详细预览信息
|
|
|
if ($isDryRun && !empty($previewData)) {
|
|
|
$this->info("=== 预览详情 ===");
|
|
|
$this->line('');
|
|
|
|
|
|
// 显示满足条件的记录
|
|
|
$willFix = array_filter($previewData, function($item) {
|
|
|
return !isset($item['skip_reason']);
|
|
|
});
|
|
|
|
|
|
if (!empty($willFix)) {
|
|
|
$this->info("满足条件将修复的记录(" . count($willFix) . " 条):");
|
|
|
$this->line('');
|
|
|
$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->line('');
|
|
|
}
|
|
|
|
|
|
// 显示跳过的记录
|
|
|
$willSkip = array_filter($previewData, function($item) {
|
|
|
return isset($item['skip_reason']);
|
|
|
});
|
|
|
|
|
|
if (!empty($willSkip)) {
|
|
|
$this->warn("不满足条件将跳过的记录(" . count($willSkip) . " 条):");
|
|
|
$this->line('');
|
|
|
$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->line('');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 输出统计信息
|
|
|
$this->info(($isDryRun ? "预览" : "修复") . "完成!");
|
|
|
$this->table(
|
|
|
['统计项', '数量'],
|
|
|
[
|
|
|
['总记录数', $totalCount],
|
|
|
['满足条件' . ($isDryRun ? '(将修复)' : '并修复'), $fixedCount],
|
|
|
['余额不足跳过', $skippedCount],
|
|
|
['处理失败', $errorCount],
|
|
|
]
|
|
|
);
|
|
|
|
|
|
if ($isDryRun) {
|
|
|
$this->line('');
|
|
|
$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;
|
|
|
}
|
|
|
}
|
|
|
|