This commit is contained in:
wujinliang
2026-08-27 21:48:47 +08:00
parent daf0ee2acd
commit 9d54c57b7c
38 changed files with 432 additions and 37 deletions
@@ -71,7 +71,7 @@ public class MerchantFirstLegOrderController {
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody OrderRequests.SaveRequest request) {
return ApiResponse.success(service.createDraft(
OrderScopeGuard.merchant(user), user.userId(), request, idempotencyKey));
OrderScopeGuard.merchant(user), user.userId(), user.username(), request, idempotencyKey));
}
@BusinessAudit(event = "ORDER_BATCH_CREATE", resourceType = "FIRST_LEG_ORDER", description = "Batch create drafts / 批量创建草稿")
@@ -84,7 +84,7 @@ public class MerchantFirstLegOrderController {
@RequestHeader("Idempotency-Key") String idempotencyKey,
@RequestBody List<MerchantBatchDraftDtos.Item> items) {
return ApiResponse.success(batchDraftService.create(
OrderScopeGuard.merchant(user), user.userId(), idempotencyKey, items));
OrderScopeGuard.merchant(user), user.userId(), user.username(), idempotencyKey, items));
}
@BusinessAudit(event = "ORDER_UPDATE", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "Update draft / 修改草稿")
@@ -69,7 +69,7 @@ public class PlatformFirstLegOrderController {
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody OrderRequests.SaveRequest request) {
long scopedTenant = OrderScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.createDraft(scopedTenant, user.userId(), request, idempotencyKey));
return ApiResponse.success(service.createDraft(scopedTenant, user.userId(), user.username(), request, idempotencyKey));
}
@BusinessAudit(event = "ORDER_UPDATE", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "平台编辑订单资料")
@@ -16,6 +16,7 @@ public record MerchantInboundOrderRow(
String inboundCode,
String channelName,
String destinationType,
String pickupMethod,
String customerName,
String receivingCompanyName,
String specificationModel,
@@ -20,6 +20,12 @@ public class MerchantBatchDraftItemService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public OrderEntity create(long tenantId, long userId, OrderRequests.SaveRequest draft, String idempotencyKey) {
return create(tenantId, userId, null, draft, idempotencyKey);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public OrderEntity create(long tenantId, long userId, String username,
OrderRequests.SaveRequest draft, String idempotencyKey) {
var violations = validator.validate(draft);
if (!violations.isEmpty()) {
String message = violations.stream()
@@ -29,7 +35,7 @@ public class MerchantBatchDraftItemService {
.orElse("草稿参数不合法");
throw new BizException(ResultCode.VALIDATION_FAILED, message);
}
Long id = orders.createDraft(tenantId, userId, draft, idempotencyKey);
Long id = orders.createDraft(tenantId, userId, username, draft, idempotencyKey);
return repository.findOwned(tenantId, id);
}
}
@@ -22,6 +22,11 @@ public class MerchantBatchDraftService {
public MerchantBatchDraftDtos.Response create(long tenantId, long userId, String batchKey,
List<MerchantBatchDraftDtos.Item> items) {
return create(tenantId, userId, null, batchKey, items);
}
public MerchantBatchDraftDtos.Response create(long tenantId, long userId, String username,
String batchKey, List<MerchantBatchDraftDtos.Item> items) {
if (!StringUtils.hasText(batchKey)) throw new BizException("必须提供 Idempotency-Key");
if (batchKey.trim().length() > 128) throw new BizException("Idempotency-Key 长度不能超过128");
if (items == null || items.isEmpty()) throw new BizException("批量草稿不能为空");
@@ -44,7 +49,7 @@ public class MerchantBatchDraftService {
continue;
}
try {
OrderEntity order = itemService.create(tenantId, userId, item.draft(), derivedKey(batchKey, rowId));
OrderEntity order = itemService.create(tenantId, userId, username, item.draft(), derivedKey(batchKey, rowId));
results.add(MerchantBatchDraftDtos.Result.success(index, rowId, order.getId(), order.getOrderNo(), order.getInboundCode()));
} catch (Exception ex) {
String code = ex instanceof BizException be ? be.getResultCode().code() : "S0500";
@@ -128,7 +128,7 @@ public class MerchantInboundOrderListService {
inbound == null ? null : inbound.inboundId(),
inbound == null || inbound.inboundCode() == null || inbound.inboundCode().isBlank()
? order.getInboundCode() : inbound.inboundCode(), order.getChannelName(),
order.getDestinationType(), order.getCustomerName(),
order.getDestinationType(), order.getPickupMethod(), order.getCustomerName(),
consignee == null ? null : consignee.getCompanyName(), joined(cargoes, OrderCargoEntity::getModel),
order.getStatus(), totalQuantity, grossWeight, netWeight, volume,
commonUnitPrice(cargoes), declaredAmount, joined(cargoes, OrderCargoEntity::getCurrency),
@@ -59,17 +59,25 @@ public class OrderApplicationService {
/** 创建草稿时使用数据库唯一键保证请求幂等。 */
@Transactional
public Long createDraft(long tenantId, long userId, OrderRequests.SaveRequest request, String idempotencyKey) {
return createDraft(tenantId, userId, null, request, idempotencyKey);
}
/** 创建草稿时同时保存创建操作人的名称快照,供平台订单详情展示。 */
@Transactional
public Long createDraft(long tenantId, long userId, String username,
OrderRequests.SaveRequest request, String idempotencyKey) {
IdempotencyContext idempotency = beginIdempotency(tenantId, userId, "CREATE_DRAFT", 0L, idempotencyKey,
requestHash("CREATE_DRAFT", request));
if (idempotency.replayed()) {
return idempotency.record().getResultOrderId();
}
Long orderId = createDraftInternal(tenantId, userId, request);
Long orderId = createDraftInternal(tenantId, userId, username, request);
completeIdempotency(idempotency, orderId);
return orderId;
}
private Long createDraftInternal(long tenantId, long userId, OrderRequests.SaveRequest request) {
private Long createDraftInternal(long tenantId, long userId, String username,
OrderRequests.SaveRequest request) {
productImageGuard.requireOwned(tenantId, request.cargoes());
var snapshots = masterDataGuard.validate(tenantId, request);
requirePricingChannel(tenantId, request.receivingWarehouseId(), request.channelCode(),
@@ -86,7 +94,7 @@ public class OrderApplicationService {
throw duplicateInboundCode(order, duplicate);
}
saveChildren(tenantId, userId, order.getId(), request);
record(tenantId, order.getId(), null, OrderStatus.DRAFT, "CREATE", null, userId, null);
record(tenantId, order.getId(), null, OrderStatus.DRAFT, "CREATE", null, userId, username);
return order.getId();
}
@@ -79,8 +79,18 @@ public class UnifiedOrderQueryService {
FirstlegUnifiedOrderClient.Detail inbound = order.getStatus() == OrderStatus.SUBMITTED
? null : firstlegDetail(tenantId, orderId);
String latestNote = orderDetail.internalNotes().isEmpty() ? null : orderDetail.internalNotes().get(0).getContent();
String creatorName = orderDetail.statusHistory().stream()
.filter(item -> "CREATE".equals(item.getAction()))
.map(item -> item.getOperatorName())
.filter(StringUtils::hasText)
.findFirst()
.orElseGet(() -> orderDetail.statusHistory().stream()
.map(item -> item.getOperatorName())
.filter(StringUtils::hasText)
.findFirst()
.orElse(null));
UnifiedOrderResponse.Row summary = row(order, inbound == null ? null : inbound.summary(), latestNote,
childCounts(List.of(order)), CargoSummary.from(orderDetail.cargoes()));
childCounts(List.of(order)), CargoSummary.from(orderDetail.cargoes()), creatorName);
List<UnifiedOrderResponse.TimelineEvent> timeline = new ArrayList<>();
orderDetail.statusHistory().forEach(item -> timeline.add(new UnifiedOrderResponse.TimelineEvent(
"ORDER", "STATUS", orderMainStatus(item.getToStatus()), item.getAction(), item.getReason(),
@@ -174,6 +184,12 @@ public class UnifiedOrderQueryService {
private UnifiedOrderResponse.Row row(OrderEntity order, FirstlegUnifiedOrderClient.Summary summary,
String latestInternalNote, Map<Long, Integer> childCounts,
CargoSummary cargo) {
return row(order, summary, latestInternalNote, childCounts, cargo, null);
}
private UnifiedOrderResponse.Row row(OrderEntity order, FirstlegUnifiedOrderClient.Summary summary,
String latestInternalNote, Map<Long, Integer> childCounts,
CargoSummary cargo, String creatorName) {
if (summary == null) {
String mainStatus = orderMainStatus(order.getStatus());
return new UnifiedOrderResponse.Row(order.getId(), order.getTenantId(), order.getOrderNo(), mainStatus,
@@ -187,7 +203,7 @@ public class UnifiedOrderQueryService {
null, null, null, null, order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(),
null, null, null, null, netWeightKg(cargo), null, null, order.getExpressCompany(), order.getExpressTrackingNo(),
string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo),
StringUtils.hasText(creatorName) ? creatorName : string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo),
null, null, null, null, null,
order.getCustomerRemark(), latestInternalNote,
order.getCreatedAt(), order.getUpdatedAt(), order.getRequestedPickupAt());
@@ -206,7 +222,7 @@ public class UnifiedOrderQueryService {
inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(),
inbound.actualNetWeightKg() != null ? inbound.actualNetWeightKg() : netWeightKg(cargo),
inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(),
inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound),
StringUtils.hasText(creatorName) ? creatorName : inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound),
preload(summary.preload()), loading(summary.loading()),
delivery(summary.delivery()), transport(inbound), order.getCustomerRemark(), latestInternalNote,
order.getCreatedAt(), order.getUpdatedAt(), inbound.requestedPickupAt());
@@ -748,6 +748,7 @@ components:
inboundId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
inboundCode: {type: string}
channelName: {type: string}
pickupMethod: {type: string, enum: [SELF_EXPRESS, PICKUP, SELF_TRUCK], nullable: true}
destinationWarehouseName: {type: string}
status: {type: string}
packageCount: {type: integer}
@@ -41,6 +41,7 @@ class MerchantInboundOrderListServiceTest {
order.setStatus(OrderStatus.APPROVED);
order.setChannelName("美森");
order.setDestinationType("FBA");
order.setPickupMethod("SELF_TRUCK");
order.setDestinationWarehouseName("ONT8");
order.setCustomerName("测试客户");
order.setShipmentId("FBA-SHIPMENT-77");
@@ -82,6 +83,7 @@ class MerchantInboundOrderListServiceTest {
assertThat(row.receivingCompanyName()).isEqualTo("收货公司");
assertThat(row.operatorName()).isEqualTo("操作员");
assertThat(row.destinationWarehouseName()).isEqualTo("ONT8");
assertThat(row.pickupMethod()).isEqualTo("SELF_TRUCK");
assertThat(row.expressCompany()).isEqualTo("UPS");
assertThat(row.skuNos()).isEqualTo("SKU-A、SKU-B");
assertThat(row.shipmentId()).isEqualTo("FBA-SHIPMENT-77");
@@ -9,6 +9,7 @@ import com.baiyue.order.dto.UnifiedOrderResponse;
import com.baiyue.order.entity.OrderCargoEntity;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderInternalNoteEntity;
import com.baiyue.order.entity.OrderStatusHistoryEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository;
import jakarta.annotation.Resource;
@@ -247,6 +248,24 @@ class UnifiedOrderQueryServiceTest {
verify(firstleg, never()).detail(any(), any());
}
@Test
void detailUsesCreateHistoryOperatorNameInsteadOfNumericCreatorId() {
OrderEntity order = order(OrderStatus.SUBMITTED);
OrderStatusHistoryEntity created = new OrderStatusHistoryEntity();
created.setAction("CREATE");
created.setOperatorId(2L);
created.setOperatorName("platform_admin");
when(orders.findOwned(10001L, 77L)).thenReturn(order);
when(orders.findCargo(10001L, 77L)).thenReturn(List.of());
when(orders.findParties(10001L, 77L)).thenReturn(List.of());
when(orders.findNotes(10001L, 77L)).thenReturn(List.of());
when(orders.findHistory(10001L, 77L)).thenReturn(List.of(created));
var result = service.detail(10001L, 77L, true);
assertThat(result.summary().createdBy()).isEqualTo("platform_admin");
}
@Test
void fulfillmentStatusFilterUsesFirstlegPagingWithoutPostPageFiltering() {
UnifiedOrderQuery query = query("INBOUNDED");