Compare commits

...

4 Commits

Author SHA1 Message Date
e70c834bb0 Merge pull request '9/26 マージ' (#29) from main_watanabe into main
All checks were successful
Deploy so-manager (auto) / deploy (push) Successful in 20s
Reviewed-on: #29
2025-09-26 17:27:54 +09:00
eed5d85741 9/26 マージ
All checks were successful
Deploy preview (main_watanabe) / deploy (push) Successful in 13s
2025-09-26 17:27:15 +09:00
Your Name
3960e062b9 SHJ-5駐輪場空きチェック
All checks were successful
Deploy so-manager (auto) / deploy (push) Successful in 21s
SHJ-13契約台数追加
2025-09-26 17:03:01 +09:00
bbc7ae7e83 Merge pull request 'マイページお知らせ修正' (#28) from main_higashide into main
All checks were successful
Deploy so-manager (auto) / deploy (push) Successful in 21s
Reviewed-on: #28
2025-09-25 14:12:03 +09:00
8 changed files with 1602 additions and 77 deletions

View File

@ -0,0 +1,207 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjFiveService;
use App\Models\Batch\BatchLog;
/**
* 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()
{
$batchLog = null;
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-5 空き待ち通知処理を開始します。');
Log::info('SHJ-5 空き待ち通知処理開始', [
'start_time' => $startTime
]);
// SHJ-8共通処理呼び出し - 仕様書準拠
$batchLog = BatchLog::createBatchLog(
'SHJ-5空き待ち通知処理',
BatchLog::STATUS_START,
[
'device_id' => 1, // 仕様書device_id=1
'process_code' => 1, // 仕様書process_code=1
'status_comment' => '処理開始' // 仕様書:内部変数.ステータスコメント
],
'SHJ-5処理開始: 駐輪場空き状況確認と空き待ち通知処理'
);
// SHJ-5メイン処理実行
$result = $this->shjFiveService->executeParkVacancyNotification();
$endTime = now();
$this->info('SHJ-5 空き待ち通知処理が完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
// 処理結果表示
$this->displayProcessResult($result);
// SHJ-8共通処理 - バッチログ完了記録(仕様書準拠)
$logStatus = $result['success'] ? BatchLog::STATUS_SUCCESS : BatchLog::STATUS_ERROR;
// 仕様書準拠:サービス側で作成した完全なステータスコメントを使用
$statusComment = $result['success'] ?
($result['status_comment'] ?? 'ステータスコメント生成エラー') :
sprintf('処理失敗: %s', $result['message'] ?? 'エラー');
$batchLog->update([
'status' => $logStatus,
'end_time' => $endTime,
'message' => $result['message'],
'parameters' => [
'device_id' => 1, // 仕様書device_id=1
'process_code' => 1, // 仕様書process_code=1
'status_comment' => $statusComment, // 仕様書:内部変数.ステータスコメント(完全版)
'processed_parks_count' => $result['processed_parks_count'] ?? 0,
'vacant_parks_count' => $result['vacant_parks_count'] ?? 0,
'total_waiting_users' => $result['total_waiting_users'] ?? 0,
'notification_success_count' => $result['notification_success_count'] ?? 0,
'operator_queue_count' => $result['operator_queue_count'] ?? 0,
'error_count' => $result['error_count'] ?? 0,
'duration_seconds' => $result['duration_seconds'] ?? 0
],
'execution_count' => 1,
'success_count' => $result['notification_success_count'] ?? 0,
'error_count' => $result['error_count'] ?? 0
]);
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());
// SHJ-8共通処理 - エラーログ記録(仕様書準拠)
if ($batchLog) {
$statusComment = sprintf(
'メール正常終了件数0メール異常終了件数1キュー登録正常終了件数0キュー登録異常終了件数1 - 処理エラー: %s',
$e->getMessage()
);
$batchLog->update([
'status' => BatchLog::STATUS_ERROR,
'end_time' => now(),
'message' => 'SHJ-5処理中にエラーが発生: ' . $e->getMessage(),
'parameters' => [
'device_id' => 1, // 仕様書device_id=1
'process_code' => 1, // 仕様書process_code=1
'status_comment' => $statusComment // 仕様書:内部変数.ステータスコメント(完全版)
],
'error_details' => $e->getMessage(),
'error_count' => 1
]);
}
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,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

@ -31,14 +31,10 @@ class ParkingSearchController extends Controller
// 検索処理 // 検索処理
public function getParkData($city_name, $station_neighbor_station, $park_name) public function getParkData($city_name, $station_neighbor_station, $park_name)
{ {
// 検索仕様
// 駐輪場マスタの全件(条件を絞った場合はその条件に一致するもの)を取得。
// 併せて各マスタから追加情報を取得するが、その際のレコードは全て1対1で結びつく想定で暫定実装する
// ※設計書に詳細な記載なし。DBの定義上は1対多の可能性もあるが、その場合現在の画面イメージと矛盾するため、実態として無い想定で進める
// 駐輪場情報検索 // 駐輪場情報検索
$park = \DB::table('park as p') $park = \DB::table('park as p')
->select( ->select(
'p.park_id',
'p.park_name', 'p.park_name',
'p.park_adrs', 'p.park_adrs',
'p.price_memo', 'p.price_memo',
@ -49,15 +45,12 @@ class ParkingSearchController extends Controller
'p.update_grace_period_end_date', 'p.update_grace_period_end_date',
'p.update_grace_period_end_time', 'p.update_grace_period_end_time',
'c.city_name', 'c.city_name',
's.station_neighbor_station', 's.station_neighbor_station'
'z.psection_id',
'z.zone_standard'
) )
->leftJoin('city as c', 'p.city_id', '=', 'c.city_id') ->leftJoin('city as c', 'p.city_id', '=', 'c.city_id')
->leftJoin(\DB::raw( ->leftJoin(\DB::raw(
'(SELECT park_id, station_neighbor_station FROM station WHERE station_id IN (SELECT MAX(station_id) FROM station GROUP BY park_id)) as s' '(SELECT park_id, station_neighbor_station FROM station WHERE station_id IN (SELECT MAX(station_id) FROM station GROUP BY park_id)) as s'
),'p.park_id','=','s.park_id') ),'p.park_id','=','s.park_id');
->leftJoin('zone as z', 'p.park_id', '=', 'z.park_id');
// プルダウン指定の条件でwhere句を付与 // プルダウン指定の条件でwhere句を付与
if (!empty($city_name)) { if (!empty($city_name)) {
@ -76,42 +69,102 @@ class ParkingSearchController extends Controller
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
foreach ($park as $row) { foreach ($park as $row) {
// ゾーンマスタの情報から空き台数を取得する // ゾーンマスタの情報を取得する
$vacantInfo = \DB::table('zone') $zoneInfo = \DB::table('zone as z')
->selectRaw('SUM(zone_tolerance) - SUM(zone_number) as vacant') ->select(
->where('psection_id', $row->psection_id) 'z.psection_id',
->groupBy('psection_id') 'z.ptype_id',
->first(); \DB::raw('SUM(z.zone_standard) as zone_standard'),
\DB::raw('SUM(z.zone_number) as zone_number'),
\DB::raw('SUM(z.zone_tolerance) as zone_tolerance'),
'ps.psection_subject',
'pt.ptype_subject'
)
->join('ptype as pt', 'z.ptype_id', '=', 'pt.ptype_id')
->leftJoin('psection as ps', 'z.psection_id', '=', 'ps.psection_id')
->where('z.park_id', $row->park_id)
->groupBy('z.park_id', 'z.ptype_id', 'z.psection_id', 'ps.psection_subject', 'pt.ptype_subject')
->get();
// 定期予約マスタから予約中の台数を取得する // ゾーンマスタが0件の場合、次のデータへ
$reservedCount = \DB::table('reserve') if ($zoneInfo->isEmpty()) {
->where('psection_id', $row->psection_id) $form_data[] = [
->where('valid_flag', 1) 'park_name' => $row->park_name,
->count(); 'park_adrs' => $row->park_adrs,
'price_memo' => $row->price_memo,
'park_latitude' => $row->park_latitude,
'park_longitude' => $row->park_longitude,
'city_name' => $row->city_name,
'station_neighbor_station' => $row->station_neighbor_station,
'zone_data' => []
];
continue;
}
// 更新期間内判定 // 更新期間内判定
$update_start = $row->update_grace_period_start_date . ' ' . $row->update_grace_period_start_time; $update_start = $row->update_grace_period_start_date . ' ' . $row->update_grace_period_start_time;
$update_end = $row->update_grace_period_end_date . ' ' . $row->update_grace_period_end_time; $update_end = $row->update_grace_period_end_date . ' ' . $row->update_grace_period_end_time;
$is_update_period = ($now >= $update_start && $now <= $update_end); $is_update_period = ($now >= $update_start && $now <= $update_end);
// ボタン表示有無判定 // ゾーンマスタの件数分だけループする
$vacant = ($vacantInfo ? $vacantInfo->vacant : 0) - $reservedCount; $zone_data = [];
if ($vacant > 0 && $is_update_period) { // 定期契約ボタン (空き台数が1台以上かつ更新期間内) foreach ($zoneInfo as $zone) {
$status = 1;
} elseif ($vacant <= 0 && $is_update_period) { // 空き待ち予約ボタン (空き台数が0台以下かつ更新期間内) // 予約中件数取得
$status = 2; $reservedCount = \DB::table('reserve')
} elseif ($vacant <= 0 && !$is_update_period) { // 販売期間外ボタン (空き台数が0台以下かつ更新期間外) ->where('park_id', $row->park_id)
$status = 3; ->where('psection_id', $zone->psection_id)
} else { ->where('ptype_id', $zone->ptype_id)
$status = null; ->where('valid_flag', 1)
->count();
// ステータス(表示ボタン)判定
$status = 0; // 0:非表示, 1:定期契約, 2:空き待ち予約, 3:販売期間外
$zone_vacant = $zone->zone_tolerance - $zone->zone_number - $reservedCount;
if ($zone_vacant > 0 && $is_update_period) { // 定期契約ボタン (空き台数が1台以上かつ更新期間内)
$status = 1;
} elseif ($zone_vacant <= 0 && $is_update_period) { // 空き待ち予約ボタン (空き台数が0台以下かつ更新期間内)
$status = 2;
} elseif ($zone_vacant <= 0 && !$is_update_period) { // 販売期間外ボタン (空き台数が0台以下かつ更新期間外)
$status = 3;
}
// 返却用データに追加
$zone_data[] = [
'psection_subject' => $zone->psection_subject,
'ptype_subject' => $zone->ptype_subject,
'zone_standard' => $zone->zone_standard,
'zone_vacant' => $zone_vacant,
'status' => $status
];
} }
// $zone_dataを並び替え
usort($zone_data, function($a, $b) {
// 第一優先: ptype_subject昇順
$ptypeCmp = strcmp($a['ptype_subject'], $b['ptype_subject']);
if ($ptypeCmp !== 0) {
return $ptypeCmp;
}
// 第二優先: psection_subject昇順
$psectionCmp = strcmp($a['psection_subject'], $b['psection_subject']);
if ($psectionCmp !== 0) {
return $psectionCmp;
}
// 第三優先: status昇順
return $a['status'] <=> $b['status'];
});
// 画面返却用データに追加 // 画面返却用データに追加
$form_data[] = [ $form_data[] = [
'park_name' => $row->park_name, 'park_name' => $row->park_name,
'park_adrs' => $row->park_adrs,
'price_memo' => $row->price_memo,
'park_latitude' => $row->park_latitude,
'park_longitude' => $row->park_longitude,
'city_name' => $row->city_name, 'city_name' => $row->city_name,
'station_neighbor_station' => $row->station_neighbor_station, 'station_neighbor_station' => $row->station_neighbor_station,
'status' => $status 'zone_data' => $zone_data
]; ];
} }
@ -133,7 +186,10 @@ class ParkingSearchController extends Controller
'わ行'=>'わをんワヲン ' 'わ行'=>'わをんワヲン '
]; ];
// 車種区分リスト取得
$psections = \DB::table('psection')->select('psection_subject')->orderBy('psection_id', 'asc')->limit(4)->get();
// 情報返却 // 情報返却
return ['form_data' => $form_data, 'cities' => $cities, 'stations' => $stations, 'parks' => $parks, 'conditions' => $conditions]; return ['form_data' => $form_data, 'conditions' => $conditions, 'cities' => $cities, 'stations' => $stations, 'parks' => $parks, 'psections' => $psections];
} }
} }

View File

@ -0,0 +1,650 @@
<?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()
];
}
}
}

View File

@ -7,6 +7,7 @@ use App\Models\RegularContract;
use App\Models\Park; use App\Models\Park;
use App\Models\PriceA; use App\Models\PriceA;
use App\Models\Batch\BatchLog; use App\Models\Batch\BatchLog;
use App\Services\ShjThirteenService;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Carbon\Carbon; use Carbon\Carbon;
@ -170,6 +171,7 @@ class ShjFourBService
'T1.billing_amount', 'T1.billing_amount',
'T4.price_ptypeid as ptype_id', 'T4.price_ptypeid as ptype_id',
'T1.psection_id', 'T1.psection_id',
'T1.zone_id',
'T1.update_flag', 'T1.update_flag',
'T1.reserve_id', 'T1.reserve_id',
'T1.contract_payment_number', 'T1.contract_payment_number',
@ -765,26 +767,56 @@ class ShjFourBService
/** /**
* SHJ-13実行処理(新規のみ) * SHJ-13実行処理(新規のみ)
* *
* ShjThirteenServiceを使用した契約台数追加処理
*
* @param object $contract * @param object $contract
* @return array * @return array
*/ */
private function triggerShjThirteen($contract): array private function triggerShjThirteen($contract): array
{ {
// TODO: SHJ-13の具体的な処理を実装
// 現在はプレースホルダー
Log::info('SHJ-4B SHJ-13実行処理', [ Log::info('SHJ-4B SHJ-13実行処理', [
'contract_id' => $contract->contract_id, 'contract_id' => $contract->contract_id,
'user_id' => $contract->user_id,
'park_id' => $contract->park_id, 'park_id' => $contract->park_id,
'psection_id' => $contract->psection_id,
'ptype_id' => $contract->ptype_id,
'zone_id' => $contract->zone_id,
]); ]);
return [ try {
'triggered' => true, // 契約データ準備
'method' => 'placeholder', $contractData = [
'message' => 'SHJ-13処理は実装予定です', 'contract_id' => $contract->contract_id,
'contract_id' => $contract->contract_id, 'park_id' => $contract->park_id,
]; 'psection_id' => $contract->psection_id,
'ptype_id' => $contract->ptype_id,
'zone_id' => $contract->zone_id,
];
// ShjThirteenService実行
$shjThirteenService = app(ShjThirteenService::class);
$result = $shjThirteenService->execute($contractData);
Log::info('SHJ-4B SHJ-13実行完了', [
'contract_id' => $contract->contract_id,
'result' => $result,
]);
return $result;
} catch (\Throwable $e) {
Log::error('SHJ-4B SHJ-13実行エラー', [
'contract_id' => $contract->contract_id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return [
'result' => 1,
'error_code' => $e->getCode() ?: 1999,
'error_message' => $e->getMessage(),
'stack_trace' => $e->getTraceAsString(),
];
}
} }
/** /**

View File

@ -0,0 +1,346 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\Batch\BatchLog;
use Carbon\Carbon;
/**
* SHJ-13 契約台数追加処理サービス
*
* 新規契約時の契約台数を park_number・zone テーブルに反映する処理
* SHJ-4B の副作用処理として実行される
*/
class ShjThirteenService
{
/**
* 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);
}
// park_number テーブル更新
$parkNumberUpdated = DB::table('park_number')
->where('park_id', $contractData['park_id'])
->where('psection_id', $contractData['psection_id'])
->where('ptype_id', $contractData['ptype_id'])
->increment('park_number', 1, [
'updated_at' => now(),
'operator_id' => 'SHJ-13',
]);
if ($parkNumberUpdated === 0) {
throw new \Exception('park_numberテーブルの対象レコードが存在しません', 1011);
}
// zone テーブル更新
$zoneUpdated = DB::table('zone')
->where('zone_id', $contractData['zone_id'])
->increment('zone_number', 1, [
'updated_at' => now(),
'ope_id' => 'SHJ-13',
]);
if ($zoneUpdated === 0) {
throw new \Exception('zoneテーブルの対象レコードが存在しません', 1012);
}
// 更新後の契約台数取得
$updatedZone = DB::table('zone')
->where('zone_id', $contractData['zone_id'])
->first(['zone_number']);
// ステータスコメント構築
$statusComment = $this->buildStatusComment($names['names'], $updatedZone->zone_number);
// バッチ処理ログ作成(同一トランザクション内)
$currentDate = now()->format('Y/m/d');
$batchLog = BatchLog::createBatchLog(
'SHJ-13', // process_name
BatchLog::STATUS_SUCCESS,
[
'device_id' => 1,
'job_name' => 'SHJ-13',
'status' => 'success',
'status_comment' => $statusComment,
'created_date' => $currentDate,
'updated_date' => $currentDate,
'contract_id' => $contractData['contract_id'] ?? null,
'park_id' => $contractData['park_id'],
'psection_id' => $contractData['psection_id'],
'ptype_id' => $contractData['ptype_id'],
'zone_id' => $contractData['zone_id'],
],
$statusComment
);
Log::info('SHJ-13 契約台数更新・ログ作成完了', [
'contract_id' => $contractData['contract_id'],
'park_number_updated' => $parkNumberUpdated,
'zone_updated' => $zoneUpdated,
'updated_count' => $updatedZone->zone_number,
'batch_log_id' => $batchLog->id,
'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
);
}
/**
* エラー結果作成
*
* @param int $errorCode
* @param string $errorMessage
* @param string $stackTrace
* @return array
*/
private function createErrorResult(int $errorCode, string $errorMessage, string $stackTrace = ''): array
{
return [
'result' => 1,
'error_code' => $errorCode,
'error_message' => $errorMessage,
'stack_trace' => $stackTrace,
];
}
}

View File

@ -31,7 +31,7 @@
<h3 class="other mt50">開示等の依頼の手続き、使用する様式</h3> <h3 class="other mt50">開示等の依頼の手続き、使用する様式</h3>
<p class="p1">開示等の依頼は、以下の手続き及び様式に則って実施致します。</p> <p class="p1">開示等の依頼は、以下の手続き及び様式に則って実施致します。</p>
<p class="p1">利用目的の通知:本書面の“開示対象個人情報の利用目的”をご覧下さい。</p> <p class="p1">利用目的の通知:本書面の“開示対象個人情報の利用目的”をご覧下さい。</p>
<p class="p1">開示、訂正・削除、利用停止:当社の定める様式にて実施致します。該当の受付け窓口にご連絡頂き、所定の様式『<a href="{{ asset('assets/page-img/privacy_disclosure.pdf') }}" target="_blank">個人情報開示等依頼書(PDF:9KB)</a>』を入手のうえ、手続きをお願い致します。</p> <p class="p1">開示、訂正・削除、利用停止:当社の定める様式にて実施致します。該当の受付け窓口にご連絡頂き、所定の様式『<a href="{{ asset('assets/privacy_disclosure.pdf') }}" target="_blank">個人情報開示等依頼書(PDF:9KB)</a>』を入手のうえ、手続きをお願い致します。</p>
<p class="p1">回答に関しては、記入済み個人情報開示等依頼書を、ご自宅への郵送のみとさせて頂きます。</p> <p class="p1">回答に関しては、記入済み個人情報開示等依頼書を、ご自宅への郵送のみとさせて頂きます。</p>
<h3 class="other mt50">開示対象個人情報の取扱いに関する苦情受付け窓口</h3> <h3 class="other mt50">開示対象個人情報の取扱いに関する苦情受付け窓口</h3>
<p class="p1">開示対象個人情報の取扱いに関する苦情、相談に関しましては、以下の窓口宛てにご連絡ください。</p> <p class="p1">開示対象個人情報の取扱いに関する苦情、相談に関しましては、以下の窓口宛てにご連絡ください。</p>

View File

@ -65,10 +65,12 @@
<table id="searchTable" class="tablesorter table table-striped"> <table id="searchTable" class="tablesorter table table-striped">
<thead> <thead>
<tr> <tr>
<th>駐輪場名</th> <th width="20%">駐輪場名</th>
<th>市町村名</th> <th width="15%">市町村名</th>
<th>駅名</th> <th width="5%">駅名</th>
<th></th> @foreach($psections as $psection)
<th width="15%">{{ $psection->psection_subject }}</th>
@endforeach
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -82,18 +84,39 @@
@endphp @endphp
@foreach($pagedData as $data) @foreach($pagedData as $data)
<tr> <tr>
<td><a href="#placeModal" data-toggle="modal" data-target="#placeModal">{{ $data['park_name'] }}</a></td> <td>
<a href="#placeModal"
data-toggle="modal"
data-target="#placeModal"
data-park_name="{{ $data['park_name'] }}"
data-park_adrs="{{ $data['park_adrs'] ?? '' }}"
data-price_memo="{{ $data['price_memo'] ?? '' }}"
data-park_latitude="{{ $data['park_latitude'] ?? '' }}"
data-park_longitude="{{ $data['park_longitude'] ?? '' }}"
data-city_name="{{ $data['city_name'] }}"
data-station="{{ $data['station_neighbor_station'] }}"
data-zone_data='@json($data["zone_data"])'>
{{ $data['park_name'] }}
</a>
</td>
<td>{{ $data['city_name'] }}</td> <td>{{ $data['city_name'] }}</td>
<td>{{ $data['station_neighbor_station'] }}</td> <td>{{ $data['station_neighbor_station'] }}</td>
<td> @foreach($psections as $psection)
@if($data['status'] == 1) <td>
<a href="{{route('user.info')}}" class="btn btn-block btn-sm btn-outline-success">定期契約</a> @foreach($data['zone_data'] as $zone)
@elseif($data['status'] == 2) @if($zone['psection_subject'] == $psection->psection_subject)
<a href="{{route('park_waitlist.index')}}" class="btn btn-block btn-sm btn-outline-primary">空き待ち予約</a> @if($zone['status'] == 1)
@elseif($data['status'] == 3) <a href="{{route('user.info')}}" class="btn btn-block btn-sm btn-outline-success">定期契約</a>
<a href="{{route('park_waitlist.index')}}" class="btn btn-block btn-sm btn-secondary">販売期間外</a> @elseif($zone['status'] == 2)
@endif <a href="{{route('park_waitlist.index')}}" class="btn btn-block btn-sm btn-outline-primary">空き待ち予約</a>
</td> @elseif($zone['status'] == 3)
<a href="{{route('park_waitlist.index')}}" class="btn btn-block btn-sm btn-secondary">販売期間外</a>
@endif
@break;
@endif
@endforeach
</td>
@endforeach
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
@ -133,29 +156,86 @@
<div class="modal fade" id="placeModal" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal fade" id="placeModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <script>
<h5 class="modal-title" id="modalParkName">駐輪場名</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="閉じる"><span aria-hidden="true">&times;</span></button>
</div>
<script>
$(function() {
$('#placeModal').on('show.bs.modal', function (event) { $('#placeModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); var button = $(event.relatedTarget);
var parkName = button.text(); var parkName = button.data('park_name') || '';
$('#modalParkName').text(parkName); var parkAdrs = button.data('park_adrs') || '';
var parkMemo = button.data('price_memo') || '';
var lat = button.data('park_latitude') || '';
var lng = button.data('park_longitude') || '';
$('#parkName').text(parkName).attr('data-park_name', parkName);
$('#parkAdrs').text('住所:' + parkAdrs).attr('data-park_adrs', parkAdrs);
$('#parkmemo').text(parkMemo).attr('data-price_memo', parkMemo);
$('#parkMap').attr('src', 'https://www.google.com/maps?q=' + lat + ',' + lng + '&z=15&output=embed');
// zone_dataはJSON文字列として渡されているのでパース
var zoneData = button.data('zone_data') || [];
// psection_subjectごとの標準収容台数を集計
var standardMap = {};
if (zoneData && Array.isArray(zoneData)) {
zoneData.forEach(function(zone) {
if (!standardMap[zone.psection_subject]) {
standardMap[zone.psection_subject] = 0;
}
standardMap[zone.psection_subject] += parseInt(zone.zone_standard, 10) || 0;
});
}
// 表示用文字列を生成
var standardText = '';
var keys = Object.keys(standardMap);
if (keys.length > 0) {
standardText = '【標準収容台数】';
standardText += keys.map(function(key) {
return key + '' + standardMap[key] + '台';
}).join(' / ');
}
$('#parkStandard').text(standardText);
// 各ゾーンの空き台数・ボタンを表示
var html = '';
if (zoneData && Array.isArray(zoneData)) {
var grouped = {};
zoneData.forEach(function(zone) {
if (!grouped[zone.ptype_subject]) grouped[zone.ptype_subject] = [];
grouped[zone.ptype_subject].push(zone);
});
Object.keys(grouped).forEach(function(ptype) {
html += '<h4 class="mt-3">' + ptype + '</h4>';
grouped[ptype].forEach(function(zone) {
html += '<div class="d-flex align-items-center mb-2">';
html += '<span>' + zone.psection_subject + ':空き' + zone.zone_vacant + '台</span>';
if (zone.status == 1) {
html += '<a href="{{route('user.info')}}" class="btn btn-sm btn-outline-success ml-2">定期契約</a>';
} else if (zone.status == 2) {
html += '<a href="{{route('park_waitlist.index')}}" class="btn btn-sm btn-outline-primary ml-2">空き待ち予約</a>';
} else if (zone.status == 3) {
html += '<a href="{{route('park_waitlist.index')}}" class="btn btn-sm btn-outline-secondary ml-2">販売期間外</a>';
}
html += '</div>';
});
});
}
$('#zoneData').html(html);
}); });
}); </script>
</script> <div class="modal-header">
<h5 class="modal-title" id="parkName" data-park_name=""></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="閉じる"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body"> <div class="modal-body">
<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3240.722943699139!2d139.75162621525894!3d35.68382338019366!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x60188c0b185b3b75%3A0x3282e79fbc91959c!2z44CSMTAwLTAwMDEg5p2x5Lqs6YO95Y2D5Luj55Sw5Yy65Y2D5Luj55Sw77yR4oiS77yR!5e0!3m2!1sja!2sjp!4v1536695351221" width="100%" height="450" frameborder="0" style="border:0" allowfullscreen></iframe> <iframe id="parkMap" src="" width="100%" height="450" frameborder="0" style="border:0" allowfullscreen></iframe>
<p class="small">〒000-0000 東京都千代田区1-1 <br class="sp">標準収容台数XXX台</p> <p class="small">
<p class="text-danger">空き台数XXX台</p> <span id="parkAdrs"> </span>
<span id="parkStandard"></span><br />
<span id="parkmemo"></span>
</p>
<span id="zoneData"></span>
<div class="text-right"><button type="button" class="btn btn-outline-secondary" data-dismiss="modal">閉じる</button></div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer"></div>
<button type="submit" class="btn btn-success" onClick="location.href='./SWC-08-02.html'">定期契約</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">閉じる</button>
</div>
</div> </div>
</div> </div>
@endsection @endsection