一处 === ‘vm’,让新套餐开了单却只发码不建机

一处 === 'vm',让新套餐开了单却只发码不建机

上线一个新套餐类型,最省事的做法是往 type 字段里加一个值。我们加的是 bot——养鱼套餐,卖的是「开箱即用的 AstrBot + NapCat 容器」。加完之后下单,日志漂亮得很:


[03:29:09] 开始开通:养鱼套餐 1C2G|20G | AstrBot+QQ ¥18
[03:29:09] 绑定设备代码 / 写入归属 / 设置到期…
[03:29:09] 生成设备代码:狸花网络-NVBE3V
[03:29:09] ✅ 开通完成

四个勾全绿,订单状态 active。用户手里只有一个设备码,机器一台没有。

根因:分支里的 else 是给谁准备的

开通流程大致长这样:


let code = null
if (plan.type === 'vless' && D.takeStock) {
  code = await D.takeStock({ deviceName, note, order, step })
}
if (!code) code = await addCode({ deviceName, note, issuedBy })   // 所有类型都发码

let vmid = ''
if (plan.type === 'vm') {                                        // 只有 vm 建容器
  const mode = plan.vmMode || (String(plan.poolVmids || '').trim() ? 'pool' : 'create')
  ...
}

写这段代码的时候,世界上只有两种套餐:vm 和 vless。所以「不是 vless 就是 vm」这个隐含假设成立,=== 'vm' 判断也就够用了。

第三种类型一进来,它既不是 vless,也不是 vm。于是:

  • 拿不到库存链接(第一个 if 不成立)
  • 走了兜底的 addCode,凭空生成一个新码
  • 建容器那一段被跳过

它同时掉进了两个分支的空隙:既没走链接交付,也没走容器交付,最后按链接的形态交付了一个不存在的资源。

比 bug 更麻烦的:底层是「码驱动」

再往里看一层,问题不在那个 if,在订单模型。

订单的交付物一直是 code。归属、到期、续费、退订、后台列表、体检——全都挂在码上:

环节 依赖
归属写入 bindPayload(code.code, {...})
续费 遍历 items[].code 改 expiresAt
退款 bindPayload(it.code, { enabled: false })
后台体检 if (isContainerPlan(...) && !it.vmid)
「我的服务器」 按 items[].code 反查

容器只是码的一个附属属性。所以哪怕容器建出来了,用户看到的还是「设备代码 狸花网络-XXXX」,而不是「一台 2 核 2G 的机器」。

这就是为什么加一个容器型套餐会觉得处处不对劲——不是新套餐有问题,是这层模型本来就是为卖链接设计的。

修法:别判断「是什么」,判断「不是什」

如果继续用 === 'vm',每加一种容器型套餐就要改一处、漏一处。改成反向判断:


// 容器型套餐:除 vless 外都算(vm / bot / 以后的应用套餐)
const isContainerPlan = (t) => String(t || '') !== 'vless'

然后把全文件 6 处 planType === 'vm' / !== 'vm' 全部替换。这样一来,将来再加 wordpress、gitlab、mc 这类套餐,不改这行就不会漏建容器。

配套还改了两处文案,让容器订单的日志不再出现「设备代码」:

  • 阶段标题 → 写入归属 / 设置到期…
  • 结尾 → 容器交付完成:#401

后续两个连坐

一、字段被 PATCH 抹掉

套餐加了 botBootstrap: true 标记用来触发部署。管理员在后台点一次「保存」,PATCH 请求只带固定字段,botBootstrap 就被覆盖成 null,部署钩子从此静默失效。

修法两件:payload 里显式带上 payload.botBootstrap = (type === 'bot') || (app === 'bot');后端判定改成三重——botBootstrap || app === 'bot' || type === 'bot'。任何一条成立就部署。

二、下拉框里没有这一项

预装应用下拉只有「不预装 / WordPress / ddns-go」,养鱼套餐保存后会被重置成「不预装」。补一项 bot,并且选中逻辑要排除互斥:!p.app && !p.botBootstrap 才让「不预装」拿默认态。

三条可以带走的

1. 新增枚举值,先 grep 所有 === '旧值'。每一处都是在假设「集合里只有这几个」。

2. 兜底分支比报错更危险。if (!code) code = await addCode() 没有任何错,它只是安静地把一台不存在的机器交付出去了。宁可抛 409。

3. 日志全绿不等于交付正确。四个 step 都打勾的订单,可以完全没有资源。

养鱼套餐现在走的是容器交付:选节点 → 分配 vmid → 建容器 → 初始化 → 部署 AstrBot + NapCat → 绑二级域名。码还在底层,作为归属和续费的键,界面上不再露面。

要彻底清掉它,得把订单交付物从「码」换成「资源」,那是另一次手术。不过至少现在,用户拿到的是机器,不是一串字符。

💬 留下你的爪印