# 用户/骑手/商家 App 单设备登录 > 来源:需求讨论(参照 cte_server 已有实现),2026-09-22 > 状态:后端已实现 > 记录人:开发 ## 需求 用户、骑手、商家三个 App,一个账号只能在一台设备登录;新设备登录后旧设备被踢下线。 ## cte_server 参考实现(两个半环) 1. **登录删旧会话**:三端登录时 `deleteKeys(tokenKey + userId + ":*")`;商家按 User-Agent 分 APP/PC 两个 key,只踢 APP(PC 独立并存,即商家 = 1 App + 1 PC)。 2. **鉴权强制验会话**(`AuthAspect`):JWT 验签后,jti 含冒号(新格式会话 token)→ Redis 必须存在该键,否则 401;无冒号的历史 token 跳过(兼容)。 ## foodie-server 改造前现状 - 登录删旧会话:**已齐全**(与 cte 同构代码):`shanglodeing`(APP 踢 APP/PC 独立)、`syslodeing` 踢 QS、`lodeing` 踢 USER、oauthLogin/LineCallback/注册路径均踢。 - 鉴权侧:`AuthAspect` 被改造为 `@Auth(session=true)` 才校验,且仅对商家账号(userType 1/3/4/5)拒绝;**用户(0)/骑手(2) 显式豁免**——登录虽删了旧会话,但旧设备 token(15 天内)在所有接口照常放行,单设备实际不生效(半成品)。 ## 本次改动 | 文件 | 改动 | |------|------| | `Auth.java` | 删除 `session()` 属性(全局校验后无存在意义;8 个商家控制器 40 处 `@Auth(session = true)` 同步改为 `@Auth`) | | `AuthAspect.java` | 改为 cte 同款全局规则:jti 含冒号 → `hasKey(jti)` 必须为真否则返回 401(与 token 过期同响应,App 走既有登出路径);无冒号 → 放行。删除 `isConsumerUser` 豁免及每次请求的用户表查询 | | `MerchantSessionAuthAnnotationTest` | 重写:商家端点 @Auth 声明断言(不含 session)+ 切面行为用例(会话被删 → 401、会话存活 → 放行并写入可信身份、无冒号旧 token → 放行且不查 Redis) | 商家行为对账:改前 session=true 路径就是 hasKey 校验,改后语义一致;变化仅在用户/骑手从"豁免"变"校验",即本次需求本身。 ## 风险评估(已逐项核对) 1. **存量 token 兼容**:全部 7 处 token 签发都走写会话的 `JwtUtil.setToken(tokenKey, dto)`,15 天内的 token 会话都在 Redis,开启校验不掉线;无冒号历史 token 走兼容分支放行。 2. **App 端无需升级**:被踢返回与 token 过期相同的 401 响应,App 走既有处理。 3. **商家被踢体验变化**:改前商家会话失效抛 ServiceException(500 语义),改后统一 401——更一致,商家 App 按 401 重新登录。 4. **共享账号互顶**:单设备需求本身预期内。 5. **推送 cid**:登录覆盖 cid,单设备后与设备一一对应,推送不再串设备(改善)。 6. **029 设备信任**:DeviceTrustService 信任记录与会话删除独立,无冲突。 7. **管理后台**:ruoyi-framework 独立 TokenService,不受影响。 8. **Redis 故障**:会话丢失 → 全端重登(cte 线上同款风险,可接受)。 9. **骑手在线标识**:被踢旧设备的 offline 标志保持到下次登录,推送 cid 已切到新设备,接单不受影响。 ## 验证 - `MerchantSessionAuthAnnotationTest`:注解声明 + 切面行为(401/放行/可信身份写入)全绿 - 全量回归:除既有已知红测试 `createRejectsUnsupportedPayType` 外全部通过 ## 待办 - [ ] 观察上线后 401 量与登录量变化,确认无异常掉线投诉