Procházet zdrojové kódy

token 24小时,去掉误加接口

doge před 9 hodinami
rodič
revize
2804a66762

+ 0 - 31
app/Http/Controllers/api/Rebate.php

@@ -1,31 +0,0 @@
-<?php
-
-namespace App\Http\Controllers\api;
-
-use App\Services\RebateService;
-use Illuminate\Http\JsonResponse;
-use Illuminate\Validation\ValidationException;
-
-class Rebate extends BaseController
-{
-    public function index(): JsonResponse
-    {
-        $memberId = request()->user->member_id ?? null;
-        if (empty($memberId)) {
-            return response()->json(['code' => 401, 'msg' => lang('用户不存在')], 401);
-        }
-
-        try {
-            $search = request()->validate([
-                'page' => ['nullable', 'integer', 'min:1'],
-                'limit' => ['nullable', 'integer', 'min:1', 'max:100'],
-                'date' => ['nullable', 'date_format:Y-m-d'],
-                'status' => ['nullable', 'integer', 'in:0,1'],
-            ]);
-
-            return $this->success(RebateService::paginateForMember((string) $memberId, $search));
-        } catch (ValidationException $e) {
-            return $this->error($e->validator->errors()->first());
-        }
-    }
-}

+ 5 - 2
app/Http/Middleware/CheckToken.php

