cloud 1 tuần trước cách đây
mục cha
commit
266541cb21

+ 7 - 0
PROJECT_CONTEXT.md

@@ -1,5 +1,12 @@
 # Project Context
 
+## 2026-10-24 测试登录取消来源及环境限制(用户明确要求)
+- 用户实际收到 `40301 测试手机号登录仅允许指定来源`,明确要求指定测试号码与固定验证码正确即可直接登录。此前保留的 local 环境和来源 IP 白名单与该要求不符;本节覆盖下方历史记录中的这些限制。
+- `AuthController::testPhone` 移除来源白名单检查,`TestPhoneLogin::assertSourceAllowed` 及 `MINI_TEST_PHONE_ALLOWED_IPS` 配置移除;`TestPhoneIdentityProvider::enabled` 仅由 `MINI_TEST_PHONE_LOGIN` 控制,不再检查 `APP_ENV`。SessionVerifier 共用该开关,因此 production 环境会话可用,关闭开关仍使既有测试会话失效。
+- 三个指定测试号码、固定码 `66666`、账号预初始化/启用、业务授权、来源 IP 及全局限流继续生效;正式手机号仍走真实短信,不允许固定码登录。APP_ID 保持 `local-test-phone` 以复用原账号,不迁移或改动现有用户/授权。
+- 测试登录63项通过,包含直接调用HTTP控制器,production配置下模拟局域网、公网、IPv6、可信代理四类来源×三个账号均返回200且会话校验通过。短信51项、调用方IP16项、架构126项、契约911项、配置提示5项通过;所有业务夹具回滚,无真实短信发送。
+- API文档、OpenAPI及公开副本、配置示例、初始化脚本说明与小程序README同步。未部署线上后端;线上需要同步代码并重载Webman,单独重编译小程序不能移除服务器的旧来源检查。
+
 ## 2026-10-24 登录协议勾选状态同步
 - 用户截图复现:点击登录协议文字后控件显示勾选,但 `agreed` 未更新,登录仍提示未同意。原因是原生 label 触发控件状态变化,而页面只在 checkbox 的 click 中手动反转布尔值。
 - 登录页改用 `checkbox-group` 的 change 事件读取 `detail.value`,文字与勾选框共享同一状态;隐私链接移到 label 外,点击链接不影响同意状态。测试码输入限制5位,短信码6位,验证码格式提示与协议提示分开。

+ 4 - 4
server/.env.example

@@ -63,15 +63,15 @@ MINI_SMS_USER = ''
 MINI_SMS_PASSWORD = ''
 MINI_SMS_ENDPOINT = 'https://api.smsbao.com/sms'
 MINI_SMS_CODE_PEPPER = ''
-# Fixed test phone login requires APP_ENV=local and this switch. Keep disabled when deployed.
+# Fixed test phone login is controlled by this switch, independently of APP_ENV and client IP.
+# When enabled, anyone with a listed test number and fixed code can access that test account.
 APP_ENV = production
 MINI_TEST_PHONE_LOGIN = false
-MINI_TEST_PHONE_ALLOWED_IPS = '127.0.0.1,::1'
 # 只有这些对端地址可以设置 X-Forwarded-For。默认信任回环,nginx/负载均衡部署时需列出每一跳代理地址;
-# 保持默认时反向代理后的调用方都会被视为代理自身,登录限流与测试手机号来源白名单都会按单一来源计算。
+# 正确配置代理链,才能按实际调用方 IP 分别计算登录限流。
 MINI_TRUSTED_PROXIES = '127.0.0.1,::1'
 MINI_SUPPORT_PHONE = ''
 MINI_PURCHASE_AGREEMENT_URL = ''
 MINI_PRIVACY_URL = ''
 # Exact browser origins for Swagger UI / H5, comma-separated. Replace with the actual HTTPS origin when deployed.
-MINI_ALLOWED_ORIGINS = 'http://127.0.0.1:8787,http://localhost:8787'
+MINI_ALLOWED_ORIGINS = 'http://127.0.0.1:8787,http://localhost:8787'

+ 2 - 3
server/app/miniapi/auth/TestPhoneIdentityProvider.php

@@ -6,7 +6,7 @@ namespace app\miniapi\auth;
 
 use app\miniapi\exception\ApiException;
 
