日志记录提交

This commit is contained in:
2026-08-05 14:20:04 +08:00
parent 264458920b
commit 06f7f9d2f2
83 changed files with 3256 additions and 27 deletions
@@ -0,0 +1,105 @@
package com.baiyue.firstleg.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.FeeItemDtos.SaveRequest;
import com.baiyue.firstleg.dto.FeeItemDtos.StatusRequest;
import com.baiyue.firstleg.entity.FeeItemEntity;
import com.baiyue.firstleg.service.FeeItemService;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** 平台端费用项目维护接口。 */
@Tag(name = "费用项目")
@RestController
@RequestMapping("/api/platform/v1/fee-items")
public class PlatformFeeItemController {
@Resource
private FeeItemService service;
@GetMapping
@PreAuthorize("hasAuthority('firstleg:fee-item:view')")
@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));
}
@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));
}
@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));
}
@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));
}
@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());
return ApiResponse.success(null);
}
@DeleteMapping("/{id}")
@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);
return ApiResponse.success(null);
}
}
@@ -0,0 +1,107 @@
package com.baiyue.firstleg.controller;
import com.baiyue.common.core.api.ApiResponse;
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.entity.InboundFeeDetailEntity;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import com.baiyue.firstleg.service.InboundFeeDetailService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** 平台端入库单费用明细接口。 */
@Tag(name = "入库单费用明细")
@RestController
@RequestMapping("/api/platform/v1/inbounds/{inboundId}/fee-details")
public class PlatformInboundFeeDetailController {
@Resource
private InboundFeeDetailService service;
@GetMapping
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:view')")
@Operation(summary = "查询入库单费用明细")
public ApiResponse<List<InboundFeeDetailEntity>> list(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long inboundId) {
return ApiResponse.success(service.list(FirstlegScopeGuard.platform(user, accountId), inboundId));
}
@PostMapping
@BusinessAudit(event = "INBOUND_FEE_DETAIL_CREATE", resourceType = "INBOUND", resourceId = "#inboundId", description = "新增入库费用明细", captureBefore = false)
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:edit')")
public ApiResponse<InboundFeeDetailEntity> create(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long inboundId,
@Valid @RequestBody SaveRequest request) {
long scope = FirstlegScopeGuard.platform(user, accountId);
return ApiResponse.success(service.create(scope, inboundId, user.userId(), request));
}
@PutMapping("/{detailId}")
@BusinessAudit(event = "INBOUND_FEE_DETAIL_UPDATE", resourceType = "INBOUND_FEE_DETAIL", resourceId = "#detailId", description = "编辑入库费用明细")
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:edit')")
public ApiResponse<InboundFeeDetailEntity> update(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long inboundId,
@PathVariable long detailId,
@Valid @RequestBody SaveRequest request) {
long scope = FirstlegScopeGuard.platform(user, accountId);
return ApiResponse.success(service.update(scope, inboundId, detailId, user.userId(), request));
}
@DeleteMapping("/{detailId}")
@BusinessAudit(event = "INBOUND_FEE_DETAIL_DELETE", resourceType = "INBOUND_FEE_DETAIL", resourceId = "#detailId", description = "删除入库费用明细")
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:delete')")
public ApiResponse<Void> delete(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long inboundId,
@PathVariable long detailId) {
service.delete(FirstlegScopeGuard.platform(user, accountId), inboundId, detailId);
return ApiResponse.success(null);
}
@PostMapping("/{detailId}/approve")
@BusinessAudit(event = "INBOUND_FEE_DETAIL_APPROVE", resourceType = "INBOUND_FEE_DETAIL", resourceId = "#detailId", description = "审核通过入库费用明细")
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')")
public ApiResponse<Void> approve(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long inboundId,
@PathVariable long detailId) {
service.approve(FirstlegScopeGuard.platform(user, accountId), inboundId, detailId,
user.userId(), user.username());
return ApiResponse.success(null);
}
@PostMapping("/{detailId}/reject")
@BusinessAudit(event = "INBOUND_FEE_DETAIL_REJECT", resourceType = "INBOUND_FEE_DETAIL", resourceId = "#detailId", description = "驳回入库费用明细")
@PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')")
public ApiResponse<Void> reject(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@PathVariable long inboundId,
@PathVariable long detailId,
@Valid @RequestBody RejectRequest request) {
service.reject(FirstlegScopeGuard.platform(user, accountId), inboundId, detailId,
user.userId(), user.username(), request.reason());
return ApiResponse.success(null);
}
}
@@ -0,0 +1,30 @@
package com.baiyue.firstleg.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
/** 费用项目请求模型。 */
public final class FeeItemDtos {
private FeeItemDtos() {
}
public record SaveRequest(
@NotBlank @Size(max = 64) String category,
@NotBlank @Pattern(regexp = "DOMESTIC|INTERNATIONAL") String tradeScope,
@NotBlank @Size(max = 128) String feeTypeName,
@NotNull Boolean pickupPayable,
@NotNull Boolean pickupReceivable,
@NotNull Boolean warehousePayable,
@NotNull Boolean warehouseReceivable,
@NotNull Boolean allocationFee,
@Size(max = 500) String description
) {
}
public record StatusRequest(
@NotBlank @Pattern(regexp = "ACTIVE|DISABLED") String status
) {
}
}
@@ -0,0 +1,30 @@
package com.baiyue.firstleg.dto;
import jakarta.validation.constraints.DecimalMin;
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 InboundFeeDetailDtos {
private InboundFeeDetailDtos() {
}
public record SaveRequest(
@NotNull Long feeItemId,
@NotBlank @Pattern(regexp = "RECEIVABLE|PAYABLE") String direction,
@NotNull LocalDate businessDate,
@NotNull @DecimalMin("0.00") BigDecimal unitPrice,
@NotBlank @Pattern(regexp = "[A-Z]{3}") String currency,
@NotNull @DecimalMin("0.00") BigDecimal amount,
@Size(max = 500) String remark
) {
}
public record RejectRequest(@NotBlank @Size(max = 500) String reason) {
}
}
@@ -0,0 +1,30 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baiyue.common.core.model.BaseEntity;
import lombok.Getter;
import lombok.Setter;
/** 费用项目主数据。 */
@Getter
@Setter
@TableName("fl_fee_item")
public class FeeItemEntity extends BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private String category;
private String tradeScope;
private String feeTypeName;
private Boolean pickupPayable;
private Boolean pickupReceivable;
private Boolean warehousePayable;
private Boolean warehouseReceivable;
private Boolean allocationFee;
private String description;
private String status;
private String createdByName;
private String updatedByName;
}
@@ -0,0 +1,49 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baiyue.common.core.model.BaseEntity;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/** 入库单人工费用明细及业务快照。 */
@Getter
@Setter
@TableName("fl_inbound_fee_detail")
public class InboundFeeDetailEntity extends BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long inboundId;
private Long feeItemId;
private String inboundCode;
private String direction;
private LocalDate businessDate;
private String feeCategorySnapshot;
private String tradeScopeSnapshot;
private String feeTypeNameSnapshot;
private String channelCode;
private String channelName;
private String destinationCode;
private String destinationName;
private Integer packageCount;
private BigDecimal actualGrossWeightKg;
private BigDecimal actualVolumeCbm;
private BigDecimal volumeWeightKg;
private BigDecimal chargeableWeightKg;
private BigDecimal chargeableVolumeCbm;
private BigDecimal unitPrice;
private String currency;
private BigDecimal amount;
private String remark;
private String reviewStatus;
private Long reviewerId;
private String reviewerName;
private LocalDateTime reviewedAt;
private String reviewReason;
}
@@ -0,0 +1,9 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.entity.FeeItemEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FeeItemMapper extends BaseMapper<FeeItemEntity> {
}
@@ -0,0 +1,9 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface InboundFeeDetailMapper extends BaseMapper<InboundFeeDetailEntity> {
}
@@ -0,0 +1,123 @@
package com.baiyue.firstleg.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.firstleg.entity.FeeItemEntity;
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.mapper.FeeItemMapper;
import com.baiyue.firstleg.mapper.InboundFeeDetailMapper;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
/** 费用项目与入库费用明细仓储。 */
@Repository
public class FeeItemRepository {
@Resource
private FeeItemMapper feeItems;
@Resource
private InboundFeeDetailMapper feeDetails;
@Resource
private InboundOrderMapper inboundOrders;
public PageResult<FeeItemEntity> listItems(long tenantId, 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(StringUtils.hasText(category), FeeItemEntity::getCategory, category)
.eq(StringUtils.hasText(tradeScope), FeeItemEntity::getTradeScope, tradeScope)
.like(StringUtils.hasText(keyword), FeeItemEntity::getFeeTypeName, keyword)
.eq(StringUtils.hasText(status), FeeItemEntity::getStatus, status)
.orderByAsc(FeeItemEntity::getCategory)
.orderByAsc(FeeItemEntity::getFeeTypeName)
.orderByDesc(FeeItemEntity::getId)));
}
public FeeItemEntity findItem(long tenantId, long id) {
return feeItems.selectOne(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getId, id));
}
public boolean itemNameExists(long tenantId, String tradeScope, String feeTypeName, Long excludedId) {
return feeItems.selectCount(new LambdaQueryWrapper<FeeItemEntity>()
.eq(FeeItemEntity::getTenantId, tenantId)
.eq(FeeItemEntity::getTradeScope, tradeScope)
.eq(FeeItemEntity::getFeeTypeName, feeTypeName)
.ne(excludedId != null, FeeItemEntity::getId, excludedId)) > 0;
}
public void insertItem(FeeItemEntity item) {
feeItems.insert(item);
}
public int updateItem(FeeItemEntity item) {
return feeItems.updateById(item);
}
public int deleteItem(long id) {
return feeItems.deleteById(id);
}
public long countBindings(long tenantId, long feeItemId) {
return feeDetails.selectCount(new LambdaQueryWrapper<InboundFeeDetailEntity>()
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
.eq(InboundFeeDetailEntity::getFeeItemId, feeItemId));
}
public InboundOrderEntity findInbound(long tenantId, long inboundId) {
return inboundOrders.selectOne(new LambdaQueryWrapper<InboundOrderEntity>()
.eq(InboundOrderEntity::getTenantId, tenantId)
.eq(InboundOrderEntity::getId, inboundId));
}
public List<InboundFeeDetailEntity> listDetails(long tenantId, long inboundId) {
return feeDetails.selectList(new LambdaQueryWrapper<InboundFeeDetailEntity>()
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
.eq(InboundFeeDetailEntity::getInboundId, inboundId)
.orderByDesc(InboundFeeDetailEntity::getBusinessDate)
.orderByDesc(InboundFeeDetailEntity::getId));
}
public InboundFeeDetailEntity findDetail(long tenantId, long inboundId, long detailId) {
return feeDetails.selectOne(new LambdaQueryWrapper<InboundFeeDetailEntity>()
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
.eq(InboundFeeDetailEntity::getInboundId, inboundId)
.eq(InboundFeeDetailEntity::getId, detailId));
}
public void insertDetail(InboundFeeDetailEntity detail) {
feeDetails.insert(detail);
}
public int updateDetail(InboundFeeDetailEntity detail) {
return feeDetails.updateById(detail);
}
public int deleteDetail(long detailId) {
return feeDetails.deleteById(detailId);
}
public int review(long tenantId, long inboundId, long detailId, String targetStatus,
long reviewerId, String reviewerName, String reason) {
return feeDetails.update(null, new LambdaUpdateWrapper<InboundFeeDetailEntity>()
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
.eq(InboundFeeDetailEntity::getInboundId, inboundId)
.eq(InboundFeeDetailEntity::getId, detailId)
.eq(InboundFeeDetailEntity::getReviewStatus, "PENDING")
.set(InboundFeeDetailEntity::getReviewStatus, targetStatus)
.set(InboundFeeDetailEntity::getReviewerId, reviewerId)
.set(InboundFeeDetailEntity::getReviewerName, reviewerName)
.set(InboundFeeDetailEntity::getReviewedAt, LocalDateTime.now())
.set(InboundFeeDetailEntity::getReviewReason, reason)
.set(InboundFeeDetailEntity::getUpdatedBy, reviewerId));
}
}
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
@@ -18,6 +19,8 @@ public class AutomaticInboundCompletionService {
@Resource
private InboundCompletionOrchestrator completion;
@BusinessAudit(event = "INBOUND_AUTO_COMPLETE", resourceType = "INBOUND", resourceId = "#inboundId",
description = "测量审核通过后自动计费、发送费用并完成入库", captureBefore = false)
public FeeSnapshotResponse completeApproved(long tenantId, long inboundId, long operatorId,
String operatorName, long measurementId,
int reviewedOrderVersion) {
@@ -0,0 +1,100 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.exception.BizException;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** 费用项目主数据应用服务。 */
@Service
public class FeeItemService {
@Resource
private FeeItemRepository repository;
public PageResult<FeeItemEntity> list(long tenantId, String category, String tradeScope,
String keyword, String status, long page, long size) {
return repository.listItems(tenantId, category, tradeScope, keyword, status, page, size);
}
public FeeItemEntity detail(long tenantId, long id) {
return required(tenantId, id);
}
@Transactional
public FeeItemEntity create(long tenantId, long operatorId, String operatorName, SaveRequest request) {
ensureUnique(tenantId, request, null);
FeeItemEntity value = new FeeItemEntity();
value.setTenantId(tenantId);
copy(value, request);
value.setStatus("ACTIVE");
value.setCreatedBy(operatorId);
value.setUpdatedBy(operatorId);
value.setCreatedByName(operatorName);
value.setUpdatedByName(operatorName);
repository.insertItem(value);
return value;
}
@Transactional
public FeeItemEntity update(long tenantId, long id, long operatorId, String operatorName,
SaveRequest request) {
FeeItemEntity value = required(tenantId, id);
ensureUnique(tenantId, request, id);
copy(value, request);
value.setUpdatedBy(operatorId);
value.setUpdatedByName(operatorName);
if (repository.updateItem(value) != 1) throw conflict("费用项目已被其他人修改,请刷新后重试");
return required(tenantId, id);
}
@Transactional
public void changeStatus(long tenantId, long id, long operatorId, String operatorName, String status) {
FeeItemEntity value = required(tenantId, id);
value.setStatus(status);
value.setUpdatedBy(operatorId);
value.setUpdatedByName(operatorName);
if (repository.updateItem(value) != 1) throw conflict("费用项目状态更新冲突");
}
@Transactional
public void delete(long tenantId, long id) {
required(tenantId, id);
if (repository.countBindings(tenantId, id) > 0) {
throw conflict("费用项目已被入库费用明细使用,只能停用,不能删除");
}
if (repository.deleteItem(id) != 1) throw conflict("费用项目删除冲突");
}
private FeeItemEntity required(long tenantId, long id) {
FeeItemEntity value = repository.findItem(tenantId, 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)) {
throw conflict("相同国内国际范围下的费用类型名称已存在");
}
}
private static void copy(FeeItemEntity value, SaveRequest request) {
value.setCategory(request.category().trim());
value.setTradeScope(request.tradeScope());
value.setFeeTypeName(request.feeTypeName().trim());
value.setPickupPayable(request.pickupPayable());
value.setPickupReceivable(request.pickupReceivable());
value.setWarehousePayable(request.warehousePayable());
value.setWarehouseReceivable(request.warehouseReceivable());
value.setAllocationFee(request.allocationFee());
value.setDescription(request.description() == null ? null : request.description().trim());
}
private static BizException conflict(String message) {
return new BizException(ResultCode.CONFLICT, message);
}
}
@@ -0,0 +1,149 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.InboundFeeDetailDtos.SaveRequest;
import com.baiyue.firstleg.entity.FeeItemEntity;
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.FeeItemRepository;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.RoundingMode;
import java.util.List;
/** 入库单人工费用明细应用服务。 */
@Service
public class InboundFeeDetailService {
@Resource
private FeeItemRepository repository;
public List<InboundFeeDetailEntity> list(long tenantId, long inboundId) {
requiredInbound(tenantId, inboundId);
return repository.listDetails(tenantId, inboundId);
}
@Transactional
public InboundFeeDetailEntity create(long tenantId, long inboundId, long operatorId,
SaveRequest request) {
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
FeeItemEntity item = requiredActiveItem(tenantId, request.feeItemId());
InboundFeeDetailEntity value = new InboundFeeDetailEntity();
value.setTenantId(tenantId);
value.setInboundId(inboundId);
value.setCreatedBy(operatorId);
apply(value, inbound, item, request, operatorId);
value.setReviewStatus("PENDING");
repository.insertDetail(value);
return value;
}
@Transactional
public InboundFeeDetailEntity update(long tenantId, long inboundId, long detailId,
long operatorId, SaveRequest request) {
InboundFeeDetailEntity value = requiredEditable(tenantId, inboundId, detailId);
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
FeeItemEntity item = requiredActiveItem(tenantId, request.feeItemId());
apply(value, inbound, item, request, operatorId);
value.setReviewStatus("PENDING");
value.setReviewerId(null);
value.setReviewerName(null);
value.setReviewedAt(null);
value.setReviewReason(null);
if (repository.updateDetail(value) != 1) throw conflict("费用明细已被其他人修改,请刷新后重试");
return required(tenantId, inboundId, detailId);
}
@Transactional
public void delete(long tenantId, long inboundId, long detailId) {
requiredEditable(tenantId, inboundId, detailId);
if (repository.deleteDetail(detailId) != 1) throw conflict("费用明细删除冲突");
}
@Transactional
public void approve(long tenantId, long inboundId, long detailId,
long reviewerId, String reviewerName) {
required(tenantId, inboundId, detailId);
if (repository.review(tenantId, inboundId, detailId, "APPROVED",
reviewerId, reviewerName, null) != 1) {
throw conflict("只有待审核费用明细可以审核通过");
}
}
@Transactional
public void reject(long tenantId, long inboundId, long detailId, long reviewerId,
String reviewerName, String reason) {
required(tenantId, inboundId, detailId);
if (repository.review(tenantId, inboundId, detailId, "REJECTED",
reviewerId, reviewerName, reason.trim()) != 1) {
throw conflict("只有待审核费用明细可以驳回");
}
}
private InboundOrderEntity requiredInbound(long tenantId, long inboundId) {
InboundOrderEntity value = repository.findInbound(tenantId, inboundId);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
return value;
}
private FeeItemEntity requiredActiveItem(long tenantId, long itemId) {
FeeItemEntity value = repository.findItem(tenantId, itemId);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在");
if (!"ACTIVE".equals(value.getStatus())) throw conflict("费用项目已停用,不能绑定");
return value;
}
private static void ensureWarehouseApplicable(FeeItemEntity item, String direction) {
boolean applicable = "RECEIVABLE".equals(direction)
? Boolean.TRUE.equals(item.getWarehouseReceivable())
: Boolean.TRUE.equals(item.getWarehousePayable());
if (!applicable) {
throw conflict("所选费用项目未启用对应的仓储应收/应付,不能绑定入库单");
}
}
private InboundFeeDetailEntity required(long tenantId, long inboundId, long detailId) {
InboundFeeDetailEntity value = repository.findDetail(tenantId, inboundId, detailId);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "入库费用明细不存在");
return value;
}
private InboundFeeDetailEntity requiredEditable(long tenantId, long inboundId, long detailId) {
InboundFeeDetailEntity value = required(tenantId, inboundId, detailId);
if ("APPROVED".equals(value.getReviewStatus())) throw conflict("审核通过的费用明细不能修改或删除");
return value;
}
private static void apply(InboundFeeDetailEntity value, InboundOrderEntity inbound,
FeeItemEntity item, SaveRequest request, long operatorId) {
ensureWarehouseApplicable(item, request.direction());
value.setFeeItemId(item.getId());
value.setInboundCode(inbound.getInboundCode());
value.setDirection(request.direction());
value.setBusinessDate(request.businessDate());
value.setFeeCategorySnapshot(item.getCategory());
value.setTradeScopeSnapshot(item.getTradeScope());
value.setFeeTypeNameSnapshot(item.getFeeTypeName());
value.setChannelCode(inbound.getChannelCode());
value.setChannelName(inbound.getChannelName());
value.setDestinationCode(inbound.getDestinationCode());
value.setDestinationName(inbound.getDestinationName());
value.setPackageCount(inbound.getTotalPackageCount());
value.setActualGrossWeightKg(inbound.getActualGrossWeightKg());
value.setActualVolumeCbm(inbound.getActualVolumeCbm());
value.setVolumeWeightKg(inbound.getVolumeWeightKg());
value.setChargeableWeightKg(inbound.getChargeableWeightKg());
value.setChargeableVolumeCbm(inbound.getChargeableVolumeCbm());
value.setUnitPrice(request.unitPrice().setScale(4, RoundingMode.HALF_UP));
value.setCurrency(request.currency());
value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP));
value.setRemark(request.remark() == null ? null : request.remark().trim());
value.setUpdatedBy(operatorId);
}
private static BizException conflict(String message) {
return new BizException(ResultCode.CONFLICT, message);
}
}