This commit is contained in:
2026-08-10 15:36:47 +08:00
parent 013de5e921
commit c9f1fe0093
20 changed files with 1480 additions and 135 deletions
@@ -41,55 +41,51 @@ public class PlatformFeeItemController {
@Operation(summary = "查询费用项目")
public ApiResponse<PageResult<FeeItemEntity>> list(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@RequestParam(required = false) String category,
@RequestParam(required = false) String tradeScope,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "1") @Min(1) long page,
@RequestParam(defaultValue = "20") @Min(1) @Max(200) long size) {
return ApiResponse.success(service.list(FirstlegScopeGuard.platform(user, accountId),
category, tradeScope, keyword, status, page, size));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.list(category, tradeScope, keyword, status, page, size));
}
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('firstleg:fee-item:view')")
public ApiResponse<FeeItemEntity> detail(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id) {
return ApiResponse.success(service.detail(FirstlegScopeGuard.platform(user, accountId), id));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.detail(id));
}
@PostMapping
@BusinessAudit(event = "FEE_ITEM_CREATE", resourceType = "FEE_ITEM", description = "新增费用项目", captureBefore = false)
@PreAuthorize("hasAuthority('firstleg:fee-item:create')")
public ApiResponse<FeeItemEntity> create(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@Valid @RequestBody SaveRequest request) {
long scope = FirstlegScopeGuard.platform(user, accountId);
return ApiResponse.success(service.create(scope, user.userId(), user.username(), request));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.create(user.userId(), user.username(), request));
}
@PutMapping("/{id}")
@BusinessAudit(event = "FEE_ITEM_UPDATE", resourceType = "FEE_ITEM", resourceId = "#id", description = "编辑费用项目")
@PreAuthorize("hasAuthority('firstleg:fee-item:edit')")
public ApiResponse<FeeItemEntity> update(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id,
@Valid @RequestBody SaveRequest request) {
long scope = FirstlegScopeGuard.platform(user, accountId);
return ApiResponse.success(service.update(scope, id, user.userId(), user.username(), request));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.update(id, user.userId(), user.username(), request));
}
@PatchMapping("/{id}/status")
@BusinessAudit(event = "FEE_ITEM_STATUS_CHANGE", resourceType = "FEE_ITEM", resourceId = "#id", description = "启停费用项目")
@PreAuthorize("hasAuthority('firstleg:fee-item:edit')")
public ApiResponse<Void> changeStatus(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id,
@Valid @RequestBody StatusRequest request) {
service.changeStatus(FirstlegScopeGuard.platform(user, accountId), id,
user.userId(), user.username(), request.status());
FirstlegScopeGuard.platform(user);
service.changeStatus(id, user.userId(), user.username(), request.status());
return ApiResponse.success(null);
}
@@ -97,9 +93,9 @@ public class PlatformFeeItemController {
@BusinessAudit(event = "FEE_ITEM_DELETE", resourceType = "FEE_ITEM", resourceId = "#id", description = "删除费用项目")
@PreAuthorize("hasAuthority('firstleg:fee-item:delete')")
public ApiResponse<Void> delete(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long id) {
service.delete(FirstlegScopeGuard.platform(user, accountId), id);
FirstlegScopeGuard.platform(user);
service.delete(id);
return ApiResponse.success(null);
}
}
@@ -13,6 +13,7 @@ public final class FeeItemDtos {
public record SaveRequest(
@NotBlank @Size(max = 64) String category,
@NotBlank @Pattern(regexp = "DOMESTIC|INTERNATIONAL") String tradeScope,
@Size(max = 50) @Pattern(regexp = "[A-Za-z0-9_-]{1,50}") String feeCode,
@NotBlank @Size(max = 128) String feeTypeName,
@NotNull Boolean pickupPayable,
@NotNull Boolean pickupReceivable,
@@ -21,6 +22,14 @@ public final class FeeItemDtos {
@NotNull Boolean allocationFee,
@Size(max = 500) String description
) {
/** Backward-compatible constructor for callers predating the stable fee code. */
public SaveRequest(String category, String tradeScope, String feeTypeName,
Boolean pickupPayable, Boolean pickupReceivable,
Boolean warehousePayable, Boolean warehouseReceivable,
Boolean allocationFee, String description) {
this(category, tradeScope, null, feeTypeName, pickupPayable, pickupReceivable,
warehousePayable, warehouseReceivable, allocationFee, description);
}
}
public record StatusRequest(
@@ -27,7 +27,7 @@ public interface InboundSplitSettlementMapper {
inbound_code AS inboundCode, inbound_role AS inboundRole,
root_inbound_id AS rootInboundId,
process_status AS processStatus, total_package_count AS totalPackageCount,
package_count AS packageCount, version
package_count AS packageCount, split_status AS splitStatus, version
FROM fl_inbound_order
WHERE id = #{inboundId} AND deleted = 0
FOR UPDATE
@@ -158,7 +158,8 @@ public interface InboundSplitSettlementMapper {
@Param("expectedVersion") int expectedVersion);
@Update("UPDATE fl_inbound_order SET package_count=0,split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 "
+ "WHERE id=#{parentInboundId} AND tenant_id=#{accountId} AND inbound_role='MASTER' AND deleted=0 AND version=#{expectedVersion}")
+ "WHERE id=#{parentInboundId} AND tenant_id=#{accountId} AND inbound_role='MASTER' AND deleted=0 "
+ "AND process_status IN ('INBOUNDED','LOADING') AND version=#{expectedVersion}")
int markRootSplit(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@@ -269,11 +270,19 @@ public interface InboundSplitSettlementMapper {
record ParentRow(Long id, Long tenantId, Long orderId, String orderNo,
String inboundCode, String inboundRole, Long rootInboundId,
String processStatus, Integer totalPackageCount, Integer packageCount,
Integer version) {
String splitStatus, Integer version) {
public ParentRow(Long id, Long tenantId, String inboundCode, String inboundRole,
String processStatus, Integer totalPackageCount, Integer packageCount, Integer version) {
this(id, tenantId, 901L, "ORDER-001", inboundCode, inboundRole, id,
processStatus, totalPackageCount, packageCount, version);
processStatus, totalPackageCount, packageCount, "NOT_SPLIT", version);
}
public ParentRow(Long id, Long tenantId, Long orderId, String orderNo,
String inboundCode, String inboundRole, Long rootInboundId,
String processStatus, Integer totalPackageCount, Integer packageCount,
Integer version) {
this(id, tenantId, orderId, orderNo, inboundCode, inboundRole, rootInboundId,
processStatus, totalPackageCount, packageCount, "NOT_SPLIT", version);
}
}
@@ -20,6 +20,7 @@ import java.util.List;
/** 费用项目与入库费用明细仓储。 */
@Repository
public class FeeItemRepository {
private static final long PLATFORM_TENANT_ID = 0L;
@Resource
private FeeItemMapper feeItems;
@Resource
@@ -27,11 +28,11 @@ public class FeeItemRepository {
@Resource
private InboundOrderMapper inboundOrders;
public PageResult<FeeItemEntity> listItems(long tenantId, String category, String tradeScope,
public PageResult<FeeItemEntity> listItems(String category, String tradeScope,
String keyword, String status, long page, long size) {
return PageResult.from(feeItems.selectPage(new Page<>(page, size),
new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(StringUtils.hasText(category), FeeItemEntity::getCategory, category)
.eq(StringUtils.hasText(tradeScope), FeeItemEntity::getTradeScope, tradeScope)
.like(StringUtils.hasText(keyword), FeeItemEntity::getFeeTypeName, keyword)
@@ -41,45 +42,53 @@ public class FeeItemRepository {
.orderByDesc(FeeItemEntity::getId)));
}
public FeeItemEntity findItem(long tenantId, long id) {
public FeeItemEntity findItem(long id) {
return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getId, id));
}
public FeeItemEntity findItemByCode(long tenantId, String feeCode) {
public FeeItemEntity findItemByCode(String feeCode) {
return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getFeeCode, feeCode));
}
public boolean itemNameExists(long tenantId, String tradeScope, String feeTypeName, Long excludedId) {
public boolean itemNameExists(String tradeScope, String feeTypeName, Long excludedId) {
return feeItems.selectCount(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getTradeScope, tradeScope)
.eq(FeeItemEntity::getFeeTypeName, feeTypeName)
.ne(excludedId != null, FeeItemEntity::getId, excludedId)) > 0;
}
public boolean itemCodeExists(String feeCode, Long excludedId) {
return feeItems.selectCount(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getFeeCode, feeCode)
.ne(excludedId != null, FeeItemEntity::getId, excludedId)) > 0;
}
public void insertItem(FeeItemEntity item) {
item.setTenantId(PLATFORM_TENANT_ID);
feeItems.insert(item);
}
public int updateItem(FeeItemEntity item) {
item.setTenantId(PLATFORM_TENANT_ID);
return feeItems.update(item, new LambdaUpdateWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, item.getTenantId())
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getId, item.getId()));
}
public int deleteItem(long tenantId, long id) {
public int deleteItem(long id) {
return feeItems.delete(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTenantId, PLATFORM_TENANT_ID)
.eq(FeeItemEntity::getId, id));
}
public long countBindings(long tenantId, long feeItemId) {
public long countBindings(long feeItemId) {
return feeDetails.selectCount(new LambdaQueryWrapper<InboundFeeDetailEntity>()
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
.eq(InboundFeeDetailEntity::getFeeItemId, feeItemId));
}
@@ -7,54 +7,69 @@ import com.baiyue.firstleg.dto.FeeItemDtos.SaveRequest;
import com.baiyue.firstleg.entity.FeeItemEntity;
import com.baiyue.firstleg.repository.FeeItemRepository;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
/** 费用项目主数据应用服务。 */
@Service
public class FeeItemService {
private static final long PLATFORM_TENANT_ID = 0L;
@Resource
private FeeItemRepository repository;
public PageResult<FeeItemEntity> list(long tenantId, String category, String tradeScope,
public PageResult<FeeItemEntity> list(String category, String tradeScope,
String keyword, String status, long page, long size) {
return repository.listItems(tenantId, category, tradeScope, keyword, status, page, size);
return repository.listItems(category, tradeScope, keyword, status, page, size);
}
public FeeItemEntity detail(long tenantId, long id) {
return required(tenantId, id);
public FeeItemEntity detail(long id) {
return required(id);
}
@Transactional
public FeeItemEntity create(long tenantId, long operatorId, String operatorName, SaveRequest request) {
ensureUnique(tenantId, request, null);
public FeeItemEntity create(long operatorId, String operatorName, SaveRequest request) {
ensureUnique(request, null);
FeeItemEntity value = new FeeItemEntity();
value.setTenantId(tenantId);
value.setTenantId(PLATFORM_TENANT_ID);
copy(value, request);
if (value.getFeeCode() == null || value.getFeeCode().isBlank()) {
value.setFeeCode("MANUAL_" + UUID.randomUUID().toString().replace("-", "").substring(0, 24));
}
value.setStatus("ACTIVE");
value.setCreatedBy(operatorId);
value.setUpdatedBy(operatorId);
value.setCreatedByName(operatorName);
value.setUpdatedByName(operatorName);
repository.insertItem(value);
try {
repository.insertItem(value);
} catch (DuplicateKeyException exception) {
throw conflict("费用项目编码已存在");
}
return value;
}
@Transactional
public FeeItemEntity update(long tenantId, long id, long operatorId, String operatorName,
public FeeItemEntity update(long id, long operatorId, String operatorName,
SaveRequest request) {
FeeItemEntity value = required(tenantId, id);
ensureUnique(tenantId, request, id);
FeeItemEntity value = required(id);
ensureUnique(request, id);
copy(value, request);
value.setUpdatedBy(operatorId);
value.setUpdatedByName(operatorName);
if (repository.updateItem(value) != 1) throw conflict("费用项目已被其他人修改,请刷新后重试");
return required(tenantId, id);
try {
if (repository.updateItem(value) != 1) throw conflict("费用项目已被其他人修改,请刷新后重试");
} catch (DuplicateKeyException exception) {
throw conflict("费用项目编码已存在");
}
return required(id);
}
@Transactional
public void changeStatus(long tenantId, long id, long operatorId, String operatorName, String status) {
FeeItemEntity value = required(tenantId, id);
public void changeStatus(long id, long operatorId, String operatorName, String status) {
FeeItemEntity value = required(id);
value.setStatus(status);
value.setUpdatedBy(operatorId);
value.setUpdatedByName(operatorName);
@@ -62,22 +77,26 @@ public class FeeItemService {
}
@Transactional
public void delete(long tenantId, long id) {
required(tenantId, id);
if (repository.countBindings(tenantId, id) > 0) {
public void delete(long id) {
required(id);
if (repository.countBindings(id) > 0) {
throw conflict("费用项目已被入库费用明细使用,只能停用,不能删除");
}
if (repository.deleteItem(tenantId, id) != 1) throw conflict("费用项目删除冲突");
if (repository.deleteItem(id) != 1) throw conflict("费用项目删除冲突");
}
private FeeItemEntity required(long tenantId, long id) {
FeeItemEntity value = repository.findItem(tenantId, id);
private FeeItemEntity required(long id) {
FeeItemEntity value = repository.findItem(id);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在");
return value;
}
private void ensureUnique(long tenantId, SaveRequest request, Long excludedId) {
if (repository.itemNameExists(tenantId, request.tradeScope(), request.feeTypeName(), excludedId)) {
private void ensureUnique(SaveRequest request, Long excludedId) {
if (request.feeCode() != null && !request.feeCode().isBlank()
&& repository.itemCodeExists(request.feeCode().trim().toUpperCase(), excludedId)) {
throw conflict("费用项目编码已存在");
}
if (repository.itemNameExists(request.tradeScope(), request.feeTypeName(), excludedId)) {
throw conflict("相同国内国际范围下的费用类型名称已存在");
}
}
@@ -85,6 +104,8 @@ public class FeeItemService {
private static void copy(FeeItemEntity value, SaveRequest request) {
value.setCategory(request.category().trim());
value.setTradeScope(request.tradeScope());
value.setFeeCode(request.feeCode() == null || request.feeCode().isBlank()
? value.getFeeCode() : request.feeCode().trim().toUpperCase());
value.setFeeTypeName(request.feeTypeName().trim());
value.setPickupPayable(request.pickupPayable());
value.setPickupReceivable(request.pickupReceivable());
@@ -22,6 +22,7 @@ import java.util.List;
/** 入库单人工费用明细应用服务。 */
@Service
public class InboundFeeDetailService {
private static final long PLATFORM_TENANT_ID = 0L;
private static final String FREIGHT_FEE_CODE = "FBA_HEAD_FREIGHT";
@Resource
private FeeItemRepository repository;
@@ -56,7 +57,7 @@ public class InboundFeeDetailService {
throw conflict("只有已发送状态的计费快照才能生成客户运费明细");
}
LocalDateTime now = publishedAt == null ? LocalDateTime.now() : publishedAt;
FeeItemEntity feeItem = requiredFreightItem(tenantId, operatorId, operatorName);
FeeItemEntity feeItem = requiredFreightItem(operatorId, operatorName);
InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, feeId);
repository.supersedePublishedFreight(tenantId, inboundId, feeId, operatorId, now);
@@ -123,7 +124,7 @@ public class InboundFeeDetailService {
public InboundFeeDetailEntity create(long tenantId, long inboundId, long operatorId,
SaveRequest request) {
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
FeeItemEntity item = requiredActiveItem(tenantId, request.feeItemId());
FeeItemEntity item = requiredActiveItem(request.feeItemId());
InboundFeeDetailEntity value = new InboundFeeDetailEntity();
value.setTenantId(tenantId);
value.setInboundId(inboundId);
@@ -142,7 +143,7 @@ public class InboundFeeDetailService {
long operatorId, SaveRequest request) {
InboundFeeDetailEntity value = requiredEditable(tenantId, inboundId, detailId);
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
FeeItemEntity item = requiredActiveItem(tenantId, request.feeItemId());
FeeItemEntity item = requiredActiveItem(request.feeItemId());
apply(value, inbound, item, request, operatorId);
value.setReviewStatus("PENDING");
value.setReviewerId(null);
@@ -185,21 +186,21 @@ public class InboundFeeDetailService {
return value;
}
private FeeItemEntity requiredActiveItem(long tenantId, long itemId) {
FeeItemEntity value = repository.findItem(tenantId, itemId);
private FeeItemEntity requiredActiveItem(long itemId) {
FeeItemEntity value = repository.findItem(itemId);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在");
if (!"ACTIVE".equals(value.getStatus())) throw conflict("费用项目已停用,不能绑定");
return value;
}
private FeeItemEntity requiredFreightItem(long tenantId, long operatorId, String operatorName) {
FeeItemEntity item = repository.findItemByCode(tenantId, FREIGHT_FEE_CODE);
private FeeItemEntity requiredFreightItem(long operatorId, String operatorName) {
FeeItemEntity item = repository.findItemByCode(FREIGHT_FEE_CODE);
if (item != null) {
if (!"ACTIVE".equals(item.getStatus())) throw conflict("系统费用项目FBA头程运费已停用");
return item;
}
FeeItemEntity created = new FeeItemEntity();
created.setTenantId(tenantId);
created.setTenantId(PLATFORM_TENANT_ID);
created.setCategory("头程运费");
created.setTradeScope("INTERNATIONAL");
created.setFeeCode(FREIGHT_FEE_CODE);
@@ -219,8 +220,13 @@ public class InboundFeeDetailService {
repository.insertItem(created);
return created;
} catch (DuplicateKeyException exception) {
FeeItemEntity concurrent = repository.findItemByCode(tenantId, FREIGHT_FEE_CODE);
if (concurrent != null) return concurrent;
FeeItemEntity concurrent = repository.findItemByCode(FREIGHT_FEE_CODE);
if (concurrent != null) {
if (!"ACTIVE".equals(concurrent.getStatus())) {
throw conflict("系统费用项目FBA头程运费已停用");
}
return concurrent;
}
throw exception;
}
}
@@ -98,7 +98,8 @@ public class PdaPickSettlementService {
ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId);
if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0
|| !("MASTER".equals(parent.inboundRole()) || "CHILD".equals(parent.inboundRole()))
|| !"INBOUNDED".equals(parent.processStatus())) {
|| !("INBOUNDED".equals(parent.processStatus()) || "LOADING".equals(parent.processStatus()))
|| ("MASTER".equals(parent.inboundRole()) && "FULLY_SPLIT".equals(parent.splitStatus()))) {
throw conflict("待拆分主入库单不在可拆分状态");
}
String operationId = "PDA-SPLIT-" + taskId + '-' + parentInboundId;
@@ -109,7 +110,7 @@ public class PdaPickSettlementService {
long loadingPlanId = task.getLoadingPlanId();
Long containerBookingId = splits.containerBookingId(loadingPlanId);
if (containerBookingId == null) throw conflict("PDA任务装柜计划缺少真实柜次");
int parentAvailable = count(parent.packageCount(), "主入库单可用箱数");
int parentAvailable = availablePackageCount(parent);
if (pickedPackageCount <= 0 || pickedPackageCount > parentAvailable) {
throw conflict("本次已拣箱数与主入库单可用箱数不一致");
}
@@ -457,6 +458,14 @@ public class PdaPickSettlementService {
return value;
}
private static int availablePackageCount(ParentRow parent) {
if ("FULLY_SPLIT".equals(parent.splitStatus())) return 0;
Integer available = parent.packageCount();
if (available != null && available > 0) return available;
if (available != null && available == 0 && !"NOT_SPLIT".equals(parent.splitStatus())) return 0;
return count(parent.totalPackageCount(), "主入库单可用箱数");
}
private static void requireAffected(int affected, String resource) {
if (affected != 1) {
throw conflict(resource + "已被并发修改,请刷新后重试");