shjEightService = $shjEightService; } /** * SHJ-5 メイン処理を実行 * * 処理フロー(仕様SHJ-5-1準拠): * JOB1: 駐輪場の空き状況を取得する(SQL-1) * JOB1-STEP1: 空き状況判定(空き台数 > 0 → JOB2、空きなし → ステータスコメント設定) * JOB2: 空き待ち者の情報を取得する(SQL-2) * JOB2-STEP1: 取得件数判定(> 0 → JOB3、なし → ステータスコメント設定) * JOB3: 空き待ち者への通知/オペレーターキュー追加処理 * JOB4: SHJ-8 バッチ処理ログ作成 * * @return array 処理結果 */ public function executeParkVacancyNotification(): array { try { $startTime = now(); Log::info('SHJ-5 駐輪場空きチェック処理開始'); // 処理統計(グローバル集計用) $processedParksCount = 0; $vacantParksCount = 0; $totalWaitingUsers = 0; $globalMailSuccessCount = 0; $globalMailErrorCount = 0; $globalQueueSuccessCount = 0; $globalQueueErrorCount = 0; $errors = []; $job4LogCount = 0; // 【JOB1】駐輪場の空き状況を取得する(SQL-1) $parkVacancyList = $this->getParkVacancyStatus(); Log::info('駐輪場空き状況取得完了', [ 'total_parks' => count($parkVacancyList) ]); // 各駐輪場に対する処理 foreach ($parkVacancyList as $parkVacancyData) { $parkVacancy = (object) $parkVacancyData; $processedParksCount++; Log::info('駐輪場処理開始', [ 'park_id' => $parkVacancy->park_id, 'park_name' => $parkVacancy->park_name, 'psection_id' => $parkVacancy->psection_id, 'ptype_id' => $parkVacancy->ptype_id, 'vacant_count' => $parkVacancy->vacant_count ]); $statusCommentNameBase = sprintf( '%s/%s/%s', (string) $parkVacancy->park_name, (string) $parkVacancy->ptype_subject, (string) $parkVacancy->psection_subject ); $statusCommentVacancyBase = $statusCommentNameBase . '/空き台数' . (int) $parkVacancy->vacant_count; // 【JOB1-STEP1】空き状況判定 if ($parkVacancy->vacant_count < 1) { // 仕様準拠:空きなしはJOB4へ(ステータスコメントは駐輪場/分類/車種のみ) $this->createBatchProcessLog('success', $statusCommentNameBase); $job4LogCount++; Log::info('空きなし', ['park_id' => $parkVacancy->park_id]); continue; } $vacantParksCount++; // 【JOB2】空き待ち者の情報を取得する(SQL-2) $waitingUsers = $this->getWaitingUsersInfo( $parkVacancy->park_id, $parkVacancy->psection_id, $parkVacancy->ptype_id ); // 【JOB2-STEP1】取得件数判定 if (empty($waitingUsers)) { // 仕様準拠:空き待ち該当者なしはJOB4へ $this->createBatchProcessLog('success', $statusCommentVacancyBase . ':空き待ち該当者なし'); $job4LogCount++; Log::info('空き待ち者なし', ['park_id' => $parkVacancy->park_id]); continue; } $totalWaitingUsers += count($waitingUsers); Log::info('空き待ち者情報取得完了', [ 'park_id' => $parkVacancy->park_id, 'waiting_users_count' => count($waitingUsers) ]); // 【JOB3】空き待ち者への通知、またはオペレーターキュー追加処理 $notificationResult = $this->processWaitingUsersNotification( $waitingUsers, $parkVacancy ); // グローバル集計 $globalMailSuccessCount += $notificationResult['mail_success_count']; $globalMailErrorCount += $notificationResult['mail_error_count']; $globalQueueSuccessCount += $notificationResult['queue_success_count']; $globalQueueErrorCount += $notificationResult['queue_error_count']; if (!empty($notificationResult['errors'])) { $errors = array_merge($errors, $notificationResult['errors']); } $reserveIdText = !empty($notificationResult['processed_reserve_ids']) ? implode('、', $notificationResult['processed_reserve_ids']) : '-'; $batchComment = (string) ($notificationResult['batch_comment'] ?? ''); // 仕様書記載の形式に合わせたステータスコメント $statusComment = $statusCommentVacancyBase . '/対象予約ID ' . $reserveIdText . '/' . $batchComment . ':メール正常終了件数' . $notificationResult['mail_success_count'] . '、メール異常終了件数' . $notificationResult['mail_error_count'] . '、キュー登録正常終了件数' . $notificationResult['queue_success_count'] . '、キュー登録異常終了件数' . $notificationResult['queue_error_count']; $status = ($notificationResult['mail_error_count'] > 0 || $notificationResult['queue_error_count'] > 0) ? 'error' : 'success'; $this->createBatchProcessLog($status, $statusComment); $job4LogCount++; Log::info('駐輪場処理完了', [ 'park_id' => $parkVacancy->park_id, 'mail_success_count' => $notificationResult['mail_success_count'], 'mail_error_count' => $notificationResult['mail_error_count'], 'queue_success_count' => $notificationResult['queue_success_count'], 'queue_error_count' => $notificationResult['queue_error_count'], ]); } // JOB1対象が0件の場合もJOB4ログを残す if ($processedParksCount === 0) { $this->createBatchProcessLog('success', '対象駐輪場データなし'); $job4LogCount++; } $endTime = now(); $duration = $startTime->diffInSeconds($endTime); Log::info('SHJ-5 駐輪場空きチェック処理完了', [ 'duration_seconds' => $duration, 'processed_parks_count' => $processedParksCount, 'vacant_parks_count' => $vacantParksCount, 'total_waiting_users' => $totalWaitingUsers, 'total_error_count' => count($errors) ]); return [ 'success' => true, 'message' => 'SHJ-5 駐輪場空きチェック処理が正常に完了しました', 'processed_parks_count' => $processedParksCount, 'vacant_parks_count' => $vacantParksCount, 'total_waiting_users' => $totalWaitingUsers, 'notification_success_count' => $globalMailSuccessCount, 'operator_queue_count' => $globalQueueSuccessCount, 'mail_error_count' => $globalMailErrorCount, 'queue_error_count' => $globalQueueErrorCount, 'error_count' => count($errors), 'errors' => $errors, 'duration_seconds' => $duration, 'job4_log_count' => $job4LogCount, 'status_comment' => sprintf( '処理対象駐輪場数:%d/空きあり駐輪場数:%d/メール正常:%d/メール異常:%d/キュー正常:%d/キュー異常:%d', $processedParksCount, $vacantParksCount, $globalMailSuccessCount, $globalMailErrorCount, $globalQueueSuccessCount, $globalQueueErrorCount ) ]; } catch (Exception $e) { Log::error('SHJ-5 駐輪場空きチェック処理でエラーが発生', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); // 異常時もJOB4(error)を出力する $this->createBatchProcessLog('error', 'SHJ-5処理エラー:' . $e->getMessage()); return [ 'success' => false, 'message' => 'SHJ-5 駐輪場空きチェック処理でエラーが発生: ' . $e->getMessage(), 'error_details' => $e->getMessage() ]; } } /** * 【JOB1】駐輪場の空き状況を取得する * * 仕様書に基づくSQL(SQL-1): * - 「駐輪場マスタ」と「ゾーンマスタ」より空き状況を取得する * - zone表から zone_number(現在契約台数)、zone_tolerance(限界収容台数)を取得 * - 空き台数 = 限界収容台数 - 現在契約台数 * * @return array 駐輪場空き状況リスト(全レコード、フィルターなし) */ private function getParkVacancyStatus(): array { try { // 仕様書SQL-1に基づくクエリ:park + zone + ptype + psection $vacancyList = DB::table('park as T1') ->select([ 'T1.park_id', 'T1.park_name', 'T1.park_ruby', 'T2.ptype_id', 'T3.ptype_subject', 'T2.psection_id', 'T4.psection_subject', DB::raw('sum(T2.zone_number) as sum_zone_number'), // 現在契約台数 DB::raw('sum(T2.zone_standard) as sum_zone_standard'), // 標準収容台数 DB::raw('sum(T2.zone_tolerance) as sum_zone_tolerance') // 限界収容台数 ]) ->join('zone as T2', 'T1.park_id', '=', 'T2.park_id') ->join('ptype as T3', 'T2.ptype_id', '=', 'T3.ptype_id') ->join('psection as T4', 'T2.psection_id', '=', 'T4.psection_id') ->where([ ['T1.park_close_flag', '=', 0], // 駐輪場開設 ['T2.delete_flag', '=', 0], // ゾーン有効 ]) ->groupBy(['T1.park_id', 'T2.ptype_id', 'T2.psection_id']) ->orderBy('T1.park_ruby') ->orderBy('T3.floor_sort') ->orderBy('T2.psection_id') ->get() ->map(function($record) { // 【JOB1-STEP1】空き台数 = 限界収容台数 - 現在契約台数 $vacant_count = (int) $record->sum_zone_tolerance - (int) $record->sum_zone_number; return (object)[ 'park_id' => $record->park_id, 'park_name' => $record->park_name, 'park_ruby' => $record->park_ruby, 'ptype_id' => $record->ptype_id, 'ptype_subject' => $record->ptype_subject, 'psection_id' => $record->psection_id, 'psection_subject' => $record->psection_subject, 'sum_zone_number' => $record->sum_zone_number, // JOB1 現在契約台数 'sum_zone_standard' => $record->sum_zone_standard, // JOB1 標準収容台数 'sum_zone_tolerance' => $record->sum_zone_tolerance, // JOB1 限界収容台数 'vacant_count' => $vacant_count, // 内部変数.空き台数 ]; }) ->values(); Log::info('駐輪場空き状況取得完了(仕様書SQL-1準拠)', [ 'total_records' => count($vacancyList) ]); return $vacancyList->toArray(); } catch (Exception $e) { Log::error('駐輪場空き状況取得エラー', [ 'error' => $e->getMessage() ]); throw $e; } } /** * 【JOB2】空き待ち者の情報を取得する * * 仕様書JOB2に基づくSQL(SQL-2): * - 空きが発生している「駐輪場ID」「駐輪分類ID」「車種区分ID」で空き待ちしている利用者を抽出する * - reserve表 + user表のみ(仕様書準拠) * - JOIN条件:T1.user_id = T2.user_seq(重要!) * * @param int $parkId 駐輪場ID(JOB1.駐輪場ID) * @param int $psectionId 車種区分ID(JOB1.車種区分ID) * @param int $ptypeId 駐輪分類ID(JOB1.駐輪分類ID) * @return array 空き待ち者情報リスト */ private function getWaitingUsersInfo(int $parkId, int $psectionId, int $ptypeId): array { try { // 仕様書JOB2 SQL準拠:reserve + user のみ $waitingUsers = DB::table('reserve as T1') ->select([ 'T1.user_id', // 利用者ID 'T1.reserve_id', // 定期予約ID 'T2.user_name', // 利用者名 'T2.user_manual_regist_flag', // 手動登録フラグ 'T2.user_primemail', // メールアドレス 'T2.user_submail', // 予備メールアドレス 'T1.reserve_manual', // 手動通知 ]) // 仕様書準拠:T1.user_id = T2.user_seq(user表のPK) ->join('user as T2', 'T1.user_id', '=', 'T2.user_seq') ->where([ ['T1.park_id', '=', $parkId], // JOB1.駐輪場ID ['T1.psection_id', '=', $psectionId], // JOB1.車種区分ID ['T1.ptype_id', '=', $ptypeId], // JOB1.駐輪分類ID ['T1.valid_flag', '=', 1], // 有効フラグ = 1 ['T2.user_quit_flag', '<>', 1] // 退会フラグ <> 1 ]) ->whereNull('T1.contract_id') // 定期契約 is null ->orderBy('T1.reserve_date', 'asc') // 予約日時順 ->get() ->toArray(); Log::info('空き待ち者情報取得完了(仕様書JOB2準拠)', [ 'park_id' => $parkId, 'psection_id' => $psectionId, 'ptype_id' => $ptypeId, 'waiting_users_count' => count($waitingUsers) ]); return $waitingUsers; } catch (Exception $e) { Log::error('空き待ち者情報取得エラー', [ 'park_id' => $parkId, 'psection_id' => $psectionId, 'ptype_id' => $ptypeId, 'error' => $e->getMessage() ]); throw $e; } } /** * 【JOB3】空き待ち者への通知、またはオペレーターキュー追加処理 * * 仕様SHJ-5-1準拠の分岐処理: * - 手動通知 <> 1: SHJ-7メール送信 → 成功時SQL-3更新 / 失敗時はカウント+バッチコメントのみ * - 手動通知 = 1: SQL-4オペレーターキュー登録 * * @param array $waitingUsers 空き待ち者リスト * @param object $parkVacancy 駐輪場空き情報 * @return array 通知処理結果(駐輪場毎の統計) */ private function processWaitingUsersNotification(array $waitingUsers, object $parkVacancy): array { // 仕様準拠:駐輪場毎のカウント $mailSuccessCount = 0; $mailErrorCount = 0; $queueSuccessCount = 0; $queueErrorCount = 0; $errors = []; $processedReserveIds = []; // 仕様:内部変数.対象予約ID $batchComments = []; // 仕様:内部変数.バッチコメント $queuedIds = []; // SQL-4で登録したキューID try { // 空きがある分だけ処理(先着順) $availableSpots = min($parkVacancy->vacant_count, count($waitingUsers)); for ($i = 0; $i < $availableSpots; $i++) { $waitingUser = (object) $waitingUsers[$i]; $processedReserveIds[] = $waitingUser->reserve_id; try { // 【仕様判断】手動通知フラグチェック if ($waitingUser->reserve_manual == 1) { // 手動通知 = 1 → SQL-4 オペレーターキュー登録 $queueResult = $this->addToOperatorQueue($waitingUser, $parkVacancy); if ($queueResult['success']) { // 仕様:キュー登録正常終了件数+1 $queueSuccessCount++; if (!empty($queueResult['que_id'])) { $queuedIds[] = (int) $queueResult['que_id']; } Log::info('オペレーターキュー登録成功', [ 'user_id' => $waitingUser->user_id, 'reserve_id' => $waitingUser->reserve_id ]); } else { // 仕様:キュー登録異常終了件数+1 → バッチコメント設定 $queueErrorCount++; $errorMsg = $queueResult['error'] ?? 'キュー登録エラー'; $batchComments[] = '予約ID' . $waitingUser->reserve_id . ':' . $errorMsg; $errors[] = $errorMsg; } } else { // 手動通知 <> 1 → SHJ-7 メール送信 $mailResult = $this->sendVacancyNotificationMail($waitingUser, $parkVacancy); if ($mailResult['success']) { // 仕様:メール送信成功 → SQL-3 reserve更新 → メール正常終了件数+1 $this->updateReserveSentDate($waitingUser->reserve_id); $mailSuccessCount++; Log::info('空き待ち通知メール送信成功', [ 'user_id' => $waitingUser->user_id, 'reserve_id' => $waitingUser->reserve_id, 'park_id' => $parkVacancy->park_id ]); } else { // 仕様:メール送信失敗 → メール異常終了件数+1 → バッチコメント設定 → 次のユーザーへ // ※オペレーターキューは作成しない(仕様準拠) $mailErrorCount++; $errorMsg = $mailResult['error'] ?? $mailResult['message'] ?? 'SHJ-7メール送信エラー'; $batchComments[] = '予約ID' . $waitingUser->reserve_id . ':' . $errorMsg; $errors[] = $errorMsg; } } } catch (Exception $e) { Log::error('空き待ち者通知処理エラー', [ 'user_id' => $waitingUser->user_id, 'reserve_id' => $waitingUser->reserve_id, 'error' => $e->getMessage() ]); // 仕様準拠:分岐に応じて異常件数を加算 if ((int) $waitingUser->reserve_manual === 1) { $queueErrorCount++; } else { $mailErrorCount++; } $batchComments[] = '予約ID' . $waitingUser->reserve_id . ':システムエラー:' . $e->getMessage(); $errors[] = $e->getMessage(); } } // SQL関連仕様準拠:que_status_commentに集計情報を反映 if (!empty($queuedIds)) { $reserveIdText = !empty($processedReserveIds) ? implode('、', $processedReserveIds) : '-'; $batchCommentText = implode('、', $batchComments); $queueStatusComment = sprintf( '%s/%s/%s/空き台数%d/対象予約ID %s/%s:メール正常終了件数%d、メール異常終了件数%d、キュー登録正常終了件数%d、キュー登録異常終了件数%d', (string) $parkVacancy->park_name, (string) $parkVacancy->ptype_subject, (string) $parkVacancy->psection_subject, (int) $parkVacancy->vacant_count, $reserveIdText, $batchCommentText, $mailSuccessCount, $mailErrorCount, $queueSuccessCount, $queueErrorCount ); DB::table('operator_que') ->whereIn('que_id', $queuedIds) ->update([ 'que_status_comment' => mb_strimwidth($queueStatusComment, 0, 255, ''), 'work_instructions' => '空き待ち者への連絡をお願いします。', 'updated_at' => now(), ]); } return [ 'mail_success_count' => $mailSuccessCount, 'mail_error_count' => $mailErrorCount, 'queue_success_count' => $queueSuccessCount, 'queue_error_count' => $queueErrorCount, 'processed_reserve_ids' => $processedReserveIds, 'batch_comment' => implode('、', $batchComments), 'errors' => $errors, ]; } catch (Exception $e) { Log::error('空き待ち者通知処理全体エラー', [ 'park_id' => $parkVacancy->park_id, 'error' => $e->getMessage() ]); throw $e; } } /** * 空き待ち通知メールを送信 * * 仕様書JOB3に基づくSHJ-7呼び出し: * - パラメータ1: JOB1.メールアドレス(user_primemail) * - パラメータ2: JOB1.予備メールアドレス(user_submail) * - パラメータ3: メールID = 201 * - 追加: reserve_id=%reserve_id%&expiry=%expiry%(予約ID と 無効日) * * @param object $waitingUser 空き待ち者情報 * @param object $parkVacancy 駐輪場空き情報 * @return array 送信結果 */ private function sendVacancyNotificationMail(object $waitingUser, object $parkVacancy): array { try { // ShjMailSendServiceを利用してメール送信 $mailService = app(ShjMailSendService::class); // 仕様書JOB3準拠:メールID = 201 $mailTemplateId = 201; // 仕様書No1/No2に基づく主メール・副メール設定 $mainEmail = $waitingUser->user_primemail ?? ''; $subEmail = $waitingUser->user_submail ?? ''; // 仕様書準拠:reserve_id と expiry(当月末日、休日考慮しない)を追加 $expiry = now()->endOfMonth()->format('Y-m-d'); // 当月末日 $additionalParams = [ 'reserve_id' => $waitingUser->reserve_id, 'expiry' => $expiry ]; // メール送信実行(仕様書JOB3準拠) $mailResult = $mailService->executeMailSend( $mainEmail, $subEmail, $mailTemplateId, $additionalParams ); // SHJ-7の結果を標準形式に変換(result: 0=成功, 1=失敗) $success = ($mailResult['result'] ?? 1) === 0; Log::info('空き待ち通知メール送信試行完了', [ 'user_id' => $waitingUser->user_id, 'main_email' => $mainEmail, 'sub_email' => $subEmail, 'mail_template_id' => $mailTemplateId, 'result' => $mailResult['result'] ?? 1, 'success' => $success ]); return [ 'success' => $success, 'result' => $mailResult['result'] ?? 1, 'error' => $mailResult['error_info'] ?? null, 'message' => $success ? 'メール送信成功' : ($mailResult['error_info'] ?? 'メール送信失敗') ]; } catch (Exception $e) { Log::error('空き待ち通知メール送信エラー', [ 'user_id' => $waitingUser->user_id, 'main_email' => $waitingUser->user_primemail ?? '', 'sub_email' => $waitingUser->user_submail ?? '', 'error' => $e->getMessage() ]); return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * SQL-3: reserve.sent_date及びvalid_flag更新 * * 仕様書準拠:メール送信成功時にreserve.sent_dateとvalid_flag=0を同時更新 * 重複通知を防ぎ、処理済みマークを設定 * * @param int $reserveId 予約ID * @return void */ private function updateReserveSentDate(int $reserveId): void { try { DB::table('reserve') ->where('reserve_id', $reserveId) ->update([ 'sent_date' => now()->format('Y-m-d H:i:s'), 'valid_flag' => 0, // 仕様書:メール送信成功時に0に更新 'updated_at' => now() ]); Log::info('reserve.sent_date及びvalid_flag更新完了', [ 'reserve_id' => $reserveId, 'sent_date' => now()->format('Y-m-d H:i:s'), 'valid_flag' => 0 ]); } catch (Exception $e) { Log::error('reserve.sent_date及びvalid_flag更新エラー', [ 'reserve_id' => $reserveId, 'error' => $e->getMessage() ]); throw $e; } } /** * SQL-4: オペレーターキューに追加 * * SHJ-5-1準拠のキュー登録: * - que_comment: 空文字列 * - que_status_comment: 初期登録は空文字列(フロー仕様) * - work_instructions: 初期登録は空文字列(フロー仕様) * - operator_id: 9999999固定 * * @param object $waitingUser 空き待ち者情報 * @param object $parkVacancy 駐輪場空き情報 * @return array 追加結果 */ private function addToOperatorQueue(object $waitingUser, object $parkVacancy): array { try { $queue = OperatorQue::create([ 'que_class' => 4, // 予約告知通知 'user_id' => $waitingUser->user_id, 'contract_id' => null, 'park_id' => $parkVacancy->park_id, 'que_comment' => '', // 仕様書:空文字列 'que_status' => 1, // キュー発生 'que_status_comment' => '', // フロー仕様: 空文字列 'work_instructions' => '', // フロー仕様: 空文字列 'operator_id' => 9999999, // 仕様書:固定値9999999 ]); Log::info('オペレーターキュー追加成功', [ 'user_id' => $waitingUser->user_id, 'park_id' => $parkVacancy->park_id, 'reserve_id' => $waitingUser->reserve_id, 'que_class' => 4, 'operator_id' => 9999999, ]); return [ 'success' => true, 'que_id' => (int) $queue->que_id, ]; } catch (Exception $e) { Log::error('オペレーターキュー追加エラー', [ 'user_id' => $waitingUser->user_id, 'park_id' => $parkVacancy->park_id, 'reserve_id' => $waitingUser->reserve_id ?? null, 'error' => $e->getMessage() ]); return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 【JOB4】SHJ-8バッチ処理ログを作成 * * @param string $status success|error * @param string $statusComment ステータスコメント * @return void */ private function createBatchProcessLog(string $status, string $statusComment): void { try { $device = Device::orderBy('device_id')->first(); $deviceId = $device ? $device->device_id : 1; $today = now()->format('Y/m/d'); // SHJ-8の制約(255文字以内)に合わせる $statusComment = mb_strimwidth($statusComment, 0, 255, ''); if ($statusComment === '') { $statusComment = 'SHJ-5処理実行'; } $result = $this->shjEightService->execute( $deviceId, 'SHJ-5', 'SHJ-5 駐輪場空きチェック', $status, $statusComment, $today, $today ); if (($result['result'] ?? 1) !== 0) { Log::error('SHJ-5 JOB4バッチログ作成失敗', [ 'status' => $status, 'status_comment' => $statusComment, 'result' => $result, ]); } } catch (Exception $e) { Log::error('SHJ-5 JOB4バッチログ作成例外', [ 'status' => $status, 'status_comment' => $statusComment, 'error' => $e->getMessage(), ]); } } }