feat(appointment): share pickup and receiving followups by container
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.lastleg.appointmentplanning;
|
||||
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AppointmentContainer {
|
||||
private String containerBookingId;
|
||||
private String containerNo;
|
||||
private long orderCount;
|
||||
private long customerCount;
|
||||
private Long packages;
|
||||
private long unknownPackageOrders;
|
||||
private LocalDateTime earliestArrivalAt;
|
||||
private LocalDateTime latestArrivalAt;
|
||||
private String standardNode;
|
||||
private Long sourceVersion;
|
||||
private boolean incompleteScope;
|
||||
private List<Warehouse> warehouses = List.of();
|
||||
public record Warehouse(String containerBookingId, String warehouseId, String warehouseName,
|
||||
long orderCount, long customerCount) { }
|
||||
public record Context(String containerBookingId, String containerNo, String warehouseId,
|
||||
String warehouseName, boolean canFollowup, List<String> blockReasons) { }
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.baiyue.lastleg.appointmentplanning;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/appointment-planning/containers")
|
||||
public class AppointmentContainerController {
|
||||
private final AppointmentContainerService containers;
|
||||
private final AppointmentFollowupService followups;
|
||||
public AppointmentContainerController(AppointmentContainerService containers,AppointmentFollowupService followups) {
|
||||
this.containers=containers; this.followups=followups;
|
||||
}
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:appointment:list') and hasAuthority('platform:overseas-warehouse:last-mile-order:list')")
|
||||
public ApiResponse<PageResult<AppointmentContainer>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(defaultValue="1") int page,@RequestParam(defaultValue="20") int size,@RequestParam(required=false) String keyword) {
|
||||
AppointmentPlanningController.requirePlatform(user);
|
||||
return ApiResponse.success(containers.page(page,size,keyword));
|
||||
}
|
||||
@GetMapping("/{containerId}")
|
||||
@PreAuthorize("hasAuthority('platform:appointment:list') and hasAuthority('platform:overseas-warehouse:last-mile-order:list')")
|
||||
public ApiResponse<AppointmentContainer.Context> context(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String containerId,@RequestParam(required=false) String warehouseId) {
|
||||
AppointmentPlanningController.requirePlatform(user);
|
||||
return ApiResponse.success(containers.context(containerId,warehouseId));
|
||||
}
|
||||
@GetMapping("/{containerId}/follow-ups")
|
||||
@PreAuthorize("hasAuthority('platform:appointment:list') and hasAuthority('platform:overseas-warehouse:last-mile-order:list')")
|
||||
public ApiResponse<AppointmentFollowupService.History> history(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String containerId,@RequestParam(required=false) String warehouseId,
|
||||
@RequestParam(defaultValue="1") int page,@RequestParam(defaultValue="20") int size) {
|
||||
AppointmentPlanningController.requirePlatform(user);
|
||||
return ApiResponse.success(followups.containerHistory(containerId,warehouseId,page,size));
|
||||
}
|
||||
@PostMapping("/{containerId}/follow-ups")
|
||||
@PreAuthorize("hasAuthority('platform:appointment:list') and hasAuthority('platform:overseas-warehouse:last-mile-order:list') and hasAuthority('platform:appointment:followup')")
|
||||
public ApiResponse<AppointmentFollowupService.Record> save(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String containerId,@RequestParam(required=false) String warehouseId,
|
||||
@Valid @RequestBody AppointmentFollowupService.Save request) {
|
||||
AppointmentPlanningController.requirePlatform(user);
|
||||
return ApiResponse.success(followups.saveContainer(containerId,warehouseId,request,user.userId().toString(),user.displayName()==null?user.username():user.displayName()));
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.baiyue.lastleg.appointmentplanning;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AppointmentContainerMapper {
|
||||
String FILTER = """
|
||||
FROM ll_order o JOIN ll_container_shipping_gate g ON g.container_booking_id=o.source_container_booking_id
|
||||
WHERE o.order_type='FIRST_LEG' AND o.current_status NOT IN ('CANCELLED','COMPLETED') AND g.node_sequence >=40
|
||||
AND o.overseas_warehouse_id IN <foreach collection='origins' item='id' open='(' separator=',' close=')'>#{id}</foreach>
|
||||
<if test='containerId != null'>AND o.source_container_booking_id=#{containerId}</if>
|
||||
<if test='keyword != null'>AND EXISTS(SELECT 1 FROM ll_order search_o
|
||||
WHERE search_o.source_container_booking_id=o.source_container_booking_id AND search_o.order_type='FIRST_LEG'
|
||||
AND search_o.current_status NOT IN ('CANCELLED','COMPLETED') AND search_o.overseas_warehouse_id IN
|
||||
<foreach collection='origins' item='id' open='(' separator=',' close=')'>#{id}</foreach>
|
||||
AND (search_o.container_no LIKE CONCAT('%',#{keyword},'%') OR search_o.inbound_code LIKE CONCAT('%',#{keyword},'%')))</if>
|
||||
""";
|
||||
String GROUP = " GROUP BY o.source_container_booking_id,g.container_no,g.standard_node,g.aggregate_version ";
|
||||
@Select("<script>SELECT COUNT(*) FROM (SELECT o.source_container_booking_id " + FILTER + GROUP + ") counted</script>")
|
||||
long count(@Param("origins") List<String> origins, @Param("keyword") String keyword, @Param("containerId") String containerId);
|
||||
|
||||
@Select("<script>" + """
|
||||
SELECT o.source_container_booking_id AS container_booking_id,g.container_no,
|
||||
COUNT(*) AS order_count,COUNT(DISTINCT o.customer_id) AS customer_count,
|
||||
SUM(CASE WHEN o.package_count IS NULL OR o.package_count<=0 THEN 1 ELSE 0 END) AS unknown_package_orders,
|
||||
CASE WHEN SUM(CASE WHEN o.package_count IS NULL OR o.package_count<=0 THEN 1 ELSE 0 END)>0
|
||||
THEN NULL ELSE SUM(o.package_count) END AS packages,
|
||||
MIN(o.estimated_arrival_at) AS earliest_arrival_at,MAX(o.estimated_arrival_at) AS latest_arrival_at,
|
||||
g.standard_node,g.aggregate_version AS source_version,
|
||||
EXISTS(SELECT 1 FROM ll_order other_o WHERE other_o.source_container_booking_id=o.source_container_booking_id
|
||||
AND other_o.order_type='FIRST_LEG' AND other_o.current_status NOT IN ('CANCELLED','COMPLETED')
|
||||
AND (other_o.overseas_warehouse_id IS NULL OR other_o.overseas_warehouse_id NOT IN
|
||||
<foreach collection='origins' item='id' open='(' separator=',' close=')'>#{id}</foreach>)) AS incomplete_scope
|
||||
""" + FILTER + GROUP + " ORDER BY MAX(o.created_at) DESC,o.source_container_booking_id LIMIT #{size} OFFSET #{offset}</script>")
|
||||
List<AppointmentContainer> page(@Param("origins") List<String> origins, @Param("keyword") String keyword,
|
||||
@Param("containerId") String containerId, @Param("size") int size, @Param("offset") long offset);
|
||||
|
||||
@Select("<script>" + """
|
||||
SELECT o.source_container_booking_id AS containerBookingId,o.overseas_warehouse_id AS warehouseId,
|
||||
COUNT(*) AS orderCount,COUNT(DISTINCT o.customer_id) AS customerCount
|
||||
FROM ll_order o WHERE o.order_type='FIRST_LEG' AND o.current_status NOT IN ('CANCELLED','COMPLETED')
|
||||
AND o.source_container_booking_id IN <foreach collection='containers' item='id' open='(' separator=',' close=')'>#{id}</foreach>
|
||||
AND o.overseas_warehouse_id IN <foreach collection='origins' item='id' open='(' separator=',' close=')'>#{id}</foreach>
|
||||
GROUP BY o.source_container_booking_id,o.overseas_warehouse_id ORDER BY o.source_container_booking_id,o.overseas_warehouse_id
|
||||
""" + "</script>")
|
||||
List<WarehouseCount> warehouses(@Param("origins") List<String> origins, @Param("containers") List<String> containers);
|
||||
record WarehouseCount(String containerBookingId, String warehouseId, long orderCount, long customerCount) { }
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.baiyue.lastleg.appointmentplanning;
|
||||
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AppointmentContainerService {
|
||||
private final AppointmentContainerMapper mapper;
|
||||
private final AppointmentWarehouseClient warehouses;
|
||||
private final String token;
|
||||
public AppointmentContainerService(AppointmentContainerMapper mapper, AppointmentWarehouseClient warehouses,
|
||||
@Value("${baiyue.security.internal-service-token:}") String token) {
|
||||
this.mapper=mapper; this.warehouses=warehouses; this.token=token;
|
||||
}
|
||||
public PageResult<AppointmentContainer> page(int page,int size,String keyword) {
|
||||
if(page<1 || size<1 || size>100 || keyword!=null && keyword.length()>128) throw bad("分页或搜索条件无效");
|
||||
var catalog=catalog();
|
||||
if(catalog.isEmpty()) return new PageResult<>(List.of(),page,size,0,0);
|
||||
var origins=List.copyOf(catalog.keySet());
|
||||
keyword=clean(keyword);
|
||||
long total=mapper.count(origins,keyword,null);
|
||||
var rows=total==0 ? List.<AppointmentContainer>of() : mapper.page(origins,keyword,null,size,(long)(page-1)*size);
|
||||
attachWarehouses(rows,catalog);
|
||||
return new PageResult<>(rows,page,size,total,(total+size-1)/size);
|
||||
}
|
||||
public AppointmentContainer.Context context(String containerId,String warehouseId) {
|
||||
subjectKey(containerId,warehouseId);
|
||||
var catalog=catalog();
|
||||
var rows=catalog.isEmpty() ? List.<AppointmentContainer>of() : mapper.page(List.copyOf(catalog.keySet()),null,containerId,1,0);
|
||||
if(rows.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND,"未找到美国范围内已开船的有效柜次");
|
||||
attachWarehouses(rows,catalog);
|
||||
var row=rows.get(0);
|
||||
String warehouseName=null;
|
||||
if(warehouseId!=null) warehouseName=row.getWarehouses().stream().filter(w->w.warehouseId().equals(warehouseId))
|
||||
.map(AppointmentContainer.Warehouse::warehouseName).findFirst().orElseThrow(()->bad("该接仓点不属于当前柜次"));
|
||||
return new AppointmentContainer.Context(containerId,row.getContainerNo(),warehouseId,warehouseName,
|
||||
!row.isIncompleteScope(),row.isIncompleteScope()?List.of("同柜仍有未明确或非美国接仓点,请先核对整柜范围"):List.of());
|
||||
}
|
||||
public static String subjectKey(String containerId,String warehouseId) {
|
||||
if(containerId==null || !containerId.matches("[A-Za-z0-9_-]{1,64}") || warehouseId!=null && !warehouseId.matches("[0-9]{1,20}")) throw bad("柜次或接仓点标识无效");
|
||||
return "CONTAINER:"+containerId+(warehouseId==null?"":":WAREHOUSE:"+warehouseId);
|
||||
}
|
||||
private void attachWarehouses(List<AppointmentContainer> rows,Map<String,AppointmentWarehouseClient.Warehouse> catalog) {
|
||||
if(rows.isEmpty()) return;
|
||||
var groups=mapper.warehouses(List.copyOf(catalog.keySet()),rows.stream().map(AppointmentContainer::getContainerBookingId).toList()).stream()
|
||||
.collect(Collectors.groupingBy(AppointmentContainerMapper.WarehouseCount::containerBookingId));
|
||||
for(var row:rows) row.setWarehouses(groups.getOrDefault(row.getContainerBookingId(),List.of()).stream()
|
||||
.map(w->new AppointmentContainer.Warehouse(w.containerBookingId(),w.warehouseId(),catalog.get(w.warehouseId()).warehouseName(),w.orderCount(),w.customerCount())).toList());
|
||||
}
|
||||
private Map<String,AppointmentWarehouseClient.Warehouse> catalog() {
|
||||
try {
|
||||
var response=warehouses.catalog(token);
|
||||
if(response==null || !response.successful() || response.data()==null) throw new IllegalStateException();
|
||||
var result=new LinkedHashMap<String,AppointmentWarehouseClient.Warehouse>();
|
||||
for(var w:response.data()) if(w!=null && w.id()!=null && w.id()>0 && "US".equals(w.countryCode()) && Set.of("OVERSEAS","OVERSEAS_PARTNER").contains(Objects.toString(w.warehouseType(),""))) result.put(w.id().toString(),w);
|
||||
return result;
|
||||
} catch(RuntimeException error) { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"仓库目录暂不可用,无法核对美国整柜范围",error); }
|
||||
}
|
||||
private static String clean(String value) { return value==null || value.isBlank()?null:value.trim(); }
|
||||
private static ResponseStatusException bad(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST,message); }
|
||||
}
|
||||
+38
-5
@@ -24,8 +24,10 @@ public class AppointmentFollowupService {
|
||||
private final AppointmentSourceService sources;
|
||||
private final AppointmentFollowupMapper mapper;
|
||||
private final ObjectMapper json;
|
||||
public AppointmentFollowupService(AppointmentSourceService sources, AppointmentFollowupMapper mapper, ObjectMapper json) {
|
||||
this.sources = sources; this.mapper = mapper; this.json = json;
|
||||
private final AppointmentContainerService containers;
|
||||
public AppointmentFollowupService(AppointmentSourceService sources, AppointmentFollowupMapper mapper, ObjectMapper json,
|
||||
AppointmentContainerService containers) {
|
||||
this.sources = sources; this.mapper = mapper; this.json = json; this.containers = containers;
|
||||
}
|
||||
public record Save(@NotBlank @Size(max=32) String stage, @Min(0) int expectedVersion,
|
||||
@NotBlank @Size(max=64) String idempotencyKey,
|
||||
@@ -37,9 +39,16 @@ public class AppointmentFollowupService {
|
||||
private static final Set<String> STAGES = Set.of("DRAYAGE_CONTACT", "TERMINAL_APPOINTMENT", "WAREHOUSE_RECEIVING", "FBA_RECEIVING");
|
||||
|
||||
public History history(String sourceKey, String customerId, int page, int size) {
|
||||
requireCargoKey(sourceKey);
|
||||
return history(sourceKey,customerId,page,size,()->sources.resolve(sourceKey,customerId,null));
|
||||
}
|
||||
public History containerHistory(String containerId,String warehouseId,int page,int size) {
|
||||
return history(AppointmentContainerService.subjectKey(containerId,warehouseId),"0",page,size,()->containers.context(containerId,warehouseId));
|
||||
}
|
||||
private History history(String sourceKey,String customerId,int page,int size,Runnable verifySource) {
|
||||
if (page<1 || size<1 || size>100) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"分页参数无效");
|
||||
long total = mapper.count(sourceKey,customerId);
|
||||
if (total == 0) sources.resolve(sourceKey,customerId,null);
|
||||
if (total == 0) verifySource.run();
|
||||
var records = mapper.page(sourceKey,customerId,size,(long)(page-1)*size).stream().map(AppointmentFollowupService::view).toList();
|
||||
var versions = new java.util.LinkedHashMap<String,Integer>();
|
||||
STAGES.stream().sorted().forEach(stage -> versions.put(stage, mapper.version(sourceKey,customerId,stage)));
|
||||
@@ -48,6 +57,26 @@ public class AppointmentFollowupService {
|
||||
|
||||
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public Record save(String sourceKey, String customerId, Save request, String operatorId, String operatorName) {
|
||||
requireCargoKey(sourceKey);
|
||||
if(!"FBA_RECEIVING".equals(request.stage())) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"提柜及接仓事项请进入整柜跟进,避免按货物重复联系");
|
||||
return append(sourceKey,customerId,request,operatorId,operatorName,()->{
|
||||
var source=sources.resolve(sourceKey,customerId,null);
|
||||
return new Subject(source.orderId(),source.canFollowup());
|
||||
});
|
||||
}
|
||||
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public Record saveContainer(String containerId,String warehouseId,Save request,String operatorId,String operatorName) {
|
||||
String key=AppointmentContainerService.subjectKey(containerId,warehouseId);
|
||||
boolean allowed=warehouseId==null ? Set.of("DRAYAGE_CONTACT","TERMINAL_APPOINTMENT").contains(request.stage()) : "WAREHOUSE_RECEIVING".equals(request.stage());
|
||||
if(!allowed) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"请选择对应柜次或接仓点的跟进事项");
|
||||
return append(key,"0",request,operatorId,operatorName,()->{
|
||||
var context=containers.context(containerId,warehouseId);
|
||||
return new Subject(null,context.canFollowup());
|
||||
});
|
||||
}
|
||||
private record Subject(Long orderId,boolean canFollowup) { }
|
||||
private Record append(String sourceKey,String customerId,Save request,String operatorId,String operatorName,
|
||||
java.util.function.Supplier<Subject> resolve) {
|
||||
if (!STAGES.contains(request.stage())) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"请选择有效跟进事项");
|
||||
if (request.followupDate()!=null && (request.nextAction()==null || request.nextAction().isBlank())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"设置跟进日期时请填写下一步安排");
|
||||
@@ -60,10 +89,10 @@ public class AppointmentFollowupService {
|
||||
if (!hash.equals(existing.getRequestHash())) throw new ResponseStatusException(HttpStatus.CONFLICT,"重复请求内容不一致,请核对跟进记录");
|
||||
return view(existing);
|
||||
}
|
||||
var source = sources.resolve(sourceKey,customerId,null);
|
||||
var source = resolve.get();
|
||||
if (!source.canFollowup()) throw new ResponseStatusException(HttpStatus.CONFLICT,"当前来源仅可查看,请先核对承运状态");
|
||||
if (mapper.version(sourceKey,customerId,request.stage())!=request.expectedVersion()) throw new ResponseStatusException(HttpStatus.CONFLICT,"跟进记录已更新,请重新读取后提交");
|
||||
var value = new AppointmentFollowup(); value.setId(IdWorker.getId()); value.setSourceOrderId(source.orderId()); value.setSourceKey(source.sourceKey()); value.setCustomerId(source.customerId());
|
||||
var value = new AppointmentFollowup(); value.setId(IdWorker.getId()); value.setSourceOrderId(source.orderId()); value.setSourceKey(sourceKey); value.setCustomerId(customerId);
|
||||
value.setStage(request.stage()); value.setVersion(request.expectedVersion()+1); value.setContactedParty(clean(request.contactedParty()));
|
||||
value.setResult(request.result().trim()); value.setNextAction(clean(request.nextAction())); value.setFollowupDate(request.followupDate());
|
||||
value.setOperatorId(operatorId); value.setOperatorName(clean(operatorName)); value.setCommandKey(request.idempotencyKey());
|
||||
@@ -79,6 +108,10 @@ public class AppointmentFollowupService {
|
||||
return view(value);
|
||||
}
|
||||
private static String clean(String value) { return value==null || value.isBlank() ? null : value.trim(); }
|
||||
private static void requireCargoKey(String sourceKey) {
|
||||
if(sourceKey==null || !(sourceKey.startsWith("ORDER:") || sourceKey.startsWith("BOOKING:")))
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"请选择有效货物来源");
|
||||
}
|
||||
private static Record view(AppointmentFollowup row) {
|
||||
return new Record(row.getStage(),row.getVersion(),row.getContactedParty(),row.getResult(),row.getNextAction(),row.getFollowupDate(),row.getOperatorName(),row.getCreatedAt().toInstant(ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ public class AppointmentPlanningController {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(followups.save(sourceKey,customerId,request,user.userId().toString(),user.displayName()==null ? user.username() : user.displayName()));
|
||||
}
|
||||
private static void requirePlatform(AuthenticatedUser user) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1627,8 +1627,8 @@ CREATE TABLE IF NOT EXISTS ll_pda_inventory_move (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_appointment_followup (
|
||||
id BIGINT NOT NULL COMMENT '预约筹划跟进记录ID',
|
||||
source_key VARCHAR(160) NOT NULL COMMENT '稳定ORDER或BOOKING来源键,不因实收转换而变化',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '服务端核实的来源客户',
|
||||
source_key VARCHAR(160) NOT NULL COMMENT '稳定ORDER、BOOKING或CONTAINER及接仓点来源键',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '货源客户;整柜共享记录由服务端固定为平台作用域0',
|
||||
source_order_id BIGINT NULL COMMENT '已形成的真实尾程货源ID,尚未生成时为空',
|
||||
stage VARCHAR(32) NOT NULL COMMENT '跟进对象:DRAYAGE_CONTACT/TERMINAL_APPOINTMENT/WAREHOUSE_RECEIVING/FBA_RECEIVING',
|
||||
version INT NOT NULL COMMENT '同一货源及跟进对象的记录版本',
|
||||
|
||||
@@ -6,6 +6,108 @@ info:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/api/platform/v1/last-leg/appointment-planning/containers:
|
||||
get:
|
||||
operationId: appointmentPlanningContainers
|
||||
summary: 分页读取美国整柜提柜与接仓范围
|
||||
description: >-
|
||||
仅平台,同时要求platform:appointment:list与platform:overseas-warehouse:last-mile-order:list。
|
||||
已实际开船且仍有效的FIRST_LEG货物按柜汇总,不限FBA目的地,不要求入仓计划。
|
||||
仅统计美国接仓点货物;存在未知或非美国接仓点的同柜有效货物时incompleteScope=true,禁止新增整柜跟进。
|
||||
关键词命中某票时仍返回整柜美国范围数量;任一数量缺失时packages为空。预计到港保留来源快照,不代表实时船司ETA。
|
||||
parameters:
|
||||
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
|
||||
- {name: keyword, in: query, schema: {type: string, maxLength: 128}}
|
||||
responses:
|
||||
'200':
|
||||
description: 核对code后读取整柜分页
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
message: {type: string}
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
records: {type: array, items: {$ref: '#/components/schemas/Container'}}
|
||||
page: {type: integer}
|
||||
size: {type: integer}
|
||||
total: {type: integer}
|
||||
pages: {type: integer}
|
||||
'400': {description: 分页或搜索条件无效}
|
||||
'403': {description: 缺少读取权限或非平台身份}
|
||||
'503': {description: 美国仓库目录不可用}
|
||||
/api/platform/v1/last-leg/appointment-planning/containers/{containerId}:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/ContainerId'}
|
||||
- {$ref: '#/components/parameters/ReceivingWarehouse'}
|
||||
get:
|
||||
operationId: resolveAppointmentContainer
|
||||
summary: 核对整柜或接仓点的跟进资格
|
||||
description: 仅平台,同时要求两项列表权限;接仓点必须属于当前柜次。内部柜次ID仅供关联,不能作为展示柜号。
|
||||
responses:
|
||||
'200':
|
||||
description: code=200时data为已核实上下文
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
data: {$ref: '#/components/schemas/ContainerContext'}
|
||||
'400': {description: 标识无效或接仓点不属于该柜}
|
||||
'403': {description: 缺少读取权限或非平台身份}
|
||||
'404': {description: 未找到有效美国整柜来源}
|
||||
'503': {description: 仓库目录不可用}
|
||||
/api/platform/v1/last-leg/appointment-planning/containers/{containerId}/follow-ups:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/ContainerId'}
|
||||
- {$ref: '#/components/parameters/ReceivingWarehouse'}
|
||||
get:
|
||||
operationId: appointmentContainerFollowupHistory
|
||||
summary: 读取整柜或接仓点共享跟进历史
|
||||
description: 仅平台,同时要求两项列表权限;已有历史在来源离开活动列表后仍可读取,不按客户拆分。
|
||||
parameters:
|
||||
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
|
||||
responses:
|
||||
'200': {$ref: '#/paths/~1api~1platform~1v1~1last-leg~1appointment-planning~1sources~1{sourceKey}~1follow-ups/get/responses/200'}
|
||||
'400': {description: 标识或分页无效}
|
||||
'403': {description: 缺少读取权限或非平台身份}
|
||||
'404': {description: 无历史且来源不存在}
|
||||
post:
|
||||
operationId: appendAppointmentContainerFollowup
|
||||
summary: 追加整柜提柜联系或指定仓库接仓联系
|
||||
description: >-
|
||||
仅平台,两项列表权限及platform:appointment:followup均必需。
|
||||
未传warehouseId只接受DRAYAGE_CONTACT或TERMINAL_APPOINTMENT;传入时只接受WAREHOUSE_RECEIVING。
|
||||
服务端产生共享来源键,不接收customerId或操作者字段;同柜跨客户共用一份历史。
|
||||
版本、持久化幂等和跟进日期规则与货源跟进相同,不产生正式预约或执行结果。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [stage, expectedVersion, idempotencyKey, result]
|
||||
properties:
|
||||
stage: {type: string, enum: [DRAYAGE_CONTACT, TERMINAL_APPOINTMENT, WAREHOUSE_RECEIVING]}
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
idempotencyKey: {type: string, minLength: 1, maxLength: 64}
|
||||
contactedParty: {type: string, maxLength: 200, nullable: true}
|
||||
result: {type: string, minLength: 1, maxLength: 1000}
|
||||
nextAction: {type: string, maxLength: 500, nullable: true}
|
||||
followupDate: {type: string, format: date, nullable: true}
|
||||
responses:
|
||||
'200': {$ref: '#/paths/~1api~1platform~1v1~1last-leg~1appointment-planning~1sources~1{sourceKey}~1follow-ups/post/responses/200'}
|
||||
'400': {description: 事项与范围不符或参数无效}
|
||||
'403': {description: 缺少权限或非平台身份}
|
||||
'404': {description: 来源不存在}
|
||||
'409': {description: 范围待核对、版本或幂等内容冲突}
|
||||
'503': {description: 仓库目录不可用}
|
||||
/api/platform/v1/last-leg/appointment-planning/cargo:
|
||||
get:
|
||||
operationId: appointmentPlanningCargo
|
||||
@@ -123,7 +225,7 @@ paths:
|
||||
type: object
|
||||
required: [stage, expectedVersion, idempotencyKey, result]
|
||||
properties:
|
||||
stage: {type: string, enum: [DRAYAGE_CONTACT, TERMINAL_APPOINTMENT, WAREHOUSE_RECEIVING, FBA_RECEIVING]}
|
||||
stage: {type: string, enum: [FBA_RECEIVING], description: 提柜及接仓统一使用整柜接口,不按客户货源重复登记}
|
||||
expectedVersion: {type: integer, minimum: 0, description: 初次为0,后续取history.versions中对应事项版本}
|
||||
idempotencyKey: {type: string, maxLength: 64}
|
||||
contactedParty: {type: string, maxLength: 200, nullable: true}
|
||||
@@ -146,9 +248,45 @@ paths:
|
||||
'404': {description: 来源不存在或不可筹划}
|
||||
'409': {description: 版本冲突或同键异内容}
|
||||
components:
|
||||
parameters:
|
||||
ContainerId: {name: containerId, in: path, required: true, schema: {type: string, pattern: '^[A-Za-z0-9_-]{1,64}$'}}
|
||||
ReceivingWarehouse: {name: warehouseId, in: query, schema: {type: string, pattern: '^[0-9]{1,20}$'}, description: 不传表示整柜,传入表示该柜实际接仓点}
|
||||
securitySchemes:
|
||||
bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT}
|
||||
schemas:
|
||||
Container:
|
||||
type: object
|
||||
properties:
|
||||
containerBookingId: {type: string}
|
||||
containerNo: {type: string, nullable: true}
|
||||
orderCount: {type: integer}
|
||||
customerCount: {type: integer}
|
||||
packages: {type: integer, nullable: true}
|
||||
unknownPackageOrders: {type: integer}
|
||||
earliestArrivalAt: {type: string, nullable: true}
|
||||
latestArrivalAt: {type: string, nullable: true}
|
||||
standardNode: {type: string}
|
||||
sourceVersion: {type: string, nullable: true}
|
||||
incompleteScope: {type: boolean}
|
||||
warehouses:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
containerBookingId: {type: string}
|
||||
warehouseId: {type: string}
|
||||
warehouseName: {type: string}
|
||||
orderCount: {type: integer}
|
||||
customerCount: {type: integer}
|
||||
ContainerContext:
|
||||
type: object
|
||||
properties:
|
||||
containerBookingId: {type: string}
|
||||
containerNo: {type: string, nullable: true}
|
||||
warehouseId: {type: string, nullable: true}
|
||||
warehouseName: {type: string, nullable: true}
|
||||
canFollowup: {type: boolean}
|
||||
blockReasons: {type: array, items: {type: string}}
|
||||
FollowupRecord:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in New Issue
Block a user