This commit is contained in:
gaofeng
2026-06-17 09:23:18 +08:00
commit 81ade70944
1638 changed files with 213697 additions and 0 deletions

1
app/.htaccess Normal file
View File

@@ -0,0 +1 @@
deny from all

22
app/AppService.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
declare (strict_types = 1);
namespace app;
use think\Service;
/**
* 应用服务类
*/
class AppService extends Service
{
public function register()
{
// 服务注册
}
public function boot()
{
// 服务启动
}
}

94
app/BaseController.php Normal file
View File

@@ -0,0 +1,94 @@
<?php
declare (strict_types = 1);
namespace app;
use think\App;
use think\exception\ValidateException;
use think\Validate;
/**
* 控制器基础类
*/
abstract class BaseController
{
/**
* Request实例
* @var \think\Request
*/
protected $request;
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 是否批量验证
* @var bool
*/
protected $batchValidate = false;
/**
* 控制器中间件
* @var array
*/
protected $middleware = [];
/**
* 构造方法
* @access public
* @param App $app 应用对象
*/
public function __construct(App $app)
{
$this->app = $app;
$this->request = $this->app->request;
// 控制器初始化
$this->initialize();
}
// 初始化
protected function initialize()
{}
/**
* 验证数据
* @access protected
* @param array $data 数据
* @param string|array $validate 验证器名或者验证规则数组
* @param array $message 提示信息
* @param bool $batch 是否批量验证
* @return array|string|true
* @throws ValidateException
*/
protected function validate(array $data, string|array $validate, array $message = [], bool $batch = false)
{
if (is_array($validate)) {
$v = new Validate();
$v->rule($validate);
} else {
if (strpos($validate, '.')) {
// 支持场景
[$validate, $scene] = explode('.', $validate);
}
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
$v = new $class();
if (!empty($scene)) {
$v->scene($scene);
}
}
$v->message($message);
// 是否批量验证
if ($batch || $this->batchValidate) {
$v->batch(true);
}
return $v->failException(true)->check($data);
}
}

