Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f139a3f608
commit
e3a60540be
@ -544,11 +544,11 @@ class ShjOneService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SHJ-1文書通り:OCRテキストから氏名と住所を抽出
|
* SHJ-1文書通り:OCRテキストから氏名と住所を抽出
|
||||||
*
|
*
|
||||||
* 文書仕様:
|
* 文書仕様(2026/01更新):
|
||||||
* 1. "氏名"があるかチェック
|
* 1. "氏名"があるかチェック
|
||||||
* 2. "氏名"の次から年号和暦(昭和|平成)まで → [氏名]
|
* 2. "氏名"の次から年号和暦(昭和|平成|令和)まで → [氏名]
|
||||||
* 3. "住所"があるかチェック
|
* 3. "住所"があるかチェック
|
||||||
* 4. "住所"の次から和暦(昭和|平成|令和)または"交付"まで → [住所]
|
* 4. "住所"の次から和暦(昭和|平成|令和)または"交付"まで → [住所]
|
||||||
* 5. [OCR値] = [氏名] + [住所]
|
* 5. [OCR値] = [氏名] + [住所]
|
||||||
*/
|
*/
|
||||||
@ -556,7 +556,7 @@ class ShjOneService
|
|||||||
{
|
{
|
||||||
// ノイズ除去:改行とスペースを統一
|
// ノイズ除去:改行とスペースを統一
|
||||||
$cleanedOcr = preg_replace('/\s+/', '', $ocrText);
|
$cleanedOcr = preg_replace('/\s+/', '', $ocrText);
|
||||||
|
|
||||||
// 1. "氏名"キーワードの存在チェック
|
// 1. "氏名"キーワードの存在チェック
|
||||||
if (strpos($cleanedOcr, '氏名') === false) {
|
if (strpos($cleanedOcr, '氏名') === false) {
|
||||||
return [
|
return [
|
||||||
@ -567,9 +567,9 @@ class ShjOneService
|
|||||||
'ocr_value' => ''
|
'ocr_value' => ''
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 氏名抽出:「氏名」の次から「住所」まで
|
// 2. 氏名抽出:「氏名」の次から年号和暦(昭和|平成|令和)まで
|
||||||
$namePattern = '/氏名([^住]*?)住所/u';
|
$namePattern = '/氏名(.+?)(?:昭和|平成|令和)/u';
|
||||||
if (!preg_match($namePattern, $cleanedOcr, $nameMatches)) {
|
if (!preg_match($namePattern, $cleanedOcr, $nameMatches)) {
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@ -631,21 +631,21 @@ class ShjOneService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SHJ-1文書通り:[OCR值]と[利用者住所]の照合処理
|
* SHJ-1文書通り:[OCR值]と[利用者住所]の照合処理
|
||||||
*
|
*
|
||||||
* 文書仕様:
|
* 文書仕様(2026/01更新):
|
||||||
* 1. [利用者居住所]と[OCR値]を照合 → 閾値以下なら次へ
|
* - [利用者居住所]と[OCR値]を照合 → 閾値(75%)以上なら成功、未満なら失敗
|
||||||
* 2. [利用者関連住所]と[OCR値]を照合 → 閾値以下ならOCR照合失敗
|
* - ※関連住所の照合は削除(運転免許証には居住所のみ記載のため)
|
||||||
*/
|
*/
|
||||||
private function performOcrMatching(User $user, string $ocrValue): array
|
private function performOcrMatching(User $user, string $ocrValue): array
|
||||||
{
|
{
|
||||||
$threshold = config('shj1.ocr.similarity_threshold');
|
$threshold = config('shj1.ocr.similarity_threshold');
|
||||||
|
|
||||||
// [利用者居住所]を構築
|
// [利用者居住所]を構築
|
||||||
$residentAddress = $user->user_regident_pre . $user->user_regident_city . $user->user_regident_add;
|
$residentAddress = $user->user_regident_pre . $user->user_regident_city . $user->user_regident_add;
|
||||||
|
|
||||||
// 1. [利用者居住所]と[OCR値]を照合
|
// [利用者居住所]と[OCR値]を照合
|
||||||
similar_text($residentAddress, $ocrValue, $residentSimilarity);
|
similar_text($residentAddress, $ocrValue, $residentSimilarity);
|
||||||
|
|
||||||
Log::info('SHJ-1 居住住所照合', [
|
Log::info('SHJ-1 居住住所照合', [
|
||||||
'user_seq' => $user->user_seq,
|
'user_seq' => $user->user_seq,
|
||||||
'resident_address' => $residentAddress,
|
'resident_address' => $residentAddress,
|
||||||
@ -653,7 +653,7 @@ class ShjOneService
|
|||||||
'similarity' => $residentSimilarity,
|
'similarity' => $residentSimilarity,
|
||||||
'threshold' => $threshold
|
'threshold' => $threshold
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($residentSimilarity >= $threshold) {
|
if ($residentSimilarity >= $threshold) {
|
||||||
// 居住住所で照合成功
|
// 居住住所で照合成功
|
||||||
Log::info('SHJ-1 OCR照合成功(居住住所)', [
|
Log::info('SHJ-1 OCR照合成功(居住住所)', [
|
||||||
@ -661,7 +661,7 @@ class ShjOneService
|
|||||||
'matched_address_type' => '居住住所',
|
'matched_address_type' => '居住住所',
|
||||||
'similarity' => $residentSimilarity
|
'similarity' => $residentSimilarity
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'matched_address_type' => '居住住所',
|
'matched_address_type' => '居住住所',
|
||||||
@ -670,122 +670,130 @@ class ShjOneService
|
|||||||
'reason' => 'resident_address_matched'
|
'reason' => 'resident_address_matched'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. [利用者関連住所]と[OCR値]を照合
|
// 閾値未満: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照合失敗', [
|
Log::info('SHJ-1 OCR照合失敗', [
|
||||||
'user_seq' => $user->user_seq,
|
'user_seq' => $user->user_seq,
|
||||||
'resident_similarity' => $residentSimilarity,
|
'resident_similarity' => $residentSimilarity,
|
||||||
'related_similarity' => $relatedAddress ? ($relatedSimilarity ?? 0) : 'N/A',
|
|
||||||
'threshold' => $threshold
|
'threshold' => $threshold
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'matched_address_type' => null,
|
'matched_address_type' => null,
|
||||||
'matched_address' => null,
|
'matched_address' => null,
|
||||||
'similarity' => max($residentSimilarity, $relatedSimilarity ?? 0),
|
'similarity' => $residentSimilarity,
|
||||||
'reason' => 'threshold_not_met'
|
'reason' => 'threshold_not_met'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 距離チェック処理(設計書通りpark.distance_between_two_pointsを使用)
|
* 距離チェック処理(2026/01更新:近傍駅座標を使用)
|
||||||
|
*
|
||||||
|
* 文書仕様:
|
||||||
|
* - 近傍駅マスタ(station)から座標を取得
|
||||||
|
* - 近傍駅~自宅間の直線距離を計算
|
||||||
|
* - 駐輪場マスタ.二点間距離と比較
|
||||||
*/
|
*/
|
||||||
private function performDistanceCheck(User $user, Park $park): array
|
private function performDistanceCheck(User $user, Park $park): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// ユーザー住所を構築
|
// ユーザー住所を構築
|
||||||
$userAddress = $user->user_regident_pre . $user->user_regident_city . $user->user_regident_add;
|
$userAddress = $user->user_regident_pre . $user->user_regident_city . $user->user_regident_add;
|
||||||
|
|
||||||
// 駐輪場住所
|
// 近傍駅座標を取得(LIMIT 1で最初の1件)
|
||||||
$parkAddress = $park->park_adrs;
|
$station = DB::table('station')
|
||||||
|
->where('park_id', $park->park_id)
|
||||||
Log::info('SHJ-1 距離計算開始', [
|
->first();
|
||||||
|
|
||||||
|
// 近傍駅データがない、または座標がNULLの場合
|
||||||
|
if (!$station || empty($station->station_latitude) || empty($station->station_longitude)) {
|
||||||
|
Log::warning('SHJ-1 近傍駅座標データなし', [
|
||||||
|
'user_seq' => $user->user_seq,
|
||||||
|
'park_id' => $park->park_id,
|
||||||
|
'station_exists' => !empty($station),
|
||||||
|
'has_latitude' => !empty($station->station_latitude ?? null),
|
||||||
|
'has_longitude' => !empty($station->station_longitude ?? null)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// オペレーターキューに追加するためエラーとして返す
|
||||||
|
return [
|
||||||
|
'within_limit' => false,
|
||||||
|
'distance_meters' => 999999,
|
||||||
|
'limit_meters' => $park->distance_between_two_points ?? config('shj1.distance.default_limit_meters'),
|
||||||
|
'error' => '近傍駅の座標データが見つかりません',
|
||||||
|
'distance_detail' => $park->park_id . '/' . $park->park_id . '/近傍駅座標データなし',
|
||||||
|
'station_coordinate_missing' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stationLat = (float) $station->station_latitude;
|
||||||
|
$stationLng = (float) $station->station_longitude;
|
||||||
|
$stationName = $station->station_neighbor_station ?? '不明';
|
||||||
|
|
||||||
|
Log::info('SHJ-1 距離計算開始(近傍駅ベース)', [
|
||||||
'user_seq' => $user->user_seq,
|
'user_seq' => $user->user_seq,
|
||||||
'park_id' => $park->park_id,
|
'park_id' => $park->park_id,
|
||||||
'user_address' => $userAddress,
|
'user_address' => $userAddress,
|
||||||
'park_address' => $parkAddress
|
'station_name' => $stationName,
|
||||||
|
'station_lat' => $stationLat,
|
||||||
|
'station_lng' => $stationLng
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Google Maps APIで距離計算
|
// Google Maps Geocoding APIでユーザー住所から座標を取得
|
||||||
$distanceResult = $this->googleMapsService->calculateDistance($userAddress, $parkAddress);
|
$userCoords = $this->googleMapsService->geocodeAddress($userAddress);
|
||||||
$distanceMeters = $distanceResult['distance_meters'];
|
|
||||||
|
if (!$userCoords || !isset($userCoords['lat']) || !isset($userCoords['lng'])) {
|
||||||
// 駐輪場の二点間距離制限を取得(設計書の要求)
|
throw new Exception('ユーザー住所の座標取得に失敗しました');
|
||||||
$limitMeters = $park->distance_between_two_points ?? config('shj1.distance.default_limit_meters');
|
}
|
||||||
|
|
||||||
$withinLimit = $distanceMeters <= $limitMeters;
|
// Haversine公式で2点間の直線距離を計算
|
||||||
$distanceDetail = $this->googleMapsService->generateDistanceDetailString(
|
$distanceMeters = $this->googleMapsService->calculateStraightLineDistance(
|
||||||
$park->park_id,
|
$stationLat,
|
||||||
$distanceMeters,
|
$stationLng,
|
||||||
'google_maps'
|
$userCoords['lat'],
|
||||||
|
$userCoords['lng']
|
||||||
);
|
);
|
||||||
|
|
||||||
Log::info('SHJ-1 距離計算完了', [
|
// 駐輪場の二点間距離制限を取得
|
||||||
|
$limitMeters = $park->distance_between_two_points ?? config('shj1.distance.default_limit_meters');
|
||||||
|
|
||||||
|
$withinLimit = $distanceMeters <= $limitMeters;
|
||||||
|
$distanceDetail = $park->park_id . '/' . $stationName . '/二点間距離:' . round($distanceMeters) . 'm';
|
||||||
|
|
||||||
|
Log::info('SHJ-1 距離計算完了(近傍駅ベース)', [
|
||||||
'user_seq' => $user->user_seq,
|
'user_seq' => $user->user_seq,
|
||||||
'park_id' => $park->park_id,
|
'park_id' => $park->park_id,
|
||||||
'calculated_distance_meters' => $distanceMeters,
|
'station_name' => $stationName,
|
||||||
'distance_text' => $distanceResult['distance_text'] ?? null,
|
'calculated_distance_meters' => round($distanceMeters),
|
||||||
'limit_meters' => $limitMeters,
|
'limit_meters' => $limitMeters,
|
||||||
'within_limit' => $withinLimit,
|
'within_limit' => $withinLimit,
|
||||||
'distance_detail' => $distanceDetail
|
'distance_detail' => $distanceDetail
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'within_limit' => $withinLimit,
|
'within_limit' => $withinLimit,
|
||||||
'distance_meters' => $distanceMeters,
|
'distance_meters' => round($distanceMeters),
|
||||||
'limit_meters' => $limitMeters,
|
'limit_meters' => $limitMeters,
|
||||||
'distance_detail' => $distanceDetail,
|
'distance_detail' => $distanceDetail,
|
||||||
'user_address' => $userAddress,
|
'user_address' => $userAddress,
|
||||||
'park_address' => $parkAddress
|
'station_name' => $stationName
|
||||||
];
|
];
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error('Distance check error', [
|
Log::error('Distance check error', [
|
||||||
'user_id' => $user->user_seq,
|
'user_id' => $user->user_seq,
|
||||||
'park_id' => $park->park_id,
|
'park_id' => $park->park_id,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// API失敗時は距離NGとして処理(設計書の要求)
|
// API失敗時は距離NGとして処理
|
||||||
return [
|
return [
|
||||||
'within_limit' => false,
|
'within_limit' => false,
|
||||||
'distance_meters' => 999999,
|
'distance_meters' => 999999,
|
||||||
'limit_meters' => $park->distance_between_two_points ?? config('shj1.distance.default_limit_meters'),
|
'limit_meters' => $park->distance_between_two_points ?? config('shj1.distance.default_limit_meters'),
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'distance_detail' => $park->park_id . "/" . $park->park_id . "/API Error: " . $e->getMessage()
|
'distance_detail' => $park->park_id . '/' . $park->park_id . '/API Error: ' . $e->getMessage()
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -416,9 +416,8 @@ class ShjThreeService
|
|||||||
$elapsedDays = $todayDay - $startDay;
|
$elapsedDays = $todayDay - $startDay;
|
||||||
} elseif ($endDay >= $todayDay) {
|
} elseif ($endDay >= $todayDay) {
|
||||||
// 駐輪場マスタ.更新期間終了日 >= [本日の日付]の日 の場合
|
// 駐輪場マスタ.更新期間終了日 >= [本日の日付]の日 の場合
|
||||||
// 仕様書の計算式: ([先月の月末日]の日 − 駐輪場マスタ.更新期間開始日) + [本日の日付]の日
|
// 対象外(翌月1日以降の猶予期間はメール送信しない)
|
||||||
$lastMonthEnd = $now->copy()->subMonth()->endOfMonth()->day;
|
$elapsedDays = 99;
|
||||||
$elapsedDays = ($lastMonthEnd - $startDay) + $todayDay;
|
|
||||||
} else {
|
} else {
|
||||||
// その他の場合
|
// その他の場合
|
||||||
$elapsedDays = 99; // 対象外
|
$elapsedDays = 99; // 対象外
|
||||||
@ -548,10 +547,9 @@ class ShjThreeService
|
|||||||
if ($startDay <= $todayDay) {
|
if ($startDay <= $todayDay) {
|
||||||
// 処理0.更新期間開始日 <= [本日の日付]の日 の場合
|
// 処理0.更新期間開始日 <= [本日の日付]の日 の場合
|
||||||
$query->where('T1.contract_periode', '=', $thisMonthEnd);
|
$query->where('T1.contract_periode', '=', $thisMonthEnd);
|
||||||
} elseif ($endDay >= $todayDay) {
|
|
||||||
// 処理0.更新期間終了日 >= [本日の日付]の日 の場合
|
|
||||||
$query->where('T1.contract_periode', '=', $lastMonthEnd);
|
|
||||||
}
|
}
|
||||||
|
// 翌月1日以降の猶予期間はメール送信対象外のため、
|
||||||
|
// 処理0.更新期間終了日 >= [本日の日付]の日 の条件は削除
|
||||||
}
|
}
|
||||||
|
|
||||||
$targetUsers = $query->get()->toArray();
|
$targetUsers = $query->get()->toArray();
|
||||||
|
|||||||
@ -36,8 +36,8 @@ return [
|
|||||||
| OCR処理関連設定
|
| OCR処理関連設定
|
||||||
*/
|
*/
|
||||||
'ocr' => [
|
'ocr' => [
|
||||||
// 文字列類似度の閾値(70%)
|
// 文字列類似度の閾値(75%)
|
||||||
'similarity_threshold' => env('SHJ1_OCR_SIMILARITY_THRESHOLD', 70),
|
'similarity_threshold' => env('SHJ1_OCR_SIMILARITY_THRESHOLD', 75),
|
||||||
|
|
||||||
// 対応身分証明書タイプ
|
// 対応身分証明書タイプ
|
||||||
'supported_id_types' => [
|
'supported_id_types' => [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user