parkModel = $parkModel; $this->userModel = $userModel; $this->contractModel = $contractModel; $this->operatorQueModel = $operatorQueModel; $this->batchLogModel = $batchLogModel; $this->mailSendService = $mailSendService; } /** * SHJ-3 定期更新リマインダー処理メイン実行 * * 処理フロー(仕様書準拠): * 【処理0】駐輪場マスタの情報を取得する * 【判断0】当該駐輪場実行タイミングチェック * 【処理2】定期更新対象者を取得する * 【判断2】利用者有無をチェック * 【処理3】対象者向けにメール送信、またはオペレーターキュー追加処理 * 【処理4】バッチ処理ログを作成する(各駐輪場ごとに実行) * * @return array 処理結果 */ public function executeReminderProcess(): array { $overallProcessedParksCount = 0; $overallTotalTargetUsers = 0; $overallMailSuccessCount = 0; $overallMailErrorCount = 0; $overallQueueSuccessCount = 0; $overallQueueErrorCount = 0; try { Log::info('SHJ-3 定期更新リマインダー処理開始'); // 【処理0】駐輪場マスタの情報を取得する $parkList = $this->getParkMasterInfo(); if (empty($parkList)) { $message = '対象の駐輪場マスタが見つかりません'; Log::warning($message); return [ 'success' => false, 'message' => $message, 'processed_parks_count' => 0, 'total_target_users' => 0, 'mail_success_count' => 0, 'mail_error_count' => 0, 'operator_queue_count' => 0 ]; } // 取得レコード数分【判断0】を繰り返す foreach ($parkList as $park) { // 各駐輪場ごとの内部変数(仕様書:場景A) $mailSuccessCount = 0; $mailErrorCount = 0; $queueSuccessCount = 0; $queueErrorCount = 0; $batchComment = ''; Log::info('駐輪場処理開始', [ 'park_id' => $park->park_id, 'park_name' => $park->park_name ]); // 【判断0】当該駐輪場実行タイミングチェック $timingCheckResult = $this->checkExecutionTiming($park); if (!$timingCheckResult['should_execute']) { Log::info('実行タイミング対象外', [ 'park_id' => $park->park_id, 'reason' => $timingCheckResult['reason'] ]); // 次の駐輪場マスタへ continue; } $overallProcessedParksCount++; // 【処理2】定期更新対象者を取得する $targetUsers = $this->getRegularUpdateTargetUsers( $park, $timingCheckResult['update_pattern'] ); // 【判断2】利用者有無をチェック if (empty($targetUsers)) { // 仕様書:利用者なしの結果を設定する $batchComment = "定期更新リマインダー:今月の定期更新対象者は無しです / {$park->park_name}"; Log::info('利用者なし', [ 'park_id' => $park->park_id, 'batch_comment' => $batchComment ]); // 【処理4】バッチ処理ログを作成する $this->createShjBatchLog( $park, $batchComment, $mailSuccessCount, $mailErrorCount, $queueSuccessCount, $queueErrorCount ); // 次の駐輪場マスタへ continue; } $overallTotalTargetUsers += count($targetUsers); // 【処理3】処理2の対象レコード数分繰り返す foreach ($targetUsers as $targetUser) { $processResult = $this->processTargetUser($targetUser, $park); if ($processResult['type'] === 'mail_success') { $mailSuccessCount++; } elseif ($processResult['type'] === 'mail_error') { $mailErrorCount++; // バッチコメントに異常情報を追加 if (!empty($processResult['error_info'])) { $batchComment .= ($batchComment ? ' / ' : '') . $processResult['error_info']; } } elseif ($processResult['type'] === 'queue_success') { $queueSuccessCount++; } elseif ($processResult['type'] === 'queue_error') { $queueErrorCount++; } } // 全体集計用に加算 $overallMailSuccessCount += $mailSuccessCount; $overallMailErrorCount += $mailErrorCount; $overallQueueSuccessCount += $queueSuccessCount; $overallQueueErrorCount += $queueErrorCount; Log::info('駐輪場処理完了', [ 'park_id' => $park->park_id, 'target_users_count' => count($targetUsers), 'mail_success' => $mailSuccessCount, 'mail_error' => $mailErrorCount, 'queue_success' => $queueSuccessCount, 'queue_error' => $queueErrorCount ]); // 【処理4】バッチ処理ログを作成する(各駐輪場ごと) $this->createShjBatchLog( $park, $batchComment, $mailSuccessCount, $mailErrorCount, $queueSuccessCount, $queueErrorCount ); } Log::info('SHJ-3 定期更新リマインダー処理完了', [ 'processed_parks_count' => $overallProcessedParksCount, 'total_target_users' => $overallTotalTargetUsers, 'mail_success_count' => $overallMailSuccessCount, 'mail_error_count' => $overallMailErrorCount, 'queue_success_count' => $overallQueueSuccessCount, 'queue_error_count' => $overallQueueErrorCount ]); return [ 'success' => true, 'message' => 'SHJ-3 定期更新リマインダー処理が正常に完了しました', 'processed_parks_count' => $overallProcessedParksCount, 'total_target_users' => $overallTotalTargetUsers, 'mail_success_count' => $overallMailSuccessCount, 'mail_error_count' => $overallMailErrorCount, 'operator_queue_count' => $overallQueueSuccessCount + $overallQueueErrorCount ]; } catch (\Exception $e) { $errorMessage = 'SHJ-3 定期更新リマインダー処理でエラーが発生: ' . $e->getMessage(); Log::error('SHJ-3 定期更新リマインダー処理エラー', [ 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ 'success' => false, 'message' => $errorMessage, 'details' => $e->getMessage(), 'processed_parks_count' => $overallProcessedParksCount, 'total_target_users' => $overallTotalTargetUsers, 'mail_success_count' => $overallMailSuccessCount, 'mail_error_count' => $overallMailErrorCount, 'operator_queue_count' => $overallQueueSuccessCount + $overallQueueErrorCount ]; } } /** * 【処理0】駐輪場マスタの情報を取得する * * 仕様書に基づくSQLクエリ: * SELECT 駐輪場ID, 駐輪場名, 更新期間開始日, 更新期間開始時, * 更新期間終了日, 更新期間終了時, リマインダー種別, リマインダー時間 * FROM 駐輪場マスタ * WHERE 閉設フラグ = 0 * ORDER BY 駐輪場ふりがな asc * * @return array 駐輪場マスタ情報 */ private function getParkMasterInfo(): array { try { $parkInfo = DB::table('park') ->select([ 'park_id', // 駐輪場ID 'park_name', // 駐輪場名 'update_grace_period_start_date', // 更新期間開始日(例:"20") 'update_grace_period_start_time', // 更新期間開始時(例:"09:00") 'update_grace_period_end_date', // 更新期間終了日(例:"6") 'update_grace_period_end_time', // 更新期間終了時(例:"23:59") 'reminder_type', // リマインダー種別(0=毎日,1=1日おき,2=2日おき) 'reminder_time' // リマインダー時間(例:"09:00") ]) ->where('park_close_flag', 0) // 閉設フラグ = 0 ->orderBy('park_ruby', 'asc') // 駐輪場ふりがな 昇順 ->get() ->toArray(); Log::info('駐輪場マスタ情報取得完了', [ 'park_count' => count($parkInfo) ]); return $parkInfo; } catch (\Exception $e) { Log::error('駐輪場マスタ情報取得エラー', [ 'error' => $e->getMessage() ]); throw $e; } } /** * 【判断0】当該駐輪場実行タイミングチェック * * 仕様書に基づく実行タイミング判定: * 1. リマインダー時間 = 現在の時間 のチェック * 2. 内部変数.更新パターン の設定(A or B) * 3. 内部変数.更新期間開始日からの経過日数 の算出 * 4. 内部変数.実行フラグ の判定 * * @param object $park 駐輪場情報 * @return array 実行タイミング判定結果 */ private function checkExecutionTiming($park): array { try { $now = Carbon::now(); $currentTime = $now->format('H:i'); $todayDay = (int)$now->format('d'); // 本日の日(1-31) Log::info('実行タイミングチェック開始', [ 'park_id' => $park->park_id, 'current_time' => $currentTime, 'today_day' => $todayDay, 'reminder_time' => $park->reminder_time, 'reminder_type' => $park->reminder_type ]); // 仕様書:駐輪場マスタ.リマインダー時間 = [現在の時間] の場合 if ($park->reminder_time !== $currentTime) { return [ 'should_execute' => false, 'reason' => "リマインダー時間不一致(設定:{$park->reminder_time} vs 現在:{$currentTime})" ]; } // 内部変数.更新パターン を設定 // DBから返る値は文字列なので、型変換して使用 $startDay = (int)$park->update_grace_period_start_date; // 例:"20" → 20 $endDay = (int)$park->update_grace_period_end_date; // 例:"6" → 6 $updatePattern = ''; if ($startDay <= $endDay) { // パターンA: 月を跨らない場合 $updatePattern = 'A'; } else { // パターンB: 月を跨る場合 $updatePattern = 'B'; } // 内部変数.更新期間開始日からの経過日数 を設定 $elapsedDays = 99; // デフォルト: 対象外 if ($updatePattern === 'A') { // パターンA の場合 if ($endDay < $todayDay) { // 駐輪場マスタ.更新期間終了日 > [本日の日付]の日 の場合 $elapsedDays = 99; // 対象外 } elseif ($startDay <= $todayDay) { // 駐輪場マスタ.更新期間開始日 <= [本日の日付]の日 の場合 $elapsedDays = $todayDay - $startDay; } else { // その他の場合 $elapsedDays = 99; // 対象外 } } else { // パターンB の場合 if ($startDay <= $todayDay) { // 駐輪場マスタ.更新期間開始日 <= [本日の日付]の日 の場合 $elapsedDays = $todayDay - $startDay; } elseif ($endDay >= $todayDay) { // 駐輪場マスタ.更新期間終了日 >= [本日の日付]の日 の場合 // 仕様書の計算式: ([先月の月末日]の日 − 駐輪場マスタ.更新期間開始日) + [本日の日付]の日 $lastMonthEnd = $now->copy()->subMonth()->endOfMonth()->day; $elapsedDays = ($lastMonthEnd - $startDay) + $todayDay; } else { // その他の場合 $elapsedDays = 99; // 対象外 } } Log::info('経過日数算出完了', [ 'park_id' => $park->park_id, 'update_pattern' => $updatePattern, 'start_day' => $startDay, 'end_day' => $endDay, 'today_day' => $todayDay, 'elapsed_days' => $elapsedDays ]); // 内部変数.実行フラグ を設定 $executionFlag = 0; if ($elapsedDays !== 99) { // DBから返る値は文字列なので、型変換して比較 $reminderType = (int)($park->reminder_type ?? 0); if ($reminderType === 0) { // 仕様書:毎日 $executionFlag = 1; } elseif ($reminderType === 1) { // 仕様書:1日おき(経過日数が偶数の場合) $executionFlag = ($elapsedDays % 2 === 0) ? 1 : 0; } elseif ($reminderType === 2) { // 仕様書:2日おき(経過日数を3で割った余りが0の場合) $executionFlag = ($elapsedDays % 3 === 0) ? 1 : 0; } else { // あり得ない $executionFlag = 0; } } $shouldExecute = ($executionFlag === 1); Log::info('実行タイミングチェック完了', [ 'park_id' => $park->park_id, 'update_pattern' => $updatePattern, 'elapsed_days' => $elapsedDays, 'reminder_type' => $park->reminder_type, 'execution_flag' => $executionFlag, 'should_execute' => $shouldExecute ]); return [ 'should_execute' => $shouldExecute, 'reason' => $shouldExecute ? '実行対象' : "実行フラグ=0(経過日数:{$elapsedDays}, リマインダー種別:{$park->reminder_type})", 'update_pattern' => $updatePattern, 'elapsed_days' => $elapsedDays, 'execution_flag' => $executionFlag ]; } catch (\Exception $e) { Log::error('実行タイミングチェックエラー', [ 'park_id' => $park->park_id ?? 'unknown', 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } } /** * 【処理2】定期更新対象者を取得する * * 仕様書に基づくSQLクエリ: * 定期契約マスタ T1 と 利用者マスタ T2 を結合して更新対象者の情報を取得する * * WHERE条件: * - T1.駐輪場ID = 処理0.駐輪場ID * - T1.更新可能日 <= 本日の日付 * - T1.解約フラグ = 0 * - T2.退会フラグ = 0 * - T1.授受フラグ = 1 * - T1.更新済フラグ is null * - 更新パターンによる有効期間Eの判定 * * @param object $park 駐輪場情報 * @param string $updatePattern 更新パターン("A" or "B") * @return array 定期更新対象者情報 */ private function getRegularUpdateTargetUsers($park, string $updatePattern): array { try { $now = Carbon::now(); $currentDate = $now->format('Y-m-d'); $todayDay = (int)$now->format('d'); // 本日の日(1-31) // DBから返る値は文字列なので、型変換して使用 $startDay = (int)$park->update_grace_period_start_date; // 例:"20" → 20 $endDay = (int)$park->update_grace_period_end_date; // 例:"6" → 6 // 有効期間E(契約終了日)の判定 $thisMonthEnd = $now->copy()->endOfMonth()->format('Y-m-d'); $lastMonthEnd = $now->copy()->subMonth()->endOfMonth()->format('Y-m-d'); $query = DB::table('regular_contract as T1') ->select([ 'T1.contract_id as 定期契約ID', 'T1.park_id as 駐輪場ID', 'T2.user_seq as 利用者ID', // user_seqが主キー 'T2.user_manual_regist_flag as 手動登録フラグ', 'T2.user_primemail as メールアドレス', 'T2.user_submail as 予備メールアドレス', 'T2.user_name as 氏名', 'T1.contract_periode as 有効期間E' ]) ->join('user as T2', 'T1.user_id', '=', 'T2.user_seq') // user_seqに結合 ->where('T1.park_id', $park->park_id) // 駐輪場ID ->where('T1.contract_updated_at', '<=', $currentDate) // 更新可能日 ->where('T1.contract_cancel_flag', 0) // 解約フラグ = 0 ->where('T2.user_quit_flag', 0) // 退会フラグ = 0 ->where('T1.contract_flag', 1) // 授受フラグ = 1 ->whereNull('T1.contract_renewal'); // 更新済フラグ is null // 仕様書:更新パターンによる有効期間Eの判定 if ($updatePattern === 'A') { // パターンA の場合: 有効期間E = 今月末 $query->where('T1.contract_periode', '=', $thisMonthEnd); } else { // パターンB の場合 if ($startDay <= $todayDay) { // 処理0.更新期間開始日 <= [本日の日付]の日 の場合 $query->where('T1.contract_periode', '=', $thisMonthEnd); } elseif ($endDay >= $todayDay) { // 処理0.更新期間終了日 >= [本日の日付]の日 の場合 $query->where('T1.contract_periode', '=', $lastMonthEnd); } } $targetUsers = $query->get()->toArray(); Log::info('定期更新対象者取得完了', [ 'park_id' => $park->park_id, 'update_pattern' => $updatePattern, 'this_month_end' => $thisMonthEnd, 'last_month_end' => $lastMonthEnd, 'target_users_count' => count($targetUsers) ]); return $targetUsers; } catch (\Exception $e) { Log::error('定期更新対象者取得エラー', [ 'park_id' => $park->park_id ?? 'unknown', 'update_pattern' => $updatePattern ?? 'unknown', 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } } /** * 【処理3】対象者の処理実行 * * 手動登録フラグによって処理を分岐: * - = 0 (ウェブ申込み): SHJ-7メール送信を呼び出し * - その他: オペレーターキュー追加処理 * * @param object $targetUser 対象者情報 * @param object $park 駐輪場情報 * @return array 処理結果 */ private function processTargetUser($targetUser, $park): array { try { $manualRegistFlag = $targetUser->手動登録フラグ ?? 1; if ($manualRegistFlag == 0) { // 仕様書:手動登録フラグ = 0(ウェブ申込み)の場合 // SHJ-7メール送信処理を呼び出し return $this->sendReminderMail($targetUser); } else { // 仕様書:手動登録フラグ <> 0 の場合 // オペレーターキュー追加処理 // ※文書には詳細仕様なし。他のService実装を参考に実装 return $this->addToOperatorQueue($targetUser, $park); } } catch (\Exception $e) { Log::error('対象者処理エラー', [ 'user_id' => $targetUser->利用者ID ?? 'unknown', 'contract_id' => $targetUser->定期契約ID ?? 'unknown', 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ 'type' => 'error', 'success' => false, 'message' => $e->getMessage() ]; } } /** * リマインダーメール送信処理 * * 仕様書:SHJ-7メール送信を呼び出し、使用プログラムID=200を使用 * * @param object $targetUser 対象者情報 * @return array 送信結果 */ private function sendReminderMail($targetUser): array { try { $mailAddress = $targetUser->メールアドレス ?? ''; $backupMailAddress = $targetUser->予備メールアドレス ?? ''; $mailTemplateId = 200; // 仕様書:使用プログラムID = 200 Log::info('SHJ-7メール送信処理呼び出し', [ 'user_id' => $targetUser->利用者ID, 'contract_id' => $targetUser->定期契約ID, 'mail_address' => $mailAddress, 'backup_mail_address' => $backupMailAddress, 'mail_template_id' => $mailTemplateId ]); // 仕様書:共通処理「SHJ-7 メール送信」を呼び出し $mailResult = $this->mailSendService->executeMailSend( $mailAddress, $backupMailAddress, $mailTemplateId ); if ($mailResult['success']) { // 仕様書:処理結果 = 0(正常)の場合 Log::info('メール送信成功', [ 'user_id' => $targetUser->利用者ID, 'contract_id' => $targetUser->定期契約ID ]); return [ 'type' => 'mail_success', 'success' => true, 'message' => 'メール送信成功', 'error_info' => null ]; } else { // 仕様書:その他の場合(異常) // バッチコメントに「処理2.定期契約ID」+「SHJ-7 メール送信.異常情報」を設定する(後ろに足す) $errorInfo = "定期契約ID:{$targetUser->定期契約ID} / SHJ-7 メール送信.異常情報:{$mailResult['message']}"; Log::warning('メール送信失敗', [ 'user_id' => $targetUser->利用者ID, 'contract_id' => $targetUser->定期契約ID, 'error' => $mailResult['message'] ]); return [ 'type' => 'mail_error', 'success' => false, 'message' => 'メール送信失敗', 'error_info' => $errorInfo ]; } } catch (\Exception $e) { Log::error('リマインダーメール送信エラー', [ 'user_id' => $targetUser->利用者ID ?? 'unknown', 'contract_id' => $targetUser->定期契約ID ?? 'unknown', 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); // 仕様書準拠のエラー情報フォーマット $errorInfo = "定期契約ID:{$targetUser->定期契約ID} / SHJ-7 メール送信.異常情報:例外エラー - {$e->getMessage()}"; return [ 'type' => 'mail_error', 'success' => false, 'message' => 'メール送信例外エラー', 'error_info' => $errorInfo ]; } } /** * オペレーターキュー追加処理 * * 仕様書には詳細記載なし。 * 他のService(ShjOneService、ShjSixService)の実装を参考に実装。 * * @param object $targetUser 対象者情報 * @param object $park 駐輪場情報 * @return array 追加結果 */ private function addToOperatorQueue($targetUser, $park): array { try { // operator_queテーブルに登録 $operatorQue = OperatorQue::create([ 'que_class' => 5, // 定期更新通知(OperatorQueモデルの定数参照) 'user_id' => $targetUser->利用者ID, 'contract_id' => $targetUser->定期契約ID, 'park_id' => $targetUser->駐輪場ID, 'que_comment' => sprintf( '定期更新通知 / 契約ID:%s / 利用者:%s / 駐輪場:%s', $targetUser->定期契約ID, $targetUser->氏名 ?? '', $park->park_name ?? '' ), 'que_status' => 1, // キュー発生 'que_status_comment' => '', 'work_instructions' => '', 'operator_id' => 9999999, // 仕様書準拠:固定値(他Serviceと同様) 'created_at' => now(), 'updated_at' => now() ]); Log::info('オペレーターキュー追加成功', [ 'que_id' => $operatorQue->que_id, 'que_class' => 5, 'user_id' => $targetUser->利用者ID, 'contract_id' => $targetUser->定期契約ID, 'park_id' => $targetUser->駐輪場ID ]); return [ 'type' => 'queue_success', 'success' => true, 'message' => 'オペレーターキュー追加成功', 'que_id' => $operatorQue->que_id ]; } catch (\Exception $e) { Log::error('オペレーターキュー追加エラー', [ 'user_id' => $targetUser->利用者ID ?? 'unknown', 'contract_id' => $targetUser->定期契約ID ?? 'unknown', 'park_id' => $targetUser->駐輪場ID ?? 'unknown', 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ 'type' => 'queue_error', 'success' => false, 'message' => 'オペレーターキュー追加エラー: ' . $e->getMessage() ]; } } /** * 【処理4】SHJ-8バッチ処理ログ作成 * * 仕様書に基づくSHJ-8共通処理呼び出し * BatchLog統一システムを使用してバッチ処理の実行ログを記録 * ※各駐輪場ごとに1回実行される * * @param object $park 駐輪場情報 * @param string $batchComment バッチコメント * @param int $mailSuccessCount メール正常終了件数 * @param int $mailErrorCount メール異常終了件数 * @param int $queueSuccessCount キュー登録正常終了件数 * @param int $queueErrorCount キュー登録異常終了件数 * @return void */ private function createShjBatchLog( $park, string $batchComment, int $mailSuccessCount, int $mailErrorCount, int $queueSuccessCount, int $queueErrorCount ): void { try { // 仕様書:SHJ-8パラメータ設定 $deviceId = 9999999; // バッチ処理用固定デバイスID(他Serviceと同様) $processName = 'SHJ-3'; $jobName = 'SHJ-3定期更新リマインダー'; $status = BatchLog::STATUS_SUCCESS; // 仕様書:ステータスコメント生成 // 「内部変数.バッチコメント」+ "/" + 「処理1.駐輪場名」 // + ":メール正常終了件数" + 「内部変数.メール正常終了件数」 // + "、メール異常終了件数" + 「内部変数.メール異常終了件数」 // + "、キュー登録正常終了件数" + 「内部変数.キュー登録正常終了件数」 // + "、キュー登録異常終了件数" + 「内部変数.キュー登録異常終了件数」 $statusComment = ($batchComment ? $batchComment . ' / ' : '') . "{$park->park_name} : " . "メール正常終了件数={$mailSuccessCount}、" . "メール異常終了件数={$mailErrorCount}、" . "キュー登録正常終了件数={$queueSuccessCount}、" . "キュー登録異常終了件数={$queueErrorCount}"; $createdDate = now()->format('Y/m/d'); $updatedDate = now()->format('Y/m/d'); Log::info('SHJ-8バッチ処理ログ作成', [ 'park_id' => $park->park_id, 'park_name' => $park->park_name, 'device_id' => $deviceId, 'process_name' => $processName, 'job_name' => $jobName, 'status' => $status, 'status_comment' => $statusComment ]); // 仕様書:共通処理「SHJ-8 バッチ処理ログ作成」を呼び出す // BatchLog::createBatchLog を使用して統一的にログを記録 BatchLog::createBatchLog( $processName, $status, [ 'device_id' => $deviceId, 'job_name' => $jobName, 'park_id' => $park->park_id, 'park_name' => $park->park_name, 'status_comment' => $statusComment, 'statistics' => [ 'mail_success_count' => $mailSuccessCount, 'mail_error_count' => $mailErrorCount, 'queue_success_count' => $queueSuccessCount, 'queue_error_count' => $queueErrorCount, 'batch_comment' => $batchComment ], 'shj8_params' => [ 'device_id' => $deviceId, 'process_name' => $processName, 'job_name' => $jobName, 'status' => $status, 'created_date' => $createdDate, 'updated_date' => $updatedDate ] ], $statusComment ); } catch (\Exception $e) { Log::error('SHJ-8バッチ処理ログ作成エラー', [ 'park_id' => $park->park_id ?? 'unknown', 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); // 仕様書:SHJ-8でエラーが発生してもメイン処理は継続 // エラーログのみ出力 } } }