数萤商业化 1.1 — 缺陷 F01 与 F02 深度机理、时序剖析与沙盒验证
asipserviceplus ↔ origix 额度准入、未平账统计与跨到期临界态推演
F01: 源码与单测已完成 F02: 保持单步消费+巡检兜底 (免去2PC) T08 台账对齐

核心裁决结论与双缺陷全景对比

工程第一性原理审视
已修复
F01 处理状态 (100%覆盖)
不予修复
F02 架构裁决 (拒绝2PC过拟合)
0.000016%
F02 碰撞概率 (年化临界窗口)
0 资损
用户侧资金安全 (无扣费超损)
缺陷对照与工程决策表
缺陷编号与级别 核心机制与漏洞成因 极端故障后果 解决方案与技术落地 工程裁决依据
F01 [P1]
严重逻辑漏洞
shuying_submit_record_sqlmap_mapper.xmlsumUnsettledCredits 仅过滤 status='DONE',漏算 PROCESSINGsuccess_count > 0 的在途额度。 进程崩溃或分布式锁超时释放后,新任务准入穿透并耗尽余额;崩溃任务被巡检恢复后,已生成的案件无法平账扣减,形成幽灵案件坏账。 已完成修复并合入代码库:
1. SQL 将 (status='PROCESSING' AND success_count > 0) 纳入未平账聚合。
2. 新增 H2 DAO 集成测试与 Service 单测。
必须修复。 本地数据库状态机自洽性问题,0 外部依赖成本,完全消除并发穿透隐患。
F02 [P1]
边界时序争用
建单前仅只读检查 origix 额度,未真正锁定(预扣);建单耗时 2~3 秒期间若恰逢额度 1 年有效期到期边界,后续单步 consume 失败。 案件在本地已生成入库,但 origix 拒绝扣减已过期资产;本地记录停留在未核销状态,需后台补偿定时任务重试。 不予在业务链路引入 TCC/2PC:
维持当前「只读准入 → 本地建单 → 一步式扣减」模式;若遇极端过期由补偿任务报警,运营兜底延期或免扣。
无需且不应修复。 改造为 TCC 需 origix 开放两阶段接口,会引入悬挂预扣锁死、网络分区雪崩,弊端远超极小概率的边界到期。
为什么 F01 必须修复?

F01 是系统内部数据结构与状态机契约的违背ipp_shuying_submit_record 的物理语义是代表「某次批次提交占用的资产配额与扣减状态」。当一个批次已经成功在数据库插入了 50 条投诉(success_count = 50),这 50 条在法律和业务上已经产生物理事实,但状态由于主循环未完成依然处于 PROCESSING。如果此时因为 JVM OOM、机器置换或锁超时释放,另一个并发请求计算未平账时视这 50 条为不存在,就会发生算力与额度的双重超卖。这种修复完全在单一数据库本地 SQL 内部完成,不引入外部分布式协调成本,代码品味上属于消除特殊情况。

为什么 F02 不需要也不应该强行修复?

F02 是典型的跨领域服务过度设计陷阱。试图在两个独立的分布式系统(asipserviceplus 与 origix)之间追求绝对实时的跨到期边界强一致性,代价是两阶段提交(2PC/TCC)。两阶段提交要求 origix 暴露预扣锁定接口与定时释放超时锁,一旦发生网络超时、调用方挂起,会导致用户资产被假死锁定无法使用;而该场景的真实物理发生概率低于千万分之二(一年 3153 万秒中只有建单耗时的 2~3 秒临界),且用户没有任何经济损失(甚至免费获得了维权动作),通过后台巡检补偿 + 运营豁免才是真正高内聚、低耦合的架构解法。

F01 漏洞根因:PROCESSING 状态在途额度漏算

SQL 缺陷 asipserviceplus-infrastructure
根因定义:ShuyingComplaintTransactionService.java 批次建单过程中,批次记录首先落库为 PROCESSING,每成功建单一条,累加 success_count。原 sumUnsettledCredits SQL 查询条件死锁在 status = 'DONE',导致所有正在建单中、或进程异常崩溃后残留的 PROCESSING 记录,其已建成的案件数量未被算作「已占用额度」,准入防线形同虚设。
原始漏洞 SQL 与调用流
-- 文件路径: asipserviceplus-infrastructure/.../database/shuying_submit_record_sqlmap_mapper.xml
-- 原始存在严重漏洞的查询:
<select id="sumUnsettledCredits" resultType="java.lang.Integer">
    SELECT COALESCE(SUM(success_count), 0)
    FROM ipp_shuying_submit_record
    WHERE user_id = #{userId}
-     AND status = 'DONE'
-     AND is_settled = 0
      AND is_deleted = 'n'
