Browse Source

fix: 骑手定位上传兼容重复行与并发插入(一人一条)

setRiderposition 的 getOne 默认 throwEx=true,rider_id 存在历史重复行时直接 500
(selectOne found: 5)。改为 getOne(qw,false) 取第一条继续更新;插入分支捕获
DuplicateKeyException 转按骑手更新,配合 updatesql/sql.md 新增 rider_id 唯一索引
(清理历史重复后手动执行)从 DB 层保证每个骑手仅一条定位记录。
qmj 8 hours ago
parent
commit
a496b9cacd

+ 11 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/user/RiderPositionController.java

@@ -16,6 +16,7 @@ import com.ruoyi.system.utils.Auth;
 import com.ruoyi.system.utils.JwtUtil;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.web.bind.annotation.*;
 import com.ruoyi.common.annotation.Log;
 import com.ruoyi.common.core.controller.BaseController;
@@ -59,7 +60,8 @@ public class RiderPositionController extends BaseController
         InfoUser user = infoUserService.getById(id);
         QueryWrapper<RiderPosition> queryWrapper= new QueryWrapper<>();
         queryWrapper.eq("rider_id",id);
-        RiderPosition position = riderPositionService.getOne(queryWrapper);
+        // 骑手定位一人一条:历史重复行取第一条继续更新,不再抛 selectOne 多行异常
+        RiderPosition position = riderPositionService.getOne(queryWrapper, false);
         RiderPosition rider = new RiderPosition();
         if(position==null){
             rider.setRiderId(Long.valueOf(id));
@@ -70,7 +72,14 @@ public class RiderPositionController extends BaseController
             }
             System.out.println("setRiderposition更新定位,用户id,新增信息:"+ JSON.toJSONString( rider));
             logger.info("setRiderposition更新定位,用户id,新增信息:"+ JSON.toJSONString( rider));
