|
|
|
|
|
<?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;
|
|
|
|
|
|
$sentAt = microtime(true);
|
|
|
|
|
|
$result = (new CarRepository())->courseAppointment($course, $sign, $plate, $out, function ($request) {
|
|
|
|
|
|
$seconds = (int) floor($request['ts'] / 1000);
|
|
|
|
|
|
$this->line('本次请求 reqId:' . $request['reqId']);
|
|
|
|
|
|
$this->line('本次请求 ts(毫秒):' . sprintf('%.0f', $request['ts']));
|
|
|
|
|
|
$this->line('请求时间(北京时间):' . (new \DateTimeImmutable('@' . $seconds))
|
|
|
|
|
|
->setTimezone(new \DateTimeZone('Asia/Shanghai'))->format('Y-m-d H:i:s P'));
|
|
|
|
|
|
$this->line('请求时间(UTC):' . gmdate('Y-m-d H:i:s', $seconds) . ' UTC');
|
|
|
|
|
|
foreach (['enterTime', 'leaveTime', 'failureTime'] as $field) {
|
|
|
|
|
|
$this->line($field . ':' . $request[$field]);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
$this->line('请求及日志处理耗时(秒):' . number_format(microtime(true) - $sentAt, 3, '.', ''));
|
|
|
|
|
|
$this->output->writeln('本次接口返回:' . ((string) $out !== '' ? (string) $out : '(空响应)'), \Symfony\Component\Console\Output\OutputInterface::OUTPUT_RAW);
|
|
|
|
|
|
if ($result) {
|
|
|
|
|
|
$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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|