@@ -18,7 +18,7 @@ class CheckToken
      */
     public function handle(Request $request, Closure $next)
     {
-        // 1. 从请求头获取 token(header 名称:token / Token 都支持)
+        // 1. 前台沿用 Authorization 请求头中的原始 Token
         $token = $request->header('authorization');
 
         // 2. 如果请求头没有 token,直接返回未授权
@@ -47,12 +47,15 @@ class CheckToken
         $user = User::where('user_id',$session->user_id)->first();
 
         // 6. 用户不存在
-        if (!$user) {
+        if (!$user || !empty($user->getAttributes()['delete_time'])) {
             return response()->json([
                 'code' => 401,
                 'msg' => lang('用户不存在')
             ], 401);
         }
+        if ((int)($user->getAttributes()['is_banned'] ?? 0) === 1) {
+            return response()->json(['code' => 401, 'msg' => lang('账号已禁用')], 401);
+        }
 
         if (!UserSession::renewActiveToken((string)$token, $time, (int)$session->id)) {
             return response()->json(['code' => 401, 'msg' => lang('token无效或已过期')], 401);

+ 0 - 19
app/Services/RebateService.php

@@ -88,25 +88,6 @@ class RebateService extends BaseService
         return ['total' => $paginator->total(), 'data' => $paginator->items()];
     }
 
-    /** Member list: always scope by the authenticated member and omit admin/member details. */
-    public static function paginateForMember(string $memberId, array $search = []): array
-    {
-        $filters = array_intersect_key($search, array_flip(['date', 'status']));
-        $paginator = Rebate::where('member_id', $memberId)
-            ->where(self::getWhere($filters))
-            ->where('date', '<', Carbon::now('America/New_York')->format('Y-m-d'))
-            ->orderByDesc('date')
-            ->orderBy('status')
-            ->orderByDesc('betting_amount')
-            ->orderByDesc('id')
-            ->paginate($search['limit'] ?? 15, [
-                'id', 'date', 'member_id', 'betting_amount', 'effective_betting_amount',
-                'profit', 'rebate_ratio', 'amount', 'status', 'grant_time',
-            ], 'page', $search['page'] ?? 1);
-
-        return ['total' => $paginator->total(), 'data' => $paginator->items()];
-    }
-
 
     //更新有效投注额
     public static function updateEffectiveBettingAmount($rebate, $amount): void

+ 53 - 0
docs/前端接口/会员Token连续24小时无请求失效-接入说明.md

@@ -0,0 +1,53 @@
+# 会员 Token:连续 24 小时无有效请求失效
+
+会员前台 Token 改为 24 小时滑动有效期。计时依据是服务端收到的最后一次有效鉴权 HTTP 请求,不是浏览器鼠标或键盘事件。
+
+## 1. 有效期规则
+
+- 登录成功时,Token 初始有效期为 24 小时。
+- 每次携带有效 Token 的请求通过鉴权后,到期时间顺延为该次请求时间加 24 小时。
+- 连续 24 小时没有有效鉴权请求,再次访问需登录的接口会返回原有的业务码 `401`。
+- 到达截止时间的当秒即失效,已过期 Token 不会被请求自动续活。
+- 续期使用原 Token,不要求前端替换成新 Token。
+
+例如:10 月 25 日 10:00 登录,初始截止时间为 10 月 26 日 10:00;10 月 25 日 20:00 发出一次有效鉴权请求,截止时间变为 10 月 26 日 20:00。持续请求可以使登录保持超过最初的 24 小时。
+
+## 2. 自动请求是否续期
+
+按本次确认的口径,所有有效鉴权 HTTP 请求都算活动:
+
+| 请求 | 是否续期 |
+|---|---|
+| 用户点击后发出的有效鉴权请求 | 是 |
+| 页面自动轮询且携带有效 Token | 是 |
+| 免登录接口,但请求携带有效 Token | 是 |
+| 不携带 Token 的公开请求 | 否 |
+| Token 已过期、已退出或被新登录替换 | 否 |
+| WebSocket 心跳或只读身份检查 | 否 |
+
+因此,页面持续携带有效 Token 轮询时,即使用户没有点击,也会继续续期;只有停止有效鉴权请求满 24 小时才失效。
+
+## 3. 前端接法
+
+沿用现有接口和请求头:会员接口继续传原始 `Authorization: <token>`,不新增心跳接口、请求参数或活动标记。
+
+会员端继续按现有业务 `code=401` 处理登录失效:清除本地旧 Token,并引导用户重新登录。需要登录的接口在 Token 失效时沿用以下响应结构,具体文案以服务端为准:
+
+```json
+{
+  "code": 401,
+  "show": 0,
+  "msg": "登录失效,请重新登录",
+  "data": []
+}
+```
+
+公开接口仍可匿名访问;失效 Token 不会获得登录身份,也不会通过公开请求被恢复。
+
+登录时若返回 `expire_time`,它表示当时的到期时间,后续有效请求会继续延长服务端期限;判断当前是否失效以服务端鉴权结果为准,不把首次登录的到期时间当作永久截止时间。
+
+## 4. 旧会话与联调
+
+旧会话按数据库记录的最后更新时间校验;旧记录已经超出 24 小时窗口时,需要重新登录。清除缓存不会重新开始计时。
+
+联调检查:登录后有效期为 24 小时;窗口内请求会顺延;超过最初登录的 24 小时但持续活跃仍可访问;最后一次有效请求后的第 24 小时到期后返回 `401`;退出或新登录替换的旧 Token 不会被续期恢复。

+ 0 - 109
docs/前端接口/返佣列表.md

@@ -1,109 +0,0 @@
-# 当前登录用户返佣列表
-
-## 接口
-
-`GET /api/rebate`
-
-请求头:
-
-```http
-Authorization: <用户登录返回的原始 token>
-Accept: application/json
-```
-
-沿用前台 `check.token` 登录校验,`Authorization` 直接传用户 token,不添加 `Bearer ` 前缀,也不使用后台管理员 token。
-
-数据来自 `/admin/rebate` 使用的 `rebates` 表,仅查询当前登录用户的 `member_id`。前端无需传用户编号;传入 `member_id`、`user_id`、`username`、`first_name` 等参数不会改变查询归属,也不会按这些字段筛选。
-
-## 查询参数
-
-| 参数 | 必填 | 说明 |
-| --- | --- | --- |
-| `page` | 否 | 页码,正整数,默认 `1` |
-| `limit` | 否 | 每页条数,整数 `1`~`100`,默认 `15` |
-| `date` | 否 | 指定返佣日期,格式 `Y-m-d`,例如 `2026-10-23` |
-| `status` | 否 | `0`:未返佣;`1`:已返佣;不传或为空时返回全部状态 |
-
-日期口径与 `/admin/rebate` 相同:仅返回 `date` 早于服务器当前纽约日期(`America/New_York`)的记录,当天和未来的记录不显示。指定当天或未来日期时返回空列表。
-
-列表按日期倒序、状态升序、投注额倒序排列;以上条件相同时按记录 ID 倒序,保证分页顺序稳定。
-
-```text
-/api/rebate?page=1&limit=15
-/api/rebate?date=2026-10-23&status=1&page=1&limit=15
-/api/rebate?status=0&page=1&limit=15
-```
-
-## 成功响应
-
-```json
-{
-  "code": 0,
-  "timestamp": 1792900000,
-  "msg": "OK",
-  "data": {
-    "total": 1,
-    "data": [
-      {
-        "id": 123,
-        "date": "2026-10-23",
-        "member_id": "100001",
-        "betting_amount": "1000.00",
-        "effective_betting_amount": "800.00",
-        "profit": "-100.00",
-        "rebate_ratio": "0.0050",
-        "amount": "4.00",
-        "status": 1,
-        "grant_time": "2026-10-24 12:30"
-      }
-    ]
-  }
-}
-```
-
-`data.total` 为当前用户在本次筛选条件下的记录总数,`data.data` 为当前页列表。
-
-| 列表字段 | 说明 |
-| --- | --- |
-| `id` | 返佣记录 ID |
-| `date` | 返佣统计日期,`Y-m-d` |
-| `member_id` | 当前登录用户的会员编号 |
-| `betting_amount` | 当日投注金额 |
-| `effective_betting_amount` | 当日有效投注金额 |
-| `profit` | 当日输赢,正数为盈利,负数为亏损 |
-| `rebate_ratio` | 该记录保存的返佣比例,小数形式;`0.0050` 表示 `0.5%`,历史记录可能为 `null` |
-| `amount` | 记录中的返佣金额;未返佣时通常为 `0`,不能当作预计返佣金额 |
-| `status` | `0`:未返佣;`1`:已返佣 |
-| `grant_time` | 发放时间,`Y-m-d H:i`;未发放时为空字符串;时区沿用服务端配置 |
-
-金额和比例沿用数据库返回的数值或数值字符串,前端展示时统一格式化。未返佣记录的比例是记录中保存的值,实际发放时后台会按当时配置更新比例和金额。
-
-列表仅返回上述字段,不包含后台操作人、会员完整资料、后台备注、手机号、IP 或设备标识。接口只查询,不触发返佣发放或钱包变动。
-
-无记录或页码超过末页时:
-
-```json
-{
-  "code": 0,
-  "timestamp": 1792900000,
-  "msg": "OK",
-  "data": {
-    "total": 0,
-    "data": []
-  }
-}
-```
-
-页码超过末页但筛选结果仍有记录时,`total` 保持实际记录总数,只有列表为空。
-
-## 错误处理
-
-- 未携带 token、token 无效或过期、用户不存在或未关联会员编号:HTTP `401`,响应 `code=401`,前端应重新登录。
-- 参数不合法(例如 `page=0`、`limit=101`、日期格式错误、`status=2`):HTTP `200`,业务 `code=-1`,`msg` 返回参数校验提示,`data=[]`。
-
-```json
-{
-  "code": 401,
-  "msg": "请携带token访问"
-}
-```

+ 1 - 4
routes/api.php

@@ -10,7 +10,6 @@ use App\Http\Controllers\api\Issue;
 use App\Http\Controllers\api\Pay;
 use App\Http\Controllers\api\NewPc;
 use App\Http\Controllers\api\PcIssue;
-use App\Http\Controllers\api\Rebate;
 use App\Http\Controllers\api\Wallet;
 
 Route::post("/onMessage", [TelegramWebHook::class, 'handle']);
@@ -79,10 +78,8 @@ Route::fallback(function () {
     ], 200);
 });
 
