Commit 2f424bcf5472495f13e2a7ac8d7d925dba56e0de
1 parent
e4613a7a
新增订单创建后自动派单功能,支持根据规则配置立即调度
Showing
2 changed files
with
36 additions
and
142 deletions
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(); |