فهرست منبع

移除闪送运价专用锁表

保留事务、时段重叠和版本校验,删除低频后台配置不需要的全局数据库行锁及迁移表。
qmj 3 روز پیش
والد
کامیت
7a6e139c4e

+ 10 - 14
docs/superpowers/plans/2026-09-07-remove-flash-pricing-lock.md

@@ -35,29 +35,25 @@
 - Consumes: `FlashDeliveryPricingMapper.countOverlapping(String, String, Long)`、`BaseMapper` CRUD 和 `FlashDeliveryPricing.configVersion`。
 - Produces: 不再暴露 `FlashDeliveryPricingMapper.lockPricing()`;运价接口和请求响应契约保持不变。
 
-- [ ] **Step 1: 先写删除锁接口的失败契约测试**
+- [x] **Step 1: 先让新增运价不依赖专用锁行的行为测试红灯**
 
-将原锁表测试改为要求 Mapper 不再暴露锁方法
+删除 `FlashDeliveryApplicationServiceTest.Fixture` 中的以下默认模拟,让既有 `createPricingPersistsImmediatelyActiveTimePeriod` 测试表达“数据库没有专用锁行时仍可新增运价”的目标行为
 
 ```java
-@Test
-void pricingMapperDoesNotExposeDedicatedDatabaseLock() {
-    assertThrows(NoSuchMethodException.class,
-            () -> FlashDeliveryPricingMapper.class.getMethod("lockPricing"));
-}
+when(pricingMapper.lockPricing()).thenReturn("GLOBAL");
 ```
 
-- [ ] **Step 2: 运行测试确认旧实现红灯**
+- [x] **Step 2: 运行测试确认旧实现红灯**
 
 ```powershell
 $env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
 $env:PATH="$env:JAVA_HOME\bin;$env:PATH"
-mvn -pl ruoyi-system -am -Dtest=FlashDeliveryPricingMapperContractTest -Dsurefire.failIfNoSpecifiedTests=false test
+mvn -pl ruoyi-admin -am "-Dtest=FlashDeliveryApplicationServiceTest#createPricingPersistsImmediatelyActiveTimePeriod" "-Dsurefire.failIfNoSpecifiedTests=false" test
 ```
 
-预期:`pricingMapperDoesNotExposeDedicatedDatabaseLock` 失败,因为旧 Mapper 仍存在 `lockPricing()`。
+预期:`createPricingPersistsImmediatelyActiveTimePeriod` 因旧实现读取不到 `GLOBAL` 锁行而抛出 `ServiceException`。
 
-- [ ] **Step 3: 删除最小生产锁逻辑并修正既有测试**
+- [x] **Step 3: 删除最小生产锁逻辑并修正既有测试**
 
 从 Mapper 删除:
 
@@ -71,9 +67,9 @@ mvn -pl ruoyi-system -am -Dtest=FlashDeliveryPricingMapperContractTest -Dsurefir
 String lockPricing();
 ```
 
-从 `createPricing`、`updatePricing`、`deletePricing` 删除 `lockPricing();`,并删除服务层私有 `lockPricing()` 方法。应用服务测试删除 `lockPricing()` 的 mock 和顺序验证,但继续验证重叠查询、插入、版本更新和删除行为。
+从 `createPricing`、`updatePricing`、`deletePricing` 删除 `lockPricing();`,并删除服务层私有 `lockPricing()` 方法。Mapper 契约测试删除仅验证专用锁 SQL 的测试;应用服务测试删除 `lockPricing()` 的调用顺序断言,但继续验证重叠查询、插入、版本更新和删除行为。
 
-- [ ] **Step 4: 同步规格和迁移 SQL**
+- [x] **Step 4: 同步规格和迁移 SQL**
 
 删除当前数据模型中的 `flash_delivery_pricing_lock`,在规格中明确低频管理场景接受并发新增的极低概率重叠风险;修改仍使用版本校验,读取仍拒绝多条匹配。在 `updatesql/sql.md` 末尾追加:
 
@@ -87,7 +83,7 @@ DROP TABLE IF EXISTS flash_delivery_pricing_lock;
 
 更新 `tasks.md` 增加并完成本次规格、红灯、实现、验证任务。
 
-- [ ] **Step 5: 运行定向测试和模块构建**
+- [x] **Step 5: 运行定向测试和模块构建**
 
 ```powershell
 $env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'

