ソースを参照

Merge branch 'sit-OrderFood-LuTong' into sit-OrderFood

# Conflicts:
#	alien-dining/src/main/java/shop/alien/dining/service/impl/DiningUserServiceImpl.java
lutong 2 ヶ月 前
コミット
3267d96213

+ 47 - 24
alien-dining/src/main/java/shop/alien/dining/controller/DiningController.java

@@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.vo.*;
 import shop.alien.dining.service.DiningService;
-import shop.alien.util.common.JwtUtil;
+import shop.alien.dining.util.TokenUtil;
 
 import java.util.List;
 
@@ -34,6 +34,10 @@ public class DiningController {
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "就餐人数", required = true) @RequestParam Integer dinerCount) {
         try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             DiningPageInfoVO vo = diningService.getDiningPageInfo(tableId, dinerCount);
             return R.data(vo);
         } catch (Exception e) {
@@ -49,6 +53,10 @@ public class DiningController {
             @ApiParam(value = "搜索关键词", required = false) @RequestParam(required = false) String keyword,
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             // 限制关键词长度
             if (StringUtils.hasText(keyword) && keyword.length() > 10) {
                 keyword = keyword.substring(0, 10);
@@ -70,6 +78,10 @@ public class DiningController {
             @ApiParam(value = "页码", required = false) @RequestParam(defaultValue = "1") Integer page,
             @ApiParam(value = "每页数量", required = false) @RequestParam(defaultValue = "12") Integer size) {
         try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             List<CuisineListVO> list = diningService.getCuisinesByCategory(storeId, categoryId, tableId, page, size);
             return R.data(list);
         } catch (Exception e) {
@@ -84,6 +96,10 @@ public class DiningController {
             @ApiParam(value = "菜品ID", required = true) @PathVariable Integer cuisineId,
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             CuisineDetailVO vo = diningService.getCuisineDetail(cuisineId, tableId);
             return R.data(vo);
         } catch (Exception e) {
@@ -97,8 +113,11 @@ public class DiningController {
     public R<List<AvailableCouponVO>> getAvailableCoupons(
             @ApiParam(value = "门店ID", required = true) @RequestParam Integer storeId) {
         try {
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
             List<AvailableCouponVO> list = diningService.getAvailableCoupons(storeId, userId);
             return R.data(list);
         } catch (Exception e) {
@@ -112,11 +131,11 @@ public class DiningController {
     public R<Boolean> receiveCoupon(
             @ApiParam(value = "优惠券ID", required = true) @RequestParam Integer couponId) {
         try {
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            if (userInfo == null) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
-            Integer userId = userInfo.getInteger("userId");
             boolean result = diningService.receiveCoupon(couponId, userId);
             return R.data(result);
         } catch (Exception e) {
@@ -131,11 +150,11 @@ public class DiningController {
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "就餐人数", required = true) @RequestParam Integer dinerCount) {
         try {
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            if (userInfo == null) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
-            Integer userId = userInfo.getInteger("userId");
             OrderConfirmVO vo = diningService.getOrderConfirmInfo(tableId, dinerCount, userId);
             return R.data(vo);
         } catch (Exception e) {
@@ -149,11 +168,11 @@ public class DiningController {
     public R<Boolean> lockOrder(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            if (userInfo == null) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
-            Integer userId = userInfo.getInteger("userId");
             boolean result = diningService.lockOrder(tableId, userId);
             if (!result) {
                 return R.fail("订单已被其他用户锁定,无法下单");
@@ -170,11 +189,11 @@ public class DiningController {
     public R<Boolean> unlockOrder(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            if (userInfo == null) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
-            Integer userId = userInfo.getInteger("userId");
             diningService.unlockOrder(tableId, userId);
             return R.data(true);
         } catch (Exception e) {
@@ -188,6 +207,10 @@ public class DiningController {
     public R<Integer> checkOrderLock(
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId) {
         try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             Integer lockUserId = diningService.checkOrderLock(tableId);
             return R.data(lockUserId);
         } catch (Exception e) {
@@ -201,11 +224,11 @@ public class DiningController {
     public R<shop.alien.entity.store.vo.OrderSettlementVO> getOrderSettlementInfo(
             @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
         try {
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            if (userInfo == null) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
-            Integer userId = userInfo.getInteger("userId");
             shop.alien.entity.store.vo.OrderSettlementVO vo = diningService.getOrderSettlementInfo(orderId, userId);
             return R.data(vo);
         } catch (Exception e) {
@@ -219,11 +242,11 @@ public class DiningController {
     public R<Boolean> lockSettlement(
             @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
         try {
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            if (userInfo == null) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
-            Integer userId = userInfo.getInteger("userId");
             boolean result = diningService.lockSettlement(orderId, userId);
             if (!result) {
                 return R.fail("订单已被其他用户锁定,无法结算");
@@ -240,11 +263,11 @@ public class DiningController {
     public R<Boolean> unlockSettlement(
             @ApiParam(value = "订单ID", required = true) @RequestParam Integer orderId) {
         try {
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            if (userInfo == null) {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
                 return R.fail("用户未登录");
             }
-            Integer userId = userInfo.getInteger("userId");
             diningService.unlockSettlement(orderId, userId);
             return R.data(true);
         } catch (Exception e) {

+ 64 - 11
alien-dining/src/main/java/shop/alien/dining/controller/DiningUserController.java

@@ -9,9 +9,12 @@ import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.dining.dto.ChangePhoneDto;
 import shop.alien.dining.dto.UserProfileUpdateDto;
+import shop.alien.dining.dto.VerifyTokenDto;
 import shop.alien.dining.dto.WeChatLoginDto;
 import shop.alien.dining.service.DiningUserService;
 import shop.alien.dining.vo.DiningUserVo;
+import shop.alien.dining.vo.TokenVerifyVo;
+import org.apache.commons.lang3.StringUtils;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.validation.Valid;
@@ -34,24 +37,20 @@ public class DiningUserController {
 
     private final DiningUserService diningUserService;
 
-    @ApiOperation(value = "微信小程序登录", notes = "仅通过 wx.getPhoneNumber() 的 phoneCode 登录,无需 wx.login。后端调用 phonenumber.getPhoneNumber 换取手机号。")
+    @ApiOperation(value = "微信小程序登录", notes = "标准流程:1. 前端调用 wx.login() 获取 code;2. 后端调用 jscode2session 获取 openid;3. 可选:通过 wx.getPhoneNumber() 获取 phoneCode 绑定手机号")
     @PostMapping("/wechatLogin")
-    public R<DiningUserVo> wechatLogin(@RequestBody WeChatLoginDto loginDto, HttpServletRequest request) {
-        log.info("微信小程序登录: phoneCode={}, storeId={}",
-                loginDto.getPhoneCode(),
-                loginDto.getStoreId());
-
-        if (loginDto.getPhoneCode() == null || loginDto.getPhoneCode().trim().isEmpty()) {
-            return R.fail("phoneCode不能为空,请通过 wx.getPhoneNumber() 获取");
-        }
+    public R<DiningUserVo> wechatLogin(@Valid @RequestBody WeChatLoginDto loginDto, HttpServletRequest request) {
+        log.info("微信小程序登录: code={}, phoneCode={}",
+                loginDto.getCode(),
+                loginDto.getPhoneCode());
 
         String macIp = getClientIp(request);
         DiningUserVo userVo = diningUserService.wechatLogin(
+                loginDto.getCode(),
                 loginDto.getPhoneCode(),
-                loginDto.getStoreId(),
                 macIp);
         if (userVo == null) {
-            return R.fail("登录失败,请先授权手机号(phoneCode 有效期5分钟,仅能使用一次)");
+            return R.fail("登录失败,请检查 code 是否有效(code 有效期5分钟,仅能使用一次)");
         }
         return R.data(userVo);
     }
@@ -95,6 +94,60 @@ public class DiningUserController {
         return R.data(userVo);
     }
 
+    @ApiOperation(value = "校验Token", notes = "验证Token是否有效,支持从请求头Authorization或请求体获取Token")
+    @PostMapping("/verifyToken")
+    public R<TokenVerifyVo> verifyToken(@RequestBody(required = false) VerifyTokenDto dto, HttpServletRequest request) {
+        // 优先从请求体获取 token,如果没有则从请求头获取
+        String token = null;
+        if (dto != null && StringUtils.isNotBlank(dto.getToken())) {
+            token = dto.getToken();
+        } else {
+            // 从请求头获取
+            token = request.getHeader("Authorization");
+        }
+
+        if (StringUtils.isBlank(token)) {
+            return R.fail("Token 不能为空,请通过请求头 Authorization 或请求体传递");
+        }
+
+        log.info("校验Token: token={}", token.length() > 20 ? token.substring(0, 20) + "****" : token);
+
+        TokenVerifyVo verifyVo = diningUserService.verifyToken(token);
+        if (verifyVo == null) {
+            return R.fail("Token 校验失败");
+        }
+
+        if (verifyVo.getValid()) {
+            return R.data(verifyVo);
+        } else {
+            return R.fail(verifyVo.getReason() != null ? verifyVo.getReason() : "Token 无效");
+        }
+    }
+
+    @ApiOperation(value = "解析Token(调试用)", notes = "解析Token内容,用于调试,返回Token中的所有用户信息")
+    @PostMapping("/parseToken")
+    public R<com.alibaba.fastjson.JSONObject> parseToken(@RequestBody(required = false) VerifyTokenDto dto, HttpServletRequest request) {
+        // 优先从请求体获取 token,如果没有则从请求头获取
+        String token = null;
+        if (dto != null && StringUtils.isNotBlank(dto.getToken())) {
+            token = dto.getToken();
+        } else {
+            // 从请求头获取
+            token = request.getHeader("Authorization");
+        }
+
+        if (StringUtils.isBlank(token)) {
+            return R.fail("Token 不能为空");
+        }
+
+        com.alibaba.fastjson.JSONObject userInfo = shop.alien.dining.util.TokenUtil.parseToken(token);
+        if (userInfo == null) {
+            return R.fail("Token 解析失败,请查看日志获取详细信息");
+        }
+
+        return R.data(userInfo);
+    }
+
     /**
      * 获取客户端IP地址
      */

+ 72 - 5
alien-dining/src/main/java/shop/alien/dining/controller/StoreOrderController.java

@@ -23,7 +23,7 @@ import shop.alien.dining.config.CartWebSocketProcess;
 import shop.alien.dining.service.CartService;
 import shop.alien.dining.service.SseService;
 import shop.alien.dining.service.StoreOrderService;
-import shop.alien.util.common.JwtUtil;
+import shop.alien.dining.util.TokenUtil;
 
 import javax.validation.Valid;
 import java.util.List;
@@ -54,6 +54,10 @@ public class StoreOrderController {
     @GetMapping("/cart/{tableId}")
     public R<CartDTO> getCart(@ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
         try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             CartDTO cart = cartService.getCart(tableId);
             return R.data(cart);
         } catch (Exception e) {
@@ -66,6 +70,10 @@ public class StoreOrderController {
     @PostMapping("/cart/add")
     public R<CartDTO> addCartItem(@Valid @RequestBody AddCartItemDTO dto) {
         try {
+            // 验证 token(用户信息在 CartService 中从 token 获取)
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             CartDTO cart = cartService.addItem(dto);
             // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(dto.getTableId(), cart);
@@ -85,6 +93,10 @@ public class StoreOrderController {
             @ApiParam(value = "菜品ID", required = true) @RequestParam Integer cuisineId,
             @ApiParam(value = "数量", required = true) @RequestParam Integer quantity) {
         try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             CartDTO cart = cartService.updateItemQuantity(tableId, cuisineId, quantity);
             // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(tableId, cart);
@@ -103,6 +115,10 @@ public class StoreOrderController {
             @ApiParam(value = "桌号ID", required = true) @RequestParam Integer tableId,
             @ApiParam(value = "菜品ID", required = true) @RequestParam Integer cuisineId) {
         try {
+            // 验证 token
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             CartDTO cart = cartService.removeItem(tableId, cuisineId);
             // 推送购物车更新消息(SSE)
             sseService.pushCartUpdate(tableId, cart);
@@ -119,6 +135,10 @@ public class StoreOrderController {
     @PostMapping("/create")
     public R<shop.alien.entity.store.vo.OrderSuccessVO> createOrder(@Valid @RequestBody CreateOrderDTO dto) {
         try {
+            // 验证 token(用户信息在 StoreOrderService 中从 token 获取)
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             // 限制备注长度
             if (dto.getRemark() != null && dto.getRemark().length() > 30) {
                 dto.setRemark(dto.getRemark().substring(0, 30));
@@ -153,6 +173,10 @@ public class StoreOrderController {
     @PostMapping("/add-dish")
     public R<StoreOrder> addDishToOrder(@Valid @RequestBody shop.alien.entity.store.dto.AddDishDTO dto) {
         try {
+            // 验证 token(用户信息在 StoreOrderService 中从 token 获取)
+            if (!TokenUtil.hasValidToken()) {
+                return R.fail("用户未登录");
+            }
             StoreOrder order = orderService.addDishToOrder(dto.getOrderId(), dto.getCuisineId(), dto.getQuantity(), dto.getRemark());
             return R.data(order);
         } catch (Exception e) {
@@ -167,6 +191,12 @@ public class StoreOrderController {
             @ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId,
             @ApiParam(value = "支付方式(1:微信, 2:支付宝, 3:现金)", required = true) @RequestParam Integer payType) {
         try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            
             StoreOrder order = orderService.getOrderById(orderId);
             if (order == null) {
                 return R.fail("订单不存在");
@@ -203,6 +233,11 @@ public class StoreOrderController {
     @PostMapping("/cancel/{orderId}")
     public R<Boolean> cancelOrder(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
             boolean result = orderService.cancelOrder(orderId);
             return R.data(result);
         } catch (Exception e) {
@@ -215,6 +250,11 @@ public class StoreOrderController {
     @GetMapping("/detail/{orderId}")
     public R<StoreOrder> getOrderDetail(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
             StoreOrder order = orderService.getOrderById(orderId);
             if (order == null) {
                 return R.fail("订单不存在");
@@ -230,6 +270,11 @@ public class StoreOrderController {
     @GetMapping("/detail/list/{orderId}")
     public R<List<StoreOrderDetail>> getOrderDetailList(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
             com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StoreOrderDetail> wrapper =
                     new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
             wrapper.eq(StoreOrderDetail::getOrderId, orderId);
@@ -252,6 +297,11 @@ public class StoreOrderController {
             @ApiParam(value = "桌号ID") @RequestParam(required = false) Integer tableId,
             @ApiParam(value = "订单状态") @RequestParam(required = false) Integer orderStatus) {
         try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
             Page<StoreOrder> page = new Page<>(current, size);
             IPage<StoreOrder> result = orderService.getOrderPage(page, storeId, tableId, orderStatus);
             return R.data(result);
@@ -265,13 +315,15 @@ public class StoreOrderController {
     @PostMapping("/change-table")
     public R<CartDTO> changeTable(@Valid @RequestBody ChangeTableDTO dto) {
         try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
+            
             // 迁移购物车
             CartDTO cart = cartService.migrateCart(dto.getFromTableId(), dto.getToTableId());
 
-            // 记录换桌日志
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
-
             // 查询桌号信息
             StoreTable fromTable = storeTableMapper.selectById(dto.getFromTableId());
             StoreTable toTable = storeTableMapper.selectById(dto.getToTableId());
@@ -299,6 +351,11 @@ public class StoreOrderController {
     @ApiOperation(value = "建立SSE连接", notes = "建立SSE连接,用于接收购物车更新消息")
     @GetMapping(value = "/sse/{tableId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
     public SseEmitter createSseConnection(@ApiParam(value = "桌号ID", required = true) @PathVariable Integer tableId) {
+        // 验证 token
+        if (!TokenUtil.hasValidToken()) {
+            log.warn("建立SSE连接失败: 用户未登录, tableId={}", tableId);
+            return null;
+        }
         log.info("建立SSE连接, tableId={}", tableId);
         return sseService.createConnection(tableId);
     }
@@ -307,6 +364,11 @@ public class StoreOrderController {
     @PostMapping("/complete/{orderId}")
     public R<Boolean> completeOrder(@ApiParam(value = "订单ID", required = true) @PathVariable Integer orderId) {
         try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
             boolean result = orderService.completeOrder(orderId);
             return R.data(result);
         } catch (Exception e) {
@@ -319,6 +381,11 @@ public class StoreOrderController {
     @PostMapping("/update-coupon")
     public R<StoreOrder> updateOrderCoupon(@Valid @RequestBody shop.alien.entity.store.dto.UpdateOrderCouponDTO dto) {
         try {
+            // 从 token 获取用户信息
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId == null) {
+                return R.fail("用户未登录");
+            }
             StoreOrder order = orderService.updateOrderCoupon(dto.getOrderId(), dto.getCouponId());
             return R.data(order);
         } catch (Exception e) {

+ 20 - 0
alien-dining/src/main/java/shop/alien/dining/dto/VerifyTokenDto.java

@@ -0,0 +1,20 @@
+package shop.alien.dining.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * Token 校验请求DTO
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("Token校验请求")
+public class VerifyTokenDto {
+
+    @ApiModelProperty(value = "Token(可选,如果不传则从请求头 Authorization 获取)", required = false)
+    private String token;
+}

+ 9 - 6
alien-dining/src/main/java/shop/alien/dining/dto/WeChatLoginDto.java

@@ -7,20 +7,23 @@ import lombok.Data;
 import javax.validation.constraints.NotBlank;
 
 /**
- * 微信小程序登录DTO(新方式:仅 getPhoneNumber,无需 wx.login)
+ * 微信小程序登录DTO
+ * 标准流程:通过 wx.login() 获取 code,调用 jscode2session 获取 openid
+ * 可选:通过 wx.getPhoneNumber() 获取 phoneCode,用于绑定手机号
  *
  * @author ssk
- * @version 1.0
+ * @version 2.0
  * @date 2024/12/4
  */
 @Data
 @ApiModel("微信小程序登录请求")
 public class WeChatLoginDto {
 
-    @ApiModelProperty(value = "手机号凭证 code(通过 wx.getPhoneNumber() 获取,必填)", required = true)
-    @NotBlank(message = "phoneCode不能为空")
+    @ApiModelProperty(value = "微信登录凭证 code(通过 wx.login() 获取,必填)", required = true)
+    @NotBlank(message = "code不能为空,请通过 wx.login() 获取")
+    private String code;
+
+    @ApiModelProperty(value = "手机号凭证 code(通过 wx.getPhoneNumber() 获取,可选,用于绑定手机号)", required = false)
     private String phoneCode;
 
-    @ApiModelProperty(value = "店铺ID", required = false)
-    private Long storeId;
 }

+ 13 - 3
alien-dining/src/main/java/shop/alien/dining/service/DiningUserService.java

@@ -3,6 +3,7 @@ package shop.alien.dining.service;
 import shop.alien.dining.dto.ChangePhoneDto;
 import shop.alien.dining.dto.UserProfileUpdateDto;
 import shop.alien.dining.vo.DiningUserVo;
+import shop.alien.dining.vo.TokenVerifyVo;
 
 /**
  * 点餐用户服务接口
@@ -14,14 +15,15 @@ import shop.alien.dining.vo.DiningUserVo;
 public interface DiningUserService {
 
     /**
-     * 微信小程序登录(新方式:仅 phoneCode,无需 wx.login
+     * 微信小程序登录(标准流程:通过 code2session 获取 openid
      *
-     * @param phoneCode 手机号凭证(通过 wx.getPhoneNumber() 获取,必填)
+     * @param code      微信登录凭证(通过 wx.login() 获取,必填)
+     * @param phoneCode 手机号凭证(通过 wx.getPhoneNumber() 获取,可选)
      * @param storeId   店铺ID(可选)
      * @param macIp     客户端IP地址
      * @return 用户信息(包含 token)
      */
-    DiningUserVo wechatLogin(String phoneCode, Long storeId, String macIp);
+    DiningUserVo wechatLogin(String code, String phoneCode, String macIp);
 
     /**
      * 更新用户个人信息
@@ -46,4 +48,12 @@ public interface DiningUserService {
      * @return 更新后的用户信息,失败返回 null
      */
     DiningUserVo changePhone(ChangePhoneDto dto);
+
+    /**
+     * 校验 Token 是否有效
+     *
+     * @param token Token字符串
+     * @return Token校验结果,包含用户信息和验证状态
+     */
+    TokenVerifyVo verifyToken(String token);
 }

+ 9 - 11
alien-dining/src/main/java/shop/alien/dining/service/impl/CartServiceImpl.java

@@ -7,6 +7,8 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
+import shop.alien.dining.config.BaseRedisService;
+import shop.alien.dining.service.CartService;
 import shop.alien.entity.store.StoreCart;
 import shop.alien.entity.store.StoreCouponUsage;
 import shop.alien.entity.store.StoreCuisine;
@@ -18,9 +20,7 @@ import shop.alien.mapper.StoreCartMapper;
 import shop.alien.mapper.StoreCouponUsageMapper;
 import shop.alien.mapper.StoreCuisineMapper;
 import shop.alien.mapper.StoreTableMapper;
-import shop.alien.dining.config.BaseRedisService;
-import shop.alien.dining.service.CartService;
-import shop.alien.util.common.JwtUtil;
+import shop.alien.dining.util.TokenUtil;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
@@ -174,9 +174,8 @@ public class CartServiceImpl implements CartService {
         }
 
         // 获取当前用户信息
-        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-        Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
-        String userPhone = userInfo != null ? userInfo.getString("phone") : null;
+        Integer userId = TokenUtil.getCurrentUserId();
+        String userPhone = TokenUtil.getCurrentUserPhone();
 
         // 获取购物车
         CartDTO cart = getCart(dto.getTableId());
@@ -428,9 +427,9 @@ public class CartServiceImpl implements CartService {
             usage.setCouponId(couponId);
             usage.setUsageStatus(0); // 已标记使用
             usage.setCreatedTime(new Date());
-            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-            if (userInfo != null) {
-                usage.setCreatedUserId(userInfo.getInteger("userId"));
+            Integer userId = TokenUtil.getCurrentUserId();
+            if (userId != null) {
+                usage.setCreatedUserId(userId);
             }
             storeCouponUsageMapper.insert(usage);
         }
@@ -509,8 +508,7 @@ public class CartServiceImpl implements CartService {
 
             // 插入新的购物车记录
             if (cart.getItems() != null && !cart.getItems().isEmpty()) {
-                com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-                Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
+                Integer userId = TokenUtil.getCurrentUserId();
                 Date now = new Date();
 
                 for (CartItemDTO item : cart.getItems()) {

+ 355 - 56
alien-dining/src/main/java/shop/alien/dining/service/impl/DiningUserServiceImpl.java

@@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -15,23 +14,28 @@ import shop.alien.dining.dto.UserProfileUpdateDto;
 import shop.alien.dining.feign.AlienStoreFeign;
 import shop.alien.dining.service.DiningUserService;
 import shop.alien.dining.util.WeChatMiniProgramUtil;
+import shop.alien.dining.util.WeChatMiniProgramUtil.WeChatSessionInfo;
 import shop.alien.entity.result.R;
 import shop.alien.dining.vo.DiningUserVo;
+import shop.alien.dining.vo.TokenVerifyVo;
 import shop.alien.entity.store.LifeUser;
-import shop.alien.entity.store.vo.LifeUserVo;
 import shop.alien.mapper.LifeUserMapper;
 import shop.alien.util.common.JwtUtil;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.SignatureException;
 
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.ThreadLocalRandom;
 
 /**
- * 点餐用户服务实现类(新方式:仅 phoneCode,无需 wx.login)
+ * 点餐用户服务实现类
+ * 标准流程:通过 wx.login() 获取 code,调用 jscode2session 获取 openid
  *
  * @author ssk
- * @version 1.0
+ * @version 2.0
  * @date 2024/12/4
  */
 @Slf4j
@@ -47,79 +51,241 @@ public class DiningUserServiceImpl implements DiningUserService {
     @Value("${jwt.expiration-time}")
     private String effectiveTime;
 
+    /**
+     * Redis key 常量(小程序专用,避免与 APP 端冲突)
+     */
+    private static final String REDIS_KEY_OPENID_PREFIX = "wechat:openid:";
+    private static final String REDIS_KEY_TOKEN_PREFIX = "miniprogram_user_token:";
+    private static final String REDIS_KEY_USER_PHONE_PREFIX = "miniprogram_user_";
+    private static final long OPENID_MAPPING_EXPIRE_SECONDS = 30 * 24 * 60 * 60L; // 30天
+    private static final long TOKEN_EXPIRE_SECONDS = 7 * 24 * 60 * 60L; // 7天
+
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public DiningUserVo wechatLogin(String phoneCode, Long storeId, String macIp) {
-        // 1. 通过 phonenumber.getPhoneNumber 换取手机号(必填)
-        String phone = weChatMiniProgramUtil.getPhoneNumberByCode(phoneCode);
-        if (StringUtils.isBlank(phone)) {
-            log.warn("登录失败:无法通过 phoneCode 获取手机号(请先授权手机号,code 有效期5分钟且仅能使用一次)");
+    public DiningUserVo wechatLogin(String code, String phoneCode, String macIp) {
+        // 1. 通过 code2session 获取 openid 和 session_key
+        String openid = getOpenidFromCode(code);
+        if (StringUtils.isBlank(openid)) {
+            return null;
+        }
+
+        // 2. 查找或创建用户
+        LifeUser user = findOrCreateUser(openid, phoneCode);
+        if (user == null) {
+            return null;
+        }
+
+        // 3. 检查用户状态(提前检查,避免不必要的操作)
+        if (!isUserValid(user)) {
+            return null;
+        }
+
+        // 4. 生成并存储 token
+        String token = generateAndStoreToken(openid, user);
+
+        // 5. 构建返回对象
+        return buildDiningUserVo(user, token);
+    }
+
+    /**
+     * 通过 code 获取 openid
+     */
+    private String getOpenidFromCode(String code) {
+        WeChatSessionInfo sessionInfo = weChatMiniProgramUtil.code2Session(code);
+        if (sessionInfo == null || sessionInfo.getErrcode() != null) {
+            Integer errcode = sessionInfo != null ? sessionInfo.getErrcode() : null;
+            String errmsg = sessionInfo != null ? sessionInfo.getErrmsg() : "调用微信接口失败";
+            log.error("登录失败:code2session 失败, errcode={}, errmsg={}", errcode, errmsg);
+            return null;
+        }
+
+        String openid = sessionInfo.getOpenid();
+        if (StringUtils.isBlank(openid)) {
+            log.error("登录失败:无法获取 openid");
+            return null;
+        }
+        log.info("成功获取 openid: {}", maskString(openid, 8));
+        return openid;
+    }
+
+    /**
+     * 查找或创建用户
+     */
+    private LifeUser findOrCreateUser(String openid, String phoneCode) {
+        // 1. 通过 openid 查找用户(从 Redis 映射)
+        LifeUser user = findUserByOpenid(openid);
+        
+        // 2. 如果找不到,尝试通过手机号查找
+        String phone = null;
+        if (user == null && StringUtils.isNotBlank(phoneCode)) {
+            phone = weChatMiniProgramUtil.getPhoneNumberByCode(phoneCode);
+            if (StringUtils.isNotBlank(phone)) {
+                log.info("成功获取手机号: {}", maskString(phone, 7));
+                user = findUserByPhone(phone);
+                if (user != null) {
+                    // 建立 openid 和 userId 的映射关系
+                    saveOpenidMapping(openid, user.getId());
+                }
+            }
+        }
+
+        // 3. 用户不存在则创建
+        if (user == null) {
+            user = createNewUser(openid, phone);
+            if (user != null) {
+                saveOpenidMapping(openid, user.getId());
+            }
+        } else {
+            // 4. 如果用户存在但没有手机号,且提供了 phoneCode,则更新手机号
+            if (StringUtils.isBlank(user.getUserPhone()) && StringUtils.isNotBlank(phone)) {
+                updateUserPhone(user, phone);
+            }
+        }
+
+        return user;
+    }
+
+    /**
+     * 通过 openid 查找用户
+     */
+    private LifeUser findUserByOpenid(String openid) {
+        String openidKey = REDIS_KEY_OPENID_PREFIX + openid;
+        String userIdStr = baseRedisService.getString(openidKey);
+        
+        if (StringUtils.isBlank(userIdStr)) {
             return null;
         }
-        log.info("成功获取手机号: {}", phone.substring(0, Math.min(7, phone.length())) + "****");
 
-        // 2. 记录店铺ID(如果提供)
-        if (storeId != null) {
-            log.info("登录请求包含店铺ID: storeId={}", storeId);
+        try {
+            Integer userId = Integer.parseInt(userIdStr);
+            LifeUser user = lifeUserMapper.selectById(userId);
+            if (user != null) {
+                log.info("通过 openid 找到用户: openid={}, userId={}", maskString(openid, 8), userId);
+            }
+            return user;
+        } catch (NumberFormatException e) {
+            log.warn("Redis 中的 userId 格式错误: {}", userIdStr);
+            return null;
         }
+    }
 
-        // 3. 根据手机号查询用户
+    /**
+     * 通过手机号查找用户
+     */
+    private LifeUser findUserByPhone(String phone) {
         LambdaQueryWrapper<LifeUser> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(LifeUser::getUserPhone, phone);
         LifeUser user = lifeUserMapper.selectOne(queryWrapper);
+        if (user != null) {
+            log.info("通过手机号找到用户: phone={}, userId={}", maskString(phone, 7), user.getId());
+        }
+        return user;
+    }
 
-        // 4. 用户不存在则创建,并自动填默认/随机资料
-        if (user == null) {
-            user = new LifeUser();
+    /**
+     * 创建新用户
+     */
+    private LifeUser createNewUser(String openid, String phone) {
+        LifeUser user = new LifeUser();
+        if (StringUtils.isNotBlank(phone)) {
             user.setUserPhone(phone);
-            // 昵称:微信用户 + 6位随机数 + 时间戳后缀,避免重复
-            String nickSuffix = ThreadLocalRandom.current().nextInt(100000, 1000000)
-                    + "_" + (System.currentTimeMillis() % 10000);
-            user.setUserName("微信用户" + nickSuffix);
+            user.setUserName(phone);
             user.setRealName(phone);
-            // 性别:随机 "男" 或 "女",存 user_sex(varchar),默认男
-            user.setUserSex(ThreadLocalRandom.current().nextBoolean() ? "女" : "男");
-            // 生日:先不填,保持 null
-            user.setCreatedTime(new Date());
-            int ret = lifeUserMapper.insert(user);
-            if (ret != 1) {
-                log.error("创建用户失败");
-                return null;
-            }
-            queryWrapper = new LambdaQueryWrapper<>();
-            queryWrapper.eq(LifeUser::getUserPhone, phone);
-            user = lifeUserMapper.selectOne(queryWrapper);
-            log.info("创建新用户: phone={}, userId={}", phone, user.getId());
         } else {
-            log.info("用户已存在,直接登录: phone={}, userId={}", phone, user.getId());
+            // 没有手机号时,使用 openid 后8位作为临时用户名
+            String tempName = "微信用户" + openid.substring(Math.max(0, openid.length() - 8));
+            user.setUserName(tempName);
+            user.setRealName(tempName);
+        }
+        user.setCreatedTime(new Date());
+        
+        int ret = lifeUserMapper.insert(user);
+        if (ret != 1) {
+            log.error("创建用户失败");
+            return null;
+        }
+        
+        // MyBatis-Plus 的 insert 会自动填充 ID,不需要重新查询
+        log.info("创建新用户: openid={}, userId={}, phone={}", maskString(openid, 8), user.getId(), maskString(phone, 7));
+        return user;
+    }
+
+    /**
+     * 更新用户手机号
+     */
+    private void updateUserPhone(LifeUser user, String phone) {
+        user.setUserPhone(phone);
+        if (StringUtils.isBlank(user.getUserName()) || user.getUserName().startsWith("微信用户")) {
+            user.setUserName(phone);
         }
+        if (StringUtils.isBlank(user.getRealName()) || user.getRealName().startsWith("微信用户")) {
+            user.setRealName(phone);
+        }
+        user.setUpdatedTime(new Date());
+        lifeUserMapper.updateById(user);
+        log.info("更新用户手机号: userId={}, phone={}", user.getId(), maskString(phone, 7));
+    }
 
-        // 5. 检查用户状态
+    /**
+     * 保存 openid 和 userId 的映射关系
+     */
+    private void saveOpenidMapping(String openid, Integer userId) {
+        String openidKey = REDIS_KEY_OPENID_PREFIX + openid;
+        baseRedisService.setString(openidKey, userId.toString(), OPENID_MAPPING_EXPIRE_SECONDS);
+    }
+
+    /**
+     * 检查用户状态是否有效
+     */
+    private boolean isUserValid(LifeUser user) {
         if (user.getIsBanned() != null && user.getIsBanned() == 1) {
             log.warn("用户已被封禁: userId={}", user.getId());
-            return null;
+            return false;
         }
         if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
             log.warn("用户已注销: userId={}", user.getId());
-            return null;
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 生成并存储 token
+     */
+    private String generateAndStoreToken(String openid, LifeUser user) {
+        // 构建 token 信息
+        Map<String, String> tokenMap = buildTokenMap(openid, user);
+        String userName = StringUtils.isNotBlank(user.getUserName()) ? user.getUserName() : "用户";
+        String token = generateToken(openid, userName, tokenMap);
+
+        // 存入 Redis(使用 openid 作为 key)
+        baseRedisService.setString(REDIS_KEY_TOKEN_PREFIX + openid, token, TOKEN_EXPIRE_SECONDS);
+        
+        // 兼容旧版本:如果用户有手机号,也存储
+        if (StringUtils.isNotBlank(user.getUserPhone())) {
+            baseRedisService.setString(REDIS_KEY_USER_PHONE_PREFIX + user.getUserPhone(), token, TOKEN_EXPIRE_SECONDS);
         }
 
-        // 6. 生成 token(不含 openid,新方式不做 code2Session)
+        return token;
+    }
+
+    /**
+     * 构建 token 信息 Map
+     */
+    private Map<String, String> buildTokenMap(String openid, LifeUser user) {
         Map<String, String> tokenMap = new HashMap<>();
-        tokenMap.put("phone", user.getUserPhone());
-        tokenMap.put("userName", user.getUserName() != null ? user.getUserName() : "用户");
+        tokenMap.put("openid", openid);
+        tokenMap.put("phone", StringUtils.isNotBlank(user.getUserPhone()) ? user.getUserPhone() : "");
+        tokenMap.put("userName", StringUtils.isNotBlank(user.getUserName()) ? user.getUserName() : "用户");
         tokenMap.put("userId", user.getId().toString());
         tokenMap.put("userType", "user");
-        String token = generateToken(user.getUserPhone(), user.getUserName() != null ? user.getUserName() : "用户", tokenMap);
-
-        // 7. 存入 Redis
-        baseRedisService.setString("user_" + user.getUserPhone(), token);
-
-        // 8. 构建返回
-        LifeUserVo userVo = new LifeUserVo();
-        BeanUtils.copyProperties(user, userVo);
-        userVo.setToken(token);
+        return tokenMap;
+    }
 
+    /**
+     * 构建返回对象
+     */
+    private DiningUserVo buildDiningUserVo(LifeUser user, String token) {
         DiningUserVo diningUserVo = new DiningUserVo();
         diningUserVo.setId(user.getId().longValue());
         diningUserVo.setPhone(user.getUserPhone());
@@ -128,10 +294,19 @@ public class DiningUserServiceImpl implements DiningUserService {
         diningUserVo.setStatus(0);
         diningUserVo.setCreatedTime(user.getCreatedTime());
         diningUserVo.setToken(token);
-
         return diningUserVo;
     }
 
+    /**
+     * 掩码字符串(用于日志脱敏)
+     */
+    private String maskString(String str, int visibleLength) {
+        if (StringUtils.isBlank(str) || str.length() <= visibleLength) {
+            return "****";
+        }
+        return str.substring(0, Math.min(visibleLength, str.length())) + "****";
+    }
+
     private String generateToken(String userId, String userName, Map<String, String> tokenMap) {
         int effectiveTimeInt = Integer.parseInt(effectiveTime.substring(0, effectiveTime.length() - 1));
         String effectiveTimeUnit = effectiveTime.substring(effectiveTime.length() - 1);
@@ -249,7 +424,7 @@ public class DiningUserServiceImpl implements DiningUserService {
             log.warn("更换手机号失败:验证码格式错误, userId={}, newPhone={}", dto.getUserId(), newPhone);
             return null;
         }
-        R checkRes = alienStoreFeign.checkSmsCode(newPhone, 0, 3, codeInt);
+        R<?> checkRes = alienStoreFeign.checkSmsCode(newPhone, 0, 3, codeInt);
         if (!R.isSuccess(checkRes)) {
             log.warn("更换手机号失败:验证码错误或已过期, userId={}, newPhone={}", dto.getUserId(), newPhone);
             return null;
@@ -295,13 +470,137 @@ public class DiningUserServiceImpl implements DiningUserService {
             return null;
         }
 
-        // 6. 清除旧手机号对应 token,强制重新登录
-        baseRedisService.delete("user_" + oldPhone);
-        log.info("更换手机号成功, userId={}, oldPhone={}, newPhone={}", dto.getUserId(), oldPhone, newPhone);
+        // 6. 只清除小程序旧手机号对应的 token,APP 的 token 保持不变
+        // 因为小程序更换手机号只影响小程序平台,APP 可以继续使用旧手机号登录
+        baseRedisService.delete(REDIS_KEY_USER_PHONE_PREFIX + oldPhone);
+        log.info("更换手机号成功, userId={}, oldPhone={}, newPhone={}(仅清除小程序 token,APP token 保持不变)", dto.getUserId(), oldPhone, newPhone);
 
         return buildDiningUserVo(user);
     }
 
+    @Override
+    public TokenVerifyVo verifyToken(String token) {
+        TokenVerifyVo verifyVo = new TokenVerifyVo();
+        verifyVo.setValid(false);
+
+        // 1. 检查 token 是否为空
+        if (StringUtils.isBlank(token)) {
+            verifyVo.setReason("Token 不能为空");
+            return verifyVo;
+        }
+
+        // 去除 "Bearer " 前缀(不区分大小写)
+        token = token.trim();
+        if (token.length() > 7 && token.substring(0, 7).equalsIgnoreCase("Bearer ")) {
+            token = token.substring(7).trim();
+        }
+        
+        if (StringUtils.isBlank(token)) {
+            verifyVo.setReason("Token 去除前缀后为空");
+            return verifyVo;
+        }
+
+        try {
+            // 2. 解析 token,验证格式和签名
+            Claims claims = JwtUtil.parseJWT(token);
+            
+            // 3. 检查 token 是否过期
+            Date expiration = claims.getExpiration();
+            if (expiration != null && expiration.before(new Date())) {
+                verifyVo.setReason("Token 已过期");
+                verifyVo.setExpirationTime(expiration);
+                return verifyVo;
+            }
+
+            // 4. 从 token 中提取用户信息
+            String sub = claims.get("sub").toString();
+            JSONObject tokenInfo = JSONObject.parseObject(sub);
+            String openid = tokenInfo.getString("openid");
+            String phone = tokenInfo.getString("phone");
+            String userIdStr = tokenInfo.getString("userId");
+            String userName = tokenInfo.getString("userName");
+
+            // 5. 验证 Redis 中是否存在该 token(使用小程序专用的 key 前缀)
+            boolean tokenExists = false;
+            if (StringUtils.isNotBlank(openid)) {
+                // 优先使用 openid 查找
+                String redisToken = baseRedisService.getString(REDIS_KEY_TOKEN_PREFIX + openid);
+                if (token.equals(redisToken)) {
+                    tokenExists = true;
+                }
+            }
+            
+            // 兼容旧版本:通过手机号查找
+            if (!tokenExists && StringUtils.isNotBlank(phone)) {
+                String redisToken = baseRedisService.getString(REDIS_KEY_USER_PHONE_PREFIX + phone);
+                if (token.equals(redisToken)) {
+                    tokenExists = true;
+                }
+            }
+
+            if (!tokenExists) {
+                verifyVo.setReason("Token 不存在或已失效(可能已退出登录)");
+                return verifyVo;
+            }
+
+            // 6. 验证用户状态(如果提供了 userId)
+            if (StringUtils.isNotBlank(userIdStr)) {
+                try {
+                    Integer userId = Integer.parseInt(userIdStr);
+                    LifeUser user = lifeUserMapper.selectById(userId);
+                    if (user == null) {
+                        verifyVo.setReason("用户不存在");
+                        return verifyVo;
+                    }
+                    if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+                        verifyVo.setReason("用户已被封禁");
+                        return verifyVo;
+                    }
+                    if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+                        verifyVo.setReason("用户已注销");
+                        return verifyVo;
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("Token 中的 userId 格式错误: {}", userIdStr);
+                }
+            }
+
+            // 7. Token 验证通过,返回用户信息
+            verifyVo.setValid(true);
+            if (StringUtils.isNotBlank(userIdStr)) {
+                try {
+                    verifyVo.setUserId(Long.parseLong(userIdStr));
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析 userId: {}", userIdStr);
+                }
+            }
+            verifyVo.setPhone(phone);
+            verifyVo.setNickName(userName);
+            verifyVo.setOpenid(openid);
+            verifyVo.setExpirationTime(expiration);
+
+            log.info("Token 验证成功: userId={}, openid={}, phone={}", userIdStr, openid, phone);
+            return verifyVo;
+
+        } catch (ExpiredJwtException e) {
+            log.warn("Token 已过期: {}", e.getMessage());
+            verifyVo.setReason("Token 已过期: " + e.getMessage());
+            return verifyVo;
+        } catch (MalformedJwtException e) {
+            log.warn("Token 格式错误: {}", e.getMessage());
+            verifyVo.setReason("Token 格式错误: " + e.getMessage());
+            return verifyVo;
+        } catch (SignatureException e) {
+            log.warn("Token 签名验证失败: {}", e.getMessage());
+            verifyVo.setReason("Token 签名验证失败: " + e.getMessage());
+            return verifyVo;
+        } catch (Exception e) {
+            log.error("Token 验证异常: {}", e.getMessage(), e);
+            verifyVo.setReason("Token 验证异常: " + e.getMessage());
+            return verifyVo;
+        }
+    }
+
     /**
      * 构建 DiningUserVo
      */

+ 25 - 25
alien-dining/src/main/java/shop/alien/dining/service/impl/StoreOrderServiceImpl.java

@@ -9,13 +9,13 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
+import shop.alien.dining.service.CartService;
+import shop.alien.dining.service.StoreOrderService;
+import shop.alien.dining.util.TokenUtil;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.CartDTO;
 import shop.alien.entity.store.dto.CreateOrderDTO;
 import shop.alien.mapper.*;
-import shop.alien.dining.service.CartService;
-import shop.alien.dining.service.StoreOrderService;
-import shop.alien.util.common.JwtUtil;
 
 import java.math.BigDecimal;
 import java.text.SimpleDateFormat;
@@ -49,12 +49,11 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         log.info("创建订单, dto={}", dto);
 
         // 获取当前用户信息
-        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-        if (userInfo == null) {
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId == null) {
             throw new RuntimeException("用户未登录");
         }
-        Integer userId = userInfo.getInteger("userId");
-        String userPhone = userInfo.getString("phone");
+        String userPhone = TokenUtil.getCurrentUserPhone();
 
         // 验证桌号
         StoreTable table = storeTableMapper.selectById(dto.getTableId());
@@ -226,9 +225,9 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         order.setUpdatedTime(new Date());
 
         // 获取当前用户信息
-        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-        if (userInfo != null) {
-            order.setUpdatedUserId(userInfo.getInteger("userId"));
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            order.setUpdatedUserId(userId);
         }
 
         this.updateById(order);
@@ -273,9 +272,9 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         order.setUpdatedTime(new Date());
 
         // 获取当前用户信息
-        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-        if (userInfo != null) {
-            order.setUpdatedUserId(userInfo.getInteger("userId"));
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            order.setUpdatedUserId(userId);
         }
 
         this.updateById(order);
@@ -349,9 +348,8 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         }
 
         // 获取当前用户信息
-        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-        Integer userId = userInfo != null ? userInfo.getInteger("userId") : null;
-        String userPhone = userInfo != null ? userInfo.getString("phone") : null;
+        Integer userId = TokenUtil.getCurrentUserId();
+        String userPhone = TokenUtil.getCurrentUserPhone();
 
         // 查询是否已有该菜品
         LambdaQueryWrapper<StoreOrderDetail> detailWrapper = new LambdaQueryWrapper<>();
@@ -437,9 +435,10 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         order.setOrderStatus(3); // 已完成
         order.setUpdatedTime(new Date());
 
-        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-        if (userInfo != null) {
-            order.setUpdatedUserId(userInfo.getInteger("userId"));
+        // 从 token 获取用户信息
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            order.setUpdatedUserId(userId);
         }
 
         this.updateById(order);
@@ -541,9 +540,9 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
                     newUsage.setDiscountAmount(discountAmount);
                     newUsage.setUsageStatus(1); // 已下单
                     newUsage.setCreatedTime(new Date());
-                    com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-                    if (userInfo != null) {
-                        newUsage.setCreatedUserId(userInfo.getInteger("userId"));
+                    Integer userId = TokenUtil.getCurrentUserId();
+                    if (userId != null) {
+                        newUsage.setCreatedUserId(userId);
                     }
                     storeCouponUsageMapper.insert(newUsage);
                 }
@@ -560,9 +559,10 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
         order.setPayAmount(payAmount);
         order.setUpdatedTime(new Date());
 
-        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
-        if (userInfo != null) {
-            order.setUpdatedUserId(userInfo.getInteger("userId"));
+        // 从 token 获取用户信息
+        Integer userId = TokenUtil.getCurrentUserId();
+        if (userId != null) {
+            order.setUpdatedUserId(userId);
         }
 
         this.updateById(order);

+ 227 - 0
alien-dining/src/main/java/shop/alien/dining/util/TokenUtil.java

@@ -0,0 +1,227 @@
+package shop.alien.dining.util;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import shop.alien.util.common.JwtUtil;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Token 工具类
+ * 用于统一处理 token 的获取和解析
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Slf4j
+public class TokenUtil {
+
+    /**
+     * 去除 token 中的 "Bearer " 前缀
+     *
+     * @param token 原始 token 字符串
+     * @return 去除前缀后的 token
+     */
+    private static String removeBearerPrefix(String token) {
+        if (StringUtils.isBlank(token)) {
+            return token;
+        }
+        // 去除首尾空格
+        token = token.trim();
+        
+        // 去除 "Bearer " 前缀(不区分大小写)
+        if (token.length() > 7 && token.substring(0, 7).equalsIgnoreCase("Bearer ")) {
+            token = token.substring(7).trim();
+        }
+        
+        return token;
+    }
+
+    /**
+     * 从请求头获取当前用户信息
+     * 兼容新的 token 格式(包含 openid)和旧格式
+     *
+     * @return 用户信息 JSONObject,包含 userId、openid、phone、userName 等,如果获取失败返回 null
+     */
+    public static JSONObject getCurrentUserInfo() {
+        try {
+            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+            if (attributes == null) {
+                return null;
+            }
+
+            HttpServletRequest request = attributes.getRequest();
+            String token = request.getHeader("Authorization");
+            
+            if (StringUtils.isBlank(token)) {
+                return null;
+            }
+
+            // 去除 "Bearer " 前缀
+            token = removeBearerPrefix(token);
+            
+            if (StringUtils.isBlank(token)) {
+                log.warn("Token 去除前缀后为空");
+                return null;
+            }
+
+            // 解析 token,验证格式和签名(与 verifyToken 方法保持一致)
+            io.jsonwebtoken.Claims claims = JwtUtil.parseJWT(token);
+            
+            // 从 token 中提取用户信息(与 verifyToken 方法保持一致)
+            String sub = claims.get("sub").toString();
+            JSONObject userInfo = JSONObject.parseObject(sub);
+            
+            return userInfo;
+        } catch (io.jsonwebtoken.ExpiredJwtException e) {
+            log.warn("Token 已过期: {}", e.getMessage());
+            return null;
+        } catch (io.jsonwebtoken.MalformedJwtException e) {
+            log.error("Token 格式错误: {}", e.getMessage());
+            return null;
+        } catch (io.jsonwebtoken.SignatureException e) {
+            log.error("Token 签名验证失败: {}", e.getMessage());
+            return null;
+        } catch (Exception e) {
+            log.warn("获取当前用户信息失败: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 获取当前用户ID
+     *
+     * @return 用户ID,如果获取失败返回 null
+     */
+    public static Integer getCurrentUserId() {
+        JSONObject userInfo = getCurrentUserInfo();
+        if (userInfo == null) {
+            log.debug("getCurrentUserInfo() 返回 null,无法获取 userId");
+            return null;
+        }
+        
+        try {
+            String userIdStr = userInfo.getString("userId");
+            if (StringUtils.isBlank(userIdStr)) {
+                log.warn("Token 中的 userId 字段为空或不存在,userInfo keys: {}", userInfo.keySet());
+                return null;
+            }
+            Integer userId = Integer.parseInt(userIdStr);
+            log.debug("成功获取 userId: {}", userId);
+            return userId;
+        } catch (NumberFormatException e) {
+            log.error("解析 userId 失败,userIdStr: {}, 错误: {}", userInfo.getString("userId"), e.getMessage());
+        } catch (Exception e) {
+            log.error("获取 userId 时发生异常: {}", e.getMessage(), e);
+        }
+        
+        return null;
+    }
+
+    /**
+     * 获取当前用户的 openid
+     *
+     * @return openid,如果获取失败返回 null
+     */
+    public static String getCurrentUserOpenid() {
+        JSONObject userInfo = getCurrentUserInfo();
+        if (userInfo == null) {
+            return null;
+        }
+        return userInfo.getString("openid");
+    }
+
+    /**
+     * 获取当前用户的手机号
+     *
+     * @return 手机号,如果获取失败返回 null
+     */
+    public static String getCurrentUserPhone() {
+        JSONObject userInfo = getCurrentUserInfo();
+        if (userInfo == null) {
+            return null;
+        }
+        return userInfo.getString("phone");
+    }
+
+    /**
+     * 检查是否有有效的 token
+     *
+     * @return 如果有有效的 token 返回 true,否则返回 false
+     */
+    public static boolean hasValidToken() {
+        JSONObject userInfo = getCurrentUserInfo();
+        return userInfo != null && userInfo.containsKey("userId");
+    }
+
+    /**
+     * 从 token 字符串直接解析用户信息(用于调试)
+     *
+     * @param token Token 字符串
+     * @return 用户信息 JSONObject,如果解析失败返回 null
+     */
+    public static JSONObject parseToken(String token) {
+        if (StringUtils.isBlank(token)) {
+            log.warn("Token 字符串为空");
+            return null;
+        }
+
+        try {
+            // 去除 "Bearer " 前缀
+            token = removeBearerPrefix(token);
+            
+            if (StringUtils.isBlank(token)) {
+                log.warn("Token 去除前缀后为空");
+                return null;
+            }
+            
+            log.info("开始解析 token: {}", token.length() > 30 ? token.substring(0, 30) + "..." : token);
+
+            // 解析 token
+            io.jsonwebtoken.Claims claims = JwtUtil.parseJWT(token);
+            if (claims == null) {
+                log.warn("Token 解析失败,claims 为 null");
+                return null;
+            }
+
+            log.info("Token claims keys: {}", claims.keySet());
+
+            // 从 token 中提取用户信息
+            Object subObj = claims.get("sub");
+            if (subObj == null) {
+                log.warn("Token claims 中没有 'sub' 字段");
+                return null;
+            }
+
+            String sub;
+            if (subObj instanceof String) {
+                sub = (String) subObj;
+            } else {
+                sub = subObj.toString();
+            }
+
+            log.info("Token sub 内容: {}", sub);
+
+            JSONObject userInfo = JSONObject.parseObject(sub);
+            if (userInfo == null) {
+                log.warn("解析 sub 为 JSONObject 返回 null");
+                return null;
+            }
+
+            log.info("成功解析用户信息: userId={}, openid={}, phone={}, 所有 keys: {}",
+                    userInfo.getString("userId"),
+                    userInfo.getString("openid"),
+                    userInfo.getString("phone"),
+                    userInfo.keySet());
+
+            return userInfo;
+        } catch (Exception e) {
+            log.error("解析 token 失败: {}, 异常类型: {}", e.getMessage(), e.getClass().getName(), e);
+            return null;
+        }
+    }
+}

+ 34 - 4
alien-dining/src/main/java/shop/alien/dining/util/WeChatMiniProgramUtil.java

@@ -1,6 +1,7 @@
 package shop.alien.dining.util;
 
 import com.alibaba.fastjson.JSONObject;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.codec.binary.Base64;
 import org.apache.http.HttpResponse;
@@ -12,7 +13,7 @@ import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
-import shop.alien.util.encryption.StandardAesUtil;
+import shop.alien.dining.config.BaseRedisService;
 
 import javax.crypto.Cipher;
 import javax.crypto.spec.IvParameterSpec;
@@ -30,6 +31,7 @@ import java.nio.charset.StandardCharsets;
  */
 @Slf4j
 @Component
+@RequiredArgsConstructor
 public class WeChatMiniProgramUtil {
 
     @Value("${wechat.miniprogram.appId}")
@@ -38,6 +40,18 @@ public class WeChatMiniProgramUtil {
     @Value("${wechat.miniprogram.appSecret}")
     private String appSecret;
 
+    private final BaseRedisService baseRedisService;
+
+    /**
+     * access_token 缓存键前缀
+     */
+    private static final String ACCESS_TOKEN_CACHE_KEY = "wechat:access_token:";
+    
+    /**
+     * access_token 有效期(秒),微信官方为7200秒,这里设置为7000秒,提前刷新
+     */
+    private static final long ACCESS_TOKEN_EXPIRE_TIME = 7000L;
+
     /**
      * 微信小程序登录接口地址
      */
@@ -261,10 +275,21 @@ public class WeChatMiniProgramUtil {
     }
 
     /**
-     * 获取微信 access_token
-     * TODO: 应该实现token缓存机制,避免频繁请求(access_token有效期为7200秒)
+     * 获取微信 access_token(带缓存机制)
+     * access_token有效期为7200秒,这里缓存7000秒,提前刷新避免过期
+     * 
+     * @return access_token,获取失败返回null
      */
     private String getAccessToken() {
+        // 1. 先从缓存获取
+        String cacheKey = ACCESS_TOKEN_CACHE_KEY + appId;
+        String cachedToken = baseRedisService.getString(cacheKey);
+        if (cachedToken != null && !cachedToken.isEmpty()) {
+            log.debug("从缓存获取access_token成功");
+            return cachedToken;
+        }
+
+        // 2. 缓存未命中,请求微信接口
         try {
             String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
                     appId, appSecret);
@@ -274,10 +299,15 @@ public class WeChatMiniProgramUtil {
             HttpResponse response = httpClient.execute(httpGet);
             String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
             
+            log.info("微信获取access_token响应: {}", responseBody);
+            
             JSONObject jsonObject = JSONObject.parseObject(responseBody);
             if (jsonObject.containsKey("access_token")) {
                 String accessToken = jsonObject.getString("access_token");
-                log.debug("成功获取access_token");
+                
+                // 3. 存入缓存(有效期7000秒,提前刷新)
+                baseRedisService.setString(cacheKey, accessToken, ACCESS_TOKEN_EXPIRE_TIME);
+                log.info("成功获取access_token并存入缓存");
                 return accessToken;
             } else {
                 Integer errcode = jsonObject.getInteger("errcode");

+ 40 - 0
alien-dining/src/main/java/shop/alien/dining/vo/TokenVerifyVo.java

@@ -0,0 +1,40 @@
+package shop.alien.dining.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * Token 校验结果VO
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2024/12/4
+ */
+@Data
+@ApiModel("Token校验结果")
+public class TokenVerifyVo {
+
+    @ApiModelProperty(value = "Token是否有效")
+    private Boolean valid;
+
+    @ApiModelProperty(value = "用户ID")
+    private Long userId;
+
+    @ApiModelProperty(value = "用户手机号")
+    private String phone;
+
+    @ApiModelProperty(value = "用户昵称")
+    private String nickName;
+
+    @ApiModelProperty(value = "微信OpenID")
+    private String openid;
+
+    @ApiModelProperty(value = "Token过期时间")
+    private Date expirationTime;
+
+    @ApiModelProperty(value = "验证失败原因")
+    private String reason;
+}

+ 1 - 1
alien-store/src/main/resources/bootstrap.yml

@@ -1,3 +1,3 @@
 spring:
   profiles:
-    active: dev
+    active: test