Commit 2f424bcf5472495f13e2a7ac8d7d925dba56e0de

Authored by 杨刚
1 parent e4613a7a

新增订单创建后自动派单功能,支持根据规则配置立即调度

CLAUDE.md deleted 100644 → 0
1 -# CLAUDE.md  
2 -  
3 -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.  
4 -  
5 -## Common commands  
6 -  
7 -- `mvn spring-boot:run` — run the service with the checked-in `src/main/resources/application.yml`  
8 -- `mvn test` — run all tests  
9 -- `mvn -Dtest=DeliveryFeeServiceImplTest test` — run the pricing-engine test class  
10 -- `mvn -Dtest=DeliveryFeeServiceImplTest#shouldApplyMinFee test` — run one test method  
11 -- `mvn package` — build the jar under `target/`  
12 -- `mvn -DskipTests package` — build without running tests  
13 -- `mysql -u root -p dili_rider < src/main/resources/schema.sql` — initialize schema  
14 -- `mysql -u root -p dili_rider < src/main/resources/data-init.sql` — load seed data  
15 -  
16 -Notes:  
17 -- There is no Maven wrapper in this repo; use the system `mvn`.  
18 -- No dedicated lint/format command is configured in `pom.xml`.  
19 -- Test coverage is currently sparse; the only checked-in unit tests are in `src/test/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImplTest.java`.  
20 -  
21 -## Architecture overview  
22 -  
23 -This is a Java 17 / Spring Boot 3.2 monolith for rider delivery operations. The main entrypoint is `src/main/java/com/diligrp/rider/RiderServiceApplication.java`, which enables MyBatis mapper scanning and `@EnableAsync`.  
24 -  
25 -### Main layers  
26 -  
27 -Code follows a conventional Spring layout under `src/main/java/com/diligrp/rider/`:  
28 -- `controller` — REST endpoints grouped by caller type  
29 -- `service` / `service/impl` — business logic  
30 -- `mapper` — MyBatis-Plus mappers  
31 -- `entity` — database entities  
32 -- `dto` / `vo` — request and response shapes  
33 -- `config` — interceptors, JWT utilities, MVC and WebSocket wiring  
34 -- `task` — scheduled background jobs  
35 -- `websocket` — live rider-location subscriptions and push  
36 -- `common` — shared `Result`, enums, and exception handling  
37 -  
38 -Most persistence uses MyBatis-Plus query/update wrappers. XML SQL exists, but only in a few files under `src/main/resources/mapper/`. Logical delete is globally configured through the `isDel` field in `src/main/resources/application.yml`.  
39 -  
40 -### API surfaces  
41 -  
42 -Controllers are split by audience rather than by technical module:  
43 -- `/api/rider/**` — rider app APIs  
44 -- `/api/admin/**` — admin and substation admin APIs  
45 -- `/api/platform/**` — super-admin platform APIs  
46 -- `/api/open/**` — signed open-platform APIs for third-party integrations  
47 -- `/api/delivery/fee/**` — internal fee-calculation APIs  
48 -  
49 -All controllers return the shared `Result<T>` envelope from `src/main/java/com/diligrp/rider/common/result/Result.java`. Cross-cutting exception mapping lives in `src/main/java/com/diligrp/rider/common/exception/GlobalExceptionHandler.java`.  
50 -  
51 -### Authentication and tenant/city scoping  
52 -  
53 -This service does not use Spring Security. Authentication is interceptor-driven:  
54 -- `src/main/java/com/diligrp/rider/config/AuthInterceptor.java` handles JWT auth for rider/admin/platform APIs.  
55 -- `src/main/java/com/diligrp/rider/config/OpenApiInterceptor.java` handles signed auth for `/api/open/**` using `X-App-Key`, `X-Timestamp`, `X-Nonce`, and `X-Sign`.  
56 -- `src/main/java/com/diligrp/rider/config/WebMvcConfig.java` wires both interceptors.  
57 -- JWT creation/parsing lives in `src/main/java/com/diligrp/rider/config/JwtUtil.java`.  
58 -  
59 -Important boundary rule: city/tenant identity is derived from trusted server-side state, not from caller input.  
60 -- Rider/admin JWTs inject `riderId`, `adminId`, `role`, and sometimes `cityId` into the request.  
61 -- Substation admins get `cityId` from the `substation` record, not from the request.  
62 -- Open-platform requests derive `cityId` from the bound `OpenApp`, not from payload fields.  
63 -  
64 -If you touch auth or routing, preserve that pattern.  
65 -  
66 -Also note: password checking in `RiderAuthServiceImpl` and `AdminAuthServiceImpl` uses MD5 hashing, so do not assume bcrypt/Spring Security conventions are already in place.  
67 -  
68 -### Core business flows  
69 -  
70 -#### Delivery pricing is DB-driven  
71 -  
72 -The pricing engine is centered on `src/main/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImpl.java`, but the actual pricing configuration comes from DB tables, not hardcoded constants.  
73 -  
74 -`src/main/java/com/diligrp/rider/service/impl/CityServiceImpl.java` assembles the active pricing plan from:  
75 -- `delivery_fee_plan`  
76 -- `delivery_fee_plan_dimension`  
77 -- `delivery_fee_plan_distance_step`  
78 -- `delivery_fee_plan_piece_rule`  
79 -- `delivery_fee_plan_time_rule`  
80 -  
81 -That assembled config is then used to compute:  
82 -- base fee  
83 -- distance fee / distance steps  
84 -- weight fee  
85 -- piece-count fee  
86 -- time-window surcharge  
87 -- minimum fee  
88 -- estimated delivery time  
89 -  
90 -If you change pricing behavior, check both `CityServiceImpl` and `DeliveryFeeServiceImpl`, and extend `DeliveryFeeServiceImplTest`.  
91 -  
92 -#### Open-platform order creation drives the main order lifecycle  
93 -  
94 -`src/main/java/com/diligrp/rider/service/impl/DeliveryOrderServiceImpl.java` is the main open-platform order entry path. It:  
95 -- resolves the `OpenApp` from `appKey`  
96 -- forces `cityId` from the app binding  
97 -- optionally hydrates store info from merchant data  
98 -- computes the delivery fee  
99 -- creates the `orders` record  
100 -- emits webhook notifications for order events  
101 -  
102 -That service is a good starting point when tracing order ingestion and external callbacks.  
103 -  
104 -#### Dispatch is a scoring engine over DB state  
105 -  
106 -`src/main/java/com/diligrp/rider/service/impl/DispatchServiceImpl.java` performs rider selection. It scores candidates using current city, online/rest state, rider location, order load, refusal history, daily counts, and configured dispatch conditions.  
107 -  
108 -The dispatch engine depends on:  
109 -- the active dispatch rule template for the city  
110 -- current `rider_location` rows  
111 -- open `orders`  
112 -- rider/day statistics  
113 -  
114 -Scheduled jobs then advance the order lifecycle:  
115 -- `src/main/java/com/diligrp/rider/task/DispatchScheduleTask.java` runs every 3 seconds to auto-dispatch timed-out grab orders  
116 -- `src/main/java/com/diligrp/rider/task/OrderScheduleTask.java` runs every 60 seconds to auto-cancel stale unaccepted orders  
117 -  
118 -This app is DB-state-driven; there is no message queue coordinating dispatch.  
119 -  
120 -### Real-time location flow  
121 -  
122 -Live rider location is implemented with raw Spring WebSocket, not STOMP/SockJS.  
123 -- WebSocket endpoint: `/ws/location`  
124 -- Config: `src/main/java/com/diligrp/rider/config/LocationWebSocketConfig.java`  
125 -- Handshake auth: `src/main/java/com/diligrp/rider/websocket/LocationWebSocketHandshakeInterceptor.java`  
126 -- Update/push path: `src/main/java/com/diligrp/rider/service/impl/RiderLocationServiceImpl.java`  
127 -  
128 -Rider location updates are written to `rider_location` and then pushed to subscribed admin clients. Super admins must provide `cityId` when connecting; substation admins derive it from their account.  
129 -  
130 -### External integrations  
131 -  
132 -External notifications are sent asynchronously by `src/main/java/com/diligrp/rider/service/impl/WebhookServiceImpl.java` using JDK `HttpClient` plus `@Async`. There is no Kafka/RabbitMQ-style event bus in this repo.  
133 -  
134 -Redis is present, but its main visible use is SMS verification code storage in `src/main/java/com/diligrp/rider/service/impl/RiderAuthServiceImpl.java`; do not assume Redis-backed sessions or broad caching layers exist.  
135 -  
136 -### Database and runtime config  
137 -  
138 -- Main runtime config is `src/main/resources/application.yml`  
139 -- Schema lives in `src/main/resources/schema.sql`  
140 -- Seed data lives in `src/main/resources/data-init.sql`  
141 -  
142 -There is only one checked-in Spring config file; do not assume profile-specific config files already exist.  
src/main/java/com/diligrp/rider/service/impl/DeliveryOrderServiceImpl.java
@@ -13,10 +13,13 @@ import com.diligrp.rider.mapper.OpenAppMapper; @@ -13,10 +13,13 @@ import com.diligrp.rider.mapper.OpenAppMapper;
13 import com.diligrp.rider.mapper.OrdersMapper; 13 import com.diligrp.rider.mapper.OrdersMapper;
14 import com.diligrp.rider.service.DeliveryFeeService; 14 import com.diligrp.rider.service.DeliveryFeeService;
15 import com.diligrp.rider.service.DeliveryOrderService; 15 import com.diligrp.rider.service.DeliveryOrderService;
  16 +import com.diligrp.rider.service.DispatchRuleService;
  17 +import com.diligrp.rider.service.DispatchService;
