评审多商户外卖系统二次开发版本时,抽成变更需要同时检查新订单和历史订单。若重试请求读到最新费率,可能改写原交易含义。本文给出订单规则快照和幂等约束的完整本地实现,用输入输出核对升级兼容这一局部问题。
模型与约束先于代码
示例订单包含orderId、merchantId、amountCents、ruleRevision、rateBps和commissionCents。订单号在模型内唯一,金额为正安全整数且单位为分,修订号为正安全整数,费率用0到10000的整数基点。商家身份由可信上游认证,示例参数不能替代授权检查。
创建订单时复制当前规则,已存在订单在商家和金额相同的重试中返回原结果。相同订单号改商家或金额则冲突。计算使用BigInt避免中间乘法精度损失,最终金额向下取整到分,这是示例舍入策略,实际规则由业务与支付约定确定。
微订官网产品与业务全景介绍,系统支持多商户入驻、商家独立后台、骑手抢单和派单、平台抽成、商家结算、提现、多区域及多分站运营。客户可以先使用标准SaaS产品,也可以按项目需要采用独立品牌、私有化部署、源码安装或个性化开发。
完整核心逻辑及断言
const assert = require('node:assert/strict');
// An order retains the rule accepted at creation, including on retry.
class OrderRules {
constructor() {
this.orders = new Map();
}
create(orderId, merchantId, amountCents, rule) {
if (typeof orderId !== 'string' || !orderId.trim() || typeof merchantId !== 'string' || !merchantId.trim()) throw Error('INVALID_ID');
if (!Number.isSafeInteger(amountCents) || amountCents <= 0) throw Error('INVALID_AMOUNT');
if (!rule || !Number.isSafeInteger(rule.revision) || rule.revision < 1 || !Number.isSafeInteger(rule.rateBps) || rule.rateBps < 0 || rule.rateBps > 10000) throw Error('INVALID_RULE');
const previous = this.orders.get(orderId);
if (previous) {
if (previous.merchantId !== merchantId || previous.amountCents !== amountCents) throw Error('IDEMPOTENCY_CONFLICT');
return previous;
}
const commissionCents = Number(BigInt(amountCents) * BigInt(rule.rateBps) / 10000n);
const order = Object.freeze({
orderId, merchantId, amountCents, ruleRevision: rule.revision, rateBps: rule.rateBps, commissionCents});
this.orders.set(orderId, order);
return order;
}
}
const model = new OrderRules();
const rule = {
revision: 1, rateBps: 500};
const oldOrder = model.create('O1', 'M1', 2000, rule);
assert.equal(oldOrder.commissionCents, 100);
rule.revision = 2;
rule.rateBps = 800;
assert.equal(oldOrder.rateBps, 500);
assert.equal(oldOrder.ruleRevision, 1);
assert.equal(model.create('O1', 'M1', 2000, rule), oldOrder);
const newOrder = model.create('O2', 'M1', 2000, rule);
assert.equal(newOrder.commissionCents, 160);
assert.equal(newOrder.ruleRevision, 2);
assert.throws(() => model.create('O1', 'M1', 2100, rule), /IDEMPOTENCY_CONFLICT/);
assert.throws(() => model.create('O1', 'M2', 2000, rule), /IDEMPOTENCY_CONFLICT/);
assert.throws(() => model.create('O3', 'M1', 1.2, rule), /INVALID_AMOUNT/);
assert.throws(() => model.create('O3', 'M1', 2000, {
revision: 0, rateBps: 500}), /INVALID_RULE/);
assert.throws(() => model.create('O3', 'M1', 2000, {
revision: 2, rateBps: 10001}), /INVALID_RULE/);
assert.throws(() => model.create('', 'M1', 2000, rule), /INVALID_ID/);
assert.equal(model.create('O4', 'M1', 101, {
revision: 1, rateBps: 500}).commissionCents, 5);
assert.equal(model.orders.size, 3);
console.log(JSON.stringify({
assertions: 14, oldOrder, newOrder, rounding: 'floor_to_cent', retryUsesOriginalRule: true}));
状态步骤与复现输出
依赖Node.js内置assert。保存为transitions.cjs并执行node transitions.cjs。本次Windows本地环境为v24.18.0,退出码0,14项断言通过。原代码、环境、哈希和输出已保存。
先用revision=1、500基点创建O1,金额2000分,得到抽成100分。将规则改成revision=2、800基点后,O1仍保存原费率和版本。以相同商家和金额重试O1返回原记录,新建O2得到160分。
{"assertions":14,"oldOrder":{"orderId":"O1","merchantId":"M1","amountCents":2000,"ruleRevision":1,"rateBps":500,"commissionCents":100},"newOrder":{"orderId":"O2","merchantId":"M1","amountCents":2000,"ruleRevision":2,"rateBps":800,"commissionCents":160},"rounding":"floor_to_cent","retryUsesOriginalRule":true}
异常反例怎样解释
O1改为2100分或更换商家触发IDEMPOTENCY_CONFLICT。小数分金额、修订号0及超范围基点被输入校验拒绝。101分按500基点计算得到5分,验证示例舍入规则。拒绝请求后没有新增相应订单,记录数量仍可核对。
状态从不存在到创建,之后保持原交易快照。调整配置只影响后续新建记录。退款、取消和结算属于接续状态,需关联原交易与原规则另行设计,不能用当前配置重新解释所有历史金额。
数据库和云上实现职责
生产实现需要在同一事务中核订单唯一键、读取适用规则并写入订单快照。并发创建发生唯一键冲突时回读原单,再比较业务参数。规则修改与创建订单的生效顺序、时区和事务隔离应由项目方案明确,内存串行模型没有证明跨进程一致性。
订单存储、消息传递和审计分别确定职责。数据库保留唯一约束与规则快照,消息可能重复时消费按原交易幂等处理,日志保存必要关联信息并按权限访问。云组件按实际环境选择,本文未部署、联调或压测云服务。
版本采购继续核源码许可、构建依赖、迁移脚本、历史订单回归与恢复责任。微订第一方说明的源码安装和个性化开发可作为交付评审方向,具体文件和内部规则须以项目资料确认。本地示例仅支撑新旧单兼容用例。
作者:微订(上海逊柯计算机科技有限公司)。