feat: complete finance and overseas receiving workflow

This commit is contained in:
2026-09-10 22:43:11 +08:00
parent e531ae9407
commit 56635ad4e4
66 changed files with 1601 additions and 417 deletions
@@ -0,0 +1,23 @@
package com.baiyue.order.client;
import com.baiyue.common.core.api.ApiResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import java.math.BigDecimal;
import java.time.LocalDate;
@FeignClient(name = "finance-service", contextId = "orderFinanceWalletClient",
url = "${baiyue.clients.finance.url:}", path = "/internal/finance/v1/wallet")
public interface FinanceWalletClient {
@GetMapping("/accounts/{customerId}")
ApiResponse<WalletAccount> account(
@RequestHeader("X-Internal-Service-Token") String internalToken,
@PathVariable("customerId") long customerId);
record WalletAccount(BigDecimal actualBalance, BigDecimal availableBalance,
BigDecimal creditLimit, BigDecimal availableCredit,
LocalDate creditExpiresOn, boolean creditActive) { }
}
@@ -3,14 +3,18 @@ package com.baiyue.order.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.client.TenantEligibilityClient;
import com.baiyue.order.client.FinanceWalletClient;
import feign.FeignException;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class OrderEligibilityGuard {
@Resource private TenantEligibilityClient client;
@Resource private FinanceWalletClient financeWalletClient;
@Value("${baiyue.security.internal-service-token}") private String internalToken;
public void requireApproved(long tenantId) {
@@ -32,6 +36,7 @@ public class OrderEligibilityGuard {
throw new BizException(ResultCode.FORBIDDEN,
response.data().reason() == null ? "Enterprise review approval is required" : response.data().reason());
}
requireNonNegativeActualBalance(tenantId);
} catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM eligibility service is unavailable");
} catch (BizException exception) {
@@ -41,4 +46,23 @@ public class OrderEligibilityGuard {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM 下单资格响应无效");
}
}
private void requireNonNegativeActualBalance(long tenantId) {
try {
var response = financeWalletClient.account(internalToken, tenantId);
if (response == null || !response.successful() || response.data() == null
|| response.data().actualBalance() == null) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Unable to verify customer account balance");
}
if (response.data().actualBalance().compareTo(BigDecimal.ZERO) < 0) {
throw new BizException(ResultCode.FORBIDDEN, "账户实际余额为负,请充值后再下单");
}
} catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Finance account service is unavailable");
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Finance account response is invalid");
}
}
}
@@ -3,11 +3,15 @@ package com.baiyue.order.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.client.FinanceWalletClient;
import com.baiyue.order.client.TenantEligibilityClient;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -48,4 +52,46 @@ class OrderEligibilityGuardTest {
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
}
@Test
void negativeActualBalanceBlocksNewOrderSubmission() {
TenantEligibilityClient client = approvedClient();
FinanceWalletClient finance = mock(FinanceWalletClient.class);
when(finance.account("internal-token", 10001L)).thenReturn(ApiResponse.success(
new FinanceWalletClient.WalletAccount(new BigDecimal("-0.01"), new BigDecimal("99.99"),
new BigDecimal("100.00"), new BigDecimal("99.99"), LocalDate.now().plusDays(7), true)));
OrderEligibilityGuard guard = guard(client, finance);
assertThatThrownBy(() -> guard.requireApproved(10001L))
.isInstanceOf(BizException.class)
.hasMessage("账户实际余额为负,请充值后再下单")
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.FORBIDDEN);
}
@Test
void zeroActualBalanceAllowsOrderWhileCreditIsActive() {
TenantEligibilityClient client = approvedClient();
FinanceWalletClient finance = mock(FinanceWalletClient.class);
when(finance.account("internal-token", 10001L)).thenReturn(ApiResponse.success(
new FinanceWalletClient.WalletAccount(BigDecimal.ZERO, new BigDecimal("100.00"),
new BigDecimal("100.00"), new BigDecimal("100.00"), LocalDate.now().plusDays(7), true)));
guard(client, finance).requireApproved(10001L);
}
private static TenantEligibilityClient approvedClient() {
TenantEligibilityClient client = mock(TenantEligibilityClient.class);
when(client.check("internal-token", 10001L)).thenReturn(ApiResponse.success(
new TenantEligibilityClient.EligibilityResponse(true, "ACTIVE", null)));
return client;
}
private static OrderEligibilityGuard guard(TenantEligibilityClient client, FinanceWalletClient finance) {
OrderEligibilityGuard guard = new OrderEligibilityGuard();
ReflectionTestUtils.setField(guard, "client", client);
ReflectionTestUtils.setField(guard, "financeWalletClient", finance);
ReflectionTestUtils.setField(guard, "internalToken", "internal-token");
return guard;
}
}