ソースを参照

统一商家会话认证入口

将 JWT 与 Redis 会话校验集中到认证切面,业务控制器仅使用认证上下文执行门店和订单归属校验。

补充会话缺失、注解覆盖和商家入口回归测试,并移除用户信息接口的 Token 日志。
qmj 4 時間 前
コミット
fc657cbb95
26 ファイル変更767 行追加204 行削除
  1. 15 18
      ruoyi-admin/src/main/java/com/ruoyi/app/mendian/FoodSpecController.java
  2. 5 8
      ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFenleiController.java
  3. 18 20
      ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFoodController.java
  4. 13 15
      ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java
  5. 9 11
      ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java
  6. 27 29
      ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java
  7. 3 3
      ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java
  8. 12 14
      ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountController.java
  9. 7 27
      ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantTokenSessionService.java
  10. 5 5
      ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerAccessTest.java
  11. 5 5
      ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerShelfFilterTest.java
  12. 5 5
      ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosStoreControllerTest.java
  13. 6 7
      ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderListPaymentFilterTest.java
  14. 8 6
      ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderAdminStatusControllerTest.java
  15. 8 11
      ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.java
  16. 11 2
      ruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.java
  17. 55 0
      ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSessionAuthAnnotationTest.java
  18. 6 4
      ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountControllerTest.java
  19. 8 1
      ruoyi-system/src/main/java/com/ruoyi/system/utils/Auth.java
  20. 38 6
      ruoyi-system/src/main/java/com/ruoyi/system/utils/AuthAspect.java
  21. 39 0
      ruoyi-system/src/main/java/com/ruoyi/system/utils/AuthContext.java
  22. 1 0
      ruoyi-system/src/main/java/com/ruoyi/system/utils/JwtUtil.java
  23. 138 0
      ruoyi-system/src/test/java/com/ruoyi/system/utils/AuthAspectTest.java
  24. 1 1
      specs/022-merchant-store-subaccounts/spec.md
  25. 318 0
      specs/022-merchant-store-subaccounts/t012-auth-session-implementation-plan.md
  26. 6 6
      specs/022-merchant-store-subaccounts/tasks.md

+ 15 - 18
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/FoodSpecController.java

@@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.common.annotation.Anonymous;
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.system.domain.FoodSpecs;
@@ -13,6 +12,7 @@ import com.ruoyi.system.service.IFoodSpecsService;
 import com.ruoyi.system.service.IFoodSpecsValueService;
 import com.ruoyi.system.service.MerchantStoreAccessService;
 import com.ruoyi.system.utils.Auth;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.JwtUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
@@ -41,14 +41,11 @@ public class FoodSpecController extends BaseController
     private IFoodSpecsValueService foodSpecsValueService;
     @Autowired
     private MerchantStoreAccessService merchantStoreAccessService;
-    @Autowired
-    private MerchantTokenSessionService merchantTokenSessionService;
-
     /**
      * 规格组分页列表(每条带 foodSpecsItems)
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/foodSpecPageList")
     public AjaxResult foodSpecPageList(@RequestHeader String token,
                                        @RequestParam(defaultValue = "1") Integer pageNum,
@@ -56,7 +53,7 @@ public class FoodSpecController extends BaseController
                                        @RequestParam Long mdId,
                                        @RequestParam(required = false) String language) {
         merchantStoreAccessService.requireStoreAccess(
-                merchantTokenSessionService.requireUserId(token), mdId);
+                AuthContext.requireUserId(), mdId);
         Page<FoodSpecs> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<FoodSpecs> qw = new LambdaQueryWrapper<>();
         qw.eq(FoodSpecs::getMdId, mdId);
@@ -76,14 +73,14 @@ public class FoodSpecController extends BaseController
      * 保存规格组(含级联规格值):id<=0 新增,否则更新
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PostMapping("/saveFoodSpec")
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult saveFoodSpec(@RequestHeader String token, @RequestBody List<FoodSpecs> list) {
         if (list == null || list.isEmpty()) {
             return error();
         }
-        Long userId = merchantTokenSessionService.requireUserId(token);
+        Long userId = AuthContext.requireUserId();
         for (FoodSpecs s : list) {
             if (s.getId() == null || s.getId() <= 0) {
                 merchantStoreAccessService.requireStoreAccess(userId, s.getMdId());
@@ -163,10 +160,10 @@ public class FoodSpecController extends BaseController
      * 规格详情(带全部规格值)
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/getSpecs")
     public AjaxResult getSpecs(@RequestHeader String token, @RequestParam Long id) {
-        requireSpecsAccess(merchantTokenSessionService.requireUserId(token), id);
+        requireSpecsAccess(AuthContext.requireUserId(), id);
         FoodSpecs s = foodSpecsService.getById(id);
         if (s != null) {
             List<FoodSpecs> list = new ArrayList<>();
@@ -180,10 +177,10 @@ public class FoodSpecController extends BaseController
      * 软删除规格组(is_delete=1)
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/deleteFoodSpec")
     public AjaxResult deleteFoodSpec(@RequestHeader String token, @RequestParam Long id) {
-        requireSpecsAccess(merchantTokenSessionService.requireUserId(token), id);
+        requireSpecsAccess(AuthContext.requireUserId(), id);
         FoodSpecs s = new FoodSpecs();
         s.setId(id);
         s.setIsDelete(true);
@@ -194,12 +191,12 @@ public class FoodSpecController extends BaseController
      * 商品编辑页:本门店可用规格(is_open=1 & is_delete=0,仅含 is_open=1 的规格值)
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/getAvailableSpecsList")
     public AjaxResult getAvailableSpecsList(@RequestHeader String token, @RequestParam Long mdId,
                                             @RequestParam(required = false) String language) {
         merchantStoreAccessService.requireStoreAccess(
-                merchantTokenSessionService.requireUserId(token), mdId);
+                AuthContext.requireUserId(), mdId);
         LambdaQueryWrapper<FoodSpecs> qw = new LambdaQueryWrapper<>();
         qw.eq(FoodSpecs::getMdId, mdId);
         qw.eq(FoodSpecs::getIsOpen, true);
@@ -217,11 +214,11 @@ public class FoodSpecController extends BaseController
      * 启停规格组
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/changeOpen")
     public AjaxResult changeOpen(@RequestHeader String token,
                                  @RequestParam Long id, @RequestParam Boolean isOpen) {
-        requireSpecsAccess(merchantTokenSessionService.requireUserId(token), id);
+        requireSpecsAccess(AuthContext.requireUserId(), id);
         FoodSpecs s = new FoodSpecs();
         s.setId(id);
         s.setIsOpen(isOpen);
@@ -232,11 +229,11 @@ public class FoodSpecController extends BaseController
      * 启停规格值
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/changeSpecValueOpen")
     public AjaxResult changeSpecValueOpen(@RequestHeader String token,
                                           @RequestParam Long id, @RequestParam Boolean isOpen) {
-        Long userId = merchantTokenSessionService.requireUserId(token);
+        Long userId = AuthContext.requireUserId();
         FoodSpecsValue existingValue = foodSpecsValueService.getById(id);
         if (existingValue == null) {
             return error();

+ 5 - 8
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFenleiController.java

@@ -7,12 +7,12 @@ import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.ruoyi.common.annotation.Anonymous;
 import com.ruoyi.common.utils.MessageUtils;
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.system.domain.PosFood;
 import com.ruoyi.system.domain.PosStore;
 import com.ruoyi.system.service.IPosFoodService;
 import com.ruoyi.system.service.MerchantStoreAccessService;
 import com.ruoyi.system.utils.Auth;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.JwtUtil;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -42,15 +42,12 @@ public class PosFenleiController extends BaseController
     private IPosFoodService posFoodService;
     @Autowired
     private MerchantStoreAccessService merchantStoreAccessService;
-    @Autowired
-    private MerchantTokenSessionService merchantTokenSessionService;
-
 
     /**
      * 删除商品分类
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/delefenlei")
     public AjaxResult delefenlei(@RequestHeader String token, @RequestParam String id){
         PosFenlei existing = posFenleiService.getById(Long.valueOf(id));
@@ -58,7 +55,7 @@ public class PosFenleiController extends BaseController
             return error(MessageUtils.message("no.data.not.exist"));
         }
         merchantStoreAccessService.requireStoreAccess(
-                merchantTokenSessionService.requireUserId(token), existing.getMendid());
+                AuthContext.requireUserId(), existing.getMendid());
         QueryWrapper<PosFood> query = new QueryWrapper<>();
         query.eq("fl_id",id);
         List<PosFood> fdlist = posFoodService.list(query);
@@ -72,10 +69,10 @@ public class PosFenleiController extends BaseController
      * 添加或修改商品分类
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PostMapping("/addfenlei")
     public AjaxResult addfenlei(@RequestHeader String token, @RequestBody PosFenlei posFenlei){
-        Long userId = merchantTokenSessionService.requireUserId(token);
+        Long userId = AuthContext.requireUserId();
         if (posFenlei.getId() == null) {
             merchantStoreAccessService.requireStoreAccess(userId, posFenlei.getMendid());
         } else {

+ 18 - 20
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFoodController.java

@@ -9,8 +9,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.app.mendian.dto.FoodSearchOutput;
 import com.ruoyi.app.order.OrderInvoiceService;
 import com.ruoyi.app.utils.ImageCompressUtils;
-import com.ruoyi.app.user.MerchantTokenSessionService;
-import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.annotation.Anonymous;
 import com.ruoyi.common.annotation.Log;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
@@ -21,9 +20,10 @@ import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.system.domain.*;
 import com.ruoyi.system.mapper.PosStoreMapper;
-import com.ruoyi.system.service.*;
-import com.ruoyi.system.utils.Auth;
-import com.ruoyi.system.utils.JwtUtil;
+import com.ruoyi.system.service.*;
+import com.ruoyi.system.utils.Auth;
+import com.ruoyi.system.utils.AuthContext;
+import com.ruoyi.system.utils.JwtUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -77,26 +77,23 @@ public class PosFoodController extends BaseController {
     private OrderInvoiceService orderInvoiceService;
     @Autowired
     private MerchantStoreAccessService merchantStoreAccessService;
-    @Autowired
-    private MerchantTokenSessionService merchantTokenSessionService;
-
     //删除商品
     @Anonymous
-    @Auth
-    @GetMapping("/delefood")
+    @Auth(session = true)
+    @GetMapping("/delefood")
     public AjaxResult delefood(@RequestHeader String token, @RequestParam String id) {
         merchantStoreAccessService.requireFoodAccess(
-                merchantTokenSessionService.requireUserId(token), Long.valueOf(id));
+                AuthContext.requireUserId(), Long.valueOf(id));
         return toAjax(posFoodService.deletePosFoodById(Long.valueOf(id)));
     }
 
     //推荐商品
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PutMapping("/changerecommend")
     public AjaxResult changerecommend(@RequestHeader String token, @RequestBody PosFood posFood) {
         PosFood existing = merchantStoreAccessService.requireFoodAccess(
-                merchantTokenSessionService.requireUserId(token), posFood.getId());
+                AuthContext.requireUserId(), posFood.getId());
         posFood.setMdid(existing.getMdid());
         return toAjax(posFoodService.saveOrUpdate(posFood));
     }
@@ -105,10 +102,10 @@ public class PosFoodController extends BaseController {
      * 添加或修改商品
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PostMapping("/setposfood")
     public AjaxResult setposfood(@RequestHeader String token, @RequestBody PosFood posFood) {
-        Long userId = merchantTokenSessionService.requireUserId(token);
+        Long userId = AuthContext.requireUserId();
         if (posFood.getId() == null) {
             merchantStoreAccessService.requireStoreAccess(userId, posFood.getMdid());
         } else {
@@ -172,10 +169,11 @@ public class PosFoodController extends BaseController {
     /**
      * 通过分类查询商品列表
      */