+ 2 - 2
docs/superpowers/specs/2026-09-07-remove-flash-pricing-lock-design.md

@@ -27,7 +27,7 @@
 
 ## 验证
 
-- 先调整 Mapper 契约测试,使旧实现因仍暴露 `lockPricing()` 而失败。
-- 删除生产锁逻辑后运行 Mapper、应用服务和 Controller 闪送定向测试。
+- 先删除应用服务测试夹具对固定 `GLOBAL` 锁行的模拟,验证管理员新增运价不应依赖专用锁行;旧实现会因此失败。
+- 删除生产锁逻辑后,移除只验证锁方法调用顺序的内部实现断言,并运行 Mapper、应用服务和 Controller 闪送定向测试。
 - 使用 JDK 21 执行 `ruoyi-admin` 相关模块测试和构建。
 - 提交前检查暂存清单,不能包含工作区原有注释修改或 `.claude/homunculus/observations.jsonl`。

+ 0 - 9
ruoyi-admin/src/main/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryApplicationService.java

@@ -342,7 +342,6 @@ public class FlashDeliveryApplicationService {
     @Transactional
     public FlashDeliveryPricing createPricing(Long adminId, FlashDeliveryPricingRequest request) {
         validatePricingRequest(request, false);
-        lockPricing();
         ensureNoPricingOverlap(request, null);
         Date now = new Date();
         FlashDeliveryPricing pricing = pricingFrom(request);
@@ -360,7 +359,6 @@ public class FlashDeliveryApplicationService {
         validatePricingRequest(request, true);
         FlashDeliveryPricing preliminary = pricingId == null ? null : pricingMapper.selectById(pricingId);
         if (preliminary == null) throw fail("flash.delivery.pricing.not.found");
-        lockPricing();
         FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
         if (pricing == null) throw fail("flash.delivery.pricing.not.found");
         ensureNoPricingOverlap(request, pricingId);
@@ -400,7 +398,6 @@ public class FlashDeliveryApplicationService {
         if (preliminary == null) {
             throw fail("flash.delivery.pricing.not.found");
         }
-        lockPricing();
         FlashDeliveryPricing pricing = pricingMapper.selectById(pricingId);
         if (pricing == null) throw fail("flash.delivery.pricing.not.found");
         if (pricingMapper.deleteById(pricingId) != 1) throw fail("flash.delivery.pricing.not.found");
@@ -524,12 +521,6 @@ public class FlashDeliveryApplicationService {
         }
     }
 
-    private void lockPricing() {
-        if (!Objects.equals("GLOBAL", pricingMapper.lockPricing())) {
-            throw fail("flash.delivery.pricing.invalid");
-        }
-    }
-
     private FlashDeliveryPricing pricingFrom(FlashDeliveryPricingRequest request) {
         FlashDeliveryPricing pricing = new FlashDeliveryPricing();
         pricing.setStartTime(request.getStartTime());

+ 5 - 11
ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryApplicationServiceTest.java

@@ -705,7 +705,6 @@ class FlashDeliveryApplicationServiceTest {
         assertEquals(90L, saved.getValue().getStartingFare());
 
         InOrder writeOrder = inOrder(fixture.pricingMapper);
-        writeOrder.verify(fixture.pricingMapper).lockPricing();
         writeOrder.verify(fixture.pricingMapper)
                 .countOverlapping("00:00", "24:00", null);
         writeOrder.verify(fixture.pricingMapper).insert(any(FlashDeliveryPricing.class));
@@ -739,7 +738,7 @@ class FlashDeliveryApplicationServiceTest {
     }
 
     @Test
-    void updatingPricingUsesTheSingleGlobalLock() {
+    void updatingPricingChecksOverlapBeforeVersionedWrite() {
         Fixture fixture = new Fixture();
         FlashDeliveryPricing existing = pricing();
         when(fixture.pricingMapper.selectById(1L)).thenReturn(existing);
@@ -749,10 +748,10 @@ class FlashDeliveryApplicationServiceTest {
 
         fixture.service.updatePricing(9L, 1L, request);
 
-        InOrder lockOrder = inOrder(fixture.pricingMapper);
-        lockOrder.verify(fixture.pricingMapper).lockPricing();
-        lockOrder.verify(fixture.pricingMapper)
+        InOrder writeOrder = inOrder(fixture.pricingMapper);
+        writeOrder.verify(fixture.pricingMapper)
                 .countOverlapping("00:00", "24:00", 1L);
+        writeOrder.verify(fixture.pricingMapper).update(isNull(), any(Wrapper.class));
     }
 
     @Test
@@ -763,11 +762,7 @@ class FlashDeliveryApplicationServiceTest {
 
         fixture.service.deletePricing(18L);
 
-        InOrder deleteOrder = inOrder(fixture.pricingMapper);
-        deleteOrder.verify(fixture.pricingMapper).selectById(18L);
-        deleteOrder.verify(fixture.pricingMapper).lockPricing();
-        deleteOrder.verify(fixture.pricingMapper).selectById(18L);
-        deleteOrder.verify(fixture.pricingMapper).deleteById(18L);
+        verify(fixture.pricingMapper).deleteById(18L);
     }
 
     @Test
@@ -1011,7 +1006,6 @@ class FlashDeliveryApplicationServiceTest {
                 routeService, new FlashDeliveryPricingCalculator(), lockService, exclusivityService);
 
         Fixture() {
-            when(pricingMapper.lockPricing()).thenReturn("GLOBAL");
             when(lockService.withLock(any(), any())).thenAnswer(invocation -> {
                 RiderDeliveryLockService.LockedCall<?> call = invocation.getArgument(1);
                 return call.call();

+ 0 - 8
ruoyi-system/src/main/java/com/ruoyi/system/mapper/flash/FlashDeliveryPricingMapper.java

@@ -9,14 +9,6 @@ import java.util.List;
 
 /** 闪送时段运价持久化接口。 */
 public interface FlashDeliveryPricingMapper extends BaseMapper<FlashDeliveryPricing> {
-    @Select("""
-            select lock_key
-            from flash_delivery_pricing_lock
-            where lock_key = 'GLOBAL'
-            for update
-            """)
-    String lockPricing();
-
     @Select("""
             select id, start_time, end_time, starting_distance, starting_fare,
                    distance, freight, urgent_rate, minimum_urgent_fee,

+ 0 - 9
ruoyi-system/src/test/java/com/ruoyi/system/mapper/flash/FlashDeliveryPricingMapperContractTest.java

@@ -12,15 +12,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class FlashDeliveryPricingMapperContractTest {
 
-    @Test
-    void globalPricingLockUsesDatabaseRowLock() throws Exception {
-        Method method = FlashDeliveryPricingMapper.class.getMethod("lockPricing");
-        String sql = sql(method);
-
-        assertTrue(sql.contains("flash_delivery_pricing_lock"));
-        assertTrue(sql.contains("for update"));
-    }
-
     @Test
     void currentPeriodUsesInclusiveStartExclusiveEndAndDoesNotHideDuplicates() throws Exception {
         Method method = FlashDeliveryPricingMapper.class.getMethod("selectAtTime", String.class);

+ 1 - 5
specs/024-flash-delivery/data-model.md

@@ -8,11 +8,7 @@
 
 ## `flash_delivery_pricing`
 
-帮送与帮取共享多个统一运价时段,不再保存 `service_type`。字段为 `start_time`、`end_time`、`starting_distance`、整数 `starting_fare`、`distance`、整数 `freight`、非负两位小数百分比 `urgent_rate`、非负整数 `minimum_urgent_fee`、`config_version`、`updated_by` 和创建/更新时间。配置保存即生效,不保存 `enabled`;全部时间段不得重叠,索引支持按时间匹配目标时刻。有效修改后版本加一,历史订单不回写。时段查询不使用 `LIMIT 1` 隐藏异常数据;若匹配到多条则明确返回配置重叠错误。
-
-## `flash_delivery_pricing_lock`
-
-改为只保存一行全局运价锁,例如固定主键 `GLOBAL`。新增、修改和删除运价时,事务先通过 `SELECT ... FOR UPDATE` 取得该锁,再检查全部时间段重叠并写入,避免并发请求同时通过“先查重、后写入”。
+帮送与帮取共享多个统一运价时段,不再保存 `service_type`。字段为 `start_time`、`end_time`、`starting_distance`、整数 `starting_fare`、`distance`、整数 `freight`、非负两位小数百分比 `urgent_rate`、非负整数 `minimum_urgent_fee`、`config_version`、`updated_by` 和创建/更新时间。配置保存即生效,不保存 `enabled`;全部时间段不得重叠,索引支持按时间匹配目标时刻。有效修改后版本加一,历史订单不回写。时段查询不使用 `LIMIT 1` 隐藏异常数据;若匹配到多条则明确返回配置重叠错误。运价写入不再维护专用锁表;新增和修改仍执行重叠校验,修改仍使用 `config_version` 防止旧版本覆盖。
 
 ## `flash_delivery_order_image`
 

+ 1 - 1
specs/024-flash-delivery/design.md

@@ -97,7 +97,7 @@ ruoyi-system
 
 ### 4.2 `flash_delivery_pricing`
 
-保存一套供帮送和帮取共享、全局互不重叠的运价时段;每个时段保存开始时间、结束时间、起送距离、起送价格、计价距离、计价金额、加急比例、最低加急费、配置版本、修改人和修改时间。保存即生效,删除即失效,不设置启停字段。
+保存一套供帮送和帮取共享、全局互不重叠的运价时段;每个时段保存开始时间、结束时间、起送距离、起送价格、计价距离、计价金额、加急比例、最低加急费、配置版本、修改人和修改时间。保存即生效,删除即失效,不设置启停字段。低频管理员配置不使用专用数据库锁表;服务端继续检查时段重叠,修改继续使用配置版本条件,读取侧发现多条匹配时明确报错。
 
 ### 4.3 `flash_delivery_order_image`
 

+ 10 - 0
specs/024-flash-delivery/plan.md

@@ -673,3 +673,13 @@ The staged list must not contain `.claude/homunculus/observations.jsonl`、`.tmp
 - [ ] 在平台前端运行 `npm run test:flash-delivery`、修改文件 ESLint 和 `npm run build:prod`。
 - [ ] 运行两个仓库的 `git diff --check`,核对编码、换行和暂存文件清单。
 - [ ] 后端与平台前端分别形成中文独立提交;最终报告两个 SHA、实际检查结果和任何未执行项。
+
+## 移除运价专用锁表增量(2026-09-07)
+
+本节替代本文此前关于 `flash_delivery_pricing_lock`、全局锁和 `SELECT ... FOR UPDATE` 的当前架构要求;旧段落仅保留历史设计背景。
+
+- 平台运价增删改继续在 Spring 事务内执行,不再读取专用锁行。
+- 新增和修改继续通过 `countOverlapping(startTime,endTime,excludedId)` 检查所有统一时段;修改继续使用 `config_version` 更新条件。
+- 报价和创建订单仍要求目标时刻只匹配一个时段,异常多条匹配时明确失败,不使用 `LIMIT 1` 隐藏数据问题。
+- 业务接受低频后台并发新增极低概率同时通过重叠检查;不为该风险保留专用数据库表。
+- 在 `updatesql/sql.md` 追加删表语句,仅供开发者手动执行,不连接数据库。

+ 2 - 1
specs/024-flash-delivery/spec.md

@@ -127,7 +127,7 @@
 **验收场景**:
 
 1. **假如** 管理员进入价格配置页面,**当** 页面加载,**那么** 系统按开始时间展示全部统一运价时段、起送规则、普通超距规则、加急比例、最低加急费及配置版本。
-2. **假如** 管理员新增、修改或删除合法时段,**当** 操作成功,**那么** 页面刷新服务端配置;非法值、重叠时段或并发冲突必须保留编辑内容并展示错误。
+2. **假如** 管理员新增、修改或删除合法时段,**当** 操作成功,**那么** 页面刷新服务端配置;非法值、重叠时段或配置版本冲突必须保留编辑内容并展示错误。
 3. **假如** 管理员进入订单页面,**当** 按状态、服务类型、订单号、用户 ID 或骑手 ID 查询,**那么** 表格按服务端分页结果展示并可重置条件。
 4. **假如** 管理员查看订单详情,**当** 详情加载,**那么** 页面分区展示订单概况、取送地址、费用明细、预约与 PIN、骑手、图片凭证和状态日志。
 5. **假如** 订单处于非终态,**当** 具有取消权限的管理员填写原因并二次确认,**那么** 页面调用平台取消接口并刷新列表与详情。
@@ -272,6 +272,7 @@
 - 继续使用现有用户、骑手、token、骑手位置、图片上传和站内消息/推送基础设施。
 - 地图路线服务的具体供应商与密钥沿用项目现有配置能力;计划阶段确定现有配置键和 HTTP 调用位置。
 - 币种为新台币,接口使用稳定币种代码 `TWD`。
+- 运价属于低频管理员配置,不维护专用数据库锁表;业务接受并发新增请求极低概率同时通过重叠检查,修改仍由配置版本校验保护,读取侧仍拒绝异常多条匹配。
 - 第一阶段不根据有效骑手数量阻止下单,也不实现自动匹配、自动派单或定向派单。
 - 蓝湖页面中的附近骑手数量与预计接单时间不作为动态业务承诺,用户端统一显示“发布后等待附近骑手接单”。
 - 骑手第一阶段不能自行放弃已抢订单,由平台管理员介入处理异常订单。

+ 7 - 0
specs/024-flash-delivery/tasks.md

@@ -116,3 +116,10 @@
 - [x] T068 在 `updatesql/sql.md` 追加迁移 SQL,不执行数据库;同步五语言后端错误、App 接入文档和 quickstart
 - [x] T069 使用 JDK 21 完成后端 69 项定向测试和模块构建、平台前端 8 项定向测试及两个仓库差异检查;用户要求立即提交后,不再执行平台 ESLint 与生产构建
 - [x] T070 核对后端与平台前端暂存范围,分别使用中文提交;SHA 与未执行项记录在最终交付中
+
+## Phase 14:移除运价专用锁表
+
+- [x] T071 记录简化设计和实施计划,确认接受低频后台并发新增的极低概率重叠风险,保留事务、重叠校验、版本校验和读取侧异常保护
+- [x] T072 先移除应用服务测试夹具对固定锁行的模拟,运行新增运价测试并确认旧实现因依赖锁行红灯
+- [x] T073 删除 Mapper 锁方法、运价增删改锁调用及内部锁断言,同步规格和删表 SQL
+- [ ] T074 使用 JDK 21 运行闪送运价定向测试、模块构建和差异检查,核对暂存范围后中文提交并推送

+ 7 - 0
updatesql/sql.md

@@ -1496,3 +1496,10 @@ ALTER TABLE flash_delivery_order
   DROP INDEX idx_flash_available,
   ADD KEY idx_flash_available (status, delivery_mode, scheduled_pickup_start_at, delivery_type, create_time);
 ```
+
+## 2026-09-07 移除闪送运价专用锁表
+
+```sql
+-- 运价为低频管理员配置;保留事务、时段重叠检查和配置版本校验,不再维护专用锁行。
+DROP TABLE IF EXISTS flash_delivery_pricing_lock;
+```