58
app/ExceptionHandle.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace app;
use think\db\exception\DataNotFoundException;
use think\db\exception\ModelNotFoundException;
use think\exception\Handle;
use think\exception\HttpException;
use think\exception\HttpResponseException;
use think\exception\ValidateException;
use think\Response;
use Throwable;
/**
* 应用异常处理类
*/
class ExceptionHandle extends Handle
{
/**
* 不需要记录信息(日志)的异常类列表
* @var array
*/
protected $ignoreReport = [
HttpException::class,
HttpResponseException::class,
ModelNotFoundException::class,
DataNotFoundException::class,
ValidateException::class,
];
/**
* 记录异常信息(包括日志或者其它方式记录)
*
* @access public
* @param Throwable $exception
* @return void
*/
public function report(Throwable $exception): void
{
// 使用内置的方式记录异常日志
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @access public
* @param \think\Request $request
* @param Throwable $e
* @return Response
*/
public function render($request, Throwable $e): Response
{
// 添加自定义异常处理机制
// 其他错误交给系统处理
return parent::render($request, $e);
}
}

8
app/Request.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
namespace app;
// 应用请求对象类
class Request extends \think\Request
{
}

2
app/common.php Normal file
View File

@@ -0,0 +1,2 @@
<?php
// 应用公共文件

17
app/event.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
// 事件定义文件
return [
'bind' => [
],
'listen' => [
'AppInit' => [],
'HttpRun' => [],
'HttpEnd' => [],
'LogLevel' => [],
'LogWrite' => [],
],
'subscribe' => [
],
];

1058
app/lang/en-us.php Normal file

File diff suppressed because it is too large Load Diff

911
app/lang/zh-cn.php Normal file
View File

@@ -0,0 +1,911 @@
<?php
return [
'site' => [
'title' => 'EVUS中文网_evus美国签证登记_evus登记步骤',
'description' => 'evus,evus登记步骤,evus系统登记入口,evus是什么,evus美国签证登记,evus官网中文',
'keywords' => 'EVUS中文网为您提供申请登记新的签证更新电子系统(EVUS)。提供evus官网查询、更新、evus系统登记入口、evus登记步骤等相关信息。',
'footer_1' => '声明:本网站是经工商注册、工信部备案的专业服务站点,本网站致力于协助个人或者企业获得他们得旅行许可或短时间的入境停留。登记不成功全额退费,登记成功服务费用不退。纠纷解决,适用本网站所属机构注册地仲裁委员会管辖',
'footer_2' => 'ITS INTERNATIONAL TRAVEL SERVICE COMPANY Copyright © 2014-2025 All rights reserved',
],
'menu_home' => '首页',
'menu_apply' => 'EVUS登记',
'menu_search' => '查询EVUS状态',
'menu_us_apply' => '美签申请',
'menu_esta_apply' => 'ESTA申请',
'menu_help' => '使用帮助',
'menu_user' => '个人中心',
'menu_news' => '新闻资讯',
'menu_company' => '关于我们',
'user_my_order' => '我的订单',
'user_invoice' => '自助开票',
'user_multiple_invoice' => '合并开票',
'user_logout' => '退出',
'order_paid' => '已支付',
'order_unpaid' => '未支付',
'index_title' => 'eVUS中文办理登记',
'index_desc' => '点击登记按钮,轻松完成办理',
'index_button' => 'eVUS登记',
'index_step1' => '填写旅行信息',
'index_step2' => '3-5分钟信息录入',
'index_step3' => '支付服务费',
'index_step4' => '3-15分钟内递交',
'index_step5' => '查询登记结果',
'index_attention_tips' => '请注意',
'index_attention_1' => '根据美国海关和边境保护局CBP规定自2025年9月30日起eVUS登记将收取30美元官方费用。',
'index_attention_2' => '本网站为独立运营的商务服务平台,凭借自主研发的智能系统,带来三大核心服务:智能填表系统可高效简化表单填写流程,精准 OCR 识别能快速提取图文信息,智能 AI 翻译则支持多语种精准转换。网站经相关机构注册、登记和备案后上线运营,专注为用户提供专业有偿服务,不隶属于任何政府部门。',
'index_attention_3' => '登记成功,所有费用不予退还;若登记未成功,官方登记费无法退回,但我们承诺全额退还服务费。',
'index_price_1_1' => 'eVUS快速登记',
'index_price_1_2' => '精准OCR识别',
'index_price_1_3' => '智能AI翻译',
'index_price_1_4' => '6小时内递交',
'index_price_1_5' => '包含30.75美金官方收费',
'index_price_1_6' => '6小时内递交',
'index_price_2_1' => 'eVUS专业登记',
'index_price_2_2' => '精准OCR识别',
'index_price_2_3' => '智能AI翻译',
'index_price_2_4' => '15分钟内递交',
'index_service' => '我们的服务',
'index_advantage' => '我们的优势',
'index_advantage_1_title' => '7×24小时急速响应',
'index_advantage_1_content_1' => '提供机场加急服务,专为应对各类临时出行需求而设,为赶时间的旅客开辟高效登记服务',
'index_advantage_1_content_2' => '旅客抵达机场后,无需繁琐排队,仅需 5-7 分钟即可递交',
'index_advantage_1_content_3' => '简化流程,助您快速通关,彻底规避误机风险,让紧急出行也能从容无忧',
'index_advantage_2_title' => '智能填表系统',
'index_advantage_2_content_1' => '我们推出简化中文表单填写服务全程仅需5分钟即可完成核心信息录入大幅降低语言壁垒带来的操作困扰',
'index_advantage_2_content_2' => '专业团队开展二次审核重点核查信息逻辑、格式规范及关键内容匹配度从源头减少登记失误显著降低因信息错漏导致的不成功风险较用户自行英文操作30-40分钟提速8倍',
'evus_pic' => '/new/assets/images/evus.jpg',
'evusearch_pic' => '/new/assets/images/evusearch.jpg',
'esta_pic' => '/new/assets/images/esta.jpg',
'evus' => [
'other_country_acquired_other_required' => '请填写其他',
'other_country_acquired_other_label' => '其他',
'add' => '添加',
'delete' => '删除',
'title' => '旅行证件信息',
'passport_number_label' => '护照号码',
'passport_placeholder' => '输入或拍照上传',
'passport_required' => '请填写护照号码',
'passport_upload_title' => '拍照/上传有效护照照片',
'passport_tooltip' => '护照号码通常为G、E、EA、EB、EC等开头的9位字符公务护照以及外交护照例外。请仔细区分数字1与字母I以及数字0与字母O。',
'passport_hint' => '依照您旅游时将使用的护照来输入护照号。',
'passport_country_label' => '护照国家',
'select_country_required' => '请选择一个国家',
'select_country_prompt' => '请选择一个国家',
'china' => '中国',
'country_tooltip' => '', // Can add tooltip text if needed
'country_hint' => '目前只有中华人民共和国的公民国民必须登记 EVUS。',
'pinyin' => '拼音',
'last_name_label' => '姓氏',
'last_name_placeholder' => '请填写拼音姓',
'last_name_required' => '请填写拼音姓',
'last_name_tooltip' => '请勿在此栏输入名字。复姓请按照护照信息为准。若您没有姓氏,请在姓氏栏位填 "UNKNOWN"',
'last_name_hint' => '请参考护照上的信息用英文字母填写您的姓氏',
'first_name_label' => '名字',
'first_name_placeholder' => '请填写拼音名',
'first_name_required' => '请填写拼音名',
'first_name_tooltip' => '请参考护照上的信息填写您的名字,请勿在此栏输入姓氏',
'first_name_hint' => '依照您的护照正确输入您的名字',
'birth_date_label' => '出生日期',
'birth_date_placeholder' => '请选择出生日期',
'birth_date_required' => '请选择出生日期',
'birth_date_tooltip' => '请参考您护照上的信息填写您的出生日期,请确保出生日期正确',
'birth_city_label' => '出生城市',
'birth_city_placeholder' => '请填写出生城市',
'birth_city_required' => '请填写出生城市',
'birth_city_tooltip' => '请参考您护照上的信息填写您的出生地点。',
'birth_country_label' => '出生国家',
'birth_country_tooltip' => '请从下拉式菜单选择您护照上出生地栏位显示的国家名称',
'country_required' => '请选择一个国家',
'gender_label' => '性别',
'male' => '男性',
'female' => '女性',
'passport_issue_date_label' => '护照签发时间',
'passport_issue_placeholder' => '请选择护照签发日期',
'passport_issue_required' => '请选择护照签发日期',
'passport_issue_tooltip' => '请参照护照上的签发日期填写',
'passport_expiry_label' => '护照有效期',
'passport_expiry_placeholder' => '请选择护照有效期',
'passport_expiry_required' => '请选择护照有效期',
'passport_expiry_tooltip' => '请参照护照上的有效期填写',
'ten_year_visa_label' => '您持有十年效期的美国访客签证吗B1、B2 或 B1/B2 类)?',
'ten_year_visa_hint' => '请确认是否持有美国十年效期商务/旅游B1、B2、B1/B2类型签证',
'ten_year_visa_warning' => '要在 EVUS 登记,您必须持有 10 年效期的美国访客签证B1、 B2 或 B1/B2 类)。若希望到美国旅游,但未持有这类的签证,您必须到美国大使馆申请',
'visa_number_label' => '美国B1/B2签证号码',
'visa_number_placeholder' => '输入或拍照上传',
'visa_number_required' => '请填写签证号码',
'visa_number_tooltip' => '请参考您的美国签证贴纸输入右下角8位红色字符的签证号码',
'upload_visa_title' => '拍照/上传有效护照照片',
'visa_in_passport_label' => '您旅行时所使用的护照是否含有您的美国签证?',
'visa_in_passport_hint' => '如果你的签证在旧护照上面,请选择"否",并填写旧护照信息',
'yes' => '是',
'no' => '否',
'old_passport_number_label' => '含美国签证的旧护照号码',
'enter_or_upload' => '输入或拍照上传',
'passport_number_required' => '请填写护照号码',
'upload_passport_photo' => '拍照/上传有效护照照片',
'old_passport_tooltip' => '请输入包含美国签证的旧护照号码',
'passport_country_tooltip' => '请选择旧护照的签发国家',
'next_step' => '下一步',
'other_citizenship_title' => '其他公民身份、国籍、护照',
'other_nationality_label' => '您现在是否拥有其他国籍?',
'other_nationality_tooltip' => '请确认是否拥有除中国以外的他国国籍',
'other_citizenship_current_label' => '其他国家公民当前',
'other_citizenship_current_tooltip' => '选择您的其他国籍的国家。如果您以上回答"是",该国家必须完成您的申请',
'how_acquired_citizenship_label' => '你如何从这个国家获得公民/国籍',
'how_acquired_citizenship_tooltip' => '选择你如何获得国籍/从这个国家的国籍',
'past_nationality_label' => '您曾经是否拥有其他国籍?',
'past_nationality_tooltip' => '在此问题上回答"是"代表着现在已无此国籍',
'past_citizenship_label' => '其他国家公民过去',
'past_citizenship_tooltip' => '请从下拉式菜单选择您护照上出生地栏位显示的国家名称',
'other_passport_label' => '您曾经是否拥有其他国家的护照或居民身份证?',
'other_passport_tooltip' => '请确认是否拥有除中国以外的他国护照。若您有此材料但已遗失或不记得号码,可填写"UNKNOWN"',
'issuing_country_label' => '发行国',
'issuing_country_tooltip' => '选择国家的公民身份,因为它出现在您的文档。完成应用程序需签发国的文档。',
'document_type_label' => '文档类型',
'document_type_tooltip' => '请选择另一个国家颁布的旅行文件类型',
'document_number_label' => '文档编号',
'document_number_placeholder' => '字母及数字(如无法提供请填写000000)',
'document_number_tooltip' => '输入您的旅行证件上显示的文件编号。旅行证件可能包含数字和/或字符。请仔细区分数字零和字母O.',
'expiry_year_label' => '到期年份',
'expiry_year_placeholder' => '请选择到期年份',
'expiry_year_tooltip' => '请即使它已过期,说明颁发的另一个国家,你的旅行证件到期年份。',
'social_media_label' => '是否有社交媒体?',
'social_media_tooltip' => '请选择是否有社交媒体',
'facebook_label' => '脸书(facebook)',
'facebook_placeholder' => '请填写脸书用户名',
'facebook_tooltip' => '如有请填写facebook账号名称。',
'linkedin_label' => '领英链接(linkedin)',
'linkedin_placeholder' => '请填写领英链接',
'linkedin_tooltip' => '如有请填写linkedin链接。',
'twitter_label' => '推特(Twitter)',
'twitter_placeholder' => '请填写推特用户名',
'twitter_tooltip' => '如有请填写Twitter账号名称。',
'instagram_label' => '照片墙(Instagram)',
'instagram_placeholder' => '请填写照片墙用户名',
'instagram_tooltip' => '如有,请填写照片墙用户名。',
'platform_label' => '提供者/平台',
'platform_tooltip' => '如有,请请选择提供者/平台。',
'identifier_label' => '社交媒体标识符',
'identifier_placeholder' => '请填写社交媒体标识符',
'identifier_tooltip' => '如有,请填写社交媒体标识符。',
'alias_label' => '是否有曾用名?',
'alias_tooltip' => '是否有曾用名',
'alias_last_name_label' => '曾用 姓氏',
'alias_last_name_placeholder' => '请填写曾用 姓氏',
'alias_last_name_tooltip' => '请填写您曾用名的姓氏',
'alias_last_name_hint' => '请填写您曾用名的姓氏,请勿在此栏输入名字。',
'alias_first_name_label' => '曾用 名字',
'alias_first_name_placeholder' => '请填写曾用 名字',
'alias_first_name_tooltip' => '请填写您曾用名的名字',
'alias_first_name_hint' => '请填写您曾用名的名字,请勿在此栏输入名字。',
'select_option_required' => '请选择',
'select_option_prompt' => '请选择',
'select_document_type_required' => '请选择文档类型',
'select_document_type_prompt' => '请选择文档类型',
'select_platform_required' => '请选择提供者/平台',
'select_platform_prompt' => '请选择提供者/平台',
'by_birth' => '经由出生',
'by_parents' => '通过父母',
'naturalization' => '归化',
'other' => '其他',
'passport_number' => '护照号',
'permanent_residence_id' => '永久居留证件号码(绿卡号)',
'address_contact_title' => '住址及联系方式',
'id_card_instructions' => '输入号码或点击【相机】识别您的有效<span style="color: #7F1313">身份证正面照</span>信息;拍照上传方式,可自动识别导入您的信息。',
'id_number_label' => '身份证号码',
'id_number_required' => '请填写身份证号码',
'upload_photo_title' => '拍照/上传有效照片',
'id_number_tooltip' => '请输入18位的身份证号码。如果没有身份证号码请填写UNKNOWN。',
'email_label' => '电子邮箱(选填)',
'email_placeholder' => '建议填写用于收入eVUS批准信',
'email_required' => '请填写电子邮箱',
'email_tooltip' => '您将通过邮件方式收到您的申请确认函。如有任何状态更新也将通过邮件通知您。',
'current_country_label' => '现居住国家',
'current_country_tooltip' => '请选择您当前居住的国家',
'current_province_label' => '现居住省份/州',
'current_province_placeholder' => '填写居住省份/州',
'current_province_required' => '填写居住省份/州',
'current_province_tooltip' => '请填写您当前居住的省份或州',
'current_city_label' => '现居住城市',
'current_city_placeholder' => '填写现居住城市',
'current_city_required' => '填写现居住城市',
'current_city_tooltip' => '请填写您当前居住的城市',
'current_address_label' => '现居住地址',
'current_address_placeholder' => '填写现居住地址',
'current_address_required' => '填写现居住地址',
'current_address_tooltip' => '请填写您当前居住的详细地址',
'phone_label' => '联系电话',
'phone_placeholder' => '请输入手机号码',
'phone_tooltip' => '请填写有效的联系电话号码',
'parents_info_title' => '父母信息',
'father_surname_label' => '父亲的姓氏',
'father_surname_placeholder' => '填写父亲的姓氏',
'father_surname_required' => '填写父亲的姓氏',
'father_surname_tooltip' => '请输入申请人父亲的姓氏',
'father_name_label' => '父亲的名字',
'father_name_placeholder' => '填写父亲的名字',
'father_name_required' => '填写父亲的名字',
'father_name_tooltip' => '请输入申请人父亲的名字',
'mother_surname_label' => '母亲的姓氏',
'mother_surname_placeholder' => '填写母亲的姓氏',
'mother_surname_required' => '填写母亲的姓氏',
'mother_surname_tooltip' => '请输入申请人母亲的姓氏',
'mother_name_label' => '母亲的名字',
'mother_name_placeholder' => '填写母亲的名字',
'mother_name_required' => '填写母亲的名字',
'mother_name_tooltip' => '请输入申请人母亲的名字',
'employment_info_title' => '工作信息',
'has_employer_label' => '您现在或之前是否有过工作单位或雇主',
'has_employer_tooltip' => '如填"是"请填写工作单位信息',
'employer_country_label' => '工作单位所在国家',
'employer_country_tooltip' => '请在列表中选择工作单位所在国家',
'employer_state_label' => '工作单位所在省份/州',
'employer_state_placeholder' => '请填写工作单位所在省份/州',
'employer_state_required' => '请填写工作单位所在省份/州',
'employer_state_tooltip' => '建议使用英文或拼音输入工作单位所在省份。若不清楚可填写"UNKNOWN"',
'employer_city_label' => '工作单位所在城市',
'employer_city_placeholder' => '请填写工作单位所在城市',
'employer_city_required' => '请填写工作单位所在城市,请勿填写省份',
'employer_city_tooltip' => '建议使用英文或拼音输入工作单位所在城市。若不清楚可填写"UNKNOWN"',
'employer_address_label' => '工作单位地址',
'employer_address_placeholder' => '请填写工作单位地址',
'employer_address_required' => '请填写工作单位地址',
'employer_address_tooltip' => '建议使用英文或拼音输入工作单位的地址,不必在此栏填写省份或城市。若不清楚可填写"UNKNOWN"',
'employer_name_label' => '工作单位名称',
'employer_name_placeholder' => '请填写工作单位名称',
'employer_name_required' => '填写工作单位名称限30字',
'employer_name_tooltip' => '请使用英文或拼音输入工作单位的名称。若没有工作单位,您也可以填写自雇、家庭主妇、等信息形容您的工作状况',
'travel_info_title' => '旅游信息',
'transit_purpose_label' => '您到美国的目的,是为了过境到另一国家吗?',
'transit_purpose_tooltip' => '请选择您前往美国是否只是为了过境而不停留。选否可选择填写在美联系人。',
'us_address_section_title' => '在美国的地址(选填)',
'us_contact_name_label' => '将在美国到访的酒店名称 / 商业上往来的友人 / 亲友全名(选填)',
'us_contact_name_placeholder' => '不得含有数字和特殊字符。如无法提供请留空',
'us_contact_name_required' => '不得含有数字和特殊字符。如无法提供请留空',
'us_contact_name_tooltip' => '在美国的联系人可以是友人、亲戚或生意上的伙伴。若您在美国没有联系人,请输入您住宿地点的名称、地址和电话号码(例如酒店名称)。您也可输入"UNKNOWN"。',
'us_state_label' => '所在州(选填)',
'select_state_required' => '请选择所在州',
'select_state_prompt' => '请选择所在州',
'us_state_tooltip' => '请在列表中选择您在美国的联系人所在的州。若不清楚可选择"未知"',
'us_city_label' => '所在城市(选填)',
'us_city_placeholder' => '填写联系人所在城市,不必填写省份',
'us_city_required' => '填写联系人所在城市,不必填写省份',
'us_city_tooltip' => '请输入您在美国的联系人所在城市。若不清楚可填写"UNKNOWN"。',
'us_address_label' => '地址(选填)',
'us_address_placeholder' => '填写联系人的地址街道及号码,不必填写省份或城市',
'us_address_required' => '填写联系人的地址街道及号码,不必填写省份或城市',
'us_address_tooltip' => '请输入您在美国的联系人的地址街道及号码,不必在此栏填写省份或城市。若不清楚可填写"UNKNOWN"。',
'us_phone_label' => '电话号码(选填)',
'select_phone_prefix_prompt' => '请选择手机号前缀',
'us_phone_placeholder' => '请输入手机号码',
'us_phone_tooltip' => '请输入您在美国的联系人的电话号码。若不清楚可填写"0"。',
'emergency_contact_title' => '紧急联系人(选填)',
'emergency_last_name_label' => '姓氏(选填)',
'emergency_last_name_placeholder' => '填写紧急联系人姓氏',
'emergency_last_name_required' => '填写紧急联系人姓氏',
'emergency_last_name_tooltip' => '请输入您的紧急联系人姓氏。紧急联系人可以是您在美国或其他国家的联系人,如家属、好友或商业伙伴。若没有紧急联系人,请留空',
'emergency_first_name_label' => '名字(选填)',
'emergency_first_name_placeholder' => '填写紧急联系人名字',
'emergency_first_name_required' => '填写紧急联系人名字',
'emergency_first_name_tooltip' => '请输入您的紧急联系人名字。紧急联系人名字可以是您在美国或其他国家的联系人,如家属、好友或商业伙伴。若没有紧急联系人,请留空',
'emergency_email_label' => '电子邮箱(选填)',
'emergency_email_placeholder' => '填写紧急联系人电子邮箱',
'emergency_email_required' => '填写紧急联系人电子邮箱',
'emergency_email_tooltip' => '请输入您的紧急联系人的电子邮箱。若没有紧急联系人,请留空',
'emergency_phone_label' => '电话号码(选填)',
'emergency_phone_placeholder' => '填写紧急联系人电话号码',
'emergency_phone_tooltip' => '请输入您的紧急联系人的电话号码。若没有紧急联系人请输入0',
'eligibility_question' => '资格问题',
'warning_cannot_continue' => '您确定要对此问题回答「是」吗?如果您的答案为「是」,您将不能继续在线申请。',
'q_a' => '您目前是否患有以下任何疾病《公共健康法》第361条b款所列传染病霍乱、白喉、肺结核、天花、黄热病、病毒性出血热包括埃博拉、拉沙热、马尔堡病、克里米亚-刚果出血热)、可传染他人且可能致命的严重急性呼吸道疾病?',
'q_b' => '您是否曾因严重破坏财产或严重伤害他人或政府机关而被逮捕或定罪?',
'q_c' => '您是否曾违反过与非法药物的持有、使用或分发相关的法律?',
'q_d' => '您是否曾经或试图从事恐怖活动、间谍活动、破坏活动或种族灭绝?',
'q_e' => '您是否曾为获取签证或协助他人获取签证、或进入美国而欺诈或伪造自己或他人的信息?',
'q_f' => '您目前是否在美国寻求工作,或是否曾在未获许可的情况下在美国工作?',
'q_g' => '您是否曾被拒签美国签证或被拒绝入境美国,或在入境口岸撤回过申请?',
'q_h' => '您是否曾逾期居留美国或违反过美国签证的规定?',
'q_i' => '自2011年3月1日以来您是否曾去过或居住在伊朗、伊拉克、利比亚、朝鲜、索马里、苏丹、叙利亚或也门',
'reject_date' => '拒签日期',
'reject_address' => '拒签地点',
'vist_country' => '访问国家',
'vist_from_date' => '开始日期',
'vist_end_date' => '结束日期',
'vist_country_des' => '访问主要原因',
'IRN' => '伊朗',
'IRQ' => '伊拉克',
'LBY' => '利比亚',
'PRK' => '朝鲜',
'SOM' => '索马里',
'SDN' => '苏丹',
'SYR' => '叙利亚',
'YEM' => '也门',
'reason_1' => '旅游',
'reason_2' => '探亲',
'reason_3' => '工作',
'reason_4' => '商务',
'reason_5' => '学习',
'reason_6' => '新闻报道',
'reason_7' => '会议',
'reason_8' => '文化交流',
'reason_9' => '宗教活动',
'reason_10' => '医疗',
'reason_11' => '过境',
'reason_12' => '外交/公务',
'reason_13' => '其他',
'declaration_title' => '申请人声明',
'declaration_check1' => '本人声明在此申请中提供真实丶完整和正确的信息。',
'declaration_check2' => '我已阅读并理解',
'choose_package' => '选择套餐',
'package_a' => 'A套餐:398元/人民币 Vip快速登记(资料完整15分钟反馈信息)',
'package_b' => 'B套餐:98元/人民币 安逸登记(资料完整6小时内反馈信息)',
'submit_now' => '立即提交',
'termsCheckbox' => '
<div style="padding: 20px;">
<h5>条款与条件</h5>
<p>
签证更新电子系统EVUS与执法部门 的数据库进行比对。所有持10年有效期访客签证的旅客B1, B2 或 B1/B2,
如果需要以短期访客身份入境美国都必须在赴美前在EVUS上登记。
如果您的EVUS登记获得批准就意味着您可以前往美国。但这并不意味着您可以入境美国。抵达美国入境口岸时您将接受United States
Customs and Border Protection官员的检查。 入境口岸的官员会根据美国移民法的规定决定您是否可以入境。
由您或指定的第三方代理人所提供的所有信息必须是真实和正确的。如果您在填写信息时由您或您的代理人有意提交任何虚假、虚构或伪造的声明或陈述造成EVUS更新失败或者美国签证被注销责任由您本人承担。
信息填写完成后,我们根据你所选择的套餐时效反馈信息,提交后信息将不能再更改。
</p>
<p> 请表明您已阅读并了解上述所提供的信息</p>
<h5>隐私政策</h5>
<p>
<p>
本应用尊重并保护所有使用服务用户的个人隐私权。为了给您提供更准确、更有个性化的服务,本应用会按照本隐私权政策的规定使用和披露您的个人信息。但本应用将以高度的勤勉、审慎义务对待这些信息。除本隐私权政策另有规定外,在未征得您事先许可的情况下,本应用不会将这些信息对外披露或向第三方提供。本应用会不时更新本隐私权政策。
您在同意本应用服务使用协议之时,即视为您已经同意本隐私权政策全部内容。本隐私权政策属于本应用服务使用协议不可分割的一部分。
<h5>1. 适用范围</h5>
<p>(a) 在您注册本应用帐号时,您根据本应用要求提供的个人注册信息;
(b)
在您使用本应用网络服务或访问本应用平台网页时本应用自动接收并记录的您的浏览器和计算机上的信息包括但不限于您的IP地址、浏览器的类型、使用的语言、访问日期和时间、软硬件特征信息及您需求的网页记录等数据
(c) 本应用通过合法途径从商业伙伴处取得的用户个人数据。
您了解并同意,以下信息不适用本隐私权政策:
(a) 您在使用本应用平台提供的搜索服务时输入的关键字信息;
(b) 本应用收集到的您在本应用发布的有关信息数据,包括但不限于参与活动、成交信息及评价详情;
(c) 违反法律规定或违反本应用规则行为及本应用已对您采取的措施。</p>
<h5>2. 信息使用</h5>
<p>
(a)本应用不会向任何无关第三方提供、出售、出租、分享或交易您的个人信息,除非事先得到您的许可,或该第三方和本应用(含本应用关联公司)单独或共同为您提供服务,且在该服务结束后,其将被禁止访问包括其以前能够访问的所有这些资料。
(b)
本应用亦不允许任何第三方以任何手段收集、编辑、出售或者无偿传播您的个人信息。任何本应用平台用户如从事上述活动,一经发现,本应用有权立即终止与该用户的服务协议。
(c)
为服务用户的目的,本应用可能通过使用您的个人信息,向您提供您感兴趣的信息,包括但不限于向您发出产品和服务信息,或者与本应用合作伙伴共享信息以便他们向您发送有关其产品和服务的信息(后者需要您的事先同意)</p>
<h5>3. 信息披露</h5>
<p>在如下情况下,本应用将依据您的个人意愿或法律的规定全部或部分的披露您的个人信息:
(a) 经您事先同意,向第三方披露;
(b)为提供您所要求的产品和服务,而必须和第三方分享您的个人信息;
(c) 根据法律的有关规定,或者行政或司法机构的要求,向第三方或者行政、司法机构披露;
(d) 如您出现违反中国有关法律、法规或者本应用服务协议或相关规则的情况,需要向第三方披露;
(e) 如您是适格的知识产权投诉人并已提起投诉,应被投诉人要求,向被投诉人披露,以便双方处理可能的权利纠纷;
(f)
在本应用平台上创建的某一交易中,如交易任何一方履行或部分履行了交易义务并提出信息披露请求的,本应用有权决定向该用户提供其交易对方的联络方式等必要信息,以促成交易的完成或纠纷的解决。
(g) 其它本应用根据法律、法规或者网站政策认为合适的披露。</p>
<h5>4. 信息存储和交换</h5>
<p>
本应用收集的有关您的信息和资料将保存在本应用及(或)其关联公司的服务器上,这些信息和资料可能传送至您所在国家、地区或本应用收集信息和资料所在地的境外并在境外被访问、存储和展示。</p>
<h5>5. Cookie的使用</h5>
<p>(a) 在您未拒绝接受cookies的情况下本应用会在您的计算机上设定或取用cookies
以便您能登录或使用依赖于cookies的本应用平台服务或功能。本应用使用cookies可为您提供更加周到的个性化服务包括推广服务。
(b)
您有权选择接受或拒绝接受cookies。您可以通过修改浏览器设置的方式拒绝接受cookies。但如果您选择拒绝接受cookies则您可能无法登录或使用依赖于cookies的本应用网络服务或功能。
(c) 通过本应用所设cookies所取得的有关信息将适用本政策。</p>
<h5>6. 信息安全</h5>
<p>(a)
本应用帐号均有安全保护功能,请妥善保管您的用户名及密码信息。本应用将通过对用户密码进行加密等安全措施确保您的信息不丢失,不被滥用和变造。尽管有前述安全措施,但同时也请您注意在信息网络上不存在“完善的安全措施”。
(b) 在使用本应用网络服务进行网上交易时,您不可避免的要向交易对方或潜在的交易对</p>
<h5>7.本隐私政策的更改</h5>
<p>
(a)如果决定更改隐私政策,我们会在本政策中、本公司网站中以及我们认为适当的位置发布这些更改,以便您了解我们如何收集、使用您的个人信息,哪些人可以访问这些信息,以及在什么情况下我们会透露这些信息。
(b)本公司保留随时修改本政策的权利,因此请经常查看。如对本政策作出重大更改,本公司会通过网站通知的形式告知。
方披露自己的个人信息,如联络方式或者邮政地址。请您妥善保护自己的个人信息,仅在必要的情形下向他人提供。如您发现自己的个人信息泄密,尤其是本应用用户名及密码发生泄露,请您立即联络本应用客服,以便本应用采取相应措施。</p>
</div>
',
'terms_privacy_title' => '条款与条件及隐私政策',
'close' => '关闭'
],
'lookup' => [
'hot_price' => '热门套餐',
'status_check_title' => '查询EVUS状态',
'passport_number_label' => '护照号码',
'enter_or_upload' => '输入或拍照上传',
'passport_number_required' => '请填写护照号码',
'upload_passport_photo' => '拍照/上传有效护照照片',
'passport_number_tooltip' => '护照号码通常为G、E、EA、EB、EC等开头的9位字符公务护照以及外交护照例外。请仔细区分数字1与字母I以及数字0与字母O。',
'visa_number_label' => '美国B1/B2签证号码',
'visa_number_required' => '请填写签证号码',
'visa_number_tooltip' => '请参考您的美国签证贴纸输入右下角8位红色字符的签证号码。',
'last_name_label' => '姓氏',
'pinyin' => '拼音',
'last_name_placeholder' => '请填写拼音姓',
'last_name_required' => '请填写拼音姓',
'last_name_tooltip' => '请勿在此栏输入名字。复姓请按照护照信息为准。若您没有姓氏,请在姓氏栏位填 "UNKNOWN"。',
'last_name_hint' => '请参考护照上的信息用英文字母填写您的姓氏',
'first_name_label' => '名字',
'first_name_placeholder' => '请填写拼音名',
'first_name_required' => '请填写拼音名',
'first_name_tooltip' => '请参考护照上的信息填写您的名字,请勿在此栏输入姓氏。',
'first_name_hint' => '依照您的护照正确输入您的名字',
'birth_date_label' => '出生日期',
'birth_date_placeholder' => '请选择出生日期',
'birth_date_required' => '请选择出生日期',
'birth_date_tooltip' => '请参考您护照上的信息填写您的出生日期,请确保出生日期正确',
'citizenship_label' => '公民身份国家',
'select_country_required' => '请选择国家',
'select_country_prompt' => '请选择国家',
'citizenship_tooltip' => '请从下拉式菜单选择您护照上出生地栏位显示的国家名称。',
'terms_acceptance' => '我已阅读并理解条款与条件及隐私政策。',
'check_status_button' => '查询状态',
'status_registered' => '已登记',
'registered_description1' => '“已登记”的EVUS状态意味着EVUS的要求得到了满足得以前往美国 1从登记批准之日起两年内有效期或 2直到主要的护照到期“主要的护照”是用于登记的护照是有效的并在登记申请时尚未过期或 3直到签证过期以先到者为准。这样并不保证入境美国。美国联邦Customs and Border Protection 的官员在美国的入境口岸将做最终决定。',
'registered_description2' => '要进入您的EVUS登记申请会要求您提供您的登记申请号码护照号码签证号码和出生日期。如果有必要您可以更新一份成功的登记内以下信息1在美地址和 2电子邮件地址。如果您需要更改或更新表格上的任何其他信息您必须申请新的旅行授权。',
'registered_description3' => '您已成功提交了以下所列',
'status_pending' => '审批中',
'pending_description' => '您的信息正在登记中,请耐心等候!',
'status_unsuccessful' => '登记不成功',
'unsuccessful_description1' => '您的EVUS登记不成功并且您目前没有得到授权前往美国。',
'unsuccessful_description2' => 'EVUS登记申请失败的常见原因包括您没有完成所有必填的资料; 您没有提供正确的信息; 您没有用有效的护照或美国签证申请登记,或者其他原因。',
'unsuccessful_description3' => '如需帮助请拨打EVUS帮助中心400-0878-958 。帮助中心每天24小時每周7天提供以帮助您与您的EVUS招生。',
'unsuccessful_description4' => '您已成功提交了以下所列的登记申请。为您的个人记录,请印列此页。',
'table_name' => '姓名',
'table_birth_date' => '出生日期',
'table_enrollment_number' => '登记申请号码',
'table_passport_number' => '护照号码',
'table_visa_number' => 'B1/B2签证的签证号码',
'table_status' => '状态',
'table_expiry' => '到期',
'table_view' => '查看',
'visa_services' => '美国签证服务',
'package1_title' => '套餐一',
'package1_price' => '¥198',
'package1_feature1' => 'DS-160 英文翻译填写',
'package2_title' => '套餐二',
'package2_price' => '¥1998',
'package2_feature1' => 'DS-160 英文翻译填写',
'package2_feature2' => '即时预约面签',
'package2_feature3' => '包含签证费用',
'package3_title' => '套餐三(限时特惠)',
'package3_price' => '¥2098<s style="font-size: 14px">¥2250</s>',
'package3_feature1' => 'DS-160 英文翻译填写',
'package3_feature2' => '即时预约',
'package3_feature3' => '面签辅导',
'package3_feature4' => '包含签证费用',
'package4_title' => '套餐四',
'package4_price' => '¥2099',
'package4_feature1' => '免面谈代传递(签证续签)',
'package4_feature2' => 'DS-160 英文翻译填写',
'package4_feature3' => '即时预约代传递',
'package4_feature4' => '包含签证费用',
'package4_feature5' => '(赴美目的说明)',
],
'esta' => [
'notice' => '注意您正在申请ESTA如果您拥有中国护照的请点击申请',
'personal_details_title' => '个人详细信息',
'passport_photo_label' => '护照照片',
'upload_instructions' => '点击上传,或将文件拖拽到此处',
'upload_success_alt' => '上传成功后渲染',
'passport_photo_required' => '请上传护照照片',
'passport_example_label' => '护照示例',
'personal_photo_label' => '个人照片',
'personal_photo_required' => '请上传个人照片',
'personal_photo_example_label' => '个人照示例',
'last_name_label' => '姓氏',
'pinyin' => '拼音',
'last_name_placeholder' => '请填写拼音姓',
'last_name_required' => '请填写拼音姓',
'last_name_tooltip' => '请勿在此栏输入名字。复姓请按照护照信息为准。若您没有姓氏,请在姓氏栏位填 "UNKNOWN"。',
'last_name_hint' => '请参考护照上的信息用英文字母填写您的姓氏',
'first_name_label' => '名字',
'first_name_placeholder' => '请填写拼音名',
'first_name_required' => '请填写拼音名',
'first_name_tooltip' => '请参考护照上的信息填写您的名字,请勿在此栏输入姓氏。',
'first_name_hint' => '依照您的护照正确输入您的名字',
'birth_date_label' => '出生日期',
'birth_date_placeholder' => '请选择出生日期',
'birth_date_required' => '请选择出生日期',
'birth_date_tooltip' => '请参考您护照上的信息填写您的出生日期,请确保出生日期正确',
'birth_city_label' => '出生城市',
'birth_city_placeholder' => '请填写出生城市',
'birth_city_required' => '请填写出生城市',
'birth_city_tooltip' => '请参考您护照上的信息填写您的出生地点。',
'birth_country_label' => '出生国家',
'select_country_required' => '请选择一个国家',
'select_country_prompt' => '请选择一个国家',
'birth_country_tooltip' => '请从下拉式菜单选择您护照上出生地栏位显示的国家名称。',
'gender_label' => '性别',
'male' => '男性',
'female' => '女性',
'id_number_label' => 'ID证件号码',
'id_number_placeholder' => '请填写ID证件号码',
'id_number_required' => '请填写ID证件号码',
'id_number_tooltip' => '请输入身份证号码。如果没有身份证号码,请填写"UNKNOWN"',
'former_name_label' => '曾用名(选填)',
'former_name_placeholder' => '请填写曾用名',
'former_name_required' => '请填写曾用名',
'next_step' => '下一步',
'travel_documents_title' => '旅行文件详细信息',
'passport_country_label' => '护照国家',
'passport_number_label' => '护照号码',
'passport_number_placeholder' => '请填写护照号码',
'passport_number_required' => '请填写护照号码',
'passport_number_tooltip' => '护照号码通常为G、E、EA、EB、EC等开头的9位字符公务护照以及外交护照例外。请仔细区分数字1与字母I以及数字0与字母O。',
'passport_issue_date_label' => '护照签发时间',
'passport_issue_date_placeholder' => '请选择护照签发日期',
'passport_issue_date_required' => '请选择护照签发日期',
'passport_issue_date_tooltip' => '请参照护照上的签发日期填写',
'passport_expiry_label' => '护照有效期',
'passport_expiry_placeholder' => '请选择护照有效期',
'passport_expiry_required' => '请选择护照有效期',
'passport_expiry_tooltip' => '请参照护照上的有效期填写',
'visa_number_label' => '填写签证号码:(如果有)',
'visa_number_placeholder' => '请填写签证号码',
'visa_number_required' => '请填写签证号码',
'other_citizenship_title' => '其他公民身份、国籍、护照',
'current_other_nationality_label' => '您现在是否拥有其他国籍?',
'current_other_nationality_tooltip' => '请确认是否拥有除中国以外的他国国籍',
'other_citizenship_current_label' => '其他国家公民当前',
'other_citizenship_current_tooltip' => '选择您的其他国籍的国家。如果您以上回答"是",该国家必须完成您的申请',
'how_acquired_citizenship_label' => '你如何从这个国家获得公民/国籍',
'how_acquired_citizenship_tooltip' => '选择你如何获得国籍/从这个国家的国籍',
'past_other_nationality_label' => '您曾经是否拥有其他国籍?',
'past_other_nationality_tooltip' => '在此问题上回答"是"代表着现在已无此国籍',
'past_nationality_start_date_label' => '曾经拥有其他国籍的开始日期',
'past_nationality_end_date_label' => '曾经拥有其他国籍的结束日期',
'past_citizenship_label' => '其他国家公民过去',
'past_citizenship_tooltip' => '请从下拉式菜单选择您护照上出生地栏位显示的国家名称',
'other_passport_label' => '您曾经是否拥有其他国家的护照或居民身份证?',
'other_passport_tooltip' => '请确认是否拥有除中国以外的他国护照。若您有此材料但已遗失或不记得号码,可填写"UNKNOWN"',
'issuing_country_label' => '发行国',
'issuing_country_tooltip' => '选择国家的公民身份,因为它出现在您的文档。完成应用程序需签发国的文档',
'document_type_label' => '文档类型',
'document_type_tooltip' => '请选择另一个国家颁布的旅行文件类型',
'document_number_label' => '文档编号(选填)',
'document_number_placeholder' => '字母及数字(如无法提供请填写000000)',
'document_number_required' => '请填写文档编号',
'document_number_tooltip' => '输入您的旅行证件上显示的文件编号。旅行证件可能包含数字和/或字符。请仔细区分数字零和字母O',
'expiry_year_label' => '到期年份',
'expiry_year_placeholder' => '请选择到期年份',
'expiry_year_required' => '请选择到期年份',
'expiry_year_tooltip' => '请即使它已过期,说明颁发的另一个国家,你的旅行证件到期年份',
'select_option_required' => '请选择',
'select_option_prompt' => '请选择',
'select_document_type_required' => '请选择文档类型',
'select_document_type_prompt' => '请选择文档类型',
'select_date_required' => '请选择日期',
'select_date_prompt' => '请选择日期',
'passport_number' => '护照号',
'national_id' => '身份证号码',
'by_birth' => '经由出生',
'by_parents' => '通过父母',
'naturalization' => '归化',
'other' => '其他',
'yes' => '是',
'no' => '否',
'contact_information_title' => '联系方式',
'email_label' => '电子邮箱(选填)',
'email_placeholder' => '建议填写用于接收ESTA批准信',
'email_required' => '请填写电子邮箱',
'email_tooltip' => '您将通过邮件方式收到您的申请确认函。如有任何状态更新也将通过邮件通知您。',
'confirm_email_label' => '确认电子邮箱(选填)',
'confirm_email_placeholder' => '建议填写用于接收ESTA批准信',
'confirm_email_required' => '请填写电子邮箱',
'confirm_email_tooltip' => '您将通过邮件方式收到您的申请确认函。如有任何状态更新也将通过邮件通知您。',
'phone_number_label' => '联系电话',
'phone_number_placeholder' => '请输入手机号码',
'select_phone_prefix_prompt' => '请选择手机号前缀',
'address_details_title' => '详细地址',
'current_country_label' => '现居住国家',
'current_province_label' => '现居住省份/州',
'current_province_placeholder' => '填写居住省份/州',
'current_province_required' => '填写居住省份/州',
'current_city_label' => '现居住城市',
'current_city_placeholder' => '填写现居住城市',
'current_city_required' => '填写现居住城市',
'current_address_label' => '现居住地址',
'current_address_placeholder' => '填写现居住地址',
'current_address_required' => '填写现居住地址',
'parents_info_title' => '父母信息',
'father_surname_label' => '父亲的姓氏',
'father_surname_placeholder' => '填写父亲的姓氏',
'father_surname_required' => '填写父亲的姓氏',
'father_surname_tooltip' => '请输入申请人父亲的姓氏',
'father_name_label' => '父亲的名字',
'father_name_placeholder' => '填写父亲的名字',
'father_name_required' => '填写父亲的名字',
'father_name_tooltip' => '请输入申请人父亲的名字',
'mother_surname_label' => '母亲的姓氏',
'mother_surname_placeholder' => '填写母亲的姓氏',
'mother_surname_required' => '填写母亲的姓氏',
'mother_surname_tooltip' => '请输入申请人母亲的姓氏',
'mother_name_label' => '母亲的名字',
'mother_name_placeholder' => '填写母亲的名字',
'mother_name_required' => '填写母亲的名字',
'mother_name_tooltip' => '请输入申请人母亲的名字',
'employment_info_title' => '工作信息',
'has_employer_label' => '您现在或之前是否有过工作单位或雇主',
'has_employer_tooltip' => '如填"是"请填写工作单位信息',
'employer_country_label' => '工作单位所在国家',
'employer_country_tooltip' => '请在列表中选择工作单位所在国家',
'employer_state_label' => '工作单位所在省份/州',
'employer_state_placeholder' => '填写居住省份/州',
'employer_state_required' => '填写居住省份/州',
'employer_state_tooltip' => '建议使用英文或拼音输入工作单位所在省份。若不清楚可填写"UNKNOWN"',
'employer_city_label' => '工作单位所在城市',
'employer_city_placeholder' => '填写现居住城市',
'employer_city_required' => '填写现居住城市,请勿填写省份',
'employer_city_tooltip' => '建议使用英文或拼音输入工作单位所在城市。若不清楚可填写"UNKNOWN"',
'employer_address_label' => '工作单位地址',
'employer_address_placeholder' => '填写现居住地址',
'employer_address_required' => '填写现居住地址',
'employer_address_tooltip' => '建议使用英文或拼音输入工作单位的地址,不必在此栏填写省份或城市。若不清楚可填写"UNKNOWN"',
'employer_name_label' => '工作单位名称',
'employer_name_placeholder' => '填写工作单位名称',
'employer_name_required' => '填写工作单位名称限30字',
'employer_name_tooltip' => '请使用英文或拼音输入工作单位的名称。若没有工作单位,您也可以填写自雇、家庭主妇、等信息形容您的工作状况',
'travel_info_title' => '旅游信息',
'transit_purpose_label' => '您到美国的目的,是为了过境到另一国家吗?',
'transit_purpose_tooltip' => '请选择您前往美国是否只是为了过境而不停留。选否可选择填写在美联系人',
'us_address_section_title' => '在美国的地址(选填)',
'us_contact_name_label' => '将在美国到访的酒店名称 / 商业上往来的友人 / 亲友全名(选填)',
'us_contact_name_placeholder' => '不得含有数字和特殊字符。如无法提供请留空',
'us_contact_name_required' => '不得含有数字和特殊字符。如无法提供请留空',
'us_contact_name_tooltip' => '在美国的联系人可以是友人、亲戚或生意上的伙伴。若您在美国没有联系人,请输入您住宿地点的名称、地址和电话号码(例如酒店名称)。您也可输入"UNKNOWN"',
'us_state_label' => '所在州(选填)',
'select_state_required' => '请选择所在州',
'select_state_prompt' => '请选择所在州',
'us_state_tooltip' => '请在列表中选择您在美国的联系人所在的州。若不清楚可选择"未知"',
'us_city_label' => '所在城市(选填)',
'us_city_placeholder' => '填写联系人所在城市,不必填写省份',
'us_city_required' => '填写联系人所在城市,不必填写省份',
'us_city_tooltip' => '请输入您在美国的联系人所在城市。若不清楚可填写"UNKNOWN"',
'us_address_label' => '地址(选填)',
'us_address_placeholder' => '填写联系人的地址街道及号码,不必填写省份或城市',
'us_address_required' => '填写联系人的地址街道及号码,不必填写省份或城市',
'us_address_tooltip' => '请输入您在美国的联系人的地址街道及号码,不必在此栏填写省份或城市。若不清楚可填写"UNKNOWN"',
'us_phone_label' => '电话号码(选填)',
'us_phone_placeholder' => '请输入手机号码',
'us_phone_tooltip' => '请输入您在美国的联系人的电话号码。若不清楚可填写"0"',
'emergency_contact_title' => '紧急联系人(选填)',
'emergency_last_name_label' => '姓氏(选填)',
'emergency_last_name_placeholder' => '填写紧急联系人姓氏',
'emergency_last_name_required' => '填写紧急联系人姓氏',
'emergency_last_name_tooltip' => '请输入您的紧急联系人姓氏。紧急联系人可以是您在美国或其他国家的联系人,如家属、好友或商业伙伴。若没有紧急联系人,请留空',
'emergency_first_name_label' => '名字(选填)',
'emergency_first_name_placeholder' => '填写紧急联系人名字',
'emergency_first_name_required' => '填写紧急联系人名字',
'emergency_first_name_tooltip' => '请输入您的紧急联系人名字。紧急联系人名字可以是您在美国或其他国家的联系人,如家属、好友或商业伙伴。若没有紧急联系人,请留空',
'emergency_email_label' => '电子邮箱(选填)',
'emergency_email_placeholder' => '填写紧急联系人电子邮箱',
'emergency_email_required' => '填写紧急联系人电子邮箱',
'emergency_email_tooltip' => '请输入您的紧急联系人的电子邮箱。若没有紧急联系人,请留空',
'emergency_phone_label' => '电话号码(选填)',
'emergency_phone_placeholder' => '填写紧急联系人电话号码',
'emergency_phone_tooltip' => '请输入您的紧急联系人的电话号码。若没有紧急联系人请输入0',
'applicant_declaration_title' => '申请人声明',
'declaration_statement' => '本人声明在此申请中提供真实、完整和正确的信息。',
'terms_acceptance' => '我已阅读并理解条款与条件及隐私政策。',
'select_package_title' => '选择套餐',
'package_a_option' => 'A套餐:958元/人民币 Vip快速登记(资料完整6小时内通过)',
'package_b_option' => 'B套餐:495元/人民币 安逸登记(资料完整48小时内通过)',
'submit_application_button' => '立即提交',
'eligibility_questions_title' => '资格问题',
'question_a_label' => '您目前是否患有任何下列疾病(《公共健康法案》第 361 号 b 项)?霍乱、白喉、肺结核、结核、天花、黄热病、病毒性出血热,包括埃博拉病毒、拉萨热、马尔堡病、克里米亚-刚果出血热、可传染他人且可能致死的严重急性呼吸道疾病',
'question_b_label' => '您是否曾因严重破坏财产或伤及他人或政府机关,而被逮或被判罪?',
'question_c_label' => '您是否曾违反任何涉及持有、使用或分发非法药物的法律?',
'question_d_label' => '您是否试图或曾经从事恐怖活动、间谍、破坏活动或种族灭绝?',
'question_e_label' => '您是否曾为了取得签证、协助他人取得签证、或入境美国,犯下欺诈或伪造个人或他人的资料?',
'question_f_label' => '您目前是否在美国寻求工作或是您是否曾未经美国政府许可在美国工作?',
'question_g_label' => '您是否曾使用目前或先前的护照申请美国签证被拒?或者您是否曾被拒绝入境美国、或在美国入境口岸时,申请遭到驳回?',
'question_h_label' => '您是否曾在美国停留超过美国政府允许的入境期间?',
'question_i_label' => '您是否曾在 2011 年三月 1 日当日或其后到过伊拉克、叙利亚、伊朗或苏丹',
'eligibility_warning' => '您确定要对此问题回答「是」吗?如果您的答案为「是」,您就不能继续在线申请了。',
],
'pay' => [
'success_message' => '订单提交成功,请尽快付款,订单号',
'payment_amount' => '应付金额',
'order_success_message' => '您的订单已经提交成功,订单号为:',
'notice_1' => '1、您填写的信息必须和您的真实信息一致如提交的信息与事实不符合由此造成的后果本中心概不负责。',
'notice_3' => '2、工作时间周一至周日09:00 - 22:00 (节假日正常工作),在非工作时间段提交的申请表,系统自动延伸到下一工作日,敬请谅解。',
'notice_4' => '3、提交后我们会在规定的时间内翻译审核校对资料信息请注意查收邮件如收件箱里没有则很可能被隔离到了垃圾箱里。',
],
'jump' => [
'auto_redirect' => '秒后自动跳转',
'redirect_now' => '立即跳转'
],
'js' => [
'daxie' => '请填写大写字母',
'zhongwen' => '请填写填写中文',
'len40' => '请控制在100字以内',
'required_field' => '此项为必填项',
'min_length' => '至少需要%d个字符',
'max_length' => '最多允许%d个字符',
'ppt_min' => '至少需要9个字符',
'ppt_max' => '最多允许9个字符',
'ppt_format' => '护照格式应为字母开头 + 至少6位数字或字母',
'visa_min' => '至少需要8个字符',
'visa_max' => '最多允许8个字符',
'visa_format' => '签证格式不正确',
'default_reqtext' => '必填项不能为空',
'checkbox_checked' => '必须选择一个选项',
'checkbox_required' => '请勾选声明',
'english_only' => '只能输入英文字母',
'success_message' => '上传成功',
'fail_message' => '上传失败:',
'error_message' => '上传出错',
'upload_image_only' => '文件必须为图片',
'browser_not_supported' => '你的浏览器不支持上传',
'visa_unrecognized' => '无法识别,请手动填写签证号',
'manual_entry_required' => '无法识别,请手动填写',
'upload_retry' => '请重新上传',
'status_enrolled' => '已登记',
'status_pending' => '审批中',
'status_unsuccessful' => '登记不成功',
'status_revoked' => 'DOS已撤销您的签证',
'click_to_view' => '点击查看'
],
'controller' => [
'success' => '操作成功',
'fail' => '操作失败',
'price_error' => '价格错误',
'server_error' => '服务器错误',
'no_registration_info' => '未检索到登记信息',
'registration_expired' => '您正在搜索的登记申请已过期或取消',
'registration_success' => '登记成功',
'registration_failed' => '登记不成功',
'registration_pending' => '登记中',
'visa_revoked' => '已撤销您的签证',
'missing_order_sn' => '缺少订单号,请重新填写',
'order_not_exist' => '订单号不存在,请重新填写',
'payment_success' => '订单已支付成功',
'parameter_error' => '参数错误',
'payment_not_completed' => '订单未支付',
'order_not_exist_retry' => '订单号不存在,请重新支付',
'payment_not_completed_retry' => '您还未支付,请重新支付',
],
'payment' => [
'select_payment_method' => '选择支付方式',
'wechat_pay' => '微信支付',
'alipay' => '支付宝',
'checkout' => 'checkout',
'success' => '支付成功',
'fail' => '支付失败'
],
'foot' => [
'user_agreement' => '条款与条件',
'privacy_policy' => '隐私政策',
'disclaimer' => '免责声明',
'refund_policy' => '退款与支付条款',
'cookie_policy' => 'Cookies 使用声明',
'user_agreement_info' => '<div style="padding: 20px;"> <p>
签证更新电子系统EVUS与执法部门 的数据库进行比对。所有持10年有效期访客签证的旅客B1, B2 或 B1/B2,
如果需要以短期访客身份入境美国都必须在赴美前在EVUS上登记。
如果您的EVUS登记获得批准就意味着您可以前往美国。但这并不意味着您可以入境美国。抵达美国入境口岸时您将接受United States
Customs and Border Protection官员的检查。 入境口岸的官员会根据美国移民法的规定决定您是否可以入境。
由您或指定的第三方代理人所提供的所有信息必须是真实和正确的。如果您在填写信息时由您或您的代理人有意提交任何虚假、虚构或伪造的声明或陈述造成EVUS更新失败或者美国签证被注销责任由您本人承担。
信息填写完成后,我们根据你所选择的套餐时效反馈信息,提交后信息将不能再更改。
</p>
<p> 请表明您已阅读并了解上述所提供的信息</p></div>',
'privacy_policy_info' => ' <div style="padding: 20px;"><p>
本应用尊重并保护所有使用服务用户的个人隐私权。为了给您提供更准确、更有个性化的服务,本应用会按照本隐私权政策的规定使用和披露您的个人信息。但本应用将以高度的勤勉、审慎义务对待这些信息。除本隐私权政策另有规定外,在未征得您事先许可的情况下,本应用不会将这些信息对外披露或向第三方提供。本应用会不时更新本隐私权政策。 您在同意本应用服务使用协议之时,即视为您已经同意本隐私权政策全部内容。本隐私权政策属于本应用服务使用协议不可分割的一部分。注意:为更好的提供您所要求的产品和服务上文所述对“第三方”的限制不包括本公司的关联公司(母公司,子公司,控股、被控股公司等),“第三方”是指本司及本司关联公司之外的主体,即下文:信息披露款项中(b)款之规定。
</p>
<h3>1. 适用范围</h3>
<p>(a) 在您注册本应用帐号时,您根据本应用要求提供的个人注册信息; (b) 在您使用本应用网络服务或访问本应用平台网页时本应用自动接收并记录的您的浏览器和计算机上的信息包括但不限于您的IP地址、浏览器的类型、使用的语言、访问日期和时间、软硬件特征信息及您需求的网页记录等数据 (c) 本应用通过合法途径从商业伙伴处取得的用户个人数据。 您了解并同意,以下信息不适用本隐私权政策: (a) 您在使用本应用平台提供的搜索服务时输入的关键字信息; (b) 本应用收集到的您在本应用发布的有关信息数据,包括但不限于参与活动、成交信息及评价详情; (c) 违反法律规定或违反本应用规则行为及本应用已对您采取的措施。
</p>
<h3>2. 信息使用</h3>
<p>
(a)本应用不会向任何无关第三方提供、出售、出租、分享或交易您的个人信息,除非事先得到您的许可,或该第三方和本应用(含本应用关联公司)单独或共同为您提供服务,且在该服务结束后,其将被禁止访问包括其以前能够访问的所有这些资料。 (b) 本应用亦不允许任何第三方以任何手段收集、编辑、出售或者无偿传播您的个人信息。任何本应用平台用户如从事上述活动,一经发现,本应用有权立即终止与该用户的服务协议。 (c) 为服务用户的目的,本应用可能通过使用您的个人信息,向您提供您感兴趣的信息,包括但不限于向您发出产品和服务信息,或者与本应用合作伙伴共享信息以便他们向您发送有关其产品和服务的信息(后者需要您的事先同意)
</p>
<h3>3. 信息披露</h3>
<p>在如下情况下,本应用将依据您的个人意愿或法律的规定全部或部分的披露您的个人信息: (a) 经您事先同意,向第三方披露; (b)为提供您所要求的产品和服务,而必须和第三方分享您的个人信息; (c) 根据法律的有关规定,或者行政或司法机构的要求,向第三方或者行政、司法机构披露; (d) 如您出现违反中国有关法律、法规或者本应用服务协议或相关规则的情况,需要向第三方披露; (e) 如您是适格的知识产权投诉人并已提起投诉,应被投诉人要求,向被投诉人披露,以便双方处理可能的权利纠纷; (f) 在本应用平台上创建的某一交易中,如交易任何一方履行或部分履行了交易义务并提出信息披露请求的,本应用有权决定向该用户提供其交易对方的联络方式等必要信息,以促成交易的完成或纠纷的解决。 (g) 其它本应用根据法律、法规或者网站政策认为合适的披露。
</p>
<h3>4. 信息存储和交换</h3>
<p>
本应用收集的有关您的信息和资料将保存在本应用及(或)其关联公司的服务器上,这些信息和资料可能传送至您所在国家、地区或本应用收集信息和资料所在地的境外并在境外被访问、存储和展示。
</p>
<h3>5. Cookie的使用</h3>
<p>(a) 在您未拒绝接受cookies的情况下本应用会在您的计算机上设定或取用cookies 以便您能登录或使用依赖于cookies的本应用平台服务或功能。本应用使用cookies可为您提供更加周到的个性化服务包括推广服务。 (b) 您有权选择接受或拒绝接受cookies。您可以通过修改浏览器设置的方式拒绝接受cookies。但如果您选择拒绝接受cookies则您可能无法登录或使用依赖于cookies的本应用网络服务或功能。 (c) 通过本应用所设cookies所取得的有关信息将适用本政策。
</p>
<h3>6. 信息安全</h3>
<p>(a) 本应用帐号均有安全保护功能,请妥善保管您的用户名及密码信息。本应用将通过对用户密码进行加密等安全措施确保您的信息不丢失,不被滥用和变造。尽管有前述安全措施,但同时也请您注意在信息网络上不存在“完善的安全措施”。 (b) 在使用本应用网络服务进行网上交易时,您不可避免的要向交易对方或潜在的交易
</p>
<h3>7.本隐私政策的更改</h3>
<p>
(a)如果决定更改隐私政策,我们会在本政策中、本公司网站中以及我们认为适当的位置发布这些更改,以便您了解我们如何收集、使用您的个人信息,哪些人可以访问这些信息,以及在什么情况下我们会透露这些信息。 (b)本公司保留随时修改本政策的权利,因此请经常查看。如对本政策作出重大更改,本公司会通过网站通知的形式告知。 方披露自己的个人信息,如联络方式或者邮政地址。请您妥善保护自己的个人信息,仅在必要的情形下向他人提供。如您发现自己的个人信息泄密,尤其是本应用用户名及密码发生泄露,请您立即联络本应用客服,以便本应用采取相应措施。
</p>
</div>',
'disclaimer_info' => '<div style="padding: 20px;">
<div class="disclaimer-item">
<h4>1. 非官方性质声明</h4>
<p>本网站并非美国政府官方网站也不隶属于美国海关与边境保护局CBP。我们是独立的服务提供商专门协助申请人完成EVUS登记流程。</p>
</div>
<div class="disclaimer-item">
<h4>2. 服务范围说明</h4>
<p>ITS INTERNATIONAL TRAVEL SERVICE COMPANY 仅提供 EVUS 表格填写协助和相关技术支持服务。我们不对最终审批结果作出任何保证或承诺,因为审批权完全在于美国政府相关部门。</p>
</div>
<div class="disclaimer-item">
<h4>3. 审批权限声明</h4>
<p>EVUS申请是否获批完全由美国政府相关部门决定我们无法影响或加快审批进程。我们的服务仅限于协助您正确填写和提交申请表格。</p>
</div>
<div class="note">
<p>使用本服务即表示您已阅读、理解并同意上述免责声明。</p>
</div>
</div>
</div>',
'refund_policy_info' => '<div style="padding: 20px;"> <h4>1. 支付方式</h4> <p>(a) 本公司接受信用卡、借记卡、PayPal等多种支付方式</p> <p>(b) 所有支付交易均通过安全加密处理,保障您的资金安全</p><h4>2. 退款与支付</h4>
<p>(a) 在提交前用户可申请退款退款将在7-14个工作日内处理</p>
<p>(b) 一旦申请提交至EVUS系统费用不予退还因服务已实际提供</p>
<p>(c) 如遇重复扣款等系统问题,经核实后将全额退款</p>
<h4>3. 服务失败</h4>
<p>(a) 因用户提供的信息不准确或不完整导致申请失败,本公司不退款</p>
<p>(b) 因美国政府系统问题导致的申请失败,可提供相应证明申请部分退款</p>
<p>(c) 因本公司技术问题导致申请失败,将全额退款或重新提交申请</p></div>
',
'cookie_policy_info' => ' <div style="padding: 20px;">
<div class="cookie-notice">
<h3>Cookie使用说明</h3>
<p>本网站由 ITS INTERNATIONAL TRAVEL SERVICE COMPANY 运营,使用 Cookies 技术以提升用户体验。</p>
<h4>我们使用Cookies的主要目的</h4>
<ul>
<li>记住您的登录状态和偏好设置</li>
<li>分析网站访问量和用户行为模式</li>
<li>优化网站性能和改进服务质量</li>
<li>提供个性化的内容推荐</li>
</ul>
<h4>Cookie管理</h4>
<p>您可以通过浏览器设置限制或禁用Cookies。请注意如果禁用Cookies部分网站功能可能无法正常使用包括但不限于</p>
<ul>
<li>无法保持登录状态,需要每次访问时重新登录</li>
<li>个性化设置无法保存</li>
<li>某些服务可能完全无法使用</li>
</ul>
<p>继续使用本网站即表示您同意我们按照本说明使用Cookies技术。</p>
</div>
</div>'
],
];

View File

@@ -0,0 +1,81 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2021 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace app\log\driver;
use think\App;
use think\contract\LogHandlerInterface;
/**
* 本地化调试输出到文件
*/
class Monolog implements LogHandlerInterface
{
/**
* 配置参数
* @var array
*/
protected $config = [
'time_format' => 'c',
'json_options' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
];
/**
* 日志写入接口
* @access public
* @param array $log 日志信息
* @return bool
*/
public function save(array $log): bool
{
$info = [];
// 日志信息封装
$time = \DateTime::createFromFormat('0.u00 U', microtime())->setTimezone(new \DateTimeZone(date_default_timezone_get()))->format($this->config['time_format']);
foreach ($log as $type => $val) {
$message = [];
foreach ($val as $msg) {
$message[] = json_encode(['time' => $time, 'type' => $type, 'msg' => $msg], $this->config['json_options']) ;
}
$info[$type] = $message;
}
if ($info) {
return $this->write($info);
}
return true;
}
/**
* 日志写入
* @access protected
* @param array $message 日志信息
* @param string $destination 日志文件
* @return bool
*/
protected function write(array $message): bool
{
$tcp_log_url = env('TCP_LOG_URL');
$tcp_log_port = intval(env('TCP_LOG_PORT'));
$project = env('PROJECT_NAME');
$socket = @fsockopen($tcp_log_url,$tcp_log_port ,$errno, $errstr, 1);
if ($socket) {
fwrite($socket, json_encode(['message' => $message, 'project' => $project], $this->config['json_options']));
fclose($socket);
}
//
return true;
}
}

11
app/middleware.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
\think\middleware\LoadLangPack::class,
// Session初始化
\think\middleware\SessionInit::class,
\app\middleware\LogRecorder::class,
];

