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 13 import com.diligrp.rider.mapper.OrdersMapper;
14 14 import com.diligrp.rider.service.DeliveryFeeService;
15 15 import com.diligrp.rider.service.DeliveryOrderService;
  16 +import com.diligrp.rider.service.DispatchRuleService;
  17 +import com.diligrp.rider.service.DispatchService;
16 18 import com.diligrp.rider.service.MerchantService;
17 19 import com.diligrp.rider.service.WebhookService;
18 20 import com.diligrp.rider.vo.DeliveryFeeResultVO;
19 21 import com.diligrp.rider.vo.DeliveryOrderCreateVO;
  22 +import com.diligrp.rider.vo.DispatchRuleTemplateVO;
20 23 import lombok.RequiredArgsConstructor;
21 24 import lombok.extern.slf4j.Slf4j;
22 25 import org.springframework.stereotype.Service;
... ... @@ -39,6 +42,8 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService {
39 42 private final OpenAppMapper openAppMapper;
40 43 private final MerchantService merchantService;
41 44 private final DeliveryFeeService deliveryFeeService;
  45 + private final DispatchRuleService dispatchRuleService;
  46 + private final DispatchService dispatchService;
42 47 private final WebhookService webhookService;
43 48 private final ObjectMapper objectMapper;
44 49  
... ... @@ -167,6 +172,13 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService {
167 172 // 9. 回调接入方:订单已创建
168 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 182 return toCreateVO(order, fee);
171 183 }
172 184  
... ... @@ -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 270 private String generateOrderNo() {
235 271 String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
236 272 return "DL" + date + UUID.randomUUID().toString().replace("-", "").substring(0, 10).toUpperCase();
... ...