-/** Explicit local fixture identities, never verified phone numbers or WeChat accounts. */
+/** Explicit fixture identities, never verified phone numbers or WeChat accounts. */
 final class TestPhoneIdentityProvider implements IdentityProvider
 {
     public const APP_ID = 'local-test-phone';
@@ -22,8 +22,7 @@ final class TestPhoneIdentityProvider implements IdentityProvider
 
     public static function enabled(): bool
     {
-        return config('mini_api.environment') === 'local'
-            && config('mini_api.test_phone_login', false) === true;
+        return config('mini_api.test_phone_login', false) === true;
     }
 
     public function exchange(string $code): array

+ 3 - 8
server/app/miniapi/controller/AuthController.php

@@ -49,13 +49,8 @@ final class AuthController extends BaseController
 
     public function testPhone(Request $request): Response
     {
-        // Throttling and the source allowlist must judge the same caller; sharing one
-        // resolved address here is what keeps a local proxy from opening this endpoint.
-        $source = ClientIp::resolve($request);
-        (new LoginThrottle())->consume($source);
-        $login = new TestPhoneLogin();
-        $login->assertSourceAllowed($source);
-
-        return $this->success($request, $login->login(Input::json($request)));
+        (new LoginThrottle())->consume(ClientIp::resolve($request));
+
+        return $this->success($request, (new TestPhoneLogin())->login(Input::json($request)));
     }
 }

+ 4 - 5
server/app/miniapi/http/ClientIp.php

@@ -8,10 +8,9 @@ use support\Request;
 
 /**
  * Webman's getRemoteIp() only reports the TCP peer, so a reverse proxy collapses every caller
- * onto one address: login throttling then treats the whole internet as a single client, and a
- * loopback proxy satisfies the local test phone allowlist for anyone. Forwarded headers are read
- * only when the direct peer is a configured proxy; otherwise the peer address stands and the
- * caller-supplied header is ignored.
+ * onto one address: login throttling then treats the whole internet as a single client.
+ * Forwarded headers are read only when the direct peer is a configured proxy; otherwise
+ * the peer address stands and the caller-supplied header is ignored.
  */
 final class ClientIp
 {
@@ -68,4 +67,4 @@ final class ClientIp
 
         return $packed === false ? null : (string) inet_ntop($packed);
     }
-}
+}

+ 10 - 1
server/app/miniapi/service/SystemServiceSetup.php

@@ -24,7 +24,15 @@ final class SystemServiceSetup
             ];
         }
         if (!($capabilities['test_phone_login'] ?? false)) {
-            $messages['test_phone_login'] = ['action' => 'optional', 'summary' => 'Local test phone login is disabled.', 'steps' => ['Use APP_ENV=local and MINI_TEST_PHONE_LOGIN=true only for local testing.', 'Restart Webman after changing the setting.']];
+            $messages['test_phone_login'] = [
+                'action' => 'optional',
+                'summary' => 'Test phone login is disabled.',
+                'steps' => [
+                    'Set MINI_TEST_PHONE_LOGIN=true to enable the three fixed test accounts.',
+                    'Test login does not require a specific APP_ENV or client IP.',
+                    'Reload Webman after changing the setting.',
+                ],
+            ];
         }
         if (($capabilities['sms_login'] ?? false) === false && config('mini_api.sms_login', false) === true) {
             $messages['sms_login'] = [
@@ -37,6 +45,7 @@ final class SystemServiceSetup
                 ],
             ];
         }
+
         return $messages;
     }
 }

+ 0 - 7
server/app/miniapi/service/TestPhoneLogin.php

