so-manager-dev.com/app/Services/ShjFiveService.php
Your Name 3960e062b9
All checks were successful
Deploy so-manager (auto) / deploy (push) Successful in 21s
SHJ-5駐輪場空きチェック
SHJ-13契約台数追加
2025-09-26 17:03:01 +09:00

651 lines
29 KiB
PHP
Raw Permalink 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 Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\OperatorQue;
use Exception;
/**
* SHJ-5 空き待ち通知処理サービス
*
* 駐輪場の空き状況を確認し、空き待ち予約者への通知処理を実行する
* 仕様書に基づくバックグラウンド定期バッチ処理
*/
class ShjFiveService
{
/**
* 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 // 実際のキュー登録失敗件数
);
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】駐輪場の空き状況を取得する
*
* 仕様書に基づくSQL:
* - zone表から標準台数と現在の契約台数を比較
* - 空きがある駐輪場の情報を取得
*
* @return array 駐輪場空き状況リスト
*/
private function getParkVacancyStatus(): array
{
try {
// ゾーン毎の契約台数を取得
$contractCounts = DB::table('regular_contract as T1')
->select([
'T1.park_id',
'T1.psection_id',
'T5.ptype_id',
DB::raw('count(T1.contract_id) as contract_count')
])
->join('park as T2', 'T1.park_id', '=', 'T2.park_id')
->join('price_a 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');
})
->where([
['T1.contract_flag', '=', 1], // 有効契約
['T2.park_close_flag', '=', 0], // 駐輪場未閉鎖
])
// 契約有効期間内の条件
->whereRaw("date_format(now(), '%Y%m%d') BETWEEN T1.contract_periods AND T1.contract_periode")
->groupBy(['T1.park_id', 'T1.psection_id', 'T5.ptype_id'])
->get()
->keyBy(function($item) {
return $item->park_id . '_' . $item->psection_id . '_' . $item->ptype_id;
});
// ゾーン情報と照合して空き状況を算出
$vacancyList = DB::table('zone as T1')
->select([
'T1.park_id',
'T2.park_name',
'T1.psection_id',
'T1.ptype_id',
'T1.zone_standard',
'T3.psection_subject',
'T4.ptype_subject'
])
->join('park as T2', 'T1.park_id', '=', 'T2.park_id')
->join('psection as T3', 'T1.psection_id', '=', 'T3.psection_id')
->join('ptype as T4', 'T1.ptype_id', '=', 'T4.ptype_id')
->where([
['T1.delete_flag', '=', 0], // ゾーン有効
['T2.park_close_flag', '=', 0], // 駐輪場開設
])
->get()
->map(function($zone) use ($contractCounts) {
$key = $zone->park_id . '_' . $zone->psection_id . '_' . $zone->ptype_id;
$contractCount = isset($contractCounts[$key]) ? $contractCounts[$key]->contract_count : 0;
$zone->contract_count = $contractCount;
$zone->vacant_count = max(0, $zone->zone_standard - $contractCount);
return $zone;
})
->filter(function($zone) {
return $zone->vacant_count > 0; // 空きがあるもののみ
})
->values();
Log::info('駐輪場空き状況算出完了', [
'total_zones' => count($vacancyList),
'vacant_zones' => $vacancyList->filter(function($v) {
return $v->vacant_count > 0;
})->count()
]);
return $vacancyList->toArray();
} catch (Exception $e) {
Log::error('駐輪場空き状況取得エラー', [
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* 【処理2】空き待ち者の情報を取得する
*
* 仕様書に基づく取得条件:
* - 契約未紐付contract_id IS NULL
* - 退会でないuser_quit_flag <> 1
* - 有効な予約valid_flag = 1
* - 予約日時順で取得reserve_date昇順
*
* @param int $parkId 駐輪場ID
* @param int $psectionId 車種区分ID
* @param int $ptypeId 駐輪分類ID
* @return array 空き待ち者情報リスト
*/
private function getWaitingUsersInfo(int $parkId, int $psectionId, int $ptypeId): array
{
try {
$waitingUsers = DB::table('reserve as T1')
->select([
'T1.reserve_id',
'T1.user_id',
'T1.park_id',
'T1.psection_id',
'T1.ptype_id',
'T1.reserve_order',
'T1.reserve_date',
'T1.reserve_manual', // 手動通知フラグ
'T1.contract_id', // 契約紐付確認用
'T2.user_name',
'T2.user_primemail',
'T2.user_submail', // 副メールアドレス
'T2.user_manual_regist_flag', // 手動登録フラグ
'T2.user_quit_flag', // 退会フラグ
'T3.park_name',
'T4.psection_subject',
'T5.ptype_subject'
])
->join('user as T2', 'T1.user_id', '=', 'T2.user_id')
->join('park as T3', 'T1.park_id', '=', 'T3.park_id')
->join('psection as T4', 'T1.psection_id', '=', 'T4.psection_id')
->join('ptype as T5', 'T1.ptype_id', '=', 'T5.ptype_id')
->where([
['T1.park_id', '=', $parkId],
['T1.psection_id', '=', $psectionId],
['T1.ptype_id', '=', $ptypeId],
['T1.valid_flag', '=', 1], // 有効な予約
['T2.user_quit_flag', '<>', 1] // 退会でない
])
->whereNull('T1.contract_id') // 契約未紐付
->orderBy('T1.reserve_date', 'asc') // 仕様書に基づく予約日時順
->get()
->toArray();
Log::info('空き待ち者情報取得完了', [
'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;
}
}
/**
* 空き待ち通知メールを送信
*
* 仕様書に基づくSHJ-7呼び出し
* - 主メールアドレス・副メールアドレスを正しく渡す
* - 必要なパラメータを全て設定
*
* @param object $waitingUser 空き待ち者情報
* @param object $parkVacancy 駐輪場空き情報
* @return array 送信結果
*/
private function sendVacancyNotificationMail(object $waitingUser, object $parkVacancy): array
{
try {
// ShjMailSendServiceを利用してメール送信
$mailService = app(ShjMailSendService::class);
// 空き待ち通知用のメールテンプレートID予約告知通知
// OperatorQueの定数と合わせて4番を使用
$mailTemplateId = 4; // 予約告知通知のテンプレートID
// 仕様書No1/No2に基づく主メール・副メール設定
$mainEmail = $waitingUser->user_primemail ?? '';
$subEmail = $waitingUser->user_submail ?? '';
// メール送信実行(仕様書準拠)
$mailResult = $mailService->executeMailSend(
$mainEmail,
$subEmail,
$mailTemplateId
);
Log::info('空き待ち通知メール送信試行完了', [
'user_id' => $waitingUser->user_id,
'main_email' => $mainEmail,
'sub_email' => $subEmail,
'mail_template_id' => $mailTemplateId,
'result_success' => $mailResult['success'] ?? false
]);
return $mailResult;
} 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',
$waitingUser->park_name ?? '',
$waitingUser->ptype_subject ?? '', // 駐輪分類名
$waitingUser->psection_subject ?? '', // 車種区分名
$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()
];
}
}
}