Prechádzať zdrojové kódy

fix: 停车券核销改为原子条件更新,测试接口仅限开发环境

- 核销改条件更新(status 未使用→已使用 判影响行数),重复点击/
  并发点击不会重复核销
- testLink/testSend 测试接口增加 active profile 校验,
  生产环境调用直接拒绝,防止任意手机号被发送模板消息

Co-Authored-By: Claude <noreply@anthropic.com>
skyline 3 týždňov pred
rodič
commit
04db0cda27

+ 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);