From 9c579b3173a10dd0ee41cbca894cc2b64aa4ed65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Tue, 7 Jul 2026 15:48:11 +0800 Subject: [PATCH] feat: add delivery assignment and rider APIs --- .../delivery/DeliveryAdminController.java | 92 +++++++++ .../delivery/DeliveryAssignRequest.java | 27 +++ .../delivery/DeliveryAssignmentService.java | 181 ++++++++++++++++++ .../delivery/DeliveryOrderResponse.java | 179 +++++++++++++++++ .../linhelp/delivery/DeliveryUserRequest.java | 65 +++++++ .../delivery/DeliveryUserResponse.java | 67 +++++++ .../linhelp/delivery/DeliveryUserService.java | 104 ++++++++++ .../delivery/RiderDeliveredRequest.java | 13 ++ .../delivery/RiderExceptionRequest.java | 26 +++ .../delivery/RiderIncomeSummaryResponse.java | 40 ++++ .../linhelp/delivery/RiderTaskController.java | 65 +++++++ .../linhelp/delivery/RiderTaskService.java | 120 ++++++++++++ .../DeliveryAssignmentServiceTests.java | 81 ++++++++ .../delivery/RiderTaskServiceTests.java | 92 +++++++++ 14 files changed, 1152 insertions(+) create mode 100644 backend/src/main/java/com/linhelp/delivery/DeliveryAdminController.java create mode 100644 backend/src/main/java/com/linhelp/delivery/DeliveryAssignRequest.java create mode 100644 backend/src/main/java/com/linhelp/delivery/DeliveryAssignmentService.java create mode 100644 backend/src/main/java/com/linhelp/delivery/DeliveryOrderResponse.java create mode 100644 backend/src/main/java/com/linhelp/delivery/DeliveryUserRequest.java create mode 100644 backend/src/main/java/com/linhelp/delivery/DeliveryUserResponse.java create mode 100644 backend/src/main/java/com/linhelp/delivery/DeliveryUserService.java create mode 100644 backend/src/main/java/com/linhelp/delivery/RiderDeliveredRequest.java create mode 100644 backend/src/main/java/com/linhelp/delivery/RiderExceptionRequest.java create mode 100644 backend/src/main/java/com/linhelp/delivery/RiderIncomeSummaryResponse.java create mode 100644 backend/src/main/java/com/linhelp/delivery/RiderTaskController.java create mode 100644 backend/src/main/java/com/linhelp/delivery/RiderTaskService.java create mode 100644 backend/src/test/java/com/linhelp/delivery/DeliveryAssignmentServiceTests.java create mode 100644 backend/src/test/java/com/linhelp/delivery/RiderTaskServiceTests.java diff --git a/backend/src/main/java/com/linhelp/delivery/DeliveryAdminController.java b/backend/src/main/java/com/linhelp/delivery/DeliveryAdminController.java new file mode 100644 index 0000000..f8e759b --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/DeliveryAdminController.java @@ -0,0 +1,92 @@ +package com.linhelp.delivery; + +import com.linhelp.common.api.ApiResponse; +import com.linhelp.common.enums.DeliveryTaskStatus; +import com.linhelp.common.security.AuthService; +import com.linhelp.common.security.CurrentUser; +import com.linhelp.common.security.RequireRole; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequireRole({"PLATFORM_ADMIN", "COMMUNITY_ADMIN"}) +public class DeliveryAdminController { + private final DeliveryUserService deliveryUserService; + private final DeliveryAssignmentService assignmentService; + private final AuthService authService; + + public DeliveryAdminController(DeliveryUserService deliveryUserService, + DeliveryAssignmentService assignmentService, + AuthService authService) { + this.deliveryUserService = deliveryUserService; + this.assignmentService = assignmentService; + this.authService = authService; + } + + @GetMapping("/api/admin/delivery-users") + public ApiResponse> listUsers() { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(deliveryUserService.list(user.getCommunityId())); + } + + @PostMapping("/api/admin/delivery-users") + public ApiResponse createUser(@Valid @RequestBody DeliveryUserRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(deliveryUserService.create(request)); + } + + @PutMapping("/api/admin/delivery-users/{id}") + public ApiResponse updateUser(@PathVariable Long id, @Valid @RequestBody DeliveryUserRequest request) { + CurrentUser user = authService.currentUser(); + request.setTenantId(user.getTenantId()); + request.setCommunityId(user.getCommunityId()); + return ApiResponse.ok(deliveryUserService.update(id, request)); + } + + @PutMapping("/api/admin/delivery-users/{id}/enable") + public ApiResponse enableUser(@PathVariable Long id) { + return ApiResponse.ok(deliveryUserService.enable(id)); + } + + @PutMapping("/api/admin/delivery-users/{id}/disable") + public ApiResponse disableUser(@PathVariable Long id) { + return ApiResponse.ok(deliveryUserService.disable(id)); + } + + @PostMapping("/api/admin/delivery-orders/assign-goods-order") + public ApiResponse assignGoodsOrder(@Valid @RequestBody DeliveryAssignRequest request) { + return ApiResponse.ok(assignmentService.assignGoodsOrder(request.getOrderId(), request.getRiderId())); + } + + @PostMapping("/api/admin/delivery-orders/assign-express-order") + public ApiResponse assignExpressOrder(@Valid @RequestBody DeliveryAssignRequest request) { + return ApiResponse.ok(assignmentService.assignExpressOrder(request.getOrderId(), request.getRiderId())); + } + + @PostMapping("/api/admin/delivery-orders/assign-group-buy-order") + public ApiResponse assignGroupBuyOrder(@Valid @RequestBody DeliveryAssignRequest request) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(assignmentService.assignGroupBuyOrder(user.getCommunityId(), request.getOrderId(), request.getRiderId())); + } + + @GetMapping("/api/admin/delivery-orders") + public ApiResponse> listTasks(@RequestParam(required = false) DeliveryTaskStatus status) { + CurrentUser user = authService.currentUser(); + return ApiResponse.ok(assignmentService.listAdmin(user.getCommunityId(), status)); + } + + @GetMapping("/api/admin/delivery-orders/{id}") + public ApiResponse taskDetail(@PathVariable Long id) { + return ApiResponse.ok(assignmentService.detail(id)); + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/DeliveryAssignRequest.java b/backend/src/main/java/com/linhelp/delivery/DeliveryAssignRequest.java new file mode 100644 index 0000000..810ad1e --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/DeliveryAssignRequest.java @@ -0,0 +1,27 @@ +package com.linhelp.delivery; + +import javax.validation.constraints.NotNull; + +public class DeliveryAssignRequest { + @NotNull + private Long orderId; + + @NotNull + private Long riderId; + + public Long getOrderId() { + return orderId; + } + + public void setOrderId(Long orderId) { + this.orderId = orderId; + } + + public Long getRiderId() { + return riderId; + } + + public void setRiderId(Long riderId) { + this.riderId = riderId; + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/DeliveryAssignmentService.java b/backend/src/main/java/com/linhelp/delivery/DeliveryAssignmentService.java new file mode 100644 index 0000000..8c64999 --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/DeliveryAssignmentService.java @@ -0,0 +1,181 @@ +package com.linhelp.delivery; + +import com.linhelp.common.enums.DeliveryMethod; +import com.linhelp.common.enums.DeliveryTaskStatus; +import com.linhelp.common.enums.ExpressOrderStatus; +import com.linhelp.common.enums.GoodsOrderStatus; +import com.linhelp.common.exception.BizException; +import com.linhelp.express.ExpressOrderResponse; +import com.linhelp.express.ExpressOrderService; +import com.linhelp.order.GoodsOrderResponse; +import com.linhelp.order.GoodsOrderService; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class DeliveryAssignmentService { + public static final String BIZ_TYPE_GOODS_ORDER = "GOODS_ORDER"; + public static final String BIZ_TYPE_EXPRESS_ORDER = "EXPRESS_ORDER"; + public static final String BIZ_TYPE_GROUP_BUY_ORDER = "GROUP_BUY_ORDER"; + + private final DeliveryUserService deliveryUserService; + private final GoodsOrderService goodsOrderService; + private final ExpressOrderService expressOrderService; + private final AtomicLong ids = new AtomicLong(1L); + private final Map tasks = new LinkedHashMap(); + + public DeliveryAssignmentService(DeliveryUserService deliveryUserService, + GoodsOrderService goodsOrderService, + ExpressOrderService expressOrderService) { + this.deliveryUserService = deliveryUserService; + this.goodsOrderService = goodsOrderService; + this.expressOrderService = expressOrderService; + } + + public synchronized DeliveryOrderResponse assignGoodsOrder(Long goodsOrderId, Long riderId) { + DeliveryUserResponse rider = deliveryUserService.requireEnabled(riderId); + GoodsOrderResponse order = goodsOrderService.detail(goodsOrderId); + if (DeliveryMethod.SELF_PICKUP.equals(order.getDeliveryMethod())) { + throw new BizException("自提订单无需派单"); + } + if (GoodsOrderStatus.PENDING_DELIVERY != order.getStatus()) { + throw new BizException("订单未待配送"); + } + DeliveryOrderResponse task = createBaseTask(order.getTenantId(), order.getCommunityId(), rider); + task.setBizType(BIZ_TYPE_GOODS_ORDER); + task.setBizOrderId(order.getId()); + task.setBizOrderNo(order.getOrderNo()); + task.setAddress(order.getAddressId() == null ? null : "地址ID " + order.getAddressId()); + task.setAmountCent(order.getPayableAmountCent()); + task.setRemark(order.getRemark()); + tasks.put(task.getId(), task); + return copy(task); + } + + public synchronized DeliveryOrderResponse assignExpressOrder(Long expressOrderId, Long riderId) { + if (expressOrderService == null) { + throw new BizException("快递代取服务未启用"); + } + DeliveryUserResponse rider = deliveryUserService.requireEnabled(riderId); + ExpressOrderResponse order = expressOrderService.detail(expressOrderId); + if (ExpressOrderStatus.PENDING_PICKUP != order.getStatus()) { + throw new BizException("快递订单未待取件"); + } + DeliveryOrderResponse task = createBaseTask(order.getTenantId(), order.getCommunityId(), rider); + task.setBizType(BIZ_TYPE_EXPRESS_ORDER); + task.setBizOrderId(order.getId()); + task.setBizOrderNo(order.getOrderNo()); + task.setContactName(order.getContactName()); + task.setContactPhone(order.getContactPhone()); + task.setAddress(order.getDeliveryAddress()); + task.setAmountCent(order.getFeeCent()); + task.setRemark(order.getRemark()); + tasks.put(task.getId(), task); + return copy(task); + } + + public synchronized DeliveryOrderResponse assignGroupBuyOrder(Long communityId, Long groupBuyOrderId, Long riderId) { + DeliveryUserResponse rider = deliveryUserService.requireEnabled(riderId); + DeliveryOrderResponse task = createBaseTask(rider.getTenantId(), communityId, rider); + task.setBizType(BIZ_TYPE_GROUP_BUY_ORDER); + task.setBizOrderId(groupBuyOrderId); + task.setBizOrderNo("GB" + String.format("%08d", groupBuyOrderId)); + tasks.put(task.getId(), task); + return copy(task); + } + + public synchronized List listAdmin(Long communityId, DeliveryTaskStatus status) { + List result = new ArrayList(); + for (DeliveryOrderResponse task : tasks.values()) { + if (!communityId.equals(task.getCommunityId())) { + continue; + } + if (status != null && status != task.getStatus()) { + continue; + } + result.add(copy(task)); + } + return result; + } + + public synchronized DeliveryOrderResponse detail(Long id) { + return copy(require(id)); + } + + public synchronized List listRiderTasks(Long riderId) { + List result = new ArrayList(); + for (DeliveryOrderResponse task : tasks.values()) { + if (riderId.equals(task.getRiderId())) { + result.add(copy(task)); + } + } + return result; + } + + public synchronized DeliveryOrderResponse detailForRider(Long riderId, Long taskId) { + return copy(requireForRider(riderId, taskId)); + } + + synchronized DeliveryOrderResponse requireForRider(Long riderId, Long taskId) { + DeliveryOrderResponse task = require(taskId); + if (!riderId.equals(task.getRiderId())) { + throw new BizException(404, "配送任务不存在"); + } + return task; + } + + synchronized DeliveryOrderResponse copyTask(DeliveryOrderResponse task) { + return copy(task); + } + + private DeliveryOrderResponse createBaseTask(Long tenantId, Long communityId, DeliveryUserResponse rider) { + DeliveryOrderResponse task = new DeliveryOrderResponse(); + task.setId(ids.getAndIncrement()); + task.setTenantId(tenantId); + task.setCommunityId(communityId); + task.setRiderId(rider.getId()); + task.setRiderName(rider.getName()); + task.setStatus(DeliveryTaskStatus.ASSIGNED); + task.setCreatedAt(LocalDateTime.now()); + task.setUpdatedAt(task.getCreatedAt()); + return task; + } + + private DeliveryOrderResponse require(Long id) { + DeliveryOrderResponse task = tasks.get(id); + if (task == null) { + throw new BizException(404, "配送任务不存在"); + } + return task; + } + + private DeliveryOrderResponse copy(DeliveryOrderResponse source) { + DeliveryOrderResponse target = new DeliveryOrderResponse(); + target.setId(source.getId()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setBizType(source.getBizType()); + target.setBizOrderId(source.getBizOrderId()); + target.setBizOrderNo(source.getBizOrderNo()); + target.setRiderId(source.getRiderId()); + target.setRiderName(source.getRiderName()); + target.setContactName(source.getContactName()); + target.setContactPhone(source.getContactPhone()); + target.setAddress(source.getAddress()); + target.setAmountCent(source.getAmountCent()); + target.setRemark(source.getRemark()); + target.setStatus(source.getStatus()); + target.setDeliveredPhotoUrl(source.getDeliveredPhotoUrl()); + target.setExceptionType(source.getExceptionType()); + target.setExceptionNote(source.getExceptionNote()); + target.setCreatedAt(source.getCreatedAt()); + target.setUpdatedAt(source.getUpdatedAt()); + return target; + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/DeliveryOrderResponse.java b/backend/src/main/java/com/linhelp/delivery/DeliveryOrderResponse.java new file mode 100644 index 0000000..640220d --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/DeliveryOrderResponse.java @@ -0,0 +1,179 @@ +package com.linhelp.delivery; + +import com.linhelp.common.enums.DeliveryTaskStatus; + +import java.time.LocalDateTime; + +public class DeliveryOrderResponse { + private Long id; + private Long tenantId; + private Long communityId; + private String bizType; + private Long bizOrderId; + private String bizOrderNo; + private Long riderId; + private String riderName; + private String contactName; + private String contactPhone; + private String address; + private int amountCent; + private String remark; + private DeliveryTaskStatus status; + private String deliveredPhotoUrl; + private String exceptionType; + private String exceptionNote; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public String getBizType() { + return bizType; + } + + public void setBizType(String bizType) { + this.bizType = bizType; + } + + public Long getBizOrderId() { + return bizOrderId; + } + + public void setBizOrderId(Long bizOrderId) { + this.bizOrderId = bizOrderId; + } + + public String getBizOrderNo() { + return bizOrderNo; + } + + public void setBizOrderNo(String bizOrderNo) { + this.bizOrderNo = bizOrderNo; + } + + public Long getRiderId() { + return riderId; + } + + public void setRiderId(Long riderId) { + this.riderId = riderId; + } + + public String getRiderName() { + return riderName; + } + + public void setRiderName(String riderName) { + this.riderName = riderName; + } + + public String getContactName() { + return contactName; + } + + public void setContactName(String contactName) { + this.contactName = contactName; + } + + public String getContactPhone() { + return contactPhone; + } + + public void setContactPhone(String contactPhone) { + this.contactPhone = contactPhone; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public int getAmountCent() { + return amountCent; + } + + public void setAmountCent(int amountCent) { + this.amountCent = amountCent; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public DeliveryTaskStatus getStatus() { + return status; + } + + public void setStatus(DeliveryTaskStatus status) { + this.status = status; + } + + public String getDeliveredPhotoUrl() { + return deliveredPhotoUrl; + } + + public void setDeliveredPhotoUrl(String deliveredPhotoUrl) { + this.deliveredPhotoUrl = deliveredPhotoUrl; + } + + public String getExceptionType() { + return exceptionType; + } + + public void setExceptionType(String exceptionType) { + this.exceptionType = exceptionType; + } + + public String getExceptionNote() { + return exceptionNote; + } + + public void setExceptionNote(String exceptionNote) { + this.exceptionNote = exceptionNote; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/DeliveryUserRequest.java b/backend/src/main/java/com/linhelp/delivery/DeliveryUserRequest.java new file mode 100644 index 0000000..cdb7a76 --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/DeliveryUserRequest.java @@ -0,0 +1,65 @@ +package com.linhelp.delivery; + +import javax.validation.constraints.NotBlank; + +public class DeliveryUserRequest { + private Long tenantId; + private Long communityId; + private Long userId; + + @NotBlank + private String name; + + @NotBlank + private String phone; + + private boolean enabled = true; + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/DeliveryUserResponse.java b/backend/src/main/java/com/linhelp/delivery/DeliveryUserResponse.java new file mode 100644 index 0000000..0ddc228 --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/DeliveryUserResponse.java @@ -0,0 +1,67 @@ +package com.linhelp.delivery; + +public class DeliveryUserResponse { + private Long id; + private Long tenantId; + private Long communityId; + private Long userId; + private String name; + private String phone; + private boolean enabled; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public void setCommunityId(Long communityId) { + this.communityId = communityId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/DeliveryUserService.java b/backend/src/main/java/com/linhelp/delivery/DeliveryUserService.java new file mode 100644 index 0000000..de4912b --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/DeliveryUserService.java @@ -0,0 +1,104 @@ +package com.linhelp.delivery; + +import com.linhelp.common.exception.BizException; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class DeliveryUserService { + private final AtomicLong ids = new AtomicLong(1L); + private final Map users = new LinkedHashMap(); + + public synchronized DeliveryUserResponse create(DeliveryUserRequest request) { + DeliveryUserResponse user = new DeliveryUserResponse(); + user.setId(ids.getAndIncrement()); + user.setTenantId(request.getTenantId()); + user.setCommunityId(request.getCommunityId()); + copyFields(request, user); + users.put(user.getId(), user); + return copy(user); + } + + public synchronized DeliveryUserResponse update(Long id, DeliveryUserRequest request) { + DeliveryUserResponse user = require(id); + if (request.getTenantId() != null) { + user.setTenantId(request.getTenantId()); + } + if (request.getCommunityId() != null) { + user.setCommunityId(request.getCommunityId()); + } + copyFields(request, user); + return copy(user); + } + + public synchronized List list(Long communityId) { + List result = new ArrayList(); + for (DeliveryUserResponse user : users.values()) { + if (communityId.equals(user.getCommunityId())) { + result.add(copy(user)); + } + } + return result; + } + + public synchronized DeliveryUserResponse enable(Long id) { + DeliveryUserResponse user = require(id); + user.setEnabled(true); + return copy(user); + } + + public synchronized DeliveryUserResponse disable(Long id) { + DeliveryUserResponse user = require(id); + user.setEnabled(false); + return copy(user); + } + + public synchronized DeliveryUserResponse requireEnabled(Long id) { + DeliveryUserResponse user = require(id); + if (!user.isEnabled()) { + throw new BizException("配送员已停用"); + } + return copy(user); + } + + public synchronized DeliveryUserResponse requireByUserId(Long userId) { + for (DeliveryUserResponse user : users.values()) { + if (userId.equals(user.getUserId())) { + return copy(user); + } + } + throw new BizException(404, "配送员不存在"); + } + + private DeliveryUserResponse require(Long id) { + DeliveryUserResponse user = users.get(id); + if (user == null) { + throw new BizException(404, "配送员不存在"); + } + return user; + } + + private void copyFields(DeliveryUserRequest request, DeliveryUserResponse user) { + user.setUserId(request.getUserId()); + user.setName(request.getName()); + user.setPhone(request.getPhone()); + user.setEnabled(request.isEnabled()); + } + + private DeliveryUserResponse copy(DeliveryUserResponse source) { + DeliveryUserResponse target = new DeliveryUserResponse(); + target.setId(source.getId()); + target.setTenantId(source.getTenantId()); + target.setCommunityId(source.getCommunityId()); + target.setUserId(source.getUserId()); + target.setName(source.getName()); + target.setPhone(source.getPhone()); + target.setEnabled(source.isEnabled()); + return target; + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/RiderDeliveredRequest.java b/backend/src/main/java/com/linhelp/delivery/RiderDeliveredRequest.java new file mode 100644 index 0000000..10165ad --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/RiderDeliveredRequest.java @@ -0,0 +1,13 @@ +package com.linhelp.delivery; + +public class RiderDeliveredRequest { + private String photoUrl; + + public String getPhotoUrl() { + return photoUrl; + } + + public void setPhotoUrl(String photoUrl) { + this.photoUrl = photoUrl; + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/RiderExceptionRequest.java b/backend/src/main/java/com/linhelp/delivery/RiderExceptionRequest.java new file mode 100644 index 0000000..ea334ba --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/RiderExceptionRequest.java @@ -0,0 +1,26 @@ +package com.linhelp.delivery; + +import javax.validation.constraints.NotBlank; + +public class RiderExceptionRequest { + @NotBlank + private String exceptionType; + + private String exceptionNote; + + public String getExceptionType() { + return exceptionType; + } + + public void setExceptionType(String exceptionType) { + this.exceptionType = exceptionType; + } + + public String getExceptionNote() { + return exceptionNote; + } + + public void setExceptionNote(String exceptionNote) { + this.exceptionNote = exceptionNote; + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/RiderIncomeSummaryResponse.java b/backend/src/main/java/com/linhelp/delivery/RiderIncomeSummaryResponse.java new file mode 100644 index 0000000..a6dd084 --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/RiderIncomeSummaryResponse.java @@ -0,0 +1,40 @@ +package com.linhelp.delivery; + +public class RiderIncomeSummaryResponse { + private int assignedCount; + private int deliveringCount; + private int deliveredCount; + private int estimatedIncomeCent; + + public int getAssignedCount() { + return assignedCount; + } + + public void setAssignedCount(int assignedCount) { + this.assignedCount = assignedCount; + } + + public int getDeliveringCount() { + return deliveringCount; + } + + public void setDeliveringCount(int deliveringCount) { + this.deliveringCount = deliveringCount; + } + + public int getDeliveredCount() { + return deliveredCount; + } + + public void setDeliveredCount(int deliveredCount) { + this.deliveredCount = deliveredCount; + } + + public int getEstimatedIncomeCent() { + return estimatedIncomeCent; + } + + public void setEstimatedIncomeCent(int estimatedIncomeCent) { + this.estimatedIncomeCent = estimatedIncomeCent; + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/RiderTaskController.java b/backend/src/main/java/com/linhelp/delivery/RiderTaskController.java new file mode 100644 index 0000000..f1a83dc --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/RiderTaskController.java @@ -0,0 +1,65 @@ +package com.linhelp.delivery; + +import com.linhelp.common.api.ApiResponse; +import com.linhelp.common.security.AuthService; +import com.linhelp.common.security.CurrentUser; +import com.linhelp.common.security.RequireRole; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequireRole("RIDER") +public class RiderTaskController { + private final RiderTaskService riderTaskService; + private final DeliveryUserService deliveryUserService; + private final AuthService authService; + + public RiderTaskController(RiderTaskService riderTaskService, + DeliveryUserService deliveryUserService, + AuthService authService) { + this.riderTaskService = riderTaskService; + this.deliveryUserService = deliveryUserService; + this.authService = authService; + } + + @GetMapping("/api/rider/tasks") + public ApiResponse> listTasks() { + return ApiResponse.ok(riderTaskService.listTasks(currentDeliveryUserId())); + } + + @GetMapping("/api/rider/tasks/{id}") + public ApiResponse detail(@PathVariable Long id) { + return ApiResponse.ok(riderTaskService.detail(currentDeliveryUserId(), id)); + } + + @PutMapping("/api/rider/tasks/{id}/start") + public ApiResponse start(@PathVariable Long id) { + return ApiResponse.ok(riderTaskService.start(currentDeliveryUserId(), id)); + } + + @PutMapping("/api/rider/tasks/{id}/delivered") + public ApiResponse delivered(@PathVariable Long id, @RequestBody(required = false) RiderDeliveredRequest request) { + return ApiResponse.ok(riderTaskService.delivered(currentDeliveryUserId(), id, request == null ? null : request.getPhotoUrl())); + } + + @PutMapping("/api/rider/tasks/{id}/exception") + public ApiResponse exception(@PathVariable Long id, @Valid @RequestBody RiderExceptionRequest request) { + return ApiResponse.ok(riderTaskService.exception(currentDeliveryUserId(), id, request.getExceptionType(), request.getExceptionNote())); + } + + @GetMapping("/api/rider/income-summary") + public ApiResponse incomeSummary() { + return ApiResponse.ok(riderTaskService.incomeSummary(currentDeliveryUserId())); + } + + private Long currentDeliveryUserId() { + CurrentUser user = authService.currentUser(); + return deliveryUserService.requireByUserId(user.getUserId()).getId(); + } +} diff --git a/backend/src/main/java/com/linhelp/delivery/RiderTaskService.java b/backend/src/main/java/com/linhelp/delivery/RiderTaskService.java new file mode 100644 index 0000000..3a6b4c4 --- /dev/null +++ b/backend/src/main/java/com/linhelp/delivery/RiderTaskService.java @@ -0,0 +1,120 @@ +package com.linhelp.delivery; + +import com.linhelp.common.enums.DeliveryTaskStatus; +import com.linhelp.common.enums.ExpressOrderStatus; +import com.linhelp.common.exception.BizException; +import com.linhelp.express.ExpressOrderResponse; +import com.linhelp.express.ExpressOrderService; +import com.linhelp.order.GoodsOrderService; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class RiderTaskService { + private static final int INCOME_PER_DELIVERED_TASK_CENT = 300; + + private final DeliveryAssignmentService assignmentService; + private final GoodsOrderService goodsOrderService; + private final ExpressOrderService expressOrderService; + + public RiderTaskService(DeliveryAssignmentService assignmentService, + GoodsOrderService goodsOrderService, + ExpressOrderService expressOrderService) { + this.assignmentService = assignmentService; + this.goodsOrderService = goodsOrderService; + this.expressOrderService = expressOrderService; + } + + public List listTasks(Long riderId) { + return assignmentService.listRiderTasks(riderId); + } + + public DeliveryOrderResponse detail(Long riderId, Long taskId) { + return assignmentService.detailForRider(riderId, taskId); + } + + public DeliveryOrderResponse start(Long riderId, Long taskId) { + DeliveryOrderResponse task = assignmentService.requireForRider(riderId, taskId); + if (task.getStatus() != DeliveryTaskStatus.ASSIGNED && task.getStatus() != DeliveryTaskStatus.ACCEPTED) { + throw new BizException("任务状态不允许操作"); + } + if (DeliveryAssignmentService.BIZ_TYPE_GOODS_ORDER.equals(task.getBizType())) { + goodsOrderService.markDelivering(task.getBizOrderId()); + } else if (DeliveryAssignmentService.BIZ_TYPE_EXPRESS_ORDER.equals(task.getBizType())) { + startExpressOrder(task.getBizOrderId()); + } + task.setStatus(DeliveryTaskStatus.DELIVERING); + task.setUpdatedAt(LocalDateTime.now()); + return assignmentService.copyTask(task); + } + + public DeliveryOrderResponse delivered(Long riderId, Long taskId, String photoUrl) { + DeliveryOrderResponse task = assignmentService.requireForRider(riderId, taskId); + if (task.getStatus() != DeliveryTaskStatus.DELIVERING) { + throw new BizException("任务状态不允许操作"); + } + if (DeliveryAssignmentService.BIZ_TYPE_GOODS_ORDER.equals(task.getBizType())) { + goodsOrderService.complete(task.getBizOrderId()); + } else if (DeliveryAssignmentService.BIZ_TYPE_EXPRESS_ORDER.equals(task.getBizType())) { + deliverExpressOrder(task.getBizOrderId(), photoUrl); + } + task.setDeliveredPhotoUrl(photoUrl); + task.setStatus(DeliveryTaskStatus.DELIVERED); + task.setUpdatedAt(LocalDateTime.now()); + return assignmentService.copyTask(task); + } + + public DeliveryOrderResponse exception(Long riderId, Long taskId, String exceptionType, String exceptionNote) { + DeliveryOrderResponse task = assignmentService.requireForRider(riderId, taskId); + task.setExceptionType(exceptionType); + task.setExceptionNote(exceptionNote); + task.setStatus(DeliveryTaskStatus.EXCEPTION); + task.setUpdatedAt(LocalDateTime.now()); + return assignmentService.copyTask(task); + } + + public RiderIncomeSummaryResponse incomeSummary(Long riderId) { + RiderIncomeSummaryResponse response = new RiderIncomeSummaryResponse(); + for (DeliveryOrderResponse task : listTasks(riderId)) { + if (task.getStatus() == DeliveryTaskStatus.ASSIGNED || task.getStatus() == DeliveryTaskStatus.ACCEPTED) { + response.setAssignedCount(response.getAssignedCount() + 1); + } else if (task.getStatus() == DeliveryTaskStatus.DELIVERING) { + response.setDeliveringCount(response.getDeliveringCount() + 1); + } else if (task.getStatus() == DeliveryTaskStatus.DELIVERED || task.getStatus() == DeliveryTaskStatus.COMPLETED) { + response.setDeliveredCount(response.getDeliveredCount() + 1); + } + } + response.setEstimatedIncomeCent(response.getDeliveredCount() * INCOME_PER_DELIVERED_TASK_CENT); + return response; + } + + private void startExpressOrder(Long orderId) { + if (expressOrderService == null) { + throw new BizException("快递代取服务未启用"); + } + ExpressOrderResponse order = expressOrderService.detail(orderId); + if (order.getStatus() == ExpressOrderStatus.PENDING_PICKUP) { + expressOrderService.markPickingUp(orderId); + } else if (order.getStatus() == ExpressOrderStatus.PICKING_UP) { + expressOrderService.markDelivering(orderId); + } else { + throw new BizException("快递订单状态不允许操作"); + } + } + + private void deliverExpressOrder(Long orderId, String photoUrl) { + if (expressOrderService == null) { + throw new BizException("快递代取服务未启用"); + } + ExpressOrderResponse order = expressOrderService.detail(orderId); + if (order.getStatus() == ExpressOrderStatus.PENDING_PICKUP) { + expressOrderService.markPickingUp(orderId); + expressOrderService.markDelivering(orderId); + } else if (order.getStatus() == ExpressOrderStatus.PICKING_UP) { + expressOrderService.markDelivering(orderId); + } + expressOrderService.markDelivered(orderId, photoUrl); + } +} diff --git a/backend/src/test/java/com/linhelp/delivery/DeliveryAssignmentServiceTests.java b/backend/src/test/java/com/linhelp/delivery/DeliveryAssignmentServiceTests.java new file mode 100644 index 0000000..7908fb6 --- /dev/null +++ b/backend/src/test/java/com/linhelp/delivery/DeliveryAssignmentServiceTests.java @@ -0,0 +1,81 @@ +package com.linhelp.delivery; + +import com.linhelp.common.enums.DeliveryMethod; +import com.linhelp.common.enums.DeliveryTaskStatus; +import com.linhelp.order.CreateGoodsOrderItemRequest; +import com.linhelp.order.CreateGoodsOrderRequest; +import com.linhelp.order.GoodsOrderResponse; +import com.linhelp.order.GoodsOrderService; +import com.linhelp.product.ProductService; +import com.linhelp.product.ProductSkuResponse; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DeliveryAssignmentServiceTests { + @Test + void adminCanAssignPendingDeliveryGoodsOrderToRider() { + TestFixture fixture = new TestFixture(); + GoodsOrderResponse order = fixture.preparedDeliveryOrder(); + DeliveryUserResponse rider = fixture.rider(); + + DeliveryOrderResponse task = fixture.assignmentService.assignGoodsOrder(order.getId(), rider.getId()); + + assertThat(task.getBizType()).isEqualTo(DeliveryAssignmentService.BIZ_TYPE_GOODS_ORDER); + assertThat(task.getStatus()).isEqualTo(DeliveryTaskStatus.ASSIGNED); + assertThat(task.getRiderId()).isEqualTo(rider.getId()); + } + + @Test + void cannotAssignSelfPickupGoodsOrder() { + TestFixture fixture = new TestFixture(); + GoodsOrderResponse order = fixture.preparedSelfPickupOrder(); + DeliveryUserResponse rider = fixture.rider(); + + assertThatThrownBy(() -> fixture.assignmentService.assignGoodsOrder(order.getId(), rider.getId())) + .hasMessageContaining("自提订单无需派单"); + } + + private static class TestFixture { + private final ProductService productService = new ProductService(); + private final GoodsOrderService goodsOrderService = new GoodsOrderService(productService); + private final DeliveryUserService deliveryUserService = new DeliveryUserService(); + private final DeliveryAssignmentService assignmentService = + new DeliveryAssignmentService(deliveryUserService, goodsOrderService, null); + + private GoodsOrderResponse preparedDeliveryOrder() { + return preparedOrder(DeliveryMethod.IMMEDIATE, 1L); + } + + private GoodsOrderResponse preparedSelfPickupOrder() { + return preparedOrder(DeliveryMethod.SELF_PICKUP, null); + } + + private GoodsOrderResponse preparedOrder(DeliveryMethod deliveryMethod, Long addressId) { + ProductSkuResponse sku = productService.createDemoSku(1L, 1L, 1L, "500g", 990, 5); + CreateGoodsOrderRequest request = new CreateGoodsOrderRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setMerchantId(1L); + request.setAddressId(addressId); + request.setDeliveryMethod(deliveryMethod); + request.setItems(Collections.singletonList(new CreateGoodsOrderItemRequest(1L, sku.getId(), 1))); + GoodsOrderResponse order = goodsOrderService.create(1L, request); + goodsOrderService.confirm(order.getId()); + return goodsOrderService.markPrepared(order.getId()); + } + + private DeliveryUserResponse rider() { + DeliveryUserRequest request = new DeliveryUserRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setUserId(3L); + request.setName("骑手一号"); + request.setPhone("13900000000"); + return deliveryUserService.create(request); + } + } +} diff --git a/backend/src/test/java/com/linhelp/delivery/RiderTaskServiceTests.java b/backend/src/test/java/com/linhelp/delivery/RiderTaskServiceTests.java new file mode 100644 index 0000000..f31fdc9 --- /dev/null +++ b/backend/src/test/java/com/linhelp/delivery/RiderTaskServiceTests.java @@ -0,0 +1,92 @@ +package com.linhelp.delivery; + +import com.linhelp.common.enums.DeliveryMethod; +import com.linhelp.common.enums.DeliveryTaskStatus; +import com.linhelp.common.enums.GoodsOrderStatus; +import com.linhelp.order.CreateGoodsOrderItemRequest; +import com.linhelp.order.CreateGoodsOrderRequest; +import com.linhelp.order.GoodsOrderResponse; +import com.linhelp.order.GoodsOrderService; +import com.linhelp.product.ProductService; +import com.linhelp.product.ProductSkuResponse; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +class RiderTaskServiceTests { + @Test + void riderOnlySeesOwnTasks() { + TestFixture fixture = new TestFixture(); + GoodsOrderResponse order = fixture.preparedDeliveryOrder(); + DeliveryUserResponse firstRider = fixture.rider(3L, "骑手一号"); + DeliveryUserResponse secondRider = fixture.rider(4L, "骑手二号"); + + fixture.assignmentService.assignGoodsOrder(order.getId(), firstRider.getId()); + + assertThat(fixture.riderTaskService.listTasks(firstRider.getId())).hasSize(1); + assertThat(fixture.riderTaskService.listTasks(secondRider.getId())).isEmpty(); + } + + @Test + void startingGoodsOrderTaskMovesGoodsOrderToDelivering() { + TestFixture fixture = new TestFixture(); + GoodsOrderResponse order = fixture.preparedDeliveryOrder(); + DeliveryUserResponse rider = fixture.rider(3L, "骑手一号"); + DeliveryOrderResponse task = fixture.assignmentService.assignGoodsOrder(order.getId(), rider.getId()); + + DeliveryOrderResponse started = fixture.riderTaskService.start(rider.getId(), task.getId()); + + assertThat(started.getStatus()).isEqualTo(DeliveryTaskStatus.DELIVERING); + assertThat(fixture.goodsOrderService.detail(order.getId()).getStatus()).isEqualTo(GoodsOrderStatus.DELIVERING); + } + + @Test + void deliveredTaskStoresPhotoUrl() { + TestFixture fixture = new TestFixture(); + GoodsOrderResponse order = fixture.preparedDeliveryOrder(); + DeliveryUserResponse rider = fixture.rider(3L, "骑手一号"); + DeliveryOrderResponse task = fixture.assignmentService.assignGoodsOrder(order.getId(), rider.getId()); + fixture.riderTaskService.start(rider.getId(), task.getId()); + + DeliveryOrderResponse delivered = fixture.riderTaskService.delivered(rider.getId(), task.getId(), "https://img.test/done.jpg"); + + assertThat(delivered.getStatus()).isEqualTo(DeliveryTaskStatus.DELIVERED); + assertThat(delivered.getDeliveredPhotoUrl()).isEqualTo("https://img.test/done.jpg"); + } + + private static class TestFixture { + private final ProductService productService = new ProductService(); + private final GoodsOrderService goodsOrderService = new GoodsOrderService(productService); + private final DeliveryUserService deliveryUserService = new DeliveryUserService(); + private final DeliveryAssignmentService assignmentService = + new DeliveryAssignmentService(deliveryUserService, goodsOrderService, null); + private final RiderTaskService riderTaskService = + new RiderTaskService(assignmentService, goodsOrderService, null); + + private GoodsOrderResponse preparedDeliveryOrder() { + ProductSkuResponse sku = productService.createDemoSku(1L, 1L, 1L, "500g", 990, 5); + CreateGoodsOrderRequest request = new CreateGoodsOrderRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setMerchantId(1L); + request.setAddressId(1L); + request.setDeliveryMethod(DeliveryMethod.IMMEDIATE); + request.setItems(Collections.singletonList(new CreateGoodsOrderItemRequest(1L, sku.getId(), 1))); + GoodsOrderResponse order = goodsOrderService.create(1L, request); + goodsOrderService.confirm(order.getId()); + return goodsOrderService.markPrepared(order.getId()); + } + + private DeliveryUserResponse rider(Long userId, String name) { + DeliveryUserRequest request = new DeliveryUserRequest(); + request.setTenantId(1L); + request.setCommunityId(1L); + request.setUserId(userId); + request.setName(name); + request.setPhone("13900000000"); + return deliveryUserService.create(request); + } + } +}