View File

@@ -0,0 +1,41 @@
<?php
declare (strict_types = 1);
namespace app\middleware;
use think\facade\Log;
class LogRecorder
{
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
* @return Response
*/
public function handle($request, \Closure $next)
{
// 记录请求信息
$requestInfo = [
'time' => date('Y-m-d H:i:s'),
'method' => $request->method(),
'uri' => $request->url(),
'params' => $request->param(),
'headers' => $request->header(),
'ip' => $request->ip(),
];
Log::write($requestInfo, 'request');
// 继续处理请求
$response = $next($request);
// 记录响应信息
$responseInfo = [
'status' => $response->getCode(),
'data' => $response->getContent(),
];
Log::write($responseInfo, 'response');
return $response;
}
}

413
app/miniapi/common.php Normal file
View File

@@ -0,0 +1,413 @@
<?php
const GET_WEB_CONFIG_URL = 'https://uniuser.jzvisa.com/home/Company/index/model/evus';
const CREATE_EVUS_ORDER_URL = 'https://uniuser.jzvisa.com/home/order/applyevus';
const UPDATE_EVUS_ORDER_URL = 'https://uniuser.jzvisa.com/home/order/updateevus';
const CREATE_ORDER_URL = 'https://uniuser.jzvisa.com/home/order/do_apply';
const NEWS_LIST = 'https://uniuser.jzvisa.com/home/news/lists';
const NEWS_DETAIL = 'https://uniuser.jzvisa.com/home/news/detail';
const YZM_URL = 'https://uniuser.jzvisa.com/home/sms/sendCode';
const GET_ORDER_URL = 'https://uniuser.jzvisa.com/home/order/orderList';
const INCOICETYPE = '1';
const MODEL = 'msg';
const MINIAPI_TOKEN_KEY = '20241021';
const MINIAPI_TOKEN_EXPIRE = 604800;
const BUCKET = 'jnevus';
const COUNTRY = 'EVUS登记';
const SOURCE = 'cn';
const PAY_MARK = 'evuscn';
const OSS_URL = 'https://files.jzvisa.com/' . BUCKET . '/';
/*esta 配置*/
const ESTA_COUNTRY = 'ESTA登记';
const PAY_ESTA_MARK = 'estacn';
const ESTA_MODEL = 'esta';
const ESTA_INCOICETYPE = '6';
// 公共函数
function formDataToJson($fields, $postData, $kew_key, $checkField)
{
if (!isset($postData[$checkField]) || $postData[$checkField] != 1) {
foreach ($fields as $field) {
unset($postData[$field]);
}
return $postData;
}
$data = [];
foreach ($postData[$fields[0]] as $index => $value) {
$entry = [];
foreach ($fields as $field) {
$entry[$field] = isset($postData[$field][$index]) ? $postData[$field][$index] : null;
}
$data[] = $entry;
}
$postData[$kew_key] = json_encode($data, JSON_UNESCAPED_UNICODE);
foreach ($fields as $field) {
unset($postData[$field]);
}
return $postData;
}
function find_json($json_name, $field, $field_value)
{
$data = file_get_contents($json_name);
$data_arr = json_decode($data, true);
$found = array_filter($data_arr, function ($item) use ($field, $field_value) {
return $item[$field] === $field_value;
});
// 输出结果
if ($found) {
$found = array_values($found); // 获取过滤后的第一个元素
return $found;
} else {
return false;
}
}
function getNameById($json_name, $id)
{
$data = file_get_contents('./json/' . $json_name . '.json');
$data_arr = json_decode($data, true);
foreach ($data_arr as $item) {
if (array_key_exists('data', $item['data'])) {
foreach ($item['data']['data'] as $dataItem) {
if ($dataItem['id'] === $id) {
return $dataItem['name'];
}
}
}
}
return null;
}
function getIdByName($json_name, $id)
{
$data = file_get_contents('./json/' . $json_name . '.json');
$data_arr = json_decode($data, true);
foreach ($data_arr as $item) {
if (array_key_exists('data', $item['data'])) {
foreach ($item['data']['data'] as $dataItem) {
if ($dataItem['name'] === $id) {
return $dataItem['id'];
}
}
}
}
return null;
}
function isMobile()
{
$_SERVER['ALL_HTTP'] = isset($_SERVER['ALL_HTTP']) ? $_SERVER['ALL_HTTP'] : '';
if (preg_match('/(up.browser|up.link|mmp|symbian|smartphone|midp|wap|phone|iphone|ipad|ipod|android|xoom)/i',
strtolower($_SERVER['HTTP_USER_AGENT']))) {
return true;
}
if ((isset($_SERVER['HTTP_ACCEPT'])) and (strpos(strtolower($_SERVER['HTTP_ACCEPT']),
'application/vnd.wap.xhtml+xml') !== false)) {
return true;
}
if (isset($_SERVER['HTTP_X_WAP_PROFILE'])) {
return true;
}
if (isset($_SERVER['HTTP_PROFILE'])) {
return true;
}
$mobile_ua = strtolower(substr($_SERVER['HTTP_USER_AGENT'], 0, 4));
$mobile_agents = array(
'w3c ',
'acs-',
'alav',
'alca',
'amoi',
'audi',
'avan',
'benq',
'bird',
'blac',
'blaz',
'brew',
'cell',
'cldc',
'cmd-',
'dang',
'doco',
'eric',
'hipt',
'inno',
'ipaq',
'java',
'jigs',
'kddi',
'keji',
'leno',
'lg-c',
'lg-d',
'lg-g',
'lge-',
'maui',
'maxo',
'midp',
'mits',
'mmef',
'mobi',
'mot-',
'moto',
'mwbp',
'nec-',
'newt',
'noki',
'oper',
'palm',
'pana',
'pant',
'phil',
'play',
'port',
'prox',
'qwap',
'sage',
'sams',
'sany',
'sch-',
'sec-',
'send',
'seri',
'sgh-',
'shar',
'sie-',
'siem',
'smal',
'smar',
'sony',
'sph-',
'symb',
't-mo',
'teli',
'tim-',
'tosh',
'tsm-',
'upg1',
'upsi',
'vk-v',
'voda',
'wap-',
'wapa',
'wapi',
'wapp',
'wapr',
'webc',
'winw',
'winw',
'xda',
'xda-'
);
if (in_array($mobile_ua, $mobile_agents)) {
return true;
}
if (strpos(strtolower($_SERVER['ALL_HTTP']), 'operamini') !== false) {
return true;
}
// Pre-final check to reset everything if the user is on Windows
if (strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'windows') !== false) {
return false;
}
// But WP7 is also Windows, with a slightly different characteristic
if (strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'windows phone') !== false) {
return true;
}
return false;
}
function getOrderNumber()
{
return date('YmdHis') . mt_rand(10, 99);
}
function cmf_get_current_user_id()
{
$sessionUserId = session('user.id');
if (empty($sessionUserId)) {
return 0;
}
return $sessionUserId;
}
function getIP()
{
$realip = '';
$unknown = 'unknown';
if (isset($_SERVER)){
if(isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']) && strcasecmp($_SERVER['HTTP_X_FORWARDED_FOR'], $unknown)){
$arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
foreach($arr as $ip){
$ip = trim($ip);
if ($ip != 'unknown'){
$realip = $ip;
break;
}
}
}else if(isset($_SERVER['HTTP_CLIENT_IP']) && !empty($_SERVER['HTTP_CLIENT_IP']) && strcasecmp($_SERVER['HTTP_CLIENT_IP'], $unknown)){
$realip = $_SERVER['HTTP_CLIENT_IP'];
}else if(isset($_SERVER['REMOTE_ADDR']) && !empty($_SERVER['REMOTE_ADDR']) && strcasecmp($_SERVER['REMOTE_ADDR'], $unknown)){
$realip = $_SERVER['REMOTE_ADDR'];
}else{
$realip = $unknown;
}
}else{
if(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), $unknown)){
$realip = getenv("HTTP_X_FORWARDED_FOR");
}else if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), $unknown)){
$realip = getenv("HTTP_CLIENT_IP");
}else if(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), $unknown)){
$realip = getenv("REMOTE_ADDR");
}else{
$realip = $unknown;
}
}
$realip = preg_match("/[\d\.]{7,15}/", $realip, $matches) ? $matches[0] : $unknown;
return $realip;
}
function build_ocr_signature(string $appId, string $secret, ?int $timestamp = null): array
{
$timestamp = $timestamp ?? time();
$signString = $appId . "\n" . (string)$timestamp;
$signature = hash_hmac('sha256', $signString, $secret);
return [
'app_id' => $appId,
'timestamp' => (string)$timestamp,
'signature' => $signature,
'base_url' => config('app.OCR_BASE_URL'),
];
}
// 这是系统自动生成的公共文件
function arrayToJson($params, $values)
{
$datas = [];
$count = count($values[0]);
$valcount = count($params);
for ($i = 0; $i < $count; $i++) {
for ($j = 0; $j < $valcount; $j++) {
$data[$params[$j]] = $values[$j][$i];
}
$datas[] = $data;
}
if (empty($datas)) {
return '';
}
return json_encode($datas);
}
function getCate($val)
{
$cates = explode('-', $val);
if (count($cates) == 1) {
if ($val == 'CNMI') {
return 'CW/E2C';
}
} else {
preg_match_all('/[A-Z]+/', $val, $matches);
return $matches[0][0];
}
}
//非常给力的authcode加密函数,Discuz!经典代码(带详解)
function authcode($string, $operation = 'DECODE', $key = '20241021', $expiry = 0)
{
// 动态密匙长度,相同的明文会生成不同密文就是依靠动态密匙
$ckey_length = 4;
// 密匙
$key = md5($key ? $key : $GLOBALS['discuz_auth_key']);
// 密匙a会参与加解密
$keya = md5(substr($key, 0, 16));
// 密匙b会用来做数据完整性验证
$keyb = md5(substr($key, 16, 16));
// 密匙c用于变化生成的密文
$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : '';
// 参与运算的密匙
$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
// 明文前10位用来保存时间戳解密时验证数据有效性10到26位用来保存$keyb(密匙b)
//解密时会通过这个密匙验证数据完整性
// 如果是解码的话,会从第$ckey_length位开始因为密文前$ckey_length位保存 动态密匙,以保证解密正确
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
// 产生密匙簿
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
// 用固定的算法,打乱密匙簿,增加随机性,好像很复杂,实际上对并不会增加密文的强度
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
// 核心加解密部分
for ($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
// 从密匙簿得出密匙进行异或,再转成字符
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if ($operation == 'DECODE') {
// 验证数据有效性,请看未加密明文的格式
if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
} else {
// 把动态密匙保存在密文里,这也是为什么同样的明文,生产不同密文后能解密的原因
// 因为加密后的密文可能是一些特殊字符复制过程可能会丢失所以用base64编码
return $keyc . str_replace('=', '', base64_encode($result));
}
}
function getChineseCode($code)
{
$url = "http://code.mcdvisa.com/save.php?action=getcode&i=0&w={$code}";
$data = file_get_contents($url);
if (empty($data)){
return [
'code'=>0,
'data'=>[]
];
}
// 创建 DOMDocument 对象并加载 HTML
$dom = new DOMDocument();
libxml_use_internal_errors(true); // 抑制 HTML 解析错误
$dom->loadHTML($data);
libxml_clear_errors();
// 获取所有的 <li> 元素
$lis = $dom->getElementsByTagName('li');
$pattern = '/-?\d+(?:\.\d+)?/';
$datas=[];
foreach ($lis as $li) {
$text = $li->textContent;
preg_match_all($pattern, $text, $matches);
$datas[] = $matches[0][0];
}
return [
'code'=>200,
'data'=>$datas
];
}

View File

@@ -0,0 +1,134 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
use api\Httpcurl;
use app\service\MiniProgramWechatService;
use think\exception\ValidateException;
use think\facade\Session;
class Auth extends Base
{
public function sendSms()
{
$data = $this->postData();
if ($error = $this->requireFields($data, ['mobile'])) {
return $error;
}
$code = mt_rand(1001, 9999);
$response = Httpcurl::request(YZM_URL, 'post', [
'mobile' => $data['mobile'],
'code' => $code,
]);
$result = $this->decodeResponse($response, 'miniapi send sms');
if (empty($result) || intval($result['code'] ?? 0) !== 1) {
return $this->fail('发送验证码失败');
}
Session::set($data['mobile'] . '_code', $code);
Session::set($data['mobile'] . '_' . $code, time());
return $this->ok([], '发送成功');
}
public function login()
{
$data = $this->postData();
try {
$this->validate($data, [
'mobile' => 'require',
'code' => 'require',
], [
'mobile.require' => '手机号不能为空',
'code.require' => '验证码不能为空',
]);
} catch (ValidateException $e) {
return $this->fail($e->getError());
}
$sms = Session::get($data['mobile'] . '_code');
if (empty($sms) || strval($sms) !== strval($data['code'])) {
return $this->fail('验证码不正确');
}
$codeTime = Session::get($data['mobile'] . '_' . $data['code']);
if (empty($codeTime) || (time() - intval($codeTime)) > 300) {
Session::delete($data['mobile'] . '_code');
Session::delete($data['mobile'] . '_' . $data['code']);
return $this->fail('验证码超时,请重新发送');
}
$openid = '';
if (!empty($data['login_code'])) {
$openid = $this->wechatService()->openidByLoginCode((string)$data['login_code']);
}
return $this->loginByMobile((string)$data['mobile'], $openid);
}
public function wxPhoneLogin()
{
$data = $this->postData();
if ($error = $this->requireFields($data, ['phone_code', 'login_code'])) {
return $error;
}
$wechat = $this->wechatService();
$openid = $wechat->openidByLoginCode((string)$data['login_code'], true);
if ($openid === '') {
return $this->fail('微信登录失败,请使用验证码登录');
}
$mobile = $wechat->mobileByPhoneCode((string)$data['phone_code']);
if ($mobile === '') {
return $this->fail('微信手机号获取失败,请使用验证码登录');
}
return $this->loginByMobile($mobile, $openid);
}
public function me()
{
$userId = $this->currentUserId();
if ($userId <= 0) {
return $this->ok(['isLogin' => false]);
}
return $this->ok([
'isLogin' => true,
'user' => [
'id' => $userId,
],
]);
}
private function loginByMobile(string $mobile, string $openid = '')
{
$response = Httpcurl::request(config('app.user_url'), 'post', [
'model' => MODEL,
'mobile' => $mobile,
]);
$result = $this->decodeResponse($response, 'miniapi login');
if (empty($result) || intval($result['code'] ?? 0) !== 1 || empty($result['data']['id'])) {
return $this->fail('账号不存在');
}
$user = $result['data'];
return $this->ok([
'token' => authcode((string)$user['id'], 'ENCODE', MINIAPI_TOKEN_KEY, MINIAPI_TOKEN_EXPIRE),
'expires_in' => MINIAPI_TOKEN_EXPIRE,
'openid' => $openid,
'user' => [
'id' => intval($user['id']),
'mobile' => $user['mobile'] ?? $mobile,
'nickname' => $user['nickname'] ?? '微信用户',
],
], '登录成功');
}
private function wechatService(): MiniProgramWechatService
{
return new MiniProgramWechatService($this->currentMiniAppCode(), $this->currentMiniApp());
}
}

View File

@@ -0,0 +1,283 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
use api\Httpcurl;
use app\BaseController;
use app\miniapi\service\Context\MiniAppContext;
use think\exception\HttpResponseException;
use think\facade\Config;
use think\facade\Log;
class Base extends BaseController
{
protected $miniAppCode = '';
protected $miniApp = [];
protected function initialize()
{
parent::initialize();
$this->initializeMiniAppContext();
$this->loadSiteConfig();
}
protected function ok($data = [], string $msg = 'ok')
{
return json([
'code' => 1,
'msg' => $msg,
'data' => $data,
]);
}
protected function fail(string $msg = '操作失败', int $code = 0, $data = [])
{
$httpStatus = in_array($code, [401, 403], true) ? $code : 200;
return json([
'code' => $code,
'msg' => $msg,
'data' => $data,
], $httpStatus);
}
protected function postData(): array
{
$data = $this->request->post();
if (!empty($data)) {
return $data;
}
$raw = $this->request->getInput();
$json = json_decode($raw, true);
return is_array($json) ? $json : [];
}
protected function requireFields(array $data, array $fields)
{
foreach ($fields as $field) {
if (!isset($data[$field]) || $data[$field] === '') {
return $this->fail('缺少参数:' . $field);
}
}
return null;
}
protected function decodeResponse($response, string $scene): array
{
if (!is_array($response) || !isset($response[0]) || !empty($response[3])) {
Log::error($scene . ' request failed: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
return [];
}
$result = json_decode($response[0], true);
if (!is_array($result)) {
Log::error($scene . ' response decode failed: ' . $response[0]);
return [];
}
return $result;
}
protected function currentUserId(): int
{
$token = $this->currentToken();
if ($token === '') {
return 0;
}
$userId = authcode($token, 'DECODE', MINIAPI_TOKEN_KEY);
return is_numeric($userId) ? intval($userId) : 0;
}
protected function requireLogin()
{
$userId = $this->currentUserId();
if ($userId <= 0) {
return $this->fail('请先登录', 401);
}
return $userId;
}
protected function currentMiniAppCode(): string
{
return $this->miniAppCode;
}
protected function currentMiniApp(): array
{
return $this->miniApp;
}
protected function currentCompanyCode(): string
{
return (string)($this->miniApp['company_code'] ?? '');
}
protected function currentMiniAppContext(string $businessCode = 'evus'): MiniAppContext
{
$context = (array)Config::get('app.miniapi_context', []);
return new MiniAppContext([
'mini_app_code' => $this->currentMiniAppCode(),
'company_code' => $this->currentCompanyCode(),
'business_code' => $businessCode,
'site_domain' => (string)($context['site_domain'] ?? ''),
'mini_app' => $this->currentMiniApp(),
'site' => (array)Config::get('app.miniapi_site', []),
]);
}
private function currentToken(): string
{
$token = trim((string)$this->request->header('Authorization', ''));
if (stripos($token, 'Bearer ') === 0) {
$token = trim(substr($token, 7));
}
if ($token === '') {
$token = trim((string)$this->request->param('token', ''));
}
return $token;
}
private function loadSiteConfig(): void
{
$configFile = $this->siteConfigFile();
if (!file_exists($configFile)) {
$legacyConfig = $this->legacySiteConfig();
if (!empty($legacyConfig)) {
$config = $legacyConfig;
file_put_contents($configFile, json_encode($config, JSON_UNESCAPED_UNICODE));
} else {
$domain = $this->siteConfigDomain();
$response = Httpcurl::request(GET_WEB_CONFIG_URL . '/domain/' . $domain, 'get');
$config = $this->decodeResponse($response, 'site config');
if (empty($config) || intval($config['code'] ?? 0) !== 1) {
return;
}
file_put_contents($configFile, json_encode($config, JSON_UNESCAPED_UNICODE));
}
} else {
$config = json_decode(file_get_contents($configFile), true);
}
if (empty($config['data']) || !is_array($config['data'])) {
return;
}
$data = $config['data'];
Config::set(['EVUS_GOODS' => $data['EVUS_GOODS'] ?? []], 'app');
Config::set(['ESTA_GOODS' => $data['ESTA_GOODS'] ?? []], 'app');
Config::set(['pay_url' => $data['pay']['pay_url'] ?? ''], 'app');
Config::set(['order_url' => $data['order_url'] ?? ''], 'app');
Config::set(['user_url' => $data['user_url'] ?? ''], 'app');
Config::set(['get_evus_detail_url' => $data['get_evus_detail_url'] ?? ''], 'app');
Config::set(['get_order_detail_url' => $data['get_order_detail_url'] ?? ''], 'app');
Config::set(['download_evus_url' => $data['download_evus_url'] ?? ''], 'app');
Config::set(['update_order_url' => $data['update_order_url'] ?? ''], 'app');
Config::set(['LOOKUP_URL' => $data['evus_look_url'] ?? ''], 'app');
Config::set(['OCR_BASE_URL' => $data['ocr_base_url'] ?? ''], 'app');
Config::set(['OCR_APP_ID' => $data['ocr_app_id'] ?? ''], 'app');
Config::set(['OCR_SECRET' => $data['ocr_secret'] ?? ''], 'app');
Config::set(['miniapi_site' => [
'company_name' => $data['company_name'] ?? '',
'principals' => $data['principals'] ?? '',
'phone' => $data['phone'] ?? '',
'brief' => $data['brief'] ?? '',
'footer' => $data['footer'] ?? '',
'logo' => $data['logo'] ?? '',
'logo_icon' => $data['logo_icon'] ?? '',
]], 'app');
}
private function initializeMiniAppContext(): void
{
$apps = Config::get('miniapp.apps', []);
$headerName = (string)Config::get('miniapp.header_name', 'X-Mini-App-Code');
$defaultCode = (string)Config::get('miniapp.default_code', '');
$code = trim((string)$this->request->header($headerName, ''));
if ($code === '') {
$code = trim((string)$this->request->param('mini_app_code', ''));
}
$hasExplicitCode = $code !== '';
if ($code === '') {
$code = $defaultCode;
}
if ($code !== '' && (!isset($apps[$code]) || intval($apps[$code]['status'] ?? 1) !== 1)) {
Log::warning('miniapp config not found or disabled: ' . $code);
if ($hasExplicitCode) {
throw new HttpResponseException($this->fail('mini app config not found', 403));
}
$code = $defaultCode;
}
$this->miniAppCode = $code;
$this->miniApp = $apps[$code] ?? [];
if (!empty($this->miniApp)) {
$this->miniApp['code'] = $code;
}
Config::set(['miniapi_context' => [
'mini_app_code' => $this->miniAppCode,
'company_code' => $this->currentCompanyCode(),
'site_domain' => $this->siteConfigDomain(),
]], 'app');
}
private function siteConfigFile(): string
{
$configDir = app()->getRootPath() . 'data/config/';
if (!is_dir($configDir)) {
mkdir($configDir, 0755, true);
}
return $configDir . $this->siteConfigCacheKey() . '.json';
}
private function legacySiteConfig(): array
{
$defaultCode = (string)Config::get('miniapp.default_code', '');
if ($this->miniAppCode !== $defaultCode) {
return [];
}
$legacyFile = app()->getRootPath() . 'data/config.json';
if (!file_exists($legacyFile)) {
return [];
}
$config = json_decode(file_get_contents($legacyFile), true);
return is_array($config) ? $config : [];
}
private function siteConfigDomain(): string
{
$domain = trim((string)($this->miniApp['site_domain'] ?? ''));
if ($domain === '') {
$domain = trim((string)Config::get('app.site_domain', ''));
}
if ($domain === '') {
return $this->request->host();
}
if (stripos($domain, 'http://') === 0 || stripos($domain, 'https://') === 0) {
$host = parse_url($domain, PHP_URL_HOST);
return $host ?: $this->request->host();
}
return trim(explode('/', $domain)[0]);
}
private function siteConfigCacheKey(): string
{
$key = $this->miniAppCode ?: $this->siteConfigDomain();
$key = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $key);
return $key ?: 'default';
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
use api\Httpcurl;
use app\miniapi\service\Context\MiniAppContext;
use app\miniapi\service\Payment\MiniPayService;
use app\miniapi\service\Product\ProductService;
class Esta extends Base
{
public function apply()
{
$userId = $this->requireLogin();
if (!is_int($userId)) {
return $userId;
}
$data = $this->postData();
if (empty($data['passport_img'])) {
return $this->fail('请上传护照照片');
}
if (empty($data['person_img'])) {
return $this->fail('请上传个人照片');
}
if ($error = $this->requireFields($data, ['total_price'])) {
return $error;
}
$context = $this->currentMiniAppContext('esta');
$product = (new ProductService())->findByValue($context, $data['total_price']);
if (empty($product)) {
return $this->fail('价格错误');
}
$totalPrice = $product['value'];
$orderSn = getOrderNumber();
$payload = $this->buildOrderPayload($data, $orderSn, $product, $userId);
$response = Httpcurl::request(CREATE_ORDER_URL, 'post', $payload);
$result = $this->decodeResponse($response, 'miniapi esta apply');
if (empty($result) || intval($result['code'] ?? 0) !== 1) {
return $this->fail($result['msg'] ?? '提交失败,请联系管理员');
}
if (!empty($data['openid'])) {
$pay = $this->requestMiniPay($context, $orderSn, $totalPrice, $data['openid']);
if ($pay['code'] !== 1) {
return $this->fail($pay['msg']);
}
return $this->ok([
'order_sn' => $orderSn,
'pay' => $pay['data'],
], '提交成功');
}
return $this->ok([
'order_sn' => $orderSn,
'pay' => null,
], '提交成功');
}
private function buildOrderPayload(array $data, string $orderSn, array $product, int $userId): array
{
$payload = $data;
unset($payload['token']);
$totalPrice = (string)$product['value'];
$payload['user_id'] = $userId;
$payload['order_sn'] = $orderSn;
$payload['source'] = 'qlwxmini';
$payload['is_mobile'] = 1;
$payload['total_price'] = $totalPrice;
$payload['origin_price'] = $totalPrice;
$payload['product_value'] = $totalPrice;
$payload['product_name'] = (string)($product['name'] ?? '');
$payload['product_desc'] = (string)($product['desc'] ?? '');
$payload['ip'] = getIP();
$payload['coupon_code'] = $payload['coupon_code'] ?? '';
$payload['model'] = ESTA_MODEL;
$payload['token'] = md5(ESTA_MODEL);
$payload['other_country_acquired_other'] = $payload['other_country_acquired_other'] ?? '';
foreach ([
'birth_date' => 'birth_date',
'passport_expedition_date' => 'passport_expedition_date',
'passport_expiration_date' => 'passport_expiration_date',
'other_country_date' => 'other_country_date',
'other_country_edate' => 'other_country_edate',
] as $field => $prefix) {
$date = date_parse($payload[$field] ?? '');
$payload[$prefix . '_day'] = (string)($date['day'] ?: '');
$payload[$prefix . '_month'] = (string)($date['month'] ?: '');
$payload[$prefix . '_year'] = (string)($date['year'] ?: '');
}
unset($payload['other_country_date'], $payload['other_country_edate']);
return $payload;
}
private function requestMiniPay(MiniAppContext $context, string $orderSn, $money, string $openid): array
{
return (new MiniPayService())->request($context, $orderSn, $money, $openid, 'miniapi esta mini pay');
}
}

View File

@@ -0,0 +1,139 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
use api\Httpcurl;
use app\miniapi\service\Context\MiniAppContext;
use app\miniapi\service\Payment\MiniPayService;
use app\miniapi\service\Product\ProductService;
class Evus extends Base
{
public function lookup()
{
$data = $this->postData();
if ($error = $this->requireFields($data, [
'passport_number',
'visa_foil_number',
'last_name',
'first_name',
'birth_date',
'country_birth',
])) {
return $error;
}
$birth = date_parse($data['birth_date']);
$params = [
'passportNum' => $data['passport_number'],
'b1b2Num' => $data['visa_foil_number'],
'surname' => $data['last_name'],
'givenname' => $data['first_name'],
'year_birthday' => (string)($birth['year'] ?: ''),
'month_birthday' => (string)($birth['month'] ?: ''),
'day_birthday' => (string)($birth['day'] ?: ''),
'country_birth' => $data['country_birth'],
];
$response = Httpcurl::request(config('app.LOOKUP_URL'), 'post', json_encode($params, JSON_UNESCAPED_UNICODE), [
'Content-Type: application/json',
]);
$result = $this->decodeResponse($response, 'miniapi evus lookup');
if (empty($result)) {
return $this->fail('服务异常,请稍后重试');
}
return $this->ok($result);
}
public function apply()
{
$userId = $this->requireLogin();
if (!is_int($userId)) {
return $userId;
}
$data = $this->postData();
if ($error = $this->requireFields($data, ['total_price'])) {
return $error;
}
$context = $this->currentMiniAppContext('evus');
$product = (new ProductService())->findByValue($context, $data['total_price']);
if (empty($product)) {
return $this->fail('价格错误');
}
$totalPrice = $product['value'];
$orderSn = getOrderNumber();
$payload = $this->buildOrderPayload($data, $orderSn, $product, $userId);
$response = Httpcurl::request(CREATE_EVUS_ORDER_URL, 'post', $payload);
$result = $this->decodeResponse($response, 'miniapi evus apply');
if (empty($result) || intval($result['code'] ?? 0) !== 1) {
return $this->fail($result['msg'] ?? '提交失败,请联系管理员');
}
if (!empty($data['openid'])) {
$pay = $this->requestMiniPay($context, $orderSn, $totalPrice, $data['openid']);
if ($pay['code'] !== 1) {
return $this->fail($pay['msg']);
}
return $this->ok([
'order_sn' => $orderSn,
'pay' => $pay['data'],
], '提交成功');
}
return $this->ok([
'order_sn' => $orderSn,
'pay' => null,
], '提交成功');
}
private function buildOrderPayload(array $data, string $orderSn, array $product, int $userId): array
{
$payload = $data;
unset($payload['token']);
$totalPrice = (string)$product['value'];
$payload['user_id'] = $userId;
$payload['order_sn'] = $orderSn;
$payload['source'] = 'qlwxmini';
$payload['is_mobile'] = 1;
$payload['total_price'] = $totalPrice;
$payload['origin_price'] = $totalPrice;
$payload['product_value'] = $totalPrice;
$payload['product_name'] = (string)($product['name'] ?? '');
$payload['product_desc'] = (string)($product['desc'] ?? '');
$payload['ip'] = getIP();
$payload['coupon_code'] = $payload['coupon_code'] ?? '';
$payload['model'] = MODEL;
$payload['token'] = md5(MODEL);
foreach ([
'birth_date' => 'birth_date',
'passport_expedition_date' => 'passport_expedition_date',
'passport_expiration_date' => 'passport_expiration_date',
'us_visa_passport_issue_date' => 'us_visa_passport_issue_date',
'us_visa_passport_expiration_date' => 'us_visa_passport_expiration_date',
'us_visa_birth_date' => 'us_visa_birth_date',
] as $field => $prefix) {
$date = date_parse($payload[$field] ?? '');
$payload[$prefix . '_day'] = (string)($date['day'] ?: '');
$payload[$prefix . '_month'] = (string)($date['month'] ?: '');
$payload[$prefix . '_year'] = (string)($date['year'] ?: '');
}
if (isset($payload['vist_country']) && is_array($payload['vist_country'])) {
$payload = formDataToJson(['vist_country', 'vist_from_date', 'vist_end_date', 'vist_country_des'], $payload, 'vistitem', 'question_i');
}
$payload['other_country_acquired_other'] = $payload['other_country_acquired_other'] ?? '';
return $payload;
}
private function requestMiniPay(MiniAppContext $context, string $orderSn, $money, string $openid): array
{
return (new MiniPayService())->request($context, $orderSn, $money, $openid, 'miniapi evus mini pay');
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
class Help extends Base
{
public function faqs()
{
return $this->ok([
[
'question' => '什么是签证更新电子系统EVUS',
'answer' => '签证更新电子系统是持有十年有效 B1/B2、B1 或 B2 访问者签证的中国公民使用的个人基本信息在线定期更新系统。除有效签证外,相关旅客还需要完成 EVUS 登记,才能获得赴美旅行许可。'
],
[
'question' => 'EVUS登记从哪一天开始强制施行',
'answer' => '自 2016 年 11 月 29 日起,持中华人民共和国护照并持有最长有效期十年 B1/B2、B1 或 B2 签证的个人,必须持有有效的 EVUS 登记才能赴美旅行。没有有效登记的旅客将不能获得登机牌或经由陆地入境口入境。'
],
[
'question' => 'B1/B2签证有效期有什么要求',
'answer' => '绝大多数 B1/B2 签证有效期为 10 年,所有十年 B1/B2 签证在前往美国前都需要完成 EVUS 更新。极个别低于 10 年有效期的 B1/B2 签证,通常不需要进行 EVUS 登记。'
],
[
'question' => '护照有效期和换发护照会影响EVUS吗',
'answer' => '16 岁以上护照有效期通常为 10 年16 岁以下通常为 5 年。护照有效期低于 6 个月时禁止前往美国。如果护照过期或有效期不足,需要换发新护照;换发时应保留含有效美国签证的旧护照。'
],
[
'question' => '美国B1/B2签证EVUS有效期是多久',
'answer' => '如果十年 B1/B2 签证有效且护照有效期大于 2 年 6 个月EVUS 通常有效期为 2 年。如果护照有效期少于 2 年 6 个月EVUS 有效期通常按护照实际有效期减去 6 个月计算。'
],
[
'question' => 'EVUS失效或个人信息变更后需要更新吗',
'answer' => 'EVUS 过期并不必然导致美国签证失效,可以在前往美国前重新登记。若住址、工作信息、赴美地址、联系方式等个人信息发生变化,建议及时更新 EVUS。'
],
[
'question' => 'EVUS登记需要准备什么',
'answer' => '旅客需要准备含有最长有效期十年 B1/B2、B1 或 B2 签证的中华人民共和国护照,并确保可以连接互联网。持十年有效期签证的未成年人,也需要持有效个人护照并在赴美前申请 EVUS。'
],
[
'question' => 'EVUS表格上会填写哪些问题',
'answer' => '每次登记都需要提供姓名、出生日期、紧急联系人、护照信息、个人和就业信息,并回答旅行资格相关问题。朋友、亲属、专业旅游从业人员或第三方可以代为提交,但旅客本人应对信息真实性和准确性负责。'
],
[
'question' => '进入美国是否需要登记EVUS',
'answer' => '是的。自 2016 年 11 月起需要登记。该要求用于定期更新旅客个人信息,有助于加快中国旅客入境美国。'
],
[
'question' => '其他国家是否也有EVUS要求',
'answer' => 'EVUS 是一项针对特定旅客的信息更新要求。美国政府曾预期未来可能将类似要求适用于其他国家。'
],
[
'question' => '刚拿到十年B1/B2签证还需要申请新签证吗',
'answer' => '不需要,原签证仍然有效。但如果在 2016 年 11 月以后前往美国,需要先完成 EVUS 登记。EVUS 登记和申请签证是两个不同的程序。'
],
]);
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
use app\miniapi\service\Legal\LegalDocumentService;
use app\miniapi\service\Product\ProductService;
use think\facade\Config;
class Index extends Base
{
public function home()
{
$products = new ProductService();
return $this->ok([
'site' => Config::get('app.miniapi_site', []),
'packages' => $products->list($this->currentMiniAppContext('evus')),
'quickEntries' => [
['key' => 'apply', 'title' => '开始登记', 'path' => '/pages/apply/index'],
['key' => 'query', 'title' => '状态查询', 'path' => '/pages/query/index'],
['key' => 'orders', 'title' => '我的订单', 'path' => '/pages/orders/index'],
['key' => 'help', 'title' => '帮助中心', 'path' => '/pages/help/index'],
],
]);
}
public function packages()
{
$businessCode = strtolower(trim((string)$this->request->param('business', 'evus'))) ?: 'evus';
return $this->ok((new ProductService())->list($this->currentMiniAppContext($businessCode)));
}
public function about()
{
$site = Config::get('app.miniapi_site', []);
$companyName = trim((string)($site['company_name'] ?? '')) ?: 'EVUS登记助手';
$brief = $this->plainText((string)($site['brief'] ?? ''));
return $this->ok([
'title' => '关于我们',
'company_name' => $companyName,
'content' => $brief ?: $companyName . '提供 EVUS 登记资料提交、订单查询和进度管理服务。',
'phone' => (string)($site['phone'] ?? ''),
'notice' => '我们不是美国政府官方网站,登记结果以官方系统为准。',
'logo' => (string)($site['logo'] ?? ''),
'logo_icon' => (string)($site['logo_icon'] ?? ''),
]);
}
public function legal()
{
$type = strtolower(trim((string)$this->request->param('type', 'terms')));
$businessCode = strtolower(trim((string)$this->request->param('business', 'evus'))) ?: 'evus';
$document = (new LegalDocumentService())->get($this->currentMiniAppContext($businessCode), $type);
if (empty($document)) {
return $this->fail('legal type not found', 404);
}
return $this->ok($document);
}
public function countries()
{
$file = app()->getRootPath() . 'public/json/country.json';
if (!file_exists($file)) {
return $this->ok([]);
}
$countries = json_decode(file_get_contents($file), true);
return $this->ok(is_array($countries) ? $countries : []);
}
private function plainText(string $html): string
{
$html = preg_replace('/<\s*(h[1-6]|div|section|li)\b[^>]*>/i', "\n", $html);
$html = preg_replace('/<\s*\/\s*(h[1-6]|div|section|li)\s*>/i', "\n", (string)$html);
$text = preg_replace('/<\s*br\s*\/?\s*>/i', "\n", $html);
$text = preg_replace('/<\s*\/p\s*>/i', "\n", $text);
$text = strip_tags((string)$text);
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$text = preg_replace("/[ \t\r]+/", ' ', $text);
$text = preg_replace("/\n{3,}/", "\n\n", $text);
return trim((string)$text);
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
use api\Httpcurl;
class News extends Base
{
public function lists()
{
$page = max(1, intval($this->request->param('page', 1)));
$size = max(1, intval($this->request->param('size', 10)));
$response = Httpcurl::request(NEWS_LIST, 'post', [
'model' => 'article',
'page' => $page,
'size' => $size,
]);
$result = $this->decodeResponse($response, 'miniapi news list');
if (empty($result) || intval($result['code'] ?? 0) !== 1) {
return $this->ok(['list' => [], 'count' => 0, 'page' => $page, 'size' => $size]);
}
return $this->ok([
'list' => $this->normalizeList($result['data'] ?? []),
'count' => intval($result['count'] ?? count($result['data'] ?? [])),
'page' => $page,
'size' => $size,
]);
}
public function detail()
{
$id = intval($this->request->param('id', 0));
if ($id <= 0) {
return $this->fail('参数错误');
}
$response = Httpcurl::request(NEWS_DETAIL, 'post', [
'model' => 'article',
'id' => $id,
]);
$result = $this->decodeResponse($response, 'miniapi news detail');
if (empty($result) || intval($result['code'] ?? 0) !== 1 || empty($result['data'])) {
return $this->fail('新闻详情获取失败');
}
return $this->ok($this->normalizeItem($result['data']));
}
private function normalizeList($list): array
{
if (!is_array($list)) {
return [];
}
$data = [];
foreach ($list as $item) {
$data[] = $this->normalizeItem($item);
}
return $data;
}
private function normalizeItem($item): array
{
if (!is_array($item)) {
return [];
}
return [
'id' => intval($item['id'] ?? 0),
'title' => $item['title'] ?? '',
'content' => $item['contents'] ?? ($item['content'] ?? ''),
'create_time' => $item['create_time'] ?? '',
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
require_once dirname(__DIR__) . DIRECTORY_SEPARATOR . 'common.php';
class Ocr extends Base
{
private const ENDPOINTS = [
'passport' => '/ocr/passport/base64',
'visa' => '/ocr/visa-number/base64',
'identification' => '/ocr/idcard/base64',
];
public function signature()
{
$type = strtolower(trim((string)$this->request->param('type', 'passport')));
if (!isset(self::ENDPOINTS[$type])) {
return $this->fail('不支持的识别类型');
}
$appId = (string)(config('app.OCR_APP_ID') ?? '');
$secret = (string)(config('app.OCR_SECRET') ?? '');
$baseUrl = (string)(config('app.OCR_BASE_URL') ?? '');
if ($appId === '' || $secret === '' || $baseUrl === '') {
return $this->fail('OCR服务配置未完成');
}
$signature = build_ocr_signature($appId, $secret);
$signature['endpoint'] = self::ENDPOINTS[$type];
$signature['origin'] = $this->resolveOrigin();
$signature['expires_in'] = 300;
$signature['auto_rotate'] = true;
$signature['return_debug'] = false;
return $this->ok($signature);
}
private function resolveOrigin(): string
{
$origin = trim((string)(config('app.OCR_ORIGIN') ?? config('ocr.origin', '')));
if ($origin === '') {
$context = (array)(config('app.miniapi_context') ?? []);
$origin = trim((string)($context['site_domain'] ?? ''));
}
if ($origin === '') {
return '';
}
if (!preg_match('/^https?:\/\//i', $origin)) {
$origin = 'https://' . $origin;
}
$parts = parse_url($origin);
if (!is_array($parts) || empty($parts['host'])) {
return '';
}
$scheme = strtolower((string)($parts['scheme'] ?? 'https'));
if (!in_array($scheme, ['http', 'https'], true)) {
return '';
}
$port = isset($parts['port']) ? ':' . (int)$parts['port'] : '';
return $scheme . '://' . $parts['host'] . $port;
}
}

View File

@@ -0,0 +1,193 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
use api\Httpcurl;
use app\miniapi\service\Payment\MiniPayService;
class Order extends Base
{
public function lists()
{
$userId = $this->requireLogin();
if (!is_int($userId)) {
return $userId;
}
$payStatus = (string)$this->request->param('pay_status', 'pay');
if (!in_array($payStatus, ['pay', 'nopay'], true)) {
return $this->fail('参数错误');
}
$business = $this->businessCode((string)$this->request->param('business', 'evus'));
$model = $this->businessModel($business);
$invoiceType = $this->businessInvoiceType($business);
$response = Httpcurl::request(GET_ORDER_URL, 'post', [
'model' => $model,
'user_id' => $userId,
'invoiceType' => $invoiceType,
'pay_status' => $payStatus,
]);
$result = $this->decodeResponse($response, 'miniapi order list');
if (empty($result) || intval($result['code'] ?? 0) !== 1) {
return $this->ok([]);
}
$orders = array_map(static function ($item) use ($business) {
if (is_array($item)) {
$item['business'] = $business;
$item['business_text'] = $business === 'esta' ? 'ESTA' : 'EVUS';
}
return $item;
}, (array)($result['data'] ?? []));
return $this->ok($orders);
}
public function detail()
{
$userId = $this->requireLogin();
if (!is_int($userId)) {
return $userId;
}
$orderSn = (string)$this->request->param('order_sn', '');
if ($orderSn === '') {
return $this->fail('参数错误');
}
$business = $this->businessCode((string)$this->request->param('business', 'evus'));
$data = $this->fetchOrderDetail($orderSn, $business, $userId);
if (isset($data['error'])) {
return $this->fail($data['error'], intval($data['code'] ?? 0));
}
return $this->ok($data);
}
public function pay()
{
$userId = $this->requireLogin();
if (!is_int($userId)) {
return $userId;
}
$data = $this->postData();
$orderSn = trim((string)($data['order_sn'] ?? ''));
$business = $this->businessCode((string)($data['business'] ?? 'evus'));
$openid = trim((string)($data['openid'] ?? ''));
if ($orderSn === '' || $openid === '') {
return $this->fail('参数错误');
}
$detail = $this->fetchOrderDetail($orderSn, $business, $userId);
if (isset($detail['error'])) {
return $this->fail($detail['error'], intval($detail['code'] ?? 0));
}
$money = $this->payMoney($detail);
if ($money === '') {
return $this->fail('订单金额异常');
}
$pay = (new MiniPayService())->request(
$this->currentMiniAppContext($business),
$orderSn,
$money,
$openid,
'miniapi order mini pay'
);
if ($pay['code'] !== 1) {
return $this->fail($pay['msg']);
}
return $this->ok([
'order_sn' => $orderSn,
'pay' => $pay['data'],
], 'ok');
}
private function fetchOrderDetail(string $orderSn, string $business, int $userId): array
{
$model = $this->businessModel($business);
$detailUrl = $business === 'esta' ? config('app.get_order_detail_url') : config('app.get_evus_detail_url');
if (empty($detailUrl)) {
return ['error' => '订单详情接口未配置'];
}
$response = Httpcurl::request($detailUrl, 'post', [
'order_sn' => $orderSn,
'model' => $model,
'user_id' => $userId,
]);
$result = $this->decodeResponse($response, 'miniapi order detail');
if (empty($result) || intval($result['code'] ?? 0) !== 1) {
return ['error' => $result['msg'] ?? '订单不存在'];
}
$data = $result['data'] ?? [];
$orderUserId = intval($data['user_id'] ?? $data['order']['user_id'] ?? 0);
if ($orderUserId > 0 && $orderUserId !== $userId) {
return ['error' => '无权访问该订单', 'code' => 403];
}
if (is_array($data)) {
$data['business'] = $business;
$data['business_text'] = $business === 'esta' ? 'ESTA' : 'EVUS';
}
return is_array($data) ? $data : [];
}
private function businessCode(string $business): string
{
$business = strtolower(trim($business));
return $business === 'esta' ? 'esta' : 'evus';
}
private function businessModel(string $business): string
{
return $business === 'esta' ? ESTA_MODEL : MODEL;
}
private function businessInvoiceType(string $business): string
{
return $business === 'esta' ? ESTA_INCOICETYPE : INCOICETYPE;
}
private function payMoney(array $detail): string
{
$scopes = [$detail];
if (isset($detail['order']) && is_array($detail['order'])) {
$scopes[] = $detail['order'];
}
foreach ($scopes as $scope) {
foreach (['total_price', 'money', 'pay_money', 'order_amount', 'amount', 'product_value', 'origin_price'] as $field) {
$money = $this->cleanMoney($scope[$field] ?? '');
if ($money !== '') {
return $money;
}
}
}
return '';
}
private function cleanMoney($value): string
{
if (is_int($value) || is_float($value)) {
return (string)$value;
}
$value = trim((string)$value);
if ($value === '') {
return '';
}
$value = str_replace([',', '¥', '¥', '元', ' '], '', $value);
return preg_match('/^\d+(\.\d+)?$/', $value) ? $value : '';
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare (strict_types=1);
namespace app\miniapi\controller;
use app\service\SwooleService;
class Upload extends Base
{
public function image()
{
$file = request()->file('file');
if (!$file) {
return $this->fail('请上传图片');
}
$path = $file->getPathname();
if (!is_file($path)) {
return $this->fail('上传文件读取失败');
}
$size = filesize($path);
if ($size === false || $size <= 0) {
return $this->fail('上传文件为空');
}
if ($size > 10 * 1024 * 1024) {
return $this->fail('图片不能超过10MB');
}
$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->fail('请上传图片文件');
}
$mime = function_exists('mime_content_type') ? (string)mime_content_type($path) : '';
if ($mime !== '' && strpos($mime, 'image/') !== 0) {
return $this->fail('请上传图片文件');
}
$content = file_get_contents($path);
if ($content === false || $content === '') {
return $this->fail('上传文件读取失败');
}
$fileName = date('YmdHis') . uniqid('', true) . '.' . $ext;
try {
$client = SwooleService::getInstance();
$sent = $client->sendTask([
'type' => 'upload_ali',
'data' => [
'Bucket' => BUCKET,
'Key' => $fileName,
'file_content' => base64_encode($content),
],
]);
$client->close();
if (!$sent) {
return $this->fail('上传失败,请重试');
}
$url = OSS_URL . $fileName;
return $this->ok([
'path' => $url,
'url' => $url,
], '上传成功');
} catch (\Throwable $e) {
return $this->fail('上传失败,请重试');
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare (strict_types=1);
namespace app\miniapi\service\Context;
class MiniAppContext
{
private $miniAppCode;
private $companyCode;
private $businessCode;
private $siteDomain;
private $miniApp;
private $site;
public function __construct(array $data)
{
$this->miniAppCode = (string)($data['mini_app_code'] ?? '');
$this->companyCode = (string)($data['company_code'] ?? '');
$this->businessCode = (string)($data['business_code'] ?? '');
$this->siteDomain = (string)($data['site_domain'] ?? '');
$this->miniApp = is_array($data['mini_app'] ?? null) ? $data['mini_app'] : [];
$this->site = is_array($data['site'] ?? null) ? $data['site'] : [];
}
public function miniAppCode(): string
{
return $this->miniAppCode;
}
public function companyCode(): string
{
return $this->companyCode;
}
public function businessCode(): string
{
return $this->businessCode;
}
public function siteDomain(): string
{
return $this->siteDomain;
}
public function miniApp(): array
{
return $this->miniApp;
}
public function site(): array
{
return $this->site;
}
public function toArray(): array
{
return [
'mini_app_code' => $this->miniAppCode,
'company_code' => $this->companyCode,
'business_code' => $this->businessCode,
'site_domain' => $this->siteDomain,
'mini_app' => $this->miniApp,
'site' => $this->site,
];
}
}

View File

@@ -0,0 +1,144 @@
<?php
declare (strict_types=1);
namespace app\miniapi\service\Legal;
use app\miniapi\service\Context\MiniAppContext;
use think\facade\Config;
class LegalDocumentService
{
public function get(MiniAppContext $context, string $type): array
{
$type = strtolower(trim($type));
$businessCode = $context->businessCode() ?: (string)Config::get('mini_legal.default_business_code', 'evus');
$business = (array)Config::get('mini_legal.businesses.' . $businessCode, []);
$companyCode = $this->resolveCompanyCode($business, $context);
$document = $this->resolveDocument($business, $context, $companyCode, $type);
if (empty($document)) {
return [];
}
$company = $this->resolveCompany($business, $context, $companyCode);
$title = $this->resolveText($document, 'title', 'title_key', $type === 'privacy' ? '隐私政策' : '条款与条件');
$body = $this->resolveText($document, 'content', 'content_key', '');
$content = $this->plainText($this->buildContent($document, $company, $body));
return [
'type' => $type,
'business_code' => $businessCode,
'mini_app_code' => $context->miniAppCode(),
'company_code' => $companyCode,
'company_name' => $company['company_name'],
'site_domain' => $company['site_domain'],
'title' => $title,
'content' => $content,
];
}
private function resolveDocument(array $business, MiniAppContext $context, string $companyCode, string $type): array
{
$document = (array)($business['documents'][$type] ?? []);
if (empty($document)) {
return [];
}
$company = (array)($business['companies'][$companyCode] ?? []);
$app = (array)($business['apps'][$context->miniAppCode()] ?? []);
$companyDocument = (array)($company['documents'][$type] ?? []);
$appDocument = (array)($app['documents'][$type] ?? []);
return array_replace_recursive($document, $companyDocument, $appDocument);
}
private function resolveCompany(array $business, MiniAppContext $context, string $companyCode): array
{
$site = $context->site();
$company = (array)($business['companies'][$companyCode] ?? []);
$app = (array)($business['apps'][$context->miniAppCode()] ?? []);
$company = array_replace_recursive($company, (array)($app['company'] ?? []));
return [
'company_name' => trim((string)($company['company_name'] ?? '')) ?: trim((string)($site['company_name'] ?? '')) ?: 'EVUS登记服务',
'site_domain' => trim((string)($company['site_domain'] ?? '')) ?: $context->siteDomain(),
'phone' => trim((string)($company['phone'] ?? '')) ?: trim((string)($site['phone'] ?? '')),
];
}
private function resolveCompanyCode(array $business, MiniAppContext $context): string
{
$app = (array)($business['apps'][$context->miniAppCode()] ?? []);
return trim((string)($app['company_code'] ?? '')) ?: $context->companyCode();
}
private function resolveText(array $document, string $literalKey, string $langKey, string $fallback): string
{
$literal = trim((string)($document[$literalKey] ?? ''));
if ($literal !== '') {
return $literal;
}
$key = trim((string)($document[$langKey] ?? ''));
if ($key === '') {
return $fallback;
}
$value = (string)lang($key);
return trim($value) !== '' ? $value : $fallback;
}
private function buildContent(array $document, array $company, string $body): string
{
$companyName = $company['company_name'] ?: 'EVUS登记服务';
$siteUrl = $this->siteUrl((string)($company['site_domain'] ?? ''));
$phone = $company['phone'] ?: '请通过小程序客服入口联系我们';
$intro = $this->replaceTokens((string)($document['intro_template'] ?? ''), $company, $siteUrl, $phone);
$subjectLabel = trim((string)($document['subject_label'] ?? '服务主体'));
$prefix = [
$intro,
$subjectLabel . '' . $companyName,
$siteUrl ? '适用网站:' . $siteUrl : '',
'联系方式:' . $phone,
];
$prefix = array_values(array_filter($prefix, static function ($line) {
return trim((string)$line) !== '';
}));
return implode("\n", $prefix) . "\n\n" . $body;
}
private function replaceTokens(string $text, array $company, string $siteUrl, string $phone): string
{
return strtr($text, [
'{company_name}' => (string)($company['company_name'] ?? ''),
'{site_url}' => $siteUrl,
'{phone}' => $phone,
]);
}
private function siteUrl(string $siteDomain): string
{
$siteDomain = trim($siteDomain);
if ($siteDomain === '') {
return '';
}
return preg_match('/^https?:\/\//i', $siteDomain) ? rtrim($siteDomain, '/') : 'https://' . rtrim($siteDomain, '/');
}
private function plainText(string $html): string
{
$html = preg_replace('/<\s*(h[1-6]|div|section|li)\b[^>]*>/i', "\n", $html);
$html = preg_replace('/<\s*\/\s*(h[1-6]|div|section|li)\s*>/i', "\n", (string)$html);
$text = preg_replace('/<\s*br\s*\/?\s*>/i', "\n", (string)$html);
$text = preg_replace('/<\s*\/p\s*>/i', "\n", (string)$text);
$text = strip_tags((string)$text);
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$text = preg_replace("/[ \t\r]+/", ' ', (string)$text);
$text = preg_replace("/\n{3,}/", "\n\n", (string)$text);
return trim((string)$text);
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare (strict_types=1);
namespace app\miniapi\service\Payment;
use api\Httpcurl;
use app\miniapi\service\Context\MiniAppContext;
use think\facade\Log;
class MiniPayService
{
public function request(MiniAppContext $context, string $orderSn, $money, string $openid, string $scene = 'miniapi mini pay'): array
{
$payConfig = (new PaymentChannelService())->miniPay($context);
if ($payConfig['url'] === '') {
return [
'code' => 0,
'msg' => '支付配置未完成',
'data' => [],
];
}
$response = Httpcurl::request($payConfig['url'], 'post', [
'order_sn' => $orderSn,
'body' => $payConfig['body'],
'money' => $money,
'openid' => $openid,
'attach' => $payConfig['attach'],
]);
$result = $this->decodeResponse($response, $scene);
if (empty($result) || intval($result['code'] ?? 0) !== 200) {
return [
'code' => 0,
'msg' => $result['msg'] ?? '支付参数获取失败',
'data' => [],
];
}
if (isset($result['data']['package'])) {
$result['data']['package'] = urlencode($result['data']['package']);
}
return [
'code' => 1,
'msg' => 'ok',
'data' => $result['data'],
];
}
private function decodeResponse($response, string $scene): array
{
if (!is_array($response) || !isset($response[0]) || !empty($response[3])) {
Log::error($scene . ' request failed: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
return [];
}
$result = json_decode($response[0], true);
if (!is_array($result)) {
Log::error($scene . ' response decode failed: ' . $response[0]);
return [];
}
return $result;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare (strict_types=1);
namespace app\miniapi\service\Payment;
use app\miniapi\service\Context\MiniAppContext;
use think\facade\Config;
class PaymentChannelService
{
public function miniPay(MiniAppContext $context): array
{
$businessCode = $context->businessCode() ?: (string)Config::get('mini_payment.default_business_code', 'evus');
$business = (array)Config::get('mini_payment.businesses.' . $businessCode, []);
$companyCode = $this->resolveCompanyCode($business, $context);
$config = (array)($business['mini_pay'] ?? []);
$company = (array)($business['companies'][$companyCode] ?? []);
$app = (array)($business['apps'][$context->miniAppCode()] ?? []);
$config = array_replace_recursive($config, (array)($company['mini_pay'] ?? []), (array)($app['mini_pay'] ?? []));
return [
'url' => trim((string)($config['url'] ?? '')),
'body' => trim((string)($config['body'] ?? 'EVUS登记服务费')) ?: 'EVUS登记服务费',
'attach' => trim((string)($config['attach'] ?? 'msg')) ?: 'msg',
];
}
private function resolveCompanyCode(array $business, MiniAppContext $context): string
{
$app = (array)($business['apps'][$context->miniAppCode()] ?? []);
return trim((string)($app['company_code'] ?? '')) ?: $context->companyCode();
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare (strict_types=1);
namespace app\miniapi\service\Product;
use app\miniapi\service\Context\MiniAppContext;
use think\facade\Config;
class ProductService
{
public function list(MiniAppContext $context): array
{
$businessCode = $context->businessCode() ?: (string)Config::get('mini_product.default_business_code', 'evus');
$business = (array)Config::get('mini_product.businesses.' . $businessCode, []);
$products = $this->resolveConfiguredProducts($business, $context);
if (empty($products)) {
$legacyKey = (string)($business['legacy_config_key'] ?? '');
$products = $legacyKey !== '' ? (array)Config::get('app.' . $legacyKey, []) : [];
}
return $this->normalizeProducts($products);
}
public function values(MiniAppContext $context): array
{
$products = $this->list($context);
return array_values(array_filter(array_map(static function ($item) {
return isset($item['value']) ? (string)$item['value'] : '';
}, $products), static function ($value) {
return $value !== '';
}));
}
public function isValidValue(MiniAppContext $context, $value): bool
{
$values = $this->values($context);
return empty($values) || in_array((string)$value, $values, true);
}
public function findByValue(MiniAppContext $context, $value): array
{
$value = (string)$value;
foreach ($this->list($context) as $product) {
if ((string)($product['value'] ?? '') === $value) {
return $product;
}
}
return [];
}
private function resolveConfiguredProducts(array $business, MiniAppContext $context): array
{
$companyCode = $this->resolveCompanyCode($business, $context);
$company = (array)($business['companies'][$companyCode] ?? []);
$app = (array)($business['apps'][$context->miniAppCode()] ?? []);
$products = (array)($company['products'] ?? []);
if (!empty($app['products']) && is_array($app['products'])) {
$products = (array)$app['products'];
}
return $products;
}
private function resolveCompanyCode(array $business, MiniAppContext $context): string
{
$app = (array)($business['apps'][$context->miniAppCode()] ?? []);
return trim((string)($app['company_code'] ?? '')) ?: $context->companyCode();
}
private function normalizeProducts(array $products): array
{
return array_values(array_filter(array_map(function ($item) {
if (!is_array($item)) {
return null;
}
$value = isset($item['value']) ? (string)$item['value'] : '';
if ($value === '') {
return null;
}
return [
'value' => $value,
'desc' => (string)($item['desc'] ?? ''),
'is_checked' => (string)($item['is_checked'] ?? '0'),
'name' => (string)($item['name'] ?? ''),
];
}, $products)));
}
}

9
app/provider.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
use app\ExceptionHandle;
use app\Request;
// 容器Provider定义文件
return [
'think\Request' => Request::class,
'think\exception\Handle' => ExceptionHandle::class,
];

9
app/service.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
use app\AppService;
// 系统服务定义文件
// 服务在完成全局初始化之后执行
return [
AppService::class,
];

View File

@@ -0,0 +1,126 @@
<?php
namespace app\service;
use Aws\S3\S3Client;
use think\facade\Log;
use think\File;
class AwsUploadService
{
/**
* 处理表单文件上传并转发到目标服务器
*
* @param string $fileField 表单文件字段名
* @param array $extraFields 额外传递的字段
* @return array 包含上传结果的数组
*/
public function uploadAndForward( File $file, string $fileField,array $extraFields = []): array
{
$mimeType = $file->getMime();
$s3config = config('filesystem.disks.s3');
$s3 = new S3Client($s3config);
try {
$config = config('app.upload_conf');
$configs=[];
foreach ($config as $key=>$con){
$configs[] = $key.':'.$con;
}
// 验证文件
$validate = validate([
$fileField =>implode('|',$configs),
]);
if (!$validate->check([$fileField => $file])) {
return $this->createErrorResult($validate->getError());
}
// 根据文件类型设置 Content-Type
if (in_array($mimeType, config('app.imageMimeTypes'))) {
$contentType = $mimeType; // 使用图片的实际 MIME 类型
} else {
$contentType = 'application/octet-stream'; // 非图片文件使用默认类型
}
$checkBucket = $this->checkBucket($s3, $extraFields['Bucket']);
if (!$checkBucket) {
return $this->createErrorResult('Bucket error');
}
$s3->putObject([
'Bucket' => $extraFields['Bucket'],
'Key' => $extraFields['Key'],
'Body' => fopen($file->getRealPath(), 'r'),
'ACL' => 'public-read',
'ContentType' => $contentType, // 设置检测到的 Content-Type
]);
return [
'code' => 1,
'data' =>[
'savename'=>$s3config['endpoint'].'/'.$extraFields['Bucket'].'/'.$extraFields['Key']
] ,
];
} catch (\Exception $e) {
return $this->createErrorResult('上传过程中发生异常: ' . $e->getMessage());
}
}
public function checkBucket($s3, $bucket)
{
try {
$buckets = $s3->listBuckets()->get('Buckets');
$buckets_arr = array_column($buckets, 'Name');
if (!in_array($bucket, $buckets_arr)) {
$s3->createBucket([
'Bucket' => $bucket,
]);
$policy = [
'Version' => '2012-10-17',
'Statement' => [
[
'Effect' => 'Allow',
'Principal' => ['AWS' => ['*']],
'Action' => [
's3:GetBucketLocation',
's3:ListBucket',
's3:ListBucketMultipartUploads',
],
'Resource' => "arn:aws:s3:::$bucket",
],
[
'Effect' => 'Allow',
'Principal' => ['AWS' => ['*']],
'Action' => [
's3:DeleteObject',
's3:GetObject',
's3:ListMultipartUploadParts',
's3:PutObject',
's3:AbortMultipartUpload',
],
'Resource' => "arn:aws:s3:::$bucket/*",
],
],
];
$s3->putBucketPolicy([
'Bucket' => $bucket,
'Policy' => json_encode($policy),
]);
}
return true;
} catch (\Exception $e) {
Log::info('checkBucket error:' . $e->getMessage());
return false;
}
}
/**
* 创建错误结果数组
*
* @param string $message 错误信息
* @return array 错误结果
*/
private function createErrorResult(string $message): array
{
return [
'code' => 0,
'error' => $message
];
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace app\service;
use Curl\Curl;
class BaiduOcrService
{
private $url = 'https://uniuser.evus.cn/home/upload/';
/**
*
*护照识别
* @param string $fileData 图片的二进制数据
* @return array
*/
function passport($fileData)
{
if ($fileData === false) {
return [
'code' => 0,
'msg' => '无法读取上传的文件',
];
}
$curl = new Curl();
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, false);
$curl->setOpt(CURLOPT_SSL_VERIFYHOST, 0);
$curl->post($this->url.'passport', [
'image' => $fileData,
]);
if ($curl->error) {
return [
'code' => 0,
'msg' => $curl->errorMessage,
];
} else {
if (!$curl->response->code){
return [
'code' => 0,
'msg' => '解析失败',
];
}
return [
'code' => 1,
'data' => $curl->response->data,
];
}
}
/**
*
*身份证识别
* @param string $fileData 图片的二进制数据
* @return array
*/
function identification($fileData)
{
if ($fileData === false) {
return [
'code' => 0,
'msg' => '无法读取上传的文件',
];
}
$curl = new Curl();
$curl->post($this->url.'identification', [
'image' => $fileData,
]);
if ($curl->error) {
return [
'code' => 0,
'msg' => $curl->errorMessage,
];
} else {
if (!$curl->response->code){
return [
'code' => 0,
'msg' => '解析失败',
];
}
return [
'code' => 1,
'data' => $curl->response->data,
];
}
}
/**
*
*护照识别
* @param string $fileData 图片的二进制数据
* @return array 包含压缩后图片信息的数组
*/
function visa($fileData)
{
if ($fileData === false) {
return [
'code' => 0,
'msg' => '无法读取上传的文件',
];
}
$curl = new Curl();
$curl->post($this->url.'visa', [
'image' => $fileData,
]);
if ($curl->error) {
return [
'code' => 0,
'msg' => $curl->errorMessage,
];
} else {
if (!$curl->response->code){
return [
'code' => 0,
'msg' => '解析失败',
];
}
return [
'code' => 1,
'data' => $curl->response->data,
];
}
}
}

View File

@@ -0,0 +1,185 @@
<?php
namespace app\service;
/**
* 压缩二进制流图片
*/
class CompressImgService
{
/**
* 压缩通过表单上传的图片
*
* @param string $fileData 图片的二进制数据
* @param int $quality 压缩质量0-100仅适用于JPEG和WebP
* @param string|null $targetFormat 目标格式('jpeg', 'png', 'webp',默认使用原格式)
* @return array 包含压缩后图片信息的数组
*/
function compress($fileData, $quality = 80, $targetFormat = null)
{
// 读取上传的二进制数据
$imageData = file_get_contents($fileData);
if ($imageData === false) {
return [
'code' => 0,
'message' => '无法读取上传的文件',
];
}
// 获取图片信息
$imageInfo = getimagesizefromstring($imageData);
if (!$imageInfo) {
return [
'code' => 0,
'message' => '无法解析图片信息',
];
}
// 确定原始和目标格式
$originalMimeType = $imageInfo['mime'];
$originalExtension = image_type_to_extension($imageInfo[2], false);
if ($targetFormat === null) {
$targetFormat = $originalExtension;
}
// 创建图片资源
switch (strtolower($originalExtension)) {
case 'jpeg':
case 'jpg':
$image = imagecreatefromstring($imageData);
break;
case 'png':
$image = imagecreatefromstring($imageData);
break;
case 'webp':
$image = imagecreatefromstring($imageData);
break;
default:
return [
'code' => 0,
'message' => '不支持的图片格式',
];
}
if (!$image) {
return [
'code' => 0,
'message' => '无法创建图片资源',
];
}
// 压缩并转换图片
ob_start();
switch (strtolower($targetFormat)) {
case 'jpeg':
case 'jpg':
imagejpeg($image, null, $quality);
$compressedMimeType = 'image/jpeg';
break;
case 'png':
$pngQuality = 9 - floor($quality / 11);
imagepng($image, null, $pngQuality);
$compressedMimeType = 'image/png';
break;
case 'webp':
if (function_exists('imagewebp')) {
imagewebp($image, null, $quality);
$compressedMimeType = 'image/webp';
} else {
imagejpeg($image, null, $quality);
$compressedMimeType = 'image/jpeg';
}
break;
default:
imagedestroy($image);
ob_end_clean();
return [
'code' => 0,
'message' => "不支持的目标格式: {$targetFormat}",
];
}
$compressedData = ob_get_clean();
imagedestroy($image);
// 返回结果
return [
'code' => 1,
'compressed_data' => $compressedData,
'base64' => 'data:' . $compressedMimeType . ';base64,' . base64_encode($compressedData),
];
}
public function compressImage($filePath, $fileSize)
{
// 获取图片信息
$imageInfo = getimagesize($filePath);
if (!$imageInfo) {
return false;
}
$mime = $imageInfo['mime'];
// 根据MIME类型创建图像资源
switch ($mime) {
case 'image/jpeg':
$image = imagecreatefromjpeg($filePath);
break;
case 'image/png':
$image = imagecreatefrompng($filePath);
break;
case 'image/gif':
$image = imagecreatefromgif($filePath);
break;
default:
return false;
}
if (!$image) {
return false;
}
// 计算压缩质量(根据文件大小动态调整)
$originalQuality = 85; // 初始质量
$targetSize = 2.5 * 1024 * 1024; // 目标大小2.5MB
// 如果文件很大,降低初始质量
if ($fileSize > 5 * 1024 * 1024) {
$quality = 75;
} elseif ($fileSize > 10 * 1024 * 1024) {
$quality = 65;
} else {
$quality = $originalQuality;
}
// 创建临时文件
$tempFile = tempnam(sys_get_temp_dir(), 'compressed_');
// 保存压缩后的图片
switch ($mime) {
case 'image/jpeg':
imagejpeg($image, $tempFile, $quality);
break;
case 'image/png':
// PNG使用压缩级别0-9需要转换
$pngQuality = 9 - round(($quality / 100) * 9);
imagepng($image, $tempFile, $pngQuality);
break;
case 'image/gif':
imagegif($image, $tempFile);
break;
}
// 释放内存
imagedestroy($image);
// 检查压缩后文件大小如果仍然大于3MB继续压缩
$compressedSize = filesize($tempFile);
if ($compressedSize > 3 * 1024 * 1024) {
// 递归压缩直到满足要求
return $this->compressImage($tempFile, $compressedSize);
}
return $tempFile;
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare (strict_types=1);
namespace app\service;
use api\Httpcurl;
use think\facade\Cache;
use think\facade\Log;
class MiniProgramWechatService
{
private string $miniAppCode;
private array $miniApp;
public function __construct(string $miniAppCode, array $miniApp)
{
$this->miniAppCode = $miniAppCode;
$this->miniApp = $miniApp;
}
public function openidByLoginCode(string $loginCode, bool $required = false): string
{
$config = $this->wechatConfig();
if ($loginCode === '' || $config['appid'] === '' || $config['secret'] === '') {
return '';
}
$response = Httpcurl::request('https://api.weixin.qq.com/sns/jscode2session', 'get', [
'appid' => $config['appid'],
'secret' => $config['secret'],
'js_code' => $loginCode,
'grant_type' => 'authorization_code',
], [], 8);
$result = $this->decodeWechatResponse($response, 'miniapi wx code2session');
if (!empty($result['openid'])) {
return (string)$result['openid'];
}
if ($required) {
Log::warning('miniapi wx openid missing: ' . json_encode($result, JSON_UNESCAPED_UNICODE));
}
return '';
}
public function mobileByPhoneCode(string $phoneCode): string
{
$accessToken = $this->wechatAccessToken();
if ($phoneCode === '' || $accessToken === '') {
return '';
}
$response = Httpcurl::request(
'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=' . urlencode($accessToken),
'post',
json_encode(['code' => $phoneCode], JSON_UNESCAPED_UNICODE),
['Content-Type: application/json'],
8
);
$result = $this->decodeWechatResponse($response, 'miniapi wx get phone number');
$phoneInfo = $result['phone_info'] ?? [];
return (string)($phoneInfo['purePhoneNumber'] ?? $phoneInfo['phoneNumber'] ?? '');
}
private function wechatAccessToken(): string
{
$config = $this->wechatConfig();
if ($config['appid'] === '' || $config['secret'] === '') {
Log::warning('miniapi wx config missing: ' . $this->miniAppCode);
return '';
}
$cacheKey = 'miniapp_access_token_' . $this->miniAppCode;
$cached = Cache::get($cacheKey);
if (!empty($cached)) {
return (string)$cached;
}
$response = Httpcurl::request('https://api.weixin.qq.com/cgi-bin/token', 'get', [
'grant_type' => 'client_credential',
'appid' => $config['appid'],
'secret' => $config['secret'],
], [], 8);
$result = $this->decodeWechatResponse($response, 'miniapi wx access token');
if (empty($result['access_token'])) {
return '';
}
$ttl = max(60, intval($result['expires_in'] ?? 7200) - 300);
Cache::set($cacheKey, $result['access_token'], $ttl);
return (string)$result['access_token'];
}
private function wechatConfig(): array
{
return [
'appid' => trim((string)($this->miniApp['wechat_appid'] ?? '')),
'secret' => trim((string)($this->miniApp['wechat_secret'] ?? '')),
];
}
private function decodeWechatResponse($response, string $scene): array
{
$result = $this->decodeResponse($response, $scene);
if (isset($result['errcode']) && intval($result['errcode']) !== 0) {
Log::warning($scene . ' failed: ' . json_encode($result, JSON_UNESCAPED_UNICODE));
return [];
}
return $result;
}
private function decodeResponse($response, string $scene): array
{
if (!is_array($response) || !isset($response[0]) || !empty($response[3])) {
Log::error($scene . ' request failed: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
return [];
}
$result = json_decode($response[0], true);
if (!is_array($result)) {
Log::error($scene . ' response decode failed: ' . $response[0]);
return [];
}
return $result;
}
}

301
app/service/OssService.php Normal file
View File

@@ -0,0 +1,301 @@
<?php
namespace app\service;
use AlibabaCloud\Oss\V2 as Oss;
use Exception;
final class OssService
{
private Oss\Client $client;
private string $bucket;
private string $region;
private ?string $endpoint;
/**
* @param array{
* region: string,
* bucket: string,
* endpoint?: string,
* access_key_id?: string,
* access_key_secret?: string,
* session_token?: string
* } $config
*/
public function __construct(array $config)
{
$this->region = $config['region'] ?? '';
$this->bucket = $config['bucket'] ?? '';
$this->endpoint = $config['endpoint'] ?? null;
if ($this->region === '' || $this->bucket === '') {
throw new Exception('OSS config missing: region/bucket');
}
if (!empty($config['access_key_id']) && !empty($config['access_key_secret'])) {
$credentialsProvider = new FixedCredentialsProvider(
(string)$config['access_key_id'],
(string)$config['access_key_secret'],
$config['session_token'] ?? null
);
} else {
$credentialsProvider = new Oss\Credentials\EnvironmentVariableCredentialsProvider();
}
$cfg = Oss\Config::loadDefault();
$cfg->setCredentialsProvider($credentialsProvider);
$cfg->setRegion($this->region);
if ($this->endpoint) {
$cfg->setEndpoint($this->endpoint);
if ($this->shouldUseCname($this->endpoint)) {
$cfg->setUseCname(true);
}
}
$this->client = new Oss\Client($cfg);
$this->assertCredentialsVisible($config);
}
public function upload(string $source, string $key, ?string $bucket = null, array $options = []): array
{
if ($this->isBase64DataUri($source)) {
return $this->uploadFromBase64($source, $key, $bucket, $options);
}
if ($this->isUrl($source)) {
return $this->uploadFromUrl($source, $key, $bucket, $options);
}
return $this->uploadFromFile($source, $key, $bucket, $options);
}
public function uploadFromBase64(string $base64, string $key, ?string $bucket = null, array $options = []): array
{
$contentType = null;
$payload = $base64;
if (preg_match('/^data:([a-zA-Z0-9.+\\/-]+);base64,(.*)$/s', $base64, $m)) {
$contentType = strtolower((string)$m[1]);
$payload = (string)$m[2];
}
$payload = preg_replace('/\\s+/', '', $payload ?? '');
if ($payload === '') {
throw new Exception('Base64 payload is empty');
}
$binary = base64_decode($payload, true);
if ($binary === false) {
throw new Exception('Invalid base64 payload');
}
if ($contentType && empty($options['contentType'])) {
$options['contentType'] = $contentType;
}
return $this->uploadFromBinary($binary, $key, $bucket, $options);
}
public function uploadFromBinary(string $binary, string $key, ?string $bucket = null, array $options = []): array
{
if ($binary === '') {
throw new Exception('Binary payload is empty');
}
$stream = fopen('php://temp', 'wb+');
if ($stream === false) {
throw new Exception('Unable to create temporary stream for binary upload');
}
fwrite($stream, $binary);
rewind($stream);
return $this->putObjectFromResource($stream, $key, $bucket, $options);
}
public function uploadFromFile(string $filePath, string $key, ?string $bucket = null, array $options = []): array
{
if (!is_file($filePath)) {
throw new Exception("Local file not found: {$filePath}");
}
$fp = fopen($filePath, 'rb');
if ($fp === false) {
throw new Exception("Unable to open local file: {$filePath}");
}
return $this->putObjectFromResource($fp, $key, $bucket, $options);
}
public function uploadFromUrl(string $url, string $key, ?string $bucket = null, array $options = []): array
{
$fp = @fopen($url, 'rb');
if ($fp !== false) {
return $this->putObjectFromResource($fp, $key, $bucket, $options);
}
$tmp = tmpfile();
if ($tmp === false) {
throw new Exception('Unable to create tmpfile() for remote download');
}
$this->downloadToStreamByCurl($url, $tmp);
rewind($tmp);
return $this->putObjectFromResource($tmp, $key, $bucket, $options);
}
public function buildObjectUrl(string $key, ?string $bucket = null, bool $useHttps = true): string
{
$bucket = $bucket ?: $this->bucket;
$key = ltrim($key, '/');
$endpoint = $this->endpoint ?: sprintf('oss-%s.aliyuncs.com', $this->region);
$host = $this->extractEndpointHost($endpoint);
$scheme = $useHttps ? 'https' : 'http';
$encodedKey = implode('/', array_map('rawurlencode', explode('/', $key)));
if ($this->shouldUseCname($endpoint)) {
return sprintf('%s://%s/%s', $scheme, $host, $encodedKey);
}
return sprintf('%s://%s.%s/%s', $scheme, $bucket, $host, $encodedKey);
}
private function putObjectFromResource($resource, string $key, ?string $bucket = null, array $options = []): array
{
$bucket = $bucket ?: $this->bucket;
$key = ltrim($key, '/');
$metadata = $options['metadata'] ?? [];
$contentType = $options['contentType'] ?? null;
$acl = $options['acl'] ?? null;
$body = Oss\Utils::streamFor($resource);
$request = new Oss\Models\PutObjectRequest(
bucket: $bucket,
key: $key,
metadata: $metadata ?: null,
contentType: $contentType,
objectAcl: $acl
);
$request->body = $body;
$result = $this->client->putObject($request);
return [
'statusCode' => $result->statusCode,
'requestId' => $result->requestId,
'etag' => $result->etag,
'bucket' => $bucket,
'key' => $key,
];
}
private function downloadToStreamByCurl(string $url, $destStream): void
{
if (!function_exists('curl_init')) {
throw new Exception('cURL extension is required when allow_url_fopen is disabled.');
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_FILE => $destStream,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 60,
CURLOPT_FAILONERROR => true,
CURLOPT_USERAGENT => 'OssService/1.0',
]);
$ok = curl_exec($ch);
if ($ok !== true) {
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
throw new Exception("Failed to download remote file. HTTP={$code}, error={$err}");
}
curl_close($ch);
}
private function isUrl(string $value): bool
{
return (bool)filter_var($value, FILTER_VALIDATE_URL);
}
private function isBase64DataUri(string $value): bool
{
return (bool)preg_match('/^data:[a-zA-Z0-9.+\\/-]+;base64,/i', $value);
}
private function assertCredentialsVisible(array $config): void
{
if (!empty($config['access_key_id']) && !empty($config['access_key_secret'])) {
return;
}
$ak = getenv('OSS_ACCESS_KEY_ID');
$sk = getenv('OSS_ACCESS_KEY_SECRET');
if (empty($ak) || empty($sk)) {
throw new Exception(
"OSS credentials missing in PHP process env. " .
"getenv('OSS_ACCESS_KEY_ID')/getenv('OSS_ACCESS_KEY_SECRET') is empty. " .
"Fix: make sure the PHP runtime process can see env vars, or pass access_key_id/access_key_secret into OssService config."
);
}
}
private function shouldUseCname(string $endpoint): bool
{
$host = strtolower($this->extractEndpointHost($endpoint));
if ($host === '' || str_contains($host, 'aliyuncs.com') || str_contains($host, '.oss-')) {
return false;
}
return true;
}
private function extractEndpointHost(string $endpoint): string
{
$normalized = preg_match('#^https?://#i', $endpoint) ? $endpoint : 'https://' . ltrim($endpoint, '/');
return (string)(parse_url($normalized, PHP_URL_HOST) ?: '');
}
public function presignGetObjectUrl(string $key, ?string $bucket = null, int $expireSeconds = 900): array
{
$bucket = $bucket ?: $this->bucket;
$key = ltrim($key, '/');
$request = new Oss\Models\GetObjectRequest(bucket: $bucket, key: $key);
$result = $this->client->presign($request, [
'expires' => new \DateInterval("PT{$expireSeconds}S"),
]);
return [
'url' => $result->url,
'expire' => $expireSeconds,
'bucket' => $bucket,
'key' => $key,
];
}
}
final class FixedCredentialsProvider implements Oss\Credentials\CredentialsProvider
{
private string $ak;
private string $sk;
private ?string $token;
public function __construct(string $ak, string $sk, ?string $token = null)
{
$this->ak = $ak;
$this->sk = $sk;
$this->token = $token;
}
public function getCredentials(): Oss\Credentials\Credentials
{
return new Oss\Credentials\Credentials($this->ak, $this->sk, $this->token);
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace app\service;
use Throwable;
class OssUploadService
{
public function uploadBase64(string $fileName, string $base64File, array $extraFields = []): array
{
$fileContent = base64_decode($base64File, true);
if ($fileContent === false || $fileContent === '') {
return $this->createErrorResult('文件解析错误');
}
return $this->uploadBinaryContent($fileName, $fileContent, $extraFields);
}
public function uploadBinary(string $fileName, string $fileContent, array $extraFields = []): array
{
if ($fileContent === '') {
return $this->createErrorResult('Binary payload is empty');
}
return $this->uploadBinaryContent($fileName, $fileContent, $extraFields);
}
private function uploadBinaryContent(string $fileName, string $fileContent, array $extraFields = []): array
{
$uploadConfig = config('filesystem.upload_conf') ?: [];
$maxSize = (int)($uploadConfig['fileSize'] ?? 10 * 1024 * 1024);
if (strlen($fileContent) > $maxSize) {
return $this->createErrorResult('File size exceeds limit');
}
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$allowedExts = $uploadConfig['fileExt'] ?? [];
if ($ext === '' || (!empty($allowedExts) && !in_array($ext, $allowedExts, true))) {
return $this->createErrorResult('File extension not allowed');
}
$bucket = (string)config('oss.bucket');
$folder = (string)($extraFields['Folder'] ?? ($extraFields['Bucket'] ?? ''));
$key = $extraFields['Key'] ?? basename($fileName);
$objectKey = $this->buildObjectKey($folder, (string)$key);
if ($bucket === '' || $objectKey === '') {
return $this->createErrorResult('Bucket or key missing');
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = (string)$finfo->buffer($fileContent);
try {
$service = new OssService(config('oss'));
$result = $service->uploadFromBinary($fileContent, $objectKey, $bucket, [
'contentType' => $this->resolveContentType($mimeType, $ext),
]);
$result['url'] = $service->buildObjectUrl($objectKey, $bucket);
$result['folder'] = $folder;
return [
'code' => 1,
'msg' => 'upload ok',
'data' => $result,
];
} catch (Throwable $e) {
$fallback = $this->saveToLocalFallback($fileContent, $bucket, $objectKey);
return $this->createErrorResult(
'OSS upload failed: ' . $e->getMessage(),
[
'bucket' => $bucket,
'key' => $objectKey,
'folder' => $folder,
'fallback_saved' => $fallback['saved'],
'fallback_path' => $fallback['path'],
'fallback_error' => $fallback['error'],
]
);
}
}
private function buildObjectKey(string $folder, string $key): string
{
$folder = trim(str_replace('\\', '/', $folder), '/');
$key = trim(str_replace('\\', '/', $key), '/');
if ($folder === '') {
return $key;
}
if ($key === '') {
return $folder;
}
if (str_starts_with($key . '/', $folder . '/')) {
return $key;
}
return $folder . '/' . $key;
}
private function saveToLocalFallback(string $fileContent, string $bucket, string $key): array
{
$baseDir = $this->resolveFallbackBaseDir();
$safeBucket = $this->sanitizePathSegment($bucket);
$safeKey = $this->sanitizeObjectKey($key);
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $safeBucket . DIRECTORY_SEPARATOR . $safeKey;
$directory = dirname($fullPath);
try {
if (!is_dir($directory) && !mkdir($directory, 0777, true) && !is_dir($directory)) {
throw new \RuntimeException('Unable to create fallback directory');
}
if (file_put_contents($fullPath, $fileContent) === false) {
throw new \RuntimeException('Unable to write fallback file');
}
return [
'saved' => true,
'path' => $fullPath,
'error' => null,
];
} catch (Throwable $e) {
return [
'saved' => false,
'path' => $fullPath,
'error' => $e->getMessage(),
];
}
}
private function resolveFallbackBaseDir(): string
{
$uploadConfig = config('filesystem.upload_conf') ?: [];
$configured = (string)($uploadConfig['fallbackDir'] ?? '');
if ($configured !== '') {
return rtrim($configured, "\\/");
}
$localRoot = (string)(config('filesystem.disks.local.root') ?? '');
if ($localRoot !== '') {
return rtrim($localRoot, "\\/") . DIRECTORY_SEPARATOR . 'oss-fallback';
}
return app()->getRuntimePath() . 'storage' . DIRECTORY_SEPARATOR . 'oss-fallback';
}
private function sanitizeObjectKey(string $key): string
{
$normalized = str_replace('\\', '/', $key);
$segments = array_filter(explode('/', $normalized), static function (string $segment): bool {
return $segment !== '' && $segment !== '.' && $segment !== '..';
});
if ($segments === []) {
return 'unnamed-file';
}
$safeSegments = array_map([$this, 'sanitizePathSegment'], $segments);
return implode(DIRECTORY_SEPARATOR, $safeSegments);
}
private function sanitizePathSegment(string $segment): string
{
$cleaned = preg_replace('/[^A-Za-z0-9._-]/', '_', $segment) ?? '';
return $cleaned !== '' ? $cleaned : 'unknown';
}
private function resolveContentType(string $mimeType, string $ext): string
{
if ($mimeType !== '' && $mimeType !== 'application/octet-stream') {
return $mimeType;
}
return match ($ext) {
'jpg', 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'bmp' => 'image/bmp',
'svg' => 'image/svg+xml',
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
default => 'application/octet-stream',
};
}
private function createErrorResult(string $message, array $data = []): array
{
$result = [
'code' => 0,
'msg' => $message,
];
if ($data !== []) {
$result['data'] = $data;
}
return $result;
}
}

View File

@@ -0,0 +1,233 @@
<?php
namespace app\service;
use Throwable;
class PassportOcrService
{
private const PASSPORT_ENDPOINT = '/ocr/passport/base64';
private const IDCARD_ENDPOINT = '/ocr/idcard/base64';
private const VISA_ENDPOINT = '/ocr/visa-number/base64';
public function passport($fileData): array
{
return $this->request(self::PASSPORT_ENDPOINT, $fileData);
}
public function identification($fileData): array
{
return $this->request(self::IDCARD_ENDPOINT, $fileData);
}
public function visa($fileData): array
{
return $this->request(self::VISA_ENDPOINT, $fileData);
}
private function request(string $endpoint, $fileData): array
{
if (!is_string($fileData) || $fileData === '') {
return $this->error('Unable to read uploaded file');
}
$config = $this->loadConfig();
if ($config['base_url'] === '' || $config['app_id'] === '' || $config['secret'] === '') {
return $this->error('OCR config missing: app.OCR_BASE_URL, app.OCR_APP_ID and app.OCR_SECRET are required');
}
try {
$payload = $this->buildPayload($fileData, $config);
} catch (Throwable $e) {
return $this->error('Unable to build OCR payload: ' . $e->getMessage());
}
$timestamp = (string) time();
$signature = hash_hmac('sha256', $config['app_id'] . "\n" . $timestamp, $config['secret']);
$headers = [
'Content-Type: application/json',
'Accept: application/json',
'X-App-Id: ' . $config['app_id'],
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
];
if ($config['origin'] !== '') {
$headers[] = 'Origin: ' . $config['origin'];
}
$ch = curl_init($config['base_url'] . $endpoint);
if ($ch === false) {
return $this->error('Unable to initialize cURL');
}
try {
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_TIMEOUT => $config['timeout'],
CURLOPT_CONNECTTIMEOUT => $config['connect_timeout'],
]);
if (!$config['verify_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
}
$response = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
return $this->error('OCR request failed: ' . curl_error($ch));
}
$decoded = json_decode((string) $response, true);
if (!is_array($decoded)) {
return $this->error('OCR response is not valid JSON', [
'http_code' => $httpCode,
'response' => substr((string) $response, 0, 300),
]);
}
return $decoded;
} finally {
curl_close($ch);
}
}
private function buildPayload(string $fileData, array $config): array
{
$mimeType = $this->detectMimeType($fileData);
$extension = $this->extensionForMimeType($mimeType);
return [
'filename' => 'upload.' . $extension,
'content_type' => $mimeType,
'image_base64' => 'data:' . $mimeType . ';base64,' . base64_encode($fileData),
'auto_rotate' => $config['auto_rotate'],
'return_debug' => $config['return_debug'],
];
}
private function loadConfig(): array
{
$config = config('ocr') ?: [];
return [
'base_url' => rtrim((string) (config('app.OCR_BASE_URL') ?? ''), '/'),
'app_id' => (string) (config('app.OCR_APP_ID') ?? ''),
'secret' => (string) (config('app.OCR_SECRET') ?? ''),
'origin' => $this->resolveOrigin((string) (config('app.OCR_ORIGIN') ?? ($config['origin'] ?? ''))),
'timeout' => max(1, (int) ($config['timeout'] ?? 60)),
'connect_timeout' => max(1, (int) ($config['connect_timeout'] ?? 10)),
'auto_rotate' => $this->boolValue($config['auto_rotate'] ?? true),
'return_debug' => $this->boolValue($config['return_debug'] ?? false),
'verify_ssl' => $this->boolValue($config['verify_ssl'] ?? true),
];
}
private function resolveOrigin(string $configuredOrigin): string
{
$configuredOrigin = trim($configuredOrigin);
if ($configuredOrigin !== '') {
return rtrim($configuredOrigin, '/');
}
$requestOrigin = trim((string) ($_SERVER['HTTP_ORIGIN'] ?? ''));
if ($this->isValidOrigin($requestOrigin)) {
return rtrim($requestOrigin, '/');
}
$host = trim((string) ($_SERVER['HTTP_HOST'] ?? ''));
if ($host === '' || !preg_match('/^[A-Za-z0-9.-]+(?::\d+)?$/', $host)) {
return '';
}
return $this->currentScheme() . '://' . $host;
}
private function isValidOrigin(string $origin): bool
{
if ($origin === '') {
return false;
}
$parts = parse_url($origin);
if (!is_array($parts)) {
return false;
}
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
return in_array($scheme, ['http', 'https'], true)
&& !empty($parts['host'])
&& empty($parts['path'])
&& empty($parts['query'])
&& empty($parts['fragment']);
}
private function currentScheme(): string
{
$forwardedProto = trim((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
if ($forwardedProto !== '') {
$forwardedProto = strtolower(trim(explode(',', $forwardedProto)[0]));
if (in_array($forwardedProto, ['http', 'https'], true)) {
return $forwardedProto;
}
}
$requestScheme = strtolower((string) ($_SERVER['REQUEST_SCHEME'] ?? ''));
if (in_array($requestScheme, ['http', 'https'], true)) {
return $requestScheme;
}
$https = strtolower((string) ($_SERVER['HTTPS'] ?? ''));
return $https !== '' && $https !== 'off' ? 'https' : 'http';
}
private function detectMimeType(string $fileData): string
{
if (!class_exists(\finfo::class)) {
return 'image/jpeg';
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->buffer($fileData);
if (is_string($mimeType) && $mimeType !== '' && $mimeType !== 'application/octet-stream') {
return $mimeType;
}
return 'image/jpeg';
}
private function extensionForMimeType(string $mimeType): string
{
return match ($mimeType) {
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'image/bmp', 'image/x-ms-bmp' => 'bmp',
default => 'jpg',
};
}
private function boolValue($value): bool
{
$parsed = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
return $parsed ?? (bool) $value;
}
private function error(string $message, array $data = []): array
{
$result = [
'code' => 0,
'msg' => $message,
];
if ($data !== []) {
$result['data'] = $data;
}
return $result;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace app\service;
use Swoole\Client;
class SwooleService
{
private static ?self $instance = null;
private string $host = 'ossup.jzvisa.com';
private int $port = 19501;
private float $timeout = 3.0;
// 私有化构造函数,保留原来的单例调用方式
private function __construct()
{
}
// 获取单例
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
// 发送任务:短连接模式,每次连接、发送、关闭
public function sendTask(array $task): bool
{
$client = new Client(SWOOLE_SOCK_TCP);
try {
if (!$client->connect($this->host, $this->port, $this->timeout)) {
throw new \RuntimeException(
"Cannot connect to Swoole Server {$this->host}:{$this->port}, error: {$client->errCode}"
);
}
$data = json_encode($task, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($data === false) {
throw new \RuntimeException('JSON encode failed: ' . json_last_error_msg());
}
// 你的服务端使用 \r\n 作为 package_eof所以这里必须加 \r\n
$sendResult = $client->send($data . "\r\n");
return $sendResult !== false;
} finally {
if ($client->isConnected()) {
$client->close();
}
}
}
// 保留 close 方法,避免旧代码调用时报错
public function close(): void
{
self::$instance = null;
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace app\service;
use think\Exception;
class UploadService
{
/**
* 处理表单文件上传并转发到目标服务器
*
* @param string $fileField 表单文件字段名
* @param string $targetUrl 目标服务器URL
* @param array $extraFields 额外传递的字段
* @param array $headers 额外的请求头
* @return array 包含上传结果的数组
*/
public function uploadAndForward(string $fileField, string $targetUrl, array $extraFields = [], array $headers = []): array
{
// 获取上传的文件
$file = request()->file($fileField);
if (!$file) {
return $this->createErrorResult('未上传文件或上传失败');
}
try {
$config = config('app.upload_conf');
$configs=[];
foreach ($config as $key=>$con){
$configs[] = $key.':'.$con;
}
// 验证文件
$validate = validate([
$fileField =>implode('|',$configs),
]);
if (!$validate->check([$fileField => $file])) {
return $this->createErrorResult($validate->getError());
}
// 使用cURL转发到目标服务器
$result = $this->forwardToTargetServer($file, $targetUrl, $extraFields, $headers);
return $result;
} catch (\Exception $e) {
return $this->createErrorResult('上传过程中发生异常: ' . $e->getMessage());
}
}
/**
* 使用cURL将文件转发到目标服务器
*
* @param \think\File $file 文件对象
* @param string $targetUrl 目标URL
* @param array $extraFields 额外字段
* @param array $headers 请求头
* @return array 包含响应信息的数组
*/
private function forwardToTargetServer(\think\File $file, string $targetUrl, array $extraFields = [], array $headers = []): array
{
$ch = curl_init();
try {
// 设置基本选项
curl_setopt($ch, CURLOPT_URL, $targetUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 5分钟超时
// 构建上传数据
$postData = [
'file' => new \CURLFile(
$file->getPathname(),
$file->getOriginalMime(),
$file->getOriginalName()
)
];
// 添加额外字段
$postData = array_merge($postData, $extraFields);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
// 设置请求头
$httpHeaders = [];
foreach ($headers as $headerName => $headerValue) {
$httpHeaders[] = "$headerName: $headerValue";
}
if (!empty($httpHeaders)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
}
// 执行请求
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlInfo = curl_getinfo($ch);
// 检查错误
if ($response === false) {
throw new Exception('cURL错误: ' . curl_error($ch));
}
return [
'code' => 1,
'http_code' => $httpCode,
'response' => $response,
];
} catch (\Exception $e) {
return [
'code' => 0,
'error' => $e->getMessage()
];
} finally {
curl_close($ch);
}
}
/**
* 创建错误结果数组
*
* @param string $message 错误信息
* @return array 错误结果
*/
private function createErrorResult(string $message): array
{
return [
'code' => 0,
'error' => $message
];
}
}