Batch & API
All checks were successful
Deploy api / deploy (push) Successful in 22s

This commit is contained in:
Your Name 2026-01-16 19:28:13 +09:00
commit 0b4acd7475
130 changed files with 27664 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

65
.env.example Normal file
View File

@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

View File

@ -0,0 +1,18 @@
name: Deploy api
on:
push:
branches: [ "main" ]
workflow_dispatch:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ["native"]
steps:
- uses: actions/checkout@v4
- name: Deploy main to server
run: /usr/local/bin/deploy_api.sh

View File

@ -0,0 +1,27 @@
name: Deploy preview (main_xxx)
on:
push:
branches:
- 'main_*'
workflow_dispatch:
concurrency:
group: deploy-${{ github.ref_name }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ["native"]
steps:
- uses: actions/checkout@v4
- name: Set BRANCH env
shell: bash
run: |
BR="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
echo "BRANCH=$BR" >> "$GITHUB_ENV"
echo "Branch: $BR"
- name: Deploy to preview
run: /usr/local/bin/deploy_api_branch.sh

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Laravel
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
# SHJ-1 specific ignores
*.env.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# SO-Manager API & Batch
Laravel 12 ベースのバッチ処理・API プロジェクトです。
## 概要
本プロジェクトは、SO-Manager システムのバックエンド機能を提供します。
- **バッチ処理**: 定期実行される各種業務処理(決済処理、メール送信など)
- **API**: 外部システム連携用 REST API
## 動作環境
- PHP 8.2+
- Laravel 12
- MySQL 8.0+
## バッチコマンド
```bash
php artisan list shj
```
## API
API 認証には `X-API-Key` ヘッダーが必要です。
詳細は `api.doc/` ディレクトリ内のドキュメントを参照してください。
## ディレクトリ構成
```
app/
├── Console/Commands/ # バッチコマンド
├── Http/Controllers/Api/ # APIコントローラー
├── Models/ # Eloquentモデル
├── Services/ # ビジネスロジック
└── Mail/ # メールクラス
```
## 設定
環境変数は `.env` ファイルで管理します。
```
API_KEYS=your-api-key-here
```

46
app/CommonFunction.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App;
// 共通処理関数クラス
class CommonFunction
{
// 7DSRチェックデジット計算
public static function calc7dsr($number) {
$sum = 0;
$weights = [2, 3, 4, 5, 6, 7];
$digits = str_split(strrev($number));
foreach ($digits as $i => $digit) {
$sum += $digit * $weights[$i % count($weights)];
}
$checkDigit = (10 - ($sum % 10)) % 10;
return $checkDigit;
}
// 初期パスワード作成
public static function createPassword() {
// 使用可能文字 (使用不可1,l,L,i,I,z,Z,2,o,O,0)
$chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz3456789';
$password = '';
$length = 8;
$max = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++) {
$password .= $chars[random_int(0, $max)];
}
return $password;
}
// パスワードハッシュ化
public static function hashPassword($user_seq, $password) {
$hash = hash('sha256', $password) . $user_seq . 'SOMSALT';
for ($i = 0; $i < 25; $i++) {
$hash = hash('sha256', $hash);
}
return $hash;
}
// パスワード照合
public static function verifyPassword($user_seq, $inputPassword, $hashedPassword) {
return self::hashPassword($user_seq, $inputPassword) === $hashedPassword;
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjEightService;
/**
* SHJ-8 バッチ処理ログ登録コマンド
*
* bat_job_logテーブルにバッチ処理の実行ログを登録する
* ShjEightServiceを使用して実装
*/
class ShjBatchLogCommand extends Command
{
/**
* ShjEightService インスタンス
*
* @var ShjEightService
*/
protected $shjEightService;
/**
* コンソールコマンドの名前とシグネチャ
*
* 修正版7項目status_comment追加
* - device_id: デバイスID (必須)
* - status: ステータス (必須)
* - status_comment: ステータスコメント (必須)
* - created_date: 登録日時 (必須、yyyy/mm/dd形式)
* - updated_date: 更新日時 (必須、yyyy/mm/dd形式)
* - process_name: プロセス名 (オプション)
* - job_name: ジョブ名 (オプション)
*
* @var string
*/
protected $signature = 'shj:batch-log
{device_id : デバイスID}
{status : ステータス}
{status_comment : ステータスコメント}
{created_date : 登録日時}
{updated_date : 更新日時}
{process_name? : プロセス名}
{job_name? : ジョブ名}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-8 バッチ処理ログ登録 - bat_job_logテーブルにバッチ処理の実行ログを登録';
/**
* コンストラクタ
*
* @param ShjEightService $shjEightService
*/
public function __construct(ShjEightService $shjEightService)
{
parent::__construct();
$this->shjEightService = $shjEightService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. ShjEightServiceを呼び出して処理を実行
* 2. 処理結果を返却する
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-8 バッチ処理ログ登録を開始します。');
// 引数取得
$deviceId = (int) $this->argument('device_id');
$processName = $this->argument('process_name');
$jobName = $this->argument('job_name');
$status = $this->argument('status');
$statusComment = $this->argument('status_comment');
$createdDate = $this->argument('created_date');
$updatedDate = $this->argument('updated_date');
Log::info('SHJ-8 バッチ処理ログ登録開始', [
'start_time' => $startTime,
'device_id' => $deviceId,
'process_name' => $processName,
'job_name' => $jobName,
'status' => $status,
'status_comment' => $statusComment,
'created_date' => $createdDate,
'updated_date' => $updatedDate
]);
// ShjEightServiceを呼び出して処理を実行
$result = $this->shjEightService->execute(
$deviceId,
$processName,
$jobName,
$status,
$statusComment,
$createdDate,
$updatedDate
);
$endTime = now();
// 処理結果に応じた出力
if ($result['result'] === 0) {
// 正常終了
$this->info('SHJ-8 バッチ処理ログ登録が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
// 標準出力形式 (SHJ-8仕様書準拠)
$this->line('処理結果: 0');
$this->line('異常情報: ');
Log::info('SHJ-8 バッチ処理ログ登録完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime)
]);
return self::SUCCESS;
} else {
// 異常終了
$errorMessage = $result['error_message'] ?? '不明なエラー';
$this->error('SHJ-8 バッチ処理ログ登録エラー: ' . $errorMessage);
// 標準出力形式 (SHJ-8仕様書準拠)
$this->line('処理結果: 1');
$this->line('異常情報: ' . $errorMessage);
Log::error('SHJ-8 バッチ処理ログ登録エラー', [
'error_message' => $errorMessage,
'error_code' => $result['error_code'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-8 バッチ処理ログ登録で予期しないエラーが発生しました: ' . $e->getMessage());
// 標準出力形式 (SHJ-8仕様書準拠 - 例外時)
$this->line('処理結果: 1');
$this->line('異常情報: ' . $e->getMessage());
Log::error('SHJ-8 バッチ処理ログ登録例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,164 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjElevenService;
/**
* SHJ-11 現在契約台数集計コマンド
*
* 集計単位每个の契約台数を算出し、ゾーンマスタとの管理処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjElevenCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数: なし
* オプション: なし
*
* @var string
*/
protected $signature = 'shj:11';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-11 現在契約台数集計 - 集計単位每个契約台数を算出しゾーンマスタ管理を実行';
/**
* SHJ-11サービスクラス
*
* @var ShjElevenService
*/
protected $shjElevenService;
/**
* コンストラクタ
*
* @param ShjElevenService $shjElevenService
*/
public function __construct(ShjElevenService $shjElevenService)
{
parent::__construct();
$this->shjElevenService = $shjElevenService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー(仕様書準拠):
* 【処理1】集計単位每个の契約台数を算出する
* 【判断1】取得件数判定
* - 取得件数 = 0 【処理4】バッチログ作成「全駐輪場契約なし」 終了
* - 取得件数 1 【処理2】ゾーンマスタ処理循環 終了
*
* ※【処理4】は各レコードの処理ごとにService層で呼び出される
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-11 現在契約台数集計を開始します。');
Log::info('SHJ-11 現在契約台数集計開始', [
'start_time' => $startTime
]);
// 【処理1】集計単位每个の契約台数を算出する
$this->info('【処理1】集計単位每个の契約台数を算出しています...');
$contractCounts = $this->shjElevenService->calculateContractCounts();
// 【判断1】取得件数判定
$countResults = count($contractCounts);
$this->info("取得件数: {$countResults}");
if ($countResults === 0) {
// 【判断1】取得件数 = 0 の場合
$this->info('対象なしのため処理を終了します。');
// 【処理4】bat_job_logに直接書き込み取得件数=0時
$batchLogResult = $this->shjElevenService->writeBatJobLogForNoContracts();
// bat_job_log書き込み結果をチェック
if (!$batchLogResult['success']) {
$this->warn('bat_job_log書き込みで異常が発生しました');
if (isset($batchLogResult['error_message'])) {
$this->warn('error_message: ' . $batchLogResult['error_message']);
}
Log::warning('bat_job_log書き込み失敗対象なし', [
'error_message' => $batchLogResult['error_message'] ?? null
]);
}
Log::info('SHJ-11 現在契約台数集計完了(対象なし)', [
'end_time' => now(),
'duration_seconds' => $startTime->diffInSeconds(now()),
'bat_job_log_success' => $batchLogResult['success']
]);
return self::SUCCESS;
}
// 【判断1】取得件数 ≥ 1 の場合
// 【処理2・3・4】ゾーンマスタ処理各レコードごとに処理4を実行
$this->info('【処理2】ゾーンマスタ処理を実行しています...');
$this->info('※各レコードごとに【処理4】バッチログを作成します');
$processResult = $this->shjElevenService->processZoneManagement($contractCounts);
// 処理結果確認
if ($processResult['success']) {
$endTime = now();
$this->info('SHJ-11 現在契約台数集計が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
$this->info("処理対象件数: {$countResults}");
$this->info("ゾーン新規作成件数: {$processResult['created_zones']}");
$this->info("ゾーン更新件数: {$processResult['updated_zones']}");
$this->info("限界台数超過件数: {$processResult['over_capacity_count']}");
if (!empty($processResult['batch_log_errors'])) {
$this->warn("バッチログエラー件数: " . count($processResult['batch_log_errors']) . "");
}
Log::info('SHJ-11 現在契約台数集計完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'processed_count' => $countResults,
'created_zones' => $processResult['created_zones'],
'updated_zones' => $processResult['updated_zones'],
'over_capacity_count' => $processResult['over_capacity_count'],
'batch_log_errors' => count($processResult['batch_log_errors'])
]);
return self::SUCCESS;
} else {
$this->error('SHJ-11 現在契約台数集計でエラーが発生しました: ' . $processResult['message']);
Log::error('SHJ-11 現在契約台数集計エラー', [
'error' => $processResult['message'],
'details' => $processResult['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-11 現在契約台数集計で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-11 現在契約台数集計例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,148 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjFiveService;
/**
* SHJ-5 空き待ち通知処理コマンド
*
* 駐輪場の空き状況を確認し、空き待ち予約者への通知処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjFiveCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数なし - 全ての駐輪場を対象に処理を実行
*
* @var string
*/
protected $signature = 'shj:5';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-5 空き待ち通知処理 - 駐輪場の空き状況確認と空き待ち者への通知を実行';
/**
* SHJ-5サービスクラス
*
* @var ShjFiveService
*/
protected $shjFiveService;
/**
* コンストラクタ
*
* @param ShjFiveService $shjFiveService
*/
public function __construct(ShjFiveService $shjFiveService)
{
parent::__construct();
$this->shjFiveService = $shjFiveService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. バッチログ開始記録
* 2. 駐輪場の空き状況を取得する
* 3. 空き状況判定
* 4. 空き待ち者の情報を取得する
* 5. 取得件数判定
* 6. 空き待ち者への通知、またはオペレーターキュー追加処理
* 7. バッチ処理ログを作成する
* 8. 処理結果返却
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-5 空き待ち通知処理を開始します。');
Log::info('SHJ-5 空き待ち通知処理開始', [
'start_time' => $startTime
]);
// SHJ-5メイン処理実行
$result = $this->shjFiveService->executeParkVacancyNotification();
$endTime = now();
$this->info('SHJ-5 空き待ち通知処理が完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
// 処理結果表示
$this->displayProcessResult($result);
// バッチログはShjFiveServiceが自動的にSHJ-8経由で作成
Log::info('SHJ-5 空き待ち通知処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'result' => $result
]);
return $result['success'] ? self::SUCCESS : self::FAILURE;
} catch (\Exception $e) {
$this->error('SHJ-5 空き待ち通知処理で予期しないエラーが発生しました: ' . $e->getMessage());
// エラー時もShjFiveServiceが自動的にバッチログを作成
Log::error('SHJ-5 空き待ち通知処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
/**
* 処理結果を表示
*
* @param array $result 処理結果
* @return void
*/
private function displayProcessResult(array $result): void
{
$this->line('');
$this->info('=== 処理結果 ===');
if ($result['success']) {
$this->info("✓ 処理実行成功: " . $result['message']);
} else {
$this->error("✗ 処理実行失敗: " . $result['message']);
}
$this->line('');
$this->info('=== 処理統計 ===');
$this->line(" 処理対象駐輪場数: " . ($result['processed_parks_count'] ?? 0));
$this->line(" 空きあり駐輪場数: " . ($result['vacant_parks_count'] ?? 0));
$this->line(" 空き待ち者総数: " . ($result['total_waiting_users'] ?? 0));
$this->line(" 通知送信成功: " . ($result['notification_success_count'] ?? 0) . "");
$this->line(" オペレーターキュー追加: " . ($result['operator_queue_count'] ?? 0) . "");
$this->line(" エラー件数: " . ($result['error_count'] ?? 0) . "");
$this->line(" 処理時間: " . ($result['duration_seconds'] ?? 0) . "");
// エラー詳細があれば表示
if (!empty($result['errors'])) {
$this->line('');
$this->warn('=== エラー詳細 ===');
foreach ($result['errors'] as $index => $error) {
$this->line(" " . ($index + 1) . ". " . $error);
}
}
}
}

View File

@ -0,0 +1,317 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\SettlementTransaction;
use App\Models\BatJobLog;
use App\Models\Device;
use App\Jobs\ProcessSettlementJob;
use App\Services\ShjFourBService;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Carbon\Carbon;
/**
* SHJ-4B チェックコマンド
*
* 未処理の決済トランザクションを検索し、ProcessSettlementJobをディスパッチする兜底処理
*
* 実行方法:
* php artisan shj4b:check
* php artisan shj4b:check --dry-run # 実際の処理は行わず、対象のみ表示
* php artisan shj4b:check --limit=50 # 処理件数制限
*
* Cron設定例10分毎実行:
* 0,10,20,30,40,50 * * * * cd /path/to/project && php artisan shj4b:check > /dev/null 2>&1
*/
class ShjFourBCheckCommand extends Command
{
/**
* コマンド名と説明
*
* @var string
*/
protected $signature = 'shj4b:check
{--dry-run : 実際の処理を行わず対象のみ表示}
{--limit=100 : 処理する最大件数}
{--hours=24 : 指定時間以内の決済のみ対象}';
/**
* コマンドの説明
*
* @var string
*/
protected $description = 'SHJ-4B 兜底チェック - 未処理の決済トランザクションを検索してProcessSettlementJobをディスパッチ';
/**
* SHJ-4B サービス
*
* @var ShjFourBService
*/
protected $shjFourBService;
/**
* コンストラクタ
*/
public function __construct(ShjFourBService $shjFourBService)
{
parent::__construct();
$this->shjFourBService = $shjFourBService;
}
/**
* コマンド実行
*
* @return int
*/
public function handle()
{
$startTime = now();
$isDryRun = $this->option('dry-run');
$limit = (int) $this->option('limit');
$hours = (int) $this->option('hours');
$this->info("SHJ-4B チェックコマンド開始");
$this->info("実行モード: " . ($isDryRun ? "ドライラン(実際の処理なし)" : "本実行"));
$this->info("処理制限: {$limit}");
$this->info("対象期間: {$hours}時間以内");
try {
// 未処理の決済トランザクション取得
$unprocessedSettlements = $this->getUnprocessedSettlements($hours, $limit);
$this->info("未処理決済トランザクション: " . $unprocessedSettlements->count() . "");
if ($unprocessedSettlements->isEmpty()) {
$this->info("処理対象なし");
// バッチログ作成 - 処理対象なし
$this->createBatchLog('success', 0, 0, 'SHJ-4B チェック完了 - 処理対象なし');
return 0;
}
// 対象一覧表示
$this->displayTargets($unprocessedSettlements);
if ($isDryRun) {
$this->info("ドライランモードのため、実際の処理はスキップします");
// バッチログ作成 - ドライラン
$this->createBatchLog('success', 0, 0, 'SHJ-4B チェック完了 - ドライラン(' . $unprocessedSettlements->count() . '件検出)');
return 0;
}
// 実際の処理実行
$processed = $this->processSettlements($unprocessedSettlements);
$this->info("処理完了: {$processed['success']}件成功, {$processed['failed']}件失敗");
// バッチログ作成 - 処理完了
$status = $processed['failed'] > 0 ? 'error' : 'success';
$message = "SHJ-4B チェック完了 - 成功:{$processed['success']}件, 失敗:{$processed['failed']}";
$this->createBatchLog($status, $processed['success'], $processed['failed'], $message);
return $processed['failed'] > 0 ? 1 : 0;
} catch (\Throwable $e) {
$this->error("SHJ-4B チェック処理でエラーが発生しました: " . $e->getMessage());
Log::error('SHJ-4B チェックコマンドエラー', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// バッチログ作成 - エラー
$this->createBatchLog('error', 0, 1, 'SHJ-4B チェック失敗: ' . $e->getMessage());
return 1;
}
}
/**
* 未処理の決済トランザクション取得
*
* @param int $hours
* @param int $limit
* @return \Illuminate\Database\Eloquent\Collection
*/
private function getUnprocessedSettlements(int $hours, int $limit)
{
$cutoffTime = Carbon::now()->subHours($hours);
// 条件:
// 1. 指定時間以内に作成された
// 2. contract_payment_numberがnullでない
// 3. まだregular_contractのsettlement_transaction_idに関連付けられていない
// 4. ProcessSettlementJobが実行されていないbatch_logで確認
$query = SettlementTransaction::where('created_at', '>=', $cutoffTime)
->whereNotNull('contract_payment_number')
->whereNotNull('pay_date')
->whereNotNull('settlement_amount')
->orderBy('created_at', 'asc');
$settlements = $query->limit($limit)->get();
// 既に処理済みのものを除外
$unprocessed = $settlements->filter(function ($settlement) {
return !$this->isAlreadyProcessed($settlement);
});
return $unprocessed;
}
/**
* 既に処理済みかチェック
*
* @param SettlementTransaction $settlement
* @return bool
*/
private function isAlreadyProcessed(SettlementTransaction $settlement): bool
{
// 1. regular_contractの同一contract_payment_numberが既に処理済みかチェック
$linkedContract = DB::table('regular_contract')
->where('contract_payment_number', $settlement->contract_payment_number)
->whereNotNull('contract_payment_day')
->exists();
if ($linkedContract) {
return true;
}
// 2. bat_job_logで処理完了記録があるかチェック
$processedInBatch = BatJobLog::where('process_name', 'SHJ-4B')
->where('status', 'success')
->where('status_comment', 'like', '%settlement_transaction_id:' . $settlement->settlement_transaction_id . '%')
->exists();
if ($processedInBatch) {
return true;
}
// 3. 現在キューに入っているかチェック(簡易版)
// 注: より正確にはRedis/DBキューの内容を確認する必要がある
$recentJobDispatched = BatJobLog::where('process_name', 'SHJ-4B')
->where('status_comment', 'like', '%settlement_transaction_id:' . $settlement->settlement_transaction_id . '%')
->where('created_at', '>=', Carbon::now()->subHours(1))
->exists();
return $recentJobDispatched;
}
/**
* 対象一覧表示
*
* @param \Illuminate\Database\Eloquent\Collection $settlements
*/
private function displayTargets($settlements)
{
$this->info("対象の決済トランザクション:");
$this->table(
['ID', '契約支払番号', '決済金額', '支払日', '作成日時'],
$settlements->map(function ($settlement) {
return [
$settlement->settlement_transaction_id,
$settlement->contract_payment_number,
number_format($settlement->settlement_amount) . '円',
Carbon::parse($settlement->pay_date)->format('Y-m-d H:i:s'),
$settlement->created_at->format('Y-m-d H:i:s'),
];
})->toArray()
);
}
/**
* 決済処理実行
*
* @param \Illuminate\Database\Eloquent\Collection $settlements
* @return array
*/
private function processSettlements($settlements): array
{
$success = 0;
$failed = 0;
$results = [];
foreach ($settlements as $settlement) {
try {
$this->info("処理中: 決済トランザクションID {$settlement->settlement_transaction_id}");
// ProcessSettlementJobをディスパッチ
ProcessSettlementJob::dispatch(
$settlement->settlement_transaction_id,
[
'source' => 'shj4b_check_command',
'triggered_at' => now()->toISOString(),
]
);
$success++;
$results[] = [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'status' => 'dispatched',
'message' => 'ProcessSettlementJobディスパッチ成功',
];
$this->info("✓ 成功: {$settlement->settlement_transaction_id}");
} catch (\Throwable $e) {
$failed++;
$results[] = [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'status' => 'failed',
'error' => $e->getMessage(),
];
$this->error("✗ 失敗: {$settlement->settlement_transaction_id} - {$e->getMessage()}");
Log::error('SHJ-4B チェック 個別処理失敗', [
'settlement_transaction_id' => $settlement->settlement_transaction_id,
'error' => $e->getMessage(),
]);
}
}
return [
'success' => $success,
'failed' => $failed,
'results' => $results,
'total' => $settlements->count(),
];
}
/**
* バッチログ作成 - 直接bat_job_log書き込み
*
* @param string $status
* @param int $successCount
* @param int $errorCount
* @param string $message
*/
private function createBatchLog(string $status, int $successCount, int $errorCount, string $message): void
{
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
BatJobLog::create([
'device_id' => $deviceId,
'process_name' => 'SHJ-4B-CHECK',
'job_name' => 'SHJ-4Bチェックコマンド',
'status' => $status,
'status_comment' => $message . " (成功:{$successCount}/失敗:{$errorCount})",
'created_at' => now()->startOfDay(),
'updated_at' => now()->startOfDay()
]);
} catch (\Exception $e) {
Log::error('SHJ-4B-CHECK バッチログ作成エラー', [
'error' => $e->getMessage(),
'status' => $status,
'message' => $message
]);
}
}
}

View File

@ -0,0 +1,157 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjFourCService;
/**
* SHJ-4C 室割当処理コマンド
*
* 駐輪場の区画別利用率状況に基づく室割当処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjFourCCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - park_id: 駐輪場ID (必須)
* - ptype_id: 駐輪分類ID (必須)
* - psection_id: 車種区分ID (必須)
*
* @var string
*/
protected $signature = 'shj:4c {park_id : 駐輪場ID} {ptype_id : 駐輪分類ID} {psection_id : 車種区分ID}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-4C 室割当処理 - ゾーン情報取得及び割当処理を実行';
/**
* SHJ-4Cサービスクラス
*
* @var ShjFourCService
*/
protected $shjFourCService;
/**
* コンストラクタ
*
* @param ShjFourCService $shjFourCService
*/
public function __construct(ShjFourCService $shjFourCService)
{
parent::__construct();
$this->shjFourCService = $shjFourCService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. パラメータ取得と検証
* 2. ゾーン情報取得処理
* 3. 割当判定処理
* 4. バッチログ作成
* 5. 処理結果返却
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-4C 室割当処理を開始します。');
Log::info('SHJ-4C 室割当処理開始', [
'start_time' => $startTime,
'park_id' => $this->argument('park_id'),
'ptype_id' => $this->argument('ptype_id'),
'psection_id' => $this->argument('psection_id')
]);
// 引数取得
$parkId = $this->argument('park_id');
$ptypeId = $this->argument('ptype_id');
$psectionId = $this->argument('psection_id');
// パラメータ検証
if (!$this->validateParameters($parkId, $ptypeId, $psectionId)) {
$this->error('パラメータが不正です。');
return self::FAILURE;
}
// SHJ-4C処理実行
$result = $this->shjFourCService->executeRoomAllocation($parkId, $ptypeId, $psectionId);
// 処理結果確認
if ($result['success']) {
$endTime = now();
$this->info('SHJ-4C 室割当処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
Log::info('SHJ-4C 室割当処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'result' => $result
]);
return self::SUCCESS;
} else {
$this->error('SHJ-4C 室割当処理でエラーが発生しました: ' . $result['message']);
Log::error('SHJ-4C 室割当処理エラー', [
'error' => $result['message'],
'details' => $result['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-4C 室割当処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-4C 室割当処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
/**
* パラメータの妥当性を検証
*
* @param mixed $parkId 駐輪場ID
* @param mixed $ptypeId 駐輪分類ID
* @param mixed $psectionId 車種区分ID
* @return bool 検証結果
*/
private function validateParameters($parkId, $ptypeId, $psectionId): bool
{
// 必須パラメータチェック
if (empty($parkId) || empty($ptypeId) || empty($psectionId)) {
$this->error('全てのパラメータは必須です。');
return false;
}
// 数値形式チェック
if (!is_numeric($parkId) || !is_numeric($ptypeId) || !is_numeric($psectionId)) {
$this->error('全てのパラメータは数値である必要があります。');
return false;
}
// 正の整数チェック
if ($parkId <= 0 || $ptypeId <= 0 || $psectionId <= 0) {
$this->error('全てのパラメータは正の整数である必要があります。');
return false;
}
return true;
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjMailSendService;
/**
* SHJ メール送信処理コマンド
*
* メールテンプレートを使用したメール送信処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjMailSendCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - mail_address: メールアドレス (必須)
* - backup_mail_address: 予備メールアドレス (必須)
* - mail_template_id: メールテンプレートID (必須)
*
* @var string
*/
protected $signature = 'shj:mail-send {mail_address : メールアドレス} {backup_mail_address : 予備メールアドレス} {mail_template_id : メールテンプレートID}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ メール送信処理 - テンプレートに基づくメール送信を実行';
/**
* SHJメール送信サービスクラス
*
* @var ShjMailSendService
*/
protected $shjMailSendService;
/**
* コンストラクタ
*
* @param ShjMailSendService $shjMailSendService
*/
public function __construct(ShjMailSendService $shjMailSendService)
{
parent::__construct();
$this->shjMailSendService = $shjMailSendService;
}
/**
* コンソールコマンドを実行
*
* SHJ-7 メール送信処理フロー:
* 【処理1】入力パラメーターをチェックする
* 【判断1】チェック結果
* 【処理2】メール送信テンプレート情報を取得する
* 【判断2】取得結果
* 【処理3】メールを送信する
* 【処理4】処理結果を返却する
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-7 メール送信処理を開始します。');
Log::info('SHJ-7 メール送信処理開始', [
'start_time' => $startTime,
'mail_address' => $this->argument('mail_address'),
'backup_mail_address' => $this->argument('backup_mail_address'),
'mail_template_id' => $this->argument('mail_template_id')
]);
// 引数取得
$mailAddress = $this->argument('mail_address');
$backupMailAddress = $this->argument('backup_mail_address');
$mailTemplateId = $this->argument('mail_template_id');
// SHJ-7 メール送信処理実行
$result = $this->shjMailSendService->executeMailSend($mailAddress, $backupMailAddress, $mailTemplateId);
// 【処理4】処理結果を返却する
if ($result['result'] === 0) {
$endTime = now();
$this->info('SHJ-7 メール送信処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
Log::info('SHJ-7 メール送信処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'result' => $result['result'],
'error_info' => $result['error_info']
]);
return self::SUCCESS;
} else {
$this->error('SHJ-7 メール送信処理でエラーが発生しました: ' . $result['error_info']);
Log::error('SHJ-7 メール送信処理エラー', [
'result' => $result['result'],
'error_info' => $result['error_info']
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-7 メール送信処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-7 メール送信処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjNineService;
/**
* SHJ-9 売上集計処理コマンド
*
* 駐輪場の売上データを日次で集計する処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjNineCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - type: 集計種別 (daily のみ) (必須)
* - target_date: 集計対象日 (オプション、YYYY-MM-DD形式)
*
* @var string
*/
protected $signature = 'shj:9 {type : 集計種別(daily)} {target_date? : 集計対象日(YYYY-MM-DD)}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-9 売上集計処理 - 日次売上データ集計を実行';
/**
* SHJ-9サービスクラス
*
* @var ShjNineService
*/
protected $shjNineService;
/**
* コンストラクタ
*
* @param ShjNineService $shjNineService
*/
public function __construct(ShjNineService $shjNineService)
{
parent::__construct();
$this->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
{
// 集計種別チェックSHJ-9 は日次のみ対応)
if ($type !== 'daily') {
$this->error('SHJ-9 は日次集計dailyのみ対応しています。月次/年次は SHJ-10 を使用してください。');
return false;
}
// 対象日形式チェック(指定されている場合)
if ($targetDate && !$this->isValidDateFormat($targetDate)) {
$this->error('対象日の形式が正しくありませんYYYY-MM-DD形式で指定してください。');
return false;
}
return true;
}
/**
* 集計対象日を決定
*
* @param string $type 集計種別daily 固定)
* @param string|null $targetDate 指定日
* @return string 集計対象日
*/
private function determineAggregationDate(string $type, ?string $targetDate): string
{
if ($targetDate) {
return $targetDate;
}
// パラメータ指定がない場合は昨日本日の1日前
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]);
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Console\Commands;
use App\Services\ShjOneService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Exception;
/**
* SHJ-1 本人確認自動処理 Command
*
* 実行コマンド: php artisan shj:one {user_id} {park_id}
*/
class ShjOneCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'shj:one {user_id : 利用者連番} {park_id : 駐輪場ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'SHJ-1 本人確認自動処理 - 利用者の本人確認写真と登録情報の照合を行い自動判定する';
protected $shjOneService;
public function __construct(ShjOneService $shjOneService)
{
parent::__construct();
$this->shjOneService = $shjOneService;
}
/**
* Execute the console command.
*/
public function handle()
{
$startTime = now();
$processName = config('shj1.batch_log.process_name');
// 【処理0】パラメーターを取得する
$userId = $this->argument('user_id');
$parkId = $this->argument('park_id');
$parameters = [
'user_id' => $userId,
'park_id' => $parkId
];
$this->info("=== SHJ-1 本人確認自動処理 開始 ===");
$this->info("利用者ID: {$userId}");
$this->info("駐輪場ID: {$parkId}");
try {
// SHJ-1 メイン処理を実行
$result = $this->shjOneService->execute($userId, $parkId);
// 処理結果の表示
$this->displayResult($result);
// バッチログはShjOneServiceが自動的にSHJ-8経由で作成
$this->info("=== SHJ-1 本人確認自動処理 完了 ===");
// システムエラーまたは身元確認失敗の場合は exit code 1
$isSuccess = $result['system_success'] && ($result['identity_result'] ?? '') === 'OK';
return $isSuccess ? 0 : 1;
} catch (Exception $e) {
$this->error("SHJ-1処理中にエラーが発生しました: " . $e->getMessage());
// エラー時もShjOneServiceが自動的にバッチログを作成
Log::error('SHJ-1処理例外エラー', [
'user_id' => $userId,
'park_id' => $parkId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return 1;
}
}
/**
* 処理結果を表示
*/
private function displayResult(array $result): void
{
$this->line('');
$this->info('=== 処理結果 ===');
if ($result['system_success']) {
$this->info("✓ 処理実行成功: " . $result['message']);
if (isset($result['identity_result'])) {
$identityStatus = $result['identity_result'] === 'OK' ? '✓ 本人確認OK' : '✗ 本人確認NG';
$this->line(" 本人確認結果: " . $identityStatus);
}
} else {
$this->error("✗ 処理実行失敗: " . $result['message']);
}
if (isset($result['details'])) {
foreach ($result['details'] as $key => $value) {
$this->line(" {$key}: {$value}");
}
}
if (isset($result['stats'])) {
$this->line('');
$this->info('=== 統計情報 ===');
foreach ($result['stats'] as $key => $value) {
$this->line(" {$key}: {$value}");
}
}
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjSixService;
/**
* SHJ-6 サーバ死活監視処理コマンド
*
* サーバとデバイスの死活監視を行い、異常時にはメール通知を実行する
* 定期実行またはオンデマンド実行のバックグラウンドバッチ処理
*/
class ShjSixCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* パラメータなしで実行
*
* @var string
*/
protected $signature = 'shj:6';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-6 サーバ死活監視処理 - サーバ・デバイス監視とアラート通知を実行';
/**
* SHJ-6サービスクラス
*
* @var ShjSixService
*/
protected $shjSixService;
/**
* コンストラクタ
*
* @param ShjSixService $shjSixService
*/
public function __construct(ShjSixService $shjSixService)
{
parent::__construct();
$this->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;
}
}
}

View File

@ -0,0 +1,292 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjTenService;
/**
* SHJ-10 売上集計処理コマンド
*
* 駐輪場の売上データを財政年度ベースで年次・月次集計する処理を実行する
* 4月開始の財政年度期間で計算するバックグラウンドバッチ処理
*/
class ShjTenCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - type: 集計種別 (yearly/monthly) (必須)
* - target: 集計対象 (任意)
* - yearly: 年度 (: 2019)、指定なしの場合は前期
* - monthly: 年月 (: 2019/01)、指定なしの場合は先月
*
* @var string
*/
protected $signature = 'shj:10 {type : 集計種別(yearly/monthly)} {target? : 集計対象(yearly:年度, monthly:年月)、省略時は前期/先月}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-10 売上集計処理 - 財政年度ベース年次/月次売上データ集計を実行';
/**
* SHJ-10サービスクラス
*
* @var ShjTenService
*/
protected $shjTenService;
/**
* コンストラクタ
*
* @param ShjTenService $shjTenService
*/
public function __construct(ShjTenService $shjTenService)
{
parent::__construct();
$this->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');
// パラメータ指定なしの場合、デフォルト値を設定
if (empty($target)) {
$target = $this->getDefaultTarget($type);
$this->info("パラメーター指定なし:デフォルト値 {$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;
}
}
/**
* パラメーター指定なしの場合のデフォルト値を取得
*
* 仕様書:
* - 年次の場合前期財政年度4月開始
* - 月次の場合本日の1ヶ月前
*
* @param string $type 集計種別
* @return string デフォルトの集計対象
*/
private function getDefaultTarget(string $type): string
{
$fiscalStartMonth = 4; // 財政年度開始月4月
if ($type === 'yearly') {
// 前期を計算
// 現在の財政年度を求め、その前の年度を返す
$now = now();
$currentYear = (int)$now->format('Y');
$currentMonth = (int)$now->format('m');
// 現在の財政年度を計算4月以降なら今年、3月以前なら去年
$currentFiscalYear = $currentMonth >= $fiscalStartMonth ? $currentYear : $currentYear - 1;
// 前期 = 現在の財政年度 - 1
$previousFiscalYear = $currentFiscalYear - 1;
return (string)$previousFiscalYear;
} elseif ($type === 'monthly') {
// 本日の1ヶ月前
$lastMonth = now()->subMonth();
return $lastMonth->format('Y/m');
}
return '';
}
/**
* パラメータの妥当性を検証
*
* @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日
*
* 仕様書:
* - yearly: 売上日付 = パラメーター.集計対象年(期) 年度の1月1日で表現
* - monthly: 売上日付 = パラメーター.集計対象月 該当月の1日で表現
*
* @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))));
// 売上日付: 仕様書では年度(例:2019)、DBはDATE型なので年度の1月1日で表現
$earningsDate = sprintf('%04d-01-01', $year);
return [
'type' => 'yearly',
'fiscal_year' => $year,
'start_date' => $startDate,
'end_date' => $endDate,
'earnings_date' => $earningsDate,
'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;
// 売上日付: 仕様書では年月(例:2019/01)、DBはDATE型なので該当月の1日で表現
$earningsDate = sprintf('%04d-%02d-01', $year, $month);
return [
'type' => 'monthly',
'fiscal_year' => $fiscalYear,
'target_year' => $year,
'target_month' => $month,
'start_date' => $startDate,
'end_date' => $endDate,
'earnings_date' => $earningsDate,
'summary_type' => 2, // 月次
'target_label' => "{$year}{$month}"
];
}
throw new \InvalidArgumentException("不正な集計種別: {$type}");
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjThirteenService;
/**
* SHJ-13 契約台数追加処理コマンド
*
* 指定されたパラメータで契約台数をpark_number・zoneテーブルに反映する
* 主にSHJ-4Bから呼び出されるが、独立実行も可能
*/
class ShjThirteenCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - park_id: 駐輪場ID (必須)
* - psection_id: 車種区分ID (必須)
* - ptype_id: 駐輪分類ID (必須)
* - zone_id: ゾーンID (必須)
*
* オプション:
* - contract_id: 契約ID (任意、ログ用)
*
* @var string
*/
protected $signature = 'shj:13
{park_id : 駐輪場ID}
{psection_id : 車種区分ID}
{ptype_id : 駐輪分類ID}
{zone_id : ゾーンID}
{--contract_id= : 契約IDログ用、任意}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-13 契約台数追加処理 - park_number・zoneテーブルの契約台数を+1更新しログ記録';
/**
* SHJ-13サービスクラス
*
* @var ShjThirteenService
*/
protected $shjThirteenService;
/**
* コンストラクタ
*
* @param ShjThirteenService $shjThirteenService
*/
public function __construct(ShjThirteenService $shjThirteenService)
{
parent::__construct();
$this->shjThirteenService = $shjThirteenService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. 引数取得・検証
* 2. ShjThirteenService実行
* 3. 結果表示
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-13 契約台数追加処理を開始します。');
// 引数取得
$contractData = [
'park_id' => (int) $this->argument('park_id'),
'psection_id' => (int) $this->argument('psection_id'),
'ptype_id' => (int) $this->argument('ptype_id'),
'zone_id' => (int) $this->argument('zone_id'),
'contract_id' => $this->option('contract_id'),
];
Log::info('SHJ-13 契約台数追加処理開始', [
'start_time' => $startTime,
'contract_data' => $contractData,
]);
$this->info("駐輪場ID: {$contractData['park_id']}");
$this->info("車種区分ID: {$contractData['psection_id']}");
$this->info("駐輪分類ID: {$contractData['ptype_id']}");
$this->info("ゾーンID: {$contractData['zone_id']}");
if ($contractData['contract_id']) {
$this->info("契約ID: {$contractData['contract_id']}");
}
// SHJ-13処理実行
$this->info('【処理実行】契約台数追加処理を実行しています...');
$result = $this->shjThirteenService->execute($contractData);
// 処理結果確認・表示
if ($result['result'] === 0) {
$endTime = now();
$this->info('SHJ-13 契約台数追加処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
Log::info('SHJ-13 契約台数追加処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'contract_data' => $contractData,
]);
// 成功時の結果出力
$this->line('処理結果: 0'); // 0 = 正常終了
$this->line('異常情報: '); // 正常時は空文字
return self::SUCCESS;
} else {
$this->error('SHJ-13 契約台数追加処理でエラーが発生しました。');
$this->error("エラーコード: {$result['error_code']}");
$this->error("エラーメッセージ: {$result['error_message']}");
Log::error('SHJ-13 契約台数追加処理エラー', [
'contract_data' => $contractData,
'error_result' => $result,
]);
// エラー時の結果出力
$this->line('処理結果: 1'); // 1 = 異常終了
$this->line('異常情報: ' . $result['error_message']);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-13 契約台数追加処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-13 契約台数追加処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// 例外時の結果出力
$this->line('処理結果: 1'); // 1 = 異常終了
$this->line('異常情報: システムエラー: ' . $e->getMessage());
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjThreeService;
/**
* SHJ-3 定期更新リマインダー処理コマンド
*
* 駐輪場の定期契約更新対象者に対するリマインダー処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjThreeCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数なし - 全ての駐輪場を対象に処理を実行
*
* @var string
*/
protected $signature = 'shj:3';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-3 定期更新リマインダー処理 - 定期契約更新対象者へのリマインダー送信を実行';
/**
* SHJ-3サービスクラス
*
* @var ShjThreeService
*/
protected $shjThreeService;
/**
* コンストラクタ
*
* @param ShjThreeService $shjThreeService
*/
public function __construct(ShjThreeService $shjThreeService)
{
parent::__construct();
$this->shjThreeService = $shjThreeService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. 駐輪場マスタ情報取得
* 2. 各駐輪場の実行タイミングチェック
* 3. 定期更新対象者取得とリマインダー送信
* 4. バッチログ作成
* 5. 処理結果返却
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-3 定期更新リマインダー処理を開始します。');
Log::info('SHJ-3 定期更新リマインダー処理開始', [
'start_time' => $startTime
]);
// SHJ-3メイン処理実行
$result = $this->shjThreeService->executeReminderProcess();
$endTime = now();
$this->info('SHJ-3 定期更新リマインダー処理が完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
// 処理結果表示
$this->line('=== 処理結果 ===');
$this->line("対象駐輪場数: {$result['processed_parks_count']}");
$this->line("対象者総数: {$result['total_target_users']}");
$this->line("メール送信成功: {$result['mail_success_count']}");
$this->line("メール送信失敗: {$result['mail_error_count']}");
$this->line("オペレーターキュー追加: {$result['operator_queue_count']}");
Log::info('SHJ-3 定期更新リマインダー処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'result' => $result
]);
return $result['success'] ? self::SUCCESS : self::FAILURE;
} catch (\Exception $e) {
$this->error('SHJ-3 定期更新リマインダー処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-3 定期更新リマインダー処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,217 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjTwelveService;
/**
* SHJ-12 未払い者通知処理コマンド
*
* 定期契約マスタより未払い者を取得し、通知処理またはオペレーターキュー追加を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjTwelveCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数: なし
* オプション: なし
*
* @var string
*/
protected $signature = 'shj:12';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-12 未払い者通知処理 - 定期契約マスタより未払い者を取得し通知処理を実行';
/**
* SHJ-12サービスクラス
*
* @var ShjTwelveService
*/
protected $shjTwelveService;
/**
* コンストラクタ
*
* @param ShjTwelveService $shjTwelveService
*/
public function __construct(ShjTwelveService $shjTwelveService)
{
parent::__construct();
$this->shjTwelveService = $shjTwelveService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. 定期契約マスタより未払い者を取得する
* 2. 取得件数判定
* 3. 未払い者への通知、またはオペレーターキュー追加処理
* 4. バッチ処理ログを作成する
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-12 未払い者通知処理を開始します。');
Log::info('SHJ-12 未払い者通知処理開始', [
'start_time' => $startTime
]);
// 【処理1】定期契約マスタより未払い者を取得する
$this->info('【処理1】定期契約マスタより未払い者を取得しています...');
$unpaidUsers = $this->shjTwelveService->getUnpaidUsers();
// 【判断1】取得件数判定
$unpaidCount = count($unpaidUsers);
$this->info("取得件数: {$unpaidCount}");
if ($unpaidCount === 0) {
// 未払い者なしの結果を設定する
$this->info('未払い者なしのため処理を終了します。');
// バッチ処理ログを作成SHJ-8仕様に合わせ job_name と status_comment を追加)
$this->shjTwelveService->createBatchLog(
'success',
[
'job_name' => 'SHJ-12未払い者アラート',
'status_comment' => '未払い者なし'
],
'未払い者なし',
0,
0,
0
);
Log::info('SHJ-12 未払い者通知処理完了(対象なし)', [
'end_time' => now(),
'duration_seconds' => $startTime->diffInSeconds(now())
]);
return self::SUCCESS;
}
// 【処理2】未払い者への通知、またはオペレーターキュー追加処理
$this->info('【処理2】未払い者への通知処理を実行しています...');
$processResult = $this->shjTwelveService->processUnpaidUserNotifications($unpaidUsers);
// 処理結果確認
if ($processResult['success']) {
$endTime = now();
$this->info('SHJ-12 未払い者通知処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
$this->info("処理対象件数: {$unpaidCount}");
$this->info("メール送信成功: {$processResult['mail_success_count']}");
$this->info("メール送信失敗: {$processResult['mail_error_count']}");
$this->info("キュー登録成功: {$processResult['queue_success_count']}");
$this->info("キュー登録失敗: {$processResult['queue_error_count']}");
// 【処理3】バッチ処理ログを作成する
$statusComment = $processResult['status_comment'];
$batchComments = $processResult['batch_comments'];
$totalSuccessCount = $processResult['mail_success_count'] + $processResult['queue_success_count'];
$totalErrorCount = $processResult['mail_error_count'] + $processResult['queue_error_count'];
// status_commentに詳細エラー情報を付加
$finalMessage = $statusComment;
if (!empty($batchComments)) {
$finalMessage .= "\n" . $batchComments;
}
// SHJ-8仕様に合わせ、job_name と status_comment を parameters に追加
$batchLogParameters = array_merge($processResult['parameters'], [
'job_name' => 'SHJ-12未払い者アラート',
'status_comment' => $statusComment
]);
$this->shjTwelveService->createBatchLog(
'success',
$batchLogParameters,
$finalMessage,
$unpaidCount,
$totalSuccessCount,
$totalErrorCount
);
Log::info('SHJ-12 未払い者通知処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'processed_count' => $unpaidCount,
'mail_success_count' => $processResult['mail_success_count'],
'mail_error_count' => $processResult['mail_error_count'],
'queue_success_count' => $processResult['queue_success_count'],
'queue_error_count' => $processResult['queue_error_count']
]);
return self::SUCCESS;
} else {
$this->error('SHJ-12 未払い者通知処理でエラーが発生しました: ' . $processResult['message']);
// エラー時のバッチログ作成
$statusComment = $processResult['status_comment'] ?? 'システムエラー';
$batchComments = $processResult['batch_comments'] ?? '';
$finalMessage = $statusComment;
if (!empty($batchComments)) {
$finalMessage .= "\n" . $batchComments;
}
// SHJ-8仕様に合わせ、job_name と status_comment を parameters に追加
$batchLogParameters = array_merge($processResult['parameters'] ?? [], [
'job_name' => 'SHJ-12未払い者アラート',
'status_comment' => $statusComment
]);
$this->shjTwelveService->createBatchLog(
'error',
$batchLogParameters,
$finalMessage,
$unpaidCount,
($processResult['mail_success_count'] ?? 0) + ($processResult['queue_success_count'] ?? 0),
($processResult['mail_error_count'] ?? 0) + ($processResult['queue_error_count'] ?? 0)
);
Log::error('SHJ-12 未払い者通知処理エラー', [
'error' => $processResult['message'],
'details' => $processResult['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-12 未払い者通知処理で予期しないエラーが発生しました: ' . $e->getMessage());
// 例外時のバッチログ作成SHJ-8仕様に合わせ job_name と status_comment を追加)
$this->shjTwelveService->createBatchLog(
'error',
[
'job_name' => 'SHJ-12未払い者アラート',
'status_comment' => 'システムエラー'
],
'システムエラー: ' . $e->getMessage(),
0,
0,
1
);
Log::error('SHJ-12 未払い者通知処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjMailSendService;
/**
* メール送信テストコマンド
*
* SHJ-7メール送信機能をテストするための便利なコマンド
* 開発・検証環境で使用することを想定
*/
class TestMailCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* @var string
*/
protected $signature = 'test:mail
{email? : 送信先メールアドレス(省略時: wyf_0506@hotmail.com}
{--template=205 : テンプレートIDデフォルト: 205=SHJ-4B用}
{--backup= : 予備メールアドレス}
{--laravel : Laravel Mail を使用(デフォルト: mb_send_mail}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-7メール送信機能のテストコマンド - 指定したメールアドレスにテストメールを送信';
/**
* SHJメール送信サービス
*
* @var ShjMailSendService
*/
protected $mailSendService;
/**
* コンストラクタ
*
* @param ShjMailSendService $mailSendService
*/
public function __construct(ShjMailSendService $mailSendService)
{
parent::__construct();
$this->mailSendService = $mailSendService;
}
/**
* コマンド実行
*
* @return int
*/
public function handle()
{
// パラメータ取得
$email = $this->argument('email') ?? 'wyf_0506@hotmail.com';
$backupEmail = $this->option('backup') ?? '';
$templateId = (int) $this->option('template');
$useLaravel = $this->option('laravel');
// 確認画面
$this->info('=== SHJ-7 メール送信テスト ===');
$this->line('');
$this->line("送信先メールアドレス: {$email}");
$this->line("予備メールアドレス : " . ($backupEmail ?: '(なし)'));
$this->line("テンプレートID : {$templateId}");
$this->line("送信方式 : " . ($useLaravel ? 'Laravel Mail' : 'mb_send_mail'));
$this->line('');
// 環境チェック
$mailDriver = config('mail.default');
$this->warn("現在のメール設定: MAIL_MAILER={$mailDriver}");
if ($useLaravel) {
$this->info("✓ Laravel Mail を使用します(.envの設定に従います");
if ($mailDriver === 'log') {
$this->warn('⚠ メールはログファイルにのみ記録され、実際には送信されません');
$this->warn('⚠ 実際に送信するには .env で MAIL_MAILER=smtp 等に変更してください');
} else {
$this->info("✓ メールは実際に送信されます({$mailDriver}");
}
} else {
$this->warn('⚠ mb_send_mail() を使用しますphp.iniのSMTP設定が必要');
$this->comment(' Laravel Mail を使用する場合は --laravel オプションを追加してください');
}
$this->line('');
// 実行確認
if (!$this->confirm('メール送信を実行しますか?', true)) {
$this->info('キャンセルしました。');
return self::SUCCESS;
}
// メール送信実行
$this->line('');
$this->info('メール送信を開始します...');
try {
$startTime = now();
// SHJ-7サービス呼び出しLaravel Mail または mb_send_mail
if ($useLaravel) {
$result = $this->mailSendService->executeMailSendWithLaravel(
$email,
$backupEmail,
$templateId
);
} else {
$result = $this->mailSendService->executeMailSend(
$email,
$backupEmail,
$templateId
);
}
$endTime = now();
$duration = $startTime->diffInSeconds($endTime);
$this->line('');
$this->info('=== 実行結果 ===');
if ($result['result'] === 0) {
$this->info('✓ メール送信成功');
$this->line("処理時間: {$duration}");
if ($mailDriver === 'log') {
$this->line('');
$this->comment('※ ログファイルを確認してください:');
$this->comment(' storage/logs/laravel.log');
} else {
$this->line('');
$this->comment("{$email} のメールボックスを確認してください");
}
return self::SUCCESS;
} else {
$this->error('✗ メール送信失敗');
$this->error("エラー情報: {$result['error_info']}");
$this->line('');
$this->comment('ログファイルで詳細を確認してください: storage/logs/laravel.log');
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('✗ 予期しないエラーが発生しました');
$this->error("エラー: {$e->getMessage()}");
Log::error('test:mail コマンドエラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Http\JsonResponse;
class ApiException extends Exception
{
/**
* APIエラーコード
*/
protected string $errorCode;
/**
* HTTPステータスコード
*/
protected int $httpStatus;
/**
* コンストラクタ
*
* @param string $errorCode APIエラーコードE01, E02, etc.
* @param string $message エラーメッセージ
* @param int $httpStatus HTTPステータスコード
*/
public function __construct(string $errorCode, string $message, int $httpStatus = 400)
{
parent::__construct($message);
$this->errorCode = $errorCode;
$this->httpStatus = $httpStatus;
}
/**
* エラーコード取得
*/
public function getErrorCode(): string
{
return $this->errorCode;
}
/**
* HTTPステータス取得
*/
public function getHttpStatus(): int
{
return $this->httpStatus;
}
/**
* JSONレスポンスを生成
*/
public function render(): JsonResponse
{
return response()->json([
'error' => [
'code' => $this->errorCode,
'message' => $this->getMessage()
]
], $this->httpStatus);
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\UserInformationHistoryIndexRequest;
use App\Http\Requests\Api\UserInformationHistoryStoreRequest;
use App\Http\Requests\Api\UserInformationHistoryUpdateRequest;
use App\Services\UserInformationHistoryService;
use App\Exceptions\ApiException;
use Illuminate\Http\JsonResponse;
class UserInformationHistoryController extends Controller
{
/**
* サービスインスタンス
*/
protected UserInformationHistoryService $service;
/**
* コンストラクタ
*/
public function __construct(UserInformationHistoryService $service)
{
$this->service = $service;
}
/**
* API 7 - 一覧取得
* GET /api/user-information-history
*
* @param UserInformationHistoryIndexRequest $request
* @return JsonResponse
*/
public function index(UserInformationHistoryIndexRequest $request): JsonResponse
{
try {
$result = $this->service->getList($request->validated());
return response()->json([
'result' => 'OK',
'data' => $result['data'],
'pagination' => $result['pagination'],
], 200);
} catch (ApiException $e) {
return $e->render();
}
}
/**
* API 7 - 単一取得
* GET /api/user-information-history/{id}
*
* @param int $id
* @return JsonResponse
*/
public function show(int $id): JsonResponse
{
try {
$data = $this->service->getById($id);
return response()->json([
'result' => 'OK',
'data' => $data,
], 200);
} catch (ApiException $e) {
return $e->render();
}
}
/**
* API 8 - 新規追加
* POST /api/user-information-history
*
* @param UserInformationHistoryStoreRequest $request
* @return JsonResponse
*/
public function store(UserInformationHistoryStoreRequest $request): JsonResponse
{
try {
$data = $this->service->create($request->validated());
return response()->json(array_merge(
['result' => 'OK'],
$data
), 201);
} catch (ApiException $e) {
return $e->render();
}
}
/**
* API 9 - 更新
* PUT /api/user-information-history/{id}
*
* @param UserInformationHistoryUpdateRequest $request
* @param int $id
* @return JsonResponse
*/
public function update(UserInformationHistoryUpdateRequest $request, int $id): JsonResponse
{
try {
$data = $this->service->update($id, $request->validated());
return response()->json(array_merge(
['result' => 'OK'],
$data
), 200);
} catch (ApiException $e) {
return $e->render();
}
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiKeyAuthentication
{
/**
* API Key認証処理
*
* @param Request $request
* @param Closure $next
* @return Response
*/
public function handle(Request $request, Closure $next): Response
{
$apiKey = $request->header('X-API-Key');
// APIキー未指定
if (empty($apiKey)) {
return response()->json([
'error' => [
'code' => 'E01',
'message' => '認証エラー: APIキーが指定されていません。'
]
], 401);
}
// 有効なAPIキーリストを取得
$validApiKeys = config('api.valid_keys', []);
// APIキー検証
if (!in_array($apiKey, $validApiKeys, true)) {
return response()->json([
'error' => [
'code' => 'E01',
'message' => '認証エラー: APIキーが無効です。'
]
], 401);
}
return $next($request);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class UserInformationHistoryIndexRequest extends FormRequest
{
/**
* リクエスト認可
*/
public function authorize(): bool
{
return true;
}
/**
* バリデーションルール
*/
public function rules(): array
{
return [
'user_id' => 'nullable|integer|min:1',
'entry_date_from' => 'nullable|date_format:Y-m-d',
'entry_date_to' => 'nullable|date_format:Y-m-d|after_or_equal:entry_date_from',
'page' => 'nullable|integer|min:1',
'per_page' => 'nullable|integer|min:1|max:100',
];
}
/**
* バリデーションエラーメッセージ
*/
public function messages(): array
{
return [
'user_id.integer' => 'E03: user_id の値が不正です。整数で指定してください。',
'user_id.min' => 'E03: user_id の値が不正です。1以上の整数で指定してください。',
'entry_date_from.date_format' => 'E04: entry_date_from の日付形式が不正です。YYYY-MM-DD形式で指定してください。',
'entry_date_to.date_format' => 'E04: entry_date_to の日付形式が不正です。YYYY-MM-DD形式で指定してください。',
'entry_date_to.after_or_equal' => 'E04: entry_date_to は entry_date_from 以降の日付を指定してください。',
'page.integer' => 'E03: page の値が不正です。整数で指定してください。',
'page.min' => 'E03: page の値が不正です。1以上の整数で指定してください。',
'per_page.integer' => 'E03: per_page の値が不正です。整数で指定してください。',
'per_page.min' => 'E03: per_page の値が不正です。1以上の整数で指定してください。',
'per_page.max' => 'E03: per_page の値が不正です。最大値は100です。',
];
}
/**
* バリデーション失敗時の処理
*/
protected function failedValidation(Validator $validator)
{
$errors = $validator->errors()->first();
// エラーコード抽出
preg_match('/^(E\d{2}):/', $errors, $matches);
$errorCode = $matches[1] ?? 'E99';
$message = preg_replace('/^E\d{2}: /', '', $errors);
throw new HttpResponseException(
response()->json([
'error' => [
'code' => $errorCode,
'message' => $message
]
], 400)
);
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Models\User;
class UserInformationHistoryStoreRequest extends FormRequest
{
/**
* リクエスト認可
*/
public function authorize(): bool
{
return true;
}
/**
* バリデーションルール
*/
public function rules(): array
{
return [
'user_id' => 'required|integer',
'entry_date' => 'required|date_format:Y-m-d',
'user_information_history' => 'required|string|max:255',
];
}
/**
* バリデーションエラーメッセージ
*/
public function messages(): array
{
return [
'user_id.required' => 'E02: user_id が未設定です。',
'user_id.integer' => 'E02: user_id は整数で指定してください。',
'entry_date.required' => 'E04: entry_date が未設定です。',
'entry_date.date_format' => 'E05: entry_date の形式が不正です。YYYY-MM-DD形式で指定してください。',
'user_information_history.required' => 'E06: user_information_history が未設定です。',
'user_information_history.string' => 'E06: user_information_history は文字列で指定してください。',
'user_information_history.max' => 'E07: user_information_history が255文字を超えています。',
];
}
/**
* 追加バリデーション
*/
public function withValidator(Validator $validator)
{
$validator->after(function ($validator) {
// user_idのエラーがない場合のみ存在チェック
if (!$validator->errors()->has('user_id') && $this->user_id) {
if (!User::where('user_id', $this->user_id)->exists()) {
$validator->errors()->add('user_id', 'E03: 指定された user_id が userテーブルに存在しません。');
}
}
});
}
/**
* バリデーション失敗時の処理
*/
protected function failedValidation(Validator $validator)
{
$errors = $validator->errors()->first();
// エラーコード抽出
preg_match('/^(E\d{2}):/', $errors, $matches);
$errorCode = $matches[1] ?? 'E99';
$message = preg_replace('/^E\d{2}: /', '', $errors);
throw new HttpResponseException(
response()->json([
'error' => [
'code' => $errorCode,
'message' => $message
]
], 400)
);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Models\User;
class UserInformationHistoryUpdateRequest extends FormRequest
{
/**
* リクエスト認可
*/
public function authorize(): bool
{
return true;
}
/**
* バリデーションルール
*/
public function rules(): array
{
return [
'user_id' => 'sometimes|integer',
'entry_date' => 'sometimes|date_format:Y-m-d',
'user_information_history' => 'sometimes|string|max:255',
];
}
/**
* バリデーションエラーメッセージ
*/
public function messages(): array
{
return [
'user_id.integer' => 'E03: user_id は整数で指定してください。',
'entry_date.date_format' => 'E04: entry_date の形式が不正です。YYYY-MM-DD形式で指定してください。',
'user_information_history.string' => 'E05: user_information_history は文字列で指定してください。',
'user_information_history.max' => 'E05: user_information_history が255文字を超えています。',
];
}
/**
* 追加バリデーション
*/
public function withValidator(Validator $validator)
{
$validator->after(function ($validator) {
// 少なくとも1つの更新項目が必要
$updateFields = array_filter(
$this->only(['user_id', 'entry_date', 'user_information_history']),
function ($value) {
return $value !== null;
}
);
if (empty($updateFields)) {
$validator->errors()->add('_general', 'E06: 更新項目が1つも指定されていません。');
}
// user_idの存在チェック
if (!$validator->errors()->has('user_id') && $this->has('user_id') && $this->user_id !== null) {
if (!User::where('user_id', $this->user_id)->exists()) {
$validator->errors()->add('user_id', 'E03: 指定された user_id が userテーブルに存在しません。');
}
}
});
}
/**
* バリデーション失敗時の処理
*/
protected function failedValidation(Validator $validator)
{
$errors = $validator->errors()->first();
// エラーコード抽出
preg_match('/^(E\d{2}):/', $errors, $matches);
$errorCode = $matches[1] ?? 'E99';
$message = preg_replace('/^E\d{2}: /', '', $errors);
throw new HttpResponseException(
response()->json([
'error' => [
'code' => $errorCode,
'message' => $message
]
], 400)
);
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use App\Services\ShjFourBService;
/**
* SHJ-4B 決済トランザクション処理ジョブ
*
* SHJ-4Aで登録された決済情報を基に定期契約の更新処理を行う
*/
class ProcessSettlementJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* ジョブの実行可能回数
*
* @var int
*/
public $tries = 3;
/**
* ジョブの実行間隔(秒)
*
* @var array
*/
public $backoff = [60, 300, 900];
/**
* 使用するキュー名
*
* @var string
*/
public $queue = 'settlement';
/**
* 決済トランザクションID
*
* @var int
*/
protected $settlementTransactionId;
/**
* 追加のコンテキスト情報
*
* @var array
*/
protected $context;
/**
* コンストラクタ
*
* @param int $settlementTransactionId 決済トランザクションID
* @param array $context 追加のコンテキスト情報
*/
public function __construct(int $settlementTransactionId, array $context = [])
{
$this->settlementTransactionId = $settlementTransactionId;
$this->context = $context;
}
/**
* ジョブを実行
*
* SHJ-4Bサービスを使用して決済トランザクション処理を実行
*
* @return void
*/
public function handle()
{
$startTime = now();
try {
Log::info('SHJ-4B ProcessSettlementJob開始', [
'settlement_transaction_id' => $this->settlementTransactionId,
'context' => $this->context,
'start_time' => $startTime,
]);
// SHJ-4Bサービスを使用して決済トランザクション処理を実行
// バッチログはShjFourBServiceが自動的にSHJ-8経由で作成
$shjFourBService = app(ShjFourBService::class);
$result = $shjFourBService->processSettlementTransaction(
$this->settlementTransactionId,
$this->context
);
// 処理結果のログ記録
if ($result['success']) {
Log::info('SHJ-4B ProcessSettlementJob完了', [
'settlement_transaction_id' => $this->settlementTransactionId,
'execution_time' => now()->diffInSeconds($startTime),
'result' => $result,
]);
} else {
// ビジネスロジック上の問題(エラーではない)
Log::warning('SHJ-4B ProcessSettlementJob要手動対応', [
'settlement_transaction_id' => $this->settlementTransactionId,
'result' => $result,
]);
}
} catch (\Throwable $e) {
Log::error('SHJ-4B ProcessSettlementJob失敗', [
'settlement_transaction_id' => $this->settlementTransactionId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// エラー時もShjFourBServiceが自動的にバッチログを作成
// ジョブを失敗させて再試行を促す
throw $e;
}
}
/**
* ジョブが失敗した場合の処理
*
* @param \Throwable $exception
* @return void
*/
public function failed(\Throwable $exception)
{
Log::error('SHJ-4B ProcessSettlementJob最終失敗', [
'settlement_transaction_id' => $this->settlementTransactionId,
'context' => $this->context,
'error' => $exception->getMessage(),
'attempts' => $this->attempts(),
]);
// バッチログはShjFourBServiceが既に作成済み
// 最終失敗時の追加処理があればここに記述
// 例:管理者への通知、障害キューへの登録など
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ReservationCancelledMail extends Mailable
{
use Queueable, SerializesModels;
public $user_name;
public $reserve;
public function __construct($user_name, $reserve)
{
$this->user_name = $user_name;
$this->reserve = $reserve;
}
public function build()
{
return $this->subject('So-Manager空き待ちのキャンセルが完了しました')
->view('emails.reservation_cancelled')
->with([
'user_name' => $this->user_name,
'reserve' => $this->reserve,
]);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class UserEditVerifyMail extends Mailable
{
use Queueable, SerializesModels;
public $verifyUrl;
public $user;
public function __construct($verifyUrl, $user)
{
$this->verifyUrl = $verifyUrl;
$this->user = $user;
}
public function build()
{
return $this
->subject('【So-Manager】ユーザー情報変更のご確認')
->view('emails.user_edit_verify');
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class WithdrawCompleteMail extends Mailable
{
use Queueable, SerializesModels;
public $user_name;
public $user_primemail;
public $user_quitday;
public function __construct($user_name, $user_primemail, $user_quitday)
{
$this->user_name = $user_name;
$this->user_primemail = $user_primemail;
$this->user_quitday = $user_quitday;
}
public function build()
{
return $this->subject('退会完了のお知らせ')
->view('emails.withdraw_complete')
->with([
'user_name' => $this->user_name,
'user_primemail' => $this->user_primemail,
'user_quitday' => $this->user_quitday,
]);
}
}

70
app/Models/BatJobLog.php Normal file
View File

@ -0,0 +1,70 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* バッチジョブログモデル - bat_job_logテーブル
*
* SHJ-8で使用する旧バッチログテーブル
* 各バッチ処理の実行ログを記録する
*/
class BatJobLog extends Model
{
/**
* テーブル名
*
* @var string
*/
protected $table = 'bat_job_log';
/**
* プライマリキー
*
* @var string
*/
protected $primaryKey = 'job_log_id';
/**
* 一括代入可能な属性
*
* @var array
*/
protected $fillable = [
'device_id', // デバイスID
'process_name', // プロセス名
'job_name', // ジョブ名
'status', // ステータス
'status_comment', // ステータスコメント
'created_at', // 登録日時
'updated_at' // 更新日時
];
/**
* キャストする属性
*
* @var array
*/
protected $casts = [
'job_log_id' => 'integer',
'device_id' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* タイムスタンプを使用
*
* @var bool
*/
public $timestamps = true;
/**
* deviceとのリレーション
*/
public function device()
{
return $this->belongsTo(Device::class, 'device_id', 'device_id');
}
}

36
app/Models/City.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class City extends Model
{
protected $table = 'city';
protected $primaryKey = 'city_id';
protected $keyType = 'int';
public $incrementing = true;
protected $fillable = [
'city_id',
'city_name',
'print_layout',
'city_user',
'city_remarks',
'created_at',
'updated_at',
];
/**
* 都市のリストを取得
*/
public static function getList(?int $operatorId = null): array
{
return static::query()
->when($operatorId, fn ($q) => $q->where('operator_id', $operatorId))
->orderBy('city_name')
->pluck('city_name', 'city_id')
->toArray();
}
}

44
app/Models/Device.php Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Device extends Model
{
protected $table = 'device';
protected $primaryKey = 'device_id';
public $incrementing = true;
protected $keyType = 'int';
protected $fillable = [
'park_id',
'device_type',
'device_subject',
'device_identifier',
'device_work',
'device_workstart',
'device_replace',
'device_remarks',
'operator_id',
];
protected $casts = [
'park_id' => 'integer',
'device_workstart' => 'date',
'device_replace' => 'date',
'operator_id' => 'integer',
];
public function park()
{
return $this->belongsTo(Park::class, 'park_id', 'park_id');
}
public static function getList(): array
{
return static::orderBy('device_subject')->pluck('device_subject', 'device_id')->toArray();
}
}

View File

@ -0,0 +1,298 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
/**
* 売上集計結果モデル - earnings_summaryテーブル
*
* SHJ-9で作成される日次・月次・年次の売上集計データを管理
*/
class EarningsSummary extends Model
{
/**
* テーブル名
*
* @var string
*/
protected $table = 'earnings_summary';
/**
* プライマリキー
*
* @var string
*/
protected $primaryKey = 'earnings_summary_id';
/**
* 一括代入可能な属性
*
* @var array
*/
protected $fillable = [
'park_id', // 駐輪場ID
'summary_type', // 集計区分
'summary_start_date', // 集計開始日
'summary_end_date', // 集計終了日
'earnings_date', // 売上日
'psection_id', // 車種区分ID
'usertype_subject1', // 規格
'enable_months', // 期間(月数)
'regular_new_count', // 期間件数
'regular_new_amount', // 期間金額
'regular_new_reduction_count', // 期間成免件数
'regular_new_reduction_amount', // 期間成免金額
'regular_update_count', // 更新件数
'regular_update_amount', // 更新金額
'regular_update_reduction_count', // 更新成免件数
'regular_update_reduction_amount', // 更新成免金額
'lumpsum_count', // 一時金件数
'lumpsum', // 一時金
'refunds', // 解時返戻金
'other_income', // 分別収入
'other_spending', // 分別支出
'reissue_count', // 発行件数
'reissue_amount', // 発行金額
'summary_note', // 計備考
'created_at', // 登録日時
'updated_at', // 更新日時
'operator_id' // 新法・ページID
];
/**
* キャストする属性
*
* @var array
*/
protected $casts = [
'earnings_summary_id' => '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',
'lumpsum_count' => 'integer',
'lumpsum' => '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(lumpsum_count) as total_lumpsum_count,
SUM(lumpsum) as total_lumpsum,
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,
'lumpsum_count' => 0,
'lumpsum' => 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->lumpsum +
$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'
);
}
}

View File

@ -0,0 +1,229 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
/**
* ハードウェアチェックログモデル - hardware_check_logテーブル
*
* デバイスのハードウェア状態監視ログを管理
*/
class HardwareCheckLog extends Model
{
/**
* テーブル名
*
* @var string
*/
protected $table = 'hardware_check_log';
/**
* プライマリキー
*
* @var string
*/
protected $primaryKey = 'hardware_check_log_id';
/**
* 一括代入可能な属性
*
* @var array
*/
protected $fillable = [
'device_id', // デバイスID
'status', // ステータス
'status_comment', // ステータスコメント
'created_at', // 作成日時
'updated_at', // 更新日時
'operator_id' // オペレータID
];
/**
* キャストする属性
*
* @var array
*/
protected $casts = [
'hardware_check_log_id' => '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->hardware_check_log_id,
$this->device_id,
$this->getStatusNameAttribute(),
$this->created_at ? $this->created_at->format('Y-m-d H:i:s') : 'N/A'
);
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* 管轄駐輪場モデル - jurisdiction_parkingテーブル
*
* オペレータが管轄する駐輪場の情報を管理
*/
class JurisdictionParking extends Model
{
/**
* テーブル名
*
* @var string
*/
protected $table = 'jurisdiction_parking';
/**
* プライマリキー
*
* @var string
*/
protected $primaryKey = 'jurisdiction_parking_id';
/**
* タイムスタンプ使用
*
* @var bool
*/
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
/**
* 一括代入可能な属性
*
* @var array
*/
protected $fillable = [
'jurisdiction_parking_name',
'ope_id',
'park_id',
'operator_id'
];
/**
* キャストする属性
*
* @var array
*/
protected $casts = [
'jurisdiction_parking_id' => 'integer',
'ope_id' => 'integer',
'park_id' => 'integer',
'operator_id' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* オペレータとの関連
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function ope()
{
return $this->belongsTo(Ope::class, 'ope_id', 'ope_id');
}
/**
* 駐輪場との関連
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function park()
{
return $this->belongsTo(Park::class, 'park_id', 'park_id');
}
/**
* 指定駐輪場を管轄するオペレータIDリストを取得
*
* @param int $parkId 駐輪場ID
* @return array オペレータIDの配列
*/
public static function getOperatorIdsByPark(int $parkId): array
{
return self::where('park_id', $parkId)
->pluck('ope_id')
->toArray();
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* メールテンプレートモデル - mail_templateテーブル
*
* メール送信テンプレート情報を管理するモデル
* 使用プログラムIDに基づいてテンプレートを取得し、メール送信処理で使用される
*/
class MailTemplate extends Model
{
protected $table = 'mail_template';
protected $primaryKey = 'mail_template_id';
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
protected $fillable = [
'pg_id', // 使用プログラムID
'internal_id', // 内部ID
'mgr_cc_flag', // エリアマネージャー同報フラグ
'bcc_adrs', // BCCアドレス
'use_flag', // 使用フラグ
'memo', // メモ
'subject', // 件名
'text', // 本文
'operator_id' // オペレータID
];
protected $casts = [
'mgr_cc_flag' => 'boolean',
'use_flag' => 'boolean',
];
/**
* 使用プログラムIDでメールテンプレート情報を取得
*
* 仕様書に基づく検索条件:
* - 使用プログラムID = 入力パラメーター
* - 使用フラグ = 1
*
* @param int $pgId 使用プログラムID
* @return MailTemplate|null メールテンプレート情報
*/
public static function getByProgramId(int $pgId): ?MailTemplate
{
return self::where('pg_id', $pgId)
->where('use_flag', 1)
->first();
}
/**
* エリアマネージャー同報が有効かチェック
*
* @return bool エリアマネージャー同報フラグ
*/
public function isManagerCcEnabled(): bool
{
return (bool) $this->mgr_cc_flag;
}
/**
* BCCアドレスを取得
*
* @return string|null BCCアドレス
*/
public function getBccAddress(): ?string
{
return $this->bcc_adrs;
}
/**
* メール件名を取得
*
* @return string|null メール件名
*/
public function getSubject(): ?string
{
return $this->subject;
}
/**
* メール本文を取得
*
* @return string|null メール本文
*/
public function getText(): ?string
{
return $this->text;
}
}

228
app/Models/Manager.php Normal file
View File

@ -0,0 +1,228 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* 駐輪場管理者モデル - managerテーブル
*
* 駐輪場管理者マスタ情報を管理するモデル
* 各駐輪場に紐づく管理者の情報、警報送信設定などを保持
*/
class Manager extends Model
{
/**
* テーブル名
*
* @var string
*/
protected $table = 'manager';
/**
* プライマリキー
*
* @var string
*/
protected $primaryKey = 'manager_id';
/**
* タイムスタンプ使用
*
* @var bool
*/
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
/**
* 一括代入可能な属性
*
* @var array
*/
protected $fillable = [
'manager_name', // 管理者名
'manager_type', // 管理者種別
'manager_parkid', // 所属駐輪場ID
'manager_device1', // デバイス1
'manager_device2', // デバイス2
'manager_mail', // メールアドレス
'manager_tel', // 電話番号
'manager_alert1', // アラート送信フラグ1
'manager_alert2', // アラート送信フラグ2
'manager_quit_flag', // 退職フラグ
'manager_quitday', // 退職日
'operator_id' // 登録オペレータID
];
/**
* キャストする属性
*
* @var array
*/
protected $casts = [
'manager_id' => 'integer',
'manager_parkid' => 'integer',
'manager_device1' => 'integer',
'manager_device2' => 'integer',
'manager_alert1' => 'boolean',
'manager_alert2' => 'boolean',
'manager_quit_flag' => 'boolean',
'operator_id' => 'integer',
'manager_quitday' => 'date',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* 所属駐輪場との関連
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function park()
{
return $this->belongsTo(Park::class, 'manager_parkid', 'park_id');
}
/**
* 登録オペレータとの関連
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function operator()
{
return $this->belongsTo(Ope::class, 'operator_id', 'ope_id');
}
/**
* アクティブな管理者のみを取得するスコープ
* (退職フラグ = 0
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('manager_quit_flag', 0);
}
/**
* メールアドレスが設定されている管理者のみを取得するスコープ
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeHasEmail($query)
{
return $query->whereNotNull('manager_mail')
->where('manager_mail', '!=', '');
}
/**
* 指定駐輪場の管理者を取得するスコープ
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $parkId 駐輪場ID
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeByPark($query, int $parkId)
{
return $query->where('manager_parkid', $parkId);
}
/**
* メール送信対象の駐輪場管理者を取得
*
* 条件:
* - 退職フラグ = 0(在職中)
* - メールアドレスが設定されている
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getMailTargetManagers()
{
return self::active()
->hasEmail()
->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid'])
->get()
->map(function ($manager) {
return [
'manager_id' => $manager->manager_id,
'name' => $manager->manager_name,
'email' => $manager->manager_mail,
'park_id' => $manager->manager_parkid
];
});
}
/**
* 指定駐輪場のメール送信対象管理者を取得
*
* @param int $parkId 駐輪場ID
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getMailTargetManagersByPark(int $parkId)
{
return self::active()
->hasEmail()
->byPark($parkId)
->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid'])
->get()
->map(function ($manager) {
return [
'manager_id' => $manager->manager_id,
'name' => $manager->manager_name,
'email' => $manager->manager_mail,
'park_id' => $manager->manager_parkid
];
});
}
/**
* 退職しているかどうかを判定
*
* @return bool 退職しているかどうか
*/
public function isQuit(): bool
{
return (bool) $this->manager_quit_flag;
}
/**
* アクティブ(在職中)かどうかを判定
*
* @return bool 在職中かどうか
*/
public function isActive(): bool
{
return !$this->isQuit();
}
/**
* メールアドレスが設定されているかどうかを判定
*
* @return bool メールアドレスが設定されているかどうか
*/
public function hasEmail(): bool
{
return !empty($this->manager_mail);
}
/**
* 文字列表現
*
* @return string
*/
public function __toString(): string
{
return sprintf(
'Manager[ID:%d, Name:%s, Park:%d, Email:%s]',
$this->manager_id,
$this->manager_name,
$this->manager_parkid,
$this->manager_mail ?? 'N/A'
);
}
}

177
app/Models/Ope.php Normal file
View File

@ -0,0 +1,177 @@
<?php
namespace App\Models;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Utils;
/**
* オペレータ認証モデル
* Laravel 12変更点App\ModelsディレクトリからApp\Modelsディレクトリに移動
* Laravel 5.7: App直下に配置されていた
*/
class Ope extends Authenticatable
{
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
// オペレータタイプ定数(旧システムから継承)
const OPE_TYPE = [
'管理者',
'マネージャー',
'オペレーター',
'エリアマネージャー',
];
protected $table = 'ope'; // データベーステーブル名(旧システムと同じ)
protected $primaryKey = 'ope_id'; // プライマリキー(旧システムと同じ)
/**
* 一括代入可能な属性
* Laravel 5.7から引き継いだフィールド構成
*/
protected $fillable = [
'//TODO オペレータID not found in database specs',
'ope_name', // オペレータ名
'ope_type', // オペレータ種別
'ope_mail', // メールアドレス
'ope_phone', // 電話番号
'ope_sendalart_que1', // キュー1アラート送信
'ope_sendalart_que2', // キュー2アラート送信
'ope_sendalart_que3', // キュー3アラート送信
'ope_sendalart_que4', // キュー4アラート送信
'ope_sendalart_que5', // キュー5アラート送信
'ope_sendalart_que6', // キュー6アラート送信
'ope_sendalart_que7', // キュー7アラート送信
'ope_sendalart_que8', // キュー8アラート送信
'ope_sendalart_que9', // キュー9アラート送信
'ope_sendalart_que10', // キュー10アラート送信
'ope_sendalart_que11', // キュー11アラート送信
'ope_sendalart_que12', // キュー12アラート送信
'ope_sendalart_que13', // キュー13アラート送信
'ope_auth1', // 権限1
'ope_auth2', // 権限2
'ope_auth3', // 権限3
'ope_auth4', // 権限4
'ope_quit_flag', // 退職フラグ
'ope_quitday' // 退職日
];
/**
* 認証用パスワードを取得
* Laravel 12変更点ope_passフィールドを認証パスワードとして使用
*
* @return string
*/
public function getAuthPassword()
{
return $this->ope_pass;
}
/**
* passwordフィールドアクセサー読み取り用
* Laravel 12対応Laravel認証システムがpasswordフィールドを読み取る際にope_passの値を返す
* Laravel 5.7との差異:旧システムでは不要だった仮想フィールド対応
*
* @return string|null
*/
public function getPasswordAttribute()
{
// データベースのope_passフィールドの値を返す
return $this->attributes['ope_pass'] ?? null;
}
/**
* passwordフィールドミューテータ書き込み用
* Laravel 12対応Laravel認証システムがpasswordフィールドを更新する際にope_passフィールドに保存
* Laravel 5.7との差異:旧システムでは不要だった仮想フィールド対応
*
* @param string $value パスワード値
* @return void
*/
public function setPasswordAttribute($value)
{
// passwordフィールドへの書き込みをope_passフィールドにリダイレクト
$this->attributes['ope_pass'] = $value;
}
/**
* 属性設定のオーバーライド
* Remember Tokenの設定を無効化旧システムでは使用していない
*
* @param string $key
* @param mixed $value
*/
public function setAttribute($key, $value)
{
$isRememberTokenAttribute = $key == $this->getRememberTokenName();
if (!$isRememberTokenAttribute) {
parent::setAttribute($key, $value);
}
}
/**
* オペレータ検索機能
* Laravel 5.7から継承したメソッド(検索・ソート・ページネーション)
*
* @param array $inputs
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection
*/
public static function search($inputs)
{
$list = self::query();
// POST検索条件の処理
if ($inputs['isMethodPost']) {
// 検索条件があればここに追加
}
// ソート処理
if ($inputs['sort']) {
$list->orderBy($inputs['sort'], $inputs['sort_type']);
}
// エクスポート用かページネーション用かの判定
if ($inputs['isExport']) {
$list = $list->get();
} else {
$list = $list->paginate(\App\Utils::item_per_page);
}
return $list;
}
/**
* プライマリキーでオペレータを取得
*
* @param mixed $pk
* @return \App\Models\Ope|null
*/
public static function getByPk($pk)
{
return self::find($pk);
}
/**
* プライマリキーでオペレータを削除
*
* @param array $arr
* @return int
*/
public static function deleteByPk($arr)
{
return self::whereIn('ope_id', $arr)->delete();
}
/**
* オペレータのリストを取得(セレクトボックス用)
* Laravel 5.7から継承したメソッド
*
* @return \Illuminate\Support\Collection
*/
public static function getList()
{
return self::pluck('ope_name', 'ope_id');
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class OperatorQue extends Model
{
protected $table = 'operator_que';
protected $primaryKey = 'que_id';
public $timestamps = true;
protected $fillable = [
'que_class',
'user_id',
'contract_id',
'park_id',
'que_comment',
'que_status',
'que_status_comment',
'work_instructions',
'operator_id',
];
// 定数
/** キュー種別 */
public const QueClass = [
1 => '本人確認(社会人)',
2 => '本人確認(学生)',
3 => 'タグ発送',
4 => '予約告知通知',
5 => '定期更新通知',
6 => '返金処理',
7 => '再発行リミット超過',
8 => '支払い催促',
9 => 'シール発行催促',
10 => 'シール再発行',
11 => '名寄せフリガナ照合エラー',
12 => '本人確認(減免更新)',
13 => '本人確認(学生更新)',
101 => 'サーバーエラー',
102 => 'プリンタエラー',
103 => 'スキャナーエラー',
104 => 'プリンタ用紙残少警告',
];
/** キューステータス */
public const QueStatus = [
1 => 'キュー発生',
2 => 'キュー作業中',
3 => 'キュー作業済',
4 => '返金済',
];
public function getQueClassLabel(): string
{
return self::QueClass[$this->que_class] ?? 'キュー種別未設定';
}
public function getQueStatusLabel(): string
{
return self::QueStatus[$this->que_status] ?? (string)$this->que_status;
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function park()
{
return $this->belongsTo(Park::class, 'park_id');
}
public function contract()
{
return $this->belongsTo(Contract::class, 'contract_id');
}
public function operator()
{
return $this->belongsTo(User::class, 'operator_id');
}
public function getUser() { return $this->user; }
public function getPark() { return $this->park; }
}

143
app/Models/Park.php Normal file
View File

@ -0,0 +1,143 @@
<?php
namespace App\Models;
use App\Utils;
use App\Models\City;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
class Park extends Model
{
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
protected $table = 'park';
protected $primaryKey = 'park_id';
protected $fillable = [
'park_id', // 駐輪場ID
'city_id', // 市区
'park_name', // 駐輪場名
'park_ruby', // 駐輪場ふりがな
'park_syllabary', // 駐輪場五十音
'park_adrs', // 住所
'park_close_flag', // 閉設フラグ
'park_day', // 閉設日
'price_memo', // 価格メモ
'alert_flag', // 残警告チェックフラグ
'print_number', // 印字数
'keep_alive', // 最新キープアライブ
'renew_start_date', // 更新期間開始日
'renew_start_time', // 更新期間開始時
'renew_end_date', // 更新期間終了日
'renew_end_time', // 更新期間終了時
'parking_start_period', // 駐輪開始期間
'reminder_type', // リマインダー種別
'reminder_time', // リマインダー時間
'immediate_use_after_contract', // 契約後即利用許可
'display_gender', // 項目表示設定:性別
'display_birthday', // 項目表示設定:生年月日
'display_security_registration_number', // 項目表示設定:防犯登録番号
'distance_between_two_points', // 二点間距離
'latitude', // 駐車場座標(緯度)
'longitude', // 駐車場座標(経度)
'phone_number', // 電話番号
'contract_type_regular', // 駐輪場契約形態(定期)
'contract_type_temporary', // 駐輪場契約形態(一時利用)
'vehicle_type_limit', // 車種制限
'procedure_method', // 手続方法
'payment_method', // 支払方法
'usage_time_limit_flag', // 利用可能時間制限フラグ
'usage_time_start', // 利用可能時間(開始)
'usage_time_end', // 利用可能時間(終了)
'resident_manager_flag', // 常駐管理人フラグ
'resident_time_start', // 常駐時間(開始)
'resident_time_end', // 常駐時間(終了)
'roof_flag', // 屋根フラグ
'seal_issuing_machine_flag', // シール発行機フラグ
'usage_method', // 駐輪場利用方法
'periodic_update_period', // 定期更新期間
'waiting_reservation', // 空き待ち予約
'special_notes', // 特記事項
'student_id_confirmation_type', // 学生証確認種別
'reduction_guide_display_flag', // 減免案内表示フラグ
'reduction_target_age', // 減免対象年齢
'reduction_guide_display_start_month', // 減免案内表示開始月数
'cross_year' // 年跨ぎ
// 如有 created_at/updated_at 可省略不填
];
public static function search($inputs)
{
$list = self::query();
if ($inputs['isMethodPost']) {
// 搜索条件可追加
}
// Sort
if ($inputs['sort']) {
$list->orderBy($inputs['sort'], $inputs['sort_type']);
}
if ($inputs['isExport']){
$list = $list->get();
}else{
$list = $list->paginate(Utils::item_per_page);
}
return $list;
}
public static function getByPk($pk)
{
return self::find($pk);
}
public static function deleteByPk($arr)
{
return self::whereIn('park_id', $arr)->delete();
}
public static function boot()
{
parent::boot();
self::creating(function (Park $model) {
$model->operator_id = Auth::user()->ope_id ?? null;
});
}
/**
* GET 閉設フラグ
*/
public function getParkCloseFlagDisplay() {
if($this->park_close_flag == 1) {
return '閉設';
}
else if($this->park_close_flag == 0) {
return '開設';
}
return '';
}
public function getCity()
{
// city_id => city_id (City モデル要有 city_id PK)
return $this->belongsTo(City::class, 'city_id', 'city_id')->first();
}
public static function getList(){
return self::pluck('park_name','park_id');
}
public static function getIdByName($park_name){
return self::where('park_name',$park_name)->pluck('park_id')->first();
}
/**
* 料金設定との関連付け
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function prices()
{
return $this->hasMany(PriceA::class, 'park_id', 'park_id');
}
}

62
app/Models/PriceA.php Normal file
View File

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* 料金・容量モデル - price_aテーブル正式モデル
* 旧UsingStatusPriceの責務を置き換え
*/
class PriceA extends Model
{
protected $table = 'price_a';
protected $primaryKey = 'price_parkplaceid';
public $timestamps = true;
public const CREATED_AT = 'created_at';
public const UPDATED_AT = 'updated_at';
protected $fillable = [
'prine_name',
'price_month',
'park_id',
'psection_id',
'price_ptypeid',
'user_categoryid',
'pplace_id',
'park_number',
'park_standard',
'park_limit',
'price',
'operator_id',
];
/**
* 駐輪場
*/
public function park()
{
return $this->belongsTo(Park::class, 'park_id', 'park_id');
}
/**
* 車種分類
*/
public function ptype()
{
return $this->belongsTo(Ptype::class, 'price_ptypeid', 'ptype_id');
}
/**
* 定期契約
*/
public function contracts()
{
return $this->hasMany(RegularContract::class, 'price_parkplaceid', 'price_parkplaceid');
}
}

251
app/Models/PrintJobLog.php Normal file
View File

@ -0,0 +1,251 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
/**
* プリンタジョブログモデル - print_job_logテーブル
*
* プリンタ制御プログラムの実行ログを管理
*/
class PrintJobLog extends Model
{
/**
* テーブル名
*
* @var string
*/
protected $table = 'print_job_log';
/**
* プライマリキー
*
* @var string
*/
protected $primaryKey = 'job_log_id';
/**
* 一括代入可能な属性
*
* @var array
*/
protected $fillable = [
'park_id', // 駐輪場ID
'user_id', // ユーザーID
'contract_id', // 契約ID
'process_name', // プロセス名
'job_name', // ジョブ名
'status', // ステータス
'error_code', // エラーコード
'status_comment', // ステータスコメント
'created_at' // 作成日時
];
/**
* キャストする属性
*
* @var array
*/
protected $casts = [
'job_log_id' => '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->job_log_id,
$this->process_name,
$this->error_code,
$this->created_at ? $this->created_at->format('Y-m-d H:i:s') : 'N/A'
);
}
}

144
app/Models/Psection.php Normal file
View File

@ -0,0 +1,144 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
/**
* 車種区分モデル - psectionテーブル
*
* 駐輪場の車種区分マスタデータを管理
*/
class Psection extends Model
{
/**
* テーブル名
*
* @var string
*/
protected $table = 'psection';
/**
* プライマリキー
*
* @var string
*/
protected $primaryKey = 'psection_id';
/**
* 一括代入可能な属性
*
* @var array
*/
protected $fillable = [
'psection_subject', // 車種区分名
'operator_id', // オペレータID
'created_at', // 作成日時
'updated_at', // 更新日時
'psection_id' // 車種区分ID
];
/**
* キャストする属性
*
* @var array
*/
protected $casts = [
'psection_id' => 'integer',
'operator_id' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
// 主キーが自動増分でない場合はfalseに設定
public $incrementing = false;
// タイムスタンプ管理しない場合はfalseに設定
public $timestamps = false;
/**
* 売上集計との関連
*
* @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
);
}
// 新規作成時にoperator_idを自動設定operator_idカラムがある場合のみ
public static function boot()
{
parent::boot();
self::creating(function (Psection $model) {
// ログインしている場合のみセット
if (\Auth::check()) {
$model->operator_id = Auth::user()->ope_id;
}
});
}
// 車種区分リストを取得(プルダウン用)
public static function getList()
{
return self::orderBy('psection_id')->pluck('psection_subject', 'psection_id');
}
}

68
app/Models/Ptype.php Normal file
View File

@ -0,0 +1,68 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Ptype extends Model
{
/**
* 主キー配列で一括削除
*/
public static function deleteByPk($arr)
{
return self::whereIn('ptype_id', $arr)->delete();
}
/**
* 主キーで1件取得
*/
public static function getByPk($pk)
{
return self::find($pk);
}
protected $table = 'ptype';
protected $primaryKey = 'ptype_id';
public $timestamps = true;
public const CREATED_AT = 'created_at';
public const UPDATED_AT = 'updated_at';
protected $fillable = [
'ptype_subject',
'ptype_remarks',
'operator_id',
];
public static function search($inputs)
{
$list = self::query();
if (!empty($inputs['isMethodPost'])) {
// 必要に応じて検索条件を追加
}
if (!empty($inputs['sort'])) {
$list->orderBy($inputs['sort'], $inputs['sort_type'] ?? 'asc');
}
if (!empty($inputs['isExport'])) {
return $list->get();
} else {
return $list->paginate(\App\Models\Utils::item_per_page);
}
}
public static function getList()
{
return self::pluck('ptype_subject', 'ptype_id');
}
public function prices()
{
return $this->hasMany(PriceA::class, 'price_ptypeid', 'ptype_id');
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class RegularContract extends Model
{
protected $table = 'regular_contract';
protected $primaryKey = 'contract_id';
protected $fillable = [
'contract_qr_id',
'user_id',
'user_categoryid',
'reserve_id',
'park_id',
'price_parkplaceid',
'user_securitynum',
'reserve_date',
'contract_reserve',
'contract_created_at',
'contract_updated_at',
'contract_cancelday',
'contract_reduction',
'contract_periods',
'contract_periode',
'contract_taxid',
'billing_amount',
'contract_payment_day',
'contract_money',
'refunds',
'refunds_comment',
'repayment_at',
'contact_guid',
'contact_shop_code',
'contract_cvs_class',
'contract_flag',
'settlement_transaction_id',
'contract_seal_issue',
'seal_reissue_request',
'contract_permission',
'contract_cancel_flag',
'tag_qr_flag',
'tag_change_flag',
'park_position',
'ope_id',
'contract_manual',
'contract_notice',
'contract_payment_number',
'created_at',
'updated_at'
];
public static function search($inputs)
{
$list = self::query();
// Sort
if ($inputs['sort']) {
$list->orderBy($inputs['sort'], $inputs['sort_type']);
}
if ($inputs['isExport']){
$list = $list->get();
}else{
$list = $list->paginate(\App\Utils::item_per_page); // Utilsクラスの定数を使用
}
return $list;
}
public static function getByPk($pk)
{
return self::find($pk);
}
public static function deleteByPk($arr)
{
return self::whereIn('contract_id', $arr)->delete();
}
//TODO 定期契約ID not found in database specs
//TODO 解約/契約不可フラグ not found in database specs
public function userName()
{
return $this->belongsTo(\App\Models\User::class,'user_id','user_seq')->first();
}
public function getUserType()
{
return $this->belongsTo(\App\Models\Usertype::class,'user_categoryid','user_categoryid')->first();
}
public function getPark()
{
return $this->belongsTo(\App\Models\Park::class,'park_id','park_id')->first();
}
public function getPrice()
{
return $this->belongsTo(\App\Models\Price::class,'price_parkplaceid','price_parkplaceid')->first();
}
// public function getSettlement()
// {
// return $this->belongsTo(SettlementTransaction::class,'settlement_transaction_id','settlement_transaction_id')->first();
// }
public function getOpe()
{
return $this->belongsTo(\App\Models\Ope::class,'ope_id','ope_id')->first();
}
}

85
app/Models/Setting.php Normal file
View File

@ -0,0 +1,85 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* 設定マスタモデル - settingテーブル
*
* システム全体の設定情報を管理するモデル
*/
class Setting extends Model
{
/**
* テーブル名
*
* @var string
*/
protected $table = 'setting';
/**
* プライマリキー
*
* @var string
*/
protected $primaryKey = 'setting_id';
/**
* タイムスタンプ使用
*
* @var bool
*/
public $timestamps = true;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
/**
* 一括代入可能な属性
*
* @var array
*/
protected $fillable = [
'edit_master',
'web_master',
'auto_change_date',
'auto_chage_master',
're-issue_alert_number',
'image_base_url1',
'image_base_url2',
'printable_alert_flag',
'printable_number',
'printable_alert_number',
'printer_keep_alive',
'operator_id'
];
/**
* キャストする属性
*
* @var array
*/
protected $casts = [
'setting_id' => 'integer',
'printable_alert_flag' => 'boolean',
'printable_number' => 'integer',
'printable_alert_number' => 'integer',
'printer_keep_alive' => 'integer',
'operator_id' => 'integer',
'auto_change_date' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* 設定情報を取得通常はID=1の単一レコード)
*
* @return Setting|null
*/
public static function getSettings(): ?Setting
{
return self::first();
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SettlementTransaction extends Model
{
protected $table = 'settlement_transaction';
protected $primaryKey = 'settlement_transaction_id';
public $timestamps = true;
protected $fillable = [
'contract_id',
'status',
'pay_code',
'contract_payment_number',
'corp_code',
'mms_date',
'cvs_code',
'shop_code',
'pay_date',
'settlement_amount',
'stamp_flag',
'md5_string',
];
}

212
app/Models/User.php Normal file
View File

@ -0,0 +1,212 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Pagination\LengthAwarePaginator;
class User extends Model
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
protected $table = 'user';
protected $primaryKey = 'user_seq';
public $timestamps = true;
// 本人確認チェックフラグの定数
const USER_ID_CARD_CHK_FLG = [
0 => '未確認',
1 => '確認済み'
];
// 身分証明書種別の定数
const USER_IDCARD = [
'免許証' => '免許証',
'健康保険証' => '健康保険証',
'パスポート' => 'パスポート',
'学生証' => '学生証',
'その他' => 'その他'
];
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'user_id',
'member_id',
'user_pass',
'user_manual_regist_flag',
'user_mailing_flag',
'contract_number',
'user_tag_serial',
'user_tag_serial_64',
'qr_code',
'tag_qr_flag',
'user_aid',
'user_park_number',
'user_place_qrid',
'user_categoryid',
'user_name',
'user_phonetic',
'user_gender',
'user_birthdate',
'user_age',
'ward_residents',
'user_mobile',
'user_homephone',
'user_primemail',
'user_submail',
'user_regident_zip',
'user_regident_pre',
'user_regident_city',
'user_regident_add',
'user_relate_zip',
'user_relate_pre',
'user_relate_city',
'user_relate_add',
'user_workplace',
'user_school',
'user_graduate',
'user_reduction',
'user_idcard',
'user_idcard_chk_flag',
'user_chk_day',
'user_chk_opeid',
'user_tag_issue',
'issue_permission',
'user_quit_flag',
'user_quitday',
'user_remarks',
'photo_filename1',
'photo_filename2',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'user_pass',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'user_birthdate' => 'date',
'user_chk_day' => 'datetime',
'user_quitday' => 'date',
'user_manual_regist_flag' => 'boolean',
'user_mailing_flag' => 'boolean',
'tag_qr_flag' => 'boolean',
'user_idcard_chk_flag' => 'integer', // 修正: boolean -> integer
'issue_permission' => 'boolean',
'user_quit_flag' => 'boolean',
];
}
/**
* ユーザー検索
*/
public static function search($inputs)
{
$query = self::query();
// 検索条件の適用
if (!empty($inputs['user_id'])) {
$query->where('user_id', 'like', '%' . $inputs['user_id'] . '%');
}
if (!empty($inputs['member_id'])) {
$query->where('member_id', 'like', '%' . $inputs['member_id'] . '%');
}
if (!empty($inputs['user_tag_serial'])) {
$query->where('user_tag_serial', 'like', '%' . $inputs['user_tag_serial'] . '%');
}
if (!empty($inputs['user_phonetic'])) {
$query->where('user_phonetic', 'like', '%' . $inputs['user_phonetic'] . '%');
}
if (!empty($inputs['phone'])) {
$query->where(function($q) use ($inputs) {
$q->where('user_mobile', 'like', '%' . $inputs['phone'] . '%')
->orWhere('user_homephone', 'like', '%' . $inputs['phone'] . '%');
});
}
if (isset($inputs['black_list']) && $inputs['black_list'] !== '') {
$query->where('user_quit_flag', $inputs['black_list']);
}
if (isset($inputs['ward_residents']) && $inputs['ward_residents'] !== '') {
$query->where('ward_residents', $inputs['ward_residents']);
}
if (!empty($inputs['user_tag_serial_64'])) {
$query->where('user_tag_serial_64', 'like', '%' . $inputs['user_tag_serial_64'] . '%');
}
// ソート
if (!empty($inputs['sort'])) {
$sortType = !empty($inputs['sort_type']) ? $inputs['sort_type'] : 'asc';
$query->orderBy($inputs['sort'], $sortType);
} else {
$query->orderBy('user_seq', 'desc');
}
// エクスポート用の場合はページネーションしない
if (!empty($inputs['isExport'])) {
return $query->get();
}
// ページネーションUtilsクラスの定数を使用
return $query->paginate(\App\Utils::item_per_page);
}
/**
* シーケンスでユーザーを取得
*/
public static function getUserBySeq($seq)
{
return self::where('user_seq', $seq)->first();
}
/**
* シーケンス配列でユーザーを削除
*/
public static function deleteUsersBySeq($seqArray)
{
try {
return self::whereIn('user_seq', $seqArray)->delete();
} catch (\Exception $e) {
return false;
}
}
/**
* ユーザータイプとのリレーション
*/
public function getUserType()
{
return $this->belongsTo(Usertype::class, 'user_categoryid', 'id');
}
public static function getList()
{
return self::pluck('user_name', 'user_seq');
}
public static function getUserPhone()
{
return self::select('user_seq', 'user_name', 'user_mobile', 'user_homephone')->get();
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class UserInformationHistory extends Model
{
/**
* テーブル名
*/
protected $table = 'user_information_history';
/**
* 主キー
*/
protected $primaryKey = 'user_information_history_id';
/**
* タイムスタンプ使用
*/
public $timestamps = true;
/**
* 一括代入可能なカラム
*/
protected $fillable = [
'user_id',
'entry_date',
'user_information_history',
];
/**
* キャスト定義
*/
protected $casts = [
'user_information_history_id' => 'integer',
'user_id' => 'integer',
'entry_date' => 'date:Y-m-d',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* ユーザーとの関連
*/
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'user_id');
}
/**
* APIレスポンス用の配列形式に変換
*/
public function toApiArray(): array
{
return [
'user_information_history_id' => $this->user_information_history_id,
'user_id' => $this->user_id,
'entry_date' => $this->entry_date?->format('Y-m-d'),
'user_information_history' => $this->user_information_history,
'created_at' => $this->created_at?->toIso8601String(),
'updated_at' => $this->updated_at?->toIso8601String(),
];
}
}

66
app/Models/Usertype.php Normal file
View File

@ -0,0 +1,66 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
class Usertype extends Model
{
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
const PERPAGE = 50;
protected $table = 'usertype';
protected $primaryKey = 'user_categoryid';
protected $fillable = [
'print_name',
'usertype_money',
'usertype_remarks'
];
public static function boot()
{
parent::boot();
self::creating(function (Usertype $model) {
$model->operator_id = Auth::user()->ope_id;
});
}
public static function search($inputs)
{
$list = self::query();
if ($inputs['isMethodPost']) {
}
// Sort
if ($inputs['sort']) {
$list->orderBy($inputs['sort'], $inputs['sort_type']);
}
if ($inputs['isExport']){
$list = $list->get();
}else{
$list = $list->paginate(Utils::item_per_page);
}
return $list;
}
public static function getByPk($pk)
{
return self::find($pk);
}
public static function deleteByPk($arr)
{
return self::whereIn('user_categoryid', $arr)->delete();
}
//TODO 利用者分類ID not found in database specs
//TODO 利用者分類名 not found in database specs
public static function getList(){
return self::pluck('print_name','user_categoryid');
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Console\Commands\ShjOneCommand;
use App\Services\ShjOneService;
use App\Services\GoogleVisionService;
use App\Services\GoogleMapsService;
/**
* SHJ Services Provider
* SHJ-1関連サービスの登録
*/
class ShjServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// SHJ-1 Services
$this->app->singleton(GoogleVisionService::class);
$this->app->singleton(GoogleMapsService::class);
$this->app->singleton(ShjOneService::class);
// Commands
$this->commands([
ShjOneCommand::class,
]);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

View File

@ -0,0 +1,185 @@
<?php
namespace App\Services;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Google Maps API サービス
* SHJ-1本人確認自動処理で距離計算に使用
*/
class GoogleMapsService
{
protected $apiKey;
protected $baseUrl;
protected $config;
public function __construct()
{
$this->apiKey = config('shj1.apis.google_maps.api_key');
$this->baseUrl = config('shj1.apis.google_maps.base_url');
$this->config = config('shj1.distance.google_maps_config');
}
/**
* 2点間の距離を計算Google Maps Distance Matrix API使用
*
* @param string $origin 出発地(住所)
* @param string $destination 目的地(住所)
* @return array ['distance_meters' => int, 'distance_text' => string, 'status' => string]
* @throws Exception
*/
public function calculateDistance(string $origin, string $destination): array
{
try {
$url = "{$this->baseUrl}/distancematrix/json";
$params = [
'origins' => $origin,
'destinations' => $destination,
'units' => $this->config['units'] ?? 'metric',
'mode' => $this->config['mode'] ?? 'walking',
'language' => $this->config['language'] ?? 'ja',
'region' => $this->config['region'] ?? 'jp',
'key' => $this->apiKey,
];
// タイムアウト短縮従来30秒→15秒
$response = Http::timeout(15)->get($url, $params);
if (!$response->successful()) {
throw new Exception("Google Maps API request failed: " . $response->status());
}
$data = $response->json();
// APIエラーチェック
if ($data['status'] !== 'OK') {
throw new Exception("Google Maps API error: " . $data['status']);
}
$element = $data['rows'][0]['elements'][0];
if ($element['status'] !== 'OK') {
throw new Exception("Distance calculation failed: " . $element['status']);
}
return [
'distance_meters' => $element['distance']['value'],
'distance_text' => $element['distance']['text'],
'duration_text' => $element['duration']['text'] ?? null,
'status' => 'success'
];
} catch (Exception $e) {
Log::error('Google Maps distance calculation error', [
'origin' => $origin,
'destination' => $destination,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 住所から座標を取得Geocoding API使用
*
* @param string $address 住所
* @return array ['lat' => float, 'lng' => float]
* @throws Exception
*/
public function geocodeAddress(string $address): array
{
try {
$url = "{$this->baseUrl}/geocode/json";
$params = [
'address' => $address,
'language' => $this->config['language'] ?? 'ja',
'region' => $this->config['region'] ?? 'jp',
'key' => $this->apiKey,
];
$response = Http::timeout(30)->get($url, $params);
if (!$response->successful()) {
throw new Exception("Google Geocoding API request failed: " . $response->status());
}
$data = $response->json();
if ($data['status'] !== 'OK') {
throw new Exception("Google Geocoding API error: " . $data['status']);
}
$location = $data['results'][0]['geometry']['location'];
return [
'lat' => $location['lat'],
'lng' => $location['lng']
];
} catch (Exception $e) {
Log::error('Google Maps geocoding error', [
'address' => $address,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 座標から直線距離を計算Haversine formula
* Google Maps APIが利用できない場合のフォールバック
*
* @param float $lat1 緯度1
* @param float $lon1 経度1
* @param float $lat2 緯度2
* @param float $lon2 経度2
* @return int 距離(メートル)
*/
public function calculateStraightLineDistance(float $lat1, float $lon1, float $lat2, float $lon2): int
{
$earthRadius = 6371000; // 地球の半径(メートル)
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
$distance = $earthRadius * $c;
return round($distance);
}
/**
* 距離詳細文字列を生成SHJ-1の要求仕様に準拠)
*
* @param int $parkId 駐輪場ID
* @param int $distanceMeters 距離(メートル)
* @param string $method 計算方法
* @return string
*/
public function generateDistanceDetailString(int $parkId, int $distanceMeters, string $method = 'google_maps'): string
{
return $parkId . "/" . $parkId . "/二点間距離:" . $distanceMeters . "m (" . $method . ")";
}
/**
* APIキーが設定されているかチェック
*
* @return bool
*/
public function isApiKeyConfigured(): bool
{
$dummyKey = env('GOOGLE_MAPS_API_KEY') === 'dummy_google_maps_api_key_replace_with_real_one';
return !empty($this->apiKey) && !$dummyKey;
}
}

View File

@ -0,0 +1,332 @@
<?php
namespace App\Services;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Http;
/**
* Google Cloud Vision API サービス
* SHJ-1本人確認自動処理でOCR処理に使用
*/
class GoogleVisionService
{
protected $apiKey;
protected $projectId;
protected $baseUrl = 'https://vision.googleapis.com/v1';
public function __construct()
{
$this->apiKey = config('shj1.apis.google_vision.api_key');
$this->projectId = config('shj1.apis.google_vision.project_id');
}
/**
* 画像からテキストを抽出OCR処理
*
* @param string $imageFilename 画像ファイル名
* @return string 抽出されたテキスト
* @throws Exception
*/
public function extractTextFromImage(string $imageFilename): string
{
try {
Log::info('GoogleVision OCR処理開始', [
'filename' => $imageFilename
]);
// 画像ファイルを取得
$imageData = $this->getImageData($imageFilename);
// Vision API リクエスト実行
$detectedText = $this->callVisionApi($imageData);
Log::info('GoogleVision OCR処理完了', [
'filename' => $imageFilename,
'text_length' => strlen($detectedText),
'text_preview' => substr($detectedText, 0, 100) . '...',
'text_full' => $detectedText, // ← 完整のOCR結果を追加
'debug_marker' => 'NEW_CODE_EXECUTED_' . time() // ← デバッグマーカー追加
]);
return $detectedText;
} catch (Exception $e) {
Log::error('GoogleVision OCR処理エラー', [
'filename' => $imageFilename,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* 画像データを取得Laravel Storage使用
*/
private function getImageData(string $filename): string
{
$disk = config('shj1.storage.photo_disk');
Log::info('GoogleVision 画像ファイル読取開始', [
'filename' => $filename,
'disk' => $disk
]);
if (!Storage::disk($disk)->exists($filename)) {
Log::error('GoogleVision 画像ファイルが見つかりません', [
'filename' => $filename,
'disk' => $disk
]);
throw new Exception("画像ファイルが見つかりません: {$filename}");
}
$imageContent = Storage::disk($disk)->get($filename);
if (empty($imageContent)) {
Log::error('GoogleVision 画像ファイルが空です', [
'filename' => $filename
]);
throw new Exception("画像ファイルが空です: {$filename}");
}
Log::info('GoogleVision 画像ファイル読取成功', [
'filename' => $filename,
'size' => strlen($imageContent)
]);
return base64_encode($imageContent);
}
/**
* Google Cloud Vision API呼び出し
*/
private function callVisionApi(string $base64Image): string
{
if (!$this->isApiKeyConfigured()) {
throw new Exception("Google Vision API キーが設定されていません");
}
$url = "{$this->baseUrl}/images:annotate";
$requestBody = [
'requests' => [
[
'image' => [
'content' => $base64Image
],
'features' => [
[
'type' => 'TEXT_DETECTION',
'maxResults' => 1
]
],
'imageContext' => [
'languageHints' => ['ja'] // 日本語優先
]
]
]
];
// タイムアウト短縮従来30秒→15秒
$response = Http::timeout(15)
->withHeaders([
'Content-Type' => 'application/json',
])
->post($url . '?key=' . $this->apiKey, $requestBody);
if (!$response->successful()) {
throw new Exception("Google Vision API request failed: " . $response->status());
}
$data = $response->json();
// APIエラーチェック
if (isset($data['responses'][0]['error'])) {
$error = $data['responses'][0]['error'];
throw new Exception("Google Vision API error: " . $error['message']);
}
// テキスト抽出結果を取得
$textAnnotations = $data['responses'][0]['textAnnotations'] ?? [];
if (empty($textAnnotations)) {
return ''; // テキストが検出されなかった場合
}
// 最初のアノテーションが全体のテキスト
return $textAnnotations[0]['description'] ?? '';
}
/**
* 文字列の類似度を計算
*
* @param string $expected 期待する文字列
* @param string $detected 検出された文字列
* @return float 類似度0-100
*/
public function calculateSimilarity(string $expected, string $detected): float
{
// 前処理:空白文字削除、大文字小文字統一
$expected = $this->normalizeText($expected);
$detected = $this->normalizeText($detected);
if (empty($expected) || empty($detected)) {
return 0.0;
}
// 複数の計算方法を試し、最高値を返す
$similarities = [];
// 1. 標準的な類似度計算
similar_text($expected, $detected, $percent);
$similarities[] = $percent;
// 2. 文字含有率計算OCRの分離文字対応
$containsScore = $this->calculateContainsScore($expected, $detected);
$similarities[] = $containsScore;
// 3. 部分文字列マッチング
$substringScore = $this->calculateSubstringScore($expected, $detected);
$similarities[] = $substringScore;
// 最高スコアを返す
return max($similarities);
}
/**
* 文字含有率スコア計算(姓名の分離文字対応)
*/
private function calculateContainsScore(string $expected, string $detected): float
{
if (empty($expected)) return 0.0;
// 期待文字列の各文字が検出文字列に含まれるかチェック
$expectedChars = mb_str_split($expected, 1, 'UTF-8');
$containedCount = 0;
foreach ($expectedChars as $char) {
if (mb_strpos($detected, $char, 0, 'UTF-8') !== false) {
$containedCount++;
}
}
return ($containedCount / count($expectedChars)) * 100;
}
/**
* 部分文字列マッチングスコア計算
*/
private function calculateSubstringScore(string $expected, string $detected): float
{
if (empty($expected)) return 0.0;
// 期待文字列を2文字以上のチャンクに分割してマッチング
$chunks = [];
$expectedLength = mb_strlen($expected, 'UTF-8');
// 2文字チャンク
for ($i = 0; $i <= $expectedLength - 2; $i++) {
$chunks[] = mb_substr($expected, $i, 2, 'UTF-8');
}
// 3文字チャンク
for ($i = 0; $i <= $expectedLength - 3; $i++) {
$chunks[] = mb_substr($expected, $i, 3, 'UTF-8');
}
if (empty($chunks)) return 0.0;
$matchedChunks = 0;
foreach ($chunks as $chunk) {
if (mb_strpos($detected, $chunk, 0, 'UTF-8') !== false) {
$matchedChunks++;
}
}
return ($matchedChunks / count($chunks)) * 100;
}
/**
* テキスト正規化OCR認識結果の形式差異に対応
*/
private function normalizeText(string $text): string
{
// 空白文字除去、改行除去
$text = preg_replace('/\s+/', '', $text);
// 全角英数字・記号を半角に変換
$text = mb_convert_kana($text, 'rnask', 'UTF-8');
// 住所の同義語統一
$text = str_replace(['東京市', '東京府'], '東京都', $text);
$text = str_replace(['大阪市', '大阪府'], '大阪', $text);
// 数字の統一(全角→半角は上でやっているが、明示的に処理)
$text = str_replace(['', '', '', '', '', '', '', '', '', ''],
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], $text);
// 記号の統一
$text = str_replace(['ー', '', '―'], '-', $text);
$text = str_replace(['の'], '', $text); // 「1番地の1」→「1番地1」
return $text;
}
/**
* 部分マッチング検索
*
* @param string $needle 検索する文字列
* @param string $haystack 検索対象の文字列
* @return bool
*/
public function containsText(string $needle, string $haystack): bool
{
$needle = $this->normalizeText($needle);
$haystack = $this->normalizeText($haystack);
return mb_strpos($haystack, $needle) !== false;
}
/**
* 画像形式の検証
*/
public function isValidImageFormat(string $filename): bool
{
$allowedExtensions = config('shj1.storage.allowed_extensions');
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
return in_array($extension, $allowedExtensions);
}
/**
* APIキーが設定されているかチェック
*/
public function isApiKeyConfigured(): bool
{
$dummyKey = env('GOOGLE_VISION_API_KEY') === 'dummy_google_vision_api_key_replace_with_real_one';
return !empty($this->apiKey) && !$dummyKey;
}
/**
* Vision API レスポンスから信頼度を取得
*/
public function getConfidenceScore(array $textAnnotation): float
{
if (!isset($textAnnotation['boundingPoly']['vertices'])) {
return 0.0;
}
// 検出領域のサイズから信頼度を推定
$vertices = $textAnnotation['boundingPoly']['vertices'];
if (count($vertices) < 4) {
return 0.0;
}
// 単純な信頼度計算(実際の実装では、より詳細な分析が必要)
return 0.95; // デフォルト高信頼度
}
}

View File

@ -0,0 +1,270 @@
<?php
namespace App\Services;
use App\Models\BatJobLog;
use App\Models\Device;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
/**
* SHJ-8 バッチ処理ログ作成サービス
*
* 概要: 入力パラメーターの情報を元にバッチ処理ログ情報を作成する
*
* 処理フロー:
* 【処理1】入力パラメーターをチェックする
* 【判断1】チェック結果
* NG パラメーターNGの結果を設定する 【処理3】
* OK 【処理2】
* 【処理2】バッチ処理ログを登録する
* 【処理3】処理結果を返却する
*/
class ShjEightService
{
/**
* SHJ-8 メイン処理実行
*
* 修正版7項目入力status_comment追加
* @param int $deviceId デバイスID (必須)
* @param string|null $processName プロセス名
* @param string|null $jobName ジョブ名
* @param string $status ステータス
* @param string $statusComment ステータスコメント (必須, ≤255文字)
* @param string $createdDate 登録日時 (yyyy/mm/dd形式)
* @param string $updatedDate 更新日時 (yyyy/mm/dd形式)
* @return array 処理結果 ['result' => 0|1, 'error_message' => string|null]
*/
public function execute(
int $deviceId,
?string $processName,
?string $jobName,
string $status,
string $statusComment,
string $createdDate,
string $updatedDate
): array {
try {
Log::info('SHJ-8 バッチ処理ログ作成開始', [
'device_id' => $deviceId,
'process_name' => $processName,
'job_name' => $jobName,
'status' => $status,
'status_comment' => $statusComment,
'created_date' => $createdDate,
'updated_date' => $updatedDate
]);
// 【処理1】入力パラメーターをチェックする
$validationResult = $this->validateParameters(
$deviceId,
$processName,
$jobName,
$status,
$statusComment,
$createdDate,
$updatedDate
);
// 【判断1】チェック結果
if (!$validationResult['valid']) {
// パラメーターNG
$errorMessage = $validationResult['error_message'];
Log::warning('SHJ-8 パラメーターチェックNG', [
'error_message' => $errorMessage
]);
// 【処理3】異常終了の結果を返却
return [
'result' => 1,
'error_message' => $errorMessage
];
}
// 【処理2】バッチ処理ログを登録する
$batJobLog = $this->createBatchJobLog(
$deviceId,
$processName,
$jobName,
$status,
$statusComment,
$createdDate,
$updatedDate
);
Log::info('SHJ-8 バッチ処理ログ作成完了', [
'job_log_id' => $batJobLog->job_log_id,
'device_id' => $batJobLog->device_id,
'process_name' => $batJobLog->process_name,
'job_name' => $batJobLog->job_name
]);
// 【処理3】正常終了の結果を返却
return [
'result' => 0
];
} catch (\Exception $e) {
Log::error('SHJ-8 バッチ処理ログ作成エラー', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// 例外発生時の処理結果を返却
return [
'result' => 1,
'error_code' => $e->getCode(),
'error_message' => $e->getMessage(),
'stack_trace' => $e->getTraceAsString()
];
}
}
/**
* 【処理1】入力パラメーターをチェックする
*
* 修正版7項目チェックstatus_comment追加
* 1. デバイスID: 必須、device表に存在チェック
* 2. プロセス名: 「プロセス名」「ジョブ名」いずれか必須
* 3. ジョブ名: 「プロセス名」「ジョブ名」いずれか必須
* 4. ステータス: 必須
* 5. ステータスコメント: 必須、≤255文字
* 6. 登録日時: 日付型(yyyy/mm/dd形式)
* 7. 更新日時: 日付型(yyyy/mm/dd形式)
*
* @param int $deviceId デバイスID
* @param string|null $processName プロセス名
* @param string|null $jobName ジョブ名
* @param string $status ステータス
* @param string $statusComment ステータスコメント
* @param string $createdDate 登録日時
* @param string $updatedDate 更新日時
* @return array 検証結果 ['valid' => bool, 'error_message' => string|null]
*/
private function validateParameters(
int $deviceId,
?string $processName,
?string $jobName,
string $status,
string $statusComment,
string $createdDate,
string $updatedDate
): array {
$errors = [];
// 1. デバイスIDチェック (必須、存在チェック)
if ($deviceId <= 0) {
$errors[] = "パラメーターNGデバイスID/{$deviceId}";
} elseif (!Device::where('device_id', $deviceId)->exists()) {
$errors[] = "パラメーターNGデバイスID/{$deviceId}";
}
// 2. プロセス名とジョブ名のいずれか必須チェック
if (empty($processName) && empty($jobName)) {
$errors[] = "パラメーターNGプロセス名/<空>";
$errors[] = "パラメーターNGジョブ名/<空>";
}
// 3. ステータス必須チェック
if (empty($status)) {
$errors[] = "パラメーターNGステータス/<空>";
}
// 4. ステータスコメント必須チェック、255文字以内
if (empty($statusComment)) {
$errors[] = "パラメーターNGステータスコメント/<空>";
} elseif (mb_strlen($statusComment) > 255) {
$errors[] = "パラメーターNGステータスコメント/長さ超過(" . mb_strlen($statusComment) . "文字)";
}
// 5. 登録日時の日付型チェック
if (!$this->isValidDateFormat($createdDate)) {
$errors[] = "パラメーターNG登録日時/{$createdDate}";
}
// 6. 更新日時の日付型チェック
if (!$this->isValidDateFormat($updatedDate)) {
$errors[] = "パラメーターNG更新日時/{$updatedDate}";
}
// エラーがある場合
if (!empty($errors)) {
// 複数のエラーがある場合は全角カンマ(、)で連結
$errorMessage = implode('、', $errors);
return [
'valid' => false,
'error_message' => $errorMessage
];
}
// 正常終了
return [
'valid' => true,
'error_message' => null
];
}
/**
* 日付形式の検証
*
* yyyy/mm/dd形式かチェック
*
* @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]);
}
/**
* 【処理2】バッチ処理ログを登録する
*
* bat_job_logテーブルにINSERT
* 修正版:ステータスコメントは呼び出し元から受け取る(固定値廃止)
*
* @param int $deviceId デバイスID
* @param string|null $processName プロセス名
* @param string|null $jobName ジョブ名
* @param string $status ステータス
* @param string $statusComment ステータスコメント(業務固有)
* @param string $createdDate 登録日時 (yyyy/mm/dd形式)
* @param string $updatedDate 更新日時 (yyyy/mm/dd形式)
* @return BatJobLog 作成されたバッチジョブログ
*/
private function createBatchJobLog(
int $deviceId,
?string $processName,
?string $jobName,
string $status,
string $statusComment,
string $createdDate,
string $updatedDate
): BatJobLog {
// 日付文字列をdatetime型に変換現在時刻を使用
$createdDatetime = Carbon::createFromFormat('Y/m/d', $createdDate);
$updatedDatetime = Carbon::createFromFormat('Y/m/d', $updatedDate);
// bat_job_logテーブルに登録status_commentは呼び出し元から受け取った値を使用
$batJobLog = BatJobLog::create([
'device_id' => $deviceId,
'process_name' => $processName,
'job_name' => $jobName,
'status' => $status,
'status_comment' => $statusComment,
'created_at' => $createdDatetime,
'updated_at' => $updatedDatetime
]);
return $batJobLog;
}
}

View File

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

View File

@ -0,0 +1,693 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\OperatorQue;
use App\Models\Device;
use Exception;
/**
* SHJ-5 空き待ち通知処理サービス
*
* 駐輪場の空き状況を確認し、空き待ち予約者への通知処理を実行する
* 仕様書に基づくバックグラウンド定期バッチ処理
*/
class ShjFiveService
{
/**
* ShjEightService
*
* @var ShjEightService
*/
protected $shjEightService;
/**
* コンストラクタ
*
* @param ShjEightService $shjEightService
*/
public function __construct(ShjEightService $shjEightService)
{
$this->shjEightService = $shjEightService;
}
/**
* SHJ-5 メイン処理を実行
*
* 処理フロー:
* 1. 駐輪場の空き状況を取得する
* 2. 空き状況判定
* 3. 空き待ち者の情報を取得する
* 4. 取得件数判定
* 5. 空き待ち者への通知、またはオペレーターキュー追加処理
* 6. バッチ処理ログを作成する
*
* @return array 処理結果
*/
public function executeParkVacancyNotification(): array
{
try {
$startTime = now();
Log::info('SHJ-5 空き待ち通知処理開始');
// 処理統計
$processedParksCount = 0;
$vacantParksCount = 0;
$totalWaitingUsers = 0;
$notificationSuccessCount = 0;
$operatorQueueCount = 0;
$mailErrors = []; // メール異常終了件数専用
$errors = []; // 全体エラー収集用
$allQueueItems = []; // 全オペレーターキュー作成用データ
// 【処理1】駐輪場の空き状況を取得する
$parkVacancyList = $this->getParkVacancyStatus();
Log::info('駐輪場空き状況取得完了', [
'total_parks' => count($parkVacancyList)
]);
// 各駐輪場に対する処理
foreach ($parkVacancyList as $parkVacancyData) {
// 配列をオブジェクトに変換
$parkVacancy = (object) $parkVacancyData;
$processedParksCount++;
Log::info('駐輪場処理開始', [
'park_id' => $parkVacancy->park_id,
'park_name' => $parkVacancy->park_name,
'psection_id' => $parkVacancy->psection_id,
'ptype_id' => $parkVacancy->ptype_id,
'vacant_count' => $parkVacancy->vacant_count
]);
// 【判断1】空き状況判定
if ($parkVacancy->vacant_count < 1) {
Log::info('空きなし - 処理スキップ', [
'park_id' => $parkVacancy->park_id,
'vacant_count' => $parkVacancy->vacant_count
]);
continue;
}
$vacantParksCount++;
// 【処理2】空き待ち者の情報を取得する
$waitingUsers = $this->getWaitingUsersInfo(
$parkVacancy->park_id,
$parkVacancy->psection_id,
$parkVacancy->ptype_id
);
// 【判断2】取得件数判定
if (empty($waitingUsers)) {
Log::info('空き待ち者なし', [
'park_id' => $parkVacancy->park_id
]);
continue;
}
$totalWaitingUsers += count($waitingUsers);
Log::info('空き待ち者情報取得完了', [
'park_id' => $parkVacancy->park_id,
'waiting_users_count' => count($waitingUsers)
]);
// 【処理3】空き待ち者への通知、またはオペレーターキュー追加処理
$notificationResult = $this->processWaitingUsersNotification(
$waitingUsers,
$parkVacancy
);
$notificationSuccessCount += $notificationResult['notification_success_count'];
$operatorQueueCount += $notificationResult['operator_queue_count'];
if (!empty($notificationResult['errors'])) {
$mailErrors = array_merge($mailErrors, $notificationResult['errors']);
$errors = array_merge($errors, $notificationResult['errors']);
}
// オペレーターキュー作成用データを収集
if (!empty($notificationResult['queue_items'])) {
$allQueueItems = array_merge($allQueueItems ?? [], $notificationResult['queue_items']);
}
}
// 【処理4】仕様書準拠先に呼び出し、成功時のみ内部変数を更新
$queueErrorCount = 0; // キュー登録異常終了件数(累計)
$queueSuccessCount = 0; // キュー登録正常終了件数(累計)
foreach ($allQueueItems as $queueItem) {
// 仕様書準拠:在呼叫前先計算"如果這次成功會是第幾件",確保記錄反映最新件數
$predictedSuccessCount = $queueSuccessCount + 1;
$predictedErrorCount = $queueErrorCount; // 暂时保持当前错误计数
$queueResult = $this->addToOperatorQueue(
$queueItem['waiting_user'],
$queueItem['park_vacancy'],
$queueItem['batch_comment'],
$notificationSuccessCount, // 最終メール正常終了件数
$predictedSuccessCount, // 預測成功時的件數(包含本次)
count($mailErrors), // 現在のメール異常終了件数(動態計算)
$predictedErrorCount // 現在のキュー登録異常終了件数
);
// 仕様書:根据实际结果决定是否采用预测值
if ($queueResult['success']) {
$queueSuccessCount = $predictedSuccessCount; // 采用预测的成功计数
} else {
$queueErrorCount++; // 失败时递增错误计数
// 仕様書:包含具体错误消息,满足"エラーメッセージ/スタックトレースを保持"要求
$errorDetail = $queueResult['error'] ?? 'Unknown error';
$queueErrorInfo = sprintf('キュー登録失敗:予約ID:%d - %s',
$queueItem['waiting_user']->reserve_id ?? 0,
$errorDetail
);
$errors[] = $queueErrorInfo; // 加入总错误统计(包含具体原因)
Log::error('オペレーターキュー作成失敗', [
'user_id' => $queueItem['waiting_user']->user_id,
'reserve_id' => $queueItem['waiting_user']->reserve_id ?? 0,
'error' => $errorDetail
]);
}
}
$endTime = now();
$duration = $startTime->diffInSeconds($endTime);
Log::info('SHJ-5 空き待ち通知処理完了', [
'duration_seconds' => $duration,
'processed_parks_count' => $processedParksCount,
'vacant_parks_count' => $vacantParksCount,
'total_waiting_users' => $totalWaitingUsers,
'notification_success_count' => $notificationSuccessCount,
'operator_queue_success_count' => $queueSuccessCount, // 仕様書:正常完了件数
'queue_error_count' => $queueErrorCount,
'mail_error_count' => count($mailErrors), // メール異常終了件数(分離)
'total_error_count' => count($errors) // 全体エラー件数
]);
// 仕様書に基づく内部変数.ステータスコメント生成
$statusComment = sprintf(
'メール正常終了件数:%dメール異常終了件数%dキュー登録正常終了件数%dキュー登録異常終了件数%d',
$notificationSuccessCount,
count($mailErrors), // メール異常終了件数(キュー失敗を除外)
$queueSuccessCount ?? 0, // 実際のキュー登録成功件数
$queueErrorCount ?? 0 // 実際のキュー登録失敗件数
);
// SHJ-8 バッチ処理ログ作成
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
$this->shjEightService->execute(
$deviceId,
'SHJ-5',
'SHJ-5空き待ち通知',
'success',
$statusComment,
$today,
$today
);
Log::info('SHJ-8 バッチ処理ログ作成完了');
} catch (Exception $e) {
Log::error('SHJ-8 バッチ処理ログ作成エラー', [
'error' => $e->getMessage()
]);
}
return [
'success' => true,
'message' => 'SHJ-5 空き待ち通知処理が正常に完了しました',
'processed_parks_count' => $processedParksCount,
'vacant_parks_count' => $vacantParksCount,
'total_waiting_users' => $totalWaitingUsers,
'notification_success_count' => $notificationSuccessCount,
'operator_queue_count' => $queueSuccessCount ?? 0, // 仕様書:正常完了件数を使用
'error_count' => count($errors),
'errors' => $errors,
'duration_seconds' => $duration,
'status_comment' => $statusComment // SHJ-8用の完全なステータスコメント
];
} catch (Exception $e) {
Log::error('SHJ-5 空き待ち通知処理でエラーが発生', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'message' => 'SHJ-5 空き待ち通知処理でエラーが発生: ' . $e->getMessage(),
'error_details' => $e->getMessage()
];
}
}
/**
* 【処理1】駐輪場の空き状況を取得する
*
* 仕様書に基づくSQLSQL-1:
* - 「駐輪場マスタ」と「ゾーンマスタ」より空き状況を取得する
* - zone表から zone_number現在契約台数、zone_tolerance限界収容台数を取得
* - 空き台数 = 限界収容台数 - 現在契約台数
*
* @return array 駐輪場空き状況リスト
*/
private function getParkVacancyStatus(): array
{
try {
// 仕様書SQL-1に基づくクエリpark + zone + ptype + psection
$vacancyList = DB::table('park as T1')
->select([
'T1.park_id',
'T1.park_name',
'T1.park_ruby',
'T2.ptype_id',
'T3.ptype_subject',
'T2.psection_id',
'T4.psection_subject',
DB::raw('sum(T2.zone_number) as sum_zone_number'), // 現在契約台数
DB::raw('sum(T2.zone_standard) as sum_zone_standard'), // 標準収容台数
DB::raw('sum(T2.zone_tolerance) as sum_zone_tolerance') // 限界収容台数
])
->join('zone as T2', 'T1.park_id', '=', 'T2.park_id')
->join('ptype as T3', 'T2.ptype_id', '=', 'T3.ptype_id')
->join('psection as T4', 'T2.psection_id', '=', 'T4.psection_id')
->where([
['T1.park_close_flag', '=', 0], // 駐輪場開設
['T2.delete_flag', '=', 0], // ゾーン有効
])
->groupBy(['T1.park_id', 'T2.ptype_id', 'T2.psection_id'])
->orderBy('T1.park_ruby')
->orderBy('T3.floor_sort')
->orderBy('T2.psection_id')
->get()
->map(function($record) {
// 【JOB1-STEP1】空き台数 = 限界収容台数 - 現在契約台数
$vacant_count = max(0, $record->sum_zone_tolerance - $record->sum_zone_number);
return (object)[
'park_id' => $record->park_id,
'park_name' => $record->park_name,
'park_ruby' => $record->park_ruby,
'ptype_id' => $record->ptype_id,
'ptype_subject' => $record->ptype_subject,
'psection_id' => $record->psection_id,
'psection_subject' => $record->psection_subject,
'sum_zone_number' => $record->sum_zone_number, // JOB1 現在契約台数
'sum_zone_standard' => $record->sum_zone_standard, // JOB1 標準収容台数
'sum_zone_tolerance' => $record->sum_zone_tolerance, // JOB1 限界収容台数
'vacant_count' => $vacant_count, // 内部変数.空き台数
];
})
->filter(function($zone) {
// 【判断1】空き状況判定空きがあるもののみ
return $zone->vacant_count > 0;
})
->values();
Log::info('駐輪場空き状況取得完了仕様書SQL-1準拠', [
'total_records' => count($vacancyList),
'vacant_records' => $vacancyList->filter(function($v) {
return $v->vacant_count > 0;
})->count()
]);
return $vacancyList->toArray();
} catch (Exception $e) {
Log::error('駐輪場空き状況取得エラー', [
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 【処理2】空き待ち者の情報を取得する
*
* 仕様書JOB2に基づくSQL
* - 空きが発生している「駐輪場ID」「駐輪分類ID」「車種区分ID」で空き待ちしている利用者を抽出する
* - reserve表 + user表のみ仕様書準拠
* - JOIN条件T1.user_id = T2.user_seq重要
*
* @param int $parkId 駐輪場IDJOB1.駐輪場ID
* @param int $psectionId 車種区分IDJOB1.車種区分ID
* @param int $ptypeId 駐輪分類IDJOB1.駐輪分類ID
* @return array 空き待ち者情報リスト
*/
private function getWaitingUsersInfo(int $parkId, int $psectionId, int $ptypeId): array
{
try {
// 仕様書JOB2 SQL準拠reserve + user のみ
$waitingUsers = DB::table('reserve as T1')
->select([
'T1.user_id', // 利用者ID
'T1.reserve_id', // 定期予約ID
'T2.user_name', // 利用者名
'T2.user_manual_regist_flag', // 手動登録フラグ
'T2.user_primemail', // メールアドレス
'T2.user_submail', // 予備メールアドレス
'T1.reserve_manual', // 手動通知
// 以下は処理に必要な追加フィールド
'T1.park_id',
'T1.psection_id',
'T1.ptype_id',
'T1.reserve_date',
])
// 仕様書準拠T1.user_id = T2.user_sequser表のPK
->join('user as T2', 'T1.user_id', '=', 'T2.user_seq')
->where([
['T1.park_id', '=', $parkId], // JOB1.駐輪場ID
['T1.psection_id', '=', $psectionId], // JOB1.車種区分ID
['T1.ptype_id', '=', $ptypeId], // JOB1.駐輪分類ID
['T1.valid_flag', '=', 1], // 有効フラグ = 1
['T2.user_quit_flag', '<>', 1] // 退会フラグ <> 1
])
->whereNull('T1.contract_id') // 定期契約 is null
->orderBy('T1.reserve_date', 'asc') // 予約日時順
->get()
->toArray();
Log::info('空き待ち者情報取得完了仕様書JOB2準拠', [
'park_id' => $parkId,
'psection_id' => $psectionId,
'ptype_id' => $ptypeId,
'waiting_users_count' => count($waitingUsers)
]);
return $waitingUsers;
} catch (Exception $e) {
Log::error('空き待ち者情報取得エラー', [
'park_id' => $parkId,
'psection_id' => $psectionId,
'ptype_id' => $ptypeId,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 【処理3】空き待ち者への通知、またはオペレーターキュー追加処理
*
* 仕様書に基づく分岐処理:
* - 手動通知フラグ判定reserve_manual
* - メール送信成功時のreserve.sent_date更新
* - 失敗時のオペレーターキュー追加(最終統計で処理)
*
* @param array $waitingUsers 空き待ち者リスト
* @param object $parkVacancy 駐輪場空き情報
* @return array 通知処理結果
*/
private function processWaitingUsersNotification(array $waitingUsers, object $parkVacancy): array
{
$notificationSuccessCount = 0;
$operatorQueueCount = 0;
$errors = [];
$queueItems = []; // オペレーターキュー作成用データ収集
try {
// 空きがある分だけ処理(先着順)
$availableSpots = min($parkVacancy->vacant_count, count($waitingUsers));
for ($i = 0; $i < $availableSpots; $i++) {
$waitingUserData = $waitingUsers[$i];
// 配列をオブジェクトに変換
$waitingUser = (object) $waitingUserData;
try {
// 【仕様判断】手動通知フラグチェック
if ($waitingUser->reserve_manual == 1) {
// 手動通知 → オペレーターキュー作成データ収集
$batchComment = '手動通知フラグ設定のため予約ID:' . $waitingUser->reserve_id;
$queueItems[] = [
'waiting_user' => $waitingUser,
'park_vacancy' => $parkVacancy,
'batch_comment' => $batchComment
];
$operatorQueueCount++;
Log::info('手動通知フラグによりオペレーターキュー登録予定', [
'user_id' => $waitingUser->user_id,
'reserve_id' => $waitingUser->reserve_id
]);
} else {
// 自動通知 → メール送信を試行
$mailResult = $this->sendVacancyNotificationMail($waitingUser, $parkVacancy);
if ($mailResult['success']) {
// メール送信成功 → reserve.sent_date更新
$this->updateReserveSentDate($waitingUser->reserve_id);
$notificationSuccessCount++;
Log::info('空き待ち通知メール送信成功', [
'user_id' => $waitingUser->user_id,
'reserve_id' => $waitingUser->reserve_id,
'park_id' => $parkVacancy->park_id
]);
} else {
// メール送信失敗 → オペレーターキュー作成データ収集
$shjSevenError = $mailResult['error'] ?? $mailResult['message'] ?? 'SHJ-7メール送信エラー';
$batchComment = $shjSevenError . '予約ID:' . $waitingUser->reserve_id;
$queueItems[] = [
'waiting_user' => $waitingUser,
'park_vacancy' => $parkVacancy,
'batch_comment' => $batchComment
];
$operatorQueueCount++;
$errors[] = $shjSevenError;
}
}
} catch (Exception $e) {
Log::error('空き待ち者通知処理エラー', [
'user_id' => $waitingUser->user_id,
'reserve_id' => $waitingUser->reserve_id,
'error' => $e->getMessage()
]);
// エラー発生時もオペレーターキュー作成データ収集
$batchComment = 'システムエラー:' . $e->getMessage() . '予約ID:' . $waitingUser->reserve_id;
$queueItems[] = [
'waiting_user' => $waitingUser,
'park_vacancy' => $parkVacancy,
'batch_comment' => $batchComment
];
$operatorQueueCount++;
$errors[] = $e->getMessage();
}
}
return [
'notification_success_count' => $notificationSuccessCount,
'operator_queue_count' => $operatorQueueCount,
'errors' => $errors,
'queue_items' => $queueItems // 後でキュー作成用
];
} catch (Exception $e) {
Log::error('空き待ち者通知処理全体エラー', [
'park_id' => $parkVacancy->park_id,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 空き待ち通知メールを送信
*
* 仕様書JOB3に基づくSHJ-7呼び出し:
* - パラメータ1: JOB1.メールアドレスuser_primemail
* - パラメータ2: JOB1.予備メールアドレスuser_submail
* - パラメータ3: メールID = 201
* - 追加: reserve_id=%reserve_id%&expiry=%expiry%予約ID 無効日)
*
* @param object $waitingUser 空き待ち者情報
* @param object $parkVacancy 駐輪場空き情報
* @return array 送信結果
*/
private function sendVacancyNotificationMail(object $waitingUser, object $parkVacancy): array
{
try {
// ShjMailSendServiceを利用してメール送信
$mailService = app(ShjMailSendService::class);
// 仕様書JOB3準拠メールID = 201
$mailTemplateId = 201;
// 仕様書No1/No2に基づく主メール・副メール設定
$mainEmail = $waitingUser->user_primemail ?? '';
$subEmail = $waitingUser->user_submail ?? '';
// 仕様書準拠reserve_id と expiry当月末日、休日考慮しないを追加
$expiry = now()->endOfMonth()->format('Y-m-d'); // 当月末日
$additionalParams = [
'reserve_id' => $waitingUser->reserve_id,
'expiry' => $expiry
];
// メール送信実行仕様書JOB3準拠
$mailResult = $mailService->executeMailSend(
$mainEmail,
$subEmail,
$mailTemplateId,
$additionalParams
);
// SHJ-7の結果を標準形式に変換result: 0=成功, 1=失敗)
$success = ($mailResult['result'] ?? 1) === 0;
Log::info('空き待ち通知メール送信試行完了', [
'user_id' => $waitingUser->user_id,
'main_email' => $mainEmail,
'sub_email' => $subEmail,
'mail_template_id' => $mailTemplateId,
'result' => $mailResult['result'] ?? 1,
'success' => $success
]);
return [
'success' => $success,
'result' => $mailResult['result'] ?? 1,
'error' => $mailResult['error_info'] ?? null,
'message' => $success ? 'メール送信成功' : ($mailResult['error_info'] ?? 'メール送信失敗')
];
} catch (Exception $e) {
Log::error('空き待ち通知メール送信エラー', [
'user_id' => $waitingUser->user_id,
'main_email' => $waitingUser->user_primemail ?? '',
'sub_email' => $waitingUser->user_submail ?? '',
'error' => $e->getMessage()
]);
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* reserve.sent_date及びvalid_flag更新
*
* 仕様書準拠メール送信成功時にreserve.sent_dateとvalid_flag=0を同時更新
* 重複通知を防ぎ、処理済みマークを設定
*
* @param int $reserveId 予約ID
* @return void
*/
private function updateReserveSentDate(int $reserveId): void
{
try {
DB::table('reserve')
->where('reserve_id', $reserveId)
->update([
'sent_date' => now()->format('Y-m-d H:i:s'),
'valid_flag' => 0, // 仕様書メール送信成功時に0に更新
'updated_at' => now()
]);
Log::info('reserve.sent_date及びvalid_flag更新完了', [
'reserve_id' => $reserveId,
'sent_date' => now()->format('Y-m-d H:i:s'),
'valid_flag' => 0
]);
} catch (Exception $e) {
Log::error('reserve.sent_date及びvalid_flag更新エラー', [
'reserve_id' => $reserveId,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* オペレーターキューに追加
*
* 仕様書に基づくキュー登録:
* - que_comment: 空文字列
* - que_status_comment: 仕様書完全準拠形式(統計情報含む)
* - operator_id: 9999999固定
*
* @param object $waitingUser 空き待ち者情報
* @param object $parkVacancy 駐輪場空き情報
* @param string $batchComment 内部変数.バッチコメント
* @param int $mailSuccessCount メール正常終了件数
* @param int $queueSuccessCount キュー登録正常終了件数
* @param int $mailErrorCount メール異常終了件数
* @param int $queueErrorCount キュー登録異常終了件数
* @return array 追加結果
*/
private function addToOperatorQueue(object $waitingUser, object $parkVacancy, string $batchComment, int $mailSuccessCount, int $queueSuccessCount, int $mailErrorCount, int $queueErrorCount): array
{
try {
// 仕様書完全準拠駐輪場名駐輪分類名車種区分名空き台数…対象予約ID…内部変数.バッチコメント/内部変数.メール正常終了件数…メール異常終了件数…キュー登録正常終了件数…キュー登録異常終了件数…
$statusComment = sprintf(
'%s%s%s空き台数%d台対象予約ID%d%sメール正常終了件数%dメール異常終了件数%dキュー登録正常終了件数%dキュー登録異常終了件数%d',
$parkVacancy->park_name ?? '', // JOB1から取得
$parkVacancy->ptype_subject ?? '', // 駐輪分類名JOB1から取得
$parkVacancy->psection_subject ?? '', // 車種区分名JOB1から取得
$parkVacancy->vacant_count ?? 0,
$waitingUser->reserve_id ?? 0,
$batchComment, // 内部変数.バッチコメント
$mailSuccessCount, // 内部変数.メール正常終了件数
$mailErrorCount,
$queueSuccessCount,
$queueErrorCount
);
OperatorQue::create([
'que_class' => 4, // 予約告知通知
'user_id' => $waitingUser->user_id,
'contract_id' => null,
'park_id' => $waitingUser->park_id,
'que_comment' => '', // 仕様書:空文字列
'que_status' => 1, // キュー発生
'que_status_comment' => $statusComment, // 仕様書:完全準拠形式
'work_instructions' => '空き待ち者への連絡をお願いします。',
'operator_id' => 9999999, // 仕様書固定値9999999
]);
Log::info('オペレーターキュー追加成功', [
'user_id' => $waitingUser->user_id,
'park_id' => $waitingUser->park_id,
'reserve_id' => $waitingUser->reserve_id,
'que_class' => 4,
'operator_id' => 9999999,
'batch_comment' => $batchComment,
'mail_success_count' => $mailSuccessCount,
'mail_error_count' => $mailErrorCount,
'queue_success_count' => $queueSuccessCount,
'queue_error_count' => $queueErrorCount,
'status_comment' => $statusComment
]);
return ['success' => true];
} catch (Exception $e) {
Log::error('オペレーターキュー追加エラー', [
'user_id' => $waitingUser->user_id,
'park_id' => $waitingUser->park_id,
'reserve_id' => $waitingUser->reserve_id ?? null,
'batch_comment' => $batchComment,
'error' => $e->getMessage()
]);
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,418 @@
<?php
namespace App\Services;
use App\Models\Park;
use App\Models\RegularContract;
use App\Models\Device;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
/**
* SHJ-4C 室割当処理サービス
*
* ゾーン情報取得及び割当処理を実行するビジネスロジック
* バッチ処理「SHJ-4C室割当」の核となる処理を担当
*/
class ShjFourCService
{
/**
* Park モデル
*
* @var Park
*/
protected $parkModel;
/**
* RegularContract モデル
*
* @var RegularContract
*/
protected $contractModel;
/**
* ShjEightService
*
* @var ShjEightService
*/
protected $shjEightService;
/**
* コンストラクタ
*
* @param Park $parkModel
* @param RegularContract $contractModel
* @param ShjEightService $shjEightService
*/
public function __construct(
Park $parkModel,
RegularContract $contractModel,
ShjEightService $shjEightService
) {
$this->parkModel = $parkModel;
$this->contractModel = $contractModel;
$this->shjEightService = $shjEightService;
}
/**
* SHJ-4C 室割当処理メイン実行
*
* 処理フロー:
* 【処理1】ゾーン情報取得
* 【判断1】割当判定
* 【処理2】バッチログ作成
* 【処理3】処理結果返却
*
* @param int $parkId 駐輪場ID
* @param int $ptypeId 駐輪分類ID
* @param int $psectionId 車種区分ID
* @return array 処理結果
*/
public function executeRoomAllocation(int $parkId, int $ptypeId, int $psectionId): array
{
$statusComment = '';
$status = 'success';
try {
Log::info('SHJ-4C 室割当処理開始', [
'park_id' => $parkId,
'ptype_id' => $ptypeId,
'psection_id' => $psectionId
]);
// 【処理1】ゾーン情報取得
$zoneInfo = $this->getZoneInformation($parkId, $ptypeId, $psectionId);
if (empty($zoneInfo)) {
$message = '対象のゾーン情報が見つかりません';
$status = 'error';
$statusComment = sprintf('エラー: %s (park_id:%d, ptype_id:%d, psection_id:%d)',
$message, $parkId, $ptypeId, $psectionId);
// バッチログ作成
$this->createBatchLog($status, $statusComment);
// JOB3: ゾーンID, 車室番号, 異常情報を返却
return [
'success' => false,
'zone_id' => null,
'pplace_no' => null,
'error_info' => $message
];
}
// 【判断1】割当判定処理
$allocationResult = $this->performAllocationJudgment($zoneInfo, $parkId, $ptypeId, $psectionId);
if (!$allocationResult['can_allocate']) {
// 割当NGの場合、対象事室番号を設定
$this->setTargetRoomNumber($allocationResult['target_room_number']);
$status = 'warning';
$statusComment = sprintf('割当NG: %s (park_id:%d, ptype_id:%d, psection_id:%d)',
$allocationResult['reason'], $parkId, $ptypeId, $psectionId);
// バッチログ作成
$this->createBatchLog($status, $statusComment);
// JOB3: ゾーンID, 車室番号, 異常情報を返却割当NG = 空き車室なし)
return [
'success' => true,
'zone_id' => null,
'pplace_no' => null,
'error_info' => $allocationResult['reason']
];
}
// 【処理2】バッチログ作成
$statusComment = sprintf('室割当処理完了 (park_id:%d, ptype_id:%d, psection_id:%d, zone_id:%d, pplace_no:%d)',
$parkId, $ptypeId, $psectionId, $allocationResult['zone_id'], $allocationResult['pplace_no']);
$this->createBatchLog($status, $statusComment);
Log::info('SHJ-4C 室割当処理完了', [
'zone_id' => $allocationResult['zone_id'],
'pplace_no' => $allocationResult['pplace_no']
]);
// 【処理3】処理結果返却
// JOB3: ゾーンID, 車室番号, 異常情報を返却
return [
'success' => true,
'message' => 'SHJ-4C 室割当処理が正常に完了しました',
'zone_id' => $allocationResult['zone_id'],
'pplace_no' => $allocationResult['pplace_no'],
'error_info' => null
];
} catch (\Exception $e) {
$errorMessage = 'SHJ-4C 室割当処理でエラーが発生: ' . $e->getMessage();
$status = 'error';
$statusComment = sprintf('例外エラー: %s (park_id:%d, ptype_id:%d, psection_id:%d)',
$e->getMessage(), $parkId, $ptypeId, $psectionId);
// バッチログ作成
$this->createBatchLog($status, $statusComment);
Log::error('SHJ-4C 室割当処理エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// JOB3: ゾーンID, 車室番号, 異常情報を返却
return [
'success' => false,
'zone_id' => null,
'pplace_no' => null,
'error_info' => $errorMessage
];
}
}
/**
* SHJ-8バッチ処理ログ作成
*
* @param string $status ステータス
* @param string $statusComment ステータスコメント
* @return void
*/
private function createBatchLog(string $status, string $statusComment): void
{
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
Log::info('SHJ-8バッチ処理ログ作成', [
'device_id' => $deviceId,
'process_name' => 'SHJ-4C',
'job_name' => 'SHJ-4C室割当',
'status' => $status,
'status_comment' => $statusComment
]);
// SHJ-8サービスを呼び出し
$this->shjEightService->execute(
$deviceId,
'SHJ-4C',
'SHJ-4C室割当',
$status,
$statusComment,
$today,
$today
);
} catch (\Exception $e) {
Log::error('SHJ-8 バッチログ作成エラー', [
'error' => $e->getMessage()
]);
}
}
/**
* 【処理1】ゾーン情報取得
*
* 駐輪場ID、駐輪分類ID、車種区分IDに紐づくゾーン情報を取得する
* SQLクエリは設計書の仕様に基づく
*
* @param int $parkId 駐輪場ID
* @param int $ptypeId 駐輪分類ID
* @param int $psectionId 車種区分ID
* @return array ゾーン情報
*/
private function getZoneInformation(int $parkId, int $ptypeId, int $psectionId): array
{
try {
// 設計書に記載されたSQLクエリに基づくゾーン情報取得
$zoneInfo = DB::table('zone as T1')
->select([
'T1.zone_id',
'T1.zone_name',
'T1.zone_number as zone_number',
'T1.zone_standard as zone_standard',
'T1.zone_tolerance as zone_tolerance',
'T1.zone_sort',
'T2.update_grace_period_start_date',
'T2.update_grace_period_end_date'
])
->join('park as T2', 'T1.park_id', '=', 'T2.park_id')
->where('T1.park_id', $parkId)
->where('T1.ptype_id', $ptypeId)
->where('T1.psection_id', $psectionId)
->where('T1.delete_flag', 0)
->orderBy('T1.zone_sort')
->get()
->toArray();
Log::info('ゾーン情報取得完了', [
'park_id' => $parkId,
'ptype_id' => $ptypeId,
'psection_id' => $psectionId,
'zone_count' => count($zoneInfo)
]);
return $zoneInfo;
} catch (\Exception $e) {
Log::error('ゾーン情報取得エラー', [
'park_id' => $parkId,
'ptype_id' => $ptypeId,
'psection_id' => $psectionId,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 【判断1】割当判定処理
*
* ゾーンIDごとにわか順インクリメント、内部変数.対象番号とする)に
* ゾーン内標準台数に達するまで、定期契約マスタから該当する車室番号で
* 契約済みの情報の有無を確認する
*
* @param array $zoneInfo ゾーン情報
* @param int $parkId 駐輪場ID
* @param int $ptypeId 駐輪分類ID
* @param int $psectionId 車種区分ID
* @return array 割当判定結果
*/
private function performAllocationJudgment(array $zoneInfo, int $parkId, int $ptypeId, int $psectionId): array
{
try {
foreach ($zoneInfo as $zone) {
// ゾーン内標準台数まで車室番号をインクリメントして検索
for ($pplaceNo = 1; $pplaceNo <= $zone->zone_standard; $pplaceNo++) {
// 該当する車室番号で契約済みの情報の有無を確認
$contractInfo = $this->getRegularContractInfo(
$parkId,
$zone->zone_id,
$pplaceNo,
(int) $zone->update_grace_period_start_date,
(int) $zone->update_grace_period_end_date
);
// 契約情報が存在しない場合、この車室番号は空き
if ($contractInfo === null) {
Log::info('SHJ-4C 割当OK', [
'zone_id' => $zone->zone_id,
'zone_name' => $zone->zone_name,
'pplace_no' => $pplaceNo,
]);
return [
'can_allocate' => true,
'zone_id' => $zone->zone_id,
'zone_name' => $zone->zone_name,
'pplace_no' => $pplaceNo,
'reason' => "割当OK: ゾーンID {$zone->zone_id}, 車室番号 {$pplaceNo}"
];
}
}
Log::info('SHJ-4C ゾーン満杯', [
'zone_id' => $zone->zone_id,
'zone_name' => $zone->zone_name,
]);
}
// 全ゾーンで割当NGの場合
$targetRoomNumber = $this->generateTargetRoomNumber($parkId, $ptypeId, $psectionId);
Log::warning('SHJ-4C 割当NG', [
'target_room_number' => $targetRoomNumber,
]);
return [
'can_allocate' => false,
'target_room_number' => $targetRoomNumber,
'reason' => "車室割り当てNG: " . $targetRoomNumber
];
} catch (\Exception $e) {
Log::error('割当判定処理エラー', [
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 定期契約情報取得
*
* 設計書のSQLクエリに基づく定期契約マスタ検索
* 特定の車室番号で契約済みの情報の有無を確認する
*
* @param int $parkId 駐輪場ID
* @param int $zoneId ゾーンID
* @param int $pplaceNo 車室番号(対象番号)
* @param int $updateGracePeriodStartDate 更新期間開始日
* @param int $updateGracePeriodEndDate 更新期間終了日
* @return array|null 定期契約情報存在しない場合はnull
*/
private function getRegularContractInfo(
int $parkId,
int $zoneId,
int $pplaceNo,
int $updateGracePeriodStartDate,
int $updateGracePeriodEndDate
): ?array {
$currentDate = Carbon::now()->format('y.%m.%d');
$query = DB::table('regular_contract as T1')
->select(['T1.contract_id'])
->where('T1.park_id', $parkId)
->where('T1.zone_id', $zoneId)
->where('T1.pplace_no', $pplaceNo);
// 【「JOB1.更新期間開始日」<=「JOB1. 更新期間終了日」の場合】
if ($updateGracePeriodStartDate <= $updateGracePeriodEndDate) {
// パターンA: T1.有効期間E >= date_format(now(), '%y.%m.%d')
$query->whereRaw("date_format(T1.contract_periode, '%y.%m.%d') >= ?", [$currentDate]);
} else {
// 【その他の場合】
// パターンB: T1.有効期間E + 「JOB1. 更新期間終了日」>= date_format(now(), '%y.%m.%d')
$query->whereRaw("DATE_ADD(T1.contract_periode, INTERVAL ? DAY) >= ?", [$updateGracePeriodEndDate, $currentDate]);
}
$query->where('T1.contract_flag', 1);
$result = $query->first();
return $result ? (array) $result : null;
}
/**
* 対象事室番号生成
*
* @param int $parkId 駐輪場ID
* @param int $ptypeId 駐輪分類ID
* @param int $psectionId 車種区分ID
* @return string 対象事室番号
*/
private function generateTargetRoomNumber(int $parkId, int $ptypeId, int $psectionId): string
{
return sprintf('%d_%d_%d', $parkId, $ptypeId, $psectionId);
}
/**
* 対象事室番号設定
*
* @param string $targetRoomNumber 対象事室番号
* @return void
*/
private function setTargetRoomNumber(string $targetRoomNumber): void
{
Log::info('対象事室番号設定', [
'target_room_number' => $targetRoomNumber
]);
// 実際の事室番号設定ロジックをここに実装
// 具体的な仕様が必要な場合は後で追加実装
}
}

View File

@ -0,0 +1,666 @@
<?php
namespace App\Services;
use App\Models\MailTemplate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Carbon\Carbon;
/**
* SHJ メール送信処理サービス
*
* メールテンプレートを使用したメール送信処理を実行するビジネスロジック
* バッチ処理「SHJメール送信」の核となる処理を担当
*/
class ShjMailSendService
{
/**
* MailTemplate モデル
*
* @var MailTemplate
*/
protected $mailTemplateModel;
/**
* コンストラクタ
*
* @param MailTemplate $mailTemplateModel
*/
public function __construct(
MailTemplate $mailTemplateModel
) {
$this->mailTemplateModel = $mailTemplateModel;
}
/**
* SHJ-7 メール送信処理メイン実行
*
* 処理フロー:
* 【処理1】入力パラメーターをチェックする
* 【判断1】チェック結果
* 【処理2】メール送信テンプレート情報を取得する
* 【判断2】取得結果判定
* 【処理3】メールを送信する
* 【処理4】処理結果を返却する
*
* @param string $mailAddress メールアドレス
* @param string $backupMailAddress 予備メールアドレス
* @param int $mailTemplateId メールテンプレートID使用プログラムID
* @param array $additionalParams 追加パラメータreserve_id, expiryなど
* @return array 処理結果 ['result' => 0|1, 'error_info' => string]
*/
public function executeMailSend(string $mailAddress, string $backupMailAddress, int $mailTemplateId, array $additionalParams = []): array
{
try {
Log::info('SHJ-7 メール送信処理開始', [
'mail_address' => $mailAddress,
'backup_mail_address' => $backupMailAddress,
'mail_template_id' => $mailTemplateId,
'additional_params' => $additionalParams
]);
// 【処理1】入力パラメーターをチェックする
$paramCheckResult = $this->checkInputParameters($mailAddress, $backupMailAddress, $mailTemplateId);
// 【判断1】チェック結果
if (!$paramCheckResult['valid']) {
$errorInfo = $paramCheckResult['error_info'];
Log::warning('SHJ-7 パラメーターチェックNG', [
'error_info' => $errorInfo
]);
// 【処理4】処理結果を返却する異常終了
return [
'result' => 1,
'error_info' => $errorInfo
];
}
// 【処理2】メール送信テンプレート情報を取得する
$templateInfo = $this->getMailTemplateInfo($mailTemplateId);
// 【判断2】取得結果判定
if (empty($templateInfo)) {
// 0件の場合取得NGの結果を設定する
$errorInfo = "メール送信NGメール送信テンプレートが存在しません。/{$mailTemplateId}";
Log::warning('SHJ-7 メールテンプレート取得NG', [
'mail_template_id' => $mailTemplateId,
'error_info' => $errorInfo
]);
// 【処理4】処理結果を返却する異常終了
return [
'result' => 1,
'error_info' => $errorInfo
];
}
// 【処理3】メールを送信する
$mailSendResult = $this->sendMail($mailAddress, $backupMailAddress, $templateInfo);
if (!$mailSendResult['success']) {
$errorInfo = $mailSendResult['error_info'];
Log::error('SHJ-7 メール送信失敗', [
'error_info' => $errorInfo
]);
// 【処理4】処理結果を返却する異常終了
return [
'result' => 1,
'error_info' => $errorInfo
];
}
Log::info('SHJ-7 メール送信処理完了', [
'to_address' => $mailSendResult['to_address']
]);
// 【処理4】処理結果を返却する正常終了
return [
'result' => 0,
'error_info' => ''
];
} catch (\Exception $e) {
$errorInfo = 'メール送信NG' . $e->getMessage();
Log::error('SHJ-7 メール送信処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// 【処理4】処理結果を返却する異常終了
return [
'result' => 1,
'error_info' => $errorInfo
];
}
}
/**
* 【処理1】入力パラメーターをチェックする
*
* SHJ-7仕様書に基づくチェック内容:
* 1. メールアドレス: 「メールアドレス」「予備メールアドレス」いずれか必須
* 2. 予備メールアドレス: 「メールアドレス」「予備メールアドレス」いずれか必須
* 3. 使用プログラムID: 必須
*
* エラーメッセージ形式: "パラメーターNG: 項目名/入力値"
* 複数エラーの場合は全角カンマ(、)で連結
*
* @param string $mailAddress メールアドレス
* @param string $backupMailAddress 予備メールアドレス
* @param int $mailTemplateId メールテンプレートID使用プログラムID
* @return array チェック結果 ['valid' => bool, 'error_info' => string]
*/
private function checkInputParameters(string $mailAddress, string $backupMailAddress, int $mailTemplateId): array
{
$errors = [];
try {
// 1. メールアドレスと予備メールアドレスのいずれか必須チェック
// 両方とも空の場合は、それぞれ別エラーとして追加
if (empty($mailAddress) && empty($backupMailAddress)) {
$errors[] = "パラメーターNGメールアドレス/<空>";
$errors[] = "パラメーターNG予備メールアドレス/<空>";
} else {
// いずれかに値がある場合は、形式チェックを実施
// 2. メールアドレス形式チェック(値がある場合のみ)
if (!empty($mailAddress) && !filter_var($mailAddress, FILTER_VALIDATE_EMAIL)) {
$errors[] = "パラメーターNGメールアドレス/{$mailAddress}";
}
// 3. 予備メールアドレス形式チェック(値がある場合のみ)
if (!empty($backupMailAddress) && !filter_var($backupMailAddress, FILTER_VALIDATE_EMAIL)) {
$errors[] = "パラメーターNG予備メールアドレス/{$backupMailAddress}";
}
}
// 4. 使用プログラムIDメールテンプレートID必須チェック
if (empty($mailTemplateId) || $mailTemplateId <= 0) {
$errors[] = "パラメーターNG使用プログラムID/{$mailTemplateId}";
}
// エラーがある場合
if (!empty($errors)) {
// 複数のエラーがある場合は全角カンマ(、)で連結
$errorInfo = implode('、', $errors);
Log::warning('SHJ-7 入力パラメーターチェックNG', [
'mail_address' => $mailAddress,
'backup_mail_address' => $backupMailAddress,
'mail_template_id' => $mailTemplateId,
'error_info' => $errorInfo
]);
return [
'valid' => false,
'error_info' => $errorInfo
];
}
// 正常終了
Log::info('SHJ-7 入力パラメーターチェックOK', [
'mail_address' => $mailAddress,
'backup_mail_address' => $backupMailAddress,
'mail_template_id' => $mailTemplateId
]);
return [
'valid' => true,
'error_info' => ''
];
} catch (\Exception $e) {
$errorInfo = 'パラメーターチェック中にエラーが発生:' . $e->getMessage();
Log::error('SHJ-7 入力パラメーターチェック例外エラー', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'valid' => false,
'error_info' => $errorInfo
];
}
}
/**
* 【処理2】メール送信テンプレート情報を取得する
*
* SHJ-7仕様書に基づくSQLクエリ:
* SELECT エリアマネージャー同報, bccアドレス, 件名, 本文
* FROM メール送信テンプレート
* WHERE 使用プログラムID = 入力パラメーター.使用プログラムID
* AND 使用フラグ = 1
*
* @param int $mailTemplateId メールテンプレートID使用プログラムID
* @return MailTemplate|null メールテンプレート情報
*/
private function getMailTemplateInfo(int $mailTemplateId): ?MailTemplate
{
try {
// SHJ-7仕様: 使用プログラムIDpg_idで検索、使用フラグ=1
$templateInfo = $this->mailTemplateModel::where('pg_id', $mailTemplateId)
->where('use_flag', 1)
->first();
if ($templateInfo) {
Log::info('SHJ-7 メールテンプレート情報取得完了', [
'mail_template_id' => $mailTemplateId,
'template_found' => true,
'subject' => $templateInfo->getSubject(),
'mgr_cc_flag' => $templateInfo->isManagerCcEnabled(),
'has_bcc' => !empty($templateInfo->getBccAddress())
]);
} else {
Log::warning('SHJ-7 メールテンプレート情報取得結果0件', [
'mail_template_id' => $mailTemplateId,
'template_found' => false
]);
}
return $templateInfo;
} catch (\Exception $e) {
Log::error('SHJ-7 メールテンプレート情報取得エラー', [
'mail_template_id' => $mailTemplateId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* 【処理3】メールを送信する
*
* SHJ-7仕様書に基づくmb_send_mail関数使用:
* - 送信先: 入力パラメーター.メールアドレス(空なら予備メールアドレス)
* - タイトル: 処理2.件名
* - 本文: 処理2.本文
* - 追加ヘッダ: 処理2.bccアドレス※値が設定されている場合のみ
*
* ※待ち時間: 仕様では「一定の待ち時間を設ける」とあるが、現時点では実装しない0秒
*
* @param string $mailAddress メールアドレス
* @param string $backupMailAddress 予備メールアドレス
* @param MailTemplate $templateInfo テンプレート情報
* @return array 送信結果 ['success' => bool, 'error_info' => string, 'to_address' => string]
*/
private function sendMail(string $mailAddress, string $backupMailAddress, MailTemplate $templateInfo): array
{
try {
// 送信先アドレス決定(優先: メールアドレス、代替: 予備メールアドレス)
$toAddress = !empty($mailAddress) ? $mailAddress : $backupMailAddress;
// 処理2で取得したメール内容を取得
$subject = $templateInfo->getSubject() ?? '';
$message = $templateInfo->getText() ?? '';
// 追加ヘッダ設定BCC、From等
$headers = $this->buildMailHeaders($templateInfo);
Log::info('SHJ-7 メール送信準備完了', [
'to_address' => $toAddress,
'subject' => $subject,
'has_bcc' => !empty($templateInfo->getBccAddress()),
'mgr_cc_flag' => $templateInfo->isManagerCcEnabled()
]);
// mb_send_mail関数を使用してメール送信
$sendResult = mb_send_mail(
$toAddress, // 送信先
$subject, // 件名(タイトル)
$message, // 本文
$headers // 追加ヘッダ
);
if ($sendResult) {
Log::info('SHJ-7 メール送信成功', [
'to_address' => $toAddress,
'subject' => $subject
]);
return [
'success' => true,
'error_info' => '',
'to_address' => $toAddress
];
} else {
// mb_send_mail がfalseを返した場合
$errorInfo = 'メール送信NGmb_send_mail関数がfalseを返しました';
Log::error('SHJ-7 メール送信失敗', [
'to_address' => $toAddress,
'subject' => $subject,
'error_info' => $errorInfo
]);
return [
'success' => false,
'error_info' => $errorInfo
];
}
} catch (\Exception $e) {
$errorInfo = 'メール送信NG' . $e->getMessage();
Log::error('SHJ-7 メール送信例外エラー', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'error_info' => $errorInfo
];
}
}
/**
* メールヘッダを構築
*
* SHJ-7仕様書に基づく追加ヘッダ設定:
* - 追加ヘッダ: 処理2.bccアドレス※値が設定されている場合のみ
* - From: システム設定から取得
* - エリアマネージャー同報: フラグが有効な場合はログ出力のみ(実装保留)
*
* @param MailTemplate $templateInfo テンプレート情報
* @return string メールヘッダ
*/
private function buildMailHeaders(MailTemplate $templateInfo): string
{
$headers = [];
// BCCアドレス設定値が設定されている場合のみ追加
$bccAddress = $templateInfo->getBccAddress();
if (!empty($bccAddress)) {
$headers[] = "Bcc: {$bccAddress}";
}
// エリアマネージャー同報フラグが有効な場合
// ※SHJ-7では実装しない仕様詳細不明。ログ出力のみ。
if ($templateInfo->isManagerCcEnabled()) {
Log::info('SHJ-7 エリアマネージャー同報フラグが有効', [
'mail_template_id' => $templateInfo->mail_template_id
]);
// 実際のエリアマネージャーアドレス取得・設定処理は将来実装
}
// From設定システム設定から取得
$fromAddress = config('mail.from.address', 'noreply@so-manager.com');
$headers[] = "From: {$fromAddress}";
// Content-Type設定日本語対応
$headers[] = "Content-Type: text/plain; charset=UTF-8";
return implode("\r\n", $headers);
}
/**
* SHJ-12 未払い者通知メール送信
*
* SHJ-12から呼び出される専用メール送信メソッド
* 未払い者への通知メールを送信する
*
* @param array $mailParams メール送信パラメータ
* @return array 送信結果
*/
public function sendUnpaidNotificationMail(array $mailParams): array
{
try {
// パラメータ展開
$toEmail = $mailParams['to_email'] ?? '';
$userName = $mailParams['user_name'] ?? '';
$parkName = $mailParams['park_name'] ?? '';
$billingAmount = $mailParams['billing_amount'] ?? 0;
$contractId = $mailParams['contract_id'] ?? '';
// 未払い者通知専用のメールテンプレートID仮定: 1
// 実際の運用では設定ファイルまたはデータベースから取得
$mailTemplateId = 1;
// メールテンプレート情報取得
$templateInfo = $this->getMailTemplateInfo($mailTemplateId);
if (!$templateInfo) {
return [
'success' => false,
'message' => '未払い者通知用メールテンプレートが見つかりません'
];
}
// メール件名とメッセージのカスタマイズ
$subject = str_replace(
['{park_name}', '{user_name}'],
[$parkName, $userName],
$templateInfo->getSubject()
);
$message = str_replace(
['{user_name}', '{park_name}', '{billing_amount}', '{contract_id}'],
[$userName, $parkName, number_format($billingAmount), $contractId],
$templateInfo->getText()
);
// 追加ヘッダ設定
$headers = $this->buildMailHeaders($templateInfo);
Log::info('SHJ-12 未払い者通知メール送信準備完了', [
'to_email' => $toEmail,
'user_name' => $userName,
'billing_amount' => $billingAmount,
'contract_id' => $contractId
]);
// メール送信実行
$sendResult = mb_send_mail(
$toEmail, // 送信先
$subject, // 件名
$message, // 本文
$headers // 追加ヘッダ
);
if ($sendResult) {
Log::info('SHJ-12 未払い者通知メール送信成功', [
'to_email' => $toEmail,
'contract_id' => $contractId
]);
return [
'success' => true,
'message' => '未払い者通知メール送信完了'
];
} else {
$errorMessage = '未払い者通知メール送信に失敗しました';
Log::error('SHJ-12 未払い者通知メール送信失敗', [
'to_email' => $toEmail,
'contract_id' => $contractId,
'error' => $errorMessage
]);
return [
'success' => false,
'message' => $errorMessage
];
}
} catch (\Exception $e) {
$errorMessage = 'SHJ-12 未払い者通知メール送信エラー: ' . $e->getMessage();
Log::error('SHJ-12 未払い者通知メール送信エラー', [
'error' => $e->getMessage(),
'mail_params' => $mailParams
]);
return [
'success' => false,
'message' => $errorMessage
];
}
}
/**
* Laravel Mail を使用したメール送信(開発・テスト用)
*
* mb_send_mail() の代わりに Laravel Mail ファサードを使用
* .env MAIL_MAILER 設定smtp, log, array 等)に従って送信
*
* @param string $mailAddress メールアドレス
* @param string $backupMailAddress 予備メールアドレス
* @param int $mailTemplateId メールテンプレートID使用プログラムID
* @return array 処理結果 ['result' => 0|1, 'error_info' => string]
*/
public function executeMailSendWithLaravel(string $mailAddress, string $backupMailAddress, int $mailTemplateId): array
{
try {
Log::info('SHJ-7 メール送信処理開始Laravel Mail使用', [
'mail_address' => $mailAddress,
'backup_mail_address' => $backupMailAddress,
'mail_template_id' => $mailTemplateId
]);
// 【処理1】入力パラメーターをチェックする
$paramCheckResult = $this->checkInputParameters($mailAddress, $backupMailAddress, $mailTemplateId);
// 【判断1】チェック結果
if (!$paramCheckResult['valid']) {
$errorInfo = $paramCheckResult['error_info'];
Log::warning('SHJ-7 パラメーターチェックNG', [
'error_info' => $errorInfo
]);
return [
'result' => 1,
'error_info' => $errorInfo
];
}
// 【処理2】メール送信テンプレート情報を取得する
$templateInfo = $this->getMailTemplateInfo($mailTemplateId);
// 【判断2】取得結果判定
if (empty($templateInfo)) {
$errorInfo = "メール送信NGメール送信テンプレートが存在しません。/{$mailTemplateId}";
Log::warning('SHJ-7 メールテンプレート取得NG', [
'mail_template_id' => $mailTemplateId,
'error_info' => $errorInfo
]);
return [
'result' => 1,
'error_info' => $errorInfo
];
}
// 【処理3】Laravel Mail を使用してメールを送信する
$mailSendResult = $this->sendMailWithLaravel($mailAddress, $backupMailAddress, $templateInfo);
if (!$mailSendResult['success']) {
$errorInfo = $mailSendResult['error_info'];
Log::error('SHJ-7 メール送信失敗Laravel Mail', [
'error_info' => $errorInfo
]);
return [
'result' => 1,
'error_info' => $errorInfo
];
}
Log::info('SHJ-7 メール送信処理完了Laravel Mail', [
'to_address' => $mailSendResult['to_address']
]);
// 【処理4】処理結果を返却する正常終了
return [
'result' => 0,
'error_info' => ''
];
} catch (\Exception $e) {
$errorInfo = 'メール送信NG' . $e->getMessage();
Log::error('SHJ-7 メール送信処理例外エラーLaravel Mail', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'result' => 1,
'error_info' => $errorInfo
];
}
}
/**
* Laravel Mail ファサードを使用してメールを送信
*
* @param string $mailAddress メールアドレス
* @param string $backupMailAddress 予備メールアドレス
* @param MailTemplate $templateInfo テンプレート情報
* @return array 送信結果 ['success' => bool, 'error_info' => string, 'to_address' => string]
*/
private function sendMailWithLaravel(string $mailAddress, string $backupMailAddress, MailTemplate $templateInfo): array
{
try {
// 送信先アドレス決定(優先: メールアドレス、代替: 予備メールアドレス)
$toAddress = !empty($mailAddress) ? $mailAddress : $backupMailAddress;
// メール内容取得
$subject = $templateInfo->getSubject() ?? '';
$message = $templateInfo->getText() ?? '';
$bccAddress = $templateInfo->getBccAddress();
Log::info('SHJ-7 メール送信準備完了Laravel Mail', [
'to_address' => $toAddress,
'subject' => $subject,
'has_bcc' => !empty($bccAddress),
'mgr_cc_flag' => $templateInfo->isManagerCcEnabled()
]);
// Laravel Mail で送信
Mail::raw($message, function ($mail) use ($toAddress, $subject, $bccAddress) {
$mail->to($toAddress)
->subject($subject);
// BCC設定値がある場合のみ
if (!empty($bccAddress)) {
$mail->bcc($bccAddress);
}
});
Log::info('SHJ-7 メール送信成功Laravel Mail', [
'to_address' => $toAddress,
'subject' => $subject
]);
return [
'success' => true,
'error_info' => '',
'to_address' => $toAddress
];
} catch (\Exception $e) {
$errorInfo = 'メール送信NGLaravel Mail' . $e->getMessage();
Log::error('SHJ-7 メール送信例外エラーLaravel Mail', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'error_info' => $errorInfo
];
}
}
}

View File

@ -0,0 +1,814 @@
<?php
namespace App\Services;
use App\Models\Park;
use App\Models\EarningsSummary;
use App\Models\Psection;
use App\Models\OperatorQue;
use App\Models\Device;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
/**
* SHJ-9 売上集計処理サービス
*
* 日次の売上集計処理を実行するビジネスロジック
* バッチ処理「SHJ-9売上集計(日次)」の核となる処理を担当
*/
class ShjNineService
{
/**
* バッチ実行者の固定オペレータID
*/
const BATCH_OPERATOR_ID = 9999999;
/**
* 日次集計の summary_type
*/
const SUMMARY_TYPE_DAILY = 3;
/**
* ShjEightService インスタンス
*
* @var ShjEightService
*/
protected $shjEightService;
/**
* コンストラクタ
*
* @param ShjEightService $shjEightService
*/
public function __construct(ShjEightService $shjEightService)
{
$this->shjEightService = $shjEightService;
}
/**
* SHJ-9 売上集計処理メイン実行(日次のみ)
*
* 処理フロー (todo/SHJ-9/SHJ-9.txt):
* 【処理1】集計対象を設定する
* 【処理2】駐輪場マスタを取得する
* 【判断1】取得件数判定
* 【処理3】車種区分毎に算出する
* 【判断2】取得判定
* 【処理4】売上集計結果を削除→登録する
* 【処理5】オペレータキュー作成およびバッチ処理ログを作成する
*
* @param string $type 集計種別daily 固定)
* @param string $aggregationDate 集計対象日YYYY-MM-DD
* @return array 処理結果
*/
public function executeEarningsAggregation(string $type, string $aggregationDate): array
{
$statusComments = []; // 内部変数.ステータスコメント
$dataIntegrityIssues = []; // 内部変数.情報不備
try {
// 【処理1】集計対象を設定する
// パラメーター検証(日付形式チェック)
if (!$this->isValidDateFormat($aggregationDate)) {
// 日付形式エラー時は【処理】へwarning扱い
$statusComment = "売上集計(日次):パラメーターが不正です。(日付形式ではありません)";
// 【処理5】オペレータキュー作成
$this->createOperatorQueue($statusComment, null);
// SHJ-8 バッチ処理ログ作成
$this->callShjEight('SHJ-9売上集計日次', 'success', $statusComment);
return [
'success' => true, // 仕様上はwarningで成功扱い
'message' => $statusComment
];
}
$targetDate = Carbon::parse($aggregationDate)->format('Y-m-d');
Log::info('SHJ-9 売上集計処理開始', [
'type' => $type,
'target_date' => $targetDate
]);
// 【処理2】駐輪場マスタを取得する
$parkInfo = $this->getParkInformation();
// 【判断1】取得件数判定
if (empty($parkInfo)) {
$statusComment = '売上集計(日次):駐輪場マスタが存在していません。';
$statusComments[] = $statusComment;
// 【処理5】オペレータキュー作成
$this->createOperatorQueue($statusComment, null);
// SHJ-8 バッチ処理ログ作成
$this->callShjEight('SHJ-9売上集計日次', 'success', $statusComment);
return [
'success' => true,
'message' => $statusComment,
'processed_parks' => 0,
'summary_records' => 0
];
}
// 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する
$summaryRecords = 0;
$processedParks = 0;
foreach ($parkInfo as $park) {
$result = $this->processEarningsForPark($park, $targetDate);
$processedParks++;
$summaryRecords += $result['summary_records'];
// 対象データなしの場合のステータスコメント収集
if (!empty($result['no_data_message'])) {
$statusComments[] = $result['no_data_message'];
}
// 情報不備を収集("なし"でない場合)
if ($result['data_integrity_issue'] !== '情報不備:なし') {
$dataIntegrityIssues[] = $result['data_integrity_issue'];
}
}
// 最終ステータスコメント生成
$finalStatusComment = "売上集計(日次):対象日={$targetDate}、駐輪場数={$processedParks}、集計レコード数={$summaryRecords}";
if (!empty($dataIntegrityIssues)) {
$finalStatusComment .= "、情報不備=" . implode('、', $dataIntegrityIssues);
}
// 【処理5】オペレータキュー作成
// ※ 駐輪場単位で既に作成済みprocessEarningsForPark内で情報不備検出時に実施
if (!empty($dataIntegrityIssues)) {
Log::warning('SHJ-9 情報不備検出', [
'issues' => $dataIntegrityIssues
]);
}
// SHJ-8 バッチ処理ログ作成
$this->callShjEight('SHJ-9売上集計日次', 'success', $finalStatusComment);
Log::info('SHJ-9 売上集計処理完了', [
'processed_parks' => $processedParks,
'summary_records' => $summaryRecords,
'data_integrity_issues' => count($dataIntegrityIssues),
'no_data_parks' => count($statusComments)
]);
return [
'success' => true,
'message' => 'SHJ-9 売上集計処理が正常に完了しました',
'processed_parks' => $processedParks,
'summary_records' => $summaryRecords,
'data_integrity_issues' => count($dataIntegrityIssues)
];
} catch (\Exception $e) {
$errorMessage = '売上集計(日次):エラー発生 - ' . $e->getMessage();
// SHJ-8 バッチ処理ログ作成(エラー時も作成)
try {
$this->callShjEight('SHJ-9売上集計日次', 'error', $errorMessage);
} catch (\Exception $shjException) {
Log::error('SHJ-8呼び出しエラー', ['error' => $shjException->getMessage()]);
}
Log::error('SHJ-9 売上集計処理エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'message' => $errorMessage,
'details' => $e->getMessage()
];
}
}
/**
* 日付形式の検証
*
* @param string $date 日付文字列
* @return bool 有効な日付形式かどうか
*/
private function isValidDateFormat(string $date): bool
{
try {
$parsed = Carbon::parse($date);
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* 【処理2】駐輪場マスタを取得する
*
* @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 string $targetDate 集計対象日YYYY-MM-DD
* @return array 処理結果 ['summary_records' => int, 'data_integrity_issue' => string, 'no_data_message' => string|null]
*/
private function processEarningsForPark($park, string $targetDate): array
{
try {
// 0. 情報不備チェック
$dataIntegrityIssue = $this->checkDataIntegrity($park->park_id, $targetDate);
// 情報不備がある場合、駐輪場単位でオペレータキュー作成(仕様 todo/SHJ-9/SHJ-9.txt:253-263
if ($dataIntegrityIssue !== '情報不備:なし') {
$this->createOperatorQueue($dataIntegrityIssue, $park->park_id);
}
// ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎)
$regularData = $this->calculateRegularEarnings($park->park_id, $targetDate);
// ② 一時金データ取得(車種毎)
$lumpsumData = $this->calculateLumpsumEarnings($park->park_id, $targetDate);
// ③ 解約返戻金データ取得(車種区分毎)
$refundData = $this->calculateRefundEarnings($park->park_id, $targetDate);
// ④ 再発行データ取得(車種区分毎)
$reissueData = $this->calculateReissueCount($park->park_id, $targetDate);
// 【判断2】データがいずれかあれば【処理4】へ
if (empty($regularData) && empty($lumpsumData) && empty($refundData) && empty($reissueData)) {
// 対象データなし - 仕様 todo/SHJ-9/SHJ-9.txt:172-175
$noDataMessage = "売上集計(日次):対象日:{$targetDate}/駐輪場:{$park->park_name}:売上データが存在しません。";
return [
'summary_records' => 0,
'data_integrity_issue' => $dataIntegrityIssue,
'no_data_message' => $noDataMessage
];
}
// 【処理4】既存の売上集計結果を削除
$this->deleteExistingSummary($park->park_id, $targetDate);
// 【処理4】売上集計結果を登録
$summaryRecords = 0;
// ① 定期契約データがある場合同じ組合せpsection×usertype×monthsを統合
$mergedRegularData = $this->mergeRegularDataByGroup($regularData);
foreach ($mergedRegularData as $key => $mergedRow) {
$this->createEarningsSummary($park, $mergedRow, $targetDate, 'regular');
$summaryRecords++;
}
// ②③④ 一時金・解約・再発行データがある場合(車種区分毎に集約)
$otherDataByPsection = $this->mergeOtherEarningsData($lumpsumData, $refundData, $reissueData);
foreach ($otherDataByPsection as $psectionId => $data) {
$this->createEarningsSummary($park, $data, $targetDate, 'other');
$summaryRecords++;
}
Log::info('駐輪場売上集計完了', [
'park_id' => $park->park_id,
'park_name' => $park->park_name,
'summary_records' => $summaryRecords,
'data_integrity_issue' => $dataIntegrityIssue
]);
return [
'summary_records' => $summaryRecords,
'data_integrity_issue' => $dataIntegrityIssue,
'no_data_message' => null
];
} catch (\Exception $e) {
Log::error('駐輪場売上集計エラー', [
'park_id' => $park->park_id,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 0. 情報不備チェック
*
* 仕様 todo/SHJ-9/SHJ-9.txt:44-68
*
* @param int $parkId 駐輪場ID
* @param string $targetDate 集計対象日YYYY-MM-DD
* @return string 情報不備メッセージ(仕様フォーマット:"情報不備xxx" or "情報不備:なし"
*/
private function checkDataIntegrity(int $parkId, string $targetDate): string
{
$incompleteContracts = DB::table('regular_contract')
->select('contract_id')
->where('park_id', $parkId)
->where('contract_flag', 1)
->whereDate('contract_payment_day', '=', $targetDate)
->where(function($query) {
$query->whereNull('update_flag')
->orWhereNull('psection_id')
->orWhereNull('enable_months');
})
->pluck('contract_id')
->toArray();
if (empty($incompleteContracts)) {
return '情報不備:なし';
}
// 仕様フォーマット:"情報不備:" + 契約IDカンマ区切り
return '情報不備:' . implode(',', $incompleteContracts);
}
/**
* 定期契約データ取得(車種区分・分類名1・定期有効月数毎)
*
* 仕様 todo/SHJ-9/SHJ-9.txt:70-95
* SQL定義減免措置・継続フラグ・車種区分・分類名・有効月数でグループ化し、
* 授受金額の合計と件数を算出する
*
* @param int $parkId 駐輪場ID
* @param string $targetDate 集計対象日YYYY-MM-DD
* @return array
*/
private function calculateRegularEarnings(int $parkId, string $targetDate): array
{
$results = DB::table('regular_contract as T1')
->join('usertype as T3', 'T1.user_categoryid', '=', 'T3.user_categoryid')
->select([
DB::raw('IFNULL(T1.contract_reduction, 0) as contract_reduction'),
'T1.update_flag',
'T1.psection_id',
'T3.usertype_subject1',
'T1.enable_months',
DB::raw('SUM(T1.contract_money) as total_amount'), // 仕様line 77: 授受金額の合計
DB::raw('COUNT(T1.contract_money) as contract_count') // 仕様line 78: 授受件数
])
->where('T1.park_id', $parkId)
->where('T1.contract_flag', 1)
->whereDate('T1.contract_payment_day', '=', $targetDate)
->whereNotNull('T1.update_flag')
->whereNotNull('T1.psection_id')
->whereNotNull('T1.enable_months')
->groupBy([
DB::raw('IFNULL(T1.contract_reduction, 0)'),
'T1.update_flag',
'T1.psection_id',
'T3.usertype_subject1',
'T1.enable_months'
])
->get();
return $results->toArray();
}
/**
* 一時金データ取得(車種毎)
*
* 仕様 todo/SHJ-9/SHJ-9.txt:114-125
*
* @param int $parkId 駐輪場ID
* @param string $targetDate 集計対象日YYYY-MM-DD
* @return array
*/
private function calculateLumpsumEarnings(int $parkId, string $targetDate): array
{
$results = DB::table('lumpsum_transaction')
->select([
'type_class as psection_id',
DB::raw('COUNT(*) as lumpsum_count'),
DB::raw('COALESCE(SUM(deposit_amount), 0) as lumpsum')
])
->where('park_id', $parkId)
->whereDate('pay_date', '=', $targetDate)
->groupBy('type_class')
->get();
return $results->toArray();
}
/**
* 解約返戻金データ取得(車種区分毎)
*
* 仕様 todo/SHJ-9/SHJ-9.txt:126-137
*
* @param int $parkId 駐輪場ID
* @param string $targetDate 集計対象日YYYY-MM-DD
* @return array
*/
private function calculateRefundEarnings(int $parkId, string $targetDate): array
{
$results = DB::table('regular_contract')
->select([
'psection_id',
DB::raw('COALESCE(SUM(refunds), 0) as refunds')
])
->where('park_id', $parkId)
->where('contract_cancel_flag', 1)
->whereDate('repayment_at', '=', $targetDate)
->groupBy('psection_id')
->get();
return $results->toArray();
}
/**
* 再発行データ取得(車種区分毎)
*
* 仕様 todo/SHJ-9/SHJ-9.txt:138-149
*
* @param int $parkId 駐輪場ID
* @param string $targetDate 集計対象日YYYY-MM-DD
* @return array
*/
private function calculateReissueCount(int $parkId, string $targetDate): array
{
$results = DB::table('seal')
->select([
'psection_id',
DB::raw('COUNT(contract_id) as reissue_count')
])
->where('park_id', $parkId)
->where('contract_seal_issue', '>=', 2)
->whereDate('seal_day', '=', $targetDate)
->groupBy('psection_id')
->get();
return $results->toArray();
}
/**
* 定期契約データを組合せ毎に統合
*
* SQLは contract_reduction × update_flag で分組しているため、
* 同じ psection × usertype × months の組合せで複数行が返る場合がある。
* ここで park_id + psection_id + usertype_subject1 + enable_months をキーに統合し、
* 新規/更新 × 減免/通常 の各件数・金額を1つのオブジェクトに集約する。
*
* 仕様 todo/SHJ-9/SHJ-9.txt:96-113
*
* @param array $regularData calculateRegularEarnings()の結果
* @return array キーpsection_id|usertype|months、値統合されたデータオブジェクト
*/
private function mergeRegularDataByGroup(array $regularData): array
{
$merged = [];
foreach ($regularData as $row) {
// 統合キーpsection_id|usertype_subject1|enable_months
$key = $row->psection_id . '|' . $row->usertype_subject1 . '|' . $row->enable_months;
// 初回作成
if (!isset($merged[$key])) {
$merged[$key] = (object)[
'psection_id' => $row->psection_id,
'usertype_subject1' => $row->usertype_subject1,
'enable_months' => $row->enable_months,
// 新規・通常
'regular_new_count' => 0,
'regular_new_amount' => 0,
// 新規・減免
'regular_new_reduction_count' => 0,
'regular_new_reduction_amount' => 0,
// 更新・通常
'regular_update_count' => 0,
'regular_update_amount' => 0,
// 更新・減免
'regular_update_reduction_count' => 0,
'regular_update_reduction_amount' => 0
];
}
// 区分判定
$isNew = in_array($row->update_flag, [2, null]); // 新規
$isReduction = ($row->contract_reduction == 1); // 減免
$count = $row->contract_count ?? 0;
$amount = $row->total_amount ?? 0;
// 対応するフィールドに累加
if ($isNew && !$isReduction) {
// 新規・通常
$merged[$key]->regular_new_count += $count;
$merged[$key]->regular_new_amount += $amount;
} elseif ($isNew && $isReduction) {
// 新規・減免
$merged[$key]->regular_new_reduction_count += $count;
$merged[$key]->regular_new_reduction_amount += $amount;
} elseif (!$isNew && !$isReduction) {
// 更新・通常
$merged[$key]->regular_update_count += $count;
$merged[$key]->regular_update_amount += $amount;
} elseif (!$isNew && $isReduction) {
// 更新・減免
$merged[$key]->regular_update_reduction_count += $count;
$merged[$key]->regular_update_reduction_amount += $amount;
}
}
return $merged;
}
/**
* 一時金・解約・再発行データを車種区分毎に統合
*
* @param array $lumpsumData
* @param array $refundData
* @param array $reissueData
* @return array
*/
private function mergeOtherEarningsData(array $lumpsumData, array $refundData, array $reissueData): array
{
$merged = [];
// 一時金
foreach ($lumpsumData as $row) {
$psectionId = $row->psection_id;
if (!isset($merged[$psectionId])) {
$merged[$psectionId] = (object)[
'psection_id' => $psectionId,
'usertype_subject1' => null,
'enable_months' => 0,
'lumpsum_count' => 0,
'lumpsum' => 0,
'refunds' => 0,
'reissue_count' => 0
];
}
$merged[$psectionId]->lumpsum_count = $row->lumpsum_count;
$merged[$psectionId]->lumpsum = $row->lumpsum;
}
// 解約返戻金
foreach ($refundData as $row) {
$psectionId = $row->psection_id;
if (!isset($merged[$psectionId])) {
$merged[$psectionId] = (object)[
'psection_id' => $psectionId,
'usertype_subject1' => null,
'enable_months' => 0,
'lumpsum_count' => 0,
'lumpsum' => 0,
'refunds' => 0,
'reissue_count' => 0
];
}
$merged[$psectionId]->refunds = $row->refunds;
}
// 再発行
foreach ($reissueData as $row) {
$psectionId = $row->psection_id;
if (!isset($merged[$psectionId])) {
$merged[$psectionId] = (object)[
'psection_id' => $psectionId,
'usertype_subject1' => null,
'enable_months' => 0,
'lumpsum_count' => 0,
'lumpsum' => 0,
'refunds' => 0,
'reissue_count' => 0
];
}
$merged[$psectionId]->reissue_count = $row->reissue_count;
}
return $merged;
}
/**
* 【処理4】既存の売上集計結果を削除
*
* 仕様書のキー駐輪場ID, 集計種別, 集計開始日, 集計終了日, 売上日付, 車種区分, 分類名1, 定期有効月数
* 仕様 todo/SHJ-9/SHJ-9.txt:181-189
*
* @param int $parkId 駐輪場ID
* @param string $targetDate 集計対象日YYYY-MM-DD
* @return void
*/
private function deleteExistingSummary(int $parkId, string $targetDate): void
{
// 仕様書どおり、同一キーの組み合わせで削除
// 日次の場合、集計開始日・終了日はNULL、売上日付で判定
DB::table('earnings_summary')
->where('park_id', $parkId)
->where('summary_type', self::SUMMARY_TYPE_DAILY)
->whereNull('summary_start_date')
->whereNull('summary_end_date')
->where('earnings_date', $targetDate)
// psection_id, usertype_subject1, enable_months は
// レコードごとに異なるため、ここでは指定しない
->delete();
Log::debug('既存の売上集計結果削除', [
'park_id' => $parkId,
'target_date' => $targetDate
]);
}
/**
* 売上集計結果を登録
*
* 仕様 todo/SHJ-9/SHJ-9.txt:181-247
*
* @param object $park 駐輪場情報
* @param object $data 売上データ
* @param string $targetDate 集計対象日YYYY-MM-DD
* @param string $dataType データ種別regular or other
* @return void
*/
private function createEarningsSummary($park, $data, string $targetDate, string $dataType): void
{
$insertData = [
'park_id' => $park->park_id,
'summary_type' => self::SUMMARY_TYPE_DAILY, // 3 = 日次
'summary_start_date' => null, // 日次は NULL (仕様line 215)
'summary_end_date' => null, // 日次は NULL (仕様line 216)
'earnings_date' => $targetDate,
'psection_id' => $data->psection_id,
'usertype_subject1' => $data->usertype_subject1 ?? null, // 実際の分類名
'enable_months' => $data->enable_months ?? 0, // 実際の定期有効月数
'summary_note' => "SHJ-9{$targetDate}", // 仕様line 236
'created_at' => now(),
'updated_at' => now(),
'operator_id' => self::BATCH_OPERATOR_ID // 9999999 (仕様line 239)
];
if ($dataType === 'regular') {
// 定期契約データの場合mergeRegularDataByGroup()で既に統合済み
// 新規/更新 × 減免/通常 の各件数・金額がすべて含まれている (仕様line 98-113)
$insertData = array_merge($insertData, [
'regular_new_count' => $data->regular_new_count ?? 0,
'regular_new_amount' => $data->regular_new_amount ?? 0,
'regular_new_reduction_count' => $data->regular_new_reduction_count ?? 0,
'regular_new_reduction_amount' => $data->regular_new_reduction_amount ?? 0,
'regular_update_count' => $data->regular_update_count ?? 0,
'regular_update_amount' => $data->regular_update_amount ?? 0,
'regular_update_reduction_count' => $data->regular_update_reduction_count ?? 0,
'regular_update_reduction_amount' => $data->regular_update_reduction_amount ?? 0,
'lumpsum_count' => 0, // 仕様line 106
'lumpsum' => 0, // 仕様line 107
'refunds' => 0, // 仕様line 108
'other_income' => 0, // 仕様line 109
'other_spending' => 0, // 仕様line 110
'reissue_count' => 0, // 仕様line 111
'reissue_amount' => 0 // 仕様line 112
]);
} else {
// 一時金・解約・再発行データの場合定期フィールドは0固定 (仕様line 152-167)
$insertData = array_merge($insertData, [
'regular_new_count' => 0, // 仕様line 152
'regular_new_amount' => 0, // 仕様line 153
'regular_new_reduction_count' => 0, // 仕様line 154
'regular_new_reduction_amount' => 0, // 仕様line 155
'regular_update_count' => 0, // 仕様line 156
'regular_update_amount' => 0, // 仕様line 157
'regular_update_reduction_count' => 0, // 仕様line 158
'regular_update_reduction_amount' => 0, // 仕様line 159
'lumpsum_count' => $data->lumpsum_count ?? 0, // 仕様line 160
'lumpsum' => $data->lumpsum ?? 0, // 仕様line 161
'refunds' => $data->refunds ?? 0, // 仕様line 162
'other_income' => 0, // 仕様line 163
'other_spending' => 0, // 仕様line 164
'reissue_count' => $data->reissue_count ?? 0, // 仕様line 165
'reissue_amount' => 0 // 仕様line 166: 0固定
]);
}
DB::table('earnings_summary')->insert($insertData);
Log::debug('売上集計結果登録', [
'park_id' => $park->park_id,
'psection_id' => $data->psection_id,
'data_type' => $dataType,
'target_date' => $targetDate
]);
}
/**
* 【処理5】オペレータキュー作成駐輪場単位・情報不備がある場合のみ
*
* 仕様 todo/SHJ-9/SHJ-9.txt:253-280
* - que_class: 14(集計対象エラー)
* - que_comment: 空文字(""
* - que_status: 1(キュー発生)
* - que_status_comment: 空文字(""
* - work_instructions: 情報不備メッセージ
* - park_id: 駐輪場ID仕様 "処理1.駐輪場ID"、パラメータエラー時はnull
* - operator_id: 9999999(バッチ処理固定値)
*
* @param string $message 情報不備メッセージ
* @param int|null $parkId 駐輪場IDパラメータエラー時はnull
* @return void
*/
private function createOperatorQueue(string $message, ?int $parkId = null): void
{
try {
DB::table('operator_que')->insert([
'que_class' => 14, // 集計対象エラー
'user_id' => null,
'contract_id' => null,
'park_id' => $parkId, // 仕様処理1.駐輪場ID
'que_comment' => '', // 仕様line 260: ""
'que_status' => 1, // キュー発生
'que_status_comment' => '', // 仕様line 262: ""
'work_instructions' => $message, // 仕様line 263: 情報不備
'operator_id' => self::BATCH_OPERATOR_ID, // 9999999
'created_at' => now(),
'updated_at' => now()
]);
Log::info('オペレータキュー作成完了', [
'park_id' => $parkId,
'que_class' => 14,
'que_status' => 1,
'operator_id' => self::BATCH_OPERATOR_ID,
'work_instructions' => $message
]);
} catch (\Exception $e) {
Log::error('オペレータキュー作成エラー', [
'park_id' => $parkId,
'error' => $e->getMessage()
]);
}
}
/**
* SHJ-8 バッチ処理ログ作成
*
* 仕様 todo/SHJ-9/SHJ-9.txt:289-300
* 共通処理「SHJ-8 バッチ処理ログ作成」を呼び出す
*
* @param string $jobName ジョブ名
* @param string $status ステータス (success/error)
* @param string $statusComment 業務固有のステータスコメント
* @return void
*/
private function callShjEight(string $jobName, string $status, string $statusComment): void
{
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
$this->shjEightService->execute(
$deviceId,
'SHJ-9',
$jobName,
$status,
$statusComment,
$today,
$today
);
Log::info('SHJ-8 バッチ処理ログ作成完了', [
'job_name' => $jobName,
'status' => $status
]);
} catch (\Exception $e) {
Log::error('SHJ-8 バッチ処理ログ作成エラー', [
'error' => $e->getMessage(),
'job_name' => $jobName,
'status_comment' => $statusComment
]);
throw $e;
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,885 @@
<?php
namespace App\Services;
use App\Models\Park;
use App\Models\EarningsSummary;
use App\Models\Psection;
use App\Models\OperatorQue;
use App\Models\Device;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
/**
* SHJ-10 売上集計処理サービス
*
* 財政年度ベースの年次・月次売上集計処理を実行するビジネスロジック
* バッチ処理「SHJ-10売上集計(年次・月次)」の核となる処理を担当
*/
class ShjTenService
{
/**
* 財政年度開始月
*
* @var int
*/
const FISCAL_START_MONTH = 4;
/**
* バッチ実行者の固定オペレータID
*/
const BATCH_OPERATOR_ID = 9999999;
/**
* 年次集計の summary_type
*/
const SUMMARY_TYPE_YEARLY = 1;
/**
* 月次集計の summary_type
*/
const SUMMARY_TYPE_MONTHLY = 2;
/**
* ShjEightService インスタンス
*
* @var ShjEightService
*/
protected $shjEightService;
/**
* コンストラクタ
*
* @param ShjEightService $shjEightService
*/
public function __construct(ShjEightService $shjEightService)
{
$this->shjEightService = $shjEightService;
}
/**
* SHJ-10 財政年度売上集計処理メイン実行
*
* 処理フロー (todo/SHJ-10/SHJ-10.txt):
* 【処理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
{
$statusComments = []; // 内部変数.ステータスコメント
$dataIntegrityIssues = []; // 内部変数.情報不備
try {
// 【処理1】集計対象を設定する財政年度ベース
$aggregationTarget = $this->setFiscalAggregationTarget($fiscalPeriod);
Log::info('SHJ-10 売上集計処理開始', [
'type' => $type,
'target' => $target,
'fiscal_period' => $fiscalPeriod,
'aggregation_target' => $aggregationTarget
]);
// 【処理2】駐輪場マスタを取得する
$parkInfo = $this->getParkInformation();
// 【判断1】取得件数判定
if (empty($parkInfo)) {
$typeLabel = $this->getTypeLabel($type);
$statusComment = "売上集計{$typeLabel}:駐輪場マスタが存在していません。";
$statusComments[] = $statusComment;
// 【処理5】オペレータキュー作成
$this->createOperatorQueue($statusComment, null);
// SHJ-8 バッチ処理ログ作成
$this->callShjEight('SHJ-10売上集計年次・月次', 'success', $statusComment);
return [
'success' => true,
'message' => $statusComment,
'processed_parks' => 0,
'summary_records' => 0
];
}
// 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する
$summaryRecords = 0;
$processedParks = 0;
foreach ($parkInfo as $park) {
$result = $this->processFiscalEarningsForPark($park, $aggregationTarget, $fiscalPeriod);
$processedParks++;
$summaryRecords += $result['summary_records'];
// 対象データなしの場合のステータスコメント収集
if (!empty($result['no_data_message'])) {
$statusComments[] = $result['no_data_message'];
}
// 情報不備を収集("なし"でない場合)
if ($result['data_integrity_issue'] !== '情報不備:なし') {
$dataIntegrityIssues[] = $result['data_integrity_issue'];
}
}
// 最終ステータスコメント生成
$typeLabel = $this->getTypeLabel($type);
$finalStatusComment = "売上集計{$typeLabel}:対象={$fiscalPeriod['target_label']}、駐輪場数={$processedParks}、集計レコード数={$summaryRecords}";
if (!empty($dataIntegrityIssues)) {
$finalStatusComment .= "、情報不備=" . implode('、', $dataIntegrityIssues);
}
// 【処理5】オペレータキュー作成
// ※ 駐輪場単位で既に作成済みprocessFiscalEarningsForPark内で情報不備検出時に実施
if (!empty($dataIntegrityIssues)) {
Log::warning('SHJ-10 情報不備検出', [
'issues' => $dataIntegrityIssues
]);
}
// SHJ-8 バッチ処理ログ作成
$this->callShjEight('SHJ-10売上集計年次・月次', 'success', $finalStatusComment);
Log::info('SHJ-10 売上集計処理完了', [
'processed_parks' => $processedParks,
'summary_records' => $summaryRecords,
'data_integrity_issues' => count($dataIntegrityIssues),
'no_data_parks' => count($statusComments)
]);
return [
'success' => true,
'message' => 'SHJ-10 売上集計処理が正常に完了しました',
'processed_parks' => $processedParks,
'summary_records' => $summaryRecords,
'data_integrity_issues' => count($dataIntegrityIssues)
];
} catch (\Exception $e) {
$typeLabel = $this->getTypeLabel($type ?? 'unknown');
$errorMessage = "売上集計{$typeLabel}:エラー発生 - " . $e->getMessage();
// SHJ-8 バッチ処理ログ作成(エラー時も作成)
try {
$this->callShjEight('SHJ-10売上集計年次・月次', 'error', $errorMessage);
} catch (\Exception $shjException) {
Log::error('SHJ-8呼び出しエラー', ['error' => $shjException->getMessage()]);
}
Log::error('SHJ-10 売上集計処理エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'message' => $errorMessage,
'details' => $e->getMessage()
];
}
}
/**
* 【処理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'],
'earnings_date' => $fiscalPeriod['earnings_date'],
'summary_type' => $fiscalPeriod['summary_type'],
'fiscal_year' => $fiscalPeriod['fiscal_year'],
'target_label' => $fiscalPeriod['target_label']
];
}
/**
* 【処理2】駐輪場マスタを取得する
*
* @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 array 処理結果 ['summary_records' => int, 'data_integrity_issue' => string, 'no_data_message' => string|null]
*/
private function processFiscalEarningsForPark($park, array $aggregationTarget, array $fiscalPeriod): array
{
try {
$startDate = $aggregationTarget['start_date'];
$endDate = $aggregationTarget['end_date'];
// 0. 情報不備チェック
$dataIntegrityIssue = $this->checkDataIntegrity($park->park_id, $startDate, $endDate);
// 情報不備がある場合、駐輪場単位でオペレータキュー作成(仕様 todo/SHJ-10/SHJ-10.txt:289-299
if ($dataIntegrityIssue !== '情報不備:なし') {
$this->createOperatorQueue($dataIntegrityIssue, $park->park_id);
}
// ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎)
$regularData = $this->calculateRegularEarnings($park->park_id, $startDate, $endDate);
// ② 一時金データ取得(車種毎)
$lumpsumData = $this->calculateLumpsumEarnings($park->park_id, $startDate, $endDate);
// ③ 解約返戻金データ取得(車種区分毎)
$refundData = $this->calculateRefundEarnings($park->park_id, $startDate, $endDate);
// ④ 再発行データ取得(車種区分毎)
$reissueData = $this->calculateReissueCount($park->park_id, $startDate, $endDate);
// 【判断2】データがいずれかあれば【処理4】へ
if (empty($regularData) && empty($lumpsumData) && empty($refundData) && empty($reissueData)) {
// 対象データなし - 仕様 todo/SHJ-10/SHJ-10.txt:209-211
$typeLabel = $this->getTypeLabel($aggregationTarget['type']);
$noDataMessage = "売上集計{$typeLabel}{$startDate}{$endDate}/駐輪場:{$park->park_name}:売上データが存在しません。";
return [
'summary_records' => 0,
'data_integrity_issue' => $dataIntegrityIssue,
'no_data_message' => $noDataMessage
];
}
// 【処理4】売上集計結果を登録削除は createFiscalEarningsSummary 内で実施)
$summaryRecords = 0;
// ① 定期契約データがある場合同じ組合せpsection×usertype×monthsを統合
$mergedRegularData = $this->mergeRegularDataByGroup($regularData);
foreach ($mergedRegularData as $key => $mergedRow) {
$this->createFiscalEarningsSummary($park, $mergedRow, $aggregationTarget, $fiscalPeriod, 'regular');
$summaryRecords++;
}
// ②③④ 一時金・解約・再発行データがある場合(車種区分毎に集約)
$otherDataByPsection = $this->mergeOtherEarningsData($lumpsumData, $refundData, $reissueData);
foreach ($otherDataByPsection as $psectionId => $data) {
$this->createFiscalEarningsSummary($park, $data, $aggregationTarget, $fiscalPeriod, 'other');
$summaryRecords++;
}
Log::info('駐輪場売上集計完了', [
'park_id' => $park->park_id,
'park_name' => $park->park_name,
'summary_records' => $summaryRecords,
'data_integrity_issue' => $dataIntegrityIssue
]);
return [
'summary_records' => $summaryRecords,
'data_integrity_issue' => $dataIntegrityIssue,
'no_data_message' => null
];
} catch (\Exception $e) {
Log::error('駐輪場売上集計エラー', [
'park_id' => $park->park_id,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 0. 情報不備チェック(期間集計版)
*
* 仕様 todo/SHJ-10/SHJ-10.txt:77-101
*
* @param int $parkId 駐輪場ID
* @param string $startDate 集計開始日YYYY-MM-DD
* @param string $endDate 集計終了日YYYY-MM-DD
* @return string 情報不備メッセージ(仕様フォーマット:"情報不備xxx" or "情報不備:なし"
*/
private function checkDataIntegrity(int $parkId, string $startDate, string $endDate): string
{
$incompleteContracts = DB::table('regular_contract')
->select('contract_id')
->where('park_id', $parkId)
->where('contract_flag', 1)
->whereBetween(DB::raw('DATE(contract_payment_day)'), [$startDate, $endDate])
->where(function($query) {
$query->whereNull('update_flag')
->orWhereNull('psection_id')
->orWhereNull('enable_months');
})
->pluck('contract_id')
->toArray();
if (empty($incompleteContracts)) {
return '情報不備:なし';
}
// 仕様フォーマット:"情報不備:" + 契約IDカンマ区切り
return '情報不備:' . implode(',', $incompleteContracts);
}
/**
* 定期契約データ取得(車種区分・分類名1・定期有効月数毎)
* 金額・件数は全て0固定だが、分類や月数の組み合わせごとにレコードが必要
*
* 仕様 todo/SHJ-10/SHJ-10.txt:104-128
*
* @param int $parkId 駐輪場ID
* @param string $startDate 集計開始日YYYY-MM-DD
* @param string $endDate 集計終了日YYYY-MM-DD
* @return array
*/
/**
* 定期契約データ取得(車種区分・分類名1・定期有効月数毎)
*
* 仕様 todo/SHJ-10/SHJ-10.txt:104-129
* SQL定義減免措置・継続フラグ・車種区分・分類名・有効月数でグループ化し、
* 授受金額の合計と件数を算出する
*
* @param int $parkId 駐輪場ID
* @param string $startDate 集計開始日YYYY-MM-DD
* @param string $endDate 集計終了日YYYY-MM-DD
* @return array
*/
private function calculateRegularEarnings(int $parkId, string $startDate, string $endDate): array
{
$results = DB::table('regular_contract as T1')
->join('usertype as T3', 'T1.user_categoryid', '=', 'T3.user_categoryid')
->select([
DB::raw('IFNULL(T1.contract_reduction, 0) as contract_reduction'),
'T1.update_flag',
'T1.psection_id',
'T3.usertype_subject1',
'T1.enable_months',
DB::raw('SUM(T1.contract_money) as total_amount'), // 仕様:授受金額の合計
DB::raw('COUNT(T1.contract_money) as contract_count') // 仕様:授受件数
])
->where('T1.park_id', $parkId)
->where('T1.contract_flag', 1)
->whereBetween(DB::raw('DATE(T1.contract_payment_day)'), [$startDate, $endDate])
->whereNotNull('T1.update_flag')
->whereNotNull('T1.psection_id')
->whereNotNull('T1.enable_months')
->groupBy([
DB::raw('IFNULL(T1.contract_reduction, 0)'),
'T1.update_flag',
'T1.psection_id',
'T3.usertype_subject1',
'T1.enable_months'
])
->get();
return $results->toArray();
}
/**
* 一時金データ取得(車種毎)
*
* 仕様 todo/SHJ-10/SHJ-10.txt:148-159
*
* @param int $parkId 駐輪場ID
* @param string $startDate 集計開始日YYYY-MM-DD
* @param string $endDate 集計終了日YYYY-MM-DD
* @return array
*/
private function calculateLumpsumEarnings(int $parkId, string $startDate, string $endDate): array
{
$results = DB::table('lumpsum_transaction')
->select([
'type_class as psection_id',
DB::raw('COUNT(*) as lumpsum_count'),
DB::raw('COALESCE(SUM(deposit_amount), 0) as lumpsum')
])
->where('park_id', $parkId)
->whereBetween(DB::raw('DATE(pay_date)'), [$startDate, $endDate])
->groupBy('type_class')
->get();
return $results->toArray();
}
/**
* 解約返戻金データ取得(車種区分毎)
*
* 仕様 todo/SHJ-10/SHJ-10.txt:160-171
*
* @param int $parkId 駐輪場ID
* @param string $startDate 集計開始日YYYY-MM-DD
* @param string $endDate 集計終了日YYYY-MM-DD
* @return array
*/
private function calculateRefundEarnings(int $parkId, string $startDate, string $endDate): array
{
$results = DB::table('regular_contract')
->select([
'psection_id',
DB::raw('COALESCE(SUM(refunds), 0) as refunds')
])
->where('park_id', $parkId)
->where('contract_cancel_flag', 1)
->whereBetween(DB::raw('DATE(repayment_at)'), [$startDate, $endDate])
->groupBy('psection_id')
->get();
return $results->toArray();
}
/**
* 再発行データ取得(車種区分毎)
*
* 仕様 todo/SHJ-10/SHJ-10.txt:172-183
*
* @param int $parkId 駐輪場ID
* @param string $startDate 集計開始日YYYY-MM-DD
* @param string $endDate 集計終了日YYYY-MM-DD
* @return array
*/
private function calculateReissueCount(int $parkId, string $startDate, string $endDate): array
{
$results = DB::table('seal')
->select([
'psection_id',
DB::raw('COUNT(contract_id) as reissue_count')
])
->where('park_id', $parkId)
->where('contract_seal_issue', '>=', 2)
->whereBetween(DB::raw('DATE(seal_day)'), [$startDate, $endDate])
->groupBy('psection_id')
->get();
return $results->toArray();
}
/**
* 定期契約データを組合せ毎に統合
*
* SQLは contract_reduction × update_flag で分組しているため、
* 同じ psection × usertype × months の組合せで複数行が返る場合がある。
* ここで park_id + psection_id + usertype_subject1 + enable_months をキーに統合し、
* 新規/更新 × 減免/通常 の各件数・金額を1つのオブジェクトに集約する。
*
* 仕様 todo/SHJ-10/SHJ-10.txt:130-147
*
* @param array $regularData calculateRegularEarnings()の結果
* @return array キーpsection_id|usertype|months、値統合されたデータオブジェクト
*/
private function mergeRegularDataByGroup(array $regularData): array
{
$merged = [];
foreach ($regularData as $row) {
// 統合キーpsection_id|usertype_subject1|enable_months
$key = $row->psection_id . '|' . $row->usertype_subject1 . '|' . $row->enable_months;
// 初回作成
if (!isset($merged[$key])) {
$merged[$key] = (object)[
'psection_id' => $row->psection_id,
'usertype_subject1' => $row->usertype_subject1,
'enable_months' => $row->enable_months,
// 新規・通常
'regular_new_count' => 0,
'regular_new_amount' => 0,
// 新規・減免
'regular_new_reduction_count' => 0,
'regular_new_reduction_amount' => 0,
// 更新・通常
'regular_update_count' => 0,
'regular_update_amount' => 0,
// 更新・減免
'regular_update_reduction_count' => 0,
'regular_update_reduction_amount' => 0
];
}
// 区分判定
$isNew = in_array($row->update_flag, [2, null]); // 新規
$isReduction = ($row->contract_reduction == 1); // 減免
$count = $row->contract_count ?? 0;
$amount = $row->total_amount ?? 0;
// 対応するフィールドに累加
if ($isNew && !$isReduction) {
// 新規・通常
$merged[$key]->regular_new_count += $count;
$merged[$key]->regular_new_amount += $amount;
} elseif ($isNew && $isReduction) {
// 新規・減免
$merged[$key]->regular_new_reduction_count += $count;
$merged[$key]->regular_new_reduction_amount += $amount;
} elseif (!$isNew && !$isReduction) {
// 更新・通常
$merged[$key]->regular_update_count += $count;
$merged[$key]->regular_update_amount += $amount;
} elseif (!$isNew && $isReduction) {
// 更新・減免
$merged[$key]->regular_update_reduction_count += $count;
$merged[$key]->regular_update_reduction_amount += $amount;
}
}
return $merged;
}
/**
* 一時金・解約・再発行データを車種区分毎に統合
*
* @param array $lumpsumData
* @param array $refundData
* @param array $reissueData
* @return array
*/
private function mergeOtherEarningsData(array $lumpsumData, array $refundData, array $reissueData): array
{
$merged = [];
// 一時金
foreach ($lumpsumData as $row) {
$psectionId = $row->psection_id;
if (!isset($merged[$psectionId])) {
$merged[$psectionId] = (object)[
'psection_id' => $psectionId,
'usertype_subject1' => null,
'enable_months' => 0,
'lumpsum_count' => 0,
'lumpsum' => 0,
'refunds' => 0,
'reissue_count' => 0
];
}
$merged[$psectionId]->lumpsum_count = $row->lumpsum_count;
$merged[$psectionId]->lumpsum = $row->lumpsum;
}
// 解約返戻金
foreach ($refundData as $row) {
$psectionId = $row->psection_id;
if (!isset($merged[$psectionId])) {
$merged[$psectionId] = (object)[
'psection_id' => $psectionId,
'usertype_subject1' => null,
'enable_months' => 0,
'lumpsum_count' => 0,
'lumpsum' => 0,
'refunds' => 0,
'reissue_count' => 0
];
}
$merged[$psectionId]->refunds = $row->refunds;
}
// 再発行
foreach ($reissueData as $row) {
$psectionId = $row->psection_id;
if (!isset($merged[$psectionId])) {
$merged[$psectionId] = (object)[
'psection_id' => $psectionId,
'usertype_subject1' => null,
'enable_months' => 0,
'lumpsum_count' => 0,
'lumpsum' => 0,
'refunds' => 0,
'reissue_count' => 0
];
}
$merged[$psectionId]->reissue_count = $row->reissue_count;
}
return $merged;
}
/**
* 【処理4】既存の売上集計結果を削除
*
* 仕様書のキー駐輪場ID, 集計種別, 集計開始日, 集計終了日, 売上日付, 車種区分, 分類名1, 定期有効月数
* 仕様書どおり summary_start_date summary_end_date NULL で保存されているため、NULL で検索する
*
* @param int $parkId 駐輪場ID
* @param array $aggregationTarget 集計対象
* @param int $psectionId 車種区分ID
* @param string|null $usertypeSubject1 分類名1
* @param int $enableMonths 定期有効月数
* @return void
*/
private function deleteExistingFiscalSummary(
int $parkId,
array $aggregationTarget,
int $psectionId,
?string $usertypeSubject1,
int $enableMonths
): void {
// 仕様書どおり、同一キーの組み合わせで削除
$query = DB::table('earnings_summary')
->where('park_id', $parkId)
->where('summary_type', $aggregationTarget['summary_type'])
->whereNull('summary_start_date')
->whereNull('summary_end_date')
->where('earnings_date', $aggregationTarget['earnings_date'])
->where('psection_id', $psectionId)
->where('enable_months', $enableMonths);
// 分類名がnullの場合はIS NULLで検索
if ($usertypeSubject1 === null) {
$query->whereNull('usertype_subject1');
} else {
$query->where('usertype_subject1', $usertypeSubject1);
}
$deletedCount = $query->delete();
if ($deletedCount > 0) {
Log::debug('既存の売上集計結果削除', [
'park_id' => $parkId,
'summary_type' => $aggregationTarget['summary_type'],
'earnings_date' => $aggregationTarget['earnings_date'],
'psection_id' => $psectionId,
'usertype_subject1' => $usertypeSubject1,
'enable_months' => $enableMonths,
'deleted_count' => $deletedCount
]);
}
}
/**
* 売上集計結果を登録(財政年度ベース)
*
* 仕様書: 同一キーのレコードが存在する場合は先に削除してから登録
*
* @param object $park 駐輪場情報
* @param object $data 売上データ
* @param array $aggregationTarget 集計対象
* @param array $fiscalPeriod 財政期間情報
* @param string $dataType データ種別regular or other
* @return void
*/
private function createFiscalEarningsSummary($park, $data, array $aggregationTarget, array $fiscalPeriod, string $dataType): void
{
// 仕様書: 同一キーのレコードを先に削除
$this->deleteExistingFiscalSummary(
$park->park_id,
$aggregationTarget,
$data->psection_id,
$data->usertype_subject1 ?? null,
$data->enable_months ?? 0
);
$insertData = [
'park_id' => $park->park_id,
'summary_type' => $aggregationTarget['summary_type'], // 1=年次, 2=月次
'summary_start_date' => null, // 仕様書: null
'summary_end_date' => null, // 仕様書: null
'earnings_date' => $aggregationTarget['earnings_date'], // 仕様書: 年次=年度(yyyy-01-01), 月次=年月(yyyy-mm-01)
'psection_id' => $data->psection_id,
'usertype_subject1' => $data->usertype_subject1 ?? null, // 実際の分類名
'enable_months' => $data->enable_months ?? 0, // 実際の定期有効月数
'summary_note' => "SHJ-10{$fiscalPeriod['target_label']}{$aggregationTarget['start_date']}{$aggregationTarget['end_date']}", // 仕様: 集計備考
'created_at' => now(),
'updated_at' => now(),
'operator_id' => self::BATCH_OPERATOR_ID // 9999999 (仕様: バッチオペレータID)
];
if ($dataType === 'regular') {
// 定期契約データの場合mergeRegularDataByGroup()で既に統合済み
// 新規/更新 × 減免/通常 の各件数・金額がすべて含まれている (仕様line 130-147)
$insertData = array_merge($insertData, [
'regular_new_count' => $data->regular_new_count ?? 0,
'regular_new_amount' => $data->regular_new_amount ?? 0,
'regular_new_reduction_count' => $data->regular_new_reduction_count ?? 0,
'regular_new_reduction_amount' => $data->regular_new_reduction_amount ?? 0,
'regular_update_count' => $data->regular_update_count ?? 0,
'regular_update_amount' => $data->regular_update_amount ?? 0,
'regular_update_reduction_count' => $data->regular_update_reduction_count ?? 0,
'regular_update_reduction_amount' => $data->regular_update_reduction_amount ?? 0,
'lumpsum_count' => 0, // 仕様line 140
'lumpsum' => 0, // 仕様line 141
'refunds' => 0, // 仕様line 142
'other_income' => 0, // 仕様line 143
'other_spending' => 0, // 仕様line 144
'reissue_count' => 0, // 仕様line 145
'reissue_amount' => 0 // 仕様line 146
]);
} else {
// 一時金・解約・再発行データの場合定期フィールドは0固定 (仕様line 186-202)
$insertData = array_merge($insertData, [
'regular_new_count' => 0, // 仕様line 186
'regular_new_amount' => 0, // 仕様line 187
'regular_new_reduction_count' => 0, // 仕様line 188
'regular_new_reduction_amount' => 0, // 仕様line 189
'regular_update_count' => 0, // 仕様line 190
'regular_update_amount' => 0, // 仕様line 191
'regular_update_reduction_count' => 0, // 仕様line 192
'regular_update_reduction_amount' => 0, // 仕様line 193
'lumpsum_count' => $data->lumpsum_count ?? 0, // 仕様line 194
'lumpsum' => $data->lumpsum ?? 0, // 仕様line 195
'refunds' => $data->refunds ?? 0, // 仕様line 196
'other_income' => 0, // 仕様line 197
'other_spending' => 0, // 仕様line 198
'reissue_count' => $data->reissue_count ?? 0, // 仕様line 199
'reissue_amount' => 0 // 仕様line 200: 0固定
]);
}
DB::table('earnings_summary')->insert($insertData);
Log::debug('売上集計結果登録', [
'park_id' => $park->park_id,
'psection_id' => $data->psection_id,
'data_type' => $dataType,
'summary_type' => $aggregationTarget['summary_type']
]);
}
/**
* 【処理5】オペレータキュー作成駐輪場単位・情報不備がある場合のみ
*
* 仕様 todo/SHJ-10/SHJ-10.txt:289-317
* - que_class: 14(集計対象エラー)
* - que_comment: 仕様書line 297
* - que_status: 1(キュー発生)
* - que_status_comment: 仕様書line 299
* - work_instructions: 情報不備メッセージ仕様書line 300
* - park_id: 駐輪場ID仕様 "処理1.駐輪場ID"、パラメータエラー時はnull
* - operator_id: 9999999(バッチ処理固定値)
*
* @param string $message 情報不備メッセージ
* @param int|null $parkId 駐輪場IDパラメータエラー時はnull
* @return void
*/
private function createOperatorQueue(string $message, ?int $parkId = null): void
{
try {
DB::table('operator_que')->insert([
'que_class' => 14, // 集計対象エラー
'user_id' => null,
'contract_id' => null,
'park_id' => $parkId, // 仕様処理1.駐輪場ID
'que_comment' => '', // 仕様: 空
'que_status' => 1, // キュー発生
'que_status_comment' => '', // 仕様: 空
'work_instructions' => $message, // 仕様: 情報不備
'operator_id' => self::BATCH_OPERATOR_ID, // 9999999
'created_at' => now(),
'updated_at' => now()
]);
Log::info('オペレータキュー作成完了', [
'park_id' => $parkId,
'que_class' => 14,
'que_status' => 1,
'operator_id' => self::BATCH_OPERATOR_ID,
'work_instructions' => $message
]);
} catch (\Exception $e) {
Log::error('オペレータキュー作成エラー', [
'park_id' => $parkId,
'error' => $e->getMessage()
]);
}
}
/**
* SHJ-8 バッチ処理ログ作成
*
* 共通処理「SHJ-8 バッチ処理ログ作成」を呼び出す
*
* @param string $jobName ジョブ名
* @param string $status ステータス (success/error)
* @param string $statusComment 業務固有のステータスコメント
* @return void
*/
private function callShjEight(string $jobName, string $status, string $statusComment): void
{
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
$this->shjEightService->execute(
$deviceId,
'SHJ-10',
$jobName,
$status,
$statusComment,
$today,
$today
);
Log::info('SHJ-8 バッチ処理ログ作成完了', [
'job_name' => $jobName,
'status' => $status
]);
} catch (\Exception $e) {
Log::error('SHJ-8 バッチ処理ログ作成エラー', [
'error' => $e->getMessage(),
'job_name' => $jobName,
'status_comment' => $statusComment
]);
throw $e;
}
}
/**
* 集計種別のラベル取得
*
* @param string $type 集計種別
* @return string ラベル
*/
private function getTypeLabel(string $type): string
{
switch ($type) {
case 'yearly':
return '(年次)';
case 'monthly':
return '(月次)';
default:
return '';
}
}
}

View File

@ -0,0 +1,380 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\Device;
use Carbon\Carbon;
/**
* SHJ-13 契約台数追加処理サービス
*
* 新規契約時の契約台数を park_number・zone テーブルに反映する処理
* SHJ-4B の副作用処理として実行される
*/
class ShjThirteenService
{
/**
* ShjEightService
*
* @var ShjEightService
*/
protected $shjEightService;
/**
* コンストラクタ
*
* @param ShjEightService $shjEightService
*/
public function __construct(ShjEightService $shjEightService)
{
$this->shjEightService = $shjEightService;
}
/**
* SHJ-13 契約台数追加処理実行
*
* 【処理1】契約台数を反映する - park_number・zone テーブルの契約台数を+1更新
* 【処理2】バッチ処理ログを作成する - SHJ-8共通仕様でログ登録
*
* @param array $contractData 契約データ
* @return array 処理結果
*/
public function execute(array $contractData): array
{
$startTime = now();
Log::info('SHJ-13 契約台数追加処理開始', [
'contract_id' => $contractData['contract_id'] ?? null,
'park_id' => $contractData['park_id'] ?? null,
'psection_id' => $contractData['psection_id'] ?? null,
'ptype_id' => $contractData['ptype_id'] ?? null,
'zone_id' => $contractData['zone_id'] ?? null,
]);
try {
// パラメータ検証
$validationResult = $this->validateParameters($contractData);
if (!$validationResult['valid']) {
return $this->createErrorResult(
1001,
'パラメータエラー: ' . $validationResult['message']
);
}
// 【処理1・2】契約台数反映とバッチログ作成を一体で実行
$processResult = $this->executeProcessWithLogging($contractData);
if (!$processResult['success']) {
return $this->createErrorResult(
$processResult['error_code'],
$processResult['error_message'],
$processResult['stack_trace'] ?? ''
);
}
$statusComment = $processResult['status_comment'];
$endTime = now();
Log::info('SHJ-13 契約台数追加処理完了', [
'contract_id' => $contractData['contract_id'],
'execution_time' => $startTime->diffInSeconds($endTime),
'updated_count' => $processResult['updated_count'],
'status_comment' => $statusComment,
]);
return ['result' => 0];
} catch (\Throwable $e) {
Log::error('SHJ-13 契約台数追加処理例外エラー', [
'contract_data' => $contractData,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->createErrorResult(
$e->getCode() ?: 1999,
$e->getMessage(),
$e->getTraceAsString()
);
}
}
/**
* パラメータ検証
*
* @param array $contractData
* @return array
*/
private function validateParameters(array $contractData): array
{
$errors = [];
if (empty($contractData['park_id'])) {
$errors[] = '駐輪場IDが設定されていません';
}
if (empty($contractData['psection_id'])) {
$errors[] = '車種区分IDが設定されていません';
}
if (empty($contractData['ptype_id'])) {
$errors[] = '駐輪分類IDが設定されていません';
}
if (empty($contractData['zone_id'])) {
$errors[] = 'ゾーンIDが設定されていません';
}
if (!empty($errors)) {
return [
'valid' => false,
'message' => implode(', ', $errors),
'details' => $errors,
];
}
return ['valid' => true];
}
/**
* 契約台数反映とバッチログ作成の一体処理
*
* park_number・zone テーブルの契約台数を+1更新し、バッチログを作成
* 全てを1つのトランザクション内で実行
*
* @param array $contractData
* @return array
*/
private function executeProcessWithLogging(array $contractData): array
{
try {
return DB::transaction(function() use ($contractData) {
// 各テーブルの名称取得
$names = $this->getTableNames($contractData);
if (!$names['success']) {
throw new \Exception('名称取得エラー: ' . $names['message'], 1002);
}
// 【JOB1】zone テーブル更新仕様書SQL-1
// WHERE: 駐輪場ID, 車種区分ID, 駐輪分類ID, ゾーンID
// SET: 現在契約台数+1, 更新日時, 更新オペレータID
$zoneUpdated = DB::table('zone')
->where('park_id', $contractData['park_id'])
->where('psection_id', $contractData['psection_id'])
->where('ptype_id', $contractData['ptype_id'])
->where('zone_id', $contractData['zone_id'])
->increment('zone_number', 1, [
'updated_at' => now(),
'ope_id' => 9999999, // 仕様書: 更新オペレータID
]);
if ($zoneUpdated === 0) {
throw new \Exception('zoneテーブルの対象レコードが存在しません', 1012);
}
// 更新後の契約台数取得
$updatedZone = DB::table('zone')
->where('park_id', $contractData['park_id'])
->where('psection_id', $contractData['psection_id'])
->where('ptype_id', $contractData['ptype_id'])
->where('zone_id', $contractData['zone_id'])
->first(['zone_number']);
// ステータスコメント構築
$statusComment = $this->buildStatusComment($names['names'], $updatedZone->zone_number);
// バッチ処理ログ作成(同一トランザクション内)
// SHJ-8サービスを呼び出し
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
$this->shjEightService->execute(
$deviceId,
'SHJ-13',
'SHJ-13 契約台数追加',
'success',
$statusComment,
$today,
$today
);
Log::info('SHJ-13 契約台数更新・ログ作成完了', [
'contract_id' => $contractData['contract_id'],
'zone_updated' => $zoneUpdated,
'updated_count' => $updatedZone->zone_number,
'status_comment' => $statusComment,
]);
return [
'success' => true,
'updated_count' => $updatedZone->zone_number,
'status_comment' => $statusComment,
];
});
} catch (\Throwable $e) {
Log::error('SHJ-13 契約台数更新・ログ作成エラー', [
'contract_data' => $contractData,
'error_code' => $e->getCode(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return [
'success' => false,
'error_code' => $e->getCode() ?: 1010,
'error_message' => $e->getMessage(),
'stack_trace' => $e->getTraceAsString(),
];
}
}
/**
* テーブル名称取得
*
* @param array $contractData
* @return array
*/
private function getTableNames(array $contractData): array
{
try {
// 駐輪場名取得
$park = DB::table('park')
->where('park_id', $contractData['park_id'])
->first(['park_name']);
if (!$park) {
return [
'success' => false,
'message' => "駐輪場が見つかりません: park_id={$contractData['park_id']}",
];
}
// 駐輪分類名取得
$ptype = DB::table('ptype')
->where('ptype_id', $contractData['ptype_id'])
->first(['ptype_subject']);
if (!$ptype) {
return [
'success' => false,
'message' => "駐輪分類が見つかりません: ptype_id={$contractData['ptype_id']}",
];
}
// 車種区分名取得
$psection = DB::table('psection')
->where('psection_id', $contractData['psection_id'])
->first(['psection_subject']);
if (!$psection) {
return [
'success' => false,
'message' => "車種区分が見つかりません: psection_id={$contractData['psection_id']}",
];
}
// ゾーン名取得
$zone = DB::table('zone')
->where('zone_id', $contractData['zone_id'])
->first(['zone_name']);
if (!$zone) {
return [
'success' => false,
'message' => "ゾーンが見つかりません: zone_id={$contractData['zone_id']}",
];
}
return [
'success' => true,
'names' => [
'park_name' => $park->park_name,
'ptype_subject' => $ptype->ptype_subject,
'psection_subject' => $psection->psection_subject,
'zone_name' => $zone->zone_name,
],
];
} catch (\Throwable $e) {
return [
'success' => false,
'message' => 'テーブル名称取得エラー: ' . $e->getMessage(),
'details' => $e->getTraceAsString(),
];
}
}
/**
* ステータスコメント構築
*
* 形式:駐輪場名/駐輪分類名/車種区分名/ゾーン名/現在契約台数(更新後):{数値}
*
* @param array $names
* @param int $updatedCount
* @return string
*/
private function buildStatusComment(array $names, int $updatedCount): string
{
return sprintf(
'%s%s%s%s現在契約台数更新後%d',
$names['park_name'],
$names['ptype_subject'],
$names['psection_subject'],
$names['zone_name'],
$updatedCount
);
}
/**
* エラー結果作成SHJ-8ログも作成)
*
* @param int $errorCode
* @param string $errorMessage
* @param string $stackTrace
* @return array
*/
private function createErrorResult(int $errorCode, string $errorMessage, string $stackTrace = ''): array
{
// エラー時もSHJ-8でバッチログを作成
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
$statusComment = sprintf(
'エラーコード:%dエラーメッセージ%s',
$errorCode,
$errorMessage
);
$this->shjEightService->execute(
$deviceId,
'SHJ-13',
'SHJ-13 契約台数追加',
'success', // 仕様書:エラー時も"success"で記録
$statusComment,
$today,
$today
);
Log::info('SHJ-13 エラー時バッチログ作成完了', [
'error_code' => $errorCode,
'status_comment' => $statusComment
]);
} catch (\Exception $e) {
Log::error('SHJ-13 エラー時バッチログ作成失敗', [
'error' => $e->getMessage()
]);
}
return [
'result' => 1,
'error_code' => $errorCode,
'error_message' => $errorMessage,
'stack_trace' => $stackTrace,
];
}
}

View File

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

View File

@ -0,0 +1,523 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\Device;
use App\Models\RegularContract;
use App\Models\User;
use App\Models\Park;
use App\Models\Ope;
use App\Services\ShjMailSendService;
/**
* SHJ-12 未払い者通知処理サービス
*
* 定期契約マスタより未払い者を取得し、通知処理またはオペレーターキュー追加を実行
*/
class ShjTwelveService
{
/**
* SHJメール送信サービス
*
* @var ShjMailSendService
*/
protected $shjMailSendService;
/**
* ShjEightService
*
* @var ShjEightService
*/
protected $shjEightService;
/**
* コンストラクタ
*
* @param ShjMailSendService $shjMailSendService
* @param ShjEightService $shjEightService
*/
public function __construct(
ShjMailSendService $shjMailSendService,
ShjEightService $shjEightService
) {
$this->shjMailSendService = $shjMailSendService;
$this->shjEightService = $shjEightService;
}
/**
* 【処理1】定期契約マスタより未払い者を取得する
*
* SQL条件:
* - 解約フラグ = 0 (未解約)
* - 授受フラグ = 0 (授受フラグOFF)
* - 請求金額 > 0 (請求金額あり)
*
* @return array 未払い者リスト
*/
public function getUnpaidUsers(): array
{
try {
$query = DB::table('regular_contract as T1')
->select([
'T1.contract_id', // 定期契約ID
'T2.user_seq', // 利用者ID
'T2.user_name', // 利用者名
'T2.user_manual_regist_flag', // 手動登録フラグ
'T2.user_primemail', // メールアドレス
'T2.user_submail', // 予備メールアドレス
'T1.park_id', // 駐輪場ID (regular_contractテーブルから)
'T3.park_name', // 駐輪場名
'T1.billing_amount' // 請求金額
])
->join('user as T2', 'T1.user_id', '=', 'T2.user_seq')
->join('park as T3', 'T1.park_id', '=', 'T3.park_id')
->where([
['T1.contract_cancel_flag', '=', 0], // 解約フラグ = 0
['T1.contract_flag', '=', 0], // 授受フラグ = 0
])
->where('T1.billing_amount', '>', 0) // 請求金額 > 0
->get();
Log::info('SHJ-12 未払い者取得完了', [
'count' => $query->count(),
'sql_conditions' => [
'contract_cancel_flag' => 0,
'contract_flag' => 0,
'billing_amount' => '> 0'
]
]);
return $query->toArray();
} catch (\Exception $e) {
Log::error('SHJ-12 未払い者取得エラー', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
}
/**
* 【処理2】未払い者への通知、またはオペレーターキュー追加処理
*
* 仕様:
* - 手動登録フラグ = 0 (Web申込) SHJ-7メール送信のみ
* - 手動登録フラグ 0 (その他) オペレーターキュー追加のみ
*
* @param array $unpaidUsers 未払い者リスト
* @return array 処理結果
*/
public function processUnpaidUserNotifications(array $unpaidUsers): array
{
// 内部変数(カウンタ)初期化
$mailSuccessCount = 0;
$mailErrorCount = 0;
$queueSuccessCount = 0;
$queueErrorCount = 0;
$batchComments = []; // バッチコメント(エラー情報)累積用
$processParameters = [];
try {
DB::beginTransaction();
// 処理1の取得レコード数分繰り返し
foreach ($unpaidUsers as $user) {
try {
// 【判定】手動登録フラグによる分岐
if ($user->user_manual_regist_flag == 0) {
// Web申込 → SHJ-7メール送信
$mailResult = $this->sendNotificationMailViaShj7($user);
// 処理結果判定: result === 0 → 正常
if (($mailResult['result'] ?? 1) === 0) {
$mailSuccessCount++;
} else {
$mailErrorCount++;
// バッチコメントにSHJ-7異常情報を設定後ろに足す
$batchComments[] = sprintf(
'SHJ-7メール送信: 契約ID:%s メール送信失敗: %s',
$user->contract_id,
$mailResult['error_info'] ?? 'Unknown error'
);
}
// 処理パラメータ記録
$processParameters[] = [
'contract_id' => $user->contract_id,
'user_seq' => $user->user_seq,
'billing_amount' => $user->billing_amount,
'process_type' => 'mail',
'result' => ($mailResult['result'] ?? 1) === 0 ? 'success' : 'error'
];
} else {
// その他(手動登録)→ オペレーターキュー追加
$queueResult = $this->addToOperatorQueue($user);
if ($queueResult['success']) {
$queueSuccessCount++;
} else {
$queueErrorCount++;
// バッチコメントに異常情報を設定(後ろに足す)
$batchComments[] = sprintf(
'キュー登録: 契約ID:%s キュー登録失敗: %s',
$user->contract_id,
$queueResult['message'] ?? 'Unknown error'
);
}
// 処理パラメータ記録
$processParameters[] = [
'contract_id' => $user->contract_id,
'user_seq' => $user->user_seq,
'billing_amount' => $user->billing_amount,
'process_type' => 'queue',
'result' => $queueResult['success'] ? 'success' : 'error'
];
}
} catch (\Exception $e) {
// 個別エラーを記録して次の繰り返し処理へ
$batchComments[] = sprintf(
'契約ID:%s 処理エラー: %s',
$user->contract_id,
$e->getMessage()
);
Log::warning('SHJ-12 個別処理エラー', [
'contract_id' => $user->contract_id,
'user_seq' => $user->user_seq,
'error' => $e->getMessage()
]);
}
}
DB::commit();
// ステータスコメント構築
$statusComment = $this->buildStatusComment(
$mailSuccessCount,
$mailErrorCount,
$queueSuccessCount,
$queueErrorCount
);
// バッチコメント整形最大100件、超過分は省略
$formattedBatchComments = $this->formatBatchComments($batchComments);
return [
'success' => true,
'mail_success_count' => $mailSuccessCount,
'mail_error_count' => $mailErrorCount,
'queue_success_count' => $queueSuccessCount,
'queue_error_count' => $queueErrorCount,
'status_comment' => $statusComment,
'batch_comments' => $formattedBatchComments,
'parameters' => $processParameters,
'message' => '未払い者通知処理完了'
];
} catch (\Exception $e) {
DB::rollBack();
Log::error('SHJ-12 通知処理全体エラー', [
'error' => $e->getMessage(),
'processed_count' => count($processParameters)
]);
return [
'success' => false,
'mail_success_count' => $mailSuccessCount,
'mail_error_count' => $mailErrorCount,
'queue_success_count' => $queueSuccessCount,
'queue_error_count' => $queueErrorCount,
'status_comment' => 'システムエラー',
'batch_comments' => $batchComments,
'parameters' => $processParameters,
'message' => '通知処理エラー: ' . $e->getMessage(),
'details' => $e->getTraceAsString()
];
}
}
/**
* SHJ-7 メール送信サービス経由でメール通知送信
*
* 仕様書パラメータ:
* - メールアドレス: 処理1.メールアドレス
* - 予備メールアドレス: 処理1.予備メールアドレス
* - 使用プログラムID: 204
*
* @param object $user 未払い者情報
* @return array SHJ-7の処理結果 (result_code: 0=正常, 1=異常)
*/
private function sendNotificationMailViaShj7($user): array
{
try {
// SHJ-7 executeMailSend 呼び出し
$result = $this->shjMailSendService->executeMailSend(
(string)($user->user_primemail ?? ''), // メールアドレス
(string)($user->user_submail ?? ''), // 予備メールアドレス
204 // 使用プログラムID
);
Log::info('SHJ-12 SHJ-7メール送信結果', [
'contract_id' => $user->contract_id,
'user_seq' => $user->user_seq,
'result' => $result['result'] ?? 1,
'error_info' => $result['error_info'] ?? ''
]);
return $result;
} catch (\Exception $e) {
Log::error('SHJ-12 SHJ-7メール送信エラー', [
'contract_id' => $user->contract_id,
'error' => $e->getMessage()
]);
return [
'result' => 1, // 異常終了
'error_info' => 'SHJ-7メール送信エラー: ' . $e->getMessage()
];
}
}
/**
* オペレーターキューへの追加
*
* 仕様書SQL定義に基づき operator_que テーブルに登録
* - キュー種別ID: 8 (支払い催促)
* - キューステータスID: 1 (キュー発生)
* - que_id: MAX+1方式で生成db_now.sqlは非AUTO_INCREMENT
* - 主キー衝突時に1回リトライ
*
* @param object $user 未払い者情報
* @return array 追加結果
*/
private function addToOperatorQueue($user): array
{
$maxRetries = 2; // 最大2回試行初回 + リトライ1回
$lastException = null;
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
try {
// オペレーターキューデータ構築
// 注: que_idはAUTO_INCREMENTのため、DBに委任手動採番しない
$queueData = [
// 'que_id' は削除AUTO_INCREMENTに委任
'que_class' => 8, // キュー種別ID: 8=支払い催促
'user_id' => $user->user_seq, // 利用者ID
'contract_id' => $user->contract_id, // 定期契約ID
'park_id' => $user->park_id, // 駐輪場ID
'que_comment' => '', // キューコメント (空文字)
'que_status' => 1, // キューステータスID: 1=キュー発生
'que_status_comment' => '', // キューステータスコメント (空文字)
'work_instructions' => '', // 業務指示コメント (空文字)
'created_at' => now(), // 登録日時
'updated_at' => now(), // 更新日時
'operator_id' => 9999999 // 更新オペレータID: バッチ処理用固定値
];
// operator_queテーブルに挿入que_idはDBが自動採番
$newQueId = DB::table('operator_que')->insertGetId($queueData);
Log::info('SHJ-12 オペレーターキュー追加完了', [
'que_id' => $newQueId,
'contract_id' => $user->contract_id,
'user_seq' => $user->user_seq,
'park_id' => $user->park_id,
'que_class' => 8,
'que_status' => 1,
'attempt' => $attempt
]);
return [
'success' => true,
'message' => 'オペレーターキュー追加成功',
'que_id' => $newQueId
];
} catch (\Illuminate\Database\QueryException $e) {
$lastException = $e;
// 主キー重複エラーDuplicate entryの場合のみリトライ
// SQLSTATEコード 23000 は整合性制約違反を示す
if ($e->getCode() == 23000 && $attempt < $maxRetries) {
Log::warning('SHJ-12 que_id重複検出、リトライします', [
'attempt' => $attempt,
'max_retries' => $maxRetries,
'que_id' => $newQueId ?? null,
'contract_id' => $user->contract_id,
'error_code' => $e->getCode()
]);
// 短時間待機後に再試行100ms
usleep(100000);
continue;
}
// その他のエラー、または最終試行での失敗時は例外をスロー
throw $e;
} catch (\Exception $e) {
// QueryException以外の例外は即座にスロー
$lastException = $e;
throw $e;
}
}
// ループを抜けた場合(通常は到達しない)
Log::error('SHJ-12 オペレーターキュー追加エラー(最大リトライ回数超過)', [
'contract_id' => $user->contract_id,
'user_seq' => $user->user_seq,
'max_retries' => $maxRetries,
'error' => $lastException ? $lastException->getMessage() : 'Unknown error',
'trace' => $lastException ? $lastException->getTraceAsString() : ''
]);
return [
'success' => false,
'message' => 'オペレーターキュー追加エラー(リトライ失敗): ' .
($lastException ? $lastException->getMessage() : 'Unknown error')
];
}
/**
* ステータスコメント構築
*
* 仕様書に基づくステータスコメント形式:
* - エラーなし: "メール送信成功:X件 / キュー登録成功:Y件"
* - エラーあり: "メール送信成功:X件 / メール送信失敗:A件 / キュー登録成功:Y件 / キュー登録失敗:B件"
*
* @param int $mailSuccessCount メール正常終了件数
* @param int $mailErrorCount メール異常終了件数
* @param int $queueSuccessCount キュー登録正常終了件数
* @param int $queueErrorCount キュー登録異常終了件数
* @return string ステータスコメント
*/
private function buildStatusComment(
int $mailSuccessCount,
int $mailErrorCount,
int $queueSuccessCount,
int $queueErrorCount
): string {
$parts = [];
// メール送信結果
if ($mailSuccessCount > 0 || $mailErrorCount > 0) {
if ($mailErrorCount > 0) {
$parts[] = "メール送信成功:{$mailSuccessCount}";
$parts[] = "メール送信失敗:{$mailErrorCount}";
} else {
$parts[] = "メール送信成功:{$mailSuccessCount}";
}
}
// キュー登録結果
if ($queueSuccessCount > 0 || $queueErrorCount > 0) {
if ($queueErrorCount > 0) {
$parts[] = "キュー登録成功:{$queueSuccessCount}";
$parts[] = "キュー登録失敗:{$queueErrorCount}";
} else {
$parts[] = "キュー登録成功:{$queueSuccessCount}";
}
}
return implode(' / ', $parts);
}
/**
* バッチコメント整形
*
* エラー情報を最大100件に制限し、超過分は省略表記
*
* @param array $batchComments エラー情報配列
* @return string 整形済みバッチコメント
*/
private function formatBatchComments(array $batchComments): string
{
if (empty($batchComments)) {
return '';
}
// 最大100件に制限
$limitedComments = array_slice($batchComments, 0, 100);
// 超過分の件数を追加
if (count($batchComments) > 100) {
$limitedComments[] = sprintf(
'...他%d件省略',
count($batchComments) - 100
);
}
return implode("\n", $limitedComments);
}
/**
* 【処理3】バッチ処理ログを作成する
*
* SHJ-8サービスを呼び出してbat_job_logテーブルに登録業務固有のstatus_comment記録
*
* @param string $status ステータス
* @param array $parameters パラメータ
* @param string $message メッセージ
* @param int $executionCount 実行回数
* @param int $successCount 成功回数
* @param int $errorCount エラー回数
* @return void
*/
public function createBatchLog(
string $status,
array $parameters,
string $message,
int $executionCount = 0,
int $successCount = 0,
int $errorCount = 0
): void {
try {
// デバイスIDを取得
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
// status_commentを構築業務情報を含む
$statusComment = sprintf(
'%s (実行:%d/成功:%d/エラー:%d)',
$message,
$executionCount,
$successCount,
$errorCount
);
$today = now()->format('Y/m/d');
Log::info('SHJ-8 バッチ処理ログ作成', [
'device_id' => $deviceId,
'status' => $status,
'status_comment' => $statusComment
]);
// SHJ-8サービスを呼び出し
$this->shjEightService->execute(
$deviceId,
'SHJ-12',
'SHJ-12未払い者アラート',
$status,
$statusComment,
$today,
$today
);
} catch (\Exception $e) {
Log::error('SHJ-8 バッチログ作成エラー', [
'error' => $e->getMessage(),
'status' => $status,
'message' => $message
]);
}
}
}

View File

@ -0,0 +1,184 @@
<?php
namespace App\Services;
use App\Models\UserInformationHistory;
use App\Models\User;
use App\Exceptions\ApiException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class UserInformationHistoryService
{
/**
* 履歴一覧取得
*
* @param array $params クエリパラメータ
* @return array
* @throws ApiException
*/
public function getList(array $params): array
{
try {
$query = UserInformationHistory::query();
// user_id フィルター
if (!empty($params['user_id'])) {
// user_idの存在チェック
if (!User::where('user_id', $params['user_id'])->exists()) {
throw new ApiException('E03', 'user_id の値が不正です(存在しないユーザー)。', 400);
}
$query->where('user_id', $params['user_id']);
}
// 日付範囲フィルター
if (!empty($params['entry_date_from'])) {
$query->where('entry_date', '>=', $params['entry_date_from']);
}
if (!empty($params['entry_date_to'])) {
$query->where('entry_date', '<=', $params['entry_date_to']);
}
// ソートentry_date降順、ID降順
$query->orderBy('entry_date', 'desc')
->orderBy('user_information_history_id', 'desc');
// ページネーション
$perPage = $params['per_page'] ?? config('api.pagination.default_per_page', 20);
$perPage = min($perPage, config('api.pagination.max_per_page', 100));
$page = $params['page'] ?? 1;
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
return [
'data' => collect($paginator->items())->map(fn($item) => $item->toApiArray())->toArray(),
'pagination' => [
'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'total_pages' => $paginator->lastPage(),
]
];
} catch (ApiException $e) {
throw $e;
} catch (\Exception $e) {
Log::error('UserInformationHistoryService::getList error', [
'error' => $e->getMessage(),
'params' => $params
]);
throw new ApiException('E99', 'システム内部エラーが発生しました。', 500);
}
}
/**
* 単一履歴取得
*
* @param int $id 履歴ID
* @return array
* @throws ApiException
*/
public function getById(int $id): array
{
try {
$history = UserInformationHistory::find($id);
if (!$history) {
throw new ApiException('E02', '指定された履歴IDが存在しません。', 404);
}
return $history->toApiArray();
} catch (ApiException $e) {
throw $e;
} catch (\Exception $e) {
Log::error('UserInformationHistoryService::getById error', [
'error' => $e->getMessage(),
'id' => $id
]);
throw new ApiException('E99', 'システム内部エラーが発生しました。', 500);
}
}
/**
* 履歴新規作成
*
* @param array $data リクエストデータ
* @return array
* @throws ApiException
*/
public function create(array $data): array
{
try {
DB::beginTransaction();
$history = UserInformationHistory::create([
'user_id' => $data['user_id'],
'entry_date' => $data['entry_date'],
'user_information_history' => $data['user_information_history'],
]);
DB::commit();
Log::info('UserInformationHistory created', [
'user_information_history_id' => $history->user_information_history_id,
'user_id' => $history->user_id
]);
return $history->fresh()->toApiArray();
} catch (\Exception $e) {
DB::rollBack();
Log::error('UserInformationHistoryService::create error', [
'error' => $e->getMessage(),
'data' => $data
]);
throw new ApiException('E99', 'システム内部エラーが発生しました。', 500);
}
}
/**
* 履歴更新
*
* @param int $id 履歴ID
* @param array $data リクエストデータ
* @return array
* @throws ApiException
*/
public function update(int $id, array $data): array
{
try {
DB::beginTransaction();
$history = UserInformationHistory::find($id);
if (!$history) {
throw new ApiException('E02', '指定された履歴IDが存在しません。', 404);
}
// 更新可能フィールドのみ抽出
$updateData = array_filter($data, function ($value, $key) {
return in_array($key, ['user_id', 'entry_date', 'user_information_history']) && $value !== null;
}, ARRAY_FILTER_USE_BOTH);
$history->update($updateData);
DB::commit();
Log::info('UserInformationHistory updated', [
'user_information_history_id' => $id,
'updated_fields' => array_keys($updateData)
]);
return $history->fresh()->toApiArray();
} catch (ApiException $e) {
DB::rollBack();
throw $e;
} catch (\Exception $e) {
DB::rollBack();
Log::error('UserInformationHistoryService::update error', [
'error' => $e->getMessage(),
'id' => $id,
'data' => $data
]);
throw new ApiException('E99', 'システム内部エラーが発生しました。', 500);
}
}
}

18
artisan Normal file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

22
bootstrap/app.php Normal file
View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// APIキー認証ミドルウェアのエイリアス登録
$middleware->alias([
'api.key' => \App\Http\Middleware\ApiKeyAuthentication::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();

2
bootstrap/cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

6
bootstrap/providers.php Normal file
View File

@ -0,0 +1,6 @@
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\ShjServiceProvider::class,
];

76
composer.json Normal file
View File

@ -0,0 +1,76 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"mpdf/mpdf": "^8.2",
"simplesoftwareio/simple-qrcode": "^4.2"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.13",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^11.5.3"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
]
},
"extra": {
"branch-alias": {
"dev-master": "12.x-dev"
},
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

8572
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

43
config/api.php Normal file
View File

@ -0,0 +1,43 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| API Key Configuration
|--------------------------------------------------------------------------
|
| 有効なAPIキーのリスト
| 内部管理システムからの呼び出しに使用
| 複数のキーはカンマ区切りで設定可能
|
| : API_KEYS=key1,key2,key3
|
*/
'valid_keys' => array_filter(explode(',', env('API_KEYS', ''))),
/*
|--------------------------------------------------------------------------
| Pagination Configuration
|--------------------------------------------------------------------------
|
| API一覧取得時のページネーション設定
|
*/
'pagination' => [
'default_per_page' => 20,
'max_per_page' => 100,
],
/*
|--------------------------------------------------------------------------
| Rate Limiting Configuration
|--------------------------------------------------------------------------
|
| APIレート制限の設定
|
*/
'rate_limit' => [
'per_minute' => 60,
'per_hour' => 1000,
],
];

126
config/app.php Normal file
View File

@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'Asia/Tokyo',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];

115
config/auth.php Normal file
View File

@ -0,0 +1,115 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

108
config/cache.php Normal file
View File

@ -0,0 +1,108 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
];

174
config/database.php Normal file
View File

@ -0,0 +1,174 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
];

6
config/env.php Normal file
View File

@ -0,0 +1,6 @@
<?php
// 環境変数保持用config
return [
'mail_admin' => env('MAIL_ADMIN', 'admin@example.com'),
];

79
config/filesystems.php Normal file
View File

@ -0,0 +1,79 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem "disks" as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

132
config/logging.php Normal file
View File

@ -0,0 +1,132 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

116
config/mail.php Normal file
View File

@ -0,0 +1,116 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
];

112
config/queue.php Normal file
View File

@ -0,0 +1,112 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];

38
config/services.php Normal file
View File

@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'resend' => [
'key' => env('RESEND_KEY'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];

217
config/session.php Normal file
View File

@ -0,0 +1,217 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "apc",
| "memcached", "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "apc", "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain and all subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
];

208
config/shj1.php Normal file
View File

@ -0,0 +1,208 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| SHJ-1 本人確認自動処理 Configuration
|--------------------------------------------------------------------------
| SHJ-1バッチ処理の設定ファイル
|
*/
/*
|--------------------------------------------------------------------------
| External API Configuration
|--------------------------------------------------------------------------
| 外部API接続設定環境変数で設定
*/
'apis' => [
// Google Cloud Vision APIOCR処理用
'google_vision' => [
'api_key' => env('GOOGLE_VISION_API_KEY', 'dummy_google_vision_api_key_replace_with_real_one'),
'project_id' => env('GOOGLE_CLOUD_PROJECT_ID', 'dummy-project-id'),
],
// Google Maps API距離計算用
'google_maps' => [
'api_key' => env('GOOGLE_MAPS_API_KEY', 'dummy_google_maps_api_key_replace_with_real_one'),
'base_url' => 'https://maps.googleapis.com/maps/api',
],
],
/*
|--------------------------------------------------------------------------
| OCR Processing Configuration
|--------------------------------------------------------------------------
| OCR処理関連設定
*/
'ocr' => [
// 文字列類似度の閾値70%
'similarity_threshold' => env('SHJ1_OCR_SIMILARITY_THRESHOLD', 70),
// 対応身分証明書タイプ
'supported_id_types' => [
'免許証',
'健康保険証',
'パスポート',
'学生証',
'その他'
],
// OCR結果のキャッシュ時間
'cache_duration' => 60,
],
/*
|--------------------------------------------------------------------------
| File Storage Configuration
|--------------------------------------------------------------------------
| ファイル保存設定
*/
'storage' => [
// 本人確認写真の保存ディスクLaravel Storage使用
'photo_disk' => 'public',
// 写真ファイルの公開URL用パス
'photo_public_path' => '/storage/photo',
// 許可されるファイル拡張子
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'pdf'],
// 最大ファイルサイズMB
'max_file_size' => env('SHJ1_MAX_FILE_SIZE', 10),
],
/*
|--------------------------------------------------------------------------
| Distance Check Configuration
|--------------------------------------------------------------------------
| 距離チェック設定
*/
'distance' => [
// デフォルト距離制限(メートル)
// ※注意park表にdistance_between_two_pointsフィールドが存在しないため、
//     この値をデフォルトとして使用
'default_limit_meters' => env('SHJ1_DEFAULT_DISTANCE_LIMIT', 800),
// 距離計算の精度(小数点以下桁数)
'calculation_precision' => 2,
// Park表の実際のフィールド名
'park_latitude_field' => 'park_latitude',
'park_longitude_field' => 'park_longitude',
'park_address_field' => 'park_adrs',
// Google Maps Distance Matrix API設定
'google_maps_config' => [
'units' => 'metric', // メートル単位
'mode' => 'walking', // 徒歩での距離
'language' => 'ja', // 日本語
'region' => 'jp', // 日本地域
],
],
/*
|--------------------------------------------------------------------------
| User Category Configuration
|--------------------------------------------------------------------------
| 利用者分類設定
*/
'user_categories' => [
// 対象外とする分類名3の値実際の運用に合わせて調整
'excluded_categories' => [
'高齢者',
'障がい者',
'生活保護',
'中国',
'母子家庭'
],
// 通常処理対象の分類名3実際のDB値に基づく
'normal_category' => '該当なし',
// Usertype表の実際のフィールド名
'category_field' => 'usertype_subject3',
],
/*
|--------------------------------------------------------------------------
| Identity Check Status Values
|--------------------------------------------------------------------------
| 本人確認ステータス値
*/
'identity_check_status' => [
'auto_ok' => 2, // 自動チェックOK
'auto_ng' => 5, // 自動チェックNG
'pending' => 0, // 未確認
'manual_ok' => 1, // 手動確認済み
],
/*
|--------------------------------------------------------------------------
| Operator Queue Configuration
|--------------------------------------------------------------------------
| オペレータキュー設定
*/
'operator_queue' => [
// キュー種別ID
'queue_types' => [
'general' => 1, // 本人確認(社会人)
'student' => 2, // 本人確認(学生)
],
// キューステータスID
'queue_status' => [
'created' => 1, // キュー発生
'processing' => 2, // キュー作業中
'completed' => 3, // キュー作業済
],
// バッチジョブのオペレータID
'batch_operator_id' => env('SHJ1_BATCH_OPERATOR_ID', 9999999),
// デフォルトコメント
'default_comment' => '本人確認手動処理を行ってください',
],
/*
|--------------------------------------------------------------------------
| Mail Template Configuration
|--------------------------------------------------------------------------
| メール送信設定
*/
'mail' => [
// 使用プログラムIDバッチ処理200299
// 注意実際のmail_templateテーブルにSHJ-1用テンプレートを追加する必要あり
'program_id_success' => env('SHJ1_MAIL_SUCCESS_TEMPLATE_ID', 201), // 本人確認成功時
'program_id_failure' => env('SHJ1_MAIL_FAILURE_TEMPLATE_ID', 202), // 本人確認失敗時
// Mail Template テーブルの検索条件
'template_search' => [
'success' => ['pg_id' => env('SHJ1_MAIL_SUCCESS_TEMPLATE_ID', 201), 'internal_id' => 1, 'use_flag' => 1],
'failure' => ['pg_id' => env('SHJ1_MAIL_FAILURE_TEMPLATE_ID', 202), 'internal_id' => 1, 'use_flag' => 1],
],
],
/*
|--------------------------------------------------------------------------
| Batch Log Configuration
|--------------------------------------------------------------------------
| バッチログ設定
*/
'batch_log' => [
'process_name' => 'SHJ-1本人確認自動処理',
'status_success' => 'success',
'status_error' => 'error',
],
/*
|--------------------------------------------------------------------------
| Regular Contract 800M Flag
|--------------------------------------------------------------------------
| 定期契約800M設定
*/
'contract_800m' => [
'violation_flag' => 1, // 800M違反フラグdistance > limit時に設定
'normal_flag' => 0, // 通常フラグ
],
];

1
database/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.sqlite*

View File

@ -0,0 +1,44 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

View File

@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@ -0,0 +1,23 @@
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
}
}

33
phpunit.xml Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>

25
public/.htaccess Normal file
View File

@ -0,0 +1,25 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

View File

@ -0,0 +1,150 @@
%PDF-1.3
1 0 obj
<<
/CreationDate (D:20081106111308+09'00')
/Creator <feff30af30bb30ed005000440046002000760031002e0031003400200052006500760030003400390035>
/ModDate (D:20081106111308+09'00')
/Producer (Xelo PDF Library)
>>
endobj
2 0 obj
<<
/Pages 3 0 R
/Type /Catalog
>>
endobj
3 0 obj
<<
/Count 1
/Kids [ 4 0 R ]
/Type /Pages
>>
endobj
4 0 obj
<<
/Contents 11 0 R
/MediaBox [ 0 0 595 842 ]
/Parent 3 0 R
/Resources <<
/Font <<
/F1 5 0 R
/F2 8 0 R
>>
>>
/Type /Page
>>
endobj
5 0 obj
<<
/BaseFont /MS-Gothic
/DescendantFonts [ 6 0 R ]
/Encoding /UniJIS-UCS2-HW-H
/Subtype /Type0
/Type /Font
>>
endobj
6 0 obj
<<
/BaseFont /MS-Gothic
/CIDSystemInfo <<
/Ordering (Japan1)
/Registry (Adobe)
/Supplement 2
>>
/DW 1000
/FontDescriptor 7 0 R
/Subtype /CIDFontType2
/Type /Font
/W [ 97 97 500 99 99 500 101 104 500 107 107 500 109 114 500 117 117 500 119 121 500 123 123 500 126 126 500 129 129 500 134 134 500 139 154 500 157 185 500 187 214 500 216 225 500 227 229 500 231 243 500 245 322 500 324 325 500 327 389 500 7483 7490 500 7492 7493 500 7496 7497 500 7500 7501 500 7504 7505 500 7509 7510 500 7512 7513 500 7517 7518 500 7520 7521 500 7524 7525 500 7528 7529 500 7532 7533 500 7536 7537 500 7540 7541 500 7543 7544 500 7546 7553 500 8009 8019 500 8025 8025 500 8195 8195 500 8210 8214 500 8230 8258 500 8261 8263 500 8304 8305 500 8309 8311 500 9355 9355 500 9361 9393 500 9395 9395 500 9400 9400 500 9404 9405 500 9407 9407 500 9412 9412 500 9416 9418 500 9421 9421 500 9423 9423 500 9426 9426 500 9429 9429 500 9432 9432 500 9436 9438 500 9441 9442 500 10502 10502 500 11855 11855 500 11859 11859 500 12089 12089 500 12091 12092 500 12099 12100 500 12111 12111 500 12118 12118 500 12120 12122 500 12184 12190 500 12194 12195 500 12200 12206 500 12215 12215 500 12237 12237 500 12239 12239 500 12254 [ 500 ] ]
>>
endobj
7 0 obj
<<
/Ascent 859
/CapHeight 859
/Descent -140
/Flags 5
/FontBBox [ 0 -137 1000 859 ]
/FontName /MS-Gothic
/ItalicAngle 0
/StemV 87
/Type /FontDescriptor
>>
endobj
8 0 obj
<<
/BaseFont /MS-PGothic
/DescendantFonts [ 9 0 R ]
/Encoding /UniJIS-UCS2-H
/Subtype /Type0
/Type /Font
>>
endobj
9 0 obj
<<
/BaseFont /MS-PGothic
/CIDSystemInfo <<
/Ordering (Japan1)
/Registry (Adobe)
/Supplement 2
>>
/DW 1000
/FontDescriptor 10 0 R
/Subtype /CIDFontType2
/Type /Font
/W [ 1 [ 304 218 500 ] 4 6 500 7 [ 593 203 304 ] 10 [ 304 500 ] 12 [ 500 203 500 203 500 ] 17 26 500 27 28 203 29 31 500 32 [ 453 667 632 636 664 648 566 550 679 640 246 542 597 539 742 640 707 617 707 625 601 589 640 632 742 601 589 566 335 589 335 414 304 414 476 496 500 496 500 304 460 500 210 218 460 210 734 500 507 496 ] 82 [ 496 347 460 351 500 476 648 460 476 457 234 226 234 ] 97 [ 503 ] 99 [ 234 ] 101 [ 218 500 667 500 ] 107 [ 511 ] 109 [ 667 500 ] 111 [ 500 433 460 500 ] 117 117 203 119 120 203 121 [ 347 ] 123 [ 667 ] 126 [ 453 ] 129 [ 667 ] 134 [ 203 ] 139 [ 667 355 539 667 ] 143 [ 667 238 667 210 308 507 667 542 ] 152 [ 667 640 667 ] 157 158 238 159 [ 503 238 511 ] 162 163 511 164 169 632 170 [ 664 566 ] 172 174 566 175 [ 335 289 433 335 648 640 667 ] 182 185 667 187 190 640 191 [ 589 542 476 ] 194 198 476 199 203 500 204 205 292 206 [ 433 359 511 500 507 ] 211 214 507 216 219 500 220 [ 476 496 476 601 589 566 ] 227 [ 460 558 457 ] 231 [ 500 ] 325 [ 304 ] 327 331 441 332 [ 546 523 445 480 468 515 523 503 437 500 640 617 566 625 597 636 562 652 539 621 523 664 589 636 644 554 527 601 ] 360 361 601 362 [ 460 644 597 578 648 492 636 515 546 613 640 605 453 660 507 609 664 640 519 558 511 656 566 558 589 562 250 230 ] 633 637 664 638 640 500 643 648 500 651 652 746 653 [ 734 699 ] 660 [ 960 ] 662 662 500 670 691 500 776 778 500 780 789 683 790 [ 714 777 742 757 710 632 773 769 273 605 753 628 933 769 804 710 804 757 742 617 769 714 980 652 648 ] 815 [ 648 574 601 562 601 562 296 578 621 250 ] 825 [ 250 593 250 937 621 605 ] 831 [ 605 601 378 570 335 621 511 777 519 496 507 746 941 804 945 601 707 750 902 804 945 ] 854 [ 843 902 589 816 945 980 796 894 765 882 765 ] 865 [ 765 960 980 ] 870 [ 921 960 921 ] 873 [ 921 863 902 804 953 957 902 ] 880 [ 902 765 882 902 941 ] 891 893 960 903 [ 890 ] 905 906 980 907 [ 804 843 ] 910 [ 843 980 726 863 804 746 863 ] 918 [ 843 863 ] 923 [ 855 960 757 898 652 824 753 941 742 894 808 933 824 921 960 964 804 941 929 960 796 890 ] 947 948 898 949 [ 902 964 914 980 804 882 765 921 910 960 734 863 921 886 960 648 707 941 910 824 929 707 ] 974 [ 765 863 ] 976 [ 863 804 882 ] 979 [ 882 945 ] 981 982 945 983 [ 921 953 ] 985 [ 953 902 667 976 718 898 804 980 812 960 628 726 808 746 ] 1000 [ 851 863 765 941 ] 1006 [ 804 863 960 726 777 ] 7483 7490 664 7492 7493 664 7496 7497 664 7500 7501 664 7504 7505 664 7509 7510 664 7512 7513 664 7517 7518 664 7520 7521 664 7524 7525 664 7528 7529 664 7532 7533 664 7536 7537 664 7540 7541 664 7543 7544 664 7546 7553 664 7958 [ 824 ] 8009 8010 500 8011 8012 554 8013 8014 531 8015 [ 500 664 ] 8017 [ 664 601 617 ] 8025 [ 453 ] 8195 8195 500 8210 [ 500 667 ] 8212 8214 667 8230 8232 664 8233 [ 667 664 ] 8235 8236 664 8237 [ 667 664 ] 8239 8240 664 8241 [ 667 664 ] 8243 8250 664 8251 8254 667 8255 8258 500 8261 8263 664 8304 [ 453 664 ] 8309 8310 664 8311 [ 585 ] 8313 [ 910 960 964 921 ] 9355 [ 621 ] 9361 [ 476 355 500 ] 9364 [ 500 507 632 363 640 566 667 511 ] 9372 9374 511 9375 9376 500 9377 9393 289 9395 [ 566 ] 9400 [ 453 ] 9404 9405 640 9407 [ 500 ] 9412 [ 421 ] 9416 9418 500 9421 [ 667 ] 9423 9423 500 9426 [ 500 ] 9429 [ 664 ] 9432 [ 437 ] 9436 [ 500 503 554 ] 9441 [ 472 636 ] 10502 [ 500 ] 11855 [ 664 ] 11859 [ 500 ] 12089 [ 472 ] 12091 [ 500 523 ] 12099 [ 351 664 ] 12111 [ 386 ] 12118 12118 500 12120 12122 500 12184 12190 500 12194 12195 500 12200 [ 664 667 664 ] 12203 12206 664 12215 [ 394 ] 12237 12237 269 12239 [ 269 ] 12254 [ 277 ] ]
>>
endobj
10 0 obj
<<
/Ascent 859
/CapHeight 859
/Descent -140
/Flags 4
/FontBBox [ -121 -137 996 859 ]
/FontName /MS-PGothic
/ItalicAngle 0
/StemV 87
/Type /FontDescriptor
>>
endobj
11 0 obj
<<
/Filter /FlateDecode
/Length 2315
>>
stream
xœ½œK$EÇïóú,XæûâAPÏÊ€çé<C3A7>iEPp/~}£ºóѹ.Èì°U¿ÊŒŒøGFeÅ<65>/ÏOßý¬úðrz~R‡—Ïëÿ×?Ÿÿ¨?ýöËó“6RÚ…ó/´^2ü—6,.)¥ÑèÅ©óáåïç§ï½RÇ/=?ý÷ÿñ!†K4ýAÑŒ >y)F4g†æ*Y-ÅHŽfdŸÄì͈يÙC«trtY b FÎêÚš.ròV â <0A>ßÉ<C39F>Äßpv ¿ƒ„Þž’û$I7ÜÝ*÷Ö!Ô {ÀÓy‰&G·ÎËA-9¬ éóûóÓŸÏO§o>ðˆjñA|yDø/<2F>Õ¸õŽ!Û%Áúú„Þ1¦<31>d¸°¬CQ˜áNùMŠáãbÁÈÉrÌI2B\²!ÌÅO2b<32>G˜ñª“#åEQ6÷:ˆ<>#çe5ÇÀG5¬+sŸ¡á‰µõÕ½”Ý®]pæ V¹¹v¹ X»ê쇈A­].Ö®6ƒZ»\ÜYü»˜Íu´oIn7p?nt«—”rŒe ŸÔIÌèq1£oÞ)±€ÕÆá³ßdPÊpäz?ð"F| œLp?ð"†×N,`µ¹rÁ/Ö4{L)Ę"Rˆ1E¤Ãë$/R˜q<ræ;ÁÄGÈzLSã_ů†}Ì%¼#ˆ7Fn$Ù-Á<11>è<EFBFBD>˜å£(Hxƒh¿Dø)<29><>$æˆÆXSçȸ…XäF!%Q†ÏJs6—4<E28098>r¢k|'*±Ûƒ’û@Œ¶0rsÉ€°5—óH{C$/ŒDlñB¦³KB|ƒÀOë@Hó ;VKù:ŒD,4Zȯ2íëJl _) ˆü|%ÙB~ó“+%ÙBüÛIl$WJ²…ˆÆß®$[³zOIÐtñ*¡÷”dâtu%ÙB˜õÖÛJ²½=³ÔzOIðD ÊUW ”«®$Èru¥$ÃHääª+ÉDHW ñÖ]@Iˆœðºúje€
¯·¥<7ŽDNxƒ-õ¹2Ex<45>ù
»…LÞ-dðn!“„w ™$¼hºæï0]ršØ…w ÉþUN´VáÅc<C385>óõ+áÅÁä¡ ïâÞ÷º]x1„u àžðÎsÃ+á<>ç†WÂ;Ï ¯„wâtuáEAþUN®®„ÙäÄJòW§K. ¶Ñ•·=D. ¶ ¼òxÑ,8ûò*f‰\é„7QA^¢A)<29>—Ìo¦<^2 v6.^“¹,ع´(*ÔK*£óéœÖÎ8% Ö¸ÆÅ9Îr/ žéYð^£\2Ô³à-„yŽé^Œ§K0ïêY0†æ]= ÞBDóùž#&ÝO\Nóýd&¤ù ÿ.…»Ÿ Èé$÷ëÊ&ökØd"¤ÛÄ"Å W6±ÓlÒc×Äéê±k&¤Å®™<C2AE>»fB|šzû¶ƒŸ i;ø™<C3B8>¶ƒŸé;ø™<C3B8>¶ƒŸ i;ø™<C3B8>¶ƒGHÅòù+%Q8Õ ò bãf$s
Î2§àŒ s
Î2§àŒ s
Îxº¦œgNW“+<Ái«\¡ÛžEkr5s¢š\á‰R^ì0e—+¼x<C2BC>A®Z×ä
#W:ïr5ØDðýh“«â&¼éW—¤œ7¬ 3ääÞ¾·ó0]3
Σá'œñtM)8<>#™PpžèŒ½à<@Ë´­à<ŽD®LÛ
ÎäºLûï“uå ÿ9¤ª!Ò©dkº~ÉS¾<53>ûýÃ?øPd½Zçxy²PjÓßÂðÃå³ÈjmØÍjÙ<6A>öÓLˆ¯S<<3C>áÏŸ×LE@²YÝ<ؼĺy H5K¨Çh_9Í4y¬<79>nw¶Í׸&3 ×ô³m®!0ªÙF×¹Ãl«kÈ5Ëæu"rͲy<C2B2><08>\³l^÷ ¿>Mݾn1!¿ éÓ@öût˜<E2809D>r í€Î<E282AC>r­ObPŸHs­Ob€I†zbPgº¹ŒµÅ vsÙÐ æ'Ò7út¸“ÅûFš†€¯<E282AC><C2AF>Nb+KTÒòai<61>ÇÙ$ >6Äå@Œ#.{º†ârùhv€8¹%œˆ¨8xطφºb+Ê(GX`ØG²o¯Ã¢Þ<GbéÛ4øtÌb<C38C>Ã8båŒÕöí½* ~ÄǪlH04„úX• ‰Ž†Xåå É–ú‘ó‡µÛ A,R¡T†Õϧ±!:ÑÉàgWÕ£ ?£Ø[_ Œ†ŠQlˆWåÌà˜rl²ßäÄDîþT,nr<18>r,Bw¶iVß2˜©:ÝÙ&—¦ˆnF\FO£·Œ)ÝŒŒw°Œ òwl¾e0cüoT§¡‡çjÍÜÖ¦3<C2A6>å ûs…ÌÄaòu×Ñðáal{§iÍM„Ç=üw³Â<C2B3>껓èjÓº†GaFéRZD0A= Óu`JÝ>_À†™ÚŠh„Ý|§Â†ÙzÁöÎå°a¾Ó°ÍvÚ¸²a¡"Ûëçʆ%WŠkxéïœráÂ,,}G.ý<>Ó.l˜N³×t€<74>±®œA°½l¬ÃK¢ˆa;]_Ù°K-¯Ã<C2AF>Îbl¬ÃK2<4B>m¶ÓbŒ ƒ%XΔàE¿S/û˜]²ÑQ_`ð}Ù:ÚzÞǨMî²£ùlXjïÆ¶°=ñçº;#Ø^æÇ†õ7¤h7) uckK¢m}8ww ]ÊfÖVå¿jLÛïOQmÙa ëùÚÉv=Îs=y©¡l·(ª-Ç<>ÈKÛó[šÔÕ <0B>&úèR<C3A8>`X眩þ{£j¯Î;O;2U.¯¿é‡­r=ºàaëïl»Ç<C2BB>+=½­ç°vÇÙ¯Ôñ<Îðá+=쌼á0»9¿tœ­t”syZ½ 9_ñeLë¯<E280B9>έq"àjÏ<6A>ŽS+ˆ[<5B>se[B»ûà»4ßÓ¹³cÖ8—ŒÜûº`I•¨³°/§Žøð†ÿÞ1 fÁ„fäL2¬:ŠÕâ<D¢rÖ1NbqoÚ1Æ3ò|F®¢†Çqµµý¡¼ª†endstream
endobj
xref
0 12
0000000000 65535 f
0000000009 00000 n
0000000230 00000 n
0000000279 00000 n
0000000338 00000 n
0000000477 00000 n
0000000602 00000 n
0000001825 00000 n
0000001994 00000 n
0000002117 00000 n
0000005772 00000 n
0000005945 00000 n
trailer
<<
/ID [ <ccc3e4975c1cb1bed7c3f953a623ce6d> <ccc3e4975c1cb1bed7c3f953a623ce6d> ]
/Info 1 0 R
/Root 2 0 R
/Size 12
>>
startxref
8334
%%EOF

Some files were not shown because too many files have changed in this diff Show More