feat(ticket): 添加水票订单配送流程功能

- 在GltTicketOrder实体类中新增配送相关的字段,包括配送状态、配送时间、收货人信息等
- 实现配送员端订单查询接口,支持按配送状态筛选和权限隔离
- 添加配送流程核心接口:接单、开始配送、确认送达、用户确认收货等功能
- 实现配送状态流转的状态机校验和并发安全的原子更新操作
- 优化数据库查询SQL,增加配送状态和租户ID的索引提升查询性能
- 添加配送员身份验证和权限检查机制,确保操作安全性
This commit is contained in:
2026-02-06 22:00:59 +08:00
parent 7191c93b4c
commit 922e7def9d
9 changed files with 551 additions and 4 deletions

View File

@@ -0,0 +1,14 @@
-- 水票配送订单:配送流程字段(需在数据库执行)
-- 表glt_ticket_order
ALTER TABLE glt_ticket_order
ADD COLUMN delivery_status INT NULL DEFAULT 10 COMMENT '配送状态10待配送、20配送中、30待客户确认、40已完成',
ADD COLUMN send_start_time DATETIME NULL COMMENT '开始配送时间',
ADD COLUMN send_end_time DATETIME NULL COMMENT '确认送达时间',
ADD COLUMN send_end_img VARCHAR(512) NULL COMMENT '送达拍照留档图片URL',
ADD COLUMN receive_confirm_time DATETIME NULL COMMENT '客户确认收货时间',
ADD COLUMN receive_confirm_type INT NULL COMMENT '确认方式10手动、20照片、30超时';
CREATE INDEX idx_glt_ticket_order_rider_status ON glt_ticket_order (tenant_id, rider_id, delivery_status, deleted);
CREATE INDEX idx_glt_ticket_order_user_status ON glt_ticket_order (tenant_id, user_id, delivery_status, deleted);

View File

