12 次代碼提交 6fea128ac2 ... 9f268792a0

作者 SHA1 備註 提交日期
  skyline 9f268792a0 fix: 删除 C 端全量用户列表接口,杜绝 PII 泄露 3 周之前
  skyline f1cecc2a97 fix: 操作日志列表不再匿名可查,清空日志需要独立权限 3 周之前
  skyline 3d2db49f3e fix: 模板消息发送改为真异步,退款成功模板注册修正 3 周之前
  skyline e731cd1a18 fix: ID生成器改为进程内单例,多实例支持配置 workerId 3 周之前
  skyline b12e34d1bd fix: 车辆设置默认校验归属,防止越权修改他人车辆 3 周之前
  skyline b4da08cb9e fix: C端钱包流水查询强制按登录用户过滤,修复 IDOR 3 周之前
  skyline f977e4aab4 fix: 创建设备订单增加占用互斥,他人使用中的设备拒绝开单 3 周之前
  skyline da782d8fe2 fix: 订单更新事件防乱序覆盖,事件 topic 解析增加长度校验 3 周之前
  skyline 04db0cda27 fix: 停车券核销改为原子条件更新,测试接口仅限开发环境 3 周之前
  skyline 582a0fd7a3 fix: 修改管理员用户不再支持通过接口覆盖密码 3 周之前
  skyline 74d6f18815 fix: getParkingDiscounts 断言写反导致功能完全不可用 3 周之前
  skyline d753ace34c fix: 订单详情强制按登录用户过滤,修复 IDOR 越权 3 周之前
共有 20 個文件被更改,包括 164 次插入49 次删除
  1. 2 2
      car-wash-admin/src/main/java/com/kym/admin/controller/SystemLogController.java
  2. 10 5
      car-wash-common/src/main/java/com/kym/common/config/CustomIdGenerator.java
  3. 8 2
      car-wash-common/src/main/java/com/kym/common/utils/IDGenerator.java
  4. 41 0
      car-wash-entity/src/main/resources/sql/v22_add_system_log_clear_permission.sql
  5. 2 1
      car-wash-miniapp/src/main/java/com/kym/miniapp/controller/AccountController.java
  6. 29 2
      car-wash-miniapp/src/main/java/com/kym/miniapp/controller/ParkingCouponController.java
  7. 0 12
      car-wash-miniapp/src/main/java/com/kym/miniapp/controller/UserController.java
  8. 0 1
      car-wash-service/src/main/java/com/kym/service/UserService.java
  9. 5 0
      car-wash-service/src/main/java/com/kym/service/WalletDetailService.java
  10. 4 0
      car-wash-service/src/main/java/com/kym/service/awoara/event/handle/BootEventHandler.java
  11. 4 0
      car-wash-service/src/main/java/com/kym/service/awoara/event/handle/DeviceStateEventHandler.java
  12. 4 0
      car-wash-service/src/main/java/com/kym/service/awoara/event/handle/OrderCreateEventHandler.java
  13. 8 0
      car-wash-service/src/main/java/com/kym/service/awoara/event/handle/OrderUpdateEventHandler.java
  14. 3 0
      car-wash-service/src/main/java/com/kym/service/impl/AdminUserServiceImpl.java
  15. 2 1
      car-wash-service/src/main/java/com/kym/service/impl/CarsServiceImpl.java
  16. 14 4
      car-wash-service/src/main/java/com/kym/service/impl/MpMsgTemplateServiceImpl.java
  17. 0 11
      car-wash-service/src/main/java/com/kym/service/impl/UserServiceImpl.java
  18. 10 0
      car-wash-service/src/main/java/com/kym/service/impl/WalletDetailServiceImpl.java
  19. 16 7
      car-wash-service/src/main/java/com/kym/service/impl/WashOrderServiceImpl.java
  20. 2 1
      car-wash-service/src/main/java/com/kym/service/wechat/TemplateParamBuilderConfig.java

+ 2 - 2
car-wash-admin/src/main/java/com/kym/admin/controller/SystemLogController.java

@@ -1,10 +1,10 @@
 package com.kym.admin.controller;
 
+import cn.dev33.satoken.annotation.SaCheckPermission;
 import com.kym.common.R;
 import com.kym.common.annotation.SysLog;
 import com.kym.entity.queryParams.SystemLogQueryParam;
 import com.kym.service.SystemLogService;
