优化
This commit is contained in:
+65
@@ -0,0 +1,65 @@
|
||||
package com.baiyue.finance.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.finance.client.FinanceFileClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/finance/v1/vouchers")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
|
||||
public class PlatformFinanceVoucherController {
|
||||
private final FinanceFileClient files;
|
||||
private final String token;
|
||||
|
||||
public PlatformFinanceVoucherController(FinanceFileClient files,
|
||||
@Value("${baiyue.security.internal-service-token:}") String token) {
|
||||
this.files = files;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public record Voucher(String fileId, String fileName, String contentType, Long sizeBytes) {}
|
||||
|
||||
@GetMapping("/{fileId}")
|
||||
public ApiResponse<Voucher> metadata(@PathVariable long fileId, @RequestParam long customerId) {
|
||||
var file = scopedFile(customerId, fileId);
|
||||
return FinanceResponses.success(new Voucher(String.valueOf(fileId), file.fileName(), file.contentType(), file.sizeBytes()));
|
||||
}
|
||||
|
||||
@GetMapping("/{fileId}/content")
|
||||
public ResponseEntity<byte[]> content(@PathVariable long fileId, @RequestParam long customerId) {
|
||||
var file = scopedFile(customerId, fileId);
|
||||
var response = files.download(token, customerId, customerId, fileId);
|
||||
if (response == null || !response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "付款凭证读取失败,请稍后重试");
|
||||
}
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.header("X-Content-Type-Options", "nosniff")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
||||
.filename(file.fileName() == null ? "付款凭证" : file.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM).body(response.getBody());
|
||||
}
|
||||
|
||||
private FinanceFileClient.Metadata scopedFile(long customerId, long fileId) {
|
||||
if (customerId <= 0 || fileId <= 0) throw new BizException(ResultCode.BAD_REQUEST, "客户或付款凭证无效");
|
||||
var response = files.metadata(token, customerId, customerId, fileId);
|
||||
var file = response == null ? null : response.data();
|
||||
if (file == null || !Long.valueOf(fileId).equals(file.fileId())
|
||||
|| !Long.valueOf(customerId).equals(file.tenantId())
|
||||
|| !"FINANCE_VOUCHER".equals(file.businessType())
|
||||
|| !String.valueOf(customerId).equals(file.businessId())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "付款凭证不存在或不属于当前客户");
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -115,6 +115,13 @@ public class BusinessFeeSourceEventService {
|
||||
|| blank(value.feeItemName()) || !DIRECTIONS.contains(value.direction())
|
||||
|| value.businessDate() == null || !"CNY".equals(value.currency())
|
||||
|| value.totalAmount() == null || value.totalAmount().signum() <= 0
|
||||
|| !Set.of("CNY", "USD").contains(value.originalCurrency())
|
||||
|| value.originalAmount() == null || value.originalAmount().signum() <= 0
|
||||
|| value.exchangeRate() == null || value.exchangeRate().signum() <= 0
|
||||
|| blank(value.exchangeRateMonth())
|
||||
|| (value.periodStart() == null) != (value.periodEnd() == null)
|
||||
|| value.periodStart() != null && value.periodStart().isAfter(value.periodEnd())
|
||||
|| (value.chargeableQuantity() == null) != blank(value.quantityUnit())
|
||||
|| !"APPROVED".equals(value.reviewStatus()) || blank(value.sourceHash())) {
|
||||
throw error("FIN-EVT-012", "跨区费用来源快照字段不完整");
|
||||
}
|
||||
@@ -148,7 +155,9 @@ public class BusinessFeeSourceEventService {
|
||||
value.warehouseId(), value.feeItemCode(), value.feeItemName(), value.direction(), value.supplierId(),
|
||||
value.supplierName(), value.supplierCategory(), value.settlementMode(), value.paidAt(), value.paymentMethod(),
|
||||
value.paymentReferenceNo(), value.paymentVoucherFileIds(), value.businessDate(), value.currency(),
|
||||
value.totalAmount(), value.reviewStatus(), "PUBLISHED", "ACTIVE", null);
|
||||
value.totalAmount(), value.originalCurrency(), value.originalAmount(), value.exchangeRate(),
|
||||
value.exchangeRateMonth(), value.chargeableQuantity(), value.quantityUnit(), value.unitPrice(),
|
||||
value.periodStart(), value.periodEnd(), value.reviewStatus(), "PUBLISHED", "ACTIVE", null);
|
||||
String expected = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(objectMapper.valueToTree(unsigned)));
|
||||
if (!expected.equals(value.sourceHash())) throw error("FIN-EVT-004", "费用来源快照哈希不一致");
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ class BusinessFeeSourceEventServiceTest {
|
||||
"OVERSEAS_PROCUREMENT", "801", "PO-US-001", null, null, null, null, null,
|
||||
null, "91", "PROCUREMENT_SERVICE", "采购服务费", "RECEIVABLE", null, null,
|
||||
null, null, null, null, null, List.of(), LocalDate.of(2026, 9, 2), "CNY",
|
||||
new BigDecimal("120.00"), "APPROVED", "PUBLISHED", "ACTIVE", null);
|
||||
new BigDecimal("120.00"), "CNY", new BigDecimal("120.00"), BigDecimal.ONE,
|
||||
"2026-09", null, null, null, null, null, "APPROVED", "PUBLISHED", "ACTIVE", null);
|
||||
String sourceHash = EventPayloadHash.sha256(
|
||||
EventPayloadCanonicalizer.canonicalize(json.valueToTree(unsigned)));
|
||||
BusinessFeeSourceChangedV1 payload = new BusinessFeeSourceChangedV1("PUBLISHED", "LASTLEG",
|
||||
@@ -77,7 +78,8 @@ class BusinessFeeSourceEventServiceTest {
|
||||
"OVERSEAS_PROCUREMENT", "801", "PO-US-001", null, null, null, null, null,
|
||||
null, "91", "PROCUREMENT_SERVICE", "采购服务费", "RECEIVABLE", null, null,
|
||||
null, null, null, null, null, List.of(), LocalDate.of(2026, 9, 2), "CNY",
|
||||
new BigDecimal("120.00"), "APPROVED", "PUBLISHED", "ACTIVE", sourceHash);
|
||||
new BigDecimal("120.00"), "CNY", new BigDecimal("120.00"), BigDecimal.ONE,
|
||||
"2026-09", null, null, null, null, null, "APPROVED", "PUBLISHED", "ACTIVE", sourceHash);
|
||||
JsonNode node = json.valueToTree(payload);
|
||||
String payloadHash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
|
||||
return new EventEnvelope("123e4567-e89b-12d3-a456-426614174701",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.finance.client.FinanceFileClient;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.finance.controller.PlatformFinanceVoucherController;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class PlatformFinanceVoucherControllerTest {
|
||||
private final FinanceFileClient files = mock(FinanceFileClient.class);
|
||||
private final PlatformFinanceVoucherController controller = new PlatformFinanceVoucherController(files, "test-token");
|
||||
|
||||
private FinanceFileClient.Metadata file(long customerId, String type, String businessId) {
|
||||
return new FinanceFileClient.Metadata(91L, customerId, type, businessId, "付款.png", "sha", "image/png", 3L);
|
||||
}
|
||||
|
||||
@Test void returnsFilenameAndDownloadsWithinCustomerScope() {
|
||||
when(files.metadata("test-token", 11, 11, 91)).thenReturn(ApiResponse.success(file(11, "FINANCE_VOUCHER", "11"), "test", "test"));
|
||||
when(files.download("test-token", 11, 11, 91)).thenReturn(ResponseEntity.ok(new byte[]{1, 2, 3}));
|
||||
assertThat(controller.metadata(91, 11).data().fileName()).isEqualTo("付款.png");
|
||||
var content = controller.content(91, 11);
|
||||
assertThat(content.getBody()).containsExactly(1, 2, 3);
|
||||
assertThat(content.getHeaders().getCacheControl()).isEqualTo("no-store");
|
||||
assertThat(content.getHeaders().getFirst("Content-Disposition")).startsWith("attachment;");
|
||||
}
|
||||
|
||||
@Test void rejectsWrongCustomerBusinessTypeAndBusinessIdBeforeDownload() {
|
||||
for (var invalid : new FinanceFileClient.Metadata[]{file(12, "FINANCE_VOUCHER", "11"), file(11, "OTHER", "11"), file(11, "FINANCE_VOUCHER", "12")}) {
|
||||
when(files.metadata("test-token", 11, 11, 91)).thenReturn(ApiResponse.success(invalid, "test", "test"));
|
||||
assertThatThrownBy(() -> controller.content(91, 11)).isInstanceOf(BizException.class);
|
||||
}
|
||||
verify(files, never()).download(anyString(), anyLong(), anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test void rejectsMissingMetadataAndFailedContent() {
|
||||
assertThatThrownBy(() -> controller.content(91, 11)).isInstanceOf(BizException.class);
|
||||
when(files.metadata("test-token", 11, 11, 91)).thenReturn(ApiResponse.success(file(11, "FINANCE_VOUCHER", "11"), "test", "test"));
|
||||
when(files.download("test-token", 11, 11, 91)).thenReturn(ResponseEntity.notFound().build());
|
||||
assertThatThrownBy(() -> controller.content(91, 11)).isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
@Test void requiresPlatformListPermission() {
|
||||
assertThat(PlatformFinanceVoucherController.class.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('platform:finance:customer-arap:list')");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user