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

1102 lines
42 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<?php
namespace App\Services;
use App\Models\User;
use App\Models\Usertype;
use App\Models\Park;
use App\Models\RegularContract;
use App\Models\OperatorQue;
use App\Models\Device;
use App\Services\GoogleVisionService;
use App\Services\GoogleMapsService;
use App\Services\ShjMailSendService;
use App\Services\ShjEightService;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
/**
* SHJ-1 本人確認自動処理 Service
*/
class ShjOneService
{
protected $googleVisionService;
protected $googleMapsService;
protected $mailSendService;
protected $shjEightService;
public function __construct(
GoogleVisionService $googleVisionService,
GoogleMapsService $googleMapsService,
ShjMailSendService $mailSendService,
ShjEightService $shjEightService
) {
$this->googleVisionService = $googleVisionService;
$this->googleMapsService = $googleMapsService;
$this->mailSendService = $mailSendService;
$this->shjEightService = $shjEightService;
}
/**
* SHJ-1メイン処理実行
*
* @param int $userId 利用者連番
* @param int $parkId 駐輪場ID
* @return array
*/
public function execute(int $userId, int $parkId): array
{
try {
Log::info('SHJ-1 【処理1】開始: 本人確認自動処理レコード群取得', [
'user_id' => $userId,
'park_id' => $parkId
]);
// 【処理1】本人確認自動処理レコード群取得 - 設計書通りの条件でフィルタリング
$user = $this->getUserRecord($userId);
if (!$user) {
Log::error('SHJ-1 【処理1】失敗: 利用者が見つかりません', [
'user_id' => $userId,
'park_id' => $parkId
]);
$result = [
'system_success' => false,
'message' => '利用者が見つかりません',
'stats' => ['error_count' => 1, 'processed_count' => 0]
];
$this->createBatchLog($userId, null, null, $result);
return $result;
}
Log::info('SHJ-1 【処理1】成功: 利用者データ取得', [
'user_seq' => $user->user_seq,
'user_name' => $user->user_name,
'user_idcard' => $user->user_idcard,
'photo_filename1' => $user->photo_filename1,
'photo_filename2' => $user->photo_filename2
]);
// 本人確認書類が免許証以外、または写真ファイルがない場合は対象外
if (!$this->isTargetUser($user)) {
Log::warning('SHJ-1 処理対象外判定', [
'user_seq' => $user->user_seq,
'user_idcard' => $user->user_idcard,
'photo_filename1' => $user->photo_filename1,
'user_idcard_chk_flag' => $user->user_idcard_chk_flag,
'reason' => '免許証以外または写真なしまたは既に処理済み'
]);
$result = [
'system_success' => false,
'message' => '処理対象外の利用者です(免許証以外または写真なし)',
'stats' => ['error_count' => 1, 'processed_count' => 0]
];
$this->createBatchLog($userId, $user->user_name, null, $result);
return $result;
}
$park = $this->getParkRecord($parkId);
if (!$park) {
Log::error('SHJ-1 駐輪場データ取得失敗', [
'park_id' => $parkId
]);
$result = [
'system_success' => false,
'message' => '駐輪場が見つかりません',
'stats' => ['error_count' => 1, 'processed_count' => 0]
];
$this->createBatchLog($userId, $user->user_name, null, $result);
return $result;
}
Log::info('SHJ-1 駐輪場データ取得成功', [
'park_id' => $park->park_id,
'park_name' => $park->park_name
]);
// トランザクション開始
DB::beginTransaction();
Log::info('SHJ-1 データベーストランザクション開始');
// 【判断1】対象外判定
Log::info('SHJ-1 【判断1】開始: 利用者分類チェック');
$categoryCheck = $this->checkUserCategory($user);
if (!$categoryCheck['is_target']) {
Log::warning('SHJ-1 【判断1】対象外: 手動処理対象', [
'user_seq' => $user->user_seq,
'category_name' => $categoryCheck['category_name'],
'reason' => '高齢者、障がい者、生活保護、中国、母子家庭等'
]);
$result = $this->processNonTargetUser($user, $categoryCheck['category_name']);
DB::commit(); // 対象外処理後はコミット
$this->createBatchLog($userId, $user->user_name, null, $result);
return $result;
}
Log::info('SHJ-1 【判断1】成功: 自動処理対象', [
'user_seq' => $user->user_seq,
'category_name' => $categoryCheck['category_name']
]);
// 【処理】本人確認自動処理800Mチェック処理
Log::info('SHJ-1 【処理2】開始: 本人確認自動処理');
$identityResult = $this->processIdentityVerification($user, $park);
DB::commit();
// バッチ処理ログ作成
$this->createBatchLog($userId, $user->user_name, $identityResult['similarity_rate'] ?? null, $identityResult);
return $identityResult;
} catch (Exception $e) {
DB::rollBack();
Log::error('SHJ-1処理エラー', [
'user_id' => $userId,
'park_id' => $parkId,
'error' => $e->getMessage()
]);
$result = [
'system_success' => false,
'message' => 'システムエラーが発生しました: ' . $e->getMessage(),
'stats' => ['error_count' => 1, 'processed_count' => 0]
];
// エラー時もバッチログ作成
$this->createBatchLog($userId, null, null, $result);
return $result;
}
}
/**
* 利用者レコード取得(設計書の条件に従ってフィルタリング)
*/
private function getUserRecord(int $userId): ?User
{
return User::where('user_seq', $userId)
->whereNotNull('photo_filename1') // 本人確認写真必須
->first();
}
/**
* 処理対象ユーザーかチェック(設計書の条件)
*/
private function isTargetUser(User $user): bool
{
Log::info('SHJ-1 isTargetUser チェック開始', [
'user_seq' => $user->user_seq,
'user_idcard' => $user->user_idcard,
'photo_filename1' => $user->photo_filename1,
'user_idcard_chk_flag' => $user->user_idcard_chk_flag,
'user_idcard_chk_flag_type' => gettype($user->user_idcard_chk_flag),
'auto_ok_config' => config('shj1.identity_check_status.auto_ok')
]);
// 本人確認書類が免許証であること
if ($user->user_idcard !== '免許証') {
Log::info('SHJ-1 isTargetUser: 免許証以外', ['user_idcard' => $user->user_idcard]);
return false;
}
// 写真ファイルが存在すること
if (empty($user->photo_filename1)) {
Log::info('SHJ-1 isTargetUser: 写真ファイルなし');
return false;
}
// 既に本人確認済みの場合はスキップ一度OKなら再チェック省略
// ※テスト用に一時的にコメントアウト - 重複処理を許可
/*
$autoOk = config('shj1.identity_check_status.auto_ok');
if ($user->user_idcard_chk_flag === $autoOk) { // 厳密比較に変更
Log::info('SHJ-1 isTargetUser: 既に処理済み', [
'current_flag' => $user->user_idcard_chk_flag,
'current_flag_type' => gettype($user->user_idcard_chk_flag),
'auto_ok' => $autoOk,
'auto_ok_type' => gettype($autoOk),
'loose_comparison' => $user->user_idcard_chk_flag == $autoOk ? 'equal' : 'not_equal',
'strict_comparison' => $user->user_idcard_chk_flag === $autoOk ? 'equal' : 'not_equal'
]);
return false;
}
*/
Log::info('SHJ-1 isTargetUser: 処理対象OK');
return true;
}
/**
* 駐輪場レコード取得
*/
private function getParkRecord(int $parkId): ?Park
{
return Park::where('park_id', $parkId)->first();
}
/**
* 利用者分類チェック
*/
private function checkUserCategory(User $user): array
{
$usertype = Usertype::where('user_categoryid', $user->user_categoryid)->first();
if (!$usertype) {
return ['is_target' => false, 'category_name' => 'unknown'];
}
$categoryName = $usertype->usertype_subject3;
$excludedCategories = config('shj1.user_categories.excluded_categories');
// 対象外カテゴリーかチェック
$isExcluded = in_array($categoryName, $excludedCategories);
return [
'is_target' => !$isExcluded,
'category_name' => $categoryName
];
}
/**
* 対象外ユーザーの処理
*/
private function processNonTargetUser(User $user, string $categoryName): array
{
// 本人確認済ステータスをNGで更新設計書通り
$autoNgValue = config('shj1.identity_check_status.auto_ng');
Log::info('SHJ-1 対象外ユーザー: ユーザー情報更新開始', [
'user_seq' => $user->user_seq,
'category_name' => $categoryName,
'before_user_idcard_chk_flag' => $user->user_idcard_chk_flag,
'will_set_to' => $autoNgValue
]);
$updateResult = $user->update([
'user_idcard_chk_flag' => $autoNgValue,
'user_chk_day' => now(),
'user_chk_opeid' => 'SHJ-1'
]);
Log::info('SHJ-1 対象外ユーザー: ユーザー情報更新完了', [
'user_seq' => $user->user_seq,
'update_result' => $updateResult,
'after_user_idcard_chk_flag' => $user->fresh()->user_idcard_chk_flag
]);
// オペレータキューも作成(設計書の要求)
$this->createOperatorQueue($user, null, [], true);
return [
'system_success' => true,
'identity_result' => 'NG',
'message' => '対象外ユーザーのためNG処理完了',
'details' => [
'user_id' => $user->user_seq,
'category' => $categoryName,
'action' => 'set_ng_status_with_queue'
],
'stats' => ['processed_count' => 1, 'ng_count' => 1, 'non_target_count' => 1]
];
}
/**
* 本人確認自動処理800Mチェック処理SHJ-1文書仕様
*
* 文書仕様:
* 1. まずOCR照合処理を実行
* 2. OCR照合成功の場合のみ距離チェック処理を実行
* 3. 両方成功で最終成功
*/
private function processIdentityVerification(User $user, Park $park): array
{
try {
// 1. OCR処理による本人確認
Log::info('SHJ-1 【処理2】開始: 本人確認自動処理');
$ocrResult = $this->performComprehensiveOcrVerification($user);
// OCR処理失敗の場合、距離チェックを実行せずに失敗処理
if (!$ocrResult['success']) {
Log::info('SHJ-1 OCR照合失敗、距離チェックスキップ', [
'user_seq' => $user->user_seq,
'reason' => $ocrResult['reason']
]);
// 距離チェック結果をダミーで設定(実行しない)
$distanceResult = [
'within_limit' => true, // OCR失敗なので距離は関係ない
'skipped' => true,
'reason' => 'ocr_failed'
];
return $this->processFailureCase($user, $park, $ocrResult, $distanceResult);
}
// 2. OCR成功 → 距離チェック処理を実行
Log::info('SHJ-1 OCR照合成功、距離チェック開始', [
'user_seq' => $user->user_seq,
'matched_address_type' => $ocrResult['matched_address_type']
]);
$distanceResult = $this->performDistanceCheck($user, $park);
// 3. 最終判定OCR成功 AND 距離内
if ($distanceResult['within_limit']) {
return $this->processSuccessCase($user, $park, $ocrResult, $distanceResult);
} else {
return $this->processFailureCase($user, $park, $ocrResult, $distanceResult);
}
} catch (Exception $e) {
Log::error('SHJ-1 【処理2】エラー', [
'user_seq' => $user->user_seq,
'park_id' => $park->park_id,
'error' => $e->getMessage()
]);
// システムエラーとして失敗処理
return $this->processFailureCase($user, $park,
['success' => false, 'reason' => 'system_error'],
['within_limit' => true, 'skipped' => true, 'reason' => 'system_error']
);
}
}
/**
* 包括的OCR処理SHJ-1文書仕様に従った実装
*
* 文書仕様:
* - 表面画像で一度OKが出た場合、裏面のチェックはスキップする
* - [OCR值]と[利用者住所]を照合する
*/
private function performComprehensiveOcrVerification(User $user): array
{
try {
Log::info('SHJ-1 OCR処理開始', [
'user_seq' => $user->user_seq,
'photo_filename1' => $user->photo_filename1,
'photo_filename2' => $user->photo_filename2
]);
// 表面画像の処理
if ($user->photo_filename1) {
$photoPath = $this->buildPhotoPath($user->photo_filename1);
Log::info('SHJ-1 表面画像処理開始', [
'user_seq' => $user->user_seq,
'photo_filename1' => $user->photo_filename1,
'photo_path' => $photoPath
]);
$frontResult = $this->processImageForOcr($user, $photoPath, 'front');
Log::info('SHJ-1 表面画像処理完了', [
'user_seq' => $user->user_seq,
'front_result' => $frontResult
]);
// SHJ-1文書通り一度OKが出た場合、裏面チェックはスキップ
if ($frontResult['success']) {
Log::info('SHJ-1 表面画像でOK、裏面チェックスキップ', [
'user_seq' => $user->user_seq
]);
return [
'success' => true,
'processed_side' => 'front',
'extracted_name' => $frontResult['extracted_name'],
'extracted_address' => $frontResult['extracted_address'],
'ocr_value' => $frontResult['ocr_value'],
'matched_address_type' => $frontResult['matched_address_type'],
'matched_address' => $frontResult['matched_address'],
'similarity' => $frontResult['similarity'],
'reason' => $frontResult['reason']
];
}
}
// 表面画像がNGまたは存在しない場合、裏面画像を処理
if ($user->photo_filename2) {
$photoPath = $this->buildPhotoPath($user->photo_filename2);
Log::info('SHJ-1 裏面画像処理開始', [
'user_seq' => $user->user_seq,
'photo_filename2' => $user->photo_filename2,
'photo_path' => $photoPath
]);
$backResult = $this->processImageForOcr($user, $photoPath, 'back');
Log::info('SHJ-1 裏面画像処理完了', [
'user_seq' => $user->user_seq,
'back_result' => $backResult
]);
if ($backResult['success']) {
return [
'success' => true,
'processed_side' => 'back',
'extracted_name' => $backResult['extracted_name'],
'extracted_address' => $backResult['extracted_address'],
'ocr_value' => $backResult['ocr_value'],
'matched_address_type' => $backResult['matched_address_type'],
'matched_address' => $backResult['matched_address'],
'similarity' => $backResult['similarity'],
'reason' => $backResult['reason']
];
}
// 裏面もNG
return [
'success' => false,
'processed_side' => 'both',
'reason' => 'both_sides_failed',
'front_result' => $frontResult ?? null,
'back_result' => $backResult
];
}
// 表面のみでNG
return [
'success' => false,
'processed_side' => 'front_only',
'reason' => 'front_failed_no_back',
'front_result' => $frontResult ?? null
];
} catch (Exception $e) {
Log::error('SHJ-1 OCR処理エラー', [
'user_seq' => $user->user_seq,
'error' => $e->getMessage()
]);
return [
'success' => false,
'reason' => 'system_error',
'error' => $e->getMessage()
];
}
}
/**
* 照片文件路径构建Laravel Storage方式
*/
private function buildPhotoPath(string $filename): string
{
// Storage::disk('public')用,需要 "photo/filename" 格式
return 'photo/' . $filename;
}
/**
* 画像OCR処理SHJ-1文書仕様に従った実装
*/
private function processImageForOcr(User $user, string $filename, string $side): array
{
// Google Vision APIでOCR実行
$ocrText = $this->googleVisionService->extractTextFromImage($filename);
// 完全なOCR認識結果をログ出力
Log::info('SHJ-1 完全OCR認識結果', [
'user_seq' => $user->user_seq,
'filename' => $filename,
'side' => $side,
'ocr_text_length' => strlen($ocrText),
'ocr_full_text' => base64_encode($ocrText)
]);
if (empty($ocrText)) {
Log::error('SHJ-1 OCR結果が空', ['user_seq' => $user->user_seq, 'filename' => $filename]);
return [
'success' => false,
'reason' => 'ocr_empty',
'extracted_name' => '',
'extracted_address' => '',
'ocr_value' => ''
];
}
// SHJ-1文書通りOCR文本からの氏名・住所抽出
$extractResult = $this->extractNameAndAddressFromOcr($ocrText);
if (!$extractResult['success']) {
Log::error('SHJ-1 OCR抽出失敗', [
'user_seq' => $user->user_seq,
'filename' => $filename,
'reason' => $extractResult['reason']
]);
return $extractResult;
}
Log::info('SHJ-1 OCR抽出成功', [
'user_seq' => $user->user_seq,
'filename' => $filename,
'side' => $side,
'extracted_name' => $extractResult['extracted_name'],
'extracted_address' => $extractResult['extracted_address'],
'ocr_value' => $extractResult['ocr_value']
]);
// SHJ-1文書通り[OCR値]と[利用者住所]の照合
$matchResult = $this->performOcrMatching($user, $extractResult['ocr_value']);
return array_merge($extractResult, $matchResult);
}
/**
* SHJ-1文書通りOCRテキストから氏名と住所を抽出
*
* 文書仕様:
* 1. "氏名"があるかチェック
* 2. "氏名"の次から年号和暦(昭和|平成)まで → [氏名]
* 3. "住所"があるかチェック
* 4. "住所"の次から和暦(昭和|平成|令和)または"交付"まで → [住所]
* 5. [OCR値] = [氏名] + [住所]
*/
private function extractNameAndAddressFromOcr(string $ocrText): array
{
// ノイズ除去:改行とスペースを統一
$cleanedOcr = preg_replace('/\s+/', '', $ocrText);
// 1. "氏名"キーワードの存在チェック
if (strpos($cleanedOcr, '氏名') === false) {
return [
'success' => false,
'reason' => 'name_keyword_not_found',
'extracted_name' => '',
'extracted_address' => '',
'ocr_value' => ''
];
}
// 2. 氏名抽出:「氏名」の次から「住所」まで
$namePattern = '/氏名([^住]*?)住所/u';
if (!preg_match($namePattern, $cleanedOcr, $nameMatches)) {
return [
'success' => false,
'reason' => 'name_extraction_failed',
'extracted_name' => '',
'extracted_address' => '',
'ocr_value' => ''
];
}
$extractedName = $this->removeSymbols($nameMatches[1]);
// 3. "住所"キーワードの存在チェック
if (strpos($cleanedOcr, '住所') === false) {
return [
'success' => false,
'reason' => 'address_keyword_not_found',
'extracted_name' => $extractedName,
'extracted_address' => '',
'ocr_value' => ''
];
}
// 4. 住所抽出:「住所」の次から改行文字または「交付」まで
$addressPattern = '/住所([^\r\n|]*?)(?:\r|\n|\|交付|交付)/u';
if (!preg_match($addressPattern, $cleanedOcr, $addressMatches)) {
return [
'success' => false,
'reason' => 'address_extraction_failed',
'extracted_name' => $extractedName,
'extracted_address' => '',
'ocr_value' => ''
];
}
$extractedAddress = $this->removeSymbols($addressMatches[1]);
// 5. SHJ-1文書通り[OCR値] = [氏名] + [住所]
$ocrValue = $extractedName . $extractedAddress;
return [
'success' => true,
'extracted_name' => $extractedName,
'extracted_address' => $extractedAddress,
'ocr_value' => $ocrValue
];
}
/**
* 記号を除去SHJ-1文書仕様
*/
private function removeSymbols(string $text): string
{
// 基本的な記号・区切り文字を除去
$cleaned = preg_replace('/[|\-\|\/\\\\()\[\]【】「」『』<>《》≪≫]/u', '', $text);
$cleaned = preg_replace('/\s+/u', '', $cleaned); // スペース除去
return trim($cleaned);
}
/**
* SHJ-1文書通り[OCR值]と[利用者住所]の照合処理
*
* 文書仕様:
* 1. [利用者居住所]と[OCR値]を照合 → 閾値以下なら次へ
* 2. [利用者関連住所]と[OCR値]を照合 → 閾値以下ならOCR照合失敗
*/
private function performOcrMatching(User $user, string $ocrValue): array
{
$threshold = config('shj1.ocr.similarity_threshold');
// [利用者居住所]を構築
$residentAddress = $user->user_regident_pre . $user->user_regident_city . $user->user_regident_add;
// 1. [利用者居住所]と[OCR値]を照合
similar_text($residentAddress, $ocrValue, $residentSimilarity);
Log::info('SHJ-1 居住住所照合', [
'user_seq' => $user->user_seq,
'resident_address' => $residentAddress,
'ocr_value' => $ocrValue,
'similarity' => $residentSimilarity,
'threshold' => $threshold
]);
if ($residentSimilarity >= $threshold) {
// 居住住所で照合成功
Log::info('SHJ-1 OCR照合成功居住住所', [
'user_seq' => $user->user_seq,
'matched_address_type' => '居住住所',
'similarity' => $residentSimilarity
]);
return [
'success' => true,
'matched_address_type' => '居住住所',
'matched_address' => $residentAddress,
'similarity' => $residentSimilarity,
'reason' => 'resident_address_matched'
];
}
// 2. [利用者関連住所]と[OCR値]を照合
$relatedAddress = $user->user_relate_pre . $user->user_relate_city . $user->user_relate_add;
if (!empty(trim($relatedAddress))) {
similar_text($relatedAddress, $ocrValue, $relatedSimilarity);
Log::info('SHJ-1 関連住所照合', [
'user_seq' => $user->user_seq,
'related_address' => $relatedAddress,
'ocr_value' => $ocrValue,
'similarity' => $relatedSimilarity,
'threshold' => $threshold
]);
if ($relatedSimilarity >= $threshold) {
// 関連住所で照合成功
Log::info('SHJ-1 OCR照合成功関連住所', [
'user_seq' => $user->user_seq,
'matched_address_type' => '関連住所',
'similarity' => $relatedSimilarity
]);
return [
'success' => true,
'matched_address_type' => '関連住所',
'matched_address' => $relatedAddress,
'similarity' => $relatedSimilarity,
'reason' => 'related_address_matched'
];
}
}
// 両方とも閾值以下OCR照合失敗
Log::info('SHJ-1 OCR照合失敗', [
'user_seq' => $user->user_seq,
'resident_similarity' => $residentSimilarity,
'related_similarity' => $relatedAddress ? ($relatedSimilarity ?? 0) : 'N/A',
'threshold' => $threshold
]);
return [
'success' => false,
'matched_address_type' => null,
'matched_address' => null,
'similarity' => max($residentSimilarity, $relatedSimilarity ?? 0),
'reason' => 'threshold_not_met'
];
}
/**
* 距離チェック処理設計書通りpark.distance_between_two_pointsを使用
*/
private function performDistanceCheck(User $user, Park $park): array
{
try {
// ユーザー住所を構築
$userAddress = $user->user_regident_pre . $user->user_regident_city . $user->user_regident_add;
// 駐輪場住所
$parkAddress = $park->park_adrs;
Log::info('SHJ-1 距離計算開始', [
'user_seq' => $user->user_seq,
'park_id' => $park->park_id,
'user_address' => $userAddress,
'park_address' => $parkAddress
]);
// Google Maps APIで距離計算
$distanceResult = $this->googleMapsService->calculateDistance($userAddress, $parkAddress);
$distanceMeters = $distanceResult['distance_meters'];
// 駐輪場の二点間距離制限を取得(設計書の要求)
$limitMeters = $park->distance_between_two_points ?? config('shj1.distance.default_limit_meters');
$withinLimit = $distanceMeters <= $limitMeters;
$distanceDetail = $this->googleMapsService->generateDistanceDetailString(
$park->park_id,
$distanceMeters,
'google_maps'
);
Log::info('SHJ-1 距離計算完了', [
'user_seq' => $user->user_seq,
'park_id' => $park->park_id,
'calculated_distance_meters' => $distanceMeters,
'distance_text' => $distanceResult['distance_text'] ?? null,
'limit_meters' => $limitMeters,
'within_limit' => $withinLimit,
'distance_detail' => $distanceDetail
]);
return [
'within_limit' => $withinLimit,
'distance_meters' => $distanceMeters,
'limit_meters' => $limitMeters,
'distance_detail' => $distanceDetail,
'user_address' => $userAddress,
'park_address' => $parkAddress
];
} catch (Exception $e) {
Log::error('Distance check error', [
'user_id' => $user->user_seq,
'park_id' => $park->park_id,
'error' => $e->getMessage()
]);
// API失敗時は距離NGとして処理設計書の要求
return [
'within_limit' => false,
'distance_meters' => 999999,
'limit_meters' => $park->distance_between_two_points ?? config('shj1.distance.default_limit_meters'),
'error' => $e->getMessage(),
'distance_detail' => $park->park_id . "/" . $park->park_id . "/API Error: " . $e->getMessage()
];
}
}
/**
* 成功ケースの処理
*/
private function processSuccessCase(User $user, Park $park, array $ocrResult, array $distanceResult): array
{
// ユーザー情報更新
$autoOkValue = config('shj1.identity_check_status.auto_ok');
Log::info('SHJ-1 成功ケース: ユーザー情報更新開始', [
'user_seq' => $user->user_seq,
'before_user_idcard_chk_flag' => $user->user_idcard_chk_flag,
'will_set_to' => $autoOkValue
]);
$updateResult = $user->update([
'user_idcard_chk_flag' => $autoOkValue,
'user_chk_day' => now(),
'user_chk_opeid' => 'SHJ-1'
]);
Log::info('SHJ-1 成功ケース: ユーザー情報更新完了', [
'user_seq' => $user->user_seq,
'update_result' => $updateResult,
'after_user_idcard_chk_flag' => $user->fresh()->user_idcard_chk_flag
]);
// 成功メール送信
$this->sendSuccessEmail($user, $park);
return [
'system_success' => true,
'identity_result' => 'OK',
'message' => '本人確認自動処理が成功しました',
'similarity_rate' => $ocrResult['similarity'] ?? 0,
'details' => [
'user_id' => $user->user_seq,
'park_id' => $park->park_id,
'ocr_result' => $ocrResult['reason'],
'distance_check' => 'within_limit'
],
'stats' => ['processed_count' => 1, 'success_count' => 1]
];
}
/**
* 失敗ケースの処理
*/
private function processFailureCase(User $user, Park $park, array $ocrResult, array $distanceResult): array
{
// ユーザー情報更新設計書通り、NG時もuser_chk_dayを更新
$autoNgValue = config('shj1.identity_check_status.auto_ng');
Log::info('SHJ-1 失敗ケース: ユーザー情報更新開始', [
'user_seq' => $user->user_seq,
'before_user_idcard_chk_flag' => $user->user_idcard_chk_flag,
'will_set_to' => $autoNgValue,
'ocr_result' => $ocrResult['reason'] ?? 'unknown',
'distance_within_limit' => $distanceResult['within_limit'] ?? 'unknown'
]);
$updateResult = $user->update([
'user_idcard_chk_flag' => $autoNgValue,
'user_chk_day' => now(),
'user_chk_opeid' => 'SHJ-1'
]);
Log::info('SHJ-1 失敗ケース: ユーザー情報更新完了', [
'user_seq' => $user->user_seq,
'update_result' => $updateResult,
'after_user_idcard_chk_flag' => $user->fresh()->user_idcard_chk_flag
]);
// オペレータキュー作成
$this->createOperatorQueue($user, $park, $distanceResult);
// 800M違反フラグ更新距離NGの場合
if (!$distanceResult['within_limit']) {
$this->update800mFlag($user, $park);
}
// 失敗メール送信
$this->sendFailureEmail($user, $park);
return [
'system_success' => true,
'identity_result' => 'NG',
'message' => '本人確認自動処理NGのため手動処理キューを作成しました',
'similarity_rate' => $ocrResult['similarity'] ?? 0,
'details' => [
'user_id' => $user->user_seq,
'park_id' => $park->park_id,
'ocr_result' => $ocrResult['reason'],
'distance_check' => $distanceResult['within_limit'] ? 'within_limit' : 'over_limit'
],
'stats' => ['processed_count' => 1, 'ng_count' => 1]
];
}
/**
* オペレータキュー作成
*/
private function createOperatorQueue(User $user, ?Park $park, array $distanceResult, bool $isNonTarget = false): void
{
$queueType = $user->user_school ?
config('shj1.operator_queue.queue_types.student') :
config('shj1.operator_queue.queue_types.general');
$comment = config('shj1.operator_queue.default_comment');
if (isset($distanceResult['distance_detail'])) {
$comment .= " / " . $distanceResult['distance_detail'];
}
if ($isNonTarget) {
$comment .= " / 対象外ユーザー";
}
OperatorQue::create([
'que_class' => $queueType,
'user_id' => $user->user_seq,
'park_id' => $park ? $park->park_id : null,
'que_status' => config('shj1.operator_queue.queue_status.created'),
'que_status_comment' => $comment,
'operator_id' => config('shj1.operator_queue.batch_operator_id'),
'created_at' => now(),
'updated_at' => now()
]);
}
/**
* 800M違反フラグ更新設計書通り本人確認オペレータIDも更新
*/
private function update800mFlag(User $user, Park $park): void
{
RegularContract::where('user_id', $user->user_seq)
->where('park_id', $park->park_id)
->update([
'800m_flag' => config('shj1.contract_800m.violation_flag'),
'updated_at' => now()
]);
}
/**
* 成功メール送信
*/
private function sendSuccessEmail(User $user, Park $park): void
{
try {
// SHJ-7 メール送信処理
$mailResult = $this->mailSendService->executeMailSend(
$user->user_primemail,
$user->user_submail,
config('shj1.mail.program_id_success')
);
// SHJ-7仕様準拠: result === 0 が正常、それ以外は異常
if (($mailResult['result'] ?? 1) === 0) {
Log::info('SHJ-1 成功メール送信成功', [
'user_id' => $user->user_seq,
'email' => $user->user_primemail
]);
} else {
// 仕様準拠: error_info をログに記録
Log::error('SHJ-1 成功メール送信失敗', [
'user_id' => $user->user_seq,
'email' => $user->user_primemail,
'error_info' => $mailResult['error_info'] ?? 'メール送信失敗'
]);
}
} catch (Exception $e) {
Log::error('SHJ-1 成功メール送信例外エラー', [
'user_id' => $user->user_seq,
'error' => $e->getMessage()
]);
}
}
/**
* 失敗メール送信
*/
private function sendFailureEmail(User $user, Park $park): void
{
try {
// SHJ-7 メール送信処理
$mailResult = $this->mailSendService->executeMailSend(
$user->user_primemail,
$user->user_submail,
config('shj1.mail.program_id_failure')
);
// SHJ-7仕様準拠: result === 0 が正常、それ以外は異常
if (($mailResult['result'] ?? 1) === 0) {
Log::info('SHJ-1 失敗メール送信成功', [
'user_id' => $user->user_seq,
'email' => $user->user_primemail
]);
} else {
// 仕様準拠: error_info をログに記録
Log::error('SHJ-1 失敗メール送信失敗', [
'user_id' => $user->user_seq,
'email' => $user->user_primemail,
'error_info' => $mailResult['error_info'] ?? 'メール送信失敗'
]);
}
} catch (Exception $e) {
Log::error('SHJ-1 失敗メール送信例外エラー', [
'user_id' => $user->user_seq,
'error' => $e->getMessage()
]);
}
}
/**
* 分析用テキスト正規化
*/
private function normalizeForAnalysis(string $text): string
{
// 空白・改行除去
$text = preg_replace('/\s+/', '', $text);
// 全角→半角統一
$text = mb_convert_kana($text, 'rnask', 'UTF-8');
// 住所同義語統一
$text = str_replace(['東京市', '東京府'], '東京都', $text);
$text = str_replace(['の'], '', $text);
return $text;
}
/**
* 文字レベル一致分析
*/
private function analyzeCharacterMatches(string $expected, string $ocrText): array
{
$expectedChars = mb_str_split($expected, 1, 'UTF-8');
$normalizedOcr = $this->normalizeForAnalysis($ocrText);
$analysis = [
'total_chars' => count($expectedChars),
'matched_chars' => 0,
'missing_chars' => [],
'match_rate' => 0
];
foreach ($expectedChars as $char) {
if (mb_strpos($normalizedOcr, $char, 0, 'UTF-8') !== false) {
$analysis['matched_chars']++;
} else {
$analysis['missing_chars'][] = $char;
}
}
$analysis['match_rate'] = round(($analysis['matched_chars'] / $analysis['total_chars']) * 100, 2);
return $analysis;
}
/**
* バッチ処理ログ作成
*
* SHJ-8サービスを呼び出してbat_job_logテーブルに登録
*
* @param int $userId 利用者連番
* @param string|null $userName 利用者名
* @param float|null $similarityRate 類似度
* @param array $result 処理結果
* @return void
*/
private function createBatchLog(int $userId, ?string $userName, ?float $similarityRate, array $result): void
{
try {
$device = Device::orderBy('device_id')->first();
$deviceId = $device ? $device->device_id : 1;
$today = now()->format('Y/m/d');
// ステータス判定
$status = $result['system_success'] ? 'success' : 'error';
// ステータスコメント生成: {user_id}/{user_name}/{similarity_rate}%
$displayUserId = $userId;
$displayUserName = $userName ?? '不明';
$displaySimilarity = $similarityRate !== null ? number_format($similarityRate, 1) : '0.0';
$statusComment = "{$displayUserId}/{$displayUserName}/{$displaySimilarity}%";
// SHJ-8サービス呼び出し
$this->shjEightService->execute(
$deviceId,
'SHJ-1',
'SHJ-1本人確認自動処理',
$status,
$statusComment,
$today,
$today
);
Log::info('SHJ-1 バッチ処理ログ作成完了', [
'user_id' => $userId,
'status' => $status,
'status_comment' => $statusComment
]);
} catch (Exception $e) {
Log::error('SHJ-1 バッチ処理ログ作成エラー', [
'error' => $e->getMessage(),
'user_id' => $userId
]);
}
}
}