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

2 weeks ago
<?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;
}
}