提交
This commit is contained in:
@@ -5,6 +5,7 @@ namespace app\miniapi\controller;
|
||||
|
||||
use app\miniapi\service\Product\AppCatalogService;
|
||||
use app\miniapi\service\Product\ProductService;
|
||||
use app\miniapi\service\AppUpdateService;
|
||||
use think\facade\Config;
|
||||
|
||||
class AppCatalog extends Base
|
||||
@@ -13,6 +14,7 @@ class AppCatalog extends Base
|
||||
{
|
||||
$site = (array)Config::get('app.miniapi_site', []);
|
||||
$config = (array)Config::get('mini_app_catalog', []);
|
||||
$updateService = new AppUpdateService();
|
||||
|
||||
return $this->ok([
|
||||
'brand' => [
|
||||
@@ -32,9 +34,27 @@ class AppCatalog extends Base
|
||||
],
|
||||
'features' => ['visa_catalog' => true],
|
||||
'upload_limits' => (array)($config['upload_limits'] ?? []),
|
||||
'android_update' => $updateService->bootstrap(
|
||||
(array)($config['android_update'] ?? []),
|
||||
(string)$this->request->get('channel', 'official'),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$config = (array)Config::get('mini_app_catalog.android_update', []);
|
||||
$target = (new AppUpdateService())->storeTarget(
|
||||
$config,
|
||||
(string)$this->request->get('channel', 'official'),
|
||||
);
|
||||
if ($target === '') {
|
||||
return $this->fail('该渠道更新地址尚未配置', 404);
|
||||
}
|
||||
|
||||
return redirect($target);
|
||||
}
|
||||
|
||||
public function home()
|
||||
{
|
||||
$catalog = $this->catalog();
|
||||
|
||||
47
app/miniapi/controller/EvusCorrection.php
Normal file
47
app/miniapi/controller/EvusCorrection.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\miniapi\controller;
|
||||
|
||||
use app\miniapi\service\EvusCorrection\EvusCorrectionService;
|
||||
|
||||
final class EvusCorrection extends Base
|
||||
{
|
||||
public function detail()
|
||||
{
|
||||
$userId = $this->requireLogin();
|
||||
if (!is_int($userId)) {
|
||||
return $userId;
|
||||
}
|
||||
|
||||
$orderSn = trim((string)$this->request->param('order_sn', ''));
|
||||
if ($orderSn === '') {
|
||||
return $this->fail('参数错误');
|
||||
}
|
||||
$result = (new EvusCorrectionService())->detail($userId, $orderSn);
|
||||
if ((int)($result['code'] ?? 0) !== 1) {
|
||||
return $this->fail((string)($result['msg'] ?? '服务异常,请稍后重试'));
|
||||
}
|
||||
|
||||
return $this->ok((array)($result['data'] ?? []));
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$userId = $this->requireLogin();
|
||||
if (!is_int($userId)) {
|
||||
return $userId;
|
||||
}
|
||||
|
||||
$params = $this->postData();
|
||||
if (trim((string)($params['order_sn'] ?? '')) === '') {
|
||||
return $this->fail('参数错误');
|
||||
}
|
||||
$result = (new EvusCorrectionService())->submit($userId, $params);
|
||||
if ((int)($result['code'] ?? 0) !== 1) {
|
||||
return $this->fail((string)($result['msg'] ?? '提交失败,请稍后重试'));
|
||||
}
|
||||
|
||||
return $this->ok((array)($result['data'] ?? []), '提交成功');
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ class Upload extends Base
|
||||
{
|
||||
$scene = strtolower(trim((string)$this->request->post('scene', '')));
|
||||
$contactUserId = 0;
|
||||
if (in_array($scene, ['passport', 'id_card'], true)) {
|
||||
if (in_array($scene, ['passport', 'id_card', 'evus_correction'], true)) {
|
||||
$login = $this->requireLogin();
|
||||
if (!is_int($login)) {
|
||||
return $login;
|
||||
@@ -55,9 +55,13 @@ class Upload extends Base
|
||||
}
|
||||
|
||||
$fileName = date('YmdHis') . uniqid('', true) . '.' . $ext;
|
||||
$objectKey = $contactUserId > 0
|
||||
? 'contact/' . $contactUserId . '/' . $fileName
|
||||
: $fileName;
|
||||
if ($scene === 'evus_correction') {
|
||||
$objectKey = 'evus-correction/' . $contactUserId . '/' . $fileName;
|
||||
} elseif ($contactUserId > 0) {
|
||||
$objectKey = 'contact/' . $contactUserId . '/' . $fileName;
|
||||
} else {
|
||||
$objectKey = $fileName;
|
||||
}
|
||||
|
||||
try {
|
||||
$client = SwooleService::getInstance();
|
||||
|
||||
@@ -6,6 +6,7 @@ Route::pattern(['product_id' => '[A-Za-z0-9_-]+']);
|
||||
|
||||
Route::group('app', function () {
|
||||
Route::get('bootstrap', 'AppCatalog/bootstrap');
|
||||
Route::get('update', 'AppCatalog/update');
|
||||
Route::get('home', 'AppCatalog/home');
|
||||
Route::get('visas/:product_id/schema', 'AppCatalog/schema');
|
||||
Route::get('visas/:product_id', 'AppCatalog/detail');
|
||||
|
||||
60
app/miniapi/service/AppUpdateService.php
Normal file
60
app/miniapi/service/AppUpdateService.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\miniapi\service;
|
||||
|
||||
final class AppUpdateService
|
||||
{
|
||||
private const CHANNELS = ['official', 'huawei', 'xiaomi', 'oppo', 'vivo', 'tencent'];
|
||||
|
||||
public function normalizeChannel(string $value): string
|
||||
{
|
||||
$channel = strtolower(trim($value));
|
||||
return in_array($channel, self::CHANNELS, true) ? $channel : 'official';
|
||||
}
|
||||
|
||||
public function bootstrap(array $config, string $requestedChannel): array
|
||||
{
|
||||
$channel = $this->normalizeChannel($requestedChannel);
|
||||
$gateway = $this->safeHttpsUrl((string)($config['gateway_url'] ?? ''));
|
||||
|
||||
return [
|
||||
'latest_version_code' => max(0, (int)($config['latest_version_code'] ?? 0)),
|
||||
'latest_version_name' => trim((string)($config['latest_version_name'] ?? '')),
|
||||
'minimum_version_code' => max(0, (int)($config['minimum_version_code'] ?? 0)),
|
||||
'download_url' => $gateway === '' ? '' : $gateway . (str_contains($gateway, '?') ? '&' : '?') . http_build_query(
|
||||
['channel' => $channel],
|
||||
'',
|
||||
'&',
|
||||
PHP_QUERY_RFC3986,
|
||||
),
|
||||
'changelog' => trim((string)($config['changelog'] ?? '')),
|
||||
'force_update' => filter_var($config['force_update'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
];
|
||||
}
|
||||
|
||||
public function storeTarget(array $config, string $requestedChannel): string
|
||||
{
|
||||
$channel = $this->normalizeChannel($requestedChannel);
|
||||
$urls = (array)($config['store_urls'] ?? []);
|
||||
$target = $this->safeHttpsUrl((string)($urls[$channel] ?? ''));
|
||||
if ($target !== '') {
|
||||
return $target;
|
||||
}
|
||||
|
||||
return $this->safeHttpsUrl((string)($urls['official'] ?? $config['fallback_url'] ?? ''));
|
||||
}
|
||||
|
||||
private function safeHttpsUrl(string $value): string
|
||||
{
|
||||
$url = trim($value);
|
||||
if ($url === '' || filter_var($url, FILTER_VALIDATE_URL) === false) {
|
||||
return '';
|
||||
}
|
||||
$parts = parse_url($url);
|
||||
if (($parts['scheme'] ?? '') !== 'https' || empty($parts['host']) || isset($parts['user'])) {
|
||||
return '';
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
64
app/miniapi/service/EvusCorrection/EvusCorrectionService.php
Normal file
64
app/miniapi/service/EvusCorrection/EvusCorrectionService.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\miniapi\service\EvusCorrection;
|
||||
|
||||
use api\Httpcurl;
|
||||
use think\facade\Log;
|
||||
|
||||
final class EvusCorrectionService
|
||||
{
|
||||
public function detail(int $userId, string $orderSn): array
|
||||
{
|
||||
return $this->request('detail', $userId, ['order_sn' => $orderSn]);
|
||||
}
|
||||
|
||||
public function submit(int $userId, array $params): array
|
||||
{
|
||||
return $this->request('submit', $userId, $params);
|
||||
}
|
||||
|
||||
public function payload(int $userId, array $params = []): array
|
||||
{
|
||||
unset($params['user_id']);
|
||||
return ['user_id' => $userId] + $params;
|
||||
}
|
||||
|
||||
public function endpoint(string $userUrl, string $action): string
|
||||
{
|
||||
if (!in_array($action, ['detail', 'submit'], true)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$userUrl = rtrim(trim($userUrl), '/');
|
||||
$endpoint = preg_replace(
|
||||
'#/users/[^/?]+(?:\?.*)?$#i',
|
||||
'/evus_correction/' . $action,
|
||||
$userUrl
|
||||
);
|
||||
|
||||
return is_string($endpoint) && $endpoint !== $userUrl ? $endpoint : '';
|
||||
}
|
||||
|
||||
private function request(string $action, int $userId, array $params): array
|
||||
{
|
||||
$url = $this->endpoint((string)config('app.user_url'), $action);
|
||||
if ($url === '') {
|
||||
return ['code' => 0, 'msg' => 'EVUS资料服务地址异常', 'data' => []];
|
||||
}
|
||||
|
||||
$response = Httpcurl::request($url, 'post', $this->payload($userId, $params));
|
||||
if (!is_array($response) || !isset($response[0]) || !empty($response[3])) {
|
||||
Log::error('miniapi evus correction ' . $action . ' request failed');
|
||||
return ['code' => 0, 'msg' => '服务异常,请稍后重试', 'data' => []];
|
||||
}
|
||||
|
||||
$result = json_decode($response[0], true);
|
||||
if (!is_array($result)) {
|
||||
Log::error('miniapi evus correction ' . $action . ' response decode failed');
|
||||
return ['code' => 0, 'msg' => '服务异常,请稍后重试', 'data' => []];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -93,8 +93,9 @@ class LegalDocumentService
|
||||
{
|
||||
$companyName = $company['company_name'] ?: 'EVUS登记服务';
|
||||
$siteUrl = $this->siteUrl((string)($company['site_domain'] ?? ''));
|
||||
$phone = $company['phone'] ?: '请通过小程序客服入口联系我们';
|
||||
$phone = $company['phone'] ?: '请通过应用内客服入口联系我们';
|
||||
$intro = $this->replaceTokens((string)($document['intro_template'] ?? ''), $company, $siteUrl, $phone);
|
||||
$append = $this->replaceTokens((string)($document['append_content'] ?? ''), $company, $siteUrl, $phone);
|
||||
$subjectLabel = trim((string)($document['subject_label'] ?? '服务主体'));
|
||||
|
||||
$prefix = [
|
||||
@@ -108,7 +109,7 @@ class LegalDocumentService
|
||||
return trim((string)$line) !== '';
|
||||
}));
|
||||
|
||||
return implode("\n", $prefix) . "\n\n" . $body;
|
||||
return implode("\n", $prefix) . "\n\n" . $body . ($append !== '' ? "\n\n" . $append : '');
|
||||
}
|
||||
|
||||
private function replaceTokens(string $text, array $company, string $siteUrl, string $phone): string
|
||||
|
||||
@@ -9,6 +9,23 @@ return [
|
||||
'image_mb' => 10,
|
||||
'pdf_mb' => 20,
|
||||
],
|
||||
'android_update' => [
|
||||
'latest_version_code' => (int)env('APP_ANDROID_LATEST_VERSION_CODE', 0),
|
||||
'latest_version_name' => env('APP_ANDROID_LATEST_VERSION_NAME', ''),
|
||||
'minimum_version_code' => (int)env('APP_ANDROID_MINIMUM_VERSION_CODE', 0),
|
||||
'gateway_url' => env('APP_ANDROID_UPDATE_GATEWAY_URL', 'https://miniapi.jzvisa.cn/miniapi/app/update'),
|
||||
'fallback_url' => env('APP_ANDROID_FALLBACK_URL', ''),
|
||||
'changelog' => env('APP_ANDROID_CHANGELOG', ''),
|
||||
'force_update' => env('APP_ANDROID_FORCE_UPDATE', false),
|
||||
'store_urls' => [
|
||||
'official' => env('APP_ANDROID_STORE_OFFICIAL_URL', ''),
|
||||
'huawei' => env('APP_ANDROID_STORE_HUAWEI_URL', ''),
|
||||
'xiaomi' => env('APP_ANDROID_STORE_XIAOMI_URL', ''),
|
||||
'oppo' => env('APP_ANDROID_STORE_OPPO_URL', ''),
|
||||
'vivo' => env('APP_ANDROID_STORE_VIVO_URL', ''),
|
||||
'tencent' => env('APP_ANDROID_STORE_TENCENT_URL', ''),
|
||||
],
|
||||
],
|
||||
'service_guarantees' => [
|
||||
['title' => '资料安全', 'description' => '敏感资料加密传输与存储'],
|
||||
['title' => '进度清晰', 'description' => '订单状态和补充材料要求及时可查'],
|
||||
|
||||
@@ -37,6 +37,19 @@ return [
|
||||
'evus_bj_mp' => [
|
||||
'company_code' => 'bj',
|
||||
],
|
||||
'evus_jn_app' => [
|
||||
'company_code' => 'jn',
|
||||
'documents' => [
|
||||
'terms' => [
|
||||
'intro_template' => '本条款与条件适用于{company_name}提供的eVUS移动应用服务。',
|
||||
],
|
||||
'privacy' => [
|
||||
'intro_template' => '本隐私政策适用于{company_name}提供的eVUS移动应用服务。',
|
||||
'content' => "一、我们处理的信息\n1. 账号信息:短信登录所需的手机号码、验证码校验结果及登录状态。\n2. 申请人信息:姓名、出生日期、性别、国籍、身份证明号码、联系方式、家庭与工作信息,以及办理EVUS或ESTA所需的其他资料。\n3. 证件和材料:您主动拍摄或选择的护照、美国签证、身份证明及补充材料图片,以及OCR识别结果。\n4. 订单信息:订单编号、服务套餐、金额、申请人、订单状态、支付渠道和支付结果。我们不会保存支付宝或微信的支付密码。\n5. 必要技术信息:为保障网络通信、登录安全和故障排查所需的应用版本、渠道标识、网络请求时间及必要安全日志。\n\n二、处理目的与方式\n上述信息仅用于身份验证、资料预填与校验、提交登记申请、生成和管理订单、确认支付结果、查询办理进度、生成业务文件、申请发票及提供电话客服。相机或系统图片选择器仅在您主动拍摄或选择材料时使用;本应用不读取通讯录、通话记录或精确位置。\n\n三、共享、委托处理与对外提供\n为完成您主动选择的功能,我们可能向支付机构、OCR识别服务和文件存储服务传输完成该功能所必需的信息。除取得您的单独同意、履行服务所必需或法律法规另有规定外,我们不会出售或向无关第三方提供您的个人信息。第三方服务的具体情况见本政策后的《第三方SDK与服务说明》及App内《第三方SDK与服务清单》。\n\n四、存储与保存期限\n个人信息原则上存储在中华人民共和国境内,并仅在实现服务目的和满足法律、财税、争议处理及安全审计要求所必需的期限内保存。超过保存期限后,我们将删除或匿名化处理;法律法规另有要求的除外。申请草稿和部分发票记录可能加密保存在当前设备,清除应用数据或卸载应用后可能无法恢复。\n\n五、安全保护\n我们采用HTTPS加密传输、访问控制、敏感字段脱敏和本地安全存储等措施保护信息。请妥善保管短信验证码,不要向他人泄露。\n\n六、您的权利\n您可以在App内查看、修改或删除常用申请人资料,查看订单与申请进度,并可退出当前登录账号。如需查阅、更正、复制或删除其他个人信息,可通过App内电话客服联系我们。我们将在核实身份后依法处理。\n\n七、未成年人信息\n如您为未成年人提交资料,应由监护人操作或在监护人同意和指导下使用本服务。代他人提交资料前,请确保已取得申请人的合法授权。\n\n八、政策更新与联系我们\n当处理目的、信息类型或第三方服务发生重要变化时,我们将更新本政策并通过App内显著方式提示。对本政策有疑问,可通过App内“关于我们”所列电话联系我们。",
|
||||
'append_content' => "第三方 SDK 与服务说明\n支付宝 App 支付 SDK:仅在用户主动选择支付宝付款时,用于调起支付宝并完成订单支付。\n微信 OpenSDK:当前微信支付尚未启用;启用后仅在用户主动选择微信支付时调用。\nOCR 识别服务:仅在用户主动拍摄或上传证件材料时,用于识别文字并辅助填写。\n阿里云 OSS 文件存储服务:用于加密传输和存储用户主动提交的申请材料及业务文件。\n上述能力不会因浏览普通页面而调用,具体处理范围以 App 内《第三方 SDK 与服务清单》为准。",
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -44,6 +44,8 @@ Route::group('miniapi', function () {
|
||||
Route::get('ocr/signature', 'Ocr/signature');
|
||||
Route::post('upload/image', 'Upload/image');
|
||||
|
||||
Route::get('evus/correction', 'EvusCorrection/detail');
|
||||
Route::post('evus/correction/submit', 'EvusCorrection/submit');
|
||||
Route::post('evus/lookup', 'Evus/lookup');
|
||||
Route::post('evus/apply', 'Evus/apply');
|
||||
Route::post('esta/apply', 'Esta/apply');
|
||||
|
||||
25
tests/customer_center_action_dispatch_self_check.php
Normal file
25
tests/customer_center_action_dispatch_self_check.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$controller = file_get_contents(__DIR__ . '/../app/miniapi/controller/CustomerCenter.php');
|
||||
if (!is_string($controller)) {
|
||||
throw new RuntimeException('Unable to read customer center controller');
|
||||
}
|
||||
if (!str_contains($controller, "param('_action', '')")) {
|
||||
throw new RuntimeException('Applicant endpoint must inspect the explicit action parameter');
|
||||
}
|
||||
if (!str_contains($controller, "\$action === 'save'")) {
|
||||
throw new RuntimeException('Applicant endpoint must dispatch the save action');
|
||||
}
|
||||
if (!str_contains($controller, "\$action === 'remove'")) {
|
||||
throw new RuntimeException('Applicant endpoint must dispatch the remove action');
|
||||
}
|
||||
|
||||
foreach (['miniprogram', 'miniprogram-bj'] as $miniapp) {
|
||||
$profiles = file_get_contents(__DIR__ . '/../../' . $miniapp . '/pages/profiles/index.js');
|
||||
if (!is_string($profiles) || !str_contains($profiles, '/miniapi/customer-center/applicant?_action=remove')) {
|
||||
throw new RuntimeException($miniapp . ' must call the explicit remove action');
|
||||
}
|
||||
}
|
||||
|
||||
echo "Customer center action dispatch self-check passed\n";
|
||||
22
tests/customer_center_route_order_self_check.php
Normal file
22
tests/customer_center_route_order_self_check.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$routes = file_get_contents(__DIR__ . '/../route/app.php');
|
||||
if (!is_string($routes)) {
|
||||
throw new RuntimeException('Unable to read route configuration');
|
||||
}
|
||||
|
||||
$detail = strpos($routes, "Route::post('customer-center/applicant',");
|
||||
$save = strpos($routes, "Route::post('customer-center/applicant/save',");
|
||||
$remove = strpos($routes, "Route::post('customer-center/applicant/remove',");
|
||||
if ($detail === false || $save === false || $remove === false) {
|
||||
throw new RuntimeException('Customer center applicant routes are incomplete');
|
||||
}
|
||||
if ($save > $detail || $remove > $detail) {
|
||||
throw new RuntimeException('Specific applicant routes must be registered before the detail route');
|
||||
}
|
||||
if (!preg_match("/Route::post\('customer-center\\/applicant',\s*'CustomerCenter\\/applicant'\)->completeMatch\(\);/", $routes)) {
|
||||
throw new RuntimeException('Applicant detail route must use complete matching');
|
||||
}
|
||||
|
||||
echo "Customer center route order self-check passed\n";
|
||||
51
tests/evus_correction_route_self_check.php
Normal file
51
tests/evus_correction_route_self_check.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use app\miniapi\service\EvusCorrection\EvusCorrectionService;
|
||||
|
||||
function miniCorrectionAssertSame(mixed $expected, mixed $actual, string $message): void
|
||||
{
|
||||
if ($expected !== $actual) {
|
||||
throw new RuntimeException($message . PHP_EOL
|
||||
. 'Expected: ' . var_export($expected, true) . PHP_EOL
|
||||
. 'Actual: ' . var_export($actual, true));
|
||||
}
|
||||
}
|
||||
|
||||
$service = new EvusCorrectionService();
|
||||
miniCorrectionAssertSame(
|
||||
'https://user.example.com/home/evus_correction/detail',
|
||||
$service->endpoint('https://user.example.com/home/users/login', 'detail'),
|
||||
'Detail must use the existing user URL host and isolated upstream controller'
|
||||
);
|
||||
miniCorrectionAssertSame(
|
||||
'https://user.example.com/home/evus_correction/submit',
|
||||
$service->endpoint('https://user.example.com/home/users/login?source=mini', 'submit'),
|
||||
'Submit must remove the existing users action and query string'
|
||||
);
|
||||
miniCorrectionAssertSame('', $service->endpoint('https://user.example.com/home/users/login', 'delete'), 'Unknown upstream actions must be rejected');
|
||||
miniCorrectionAssertSame(
|
||||
['user_id' => 88, 'order_sn' => 'EVUS-1'],
|
||||
$service->payload(88, ['user_id' => 999, 'order_sn' => 'EVUS-1']),
|
||||
'Authenticated user ID must override an untrusted client value'
|
||||
);
|
||||
|
||||
$routes = file_get_contents(__DIR__ . '/../route/app.php');
|
||||
if (!is_string($routes)
|
||||
|| !str_contains($routes, "Route::get('evus/correction', 'EvusCorrection/detail');")
|
||||
|| !str_contains($routes, "Route::post('evus/correction/submit', 'EvusCorrection/submit');")
|
||||
) {
|
||||
throw new RuntimeException('The miniapi must register explicit correction detail and submit routes');
|
||||
}
|
||||
|
||||
$upload = file_get_contents(__DIR__ . '/../app/miniapi/controller/Upload.php');
|
||||
if (!is_string($upload)
|
||||
|| !str_contains($upload, "'evus_correction'")
|
||||
|| !str_contains($upload, "'evus-correction/' . \$contactUserId . '/' . \$fileName")
|
||||
) {
|
||||
throw new RuntimeException('The correction upload scene must require login and use the account-scoped OSS prefix');
|
||||
}
|
||||
|
||||
echo "Miniapi EVUS correction route self-check passed\n";
|
||||
40
tests/legal_app_config_self_check.php
Normal file
40
tests/legal_app_config_self_check.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$config = require dirname(__DIR__) . '/config/mini_legal.php';
|
||||
$app = $config['businesses']['evus']['apps']['evus_jn_app'] ?? [];
|
||||
|
||||
if (($app['company_code'] ?? '') !== 'jn') {
|
||||
fwrite(STDERR, "evus_jn_app must use the Jinan company.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
foreach (['terms', 'privacy'] as $type) {
|
||||
$intro = (string)($app['documents'][$type]['intro_template'] ?? '');
|
||||
if (strpos($intro, '移动应用') === false) {
|
||||
fwrite(STDERR, "$type must identify the Android mobile app.\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$privacyAppendix = (string)($app['documents']['privacy']['append_content'] ?? '');
|
||||
foreach (['支付宝', '微信 OpenSDK', 'OCR', '阿里云 OSS'] as $service) {
|
||||
if (strpos($privacyAppendix, $service) === false) {
|
||||
fwrite(STDERR, "privacy disclosure is missing $service.\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$privacyContent = (string)($app['documents']['privacy']['content'] ?? '');
|
||||
foreach (['手机号码', '护照', '美国签证', '订单', '支付结果', '保存期限'] as $topic) {
|
||||
if (strpos($privacyContent, $topic) === false) {
|
||||
fwrite(STDERR, "Android privacy policy is missing $topic.\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if (stripos($privacyContent, 'cookie') !== false || strpos($privacyContent, '计算机') !== false) {
|
||||
fwrite(STDERR, "Android privacy policy must not contain legacy website-only collection claims.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "legal app config self-check passed\n";
|
||||
34
tests/order_source_self_check.php
Normal file
34
tests/order_source_self_check.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('env')) {
|
||||
function env(string $name, $default = null)
|
||||
{
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
$config = require __DIR__ . '/../config/miniapp.php';
|
||||
$apps = (array)($config['apps'] ?? []);
|
||||
|
||||
if (($apps['evus_jn_app']['order_source'] ?? '') !== 'jnandroid') {
|
||||
throw new RuntimeException('Android EVUS orders must use source jnandroid.');
|
||||
}
|
||||
|
||||
if (($apps['jiuzhouqian-android']['order_source'] ?? '') !== 'jnandroid') {
|
||||
throw new RuntimeException('Legacy Android clients must use source jnandroid.');
|
||||
}
|
||||
|
||||
foreach (['evus_jn_mp', 'evus_bj_mp'] as $miniAppCode) {
|
||||
if (($apps[$miniAppCode]['order_source'] ?? '') !== 'qlwxmini') {
|
||||
throw new RuntimeException($miniAppCode . ' must keep source qlwxmini.');
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['Evus.php', 'Esta.php'] as $controllerFile) {
|
||||
$controller = file_get_contents(__DIR__ . '/../app/miniapi/controller/' . $controllerFile);
|
||||
if (!is_string($controller) || !str_contains($controller, "['order_source'] ?? 'qlwxmini'")) {
|
||||
throw new RuntimeException($controllerFile . ' must read source from the current mini app config.');
|
||||
}
|
||||
}
|
||||
|
||||
echo "order source self-check passed\n";
|
||||
53
tests/unit/AppUpdateServiceTest.php
Normal file
53
tests/unit/AppUpdateServiceTest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
use app\miniapi\service\AppUpdateService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class AppUpdateServiceTest extends TestCase
|
||||
{
|
||||
public function testBootstrapUsesStableGatewayForRequestedStoreChannel(): void
|
||||
{
|
||||
$result = (new AppUpdateService())->bootstrap([
|
||||
'latest_version_code' => 21,
|
||||
'latest_version_name' => '1.0.19',
|
||||
'gateway_url' => 'https://miniapi.jzvisa.cn/miniapi/app/update',
|
||||
], 'huawei');
|
||||
|
||||
self::assertSame(21, $result['latest_version_code']);
|
||||
self::assertSame('https://miniapi.jzvisa.cn/miniapi/app/update?channel=huawei', $result['download_url']);
|
||||
}
|
||||
|
||||
public function testUnknownChannelFallsBackToOfficial(): void
|
||||
{
|
||||
$service = new AppUpdateService();
|
||||
|
||||
self::assertSame('official', $service->normalizeChannel('unknown-store'));
|
||||
self::assertSame('official', $service->normalizeChannel(''));
|
||||
}
|
||||
|
||||
public function testTextFalseDoesNotEnableForcedUpdate(): void
|
||||
{
|
||||
$result = (new AppUpdateService())->bootstrap([
|
||||
'force_update' => 'false',
|
||||
], 'official');
|
||||
|
||||
self::assertFalse($result['force_update']);
|
||||
}
|
||||
|
||||
public function testStoreTargetFallsBackToOfficialAndRejectsUnsafeUrls(): void
|
||||
{
|
||||
$service = new AppUpdateService();
|
||||
$config = [
|
||||
'store_urls' => [
|
||||
'official' => 'https://jzvisa.cn/evus',
|
||||
'huawei' => 'javascript:alert(1)',
|
||||
],
|
||||
];
|
||||
|
||||
self::assertSame('https://jzvisa.cn/evus', $service->storeTarget($config, 'huawei'));
|
||||
self::assertSame('https://jzvisa.cn/evus', $service->storeTarget($config, 'xiaomi'));
|
||||
}
|
||||
}
|
||||
20
tests/upload_transport_self_check.php
Normal file
20
tests/upload_transport_self_check.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$source = file_get_contents(__DIR__ . '/../app/service/SwooleService.php');
|
||||
if (!is_string($source)) {
|
||||
throw new RuntimeException('Unable to read upload transport');
|
||||
}
|
||||
if (!str_contains($source, "private string \$host = 'ossup.jzvisa.cn';")) {
|
||||
throw new RuntimeException('Upload transport must use ossup.jzvisa.cn');
|
||||
}
|
||||
if (!str_contains($source, 'private int $port = 19501;')) {
|
||||
throw new RuntimeException('Upload transport must keep port 19501');
|
||||
}
|
||||
|
||||
$controller = file_get_contents(__DIR__ . '/../app/miniapi/controller/Upload.php');
|
||||
if (!is_string($controller) || !str_contains($controller, "'json_encode_param' => 0")) {
|
||||
throw new RuntimeException('Upload responses must escape Unicode for wx.uploadFile compatibility');
|
||||
}
|
||||
|
||||
echo "Upload transport self-check passed\n";
|
||||
97
tests/wechat_phone_token_retry_self_check.php
Normal file
97
tests/wechat_phone_token_retry_self_check.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api {
|
||||
final class Httpcurl
|
||||
{
|
||||
public static array $requests = [];
|
||||
public static array $responses = [];
|
||||
|
||||
public static function request($url, $type, $data = false, $header = [], $timeout = 0): array
|
||||
{
|
||||
self::$requests[] = compact('url', 'type', 'data', 'header', 'timeout');
|
||||
$body = array_shift(self::$responses);
|
||||
return [json_encode($body, JSON_UNESCAPED_UNICODE), '', ['http_code' => 200], 0, ''];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace think\facade {
|
||||
final class Cache
|
||||
{
|
||||
public static array $items = [];
|
||||
|
||||
public static function get(string $key, $default = null)
|
||||
{
|
||||
return self::$items[$key] ?? $default;
|
||||
}
|
||||
|
||||
public static function set(string $key, $value, $ttl = null): bool
|
||||
{
|
||||
self::$items[$key] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function delete(string $key): bool
|
||||
{
|
||||
unset(self::$items[$key]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final class Log
|
||||
{
|
||||
public static array $warnings = [];
|
||||
|
||||
public static function warning(string $message): void
|
||||
{
|
||||
self::$warnings[] = $message;
|
||||
}
|
||||
|
||||
public static function error(string $message): void
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
require __DIR__ . '/../app/service/MiniProgramWechatService.php';
|
||||
|
||||
use api\Httpcurl;
|
||||
use app\service\MiniProgramWechatService;
|
||||
use think\facade\Cache;
|
||||
|
||||
$appid = 'wx-test-appid';
|
||||
$cacheKey = 'miniapp_stable_access_token_' . md5('evus_bj_mp|' . $appid);
|
||||
Cache::$items[$cacheKey] = 'stale-token';
|
||||
Httpcurl::$responses = [
|
||||
['errcode' => 40001, 'errmsg' => 'invalid credential'],
|
||||
['access_token' => 'fresh-stable-token', 'expires_in' => 7200],
|
||||
['errcode' => 0, 'phone_info' => ['purePhoneNumber' => '18668962632']],
|
||||
];
|
||||
|
||||
$service = new MiniProgramWechatService('evus_bj_mp', [
|
||||
'wechat_appid' => $appid,
|
||||
'wechat_secret' => 'test-secret',
|
||||
]);
|
||||
$mobile = $service->mobileByPhoneCode('single-use-phone-code');
|
||||
|
||||
if ($mobile !== '18668962632') {
|
||||
throw new RuntimeException('Phone lookup must retry once after an invalid access token');
|
||||
}
|
||||
if (count(Httpcurl::$requests) !== 3) {
|
||||
throw new RuntimeException('Phone lookup must make exactly one retry');
|
||||
}
|
||||
if (!str_contains((string)Httpcurl::$requests[1]['url'], '/cgi-bin/stable_token')) {
|
||||
throw new RuntimeException('Token refresh must use the stable access token endpoint');
|
||||
}
|
||||
$refreshBody = json_decode((string)Httpcurl::$requests[1]['data'], true);
|
||||
if (($refreshBody['force_refresh'] ?? null) !== true) {
|
||||
throw new RuntimeException('Invalid access token retry must force one stable token refresh');
|
||||
}
|
||||
if ((Cache::$items[$cacheKey] ?? '') !== 'fresh-stable-token') {
|
||||
throw new RuntimeException('Refreshed stable access token must be cached');
|
||||
}
|
||||
|
||||
echo "Wechat phone token retry self-check passed\n";
|
||||
}
|
||||
Reference in New Issue
Block a user