shjEightService = $shjEightService; } /** * SHJ-5 メイン処理を実行 * * 処理フロー: * 1. 駐輪場の空き状況を取得する * 2. 空き状況判定 * 3. 空き待ち者の情報を取得する * 4. 取得件数判定 * 5. 空き待ち者への通知、またはオペレーターキュー追加処理 * 6. バッチ処理ログを作成する * * @return array 処理結果 */ public function executeParkVacancyNotification(): array { try { $startTime = now(); Log::info('SHJ-5 空き待ち通知処理開始'); // 処理統計 $processedParksCount = 0; $vacantParksCount = 0; $totalWaitingUsers = 0; $notificationSuccessCount = 0; $operatorQueueCount = 0; $mailErrors = []; // メール異常終了件数専用 $errors = []; // 全体エラー収集用 $allQueueItems = []; // 全オペレーターキュー作成用データ // 【処理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 ]); // 【判断1】空き状況判定 if ($parkVacancy->vacant_count < 1) { Log::info('空きなし - 処理スキップ', [ 'park_id' => $parkVacancy->park_id, 'vacant_count' => $parkVacancy->vacant_count ]); continue; } $vacantParksCount++; // 【処理2】空き待ち者の情報を取得する $waitingUsers = $this->getWaitingUsersInfo( $parkVacancy->park_id, $parkVacancy->psection_id, $parkVacancy->ptype_id ); // 【判断2】取得件数判定 if (empty($waitingUsers)) { Log::info('空き待ち者なし', [ 'park_id' => $parkVacancy->park_id ]); continue; } $totalWaitingUsers += count($waitingUsers); Log::info('空き待ち者情報取得完了', [ 'park_id' => $parkVacancy->park_id, 'waiting_users_count' => count($waitingUsers) ]); // 【処理3】空き待ち者への通知、またはオペレーターキュー追加処理 $notificationResult = $this->processWaitingUsersNotification( $waitingUsers, $parkVacancy ); $notificationSuccessCount += $notificationResult['notification_success_count']; $operatorQueueCount += $notificationResult['operator_queue_count']; if (!empty($notificationResult['errors'])) { $mailErrors = array_merge($mailErrors, $notificationResult['errors']); $errors = array_merge($errors, $notificationResult['errors']); } // オペレーターキュー作成用データを収集 if (!empty($notificationResult['queue_items'])) { $allQueueItems = array_merge($allQueueItems ?? [], $notificationResult['queue_items']); } } // 【処理4】仕様書準拠:先に呼び出し、成功時のみ内部変数を更新 $queueErrorCount = 0; // キュー登録異常終了件数(累計) $queueSuccessCount = 0; // キュー登録正常終了件数(累計) foreach ($allQueueItems as $queueItem) { // 仕様書準拠:在呼叫前先計算"如果這次成功會是第幾件",確保記錄反映最新件數 $predictedSuccessCount = $queueSuccessCount + 1; $predictedErrorCount = $queueErrorCount; // 暂时保持当前错误计数 $queueResult = $this->addToOperatorQueue( $queueItem['waiting_user'], $queueItem['park_vacancy'], $queueItem['batch_comment'], $notificationSuccessCount, // 最終メール正常終了件数 $predictedSuccessCount, // 預測成功時的件數(包含本次) count($mailErrors), // 現在のメール異常終了件数(動態計算) $predictedErrorCount // 現在のキュー登録異常終了件数 ); // 仕様書:根据实际结果决定是否采用预测值 if ($queueResult['success']) { $queueSuccessCount = $predictedSuccessCount; // 采用预测的成功计数 } else { $queueErrorCount++; // 失败时递增错误计数 // 仕様書:包含具体错误消息,满足"エラーメッセージ/スタックトレースを保持"要求 $errorDetail = $queueResult['error'] ?? 'Unknown error'; $queueErrorInfo = sprintf('キュー登録失敗:予約ID:%d - %s', $queueItem['waiting_user']->reserve_id ?? 0, $errorDetail ); $errors[] = $queueErrorInfo; // 加入总错误统计(包含具体原因) Log::error('オペレーターキュー作成失敗', [ 'user_id' => $queueItem['waiting_user']->user_id, 'reserve_id' => $queueItem['waiting_user']->reserve_id ?? 0, 'error' => $errorDetail ]); } } $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, 'notification_success_count' => $notificationSuccessCount, 'operator_queue_success_count' => $queueSuccessCount, // 仕様書:正常完了件数 'queue_error_count' => $queueErrorCount, 'mail_error_count' => count($mailErrors), // メール異常終了件数(分離) 'total_error_count' => count($errors) // 全体エラー件数 ]); // 仕様書に基づく内部変数.ステータスコメント生成 $statusComment = sprintf( 'メール正常終了件数:%d/メール異常終了件数:%d/キュー登録正常終了件数:%d/キュー登録異常終了件数:%d', $notificationSuccessCount, count($mailErrors), // メール異常終了件数(キュー失敗を除外) $queueSuccessCount ?? 0, // 実際のキュー登録成功件数 $queueErrorCount ?? 0 // 実際のキュー登録失敗件数 ); // SHJ-8 バッチ処理ログ作成 try { $device = Device::orderBy('device_id')->first(); $deviceId = $device ? $device->device_id : 1; $today = now()->format('Y/m/d'); $this->shjEightService->execute( $deviceId, 'SHJ-5', 'SHJ-5空き待ち通知', 'success', $statusComment, $today, $today ); Log::info('SHJ-8 バッチ処理ログ作成完了'); } catch (Exception $e) { Log::error('SHJ-8 バッチ処理ログ作成エラー', [ 'error' => $e->getMessage() ]); } return [ 'success' => true, 'message' => 'SHJ-5 空き待ち通知処理が正常に完了しました', 'processed_parks_count' => $processedParksCount, 'vacant_parks_count' => $vacantParksCount, 'total_waiting_users' => $totalWaitingUsers, 'notification_success_count' => $notificationSuccessCount, 'operator_queue_count' => $queueSuccessCount ?? 0, // 仕様書:正常完了件数を使用 'error_count' => count($errors), 'errors' => $errors, 'duration_seconds' => $duration, 'status_comment' => $statusComment // SHJ-8用の完全なステータスコメント ]; } catch (Exception $e) { Log::error('SHJ-5 空き待ち通知処理でエラーが発生', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ 'success' => false, 'message' => 'SHJ-5 空き待ち通知処理でエラーが発生: ' . $e->getMessage(), 'error_details' => $e->getMessage() ]; } } /** * 【処理1】駐輪場の空き状況を取得する * * 仕様書に基づくSQL: * - zone表から標準台数と現在の契約台数を比較 * - 空きがある駐輪場の情報を取得 * * @return array 駐輪場空き状況リスト */ private function getParkVacancyStatus(): array { try { // ゾーン毎の契約台数を取得 $contractCounts = DB::table('regular_contract as T1') ->select([ 'T1.park_id', 'T1.psection_id', 'T5.ptype_id', DB::raw('count(T1.contract_id) as contract_count') ]) ->join('park as T2', 'T1.park_id', '=', 'T2.park_id') ->join('price_a as T5', function($join) { $join->on('T1.park_id', '=', 'T5.park_id') ->on('T1.price_parkplaceid', '=', 'T5.price_parkplaceid') ->on('T1.psection_id', '=', 'T5.psection_id'); }) ->where([ ['T1.contract_flag', '=', 1], // 有効契約 ['T2.park_close_flag', '=', 0], // 駐輪場未閉鎖 ]) // 契約有効期間内の条件 ->whereRaw("date_format(now(), '%Y%m%d') BETWEEN T1.contract_periods AND T1.contract_periode") ->groupBy(['T1.park_id', 'T1.psection_id', 'T5.ptype_id']) ->get() ->keyBy(function($item) { return $item->park_id . '_' . $item->psection_id . '_' . $item->ptype_id; }); // ゾーン情報と照合して空き状況を算出 $vacancyList = DB::table('zone as T1') ->select([ 'T1.park_id', 'T2.park_name', 'T1.psection_id', 'T1.ptype_id', 'T1.zone_standard', 'T3.psection_subject', 'T4.ptype_subject' ]) ->join('park as T2', 'T1.park_id', '=', 'T2.park_id') ->join('psection as T3', 'T1.psection_id', '=', 'T3.psection_id') ->join('ptype as T4', 'T1.ptype_id', '=', 'T4.ptype_id') ->where([ ['T1.delete_flag', '=', 0], // ゾーン有効 ['T2.park_close_flag', '=', 0], // 駐輪場開設 ]) ->get() ->map(function($zone) use ($contractCounts) { $key = $zone->park_id . '_' . $zone->psection_id . '_' . $zone->ptype_id; $contractCount = isset($contractCounts[$key]) ? $contractCounts[$key]->contract_count : 0; $zone->contract_count = $contractCount; $zone->vacant_count = max(0, $zone->zone_standard - $contractCount); return $zone; }) ->filter(function($zone) { return $zone->vacant_count > 0; // 空きがあるもののみ }) ->values(); Log::info('駐輪場空き状況算出完了', [ 'total_zones' => count($vacancyList), 'vacant_zones' => $vacancyList->filter(function($v) { return $v->vacant_count > 0; })->count() ]); return $vacancyList->toArray(); } catch (Exception $e) { Log::error('駐輪場空き状況取得エラー', [ 'error' => $e->getMessage() ]); throw $e; } } /** * 【処理2】空き待ち者の情報を取得する * * 仕様書に基づく取得条件: * - 契約未紐付(contract_id IS NULL) * - 退会でない(user_quit_flag <> 1) * - 有効な予約(valid_flag = 1) * - 予約日時順で取得(reserve_date昇順) * * @param int $parkId 駐輪場ID * @param int $psectionId 車種区分ID * @param int $ptypeId 駐輪分類ID * @return array 空き待ち者情報リスト */ private function getWaitingUsersInfo(int $parkId, int $psectionId, int $ptypeId): array { try { $waitingUsers = DB::table('reserve as T1') ->select([ 'T1.reserve_id', 'T1.user_id', 'T1.park_id', 'T1.psection_id', 'T1.ptype_id', 'T1.reserve_order', 'T1.reserve_date', 'T1.reserve_manual', // 手動通知フラグ 'T1.contract_id', // 契約紐付確認用 'T2.user_name', 'T2.user_primemail', 'T2.user_submail', // 副メールアドレス 'T2.user_manual_regist_flag', // 手動登録フラグ 'T2.user_quit_flag', // 退会フラグ 'T3.park_name', 'T4.psection_subject', 'T5.ptype_subject' ]) ->join('user as T2', 'T1.user_id', '=', 'T2.user_id') ->join('park as T3', 'T1.park_id', '=', 'T3.park_id') ->join('psection as T4', 'T1.psection_id', '=', 'T4.psection_id') ->join('ptype as T5', 'T1.ptype_id', '=', 'T5.ptype_id') ->where([ ['T1.park_id', '=', $parkId], ['T1.psection_id', '=', $psectionId], ['T1.ptype_id', '=', $ptypeId], ['T1.valid_flag', '=', 1], // 有効な予約 ['T2.user_quit_flag', '<>', 1] // 退会でない ]) ->whereNull('T1.contract_id') // 契約未紐付 ->orderBy('T1.reserve_date', 'asc') // 仕様書に基づく予約日時順 ->get() ->toArray(); Log::info('空き待ち者情報取得完了', [ '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; } } /** * 【処理3】空き待ち者への通知、またはオペレーターキュー追加処理 * * 仕様書に基づく分岐処理: * - 手動通知フラグ判定(reserve_manual) * - メール送信成功時のreserve.sent_date更新 * - 失敗時のオペレーターキュー追加(最終統計で処理) * * @param array $waitingUsers 空き待ち者リスト * @param object $parkVacancy 駐輪場空き情報 * @return array 通知処理結果 */ private function processWaitingUsersNotification(array $waitingUsers, object $parkVacancy): array { $notificationSuccessCount = 0; $operatorQueueCount = 0; $errors = []; $queueItems = []; // オペレーターキュー作成用データ収集 try { // 空きがある分だけ処理(先着順) $availableSpots = min($parkVacancy->vacant_count, count($waitingUsers)); for ($i = 0; $i < $availableSpots; $i++) { $waitingUserData = $waitingUsers[$i]; // 配列をオブジェクトに変換 $waitingUser = (object) $waitingUserData; try { // 【仕様判断】手動通知フラグチェック if ($waitingUser->reserve_manual == 1) { // 手動通知 → オペレーターキュー作成データ収集 $batchComment = '手動通知フラグ設定のため予約ID:' . $waitingUser->reserve_id; $queueItems[] = [ 'waiting_user' => $waitingUser, 'park_vacancy' => $parkVacancy, 'batch_comment' => $batchComment ]; $operatorQueueCount++; Log::info('手動通知フラグによりオペレーターキュー登録予定', [ 'user_id' => $waitingUser->user_id, 'reserve_id' => $waitingUser->reserve_id ]); } else { // 自動通知 → メール送信を試行 $mailResult = $this->sendVacancyNotificationMail($waitingUser, $parkVacancy); if ($mailResult['success']) { // メール送信成功 → reserve.sent_date更新 $this->updateReserveSentDate($waitingUser->reserve_id); $notificationSuccessCount++; Log::info('空き待ち通知メール送信成功', [ 'user_id' => $waitingUser->user_id, 'reserve_id' => $waitingUser->reserve_id, 'park_id' => $parkVacancy->park_id ]); } else { // メール送信失敗 → オペレーターキュー作成データ収集 $shjSevenError = $mailResult['error'] ?? $mailResult['message'] ?? 'SHJ-7メール送信エラー'; $batchComment = $shjSevenError . '予約ID:' . $waitingUser->reserve_id; $queueItems[] = [ 'waiting_user' => $waitingUser, 'park_vacancy' => $parkVacancy, 'batch_comment' => $batchComment ]; $operatorQueueCount++; $errors[] = $shjSevenError; } } } catch (Exception $e) { Log::error('空き待ち者通知処理エラー', [ 'user_id' => $waitingUser->user_id, 'reserve_id' => $waitingUser->reserve_id, 'error' => $e->getMessage() ]); // エラー発生時もオペレーターキュー作成データ収集 $batchComment = 'システムエラー:' . $e->getMessage() . '予約ID:' . $waitingUser->reserve_id; $queueItems[] = [ 'waiting_user' => $waitingUser, 'park_vacancy' => $parkVacancy, 'batch_comment' => $batchComment ]; $operatorQueueCount++; $errors[] = $e->getMessage(); } } return [ 'notification_success_count' => $notificationSuccessCount, 'operator_queue_count' => $operatorQueueCount, 'errors' => $errors, 'queue_items' => $queueItems // 後でキュー作成用 ]; } catch (Exception $e) { Log::error('空き待ち者通知処理全体エラー', [ 'park_id' => $parkVacancy->park_id, 'error' => $e->getMessage() ]); throw $e; } } /** * 空き待ち通知メールを送信 * * 仕様書に基づくSHJ-7呼び出し: * - 主メールアドレス・副メールアドレスを正しく渡す * - 必要なパラメータを全て設定 * * @param object $waitingUser 空き待ち者情報 * @param object $parkVacancy 駐輪場空き情報 * @return array 送信結果 */ private function sendVacancyNotificationMail(object $waitingUser, object $parkVacancy): array { try { // ShjMailSendServiceを利用してメール送信 $mailService = app(ShjMailSendService::class); // 空き待ち通知用のメールテンプレートID(予約告知通知) // OperatorQueの定数と合わせて4番を使用 $mailTemplateId = 4; // 予約告知通知のテンプレートID // 仕様書No1/No2に基づく主メール・副メール設定 $mainEmail = $waitingUser->user_primemail ?? ''; $subEmail = $waitingUser->user_submail ?? ''; // メール送信実行(仕様書準拠) $mailResult = $mailService->executeMailSend( $mainEmail, $subEmail, $mailTemplateId ); // 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() ]; } } /** * 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; } } /** * オペレーターキューに追加 * * 仕様書に基づくキュー登録: * - que_comment: 空文字列 * - que_status_comment: 仕様書完全準拠形式(統計情報含む) * - operator_id: 9999999固定 * * @param object $waitingUser 空き待ち者情報 * @param object $parkVacancy 駐輪場空き情報 * @param string $batchComment 内部変数.バッチコメント * @param int $mailSuccessCount メール正常終了件数 * @param int $queueSuccessCount キュー登録正常終了件数 * @param int $mailErrorCount メール異常終了件数 * @param int $queueErrorCount キュー登録異常終了件数 * @return array 追加結果 */ private function addToOperatorQueue(object $waitingUser, object $parkVacancy, string $batchComment, int $mailSuccessCount, int $queueSuccessCount, int $mailErrorCount, int $queueErrorCount): array { try { // 仕様書完全準拠:駐輪場名/駐輪分類名/車種区分名/空き台数…/対象予約ID…/内部変数.バッチコメント/内部変数.メール正常終了件数…メール異常終了件数…キュー登録正常終了件数…キュー登録異常終了件数… $statusComment = sprintf( '%s/%s/%s/空き台数:%d台/対象予約ID:%d/%s/メール正常終了件数:%d/メール異常終了件数:%d/キュー登録正常終了件数:%d/キュー登録異常終了件数:%d', $waitingUser->park_name ?? '', $waitingUser->ptype_subject ?? '', // 駐輪分類名 $waitingUser->psection_subject ?? '', // 車種区分名 $parkVacancy->vacant_count ?? 0, $waitingUser->reserve_id ?? 0, $batchComment, // 内部変数.バッチコメント $mailSuccessCount, // 内部変数.メール正常終了件数 $mailErrorCount, $queueSuccessCount, $queueErrorCount ); OperatorQue::create([ 'que_class' => 4, // 予約告知通知 'user_id' => $waitingUser->user_id, 'contract_id' => null, 'park_id' => $waitingUser->park_id, 'que_comment' => '', // 仕様書:空文字列 'que_status' => 1, // キュー発生 'que_status_comment' => $statusComment, // 仕様書:完全準拠形式 'work_instructions' => '空き待ち者への連絡をお願いします。', 'operator_id' => 9999999, // 仕様書:固定値9999999 ]); Log::info('オペレーターキュー追加成功', [ 'user_id' => $waitingUser->user_id, 'park_id' => $waitingUser->park_id, 'reserve_id' => $waitingUser->reserve_id, 'que_class' => 4, 'operator_id' => 9999999, 'batch_comment' => $batchComment, 'mail_success_count' => $mailSuccessCount, 'mail_error_count' => $mailErrorCount, 'queue_success_count' => $queueSuccessCount, 'queue_error_count' => $queueErrorCount, 'status_comment' => $statusComment ]); return ['success' => true]; } catch (Exception $e) { Log::error('オペレーターキュー追加エラー', [ 'user_id' => $waitingUser->user_id, 'park_id' => $waitingUser->park_id, 'reserve_id' => $waitingUser->reserve_id ?? null, 'batch_comment' => $batchComment, 'error' => $e->getMessage() ]); return [ 'success' => false, 'error' => $e->getMessage() ]; } } }