This commit is contained in:
2026-10-20 23:49:55 +08:00
parent 35a1b835fe
commit af5cf1e3bd
37 changed files with 714 additions and 184 deletions
@@ -0,0 +1,21 @@
package com.baiyue.finance.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/** 本服务业务编号序列,更新与读取必须处于同一事务连接。 */
@Mapper
public interface DocumentSequenceMapper {
/** 唯一序列键原子递增,并在连接内保存本次分配值。 */
@Insert("INSERT INTO fin_document_sequence(sequence_key,current_value) VALUES(#{key},LAST_INSERT_ID(1)) "
+ "ON DUPLICATE KEY UPDATE current_value=LAST_INSERT_ID(current_value+1)")
int allocate(@Param("key") String key);
/** 禁用一级和二级缓存,返回本事务刚刚分配的值。 */
@Select("SELECT LAST_INSERT_ID()")
@Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE)
Long allocatedValue();
}
@@ -0,0 +1,24 @@
package com.baiyue.finance.repository;
import com.baiyue.finance.mapper.DocumentSequenceMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/** 各业务共用本库序列,独立短事务避免持有编号锁进入业务资源锁。 */
@Repository
public class DocumentSequenceRepository {
private final DocumentSequenceMapper mapper;
/** 只访问所属服务的编号表。 */
public DocumentSequenceRepository(DocumentSequenceMapper mapper) { this.mapper = mapper; }
/** 已分配号不因外层业务回滚而复用,允许失败请求产生断号。 */
@Transactional(propagation = Propagation.REQUIRES_NEW)
public long next(String key) {
mapper.allocate(key);
Long value = mapper.allocatedValue();
if (value == null || value < 1) throw new IllegalStateException("业务编号分配失败");
return value;
}
}
@@ -1,55 +1,34 @@
package com.baiyue.finance.service;
import com.baiyue.common.core.number.DailyDocumentNumber;
import com.baiyue.finance.repository.DocumentSequenceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.Clock;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Set;
/** 按北京时间生成两至四位前缀、八位日期和四位日流水。 */
@Component
public class FinanceDocumentNumberGenerator {
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMdd", Locale.ROOT);
private static final Set<String> SUPPORTED_PREFIXES = Set.of("REC", "AR", "AP", "RC", "AC", "VC", "VP");
private static final long MAX_DAILY_SEQUENCE = 9_999L;
private final JdbcTemplate jdbc;
private static final Set<String> PREFIXES = Set.of("REC", "AR", "AP", "RC", "AC", "VC", "VP", "PAY");
private final DocumentSequenceRepository sequences;
private final Clock clock;
/** 正式运行使用统一业务时区。 */
@Autowired
public FinanceDocumentNumberGenerator(JdbcTemplate jdbc) {
this(jdbc, Clock.system(BUSINESS_ZONE));
public FinanceDocumentNumberGenerator(DocumentSequenceRepository sequences) {
this(sequences, Clock.system(DailyDocumentNumber.BUSINESS_ZONE));
}
FinanceDocumentNumberGenerator(JdbcTemplate jdbc, Clock clock) {
this.jdbc = jdbc;
/** 固定时钟用于跨日及前缀独立性验证。 */
FinanceDocumentNumberGenerator(DocumentSequenceRepository sequences, Clock clock) {
this.sequences = sequences;
this.clock = clock;
}
@Transactional(propagation = Propagation.MANDATORY)
/** 只为本服务已登记的业务类型分配号码。 */
public String next(String prefix) {
if (!SUPPORTED_PREFIXES.contains(prefix)) {
throw new IllegalArgumentException("unsupported finance document prefix: " + prefix);
}
LocalDate sequenceDate = LocalDate.now(clock);
String sequenceKey = prefix + ":" + DateTimeFormatter.BASIC_ISO_DATE.format(sequenceDate);
jdbc.update(
"INSERT INTO fin_document_sequence(sequence_key,current_value) " +
"VALUES(?,LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE " +
"current_value=LAST_INSERT_ID(current_value+1)",
sequenceKey);
Long sequence = jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
if (sequence == null || sequence <= 0 || sequence > MAX_DAILY_SEQUENCE) {
throw new FinanceEventException("FIN_409", "当日" + prefix + "单据编号已用尽");
}
return prefix + "-" + DATE_FORMAT.format(sequenceDate) + "-" +
String.format(Locale.ROOT, "%04d", sequence);
if (!PREFIXES.contains(prefix)) throw new IllegalArgumentException("未登记的业务编号前缀:" + prefix);
return DailyDocumentNumber.next(prefix, clock, sequences::next);
}
}
@@ -521,9 +521,9 @@ public class FinanceSettlementService {
"outstandingAmount", decimal(settlement, "outstanding_amount"), "status", status);
}
/** 结算收付款使用独立 PAY 日流水,重试由原业务幂等记录恢复。 */
private String transactionNo() {
return "PAY-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-" +
UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase(Locale.ROOT);
return documentNumbers.next("PAY");
}
private String hash(Object value) {