diff --git a/docs/sql/2026-02-06_glt_ticket_order_delivery_fields.sql b/docs/sql/2026-02-06_glt_ticket_order_delivery_fields.sql new file mode 100644 index 0000000..5de4ec2 --- /dev/null +++ b/docs/sql/2026-02-06_glt_ticket_order_delivery_fields.sql @@ -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); + diff --git a/docs/水票配送订单-后端提示词.md b/docs/水票配送订单-后端提示词.md new file mode 100644 index 0000000..3ac8a7f --- /dev/null +++ b/docs/水票配送订单-后端提示词.md @@ -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` 风格一致) diff --git a/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java b/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java index 4c660e3..a65da97 100644 --- a/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java +++ b/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java @@ -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> 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(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() + .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; diff --git a/src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java b/src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java index 4a26778..dfae59e 100644 --- a/src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java +++ b/src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java @@ -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; diff --git a/src/main/java/com/gxwebsoft/glt/mapper/xml/GltTicketOrderMapper.xml b/src/main/java/com/gxwebsoft/glt/mapper/xml/GltTicketOrderMapper.xml index 9360bbe..dd24b15 100644 --- a/src/main/java/com/gxwebsoft/glt/mapper/xml/GltTicketOrderMapper.xml +++ b/src/main/java/com/gxwebsoft/glt/mapper/xml/GltTicketOrderMapper.xml @@ -4,13 +4,19 @@ - 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 @@ -25,6 +31,9 @@ AND a.rider_id = #{param.riderId} + + AND a.delivery_status = #{param.deliveryStatus} + AND a.warehouse_id = #{param.warehouseId} @@ -55,6 +64,9 @@ AND a.status = #{param.status} + + AND a.tenant_id = #{param.tenantId} + AND a.deleted = #{param.deleted} @@ -68,9 +80,15 @@ AND a.create_time <= #{param.createTimeEnd} - 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}, '%') ) - + diff --git a/src/main/java/com/gxwebsoft/glt/param/GltTicketOrderDeliveredParam.java b/src/main/java/com/gxwebsoft/glt/param/GltTicketOrderDeliveredParam.java new file mode 100644 index 0000000..7158cd2 --- /dev/null +++ b/src/main/java/com/gxwebsoft/glt/param/GltTicketOrderDeliveredParam.java @@ -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; +} + diff --git a/src/main/java/com/gxwebsoft/glt/param/GltTicketOrderParam.java b/src/main/java/com/gxwebsoft/glt/param/GltTicketOrderParam.java index 771be89..976a20b 100644 --- a/src/main/java/com/gxwebsoft/glt/param/GltTicketOrderParam.java +++ b/src/main/java/com/gxwebsoft/glt/param/GltTicketOrderParam.java @@ -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; diff --git a/src/main/java/com/gxwebsoft/glt/service/GltTicketOrderService.java b/src/main/java/com/gxwebsoft/glt/service/GltTicketOrderService.java index 39c784b..0b7ce5a 100644 --- a/src/main/java/com/gxwebsoft/glt/service/GltTicketOrderService.java +++ b/src/main/java/com/gxwebsoft/glt/service/GltTicketOrderService.java @@ -15,6 +15,15 @@ import java.util.List; */ public interface GltTicketOrderService extends IService { + 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 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); + } diff --git a/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java b/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java index f4ecf67..f0c89d0 100644 --- a/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java +++ b/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java @@ -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 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("订单状态不允许确认收货"); + } + }