All checks were successful
Deploy api / deploy (push) Successful in 23s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
430 lines
15 KiB
PHP
430 lines
15 KiB
PHP
<?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',
|
||
'T1.zone_pplace_start', // 車室番号開始(NULLの場合は1)
|
||
'T1.zone_pplace_end', // 車室番号終了(NULLの場合は許容台数まで)
|
||
'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ごとに車室番号開始から順(インクリメント、内部変数.対象番号とする)に
|
||
* 車室番号終了に達するまで、定期契約マスタから該当する車室番号で
|
||
* 契約済みの情報の有無を確認する
|
||
*
|
||
* 【パフォーマンス改善】
|
||
* 変更前: 各車室番号ごとに1回SQLクエリ(最大1200回)
|
||
* 変更後: ゾーンごとに使用中の車室番号を一括取得(1回)
|
||
*
|
||
* 【車室番号範囲対応】
|
||
* zone_pplace_start: 開始番号(NULLの場合は1)
|
||
* zone_pplace_end: 終了番号(NULLの場合は開始番号+許容台数-1)
|
||
*
|
||
* @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) {
|
||
// 【改善】使用中の車室番号を一括取得(1回のクエリ)
|
||
$occupiedNumbers = $this->getOccupiedRoomNumbers(
|
||
$parkId,
|
||
$zone->zone_id,
|
||
(int) $zone->update_grace_period_start_date,
|
||
(int) $zone->update_grace_period_end_date
|
||
);
|
||
|
||
// 【車室番号範囲対応】開始・終了番号を取得
|
||
$start = $zone->zone_pplace_start ?? 1;
|
||
$end = $zone->zone_pplace_end ?? ($start + $zone->zone_tolerance - 1);
|
||
|
||
// PHP側で空き番号を検索(開始番号から終了番号まで)
|
||
for ($pplaceNo = $start; $pplaceNo <= $end; $pplaceNo++) {
|
||
// 使用中でなければ空き
|
||
if (!in_array($pplaceNo, $occupiedNumbers, true)) {
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用中の車室番号を一括取得
|
||
*
|
||
* 【パフォーマンス改善】
|
||
* 1ゾーンあたり1回のSQLクエリで全ての使用中車室番号を取得
|
||
*
|
||
* @param int $parkId 駐輪場ID
|
||
* @param int $zoneId ゾーンID
|
||
* @param int $updateGracePeriodStartDate 更新期間開始日
|
||
* @param int $updateGracePeriodEndDate 更新期間終了日
|
||
* @return array 使用中の車室番号配列
|
||
*/
|
||
private function getOccupiedRoomNumbers(
|
||
int $parkId,
|
||
int $zoneId,
|
||
int $updateGracePeriodStartDate,
|
||
int $updateGracePeriodEndDate
|
||
): array {
|
||
$currentDate = Carbon::now()->format('Y-m-d');
|
||
|
||
$query = DB::table('regular_contract as T1')
|
||
->select('T1.pplace_no')
|
||
->where('T1.park_id', $parkId)
|
||
->where('T1.zone_id', $zoneId)
|
||
->where('T1.contract_flag', 1)
|
||
->whereNotNull('T1.pplace_no');
|
||
|
||
// パターンA/B の条件(既存ロジックと同じ)
|
||
if ($updateGracePeriodStartDate <= $updateGracePeriodEndDate) {
|
||
// パターンA: 有効期間E >= 現在日付
|
||
$query->where('T1.contract_periode', '>=', $currentDate);
|
||
} else {
|
||
// パターンB: 有効期間E + 更新期間終了日 >= 現在日付
|
||
$query->whereRaw(
|
||
"DATE_ADD(T1.contract_periode, INTERVAL ? DAY) >= ?",
|
||
[$updateGracePeriodEndDate, $currentDate]
|
||
);
|
||
}
|
||
|
||
return $query->pluck('pplace_no')->map(function ($val) {
|
||
return (int) $val;
|
||
})->toArray();
|
||
}
|
||
|
||
/**
|
||
* 対象事室番号生成
|
||
*
|
||
* @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
|
||
]);
|
||
|
||
// 実際の事室番号設定ロジックをここに実装
|
||
// 具体的な仕様が必要な場合は後で追加実装
|
||
}
|
||
|
||
} |