This commit is contained in:
gaofeng
2026-09-01 11:07:13 +08:00
parent f6f37310ce
commit e0aeff2af7
18 changed files with 577 additions and 6 deletions

View 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";

View 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";

View 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";

View 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";

View 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";

View 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'));
}
}

View 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";

View 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";
}