-// 前台登录用户接口,需要校验 token
+// 足球app充值提现的接口需要校验 token 的接口
 Route::middleware('check.token')->group(function () {
-    Route::get('/rebate', [Rebate::class, 'index']);
-
     Route::prefix('/wallet')->group(function () {
         Route::get("/scan", [Wallet::class, 'scan']);
         Route::post("/recharge", [Wallet::class, 'recharge']);

+ 0 - 245
tests/Integration/MemberRebateApiTest.php

@@ -1,245 +0,0 @@
-<?php
-
-namespace Tests\Integration;
-
-use App\Http\Controllers\api\Rebate;
-use App\Http\Middleware\CheckToken;
-use Carbon\Carbon;
-use Illuminate\Config\Repository;
-use Illuminate\Database\Capsule\Manager;
-use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Events\Dispatcher;
-use Illuminate\Foundation\Application;
-use Illuminate\Http\JsonResponse;
-use Illuminate\Http\Request;
-use Illuminate\Routing\Router;
-use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Facade;
-use Illuminate\Translation\ArrayLoader;
-use Illuminate\Translation\Translator;
-use Illuminate\Validation\Factory;
-use PHPUnit\Framework\TestCase;
-
-/** Real API routes, member-token middleware and queries, using SQLite without loading .env. */
-class MemberRebateApiTest extends TestCase
-{
-    private Application $app;
-    private Router $router;
-
-    protected function setUp(): void
-    {
-        parent::setUp();
-        Carbon::setTestNow(Carbon::parse('2026-10-25 00:30:00', 'Asia/Shanghai'));
-        Facade::clearResolvedInstances();
-        $this->app = new Application(dirname(__DIR__, 2));
-        $this->app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']]));
-        Facade::setFacadeApplication($this->app);
-        $db = new Manager($this->app);
-        $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
-        $db->setAsGlobal();
-        $db->bootEloquent();
-        $this->app->instance('db', $db->getDatabaseManager());
-        $this->app->bind('db.schema', fn () => $db->schema());
-        $translator = new Translator(new ArrayLoader(), 'zh');
-        $this->app->instance('translator', $translator);
-        $this->app->instance('validator', new Factory($translator, $this->app));
-        $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new class {
-            public function json($data, $status = 200, $headers = [], $options = 0) {
-                return new JsonResponse($data, $status, $headers, $options);
-            }
-        });
-        Request::macro('validate', function (array $rules) {
-            return app('validator')->make($this->all(), $rules)->validate();
-        });
-        if (!function_exists('lang')) require_once dirname(__DIR__, 2) . '/app/Helpers/helpers.php';
-        $db->schema()->create('users', function (Blueprint $t) {
-            $t->id(); $t->string('user_id'); $t->string('member_id')->nullable();
-        });
-        $db->schema()->create('user_session', function (Blueprint $t) {
-            $t->id(); $t->string('user_id'); $t->string('token');
-            $t->bigInteger('expire_time'); $t->bigInteger('create_time'); $t->bigInteger('update_time');
-        });
-        foreach ([
-            '2025_10_31_145445_create_rebates.php',
-            '2025_10_31_155143_update_rebates.php',
-            '2025_11_03_144118_update_rebates.php',
-            '2025_11_20_145643_update_rebates.php',
-            '2025_11_20_151650_update_rebates.php',
-            '2026_01_13_153629_update_rebates.php',
-            '2026_01_28_093600_update_rebate.php',
-        ] as $migration) {
-            (require dirname(__DIR__, 2) . '/database/migrations/' . $migration)->up();
-        }
-        foreach (['one' => '100001', 'two' => '200002', 'unlinked' => null] as $suffix => $memberId) {
-            DB::table('users')->insert(['user_id' => 'login-' . $suffix, 'member_id' => $memberId]);
-            DB::table('user_session')->insert([
-                'user_id' => 'login-' . $suffix, 'token' => 'token-' . $suffix,
-                'expire_time' => now()->timestamp + 86400,
-                'create_time' => now()->timestamp, 'update_time' => now()->timestamp,
-            ]);
-        }
-        $this->router = new Router(new Dispatcher($this->app), $this->app);
-        $this->app->instance('router', $this->router);
-        $this->router->aliasMiddleware('check.token', CheckToken::class);
-        $this->router->group(['prefix' => 'api'], function () {
-            require dirname(__DIR__, 2) . '/routes/api.php';
-        });
-    }
-
-    protected function tearDown(): void
-    {
-        Carbon::setTestNow();
-        DB::disconnect();
-        Facade::clearResolvedInstances();
-        parent::tearDown();
-    }
-
-    private function request(array $params = [], ?string $token = 'token-one'): JsonResponse
-    {
-        $request = Request::create('/api/rebate', 'GET', $params);
-        if ($token !== null) $request->headers->set('Authorization', $token);
-        $this->app->instance('request', $request);
-        return $this->router->dispatch($request);
-    }
-
-    private function rebate(array $overrides = []): int
-    {
-        return DB::table('rebates')->insertGetId($overrides + [
-            'member_id' => '100001', 'date' => '2026-10-23',
-            'betting_amount' => '1000.00', 'effective_betting_amount' => '800.00',
-            'profit' => '-100.00', 'rebate_ratio' => '0.0050', 'amount' => '0.00', 'status' => 0,
-            'audited_by' => 'internal-admin', 'username' => 'historical-account', 'first_name' => '历史昵称',
-        ]);
-    }
-
-    private function listing(array $params = [], string $token = 'token-one'): array
-    {
-        $response = $this->request($params, $token);
-        $this->assertSame(200, $response->getStatusCode());
-        $body = $response->getData(true);
-        $this->assertSame(0, $body['code'], $body['msg']);
-        return $body['data'];
-    }
-
-    public function test_route_requires_member_login_and_rejects_missing_invalid_expired_and_unlinked_sessions(): void
-    {
-        $this->rebate();
-        $route = $this->router->getRoutes()->match(Request::create('/api/rebate', 'GET'));
-        $this->assertSame(Rebate::class . '@index', $route->getAction('uses'));
-        $this->assertContains('check.token', $route->middleware());
-        $this->assertSame(['GET', 'HEAD'], $route->methods());
-        foreach ([null, 'invalid', 'Bearer token-one', 'token-unlinked'] as $token) {
-            $response = $this->request([], $token);
-            $this->assertSame(401, $response->getStatusCode());
-            $this->assertSame(401, $response->getData(true)['code']);
-        }
-        DB::table('user_session')->where('token', 'token-one')->update(['expire_time' => now()->timestamp]);
-        $this->assertSame(401, $this->request()->getStatusCode());
-    }
-
-    public function test_user_cannot_select_another_members_rows_or_total_by_overriding_identity_parameters(): void
-    {
-        $own = $this->rebate();
-        $other = $this->rebate(['member_id' => '200002', 'betting_amount' => 9000]);
-        $before = DB::table('rebates')->orderBy('id')->get()->toJson();
-        $list = $this->listing([
-            'member_id' => '200002', 'user_id' => 'login-two', 'id' => $other,
-            'username' => 'another-account', 'first_name' => '其他会员',
-            'user' => ['member_id' => '200002'],
-        ]);
-        $this->assertSame(1, $list['total']);
-        $this->assertSame([$own], array_column($list['data'], 'id'));
-        $this->assertSame(['100001'], array_column($list['data'], 'member_id'));
-        $otherList = $this->listing([], 'token-two');
-        $this->assertSame(1, $otherList['total']);
-        $this->assertSame([$other], array_column($otherList['data'], 'id'));
-        $this->assertSame($before, DB::table('rebates')->orderBy('id')->get()->toJson());
-    }
-
-    public function test_date_cutoff_uses_new_york_midnight_and_never_includes_today_or_future_rows(): void
-    {
-        $yesterday = $this->rebate();
-        $today = $this->rebate(['date' => '2026-10-24']);
-        $this->rebate(['date' => '2026-10-25']);
-        $list = $this->listing();
-        $this->assertSame(1, $list['total']);
-        $this->assertSame([$yesterday], array_column($list['data'], 'id'));
-        $this->assertSame(['total' => 0, 'data' => []], $this->listing(['date' => '2026-10-24']));
-        Carbon::setTestNow(Carbon::parse('2026-10-25 00:00:00', 'America/New_York'));
-        $list = $this->listing();
-        $this->assertSame(2, $list['total']);
-        $this->assertSame([$today, $yesterday], array_column($list['data'], 'id'));
-    }
-
-    public function test_filters_sorting_pagination_and_empty_results_use_the_same_member_scope(): void
-    {
-        $paid = $this->rebate(['status' => 1, 'betting_amount' => 9000]);
-        $small = $this->rebate(['betting_amount' => 100]);
-        $large = $this->rebate(['betting_amount' => 200]);
-        $tied = $this->rebate(['betting_amount' => 200]);
-        $older = $this->rebate(['date' => '2026-10-22', 'betting_amount' => 10000]);
-        $this->rebate(['member_id' => '200002', 'status' => 1]);
-        $all = $this->listing();
-        $this->assertSame(5, $all['total']);
-        $this->assertSame([$tied, $large, $small, $paid, $older], array_column($all['data'], 'id'));
-        $unpaid = $this->listing(['status' => '0', 'date' => '2026-10-23']);
-        $this->assertSame(3, $unpaid['total']);
-        $this->assertSame([$tied, $large, $small], array_column($unpaid['data'], 'id'));
-        $page = $this->listing(['status' => 0, 'limit' => 2, 'page' => 2]);
-        $this->assertSame(4, $page['total']);
-        $this->assertSame([$small, $older], array_column($page['data'], 'id'));
-        $paidList = $this->listing(['status' => '1']);
-        $this->assertSame(1, $paidList['total']);
-        $this->assertSame([$paid], array_column($paidList['data'], 'id'));
-        $this->assertSame(['total' => 5, 'data' => []], $this->listing(['page' => 99]));
-        $this->assertSame(['total' => 0, 'data' => []], $this->listing(['date' => '2020-01-01']));
-        $this->assertSame(5, $this->listing(['date' => null, 'status' => null, 'limit' => null, 'page' => null])['total']);
-        $this->assertSame(5, $this->listing(['date' => '', 'status' => ''])['total']);
-    }
-
-    public function test_list_returns_display_fields_and_saved_money_without_serializing_admin_or_dynamic_backflow_fields(): void
-    {
-        $this->rebate(['rebate_ratio' => null]);
-        $grantTime = Carbon::parse('2026-10-24 10:00:00', 'Asia/Shanghai')->timestamp;
-        $paid = $this->rebate(['status' => 1, 'amount' => '4.00', 'grant_time' => $grantTime]);
-        $before = DB::table('rebates')->get()->toJson();
-        $rows = collect($this->listing()['data'])->keyBy('id');
-        $row = $rows[$paid];
-        $this->assertSame([
-            'id', 'date', 'member_id', 'betting_amount', 'effective_betting_amount',
-            'profit', 'rebate_ratio', 'amount', 'status', 'grant_time',
-        ], array_keys($row));
-        $this->assertEquals(800, $row['effective_betting_amount']);
-        $this->assertEquals(-100, $row['profit']);
-        $this->assertEquals(0.005, $row['rebate_ratio']);
-        $this->assertEquals(4, $row['amount']);
-        $this->assertSame(date('Y-m-d H:i', $grantTime), $row['grant_time']);
-        $this->assertSame('', $rows->first()['grant_time']);
-        $this->assertNull($rows->first()['rebate_ratio']);
-        $this->assertSame($before, DB::table('rebates')->get()->toJson());
-    }
-
-    /** @dataProvider invalidFilters */
-    public function test_invalid_filters_return_a_frontend_validation_error(array $params): void
-    {
-        $response = $this->request($params);
-        $this->assertSame(200, $response->getStatusCode());
-        $body = $response->getData(true);
-        $this->assertSame(-1, $body['code']);
-        $this->assertSame([], $body['data']);
-        $this->assertNotEmpty($body['msg']);
-    }
-
-    public static function invalidFilters(): array
-    {
-        return [
-            'page zero' => [['page' => 0]],
-            'invalid page' => [['page' => 'abc']],
-            'limit zero' => [['limit' => 0]],
-            'oversized limit' => [['limit' => 101]],
-            'invalid date' => [['date' => '2026-02-30']],
-            'invalid status' => [['status' => 2]],
-            'array status' => [['status' => [0, 1]]],
-        ];
-    }
-}

+ 8 - 1
tests/Integration/MemberTokenIdleTimeoutTest.php

@@ -38,7 +38,7 @@ class MemberTokenIdleTimeoutTest extends TestCase
             public function json($data, $status = 200, $headers = [], $options = 0) { return new JsonResponse($data, $status, $headers, $options); }
         });
         if (!function_exists('create_token') || !function_exists('lang')) require_once dirname(__DIR__, 2) . '/app/Helpers/helpers.php';
-        $db->schema()->create('users', function (Blueprint $t) { $t->id(); $t->string('user_id'); $t->string('member_id'); });
+        $db->schema()->create('users', function (Blueprint $t) { $t->id(); $t->string('user_id'); $t->string('member_id'); $t->integer('is_banned')->default(0); $t->bigInteger('delete_time')->nullable(); });
         $db->schema()->create('user_session', function (Blueprint $t) {
             $t->id(); $t->string('user_id'); $t->string('token'); $t->bigInteger('expire_time');
             $t->bigInteger('create_time'); $t->bigInteger('update_time')->nullable();
@@ -106,6 +106,13 @@ class MemberTokenIdleTimeoutTest extends TestCase
         $this->assertSame(401, $this->request('replacement')->getStatusCode());
         $this->assertSame($before, (array)DB::table('user_session')->first());
         DB::table('user_session')->update(['expire_time' => $this->start + 86400]);
+        DB::table('users')->update(['is_banned' => 1]);
+        $before = (array)DB::table('user_session')->first();
+        $this->assertSame(401, $this->request('replacement')->getStatusCode());
+        $this->assertSame($before, (array)DB::table('user_session')->first());
+        DB::table('users')->update(['is_banned' => 0, 'delete_time' => $this->start]);
+        $this->assertSame(401, $this->request('replacement')->getStatusCode());
+        $this->assertSame($before, (array)DB::table('user_session')->first());
         DB::table('users')->delete();
         $before = (array)DB::table('user_session')->first();
         $this->assertSame(401, $this->request('replacement')->getStatusCode());

+ 17 - 1
tests/Regression/member_token_idle_timeout.php

@@ -32,7 +32,7 @@ namespace {
     ]], 'auto_timestamp' => false]);
     $app->instance(\think\DbManager::class, $db); $app->instance('log', new \Psr\Log\NullLogger());
     $db->execute('CREATE TABLE bot_user_session (id INTEGER PRIMARY KEY,user_id TEXT,token TEXT,expire_time INTEGER,create_time INTEGER,update_time INTEGER)');