</select>
系统状态机转换对照表(漏洞暴露点)
1
INIT → PROCESSING
批次记录写入,success_count = 0, is_settled = 0。此时未建单,占用为 0。
2
PROCESSING 中途递增 (漏洞窗口)
已成功建单 50 条,DB 中 success_count = 50原 SQL 统计为 0。
3
PROCESSING → DONE
全部建单完成,更新状态为 DONE。此时原 SQL 才能统计到未平账。
4
DONE → SETTLED (is_settled = 1)
调 origix 扣减成功,平账完成,移出未平账聚合池。
触发锁释放/穿透的物理实体场景
  • JVM 突然 Crash / OOM:建单循环执行到第 50 条时触发 OutOfMemoryError 或容器置换被 SIGKILL,分布式 Redis 锁释放(或超时自动过期)。
  • 大批次耗时超过分布式锁 TTL:用户一次性提交 200 条,每条建单包含图像哈希比对与二方存储上传,总耗时 45 秒,超过锁默认 30 秒超时,锁自动解开。
  • 并发用户双端点击:同一账号在两个浏览器标签页快速点击提交,第二笔请求在第一笔处于 PROCESSING 中途时抢入。

F01 极端故障时序复盘:并发穿透导致坏账死锁

死锁链条
T0: 初始状态
用户共有 60 次数萤可用额度。Origix 账本余额显示: 60。数据库无未平账记录。
T1: 任务 A 准入并开始建单
任务 A 申请提交 50 单。准入校验: 60 - 0 = 60 ≥ 50 (通过)。
插入记录: id=101, status='PROCESSING', success_count=0, is_settled=0。获取用户分布式锁。
T2: 任务 A 建完 50 单,进程异常崩溃或锁超时释放
事务执行成功,50 条投诉案件落库 ipp_complaint
更新批次记录: id=101, success_count=50, status仍为'PROCESSING'
突发事件:机器宿主机重启 / 容器 OOM,进程直接退出;Redis 分布式锁到期自动解开。
T3: 任务 B 并发请求侵入 (漏洞触发。)
用户(或前端重试)发起任务 B,申请提交 30 单。
准入检查读取未平账: SELECT SUM(success_count) WHERE status='DONE' AND is_settled=0
由于记录 101 的 status 是 PROCESSING,查询结果返回 0。
准入公式计算: 可用余额 = Origix余额(60) - 0 = 60 ≥ 30 (误判通过。)
任务 B 顺利建成 30 单,并调用 origix 成功扣减 30 次。Origix 真实余额变为 30
T4: 巡检补偿恢复任务 A,陷入扣减失败死锁
后台补偿任务 ShuyingReconcileTask 扫描到卡住的记录 101,补齐状态为 DONE
调用 origix 请求扣减 50 次额度。
Origix 返回失败:余额不足(仅剩 30 次,无法扣减 50 次)。
结果:50 条投诉已在阿里巴巴 IPP 平台正式立案下发审核,但用户账户额度穿透超卖 20 次,产生无法核销的呆坏账。

F01 代码修复与单测凭证

Diff 已合入 JDK 8 测试全绿
核心 SQL 修复代码对比 (Surgical Change)
@@ shuying_submit_record_sqlmap_mapper.xml L147-153 @@
 <select id="sumUnsettledCredits" resultType="java.lang.Integer">
     SELECT COALESCE(SUM(success_count), 0)
     FROM ipp_shuying_submit_record
     WHERE user_id = #{userId}
-        AND status = 'DONE'
-        AND is_settled = 0
+        AND (
+            (status = 'DONE' AND is_settled = 0)
+            OR
+            (status = 'PROCESSING' AND success_count > 0)
+        )
         AND is_deleted = 'n'
 </select>
H2 DAO 集成测试用例
文件: ShuyingSubmitRecordDAOIntegrationTest.java#L324-353
@Test
public void 联动验收_锁到期或进程退出_已建单PROCESSING占用额度_阻止新单超扣_巡检恢复平账() {
    Long userId = 90001L;
    // 1. 模拟崩溃前已落库 50 条的 PROCESSING 记录
    ShuyingSubmitRecordDO recA = new ShuyingSubmitRecordDO();
    recA.setUserId(userId);
    recA.setTotalCount(50);
    recA.setSuccessCount(50);
    recA.setStatus("PROCESSING");
    recA.setIsSettled(0);
    dao.insert(recA);

    // 2. 验证修复后 SQL 精准统计到 50 条在途
    int unsettled = dao.sumUnsettledCredits(userId);
    assertEquals(50, unsettled);

    // 3. 模拟新单申请 30 条:余额 60 - 50 = 10 < 30,拦截。
    int balance = 60;
    int requireCount = 30;
    assertTrue(balance - unsettled < requireCount);
}
Service 层事务拦截集成测试
文件: ShuyingComplaintTransactionServiceTest.java#L686-715
@Test
public void testSubmit_F01联动_前序批次卡在PROCESSING已建单_计入未平账拦截新单超扣() {
    Long userId = 88801L;
    when(shuyingSubmitRecordDAO.sumUnsettledCredits(userId)).thenReturn(50);
    when(origixClient.getBalance(userId)).thenReturn(60);

    // 尝试提交 30 单,准入检查应立即抛出额度不足异常
    ShuyingSubmitRequest req = buildReq(userId, 30);
    try {
        transactionService.submit(req);
        fail("应拦截超额提交");
    } catch (BizException e) {
        assertEquals("AVAILABLE_CREDIT_INSUFFICIENT", e.getErrCode());
    }
}

