道闸预约

master
lion 2 weeks ago
parent 9bd60445fd
commit c40a4a00a0

@ -0,0 +1,121 @@
<?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;
}
}

@ -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,
];

@ -0,0 +1,34 @@
<?php
namespace Tests\Unit;
use App\Console\Commands\RetryInvalidTimeCourseCarAppointments;
use PHPUnit\Framework\TestCase;
class RetryInvalidTimeCourseCarAppointmentsTest extends TestCase
{
/** @dataProvider payloads */
public function test_only_the_incident_time_format_is_selected($payload, $expected)
{
$command = new class extends RetryInvalidTimeCourseCarAppointments {
public function matches($payload)
{
return $this->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],
];
}
}
Loading…
Cancel
Save