提交
This commit is contained in:
@@ -16569,6 +16569,35 @@ PREPARE delivery_detail_stmt FROM @ddl; EXECUTE delivery_detail_stmt; DEALLOCATE
|
||||
CREATE TABLE IF NOT EXISTS ll_delivery_fee (id BIGINT NOT NULL COMMENT '派送费用主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', delivery_order_id BIGINT NOT NULL COMMENT '派送单ID', fee_direction VARCHAR(16) NOT NULL DEFAULT 'PAYABLE' COMMENT '应收应付方向', fee_type VARCHAR(64) NOT NULL COMMENT '费用类型', currency VARCHAR(8) NOT NULL COMMENT '币种', amount DECIMAL(18,2) NOT NULL COMMENT '费用金额', remark VARCHAR(500) NULL COMMENT '费用备注', created_by VARCHAR(128) NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY(id), KEY idx_ll_delivery_fee_order(customer_id,delivery_order_id,id)) ENGINE=InnoDB COMMENT='派送单费用明细';
|
||||
CREATE TABLE IF NOT EXISTS ll_delivery_operation (id BIGINT NOT NULL COMMENT '派送操作主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', delivery_order_id BIGINT NOT NULL COMMENT '派送单ID', operation_node VARCHAR(64) NOT NULL COMMENT '操作节点', operator_id VARCHAR(128) NULL COMMENT '操作人ID', operator_name VARCHAR(128) NULL COMMENT '操作人名称', result VARCHAR(500) NOT NULL COMMENT '处理结果', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间', PRIMARY KEY(id), KEY idx_ll_delivery_operation_order(customer_id,delivery_order_id,created_at,id)) ENGINE=InnoDB COMMENT='派送单操作记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_exception_order (
|
||||
id BIGINT NOT NULL, customer_id VARCHAR(128) NOT NULL, exception_no VARCHAR(64) NOT NULL,
|
||||
exception_type VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'PENDING', reason VARCHAR(1000) NOT NULL,
|
||||
last_mile_order_id BIGINT NULL, last_mile_order_no VARCHAR(64) NULL, inbound_order_id BIGINT NULL, inbound_code VARCHAR(128) NULL,
|
||||
delivery_order_id BIGINT NULL, delivery_no VARCHAR(64) NULL, outbound_no VARCHAR(64) NULL,
|
||||
customer_name VARCHAR(255) NULL, overseas_warehouse_id VARCHAR(128) NULL, overseas_warehouse_name VARCHAR(255) NULL,
|
||||
sku_code VARCHAR(128) NULL, package_count INT NULL, return_direction VARCHAR(32) NULL, return_logistics_no VARCHAR(128) NULL,
|
||||
handling_action VARCHAR(500) NULL, handler_id VARCHAR(128) NULL, handler_name VARCHAR(128) NULL,
|
||||
source_type VARCHAR(32) NOT NULL DEFAULT 'MANUAL', created_by VARCHAR(128) NULL,
|
||||
exception_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_exception_no(customer_id,exception_no),
|
||||
UNIQUE KEY uk_ll_exception_delivery_return(customer_id,delivery_order_id,exception_type),
|
||||
KEY idx_ll_exception_list(customer_id,status,exception_at), KEY idx_ll_exception_tail(customer_id,last_mile_order_no),
|
||||
KEY idx_ll_exception_inbound(customer_id,inbound_code)
|
||||
) ENGINE=InnoDB COMMENT='海外仓异常订单';
|
||||
|
||||
INSERT INTO baiyue_iam.iam_permission
|
||||
(id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,route_path,http_method,sort_no,status,built_in,deleted)
|
||||
VALUES
|
||||
(5107070,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:exception-order:list','海外异常订单','/overseas-warehouse/exception-orders','GET',770,'ACTIVE',1,0),
|
||||
(5107071,0,'PLATFORM','BUTTON',5107070,'platform:overseas-warehouse:exception-order:create','新增海外异常订单',NULL,'POST',771,'ACTIVE',1,0),
|
||||
(5107072,0,'PLATFORM','BUTTON',5107070,'platform:overseas-warehouse:delivery:return','派送单标记退货',NULL,'POST',772,'ACTIVE',1,0)
|
||||
ON DUPLICATE KEY UPDATE permission_name=VALUES(permission_name),route_path=VALUES(route_path),http_method=VALUES(http_method),sort_no=VALUES(sort_no),status='ACTIVE',built_in=1,deleted=0;
|
||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id,role_id,permission_id,created_at)
|
||||
SELECT 0,role.id,permission.id,NOW(3) FROM baiyue_iam.iam_role role
|
||||
JOIN baiyue_iam.iam_permission permission ON permission.id BETWEEN 5107070 AND 5107072 AND permission.tenant_id=0
|
||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='PLATFORM_ADMIN' AND role.status='ACTIVE' AND role.deleted=0;
|
||||
|
||||
CALL baiyue_firstleg.assert_unified_final_schema();
|
||||
DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema;
|
||||
SELECT 'PASS' AS migration_status,
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.baiyue.lastleg.exceptionorder.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderRequests;
|
||||
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderResponses;
|
||||
import com.baiyue.lastleg.exceptionorder.service.ExceptionOrderService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@ConditionalOnProperty(name = "lastleg.future-outbound-api.enabled", havingValue = "true")
|
||||
@RequestMapping("/api/platform/v1/last-leg")
|
||||
public class ExceptionOrderController {
|
||||
@Resource private ExceptionOrderService service;
|
||||
|
||||
@GetMapping("/exception-orders")
|
||||
public ApiResponse<PageResult<ExceptionOrderResponses.Row>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid ExceptionOrderRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user), query));
|
||||
}
|
||||
|
||||
@GetMapping("/exception-orders/resolve")
|
||||
public ApiResponse<ExceptionOrderResponses.Relation> resolve(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@RequestParam(required = false) String inboundCode,
|
||||
@RequestParam(required = false) String lastMileOrderNo) {
|
||||
return ApiResponse.success(service.resolve(scope(user), customerId, inboundCode, lastMileOrderNo));
|
||||
}
|
||||
|
||||
@PostMapping("/exception-orders")
|
||||
public ApiResponse<ExceptionOrderResponses.Created> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ExceptionOrderRequests.Create request) {
|
||||
return ApiResponse.success(service.create(scope(user), operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/delivery-orders/{deliveryNo}/return-inbound-options")
|
||||
public ApiResponse<List<ExceptionOrderResponses.Relation>> returnOptions(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String deliveryNo) {
|
||||
return ApiResponse.success(service.returnCandidates(requiredScope(user, customerId), deliveryNo));
|
||||
}
|
||||
|
||||
@PostMapping("/delivery-orders/{deliveryNo}/mark-return")
|
||||
public ApiResponse<ExceptionOrderResponses.Created> markReturn(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String deliveryNo,
|
||||
@Valid @RequestBody ExceptionOrderRequests.MarkReturn request) {
|
||||
return ApiResponse.success(service.markReturn(requiredScope(user, customerId), operator(user), deliveryNo, request));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
return "PLATFORM".equals(user.appCode()) && user.tenantId() == 0L ? null : String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
private String requiredScope(AuthenticatedUser user, String requestedCustomer) {
|
||||
String own = scope(user);
|
||||
if (own != null) return own;
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "请选择客户");
|
||||
return requestedCustomer.trim();
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.lastleg.exceptionorder.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public final class ExceptionOrderRequests {
|
||||
private ExceptionOrderRequests() {}
|
||||
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String keyword, String exceptionType, String status,
|
||||
String customerId, String warehouseId,
|
||||
LocalDateTime createdFrom, LocalDateTime createdTo) {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
|
||||
public record Create(String customerId, @Size(max = 128) String inboundCode,
|
||||
@Size(max = 64) String lastMileOrderNo,
|
||||
@NotBlank @Size(max = 32) String exceptionType,
|
||||
@NotBlank @Size(max = 1000) String reason,
|
||||
@Min(1) Integer packageCount,
|
||||
@Size(max = 128) String skuCode) {}
|
||||
|
||||
public record MarkReturn(@NotNull Long inboundOrderId,
|
||||
@NotBlank @Size(max = 1000) String reason,
|
||||
@NotBlank @Size(max = 32) String returnDirection,
|
||||
@Size(max = 128) String returnLogisticsNo,
|
||||
@Min(1) Integer packageCount,
|
||||
@Size(max = 128) String skuCode) {}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.lastleg.exceptionorder.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public final class ExceptionOrderResponses {
|
||||
private ExceptionOrderResponses() {}
|
||||
|
||||
public record Row(Long id, String exceptionNo, String exceptionType, String status,
|
||||
String reason, String customerId, String customerName,
|
||||
String overseasWarehouseId, String overseasWarehouseName,
|
||||
Long lastMileOrderId, String lastMileOrderNo,
|
||||
Long inboundOrderId, String inboundCode,
|
||||
Long deliveryOrderId, String deliveryNo, String outboundNo,
|
||||
String skuCode, Integer packageCount, String returnDirection,
|
||||
String returnLogisticsNo, String handlingAction,
|
||||
String handlerName, LocalDateTime exceptionAt, LocalDateTime updatedAt) {}
|
||||
|
||||
public record Relation(Long lastMileOrderId, String lastMileOrderNo,
|
||||
Long inboundOrderId, String inboundCode,
|
||||
String customerId, String customerName,
|
||||
String overseasWarehouseId, String overseasWarehouseName,
|
||||
String deliveryNo, String outboundNo, Integer availablePackages) {}
|
||||
|
||||
public record Created(Long id, String exceptionNo, String status) {}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.baiyue.lastleg.exceptionorder.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_exception_order")
|
||||
public class ExceptionOrder {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String customerId;
|
||||
private String exceptionNo;
|
||||
private String exceptionType;
|
||||
private String status;
|
||||
private String reason;
|
||||
private Long lastMileOrderId;
|
||||
private String lastMileOrderNo;
|
||||
private Long inboundOrderId;
|
||||
private String inboundCode;
|
||||
private Long deliveryOrderId;
|
||||
private String deliveryNo;
|
||||
private String outboundNo;
|
||||
private String customerName;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseName;
|
||||
private String skuCode;
|
||||
private Integer packageCount;
|
||||
private String returnDirection;
|
||||
private String returnLogisticsNo;
|
||||
private String handlingAction;
|
||||
private String handlerId;
|
||||
private String handlerName;
|
||||
private String sourceType;
|
||||
private String createdBy;
|
||||
private LocalDateTime exceptionAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.lastleg.exceptionorder.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrder;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ExceptionOrderMapper extends BaseMapper<ExceptionOrder> {
|
||||
@Select("SELECT * FROM ll_exception_order WHERE customer_id=#{customerId} AND delivery_order_id=#{deliveryOrderId} " +
|
||||
"AND exception_type='RETURN' LIMIT 1")
|
||||
ExceptionOrder findDeliveryReturn(@Param("customerId") String customerId,
|
||||
@Param("deliveryOrderId") Long deliveryOrderId);
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
package com.baiyue.lastleg.exceptionorder.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.delivery.entity.DeliveryOperation;
|
||||
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
|
||||
import com.baiyue.lastleg.delivery.mapper.DeliveryOperationMapper;
|
||||
import com.baiyue.lastleg.delivery.mapper.DeliveryOrderMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegInboundOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderRequests;
|
||||
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderResponses;
|
||||
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrder;
|
||||
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegInboundOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class ExceptionOrderService {
|
||||
@Resource private ExceptionOrderMapper exceptionOrders;
|
||||
@Resource private LastlegOrderMapper lastMileOrders;
|
||||
@Resource private LastlegInboundOrderMapper inboundOrders;
|
||||
@Resource private DeliveryOrderMapper deliveryOrders;
|
||||
@Resource private DeliveryOperationMapper deliveryOperations;
|
||||
|
||||
public PageResult<ExceptionOrderResponses.Row> page(String scopedCustomerId, ExceptionOrderRequests.Query query) {
|
||||
String customerId = customerScope(scopedCustomerId, query.customerId());
|
||||
Page<ExceptionOrder> result = exceptionOrders.selectPage(new Page<>(query.page(), query.size()),
|
||||
new LambdaQueryWrapper<ExceptionOrder>()
|
||||
.eq(StringUtils.hasText(customerId), ExceptionOrder::getCustomerId, customerId)
|
||||
.and(StringUtils.hasText(query.keyword()), wrapper -> wrapper
|
||||
.like(ExceptionOrder::getExceptionNo, trim(query.keyword()))
|
||||
.or().like(ExceptionOrder::getLastMileOrderNo, trim(query.keyword()))
|
||||
.or().like(ExceptionOrder::getInboundCode, trim(query.keyword()))
|
||||
.or().like(ExceptionOrder::getDeliveryNo, trim(query.keyword()))
|
||||
.or().like(ExceptionOrder::getCustomerName, trim(query.keyword()))
|
||||
.or().like(ExceptionOrder::getReason, trim(query.keyword())))
|
||||
.eq(StringUtils.hasText(query.exceptionType()), ExceptionOrder::getExceptionType, trim(query.exceptionType()))
|
||||
.eq(StringUtils.hasText(query.status()), ExceptionOrder::getStatus, trim(query.status()))
|
||||
.eq(StringUtils.hasText(query.warehouseId()), ExceptionOrder::getOverseasWarehouseId, trim(query.warehouseId()))
|
||||
.ge(query.createdFrom() != null, ExceptionOrder::getExceptionAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, ExceptionOrder::getExceptionAt, query.createdTo())
|
||||
.orderByDesc(ExceptionOrder::getExceptionAt).orderByDesc(ExceptionOrder::getId));
|
||||
return new PageResult<>(result.getRecords().stream().map(this::row).toList(), result.getCurrent(),
|
||||
result.getSize(), result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
public ExceptionOrderResponses.Relation resolve(String scopedCustomerId, String requestedCustomerId,
|
||||
String inboundCode, String lastMileOrderNo) {
|
||||
return relation(scopedCustomerId, requestedCustomerId, inboundCode, lastMileOrderNo);
|
||||
}
|
||||
|
||||
public List<ExceptionOrderResponses.Relation> returnCandidates(String customerId, String deliveryNo) {
|
||||
DeliveryOrder delivery = deliveryOrders.findByNo(customerId, deliveryNo);
|
||||
if (delivery == null) throw notFound("派送单不存在");
|
||||
return lastMileOrders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId)
|
||||
.eq(LastlegOrder::getDeliveryOrderNo, deliveryNo)
|
||||
.orderByAsc(LastlegOrder::getOrderNo)).stream()
|
||||
.map(order -> relation(customerId, customerId, order.getInboundCode(), order.getOrderNo()))
|
||||
.distinct().toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ExceptionOrderResponses.Created create(String scopedCustomerId, String operatorId,
|
||||
ExceptionOrderRequests.Create request) {
|
||||
ExceptionOrderResponses.Relation relation = relation(scopedCustomerId, request.customerId(),
|
||||
request.inboundCode(), request.lastMileOrderNo());
|
||||
return insert(relation, null, normalizeType(request.exceptionType()), request.reason(), request.packageCount(),
|
||||
request.skuCode(), null, null, "MANUAL", operatorId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ExceptionOrderResponses.Created markReturn(String customerId, String operatorId, String deliveryNo,
|
||||
ExceptionOrderRequests.MarkReturn request) {
|
||||
DeliveryOrder delivery = deliveryOrders.findByNo(customerId, deliveryNo);
|
||||
if (delivery == null) throw notFound("派送单不存在");
|
||||
ExceptionOrder existing = exceptionOrders.findDeliveryReturn(customerId, delivery.getId());
|
||||
if (existing != null) return new ExceptionOrderResponses.Created(existing.getId(), existing.getExceptionNo(), existing.getStatus());
|
||||
|
||||
List<ExceptionOrderResponses.Relation> candidates = returnCandidates(customerId, deliveryNo);
|
||||
ExceptionOrderResponses.Relation selected = candidates.stream()
|
||||
.filter(candidate -> Objects.equals(candidate.inboundOrderId(), request.inboundOrderId()))
|
||||
.findFirst().orElseThrow(() -> conflict("所选入仓单不属于该派送单"));
|
||||
ExceptionOrderResponses.Created created = insert(selected, delivery, "RETURN", request.reason(),
|
||||
request.packageCount(), request.skuCode(), request.returnDirection(), request.returnLogisticsNo(),
|
||||
"DELIVERY_RETURN", operatorId);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
delivery.setStatus("RETURN_PENDING");
|
||||
delivery.setUpdatedAt(now);
|
||||
deliveryOrders.updateById(delivery);
|
||||
LastlegOrder tail = lastMileOrders.selectById(selected.lastMileOrderId());
|
||||
if (tail != null) {
|
||||
tail.setAbnormal(true);
|
||||
tail.setSubOrderStatus("RETURN_PENDING");
|
||||
tail.setUpdatedAt(now);
|
||||
lastMileOrders.updateById(tail);
|
||||
}
|
||||
DeliveryOperation operation = new DeliveryOperation();
|
||||
operation.setCustomerId(customerId); operation.setDeliveryOrderId(delivery.getId());
|
||||
operation.setOperationNode("MARK_RETURN"); operation.setOperatorId(operatorId); operation.setOperatorName(operatorId);
|
||||
operation.setResult("已生成异常退货单 " + created.exceptionNo()); operation.setCreatedAt(now);
|
||||
deliveryOperations.insert(operation);
|
||||
return created;
|
||||
}
|
||||
|
||||
private ExceptionOrderResponses.Relation relation(String scopedCustomerId, String requestedCustomerId,
|
||||
String inboundCode, String lastMileOrderNo) {
|
||||
String customerId = customerScope(scopedCustomerId, requestedCustomerId);
|
||||
String inbound = trim(inboundCode);
|
||||
String tailNo = trim(lastMileOrderNo);
|
||||
if (!StringUtils.hasText(inbound) && !StringUtils.hasText(tailNo)) {
|
||||
throw badRequest("入仓码和尾程订单号至少填写一个");
|
||||
}
|
||||
LastlegOrder tail = StringUtils.hasText(tailNo) ? uniqueTail(customerId, tailNo) : null;
|
||||
LastlegInboundOrder inboundOrder = StringUtils.hasText(inbound) ? uniqueInbound(customerId, inbound) : null;
|
||||
if (tail != null && inboundOrder != null && !Objects.equals(tail.getInboundCode(), inboundOrder.getInboundCode())) {
|
||||
throw conflict("尾程订单与入仓单不是同一关联关系");
|
||||
}
|
||||
if (tail == null && inboundOrder != null) tail = optionalTail(inboundOrder.getCustomerId(), inboundOrder.getInboundCode());
|
||||
if (inboundOrder == null && tail != null) inboundOrder = optionalInbound(tail.getCustomerId(), tail.getInboundCode());
|
||||
if (tail == null && inboundOrder == null) throw notFound("未找到对应单据");
|
||||
String resolvedCustomer = tail != null ? tail.getCustomerId() : inboundOrder.getCustomerId();
|
||||
if (tail != null && inboundOrder != null && !Objects.equals(tail.getCustomerId(), inboundOrder.getCustomerId())) {
|
||||
throw conflict("尾程订单与入仓单不属于同一客户");
|
||||
}
|
||||
return new ExceptionOrderResponses.Relation(
|
||||
tail == null ? null : tail.getId(), tail == null ? null : tail.getOrderNo(),
|
||||
inboundOrder == null ? null : inboundOrder.getId(), inboundOrder == null ? null : inboundOrder.getInboundCode(),
|
||||
resolvedCustomer, tail == null ? null : tail.getCustomerName(),
|
||||
tail == null ? null : tail.getOverseasWarehouseId(), tail == null ? null : tail.getOverseasWarehouseName(),
|
||||
tail == null ? null : tail.getDeliveryOrderNo(), tail == null ? null : tail.getOutboundOrderNo(),
|
||||
tail == null ? inboundOrder.getPackageCount() : tail.getPackageCount());
|
||||
}
|
||||
|
||||
private ExceptionOrderResponses.Created insert(ExceptionOrderResponses.Relation relation, DeliveryOrder delivery,
|
||||
String type, String reason, Integer packages, String sku,
|
||||
String direction, String logisticsNo, String sourceType,
|
||||
String operatorId) {
|
||||
if (packages != null && relation.availablePackages() != null && packages > relation.availablePackages()) {
|
||||
throw conflict("异常箱数不能大于订单箱数 " + relation.availablePackages());
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ExceptionOrder value = new ExceptionOrder();
|
||||
value.setId(IdWorker.getId()); value.setCustomerId(relation.customerId());
|
||||
value.setExceptionNo("EXC" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + value.getId());
|
||||
value.setExceptionType(type); value.setStatus("PENDING"); value.setReason(trim(reason));
|
||||
value.setLastMileOrderId(relation.lastMileOrderId()); value.setLastMileOrderNo(relation.lastMileOrderNo());
|
||||
value.setInboundOrderId(relation.inboundOrderId()); value.setInboundCode(relation.inboundCode());
|
||||
value.setDeliveryOrderId(delivery == null ? null : delivery.getId());
|
||||
value.setDeliveryNo(delivery == null ? relation.deliveryNo() : delivery.getDeliveryNo());
|
||||
value.setOutboundNo(delivery == null ? relation.outboundNo() : delivery.getOutboundNo());
|
||||
value.setCustomerName(relation.customerName());
|
||||
value.setOverseasWarehouseId(delivery == null ? relation.overseasWarehouseId() : delivery.getOverseasWarehouseId());
|
||||
value.setOverseasWarehouseName(delivery == null ? relation.overseasWarehouseName() : delivery.getOverseasWarehouseName());
|
||||
value.setSkuCode(trim(sku)); value.setPackageCount(packages); value.setReturnDirection(trim(direction));
|
||||
value.setReturnLogisticsNo(trim(logisticsNo));
|
||||
if (StringUtils.hasText(direction)) {
|
||||
value.setHandlingAction("OVERSEAS_WAREHOUSE".equals(direction) ? "退回海外仓" : "CUSTOMER".equals(direction) ? "退回客户" : "其他退货处理");
|
||||
value.setHandlerId(operatorId); value.setHandlerName(operatorId);
|
||||
}
|
||||
value.setSourceType(sourceType); value.setCreatedBy(operatorId);
|
||||
value.setExceptionAt(now); value.setCreatedAt(now); value.setUpdatedAt(now);
|
||||
try { exceptionOrders.insert(value); }
|
||||
catch (DuplicateKeyException exception) { throw conflict("该关联单据已存在异常退货单"); }
|
||||
return new ExceptionOrderResponses.Created(value.getId(), value.getExceptionNo(), value.getStatus());
|
||||
}
|
||||
|
||||
private LastlegOrder uniqueTail(String customerId, String orderNo) {
|
||||
List<LastlegOrder> values = lastMileOrders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||
.eq(LastlegOrder::getOrderNo, orderNo).last("LIMIT 2"));
|
||||
if (values.isEmpty()) throw notFound("尾程订单不存在");
|
||||
if (values.size() > 1) throw conflict("尾程订单号不唯一,请先选择客户");
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
private LastlegInboundOrder uniqueInbound(String customerId, String inboundCode) {
|
||||
List<LastlegInboundOrder> values = inboundOrders.selectList(new LambdaQueryWrapper<LastlegInboundOrder>()
|
||||
.eq(StringUtils.hasText(customerId), LastlegInboundOrder::getCustomerId, customerId)
|
||||
.eq(LastlegInboundOrder::getInboundCode, inboundCode).last("LIMIT 2"));
|
||||
if (values.isEmpty()) throw notFound("入仓单不存在");
|
||||
if (values.size() > 1) throw conflict("入仓码不唯一,请先选择客户");
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
private LastlegOrder optionalTail(String customerId, String inboundCode) {
|
||||
List<LastlegOrder> values = lastMileOrders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getInboundCode, inboundCode).last("LIMIT 2"));
|
||||
if (values.size() > 1) throw conflict("该入仓码关联了多个尾程订单,请修复关联关系后重试");
|
||||
return values.size() == 1 ? values.get(0) : null;
|
||||
}
|
||||
|
||||
private LastlegInboundOrder optionalInbound(String customerId, String inboundCode) {
|
||||
if (!StringUtils.hasText(inboundCode)) return null;
|
||||
List<LastlegInboundOrder> values = inboundOrders.selectList(new LambdaQueryWrapper<LastlegInboundOrder>()
|
||||
.eq(LastlegInboundOrder::getCustomerId, customerId).eq(LastlegInboundOrder::getInboundCode, inboundCode).last("LIMIT 2"));
|
||||
if (values.size() > 1) throw conflict("该尾程订单关联了多个入仓单,请修复关联关系后重试");
|
||||
return values.size() == 1 ? values.get(0) : null;
|
||||
}
|
||||
|
||||
private ExceptionOrderResponses.Row row(ExceptionOrder value) {
|
||||
return new ExceptionOrderResponses.Row(value.getId(), value.getExceptionNo(), value.getExceptionType(), value.getStatus(),
|
||||
value.getReason(), value.getCustomerId(), value.getCustomerName(), value.getOverseasWarehouseId(),
|
||||
value.getOverseasWarehouseName(), value.getLastMileOrderId(), value.getLastMileOrderNo(),
|
||||
value.getInboundOrderId(), value.getInboundCode(), value.getDeliveryOrderId(), value.getDeliveryNo(),
|
||||
value.getOutboundNo(), value.getSkuCode(), value.getPackageCount(), value.getReturnDirection(),
|
||||
value.getReturnLogisticsNo(), value.getHandlingAction(), value.getHandlerName(),
|
||||
value.getExceptionAt(), value.getUpdatedAt());
|
||||
}
|
||||
|
||||
private String customerScope(String scopedCustomerId, String requestedCustomerId) {
|
||||
if (StringUtils.hasText(scopedCustomerId)) {
|
||||
if (StringUtils.hasText(requestedCustomerId) && !scopedCustomerId.equals(trim(requestedCustomerId))) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "不能访问其他客户数据");
|
||||
}
|
||||
return scopedCustomerId;
|
||||
}
|
||||
return trim(requestedCustomerId);
|
||||
}
|
||||
|
||||
private static String normalizeType(String value) {
|
||||
String normalized = trim(value).toUpperCase();
|
||||
if (!java.util.Set.of("RETURN", "REJECT", "DAMAGED", "OTHER").contains(normalized)) throw badRequest("不支持的异常类型");
|
||||
return normalized;
|
||||
}
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
@@ -1703,3 +1703,20 @@ CREATE TABLE IF NOT EXISTS ll_local_looseboard_operation (
|
||||
UNIQUE KEY uk_ll_local_looseboard_operation_key(customer_id,order_id,idempotency_key),
|
||||
KEY idx_ll_local_looseboard_operation_order(customer_id,order_id,created_at)
|
||||
) ENGINE=InnoDB COMMENT='本土散板订单幂等操作账';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_exception_order (
|
||||
id BIGINT NOT NULL, customer_id VARCHAR(128) NOT NULL, exception_no VARCHAR(64) NOT NULL,
|
||||
exception_type VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'PENDING', reason VARCHAR(1000) NOT NULL,
|
||||
last_mile_order_id BIGINT NULL, last_mile_order_no VARCHAR(64) NULL, inbound_order_id BIGINT NULL, inbound_code VARCHAR(128) NULL,
|
||||
delivery_order_id BIGINT NULL, delivery_no VARCHAR(64) NULL, outbound_no VARCHAR(64) NULL,
|
||||
customer_name VARCHAR(255) NULL, overseas_warehouse_id VARCHAR(128) NULL, overseas_warehouse_name VARCHAR(255) NULL,
|
||||
sku_code VARCHAR(128) NULL, package_count INT NULL, return_direction VARCHAR(32) NULL, return_logistics_no VARCHAR(128) NULL,
|
||||
handling_action VARCHAR(500) NULL, handler_id VARCHAR(128) NULL, handler_name VARCHAR(128) NULL,
|
||||
source_type VARCHAR(32) NOT NULL DEFAULT 'MANUAL', created_by VARCHAR(128) NULL,
|
||||
exception_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_exception_no(customer_id,exception_no),
|
||||
UNIQUE KEY uk_ll_exception_delivery_return(customer_id,delivery_order_id,exception_type),
|
||||
KEY idx_ll_exception_list(customer_id,status,exception_at), KEY idx_ll_exception_tail(customer_id,last_mile_order_no),
|
||||
KEY idx_ll_exception_inbound(customer_id,inbound_code)
|
||||
) ENGINE=InnoDB COMMENT='海外仓异常订单';
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.baiyue.lastleg.exceptionorder.service;
|
||||
|
||||
import com.baiyue.lastleg.entity.LastlegInboundOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegInboundOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ExceptionOrderServiceTest {
|
||||
@Mock private ExceptionOrderMapper exceptionOrders;
|
||||
@Mock private LastlegOrderMapper lastMileOrders;
|
||||
@Mock private LastlegInboundOrderMapper inboundOrders;
|
||||
private ExceptionOrderService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ExceptionOrderService();
|
||||
ReflectionTestUtils.setField(service, "exceptionOrders", exceptionOrders);
|
||||
ReflectionTestUtils.setField(service, "lastMileOrders", lastMileOrders);
|
||||
ReflectionTestUtils.setField(service, "inboundOrders", inboundOrders);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesTheOneToOneInboundAndLastMileRelation() {
|
||||
LastlegOrder tail = tail("LM-1", "IN-1");
|
||||
LastlegInboundOrder inbound = inbound("IN-1");
|
||||
when(lastMileOrders.selectList(any())).thenReturn(List.of(tail));
|
||||
when(inboundOrders.selectList(any())).thenReturn(List.of(inbound));
|
||||
|
||||
var relation = service.resolve(null, null, "IN-1", "LM-1");
|
||||
|
||||
assertThat(relation.lastMileOrderNo()).isEqualTo("LM-1");
|
||||
assertThat(relation.inboundCode()).isEqualTo("IN-1");
|
||||
assertThat(relation.customerId()).isEqualTo("1001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInputsThatExistButDoNotBelongTogether() {
|
||||
when(lastMileOrders.selectList(any())).thenReturn(List.of(tail("LM-1", "IN-1")));
|
||||
when(inboundOrders.selectList(any())).thenReturn(List.of(inbound("IN-2")));
|
||||
|
||||
assertThatThrownBy(() -> service.resolve(null, null, "IN-2", "LM-1"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("不是同一关联关系");
|
||||
}
|
||||
|
||||
private LastlegOrder tail(String orderNo, String inboundCode) {
|
||||
LastlegOrder value = new LastlegOrder();
|
||||
value.setId(12L); value.setCustomerId("1001"); value.setOrderNo(orderNo); value.setInboundCode(inboundCode);
|
||||
value.setCustomerName("测试客户"); value.setPackageCount(10);
|
||||
return value;
|
||||
}
|
||||
|
||||
private LastlegInboundOrder inbound(String inboundCode) {
|
||||
LastlegInboundOrder value = new LastlegInboundOrder();
|
||||
value.setId(11L); value.setCustomerId("1001"); value.setInboundCode(inboundCode); value.setPackageCount(10);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -3848,3 +3848,20 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
|
||||
PREPARE delivery_detail_stmt FROM @ddl; EXECUTE delivery_detail_stmt; DEALLOCATE PREPARE delivery_detail_stmt;
|
||||
CREATE TABLE IF NOT EXISTS ll_delivery_fee (id BIGINT NOT NULL COMMENT '派送费用主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', delivery_order_id BIGINT NOT NULL COMMENT '派送单ID', fee_direction VARCHAR(16) NOT NULL DEFAULT 'PAYABLE' COMMENT '应收应付方向', fee_type VARCHAR(64) NOT NULL COMMENT '费用类型', currency VARCHAR(8) NOT NULL COMMENT '币种', amount DECIMAL(18,2) NOT NULL COMMENT '费用金额', remark VARCHAR(500) NULL COMMENT '费用备注', created_by VARCHAR(128) NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY(id), KEY idx_ll_delivery_fee_order(customer_id,delivery_order_id,id)) ENGINE=InnoDB COMMENT='派送单费用明细';
|
||||
CREATE TABLE IF NOT EXISTS ll_delivery_operation (id BIGINT NOT NULL COMMENT '派送操作主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', delivery_order_id BIGINT NOT NULL COMMENT '派送单ID', operation_node VARCHAR(64) NOT NULL COMMENT '操作节点', operator_id VARCHAR(128) NULL COMMENT '操作人ID', operator_name VARCHAR(128) NULL COMMENT '操作人名称', result VARCHAR(500) NOT NULL COMMENT '处理结果', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间', PRIMARY KEY(id), KEY idx_ll_delivery_operation_order(customer_id,delivery_order_id,created_at,id)) ENGINE=InnoDB COMMENT='派送单操作记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_exception_order (
|
||||
id BIGINT NOT NULL, customer_id VARCHAR(128) NOT NULL, exception_no VARCHAR(64) NOT NULL,
|
||||
exception_type VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'PENDING', reason VARCHAR(1000) NOT NULL,
|
||||
last_mile_order_id BIGINT NULL, last_mile_order_no VARCHAR(64) NULL, inbound_order_id BIGINT NULL, inbound_code VARCHAR(128) NULL,
|
||||
delivery_order_id BIGINT NULL, delivery_no VARCHAR(64) NULL, outbound_no VARCHAR(64) NULL,
|
||||
customer_name VARCHAR(255) NULL, overseas_warehouse_id VARCHAR(128) NULL, overseas_warehouse_name VARCHAR(255) NULL,
|
||||
sku_code VARCHAR(128) NULL, package_count INT NULL, return_direction VARCHAR(32) NULL, return_logistics_no VARCHAR(128) NULL,
|
||||
handling_action VARCHAR(500) NULL, handler_id VARCHAR(128) NULL, handler_name VARCHAR(128) NULL,
|
||||
source_type VARCHAR(32) NOT NULL DEFAULT 'MANUAL', created_by VARCHAR(128) NULL,
|
||||
exception_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_exception_no(customer_id,exception_no),
|
||||
UNIQUE KEY uk_ll_exception_delivery_return(customer_id,delivery_order_id,exception_type),
|
||||
KEY idx_ll_exception_list(customer_id,status,exception_at), KEY idx_ll_exception_tail(customer_id,last_mile_order_no),
|
||||
KEY idx_ll_exception_inbound(customer_id,inbound_code)
|
||||
) ENGINE=InnoDB COMMENT='海外仓异常订单';
|
||||
|
||||
Reference in New Issue
Block a user