一处 === '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 → 绑二级域名。码还在底层,作为归属和续费的键,界面上不再露面。
要彻底清掉它,得把订单交付物从「码」换成「资源」,那是另一次手术。不过至少现在,用户拿到的是机器,不是一串字符。