shjEightService = $shjEightService; } /** * バッチ処理用の有効なデバイスIDを取得 * * deviceテーブルから最初の有効なdevice_idを取得する。 * データが存在しない場合は例外をスローする。 * * @return int 有効なdevice_id * @throws \Exception deviceテーブルにレコードが存在しない場合 */ private function getBatchDeviceId(): int { $device = Device::orderBy('device_id')->first(); if (!$device) { throw new \Exception('deviceテーブルにレコードが存在しません。SHJ-8バッチログ作成に必要なデバイスIDを取得できません。'); } Log::debug('SHJ-11 バッチ用デバイスID取得', [ 'device_id' => $device->device_id ]); return $device->device_id; } /** * 現在使用中のprice主表名を取得する * * setting.web_master から価格マスタテーブル名を決定 * web_master の値('_a' または '_b')から 'price_a' または 'price_b' を返す * * @return string price主表名('price_a' または 'price_b') * @throws \Exception setting取得失敗時 */ private function getPriceTableName(): string { try { $setting = Setting::getSettings(); if (!$setting || empty($setting->web_master)) { throw new \Exception('setting.web_masterの取得に失敗しました'); } // web_master の値:'_a' または '_b' $webMaster = $setting->web_master; // 'price_' + ('_a' → 'a' / '_b' → 'b') $priceTable = 'price_' . ltrim($webMaster, '_'); Log::info('SHJ-11 price主表名決定', [ 'web_master' => $webMaster, 'price_table' => $priceTable ]); return $priceTable; } catch (\Exception $e) { Log::error('SHJ-11 price主表名取得エラー', [ 'error' => $e->getMessage() ]); throw $e; } } /** * 【処理1】集計単位每个の契約台数を算出する * * 集計単位: 駐輪場ID + 車種区分ID + 駐輪分類ID + ゾーンID * * SQL仕様に基づく複雑なJOIN処理: * - regular_contract (T1) * - park (T2) * - psection (T4) * - price_a/price_b (T5) ※web_master設定により動的に決定 * - ptype (T3) * * @return array 契約台数集計結果 */ public function calculateContractCounts(): array { try { // setting.web_master から使用するprice主表を決定 $priceTable = $this->getPriceTableName(); $query = DB::table('regular_contract as T1') ->select([ 'T1.park_id', // 駐輪場ID 'T2.park_name', // 駐輪場名 'T5.psection_id', // 車種区分ID 'T4.psection_subject', // 車種区分名 'T5.ptype_id', // 駐輪分類ID 'T3.ptype_subject', // 駐輪分類名 'T1.zone_id', // ゾーンID DB::raw('count(T1.contract_id) as cnt') // 契約台数 ]) // park テーブルとの JOIN ->join('park as T2', 'T1.park_id', '=', 'T2.park_id') // psection テーブルとの JOIN ->join('psection as T4', 'T1.psection_id', '=', 'T4.psection_id') // price_a/price_b テーブルとの複合条件 JOIN(動的テーブル名) ->join(DB::raw($priceTable . ' 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'); }) // ptype テーブルとの JOIN ->join('ptype as T3', 'T5.ptype_id', '=', 'T3.ptype_id') ->where([ ['T1.contract_flag', '=', 1], // 授受フラグ = 1 ['T2.park_close_flag', '=', 0], // 閉設フラグ = 0 ]) // 契約有効期間内の条件(仕様書指定:'%y.%m.%d'形式) ->whereRaw("date_format(now(), '%y.%m.%d') BETWEEN T1.contract_periods AND T1.contract_periode") // zone_idがNULLのレコードは除外(ゾーンマスタ更新不可のため) ->whereNotNull('T1.zone_id') ->groupBy([ 'T1.park_id', 'T2.park_name', 'T5.psection_id', 'T4.psection_subject', 'T5.ptype_id', 'T3.ptype_subject', 'T1.zone_id' ]) ->get(); Log::info('SHJ-11 契約台数算出完了', [ 'count' => $query->count(), 'price_table' => $priceTable, 'sql_conditions' => [ 'contract_flag' => 1, 'park_close_flag' => 0, 'date_format' => '%y.%m.%d', 'contract_period_check' => 'BETWEEN contract_periods AND contract_periode' ] ]); return $query->toArray(); } catch (\Exception $e) { Log::error('SHJ-11 契約台数算出エラー', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } } /** * 【処理2・3】ゾーンマスタ管理処理 * * 処理フロー(仕様書準拠): * 処理1の取得レコード数分繰り返し: * 【処理2】ゾーンマスタを取得する * 【判断2】取得判定 * - ゾーンマスタなし → INSERT(トランザクション内) → commit → 【処理4】SHJ-8(トランザクション外) → 次へ * - ゾーンマスタあり → 【判断3】契約台数チェック → 【処理3】UPDATE(トランザクション内) → commit → 【処理4】SHJ-8(トランザクション外) → 次へ * * ※SHJ-8ログは各レコード処理後に確実に記録するため、トランザクション外で実行 * * @param array $contractCounts 契約台数集計結果 * @return array 処理結果 */ public function processZoneManagement(array $contractCounts): array { $createdZones = 0; $updatedZones = 0; $overCapacityCount = 0; $batchLogErrors = []; try { // 処理1の取得レコード数分繰り返し foreach ($contractCounts as $contractData) { try { // 【防御的チェック】必須キーがNULLの場合はスキップ if (empty($contractData->zone_id) || empty($contractData->psection_id) || empty($contractData->ptype_id)) { Log::warning('SHJ-11 必須キー欠落のためスキップ', [ 'park_id' => $contractData->park_id ?? null, 'zone_id' => $contractData->zone_id ?? null, 'psection_id' => $contractData->psection_id ?? null, 'ptype_id' => $contractData->ptype_id ?? null ]); $batchLogErrors[] = [ 'park_id' => $contractData->park_id ?? null, 'zone_id' => $contractData->zone_id ?? null, 'error' => '必須キー欠落(zone_id/psection_id/ptype_id)' ]; continue; } // 各レコードごとに独立したトランザクションを開始 DB::beginTransaction(); // 【処理2】ゾーンマスタを取得する $zoneData = $this->getZoneData($contractData); // 【判断2】取得判定 if (!$zoneData) { // ゾーンマスタなし → INSERT $createResult = $this->createZoneData($contractData); if (!$createResult['success']) { // INSERT失敗時のエラー処理 DB::rollBack(); Log::error('SHJ-11 ゾーンマスタINSERT失敗', [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'error' => $createResult['message'] ]); $batchLogErrors[] = [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'error' => 'INSERT失敗: ' . $createResult['message'] ]; // INSERT失敗時も次の繰り返しへ continue; } // INSERTトランザクションをcommit DB::commit(); $createdZones++; // status_comment作成(INSERT分岐) $statusComment = $this->formatStatusComment( $contractData->park_name, $contractData->ptype_subject, $contractData->psection_subject, $contractData->zone_id, '' // INSERT分岐では台数アラートなし ); // 【処理4】bat_job_logに直接書き込み(トランザクション外・INSERT分岐) $batchLogResult = $this->writeBatJobLog( $contractData, $statusComment, false // 限界台数超過なし ); // bat_job_log書き込み結果チェック if (!$batchLogResult['success']) { Log::warning('bat_job_log書き込み失敗(INSERT分岐)', [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'error_message' => $batchLogResult['error_message'] ?? null ]); // bat_job_log書き込み失敗をbatch_log_errorsに記録 $batchLogErrors[] = [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'error' => 'bat_job_log書き込み失敗(INSERT分岐): ' . ($batchLogResult['error_message'] ?? 'unknown error') ]; } // INSERT分岐は【処理4】後に次の繰り返しへ continue; } // ゾーンマスタあり → 【判断3】契約台数チェック $isOverCapacity = $this->checkCapacityLimit($contractData, $zoneData); if ($isOverCapacity) { $overCapacityCount++; Log::warning('SHJ-11 限界台数超過検出', [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'current_count' => $contractData->cnt, 'limit_capacity' => $zoneData->zone_tolerance ?? 0 ]); } // 【処理3】契約台数を反映する(UPDATE) $updateResult = $this->updateZoneContractCount($contractData); if (!$updateResult['success']) { // UPDATE失敗時のエラー処理 DB::rollBack(); Log::error('SHJ-11 ゾーンマスタUPDATE失敗', [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'error' => $updateResult['message'] ]); $batchLogErrors[] = [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'error' => 'UPDATE失敗: ' . $updateResult['message'] ]; // UPDATE失敗時も次の繰り返しへ continue; } // UPDATEトランザクションをcommit DB::commit(); $updatedZones++; // status_comment作成(UPDATE分岐) $capacityAlert = $isOverCapacity ? '限界収容台数を超えています。' : ''; $statusComment = $this->formatStatusComment( $contractData->park_name, $contractData->ptype_subject, $contractData->psection_subject, $contractData->zone_id, $capacityAlert ); // 【処理4】bat_job_logに直接書き込み(トランザクション外・UPDATE分岐) $batchLogResult = $this->writeBatJobLog( $contractData, $statusComment, $isOverCapacity ); // bat_job_log書き込み結果チェック if (!$batchLogResult['success']) { Log::warning('bat_job_log書き込み失敗(UPDATE分岐)', [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'error_message' => $batchLogResult['error_message'] ?? null ]); // bat_job_log書き込み失敗をbatch_log_errorsに記録 $batchLogErrors[] = [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'error' => 'bat_job_log書き込み失敗(UPDATE分岐): ' . ($batchLogResult['error_message'] ?? 'unknown error') ]; } } catch (\Exception $e) { // 例外時はロールバック(アクティブなトランザクションがある場合のみ) if (DB::transactionLevel() > 0) { DB::rollBack(); } // 個別レコードエラーもログ記録して次へ進む $batchLogErrors[] = [ 'park_id' => $contractData->park_id ?? null, 'zone_id' => $contractData->zone_id ?? null, 'error' => $e->getMessage() ]; Log::warning('SHJ-11 個別レコード処理エラー', [ 'park_id' => $contractData->park_id ?? null, 'zone_id' => $contractData->zone_id ?? null, 'error' => $e->getMessage() ]); // エラーでも次の繰り返しへ続行 continue; } } return [ 'success' => true, 'created_zones' => $createdZones, 'updated_zones' => $updatedZones, 'over_capacity_count' => $overCapacityCount, 'batch_log_errors' => $batchLogErrors, 'message' => '現在契約台数集計処理完了' ]; } catch (\Exception $e) { // 外層エラー時のロールバック // ※各レコード処理は独立トランザクションのため、 // ここでのrollBackは不要だが安全のため実行 if (DB::transactionLevel() > 0) { DB::rollBack(); } Log::error('SHJ-11 ゾーンマスタ管理処理全体エラー', [ 'error' => $e->getMessage(), 'created_zones' => $createdZones, 'updated_zones' => $updatedZones ]); return [ 'success' => false, 'created_zones' => $createdZones, 'updated_zones' => $updatedZones, 'over_capacity_count' => $overCapacityCount, 'batch_log_errors' => $batchLogErrors, 'message' => 'ゾーンマスタ管理処理エラー: ' . $e->getMessage(), 'details' => $e->getTraceAsString() ]; } } /** * ゾーンマスタデータ取得 * * 集計単位に対応するゾーンマスタを取得 * * @param object $contractData 契約台数集計データ * @return object|null ゾーンマスタデータ */ private function getZoneData($contractData) { try { return DB::table('zone') ->select([ 'zone_id', 'park_id', 'ptype_id', 'psection_id', 'zone_name', 'zone_number', // 現在契約台数 'zone_standard', // 標準収容台数 'zone_tolerance' // 限界収容台数 ]) ->where([ ['park_id', '=', $contractData->park_id], ['psection_id', '=', $contractData->psection_id], ['ptype_id', '=', $contractData->ptype_id], ['zone_id', '=', $contractData->zone_id] ]) ->first(); } catch (\Exception $e) { Log::error('SHJ-11 ゾーンマスタ取得エラー', [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id, 'error' => $e->getMessage() ]); throw $e; } } /** * ゾーンマスタ新規作成 * * 仕様書に基づくINSERT処理 * * @param object $contractData 契約台数集計データ * @return array 作成結果 */ private function createZoneData($contractData): array { try { // 新規ゾーンマスタのデフォルト値設定 $newZoneData = [ 'zone_id' => $contractData->zone_id, 'park_id' => $contractData->park_id, 'ptype_id' => $contractData->ptype_id, 'psection_id' => $contractData->psection_id, 'zone_name' => null, // デフォルトnull 'zone_number' => $contractData->cnt, // 算出した契約台数 'zone_standard' => null, // デフォルトnull 'zone_tolerance' => null, // デフォルトnull 'zone_sort' => null, // デフォルトnull 'delete_flag' => 0, // 削除フラグOFF 'created_at' => now(), 'updated_at' => now(), 'ope_id' => 9999999 // 仕様書指定値 ]; DB::table('zone')->insert($newZoneData); Log::info('SHJ-11 ゾーンマスタ新規作成完了', [ 'zone_id' => $contractData->zone_id, 'park_id' => $contractData->park_id, 'contract_count' => $contractData->cnt ]); return [ 'success' => true, 'zone_data' => (object) $newZoneData, 'message' => 'ゾーンマスタを新規作成しました' ]; } catch (\Exception $e) { Log::error('SHJ-11 ゾーンマスタ新規作成エラー', [ 'zone_id' => $contractData->zone_id, 'park_id' => $contractData->park_id, 'error' => $e->getMessage() ]); return [ 'success' => false, 'zone_data' => null, 'message' => 'ゾーンマスタ新規作成エラー: ' . $e->getMessage() ]; } } /** * 契約台数限界チェック * * 【判断3】契約台数 > 限界収容台数の判定 * * @param object $contractData 契約台数集計データ * @param object|null $zoneData ゾーンマスタデータ * @return bool 限界台数超過フラグ */ private function checkCapacityLimit($contractData, $zoneData = null): bool { if (!$zoneData || is_null($zoneData->zone_tolerance)) { // ゾーンデータが存在しないか、限界収容台数が未設定の場合は超過判定しない return false; } // 契約台数 > 限界収容台数の判定 return $contractData->cnt > $zoneData->zone_tolerance; } /** * ゾーンマスタ契約台数更新 * * 【処理3】現在契約台数をゾーンマスタに反映 * * @param object $contractData 契約台数集計データ * @return array 更新結果 */ private function updateZoneContractCount($contractData): array { try { $updateData = [ 'zone_number' => $contractData->cnt, // 現在契約台数を更新 'updated_at' => now(), // 更新日時 'ope_id' => 9999999 // 更新オペレータID(INSERT時と同様、DB型int unsignedに対応) ]; $updated = DB::table('zone') ->where([ ['park_id', '=', $contractData->park_id], ['psection_id', '=', $contractData->psection_id], ['ptype_id', '=', $contractData->ptype_id], ['zone_id', '=', $contractData->zone_id] ]) ->update($updateData); if ($updated > 0) { Log::info('SHJ-11 ゾーンマスタ更新完了', [ 'zone_id' => $contractData->zone_id, 'park_id' => $contractData->park_id, 'new_contract_count' => $contractData->cnt ]); return [ 'success' => true, 'message' => 'ゾーンマスタ契約台数を更新しました' ]; } else { Log::warning('SHJ-11 ゾーンマスタ更新対象なし', [ 'zone_id' => $contractData->zone_id, 'park_id' => $contractData->park_id ]); return [ 'success' => false, 'message' => 'ゾーンマスタ更新対象が見つかりません' ]; } } catch (\Exception $e) { Log::error('SHJ-11 ゾーンマスタ更新エラー', [ 'zone_id' => $contractData->zone_id, 'park_id' => $contractData->park_id, 'error' => $e->getMessage() ]); return [ 'success' => false, 'message' => 'ゾーンマスタ更新エラー: ' . $e->getMessage() ]; } } /** * status_commentを仕様書指定フォーマットで作成 * * フォーマット: 駐輪場名/駐輪分類名/車種区分名/ゾーンID[台数アラート] * * @param string $parkName 駐輪場名 * @param string $ptypeSubject 駐輪分類名 * @param string $psectionSubject 車種区分名 * @param int $zoneId ゾーンID * @param string $capacityAlert 台数アラート(「限界収容台数を超えています。」または空文字) * @return string フォーマット済みstatus_comment */ private function formatStatusComment( string $parkName, string $ptypeSubject, string $psectionSubject, int $zoneId, string $capacityAlert ): string { $baseComment = $parkName . '/' . $ptypeSubject . '/' . $psectionSubject . '/' . $zoneId; if (!empty($capacityAlert)) { return $baseComment . ' ' . $capacityAlert; } return $baseComment; } /** * 【処理4】個別レコードのバッチ処理ログを作成する * * SHJ-11は業務固有のstatus_commentを記録するため、SHJ-8を使わずbat_job_logに直接書き込む * * bat_job_log登録内容: * 1. デバイスID * 2. プロセス名: SHJ-11 * 3. ジョブ名: SHJ-11現在契約台数集計 * 4. ステータス: success * 5. ステータスコメント: 業務固有(駐輪場名/駐輪分類名/車種区分名/ゾーンID[台数アラート]) * 6. 登録日時: 現在の日付 * 7. 更新日時: 現在の日付 * * @param object $contractData 契約台数集計データ * @param string $statusComment status_comment(台数アラート含む) * @param bool $isOverCapacity 限界台数超過フラグ * @return array 処理結果 ['success' => bool, 'error_message' => string|null] */ private function writeBatJobLog( $contractData, string $statusComment, bool $isOverCapacity ): array { try { // SHJ-8バッチ処理ログ作成パラメータ設定 $deviceId = $this->getBatchDeviceId(); // deviceテーブルから有効なIDを取得 $processName = 'SHJ-11'; $jobName = 'SHJ-11現在契約台数集計'; $status = 'success'; $today = now()->format('Y/m/d'); Log::info('SHJ-8バッチ処理ログ作成', [ 'device_id' => $deviceId, 'process_name' => $processName, 'job_name' => $jobName, 'status' => $status, 'status_comment' => $statusComment, 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id ]); // SHJ-8サービスを呼び出し $this->shjEightService->execute( $deviceId, $processName, $jobName, $status, $statusComment, $today, $today ); Log::info('SHJ-8バッチ処理ログ作成完了', [ 'park_id' => $contractData->park_id, 'zone_id' => $contractData->zone_id ]); return [ 'success' => true, 'error_message' => null ]; } catch (\Exception $e) { Log::error('bat_job_log書き込みエラー', [ 'park_id' => $contractData->park_id ?? null, 'zone_id' => $contractData->zone_id ?? null, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); // エラーが発生してもメイン処理は継続 return [ 'success' => false, 'error_message' => $e->getMessage() ]; } } /** * 【判断1】取得件数=0時のバッチログ作成 * * 取得件数が0件の場合に呼び出される特別なバッチログ作成 * bat_job_logに直接書き込み * * bat_job_log登録内容: * 1. デバイスID * 2. プロセス名: SHJ-11 * 3. ジョブ名: SHJ-11現在契約台数集計 * 4. ステータス: success * 5. ステータスコメント: 全駐輪場契約なし * 6. 登録日時: 現在の日付 * 7. 更新日時: 現在の日付 * * @return array 処理結果 ['success' => bool, 'error_message' => string|null] */ public function writeBatJobLogForNoContracts(): array { try { // SHJ-8バッチ処理ログ作成パラメータ設定 $deviceId = $this->getBatchDeviceId(); // deviceテーブルから有効なIDを取得 $processName = 'SHJ-11'; $jobName = 'SHJ-11現在契約台数集計'; $status = 'success'; $statusComment = '全駐輪場契約なし'; $today = now()->format('Y/m/d'); Log::info('SHJ-8バッチ処理ログ作成(対象なし)', [ 'device_id' => $deviceId, 'process_name' => $processName, 'job_name' => $jobName, 'status' => $status, 'status_comment' => $statusComment ]); // SHJ-8サービスを呼び出し $this->shjEightService->execute( $deviceId, $processName, $jobName, $status, $statusComment, $today, $today ); Log::info('SHJ-8バッチ処理ログ作成完了(対象なし)'); return [ 'success' => true, 'error_message' => null ]; } catch (\Exception $e) { Log::error('bat_job_log書き込みエラー(対象なし)', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ 'success' => false, 'error_message' => $e->getMessage() ]; } } }