-    $db->execute('CREATE TABLE bot_users (id INTEGER PRIMARY KEY,user_id TEXT,account TEXT,avatar TEXT,"from" INTEGER,recharge_channel_group_id INTEGER,delete_time TEXT,created_at TEXT,updated_at TEXT)');
+    $db->execute('CREATE TABLE bot_users (id INTEGER PRIMARY KEY,user_id TEXT,account TEXT,avatar TEXT,"from" INTEGER,recharge_channel_group_id INTEGER,is_banned INTEGER DEFAULT 0,delete_time TEXT,created_at TEXT,updated_at TEXT)');
     \think\facade\Db::table('bot_users')->insert(['id' => 1, 'user_id' => '100001', 'account' => 'member', 'avatar' => '', 'from' => 1, 'recharge_channel_group_id' => 1]);
     $GLOBALS['idle_token_now'] = 1792912735;
     $start = $GLOBALS['idle_token_now'];
@@ -60,6 +60,10 @@ namespace {
     check($info['expire_time'], $start + 86400, 'Login issues 24-hour token');
     check(row($token)['update_time'], $start, 'Login records the activity clock');
     check(\think\facade\Cache::$entries['token_user_' . $token]['ttl'], 86400, 'Cache TTL equals the effective expiry');
+    \think\facade\Cache::set('token_user_' . $token, ['token' => $token, 'user_id' => 'wrong-user', 'expire_time' => $start + 7 * 86400], 7 * 86400);
+    $corrected = $cache->getUserInfo($token);
+    check((string)$corrected['user_id'], '100001', 'A stale cached lookup hint cannot change session identity');
+    check($corrected['expire_time'], $start + 86400, 'Cached seven-day deadline cannot replace the database inactivity deadline');
 
     $GLOBALS['idle_token_now'] = $start + 3600;
     $unchanged = row($token);
@@ -142,9 +146,21 @@ namespace {
     $laterRequest = row($newToken);
     check(authenticate($newToken)['code'], 1, 'Older concurrent request still sees the valid renewed session');
     check(row($newToken), $laterRequest, 'An older renewal never moves deadline backwards');
+    \think\facade\Db::table('bot_users')->where('user_id', '100001')->update(['is_banned' => 1]);
+    $beforeBanned = row($newToken);
+    check($cache->getUserInfo($newToken), false, 'Read-only socket/crypto cache lookup also rejects a banned account');
+    check(authenticate($newToken)['code'], 401, 'Banned account cannot keep a cached token alive indefinitely');
+    check(row($newToken), $beforeBanned, 'Banned account does not count as valid activity');
+    \think\facade\Db::table('bot_users')->where('user_id', '100001')->update(['is_banned' => 0]);
     \think\facade\Db::table('bot_users')->where('user_id', '100001')->update(['delete_time' => time()]);
     $beforeMissingUser = row($newToken);
+    check(empty($cache->getUserInfo($newToken)), true, 'Deleted identity cannot survive through a stale cached profile');
     check(authenticate($newToken)['code'], 401, 'Deleted user cannot authenticate through a cached profile');
     check(row($newToken), $beforeMissingUser, 'Failed identity check does not count as activity');
+    \think\facade\Db::table('bot_users')->insert(['id' => 2, 'user_id' => '100002', 'account' => '', 'avatar' => '', 'from' => 2, 'recharge_channel_group_id' => 1]);
+    $guest = \app\api\service\UserTokenService::setToken('100002');
+    check($guest['from'], 2, 'Guest identity remains a guest');
+    check($guest['expire_time'], $GLOBALS['idle_token_now'] + 86400, 'Guest token also receives a sliding-day lifetime');
+    check(authenticate($guest['token'])['code'], 1, 'Token timeout change does not disable existing guest authentication');
     echo 'PASS ' . $checks . " member idle-token assertions (isolated SQLite and memory cache; no live sessions)\n";
 }