SH-6 SHJ-9 実装
All checks were successful
Deploy api / deploy (push) Successful in 24s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
unhi.go 2026-02-20 20:16:47 +08:00
parent 41814dd908
commit e1073e2577
8 changed files with 1072 additions and 496 deletions

View File

@ -18,12 +18,11 @@ class ShjNineCommand extends Command
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - type: 集計種別 (daily のみ) (必須)
* - target_date: 集計対象日 (オプション、YYYY-MM-DD形式)
*
* @var string
*/
protected $signature = 'shj:9 {type : 集計種別(daily)} {target_date? : 集計対象日(YYYY-MM-DD)}';
protected $signature = 'shj:9 {target_date? : 集計対象日(YYYY-MM-DD)}';
/**
* コンソールコマンドの説明
@ -54,52 +53,50 @@ class ShjNineCommand extends Command
* コンソールコマンドを実行
*
* 処理フロー:
* 1. パラメータ取得と検証
* 2. 集計対象日設定
* 3. 売上集計処理実行
* 4. バッチログ作成
* 5. 処理結果返却
* 1. 集計対象日設定JOB1
* 2. 売上集計処理実行JOB2JOB4
* 3. バッチログ作成JOB5
*
* 日付バリデーション・バッチログ作成はService側で実施
* ステータスは常にsuccess式様書準拠
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-9 売上集計処理を開始します。');
// 引数取得
$type = $this->argument('type');
// 集計種別は日次固定SHJ-9は日次のみ
$type = 'daily';
// 集計対象日設定JOB1
$targetDate = $this->argument('target_date');
// パラメータ指定がない場合は昨日本日の1日前
$aggregationDate = $targetDate ?? now()->subDay()->format('Y-m-d');
Log::info('SHJ-9 売上集計処理開始', [
'start_time' => $startTime,
'type' => $type,
'target_date' => $targetDate
'target_date' => $aggregationDate
]);
// パラメータ検証
if (!$this->validateParameters($type, $targetDate)) {
$this->error('パラメータが不正です。');
return self::FAILURE;
}
// 集計対象日設定
$aggregationDate = $this->determineAggregationDate($type, $targetDate);
$this->info("集計種別: {$type}");
$this->info("集計対象日: {$aggregationDate}");
// SHJ-9処理実行
// SHJ-9処理実行日付バリデーション・バッチログ作成含む
$result = $this->shjNineService->executeEarningsAggregation($type, $aggregationDate);
// 処理結果確認
if ($result['success']) {
$endTime = now();
$this->info('SHJ-9 売上集計処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
if ($result['success']) {
$this->info('SHJ-9 売上集計処理が正常に完了しました。');
$this->info("処理結果: 駐輪場数 {$result['processed_parks']}, 集計レコード数 {$result['summary_records']}");
} else {
$this->info('SHJ-9 売上集計処理が完了しました(エラーあり): ' . $result['message']);
}
Log::info('SHJ-9 売上集計処理完了', [
'end_time' => $endTime,
@ -107,84 +104,7 @@ class ShjNineCommand extends Command
'result' => $result
]);
// 式様書準拠: ステータスは常にsuccess
return self::SUCCESS;
} else {
$this->error('SHJ-9 売上集計処理でエラーが発生しました: ' . $result['message']);
Log::error('SHJ-9 売上集計処理エラー', [
'error' => $result['message'],
'details' => $result['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-9 売上集計処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-9 売上集計処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
/**
* パラメータの妥当性を検証
*
* @param string $type 集計種別
* @param string|null $targetDate 対象日
* @return bool 検証結果
*/
private function validateParameters(string $type, ?string $targetDate): bool
{
// 集計種別チェックSHJ-9 は日次のみ対応)
if ($type !== 'daily') {
$this->error('SHJ-9 は日次集計dailyのみ対応しています。月次/年次は SHJ-10 を使用してください。');
return false;
}
// 対象日形式チェック(指定されている場合)
if ($targetDate && !$this->isValidDateFormat($targetDate)) {
$this->error('対象日の形式が正しくありませんYYYY-MM-DD形式で指定してください。');
return false;
}
return true;
}
/**
* 集計対象日を決定
*
* @param string $type 集計種別daily 固定)
* @param string|null $targetDate 指定日
* @return string 集計対象日
*/
private function determineAggregationDate(string $type, ?string $targetDate): string
{
if ($targetDate) {
return $targetDate;
}
// パラメータ指定がない場合は昨日本日の1日前
return now()->subDay()->format('Y-m-d');
}
/**
* 日付形式の検証
*
* @param string $date 日付文字列
* @return bool 有効な日付形式かどうか
*/
private function isValidDateFormat(string $date): bool
{
// YYYY-MM-DD形式の正規表現チェック
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return false;
}
// 実際の日付として有効かチェック
$dateParts = explode('-', $date);
return checkdate((int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]);
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjTwoService;
/**
* SHJ-2 データバックアップコマンド
*
* データベースの夜間自動バックアップを実行する
* フルバックアップを5世代保持する
* 定期実行(日次 02:45)またはオンデマンド実行
*/
class ShjTwoCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数なし - 全自動でバックアップを実行
*
* @var string
*/
protected $signature = 'shj:2';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-2 データバックアップ - データベースとソースコードのフルバックアップを実行5世代保持';
/**
* SHJ-2サービスクラス
*
* @var ShjTwoService
*/
protected $shjTwoService;
/**
* コンストラクタ
*
* @param ShjTwoService $shjTwoService
*/
public function __construct(ShjTwoService $shjTwoService)
{
parent::__construct();
$this->shjTwoService = $shjTwoService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* <JOB1> 現時点の世代シフトを行う
* <JOB2> フルバックアップを行う
* <JOB3> バッチ処理ログを作成する
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-2 データバックアップ処理を開始します。');
Log::info('SHJ-2 データバックアップ処理開始', [
'start_time' => $startTime
]);
// SHJ-2メイン処理実行
$result = $this->shjTwoService->execute();
$endTime = now();
// 処理結果に応じた出力
if ($result['success']) {
$this->info('SHJ-2 データバックアップ処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
$this->info("ステータス: {$result['status']}");
$this->info("コメント: {$result['status_comment']}");
Log::info('SHJ-2 データバックアップ処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'status' => $result['status'],
'status_comment' => $result['status_comment']
]);
return self::SUCCESS;
} else {
$this->error('SHJ-2 データバックアップ処理でエラーが発生しました: ' . $result['status_comment']);
Log::error('SHJ-2 データバックアップ処理エラー', [
'status' => $result['status'],
'status_comment' => $result['status_comment']
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-2 データバックアップ処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-2 データバックアップ処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -38,6 +38,7 @@ class OperatorQue extends Model
11 => '名寄せフリガナ照合エラー',
12 => '本人確認(減免更新)',
13 => '本人確認(学生更新)',
14 => '集計対象エラー',
101 => 'サーバーエラー',
102 => 'プリンタエラー',
103 => 'スキャナーエラー',

View File

@ -29,6 +29,11 @@ class ShjNineService
*/
const SUMMARY_TYPE_DAILY = 3;
/**
* 情報不備なしの固定文字列(全角スペース付き)
*/
const DATA_INTEGRITY_NONE = ' 情報不備:なし';
/**
* ShjEightService インスタンス
*
@ -65,24 +70,27 @@ class ShjNineService
public function executeEarningsAggregation(string $type, string $aggregationDate): array
{
$statusComments = []; // 内部変数.ステータスコメント
$dataIntegrityIssues = []; // 内部変数.情報不備
$allDataIntegrity = []; // 内部変数.情報不備(なし含む全件)
$dataIntegrityIssues = []; // オペレータキュー作成用NULL以外
try {
// 【処理1】集計対象を設定する
// パラメーター検証(日付形式チェック)
if (!$this->isValidDateFormat($aggregationDate)) {
// 日付形式エラー時は【処理5】へ(warning扱い
// 日付形式エラー時は【処理5】へ(仕様JOB1ステータスコメント設定→JOB5
$statusComment = "売上集計(日次):パラメーターが不正です。(日付形式ではありません)";
// 【処理5】オペレータキュー作成
$this->createOperatorQueue($statusComment, null);
// 仕様JOB5情報不備がNULL→オペレータキュー作成しない日付エラーは情報不備ではない
// SHJ-8 バッチ処理ログ作成
$this->callShjEight('SHJ-9売上集計日次', 'success', $statusComment);
// SHJ-8 バッチ処理ログ作成仕様JOB5ステータスは常にsuccess
$shjEightResult = $this->callShjEight('SHJ-9売上集計日次', 'success', $statusComment);
$this->evaluateShjEightResult($shjEightResult);
return [
'success' => true, // 仕様上はwarningで成功扱い
'message' => $statusComment
'message' => $statusComment,
'processed_parks' => 0,
'summary_records' => 0
];
}
@ -99,13 +107,12 @@ class ShjNineService
// 【判断1】取得件数判定
if (empty($parkInfo)) {
$statusComment = '売上集計(日次):駐輪場マスタが存在していません。';
$statusComments[] = $statusComment;
// 【処理5】オペレータキュー作成
$this->createOperatorQueue($statusComment, null);
// 仕様JOB2-STEP1情報不備がNULL→オペレータキュー作成しない
// SHJ-8 バッチ処理ログ作成
$this->callShjEight('SHJ-9売上集計日次', 'success', $statusComment);
// SHJ-8 バッチ処理ログ作成仕様JOB5ステータスは常にsuccess
$shjEightResult = $this->callShjEight('SHJ-9売上集計日次', 'success', $statusComment);
$this->evaluateShjEightResult($shjEightResult);
return [
'success' => true,
@ -115,7 +122,7 @@ class ShjNineService
];
}
// 【処理3】車種区分毎に算出する & 【処理4】売上集計結果を削除→登録する
// 仕様JOB3/JOB4車種区分毎に算出→売上集計結果を登録
$summaryRecords = 0;
$processedParks = 0;
@ -125,39 +132,53 @@ class ShjNineService
$processedParks++;
$summaryRecords += $result['summary_records'];
// 対象データなしの場合のステータスコメント収集
// 仕様JOB4レコード毎のステータスコメントを収集
if (!empty($result['status_comments'])) {
$statusComments = array_merge($statusComments, $result['status_comments']);
}
// 仕様JOB3-STEP1対象データなしの場合のステータスコメント
if (!empty($result['no_data_message'])) {
$statusComments[] = $result['no_data_message'];
}
// 情報不備を収集("なし"でない場合)
if ($result['data_integrity_issue'] !== '情報不備:なし') {
$dataIntegrityIssues[] = $result['data_integrity_issue'];
// 仕様JOB5全parkの情報不備を収集なし含む
$allDataIntegrity[] = $result['data_integrity_issue'];
// 仕様JOB5NULL以外の場合にオペレータキューを登録
if ($result['data_integrity_issue'] !== null) {
$dataIntegrityIssues[] = [
'park_id' => $park->park_id,
'message' => $result['data_integrity_issue']
];
}
}
// 最終ステータスコメント生成
$finalStatusComment = "売上集計(日次):対象日={$targetDate}、駐輪場数={$processedParks}、集計レコード数={$summaryRecords}";
if (!empty($dataIntegrityIssues)) {
$finalStatusComment .= "、情報不備=" . implode('、', $dataIntegrityIssues);
// 仕様JOB5情報不備がNULL以外の場合、オペレータキューを登録
foreach ($dataIntegrityIssues as $issue) {
$this->createOperatorQueue($issue['message'], $issue['park_id']);
}
// 【処理5】オペレータキュー作成
// ※ 駐輪場単位で既に作成済みprocessEarningsForPark内で情報不備検出時に実施
if (!empty($dataIntegrityIssues)) {
Log::warning('SHJ-9 情報不備検出', [
'issues' => $dataIntegrityIssues
]);
}
// SHJ-8 バッチ処理ログ作成
$this->callShjEight('SHJ-9売上集計日次', 'success', $finalStatusComment);
// 仕様JOB5ステータスコメント = 内部変数.ステータスコメント + 内部変数.情報不備(なし含む全件)
$allParts = array_merge($statusComments, $allDataIntegrity);
$finalStatusComment = implode("\n", $allParts);
Log::info('SHJ-9 完全ステータスコメント', ['status_comment' => $finalStatusComment]);
// 仕様JOB5SHJ-8 バッチ処理ログ作成ステータスは常にsuccess
$shjEightResult = $this->callShjEight('SHJ-9売上集計日次', 'success', $finalStatusComment);
$this->evaluateShjEightResult($shjEightResult);
Log::info('SHJ-9 売上集計処理完了', [
'processed_parks' => $processedParks,
'summary_records' => $summaryRecords,
'data_integrity_issues' => count($dataIntegrityIssues),
'no_data_parks' => count($statusComments)
'data_integrity_issues' => count($dataIntegrityIssues)
]);
return [
@ -173,7 +194,8 @@ class ShjNineService
// SHJ-8 バッチ処理ログ作成(エラー時も作成)
try {
$this->callShjEight('SHJ-9売上集計日次', 'error', $errorMessage);
$shjEightResult = $this->callShjEight('SHJ-9売上集計日次', 'success', $errorMessage);
$this->evaluateShjEightResult($shjEightResult);
} catch (\Exception $shjException) {
Log::error('SHJ-8呼び出しエラー', ['error' => $shjException->getMessage()]);
}
@ -192,19 +214,21 @@ class ShjNineService
}
/**
* 日付形式の検証
* 日付形式の検証厳格YYYY-MM-DD形式のみ許可
*
* @param string $date 日付文字列
* @return bool 有効な日付形式かどうか
*/
private function isValidDateFormat(string $date): bool
{
try {
$parsed = Carbon::parse($date);
return true;
} catch (\Exception $e) {
// YYYY-MM-DD形式の正規表現チェック
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return false;
}
// 実際の日付として有効かチェック
$dateParts = explode('-', $date);
return checkdate((int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]);
}
/**
@ -242,60 +266,56 @@ class ShjNineService
*
* @param object $park 駐輪場情報
* @param string $targetDate 集計対象日YYYY-MM-DD
* @return array 処理結果 ['summary_records' => int, 'data_integrity_issue' => string, 'no_data_message' => string|null]
* @return array 処理結果
*/
private function processEarningsForPark($park, string $targetDate): array
{
try {
// 0. 情報不備チェック
// 仕様JOB3-0情報不備チェックSQL-2
$dataIntegrityIssue = $this->checkDataIntegrity($park->park_id, $targetDate);
// 情報不備がある場合、駐輪場単位でオペレータキュー作成(仕様 todo/SHJ-9/SHJ-9.txt:253-263
if ($dataIntegrityIssue !== '情報不備:なし') {
$this->createOperatorQueue($dataIntegrityIssue, $park->park_id);
}
// ① 定期契約データ取得(車種区分・分類名1・定期有効月数毎)
// 仕様JOB3-①:定期契約データ取得(車種区分・分類名1・定期有効月数毎)
$regularData = $this->calculateRegularEarnings($park->park_id, $targetDate);
// 一時金データ取得(車種毎)
// 仕様JOB3-②:一時金データ取得(車種毎)
$lumpsumData = $this->calculateLumpsumEarnings($park->park_id, $targetDate);
// 解約返戻金データ取得(車種区分毎)
// 仕様JOB3-③:解約返戻金データ取得(車種区分毎)
$refundData = $this->calculateRefundEarnings($park->park_id, $targetDate);
// 再発行データ取得(車種区分毎)
// 仕様JOB3-④:再発行データ取得(車種区分毎)
$reissueData = $this->calculateReissueCount($park->park_id, $targetDate);
// 【判断2】データがいずれかあれば【処理4】
// 仕様JOB3-STEP1①②③④のデータがいずれかあれば→JOB4
if (empty($regularData) && empty($lumpsumData) && empty($refundData) && empty($reissueData)) {
// 対象データなし - 仕様 todo/SHJ-9/SHJ-9.txt:172-175
// 仕様JOB3-STEP1対象データなし
$noDataMessage = "売上集計(日次):対象日:{$targetDate}/駐輪場:{$park->park_name}:売上データが存在しません。";
return [
'summary_records' => 0,
'data_integrity_issue' => $dataIntegrityIssue,
'no_data_message' => $noDataMessage
'no_data_message' => $noDataMessage,
'status_comments' => []
];
}
// 【処理4】既存の売上集計結果を削除
$this->deleteExistingSummary($park->park_id, $targetDate);
// 【処理4】売上集計結果を登録
// 仕様JOB4売上集計結果を登録同一キーの既存レコードは各insert前に削除
$summaryRecords = 0;
$statusComments = [];
// ① 定期契約データがある場合同じ組合せpsection×usertype×monthsを統合
// ① 定期契約データ同じ組合せpsection×usertype×monthsを統合
$mergedRegularData = $this->mergeRegularDataByGroup($regularData);
foreach ($mergedRegularData as $key => $mergedRow) {
$this->createEarningsSummary($park, $mergedRow, $targetDate, 'regular');
$sc = $this->createEarningsSummary($park, $mergedRow, $targetDate, 'regular', $dataIntegrityIssue);
$statusComments[] = $sc;
$summaryRecords++;
}
// ②③④ 一時金・解約・再発行データがある場合(車種区分毎に集約)
// ②③④ 一時金・解約・再発行データ(車種区分毎に集約)
$otherDataByPsection = $this->mergeOtherEarningsData($lumpsumData, $refundData, $reissueData);
foreach ($otherDataByPsection as $psectionId => $data) {
$this->createEarningsSummary($park, $data, $targetDate, 'other');
$sc = $this->createEarningsSummary($park, $data, $targetDate, 'other', null);
$statusComments[] = $sc;
$summaryRecords++;
}
@ -309,7 +329,8 @@ class ShjNineService
return [
'summary_records' => $summaryRecords,
'data_integrity_issue' => $dataIntegrityIssue,
'no_data_message' => null
'no_data_message' => null,
'status_comments' => $statusComments
];
} catch (\Exception $e) {
@ -333,8 +354,9 @@ class ShjNineService
*/
private function checkDataIntegrity(int $parkId, string $targetDate): string
{
// 仕様SQL-2情報不備チェックログ吐き出し
$incompleteContracts = DB::table('regular_contract')
->select('contract_id')
->select(['contract_id', 'contract_reduction', 'update_flag', 'psection_id', 'enable_months'])
->where('park_id', $parkId)
->where('contract_flag', 1)
->whereDate('contract_payment_day', '=', $targetDate)
@ -343,15 +365,26 @@ class ShjNineService
->orWhereNull('psection_id')
->orWhereNull('enable_months');
})
->pluck('contract_id')
->get()
->toArray();
if (empty($incompleteContracts)) {
return '情報不備:なし';
return self::DATA_INTEGRITY_NONE;
}
// 仕様フォーマット:"情報不備:" + 契約IDカンマ区切り
return '情報不備:' . implode(',', $incompleteContracts);
// 仕様SQL-2ログ吐き出し詳細情報を出力
Log::warning('SHJ-9 情報不備契約詳細', [
'park_id' => $parkId,
'target_date' => $targetDate,
'contracts' => $incompleteContracts
]);
// 仕様フォーマット:" 情報不備:" + 契約IDカンマ区切り全角スペース付き
$contractIds = array_map(function($c) {
return is_object($c) ? $c->contract_id : $c['contract_id'];
}, $incompleteContracts);
return ' 情報不備:' . implode(',', $contractIds);
}
/**
@ -611,66 +644,92 @@ class ShjNineService
}
/**
* 【処理4】既存の売上集計結果を削除
* 車種区分名を取得
*
* 仕様書のキー駐輪場ID, 集計種別, 集計開始日, 集計終了日, 売上日付, 車種区分, 分類名1, 定期有効月数
* 仕様 todo/SHJ-9/SHJ-9.txt:181-189
*
* @param int $parkId 駐輪場ID
* @param string $targetDate 集計対象日YYYY-MM-DD
* @return void
* @param int|null $psectionId 車種区分ID
* @return string 車種区分名
*/
private function deleteExistingSummary(int $parkId, string $targetDate): void
private function getPsectionName(?int $psectionId): string
{
// 仕様書どおり、同一キーの組み合わせで削除
// 日次の場合、集計開始日・終了日はNULL、売上日付で判定
DB::table('earnings_summary')
->where('park_id', $parkId)
->where('summary_type', self::SUMMARY_TYPE_DAILY)
->whereNull('summary_start_date')
->whereNull('summary_end_date')
->where('earnings_date', $targetDate)
// psection_id, usertype_subject1, enable_months は
// レコードごとに異なるため、ここでは指定しない
->delete();
if ($psectionId === null) {
return '';
}
Log::debug('既存の売上集計結果削除', [
'park_id' => $parkId,
'target_date' => $targetDate
]);
$name = DB::table('psection')
->where('psection_id', $psectionId)
->value('psection_subject');
return $name ?? '';
}
/**
* 売上集計結果を登録
* 仕様JOB4売上集計結果を削除→登録
*
* 仕様 todo/SHJ-9/SHJ-9.txt:181-247
* 同一キー駐車場ID,集計種別,集計開始日,集計終了日,売上日付,車種区分,分類名1,定期有効月数)
* が既に登録済みの場合削除した上で、新規レコードを登録する。
*
* @param object $park 駐輪場情報
* @param object $data 売上データ
* @param string $targetDate 集計対象日YYYY-MM-DD
* @param string $dataType データ種別regular or other
* @return void
* @param string|null $dataIntegrityIssue 情報不備メッセージregularのみ使用
* @return string 仕様JOB4形式のステータスコメント
*/
private function createEarningsSummary($park, $data, string $targetDate, string $dataType): void
private function createEarningsSummary($park, $data, string $targetDate, string $dataType, ?string $dataIntegrityIssue = null): string
{
$usertypeSubject1 = $data->usertype_subject1 ?? null;
$enableMonths = $data->enable_months ?? 0;
// 仕様JOB4同一キーの既存レコードを削除
$deleteQuery = DB::table('earnings_summary')
->where('park_id', $park->park_id)
->where('summary_type', self::SUMMARY_TYPE_DAILY)
->whereNull('summary_start_date')
->whereNull('summary_end_date')
->where('earnings_date', $targetDate);
// NULL値対応psection_idがnullの場合はwhereNull
if ($data->psection_id === null) {
$deleteQuery->whereNull('psection_id');
} else {
$deleteQuery->where('psection_id', $data->psection_id);
}
// NULL値対応usertype_subject1がnullの場合はwhereNull
if ($usertypeSubject1 === null) {
$deleteQuery->whereNull('usertype_subject1');
} else {
$deleteQuery->where('usertype_subject1', $usertypeSubject1);
}
$deleteQuery->where('enable_months', $enableMonths);
$deleteQuery->delete();
// 仕様:集計備考の生成
if ($dataType === 'regular' && $dataIntegrityIssue !== null) {
// 仕様JOB3-①定期データのsummary_noteには情報不備を含める
$summaryNote = "SHJ-9{$targetDate}{$dataIntegrityIssue}";
} else {
// 仕様JOB3-②③④otherデータのsummary_note
$summaryNote = "SHJ-9{$targetDate}";
}
$insertData = [
'park_id' => $park->park_id,
'summary_type' => self::SUMMARY_TYPE_DAILY, // 3 = 日次
'summary_start_date' => null, // 日次は NULL (仕様line 215)
'summary_end_date' => null, // 日次は NULL (仕様line 216)
'summary_type' => self::SUMMARY_TYPE_DAILY,
'summary_start_date' => null,
'summary_end_date' => null,
'earnings_date' => $targetDate,
'psection_id' => $data->psection_id,
'usertype_subject1' => $data->usertype_subject1 ?? null, // 実際の分類名
'enable_months' => $data->enable_months ?? 0, // 実際の定期有効月数
'summary_note' => "SHJ-9{$targetDate}", // 仕様line 236
'usertype_subject1' => $usertypeSubject1,
'enable_months' => $enableMonths,
'summary_note' => $summaryNote,
'created_at' => now(),
'updated_at' => now(),
'operator_id' => self::BATCH_OPERATOR_ID // 9999999 (仕様line 239)
'operator_id' => self::BATCH_OPERATOR_ID
];
if ($dataType === 'regular') {
// 定期契約データの場合mergeRegularDataByGroup()で既に統合済み
// 新規/更新 × 減免/通常 の各件数・金額がすべて含まれている (仕様line 98-113)
// 仕様JOB3-①:定期契約データ
$insertData = array_merge($insertData, [
'regular_new_count' => $data->regular_new_count ?? 0,
'regular_new_amount' => $data->regular_new_amount ?? 0,
@ -680,43 +739,50 @@ class ShjNineService
'regular_update_amount' => $data->regular_update_amount ?? 0,
'regular_update_reduction_count' => $data->regular_update_reduction_count ?? 0,
'regular_update_reduction_amount' => $data->regular_update_reduction_amount ?? 0,
'lumpsum_count' => 0, // 仕様line 106
'lumpsum' => 0, // 仕様line 107
'refunds' => 0, // 仕様line 108
'other_income' => 0, // 仕様line 109
'other_spending' => 0, // 仕様line 110
'reissue_count' => 0, // 仕様line 111
'reissue_amount' => 0 // 仕様line 112
'lumpsum_count' => 0,
'lumpsum' => 0,
'refunds' => 0,
'other_income' => 0,
'other_spending' => 0,
'reissue_count' => 0,
'reissue_amount' => 0
]);
} else {
// 一時金・解約・再発行データの場合定期フィールドは0固定 (仕様line 152-167)
// 仕様JOB3-②③④一時金・解約・再発行データ定期フィールドは0固定
$insertData = array_merge($insertData, [
'regular_new_count' => 0, // 仕様line 152
'regular_new_amount' => 0, // 仕様line 153
'regular_new_reduction_count' => 0, // 仕様line 154
'regular_new_reduction_amount' => 0, // 仕様line 155
'regular_update_count' => 0, // 仕様line 156
'regular_update_amount' => 0, // 仕様line 157
'regular_update_reduction_count' => 0, // 仕様line 158
'regular_update_reduction_amount' => 0, // 仕様line 159
'lumpsum_count' => $data->lumpsum_count ?? 0, // 仕様line 160
'lumpsum' => $data->lumpsum ?? 0, // 仕様line 161
'refunds' => $data->refunds ?? 0, // 仕様line 162
'other_income' => 0, // 仕様line 163
'other_spending' => 0, // 仕様line 164
'reissue_count' => $data->reissue_count ?? 0, // 仕様line 165
'reissue_amount' => 0 // 仕様line 166: 0固定
'regular_new_count' => 0,
'regular_new_amount' => 0,
'regular_new_reduction_count' => 0,
'regular_new_reduction_amount' => 0,
'regular_update_count' => 0,
'regular_update_amount' => 0,
'regular_update_reduction_count' => 0,
'regular_update_reduction_amount' => 0,
'lumpsum_count' => $data->lumpsum_count ?? 0,
'lumpsum' => $data->lumpsum ?? 0,
'refunds' => $data->refunds ?? 0,
'other_income' => 0,
'other_spending' => 0,
'reissue_count' => $data->reissue_count ?? 0,
'reissue_amount' => 0
]);
}
DB::table('earnings_summary')->insert($insertData);
// 仕様JOB4レコード毎のステータスコメント生成全データ型共通フォーマット
$psectionName = $this->getPsectionName($data->psection_id);
$displayUsertype = $usertypeSubject1 ?? '';
$statusComment = "売上集計(日次):対象日:{$targetDate}/駐輪場:{$park->park_name}/車種区分:{$psectionName}/分類名1:{$displayUsertype}/定期有効月数:{$enableMonths}";
Log::debug('売上集計結果登録', [
'park_id' => $park->park_id,
'psection_id' => $data->psection_id,
'data_type' => $dataType,
'target_date' => $targetDate
]);
return $statusComment;
}
/**
@ -777,9 +843,9 @@ class ShjNineService
* @param string $jobName ジョブ名
* @param string $status ステータス (success/error)
* @param string $statusComment 業務固有のステータスコメント
* @return void
* @return array SHJ-8実行結果
*/
private function callShjEight(string $jobName, string $status, string $statusComment): void
private function callShjEight(string $jobName, string $status, string $statusComment): array
{
try {
$device = Device::orderBy('device_id')->first();
@ -787,7 +853,7 @@ class ShjNineService
$today = now()->format('Y/m/d');
$this->shjEightService->execute(
$result = $this->shjEightService->execute(
$deviceId,
'SHJ-9',
$jobName,
@ -799,9 +865,12 @@ class ShjNineService
Log::info('SHJ-8 バッチ処理ログ作成完了', [
'job_name' => $jobName,
'status' => $status
'status' => $status,
'shj8_result' => $result['result'] ?? null
]);
return $result;
} catch (\Exception $e) {
Log::error('SHJ-8 バッチ処理ログ作成エラー', [
'error' => $e->getMessage(),
@ -811,4 +880,24 @@ class ShjNineService
throw $e;
}
}
/**
* SHJ-8実行結果の判定
*
* 仕様JOB5の判定処理結果=0/その他)をログで明示する。
*
* @param array $shjEightResult SHJ-8返却値
* @return void
*/
private function evaluateShjEightResult(array $shjEightResult): void
{
if ((int)($shjEightResult['result'] ?? 1) === 0) {
Log::info('SHJ-9 SHJ-8処理結果判定: 0正常');
return;
}
Log::warning('SHJ-9 SHJ-8処理結果判定: 0以外異常', [
'shj8_result' => $shjEightResult
]);
}
}

View File

@ -201,63 +201,40 @@ class ShjSixService
}
// 【処理5】バッチ処理ログを作成する - SHJ-8呼び出し
// 仕様書準拠:キュー登録件数も含める
$statusComment = sprintf(
'デバイス数: %d, アラート件数: %d, メール正常: %d件, メール異常: %d件, キュー登録正常: %d件, キュー登録異常: %d件',
count($devices),
$alertCount,
$totalMailSuccessCount,
$totalMailErrorCount,
$totalQueueSuccessCount,
$totalQueueErrorCount
);
// バッチコメントが累積されている場合は追加
if (!empty($accumulatedBatchComment)) {
$statusComment .= ' | ' . $accumulatedBatchComment;
}
// 仕様書準拠:ステータスコメント = バッチコメント + 各件数
$statusComment = $accumulatedBatchComment
. ':メール正常終了件数' . $totalMailSuccessCount
. '、メール異常終了件数' . $totalMailErrorCount
. '、キュー登録正常終了件数' . $totalQueueSuccessCount
. '、キュー登録異常終了件数' . $totalQueueErrorCount;
$status = 'success'; // 仕様書:常に"success"で記録
$message = empty($warnings) ?
'SHJ-6 サーバ死活監視処理正常完了' :
'SHJ-6 サーバ死活監視処理完了(警告あり)';
// 仕様書準拠処理2で取得したデバイスIDを連結
// 仕様書準拠JOB5 は常に実行するJOB4 の結果に関わらず)
// 注bat_job_log.device_id は int unsigned のため連結文字列は格納不可
// 先頭デバイスIDを使用。デバイスなしの場合は 0
// SHJ-8 はエラー返却するが、式様書上 result≠0 でも「処理を終了する」のため問題なし)
$deviceIds = $devices->pluck('device_id')->toArray();
$concatenatedDeviceIds = !empty($deviceIds) ? implode(',', $deviceIds) : '';
$deviceId = !empty($deviceIds) ? (int)$deviceIds[0] : 0;
// SHJ-8 バッチ処理ログ作成(仕様書準拠)
// device_id = 処理2で取得したデバイスID複数なら連結
// process_name = 処理4のプロセス名プリンタログから取得、なければ'SHJ-6'
// job_name = "SHJ-6サーバ死活監視" 固定
// status = 常に "success"
$shj8Result = $this->createShjBatchLog([
'device_id' => $concatenatedDeviceIds, // 処理2のデバイスID連結
'process_name' => $printerResult['process_name'] ?? 'SHJ-6', // 処理4のプロセス名
'job_name' => 'SHJ-6サーバ死活監視',
'status' => 'success', // 仕様書常にsuccess
'status_comment' => $statusComment,
'mail_success_count' => $totalMailSuccessCount,
'mail_error_count' => $totalMailErrorCount,
'queue_success_count' => $totalQueueSuccessCount, // 仕様書準拠
'queue_error_count' => $totalQueueErrorCount, // 仕様書準拠
'device_count' => count($devices),
'alert_count' => $alertCount
]);
// SHJ-8 バッチ処理ログ作成
$shj8Result = $this->createShjBatchLog(
$deviceId,
$printerResult['process_name'] ?? null, // 仕様書準拠JOB4のプロセス名のみ
'SHJ-6サーバ死活監視', // job_name 固定
'success', // 仕様書常にsuccess
$statusComment
);
// 仕様書準拠SHJ-8の戻り値確認処理結果 = 0以外なら異常
// 式様書result=0 でも result≠0 でも「処理を終了する」
if (!$shj8Result['success']) {
$shj8Error = $shj8Result['error'] ?? 'Unknown error';
Log::warning('SHJ-8 バッチ処理ログ作成で異常が発生しました', [
'error' => $shj8Error
Log::warning('SHJ-8 バッチ処理ログ作成で異常', [
'device_id' => $deviceId,
'error' => $shj8Result['error'] ?? ''
]);
// 仕様書準拠:異常時はバッチコメントへ反映
$accumulatedBatchComment .= ($accumulatedBatchComment ? ' | ' : '') .
sprintf('SHJ-8異常: %s', $shj8Error);
// statusCommentも更新
$statusComment .= sprintf(' | SHJ-8異常: %s', $shj8Error);
}
Log::info('SHJ-6 サーバ死活監視処理完了', [
@ -378,16 +355,14 @@ class ShjSixService
private function getServerDevices()
{
try {
// 仕様書準拠SQL-2デバイス管理マスタを取得
$devices = DB::table('device')
->select([
'device_id',
'park_id',
'device_type',
'device_subject',
'device_identifier',
'device_work',
'device_workstart',
'device_remarks'
])
->where('device_type', self::DEVICE_TYPE_SERVER)
->where('device_work', self::DEVICE_WORK_ACTIVE)
@ -433,7 +408,7 @@ class ShjSixService
->orderBy('created_at', 'desc')
->first();
// ログが取得できない、または異常状態の場合
// 仕様書準拠JOB3-STEP1 取得レコードが0件の場合のみアラート
if (!$latestLog) {
// ログが存在しない = 異常
$alertMessage = sprintf(
@ -464,40 +439,7 @@ class ShjSixService
];
}
// status != 1正常の場合は異常
if ($latestLog->status != HardwareCheckLog::STATUS_NORMAL) {
$statusName = HardwareCheckLog::getStatusName($latestLog->status);
$alertMessage = sprintf(
'ハードウェア監視異常: デバイスID=%d, デバイス名=%s, 状態=%s, コメント=%s',
$device->device_id,
$device->device_subject ?? 'N/A',
$statusName,
$latestLog->status_comment ?? 'N/A'
);
$commonAResult = $this->executeCommonProcessA(
$batchLogId,
$alertMessage,
self::QUE_CLASS_SERVER_ERROR,
$device->park_id,
null,
null,
$dbRegisterFlag,
'ハードウェア監視:異常状態検出'
);
return [
'has_alert' => true,
'reason' => '異常状態',
'mail_success_count' => $commonAResult['mail_success_count'] ?? 0,
'mail_error_count' => $commonAResult['mail_error_count'] ?? 0,
'queue_success_count' => $commonAResult['queue_success_count'] ?? 0, // 仕様書準拠
'queue_error_count' => $commonAResult['queue_error_count'] ?? 0, // 仕様書準拠
'updated_batch_comment' => $commonAResult['updated_batch_comment'] ?? ''
];
}
// 正常
// 仕様書準拠レコードが存在する場合はステータス値に関わらずJOB4へアラートなし
return [
'has_alert' => false,
'reason' => '正常',
@ -562,19 +504,28 @@ class ShjSixService
// エラーログごとに共通A処理を実行
foreach ($errorLogs as $log) {
// 仕様書準拠statusの値に応じてキュー種別を判定
// - status 200番台 → 102プリンタエラー
// - status 300番台 → 103スキャナエラー
// - status 400番台 → 104プリンタ用紙残少警報
// - 200番台 → 102プリンタエラー
// - 300番台 → 103スキャナエラー
// - 400番台 → 104プリンタ用紙残少警報
$statusValue = (int)$log->status;
if ($statusValue >= 400 && $statusValue < 500) {
$queClass = self::QUE_CLASS_PAPER_WARNING; // 104 正しい定数名
$queClass = self::QUE_CLASS_PAPER_WARNING; // 104
$errorType = 'プリンタ用紙残少警報';
} elseif ($statusValue >= 300 && $statusValue < 400) {
$queClass = self::QUE_CLASS_SCANNER_ERROR; // 103
$errorType = 'スキャナエラー';
} else {
$queClass = self::QUE_CLASS_PRINTER_ERROR; // 102(デフォルト)
} elseif ($statusValue >= 200 && $statusValue < 300) {
$queClass = self::QUE_CLASS_PRINTER_ERROR; // 102
$errorType = 'プリンタエラー';
} else {
// 仕様書未定義のステータス範囲 → 102プリンタエラーをデフォルト使用
// 式様書「対象レコード数分」に準拠し、スキップしない
$queClass = self::QUE_CLASS_PRINTER_ERROR; // 102
$errorType = 'プリンタエラー';
Log::warning('JOB4: 仕様書未定義のステータス範囲デフォルト102適用', [
'status' => $statusValue,
'process_name' => $log->process_name ?? null
]);
}
$alertMessage = sprintf(
@ -618,8 +569,7 @@ class ShjSixService
$processNames[] = $log->process_name;
}
}
// 重複を除去して連結
$processNames = array_unique($processNames);
// 仕様書準拠:複数の場合は後ろ連結(重複除去しない)
$concatenatedProcessNames = !empty($processNames) ? implode(',', $processNames) : null;
return [
@ -676,12 +626,10 @@ class ShjSixService
// メール送信結果統計
$mailSuccessCount = 0;
$mailErrorCount = 0;
$mailErrorDetails = [];
// キュー登録結果統計(仕様書準拠)
$queueSuccessCount = 0;
$queueErrorCount = 0;
$registeredQueId = null; // 登録したキューID後で更新するため
try {
Log::info('共通A処理開始', [
@ -696,12 +644,8 @@ class ShjSixService
if ($dbRegisterFlag === 1) {
// DB登録可能な場合
// 仕様書準拠:バッチコメント = 処理1.定期契約ID + 元のバッチコメント
$updatedBatchComment = sprintf(
'定期契約ID: %s / %s',
$contractId ?? 'なし',
$batchComment ?? ''
);
// 仕様書準拠:キューコメント = 内部変数.バッチコメント(そのまま使用)
$updatedBatchComment = $batchComment ?? '';
// 【共通処理1】オペレータキューを登録する仕様書順序準拠
// 注SHJ-7異常情報はメール送信後に追記
@ -718,13 +662,11 @@ class ShjSixService
// 仕様書準拠:キュー登録正常終了件数/異常終了件数を更新
if ($queueResult['success']) {
$queueSuccessCount++;
$registeredQueId = $queueResult['que_id']; // キューIDを保存
} else {
$queueErrorCount++;
$updatedBatchComment .= sprintf(
' | キュー登録異常: %s',
$queueResult['error'] ?? 'Unknown error'
);
// 仕様書準拠line 209定期契約ID + 異常情報をバッチコメントに追記
$errorPrefix = ($contractId !== null) ? $contractId : '';
$updatedBatchComment .= $errorPrefix . ($queueResult['error'] ?? '');
}
// 【共通処理2】メール送信対象オペレータを取得する
@ -740,11 +682,9 @@ class ShjSixService
$mailSuccessCount++;
} else {
$mailErrorCount++;
$mailErrorDetails[] = sprintf(
'オペレータ[%s]へのメール送信失敗: %s',
$operator['email'],
$result['error'] ?? 'Unknown error'
);
// 仕様書準拠line 209定期契約ID + SHJ-7.異常情報をバッチコメントに追記
$errorPrefix = ($contractId !== null) ? $contractId : '';
$updatedBatchComment .= $errorPrefix . ($result['error'] ?? '');
}
}
}
@ -762,45 +702,13 @@ class ShjSixService
$mailSuccessCount++;
} else {
$mailErrorCount++;
$mailErrorDetails[] = sprintf(
'駐輪場管理者[%s]へのメール送信失敗: %s',
$manager['email'],
$result['error'] ?? 'Unknown error'
);
// 仕様書準拠line 209定期契約ID + SHJ-7.異常情報をバッチコメントに追記
$errorPrefix = ($contractId !== null) ? $contractId : '';
$updatedBatchComment .= $errorPrefix . ($result['error'] ?? '');
}
}
}
// 仕様書準拠メール異常時にSHJ-7異常情報を追記
if ($mailErrorCount > 0) {
$updatedBatchComment .= sprintf(
' | SHJ-7メール異常: %d件 (%s)',
$mailErrorCount,
implode('; ', $mailErrorDetails)
);
}
// 仕様書準拠キュー登録後、SHJ-7異常情報を含めてキューコメントを更新
// キューコメント = 内部変数.バッチコメント定期契約ID + SHJ-7異常情報
if ($registeredQueId && $mailErrorCount > 0) {
try {
OperatorQue::where('que_id', $registeredQueId)->update([ // 主キーはque_id
'que_comment' => $updatedBatchComment, // 仕様書:キューコメントに追記
'updated_at' => now()
]);
Log::info('オペレータキューコメント更新完了SHJ-7異常情報追記', [
'que_id' => $registeredQueId,
'updated_comment' => $updatedBatchComment
]);
} catch (\Exception $e) {
Log::error('オペレータキューコメント更新エラー', [
'que_id' => $registeredQueId,
'error' => $e->getMessage()
]);
}
}
} else {
// DB反映NGの場合は固定メールアドレスに緊急メール送信
// 仕様書準拠:テンプレート不使用、件名固定、本文なし
@ -809,24 +717,13 @@ class ShjSixService
$alertMessage
);
$updatedBatchComment = $batchComment ?? '';
if ($result['success']) {
$mailSuccessCount++;
} else {
$mailErrorCount++;
$mailErrorDetails[] = sprintf(
'固定アドレス[%s]への緊急メール送信失敗: %s',
self::FIXED_EMAIL_ADDRESS,
$result['error'] ?? 'Unknown error'
);
}
$updatedBatchComment = $batchComment ?? '';
if ($mailErrorCount > 0) {
$updatedBatchComment .= sprintf(
' | 緊急メール異常: %d件 (%s)',
$mailErrorCount,
implode('; ', $mailErrorDetails)
);
// 仕様書準拠:異常情報をバッチコメントに直接追記
$updatedBatchComment .= $result['error'] ?? '';
}
}
@ -844,7 +741,6 @@ class ShjSixService
'success' => true,
'mail_success_count' => $mailSuccessCount,
'mail_error_count' => $mailErrorCount,
'mail_error_details' => $mailErrorDetails,
'queue_success_count' => $queueSuccessCount, // 仕様書準拠
'queue_error_count' => $queueErrorCount, // 仕様書準拠
'updated_batch_comment' => $updatedBatchComment ?? $batchComment
@ -863,7 +759,6 @@ class ShjSixService
'success' => false,
'mail_success_count' => $mailSuccessCount,
'mail_error_count' => $mailErrorCount,
'mail_error_details' => $mailErrorDetails,
'queue_success_count' => $queueSuccessCount,
'queue_error_count' => $queueErrorCount,
'updated_batch_comment' => $updatedBatchComment,
@ -876,8 +771,9 @@ class ShjSixService
* オペレータキューを登録
*
* 仕様書準拠:
* - 定期契約IDをバッチコメントに含める
* - 登録の成否とque_idを返却後で更新するため
* - キューコメント = 内部変数.バッチコメント
* - キューステータスID = 1(キュー発生)
* - 更新オペレータID = 9999999機器ID固定値
*
* @param string $alertMessage アラートメッセージ
* @param int|null $batchLogId バッチログID
@ -885,7 +781,7 @@ class ShjSixService
* @param int|null $parkId 駐輪場ID
* @param int|null $userId 利用者ID
* @param int|null $contractId 定期契約ID
* @param string|null $batchComment バッチコメントSHJ-7異常情報は後で追記)
* @param string|null $batchComment バッチコメント
* @return array 登録結果 ['success' => bool, 'error' => string|null, 'que_id' => int|null]
*/
private function registerOperatorQueue(
@ -959,12 +855,20 @@ class ShjSixService
* - 104(プリンタ用紙残少) ope_sendalart_que13
*
* @param int $queClass 対象キュー種別ID
* @param int|null $parkId 駐輪場IDnullの場合は全オペレータ
* @param int|null $parkId 駐輪場ID
* @return array オペレータ一覧
*/
private function getMailTargetOperators(int $queClass, ?int $parkId): array
{
try {
// 仕様書準拠駐輪場IDがnullの場合はJOIN条件が成立しないため空返却
if ($parkId === null) {
Log::info('park_idがnullのため、メール送信対象オペレータなし', [
'que_class' => $queClass
]);
return [];
}
// キュー種別IDに対応する送信フラグカラム名を決定
$alertFlagColumn = $this->getOperatorAlertFlagColumn($queClass);
@ -973,20 +877,17 @@ class ShjSixService
return [];
}
// 管轄駐輪場マスタをJOINしてオペレータを取得
// 仕様書準拠SQL-6管轄駐輪場マスタをJOINしてオペレータを取得
$query = DB::table('ope as T1')
->select(['T1.ope_id', 'T1.ope_name', 'T1.ope_mail'])
->join('jurisdiction_parking as T2', 'T1.ope_id', '=', 'T2.ope_id')
// 仕様書SQL-6: ope_login_id と記載があるが、実DBカラム名は login_id
->select(['T1.ope_id', 'T1.login_id', 'T1.ope_name', 'T1.ope_mail'])
->where('T2.park_id', $parkId)
->where('T1.' . $alertFlagColumn, 1) // 該当アラート送信フラグが有効
->where('T1.ope_quit_flag', 0) // 退職していない
->whereNotNull('T1.ope_mail')
->where('T1.ope_mail', '!=', '');
// 駐輪場IDが指定されている場合は管轄駐輪場マスタでフィルタ
if ($parkId !== null) {
$query->join('jurisdiction_parking as T2', 'T1.ope_id', '=', 'T2.ope_id')
->where('T2.park_id', $parkId);
}
$operators = $query->get()
->map(function ($ope) {
return [
@ -1043,19 +944,21 @@ class ShjSixService
* - メールアドレスが設定されている
* - 所属駐輪場ID = 指定駐輪場ID
*
* @param int|null $parkId 駐輪場IDnullの場合は全管理者
* @param int|null $parkId 駐輪場ID
* @return array 駐輪場管理者一覧
*/
private function getParkManagers(?int $parkId): array
{
try {
$query = Manager::active()->hasEmail();
// 駐輪場IDが指定されている場合はフィルタ
if ($parkId !== null) {
$query->where('manager_parkid', $parkId);
// 仕様書準拠駐輪場IDがnullの場合は該当なしのため空返却
if ($parkId === null) {
Log::info('park_idがnullのため、駐輪場管理者なし');
return [];
}
$query = Manager::active()->hasEmail()
->where('manager_parkid', $parkId);
$managers = $query->select(['manager_id', 'manager_name', 'manager_mail', 'manager_parkid'])
->get()
->map(function ($manager) {
@ -1205,32 +1108,26 @@ class ShjSixService
*
* 仕様書に基づくSHJ-8共通処理呼び出し
*
* @param array $statistics 処理統計情報
* @param int $deviceId デバイスID
* @param string|null $processName プロセス名JOB4から取得
* @param string $jobName ジョブ名
* @param string $status ステータス
* @param string $statusComment ステータスコメント
* @return array 処理結果 ['success' => bool, 'error' => string|null]
*/
private function createShjBatchLog(array $statistics): array
{
private function createShjBatchLog(
int $deviceId,
?string $processName,
string $jobName,
string $status,
string $statusComment
): array {
try {
// 仕様書準拠のSHJ-8パラメータ設定
// device_id: 処理2で取得したデバイスID複数ある場合は最初のIDを使用
// process_name: 処理4のプロセス名
$deviceIdString = $statistics['device_id']; // 連結されたデバイスID文字列 "1,2,3"
// 複数デバイスIDがある場合は最初のIDを使用SHJ-8はint型を期待
$deviceIdArray = !empty($deviceIdString) ? explode(',', $deviceIdString) : [];
$deviceId = !empty($deviceIdArray) ? (int)$deviceIdArray[0] : 1;
$processName = $statistics['process_name'] ?? 'SHJ-6';
$jobName = $statistics['job_name']; // "SHJ-6サーバ死活監視" 固定
$status = $statistics['status']; // 常に "success"
$statusComment = $statistics['status_comment'] ?? '';
$createdDate = now()->format('Y/m/d');
$updatedDate = now()->format('Y/m/d');
Log::info('SHJ-8 バッチ処理ログ作成', [
'device_id' => $deviceId,
'device_id_original' => $deviceIdString,
'process_name' => $processName,
'job_name' => $jobName,
'status' => $status,
@ -1238,7 +1135,7 @@ class ShjSixService
]);
// SHJ-8サービスを呼び出し
$this->shjEightService->execute(
$shj8Result = $this->shjEightService->execute(
$deviceId,
$processName,
$jobName,
@ -1248,20 +1145,33 @@ class ShjSixService
$updatedDate
);
Log::info('SHJ-8 バッチ処理ログ作成完了', [
// 仕様書準拠SHJ-8の処理結果を判定result=0が正常
if (($shj8Result['result'] ?? 1) === 0) {
Log::info('SHJ-8 バッチ処理ログ作成完了(正常)', [
'device_id' => $deviceId,
'process_name' => $processName
]);
return [
'success' => true,
'error' => null
];
} else {
$errorMessage = $shj8Result['error_message'] ?? 'SHJ-8 処理結果異常';
Log::warning('SHJ-8 バッチ処理ログ作成異常', [
'device_id' => $deviceId,
'result' => $shj8Result['result'] ?? null,
'error_message' => $errorMessage
]);
return [
'success' => false,
'error' => $errorMessage
];
}
} catch (\Exception $e) {
Log::error('SHJ-8 バッチ処理ログ作成エラー', [
'error' => $e->getMessage(),
'statistics' => $statistics
'device_id' => $deviceId
]);
// 仕様書準拠SHJ-8でエラーが発生してもメイン処理は継続

View File

@ -0,0 +1,470 @@
<?php
namespace App\Services;
use App\Models\Device;
use App\Services\ShjEightService;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
/**
* SHJ-2 データバックアップサービス
*
* 概要: データベースの夜間自動フルバックアップ。5世代保持。
*
* 処理フロー:
* <JOB1> 現時点の世代シフトを行う
* <JOB2> フルバックアップを行う(ソース + DB
* <JOB3> バッチ処理ログを作成するSHJ-8呼び出し)
*/
class ShjTwoService
{
/**
* ShjEightService インスタンス
*
* @var ShjEightService
*/
protected $shjEightService;
/**
* コンストラクタ
*
* @param ShjEightService $shjEightService
*/
public function __construct(ShjEightService $shjEightService)
{
$this->shjEightService = $shjEightService;
}
/**
* SHJ-2 メイン処理実行
*
* @return array 処理結果
*/
public function execute(): array
{
$status = 'error';
$statusComment = '';
try {
Log::info('SHJ-2 データバックアップ処理開始');
$backupRoot = config('shj2.backup_root');
$dbName = config('database.connections.mysql.database');
$generations = config('shj2.generations', 5);
// <JOB1> 世代シフト
$this->shiftGenerations($backupRoot, $dbName, $generations);
// <JOB2> フルバックアップ
$backupResult = $this->executeFullBackup($backupRoot, $dbName);
// [JOB2-STEP1] 結果設定
$status = $backupResult['status'];
$statusComment = $backupResult['status_comment'];
Log::info('SHJ-2 データバックアップ処理完了', [
'status' => $status,
'status_comment' => $statusComment
]);
} catch (\Exception $e) {
$status = 'error';
$statusComment = $e->getMessage();
Log::error('SHJ-2 データバックアップ処理エラー', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
// <JOB3> バッチ処理ログ作成(成功・失敗に関わらず実行)
$this->createBatchLog($status, $statusComment);
return [
'success' => $status === 'success',
'status' => $status,
'status_comment' => $statusComment
];
}
/**
* <JOB1> 世代シフト処理
*
* ・5世代前 削除
* ・4世代前 5世代前へ移動
* ・3世代前 4世代前へ移動
* ・2世代前 3世代前へ移動
* ・1世代前<DB名>直下)→ 2世代前へ移動
*
* @param string $backupRoot バックアップルートディレクトリ
* @param string $dbName データベース名
* @param int $generations 世代数
* @return void
*/
private function shiftGenerations(string $backupRoot, string $dbName, int $generations): void
{
Log::info('JOB1 世代シフト開始', [
'backup_root' => $backupRoot,
'db_name' => $dbName,
'generations' => $generations
]);
// 最古世代gen5のディレクトリパスを取得して削除
$oldestDir = $backupRoot . '/' . $dbName . '_' . $generations;
if (is_dir($oldestDir)) {
$this->deleteDirectory($oldestDir);
Log::info("世代シフト: {$generations}世代前を削除", ['path' => $oldestDir]);
}
// gen4→gen5, gen3→gen4, gen2→gen3 のシフト
for ($i = $generations - 1; $i >= 2; $i--) {
$fromDir = $backupRoot . '/' . $dbName . '_' . $i;
$toDir = $backupRoot . '/' . $dbName . '_' . ($i + 1);
if (is_dir($fromDir)) {
rename($fromDir, $toDir);
Log::info("世代シフト: {$i}世代前 → " . ($i + 1) . "世代前", [
'from' => $fromDir,
'to' => $toDir
]);
}
}
// gen1<DB名>直下)→ gen2 へ移動
$gen1Dir = $backupRoot . '/' . $dbName;
$gen2Dir = $backupRoot . '/' . $dbName . '_2';
if (is_dir($gen1Dir)) {
rename($gen1Dir, $gen2Dir);
Log::info('世代シフト: 1世代前 → 2世代前', [
'from' => $gen1Dir,
'to' => $gen2Dir
]);
}
Log::info('JOB1 世代シフト完了');
}
/**
* <JOB2> フルバックアップ実行
*
* 保存内容: {vendor以外のソース + DB}_YYYYMMDD.tar.gz
*
* @param string $backupRoot バックアップルートディレクトリ
* @param string $dbName データベース名
* @return array ['status' => string, 'status_comment' => string]
*/
private function executeFullBackup(string $backupRoot, string $dbName): array
{
Log::info('JOB2 フルバックアップ開始');
$today = Carbon::now()->format('Ymd');
$backupDir = $backupRoot . '/' . $dbName;
$tarFileName = $dbName . '_' . $today . '.tar';
$gzFileName = $tarFileName . '.gz';
$tarFilePath = $backupDir . '/' . $tarFileName;
$gzFilePath = $backupDir . '/' . $gzFileName;
// バックアップディレクトリ作成
if (!is_dir($backupDir)) {
mkdir($backupDir, 0755, true);
}
// 一時ディレクトリtar作成用のステージング領域
$tempDir = $backupDir . '/temp_' . $today;
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
try {
// 1. mysqldump実行
$sqlFileName = $dbName . '.sql';
$sqlFilePath = $tempDir . '/' . $sqlFileName;
$this->executeMysqlDump($sqlFilePath);
// 2. ソースファイルをコピー
$this->copySourceFiles($tempDir);
// 3. tar.gz作成
$this->createTarGz($tempDir, $gzFilePath);
// 4. ファイルサイズ検証
if (!file_exists($gzFilePath) || filesize($gzFilePath) === 0) {
throw new \RuntimeException('バックアップファイルの作成に失敗しましたファイルサイズ0');
}
$fileSizeMb = round(filesize($gzFilePath) / 1024 / 1024, 2);
Log::info('JOB2 フルバックアップ完了', [
'file' => $gzFilePath,
'size_mb' => $fileSizeMb
]);
// [JOB2-STEP1] OK
return [
'status' => 'success',
'status_comment' => $backupDir . ' ' . $gzFileName
];
} catch (\Exception $e) {
Log::error('JOB2 フルバックアップエラー', [
'error' => $e->getMessage()
]);
// [JOB2-STEP1] NG
return [
'status' => 'error',
'status_comment' => $e->getMessage()
];
} finally {
// 一時ディレクトリを削除
if (is_dir($tempDir)) {
$this->deleteDirectory($tempDir);
}
}
}
/**
* mysqldump実行
*
* @param string $outputPath 出力ファイルパス
* @return void
* @throws \RuntimeException dump失敗時
*/
private function executeMysqlDump(string $outputPath): void
{
$mysqldumpPath = config('shj2.mysqldump_path');
$dbHost = config('database.connections.mysql.host');
$dbPort = config('database.connections.mysql.port', '3306');
$dbName = config('database.connections.mysql.database');
$dbUser = config('database.connections.mysql.username');
$dbPass = config('database.connections.mysql.password');
// mysqldumpコマンド構築Windows環境対応
// stderrは一時ファイルに出力し、エラー時に読み取る
$stderrFile = sys_get_temp_dir() . '/shj2_mysqldump_stderr.txt';
$command = sprintf(
'"%s" --host="%s" --port="%s" --user="%s" --password="%s" --single-transaction --routines --triggers "%s" > "%s" 2>"%s"',
$mysqldumpPath,
$dbHost,
$dbPort,
$dbUser,
$dbPass,
$dbName,
$outputPath,
$stderrFile
);
Log::info('mysqldump実行開始', ['database' => $dbName]);
exec($command, $output, $returnCode);
if ($returnCode !== 0) {
$errorOutput = file_exists($stderrFile) ? trim(file_get_contents($stderrFile)) : '不明なエラー';
@unlink($stderrFile);
throw new \RuntimeException('mysqldumpエラー: ' . $errorOutput);
}
@unlink($stderrFile);
if (!file_exists($outputPath) || filesize($outputPath) === 0) {
throw new \RuntimeException('mysqldumpの出力ファイルが空です');
}
Log::info('mysqldump実行完了', [
'output_path' => $outputPath,
'size_bytes' => filesize($outputPath)
]);
}
/**
* ソースファイルを一時ディレクトリにコピー
*
* vendor等の除外ディレクトリを除いてコピーする
*
* @param string $tempDir コピー先一時ディレクトリ
* @return void
*/
private function copySourceFiles(string $tempDir): void
{
$sourcePaths = config('shj2.source_paths', []);
$excludeDirs = config('shj2.exclude_dirs', []);
foreach ($sourcePaths as $sourcePath) {
// 空パスはスキップ
if (empty($sourcePath)) {
continue;
}
if (!is_dir($sourcePath)) {
Log::warning('ソースディレクトリが存在しません', ['path' => $sourcePath]);
continue;
}
// ディレクトリ名を取得してコピー先を決定
$dirName = basename($sourcePath);
$destDir = $tempDir . '/' . $dirName;
Log::info('ソースコピー開始', [
'source' => $sourcePath,
'dest' => $destDir
]);
$this->copyDirectoryRecursive($sourcePath, $destDir, $excludeDirs);
Log::info('ソースコピー完了', ['dir_name' => $dirName]);
}
}
/**
* ディレクトリを再帰的にコピー(除外ディレクトリ対応)
*
* @param string $source コピー元パス
* @param string $dest コピー先パス
* @param array $excludeDirs 除外ディレクトリ名リスト
* @return void
*/
private function copyDirectoryRecursive(string $source, string $dest, array $excludeDirs): void
{
if (!is_dir($dest)) {
mkdir($dest, 0755, true);
}
$iterator = new \DirectoryIterator($source);
foreach ($iterator as $item) {
if ($item->isDot()) {
continue;
}
$itemName = $item->getFilename();
// 除外ディレクトリチェック
if ($item->isDir() && in_array($itemName, $excludeDirs)) {
continue;
}
$sourcePath = $item->getPathname();
$destPath = $dest . '/' . $itemName;
if ($item->isDir()) {
$this->copyDirectoryRecursive($sourcePath, $destPath, $excludeDirs);
} else {
copy($sourcePath, $destPath);
}
}
}
/**
* tar.gzアーカイブを作成
*
* @param string $sourceDir アーカイブ対象ディレクトリ
* @param string $gzFilePath 出力tar.gzファイルパス
* @return void
* @throws \RuntimeException 作成失敗時
*/
private function createTarGz(string $sourceDir, string $gzFilePath): void
{
Log::info('tar.gz作成開始', ['source' => $sourceDir, 'output' => $gzFilePath]);
// tarファイルパス.gzを除いたパス
$tarFilePath = preg_replace('/\.gz$/', '', $gzFilePath);
try {
$phar = new \PharData($tarFilePath);
$phar->buildFromDirectory($sourceDir);
$phar->compress(\Phar::GZ);
// PharData::compress() は新しい .tar.gz ファイルを作成する
// 元の .tar ファイルを削除
if (file_exists($tarFilePath)) {
unlink($tarFilePath);
}
Log::info('tar.gz作成完了', ['output' => $gzFilePath]);
} catch (\Exception $e) {
// 中間ファイルのクリーンアップ
if (file_exists($tarFilePath)) {
unlink($tarFilePath);
}
if (file_exists($gzFilePath)) {
unlink($gzFilePath);
}
throw new \RuntimeException('tar.gz作成エラー: ' . $e->getMessage());
}
}
/**
* <JOB3> バッチ処理ログ作成
*
* SHJ-8 バッチ処理ログ作成を呼び出す
*
* @param string $status ステータスsuccess/error
* @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');
// ステータスコメントは255文字以内に切り詰め
if (mb_strlen($statusComment) > 255) {
$statusComment = mb_substr($statusComment, 0, 252) . '...';
}
$this->shjEightService->execute(
$deviceId,
null, // プロセス名
'SHJ-2データバックアップ', // ジョブ名
$status, // ステータス
$statusComment, // ステータスコメント
$today, // 登録日時
$today // 更新日時
);
Log::info('JOB3 バッチ処理ログ作成完了', [
'device_id' => $deviceId,
'status' => $status
]);
} catch (\Exception $e) {
Log::error('JOB3 バッチ処理ログ作成エラー', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
}
/**
* ディレクトリを再帰的に削除
*
* @param string $dirPath 削除対象ディレクトリパス
* @return void
*/
private function deleteDirectory(string $dirPath): void
{
if (!is_dir($dirPath)) {
return;
}
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dirPath, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
if ($item->isDir()) {
rmdir($item->getPathname());
} else {
unlink($item->getPathname());
}
}
rmdir($dirPath);
}
}

61
config/shj2.php Normal file
View File

@ -0,0 +1,61 @@
<?php
/**
* SHJ-2 データバックアップ設定
*
* バックアップ対象パス、保存先、世代数などの設定
* 環境に応じて .env ファイルで上書き可能
*/
return [
/*
|--------------------------------------------------------------------------
| バックアップ保存先ルートディレクトリ
|--------------------------------------------------------------------------
| 世代ディレクトリの親ディレクトリ
| : C:/xampp8/backup/somanager/krgm/ が1世代前のバックアップ先
*/
'backup_root' => env('SHJ2_BACKUP_ROOT', 'C:/xampp8/backup/somanager'),
/*
|--------------------------------------------------------------------------
| バックアップ対象ソースディレクトリ
|--------------------------------------------------------------------------
| vendor等を除外してバックアップするプロジェクトパス
| 空文字列のパスは自動的にスキップされる
*/
'source_paths' => [
env('SHJ2_SOURCE_1', 'C:/xampp8/htdocs/somanager/api-batch.app'),
env('SHJ2_SOURCE_2', 'C:/xampp8/htdocs/somanager/www.app'),
env('SHJ2_SOURCE_3', ''), // 第3プロジェクトパス設定後に有効
],
/*
|--------------------------------------------------------------------------
| 除外ディレクトリ
|--------------------------------------------------------------------------
| バックアップ対象から除外するディレクトリ名
*/
'exclude_dirs' => [
'vendor',
'node_modules',
'.git',
],
/*
|--------------------------------------------------------------------------
| 世代数
|--------------------------------------------------------------------------
| フルバックアップを保持する世代数
*/
'generations' => 5,
/*
|--------------------------------------------------------------------------
| mysqldumpパス
|--------------------------------------------------------------------------
| mysqldumpコマンドの絶対パス
*/
'mysqldump_path' => env('SHJ2_MYSQLDUMP_PATH', 'C:/xampp8/mysql/bin/mysqldump'),
];

View File

@ -11,5 +11,14 @@ Artisan::command('inspire', function () {
// 支払期限切れチェック15分毎
Schedule::command('payment:expire')->everyFifteenMinutes();
// SHJ-2 データバックアップ(毎日 02:45
Schedule::command('shj:2')->dailyAt('02:45');
// SHJ-5 駐輪場空きチェック毎月20日 11:00
Schedule::command('shj:5')->monthlyOn(20, '11:00');
// SHJ-6 サーバ死活監視15分毎
Schedule::command('shj:6')->everyFifteenMinutes();
// SHJ-9 売上集計(日次)(毎日 02:00
Schedule::command('shj:9')->dailyAt('02:00');