From e3a60540be500db6a73ee5b03e226c291b317c05 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 30 Jan 2026 22:23:42 +0900 Subject: [PATCH] =?UTF-8?q?shj1=20shj2=20shj3=20=E6=94=B9=E4=BF=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- app/Services/ShjOneService.php | 180 ++++++++++++++++--------------- app/Services/ShjThreeService.php | 10 +- config/shj1.php | 4 +- 3 files changed, 100 insertions(+), 94 deletions(-) diff --git a/app/Services/ShjOneService.php b/app/Services/ShjOneService.php index 7932e59..c30fffa 100644 --- a/app/Services/ShjOneService.php +++ b/app/Services/ShjOneService.php @@ -544,11 +544,11 @@ class ShjOneService /** * SHJ-1文書通り:OCRテキストから氏名と住所を抽出 - * - * 文書仕様: + * + * 文書仕様(2026/01更新): * 1. "氏名"があるかチェック - * 2. "氏名"の次から年号和暦(昭和|平成)まで → [氏名] - * 3. "住所"があるかチェック + * 2. "氏名"の次から年号和暦(昭和|平成|令和)まで → [氏名] + * 3. "住所"があるかチェック * 4. "住所"の次から和暦(昭和|平成|令和)または"交付"まで → [住所] * 5. [OCR値] = [氏名] + [住所] */ @@ -556,7 +556,7 @@ class ShjOneService { // ノイズ除去:改行とスペースを統一 $cleanedOcr = preg_replace('/\s+/', '', $ocrText); - + // 1. "氏名"キーワードの存在チェック if (strpos($cleanedOcr, '氏名') === false) { return [ @@ -567,9 +567,9 @@ class ShjOneService 'ocr_value' => '' ]; } - - // 2. 氏名抽出:「氏名」の次から「住所」まで - $namePattern = '/氏名([^住]*?)住所/u'; + + // 2. 氏名抽出:「氏名」の次から年号和暦(昭和|平成|令和)まで + $namePattern = '/氏名(.+?)(?:昭和|平成|令和)/u'; if (!preg_match($namePattern, $cleanedOcr, $nameMatches)) { return [ 'success' => false, @@ -631,21 +631,21 @@ class ShjOneService /** * SHJ-1文書通り:[OCR值]と[利用者住所]の照合処理 - * - * 文書仕様: - * 1. [利用者居住所]と[OCR値]を照合 → 閾値以下なら次へ - * 2. [利用者関連住所]と[OCR値]を照合 → 閾値以下ならOCR照合失敗 + * + * 文書仕様(2026/01更新): + * - [利用者居住所]と[OCR値]を照合 → 閾値(75%)以上なら成功、未満なら失敗 + * - ※関連住所の照合は削除(運転免許証には居住所のみ記載のため) */ 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値]を照合 + + // [利用者居住所]と[OCR値]を照合 similar_text($residentAddress, $ocrValue, $residentSimilarity); - + Log::info('SHJ-1 居住住所照合', [ 'user_seq' => $user->user_seq, 'resident_address' => $residentAddress, @@ -653,7 +653,7 @@ class ShjOneService 'similarity' => $residentSimilarity, 'threshold' => $threshold ]); - + if ($residentSimilarity >= $threshold) { // 居住住所で照合成功 Log::info('SHJ-1 OCR照合成功(居住住所)', [ @@ -661,7 +661,7 @@ class ShjOneService 'matched_address_type' => '居住住所', 'similarity' => $residentSimilarity ]); - + return [ 'success' => true, 'matched_address_type' => '居住住所', @@ -670,122 +670,130 @@ class ShjOneService '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照合失敗 + + // 閾値未満: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), + 'similarity' => $residentSimilarity, 'reason' => 'threshold_not_met' ]; } /** - * 距離チェック処理(設計書通りpark.distance_between_two_pointsを使用) + * 距離チェック処理(2026/01更新:近傍駅座標を使用) + * + * 文書仕様: + * - 近傍駅マスタ(station)から座標を取得 + * - 近傍駅~自宅間の直線距離を計算 + * - 駐輪場マスタ.二点間距離と比較 */ 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 距離計算開始', [ + + // 近傍駅座標を取得(LIMIT 1で最初の1件) + $station = DB::table('station') + ->where('park_id', $park->park_id) + ->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, 'park_id' => $park->park_id, 'user_address' => $userAddress, - 'park_address' => $parkAddress + 'station_name' => $stationName, + 'station_lat' => $stationLat, + 'station_lng' => $stationLng ]); - - // 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' + + // Google Maps Geocoding APIでユーザー住所から座標を取得 + $userCoords = $this->googleMapsService->geocodeAddress($userAddress); + + if (!$userCoords || !isset($userCoords['lat']) || !isset($userCoords['lng'])) { + throw new Exception('ユーザー住所の座標取得に失敗しました'); + } + + // Haversine公式で2点間の直線距離を計算 + $distanceMeters = $this->googleMapsService->calculateStraightLineDistance( + $stationLat, + $stationLng, + $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, 'park_id' => $park->park_id, - 'calculated_distance_meters' => $distanceMeters, - 'distance_text' => $distanceResult['distance_text'] ?? null, + 'station_name' => $stationName, + 'calculated_distance_meters' => round($distanceMeters), 'limit_meters' => $limitMeters, 'within_limit' => $withinLimit, 'distance_detail' => $distanceDetail ]); - + return [ 'within_limit' => $withinLimit, - 'distance_meters' => $distanceMeters, + 'distance_meters' => round($distanceMeters), 'limit_meters' => $limitMeters, 'distance_detail' => $distanceDetail, 'user_address' => $userAddress, - 'park_address' => $parkAddress + 'station_name' => $stationName ]; - + } catch (Exception $e) { Log::error('Distance check error', [ 'user_id' => $user->user_seq, 'park_id' => $park->park_id, 'error' => $e->getMessage() ]); - - // API失敗時は距離NGとして処理(設計書の要求) + + // 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() + 'distance_detail' => $park->park_id . '/' . $park->park_id . '/API Error: ' . $e->getMessage() ]; } } diff --git a/app/Services/ShjThreeService.php b/app/Services/ShjThreeService.php index e8c4763..ac37c83 100644 --- a/app/Services/ShjThreeService.php +++ b/app/Services/ShjThreeService.php @@ -416,9 +416,8 @@ class ShjThreeService $elapsedDays = $todayDay - $startDay; } elseif ($endDay >= $todayDay) { // 駐輪場マスタ.更新期間終了日 >= [本日の日付]の日 の場合 - // 仕様書の計算式: ([先月の月末日]の日 − 駐輪場マスタ.更新期間開始日) + [本日の日付]の日 - $lastMonthEnd = $now->copy()->subMonth()->endOfMonth()->day; - $elapsedDays = ($lastMonthEnd - $startDay) + $todayDay; + // 対象外(翌月1日以降の猶予期間はメール送信しない) + $elapsedDays = 99; } else { // その他の場合 $elapsedDays = 99; // 対象外 @@ -548,10 +547,9 @@ class ShjThreeService if ($startDay <= $todayDay) { // 処理0.更新期間開始日 <= [本日の日付]の日 の場合 $query->where('T1.contract_periode', '=', $thisMonthEnd); - } elseif ($endDay >= $todayDay) { - // 処理0.更新期間終了日 >= [本日の日付]の日 の場合 - $query->where('T1.contract_periode', '=', $lastMonthEnd); } + // 翌月1日以降の猶予期間はメール送信対象外のため、 + // 処理0.更新期間終了日 >= [本日の日付]の日 の条件は削除 } $targetUsers = $query->get()->toArray(); diff --git a/config/shj1.php b/config/shj1.php index 52bda8e..68129a8 100644 --- a/config/shj1.php +++ b/config/shj1.php @@ -36,8 +36,8 @@ return [ | OCR処理関連設定 */ 'ocr' => [ - // 文字列類似度の閾値(70%) - 'similarity_threshold' => env('SHJ1_OCR_SIMILARITY_THRESHOLD', 70), + // 文字列類似度の閾値(75%) + 'similarity_threshold' => env('SHJ1_OCR_SIMILARITY_THRESHOLD', 75), // 対応身分証明書タイプ 'supported_id_types' => [