Files
baiyue-firstleg/finance/src/test/java/com/baiyue/finance/FinanceSettlementStateMapperTest.java
T

124 lines
8.0 KiB
Java

package com.baiyue.finance;
import com.baiyue.finance.mapper.FinanceSettlementStateMapper;
import com.baiyue.finance.repository.FinanceSettlementStateRepository;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class FinanceSettlementStateMapperTest {
/** 真正执行隔离查询,既保留损坏明细供业务拒绝,也不把其他客户占用算入当前客户。 */
@Test
void originalRowsAndOccupancyStayCustomerScoped() {
var fixture = fixture();
fixture.jdbc.update("INSERT INTO fin_fee_source VALUES (32,11,'PAYABLE','CNY',100,'APPROVED','AVAILABLE','ON_ACCOUNT','{}')");
fixture.jdbc.update("INSERT INTO fin_settlement_line VALUES (3,10,88,32,100,'CNY'),(4,11,88,31,100,'CNY')");
try (var session = fixture.factory.openSession()) {
var repository = new FinanceSettlementStateRepository(session.getMapper(FinanceSettlementStateMapper.class));
assertThat(repository.lockSources(10, 88)).hasSize(2);
assertThat(repository.lockSources(10, 88).get(1).get("customer_id")).isEqualTo(11L);
assertThat(repository.sourceOccupied(10, 31, 88)).isFalse();
assertThat(repository.sourceOccupied(11, 31, 88)).isFalse();
assertThat(repository.lockBill(10, 501)).hasSize(1);
assertThat(repository.lockBill(11, 501)).isEmpty();
assertThat(repository.lockBillLines(10, 501)).hasSize(1);
assertThat(repository.lockBillLines(11, 501)).isEmpty();
}
}
/** 同一会话重复校验必须读取最新状态,状态写入继续校验客户与版本。 */
@Test
void stateUpdatesCannotReuseCachedOccupancyOrBypassVersion() throws Exception {
var fixture = fixture();
try (var session = fixture.factory.openSession()) {
var repository = new FinanceSettlementStateRepository(session.getMapper(FinanceSettlementStateMapper.class));
assertThat(repository.sourceOccupied(10, 31, 99)).isFalse();
assertThat(repository.changeStatus("ISSUED", 7, 88, 11, 3)).isZero();
assertThat(repository.changeStatus("ISSUED", 7, 88, 10, 2)).isZero();
assertThat(repository.changeStatus("ISSUED", 7, 88, 10, 3)).isEqualTo(1);
assertThat(repository.sourceOccupied(10, 31, 99)).isTrue();
assertThat(repository.lockSettlement(88).get(0).get("version")).isEqualTo(4);
assertThat(repository.lockSources(10, 88).get(0).get("status")).isEqualTo("AVAILABLE");
try (var statement = session.getConnection().createStatement()) {
statement.executeUpdate("UPDATE fin_fee_source SET status='SUPERSEDED' WHERE id=31");
}
assertThat(repository.lockSources(10, 88).get(0).get("status")).isEqualTo("SUPERSEDED");
}
}
/** 同账单唯一应收采用当前读取,排除自身、作废单及其他客户。 */
@Test
void receivableOccupancyReflectsCurrentActiveSettlement() {
var fixture = fixture();
fixture.jdbc.update("UPDATE fin_settlement SET direction='RECEIVABLE',reconciliation_statement_id=501,status='ISSUED' WHERE id=88");
try (var session = fixture.factory.openSession()) {
var repository = new FinanceSettlementStateRepository(session.getMapper(FinanceSettlementStateMapper.class));
assertThat(repository.billOccupied(10, 501, 99)).isTrue();
assertThat(repository.billOccupied(10, 501, 88)).isFalse();
assertThat(repository.billOccupied(11, 501, 99)).isFalse();
repository.changeStatus("VOIDED", 7, 88, 10, 3);
assertThat(repository.billOccupied(10, 501, 99)).isFalse();
}
}
/** 两个事务争用同一来源时,后到事务须等待并读取先到事务提交后的占用。 */
@Test
void sourceLockSerializesCompetingReopens() throws Exception {
var fixture = fixture();
var executor = Executors.newSingleThreadExecutor();
try (var first = fixture.factory.openSession()) {
var repository = new FinanceSettlementStateRepository(first.getMapper(FinanceSettlementStateMapper.class));
repository.lockSources(10, 88);
var started = new CountDownLatch(1);
var second = executor.submit(() -> {
try (var session = fixture.factory.openSession()) {
var other = new FinanceSettlementStateRepository(session.getMapper(FinanceSettlementStateMapper.class));
started.countDown();
other.lockSources(10, 99);
return other.sourceOccupied(10, 31, 99);
}
});
assertThat(started.await(2, TimeUnit.SECONDS)).isTrue();
assertThatThrownBy(() -> second.get(150, TimeUnit.MILLISECONDS)).isInstanceOf(TimeoutException.class);
repository.changeStatus("ISSUED", 7, 88, 10, 3);
first.commit();
assertThat(second.get(3, TimeUnit.SECONDS)).isTrue();
} finally {
executor.shutdownNow();
}
}
/** 使用独立内存库执行真实 MyBatis SQL,不启动本地业务服务。 */
private static Fixture fixture() {
var dataSource = new DriverManagerDataSource("jdbc:h2:mem:reopen-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=4000", "sa", "");
var jdbc = new JdbcTemplate(dataSource);
jdbc.execute("CREATE TABLE fin_settlement(id BIGINT PRIMARY KEY,tenant_id BIGINT,total_amount DECIMAL(18,2),paid_amount DECIMAL(18,2),outstanding_amount DECIMAL(18,2),payment_status VARCHAR(32),status VARCHAR(32),version INT,direction VARCHAR(32),currency_code VARCHAR(3),supplier_id BIGINT,reconciliation_statement_id BIGINT,updated_by BIGINT)");
jdbc.execute("CREATE TABLE fin_settlement_line(id BIGINT PRIMARY KEY,tenant_id BIGINT,settlement_id BIGINT,fee_source_id BIGINT,amount DECIMAL(18,2),currency_code VARCHAR(3))");
jdbc.execute("CREATE TABLE fin_fee_source(id BIGINT PRIMARY KEY,customer_id BIGINT,direction VARCHAR(32),currency_code VARCHAR(3),amount DECIMAL(18,2),review_status VARCHAR(32),status VARCHAR(32),settlement_mode VARCHAR(32),source_snapshot VARCHAR(500))");
jdbc.execute("CREATE TABLE fin_bill(id BIGINT PRIMARY KEY,tenant_id BIGINT,status VARCHAR(32),currency_code VARCHAR(3),total_amount DECIMAL(18,2),received_amount DECIMAL(18,2))");
jdbc.execute("CREATE TABLE fin_bill_line(id BIGINT PRIMARY KEY,tenant_id BIGINT,bill_id BIGINT,source_id BIGINT,amount DECIMAL(18,2),currency_code VARCHAR(3))");
jdbc.update("INSERT INTO fin_settlement VALUES (88,10,100,0,100,'UNPAID','VOIDED',3,'PAYABLE','CNY',20,NULL,7),(99,10,100,0,100,'UNPAID','VOIDED',3,'PAYABLE','CNY',20,NULL,7)");
jdbc.update("INSERT INTO fin_settlement_line VALUES (1,10,88,31,100,'CNY'),(2,10,99,31,100,'CNY')");
jdbc.update("INSERT INTO fin_fee_source VALUES (31,10,'PAYABLE','CNY',100,'APPROVED','AVAILABLE','ON_ACCOUNT','{}')");
jdbc.update("INSERT INTO fin_bill VALUES (501,10,'ISSUED','CNY',100,0)");
jdbc.update("INSERT INTO fin_bill_line VALUES (1,10,501,31,100,'CNY')");
var configuration = new Configuration(new Environment("test", new JdbcTransactionFactory(), dataSource));
configuration.addMapper(FinanceSettlementStateMapper.class);
return new Fixture(jdbc, new SqlSessionFactoryBuilder().build(configuration));
}
private record Fixture(JdbcTemplate jdbc, SqlSessionFactory factory) { }
}