@@ -11,13 +11,6 @@ use app\miniapi\model\MiniUser;
 
 final class TestPhoneLogin
 {
-    public function assertSourceAllowed(string $remoteIp): void
-    {
-        if (!in_array($remoteIp, config('mini_api.test_phone_allowed_ips', []), true)) {
-            throw new ApiException(40301, '测试手机号登录仅允许指定来源', 403);
-        }
-    }
-
     public function login(array $input): array
     {
         if (!TestPhoneIdentityProvider::enabled()) {

+ 2 - 6
server/config/mini_api.php

@@ -1,19 +1,15 @@
 <?php
 
 return [
-    // Independent opaque sessions; local test identities are isolated from WeChat.
+    // Independent opaque sessions; test identities are isolated from WeChat.
     // This API never uses the admin JWT verifier or trusts user IDs sent by the client.
     'token_verifier' => \app\miniapi\auth\SessionVerifier::class,
     'wechat_appid' => env('MINI_WECHAT_APPID', ''),
     'wechat_secret' => env('MINI_WECHAT_SECRET', ''),
     'environment' => env('APP_ENV', 'production'),
     'test_phone_login' => filter_var(env('MINI_TEST_PHONE_LOGIN', false), FILTER_VALIDATE_BOOLEAN),
-    'test_phone_allowed_ips' => array_values(array_filter(array_map('trim', explode(',', (string) env(
-        'MINI_TEST_PHONE_ALLOWED_IPS',
-        '127.0.0.1,::1'
-    ))))),
     // Only these peers may set X-Forwarded-For. Behind a reverse proxy every caller otherwise
-    // shares one address, which merges login throttling and the test phone allowlist.
+    // shares one address, which merges login throttling.
     // Loopback is trusted by default because it is unreachable from the network; list every
     // proxy hop explicitly when deploying behind Nginx or a load balancer.
     'trusted_proxies' => array_values(array_filter(array_map('trim', explode(',', (string) env(

+ 6 - 6
server/docs/WATER_MINI_API.md

@@ -4,11 +4,11 @@ UI 依据:`D:\水后台UI\mobile\M01.png` 至 `M18.png`。后端接口持续
 
 前端交接使用 [WATER_MINI_FRONTEND.md](WATER_MINI_FRONTEND.md) 和 [OpenAPI 3.0.3 定义](openapi/water-mini-v1.json)。本文件保留架构、后台绑定和维护说明;规则整改及验证记录见 [WATER_MINI_REVIEW_FIXES.md](WATER_MINI_REVIEW_FIXES.md)。
 
-## 2026-10-13 本机测试手机号入口
+## 2026-10-24 测试手机号入口
 
-本地测试入口 `POST /auth/test-phone` 使用三个独立测试号:企业 `13012345678`、司机 `13012345679`、代理 `13012345680`,固定验证码均为 `66666`。返回与微信登录相同的 Login 结构。仅 `APP_ENV=local` 且 `MINI_TEST_PHONE_LOGIN=true` 时启用,默认只接受 `127.0.0.1` 和 `::1` 来源。局域网设备调试时,可在 `MINI_TEST_PHONE_ALLOWED_IPS` 中显式添加设备 IP(逗号分隔),并重载 Webman。`GET /config` 的 `capabilities.test_phone_login` 控制前端测试登录;该能力标志不表示当前客户端 IP 已获准。正式短信登录已单独接入,此测试接口不会验证手机号所有权或关联同号微信账号。
+测试入口 `POST /auth/test-phone` 使用三个独立测试号:企业 `13012345678`、司机 `13012345679`、代理 `13012345680`,固定验证码均为 `66666`。返回与微信登录相同的 Login 结构。按用户明确要求,`MINI_TEST_PHONE_LOGIN=true` 时即可登录,不再限制客户端 IP,也不要求 `APP_ENV=local`。`MINI_TEST_PHONE_ALLOWED_IPS` 已移除,不再生效。`GET /config` 的 `capabilities.test_phone_login` 控制前端测试登录。正式短信登录单独校验,此测试接口不会验证手机号所有权或关联同号微信账号。
 
-该来源白名单与登录限流使用同一个解析后的调用方地址,因此本机默认的 `127.0.0.1,::1` 不会因为前面挂了回环代理而对所有来源放行;实际放行仍取决于调用方解析出的真实地址。
+测试登录仍保留来源 IP 限流、全局限流、指定号码/验证码校验、账号停用校验和现有业务授权。开启后,知道测试凭据的调用方可从任意来源访问对应测试账号;关闭开关并重载后端配置后,既有测试会话也失效。
 
 ## 2026-10-21 微信登录失败分流
 
@@ -25,7 +25,7 @@ UI 依据:`D:\水后台UI\mobile\M01.png` 至 `M18.png`。后端接口持续
 
 先执行 `php -d extension=mbstring scripts/seed_mini_test_phone.php --apply` 初始化三个独立的 `local-test-phone` 账号及对应测试档案权限,同时停用原企业测试号上的司机和代理授权。脚本复跑不覆盖账号/授权停用状态,不创建业务收款、库存或订单。会话仍随机生成并只保存哈希,沿用账号停用、权限校验、限流与退出撤销。关闭开关并重新加载后端配置后,既有测试会话不能继续访问。
 
-`tests/mini_test_phone_test.php` 在事务回滚中覆盖固定凭据、非本机来源、生产环境、关闭开关、账号停用、独立会话与注销。本机实际 HTTP 已验证登录后读取三个身份及无权限资源被拒绝。前端运行、微信开发者工具导入目录和 API 地址配置见 `water-mini/README.md`。
+`tests/mini_test_phone_test.php` 在事务回滚中覆盖固定凭据、production环境开启/关闭、账号停用、独立会话与注销;直接调用 HTTP 控制器,验证三个号码在局域网、公网、IPv6 和可信代理转发来源下均返回200并生成可校验的会话。前端运行、微信开发者工具导入目录和 API 地址配置见 `water-mini/README.md`。
 
 ## 分层与设计模式
 
@@ -51,7 +51,7 @@ routes/mini_api.php
 ## 公共契约
 
 - 基础路径:`/api/mini/v1`,本机为 `http://127.0.0.1:8787/api/mini/v1`。
-- 公开接口:`GET /health`、`GET /config`、`GET /packages`、`GET /packages/{id}`、`POST /auth/wechat`、`POST /auth/send-sms`、`POST /auth/phone-login`,以及仅在本地测试配置下可用的 `POST /auth/test-phone`。
+- 公开接口:`GET /health`、`GET /config`、`GET /packages`、`GET /packages/{id}`、`POST /auth/wechat`、`POST /auth/send-sms`、`POST /auth/phone-login`,以及由测试开关控制的 `POST /auth/test-phone`。
 - 其他接口:`Authorization: Bearer <access_token>`。后台 JWT 与小程序令牌不能混用。
 - POST 使用 `Content-Type: application/json`,顶层必须是 JSON 对象;命令不把 URL 查询参数混入 JSON。
 - 金额均为人民币整数分;数量为桶;业务时间为 Asia/Shanghai,`Y-m-d H:i:s`。
@@ -86,7 +86,7 @@ routes/mini_api.php
 
 登录限流以真实调用方地址计算:单 IP 60 次/分钟,全机 600 次/分钟。文件锁在单机多进程间共享;多机部署前改为共享限流设施。
 
-调用方地址由 `app/miniapi/http/ClientIp.php` 解析。仅当直连对端地址列在 `MINI_TRUSTED_PROXIES`(默认 `127.0.0.1,::1`)中时才解析 `X-Forwarded-For`,并从右向左跳过受信任的代理跳;否则直接使用 TCP 对端地址,调用方自带的该请求头被完全忽略。因此未配置该项时行为与只信任 TCP 地址一致,但反向代理后的所有调用方会被视为同一来源,登录限流与测试手机号来源白名单都会按聚合流量计算。部署在 nginx 或负载均衡之后时,须把每一跳代理地址写入 `MINI_TRUSTED_PROXIES` 并重载 Webman。
+调用方地址由 `app/miniapi/http/ClientIp.php` 解析。仅当直连对端地址列在 `MINI_TRUSTED_PROXIES`(默认 `127.0.0.1,::1`)中时才解析 `X-Forwarded-For`,并从右向左跳过受信任的代理跳;否则直接使用 TCP 对端地址,调用方自带的该请求头被完全忽略。该地址用于登录限流,不再用于限制测试登录来源。部署在 nginx 或负载均衡之后时,须把每一跳代理地址写入 `MINI_TRUSTED_PROXIES` 并重载 Webman,才能按实际调用方计算限流。
 
 ## 2026-10-21 短信验证码登录
 

+ 1 - 1
server/docs/openapi/water-mini-v1.json

@@ -4848,7 +4848,7 @@
         ],
         "summary": "本地测试手机号登录",
         "operationId": "post_auth_test_phone",
-        "description": "仅 APP_ENV=local 且 MINI_TEST_PHONE_LOGIN=true 开启,要求连接来源为本机回环地址(不信任代理头)。固定测试身份,与微信及真实手机身份隔离;账号须预先初始化,仅获得已绑定权限。关闭开关后既有测试会话失效。非短信验证接口,禁止正式部署启用。",
+        "description": "由 MINI_TEST_PHONE_LOGIN=true 显式开启,不限制 APP_ENV 或客户端 IP。仅三个指定测试号码可使用固定验证码 66666 登录,仍有来源 IP 及全局限流。固定测试身份与微信及真实手机身份隔离;账号须预先初始化且启用,仅获得已绑定权限。关闭开关后既有测试会话失效。开启时知道测试凭据的调用方可从任意来源访问对应测试账号。",
         "parameters": [],
         "responses": {
           "200": {

+ 1 - 1
server/public/docs/mini/openapi.json

@@ -4848,7 +4848,7 @@
         ],
         "summary": "本地测试手机号登录",
         "operationId": "post_auth_test_phone",
-        "description": "仅 APP_ENV=local 且 MINI_TEST_PHONE_LOGIN=true 开启,要求连接来源为本机回环地址(不信任代理头)。固定测试身份,与微信及真实手机身份隔离;账号须预先初始化,仅获得已绑定权限。关闭开关后既有测试会话失效。非短信验证接口,禁止正式部署启用。",
+        "description": "由 MINI_TEST_PHONE_LOGIN=true 显式开启,不限制 APP_ENV 或客户端 IP。仅三个指定测试号码可使用固定验证码 66666 登录,仍有来源 IP 及全局限流。固定测试身份与微信及真实手机身份隔离;账号须预先初始化且启用,仅获得已绑定权限。关闭开关后既有测试会话失效。开启时知道测试凭据的调用方可从任意来源访问对应测试账号。",
         "parameters": [],
         "responses": {
           "200": {

+ 2 - 2
server/scripts/seed_mini_test_phone.php

@@ -1,6 +1,6 @@
 <?php
 
-/** Initialize three separate local test accounts with one demo grant each. */
+/** Initialize three separate test accounts with one demo grant each. */
 declare(strict_types=1);
 
 require dirname(__DIR__) . '/vendor/autoload.php';
@@ -15,7 +15,7 @@ use app\miniapi\service\Access;
 use app\miniapi\service\IdentityManagement;
 
 if (!TestPhoneIdentityProvider::enabled()) {
-    throw new RuntimeException('Requires APP_ENV=local and MINI_TEST_PHONE_LOGIN=true');
+    throw new RuntimeException('Requires MINI_TEST_PHONE_LOGIN=true');
 }
 $targets = [
     ['enterprise_admin', TestPhoneIdentityProvider::PHONES['enterprise_admin'], '【测试0910】科技园配送测试'],

+ 60 - 7
server/tests/mini_test_phone_test.php

@@ -7,12 +7,31 @@ require dirname(__DIR__) . '/vendor/autoload.php';
 
 use app\miniapi\auth\TestPhoneIdentityProvider;
 use app\miniapi\auth\SessionVerifier;
+use app\miniapi\controller\AuthController;
 use app\miniapi\exception\ApiException;
 use app\miniapi\model\MiniUser;
 use app\miniapi\model\MiniSession;
 use app\miniapi\service\TestPhoneLogin;
 use app\miniapi\service\AuthService;
 
+final class TestPhoneRequest extends \support\Request
+{
+    public function __construct(private readonly string $source, array $input, string $forwarded = '')
+    {
+        $body = json_encode($input, JSON_THROW_ON_ERROR);
+        parent::__construct(
+            "POST /api/mini/v1/auth/test-phone HTTP/1.1\r\nHost: localhost\r\n"
+            . "Content-Type: application/json\r\nX-Forwarded-For: " . $forwarded . "\r\n"
+            . 'Content-Length: ' . strlen($body) . "\r\n\r\n" . $body
+        );
+    }
+
+    public function getRemoteIp(): string
+    {
+        return $this->source;
+    }
+}
+
 $checks = 0;
 function check(bool $ok, string $label): void
 {
@@ -42,19 +61,21 @@ function settings(array $changes): void
     (new ReflectionProperty(\Webman\Config::class, 'flatCache'))->setValue(null, []);
 }
 $original = config('mini_api');
+$rateFile = tempnam(sys_get_temp_dir(), 'mini-test-phone-');
 $db = (new MiniUser())->getConnection();
 $db->beginTransaction();
 try {
     $service = new TestPhoneLogin();
     $input = ['phone' => TestPhoneIdentityProvider::PHONES['enterprise_admin'], 'code' => '66666'];
     settings(['environment' => 'production', 'test_phone_login' => true]);
+    check(TestPhoneIdentityProvider::enabled(), 'Production permits test accounts when explicitly enabled');
+    settings(['test_phone_login' => false]);
     reject(fn () => $service->login($input), 50301);
-    settings(['environment' => 'local', 'test_phone_login' => false]);
-    reject(fn () => $service->login($input), 50301);
-    settings(['environment' => 'local', 'test_phone_login' => true]);
-    settings(['test_phone_allowed_ips' => ['127.0.0.1', '::1']]);
-    $service->assertSourceAllowed('127.0.0.1');
-    reject(fn () => $service->assertSourceAllowed('192.168.1.10'), 40301);
+    settings([
+        'test_phone_login' => true,
+        'login_throttle_file' => $rateFile,
+        'trusted_proxies' => ['127.0.0.1', '::1'],
+    ]);
     reject(fn () => $service->login($input + ['user_id' => 1]), 42201);
     reject(fn () => $service->login(['phone' => '13012345681', 'code' => '66666']), 40102);
     reject(fn () => $service->login(['phone' => '13012345678', 'code' => '66665']), 40102);
@@ -76,6 +97,34 @@ try {
     }
     check(count(array_unique(array_column($sessions, 'user_id'))) === 3, 'Distinct test accounts');
     check(MiniUser::query()->count() === $count, 'No duplicate users');
+
+    // Exercise the real HTTP controller, not only the service. The original regression
+    // happened before credential validation, at the controller's source-IP gate.
+    $controller = new AuthController();
+    foreach ([
+        ['192.168.1.20', ''],
+        ['203.0.113.20', ''],
+        ['2001:db8::20', ''],
+        ['127.0.0.1', '198.51.100.25'],
+    ] as [$source, $forwarded]) {
+        foreach (TestPhoneIdentityProvider::PHONES as $role => $phone) {
+            $request = new TestPhoneRequest($source, ['phone' => $phone, 'code' => '66666'], $forwarded);
+            $response = $controller->testPhone($request);
+            $body = json_decode($response->rawBody(), true, 512, JSON_THROW_ON_ERROR);
+            check($response->getStatusCode() === 200 && $body['code'] === 0, 'HTTP test login succeeds from ' . $source);
+            check($body['data']['user_id'] === (string) $users[$role]->id, 'HTTP returns the correct account for ' . $role);
+            check(
+                (new SessionVerifier())->verify($body['data']['access_token'])->userId === (string) $users[$role]->id,
+                'HTTP session authenticates for ' . $role
+            );
+        }
+    }
+    reject(fn () => $controller->testPhone(new TestPhoneRequest('203.0.113.20', ['phone' => $input['phone'], 'code' => '66665'])), 40102);
+    reject(fn () => $controller->testPhone(new TestPhoneRequest('203.0.113.20', ['phone' => '13800138000', 'code' => '66666'])), 40102);
+    settings(['test_phone_login' => false]);
+    reject(fn () => $controller->testPhone(new TestPhoneRequest('203.0.113.20', $input)), 50301);
+    settings(['test_phone_login' => true]);
+
     $one = $sessions['enterprise_admin'];
     $two = $service->login($input);
     check($one['user_id'] === $two['user_id'], 'Stable identity');
@@ -85,7 +134,10 @@ try {
     settings(['test_phone_login' => false]);
     reject(fn () => (new SessionVerifier())->verify($one['access_token']), 40101);
     settings(['test_phone_login' => true, 'environment' => 'production']);
-    reject(fn () => (new SessionVerifier())->verify($one['access_token']), 40101);
+    check(
+        (new SessionVerifier())->verify($one['access_token'])->userId === (string) $users['enterprise_admin']->id,
+        'Production sessions remain valid when test login is enabled'
+    );
     settings(['environment' => 'local']);
     MiniUser::query()->where('id', $users['enterprise_admin']->id)->update(['status' => 0]);
     reject(fn () => $service->login($input), 40302);
@@ -97,5 +149,6 @@ try {
 } finally {
     $db->rollBack();
     settings($original);
+    unlink($rateFile);
 }
 echo "PASS: $checks test phone checks, database changes rolled back.\n";

+ 3 - 3
water-mini/README.md

@@ -6,13 +6,13 @@ uni-app Vue 3 + TypeScript,微信小程序为主要交付端,同时提供 H5
 
 构建并发布本机 H5 后打开 <http://127.0.0.1:8787/water-mini/index.html>。默认复用当前后端服务,不另起接口代理。首页和套餐公开可浏览;个人、企业、司机及代理数据需要登录及授权。可以使用下述明确隔离的本地测试账号,业务读写仍走真实接口和身份权限。
 
-## 本地测试手机号登录
+## 测试手机号登录
 
 页面上的“手机验证码登录”同时支持测试账号与正式手机号。三个指定测试号直接输入固定验证码 `66666`,调用 `/auth/test-phone`,无需获取或发送短信;其余号码调用 `/auth/send-sms` 获取随机六位短信码,再通过 `/auth/phone-login` 登录。短信验证码两分钟有效,过期必须重新获取,重发后旧码作废。发送间隔60秒与验证码有效期120秒是两个独立规则。
 
 手机号入口在 `capabilities.sms_login` 或 `capabilities.test_phone_login` 任一开启时显示,但各自独立控制对应登录方式。测试号只走测试接口,不会因为测试能力关闭而改走短信。获取短信无需先勾选协议,提交登录仍需同意协议。正式手机号沿用后台预绑定已有账号的规则。
 
-在后端 `.env` 设置 `APP_ENV=local` 和 `MINI_TEST_PHONE_LOGIN=true`,执行 `php -d extension=mbstring scripts/seed_mini_test_phone.php --apply` 并重启后端。本地固定验证码均为 `66666`,不发送短信。每个测试账号只绑定一种身份,登录后直接进入对应页面:
+在后端 `.env` 设置 `MINI_TEST_PHONE_LOGIN=true`,执行 `php -d extension=mbstring scripts/seed_mini_test_phone.php --apply` 并重载后端。按用户要求,测试登录不限制 `APP_ENV` 或客户端 IP,开发者工具和真机均可使用;固定验证码均为 `66666`,不发送短信。每个测试账号只绑定一种身份,登录后直接进入对应页面:
 
 | 角色 | 测试手机号 | 登录后页面 |
 | --- | --- | --- |
@@ -20,7 +20,7 @@ uni-app Vue 3 + TypeScript,微信小程序为主要交付端,同时提供 H5
 | 配送员 | `13012345679` | 配送工作台 |
 | 代理商 | `13012345680` | 代理工作台 |
 
-初始化脚本仅绑定指定的既有测试档案(科技园配送测试、地图配送员-KJY、测试商贸),并停用原企业测试号上的司机、代理授权;不新增订单、库存或收款。重复运行不恢复被管理员停用的账号或身份。关闭开关并重启后端后,既有测试 Token 同时失效。正式部署保持 `APP_ENV=production`、`MINI_TEST_PHONE_LOGIN=false`。真实短信服务的配置、手机号绑定及迁移说明见 `../server/docs/WATER_MINI_API.md` 的“短信验证码登录”。
+初始化脚本仅绑定指定的既有测试档案(科技园配送测试、地图配送员-KJY、测试商贸),并停用原企业测试号上的司机、代理授权;不新增订单、库存或收款。重复运行不恢复被管理员停用的账号或身份。关闭开关并重载后端后,既有测试 Token 同时失效。开启后,知道测试凭据的人可访问对应测试账号,因此开关默认关闭,开启时使用测试业务档案;账号停用、权限和登录限流仍生效。真实短信服务的配置、手机号绑定及迁移说明见 `../server/docs/WATER_MINI_API.md` 的“短信验证码登录”。
 
 ## 安装与运行