From afe82b8f2dd98548b1b27e37fb557a33ff052cee Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 21 Aug 2025 16:11:22 +0900 Subject: [PATCH 01/14] =?UTF-8?q?.env=20=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 5ebf6e4..bd9e970 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ APP_NAME=so-manager APP_ENV=local APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_DEBUG=true -APP_URL=https://krgm.so-manager-dev.com/ +APP_URL=https://krgm.so-manager-dev.com/gitea/main_go APP_LOCALE=ja APP_FALLBACK_LOCALE=ja From cd4bef3633a20922623a7f500db8fd71a60d3491 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 21 Aug 2025 16:32:20 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20test.html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test.html | 1 + 1 file changed, 1 insertion(+) diff --git a/test.html b/test.html index 9daeafb..76afcce 100644 --- a/test.html +++ b/test.html @@ -1 +1,2 @@ test +test \ No newline at end of file From 3edbbff3384f1bce1c60f5ee9d5debd376dfa94a Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 21 Aug 2025 16:33:36 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.gitea/workflows/dep?= =?UTF-8?q?loy-preview.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy-preview.yml | 21 +++++++++++++++++++++ .gitea/workflows/deploy.yml | 14 -------------- 2 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 .gitea/workflows/deploy-preview.yml delete mode 100644 .gitea/workflows/deploy.yml diff --git a/.gitea/workflows/deploy-preview.yml b/.gitea/workflows/deploy-preview.yml new file mode 100644 index 0000000..e8893a0 --- /dev/null +++ b/.gitea/workflows/deploy-preview.yml @@ -0,0 +1,21 @@ +name: Deploy previews (main_*) + +on: + push: + branches: + - 'main_*' + workflow_dispatch: + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + preview: + runs-on: ["native"] + steps: + - uses: actions/checkout@v4 + - name: Deploy preview for this branch + env: + BRANCH: ${{ github.ref_name }} + run: /usr/local/bin/deploy_krgm_branch.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml deleted file mode 100644 index 976e13c..0000000 --- a/.gitea/workflows/deploy.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Deploy krgm (auto) - -on: - push: - branches: [ "main" ] - workflow_dispatch: - -jobs: - deploy: - runs-on: [ "native" ] - steps: - - uses: actions/checkout@v4 - - name: Deploy to server - run: /usr/local/bin/deploy_krgm.sh \ No newline at end of file From 3287f7c01f403d5a51e837e9ea7c7429af900a55 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 21 Aug 2025 22:41:37 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.gitea/workflows/dep?= =?UTF-8?q?loy-preview.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy-preview.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/deploy-preview.yml b/.gitea/workflows/deploy-preview.yml index e8893a0..96f8524 100644 --- a/.gitea/workflows/deploy-preview.yml +++ b/.gitea/workflows/deploy-preview.yml @@ -1,21 +1,20 @@ -name: Deploy previews (main_*) +name: Deploy preview (main_go) on: push: - branches: - - 'main_*' + branches: ["main_go"] workflow_dispatch: concurrency: - group: deploy-${{ github.ref }} + group: deploy-main_go cancel-in-progress: true jobs: - preview: - runs-on: ["native"] + deploy: + runs-on: ["native"] steps: - uses: actions/checkout@v4 - - name: Deploy preview for this branch + - name: Deploy to preview (main_go) env: - BRANCH: ${{ github.ref_name }} + BRANCH: main_go run: /usr/local/bin/deploy_krgm_branch.sh From 7117ca0d67cec624289cc61ceeefd1bc1f426245 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 21 Aug 2025 22:42:33 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index bd9e970..cf0212e 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ APP_NAME=so-manager APP_ENV=local APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_DEBUG=true -APP_URL=https://krgm.so-manager-dev.com/gitea/main_go +APP_URL=https://branch.so-manager-dev.com/main_go APP_LOCALE=ja APP_FALLBACK_LOCALE=ja From 17aa80c8ebfbdd3eb5772e110cef47f953008012 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 21 Aug 2025 23:23:48 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.gitea/workflows/dep?= =?UTF-8?q?loy-preview.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy-preview.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/deploy-preview.yml b/.gitea/workflows/deploy-preview.yml index 96f8524..76e6bc5 100644 --- a/.gitea/workflows/deploy-preview.yml +++ b/.gitea/workflows/deploy-preview.yml @@ -11,10 +11,10 @@ concurrency: jobs: deploy: - runs-on: ["native"] + runs-on: ["native"] steps: - uses: actions/checkout@v4 - name: Deploy to preview (main_go) env: BRANCH: main_go - run: /usr/local/bin/deploy_krgm_branch.sh + run: /usr/local/bin/deploy_branch_simple.sh From dc2e0656aeacb95a9db449d4e24d6bf860cc509d Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 21 Aug 2025 23:45:57 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index cf0212e..2a43367 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ APP_NAME=so-manager APP_ENV=local APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_DEBUG=true -APP_URL=https://branch.so-manager-dev.com/main_go +APP_URL=https://branch.so-manager-dev.com/main_go/public/ APP_LOCALE=ja APP_FALLBACK_LOCALE=ja From bdf98d49165282b06861ab2496ee52eadf084f0d Mon Sep 17 00:00:00 2001 From: gitadmin Date: Thu, 21 Aug 2025 23:59:03 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 2a43367..8c66bad 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ APP_NAME=so-manager APP_ENV=local APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_DEBUG=true -APP_URL=https://branch.so-manager-dev.com/main_go/public/ +APP_URL=https://branch.so-manager-dev.com/main_go/ APP_LOCALE=ja APP_FALLBACK_LOCALE=ja From 73804609384c88ba83269003c6931043adeae0b1 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Fri, 22 Aug 2025 02:04:39 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 8c66bad..164d1f6 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ APP_NAME=so-manager APP_ENV=local APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_DEBUG=true -APP_URL=https://branch.so-manager-dev.com/main_go/ +APP_URL=https://main-go.so-manager-dev.com/ APP_LOCALE=ja APP_FALLBACK_LOCALE=ja From 89fa4e999a4716d454b6d29d5dad5b98d2bf2ded Mon Sep 17 00:00:00 2001 From: gitadmin Date: Fri, 22 Aug 2025 02:04:53 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 164d1f6..b01098b 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ APP_NAME=so-manager APP_ENV=local APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_DEBUG=true -APP_URL=https://main-go.so-manager-dev.com/ +APP_URL=https://main-kin.so-manager-dev.com/ APP_LOCALE=ja APP_FALLBACK_LOCALE=ja From 91877e332b24ccfd986b71103ede5e664d868e2f Mon Sep 17 00:00:00 2001 From: gitadmin Date: Fri, 22 Aug 2025 02:05:02 +0900 Subject: [PATCH 11/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index b01098b..164d1f6 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ APP_NAME=so-manager APP_ENV=local APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_DEBUG=true -APP_URL=https://main-kin.so-manager-dev.com/ +APP_URL=https://main-go.so-manager-dev.com/ APP_LOCALE=ja APP_FALLBACK_LOCALE=ja From 6711341c4ef0f0694ac4f38c3844064137bec150 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Fri, 22 Aug 2025 02:36:33 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20public/index.php?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.php b/public/index.php index 4e38bc6..9ef97fa 100644 --- a/public/index.php +++ b/public/index.php @@ -1,6 +1,6 @@ Date: Fri, 22 Aug 2025 19:44:06 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=20=E5=AE=9E=E8=A3=85SHJ-6/9/10?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=83=81=E5=87=A6=E7=90=86=E3=82=B7=E3=82=B9?= =?UTF-8?q?=E3=83=86=E3=83=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SHJ-9: 日次売上集計処理 - SHJ-10: 年次月次売上集計処理 - SHJ-6: サーバ死活監視処理 - 各種モデルサービスコマンド追加 - earnings_summary, device, hardware_check_log, print_job_log テーブル用SQL追加 --- .env | 2 +- .gitignore | 1 + app/Console/Commands/ShjNineCommand.php | 206 +++++ app/Console/Commands/ShjSixCommand.php | 129 ++++ app/Console/Commands/ShjTenCommand.php | 236 ++++++ app/Enums/QueueClass.php | 2 + app/Enums/QueueStatus.php | 2 + app/Legacy/OperatorQue.php | 2 + app/Legacy/Park.php | 2 + app/Legacy/User.php | 2 + app/Models/BaseModel.php | 1 + app/Models/Concerns/HasSortable.php | 2 + app/Models/EarningsSummary.php | 298 ++++++++ app/Models/HardwareCheckLog.php | 229 ++++++ app/Models/OperatorQue.php | 293 ++++++++ app/Models/Park.php | 2 + app/Models/PriceA.php | 2 + app/Models/PrintJobLog.php | 251 +++++++ app/Models/Psection.php | 106 +++ app/Models/Ptype.php | 2 + app/Models/RegularContract.php | 2 + app/Providers/AppServiceProvider.php | 37 + app/Providers/LegacyServiceProvider.php | 1 + app/Services/FileService.php | 2 + app/Services/OperatorQueService.php | 2 + app/Services/ShjNineService.php | 595 +++++++++++++++ app/Services/ShjSixService.php | 710 ++++++++++++++++++ app/Services/ShjTenService.php | 583 ++++++++++++++ app/Services/UserService.php | 2 + app/Support/Csv.php | 2 + app/Support/Files.php | 2 + create_device_table.sql | 24 + create_earnings_summary_table.sql | 39 + create_hardware_check_log_table.sql | 19 + create_print_job_log_table.sql | 24 + ...7_000000_create_earnings_summary_table.php | 62 ++ public/index.php | 2 +- test.html | 1 - 38 files changed, 3876 insertions(+), 3 deletions(-) create mode 100644 app/Console/Commands/ShjNineCommand.php create mode 100644 app/Console/Commands/ShjSixCommand.php create mode 100644 app/Console/Commands/ShjTenCommand.php create mode 100644 app/Models/EarningsSummary.php create mode 100644 app/Models/HardwareCheckLog.php create mode 100644 app/Models/OperatorQue.php create mode 100644 app/Models/PrintJobLog.php create mode 100644 app/Models/Psection.php create mode 100644 app/Services/ShjNineService.php create mode 100644 app/Services/ShjSixService.php create mode 100644 app/Services/ShjTenService.php create mode 100644 create_device_table.sql create mode 100644 create_earnings_summary_table.sql create mode 100644 create_hardware_check_log_table.sql create mode 100644 create_print_job_log_table.sql create mode 100644 database/migrations/2025_01_17_000000_create_earnings_summary_table.php diff --git a/.env b/.env index 164d1f6..5ebf6e4 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ APP_NAME=so-manager APP_ENV=local APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_DEBUG=true -APP_URL=https://main-go.so-manager-dev.com/ +APP_URL=https://krgm.so-manager-dev.com/ APP_LOCALE=ja APP_FALLBACK_LOCALE=ja diff --git a/.gitignore b/.gitignore index c7cf1fa..caa2223 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ yarn-error.log /.nova /.vscode /.zed +/docs diff --git a/app/Console/Commands/ShjNineCommand.php b/app/Console/Commands/ShjNineCommand.php new file mode 100644 index 0000000..11222e9 --- /dev/null +++ b/app/Console/Commands/ShjNineCommand.php @@ -0,0 +1,206 @@ +shjNineService = $shjNineService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. パラメータ取得と検証 + * 2. 集計対象日設定 + * 3. 売上集計処理実行 + * 4. バッチログ作成 + * 5. 処理結果返却 + * + * @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 + ]); + + // パラメータ検証 + 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 + { + // 集計種別チェック + $allowedTypes = ['daily', 'monthly', 'yearly']; + if (!in_array($type, $allowedTypes)) { + $this->error('集計種別は daily, monthly, yearly のいずれかを指定してください。'); + return false; + } + + // 対象日形式チェック(指定されている場合) + if ($targetDate && !$this->isValidDateFormat($targetDate)) { + $this->error('対象日の形式が正しくありません(YYYY-MM-DD形式で指定してください)。'); + return false; + } + + return true; + } + + /** + * 集計対象日を決定 + * + * @param string $type 集計種別 + * @param string|null $targetDate 指定日 + * @return string 集計対象日 + */ + private function determineAggregationDate(string $type, ?string $targetDate): string + { + if ($targetDate) { + return $targetDate; + } + + // パラメータ指定がない場合のデフォルト設定 + switch ($type) { + case 'daily': + // 日次:昨日(本日の1日前) + return now()->subDay()->format('Y-m-d'); + + case 'monthly': + // 月次:前月の最終日 + return now()->subMonth()->endOfMonth()->format('Y-m-d'); + + case 'yearly': + // 年次:前年の最終日 + return now()->subYear()->endOfYear()->format('Y-m-d'); + + default: + return 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; + } + + // 実際の日付として有効かチェック + $dateParts = explode('-', $date); + return checkdate((int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]); + } +} diff --git a/app/Console/Commands/ShjSixCommand.php b/app/Console/Commands/ShjSixCommand.php new file mode 100644 index 0000000..fc89db4 --- /dev/null +++ b/app/Console/Commands/ShjSixCommand.php @@ -0,0 +1,129 @@ +shjSixService = $shjSixService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. サーバ死活監視(DBアクセス) + * 2. デバイス管理マスタを取得する + * 3. デバイス毎のハードウェア状態を取得する + * 4. プリンタ制御プログラムログを取得する + * 5. バッチ処理ログを作成する + * ※ 異常検出時は共通A処理(メール通知)を実行 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-6 サーバ死活監視処理を開始します。'); + + Log::info('SHJ-6 サーバ死活監視処理開始', [ + 'start_time' => $startTime + ]); + + // SHJ-6監視処理実行 + $result = $this->shjSixService->executeServerMonitoring(); + + // 処理結果確認 + if ($result['success']) { + $endTime = now(); + $this->info('SHJ-6 サーバ死活監視処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + $this->info("監視結果: {$result['monitoring_summary']}"); + + // 警告がある場合は表示 + if (!empty($result['warnings'])) { + $this->warn('警告が検出されました:'); + foreach ($result['warnings'] as $warning) { + $this->warn("- {$warning}"); + } + } + + Log::info('SHJ-6 サーバ死活監視処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-6 サーバ死活監視処理でエラーが発生しました: ' . $result['message']); + + // エラー詳細があれば表示 + if (!empty($result['error_details'])) { + $this->error('エラー詳細:'); + foreach ($result['error_details'] as $detail) { + $this->error("- {$detail}"); + } + } + + Log::error('SHJ-6 サーバ死活監視処理エラー', [ + 'error' => $result['message'], + 'details' => $result['error_details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-6 サーバ死活監視処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-6 サーバ死活監視処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/ShjTenCommand.php b/app/Console/Commands/ShjTenCommand.php new file mode 100644 index 0000000..1a81125 --- /dev/null +++ b/app/Console/Commands/ShjTenCommand.php @@ -0,0 +1,236 @@ +shjTenService = $shjTenService; + } + + /** + * コンソールコマンドを実行 + * + * 処理フロー: + * 1. パラメータ取得と検証 + * 2. 財政年度期間設定 + * 3. 売上集計処理実行 + * 4. バッチログ作成 + * 5. 処理結果返却 + * + * @return int + */ + public function handle() + { + try { + // 開始ログ出力 + $startTime = now(); + $this->info('SHJ-10 売上集計処理を開始します。'); + + // 引数取得 + $type = $this->argument('type'); + $target = $this->argument('target'); + + Log::info('SHJ-10 売上集計処理開始', [ + 'start_time' => $startTime, + 'type' => $type, + 'target' => $target + ]); + + // パラメータ検証 + if (!$this->validateParameters($type, $target)) { + $this->error('パラメータが不正です。'); + return self::FAILURE; + } + + // 財政年度期間設定 + $fiscalPeriod = $this->determineFiscalPeriod($type, $target); + + $this->info("集計種別: {$type}"); + $this->info("集計対象: {$target}"); + $this->info("財政期間: {$fiscalPeriod['start_date']} ~ {$fiscalPeriod['end_date']}"); + + // SHJ-10処理実行 + $result = $this->shjTenService->executeFiscalEarningsAggregation($type, $target, $fiscalPeriod); + + // 処理結果確認 + if ($result['success']) { + $endTime = now(); + $this->info('SHJ-10 売上集計処理が正常に完了しました。'); + $this->info("処理時間: {$startTime->diffInSeconds($endTime)}秒"); + $this->info("処理結果: 駐輪場数 {$result['processed_parks']}, 集計レコード数 {$result['summary_records']}"); + + Log::info('SHJ-10 売上集計処理完了', [ + 'end_time' => $endTime, + 'duration_seconds' => $startTime->diffInSeconds($endTime), + 'result' => $result + ]); + + return self::SUCCESS; + } else { + $this->error('SHJ-10 売上集計処理でエラーが発生しました: ' . $result['message']); + Log::error('SHJ-10 売上集計処理エラー', [ + 'error' => $result['message'], + 'details' => $result['details'] ?? null + ]); + + return self::FAILURE; + } + + } catch (\Exception $e) { + $this->error('SHJ-10 売上集計処理で予期しないエラーが発生しました: ' . $e->getMessage()); + Log::error('SHJ-10 売上集計処理例外エラー', [ + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return self::FAILURE; + } + } + + /** + * パラメータの妥当性を検証 + * + * @param string $type 集計種別 + * @param string $target 集計対象 + * @return bool 検証結果 + */ + private function validateParameters(string $type, string $target): bool + { + // 集計種別チェック + $allowedTypes = ['yearly', 'monthly']; + if (!in_array($type, $allowedTypes)) { + $this->error('集計種別は yearly, monthly のいずれかを指定してください。'); + return false; + } + + // 集計対象形式チェック + if ($type === 'yearly') { + // 年度形式チェック (例: 2019) + if (!preg_match('/^\d{4}$/', $target)) { + $this->error('年次集計の場合、年度を4桁の数字で指定してください。(例: 2019)'); + return false; + } + } elseif ($type === 'monthly') { + // 年月形式チェック (例: 2019/01) + if (!preg_match('/^\d{4}\/\d{2}$/', $target)) { + $this->error('月次集計の場合、年月をYYYY/MM形式で指定してください。(例: 2019/01)'); + return false; + } + + // 月の範囲チェック (01-12) + $parts = explode('/', $target); + $month = (int)$parts[1]; + if ($month < 1 || $month > 12) { + $this->error('月は01から12までの範囲で指定してください。'); + return false; + } + } + + return true; + } + + /** + * 財政年度期間を決定 + * + * 財政年度は4月開始(Config設定可能) + * - yearly 2019: 2019年4月1日 ~ 2020年3月31日 + * - monthly 2019/01: 2019年1月1日 ~ 2019年1月31日 + * + * @param string $type 集計種別 + * @param string $target 集計対象 + * @return array 財政期間情報 + */ + private function determineFiscalPeriod(string $type, string $target): array + { + $fiscalStartMonth = 4; // 財政年度開始月(4月) + + if ($type === 'yearly') { + $year = (int)$target; + + // 財政年度期間計算 + $startDate = sprintf('%04d-%02d-01', $year, $fiscalStartMonth); + $endDate = sprintf('%04d-%02d-%02d', $year + 1, $fiscalStartMonth - 1, + date('t', strtotime(sprintf('%04d-%02d-01', $year + 1, $fiscalStartMonth - 1)))); + + return [ + 'type' => 'yearly', + 'fiscal_year' => $year, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'summary_type' => 1, // 年次 + 'target_label' => "{$year}年度" + ]; + + } elseif ($type === 'monthly') { + $parts = explode('/', $target); + $year = (int)$parts[0]; + $month = (int)$parts[1]; + + // 指定月の期間計算 + $startDate = sprintf('%04d-%02d-01', $year, $month); + $endDate = sprintf('%04d-%02d-%02d', $year, $month, + date('t', strtotime($startDate))); + + // 該当する財政年度を計算 + $fiscalYear = $month >= $fiscalStartMonth ? $year : $year - 1; + + return [ + 'type' => 'monthly', + 'fiscal_year' => $fiscalYear, + 'target_year' => $year, + 'target_month' => $month, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'summary_type' => 2, // 月次 + 'target_label' => "{$year}年{$month}月" + ]; + } + + throw new \InvalidArgumentException("不正な集計種別: {$type}"); + } +} diff --git a/app/Enums/QueueClass.php b/app/Enums/QueueClass.php index 40879ec..716444d 100644 --- a/app/Enums/QueueClass.php +++ b/app/Enums/QueueClass.php @@ -24,3 +24,5 @@ enum QueueClass: string } + + diff --git a/app/Enums/QueueStatus.php b/app/Enums/QueueStatus.php index ec8ff43..fc94164 100644 --- a/app/Enums/QueueStatus.php +++ b/app/Enums/QueueStatus.php @@ -15,3 +15,5 @@ enum QueueStatus: string } + + diff --git a/app/Legacy/OperatorQue.php b/app/Legacy/OperatorQue.php index 5c4d496..a9d4376 100644 --- a/app/Legacy/OperatorQue.php +++ b/app/Legacy/OperatorQue.php @@ -85,3 +85,5 @@ class OperatorQue extends Model } + + diff --git a/app/Legacy/Park.php b/app/Legacy/Park.php index 9cfe508..b5b5910 100644 --- a/app/Legacy/Park.php +++ b/app/Legacy/Park.php @@ -56,3 +56,5 @@ class Park extends Model } + + diff --git a/app/Legacy/User.php b/app/Legacy/User.php index 4db6eee..eb632e9 100644 --- a/app/Legacy/User.php +++ b/app/Legacy/User.php @@ -112,3 +112,5 @@ class User extends Model } + + diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index eb259c3..5f7bb56 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -19,3 +19,4 @@ abstract class BaseModel extends Model } + diff --git a/app/Models/Concerns/HasSortable.php b/app/Models/Concerns/HasSortable.php index e5e0b98..b23821b 100644 --- a/app/Models/Concerns/HasSortable.php +++ b/app/Models/Concerns/HasSortable.php @@ -38,3 +38,5 @@ trait HasSortable } + + diff --git a/app/Models/EarningsSummary.php b/app/Models/EarningsSummary.php new file mode 100644 index 0000000..c23cda5 --- /dev/null +++ b/app/Models/EarningsSummary.php @@ -0,0 +1,298 @@ + 'integer', + 'park_id' => 'integer', + 'psection_id' => 'integer', + 'enable_months' => 'integer', + 'regular_new_count' => 'integer', + 'regular_new_amount' => 'decimal:2', + 'regular_new_reduction_count' => 'integer', + 'regular_new_reduction_amount' => 'decimal:2', + 'regular_update_count' => 'integer', + 'regular_update_amount' => 'decimal:2', + 'regular_update_reduction_count' => 'integer', + 'regular_update_reduction_amount' => 'decimal:2', + 'turnsum_count' => 'integer', + 'turnsum' => 'decimal:2', + 'refunds' => 'decimal:2', + 'other_income' => 'decimal:2', + 'other_spending' => 'decimal:2', + 'reissue_count' => 'integer', + 'reissue_amount' => 'decimal:2', + 'operator_id' => 'integer', + 'summary_start_date' => 'date', + 'summary_end_date' => 'date', + 'earnings_date' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * 日付属性 + * + * @var array + */ + protected $dates = [ + 'summary_start_date', + 'summary_end_date', + 'earnings_date', + 'created_at', + 'updated_at' + ]; + + /** + * 集計タイプの定数 + */ + const TYPE_DAILY = '日次'; + const TYPE_MONTHLY = '月次'; + const TYPE_YEARLY = '年次'; + + /** + * 駐輪場との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function park() + { + return $this->belongsTo(Park::class, 'park_id', 'park_id'); + } + + /** + * 車種区分との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function psection() + { + return $this->belongsTo(Psection::class, 'psection_id', 'psection_id'); + } + + /** + * 指定期間の売上集計データを取得 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 開始日 + * @param string $endDate 終了日 + * @param string|null $summaryType 集計タイプ + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getEarningsByPeriod(int $parkId, string $startDate, string $endDate, ?string $summaryType = null) + { + $query = self::where('park_id', $parkId) + ->whereBetween('earnings_date', [$startDate, $endDate]); + + if ($summaryType) { + $query->where('summary_type', $summaryType); + } + + return $query->with(['park', 'psection']) + ->orderBy('earnings_date') + ->orderBy('psection_id') + ->get(); + } + + /** + * 駐輪場別の売上合計を取得 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 開始日 + * @param string $endDate 終了日 + * @return array 売上合計データ + */ + public static function getEarningsTotalByPark(int $parkId, string $startDate, string $endDate): array + { + $result = self::where('park_id', $parkId) + ->whereBetween('earnings_date', [$startDate, $endDate]) + ->selectRaw(' + SUM(regular_new_count) as total_new_count, + SUM(regular_new_amount) as total_new_amount, + SUM(regular_update_count) as total_update_count, + SUM(regular_update_amount) as total_update_amount, + SUM(turnsum_count) as total_turnsum_count, + SUM(turnsum) as total_turnsum, + SUM(refunds) as total_refunds, + SUM(reissue_count) as total_reissue_count, + SUM(reissue_amount) as total_reissue_amount + ') + ->first(); + + return $result ? $result->toArray() : []; + } + + /** + * 最新の集計日を取得 + * + * @param int|null $parkId 駐輪場ID(省略時は全体) + * @param string|null $summaryType 集計タイプ + * @return string|null 最新集計日 + */ + public static function getLatestEarningsDate(?int $parkId = null, ?string $summaryType = null): ?string + { + $query = self::query(); + + if ($parkId) { + $query->where('park_id', $parkId); + } + + if ($summaryType) { + $query->where('summary_type', $summaryType); + } + + $latest = $query->max('earnings_date'); + + return $latest ? Carbon::parse($latest)->format('Y-m-d') : null; + } + + /** + * 期間の売上データを削除 + * + * @param int $parkId 駐輪場ID + * @param string $startDate 開始日 + * @param string $endDate 終了日 + * @param string|null $summaryType 集計タイプ + * @return int 削除件数 + */ + public static function deleteEarningsByPeriod(int $parkId, string $startDate, string $endDate, ?string $summaryType = null): int + { + $query = self::where('park_id', $parkId) + ->where('summary_start_date', $startDate) + ->where('summary_end_date', $endDate); + + if ($summaryType) { + $query->where('summary_type', $summaryType); + } + + return $query->delete(); + } + + /** + * 売上集計データの作成 + * + * @param array $data 売上データ + * @return EarningsSummary 作成されたモデル + */ + public static function createEarningsSummary(array $data): EarningsSummary + { + $defaultData = [ + 'regular_new_count' => 0, + 'regular_new_amount' => 0.00, + 'regular_new_reduction_count' => 0, + 'regular_new_reduction_amount' => 0.00, + 'regular_update_count' => 0, + 'regular_update_amount' => 0.00, + 'regular_update_reduction_count' => 0, + 'regular_update_reduction_amount' => 0.00, + 'turnsum_count' => 0, + 'turnsum' => 0.00, + 'refunds' => 0.00, + 'other_income' => 0.00, + 'other_spending' => 0.00, + 'reissue_count' => 0, + 'reissue_amount' => 0.00, + 'operator_id' => 0 + ]; + + $mergedData = array_merge($defaultData, $data); + + return self::create($mergedData); + } + + /** + * 売上合計の計算 + * + * @return float 売上合計 + */ + public function getTotalEarningsAttribute(): float + { + return $this->regular_new_amount + + $this->regular_update_amount + + $this->turnsum + + $this->reissue_amount + + $this->other_income - + $this->other_spending - + $this->refunds; + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'EarningsSummary[ID:%d, Park:%d, Type:%s, Date:%s]', + $this->earnings_summary_id, + $this->park_id, + $this->summary_type, + $this->earnings_date ? $this->earnings_date->format('Y-m-d') : 'N/A' + ); + } +} diff --git a/app/Models/HardwareCheckLog.php b/app/Models/HardwareCheckLog.php new file mode 100644 index 0000000..f9cff6f --- /dev/null +++ b/app/Models/HardwareCheckLog.php @@ -0,0 +1,229 @@ + 'integer', + 'device_id' => 'integer', + 'status' => 'integer', + 'operator_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * ステータスの定数 + */ + const STATUS_NORMAL = 1; // 正常 + const STATUS_WARNING = 2; // 警告 + const STATUS_ERROR = 3; // エラー + const STATUS_UNKNOWN = 0; // 不明 + + /** + * デバイスとの関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function device() + { + return $this->belongsTo(Device::class, 'device_id', 'device_id'); + } + + /** + * 指定デバイスの最新ハードウェア状態を取得 + * + * @param int $deviceId デバイスID + * @return HardwareCheckLog|null 最新ログ + */ + public static function getLatestStatusByDevice(int $deviceId): ?HardwareCheckLog + { + return self::where('device_id', $deviceId) + ->orderBy('created_at', 'desc') + ->first(); + } + + /** + * 全デバイスの最新ハードウェア状態を取得 + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getLatestStatusForAllDevices() + { + return self::select('device_id') + ->selectRaw('MAX(created_at) as latest_created_at') + ->groupBy('device_id') + ->with(['device']) + ->get() + ->map(function ($log) { + return self::where('device_id', $log->device_id) + ->where('created_at', $log->latest_created_at) + ->with(['device']) + ->first(); + }) + ->filter(); + } + + /** + * 異常状態のデバイスを取得 + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getAbnormalDevices() + { + $latestLogs = self::getLatestStatusForAllDevices(); + + return $latestLogs->filter(function ($log) { + return $log->status !== self::STATUS_NORMAL; + }); + } + + /** + * 指定期間内のログを取得 + * + * @param int $deviceId デバイスID + * @param string $startTime 開始時刻 + * @param string $endTime 終了時刻 + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getLogsByPeriod(int $deviceId, string $startTime, string $endTime) + { + return self::where('device_id', $deviceId) + ->whereBetween('created_at', [$startTime, $endTime]) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * ハードウェア状態ログを作成 + * + * @param int $deviceId デバイスID + * @param int $status ステータス + * @param string $statusComment ステータスコメント + * @param int|null $operatorId オペレータID + * @return HardwareCheckLog 作成されたログ + */ + public static function createLog( + int $deviceId, + int $status, + string $statusComment = '', + ?int $operatorId = null + ): HardwareCheckLog { + return self::create([ + 'device_id' => $deviceId, + 'status' => $status, + 'status_comment' => $statusComment, + 'operator_id' => $operatorId ?? 0 + ]); + } + + /** + * ステータス名を取得 + * + * @param int $status ステータス + * @return string ステータス名 + */ + public static function getStatusName(int $status): string + { + switch ($status) { + case self::STATUS_NORMAL: + return '正常'; + case self::STATUS_WARNING: + return '警告'; + case self::STATUS_ERROR: + return 'エラー'; + case self::STATUS_UNKNOWN: + return '不明'; + default: + return "ステータス{$status}"; + } + } + + /** + * 現在のステータス名を取得 + * + * @return string ステータス名 + */ + public function getStatusNameAttribute(): string + { + return self::getStatusName($this->status); + } + + /** + * 正常状態かどうかを判定 + * + * @return bool 正常状態かどうか + */ + public function isNormal(): bool + { + return $this->status === self::STATUS_NORMAL; + } + + /** + * 異常状態かどうかを判定 + * + * @return bool 異常状態かどうか + */ + public function isAbnormal(): bool + { + return $this->status !== self::STATUS_NORMAL; + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'HardwareCheckLog[ID:%d, Device:%d, Status:%s, Time:%s]', + $this->log_id, + $this->device_id, + $this->getStatusNameAttribute(), + $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : 'N/A' + ); + } +} diff --git a/app/Models/OperatorQue.php b/app/Models/OperatorQue.php new file mode 100644 index 0000000..a01fc88 --- /dev/null +++ b/app/Models/OperatorQue.php @@ -0,0 +1,293 @@ + 'integer', + 'que_class' => 'integer', + 'user_id' => 'integer', + 'contract_id' => 'integer', + 'park_id' => 'integer', + 'que_status' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * キュークラスの定数 + */ + const CLASS_SHJ4C = 4; // SHJ-4C室割当処理 + const CLASS_SHJ6 = 6; // SHJ-6サーバ死活監視処理 + const CLASS_SHJ8 = 8; // SHJ-8バッチログ処理 + const CLASS_SHJ9 = 9; // SHJ-9売上集計処理 + const CLASS_SHJ10 = 10; // SHJ-10財政年度売上集計処理 + const CLASS_MAIL_SEND = 11; // メール送信処理 + + /** + * キューステータスの定数 + */ + const STATUS_PENDING = 0; // 待機中 + const STATUS_COMPLETED = 1; // 完了 + const STATUS_ERROR = 2; // エラー + const STATUS_CANCELLED = 3; // キャンセル + + /** + * 駐輪場との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function park() + { + return $this->belongsTo(Park::class, 'park_id', 'park_id'); + } + + /** + * ユーザーとの関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id', 'user_id'); + } + + /** + * 契約との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function contract() + { + return $this->belongsTo(RegularContract::class, 'contract_id', 'contract_id'); + } + + /** + * バッチ処理用キューを作成 + * + * @param int $queClass キュークラス + * @param string $comment コメント + * @param int $status ステータス + * @param string|null $workInstructions 作業指示 + * @param int|null $parkId 駐輪場ID + * @return OperatorQue 作成されたキュー + */ + public static function createBatchQueue( + int $queClass, + string $comment, + int $status = self::STATUS_COMPLETED, + ?string $workInstructions = null, + ?int $parkId = null + ): OperatorQue { + return self::create([ + 'que_class' => $queClass, + 'user_id' => null, + 'contract_id' => null, + 'park_id' => $parkId, + 'que_comment' => $comment, + 'que_status' => $status, + 'que_status_comment' => self::getStatusComment($status), + 'work_instructions' => $workInstructions + ]); + } + + /** + * SHJ-9用キューを作成 + * + * @param string $message メッセージ + * @param int $batchLogId バッチログID + * @param int $status ステータス + * @return OperatorQue 作成されたキュー + */ + public static function createShjNineQueue( + string $message, + int $batchLogId, + int $status = self::STATUS_COMPLETED + ): OperatorQue { + return self::createBatchQueue( + self::CLASS_SHJ9, + $message, + $status, + "SHJ-9売上集計処理 BatchLogID: {$batchLogId}" + ); + } + + /** + * 指定期間のキューを取得 + * + * @param string $startDate 開始日 + * @param string $endDate 終了日 + * @param int|null $queClass キュークラス + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getQueuesByPeriod(string $startDate, string $endDate, ?int $queClass = null) + { + $query = self::whereBetween('created_at', [$startDate, $endDate]); + + if ($queClass) { + $query->where('que_class', $queClass); + } + + return $query->with(['park', 'user', 'contract']) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * 未完了のキューを取得 + * + * @param int|null $queClass キュークラス + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getPendingQueues(?int $queClass = null) + { + $query = self::where('que_status', self::STATUS_PENDING); + + if ($queClass) { + $query->where('que_class', $queClass); + } + + return $query->with(['park', 'user', 'contract']) + ->orderBy('created_at') + ->get(); + } + + /** + * キューを完了状態に更新 + * + * @param string|null $comment 完了コメント + * @return bool 更新結果 + */ + public function markAsCompleted(?string $comment = null): bool + { + return $this->update([ + 'que_status' => self::STATUS_COMPLETED, + 'que_status_comment' => $comment ?? self::getStatusComment(self::STATUS_COMPLETED), + 'updated_at' => now() + ]); + } + + /** + * キューをエラー状態に更新 + * + * @param string $errorMessage エラーメッセージ + * @return bool 更新結果 + */ + public function markAsError(string $errorMessage): bool + { + return $this->update([ + 'que_status' => self::STATUS_ERROR, + 'que_status_comment' => $errorMessage, + 'updated_at' => now() + ]); + } + + /** + * ステータスコメントを取得 + * + * @param int $status ステータス + * @return string ステータスコメント + */ + public static function getStatusComment(int $status): string + { + switch ($status) { + case self::STATUS_PENDING: + return '待機中'; + case self::STATUS_COMPLETED: + return '完了'; + case self::STATUS_ERROR: + return 'エラー'; + case self::STATUS_CANCELLED: + return 'キャンセル'; + default: + return '不明'; + } + } + + /** + * キュークラス名を取得 + * + * @param int $queClass キュークラス + * @return string クラス名 + */ + public static function getClassName(int $queClass): string + { + switch ($queClass) { + case self::CLASS_SHJ4C: + return 'SHJ-4C室割当処理'; + case self::CLASS_SHJ6: + return 'SHJ-6サーバ死活監視処理'; + case self::CLASS_SHJ8: + return 'SHJ-8バッチログ処理'; + case self::CLASS_SHJ9: + return 'SHJ-9売上集計処理'; + case self::CLASS_SHJ10: + return 'SHJ-10財政年度売上集計処理'; + case self::CLASS_MAIL_SEND: + return 'メール送信処理'; + default: + return "クラス{$queClass}"; + } + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'OperatorQue[ID:%d, Class:%s, Status:%s, Date:%s]', + $this->que_id, + self::getClassName($this->que_class), + self::getStatusComment($this->que_status), + $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : 'N/A' + ); + } +} diff --git a/app/Models/Park.php b/app/Models/Park.php index 7b0368e..c00c706 100644 --- a/app/Models/Park.php +++ b/app/Models/Park.php @@ -42,3 +42,5 @@ class Park extends Model } + + diff --git a/app/Models/PriceA.php b/app/Models/PriceA.php index 50aa59d..162d4e9 100644 --- a/app/Models/PriceA.php +++ b/app/Models/PriceA.php @@ -58,3 +58,5 @@ class PriceA extends Model } + + diff --git a/app/Models/PrintJobLog.php b/app/Models/PrintJobLog.php new file mode 100644 index 0000000..d5194b1 --- /dev/null +++ b/app/Models/PrintJobLog.php @@ -0,0 +1,251 @@ + 'integer', + 'park_id' => 'integer', + 'user_id' => 'integer', + 'contract_id' => 'integer', + 'error_code' => 'integer', + 'created_at' => 'datetime' + ]; + + /** + * エラーコードの定数 + */ + const ERROR_CODE_THRESHOLD = 100; // エラー判定閾値(>=100がエラー) + + /** + * updateされない設定(created_atのみ) + * + * @var bool + */ + public $timestamps = false; + + /** + * 駐輪場との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function park() + { + return $this->belongsTo(Park::class, 'park_id', 'park_id'); + } + + /** + * ユーザーとの関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id', 'user_id'); + } + + /** + * 契約との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function contract() + { + return $this->belongsTo(RegularContract::class, 'contract_id', 'contract_id'); + } + + /** + * 過去15分間のエラーログを取得 + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getRecentErrorLogs() + { + $fifteenMinutesAgo = Carbon::now()->subMinutes(15); + + return self::where('created_at', '>=', $fifteenMinutesAgo) + ->where('error_code', '>=', self::ERROR_CODE_THRESHOLD) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * 指定期間内のエラーログを取得 + * + * @param string $startTime 開始時刻 + * @param string $endTime 終了時刻 + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getErrorLogsByPeriod(string $startTime, string $endTime) + { + return self::whereBetween('created_at', [$startTime, $endTime]) + ->where('error_code', '>=', self::ERROR_CODE_THRESHOLD) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * 指定期間内の全ログを取得 + * + * @param string $startTime 開始時刻 + * @param string $endTime 終了時刻 + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getLogsByPeriod(string $startTime, string $endTime) + { + return self::whereBetween('created_at', [$startTime, $endTime]) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * プリンタジョブログを作成 + * + * @param array $data ログデータ + * @return PrintJobLog 作成されたログ + */ + public static function createLog(array $data): PrintJobLog + { + $defaultData = [ + 'park_id' => null, + 'user_id' => null, + 'contract_id' => null, + 'process_name' => '', + 'job_name' => '', + 'status' => '', + 'error_code' => 0, + 'status_comment' => '', + 'created_at' => now() + ]; + + $mergedData = array_merge($defaultData, $data); + + return self::create($mergedData); + } + + /** + * エラーログかどうかを判定 + * + * @return bool エラーログかどうか + */ + public function isError(): bool + { + return $this->error_code >= self::ERROR_CODE_THRESHOLD; + } + + /** + * 正常ログかどうかを判定 + * + * @return bool 正常ログかどうか + */ + public function isNormal(): bool + { + return $this->error_code < self::ERROR_CODE_THRESHOLD; + } + + /** + * エラーレベルを取得 + * + * @return string エラーレベル + */ + public function getErrorLevel(): string + { + if ($this->error_code >= 200) { + return '重大エラー'; + } elseif ($this->error_code >= self::ERROR_CODE_THRESHOLD) { + return 'エラー'; + } else { + return '正常'; + } + } + + /** + * エラーログの統計情報を取得 + * + * @param string $startTime 開始時刻 + * @param string $endTime 終了時刻 + * @return array 統計情報 + */ + public static function getErrorStatistics(string $startTime, string $endTime): array + { + $errorLogs = self::getErrorLogsByPeriod($startTime, $endTime); + $totalLogs = self::getLogsByPeriod($startTime, $endTime); + + $errorByCode = $errorLogs->groupBy('error_code')->map->count(); + $errorByProcess = $errorLogs->groupBy('process_name')->map->count(); + + return [ + 'total_logs' => $totalLogs->count(), + 'error_logs' => $errorLogs->count(), + 'error_rate' => $totalLogs->count() > 0 ? + round(($errorLogs->count() / $totalLogs->count()) * 100, 2) : 0, + 'error_by_code' => $errorByCode->toArray(), + 'error_by_process' => $errorByProcess->toArray(), + 'period' => [ + 'start' => $startTime, + 'end' => $endTime + ] + ]; + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'PrintJobLog[ID:%d, Process:%s, ErrorCode:%d, Time:%s]', + $this->log_id, + $this->process_name, + $this->error_code, + $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : 'N/A' + ); + } +} diff --git a/app/Models/Psection.php b/app/Models/Psection.php new file mode 100644 index 0000000..a388378 --- /dev/null +++ b/app/Models/Psection.php @@ -0,0 +1,106 @@ + 'integer', + 'operator_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime' + ]; + + /** + * 売上集計との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function earningsSummaries() + { + return $this->hasMany(EarningsSummary::class, 'psection_id', 'psection_id'); + } + + /** + * 定期契約との関連 + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function regularContracts() + { + return $this->hasMany(RegularContract::class, 'psection_id', 'psection_id'); + } + + /** + * アクティブな車種区分一覧を取得 + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getActivePsections() + { + return self::orderBy('psection_id')->get(); + } + + /** + * 車種区分名で検索 + * + * @param string $subject 車種区分名 + * @return Psection|null + */ + public static function findBySubject(string $subject): ?Psection + { + return self::where('psection_subject', $subject)->first(); + } + + /** + * 文字列表現 + * + * @return string + */ + public function __toString(): string + { + return sprintf( + 'Psection[ID:%d, Subject:%s]', + $this->psection_id, + $this->psection_subject + ); + } +} diff --git a/app/Models/Ptype.php b/app/Models/Ptype.php index f8adcf2..2164953 100644 --- a/app/Models/Ptype.php +++ b/app/Models/Ptype.php @@ -33,3 +33,5 @@ class Ptype extends Model } + + diff --git a/app/Models/RegularContract.php b/app/Models/RegularContract.php index 93ffcbc..44d0fc5 100644 --- a/app/Models/RegularContract.php +++ b/app/Models/RegularContract.php @@ -72,3 +72,5 @@ class RegularContract extends Model } + + diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index b6324de..690cf8b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,9 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; use App\Services\ShjFourCService; use App\Services\ShjMailSendService; +use App\Services\ShjNineService; +use App\Services\ShjTenService; +use App\Services\ShjSixService; class AppServiceProvider extends ServiceProvider { @@ -30,7 +33,41 @@ class AppServiceProvider extends ServiceProvider ); }); + // SHJ-9売上集計処理サービスを登録 + $this->app->singleton(ShjNineService::class, function ($app) { + return new ShjNineService( + $app->make(\App\Models\Park::class), + $app->make(\App\Models\RegularContract::class), + $app->make(\App\Models\EarningsSummary::class), + $app->make(\App\Models\Psection::class), + $app->make(\App\Models\Batch\BatchLog::class), + $app->make(\App\Models\OperatorQue::class) + ); + }); + // SHJ-10財政年度売上集計処理サービスを登録 + $this->app->singleton(ShjTenService::class, function ($app) { + return new ShjTenService( + $app->make(\App\Models\Park::class), + $app->make(\App\Models\RegularContract::class), + $app->make(\App\Models\EarningsSummary::class), + $app->make(\App\Models\Psection::class), + $app->make(\App\Models\Batch\BatchLog::class), + $app->make(\App\Models\OperatorQue::class) + ); + }); + + // SHJ-6サーバ死活監視処理サービスを登録 + $this->app->singleton(ShjSixService::class, function ($app) { + return new ShjSixService( + $app->make(\App\Models\Device::class), + $app->make(\App\Models\HardwareCheckLog::class), + $app->make(\App\Models\PrintJobLog::class), + $app->make(\App\Models\Batch\BatchLog::class), + $app->make(\App\Models\OperatorQue::class), + $app->make(\App\Services\ShjMailSendService::class) + ); + }); } /** diff --git a/app/Providers/LegacyServiceProvider.php b/app/Providers/LegacyServiceProvider.php index ccd561a..bdc377a 100644 --- a/app/Providers/LegacyServiceProvider.php +++ b/app/Providers/LegacyServiceProvider.php @@ -42,3 +42,4 @@ class LegacyServiceProvider extends ServiceProvider } + diff --git a/app/Services/FileService.php b/app/Services/FileService.php index c005e69..3b46b48 100644 --- a/app/Services/FileService.php +++ b/app/Services/FileService.php @@ -32,3 +32,5 @@ class FileService } + + diff --git a/app/Services/OperatorQueService.php b/app/Services/OperatorQueService.php index 2473794..4c43d34 100644 --- a/app/Services/OperatorQueService.php +++ b/app/Services/OperatorQueService.php @@ -25,3 +25,5 @@ class OperatorQueService } + + diff --git a/app/Services/ShjNineService.php b/app/Services/ShjNineService.php new file mode 100644 index 0000000..f5e0957 --- /dev/null +++ b/app/Services/ShjNineService.php @@ -0,0 +1,595 @@ +parkModel = $parkModel; + $this->contractModel = $contractModel; + $this->earningsSummaryModel = $earningsSummaryModel; + $this->psectionModel = $psectionModel; + $this->batchLogModel = $batchLogModel; + $this->operatorQueModel = $operatorQueModel; + } + + /** + * SHJ-9 売上集計処理メイン実行 + * + * 処理フロー: + * 【処理1】集計対象を設定する + * 【処理2】駐輪場マスタを取得する + * 【判断1】取得件数判定 + * 【処理3】車種区分毎に算出する + * 【判断2】取得判定 + * 【処理4】売上集計結果を削除→登録する + * 【処理5】オペレータキュー作成およびバッチ処理ログを作成する + * + * @param string $type 集計種別(daily/monthly/yearly) + * @param string $aggregationDate 集計対象日 + * @return array 処理結果 + */ + public function executeEarningsAggregation(string $type, string $aggregationDate): array + { + $batchLogId = null; + + try { + // 【処理1】集計対象を設定する + $aggregationTarget = $this->setAggregationTarget($type, $aggregationDate); + + // バッチ処理開始ログ作成 + $batchLog = BatchLog::createBatchLog( + 'shj9', + BatchLog::STATUS_START, + [ + 'type' => $type, + 'aggregation_date' => $aggregationDate, + 'aggregation_target' => $aggregationTarget + ], + "SHJ-9 売上集計処理開始 ({$type})" + ); + $batchLogId = $batchLog->id; + + Log::info('SHJ-9 売上集計処理開始', [ + 'batch_log_id' => $batchLogId, + 'type' => $type, + 'aggregation_date' => $aggregationDate, + 'aggregation_target' => $aggregationTarget + ]); + + // 【処理2】駐輪場マスタを取得する + $parkInfo = $this->getParkInformation(); + + // 【判断1】取得件数判定 + if (empty($parkInfo)) { + $message = '売上集計(' . $this->getTypeLabel($type) . '):駐輪場マスタが存在していません。'; + + // バッチログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_WARNING, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 // 処理は成功したが対象なし + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($message, $batchLogId); + + return [ + 'success' => true, + 'message' => $message, + 'processed_parks' => 0, + 'summary_records' => 0, + 'batch_log_id' => $batchLogId + ]; + } + + // 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する + $summaryRecords = 0; + $processedParks = 0; + + foreach ($parkInfo as $park) { + $parkSummaryRecords = $this->processEarningsForPark($park, $aggregationTarget, $type); + + if ($parkSummaryRecords > 0) { + $processedParks++; + $summaryRecords += $parkSummaryRecords; + } + } + + // 【判断2】取得判定 + if ($summaryRecords === 0) { + $message = '対象なしの結果を設定する(契約)'; + + // バッチログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_WARNING, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($message, $batchLogId); + + return [ + 'success' => true, + 'message' => $message, + 'processed_parks' => $processedParks, + 'summary_records' => 0, + 'batch_log_id' => $batchLogId + ]; + } + + // バッチ処理完了ログ更新 + $completionMessage = "SHJ-9 売上集計処理正常完了 ({$type}) - 駐輪場数: {$processedParks}, 集計レコード数: {$summaryRecords}"; + $batchLog->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => $completionMessage, + 'success_count' => 1 + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($completionMessage, $batchLogId); + + Log::info('SHJ-9 売上集計処理完了', [ + 'batch_log_id' => $batchLogId, + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords + ]); + + return [ + 'success' => true, + 'message' => 'SHJ-9 売上集計処理が正常に完了しました', + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-9 売上集計処理でエラーが発生: ' . $e->getMessage(); + + if (isset($batchLog) && $batchLog) { + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $errorMessage, + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + Log::error('SHJ-9 売上集計処理エラー', [ + 'batch_log_id' => $batchLogId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'details' => $e->getMessage(), + 'batch_log_id' => $batchLogId + ]; + } + } + + /** + * 【処理1】集計対象を設定する + * + * @param string $type 集計種別 + * @param string $aggregationDate 集計対象日 + * @return array 集計対象情報 + */ + private function setAggregationTarget(string $type, string $aggregationDate): array + { + $date = Carbon::parse($aggregationDate); + + switch ($type) { + case 'daily': + return [ + 'type' => 'daily', + 'start_date' => $date->format('Y-m-d'), + 'end_date' => $date->format('Y-m-d'), + 'summary_type' => '日次' + ]; + + case 'monthly': + return [ + 'type' => 'monthly', + 'start_date' => $date->startOfMonth()->format('Y-m-d'), + 'end_date' => $date->endOfMonth()->format('Y-m-d'), + 'summary_type' => '月次' + ]; + + case 'yearly': + return [ + 'type' => 'yearly', + 'start_date' => $date->startOfYear()->format('Y-m-d'), + 'end_date' => $date->endOfYear()->format('Y-m-d'), + 'summary_type' => '年次' + ]; + + default: + throw new \InvalidArgumentException("不正な集計種別: {$type}"); + } + } + + /** + * 【処理2】駐輪場マスタを取得する + * + * 仕様書のSQLクエリに基づく駐輪場情報取得 + * SELECT 駐輪場ID, 駐輪場名 + * FROM 駐輪場マスタ + * WHERE 閉設フラグ <> 1 + * ORDER BY 駐輪場ふりがな + * + * @return array 駐輪場情報 + */ + private function getParkInformation(): array + { + try { + $parkInfo = DB::table('park') + ->select(['park_id', 'park_name']) + ->where('park_close_flag', '<>', 1) + ->orderBy('park_ruby') + ->get() + ->toArray(); + + Log::info('駐輪場マスタ取得完了', [ + 'park_count' => count($parkInfo) + ]); + + return $parkInfo; + + } catch (\Exception $e) { + Log::error('駐輪場マスタ取得エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 駐輪場毎の売上集計処理 + * + * @param object $park 駐輪場情報 + * @param array $aggregationTarget 集計対象 + * @param string $type 集計種別 + * @return int 作成された集計レコード数 + */ + private function processEarningsForPark($park, array $aggregationTarget, string $type): int + { + try { + // 【処理4】既存の売上集計結果を削除 + $this->deleteExistingSummary($park->park_id, $aggregationTarget); + + // 【処理3】車種区分毎に算出する + $psections = $this->getPsectionInformation(); + $summaryRecords = 0; + + foreach ($psections as $psection) { + $earningsData = $this->calculateEarningsForPsection( + $park->park_id, + $psection->psection_id, + $aggregationTarget + ); + + if ($this->hasEarningsData($earningsData)) { + // 売上集計結果を登録 + $this->createEarningsSummary($park, $psection, $aggregationTarget, $earningsData, $type); + $summaryRecords++; + } + } + + Log::info('駐輪場売上集計完了', [ + 'park_id' => $park->park_id, + 'park_name' => $park->park_name, + 'summary_records' => $summaryRecords + ]); + + return $summaryRecords; + + } catch (\Exception $e) { + Log::error('駐輪場売上集計エラー', [ + 'park_id' => $park->park_id, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 車種区分情報取得 + * + * @return array 車種区分情報 + */ + private function getPsectionInformation(): array + { + return DB::table('psection') + ->select(['psection_id', 'psection_subject']) + ->get() + ->toArray(); + } + + /** + * 【処理3】車種区分毎に売上を算出する + * + * 4つの項目を計算: + * ①売上・件数 + * ②一時金売上 + * ③解約返戻金 + * ④再発行金額・件数 + * + * @param int $parkId 駐輪場ID + * @param int $psectionId 車種区分ID + * @param array $aggregationTarget 集計対象 + * @return array 売上データ + */ + private function calculateEarningsForPsection(int $parkId, int $psectionId, array $aggregationTarget): array + { + $startDate = $aggregationTarget['start_date']; + $endDate = $aggregationTarget['end_date']; + + // ①売上・件数(billing_amount) + $salesData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as sales_count'), + DB::raw('COALESCE(SUM(billing_amount), 0) as sales_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('contract_flag', 1) + ->whereBetween('contract_payment_day', [$startDate, $endDate]) + ->whereNull('contract_cancelday') + ->first(); + + // ②一時金売上(contract_money) + $temporaryData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as temporary_count'), + DB::raw('COALESCE(SUM(contract_money), 0) as temporary_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('contract_flag', 1) + ->whereBetween('contract_payment_day', [$startDate, $endDate]) + ->whereNotNull('contract_money') + ->where('contract_money', '>', 0) + ->first(); + + // ③解約返戻金(refunds) + $refundData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as refund_count'), + DB::raw('COALESCE(SUM(refunds), 0) as refund_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->whereBetween('contract_cancelday', [$startDate, $endDate]) + ->whereNotNull('refunds') + ->where('refunds', '>', 0) + ->first(); + + // ④再発行金額・件数(seal_reissue_request) + $reissueData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as reissue_count'), + DB::raw('COALESCE(SUM(contract_seal_issue), 0) as reissue_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('seal_reissue_request', 1) + ->whereBetween('updated_at', [$startDate, $endDate]) + ->first(); + + return [ + 'sales_count' => $salesData->sales_count ?? 0, + 'sales_amount' => $salesData->sales_amount ?? 0, + 'temporary_count' => $temporaryData->temporary_count ?? 0, + 'temporary_amount' => $temporaryData->temporary_amount ?? 0, + 'refund_count' => $refundData->refund_count ?? 0, + 'refund_amount' => $refundData->refund_amount ?? 0, + 'reissue_count' => $reissueData->reissue_count ?? 0, + 'reissue_amount' => $reissueData->reissue_amount ?? 0 + ]; + } + + /** + * 売上データの存在チェック + * + * @param array $earningsData 売上データ + * @return bool データが存在するかどうか + */ + private function hasEarningsData(array $earningsData): bool + { + return $earningsData['sales_count'] > 0 || + $earningsData['temporary_count'] > 0 || + $earningsData['refund_count'] > 0 || + $earningsData['reissue_count'] > 0; + } + + /** + * 【処理4】既存の売上集計結果を削除 + * + * @param int $parkId 駐輪場ID + * @param array $aggregationTarget 集計対象 + * @return void + */ + private function deleteExistingSummary(int $parkId, array $aggregationTarget): void + { + DB::table('earnings_summary') + ->where('park_id', $parkId) + ->where('summary_start_date', $aggregationTarget['start_date']) + ->where('summary_end_date', $aggregationTarget['end_date']) + ->delete(); + } + + /** + * 売上集計結果を登録 + * + * @param object $park 駐輪場情報 + * @param object $psection 車種区分情報 + * @param array $aggregationTarget 集計対象 + * @param array $earningsData 売上データ + * @param string $type 集計種別 + * @return void + */ + private function createEarningsSummary($park, $psection, array $aggregationTarget, array $earningsData, string $type): void + { + DB::table('earnings_summary')->insert([ + 'park_id' => $park->park_id, + 'summary_type' => $aggregationTarget['summary_type'], + 'summary_start_date' => $aggregationTarget['start_date'], + 'summary_end_date' => $aggregationTarget['end_date'], + 'earnings_date' => $aggregationTarget['end_date'], // 集計日として終了日を使用 + 'psection_id' => $psection->psection_id, + 'usertype_subject1' => $psection->psection_subject, + 'regular_new_count' => $earningsData['sales_count'], + 'regular_new_amount' => $earningsData['sales_amount'], + 'turnsum' => $earningsData['temporary_amount'], + 'turnsum_count' => $earningsData['temporary_count'], + 'refunds' => $earningsData['refund_amount'], + 'reissue_count' => $earningsData['reissue_count'], + 'reissue_amount' => $earningsData['reissue_amount'], + 'summary_note' => "SHJ-9 {$type} 売上集計結果", + 'created_at' => now(), + 'updated_at' => now(), + 'operator_id' => 0 // システム処理 + ]); + } + + /** + * 【処理5】オペレータキュー作成 + * + * @param string $message メッセージ + * @param int $batchLogId バッチログID + * @return void + */ + private function createOperatorQueue(string $message, int $batchLogId): void + { + try { + DB::table('operator_que')->insert([ + 'que_class' => 9, // SHJ-9用のクラス + 'user_id' => null, + 'contract_id' => null, + 'park_id' => null, + 'que_comment' => $message, + 'que_status' => 1, // 完了 + 'que_status_comment' => 'バッチ処理完了', + 'work_instructions' => "SHJ-9売上集計処理 BatchLogID: {$batchLogId}", + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレータキュー作成完了', [ + 'batch_log_id' => $batchLogId, + 'message' => $message + ]); + + } catch (\Exception $e) { + Log::error('オペレータキュー作成エラー', [ + 'batch_log_id' => $batchLogId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * 集計種別のラベル取得 + * + * @param string $type 集計種別 + * @return string ラベル + */ + private function getTypeLabel(string $type): string + { + switch ($type) { + case 'daily': + return '日次'; + case 'monthly': + return '月次'; + case 'yearly': + return '年次'; + default: + return $type; + } + } +} diff --git a/app/Services/ShjSixService.php b/app/Services/ShjSixService.php new file mode 100644 index 0000000..9ec63af --- /dev/null +++ b/app/Services/ShjSixService.php @@ -0,0 +1,710 @@ +deviceModel = $deviceModel; + $this->hardwareCheckLogModel = $hardwareCheckLogModel; + $this->printJobLogModel = $printJobLogModel; + $this->batchLogModel = $batchLogModel; + $this->operatorQueModel = $operatorQueModel; + $this->mailSendService = $mailSendService; + } + + /** + * SHJ-6 サーバ死活監視処理メイン実行 + * + * 処理フロー: + * 【処理1】サーバ死活監視(DBアクセス) + * 【処理2】デバイス管理マスタを取得する + * 【処理3】デバイス毎のハードウェア状態を取得する + * 【処理4】プリンタ制御プログラムログを取得する + * 【判断3】エラーログ有無 + * 【処理5】バッチ処理ログを作成する + * ※ 異常時は共通A処理を実行 + * + * @return array 処理結果 + */ + public function executeServerMonitoring(): array + { + $batchLogId = null; + $warnings = []; + $errorDetails = []; + + try { + // バッチ処理開始ログ作成 + $batchLog = BatchLog::createBatchLog( + 'shj6', + BatchLog::STATUS_START, + [], + 'SHJ-6 サーバ死活監視処理開始' + ); + $batchLogId = $batchLog->id; + + Log::info('SHJ-6 サーバ死活監視処理開始', [ + 'batch_log_id' => $batchLogId + ]); + + // 【処理1】サーバ死活監視(DBアクセス) + $dbAccessResult = $this->checkDatabaseAccess(); + if (!$dbAccessResult['success']) { + // DB接続NGの場合は共通A処理実行 + $this->executeCommonProcessA($batchLogId, 'DB接続エラー: ' . $dbAccessResult['message']); + + return [ + 'success' => false, + 'message' => 'データベース接続エラーが発生しました', + 'error_details' => [$dbAccessResult['message']], + 'batch_log_id' => $batchLogId + ]; + } + + // 【処理2】デバイス管理マスタを取得する + $devices = $this->getDeviceManagementData(); + Log::info('デバイス管理マスタ取得完了', [ + 'device_count' => count($devices) + ]); + + // 【処理3】デバイス毎のハードウェア状態を取得する + $hardwareStatusResult = $this->getHardwareStatus($devices); + if (!$hardwareStatusResult['success']) { + // ハードウェア状態取得できなかった場合は共通A処理実行 + $this->executeCommonProcessA($batchLogId, 'ハードウェア状態取得エラー: ' . $hardwareStatusResult['message']); + $warnings[] = 'ハードウェア状態の一部で異常を検出しました'; + $errorDetails[] = $hardwareStatusResult['message']; + } + + // 【処理4】プリンタ制御プログラムログを取得する + $printerLogResult = $this->getPrinterControlLogs(); + + // 【判断3】エラーログ有無 + if ($printerLogResult['has_errors']) { + // エラーログ有の場合は共通A処理実行 + $this->executeCommonProcessA($batchLogId, 'プリンタエラーログ検出: ' . $printerLogResult['error_summary']); + $warnings[] = 'プリンタ制御でエラーが検出されました'; + $errorDetails[] = $printerLogResult['error_summary']; + } + + // 【処理5】バッチ処理ログを作成する + $monitoringSummary = $this->createMonitoringSummary($devices, $hardwareStatusResult, $printerLogResult); + + $status = empty($warnings) ? BatchLog::STATUS_SUCCESS : BatchLog::STATUS_WARNING; + $message = empty($warnings) ? + 'SHJ-6 サーバ死活監視処理正常完了' : + 'SHJ-6 サーバ死活監視処理完了(警告あり)'; + + $batchLog->update([ + 'status' => $status, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 + ]); + + Log::info('SHJ-6 サーバ死活監視処理完了', [ + 'batch_log_id' => $batchLogId, + 'monitoring_summary' => $monitoringSummary, + 'warnings' => $warnings + ]); + + return [ + 'success' => true, + 'message' => 'SHJ-6 サーバ死活監視処理が完了しました', + 'monitoring_summary' => $monitoringSummary, + 'warnings' => $warnings, + 'error_details' => $errorDetails, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-6 サーバ死活監視処理でエラーが発生: ' . $e->getMessage(); + + if (isset($batchLog) && $batchLog) { + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $errorMessage, + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + // 例外発生時も共通A処理実行 + $this->executeCommonProcessA($batchLogId, $errorMessage); + + Log::error('SHJ-6 サーバ死活監視処理エラー', [ + 'batch_log_id' => $batchLogId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'error_details' => [$e->getMessage()], + 'batch_log_id' => $batchLogId + ]; + } + } + + /** + * 【処理1】サーバ死活監視(DBアクセス) + * + * @return array アクセス結果 + */ + private function checkDatabaseAccess(): array + { + try { + // 設定マスタテーブルへの簡単なクエリでDB接続確認 + $result = DB::select('SELECT 1 as test'); + + if (empty($result)) { + return [ + 'success' => false, + 'message' => 'データベースクエリの結果が空です' + ]; + } + + Log::info('データベース接続確認成功'); + + return [ + 'success' => true, + 'message' => 'データベース接続正常' + ]; + + } catch (\Exception $e) { + Log::error('データベース接続エラー', [ + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => $e->getMessage() + ]; + } + } + + /** + * 【処理2】デバイス管理マスタを取得する + * + * @return array デバイス情報 + */ + private function getDeviceManagementData(): array + { + try { + $devices = DB::table('device') + ->select([ + 'device_id', + 'park_id', + 'device_type', + 'device_subject', + 'device_identifier', + 'device_work', + 'device_workstart', + 'device_replace', + 'device_remarks', + 'operator_id' + ]) + ->where('device_workstart', '<=', now()) + ->whereNull('device_replace') + ->get() + ->toArray(); + + Log::info('デバイス管理マスタ取得完了', [ + 'device_count' => count($devices) + ]); + + return $devices; + + } catch (\Exception $e) { + Log::error('デバイス管理マスタ取得エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 【処理3】デバイス毎のハードウェア状態を取得する + * + * @param array $devices デバイス一覧 + * @return array ハードウェア状態結果 + */ + private function getHardwareStatus(array $devices): array + { + try { + $normalDevices = 0; + $abnormalDevices = 0; + $abnormalDetails = []; + + foreach ($devices as $device) { + $latestStatus = HardwareCheckLog::getLatestStatusByDevice($device->device_id); + + if (!$latestStatus) { + $abnormalDevices++; + $abnormalDetails[] = "デバイスID {$device->device_id}: ハードウェア状態ログが存在しません"; + continue; + } + + if ($latestStatus->isNormal()) { + $normalDevices++; + } else { + $abnormalDevices++; + $abnormalDetails[] = "デバイスID {$device->device_id}: {$latestStatus->getStatusNameAttribute()} - {$latestStatus->status_comment}"; + } + } + + Log::info('ハードウェア状態取得完了', [ + 'total_devices' => count($devices), + 'normal_devices' => $normalDevices, + 'abnormal_devices' => $abnormalDevices + ]); + + return [ + 'success' => $abnormalDevices === 0, + 'total_devices' => count($devices), + 'normal_devices' => $normalDevices, + 'abnormal_devices' => $abnormalDevices, + 'abnormal_details' => $abnormalDetails, + 'message' => $abnormalDevices > 0 ? + "{$abnormalDevices}台のデバイスで異常を検出" : + '全デバイス正常' + ]; + + } catch (\Exception $e) { + Log::error('ハードウェア状態取得エラー', [ + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => $e->getMessage(), + 'abnormal_details' => ["ハードウェア状態取得中にエラーが発生: " . $e->getMessage()] + ]; + } + } + + /** + * 【処理4】プリンタ制御プログラムログを取得する + * + * @return array プリンタログ結果 + */ + private function getPrinterControlLogs(): array + { + try { + // 過去15分間のエラーログを取得 + $errorLogs = PrintJobLog::getRecentErrorLogs(); + + $hasErrors = $errorLogs->count() > 0; + $errorSummary = ''; + $errorDetails = []; + + if ($hasErrors) { + $errorsByCode = $errorLogs->groupBy('error_code'); + $errorSummaryParts = []; + + foreach ($errorsByCode as $errorCode => $logs) { + $count = $logs->count(); + $errorSummaryParts[] = "エラーコード{$errorCode}: {$count}件"; + + foreach ($logs as $log) { + $errorDetails[] = sprintf( + "[%s] %s - エラーコード: %d, %s", + $log->created_at->format('Y-m-d H:i:s'), + $log->process_name, + $log->error_code, + $log->status_comment + ); + } + } + + $errorSummary = implode(', ', $errorSummaryParts); + } + + Log::info('プリンタ制御プログラムログ取得完了', [ + 'monitoring_period_minutes' => self::PRINTER_LOG_MONITOR_MINUTES, + 'error_logs_count' => $errorLogs->count(), + 'has_errors' => $hasErrors + ]); + + return [ + 'success' => true, + 'has_errors' => $hasErrors, + 'error_count' => $errorLogs->count(), + 'error_summary' => $errorSummary, + 'error_details' => $errorDetails, + 'monitoring_period' => self::PRINTER_LOG_MONITOR_MINUTES . '分間' + ]; + + } catch (\Exception $e) { + Log::error('プリンタ制御プログラムログ取得エラー', [ + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'has_errors' => true, + 'error_summary' => 'ログ取得エラー: ' . $e->getMessage(), + 'error_details' => ["プリンタログ取得中にエラーが発生: " . $e->getMessage()] + ]; + } + } + + /** + * 監視結果サマリーを作成 + * + * @param array $devices デバイス一覧 + * @param array $hardwareResult ハードウェア結果 + * @param array $printerResult プリンタ結果 + * @return string 監視サマリー + */ + private function createMonitoringSummary(array $devices, array $hardwareResult, array $printerResult): string + { + $summary = [ + 'デバイス数: ' . count($devices), + 'ハードウェア正常: ' . ($hardwareResult['normal_devices'] ?? 0) . '台', + 'ハードウェア異常: ' . ($hardwareResult['abnormal_devices'] ?? 0) . '台', + 'プリンタエラー: ' . ($printerResult['error_count'] ?? 0) . '件' + ]; + + return implode(', ', $summary); + } + + /** + * 共通A処理:監視結果を反映 + * + * @param int|null $batchLogId バッチログID + * @param string $alertMessage アラートメッセージ + * @return void + */ + private function executeCommonProcessA(?int $batchLogId, string $alertMessage): void + { + try { + Log::info('共通A処理開始', [ + 'batch_log_id' => $batchLogId, + 'alert_message' => $alertMessage + ]); + + // 【共通判断1】DB反映可否判定 + $canReflectToDb = $this->canReflectToDatabase(); + + if ($canReflectToDb) { + // 【共通処理1】オペレータキューを登録する + $this->registerOperatorQueue($alertMessage, $batchLogId); + + // 【共通処理2】メール送信対象オペレータを取得する + $operators = $this->getMailTargetOperators(); + + // 【共通判断2】送信対象有無 + if (!empty($operators)) { + foreach ($operators as $operator) { + $this->sendAlertMail($operator['email'], $alertMessage, 'オペレータ'); + } + } + + // 【共通処理3】駐輪場管理者を取得する + $parkManagers = $this->getParkManagers(); + + // 【共通判断3】送信対象有無 + if (!empty($parkManagers)) { + foreach ($parkManagers as $manager) { + $this->sendAlertMail($manager['email'], $alertMessage, '駐輪場管理者'); + } + } + + } else { + // DB反映NGの場合は固定メールアドレスに送信 + $this->sendAlertMail(self::FIXED_EMAIL_ADDRESS, $alertMessage, 'システム管理者'); + } + + Log::info('共通A処理完了', [ + 'batch_log_id' => $batchLogId + ]); + + } catch (\Exception $e) { + Log::error('共通A処理エラー', [ + 'batch_log_id' => $batchLogId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * DB反映可否判定 + * + * @return bool 反映可能かどうか + */ + private function canReflectToDatabase(): bool + { + try { + // 簡単なINSERTテストでDB反映可否を確認 + DB::beginTransaction(); + $testId = DB::table('operator_que')->insertGetId([ + 'que_class' => 6, + 'que_comment' => 'DB反映テスト', + 'que_status' => 0, + 'created_at' => now(), + 'updated_at' => now() + ]); + + // テストレコードを削除 + DB::table('operator_que')->where('que_id', $testId)->delete(); + DB::commit(); + + return true; + + } catch (\Exception $e) { + DB::rollBack(); + Log::warning('DB反映不可', ['error' => $e->getMessage()]); + return false; + } + } + + /** + * オペレータキューを登録 + * + * @param string $alertMessage アラートメッセージ + * @param int|null $batchLogId バッチログID + * @return void + */ + private function registerOperatorQueue(string $alertMessage, ?int $batchLogId): void + { + try { + OperatorQue::create([ + 'que_class' => 6, // SHJ-6用のクラス + 'user_id' => null, + 'contract_id' => null, + 'park_id' => null, + 'que_comment' => $alertMessage, + 'que_status' => 0, // 待機中 + 'que_status_comment' => 'システム監視アラート', + 'work_instructions' => "SHJ-6監視処理 BatchLogID: {$batchLogId}", + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレータキュー登録完了', [ + 'batch_log_id' => $batchLogId, + 'alert_message' => $alertMessage + ]); + + } catch (\Exception $e) { + Log::error('オペレータキュー登録エラー', [ + 'batch_log_id' => $batchLogId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * メール送信対象オペレータを取得 + * + * @return array オペレータ一覧 + */ + private function getMailTargetOperators(): array + { + try { + // user_typeがオペレータのユーザーを取得(仮の条件) + $operators = DB::table('users') + ->select(['user_id', 'email', 'name']) + ->where('user_type', 'operator') // 実際のテーブル構造に合わせて調整 + ->whereNotNull('email') + ->where('email', '!=', '') + ->get() + ->map(function ($user) { + return [ + 'user_id' => $user->user_id, + 'email' => $user->email, + 'name' => $user->name ?? 'オペレータ' + ]; + }) + ->toArray(); + + Log::info('メール送信対象オペレータ取得完了', [ + 'operator_count' => count($operators) + ]); + + return $operators; + + } catch (\Exception $e) { + Log::error('メール送信対象オペレータ取得エラー', [ + 'error' => $e->getMessage() + ]); + + return []; + } + } + + /** + * 駐輪場管理者を取得 + * + * @return array 駐輪場管理者一覧 + */ + private function getParkManagers(): array + { + try { + // 駐輪場管理者を取得(仮の条件) + $managers = DB::table('users') + ->select(['user_id', 'email', 'name']) + ->where('user_type', 'park_manager') // 実際のテーブル構造に合わせて調整 + ->whereNotNull('email') + ->where('email', '!=', '') + ->get() + ->map(function ($user) { + return [ + 'user_id' => $user->user_id, + 'email' => $user->email, + 'name' => $user->name ?? '駐輪場管理者' + ]; + }) + ->toArray(); + + Log::info('駐輪場管理者取得完了', [ + 'manager_count' => count($managers) + ]); + + return $managers; + + } catch (\Exception $e) { + Log::error('駐輪場管理者取得エラー', [ + 'error' => $e->getMessage() + ]); + + return []; + } + } + + /** + * アラートメールを送信 + * + * @param string $email メールアドレス + * @param string $alertMessage アラートメッセージ + * @param string $recipientType 受信者タイプ + * @return void + */ + private function sendAlertMail(string $email, string $alertMessage, string $recipientType): void + { + try { + // SHJメール送信機能を使用(メールテンプレートID=1を使用、実際の値に調整) + $result = $this->mailSendService->executeMailSend( + $email, + '', // 予備メールアドレスは空 + 1 // システムアラート用メールテンプレートID + ); + + if ($result['success']) { + Log::info('アラートメール送信成功', [ + 'email' => $email, + 'recipient_type' => $recipientType, + 'alert_message' => $alertMessage + ]); + } else { + Log::error('アラートメール送信失敗', [ + 'email' => $email, + 'recipient_type' => $recipientType, + 'error' => $result['message'] + ]); + } + + } catch (\Exception $e) { + Log::error('アラートメール送信エラー', [ + 'email' => $email, + 'recipient_type' => $recipientType, + 'error' => $e->getMessage() + ]); + } + } +} diff --git a/app/Services/ShjTenService.php b/app/Services/ShjTenService.php new file mode 100644 index 0000000..f1a59cd --- /dev/null +++ b/app/Services/ShjTenService.php @@ -0,0 +1,583 @@ +parkModel = $parkModel; + $this->contractModel = $contractModel; + $this->earningsSummaryModel = $earningsSummaryModel; + $this->psectionModel = $psectionModel; + $this->batchLogModel = $batchLogModel; + $this->operatorQueModel = $operatorQueModel; + } + + /** + * SHJ-10 財政年度売上集計処理メイン実行 + * + * 処理フロー: + * 【処理1】集計対象を設定する + * 【処理2】駐輪場マスタを取得する + * 【判断1】取得件数判定 + * 【処理3】車種区分毎に算出する + * 【判断2】取得判定 + * 【処理4】売上集計結果を削除→登録する + * 【処理5】オペレータキュー作成およびバッチ処理ログを作成する + * + * @param string $type 集計種別(yearly/monthly) + * @param string $target 集計対象 + * @param array $fiscalPeriod 財政期間情報 + * @return array 処理結果 + */ + public function executeFiscalEarningsAggregation(string $type, string $target, array $fiscalPeriod): array + { + $batchLogId = null; + + try { + // 【処理1】集計対象を設定する(財政年度ベース) + $aggregationTarget = $this->setFiscalAggregationTarget($fiscalPeriod); + + // バッチ処理開始ログ作成 + $batchLog = BatchLog::createBatchLog( + 'shj10', + BatchLog::STATUS_START, + [ + 'type' => $type, + 'target' => $target, + 'fiscal_period' => $fiscalPeriod, + 'aggregation_target' => $aggregationTarget + ], + "SHJ-10 売上集計処理開始 ({$type}: {$fiscalPeriod['target_label']})" + ); + $batchLogId = $batchLog->id; + + Log::info('SHJ-10 売上集計処理開始', [ + 'batch_log_id' => $batchLogId, + 'type' => $type, + 'target' => $target, + 'fiscal_period' => $fiscalPeriod, + 'aggregation_target' => $aggregationTarget + ]); + + // 【処理2】駐輪場マスタを取得する + $parkInfo = $this->getParkInformation(); + + // 【判断1】取得件数判定 + if (empty($parkInfo)) { + $typeLabel = $this->getTypeLabel($type); + $message = "売上集計({$typeLabel}):駐輪場マスタが存在していません。"; + + // バッチログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_WARNING, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 // 処理は成功したが対象なし + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($message, $batchLogId); + + return [ + 'success' => true, + 'message' => $message, + 'processed_parks' => 0, + 'summary_records' => 0, + 'batch_log_id' => $batchLogId + ]; + } + + // 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する + $summaryRecords = 0; + $processedParks = 0; + + foreach ($parkInfo as $park) { + $parkSummaryRecords = $this->processFiscalEarningsForPark($park, $aggregationTarget, $fiscalPeriod); + + if ($parkSummaryRecords > 0) { + $processedParks++; + $summaryRecords += $parkSummaryRecords; + } + } + + // 【判断2】取得判定 + if ($summaryRecords === 0) { + $message = '対象なしの結果を設定する(契約)'; + + // バッチログ更新 + $batchLog->update([ + 'status' => BatchLog::STATUS_WARNING, + 'end_time' => now(), + 'message' => $message, + 'success_count' => 1 + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($message, $batchLogId); + + return [ + 'success' => true, + 'message' => $message, + 'processed_parks' => $processedParks, + 'summary_records' => 0, + 'batch_log_id' => $batchLogId + ]; + } + + // バッチ処理完了ログ更新 + $completionMessage = "SHJ-10 売上集計処理正常完了 ({$type}: {$fiscalPeriod['target_label']}) - 駐輪場数: {$processedParks}, 集計レコード数: {$summaryRecords}"; + $batchLog->update([ + 'status' => BatchLog::STATUS_SUCCESS, + 'end_time' => now(), + 'message' => $completionMessage, + 'success_count' => 1 + ]); + + // 【処理5】オペレータキュー作成 + $this->createOperatorQueue($completionMessage, $batchLogId); + + Log::info('SHJ-10 売上集計処理完了', [ + 'batch_log_id' => $batchLogId, + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords + ]); + + return [ + 'success' => true, + 'message' => 'SHJ-10 売上集計処理が正常に完了しました', + 'processed_parks' => $processedParks, + 'summary_records' => $summaryRecords, + 'batch_log_id' => $batchLogId + ]; + + } catch (\Exception $e) { + $errorMessage = 'SHJ-10 売上集計処理でエラーが発生: ' . $e->getMessage(); + + if (isset($batchLog) && $batchLog) { + $batchLog->update([ + 'status' => BatchLog::STATUS_ERROR, + 'end_time' => now(), + 'message' => $errorMessage, + 'error_details' => $e->getMessage(), + 'error_count' => 1 + ]); + } + + Log::error('SHJ-10 売上集計処理エラー', [ + 'batch_log_id' => $batchLogId, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return [ + 'success' => false, + 'message' => $errorMessage, + 'details' => $e->getMessage(), + 'batch_log_id' => $batchLogId + ]; + } + } + + /** + * 【処理1】財政年度集計対象を設定する + * + * @param array $fiscalPeriod 財政期間情報 + * @return array 集計対象情報 + */ + private function setFiscalAggregationTarget(array $fiscalPeriod): array + { + return [ + 'type' => $fiscalPeriod['type'], + 'start_date' => $fiscalPeriod['start_date'], + 'end_date' => $fiscalPeriod['end_date'], + 'summary_type' => $fiscalPeriod['summary_type'], + 'fiscal_year' => $fiscalPeriod['fiscal_year'], + 'target_label' => $fiscalPeriod['target_label'] + ]; + } + + /** + * 【処理2】駐輪場マスタを取得する + * + * 仕様書のSQLクエリに基づく駐輪場情報取得 + * SELECT 駐輪場ID, 駐輪場名 + * FROM 駐輪場マスタ + * WHERE 閉設フラグ <> 1 + * ORDER BY 駐輪場ふりがな + * + * @return array 駐輪場情報 + */ + private function getParkInformation(): array + { + try { + $parkInfo = DB::table('park') + ->select(['park_id', 'park_name']) + ->where('park_close_flag', '<>', 1) + ->orderBy('park_ruby') + ->get() + ->toArray(); + + Log::info('駐輪場マスタ取得完了', [ + 'park_count' => count($parkInfo) + ]); + + return $parkInfo; + + } catch (\Exception $e) { + Log::error('駐輪場マスタ取得エラー', [ + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 駐輪場毎の財政年度売上集計処理 + * + * @param object $park 駐輪場情報 + * @param array $aggregationTarget 集計対象 + * @param array $fiscalPeriod 財政期間情報 + * @return int 作成された集計レコード数 + */ + private function processFiscalEarningsForPark($park, array $aggregationTarget, array $fiscalPeriod): int + { + try { + // 【処理4】既存の売上集計結果を削除 + $this->deleteExistingFiscalSummary($park->park_id, $aggregationTarget); + + // 【処理3】車種区分毎に算出する + $psections = $this->getPsectionInformation(); + $summaryRecords = 0; + + foreach ($psections as $psection) { + $earningsData = $this->calculateFiscalEarningsForPsection( + $park->park_id, + $psection->psection_id, + $aggregationTarget + ); + + if ($this->hasEarningsData($earningsData)) { + // 売上集計結果を登録 + $this->createFiscalEarningsSummary($park, $psection, $aggregationTarget, $earningsData, $fiscalPeriod); + $summaryRecords++; + } + } + + Log::info('駐輪場財政年度売上集計完了', [ + 'park_id' => $park->park_id, + 'park_name' => $park->park_name, + 'summary_records' => $summaryRecords, + 'fiscal_period' => $fiscalPeriod['target_label'] + ]); + + return $summaryRecords; + + } catch (\Exception $e) { + Log::error('駐輪場財政年度売上集計エラー', [ + 'park_id' => $park->park_id, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + /** + * 車種区分情報取得 + * + * @return array 車種区分情報 + */ + private function getPsectionInformation(): array + { + return DB::table('psection') + ->select(['psection_id', 'psection_subject']) + ->get() + ->toArray(); + } + + /** + * 【処理3】車種区分毎に財政年度売上を算出する + * + * 4つの項目を計算: + * ①売上・件数 + * ②一時金売上 + * ③解約返戻金 + * ④再発行金額・件数 + * + * @param int $parkId 駐輪場ID + * @param int $psectionId 車種区分ID + * @param array $aggregationTarget 集計対象 + * @return array 売上データ + */ + private function calculateFiscalEarningsForPsection(int $parkId, int $psectionId, array $aggregationTarget): array + { + $startDate = $aggregationTarget['start_date']; + $endDate = $aggregationTarget['end_date']; + + // ①売上・件数(billing_amount) + $salesData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as sales_count'), + DB::raw('COALESCE(SUM(billing_amount), 0) as sales_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('contract_flag', 1) + ->whereBetween('contract_payment_day', [$startDate, $endDate]) + ->whereNull('contract_cancelday') + ->first(); + + // ②一時金売上(contract_money) + $temporaryData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as temporary_count'), + DB::raw('COALESCE(SUM(contract_money), 0) as temporary_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('contract_flag', 1) + ->whereBetween('contract_payment_day', [$startDate, $endDate]) + ->whereNotNull('contract_money') + ->where('contract_money', '>', 0) + ->first(); + + // ③解約返戻金(refunds) + $refundData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as refund_count'), + DB::raw('COALESCE(SUM(refunds), 0) as refund_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->whereBetween('contract_cancelday', [$startDate, $endDate]) + ->whereNotNull('refunds') + ->where('refunds', '>', 0) + ->first(); + + // ④再発行金額・件数(seal_reissue_request) + $reissueData = DB::table('regular_contract') + ->select([ + DB::raw('COUNT(*) as reissue_count'), + DB::raw('COALESCE(SUM(contract_seal_issue), 0) as reissue_amount') + ]) + ->where('park_id', $parkId) + ->where('psection_id', $psectionId) + ->where('seal_reissue_request', 1) + ->whereBetween('updated_at', [$startDate, $endDate]) + ->first(); + + return [ + 'sales_count' => $salesData->sales_count ?? 0, + 'sales_amount' => $salesData->sales_amount ?? 0, + 'temporary_count' => $temporaryData->temporary_count ?? 0, + 'temporary_amount' => $temporaryData->temporary_amount ?? 0, + 'refund_count' => $refundData->refund_count ?? 0, + 'refund_amount' => $refundData->refund_amount ?? 0, + 'reissue_count' => $reissueData->reissue_count ?? 0, + 'reissue_amount' => $reissueData->reissue_amount ?? 0 + ]; + } + + /** + * 売上データの存在チェック + * + * @param array $earningsData 売上データ + * @return bool データが存在するかどうか + */ + private function hasEarningsData(array $earningsData): bool + { + return $earningsData['sales_count'] > 0 || + $earningsData['temporary_count'] > 0 || + $earningsData['refund_count'] > 0 || + $earningsData['reissue_count'] > 0; + } + + /** + * 【処理4】既存の財政年度売上集計結果を削除 + * + * @param int $parkId 駐輪場ID + * @param array $aggregationTarget 集計対象 + * @return void + */ + private function deleteExistingFiscalSummary(int $parkId, array $aggregationTarget): void + { + DB::table('earnings_summary') + ->where('park_id', $parkId) + ->where('summary_start_date', $aggregationTarget['start_date']) + ->where('summary_end_date', $aggregationTarget['end_date']) + ->where('summary_type', $aggregationTarget['summary_type']) + ->delete(); + } + + /** + * 財政年度売上集計結果を登録 + * + * @param object $park 駐輪場情報 + * @param object $psection 車種区分情報 + * @param array $aggregationTarget 集計対象 + * @param array $earningsData 売上データ + * @param array $fiscalPeriod 財政期間情報 + * @return void + */ + private function createFiscalEarningsSummary($park, $psection, array $aggregationTarget, array $earningsData, array $fiscalPeriod): void + { + DB::table('earnings_summary')->insert([ + 'park_id' => $park->park_id, + 'summary_type' => $aggregationTarget['summary_type'], // 1:年次, 2:月次 + 'summary_start_date' => $aggregationTarget['start_date'], + 'summary_end_date' => $aggregationTarget['end_date'], + 'earnings_date' => $aggregationTarget['end_date'], // 集計日として終了日を使用 + 'psection_id' => $psection->psection_id, + 'usertype_subject1' => $psection->psection_subject, + 'regular_new_count' => $earningsData['sales_count'], + 'regular_new_amount' => $earningsData['sales_amount'], + 'turnsum' => $earningsData['temporary_amount'], + 'turnsum_count' => $earningsData['temporary_count'], + 'refunds' => $earningsData['refund_amount'], + 'reissue_count' => $earningsData['reissue_count'], + 'reissue_amount' => $earningsData['reissue_amount'], + 'summary_note' => "SHJ-10 {$fiscalPeriod['target_label']} 財政年度売上集計結果", + 'created_at' => now(), + 'updated_at' => now(), + 'operator_id' => 0 // システム処理 + ]); + } + + /** + * 【処理5】オペレータキュー作成 + * + * @param string $message メッセージ + * @param int $batchLogId バッチログID + * @return void + */ + private function createOperatorQueue(string $message, int $batchLogId): void + { + try { + DB::table('operator_que')->insert([ + 'que_class' => 10, // SHJ-10用のクラス + 'user_id' => null, + 'contract_id' => null, + 'park_id' => null, + 'que_comment' => $message, + 'que_status' => 1, // 完了 + 'que_status_comment' => 'バッチ処理完了', + 'work_instructions' => "SHJ-10売上集計処理 BatchLogID: {$batchLogId}", + 'created_at' => now(), + 'updated_at' => now() + ]); + + Log::info('オペレータキュー作成完了', [ + 'batch_log_id' => $batchLogId, + 'message' => $message + ]); + + } catch (\Exception $e) { + Log::error('オペレータキュー作成エラー', [ + 'batch_log_id' => $batchLogId, + 'error' => $e->getMessage() + ]); + } + } + + /** + * 集計種別のラベル取得 + * + * @param string $type 集計種別 + * @return string ラベル + */ + private function getTypeLabel(string $type): string + { + switch ($type) { + case 'yearly': + return '年次'; + case 'monthly': + return '月次'; + default: + return $type; + } + } +} diff --git a/app/Services/UserService.php b/app/Services/UserService.php index e27e7c9..12000fe 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -53,3 +53,5 @@ class UserService } + + diff --git a/app/Support/Csv.php b/app/Support/Csv.php index 422b897..3e4b63e 100644 --- a/app/Support/Csv.php +++ b/app/Support/Csv.php @@ -26,3 +26,5 @@ class Csv } + + diff --git a/app/Support/Files.php b/app/Support/Files.php index 7bcca3e..e1a4ce9 100644 --- a/app/Support/Files.php +++ b/app/Support/Files.php @@ -28,3 +28,5 @@ class Files } + + diff --git a/create_device_table.sql b/create_device_table.sql new file mode 100644 index 0000000..71bbd64 --- /dev/null +++ b/create_device_table.sql @@ -0,0 +1,24 @@ +-- +-- SHJ-6 デバイス管理マスタテーブル +-- デバイス情報を管理するマスタテーブル +-- + +CREATE TABLE `device` ( + `device_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'デバイスID(PK)', + `created_at` datetime DEFAULT NULL COMMENT '登録日時', + `updated_at` datetime DEFAULT NULL COMMENT '更新日時', + `park_id` int(10) UNSIGNED DEFAULT NULL COMMENT '駐輪場ID', + `device_type` varchar(255) DEFAULT NULL COMMENT 'デバイス種別', + `device_subject` varchar(255) DEFAULT NULL COMMENT 'デバイス名', + `device_identifier` varchar(255) DEFAULT NULL COMMENT '識別子(IP/FQDN)', + `device_work` varchar(255) DEFAULT NULL COMMENT '稼働(状態)', + `device_workstart` date DEFAULT NULL COMMENT '稼働開始日', + `device_replace` date DEFAULT NULL COMMENT 'リプレース予定日', + `device_remarks` varchar(255) DEFAULT NULL COMMENT '備考', + `operator_id` int(10) UNSIGNED DEFAULT NULL COMMENT '更新オペレータID', + PRIMARY KEY (`device_id`), + KEY `idx_park_id` (`park_id`), + KEY `idx_device_type` (`device_type`), + KEY `idx_workstart` (`device_workstart`), + KEY `idx_replace` (`device_replace`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='デバイス管理マスタテーブル'; diff --git a/create_earnings_summary_table.sql b/create_earnings_summary_table.sql new file mode 100644 index 0000000..d375344 --- /dev/null +++ b/create_earnings_summary_table.sql @@ -0,0 +1,39 @@ +-- +-- SHJ-9 売上集計結果テーブル +-- 日次売上集計データを保存するテーブル +-- + +CREATE TABLE `earnings_summary` ( + `earnings_summary_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '売上集計ID', + `park_id` int(10) UNSIGNED NOT NULL COMMENT '駐輪場ID', + `summary_type` varchar(255) DEFAULT NULL COMMENT '集計区分', + `summary_start_date` date DEFAULT NULL COMMENT '集計開始日', + `summary_end_date` date DEFAULT NULL COMMENT '集計終了日', + `earnings_date` date DEFAULT NULL COMMENT '売上日', + `psection_id` int(10) UNSIGNED DEFAULT NULL COMMENT '車種区分ID', + `usertype_subject1` varchar(255) DEFAULT NULL COMMENT '規格', + `enable_months` int(10) UNSIGNED DEFAULT NULL COMMENT '期間(月数)', + `regular_new_count` int(10) UNSIGNED DEFAULT 0 COMMENT '期間件数', + `regular_new_amount` decimal(10,2) DEFAULT 0.00 COMMENT '期間金額', + `regular_new_reduction_count` int(10) UNSIGNED DEFAULT 0 COMMENT '期間成免件数', + `regular_new_reduction_amount` decimal(10,2) DEFAULT 0.00 COMMENT '期間成免金額', + `regular_update_count` int(10) UNSIGNED DEFAULT 0 COMMENT '更新件数', + `regular_update_amount` decimal(10,2) DEFAULT 0.00 COMMENT '更新金額', + `regular_update_reduction_count` int(10) UNSIGNED DEFAULT 0 COMMENT '更新成免件数', + `regular_update_reduction_amount` decimal(10,2) DEFAULT 0.00 COMMENT '更新成免金額', + `turnsum_count` int(10) UNSIGNED DEFAULT 0 COMMENT '残金件数', + `turnsum` decimal(10,2) DEFAULT 0.00 COMMENT '残金', + `refunds` decimal(10,2) DEFAULT 0.00 COMMENT '解時返戻金', + `other_income` decimal(10,2) DEFAULT 0.00 COMMENT '分別収入', + `other_spending` decimal(10,2) DEFAULT 0.00 COMMENT '分別支出', + `reissue_count` int(10) UNSIGNED DEFAULT 0 COMMENT '発行件数', + `reissue_amount` decimal(10,2) DEFAULT 0.00 COMMENT '発行金額', + `summary_note` text DEFAULT NULL COMMENT '計備考', + `created_at` datetime DEFAULT NULL COMMENT '登録日時', + `updated_at` datetime DEFAULT NULL COMMENT '更新日時', + `operator_id` int(10) UNSIGNED DEFAULT NULL COMMENT '新法・ページID', + PRIMARY KEY (`earnings_summary_id`), + KEY `idx_park_earnings_date` (`park_id`, `earnings_date`), + KEY `idx_psection` (`psection_id`), + KEY `idx_earnings_date` (`earnings_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='売上集計結果テーブル'; diff --git a/create_hardware_check_log_table.sql b/create_hardware_check_log_table.sql new file mode 100644 index 0000000..55f1ef5 --- /dev/null +++ b/create_hardware_check_log_table.sql @@ -0,0 +1,19 @@ +-- +-- SHJ-6 ハードウェアチェックログテーブル +-- デバイスのハードウェア状態監視ログを保存するテーブル +-- + +CREATE TABLE `hardware_check_log` ( + `log_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ハードウェアチェックログID(PK)', + `device_id` int(10) UNSIGNED NOT NULL COMMENT 'デバイスID', + `status` int(10) UNSIGNED DEFAULT NULL COMMENT 'ステータス', + `status_comment` varchar(255) DEFAULT NULL COMMENT 'ステータスコメント', + `created_at` datetime DEFAULT NULL COMMENT '登録日時', + `updated_at` datetime DEFAULT NULL COMMENT '更新日時', + `operator_id` int(10) UNSIGNED DEFAULT NULL COMMENT 'オペレータID', + PRIMARY KEY (`log_id`), + KEY `idx_device_id` (`device_id`), + KEY `idx_status` (`status`), + KEY `idx_created_at` (`created_at`), + KEY `idx_device_created` (`device_id`, `created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='ハードウェアチェックログテーブル'; diff --git a/create_print_job_log_table.sql b/create_print_job_log_table.sql new file mode 100644 index 0000000..e892e2e --- /dev/null +++ b/create_print_job_log_table.sql @@ -0,0 +1,24 @@ +-- +-- SHJ-6 プリンタジョブログテーブル +-- プリンタ制御プログラムの実行ログを保存するテーブル +-- + +CREATE TABLE `print_job_log` ( + `log_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '処理ログID(PK)', + `park_id` int(10) UNSIGNED DEFAULT NULL COMMENT '駐輪場ID', + `user_id` int(10) UNSIGNED DEFAULT NULL COMMENT 'ユーザーID', + `contract_id` int(10) UNSIGNED DEFAULT NULL COMMENT '契約ID', + `process_name` varchar(255) DEFAULT NULL COMMENT 'プロセス名', + `job_name` varchar(255) DEFAULT NULL COMMENT 'ジョブ名', + `status` varchar(255) DEFAULT NULL COMMENT 'ステータス', + `error_code` int(10) UNSIGNED DEFAULT NULL COMMENT 'エラーコード', + `status_comment` varchar(255) DEFAULT NULL COMMENT 'ステータスコメント', + `created_at` datetime DEFAULT NULL COMMENT '登録日時', + PRIMARY KEY (`log_id`), + KEY `idx_park_id` (`park_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_contract_id` (`contract_id`), + KEY `idx_error_code` (`error_code`), + KEY `idx_created_at` (`created_at`), + KEY `idx_created_error` (`created_at`, `error_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='プリンタジョブログテーブル'; diff --git a/database/migrations/2025_01_17_000000_create_earnings_summary_table.php b/database/migrations/2025_01_17_000000_create_earnings_summary_table.php new file mode 100644 index 0000000..70c1306 --- /dev/null +++ b/database/migrations/2025_01_17_000000_create_earnings_summary_table.php @@ -0,0 +1,62 @@ +id('earnings_summary_id')->comment('売上集計ID'); + $table->unsignedInteger('park_id')->comment('駐輪場ID'); + $table->string('summary_type', 255)->nullable()->comment('集計区分'); + $table->date('summary_start_date')->nullable()->comment('集計開始日'); + $table->date('summary_end_date')->nullable()->comment('集計終了日'); + $table->date('earnings_date')->nullable()->comment('売上日'); + $table->unsignedInteger('psection_id')->nullable()->comment('車種区分ID'); + $table->string('usertype_subject1', 255)->nullable()->comment('規格'); + $table->unsignedInteger('enable_months')->nullable()->comment('期間(月数)'); + $table->unsignedInteger('regular_new_count')->nullable()->comment('期間件数'); + $table->decimal('regular_new_amount', 10, 2)->nullable()->comment('期間金額'); + $table->unsignedInteger('regular_new_reduction_count')->nullable()->comment('期間成免件数'); + $table->decimal('regular_new_reduction_amount', 10, 2)->nullable()->comment('期間成免金額'); + $table->unsignedInteger('regular_update_count')->nullable()->comment('更新件数'); + $table->decimal('regular_update_amount', 10, 2)->nullable()->comment('更新金額'); + $table->unsignedInteger('regular_update_reduction_count')->nullable()->comment('更新成免件数'); + $table->decimal('regular_update_reduction_amount', 10, 2)->nullable()->comment('更新成免金額'); + $table->unsignedInteger('turnsum_count')->nullable()->comment('残金件数'); + $table->decimal('turnsum', 10, 2)->nullable()->comment('残金'); + $table->decimal('refunds', 10, 2)->nullable()->comment('解時返戻金'); + $table->decimal('other_income', 10, 2)->nullable()->comment('分別収入'); + $table->decimal('other_spending', 10, 2)->nullable()->comment('分別支出'); + $table->unsignedInteger('reissue_count')->nullable()->comment('発行件数'); + $table->decimal('reissue_amount', 10, 2)->nullable()->comment('発行金額'); + $table->text('summary_note')->nullable()->comment('計備考'); + $table->datetime('created_at')->nullable()->comment('登録日時'); + $table->datetime('updated_at')->nullable()->comment('更新日時'); + $table->unsignedInteger('operator_id')->nullable()->comment('新法・ページID'); + + // インデックス + $table->index(['park_id', 'earnings_date'], 'idx_park_earnings_date'); + $table->index(['psection_id'], 'idx_psection'); + $table->index(['earnings_date'], 'idx_earnings_date'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('earnings_summary'); + } +}; diff --git a/public/index.php b/public/index.php index 9ef97fa..4ab0d04 100644 --- a/public/index.php +++ b/public/index.php @@ -1,6 +1,6 @@ Date: Fri, 22 Aug 2025 22:27:55 +0900 Subject: [PATCH 14/14] fix index --- public/index.php | 1 - 1 file changed, 1 deletion(-) diff --git a/public/index.php b/public/index.php index 4ab0d04..249163f 100644 --- a/public/index.php +++ b/public/index.php @@ -1,6 +1,5 @@