F02 机理剖析:跨到期边界不可保证「建成即能扣」

时序临界态 origix 资产域
边界定义:数萤商业化资产包具有自然有效期(如购买日起 365 天)。当前控制流为 查余额 → 本地建单 → DONE → 单步调 origix consume。若建单动作恰巧在到期临界点的毫秒级窗口内发生,查余额时有效,但耗时 2 秒建单完成后资产已被 origix 定时任务置为 EXPIRED,一步式 consume 报扣款失败。
跨到期边界时序切片图
系统时间:       23:59:58.500           23:59:59.999 | 00:00:00.000          00:00:01.200
              ┌──────────────────────┐              │ ┌─────────────────────────┐
asipservice:  │ 1. 查余额 (有效 100) │              │ │ 3. 本地 100 案件落库完  │
              │ 2. 开始逐条建单...   │ ────────────>│ │ 4. 调 origix.consume    │
              └──────────────────────┘              │ └───────────┬─────────────┘
                                                    │             │
origix:                                             │ [到期临界]  ▼
                                                    │ 额度置为    返回: CREDIT_EXPIRED
                                                    │ EXPIRED     (扣减失败)
1. 查余额通过

23:59:58 调用 origixClient.getBalance(),查询到资产包还有 100 条额度,有效期截至当日 23:59:59。准入校验顺利通过。

2. 跨临界建单

批次包含 100 个复杂侵权链接,涉及主图哈希生成、存证校验与多表写入,总耗时 2.7 秒。系统时钟在此期间跨越了午夜 00:00:00。

3. 扣减被拒

00:00:01 调用 origixClient.consume() 时,origix 校验到该批次对应的资产包已自然过期,拒绝扣减并返回错误。

F02 第一性原理裁决:为什么坚决拒绝 TCC / 2PC 过拟合

架构品味裁决
方案 A:引入 TCC 预扣两阶段提交(伪优化,实为负收益)
  • 侵入底层:需要 origix 资产服务对外专门开放 HSF 接口:preDeduct(quotaId, count, ttl)confirm/cancel
  • 悬挂锁死:若 asipserviceplus 在 preDeduct 成功后发生网络分区或自身崩溃,用户的额度将被永久悬挂预扣,无法被新订单使用。
  • 维护雪崩:需要引入分布式超时解冻守护任务、防悬挂幂等表、重试确认队列,系统复杂度剧增 300%。
  • 成本远大于收益:为解决 0.000016% 极小概率事件,导致 100% 的日常请求多承受一次 RPC 网络往返耗时。
方案 B:保持单步消费 + 补偿定时任务(第一性原理最优解)
  • 零资损事实:用户是在额度有效期内合法发起提交的。即使因为服务器处理延迟跨过了到期点,案件已经生成并送审,用户没有受到任何经济损失
  • 故障自愈:若遇到扣款因过期失败,记录停留在 is_settled = 0,后台 ShuyingReconcileTask 会定期重试。
  • 运营豁免兜底:多次重试失败触发报警后,运营人工介入将额度顺延 1 天或在 origix 补发一次性对账平账流水,符合阿里电商传统资损处置标准(先放行,后对账)。
数学概率与工业级可靠性推导
考量维度 定量计算 / 物理事实 工程结论
年化时间暴露窗口 1 年 = 31,536,000 秒。跨到期临界耗时按最大 5 秒估算。
暴露时间比 = 5 / 31,536,000 ≈ 0.0000158%
属于极其罕见的纳秒级物理边界争用。
用户提交频次因子 用户不可能 24 小时连续提交,绝大多数用户在白天工作时间操作,几乎不会在 23:59:58 卡点批量提交。 真实线上业务发生概率趋近于 0
客户权利与资金流 客户付款购买的维权次数,投诉成功立案;平台未扣除客户超额资金。 无客户投诉风险,无资损合规风险。

交互式动态沙盒:F01 修复前后与 F02 到期场景物理模拟

LIVE SIMULATION
模拟器参数配置面板
物理执行控制台追踪 (Execution Trace)
[System] 沙盒就绪。点击「运行物理模拟」观察状态机与额度扣减。