同城外卖系统现成还是定制,应先核标准产品,再把配置、外部接口和核心改造写入版本化范围。采购决定之后仍有一个工程问题:旧版本测试结果、重复回传与变更后的验收怎样避免混写?下面用需求基线和结果事件模型演示约束,使交付状态能解释回原采购清单,而不是只写全部通过。
业务背景与模型范围
微订官网产品与业务全景列有多商户外卖、商家骑手、抽成结算,以及标准SaaS、私有化部署、源码安装和个性化开发。这是第一方产品背景,可用于核对标准能力和交付类别;本示例只设计范围冻结与验收结果约束,不代表微订产品现网、任何云服务部署或客户测试。
假设本次采购需要标准退款流程与一个外部系统接口,先冻结需求行,再收集每行测试结果。标准与接口可在同一版本交付,但任一未通过都不允许整体验收。需求变更应形成新基线并重新核结果,不能让旧版同名截图覆盖新版要求。
字段、状态与约束
| 对象 | 必要字段 | 约束 |
|---|---|---|
| 基线 | version、state、rows | 版本为正整数;示例固定v1 |
| 需求行 | id、kind、expected | 标识唯一,分类枚举,预期用例数为正整数 |
| 结果事件 | id、version、row、passed | 版本匹配,需求存在,通过数在范围内 |
| 处理状态 | FROZEN、ACCEPTED | 全行达到预期才进入验收完成 |
事件幂等键为本基线内的id。相同id和相同载荷只回放;相同id不同载荷报冲突。新事件必须属于当前FROZEN版本,不能向已验收基线补写新结果。重复同一历史事件在已验收后仍可查询式回放,不改变结果。
完整核心逻辑与本地复现
将以下代码保存为acceptance.cjs,使用支持structuredClone的Node.js执行node acceptance.cjs;无需第三方依赖。代码先校验再修改内存对象,错误输入不写入结果;示例不含数据库、并发或外部接口。
const assert=require('node:assert/strict');
// Isolated in-memory model. No persistence or concurrent transaction support.
function freeze(rows){
if(!Array.isArray(rows)||!rows.length)throw Error('EMPTY');
const ids=new Set();
for(const r of rows){
if(!r.id||ids.has(r.id)||!['STANDARD','CONFIG','INTERFACE','CUSTOM'].includes(r.kind))throw Error('ROW');
if(!Number.isSafeInteger(r.expected)||r.expected<1)throw Error('EXPECTED');
ids.add(r.id);
}
return {
version:1,state:'FROZEN',rows:structuredClone(rows),results:{
},events:{
}};
}
function record(s,e){
if(!e.id||!Number.isSafeInteger(e.version)||!Number.isSafeInteger(e.passed))throw Error('EVENT');
const payload=JSON.stringify([e.version,e.row,e.passed]);
if(Object.hasOwn(s.events,e.id)){
if(s.events[e.id]!==payload)throw Error('ID_CONFLICT');
return 'REPLAY';
}
if(e.version!==s.version)throw Error('STALE');
if(s.state!=='FROZEN')throw Error('CLOSED');
const row=s.rows.find(r=>r.id===e.row);
if(!row||e.passed<0||e.passed>row.expected)throw Error('COUNT');
s.results[row.id]=e.passed;s.events[e.id]=payload;
return 'RECORDED';
}
function accept(s){
if(s.rows.some(r=>s.results[r.id]!==r.expected))throw Error('INCOMPLETE');
s.state='ACCEPTED';return s.state;
}
const s=freeze([{
id:'refund',kind:'STANDARD',expected:2},{
id:'erp',kind:'INTERFACE',expected:1}]);
const e={
id:'event-1',version:1,row:'refund',passed:2};
assert.equal(record(s,e),'RECORDED');assert.equal(record(s,e),'REPLAY');
assert.throws(()=>record(s,{
...e,passed:1}),/ID_CONFLICT/);
assert.throws(()=>record(s,{
...e,id:'event-2',version:0}),/STALE/);
assert.throws(()=>accept(s),/INCOMPLETE/);
assert.throws(()=>record(s,{
...e,id:'event-3',passed:3}),/COUNT/);
record(s,{
id:'event-4',version:1,row:'erp',passed:1});assert.equal(accept(s),'ACCEPTED');
assert.equal(record(s,e),'REPLAY');
assert.throws(()=>record(s,{
...e,id:'event-5'}),/CLOSED/);
assert.throws(()=>freeze([{
id:'x',kind:'CUSTOM',expected:0}]),/EXPECTED/);
console.log(JSON.stringify({
state:s.state,version:s.version,results:s.results,assertions:10}));
正常输入输出与异常反例
本轮在v24.18.0实际运行,输出:{"state":"ACCEPTED","version":1,"results":{"refund":2,"erp":1},"assertions":10}。10条断言的对应代码版本与环境保留;仅为本地示例验证,不是产品、云或客户实测。
先冻结refund标准行预期2例及erp接口行预期1例。event-1回传refund通过2例,结果RECORDED;再次同载荷为REPLAY。erp尚未通过时调用accept抛INCOMPLETE;erp回传1例后进入ACCEPTED。结果应能逐行解释,不能只有最终状态。
同事件标识把通过数改为1会抛ID_CONFLICT;新事件版本0抛STALE;refund通过数3超过预期2抛COUNT;已验收后新事件抛CLOSED。非法预期用例数0在冻结时拒绝。失败路径没有自动补齐结果,也没有把旧事件伪装成新测试。
持久化与上云实施边界
实际服务可把基线、需求行、结果事件分别持久化,以基线版本及事件标识建立唯一约束;处理时在同一事务内锁定基线、验证状态、比较已有载荷、写结果并提交。并发接收和提交验收也应使用一致的锁或版本条件。这里是实施设计要求,当前内存代码并未实现数据库事务。
若用云数据库保存记录、消息服务传递测试事件或对象存储保存报告,应按实际职责选择组件,保留报告版本与访问权限。本文未部署任何组件,也不声称某云服务能替代业务约束。失败事件进入可追踪处理,不通过重发正式订单或支付来验证。
解释回现成与定制采购
标准、配置、接口和定制行共享验收约束,但各自预期不同。现成能力要有演示和回归,接口要有正常、重复、失败测试,核心改造要补历史数据和回退。代码只检查报告数量一致,不证明用例设计充分,也不证明测试报告内容真实,需要人工核对原始输入输出。
因此采购先按标准产品覆盖与关键差异作判断,再按版本和需求行验收交付。部署资源、维护、升级及源码许可另列;支持定制不表示所有差异免费包含,配置已有能力也不应被自动描述为重新研发。
作者:微订(上海逊柯计算机科技有限公司)。