提交
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));
|
||||
}
|
||||
|
||||
/** 选择其他并填写原因后人工完结,仅处理异常工单状态。 */
|
||||
@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")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_PROCESS + "')")
|
||||
public ApiResponse<ExceptionOrderResponses.CommandResult> cancel(
|
||||
|
||||
+5
@@ -59,4 +59,9 @@ public final class ExceptionOrderRequests {
|
||||
public record Cancel(@Min(0) long expectedVersion,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@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.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.ExceptionOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
@@ -11,6 +13,7 @@ import java.util.List;
|
||||
/** 异常退回尾程关系及仓库任务的数据访问边界。 */
|
||||
@Repository
|
||||
public class ExceptionWorkflowRepository {
|
||||
@jakarta.annotation.Resource private ExceptionOrderCommandMapper commands;
|
||||
private final ExceptionWorkflowMapper workflow;
|
||||
private final ExceptionOrderMapper exceptions;
|
||||
private final LastlegOrderMapper orders;
|
||||
@@ -22,6 +25,14 @@ public class ExceptionWorkflowRepository {
|
||||
public List<ExceptionWorkflowMapper.Delivery> deliveries(long id) { return workflow.deliveries(id); }
|
||||
/** 在客户范围锁定异常。 */
|
||||
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); }
|
||||
/** 查询原尾程单以冻结来源关系。 */
|
||||
|
||||
+34
-2
@@ -330,6 +330,35 @@ public class ExceptionOrderService {
|
||||
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
|
||||
public ExceptionOrderResponses.CommandResult complete(String customerId, String operatorId,
|
||||
@@ -688,14 +717,17 @@ public class ExceptionOrderService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 所有未结束异常均可选择其他方式,原退回及正常完成动作继续按资格开放。 */
|
||||
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())
|
||||
? 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
|
||||
? List.of("GENERATE_RETURN_INBOUND") : List.of("COMPLETE_EXCEPTION");
|
||||
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,
|
||||
|
||||
@@ -2309,6 +2309,16 @@ paths:
|
||||
parameters: [{ name: exceptionNo, in: path, required: true, schema: { type: string } }]
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ExceptionComplete' } } } }
|
||||
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:
|
||||
post:
|
||||
summary: 取消未处理异常订单
|
||||
@@ -4193,6 +4203,13 @@ components:
|
||||
expectedVersion: { type: integer, format: int64, minimum: 0 }
|
||||
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||
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:
|
||||
type: object
|
||||
required: [expectedVersion, idempotencyKey, handlingResult, evidenceFileIds]
|
||||
@@ -4218,7 +4235,7 @@ components:
|
||||
targetLocationCode: { type: string, minLength: 1, maxLength: 128 }
|
||||
ExceptionAllowedAction:
|
||||
type: string
|
||||
enum: [START_PROCESSING, COMPLETE_EXCEPTION, CANCEL_EXCEPTION]
|
||||
enum: [START_PROCESSING, COMPLETE_EXCEPTION, CANCEL_EXCEPTION, GENERATE_RETURN_INBOUND, FORCE_CLOSE]
|
||||
DeliveryFeeInput:
|
||||
type: object
|
||||
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("startProcessing"));
|
||||
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_LIST), permissions.get("returnOptions"));
|
||||
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}/start-processing:",
|
||||
"/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/delivery-orders/{deliveryNo}/mark-return:",
|
||||
"/api/platform/v1/last-leg/delivery-orders/{deliveryNo}/status:",
|
||||
|
||||
Reference in New Issue
Block a user