提交
This commit is contained in:
+10
-13
@@ -56,7 +56,7 @@ public class LastlegOrderController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid LastlegOrderRequests.Query query) {
|
||||
return ApiResponse.success(orderService.page(scope(user, customerId), query));
|
||||
return ApiResponse.success(orderService.page(platformScope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/last-leg/orders/{id}")
|
||||
@@ -66,7 +66,7 @@ public class LastlegOrderController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable long id) {
|
||||
return ApiResponse.success(orderService.detail(scope(user, customerId), id));
|
||||
return ApiResponse.success(orderService.detail(platformScope(user, customerId), id));
|
||||
}
|
||||
|
||||
@GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv")
|
||||
@@ -76,7 +76,7 @@ public class LastlegOrderController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid LastlegOrderRequests.Query query) {
|
||||
byte[] body = orderService.exportCsv(scope(user, customerId), query);
|
||||
byte[] body = orderService.exportCsv(platformScope(user, customerId), query);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=lastleg-orders.csv")
|
||||
@@ -91,7 +91,7 @@ public class LastlegOrderController {
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable long id,
|
||||
@Valid @RequestBody LastlegOrderRequests.SingleTransferDestination request) {
|
||||
return ApiResponse.success(orderService.transferDestination(scope(user, customerId), request.withOrderId(id)));
|
||||
return ApiResponse.success(orderService.transferDestination(platformScope(user, customerId), request.withOrderId(id)));
|
||||
}
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/destination")
|
||||
@@ -101,20 +101,17 @@ public class LastlegOrderController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody LastlegOrderRequests.TransferDestination request) {
|
||||
return ApiResponse.success(orderService.transferDestination(scope(user, customerId), request));
|
||||
return ApiResponse.success(orderService.transferDestination(platformScope(user, customerId), request));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
private String platformScope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform queries must specify a customer");
|
||||
}
|
||||
return requestedCustomer;
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform identity is required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
return requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)
|
||||
? null : requestedCustomer.trim();
|
||||
}
|
||||
}
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.lastleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
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.service.LastlegOrderService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/orders")
|
||||
public class MerchantLastlegOrderController {
|
||||
@Resource private LastlegOrderService orderService;
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<LastlegOrderSummary>> list(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid LastlegOrderRequests.Query query) {
|
||||
return ApiResponse.success(orderService.page(customer(user), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<LastlegOrderDetailResponse> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long id) {
|
||||
return ApiResponse.success(orderService.detail(customer(user), id));
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
}
|
||||
-19
@@ -1,7 +1,6 @@
|
||||
package com.baiyue.lastleg.dispatch.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||
@@ -61,24 +60,6 @@ public class DispatchController {
|
||||
return ApiResponse.success(service.preArrange(scopedTenant, operatorId, request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "Page dispatch orders")
|
||||
public ApiResponse<PageResult<DispatchResponse>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid DispatchRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{dispatchNo}")
|
||||
@Operation(summary = "Get dispatch details and all pallet destinations")
|
||||
public ApiResponse<DispatchResponse> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String dispatchNo) {
|
||||
return ApiResponse.success(service.detail(scope(user, customerId), dispatchNo));
|
||||
}
|
||||
|
||||
@PutMapping("/{dispatchNo}")
|
||||
@Operation(summary = "Edit vehicle, driver, appointment, route and departure data")
|
||||
public ApiResponse<DispatchResponse> update(
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.baiyue.lastleg.dispatch.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@ConditionalOnProperty(name = "lastleg.dispatch-query-api.enabled", havingValue = "true")
|
||||
@RequestMapping("/api/platform/v1/last-leg/dispatches")
|
||||
@Tag(name = "Tail-leg vehicle dispatch query")
|
||||
public class DispatchQueryController {
|
||||
@Resource private DispatchService service;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "Page dispatch orders")
|
||||
public ApiResponse<PageResult<DispatchResponse>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid DispatchRequests.Query query) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.page(null, query));
|
||||
}
|
||||
|
||||
@GetMapping("/{dispatchNo}")
|
||||
@Operation(summary = "Get dispatch details and all pallet destinations")
|
||||
public ApiResponse<DispatchResponse> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.detail(null, dispatchNo));
|
||||
}
|
||||
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -11,6 +11,9 @@ public interface DispatchOrderMapper extends BaseMapper<DispatchOrder> {
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND dispatch_no=#{dispatchNo} LIMIT 1")
|
||||
DispatchOrder findByDispatchNo(@Param("customerId") String customerId, @Param("dispatchNo") String dispatchNo);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE dispatch_no=#{dispatchNo} LIMIT 1")
|
||||
DispatchOrder findAnyByDispatchNo(@Param("dispatchNo") String dispatchNo);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND id=#{id} LIMIT 1 FOR UPDATE")
|
||||
DispatchOrder lockById(@Param("customerId") String customerId, @Param("id") Long id);
|
||||
}
|
||||
|
||||
+3
-3
@@ -112,11 +112,11 @@ public class DispatchPdaWorkflowService {
|
||||
long picked = picks.totalItem(customerId, task.getId(), itemId);
|
||||
if (picked + request.packages() > item.getPackageCount()) throw conflict("Picked packages exceed dispatch pallet packages");
|
||||
String keyBase = "DISPATCH:" + order.getDispatchNo() + ":" + itemId + ":" + request.idempotencyKey();
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(customerId, request.positionId(),
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(request.positionId(),
|
||||
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
|
||||
keyBase + ":ALLOCATE", request.remark());
|
||||
inventory.allocate(customerId, operatorId, move);
|
||||
inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(customerId, request.positionId(),
|
||||
inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(request.positionId(),
|
||||
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
|
||||
keyBase + ":PICK", request.remark()));
|
||||
DispatchPdaPick pick = new DispatchPdaPick();
|
||||
@@ -200,7 +200,7 @@ public class DispatchPdaWorkflowService {
|
||||
long shipped = 0;
|
||||
for (DispatchPdaPick pick : dispatchPicks) {
|
||||
String key = "DISPATCH:" + order.getDispatchNo() + ":PICK:" + pick.getId() + ":SHIP";
|
||||
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(customerId,
|
||||
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
pick.getInventoryPositionId(), n(pick.getPackages()),
|
||||
outbound.getOutboundNo(), outbound.getOutboundNo(), null, key, request.remark()));
|
||||
shipped += n(pick.getPackages());
|
||||
|
||||
+6
-4
@@ -150,7 +150,7 @@ public class DispatchService {
|
||||
|
||||
public PageResult<DispatchResponse> page(String customerId, DispatchRequests.Query query) {
|
||||
Page<DispatchOrder> page = orders.selectPage(new Page<>(query.page(), query.size()),
|
||||
new LambdaQueryWrapper<DispatchOrder>().eq(DispatchOrder::getCustomerId, customerId)
|
||||
new LambdaQueryWrapper<DispatchOrder>().eq(StringUtils.hasText(customerId), DispatchOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.dispatchNo()), DispatchOrder::getDispatchNo, query.dispatchNo())
|
||||
.like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate())
|
||||
.eq(StringUtils.hasText(query.status()), DispatchOrder::getStatus, query.status())
|
||||
@@ -158,13 +158,13 @@ public class DispatchService {
|
||||
query.fbaAppointmentNo())
|
||||
.orderByDesc(DispatchOrder::getCreatedAt));
|
||||
List<DispatchResponse> records = page.getRecords().stream()
|
||||
.map(order -> response(order, items.findByDispatchOrder(customerId, order.getId()))).toList();
|
||||
.map(order -> response(order, items.findByDispatchOrder(order.getCustomerId(), order.getId()))).toList();
|
||||
return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
public DispatchResponse detail(String customerId, String dispatchNo) {
|
||||
DispatchOrder dispatch = find(customerId, dispatchNo);
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -216,7 +216,9 @@ public class DispatchService {
|
||||
}
|
||||
|
||||
private DispatchOrder find(String customerId, String dispatchNo) {
|
||||
DispatchOrder dispatch = orders.findByDispatchNo(customerId, dispatchNo);
|
||||
DispatchOrder dispatch = StringUtils.hasText(customerId)
|
||||
? orders.findByDispatchNo(customerId, dispatchNo)
|
||||
: orders.findAnyByDispatchNo(dispatchNo);
|
||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
return dispatch;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public record LastlegOrderSummary(
|
||||
String sourceInboundId,
|
||||
String inboundCode,
|
||||
String sourceOrderNo,
|
||||
String customerId,
|
||||
String customerName,
|
||||
String overseasWarehouseId,
|
||||
String overseasWarehouseName,
|
||||
|
||||
+3
-4
@@ -37,7 +37,7 @@ public class InboundPlanController {
|
||||
@Operation(summary = "分页查询入库订单", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
|
||||
public ApiResponse<PageResult<InboundPlanListResponse>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String customerId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InboundPlanRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
}
|
||||
@@ -111,10 +111,9 @@ public class InboundPlanController {
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform request must specify target tenant");
|
||||
return null;
|
||||
}
|
||||
return requestedCustomer;
|
||||
return requestedCustomer.trim();
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
+7
-6
@@ -11,14 +11,15 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface InboundPlanQueryMapper {
|
||||
String FROM_AND_FILTER = " FROM ll_inbound_plan_line l" +
|
||||
" JOIN ll_inbound_plan p ON p.id=l.plan_id AND p.customer_id=#{customerId}" +
|
||||
" JOIN ll_order o ON o.id=l.order_id AND o.customer_id=#{customerId}" +
|
||||
" JOIN ll_inbound_plan p ON p.id=l.plan_id" +
|
||||
" JOIN ll_order o ON o.id=l.order_id" +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND p.customer_id=#{customerId} AND o.customer_id=#{customerId}</if>" +
|
||||
" LEFT JOIN ll_inbound_fba_appointment_item ai ON ai.line_id=l.id" +
|
||||
" AND ai.id=(SELECT ai2.id FROM ll_inbound_fba_appointment_item ai2" +
|
||||
" JOIN ll_inbound_fba_appointment a2 ON a2.id=ai2.appointment_id" +
|
||||
" WHERE ai2.line_id=l.id AND a2.customer_id=#{customerId}" +
|
||||
" WHERE ai2.line_id=l.id <if test='customerId != null and customerId != \"\"'> AND a2.customer_id=#{customerId}</if>" +
|
||||
" ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1)" +
|
||||
" LEFT JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id AND a.customer_id=#{customerId}" +
|
||||
" LEFT JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id <if test='customerId != null and customerId != \"\"'> AND a.customer_id=#{customerId}</if>" +
|
||||
" WHERE 1=1" +
|
||||
"<if test='q.planNo != null and q.planNo != \"\"'> AND p.plan_no LIKE CONCAT('%',#{q.planNo},'%')</if>" +
|
||||
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND l.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
|
||||
@@ -29,8 +30,8 @@ public interface InboundPlanQueryMapper {
|
||||
"<if test='q.fbaWarehouseCode != null and q.fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code LIKE CONCAT('%',#{q.fbaWarehouseCode},'%')</if>" +
|
||||
"<choose><when test='q.appointmentStatus == \"UNAPPOINTED\"'> AND ai.id IS NULL</when>" +
|
||||
"<when test='q.appointmentStatus != null and q.appointmentStatus != \"\"'> AND a.appointment_status=#{q.appointmentStatus}</when></choose>" +
|
||||
"<choose><when test='q.dispatchStatus == \"SCHEDULED\"'> AND EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.customer_id=#{customerId} AND di.order_id=l.order_id)</when>" +
|
||||
"<when test='q.dispatchStatus == \"UNSCHEDULED\"'> AND NOT EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.customer_id=#{customerId} AND di.order_id=l.order_id)</when></choose>" +
|
||||
"<choose><when test='q.dispatchStatus == \"SCHEDULED\"'> AND EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.order_id=l.order_id <if test='customerId != null and customerId != \"\"'> AND di.customer_id=#{customerId}</if>)</when>" +
|
||||
"<when test='q.dispatchStatus == \"UNSCHEDULED\"'> AND NOT EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.order_id=l.order_id <if test='customerId != null and customerId != \"\"'> AND di.customer_id=#{customerId}</if>)</when></choose>" +
|
||||
"<if test='q.createdFrom != null'> AND p.created_at >=#{q.createdFrom}</if>" +
|
||||
"<if test='q.createdTo != null'> AND p.created_at <=#{q.createdTo}</if>";
|
||||
|
||||
|
||||
+4
-3
@@ -156,9 +156,10 @@ public class InboundFbaAppointmentService {
|
||||
}
|
||||
|
||||
private InboundFbaAppointment find(String customerId, String appointmentNo) {
|
||||
return appointmentMapper.selectOne(new LambdaQueryWrapper<InboundFbaAppointment>()
|
||||
.eq(InboundFbaAppointment::getCustomerId, customerId)
|
||||
.eq(InboundFbaAppointment::getAppointmentNo, appointmentNo));
|
||||
LambdaQueryWrapper<InboundFbaAppointment> wrapper = new LambdaQueryWrapper<InboundFbaAppointment>()
|
||||
.eq(InboundFbaAppointment::getAppointmentNo, appointmentNo);
|
||||
if (customerId != null) wrapper.eq(InboundFbaAppointment::getCustomerId, customerId);
|
||||
return appointmentMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
private List<InboundFbaAppointmentItem> items(Long appointmentId) {
|
||||
|
||||
+5
-3
@@ -117,8 +117,10 @@ public class InboundPlanService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
.eq(InboundPlan::getCustomerId, customerId).eq(InboundPlan::getPlanNo, planNo));
|
||||
LambdaQueryWrapper<InboundPlan> wrapper = new LambdaQueryWrapper<InboundPlan>()
|
||||
.eq(InboundPlan::getPlanNo, planNo);
|
||||
if (customerId != null) wrapper.eq(InboundPlan::getCustomerId, customerId);
|
||||
InboundPlan plan = planMapper.selectOne(wrapper);
|
||||
if (plan == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
}
|
||||
@@ -244,7 +246,7 @@ public class InboundPlanService {
|
||||
}
|
||||
|
||||
private InboundPlanResponse detail(String customerId, InboundPlan plan) {
|
||||
if (!customerId.equals(plan.getCustomerId())) {
|
||||
if (customerId != null && !customerId.equals(plan.getCustomerId())) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
}
|
||||
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
|
||||
+9
-11
@@ -33,7 +33,8 @@ public class InventoryCommandController {
|
||||
public ApiResponse<InventoryCommandResponse> hold(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Move request) {
|
||||
requirePermission(user, MANAGE_PERMISSION);
|
||||
return ApiResponse.success(service.hold(scope(user, request.customerId()), operator(user), request));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.holdPlatform(operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/release")
|
||||
@@ -42,7 +43,8 @@ public class InventoryCommandController {
|
||||
public ApiResponse<InventoryCommandResponse> release(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Move request) {
|
||||
requirePermission(user, MANAGE_PERMISSION);
|
||||
return ApiResponse.success(service.release(scope(user, request.customerId()), operator(user), request));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.releasePlatform(operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/adjust")
|
||||
@@ -51,19 +53,15 @@ public class InventoryCommandController {
|
||||
public ApiResponse<InventoryCommandResponse> adjust(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Adjustment request) {
|
||||
requirePermission(user, ADJUST_PERMISSION);
|
||||
return ApiResponse.success(service.adjust(scope(user, request.customerId()), operator(user), request));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.adjustPlatform(operator(user), request));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform request must specify target tenant");
|
||||
}
|
||||
return requestedCustomer.trim();
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
|
||||
-2
@@ -39,7 +39,6 @@ public final class InventoryCommandRequests {
|
||||
}
|
||||
|
||||
public record Move(
|
||||
String customerId,
|
||||
@NotNull Long positionId,
|
||||
@Positive long packages,
|
||||
String businessOrderNo,
|
||||
@@ -51,7 +50,6 @@ public final class InventoryCommandRequests {
|
||||
}
|
||||
|
||||
public record Adjustment(
|
||||
String customerId,
|
||||
@NotNull Long positionId,
|
||||
long packageDelta,
|
||||
@NotBlank String reason,
|
||||
|
||||
+3
@@ -8,6 +8,9 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface InventoryCommandMapper {
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE id=#{positionId} LIMIT 1")
|
||||
InventoryPosition findPosition(@Param("positionId") Long positionId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND id=#{positionId} LIMIT 1 FOR UPDATE")
|
||||
InventoryPosition lockPosition(@Param("customerId") String customerId,
|
||||
@Param("positionId") Long positionId);
|
||||
|
||||
+21
@@ -67,11 +67,21 @@ public class InventoryCommandService {
|
||||
return move(customerId, operatorId, "HOLD", Bucket.AVAILABLE, Bucket.HOLD, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse holdPlatform(String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(customerForPosition(request.positionId()), operatorId, "HOLD", Bucket.AVAILABLE, Bucket.HOLD, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse release(String customerId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(customerId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse releasePlatform(String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(customerForPosition(request.positionId()), operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse allocate(String customerId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(customerId, operatorId, "ALLOCATE", Bucket.AVAILABLE, Bucket.ALLOCATED, request);
|
||||
@@ -147,6 +157,11 @@ public class InventoryCommandService {
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse adjustPlatform(String operatorId, InventoryCommandRequests.Adjustment request) {
|
||||
return adjust(customerForPosition(request.positionId()), operatorId, request);
|
||||
}
|
||||
|
||||
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
|
||||
Bucket from, Bucket to, InventoryCommandRequests.Move request) {
|
||||
return execute(customerId, operatorId, operationType, request.idempotencyKey(), request, operation -> {
|
||||
@@ -255,6 +270,12 @@ public class InventoryCommandService {
|
||||
return new Locked(balance, position);
|
||||
}
|
||||
|
||||
private String customerForPosition(Long positionId) {
|
||||
InventoryPosition position = commands.findPosition(positionId);
|
||||
if (position == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory position not found");
|
||||
return position.getCustomerId();
|
||||
}
|
||||
|
||||
private InventoryCommandResponse execute(String customerId, String operatorId, String operationType,
|
||||
String key, Object request, Work work) {
|
||||
String normalizedKey = required(key, "Idempotency key is required");
|
||||
|
||||
+15
-21
@@ -22,7 +22,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@@ -41,9 +40,9 @@ public class InventoryQueryController {
|
||||
@Operation(summary = "Get inbound-order package inventory totals", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
public ApiResponse<InventoryDashboardResponse> summary(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.BalanceQuery query) {
|
||||
return ApiResponse.success(service.dashboard(scope(user, customerId), query));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.dashboard(null, query));
|
||||
}
|
||||
|
||||
@GetMapping(BASE_PATH + "/balances")
|
||||
@@ -51,9 +50,9 @@ public class InventoryQueryController {
|
||||
@Operation(summary = "Page inventory by customer, warehouse and inbound order", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
public ApiResponse<PageResult<InventoryBalanceView>> balances(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.BalanceQuery query) {
|
||||
return ApiResponse.success(service.balances(scope(user, customerId), query));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.balances(null, query));
|
||||
}
|
||||
|
||||
@GetMapping(BASE_PATH + "/balances/{balanceId}/positions")
|
||||
@@ -61,10 +60,10 @@ public class InventoryQueryController {
|
||||
@Operation(summary = "List pallet and batch positions for one inbound-order balance", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
public ApiResponse<List<InventoryPositionView>> positions(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable long balanceId,
|
||||
@Valid InventoryQueryRequests.PositionQuery query) {
|
||||
return ApiResponse.success(service.positions(scope(user, customerId), balanceId, query));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.positions(null, balanceId, query));
|
||||
}
|
||||
|
||||
@GetMapping(BASE_PATH + "/ledger")
|
||||
@@ -72,9 +71,9 @@ public class InventoryQueryController {
|
||||
@Operation(summary = "Page append-only inventory movements", description = "Required authority: " + OverseasPermissions.INVENTORY_LIST)
|
||||
public ApiResponse<PageResult<InventoryLedgerView>> ledgers(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.LedgerQuery query) {
|
||||
return ApiResponse.success(service.ledgers(scope(user, customerId), query));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.ledgers(null, query));
|
||||
}
|
||||
|
||||
@GetMapping(value = BASE_PATH + "/balances/export", produces = "text/csv")
|
||||
@@ -82,9 +81,9 @@ public class InventoryQueryController {
|
||||
@Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
|
||||
public ResponseEntity<byte[]> exportBalances(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.BalanceQuery query) {
|
||||
return csv(service.exportBalances(scope(user, customerId), query), "inventory-balances.csv");
|
||||
requirePlatform(user);
|
||||
return csv(service.exportBalances(null, query), "inventory-balances.csv");
|
||||
}
|
||||
|
||||
@GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv")
|
||||
@@ -92,9 +91,9 @@ public class InventoryQueryController {
|
||||
@Operation(summary = "Export filtered inventory movements as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
|
||||
public ResponseEntity<byte[]> exportLedgers(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.LedgerQuery query) {
|
||||
return csv(service.exportLedgers(scope(user, customerId), query), "inventory-ledger.csv");
|
||||
requirePlatform(user);
|
||||
return csv(service.exportLedgers(null, query), "inventory-ledger.csv");
|
||||
}
|
||||
|
||||
private static ResponseEntity<byte[]> csv(byte[] body, String filename) {
|
||||
@@ -104,17 +103,12 @@ public class InventoryQueryController {
|
||||
.body(body);
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform queries must specify a customer");
|
||||
}
|
||||
return requestedCustomer;
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
}
|
||||
|
||||
+15
-9
@@ -11,18 +11,19 @@ import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
public interface InventoryQueryMapper {
|
||||
String BALANCE_FILTER = " WHERE customer_id=#{customerId}" +
|
||||
String BALANCE_FILTER = " WHERE 1=1" +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND customer_id=#{customerId}</if>" +
|
||||
"<if test='q.customerName != null and q.customerName != \"\"'> AND customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.overseasWarehouseCode != null and q.overseasWarehouseCode != \"\"'> AND overseas_warehouse_code=#{q.overseasWarehouseCode}</if>" +
|
||||
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
|
||||
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
|
||||
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
|
||||
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=ll_inventory_balance.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
|
||||
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=ll_inventory_balance.id AND l.customer_id=ll_inventory_balance.customer_id AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
|
||||
"<if test='q.skuCode != null and q.skuCode != \"\"'> AND sku_code LIKE CONCAT('%',#{q.skuCode},'%')</if>" +
|
||||
"<if test='q.productName != null and q.productName != \"\"'> AND (product_name_zh LIKE CONCAT('%',#{q.productName},'%') OR product_name_en LIKE CONCAT('%',#{q.productName},'%'))</if>" +
|
||||
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id AND p.customer_id=#{customerId} AND p.batch_no LIKE CONCAT('%',#{q.batchNo},'%'))</if>" +
|
||||
"<if test='q.warehouseLocationCode != null and q.warehouseLocationCode != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id AND p.customer_id=#{customerId} AND p.warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%'))</if>" +
|
||||
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id AND p.customer_id=ll_inventory_balance.customer_id AND p.batch_no LIKE CONCAT('%',#{q.batchNo},'%'))</if>" +
|
||||
"<if test='q.warehouseLocationCode != null and q.warehouseLocationCode != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id AND p.customer_id=ll_inventory_balance.customer_id AND p.warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%'))</if>" +
|
||||
"<choose>" +
|
||||
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND available_packages > 0</when>" +
|
||||
"<when test='q.inventoryStatus == \"HOLD\"'> AND hold_packages > 0</when>" +
|
||||
@@ -33,18 +34,19 @@ public interface InventoryQueryMapper {
|
||||
"<when test='q.inventoryStatus == \"PROCUREMENT\"'> AND procurement_packages > 0</when>" +
|
||||
"</choose>";
|
||||
|
||||
String JOINED_BALANCE_FILTER = " WHERE b.customer_id=#{customerId}" +
|
||||
String JOINED_BALANCE_FILTER = " WHERE 1=1" +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND b.customer_id=#{customerId}</if>" +
|
||||
"<if test='q.customerName != null and q.customerName != \"\"'> AND b.customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND b.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.overseasWarehouseCode != null and q.overseasWarehouseCode != \"\"'> AND b.overseas_warehouse_code=#{q.overseasWarehouseCode}</if>" +
|
||||
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND b.destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
|
||||
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND b.inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
|
||||
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND b.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
|
||||
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=b.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
|
||||
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=b.id AND l.customer_id=b.customer_id AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
|
||||
"<if test='q.skuCode != null and q.skuCode != \"\"'> AND b.sku_code LIKE CONCAT('%',#{q.skuCode},'%')</if>" +
|
||||
"<if test='q.productName != null and q.productName != \"\"'> AND (b.product_name_zh LIKE CONCAT('%',#{q.productName},'%') OR b.product_name_en LIKE CONCAT('%',#{q.productName},'%'))</if>" +
|
||||
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p2 WHERE p2.balance_id=b.id AND p2.customer_id=#{customerId} AND p2.batch_no LIKE CONCAT('%',#{q.batchNo},'%'))</if>" +
|
||||
"<if test='q.warehouseLocationCode != null and q.warehouseLocationCode != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p2 WHERE p2.balance_id=b.id AND p2.customer_id=#{customerId} AND p2.warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%'))</if>" +
|
||||
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p2 WHERE p2.balance_id=b.id AND p2.customer_id=b.customer_id AND p2.batch_no LIKE CONCAT('%',#{q.batchNo},'%'))</if>" +
|
||||
"<if test='q.warehouseLocationCode != null and q.warehouseLocationCode != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p2 WHERE p2.balance_id=b.id AND p2.customer_id=b.customer_id AND p2.warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%'))</if>" +
|
||||
"<choose>" +
|
||||
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND b.available_packages > 0</when>" +
|
||||
"<when test='q.inventoryStatus == \"HOLD\"'> AND b.hold_packages > 0</when>" +
|
||||
@@ -103,7 +105,11 @@ public interface InventoryQueryMapper {
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} AND id=#{balanceId} LIMIT 1")
|
||||
InventoryBalance ownedBalance(@Param("customerId") String customerId, @Param("balanceId") long balanceId);
|
||||
|
||||
String LEDGER_FILTER = " WHERE customer_id=#{customerId}" +
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE id=#{balanceId} LIMIT 1")
|
||||
InventoryBalance findBalance(@Param("balanceId") long balanceId);
|
||||
|
||||
String LEDGER_FILTER = " WHERE 1=1" +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND customer_id=#{customerId}</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
|
||||
"<if test='q.businessType != null and q.businessType != \"\"'> AND business_type=#{q.businessType}</if>" +
|
||||
|
||||
+6
-2
@@ -59,11 +59,15 @@ public class InventoryQueryService {
|
||||
public List<InventoryPositionView> positions(String customerId, long balanceId,
|
||||
InventoryQueryRequests.PositionQuery query) {
|
||||
validateStatus(query.inventoryStatus(), POSITION_STATUSES);
|
||||
InventoryBalance balance = mapper.ownedBalance(customerId, balanceId);
|
||||
InventoryBalance balance = customerId == null
|
||||
? mapper.findBalance(balanceId)
|
||||
: mapper.ownedBalance(customerId, balanceId);
|
||||
if (balance == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory balance not found");
|
||||
}
|
||||
return mapper.positions(customerId, balanceId, query).stream().map(value -> positionView(value, balance)).toList();
|
||||
String recordCustomerId = customerId == null ? balance.getCustomerId() : customerId;
|
||||
return mapper.positions(recordCustomerId, balanceId, query).stream()
|
||||
.map(value -> positionView(value, balance)).toList();
|
||||
}
|
||||
|
||||
public PageResult<InventoryLedgerView> ledgers(String customerId, InventoryQueryRequests.LedgerQuery query) {
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.baiyue.lastleg.masterdata.exchange;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/** 注册尾程主数据域的所有 Mapper。 */
|
||||
@Configuration
|
||||
@MapperScan("com.baiyue.lastleg.masterdata")
|
||||
public class ExchangeMapperConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.model;
|
||||
|
||||
/** Standard main-process statuses for a last-leg order. */
|
||||
public enum LastlegOrderStatus {
|
||||
PENDING_ASSIGNMENT("待分配"),
|
||||
PENDING_INBOUND("待入库"),
|
||||
INBOUND_COMPLETED("入库完成"),
|
||||
PENDING_DELIVERY("待派送"),
|
||||
DELIVERING("派送中"),
|
||||
DELIVERED("派送完成");
|
||||
|
||||
private final String label;
|
||||
|
||||
LastlegOrderStatus(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String label() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.lastleg.model;
|
||||
|
||||
/** Supported business types for a last-leg order. */
|
||||
public enum LastlegOrderType {
|
||||
/** Overseas order created from a completed first-leg loading plan. */
|
||||
FIRST_LEG("头程订单"),
|
||||
/** Customer-initiated relabel order. */
|
||||
RELABEL_APPLICATION("换标下单"),
|
||||
/** Warehouse-to-warehouse transfer order. */
|
||||
WAREHOUSE_TRANSFER("仓储转运"),
|
||||
/** Domestic/local loose pallet order. */
|
||||
LOCAL_LOOSEBOARD("本土散板"),
|
||||
/** Return shipment that also requires relabeling. */
|
||||
RETURN_RELABEL("退货换标"),
|
||||
/** One-click dropship order, retained for existing data and workflows. */
|
||||
ONE_CLICK_DROPSHIP("一件代发");
|
||||
|
||||
private final String label;
|
||||
|
||||
LastlegOrderType(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String label() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -25,40 +25,40 @@ public class RelabelApplicationController {
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_CREATE + "')")
|
||||
@Operation(summary = "Create a relabel application", description = "Required authority: " + OverseasPermissions.RELABEL_ORDER_CREATE)
|
||||
public ApiResponse<RelabelResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @Valid @RequestBody RelabelRequests.Create request) {
|
||||
return ApiResponse.success(service.create(scope(user, customerId), operator(user), request));
|
||||
@Valid @RequestBody RelabelRequests.Create request) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.create("0", operator(user), request));
|
||||
}
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_LIST + "')")
|
||||
@Operation(summary = "Page relabel applications", description = "Required authority: " + OverseasPermissions.RELABEL_ORDER_LIST)
|
||||
public ApiResponse<PageResult<RelabelResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @Valid RelabelRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
@Valid RelabelRequests.Query query) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.page(null, query));
|
||||
}
|
||||
@GetMapping("/{relabelNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_LIST + "')")
|
||||
@Operation(summary = "Get a relabel application", description = "Required authority: " + OverseasPermissions.RELABEL_ORDER_LIST)
|
||||
public ApiResponse<RelabelResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String relabelNo) {
|
||||
return ApiResponse.success(service.detail(scope(user, customerId), relabelNo));
|
||||
@PathVariable String relabelNo) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.detail(null, relabelNo));
|
||||
}
|
||||
@PutMapping("/{relabelNo}/status")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_STATUS + "')")
|
||||
@Operation(summary = "Change relabel application status", description = "Required authority: " + OverseasPermissions.RELABEL_ORDER_STATUS)
|
||||
public ApiResponse<RelabelResponses.Detail> status(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String relabelNo,
|
||||
@PathVariable String relabelNo,
|
||||
@Valid @RequestBody RelabelRequests.ChangeStatus request) {
|
||||
return ApiResponse.success(service.changeStatus(scope(user, customerId), relabelNo, operator(user), request));
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.changeStatus(null, relabelNo, operator(user), request));
|
||||
}
|
||||
private String scope(AuthenticatedUser user, String requested) {
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requested == null || requested.isBlank() || "0".equals(requested)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
|
||||
}
|
||||
return requested.trim();
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
private String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : String.valueOf(user.userId()); }
|
||||
}
|
||||
|
||||
+6
@@ -11,9 +11,15 @@ public interface RelabelApplicationMapper extends BaseMapper<RelabelApplication>
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1")
|
||||
RelabelApplication findNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE relabel_no=#{relabelNo} LIMIT 1")
|
||||
RelabelApplication findAnyByRelabelNo(@Param("relabelNo") String relabelNo);
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
RelabelApplication findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE")
|
||||
RelabelApplication lockNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE")
|
||||
RelabelApplication lockAnyByRelabelNo(@Param("relabelNo") String relabelNo);
|
||||
}
|
||||
|
||||
+10
-4
@@ -128,7 +128,7 @@ public class RelabelApplicationService {
|
||||
throw badRequest("createdFrom must not be after createdTo");
|
||||
}
|
||||
LambdaQueryWrapper<RelabelApplication> wrapper = new LambdaQueryWrapper<RelabelApplication>()
|
||||
.eq(RelabelApplication::getCustomerId, customerId)
|
||||
.eq(StringUtils.hasText(customerId), RelabelApplication::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.relabelNo()), RelabelApplication::getRelabelNo, query.relabelNo())
|
||||
.like(StringUtils.hasText(query.customerOrderNo()), RelabelApplication::getCustomerOrderNo, query.customerOrderNo())
|
||||
.eq(StringUtils.hasText(query.status()), RelabelApplication::getStatus, query.status())
|
||||
@@ -147,9 +147,10 @@ public class RelabelApplicationService {
|
||||
@Transactional(readOnly = true)
|
||||
public RelabelResponses.Detail detail(String customerId, String relabelNo) {
|
||||
RelabelApplication application = require(customerId, relabelNo);
|
||||
String recordCustomerId = application.getCustomerId();
|
||||
List<RelabelItem> detailItems = itemList(application.getId());
|
||||
List<RelabelStatusHistory> statusHistory = histories.selectList(new LambdaQueryWrapper<RelabelStatusHistory>()
|
||||
.eq(RelabelStatusHistory::getCustomerId, customerId)
|
||||
.eq(RelabelStatusHistory::getCustomerId, recordCustomerId)
|
||||
.eq(RelabelStatusHistory::getApplicationId, application.getId())
|
||||
.orderByAsc(RelabelStatusHistory::getCreatedAt));
|
||||
return new RelabelResponses.Detail(summary(application, detailItems), application.getRemark(),
|
||||
@@ -161,8 +162,11 @@ public class RelabelApplicationService {
|
||||
@Transactional
|
||||
public RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.ChangeStatus request) {
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
RelabelApplication application = StringUtils.hasText(customerId)
|
||||
? applications.lockNo(customerId, relabelNo)
|
||||
: applications.lockAnyByRelabelNo(relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
customerId = application.getCustomerId();
|
||||
String next = request.status().trim().toUpperCase();
|
||||
if (!STATUSES.contains(next)) throw badRequest("Unsupported relabel status");
|
||||
if (next.equals(application.getStatus())) return detail(customerId, relabelNo);
|
||||
@@ -232,7 +236,9 @@ public class RelabelApplicationService {
|
||||
.orderByAsc(RelabelItem::getId));
|
||||
}
|
||||
private RelabelApplication require(String customerId, String relabelNo) {
|
||||
RelabelApplication value = applications.findNo(customerId, relabelNo);
|
||||
RelabelApplication value = StringUtils.hasText(customerId)
|
||||
? applications.findNo(customerId, relabelNo)
|
||||
: applications.findAnyByRelabelNo(relabelNo);
|
||||
if (value == null) throw notFound("Relabel application not found");
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ public class LastlegOrderService {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required");
|
||||
}
|
||||
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, ids).last("FOR UPDATE"));
|
||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||
.in(LastlegOrder::getId, ids).last("FOR UPDATE"));
|
||||
if (orders.size() != ids.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more tail-leg orders were not found");
|
||||
}
|
||||
@@ -123,7 +124,7 @@ public class LastlegOrderService {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo");
|
||||
}
|
||||
return new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId)
|
||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.orderNo()), LastlegOrder::getOrderNo, query.orderNo())
|
||||
.like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode())
|
||||
.like(StringUtils.hasText(query.sourceOrderNo()), LastlegOrder::getSourceOrderNo, query.sourceOrderNo())
|
||||
@@ -139,7 +140,8 @@ public class LastlegOrderService {
|
||||
|
||||
private LastlegOrder findOwned(String customerId, long orderId) {
|
||||
LastlegOrder order = orderMapper.selectOne(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getId, orderId));
|
||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||
.eq(LastlegOrder::getId, orderId));
|
||||
if (order == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tail-leg order not found");
|
||||
return order;
|
||||
}
|
||||
@@ -153,7 +155,7 @@ public class LastlegOrderService {
|
||||
|
||||
private LastlegOrderSummary summary(LastlegOrder o) {
|
||||
return new LastlegOrderSummary(o.getId(), o.getOrderNo(), o.getOrderType(), o.getSourceInboundId(),
|
||||
o.getInboundCode(), o.getSourceOrderNo(), o.getCustomerName(), o.getOverseasWarehouseId(),
|
||||
o.getInboundCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
|
||||
o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
|
||||
o.getTransferWarehouseName(), o.getFbaWarehouseCode(), o.getContainerNo(), o.getContainerStatus(),
|
||||
o.getAbnormal(), o.getDetained(), o.getPalletCount(), o.getPackageCount(), o.getVolumeCbm(),
|
||||
|
||||
+3
-3
@@ -340,14 +340,14 @@ public class WarehouseTransferService {
|
||||
for (WarehouseTransferInventoryAllocation allocation : allocations) {
|
||||
if (!"RESERVED".equals(allocation.getStatus())) throw conflict("Warehouse transfer allocation was already processed");
|
||||
String keyBase = "WAREHOUSE_TRANSFER:" + transferNo + ":" + allocation.getInventoryPositionId();
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(customerId,
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(
|
||||
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":ALLOCATE", trim(request.remark()));
|
||||
inventory.allocate(customerId, operatorId, move);
|
||||
inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(customerId,
|
||||
inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":PICK", trim(request.remark())));
|
||||
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(customerId,
|
||||
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
allocation.getInventoryPositionId(), allocation.getPackageCount(), transferNo, transferNo,
|
||||
"WAREHOUSE_TRANSFER_OUTBOUND:" + transferNo, keyBase + ":SHIP", trim(request.remark())));
|
||||
allocation.setStatus("SHIPPED"); allocation.setActivePositionKey(null);
|
||||
|
||||
Reference in New Issue
Block a user