Commit a9f9dcb32bbebfc40f34ae29a7999c0a6ffe083f
1 parent
3edd6bb9
新增自动派单服务及规则配置接口
Showing
20 changed files
with
1185 additions
and
15 deletions
src/main/java/com/diligrp/rider/common/enums/DispatchConditionType.java
0 → 100644
| 1 | +package com.diligrp.rider.common.enums; | ||
| 2 | + | ||
| 3 | +import lombok.Getter; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 派单优先级条件类型枚举 | ||
| 7 | + */ | ||
| 8 | +@Getter | ||
| 9 | +public enum DispatchConditionType { | ||
| 10 | + DISTANCE("distance", "距离取单地址", true), | ||
| 11 | + DETOUR("detour", "顺路距离", true), | ||
| 12 | + WAIT("wait", "等待新订单时间", true), | ||
| 13 | + CURRENT_LOAD("currentLoad", "当前持单量", true), | ||
| 14 | + DAILY("daily", "当日总接单量", true), | ||
| 15 | + DIRECTION("direction", "目的地方向一致", false), | ||
| 16 | + ROOKIE("rookie", "新手骑手保护", true), | ||
| 17 | + PRAISE("praise", "好评率", true), | ||
| 18 | + AREA_MATCH("areaMatch", "区域匹配", false); | ||
| 19 | + | ||
| 20 | + private final String code; | ||
| 21 | + private final String desc; | ||
| 22 | + /** 是否需要阈值参数 */ | ||
| 23 | + private final boolean hasThreshold; | ||
| 24 | + | ||
| 25 | + DispatchConditionType(String code, String desc, boolean hasThreshold) { | ||
| 26 | + this.code = code; | ||
| 27 | + this.desc = desc; | ||
| 28 | + this.hasThreshold = hasThreshold; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + public static DispatchConditionType fromCode(String code) { | ||
| 32 | + for (DispatchConditionType type : values()) { | ||
| 33 | + if (type.code.equals(code)) return type; | ||
| 34 | + } | ||
| 35 | + return null; | ||
| 36 | + } | ||
| 37 | +} |
src/main/java/com/diligrp/rider/config/AuthInterceptor.java
| @@ -56,9 +56,11 @@ public class AuthInterceptor implements HandlerInterceptor { | @@ -56,9 +56,11 @@ public class AuthInterceptor implements HandlerInterceptor { | ||
| 56 | // 分站管理员:注入 cityId 供 Service 层做城市隔离 | 56 | // 分站管理员:注入 cityId 供 Service 层做城市隔离 |
| 57 | if ("substation".equals(role)) { | 57 | if ("substation".equals(role)) { |
| 58 | Substation sub = substationMapper.selectById(adminId); | 58 | Substation sub = substationMapper.selectById(adminId); |
| 59 | - if (sub != null) { | ||
| 60 | - request.setAttribute("cityId", sub.getCityId()); | 59 | + if (sub == null || sub.getCityId() == null || sub.getCityId() < 1) { |
| 60 | + writeError(response, 403, "当前分站账号未绑定有效城市"); | ||
| 61 | + return false; | ||
| 61 | } | 62 | } |
| 63 | + request.setAttribute("cityId", sub.getCityId()); | ||
| 62 | } | 64 | } |
| 63 | 65 | ||
| 64 | } else if (claims.get("riderId") != null) { | 66 | } else if (claims.get("riderId") != null) { |
src/main/java/com/diligrp/rider/controller/AdminDispatchRuleController.java
0 → 100644
| 1 | +package com.diligrp.rider.controller; | ||
| 2 | + | ||
| 3 | +import com.diligrp.rider.common.exception.BizException; | ||
| 4 | +import com.diligrp.rider.common.result.Result; | ||
| 5 | +import com.diligrp.rider.dto.DispatchRuleTemplateSaveDTO; | ||
| 6 | +import com.diligrp.rider.service.DispatchRuleService; | ||
| 7 | +import com.diligrp.rider.vo.DispatchRuleTemplateVO; | ||
| 8 | +import jakarta.servlet.http.HttpServletRequest; | ||
| 9 | +import lombok.Data; | ||
| 10 | +import lombok.RequiredArgsConstructor; | ||
| 11 | +import org.springframework.web.bind.annotation.*; | ||
| 12 | + | ||
| 13 | +import java.util.List; | ||
| 14 | + | ||
| 15 | +@RestController | ||
| 16 | +@RequestMapping("/api/admin/dispatch/rule") | ||
| 17 | +@RequiredArgsConstructor | ||
| 18 | +public class AdminDispatchRuleController { | ||
| 19 | + | ||
| 20 | + private final DispatchRuleService dispatchRuleService; | ||
| 21 | + | ||
| 22 | + /** 获取城市当前生效规则 */ | ||
| 23 | + @GetMapping("") | ||
| 24 | + public Result<DispatchRuleTemplateVO> getActiveRule(@RequestParam(required = false) Long cityId, | ||
| 25 | + HttpServletRequest request) { | ||
| 26 | + return Result.success(dispatchRuleService.getActiveRule(resolveCityId(cityId, request))); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + /** 获取城市所有模板列表 */ | ||
| 30 | + @GetMapping("/list") | ||
| 31 | + public Result<List<DispatchRuleTemplateVO>> listTemplates(@RequestParam(required = false) Long cityId, | ||
| 32 | + HttpServletRequest request) { | ||
| 33 | + return Result.success(dispatchRuleService.listTemplates(resolveCityId(cityId, request))); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + /** 保存/更新模板(含条件列表) */ | ||
| 37 | + @PostMapping("/save") | ||
| 38 | + public Result<Long> saveTemplate(@RequestBody DispatchRuleTemplateSaveDTO dto, HttpServletRequest request) { | ||
| 39 | + return Result.success(dispatchRuleService.saveTemplate(resolveCityId(dto != null ? dto.getCityId() : null, request), dto)); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + /** 激活模板 */ | ||
| 43 | + @PostMapping("/activate") | ||
| 44 | + public Result<Void> activateTemplate(@RequestBody ActivateReq req, | ||
| 45 | + @RequestParam(required = false) Long cityId, | ||
| 46 | + HttpServletRequest request) { | ||
| 47 | + dispatchRuleService.activateTemplate(resolveCityId(cityId, request), req.getTemplateId()); | ||
| 48 | + return Result.success(); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + /** 删除模板 */ | ||
| 52 | + @DeleteMapping("/{id}") | ||
| 53 | + public Result<Void> deleteTemplate(@PathVariable Long id, | ||
| 54 | + @RequestParam(required = false) Long cityId, | ||
| 55 | + HttpServletRequest request) { | ||
| 56 | + dispatchRuleService.deleteTemplate(resolveCityId(cityId, request), id); | ||
| 57 | + return Result.success(); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + /** 复制模板 */ | ||
| 61 | + @PostMapping("/copy") | ||
| 62 | + public Result<Long> copyTemplate(@RequestBody CopyReq req, | ||
| 63 | + @RequestParam(required = false) Long cityId, | ||
| 64 | + HttpServletRequest request) { | ||
| 65 | + return Result.success(dispatchRuleService.copyTemplate(resolveCityId(cityId, request), req.getTemplateId(), req.getNewName())); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + private Long resolveCityId(Long cityId, HttpServletRequest request) { | ||
| 69 | + if ("substation".equals(request.getAttribute("role"))) { | ||
| 70 | + Long requestCityId = (Long) request.getAttribute("cityId"); | ||
| 71 | + if (requestCityId == null || requestCityId < 1) { | ||
| 72 | + throw new BizException("当前账号未绑定城市"); | ||
| 73 | + } | ||
| 74 | + return requestCityId; | ||
| 75 | + } | ||
| 76 | + if (cityId == null || cityId < 1) { | ||
| 77 | + throw new BizException("城市不能为空"); | ||
| 78 | + } | ||
| 79 | + return cityId; | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + @Data | ||
| 83 | + static class ActivateReq { | ||
| 84 | + private Long templateId; | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + @Data | ||
| 88 | + static class CopyReq { | ||
| 89 | + private Long templateId; | ||
| 90 | + private String newName; | ||
| 91 | + } | ||
| 92 | +} |
src/main/java/com/diligrp/rider/controller/AdminFeePlanController.java
0 → 100644
| 1 | +package com.diligrp.rider.controller; | ||
| 2 | + | ||
| 3 | +import com.diligrp.rider.common.exception.BizException; | ||
| 4 | +import com.diligrp.rider.common.result.Result; | ||
| 5 | +import com.diligrp.rider.dto.DeliveryFeePlanPreviewDTO; | ||
| 6 | +import com.diligrp.rider.dto.DeliveryFeePlanSaveDTO; | ||
| 7 | +import com.diligrp.rider.service.DeliveryFeePlanService; | ||
| 8 | +import com.diligrp.rider.vo.DeliveryFeePlanDetailVO; | ||
| 9 | +import com.diligrp.rider.vo.DeliveryFeePlanVO; | ||
| 10 | +import com.diligrp.rider.vo.DeliveryFeeResultVO; | ||
| 11 | +import jakarta.servlet.http.HttpServletRequest; | ||
| 12 | +import lombok.RequiredArgsConstructor; | ||
| 13 | +import org.springframework.web.bind.annotation.*; | ||
| 14 | + | ||
| 15 | +import java.util.List; | ||
| 16 | + | ||
| 17 | +@RestController | ||
| 18 | +@RequestMapping("/api/admin/fee-plan") | ||
| 19 | +@RequiredArgsConstructor | ||
| 20 | +public class AdminFeePlanController { | ||
| 21 | + | ||
| 22 | + private final DeliveryFeePlanService deliveryFeePlanService; | ||
| 23 | + | ||
| 24 | + @GetMapping("/list") | ||
| 25 | + public Result<List<DeliveryFeePlanVO>> listPlans(@RequestParam(required = false) Long cityId, | ||
| 26 | + HttpServletRequest request) { | ||
| 27 | + return Result.success(deliveryFeePlanService.listPlans(resolveCityId(cityId, request))); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + @GetMapping("/{planId}") | ||
| 31 | + public Result<DeliveryFeePlanDetailVO> getPlanDetail(@PathVariable Long planId, | ||
| 32 | + @RequestParam(required = false) Long cityId, | ||
| 33 | + HttpServletRequest request) { | ||
| 34 | + Long effectiveCityId = resolveCityId(cityId, request); | ||
| 35 | + return Result.success(deliveryFeePlanService.getPlanDetail(effectiveCityId, planId)); | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + @PostMapping("") | ||
| 39 | + public Result<Long> createPlan(@RequestParam(required = false) Long cityId, | ||
| 40 | + @RequestBody DeliveryFeePlanSaveDTO dto, | ||
| 41 | + HttpServletRequest request) { | ||
| 42 | + return Result.success(deliveryFeePlanService.createPlan(resolveCityId(cityId, request), dto)); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + @PostMapping("/init-default") | ||
| 46 | + public Result<Long> initializeDefaultPlan(@RequestParam(required = false) Long cityId, | ||
| 47 | + HttpServletRequest request) { | ||
| 48 | + return Result.success(deliveryFeePlanService.initializeDefaultPlan(resolveCityId(cityId, request))); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @PutMapping("/{planId}") | ||
| 52 | + public Result<Void> updatePlan(@PathVariable Long planId, | ||
| 53 | + @RequestParam(required = false) Long cityId, | ||
| 54 | + @RequestBody DeliveryFeePlanSaveDTO dto, | ||
| 55 | + HttpServletRequest request) { | ||
| 56 | + deliveryFeePlanService.updatePlan(resolveCityId(cityId, request), planId, dto); | ||
| 57 | + return Result.success(); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + @PostMapping("/{planId}/copy") | ||
| 61 | + public Result<Long> copyPlan(@PathVariable Long planId, | ||
| 62 | + @RequestParam(required = false) Long cityId, | ||
| 63 | + HttpServletRequest request) { | ||
| 64 | + return Result.success(deliveryFeePlanService.copyPlan(resolveCityId(cityId, request), planId)); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + @PostMapping("/{planId}/default") | ||
| 68 | + public Result<Void> setDefaultPlan(@PathVariable Long planId, | ||
| 69 | + @RequestParam(required = false) Long cityId, | ||
| 70 | + HttpServletRequest request) { | ||
| 71 | + deliveryFeePlanService.setDefaultPlan(resolveCityId(cityId, request), planId); | ||
| 72 | + return Result.success(); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + @DeleteMapping("/{planId}") | ||
| 76 | + public Result<Void> deletePlan(@PathVariable Long planId, | ||
| 77 | + @RequestParam(required = false) Long cityId, | ||
| 78 | + HttpServletRequest request) { | ||
| 79 | + deliveryFeePlanService.deletePlan(resolveCityId(cityId, request), planId); | ||
| 80 | + return Result.success(); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + @PostMapping("/preview") | ||
| 84 | + public Result<DeliveryFeeResultVO> preview(@RequestParam(required = false) Long cityId, | ||
| 85 | + @RequestBody DeliveryFeePlanPreviewDTO dto, | ||
| 86 | + HttpServletRequest request) { | ||
| 87 | + return Result.success(deliveryFeePlanService.preview(resolveCityId(cityId, request), dto)); | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + private Long resolveCityId(Long cityId, HttpServletRequest request) { | ||
| 91 | + if ("substation".equals(request.getAttribute("role"))) { | ||
| 92 | + Long requestCityId = (Long) request.getAttribute("cityId"); | ||
| 93 | + if (requestCityId == null || requestCityId < 1) { | ||
| 94 | + throw new BizException("当前账号未绑定城市"); | ||
| 95 | + } | ||
| 96 | + return requestCityId; | ||
| 97 | + } | ||
| 98 | + if (cityId == null || cityId < 1) { | ||
| 99 | + throw new BizException("城市不能为空"); | ||
| 100 | + } | ||
| 101 | + return cityId; | ||
| 102 | + } | ||
| 103 | +} |
src/main/java/com/diligrp/rider/dto/DispatchRuleTemplateSaveDTO.java
0 → 100644
| 1 | +package com.diligrp.rider.dto; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.math.BigDecimal; | ||
| 6 | +import java.util.List; | ||
| 7 | + | ||
| 8 | +@Data | ||
| 9 | +public class DispatchRuleTemplateSaveDTO { | ||
| 10 | + | ||
| 11 | + /** 模板ID(新建时为null) */ | ||
| 12 | + private Long id; | ||
| 13 | + | ||
| 14 | + /** 城市ID */ | ||
| 15 | + private Long cityId; | ||
| 16 | + | ||
| 17 | + /** 模板名称 */ | ||
| 18 | + private String name; | ||
| 19 | + | ||
| 20 | + /** 抢单模式启用 */ | ||
| 21 | + private Integer grabEnabled; | ||
| 22 | + | ||
| 23 | + /** 抢单超时分钟数 */ | ||
| 24 | + private Integer grabTimeout; | ||
| 25 | + | ||
| 26 | + /** 抢单可见范围:1=区域骑手 2=全部 */ | ||
| 27 | + private Integer grabScope; | ||
| 28 | + | ||
| 29 | + /** 单人最大同时持单量 */ | ||
| 30 | + private Integer grabMaxPerRider; | ||
| 31 | + | ||
| 32 | + /** 同步开启自动派单 */ | ||
| 33 | + private Integer autoDispatch; | ||
| 34 | + | ||
| 35 | + /** 条件列表 */ | ||
| 36 | + private List<ConditionItem> conditions; | ||
| 37 | + | ||
| 38 | + @Data | ||
| 39 | + public static class ConditionItem { | ||
| 40 | + /** 条件类型 */ | ||
| 41 | + private String conditionType; | ||
| 42 | + /** 是否启用 */ | ||
| 43 | + private Integer enabled; | ||
| 44 | + /** 阈值 */ | ||
| 45 | + private BigDecimal thresholdValue; | ||
| 46 | + /** 排序 */ | ||
| 47 | + private Integer sortOrder; | ||
| 48 | + } | ||
| 49 | +} |
src/main/java/com/diligrp/rider/entity/DispatchRuleCondition.java
0 → 100644
| 1 | +package com.diligrp.rider.entity; | ||
| 2 | + | ||
| 3 | +import com.baomidou.mybatisplus.annotation.*; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.math.BigDecimal; | ||
| 7 | + | ||
| 8 | +@Data | ||
| 9 | +@TableName("dispatch_rule_condition") | ||
| 10 | +public class DispatchRuleCondition { | ||
| 11 | + | ||
| 12 | + @TableId(type = IdType.AUTO) | ||
| 13 | + private Long id; | ||
| 14 | + | ||
| 15 | + /** 所属模板ID */ | ||
| 16 | + private Long templateId; | ||
| 17 | + | ||
| 18 | + /** 条件类型 */ | ||
| 19 | + private String conditionType; | ||
| 20 | + | ||
| 21 | + /** 是否启用 */ | ||
| 22 | + private Integer enabled; | ||
| 23 | + | ||
| 24 | + /** 阈值 */ | ||
| 25 | + private BigDecimal thresholdValue; | ||
| 26 | + | ||
| 27 | + /** 优先级排序(小的优先) */ | ||
| 28 | + private Integer sortOrder; | ||
| 29 | + | ||
| 30 | + private Long createTime; | ||
| 31 | + private Long updateTime; | ||
| 32 | +} |
src/main/java/com/diligrp/rider/entity/DispatchRuleTemplate.java
0 → 100644
| 1 | +package com.diligrp.rider.entity; | ||
| 2 | + | ||
| 3 | +import com.baomidou.mybatisplus.annotation.*; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +@Data | ||
| 7 | +@TableName("dispatch_rule_template") | ||
| 8 | +public class DispatchRuleTemplate { | ||
| 9 | + | ||
| 10 | + @TableId(type = IdType.AUTO) | ||
| 11 | + private Long id; | ||
| 12 | + | ||
| 13 | + /** 城市ID */ | ||
| 14 | + private Long cityId; | ||
| 15 | + | ||
| 16 | + /** 模板名称 */ | ||
| 17 | + private String name; | ||
| 18 | + | ||
| 19 | + /** 是否当前生效:0=否 1=是 */ | ||
| 20 | + private Integer isActive; | ||
| 21 | + | ||
| 22 | + /** 抢单模式启用 */ | ||
| 23 | + private Integer grabEnabled; | ||
| 24 | + | ||
| 25 | + /** 抢单超时分钟数 */ | ||
| 26 | + private Integer grabTimeout; | ||
| 27 | + | ||
| 28 | + /** 抢单可见范围:1=区域骑手 2=全部 */ | ||
| 29 | + private Integer grabScope; | ||
| 30 | + | ||
| 31 | + /** 单人最大同时持单量 */ | ||
| 32 | + private Integer grabMaxPerRider; | ||
| 33 | + | ||
| 34 | + /** 同步开启自动派单 */ | ||
| 35 | + private Integer autoDispatch; | ||
| 36 | + | ||
| 37 | + private Long createTime; | ||
| 38 | + private Long updateTime; | ||
| 39 | +} |
src/main/java/com/diligrp/rider/entity/Orders.java
| @@ -147,4 +147,10 @@ public class Orders { | @@ -147,4 +147,10 @@ public class Orders { | ||
| 147 | 147 | ||
| 148 | /** 转单时间 */ | 148 | /** 转单时间 */ |
| 149 | private Long transTime; | 149 | private Long transTime; |
| 150 | + | ||
| 151 | + /** 系统派单时间 */ | ||
| 152 | + private Long dispatchTime; | ||
| 153 | + | ||
| 154 | + /** 系统指派骑手ID */ | ||
| 155 | + private Long dispatchRiderId; | ||
| 150 | } | 156 | } |
src/main/java/com/diligrp/rider/entity/Rider.java
| @@ -65,6 +65,12 @@ public class Rider { | @@ -65,6 +65,12 @@ public class Rider { | ||
| 65 | /** 手持身份证照片 */ | 65 | /** 手持身份证照片 */ |
| 66 | private String thumb; | 66 | private String thumb; |
| 67 | 67 | ||
| 68 | + /** 评分总分 */ | ||
| 69 | + private Integer starTotal; | ||
| 70 | + | ||
| 71 | + /** 评分次数 */ | ||
| 72 | + private Integer starCount; | ||
| 73 | + | ||
| 68 | @TableField(fill = FieldFill.INSERT) | 74 | @TableField(fill = FieldFill.INSERT) |
| 69 | private Long createTime; | 75 | private Long createTime; |
| 70 | } | 76 | } |
src/main/java/com/diligrp/rider/mapper/DispatchRuleConditionMapper.java
0 → 100644
| 1 | +package com.diligrp.rider.mapper; | ||
| 2 | + | ||
| 3 | +import com.baomidou.mybatisplus.core.mapper.BaseMapper; | ||
| 4 | +import com.diligrp.rider.entity.DispatchRuleCondition; | ||
| 5 | +import org.apache.ibatis.annotations.Mapper; | ||
| 6 | + | ||
| 7 | +@Mapper | ||
| 8 | +public interface DispatchRuleConditionMapper extends BaseMapper<DispatchRuleCondition> { | ||
| 9 | +} |
src/main/java/com/diligrp/rider/mapper/DispatchRuleTemplateMapper.java
0 → 100644
| 1 | +package com.diligrp.rider.mapper; | ||
| 2 | + | ||
| 3 | +import com.baomidou.mybatisplus.core.mapper.BaseMapper; | ||
| 4 | +import com.diligrp.rider.entity.DispatchRuleTemplate; | ||
| 5 | +import org.apache.ibatis.annotations.Mapper; | ||
| 6 | + | ||
| 7 | +@Mapper | ||
| 8 | +public interface DispatchRuleTemplateMapper extends BaseMapper<DispatchRuleTemplate> { | ||
| 9 | +} |
src/main/java/com/diligrp/rider/service/DispatchRuleService.java
0 → 100644
| 1 | +package com.diligrp.rider.service; | ||
| 2 | + | ||
| 3 | +import com.diligrp.rider.dto.DispatchRuleTemplateSaveDTO; | ||
| 4 | +import com.diligrp.rider.vo.DispatchRuleTemplateVO; | ||
| 5 | + | ||
| 6 | +import java.util.List; | ||
| 7 | + | ||
| 8 | +public interface DispatchRuleService { | ||
| 9 | + | ||
| 10 | + /** 获取城市当前生效的规则模板(含条件列表) */ | ||
| 11 | + DispatchRuleTemplateVO getActiveRule(Long cityId); | ||
| 12 | + | ||
| 13 | + /** 获取城市所有规则模板列表 */ | ||
| 14 | + List<DispatchRuleTemplateVO> listTemplates(Long cityId); | ||
| 15 | + | ||
| 16 | + /** 保存/更新规则模板(含条件列表,整体替换) */ | ||
| 17 | + Long saveTemplate(Long cityId, DispatchRuleTemplateSaveDTO dto); | ||
| 18 | + | ||
| 19 | + /** 激活指定模板(同城市其他模板自动失效) */ | ||
| 20 | + void activateTemplate(Long cityId, Long templateId); | ||
| 21 | + | ||
| 22 | + /** 删除模板(非激活状态才可删) */ | ||
| 23 | + void deleteTemplate(Long cityId, Long templateId); | ||
| 24 | + | ||
| 25 | + /** 复制模板 */ | ||
| 26 | + Long copyTemplate(Long cityId, Long templateId, String newName); | ||
| 27 | +} |
src/main/java/com/diligrp/rider/service/DispatchService.java
0 → 100644
src/main/java/com/diligrp/rider/service/impl/DispatchRuleServiceImpl.java
0 → 100644
| 1 | +package com.diligrp.rider.service.impl; | ||
| 2 | + | ||
| 3 | +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||
| 4 | +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; | ||
| 5 | +import com.diligrp.rider.common.enums.DispatchConditionType; | ||
| 6 | +import com.diligrp.rider.common.exception.BizException; | ||
| 7 | +import com.diligrp.rider.dto.DispatchRuleTemplateSaveDTO; | ||
| 8 | +import com.diligrp.rider.entity.DispatchRuleCondition; | ||
| 9 | +import com.diligrp.rider.entity.DispatchRuleTemplate; | ||
| 10 | +import com.diligrp.rider.mapper.DispatchRuleConditionMapper; | ||
| 11 | +import com.diligrp.rider.mapper.DispatchRuleTemplateMapper; | ||
| 12 | +import com.diligrp.rider.service.DispatchRuleService; | ||
| 13 | +import com.diligrp.rider.vo.DispatchRuleTemplateVO; | ||
| 14 | +import lombok.RequiredArgsConstructor; | ||
| 15 | +import org.springframework.stereotype.Service; | ||
| 16 | +import org.springframework.transaction.annotation.Transactional; | ||
| 17 | + | ||
| 18 | +import java.math.BigDecimal; | ||
| 19 | +import java.util.ArrayList; | ||
| 20 | +import java.util.List; | ||
| 21 | + | ||
| 22 | +@Service | ||
| 23 | +@RequiredArgsConstructor | ||
| 24 | +public class DispatchRuleServiceImpl implements DispatchRuleService { | ||
| 25 | + | ||
| 26 | + private final DispatchRuleTemplateMapper templateMapper; | ||
| 27 | + private final DispatchRuleConditionMapper conditionMapper; | ||
| 28 | + | ||
| 29 | + @Override | ||
| 30 | + public DispatchRuleTemplateVO getActiveRule(Long cityId) { | ||
| 31 | + DispatchRuleTemplate template = templateMapper.selectOne( | ||
| 32 | + new LambdaQueryWrapper<DispatchRuleTemplate>() | ||
| 33 | + .eq(DispatchRuleTemplate::getCityId, cityId) | ||
| 34 | + .eq(DispatchRuleTemplate::getIsActive, 1) | ||
| 35 | + .last("LIMIT 1")); | ||
| 36 | + if (template == null) return null; | ||
| 37 | + return toVO(template); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + @Override | ||
| 41 | + public List<DispatchRuleTemplateVO> listTemplates(Long cityId) { | ||
| 42 | + List<DispatchRuleTemplate> templates = templateMapper.selectList( | ||
| 43 | + new LambdaQueryWrapper<DispatchRuleTemplate>() | ||
| 44 | + .eq(DispatchRuleTemplate::getCityId, cityId) | ||
| 45 | + .orderByDesc(DispatchRuleTemplate::getIsActive) | ||
| 46 | + .orderByDesc(DispatchRuleTemplate::getUpdateTime)); | ||
| 47 | + List<DispatchRuleTemplateVO> result = new ArrayList<>(); | ||
| 48 | + for (DispatchRuleTemplate t : templates) { | ||
| 49 | + result.add(toVO(t)); | ||
| 50 | + } | ||
| 51 | + return result; | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + @Override | ||
| 55 | + @Transactional | ||
| 56 | + public Long saveTemplate(Long cityId, DispatchRuleTemplateSaveDTO dto) { | ||
| 57 | + if (cityId == null || cityId < 1) throw new BizException("城市ID不能为空"); | ||
| 58 | + if (dto == null) throw new BizException("请求参数不能为空"); | ||
| 59 | + if (dto.getName() == null || dto.getName().isBlank()) throw new BizException("模板名称不能为空"); | ||
| 60 | + | ||
| 61 | + long now = System.currentTimeMillis() / 1000; | ||
| 62 | + DispatchRuleTemplate template; | ||
| 63 | + | ||
| 64 | + if (dto.getId() != null) { | ||
| 65 | + template = requireTemplate(cityId, dto.getId()); | ||
| 66 | + } else { | ||
| 67 | + template = new DispatchRuleTemplate(); | ||
| 68 | + template.setCityId(cityId); | ||
| 69 | + template.setIsActive(0); | ||
| 70 | + template.setCreateTime(now); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + template.setName(dto.getName()); | ||
| 74 | + template.setGrabEnabled(dto.getGrabEnabled() != null ? dto.getGrabEnabled() : 1); | ||
| 75 | + template.setGrabTimeout(dto.getGrabTimeout() != null ? dto.getGrabTimeout() : 30); | ||
| 76 | + template.setGrabScope(dto.getGrabScope() != null ? dto.getGrabScope() : 1); | ||
| 77 | + template.setGrabMaxPerRider(dto.getGrabMaxPerRider() != null ? dto.getGrabMaxPerRider() : 3); | ||
| 78 | + template.setAutoDispatch(dto.getAutoDispatch() != null ? dto.getAutoDispatch() : 1); | ||
| 79 | + template.setUpdateTime(now); | ||
| 80 | + | ||
| 81 | + if (dto.getId() != null) { | ||
| 82 | + templateMapper.updateById(template); | ||
| 83 | + } else { | ||
| 84 | + templateMapper.insert(template); | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + // 整体替换条件列表 | ||
| 88 | + conditionMapper.delete(new LambdaQueryWrapper<DispatchRuleCondition>() | ||
| 89 | + .eq(DispatchRuleCondition::getTemplateId, template.getId())); | ||
| 90 | + | ||
| 91 | + if (dto.getConditions() != null) { | ||
| 92 | + for (DispatchRuleTemplateSaveDTO.ConditionItem item : dto.getConditions()) { | ||
| 93 | + DispatchConditionType type = DispatchConditionType.fromCode(item.getConditionType()); | ||
| 94 | + if (type == null) continue; | ||
| 95 | + | ||
| 96 | + DispatchRuleCondition condition = new DispatchRuleCondition(); | ||
| 97 | + condition.setTemplateId(template.getId()); | ||
| 98 | + condition.setConditionType(item.getConditionType()); | ||
| 99 | + condition.setEnabled(item.getEnabled() != null ? item.getEnabled() : 1); | ||
| 100 | + condition.setThresholdValue(item.getThresholdValue() != null ? item.getThresholdValue() : BigDecimal.ZERO); | ||
| 101 | + condition.setSortOrder(item.getSortOrder() != null ? item.getSortOrder() : 0); | ||
| 102 | + condition.setCreateTime(now); | ||
| 103 | + condition.setUpdateTime(now); | ||
| 104 | + conditionMapper.insert(condition); | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + return template.getId(); | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + @Override | ||
| 112 | + @Transactional | ||
| 113 | + public void activateTemplate(Long cityId, Long templateId) { | ||
| 114 | + DispatchRuleTemplate template = requireTemplate(cityId, templateId); | ||
| 115 | + | ||
| 116 | + templateMapper.update(null, new LambdaUpdateWrapper<DispatchRuleTemplate>() | ||
| 117 | + .eq(DispatchRuleTemplate::getCityId, template.getCityId()) | ||
| 118 | + .set(DispatchRuleTemplate::getIsActive, 0)); | ||
| 119 | + | ||
| 120 | + templateMapper.update(null, new LambdaUpdateWrapper<DispatchRuleTemplate>() | ||
| 121 | + .eq(DispatchRuleTemplate::getId, templateId) | ||
| 122 | + .eq(DispatchRuleTemplate::getCityId, cityId) | ||
| 123 | + .set(DispatchRuleTemplate::getIsActive, 1) | ||
| 124 | + .set(DispatchRuleTemplate::getUpdateTime, System.currentTimeMillis() / 1000)); | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + @Override | ||
| 128 | + @Transactional | ||
| 129 | + public void deleteTemplate(Long cityId, Long templateId) { | ||
| 130 | + DispatchRuleTemplate template = requireTemplate(cityId, templateId); | ||
| 131 | + if (template.getIsActive() == 1) throw new BizException("不能删除当前生效的模板"); | ||
| 132 | + | ||
| 133 | + conditionMapper.delete(new LambdaQueryWrapper<DispatchRuleCondition>() | ||
| 134 | + .eq(DispatchRuleCondition::getTemplateId, templateId)); | ||
| 135 | + templateMapper.deleteById(templateId); | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + @Override | ||
| 139 | + @Transactional | ||
| 140 | + public Long copyTemplate(Long cityId, Long templateId, String newName) { | ||
| 141 | + DispatchRuleTemplate source = requireTemplate(cityId, templateId); | ||
| 142 | + | ||
| 143 | + long now = System.currentTimeMillis() / 1000; | ||
| 144 | + | ||
| 145 | + DispatchRuleTemplate copy = new DispatchRuleTemplate(); | ||
| 146 | + copy.setCityId(cityId); | ||
| 147 | + copy.setName(newName != null && !newName.isBlank() ? newName : source.getName() + "(副本)"); | ||
| 148 | + copy.setIsActive(0); | ||
| 149 | + copy.setGrabEnabled(source.getGrabEnabled()); | ||
| 150 | + copy.setGrabTimeout(source.getGrabTimeout()); | ||
| 151 | + copy.setGrabScope(source.getGrabScope()); | ||
| 152 | + copy.setGrabMaxPerRider(source.getGrabMaxPerRider()); | ||
| 153 | + copy.setAutoDispatch(source.getAutoDispatch()); | ||
| 154 | + copy.setCreateTime(now); | ||
| 155 | + copy.setUpdateTime(now); | ||
| 156 | + templateMapper.insert(copy); | ||
| 157 | + | ||
| 158 | + // 复制条件 | ||
| 159 | + List<DispatchRuleCondition> conditions = conditionMapper.selectList( | ||
| 160 | + new LambdaQueryWrapper<DispatchRuleCondition>() | ||
| 161 | + .eq(DispatchRuleCondition::getTemplateId, templateId)); | ||
| 162 | + for (DispatchRuleCondition c : conditions) { | ||
| 163 | + DispatchRuleCondition nc = new DispatchRuleCondition(); | ||
| 164 | + nc.setTemplateId(copy.getId()); | ||
| 165 | + nc.setConditionType(c.getConditionType()); | ||
| 166 | + nc.setEnabled(c.getEnabled()); | ||
| 167 | + nc.setThresholdValue(c.getThresholdValue()); | ||
| 168 | + nc.setSortOrder(c.getSortOrder()); | ||
| 169 | + nc.setCreateTime(now); | ||
| 170 | + nc.setUpdateTime(now); | ||
| 171 | + conditionMapper.insert(nc); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + return copy.getId(); | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + private DispatchRuleTemplate requireTemplate(Long cityId, Long templateId) { | ||
| 178 | + if (cityId == null || cityId < 1) throw new BizException("城市ID不能为空"); | ||
| 179 | + DispatchRuleTemplate template = templateMapper.selectOne( | ||
| 180 | + new LambdaQueryWrapper<DispatchRuleTemplate>() | ||
| 181 | + .eq(DispatchRuleTemplate::getId, templateId) | ||
| 182 | + .eq(DispatchRuleTemplate::getCityId, cityId) | ||
| 183 | + .last("LIMIT 1")); | ||
| 184 | + if (template == null) throw new BizException("模板不存在"); | ||
| 185 | + return template; | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + private DispatchRuleTemplateVO toVO(DispatchRuleTemplate template) { | ||
| 189 | + DispatchRuleTemplateVO vo = new DispatchRuleTemplateVO(); | ||
| 190 | + vo.setId(template.getId()); | ||
| 191 | + vo.setCityId(template.getCityId()); | ||
| 192 | + vo.setName(template.getName()); | ||
| 193 | + vo.setIsActive(template.getIsActive()); | ||
| 194 | + vo.setGrabEnabled(template.getGrabEnabled()); | ||
| 195 | + vo.setGrabTimeout(template.getGrabTimeout()); | ||
| 196 | + vo.setGrabScope(template.getGrabScope()); | ||
| 197 | + vo.setGrabMaxPerRider(template.getGrabMaxPerRider()); | ||
| 198 | + vo.setAutoDispatch(template.getAutoDispatch()); | ||
| 199 | + vo.setCreateTime(template.getCreateTime()); | ||
| 200 | + vo.setUpdateTime(template.getUpdateTime()); | ||
| 201 | + | ||
| 202 | + List<DispatchRuleCondition> conditions = conditionMapper.selectList( | ||
| 203 | + new LambdaQueryWrapper<DispatchRuleCondition>() | ||
| 204 | + .eq(DispatchRuleCondition::getTemplateId, template.getId()) | ||
| 205 | + .orderByAsc(DispatchRuleCondition::getSortOrder)); | ||
| 206 | + | ||
| 207 | + List<DispatchRuleTemplateVO.ConditionItem> items = new ArrayList<>(); | ||
| 208 | + for (DispatchRuleCondition c : conditions) { | ||
| 209 | + DispatchRuleTemplateVO.ConditionItem item = new DispatchRuleTemplateVO.ConditionItem(); | ||
| 210 | + item.setId(c.getId()); | ||
| 211 | + item.setConditionType(c.getConditionType()); | ||
| 212 | + item.setEnabled(c.getEnabled()); | ||
| 213 | + item.setThresholdValue(c.getThresholdValue()); | ||
| 214 | + item.setSortOrder(c.getSortOrder()); | ||
| 215 | + | ||
| 216 | + DispatchConditionType type = DispatchConditionType.fromCode(c.getConditionType()); | ||
| 217 | + if (type != null) { | ||
| 218 | + item.setConditionDesc(type.getDesc()); | ||
| 219 | + item.setHasThreshold(type.isHasThreshold()); | ||
| 220 | + } | ||
| 221 | + items.add(item); | ||
| 222 | + } | ||
| 223 | + vo.setConditions(items); | ||
| 224 | + return vo; | ||
| 225 | + } | ||
| 226 | +} |
src/main/java/com/diligrp/rider/service/impl/DispatchServiceImpl.java
0 → 100644
| 1 | +package com.diligrp.rider.service.impl; | ||
| 2 | + | ||
| 3 | +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||
| 4 | +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; | ||
| 5 | +import com.diligrp.rider.common.enums.DispatchConditionType; | ||
| 6 | +import com.diligrp.rider.entity.*; | ||
| 7 | +import com.diligrp.rider.mapper.*; | ||
| 8 | +import com.diligrp.rider.service.DispatchRuleService; | ||
| 9 | +import com.diligrp.rider.service.DispatchService; | ||
| 10 | +import com.diligrp.rider.service.WebhookService; | ||
| 11 | +import com.diligrp.rider.util.GeoUtil; | ||
| 12 | +import com.diligrp.rider.vo.DispatchRuleTemplateVO; | ||
| 13 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 14 | +import lombok.RequiredArgsConstructor; | ||
| 15 | +import lombok.extern.slf4j.Slf4j; | ||
| 16 | +import org.springframework.stereotype.Service; | ||
| 17 | +import org.springframework.transaction.annotation.Transactional; | ||
| 18 | + | ||
| 19 | +import java.math.BigDecimal; | ||
| 20 | +import java.time.LocalDate; | ||
| 21 | +import java.time.ZoneId; | ||
| 22 | +import java.time.format.DateTimeFormatter; | ||
| 23 | +import java.util.*; | ||
| 24 | + | ||
| 25 | +@Slf4j | ||
| 26 | +@Service | ||
| 27 | +@RequiredArgsConstructor | ||
| 28 | +public class DispatchServiceImpl implements DispatchService { | ||
| 29 | + | ||
| 30 | + private final DispatchRuleService dispatchRuleService; | ||
| 31 | + private final RiderMapper riderMapper; | ||
| 32 | + private final RiderLocationMapper locationMapper; | ||
| 33 | + private final OrdersMapper ordersMapper; | ||
| 34 | + private final RiderOrderCountMapper countMapper; | ||
| 35 | + private final RiderOrderRefuseMapper refuseMapper; | ||
| 36 | + private final WebhookService webhookService; | ||
| 37 | + private final ObjectMapper objectMapper; | ||
| 38 | + | ||
| 39 | + @Override | ||
| 40 | + @Transactional | ||
| 41 | + public Long dispatch(Orders order) { | ||
| 42 | + DispatchRuleTemplateVO rule = dispatchRuleService.getActiveRule(order.getCityId()); | ||
| 43 | + if (rule == null) { | ||
| 44 | + log.debug("城市 {} 无生效调度规则,跳过派单", order.getCityId()); | ||
| 45 | + return null; | ||
| 46 | + } | ||
| 47 | + if (rule.getAutoDispatch() == null || rule.getAutoDispatch() != 1) { | ||
| 48 | + return null; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // 1. 获取候选骑手:同城市、在线、审核通过 | ||
| 52 | + List<Rider> candidates = riderMapper.selectList( | ||
| 53 | + new LambdaQueryWrapper<Rider>() | ||
| 54 | + .eq(Rider::getCityId, order.getCityId()) | ||
| 55 | + .eq(Rider::getIsRest, 0) | ||
| 56 | + .eq(Rider::getUserStatus, 1) | ||
| 57 | + .eq(Rider::getStatus, 1)); | ||
| 58 | + if (candidates.isEmpty()) { | ||
| 59 | + log.debug("城市 {} 无在线骑手", order.getCityId()); | ||
| 60 | + return null; | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + // 排除已拒单的骑手 | ||
| 64 | + List<Long> refusedRiderIds = getRefusedRiderIds(order.getId()); | ||
| 65 | + if (!refusedRiderIds.isEmpty()) { | ||
| 66 | + candidates.removeIf(r -> refusedRiderIds.contains(r.getId())); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + // 排除转出该单的骑手 | ||
| 70 | + if (order.getOldRiderId() != null && order.getOldRiderId() > 0) { | ||
| 71 | + candidates.removeIf(r -> r.getId().equals(order.getOldRiderId())); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + if (candidates.isEmpty()) return null; | ||
| 75 | + | ||
| 76 | + // 2. 获取骑手位置 | ||
| 77 | + List<Long> riderIds = candidates.stream().map(Rider::getId).toList(); | ||
| 78 | + Map<Long, RiderLocation> locationMap = getLocationMap(riderIds); | ||
| 79 | + | ||
| 80 | + // 3. 获取启用的条件列表(按 sortOrder 升序) | ||
| 81 | + List<DispatchRuleTemplateVO.ConditionItem> enabledConditions = new ArrayList<>(); | ||
| 82 | + if (rule.getConditions() != null) { | ||
| 83 | + for (DispatchRuleTemplateVO.ConditionItem c : rule.getConditions()) { | ||
| 84 | + if (c.getEnabled() != null && c.getEnabled() == 1) { | ||
| 85 | + enabledConditions.add(c); | ||
| 86 | + } | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + // 4. 获取订单取货点坐标 | ||
| 91 | + double orderLat = parseDouble(order.getFLat()); | ||
| 92 | + double orderLng = parseDouble(order.getFLng()); | ||
| 93 | + double orderTLat = parseDouble(order.getTLat()); | ||
| 94 | + double orderTLng = parseDouble(order.getTLng()); | ||
| 95 | + | ||
| 96 | + // 5. 预加载每个骑手的统计数据 | ||
| 97 | + Map<Long, Integer> currentLoadMap = getCurrentLoadMap(riderIds); | ||
| 98 | + Map<Long, Integer> dailyCountMap = getDailyCountMap(riderIds); | ||
| 99 | + | ||
| 100 | + // 6. 对每个候选骑手评分 | ||
| 101 | + int totalConditions = enabledConditions.size(); | ||
| 102 | + List<RiderScore> scores = new ArrayList<>(); | ||
| 103 | + | ||
| 104 | + for (Rider rider : candidates) { | ||
| 105 | + RiderLocation loc = locationMap.get(rider.getId()); | ||
| 106 | + if (loc == null) continue; // 无位置信息的骑手不参与派单 | ||
| 107 | + | ||
| 108 | + double riderLat = parseDouble(loc.getLat()); | ||
| 109 | + double riderLng = parseDouble(loc.getLng()); | ||
| 110 | + double distanceToPickup = GeoUtil.calcDistanceKm(riderLat, riderLng, orderLat, orderLng); | ||
| 111 | + | ||
| 112 | + // 持单量检查(强制过滤) | ||
| 113 | + int currentLoad = currentLoadMap.getOrDefault(rider.getId(), 0); | ||
| 114 | + if (rule.getGrabMaxPerRider() != null && currentLoad >= rule.getGrabMaxPerRider()) { | ||
| 115 | + continue; // 持单已满,跳过 | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + double score = 0; | ||
| 119 | + boolean filtered = false; | ||
| 120 | + | ||
| 121 | + for (int i = 0; i < enabledConditions.size(); i++) { | ||
| 122 | + DispatchRuleTemplateVO.ConditionItem cond = enabledConditions.get(i); | ||
| 123 | + double weight = totalConditions - i; // 排在前面的权重更高 | ||
| 124 | + double condScore = evaluateCondition(cond, rider, loc, order, | ||
| 125 | + distanceToPickup, riderLat, riderLng, orderLat, orderLng, orderTLat, orderTLng, | ||
| 126 | + currentLoad, dailyCountMap.getOrDefault(rider.getId(), 0)); | ||
| 127 | + | ||
| 128 | + if (condScore < 0) { | ||
| 129 | + // 负分表示强制不通过(如距离超出阈值) | ||
| 130 | + filtered = true; | ||
| 131 | + break; | ||
| 132 | + } | ||
| 133 | + score += condScore * weight; | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + if (!filtered) { | ||
| 137 | + scores.add(new RiderScore(rider.getId(), score, distanceToPickup)); | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + if (scores.isEmpty()) { | ||
| 142 | + log.debug("订单 {} 无匹配骑手", order.getId()); | ||
| 143 | + return null; | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + // 7. 选择最优骑手:分数最高,平分取距离最近 | ||
| 147 | + scores.sort((a, b) -> { | ||
| 148 | + int cmp = Double.compare(b.score, a.score); | ||
| 149 | + if (cmp != 0) return cmp; | ||
| 150 | + return Double.compare(a.distance, b.distance); | ||
| 151 | + }); | ||
| 152 | + | ||
| 153 | + Long bestRiderId = scores.get(0).riderId; | ||
| 154 | + | ||
| 155 | + // 8. 指派订单 | ||
| 156 | + long now = System.currentTimeMillis() / 1000; | ||
| 157 | + int updated = ordersMapper.update(null, new LambdaUpdateWrapper<Orders>() | ||
| 158 | + .eq(Orders::getId, order.getId()) | ||
| 159 | + .eq(Orders::getStatus, 2) | ||
| 160 | + .eq(Orders::getRiderId, 0) | ||
| 161 | + .set(Orders::getRiderId, bestRiderId) | ||
| 162 | + .set(Orders::getStatus, 3) | ||
| 163 | + .set(Orders::getGrapTime, now) | ||
| 164 | + .set(Orders::getDispatchTime, now) | ||
| 165 | + .set(Orders::getDispatchRiderId, bestRiderId) | ||
| 166 | + .set(Orders::getOldRiderId, bestRiderId)); | ||
| 167 | + | ||
| 168 | + if (updated == 0) { | ||
| 169 | + log.debug("订单 {} 派单 CAS 失败(可能已被抢单)", order.getId()); | ||
| 170 | + return null; | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + log.info("订单 {} 自动派单给骑手 {},评分={}", order.getId(), bestRiderId, scores.get(0).score); | ||
| 174 | + | ||
| 175 | + // 通知接入方 | ||
| 176 | + notifyOrderEvent(order.getId(), "order.dispatched"); | ||
| 177 | + | ||
| 178 | + // 清除拒单记录 | ||
| 179 | + refuseMapper.delete(new LambdaQueryWrapper<RiderOrderRefuse>() | ||
| 180 | + .eq(RiderOrderRefuse::getOid, order.getId())); | ||
| 181 | + | ||
| 182 | + return bestRiderId; | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + /** | ||
| 186 | + * 评估单个条件,返回得分 | ||
| 187 | + * @return 得分:1.0=完全匹配, 0~1=部分匹配, -1=强制不通过 | ||
| 188 | + */ | ||
| 189 | + private double evaluateCondition(DispatchRuleTemplateVO.ConditionItem cond, | ||
| 190 | + Rider rider, RiderLocation loc, Orders order, | ||
| 191 | + double distanceToPickup, | ||
| 192 | + double riderLat, double riderLng, | ||
| 193 | + double orderLat, double orderLng, | ||
| 194 | + double orderTLat, double orderTLng, | ||
| 195 | + int currentLoad, int dailyCount) { | ||
| 196 | + DispatchConditionType type = DispatchConditionType.fromCode(cond.getConditionType()); | ||
| 197 | + if (type == null) return 0; | ||
| 198 | + | ||
| 199 | + double threshold = cond.getThresholdValue() != null ? cond.getThresholdValue().doubleValue() : 0; | ||
| 200 | + | ||
| 201 | + return switch (type) { | ||
| 202 | + case DISTANCE -> { | ||
| 203 | + // 距离取单地址 ≤ N 公里,超过则过滤 | ||
| 204 | + if (threshold > 0 && distanceToPickup > threshold) yield -1; | ||
| 205 | + // 距离越近分越高(归一化到 0~1) | ||
| 206 | + yield threshold > 0 ? Math.max(0, 1.0 - distanceToPickup / threshold) : 1.0; | ||
| 207 | + } | ||
| 208 | + case DETOUR -> { | ||
| 209 | + // 顺路距离:骑手当前有在途订单时,计算绕路程度 | ||
| 210 | + // 简化实现:以骑手到取货点的距离作为近似 | ||
| 211 | + if (threshold > 0 && distanceToPickup > threshold) yield -1; | ||
| 212 | + yield threshold > 0 ? Math.max(0, 1.0 - distanceToPickup / threshold) : 1.0; | ||
| 213 | + } | ||
| 214 | + case WAIT -> { | ||
| 215 | + // 等待新订单时间 ≥ N 分钟:骑手最后完成订单距今 | ||
| 216 | + long lastCompleteTime = getLastCompleteTime(rider.getId()); | ||
| 217 | + long now = System.currentTimeMillis() / 1000; | ||
| 218 | + double waitMinutes = lastCompleteTime > 0 ? (now - lastCompleteTime) / 60.0 : 999; | ||
| 219 | + yield waitMinutes >= threshold ? 1.0 : waitMinutes / threshold; | ||
| 220 | + } | ||
| 221 | + case CURRENT_LOAD -> { | ||
| 222 | + // 当前持单量 < N | ||
| 223 | + if (threshold > 0 && currentLoad >= threshold) yield -1; | ||
| 224 | + yield threshold > 0 ? Math.max(0, 1.0 - currentLoad / threshold) : 1.0; | ||
| 225 | + } | ||
| 226 | + case DAILY -> { | ||
| 227 | + // 当日总接单量 < N | ||
| 228 | + if (threshold > 0 && dailyCount >= threshold) yield -1; | ||
| 229 | + yield threshold > 0 ? Math.max(0, 1.0 - dailyCount / threshold) : 1.0; | ||
| 230 | + } | ||
| 231 | + case DIRECTION -> { | ||
| 232 | + // 目的地方向一致性:骑手位置到订单取货点的方向 vs 取货点到收货点的方向 | ||
| 233 | + double bearingToPickup = calcBearing(riderLat, riderLng, orderLat, orderLng); | ||
| 234 | + double bearingPickupToDest = calcBearing(orderLat, orderLng, orderTLat, orderTLng); | ||
| 235 | + double diff = Math.abs(bearingToPickup - bearingPickupToDest); | ||
| 236 | + if (diff > 180) diff = 360 - diff; | ||
| 237 | + // 方向差 ≤ 45度算顺路 | ||
| 238 | + yield diff <= 45 ? 1.0 : (diff <= 90 ? 0.5 : 0); | ||
| 239 | + } | ||
| 240 | + case ROOKIE -> { | ||
| 241 | + // 新手保护:入职 ≤ N 天的骑手优先 | ||
| 242 | + long createTime = rider.getCreateTime() != null ? rider.getCreateTime() : 0; | ||
| 243 | + long now = System.currentTimeMillis() / 1000; | ||
| 244 | + double daysRegistered = (now - createTime) / 86400.0; | ||
| 245 | + yield daysRegistered <= threshold ? 1.0 : 0; | ||
| 246 | + } | ||
| 247 | + case PRAISE -> { | ||
| 248 | + // 好评率 ≥ N% | ||
| 249 | + int starTotal = rider.getStarTotal() != null ? rider.getStarTotal() : 0; | ||
| 250 | + int starCount = rider.getStarCount() != null ? rider.getStarCount() : 0; | ||
| 251 | + if (starCount == 0) yield 0.5; // 无评价的骑手给中间分 | ||
| 252 | + double avgStar = (double) starTotal / starCount; | ||
| 253 | + double praiseRate = avgStar / 5.0 * 100; // 转换为百分比 | ||
| 254 | + yield praiseRate >= threshold ? 1.0 : praiseRate / threshold; | ||
| 255 | + } | ||
| 256 | + case AREA_MATCH -> { | ||
| 257 | + // 区域匹配:骑手当前所在城市与订单城市匹配 | ||
| 258 | + // 目前按 city_id 匹配(候选骑手已按 cityId 过滤,所以这里总是匹配的) | ||
| 259 | + // 后续可扩展为更细粒度的区域 | ||
| 260 | + yield 1.0; | ||
| 261 | + } | ||
| 262 | + }; | ||
| 263 | + } | ||
| 264 | + | ||
| 265 | + // ---- 辅助方法 ---- | ||
| 266 | + | ||
| 267 | + private List<Long> getRefusedRiderIds(Long orderId) { | ||
| 268 | + List<RiderOrderRefuse> refuses = refuseMapper.selectList( | ||
| 269 | + new LambdaQueryWrapper<RiderOrderRefuse>() | ||
| 270 | + .eq(RiderOrderRefuse::getOid, orderId) | ||
| 271 | + .select(RiderOrderRefuse::getRiderId)); | ||
| 272 | + return refuses.stream().map(RiderOrderRefuse::getRiderId).toList(); | ||
| 273 | + } | ||
| 274 | + | ||
| 275 | + private Map<Long, RiderLocation> getLocationMap(List<Long> riderIds) { | ||
| 276 | + if (riderIds.isEmpty()) return Map.of(); | ||
| 277 | + List<RiderLocation> locations = locationMapper.selectList( | ||
| 278 | + new LambdaQueryWrapper<RiderLocation>().in(RiderLocation::getUid, riderIds)); | ||
| 279 | + Map<Long, RiderLocation> map = new HashMap<>(); | ||
| 280 | + for (RiderLocation loc : locations) { | ||
| 281 | + map.put(loc.getUid(), loc); | ||
| 282 | + } | ||
| 283 | + return map; | ||
| 284 | + } | ||
| 285 | + | ||
| 286 | + /** 获取每个骑手当前持单量(status=3 或 4 的订单数) */ | ||
| 287 | + private Map<Long, Integer> getCurrentLoadMap(List<Long> riderIds) { | ||
| 288 | + Map<Long, Integer> map = new HashMap<>(); | ||
| 289 | + if (riderIds.isEmpty()) return map; | ||
| 290 | + List<Orders> inProgress = ordersMapper.selectList( | ||
| 291 | + new LambdaQueryWrapper<Orders>() | ||
| 292 | + .in(Orders::getRiderId, riderIds) | ||
| 293 | + .in(Orders::getStatus, List.of(3, 4)) | ||
| 294 | + .select(Orders::getRiderId)); | ||
| 295 | + for (Orders o : inProgress) { | ||
| 296 | + map.merge(o.getRiderId(), 1, Integer::sum); | ||
| 297 | + } | ||
| 298 | + return map; | ||
| 299 | + } | ||
| 300 | + | ||
| 301 | + /** 获取每个骑手当日接单量 */ | ||
| 302 | + private Map<Long, Integer> getDailyCountMap(List<Long> riderIds) { | ||
| 303 | + Map<Long, Integer> map = new HashMap<>(); | ||
| 304 | + if (riderIds.isEmpty()) return map; | ||
| 305 | + long todayStart = LocalDate.now().atStartOfDay(ZoneId.of("Asia/Shanghai")).toEpochSecond(); | ||
| 306 | + int countDate = Integer.parseInt(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))); | ||
| 307 | + List<RiderOrderCount> counts = countMapper.selectList( | ||
| 308 | + new LambdaQueryWrapper<RiderOrderCount>() | ||
| 309 | + .in(RiderOrderCount::getUid, riderIds) | ||
| 310 | + .eq(RiderOrderCount::getCountDate, countDate)); | ||
| 311 | + for (RiderOrderCount c : counts) { | ||
| 312 | + map.put(c.getUid(), c.getOrders()); | ||
| 313 | + } | ||
| 314 | + return map; | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + /** 获取骑手最后完成订单的时间 */ | ||
| 318 | + private long getLastCompleteTime(Long riderId) { | ||
| 319 | + Orders last = ordersMapper.selectOne( | ||
| 320 | + new LambdaQueryWrapper<Orders>() | ||
| 321 | + .eq(Orders::getRiderId, riderId) | ||
| 322 | + .eq(Orders::getStatus, 6) | ||
| 323 | + .orderByDesc(Orders::getCompleteTime) | ||
| 324 | + .select(Orders::getCompleteTime) | ||
| 325 | + .last("LIMIT 1")); | ||
| 326 | + return last != null && last.getCompleteTime() != null ? last.getCompleteTime() : 0; | ||
| 327 | + } | ||
| 328 | + | ||
| 329 | + /** 计算两点间的方位角(度数,0=北 90=东) */ | ||
| 330 | + private double calcBearing(double lat1, double lng1, double lat2, double lng2) { | ||
| 331 | + double dLng = Math.toRadians(lng2 - lng1); | ||
| 332 | + double lat1Rad = Math.toRadians(lat1); | ||
| 333 | + double lat2Rad = Math.toRadians(lat2); | ||
| 334 | + double y = Math.sin(dLng) * Math.cos(lat2Rad); | ||
| 335 | + double x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng); | ||
| 336 | + double bearing = Math.toDegrees(Math.atan2(y, x)); | ||
| 337 | + return (bearing + 360) % 360; | ||
| 338 | + } | ||
| 339 | + | ||
| 340 | + private double parseDouble(String s) { | ||
| 341 | + try { | ||
| 342 | + return s != null ? Double.parseDouble(s) : 0; | ||
| 343 | + } catch (NumberFormatException e) { | ||
| 344 | + return 0; | ||
| 345 | + } | ||
| 346 | + } | ||
| 347 | + | ||
| 348 | + private void notifyOrderEvent(Long orderId, String event) { | ||
| 349 | + try { | ||
| 350 | + Orders order = ordersMapper.selectById(orderId); | ||
| 351 | + if (order == null || order.getAppKey() == null || order.getAppKey().isBlank()) return; | ||
| 352 | + Map<String, Object> payload = new HashMap<>(); | ||
| 353 | + payload.put("event", event); | ||
| 354 | + payload.put("outOrderNo", order.getOutOrderNo()); | ||
| 355 | + payload.put("deliveryOrderId", order.getId()); | ||
| 356 | + payload.put("orderNo", order.getOrderNo()); | ||
| 357 | + payload.put("status", order.getStatus()); | ||
| 358 | + payload.put("riderId", order.getRiderId()); | ||
| 359 | + payload.put("dispatchRiderId", order.getDispatchRiderId()); | ||
| 360 | + payload.put("timestamp", System.currentTimeMillis() / 1000); | ||
| 361 | + webhookService.send(event, orderId, objectMapper.writeValueAsString(payload)); | ||
| 362 | + } catch (Exception e) { | ||
| 363 | + log.warn("派单事件通知失败 orderId={} event={}", orderId, event, e); | ||
| 364 | + } | ||
| 365 | + } | ||
| 366 | + | ||
| 367 | + private record RiderScore(Long riderId, double score, double distance) {} | ||
| 368 | +} |
src/main/java/com/diligrp/rider/service/impl/RiderOrderServiceImpl.java
| @@ -7,11 +7,11 @@ import com.diligrp.rider.mapper.*; | @@ -7,11 +7,11 @@ import com.diligrp.rider.mapper.*; | ||
| 7 | import com.fasterxml.jackson.core.type.TypeReference; | 7 | import com.fasterxml.jackson.core.type.TypeReference; |
| 8 | import com.fasterxml.jackson.databind.ObjectMapper; | 8 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 9 | import com.diligrp.rider.common.exception.BizException; | 9 | import com.diligrp.rider.common.exception.BizException; |
| 10 | -import com.diligrp.rider.entity.*; | ||
| 11 | -import com.diligrp.rider.mapper.*; | 10 | +import com.diligrp.rider.service.DispatchRuleService; |
| 12 | import com.diligrp.rider.service.RiderLevelService; | 11 | import com.diligrp.rider.service.RiderLevelService; |
| 13 | import com.diligrp.rider.service.RiderOrderService; | 12 | import com.diligrp.rider.service.RiderOrderService; |
| 14 | import com.diligrp.rider.service.WebhookService; | 13 | import com.diligrp.rider.service.WebhookService; |
| 14 | +import com.diligrp.rider.vo.DispatchRuleTemplateVO; | ||
| 15 | import com.diligrp.rider.vo.OrderVO; | 15 | import com.diligrp.rider.vo.OrderVO; |
| 16 | import com.diligrp.rider.vo.RiderMonthCountVO; | 16 | import com.diligrp.rider.vo.RiderMonthCountVO; |
| 17 | import com.diligrp.rider.vo.RiderTodayCountVO; | 17 | import com.diligrp.rider.vo.RiderTodayCountVO; |
| @@ -39,6 +39,7 @@ public class RiderOrderServiceImpl implements RiderOrderService { | @@ -39,6 +39,7 @@ public class RiderOrderServiceImpl implements RiderOrderService { | ||
| 39 | private final RiderBalanceMapper balanceMapper; | 39 | private final RiderBalanceMapper balanceMapper; |
| 40 | private final RiderMapper riderMapper; | 40 | private final RiderMapper riderMapper; |
| 41 | private final RiderLevelService riderLevelService; | 41 | private final RiderLevelService riderLevelService; |
| 42 | + private final DispatchRuleService dispatchRuleService; | ||
| 42 | private final ObjectMapper objectMapper; | 43 | private final ObjectMapper objectMapper; |
| 43 | private final WebhookService webhookService; | 44 | private final WebhookService webhookService; |
| 44 | 45 | ||
| @@ -127,6 +128,16 @@ public class RiderOrderServiceImpl implements RiderOrderService { | @@ -127,6 +128,16 @@ public class RiderOrderServiceImpl implements RiderOrderService { | ||
| 127 | if (order.getRiderId() != null && order.getRiderId() != 0) { | 128 | if (order.getRiderId() != null && order.getRiderId() != 0) { |
| 128 | throw new BizException(980, "抢单失败,订单已被接"); | 129 | throw new BizException(980, "抢单失败,订单已被接"); |
| 129 | } | 130 | } |
| 131 | + | ||
| 132 | + DispatchRuleTemplateVO rule = dispatchRuleService.getActiveRule(cityId); | ||
| 133 | + if (rule != null && rule.getGrabMaxPerRider() != null && rule.getGrabMaxPerRider() > 0) { | ||
| 134 | + Long currentLoad = ordersMapper.selectCount(new LambdaQueryWrapper<Orders>() | ||
| 135 | + .eq(Orders::getRiderId, riderId) | ||
| 136 | + .in(Orders::getStatus, List.of(3, 4))); | ||
| 137 | + if (currentLoad >= rule.getGrabMaxPerRider()) { | ||
| 138 | + throw new BizException(1000, "当前持单量已达上限,无法继续抢单"); | ||
| 139 | + } | ||
| 140 | + } | ||
| 130 | if (order.getIsTrans() == 1 && riderId.equals(order.getOldRiderId())) { | 141 | if (order.getIsTrans() == 1 && riderId.equals(order.getOldRiderId())) { |
| 131 | throw new BizException(980, "抢单失败,不能抢自己转出的单"); | 142 | throw new BizException(980, "抢单失败,不能抢自己转出的单"); |
| 132 | } | 143 | } |
src/main/java/com/diligrp/rider/task/DispatchScheduleTask.java
0 → 100644
| 1 | +package com.diligrp.rider.task; | ||
| 2 | + | ||
| 3 | +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||
| 4 | +import com.diligrp.rider.entity.City; | ||
| 5 | +import com.diligrp.rider.entity.Orders; | ||
| 6 | +import com.diligrp.rider.mapper.CityMapper; | ||
| 7 | +import com.diligrp.rider.mapper.OrdersMapper; | ||
| 8 | +import com.diligrp.rider.service.DispatchRuleService; | ||
| 9 | +import com.diligrp.rider.service.DispatchService; | ||
| 10 | +import com.diligrp.rider.vo.DispatchRuleTemplateVO; | ||
| 11 | +import lombok.RequiredArgsConstructor; | ||
| 12 | +import lombok.extern.slf4j.Slf4j; | ||
| 13 | +import org.springframework.scheduling.annotation.EnableScheduling; | ||
| 14 | +import org.springframework.scheduling.annotation.Scheduled; | ||
| 15 | +import org.springframework.stereotype.Component; | ||
| 16 | + | ||
| 17 | +import java.util.List; | ||
| 18 | + | ||
| 19 | +@Slf4j | ||
| 20 | +@Component | ||
| 21 | +@EnableScheduling | ||
| 22 | +@RequiredArgsConstructor | ||
| 23 | +public class DispatchScheduleTask { | ||
| 24 | + | ||
| 25 | + private final CityMapper cityMapper; | ||
| 26 | + private final OrdersMapper ordersMapper; | ||
| 27 | + private final DispatchRuleService dispatchRuleService; | ||
| 28 | + private final DispatchService dispatchService; | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 抢单超时后自动派单 | ||
| 32 | + * 每3秒执行一次 | ||
| 33 | + */ | ||
| 34 | + @Scheduled(fixedDelay = 3_000) | ||
| 35 | + public void autoDispatchTimeoutOrders() { | ||
| 36 | + try { | ||
| 37 | + List<City> cities = cityMapper.selectList(new LambdaQueryWrapper<City>() | ||
| 38 | + .eq(City::getStatus, 1) | ||
| 39 | + .select(City::getId)); | ||
| 40 | + if (cities.isEmpty()) return; | ||
| 41 | + | ||
| 42 | + long now = System.currentTimeMillis() / 1000; | ||
| 43 | + for (City city : cities) { | ||
| 44 | + DispatchRuleTemplateVO rule = dispatchRuleService.getActiveRule(city.getId()); | ||
| 45 | + if (rule == null || rule.getAutoDispatch() == null || rule.getAutoDispatch() != 1) { | ||
| 46 | + continue; | ||
| 47 | + } | ||
| 48 | + if (rule.getGrabEnabled() == null || rule.getGrabEnabled() != 1) { | ||
| 49 | + continue; | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + int timeoutMinutes = rule.getGrabTimeout() != null ? rule.getGrabTimeout() : 30; | ||
| 53 | + long expireTime = now - timeoutMinutes * 60L; | ||
| 54 | + | ||
| 55 | + List<Orders> timeoutOrders = ordersMapper.selectList(new LambdaQueryWrapper<Orders>() | ||
| 56 | + .eq(Orders::getCityId, city.getId()) | ||
| 57 | + .eq(Orders::getStatus, 2) | ||
| 58 | + .eq(Orders::getRiderId, 0) | ||
| 59 | + .le(Orders::getAddTime, expireTime) | ||
| 60 | + .orderByAsc(Orders::getAddTime) | ||
| 61 | + .last("LIMIT 100")); | ||
| 62 | + | ||
| 63 | + for (Orders order : timeoutOrders) { | ||
| 64 | + Long riderId = dispatchService.dispatch(order); | ||
| 65 | + if (riderId != null) { | ||
| 66 | + log.info("超时订单自动派单成功 orderId={} riderId={}", order.getId(), riderId); | ||
| 67 | + } | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + } catch (Exception e) { | ||
| 71 | + log.error("自动派单任务异常", e); | ||
| 72 | + } | ||
| 73 | + } | ||
| 74 | +} |
src/main/java/com/diligrp/rider/task/OrderScheduleTask.java
| @@ -60,16 +60,6 @@ public class OrderScheduleTask { | @@ -60,16 +60,6 @@ public class OrderScheduleTask { | ||
| 60 | } | 60 | } |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | - /** | ||
| 64 | - * 检查骑手是否有新的指派订单(每3秒,dispatchNotice) | ||
| 65 | - * 注:实时推送版本需 WebSocket,此处仅做日志记录 | ||
| 66 | - * 后续接入 WebSocket 后可在此触发推送 | ||
| 67 | - */ | ||
| 68 | - @Scheduled(fixedDelay = 3_000) | ||
| 69 | - public void checkDispatchOrders() { | ||
| 70 | - // TODO: 接入 WebSocket 后在此推送给骑手 | ||
| 71 | - // 目前骑手通过轮询 /api/rider/order/list?type=1 获取待接单列表 | ||
| 72 | - } | ||
| 73 | 63 | ||
| 74 | private void notifyCancel(Orders order) { | 64 | private void notifyCancel(Orders order) { |
| 75 | try { | 65 | try { |
src/main/java/com/diligrp/rider/vo/DispatchRuleTemplateVO.java
0 → 100644
| 1 | +package com.diligrp.rider.vo; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.math.BigDecimal; | ||
| 6 | +import java.util.List; | ||
| 7 | + | ||
| 8 | +@Data | ||
| 9 | +public class DispatchRuleTemplateVO { | ||
| 10 | + | ||
| 11 | + private Long id; | ||
| 12 | + private Long cityId; | ||
| 13 | + private String name; | ||
| 14 | + private Integer isActive; | ||
| 15 | + private Integer grabEnabled; | ||
| 16 | + private Integer grabTimeout; | ||
| 17 | + private Integer grabScope; | ||
| 18 | + private Integer grabMaxPerRider; | ||
| 19 | + private Integer autoDispatch; | ||
| 20 | + private Long createTime; | ||
| 21 | + private Long updateTime; | ||
| 22 | + | ||
| 23 | + /** 条件列表(按 sortOrder 升序) */ | ||
| 24 | + private List<ConditionItem> conditions; | ||
| 25 | + | ||
| 26 | + @Data | ||
| 27 | + public static class ConditionItem { | ||
| 28 | + private Long id; | ||
| 29 | + private String conditionType; | ||
| 30 | + private String conditionDesc; | ||
| 31 | + private Integer enabled; | ||
| 32 | + private BigDecimal thresholdValue; | ||
| 33 | + private Integer sortOrder; | ||
| 34 | + /** 是否需要阈值 */ | ||
| 35 | + private Boolean hasThreshold; | ||
| 36 | + } | ||
| 37 | +} |
src/main/resources/schema.sql
| @@ -453,4 +453,44 @@ CREATE TABLE `ext_store` ( | @@ -453,4 +453,44 @@ CREATE TABLE `ext_store` ( | ||
| 453 | -- orders 表补充货物快照字段 | 453 | -- orders 表补充货物快照字段 |
| 454 | ALTER TABLE `orders` ADD COLUMN `items_json` TEXT COMMENT '货物清单快照JSON' AFTER `callback_url`; | 454 | ALTER TABLE `orders` ADD COLUMN `items_json` TEXT COMMENT '货物清单快照JSON' AFTER `callback_url`; |
| 455 | ALTER TABLE `orders` ADD COLUMN `item_remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '整单货物备注' AFTER `items_json`; | 455 | ALTER TABLE `orders` ADD COLUMN `item_remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '整单货物备注' AFTER `items_json`; |
| 456 | -ALTER TABLE `orders` ADD COLUMN `ext_store_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联外部门店ID' AFTER `item_remark`; | ||
| 457 | \ No newline at end of file | 456 | \ No newline at end of file |
| 457 | +ALTER TABLE `orders` ADD COLUMN `ext_store_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联外部门店ID' AFTER `item_remark`; | ||
| 458 | + | ||
| 459 | +-- orders 表补充调度字段 | ||
| 460 | +ALTER TABLE `orders` ADD COLUMN `dispatch_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '系统派单时间' AFTER `trans_time`; | ||
| 461 | +ALTER TABLE `orders` ADD COLUMN `dispatch_rider_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '系统指派骑手ID' AFTER `dispatch_time`; | ||
| 462 | + | ||
| 463 | +-- rider 表补充评分统计字段 | ||
| 464 | +ALTER TABLE `rider` ADD COLUMN `star_total` INT NOT NULL DEFAULT 0 COMMENT '评分总分' AFTER `thumb`; | ||
| 465 | +ALTER TABLE `rider` ADD COLUMN `star_count` INT NOT NULL DEFAULT 0 COMMENT '评分次数' AFTER `star_total`; | ||
| 466 | + | ||
| 467 | +-- 调度规则模板表 | ||
| 468 | +CREATE TABLE `dispatch_rule_template` ( | ||
| 469 | + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | ||
| 470 | + `city_id` BIGINT UNSIGNED NOT NULL COMMENT '城市ID', | ||
| 471 | + `name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '模板名称', | ||
| 472 | + `is_active` TINYINT NOT NULL DEFAULT 0 COMMENT '是否当前生效:0=否 1=是', | ||
| 473 | + `grab_enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '抢单模式启用', | ||
| 474 | + `grab_timeout` INT NOT NULL DEFAULT 30 COMMENT '抢单超时分钟数,超时后转自动派单', | ||
| 475 | + `grab_scope` TINYINT NOT NULL DEFAULT 1 COMMENT '抢单可见范围:1=订单所属区域骑手 2=全部自营骑手', | ||
| 476 | + `grab_max_per_rider` INT NOT NULL DEFAULT 3 COMMENT '单人最大同时持单量', | ||
| 477 | + `auto_dispatch` TINYINT NOT NULL DEFAULT 1 COMMENT '同步开启自动派单:0=否 1=是', | ||
| 478 | + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0, | ||
| 479 | + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0, | ||
| 480 | + PRIMARY KEY (`id`), | ||
| 481 | + KEY `idx_city_active` (`city_id`, `is_active`) | ||
| 482 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='调度规则模板表'; | ||
| 483 | + | ||
| 484 | +-- 派单优先级条件表 | ||
| 485 | +CREATE TABLE `dispatch_rule_condition` ( | ||
| 486 | + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | ||
| 487 | + `template_id` BIGINT UNSIGNED NOT NULL COMMENT '所属模板ID', | ||
| 488 | + `condition_type` VARCHAR(32) NOT NULL COMMENT '条件类型', | ||
| 489 | + `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用', | ||
| 490 | + `threshold_value` DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '阈值', | ||
| 491 | + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '优先级排序(小的优先)', | ||
| 492 | + `create_time` BIGINT UNSIGNED NOT NULL DEFAULT 0, | ||
| 493 | + `update_time` BIGINT UNSIGNED NOT NULL DEFAULT 0, | ||
| 494 | + PRIMARY KEY (`id`), | ||
| 495 | + UNIQUE KEY `uk_template_type` (`template_id`, `condition_type`), | ||
| 496 | + KEY `idx_template_order` (`template_id`, `sort_order`) | ||
| 497 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='派单优先级条件表'; | ||
| 458 | \ No newline at end of file | 498 | \ No newline at end of file |