会员周期与独立增量池
java-db 的线程绑定事务可以将业务原操作、周期额度和结果引用原子提交;PostgreSQL 唯一约束及条件更新负责最后一个单位的并发互斥。会籍按固定期限计算,额度按业务时区自然日/月计算,二者分别建模,可以清楚解释续费、升级和实际剩余。
会籍和额度分别记录
会籍表达账号在哪个区间享有什么承诺;额度表达责任账号在某个自然周期已经使用和保留多少量。周期键使用“责任账号+能力池+来源+周期种类+周期起点”。同月续费只变更有效承诺,不能清空同月已用量;降级后剩余取非负值,历史用量不回写为新上限。
下面的独立示例只演示会员额度消费。实际业务还应先核当前账号、资源与能力权限,并将实际执行结果同事务提交。调用者传入的日/月上限应来自服务端已锁定且当前有效的承诺。
create table demo_member_bucket (
id bigint primary key,
account_id bigint not null,
pool_code varchar(24) not null,
period_kind varchar(8) not null,
period_start date not null,
used_qty bigint not null default 0,
held_qty bigint not null default 0,
unique(account_id, pool_code, period_kind, period_start),
check(used_qty >= 0 and held_qty >= 0)
);
create table demo_member_use (
id bigint primary key,
account_id bigint not null,
pool_code varchar(24) not null,
original_operation varchar(80) not null,
unique(account_id, pool_code, original_operation)
);
import java.sql.Connection;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.List;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;
public final class MemberQuotaService {
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
public void consume(long accountId, String pool, String originalOperation,
Instant acceptedAt, long dayLimit, long monthLimit) {
if (!List.of("PUBLISH", "MIMI", "SUPPLEMENT").contains(pool)
|| dayLimit <= 0 || monthLimit < dayLimit) {
throw new IllegalArgumentException("Invalid current entitlement");
}
boolean committed = Db.tx(Connection.TRANSACTION_READ_COMMITTED, () -> {
// The same account and ability use a consistent transaction lock.
Db.query("select pg_advisory_xact_lock(hashtextextended(?,0))",
"member:" + accountId + ":" + pool);
Row previous = Db.findFirst("""
select id from demo_member_use
where account_id=? and pool_code=? and original_operation=?
""", accountId, pool, originalOperation);
if (previous != null) {
return true;
}
LocalDate day = acceptedAt.atZone(BUSINESS_ZONE).toLocalDate();
for (String period : List.of("DAY", "MONTH")) {
LocalDate start = period.equals("DAY") ? day : day.withDayOfMonth(1);
long limit = period.equals("DAY") ? dayLimit : monthLimit;
Db.update("""
insert into demo_member_bucket(id,account_id,pool_code,period_kind,period_start)
values(?,?,?,?,?) on conflict do nothing
""", SnowflakeIdUtils.id(), accountId, pool, period, start);
int changed = Db.update("""
update demo_member_bucket set used_qty=used_qty+1
where account_id=? and pool_code=? and period_kind=? and period_start=?
and used_qty+held_qty<?
""", accountId, pool, period, start, limit);
if (changed != 1) {
throw new IllegalStateException("Current calendar quota exhausted");
}
}
Db.update("""
insert into demo_member_use(id,account_id,pool_code,original_operation)
values(?,?,?,?)
""", SnowflakeIdUtils.id(), accountId, pool, originalOperation);
return true;
});
if (!committed) {
throw new IllegalStateException("Member quota transaction rolled back");
}
}
}
先扣日再扣月仍是同一事务:月额度不足时,日额度的修改一起回滚。需要等待审核的能力采用 held/used 两个量,受理保留、终局核销或释放,并保留原周期引用。
报价、卡与实际服务
升级报价持久化保存剩余秒数、逐项分摊和到期,不在确认时重新用当前时间悄悄换价。连续服务差额逐行计算 floor(整期分摊差额×剩余秒数/整期秒数);卡按新增完整单位独立分摊。现金收款核实后才切换承诺,升级不自动延长原会籍。
派生卡绑定原现金行、独立授予时点和有效期。授予是数量事实,实际合法揭示才是联系服务交付;连续服务则按已核资格区间的并集累计秒数。服务中断、未核区间和退款在途分开记录,使用累计金额计算避免逐小段取整漂移。
向 JSON 边界返回白名单 DTO 或已归一化的业务值。JDBC 数组先转换成业务数组或在 SQL 中提取各元素,不把 JDBC 连接、结果集或驱动对象作为响应和持久快照序列化。ID 以字符串传输,数据库金额和计量仍用整数。
同一现金行的退款与补交共用剩余责任。补交明确记录新服务窗口或批次,原会籍期限、历史用量和原收入不变化。订单级现金退回缺少行归属时单独保留待核范围,不能猜测四项之间的分摊。
推荐验证并发最后单位、同月续费、升级后剩余、跨月新池、返回丢失重放、迟到付款和退款与实际使用竞争。可控时钟与隔离数据库让这些边界能够稳定重放。
