From c40a4a00a0edba1358b17d7570c4312317957d4f Mon Sep 17 00:00:00 2001 From: lion <120344285@qq.com> Date: Fri, 30 Oct 2026 10:54:36 +0800 Subject: [PATCH] =?UTF-8?q?=E9=81=93=E9=97=B8=E9=A2=84=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RetryInvalidTimeCourseCarAppointments.php | 121 ++++++++++++++++++ app/Repositories/CarRepository.php | 4 +- ...ryInvalidTimeCourseCarAppointmentsTest.php | 34 +++++ 3 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/RetryInvalidTimeCourseCarAppointments.php create mode 100644 tests/Unit/RetryInvalidTimeCourseCarAppointmentsTest.php diff --git a/app/Console/Commands/RetryInvalidTimeCourseCarAppointments.php b/app/Console/Commands/RetryInvalidTimeCourseCarAppointments.php new file mode 100644 index 0000000..b38fe39 --- /dev/null +++ b/app/Console/Commands/RetryInvalidTimeCourseCarAppointments.php @@ -0,0 +1,121 @@ +option('course-id'); + if ($courseId !== null && (!ctype_digit((string) $courseId) || (int) $courseId < 1)) { + $this->error('course-id 必须为正整数'); + return self::FAILURE; + } + + // 同一服务器上禁止本命令并发执行;同步请求,不留下重复的待执行队列任务。 + $lock = fopen(storage_path('framework/retry_course_car_invalid_time.lock'), 'c'); + if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) { + if ($lock) { + fclose($lock); + } + $this->error('补发命令正在运行,或无法创建锁文件'); + return self::FAILURE; + } + + $seen = []; + $candidates = $success = $failed = 0; + try { + $logs = ThirdAppointmentLog::where('course_sign_id', '>', 0) + ->where('finally', 0) + ->where('plate_status', 2) + ->where('url', 'https://kp-open.keytop.cn/unite-api/api/find/ReserveSpace') + ->where('send_data', 'like', '%59:59:%') + ->orderBy('id')->lazyById(200); + + foreach ($logs as $log) { + if (!$this->hasInvalidTimes($log->send_data)) { + continue; + } + $plate = trim((string) $log->plate); + $key = $log->course_sign_id . ':' . $plate; + if ($plate === '' || isset($seen[$key])) { + continue; + } + $seen[$key] = true; + + $sign = CourseSign::with(['course', 'user'])->find($log->course_sign_id); + if (!$sign || (int) $sign->status !== 1 || !$sign->course || !$sign->user) { + continue; + } + $course = $sign->course; + if (($courseId !== null && (int) $course->id !== (int) $courseId) + || (int) $course->status !== 1 + || !$course->start_date || !$course->end_date + || substr($course->end_date, 0, 10) < date('Y-m-d')) { + continue; + } + // 不给已解绑/更换的旧车牌重新授权。 + $currentPlates = array_filter(array_map('trim', explode(',', (string) $sign->user->plate))); + if (!in_array($plate, $currentPlates, true)) { + continue; + } + // 每次请求前重新检查,历史成功记录也跳过,避免重新激活已取消的订单。 + if (ThirdAppointmentLog::where('course_sign_id', $sign->id) + ->where('plate', $plate)->where('finally', 1) + ->whereIn('plate_status', [1, 3])->exists()) { + continue; + } + + $candidates++; + $this->line("日志={$log->id} 报名={$sign->id} 课程={$course->id} 车牌={$plate} 结束日期={$course->end_date}"); + if (!$this->option('execute')) { + $this->line('原响应:' . (string) $log->response_data); + continue; + } + + // 仓库会记录新的请求、响应和第三方订单号;仅补发车位,不发送门禁或通知。 + $out = null; + if ((new CarRepository())->courseAppointment($course, $sign, $plate, $out)) { + $success++; + $this->info('补发成功'); + } else { + $failed++; + $this->error('补发失败,请查看该报名的新第三方预约日志'); + } + } + + $this->info("符合条件 {$candidates} 条;成功 {$success} 条;失败 {$failed} 条"); + if (!$this->option('execute')) { + $this->info('当前仅预览,添加 --execute 才会实际请求第三方接口'); + } + return $failed > 0 ? self::FAILURE : self::SUCCESS; + } finally { + flock($lock, LOCK_UN); + fclose($lock); + } + } + + protected function hasInvalidTimes($sendData) + { + $data = json_decode((string) $sendData, true); + if (!is_array($data)) { + return false; + } + // 匹配本次缺陷的两个完整字段,避免误选正常时间或其他接口失败。 + return isset($data['leaveTime'], $data['failureTime']) + && is_string($data['leaveTime']) && is_string($data['failureTime']) + && preg_match('/^\d{4}-\d{2}-\d{2} 59:59:58$/D', $data['leaveTime']) === 1 + && preg_match('/^\d{4}-\d{2}-\d{2} 59:59:59$/D', $data['failureTime']) === 1; + } +} diff --git a/app/Repositories/CarRepository.php b/app/Repositories/CarRepository.php index 9bbb6e0..ff16ab7 100755 --- a/app/Repositories/CarRepository.php +++ b/app/Repositories/CarRepository.php @@ -130,8 +130,8 @@ class CarRepository 'type' => 1, 'addrId' => '', 'enterTime' => $course->start_date . ' 00:00:00', - 'leaveTime' => $course->end_date . ' 59:59:58', - 'failureTime' => date('Y-m-d 59:59:59', strtotime($course->end_date)), + 'leaveTime' => $course->end_date . ' 23:59:58', + 'failureTime' => date('Y-m-d 23:59:59', strtotime($course->end_date)), 'flag' => 1, 'cardType' => 100, ]; diff --git a/tests/Unit/RetryInvalidTimeCourseCarAppointmentsTest.php b/tests/Unit/RetryInvalidTimeCourseCarAppointmentsTest.php new file mode 100644 index 0000000..5911e1e --- /dev/null +++ b/tests/Unit/RetryInvalidTimeCourseCarAppointmentsTest.php @@ -0,0 +1,34 @@ +hasInvalidTimes($payload); + } + }; + $this->assertSame($expected, $command->matches($payload)); + } + + public function payloads() + { + return [ + 'incident' => [json_encode(['leaveTime' => '2026-11-26 59:59:58', 'failureTime' => '2026-11-26 59:59:59']), true], + 'corrected' => [json_encode(['leaveTime' => '2026-11-26 23:59:58', 'failureTime' => '2026-11-26 23:59:59']), false], + 'only one wrong field' => [json_encode(['leaveTime' => '2026-11-26 59:59:58', 'failureTime' => '2026-11-26 23:59:59']), false], + 'missing fields' => ['{}', false], + 'unrelated occurrence' => [json_encode(['message' => '59:59:59']), false], + 'invalid json' => ['not-json', false], + 'invalid types' => [json_encode(['leaveTime' => [], 'failureTime' => []]), false], + ]; + } +}