api.so-manager-dev.com/app/Services/ShjFourCService.php
Your Name 8499e0a01c
All checks were successful
Deploy api / deploy (push) Successful in 23s
SHJ-4 SHJ-5改修
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:42:35 +09:00

430 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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
]);
// 実際の事室番号設定ロジックをここに実装
// 具体的な仕様が必要な場合は後で追加実装
}
}