From e1073e25773c32c0208bdc1a27be88eaf0b1ec01 Mon Sep 17 00:00:00 2001 From: "unhi.go" Date: Fri, 20 Feb 2026 20:16:47 +0800 Subject: [PATCH] =?UTF-8?q?SH-6=20SHJ-9=20=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- app/Console/Commands/ShjNineCommand.php | 170 +++------ app/Console/Commands/ShjTwoCommand.php | 116 ++++++ app/Models/OperatorQue.php | 1 + app/Services/ShjNineService.php | 393 ++++++++++++-------- app/Services/ShjSixService.php | 348 +++++++----------- app/Services/ShjTwoService.php | 470 ++++++++++++++++++++++++ config/shj2.php | 61 +++ routes/console.php | 9 + 8 files changed, 1072 insertions(+), 496 deletions(-) create mode 100644 app/Console/Commands/ShjTwoCommand.php create mode 100644 app/Services/ShjTwoService.php create mode 100644 config/shj2.php diff --git a/app/Console/Commands/ShjNineCommand.php b/app/Console/Commands/ShjNineCommand.php index d95871c..caab710 100644 --- a/app/Console/Commands/ShjNineCommand.php +++ b/app/Console/Commands/ShjNineCommand.php @@ -16,14 +16,13 @@ class ShjNineCommand extends Command { /** * コンソールコマンドの名前とシグネチャ - * + * * 引数: - * - type: 集計種別 (daily のみ) (必須) * - target_date: 集計対象日 (オプション、YYYY-MM-DD形式) * * @var string */ - protected $signature = 'shj:9 {type : 集計種別(daily)} {target_date? : 集計対象日(YYYY-MM-DD)}'; + protected $signature = 'shj:9 {target_date? : 集計対象日(YYYY-MM-DD)}'; /** * コンソールコマンドの説明 @@ -52,139 +51,60 @@ class ShjNineCommand extends Command /** * コンソールコマンドを実行 - * + * * 処理フロー: - * 1. パラメータ取得と検証 - * 2. 集計対象日設定 - * 3. 売上集計処理実行 - * 4. バッチログ作成 - * 5. 処理結果返却 + * 1. 集計対象日設定(JOB1) + * 2. 売上集計処理実行(JOB2~JOB4) + * 3. バッチログ作成(JOB5) + * + * 日付バリデーション・バッチログ作成はService側で実施 + * ステータスは常にsuccess(式様書準拠) * * @return int */ public function handle() { - try { - // 開始ログ出力 - $startTime = now(); - $this->info('SHJ-9 売上集計処理を開始します。'); - - // 引数取得 - $type = $this->argument('type'); - $targetDate = $this->argument('target_date'); - - Log::info('SHJ-9 売上集計処理開始', [ - 'start_time' => $startTime, - 'type' => $type, - 'target_date' => $targetDate - ]); + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-9 売上集計処理を開始します。'); - // パラメータ検証 - if (!$this->validateParameters($type, $targetDate)) { - $this->error('パラメータが不正です。'); - return self::FAILURE; - } - - // 集計対象日設定 - $aggregationDate = $this->determineAggregationDate($type, $targetDate); - - $this->info("集計種別: {$type}"); - $this->info("集計対象日: {$aggregationDate}"); - - // SHJ-9処理実行 - $result = $this->shjNineService->executeEarningsAggregation($type, $aggregationDate); - - // 処理結果確認 - if ($result['success']) { - $endTime = now(); - $this->info('SHJ-9 売上集計処理が正常に完了しました。'); - $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); - $this->info("処理結果: 駐輪場数 {$result['processed_parks']}, 集計レコード数 {$result['summary_records']}"); - - Log::info('SHJ-9 売上集計処理完了', [ - 'end_time' => $endTime, - 'duration_seconds' => $startTime->diffInSeconds($endTime), - 'result' => $result - ]); - - return self::SUCCESS; - } else { - $this->error('SHJ-9 売上集計処理でエラーが発生しました: ' . $result['message']); - Log::error('SHJ-9 売上集計処理エラー', [ - 'error' => $result['message'], - 'details' => $result['details'] ?? null - ]); - - return self::FAILURE; - } - - } catch (\Exception $e) { - $this->error('SHJ-9 売上集計処理で予期しないエラーが発生しました: ' . $e->getMessage()); - Log::error('SHJ-9 売上集計処理例外エラー', [ - 'exception' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - return self::FAILURE; - } - } - - /** - * パラメータの妥当性を検証 - * - * @param string $type 集計種別 - * @param string|null $targetDate 対象日 - * @return bool 検証結果 - */ - private function validateParameters(string $type, ?string $targetDate): bool - { - // 集計種別チェック(SHJ-9 は日次のみ対応) - if ($type !== 'daily') { - $this->error('SHJ-9 は日次集計(daily)のみ対応しています。月次/年次は SHJ-10 を使用してください。'); - return false; - } - - // 対象日形式チェック(指定されている場合) - if ($targetDate && !$this->isValidDateFormat($targetDate)) { - $this->error('対象日の形式が正しくありません(YYYY-MM-DD形式で指定してください)。'); - return false; - } - - return true; - } - - /** - * 集計対象日を決定 - * - * @param string $type 集計種別(daily 固定) - * @param string|null $targetDate 指定日 - * @return string 集計対象日 - */ - private function determineAggregationDate(string $type, ?string $targetDate): string - { - if ($targetDate) { - return $targetDate; - } + // 集計種別は日次固定(SHJ-9は日次のみ) + $type = 'daily'; + // 集計対象日設定(JOB1) + $targetDate = $this->argument('target_date'); // パラメータ指定がない場合は昨日(本日の1日前) - return now()->subDay()->format('Y-m-d'); - } + $aggregationDate = $targetDate ?? now()->subDay()->format('Y-m-d'); - /** - * 日付形式の検証 - * - * @param string $date 日付文字列 - * @return bool 有効な日付形式かどうか - */ - private function isValidDateFormat(string $date): bool - { - // YYYY-MM-DD形式の正規表現チェック - if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { - return false; + Log::info('SHJ-9 売上集計処理開始', [ + 'start_time' => $startTime, + 'type' => $type, + 'target_date' => $aggregationDate + ]); + + $this->info("集計種別: {$type}"); + $this->info("集計対象日: {$aggregationDate}"); + + // SHJ-9処理実行(日付バリデーション・バッチログ作成含む) + $result = $this->shjNineService->executeEarningsAggregation($type, $aggregationDate); + + $endTime = now(); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + + if ($result['success']) { + $this->info('SHJ-9 売上集計処理が正常に完了しました。'); + $this->info("処理結果: 駐輪場数 {$result['processed_parks']}, 集計レコード数 {$result['summary_records']}"); + } else { + $this->info('SHJ-9 売上集計処理が完了しました(エラーあり): ' . $result['message']); } - // 実際の日付として有効かチェック - $dateParts = explode('-', $date); - return checkdate((int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]); + Log::info('SHJ-9 売上集計処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + // 式様書準拠: ステータスは常にsuccess + return self::SUCCESS; } } diff --git a/app/Console/Commands/ShjTwoCommand.php b/app/Console/Commands/ShjTwoCommand.php new file mode 100644 index 0000000..3403a1e --- /dev/null +++ b/app/Console/Commands/ShjTwoCommand.php @@ -0,0 +1,116 @@ +shjTwoService = $shjTwoService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 現時点の世代シフトを行う + * フルバックアップを行う + * バッチ処理ログを作成する + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-2 データバックアップ処理を開始します。'); + + Log::info('SHJ-2 データバックアップ処理開始', [ + 'start_time' => $startTime + ]); + + // SHJ-2メイン処理実行 + $result = $this->shjTwoService->execute(); + + $endTime = now(); + + // 処理結果に応じた出力 + if ($result['success']) { + $this->info('SHJ-2 データバックアップ処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + $this->info("ステータス: {$result['status']}"); + $this->info("コメント: {$result['status_comment']}"); + + Log::info('SHJ-2 データバックアップ処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'status' => $result['status'], + 'status_comment' => $result['status_comment'] + ]); + + return self::SUCCESS; + + } else { + $this->error('SHJ-2 データバックアップ処理でエラーが発生しました: ' . $result['status_comment']); + + Log::error('SHJ-2 データバックアップ処理エラー', [ + 'status' => $result['status'], + 'status_comment' => $result['status_comment'] + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-2 データバックアップ処理で予期しないエラーが発生しました: ' . $e->getMessage()); + + Log::error('SHJ-2 データバックアップ処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Models/OperatorQue.php b/app/Models/OperatorQue.php index 289c831..9ffcb0f 100644 --- a/app/Models/OperatorQue.php +++ b/app/Models/OperatorQue.php @@ -38,6 +38,7 @@ class OperatorQue extends Model 11 => '名寄せフリガナ照合エラー', 12 => '本人確認(減免更新)', 13 => '本人確認(学生更新)', + 14 => '集計対象エラー', 101 => 'サーバーエラー', 102 => 'プリンタエラー', 103 => 'スキャナーエラー', diff --git a/app/Services/ShjNineService.php b/app/Services/ShjNineService.php index b8ffec9..13e0d8a 100644 --- a/app/Services/ShjNineService.php +++ b/app/Services/ShjNineService.php @@ -29,6 +29,11 @@ class ShjNineService */ const SUMMARY_TYPE_DAILY = 3; + /** + * 情報不備なしの固定文字列(全角スペース付き) + */ + const DATA_INTEGRITY_NONE = ' 情報不備:なし'; + /** * ShjEightService インスタンス * @@ -65,24 +70,27 @@ class ShjNineService public function executeEarningsAggregation(string $type, string $aggregationDate): array { $statusComments = []; // 内部変数.ステータスコメント - $dataIntegrityIssues = []; // 内部変数.情報不備 + $allDataIntegrity = []; // 内部変数.情報不備(なし含む全件) + $dataIntegrityIssues = []; // オペレータキュー作成用(NULL以外) try { // 【処理1】集計対象を設定する // パラメーター検証(日付形式チェック) if (!$this->isValidDateFormat($aggregationDate)) { - // 日付形式エラー時は【処理5】へ(warning扱い) + // 日付形式エラー時は【処理5】へ(仕様JOB1:ステータスコメント設定→JOB5) $statusComment = "売上集計(日次):パラメーターが不正です。(日付形式ではありません)"; - // 【処理5】オペレータキュー作成 - $this->createOperatorQueue($statusComment, null); + // 仕様JOB5:情報不備がNULL→オペレータキュー作成しない(日付エラーは情報不備ではない) - // SHJ-8 バッチ処理ログ作成 - $this->callShjEight('SHJ-9売上集計(日次)', 'success', $statusComment); + // SHJ-8 バッチ処理ログ作成(仕様JOB5:ステータスは常にsuccess) + $shjEightResult = $this->callShjEight('SHJ-9売上集計(日次)', 'success', $statusComment); + $this->evaluateShjEightResult($shjEightResult); return [ 'success' => true, // 仕様上はwarningで成功扱い - 'message' => $statusComment + 'message' => $statusComment, + 'processed_parks' => 0, + 'summary_records' => 0 ]; } @@ -99,13 +107,12 @@ class ShjNineService // 【判断1】取得件数判定 if (empty($parkInfo)) { $statusComment = '売上集計(日次):駐輪場マスタが存在していません。'; - $statusComments[] = $statusComment; - // 【処理5】オペレータキュー作成 - $this->createOperatorQueue($statusComment, null); + // 仕様JOB2-STEP1:情報不備がNULL→オペレータキュー作成しない - // SHJ-8 バッチ処理ログ作成 - $this->callShjEight('SHJ-9売上集計(日次)', 'success', $statusComment); + // SHJ-8 バッチ処理ログ作成(仕様JOB5:ステータスは常にsuccess) + $shjEightResult = $this->callShjEight('SHJ-9売上集計(日次)', 'success', $statusComment); + $this->evaluateShjEightResult($shjEightResult); return [ 'success' => true, @@ -115,7 +122,7 @@ class ShjNineService ]; } - // 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する + // 仕様JOB3/JOB4:車種区分毎に算出→売上集計結果を登録 $summaryRecords = 0; $processedParks = 0; @@ -125,39 +132,53 @@ class ShjNineService $processedParks++; $summaryRecords += $result['summary_records']; - // 対象データなしの場合のステータスコメント収集 + // 仕様JOB4:レコード毎のステータスコメントを収集 + if (!empty($result['status_comments'])) { + $statusComments = array_merge($statusComments, $result['status_comments']); + } + + // 仕様JOB3-STEP1:対象データなしの場合のステータスコメント if (!empty($result['no_data_message'])) { $statusComments[] = $result['no_data_message']; } - // 情報不備を収集("なし"でない場合) - if ($result['data_integrity_issue'] !== '情報不備:なし') { - $dataIntegrityIssues[] = $result['data_integrity_issue']; + // 仕様JOB5:全parkの情報不備を収集(なし含む) + $allDataIntegrity[] = $result['data_integrity_issue']; + + // 仕様JOB5:NULL以外の場合にオペレータキューを登録 + if ($result['data_integrity_issue'] !== null) { + $dataIntegrityIssues[] = [ + 'park_id' => $park->park_id, + 'message' => $result['data_integrity_issue'] + ]; } } - // 最終ステータスコメント生成 - $finalStatusComment = "売上集計(日次):対象日={$targetDate}、駐輪場数={$processedParks}、集計レコード数={$summaryRecords}"; - if (!empty($dataIntegrityIssues)) { - $finalStatusComment .= "、情報不備=" . implode('、', $dataIntegrityIssues); + // 仕様JOB5:情報不備がNULL以外の場合、オペレータキューを登録 + foreach ($dataIntegrityIssues as $issue) { + $this->createOperatorQueue($issue['message'], $issue['park_id']); } - // 【処理5】オペレータキュー作成 - // ※ 駐輪場単位で既に作成済み(processEarningsForPark内で情報不備検出時に実施) if (!empty($dataIntegrityIssues)) { Log::warning('SHJ-9 情報不備検出', [ 'issues' => $dataIntegrityIssues ]); } - // SHJ-8 バッチ処理ログ作成 - $this->callShjEight('SHJ-9売上集計(日次)', 'success', $finalStatusComment); + // 仕様JOB5:ステータスコメント = 内部変数.ステータスコメント + 内部変数.情報不備(なし含む全件) + $allParts = array_merge($statusComments, $allDataIntegrity); + $finalStatusComment = implode("\n", $allParts); + + Log::info('SHJ-9 完全ステータスコメント', ['status_comment' => $finalStatusComment]); + + // 仕様JOB5:SHJ-8 バッチ処理ログ作成(ステータスは常にsuccess) + $shjEightResult = $this->callShjEight('SHJ-9売上集計(日次)', 'success', $finalStatusComment); + $this->evaluateShjEightResult($shjEightResult); Log::info('SHJ-9 売上集計処理完了', [ 'processed_parks' => $processedParks, 'summary_records' => $summaryRecords, - 'data_integrity_issues' => count($dataIntegrityIssues), - 'no_data_parks' => count($statusComments) + 'data_integrity_issues' => count($dataIntegrityIssues) ]); return [ @@ -173,7 +194,8 @@ class ShjNineService // SHJ-8 バッチ処理ログ作成(エラー時も作成) try { - $this->callShjEight('SHJ-9売上集計(日次)', 'error', $errorMessage); + $shjEightResult = $this->callShjEight('SHJ-9売上集計(日次)', 'success', $errorMessage); + $this->evaluateShjEightResult($shjEightResult); } catch (\Exception $shjException) { Log::error('SHJ-8呼び出しエラー', ['error' => $shjException->getMessage()]); } @@ -192,19 +214,21 @@ class ShjNineService } /** - * 日付形式の検証 + * 日付形式の検証(厳格:YYYY-MM-DD形式のみ許可) * * @param string $date 日付文字列 * @return bool 有効な日付形式かどうか */ private function isValidDateFormat(string $date): bool { - try { - $parsed = Carbon::parse($date); - return true; - } catch (\Exception $e) { + // YYYY-MM-DD形式の正規表現チェック + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { return false; } + + // 実際の日付として有効かチェック + $dateParts = explode('-', $date); + return checkdate((int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]); } /** @@ -242,82 +266,79 @@ class ShjNineService * * @param object $park 駐輪場情報 * @param string $targetDate 集計対象日(YYYY-MM-DD) - * @return array 処理結果 ['summary_records' => int, 'data_integrity_issue' => string, 'no_data_message' => string|null] + * @return array 処理結果 */ private function processEarningsForPark($park, string $targetDate): array { try { - // 0. 情報不備チェック + // 仕様JOB3-0:情報不備チェック(SQL-2) $dataIntegrityIssue = $this->checkDataIntegrity($park->park_id, $targetDate); - // 情報不備がある場合、駐輪場単位でオペレータキュー作成(仕様 todo/SHJ-9/SHJ-9.txt:253-263) - if ($dataIntegrityIssue !== '情報不備:なし') { - $this->createOperatorQueue($dataIntegrityIssue, $park->park_id); - } - - // ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎) + // 仕様JOB3-①:定期契約データ取得(車種区分・分類名1・定期有効月数毎) $regularData = $this->calculateRegularEarnings($park->park_id, $targetDate); - - // ② 一時金データ取得(車種毎) + + // 仕様JOB3-②:一時金データ取得(車種毎) $lumpsumData = $this->calculateLumpsumEarnings($park->park_id, $targetDate); - - // ③ 解約返戻金データ取得(車種区分毎) + + // 仕様JOB3-③:解約返戻金データ取得(車種区分毎) $refundData = $this->calculateRefundEarnings($park->park_id, $targetDate); - - // ④ 再発行データ取得(車種区分毎) + + // 仕様JOB3-④:再発行データ取得(車種区分毎) $reissueData = $this->calculateReissueCount($park->park_id, $targetDate); - - // 【判断2】データがいずれかあれば【処理4】へ + + // 仕様JOB3-STEP1:①②③④のデータがいずれかあれば→JOB4へ if (empty($regularData) && empty($lumpsumData) && empty($refundData) && empty($reissueData)) { - // 対象データなし - 仕様 todo/SHJ-9/SHJ-9.txt:172-175 + // 仕様JOB3-STEP1:対象データなし $noDataMessage = "売上集計(日次):対象日:{$targetDate}/駐輪場:{$park->park_name}:売上データが存在しません。"; - + return [ 'summary_records' => 0, 'data_integrity_issue' => $dataIntegrityIssue, - 'no_data_message' => $noDataMessage + 'no_data_message' => $noDataMessage, + 'status_comments' => [] ]; } - - // 【処理4】既存の売上集計結果を削除 - $this->deleteExistingSummary($park->park_id, $targetDate); - - // 【処理4】売上集計結果を登録 + + // 仕様JOB4:売上集計結果を登録(同一キーの既存レコードは各insert前に削除) $summaryRecords = 0; - - // ① 定期契約データがある場合:同じ組合せ(psection×usertype×months)を統合 + $statusComments = []; + + // ① 定期契約データ:同じ組合せ(psection×usertype×months)を統合 $mergedRegularData = $this->mergeRegularDataByGroup($regularData); foreach ($mergedRegularData as $key => $mergedRow) { - $this->createEarningsSummary($park, $mergedRow, $targetDate, 'regular'); - $summaryRecords++; - } - - // ②③④ 一時金・解約・再発行データがある場合(車種区分毎に集約) - $otherDataByPsection = $this->mergeOtherEarningsData($lumpsumData, $refundData, $reissueData); - foreach ($otherDataByPsection as $psectionId => $data) { - $this->createEarningsSummary($park, $data, $targetDate, 'other'); + $sc = $this->createEarningsSummary($park, $mergedRow, $targetDate, 'regular', $dataIntegrityIssue); + $statusComments[] = $sc; $summaryRecords++; } - + + // ②③④ 一時金・解約・再発行データ(車種区分毎に集約) + $otherDataByPsection = $this->mergeOtherEarningsData($lumpsumData, $refundData, $reissueData); + foreach ($otherDataByPsection as $psectionId => $data) { + $sc = $this->createEarningsSummary($park, $data, $targetDate, 'other', null); + $statusComments[] = $sc; + $summaryRecords++; + } + Log::info('駐輪場売上集計完了', [ 'park_id' => $park->park_id, 'park_name' => $park->park_name, 'summary_records' => $summaryRecords, 'data_integrity_issue' => $dataIntegrityIssue ]); - + return [ 'summary_records' => $summaryRecords, 'data_integrity_issue' => $dataIntegrityIssue, - 'no_data_message' => null + 'no_data_message' => null, + 'status_comments' => $statusComments ]; - + } catch (\Exception $e) { Log::error('駐輪場売上集計エラー', [ 'park_id' => $park->park_id, 'error' => $e->getMessage() ]); - + throw $e; } } @@ -333,8 +354,9 @@ class ShjNineService */ private function checkDataIntegrity(int $parkId, string $targetDate): string { + // 仕様SQL-2:情報不備チェック(ログ吐き出し) $incompleteContracts = DB::table('regular_contract') - ->select('contract_id') + ->select(['contract_id', 'contract_reduction', 'update_flag', 'psection_id', 'enable_months']) ->where('park_id', $parkId) ->where('contract_flag', 1) ->whereDate('contract_payment_day', '=', $targetDate) @@ -343,15 +365,26 @@ class ShjNineService ->orWhereNull('psection_id') ->orWhereNull('enable_months'); }) - ->pluck('contract_id') + ->get() ->toArray(); - + if (empty($incompleteContracts)) { - return '情報不備:なし'; + return self::DATA_INTEGRITY_NONE; } - - // 仕様フォーマット:"情報不備:" + 契約IDカンマ区切り - return '情報不備:' . implode(',', $incompleteContracts); + + // 仕様SQL-2:ログ吐き出し(詳細情報を出力) + Log::warning('SHJ-9 情報不備契約詳細', [ + 'park_id' => $parkId, + 'target_date' => $targetDate, + 'contracts' => $incompleteContracts + ]); + + // 仕様フォーマット:" 情報不備:" + 契約IDカンマ区切り(全角スペース付き) + $contractIds = array_map(function($c) { + return is_object($c) ? $c->contract_id : $c['contract_id']; + }, $incompleteContracts); + + return ' 情報不備:' . implode(',', $contractIds); } /** @@ -417,7 +450,7 @@ class ShjNineService ->whereDate('pay_date', '=', $targetDate) ->groupBy('type_class') ->get(); - + return $results->toArray(); } @@ -442,7 +475,7 @@ class ShjNineService ->whereDate('repayment_at', '=', $targetDate) ->groupBy('psection_id') ->get(); - + return $results->toArray(); } @@ -467,7 +500,7 @@ class ShjNineService ->whereDate('seal_day', '=', $targetDate) ->groupBy('psection_id') ->get(); - + return $results->toArray(); } @@ -611,66 +644,92 @@ class ShjNineService } /** - * 【処理4】既存の売上集計結果を削除 - * - * 仕様書のキー:駐輪場ID, 集計種別, 集計開始日, 集計終了日, 売上日付, 車種区分, 分類名1, 定期有効月数 - * 仕様 todo/SHJ-9/SHJ-9.txt:181-189 - * - * @param int $parkId 駐輪場ID - * @param string $targetDate 集計対象日(YYYY-MM-DD) - * @return void + * 車種区分名を取得 + * + * @param int|null $psectionId 車種区分ID + * @return string 車種区分名 */ - private function deleteExistingSummary(int $parkId, string $targetDate): void + private function getPsectionName(?int $psectionId): string { - // 仕様書どおり、同一キーの組み合わせで削除 - // 日次の場合、集計開始日・終了日はNULL、売上日付で判定 - DB::table('earnings_summary') - ->where('park_id', $parkId) - ->where('summary_type', self::SUMMARY_TYPE_DAILY) - ->whereNull('summary_start_date') - ->whereNull('summary_end_date') - ->where('earnings_date', $targetDate) - // psection_id, usertype_subject1, enable_months は - // レコードごとに異なるため、ここでは指定しない - ->delete(); - - Log::debug('既存の売上集計結果削除', [ - 'park_id' => $parkId, - 'target_date' => $targetDate - ]); + if ($psectionId === null) { + return ''; + } + + $name = DB::table('psection') + ->where('psection_id', $psectionId) + ->value('psection_subject'); + + return $name ?? ''; } /** - * 売上集計結果を登録 - * - * 仕様 todo/SHJ-9/SHJ-9.txt:181-247 - * + * 仕様JOB4:売上集計結果を削除→登録 + * + * 同一キー(駐車場ID,集計種別,集計開始日,集計終了日,売上日付,車種区分,分類名1,定期有効月数) + * が既に登録済みの場合削除した上で、新規レコードを登録する。 + * * @param object $park 駐輪場情報 * @param object $data 売上データ * @param string $targetDate 集計対象日(YYYY-MM-DD) * @param string $dataType データ種別(regular or other) - * @return void + * @param string|null $dataIntegrityIssue 情報不備メッセージ(regularのみ使用) + * @return string 仕様JOB4形式のステータスコメント */ - private function createEarningsSummary($park, $data, string $targetDate, string $dataType): void + private function createEarningsSummary($park, $data, string $targetDate, string $dataType, ?string $dataIntegrityIssue = null): string { + $usertypeSubject1 = $data->usertype_subject1 ?? null; + $enableMonths = $data->enable_months ?? 0; + + // 仕様JOB4:同一キーの既存レコードを削除 + $deleteQuery = DB::table('earnings_summary') + ->where('park_id', $park->park_id) + ->where('summary_type', self::SUMMARY_TYPE_DAILY) + ->whereNull('summary_start_date') + ->whereNull('summary_end_date') + ->where('earnings_date', $targetDate); + + // NULL値対応:psection_idがnullの場合はwhereNull + if ($data->psection_id === null) { + $deleteQuery->whereNull('psection_id'); + } else { + $deleteQuery->where('psection_id', $data->psection_id); + } + + // NULL値対応:usertype_subject1がnullの場合はwhereNull + if ($usertypeSubject1 === null) { + $deleteQuery->whereNull('usertype_subject1'); + } else { + $deleteQuery->where('usertype_subject1', $usertypeSubject1); + } + $deleteQuery->where('enable_months', $enableMonths); + $deleteQuery->delete(); + + // 仕様:集計備考の生成 + if ($dataType === 'regular' && $dataIntegrityIssue !== null) { + // 仕様JOB3-①:定期データのsummary_noteには情報不備を含める + $summaryNote = "SHJ-9:{$targetDate}{$dataIntegrityIssue}"; + } else { + // 仕様JOB3-②③④:otherデータのsummary_note + $summaryNote = "SHJ-9:{$targetDate}"; + } + $insertData = [ 'park_id' => $park->park_id, - 'summary_type' => self::SUMMARY_TYPE_DAILY, // 3 = 日次 - 'summary_start_date' => null, // 日次は NULL (仕様line 215) - 'summary_end_date' => null, // 日次は NULL (仕様line 216) + 'summary_type' => self::SUMMARY_TYPE_DAILY, + 'summary_start_date' => null, + 'summary_end_date' => null, 'earnings_date' => $targetDate, 'psection_id' => $data->psection_id, - 'usertype_subject1' => $data->usertype_subject1 ?? null, // 実際の分類名 - 'enable_months' => $data->enable_months ?? 0, // 実際の定期有効月数 - 'summary_note' => "SHJ-9:{$targetDate}", // 仕様line 236 + 'usertype_subject1' => $usertypeSubject1, + 'enable_months' => $enableMonths, + 'summary_note' => $summaryNote, 'created_at' => now(), 'updated_at' => now(), - 'operator_id' => self::BATCH_OPERATOR_ID // 9999999 (仕様line 239) + 'operator_id' => self::BATCH_OPERATOR_ID ]; - + if ($dataType === 'regular') { - // 定期契約データの場合:mergeRegularDataByGroup()で既に統合済み - // 新規/更新 × 減免/通常 の各件数・金額がすべて含まれている (仕様line 98-113) + // 仕様JOB3-①:定期契約データ $insertData = array_merge($insertData, [ 'regular_new_count' => $data->regular_new_count ?? 0, 'regular_new_amount' => $data->regular_new_amount ?? 0, @@ -680,43 +739,50 @@ class ShjNineService 'regular_update_amount' => $data->regular_update_amount ?? 0, 'regular_update_reduction_count' => $data->regular_update_reduction_count ?? 0, 'regular_update_reduction_amount' => $data->regular_update_reduction_amount ?? 0, - 'lumpsum_count' => 0, // 仕様line 106 - 'lumpsum' => 0, // 仕様line 107 - 'refunds' => 0, // 仕様line 108 - 'other_income' => 0, // 仕様line 109 - 'other_spending' => 0, // 仕様line 110 - 'reissue_count' => 0, // 仕様line 111 - 'reissue_amount' => 0 // 仕様line 112 + 'lumpsum_count' => 0, + 'lumpsum' => 0, + 'refunds' => 0, + 'other_income' => 0, + 'other_spending' => 0, + 'reissue_count' => 0, + 'reissue_amount' => 0 ]); } else { - // 一時金・解約・再発行データの場合:定期フィールドは0固定 (仕様line 152-167) + // 仕様JOB3-②③④:一時金・解約・再発行データ(定期フィールドは0固定) $insertData = array_merge($insertData, [ - 'regular_new_count' => 0, // 仕様line 152 - 'regular_new_amount' => 0, // 仕様line 153 - 'regular_new_reduction_count' => 0, // 仕様line 154 - 'regular_new_reduction_amount' => 0, // 仕様line 155 - 'regular_update_count' => 0, // 仕様line 156 - 'regular_update_amount' => 0, // 仕様line 157 - 'regular_update_reduction_count' => 0, // 仕様line 158 - 'regular_update_reduction_amount' => 0, // 仕様line 159 - 'lumpsum_count' => $data->lumpsum_count ?? 0, // 仕様line 160 - 'lumpsum' => $data->lumpsum ?? 0, // 仕様line 161 - 'refunds' => $data->refunds ?? 0, // 仕様line 162 - 'other_income' => 0, // 仕様line 163 - 'other_spending' => 0, // 仕様line 164 - 'reissue_count' => $data->reissue_count ?? 0, // 仕様line 165 - 'reissue_amount' => 0 // 仕様line 166: 0固定 + 'regular_new_count' => 0, + 'regular_new_amount' => 0, + 'regular_new_reduction_count' => 0, + 'regular_new_reduction_amount' => 0, + 'regular_update_count' => 0, + 'regular_update_amount' => 0, + 'regular_update_reduction_count' => 0, + 'regular_update_reduction_amount' => 0, + 'lumpsum_count' => $data->lumpsum_count ?? 0, + 'lumpsum' => $data->lumpsum ?? 0, + 'refunds' => $data->refunds ?? 0, + 'other_income' => 0, + 'other_spending' => 0, + 'reissue_count' => $data->reissue_count ?? 0, + 'reissue_amount' => 0 ]); } - + DB::table('earnings_summary')->insert($insertData); - + + // 仕様JOB4:レコード毎のステータスコメント生成(全データ型共通フォーマット) + $psectionName = $this->getPsectionName($data->psection_id); + $displayUsertype = $usertypeSubject1 ?? ''; + $statusComment = "売上集計(日次):対象日:{$targetDate}/駐輪場:{$park->park_name}/車種区分:{$psectionName}/分類名1:{$displayUsertype}/定期有効月数:{$enableMonths}"; + Log::debug('売上集計結果登録', [ 'park_id' => $park->park_id, 'psection_id' => $data->psection_id, 'data_type' => $dataType, 'target_date' => $targetDate ]); + + return $statusComment; } /** @@ -777,9 +843,9 @@ class ShjNineService * @param string $jobName ジョブ名 * @param string $status ステータス (success/error) * @param string $statusComment 業務固有のステータスコメント - * @return void + * @return array SHJ-8実行結果 */ - private function callShjEight(string $jobName, string $status, string $statusComment): void + private function callShjEight(string $jobName, string $status, string $statusComment): array { try { $device = Device::orderBy('device_id')->first(); @@ -787,7 +853,7 @@ class ShjNineService $today = now()->format('Y/m/d'); - $this->shjEightService->execute( + $result = $this->shjEightService->execute( $deviceId, 'SHJ-9', $jobName, @@ -799,9 +865,12 @@ class ShjNineService Log::info('SHJ-8 バッチ処理ログ作成完了', [ 'job_name' => $jobName, - 'status' => $status + 'status' => $status, + 'shj8_result' => $result['result'] ?? null ]); + return $result; + } catch (\Exception $e) { Log::error('SHJ-8 バッチ処理ログ作成エラー', [ 'error' => $e->getMessage(), @@ -811,4 +880,24 @@ class ShjNineService throw $e; } } + + /** + * SHJ-8実行結果の判定 + * + * 仕様JOB5の判定(処理結果=0/その他)をログで明示する。 + * + * @param array $shjEightResult SHJ-8返却値 + * @return void + */ + private function evaluateShjEightResult(array $shjEightResult): void + { + if ((int)($shjEightResult['result'] ?? 1) === 0) { + Log::info('SHJ-9 SHJ-8処理結果判定: 0(正常)'); + return; + } + + Log::warning('SHJ-9 SHJ-8処理結果判定: 0以外(異常)', [ + 'shj8_result' => $shjEightResult + ]); + } } diff --git a/app/Services/ShjSixService.php b/app/Services/ShjSixService.php index eee380e..256b4d2 100644 --- a/app/Services/ShjSixService.php +++ b/app/Services/ShjSixService.php @@ -201,63 +201,40 @@ class ShjSixService } // 【処理5】バッチ処理ログを作成する - SHJ-8呼び出し - // 仕様書準拠:キュー登録件数も含める - $statusComment = sprintf( - 'デバイス数: %d, アラート件数: %d, メール正常: %d件, メール異常: %d件, キュー登録正常: %d件, キュー登録異常: %d件', - count($devices), - $alertCount, - $totalMailSuccessCount, - $totalMailErrorCount, - $totalQueueSuccessCount, - $totalQueueErrorCount - ); - - // バッチコメントが累積されている場合は追加 - if (!empty($accumulatedBatchComment)) { - $statusComment .= ' | ' . $accumulatedBatchComment; - } + // 仕様書準拠:ステータスコメント = バッチコメント + 各件数 + $statusComment = $accumulatedBatchComment + . ':メール正常終了件数' . $totalMailSuccessCount + . '、メール異常終了件数' . $totalMailErrorCount + . '、キュー登録正常終了件数' . $totalQueueSuccessCount + . '、キュー登録異常終了件数' . $totalQueueErrorCount; $status = 'success'; // 仕様書:常に"success"で記録 $message = empty($warnings) ? 'SHJ-6 サーバ死活監視処理正常完了' : 'SHJ-6 サーバ死活監視処理完了(警告あり)'; - // 仕様書準拠:処理2で取得したデバイスIDを連結 + // 仕様書準拠:JOB5 は常に実行する(JOB4 の結果に関わらず) + // 注:bat_job_log.device_id は int unsigned のため連結文字列は格納不可 + // 先頭デバイスIDを使用。デバイスなしの場合は 0 + // (SHJ-8 はエラー返却するが、式様書上 result≠0 でも「処理を終了する」のため問題なし) $deviceIds = $devices->pluck('device_id')->toArray(); - $concatenatedDeviceIds = !empty($deviceIds) ? implode(',', $deviceIds) : ''; + $deviceId = !empty($deviceIds) ? (int)$deviceIds[0] : 0; - // SHJ-8 バッチ処理ログ作成(仕様書準拠) - // device_id = 処理2で取得したデバイスID(複数なら連結) - // process_name = 処理4のプロセス名(プリンタログから取得、なければ'SHJ-6') - // job_name = "SHJ-6サーバ死活監視" 固定 - // status = 常に "success" - $shj8Result = $this->createShjBatchLog([ - 'device_id' => $concatenatedDeviceIds, // 処理2のデバイスID(連結) - 'process_name' => $printerResult['process_name'] ?? 'SHJ-6', // 処理4のプロセス名 - 'job_name' => 'SHJ-6サーバ死活監視', - 'status' => 'success', // 仕様書:常にsuccess - 'status_comment' => $statusComment, - 'mail_success_count' => $totalMailSuccessCount, - 'mail_error_count' => $totalMailErrorCount, - 'queue_success_count' => $totalQueueSuccessCount, // 仕様書準拠 - 'queue_error_count' => $totalQueueErrorCount, // 仕様書準拠 - 'device_count' => count($devices), - 'alert_count' => $alertCount - ]); - - // 仕様書準拠:SHJ-8の戻り値確認(処理結果 = 0以外なら異常) + // SHJ-8 バッチ処理ログ作成 + $shj8Result = $this->createShjBatchLog( + $deviceId, + $printerResult['process_name'] ?? null, // 仕様書準拠:JOB4のプロセス名のみ + 'SHJ-6サーバ死活監視', // job_name 固定 + 'success', // 仕様書:常にsuccess + $statusComment + ); + + // 式様書:result=0 でも result≠0 でも「処理を終了する」 if (!$shj8Result['success']) { - $shj8Error = $shj8Result['error'] ?? 'Unknown error'; - Log::warning('SHJ-8 バッチ処理ログ作成で異常が発生しました', [ - 'error' => $shj8Error + Log::warning('SHJ-8 バッチ処理ログ作成で異常', [ + 'device_id' => $deviceId, + 'error' => $shj8Result['error'] ?? '' ]); - - // 仕様書準拠:異常時はバッチコメントへ反映 - $accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') . - sprintf('SHJ-8異常: %s', $shj8Error); - - // statusCommentも更新 - $statusComment .= sprintf(' | SHJ-8異常: %s', $shj8Error); } Log::info('SHJ-6 サーバ死活監視処理完了', [ @@ -378,16 +355,14 @@ class ShjSixService private function getServerDevices() { try { + // 仕様書準拠SQL-2:デバイス管理マスタを取得 $devices = DB::table('device') ->select([ 'device_id', 'park_id', 'device_type', 'device_subject', - 'device_identifier', - 'device_work', 'device_workstart', - 'device_remarks' ]) ->where('device_type', self::DEVICE_TYPE_SERVER) ->where('device_work', self::DEVICE_WORK_ACTIVE) @@ -433,7 +408,7 @@ class ShjSixService ->orderBy('created_at', 'desc') ->first(); - // ログが取得できない、または異常状態の場合 + // 仕様書準拠:JOB3-STEP1 取得レコードが0件の場合のみアラート if (!$latestLog) { // ログが存在しない = 異常 $alertMessage = sprintf( @@ -463,41 +438,8 @@ class ShjSixService 'updated_batch_comment' => $commonAResult['updated_batch_comment'] ?? '' ]; } - - // status != 1(正常)の場合は異常 - if ($latestLog->status != HardwareCheckLog::STATUS_NORMAL) { - $statusName = HardwareCheckLog::getStatusName($latestLog->status); - $alertMessage = sprintf( - 'ハードウェア監視異常: デバイスID=%d, デバイス名=%s, 状態=%s, コメント=%s', - $device->device_id, - $device->device_subject ?? 'N/A', - $statusName, - $latestLog->status_comment ?? 'N/A' - ); - - $commonAResult = $this->executeCommonProcessA( - $batchLogId, - $alertMessage, - self::QUE_CLASS_SERVER_ERROR, - $device->park_id, - null, - null, - $dbRegisterFlag, - 'ハードウェア監視:異常状態検出' - ); - - return [ - 'has_alert' => true, - 'reason' => '異常状態', - 'mail_success_count' => $commonAResult['mail_success_count'] ?? 0, - 'mail_error_count' => $commonAResult['mail_error_count'] ?? 0, - 'queue_success_count' => $commonAResult['queue_success_count'] ?? 0, // 仕様書準拠 - 'queue_error_count' => $commonAResult['queue_error_count'] ?? 0, // 仕様書準拠 - 'updated_batch_comment' => $commonAResult['updated_batch_comment'] ?? '' - ]; - } - - // 正常 + + // 仕様書準拠:レコードが存在する場合はステータス値に関わらずJOB4へ(アラートなし) return [ 'has_alert' => false, 'reason' => '正常', @@ -562,19 +504,28 @@ class ShjSixService // エラーログごとに共通A処理を実行 foreach ($errorLogs as $log) { // 仕様書準拠:statusの値に応じてキュー種別を判定 - // - status 200番台 → 102(プリンタエラー) - // - status 300番台 → 103(スキャナエラー) - // - status 400番台 → 104(プリンタ用紙残少警報) + // - 200番台 → 102(プリンタエラー) + // - 300番台 → 103(スキャナエラー) + // - 400番台 → 104(プリンタ用紙残少警報) $statusValue = (int)$log->status; if ($statusValue >= 400 && $statusValue < 500) { - $queClass = self::QUE_CLASS_PAPER_WARNING; // 104 正しい定数名 + $queClass = self::QUE_CLASS_PAPER_WARNING; // 104 $errorType = 'プリンタ用紙残少警報'; } elseif ($statusValue >= 300 && $statusValue < 400) { $queClass = self::QUE_CLASS_SCANNER_ERROR; // 103 $errorType = 'スキャナエラー'; - } else { - $queClass = self::QUE_CLASS_PRINTER_ERROR; // 102(デフォルト) + } elseif ($statusValue >= 200 && $statusValue < 300) { + $queClass = self::QUE_CLASS_PRINTER_ERROR; // 102 $errorType = 'プリンタエラー'; + } else { + // 仕様書未定義のステータス範囲 → 102(プリンタエラー)をデフォルト使用 + // 式様書「対象レコード数分」に準拠し、スキップしない + $queClass = self::QUE_CLASS_PRINTER_ERROR; // 102 + $errorType = 'プリンタエラー'; + Log::warning('JOB4: 仕様書未定義のステータス範囲(デフォルト102適用)', [ + 'status' => $statusValue, + 'process_name' => $log->process_name ?? null + ]); } $alertMessage = sprintf( @@ -618,8 +569,7 @@ class ShjSixService $processNames[] = $log->process_name; } } - // 重複を除去して連結 - $processNames = array_unique($processNames); + // 仕様書準拠:複数の場合は後ろ連結(重複除去しない) $concatenatedProcessNames = !empty($processNames) ? implode(',', $processNames) : null; return [ @@ -676,12 +626,10 @@ class ShjSixService // メール送信結果統計 $mailSuccessCount = 0; $mailErrorCount = 0; - $mailErrorDetails = []; - + // キュー登録結果統計(仕様書準拠) $queueSuccessCount = 0; $queueErrorCount = 0; - $registeredQueId = null; // 登録したキューID(後で更新するため) try { Log::info('共通A処理開始', [ @@ -696,12 +644,8 @@ class ShjSixService if ($dbRegisterFlag === 1) { // DB登録可能な場合 - // 仕様書準拠:バッチコメント = 処理1.定期契約ID + 元のバッチコメント - $updatedBatchComment = sprintf( - '定期契約ID: %s / %s', - $contractId ?? 'なし', - $batchComment ?? '' - ); + // 仕様書準拠:キューコメント = 内部変数.バッチコメント(そのまま使用) + $updatedBatchComment = $batchComment ?? ''; // 【共通処理1】オペレータキューを登録する(仕様書順序準拠) // 注:SHJ-7異常情報はメール送信後に追記 @@ -718,13 +662,11 @@ class ShjSixService // 仕様書準拠:キュー登録正常終了件数/異常終了件数を更新 if ($queueResult['success']) { $queueSuccessCount++; - $registeredQueId = $queueResult['que_id']; // キューIDを保存 } else { $queueErrorCount++; - $updatedBatchComment .= sprintf( - ' | キュー登録異常: %s', - $queueResult['error'] ?? 'Unknown error' - ); + // 仕様書準拠(line 209):定期契約ID + 異常情報をバッチコメントに追記 + $errorPrefix = ($contractId !== null) ? $contractId : ''; + $updatedBatchComment .= $errorPrefix . ($queueResult['error'] ?? ''); } // 【共通処理2】メール送信対象オペレータを取得する @@ -734,21 +676,19 @@ class ShjSixService if (!empty($operators)) { foreach ($operators as $operator) { $result = $this->sendAlertMail($operator['email'], $alertMessage, 'オペレータ'); - + // SHJ-7 メール送信結果を集計 if ($result['success']) { $mailSuccessCount++; } else { $mailErrorCount++; - $mailErrorDetails[] = sprintf( - 'オペレータ[%s]へのメール送信失敗: %s', - $operator['email'], - $result['error'] ?? 'Unknown error' - ); + // 仕様書準拠(line 209):定期契約ID + SHJ-7.異常情報をバッチコメントに追記 + $errorPrefix = ($contractId !== null) ? $contractId : ''; + $updatedBatchComment .= $errorPrefix . ($result['error'] ?? ''); } } } - + // 【共通処理3】駐輪場管理者を取得する $parkManagers = $this->getParkManagers($parkId); @@ -756,51 +696,19 @@ class ShjSixService if (!empty($parkManagers)) { foreach ($parkManagers as $manager) { $result = $this->sendAlertMail($manager['email'], $alertMessage, '駐輪場管理者'); - + // SHJ-7 メール送信結果を集計 if ($result['success']) { $mailSuccessCount++; } else { $mailErrorCount++; - $mailErrorDetails[] = sprintf( - '駐輪場管理者[%s]へのメール送信失敗: %s', - $manager['email'], - $result['error'] ?? 'Unknown error' - ); + // 仕様書準拠(line 209):定期契約ID + SHJ-7.異常情報をバッチコメントに追記 + $errorPrefix = ($contractId !== null) ? $contractId : ''; + $updatedBatchComment .= $errorPrefix . ($result['error'] ?? ''); } } } - // 仕様書準拠:メール異常時にSHJ-7異常情報を追記 - if ($mailErrorCount > 0) { - $updatedBatchComment .= sprintf( - ' | SHJ-7メール異常: %d件 (%s)', - $mailErrorCount, - implode('; ', $mailErrorDetails) - ); - } - - // 仕様書準拠:キュー登録後、SHJ-7異常情報を含めてキューコメントを更新 - // キューコメント = 内部変数.バッチコメント(定期契約ID + SHJ-7異常情報) - if ($registeredQueId && $mailErrorCount > 0) { - try { - OperatorQue::where('que_id', $registeredQueId)->update([ // 主キーはque_id - 'que_comment' => $updatedBatchComment, // 仕様書:キューコメントに追記 - 'updated_at' => now() - ]); - - Log::info('オペレータキューコメント更新完了(SHJ-7異常情報追記)', [ - 'que_id' => $registeredQueId, - 'updated_comment' => $updatedBatchComment - ]); - } catch (\Exception $e) { - Log::error('オペレータキューコメント更新エラー', [ - 'que_id' => $registeredQueId, - 'error' => $e->getMessage() - ]); - } - } - } else { // DB反映NGの場合は固定メールアドレスに緊急メール送信 // 仕様書準拠:テンプレート不使用、件名固定、本文なし @@ -809,24 +717,13 @@ class ShjSixService $alertMessage ); + $updatedBatchComment = $batchComment ?? ''; if ($result['success']) { $mailSuccessCount++; } else { $mailErrorCount++; - $mailErrorDetails[] = sprintf( - '固定アドレス[%s]への緊急メール送信失敗: %s', - self::FIXED_EMAIL_ADDRESS, - $result['error'] ?? 'Unknown error' - ); - } - - $updatedBatchComment = $batchComment ?? ''; - if ($mailErrorCount > 0) { - $updatedBatchComment .= sprintf( - ' | 緊急メール異常: %d件 (%s)', - $mailErrorCount, - implode('; ', $mailErrorDetails) - ); + // 仕様書準拠:異常情報をバッチコメントに直接追記 + $updatedBatchComment .= $result['error'] ?? ''; } } @@ -844,7 +741,6 @@ class ShjSixService 'success' => true, 'mail_success_count' => $mailSuccessCount, 'mail_error_count' => $mailErrorCount, - 'mail_error_details' => $mailErrorDetails, 'queue_success_count' => $queueSuccessCount, // 仕様書準拠 'queue_error_count' => $queueErrorCount, // 仕様書準拠 'updated_batch_comment' => $updatedBatchComment ?? $batchComment @@ -863,7 +759,6 @@ class ShjSixService 'success' => false, 'mail_success_count' => $mailSuccessCount, 'mail_error_count' => $mailErrorCount, - 'mail_error_details' => $mailErrorDetails, 'queue_success_count' => $queueSuccessCount, 'queue_error_count' => $queueErrorCount, 'updated_batch_comment' => $updatedBatchComment, @@ -874,18 +769,19 @@ class ShjSixService /** * オペレータキューを登録 - * + * * 仕様書準拠: - * - 定期契約IDをバッチコメントに含める - * - 登録の成否とque_idを返却(後で更新するため) - * + * - キューコメント = 内部変数.バッチコメント + * - キューステータスID = 1(キュー発生) + * - 更新オペレータID = 9999999(機器ID固定値) + * * @param string $alertMessage アラートメッセージ * @param int|null $batchLogId バッチログID * @param int $queClass 対象キュー種別ID(101〜104) * @param int|null $parkId 駐輪場ID * @param int|null $userId 利用者ID * @param int|null $contractId 定期契約ID - * @param string|null $batchComment バッチコメント(SHJ-7異常情報は後で追記) + * @param string|null $batchComment バッチコメント * @return array 登録結果 ['success' => bool, 'error' => string|null, 'que_id' => int|null] */ private function registerOperatorQueue( @@ -959,34 +855,39 @@ class ShjSixService * - 104(プリンタ用紙残少) → ope_sendalart_que13 * * @param int $queClass 対象キュー種別ID - * @param int|null $parkId 駐輪場ID(nullの場合は全オペレータ) + * @param int|null $parkId 駐輪場ID * @return array オペレータ一覧 */ private function getMailTargetOperators(int $queClass, ?int $parkId): array { try { + // 仕様書準拠:駐輪場IDがnullの場合はJOIN条件が成立しないため空返却 + if ($parkId === null) { + Log::info('park_idがnullのため、メール送信対象オペレータなし', [ + 'que_class' => $queClass + ]); + return []; + } + // キュー種別IDに対応する送信フラグカラム名を決定 $alertFlagColumn = $this->getOperatorAlertFlagColumn($queClass); - + if (empty($alertFlagColumn)) { Log::warning('不正なキュー種別ID', ['que_class' => $queClass]); return []; } - - // 管轄駐輪場マスタをJOINしてオペレータを取得 + + // 仕様書準拠SQL-6:管轄駐輪場マスタをJOINしてオペレータを取得 $query = DB::table('ope as T1') - ->select(['T1.ope_id', 'T1.ope_name', 'T1.ope_mail']) + ->join('jurisdiction_parking as T2', 'T1.ope_id', '=', 'T2.ope_id') + // 仕様書SQL-6: ope_login_id と記載があるが、実DBカラム名は login_id + ->select(['T1.ope_id', 'T1.login_id', 'T1.ope_name', 'T1.ope_mail']) + ->where('T2.park_id', $parkId) ->where('T1.' . $alertFlagColumn, 1) // 該当アラート送信フラグが有効 ->where('T1.ope_quit_flag', 0) // 退職していない ->whereNotNull('T1.ope_mail') ->where('T1.ope_mail', '!=', ''); - - // 駐輪場IDが指定されている場合は管轄駐輪場マスタでフィルタ - if ($parkId !== null) { - $query->join('jurisdiction_parking as T2', 'T1.ope_id', '=', 'T2.ope_id') - ->where('T2.park_id', $parkId); - } - + $operators = $query->get() ->map(function ($ope) { return [ @@ -1043,18 +944,20 @@ class ShjSixService * - メールアドレスが設定されている * - 所属駐輪場ID = 指定駐輪場ID * - * @param int|null $parkId 駐輪場ID(nullの場合は全管理者) + * @param int|null $parkId 駐輪場ID * @return array 駐輪場管理者一覧 */ private function getParkManagers(?int $parkId): array { try { - $query = Manager::active()->hasEmail(); - - // 駐輪場IDが指定されている場合はフィルタ - if ($parkId !== null) { - $query->where('manager_parkid', $parkId); + // 仕様書準拠:駐輪場IDがnullの場合は該当なしのため空返却 + if ($parkId === null) { + Log::info('park_idがnullのため、駐輪場管理者なし'); + return []; } + + $query = Manager::active()->hasEmail() + ->where('manager_parkid', $parkId); $managers = $query->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid']) ->get() @@ -1205,32 +1108,26 @@ class ShjSixService * * 仕様書に基づくSHJ-8共通処理呼び出し * - * @param array $statistics 処理統計情報 + * @param int $deviceId デバイスID + * @param string|null $processName プロセス名(JOB4から取得) + * @param string $jobName ジョブ名 + * @param string $status ステータス + * @param string $statusComment ステータスコメント * @return array 処理結果 ['success' => bool, 'error' => string|null] */ - private function createShjBatchLog(array $statistics): array - { + private function createShjBatchLog( + int $deviceId, + ?string $processName, + string $jobName, + string $status, + string $statusComment + ): array { try { - // 仕様書準拠のSHJ-8パラメータ設定 - // device_id: 処理2で取得したデバイスID(複数ある場合は最初のIDを使用) - // process_name: 処理4のプロセス名 - $deviceIdString = $statistics['device_id']; // 連結されたデバイスID文字列 "1,2,3" - - // 複数デバイスIDがある場合は最初のIDを使用(SHJ-8はint型を期待) - $deviceIdArray = !empty($deviceIdString) ? explode(',', $deviceIdString) : []; - $deviceId = !empty($deviceIdArray) ? (int)$deviceIdArray[0] : 1; - - $processName = $statistics['process_name'] ?? 'SHJ-6'; - $jobName = $statistics['job_name']; // "SHJ-6サーバ死活監視" 固定 - $status = $statistics['status']; // 常に "success" - $statusComment = $statistics['status_comment'] ?? ''; - $createdDate = now()->format('Y/m/d'); $updatedDate = now()->format('Y/m/d'); Log::info('SHJ-8 バッチ処理ログ作成', [ 'device_id' => $deviceId, - 'device_id_original' => $deviceIdString, 'process_name' => $processName, 'job_name' => $jobName, 'status' => $status, @@ -1238,7 +1135,7 @@ class ShjSixService ]); // SHJ-8サービスを呼び出し - $this->shjEightService->execute( + $shj8Result = $this->shjEightService->execute( $deviceId, $processName, $jobName, @@ -1248,20 +1145,33 @@ class ShjSixService $updatedDate ); - Log::info('SHJ-8 バッチ処理ログ作成完了', [ - 'device_id' => $deviceId, - 'process_name' => $processName - ]); - - return [ - 'success' => true, - 'error' => null - ]; + // 仕様書準拠:SHJ-8の処理結果を判定(result=0が正常) + if (($shj8Result['result'] ?? 1) === 0) { + Log::info('SHJ-8 バッチ処理ログ作成完了(正常)', [ + 'device_id' => $deviceId, + 'process_name' => $processName + ]); + return [ + 'success' => true, + 'error' => null + ]; + } else { + $errorMessage = $shj8Result['error_message'] ?? 'SHJ-8 処理結果異常'; + Log::warning('SHJ-8 バッチ処理ログ作成異常', [ + 'device_id' => $deviceId, + 'result' => $shj8Result['result'] ?? null, + 'error_message' => $errorMessage + ]); + return [ + 'success' => false, + 'error' => $errorMessage + ]; + } } catch (\Exception $e) { Log::error('SHJ-8 バッチ処理ログ作成エラー', [ 'error' => $e->getMessage(), - 'statistics' => $statistics + 'device_id' => $deviceId ]); // 仕様書準拠:SHJ-8でエラーが発生してもメイン処理は継続 diff --git a/app/Services/ShjTwoService.php b/app/Services/ShjTwoService.php new file mode 100644 index 0000000..382c549 --- /dev/null +++ b/app/Services/ShjTwoService.php @@ -0,0 +1,470 @@ + 現時点の世代シフトを行う + * フルバックアップを行う(ソース + DB) + * バッチ処理ログを作成する(SHJ-8呼び出し) + */ +class ShjTwoService +{ + /** + * ShjEightService インスタンス + * + * @var ShjEightService + */ + protected $shjEightService; + + /** + * コンストラクタ + * + * @param ShjEightService $shjEightService + */ + public function __construct(ShjEightService $shjEightService) + { + $this->shjEightService = $shjEightService; + } + + /** + * SHJ-2 メイン処理実行 + * + * @return array 処理結果 + */ + public function execute(): array + { + $status = 'error'; + $statusComment = ''; + + try { + Log::info('SHJ-2 データバックアップ処理開始'); + + $backupRoot = config('shj2.backup_root'); + $dbName = config('database.connections.mysql.database'); + $generations = config('shj2.generations', 5); + + // 世代シフト + $this->shiftGenerations($backupRoot, $dbName, $generations); + + // フルバックアップ + $backupResult = $this->executeFullBackup($backupRoot, $dbName); + + // [JOB2-STEP1] 結果設定 + $status = $backupResult['status']; + $statusComment = $backupResult['status_comment']; + + Log::info('SHJ-2 データバックアップ処理完了', [ + 'status' => $status, + 'status_comment' => $statusComment + ]); + + } catch (\Exception $e) { + $status = 'error'; + $statusComment = $e->getMessage(); + + Log::error('SHJ-2 データバックアップ処理エラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + } + + // バッチ処理ログ作成(成功・失敗に関わらず実行) + $this->createBatchLog($status, $statusComment); + + return [ + 'success' => $status === 'success', + 'status' => $status, + 'status_comment' => $statusComment + ]; + } + + /** + * 世代シフト処理 + * + * ・5世代前 削除 + * ・4世代前 5世代前へ移動 + * ・3世代前 4世代前へ移動 + * ・2世代前 3世代前へ移動 + * ・1世代前(直下)→ 2世代前へ移動 + * + * @param string $backupRoot バックアップルートディレクトリ + * @param string $dbName データベース名 + * @param int $generations 世代数 + * @return void + */ + private function shiftGenerations(string $backupRoot, string $dbName, int $generations): void + { + Log::info('JOB1 世代シフト開始', [ + 'backup_root' => $backupRoot, + 'db_name' => $dbName, + 'generations' => $generations + ]); + + // 最古世代(gen5)のディレクトリパスを取得して削除 + $oldestDir = $backupRoot . '/' . $dbName . '_' . $generations; + if (is_dir($oldestDir)) { + $this->deleteDirectory($oldestDir); + Log::info("世代シフト: {$generations}世代前を削除", ['path' => $oldestDir]); + } + + // gen4→gen5, gen3→gen4, gen2→gen3 のシフト + for ($i = $generations - 1; $i >= 2; $i--) { + $fromDir = $backupRoot . '/' . $dbName . '_' . $i; + $toDir = $backupRoot . '/' . $dbName . '_' . ($i + 1); + + if (is_dir($fromDir)) { + rename($fromDir, $toDir); + Log::info("世代シフト: {$i}世代前 → " . ($i + 1) . "世代前", [ + 'from' => $fromDir, + 'to' => $toDir + ]); + } + } + + // gen1(直下)→ gen2 へ移動 + $gen1Dir = $backupRoot . '/' . $dbName; + $gen2Dir = $backupRoot . '/' . $dbName . '_2'; + + if (is_dir($gen1Dir)) { + rename($gen1Dir, $gen2Dir); + Log::info('世代シフト: 1世代前 → 2世代前', [ + 'from' => $gen1Dir, + 'to' => $gen2Dir + ]); + } + + Log::info('JOB1 世代シフト完了'); + } + + /** + * フルバックアップ実行 + * + * 保存内容: {vendor以外のソース + DB}_YYYYMMDD.tar.gz + * + * @param string $backupRoot バックアップルートディレクトリ + * @param string $dbName データベース名 + * @return array ['status' => string, 'status_comment' => string] + */ + private function executeFullBackup(string $backupRoot, string $dbName): array + { + Log::info('JOB2 フルバックアップ開始'); + + $today = Carbon::now()->format('Ymd'); + $backupDir = $backupRoot . '/' . $dbName; + $tarFileName = $dbName . '_' . $today . '.tar'; + $gzFileName = $tarFileName . '.gz'; + $tarFilePath = $backupDir . '/' . $tarFileName; + $gzFilePath = $backupDir . '/' . $gzFileName; + + // バックアップディレクトリ作成 + if (!is_dir($backupDir)) { + mkdir($backupDir, 0755, true); + } + + // 一時ディレクトリ(tar作成用のステージング領域) + $tempDir = $backupDir . '/temp_' . $today; + if (!is_dir($tempDir)) { + mkdir($tempDir, 0755, true); + } + + try { + // 1. mysqldump実行 + $sqlFileName = $dbName . '.sql'; + $sqlFilePath = $tempDir . '/' . $sqlFileName; + $this->executeMysqlDump($sqlFilePath); + + // 2. ソースファイルをコピー + $this->copySourceFiles($tempDir); + + // 3. tar.gz作成 + $this->createTarGz($tempDir, $gzFilePath); + + // 4. ファイルサイズ検証 + if (!file_exists($gzFilePath) || filesize($gzFilePath) === 0) { + throw new \RuntimeException('バックアップファイルの作成に失敗しました(ファイルサイズ0)'); + } + + $fileSizeMb = round(filesize($gzFilePath) / 1024 / 1024, 2); + + Log::info('JOB2 フルバックアップ完了', [ + 'file' => $gzFilePath, + 'size_mb' => $fileSizeMb + ]); + + // [JOB2-STEP1] OK + return [ + 'status' => 'success', + 'status_comment' => $backupDir . ' ' . $gzFileName + ]; + + } catch (\Exception $e) { + Log::error('JOB2 フルバックアップエラー', [ + 'error' => $e->getMessage() + ]); + + // [JOB2-STEP1] NG + return [ + 'status' => 'error', + 'status_comment' => $e->getMessage() + ]; + + } finally { + // 一時ディレクトリを削除 + if (is_dir($tempDir)) { + $this->deleteDirectory($tempDir); + } + } + } + + /** + * mysqldump実行 + * + * @param string $outputPath 出力ファイルパス + * @return void + * @throws \RuntimeException dump失敗時 + */ + private function executeMysqlDump(string $outputPath): void + { + $mysqldumpPath = config('shj2.mysqldump_path'); + $dbHost = config('database.connections.mysql.host'); + $dbPort = config('database.connections.mysql.port', '3306'); + $dbName = config('database.connections.mysql.database'); + $dbUser = config('database.connections.mysql.username'); + $dbPass = config('database.connections.mysql.password'); + + // mysqldumpコマンド構築(Windows環境対応) + // stderrは一時ファイルに出力し、エラー時に読み取る + $stderrFile = sys_get_temp_dir() . '/shj2_mysqldump_stderr.txt'; + $command = sprintf( + '"%s" --host="%s" --port="%s" --user="%s" --password="%s" --single-transaction --routines --triggers "%s" > "%s" 2>"%s"', + $mysqldumpPath, + $dbHost, + $dbPort, + $dbUser, + $dbPass, + $dbName, + $outputPath, + $stderrFile + ); + + Log::info('mysqldump実行開始', ['database' => $dbName]); + + exec($command, $output, $returnCode); + + if ($returnCode !== 0) { + $errorOutput = file_exists($stderrFile) ? trim(file_get_contents($stderrFile)) : '不明なエラー'; + @unlink($stderrFile); + throw new \RuntimeException('mysqldumpエラー: ' . $errorOutput); + } + @unlink($stderrFile); + + if (!file_exists($outputPath) || filesize($outputPath) === 0) { + throw new \RuntimeException('mysqldumpの出力ファイルが空です'); + } + + Log::info('mysqldump実行完了', [ + 'output_path' => $outputPath, + 'size_bytes' => filesize($outputPath) + ]); + } + + /** + * ソースファイルを一時ディレクトリにコピー + * + * vendor等の除外ディレクトリを除いてコピーする + * + * @param string $tempDir コピー先一時ディレクトリ + * @return void + */ + private function copySourceFiles(string $tempDir): void + { + $sourcePaths = config('shj2.source_paths', []); + $excludeDirs = config('shj2.exclude_dirs', []); + + foreach ($sourcePaths as $sourcePath) { + // 空パスはスキップ + if (empty($sourcePath)) { + continue; + } + + if (!is_dir($sourcePath)) { + Log::warning('ソースディレクトリが存在しません', ['path' => $sourcePath]); + continue; + } + + // ディレクトリ名を取得してコピー先を決定 + $dirName = basename($sourcePath); + $destDir = $tempDir . '/' . $dirName; + + Log::info('ソースコピー開始', [ + 'source' => $sourcePath, + 'dest' => $destDir + ]); + + $this->copyDirectoryRecursive($sourcePath, $destDir, $excludeDirs); + + Log::info('ソースコピー完了', ['dir_name' => $dirName]); + } + } + + /** + * ディレクトリを再帰的にコピー(除外ディレクトリ対応) + * + * @param string $source コピー元パス + * @param string $dest コピー先パス + * @param array $excludeDirs 除外ディレクトリ名リスト + * @return void + */ + private function copyDirectoryRecursive(string $source, string $dest, array $excludeDirs): void + { + if (!is_dir($dest)) { + mkdir($dest, 0755, true); + } + + $iterator = new \DirectoryIterator($source); + + foreach ($iterator as $item) { + if ($item->isDot()) { + continue; + } + + $itemName = $item->getFilename(); + + // 除外ディレクトリチェック + if ($item->isDir() && in_array($itemName, $excludeDirs)) { + continue; + } + + $sourcePath = $item->getPathname(); + $destPath = $dest . '/' . $itemName; + + if ($item->isDir()) { + $this->copyDirectoryRecursive($sourcePath, $destPath, $excludeDirs); + } else { + copy($sourcePath, $destPath); + } + } + } + + /** + * tar.gzアーカイブを作成 + * + * @param string $sourceDir アーカイブ対象ディレクトリ + * @param string $gzFilePath 出力tar.gzファイルパス + * @return void + * @throws \RuntimeException 作成失敗時 + */ + private function createTarGz(string $sourceDir, string $gzFilePath): void + { + Log::info('tar.gz作成開始', ['source' => $sourceDir, 'output' => $gzFilePath]); + + // tarファイルパス(.gzを除いたパス) + $tarFilePath = preg_replace('/\.gz$/', '', $gzFilePath); + + try { + $phar = new \PharData($tarFilePath); + $phar->buildFromDirectory($sourceDir); + $phar->compress(\Phar::GZ); + + // PharData::compress() は新しい .tar.gz ファイルを作成する + // 元の .tar ファイルを削除 + if (file_exists($tarFilePath)) { + unlink($tarFilePath); + } + + Log::info('tar.gz作成完了', ['output' => $gzFilePath]); + + } catch (\Exception $e) { + // 中間ファイルのクリーンアップ + if (file_exists($tarFilePath)) { + unlink($tarFilePath); + } + if (file_exists($gzFilePath)) { + unlink($gzFilePath); + } + throw new \RuntimeException('tar.gz作成エラー: ' . $e->getMessage()); + } + } + + /** + * バッチ処理ログ作成 + * + * SHJ-8 バッチ処理ログ作成を呼び出す + * + * @param string $status ステータス(success/error) + * @param string $statusComment ステータスコメント + * @return void + */ + private function createBatchLog(string $status, string $statusComment): void + { + try { + $device = Device::orderBy('device_id')->first(); + $deviceId = $device ? $device->device_id : 1; + $today = now()->format('Y/m/d'); + + // ステータスコメントは255文字以内に切り詰め + if (mb_strlen($statusComment) > 255) { + $statusComment = mb_substr($statusComment, 0, 252) . '...'; + } + + $this->shjEightService->execute( + $deviceId, + null, // プロセス名 + 'SHJ-2データバックアップ', // ジョブ名 + $status, // ステータス + $statusComment, // ステータスコメント + $today, // 登録日時 + $today // 更新日時 + ); + + Log::info('JOB3 バッチ処理ログ作成完了', [ + 'device_id' => $deviceId, + 'status' => $status + ]); + + } catch (\Exception $e) { + Log::error('JOB3 バッチ処理ログ作成エラー', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + } + } + + /** + * ディレクトリを再帰的に削除 + * + * @param string $dirPath 削除対象ディレクトリパス + * @return void + */ + private function deleteDirectory(string $dirPath): void + { + if (!is_dir($dirPath)) { + return; + } + + $items = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dirPath, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($items as $item) { + if ($item->isDir()) { + rmdir($item->getPathname()); + } else { + unlink($item->getPathname()); + } + } + + rmdir($dirPath); + } +} diff --git a/config/shj2.php b/config/shj2.php new file mode 100644 index 0000000..cb47eec --- /dev/null +++ b/config/shj2.php @@ -0,0 +1,61 @@ + env('SHJ2_BACKUP_ROOT', 'C:/xampp8/backup/somanager'), + + /* + |-------------------------------------------------------------------------- + | バックアップ対象ソースディレクトリ + |-------------------------------------------------------------------------- + | vendor等を除外してバックアップするプロジェクトパス + | 空文字列のパスは自動的にスキップされる + */ + 'source_paths' => [ + env('SHJ2_SOURCE_1', 'C:/xampp8/htdocs/somanager/api-batch.app'), + env('SHJ2_SOURCE_2', 'C:/xampp8/htdocs/somanager/www.app'), + env('SHJ2_SOURCE_3', ''), // 第3プロジェクト(パス設定後に有効) + ], + + /* + |-------------------------------------------------------------------------- + | 除外ディレクトリ + |-------------------------------------------------------------------------- + | バックアップ対象から除外するディレクトリ名 + */ + 'exclude_dirs' => [ + 'vendor', + 'node_modules', + '.git', + ], + + /* + |-------------------------------------------------------------------------- + | 世代数 + |-------------------------------------------------------------------------- + | フルバックアップを保持する世代数 + */ + 'generations' => 5, + + /* + |-------------------------------------------------------------------------- + | mysqldumpパス + |-------------------------------------------------------------------------- + | mysqldumpコマンドの絶対パス + */ + 'mysqldump_path' => env('SHJ2_MYSQLDUMP_PATH', 'C:/xampp8/mysql/bin/mysqldump'), + +]; diff --git a/routes/console.php b/routes/console.php index bdc5a05..ba43015 100644 --- a/routes/console.php +++ b/routes/console.php @@ -11,5 +11,14 @@ Artisan::command('inspire', function () { // 支払期限切れチェック(15分毎) Schedule::command('payment:expire')->everyFifteenMinutes(); +// SHJ-2 データバックアップ(毎日 02:45) +Schedule::command('shj:2')->dailyAt('02:45'); + // SHJ-5 駐輪場空きチェック(毎月20日 11:00) Schedule::command('shj:5')->monthlyOn(20, '11:00'); + +// SHJ-6 サーバ死活監視(15分毎) +Schedule::command('shj:6')->everyFifteenMinutes(); + +// SHJ-9 売上集計(日次)(毎日 02:00) +Schedule::command('shj:9')->dailyAt('02:00');