fix: complete order measurement downstream flow

This commit is contained in:
2026-09-18 14:58:40 +08:00
parent 02d2474141
commit c8a99f9fff
43 changed files with 621 additions and 52 deletions
@@ -36,6 +36,10 @@ public interface FirstlegInboundClient {
ApiResponse<Void> updateDeclaration(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody OrderDeclarationDtos.UpdatedEvent event);
@PostMapping("/order-remarks-updated")
ApiResponse<Integer> updateRemarks(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody OrderRemarksSyncCommand command);
@PostMapping("/order-channel-changes/validate")
ApiResponse<OrderChannelChangeValidationResponse> validateOrderChannelChange(
@RequestHeader("X-Internal-Service-Token") String token,
@@ -51,6 +55,9 @@ public interface FirstlegInboundClient {
String channelCode) {}
record OrderChannelChangeValidationResponse(long orderId, long orderVersion,
String channelCode, String mainStatus) {}
record OrderRemarksSyncCommand(long tenantId, long orderId, String customerRemark, String internalRemark,
String warehouseRemark, String customerServiceRemark,
long operatorId, String operatorName) {}
record OrderApprovedInboundCommand(
long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName,
@@ -16,6 +16,8 @@ import org.springframework.beans.factory.annotation.Value;
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.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -32,6 +34,7 @@ import java.util.stream.Collectors;
@RequestMapping("/internal/order/v1/cargoes")
public class InternalOrderCargoController {
@Resource private OrderRepository orders;
@Resource private OrderApplicationService orderApplicationService;
@Resource private PrintableInboundSnapshotService printableInboundSnapshots;
@Value("${baiyue.security.internal-service-token}") private String internalToken;
private byte[] expectedToken;
@@ -146,6 +149,21 @@ public class InternalOrderCargoController {
.collect(Collectors.joining("、"))));
}
@PutMapping("/{tenantId}/{orderId}/remarks")
public ApiResponse<InternalRemarksUpdateResponse> updateRemarks(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@PathVariable long tenantId, @PathVariable long orderId,
@RequestBody InternalRemarksUpdateRequest request) {
requireToken(token);
if (tenantId <= 0 || orderId <= 0 || request == null || request.operatorId() <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "客户、订单和操作人必须大于0");
}
var result = orderApplicationService.updateRemarksFromFirstleg(tenantId, request.operatorId(), orderId,
request.internalRemark(), request.warehouseRemark(), request.customerServiceRemark());
return ApiResponse.success(new InternalRemarksUpdateResponse(result.version(), result.customerRemark(),
result.internalRemark(), result.warehouseRemark(), result.customerServiceRemark()));
}
private BigDecimal sum(List<OrderCargoEntity> cargoes,
java.util.function.Function<OrderCargoEntity, BigDecimal> getter) {
List<BigDecimal> values = cargoes.stream().map(getter).filter(Objects::nonNull).toList();
@@ -205,4 +223,10 @@ public class InternalOrderCargoController {
public record MeasurementDeclarationSnapshot(
String referenceId, BigDecimal originalGrossWeightKg,
BigDecimal originalVolumeCbm, String defaultActualGoodsName) {}
public record InternalRemarksUpdateRequest(String internalRemark, String warehouseRemark,
String customerServiceRemark, long operatorId) {}
public record InternalRemarksUpdateResponse(int version, String customerRemark, String internalRemark,
String warehouseRemark, String customerServiceRemark) {}
}
@@ -122,6 +122,7 @@ public class PlatformUnifiedOrderController {
@PathVariable long orderId,
@Valid @RequestBody OrderRequests.RemarksUpdateRequest request) {
long scopedTenant = OrderScopeGuard.platform(user, tenantId);
return ApiResponse.success(orderService.updateRemarks(scopedTenant, user.userId(), orderId, request));
return ApiResponse.success(orderService.updateRemarks(
scopedTenant, user.userId(), user.username(), orderId, request));
}
}
@@ -271,6 +271,12 @@ public class OrderApplicationService {
@Transactional
public OrderRequests.RemarksUpdateResponse updateRemarks(long tenantId, long userId, long id,
OrderRequests.RemarksUpdateRequest request) {
return updateRemarks(tenantId, userId, null, id, request);
}
@Transactional
public OrderRequests.RemarksUpdateResponse updateRemarks(long tenantId, long userId, String operatorName, long id,
OrderRequests.RemarksUpdateRequest request) {
OrderEntity order = require(tenantId, id);
String internalRemark = normalizeRemark(request.internalRemark());
String warehouseRemark = normalizeRemark(request.warehouseRemark());
@@ -284,10 +290,57 @@ public class OrderApplicationService {
note.setTenantId(tenantId); note.setOrderId(id); note.setContent(internalRemark);
note.setCreatedBy(userId); note.setUpdatedBy(userId); repository.insertNote(note);
}
syncRemarksToFirstleg(tenantId, id, order.getCustomerRemark(), internalRemark, warehouseRemark, customerServiceRemark,
userId, operatorName);
return new OrderRequests.RemarksUpdateResponse(order.getVersion() + 1, order.getCustomerRemark(),
internalRemark, warehouseRemark, customerServiceRemark);
}
@Transactional
public OrderRequests.RemarksUpdateResponse updateRemarksFromFirstleg(long tenantId, long userId, long id,
String internalValue,
String warehouseValue,
String customerServiceValue) {
requireRemarkLength(internalValue);
requireRemarkLength(warehouseValue);
requireRemarkLength(customerServiceValue);
OrderEntity order = require(tenantId, id);
String internalRemark = normalizeRemark(internalValue);
String warehouseRemark = normalizeRemark(warehouseValue);
String customerServiceRemark = normalizeRemark(customerServiceValue);
if (repository.updateRemarks(tenantId, id, order.getVersion(), userId,
internalRemark, warehouseRemark, customerServiceRemark) != 1) {
throw new BizException(ResultCode.CONFLICT, "订单备注已被其他人修改,请刷新后重试");
}
if (!Objects.equals(order.getInternalRemark(), internalRemark) && internalRemark != null) {
OrderInternalNoteEntity note = new OrderInternalNoteEntity();
note.setTenantId(tenantId); note.setOrderId(id); note.setContent(internalRemark);
note.setCreatedBy(userId); note.setUpdatedBy(userId); repository.insertNote(note);
}
return new OrderRequests.RemarksUpdateResponse(order.getVersion() + 1, order.getCustomerRemark(),
internalRemark, warehouseRemark, customerServiceRemark);
}
private void syncRemarksToFirstleg(long tenantId, long orderId, String customerRemark, String internalRemark,
String warehouseRemark, String customerServiceRemark,
long operatorId, String operatorName) {
FirstlegInboundClient client = firstlegProvider == null ? null : firstlegProvider.getIfAvailable();
if (client == null) return;
ApiResponse<Integer> response = client.updateRemarks(internalToken,
new FirstlegInboundClient.OrderRemarksSyncCommand(tenantId, orderId, customerRemark, internalRemark,
warehouseRemark, customerServiceRemark, operatorId, operatorName));
if (response == null || !response.successful()) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
response == null ? "头程备注同步暂不可用" : response.message());
}
}
private static void requireRemarkLength(String value) {
if (value != null && value.length() > 1000) {
throw new BizException(ResultCode.BAD_REQUEST, "每项备注不能超过1000个字符");
}
}
private static String normalizeRemark(String value) {
if (value == null || value.trim().isEmpty()) return null;
return value.trim();
@@ -59,6 +59,34 @@ paths:
application/json:
schema: {$ref: '#/components/schemas/CargoLabelBindingListEnvelope'}
'403': {description: 内部服务令牌无效}
put:
security: []
summary: Firstleg 维护订单权威协作备注
description: >-
仅接受内部服务令牌。预装清单编辑备注时写回 Order 权威订单记录;
客户备注保持只读,内部、仓库和客服备注统一按订单版本乐观更新。
parameters:
- name: X-Internal-Service-Token
in: header
required: true
schema: {type: string}
- {name: tenantId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [operatorId]
properties:
internalRemark: {type: string, nullable: true, maxLength: 1000}
warehouseRemark: {type: string, nullable: true, maxLength: 1000}
customerServiceRemark: {type: string, nullable: true, maxLength: 1000}
operatorId: {type: integer, format: int64, minimum: 1}
responses:
'200': {description: 返回订单最新版本和四类备注}
'403': {description: 内部服务令牌无效}
/internal/order/v1/cargoes/{tenantId}/{orderId}/printable-inbound-snapshot:
post:
security: []
@@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.PostMapping;
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class InternalOrderCargoControllerTest {
@MockBean private OrderRepository orders;
@MockBean private OrderApplicationService orderApplicationService;
@MockBean private PrintableInboundSnapshotService printableInboundSnapshots;
@Resource private InternalOrderCargoController controller;
@@ -0,0 +1,53 @@
package com.baiyue.order.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.order.client.FirstlegInboundClient;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.repository.OrderRepository;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class OrderRemarksSynchronizationTest {
@Test
void platformUpdateSynchronizesAuthoritativeRemarksToFirstleg() {
OrderRepository repository = mock(OrderRepository.class);
FirstlegInboundClient firstleg = mock(FirstlegInboundClient.class);
@SuppressWarnings("unchecked")
ObjectProvider<FirstlegInboundClient> provider = mock(ObjectProvider.class);
OrderApplicationService service = new OrderApplicationService();
ReflectionTestUtils.setField(service, "repository", repository);
ReflectionTestUtils.setField(service, "firstlegProvider", provider);
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
OrderEntity order = new OrderEntity();
order.setId(20L);
order.setTenantId(10L);
order.setVersion(7);
order.setCustomerRemark("客户备注");
when(repository.findOwned(10L, 20L)).thenReturn(order);
when(repository.updateRemarks(10L, 20L, 7, 30L, "内部", "仓库", "客服")).thenReturn(1);
when(provider.getIfAvailable()).thenReturn(firstleg);
when(firstleg.updateRemarks(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(2));
var response = service.updateRemarks(10L, 30L, "操作人", 20L,
new OrderRequests.RemarksUpdateRequest(7, " 内部 ", "仓库", "客服"));
assertThat(response.version()).isEqualTo(8);
ArgumentCaptor<FirstlegInboundClient.OrderRemarksSyncCommand> command =
ArgumentCaptor.forClass(FirstlegInboundClient.OrderRemarksSyncCommand.class);
verify(firstleg).updateRemarks(org.mockito.ArgumentMatchers.eq("internal-token"), command.capture());
assertThat(command.getValue().customerRemark()).isEqualTo("客户备注");
assertThat(command.getValue().internalRemark()).isEqualTo("内部");
assertThat(command.getValue().warehouseRemark()).isEqualTo("仓库");
assertThat(command.getValue().customerServiceRemark()).isEqualTo("客服");
}
}