You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

122 lines
5.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Console\Commands;
use App\Models\CourseSign;
use App\Models\ThirdAppointmentLog;
use App\Repositories\CarRepository;
use Illuminate\Console\Command;
class RetryInvalidTimeCourseCarAppointments extends Command
{
protected $signature = 'retry_course_car_invalid_time
{--execute : 实际补发,不指定时只预览}
{--course-id= : 仅处理指定课程ID}';
protected $description = '补发课程车位预约中携带59点错误时间的失败记录(默认预览)';
public function handle()
{
$courseId = $this->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;
}
}