From 9ea2faef212cd43c3552e596ad8a8f8814ed7fa9 Mon Sep 17 00:00:00 2001 From: weizong song Date: Tue, 2 Dec 2025 00:31:01 +0800 Subject: [PATCH] up --- app/Console/Commands/FixOrderItemsPaidAt.php | 327 ++++++++++++++++++ ...pdate_order_items_add_paid_at_original.php | 33 ++ 2 files changed, 360 insertions(+) create mode 100644 app/Console/Commands/FixOrderItemsPaidAt.php create mode 100644 database/migrations/2025_12_02_002228_update_order_items_add_paid_at_original.php diff --git a/app/Console/Commands/FixOrderItemsPaidAt.php b/app/Console/Commands/FixOrderItemsPaidAt.php new file mode 100644 index 0000000..695da9d --- /dev/null +++ b/app/Console/Commands/FixOrderItemsPaidAt.php @@ -0,0 +1,327 @@ +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; + } +} + diff --git a/database/migrations/2025_12_02_002228_update_order_items_add_paid_at_original.php b/database/migrations/2025_12_02_002228_update_order_items_add_paid_at_original.php new file mode 100644 index 0000000..9dc5f90 --- /dev/null +++ b/database/migrations/2025_12_02_002228_update_order_items_add_paid_at_original.php @@ -0,0 +1,33 @@ +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"); + }); + } +} +