diff --git a/app/miniapi/controller/Upload.php b/app/miniapi/controller/Upload.php index 5e9c408..b14857e 100644 --- a/app/miniapi/controller/Upload.php +++ b/app/miniapi/controller/Upload.php @@ -11,7 +11,7 @@ class Upload extends Base { $scene = strtolower(trim((string)$this->request->post('scene', ''))); $contactUserId = 0; - if (in_array($scene, ['passport', 'id_card', 'evus_correction'], true)) { + if (in_array($scene, ['passport', 'id_card', 'evus_correction', 'visa_application'], true)) { $login = $this->requireLogin(); if (!is_int($login)) { return $login; @@ -40,13 +40,14 @@ class Upload extends Base $originalName = (string)($_FILES['file']['name'] ?? 'upload.jpg'); $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); $ext = $ext === 'jpeg' ? 'jpg' : $ext; - if (!in_array($ext, ['jpg', 'png', 'gif', 'webp', 'bmp'], true)) { - return $this->uploadResponse(0, '请上传图片文件'); + $pdf = $scene === 'visa_application' && $ext === 'pdf'; + if (!in_array($ext, ['jpg', 'png', 'gif', 'webp', 'bmp'], true) && !$pdf) { + return $this->uploadResponse(0, $scene === 'visa_application' ? '请上传图片或PDF文件' : '请上传图片文件'); } $mime = function_exists('mime_content_type') ? (string)mime_content_type($path) : ''; - if ($mime !== '' && strpos($mime, 'image/') !== 0) { - return $this->uploadResponse(0, '请上传图片文件'); + if ($mime !== '' && ($pdf ? !in_array($mime, ['application/pdf', 'application/x-pdf'], true) : strpos($mime, 'image/') !== 0)) { + return $this->uploadResponse(0, $scene === 'visa_application' ? '请上传图片或PDF文件' : '请上传图片文件'); } $content = file_get_contents($path); @@ -55,7 +56,9 @@ class Upload extends Base } $fileName = date('YmdHis') . uniqid('', true) . '.' . $ext; - if ($scene === 'evus_correction') { + if ($scene === 'visa_application') { + $objectKey = 'visa-application/' . $contactUserId . '/' . $fileName; + } elseif ($scene === 'evus_correction') { $objectKey = 'evus-correction/' . $contactUserId . '/' . $fileName; } elseif ($contactUserId > 0) { $objectKey = 'contact/' . $contactUserId . '/' . $fileName; @@ -80,10 +83,12 @@ class Upload extends Base } $url = OSS_URL . $objectKey; - return $this->uploadResponse(1, '上传成功', [ + $data = [ 'path' => $url, 'url' => $url, - ]); + ]; + if ($scene === 'visa_application') $data['upload_token'] = authcode((string)json_encode(['purpose' => 'visa_upload_receipt', 'user_id' => $contactUserId, 'url' => $url], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'ENCODE', MINIAPI_TOKEN_KEY, 2592000); + return $this->uploadResponse(1, '上传成功', $data); } catch (\Throwable $e) { return $this->uploadResponse(0, '上传失败,请重试'); } diff --git a/app/miniapi/controller/VisaApplication.php b/app/miniapi/controller/VisaApplication.php new file mode 100644 index 0000000..9365c8c --- /dev/null +++ b/app/miniapi/controller/VisaApplication.php @@ -0,0 +1,29 @@ +call(fn($service, $userId) => $service->options($userId, $this->orderSource())); } + public function start() { return $this->call(fn($service, $userId) => $service->start($userId, $this->postData(), $this->orderSource())); } + public function application() { return $this->call(fn($service, $userId) => $service->application($userId, trim((string)$this->request->param('public_id', '')))); } + public function draft() { return $this->call(fn($service, $userId) => $service->saveDraft($userId, $this->postData()), '草稿已保存'); } + public function submit() { return $this->call(fn($service, $userId) => $service->submit($userId, $this->postData()), '提交成功'); } + public function dataSource() + { + $filters = (array)$this->request->get(); unset($filters['type'], $filters['s'], $filters['mini_app_code'], $filters['token']); $filters['source_code'] = $this->orderSource(); + $result = (new VisaApplicationService())->dataSource(trim((string)$this->request->param('type', '')), $filters); + return (int)($result['code'] ?? 0) === 1 ? $this->ok((array)($result['data'] ?? [])) : $this->fail((string)($result['msg'] ?? '数据源暂不可用')); + } + private function call(callable $callback, string $message = '操作成功') + { + $userId = $this->requireLogin(); if (!is_int($userId)) return $userId; + $result = $callback(new VisaApplicationService(), $userId); if ((int)($result['code'] ?? 0) !== 1) return $this->fail((string)($result['msg'] ?? '服务异常,请稍后重试')); + return $this->ok((array)($result['data'] ?? []), $message); + } + public static function sourceForCode(string $code): string { return $code === 'evus_jn_mp' ? 'jnwxmini' : ($code === 'evus_bj_mp' ? 'qlwxmini' : ''); } + private function orderSource(): string { $source = self::sourceForCode($this->currentMiniAppCode()); if ($source === '') throw new \InvalidArgumentException('当前应用不支持签证办理'); return $source; } +} diff --git a/app/miniapi/route/app.php b/app/miniapi/route/app.php index f9ab67f..34c99a1 100644 --- a/app/miniapi/route/app.php +++ b/app/miniapi/route/app.php @@ -60,4 +60,10 @@ Route::group(function () { Route::post('evus/lookup', 'Evus/lookup'); Route::post('evus/apply', 'Evus/apply'); Route::post('esta/apply', 'Esta/apply'); + Route::get('visa/options', 'VisaApplication/options'); + Route::post('visa/start', 'VisaApplication/start'); + Route::get('visa/application', 'VisaApplication/application'); + Route::post('visa/draft', 'VisaApplication/draft'); + Route::post('visa/submit', 'VisaApplication/submit'); + Route::get('visa/data-source', 'VisaApplication/dataSource'); })->namespace('app\\miniapi\\controller'); diff --git a/app/miniapi/service/VisaApplication/VisaApplicationService.php b/app/miniapi/service/VisaApplication/VisaApplicationService.php new file mode 100644 index 0000000..398fbce --- /dev/null +++ b/app/miniapi/service/VisaApplication/VisaApplicationService.php @@ -0,0 +1,54 @@ +request('options', $userId, ['order_source' => $orderSource]); } + public function start(int $userId, array $data, string $orderSource): array { return $this->request('start', $userId, array_merge($data, ['order_source' => $orderSource])); } + public function application(int $userId, string $publicId): array { return $this->request('application', $userId, ['public_id' => $publicId]); } + public function saveDraft(int $userId, array $data): array { return $this->request('saveDraft', $userId, $data); } + public function submit(int $userId, array $data): array { return self::decorateSubmitForCheck($this->request('submit', $userId, $data)); } + public static function decorateSubmitForCheck(array $result): array { if ((int)($result['code'] ?? 0) === 1 && !empty($result['data']['requires_payment']) && !empty($result['data']['order_sn'])) $result['data']['pay_url'] = 'https://www.qlevisa.com/index/pay/h5pay.html?order_sn=' . rawurlencode((string)$result['data']['order_sn']); return $result; } + public function payload(int $userId, array $params = []): array { $source = (string)($params['order_source'] ?? ''); unset($params['order_source']); return ['user_token' => authcode((string)json_encode(['purpose' => 'visa_api', 'user_id' => $userId, 'order_source' => $source], JSON_UNESCAPED_UNICODE), 'ENCODE', MINIAPI_TOKEN_KEY, 120)] + $params; } + public function endpoint(string $userUrl, string $action): string + { + if (!in_array($action, self::ACTIONS, true)) return ''; + $endpoint = preg_replace('#/users/[^/?]+(?:\?.*)?$#i', '/visa_application/' . $action, rtrim(trim($userUrl), '/')); + return is_string($endpoint) && $endpoint !== rtrim(trim($userUrl), '/') ? $endpoint : ''; + } + public function dataSource(string $type, array $filters): array + { + try { $query = self::dataSourceQueryForCheck($type, $filters); } catch (\RuntimeException $e) { return ['code' => 0, 'msg' => '数据源参数错误', 'data' => []]; } + $url = $this->endpoint((string)config('app.user_url'), 'dataSource'); $response = $url ? Httpcurl::request($url, 'post', http_build_query($query)) : false; + if (!is_array($response) || !isset($response[0]) || !empty($response[3])) { Log::warning('miniapi visa data source request failed'); return ['code' => 0, 'msg' => '数据源暂不可用', 'data' => []]; } + $result = json_decode($response[0], true); return is_array($result) ? $result : ['code' => 0, 'msg' => '数据源暂不可用', 'data' => []]; + } + public static function dataSourceQueryForCheck(string $type, array $filters): array + { + if (!preg_match('/^[A-Za-z0-9_.-]+$/', $type)) throw new \RuntimeException('invalid'); + $query = ['type' => $type]; + foreach (self::DATA_SOURCE_PARAMS as $key) { + $value = $filters[$key] ?? null; + if (!is_scalar($value) || $value === '') continue; + $value = (string)$value; + if (strlen($value) > 128 || ($key === 'model' && !preg_match('/^[A-Za-z0-9_.-]+$/', $value)) || ($key === 'source_code' && !preg_match('/^[A-Za-z0-9._-]{1,20}$/', $value)) || (in_array($key, ['page', 'size'], true) && !preg_match('/^[1-9][0-9]*$/', $value))) continue; + $query[$key] = $value; + } + return $query; + } + private function request(string $action, int $userId, array $params): array + { + $url = $this->endpoint((string)config('app.user_url'), $action); if ($url === '') return ['code' => 0, 'msg' => '签证服务接口地址异常', 'data' => []]; + $response = Httpcurl::request($url, 'post', http_build_query($this->payload($userId, $params))); + if (!is_array($response) || !isset($response[0]) || !empty($response[3])) { Log::warning('miniapi visa application request failed'); return ['code' => 0, 'msg' => '服务异常,请稍后重试', 'data' => []]; } + $result = json_decode($response[0], true); return is_array($result) ? $result : ['code' => 0, 'msg' => '服务异常,请稍后重试', 'data' => []]; + } +} diff --git a/route/app.php b/route/app.php deleted file mode 100644 index fbd46e4..0000000 --- a/route/app.php +++ /dev/null @@ -1,52 +0,0 @@ - -// +---------------------------------------------------------------------- - -use think\facade\Route; - -Route::group('miniapi', function () { - Route::get('index/home', 'Index/home'); - Route::get('index/about', 'Index/about'); - Route::get('index/legal', 'Index/legal'); - Route::get('index/packages', 'Index/packages'); - Route::get('index/countries', 'Index/countries'); - - Route::post('auth/sendSms', 'Auth/sendSms'); - Route::post('auth/login', 'Auth/login'); - Route::post('auth/wxPhoneLogin', 'Auth/wxPhoneLogin'); - Route::get('auth/me', 'Auth/me'); - - Route::get('news/lists', 'News/lists'); - Route::get('news/detail', 'News/detail'); - - Route::get('help/faqs', 'Help/faqs'); - - Route::get('order/lists', 'Order/lists'); - Route::get('order/latest-evus', 'Order/latestEvus'); - Route::get('order/detail', 'Order/detail'); - Route::post('order/pay', 'Order/pay'); - Route::post('order/addInvoice', 'Order/addInvoice'); - - Route::get('customer-center/applicants', 'CustomerCenter/applicants'); - Route::get('customer-center/applications', 'CustomerCenter/applications'); - Route::get('customer-center/orders', 'CustomerCenter/orders'); - Route::post('customer-center/applicant/save', 'CustomerCenter/saveApplicant'); - Route::post('customer-center/applicant/remove', 'CustomerCenter/removeApplicant'); - Route::post('customer-center/applicant', 'CustomerCenter/applicant')->completeMatch(); - - Route::get('ocr/signature', 'Ocr/signature'); - Route::post('upload/image', 'Upload/image'); - - Route::get('evus/correction', 'EvusCorrection/detail'); - Route::post('evus/correction/submit', 'EvusCorrection/submit'); - Route::post('evus/lookup', 'Evus/lookup'); - Route::post('evus/apply', 'Evus/apply'); - Route::post('esta/apply', 'Esta/apply'); -})->namespace('app\\miniapi\\controller'); diff --git a/tests/visa_application_facade_self_check.php b/tests/visa_application_facade_self_check.php new file mode 100644 index 0000000..867adef --- /dev/null +++ b/tests/visa_application_facade_self_check.php @@ -0,0 +1,23 @@ +endpoint('https://user.example.com/users/list', 'start') !== 'https://user.example.com/visa_application/start') throw new RuntimeException('endpoint construction failed'); +if ($service->endpoint('https://user.example.com/users/list', 'dataSource') !== 'https://user.example.com/visa_application/dataSource') throw new RuntimeException('data source forwarding endpoint failed'); +if ($service->endpoint('https://user.example.com/users/list', 'invalid') !== '') throw new RuntimeException('action allowlist failed'); +$payload = $service->payload(9, ['order_source' => 'jnwxmini']); +if (isset($payload['user_id']) || (json_decode((string)($payload['user_token'] ?? ''), true) !== ['purpose' => 'visa_api', 'user_id' => 9, 'order_source' => 'jnwxmini'])) throw new RuntimeException('trusted token payload failed'); +if (str_contains((new ReflectionMethod($service, 'start'))->getFileName() ? file_get_contents((new ReflectionMethod($service, 'start'))->getFileName()) : '', '$data + [\'order_source\'')) throw new RuntimeException('start source override failed'); +if (!str_contains((string)file_get_contents(dirname(__DIR__) . '/app/miniapi/controller/VisaApplication.php'), "sourceForCode(string \$code)") || !str_contains((string)file_get_contents(dirname(__DIR__) . '/app/miniapi/controller/VisaApplication.php'), "\$code === 'evus_bj_mp'") || !str_contains((string)file_get_contents(dirname(__DIR__) . '/app/miniapi/controller/VisaApplication.php'), "\$filters['source_code'] = \$this->orderSource()")) throw new RuntimeException('visa source allowlist failed'); +$neutral = VisaApplicationService::decorateSubmitForCheck(['code' => 1, 'data' => ['order_sn' => 'V1', 'requires_payment' => false]]); +$payable = VisaApplicationService::decorateSubmitForCheck(['code' => 1, 'data' => ['order_sn' => 'V 1', 'requires_payment' => true]]); +if (isset($neutral['data']['pay_url']) || ($payable['data']['pay_url'] ?? '') !== 'https://www.qlevisa.com/index/pay/h5pay.html?order_sn=V%201') throw new RuntimeException('miniapi payment decoration failed'); +if (VisaApplicationService::dataSourceQueryForCheck('inac.city', ['model' => 'region', 'source_code' => 'jnwxmini', 'state_id' => '11', 'hotel_state' => '12', 'provinceCode' => '13', 'country' => 'CN', 'application_token' => 'secret', 'user_token' => 'secret']) !== ['type' => 'inac.city', 'model' => 'region', 'source_code' => 'jnwxmini', 'state_id' => '11', 'hotel_state' => '12', 'provinceCode' => '13', 'country' => 'CN']) throw new RuntimeException('miniapi data source filtering failed'); +echo "PASS visa_application_facade_self_check\n";