-            Boolean org = riderPositionService.saveOrUpdate(rider);
+            Boolean org;
+            try {
+                org = riderPositionService.saveOrUpdate(rider);
+            } catch (DuplicateKeyException e) {
+                // 并发上传同时查空双双插入,撞 rider_id 唯一索引时转按骑手更新已有记录
+                logger.info("setRiderposition并发插入撞唯一索引转更新,用户id"+id);
+                org = riderPositionService.update(rider, queryWrapper);
+            }
             if(org){
                 return success();
             }else {

+ 130 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/user/RiderPositionControllerTest.java

@@ -0,0 +1,130 @@
+package com.ruoyi.app.user;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.domain.RiderPosition;
+import com.ruoyi.system.service.IInfoUserService;
+import com.ruoyi.system.service.IRiderPositionService;
+import com.ruoyi.system.utils.JwtUtil;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.support.StaticMessageSource;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Locale;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 骑手定位上传:一人一条约束下的容错——历史重复行不再抛 selectOne 多行异常;
+ * 并发插入撞唯一索引转更新;新增/更新两分支行为不变。
+ */
+class RiderPositionControllerTest {
+
+    private static ConfigurableListableBeanFactory originalBeanFactory;
+
+    private final IRiderPositionService riderPositionService = mock(IRiderPositionService.class);
+    private final IInfoUserService infoUserService = mock(IInfoUserService.class);
+    private final RiderPositionController controller = new RiderPositionController();
+
+    @BeforeAll
+    static void initializeBeanFactory() {
+        originalBeanFactory = (ConfigurableListableBeanFactory)
+                ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
+        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+        StaticMessageSource messageSource = new StaticMessageSource();
+        messageSource.addMessage("no.action.success", Locale.getDefault(), "操作成功");
+        beanFactory.registerSingleton("messageSource", messageSource);
+        new SpringUtils().postProcessBeanFactory(beanFactory);
+    }
+
+    @AfterAll
+    static void restoreBeanFactory() {
+        new SpringUtils().postProcessBeanFactory(originalBeanFactory);
+    }
+
+    @BeforeEach
+    void setUp() {
+        ReflectionTestUtils.setField(controller, "riderPositionService", riderPositionService);
+        ReflectionTestUtils.setField(controller, "infoUserService", infoUserService);
+        when(infoUserService.getById(any())).thenReturn(new InfoUser());
+    }
+
+    @Test
+    void usesTolerantGetOneSoDuplicateRowsNoLongerCrash() {
+        RiderPosition existing = new RiderPosition();
+        existing.setId(7L);
+        existing.setRiderId(9L);
+        // 多行历史数据下 getOne(qw,false) 返回第一条,不再抛"Expected one result"
+        when(riderPositionService.getOne(any(Wrapper.class), eq(false))).thenReturn(existing);
+        when(riderPositionService.saveOrUpdate(any(RiderPosition.class))).thenReturn(true);
+
+        AjaxResult result = controller.setRiderposition(JwtUtil.token("9", "user"), new RiderPosition());
+
+        assertEquals(200, ((Number) result.get(AjaxResult.CODE_TAG)).intValue());
+        // 单参 getOne 默认 throwEx=true,是本次线上 500 的直接来源,禁止再回到该调用
+        verify(riderPositionService, never()).getOne(any(Wrapper.class));
+        ArgumentCaptor<RiderPosition> captor = ArgumentCaptor.forClass(RiderPosition.class);
+        verify(riderPositionService).saveOrUpdate(captor.capture());
+        assertEquals(7L, captor.getValue().getId());
+    }
+
+    @Test
+    void concurrentInsertOnUniqueKeyFallsBackToUpdateByRider() {
+        when(riderPositionService.getOne(any(Wrapper.class), anyBoolean())).thenReturn(null);
+        // 并发上传双双查空:第二个 insert 撞 rider_id 唯一索引
+        when(riderPositionService.saveOrUpdate(any(RiderPosition.class)))
+                .thenThrow(new DuplicateKeyException("uk_rider_id"));
+        when(riderPositionService.update(any(RiderPosition.class), any(Wrapper.class))).thenReturn(true);
+
+        AjaxResult result = controller.setRiderposition(JwtUtil.token("9", "user"), new RiderPosition());
+
+        assertEquals(200, ((Number) result.get(AjaxResult.CODE_TAG)).intValue());
+        // 兜底转按 rider_id 更新已有记录,请求不 500
+        verify(riderPositionService).update(any(RiderPosition.class), any(Wrapper.class));
+    }
+
+    @Test
+    void firstUploadInsertsNewRecordWithRiderId() {
+        when(riderPositionService.getOne(any(Wrapper.class), anyBoolean())).thenReturn(null);
+        when(riderPositionService.saveOrUpdate(any(RiderPosition.class))).thenReturn(true);
+
+        AjaxResult result = controller.setRiderposition(JwtUtil.token("9", "user"), new RiderPosition());
+
+        assertEquals(200, ((Number) result.get(AjaxResult.CODE_TAG)).intValue());
+        ArgumentCaptor<RiderPosition> captor = ArgumentCaptor.forClass(RiderPosition.class);
+        verify(riderPositionService).saveOrUpdate(captor.capture());
+        assertEquals(9L, captor.getValue().getRiderId());
+        verify(riderPositionService, never()).update(any(RiderPosition.class), any(Wrapper.class));
+    }
+
+    @Test
+    void secondUploadUpdatesExistingRecordById() {
+        RiderPosition existing = new RiderPosition();
+        existing.setId(7L);
+        when(riderPositionService.getOne(any(Wrapper.class), anyBoolean())).thenReturn(existing);
+        when(riderPositionService.saveOrUpdate(any(RiderPosition.class))).thenReturn(true);
+
+        AjaxResult result = controller.setRiderposition(JwtUtil.token("9", "user"), new RiderPosition());
+
+        assertEquals(200, ((Number) result.get(AjaxResult.CODE_TAG)).intValue());
+        verify(riderPositionService, times(1)).saveOrUpdate(any(RiderPosition.class));
+        verify(riderPositionService, never()).update(any(RiderPosition.class), any(Wrapper.class));
+    }
+}

+ 9 - 0
updatesql/sql.md

@@ -1623,3 +1623,12 @@ ALTER TABLE pos_order ADD INDEX idx_user_cretim (user_id, cretim);
 ALTER TABLE flash_delivery_order ADD INDEX idx_user_ctime (user_id, create_time);
 ALTER TABLE flash_delivery_order ADD INDEX idx_receiver_ctime (receiver_user_id, create_time);
 ```
+
+## 2026-09-21 骑手定位一人一条(rider_position 唯一索引)
+
+```sql
+-- 2026-09-21 骑手定位唯一索引:每个骑手只允许一条定位记录
+-- 前置:先清理历史重复数据(每个 rider_id 只保留一条,建议保留最新一条),否则建索引会失败
+-- 清理参考:DELETE p1 FROM rider_position p1 INNER JOIN rider_position p2 ON p1.rider_id = p2.rider_id AND p1.id < p2.id;
+ALTER TABLE rider_position ADD UNIQUE INDEX uk_rider_id (rider_id);
+```