优化
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.finance.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name="firstleg-service", contextId="financeFirstlegCostSnapshotClient", url="${baiyue.clients.firstleg.url:}", path="/internal/firstleg/v1/finance-cost-snapshots")
|
||||
public interface FirstlegCostSnapshotClient {
|
||||
@GetMapping("/customers/{customerId}/containers/{containerId}")
|
||||
ApiResponse<ContainerSnapshot> container(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long customerId, @PathVariable long containerId);
|
||||
record ContainerSnapshot(long containerId, long customerId, List<OrderVolume> orders) {}
|
||||
record OrderVolume(long orderId, Long inboundId, BigDecimal volumeCbm) {}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.finance.service.FinanceService;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.finance.client.FirstlegCostSnapshotClient;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -22,11 +23,12 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
public class PlatformFinanceController {
|
||||
private final FinanceService service;
|
||||
private final FinanceFileClient fileClient;
|
||||
private final FirstlegCostSnapshotClient costSnapshotClient;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String fileToken;
|
||||
public PlatformFinanceController(FinanceService service, FinanceFileClient fileClient) { this.service = service; this.fileClient = fileClient; }
|
||||
public PlatformFinanceController(FinanceService service, FinanceFileClient fileClient, FirstlegCostSnapshotClient costSnapshotClient) { this.service = service; this.fileClient = fileClient; this.costSnapshotClient = costSnapshotClient; }
|
||||
|
||||
@PostMapping(value = "/vouchers", consumes = "multipart/form-data")
|
||||
@PreAuthorize("hasAuthority('platform:finance:receipt:create')")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:receipt:create','platform:finance:cost:create')")
|
||||
public ApiResponse<FinanceFileClient.Metadata> uploadVoucher(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long customerId,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
@@ -71,6 +73,12 @@ public class PlatformFinanceController {
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
|
||||
public ApiResponse<Map<String, Object>> bill(@PathVariable long billId) { return ApiResponse.success(service.billById(billId)); }
|
||||
|
||||
@GetMapping("/bills/{billId}/revisions")
|
||||
@PreAuthorize("hasAuthority('platform:finance:bill:view')")
|
||||
public ApiResponse<?> revisions(@PathVariable long billId, @RequestParam long customerId) {
|
||||
return ApiResponse.success(service.billRevisions(customerId, billId));
|
||||
}
|
||||
|
||||
@GetMapping("/receipts")
|
||||
@PreAuthorize("hasAuthority('platform:finance:receipt:view')")
|
||||
public ApiResponse<?> receipts(@RequestParam(required = false) Long customerId) { return ApiResponse.success(service.listReceipts(customerId)); }
|
||||
@@ -100,7 +108,17 @@ public class PlatformFinanceController {
|
||||
public ApiResponse<Map<String, Object>> cost(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CostRequest request) {
|
||||
if (request.currency() != null && !"CNY".equals(request.currency())) throw new IllegalArgumentException("首期仅支持CNY");
|
||||
var internal = new FinanceService.CostRequest(null, request.costType(), request.orderId(), null, request.containerId(), null, null, "MANUAL", request.expenseName(), request.amount(), request.voucherFileIds(), request.remark());
|
||||
return ApiResponse.success(service.createCost(request.customerId(), user.userId(), internal, key));
|
||||
java.util.List<FinanceService.ContainerOrderVolume> volumes = java.util.List.of();
|
||||
if ("CONTAINER_SHARED".equals(request.costType())) {
|
||||
if (request.containerId() == null) throw new IllegalArgumentException("整柜成本必须指定柜次");
|
||||
var response = costSnapshotClient.container(fileToken, request.customerId(), request.containerId());
|
||||
var snapshot = response == null ? null : response.data();
|
||||
if (snapshot == null || snapshot.customerId() != request.customerId() || snapshot.containerId() != request.containerId() || snapshot.orders() == null || snapshot.orders().isEmpty()) {
|
||||
throw new IllegalArgumentException("Firstleg未返回有效柜内订单体积快照");
|
||||
}
|
||||
volumes = snapshot.orders().stream().map(v -> new FinanceService.ContainerOrderVolume(v.orderId(), v.inboundId(), snapshot.containerId(), snapshot.customerId(), v.volumeCbm())).toList();
|
||||
}
|
||||
return ApiResponse.success(service.createCost(request.customerId(), user.userId(), internal, volumes, key));
|
||||
}
|
||||
|
||||
@PutMapping("/bills/{billId}")
|
||||
|
||||
@@ -74,6 +74,13 @@ public class FinanceService {
|
||||
return bill(tenantId, billId);
|
||||
}
|
||||
|
||||
public List<Map<String,Object>> billRevisions(long customerId, long billId) {
|
||||
requireCustomer(customerId);
|
||||
Number count = jdbc.queryForObject("SELECT COUNT(*) FROM fin_bill WHERE id=? AND tenant_id=?", Number.class, billId, customerId);
|
||||
if (count == null || count.intValue() != 1) throw bad("账单不存在或不属于客户");
|
||||
return jdbc.queryForList("SELECT id,bill_id,version,reason,bill_snapshot,line_snapshot,created_by,created_at FROM fin_bill_revision WHERE tenant_id=? AND bill_id=? ORDER BY version DESC,id DESC", customerId, billId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> issueBillById(long operatorId, long billId, int expectedVersion, String idempotencyKey) {
|
||||
Long tenantId = jdbc.queryForObject("SELECT tenant_id FROM fin_bill WHERE id=?", Long.class, billId);
|
||||
@@ -215,12 +222,14 @@ public class FinanceService {
|
||||
@Transactional
|
||||
public Map<String, Object> createCost(long tenantId, long operatorId, CostRequest request,
|
||||
List<ContainerOrderVolume> containerOrders, String idempotencyKey) {
|
||||
requireCustomer(tenantId); Long replay = reserve(tenantId, operatorId, "COST_CREATE", idempotencyKey, request);
|
||||
requireCustomer(tenantId); Long replay = reserve(tenantId, operatorId, "COST_CREATE", idempotencyKey, Map.of("cost", request, "containerOrders", containerOrders == null ? List.of() : containerOrders));
|
||||
if (replay != null) return jdbc.queryForMap("SELECT * FROM fin_cost WHERE id=? AND tenant_id=?", replay, tenantId);
|
||||
if (!"SINGLE_ORDER".equals(request.costType()) && !"CONTAINER_SHARED".equals(request.costType())) throw bad("成本类型只能是SINGLE_ORDER或CONTAINER_SHARED");
|
||||
if (request.amount() == null || request.amount().signum() <= 0) throw bad("成本金额必须大于0");
|
||||
validateVoucherFiles(tenantId, request.voucherFileIds());
|
||||
if ("SINGLE_ORDER".equals(request.costType()) && (request.orderId() == null || request.containerId() != null)) throw bad("单票成本必须指定订单且不能指定柜次");
|
||||
if ("CONTAINER_SHARED".equals(request.costType()) && (request.containerId() == null || containerOrders == null || containerOrders.isEmpty())) throw bad("整柜成本必须指定柜次及柜内有效订单体积");
|
||||
if ("SINGLE_ORDER".equals(request.costType())) validateSingleOrder(tenantId, request.orderId());
|
||||
validateContainerOrders(tenantId, request.containerId(), containerOrders);
|
||||
KeyHolder holder = new GeneratedKeyHolder();
|
||||
jdbc.update(connection -> { PreparedStatement ps = connection.prepareStatement("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,order_id,inbound_id,container_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,cost_amount,voucher_file_ids,remark,created_by,updated_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", Statement.RETURN_GENERATED_KEYS);
|
||||
@@ -242,9 +251,16 @@ public class FinanceService {
|
||||
|| row.volumeCbm() == null || row.volumeCbm().signum() <= 0 || !seen.add(row.orderId())) {
|
||||
throw bad("柜次、订单、客户归属或体积无效");
|
||||
}
|
||||
Number exists = jdbc.queryForObject("SELECT COUNT(*) FROM fin_fee_source WHERE customer_id=? AND order_id=? AND status='AVAILABLE'", Number.class, tenantId, row.orderId());
|
||||
if (exists != null && exists.intValue() == 0) throw bad("柜内订单不存在或不属于客户");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSingleOrder(long tenantId, long orderId) {
|
||||
Number exists = jdbc.queryForObject("SELECT COUNT(*) FROM fin_fee_source WHERE customer_id=? AND order_id=? AND status='AVAILABLE'", Number.class, tenantId, orderId);
|
||||
if (exists != null && exists.intValue() == 0) throw bad("单票订单不存在或不属于客户");
|
||||
}
|
||||
|
||||
private void persistContainerAllocations(long tenantId, long operatorId, long costId, BigDecimal amount,
|
||||
List<ContainerOrderVolume> rows) {
|
||||
for (CostAllocation allocation : allocateContainerCost(amount, rows)) {
|
||||
@@ -271,10 +287,11 @@ public class FinanceService {
|
||||
ContainerOrderVolume row = rows.get(i);
|
||||
boolean tail = i == rows.size() - 1;
|
||||
BigDecimal rate = row.volumeCbm().divide(totalVolume, 10, java.math.RoundingMode.HALF_UP);
|
||||
BigDecimal share = tail ? amount.subtract(allocated).setScale(6, java.math.RoundingMode.HALF_UP)
|
||||
: amount.multiply(rate).setScale(6, java.math.RoundingMode.HALF_UP);
|
||||
BigDecimal regularShare = amount.multiply(rate).setScale(6, java.math.RoundingMode.HALF_UP);
|
||||
BigDecimal share = tail ? amount.subtract(allocated).setScale(6, java.math.RoundingMode.HALF_UP) : regularShare;
|
||||
boolean roundingAdjustment = tail && share.compareTo(regularShare) != 0;
|
||||
allocated = allocated.add(share);
|
||||
result.add(new CostAllocation(row, totalVolume, rate, share, tail));
|
||||
result.add(new CostAllocation(row, totalVolume, rate, share, roundingAdjustment));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ class FinanceContractTest {
|
||||
assertTrue(service.contains("status='PROCESSING'") && service.contains("stableNo"));
|
||||
assertTrue(service.contains("request_hash") && service.contains("result_resource_id") && service.contains("Idempotency-Key已用于其他请求"));
|
||||
assertTrue(service.contains("status IN ('ISSUED','PARTIALLY_PAID','PAID')"));
|
||||
String controller = Files.readString(Path.of("src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java"));
|
||||
assertTrue(controller.contains("/vouchers") && controller.contains("MultipartFile"));
|
||||
assertTrue(controller.contains("costSnapshotClient.container") && controller.contains("createCost(request.customerId(), user.userId(), internal, volumes, key)"));
|
||||
assertTrue(service.contains("validateVoucherFiles(tenantId, request.voucherFileIds())"));
|
||||
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertTrue(unified.contains("CREATE DATABASE IF NOT EXISTS baiyue_finance"));
|
||||
assertTrue(unified.contains("assert_finance_permissions"));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.finance.service.FinanceService;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -14,6 +16,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
class FinanceServiceValidationTest {
|
||||
@Test
|
||||
@@ -28,7 +31,12 @@ class FinanceServiceValidationTest {
|
||||
assertEquals(new BigDecimal("50.000000"), allocations.get(2).amount());
|
||||
assertEquals(new BigDecimal("100.000000"), allocations.stream().map(FinanceService.CostAllocation::amount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
assertEquals(true, allocations.get(2).roundingAdjustment());
|
||||
assertEquals(false, allocations.get(2).roundingAdjustment());
|
||||
var tail = FinanceService.allocateContainerCost(new BigDecimal("100.00"), List.of(
|
||||
new FinanceService.ContainerOrderVolume(21L, 201L, 7L, 9L, BigDecimal.ONE),
|
||||
new FinanceService.ContainerOrderVolume(22L, 202L, 7L, 9L, BigDecimal.ONE),
|
||||
new FinanceService.ContainerOrderVolume(23L, 203L, 7L, 9L, BigDecimal.ONE)));
|
||||
assertEquals(true, tail.get(2).roundingAdjustment());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,4 +70,49 @@ class FinanceServiceValidationTest {
|
||||
List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE), new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE)));
|
||||
assertThrows(RuntimeException.class, () -> service.createReceipt(1L, 2L, request, "k3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsVoucherFromAnotherCustomer() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
FinanceFileClient files = mock(FinanceFileClient.class);
|
||||
when(files.metadata(anyString(), eq(9L), eq(9L), eq(77L))).thenReturn(ApiResponse.success(
|
||||
new FinanceFileClient.Metadata(77L, 10L, "FINANCE_VOUCHER", "9", "v.jpg", "sha", "image/jpeg", 10L)));
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "fileClient", files);
|
||||
var request = new FinanceService.ReceiptRequest("R-3", 1L, null, "BANK_TRANSFER", null, List.of("77"), null,
|
||||
List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE)));
|
||||
assertThrows(RuntimeException.class, () -> service.createReceipt(9L, 2L, request, "k4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSingleOrderWhenOrderNotOwnedByCustomer() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_fee_source"), eq(Number.class), any(), any())).thenReturn(0);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
var request = new FinanceService.CostRequest("C-2", "SINGLE_ORDER", 99L, null, null, null, null, "MANUAL", "货拉拉", BigDecimal.ONE, List.of("1"), null);
|
||||
assertThrows(RuntimeException.class, () -> service.createCost(9L, 2L, request, "k5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsBillRevisionHistoryOnlyForOwningCustomer() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_bill"), eq(Number.class), eq(50L), eq(9L)))
|
||||
.thenReturn(1);
|
||||
var rows = List.<java.util.Map<String, Object>>of(java.util.Map.of("version", 2));
|
||||
when(jdbc.queryForList(startsWith("SELECT id,bill_id,version"), eq(9L), eq(50L))).thenReturn(rows);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
|
||||
assertEquals(rows, service.billRevisions(9L, 50L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsBillRevisionHistoryFromAnotherCustomer() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_bill"), eq(Number.class), eq(50L), eq(9L)))
|
||||
.thenReturn(0);
|
||||
FinanceService service = new FinanceService(jdbc, new ObjectMapper());
|
||||
|
||||
assertThrows(RuntimeException.class, () -> service.billRevisions(9L, 50L));
|
||||
verify(jdbc, never()).queryForList(startsWith("SELECT id,bill_id,version"), (Object[]) any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.finance.client.FirstlegCostSnapshotClient;
|
||||
import com.baiyue.finance.controller.PlatformFinanceController;
|
||||
import com.baiyue.finance.service.FinanceService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PlatformFinanceControllerBehaviorTest {
|
||||
@Mock FinanceService service;
|
||||
@Mock FinanceFileClient fileClient;
|
||||
@Mock FirstlegCostSnapshotClient snapshotClient;
|
||||
private PlatformFinanceController controller;
|
||||
private final AuthenticatedUser user = new AuthenticatedUser(
|
||||
7L, null, null, null, "finance", "ADMIN", "WEB", Set.of());
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new PlatformFinanceController(service, fileClient, snapshotClient);
|
||||
ReflectionTestUtils.setField(controller, "fileToken", "internal-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerCostUsesFirstlegSnapshotInsteadOfBrowserData() {
|
||||
var request = new PlatformFinanceController.CostRequest(
|
||||
10L, "CONTAINER_SHARED", null, 88L, "清关费", new BigDecimal("300.00"),
|
||||
"CNY", List.of("91"), "整柜成本");
|
||||
var snapshot = new FirstlegCostSnapshotClient.ContainerSnapshot(88L, 10L, List.of(
|
||||
new FirstlegCostSnapshotClient.OrderVolume(101L, 1001L, new BigDecimal("1.25")),
|
||||
new FirstlegCostSnapshotClient.OrderVolume(102L, 1002L, new BigDecimal("2.75"))));
|
||||
when(snapshotClient.container("internal-token", 10L, 88L)).thenReturn(ApiResponse.success(snapshot));
|
||||
when(service.createCost(eq(10L), eq(7L), any(), any(), eq("cost-key"))).thenReturn(Map.of("id", 1L));
|
||||
|
||||
controller.cost(user, "cost-key", request);
|
||||
|
||||
verify(snapshotClient).container("internal-token", 10L, 88L);
|
||||
verify(service).createCost(eq(10L), eq(7L), any(), eq(List.of(
|
||||
new FinanceService.ContainerOrderVolume(101L, 1001L, 88L, 10L, new BigDecimal("1.25")),
|
||||
new FinanceService.ContainerOrderVolume(102L, 1002L, 88L, 10L, new BigDecimal("2.75")))), eq("cost-key"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerCostRejectsEmptyFirstlegSnapshot() {
|
||||
var request = new PlatformFinanceController.CostRequest(
|
||||
10L, "CONTAINER_SHARED", null, 88L, "拖车费", BigDecimal.TEN,
|
||||
"CNY", List.of("91"), null);
|
||||
when(snapshotClient.container("internal-token", 10L, 88L)).thenReturn(ApiResponse.success(
|
||||
new FirstlegCostSnapshotClient.ContainerSnapshot(88L, 10L, List.of())));
|
||||
|
||||
assertThatThrownBy(() -> controller.cost(user, "cost-key", request))
|
||||
.hasMessageContaining("未返回有效柜内订单体积快照");
|
||||
verify(service, never()).createCost(any(Long.class), any(Long.class), any(), any(), any());
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.firstleg.loading.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.loading.service.FinanceCostSnapshotService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/firstleg/v1/finance-cost-snapshots")
|
||||
public class InternalFinanceCostSnapshotController {
|
||||
private final FinanceCostSnapshotService service;
|
||||
@Value("${baiyue.security.internal-service-token}") private String expectedToken;
|
||||
public InternalFinanceCostSnapshotController(FinanceCostSnapshotService service) { this.service = service; }
|
||||
|
||||
@GetMapping("/customers/{customerId}/containers/{containerId}")
|
||||
public ApiResponse<FinanceCostSnapshotService.ContainerSnapshot> container(
|
||||
@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
||||
@PathVariable long customerId, @PathVariable long containerId) {
|
||||
if (token == null || !MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8), expectedToken.getBytes(StandardCharsets.UTF_8))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "内部服务令牌无效");
|
||||
}
|
||||
return ApiResponse.success(service.container(customerId, containerId));
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.firstleg.loading.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class FinanceCostSnapshotService {
|
||||
private final SmartLoadingRepository repository;
|
||||
public FinanceCostSnapshotService(SmartLoadingRepository repository) { this.repository = repository; }
|
||||
|
||||
public ContainerSnapshot container(long customerId, long loadingPlanId) {
|
||||
if (customerId <= 0 || loadingPlanId <= 0 || repository.plan(loadingPlanId) == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "柜次不存在");
|
||||
}
|
||||
List<OrderVolume> orders = repository.items(loadingPlanId).stream()
|
||||
.filter(item -> item.getAccountId() != null && item.getAccountId() == customerId)
|
||||
.filter(item -> item.getOrderId() != null && item.getOrderId() > 0)
|
||||
.filter(item -> item.getVolumeCbm() != null && item.getVolumeCbm().signum() > 0)
|
||||
.map(item -> new OrderVolume(item.getOrderId(), item.getInboundId(), item.getVolumeCbm()))
|
||||
.toList();
|
||||
if (orders.isEmpty()) throw new BizException(ResultCode.BAD_REQUEST, "柜次没有该客户的有效订单体积");
|
||||
return new ContainerSnapshot(loadingPlanId, customerId, orders);
|
||||
}
|
||||
|
||||
public record ContainerSnapshot(long containerId, long customerId, List<OrderVolume> orders) {}
|
||||
public record OrderVolume(long orderId, Long inboundId, BigDecimal volumeCbm) {}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.baiyue.firstleg.loading.service;
|
||||
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FinanceCostSnapshotServiceTest {
|
||||
@Mock SmartLoadingRepository repository;
|
||||
@InjectMocks FinanceCostSnapshotService service;
|
||||
|
||||
@Test
|
||||
void returnsOnlyValidVolumesOwnedByCustomer() {
|
||||
when(repository.plan(88L)).thenReturn(mock(LoadingPlanEntity.class));
|
||||
when(repository.items(88L)).thenReturn(List.of(
|
||||
item(10L, 101L, 1001L, "1.25"),
|
||||
item(11L, 102L, 1002L, "2.00"),
|
||||
item(10L, 103L, 1003L, "0")));
|
||||
|
||||
var snapshot = service.container(10L, 88L);
|
||||
|
||||
assertThat(snapshot.customerId()).isEqualTo(10L);
|
||||
assertThat(snapshot.containerId()).isEqualTo(88L);
|
||||
assertThat(snapshot.orders()).containsExactly(
|
||||
new FinanceCostSnapshotService.OrderVolume(101L, 1001L, new BigDecimal("1.25")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsContainerWithoutValidCustomerVolume() {
|
||||
when(repository.plan(88L)).thenReturn(mock(LoadingPlanEntity.class));
|
||||
when(repository.items(88L)).thenReturn(List.of(
|
||||
item(11L, 102L, 1002L, "2.00"),
|
||||
item(10L, 103L, 1003L, "0")));
|
||||
|
||||
assertThatThrownBy(() -> service.container(10L, 88L))
|
||||
.hasMessageContaining("没有该客户的有效订单体积");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownContainer() {
|
||||
when(repository.plan(88L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.container(10L, 88L))
|
||||
.hasMessageContaining("柜次不存在");
|
||||
}
|
||||
|
||||
private LoadingPlanItemEntity item(long customerId, long orderId, long inboundId, String volume) {
|
||||
var item = new LoadingPlanItemEntity();
|
||||
item.setAccountId(customerId);
|
||||
item.setOrderId(orderId);
|
||||
item.setInboundId(inboundId);
|
||||
item.setVolumeCbm(new BigDecimal(volume));
|
||||
return item;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user