diff --git a/app/Console/Commands/ShjFiveCommand.php b/app/Console/Commands/ShjFiveCommand.php new file mode 100644 index 0000000..4fd5930 --- /dev/null +++ b/app/Console/Commands/ShjFiveCommand.php @@ -0,0 +1,207 @@ +shjFiveService = $shjFiveService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. バッチログ開始記録 + * 2. 駐輪場の空き状況を取得する + * 3. 空き状況判定 + * 4. 空き待ち者の情報を取得する + * 5. 取得件数判定 + * 6. 空き待ち者への通知、またはオペレーターキュー追加処理 + * 7. バッチ処理ログを作成する + * 8. 処理結果返却 + * + * @return int + */ + public function handle() + { + $batchLog = null; + + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-5 空き待ち通知処理を開始します。'); + + Log::info('SHJ-5 空き待ち通知処理開始', [ + 'start_time' => $startTime + ]); + + // SHJ-8共通処理呼び出し - 仕様書準拠 + $batchLog = BatchLog::createBatchLog( + 'SHJ-5空き待ち通知処理', + BatchLog::STATUS_START, + [ + 'device_id' => 1, // 仕様書:device_id=1 + 'process_code' => 1, // 仕様書:process_code=1 + 'status_comment' => '処理開始' // 仕様書:内部変数.ステータスコメント + ], + 'SHJ-5処理開始: 駐輪場空き状況確認と空き待ち通知処理' + ); + + // SHJ-5メイン処理実行 + $result = $this->shjFiveService->executeParkVacancyNotification(); + + $endTime = now(); + $this->info('SHJ-5 空き待ち通知処理が完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + // 処理結果表示 + $this->displayProcessResult($result); + + // SHJ-8共通処理 - バッチログ完了記録(仕様書準拠) + $logStatus = $result['success'] ? BatchLog::STATUS_SUCCESS : BatchLog::STATUS_ERROR; + + // 仕様書準拠:サービス側で作成した完全なステータスコメントを使用 + $statusComment = $result['success'] ? + ($result['status_comment'] ?? 'ステータスコメント生成エラー') : + sprintf('処理失敗: %s', $result['message'] ?? 'エラー'); + + $batchLog->update([ + 'status' => $logStatus, + 'end_time' => $endTime, + 'message' => $result['message'], + 'parameters' => [ + 'device_id' => 1, // 仕様書:device_id=1 + 'process_code' => 1, // 仕様書:process_code=1 + 'status_comment' => $statusComment, // 仕様書:内部変数.ステータスコメント(完全版) + 'processed_parks_count' => $result['processed_parks_count'] ?? 0, + 'vacant_parks_count' => $result['vacant_parks_count'] ?? 0, + 'total_waiting_users' => $result['total_waiting_users'] ?? 0, + 'notification_success_count' => $result['notification_success_count'] ?? 0, + 'operator_queue_count' => $result['operator_queue_count'] ?? 0, + 'error_count' => $result['error_count'] ?? 0, + 'duration_seconds' => $result['duration_seconds'] ?? 0 + ], + 'execution_count' => 1, + 'success_count' => $result['notification_success_count'] ?? 0, + 'error_count' => $result['error_count'] ?? 0 + ]); + + Log::info('SHJ-5 空き待ち通知処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return $result['success'] ? self::SUCCESS : self::FAILURE; + + } catch (\Exception $e) { + $this->error('SHJ-5 空き待ち通知処理で予期しないエラーが発生しました: ' . $e->getMessage()); + + // SHJ-8共通処理 - エラーログ記録(仕様書準拠) + if ($batchLog) { + $statusComment = sprintf( + 'メール正常終了件数:0/メール異常終了件数:1/キュー登録正常終了件数:0/キュー登録異常終了件数:1 - 処理エラー: %s', + $e->getMessage() + ); + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => 'SHJ-5処理中にエラーが発生: ' . $e->getMessage(), + 'parameters' => [ + 'device_id' => 1, // 仕様書:device_id=1 + 'process_code' => 1, // 仕様書:process_code=1 + 'status_comment' => $statusComment // 仕様書:内部変数.ステータスコメント(完全版) + ], + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + Log::error('SHJ-5 空き待ち通知処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } + + /** + * 処理結果を表示 + * + * @param array $result 処理結果 + * @return void + */ + private function displayProcessResult(array $result): void + { + $this->line(''); + $this->info('=== 処理結果 ==='); + + if ($result['success']) { + $this->info("✓ 処理実行成功: " . $result['message']); + } else { + $this->error("✗ 処理実行失敗: " . $result['message']); + } + + $this->line(''); + $this->info('=== 処理統計 ==='); + $this->line(" 処理対象駐輪場数: " . ($result['processed_parks_count'] ?? 0)); + $this->line(" 空きあり駐輪場数: " . ($result['vacant_parks_count'] ?? 0)); + $this->line(" 空き待ち者総数: " . ($result['total_waiting_users'] ?? 0)); + $this->line(" 通知送信成功: " . ($result['notification_success_count'] ?? 0) . "件"); + $this->line(" オペレーターキュー追加: " . ($result['operator_queue_count'] ?? 0) . "件"); + $this->line(" エラー件数: " . ($result['error_count'] ?? 0) . "件"); + $this->line(" 処理時間: " . ($result['duration_seconds'] ?? 0) . "秒"); + + // エラー詳細があれば表示 + if (!empty($result['errors'])) { + $this->line(''); + $this->warn('=== エラー詳細 ==='); + foreach ($result['errors'] as $index => $error) { + $this->line(" " . ($index + 1) . ". " . $error); + } + } + } +} diff --git a/app/Console/Commands/ShjThirteenCommand.php b/app/Console/Commands/ShjThirteenCommand.php new file mode 100644 index 0000000..dc0eb95 --- /dev/null +++ b/app/Console/Commands/ShjThirteenCommand.php @@ -0,0 +1,154 @@ +shjThirteenService = $shjThirteenService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. 引数取得・検証 + * 2. ShjThirteenService実行 + * 3. 結果表示 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-13 契約台数追加処理を開始します。'); + + // 引数取得 + $contractData = [ + 'park_id' => (int) $this->argument('park_id'), + 'psection_id' => (int) $this->argument('psection_id'), + 'ptype_id' => (int) $this->argument('ptype_id'), + 'zone_id' => (int) $this->argument('zone_id'), + 'contract_id' => $this->option('contract_id'), + ]; + + Log::info('SHJ-13 契約台数追加処理開始', [ + 'start_time' => $startTime, + 'contract_data' => $contractData, + ]); + + $this->info("駐輪場ID: {$contractData['park_id']}"); + $this->info("車種区分ID: {$contractData['psection_id']}"); + $this->info("駐輪分類ID: {$contractData['ptype_id']}"); + $this->info("ゾーンID: {$contractData['zone_id']}"); + if ($contractData['contract_id']) { + $this->info("契約ID: {$contractData['contract_id']}"); + } + + // SHJ-13処理実行 + $this->info('【処理実行】契約台数追加処理を実行しています...'); + $result = $this->shjThirteenService->execute($contractData); + + // 処理結果確認・表示 + if ($result['result'] === 0) { + $endTime = now(); + $this->info('SHJ-13 契約台数追加処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + Log::info('SHJ-13 契約台数追加処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'contract_data' => $contractData, + ]); + + // 成功時の結果出力 + $this->line('処理結果: 0'); // 0 = 正常終了 + $this->line('異常情報: '); // 正常時は空文字 + + return self::SUCCESS; + } else { + $this->error('SHJ-13 契約台数追加処理でエラーが発生しました。'); + $this->error("エラーコード: {$result['error_code']}"); + $this->error("エラーメッセージ: {$result['error_message']}"); + + Log::error('SHJ-13 契約台数追加処理エラー', [ + 'contract_data' => $contractData, + 'error_result' => $result, + ]); + + // エラー時の結果出力 + $this->line('処理結果: 1'); // 1 = 異常終了 + $this->line('異常情報: ' . $result['error_message']); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-13 契約台数追加処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-13 契約台数追加処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + // 例外時の結果出力 + $this->line('処理結果: 1'); // 1 = 異常終了 + $this->line('異常情報: システムエラー: ' . $e->getMessage()); + + return self::FAILURE; + } + } +} diff --git a/app/Services/ShjFiveService.php b/app/Services/ShjFiveService.php new file mode 100644 index 0000000..4b4b57a --- /dev/null +++ b/app/Services/ShjFiveService.php @@ -0,0 +1,650 @@ +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 // 実際のキュー登録失敗件数 + ); + + 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 + ); + + Log::info('空き待ち通知メール送信試行完了', [ + 'user_id' => $waitingUser->user_id, + 'main_email' => $mainEmail, + 'sub_email' => $subEmail, + 'mail_template_id' => $mailTemplateId, + 'result_success' => $mailResult['success'] ?? false + ]); + + return $mailResult; + + } 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() + ]; + } + } +} diff --git a/app/Services/ShjFourBService.php b/app/Services/ShjFourBService.php index 491cb42..ebb5a7b 100644 --- a/app/Services/ShjFourBService.php +++ b/app/Services/ShjFourBService.php @@ -7,6 +7,7 @@ use App\Models\RegularContract; use App\Models\Park; use App\Models\PriceA; use App\Models\Batch\BatchLog; +use App\Services\ShjThirteenService; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\DB; use Carbon\Carbon; @@ -170,6 +171,7 @@ class ShjFourBService 'T1.billing_amount', 'T4.price_ptypeid as ptype_id', 'T1.psection_id', + 'T1.zone_id', 'T1.update_flag', 'T1.reserve_id', 'T1.contract_payment_number', @@ -764,27 +766,57 @@ class ShjFourBService /** * SHJ-13実行処理(新規のみ) + * + * ShjThirteenServiceを使用した契約台数追加処理 * * @param object $contract * @return array */ private function triggerShjThirteen($contract): array { - // TODO: SHJ-13の具体的な処理を実装 - // 現在はプレースホルダー - Log::info('SHJ-4B SHJ-13実行処理', [ 'contract_id' => $contract->contract_id, - 'user_id' => $contract->user_id, 'park_id' => $contract->park_id, + 'psection_id' => $contract->psection_id, + 'ptype_id' => $contract->ptype_id, + 'zone_id' => $contract->zone_id, ]); - - return [ - 'triggered' => true, - 'method' => 'placeholder', - 'message' => 'SHJ-13処理は実装予定です', - 'contract_id' => $contract->contract_id, - ]; + + try { + // 契約データ準備 + $contractData = [ + 'contract_id' => $contract->contract_id, + 'park_id' => $contract->park_id, + 'psection_id' => $contract->psection_id, + 'ptype_id' => $contract->ptype_id, + 'zone_id' => $contract->zone_id, + ]; + + // ShjThirteenService実行 + $shjThirteenService = app(ShjThirteenService::class); + $result = $shjThirteenService->execute($contractData); + + Log::info('SHJ-4B SHJ-13実行完了', [ + 'contract_id' => $contract->contract_id, + 'result' => $result, + ]); + + return $result; + + } catch (\Throwable $e) { + Log::error('SHJ-4B SHJ-13実行エラー', [ + 'contract_id' => $contract->contract_id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return [ + 'result' => 1, + 'error_code' => $e->getCode() ?: 1999, + 'error_message' => $e->getMessage(), + 'stack_trace' => $e->getTraceAsString(), + ]; + } } /** diff --git a/app/Services/ShjThirteenService.php b/app/Services/ShjThirteenService.php new file mode 100644 index 0000000..cc3f276 --- /dev/null +++ b/app/Services/ShjThirteenService.php @@ -0,0 +1,346 @@ + $contractData['contract_id'] ?? null, + 'park_id' => $contractData['park_id'] ?? null, + 'psection_id' => $contractData['psection_id'] ?? null, + 'ptype_id' => $contractData['ptype_id'] ?? null, + 'zone_id' => $contractData['zone_id'] ?? null, + ]); + + try { + // パラメータ検証 + $validationResult = $this->validateParameters($contractData); + if (!$validationResult['valid']) { + return $this->createErrorResult( + 1001, + 'パラメータエラー: ' . $validationResult['message'] + ); + } + + // 【処理1・2】契約台数反映とバッチログ作成を一体で実行 + $processResult = $this->executeProcessWithLogging($contractData); + if (!$processResult['success']) { + return $this->createErrorResult( + $processResult['error_code'], + $processResult['error_message'], + $processResult['stack_trace'] ?? '' + ); + } + + $statusComment = $processResult['status_comment']; + + $endTime = now(); + Log::info('SHJ-13 契約台数追加処理完了', [ + 'contract_id' => $contractData['contract_id'], + 'execution_time' => $startTime->diffInSeconds($endTime), + 'updated_count' => $processResult['updated_count'], + 'status_comment' => $statusComment, + ]); + + return ['result' => 0]; + + } catch (\Throwable $e) { + Log::error('SHJ-13 契約台数追加処理例外エラー', [ + 'contract_data' => $contractData, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return $this->createErrorResult( + $e->getCode() ?: 1999, + $e->getMessage(), + $e->getTraceAsString() + ); + } + } + + /** + * パラメータ検証 + * + * @param array $contractData + * @return array + */ + private function validateParameters(array $contractData): array + { + $errors = []; + + if (empty($contractData['park_id'])) { + $errors[] = '駐輪場IDが設定されていません'; + } + + if (empty($contractData['psection_id'])) { + $errors[] = '車種区分IDが設定されていません'; + } + + if (empty($contractData['ptype_id'])) { + $errors[] = '駐輪分類IDが設定されていません'; + } + + if (empty($contractData['zone_id'])) { + $errors[] = 'ゾーンIDが設定されていません'; + } + + if (!empty($errors)) { + return [ + 'valid' => false, + 'message' => implode(', ', $errors), + 'details' => $errors, + ]; + } + + return ['valid' => true]; + } + + /** + * 契約台数反映とバッチログ作成の一体処理 + * + * park_number・zone テーブルの契約台数を+1更新し、バッチログを作成 + * 全てを1つのトランザクション内で実行 + * + * @param array $contractData + * @return array + */ + private function executeProcessWithLogging(array $contractData): array + { + try { + return DB::transaction(function() use ($contractData) { + // 各テーブルの名称取得 + $names = $this->getTableNames($contractData); + if (!$names['success']) { + throw new \Exception('名称取得エラー: ' . $names['message'], 1002); + } + + // park_number テーブル更新 + $parkNumberUpdated = DB::table('park_number') + ->where('park_id', $contractData['park_id']) + ->where('psection_id', $contractData['psection_id']) + ->where('ptype_id', $contractData['ptype_id']) + ->increment('park_number', 1, [ + 'updated_at' => now(), + 'operator_id' => 'SHJ-13', + ]); + + if ($parkNumberUpdated === 0) { + throw new \Exception('park_numberテーブルの対象レコードが存在しません', 1011); + } + + // zone テーブル更新 + $zoneUpdated = DB::table('zone') + ->where('zone_id', $contractData['zone_id']) + ->increment('zone_number', 1, [ + 'updated_at' => now(), + 'ope_id' => 'SHJ-13', + ]); + + if ($zoneUpdated === 0) { + throw new \Exception('zoneテーブルの対象レコードが存在しません', 1012); + } + + // 更新後の契約台数取得 + $updatedZone = DB::table('zone') + ->where('zone_id', $contractData['zone_id']) + ->first(['zone_number']); + + // ステータスコメント構築 + $statusComment = $this->buildStatusComment($names['names'], $updatedZone->zone_number); + + // バッチ処理ログ作成(同一トランザクション内) + $currentDate = now()->format('Y/m/d'); + + $batchLog = BatchLog::createBatchLog( + 'SHJ-13', // process_name + BatchLog::STATUS_SUCCESS, + [ + 'device_id' => 1, + 'job_name' => 'SHJ-13', + 'status' => 'success', + 'status_comment' => $statusComment, + 'created_date' => $currentDate, + 'updated_date' => $currentDate, + 'contract_id' => $contractData['contract_id'] ?? null, + 'park_id' => $contractData['park_id'], + 'psection_id' => $contractData['psection_id'], + 'ptype_id' => $contractData['ptype_id'], + 'zone_id' => $contractData['zone_id'], + ], + $statusComment + ); + + Log::info('SHJ-13 契約台数更新・ログ作成完了', [ + 'contract_id' => $contractData['contract_id'], + 'park_number_updated' => $parkNumberUpdated, + 'zone_updated' => $zoneUpdated, + 'updated_count' => $updatedZone->zone_number, + 'batch_log_id' => $batchLog->id, + 'status_comment' => $statusComment, + ]); + + return [ + 'success' => true, + 'updated_count' => $updatedZone->zone_number, + 'status_comment' => $statusComment, + ]; + }); + + } catch (\Throwable $e) { + Log::error('SHJ-13 契約台数更新・ログ作成エラー', [ + 'contract_data' => $contractData, + 'error_code' => $e->getCode(), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return [ + 'success' => false, + 'error_code' => $e->getCode() ?: 1010, + 'error_message' => $e->getMessage(), + 'stack_trace' => $e->getTraceAsString(), + ]; + } + } + + /** + * テーブル名称取得 + * + * @param array $contractData + * @return array + */ + private function getTableNames(array $contractData): array + { + try { + // 駐輪場名取得 + $park = DB::table('park') + ->where('park_id', $contractData['park_id']) + ->first(['park_name']); + + if (!$park) { + return [ + 'success' => false, + 'message' => "駐輪場が見つかりません: park_id={$contractData['park_id']}", + ]; + } + + // 駐輪分類名取得 + $ptype = DB::table('ptype') + ->where('ptype_id', $contractData['ptype_id']) + ->first(['ptype_subject']); + + if (!$ptype) { + return [ + 'success' => false, + 'message' => "駐輪分類が見つかりません: ptype_id={$contractData['ptype_id']}", + ]; + } + + // 車種区分名取得 + $psection = DB::table('psection') + ->where('psection_id', $contractData['psection_id']) + ->first(['psection_subject']); + + if (!$psection) { + return [ + 'success' => false, + 'message' => "車種区分が見つかりません: psection_id={$contractData['psection_id']}", + ]; + } + + // ゾーン名取得 + $zone = DB::table('zone') + ->where('zone_id', $contractData['zone_id']) + ->first(['zone_name']); + + if (!$zone) { + return [ + 'success' => false, + 'message' => "ゾーンが見つかりません: zone_id={$contractData['zone_id']}", + ]; + } + + return [ + 'success' => true, + 'names' => [ + 'park_name' => $park->park_name, + 'ptype_subject' => $ptype->ptype_subject, + 'psection_subject' => $psection->psection_subject, + 'zone_name' => $zone->zone_name, + ], + ]; + + } catch (\Throwable $e) { + return [ + 'success' => false, + 'message' => 'テーブル名称取得エラー: ' . $e->getMessage(), + 'details' => $e->getTraceAsString(), + ]; + } + } + + /** + * ステータスコメント構築 + * + * 形式:駐輪場名/駐輪分類名/車種区分名/ゾーン名/現在契約台数(更新後):{数値}/ + * + * @param array $names + * @param int $updatedCount + * @return string + */ + private function buildStatusComment(array $names, int $updatedCount): string + { + return sprintf( + '%s/%s/%s/%s/現在契約台数(更新後):%d/', + $names['park_name'], + $names['ptype_subject'], + $names['psection_subject'], + $names['zone_name'], + $updatedCount + ); + } + + + /** + * エラー結果作成 + * + * @param int $errorCode + * @param string $errorMessage + * @param string $stackTrace + * @return array + */ + private function createErrorResult(int $errorCode, string $errorMessage, string $stackTrace = ''): array + { + return [ + 'result' => 1, + 'error_code' => $errorCode, + 'error_message' => $errorMessage, + 'stack_trace' => $stackTrace, + ]; + } +}