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; } }