feat: add self-operated analytics and enforce Lastleg module boundaries
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.lastleg.inboundplan.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.inboundplan.dto.*;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/appointment-planning/appointments")
|
||||
public class AppointmentSourceCreateController {
|
||||
private final InboundFbaAppointmentService service;
|
||||
public AppointmentSourceCreateController(InboundFbaAppointmentService service){this.service=service;}
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('platform:appointment:list') and hasAuthority('platform:overseas-warehouse:last-mile-order:list') and hasAuthority('"+OverseasPermissions.INBOUND_ORDER_FBA_APPOINTMENT+"')")
|
||||
public ApiResponse<FbaAppointmentResponse> create(@AuthenticationPrincipal AuthenticatedUser user,@Valid @RequestBody SourceFbaAppointmentRequest request) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.createFromSources(String.valueOf(user.userId()),request));
|
||||
}
|
||||
@GetMapping("/unscheduled")
|
||||
@PreAuthorize("hasAuthority('platform:appointment:list')")
|
||||
public ApiResponse<PageResult<FbaAppointmentResponse>> unscheduled(@AuthenticationPrincipal AuthenticatedUser user,@RequestParam(defaultValue="1") int page,@RequestParam(defaultValue="20") int size) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.unscheduled(page,size));
|
||||
}
|
||||
private static void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.userId()==null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (!"PLATFORM".equals(user.appCode()) || !Long.valueOf(0).equals(user.tenantId())) throw new ResponseStatusException(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import com.baiyue.lastleg.appointmentplanning.AppointmentCoverageService.SourceRef;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.util.List;
|
||||
@@ -11,4 +10,6 @@ public record SourceFbaAppointmentRequest(
|
||||
@NotBlank @Size(max=64) String fbaWarehouseCode,
|
||||
@NotNull @Valid FbaAppointmentDetails details,
|
||||
@NotBlank @Size(max=64) String idempotencyKey
|
||||
) { }
|
||||
) {
|
||||
public record SourceRef(String customerId, String sourceKey, String destinationKey) { }
|
||||
}
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import com.baiyue.lastleg.appointmentplanning.AppointmentSourceLinker;
|
||||
import com.baiyue.lastleg.appointmentplanning.AppointmentExternalSourceGateway;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointmentItem;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentItemMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Called by actual incoming materialization, never as a migration or historical data repair. */
|
||||
@Service
|
||||
public class InboundAppointmentSourceLinker implements AppointmentSourceLinker {
|
||||
private final LastlegOrderMapper orders;
|
||||
private final InboundFbaAppointmentItemMapper items;
|
||||
private final AppointmentExternalSourceGateway bookings;
|
||||
public InboundAppointmentSourceLinker(LastlegOrderMapper orders,InboundFbaAppointmentItemMapper items,AppointmentExternalSourceGateway bookings) {this.orders=orders;this.items=items;this.bookings=bookings;}
|
||||
@Override
|
||||
@Transactional(propagation=Propagation.MANDATORY)
|
||||
public void materialized(String customerId,String bookingNo,Long orderId) {
|
||||
var ref=new AppointmentExternalSourceGateway.SourceRef(customerId,bookingNo);
|
||||
bookings.lockForAppointment(List.of(ref));
|
||||
var matched=bookings.resolve(List.of(ref));
|
||||
if(matched.size()!=1 || !Objects.equals(matched.get(0).customerId(),customerId) || !Objects.equals(matched.get(0).bookingNo(),bookingNo) || !Objects.equals(matched.get(0).orderId(),orderId)) throw conflict("来货与原登记的归属不一致,请核对");
|
||||
var order=orderId==null?null:orders.selectById(orderId);
|
||||
if(order==null || !Objects.equals(order.getCustomerId(),customerId)) throw conflict("来货尚未形成对应客户的真实货物");
|
||||
var rows=items.selectList(new LambdaQueryWrapper<InboundFbaAppointmentItem>().eq(InboundFbaAppointmentItem::getSourceKey,"BOOKING:"+bookingNo)
|
||||
.eq(InboundFbaAppointmentItem::getCustomerId,customerId).eq(InboundFbaAppointmentItem::getActive,true).last("FOR UPDATE"));
|
||||
for(var row:rows) {
|
||||
if(row.getOrderId()!=null && !row.getOrderId().equals(orderId)) throw conflict("原预约已关联其他来货,不能自动改绑");
|
||||
if(Objects.equals(row.getOrderId(),orderId) && Objects.equals(row.getInboundCode(),order.getInboundCode())) continue;
|
||||
try {items.update(null,new LambdaUpdateWrapper<InboundFbaAppointmentItem>().eq(InboundFbaAppointmentItem::getId,row.getId()).eq(InboundFbaAppointmentItem::getActive,true)
|
||||
.set(InboundFbaAppointmentItem::getOrderId,orderId).set(InboundFbaAppointmentItem::getInboundCode,order.getInboundCode()));}
|
||||
catch(org.springframework.dao.DuplicateKeyException duplicate){throw conflict("该来货已有其他有效预约,请先核对原安排");}
|
||||
}
|
||||
}
|
||||
private static ResponseStatusException conflict(String message){return new ResponseStatusException(HttpStatus.CONFLICT,message);}
|
||||
}
|
||||
+4
-1
@@ -66,7 +66,10 @@ public class InboundFbaAppointmentService {
|
||||
}
|
||||
@Transactional(isolation=org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public FbaAppointmentResponse createFromSources(String operatorId,SourceFbaAppointmentRequest request) {
|
||||
return command("CREATE_FROM_SOURCES",operatorId,request.idempotencyKey(),request,()->create(operatorId,request.sources(),request.originWarehouseId(),request.fbaWarehouseCode(),request.details()));
|
||||
return command("CREATE_FROM_SOURCES",operatorId,request.idempotencyKey(),request,()->create(operatorId,
|
||||
request.sources()==null?null:request.sources().stream().map(source->source==null?null:
|
||||
new AppointmentCoverageService.SourceRef(source.customerId(),source.sourceKey(),source.destinationKey())).toList(),
|
||||
request.originWarehouseId(),request.fbaWarehouseCode(),request.details()));
|
||||
}
|
||||
private FbaAppointmentResponse create(String operator,List<AppointmentCoverageService.SourceRef> refs,String expectedOrigin,String expectedDestination,FbaAppointmentDetails details) {
|
||||
var resolved=resolveForWrite(refs,null);
|
||||
|
||||
Reference in New Issue
Block a user