DispatchServiceImpl.java
16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
package com.diligrp.rider.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.diligrp.rider.common.enums.DispatchConditionType;
import com.diligrp.rider.entity.*;
import com.diligrp.rider.mapper.*;
import com.diligrp.rider.service.DispatchRuleService;
import com.diligrp.rider.service.DispatchService;
import com.diligrp.rider.service.WebhookService;
import com.diligrp.rider.util.GeoUtil;
import com.diligrp.rider.vo.DispatchRuleTemplateVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class DispatchServiceImpl implements DispatchService {
private final DispatchRuleService dispatchRuleService;
private final RiderMapper riderMapper;
private final RiderLocationMapper locationMapper;
private final OrdersMapper ordersMapper;
private final RiderOrderCountMapper countMapper;
private final RiderOrderRefuseMapper refuseMapper;
private final WebhookService webhookService;
private final ObjectMapper objectMapper;
@Override
@Transactional
public Long dispatch(Orders order) {
DispatchRuleTemplateVO rule = dispatchRuleService.getActiveRule(order.getCityId());
if (rule == null) {
log.debug("城市 {} 无生效调度规则,跳过派单", order.getCityId());
return null;
}
if (rule.getAutoDispatch() == null || rule.getAutoDispatch() != 1) {
return null;
}
// 1. 获取候选骑手:同城市、在线、审核通过
List<Rider> candidates = riderMapper.selectList(
new LambdaQueryWrapper<Rider>()
.eq(Rider::getCityId, order.getCityId())
.eq(Rider::getIsRest, 0)
.eq(Rider::getUserStatus, 1)
.eq(Rider::getStatus, 1));
if (candidates.isEmpty()) {
log.debug("城市 {} 无在线骑手", order.getCityId());
return null;
}
// 排除已拒单的骑手
List<Long> refusedRiderIds = getRefusedRiderIds(order.getId());
if (!refusedRiderIds.isEmpty()) {
candidates.removeIf(r -> refusedRiderIds.contains(r.getId()));
}
// 排除转出该单的骑手
if (order.getOldRiderId() != null && order.getOldRiderId() > 0) {
candidates.removeIf(r -> r.getId().equals(order.getOldRiderId()));
}
if (candidates.isEmpty()) return null;
// 2. 获取骑手位置
List<Long> riderIds = candidates.stream().map(Rider::getId).toList();
Map<Long, RiderLocation> locationMap = getLocationMap(riderIds);
// 3. 获取启用的条件列表(按 sortOrder 升序)
List<DispatchRuleTemplateVO.ConditionItem> enabledConditions = new ArrayList<>();
if (rule.getConditions() != null) {
for (DispatchRuleTemplateVO.ConditionItem c : rule.getConditions()) {
if (c.getEnabled() != null && c.getEnabled() == 1) {
enabledConditions.add(c);
}
}
}
// 4. 获取订单取货点坐标
double orderLat = parseDouble(order.getFLat());
double orderLng = parseDouble(order.getFLng());
double orderTLat = parseDouble(order.getTLat());
double orderTLng = parseDouble(order.getTLng());
// 5. 预加载每个骑手的统计数据
Map<Long, Integer> currentLoadMap = getCurrentLoadMap(riderIds);
Map<Long, Integer> dailyCountMap = getDailyCountMap(riderIds);
// 6. 对每个候选骑手评分
int totalConditions = enabledConditions.size();
List<RiderScore> scores = new ArrayList<>();
for (Rider rider : candidates) {
RiderLocation loc = locationMap.get(rider.getId());
if (loc == null) continue; // 无位置信息的骑手不参与派单
double riderLat = parseDouble(loc.getLat());
double riderLng = parseDouble(loc.getLng());
double distanceToPickup = GeoUtil.calcDistanceKm(riderLat, riderLng, orderLat, orderLng);
// 持单量检查(强制过滤)
int currentLoad = currentLoadMap.getOrDefault(rider.getId(), 0);
if (rule.getGrabMaxPerRider() != null && currentLoad >= rule.getGrabMaxPerRider()) {
continue; // 持单已满,跳过
}
double score = 0;
boolean filtered = false;
for (int i = 0; i < enabledConditions.size(); i++) {
DispatchRuleTemplateVO.ConditionItem cond = enabledConditions.get(i);
double weight = totalConditions - i; // 排在前面的权重更高
double condScore = evaluateCondition(cond, rider, loc, order,
distanceToPickup, riderLat, riderLng, orderLat, orderLng, orderTLat, orderTLng,
currentLoad, dailyCountMap.getOrDefault(rider.getId(), 0));
if (condScore < 0) {
// 负分表示强制不通过(如距离超出阈值)
filtered = true;
break;
}
score += condScore * weight;
}
if (!filtered) {
scores.add(new RiderScore(rider.getId(), score, distanceToPickup));
}
}
if (scores.isEmpty()) {
log.debug("订单 {} 无匹配骑手", order.getId());
return null;
}
// 7. 选择最优骑手:分数最高,平分取距离最近
scores.sort((a, b) -> {
int cmp = Double.compare(b.score, a.score);
if (cmp != 0) return cmp;
return Double.compare(a.distance, b.distance);
});
Long bestRiderId = scores.get(0).riderId;
// 8. 指派订单
long now = System.currentTimeMillis() / 1000;
int updated = ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
.eq(Orders::getId, order.getId())
.eq(Orders::getStatus, 2)
.eq(Orders::getRiderId, 0)
.set(Orders::getRiderId, bestRiderId)
.set(Orders::getStatus, 3)
.set(Orders::getGrapTime, now)
.set(Orders::getDispatchTime, now)
.set(Orders::getDispatchRiderId, bestRiderId)
.set(Orders::getOldRiderId, bestRiderId));
if (updated == 0) {
log.debug("订单 {} 派单 CAS 失败(可能已被抢单)", order.getId());
return null;
}
log.info("订单 {} 自动派单给骑手 {},评分={}", order.getId(), bestRiderId, scores.get(0).score);
// 通知接入方
notifyOrderEvent(order.getId(), "order.dispatched");
// 清除拒单记录
refuseMapper.delete(new LambdaQueryWrapper<RiderOrderRefuse>()
.eq(RiderOrderRefuse::getOid, order.getId()));
return bestRiderId;
}
/**
* 评估单个条件,返回得分
* @return 得分:1.0=完全匹配, 0~1=部分匹配, -1=强制不通过
*/
private double evaluateCondition(DispatchRuleTemplateVO.ConditionItem cond,
Rider rider, RiderLocation loc, Orders order,
double distanceToPickup,
double riderLat, double riderLng,
double orderLat, double orderLng,
double orderTLat, double orderTLng,
int currentLoad, int dailyCount) {
DispatchConditionType type = DispatchConditionType.fromCode(cond.getConditionType());
if (type == null) return 0;
double threshold = cond.getThresholdValue() != null ? cond.getThresholdValue().doubleValue() : 0;
return switch (type) {
case DISTANCE -> {
// 距离取单地址 ≤ N 公里,超过则过滤
if (threshold > 0 && distanceToPickup > threshold) yield -1;
// 距离越近分越高(归一化到 0~1)
yield threshold > 0 ? Math.max(0, 1.0 - distanceToPickup / threshold) : 1.0;
}
case DETOUR -> {
// 顺路距离:骑手当前有在途订单时,计算绕路程度
// 简化实现:以骑手到取货点的距离作为近似
if (threshold > 0 && distanceToPickup > threshold) yield -1;
yield threshold > 0 ? Math.max(0, 1.0 - distanceToPickup / threshold) : 1.0;
}
case WAIT -> {
// 等待新订单时间 ≥ N 分钟:骑手最后完成订单距今
long lastCompleteTime = getLastCompleteTime(rider.getId());
long now = System.currentTimeMillis() / 1000;
double waitMinutes = lastCompleteTime > 0 ? (now - lastCompleteTime) / 60.0 : 999;
yield waitMinutes >= threshold ? 1.0 : waitMinutes / threshold;
}
case CURRENT_LOAD -> {
// 当前持单量 < N
if (threshold > 0 && currentLoad >= threshold) yield -1;
yield threshold > 0 ? Math.max(0, 1.0 - currentLoad / threshold) : 1.0;
}
case DAILY -> {
// 当日总接单量 < N
if (threshold > 0 && dailyCount >= threshold) yield -1;
yield threshold > 0 ? Math.max(0, 1.0 - dailyCount / threshold) : 1.0;
}
case DIRECTION -> {
// 目的地方向一致性:骑手位置到订单取货点的方向 vs 取货点到收货点的方向
double bearingToPickup = calcBearing(riderLat, riderLng, orderLat, orderLng);
double bearingPickupToDest = calcBearing(orderLat, orderLng, orderTLat, orderTLng);
double diff = Math.abs(bearingToPickup - bearingPickupToDest);
if (diff > 180) diff = 360 - diff;
// 方向差 ≤ 45度算顺路
yield diff <= 45 ? 1.0 : (diff <= 90 ? 0.5 : 0);
}
case ROOKIE -> {
// 新手保护:入职 ≤ N 天的骑手优先
long createTime = rider.getCreateTime() != null ? rider.getCreateTime() : 0;
long now = System.currentTimeMillis() / 1000;
double daysRegistered = (now - createTime) / 86400.0;
yield daysRegistered <= threshold ? 1.0 : 0;
}
case PRAISE -> {
// 好评率 ≥ N%
int starTotal = rider.getStarTotal() != null ? rider.getStarTotal() : 0;
int starCount = rider.getStarCount() != null ? rider.getStarCount() : 0;
if (starCount == 0) yield 0.5; // 无评价的骑手给中间分
double avgStar = (double) starTotal / starCount;
double praiseRate = avgStar / 5.0 * 100; // 转换为百分比
yield praiseRate >= threshold ? 1.0 : praiseRate / threshold;
}
case AREA_MATCH -> {
// 区域匹配:骑手当前所在城市与订单城市匹配
// 目前按 city_id 匹配(候选骑手已按 cityId 过滤,所以这里总是匹配的)
// 后续可扩展为更细粒度的区域
yield 1.0;
}
};
}
// ---- 辅助方法 ----
private List<Long> getRefusedRiderIds(Long orderId) {
List<RiderOrderRefuse> refuses = refuseMapper.selectList(
new LambdaQueryWrapper<RiderOrderRefuse>()
.eq(RiderOrderRefuse::getOid, orderId)
.select(RiderOrderRefuse::getRiderId));
return refuses.stream().map(RiderOrderRefuse::getRiderId).toList();
}
private Map<Long, RiderLocation> getLocationMap(List<Long> riderIds) {
if (riderIds.isEmpty()) return Map.of();
List<RiderLocation> locations = locationMapper.selectList(
new LambdaQueryWrapper<RiderLocation>().in(RiderLocation::getUid, riderIds));
Map<Long, RiderLocation> map = new HashMap<>();
for (RiderLocation loc : locations) {
map.put(loc.getUid(), loc);
}
return map;
}
/** 获取每个骑手当前持单量(status=3 或 4 的订单数) */
private Map<Long, Integer> getCurrentLoadMap(List<Long> riderIds) {
Map<Long, Integer> map = new HashMap<>();
if (riderIds.isEmpty()) return map;
List<Orders> inProgress = ordersMapper.selectList(
new LambdaQueryWrapper<Orders>()
.in(Orders::getRiderId, riderIds)
.in(Orders::getStatus, List.of(3, 4))
.select(Orders::getRiderId));
for (Orders o : inProgress) {
map.merge(o.getRiderId(), 1, Integer::sum);
}
return map;
}
/** 获取每个骑手当日接单量 */
private Map<Long, Integer> getDailyCountMap(List<Long> riderIds) {
Map<Long, Integer> map = new HashMap<>();
if (riderIds.isEmpty()) return map;
long todayStart = LocalDate.now().atStartOfDay(ZoneId.of("Asia/Shanghai")).toEpochSecond();
int countDate = Integer.parseInt(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
List<RiderOrderCount> counts = countMapper.selectList(
new LambdaQueryWrapper<RiderOrderCount>()
.in(RiderOrderCount::getUid, riderIds)
.eq(RiderOrderCount::getCountDate, countDate));
for (RiderOrderCount c : counts) {
map.put(c.getUid(), c.getOrders());
}
return map;
}
/** 获取骑手最后完成订单的时间 */
private long getLastCompleteTime(Long riderId) {
Orders last = ordersMapper.selectOne(
new LambdaQueryWrapper<Orders>()
.eq(Orders::getRiderId, riderId)
.eq(Orders::getStatus, 6)
.orderByDesc(Orders::getCompleteTime)
.select(Orders::getCompleteTime)
.last("LIMIT 1"));
return last != null && last.getCompleteTime() != null ? last.getCompleteTime() : 0;
}
/** 计算两点间的方位角(度数,0=北 90=东) */
private double calcBearing(double lat1, double lng1, double lat2, double lng2) {
double dLng = Math.toRadians(lng2 - lng1);
double lat1Rad = Math.toRadians(lat1);
double lat2Rad = Math.toRadians(lat2);
double y = Math.sin(dLng) * Math.cos(lat2Rad);
double x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng);
double bearing = Math.toDegrees(Math.atan2(y, x));
return (bearing + 360) % 360;
}
private double parseDouble(String s) {
try {
return s != null ? Double.parseDouble(s) : 0;
} catch (NumberFormatException e) {
return 0;
}
}
private void notifyOrderEvent(Long orderId, String event) {
try {
Orders order = ordersMapper.selectById(orderId);
if (order == null || order.getAppKey() == null || order.getAppKey().isBlank()) return;
Map<String, Object> payload = new HashMap<>();
payload.put("event", event);
payload.put("outOrderNo", order.getOutOrderNo());
payload.put("deliveryOrderId", order.getId());
payload.put("orderNo", order.getOrderNo());
payload.put("status", order.getStatus());
payload.put("riderId", order.getRiderId());
payload.put("dispatchRiderId", order.getDispatchRiderId());
payload.put("timestamp", System.currentTimeMillis() / 1000);
webhookService.send(event, orderId, objectMapper.writeValueAsString(payload));
} catch (Exception e) {
log.warn("派单事件通知失败 orderId={} event={}", orderId, event, e);
}
}
private record RiderScore(Long riderId, double score, double distance) {}
}