so-manager-dev.com/app/Services/ShjElevenService.php
Your Name 10a917b556
All checks were successful
Deploy so-manager (auto) / deploy (push) Successful in 24s
【更新】SHJ関連の修正
2025-10-10 19:55:46 +09:00

790 lines
31 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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->formatStatusComment(
$contractData->park_name,
$contractData->ptype_subject,
$contractData->psection_subject,
$contractData->zone_id,
'' // INSERT分岐では台数アラートなし
);
// 【処理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->formatStatusComment(
$contractData->park_name,
$contractData->ptype_subject,
$contractData->psection_subject,
$contractData->zone_id,
$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' => null, // デフォルトnull
'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()
];
}
}
/**
* status_commentを仕様書指定フォーマットで作成
*
* フォーマット: 駐輪場名駐輪分類名車種区分名ゾーンID[台数アラート]
*
* @param string $parkName 駐輪場名
* @param string $ptypeSubject 駐輪分類名
* @param string $psectionSubject 車種区分名
* @param int $zoneId ゾーンID
* @param string $capacityAlert 台数アラート(「限界収容台数を超えています。」または空文字)
* @return string フォーマット済みstatus_comment
*/
private function formatStatusComment(
string $parkName,
string $ptypeSubject,
string $psectionSubject,
int $zoneId,
string $capacityAlert
): string {
$baseComment = $parkName . '' . $ptypeSubject . '' . $psectionSubject . '' . $zoneId;
if (!empty($capacityAlert)) {
return $baseComment . ' ' . $capacityAlert;
}
return $baseComment;
}
/**
* 【処理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()
];
}
}
}