-import cn.dev33.satoken.annotation.SaIgnore;
 import org.springframework.web.bind.annotation.*;
 
 /**
@@ -26,12 +26,12 @@ public class SystemLogController {
     }
 
     @SysLog("查询操作日志列表")
-    @SaIgnore
     @GetMapping("/list")
     R<?> list(SystemLogQueryParam params) {
         return R.success(systemLogService.listSystemLog(params));
     }
 
+    @SaCheckPermission("systemLog.clear")
     @SysLog("清空操作日志")
     @PostMapping("/clear")
     R<?> clear() {

+ 10 - 5
car-wash-common/src/main/java/com/kym/common/config/CustomIdGenerator.java

@@ -2,6 +2,7 @@ package com.kym.common.config;
 
 import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
 import com.kym.common.utils.IDGenerator;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
 /**
@@ -12,15 +13,19 @@ import org.springframework.stereotype.Component;
 @Component
 public class CustomIdGenerator implements IdentifierGenerator {
 
-    private IDGenerator idGenerator;
+    private final IDGenerator idGenerator;
 
-    public CustomIdGenerator() {
-        this.idGenerator = new IDGenerator(0, 0);
+    /**
+     * workerId/datacenterId 支持配置(默认 0),多实例部署时通过
+     * kym.id.worker-id / kym.id.datacenter-id 区分,避免跨进程同毫秒撞号
+     */
+    public CustomIdGenerator(@Value("${kym.id.worker-id:0}") long workerId,
+                             @Value("${kym.id.datacenter-id:0}") long datacenterId) {
+        this.idGenerator = new IDGenerator(workerId, datacenterId);
     }
 
     @Override
     public Long nextId(Object entity) {
-        final long id = idGenerator.nextId();
-        return id;
+        return idGenerator.nextId();
     }
 }

+ 8 - 2
car-wash-common/src/main/java/com/kym/common/utils/IDGenerator.java

