调度、VNC、上游开通、续费与插件链路审查修复(2026-08-13)
- 文档性质:针对定时任务、Hooks、VNC 中继、上游开通、续费与插件体系的专项审查结论、修复记录与遗留待办。
- 审查方式:5 个并行审查子代理深挖 + 主代理直接阅读调度/Hooks/插件任务核心链路;关键高危项逐一亲验(判定 CONFIRMED/PLAUSIBLE)。
- 审查结论:共确认 25+ 项发现(高危 9 项)。本次已完成 P0/P1/P2 共 10 项修复并附回归测试;遗留中低危待办 10 项,不在本批次排期。
背景
项目 2026-08-12 全量专家团审查后,本批次聚焦六个功能域:定时任务(Scheduler/Heartbeat)、调度 Hook、VNC 中继、对接上游开通(provisioning)、续费(renewal)与插件体系(plugins)。目标是在既有"后端专家团审查修复计划"之外,补齐这些链路的资金安全、并发幂等与凭据/越权边界。
已修复项(2026-08-13,均含回归测试)
| # | 模块 | 级别 | 问题 | 修复 | 验证 |
|---|---|---|---|---|---|
| 1 | VNC | P0 | 上游连接失败日志把含 VNC 密码的完整 path 明文落库 | VncRelayCommand::maskUpstreamPath() 日志侧只保留路径段、剥离 query | BackendHealthFixRegressionTest 断言 path 脱敏;VNC 相关测试全过 |
| 2 | 开通 | P0 | services:sync-upstream 对纯门面反射不存在的方法,命令必然全量失败 | 改为注入 ServiceDetailService 直接调用公开方法 | 新增回归断言防反射错类复发 |
| 3 | 续费 | P0 | 上游续费崩溃/超时窗口内重试重复提交 /host/renew,二次扣供应商余额 | renew_inflight 幂等标记:调用前落 pending_submit,拿到账单 ID 立即升级 submitted 并落库;重试识别残留 inflight 不重提,未知账单号转人工核实 | 新增 2 个 inflight 定向测试,续费 18 项回归通过 |
| 4 | 续费 | P0 | 自动续费与手动续费同周期重叠双扣 | 建单入口拦截"服务未过期 + 同一续费窗口内已履约" | 新增同周期拦截测试,续费回归全过 |
| 5 | 开通 | P1 | 队列履约与手动重试并发时,旧内存值导致二次开通 | 购物车锁内用 $service->fresh() 复查幂等并传入插件;锁内 assertNoUnresolvedUpstreamProvisionInvoice 复查 | 开通 15 项回归通过 |
| 6 | 调度 | P1 | service-status-sync(全量)与插件任务重复同步 ZJMF | 全量任务动态排除 ZJMF(插件任务注册时),插件禁用回退全量兜底 | ServiceStatusSyncBindingTest 通过 |
| 7 | 调度管理 | P2 | retryFailedRun 重跑前不回收陈旧活跃运行,可被过期记录阻塞 | 重跑前 reclaimStaleRunsForTask(与 dispatchLocked 对齐) | 调度管理 5 项回归通过 |
| 8 | 调度管理 | P2 | manual_retry 子运行派发失败成死胡同 | 工作区已实现 requeueDispatchFailedRun 重派;本批次验证并修复测试 tearDown 残留清理 | 清理后连续两次全过 |
| 9 | 插件 | P1 | 插件 Provider 在 boot 期可无约束注册系统级路由/调度/全局中间件 | PluginProviderRegistry::activate 前后对比路由/调度/中间件数量,违规抛异常;enable() 入口回滚禁用 | 新增 2 个拦截测试,插件 38 项回归通过 |
| 10 | 开通 | P1 | ZjmfBillingRestoreService 物理删除全部财务数据并清零余额 | 空库强制审计:目标表非空时非 dry-run 必须显式 --force,否则拒绝;dry-run 输出覆盖统计 | 新增拒绝测试,恢复命令测试 4 项通过 |
第二轮遗留项处理(2026-08-13)
| # | 模块 | 级别 | 处理 |
|---|---|---|---|
| 1 | 开通 | 中 | JWT 缓存 TTL 钳制:剩余有效期不足时不再缓存失效会话(ZjmfAuthManager),消除"缓存已过期 token 必然先 401 再重登" |
| 2 | 开通 | 中 | syncServiceStatuses 返回值剥离会话 JWT(ZjmfStatusService),避免调用方整体记日志/缓存时泄露上游会话 |
| 3 | 续费/调度 | 中 | nextduedate 兼容 Y-m-d 日期字符串(ServiceRenewService/ServiceStatusSyncService),避免与上游延期口径漂移 |
| 4 | 开通 | 中 | 幂等回查校验上游账单已付(ZjmfProvisionService):host 存在但账单未付时重试余额支付,避免重试绕过支付直接确认造成漏收 |
| 5 | 插件 | 中 | 测试发送接口加节流(v2-admin.php,throttle:10,1),防止对任意收件人短信/邮件轰炸 |
以上均通过相关回归测试(73 passed)。
第三轮处理(2026-08-13)
| # | 模块 | 级别 | 处理 |
|---|---|---|---|
| 1 | 工程债 | 低 | 清理 ServiceRenewService/ProvisionService 的 PHPStan nullsafe/is_array 报错(Order 模型 invoice() 补泛型 PHPDoc、isAdminManualOrder/hostname 规则去掉冗余 is_array、log 访问改类型化写法),并修正 baseline ignore 计数;三个文件 PHPStan 全绿 |
| 2 | VNC | 中 | relay SSRF 纵深防护(VncRelayCommand::assertPublicUpstreamTarget):解析目标 host 并拒绝内网/保留地址(含 IPv6),上游或 Redis 受控时不再可把 relay 导向内网 TCP 桥 |
| 3 | VNC | 中 | relay 防 DoS 加固:并发连接上限 512、握手 15s / 空闲 300s 超时、单侧缓冲上限 1MB,逐轮清理挂起连接;relay token 窗口由 120s 缩短至 60s(ServiceVncService) |
以上均通过相关回归测试(80 passed)。全项目 PHPStan 仍有约 411 个既有错误(工作区大量未提交改动导致 baseline 整体过时),需在改动收敛后统一重新生成 baseline,另立批次。
第四轮处理(2026-08-13)
| # | 模块 | 级别 | 处理 |
|---|---|---|---|
| 1 | 插件 | 中 | manifest 的 entry/provider/定时任务/hook 监听器类必须位于插件目录内(PluginInstaller::assertPluginClassInsideDirectory),manifest 被污染时不再可实例化/调用任意已加载类 |
| 2 | VNC | 中 | VNC 密码不再明文落 volatile Redis:launch token 与 relay token 载荷移除 password,密码仅经 vnc_credentials 返回给当前认证用户供本地 noVNC 认证 |
以上均通过相关回归测试(115 passed)。
第五轮处理(2026-08-13,按用户决策执行)
| # | 模块 | 级别 | 处理 |
|---|---|---|---|
| 1 | VNC | 中 | 兑换端点 Origin 纵深校验(ServiceConsoleController::assertVncTokenOriginAllowed):请求带 Origin 且不在 console/api 白名单时拒绝;无 Origin(同源/非浏览器)放行不误伤。附 Feature 测试 |
| 2 | 开通 | 中 | 上游会话 Cookie 后端读取端支持加密存储:UpstreamBindingWriter 支持 web_session_cookie secret(提交或从 notes 迁入加密)、PluginBindingResolver 解密注入、ZjmfFinanceTransport 优先 secret 回退 notes;写入端完整加密待管理端前端配合 |
| 3 | 续费 | 中 | 尽力对账:ZjmfRenewService 容错提取上游实扣金额(upstream_amount),ServiceRenewService 履约后与本地应收比对,差异 > 0.01 元记 warning 日志(不阻断) |
| 4 | 工程债 | 低 | PHPStan baseline 重新生成(1853 个既有错误记录为新基线),全量分析恢复全绿 |
| 5 | 插件 | 中 | 轻量篡改检测:PluginScanner 计算清单内容 sha256,PluginInstaller::install 记录到 integration_plugins.manifest_hash(新迁移),加载时比对不一致记 warning(不阻断)。附 2 个测试 |
VNC 异步连接按用户决策保持现状(连接上限/超时/缓冲/SSRF 已加固),异步重构待架构评审;孤儿 host 自动回收按用户决策采用只读对账 + 人工核实;VNC token 落 URL 按用户决策保持现状,前端排期。
以上均通过相关回归测试(97 passed)。
剩余待办(需产品/架构/前端/发布决策)
| # | 模块 | 级别 | 描述 | 建议 |
|---|---|---|---|---|
| 1 | VNC | 中 | 中继单线程阻塞式 connect/握手仍可全局冻结(已加固超时/上限/缓冲/SSRF;兑换已做 Origin 校验) | 异步连接改造(架构级,用户决策保持现状,待架构评审) |
| 2 | 开通 | 中 | 孤儿 host 自动回收(只读对账命令已提供数据) | 需产品确认清理策略 |
| 3 | VNC | 中 | token 全程走 URL query(兑换端点 Origin 校验已做) | 需前端 noVNC 流程改造 |
| 4 | 插件 | 中 | 插件目录完整性/签名校验(类目录约束已做) | 需发布/分发链路立项 |
进度
- [x] 完成六个功能域并行审查并输出发现清单。
- [x] 完成 10 项修复(P0×4、P1×4、P2×2),每项附定向回归测试。
- [x] 修复测试基建缺陷:
ScheduleTaskRunAdminApiTesttearDown 未清理 manual_retry 子记录导致的共享测试库残留。 - [x] 适配既有"事务外预判 + 锁内重查"两阶段续费建单:
ServiceRenewCouponRegressionTest的previewOwnedCouponmock 由once()调整为atLeastOnce(),与两阶段检查一致。 - [ ] 遗留中低危项待产品/运维确认后另行排期。
决策日志
| 日期 | 决策 | 原因 |
|---|---|---|
| 2026-08-13 | 续费崩溃窗口采用 renew_inflight 幂等标记而非上游幂等键 | ZJMF /host/renew 无幂等键支持;标记能识别"已提交但结果未落库"的窗口,宁可转人工核实也不重复扣款 |
| 2026-08-13 | 同周期双扣拦截以"服务未过期 + 履约在一个周期自然月内"为条件 | 避免误伤"提前续多个周期"的合法场景;服务已过期的续费属于恢复动作不拦截 |
| 2026-08-13 | ZJMF 重复同步采用"全量任务动态排除"而非"插件去重" | Core 全量任务是兜底,不应收缩(插件禁用时回退全量);插件任务保留定向同步职责 |
| 2026-08-13 | 插件 Provider 系统级注册用"注册前后数量对比"检测 | 无法可靠回滚已注册路由,但能在启用入口拦截并回滚插件状态;运行时 boot 作为告警防线 |
| 2026-08-13 | BillingRestore 采用"目标表非空 + 必须 --force"而非改造合并逻辑 | 一次性恢复工具,改造合并风险高于收益;--force 显式覆盖 + dry-run 预检输出覆盖量是低成本安全闸门 |
| 2026-08-13 | Relay SSRF 防护新增 allow_private_upstream,默认本地跳过、生产严格 | 本地联调常处代理 fake-ip 环境(Clash TUN),任意域名解析为 198.18/15 与 fc00::/7 ULA 假地址,DNS 内网段校验误杀全部合法 VNC 上游;本地 Relay 仅监听 127.0.0.1,可跳过,生产保持严格拦截,可用 VNC_RELAY_ALLOW_PRIVATE_UPSTREAM 显式覆盖 |
