where('user_id', $user_id)->first(); // 市町村名(park→city JOINで重複排除) $cities = DB::table('park') ->join('city', 'park.city_id', '=', 'city.city_id') ->select('city.city_id', 'city.city_name') ->distinct() ->get(); // city_idごとの更新可能期間情報を取得 $city_grace_periods = DB::table('city') ->select('city_id', 'update_grace_period_start_date', 'update_grace_period_start_time', 'update_grace_period_end_date', 'update_grace_period_end_time') ->get() ->keyBy('city_id'); // 駅名(stationテーブルのstation_neighbor_station全件) $stations = DB::table('station') ->select('station_neighbor_station') ->distinct() ->get(); // 駐輪場名(parkテーブルのpark_name全件) $parks = DB::table('park') ->select('park_id', 'park_name') ->distinct() ->get(); // テーブル表示用データ(park/city/station JOIN, park_id昇順, 10件ずつページング) $page = request()->input('page', 1); $perPage = 10; $city_id = request()->input('city_id'); $station_name = request()->input('station_neighbor_station'); $park_id = request()->input('park_id'); $query = DB::table('park') ->join('city', 'park.city_id', '=', 'city.city_id') ->leftJoin('station', 'park.park_id', '=', 'station.park_id') ->select( 'park.park_id', 'park.park_name', 'park.park_ruby', 'city.city_name', 'city.city_id', 'station.station_neighbor_station', 'station.station_name_ruby' ); if ($city_id) { $query->where('city.city_id', $city_id); } if ($station_name) { $query->where('station.station_neighbor_station', $station_name); } if ($park_id) { $query->where('park.park_id', $park_id); } // 並び替えパラメータ取得 $sort = request()->input('sort', 'park_id'); $order = request()->input('order', 'asc'); $sortable = [ 'park_ruby' => 'park.park_ruby', 'city_id' => 'city.city_id', 'station_name_ruby' => 'station.station_name_ruby', 'park_id' => 'park.park_id', ]; if (isset($sortable[$sort])) { $query->orderBy($sortable[$sort], $order); } else { $query->orderBy('park.park_id', 'asc'); } $total = $query->count(); $parks_table = $query->skip(($page - 1) * $perPage)->take($perPage)->get(); // zoneテーブルデータを取得(psectionテーブルとJOINしてpsection_subjectも取得) $zones = DB::table('zone') ->leftJoin('psection', 'zone.psection_id', '=', 'psection.psection_id') ->select('zone.zone_id', 'zone.park_id', 'zone.psection_id', 'zone.zone_number', 'zone.zone_tolerance', 'psection.psection_subject') ->get() ->groupBy('park_id'); // 空き予約マスタデータを取得 $reserve = DB::table('reserve') ->select('reserve_id', 'park_id', 'psection_id') ->where('valid_flag', 1) ->get() ->groupBy('park_id'); // ルート名で画面表示を切り替え(新規定期契約 or 駐輪場検索) $isRegularContract = $request->route()->getName() === 'regular_contract.create'; // ヘッダーの選択状態を分岐 $active_menu = $isRegularContract ? 'SWC-8-1' : 'SWC-10-1'; if ($isRegularContract) { \Log::info('新規定期契約-駐輪場選択画面にアクセス', [ 'user_id' => $user_id, ]); } else { \Log::info('駐輪場検索-駐輪場選択画面にアクセス', [ 'user_id' => $user_id, ]); } return view('regular_contract.create', [ 'active_menu' => $active_menu, // この画面のID 'user_name' => $user ? $user->user_name : '', // ユーザー名(ヘッダー用) 'cities' => $cities, 'stations' => $stations, 'parks' => $parks, 'parks_table' => $parks_table, 'parks_table_total' => $total, 'parks_table_page' => $page, 'parks_table_perPage' => $perPage, 'zones' => $zones, 'city_grace_periods' => $city_grace_periods, 'reserve' => $reserve, 'isRegularContract' => $isRegularContract ]); } public function regulationCheck(Request $request) { // GETパラメータを取得 $parkId = $request->query('park_id'); $psectionId = $request->query('psection_id'); $ptypeId = $request->query('ptype_id'); // 必要なDB処理やロジック $park_regulation = DB::table('park')->where('park_id', $parkId)->value('parking_regulations_flag'); if ($park_regulation == 1) { // 駐輪規定画面へ return redirect()->route('regular_contract.regulation', [ 'park_id' => $parkId, 'psection_id' => $psectionId, 'ptype_id' => $ptypeId, ]); } else { // 契約情報入力画面へ return redirect()->route('regular_contract.input', [ 'park_id' => $parkId, 'psection_id' => $psectionId, 'ptype_id' => $ptypeId, ]); } } public function showRegulation(Request $request) { $user_id = session('user_id'); if (!$user_id) { return redirect('/login'); } $user_name = DB::table('user')->where('user_id', $user_id)->value('user_name'); // 必要なパラメータ取得 $parkId = $request->query('park_id'); $psectionId = $request->query('psection_id'); $ptypeId = $request->query('ptype_id'); $park_name = DB::table('park')->where('park_id', $parkId)->value('park_name'); $psection_subject = DB::table('psection')->where('psection_id', $psectionId)->value('psection_subject'); $ptype_subject = DB::table('ptype')->where('ptype_id', $ptypeId)->value('ptype_subject'); $regulations_text = DB::table('parking_regulations') ->where('park_id', $parkId) ->where('psection_id', $psectionId) ->where('ptype_id', $ptypeId) ->value('regulations_text'); \Log::info('駐輪規定確認画面にアクセス', [ 'user_id' => $user_id, ]); return view('regular_contract.regulation', [ 'park_id' => $parkId, 'park_name' => $park_name, 'psection_id' => $psectionId, 'psection_subject' => $psection_subject, 'ptype_id' => $ptypeId, 'ptype_subject' => $ptype_subject, 'regulations_text' => $regulations_text, 'active_menu' => 'SWC-8-1', // この画面のID 'user_name' => $user_name, // ユーザー名(ヘッダー用) ]); } public function insertRegulation(Request $request) { $user_id = session('user_id'); if (!$user_id) { return redirect('/login'); } $park_id = $request->input('park_id'); $psection_id = $request->input('psection_id'); $ptype_id = $request->input('ptype_id'); DB::table('parking_regulations_read')->insert([ 'park_id' => $park_id, 'psection_id' => $psection_id, 'ptype_id' => $ptype_id, 'user_id' => $user_id, 'created_at' => now(), ]); // 契約入力画面へリダイレクト return redirect()->route('regular_contract.input', [ 'park_id' => $park_id, 'psection_id' => $psection_id, 'ptype_id' => $ptype_id, ]); } // 契約入力画面表示 public function showContractForm(Request $request) { $user_id = session('user_id'); if (!$user_id) { return redirect('/login'); } $user = DB::table('user')->where('user_id', $user_id)->first(); $park_id = $request->query('park_id'); $park = DB::table('park')->where('park_id', $park_id)->first(); $psection_id = $request->query('psection_id'); $ptype_id = $request->query('ptype_id'); $city_id = DB::table('park')->where('park_id', $park_id)->value('city_id'); $terms_text = DB::table('terms')->where('city_id', $city_id)->value('terms_text'); // 利用者区分をusertypeテーブルから取得 $user_category = ''; if (isset($user->user_categoryid)) { $usertype = DB::table('usertype') ->where('user_categoryid', $user->user_categoryid) ->first(); if ($usertype && isset($usertype->usertype_subject1)) { $user_category = $usertype->usertype_subject1; } } $user->user_homephone = explode('-', $user->user_homephone ?? ''); $user->user_mobile = explode('-', $user->user_mobile ?? ''); $user->user_regident_zip_1 = substr($user->user_regident_zip ?? '', 0, 3); $user->user_regident_zip_2 = substr($user->user_regident_zip ?? '', 3, 4); $user->user_relate_zip_1 = substr($user->user_relate_zip ?? '', 0, 3); $user->user_relate_zip_2 = substr($user->user_relate_zip ?? '', 3, 4); \Log::info('新規定期契約-契約情報入力画面にアクセス', [ 'user_id' => $user_id, ]); session()->forget('show_terms_modal'); return view('regular_contract.input', [ 'park' => $park, 'psection_id' => $psection_id, 'ptype_id' => $ptype_id, 'terms_text' => $terms_text, 'user' => $user, 'user_name' => $user->user_name, // ユーザー名(ヘッダー用) 'active_menu' => 'SWC-8-1', // この画面のID 'user_category' => $user_category, 'show_terms_modal' => true, ]); } public function inputCheck(Request $request) { $user_id = session('user_id'); if (!$user_id) { return redirect('/login'); } $user = DB::table('user')->where('user_id', $user_id)->first(); $park_id = $request->input('park_id'); $park = DB::table('park')->where('park_id', $park_id)->first(); // バリデーションルール $rules = [ 'user_phonetic' => ['required', 'regex:/^[ァ-ヶー \s]+$/u'], 'user_regident_zip_1' => 'required|digits:3', 'user_regident_zip_2' => 'required|digits:4', 'user_regident_pre' => [ 'required', Rule::in([ '北海道', '青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県', '茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県', '新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県', '三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県', '鳥取県', '島根県', '岡山県', '広島県', '山口県', '徳島県', '香川県', '愛媛県', '高知県', '福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県' ]), ], 'user_regident_city' => ['required', 'string', 'max:20', 'regex:/^(?:(?![\xF0-\xF7][\x80-\xBF]{3}).)*$/'], 'user_regident_add' => ['required', 'string', 'max:50', 'regex:/^(?:(?![\xF0-\xF7][\x80-\xBF]{3}).)*$/'], 'user_homephone.*' => 'nullable|digits_between:1,5', 'user_mobile.*' => 'nullable|digits_between:1,5', 'user_submail' => 'nullable|email|different:user_primemail|max:80', 'user_category' => ['required', Rule::in(['一般', '学生'])], 'contract_reduction' => ['required', Rule::in(['はい', 'いいえ'])], 'user_relate_zip_1' => 'nullable|digits:3', 'user_relate_zip_2' => 'nullable|digits:4', 'user_relate_pre' => [ 'nullable', Rule::in([ '北海道', '青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県', '茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県', '新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県', '三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県', '鳥取県', '島根県', '岡山県', '広島県', '山口県', '徳島県', '香川県', '愛媛県', '高知県', '福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県' ]), ], 'user_relate_city' => ['nullable', 'string', 'max:20', 'regex:/^(?:(?![\xF0-\xF7][\x80-\xBF]{3}).)*$/'], 'user_relate_add' => ['nullable', 'string', 'max:50', 'regex:/^(?:(?![\xF0-\xF7][\x80-\xBF]{3}).)*$/'], ]; // 性別欄が表示されている場合のみ必須 if ((int)$park->gender_display_flag === 1) { $rules['user_gender'] = ['required', Rule::in(['男性', '女性'])]; } // 生年月日欄が表示されている場合のみ必須 if ((int)$park->bd_display_flag === 1) { $rules['user_birthdate'] = ['required', 'date']; } // 防犯登録番号欄が表示されている場合のみ必須 if ((int)$park->securityreg_display_flag === 1) { $rules['user_securitynum'] = ['required', 'max:50', 'regex:/^[a-zA-Z0-9]+$/']; } // 利用者区分ごとの追加バリデーション if ($request->input('user_category') === '学生') { $rules['user_school'] = ['required', 'string', 'max:50', 'regex:/^(?:(?![\xF0-\xF7][\x80-\xBF]{3}).)*$/']; $rules['user_graduate'] = ['required', 'date']; } else { $rules['user_workplace'] = ['nullable', 'string', 'max:50', 'regex:/^(?:(?![\xF0-\xF7][\x80-\xBF]{3}).)*$/']; } $messages = [ 'user_phonetic.required' => 'フリガナが入力されていません。', 'user_phonetic.regex' => 'フリガナはカタカナでご入力ください。', 'user_gender.required' => '性別が入力されていません。', 'user_gender.in' => '性別は「男性」または「女性」を選択してください。', 'user_regident_zip_1.required' => '居住所の郵便番号(前半3桁)が入力されていません。', 'user_regident_zip_2.required' => '居住所の郵便番号(後半4桁)が入力されていません。', 'user_regident_zip_1.digits' => '居住所の郵便番号(前半3桁)は3桁の数字で入力してください。', 'user_regident_zip_2.digits' => '居住所の郵便番号(後半4桁)は4桁の数字で入力してください。', 'user_regident_pre.required' => '居住所の都道府県が選択されていません。', 'user_regident_pre.in' => '都道府県は選択肢から選んでください。', 'user_regident_city.required' => '居住所の市区町村が入力されていません。', 'user_regident_city.max' => '居住所の市区町村は20文字以内で入力してください。', 'user_regident_city.regex' => '居住所の市区町村に絵文字などの特殊文字は使用できません。', 'user_regident_add.required' => '居住所の住所が入力されていません。', 'user_regident_add.max' => '居住所の住所は50文字以内で入力してください。', 'user_regident_add.regex' => '居住所の住所に絵文字などの特殊文字は使用できません。', 'user_birthdate.required' => '生年月日が入力されていません。', 'user_birthdate.date' => '生年月日は正しい日付で入力してください。', 'user_homephone.*.digits_between' => '自宅電話番号はそれぞれ1~5桁の数字で入力してください。', 'user_mobile.*.digits_between' => '携帯電話番号はそれぞれ1~5桁の数字で入力してください。', 'user_submail.email' => '予備メールアドレスは正しい形式で入力してください。', 'user_submail.max' => '予備メールアドレスは80文字以内で入力してください。', 'user_submail.different' => 'メールアドレスと予備メールアドレスに同じアドレスを入力できません。', 'user_category.required' => '利用者区分は必須です。', 'user_category.in' => '利用者区分の値が不正です。', 'contract_reduction.required' => '減免が選択されていません。', 'contract_reduction.in' => '減免の値が不正です。', 'user_workplace.max' => '勤務先は50文字以内で入力してください。', 'user_workplace.regex' => '勤務先に絵文字などの特殊文字は使用できません。', 'user_school.required' => '学校名が入力されていません。', 'user_school.max' => '学校名は50文字以内で入力してください。', 'user_school.regex' => '学校名に絵文字などの特殊文字は使用できません。', 'user_graduate.required' => '卒業年月日が入力されていません。', 'user_graduate.date' => '卒業年月日は正しい日付で入力してください。', 'user_relate_zip_1.digits' => '住所の郵便番号(前半3桁)は3桁の数字で入力してください。', 'user_relate_zip_2.digits' => '住所の郵便番号(後半4桁)は4桁の数字で入力してください。', 'user_relate_pre.in' => '住所の都道府県は選択肢から選んでください。', 'user_relate_city.max' => '住所の市区町村は20文字以内で入力してください。', 'user_relate_city.regex' => '住所の市区町村に絵文字などの特殊文字は使用できません。', 'user_relate_add.max' => '住所は50文字以内で入力してください。', 'user_relate_add.regex' => '住所に絵文字などの特殊文字は使用できません。', 'user_securitynum.required' => '防犯登録番号が入力されていません。', 'user_securitynum.max' => '防犯登録番号は50文字以内で入力してください。', 'user_securitynum.regex' => '防犯登録番号は英数字のみで入力してください。', ]; try { $validated = $request->validate($rules, $messages); } catch (ValidationException $e) { return Redirect()->back() ->withErrors($e->validator) ->withInput($request->except('_token') + ['show_terms_modal' => false]); } if (empty(implode('', $request->input('user_homephone', []))) && empty(implode('', $request->input('user_mobile', [])))) { return redirect()->back() ->withErrors(['user_homephone' => '自宅電話番号または携帯電話番号のいずれかは必須です']) ->withInput($request->except('_token') + ['show_terms_modal' => false]); } $city = DB::table('city')->where('city_id', $park->city_id)->first(); $matched = (mb_strpos($request->user_regident_city, $city->city_name) !== false); if ($matched === true) { $ward_residents = 1; DB::table('user') ->where('user_id', $user->user_id) ->update(['ward_residents' => $ward_residents]); } else { $contract_allowable_city_name = DB::table('contract_allowable_city')->where('city_id', $city->city_id)->value('contract_allowable_city_name'); $matched_allowable = (mb_strpos($request->user_regident_city, $contract_allowable_city_name) !== false); if ($matched_allowable) { $ward_residents = 0; DB::table('user') ->where('user_id', $user->user_id) ->update(['ward_residents' => $ward_residents]); } else { return redirect()->back()->withErrors(['契約対象地域にお住まいでないためお申込みできません'])->withInput()->with(['show_terms_modal' => false]); } } if ($ward_residents == 1) { $usertype_subject2 = '区民'; } else { $usertype_subject2 = '区民外'; } $user_categoryid = DB::table('usertype') ->where('usertype_subject1', $request->user_category) ->where('usertype_subject2', $usertype_subject2) ->value('user_categoryid'); $updateData = [ 'user_categoryid' => $user_categoryid, 'user_phonetic' => $request->user_phonetic, 'user_mobile' => implode('-', $request->input('user_mobile', [])), 'user_homephone' => implode('-', $request->input('user_homephone', [])), 'user_submail' => $request->filled('user_submail') ? $request->user_submail : null, 'user_regident_zip' => $request->user_regident_zip_1 . $request->user_regident_zip_2, 'user_regident_pre' => $request->user_regident_pre, 'user_regident_city' => $request->user_regident_city, 'user_regident_add' => $request->user_regident_add, 'user_relate_zip' => ($request->filled('user_relate_zip_1') && $request->filled('user_relate_zip_2')) ? ($request->user_relate_zip_1 . $request->user_relate_zip_2) : null, 'user_relate_pre' => $request->filled('user_relate_pre') ? $request->user_relate_pre : null, 'user_relate_city' => $request->filled('user_relate_city') ? $request->user_relate_city : null, 'user_relate_add' => $request->filled('user_relate_add') ? $request->user_relate_add : null, 'ward_residents' => $ward_residents, 'user_workplace' => $request->user_category === '一般' ? $request->user_workplace : null, 'user_school' => $request->user_category === '学生' ? $request->user_school : null, 'user_graduate' => $request->user_category === '学生' ? $request->user_graduate : null, 'updated_at' => now() ]; // 性別欄が表示されている場合のみ追加 if (!empty($park->gender_display_flag) && $park->gender_display_flag == 1) { $updateData['user_gender'] = $request->user_gender; } // 生年月日欄が表示されている場合のみ追加 if (!empty($park->bd_display_flag) && $park->bd_display_flag == 1) { $updateData['user_birthdate'] = $request->user_birthdate; $updateData['user_age'] = $request->user_age; } DB::table('user') ->where('user_id', $user->user_id) ->update($updateData); $zone_id = DB::table('zone') ->where('park_id', $request->park_id) ->where('ptype_id', $request->ptype_id) ->where('psection_id', $request->psection_id) ->orderBy('zone_sort', 'asc') ->value('zone_id'); $contract_id = DB::table('regular_contract')->insertGetId([ 'created_at' => now(), 'updated_at' => now(), 'user_id' => $user->user_id, 'user_categoryid' => $user_categoryid, 'park_id' => $park->park_id, 'contract_created_at' => now(), 'contract_reduction' => $ward_residents, 'update_flag' => 2, 'contract_cancel_flag' => 0, 'psection_id' => $request->psection_id, 'ptype_id' => $request->ptype_id, 'zone_id' => $zone_id ]); $contractUpdateData = [ 'contract_qr_id' => DB::raw("TO_BASE64(AES_ENCRYPT($contract_id, 'LJLASR4FAS34SAADFA72ASDFALLSDRGT'))") ]; // 防犯登録番号が表示されている場合のみ追加 if (!empty($park->securityreg_display_flag) && $park->securityreg_display_flag == 1) { $contractUpdateData['user_securitynum'] = $request->user_securitynum; } DB::table('regular_contract') ->where('contract_id', $contract_id) ->update($contractUpdateData); return redirect()->route('regular_contract.upload_identity_create', [ 'contract_id' => $contract_id, ]); } public function showUploadIdentityCreate(Request $request) { $user_id = session('user_id'); if (!$user_id) { return redirect('/login'); } $user_name = DB::table('user')->where('user_id', $user_id)->value('user_name'); $contract = DB::table('regular_contract')->where('contract_id', $request->contract_id)->first(); \Log::info('新規定期契約-本人確認書類アップロード画面にアクセス', [ 'user_id' => $user_id, ]); return view('regular_contract.upload_identity_create', [ 'contract_id' => $request->contract_id, 'park_id' => $contract->park_id, 'psection_id' => $contract->psection_id, 'ptype_id' => $contract->ptype_id, 'user_name' => $user_name, 'active_menu' => 'SWC-8-1' ]); } public function confirmUploadIdentity(Request $request, $contract_id) { $user_id = session('user_id'); if (!$user_id) { return redirect('/login'); } $validator = Validator::make($request->all(), [ 'idcard_type' => 'required', 'user_idcard' => 'required|file|mimes:jpg,jpeg,png,pdf', ], [ 'idcard_type.required' => '本人確認書類の種類を選択してください。', 'user_idcard.required' => '本人確認書類のおもて画像をアップロードしてください。', 'user_idcard.file' => '本人確認書類のおもて画像はファイルで指定してください。', 'user_idcard.mimes' => 'アップロードできるファイル形式はjpg、jpeg、png、pdfのみです。', ]); if ($validator->fails()) { return redirect()->route('regular_contract.upload_identity_create', [ 'contract_id' => $contract_id, ]) ->withErrors($validator) ->withInput(); } // おもて画像保存(Laravel Storageを使用) $front = $request->file('user_idcard'); $filename_front = uniqid('photo1_') . '.' . $front->getClientOriginalExtension(); $front->storeAs('photo', $filename_front, 'public'); // userテーブルに保存(チェック済フラグはSHJ-1処理後に設定) $updateData = [ 'photo_filename1' => $filename_front, 'user_idcard' => $request->idcard_type, 'updated_at' => now(), ]; // ウラ画像がある場合保存し更新項目に追加 if ($request->hasFile('user_idcard2')) { $back = $request->file('user_idcard2'); $filename_back = uniqid('photo2_') . '.' . $back->getClientOriginalExtension(); $back->storeAs('photo', $filename_back, 'public'); $updateData['photo_filename2'] = $filename_back; } DB::table('user')->where('user_id', $user_id)->update($updateData); // SHJ-1 本人確認自動処理を実行 $user = DB::table('user')->where('user_id', $user_id)->first(); $park = DB::table('park')->where('park_id', $request->park_id)->first(); $psection = DB::table('psection')->where('psection_id', $request->psection_id)->first(); $usertype = DB::table('usertype')->where('user_categoryid', $user->user_categoryid)->first(); // user_idからuser_seqを取得してSHJ-1に渡す $user_seq = $user->user_seq; $park_id = $request->park_id; \Log::info('SHJ-1バッチ処理開始', [ 'user_id' => $user_id, 'user_seq' => $user_seq, 'park_id' => $park_id, 'contract_id' => $contract_id ]); try { // SHJ-1 コマンドを同期実行 $exitCode = Artisan::call('shj:one', [ 'user_id' => $user_seq, 'park_id' => $park_id ]); \Log::info('SHJ-1バッチ処理完了', [ 'exit_code' => $exitCode, 'user_seq' => $user_seq, 'park_id' => $park_id ]); // 処理結果に基づいて遷移先を決定 if ($exitCode === 0) { // 成功の場合 return redirect("/regular-contract/upload_identity_success?contract_id={$contract_id}"); // return view('regular_contract.create_confirm', [ // 'contract_id' => $request->contract_id, // 'user' => $user, // 'park' => $park, // 'psection' => $psection, // 'usertype' => $usertype, // 'user_name' => $user->user_name, // 'active_menu' => 'SWC-8-1' // ]); } else { // 失敗の場合 または、学生証 と その他 の場合 return redirect("/regular-contract/upload_identity_fail?contract_id={$contract_id}"); // return view('regular_contract.create_confirm', [ // 'contract_id' => $request->contract_id, // 'user' => $user, // 'park' => $park, // 'psection' => $psection, // 'usertype' => $usertype, // 'user_name' => $user->user_name, // 'active_menu' => 'SWC-8-1' // ]); } } catch (\Exception $e) { \Log::error('SHJ-1バッチ処理でエラー発生', [ 'error' => $e->getMessage(), 'user_seq' => $user_seq, 'park_id' => $park_id ]); return redirect("/regular-contract/upload_identity_fail?contract_id={$contract_id}"); } } public function createConfirmNext($contract_id) { $user_id = session('user_id'); if (!$user_id) { return redirect('/login'); } $user = DB::table('user')->where('user_id', $user_id)->first(); // 本人確認自動処理結果を取得 if ($user && $user->user_idcard_chk_flag == 2) { // 本人確認OKの場合は利用期間選択画面へ // 必要な各マスタ情報を取得 $contract = DB::table('regular_contract')->where('contract_id', $contract_id)->first(); $park = DB::table('park')->where('park_id', $contract->park_id)->first(); $city = DB::table('city')->where('city_id', $park->city_id)->first(); $regular_type = DB::table('regular_type')->where('city_id', $city->city_id)->first(); $usertype = DB::table('usertype')->where('user_categoryid', $contract->user_categoryid)->first(); // 2重化しているマスタのため現在のテーブル名を取得 $master_setting = DB::table('setting')->value('web_master'); $tableName = 'price' . $master_setting; // 利用者区分に応じた逆利用フラグを取得 $inverse_use_flag_column = ($usertype->usertype_subject1 == '一般') ? 'inverse_use_flag1' : 'inverse_use_flag2'; $inverse_use_flag = $park->$inverse_use_flag_column; if ($inverse_use_flag == 0) { // regident_cityまたはrelate_cityが一致するか $is_same = ( strpos($user->user_regident_city, $city->city_name) !== false || strpos($user->user_relate_city, $city->city_name) !== false ); } else { // regident_cityのみ一致するか $is_same = (strpos($user->user_regident_city, $city->city_name) !== false); } $target_subject2 = $is_same ? '区民' : '区民外'; $user_categoryid = DB::table('usertype') ->where('usertype_subject1', $usertype->usertype_subject1) ->where('usertype_subject2', $target_subject2) ->where('usertype_subject3', $usertype->usertype_subject3) ->value('user_categoryid'); // 駐輪場所マスタから料金を取得 $prices = DB::table($tableName) ->where('park_id', $contract->park_id) ->where('psection_id', $contract->psection_id) ->where('ptype_id', $contract->ptype_id) ->where('user_categoryid', $user_categoryid) ->get(); \Log::info('利用期間選択画面にアクセス', [ 'user_id' => $user_id, ]); // 利用期間選択画面へ遷移 return view('regular_contract.create_select_period', [ 'active_menu' => 'SWC-4-1', // マイページメニューの選択状態用 'user_name' => $user->user_name, // ユーザー名(ヘッダー用) 'contract_id' => $contract_id, 'regular_type' => $regular_type, 'prices' => $prices, ]); } else { // NGの場合は本人確認書類確認中画面へ \Log::info('本人確認書類確認中画面にアクセス', [ 'user_id' => $user_id, ]); return view('regular_contract.create_idcard_checking', [ 'active_menu' => 'SWC-8-1', // マイページメニューの選択状態用 'user_name' => $user->user_name, // ユーザー名(ヘッダー用) ]); } } public function selectPeriod(Request $request) { $user_id = session('user_id'); if (!$user_id) { return redirect('/login'); } // 期間選択チェック $request->validate([ 'month' => 'required', ], [ 'month.required' => '契約期間が選択されていません。', ]); $contract_id = $request->input('contract_id'); $month = $request->input('month'); $price = $request->input('price_' . $month); $today = now(); $day = $today->day; if ($day <= 19) { // 今月の1日 $contract_periods = $today->copy()->startOfMonth()->format('Y-m-d'); } else { // 翌月の1日 $contract_periods = $today->copy()->addMonth()->startOfMonth()->format('Y-m-d'); } $contract_periode = Carbon::parse($contract_periods)->addMonths($month - 1)->endOfMonth()->format('Y-m-d'); // 契約更新 DB::table('regular_contract')->where('contract_id', $contract_id)->update([ 'enable_months' => $month, 'billing_amount' => $price, 'contract_periods' => $contract_periods, 'contract_periode' => $contract_periode, 'updated_at' => now(), ]); // 完了後はウェルネット決済画面(仮)へリダイレクト return redirect()->route('wellnet.payment'); } /** * SHJ-1本人確認処理成功ページ表示!!!!テスト後に削除して!!!!viewも!!!!! */ public function showUploadIdentitySuccess(Request $request) { $contractId = $request->get('contract_id'); $userId = Session::get('user_id'); // 詳細情報を取得 $debugInfo = $this->getShjDebugInfo($userId, $contractId); return view('regular_contract.upload_identity_success', compact('debugInfo', 'contractId')); } /** * SHJ-1本人確認処理失敗ページ表示!!!!テスト後に削除して!!!!viewも!!!!! */ public function showUploadIdentityFail(Request $request) { $contractId = $request->get('contract_id'); $userId = Session::get('user_id'); // 詳細情報を取得 $debugInfo = $this->getShjDebugInfo($userId, $contractId); return view('regular_contract.upload_identity_fail', compact('debugInfo', 'contractId')); } /** * SHJ-1デバッグ情報取得!!!!テスト後に削除して!!!! */ private function getShjDebugInfo($userId, $contractId) { try { // ユーザー情報取得 $user = DB::table('user')->where('user_id', $userId)->first(); // 契約情報取得 $contract = DB::table('regular_contract')->where('user_id', $user->user_id ?? 0)->first(); // 駐輪場情報取得 $park = null; if ($contract) { $park = DB::table('park')->where('park_id', $contract->park_id)->first(); } // 最新のSHJ-1バッチログ取得 $batchLog = DB::table('batch_log') ->where('process_name', 'SHJ-1本人確認自動処理') ->orderBy('created_at', 'desc') ->first(); // 最新のログファイルから詳細ログを取得 $logContent = $this->getRecentShjLogs(); // ログからAPI結果情報を解析 $apiResults = $this->parseApiResultsFromLogs($logContent); return [ 'user' => $user, 'contract' => $contract, 'park' => $park, 'batch_log' => $batchLog, 'detailed_logs' => $logContent, 'recent_logs' => $logContent, // 为调试页面提供日志内容访问 'timestamp' => now()->format('Y-m-d H:i:s'), // API結果情報 'ocr_text_length' => $apiResults['ocr_text_length'], 'ocr_text_preview' => $apiResults['ocr_text_preview'], 'ocr_full_text' => $apiResults['ocr_full_text'], 'ocr_threshold' => $apiResults['ocr_threshold'], 'name_similarity' => $apiResults['name_similarity'], 'address_similarity' => $apiResults['address_similarity'], 'name_passed' => $apiResults['name_passed'], 'address_passed' => $apiResults['address_passed'], 'matched_address_type' => $apiResults['matched_address_type'] ?? null, // 新增:匹配成功的地址类型 'name_match_attempts' => $apiResults['name_match_attempts'], 'address_match_attempts' => $apiResults['address_match_attempts'], 'best_name_match' => $apiResults['best_name_match'], 'best_address_match' => $apiResults['best_address_match'], 'name_match_details' => $apiResults['name_match_details'], 'address_match_details' => $apiResults['address_match_details'], 'ocr_debug_info' => $apiResults['ocr_debug_info'] ?? null, // 移除重复分析逻辑,只显示SHJ-1的结果 'calculated_distance' => $apiResults['calculated_distance'], 'distance_text' => $apiResults['distance_text'] ?? null, 'distance_limit' => $apiResults['distance_limit'] ?? null, 'distance_start_address' => $apiResults['distance_start_address'] ?? null, 'distance_end_address' => $apiResults['distance_end_address'] ?? null, 'distance_passed' => $apiResults['distance_passed'], 'maps_api_status' => $apiResults['maps_api_status'], 'maps_api_error' => $apiResults['maps_api_error'] ]; } catch (Exception $e) { \Log::error('Debug info error: ' . $e->getMessage()); return [ 'error' => 'デバッグ情報の取得に失敗しました: ' . $e->getMessage(), 'timestamp' => now()->format('Y-m-d H:i:s') ]; } } /** * 最新のSHJ-1関連ログを取得!!!!テスト後に削除して!!!! */ private function getRecentShjLogs() { try { $logPath = storage_path('logs/laravel.log'); if (!file_exists($logPath)) { return '日志文件未找到'; } $logs = file_get_contents($logPath); $lines = explode("\n", $logs); // 最新の1000行からSHJ-1関連ログを抽出(さらに範囲拡大) $recentLines = array_slice($lines, -1000); $shjLogs = []; foreach ($recentLines as $line) { if (strpos($line, 'SHJ-1') !== false || strpos($line, 'GoogleVision') !== false || strpos($line, 'Google Maps') !== false || strpos($line, 'バッチ処理') !== false) { $shjLogs[] = $line; } } return implode("\n", array_slice($shjLogs, -200)); // 最新200行(さらに拡大) } catch (Exception $e) { return 'ログ取得エラー: ' . $e->getMessage(); } } /** * ログからAPI結果情報を解析!!!!テスト後に削除して!!!! */ private function parseApiResultsFromLogs($logContent) { $results = [ 'ocr_text_length' => 'N/A', 'ocr_text_preview' => 'N/A', 'ocr_threshold' => 'N/A', 'name_similarity' => 'N/A', 'address_similarity' => 'N/A', 'name_passed' => false, 'address_passed' => false, 'calculated_distance' => 'N/A', 'distance_passed' => false, 'maps_api_status' => '成功', 'maps_api_error' => 'なし', // 新增详细OCR信息 'ocr_full_text' => 'N/A', 'name_match_attempts' => [], 'address_match_attempts' => [], 'best_name_match' => 'N/A', 'best_address_match' => 'N/A', 'match_details' => 'N/A' ]; try { // OCR処理完了ログから文字数とプレビューを抽取(清理格式) if (preg_match('/GoogleVision OCR処理完了.*"text_length":(\d+).*"text_preview":"([^"]*)"/', $logContent, $matches)) { $results['ocr_text_length'] = $matches[1]; // 清理OCR文本,移除可能的日志污染 $cleanText = $matches[2]; // 移除可能混入的日志时间戳和标记 $cleanText = preg_replace('/\[\d{4}-\d{2}-\d{2}.*?\].*?local\.INFO.*?$/', '', $cleanText); // 移除末尾的不完整JSON片段 $cleanText = preg_replace('/\s*\{?\s*$/', '', $cleanText); // 清理换行和多余空格 $cleanText = trim($cleanText); $results['ocr_text_preview'] = $cleanText; } // 尝试从GoogleVision日志提取完整的OCR文本 if (preg_match('/GoogleVision OCR処理完了.*"text_full":"([^"]*)"/', $logContent, $matches)) { $fullText = $matches[1]; // 解码JSON转义字符 $fullText = str_replace('\n', "\n", $fullText); $fullText = str_replace('\r', "\r", $fullText); $fullText = str_replace('\"', '"', $fullText); $fullText = str_replace('\\\\', '\\', $fullText); $results['ocr_full_text'] = $fullText; } // 尝试从SHJ-1日志提取完整的OCR文本(多种模式匹配) $patterns = [ '/SHJ-1 完全OCR認識結果.*"ocr_full_text":"([^"]*)"/', // 原始日文版本 '/螳悟・OCR隱崎ュ倡オ先棡.*"ocr_full_text":"([^"]*)"/', // 日文编码版本 '/OCR認識結果.*"ocr_full_text":"([^"]*)"/', // 简化匹配 '/"ocr_full_text":"([^"]*)"/', // 最宽泛匹配 ]; foreach ($patterns as $pattern) { if (empty($results['ocr_full_text']) && preg_match($pattern, $logContent, $matches)) { $encodedText = $matches[1]; // Base64解码 $fullText = base64_decode($encodedText); if ($fullText !== false && strlen($fullText) > 10) { $results['ocr_full_text'] = $fullText; // 简化OCR信息显示 - 只显示基本信息 $results['ocr_debug_info'] = [ 'decoded_length' => strlen($fullText), 'preview' => substr($fullText, 0, 200) ]; break; // 找到后停止尝试其他模式 } } } // 尝试从OCR认识内容详细日志提取(备选方案,适配日文编码) if (empty($results['ocr_full_text']) && preg_match('/OCR隱崎ュ伜・螳ケ隧ウ邏ー.*"raw_text":"OCR_TEXT_START>([^<]*) $match[1], 'score' => round(floatval($match[2]), 2), 'found_in_ocr' => $match[3] ]; } } $results['name_match_details'] = $nameMatchDetails; // 提取住所匹配详细信息(适配日文编码) $addressMatchDetails = []; if (preg_match_all('/菴乗園繝槭ャ繝√Φ繧ー隧ウ邏ー.*"target_address":"([^"]*)".*"address_type":"([^"]*)".*"similarity_score":([^,}]*).*"ocr_contains_address":"([^"]*)"/', $logContent, $addressMatches, PREG_SET_ORDER)) { foreach ($addressMatches as $match) { $addressMatchDetails[] = [ 'target' => $match[1], 'type' => $match[2], 'score' => round(floatval($match[3]), 2), 'found_in_ocr' => $match[4] ]; } } $results['address_match_details'] = $addressMatchDetails; // 从新的SHJ-1日志格式中提取信息 // 提取OCR抽出结果 if (preg_match('/SHJ-1 OCR抽出成功.*"extracted_name":"([^"]*)".*"extracted_address":"([^"]*)".*"ocr_value":"([^"]*)"/', $logContent, $matches)) { $results['extracted_name'] = $matches[1]; $results['extracted_address'] = $matches[2]; $results['extracted_ocr_value'] = $matches[3]; } // 提取居住住所照合结果 if (preg_match('/SHJ-1 居住住所照合.*"resident_address":"([^"]*)".*"similarity":([^,}]*)/', $logContent, $matches)) { $results['resident_address'] = $matches[1]; $results['resident_similarity'] = round(floatval($matches[2]), 2); } // 提取関連住所照合结果 if (preg_match('/SHJ-1 関連住所照合.*"related_address":"([^"]*)".*"similarity":([^,}]*)/', $logContent, $matches)) { $results['related_address'] = $matches[1]; $results['related_similarity'] = round(floatval($matches[2]), 2); } // 提取最終OCR結果 if (preg_match('/SHJ-1 OCR照合(成功|失敗)/', $logContent, $matches)) { $results['final_ocr_result'] = $matches[1]; $results['address_passed'] = ($matches[1] === '成功'); $results['name_passed'] = ($matches[1] === '成功'); // 新SHJ-1逻辑中,成功表示整体成功 } // 如果OCR文本为空或太短,提供说明 if (empty($results['ocr_text_preview']) || strlen($results['ocr_text_preview']) < 5) { $results['ocr_text_preview'] = '(OCR認識内容が短いか、表示できない文字が含まれています)'; } // 表面画像処理完了の詳細結果を抽取 if (preg_match('/SHJ-1 表面画像処理完了.*"front_result":\{"name_matches":\[([^\]]*)\],"address_matches":\[([^\]]*)\]\}/', $logContent, $matches)) { $nameMatches = explode(',', $matches[1]); $addressMatches = explode(',', $matches[2]); $results['name_match_attempts'] = array_map(function($val) { return round(floatval($val), 2); }, $nameMatches); $results['address_match_attempts'] = array_map(function($val) { return round(floatval($val), 2); }, $addressMatches); } // OCR類似度計算結果の詳細情報を抽取 if (preg_match('/SHJ-1 OCR類似度計算結果.*"best_name_match":([^,}]*).*"best_address_match":([^,}]*).*"all_name_matches":\[([^\]]*)\].*"all_address_matches":\[([^\]]*)\]/', $logContent, $matches)) { $results['best_name_match'] = round(floatval($matches[1]), 2); $results['best_address_match'] = round(floatval($matches[2]), 2); $allNameMatches = explode(',', $matches[3]); $allAddressMatches = explode(',', $matches[4]); $results['name_match_attempts'] = array_map(function($val) { return round(floatval($val), 2); }, $allNameMatches); $results['address_match_attempts'] = array_map(function($val) { return round(floatval($val), 2); }, $allAddressMatches); } // OCR閾値チェックログから類似度情報を抽出(新しい順序匹配対応) if (preg_match('/SHJ-1 OCR閾値チェック.*"threshold":"?([^",}]*)"?.*"name_match":([^,}]*).*"address_match":([^,}]*).*"name_passed":([^,}]*).*"address_passed":([^,}]*).*"matched_address_type":"?([^",}]*)"?/', $logContent, $matches)) { $results['ocr_threshold'] = $matches[1]; $results['name_similarity'] = round(floatval($matches[2]), 2); $results['address_similarity'] = round(floatval($matches[3]), 2); $results['name_passed'] = $matches[4] === 'true'; $results['address_passed'] = $matches[5] === 'true'; $results['matched_address_type'] = $matches[6] ?: null; } // Google Maps API エラーチェック if (strpos($logContent, 'Google Maps distance calculation error') !== false) { $results['maps_api_status'] = 'エラー'; if (preg_match('/Google Maps distance calculation error.*"error":"([^"]*)"/', $logContent, $matches)) { $results['maps_api_error'] = $matches[1]; } } else if (strpos($logContent, 'Distance calculation failed') !== false) { $results['maps_api_status'] = 'アドレス未発見'; $results['maps_api_error'] = 'NOT_FOUND'; } // 距離計算結果を抽取(最新の詳細ログから) if (preg_match_all('/SHJ-1 距離計算完了.*"calculated_distance_meters":(\d+).*"distance_text":"([^"]*)".*"limit_meters":"?([^",}]*)"?.*"within_limit":([^,}]*)/', $logContent, $allMatches, PREG_SET_ORDER)) { // 最後の(最新の)マッチを使用 $matches = end($allMatches); $results['calculated_distance'] = $matches[1]; // 距離メートル $results['distance_text'] = $matches[2]; // Google Mapsテキスト $results['distance_limit'] = $matches[3]; // 制限値 $results['distance_passed'] = $matches[4] === 'true'; } else { // ログから具体的な結果が取得できない場合はデフォルト値を設定 // ※重要:API成功≠距離制限内ではないため、明示的にfalseにする $results['distance_passed'] = false; if (strpos($logContent, 'Distance check error') === false && strpos($logContent, 'Google Maps distance calculation error') === false) { $results['calculated_distance'] = '計算成功(制限値詳細はログで確認)'; $results['maps_api_status'] = '成功(但制限確認要)'; } else { $results['calculated_distance'] = '計算失敗'; $results['maps_api_status'] = 'エラー'; } } // 距離計算開始ログから起点・終点住所を抽取 if (preg_match('/SHJ-1 距離計算開始.*"user_address":"([^"]*)".*"park_address":"([^"]*)"/', $logContent, $matches)) { $results['distance_start_address'] = $matches[1]; $results['distance_end_address'] = $matches[2]; } } catch (Exception $e) { \Log::error('API結果解析エラー: ' . $e->getMessage()); } return $results; } // 不再需要的分析方法已移除 - 只显示SHJ-1的结果 /** * 废弃的方法(已不再使用) */ private function manualOcrAnalysis($logContent, $user = null) { $analysis = [ 'found_base64' => false, 'decoded_success' => false, 'decoded_text' => '', 'contains_yamada' => false, 'contains_taro' => false, 'contains_tokyo' => false, 'contains_osaka' => false, 'full_analysis' => 'Analysis failed', 'corrected_matching' => null ]; try { // 分割日志内容为行数组,按时间倒序搜索最新的OCR结果 $logLines = explode("\n", $logContent); $logLines = array_reverse($logLines); // 从最新的开始搜索 // 查找最新的Base64编码OCR结果 $patterns = [ '/SHJ-1 完全OCR認識結果.*"ocr_full_text":"([^"]*)"/', '/SHJ-1.*OCR.*結果.*"ocr_full_text":"([^"]*)"/', '/"ocr_full_text":"([^"]*)"/' ]; foreach ($logLines as $line) { foreach ($patterns as $pattern) { if (preg_match($pattern, $line, $matches)) { $analysis['found_base64'] = true; $base64Text = $matches[1]; // Base64解码 $decodedText = base64_decode($base64Text); if ($decodedText !== false && strlen($decodedText) > 10) { $analysis['decoded_success'] = true; $analysis['decoded_text'] = $decodedText; // 内容分析 $analysis['contains_yamada'] = strpos($decodedText, '山田') !== false; $analysis['contains_taro'] = strpos($decodedText, '太郎') !== false; $analysis['contains_tokyo'] = strpos($decodedText, '東京') !== false; $analysis['contains_osaka'] = strpos($decodedText, '大阪') !== false; // ユーザー提案:空白と改行を除去して比較 $analysis['corrected_matching'] = $this->performCorrectedMatching($decodedText, $user); // 詳細分析(実際のユーザーデータから期待値を取得) if ($user) { $expectedName = $user->user_name ?? ""; $expectedAddress = ($user->user_regident_pre ?? '') . ($user->user_regident_city ?? '') . ($user->user_regident_add ?? '') ?: ""; } else { $expectedName = ""; $expectedAddress = ""; } $analysis['full_analysis'] = "OCR認識テキスト長: " . strlen($decodedText) . "文字\n" . "期待氏名: '$expectedName'\n" . "期待住所: '$expectedAddress'\n" . "山田を含む: " . ($analysis['contains_yamada'] ? 'YES' : 'NO') . "\n" . "太郎を含む: " . ($analysis['contains_taro'] ? 'YES' : 'NO') . "\n" . "東京を含む: " . ($analysis['contains_tokyo'] ? 'YES' : 'NO') . "\n" . "大阪を含む: " . ($analysis['contains_osaka'] ? 'YES' : 'NO') . "\n" . "認識完全内容: " . substr($decodedText, 0, 200) . "..."; // 成功解码最新OCR结果后立即返回 return $analysis; } // 解码失败时,继续搜索其他条目 } } } } catch (Exception $e) { $analysis['full_analysis'] = 'OCR分析エラー: ' . $e->getMessage(); } return $analysis; } /** * 修正マッチングアルゴリズム実行(空白・改行除去) */ private function performCorrectedMatching($ocrText, $user = null) { // ユーザー情報から期待値を取得、デフォルトはテスト用 if ($user) { $expectedName = $user->user_name ?? ""; $expectedAddress = ($user->user_regident_pre ?? '') . ($user->user_regident_city ?? '') . ($user->user_regident_add ?? '') ?: ""; } else { $expectedName = ""; $expectedAddress = ""; } // 統一的テキスト正規化関数 $normalize = function($text) { // 全空白文字と改行を除去(全角スペース含む) $text = preg_replace('/[\s\x{3000}]+/u', '', $text); // \x{3000}は全角スペース // 一般的な空白文字を明示的に除去 $text = str_replace([' ', ' ', "\t", "\n", "\r"], '', $text); // 全角→半角変換 $text = mb_convert_kana($text, 'rnask', 'UTF-8'); // 住所統一 $text = str_replace(['東京市', '東京府'], '東京都', $text); $text = str_replace(['の'], '', $text); // 数字統一 $text = str_replace(['1','2','3','4','5','6','7','8','9','0'], ['1','2','3','4','5','6','7','8','9','0'], $text); return $text; }; // 正規化処理 $normalizedOcr = $normalize($ocrText); $normalizedExpectedName = $normalize($expectedName); $normalizedExpectedAddr = $normalize($expectedAddress); // 使用"住所"分割OCR文本 $addressKeyword = '住所'; $ocrParts = explode($addressKeyword, $normalizedOcr, 2); $personalInfoSection = $ocrParts[0] ?? ''; // "住所"前の個人情報欄 $addressSection = isset($ocrParts[1]) ? $addressKeyword . $ocrParts[1] : ''; // "住所"後の住所欄 // 分区マッチング計算 $nameMatch = $this->calculateSimpleMatch($normalizedExpectedName, $personalInfoSection); $addrMatch = $this->calculateSimpleMatch($normalizedExpectedAddr, $addressSection); return [ 'original_ocr' => substr($ocrText, 0, 100) . '...', 'normalized_ocr' => substr($normalizedOcr, 0, 100) . '...', 'normalized_expected_name' => $normalizedExpectedName, 'normalized_expected_addr' => $normalizedExpectedAddr, 'personal_info_section' => substr($personalInfoSection, 0, 80) . '...', 'address_section' => substr($addressSection, 0, 80) . '...', 'name_match_score' => $nameMatch, 'addr_match_score' => $addrMatch, 'name_passed' => $nameMatch >= 70, 'addr_passed' => $addrMatch >= 70, 'overall_result' => ($nameMatch >= 70 && $addrMatch >= 70) ? 'PASS' : 'FAIL' ]; } /** * シンプルマッチング計算 */ private function calculateSimpleMatch($expected, $haystack) { if (empty($expected)) return 0; // 1. 完全包含チェック if (strpos($haystack, $expected) !== false) { return 100; } // 2. 文字包含率 $expectedChars = mb_str_split($expected, 1, 'UTF-8'); $foundChars = 0; foreach ($expectedChars as $char) { if (mb_strpos($haystack, $char, 0, 'UTF-8') !== false) { $foundChars++; } } $charRate = ($foundChars / count($expectedChars)) * 100; // 3. 類似度計算 similar_text($expected, $haystack, $similarRate); // 最高スコアを返す return max($charRate, $similarRate); } }