优化
This commit is contained in:
@@ -4,6 +4,7 @@ import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 头程履约微服务启动入口,负责装配履约任务组件并扫描 MyBatis-Plus Mapper。
|
||||
@@ -19,6 +20,7 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
"com.baiyue.firstleg.customs.mapper"
|
||||
})
|
||||
@EnableFeignClients(basePackages = "com.baiyue.firstleg")
|
||||
@EnableScheduling
|
||||
@SpringBootApplication(scanBasePackages = "com.baiyue")
|
||||
public class BaiyueFirstlegApplication {
|
||||
public static void main(String[] args) { SpringApplication.run(BaiyueFirstlegApplication.class, args); }
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.firstleg.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceEvent;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@FeignClient(name = "finance-service", contextId = "firstlegFinanceFeeSourceEventClient",
|
||||
url = "${baiyue.clients.finance.url:}", path = "/internal/finance/v1/fee-source-events")
|
||||
public interface FinanceFeeSourceEventClient {
|
||||
@PostMapping
|
||||
ApiResponse<Acknowledgement> receive(
|
||||
@RequestHeader("X-Internal-Service-Token") String internalToken,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@RequestBody FinanceFeeSourceEvent event);
|
||||
|
||||
record Acknowledgement(@JsonProperty("feeSourceId") Long feeSourceId) {
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot;
|
||||
import com.baiyue.firstleg.service.FinanceFeeSourceSnapshotService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/internal/firstleg/v1")
|
||||
public class InternalFinanceFeeSourceController {
|
||||
private final FinanceFeeSourceSnapshotService service;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String expectedToken;
|
||||
|
||||
@GetMapping("/inbounds/{inboundId}/fees/{feeId}/source-snapshot")
|
||||
public ApiResponse<FinanceFeeSourceSnapshot> inbound(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable long inboundId, @PathVariable long feeId) {
|
||||
authenticate(token);
|
||||
return ApiResponse.success(service.inbound(inboundId, feeId));
|
||||
}
|
||||
|
||||
@GetMapping("/containers/{containerId}/fees/{feeId}/source-snapshot")
|
||||
public ApiResponse<FinanceFeeSourceSnapshot> container(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable long containerId, @PathVariable long feeId) {
|
||||
authenticate(token);
|
||||
return ApiResponse.success(service.container(containerId, feeId));
|
||||
}
|
||||
|
||||
private void authenticate(String token) {
|
||||
if (token == null || expectedToken == null || expectedToken.isBlank()
|
||||
|| !MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8),
|
||||
expectedToken.getBytes(StandardCharsets.UTF_8))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "内部服务令牌无效");
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.AdjustRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.RejectRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.VersionRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.service.ContainerFeeDetailService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/platform/v1/loading-plans/{loadingPlanId}/fee-details")
|
||||
public class PlatformContainerFeeDetailController {
|
||||
private final ContainerFeeDetailService service;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:view')")
|
||||
public ApiResponse<List<ContainerFeeDetailEntity>> list(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long accountId,
|
||||
@PathVariable long loadingPlanId) {
|
||||
return ApiResponse.success(service.list(FirstlegScopeGuard.platform(user, accountId),
|
||||
loadingPlanId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:edit')")
|
||||
@BusinessAudit(event = "CONTAINER_FEE_DETAIL_CREATE", resourceType = "LOADING_PLAN", resourceId = "#loadingPlanId", captureBefore = false)
|
||||
public ApiResponse<ContainerFeeDetailEntity> create(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long accountId,
|
||||
@PathVariable long loadingPlanId, @Valid @RequestBody SaveRequest request) {
|
||||
return ApiResponse.success(service.create(FirstlegScopeGuard.platform(user, accountId),
|
||||
loadingPlanId, user.userId(), request));
|
||||
}
|
||||
|
||||
@PostMapping("/{feeId}/approve")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')")
|
||||
@BusinessAudit(event = "CONTAINER_FEE_DETAIL_APPROVE", resourceType = "CONTAINER_FEE_DETAIL", resourceId = "#feeId")
|
||||
public ApiResponse<Void> approve(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long accountId,
|
||||
@PathVariable long loadingPlanId,
|
||||
@PathVariable long feeId) {
|
||||
service.approve(FirstlegScopeGuard.platform(user, accountId), loadingPlanId, feeId,
|
||||
user.userId(), user.username());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/{feeId}/reject")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')")
|
||||
@BusinessAudit(event = "CONTAINER_FEE_DETAIL_REJECT", resourceType = "CONTAINER_FEE_DETAIL", resourceId = "#feeId")
|
||||
public ApiResponse<Void> reject(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long accountId,
|
||||
@PathVariable long loadingPlanId,
|
||||
@PathVariable long feeId,
|
||||
@Valid @RequestBody RejectRequest request) {
|
||||
service.reject(FirstlegScopeGuard.platform(user, accountId), loadingPlanId, feeId,
|
||||
user.userId(), user.username(), request.reason());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/{feeId}/adjust")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:edit')")
|
||||
@BusinessAudit(event = "CONTAINER_FEE_DETAIL_ADJUST", resourceType = "CONTAINER_FEE_DETAIL", resourceId = "#feeId")
|
||||
public ApiResponse<ContainerFeeDetailEntity> adjust(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long accountId,
|
||||
@PathVariable long loadingPlanId, @PathVariable long feeId,
|
||||
@Valid @RequestBody AdjustRequest request) {
|
||||
return ApiResponse.success(service.adjust(FirstlegScopeGuard.platform(user, accountId),
|
||||
loadingPlanId, feeId, request.expectedSourceVersion(), user.userId(), request.values()));
|
||||
}
|
||||
|
||||
@PostMapping("/{feeId}/void")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')")
|
||||
@BusinessAudit(event = "CONTAINER_FEE_DETAIL_VOID", resourceType = "CONTAINER_FEE_DETAIL", resourceId = "#feeId")
|
||||
public ApiResponse<Void> voidApproved(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long accountId,
|
||||
@PathVariable long loadingPlanId, @PathVariable long feeId,
|
||||
@Valid @RequestBody VersionRequest request) {
|
||||
service.voidApproved(FirstlegScopeGuard.platform(user, accountId), loadingPlanId, feeId,
|
||||
request.expectedSourceVersion(), user.userId());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
}
|
||||
+29
@@ -5,6 +5,7 @@ import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.InboundFeeDetailDtos.RejectRequest;
|
||||
import com.baiyue.firstleg.dto.InboundFeeDetailDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.dto.InboundFeeDetailDtos.VersionRequest;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.InboundFeeDetailService;
|
||||
@@ -104,4 +105,32 @@ public class PlatformInboundFeeDetailController {
|
||||
user.userId(), user.username(), request.reason());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/{detailId}/adjust")
|
||||
@BusinessAudit(event = "INBOUND_FEE_DETAIL_ADJUST", resourceType = "INBOUND_FEE_DETAIL", resourceId = "#detailId", description = "调整已审核入库费用明细")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:edit')")
|
||||
public ApiResponse<InboundFeeDetailEntity> adjust(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long accountId,
|
||||
@PathVariable long inboundId,
|
||||
@PathVariable long detailId,
|
||||
@RequestParam int expectedSourceVersion,
|
||||
@Valid @RequestBody SaveRequest request) {
|
||||
return ApiResponse.success(service.adjust(FirstlegScopeGuard.platform(user, accountId),
|
||||
inboundId, detailId, expectedSourceVersion, user.userId(), request));
|
||||
}
|
||||
|
||||
@PostMapping("/{detailId}/void")
|
||||
@BusinessAudit(event = "INBOUND_FEE_DETAIL_VOID", resourceType = "INBOUND_FEE_DETAIL", resourceId = "#detailId", description = "作废已审核入库费用明细")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')")
|
||||
public ApiResponse<Void> voidApproved(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long accountId,
|
||||
@PathVariable long inboundId,
|
||||
@PathVariable long detailId,
|
||||
@Valid @RequestBody VersionRequest request) {
|
||||
service.voidApproved(FirstlegScopeGuard.platform(user, accountId), inboundId, detailId,
|
||||
request.expectedSourceVersion(), user.userId());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
public final class ContainerFeeDetailDtos {
|
||||
private ContainerFeeDetailDtos() {
|
||||
}
|
||||
|
||||
public record SaveRequest(
|
||||
@NotNull @Min(1) Long feeItemId,
|
||||
@NotBlank @Pattern(regexp = "RECEIVABLE|PAYABLE") String direction,
|
||||
@NotNull LocalDate businessDate,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal amount,
|
||||
@NotBlank @Pattern(regexp = "CNY") String currency,
|
||||
@NotNull Boolean settlementIncluded,
|
||||
@Size(max = 500) String remark
|
||||
) {
|
||||
}
|
||||
|
||||
public record AdjustRequest(
|
||||
@NotNull @Min(1) Integer expectedSourceVersion,
|
||||
@NotNull @Min(1) Long feeItemId,
|
||||
@NotBlank @Pattern(regexp = "RECEIVABLE|PAYABLE") String direction,
|
||||
@NotNull LocalDate businessDate,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal amount,
|
||||
@NotBlank @Pattern(regexp = "CNY") String currency,
|
||||
@NotNull Boolean settlementIncluded,
|
||||
@Size(max = 500) String remark
|
||||
) {
|
||||
public SaveRequest values() {
|
||||
return new SaveRequest(feeItemId, direction, businessDate, amount, currency,
|
||||
settlementIncluded, remark);
|
||||
}
|
||||
}
|
||||
|
||||
public record VersionRequest(@NotNull @Min(1) Integer expectedSourceVersion) {
|
||||
}
|
||||
|
||||
public record RejectRequest(@NotBlank @Size(max = 500) String reason) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** Firstleg 向 Finance 发布的短事件信封;完整金额必须回查来源快照接口。 */
|
||||
public record FinanceFeeSourceEvent(
|
||||
String eventId,
|
||||
String eventType,
|
||||
int eventVersion,
|
||||
String producer,
|
||||
LocalDateTime occurredAt,
|
||||
String tenantId,
|
||||
String aggregateType,
|
||||
String aggregateId,
|
||||
int sequence,
|
||||
String idempotencyKey,
|
||||
String payloadHash,
|
||||
Payload payload
|
||||
) {
|
||||
public record Payload(String inboundId, String containerId, String feeId, int feeVersion) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** Finance 回查的 Firstleg 费用来源只读快照。 */
|
||||
public record FinanceFeeSourceSnapshot(
|
||||
long tenantId,
|
||||
String sourceType,
|
||||
long feeId,
|
||||
int feeVersion,
|
||||
Long inboundId,
|
||||
String inboundCode,
|
||||
Long orderId,
|
||||
Long containerId,
|
||||
Long containerBookingId,
|
||||
String containerNo,
|
||||
long customerId,
|
||||
String customerName,
|
||||
Long feeItemId,
|
||||
String feeItemCode,
|
||||
String feeItemName,
|
||||
String direction,
|
||||
LocalDate businessDate,
|
||||
String currency,
|
||||
BigDecimal totalAmount,
|
||||
String reviewStatus,
|
||||
String publicationStatus,
|
||||
boolean settlementIncluded,
|
||||
LocalDateTime publishedAt,
|
||||
BigDecimal chargeableQuantity,
|
||||
String chargeUnit,
|
||||
BigDecimal actualGrossWeightKg,
|
||||
BigDecimal actualVolumeCbm,
|
||||
String sourceHash,
|
||||
int snapshotVersion
|
||||
) {
|
||||
}
|
||||
@@ -21,8 +21,17 @@ public final class InboundFeeDetailDtos {
|
||||
@NotNull @DecimalMin("0.00") BigDecimal unitPrice,
|
||||
@NotBlank @Pattern(regexp = "[A-Z]{3}") String currency,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal amount,
|
||||
@NotNull Boolean settlementIncluded,
|
||||
@Size(max = 500) String remark
|
||||
) {
|
||||
public SaveRequest(Long feeItemId, String direction, LocalDate businessDate,
|
||||
BigDecimal unitPrice, String currency, BigDecimal amount,
|
||||
String remark) {
|
||||
this(feeItemId, direction, businessDate, unitPrice, currency, amount, false, remark);
|
||||
}
|
||||
}
|
||||
|
||||
public record VersionRequest(@NotNull @jakarta.validation.constraints.Min(1) Integer expectedSourceVersion) {
|
||||
}
|
||||
|
||||
public record RejectRequest(@NotBlank @Size(max = 500) String reason) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 柜次级人工应收/应付费用来源。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_container_fee_detail")
|
||||
public class ContainerFeeDetailEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long loadingPlanId;
|
||||
private Long containerBookingId;
|
||||
private String containerNo;
|
||||
private Long feeItemId;
|
||||
private String feeItemCodeSnapshot;
|
||||
private String feeItemNameSnapshot;
|
||||
private String direction;
|
||||
private LocalDate businessDate;
|
||||
private BigDecimal amount;
|
||||
private String currency;
|
||||
private Boolean settlementIncluded;
|
||||
private String reviewStatus;
|
||||
private String sourceStatus;
|
||||
private Integer sourceVersion;
|
||||
private String remark;
|
||||
private Long reviewerId;
|
||||
private String reviewerName;
|
||||
private LocalDateTime reviewedAt;
|
||||
private String reviewReason;
|
||||
private Long createdBy;
|
||||
private LocalDateTime createdAt;
|
||||
private Long updatedBy;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -19,8 +19,17 @@ public class InboundOutboxEntity {
|
||||
private String aggregateType;
|
||||
private Long aggregateId;
|
||||
private String eventType;
|
||||
private String eventKey;
|
||||
private Integer eventVersion;
|
||||
private Integer aggregateSequence;
|
||||
private String payloadHash;
|
||||
private String payloadJson;
|
||||
private String publishStatus;
|
||||
private Integer retryCount;
|
||||
private LocalDateTime nextRetryAt;
|
||||
private LocalDateTime publishedAt;
|
||||
private LocalDateTime lockedAt;
|
||||
private String lockedBy;
|
||||
private String lastError;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContainerFeeDetailMapper extends BaseMapper<ContainerFeeDetailEntity> {
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerFeeDetailMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class ContainerFeeDetailRepository {
|
||||
private final ContainerFeeDetailMapper mapper;
|
||||
|
||||
public List<ContainerFeeDetailEntity> list(long tenantId, long loadingPlanId) {
|
||||
return mapper.selectList(new LambdaQueryWrapper<ContainerFeeDetailEntity>()
|
||||
.eq(ContainerFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId)
|
||||
.orderByDesc(ContainerFeeDetailEntity::getBusinessDate)
|
||||
.orderByDesc(ContainerFeeDetailEntity::getId));
|
||||
}
|
||||
|
||||
public ContainerFeeDetailEntity find(long tenantId, long loadingPlanId, long feeId) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<ContainerFeeDetailEntity>()
|
||||
.eq(ContainerFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId)
|
||||
.eq(ContainerFeeDetailEntity::getId, feeId));
|
||||
}
|
||||
|
||||
public ContainerFeeDetailEntity find(long loadingPlanId, long feeId) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<ContainerFeeDetailEntity>()
|
||||
.eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId)
|
||||
.eq(ContainerFeeDetailEntity::getId, feeId));
|
||||
}
|
||||
|
||||
public void insert(ContainerFeeDetailEntity value) {
|
||||
mapper.insert(value);
|
||||
}
|
||||
|
||||
public int review(long tenantId, long loadingPlanId, long feeId, String status,
|
||||
long reviewerId, String reviewerName, String reason) {
|
||||
return mapper.update(null, new LambdaUpdateWrapper<ContainerFeeDetailEntity>()
|
||||
.eq(ContainerFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId)
|
||||
.eq(ContainerFeeDetailEntity::getId, feeId)
|
||||
.eq(ContainerFeeDetailEntity::getReviewStatus, "PENDING")
|
||||
.eq(ContainerFeeDetailEntity::getSourceStatus, "ACTIVE")
|
||||
.set(ContainerFeeDetailEntity::getReviewStatus, status)
|
||||
.set(ContainerFeeDetailEntity::getReviewerId, reviewerId)
|
||||
.set(ContainerFeeDetailEntity::getReviewerName, reviewerName)
|
||||
.set(ContainerFeeDetailEntity::getReviewedAt, LocalDateTime.now())
|
||||
.set(ContainerFeeDetailEntity::getReviewReason, reason)
|
||||
.set(ContainerFeeDetailEntity::getUpdatedBy, reviewerId));
|
||||
}
|
||||
|
||||
public int adjust(ContainerFeeDetailEntity value, int expectedVersion) {
|
||||
return mapper.update(value, new LambdaUpdateWrapper<ContainerFeeDetailEntity>()
|
||||
.eq(ContainerFeeDetailEntity::getTenantId, value.getTenantId())
|
||||
.eq(ContainerFeeDetailEntity::getLoadingPlanId, value.getLoadingPlanId())
|
||||
.eq(ContainerFeeDetailEntity::getId, value.getId())
|
||||
.eq(ContainerFeeDetailEntity::getReviewStatus, "APPROVED")
|
||||
.eq(ContainerFeeDetailEntity::getSourceStatus, "ACTIVE")
|
||||
.eq(ContainerFeeDetailEntity::getSourceVersion, expectedVersion));
|
||||
}
|
||||
|
||||
public int voidApproved(long tenantId, long loadingPlanId, long feeId,
|
||||
int expectedVersion, long operatorId) {
|
||||
return mapper.update(null, new LambdaUpdateWrapper<ContainerFeeDetailEntity>()
|
||||
.eq(ContainerFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId)
|
||||
.eq(ContainerFeeDetailEntity::getId, feeId)
|
||||
.eq(ContainerFeeDetailEntity::getReviewStatus, "APPROVED")
|
||||
.eq(ContainerFeeDetailEntity::getSourceStatus, "ACTIVE")
|
||||
.eq(ContainerFeeDetailEntity::getSourceVersion, expectedVersion)
|
||||
.set(ContainerFeeDetailEntity::getSourceStatus, "VOIDED")
|
||||
.set(ContainerFeeDetailEntity::getUpdatedBy, operatorId));
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,18 @@ public class FeeItemRepository {
|
||||
.eq(InboundFeeDetailEntity::getId, detailId));
|
||||
}
|
||||
|
||||
public InboundFeeDetailEntity findDetail(long inboundId, long detailId) {
|
||||
return feeDetails.selectOne(new LambdaQueryWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getInboundId, inboundId)
|
||||
.eq(InboundFeeDetailEntity::getId, detailId));
|
||||
}
|
||||
|
||||
public InboundFeeDetailEntity findBySourceFee(String sourceType, long sourceFeeId) {
|
||||
return feeDetails.selectOne(new LambdaQueryWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getSourceType, sourceType)
|
||||
.eq(InboundFeeDetailEntity::getSourceFeeId, sourceFeeId));
|
||||
}
|
||||
|
||||
public InboundFeeDetailEntity findBySourceFee(long tenantId, String sourceType, long sourceFeeId) {
|
||||
return feeDetails.selectOne(new LambdaQueryWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
|
||||
@@ -171,6 +183,33 @@ public class FeeItemRepository {
|
||||
.set(InboundFeeDetailEntity::getReviewerName, reviewerName)
|
||||
.set(InboundFeeDetailEntity::getReviewedAt, LocalDateTime.now())
|
||||
.set(InboundFeeDetailEntity::getReviewReason, reason)
|
||||
.set("APPROVED".equals(targetStatus), InboundFeeDetailEntity::getPublicationStatus,
|
||||
"PUBLISHED")
|
||||
.set("APPROVED".equals(targetStatus), InboundFeeDetailEntity::getPublishedBy, reviewerId)
|
||||
.set("APPROVED".equals(targetStatus), InboundFeeDetailEntity::getPublishedByName, reviewerName)
|
||||
.set("APPROVED".equals(targetStatus), InboundFeeDetailEntity::getPublishedAt, LocalDateTime.now())
|
||||
.set(InboundFeeDetailEntity::getUpdatedBy, reviewerId));
|
||||
}
|
||||
|
||||
public int adjustApproved(InboundFeeDetailEntity detail, int expectedSourceVersion) {
|
||||
return feeDetails.update(detail, new LambdaUpdateWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, detail.getTenantId())
|
||||
.eq(InboundFeeDetailEntity::getInboundId, detail.getInboundId())
|
||||
.eq(InboundFeeDetailEntity::getId, detail.getId())
|
||||
.eq(InboundFeeDetailEntity::getReviewStatus, "APPROVED")
|
||||
.eq(InboundFeeDetailEntity::getSourceFeeVersion, expectedSourceVersion));
|
||||
}
|
||||
|
||||
public int voidApproved(long tenantId, long inboundId, long detailId,
|
||||
int expectedSourceVersion, long operatorId) {
|
||||
return feeDetails.update(null, new LambdaUpdateWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeDetailEntity::getInboundId, inboundId)
|
||||
.eq(InboundFeeDetailEntity::getId, detailId)
|
||||
.eq(InboundFeeDetailEntity::getReviewStatus, "APPROVED")
|
||||
.eq(InboundFeeDetailEntity::getSourceFeeVersion, expectedSourceVersion)
|
||||
.ne(InboundFeeDetailEntity::getPublicationStatus, "VOIDED")
|
||||
.set(InboundFeeDetailEntity::getPublicationStatus, "VOIDED")
|
||||
.set(InboundFeeDetailEntity::getUpdatedBy, operatorId));
|
||||
}
|
||||
}
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class FinanceFeeSourceOutboxRepository {
|
||||
private static final List<String> EVENT_TYPES = List.of(
|
||||
"FIRSTLEG_FEE_PUBLISHED", "FIRSTLEG_FEE_ADJUSTED", "FIRSTLEG_FEE_VOIDED");
|
||||
private final InboundOutboxMapper mapper;
|
||||
|
||||
public List<InboundOutboxEntity> pending(int limit, LocalDateTime now) {
|
||||
return mapper.selectList(new LambdaQueryWrapper<InboundOutboxEntity>()
|
||||
.in(InboundOutboxEntity::getEventType, EVENT_TYPES)
|
||||
.eq(InboundOutboxEntity::getPublishStatus, "PENDING")
|
||||
.lt(InboundOutboxEntity::getRetryCount, 12)
|
||||
.and(value -> value.isNull(InboundOutboxEntity::getNextRetryAt)
|
||||
.or().le(InboundOutboxEntity::getNextRetryAt, now))
|
||||
.orderByAsc(InboundOutboxEntity::getCreatedAt)
|
||||
.orderByAsc(InboundOutboxEntity::getId)
|
||||
.last("LIMIT " + Math.min(Math.max(limit, 1), 100)));
|
||||
}
|
||||
|
||||
public boolean claim(long id, String workerId, LocalDateTime now) {
|
||||
return mapper.update(null, new LambdaUpdateWrapper<InboundOutboxEntity>()
|
||||
.eq(InboundOutboxEntity::getId, id)
|
||||
.eq(InboundOutboxEntity::getPublishStatus, "PENDING")
|
||||
.set(InboundOutboxEntity::getPublishStatus, "PROCESSING")
|
||||
.set(InboundOutboxEntity::getLockedAt, now)
|
||||
.set(InboundOutboxEntity::getLockedBy, workerId)) == 1;
|
||||
}
|
||||
|
||||
public InboundOutboxEntity findClaimed(long id, String workerId) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<InboundOutboxEntity>()
|
||||
.eq(InboundOutboxEntity::getId, id)
|
||||
.eq(InboundOutboxEntity::getPublishStatus, "PROCESSING")
|
||||
.eq(InboundOutboxEntity::getLockedBy, workerId));
|
||||
}
|
||||
|
||||
public boolean published(long id, String workerId, LocalDateTime now) {
|
||||
return mapper.update(null, claimed(id, workerId)
|
||||
.set(InboundOutboxEntity::getPublishStatus, "PUBLISHED")
|
||||
.set(InboundOutboxEntity::getPublishedAt, now)
|
||||
.set(InboundOutboxEntity::getNextRetryAt, null)
|
||||
.set(InboundOutboxEntity::getLockedAt, null)
|
||||
.set(InboundOutboxEntity::getLockedBy, null)
|
||||
.set(InboundOutboxEntity::getLastError, null)) == 1;
|
||||
}
|
||||
|
||||
public boolean retry(long id, String workerId, LocalDateTime nextRetryAt,
|
||||
String error, boolean exhausted) {
|
||||
return mapper.update(null, claimed(id, workerId)
|
||||
.setSql("retry_count = retry_count + 1")
|
||||
.set(InboundOutboxEntity::getPublishStatus, exhausted ? "FAILED" : "PENDING")
|
||||
.set(InboundOutboxEntity::getNextRetryAt, exhausted ? null : nextRetryAt)
|
||||
.set(InboundOutboxEntity::getLockedAt, null)
|
||||
.set(InboundOutboxEntity::getLockedBy, null)
|
||||
.set(InboundOutboxEntity::getLastError, truncate(error))) == 1;
|
||||
}
|
||||
|
||||
public int recoverStale(LocalDateTime staleBefore, LocalDateTime retryAt, int maxRetries) {
|
||||
int failed = mapper.update(null, new LambdaUpdateWrapper<InboundOutboxEntity>()
|
||||
.in(InboundOutboxEntity::getEventType, EVENT_TYPES)
|
||||
.eq(InboundOutboxEntity::getPublishStatus, "PROCESSING")
|
||||
.le(InboundOutboxEntity::getLockedAt, staleBefore)
|
||||
.ge(InboundOutboxEntity::getRetryCount, maxRetries - 1)
|
||||
.setSql("retry_count = retry_count + 1")
|
||||
.set(InboundOutboxEntity::getPublishStatus, "FAILED")
|
||||
.set(InboundOutboxEntity::getNextRetryAt, null)
|
||||
.set(InboundOutboxEntity::getLockedAt, null)
|
||||
.set(InboundOutboxEntity::getLockedBy, null)
|
||||
.set(InboundOutboxEntity::getLastError, "relay lease expired"));
|
||||
int pending = mapper.update(null, new LambdaUpdateWrapper<InboundOutboxEntity>()
|
||||
.in(InboundOutboxEntity::getEventType, EVENT_TYPES)
|
||||
.eq(InboundOutboxEntity::getPublishStatus, "PROCESSING")
|
||||
.le(InboundOutboxEntity::getLockedAt, staleBefore)
|
||||
.lt(InboundOutboxEntity::getRetryCount, maxRetries - 1)
|
||||
.setSql("retry_count = retry_count + 1")
|
||||
.set(InboundOutboxEntity::getPublishStatus, "PENDING")
|
||||
.set(InboundOutboxEntity::getNextRetryAt, retryAt)
|
||||
.set(InboundOutboxEntity::getLockedAt, null)
|
||||
.set(InboundOutboxEntity::getLockedBy, null)
|
||||
.set(InboundOutboxEntity::getLastError, "relay lease expired"));
|
||||
return failed + pending;
|
||||
}
|
||||
|
||||
private static LambdaUpdateWrapper<InboundOutboxEntity> claimed(long id, String workerId) {
|
||||
return new LambdaUpdateWrapper<InboundOutboxEntity>()
|
||||
.eq(InboundOutboxEntity::getId, id)
|
||||
.eq(InboundOutboxEntity::getPublishStatus, "PROCESSING")
|
||||
.eq(InboundOutboxEntity::getLockedBy, workerId);
|
||||
}
|
||||
|
||||
private static String truncate(String value) {
|
||||
if (value == null || value.isBlank()) return "unknown relay error";
|
||||
String sanitized = value.replaceAll("[\\r\\n\\t]+", " ").trim();
|
||||
return sanitized.length() <= 500 ? sanitized : sanitized.substring(0, 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ContainerFeeDetailService {
|
||||
private final ContainerFeeDetailRepository repository;
|
||||
private final SmartLoadingRepository loading;
|
||||
private final FeeItemRepository feeItems;
|
||||
private final FinanceFeeSourceOutboxService financeOutbox;
|
||||
|
||||
public List<ContainerFeeDetailEntity> list(long tenantId, long loadingPlanId) {
|
||||
requireOwnedPlan(tenantId, loadingPlanId);
|
||||
return repository.list(tenantId, loadingPlanId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerFeeDetailEntity create(long tenantId, long loadingPlanId, long operatorId,
|
||||
SaveRequest request) {
|
||||
PlanContext plan = requireOwnedPlan(tenantId, loadingPlanId);
|
||||
FeeItemEntity item = requireItem(request.feeItemId(), request.direction());
|
||||
ContainerFeeDetailEntity value = new ContainerFeeDetailEntity();
|
||||
value.setTenantId(tenantId);
|
||||
value.setLoadingPlanId(loadingPlanId);
|
||||
value.setContainerBookingId(plan.plan().getContainerBookingId());
|
||||
value.setContainerNo(plan.booking() == null ? null : plan.booking().getContainerNo());
|
||||
value.setSourceVersion(1);
|
||||
value.setSourceStatus("ACTIVE");
|
||||
value.setReviewStatus("PENDING");
|
||||
value.setCreatedBy(operatorId);
|
||||
value.setCreatedAt(LocalDateTime.now());
|
||||
apply(value, item, request, operatorId);
|
||||
repository.insert(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void approve(long tenantId, long loadingPlanId, long feeId,
|
||||
long reviewerId, String reviewerName) {
|
||||
required(tenantId, loadingPlanId, feeId);
|
||||
if (repository.review(tenantId, loadingPlanId, feeId, "APPROVED",
|
||||
reviewerId, reviewerName, null) != 1) {
|
||||
throw conflict("只有待审核柜次费用可以审核通过");
|
||||
}
|
||||
ContainerFeeDetailEntity approved = required(tenantId, loadingPlanId, feeId);
|
||||
if (Boolean.TRUE.equals(approved.getSettlementIncluded())) {
|
||||
String eventType = approved.getSourceVersion() > 1
|
||||
? FinanceFeeSourceOutboxService.ADJUSTED : FinanceFeeSourceOutboxService.PUBLISHED;
|
||||
financeOutbox.publishContainer(eventType, tenantId, loadingPlanId, feeId,
|
||||
approved.getSourceVersion(), LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void reject(long tenantId, long loadingPlanId, long feeId, long reviewerId,
|
||||
String reviewerName, String reason) {
|
||||
required(tenantId, loadingPlanId, feeId);
|
||||
if (repository.review(tenantId, loadingPlanId, feeId, "REJECTED", reviewerId,
|
||||
reviewerName, reason.trim()) != 1) {
|
||||
throw conflict("只有待审核柜次费用可以驳回");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerFeeDetailEntity adjust(long tenantId, long loadingPlanId, long feeId,
|
||||
int expectedVersion, long operatorId,
|
||||
SaveRequest request) {
|
||||
requireOwnedPlan(tenantId, loadingPlanId);
|
||||
ContainerFeeDetailEntity value = required(tenantId, loadingPlanId, feeId);
|
||||
FeeItemEntity item = requireItem(request.feeItemId(), request.direction());
|
||||
apply(value, item, request, operatorId);
|
||||
value.setSourceVersion(expectedVersion + 1);
|
||||
value.setReviewStatus("PENDING");
|
||||
value.setReviewerId(null);
|
||||
value.setReviewerName(null);
|
||||
value.setReviewedAt(null);
|
||||
value.setReviewReason(null);
|
||||
if (repository.adjust(value, expectedVersion) != 1) {
|
||||
throw conflict("只有当前版本的已审核柜次费用可以调整");
|
||||
}
|
||||
return required(tenantId, loadingPlanId, feeId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void voidApproved(long tenantId, long loadingPlanId, long feeId,
|
||||
int expectedVersion, long operatorId) {
|
||||
ContainerFeeDetailEntity current = required(tenantId, loadingPlanId, feeId);
|
||||
if (repository.voidApproved(tenantId, loadingPlanId, feeId, expectedVersion, operatorId) != 1) {
|
||||
throw conflict("只有当前版本的已审核柜次费用可以作废");
|
||||
}
|
||||
if (Boolean.TRUE.equals(current.getSettlementIncluded())) {
|
||||
financeOutbox.publishContainer(FinanceFeeSourceOutboxService.VOIDED, tenantId,
|
||||
loadingPlanId, feeId, expectedVersion, LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
private PlanContext requireOwnedPlan(long tenantId, long loadingPlanId) {
|
||||
LoadingPlanEntity plan = loading.plan(loadingPlanId);
|
||||
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次不存在");
|
||||
boolean owned = loading.items(loadingPlanId).stream()
|
||||
.anyMatch(item -> Long.valueOf(tenantId).equals(item.getAccountId()));
|
||||
if (!owned) throw new BizException(ResultCode.NOT_FOUND, "柜次不属于该客户");
|
||||
return new PlanContext(plan, plan.getContainerBookingId() == null
|
||||
? null : loading.booking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
private FeeItemEntity requireItem(long feeItemId, String direction) {
|
||||
FeeItemEntity item = feeItems.findItem(feeItemId);
|
||||
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在");
|
||||
if (!"ACTIVE".equals(item.getStatus())) throw conflict("费用项目已停用");
|
||||
boolean applicable = "RECEIVABLE".equals(direction)
|
||||
? Boolean.TRUE.equals(item.getPickupReceivable()) || Boolean.TRUE.equals(item.getWarehouseReceivable())
|
||||
: Boolean.TRUE.equals(item.getPickupPayable()) || Boolean.TRUE.equals(item.getWarehousePayable());
|
||||
if (!applicable) throw conflict("费用项目未启用对应应收/应付方向");
|
||||
return item;
|
||||
}
|
||||
|
||||
private ContainerFeeDetailEntity required(long tenantId, long loadingPlanId, long feeId) {
|
||||
ContainerFeeDetailEntity value = repository.find(tenantId, loadingPlanId, feeId);
|
||||
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "柜次费用明细不存在");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void apply(ContainerFeeDetailEntity value, FeeItemEntity item,
|
||||
SaveRequest request, long operatorId) {
|
||||
value.setFeeItemId(item.getId());
|
||||
value.setFeeItemCodeSnapshot(item.getFeeCode());
|
||||
value.setFeeItemNameSnapshot(item.getFeeTypeName());
|
||||
value.setDirection(request.direction());
|
||||
value.setBusinessDate(request.businessDate());
|
||||
value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP));
|
||||
value.setCurrency(request.currency());
|
||||
value.setSettlementIncluded(Boolean.TRUE.equals(request.settlementIncluded()));
|
||||
value.setRemark(request.remark() == null ? null : request.remark().trim());
|
||||
value.setUpdatedBy(operatorId);
|
||||
value.setUpdatedAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
|
||||
private record PlanContext(LoadingPlanEntity plan, ContainerBookingEntity booking) {
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import com.baiyue.firstleg.domain.PricingTaxMode;
|
||||
import com.baiyue.firstleg.dto.PriceBookDtos.EstimateRequest;
|
||||
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPricingEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
|
||||
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
|
||||
@@ -44,6 +43,8 @@ public class FbaPricingService {
|
||||
private Clock clock;
|
||||
@Resource
|
||||
private InboundFeeDetailService feeDetails;
|
||||
@Resource
|
||||
private FinanceFeeSourceOutboxService financeOutbox;
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
||||
@@ -126,16 +127,8 @@ public class FbaPricingService {
|
||||
throw conflict("费用未确认或入库单状态已变化,不能发送");
|
||||
}
|
||||
feeDetails.publishFreight(tenantId, inboundId, current.getId(), operatorId, null, now);
|
||||
InboundOutboxEntity event = new InboundOutboxEntity();
|
||||
event.setTenantId(tenantId);
|
||||
event.setAggregateType("INBOUND");
|
||||
event.setAggregateId(inboundId);
|
||||
event.setEventType("FIRSTLEG_FEE_SENT");
|
||||
event.setPayloadJson("{\"inboundId\":\"" + inboundId + "\",\"feeId\":\"" + current.getId() + "\"}");
|
||||
event.setPublishStatus("PENDING");
|
||||
event.setRetryCount(0);
|
||||
event.setCreatedAt(now);
|
||||
repository.insertOutbox(event);
|
||||
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED,
|
||||
tenantId, inboundId, current.getId(), current.getFeeVersion(), now);
|
||||
complete(token, current.getId(), now);
|
||||
return response(tenantId, current.getId(), nextProcessStatus);
|
||||
}
|
||||
@@ -208,17 +201,8 @@ public class FbaPricingService {
|
||||
repository.insertItem(extra);
|
||||
}
|
||||
feeDetails.publishFreight(tenantId, inboundId, adjusted.getId(), operatorId, null, now);
|
||||
InboundOutboxEntity event = new InboundOutboxEntity();
|
||||
event.setTenantId(tenantId);
|
||||
event.setAggregateType("INBOUND");
|
||||
event.setAggregateId(inboundId);
|
||||
event.setEventType("FIRSTLEG_FEE_ADJUSTED");
|
||||
event.setPayloadJson("{\"inboundId\":\"" + inboundId + "\",\"feeId\":\""
|
||||
+ adjusted.getId() + "\",\"feeVersion\":" + nextVersion + "}");
|
||||
event.setPublishStatus("PENDING");
|
||||
event.setRetryCount(0);
|
||||
event.setCreatedAt(now);
|
||||
repository.insertOutbox(event);
|
||||
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.ADJUSTED,
|
||||
tenantId, inboundId, adjusted.getId(), nextVersion, now);
|
||||
complete(token, adjusted.getId(), now);
|
||||
return response(tenantId, adjusted.getId(), "INBOUNDED");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.FinanceFeeSourceEventClient;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceEvent;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.repository.FinanceFeeSourceOutboxRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/** 将 Firstleg 费用来源 Outbox 至少一次投递到 Finance Inbox。 */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class FinanceFeeSourceOutboxRelay {
|
||||
private static final int MAX_RETRIES = 12;
|
||||
private static final Set<String> TYPES = Set.of(
|
||||
FinanceFeeSourceOutboxService.PUBLISHED,
|
||||
FinanceFeeSourceOutboxService.ADJUSTED,
|
||||
FinanceFeeSourceOutboxService.VOIDED);
|
||||
|
||||
private final FinanceFeeSourceOutboxRepository repository;
|
||||
private final FinanceFeeSourceEventClient client;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Clock clock;
|
||||
private final String workerId;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
public FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository,
|
||||
FinanceFeeSourceEventClient client,
|
||||
ObjectMapper objectMapper) {
|
||||
this(repository, client, objectMapper, Clock.systemDefaultZone(), UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository,
|
||||
FinanceFeeSourceEventClient client, ObjectMapper objectMapper,
|
||||
Clock clock, String workerId) {
|
||||
this.repository = repository;
|
||||
this.client = client;
|
||||
this.objectMapper = objectMapper;
|
||||
this.clock = clock;
|
||||
this.workerId = workerId;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${baiyue.finance.outbox-relay.fixed-delay-ms:5000}")
|
||||
public void relay() {
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
repository.recoverStale(now.minusMinutes(5), now.plusSeconds(10), MAX_RETRIES);
|
||||
for (InboundOutboxEntity candidate : repository.pending(50, now)) {
|
||||
if (!repository.claim(candidate.getId(), workerId, now)) continue;
|
||||
InboundOutboxEntity event = repository.findClaimed(candidate.getId(), workerId);
|
||||
if (event == null) continue;
|
||||
deliver(event);
|
||||
}
|
||||
}
|
||||
|
||||
private void deliver(InboundOutboxEntity row) {
|
||||
try {
|
||||
FinanceFeeSourceEvent event = parseAndValidate(row);
|
||||
ApiResponse<FinanceFeeSourceEventClient.Acknowledgement> response =
|
||||
client.receive(internalToken, event.idempotencyKey(), event);
|
||||
validateAcknowledgement(event, response);
|
||||
if (!repository.published(row.getId(), workerId, LocalDateTime.now(clock))) {
|
||||
log.warn("Finance fee event acknowledgement lost lease: eventId={}", row.getEventKey());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
int retryCount = row.getRetryCount() == null ? 0 : row.getRetryCount();
|
||||
boolean permanent = permanent(exception);
|
||||
boolean exhausted = permanent || retryCount + 1 >= MAX_RETRIES;
|
||||
String error = safeError(exception);
|
||||
repository.retry(row.getId(), workerId,
|
||||
LocalDateTime.now(clock).plusSeconds(backoffSeconds(retryCount + 1)),
|
||||
error, exhausted);
|
||||
log.warn("Finance fee event relay failed: eventId={}, retry={}, exhausted={}, reason={}",
|
||||
row.getEventKey(), retryCount + 1, exhausted, error);
|
||||
}
|
||||
}
|
||||
|
||||
private FinanceFeeSourceEvent parseAndValidate(InboundOutboxEntity row) throws Exception {
|
||||
FinanceFeeSourceEvent event = objectMapper.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class);
|
||||
if (event == null || event.eventVersion() != 1 || !"firstleg-service".equals(event.producer())
|
||||
|| !TYPES.contains(event.eventType()) || !event.eventType().equals(row.getEventType())
|
||||
|| event.eventId() == null || !event.eventId().equals(row.getEventKey())
|
||||
|| !event.idempotencyKey().equals(row.getEventKey())
|
||||
|| !String.valueOf(row.getTenantId()).equals(event.tenantId())
|
||||
|| !String.valueOf(row.getAggregateId()).equals(event.aggregateId())
|
||||
|| !event.aggregateType().equals(row.getAggregateType())
|
||||
|| event.payload() == null || event.payload().feeVersion() != event.sequence()
|
||||
|| !sha256(objectMapper.writeValueAsString(event.payload())).equals(event.payloadHash())
|
||||
|| !event.payloadHash().equals(row.getPayloadHash())) {
|
||||
throw new InvalidEventException("费用来源事件信封、归属或哈希不一致");
|
||||
}
|
||||
if ("INBOUND".equals(event.aggregateType())
|
||||
&& !event.aggregateId().equals(event.payload().inboundId())) {
|
||||
throw new InvalidEventException("入库费用事件聚合归属不一致");
|
||||
}
|
||||
if ("CONTAINER".equals(event.aggregateType())
|
||||
&& !event.aggregateId().equals(event.payload().containerId())) {
|
||||
throw new InvalidEventException("柜次费用事件聚合归属不一致");
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
private static void validateAcknowledgement(
|
||||
FinanceFeeSourceEvent event,
|
||||
ApiResponse<FinanceFeeSourceEventClient.Acknowledgement> response) {
|
||||
if (response != null && !response.successful()) {
|
||||
String code = response.code();
|
||||
boolean permanent = code != null && !Set.of("FIN-EVT-003", "FIN-EVT-005", "FIN-EVT-006").contains(code);
|
||||
throw new FinanceRejectedException(code == null ? "FIN-EVT-UNKNOWN" : code,
|
||||
response.message(), permanent);
|
||||
}
|
||||
if (response == null || response.data() == null
|
||||
|| response.data().feeSourceId() == null || response.data().feeSourceId() <= 0) {
|
||||
throw new IllegalStateException("Finance未确认费用来源事件");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean permanent(Exception exception) {
|
||||
if (exception instanceof InvalidEventException) return true;
|
||||
if (exception instanceof FinanceRejectedException rejected) return rejected.permanent;
|
||||
if (exception instanceof FeignException feign) {
|
||||
int status = feign.status();
|
||||
return status >= 400 && status < 500 && status != 408 && status != 429;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static long backoffSeconds(int retry) {
|
||||
return switch (Math.min(Math.max(retry, 1), 5)) {
|
||||
case 1 -> 10;
|
||||
case 2 -> 30;
|
||||
case 3 -> 120;
|
||||
case 4 -> 600;
|
||||
default -> 1800;
|
||||
};
|
||||
}
|
||||
|
||||
private String safeError(Exception exception) {
|
||||
String value = exception.getMessage();
|
||||
if (value == null || value.isBlank()) value = exception.getClass().getSimpleName();
|
||||
if (internalToken != null && !internalToken.isBlank()) value = value.replace(internalToken, "***");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("SHA-256 unavailable", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InvalidEventException extends RuntimeException {
|
||||
private InvalidEventException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FinanceRejectedException extends RuntimeException {
|
||||
private final boolean permanent;
|
||||
|
||||
private FinanceRejectedException(String code, String message, boolean permanent) {
|
||||
super(code + ": " + (message == null ? "Finance拒绝费用来源事件" : message));
|
||||
this.permanent = permanent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceEvent;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HexFormat;
|
||||
|
||||
/** 在业务事务内记录 Finance 费用来源事件,eventKey 保证重复动作不会重复入箱。 */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FinanceFeeSourceOutboxService {
|
||||
public static final String PUBLISHED = "FIRSTLEG_FEE_PUBLISHED";
|
||||
public static final String ADJUSTED = "FIRSTLEG_FEE_ADJUSTED";
|
||||
public static final String VOIDED = "FIRSTLEG_FEE_VOIDED";
|
||||
|
||||
private final InboundOutboxMapper outbox;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public void publishInbound(String eventType, long tenantId, long inboundId,
|
||||
long feeId, int feeVersion, LocalDateTime occurredAt) {
|
||||
FinanceFeeSourceEvent.Payload payload = new FinanceFeeSourceEvent.Payload(
|
||||
String.valueOf(inboundId), null, String.valueOf(feeId), feeVersion);
|
||||
publish(event(eventType, tenantId, "INBOUND", inboundId, feeId, feeVersion,
|
||||
payload, occurredAt));
|
||||
}
|
||||
|
||||
public void publishContainer(String eventType, long tenantId, long containerId,
|
||||
long feeId, int feeVersion, LocalDateTime occurredAt) {
|
||||
FinanceFeeSourceEvent.Payload payload = new FinanceFeeSourceEvent.Payload(
|
||||
null, String.valueOf(containerId), String.valueOf(feeId), feeVersion);
|
||||
publish(event(eventType, tenantId, "CONTAINER", containerId, feeId, feeVersion,
|
||||
payload, occurredAt));
|
||||
}
|
||||
|
||||
private void publish(FinanceFeeSourceEvent event) {
|
||||
InboundOutboxEntity value = new InboundOutboxEntity();
|
||||
value.setTenantId(Long.parseLong(event.tenantId()));
|
||||
value.setAggregateType(event.aggregateType());
|
||||
value.setAggregateId(Long.parseLong(event.aggregateId()));
|
||||
value.setEventType(event.eventType());
|
||||
value.setEventKey(event.eventId());
|
||||
value.setEventVersion(event.eventVersion());
|
||||
value.setAggregateSequence(event.sequence());
|
||||
value.setPayloadHash(event.payloadHash());
|
||||
value.setPayloadJson(json(event));
|
||||
value.setPublishStatus("PENDING");
|
||||
value.setRetryCount(0);
|
||||
value.setCreatedAt(event.occurredAt());
|
||||
try {
|
||||
outbox.insert(value);
|
||||
} catch (DuplicateKeyException ignored) {
|
||||
// 同一来源版本和状态的事件已经进入 Outbox,调用方可安全重试。
|
||||
}
|
||||
}
|
||||
|
||||
private FinanceFeeSourceEvent event(String eventType, long tenantId, String aggregateType,
|
||||
long aggregateId, long feeId, int feeVersion,
|
||||
FinanceFeeSourceEvent.Payload payload, LocalDateTime occurredAt) {
|
||||
String idempotencyKey = "FIRSTLEG_FEE:" + aggregateType + ":" + aggregateId
|
||||
+ ":" + feeId + ":" + feeVersion + ":" + eventType;
|
||||
String payloadJson = json(payload);
|
||||
return new FinanceFeeSourceEvent(idempotencyKey, eventType, 1, "firstleg-service",
|
||||
occurredAt == null ? LocalDateTime.now() : occurredAt, String.valueOf(tenantId),
|
||||
aggregateType, String.valueOf(aggregateId), feeVersion, idempotencyKey,
|
||||
sha256(payloadJson), payload);
|
||||
}
|
||||
|
||||
private String json(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "费用来源事件序列化失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("SHA-256 unavailable", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FinanceFeeSourceSnapshotService {
|
||||
private final FeeItemRepository inboundFees;
|
||||
private final ContainerFeeDetailRepository containerFees;
|
||||
|
||||
public FinanceFeeSourceSnapshot inbound(long inboundId, long feeId) {
|
||||
InboundFeeDetailEntity detail = inboundFees.findDetail(inboundId, feeId);
|
||||
if (detail == null) detail = inboundFees.findBySourceFee("FBA_PRICING", feeId);
|
||||
if (detail == null || !Long.valueOf(inboundId).equals(detail.getInboundId())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库费用来源不存在");
|
||||
}
|
||||
InboundOrderEntity inbound = inboundFees.findInbound(detail.getTenantId(), inboundId);
|
||||
FeeItemEntity item = inboundFees.findItem(detail.getFeeItemId());
|
||||
if (inbound == null || item == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库费用来源关联快照不完整");
|
||||
}
|
||||
long sourceFeeId = "FBA_PRICING".equals(detail.getSourceType())
|
||||
? detail.getSourceFeeId() : detail.getId();
|
||||
int version = detail.getSourceFeeVersion() == null ? 1 : detail.getSourceFeeVersion();
|
||||
String canonical = String.join("|", "INBOUND_FEE", String.valueOf(sourceFeeId),
|
||||
String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(inboundId),
|
||||
String.valueOf(inbound.getOrderId()), nullSafe(item.getFeeCode()),
|
||||
nullSafe(detail.getDirection()), detail.getAmount().toPlainString(),
|
||||
nullSafe(detail.getCurrency()), String.valueOf(detail.getSettlementIncluded()));
|
||||
return new FinanceFeeSourceSnapshot(detail.getTenantId(), "INBOUND_FEE", sourceFeeId,
|
||||
version, inboundId, inbound.getInboundCode(), inbound.getOrderId(), null, null, null,
|
||||
detail.getTenantId(), inbound.getCustomerName(), item.getId(), item.getFeeCode(),
|
||||
detail.getFeeTypeNameSnapshot(), detail.getDirection(), detail.getBusinessDate(),
|
||||
detail.getCurrency(), detail.getAmount(), detail.getReviewStatus(),
|
||||
detail.getPublicationStatus(), Boolean.TRUE.equals(detail.getSettlementIncluded()),
|
||||
detail.getPublishedAt(), detail.getChargeableQuantity(), detail.getChargeUnit(),
|
||||
detail.getActualGrossWeightKg(), detail.getActualVolumeCbm(), sha256(canonical), 1);
|
||||
}
|
||||
|
||||
public FinanceFeeSourceSnapshot container(long containerId, long feeId) {
|
||||
ContainerFeeDetailEntity detail = containerFees.find(containerId, feeId);
|
||||
if (detail == null) throw new BizException(ResultCode.NOT_FOUND, "柜次费用来源不存在");
|
||||
int version = detail.getSourceVersion();
|
||||
String publicationStatus = "VOIDED".equals(detail.getSourceStatus()) ? "VOIDED"
|
||||
: "APPROVED".equals(detail.getReviewStatus()) ? "PUBLISHED" : "DRAFT";
|
||||
String canonical = String.join("|", "CONTAINER_FEE", String.valueOf(feeId),
|
||||
String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(containerId),
|
||||
nullSafe(detail.getFeeItemCodeSnapshot()), nullSafe(detail.getDirection()),
|
||||
detail.getAmount().toPlainString(), nullSafe(detail.getCurrency()),
|
||||
String.valueOf(detail.getSettlementIncluded()));
|
||||
return new FinanceFeeSourceSnapshot(detail.getTenantId(), "CONTAINER_FEE", feeId,
|
||||
version, null, null, null, containerId, detail.getContainerBookingId(),
|
||||
detail.getContainerNo(), detail.getTenantId(), null, detail.getFeeItemId(),
|
||||
detail.getFeeItemCodeSnapshot(), detail.getFeeItemNameSnapshot(), detail.getDirection(),
|
||||
detail.getBusinessDate(), detail.getCurrency(), detail.getAmount(), detail.getReviewStatus(),
|
||||
publicationStatus, Boolean.TRUE.equals(detail.getSettlementIncluded()),
|
||||
detail.getReviewedAt(), null, null, null, null, sha256(canonical), 1);
|
||||
}
|
||||
|
||||
private static String nullSafe(Object value) {
|
||||
return value == null ? "" : String.valueOf(value);
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("SHA-256 unavailable", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-17
@@ -6,7 +6,6 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -29,6 +28,8 @@ public class InboundCompletionOrchestrator {
|
||||
private InboundIdempotencyService idempotency;
|
||||
@Resource
|
||||
private InboundFeeDetailService feeDetails;
|
||||
@Resource
|
||||
private FinanceFeeSourceOutboxService financeOutbox;
|
||||
|
||||
/**
|
||||
* 任务5对接方法一:锁定发送动作。允许前置状态仅为 FEE_CONFIRMING + CONFIRMED。
|
||||
@@ -89,11 +90,12 @@ public class InboundCompletionOrchestrator {
|
||||
if (repository.markFeeSent(tenantId, inboundId, feeId, operatorId) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "费用快照已被并发发送或状态不正确");
|
||||
}
|
||||
feeDetails.publishFreight(tenantId, inboundId, feeId, operatorId, operatorName,
|
||||
LocalDateTime.now());
|
||||
var published = feeDetails.publishFreight(tenantId, inboundId, feeId, operatorId,
|
||||
operatorName, LocalDateTime.now());
|
||||
repository.insertHistory(MeasurementReviewService.history(tenantId, inboundId,
|
||||
"INBOUNDING", "INBOUNDED", "SEND_FEE_COMPLETE", null, operatorId, operatorName));
|
||||
repository.insertOutbox(completedEvent(tenantId, inboundId, feeId));
|
||||
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED, tenantId,
|
||||
inboundId, feeId, published.getSourceFeeVersion(), LocalDateTime.now());
|
||||
idempotency.complete(context, inboundId);
|
||||
}
|
||||
|
||||
@@ -139,19 +141,6 @@ public class InboundCompletionOrchestrator {
|
||||
return order;
|
||||
}
|
||||
|
||||
private static InboundOutboxEntity completedEvent(long tenantId, long inboundId, long feeId) {
|
||||
InboundOutboxEntity event = new InboundOutboxEntity();
|
||||
event.setTenantId(tenantId);
|
||||
event.setAggregateType("INBOUND");
|
||||
event.setAggregateId(inboundId);
|
||||
event.setEventType("FIRSTLEG_INBOUND_FEE_SENT");
|
||||
event.setPayloadJson("{\"inboundId\":\"" + inboundId + "\",\"feeId\":\"" + feeId + "\"}");
|
||||
event.setPublishStatus("PENDING");
|
||||
event.setRetryCount(0);
|
||||
event.setCreatedAt(LocalDateTime.now());
|
||||
return event;
|
||||
}
|
||||
|
||||
private static BizException concurrentConflict() {
|
||||
return new BizException(ResultCode.CONFLICT, "入库单已被并发更新,请刷新后重试");
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ public class InboundFeeDetailService {
|
||||
private FeeItemRepository repository;
|
||||
@Resource
|
||||
private FbaPricingRepository pricingRepository;
|
||||
@Resource
|
||||
private FinanceFeeSourceOutboxService financeOutbox;
|
||||
|
||||
public List<InboundFeeDetailEntity> list(long tenantId, long inboundId) {
|
||||
requiredInbound(tenantId, inboundId);
|
||||
@@ -132,8 +134,8 @@ public class InboundFeeDetailService {
|
||||
apply(value, inbound, item, request, operatorId);
|
||||
value.setReviewStatus("PENDING");
|
||||
value.setSourceType("MANUAL");
|
||||
value.setSourceFeeVersion(1);
|
||||
value.setPublicationStatus("DRAFT");
|
||||
value.setSettlementIncluded(false);
|
||||
repository.insertDetail(value);
|
||||
return value;
|
||||
}
|
||||
@@ -168,6 +170,49 @@ public class InboundFeeDetailService {
|
||||
reviewerId, reviewerName, null) != 1) {
|
||||
throw conflict("只有待审核费用明细可以审核通过");
|
||||
}
|
||||
InboundFeeDetailEntity approved = required(tenantId, inboundId, detailId);
|
||||
publishFinanceSource(approved, "AVAILABLE", LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundFeeDetailEntity adjust(long tenantId, long inboundId, long detailId,
|
||||
int expectedSourceVersion, long operatorId,
|
||||
SaveRequest request) {
|
||||
InboundFeeDetailEntity value = required(tenantId, inboundId, detailId);
|
||||
if (!"APPROVED".equals(value.getReviewStatus()) || "VOIDED".equals(value.getPublicationStatus())) {
|
||||
throw conflict("只有审核通过且未作废的费用明细可以调整");
|
||||
}
|
||||
if (value.getSourceFeeVersion() == null || value.getSourceFeeVersion() != expectedSourceVersion) {
|
||||
throw conflict("费用来源版本已变化,请刷新后重试");
|
||||
}
|
||||
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
|
||||
FeeItemEntity item = requiredActiveItem(request.feeItemId());
|
||||
apply(value, inbound, item, request, operatorId);
|
||||
value.setSourceType("MANUAL");
|
||||
value.setSourceFeeVersion(expectedSourceVersion + 1);
|
||||
value.setReviewStatus("PENDING");
|
||||
value.setReviewerId(null);
|
||||
value.setReviewerName(null);
|
||||
value.setReviewedAt(null);
|
||||
value.setReviewReason(null);
|
||||
value.setPublicationStatus("DRAFT");
|
||||
if (repository.adjustApproved(value, expectedSourceVersion) != 1) {
|
||||
throw conflict("费用明细已被其他人调整,请刷新后重试");
|
||||
}
|
||||
return required(tenantId, inboundId, detailId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void voidApproved(long tenantId, long inboundId, long detailId,
|
||||
int expectedSourceVersion, long operatorId) {
|
||||
InboundFeeDetailEntity current = required(tenantId, inboundId, detailId);
|
||||
if (repository.voidApproved(tenantId, inboundId, detailId, expectedSourceVersion, operatorId) != 1) {
|
||||
throw conflict("只有当前版本的已审核费用明细可以作废");
|
||||
}
|
||||
if (Boolean.TRUE.equals(current.getSettlementIncluded())) {
|
||||
current.setPublicationStatus("VOIDED");
|
||||
publishFinanceSource(current, "VOIDED", LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -282,6 +327,7 @@ public class InboundFeeDetailService {
|
||||
value.setUnitPrice(request.unitPrice().setScale(4, RoundingMode.HALF_UP));
|
||||
value.setCurrency(request.currency());
|
||||
value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP));
|
||||
value.setSettlementIncluded(Boolean.TRUE.equals(request.settlementIncluded()));
|
||||
value.setRemark(request.remark() == null ? null : request.remark().trim());
|
||||
value.setUpdatedBy(operatorId);
|
||||
}
|
||||
@@ -289,4 +335,16 @@ public class InboundFeeDetailService {
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
|
||||
private void publishFinanceSource(InboundFeeDetailEntity detail, String status,
|
||||
LocalDateTime occurredAt) {
|
||||
if (!Boolean.TRUE.equals(detail.getSettlementIncluded())) return;
|
||||
if ("FBA_PRICING".equals(detail.getSourceType())) return;
|
||||
int version = detail.getSourceFeeVersion() == null ? 1 : detail.getSourceFeeVersion();
|
||||
String eventType = "VOIDED".equals(status) ? FinanceFeeSourceOutboxService.VOIDED
|
||||
: version > 1 ? FinanceFeeSourceOutboxService.ADJUSTED
|
||||
: FinanceFeeSourceOutboxService.PUBLISHED;
|
||||
financeOutbox.publishInbound(eventType, detail.getTenantId(), detail.getInboundId(),
|
||||
detail.getId(), version, occurredAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,23 @@ USE baiyue_firstleg;
|
||||
|
||||
-- 可靠审计本地 Outbox;与头程业务表位于同一数据库。
|
||||
CREATE TABLE IF NOT EXISTS sys_audit_outbox (
|
||||
event_id VARCHAR(64) NOT NULL, event_type VARCHAR(64) NOT NULL, resource_type VARCHAR(64) NOT NULL,
|
||||
resource_id VARCHAR(128) NULL, service_name VARCHAR(64) NULL, payload_json JSON NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING', attempt_count INT NOT NULL DEFAULT 0,
|
||||
compensation_count INT NOT NULL DEFAULT 0,
|
||||
next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), lease_owner VARCHAR(128) NULL,
|
||||
lease_until DATETIME(3) NULL, last_error VARCHAR(1000) NULL, dead_at DATETIME(3) NULL,
|
||||
sent_at DATETIME(3) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
event_id VARCHAR(64) NOT NULL COMMENT '审计事件全局唯一ID',
|
||||
event_type VARCHAR(64) NOT NULL COMMENT '审计事件类型',
|
||||
resource_type VARCHAR(64) NOT NULL COMMENT '被审计业务资源类型',
|
||||
resource_id VARCHAR(128) NULL COMMENT '被审计业务资源ID;无法确定时为空',
|
||||
service_name VARCHAR(64) NULL COMMENT '产生审计事件的服务名称',
|
||||
payload_json JSON NOT NULL COMMENT '待发送的审计事件完整负载快照',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发送状态:PENDING待发送、SENDING发送中、SENT已发送、DEAD已转死信',
|
||||
attempt_count INT NOT NULL DEFAULT 0 COMMENT '正常发送累计尝试次数',
|
||||
compensation_count INT NOT NULL DEFAULT 0 COMMENT '人工或自动补偿累计次数',
|
||||
next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '允许下次重试的最早时间',
|
||||
lease_owner VARCHAR(128) NULL COMMENT '当前发送任务租约持有者标识',
|
||||
lease_until DATETIME(3) NULL COMMENT '当前发送任务租约到期时间',
|
||||
last_error VARCHAR(1000) NULL COMMENT '最近一次发送失败原因',
|
||||
dead_at DATETIME(3) NULL COMMENT '转入死信状态的时间',
|
||||
sent_at DATETIME(3) NULL COMMENT '首次成功发送时间',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '事件创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '记录最后更新时间',
|
||||
PRIMARY KEY (event_id), KEY idx_audit_outbox_publish (status, next_retry_at, lease_until, created_at),
|
||||
KEY idx_audit_outbox_retention (status, sent_at)
|
||||
) ENGINE=InnoDB COMMENT='可靠审计事件本地发送箱';
|
||||
|
||||
@@ -4,7 +4,7 @@ USE baiyue_firstleg;
|
||||
-- 每个客户固定四条箱规尺寸配置;用于实测尺寸归档和计费体积加权,不包含价格。
|
||||
CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '配置ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
box_type VARCHAR(24) NOT NULL COMMENT '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE',
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
-- Firstleg -> Finance 费用来源事件和柜次费用模型(MySQL 8,可重复执行)
|
||||
|
||||
DROP PROCEDURE IF EXISTS firstleg_finance_add_column;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE firstleg_finance_add_column(
|
||||
IN target_table VARCHAR(64), IN target_column VARCHAR(64), IN column_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = target_table AND column_name = target_column
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `', target_column, '` ', column_definition);
|
||||
PREPARE statement FROM @ddl;
|
||||
EXECUTE statement;
|
||||
DEALLOCATE PREPARE statement;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_key',
|
||||
"VARCHAR(160) NULL COMMENT '费用来源事件幂等键;历史事件允许为空' AFTER event_type");
|
||||
CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_version',
|
||||
"INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '事件信封版本' AFTER event_key");
|
||||
CALL firstleg_finance_add_column('fl_inbound_outbox', 'aggregate_sequence',
|
||||
"INT UNSIGNED NULL COMMENT '同一费用来源的单调版本序号' AFTER event_version");
|
||||
CALL firstleg_finance_add_column('fl_inbound_outbox', 'payload_hash',
|
||||
"CHAR(64) NULL COMMENT '短payload规范化JSON的SHA-256' AFTER aggregate_sequence");
|
||||
CALL firstleg_finance_add_column('fl_inbound_outbox', 'next_retry_at',
|
||||
"DATETIME(3) NULL COMMENT '下一次允许投递时间' AFTER retry_count");
|
||||
CALL firstleg_finance_add_column('fl_inbound_outbox', 'published_at',
|
||||
"DATETIME(3) NULL COMMENT 'Finance确认接收时间' AFTER next_retry_at");
|
||||
CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_at',
|
||||
"DATETIME(3) NULL COMMENT 'Relay抢占租约时间' AFTER published_at");
|
||||
CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_by',
|
||||
"VARCHAR(64) NULL COMMENT 'Relay抢占实例ID' AFTER locked_at");
|
||||
CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error',
|
||||
"VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by");
|
||||
|
||||
DROP PROCEDURE IF EXISTS firstleg_finance_add_column;
|
||||
|
||||
DROP PROCEDURE IF EXISTS firstleg_finance_add_index;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE firstleg_finance_add_index(
|
||||
IN target_table VARCHAR(64), IN target_index VARCHAR(64), IN index_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = target_table AND index_name = target_index
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD ', index_definition);
|
||||
PREPARE statement FROM @ddl;
|
||||
EXECUTE statement;
|
||||
DEALLOCATE PREPARE statement;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL firstleg_finance_add_index('fl_inbound_outbox', 'uk_inbound_outbox_event_key',
|
||||
'UNIQUE KEY `uk_inbound_outbox_event_key` (`event_key`)');
|
||||
CALL firstleg_finance_add_index('fl_inbound_outbox', 'idx_inbound_outbox_finance_relay',
|
||||
'KEY `idx_inbound_outbox_finance_relay` (`publish_status`, `next_retry_at`, `created_at`, `id`)');
|
||||
DROP PROCEDURE IF EXISTS firstleg_finance_add_index;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_container_fee_detail (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '柜次费用来源ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '柜次ID,对应fl_loading_plan.id',
|
||||
container_booking_id BIGINT UNSIGNED NULL COMMENT '订舱柜ID快照',
|
||||
container_no VARCHAR(64) NULL COMMENT '柜号快照',
|
||||
fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目ID',
|
||||
fee_item_code_snapshot VARCHAR(50) NOT NULL COMMENT '费用项目稳定编码快照',
|
||||
fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照',
|
||||
direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付',
|
||||
business_date DATE NOT NULL COMMENT '费用业务日期',
|
||||
amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额',
|
||||
currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种,首期固定CNY',
|
||||
settlement_included TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否参与财务结算来源池',
|
||||
review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED通过、REJECTED驳回',
|
||||
source_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '来源状态:ACTIVE有效、VOIDED作废',
|
||||
source_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用来源版本;调整递增,作废携带当前版本',
|
||||
remark VARCHAR(500) NULL COMMENT '人工备注',
|
||||
reviewer_id BIGINT UNSIGNED NULL COMMENT '审核人ID',
|
||||
reviewer_name VARCHAR(64) NULL COMMENT '审核人名称快照',
|
||||
reviewed_at DATETIME(3) NULL COMMENT '审核时间',
|
||||
review_reason VARCHAR(500) NULL COMMENT '审核驳回原因',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后更新人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_container_fee_customer_plan (tenant_id, loading_plan_id, source_status),
|
||||
KEY idx_container_fee_settlement (tenant_id, review_status, settlement_included, source_status),
|
||||
CONSTRAINT chk_container_fee_direction CHECK (direction IN ('RECEIVABLE', 'PAYABLE')),
|
||||
CONSTRAINT chk_container_fee_currency CHECK (currency = 'CNY'),
|
||||
CONSTRAINT chk_container_fee_amount CHECK (amount >= 0),
|
||||
CONSTRAINT chk_container_fee_review CHECK (review_status IN ('PENDING', 'APPROVED', 'REJECTED')),
|
||||
CONSTRAINT chk_container_fee_source CHECK (source_status IN ('ACTIVE', 'VOIDED'))
|
||||
) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance';
|
||||
|
||||
DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE firstleg_finance_assert_schema()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_outbox'
|
||||
AND column_name IN ('event_key', 'event_version', 'aggregate_sequence', 'payload_hash',
|
||||
'next_retry_at', 'published_at', 'locked_at', 'locked_by', 'last_error')) <> 9 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:Outbox事件信封字段不完整';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL firstleg_finance_assert_schema();
|
||||
DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema;
|
||||
@@ -5,11 +5,11 @@ USE baiyue_firstleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_fulfillment_task (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '履约任务ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务中的订单逻辑ID,不建立跨库外键',
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
||||
order_snapshot_json JSON NOT NULL COMMENT '创建履约任务时所需的订单业务快照',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '履约任务当前状态',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '履约状态:PENDING_ASSIGNMENT待指派、ASSIGNED已指派、PROCESSING处理中、COMPLETED已完成、CANCELLED已取消',
|
||||
assignee_id BIGINT UNSIGNED NULL COMMENT '当前负责人ID',
|
||||
assignee_name VARCHAR(64) NULL COMMENT '当前负责人名称快照',
|
||||
assigned_at DATETIME(3) NULL COMMENT '最近指派时间',
|
||||
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS fl_fulfillment_task (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_assignment_history (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '指派历史ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
task_id BIGINT UNSIGNED NOT NULL COMMENT '所属履约任务ID',
|
||||
previous_assignee_id BIGINT UNSIGNED NULL COMMENT '原负责人ID',
|
||||
assignee_id BIGINT UNSIGNED NOT NULL COMMENT '新负责人ID',
|
||||
@@ -51,7 +51,7 @@ CREATE TABLE IF NOT EXISTS fl_consumed_event (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '配置ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
box_type VARCHAR(24) NOT NULL COMMENT '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE',
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
|
||||
@@ -14,15 +14,15 @@ CREATE TABLE IF NOT EXISTS fl_fee_item (
|
||||
warehouse_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '仓储应收是否参与',
|
||||
allocation_fee TINYINT(1) NOT NULL DEFAULT 0 COMMENT '平摊费用是否参与',
|
||||
description VARCHAR(500) NULL COMMENT '说明',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE或DISABLED',
|
||||
created_by BIGINT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用',
|
||||
created_by BIGINT NULL COMMENT '创建人ID;系统初始化时可为空',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT NULL COMMENT '最后修改人ID;系统初始化时可为空',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_fee_item_code (fee_code),
|
||||
KEY idx_fee_item_query (tenant_id, trade_scope, category, status, deleted),
|
||||
@@ -45,26 +45,26 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
|
||||
destination_code VARCHAR(32) NULL COMMENT '目的仓编码快照',
|
||||
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
|
||||
package_count INT NULL COMMENT '件数快照',
|
||||
actual_gross_weight_kg DECIMAL(18,4) NULL COMMENT '仓库实测毛重',
|
||||
actual_volume_cbm DECIMAL(18,6) NULL COMMENT '体积',
|
||||
volume_weight_kg DECIMAL(18,4) NULL COMMENT '材积重',
|
||||
chargeable_weight_kg DECIMAL(18,4) NULL COMMENT '计费重量',
|
||||
chargeable_volume_cbm DECIMAL(18,6) NULL COMMENT '计费体积',
|
||||
unit_price DECIMAL(18,4) NOT NULL COMMENT '单价',
|
||||
currency CHAR(3) NOT NULL COMMENT '币种',
|
||||
amount DECIMAL(18,2) NOT NULL COMMENT '金额',
|
||||
actual_gross_weight_kg DECIMAL(18,4) NULL COMMENT '仓库实测毛重快照,单位千克',
|
||||
actual_volume_cbm DECIMAL(18,6) NULL COMMENT '实际体积快照,单位立方米',
|
||||
volume_weight_kg DECIMAL(18,4) NULL COMMENT '材积重快照,单位千克',
|
||||
chargeable_weight_kg DECIMAL(18,4) NULL COMMENT '费用计算采用的计费重量,单位千克',
|
||||
chargeable_volume_cbm DECIMAL(18,6) NULL COMMENT '费用计算采用的计费体积,单位立方米',
|
||||
unit_price DECIMAL(18,4) NOT NULL COMMENT '费用单价;计价单位由对应费用项目规则确定',
|
||||
currency CHAR(3) NOT NULL COMMENT '金额币种三字码,如CNY',
|
||||
amount DECIMAL(18,2) NOT NULL COMMENT '该费用明细金额,币种由currency字段指定',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING、APPROVED、REJECTED',
|
||||
review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
||||
reviewer_id BIGINT NULL COMMENT '审核人ID',
|
||||
reviewer_name VARCHAR(64) NULL COMMENT '审核人名称快照',
|
||||
reviewed_at DATETIME(3) NULL COMMENT '审核时间',
|
||||
review_reason VARCHAR(500) NULL COMMENT '审核驳回原因',
|
||||
created_by BIGINT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 0,
|
||||
created_by BIGINT NULL COMMENT '创建人或系统主体ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT NULL COMMENT '最后修改人或系统主体ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_inbound_fee_detail (tenant_id, inbound_id, business_date, deleted),
|
||||
KEY idx_inbound_fee_review (tenant_id, review_status, business_date, deleted),
|
||||
|
||||
@@ -4,7 +4,7 @@ USE baiyue_firstleg;
|
||||
-- 存量环境可重复执行:首次打印前冻结整单箱号;Print 服务继续保持无状态。
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '实际箱唛头映射ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID快照',
|
||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键',
|
||||
|
||||
@@ -68,7 +68,7 @@ DROP PROCEDURE IF EXISTS phase2_measure_add_column;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_weight_revision (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量修订记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
weight_result_id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID',
|
||||
revision_no INT UNSIGNED NOT NULL COMMENT '结果版本号,与乐观锁版本一致',
|
||||
|
||||
@@ -6,17 +6,17 @@ USE baiyue_firstleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
task_id BIGINT UNSIGNED NULL COMMENT '头程履约任务ID',
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '租户内唯一入仓码',
|
||||
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '客户内唯一入仓码',
|
||||
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜',
|
||||
order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照',
|
||||
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量数据审核状态',
|
||||
fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用业务状态',
|
||||
fee_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态',
|
||||
notification_status VARCHAR(24) NOT NULL DEFAULT 'NOT_CREATED' COMMENT '客户通知状态',
|
||||
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
||||
fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废',
|
||||
fee_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回、CONFIRMED已确认',
|
||||
notification_status VARCHAR(24) NOT NULL DEFAULT 'NOT_CREATED' COMMENT '客户通知状态:NOT_CREATED未创建、PENDING待通知',
|
||||
expected_arrival_at DATETIME(3) NULL COMMENT '预计到达时间',
|
||||
customer_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID',
|
||||
customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照',
|
||||
@@ -79,7 +79,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_pickup_snapshot (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '提货信息快照ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
pickup_method VARCHAR(24) NULL COMMENT '提货方式',
|
||||
pickup_at DATETIME(3) NULL COMMENT '提货时间',
|
||||
@@ -100,7 +100,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pickup_snapshot (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_destination_snapshot (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '目的仓地址快照ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
destination_type VARCHAR(24) NULL COMMENT '目的仓类型',
|
||||
destination_code VARCHAR(32) NULL COMMENT '目的仓编码',
|
||||
@@ -120,7 +120,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_destination_snapshot (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_transport_snapshot (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '运输信息快照ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
loading_status VARCHAR(24) NULL COMMENT '配载状态快照',
|
||||
container_no VARCHAR(64) NULL COMMENT '集装箱号',
|
||||
@@ -142,7 +142,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_transport_snapshot (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '实际箱唛头映射ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID快照',
|
||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键',
|
||||
@@ -167,7 +167,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_measurement (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '测量批次ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
source_type VARCHAR(24) NOT NULL COMMENT '数据来源:THIRD_PARTY或MANUAL',
|
||||
source_code VARCHAR(64) NULL COMMENT '第三方应用或来源编码',
|
||||
@@ -176,7 +176,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement (
|
||||
device_code VARCHAR(64) NULL COMMENT '测量设备编码',
|
||||
warehouse_code VARCHAR(50) NULL COMMENT '第三方回传仓库编码',
|
||||
measured_at DATETIME(3) NULL COMMENT '设备测量时间',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'RECEIVED' COMMENT '测量批次状态',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'RECEIVED' COMMENT '测量批次状态:DRAFT草稿、RECEIVED已接收、SUBMITTED已提交、APPROVED已通过、REJECTED已驳回',
|
||||
raw_payload_json JSON NULL COMMENT '第三方原始报文',
|
||||
original_document_no VARCHAR(64) NULL COMMENT '原单据号码快照',
|
||||
original_gross_weight_kg DECIMAL(18,3) NULL COMMENT '原单据毛重kg',
|
||||
@@ -198,7 +198,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '测量明细ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
measurement_id BIGINT UNSIGNED NOT NULL COMMENT '测量批次ID',
|
||||
inbound_detail_id BIGINT UNSIGNED NULL COMMENT '绑定的订单货物明细ID,平台手工明细可为空',
|
||||
external_detail_no VARCHAR(128) NULL COMMENT '第三方测量明细唯一号,平台手工明细可为空',
|
||||
@@ -238,7 +238,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '墨家联调收件记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
subject_id BIGINT UNSIGNED NOT NULL COMMENT '设备应用操作主体ID',
|
||||
box_code VARCHAR(128) NOT NULL COMMENT '设备扫描的原始箱码',
|
||||
scanned_shipment_id VARCHAR(64) NULL COMMENT '设备从Amazon FBA箱标实扫的Shipment ID',
|
||||
@@ -249,7 +249,7 @@ CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt (
|
||||
machine_no VARCHAR(64) NOT NULL COMMENT '设备编号',
|
||||
measured_at DATETIME(3) NOT NULL COMMENT '设备测量时间或服务接收时间',
|
||||
image_file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务中的墨家联调图片ID',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED' COMMENT '收件状态',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED' COMMENT '测量回执匹配状态:UNMATCHED未匹配;当前首版仅记录未匹配回执',
|
||||
match_failure_code VARCHAR(16) NOT NULL COMMENT '正式业务匹配失败码',
|
||||
match_failure_reason VARCHAR(500) NOT NULL COMMENT '脱敏后的匹配失败原因',
|
||||
request_id VARCHAR(128) NULL COMMENT '请求链路ID',
|
||||
@@ -263,7 +263,7 @@ CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_weight_result (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
file_id BIGINT UNSIGNED NOT NULL COMMENT '称重图片文件ID',
|
||||
ocr_provider VARCHAR(64) NULL COMMENT 'OCR服务商编码',
|
||||
@@ -274,7 +274,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_result (
|
||||
adjusted_weight_value DECIMAL(18,3) NULL COMMENT '人工修正后的原单位重量',
|
||||
adjusted_weight_unit VARCHAR(8) NULL COMMENT '人工修正后的单位',
|
||||
final_weight_kg DECIMAL(18,3) NULL COMMENT '确认并统一换算后的重量kg',
|
||||
recognition_status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '识别和确认状态',
|
||||
recognition_status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '重量识别状态:PENDING待识别、PENDING_CONFIRM待确认、NEEDS_MANUAL需人工录入、CONFIRMED已确认',
|
||||
adjustment_reason VARCHAR(500) NULL COMMENT '人工修正原因',
|
||||
adjusted_by BIGINT UNSIGNED NULL COMMENT '修正人ID',
|
||||
adjusted_at DATETIME(3) NULL COMMENT '修正时间',
|
||||
@@ -294,7 +294,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_result (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_weight_revision (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量修订记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
weight_result_id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID',
|
||||
revision_no INT UNSIGNED NOT NULL COMMENT '结果版本号,与乐观锁版本一致',
|
||||
@@ -317,7 +317,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_revision (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_measure_review_record (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '测量审核记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
measurement_id BIGINT UNSIGNED NULL COMMENT '被审核的测量批次ID',
|
||||
review_result VARCHAR(24) NOT NULL COMMENT '审核结果',
|
||||
@@ -337,8 +337,8 @@ CREATE TABLE IF NOT EXISTS fl_measure_review_record (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customer_charge_rule (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '客户计费规则ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
rule_code VARCHAR(50) NOT NULL COMMENT '租户内规则编码',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
rule_code VARCHAR(50) NOT NULL COMMENT '客户内唯一计费规则编码',
|
||||
rule_version INT UNSIGNED NOT NULL COMMENT '规则版本号',
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户逻辑ID',
|
||||
customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照',
|
||||
@@ -351,7 +351,7 @@ CREATE TABLE IF NOT EXISTS fl_customer_charge_rule (
|
||||
currency CHAR(3) NOT NULL COMMENT '默认币种',
|
||||
effective_from DATETIME(3) NOT NULL COMMENT '生效开始时间',
|
||||
effective_to DATETIME(3) NULL COMMENT '生效结束时间',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '规则状态',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '规则状态:ACTIVE启用、DISABLED停用',
|
||||
remark VARCHAR(500) NULL COMMENT '规则备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
@@ -394,7 +394,7 @@ CREATE TABLE IF NOT EXISTS fl_carrier_fba_rate (
|
||||
source_destination_text VARCHAR(1000) NULL COMMENT '原始组合仓库或区域文本',
|
||||
effective_from DATETIME(3) NOT NULL COMMENT '生效开始时间',
|
||||
effective_to DATETIME(3) NULL COMMENT '生效结束时间',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED;分别表示待确认、已生效、已被替代;FAILED表示解析失败',
|
||||
remark VARCHAR(500) NULL COMMENT '价格备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
@@ -417,9 +417,9 @@ CREATE TABLE IF NOT EXISTS fl_price_channel_catalog (
|
||||
price_model VARCHAR(32) NOT NULL COMMENT '默认价格模型',
|
||||
source_sheet_name VARCHAR(128) NOT NULL COMMENT '来源Sheet名称;海派为同一Sheet',
|
||||
aliases_json JSON NOT NULL COMMENT '上传名称别名数组',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '目录状态',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '目录状态:ACTIVE启用、DISABLED停用',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_price_channel_code (channel_code),
|
||||
KEY idx_price_channel_status (status, price_model)
|
||||
@@ -429,9 +429,9 @@ CREATE TABLE IF NOT EXISTS fl_price_origin_alias (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '起运仓别名ID',
|
||||
warehouse_code VARCHAR(64) NOT NULL COMMENT '平台仓库稳定编码,不存环境专属ID',
|
||||
alias_name VARCHAR(128) NOT NULL COMMENT 'Excel起运仓名称别名',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_price_origin_alias (warehouse_code, alias_name),
|
||||
KEY idx_price_origin_alias_name (alias_name, status)
|
||||
@@ -444,9 +444,9 @@ CREATE TABLE IF NOT EXISTS fl_price_warehouse_mapping (
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '固定平台公共仓库ID',
|
||||
warehouse_code VARCHAR(64) NOT NULL COMMENT '平台仓库稳定编码快照',
|
||||
warehouse_type VARCHAR(16) NOT NULL COMMENT 'DOMESTIC或OVERSEAS',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_price_warehouse_mapping_source (source_type, source_value),
|
||||
KEY idx_price_warehouse_mapping_target (warehouse_id, status)
|
||||
@@ -459,20 +459,20 @@ CREATE TABLE IF NOT EXISTS fl_price_book_version (
|
||||
original_file_name VARCHAR(255) NOT NULL COMMENT '原始Excel文件名',
|
||||
source_file_id BIGINT UNSIGNED NULL COMMENT 'File服务原始文件ID',
|
||||
source_sha256 CHAR(64) NULL COMMENT '原始文件SHA-256',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '待确认上传记录;ACTIVE/SUPERSEDED/FAILED',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、FAILED解析失败',
|
||||
blocking_error_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阻断错误数',
|
||||
warning_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '警告数',
|
||||
parse_issues_json JSON NULL COMMENT '上传解析问题及待确认重新校验结果',
|
||||
channel_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '解析渠道数',
|
||||
rate_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '解析价格数',
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
published_by BIGINT UNSIGNED NULL,
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '上传价格表的操作人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '上传记录创建时间',
|
||||
published_by BIGINT UNSIGNED NULL COMMENT '确认并发布价格版本的操作人ID',
|
||||
published_at DATETIME(3) NULL COMMENT '确认并生效时间',
|
||||
superseded_at DATETIME(3) NULL COMMENT '被下一版本替换时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_price_book_version (tenant_id, version_no),
|
||||
@@ -496,9 +496,9 @@ CREATE TABLE IF NOT EXISTS fl_price_book_channel (
|
||||
surcharge_description VARCHAR(2000) NULL COMMENT '附加费说明,仅展示',
|
||||
remark VARCHAR(2000) NULL COMMENT '渠道备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '渠道快照创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '渠道快照最后更新时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
@@ -552,9 +552,9 @@ ON DUPLICATE KEY UPDATE
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_fee (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库费用ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
fee_no VARCHAR(40) NOT NULL COMMENT '租户内唯一费用单号',
|
||||
fee_no VARCHAR(40) NOT NULL COMMENT '客户内唯一费用单号',
|
||||
fee_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用快照版本',
|
||||
charge_rule_id BIGINT UNSIGNED NULL COMMENT '历史客户计费规则ID;渠道价格可为空',
|
||||
charge_rule_version INT UNSIGNED NULL COMMENT '历史客户规则版本快照',
|
||||
@@ -594,8 +594,8 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee (
|
||||
subtotal_amount DECIMAL(18,2) NOT NULL COMMENT '基础费用金额',
|
||||
additional_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '附加费金额',
|
||||
total_amount DECIMAL(18,2) NOT NULL COMMENT '总金额',
|
||||
fee_status VARCHAR(24) NOT NULL DEFAULT 'CALCULATED' COMMENT '费用状态',
|
||||
review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态',
|
||||
fee_status VARCHAR(24) NOT NULL DEFAULT 'CALCULATED' COMMENT '费用状态:CALCULATED已计算、SUPERSEDED已被新版本替代',
|
||||
review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回、CONFIRMED已确认',
|
||||
exception_release TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否基于测量异常放行生成',
|
||||
calculated_by BIGINT UNSIGNED NOT NULL COMMENT '试算人ID',
|
||||
calculated_at DATETIME(3) NOT NULL COMMENT '试算时间',
|
||||
@@ -616,7 +616,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_fee_item (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '费用明细ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
fee_id BIGINT UNSIGNED NOT NULL COMMENT '费用头ID',
|
||||
item_no INT UNSIGNED NOT NULL COMMENT '费用单内序号',
|
||||
fee_code VARCHAR(50) NOT NULL COMMENT '费用项目编码',
|
||||
@@ -636,7 +636,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee_item (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '头程托盘分配引用ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
allocation_no INT UNSIGNED NOT NULL COMMENT '入库单内分配序号',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '仓库服务仓库ID',
|
||||
@@ -650,7 +650,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation (
|
||||
location_code VARCHAR(50) NOT NULL COMMENT '库位编码快照',
|
||||
reservation_id BIGINT UNSIGNED NULL COMMENT '仓库预占资源唯一ID,A0共享场景用于区分托盘',
|
||||
reservation_no VARCHAR(64) NOT NULL COMMENT '仓库库位预占单号',
|
||||
allocation_status VARCHAR(24) NOT NULL DEFAULT 'RESERVED' COMMENT '分配状态',
|
||||
allocation_status VARCHAR(24) NOT NULL DEFAULT 'RESERVED' COMMENT '托盘库位分配状态:RESERVED已预占、CONFIRMED已确认、TRANSFERRED已转移、RELEASED已释放',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
@@ -663,7 +663,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_file (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库单文件关系ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
file_id BIGINT UNSIGNED NOT NULL COMMENT '文件服务文件ID',
|
||||
file_type VARCHAR(24) NOT NULL COMMENT '文件类型:RELATED、MARK、PC_ORDER、MEASURE或WEIGHT',
|
||||
@@ -681,7 +681,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_file (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_hold_record (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '扣货记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
hold_action VARCHAR(16) NOT NULL COMMENT '动作:HOLD或RELEASE',
|
||||
reason VARCHAR(1000) NOT NULL COMMENT '扣货或解除原因',
|
||||
@@ -696,7 +696,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_hold_record (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_internal_note (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '客服内部备注ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
content VARCHAR(2000) NOT NULL COMMENT '仅内部可见备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
@@ -709,7 +709,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_internal_note (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_status_history (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库状态历史ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
from_status VARCHAR(24) NULL COMMENT '变更前状态',
|
||||
to_status VARCHAR(24) NOT NULL COMMENT '变更后状态',
|
||||
@@ -725,12 +725,12 @@ CREATE TABLE IF NOT EXISTS fl_inbound_status_history (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_outbox (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '领域事件全局ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属客户ID和数据隔离键',
|
||||
aggregate_type VARCHAR(32) NOT NULL DEFAULT 'INBOUND' COMMENT '聚合类型',
|
||||
aggregate_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
||||
payload_json JSON NOT NULL COMMENT '事件负载快照',
|
||||
publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发布状态',
|
||||
publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '事件发布状态:PENDING待发布、PUBLISHED已发布、FAILED发布失败',
|
||||
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
|
||||
next_retry_at DATETIME(3) NULL COMMENT '下次重试时间',
|
||||
published_at DATETIME(3) NULL COMMENT '发布时间',
|
||||
@@ -741,13 +741,13 @@ CREATE TABLE IF NOT EXISTS fl_inbound_outbox (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_idempotency (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库写操作幂等记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '租户ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
operation VARCHAR(40) NOT NULL COMMENT '操作类型',
|
||||
operator_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '操作者或外部应用ID',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '入库单ID,创建前可为0',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '请求内容摘要',
|
||||
status VARCHAR(16) NOT NULL COMMENT '处理状态',
|
||||
status VARCHAR(16) NOT NULL COMMENT '幂等处理状态:PROCESSING处理中、SUCCEEDED已成功、FAILED已失败',
|
||||
result_json JSON NULL COMMENT '成功结果摘要',
|
||||
result_resource_id BIGINT UNSIGNED NULL COMMENT '成功结果资源ID',
|
||||
completed_at DATETIME(3) NULL COMMENT '完成时间',
|
||||
@@ -760,7 +760,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_idempotency (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '配置ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
box_type VARCHAR(24) NOT NULL COMMENT '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE',
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
|
||||
@@ -422,7 +422,7 @@ CALL phase2_publish_add_column('fl_inbound_fee_detail', 'chargeable_quantity',
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'charge_unit',
|
||||
"VARCHAR(16) NULL COMMENT 'KG或CBM' AFTER chargeable_quantity");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'publication_status',
|
||||
"VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT、PUBLISHED、SUPERSEDED' AFTER review_reason");
|
||||
"VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT '费用发布状态:DRAFT草稿、PUBLISHED已发布、SUPERSEDED已替代' AFTER review_reason");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'published_by',
|
||||
"BIGINT NULL COMMENT '发布人ID' AFTER publication_status");
|
||||
CALL phase2_publish_add_column('fl_inbound_fee_detail', 'published_by_name',
|
||||
|
||||
@@ -2,38 +2,38 @@ SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
query_no VARCHAR(40) NOT NULL,
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
container_booking_id BIGINT UNSIGNED NULL,
|
||||
container_scope_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(container_booking_id,0)) STORED,
|
||||
carrier_code VARCHAR(16) NOT NULL COMMENT 'MATSON/SML/ZIM/COSCO/EMC/HEDE',
|
||||
tracking_type VARCHAR(32) NOT NULL COMMENT 'BOOKING_NO/BILL_OF_LADING/CONTAINER_NO',
|
||||
tracking_no VARCHAR(128) NOT NULL,
|
||||
official_url VARCHAR(500) NOT NULL,
|
||||
access_mode VARCHAR(32) NOT NULL COMMENT 'PUBLIC_FORM/HCAPTCHA/DYNAMIC_IFRAME',
|
||||
status VARCHAR(24) NOT NULL COMMENT 'READY/MANUAL_REQUIRED/SUCCESS',
|
||||
window_started_at DATETIME(3) NOT NULL,
|
||||
attempt_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
next_allowed_at DATETIME(3) NOT NULL,
|
||||
last_attempt_at DATETIME(3) NULL,
|
||||
last_success_at DATETIME(3) NULL,
|
||||
source_updated_at DATETIME(3) NULL,
|
||||
expires_at DATETIME(3) NULL,
|
||||
result_port_schedule_id BIGINT UNSIGNED NULL,
|
||||
last_attempt_status VARCHAR(24) NULL,
|
||||
manual_reason VARCHAR(1000) NULL,
|
||||
last_error_code VARCHAR(64) NULL,
|
||||
last_error_message VARCHAR(1000) NULL,
|
||||
remark VARCHAR(1000) NULL,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '船司官网查询任务ID',
|
||||
query_no VARCHAR(40) NOT NULL COMMENT '船司官网查询任务编号',
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联海运订舱ID',
|
||||
container_booking_id BIGINT UNSIGNED NULL COMMENT '指定查询的柜次ID;整票订舱查询时为空',
|
||||
container_scope_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(container_booking_id,0)) STORED COMMENT '柜次唯一作用域计算值;整票订舱查询固定为0',
|
||||
carrier_code VARCHAR(16) NOT NULL COMMENT '船公司编码:MATSON美森、SML森罗、ZIM以星、COSCO中远海运、EMC长荣、HEDE合德',
|
||||
tracking_type VARCHAR(32) NOT NULL COMMENT '官网跟踪类型:BOOKING_NO订舱号、BILL_OF_LADING提单号、CONTAINER_NO箱号',
|
||||
tracking_no VARCHAR(128) NOT NULL COMMENT '提交至船公司官网的跟踪号码',
|
||||
official_url VARCHAR(500) NOT NULL COMMENT '船公司官网查询入口地址快照',
|
||||
access_mode VARCHAR(32) NOT NULL COMMENT '官网访问方式:PUBLIC_FORM公开表单、HCAPTCHA人机校验、DYNAMIC_IFRAME动态页面',
|
||||
status VARCHAR(24) NOT NULL COMMENT '任务状态:READY可查询、MANUAL_REQUIRED需人工处理、SUCCESS已取得有效结果',
|
||||
window_started_at DATETIME(3) NOT NULL COMMENT '当前三天查询窗口起始时间',
|
||||
attempt_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '当前查询窗口已执行次数;最多3次',
|
||||
next_allowed_at DATETIME(3) NOT NULL COMMENT '频控规则允许的下次查询时间',
|
||||
last_attempt_at DATETIME(3) NULL COMMENT '最近一次查询尝试时间',
|
||||
last_success_at DATETIME(3) NULL COMMENT '最近一次取得有效结果时间',
|
||||
source_updated_at DATETIME(3) NULL COMMENT '船公司官网展示的结果更新时间',
|
||||
expires_at DATETIME(3) NULL COMMENT '当前最近有效结果过期时间;到期后仍保留但标记过期',
|
||||
result_port_schedule_id BIGINT UNSIGNED NULL COMMENT '最近有效结果对应的船期记录ID',
|
||||
last_attempt_status VARCHAR(24) NULL COMMENT '最近查询尝试状态快照',
|
||||
manual_reason VARCHAR(1000) NULL COMMENT '需要人工查询或录入的原因',
|
||||
last_error_code VARCHAR(64) NULL COMMENT '最近查询失败错误码',
|
||||
last_error_message VARCHAR(1000) NULL COMMENT '最近查询失败错误说明',
|
||||
remark VARCHAR(1000) NULL COMMENT '平台内部备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '任务创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '任务最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_carrier_schedule_query_no (query_no),
|
||||
UNIQUE KEY uk_carrier_schedule_query_scope
|
||||
@@ -57,21 +57,21 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query (
|
||||
) ENGINE=InnoDB COMMENT='六家船公司官网查询任务与三天窗口频控';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_attempt (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
query_id BIGINT UNSIGNED NOT NULL,
|
||||
window_attempt_no INT UNSIGNED NOT NULL,
|
||||
carrier_code VARCHAR(16) NOT NULL,
|
||||
access_mode VARCHAR(32) NOT NULL,
|
||||
official_url VARCHAR(500) NOT NULL,
|
||||
attempt_status VARCHAR(24) NOT NULL,
|
||||
coverage_gap TINYINT(1) NOT NULL DEFAULT 0,
|
||||
coverage_gap_code VARCHAR(64) NULL,
|
||||
manual_reason VARCHAR(1000) NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1000) NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '船司官网查询尝试ID',
|
||||
query_id BIGINT UNSIGNED NOT NULL COMMENT '所属船司官网查询任务ID',
|
||||
window_attempt_no INT UNSIGNED NOT NULL COMMENT '当前三天窗口内的尝试序号;取值1至3',
|
||||
carrier_code VARCHAR(16) NOT NULL COMMENT '本次尝试的船公司编码快照',
|
||||
access_mode VARCHAR(32) NOT NULL COMMENT '本次尝试的官网访问方式快照',
|
||||
official_url VARCHAR(500) NOT NULL COMMENT '本次尝试使用的官网地址快照',
|
||||
attempt_status VARCHAR(24) NOT NULL COMMENT '尝试结果:MANUAL_REQUIRED需人工处理、SUCCESS成功、FAILED失败',
|
||||
coverage_gap TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否存在自动化覆盖缺口:0否、1是',
|
||||
coverage_gap_code VARCHAR(64) NULL COMMENT '自动化覆盖缺口编码',
|
||||
manual_reason VARCHAR(1000) NULL COMMENT '转人工处理原因',
|
||||
error_code VARCHAR(64) NULL COMMENT '查询失败错误码',
|
||||
error_message VARCHAR(1000) NULL COMMENT '查询失败错误说明',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行本次尝试的操作人ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '执行操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '尝试记录创建时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_carrier_schedule_attempt_query (query_id,created_at,id),
|
||||
CONSTRAINT fk_carrier_schedule_attempt_query FOREIGN KEY (query_id) REFERENCES fl_carrier_schedule_query(id),
|
||||
@@ -81,17 +81,17 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_attempt (
|
||||
) ENGINE=InnoDB COMMENT='船司官网查询尝试不可变审计';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
query_id BIGINT UNSIGNED NOT NULL,
|
||||
action VARCHAR(24) NOT NULL,
|
||||
from_version INT UNSIGNED NULL,
|
||||
to_version INT UNSIGNED NOT NULL,
|
||||
reason VARCHAR(1000) NULL,
|
||||
before_snapshot JSON NULL,
|
||||
after_snapshot JSON NOT NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '船司查询任务历史ID',
|
||||
query_id BIGINT UNSIGNED NOT NULL COMMENT '所属船司官网查询任务ID',
|
||||
action VARCHAR(24) NOT NULL COMMENT '历史动作:CREATE创建、ATTEMPT执行查询、MANUAL_RESULT录入人工结果',
|
||||
from_version INT UNSIGNED NULL COMMENT '变更前任务版本;创建时为空',
|
||||
to_version INT UNSIGNED NOT NULL COMMENT '变更后任务版本',
|
||||
reason VARCHAR(1000) NULL COMMENT '操作原因或说明',
|
||||
before_snapshot JSON NULL COMMENT '变更前任务数据快照;创建时为空',
|
||||
after_snapshot JSON NOT NULL COMMENT '变更后任务数据快照',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_carrier_schedule_history_query (query_id,created_at,id),
|
||||
CONSTRAINT fk_carrier_schedule_history_query FOREIGN KEY (query_id) REFERENCES fl_carrier_schedule_query(id),
|
||||
@@ -99,16 +99,16 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history (
|
||||
) ENGINE=InnoDB COMMENT='船司查询任务不可变历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
query_id BIGINT UNSIGNED NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operation VARCHAR(24) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
|
||||
result_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
completed_at DATETIME(3) NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '船司查询写操作幂等记录ID',
|
||||
query_id BIGINT UNSIGNED NULL COMMENT '关联查询任务ID;创建任务落库前允许为空',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的平台操作人ID',
|
||||
operation VARCHAR(24) NOT NULL COMMENT '幂等写操作类型',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态:PROCESSING处理中、SUCCESS成功',
|
||||
result_json JSON NULL COMMENT '成功结果JSON快照;供幂等重放返回',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间',
|
||||
completed_at DATETIME(3) NULL COMMENT '写操作成功完成时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_carrier_schedule_operation (operator_id,idempotency_key),
|
||||
KEY idx_carrier_schedule_operation_query (query_id,created_at,id),
|
||||
|
||||
@@ -2,17 +2,17 @@ SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_checklist_version (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
checklist_code VARCHAR(40) NOT NULL,
|
||||
version_no VARCHAR(32) NOT NULL,
|
||||
country_code VARCHAR(32) NOT NULL,
|
||||
channel_code VARCHAR(40) NOT NULL,
|
||||
cargo_type VARCHAR(40) NOT NULL,
|
||||
items_json JSON NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关资料清单版本逻辑ID',
|
||||
checklist_code VARCHAR(40) NOT NULL COMMENT '报关资料清单编码;同一清单的多个版本共用',
|
||||
version_no VARCHAR(32) NOT NULL COMMENT '清单业务版本号',
|
||||
country_code VARCHAR(32) NOT NULL COMMENT '清单适用的目的国家或地区编码',
|
||||
channel_code VARCHAR(40) NOT NULL COMMENT '清单适用的运输渠道编码',
|
||||
cargo_type VARCHAR(40) NOT NULL COMMENT '清单适用的货物类型编码',
|
||||
items_json JSON NOT NULL COMMENT '不可变清单项目数组;元素包含code项目编码、name项目名称和required是否必填',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '清单版本状态:ACTIVE启用、RETIRED停用',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建清单版本的平台操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建清单版本时的平台操作人名称快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '清单版本创建时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customs_checklist_version (checklist_code, version_no),
|
||||
KEY idx_customs_checklist_match (country_code, channel_code, cargo_type, status),
|
||||
@@ -20,24 +20,24 @@ CREATE TABLE IF NOT EXISTS fl_customs_checklist_version (
|
||||
) ENGINE=InnoDB COMMENT='三期报关资料清单不可变版本';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_case (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
customs_no VARCHAR(40) NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
order_id BIGINT UNSIGNED NOT NULL,
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
checklist_version_id BIGINT UNSIGNED NOT NULL,
|
||||
checklist_code VARCHAR(40) NOT NULL,
|
||||
checklist_version_no VARCHAR(32) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'DOCUMENTS_PENDING',
|
||||
remark VARCHAR(500) NULL,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关协同单逻辑ID',
|
||||
customs_no VARCHAR(40) NOT NULL COMMENT '全局唯一报关协同单号',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT 'Order服务订单逻辑ID;跨库不建立外键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT 'Firstleg入库单逻辑ID',
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联柜次逻辑ID;0表示创建时尚未关联柜次',
|
||||
checklist_version_id BIGINT UNSIGNED NOT NULL COMMENT '本单冻结使用的报关资料清单版本ID',
|
||||
checklist_code VARCHAR(40) NOT NULL COMMENT '创建协同单时的清单编码快照;源清单变化不回写',
|
||||
checklist_version_no VARCHAR(32) NOT NULL COMMENT '创建协同单时的清单版本号快照;源清单变化不回写',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'DOCUMENTS_PENDING' COMMENT '协同状态:DOCUMENTS_PENDING待补资料、REVIEWING审核中、SUPPLEMENT_REQUIRED需补件、READY_TO_SUBMIT待提交代理、RECEIPT_PENDING待回执、RECEIPT_AVAILABLE已有回执、CLEARANCE_COMPLETED已放行、CANCELLED已取消',
|
||||
remark VARCHAR(500) NULL COMMENT '报关协同单内部业务备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建协同单的平台操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建协同单时的平台操作人名称快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '协同单创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改协同单的平台操作人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改时的平台操作人名称快照',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '协同单最后修改时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号;每次状态变更递增',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customs_no (customs_no),
|
||||
UNIQUE KEY uk_customs_case_scope (tenant_id, order_id, inbound_id, container_booking_id),
|
||||
@@ -48,23 +48,23 @@ CREATE TABLE IF NOT EXISTS fl_customs_case (
|
||||
) ENGINE=InnoDB COMMENT='三期订单报关协同聚合';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_document (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL,
|
||||
checklist_item_code VARCHAR(40) NOT NULL,
|
||||
file_id BIGINT UNSIGNED NOT NULL,
|
||||
document_version INT UNSIGNED NOT NULL,
|
||||
replaces_document_id BIGINT UNSIGNED NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_hash CHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'UPLOADED',
|
||||
visibility VARCHAR(16) NOT NULL DEFAULT 'CUSTOMER',
|
||||
uploaded_by BIGINT UNSIGNED NOT NULL,
|
||||
uploaded_by_type VARCHAR(16) NOT NULL,
|
||||
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
reviewed_by BIGINT UNSIGNED NULL,
|
||||
reviewed_by_name VARCHAR(64) NULL,
|
||||
reviewed_at DATETIME(3) NULL,
|
||||
review_reason VARCHAR(500) NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关资料文件版本逻辑ID',
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID',
|
||||
checklist_item_code VARCHAR(40) NOT NULL COMMENT '对应冻结清单中的资料项目编码',
|
||||
file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID;跨库不建立外键',
|
||||
document_version INT UNSIGNED NOT NULL COMMENT '同一协同单和清单项目下的文件版本序号',
|
||||
replaces_document_id BIGINT UNSIGNED NULL COMMENT '被本版本替换的上一资料版本ID;首个版本为空',
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '上传或引用时的文件名称快照',
|
||||
file_hash CHAR(64) NOT NULL COMMENT '文件内容SHA-256摘要;用于版本审计和幂等校验',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'UPLOADED' COMMENT '资料状态:UPLOADED待审核、APPROVED审核通过、RETURNED已退回、SUPERSEDED已被替换、FROZEN已随代理提交批次冻结',
|
||||
visibility VARCHAR(16) NOT NULL DEFAULT 'CUSTOMER' COMMENT '资料可见范围:PLATFORM仅平台可见、CUSTOMER客户可见',
|
||||
uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传或引用资料的操作人ID',
|
||||
uploaded_by_type VARCHAR(16) NOT NULL COMMENT '上传主体类型:PLATFORM平台操作人、MERCHANT客户操作人',
|
||||
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '资料上传或引用时间',
|
||||
reviewed_by BIGINT UNSIGNED NULL COMMENT '审核资料的平台操作人ID;未审核时为空',
|
||||
reviewed_by_name VARCHAR(64) NULL COMMENT '审核时的平台操作人名称快照',
|
||||
reviewed_at DATETIME(3) NULL COMMENT '资料审核时间',
|
||||
review_reason VARCHAR(500) NULL COMMENT '审核退回原因或审核备注',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customs_document_version (customs_case_id, checklist_item_code, document_version),
|
||||
UNIQUE KEY uk_customs_document_file (customs_case_id, file_id),
|
||||
@@ -76,19 +76,19 @@ CREATE TABLE IF NOT EXISTS fl_customs_document (
|
||||
) ENGINE=InnoDB COMMENT='报关客户文件版本引用';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_supplement_request (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL,
|
||||
source_document_id BIGINT UNSIGNED NOT NULL,
|
||||
resolved_document_id BIGINT UNSIGNED NULL,
|
||||
checklist_item_code VARCHAR(40) NOT NULL,
|
||||
reason VARCHAR(500) NOT NULL,
|
||||
due_at DATETIME(3) NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'OPEN',
|
||||
requested_by BIGINT UNSIGNED NOT NULL,
|
||||
requested_by_name VARCHAR(64) NULL,
|
||||
requested_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
resolved_by BIGINT UNSIGNED NULL,
|
||||
resolved_at DATETIME(3) NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关补件请求逻辑ID',
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID',
|
||||
source_document_id BIGINT UNSIGNED NOT NULL COMMENT '触发本次补件的被退回资料版本ID',
|
||||
resolved_document_id BIGINT UNSIGNED NULL COMMENT '解决本次补件的新资料版本ID;未解决时为空',
|
||||
checklist_item_code VARCHAR(40) NOT NULL COMMENT '需要补充的报关资料项目编码',
|
||||
reason VARCHAR(500) NOT NULL COMMENT '平台审核人员填写的补件原因',
|
||||
due_at DATETIME(3) NULL COMMENT '期望完成补件时间;未设置时为空',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'OPEN' COMMENT '补件状态:OPEN待补件、RESOLVED已由新资料解决、CANCELLED已取消',
|
||||
requested_by BIGINT UNSIGNED NOT NULL COMMENT '发起补件的平台操作人ID',
|
||||
requested_by_name VARCHAR(64) NULL COMMENT '发起补件时的平台操作人名称快照',
|
||||
requested_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '补件请求发起时间',
|
||||
resolved_by BIGINT UNSIGNED NULL COMMENT '提交解决资料的操作人ID;未解决时为空',
|
||||
resolved_at DATETIME(3) NULL COMMENT '补件请求解决时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_customs_supplement_case (customs_case_id, status, due_at),
|
||||
CONSTRAINT fk_customs_supplement_case FOREIGN KEY (customs_case_id) REFERENCES fl_customs_case(id),
|
||||
@@ -96,18 +96,18 @@ CREATE TABLE IF NOT EXISTS fl_customs_supplement_request (
|
||||
) ENGINE=InnoDB COMMENT='报关补件请求';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_submission_batch (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL,
|
||||
batch_no VARCHAR(40) NOT NULL,
|
||||
batch_version INT UNSIGNED NOT NULL,
|
||||
customs_agent_name VARCHAR(128) NOT NULL,
|
||||
external_reference VARCHAR(128) NULL,
|
||||
file_snapshot_json JSON NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'SUBMITTED',
|
||||
note VARCHAR(500) NULL,
|
||||
submitted_by BIGINT UNSIGNED NOT NULL,
|
||||
submitted_by_name VARCHAR(64) NULL,
|
||||
submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关代理提交批次逻辑ID',
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID',
|
||||
batch_no VARCHAR(40) NOT NULL COMMENT '全局唯一代理提交批次号',
|
||||
batch_version INT UNSIGNED NOT NULL COMMENT '同一报关协同单内的提交批次版本序号',
|
||||
customs_agent_name VARCHAR(128) NOT NULL COMMENT '本批次接收报关资料的报关代理名称快照',
|
||||
external_reference VARCHAR(128) NULL COMMENT '报关代理或外部系统返回的业务参考号',
|
||||
file_snapshot_json JSON NOT NULL COMMENT '提交时冻结的已审核资料数组;元素包含documentId、fileId、itemCode、documentVersion和fileHash',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'SUBMITTED' COMMENT '代理提交状态:SUBMITTED已提交、ACKNOWLEDGED代理已确认、REJECTED代理已拒绝、COMPLETED代理处理完成',
|
||||
note VARCHAR(500) NULL COMMENT '提交报关代理时的平台备注',
|
||||
submitted_by BIGINT UNSIGNED NOT NULL COMMENT '提交报关代理的平台操作人ID',
|
||||
submitted_by_name VARCHAR(64) NULL COMMENT '提交时的平台操作人名称快照',
|
||||
submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '提交报关代理时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customs_batch_no (batch_no),
|
||||
UNIQUE KEY uk_customs_batch_version (customs_case_id, batch_version),
|
||||
@@ -116,20 +116,20 @@ CREATE TABLE IF NOT EXISTS fl_customs_submission_batch (
|
||||
) ENGINE=InnoDB COMMENT='报关代理提交批次及冻结文件版本';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_receipt (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL,
|
||||
submission_batch_id BIGINT UNSIGNED NOT NULL,
|
||||
receipt_type VARCHAR(16) NOT NULL,
|
||||
file_id BIGINT UNSIGNED NOT NULL,
|
||||
receipt_version INT UNSIGNED NOT NULL,
|
||||
replaces_receipt_id BIGINT UNSIGNED NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_hash CHAR(64) NOT NULL,
|
||||
visibility VARCHAR(16) NOT NULL DEFAULT 'PLATFORM',
|
||||
note VARCHAR(500) NULL,
|
||||
uploaded_by BIGINT UNSIGNED NOT NULL,
|
||||
uploaded_by_name VARCHAR(64) NULL,
|
||||
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关回执文件版本逻辑ID',
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID',
|
||||
submission_batch_id BIGINT UNSIGNED NOT NULL COMMENT '产生本回执的报关代理提交批次ID',
|
||||
receipt_type VARCHAR(16) NOT NULL COMMENT '回执类型:RECEIVED已收件、DECLARED已申报、REJECTED已拒绝、INSPECTION查验通知、RELEASED放行回执、OTHER其他回执',
|
||||
file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务回执文件逻辑ID;跨库不建立外键',
|
||||
receipt_version INT UNSIGNED NOT NULL COMMENT '同一提交批次和回执类型下的版本序号',
|
||||
replaces_receipt_id BIGINT UNSIGNED NULL COMMENT '被本版本替换的上一回执版本ID;首个版本为空',
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '上传或引用时的回执文件名称快照',
|
||||
file_hash CHAR(64) NOT NULL COMMENT '回执文件内容SHA-256摘要;用于版本审计和幂等校验',
|
||||
visibility VARCHAR(16) NOT NULL DEFAULT 'PLATFORM' COMMENT '回执可见范围:PLATFORM仅平台可见、CUSTOMER客户可查询下载',
|
||||
note VARCHAR(500) NULL COMMENT '回执业务备注',
|
||||
uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传或引用回执的平台操作人ID',
|
||||
uploaded_by_name VARCHAR(64) NULL COMMENT '上传回执时的平台操作人名称快照',
|
||||
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '回执上传或引用时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customs_receipt_version (submission_batch_id, receipt_type, receipt_version),
|
||||
UNIQUE KEY uk_customs_receipt_file (submission_batch_id, file_id),
|
||||
@@ -141,18 +141,18 @@ CREATE TABLE IF NOT EXISTS fl_customs_receipt (
|
||||
) ENGINE=InnoDB COMMENT='报关代理回执版本与客户可见范围';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_history (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL,
|
||||
action VARCHAR(40) NOT NULL,
|
||||
from_status VARCHAR(32) NULL,
|
||||
to_status VARCHAR(32) NULL,
|
||||
resource_type VARCHAR(32) NOT NULL,
|
||||
resource_id BIGINT UNSIGNED NOT NULL,
|
||||
note VARCHAR(500) NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL,
|
||||
operator_type VARCHAR(16) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关协同历史逻辑ID',
|
||||
customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID',
|
||||
action VARCHAR(40) NOT NULL COMMENT '业务动作编码,如CREATE、UPLOAD_DOCUMENT、REPLACE_DOCUMENT、REVIEW_DOCUMENT_APPROVE、REVIEW_DOCUMENT_RETURN、SUBMIT_AGENT_BATCH或UPLOAD_RECEIPT',
|
||||
from_status VARCHAR(32) NULL COMMENT '动作发生前的协同状态;创建动作时为空',
|
||||
to_status VARCHAR(32) NULL COMMENT '动作完成后的协同状态',
|
||||
resource_type VARCHAR(32) NOT NULL COMMENT '动作关联资源类型:CASE协同单、DOCUMENT资料、SUBMISSION_BATCH提交批次或RECEIPT回执',
|
||||
resource_id BIGINT UNSIGNED NOT NULL COMMENT '动作关联资源逻辑ID',
|
||||
note VARCHAR(500) NULL COMMENT '动作原因、资料项目编码或业务备注快照',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行业务动作的操作人ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '执行动作时的操作人名称快照',
|
||||
operator_type VARCHAR(16) NOT NULL COMMENT '操作主体类型:PLATFORM平台操作人、MERCHANT客户操作人',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_customs_history_case (customs_case_id, created_at, id),
|
||||
CONSTRAINT fk_customs_history_case FOREIGN KEY (customs_case_id) REFERENCES fl_customs_case(id),
|
||||
@@ -160,16 +160,16 @@ CREATE TABLE IF NOT EXISTS fl_customs_history (
|
||||
) ENGINE=InnoDB COMMENT='报关协同稳定历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customs_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
|
||||
result_type VARCHAR(24) NULL,
|
||||
result_id BIGINT UNSIGNED NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
completed_at DATETIME(3) NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '报关写命令幂等记录逻辑ID',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写命令的操作人ID;与动作和幂等键共同限定重放范围',
|
||||
action VARCHAR(64) NOT NULL COMMENT '幂等业务动作编码,包含平台或客户动作及资料、审核、提交、回执类型',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端提供的幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '规范化完整请求内容的SHA-256摘要;同键不同请求时拒绝',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS处理成功',
|
||||
result_type VARCHAR(24) NULL COMMENT '成功结果资源类型:CHECKLIST、CASE、DOCUMENT、BATCH或RECEIPT',
|
||||
result_id BIGINT UNSIGNED NULL COMMENT '成功结果资源逻辑ID;处理中为空',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间',
|
||||
completed_at DATETIME(3) NULL COMMENT '写命令成功完成时间;处理中为空',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customs_operation (operator_id, action, idempotency_key),
|
||||
CONSTRAINT chk_customs_operation_status CHECK (status IN ('PROCESSING','SUCCESS'))
|
||||
|
||||
@@ -169,19 +169,19 @@ DROP PROCEDURE IF EXISTS phase3_inbound_add_index;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_split_batch (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入仓单ID',
|
||||
operation_id VARCHAR(64) NOT NULL COMMENT '业务操作幂等号',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '请求幂等键',
|
||||
expected_parent_version INT NOT NULL COMMENT '操作时主单乐观锁版本',
|
||||
package_count INT UNSIGNED NOT NULL COMMENT '本批次拆分箱数',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PREPARED' COMMENT 'PREPARED、APPLIED、CANCELLED',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PREPARED' COMMENT '拆分批次状态:PREPARED已准备、APPLIED已生效、CANCELLED已取消',
|
||||
remark VARCHAR(500) NULL COMMENT '拆分原因或备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拆分批次创建时间',
|
||||
applied_at DATETIME(3) NULL COMMENT '批次生效时间',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_inbound_split_operation (tenant_id, operation_id),
|
||||
UNIQUE KEY uk_inbound_split_idempotency (tenant_id, parent_inbound_id, idempotency_key),
|
||||
@@ -193,7 +193,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_batch (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_split_item (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '拆分明细ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
split_batch_id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID',
|
||||
parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入仓单ID',
|
||||
child_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '子入仓单ID',
|
||||
@@ -202,7 +202,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_item (
|
||||
source_measurement_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量批次ID',
|
||||
source_measurement_item_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量箱规明细ID',
|
||||
package_count INT UNSIGNED NOT NULL COMMENT '分配给子单的箱数',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拆分数量台账创建时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_inbound_split_batch_line (tenant_id, split_batch_id, line_no),
|
||||
UNIQUE KEY uk_inbound_split_batch_source_child (tenant_id, split_batch_id, source_measurement_item_id, child_inbound_id),
|
||||
@@ -217,16 +217,16 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_item (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_split_history (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '拆分历史ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
split_batch_id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID',
|
||||
operation_id VARCHAR(64) NOT NULL COMMENT '历史操作幂等号',
|
||||
action_type VARCHAR(24) NOT NULL COMMENT 'CREATE、APPLY、CANCEL',
|
||||
action_type VARCHAR(24) NOT NULL COMMENT '拆分动作:CREATE创建批次、APPLY生效、CANCEL取消',
|
||||
before_snapshot JSON NULL COMMENT '操作前快照',
|
||||
after_snapshot JSON NOT NULL COMMENT '操作后快照',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '操作人名称快照',
|
||||
operation_reason VARCHAR(500) NULL COMMENT '操作原因',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变拆分历史创建时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_inbound_split_history_operation (tenant_id, operation_id),
|
||||
KEY idx_inbound_split_history (tenant_id, split_batch_id, created_at, id),
|
||||
|
||||
@@ -3,26 +3,26 @@ USE baiyue_firstleg;
|
||||
|
||||
/* 装柜证据、PDA 拣货复核和派送完成。五类证据按有效记录计数,同类型允许多图追加;作废记录保留审计。 */
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_evidence (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '装柜证据记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID',
|
||||
evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1空仓/PHOTO_STEP_2半仓/PHOTO_STEP_3满仓/PHOTO_STEP_4锁仓/SEAL带锁码照',
|
||||
file_id BIGINT UNSIGNED NOT NULL COMMENT 'File 服务逻辑文件ID',
|
||||
file_name VARCHAR(255) NULL,
|
||||
seal_no VARCHAR(64) NULL,
|
||||
remark VARCHAR(1000) NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/VOID,作废不硬删除',
|
||||
void_reason VARCHAR(500) NULL,
|
||||
voided_by BIGINT UNSIGNED NULL,
|
||||
voided_by_name VARCHAR(64) NULL,
|
||||
voided_at DATETIME(3) NULL,
|
||||
version INT NOT NULL DEFAULT 0,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
file_name VARCHAR(255) NULL COMMENT '上传时文件名称快照',
|
||||
seal_no VARCHAR(64) NULL COMMENT '锁码文本;带锁码照必填,客户不可见',
|
||||
remark VARCHAR(1000) NULL COMMENT '证据补充说明',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID已作废;错误照片保留记录不硬删除',
|
||||
void_reason VARCHAR(500) NULL COMMENT '照片作废原因',
|
||||
voided_by BIGINT UNSIGNED NULL COMMENT '执行作废的平台操作人ID',
|
||||
voided_by_name VARCHAR(64) NULL COMMENT '执行作废的平台操作人姓名快照',
|
||||
voided_at DATETIME(3) NULL COMMENT '照片作废时间',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '上传证据的操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '上传证据的操作人姓名快照',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '证据记录创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '证据记录最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status),
|
||||
KEY idx_loading_evidence_seal (tenant_id, seal_no, status),
|
||||
@@ -46,11 +46,11 @@ BEGIN
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL phase3_loading_add_column('status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''ACTIVE/VOID,作废不硬删除''');
|
||||
CALL phase3_loading_add_column('void_reason', 'VARCHAR(500) NULL');
|
||||
CALL phase3_loading_add_column('voided_by', 'BIGINT UNSIGNED NULL');
|
||||
CALL phase3_loading_add_column('voided_by_name', 'VARCHAR(64) NULL');
|
||||
CALL phase3_loading_add_column('voided_at', 'DATETIME(3) NULL');
|
||||
CALL phase3_loading_add_column('status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''证据状态:ACTIVE有效、VOID已作废;错误照片保留记录不硬删除''');
|
||||
CALL phase3_loading_add_column('void_reason', 'VARCHAR(500) NULL COMMENT ''照片作废原因''');
|
||||
CALL phase3_loading_add_column('voided_by', 'BIGINT UNSIGNED NULL COMMENT ''执行作废的平台操作人ID''');
|
||||
CALL phase3_loading_add_column('voided_by_name', 'VARCHAR(64) NULL COMMENT ''执行作废的平台操作人姓名快照''');
|
||||
CALL phase3_loading_add_column('voided_at', 'DATETIME(3) NULL COMMENT ''照片作废时间''');
|
||||
DROP PROCEDURE IF EXISTS phase3_loading_add_column;
|
||||
|
||||
SET @phase3_loading_has_legacy_type_index = (
|
||||
@@ -94,17 +94,17 @@ EXECUTE phase3_loading_seal_stmt;
|
||||
DEALLOCATE PREPARE phase3_loading_seal_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operation VARCHAR(48) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
|
||||
result_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
completed_at DATETIME(3) NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '装柜写操作记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '操作目标入库单逻辑ID',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行写操作的平台用户ID',
|
||||
operation VARCHAR(48) NOT NULL COMMENT '装柜或证据写操作类型',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键,同一操作重试复用',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS成功、FAILED失败',
|
||||
result_json JSON NULL COMMENT '成功结果或失败信息JSON快照,用于幂等重放',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作声明创建时间',
|
||||
completed_at DATETIME(3) NULL COMMENT '操作完成时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key),
|
||||
KEY idx_loading_operation_time (tenant_id, inbound_id, created_at),
|
||||
@@ -112,18 +112,18 @@ CREATE TABLE IF NOT EXISTS fl_loading_operation (
|
||||
) ENGINE=InnoDB COMMENT='装柜与派送写操作幂等记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_loading_pick_task (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/IN_PROGRESS/COMPLETED',
|
||||
version INT NOT NULL DEFAULT 0,
|
||||
expected_count INT UNSIGNED NULL,
|
||||
picked_count INT UNSIGNED NULL,
|
||||
remark VARCHAR(1000) NULL,
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库单级PDA拣货任务ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态:PENDING待拣货、IN_PROGRESS拣货中、COMPLETED已完成',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增',
|
||||
expected_count INT UNSIGNED NULL COMMENT '计划应拣箱数',
|
||||
picked_count INT UNSIGNED NULL COMMENT '实际已拣箱数',
|
||||
remark VARCHAR(1000) NULL COMMENT '拣货结果或异常补充说明',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后操作人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货任务创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货任务最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_pick_tenant_inbound (tenant_id, inbound_id),
|
||||
KEY idx_loading_pick_status (tenant_id, status, updated_at),
|
||||
|
||||
@@ -10,9 +10,9 @@ CREATE TABLE IF NOT EXISTS fl_preload_loading_plan_link (
|
||||
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '柜次级装柜计划ID',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '建立关联幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '建立关联请求SHA-256',
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '建立预装与装柜计划关联的操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '建立关联的操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '关联记录创建时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_preload_loading_pair (tenant_id, inbound_id, loading_plan_id),
|
||||
UNIQUE KEY uk_preload_loading_idempotency (created_by, idempotency_key),
|
||||
|
||||
@@ -2,42 +2,42 @@ SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
booking_no VARCHAR(40) NOT NULL,
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT',
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID',
|
||||
customer_name VARCHAR(128) NOT NULL,
|
||||
bill_of_lading_no VARCHAR(128) NULL,
|
||||
shipping_company_id BIGINT UNSIGNED NULL,
|
||||
shipping_company_name VARCHAR(128) NULL,
|
||||
booking_agent_id BIGINT UNSIGNED NULL,
|
||||
booking_agent_name VARCHAR(128) NULL,
|
||||
vessel_name VARCHAR(128) NULL,
|
||||
voyage_no VARCHAR(64) NULL,
|
||||
departure_port_code VARCHAR(32) NULL,
|
||||
departure_port_name VARCHAR(128) NULL,
|
||||
destination_port_code VARCHAR(32) NULL,
|
||||
destination_port_name VARCHAR(128) NULL,
|
||||
place_of_delivery VARCHAR(128) NULL,
|
||||
service_term VARCHAR(32) NULL,
|
||||
freight_term VARCHAR(32) NULL,
|
||||
estimated_sailing_at DATETIME(3) NULL,
|
||||
estimated_arrival_at DATETIME(3) NULL,
|
||||
pending_vessel_name VARCHAR(128) NULL,
|
||||
pending_voyage_no VARCHAR(64) NULL,
|
||||
pending_estimated_sailing_at DATETIME(3) NULL,
|
||||
pending_estimated_arrival_at DATETIME(3) NULL,
|
||||
change_reason VARCHAR(1000) NULL,
|
||||
failure_reason VARCHAR(1000) NULL,
|
||||
cancellation_reason VARCHAR(1000) NULL,
|
||||
remark VARCHAR(1000) NULL,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '海运订舱ID',
|
||||
booking_no VARCHAR(40) NOT NULL COMMENT '平台海运订舱编号',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订舱状态:DRAFT草稿、SUBMITTED已提交、CONFIRMED已确认、RELEASED已放舱、FAILED失败、CHANGE_PENDING船期待变更、CANCELLED已取消',
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照',
|
||||
bill_of_lading_no VARCHAR(128) NULL COMMENT '提单号',
|
||||
shipping_company_id BIGINT UNSIGNED NULL COMMENT '船公司逻辑ID快照',
|
||||
shipping_company_name VARCHAR(128) NULL COMMENT '船公司名称快照',
|
||||
booking_agent_id BIGINT UNSIGNED NULL COMMENT '订舱代理逻辑ID快照',
|
||||
booking_agent_name VARCHAR(128) NULL COMMENT '订舱代理名称快照',
|
||||
vessel_name VARCHAR(128) NULL COMMENT '当前确认船名快照',
|
||||
voyage_no VARCHAR(64) NULL COMMENT '当前确认航次号快照',
|
||||
departure_port_code VARCHAR(32) NULL COMMENT '起运港编码快照',
|
||||
departure_port_name VARCHAR(128) NULL COMMENT '起运港名称快照',
|
||||
destination_port_code VARCHAR(32) NULL COMMENT '目的港编码快照',
|
||||
destination_port_name VARCHAR(128) NULL COMMENT '目的港名称快照',
|
||||
place_of_delivery VARCHAR(128) NULL COMMENT '最终交货地快照',
|
||||
service_term VARCHAR(32) NULL COMMENT '运输服务条款快照',
|
||||
freight_term VARCHAR(32) NULL COMMENT '海运费支付条款快照',
|
||||
estimated_sailing_at DATETIME(3) NULL COMMENT '当前确认预计开船时间',
|
||||
estimated_arrival_at DATETIME(3) NULL COMMENT '当前确认预计到港时间',
|
||||
pending_vessel_name VARCHAR(128) NULL COMMENT '待确认变更船名',
|
||||
pending_voyage_no VARCHAR(64) NULL COMMENT '待确认变更航次号',
|
||||
pending_estimated_sailing_at DATETIME(3) NULL COMMENT '待确认变更预计开船时间',
|
||||
pending_estimated_arrival_at DATETIME(3) NULL COMMENT '待确认变更预计到港时间',
|
||||
change_reason VARCHAR(1000) NULL COMMENT '船期变更原因',
|
||||
failure_reason VARCHAR(1000) NULL COMMENT '订舱失败原因',
|
||||
cancellation_reason VARCHAR(1000) NULL COMMENT '取消订舱原因',
|
||||
remark VARCHAR(1000) NULL COMMENT '平台内部备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '订舱记录创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '订舱记录最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ocean_booking_no (booking_no),
|
||||
KEY idx_ocean_booking_customer (customer_id,status,updated_at,id),
|
||||
@@ -53,13 +53,13 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking (
|
||||
) ENGINE=InnoDB COMMENT='独立海运订舱聚合;柜次仍由fl_container_booking承载';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_party (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
party_type VARCHAR(24) NOT NULL COMMENT 'SHIPPER/CONSIGNEE/NOTIFY_PARTY',
|
||||
party_name VARCHAR(255) NOT NULL,
|
||||
address VARCHAR(1000) NULL,
|
||||
contact_name VARCHAR(128) NULL,
|
||||
contact_phone VARCHAR(64) NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订舱提单关系方ID',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID',
|
||||
party_type VARCHAR(24) NOT NULL COMMENT '关系方类型:SHIPPER发货人、CONSIGNEE收货人、NOTIFY_PARTY通知人',
|
||||
party_name VARCHAR(255) NOT NULL COMMENT '关系方名称快照',
|
||||
address VARCHAR(1000) NULL COMMENT '关系方地址快照',
|
||||
contact_name VARCHAR(128) NULL COMMENT '关系方联系人快照',
|
||||
contact_phone VARCHAR(64) NULL COMMENT '关系方联系电话快照',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ocean_booking_party (ocean_booking_id,party_type),
|
||||
CONSTRAINT fk_ocean_booking_party FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id),
|
||||
@@ -67,12 +67,12 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_party (
|
||||
) ENGINE=InnoDB COMMENT='订舱提单关系方快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
container_type VARCHAR(32) NOT NULL,
|
||||
container_kind VARCHAR(32) NOT NULL,
|
||||
quantity INT UNSIGNED NOT NULL,
|
||||
sort_no INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订舱箱型需求ID',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID',
|
||||
container_type VARCHAR(32) NOT NULL COMMENT '所需集装箱尺寸类型编码',
|
||||
container_kind VARCHAR(32) NOT NULL COMMENT '所需集装箱箱种编码',
|
||||
quantity INT UNSIGNED NOT NULL COMMENT '该箱型箱种所需柜数',
|
||||
sort_no INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '订舱需求展示顺序',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_ocean_requirement_booking (ocean_booking_id,sort_no,id),
|
||||
CONSTRAINT fk_ocean_requirement_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id),
|
||||
@@ -80,13 +80,13 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement (
|
||||
) ENGINE=InnoDB COMMENT='订舱箱型箱种及数量需求';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
requirement_id BIGINT UNSIGNED NOT NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订舱柜次分配关系ID',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID',
|
||||
requirement_id BIGINT UNSIGNED NOT NULL COMMENT '对应订舱箱型需求ID',
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '明确引用现有柜次ID',
|
||||
allocated_by BIGINT UNSIGNED NOT NULL,
|
||||
allocated_by_name VARCHAR(64) NULL,
|
||||
allocated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
allocated_by BIGINT UNSIGNED NOT NULL COMMENT '分配操作人ID',
|
||||
allocated_by_name VARCHAR(64) NULL COMMENT '分配操作人姓名快照',
|
||||
allocated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '柜次分配时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ocean_allocation_container (container_booking_id),
|
||||
KEY idx_ocean_allocation_booking (ocean_booking_id,requirement_id,allocated_at),
|
||||
@@ -97,33 +97,33 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation (
|
||||
) ENGINE=InnoDB COMMENT='订舱需求与现有柜次的唯一分配关系';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_status_history (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
from_status VARCHAR(24) NULL,
|
||||
to_status VARCHAR(24) NOT NULL,
|
||||
action VARCHAR(40) NOT NULL,
|
||||
reason VARCHAR(1000) NULL,
|
||||
before_snapshot JSON NULL,
|
||||
after_snapshot JSON NOT NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订舱状态历史ID',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID',
|
||||
from_status VARCHAR(24) NULL COMMENT '变更前订舱状态;创建时为空',
|
||||
to_status VARCHAR(24) NOT NULL COMMENT '变更后订舱状态',
|
||||
action VARCHAR(40) NOT NULL COMMENT '触发状态变更的业务动作',
|
||||
reason VARCHAR(1000) NULL COMMENT '状态变更原因',
|
||||
before_snapshot JSON NULL COMMENT '状态变更前订舱数据快照;创建时为空',
|
||||
after_snapshot JSON NOT NULL COMMENT '状态变更后订舱数据快照',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '状态历史创建时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_ocean_history_booking (ocean_booking_id,created_at,id),
|
||||
CONSTRAINT fk_ocean_history_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id)
|
||||
) ENGINE=InnoDB COMMENT='海运订舱不可变状态历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_ocean_booking_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NULL COMMENT '创建请求完成前允许为空',
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operation VARCHAR(40) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
|
||||
result_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
completed_at DATETIME(3) NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订舱写操作幂等记录ID',
|
||||
ocean_booking_id BIGINT UNSIGNED NULL COMMENT '关联海运订舱ID;创建请求完成前允许为空',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的平台操作人ID',
|
||||
operation VARCHAR(40) NOT NULL COMMENT '幂等写操作类型',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态:PROCESSING处理中、SUCCESS成功',
|
||||
result_json JSON NULL COMMENT '成功结果JSON快照;供幂等重放返回',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间',
|
||||
completed_at DATETIME(3) NULL COMMENT '写操作成功完成时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ocean_operation (operator_id,operation,idempotency_key),
|
||||
KEY idx_ocean_operation_booking (ocean_booking_id,created_at),
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
USE baiyue_firstleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_outbound_dispatch (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单引用,不复制订单主数据',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单主对象',
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '柜次引用',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'READY_FOR_DISPATCH',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
confirmed_by BIGINT UNSIGNED NULL,
|
||||
confirmed_by_name VARCHAR(64) NULL,
|
||||
confirmed_at DATETIME(3) NULL,
|
||||
reversed_by BIGINT UNSIGNED NULL,
|
||||
reversed_by_name VARCHAR(64) NULL,
|
||||
reversed_at DATETIME(3) NULL,
|
||||
remark VARCHAR(500) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '国内出仓调度记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT 'Order服务订单逻辑ID,不复制订单主数据',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID',
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联柜次逻辑ID',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'READY_FOR_DISPATCH' COMMENT '出仓状态:READY_FOR_DISPATCH待确认出仓、WAITING_FOR_LOADING等待装柜',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增',
|
||||
confirmed_by BIGINT UNSIGNED NULL COMMENT '确认国内出仓的操作人ID',
|
||||
confirmed_by_name VARCHAR(64) NULL COMMENT '确认国内出仓的操作人姓名快照',
|
||||
confirmed_at DATETIME(3) NULL COMMENT '国内出仓确认时间',
|
||||
reversed_by BIGINT UNSIGNED NULL COMMENT '撤销国内出仓的操作人ID',
|
||||
reversed_by_name VARCHAR(64) NULL COMMENT '撤销国内出仓的操作人姓名快照',
|
||||
reversed_at DATETIME(3) NULL COMMENT '国内出仓撤销时间',
|
||||
remark VARCHAR(500) NULL COMMENT '国内出仓调度备注',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '出仓调度记录创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '出仓调度记录最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_outbound_dispatch_owner (tenant_id,inbound_id,container_booking_id),
|
||||
KEY idx_outbound_dispatch_order (tenant_id,order_id,updated_at),
|
||||
@@ -28,33 +28,33 @@ CREATE TABLE IF NOT EXISTS fl_outbound_dispatch (
|
||||
) ENGINE=InnoDB COMMENT='三期国内出仓派车状态,不重复创建出仓单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_outbound_dispatch_history (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
dispatch_id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
action VARCHAR(32) NOT NULL,
|
||||
from_status VARCHAR(32) NULL,
|
||||
to_status VARCHAR(32) NOT NULL,
|
||||
remark VARCHAR(500) NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '出仓状态历史记录ID',
|
||||
dispatch_id BIGINT UNSIGNED NOT NULL COMMENT '关联国内出仓调度记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID快照',
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联柜次逻辑ID快照',
|
||||
action VARCHAR(32) NOT NULL COMMENT '出仓状态变更动作',
|
||||
from_status VARCHAR(32) NULL COMMENT '变更前出仓状态;首次创建时为空',
|
||||
to_status VARCHAR(32) NOT NULL COMMENT '变更后出仓状态',
|
||||
remark VARCHAR(500) NULL COMMENT '状态变更原因或备注',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行状态变更的平台用户ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '执行状态变更的平台用户姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '状态历史创建时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_outbound_dispatch_history (tenant_id,inbound_id,container_booking_id,created_at,id),
|
||||
CONSTRAINT fk_outbound_dispatch_history FOREIGN KEY (dispatch_id) REFERENCES fl_outbound_dispatch(id)
|
||||
) ENGINE=InnoDB COMMENT='国内出仓追加式审计历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_outbound_dispatch_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '出仓幂等操作记录ID',
|
||||
dispatch_id BIGINT UNSIGNED NULL COMMENT '首次声明幂等键时允许为空',
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operation VARCHAR(32) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
completed_at DATETIME(3) NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行写操作的平台用户ID',
|
||||
operation VARCHAR(32) NOT NULL COMMENT '国内出仓写操作类型',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键,同一操作重试复用',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS成功',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等声明创建时间',
|
||||
completed_at DATETIME(3) NULL COMMENT '幂等操作完成时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_outbound_dispatch_operation (operator_id,operation,idempotency_key),
|
||||
KEY idx_outbound_dispatch_operation_resource (dispatch_id,created_at),
|
||||
|
||||
@@ -2,30 +2,46 @@ SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment (
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL,
|
||||
assignee_id BIGINT UNSIGNED NOT NULL,
|
||||
assignee_name VARCHAR(64) NOT NULL,
|
||||
assigned_by BIGINT UNSIGNED NOT NULL,
|
||||
assigned_by_name VARCHAR(64) NULL,
|
||||
assignment_type VARCHAR(16) NOT NULL COMMENT 'CLAIMED/ASSIGNED',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '柜次PDA拣货任务ID;同一任务仅保留一名作业员',
|
||||
assignee_id BIGINT UNSIGNED NOT NULL COMMENT '当前唯一作业员ID',
|
||||
assignee_name VARCHAR(64) NOT NULL COMMENT '当前作业员姓名快照',
|
||||
assigned_by BIGINT UNSIGNED NOT NULL COMMENT '领取任务的作业员或执行分配的平台用户ID',
|
||||
assigned_by_name VARCHAR(64) NULL COMMENT '领取或分配操作人姓名快照',
|
||||
assignment_type VARCHAR(16) NOT NULL COMMENT '分配方式:CLAIMED作业员领取、ASSIGNED平台指派',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,防止多人并发领取或覆盖',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '任务领取或分配时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '作业员分配最后更新时间',
|
||||
PRIMARY KEY (pick_task_id),
|
||||
KEY idx_loading_pick_assignee (assignee_id, updated_at),
|
||||
CONSTRAINT fk_loading_pick_assignment_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
||||
CONSTRAINT chk_loading_pick_assignment_type CHECK (assignment_type IN ('CLAIMED', 'ASSIGNED'))
|
||||
) ENGINE=InnoDB COMMENT='柜次PDA任务领取与作业员分配';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment (
|
||||
pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID,同时作为本表主键',
|
||||
assignee_id BIGINT UNSIGNED NOT NULL COMMENT '当前领取或被分配的作业员ID',
|
||||
assignee_name VARCHAR(64) NOT NULL COMMENT '当前作业员姓名快照',
|
||||
assigned_by BIGINT UNSIGNED NOT NULL COMMENT '执行领取或分配操作的用户ID',
|
||||
assigned_by_name VARCHAR(64) NULL COMMENT '执行领取或分配操作的用户姓名快照',
|
||||
assignment_type VARCHAR(16) NOT NULL COMMENT '分配类型:CLAIMED作业员领取、ASSIGNED管理员分配',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '明细分配记录创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '明细分配记录最后更新时间',
|
||||
PRIMARY KEY (pick_item_id),
|
||||
KEY idx_loading_pick_item_assignee (assignee_id, updated_at),
|
||||
CONSTRAINT fk_loading_pick_item_assignment_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id),
|
||||
CONSTRAINT chk_loading_pick_item_assignment_type CHECK (assignment_type IN ('CLAIMED', 'ASSIGNED'))
|
||||
) ENGINE=InnoDB COMMENT='PDA拣货明细领取与作业员分配';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_pick_review (
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL,
|
||||
review_status VARCHAR(16) NOT NULL DEFAULT 'APPROVED',
|
||||
reviewed_by BIGINT UNSIGNED NOT NULL,
|
||||
reviewed_by_name VARCHAR(64) NULL,
|
||||
remark VARCHAR(500) NULL,
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '已完成的柜次PDA拣货任务ID',
|
||||
review_status VARCHAR(16) NOT NULL DEFAULT 'APPROVED' COMMENT '复核状态:APPROVED已通过;记录创建后不可修改或删除',
|
||||
reviewed_by BIGINT UNSIGNED NOT NULL COMMENT '执行完成复核的平台用户ID',
|
||||
reviewed_by_name VARCHAR(64) NULL COMMENT '复核人姓名快照',
|
||||
remark VARCHAR(500) NULL COMMENT 'PDA任务复核备注',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '复核记录版本号',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '复核通过时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '复核记录最后更新时间;正常业务不允许更新',
|
||||
PRIMARY KEY (pick_task_id),
|
||||
KEY idx_loading_pick_reviewer (reviewed_by, created_at),
|
||||
CONSTRAINT fk_loading_pick_review_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
||||
@@ -33,14 +49,14 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_review (
|
||||
) ENGINE=InnoDB COMMENT='柜次PDA任务完成复核';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_pick_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL,
|
||||
operation_id VARCHAR(128) NOT NULL,
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
action_type VARCHAR(16) NOT NULL COMMENT 'CLAIM/ASSIGN/REVIEW',
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT 'PDA任务操作审计记录ID',
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '关联柜次PDA拣货任务ID',
|
||||
operation_id VARCHAR(128) NOT NULL COMMENT '全局唯一操作ID,用于幂等重放',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '完整批量请求或任务操作内容SHA-256摘要',
|
||||
action_type VARCHAR(16) NOT NULL COMMENT '动作类型:CLAIM领取、ASSIGN分配、REVIEW复核、BATCH批量提交',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行PDA任务操作的用户ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变操作审计创建时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_pick_operation_id (operation_id),
|
||||
KEY idx_loading_pick_operation_task (pick_task_id, created_at),
|
||||
|
||||
@@ -2,35 +2,35 @@ SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_port_schedule (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
schedule_no VARCHAR(40) NOT NULL,
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
container_booking_id BIGINT UNSIGNED NULL,
|
||||
source_type VARCHAR(24) NOT NULL COMMENT 'MANUAL/CARRIER/TERMINAL/API',
|
||||
source_name VARCHAR(128) NOT NULL,
|
||||
source_ref VARCHAR(255) NOT NULL,
|
||||
vessel_name VARCHAR(128) NOT NULL,
|
||||
voyage_no VARCHAR(64) NOT NULL,
|
||||
departure_port_code VARCHAR(32) NOT NULL,
|
||||
departure_port_name VARCHAR(128) NULL,
|
||||
destination_port_code VARCHAR(32) NOT NULL,
|
||||
destination_port_name VARCHAR(128) NULL,
|
||||
etd_at DATETIME(3) NULL,
|
||||
eta_at DATETIME(3) NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '港务船期记录ID',
|
||||
schedule_no VARCHAR(40) NOT NULL COMMENT '港务船期记录编号',
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联海运订舱ID',
|
||||
container_booking_id BIGINT UNSIGNED NULL COMMENT '关联柜次ID;订舱整票船期时为空',
|
||||
source_type VARCHAR(24) NOT NULL COMMENT '船期来源类型:MANUAL人工录入、CARRIER船公司、TERMINAL码头、API外部接口',
|
||||
source_name VARCHAR(128) NOT NULL COMMENT '船期来源名称',
|
||||
source_ref VARCHAR(255) NOT NULL COMMENT '来源侧记录唯一引用或查询凭据摘要',
|
||||
vessel_name VARCHAR(128) NOT NULL COMMENT '船名快照',
|
||||
voyage_no VARCHAR(64) NOT NULL COMMENT '航次号快照',
|
||||
departure_port_code VARCHAR(32) NOT NULL COMMENT '起运港编码快照',
|
||||
departure_port_name VARCHAR(128) NULL COMMENT '起运港名称快照',
|
||||
destination_port_code VARCHAR(32) NOT NULL COMMENT '目的港编码快照',
|
||||
destination_port_name VARCHAR(128) NULL COMMENT '目的港名称快照',
|
||||
etd_at DATETIME(3) NULL COMMENT '预计离港时间',
|
||||
eta_at DATETIME(3) NULL COMMENT '预计到港时间',
|
||||
booking_cutoff_at DATETIME(3) NULL COMMENT '截单时间',
|
||||
customs_cutoff_at DATETIME(3) NULL COMMENT '截关时间',
|
||||
port_cutoff_at DATETIME(3) NULL COMMENT '截港时间',
|
||||
observed_at DATETIME(3) NOT NULL COMMENT '来源数据观测时间',
|
||||
expires_at DATETIME(3) NOT NULL COMMENT '超过该时间即由API计算为过期',
|
||||
remark VARCHAR(1000) NULL,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
remark VARCHAR(1000) NULL COMMENT '平台内部备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '船期记录创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '船期记录最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_port_schedule_no (schedule_no),
|
||||
UNIQUE KEY uk_port_schedule_source (source_type,source_name,source_ref),
|
||||
@@ -53,17 +53,17 @@ CREATE TABLE IF NOT EXISTS fl_port_schedule (
|
||||
) ENGINE=InnoDB COMMENT='独立港务船期来源聚合,不替代订舱快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_port_schedule_history (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
port_schedule_id BIGINT UNSIGNED NOT NULL,
|
||||
action VARCHAR(24) NOT NULL,
|
||||
from_version INT UNSIGNED NULL,
|
||||
to_version INT UNSIGNED NOT NULL,
|
||||
reason VARCHAR(1000) NULL,
|
||||
before_snapshot JSON NULL,
|
||||
after_snapshot JSON NOT NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '港务船期变更历史ID',
|
||||
port_schedule_id BIGINT UNSIGNED NOT NULL COMMENT '所属港务船期记录ID',
|
||||
action VARCHAR(24) NOT NULL COMMENT '变更动作:CREATE创建、UPDATE更新',
|
||||
from_version INT UNSIGNED NULL COMMENT '变更前版本;创建时为空',
|
||||
to_version INT UNSIGNED NOT NULL COMMENT '变更后版本',
|
||||
reason VARCHAR(1000) NULL COMMENT '变更原因或说明',
|
||||
before_snapshot JSON NULL COMMENT '变更前船期数据快照;创建时为空',
|
||||
after_snapshot JSON NOT NULL COMMENT '变更后船期数据快照',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_port_schedule_history (port_schedule_id,created_at,id),
|
||||
CONSTRAINT fk_port_schedule_history FOREIGN KEY (port_schedule_id) REFERENCES fl_port_schedule(id),
|
||||
@@ -71,16 +71,16 @@ CREATE TABLE IF NOT EXISTS fl_port_schedule_history (
|
||||
) ENGINE=InnoDB COMMENT='港务船期不可变变更历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_port_schedule_operation (
|
||||
id BIGINT UNSIGNED NOT NULL,
|
||||
port_schedule_id BIGINT UNSIGNED NULL COMMENT '创建请求完成前允许为空',
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operation VARCHAR(24) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
|
||||
result_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
completed_at DATETIME(3) NULL,
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '港务船期写操作幂等记录ID',
|
||||
port_schedule_id BIGINT UNSIGNED NULL COMMENT '关联港务船期ID;创建请求完成前允许为空',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的平台操作人ID',
|
||||
operation VARCHAR(24) NOT NULL COMMENT '幂等写操作类型',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态:PROCESSING处理中、SUCCESS成功',
|
||||
result_json JSON NULL COMMENT '成功结果JSON快照;供幂等重放返回',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间',
|
||||
completed_at DATETIME(3) NULL COMMENT '写操作成功完成时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_port_schedule_operation (operator_id,idempotency_key),
|
||||
KEY idx_port_schedule_operation_resource (port_schedule_id,created_at),
|
||||
|
||||
@@ -60,8 +60,7 @@ CREATE TABLE IF NOT EXISTS fl_container_booking (
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
COMMENT '更新时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '货柜资料最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_container_booking_no (booking_no),
|
||||
@@ -77,8 +76,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '智能装柜计划ID(雪花ID)',
|
||||
plan_no VARCHAR(40) NOT NULL COMMENT '平台唯一装柜计划号',
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID',
|
||||
plan_status VARCHAR(24) NOT NULL DEFAULT 'EDITING'
|
||||
COMMENT '唯一装柜状态:EDITING、LOADING、COMPLETED',
|
||||
plan_status VARCHAR(24) NOT NULL DEFAULT 'EDITING' COMMENT '装柜计划状态:EDITING编辑中、LOADING装柜中、COMPLETED装柜完成;建议须经人工确认',
|
||||
push_idempotency_key VARCHAR(128) NULL COMMENT '推送预装幂等键',
|
||||
preload_pushed_by BIGINT UNSIGNED NULL COMMENT '推送预装操作人ID',
|
||||
preload_pushed_by_name VARCHAR(64) NULL COMMENT '推送预装操作人名称快照',
|
||||
@@ -95,8 +93,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan (
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
COMMENT '更新时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '装柜计划最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_plan_no (plan_no),
|
||||
@@ -149,16 +146,13 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
|
||||
removed_by_name VARCHAR(64) NULL COMMENT '推送前逻辑删除操作人名称快照',
|
||||
removed_at DATETIME(3) NULL COMMENT '推送前逻辑删除时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '推送前逻辑删除标记;冻结后不可修改',
|
||||
active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS (
|
||||
CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END
|
||||
) STORED COMMENT '未删除明细唯一锁;删除后返回NULL释放分配资格',
|
||||
active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END) STORED COMMENT '未删除明细唯一锁;删除后返回NULL释放分配资格',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
COMMENT '更新时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '装柜明细最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_item_plan_detail (loading_plan_id, active_inbound_detail_id),
|
||||
@@ -185,15 +179,14 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
|
||||
CREATE TABLE IF NOT EXISTS fl_container_document (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '货柜文件关联ID(雪花ID)',
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID',
|
||||
document_type VARCHAR(32) NOT NULL COMMENT 'CUSTOMS_CLEARANCE、DELIVERY_ORDER、PICKUP_ORDER',
|
||||
document_type VARCHAR(32) NOT NULL COMMENT '文件类型:CUSTOMS_CLEARANCE清关文件、DELIVERY_ORDER提货单、PICKUP_ORDER取货单',
|
||||
file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件ID,不建立跨库外键',
|
||||
file_name VARCHAR(255) NOT NULL COMMENT '文件名称快照',
|
||||
file_url VARCHAR(1000) NULL COMMENT '文件访问地址快照',
|
||||
uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传操作人ID',
|
||||
uploaded_by_name VARCHAR(64) NULL COMMENT '上传操作人名称快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
COMMENT '更新时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '货柜文件关联最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_container_document_type (container_booking_id, document_type),
|
||||
@@ -209,8 +202,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_history (
|
||||
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '智能装柜计划ID',
|
||||
loading_plan_item_id BIGINT UNSIGNED NULL COMMENT '装柜计划明细ID',
|
||||
operation_id VARCHAR(128) NOT NULL COMMENT '操作幂等ID',
|
||||
action_type VARCHAR(32) NOT NULL
|
||||
COMMENT 'CREATE_PLAN、ADD_ITEM、ADJUST_ITEM、REMOVE_ITEM、START_LOADING、COMPLETE_LOADING、PICK_ITEM',
|
||||
action_type VARCHAR(32) NOT NULL COMMENT '动作类型:CREATE_PLAN创建计划、ADD_ITEM新增明细、ADJUST_ITEM调整明细、REMOVE_ITEM移除明细、START_LOADING开始装柜、COMPLETE_LOADING完成装柜、PICK_ITEM拣货',
|
||||
from_status VARCHAR(24) NULL COMMENT '变更前状态',
|
||||
to_status VARCHAR(24) NULL COMMENT '变更后状态',
|
||||
before_snapshot JSON NULL COMMENT '变更前快照',
|
||||
@@ -405,7 +397,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_task (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID',
|
||||
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID',
|
||||
container_no VARCHAR(64) NOT NULL COMMENT '集装箱号快照',
|
||||
task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING' COMMENT 'LOADING、COMPLETED',
|
||||
task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING' COMMENT 'PDA任务状态:LOADING拣货装柜中、COMPLETED已完成',
|
||||
started_by BIGINT UNSIGNED NOT NULL COMMENT '开始装柜人',
|
||||
started_by_name VARCHAR(64) NULL COMMENT '开始装柜人名称',
|
||||
started_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '开始时间',
|
||||
@@ -428,12 +420,12 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_item (
|
||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID',
|
||||
planned_package_count INT UNSIGNED NOT NULL COMMENT '计划拣货箱数;上层拆分按业务规则向上取整',
|
||||
picked_package_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已拣货箱数',
|
||||
item_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING、PICKING、PICKED、PARTIAL、SKIPPED',
|
||||
item_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '拣货明细状态:PENDING待拣、PICKING拣货中、PICKED已拣完、PARTIAL部分拣货、SKIPPED跳拣',
|
||||
release_mode VARCHAR(10) NULL COMMENT 'AUTO按比例释放,MANUAL按人工选择释放',
|
||||
release_location_ids JSON NULL COMMENT '人工选择释放库位ID;空数组表示明确释放0个',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货明细创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货明细最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_pick_item_plan_item (loading_plan_item_id),
|
||||
KEY idx_loading_pick_item_task (pick_task_id, item_status, id),
|
||||
@@ -449,13 +441,13 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_history (
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID',
|
||||
pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID',
|
||||
operation_id VARCHAR(128) NOT NULL COMMENT '客户端幂等操作号',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '请求指纹',
|
||||
action_type VARCHAR(24) NOT NULL DEFAULT 'PICK' COMMENT 'PICK、PARTIAL、SKIP',
|
||||
request_hash CHAR(64) NOT NULL COMMENT '完整批量请求内容SHA-256摘要',
|
||||
action_type VARCHAR(24) NOT NULL DEFAULT 'PICK' COMMENT '拣货动作:PICK正常拣货、PARTIAL部分拣货、SKIP跳拣',
|
||||
picked_package_count INT UNSIGNED NOT NULL COMMENT '本次拣货箱数',
|
||||
operation_reason VARCHAR(500) NULL COMMENT '部分拣货或跳拣原因',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人',
|
||||
operator_name VARCHAR(64) NULL COMMENT '操作人名称',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行PDA拣货操作的作业员ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '作业员姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变拣货操作历史创建时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_pick_history_operation (operation_id),
|
||||
KEY idx_loading_pick_history_item (pick_item_id, created_at),
|
||||
|
||||
@@ -4,18 +4,18 @@ USE baiyue_firstleg;
|
||||
/* 三期分柜与预装清单;独立于现有测量、计费表,可重复部署。 */
|
||||
CREATE TABLE IF NOT EXISTS fl_preload_plan (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '预装清单ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单/订单ID',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/CONFIRMING/CONFIRMED',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
|
||||
items_json JSON NOT NULL COMMENT '可编辑分柜明细快照',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '预装状态:PENDING待确认、CONFIRMING确认中、CONFIRMED已确认',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增',
|
||||
items_json JSON NOT NULL COMMENT '可编辑分柜明细JSON快照;人工确认后作为预装依据',
|
||||
remark VARCHAR(1000) NULL COMMENT '预装备注',
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
updated_by BIGINT UNSIGNED NOT NULL,
|
||||
updated_by_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建预装清单的操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '预装清单创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '预装清单最后更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_preload_tenant_inbound (tenant_id, inbound_id),
|
||||
KEY idx_preload_status (tenant_id, status, updated_at),
|
||||
@@ -24,14 +24,14 @@ CREATE TABLE IF NOT EXISTS fl_preload_plan (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_split_operation (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operation VARCHAR(40) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT 'PROCESSING/SUCCESS',
|
||||
result_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '操作目标入库单逻辑ID',
|
||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行分柜或预装写操作的平台用户ID',
|
||||
operation VARCHAR(40) NOT NULL COMMENT '分柜或预装写操作类型',
|
||||
idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键,同一操作重试复用',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS成功',
|
||||
result_json JSON NULL COMMENT '操作结果JSON快照,用于幂等重放',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等操作声明创建时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_split_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key),
|
||||
KEY idx_split_operation_time (tenant_id, inbound_id, created_at),
|
||||
@@ -96,25 +96,23 @@ DEALLOCATE PREPARE phase3_split_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_split_detail (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '部分锁定明细ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入库单ID',
|
||||
child_inbound_id BIGINT UNSIGNED NULL COMMENT '完成拆单后生成的二级子入库单ID',
|
||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '原订单货物明细ID,不建立跨库外键',
|
||||
source_package_count INT UNSIGNED NOT NULL COMMENT '拆分前该明细总箱数快照',
|
||||
locked_package_count INT UNSIGNED NOT NULL COMMENT '本次装柜锁定箱数',
|
||||
remaining_package_count INT UNSIGNED GENERATED ALWAYS AS (
|
||||
source_package_count - locked_package_count
|
||||
) STORED COMMENT '生成二级子单的剩余箱数',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'LOCKED' COMMENT 'LOCKED/COMPLETED',
|
||||
remaining_package_count INT UNSIGNED GENERATED ALWAYS AS (source_package_count - locked_package_count) STORED COMMENT '锁定后剩余箱数,由来源箱数减锁定箱数生成',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'LOCKED' COMMENT '分柜明细状态:LOCKED已锁定、COMPLETED已完成拆分',
|
||||
operation_id VARCHAR(128) NOT NULL COMMENT '部分锁定操作幂等号',
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
created_by_name VARCHAR(64) NULL,
|
||||
completed_by BIGINT UNSIGNED NULL,
|
||||
completed_by_name VARCHAR(64) NULL,
|
||||
completed_at DATETIME(3) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建部分锁定明细的操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照',
|
||||
completed_by BIGINT UNSIGNED NULL COMMENT '完成拆分的操作人ID',
|
||||
completed_by_name VARCHAR(64) NULL COMMENT '完成操作人姓名快照',
|
||||
completed_at DATETIME(3) NULL COMMENT '部分拆分完成时间',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '部分锁定明细创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '部分锁定明细最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_inbound_split_parent_detail (tenant_id, parent_inbound_id, inbound_detail_id),
|
||||
UNIQUE KEY uk_inbound_split_child_detail (tenant_id, child_inbound_id, inbound_detail_id),
|
||||
|
||||
@@ -1,33 +1,64 @@
|
||||
SET NAMES utf8mb4;
|
||||
CREATE TABLE IF NOT EXISTS fl_truck_appointment (
|
||||
id BIGINT UNSIGNED NOT NULL, appointment_no VARCHAR(40) NOT NULL, outbound_order_id BIGINT UNSIGNED NULL,
|
||||
ocean_booking_id BIGINT UNSIGNED NULL, container_booking_id BIGINT UNSIGNED NULL, loading_plan_id BIGINT UNSIGNED NULL,
|
||||
warehouse_id BIGINT UNSIGNED NULL, customer_id BIGINT UNSIGNED NULL, customer_name VARCHAR(128) NULL,
|
||||
carrier_id BIGINT UNSIGNED NULL, carrier_name VARCHAR(128) NULL, fleet_contact VARCHAR(64) NULL, fleet_phone VARCHAR(32) NULL,
|
||||
driver_name VARCHAR(64) NULL, driver_phone VARCHAR(32) NULL, driver_id_card VARCHAR(32) NULL, vehicle_no VARCHAR(32) NULL,
|
||||
store_name VARCHAR(128) NULL, store_contact VARCHAR(64) NULL, store_phone VARCHAR(32) NULL, store_address VARCHAR(255) NULL,
|
||||
store_city VARCHAR(64) NULL, store_zipcode VARCHAR(16) NULL, package_count INT UNSIGNED NULL, gross_weight_kg DECIMAL(18,3) NULL,
|
||||
volume_cbm DECIMAL(18,4) NULL, pallet_count INT UNSIGNED NULL, po_no VARCHAR(255) NULL, planned_loading_at DATETIME(3) NULL,
|
||||
actual_arrived_warehouse_at DATETIME(3) NULL, actual_loading_at DATETIME(3) NULL, actual_departed_at DATETIME(3) NULL, transit_minutes INT UNSIGNED NULL, payable_amount DECIMAL(18,2) NULL, receivable_amount DECIMAL(18,2) NULL,
|
||||
currency VARCHAR(8) NOT NULL DEFAULT 'RMB', status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', revision INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0, remark VARCHAR(1000) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_appointment_no(appointment_no),
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '拖车预约ID',
|
||||
appointment_no VARCHAR(40) NOT NULL COMMENT '平台拖车预约编号',
|
||||
outbound_order_id BIGINT UNSIGNED NULL COMMENT '关联头程订单逻辑ID;不跨库建外键',
|
||||
ocean_booking_id BIGINT UNSIGNED NULL COMMENT '关联海运订舱逻辑ID',
|
||||
container_booking_id BIGINT UNSIGNED NULL COMMENT '关联柜次逻辑ID;拖车完成供装柜门禁校验',
|
||||
loading_plan_id BIGINT UNSIGNED NULL COMMENT '关联装柜计划逻辑ID',
|
||||
warehouse_id BIGINT UNSIGNED NULL COMMENT '预约到仓的国内收货仓逻辑ID',
|
||||
customer_id BIGINT UNSIGNED NULL COMMENT '客户ID和数据隔离键',
|
||||
customer_name VARCHAR(128) NULL COMMENT '客户名称快照',
|
||||
carrier_id BIGINT UNSIGNED NULL COMMENT '拖车服务商逻辑ID',
|
||||
carrier_name VARCHAR(128) NULL COMMENT '拖车服务商名称快照',
|
||||
fleet_contact VARCHAR(64) NULL COMMENT '车队联系人',
|
||||
fleet_phone VARCHAR(32) NULL COMMENT '车队联系电话',
|
||||
driver_name VARCHAR(64) NULL COMMENT '司机姓名',
|
||||
driver_phone VARCHAR(32) NULL COMMENT '司机联系电话',
|
||||
driver_id_card VARCHAR(32) NULL COMMENT '司机身份证号;平台内部敏感信息',
|
||||
vehicle_no VARCHAR(32) NULL COMMENT '车辆车牌号',
|
||||
store_name VARCHAR(128) NULL COMMENT '到仓装柜地点名称快照',
|
||||
store_contact VARCHAR(64) NULL COMMENT '到仓地点联系人快照',
|
||||
store_phone VARCHAR(32) NULL COMMENT '到仓地点联系电话快照',
|
||||
store_address VARCHAR(255) NULL COMMENT '到仓装柜地点地址快照',
|
||||
store_city VARCHAR(64) NULL COMMENT '到仓地点城市快照',
|
||||
store_zipcode VARCHAR(16) NULL COMMENT '到仓地点邮编快照',
|
||||
package_count INT UNSIGNED NULL COMMENT '预约货物包装件数快照',
|
||||
gross_weight_kg DECIMAL(18,3) NULL COMMENT '预约货物毛重,单位千克',
|
||||
volume_cbm DECIMAL(18,4) NULL COMMENT '预约货物体积,单位立方米',
|
||||
pallet_count INT UNSIGNED NULL COMMENT '预约托盘数量',
|
||||
po_no VARCHAR(255) NULL COMMENT '关联采购单号或客户PO号',
|
||||
planned_loading_at DATETIME(3) NULL COMMENT '预约到仓装柜时间',
|
||||
actual_arrived_warehouse_at DATETIME(3) NULL COMMENT '车辆实际到达收货仓时间',
|
||||
actual_loading_at DATETIME(3) NULL COMMENT '实际开始装柜时间',
|
||||
actual_departed_at DATETIME(3) NULL COMMENT '车辆实际离仓时间;DEPARTED后供LOADED门禁使用',
|
||||
transit_minutes INT UNSIGNED NULL COMMENT '从到仓到离仓的运输作业耗时,单位分钟',
|
||||
payable_amount DECIMAL(18,2) NULL COMMENT '应付拖车服务商金额;币种由currency指定',
|
||||
receivable_amount DECIMAL(18,2) NULL COMMENT '向客户收取的拖车金额;币种由currency指定',
|
||||
currency VARCHAR(8) NOT NULL DEFAULT 'RMB' COMMENT '金额币种;首期默认RMB',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '拖车预约状态:DRAFT草稿、PENDING_DISPATCH待派车、DISPATCHED已派车、PICKUP_IN_PROGRESS提货中、ARRIVED_WAREHOUSE已到仓、LOADING装柜中、DEPARTED已离仓、COMPLETED已完成、RESCHEDULED已改约、EXCEPTION异常、CANCELLED已取消',
|
||||
revision INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '拖车预约业务修订号;每次状态或改约递增',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
remark VARCHAR(1000) NULL COMMENT '平台内部备注或异常说明',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拖车预约创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '拖车预约最后更新时间',
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_truck_appointment_no(appointment_no),
|
||||
KEY idx_truck_appointment_status(status,updated_at), KEY idx_truck_appointment_container(container_booking_id), KEY idx_truck_appointment_customer(customer_id),
|
||||
CONSTRAINT chk_truck_appointment_status CHECK(status IN ('DRAFT','PENDING_DISPATCH','DISPATCHED','PICKUP_IN_PROGRESS','ARRIVED_WAREHOUSE','LOADING','DEPARTED','COMPLETED','RESCHEDULED','EXCEPTION','CANCELLED'))
|
||||
) ENGINE=InnoDB COMMENT='三期拖车预约派车聚合';
|
||||
CREATE TABLE IF NOT EXISTS fl_truck_appointment_history (
|
||||
id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, revision INT UNSIGNED NOT NULL, action VARCHAR(32) NOT NULL,
|
||||
from_status VARCHAR(32) NULL, to_status VARCHAR(32) NULL, reason VARCHAR(500) NULL, operator_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_name VARCHAR(64) NULL, request_id VARCHAR(128) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), KEY idx_truck_history(appointment_id,revision)
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '拖车预约历史ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', revision INT UNSIGNED NOT NULL COMMENT '该预约业务修订号', action VARCHAR(32) NOT NULL COMMENT '触发状态或改约的业务动作',
|
||||
from_status VARCHAR(32) NULL COMMENT '变更前拖车状态;创建时为空', to_status VARCHAR(32) NULL COMMENT '变更后拖车状态;改约时可保持原状态', reason VARCHAR(500) NULL COMMENT '状态或改约原因', operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行操作的平台用户ID',
|
||||
operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', request_id VARCHAR(128) NULL COMMENT '请求链路中的请求ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY(id), KEY idx_truck_history(appointment_id,revision)
|
||||
) ENGINE=InnoDB COMMENT='拖车状态与改约历史';
|
||||
CREATE TABLE IF NOT EXISTS fl_truck_appointment_attachment (
|
||||
id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, file_id BIGINT UNSIGNED NOT NULL, attachment_type VARCHAR(32) NOT NULL,
|
||||
file_name VARCHAR(255) NULL, file_hash CHAR(64) NULL, visibility VARCHAR(24) NOT NULL DEFAULT 'INTERNAL', uploaded_by BIGINT UNSIGNED NOT NULL,
|
||||
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id)
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '拖车附件关系ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID', attachment_type VARCHAR(32) NOT NULL COMMENT '附件类型编码;由业务动作约定',
|
||||
file_name VARCHAR(255) NULL COMMENT '上传时文件名快照', file_hash CHAR(64) NULL COMMENT '文件内容SHA-256摘要', visibility VARCHAR(24) NOT NULL DEFAULT 'INTERNAL' COMMENT '可见范围:INTERNAL仅平台内部、CLIENT客户可见', uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传操作人ID',
|
||||
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '附件上传时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id)
|
||||
) ENGINE=InnoDB COMMENT='拖车附件文件引用';
|
||||
CREATE TABLE IF NOT EXISTS fl_truck_appointment_operation (
|
||||
id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, idempotency_key VARCHAR(128) NOT NULL, request_hash CHAR(64) NOT NULL,
|
||||
action VARCHAR(32) NOT NULL, response_snapshot JSON NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_operation(idempotency_key), KEY idx_truck_operation_appointment(appointment_id)
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '拖车写操作幂等记录ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求',
|
||||
action VARCHAR(32) NOT NULL COMMENT '执行的拖车业务动作', response_snapshot JSON NULL COMMENT '成功响应JSON快照;供相同幂等请求重放', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_operation(idempotency_key), KEY idx_truck_operation_appointment(appointment_id)
|
||||
) ENGINE=InnoDB COMMENT='拖车操作幂等记录';
|
||||
|
||||
DROP PROCEDURE IF EXISTS phase3_truck_add_column;
|
||||
@@ -54,11 +85,11 @@ DELIMITER ;
|
||||
|
||||
CALL phase3_truck_add_column(
|
||||
'actual_arrived_warehouse_at',
|
||||
'DATETIME(3) NULL AFTER planned_loading_at'
|
||||
'DATETIME(3) NULL COMMENT ''车辆实际到达收货仓时间'' AFTER planned_loading_at'
|
||||
);
|
||||
CALL phase3_truck_add_column(
|
||||
'actual_departed_at',
|
||||
'DATETIME(3) NULL AFTER actual_loading_at'
|
||||
'DATETIME(3) NULL COMMENT ''车辆实际离仓时间;DEPARTED后供LOADED门禁使用'' AFTER actual_loading_at'
|
||||
);
|
||||
DROP PROCEDURE IF EXISTS phase3_truck_add_column;
|
||||
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.firstleg.client;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FinanceFeeSourceEventClientContractTest {
|
||||
@Test
|
||||
void usesFrozenFinanceInboxPathAndIdempotencyHeaders() {
|
||||
FeignClient client = FinanceFeeSourceEventClient.class.getAnnotation(FeignClient.class);
|
||||
assertThat(client.name()).isEqualTo("finance-service");
|
||||
assertThat(client.path()).isEqualTo("/internal/finance/v1/fee-source-events");
|
||||
var method = FinanceFeeSourceEventClient.class.getDeclaredMethods()[0];
|
||||
assertThat(Arrays.stream(method.getParameterAnnotations())
|
||||
.flatMap(Arrays::stream)
|
||||
.filter(RequestHeader.class::isInstance)
|
||||
.map(RequestHeader.class::cast)
|
||||
.map(RequestHeader::value))
|
||||
.contains("X-Internal-Service-Token", "Idempotency-Key");
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.service.FinanceFeeSourceSnapshotService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class InternalFinanceFeeSourceControllerTest {
|
||||
private final FinanceFeeSourceSnapshotService service = mock(FinanceFeeSourceSnapshotService.class);
|
||||
private final InternalFinanceFeeSourceController controller = controller();
|
||||
|
||||
@Test
|
||||
void rejectsMissingOrWrongInternalTokenBeforeSnapshotLookup() {
|
||||
assertThatThrownBy(() -> controller.inbound(null, 21L, 31L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("令牌无效");
|
||||
assertThatThrownBy(() -> controller.container("wrong", 41L, 51L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("令牌无效");
|
||||
}
|
||||
|
||||
@Test
|
||||
void routesAuthorizedSnapshotLookup() {
|
||||
controller.inbound("finance-token", 21L, 31L);
|
||||
controller.container("finance-token", 41L, 51L);
|
||||
|
||||
verify(service).inbound(21L, 31L);
|
||||
verify(service).container(41L, 51L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesFrozenInternalPaths() {
|
||||
var mapping = InternalFinanceFeeSourceController.class
|
||||
.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
|
||||
assertThat(mapping.value()).containsExactly("/internal/firstleg/v1");
|
||||
assertThat(methodPath("inbound")).isEqualTo("/inbounds/{inboundId}/fees/{feeId}/source-snapshot");
|
||||
assertThat(methodPath("container")).isEqualTo("/containers/{containerId}/fees/{feeId}/source-snapshot");
|
||||
}
|
||||
|
||||
private InternalFinanceFeeSourceController controller() {
|
||||
InternalFinanceFeeSourceController value = new InternalFinanceFeeSourceController(service);
|
||||
ReflectionTestUtils.setField(value, "expectedToken", "finance-token");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String methodPath(String name) {
|
||||
return java.util.Arrays.stream(InternalFinanceFeeSourceController.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().equals(name)).findFirst().orElseThrow()
|
||||
.getAnnotation(org.springframework.web.bind.annotation.GetMapping.class).value()[0];
|
||||
}
|
||||
}
|
||||
+19
@@ -8,6 +8,8 @@ import org.apache.ibatis.annotations.Select;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -18,6 +20,9 @@ class PdaConvergenceSqlContractTest {
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment")
|
||||
.contains("assignment_type VARCHAR(16) NOT NULL COMMENT '分配类型:CLAIMED作业员领取、ASSIGNED管理员分配'")
|
||||
.contains("CONSTRAINT fk_loading_pick_item_assignment_item")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation")
|
||||
.contains("UNIQUE KEY uk_loading_pick_operation_id")
|
||||
@@ -30,6 +35,11 @@ class PdaConvergenceSqlContractTest {
|
||||
.contains("platform:firstleg:pda:assign")
|
||||
.contains("platform:firstleg:pda:work")
|
||||
.contains("platform:firstleg:pda:review");
|
||||
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertThat(createBlock(unified, "fl_loading_pick_item_assignment"))
|
||||
.isEqualTo(createBlock(sql, "fl_loading_pick_item_assignment"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,4 +61,13 @@ class PdaConvergenceSqlContractTest {
|
||||
.contains("ON DUPLICATE KEY UPDATE")
|
||||
.contains("柜次PDA自动汇总");
|
||||
}
|
||||
|
||||
private static String createBlock(String sql, String table) {
|
||||
String marker = "CREATE TABLE IF NOT EXISTS " + table + " (";
|
||||
int start = sql.indexOf(marker);
|
||||
int end = sql.indexOf(";", start);
|
||||
assertThat(start).as("missing table %s", table).isGreaterThanOrEqualTo(0);
|
||||
assertThat(end).as("missing table terminator %s", table).isGreaterThan(start);
|
||||
return sql.substring(start, end + 1).replace("\r\n", "\n");
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FinanceFeeSourceMigrationContractTest {
|
||||
@Test
|
||||
void migrationDefinesIdempotentOutboxEnvelopeAndContainerFeeModel() throws Exception {
|
||||
String sql = new ClassPathResource("db/finance-fee-source-migration.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql).contains("information_schema.columns", "event_key", "payload_hash",
|
||||
"uk_inbound_outbox_event_key", "next_retry_at", "locked_at", "locked_by",
|
||||
"idx_inbound_outbox_finance_relay", "CREATE TABLE IF NOT EXISTS fl_container_fee_detail",
|
||||
"settlement_included", "source_version", "RECEIVABLE", "PAYABLE",
|
||||
"chk_container_fee_currency", "currency = 'CNY'", "firstleg_finance_assert_schema");
|
||||
}
|
||||
}
|
||||
+141
@@ -5,11 +5,21 @@ import org.junit.jupiter.api.Test;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class Phase3FinalUnifiedSqlContractTest {
|
||||
private static final Path UNIFIED = Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
private static final Pattern CREATE_TABLE = Pattern.compile(
|
||||
"(?i)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+(?:`[^`]+`\\.)?`?([a-zA-Z0-9_]+)`?\\s*\\(");
|
||||
private static final Pattern COLUMN = Pattern.compile(
|
||||
"(?i)^\\s*`?([a-zA-Z0-9_]+)`?\\s+(bigint|binary|bit|blob|boolean|char|date|datetime|decimal|double|enum|float|int|integer|json|longblob|longtext|mediumblob|mediumint|mediumtext|numeric|real|set|smallint|text|time|timestamp|tinyblob|tinyint|tinytext|varbinary|varchar|year)\\b");
|
||||
private static final Pattern CHINESE_COMMENT = Pattern.compile(
|
||||
"(?i)\\bCOMMENT\\s*=?\\s*'(?:(?:''|\\\\'|[^'])*[\\u3400-\\u9fff](?:''|\\\\'|[^'])*)'");
|
||||
|
||||
@Test
|
||||
void unifiedEntryOwnsPhase3OutboundCustomsPortAndEvidenceHardening() throws Exception {
|
||||
@@ -27,6 +37,10 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_split_detail")
|
||||
.contains("trg_inbound_split_detail_before_insert")
|
||||
.contains("trg_inbound_split_detail_before_update")
|
||||
.contains("trg_inbound_split_detail_before_delete")
|
||||
.contains("CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH'))")
|
||||
.contains("phase3_loading_evidence_add_column")
|
||||
.contains("DROP INDEX uk_loading_evidence_type")
|
||||
@@ -74,6 +88,86 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
"iam/src/main/resources/db/phase3-warehouse-roles.sql");
|
||||
}
|
||||
|
||||
@Test
|
||||
void financeFeeSourceMigrationsAreContiguousAndSynchronised() throws Exception {
|
||||
String unified = normalise(Files.readString(resolveUnified()));
|
||||
assertBlock(unified, "-- 来源:firstleg/src/main/resources/db/finance-fee-source-migration.sql",
|
||||
"-- 来源:finance/src/main/resources/db/fee-source-event-migration.sql",
|
||||
"firstleg/src/main/resources/db/finance-fee-source-migration.sql");
|
||||
assertBlock(unified, "-- 来源:finance/src/main/resources/db/fee-source-event-migration.sql",
|
||||
"-- END FINANCE FEE SOURCE MODULE MIGRATIONS",
|
||||
"finance/src/main/resources/db/fee-source-event-migration.sql");
|
||||
|
||||
assertThat(unified)
|
||||
.contains("column_name IN ('source_type','source_no','direction','container_id','fee_item_code',")
|
||||
.contains("column_name IN ('payload_hash','result_resource_id','error_code')")
|
||||
.contains("column_name IN ('event_key', 'event_version', 'aggregate_sequence', 'payload_hash',")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_container_fee_detail");
|
||||
}
|
||||
|
||||
@Test
|
||||
void commentUpgradeIsIdempotentAndGuardsNineSchemaStructure() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE PROCEDURE queue_table_comment")
|
||||
.contains("CREATE PROCEDURE queue_column_comment")
|
||||
.contains("CREATE PROCEDURE apply_one_table_comments")
|
||||
.contains("CREATE PROCEDURE apply_queued_comments")
|
||||
.contains("NOT (BINARY v_current_comment=BINARY p_column_comment)")
|
||||
.contains("SET v_signature_before=SHA2")
|
||||
.contains("BINARY target.signature_before")
|
||||
.contains("字段注释升级遇到未知数据类型")
|
||||
.contains("字段注释升级遇到未知EXTRA定义")
|
||||
.contains("字段注释升级遇到未知生成列结构")
|
||||
.contains("MySQL DDL 会隐式提交")
|
||||
.contains("不使用 GROUP_CONCAT")
|
||||
.contains("DECLARE table_cursor CURSOR FOR")
|
||||
.contains("CALL apply_one_table_comments(v_schema_name,v_table_name)")
|
||||
.contains("CALL apply_queued_comments();")
|
||||
.contains("PREPARE table_comment_batch_stmt FROM @table_comment_batch_sql")
|
||||
.contains("九个业务数据库未完整创建")
|
||||
.contains("存在缺失中文注释的业务表")
|
||||
.contains("存在缺失中文注释的业务字段")
|
||||
.doesNotContain("CREATE PROCEDURE upgrade_column_comment");
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(150);
|
||||
assertThat(count(sql, "CALL queue_column_comment(")).isEqualTo(2456);
|
||||
assertThat(sql.indexOf("CALL apply_queued_comments();",
|
||||
sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS")))
|
||||
.isGreaterThan(sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedCreateDefinitionsHaveChineseTableAndColumnComments() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
List<String> issues = new ArrayList<>();
|
||||
Matcher tableMatcher = CREATE_TABLE.matcher(sql);
|
||||
int tableCount = 0;
|
||||
int columnCount = 0;
|
||||
while (tableMatcher.find()) {
|
||||
tableCount++;
|
||||
String table = tableMatcher.group(1);
|
||||
int open = tableMatcher.end() - 1;
|
||||
int close = matchingParen(sql, open);
|
||||
int semicolon = sql.indexOf(';', close);
|
||||
assertThat(semicolon).as("missing semicolon for %s", table).isGreaterThan(close);
|
||||
if (!CHINESE_COMMENT.matcher(sql.substring(close + 1, semicolon)).find()) {
|
||||
issues.add(table + ": table comment");
|
||||
}
|
||||
for (String definition : splitDefinitions(sql.substring(open + 1, close))) {
|
||||
Matcher columnMatcher = COLUMN.matcher(definition);
|
||||
if (!columnMatcher.find()) continue;
|
||||
columnCount++;
|
||||
if (!CHINESE_COMMENT.matcher(definition).find()) {
|
||||
issues.add(table + "." + columnMatcher.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThat(tableCount).isGreaterThan(100);
|
||||
assertThat(columnCount).isGreaterThan(2000);
|
||||
assertThat(issues).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void passIsUniqueAndOnlyTheFinalExecutableStatement() throws Exception {
|
||||
String sql = normalise(Files.readString(resolveUnified()));
|
||||
@@ -153,4 +247,51 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
private static int count(String value, String token) {
|
||||
return value.split(java.util.regex.Pattern.quote(token), -1).length - 1;
|
||||
}
|
||||
|
||||
private static int matchingParen(String sql, int open) {
|
||||
int depth = 0;
|
||||
char quote = 0;
|
||||
for (int index = open; index < sql.length(); index++) {
|
||||
char current = sql.charAt(index);
|
||||
if (quote != 0) {
|
||||
if (current == '\\') index++;
|
||||
else if (current == quote) {
|
||||
if (index + 1 < sql.length() && sql.charAt(index + 1) == quote) index++;
|
||||
else quote = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (current == '\'' || current == '"' || current == '`') quote = current;
|
||||
else if (current == '(') depth++;
|
||||
else if (current == ')' && --depth == 0) return index;
|
||||
}
|
||||
throw new AssertionError("unclosed CREATE TABLE at " + open);
|
||||
}
|
||||
|
||||
private static List<String> splitDefinitions(String body) {
|
||||
List<String> definitions = new ArrayList<>();
|
||||
int start = 0;
|
||||
int depth = 0;
|
||||
char quote = 0;
|
||||
for (int index = 0; index < body.length(); index++) {
|
||||
char current = body.charAt(index);
|
||||
if (quote != 0) {
|
||||
if (current == '\\') index++;
|
||||
else if (current == quote) {
|
||||
if (index + 1 < body.length() && body.charAt(index + 1) == quote) index++;
|
||||
else quote = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (current == '\'' || current == '"' || current == '`') quote = current;
|
||||
else if (current == '(') depth++;
|
||||
else if (current == ')') depth--;
|
||||
else if (current == ',' && depth == 0) {
|
||||
definitions.add(body.substring(start, index));
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
definitions.add(body.substring(start));
|
||||
return definitions;
|
||||
}
|
||||
}
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.ContainerFeeDetailDtos;
|
||||
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ContainerFeeDetailServiceTest {
|
||||
private final ContainerFeeDetailRepository repository = mock(ContainerFeeDetailRepository.class);
|
||||
private final SmartLoadingRepository loading = mock(SmartLoadingRepository.class);
|
||||
private final FeeItemRepository feeItems = mock(FeeItemRepository.class);
|
||||
private final FinanceFeeSourceOutboxService outbox = mock(FinanceFeeSourceOutboxService.class);
|
||||
private final ContainerFeeDetailService service =
|
||||
new ContainerFeeDetailService(repository, loading, feeItems, outbox);
|
||||
|
||||
@Test
|
||||
void rejectsContainerThatDoesNotContainCustomerOrders() {
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setId(41L);
|
||||
when(loading.plan(41L)).thenReturn(plan);
|
||||
LoadingPlanItemEntity other = new LoadingPlanItemEntity();
|
||||
other.setAccountId(20002L);
|
||||
when(loading.items(41L)).thenReturn(List.of(other));
|
||||
|
||||
assertThatThrownBy(() -> service.create(10001L, 41L, 9L, request(true)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不属于该客户");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedSettlementFeePublishesContainerSource() {
|
||||
ownedPlan();
|
||||
ContainerFeeDetailEntity approved = new ContainerFeeDetailEntity();
|
||||
approved.setId(51L);
|
||||
approved.setTenantId(10001L);
|
||||
approved.setLoadingPlanId(41L);
|
||||
approved.setSourceVersion(1);
|
||||
approved.setSettlementIncluded(true);
|
||||
when(repository.find(10001L, 41L, 51L)).thenReturn(approved);
|
||||
when(repository.review(10001L, 41L, 51L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
service.approve(10001L, 41L, 51L, 9L, "admin");
|
||||
|
||||
verify(outbox).publishContainer(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(10001L), eq(41L), eq(51L), eq(1), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adjustmentIncrementsSourceVersionAndReturnsToReview() {
|
||||
ownedPlan();
|
||||
FeeItemEntity item = item();
|
||||
when(feeItems.findItem(11L)).thenReturn(item);
|
||||
ContainerFeeDetailEntity current = new ContainerFeeDetailEntity();
|
||||
current.setId(51L);
|
||||
current.setTenantId(10001L);
|
||||
current.setLoadingPlanId(41L);
|
||||
current.setSourceVersion(1);
|
||||
when(repository.find(10001L, 41L, 51L)).thenReturn(current);
|
||||
when(repository.adjust(any(), eq(1))).thenReturn(1);
|
||||
|
||||
service.adjust(10001L, 41L, 51L, 1, 9L, request(true));
|
||||
|
||||
ArgumentCaptor<ContainerFeeDetailEntity> captor = ArgumentCaptor.forClass(ContainerFeeDetailEntity.class);
|
||||
verify(repository).adjust(captor.capture(), eq(1));
|
||||
assertThat(captor.getValue().getSourceVersion()).isEqualTo(2);
|
||||
assertThat(captor.getValue().getReviewStatus()).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
private void ownedPlan() {
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setId(41L);
|
||||
when(loading.plan(41L)).thenReturn(plan);
|
||||
LoadingPlanItemEntity item = new LoadingPlanItemEntity();
|
||||
item.setAccountId(10001L);
|
||||
when(loading.items(41L)).thenReturn(List.of(item));
|
||||
}
|
||||
|
||||
private static FeeItemEntity item() {
|
||||
FeeItemEntity value = new FeeItemEntity();
|
||||
value.setId(11L);
|
||||
value.setFeeCode("TRUCKING");
|
||||
value.setFeeTypeName("拖车费");
|
||||
value.setWarehousePayable(true);
|
||||
value.setStatus("ACTIVE");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static ContainerFeeDetailDtos.SaveRequest request(boolean settlementIncluded) {
|
||||
return new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE",
|
||||
LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY",
|
||||
settlementIncluded, "整柜拖车费");
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import static org.mockito.Mockito.when;
|
||||
@DisplayName("FBA费用状态机")
|
||||
@SpringJUnitConfig(classes = {FbaPricingService.class, FbaPricingServiceTest.TestConfig.class})
|
||||
class FbaPricingServiceTest {
|
||||
@MockBean private FinanceFeeSourceOutboxService financeOutbox;
|
||||
private static final long TENANT = 10001L;
|
||||
private static final long OPERATOR = 20001L;
|
||||
private static final long INBOUND = 30001L;
|
||||
@@ -224,7 +225,9 @@ class FbaPricingServiceTest {
|
||||
|
||||
assertThat(first.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
||||
assertThat(repeated.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
||||
verify(repository, times(1)).insertOutbox(any());
|
||||
verify(financeOutbox, times(1)).publishInbound(
|
||||
eq(FinanceFeeSourceOutboxService.PUBLISHED), eq(TENANT), eq(INBOUND),
|
||||
eq(501L), eq(1), eq(NOW));
|
||||
verify(repository, times(1)).sendFee(TENANT, 501L, OPERATOR, NOW);
|
||||
verify(feeDetails, times(2)).publishFreight(TENANT, INBOUND, 501L, OPERATOR, null, NOW);
|
||||
}
|
||||
@@ -250,7 +253,8 @@ class FbaPricingServiceTest {
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
verify(repository).markInboundFeeSent(TENANT, INBOUND, "INBOUNDED");
|
||||
verify(repository).insertOutbox(any());
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(TENANT), eq(INBOUND), eq(501L), eq(1), eq(NOW));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -300,7 +304,8 @@ class FbaPricingServiceTest {
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(inbound.getProcessStatus()).isEqualTo("INBOUNDED");
|
||||
verify(repository).supersedeSent(TENANT, 501L);
|
||||
verify(repository).insertOutbox(any());
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.ADJUSTED),
|
||||
eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW));
|
||||
}
|
||||
|
||||
private void readyRepository() {
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -40,6 +41,8 @@ class FeeManagementServiceTest {
|
||||
private FeeItemRepository repository;
|
||||
@MockBean
|
||||
private FbaPricingRepository pricingRepository;
|
||||
@MockBean
|
||||
private FinanceFeeSourceOutboxService financeOutbox;
|
||||
@Resource
|
||||
private FeeItemService feeItems;
|
||||
@Resource
|
||||
@@ -268,6 +271,54 @@ class FeeManagementServiceTest {
|
||||
verify(repository).review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核通过且参与结算的人工费用写入Finance事件")
|
||||
void publishesApprovedSettlementFee() {
|
||||
InboundFeeDetailEntity approved = new InboundFeeDetailEntity();
|
||||
approved.setId(31L);
|
||||
approved.setTenantId(10001L);
|
||||
approved.setInboundId(21L);
|
||||
approved.setFeeItemId(11L);
|
||||
approved.setSourceType("MANUAL");
|
||||
approved.setSourceFeeVersion(1);
|
||||
approved.setSettlementIncluded(true);
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(approved);
|
||||
when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
details.approve(10001L, 21L, 31L, 9L, "admin");
|
||||
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("作废已入池人工费用引用当前版本并发布VOIDED")
|
||||
void voidsPublishedSettlementFee() {
|
||||
InboundFeeDetailEntity current = new InboundFeeDetailEntity();
|
||||
current.setId(31L);
|
||||
current.setTenantId(10001L);
|
||||
current.setInboundId(21L);
|
||||
current.setFeeItemId(11L);
|
||||
current.setSourceType("MANUAL");
|
||||
current.setSourceFeeVersion(1);
|
||||
current.setSettlementIncluded(true);
|
||||
InboundFeeDetailEntity voided = new InboundFeeDetailEntity();
|
||||
voided.setId(31L);
|
||||
voided.setTenantId(10001L);
|
||||
voided.setInboundId(21L);
|
||||
voided.setFeeItemId(11L);
|
||||
voided.setSourceType("MANUAL");
|
||||
voided.setSourceFeeVersion(1);
|
||||
voided.setSettlementIncluded(true);
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(current, voided);
|
||||
when(repository.voidApproved(10001L, 21L, 31L, 1, 9L)).thenReturn(1);
|
||||
|
||||
details.voidApproved(10001L, 21L, 31L, 1, 9L);
|
||||
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.VOIDED),
|
||||
eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
private static FeeItemDtos.SaveRequest itemRequest() {
|
||||
return new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", "仓库租赁费",
|
||||
false, false, true, true, true, "应付仓库租赁费");
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.FinanceFeeSourceEventClient;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceEvent;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||
import com.baiyue.firstleg.repository.FinanceFeeSourceOutboxRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class FinanceFeeSourceOutboxRelayTest {
|
||||
private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 11, 15, 0);
|
||||
private final ObjectMapper json = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
private final FinanceFeeSourceOutboxRepository repository = mock(FinanceFeeSourceOutboxRepository.class);
|
||||
private final FinanceFeeSourceEventClient client = mock(FinanceFeeSourceEventClient.class);
|
||||
private final FinanceFeeSourceOutboxRelay relay = relay();
|
||||
|
||||
@Test
|
||||
void claimsThenPublishesOnlyAfterFinanceAcknowledgesSameEvent() throws Exception {
|
||||
InboundOutboxEntity row = event();
|
||||
ready(row);
|
||||
FinanceFeeSourceEvent envelope = json.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class);
|
||||
when(client.receive("finance-token", envelope.idempotencyKey(), envelope)).thenReturn(
|
||||
ApiResponse.success(new FinanceFeeSourceEventClient.Acknowledgement(
|
||||
901L)));
|
||||
when(repository.published(row.getId(), "worker-1", NOW)).thenReturn(true);
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).recoverStale(NOW.minusMinutes(5), NOW.plusSeconds(10), 12);
|
||||
verify(repository).claim(row.getId(), "worker-1", NOW);
|
||||
verify(client).receive("finance-token", envelope.idempotencyKey(), envelope);
|
||||
verify(repository).published(row.getId(), "worker-1", NOW);
|
||||
verify(repository, never()).retry(any(Long.class), any(), any(), any(), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentWorkerThatLosesClaimDoesNotDeliver() {
|
||||
InboundOutboxEntity row = event();
|
||||
when(repository.pending(50, NOW)).thenReturn(List.of(row));
|
||||
when(repository.claim(row.getId(), "worker-1", NOW)).thenReturn(false);
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(client, never()).receive(any(), any(), any());
|
||||
verify(repository, never()).published(any(Long.class), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transientFailureReturnsClaimToPendingWithBackoff() throws Exception {
|
||||
InboundOutboxEntity row = event();
|
||||
ready(row);
|
||||
FinanceFeeSourceEvent envelope = json.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class);
|
||||
when(client.receive("finance-token", envelope.idempotencyKey(), envelope))
|
||||
.thenThrow(new IllegalStateException("finance unavailable"));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).retry(row.getId(), "worker-1", NOW.plusSeconds(10),
|
||||
"finance unavailable", false);
|
||||
verify(repository, never()).published(any(Long.class), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidEventHashFailsPermanentlyWithoutCallingFinance() {
|
||||
InboundOutboxEntity row = event();
|
||||
row.setPayloadHash("0".repeat(64));
|
||||
ready(row);
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).retry(eq(row.getId()), eq("worker-1"), eq(NOW.plusSeconds(10)),
|
||||
any(), eq(true));
|
||||
verify(client, never()).receive(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void financeIdempotencyConflictFailsPermanently() throws Exception {
|
||||
InboundOutboxEntity row = event();
|
||||
ready(row);
|
||||
FinanceFeeSourceEvent envelope = json.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class);
|
||||
when(client.receive("finance-token", envelope.idempotencyKey(), envelope)).thenReturn(
|
||||
new ApiResponse<>("FIN-EVT-002", "同一事件ID对应不同请求内容", null, "r", "t", Instant.now()));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).retry(eq(row.getId()), eq("worker-1"), eq(NOW.plusSeconds(10)),
|
||||
any(), eq(true));
|
||||
verify(repository, never()).published(any(Long.class), any(), any());
|
||||
}
|
||||
|
||||
private void ready(InboundOutboxEntity row) {
|
||||
when(repository.pending(50, NOW)).thenReturn(List.of(row));
|
||||
when(repository.claim(row.getId(), "worker-1", NOW)).thenReturn(true);
|
||||
when(repository.findClaimed(row.getId(), "worker-1")).thenReturn(row);
|
||||
}
|
||||
|
||||
private InboundOutboxEntity event() {
|
||||
InboundOutboxMapper mapper = mock(InboundOutboxMapper.class);
|
||||
FinanceFeeSourceOutboxService producer = new FinanceFeeSourceOutboxService(mapper, json);
|
||||
producer.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED,
|
||||
10001L, 21L, 31L, 1, NOW);
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(mapper).insert(captor.capture());
|
||||
InboundOutboxEntity row = captor.getValue();
|
||||
row.setId(71L);
|
||||
return row;
|
||||
}
|
||||
|
||||
private FinanceFeeSourceOutboxRelay relay() {
|
||||
Clock clock = Clock.fixed(Instant.parse("2026-08-11T07:00:00Z"), ZoneId.of("Asia/Shanghai"));
|
||||
FinanceFeeSourceOutboxRelay value =
|
||||
new FinanceFeeSourceOutboxRelay(repository, client, json, clock, "worker-1");
|
||||
ReflectionTestUtils.setField(value, "internalToken", "finance-token");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class FinanceFeeSourceOutboxServiceTest {
|
||||
private final InboundOutboxMapper mapper = mock(InboundOutboxMapper.class);
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
private final FinanceFeeSourceOutboxService service =
|
||||
new FinanceFeeSourceOutboxService(mapper, objectMapper);
|
||||
|
||||
@Test
|
||||
void writesContractEnvelopeWithShortPayloadAndStableHash() {
|
||||
LocalDateTime occurredAt = LocalDateTime.of(2026, 8, 11, 12, 0);
|
||||
|
||||
service.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED,
|
||||
10001L, 21L, 31L, 1, occurredAt);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(mapper).insert(captor.capture());
|
||||
InboundOutboxEntity event = captor.getValue();
|
||||
assertThat(event.getEventType()).isEqualTo("FIRSTLEG_FEE_PUBLISHED");
|
||||
assertThat(event.getEventVersion()).isEqualTo(1);
|
||||
assertThat(event.getAggregateSequence()).isEqualTo(1);
|
||||
assertThat(event.getPayloadHash()).matches("[0-9a-f]{64}");
|
||||
assertThat(event.getPayloadJson()).contains("\"producer\":\"firstleg-service\"",
|
||||
"\"inboundId\":\"21\"", "\"feeId\":\"31\"", "\"feeVersion\":1")
|
||||
.doesNotContain("amount", "currency");
|
||||
}
|
||||
|
||||
@Test
|
||||
void treatsDuplicateEventKeyAsSuccessfulReplay() {
|
||||
doThrow(new DuplicateKeyException("duplicate event_key")).when(mapper)
|
||||
.insert((InboundOutboxEntity) org.mockito.ArgumentMatchers.any());
|
||||
|
||||
assertThatCode(() -> service.publishContainer(FinanceFeeSourceOutboxService.VOIDED,
|
||||
10001L, 41L, 51L, 2, LocalDateTime.now())).doesNotThrowAnyException();
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class FinanceFeeSourceSnapshotServiceTest {
|
||||
private final FeeItemRepository inboundFees = mock(FeeItemRepository.class);
|
||||
private final ContainerFeeDetailRepository containerFees = mock(ContainerFeeDetailRepository.class);
|
||||
private final FinanceFeeSourceSnapshotService service =
|
||||
new FinanceFeeSourceSnapshotService(inboundFees, containerFees);
|
||||
|
||||
@Test
|
||||
void returnsReviewedSettlementSnapshotWithStableSourceHash() {
|
||||
InboundFeeDetailEntity detail = new InboundFeeDetailEntity();
|
||||
detail.setId(31L);
|
||||
detail.setTenantId(10001L);
|
||||
detail.setInboundId(21L);
|
||||
detail.setFeeItemId(11L);
|
||||
detail.setSourceType("MANUAL");
|
||||
detail.setSourceFeeVersion(2);
|
||||
detail.setDirection("RECEIVABLE");
|
||||
detail.setAmount(new BigDecimal("100.00"));
|
||||
detail.setCurrency("CNY");
|
||||
detail.setReviewStatus("APPROVED");
|
||||
detail.setPublicationStatus("PUBLISHED");
|
||||
detail.setSettlementIncluded(true);
|
||||
when(inboundFees.findDetail(21L, 31L)).thenReturn(detail);
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
inbound.setId(21L);
|
||||
inbound.setOrderId(81L);
|
||||
inbound.setInboundCode("RK-21");
|
||||
inbound.setCustomerName("客户A");
|
||||
when(inboundFees.findInbound(10001L, 21L)).thenReturn(inbound);
|
||||
FeeItemEntity item = new FeeItemEntity();
|
||||
item.setId(11L);
|
||||
item.setFeeCode("STORAGE");
|
||||
when(inboundFees.findItem(11L)).thenReturn(item);
|
||||
|
||||
FinanceFeeSourceSnapshot snapshot = service.inbound(21L, 31L);
|
||||
|
||||
assertThat(snapshot.customerId()).isEqualTo(10001L);
|
||||
assertThat(snapshot.orderId()).isEqualTo(81L);
|
||||
assertThat(snapshot.feeVersion()).isEqualTo(2);
|
||||
assertThat(snapshot.sourceHash()).matches("[0-9a-f]{64}");
|
||||
detail.setPublicationStatus("VOIDED");
|
||||
assertThat(service.inbound(21L, 31L).sourceHash()).isEqualTo(snapshot.sourceHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsFeeThatDoesNotBelongToInboundPath() {
|
||||
InboundFeeDetailEntity detail = new InboundFeeDetailEntity();
|
||||
detail.setInboundId(22L);
|
||||
when(inboundFees.findDetail(21L, 31L)).thenReturn(detail);
|
||||
|
||||
assertThatThrownBy(() -> service.inbound(21L, 31L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不存在");
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.when;
|
||||
@DisplayName("发送费用并入库状态编排")
|
||||
@SpringJUnitConfig(InboundCompletionOrchestrator.class)
|
||||
class InboundCompletionOrchestratorTest {
|
||||
@MockBean private FinanceFeeSourceOutboxService financeOutbox;
|
||||
@MockBean
|
||||
private InboundWorkflowRepository repository;
|
||||
@MockBean
|
||||
@@ -78,6 +79,11 @@ class InboundCompletionOrchestratorTest {
|
||||
fixture.order.setProcessStatus("INBOUNDING");
|
||||
when(fixture.repository.completeInbound(10001L, 10L, 9L, 5)).thenReturn(1);
|
||||
when(fixture.repository.markFeeSent(10001L, 10L, 30L, 9L)).thenReturn(1);
|
||||
com.baiyue.firstleg.entity.InboundFeeDetailEntity published =
|
||||
new com.baiyue.firstleg.entity.InboundFeeDetailEntity();
|
||||
published.setSourceFeeVersion(1);
|
||||
when(feeDetails.publishFreight(eq(10001L), eq(10L), eq(30L), eq(9L), eq("操作员"),
|
||||
org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class))).thenReturn(published);
|
||||
|
||||
fixture.service.completeSendFeeAndInbound(
|
||||
10001L, 9L, "操作员", 10L, 30L, 5, "complete-1");
|
||||
@@ -86,7 +92,9 @@ class InboundCompletionOrchestratorTest {
|
||||
verify(fixture.repository).markFeeSent(10001L, 10L, 30L, 9L);
|
||||
verify(feeDetails).publishFreight(eq(10001L), eq(10L), eq(30L), eq(9L), eq("操作员"),
|
||||
org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class));
|
||||
verify(fixture.repository).insertOutbox(any());
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(10001L), eq(10L), eq(30L), eq(1),
|
||||
org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -102,7 +110,9 @@ class InboundCompletionOrchestratorTest {
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.CONFLICT);
|
||||
verify(fixture.repository, never()).markFeeSent(anyLong(), anyLong(), anyLong(), anyLong());
|
||||
verify(fixture.repository, never()).insertOutbox(any());
|
||||
verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(),
|
||||
org.mockito.ArgumentMatchers.anyInt(),
|
||||
org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user