日志记录提交
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+107
@@ -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));
|
||||
}
|
||||
}
|
||||
+3
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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),
|
||||
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='可靠审计事件本地发送箱';
|
||||
@@ -0,0 +1,72 @@
|
||||
USE baiyue_firstleg;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_fee_item (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '费用项目ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '账号ID,沿用tenant_id字段名',
|
||||
category VARCHAR(64) NOT NULL COMMENT '费用分类',
|
||||
trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际',
|
||||
fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称',
|
||||
pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与',
|
||||
pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与',
|
||||
warehouse_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '仓储应付是否参与',
|
||||
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,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_fee_item_query (tenant_id, trade_scope, category, status, deleted),
|
||||
KEY idx_fee_item_name (tenant_id, fee_type_name, deleted)
|
||||
) ENGINE=InnoDB COMMENT='费用项目主数据';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库费用明细ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '账号ID,沿用tenant_id字段名',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '绑定费用项目ID',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
||||
direction VARCHAR(16) NOT NULL COMMENT 'RECEIVABLE应收、PAYABLE应付',
|
||||
business_date DATE NOT NULL COMMENT '业务发生日',
|
||||
fee_category_snapshot VARCHAR(64) NOT NULL COMMENT '费用分类快照',
|
||||
trade_scope_snapshot VARCHAR(20) NOT NULL COMMENT '国内国际快照',
|
||||
fee_type_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用类型名称快照',
|
||||
channel_code VARCHAR(64) NULL COMMENT '渠道编码快照',
|
||||
channel_name VARCHAR(128) NULL COMMENT '渠道名称快照',
|
||||
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 '金额',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
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,
|
||||
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),
|
||||
KEY idx_inbound_fee_item (tenant_id, fee_item_id, deleted),
|
||||
CONSTRAINT fk_inbound_fee_detail_order FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id),
|
||||
CONSTRAINT fk_inbound_fee_detail_item FOREIGN KEY (fee_item_id) REFERENCES fl_fee_item (id)
|
||||
) ENGINE=InnoDB COMMENT='入库单应收应付费用明细';
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("费用管理接口权限契约")
|
||||
class FeeManagementContractTest {
|
||||
@Test
|
||||
@DisplayName("通过和驳回共用审核按钮权限")
|
||||
void protectsReviewActions() {
|
||||
for (String methodName : new String[] {"approve", "reject"}) {
|
||||
Method method = Arrays.stream(PlatformInboundFeeDetailController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(methodName))
|
||||
.findFirst().orElseThrow();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('firstleg:inbound:fee-detail:review')");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("费用项目增删改查权限彼此独立")
|
||||
void separatesCrudPermissions() {
|
||||
assertPermission("list", "firstleg:fee-item:view");
|
||||
assertPermission("detail", "firstleg:fee-item:view");
|
||||
assertPermission("create", "firstleg:fee-item:create");
|
||||
assertPermission("update", "firstleg:fee-item:edit");
|
||||
assertPermission("changeStatus", "firstleg:fee-item:edit");
|
||||
assertPermission("delete", "firstleg:fee-item:delete");
|
||||
}
|
||||
|
||||
private static void assertPermission(String methodName, String permission) {
|
||||
Method method = Arrays.stream(PlatformFeeItemController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(methodName))
|
||||
.findFirst().orElseThrow();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + permission + "')");
|
||||
}
|
||||
}
|
||||
+13
@@ -1,8 +1,10 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
|
||||
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
|
||||
import com.baiyue.firstleg.service.AutomaticInboundCompletionService;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
@@ -43,6 +45,17 @@ class InboundMeasurementReviewContractTest {
|
||||
.doesNotContain("待客户确认");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAuditAutomaticInboundCompletion() throws Exception {
|
||||
Method method = AutomaticInboundCompletionService.class.getDeclaredMethod("completeApproved",
|
||||
long.class, long.class, long.class, String.class, long.class, int.class);
|
||||
BusinessAudit audit = method.getAnnotation(BusinessAudit.class);
|
||||
assertThat(audit.event()).isEqualTo("INBOUND_AUTO_COMPLETE");
|
||||
assertThat(audit.resourceType()).isEqualTo("INBOUND");
|
||||
assertThat(audit.resourceId()).isEqualTo("#inboundId");
|
||||
assertThat(audit.captureBefore()).isFalse();
|
||||
}
|
||||
|
||||
private void assertAuthority(String name, String expression, Class<?>... types) throws Exception {
|
||||
Method method = PlatformInboundMeasurementReviewController.class.getDeclaredMethod(name, types);
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value()).isEqualTo(expression);
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FeeItemDtos;
|
||||
import com.baiyue.firstleg.dto.InboundFeeDetailDtos;
|
||||
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.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
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.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("费用项目和入库费用明细")
|
||||
@SpringJUnitConfig({FeeItemService.class, InboundFeeDetailService.class})
|
||||
class FeeManagementServiceTest {
|
||||
@MockBean
|
||||
private FeeItemRepository repository;
|
||||
@Resource
|
||||
private FeeItemService feeItems;
|
||||
@Resource
|
||||
private InboundFeeDetailService details;
|
||||
|
||||
@Test
|
||||
@DisplayName("费用项目列表返回分页结果")
|
||||
void pagesItems() {
|
||||
PageResult<FeeItemEntity> expected = new PageResult<>(List.of(), 2, 20, 21, 2);
|
||||
when(repository.listItems(10001L, "仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20))
|
||||
.thenReturn(expected);
|
||||
|
||||
assertThat(feeItems.list(10001L, "仓储成本", "INTERNATIONAL", "人工", "ACTIVE", 2, 20))
|
||||
.isSameAs(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("新增费用项目保存原型字段")
|
||||
void createsItem() {
|
||||
doAnswer(invocation -> {
|
||||
((FeeItemEntity) invocation.getArgument(0)).setId(11L);
|
||||
return null;
|
||||
}).when(repository).insertItem(any());
|
||||
|
||||
FeeItemEntity created = feeItems.create(10001L, 9L, "admin", itemRequest());
|
||||
|
||||
assertThat(created.getId()).isEqualTo(11L);
|
||||
assertThat(created.getAllocationFee()).isTrue();
|
||||
assertThat(created.getWarehouseReceivable()).isTrue();
|
||||
assertThat(created.getCreatedByName()).isEqualTo("admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("入库费用绑定项目并生成订单快照")
|
||||
void bindsInboundFee() {
|
||||
InboundOrderEntity inbound = inbound();
|
||||
FeeItemEntity item = item(true, true);
|
||||
when(repository.findInbound(10001L, 21L)).thenReturn(inbound);
|
||||
when(repository.findItem(10001L, 11L)).thenReturn(item);
|
||||
doAnswer(invocation -> {
|
||||
((InboundFeeDetailEntity) invocation.getArgument(0)).setId(31L);
|
||||
return null;
|
||||
}).when(repository).insertDetail(any());
|
||||
|
||||
InboundFeeDetailEntity created = details.create(10001L, 21L, 9L,
|
||||
detailRequest("RECEIVABLE"));
|
||||
|
||||
assertThat(created.getId()).isEqualTo(31L);
|
||||
assertThat(created.getInboundCode()).isEqualTo("RK-20260805-001");
|
||||
assertThat(created.getFeeTypeNameSnapshot()).isEqualTo("仓库租赁费");
|
||||
assertThat(created.getActualGrossWeightKg()).isEqualByComparingTo("10.5000");
|
||||
assertThat(created.getReviewStatus()).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("拒绝绑定未启用仓储应收的项目")
|
||||
void rejectsInapplicableItem() {
|
||||
when(repository.findInbound(10001L, 21L)).thenReturn(inbound());
|
||||
when(repository.findItem(10001L, 11L)).thenReturn(item(false, true));
|
||||
|
||||
assertThatThrownBy(() -> details.create(10001L, 21L, 9L,
|
||||
detailRequest("RECEIVABLE")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("仓储应收/应付");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核只更新待审核明细")
|
||||
void approvesPendingDetail() {
|
||||
when(repository.findDetail(10001L, 21L, 31L)).thenReturn(new InboundFeeDetailEntity());
|
||||
when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1);
|
||||
|
||||
details.approve(10001L, 21L, 31L, 9L, "admin");
|
||||
|
||||
verify(repository).review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null);
|
||||
}
|
||||
|
||||
private static FeeItemDtos.SaveRequest itemRequest() {
|
||||
return new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", "仓库租赁费",
|
||||
false, false, true, true, true, "应付仓库租赁费");
|
||||
}
|
||||
|
||||
private static InboundFeeDetailDtos.SaveRequest detailRequest(String direction) {
|
||||
return new InboundFeeDetailDtos.SaveRequest(11L, direction, LocalDate.of(2026, 8, 5),
|
||||
new BigDecimal("12.3456"), "CNY", new BigDecimal("123.46"), "联调费用");
|
||||
}
|
||||
|
||||
private static FeeItemEntity item(boolean receivable, boolean payable) {
|
||||
FeeItemEntity item = new FeeItemEntity();
|
||||
item.setId(11L);
|
||||
item.setCategory("仓储成本");
|
||||
item.setTradeScope("INTERNATIONAL");
|
||||
item.setFeeTypeName("仓库租赁费");
|
||||
item.setWarehouseReceivable(receivable);
|
||||
item.setWarehousePayable(payable);
|
||||
item.setStatus("ACTIVE");
|
||||
return item;
|
||||
}
|
||||
|
||||
private static InboundOrderEntity inbound() {
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
inbound.setId(21L);
|
||||
inbound.setInboundCode("RK-20260805-001");
|
||||
inbound.setChannelCode("SEA");
|
||||
inbound.setChannelName("海运");
|
||||
inbound.setDestinationCode("ONT8");
|
||||
inbound.setDestinationName("ONT8");
|
||||
inbound.setTotalPackageCount(10);
|
||||
inbound.setActualGrossWeightKg(new BigDecimal("10.5000"));
|
||||
inbound.setActualVolumeCbm(new BigDecimal("1.250000"));
|
||||
inbound.setVolumeWeightKg(new BigDecimal("208.3333"));
|
||||
inbound.setChargeableWeightKg(new BigDecimal("208.3333"));
|
||||
inbound.setChargeableVolumeCbm(new BigDecimal("1.250000"));
|
||||
return inbound;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user