16 import com.diligrp.rider.service.MerchantService; 18 import com.diligrp.rider.service.MerchantService;
17 import com.diligrp.rider.service.WebhookService; 19 import com.diligrp.rider.service.WebhookService;
18 import com.diligrp.rider.vo.DeliveryFeeResultVO; 20 import com.diligrp.rider.vo.DeliveryFeeResultVO;
19 import com.diligrp.rider.vo.DeliveryOrderCreateVO; 21 import com.diligrp.rider.vo.DeliveryOrderCreateVO;
  22 +import com.diligrp.rider.vo.DispatchRuleTemplateVO;
20 import lombok.RequiredArgsConstructor; 23 import lombok.RequiredArgsConstructor;
21 import lombok.extern.slf4j.Slf4j; 24 import lombok.extern.slf4j.Slf4j;
22 import org.springframework.stereotype.Service; 25 import org.springframework.stereotype.Service;
@@ -39,6 +42,8 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService { @@ -39,6 +42,8 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService {
39 private final OpenAppMapper openAppMapper; 42 private final OpenAppMapper openAppMapper;
40 private final MerchantService merchantService; 43 private final MerchantService merchantService;
41 private final DeliveryFeeService deliveryFeeService; 44 private final DeliveryFeeService deliveryFeeService;
  45 + private final DispatchRuleService dispatchRuleService;
  46 + private final DispatchService dispatchService;
42 private final WebhookService webhookService; 47 private final WebhookService webhookService;
43 private final ObjectMapper objectMapper; 48 private final ObjectMapper objectMapper;
44 49
@@ -167,6 +172,13 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService { @@ -167,6 +172,13 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService {
167 // 9. 回调接入方:订单已创建 172 // 9. 回调接入方:订单已创建
168 notifyCallback(order, "order.created"); 173 notifyCallback(order, "order.created");
169 174
  175 + // 10. 未开启抢单模式时,若开启自动派单则下单后立即调度
  176 + tryImmediateDispatch(order);
  177 +
  178 + Orders latestOrder = ordersMapper.selectById(order.getId());
  179 + if (latestOrder != null) {
  180 + order = latestOrder;
  181 + }
170 return toCreateVO(order, fee); 182 return toCreateVO(order, fee);
171 } 183 }
172 184
@@ -231,6 +243,30 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService { @@ -231,6 +243,30 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService {
231 } 243 }
232 } 244 }
233 245
  246 + private void tryImmediateDispatch(Orders order) {
  247 + if (order == null || order.getCityId() == null || order.getCityId() < 1) {
  248 + return;
  249 + }
  250 + try {
  251 + DispatchRuleTemplateVO rule = dispatchRuleService.getActiveRule(order.getCityId());
  252 + if (rule == null) {
  253 + return;
  254 + }
  255 + if (rule.getAutoDispatch() == null || rule.getAutoDispatch() != 1) {
  256 + return;
  257 + }
  258 + if (rule.getGrabEnabled() != null && rule.getGrabEnabled() == 1) {
  259 + return;
  260 + }
  261 + Long riderId = dispatchService.dispatch(order);
  262 + if (riderId != null) {
  263 + log.info("订单 {} 创建后立即自动派单成功 riderId={}", order.getId(), riderId);
  264 + }
  265 + } catch (Exception e) {
  266 + log.error("订单 {} 创建后立即自动派单异常", order.getId(), e);
  267 + }
  268 + }
  269 +
234 private String generateOrderNo() { 270 private String generateOrderNo() {
235 String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); 271 String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
236 return "DL" + date + UUID.randomUUID().toString().replace("-", "").substring(0, 10).toUpperCase(); 272 return "DL" + date + UUID.randomUUID().toString().replace("-", "").substring(0, 10).toUpperCase();