-    @Anonymous
-    @GetMapping("/getidlist")
+    @Anonymous
+    @Auth(session = true)
+    @GetMapping("/getidlist")
     public AjaxResult getidlist(@RequestHeader String token, @RequestParam Integer id, @RequestParam(defaultValue = "") String language, @RequestParam(required = false) String stackingUp, @RequestParam(required = false) String name) {
-        Long userId = merchantTokenSessionService.requireUserId(token);
+        Long userId = AuthContext.requireUserId();
         InfoUser user = infoUserService.getById(userId);
         PosFenlei category = posFenleiService.getById(id);
         if (category != null && ("1".equals(user.getUserType()) || "5".equals(user.getUserType()))) {
@@ -445,7 +443,7 @@ public class PosFoodController extends BaseController {
 
 
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/getFoodPageList")
     public AjaxResult getFoodPageList(@RequestHeader String token,
                                       @RequestParam Integer page,
@@ -454,7 +452,7 @@ public class PosFoodController extends BaseController {
                                       @RequestParam Long mdId,
                                       @RequestParam(defaultValue = "") String language,
                                       @RequestParam(defaultValue = "") String name) {
-        Long id = merchantTokenSessionService.requireUserId(token);
+        Long id = AuthContext.requireUserId();
         merchantStoreAccessService.requireStoreAccess(id, mdId);
         IPage<PosFood> stlist = new Page<>(page, size);
         LambdaQueryWrapper<PosFood> queryWrapper = new LambdaQueryWrapper<>();

+ 13 - 15
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java

@@ -14,7 +14,6 @@ import com.ruoyi.app.service.UserService;
 import com.ruoyi.app.utils.DateUtil;
 import com.ruoyi.app.utils.ImageCompressUtils;
 import com.ruoyi.app.user.dto.StoreOutput;
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.app.utils.PayPush;
 import com.ruoyi.common.annotation.Anonymous;
 import com.ruoyi.common.annotation.Log;
@@ -33,6 +32,7 @@ import com.ruoyi.system.mapper.PosStoreMapper;
 import com.ruoyi.system.mapper.SysConfigMapper;
 import com.ruoyi.system.service.*;
 import com.ruoyi.system.utils.Auth;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.JwtUtil;
 import io.swagger.models.auth.In;
 import org.springframework.beans.BeanUtils;
@@ -87,8 +87,6 @@ public class PosStoreController extends BaseController {
     @Autowired
     private MerchantStoreAccessService merchantStoreAccessService;
     @Autowired
-    private MerchantTokenSessionService merchantTokenSessionService;
-    @Autowired
     private IMerchantSubaccountStoreService merchantSubaccountStoreService;
 
 
@@ -125,11 +123,11 @@ public class PosStoreController extends BaseController {
 
     //删除我的门店
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @Transactional(rollbackFor = Exception.class)
     @GetMapping("/delemendian")
     public AjaxResult delemendian(@RequestHeader String token, @RequestParam String id) {
-        Long userId = merchantTokenSessionService.requireUserId(token);
+        Long userId = AuthContext.requireUserId();
         merchantStoreAccessService.requireOwner(userId);
         merchantStoreAccessService.requireStoreAccess(userId, Long.valueOf(id));
         QueryWrapper<PosFenlei> queryWrapper = new QueryWrapper<>();
@@ -150,12 +148,12 @@ public class PosStoreController extends BaseController {
      * H5端门店列表
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/storelistlist")
     public AjaxResult storelistlist(@RequestHeader String token,
                                     @RequestParam Integer page,
                                     @RequestParam Integer size) {
-        Long id = merchantTokenSessionService.requireUserId(token);
+        Long id = AuthContext.requireUserId();
         IPage<PosStore> stlist = new Page<>(page, size);
         QueryWrapper<PosStore> queryWrapper = new QueryWrapper<>();
 
@@ -193,10 +191,10 @@ public class PosStoreController extends BaseController {
 
     //查询我的门店列表
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/getmystorelist")
     public AjaxResult getmystorelist(@RequestHeader String token) {
-        Long id = merchantTokenSessionService.requireUserId(token);
+        Long id = AuthContext.requireUserId();
         QueryWrapper<PosStore> queryWrapper = new QueryWrapper<>();
         InfoUser loginUser = infoUserService.selectInfoUserByUserId(id);
         if (loginUser != null && "4".equals(loginUser.getUserType())) {
@@ -288,11 +286,11 @@ public class PosStoreController extends BaseController {
 
     //添加门店
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PostMapping("/addmendian")
     @Transactional
     public AjaxResult addmendian(@RequestHeader String token, @RequestBody PosStore posStore) {
-        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        Long loginUserId = AuthContext.requireUserId();
         InfoUser loginUser = infoUserService.selectInfoUserByUserId(loginUserId);
         if (posStore.getId() == null) {
             merchantStoreAccessService.requireOwner(loginUserId);
@@ -382,10 +380,10 @@ public class PosStoreController extends BaseController {
      * @return
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/getMdBusinessHours")
     public AjaxResult getMdBusinessHours(@RequestHeader String token, @RequestParam Long mdId){
-        Long userId = merchantTokenSessionService.requireUserId(token);
+        Long userId = AuthContext.requireUserId();
         requireStoreAccess(userId, infoUserService.selectInfoUserByUserId(userId), mdId);
         LambdaQueryWrapper<OperatingHours> query=new LambdaQueryWrapper<>();
         query.eq(OperatingHours::getMdId,mdId);
@@ -403,7 +401,7 @@ public class PosStoreController extends BaseController {
      * @return
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PostMapping("/saveMdBusinessHours")
     public AjaxResult saveMdBusinessHours(@RequestHeader String token,
                                           @RequestBody List<StoreOperatOutput> businessHours) {
@@ -411,7 +409,7 @@ public class PosStoreController extends BaseController {
             throw new ServiceException(MessageUtils.message("no.mendian.not.set.business.hours"));
         }
         Long mdId = businessHours.get(0).getMdId();
-        Long userId = merchantTokenSessionService.requireUserId(token);
+        Long userId = AuthContext.requireUserId();
         requireStoreAccess(userId, infoUserService.selectInfoUserByUserId(userId), mdId);
         if (businessHours.stream().anyMatch(item -> !Objects.equals(mdId, item.getMdId()))) {
             throw new ServiceException(MessageUtils.message("merchant.store.access.denied"));

+ 9 - 11
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java

@@ -21,7 +21,6 @@ import com.ruoyi.app.pay.LinePayRefundService;
 import com.ruoyi.app.order.dto.AdminOrderStatusUpdateRequest;
 import com.ruoyi.app.service.UserService;
 import com.ruoyi.app.service.WalletService;
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.app.user.dto.StoreOutput;
 import com.ruoyi.app.utils.DateUtil;
 import com.ruoyi.app.utils.OperatingUtil;
@@ -50,6 +49,7 @@ import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt;
 import com.ruoyi.system.omgpay.service.IOmgPaymentAttemptService;
 import com.ruoyi.system.service.*;
 import com.ruoyi.system.utils.Auth;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.GetArea;
 import com.ruoyi.system.utils.JwtUtil;
 import com.ruoyi.system.utils.OrderLogHelper;
@@ -169,8 +169,6 @@ public class PosOrderController extends BaseController {
     private IOmgPaymentAttemptService omgPaymentAttemptService;
     @Autowired
     private MerchantStoreAccessService merchantStoreAccessService;
-    @Autowired
-    private MerchantTokenSessionService merchantTokenSessionService;
 
 
     //查询用户足迹
@@ -230,13 +228,13 @@ public class PosOrderController extends BaseController {
 
     //修改订单
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @RepeatSubmit(interval = 1000, message = "请求过于频繁")
     @PostMapping("/setorderuzt")
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult setorderuzt(@RequestHeader String token, @RequestBody PosOrder posOrder) {
         PayPush push = new PayPush();
-        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        Long loginUserId = AuthContext.requireUserId();
         String id = String.valueOf(loginUserId);
         PosOrder persisted = posOrder.getId() == null ? null : posOrderService.getById(posOrder.getId());
         InfoUser actor = infoUserService.getById(loginUserId);
@@ -700,13 +698,13 @@ public class PosOrderController extends BaseController {
      */
     //查询订单详情
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/getorderxq")
     public AjaxResult getorderxq(@RequestHeader String token, @RequestParam String ddid) {
         QueryWrapper<PosOrder> queryWrapper = new QueryWrapper<>();
         queryWrapper.eq("dd_id", ddid);
         PosOrder orlist = posOrderService.getOne(queryWrapper);
-        requireOrderDetailAccess(token, orlist);
+        requireOrderDetailAccess(orlist);
         // 自动序列化所有PosOrder字段,新增字段无需手动添加
         JSONObject org = JSON.parseObject(
             JSON.toJSONStringWithDateFormat(orlist, "yyyy-MM-dd HH:mm:ss")
@@ -731,8 +729,8 @@ public class PosOrderController extends BaseController {
         return success(org);
     }
 
-    private void requireOrderDetailAccess(String token, PosOrder order) {
-        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+    private void requireOrderDetailAccess(PosOrder order) {
+        Long loginUserId = AuthContext.requireUserId();
         InfoUser actor = infoUserService.getById(loginUserId);
         if (order == null || actor == null) {
             throw new ServiceException(MessageUtils.message("merchant.order.not.found"));
@@ -879,10 +877,10 @@ public class PosOrderController extends BaseController {
 
     //查询商家的订单
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/getstoreorderlist")
     public AjaxResult getstoreorderlist(@RequestHeader String token, @RequestParam Integer page, @RequestParam Integer size, @RequestParam String mdId, @RequestParam(defaultValue = "") String state, @RequestParam(defaultValue = "") String diningStatus,@RequestParam(defaultValue = "")String type) {
-        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        Long loginUserId = AuthContext.requireUserId();
         Long requestedStoreId;
         try {
             requestedStoreId = Long.valueOf(mdId);

+ 27 - 29
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java

@@ -12,7 +12,6 @@ import com.ruoyi.app.order.dto.OrderCreateInput;
 import com.ruoyi.app.order.dto.OrderPushBodyDto;
 import com.ruoyi.app.pay.LinePayOrderGuard;
 import com.ruoyi.app.pay.LinePayRefundService;
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.app.utils.PayPush;
 import com.ruoyi.app.utils.event.PushEventService;
 import com.ruoyi.common.annotation.Anonymous;
@@ -25,6 +24,7 @@ import com.ruoyi.framework.manager.AsyncManager;
 import com.ruoyi.system.domain.*;
 import com.ruoyi.system.service.*;
 import com.ruoyi.system.utils.Auth;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.JwtUtil;
 import com.ruoyi.system.domain.dto.InvalidInvoiceDto;
 import com.ruoyi.system.utils.OrderLogHelper;
@@ -75,8 +75,6 @@ public class PosOrderShOprateController extends BaseController {
     private DeliveryOrderNotificationService deliveryOrderNotificationService;
     @Autowired
     private MerchantStoreAccessService merchantStoreAccessService;
-    @Autowired
-    private MerchantTokenSessionService merchantTokenSessionService;
 
 
 
@@ -85,10 +83,10 @@ public class PosOrderShOprateController extends BaseController {
      */
 
     @GetMapping("/getInvoice")
-    @Auth
+    @Auth(session = true)
     @Anonymous
     public AjaxResult getInvoice(@RequestHeader String token, @RequestParam Long orderId) {
-        MerchantAccessContext context = requireManagedOrder(token, orderId);
+        MerchantAccessContext context = requireManagedOrder(orderId);
         return AjaxResult.success(orderInvoiceService.getInvoiceForMerchant(orderId, context.ownerUserId()));
     }
 
@@ -97,11 +95,11 @@ public class PosOrderShOprateController extends BaseController {
      */
 
     @PutMapping("/invalidInvoice/{orderId}")
-    @Auth
+    @Auth(session = true)
     @Anonymous
     public AjaxResult invalidInvoice(@RequestHeader String token, @PathVariable Long orderId,
                                      @RequestBody(required = false) InvalidInvoiceDto dto) {
-        MerchantAccessContext context = requireManagedOrder(token, orderId);
+        MerchantAccessContext context = requireManagedOrder(orderId);
         String reason = dto == null ? null : dto.getInvalidReason();
         return toAjax(orderInvoiceService.invalidForMerchant(orderId, reason, context.ownerUserId()));
     }
@@ -110,12 +108,12 @@ public class PosOrderShOprateController extends BaseController {
      * 商家端下单
      */
     @PostMapping("/createOrder")
-    @Auth
+    @Auth(session = true)
     @Anonymous
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult createOrder(@RequestHeader String token, @RequestBody OrderCreateInput input)
     {
-        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        Long loginUserId = AuthContext.requireUserId();
         InfoUser merchant = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>()
                 .eq(InfoUser::getUserId, loginUserId));
         validateMerchantOrderInput(merchant, input);
@@ -301,11 +299,11 @@ public class PosOrderShOprateController extends BaseController {
      * 商家接单:state 从 0 改为 1
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/acceptOrder")
     public AjaxResult acceptOrder(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
-        InfoUser currentUser = requireMerchantOrderAccess(token, order);
+        InfoUser currentUser = requireMerchantOrderAccess(order);
         if (order == null || order.getState() == null || order.getState() != 0L) {
             throw new ServiceException("订单不存在或状态不允许接单");
         }
@@ -326,11 +324,11 @@ public class PosOrderShOprateController extends BaseController {
      * 外送订单保持 deliveryStatus=1(骑手已接单)
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/dispatchOrder")
     public AjaxResult dispatchOrder(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
-        requireMerchantOrderAccess(token, order);
+        requireMerchantOrderAccess(order);
         if (order == null || order.getState() == null || order.getState() != 1L) {
             throw new ServiceException("订单不存在或状态不允许出餐");
         }
@@ -357,12 +355,12 @@ public class PosOrderShOprateController extends BaseController {
      * 商家完成(自取/堂食):state 从 2 改为 3,payStatus 从 0 改为 1
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @Transactional(rollbackFor = Exception.class)
     @GetMapping("/completeOrder")
     public AjaxResult completeOrder(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
-        requireMerchantOrderAccess(token, order);
+        requireMerchantOrderAccess(order);
         if (order == null || order.getState() == null || order.getState() != 2L) {
             throw new ServiceException("订单不存在或状态不允许完成");
         }
@@ -395,11 +393,11 @@ public class PosOrderShOprateController extends BaseController {
      * 商家确认现金收款:payType=4 的订单 payStatus 0→1(现金单创建时未支付,商家收到现金后在App触发)
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/confirmCashPayment")
     public AjaxResult confirmCashPayment(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
-        requireMerchantOrderAccess(token, order);
+        requireMerchantOrderAccess(order);
         if (order == null) {
             throw new ServiceException(MessageUtils.message("no.order.not.exist"));
         }
@@ -427,11 +425,11 @@ public class PosOrderShOprateController extends BaseController {
      * 商家取消订单:校验 state IN (0,1),设 state=4
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/cancelOrder")
     public AjaxResult cancelOrder(@RequestHeader String token, @RequestParam Long id) {
         PosOrder order = posOrderService.getOne(new LambdaQueryWrapper<PosOrder>().eq(PosOrder::getId, id));
-        InfoUser currentUser = requireMerchantOrderAccess(token, order);
+        InfoUser currentUser = requireMerchantOrderAccess(order);
         if (order == null || order.getState() == null) {
             throw new ServiceException("订单不存在");
         }
@@ -464,8 +462,8 @@ public class PosOrderShOprateController extends BaseController {
         }
     }
 
-    private InfoUser currentMerchant(String token) {
-        Long userId = merchantTokenSessionService.requireUserId(token);
+    private InfoUser currentMerchant() {
+        Long userId = AuthContext.requireUserId();
         InfoUser user = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>()
                 .eq(InfoUser::getUserId, userId));
         if (user == null) {
@@ -474,8 +472,8 @@ public class PosOrderShOprateController extends BaseController {
         return user;
     }
 
-    private InfoUser requireMerchantOrderAccess(String token, PosOrder order) {
-        InfoUser user = currentMerchant(token);
+    private InfoUser requireMerchantOrderAccess(PosOrder order) {
+        InfoUser user = currentMerchant();
         if ("1".equals(user.getUserType()) || "5".equals(user.getUserType())) {
             merchantStoreAccessService.requireOrderAccess(user.getUserId(), order == null ? null : order.getId());
         } else {
@@ -484,8 +482,8 @@ public class PosOrderShOprateController extends BaseController {
         return user;
     }
 
-    private MerchantAccessContext requireManagedOrder(String token, Long orderId) {
-        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+    private MerchantAccessContext requireManagedOrder(Long orderId) {
+        Long loginUserId = AuthContext.requireUserId();
         InfoUser user = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>()
                 .eq(InfoUser::getUserId, loginUserId));
         if (user != null && ("1".equals(user.getUserType()) || "5".equals(user.getUserType()))) {
@@ -562,7 +560,7 @@ public class PosOrderShOprateController extends BaseController {
      * ta
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/orderList")
     public AjaxResult orderList(@RequestHeader String token,
                                 @RequestParam(defaultValue = "1") int page,
@@ -570,7 +568,7 @@ public class PosOrderShOprateController extends BaseController {
                                 @RequestParam(required = false) String tab,
                                 @RequestParam(required = false) Long mdId,
                                 @RequestParam(required = false) String type) {
-        Long loginUserId = merchantTokenSessionService.requireUserId(token);
+        Long loginUserId = AuthContext.requireUserId();
         // 查询当前用户信息
         InfoUser currentUser = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>().eq(InfoUser::getUserId, loginUserId));
         if (currentUser == null) {
@@ -638,10 +636,10 @@ public class PosOrderShOprateController extends BaseController {
     }
 
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping("/orderDetail")
     public AjaxResult orderDetail(@RequestHeader String token, @RequestParam Long id) {
-        requireManagedOrder(token, id);
+        requireManagedOrder(id);
         PosOrder order = posOrderService.getById(id);
         if (order == null) {
             throw new ServiceException(MessageUtils.message("merchant.order.not.found"));

+ 3 - 3
ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java

@@ -38,6 +38,7 @@ import com.ruoyi.system.service.IUserWalletService;
 import com.ruoyi.system.service.IVipUserService;
 import com.ruoyi.system.service.MerchantStoreAccessService;
 import com.ruoyi.system.utils.Auth;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.JwtUtil;
 import com.ruoyi.system.utils.MobileSMS;
 import com.ruoyi.app.user.dto.OAuthBindDto;
@@ -579,10 +580,10 @@ public class InfoUserController extends BaseController {
      * 商家端退出登录,仅撤销当前 JWT 对应的会话。
      */
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PostMapping("/merchantLogout")
     public AjaxResult merchantLogout(@RequestHeader String token) {
-        merchantTokenSessionService.logoutCurrent(token);
+        merchantTokenSessionService.logoutCurrent(AuthContext.requireJti());
         return success();
     }
 
@@ -791,7 +792,6 @@ public class InfoUserController extends BaseController {
     @Auth
     @GetMapping("/getuserinfo")
     public AjaxResult getuserinfo(@RequestHeader String token) {
-        System.out.println(token);
         JwtUtil jwtUtil = new JwtUtil();
         String id = jwtUtil.getusid(token);
         QueryWrapper<InfoUser> wrapper = new QueryWrapper<>();

+ 12 - 14
ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountController.java

@@ -8,6 +8,7 @@ import com.ruoyi.common.annotation.Anonymous;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.system.utils.Auth;
+import com.ruoyi.system.utils.AuthContext;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -22,57 +23,54 @@ import org.springframework.web.bind.annotation.RestController;
 public class MerchantSubaccountController extends BaseController {
 
     private final MerchantSubaccountApplicationService applicationService;
-    private final MerchantTokenSessionService tokenSessionService;
 
-    public MerchantSubaccountController(MerchantSubaccountApplicationService applicationService,
-                                        MerchantTokenSessionService tokenSessionService) {
+    public MerchantSubaccountController(MerchantSubaccountApplicationService applicationService) {
         this.applicationService = applicationService;
-        this.tokenSessionService = tokenSessionService;
     }
 
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @GetMapping
     public AjaxResult list(@RequestHeader String token) {
-        return success(applicationService.listForOwner(tokenSessionService.requireUserId(token)));
+        return success(applicationService.listForOwner(AuthContext.requireUserId()));
     }
 
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PostMapping
     public AjaxResult create(@RequestHeader String token,
                              @RequestBody MerchantSubaccountCreateRequest request) {
-        return success(applicationService.create(tokenSessionService.requireUserId(token), request));
+        return success(applicationService.create(AuthContext.requireUserId(), request));
     }
 
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PutMapping("/{subaccountUserId}")
     public AjaxResult update(@RequestHeader String token,
                              @PathVariable Long subaccountUserId,
                              @RequestBody MerchantSubaccountUpdateRequest request) {
-        return success(applicationService.update(tokenSessionService.requireUserId(token),
+        return success(applicationService.update(AuthContext.requireUserId(),
                 subaccountUserId, request));
     }
 
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PutMapping("/{subaccountUserId}/password")
     public AjaxResult updatePassword(@RequestHeader String token,
                                      @PathVariable Long subaccountUserId,
                                      @RequestBody MerchantSubaccountPasswordRequest request) {
-        applicationService.updatePassword(tokenSessionService.requireUserId(token),
+        applicationService.updatePassword(AuthContext.requireUserId(),
                 subaccountUserId, request);
         return success();
     }
 
     @Anonymous
-    @Auth
+    @Auth(session = true)
     @PutMapping("/{subaccountUserId}/status")
     public AjaxResult updateStatus(@RequestHeader String token,
                                    @PathVariable Long subaccountUserId,
                                    @RequestBody MerchantSubaccountStatusRequest request) {
-        applicationService.updateOwnerStatus(tokenSessionService.requireUserId(token),
+        applicationService.updateOwnerStatus(AuthContext.requireUserId(),
                 subaccountUserId, request);
         return success();
     }

+ 7 - 27
ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantTokenSessionService.java

@@ -2,9 +2,6 @@ package com.ruoyi.app.user;
 
 import com.ruoyi.common.constant.CacheConstants;
 import com.ruoyi.common.core.redis.RedisCache;
-import com.ruoyi.common.exception.ServiceException;
-import com.ruoyi.common.utils.MessageUtils;
-import com.ruoyi.system.utils.JwtUtil;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -18,27 +15,13 @@ public class MerchantTokenSessionService {
         this.redisCache = redisCache;
     }
 
-    public Long requireUserId(String token) {
-        if (token == null || token.isBlank()) {
-            throw unauthorized();
-        }
-        JwtUtil jwtUtil = new JwtUtil();
-        String userId = jwtUtil.getusid(token);
-        String jti = jwtUtil.getJti(token);
-        if (userId == null || jti == null || !Boolean.TRUE.equals(redisCache.hasKey(jti))) {
-            throw unauthorized();
-        }
-        try {
-            return Long.valueOf(userId);
-        } catch (NumberFormatException exception) {
-            throw unauthorized();
-        }
-    }
-
-    public void logoutCurrent(String token) {
-        requireUserId(token);
-        String jti = new JwtUtil().getJti(token);
-        if (jti != null) {
+    /**
+     * 删除已经由认证切面校验通过的当前会话。
+     *
+     * @param jti 当前 JWT 的唯一会话标识
+     */
+    public void logoutCurrent(String jti) {
+        if (jti != null && !jti.isBlank()) {
             redisCache.deleteObject(jti);
         }
     }
@@ -65,7 +48,4 @@ public class MerchantTokenSessionService {
         return keys != null && !keys.isEmpty();
     }
 
-    private ServiceException unauthorized() {
-        return new ServiceException(MessageUtils.message("merchant.session.invalid"));
-    }
 }

+ 5 - 5
ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerAccessTest.java

@@ -1,6 +1,5 @@
 package com.ruoyi.app.mendian;
 
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.system.domain.PosFood;
@@ -9,6 +8,7 @@ import com.ruoyi.system.service.IFoodSpecsService;
 import com.ruoyi.system.service.IFoodSpecsValueService;
 import com.ruoyi.system.service.IPosFoodService;
 import com.ruoyi.system.service.MerchantStoreAccessService;
+import com.ruoyi.system.utils.AuthContext;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -29,30 +29,30 @@ class PosFoodControllerAccessTest {
     private PosFoodController controller;
     private IPosFoodService posFoodService;
     private MerchantStoreAccessService accessService;
-    private MerchantTokenSessionService tokenSessionService;
     private MockedStatic<MessageUtils> messages;
+    private MockedStatic<AuthContext> authContext;
 
     @BeforeEach
     void setUp() {
         messages = mockStatic(MessageUtils.class);
         messages.when(() -> MessageUtils.message(org.mockito.ArgumentMatchers.anyString()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
+        authContext = mockStatic(AuthContext.class);
+        authContext.when(AuthContext::requireUserId).thenReturn(5L);
         controller = new PosFoodController();
         posFoodService = mock(IPosFoodService.class);
         accessService = mock(MerchantStoreAccessService.class);
-        tokenSessionService = mock(MerchantTokenSessionService.class);
         ReflectionTestUtils.setField(controller, "posFoodService", posFoodService);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", accessService);
-        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", tokenSessionService);
         ReflectionTestUtils.setField(controller, "foodSpecsService", mock(IFoodSpecsService.class));
         ReflectionTestUtils.setField(controller, "foodSpecsValueService", mock(IFoodSpecsValueService.class));
         ReflectionTestUtils.setField(controller, "foodSpecRelationService", mock(IFoodSpecRelationService.class));
-        when(tokenSessionService.requireUserId("token")).thenReturn(5L);
         when(posFoodService.saveOrUpdate(any(PosFood.class))).thenReturn(true);
     }
 
     @AfterEach
     void tearDown() {
+        authContext.close();
         messages.close();
     }
 

+ 5 - 5
ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerShelfFilterTest.java

@@ -1,7 +1,6 @@
 package com.ruoyi.app.mendian;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.system.domain.InfoUser;
 import com.ruoyi.system.domain.PosFood;
@@ -9,6 +8,7 @@ import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosFenleiService;
 import com.ruoyi.system.service.IPosFoodService;
 import com.ruoyi.system.utils.JwtUtil;
+import com.ruoyi.system.utils.AuthContext;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -37,14 +37,12 @@ class PosFoodControllerShelfFilterTest {
     @Mock
     private IPosFenleiService posFenleiService;
 
-    @Mock
-    private MerchantTokenSessionService merchantTokenSessionService;
-
     @InjectMocks
     private PosFoodController controller;
 
     private AutoCloseable mocks;
     private MockedStatic<MessageUtils> messages;
+    private MockedStatic<AuthContext> authContext;
 
     @BeforeEach
     void setUp() {
@@ -52,10 +50,13 @@ class PosFoodControllerShelfFilterTest {
         messages = org.mockito.Mockito.mockStatic(MessageUtils.class);
         messages.when(() -> MessageUtils.message(anyString()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
+        authContext = org.mockito.Mockito.mockStatic(AuthContext.class);
+        authContext.when(AuthContext::requireUserId).thenReturn(101L);
     }
 
     @AfterEach
     void tearDown() throws Exception {
+        authContext.close();
         messages.close();
         mocks.close();
     }
@@ -64,7 +65,6 @@ class PosFoodControllerShelfFilterTest {
     void getidlistExcludesOffShelfFoodsWhenNoShelfFilterIsProvided() {
         InfoUser user = new InfoUser();
         user.setUserType("0");
-        when(merchantTokenSessionService.requireUserId(anyString())).thenReturn(101L);
         when(infoUserService.getById(101L)).thenReturn(user);
         when(posFoodService.list(any(QueryWrapper.class))).thenReturn(Collections.emptyList());
 

+ 5 - 5
ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosStoreControllerTest.java

@@ -2,7 +2,6 @@ package com.ruoyi.app.mendian;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.ruoyi.app.service.UserService;
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.framework.web.exception.GlobalExceptionHandler;
@@ -11,6 +10,7 @@ import com.ruoyi.system.domain.PosStore;
 import com.ruoyi.system.service.IPosStoreService;
 import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.MerchantStoreAccessService;
+import com.ruoyi.system.utils.AuthContext;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -41,9 +41,6 @@ class PosStoreControllerTest {
     @Mock
     private UserService userService;
 
-    @Mock
-    private MerchantTokenSessionService merchantTokenSessionService;
-
     @Mock
     private MerchantStoreAccessService merchantStoreAccessService;
 
@@ -52,6 +49,7 @@ class PosStoreControllerTest {
 
     private AutoCloseable mocks;
     private MockedStatic<MessageUtils> messages;
+    private MockedStatic<AuthContext> authContext;
     private MockMvc mockMvc;
     private ObjectMapper objectMapper;
 
@@ -61,16 +59,18 @@ class PosStoreControllerTest {
         messages = org.mockito.Mockito.mockStatic(MessageUtils.class);
         messages.when(() -> MessageUtils.message(anyString()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
+        authContext = org.mockito.Mockito.mockStatic(AuthContext.class);
+        authContext.when(AuthContext::requireUserId).thenReturn(101L);
         mockMvc = MockMvcBuilders.standaloneSetup(controller)
                 .setControllerAdvice(new GlobalExceptionHandler())
                 .build();
         objectMapper = new ObjectMapper();
         when(posStoreService.saveOrUpdate(any(PosStore.class))).thenReturn(true);
-        when(merchantTokenSessionService.requireUserId("token")).thenReturn(101L);
     }
 
     @AfterEach
     void tearDown() throws Exception {
+        authContext.close();
         messages.close();
         mocks.close();
     }

+ 6 - 7
ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderListPaymentFilterTest.java

@@ -10,7 +10,7 @@ import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosOrderService;
 import com.ruoyi.system.service.MerchantStoreAccessService;
-import com.ruoyi.app.user.MerchantTokenSessionService;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.JwtUtil;
 import com.ruoyi.common.utils.MessageUtils;
 import org.junit.jupiter.api.AfterEach;
@@ -39,6 +39,7 @@ import static org.mockito.Mockito.when;
 
 class OrderListPaymentFilterTest {
     private MockedStatic<MessageUtils> messages;
+    private MockedStatic<AuthContext> auth;
 
     @BeforeAll
     static void initializeTableMetadata() {
@@ -89,11 +90,9 @@ class OrderListPaymentFilterTest {
         PosOrderShOprateController controller = new PosOrderShOprateController();
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
-        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
         MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
-        when(sessions.requireUserId(any())).thenReturn(801L);
+        auth.when(AuthContext::requireUserId).thenReturn(801L);
         when(access.getAccessibleStoreIds(801L)).thenReturn(Set.of(11L, 12L));
-        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
         InfoUser merchant = new InfoUser();
         merchant.setUserId(801L);
@@ -117,10 +116,12 @@ class OrderListPaymentFilterTest {
         messages = mockStatic(MessageUtils.class);
         messages.when(() -> MessageUtils.message(anyString()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
+        auth = mockStatic(AuthContext.class);
     }
 
     @AfterEach
     void closeMessages() {
+        auth.close();
         messages.close();
     }
 
@@ -128,17 +129,15 @@ class OrderListPaymentFilterTest {
     void subaccountOrderListFiltersByAllAuthorizedStores() {
         IPosOrderService orders = mock(IPosOrderService.class);
         IInfoUserService users = mock(IInfoUserService.class);
-        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
         MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
         PosOrderShOprateController controller = new PosOrderShOprateController();
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
-        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
         InfoUser subaccount = new InfoUser();
         subaccount.setUserId(501L);
         subaccount.setUserType("5");
-        when(sessions.requireUserId(any())).thenReturn(501L);
+        auth.when(AuthContext::requireUserId).thenReturn(501L);
         when(users.getOne(any())).thenReturn(subaccount);
         when(access.getAccessibleStoreIds(501L)).thenReturn(Set.of(11L, 12L));
         when(orders.page(any(Page.class), any(Wrapper.class))).thenReturn(new Page<>());

+ 8 - 6
ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderAdminStatusControllerTest.java

@@ -2,7 +2,6 @@ package com.ruoyi.app.order;
 
 import com.ruoyi.app.order.dto.AdminOrderActionRequest;
 import com.ruoyi.app.order.dto.AdminOrderStatusUpdateRequest;
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.system.domain.PosOrder;
@@ -12,8 +11,10 @@ import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosOrderLinePaymentService;
 import com.ruoyi.system.service.IPosOrderService;
 import com.ruoyi.system.service.MerchantStoreAccessService;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.JwtUtil;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.test.util.ReflectionTestUtils;
 
@@ -26,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.when;
 
 class PosOrderAdminStatusControllerTest {
@@ -74,9 +76,7 @@ class PosOrderAdminStatusControllerTest {
         IPosOrderLinePaymentService paymentService = mock(IPosOrderLinePaymentService.class);
         ReflectionTestUtils.setField(controller, "posOrderService", orderService);
         ReflectionTestUtils.setField(controller, "linePaymentService", paymentService);
-        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
         IInfoUserService users = mock(IInfoUserService.class);
-        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", mock(MerchantStoreAccessService.class));
         ReflectionTestUtils.setField(controller, "infoUserService", users);
         PosOrder stored = new PosOrder();
@@ -86,7 +86,6 @@ class PosOrderAdminStatusControllerTest {
         PosOrderLinePayment payment = new PosOrderLinePayment();
         payment.setDdId("D-1");
         when(orderService.getById(1L)).thenReturn(stored);
-        when(sessions.requireUserId(org.mockito.ArgumentMatchers.anyString())).thenReturn(9L);
         InfoUser merchant = new InfoUser();
         merchant.setUserId(9L);
         merchant.setUserType("1");
@@ -97,8 +96,11 @@ class PosOrderAdminStatusControllerTest {
         malicious.setState(7L);
         malicious.setPoints(999);
 
-        assertThrows(ServiceException.class,
-                () -> controller.setorderuzt(JwtUtil.token("9", "tester"), malicious));
+        try (MockedStatic<AuthContext> auth = mockStatic(AuthContext.class)) {
+            auth.when(AuthContext::requireUserId).thenReturn(9L);
+            assertThrows(ServiceException.class,
+                    () -> controller.setorderuzt(JwtUtil.token("9", "tester"), malicious));
+        }
     }
 
     private boolean hasField(Class<?> type, String name) {

+ 8 - 11
ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.java

@@ -4,7 +4,6 @@ import com.ruoyi.app.order.dto.OrderCreatItem;
 import com.ruoyi.app.order.dto.OrderCreateInput;
 import com.ruoyi.app.pay.LinePayOrderGuard;
 import com.ruoyi.app.pay.LinePayRefundService;
-import com.ruoyi.app.user.MerchantTokenSessionService;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.system.domain.InfoUser;
@@ -19,6 +18,7 @@ import com.ruoyi.system.service.IPosStoreService;
 import com.ruoyi.system.service.MerchantAccessContext;
 import com.ruoyi.system.service.MerchantStoreAccessService;
 import com.ruoyi.system.utils.JwtUtil;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.OrderLogHelper;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -42,16 +42,19 @@ import static org.mockito.Mockito.when;
 
 class PosOrderShOprateControllerTest {
     private MockedStatic<MessageUtils> messages;
+    private MockedStatic<AuthContext> authContext;
 
     @BeforeEach
     void setUp() {
         messages = mockStatic(MessageUtils.class);
         messages.when(() -> MessageUtils.message(anyString()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
+        authContext = mockStatic(AuthContext.class);
     }
 
     @AfterEach
     void tearDown() {
+        authContext.close();
         messages.close();
     }
 
@@ -205,11 +208,9 @@ class PosOrderShOprateControllerTest {
         PosOrderShOprateController controller = new PosOrderShOprateController();
         IInfoUserService users = mock(IInfoUserService.class);
         IPosOrderService orders = mock(IPosOrderService.class);
-        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
         MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
-        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
         ReflectionTestUtils.setField(controller, "linePayOrderGuard", mock(LinePayOrderGuard.class));
 
@@ -220,7 +221,7 @@ class PosOrderShOprateControllerTest {
         order.setState(0L);
         InfoUser subaccount = user(501L, "5", null);
         subaccount.setMerchantOwnerId(101L);
-        when(sessions.requireUserId(anyString())).thenReturn(501L);
+        authContext.when(AuthContext::requireUserId).thenReturn(501L);
         when(users.getOne(any())).thenReturn(subaccount);
         when(orders.getOne(any())).thenReturn(order);
         org.mockito.Mockito.doThrow(new ServiceException("merchant.store.access.denied"))
@@ -236,12 +237,10 @@ class PosOrderShOprateControllerTest {
         PosOrderShOprateController controller = new PosOrderShOprateController();
         IInfoUserService users = mock(IInfoUserService.class);
         IPosOrderService orders = mock(IPosOrderService.class);
-        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
         MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
         LinePayRefundService refunds = mock(LinePayRefundService.class);
         ReflectionTestUtils.setField(controller, "infoUserService", users);
         ReflectionTestUtils.setField(controller, "posOrderService", orders);
-        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
         ReflectionTestUtils.setField(controller, "linePayOrderGuard", mock(LinePayOrderGuard.class));
         ReflectionTestUtils.setField(controller, "linePayRefundService", refunds);
@@ -252,7 +251,7 @@ class PosOrderShOprateControllerTest {
         order.setShId(101L);
         order.setState(0L);
         InfoUser subaccount = user(501L, "5", null);
-        when(sessions.requireUserId(anyString())).thenReturn(501L);
+        authContext.when(AuthContext::requireUserId).thenReturn(501L);
         when(users.getOne(any())).thenReturn(subaccount);
         when(orders.getOne(any())).thenReturn(order);
         org.mockito.Mockito.doThrow(new ServiceException("merchant.store.access.denied"))
@@ -315,11 +314,9 @@ class PosOrderShOprateControllerTest {
         return controller;
     }
 
-    private static void wireMerchantSecurity(PosOrderShOprateController controller, Long userId) {
-        MerchantTokenSessionService sessions = mock(MerchantTokenSessionService.class);
+    private void wireMerchantSecurity(PosOrderShOprateController controller, Long userId) {
         MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
-        when(sessions.requireUserId(anyString())).thenReturn(userId);
-        ReflectionTestUtils.setField(controller, "merchantTokenSessionService", sessions);
+        authContext.when(AuthContext::requireUserId).thenReturn(userId);
         ReflectionTestUtils.setField(controller, "merchantStoreAccessService", access);
     }
 }

+ 11 - 2
ruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.java

@@ -13,6 +13,7 @@ import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.service.IInfoUserService;
 import com.ruoyi.system.service.IPosOrderService;
 import com.ruoyi.system.service.IUserWalletService;
+import com.ruoyi.system.utils.AuthContext;
 import com.ruoyi.system.utils.JwtUtil;
 import com.ruoyi.common.utils.spring.SpringUtils;
 import org.apache.ibatis.builder.MapperBuilderAssistant;
@@ -21,6 +22,7 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
 import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 import org.springframework.beans.factory.support.DefaultListableBeanFactory;
 import org.springframework.context.support.StaticMessageSource;
@@ -37,6 +39,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -250,9 +253,15 @@ class InfoUserControllerTest {
 
     @Test
     void merchantLogoutRevokesOnlyCurrentTokenSession() {
-        controller.merchantLogout("merchant-token");
+        try (MockedStatic<AuthContext> authContext = mockStatic(AuthContext.class)) {
+            authContext.when(AuthContext::requireJti)
+                    .thenReturn("qtw_tokens:sh:app:936:current-session");
 
-        verify(merchantTokenSessionService).logoutCurrent("merchant-token");
+            controller.merchantLogout("merchant-token");
+        }
+
+        verify(merchantTokenSessionService)
+                .logoutCurrent("qtw_tokens:sh:app:936:current-session");
     }
 
     private String tokenFor(Long userId) {

+ 55 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSessionAuthAnnotationTest.java

@@ -0,0 +1,55 @@
+package com.ruoyi.app.user;
+
+import com.ruoyi.app.mendian.FoodSpecController;
+import com.ruoyi.app.mendian.PosFenleiController;
+import com.ruoyi.app.mendian.PosFoodController;
+import com.ruoyi.app.mendian.PosStoreController;
+import com.ruoyi.app.order.PosOrderController;
+import com.ruoyi.app.order.PosOrderShOprateController;
+import com.ruoyi.system.utils.Auth;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MerchantSessionAuthAnnotationTest {
+
+    @Test
+    void merchantEndpointsRequireServerSessionValidation() {
+        assertSessionAuth(MerchantSubaccountController.class,
+                "list", "create", "update", "updatePassword", "updateStatus");
+        assertSessionAuth(InfoUserController.class, "merchantLogout");
+        assertSessionAuth(FoodSpecController.class,
+                "foodSpecPageList", "saveFoodSpec", "getSpecs", "deleteFoodSpec",
+                "getAvailableSpecsList", "changeOpen", "changeSpecValueOpen");
+        assertSessionAuth(PosFenleiController.class, "delefenlei", "addfenlei");
+        assertSessionAuth(PosFoodController.class,
+                "delefood", "changerecommend", "setposfood", "getidlist", "getFoodPageList");
+        assertSessionAuth(PosStoreController.class,
+                "delemendian", "storelistlist", "getmystorelist", "addmendian",
+                "getMdBusinessHours", "saveMdBusinessHours");
+        assertSessionAuth(PosOrderController.class,
+                "setorderuzt", "getorderxq", "getstoreorderlist");
+        assertSessionAuth(PosOrderShOprateController.class,
+                "getInvoice", "invalidInvoice", "createOrder", "acceptOrder", "dispatchOrder",
+                "completeOrder", "confirmCashPayment", "cancelOrder", "orderList", "orderDetail");
+    }
+
+    private void assertSessionAuth(Class<?> controllerType, String... methodNames) {
+        for (String methodName : methodNames) {
+            Method method = Arrays.stream(controllerType.getDeclaredMethods())
+                    .filter(candidate -> candidate.getName().equals(methodName))
+                    .findFirst()
+                    .orElseThrow(() -> new AssertionError(controllerType.getSimpleName()
+                            + "." + methodName + " not found"));
+            Auth auth = method.getAnnotation(Auth.class);
+            assertNotNull(auth, controllerType.getSimpleName() + "." + methodName
+                    + " must declare @Auth");
+            assertTrue(auth.session(), controllerType.getSimpleName() + "." + methodName
+                    + " must validate the Redis session");
+        }
+    }
+}

+ 6 - 4
ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountControllerTest.java

@@ -7,6 +7,7 @@ import com.ruoyi.app.user.dto.MerchantSubaccountUpdateRequest;
 import com.ruoyi.app.user.dto.MerchantSubaccountView;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.utils.AuthContext;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -23,23 +24,24 @@ import static org.mockito.Mockito.when;
 class MerchantSubaccountControllerTest {
 
     private MerchantSubaccountApplicationService applicationService;
-    private MerchantTokenSessionService tokenSessionService;
     private MerchantSubaccountController controller;
     private MockedStatic<MessageUtils> messages;
+    private MockedStatic<AuthContext> authContext;
 
     @BeforeEach
     void setUp() {
         messages = mockStatic(MessageUtils.class);
         messages.when(() -> MessageUtils.message(org.mockito.ArgumentMatchers.anyString()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
+        authContext = mockStatic(AuthContext.class);
+        authContext.when(AuthContext::requireUserId).thenReturn(10L);
         applicationService = mock(MerchantSubaccountApplicationService.class);
-        tokenSessionService = mock(MerchantTokenSessionService.class);
-        controller = new MerchantSubaccountController(applicationService, tokenSessionService);
-        when(tokenSessionService.requireUserId("token")).thenReturn(10L);
+        controller = new MerchantSubaccountController(applicationService);
     }
 
     @AfterEach
     void tearDown() {
+        authContext.close();
         messages.close();
     }
 

+ 8 - 1
ruoyi-system/src/main/java/com/ruoyi/system/utils/Auth.java

@@ -14,4 +14,11 @@ import java.lang.annotation.*;
 @Documented
 public @interface Auth {
     boolean token() default true;
-}
+
+    /**
+     * 是否要求 Token 对应的服务端 Redis 会话仍然有效。
+     *
+     * <p>默认关闭以保持普通用户和骑手接口的现有认证行为;商家业务接口按需显式开启。</p>
+     */
+    boolean session() default false;
+}

+ 38 - 6
ruoyi-system/src/main/java/com/ruoyi/system/utils/AuthAspect.java

@@ -1,6 +1,9 @@
 package com.ruoyi.system.utils;
 
 import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
@@ -11,6 +14,8 @@ import org.springframework.stereotype.Component;
 import jakarta.annotation.Resource;
 import jakarta.servlet.http.HttpServletRequest;
 
+import java.util.Map;
+
 /**
  * Created with IntelliJ IDEA.
  *
@@ -22,10 +27,16 @@ import jakarta.servlet.http.HttpServletRequest;
 @Aspect
 public class AuthAspect {
 
-    private static HttpServletRequest request;
+    private HttpServletRequest request;
+    private final RedisCache redisCache;
+
+    public AuthAspect(RedisCache redisCache) {
+        this.redisCache = redisCache;
+    }
+
     @Resource
     public void setHttpServletRequest(HttpServletRequest request) {
-        AuthAspect.request = request;
+        this.request = request;
     }
 
 
@@ -41,14 +52,35 @@ public class AuthAspect {
         // 获取目标方法对象上注解中的属性值
         Auth auth = methodSignature.getMethod().getAnnotation(Auth.class);
 
-        // 校验签名
+        // JWT 只解析和验签一次,后续会话校验及 Controller 均复用这里提取的可信身份。
         if (auth.token()) {
-            boolean token = JwtUtil.verify(request.getHeader("token"));
-            // 如果token校验失败返回
-            if (!token) {
+            Map<String, Object> claims = JwtUtil.verifyToken(request.getHeader("token"));
+            if (!Boolean.TRUE.equals(claims.get("valid"))) {
                 return AjaxResult.error(401,"token已过期,请重新登录!");
             }
+
+            Long userId = parseUserId(claims.get("id"));
+            Object jtiValue = claims.get("jti");
+            String jti = jtiValue instanceof String ? (String) jtiValue : null;
+            if (auth.session() && (userId == null || jti == null || jti.isBlank()
+                    || !Boolean.TRUE.equals(redisCache.hasKey(jti)))) {
+                throw new ServiceException(MessageUtils.message("merchant.session.invalid"));
+            }
+
+            request.setAttribute(AuthContext.USER_ID_ATTRIBUTE, userId);
+            request.setAttribute(AuthContext.JTI_ATTRIBUTE, jti);
         }
         return joinPoint.proceed();
     }
+
+    private Long parseUserId(Object claim) {
+        if (!(claim instanceof String value) || value.isBlank() || "null".equals(value)) {
+            return null;
+        }
+        try {
+            return Long.valueOf(value);
+        } catch (NumberFormatException exception) {
+            return null;
+        }
+    }
 }

+ 39 - 0
ruoyi-system/src/main/java/com/ruoyi/system/utils/AuthContext.java

@@ -0,0 +1,39 @@
+package com.ruoyi.system.utils;
+
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.ServletUtils;
+
+/**
+ * 读取认证切面写入当前请求的可信身份信息。
+ *
+ * <p>该上下文不解析或重新验证客户端 Token,避免业务 Controller 重复执行认证逻辑。</p>
+ */
+public final class AuthContext {
+
+    public static final String USER_ID_ATTRIBUTE = AuthContext.class.getName() + ".userId";
+    public static final String JTI_ATTRIBUTE = AuthContext.class.getName() + ".jti";
+
+    private AuthContext() {
+    }
+
+    public static Long requireUserId() {
+        Object value = ServletUtils.getRequest().getAttribute(USER_ID_ATTRIBUTE);
+        if (value instanceof Long userId) {
+            return userId;
+        }
+        throw unauthorized();
+    }
+
+    public static String requireJti() {
+        Object value = ServletUtils.getRequest().getAttribute(JTI_ATTRIBUTE);
+        if (value instanceof String jti && !jti.isBlank()) {
+            return jti;
+        }
+        throw unauthorized();
+    }
+
+    private static ServiceException unauthorized() {
+        return new ServiceException(MessageUtils.message("merchant.session.invalid"));
+    }
+}

+ 1 - 0
ruoyi-system/src/main/java/com/ruoyi/system/utils/JwtUtil.java

@@ -199,6 +199,7 @@ public class JwtUtil {
                         value = "null";
                     }
                 }
+                result.put(key, value);
             }
             result.replace("valid",true);
             return result;

+ 138 - 0
ruoyi-system/src/test/java/com/ruoyi/system/utils/AuthAspectTest.java

@@ -0,0 +1,138 @@
+package com.ruoyi.system.utils;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import java.lang.reflect.Method;
+import java.util.Date;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class AuthAspectTest {
+
+    private RedisCache redisCache;
+    private HttpServletRequest request;
+    private ProceedingJoinPoint joinPoint;
+    private MethodSignature signature;
+    private AuthAspect aspect;
+
+    @BeforeEach
+    void setUp() {
+        redisCache = mock(RedisCache.class);
+        request = mock(HttpServletRequest.class);
+        joinPoint = mock(ProceedingJoinPoint.class);
+        signature = mock(MethodSignature.class);
+        aspect = new AuthAspect(redisCache);
+        aspect.setHttpServletRequest(request);
+        when(joinPoint.getSignature()).thenReturn(signature);
+    }
+
+    @Test
+    void jwtOnlyAuthDoesNotRequireRedisSession() throws Throwable {
+        String token = JwtUtil.token("936", "merchant");
+        Object expected = new Object();
+        prepareInvocation("jwtOnly", token);
+        when(joinPoint.proceed()).thenReturn(expected);
+
+        Object actual = aspect.around(joinPoint);
+
+        assertSame(expected, actual);
+        verify(redisCache, never()).hasKey(anyString());
+        verify(request).setAttribute(AuthContext.USER_ID_ATTRIBUTE, 936L);
+    }
+
+    @Test
+    void merchantSessionAuthAllowsExistingRedisSession() throws Throwable {
+        String token = JwtUtil.token("936", "merchant");
+        String jti = tokenJti(token);
+        Object expected = new Object();
+        prepareInvocation("merchantSession", token);
+        when(redisCache.hasKey(jti)).thenReturn(true);
+        when(joinPoint.proceed()).thenReturn(expected);
+
+        Object actual = aspect.around(joinPoint);
+
+        assertSame(expected, actual);
+        verify(request).setAttribute(AuthContext.USER_ID_ATTRIBUTE, 936L);
+        verify(request).setAttribute(AuthContext.JTI_ATTRIBUTE, jti);
+    }
+
+    @Test
+    void merchantSessionAuthRejectsMissingRedisSessionBeforeController() throws Throwable {
+        String token = JwtUtil.token("936", "merchant");
+        String jti = tokenJti(token);
+        prepareInvocation("merchantSession", token);
+        when(redisCache.hasKey(jti)).thenReturn(false);
+
+        try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
+            messages.when(() -> MessageUtils.message("merchant.session.invalid"))
+                    .thenReturn("session invalid");
+
+            ServiceException exception = assertThrows(ServiceException.class,
+                    () -> aspect.around(joinPoint));
+
+            assertEquals("session invalid", exception.getMessage());
+            verify(joinPoint, never()).proceed();
+        }
+    }
+
+    @Test
+    void merchantSessionAuthRejectsTokenWithoutJti() throws Throwable {
+        String token = JWT.create()
+                .withClaim("id", "936")
+                .withExpiresAt(new Date(System.currentTimeMillis() + 60_000))
+                .sign(Algorithm.HMAC256("TEST-AUTH-TOKEN"));
+        prepareInvocation("merchantSession", token);
+
+        try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
+            messages.when(() -> MessageUtils.message("merchant.session.invalid"))
+                    .thenReturn("session invalid");
+
+            ServiceException exception = assertThrows(ServiceException.class,
+                    () -> aspect.around(joinPoint));
+
+            assertEquals("session invalid", exception.getMessage());
+            verify(redisCache, never()).hasKey(anyString());
+            verify(joinPoint, never()).proceed();
+        }
+    }
+
+    private void prepareInvocation(String methodName, String token) throws Exception {
+        Method method = SecuredMethods.class.getDeclaredMethod(methodName);
+        when(signature.getMethod()).thenReturn(method);
+        when(request.getHeader("token")).thenReturn(token);
+    }
+
+    private String tokenJti(String token) {
+        Map<String, Object> claims = JwtUtil.verifyToken(token);
+        return (String) claims.get("jti");
+    }
+
+    static class SecuredMethods {
+        @Auth
+        public void jwtOnly() {
+        }
+
+        @Auth(session = true)
+        public void merchantSession() {
+        }
+    }
+}

+ 1 - 1
specs/022-merchant-store-subaccounts/spec.md

@@ -152,7 +152,7 @@
 - **FR-033**:平台不得修改分管账号的店铺授权或密码;这些能力只属于该账号的主账号。
 - **FR-034**:主商家被平台停用时,系统必须同时禁止其全部分管账号登录、访问商家业务和接收店铺通知。
 - **FR-035**:平台现有商家新增、修改和审核入口不得创建分管账号或把普通账号转换为分管账号。
-- **FR-036**:商家业务接口的 JWT 签名、有效期和 Redis 会话存在性必须在进入 Controller 前由统一认证层完成,不得由各 Controller 重复执行会话认证。
+- **FR-036**:需要服务端会话撤销能力的商家业务接口,其 JWT 签名、有效期和 Redis 会话存在性必须在进入 Controller 前由统一认证层完成,不得由各 Controller 重复执行会话认证。
 - **FR-037**:Controller 只能使用统一认证通过后的 Token 身份执行门店、订单、商品等业务归属校验;会话服务只负责退出、批量撤销和在线状态查询。
 
 ### 关键实体

+ 318 - 0
specs/022-merchant-store-subaccounts/t012-auth-session-implementation-plan.md

@@ -0,0 +1,318 @@
+# 商家会话统一认证实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将商家 JWT 签名、有效期和 Redis 会话校验集中到 `@Auth` 认证切面,删除业务 Controller 对 `MerchantTokenSessionService.requireUserId` 的二次认证。
+
+**Architecture:** `@Auth(session = true)` 显式声明需要 Redis 会话的商家接口;`AuthAspect` 一次解析已验证 JWT、校验 JTI 并把可信用户 ID/JTI 写入当前请求属性。Controller 通过只读的 `AuthContext` 获取认证结果并继续执行门店、订单和商品归属校验;`MerchantTokenSessionService` 只管理退出、撤销和在线状态。
+
+**Tech Stack:** Java 21、Spring Boot、Spring AOP、Auth0 Java JWT、Redis、JUnit 5、Mockito、Maven。
+
+## Global Constraints
+
+- 保持模块依赖方向 `ruoyi-admin -> ruoyi-system`,`ruoyi-system` 不导入 `com.ruoyi.app.*`。
+- 普通用户、骑手和未显式设置 `session = true` 的 `@Auth` 接口保持现有 JWT 校验行为。
+- 商家接口继续直接声明 `@RequestHeader String token`,不使用 `HttpServletRequest` 读取 Controller 入参。
+- Controller 的门店、订单和商品实际归属校验必须保留,不得把认证收口等同于业务授权。
+- 所有错误使用现有国际化键 `merchant.session.invalid`,不新增硬编码单语言业务错误。
+- 使用 `C:\Users\qmj\.jdks\graalvm-jdk-21.0.7`,只在当前命令设置 `JAVA_HOME` 和 `PATH`。
+- 仅暂存本任务源码、测试和规格,不混入 `.claude/homunculus/observations.jsonl`、`.tmp/` 或其他已有脏文件。
+
+---
+
+### Task 1:认证切面与可信身份上下文
+
+**Files:**
+
+- Modify: `ruoyi-system/src/main/java/com/ruoyi/system/utils/Auth.java`
+- Modify: `ruoyi-system/src/main/java/com/ruoyi/system/utils/AuthAspect.java`
+- Modify: `ruoyi-system/src/main/java/com/ruoyi/system/utils/JwtUtil.java`
+- Create: `ruoyi-system/src/main/java/com/ruoyi/system/utils/AuthContext.java`
+- Create: `ruoyi-system/src/test/java/com/ruoyi/system/utils/AuthAspectTest.java`
+
+**Interfaces:**
+
+- Produces: `Auth.session(): boolean`,默认 `false`。
+- Produces: `AuthContext.requireUserId(): Long` 与 `AuthContext.requireJti(): String`。
+- Produces: 请求属性 `AuthContext.USER_ID_ATTRIBUTE`、`AuthContext.JTI_ATTRIBUTE`。
+
+- [ ] **Step 1: 编写失败测试**
+
+在 `AuthAspectTest` 定义分别标注 `@Auth` 和 `@Auth(session = true)` 的样例方法,并验证三项行为:默认认证不访问 Redis;启用会话认证且 Redis 含 JTI 时调用 `joinPoint.proceed()`;Redis 缺少 JTI 时抛出 `ServiceException` 且不进入目标方法。
+
+```java
+static class SecuredMethods {
+    @Auth
+    public void jwtOnly() {}
+
+    @Auth(session = true)
+    public void merchantSession() {}
+}
+
+assertSame(expected, aspect.around(joinPoint));
+verify(redisCache, never()).hasKey(anyString());
+
+when(redisCache.hasKey(jti)).thenReturn(false);
+assertThrows(ServiceException.class, () -> aspect.around(joinPoint));
+verify(joinPoint, never()).proceed();
+```
+
+- [ ] **Step 2: 运行测试确认 RED**
+
+Run:
+
+```powershell
+$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
+$env:PATH="$env:JAVA_HOME\bin;$env:PATH"
+mvn -pl ruoyi-system -Dtest=AuthAspectTest test
+```
+
+Expected: FAIL,因为 `Auth.session`、Redis 会话校验和可信身份请求属性尚不存在。
+
+- [ ] **Step 3: 实现最小认证能力**
+
+为 `Auth` 增加默认关闭的选项:
+
+```java
+boolean session() default false;
+```
+
+修正 `JwtUtil.verifyToken`,将已验证的 `id` 等 Claim 写入返回结果,避免认证切面再次验签:
+
+```java
+result.put(key, value);
+```
+
+`AuthAspect` 使用 `verifyToken` 完成一次认证;`session = true` 时检查 JTI Redis Key,并在放行前写入可信请求属性:
+
+```java
+Map<String, Object> claims = JwtUtil.verifyToken(rawToken);
+if (!Boolean.TRUE.equals(claims.get("valid"))) {
+    return AjaxResult.error(401, "token已过期,请重新登录!");
+}
+Long userId = parseUserId(claims.get("id"));
+Object jtiValue = claims.get("jti");
+String jti = jtiValue instanceof String ? (String) jtiValue : null;
+if (auth.session() && (userId == null || jti == null || jti.isBlank()
+        || !redisCache.hasKey(jti))) {
+    throw new ServiceException(MessageUtils.message("merchant.session.invalid"));
+}
+request.setAttribute(AuthContext.USER_ID_ATTRIBUTE, userId);
+request.setAttribute(AuthContext.JTI_ATTRIBUTE, jti);
+```
+
+`AuthContext` 只读取认证切面写入的属性,不解析或重新校验 Token:
+
+```java
+public static Long requireUserId() {
+    Object value = ServletUtils.getRequest().getAttribute(USER_ID_ATTRIBUTE);
+    if (value instanceof Long userId) {
+        return userId;
+    }
+    throw new ServiceException(MessageUtils.message("merchant.session.invalid"));
+}
+```
+
+- [ ] **Step 4: 运行测试确认 GREEN**
+
+Run: `mvn -pl ruoyi-system -Dtest=AuthAspectTest test`
+
+Expected: PASS,且无 Redis 访问发生在默认 `@Auth` 场景。
+
+---
+
+### Task 2:会话服务职责收缩与账号管理入口迁移
+
+**Files:**
+
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantTokenSessionService.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountController.java`
+- Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.java`
+- Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountControllerTest.java`
+
+**Interfaces:**
+
+- Consumes: `@Auth(session = true)`、`AuthContext.requireUserId()`、`AuthContext.requireJti()`。
+- Produces: `MerchantTokenSessionService.logoutCurrent(String jti)` 只删除已经过认证的当前 JTI。
+- Preserves: `revokeAll(Long)`、`isOnline(Long)`、`hasAppSession(Long)`。
+
+- [ ] **Step 1: 更新失败测试**
+
+账号管理 Controller 测试不再模拟 `tokenSessionService.requireUserId(token)`,改为静态模拟认证上下文;退出测试验证删除可信 JTI 而不是重新解析 Token:
+
+```java
+try (MockedStatic<AuthContext> auth = mockStatic(AuthContext.class)) {
+    auth.when(AuthContext::requireUserId).thenReturn(936L);
+    controller.list("merchant-token");
+    verify(applicationService).listForOwner(936L);
+}
+
+auth.when(AuthContext::requireJti).thenReturn("qtw_tokens:sh:app:936:test");
+verify(merchantTokenSessionService).logoutCurrent("qtw_tokens:sh:app:936:test");
+```
+
+- [ ] **Step 2: 运行定向测试确认 RED**
+
+Run:
+
+```powershell
+mvn -pl ruoyi-admin -am -Dtest=InfoUserControllerTest,MerchantSubaccountControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
+```
+
+Expected: FAIL,因为 Controller 仍调用 `requireUserId(token)`,退出仍把原 Token 传给会话服务。
+
+- [ ] **Step 3: 迁移账号入口**
+
+将商家退出和五个分管账号接口改为 `@Auth(session = true)`;Controller 使用 `AuthContext`:
+
+```java
+@Auth(session = true)
+public AjaxResult list(@RequestHeader String token) {
+    return success(applicationService.listForOwner(AuthContext.requireUserId()));
+}
+
+@Auth(session = true)
+public AjaxResult merchantLogout(@RequestHeader String token) {
+    merchantTokenSessionService.logoutCurrent(AuthContext.requireJti());
+    return success();
+}
+```
+
+删除 `MerchantTokenSessionService.requireUserId`,`logoutCurrent` 只删除非空 JTI;撤销和在线查询方法不变。
+
+- [ ] **Step 4: 运行定向测试确认 GREEN**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=InfoUserControllerTest,MerchantSubaccountControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`
+
+Expected: PASS。
+
+---
+
+### Task 3:店铺、商品和订单 Controller 迁移
+
+**Files:**
+
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/FoodSpecController.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFenleiController.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFoodController.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java`
+- Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerAccessTest.java`
+- Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerShelfFilterTest.java`
+- Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosStoreControllerTest.java`
+- Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.java`
+
+**Interfaces:**
+
+- Consumes: `@Auth(session = true)` 与 `AuthContext.requireUserId()`。
+- Preserves: `MerchantStoreAccessService` 的门店、商品和订单归属校验。
+- Removes: 六个业务 Controller 对 `MerchantTokenSessionService` 的注入和调用。
+
+- [ ] **Step 1: 更新 Controller 定向测试形成 RED**
+
+在直接调用 Controller 的测试中静态模拟 `AuthContext.requireUserId()`,删除 `MerchantTokenSessionService.requireUserId` 桩;保留并断言原有归属校验:
+
+```java
+authContext = mockStatic(AuthContext.class);
+authContext.when(AuthContext::requireUserId).thenReturn(936L);
+
+controller.getmystorelist("merchant-token");
+verify(merchantStoreAccessService).getAccessibleStoreIds(936L);
+```
+
+- [ ] **Step 2: 运行店铺与订单测试确认 RED**
+
+Run:
+
+```powershell
+mvn -pl ruoyi-admin -am -Dtest=PosStoreControllerTest,PosFoodControllerAccessTest,PosFoodControllerShelfFilterTest,PosOrderShOprateControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
+```
+
+Expected: FAIL,因为生产 Controller 仍依赖 `MerchantTokenSessionService.requireUserId(token)`。
+
+- [ ] **Step 3: 迁移六个业务 Controller**
+
+所有当前依赖 `requireUserId(token)` 的公开入口改为 `@Auth(session = true)`,方法体改取可信上下文:
+
+```java
+Long loginUserId = AuthContext.requireUserId();
+merchantStoreAccessService.requireStoreAccess(loginUserId, mdId);
+```
+
+私有辅助方法不再接收 Token 来认证,统一读取或接收已经取得的 `loginUserId`;删除六个 Controller 中不再使用的 `MerchantTokenSessionService` import 和字段。不得删除 `requireStoreAccess`、`requireFoodAccess`、`requireOrderAccess` 或订单所有权检查。
+
+- [ ] **Step 4: 静态确认没有 Controller 二次认证**
+
+Run:
+
+```powershell
+rg -n "MerchantTokenSessionService.*requireUserId|merchantTokenSessionService\.requireUserId|tokenSessionService\.requireUserId" ruoyi-admin/src/main/java
+```
+
+Expected: 无匹配。
+
+- [ ] **Step 5: 运行店铺与订单测试确认 GREEN**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=PosStoreControllerTest,PosFoodControllerAccessTest,PosFoodControllerShelfFilterTest,PosOrderShOprateControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`
+
+Expected: PASS。
+
+---
+
+### Task 4:统一验证、规格回填、提交和推送
+
+**Files:**
+
+- Modify: `specs/022-merchant-store-subaccounts/tasks.md`
+- Verify: Task 1~3 的全部源码与测试文件。
+
+**Interfaces:**
+
+- Verifies: FR-036、FR-037、SC-010。
+- Produces: 一个不混入既有脏文件的中文实现提交并推送当前分支。
+
+- [ ] **Step 1: 运行全部定向测试**
+
+Run:
+
+```powershell
+$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
+$env:PATH="$env:JAVA_HOME\bin;$env:PATH"
+mvn -pl ruoyi-admin -am -Dtest=AuthAspectTest,InfoUserControllerTest,MerchantSubaccountControllerTest,PosStoreControllerTest,PosFoodControllerAccessTest,PosFoodControllerShelfFilterTest,PosOrderShOprateControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
+```
+
+Expected: BUILD SUCCESS,所有指定测试零失败。
+
+- [ ] **Step 2: 构建后端模块**
+
+Run: `mvn -pl ruoyi-admin -am -DskipTests package`
+
+Expected: BUILD SUCCESS。
+
+- [ ] **Step 3: 回填任务状态并检查差异**
+
+将 `tasks.md` 的 T012 六项全部勾选;运行:
+
+```powershell
+git diff --check
+rg -n "MerchantTokenSessionService.*requireUserId|merchantTokenSessionService\.requireUserId|tokenSessionService\.requireUserId" ruoyi-admin/src/main/java
+git status --short
+```
+
+Expected: `git diff --check` 无错误,`rg` 无匹配,状态清单只包含本任务文件和既有未暂存文件。
+
+- [ ] **Step 4: 精确暂存并提交**
+
+仅暂存 Task 1~3 的源码、测试及 `specs/022-merchant-store-subaccounts` 本任务文件,核对 `git diff --cached --name-only` 后提交:
+
+```powershell
+git commit -m "统一商家会话认证入口" -m "将 JWT 与 Redis 会话校验集中到认证切面,业务控制器仅使用可信身份执行门店和订单归属校验。"
+```
+
+- [ ] **Step 5: 推送当前分支**
+
+Run: `git push`
+
+Expected: 当前分支推送成功,记录提交 SHA 和远端分支。

+ 6 - 6
specs/022-merchant-store-subaccounts/tasks.md

@@ -372,9 +372,9 @@ T005..T007 --------------------------> T012
 - 修改:当前调用 `MerchantTokenSessionService.requireUserId` 的商家 Controller
 - 新增或修改:认证切面、商家店铺和商家订单定向测试
 
-- [ ] 先写认证切面测试,覆盖启用会话校验时有效 JTI 放行、缺失 JTI/Redis Key 拒绝,以及默认 `@Auth` 不增加 Redis 会话要求;运行并确认 RED。
-- [ ] 为 `@Auth` 增加默认关闭的会话校验选项,由 `AuthAspect` 在 JWT 校验通过后检查 JTI 对应 Redis Key,并使用现有国际化登录失效错误。
-- [ ] 商家业务入口显式启用会话校验,删除 Controller 对 `MerchantTokenSessionService.requireUserId` 的调用;Controller 只从已验证 Token 获取登录用户 ID 并继续执行目标实体归属校验。
-- [ ] `MerchantTokenSessionService` 删除身份认证职责,只保留退出当前会话、撤销账号全部 App/PC 会话和在线状态查询。
-- [ ] 运行认证切面、商家登录、店铺和订单定向测试,并使用 JDK 21 构建 `ruoyi-admin` 及依赖模块。
-- [ ] 检查 `git diff --check`、暂存文件清单和测试结果,形成独立中文提交并推送当前分支。
+- [x] 先写认证切面测试,覆盖启用会话校验时有效 JTI 放行、缺失 JTI/Redis Key 拒绝,以及默认 `@Auth` 不增加 Redis 会话要求;运行并确认 RED。
+- [x] 为 `@Auth` 增加默认关闭的会话校验选项,由 `AuthAspect` 在 JWT 校验通过后检查 JTI 对应 Redis Key,并使用现有国际化登录失效错误。
+- [x] 商家业务入口显式启用会话校验,删除 Controller 对 `MerchantTokenSessionService.requireUserId` 的调用;Controller 只从已验证 Token 获取登录用户 ID 并继续执行目标实体归属校验。
+- [x] `MerchantTokenSessionService` 删除身份认证职责,只保留退出当前会话、撤销账号全部 App/PC 会话和在线状态查询。
+- [x] 运行认证切面、商家登录、店铺和订单定向测试,并使用 JDK 21 构建 `ruoyi-admin` 及依赖模块。
+- [x] 检查 `git diff --check`、暂存文件清单和测试结果,形成独立中文提交并推送当前分支。