feat: complete finance and overseas receiving workflow
This commit is contained in:
@@ -33,6 +33,7 @@ public final class FinancePhase1Dtos {
|
||||
BigDecimal actualBalance, BigDecimal frozenBalance,
|
||||
BigDecimal availableBalance, BigDecimal creditLimit,
|
||||
BigDecimal creditUsed, BigDecimal availableCredit,
|
||||
LocalDate creditExpiresOn, boolean creditActive,
|
||||
int accountVersion,
|
||||
long pendingRechargeCount,
|
||||
String status, LocalDateTime lastChangedAt) {}
|
||||
|
||||
@@ -12,6 +12,7 @@ public final class FinanceWalletDtos {
|
||||
BigDecimal frozenBalance, BigDecimal availableBalance,
|
||||
BigDecimal creditLimit, BigDecimal creditUsed,
|
||||
BigDecimal creditFrozen, BigDecimal availableCredit,
|
||||
LocalDate creditExpiresOn, boolean creditActive,
|
||||
int version, LocalDateTime updatedAt) { }
|
||||
|
||||
public record WalletPage<T>(List<T> records, long total, int page, int size, int pages) {
|
||||
@@ -26,6 +27,7 @@ public final class FinanceWalletDtos {
|
||||
BigDecimal frozenBalanceAfter, BigDecimal availableBalanceAfter,
|
||||
BigDecimal creditLimitAfter, BigDecimal creditUsedAfter,
|
||||
BigDecimal availableCreditAfter,
|
||||
LocalDate creditExpiresOnAfter,
|
||||
String sourceType, String sourceNo, LocalDate occurredDate,
|
||||
String paymentMethod, String referenceNo, String operatorName,
|
||||
String remark, LocalDateTime createdAt) { }
|
||||
@@ -45,7 +47,7 @@ public final class FinanceWalletDtos {
|
||||
String paymentMethod, String referenceNo, String remark,
|
||||
int expectedAccountVersion) { }
|
||||
|
||||
public record CreditLimitRequest(BigDecimal creditLimit, String remark,
|
||||
public record CreditLimitRequest(BigDecimal creditLimit, LocalDate creditExpiresOn, String remark,
|
||||
int expectedAccountVersion) { }
|
||||
|
||||
public record WalletFreezeRequest(BigDecimal amount, String businessType,
|
||||
|
||||
+28
-16
@@ -12,6 +12,7 @@ import org.springframework.stereotype.Service;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -21,18 +22,12 @@ public class FinanceCustomerAccountService {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final IamCustomerClient iamCustomerClient;
|
||||
private final String internalToken;
|
||||
private final BigDecimal defaultCreditLimit;
|
||||
|
||||
public FinanceCustomerAccountService(JdbcTemplate jdbc, IamCustomerClient iamCustomerClient,
|
||||
@Value("${baiyue.security.internal-service-token:}") String internalToken,
|
||||
@Value("${baiyue.finance.wallet.default-credit-limit:0.00}") BigDecimal defaultCreditLimit) {
|
||||
@Value("${baiyue.security.internal-service-token:}") String internalToken) {
|
||||
this.jdbc = jdbc;
|
||||
this.iamCustomerClient = iamCustomerClient;
|
||||
this.internalToken = internalToken;
|
||||
if (defaultCreditLimit == null || defaultCreditLimit.signum() < 0 || defaultCreditLimit.scale() > 2) {
|
||||
throw new IllegalArgumentException("默认授信额度不能为负数且最多保留两位小数");
|
||||
}
|
||||
this.defaultCreditLimit = defaultCreditLimit.setScale(2);
|
||||
}
|
||||
|
||||
public PageEnvelope<AccountSummary> list(int page, int pageSize, String keyword, String status, Long customerId) {
|
||||
@@ -60,6 +55,7 @@ public class FinanceCustomerAccountService {
|
||||
value.outstandingPayable(),
|
||||
value.actualBalance(), value.frozenBalance(), value.availableBalance(),
|
||||
value.creditLimit(), value.creditUsed(), value.availableCredit(),
|
||||
value.creditExpiresOn(), value.creditActive(),
|
||||
value.accountVersion(), value.pendingRechargeCount(),
|
||||
customer.status(), value.lastChangedAt());
|
||||
}).toList();
|
||||
@@ -70,7 +66,7 @@ public class FinanceCustomerAccountService {
|
||||
if (customerIds.isEmpty()) return Map.of();
|
||||
String placeholders = String.join(",", customerIds.stream().map(id -> "?").toList());
|
||||
Map<Long, MutableMetrics> result = new HashMap<>();
|
||||
customerIds.forEach(id -> result.put(id, new MutableMetrics(defaultCreditLimit)));
|
||||
customerIds.forEach(id -> result.put(id, new MutableMetrics()));
|
||||
jdbc.queryForList("SELECT tenant_id customer_id,SUM(outstanding_amount) amount FROM fin_settlement WHERE tenant_id IN (" + placeholders + ") AND direction='RECEIVABLE' AND status<>'VOIDED' GROUP BY tenant_id", customerIds.toArray())
|
||||
.forEach(row -> metric(result, row, "outstandingReceivable", "amount"));
|
||||
jdbc.queryForList("SELECT customer_id,SUM(amount) amount FROM fin_fee_source f WHERE customer_id IN (" + placeholders + ") AND direction='RECEIVABLE' AND review_status='APPROVED' AND currency_code='CNY' AND amount>0 AND status='AVAILABLE' AND NOT EXISTS (SELECT 1 FROM fin_bill_line bl JOIN fin_bill b ON b.id=bl.bill_id AND b.tenant_id=bl.tenant_id WHERE bl.tenant_id=f.customer_id AND bl.source_id=f.id AND bl.source_version=f.source_fee_version AND b.status<>'VOIDED') GROUP BY customer_id", customerIds.toArray())
|
||||
@@ -83,7 +79,7 @@ public class FinanceCustomerAccountService {
|
||||
.forEach(row -> metric(result, row, "paidCost", "amount"));
|
||||
jdbc.queryForList("SELECT customer_id,SUM(amount) amount FROM (SELECT tenant_id customer_id,outstanding_amount amount FROM fin_settlement WHERE tenant_id IN (" + placeholders + ") AND direction='PAYABLE' AND status<>'VOIDED' UNION ALL SELECT f.customer_id,f.amount FROM fin_fee_source f WHERE f.customer_id IN (" + placeholders + ") AND f.direction='PAYABLE' AND f.settlement_mode='ON_ACCOUNT' AND f.review_status='APPROVED' AND f.currency_code='CNY' AND f.amount>0 AND f.status NOT IN ('VOIDED','SUPERSEDED') AND NOT EXISTS (SELECT 1 FROM fin_settlement_line sl JOIN fin_settlement s ON s.id=sl.settlement_id AND s.tenant_id=sl.tenant_id WHERE sl.tenant_id=f.customer_id AND sl.fee_source_id=f.id AND s.direction='PAYABLE' AND s.status<>'VOIDED')) payable_obligations GROUP BY customer_id", concat(customerIds, 2))
|
||||
.forEach(row -> metric(result, row, "outstandingPayable", "amount"));
|
||||
jdbc.queryForList("SELECT customer_id,actual_balance,frozen_balance,available_balance,credit_limit,credit_used,credit_frozen,version FROM fin_wallet_account WHERE customer_id IN (" + placeholders + ")", customerIds.toArray())
|
||||
jdbc.queryForList("SELECT customer_id,actual_balance,frozen_balance,available_balance,credit_limit,credit_used,credit_frozen,credit_expires_on,version FROM fin_wallet_account WHERE customer_id IN (" + placeholders + ")", customerIds.toArray())
|
||||
.forEach(row -> walletMetric(result, row));
|
||||
jdbc.queryForList("SELECT customer_id,COUNT(*) pending_count FROM fin_recharge_request WHERE customer_id IN (" + placeholders + ") AND status='PENDING' GROUP BY customer_id", customerIds.toArray())
|
||||
.forEach(row -> pendingMetric(result, row));
|
||||
@@ -100,10 +96,22 @@ public class FinanceCustomerAccountService {
|
||||
MutableMetrics target = result.get(number.longValue());
|
||||
target.actualBalance = decimal(row.get("actual_balance"));
|
||||
target.frozenBalance = decimal(row.get("frozen_balance"));
|
||||
target.availableBalance = decimal(row.get("available_balance"));
|
||||
target.creditLimit = decimal(row.get("credit_limit"));
|
||||
BigDecimal creditFrozen = decimal(row.get("credit_frozen"));
|
||||
target.creditExpiresOn = row.get("credit_expires_on") instanceof java.sql.Date value ? value.toLocalDate()
|
||||
: row.get("credit_expires_on") instanceof LocalDate value ? value : null;
|
||||
target.creditActive = target.creditLimit.signum() > 0 && target.creditExpiresOn != null
|
||||
&& !target.creditExpiresOn.isBefore(LocalDate.now());
|
||||
target.creditUsed = decimal(row.get("credit_used"));
|
||||
target.availableCredit = target.creditLimit.subtract(target.creditUsed).subtract(decimal(row.get("credit_frozen")));
|
||||
if (!target.creditActive && target.creditUsed.signum() > 0) {
|
||||
target.actualBalance = target.actualBalance.subtract(target.creditUsed);
|
||||
target.creditUsed = BigDecimal.ZERO;
|
||||
}
|
||||
target.availableCredit = target.creditActive
|
||||
? target.creditLimit.subtract(target.creditUsed).subtract(creditFrozen) : BigDecimal.ZERO;
|
||||
target.availableBalance = target.creditActive
|
||||
? target.actualBalance.subtract(target.frozenBalance.subtract(creditFrozen)).add(target.availableCredit)
|
||||
: target.actualBalance.subtract(target.frozenBalance);
|
||||
target.accountVersion = row.get("version") instanceof Number value ? value.intValue() : 0;
|
||||
}
|
||||
|
||||
@@ -158,13 +166,15 @@ public class FinanceCustomerAccountService {
|
||||
private BigDecimal creditLimit;
|
||||
private BigDecimal creditUsed = BigDecimal.ZERO;
|
||||
private BigDecimal availableCredit;
|
||||
private LocalDate creditExpiresOn;
|
||||
private boolean creditActive;
|
||||
private int accountVersion;
|
||||
private long pendingRechargeCount;
|
||||
private LocalDateTime lastChangedAt;
|
||||
private MutableMetrics(BigDecimal defaultCreditLimit) {
|
||||
this.creditLimit = defaultCreditLimit;
|
||||
this.availableCredit = defaultCreditLimit;
|
||||
this.availableBalance = defaultCreditLimit;
|
||||
private MutableMetrics() {
|
||||
this.creditLimit = BigDecimal.ZERO.setScale(2);
|
||||
this.availableCredit = BigDecimal.ZERO.setScale(2);
|
||||
this.availableBalance = BigDecimal.ZERO.setScale(2);
|
||||
}
|
||||
void set(String field, BigDecimal value) {
|
||||
switch (field) {
|
||||
@@ -179,6 +189,7 @@ public class FinanceCustomerAccountService {
|
||||
}
|
||||
Metrics freeze() { return new Metrics(outstandingReceivable, pendingBilling, receivedInPeriod, totalCost, paidCost, outstandingPayable,
|
||||
actualBalance, frozenBalance, availableBalance, creditLimit, creditUsed, availableCredit,
|
||||
creditExpiresOn, creditActive,
|
||||
accountVersion, pendingRechargeCount, lastChangedAt); }
|
||||
}
|
||||
|
||||
@@ -186,7 +197,8 @@ public class FinanceCustomerAccountService {
|
||||
BigDecimal receivedInPeriod, BigDecimal totalCost, BigDecimal paidCost,
|
||||
BigDecimal outstandingPayable, BigDecimal actualBalance, BigDecimal frozenBalance,
|
||||
BigDecimal availableBalance, BigDecimal creditLimit, BigDecimal creditUsed,
|
||||
BigDecimal availableCredit, int accountVersion, long pendingRechargeCount,
|
||||
BigDecimal availableCredit, LocalDate creditExpiresOn, boolean creditActive,
|
||||
int accountVersion, long pendingRechargeCount,
|
||||
LocalDateTime lastChangedAt) {
|
||||
}
|
||||
}
|
||||
|
||||
+31
-7
@@ -78,22 +78,46 @@ public class FinancePhase1OperationService {
|
||||
"COALESCE((SELECT available_balance FROM fin_wallet_account w WHERE w.customer_id=c.customer_id),0) available_balance," +
|
||||
"COALESCE((SELECT credit_limit FROM fin_wallet_account w WHERE w.customer_id=c.customer_id),0) credit_limit," +
|
||||
"COALESCE((SELECT credit_used FROM fin_wallet_account w WHERE w.customer_id=c.customer_id),0) credit_used," +
|
||||
"COALESCE((SELECT credit_frozen FROM fin_wallet_account w WHERE w.customer_id=c.customer_id),0) credit_frozen," +
|
||||
"COALESCE((SELECT credit_limit-credit_used-credit_frozen FROM fin_wallet_account w WHERE w.customer_id=c.customer_id),0) available_credit," +
|
||||
"(SELECT credit_expires_on FROM fin_wallet_account w WHERE w.customer_id=c.customer_id) credit_expires_on," +
|
||||
"COALESCE((SELECT version FROM fin_wallet_account w WHERE w.customer_id=c.customer_id),0) account_version," +
|
||||
"(SELECT COUNT(*) FROM fin_recharge_request rr WHERE rr.customer_id=c.customer_id AND rr.status='PENDING') pending_recharge_count," +
|
||||
"GREATEST(COALESCE((SELECT MAX(updated_at) FROM fin_bill b WHERE b.tenant_id=c.customer_id),'1970-01-01'),COALESCE((SELECT MAX(updated_at) FROM fin_receipt r WHERE r.tenant_id=c.customer_id),'1970-01-01'),COALESCE((SELECT MAX(updated_at) FROM fin_cost x WHERE x.tenant_id=c.customer_id),'1970-01-01'),COALESCE((SELECT MAX(updated_at) FROM fin_fee_source f WHERE f.customer_id=c.customer_id),'1970-01-01'),COALESCE((SELECT MAX(updated_at) FROM fin_settlement x WHERE x.tenant_id=c.customer_id),'1970-01-01')) last_changed_at" + base + " ORDER BY last_changed_at DESC,c.customer_id DESC LIMIT ? OFFSET ?";
|
||||
Object[] args = java.util.Arrays.copyOf(filterArgs, filterArgs.length + 2);
|
||||
args[args.length - 2] = pageSize; args[args.length - 1] = (page - 1) * pageSize;
|
||||
List<AccountSummary> rows = jdbc.query(select, (rs, row) -> new AccountSummary(rs.getLong("customer_id"), String.valueOf(rs.getLong("customer_id")), rs.getString("customer_name"), null, null,
|
||||
money(rs, "outstanding_receivable"), money(rs, "pending_billing"), money(rs, "received_period"),
|
||||
money(rs, "total_cost"), money(rs, "paid_cost"), money(rs, "outstanding_payable"),
|
||||
money(rs, "actual_balance"), money(rs, "frozen_balance"), money(rs, "available_balance"),
|
||||
money(rs, "credit_limit"), money(rs, "credit_used"), money(rs, "available_credit"),
|
||||
rs.getInt("account_version"), rs.getLong("pending_recharge_count"),
|
||||
rs.getString("account_status"), time(rs, "last_changed_at")), args);
|
||||
List<AccountSummary> rows = jdbc.query(select, (rs, row) -> accountSummaryRow(rs), args);
|
||||
return PageEnvelope.success(rows, page, pageSize, total == null ? 0 : total);
|
||||
}
|
||||
|
||||
private static AccountSummary accountSummaryRow(ResultSet rs) throws SQLException {
|
||||
BigDecimal actualBalance = money(rs, "actual_balance");
|
||||
BigDecimal frozenBalance = money(rs, "frozen_balance");
|
||||
BigDecimal creditLimit = money(rs, "credit_limit");
|
||||
BigDecimal creditFrozen = money(rs, "credit_frozen");
|
||||
LocalDate creditExpiresOn = rs.getDate("credit_expires_on") == null
|
||||
? null : rs.getDate("credit_expires_on").toLocalDate();
|
||||
boolean creditActive = creditLimit.signum() > 0 && creditExpiresOn != null
|
||||
&& !creditExpiresOn.isBefore(LocalDate.now());
|
||||
BigDecimal creditUsed = money(rs, "credit_used");
|
||||
if (!creditActive && creditUsed.signum() > 0) {
|
||||
actualBalance = actualBalance.subtract(creditUsed);
|
||||
creditUsed = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal availableCredit = creditActive
|
||||
? creditLimit.subtract(creditUsed).subtract(creditFrozen) : BigDecimal.ZERO;
|
||||
BigDecimal availableBalance = creditActive
|
||||
? actualBalance.subtract(frozenBalance.subtract(creditFrozen)).add(availableCredit)
|
||||
: actualBalance.subtract(frozenBalance);
|
||||
return new AccountSummary(rs.getLong("customer_id"), String.valueOf(rs.getLong("customer_id")),
|
||||
rs.getString("customer_name"), null, null, money(rs, "outstanding_receivable"),
|
||||
money(rs, "pending_billing"), money(rs, "received_period"), money(rs, "total_cost"),
|
||||
money(rs, "paid_cost"), money(rs, "outstanding_payable"), actualBalance, frozenBalance,
|
||||
availableBalance, creditLimit, creditUsed, availableCredit, creditExpiresOn, creditActive,
|
||||
rs.getInt("account_version"), rs.getLong("pending_recharge_count"), rs.getString("account_status"),
|
||||
time(rs, "last_changed_at"));
|
||||
}
|
||||
|
||||
public PageEnvelope<LedgerEntry> listDerivedCustomerAccountLedger(long customerId, int page, int pageSize,
|
||||
String businessType, LocalDate startDate, LocalDate endDate) {
|
||||
customer(customerId); page = page(page); pageSize = pageSize(pageSize);
|
||||
|
||||
@@ -53,7 +53,6 @@ public class FinanceWalletService {
|
||||
private final String internalToken;
|
||||
private final String wechatQrCodeUrl;
|
||||
private final String alipayQrCodeUrl;
|
||||
private final BigDecimal defaultCreditLimit;
|
||||
|
||||
public FinanceWalletService(JdbcTemplate jdbc, ObjectMapper objectMapper,
|
||||
FinanceFileClient fileClient,
|
||||
@@ -61,7 +60,6 @@ public class FinanceWalletService {
|
||||
@Value("${baiyue.security.internal-service-token:}") String internalToken,
|
||||
@Value("${baiyue.finance.recharge.wechat-qr-code-url:}") String wechatQrCodeUrl,
|
||||
@Value("${baiyue.finance.recharge.alipay-qr-code-url:}") String alipayQrCodeUrl,
|
||||
@Value("${baiyue.finance.wallet.default-credit-limit:0.00}") BigDecimal defaultCreditLimit,
|
||||
@Value("${spring.profiles.active:}") String activeProfiles,
|
||||
@Value("${DEPLOY_ENV:}") String deployEnvironment) {
|
||||
this.jdbc = jdbc;
|
||||
@@ -69,7 +67,6 @@ public class FinanceWalletService {
|
||||
this.fileClient = fileClient;
|
||||
this.numberGenerator = numberGenerator;
|
||||
this.internalToken = internalToken;
|
||||
this.defaultCreditLimit = nonNegativeMoney(defaultCreditLimit, "默认授信额度");
|
||||
boolean development = "development".equalsIgnoreCase(deployEnvironment.trim())
|
||||
|| List.of(activeProfiles.split(",")).stream().map(String::trim).anyMatch("dev"::equalsIgnoreCase);
|
||||
this.wechatQrCodeUrl = rechargeQrCode(wechatQrCodeUrl, development);
|
||||
@@ -84,7 +81,7 @@ public class FinanceWalletService {
|
||||
public WalletAccount account(long customerId) {
|
||||
requirePositive(customerId, "客户");
|
||||
List<WalletAccount> rows = jdbc.query(
|
||||
"SELECT currency_code,actual_balance,frozen_balance,available_balance,credit_limit,credit_used,credit_frozen,version,updated_at FROM fin_wallet_account WHERE customer_id=?",
|
||||
"SELECT currency_code,actual_balance,frozen_balance,available_balance,credit_limit,credit_used,credit_frozen,credit_expires_on,version,updated_at FROM fin_wallet_account WHERE customer_id=?",
|
||||
(rs, n) -> accountRow(rs), customerId);
|
||||
return rows.isEmpty() ? zeroAccount() : rows.get(0);
|
||||
}
|
||||
@@ -202,7 +199,7 @@ public class FinanceWalletService {
|
||||
if (replay != null) return requiredLedger(replay, customerId);
|
||||
WalletAccount before = accountForUpdate(customerId);
|
||||
if (before.version() != request.expectedAccountVersion()) throw error("FIN-WALLET-409", "账户余额版本已变化,请刷新后重试");
|
||||
if ("DEDUCTION".equals(type) && before.availableBalance().compareTo(amount) < 0) {
|
||||
if ("DEDUCTION".equals(type) && before.creditActive() && before.availableBalance().compareTo(amount) < 0) {
|
||||
throw error("FIN-WALLET-INSUFFICIENT", "扣款金额不能超过当前可用余额");
|
||||
}
|
||||
BigDecimal signed = "RECHARGE".equals(type) ? amount : amount.negate();
|
||||
@@ -225,19 +222,26 @@ public class FinanceWalletService {
|
||||
requirePositive(customerId, "客户"); requirePositive(operatorId, "操作人");
|
||||
if (request == null || request.expectedAccountVersion() < 0) throw bad("授信额度请求无效");
|
||||
BigDecimal limit = nonNegativeMoney(request.creditLimit(), "授信额度");
|
||||
LocalDate expiresOn = request.creditExpiresOn();
|
||||
if (expiresOn == null || !expiresOn.isAfter(LocalDate.now())) {
|
||||
throw bad("授信到期日必须晚于今天");
|
||||
}
|
||||
Long replay = reserve(customerId, operatorId, "WALLET_CREDIT_LIMIT", idempotencyKey, hash(request));
|
||||
if (replay != null) return requiredLedger(replay, customerId);
|
||||
WalletAccount before = accountForUpdate(customerId);
|
||||
if (before.version() != request.expectedAccountVersion()) {
|
||||
throw error("FIN-WALLET-409", "账户余额版本已变化,请刷新后重试");
|
||||
}
|
||||
if (limit.compareTo(before.creditUsed().add(before.creditFrozen())) < 0) {
|
||||
BigDecimal existingDebt = before.actualBalance().signum() < 0
|
||||
? before.actualBalance().negate() : before.creditUsed();
|
||||
if (limit.compareTo(existingDebt.add(before.creditFrozen())) < 0) {
|
||||
throw error("FIN-WALLET-CREDIT-IN-USE", "授信额度不能低于已用与已冻结授信合计");
|
||||
}
|
||||
BigDecimal delta = limit.subtract(before.creditLimit());
|
||||
if (delta.signum() == 0) throw bad("授信额度未发生变化");
|
||||
WalletAccount after = accountState(before.actualBalance(), before.frozenBalance(), limit,
|
||||
before.creditUsed(), before.creditFrozen(), before.version());
|
||||
if (delta.signum() == 0 && expiresOn.equals(before.creditExpiresOn())) throw bad("授信额度和有效期均未发生变化");
|
||||
BigDecimal carriedDebt = existingDebt;
|
||||
WalletAccount after = accountState(before.actualBalance().max(BigDecimal.ZERO), before.frozenBalance(), limit,
|
||||
carriedDebt, before.creditFrozen(), expiresOn, before.version());
|
||||
updateAccount(customerId, after, before.version(), operatorId);
|
||||
WalletLedgerEntry entry = insertLedger(customerId, "CREDIT_LIMIT", delta, after,
|
||||
"CREDIT_LIMIT_ADJUSTMENT", null, LocalDate.now(), null, null,
|
||||
@@ -273,11 +277,12 @@ public class FinanceWalletService {
|
||||
throw error("FIN-WALLET-INSUFFICIENT", "可用余额和授信额度不足");
|
||||
}
|
||||
BigDecimal actualAvailable = before.actualBalance()
|
||||
.subtract(before.frozenBalance().subtract(before.creditFrozen()));
|
||||
.subtract(before.frozenBalance().subtract(before.creditFrozen())).max(BigDecimal.ZERO);
|
||||
BigDecimal actualFrozen = amount.min(actualAvailable);
|
||||
BigDecimal creditFrozen = amount.subtract(actualFrozen);
|
||||
WalletAccount after = accountState(before.actualBalance(), before.frozenBalance().add(amount),
|
||||
before.creditLimit(), before.creditUsed(), before.creditFrozen().add(creditFrozen), before.version());
|
||||
before.creditLimit(), before.creditUsed(), before.creditFrozen().add(creditFrozen),
|
||||
before.creditExpiresOn(), before.version());
|
||||
updateAccount(customerId, after, before.version(), 0);
|
||||
long holdId = insertHold(customerId, businessType, businessNo, amount, actualFrozen, creditFrozen,
|
||||
clean(request.remark(), 500));
|
||||
@@ -315,12 +320,18 @@ public class FinanceWalletService {
|
||||
WalletHold hold = findHold(customerId, businessType, businessNo, true);
|
||||
if (hold == null) throw error("FIN-WALLET-HOLD-404", "资金冻结记录不存在");
|
||||
if (!"FROZEN".equals(hold.status())) throw error("FIN-WALLET-HOLD-409", "资金冻结记录已处理");
|
||||
BigDecimal actual = settle ? before.actualBalance().subtract(hold.actualFrozenAmount()) : before.actualBalance();
|
||||
BigDecimal creditUsed = settle ? before.creditUsed().add(hold.creditFrozenAmount()) : before.creditUsed();
|
||||
BigDecimal actual = before.actualBalance();
|
||||
BigDecimal creditUsed = before.creditUsed();
|
||||
if (settle && before.creditActive()) {
|
||||
actual = actual.subtract(hold.actualFrozenAmount());
|
||||
creditUsed = creditUsed.add(hold.creditFrozenAmount());
|
||||
} else if (settle) {
|
||||
actual = actual.subtract(hold.amount());
|
||||
}
|
||||
BigDecimal frozen = before.frozenBalance().subtract(hold.amount());
|
||||
BigDecimal creditFrozen = before.creditFrozen().subtract(hold.creditFrozenAmount());
|
||||
WalletAccount after = accountState(actual, frozen, before.creditLimit(), creditUsed,
|
||||
creditFrozen, before.version());
|
||||
creditFrozen, before.creditExpiresOn(), before.version());
|
||||
updateAccount(customerId, after, before.version(), 0);
|
||||
String action = settle ? "SETTLED" : "RELEASED";
|
||||
WalletLedgerEntry ledger = insertLedger(customerId, settle ? "CONSUMPTION" : "UNFREEZE",
|
||||
@@ -346,17 +357,17 @@ public class FinanceWalletService {
|
||||
}
|
||||
|
||||
private WalletAccount accountForUpdate(long customerId) {
|
||||
jdbc.update("INSERT IGNORE INTO fin_wallet_account(customer_id,currency_code,actual_balance,frozen_balance,available_balance,credit_limit,credit_used,credit_frozen,version,created_by,updated_by) VALUES(?,'CNY',0,0,?,?,0,0,0,0,0)",
|
||||
customerId, defaultCreditLimit, defaultCreditLimit);
|
||||
jdbc.update("INSERT IGNORE INTO fin_wallet_account(customer_id,currency_code,actual_balance,frozen_balance,available_balance,credit_limit,credit_used,credit_frozen,credit_expires_on,version,created_by,updated_by) VALUES(?,'CNY',0,0,0,0,0,0,NULL,0,0,0)",
|
||||
customerId);
|
||||
return jdbc.queryForObject(
|
||||
"SELECT currency_code,actual_balance,frozen_balance,available_balance,credit_limit,credit_used,credit_frozen,version,updated_at FROM fin_wallet_account WHERE customer_id=? FOR UPDATE",
|
||||
"SELECT currency_code,actual_balance,frozen_balance,available_balance,credit_limit,credit_used,credit_frozen,credit_expires_on,version,updated_at FROM fin_wallet_account WHERE customer_id=? FOR UPDATE",
|
||||
(rs, n) -> accountRow(rs), customerId);
|
||||
}
|
||||
|
||||
private void updateAccount(long customerId, WalletAccount account, int expectedVersion, long operatorId) {
|
||||
int updated = jdbc.update("UPDATE fin_wallet_account SET actual_balance=?,frozen_balance=?,available_balance=?,credit_limit=?,credit_used=?,credit_frozen=?,version=version+1,updated_by=? WHERE customer_id=? AND version=?",
|
||||
int updated = jdbc.update("UPDATE fin_wallet_account SET actual_balance=?,frozen_balance=?,available_balance=?,credit_limit=?,credit_used=?,credit_frozen=?,credit_expires_on=?,version=version+1,updated_by=? WHERE customer_id=? AND version=?",
|
||||
account.actualBalance(), account.frozenBalance(), account.availableBalance(), account.creditLimit(),
|
||||
account.creditUsed(), account.creditFrozen(), operatorId, customerId, expectedVersion);
|
||||
account.creditUsed(), account.creditFrozen(), account.creditExpiresOn(), operatorId, customerId, expectedVersion);
|
||||
if (updated != 1) throw error("FIN-WALLET-409", "账户余额版本已变化,请刷新后重试");
|
||||
}
|
||||
|
||||
@@ -369,17 +380,17 @@ public class FinanceWalletService {
|
||||
KeyHolder holder = new GeneratedKeyHolder();
|
||||
jdbc.update(connection -> {
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO fin_wallet_ledger(customer_id,business_no,change_type,amount,actual_balance_after,frozen_balance_after,available_balance_after,credit_limit_after,credit_used_after,available_credit_after,source_type,source_no,occurred_date,payment_method,reference_no,operator_id,operator_name,remark) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
"INSERT INTO fin_wallet_ledger(customer_id,business_no,change_type,amount,actual_balance_after,frozen_balance_after,available_balance_after,credit_limit_after,credit_used_after,available_credit_after,credit_expires_on_after,source_type,source_no,occurred_date,payment_method,reference_no,operator_id,operator_name,remark) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
Statement.RETURN_GENERATED_KEYS);
|
||||
statement.setLong(1, customerId); statement.setString(2, businessNo); statement.setString(3, changeType);
|
||||
statement.setBigDecimal(4, amount); statement.setBigDecimal(5, account.actualBalance());
|
||||
statement.setBigDecimal(6, account.frozenBalance()); statement.setBigDecimal(7, account.availableBalance());
|
||||
statement.setBigDecimal(8, account.creditLimit()); statement.setBigDecimal(9, account.creditUsed());
|
||||
statement.setBigDecimal(10, account.availableCredit()); statement.setString(11, sourceType);
|
||||
statement.setString(12, sourceNo); statement.setObject(13, occurredDate);
|
||||
statement.setString(14, paymentMethod); statement.setString(15, referenceNo);
|
||||
statement.setLong(16, operatorId); statement.setString(17, clean(operatorName, 128));
|
||||
statement.setString(18, clean(remark, 500)); return statement;
|
||||
statement.setBigDecimal(10, account.availableCredit()); statement.setObject(11, account.creditExpiresOn());
|
||||
statement.setString(12, sourceType); statement.setString(13, sourceNo); statement.setObject(14, occurredDate);
|
||||
statement.setString(15, paymentMethod); statement.setString(16, referenceNo);
|
||||
statement.setLong(17, operatorId); statement.setString(18, clean(operatorName, 128));
|
||||
statement.setString(19, clean(remark, 500)); return statement;
|
||||
}, holder);
|
||||
return requiredLedger(generatedId(holder), customerId);
|
||||
}
|
||||
@@ -392,48 +403,70 @@ public class FinanceWalletService {
|
||||
}
|
||||
|
||||
WalletAccount rechargeAccount(WalletAccount before, BigDecimal amount) {
|
||||
BigDecimal creditRepayment = amount.min(before.creditUsed());
|
||||
return accountState(before.actualBalance().add(amount.subtract(creditRepayment)), before.frozenBalance(),
|
||||
before.creditLimit(), before.creditUsed().subtract(creditRepayment), before.creditFrozen(), before.version());
|
||||
BigDecimal repaidCredit = before.creditActive() ? amount.min(before.creditUsed()) : BigDecimal.ZERO;
|
||||
return accountState(before.actualBalance().add(amount.subtract(repaidCredit)), before.frozenBalance(),
|
||||
before.creditLimit(), before.creditUsed().subtract(repaidCredit), before.creditFrozen(),
|
||||
before.creditExpiresOn(), before.version());
|
||||
}
|
||||
|
||||
WalletAccount deductAccount(WalletAccount before, BigDecimal amount) {
|
||||
BigDecimal actualAvailable = before.actualBalance()
|
||||
.subtract(before.frozenBalance().subtract(before.creditFrozen()));
|
||||
if (!before.creditActive()) {
|
||||
return accountState(before.actualBalance().subtract(amount), before.frozenBalance(),
|
||||
before.creditLimit(), BigDecimal.ZERO, before.creditFrozen(), before.creditExpiresOn(), before.version());
|
||||
}
|
||||
BigDecimal actualFrozen = before.frozenBalance().subtract(before.creditFrozen());
|
||||
BigDecimal actualAvailable = before.actualBalance().subtract(actualFrozen).max(BigDecimal.ZERO);
|
||||
BigDecimal actualDeduction = amount.min(actualAvailable);
|
||||
BigDecimal creditDeduction = amount.subtract(actualDeduction);
|
||||
return accountState(before.actualBalance().subtract(actualDeduction), before.frozenBalance(),
|
||||
before.creditLimit(), before.creditUsed().add(creditDeduction), before.creditFrozen(), before.version());
|
||||
before.creditLimit(), before.creditUsed().add(amount.subtract(actualDeduction)),
|
||||
before.creditFrozen(), before.creditExpiresOn(), before.version());
|
||||
}
|
||||
|
||||
WalletAccount accountState(BigDecimal actual, BigDecimal frozen, BigDecimal creditLimit,
|
||||
BigDecimal creditUsed, BigDecimal creditFrozen, int version) {
|
||||
BigDecimal availableCredit = creditLimit.subtract(creditUsed).subtract(creditFrozen);
|
||||
BigDecimal creditFrozen, LocalDate creditExpiresOn, int version) {
|
||||
return accountState(actual, frozen, creditLimit, BigDecimal.ZERO, creditFrozen, creditExpiresOn, version);
|
||||
}
|
||||
|
||||
WalletAccount accountState(BigDecimal actual, BigDecimal frozen, BigDecimal creditLimit,
|
||||
BigDecimal creditUsed, BigDecimal creditFrozen,
|
||||
LocalDate creditExpiresOn, int version) {
|
||||
boolean creditActive = creditLimit.signum() > 0 && creditExpiresOn != null
|
||||
&& !creditExpiresOn.isBefore(LocalDate.now());
|
||||
if (!creditActive && creditUsed.signum() > 0) {
|
||||
actual = actual.subtract(creditUsed);
|
||||
creditUsed = BigDecimal.ZERO.setScale(2);
|
||||
}
|
||||
BigDecimal availableCredit = creditActive
|
||||
? creditLimit.subtract(creditUsed).subtract(creditFrozen) : BigDecimal.ZERO.setScale(2);
|
||||
BigDecimal actualFrozen = frozen.subtract(creditFrozen);
|
||||
BigDecimal available = actual.subtract(actualFrozen).add(availableCredit);
|
||||
if (actual.signum() < 0 || frozen.signum() < 0 || creditLimit.signum() < 0 || creditUsed.signum() < 0
|
||||
|| creditFrozen.signum() < 0 || actualFrozen.signum() < 0 || actual.compareTo(actualFrozen) < 0
|
||||
|| availableCredit.signum() < 0 || available.signum() < 0) {
|
||||
BigDecimal available = creditActive
|
||||
? actual.subtract(actualFrozen).add(availableCredit)
|
||||
: actual.subtract(frozen);
|
||||
if (frozen.signum() < 0 || creditLimit.signum() < 0 || creditFrozen.signum() < 0
|
||||
|| creditUsed.signum() < 0 || actualFrozen.signum() < 0
|
||||
|| (creditActive && (actual.signum() < 0 || availableCredit.signum() < 0))) {
|
||||
throw error("FIN-WALLET-409", "账户余额状态无效");
|
||||
}
|
||||
return new WalletAccount("CNY", actual, frozen, available, creditLimit, creditUsed,
|
||||
creditFrozen, availableCredit, version, null);
|
||||
creditFrozen, availableCredit, creditExpiresOn, creditActive, version, null);
|
||||
}
|
||||
|
||||
private WalletAccount accountRow(java.sql.ResultSet rs) throws java.sql.SQLException {
|
||||
BigDecimal creditLimit = rs.getBigDecimal("credit_limit");
|
||||
BigDecimal creditUsed = rs.getBigDecimal("credit_used");
|
||||
BigDecimal creditFrozen = rs.getBigDecimal("credit_frozen");
|
||||
return new WalletAccount(rs.getString("currency_code"), rs.getBigDecimal("actual_balance"),
|
||||
rs.getBigDecimal("frozen_balance"), rs.getBigDecimal("available_balance"), creditLimit, creditUsed,
|
||||
creditFrozen, creditLimit.subtract(creditUsed).subtract(creditFrozen), rs.getInt("version"),
|
||||
LocalDate expiresOn = rs.getDate("credit_expires_on") == null ? null : rs.getDate("credit_expires_on").toLocalDate();
|
||||
WalletAccount calculated = accountState(rs.getBigDecimal("actual_balance"), rs.getBigDecimal("frozen_balance"),
|
||||
creditLimit, rs.getBigDecimal("credit_used"), creditFrozen, expiresOn, rs.getInt("version"));
|
||||
return new WalletAccount(calculated.currencyCode(), calculated.actualBalance(), calculated.frozenBalance(),
|
||||
calculated.availableBalance(), calculated.creditLimit(), calculated.creditUsed(), calculated.creditFrozen(),
|
||||
calculated.availableCredit(), calculated.creditExpiresOn(), calculated.creditActive(), calculated.version(),
|
||||
timestamp(rs.getTimestamp("updated_at")));
|
||||
}
|
||||
|
||||
private WalletAccount zeroAccount() {
|
||||
BigDecimal zero = BigDecimal.ZERO.setScale(2);
|
||||
return new WalletAccount("CNY", zero, zero, defaultCreditLimit, defaultCreditLimit, zero,
|
||||
zero, defaultCreditLimit, 0, null);
|
||||
return new WalletAccount("CNY", zero, zero, zero, zero, zero,
|
||||
zero, zero, null, false, 0, null);
|
||||
}
|
||||
|
||||
private long insertHold(long customerId, String businessType, String businessNo, BigDecimal amount,
|
||||
@@ -546,6 +579,7 @@ public class FinanceWalletService {
|
||||
rs.getBigDecimal("amount"), rs.getBigDecimal("actual_balance_after"), rs.getBigDecimal("frozen_balance_after"),
|
||||
rs.getBigDecimal("available_balance_after"), rs.getBigDecimal("credit_limit_after"),
|
||||
rs.getBigDecimal("credit_used_after"), rs.getBigDecimal("available_credit_after"),
|
||||
rs.getObject("credit_expires_on_after", LocalDate.class),
|
||||
rs.getString("source_type"), rs.getString("source_no"),
|
||||
rs.getObject("occurred_date", LocalDate.class), rs.getString("payment_method"), rs.getString("reference_no"),
|
||||
rs.getString("operator_name"), rs.getString("remark"), timestamp(rs.getTimestamp("created_at")));
|
||||
|
||||
Reference in New Issue
Block a user