diff --git a/docs/codebase-modularization-roadmap.md b/docs/codebase-modularization-roadmap.md index 499f44a..5af6d33 100644 --- a/docs/codebase-modularization-roadmap.md +++ b/docs/codebase-modularization-roadmap.md @@ -1171,3 +1171,4 @@ global,不能错误归入client。`client-signature-*`、发送页、企业认 - `billing.service`集中维护余额锁和流水;`settleFrozenCharge`利用冻结已完成扣减、释放与扣费净变化为零的事实,以单个幂等SQL写成流水对且不争抢账户锁,只有历史半完成恢复才进入带锁余额补扣;`send-accounting`只编排消息计费状态。 - `send-chain.helpers`提供无副作用的优先级、主备和稳定weight选择;提交服务只提供消息稳定键与真实在线/报备候选,不保存进程内轮询状态。 - Gateway既有Submit池与结果Outbox继续独立有界;先以正价六通道实测定位容量,只有证据证明单条回调仍触发停止线时才扩展批量回调协议。 +- Gateway上游模块对单分片使用聚合结果作为唯一Outbox边界,对多分片保留逐片持久化;结果Outbox不承担业务去重猜测,事件数量由上游已知分片数确定。 diff --git a/docs/first-version-development-requirements.md b/docs/first-version-development-requirements.md index cc0224d..b97a627 100644 --- a/docs/first-version-development-requirements.md +++ b/docs/first-version-development-requirements.md @@ -2135,4 +2135,5 @@ - 普通CMPP短短信批量路径必须支持正单价:按企业固定锁序一次校验批次总金额、一次更新余额,并为每短信写独立幂等冻结流水;任务、请求、消息与冻结流水同属一个短事务。余额加授信必须覆盖所需金额,不能只判断大于零。 - 供应商接受后,释放冻结与正式扣费以一个原子SQL写入两条幂等审计流水;二者净余额变化为零,不得争抢企业账户锁或重复更新账户。只有恢复历史“已释放但未扣费”半完成状态时才取得账户锁补扣。零金额不创建AccountTransaction;拒绝、超时、重投和最终失败继续沿用逐短信幂等释放、扣费和退款。 - 同通道组内在线、已报备、同地域、同最低优先级且非备用的通道按weight和稳定消息键分流;备用、离线及更低优先级不抢占。只有显式配置为同优先级非备用的通道才参与主动双活。 +- 单分片短短信的聚合SubmitResult已经携带完整分片信息,只发布聚合Outbox事件;不得再发布内容相同的分片事件造成双倍API回调和数据库写入。多分片长短信仍须在发送下一片前持久化当前分片事件,保持崩溃恢复边界。 - 压测使用隔离测试企业、真实PostgreSQL账务、单价`0.0325元/计费条`、三运营商混合号码和六个模拟供应商账号;逐档核对消息、冻结/释放/扣费/退款、SmsBillingRecord、通道分布、队列和数据库。临时单价与主动双活配置须先快照、测试后恢复。 diff --git a/docs/system-functional-test-cases.md b/docs/system-functional-test-cases.md index 6f25e24..90019a0 100644 --- a/docs/system-functional-test-cases.md +++ b/docs/system-functional-test-cases.md @@ -4785,3 +4785,4 @@ npm run verify:phase8 | TC-CMPP-500-P4-006 | 三运营商、六通道、325金额单位阶梯压测 | 每档入口/Inbox/供应商/回执/账务对账一致,双Stream和数据库最终稳定排空;失败即停止升档 | | TC-CMPP-500-P4-007 | priority与normal并发积压 | priority保持明确服务能力且normal最终不饿死 | | TC-CMPP-500-P4-008 | 测试配置治理 | 单价、账户与组项目先快照;临时主动双活和单价测试后恢复;预生产和凭据不变 | +| TC-CMPP-500-P4-009 | 单分片与多分片供应商Submit | 单分片只产生一个含segments的聚合Outbox事件;多分片逐片持久化并另有聚合事件,任何分片不丢失 | diff --git a/docs/testing-progress.md b/docs/testing-progress.md index 606cd88..13e19c5 100644 --- a/docs/testing-progress.md +++ b/docs/testing-progress.md @@ -3843,3 +3843,4 @@ git diff --check - 首轮正价三运营商100条/秒生成2999条,2999/2999 SubmitResp成功、P50/P95/P99=`32/68/130ms`,Inbox与唯一MessageId均2999;移动/联通/电信分别998/996/1005,六通道最终各承载488至514条,证明主动双活与跨运营商分流有效。注入结束时仅388条完成扣费,命令Stream`128/2131`、结果Outbox`32/192`、等待锁/idle in transaction各5,按停止线不升200档。 - 该轮最终双Stream归零,冻结/释放各2999、正式扣费2987、退款30、12条供应商最终拒绝只释放不扣费;SmsBillingRecord为charged2957/refunded30,余额恒等式精确成立,六通道与数据库最终无等待锁/idle in transaction。证据确认瓶颈是每个正价Submit结果在企业账户锁上串行,而不是单价0时可见的通道容量。 - 后续最小修复将正常“释放冻结+正式扣费”改为单个幂等SQL:冻结已在入口扣减,流水对净余额为零,因此正常结算不再取得账户锁或更新账户;历史已释放未扣费状态仍用原带锁路径补扣,并保留并发`ON CONFLICT`后一次MVCC恢复读。专项134项和TypeScript已通过,完整门禁、修复提交、新恢复资产与复测待补记。 +- 无账户锁修复后100条/秒复测仍在注入结束出现命令Stream`128/1687`、结果Outbox`32/452`;相同观测点完成扣费从388提高至637、等待锁降为0,但单分片短信同时发布分片与聚合两个结果事件,仍造成约双倍API回调和分片审计写入。新增最小Gateway修复:单分片只发布已携带完整segments的聚合事件,多分片仍逐片先持久化,待新恢复资产和同口径复测。 diff --git a/gateway/internal/upstream/submit.go b/gateway/internal/upstream/submit.go index cb8f35b..d2372fc 100644 --- a/gateway/internal/upstream/submit.go +++ b/gateway/internal/upstream/submit.go @@ -65,7 +65,11 @@ func (p *connectionPool) submit( release() segment := submitSegmentResult(part, seq, gatewayMessageID, result) segments = append(segments, segment) - if onSegment != nil { + // A one-part Submit is fully represented by the aggregate event below; + // publishing an identical segment event doubles API callbacks and database + // writes without adding crash-recovery evidence. Multi-part messages still + // persist every segment before advancing to the next supplier Submit. + if onSegment != nil && len(parts) > 1 { if publishErr := onSegment(segment); publishErr != nil { result.Segments = segments return result, publishErr