提交
This commit is contained in:
+9
@@ -90,6 +90,15 @@ public class ExceptionOrderController {
|
|||||||
return ApiResponse.success(service.complete(requiredScope(user, customerId), operatorId(user), exceptionNo, request));
|
return ApiResponse.success(service.complete(requiredScope(user, customerId), operatorId(user), exceptionNo, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 选择其他并填写原因后人工完结,仅处理异常工单状态。 */
|
||||||
|
@PostMapping("/exception-orders/{exceptionNo}/force-close")
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_PROCESS + "')")
|
||||||
|
public ApiResponse<ExceptionOrderResponses.CommandResult> forceClose(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||||
|
@PathVariable String exceptionNo, @Valid @RequestBody ExceptionOrderRequests.ForceClose request) {
|
||||||
|
return ApiResponse.success(service.forceClose(requiredScope(user, customerId), operatorId(user), exceptionNo, request));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/exception-orders/{exceptionNo}/cancel")
|
@PostMapping("/exception-orders/{exceptionNo}/cancel")
|
||||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_PROCESS + "')")
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_PROCESS + "')")
|
||||||
public ApiResponse<ExceptionOrderResponses.CommandResult> cancel(
|
public ApiResponse<ExceptionOrderResponses.CommandResult> cancel(
|
||||||
|
|||||||
+5
@@ -59,4 +59,9 @@ public final class ExceptionOrderRequests {
|
|||||||
public record Cancel(@Min(0) long expectedVersion,
|
public record Cancel(@Min(0) long expectedVersion,
|
||||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||||
@NotBlank @Size(max = 1000) String reason) {}
|
@NotBlank @Size(max = 1000) String reason) {}
|
||||||
|
|
||||||
|
/** 其他处理方式必须保留人工完结原因,不提交库存实收或文件。 */
|
||||||
|
public record ForceClose(@Min(0) long expectedVersion,
|
||||||
|
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||||
|
@NotBlank @Size(max = 1000) String reason) {}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -2,6 +2,8 @@ package com.baiyue.lastleg.exceptionorder.repository;
|
|||||||
|
|
||||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrder;
|
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrder;
|
||||||
|
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrderCommand;
|
||||||
|
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderCommandMapper;
|
||||||
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionWorkflowMapper;
|
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionWorkflowMapper;
|
||||||
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderMapper;
|
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderMapper;
|
||||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||||
@@ -11,6 +13,7 @@ import java.util.List;
|
|||||||
/** 异常退回尾程关系及仓库任务的数据访问边界。 */
|
/** 异常退回尾程关系及仓库任务的数据访问边界。 */
|
||||||
@Repository
|
@Repository
|
||||||
public class ExceptionWorkflowRepository {
|
public class ExceptionWorkflowRepository {
|
||||||
|
@jakarta.annotation.Resource private ExceptionOrderCommandMapper commands;
|
||||||
private final ExceptionWorkflowMapper workflow;
|
private final ExceptionWorkflowMapper workflow;
|
||||||
private final ExceptionOrderMapper exceptions;
|
private final ExceptionOrderMapper exceptions;
|
||||||
private final LastlegOrderMapper orders;
|
private final LastlegOrderMapper orders;
|
||||||
@@ -22,6 +25,14 @@ public class ExceptionWorkflowRepository {
|
|||||||
public List<ExceptionWorkflowMapper.Delivery> deliveries(long id) { return workflow.deliveries(id); }
|
public List<ExceptionWorkflowMapper.Delivery> deliveries(long id) { return workflow.deliveries(id); }
|
||||||
/** 在客户范围锁定异常。 */
|
/** 在客户范围锁定异常。 */
|
||||||
public ExceptionOrder lock(String customer, String number) { return exceptions.lockByNo(customer,number); }
|
public ExceptionOrder lock(String customer, String number) { return exceptions.lockByNo(customer,number); }
|
||||||
|
/** 保存人工完结后的异常状态,保留关联单据和实际库存事实。 */
|
||||||
|
public void updateException(ExceptionOrder order) { exceptions.updateById(order); }
|
||||||
|
/** 在客户及动作范围读取人工完结的幂等结果。 */
|
||||||
|
public ExceptionOrderCommand closeCommand(String customer, String key) {
|
||||||
|
return commands.findByKey(customer, "FORCE_CLOSE", key);
|
||||||
|
}
|
||||||
|
/** 同事务保存人工完结原因与原操作结果。 */
|
||||||
|
public void insertCommand(ExceptionOrderCommand command) { commands.insert(command); }
|
||||||
/** 在冻结仓范围锁定异常。 */
|
/** 在冻结仓范围锁定异常。 */
|
||||||
public ExceptionOrder lockWarehouse(String warehouse, String number) { return workflow.lockWarehouse(warehouse,number); }
|
public ExceptionOrder lockWarehouse(String warehouse, String number) { return workflow.lockWarehouse(warehouse,number); }
|
||||||
/** 查询原尾程单以冻结来源关系。 */
|
/** 查询原尾程单以冻结来源关系。 */
|
||||||
|
|||||||
+34
-2
@@ -330,6 +330,35 @@ public class ExceptionOrderService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 其他处理方式只完结异常工单,不伪造退回、签收、库存或运输完成事实。 */
|
||||||
|
@Transactional
|
||||||
|
public ExceptionOrderResponses.CommandResult forceClose(String customerId, String operatorId,
|
||||||
|
String exceptionNo, ExceptionOrderRequests.ForceClose request) {
|
||||||
|
String reason = trim(request.reason());
|
||||||
|
if (reason == null || reason.length() > 1000) throw badRequest("请选择其他处理方式并填写完结原因(最多1000字)");
|
||||||
|
ExceptionOrder order = workflow.lock(customerId, exceptionNo);
|
||||||
|
if (order == null) throw notFound("异常单不存在");
|
||||||
|
String requestHash = hash(new ExceptionOrderRequests.ForceClose(request.expectedVersion(), request.idempotencyKey().trim(), reason));
|
||||||
|
// 同一异常父锁内先恢复已提交结果,避免重复点击推进版本或重复写入审计命令。
|
||||||
|
ExceptionOrderCommand replay = workflow.closeCommand(customerId, request.idempotencyKey().trim());
|
||||||
|
if (replay != null) {
|
||||||
|
if (!Objects.equals(replay.getExceptionOrderId(), order.getId())) throw conflict("幂等键已用于其他异常单");
|
||||||
|
return replayResult(replay, requestHash);
|
||||||
|
}
|
||||||
|
if (!java.util.Set.of("PENDING", "PROCESSING").contains(Objects.requireNonNullElse(order.getStatus(), ""))
|
||||||
|
|| version(order) != request.expectedVersion()) throw conflict("异常状态或版本已变化,请刷新");
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
order.setStatus("COMPLETED"); order.setHandlingAction("其他"); order.setHandlingResult(reason);
|
||||||
|
order.setHandlerId(operatorId); order.setCompletedAt(now); order.setUpdatedAt(now); order.setVersion(version(order) + 1);
|
||||||
|
workflow.updateException(order);
|
||||||
|
var result = commandResult(order, 0, false);
|
||||||
|
var command = new ExceptionOrderCommand();
|
||||||
|
command.setId(IdWorker.getId()); command.setCustomerId(customerId); command.setExceptionOrderId(order.getId());
|
||||||
|
command.setAction("FORCE_CLOSE"); command.setIdempotencyKey(request.idempotencyKey().trim()); command.setRequestHash(requestHash);
|
||||||
|
command.setResultJson(write(result)); command.setCreatedAt(now); workflow.insertCommand(command);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/** 一次实收的普通库存、原箱账本及完成状态同事务提交,失败全部回滚。 */
|
/** 一次实收的普通库存、原箱账本及完成状态同事务提交,失败全部回滚。 */
|
||||||
@Transactional
|
@Transactional
|
||||||
public ExceptionOrderResponses.CommandResult complete(String customerId, String operatorId,
|
public ExceptionOrderResponses.CommandResult complete(String customerId, String operatorId,
|
||||||
@@ -688,14 +717,17 @@ public class ExceptionOrderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 所有未结束异常均可选择其他方式,原退回及正常完成动作继续按资格开放。 */
|
||||||
private List<String> allowedActions(ExceptionOrder order) {
|
private List<String> allowedActions(ExceptionOrder order) {
|
||||||
return switch (Objects.requireNonNullElse(order.getStatus(), "")) {
|
List<String> actions = new ArrayList<>(switch (Objects.requireNonNullElse(order.getStatus(), "")) {
|
||||||
case "PENDING" -> "RETURN".equals(order.getExceptionType()) && "OVERSEAS_WAREHOUSE".equals(order.getReturnDirection())
|
case "PENDING" -> "RETURN".equals(order.getExceptionType()) && "OVERSEAS_WAREHOUSE".equals(order.getReturnDirection())
|
||||||
? List.of("GENERATE_RETURN_INBOUND","CANCEL_EXCEPTION") : List.of("START_PROCESSING", "CANCEL_EXCEPTION");
|
? List.of("GENERATE_RETURN_INBOUND","CANCEL_EXCEPTION") : List.of("START_PROCESSING", "CANCEL_EXCEPTION");
|
||||||
case "PROCESSING" -> "RETURN".equals(order.getExceptionType()) && "OVERSEAS_WAREHOUSE".equals(order.getReturnDirection()) && workflow.returnOrder(order)==null
|
case "PROCESSING" -> "RETURN".equals(order.getExceptionType()) && "OVERSEAS_WAREHOUSE".equals(order.getReturnDirection()) && workflow.returnOrder(order)==null
|
||||||
? List.of("GENERATE_RETURN_INBOUND") : List.of("COMPLETE_EXCEPTION");
|
? List.of("GENERATE_RETURN_INBOUND") : List.of("COMPLETE_EXCEPTION");
|
||||||
default -> List.of();
|
default -> List.of();
|
||||||
};
|
});
|
||||||
|
if (java.util.Set.of("PENDING", "PROCESSING").contains(Objects.requireNonNullElse(order.getStatus(), ""))) actions.add("FORCE_CLOSE");
|
||||||
|
return List.copyOf(actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExceptionOrderResponses.CommandResult commandResult(ExceptionOrder order, long restoredPackages,
|
private ExceptionOrderResponses.CommandResult commandResult(ExceptionOrder order, long restoredPackages,
|
||||||
|
|||||||
@@ -2309,6 +2309,16 @@ paths:
|
|||||||
parameters: [{ name: exceptionNo, in: path, required: true, schema: { type: string } }]
|
parameters: [{ name: exceptionNo, in: path, required: true, schema: { type: string } }]
|
||||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ExceptionComplete' } } } }
|
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ExceptionComplete' } } } }
|
||||||
responses: { '200': { description: 异常处理与库存恢复结果 }, '409': { description: 版本、状态、归属、数量或幂等冲突 } }
|
responses: { '200': { description: 异常处理与库存恢复结果 }, '409': { description: 版本、状态、归属、数量或幂等冲突 } }
|
||||||
|
/api/platform/v1/last-leg/exception-orders/{exceptionNo}/force-close:
|
||||||
|
post:
|
||||||
|
summary: 选择其他方式人工完结异常
|
||||||
|
description: 权限 platform:overseas-warehouse:exception-order:process;所有类型的 PENDING/PROCESSING 异常均可填写原因后转 COMPLETED,无需文件及实收信息。只更新异常处理方式、原因、处理人及完成时间,保留原有实收、证据和库存事实,不推进关联退回单、派送或车辆资源。expectedVersion 校验并在异常父锁内保存幂等结果。
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
parameters:
|
||||||
|
- { name: exceptionNo, in: path, required: true, schema: { type: string } }
|
||||||
|
- { name: customerId, in: query, required: true, schema: { type: string } }
|
||||||
|
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ExceptionForceClose' } } } }
|
||||||
|
responses: { '200': { description: 人工完结结果 }, '400': { description: 未填写原因 }, '409': { description: 版本、状态或幂等冲突 } }
|
||||||
/api/platform/v1/last-leg/exception-orders/{exceptionNo}/cancel:
|
/api/platform/v1/last-leg/exception-orders/{exceptionNo}/cancel:
|
||||||
post:
|
post:
|
||||||
summary: 取消未处理异常订单
|
summary: 取消未处理异常订单
|
||||||
@@ -4193,6 +4203,13 @@ components:
|
|||||||
expectedVersion: { type: integer, format: int64, minimum: 0 }
|
expectedVersion: { type: integer, format: int64, minimum: 0 }
|
||||||
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||||
reason: { type: string, minLength: 1, maxLength: 1000 }
|
reason: { type: string, minLength: 1, maxLength: 1000 }
|
||||||
|
ExceptionForceClose:
|
||||||
|
type: object
|
||||||
|
required: [expectedVersion, idempotencyKey, reason]
|
||||||
|
properties:
|
||||||
|
expectedVersion: { type: integer, format: int64, minimum: 0 }
|
||||||
|
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||||
|
reason: { type: string, minLength: 1, maxLength: 1000, description: 其他处理方式的人工完结原因 }
|
||||||
ExceptionComplete:
|
ExceptionComplete:
|
||||||
type: object
|
type: object
|
||||||
required: [expectedVersion, idempotencyKey, handlingResult, evidenceFileIds]
|
required: [expectedVersion, idempotencyKey, handlingResult, evidenceFileIds]
|
||||||
@@ -4218,7 +4235,7 @@ components:
|
|||||||
targetLocationCode: { type: string, minLength: 1, maxLength: 128 }
|
targetLocationCode: { type: string, minLength: 1, maxLength: 128 }
|
||||||
ExceptionAllowedAction:
|
ExceptionAllowedAction:
|
||||||
type: string
|
type: string
|
||||||
enum: [START_PROCESSING, COMPLETE_EXCEPTION, CANCEL_EXCEPTION]
|
enum: [START_PROCESSING, COMPLETE_EXCEPTION, CANCEL_EXCEPTION, GENERATE_RETURN_INBOUND, FORCE_CLOSE]
|
||||||
DeliveryFeeInput:
|
DeliveryFeeInput:
|
||||||
type: object
|
type: object
|
||||||
required: [feeType, feeName, direction, currency, amount]
|
required: [feeType, feeName, direction, currency, amount]
|
||||||
|
|||||||
+2
@@ -29,6 +29,7 @@ class Phase5AdminPermissionContractTest {
|
|||||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("uploadEvidence"));
|
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("uploadEvidence"));
|
||||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("startProcessing"));
|
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("startProcessing"));
|
||||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("complete"));
|
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("complete"));
|
||||||
|
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("forceClose"));
|
||||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("cancel"));
|
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_PROCESS), permissions.get("cancel"));
|
||||||
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_LIST), permissions.get("returnOptions"));
|
assertEquals(authority(OverseasPermissions.EXCEPTION_ORDER_LIST), permissions.get("returnOptions"));
|
||||||
assertEquals(authority(OverseasPermissions.DELIVERY_RETURN), permissions.get("markReturn"));
|
assertEquals(authority(OverseasPermissions.DELIVERY_RETURN), permissions.get("markReturn"));
|
||||||
@@ -69,6 +70,7 @@ class Phase5AdminPermissionContractTest {
|
|||||||
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}:",
|
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}:",
|
||||||
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/start-processing:",
|
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/start-processing:",
|
||||||
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/complete:",
|
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/complete:",
|
||||||
|
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/force-close:",
|
||||||
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/cancel:",
|
"/api/platform/v1/last-leg/exception-orders/{exceptionNo}/cancel:",
|
||||||
"/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/mark-return:",
|
"/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/mark-return:",
|
||||||
"/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/status:",
|
"/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/status:",
|
||||||
|
|||||||
Reference in New Issue
Block a user