From 270372f56ab93c700b82a76318ea98c045b1da2c Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 26 Aug 2026 11:05:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/海外区域统一增量-MySQL8.sql | 19 +++++++ .../controller/LastlegOrderController.java | 16 ++++++ .../dto/LastlegOrderDetailResponse.java | 6 ++- .../lastleg/dto/LastlegOrderRequests.java | 13 +++++ .../lastleg/entity/LastlegOrderFee.java | 25 +++++++++ .../entity/LastlegOrderOperationLog.java | 20 +++++++ .../lastleg/mapper/LastlegOrderFeeMapper.java | 9 ++++ .../LastlegOrderOperationLogMapper.java | 9 ++++ .../lastleg/service/LastlegOrderService.java | 51 +++++++++++++++++- .../src/main/resources/db/lastleg.sql | 28 ++++++++++ .../openapi/overseas-business-v1.yaml | 54 +++++++++++++++++++ .../service/LastlegOrderServiceTest.java | 24 +++++++++ scripts/db/incremental.sql | 18 +++++++ 13 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderFee.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderOperationLog.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderFeeMapper.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderOperationLogMapper.java diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 41f10d449..6fc9929f5 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -920,6 +920,25 @@ DELIMITER ; CALL assert_phase4_pda_operations_ready(); DROP PROCEDURE assert_phase4_pda_operations_ready; +CREATE TABLE IF NOT EXISTS ll_order_fee ( + id BIGINT NOT NULL COMMENT '费用明细主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', + order_id BIGINT NOT NULL COMMENT '尾程订单ID', fee_item VARCHAR(128) NOT NULL COMMENT '费用项目', + currency VARCHAR(3) NOT NULL COMMENT '币种代码', amount DECIMAL(18,2) NOT NULL COMMENT '费用金额', + status VARCHAR(32) NOT NULL DEFAULT 'PENDING_REVIEW' COMMENT '费用状态:PENDING_REVIEW待审核、APPROVED审核通过、REJECTED已驳回', + supplier_description VARCHAR(500) NULL COMMENT '供应商或费用说明', created_by VARCHAR(128) NOT NULL COMMENT '创建操作人', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + PRIMARY KEY(id), KEY idx_ll_order_fee_customer_order(customer_id,order_id,created_at) +) ENGINE=InnoDB COMMENT='尾程订单费用明细'; + +CREATE TABLE IF NOT EXISTS ll_order_operation_log ( + id BIGINT NOT NULL COMMENT '操作记录主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', + order_id BIGINT NOT NULL COMMENT '尾程订单ID', operation_node VARCHAR(64) NOT NULL COMMENT '操作节点', + operator_name VARCHAR(128) NOT 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_order_operation_customer_order(customer_id,order_id,created_at) +) ENGINE=InnoDB COMMENT='尾程订单操作记录'; + CREATE DATABASE IF NOT EXISTS baiyue_overseas_security DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE baiyue_overseas_security; CREATE TABLE IF NOT EXISTS overseas_security_event_inbox ( diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java index 7fd3375d5..9386f9c91 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java @@ -7,6 +7,7 @@ import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.lastleg.dto.LastlegOrderDetailResponse; import com.baiyue.lastleg.dto.LastlegOrderRequests; import com.baiyue.lastleg.dto.LastlegOrderSummary; +import com.baiyue.lastleg.entity.LastlegOrderFee; import com.baiyue.lastleg.security.InternalServiceTokenGuard; import com.baiyue.lastleg.security.OverseasPermissions; import com.baiyue.lastleg.service.LastlegOrderService; @@ -81,6 +82,17 @@ public class LastlegOrderController { return ApiResponse.success(orderService.detail(platformScope(user, customerId), id)); } + @PostMapping("/api/platform/v1/last-leg/orders/{id}/fees") + @PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')") + @Operation(summary = "Add a fee to one tail-leg order", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT) + public ApiResponse createFee( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestParam(required = false) String customerId, + @PathVariable long id, + @Valid @RequestBody LastlegOrderRequests.CreateFee request) { + return ApiResponse.success(orderService.createFee(platformScope(user, customerId), id, request, operatorName(user))); + } + @GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv") @PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EXPORT + "')") @Operation(summary = "Export filtered tail-leg orders as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EXPORT) @@ -126,4 +138,8 @@ public class LastlegOrderController { return requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer) ? null : requestedCustomer.trim(); } + + private String operatorName(AuthenticatedUser user) { + return user.username() == null || user.username().isBlank() ? String.valueOf(user.userId()) : user.username(); + } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderDetailResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderDetailResponse.java index 6c1285038..27650b7b0 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderDetailResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderDetailResponse.java @@ -1,6 +1,7 @@ package com.baiyue.lastleg.dto; import com.baiyue.lastleg.entity.LastlegOrderDetail; +import com.baiyue.lastleg.entity.LastlegOrderFee; import java.time.LocalDateTime; @@ -9,7 +10,10 @@ import java.util.List; public record LastlegOrderDetailResponse( LastlegOrderSummary order, List cargo, - List statusHistory + List statusHistory, + List fees, + List operationHistory ) { public record History(Long id, String fromStatus, String toStatus, String remark, LocalDateTime createdAt) {} + public record Operation(Long id, String operationNode, String operatorName, String result, LocalDateTime createdAt) {} } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java index 99bf49851..01b53a403 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java @@ -5,7 +5,11 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; @@ -55,4 +59,13 @@ public final class LastlegOrderRequests { return new TransferDestination(List.of(orderId), warehouseId, warehouseCode, warehouseName, remark); } } + + public record CreateFee( + @NotBlank @Size(max = 128) String feeItem, + @NotBlank @Pattern(regexp = "[A-Z]{3}") String currency, + @NotNull @DecimalMin(value = "0.01") BigDecimal amount, + @NotBlank @Pattern(regexp = "PENDING_REVIEW|APPROVED|REJECTED") String status, + @Size(max = 500) String supplierDescription + ) { + } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderFee.java b/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderFee.java new file mode 100644 index 000000000..7e1b2d9e9 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderFee.java @@ -0,0 +1,25 @@ +package com.baiyue.lastleg.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@TableName("ll_order_fee") +public class LastlegOrderFee { + @TableId(type = IdType.INPUT) private Long id; + private String customerId; + private Long orderId; + private String feeItem; + private String currency; + private BigDecimal amount; + private String status; + private String supplierDescription; + private String createdBy; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderOperationLog.java b/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderOperationLog.java new file mode 100644 index 000000000..db8a7b860 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderOperationLog.java @@ -0,0 +1,20 @@ +package com.baiyue.lastleg.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_order_operation_log") +public class LastlegOrderOperationLog { + @TableId(type = IdType.INPUT) private Long id; + private String customerId; + private Long orderId; + private String operationNode; + private String operatorName; + private String result; + private LocalDateTime createdAt; +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderFeeMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderFeeMapper.java new file mode 100644 index 000000000..761e7620d --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderFeeMapper.java @@ -0,0 +1,9 @@ +package com.baiyue.lastleg.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baiyue.lastleg.entity.LastlegOrderFee; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface LastlegOrderFeeMapper extends BaseMapper { +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderOperationLogMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderOperationLogMapper.java new file mode 100644 index 000000000..c5b9fb76d --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderOperationLogMapper.java @@ -0,0 +1,9 @@ +package com.baiyue.lastleg.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baiyue.lastleg.entity.LastlegOrderOperationLog; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface LastlegOrderOperationLogMapper extends BaseMapper { +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java index 8b88d3694..a9ddd69c9 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java @@ -1,6 +1,7 @@ package com.baiyue.lastleg.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.dto.LastlegOrderDetailResponse; @@ -9,11 +10,15 @@ import com.baiyue.lastleg.dto.LastlegOrderSummary; import com.baiyue.lastleg.entity.LastlegOrder; import com.baiyue.lastleg.entity.LastlegOrderDetail; import com.baiyue.lastleg.entity.LastlegOrderStatusHistory; +import com.baiyue.lastleg.entity.LastlegOrderFee; +import com.baiyue.lastleg.entity.LastlegOrderOperationLog; import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper; import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; +import com.baiyue.lastleg.mapper.LastlegOrderFeeMapper; +import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper; import com.baiyue.lastleg.model.LastlegOrderType; import jakarta.annotation.Resource; import org.springframework.http.HttpStatus; @@ -37,6 +42,8 @@ public class LastlegOrderService { @Resource private LastlegOrderMapper orderMapper; @Resource private LastlegOrderDetailMapper detailMapper; @Resource private LastlegOrderStatusHistoryMapper historyMapper; + @Resource private LastlegOrderFeeMapper feeMapper; + @Resource private LastlegOrderOperationLogMapper operationLogMapper; @Resource private InboundPlanCartonMapper cartonMapper; public PageResult page(String customerId, LastlegOrderRequests.Query query) { @@ -56,9 +63,51 @@ public class LastlegOrderService { .eq(LastlegOrderStatusHistory::getCustomerId, order.getCustomerId()) .eq(LastlegOrderStatusHistory::getOrderId, orderId) .orderByAsc(LastlegOrderStatusHistory::getCreatedAt)); + List fees = feeMapper.selectList(new LambdaQueryWrapper() + .eq(LastlegOrderFee::getCustomerId, order.getCustomerId()) + .eq(LastlegOrderFee::getOrderId, orderId).orderByAsc(LastlegOrderFee::getCreatedAt)); + List operations = operationLogMapper.selectList( + new LambdaQueryWrapper() + .eq(LastlegOrderOperationLog::getCustomerId, order.getCustomerId()) + .eq(LastlegOrderOperationLog::getOrderId, orderId) + .orderByAsc(LastlegOrderOperationLog::getCreatedAt)); return new LastlegOrderDetailResponse(summary(order), cargo, history.stream() .map(value -> new LastlegOrderDetailResponse.History(value.getId(), value.getFromStatus(), - value.getToStatus(), value.getRemark(), value.getCreatedAt())).toList()); + value.getToStatus(), value.getRemark(), value.getCreatedAt())).toList(), fees, operations.stream() + .map(value -> new LastlegOrderDetailResponse.Operation(value.getId(), value.getOperationNode(), + value.getOperatorName(), value.getResult(), value.getCreatedAt())).toList()); + } + + @Transactional + public LastlegOrderFee createFee(String customerId, long orderId, LastlegOrderRequests.CreateFee request, + String operatorName) { + LastlegOrder order = findOwned(customerId, orderId); + LocalDateTime now = LocalDateTime.now(); + LastlegOrderFee fee = new LastlegOrderFee(); + fee.setId(IdWorker.getId()); + fee.setCustomerId(order.getCustomerId()); + fee.setOrderId(orderId); + fee.setFeeItem(request.feeItem().trim()); + fee.setCurrency(request.currency()); + fee.setAmount(request.amount()); + fee.setStatus(request.status()); + fee.setSupplierDescription(StringUtils.hasText(request.supplierDescription()) + ? request.supplierDescription().trim() : null); + fee.setCreatedBy(operatorName); + fee.setCreatedAt(now); + fee.setUpdatedAt(now); + feeMapper.insert(fee); + + LastlegOrderOperationLog operation = new LastlegOrderOperationLog(); + operation.setId(IdWorker.getId()); + operation.setCustomerId(order.getCustomerId()); + operation.setOrderId(orderId); + operation.setOperationNode("新增费用"); + operation.setOperatorName(operatorName); + operation.setResult("新增" + fee.getFeeItem() + ":" + fee.getCurrency() + " " + fee.getAmount().toPlainString()); + operation.setCreatedAt(now); + operationLogMapper.insert(operation); + return fee; } @Transactional diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index 45c3be89d..26cd0f694 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -137,6 +137,34 @@ CREATE TABLE IF NOT EXISTS ll_loading_order_status_history ( KEY idx_ll_loading_history_customer_order(customer_id,order_id,created_at) ) ENGINE=InnoDB COMMENT='尾程订单状态变更历史'; +CREATE TABLE IF NOT EXISTS ll_order_fee ( + id BIGINT NOT NULL COMMENT '费用明细主键', + customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', + order_id BIGINT NOT NULL COMMENT '尾程订单ID', + fee_item VARCHAR(128) NOT NULL COMMENT '费用项目', + currency VARCHAR(3) NOT NULL COMMENT '币种代码', + amount DECIMAL(18,2) NOT NULL COMMENT '费用金额', + status VARCHAR(32) NOT NULL DEFAULT 'PENDING_REVIEW' COMMENT '费用状态:PENDING_REVIEW待审核、APPROVED审核通过、REJECTED已驳回', + supplier_description VARCHAR(500) NULL COMMENT '供应商或费用说明', + created_by VARCHAR(128) NOT NULL COMMENT '创建操作人', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + PRIMARY KEY(id), + KEY idx_ll_order_fee_customer_order(customer_id,order_id,created_at) +) ENGINE=InnoDB COMMENT='尾程订单费用明细'; + +CREATE TABLE IF NOT EXISTS ll_order_operation_log ( + id BIGINT NOT NULL COMMENT '操作记录主键', + customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', + order_id BIGINT NOT NULL COMMENT '尾程订单ID', + operation_node VARCHAR(64) NOT NULL COMMENT '操作节点', + operator_name VARCHAR(128) NOT 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_order_operation_customer_order(customer_id,order_id,created_at) +) ENGINE=InnoDB COMMENT='尾程订单操作记录'; + CREATE TABLE IF NOT EXISTS ll_event_conflict ( id BIGINT NOT NULL COMMENT '冲突记录主键', event_id VARCHAR(128) NOT NULL COMMENT '冲突事件 ID', customer_id BIGINT NOT NULL COMMENT '租户 ID', conflict_type VARCHAR(64) NOT NULL COMMENT '冲突类型', diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 1c5d4c33f..04ae37a68 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -138,6 +138,29 @@ paths: - { name: createdFrom, in: query, schema: { type: string, format: date-time } } - { name: createdTo, in: query, schema: { type: string, format: date-time } } responses: { '200': { description: 统一 ApiResponse 包装的尾程订单分页;列表行 destinationType 为 FBA 或 OVERSEAS } } + /api/platform/v1/last-leg/orders/{id}: + get: + summary: 查询尾程订单详情 + description: 权限 platform:overseas-warehouse:last-mile-order:list;返回订单、货物、状态历史、费用明细和操作记录。 + security: [{ bearerAuth: [] }] + parameters: + - { name: id, in: path, required: true, schema: { type: integer, format: int64 } } + responses: + '200': { description: 统一 ApiResponse 包装的尾程订单详情 } + '404': { description: 尾程订单不存在或不属于查询客户 } + /api/platform/v1/last-leg/orders/{id}/fees: + post: + summary: 新增尾程订单费用 + description: 权限 platform:overseas-warehouse:last-mile-order:edit;在订单客户范围内新增费用并记录操作人和处理结果。 + security: [{ bearerAuth: [] }] + parameters: + - { name: id, in: path, required: true, schema: { type: integer, format: int64 } } + requestBody: + required: true + content: { application/json: { schema: { $ref: '#/components/schemas/LastMileFeeCreate' } } } + responses: + '200': { description: 新增后的费用明细, content: { application/json: { schema: { $ref: '#/components/schemas/LastMileFeeApiResponse' } } } } + '404': { description: 尾程订单不存在或不属于操作客户 } /api/platform/v1/last-leg/dispatches: get: summary: 分页查询排车单 @@ -513,6 +536,37 @@ paths: responses: { '200': { description: 统一 ApiResponse 包装的换标单详情 }, '409': { description: 非法状态迁移或关联尾程单缺失 } } components: schemas: + LastMileFeeCreate: + type: object + required: [feeItem, currency, amount, status] + properties: + feeItem: { type: string, minLength: 1, maxLength: 128 } + currency: { type: string, pattern: '^[A-Z]{3}$', example: USD } + amount: { type: number, format: decimal, minimum: 0.01 } + status: { type: string, enum: [PENDING_REVIEW, APPROVED, REJECTED] } + supplierDescription: { type: string, maxLength: 500 } + LastMileFee: + type: object + required: [id, customerId, orderId, feeItem, currency, amount, status, createdBy, createdAt, updatedAt] + properties: + id: { type: integer, format: int64 } + customerId: { type: string } + orderId: { type: integer, format: int64 } + feeItem: { type: string } + currency: { type: string } + amount: { type: number, format: decimal } + status: { type: string } + supplierDescription: { type: string, nullable: true } + createdBy: { type: string } + createdAt: { type: string, format: date-time } + updatedAt: { type: string, format: date-time } + LastMileFeeApiResponse: + type: object + required: [code, message, data] + properties: + code: { type: string } + message: { type: string } + data: { $ref: '#/components/schemas/LastMileFee' } PdaWarehouseScan: type: object required: [code] diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java index 9f7a4f9ab..737b4e848 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java @@ -9,6 +9,8 @@ import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper; import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; +import com.baiyue.lastleg.mapper.LastlegOrderFeeMapper; +import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,6 +21,7 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.test.util.ReflectionTestUtils; import java.util.List; +import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -33,6 +36,8 @@ class LastlegOrderServiceTest { @Mock LastlegOrderMapper orders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; + @Mock LastlegOrderFeeMapper fees; + @Mock LastlegOrderOperationLogMapper operations; @Mock InboundPlanCartonMapper cartons; LastlegOrderService service; @@ -42,6 +47,8 @@ class LastlegOrderServiceTest { ReflectionTestUtils.setField(service, "orderMapper", orders); ReflectionTestUtils.setField(service, "detailMapper", details); ReflectionTestUtils.setField(service, "historyMapper", histories); + ReflectionTestUtils.setField(service, "feeMapper", fees); + ReflectionTestUtils.setField(service, "operationLogMapper", operations); ReflectionTestUtils.setField(service, "cartonMapper", cartons); } @@ -52,6 +59,8 @@ class LastlegOrderServiceTest { when(orders.selectOne(any())).thenReturn(order); when(details.selectList(any())).thenReturn(List.of()); when(histories.selectList(any())).thenReturn(List.of()); + when(fees.selectList(any())).thenReturn(List.of()); + when(operations.selectList(any())).thenReturn(List.of()); var response = service.detail("100", 11L); @@ -91,6 +100,21 @@ class LastlegOrderServiceTest { assertEquals("DESTINATION_ASSIGNED", history.getValue().getToStatus()); } + @Test + void createFeePersistsFeeAndOperationForOwnedOrder() { + when(orders.selectOne(any())).thenReturn(order(11L, "100", "PENDING_ASSIGNMENT", null)); + var request = new LastlegOrderRequests.CreateFee( + "清关费", "USD", new BigDecimal("420.00"), "PENDING_REVIEW", "报关代理"); + + var fee = service.createFee("100", 11L, request, "platform_admin"); + + assertEquals("100", fee.getCustomerId()); + assertEquals("清关费", fee.getFeeItem()); + assertEquals(new BigDecimal("420.00"), fee.getAmount()); + verify(fees).insert(any()); + verify(operations).insert(any()); + } + @Test void transferRejectsAnOrderAlreadyBoundToInboundPlan() { LastlegOrder order = order(11L, "100", "DESTINATION_ASSIGNED", "IB-1"); diff --git a/scripts/db/incremental.sql b/scripts/db/incremental.sql index ad21defb5..58a4dd551 100644 --- a/scripts/db/incremental.sql +++ b/scripts/db/incremental.sql @@ -3824,3 +3824,21 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab AND table_name='ll_cargo_discard_application' AND column_name='review_request_hash'), 'SELECT 1', 'ALTER TABLE ll_cargo_discard_application ADD COLUMN review_request_hash CHAR(64) NULL COMMENT ''审核请求内容哈希'' AFTER review_idempotency_key')); PREPARE phase4_discard_review_stmt FROM @ddl; EXECUTE phase4_discard_review_stmt; DEALLOCATE PREPARE phase4_discard_review_stmt; +CREATE TABLE IF NOT EXISTS ll_order_fee ( + id BIGINT NOT NULL COMMENT '费用明细主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', + order_id BIGINT NOT NULL COMMENT '尾程订单ID', fee_item VARCHAR(128) NOT NULL COMMENT '费用项目', + currency VARCHAR(3) NOT NULL COMMENT '币种代码', amount DECIMAL(18,2) NOT NULL COMMENT '费用金额', + status VARCHAR(32) NOT NULL DEFAULT 'PENDING_REVIEW' COMMENT '费用状态:PENDING_REVIEW待审核、APPROVED审核通过、REJECTED已驳回', + supplier_description VARCHAR(500) NULL COMMENT '供应商或费用说明', created_by VARCHAR(128) NOT NULL COMMENT '创建操作人', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + PRIMARY KEY(id), KEY idx_ll_order_fee_customer_order(customer_id,order_id,created_at) +) ENGINE=InnoDB COMMENT='尾程订单费用明细'; + +CREATE TABLE IF NOT EXISTS ll_order_operation_log ( + id BIGINT NOT NULL COMMENT '操作记录主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', + order_id BIGINT NOT NULL COMMENT '尾程订单ID', operation_node VARCHAR(64) NOT NULL COMMENT '操作节点', + operator_name VARCHAR(128) NOT 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_order_operation_customer_order(customer_id,order_id,created_at) +) ENGINE=InnoDB COMMENT='尾程订单操作记录';