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
@@ -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,
@@ -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) {
}
}
@@ -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")));