@@ -17,8 +17,14 @@ import org.springframework.stereotype.Component;
 @Component
 public class IDGenerator {
 
-    public static IDGenerator INS(){
-        return new IDGenerator();
+    /**
+     * 进程内单例:原实现每次 new 一个新实例(sequence/workerId 均从 0 开始),
+     * 同一毫秒内两次调用会生成完全相同的 ID,导致主键冲突
+     */
+    private static final IDGenerator INSTANCE = new IDGenerator(0, 0);
+
+    public static IDGenerator INS() {
+        return INSTANCE;
     }
 
 

+ 41 - 0
car-wash-entity/src/main/resources/sql/v22_add_system_log_clear_permission.sql

@@ -0,0 +1,41 @@
+-- ====================================================
+-- v22: 新增 systemLog.clear 权限(清空操作日志)
+-- 背景:SystemLogController.clear 将增加
+--       @SaCheckPermission("systemLog.clear") 注解,
+--       需要先补齐权限数据,避免超管被 403 拦截。
+-- ====================================================
+
+DROP PROCEDURE IF EXISTS add_system_log_clear_permission;
+DELIMITER //
+CREATE PROCEDURE add_system_log_clear_permission()
+BEGIN
+    IF NOT EXISTS (
+        SELECT 1 FROM information_schema.TABLES
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_permission'
+    ) THEN
+        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 't_permission 表不存在,请先执行 init.sql';
+    END IF;
+
+    -- 1. 新增权限记录(幂等)
+    IF NOT EXISTS (
+        SELECT 1 FROM t_permission WHERE value = 'systemLog.clear'
+    ) THEN
+        INSERT INTO `t_permission` (`id`, `company_id`, `name`, `value`, `pid`, `weight`)
+        VALUES (94, NULL, '清空操作日志', 'systemLog.clear', 0, 150);
+    END IF;
+
+    -- 2. 超管角色(id=1)补齐该权限(管道分隔权限字符串,幂等)
+    UPDATE `t_role`
+    SET `permissions` = CONCAT(IFNULL(`permissions`, ''), '|systemLog.clear')
+    WHERE `id` = 1
+      AND FIND_IN_SET('systemLog.clear', REPLACE(IFNULL(`permissions`, ''), '|', ',')) = 0;
+END //
+DELIMITER ;
+CALL add_system_log_clear_permission();
+DROP PROCEDURE IF EXISTS add_system_log_clear_permission;
+
+-- ====================================================
+-- 验证:
+--   SELECT * FROM t_permission WHERE value = 'systemLog.clear';
+--   SELECT permissions FROM t_role WHERE id = 1;
+-- ====================================================

+ 2 - 1
car-wash-miniapp/src/main/java/com/kym/miniapp/controller/AccountController.java

@@ -64,7 +64,8 @@ public class AccountController {
     @GetMapping("/walletDetail")
     public R<?> walletDetailList(@RequestParam(name = "type", defaultValue = "0", required = false) int type,
                                  @RequestParam(name = "orderNo", required = false) String orderNo) {
-        return R.success(walletDetailService.getWalletDetailByOrderNo(orderNo, type));
+        // 强制按登录用户过滤,防止通过订单号越权查看他人资金流水
+        return R.success(walletDetailService.getWalletDetailByOrderNoForUser(orderNo, type, StpUtil.getLoginIdAsLong()));
     }
 
     /**

+ 29 - 2
car-wash-miniapp/src/main/java/com/kym/miniapp/controller/ParkingCouponController.java

@@ -1,6 +1,7 @@
 package com.kym.miniapp.controller;
 
 import cn.dev33.satoken.annotation.SaIgnore;
+import com.kym.common.exception.BusinessException;
 import com.kym.common.utils.HttpUtil;
 import com.kym.entity.ParkingCouponRecord;
 import com.kym.entity.User;
@@ -14,11 +15,13 @@ import com.kym.common.R;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.Environment;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
 import java.net.URL;
 import java.time.LocalDateTime;
+import java.util.Arrays;
 import java.util.UUID;
 
 /**
@@ -37,6 +40,7 @@ public class ParkingCouponController {
     private final MpMsgTemplateService mpMsgTemplateService;
     private final UserService userService;
     private final ParkingCouponRecordService parkingCouponRecordService;
+    private final Environment environment;
 
     @Value("${kym.domain}")
     private String DOMAIN;
@@ -44,11 +48,13 @@ public class ParkingCouponController {
     public ParkingCouponController(WashOrderService washOrderService,
                                    MpMsgTemplateService mpMsgTemplateService,
                                    UserService userService,
-                                   ParkingCouponRecordService parkingCouponRecordService) {
+                                   ParkingCouponRecordService parkingCouponRecordService,
+                                   Environment environment) {
         this.washOrderService = washOrderService;
         this.mpMsgTemplateService = mpMsgTemplateService;
         this.userService = userService;
         this.parkingCouponRecordService = parkingCouponRecordService;
+        this.environment = environment;
     }
 
     /**
@@ -74,11 +80,16 @@ public class ParkingCouponController {
             return;
         }
 
-        parkingCouponRecordService.lambdaUpdate()
+        // 原子核销:仅当状态为未使用时置为已使用,重复点击/并发点击不会重复核销
+        boolean consumed = parkingCouponRecordService.lambdaUpdate()
                 .eq(ParkingCouponRecord::getCode, code)
+                .eq(ParkingCouponRecord::getStatus, ParkingCouponRecord.STATUS_未使用)
                 .set(ParkingCouponRecord::getStatus, ParkingCouponRecord.STATUS_已使用)
                 .set(ParkingCouponRecord::getUsedTime, LocalDateTime.now())
                 .update();
+        if (!consumed) {
+            log.info("停车券 {} 已核销或不存在,跳过状态更新", code);
+        }
 
         if (url.length() < MAX_REDIRECT_URL_LENGTH) {
             log.info("短 URL 直接重定向, code: {}, urlLength: {}", code, url.length());
@@ -119,19 +130,23 @@ public class ParkingCouponController {
 
     /**
      * [测试] 生成停车券短链接,返回 url 可直接在浏览器打开测试
+     * 仅限开发环境(dev/test/local profile)注册,生产环境不可用
      */
     @SaIgnore
     @GetMapping("/testLink")
     public R<String> testLink(@RequestParam String mobilePhone) {
+        assertDevEnv();
         return R.success(washOrderService.checkParkingCoupon(mobilePhone));
     }
 
     /**
      * [测试] 发送停车券微信模板消息,需要用户已关注公众号且有 mpOpenid
+     * 仅限开发环境(dev/test/local profile)注册,生产环境不可用
      */
     @SaIgnore
     @GetMapping("/testSend")
     public String testSend(@RequestParam String mobilePhone) {
+        assertDevEnv();
         var user = userService.lambdaQuery().eq(User::getMobilePhone, mobilePhone).one();
         if (user == null) {
             return "用户不存在: " + mobilePhone;
@@ -153,6 +168,18 @@ public class ParkingCouponController {
         return "已发送, url=" + url;
     }
 
+    /**
+     * 测试接口仅限开发环境:通过 active profile 判断(dev/test/local)
+     */
+    private void assertDevEnv() {
+        var profiles = environment.getActiveProfiles();
+        boolean dev = profiles != null && Arrays.stream(profiles)
+                .anyMatch(p -> "dev".equals(p) || "test".equals(p) || "local".equals(p));
+        if (!dev) {
+            throw new BusinessException("测试接口仅限开发环境使用");
+        }
+    }
+
     private static String extractOrigin(String url) {
         try {
             URL u = new URL(url);

+ 0 - 12
car-wash-miniapp/src/main/java/com/kym/miniapp/controller/UserController.java

@@ -68,18 +68,6 @@ public class UserController {
         return R.success(userService.getMe());
     }
 
-    /**
-     * 用户列表
-     *
-     * @param pageNum
-     * @param pageSize
-     * @return
-     */
-    @GetMapping("listUser")
-    PageBean<UserVo> listUser(@RequestParam int pageNum, @RequestParam int pageSize) {
-        return userService.listUserVo(pageNum, pageSize);
-    }
-
     /**
      * 更新用户信息
      *

+ 0 - 1
car-wash-service/src/main/java/com/kym/service/UserService.java

@@ -26,7 +26,6 @@ public interface UserService extends MPJBaseService<User> {
 
     void updateUser(UserVo userVo);
 
-    PageBean<UserVo> listUserVo(Integer pageNum, Integer pageSize);
 
     R<?> wxLogin(WxLoginParams params);
 

+ 5 - 0
car-wash-service/src/main/java/com/kym/service/WalletDetailService.java

@@ -20,6 +20,11 @@ public interface WalletDetailService extends MPJBaseService<WalletDetail> {
 
     WalletDetail getWalletDetailByOrderNo(String orderNo, Integer status);
 
+    /**
+     * C 端查询钱包流水:强制按用户过滤
+     */
+    WalletDetail getWalletDetailByOrderNoForUser(String orderNo, Integer type, Long userId);
+
     List<WalletDetail> listWalletDetail(int type);
 
     PageBean<WalletDetailVo> listAdminWalletDetail(WalletDetailQueryParam params);

+ 4 - 0
car-wash-service/src/main/java/com/kym/service/awoara/event/handle/BootEventHandler.java

@@ -39,6 +39,10 @@ public class BootEventHandler implements AwoaraEventHandler<DeviceStateObject> {
         log.info("BootEventHandler: {}", message);
 
         var topic = message.getTopic().split("/");
+        if (topic.length < 3) {
+            log.warn("设备启动事件 topic 格式异常,跳过处理:{}", message.getTopic());
+            return;
+        }
         var productKey = topic[1];
         var deviceName = topic[2];
 

+ 4 - 0
car-wash-service/src/main/java/com/kym/service/awoara/event/handle/DeviceStateEventHandler.java

@@ -32,6 +32,10 @@ public class DeviceStateEventHandler implements AwoaraEventHandler<DeviceStateOb
         log.debug("DeviceStateEventHandler: {}", message);
 
         var topic = message.getTopic().split("/");
+        if (topic.length < 3) {
+            log.warn("设备状态事件 topic 格式异常,跳过处理:{}", message.getTopic());
+            return;
+        }
         var productKey = topic[1];
         var deviceName = topic[2];
         var deviceState = message.getPayload().getData().getDevice_state();

+ 4 - 0
car-wash-service/src/main/java/com/kym/service/awoara/event/handle/OrderCreateEventHandler.java

@@ -38,6 +38,10 @@ public class OrderCreateEventHandler implements AwoaraEventHandler<OrderInfoObje
         // 获取设备信息 从topic中获取
         log.debug("topic:{}", message.getTopic());
         var topic = message.getTopic().split("/");
+        if (topic.length < 3) {
+            log.warn("订单创建事件 topic 格式异常,跳过处理:{}", message.getTopic());
+            return;
+        }
         var productKey = topic[1];
         var deviceName = topic[2];
         log.debug("productKey:{},deviceName:{}", productKey, deviceName);

+ 8 - 0
car-wash-service/src/main/java/com/kym/service/awoara/event/handle/OrderUpdateEventHandler.java

@@ -40,12 +40,18 @@ public class OrderUpdateEventHandler implements AwoaraEventHandler<OrderInfoObje
         log.info("收到订单更新事件");
 
         var topic = message.getTopic().split("/");
+        if (topic.length < 3) {
+            log.warn("订单更新事件 topic 格式异常,跳过处理:{}", message.getTopic());
+            return;
+        }
         var productKey = topic[1];
         var deviceName = topic[2];
 
         var orderInfo = message.getPayload().getData().getOrder_info();
         log.info("订单:{},更新信息:{}", orderInfo.getOrder_id(), orderInfo);
 
+        // 防乱序覆盖:设备上报金额低于当前已记录金额时不更新(消费金额随时间只增不减),
+        // 与 javadoc 承诺的"消费金额大于等于当前记录才保存"保持一致
         washOrderService.lambdaUpdate()
                 .set(WashOrder::getOpenType, orderInfo.getOpen_type())
                 .set(WashOrder::getCloseType, orderInfo.getClose_type())
@@ -61,6 +67,8 @@ public class OrderUpdateEventHandler implements AwoaraEventHandler<OrderInfoObje
                 .eq(WashOrder::getOrderId, orderInfo.getOrder_id())
                 .eq(WashOrder::getProductKey, productKey)
                 .eq(WashOrder::getDeviceName, deviceName)
+                .and(w -> w.le(WashOrder::getAmount, orderInfo.getAmount())
+                        .or().isNull(WashOrder::getAmount))
                 .update();
 
     }

+ 3 - 0
car-wash-service/src/main/java/com/kym/service/impl/AdminUserServiceImpl.java

@@ -205,6 +205,9 @@ public class AdminUserServiceImpl extends MyBaseServiceImpl<AdminUserMapper, Adm
             investorInfoService.lambdaUpdate().set(InvestorInfo::getStatus, InvestorInfo.STATUS_有效).eq(InvestorInfo::getAdminUserId, adminUserVo.getId()).update();
         }
         BeanUtil.copyProperties(adminUserVo, adminUser);
+        // 修改用户不支持通过接口覆盖密码:防止请求体携带 password 明文重置他人密码
+        // (MyBatis-Plus 默认不更新 null 字段,password 保持原值;改密应走独立流程)
+        adminUser.setPassword(null);
         baseMapper.updateById(adminUser);
     }
 

+ 2 - 1
car-wash-service/src/main/java/com/kym/service/impl/CarsServiceImpl.java

@@ -73,7 +73,8 @@ public class CarsServiceImpl extends MPJBaseServiceImpl<CarsMapper, Cars> implem
                 .eq(Cars::getUserId, userId)
                 .set(Cars::getIsDefault, false)
                 .update();
-        var car = lambdaQuery().eq(Cars::getId, carId).one();
+        // 车辆归属校验:carId 必须属于当前用户,防止越权修改他人车辆的默认状态
+        var car = lambdaQuery().eq(Cars::getId, carId).eq(Cars::getUserId, userId).one();
         if (car != null) {
             car.setIsDefault(true);
             updateById(car);

+ 14 - 4
car-wash-service/src/main/java/com/kym/service/impl/MpMsgTemplateServiceImpl.java

@@ -11,6 +11,7 @@ import com.kym.mapper.MpMsgTemplateMapper;
 import com.kym.service.MpMsgTemplateService;
 import com.kym.service.MpRelationService;
 import com.kym.service.wechat.TemplateParamBuilder;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
@@ -30,9 +31,18 @@ public class MpMsgTemplateServiceImpl extends ServiceImpl<MpMsgTemplateMapper, M
 
     private final MpRelationService mpRelationService;
 
-    public MpMsgTemplateServiceImpl(Map<MsgTemplateType, TemplateParamBuilder> paramBuilders, MpRelationService mpRelationService) {
+    /**
+     * 自身代理:sendTemplateMessage 标有 @Async,同类自调用会使代理失效
+     * (微信 HTTP 调用将同步执行在调用方事务内),必须通过代理调用
+     */
+    private final MpMsgTemplateService self;
+
+    public MpMsgTemplateServiceImpl(Map<MsgTemplateType, TemplateParamBuilder> paramBuilders,
+                                    MpRelationService mpRelationService,
+                                    @Lazy MpMsgTemplateService self) {
         this.paramBuilders = paramBuilders;
         this.mpRelationService = mpRelationService;
+        this.self = self;
     }
 
 
@@ -44,7 +54,7 @@ public class MpMsgTemplateServiceImpl extends ServiceImpl<MpMsgTemplateMapper, M
      */
     @Override
     public void sendPaymentSuccessMsg(PayLog payLog, Integer balance) {
-        sendTemplateMessage(MsgTemplateType.PAYMENT_SUCCESS, payLog.getUserId(), payLog, balance);
+        self.sendTemplateMessage(MsgTemplateType.PAYMENT_SUCCESS, payLog.getUserId(), payLog, balance);
     }
 
     /**
@@ -55,7 +65,7 @@ public class MpMsgTemplateServiceImpl extends ServiceImpl<MpMsgTemplateMapper, M
      */
     @Override
     public void sendOrderCompletedMsg(WashOrder washOrder, Integer balance) {
-        sendTemplateMessage(MsgTemplateType.ORDER_COMPLETED, washOrder.getUserId(), washOrder, balance);
+        self.sendTemplateMessage(MsgTemplateType.ORDER_COMPLETED, washOrder.getUserId(), washOrder, balance);
     }
 
     /**
@@ -65,7 +75,7 @@ public class MpMsgTemplateServiceImpl extends ServiceImpl<MpMsgTemplateMapper, M
      */
     @Override
     public void sendParkingCouponMsg(WashOrder washOrder, String parkingCouponUrl) {
-        sendTemplateMessage(MsgTemplateType.PARKING_COUPON, washOrder.getUserId(), washOrder, parkingCouponUrl);
+        self.sendTemplateMessage(MsgTemplateType.PARKING_COUPON, washOrder.getUserId(), washOrder, parkingCouponUrl);
     }
 
 

+ 0 - 11
car-wash-service/src/main/java/com/kym/service/impl/UserServiceImpl.java

@@ -261,17 +261,6 @@ public class UserServiceImpl extends MPJBaseServiceImpl<UserMapper, User> implem
         }
     }
 
-    @Override
-    public PageBean<UserVo> listUserVo(Integer pageNum, Integer pageSize) {
-        PageHelper.startPage(pageNum, pageSize);
-        var userVoList = list().stream().map(user -> {
-            var userVo = new UserVo();
-            BeanUtils.copyProperties(user, userVo);
-            return userVo;
-        }).collect(Collectors.toList());
-        return new PageBean<>(userVoList);
-    }
-
 
     /**
      * 分页查询用户列表

+ 10 - 0
car-wash-service/src/main/java/com/kym/service/impl/WalletDetailServiceImpl.java

@@ -31,6 +31,16 @@ public class WalletDetailServiceImpl extends MPJBaseServiceImpl<WalletDetailMapp
         return lambdaQuery().eq(WalletDetail::getOrderNo, orderNo).eq(WalletDetail::getType, type).one();
     }
 
+    @Override
+    public WalletDetail getWalletDetailByOrderNoForUser(String orderNo, Integer type, Long userId) {
+        // C 端查询:强制按用户过滤,防止通过订单号越权查看他人资金流水
+        return lambdaQuery()
+                .eq(WalletDetail::getOrderNo, orderNo)
+                .eq(WalletDetail::getType, type)
+                .eq(WalletDetail::getUserId, userId)
+                .one();
+    }
+
     @Override
     public List<WalletDetail> listWalletDetail(int type) {
         var userId = StpUtil.getLoginIdAsLong();

+ 16 - 7
car-wash-service/src/main/java/com/kym/service/impl/WashOrderServiceImpl.java

@@ -110,6 +110,17 @@ public class WashOrderServiceImpl extends MyBaseServiceImpl<WashOrderMapper, Was
         // 检查设备是否有僵死订单(设备显示忙碌但订单实际已超时)
         checkAndResolveStaleOrder(params.getProductKey(), params.getDeviceName());
 
+        // 设备占用检查:僵死单清理后设备仍有他人未完结订单,说明设备真实使用中,拒绝开单
+        var occupiedOrder = lambdaQuery()
+                .eq(WashOrder::getProductKey, params.getProductKey())
+                .eq(WashOrder::getDeviceName, params.getDeviceName())
+                .eq(WashOrder::getOrderStatus, WashOrder.ORDER_STATUS_开机)
+                .eq(WashOrder::getPayStatus, WashOrder.PAY_STATUS_未支付)
+                .one();
+        if (occupiedOrder != null && !occupiedOrder.getUserId().equals(StpUtil.getLoginIdAsLong())) {
+            throw new BusinessException("设备他人使用中!");
+        }
+
         var memberName = StpUtil.getSession().getString(User.ST_SESSION_KEY_MOBILE);
         var orderId = OrderUtils.getOrderNo();
 
@@ -288,14 +299,12 @@ public class WashOrderServiceImpl extends MyBaseServiceImpl<WashOrderMapper, Was
      */
     @Override
     public WashOrder queryOrder(WashOrderQueryParams params) {
-        // 非实时数据
+        // C 端订单详情:强制按当前登录用户过滤,防止通过订单号越权查看他人订单
         WashOrder order = lambdaQuery()
                 .eq(WashOrder::getOrderId, params.getOrderId())
+                .eq(WashOrder::getUserId, StpUtil.getLoginIdAsLong())
                 .one();
-        if (null != params.getUserId()) {
-            CommUtil.asserts(null != order && order.getUserId().equals(StpUtil.getLoginIdAsLong()),
-                    "订单不存在或您没有权限查看该订单!");
-        }
+        CommUtil.asserts(null != order, "订单不存在或您没有权限查看该订单!");
         return order;
     }
 
@@ -317,8 +326,8 @@ public class WashOrderServiceImpl extends MyBaseServiceImpl<WashOrderMapper, Was
                 .orderByDesc(WashOrder::getId)
                 .list();
 
-        CommUtil.asserts(CommUtil.isEmptyOrNull(orders) && (orders.stream().mapToInt(WashOrder::getAmount).sum() >= 0),
-                "抱歉:无停车场洗车记录");
+        // 原断言写反:orders 非空时抛异常、为空时走到 orders.get(0) NPE,此处修正
+        CommUtil.asserts(!CommUtil.isEmptyOrNull(orders), "抱歉:无停车场洗车记录");
 
         return washStationService.lambdaQuery().eq(WashStation::getStationId, orders.get(0).getStationId()).one().getParkingQrCode();
     }

+ 2 - 1
car-wash-service/src/main/java/com/kym/service/wechat/TemplateParamBuilderConfig.java

@@ -5,6 +5,7 @@ import com.kym.service.wechat.impl.OrderCompletedParamBuilder;
 import com.kym.service.wechat.impl.ParkingCouponParamBuilder;
 import com.kym.service.wechat.impl.PaymentSuccessParamBuilder;
 import com.kym.service.wechat.impl.RefundApplyParamBuilder;
+import com.kym.service.wechat.impl.RefundSuccessParamBuilder;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -21,7 +22,7 @@ public class TemplateParamBuilderConfig {
         builders.put(MsgTemplateType.ORDER_COMPLETED, new OrderCompletedParamBuilder());
         builders.put(MsgTemplateType.PARKING_COUPON, new ParkingCouponParamBuilder());
         builders.put(MsgTemplateType.REFUND_APPLY, new RefundApplyParamBuilder());
-        builders.put(MsgTemplateType.REFUND_SUCCESS, new RefundApplyParamBuilder());
+        builders.put(MsgTemplateType.REFUND_SUCCESS, new RefundSuccessParamBuilder());
         return builders;
     }
 }