@@ -0,0 +1,98 @@
# 水票配送订单:后端提示词(可直接发给后端)
> 目标:把“水票下单后 -> 配送员接单/配送 -> 用户确认 -> 自动确认”的闭环放到后端,用明确的字段 + 状态机校验保证不越权、不乱跳状态、并发不重复接单。
>
> 接口前缀:当前后端控制器为 `@RequestMapping("/api/glt/glt-ticket-order")`,下文默认都带 `/api` 前缀(如需兼容旧路径,可做网关转发或保留旧路由)。
## 0) 角色与权限边界(务必在后端兜底)
- 用户端(小程序用户):只能看/操作自己的订单(`userId` = token userId
- 配送员端:只能看/操作分配给自己的订单(`riderId` = token userId / rider userId
- 管理端:按后台权限控制(可查询/派单/改状态,但仍需 tenantId 隔离)。
建议:对“配送员端接口”忽略前端传入的 `riderId/userId/tenantId`,统一从登录态注入,避免越权。
## 1) 订单查询(配送员端)
建议提供配送员专用分页接口:`GET /api/glt/glt-ticket-order/rider/page`(避免与后台管理分页混用)。
请支持以下筛选,并保证权限隔离:
- `deliveryStatus`10待配送、20配送中、30待客户确认、40已完成配送员端必要不传默认=10
- `keywords`:支持按地址/备注等模糊搜索(可选)
- 排序:建议默认 `sendTime asc, createTime desc`(或沿用后端默认排序,但请告知前端)
权限隔离要求(配送员端):
- 只返回当前登录配送员的订单:后端强制 `param.riderId = loginUserId`(前端传不传都一样)。
- `tenantId/deleted` 等同样后端兜底(只查当前租户、只查未删除)。
返回字段建议(配送员端用得上):
- 门店/仓库/用户/配送员的展示字段:`storeName/storeAddress/storePhone``warehouseName/warehouseAddress``nickname/phone/avatar``riderName/riderPhone`(现有 `pageRel/listRel` 已在做关联返回)。
- 导航相关(详见第 5 节):收货地址 `lat/lng`、门店/仓库 `lngAndLat`(可关联返回或做快照字段)。
## 2) 配送流程字段(建议后端落库并回传)
订单表建议确保有以下字段(当前前端已按这些字段做流程判断/展示):
- `riderId/riderName/riderPhone`:配送员信息
- `deliveryStatus`10/20/30/40
- `sendStartTime`:配送员点击“开始配送”的时间(建议 datetime
- `sendEndTime`:配送员点击“确认送达”的时间(建议 datetime
- `sendEndImg`:送达拍照留档图片 URL可选/必填由后端策略决定;建议 varchar(512)
- `receiveConfirmTime`:客户确认收货时间(建议 datetime
- `receiveConfirmType`10客户手动确认、20配送照片自动确认、30超时自动确认
数据库变更示例SQL见`docs/sql/2026-02-06_glt_ticket_order_delivery_fields.sql`)。
强烈建议把“配送状态”与“业务状态(status=0/1、deleted=0/1)”分开,避免混用:
- `status/deleted`:系统通用字段(现有逻辑)
- `deliveryStatus`:配送流程状态(本需求新增)
## 3) 状态流转与校验(强烈建议在后端做)
请在更新订单时做状态机校验,避免前端绕过流程:
- `10 -> 20`:仅允许订单属于当前配送员,且未开始/未送达
- `20 -> 30`:配送员确认送达(可带 `sendEndImg`
- `20/30 -> 40`:完成;来源可能是
- 客户手动确认(写 `receiveConfirmTime` + `receiveConfirmType=10`
- 配送照片直接完成(写 `receiveConfirmTime` + `receiveConfirmType=20`,并要求 `sendEndImg`
- 超时自动确认(写 `receiveConfirmTime` + `receiveConfirmType=30`,建议由定时任务执行)
并发/幂等建议(避免重复点击/重复请求带来的脏数据):
- 所有“状态变更接口”用条件更新实现原子校验:`UPDATE ... SET ... WHERE id=? AND rider_id=? AND delivery_status=? AND deleted=0`
- 对重复调用做幂等:
- `start`:如果已是 20 则直接返回成功;如果已到 30/40 返回“状态不允许”
- `delivered`:如果已是 30/40 则返回成功(或提示已送达);避免重复写 `sendEndTime`
- `confirm-receive`:如果已是 40 则返回成功(或提示已完成)
## 4) 建议新增/明确的接口能力
为了避免并发抢单/越权更新,建议新增更语义化的接口(或在 update 内做等价校验):
- 接单(抢单/派单):`POST /api/glt/glt-ticket-order/{id}/accept`
- 配送员端:后端原子校验:仅当 `rider_id IS NULL`(或为 0时才能写入当前 rider 信息
- 管理端派单:允许传 `riderId`,但需校验骑手归属门店/租户(如有该约束)
- 开始配送:`POST /api/glt/glt-ticket-order/{id}/start`
- 写:`sendStartTime=now``deliveryStatus=20`
- 校验:必须 `riderId=当前登录配送员` 且当前 `deliveryStatus=10`
- 确认送达:`POST /api/glt/glt-ticket-order/{id}/delivered`
- 入参:`sendEndImg`(可选/必填,按策略)
- 写:`sendEndTime=now``deliveryStatus=30``sendEndImg`
- 可选策略 A推荐可配置`sendEndImg` 必填且存在,则可直接 `deliveryStatus=40` 并写 `receiveConfirmTime/Type=20`
- 客户确认收货:`POST /api/glt/glt-ticket-order/{id}/confirm-receive`
- 校验:只能本人 `userId` 操作,且必须 `deliveryStatus=30`
- 写:`deliveryStatus=40``receiveConfirmTime=now``receiveConfirmType=10`
接口返回建议:
- 成功统一返回 `ApiResult.success(...)`
- 失败请返回明确 msg例如`无权限``订单不存在``订单状态不允许``订单已被其他配送员接单`
## 5) 为了“导航到收货地址/取货点”的字段补充(建议)
当前仅有 `address` 字符串,无法在小程序内 `openLocation` 精准导航;建议补充:
- 收货地址(推荐至少返回):`receiverName``receiverPhone``province/city/region/address/fullAddress``lat/lng`
- 取货点(门店/仓库,推荐至少返回):`store.lngAndLat``warehouse.lngAndLat`
实现方式二选一:
- 方式 A更快查询时关联 `shop_user_address``shop_store``shop_warehouse`,把经纬度字段透出给前端。
- 方式 B更稳下单时把收货地址的 `name/phone/lat/lng/fullAddress` 以及门店/仓库 `lngAndLat` 做快照写入订单,避免后续数据变更影响历史订单导航。
## 6)(可选但很有用)超时自动确认规则
- 建议后端提供可配置项:`autoConfirmHours`(例如 24h/48h
- 定时任务扫描:`deliveryStatus=30``sendEndTime < now - autoConfirmHours` 的订单
- 原子更新:只更新仍处于 30 的订单,写入 `deliveryStatus=40``receiveConfirmTime=now``receiveConfirmType=30`
## 7)(可选)字段/枚举建议(便于前后端对齐)
- `deliveryStatus`10待配送、20配送中、30待客户确认、40已完成
- `receiveConfirmType`10客户手动确认、20配送照片自动确认、30超时自动确认
- 时间字段统一返回格式:`yyyy-MM-dd HH:mm:ss`(与项目现有 `@JsonFormat` 风格一致)

View File

@@ -5,15 +5,20 @@ import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.param.GltTicketOrderDeliveredParam;
import com.gxwebsoft.glt.param.GltTicketOrderParam;
import com.gxwebsoft.glt.service.GltTicketOrderService;
import com.gxwebsoft.shop.entity.ShopStoreRider;
import com.gxwebsoft.shop.entity.ShopUserAddress;
import com.gxwebsoft.shop.service.ShopStoreRiderService;
import com.gxwebsoft.shop.service.ShopUserAddressService;
import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -34,6 +39,8 @@ public class GltTicketOrderController extends BaseController {
private GltTicketOrderService gltTicketOrderService;
@Resource
private ShopUserAddressService shopUserAddressService;
@Resource
private ShopStoreRiderService shopStoreRiderService;
@Operation(summary = "分页查询送水订单")
@GetMapping("/page")
@@ -42,6 +49,30 @@ public class GltTicketOrderController extends BaseController {
return success(gltTicketOrderService.pageRel(param));
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员端:分页查询我的送水订单")
@GetMapping("/rider/page")
public ApiResult<PageResult<GltTicketOrder>> riderPage(GltTicketOrderParam param) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
Integer tenantId = getTenantId();
param.setTenantId(tenantId);
// 仅允许配送员访问
requireActiveRider(loginUser.getUserId(), tenantId);
param.setRiderId(loginUser.getUserId());
if (param.getDeliveryStatus() == null) {
param.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING);
}
// 配送员端默认按期望配送时间优先
if (StrUtil.isBlank(param.getSort())) {
param.setSort("sendTime asc, createTime desc");
}
return success(gltTicketOrderService.pageRel(param));
}
@Operation(summary = "查询全部送水订单")
@GetMapping()
public ApiResult<List<GltTicketOrder>> list(GltTicketOrderParam param) {
@@ -92,6 +123,83 @@ public class GltTicketOrderController extends BaseController {
return success("下单成功");
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员接单")
@PostMapping("/{id}/accept")
public ApiResult<?> accept(@PathVariable("id") Integer id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
Integer tenantId = getTenantId();
requireActiveRider(loginUser.getUserId(), tenantId);
gltTicketOrderService.accept(id, loginUser.getUserId(), tenantId);
return success("接单成功");
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员开始配送")
@PostMapping("/{id}/start")
public ApiResult<?> start(@PathVariable("id") Integer id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
Integer tenantId = getTenantId();
requireActiveRider(loginUser.getUserId(), tenantId);
gltTicketOrderService.start(id, loginUser.getUserId(), tenantId);
return success("开始配送");
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员确认送达")
@PostMapping("/{id}/delivered")
public ApiResult<?> delivered(@PathVariable("id") Integer id,
@RequestBody(required = false) GltTicketOrderDeliveredParam body) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
Integer tenantId = getTenantId();
requireActiveRider(loginUser.getUserId(), tenantId);
String sendEndImg = body == null ? null : body.getSendEndImg();
gltTicketOrderService.delivered(id, loginUser.getUserId(), tenantId, sendEndImg);
return success("确认送达");
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "用户确认收货")
@PostMapping("/{id}/confirm-receive")
public ApiResult<?> confirmReceive(@PathVariable("id") Integer id) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
gltTicketOrderService.confirmReceive(id, loginUser.getUserId(), getTenantId());
return success("确认收货成功");
}
private ShopStoreRider requireActiveRider(Integer userId, Integer tenantId) {
if (userId == null) {
throw new BusinessException("请先登录");
}
if (tenantId == null) {
throw new BusinessException("租户信息缺失");
}
ShopStoreRider rider = shopStoreRiderService.getOne(new LambdaQueryWrapper<ShopStoreRider>()
.eq(ShopStoreRider::getUserId, userId)
.eq(ShopStoreRider::getTenantId, tenantId)
.eq(ShopStoreRider::getIsDelete, 0)
.last("limit 1"));
if (rider == null) {
throw new BusinessException("非配送员,无权限操作");
}
if (rider.getStatus() == null || rider.getStatus() != 1) {
throw new BusinessException("配送员已禁用");
}
return rider;
}
private String buildAddressSnapshot(ShopUserAddress addr) {
if (addr == null) {
return null;

View File

@@ -68,6 +68,10 @@ public class GltTicketOrder implements Serializable {
@TableField(exist = false)
private String warehouseAddress;
@Schema(description = "仓库手机号")
@TableField(exist = false)
private String warehousePhone;
@Schema(description = "关联收货地址")
private Integer addressId;
@@ -99,6 +103,27 @@ public class GltTicketOrder implements Serializable {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private String sendTime;
@Schema(description = "配送状态10待配送、20配送中、30待客户确认、40已完成")
private Integer deliveryStatus;
@Schema(description = "开始配送时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime sendStartTime;
@Schema(description = "确认送达时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime sendEndTime;
@Schema(description = "送达拍照留档图片URL")
private String sendEndImg;
@Schema(description = "客户确认收货时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime receiveConfirmTime;
@Schema(description = "确认方式10客户手动确认、20配送照片自动确认、30超时自动确认")
private Integer receiveConfirmType;
@Schema(description = "用户ID")
private Integer userId;
@@ -114,6 +139,50 @@ public class GltTicketOrder implements Serializable {
@TableField(exist = false)
private String avatar;
@Schema(description = "收货人姓名")
@TableField(exist = false)
private String receiverName;
@Schema(description = "收货人手机号")
@TableField(exist = false)
private String receiverPhone;
@Schema(description = "收货省")
@TableField(exist = false)
private String receiverProvince;
@Schema(description = "收货市")
@TableField(exist = false)
private String receiverCity;
@Schema(description = "收货区")
@TableField(exist = false)
private String receiverRegion;
@Schema(description = "收货详细地址")
@TableField(exist = false)
private String receiverAddress;
@Schema(description = "收货完整地址")
@TableField(exist = false)
private String receiverFullAddress;
@Schema(description = "收货纬度")
@TableField(exist = false)
private String receiverLat;
@Schema(description = "收货经度")
@TableField(exist = false)
private String receiverLng;
@Schema(description = "门店经纬度(lng,lat)")
@TableField(exist = false)
private String storeLngAndLat;
@Schema(description = "仓库经纬度(lng,lat)")
@TableField(exist = false)
private String warehouseLngAndLat;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;

View File

@@ -4,13 +4,19 @@
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*, b.name as storeName, b.address as storeAddress, b.phone as storePhone,
SELECT a.*, b.name as storeName, b.address as storeAddress, b.phone as storePhone, b.lng_and_lat as storeLngAndLat,
w.name as warehouseName, w.address as warehouseAddress, w.phone as warehousePhone, w.lng_and_lat as warehouseLngAndLat,
c.real_name as riderName, c.mobile as riderPhone,
u.nickname, u.phone, u.avatar
u.nickname, u.phone, u.avatar,
d.name as receiverName, d.phone as receiverPhone,
d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion,
d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng
FROM glt_ticket_order a
LEFT JOIN shop_store b ON a.store_id = b.id
LEFT JOIN shop_warehouse w ON a.warehouse_id = w.id
LEFT JOIN shop_store_rider c ON a.rider_id = c.user_id
LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id
LEFT JOIN shop_user_address d ON a.address_id = d.id
<where>
<if test="param.id != null">
@@ -25,6 +31,9 @@
<if test="param.riderId != null">
AND a.rider_id = #{param.riderId}
</if>
<if test="param.deliveryStatus != null">
AND a.delivery_status = #{param.deliveryStatus}
</if>
<if test="param.warehouseId != null">
AND a.warehouse_id = #{param.warehouseId}
</if>
@@ -55,6 +64,9 @@
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
@@ -68,7 +80,13 @@
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
AND (
a.address LIKE CONCAT('%', #{param.keywords}, '%')
OR a.buyer_remarks LIKE CONCAT('%', #{param.keywords}, '%')
OR a.comments LIKE CONCAT('%', #{param.keywords}, '%')
OR b.name LIKE CONCAT('%', #{param.keywords}, '%')
OR u.nickname LIKE CONCAT('%', #{param.keywords}, '%')
OR u.phone LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>

View File

@@ -0,0 +1,18 @@
package com.gxwebsoft.glt.param;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 配送员确认送达参数
*/
@Data
public class GltTicketOrderDeliveredParam implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "送达拍照留档图片URL")
private String sendEndImg;
}

View File

@@ -38,6 +38,10 @@ public class GltTicketOrderParam extends BaseParam {
@QueryField(type = QueryType.EQ)
private Integer riderId;
@Schema(description = "配送状态10待配送、20配送中、30待客户确认、40已完成")
@QueryField(type = QueryType.EQ)
private Integer deliveryStatus;
@Schema(description = "仓库ID")
@QueryField(type = QueryType.EQ)
private Integer warehouseId;

View File

@@ -15,6 +15,15 @@ import java.util.List;
*/
public interface GltTicketOrderService extends IService<GltTicketOrder> {
int DELIVERY_STATUS_WAITING = 10;
int DELIVERY_STATUS_DELIVERING = 20;
int DELIVERY_STATUS_WAIT_CONFIRM = 30;
int DELIVERY_STATUS_FINISHED = 40;
int RECEIVE_CONFIRM_TYPE_MANUAL = 10;
int RECEIVE_CONFIRM_TYPE_PHOTO = 20;
int RECEIVE_CONFIRM_TYPE_TIMEOUT = 30;
/**
* 分页关联查询
*
@@ -49,4 +58,24 @@ public interface GltTicketOrderService extends IService<GltTicketOrder> {
*/
GltTicketOrder createWithWriteOff(GltTicketOrder gltTicketOrder, Integer userId, Integer tenantId);
/**
* 配送员接单(原子):仅当 riderId 为空(或0)且 deliveryStatus=10 时写入 riderId。
*/
void accept(Integer id, Integer riderId, Integer tenantId);
/**
* 配送员开始配送10 -> 20并写 sendStartTime。
*/
void start(Integer id, Integer riderId, Integer tenantId);
/**
* 配送员确认送达20 -> 30并写 sendEndTime / sendEndImg。
*/
void delivered(Integer id, Integer riderId, Integer tenantId, String sendEndImg);
/**
* 用户确认收货30 -> 40并写 receiveConfirmTime / receiveConfirmType=10。
*/
void confirmReceive(Integer id, Integer userId, Integer tenantId);
}

View File

@@ -14,6 +14,7 @@ import com.gxwebsoft.glt.param.GltTicketOrderParam;
import com.gxwebsoft.glt.service.GltTicketOrderService;
import com.gxwebsoft.glt.service.GltUserTicketLogService;
import com.gxwebsoft.glt.service.GltUserTicketService;
import org.springframework.util.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -114,6 +115,9 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
gltTicketOrder.setStatus(0);
gltTicketOrder.setDeleted(0);
gltTicketOrder.setTenantId(tenantId);
if (gltTicketOrder.getDeliveryStatus() == null) {
gltTicketOrder.setDeliveryStatus(DELIVERY_STATUS_WAITING);
}
if (gltTicketOrder.getSortNumber() == null) {
gltTicketOrder.setSortNumber(0);
}
@@ -159,4 +163,189 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
return gltTicketOrder;
}
@Override
public void accept(Integer id, Integer riderId, Integer tenantId) {
if (id == null) {
throw new BusinessException("订单id不能为空");
}
if (riderId == null) {
throw new BusinessException("配送员信息缺失");
}
if (tenantId == null) {
throw new BusinessException("租户信息缺失");
}
// 原子接单:避免并发抢单
boolean ok = this.lambdaUpdate()
.set(GltTicketOrder::getRiderId, riderId)
.set(GltTicketOrder::getUpdateTime, LocalDateTime.now())
.eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.and(w -> w.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAITING)
.or().isNull(GltTicketOrder::getDeliveryStatus))
.and(w -> w.isNull(GltTicketOrder::getRiderId).or().eq(GltTicketOrder::getRiderId, 0))
.update();
if (ok) {
return;
}
// 回查给出更明确的错误
GltTicketOrder order = this.lambdaQuery()
.eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.one();
if (order == null) {
throw new BusinessException("订单不存在");
}
if (order.getRiderId() != null && order.getRiderId() > 0) {
throw new BusinessException("订单已被其他配送员接单");
}
throw new BusinessException("订单状态不允许接单");
}
@Override
public void start(Integer id, Integer riderId, Integer tenantId) {
if (id == null) {
throw new BusinessException("订单id不能为空");
}
if (riderId == null) {
throw new BusinessException("配送员信息缺失");
}
if (tenantId == null) {
throw new BusinessException("租户信息缺失");
}
LocalDateTime now = LocalDateTime.now();
boolean ok = this.lambdaUpdate()
.set(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_DELIVERING)
.set(GltTicketOrder::getSendStartTime, now)
.set(GltTicketOrder::getUpdateTime, now)
.eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.eq(GltTicketOrder::getRiderId, riderId)
.and(w -> w.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAITING)
.or().isNull(GltTicketOrder::getDeliveryStatus))
.update();
if (ok) {
return;
}
GltTicketOrder order = this.lambdaQuery()
.eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.one();
if (order == null) {
throw new BusinessException("订单不存在");
}
if (!riderId.equals(order.getRiderId())) {
throw new BusinessException("无权限操作该订单");
}
if (order.getDeliveryStatus() != null && order.getDeliveryStatus() == DELIVERY_STATUS_DELIVERING) {
// 幂等:重复开始配送视为成功
return;
}
throw new BusinessException("订单状态不允许开始配送");
}
@Override
public void delivered(Integer id, Integer riderId, Integer tenantId, String sendEndImg) {
if (id == null) {
throw new BusinessException("订单id不能为空");
}
if (riderId == null) {
throw new BusinessException("配送员信息缺失");
}
if (tenantId == null) {
throw new BusinessException("租户信息缺失");
}
LocalDateTime now = LocalDateTime.now();
var update = this.lambdaUpdate()
.set(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAIT_CONFIRM)
.set(GltTicketOrder::getSendEndTime, now)
.set(GltTicketOrder::getUpdateTime, now);
if (StringUtils.hasText(sendEndImg)) {
update.set(GltTicketOrder::getSendEndImg, sendEndImg);
}
boolean ok = update
.eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.eq(GltTicketOrder::getRiderId, riderId)
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_DELIVERING)
.update();
if (ok) {
return;
}
GltTicketOrder order = this.lambdaQuery()
.eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.one();
if (order == null) {
throw new BusinessException("订单不存在");
}
if (!riderId.equals(order.getRiderId())) {
throw new BusinessException("无权限操作该订单");
}
if (order.getDeliveryStatus() != null
&& (order.getDeliveryStatus() == DELIVERY_STATUS_WAIT_CONFIRM
|| order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED)) {
// 幂等:重复送达视为成功
return;
}
throw new BusinessException("订单状态不允许确认送达");
}
@Override
public void confirmReceive(Integer id, Integer userId, Integer tenantId) {
if (id == null) {
throw new BusinessException("订单id不能为空");
}
if (userId == null) {
throw new BusinessException("请先登录");
}
if (tenantId == null) {
throw new BusinessException("租户信息缺失");
}
LocalDateTime now = LocalDateTime.now();
boolean ok = this.lambdaUpdate()
.set(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_FINISHED)
.set(GltTicketOrder::getReceiveConfirmTime, now)
.set(GltTicketOrder::getReceiveConfirmType, RECEIVE_CONFIRM_TYPE_MANUAL)
.set(GltTicketOrder::getUpdateTime, now)
.eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.eq(GltTicketOrder::getUserId, userId)
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAIT_CONFIRM)
.update();
if (ok) {
return;
}
GltTicketOrder order = this.lambdaQuery()
.eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.one();
if (order == null) {
throw new BusinessException("订单不存在");
}
if (!userId.equals(order.getUserId())) {
throw new BusinessException("无权限操作该订单");
}
if (order.getDeliveryStatus() != null && order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED) {
// 幂等:重复确认收货视为成功
return;
}
throw new BusinessException("订单状态不允许确认收货");
}
}