fix: retain appointment cancellation reasons and query history
This commit is contained in:
+25
-2
@@ -7,6 +7,9 @@ import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ContainerInboundPlanPreviewResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.CancelFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentCancellationHistory;
|
||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentCancellationSummary;
|
||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
@@ -167,12 +170,32 @@ public class InboundPlanController {
|
||||
@PreAuthorize("hasAuthority('platform:appointment:delete')")
|
||||
@Operation(summary = "删除未关联排车的预约", description = "Required authority: platform:appointment:delete")
|
||||
public ApiResponse<Void> deleteFbaAppointment(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String appointmentNo, @RequestParam int version) {
|
||||
@PathVariable String appointmentNo, @Valid @RequestBody CancelFbaAppointmentRequest request) {
|
||||
requireAppointmentPlatform(user);
|
||||
appointmentService.delete(String.valueOf(user.userId()), appointmentNo, version);
|
||||
appointmentService.delete(String.valueOf(user.userId()), appointmentNo, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/fba-appointments/{appointmentNo}/cancellations")
|
||||
@PreAuthorize("hasAnyAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "','" + OverseasPermissions.APPOINTMENT_LIST + "')")
|
||||
@Operation(summary = "分页读取预约取消历史", description = "平台专用;沿用预约详情只读权限,不返回内部身份或原始命令JSON")
|
||||
public ApiResponse<PageResult<FbaAppointmentCancellationHistory>> fbaAppointmentCancellations(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String appointmentNo,
|
||||
@RequestParam(defaultValue="1") int page,@RequestParam(defaultValue="20") int size) {
|
||||
requireAppointmentPlatform(user);
|
||||
return ApiResponse.success(appointmentService.cancellationHistory(appointmentNo,page,size));
|
||||
}
|
||||
|
||||
@GetMapping("/fba-appointments/cancellations")
|
||||
@PreAuthorize("hasAnyAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "','" + OverseasPermissions.APPOINTMENT_LIST + "')")
|
||||
@Operation(summary = "分页读取预约取消记录摘要", description = "平台专用;沿用预约详情只读权限,仅取消命令摘要,不逐行查询")
|
||||
public ApiResponse<PageResult<FbaAppointmentCancellationSummary>> fbaAppointmentCancellationRecords(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,@RequestParam(defaultValue="1") int page,
|
||||
@RequestParam(defaultValue="20") int size) {
|
||||
requireAppointmentPlatform(user);
|
||||
return ApiResponse.success(appointmentService.cancellations(page,size));
|
||||
}
|
||||
|
||||
private void requireAppointmentPlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.userId() == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
public record CancelFbaAppointmentRequest(@NotNull @Min(0) Integer version,
|
||||
@NotBlank @Size(max=1000) String cancelReason) { }
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** Business-only cancellation history; never exposes command JSON or internal identities. */
|
||||
public record FbaAppointmentCancellationHistory(String appointmentNo, Integer version, String cancelReason,
|
||||
LocalDateTime cancelledAt, Snapshot snapshot) {
|
||||
public record Snapshot(String fbaWarehouseCode, String appointmentStatus, String appointmentRemark,
|
||||
LocalDate plannedDate, LocalDateTime plannedAt, LocalDateTime plannedEndAt,
|
||||
String confirmationNo, LocalDateTime appointmentAt, LocalDateTime appointmentEndAt,
|
||||
String timezone, int selectedCount) { }
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record FbaAppointmentCancellationSummary(String appointmentNo,Integer version,String cancelReason,
|
||||
LocalDateTime cancelledAt,String fbaWarehouseCode,int selectedCount) { }
|
||||
+5
@@ -11,4 +11,9 @@ public interface FbaAppointmentCommandMapper {
|
||||
int begin(@Param("key") String key,@Param("hash") String hash,@Param("operation") String operation,@Param("operator") String operator,@Param("createdAt") java.time.LocalDateTime createdAt);
|
||||
@Update("UPDATE ll_fba_appointment_command SET appointment_no=#{appointmentNo},response_json=#{response} WHERE command_key=#{key} AND response_json IS NULL")
|
||||
int finish(@Param("key") String key,@Param("appointmentNo") String appointmentNo,@Param("response") String response);
|
||||
record Cancellation(String responseJson,java.time.LocalDateTime createdAt) { }
|
||||
@Select("SELECT COUNT(*) FROM ll_fba_appointment_command WHERE operation=CONCAT('CANCEL:',appointment_no) AND response_json IS NOT NULL AND (#{number} IS NULL OR appointment_no=#{number})")
|
||||
long cancellationCount(@Param("number") String number);
|
||||
@Select("SELECT response_json AS responseJson,created_at AS createdAt FROM ll_fba_appointment_command WHERE operation=CONCAT('CANCEL:',appointment_no) AND response_json IS NOT NULL AND (#{number} IS NULL OR appointment_no=#{number}) ORDER BY created_at DESC,command_key DESC LIMIT #{size} OFFSET #{offset}")
|
||||
java.util.List<Cancellation> cancellations(@Param("number") String number,@Param("size") int size,@Param("offset") long offset);
|
||||
}
|
||||
|
||||
+42
-5
@@ -120,12 +120,15 @@ public class InboundFbaAppointmentService {
|
||||
});
|
||||
}
|
||||
@Transactional(isolation=org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public void delete(String operatorId,String appointmentNo,int version) {
|
||||
command("CANCEL:"+appointmentNo,operatorId,"CANCEL:"+appointmentNo+":"+version,version,()->{
|
||||
public void delete(String operatorId,String appointmentNo,CancelFbaAppointmentRequest request) {
|
||||
if(request==null || request.version()==null || request.version()<0 || clean(request.cancelReason())==null || request.cancelReason().length()>1000) throw bad("请填写取消原因和有效版本");
|
||||
int version=request.version();
|
||||
var cancellation=new CancelFbaAppointmentRequest(version,request.cancelReason().trim());
|
||||
command("CANCEL:"+appointmentNo,operatorId,"CANCEL:"+appointmentNo+":"+version,cancellation,()->{
|
||||
var appointment=find(appointmentNo,true);if(appointment==null) throw missing();
|
||||
if(!Objects.equals(appointment.getVersion(),version)) throw conflict("预约已更新,请重新读取后操作");
|
||||
if(queryMapper.countActiveDispatches(appointment.getId())>0) throw conflict("预约货物已关联排车,不允许取消,请先处理关联排车");
|
||||
appointment.setAppointmentStatus("CANCELLED");appointment.setAppointmentRemark("操作员取消预约安排");
|
||||
appointment.setAppointmentStatus("CANCELLED");
|
||||
appointment.setVersion(version+1);appointment.setUpdatedAt(now());appointmentMapper.updateById(appointment);
|
||||
var rows=items(appointment.getId());rows.forEach(item->{item.setActive(false);itemMapper.updateById(item);});
|
||||
return response(appointment,rows);
|
||||
@@ -196,13 +199,47 @@ public class InboundFbaAppointmentService {
|
||||
try {commands.begin(key,hash,operation,operator,now());}
|
||||
catch(DuplicateKeyException duplicate){return replay(commands.find(key),hash);}
|
||||
var response=action.get();
|
||||
try {if(commands.finish(key,response.appointmentNo(),json.writeValueAsString(response))!=1)throw conflict("预约命令结果未能保存");}
|
||||
try {if(commands.finish(key,response.appointmentNo(),commandSnapshot(response,request))!=1)throw conflict("预约命令结果未能保存");}
|
||||
catch(com.fasterxml.jackson.core.JsonProcessingException error){throw new IllegalStateException("无法保存预约命令结果",error);}
|
||||
return response;
|
||||
}
|
||||
private String commandSnapshot(FbaAppointmentResponse response,Object request) throws com.fasterxml.jackson.core.JsonProcessingException {
|
||||
var node=json.valueToTree(response);
|
||||
if(request instanceof CancelFbaAppointmentRequest cancellation) ((com.fasterxml.jackson.databind.node.ObjectNode)node).put("cancelReason",cancellation.cancelReason());
|
||||
return json.writeValueAsString(node);
|
||||
}
|
||||
private FbaAppointmentResponse readSnapshot(String value) throws com.fasterxml.jackson.core.JsonProcessingException {
|
||||
var node=(com.fasterxml.jackson.databind.node.ObjectNode)json.readTree(value);
|
||||
node.remove("cancelReason");
|
||||
return json.treeToValue(node,FbaAppointmentResponse.class);
|
||||
}
|
||||
@Transactional(readOnly=true)
|
||||
public PageResult<FbaAppointmentCancellationHistory> cancellationHistory(String appointmentNo,int page,int size) {
|
||||
if(page<1 || size<1 || size>100) throw bad("分页参数无效");
|
||||
var appointment=find(appointmentNo,false);if(appointment==null) throw missing();
|
||||
return readCancellationHistory(appointment.getAppointmentNo(),page,size);
|
||||
}
|
||||
@Transactional(readOnly=true)
|
||||
public PageResult<FbaAppointmentCancellationSummary> cancellations(int page,int size) {
|
||||
if(page<1 || size<1 || size>100) throw bad("分页参数无效");
|
||||
var history=readCancellationHistory(null,page,size);
|
||||
return new PageResult<>(history.records().stream().map(row->new FbaAppointmentCancellationSummary(row.appointmentNo(),row.version(),row.cancelReason(),row.cancelledAt(),row.snapshot().fbaWarehouseCode(),row.snapshot().selectedCount())).toList(),page,size,history.total(),history.pages());
|
||||
}
|
||||
private PageResult<FbaAppointmentCancellationHistory> readCancellationHistory(String number,int page,int size) {
|
||||
long total=commands.cancellationCount(number);
|
||||
var records=commands.cancellations(number,size,(long)(page-1)*size).stream().map(row->{
|
||||
try {
|
||||
var snapshot=readSnapshot(row.responseJson());
|
||||
var reason=json.readTree(row.responseJson()).path("cancelReason");
|
||||
return new FbaAppointmentCancellationHistory(snapshot.appointmentNo(),snapshot.version(),reason.isTextual()?clean(reason.asText()):null,row.createdAt(),
|
||||
new FbaAppointmentCancellationHistory.Snapshot(snapshot.fbaWarehouseCode(),snapshot.appointmentStatus(),snapshot.appointmentRemark(),snapshot.plannedDate(),snapshot.plannedAt(),snapshot.plannedEndAt(),snapshot.confirmationNo(),snapshot.appointmentAt(),snapshot.appointmentEndAt(),snapshot.timezone(),snapshot.selectedCount()));
|
||||
} catch(com.fasterxml.jackson.core.JsonProcessingException error) {throw new IllegalStateException("取消历史不可读",error);}
|
||||
}).toList();
|
||||
return new PageResult<>(records,page,size,total,(total+size-1)/size);
|
||||
}
|
||||
private FbaAppointmentResponse replay(FbaAppointmentCommandMapper.Command value,String hash) {
|
||||
if(value==null || !hash.equals(value.requestHash()) || value.responseJson()==null)throw conflict("重复请求内容不一致或结果尚未就绪,请重新核对");
|
||||
try {return json.readValue(value.responseJson(),FbaAppointmentResponse.class);}
|
||||
try {return readSnapshot(value.responseJson());}
|
||||
catch(com.fasterxml.jackson.core.JsonProcessingException error){throw new IllegalStateException("预约命令记录不可读",error);}
|
||||
}
|
||||
private InboundFbaAppointment find(String number,boolean lock) {
|
||||
|
||||
Reference in New Issue
Block a user