仰望星辰工作室

feat(zjmfmanager_reserve): 新增魔方财务代理分销插件

Y yang1145 提交于 2026-08-12 08:02 · 0116757 ·父提交 6d78841
feat(zjmfmanager_reserve): 新增魔方财务代理分销插件

该插件实现以代理商身份分销魔方财务产品,包含商品同步加价、本地余额购买、代理商直通开通、主机管理与升降级功能,依赖user_info与balance插件。
22 个文件变更 +5657 -0 13172193298@163.com
•app_plugins/zjmfmanager_reserve/README.md +99 -0
•app_plugins/zjmfmanager_reserve/assets/style.css +67 -0
•app_plugins/zjmfmanager_reserve/bootstrap.php +810 -0
•app_plugins/zjmfmanager_reserve/install.sql +104 -0
•app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php +558 -0
•app_plugins/zjmfmanager_reserve/lib/upstream.php +658 -0
•app_plugins/zjmfmanager_reserve/lib/zjmf.php +925 -0
•app_plugins/zjmfmanager_reserve/plugin.json +10 -0
•app_plugins/zjmfmanager_reserve/uninstall.sql +6 -0
•app_plugins/zjmfmanager_reserve/views/admin/hosts.php +127 -0
•app_plugins/zjmfmanager_reserve/views/admin/logs.php +94 -0
•app_plugins/zjmfmanager_reserve/views/admin/orders.php +154 -0
•app_plugins/zjmfmanager_reserve/views/admin/products.php +445 -0
•app_plugins/zjmfmanager_reserve/views/admin/suppliers.php +285 -0
•app_plugins/zjmfmanager_reserve/views/host.php +161 -0
•app_plugins/zjmfmanager_reserve/views/hosts.php +68 -0
•app_plugins/zjmfmanager_reserve/views/layout.php +46 -0
•app_plugins/zjmfmanager_reserve/views/order.php +140 -0
•app_plugins/zjmfmanager_reserve/views/orders.php +93 -0
•app_plugins/zjmfmanager_reserve/views/shop.php +78 -0
•app_plugins/zjmfmanager_reserve/views/upgrade.php +249 -0
•docs/prd/zjmfmanager-reserve.md +480 -0
变更内容
diff --git a/app_plugins/zjmfmanager_reserve/README.md b/app_plugins/zjmfmanager_reserve/README.md
new file mode 100644
index 0000000..140073d
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/README.md
@@ -0,0 +1,99 @@
+# 魔方财务代理分销(zjmfmanager_reserve)
+
+以代理商身份分销[魔方财务](https://www.zjmf.com/)(cube_finance)产品的 MNBT 业务插件。
+
+- 供应商:可维护多个魔方财务上游站点,各自独立的 API 账号、加价规则与启用状态
+- 商品:同步弹窗勾选(按供应商)+ 手动添加 + 供应商/单品加价 + 上架管理
+- 下单:本地余额(或已启用支付方式)购买,按订单所属供应商直通开通(主机归属代理商账号)
+- 主机:状态/流量查看,开关机/重启/重置密码/重装
+- 升级:配置升级 + 产品升降级(余额支付差额,按主机所属供应商路由)
+- 管理端:供应商管理、商品管理、订单管理、主机管理、操作日志
+
+依赖插件:`user_info`(认证)、`balance`(余额支付/退款)。
+
+## 安装
+
+1. 将 `zjmfmanager_reserve` 目录放入 `app_plugins/`。
+2. 后台 → 系统管理 → 插件管理 → 安装 → 启用(自动执行 `install.sql` 建表)。
+3. 供应商管理页新增供应商(名称、站点 URL、API 用户名、API 密钥、加价规则),保存后点击「连通测试」验证。
+4. 商品管理页「同步商品」弹窗选择供应商并勾选商品同步,或「手动添加」商品;按需配置单品加价并上架。
+5. 停用供应商后其商品自动不可售,主机操作与升级将被拒绝。
+
+## 目录结构
+
+```text
+app_plugins/zjmfmanager_reserve/
+├── plugin.json           # 插件元信息(声明依赖 user_info/balance)
+├── bootstrap.php         # 主入口:注册钩子/路由/菜单/AJAX
+├── install.sql           # 建表(supplier/product/order/host/log)
+├── uninstall.sql         # 卸载清理
+├── lib/
+│   ├── CubeFinanceClient.php  # 上游 API 客户端(JWT 登录/请求封装)
+│   ├── upstream.php           # ZjmfUpstream 服务层(按供应商路由:同步/开通/主机/升级)
+│   └── zjmf.php               # 辅助函数 + 数据表操作 + 开通编排
+├── views/
+│   ├── layout.php             # 用户端公共布局
+│   ├── shop.php               # 商品列表(按供应商分组)
+│   ├── order.php              # 下单页
+│   ├── orders.php             # 我的订单
+│   ├── hosts.php              # 我的主机
+│   ├── host.php               # 主机详情(状态/流量/操作)
+│   ├── upgrade.php            # 升级页(配置/产品)
+│   └── admin/                 # 管理端页面
+│       ├── suppliers.php      # 供应商管理(新增/编辑/连通测试/启停/删除)
+│       ├── products.php       # 商品管理(同步弹窗/手动添加/加价/上下架)
+│       ├── orders.php         # 订单管理(供应商/状态筛选)
+│       ├── hosts.php          # 主机管理(刷新状态)
+│       └── logs.php           # 操作日志
+└── assets/style.css           # 用户端样式
+```
+
+## 多供应商模型
+
+- 每个供应商一行 `MN_plugin_zjmf_supplier`,保存独立的站点地址、API 账号、加价规则。
+- 商品/订单/主机/日志均记录 `supplier_id`;下单、主机操作、升级按该字段路由到对应上游。
+- JWT 缓存按供应商隔离(`runtime/cache/s{id}`),多供应商凭证互不串扰。
+- 商品加价优先级:单品已配置加价 > 所属供应商加价。
+
+## 关键流程
+
+### 购买开通
+
+```
+商品列表(按供应商分组)→ 下单页(选周期/支付方式)
+        → 创建本地订单(pending, 含 supplier_id) + MN_dd(lx=zjmf)
+        → 支付网关确认 → mnbt_pay_settle_order() 触发 order.paid 钩子
+        → 本插件按 ddh 前缀 ZJM 过滤 → 标记 paid → zjmf_open_host()
+        → 校验供应商启用 → 按订单 supplier_id 调上游下单+余额抵扣 → 建主机映射(密码 authcode 加密)
+        → 供应商缺失/停用或开通失败 → 订单置 failed + balance_add(refund) 自动退款
+```
+
+### 升级
+
+```
+主机详情 → 升级页 → 选择目标 → 试算差额(GET 页面端点,不产生提交)
+        → 确认 → 创建升级订单(含 supplier_id)→ balance_deduct 扣差额
+        → 按主机供应商提交上游(POST)→ 成功回填订单/更新主机缓存;失败自动退款
+```
+
+### 金额约定
+
+所有金额以「分」整数存储;上游元金额通过 `toCents()` 转换,避免浮点误差。
+加价比例以千分比存储(如 10 表示 +1%),固定加价直接存分。
+
+## 联调注意点
+
+以下端点/字段因上游版本与定制模块而异,已集中在 `lib/upstream.php` 便于调整
+(详见 PRD §3.3 与开放问题 Q1/Q3):
+
+- 开通端点:`cart/checkout`(`ZJMF_CHECKOUT_PATH`),如上游为 provision 直通则修改此处
+- 主机操作 func:`on/off/reboot/passwd/reinstall`(`zjmf_action_func()` 映射)
+- 升级端点:`upgrade/upgrade_config_page|_post`、`upgrade/upgrade_product_page|_post`
+- 响应字段解析均为防御式(`pickPrice`/`findId`/`findHostId`/`parseTrialPrice`)
+
+## 安全说明
+
+- 上游 API 密钥存入 `MN_plugin_zjmf_supplier.api_password`,编辑页不回显,日志脱敏
+- 主机密码使用 `authcode()` 加密入库,页面默认打码
+- 操作日志内容脱敏,不落明文密码/密钥
+- 用户端全部操作经路由鉴权(`zjmf_require_user()`),管理员 AJAX 使用 `p_zjmf_*` 前缀并鉴权
diff --git a/app_plugins/zjmfmanager_reserve/assets/style.css b/app_plugins/zjmfmanager_reserve/assets/style.css
new file mode 100644
index 0000000..08396ad
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/assets/style.css
@@ -0,0 +1,67 @@
+/* zjmfmanager_reserve Layui 风格覆盖 */
+.zj-nav{background:#1e9fff;}
+.zj-nav-inner{max-width:1060px;margin:0 auto;width:100%;display:flex;
+  align-items:center;justify-content:space-between;padding:0 16px;height:54px;}
+.zj-nav-brand{color:#fff;font-size:17px;font-weight:700;text-decoration:none;letter-spacing:.5px;}
+.zj-nav-brand:hover{color:#fff;text-decoration:none;}
+.zj-nav-links a{color:rgba(255,255,255,.85);font-size:14px;margin-left:22px;text-decoration:none;transition:color .15s;}
+.zj-nav-links a:hover{color:#fff;text-decoration:none;}
+.zj-page{max-width:1060px;margin:30px auto;padding:0 16px;}
+.zj-msg{display:none;padding:10px 16px;border-radius:4px;margin-bottom:16px;font-size:14px;}
+.zj-msg-show{display:block;}
+.zj-msg-success{background:#e8f5e9;color:#2e7d32;border:1px solid #c8e6c9;}
+.zj-msg-error{background:#ffebee;color:#c62828;border:1px solid #ffcdd2;}
+.zj-section{margin-bottom:20px;text-align:center;}
+.zj-section h1{font-size:24px;color:#222;margin:0 0 6px;}
+.zj-section p{color:#888;font-size:14px;}
+
+/* 商品卡片 */
+.zj-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;margin-bottom:20px;}
+.zj-card{background:#fff;border:1px solid #e8e8e8;border-radius:8px;overflow:hidden;transition:box-shadow .15s;}
+.zj-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.08);}
+.zj-card-head{background:#f9fafb;padding:20px 20px 14px;border-bottom:1px solid #eee;}
+.zj-card-head h2{font-size:18px;color:#222;margin:0;}
+.zj-tag{display:inline-block;padding:2px 10px;font-size:11px;background:#e6f4ff;
+  color:#1e9fff;border-radius:10px;margin-top:6px;}
+.zj-card-desc{padding:14px 20px;font-size:13px;color:#666;line-height:1.7;}
+.zj-desc{padding:10px 2px 18px;font-size:13px;color:#666;line-height:1.7;}
+.zj-price{display:flex;gap:14px;padding:12px 20px;background:#fafbfc;border-top:1px solid #f0f0f0;flex-wrap:wrap;}
+.zj-price-item{display:flex;align-items:center;gap:4px;font-size:12px;}
+.zj-price-label{background:#e8e8e8;color:#666;padding:1px 6px;border-radius:3px;}
+.zj-price-value{font-size:18px;font-weight:700;color:#1e9fff;}
+.zj-buy{display:block;margin:0 20px 20px;text-align:center;}
+.zj-buy .layui-btn{width:100%;}
+
+/* 下单表单 */
+.zj-order-form .layui-form-label{width:90px;padding:9px 10px;text-align:right;box-sizing:border-box;}
+.zj-order-form .layui-input-block{margin-left:110px;min-height:36px;line-height:36px;}
+.zj-choices{display:flex;flex-wrap:wrap;gap:10px;align-items:center;}
+.zj-choice{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;
+  background:#f7f9fa;border:1px solid #e4e7ed;border-radius:6px;cursor:pointer;
+  font-size:14px;color:#333;transition:border-color .15s,background .15s;}
+.zj-choice:hover{border-color:#1e9fff;background:#f0f8ff;}
+.zj-choice input[type="radio"]{margin:0;cursor:pointer;}
+.zj-choice.active{border-color:#1e9fff;background:#e6f4ff;color:#1e9fff;}
+
+/* 表格 */
+.zj-table{width:100%;border-collapse:collapse;font-size:13px;}
+.zj-table th{background:#fafafa;padding:10px 12px;text-align:left;color:#666;
+  font-weight:600;border-bottom:2px solid #e6e6e6;}
+.zj-table td{padding:10px 12px;border-bottom:1px solid #f0f0f0;color:#333;vertical-align:middle;}
+.zj-mono{font-family:Consolas,Monaco,monospace;font-size:12px;}
+.zj-status{display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:500;}
+.zj-status-active,.zj-status-paid,.zj-status-opened{background:#e8f5e9;color:#2e7d32;}
+.zj-status-pending{background:#fff3e0;color:#e65100;}
+.zj-status-expired,.zj-status-failed,.zj-status-cancelled,.zj-status-suspend{background:#ffebee;color:#c62828;}
+.zj-pager{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:16px;padding:12px;font-size:13px;}
+.zj-pager a{color:#1e9fff;text-decoration:none;padding:4px 12px;border:1px solid #ddd;border-radius:3px;}
+.zj-pager a:hover{border-color:#1e9fff;background:#f0f8ff;}
+.zj-pager-info{color:#888;}
+.zj-muted{color:#999;font-size:12px;}
+@media(max-width:600px){
+  .zj-grid{grid-template-columns:1fr;}
+  .zj-order-form .layui-form-label{width:auto;display:block;text-align:left;padding-left:0;}
+  .zj-order-form .layui-input-block{margin-left:0;}
+  .zj-choices{gap:8px;}
+  .zj-table{display:block;overflow-x:auto;white-space:nowrap;}
+}
diff --git a/app_plugins/zjmfmanager_reserve/bootstrap.php b/app_plugins/zjmfmanager_reserve/bootstrap.php
new file mode 100644
index 0000000..df2b03a
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/bootstrap.php
@@ -0,0 +1,810 @@
+<?php
+/**
+ * zjmfmanager_reserve 插件 - 主入口
+ *
+ * 功能:以代理商身份分销魔方财务(cube_finance)产品。
+ * 依赖:user_info 插件(认证)、balance 插件(余额支付/退款)。
+ * 架构:
+ *   - 上游服务:lib/upstream.php(ZjmfUpstream,封装全部魔方财务 API 调用)
+ *   - 辅助函数:lib/zjmf.php(URL/渲染/金额/加价计算 + 数据表操作 + 开通编排)
+ *   - 用户端:通过 P2 路由注册 /reserve/* 路径
+ *   - 管理员端:通过 mnbt_register_page('admin', ...) 注册到 plugin.php
+ *   - 开通:通过 order.paid 钩子处理 lx=zjmf 订单,调用上游开通,失败自动退款
+ */
+
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+
+require_once __DIR__ . '/lib/zjmf.php';
+require_once __DIR__ . '/lib/upstream.php';
+
+mnbt_plugin_register('zjmfmanager_reserve', [
+	'name'        => '魔方财务代理分销',
+	'description' => '商品同步加价、本地余额购买、代理商直通开通、主机管理与升降级',
+]);
+
+/* ============================================================
+ *  order.paid 钩子:处理购买 / 升级订单
+ * ============================================================
+ *  支付成功后由 mnbt_pay_settle_order() 触发。
+ *  1. 按 lx=zjmf 过滤(仅处理本插件创建的 MN_dd 订单)
+ *  2. 按订单号找到本地业务订单
+ *  3. 防重复:已支付/已开通/已失败直接跳过
+ *  4. 标记 paid 后按 action 分流处理(buy→开通;升级→后续里程碑)
+ */
+mnbt_add_action('order.paid', function ($order_row, $ctx = []) {
+	if (!is_array($order_row)) {
+		return;
+	}
+	if (($order_row['lx'] ?? '') !== ZJMF_LX) {
+		return;
+	}
+	$order_no = (string)($order_row['ddh'] ?? '');
+	if ($order_no === '') {
+		return;
+	}
+
+	// 业务订单号前缀校验,防止串单
+	if (strpos($order_no, ZJMF_ORDER_PREFIX) !== 0) {
+		return;
+	}
+
+	$order = zjmf_order_get_by_no($order_no);
+	if (!$order) {
+		return;
+	}
+	// 已支付 / 已开通 / 已失败,跳过(防重复处理)
+	if (in_array($order['status'], ['paid', 'opened', 'failed'], true)) {
+		return;
+	}
+
+	zjmf_order_set_status((int)$order['id'], 'paid', '支付完成');
+
+	// 仅 buy 动作本期直接开通;升级订单由升级接口联动处理
+	if ($order['action'] === 'buy') {
+		$result = zjmf_open_host((int)$order['id']);
+		if (!$result['ok']) {
+			@error_log('[zjmfmanager_reserve] open failed order=' . $order_no
+				. ' : ' . ($result['msg'] ?? ''));
+		}
+	}
+}, 20);
+
+/* ============================================================
+ *  管理员端页面注册
+ * ============================================================ */
+
+mnbt_register_page('admin', 'suppliers', 'views/admin/suppliers.php', '供应商管理');
+mnbt_register_page('admin', 'products', 'views/admin/products.php', '商品管理');
+mnbt_register_page('admin', 'orders', 'views/admin/orders.php', '订单管理');
+mnbt_register_page('admin', 'hosts', 'views/admin/hosts.php', '主机管理');
+mnbt_register_page('admin', 'logs', 'views/admin/logs.php', '操作日志');
+
+// 侧边栏菜单(三级结构)
+mnbt_register_menu('admin', [
+	'title'    => '魔方财务分销',
+	'icon'     => 'mdi-currency-cny',
+	'order'    => 60,
+	'children' => [
+		['title' => '供应商管理', 'page' => 'suppliers', 'icon' => 'mdi-settings', 'multitabs' => true],
+		['title' => '商品管理', 'page' => 'products', 'icon' => 'mdi-package-variant', 'multitabs' => true],
+		['title' => '订单管理', 'page' => 'orders', 'icon' => 'mdi-receipt', 'multitabs' => true],
+		['title' => '主机管理', 'page' => 'hosts', 'icon' => 'mdi-server', 'multitabs' => true],
+		['title' => '操作日志', 'page' => 'logs', 'icon' => 'mdi-clipboard-text', 'multitabs' => true],
+	],
+]);
+
+/* ============================================================
+ *  管理员端 AJAX
+ * ============================================================ */
+
+// 保存供应商(新增/编辑,密码留空不修改)
+mnbt_register_ajax('admin', 'p_zjmf_admin_save_supplier', function () {
+	mnbt_plugin_require_admin();
+	global $DB, $date;
+
+	$id = (int)($_POST['id'] ?? 0);
+	$name = trim((string)($_POST['name'] ?? ''));
+	$url = trim((string)($_POST['api_url'] ?? ''));
+	$username = trim((string)($_POST['api_username'] ?? ''));
+	$password = (string)($_POST['api_password'] ?? '');
+	$timeout = max(5, min(120, (int)($_POST['api_timeout'] ?? 30)));
+	$markupType = in_array((string)($_POST['markup_type'] ?? ''), ['0', '1'], true)
+		? (int)$_POST['markup_type'] : 0;
+	$markupValue = max(0, (int)($_POST['markup_value'] ?? 0));
+	$status = in_array((string)($_POST['status'] ?? ''), ['0', '1'], true)
+		? (int)$_POST['status'] : 1;
+	$sort = max(0, (int)($_POST['sort'] ?? 0));
+	$remark = trim((string)($_POST['remark'] ?? ''));
+
+	if ($name === '') {
+		json_exit_error('请填写供应商名称');
+	}
+	if (mb_strlen($name) > 50) {
+		json_exit_error('供应商名称过长');
+	}
+	if ($url !== '' && !preg_match('#^https?://#i', $url)) {
+		json_exit_error('站点 URL 必须以 http:// 或 https:// 开头');
+	}
+	if (mb_strlen($url) > 255) {
+		json_exit_error('站点 URL 过长');
+	}
+	if ($url !== '' && $username === '') {
+		json_exit_error('请填写 API 用户名');
+	}
+	if ($password !== '' && mb_strlen($password) > 255) {
+		json_exit_error('API 密钥过长');
+	}
+
+	$now = $date ?: date('Y-m-d H:i:s');
+	if ($id > 0) {
+		$existing = zjmf_supplier_get($id);
+		if (!$existing) {
+			json_exit_error('供应商不存在');
+		}
+		$sql = "UPDATE MN_plugin_zjmf_supplier
+		        SET name=?, api_url=?, api_username=?, api_timeout=?,
+		            markup_type=?, markup_value=?, status=?, sort=?,
+		            remark=?, updated_at=?";
+		$args = [$name, $url, $username, $timeout, $markupType, $markupValue,
+		         $status, $sort, $remark, $now];
+		if ($password !== '') {
+			$sql .= ", api_password=?";
+			$args[] = $password;
+		}
+		$sql .= " WHERE id=?";
+		$args[] = $id;
+		$ok = $DB->query_prepare($sql, $args);
+		if (!$ok) {
+			json_exit_error('保存失败');
+		}
+	} else {
+		$ok = $DB->query_prepare(
+			"INSERT INTO MN_plugin_zjmf_supplier
+			 (name, api_url, api_username, api_password, api_timeout,
+			  markup_type, markup_value, status, sort, remark, created_at, updated_at)
+			 VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
+			[$name, $url, $username, $password, $timeout, $markupType,
+			 $markupValue, $status, $sort, $remark, $now, $now]
+		);
+		if (!$ok) {
+			json_exit_error('保存失败');
+		}
+	}
+	json_exit_success('已保存');
+});
+
+// 启用/停用供应商
+mnbt_register_ajax('admin', 'p_zjmf_admin_toggle_supplier', function () {
+	mnbt_plugin_require_admin();
+	global $DB, $date;
+
+	$id = (int)($_POST['id'] ?? 0);
+	$supplier = zjmf_supplier_get($id);
+	if (!$supplier) {
+		json_exit_error('供应商不存在');
+	}
+	$newStatus = (int)$supplier['status'] === 1 ? 0 : 1;
+	$now = $date ?: date('Y-m-d H:i:s');
+	$DB->query_prepare(
+		"UPDATE MN_plugin_zjmf_supplier SET status=?, updated_at=? WHERE id=?",
+		[$newStatus, $now, $id]
+	);
+	json_exit_success($newStatus === 1 ? '已启用' : '已停用');
+});
+
+// 删除供应商(有商品/订单/主机时拒绝)
+mnbt_register_ajax('admin', 'p_zjmf_admin_delete_supplier', function () {
+	mnbt_plugin_require_admin();
+	$id = (int)($_POST['id'] ?? 0);
+	$result = zjmf_supplier_delete($id);
+	if (empty($result['ok'])) {
+		json_exit_error($result['msg'] ?? '删除失败');
+	}
+	json_exit_success($result['msg'] ?? '已删除');
+});
+
+// 连通测试(登录 + 商品列表)
+mnbt_register_ajax('admin', 'p_zjmf_admin_test_supplier', function () {
+	mnbt_plugin_require_admin();
+	$supplier = zjmf_supplier_get((int)($_POST['id'] ?? 0));
+	if (!$supplier) {
+		json_exit_error('供应商不存在');
+	}
+	$result = ZjmfUpstream::testConnection($supplier);
+	if (empty($result['ok'])) {
+		json_exit_error($result['msg'] ?? '连接失败');
+	}
+	json_exit_success($result['msg'] ?? '连接成功');
+});
+
+// 拉取供应商上游商品列表(供同步弹窗选择)
+mnbt_register_ajax('admin', 'p_zjmf_admin_upstream_products', function () {
+	mnbt_plugin_require_admin();
+	$supplier = zjmf_supplier_get((int)($_POST['id'] ?? 0));
+	if (!$supplier) {
+		json_exit_error('供应商不存在');
+	}
+	$result = ZjmfUpstream::upstreamProducts($supplier);
+	if (empty($result['ok'])) {
+		json_exit_error($result['msg'] ?? '拉取失败');
+	}
+	// 标注已同步商品,便于弹窗勾选时识别
+	$existing = [];
+	$rows = zjmf_product_list_all();
+	foreach ($rows as $p) {
+		if ((int)$p['supplier_id'] === (int)$supplier['id']) {
+			$existing[(int)$p['up_product_id']] = true;
+		}
+	}
+	$list = [];
+	foreach (($result['data']['list'] ?? []) as $item) {
+		$list[] = [
+			'id'          => (int)($item['id'] ?? 0),
+			'name'        => (string)($item['name'] ?? ''),
+			'description' => (string)($item['description'] ?? ''),
+			'synced'      => isset($existing[(int)($item['id'] ?? 0)]),
+		];
+	}
+	json_exit_success('拉取成功', [
+		'currency' => (string)($result['data']['currency_code'] ?? ''),
+		'list'     => $list,
+	]);
+});
+
+// 按所选供应商 + 勾选商品 ID 列表同步
+mnbt_register_ajax('admin', 'p_zjmf_admin_sync_products', function () {
+	mnbt_plugin_require_admin();
+	$supplier = zjmf_supplier_get((int)($_POST['supplier_id'] ?? 0));
+	if (!$supplier) {
+		json_exit_error('请先选择供应商');
+	}
+	$upIds = [];
+	if (isset($_POST['up_ids']) && is_array($_POST['up_ids'])) {
+		foreach ($_POST['up_ids'] as $v) {
+			$v = (int)$v;
+			if ($v > 0) {
+				$upIds[] = $v;
+			}
+		}
+	}
+	$result = ZjmfUpstream::syncProducts($supplier, $upIds);
+	if (empty($result['ok'])) {
+		json_exit_error($result['msg'] ?? '同步失败');
+	}
+	json_exit_success($result['msg'] ?? '同步完成');
+});
+
+// 手动添加商品(供应商 + 上游商品 ID + 名称 + 描述)
+mnbt_register_ajax('admin', 'p_zjmf_admin_add_product', function () {
+	mnbt_plugin_require_admin();
+	global $DB, $date;
+
+	$supplier = zjmf_supplier_get((int)($_POST['supplier_id'] ?? 0));
+	if (!$supplier || (int)$supplier['status'] !== 1) {
+		json_exit_error('供应商不存在或已停用');
+	}
+	$upId = (int)($_POST['up_product_id'] ?? 0);
+	$name = trim((string)($_POST['name'] ?? ''));
+	$description = trim((string)($_POST['description'] ?? ''));
+	if ($upId <= 0) {
+		json_exit_error('请填写上游商品 ID');
+	}
+	if ($name === '') {
+		json_exit_error('请填写商品名称');
+	}
+	if (mb_strlen($name) > 100) {
+		json_exit_error('商品名称过长');
+	}
+	if (zjmf_product_get_by_up((int)$supplier['id'], $upId)) {
+		json_exit_error('该供应商下已存在此上游商品');
+	}
+
+	$now = $date ?: date('Y-m-d H:i:s');
+	$ok = $DB->query_prepare(
+		"INSERT INTO MN_plugin_zjmf_product
+		 (supplier_id, up_product_id, name, description, currency,
+		  agent_price_cents, cycles, status, sort, synced_at, created_at, updated_at)
+		 VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
+		[(int)$supplier['id'], $upId, $name, $description, '', 0,
+		 '[]', 0, 50, $now, $now, $now]
+	);
+	if (!$ok) {
+		json_exit_error('商品创建失败');
+	}
+
+	// 立即拉取代理价与各周期价格(失败不阻断,商品标记待同步)
+	$result = ZjmfUpstream::syncOneProductBySupplier($supplier, $upId);
+	if (empty($result['ok'])) {
+		json_exit_error('商品已添加,但价格同步失败:' . ($result['msg'] ?? ''));
+	}
+	json_exit_success('商品已添加,价格同步完成');
+});
+
+// 保存商品加价/排序/状态
+mnbt_register_ajax('admin', 'p_zjmf_admin_save_product', function () {
+	mnbt_plugin_require_admin();
+
+	$id = (int)($_POST['id'] ?? 0);
+	$product = zjmf_product_get($id);
+	if (!$product) {
+		json_exit_error('商品不存在');
+	}
+
+	$markupType = in_array((string)($_POST['markup_type'] ?? ''), ['0', '1'], true)
+		? (int)$_POST['markup_type'] : 0;
+	$markupValue = max(0, (int)($_POST['markup_value'] ?? 0));
+	$sort = max(0, (int)($_POST['sort'] ?? 50));
+	$status = in_array((string)($_POST['status'] ?? ''), ['0', '1'], true)
+		? (int)$_POST['status'] : 0;
+
+	global $DB, $date;
+	$now = $date ?: date('Y-m-d H:i:s');
+	$ok = $DB->query_prepare(
+		"UPDATE MN_plugin_zjmf_product
+		 SET markup_type=?, markup_value=?, sort=?, status=?, updated_at=?
+		 WHERE id=?",
+		[$markupType, $markupValue, $sort, $status, $now, $id]
+	);
+	if (!$ok) {
+		json_exit_error('保存失败');
+	}
+	// 加价变化后重算各周期本地售价
+	zjmf_product_recalc_price($id);
+	json_exit_success('已保存');
+});
+
+// 切换商品上架/下架
+mnbt_register_ajax('admin', 'p_zjmf_admin_toggle_product', function () {
+	mnbt_plugin_require_admin();
+
+	$id = (int)($_POST['id'] ?? 0);
+	$product = zjmf_product_get($id);
+	if (!$product) {
+		json_exit_error('商品不存在');
+	}
+
+	global $DB, $date;
+	$now = $date ?: date('Y-m-d H:i:s');
+	$newStatus = $product['status'] == 1 ? 0 : 1;
+	$DB->query_prepare(
+		"UPDATE MN_plugin_zjmf_product SET status=?, updated_at=? WHERE id=?",
+		[$newStatus, $now, $id]
+	);
+	json_exit_success($newStatus == 1 ? '已上架' : '已下架');
+});
+
+/* ============================================================
+ *  用户端页面路由
+ * ============================================================ */
+
+// 商品列表
+mnbt_register_route('GET', '/reserve/shop', function ($params, $ctx) {
+	zjmf_require_user();
+	zjmf_render('shop', [
+		'page_title' => '商品选购',
+		'products'   => zjmf_product_list_active(),
+	]);
+});
+
+// 下单页(选周期 + 支付方式)
+mnbt_register_route('GET', '/reserve/product/{product_id}', function ($params, $ctx) {
+	zjmf_require_user();
+	$product_id = (int)($params['product_id'] ?? 0);
+	$product = zjmf_product_get($product_id);
+	if (!$product || (int)$product['status'] !== 1) {
+		http_response_code(404);
+		echo '商品不存在或已下架';
+		return;
+	}
+	if (!zjmf_supplier_usable((int)$product['supplier_id'])) {
+		http_response_code(404);
+		echo '商品所属供应商已停用';
+		return;
+	}
+	$methods = function_exists('mnbt_get_enabled_payment_methods')
+		? mnbt_get_enabled_payment_methods() : [];
+
+	zjmf_render('order', [
+		'page_title' => '购买:' . $product['name'],
+		'product'    => $product,
+		'methods'    => $methods,
+	]);
+});
+
+/* ============================================================
+ *  用户端 API 路由
+ * ============================================================ */
+
+// 创建购买订单 → 生成 MN_dd → 分发支付网关
+mnbt_register_route('POST', '/reserve/api/create_order', function ($params, $ctx) {
+	global $DB, $date, $siteurl;
+
+	$user = zjmf_require_user();
+	$user_id = (int)$user['id'];
+
+	$product_id = (int)($_POST['product_id'] ?? 0);
+	$cycle = isset($_POST['cycle']) ? trim($_POST['cycle']) : '';
+	$type = isset($_POST['type']) ? trim($_POST['type']) : '';
+
+	// 校验商品与周期
+	$product = zjmf_product_get($product_id);
+	if (!$product || (int)$product['status'] !== 1) {
+		zjmf_json('商品不存在或已下架');
+	}
+	if (!zjmf_supplier_usable((int)$product['supplier_id'])) {
+		zjmf_json('商品所属供应商已停用,无法下单');
+	}
+	$cycles = zjmf_product_cycles($product);
+	if (!isset($cycles[$cycle])) {
+		zjmf_json('请选择有效的购买周期');
+	}
+	$cycleCfg = $cycles[$cycle];
+	$amount_cents = (int)$cycleCfg['price_cents'];
+	if ($amount_cents <= 0) {
+		zjmf_json('该商品此周期价格异常');
+	}
+	// 校验支付方式
+	if (!function_exists('mnbt_pay_parse_type') || !mnbt_pay_parse_type($type)) {
+		zjmf_json('请选择有效的支付方式');
+	}
+
+	// 创建本地业务订单
+	$create = zjmf_order_create($user, $product, $cycle, $cycleCfg, 'buy', [
+		'cost_cents' => (int)$product['agent_price_cents'],
+	]);
+	if (empty($create['ok'])) {
+		zjmf_json($create['msg'] ?? '创建订单失败');
+	}
+	$order_no = $create['order_no'];
+	$order_id = (int)$create['order_id'];
+
+	// 创建 MN_dd 支付订单(lx=zjmf,qk=false 待支付)
+	$amount_yuan = (string)round($amount_cents / 100, 2);
+	$cs = json_encode([
+		'user_id'  => $user_id,
+		'order_id' => $order_id,
+		'amount'   => $amount_cents,
+		'username' => $user['username'],
+	], 256);
+	$ip = $_SERVER["REMOTE_ADDR"] ?? '127.0.0.1';
+
+	$lastRow = $DB->get_row_prepare("SELECT id FROM MN_dd ORDER BY id DESC LIMIT 1");
+	$dd_id = $lastRow ? ((int)$lastRow['id'] + 1) : 1;
+	$ok = $DB->query_prepare(
+		"INSERT INTO MN_dd (id, cs, date, zffs, je, ddh, lx, qk, ip)
+		 VALUES (?,?,?,?,?,?,?,?,?)",
+		[$dd_id, $cs, $date, $type, $amount_yuan, $order_no, ZJMF_LX, 'false', $ip]
+	);
+	if (!$ok) {
+		zjmf_order_set_status($order_id, 'cancelled', '支付订单创建失败');
+		zjmf_json('支付订单创建失败,请稍后重试');
+	}
+
+	// 分发到支付网关(余额插件 / 其他支付插件)
+	$order_context = [
+		'out_trade_no' => $order_no,
+		'name'         => '购买商品:' . $product['name'] . '(' . $cycleCfg['name'] . ')',
+		'money'        => $amount_yuan,
+		'type'         => $type,
+		'siteurl'      => $siteurl,
+		'pay_lx'       => ZJMF_LX,
+	];
+	$html = mnbt_pay_dispatch_gateway($type, $order_context);
+	if ($html === false) {
+		zjmf_order_set_status($order_id, 'cancelled', '支付方式不可用');
+		zjmf_json('支付方式不可用,请检查支付插件是否已启用');
+	}
+
+	zjmf_json('正在跳转到支付页面', [
+		'html'     => $html,
+		'order_no' => $order_no,
+	]);
+});
+
+// 我的订单
+mnbt_register_route('GET', '/reserve/orders', function ($params, $ctx) {
+	$user = zjmf_require_user();
+	$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
+	$orders = zjmf_order_list_by_user((int)$user['id'], $page, 15);
+
+	zjmf_render('orders', [
+		'page_title' => '我的订单',
+		'orders'     => $orders,
+	]);
+});
+
+// 我的主机列表
+mnbt_register_route('GET', '/reserve/hosts', function ($params, $ctx) {
+	$user = zjmf_require_user();
+	zjmf_render('hosts', [
+		'page_title' => '我的主机',
+		'hosts'      => zjmf_host_list_by_user((int)$user['id']),
+	]);
+});
+
+// 主机详情(实时状态 + 流量 + 操作)
+mnbt_register_route('GET', '/reserve/hosts/{host_id}', function ($params, $ctx) {
+	$user = zjmf_require_user();
+	$host_id = (int)($params['host_id'] ?? 0);
+	$host = zjmf_host_get_by_user((int)$user['id'], $host_id);
+	if (!$host) {
+		http_response_code(404);
+		echo '主机不存在';
+		return;
+	}
+
+	// 实时信息(失败不致命,仅展示缓存)
+	$info = ['ok' => false, 'msg' => ''];
+	$traffic = ['ok' => false, 'data' => []];
+	$supplier = zjmf_supplier_get((int)$host['supplier_id']);
+	if ((int)$host['up_host_id'] > 0 && $supplier) {
+		$info = ZjmfUpstream::hostInfo($supplier, (int)$host['up_host_id']);
+		$traffic = ZjmfUpstream::hostTraffic($supplier, (int)$host['up_host_id']);
+		// 拉取成功后同步缓存状态
+		if (!empty($info['ok']) && $info['status'] !== $host['status']) {
+			zjmf_host_update_cache((int)$host['id'], ['status' => $info['status']]);
+			$host['status'] = $info['status'];
+		}
+	}
+
+	zjmf_render('host', [
+		'page_title' => '主机详情:' . $host['name'],
+		'host'       => $host,
+		'info'       => $info,
+		'traffic'    => $traffic,
+	]);
+});
+
+// 主机操作(开机/关机/重启/重置密码/重装)
+mnbt_register_route('POST', '/reserve/api/host_action', function ($params, $ctx) {
+	$user = zjmf_require_user();
+
+	$host_id = (int)($_POST['host_id'] ?? 0);
+	$action = isset($_POST['action']) ? trim($_POST['action']) : '';
+
+	$host = zjmf_host_get_by_user((int)$user['id'], $host_id);
+	if (!$host) {
+		zjmf_json('主机不存在');
+	}
+	if ((int)$host['up_host_id'] <= 0) {
+		zjmf_json('该主机缺少上游主机 ID,无法执行操作');
+	}
+	if (!zjmf_supplier_usable((int)$host['supplier_id'])) {
+		zjmf_json('供应商已停用,无法执行操作');
+	}
+	$supplier = zjmf_supplier_get((int)$host['supplier_id']);
+
+	$func = zjmf_action_func($action);
+	if ($func === '') {
+		zjmf_json('不支持的操作');
+	}
+
+	// 重置密码需要新密码
+	$extra = [];
+	if ($action === 'reset_password') {
+		$password = trim((string)($_POST['password'] ?? ''));
+		if ($password === '') {
+			zjmf_json('请输入新密码');
+		}
+		$extra['password'] = $password;
+	}
+
+	$result = ZjmfUpstream::hostAction($supplier, (int)$host['up_host_id'], $func, $extra);
+	$hostOrder = zjmf_order_get((int)$host['order_id']);
+	$orderNo = $hostOrder ? $hostOrder['order_no'] : '';
+	zjmf_log((int)$user['id'], $orderNo, 'host_action:' . $action,
+		empty($result['ok']) ? 'failed' : 'success', $result['msg'] ?? '',
+		(int)$host['supplier_id']);
+
+	if (empty($result['ok'])) {
+		zjmf_json($result['msg'] ?? '操作失败');
+	}
+
+	// 操作成功:更新缓存状态与密码
+	$status = zjmf_action_status($action);
+	if ($status !== '') {
+		zjmf_host_update_cache((int)$host['id'], ['status' => $status]);
+	}
+	if ($action === 'reset_password' && $extra['password'] !== '') {
+		global $DB, $date;
+		$now = $date ?: date('Y-m-d H:i:s');
+		$DB->query_prepare(
+			"UPDATE MN_plugin_zjmf_host SET password=?, updated_at=? WHERE id=?",
+			[zjmf_encrypt($extra['password']), $now, (int)$host['id']]
+		);
+	}
+
+	zjmf_json('ok', ['msg' => '操作成功']);
+});
+
+/* ============================================================
+ *  升级(配置升级 / 产品升降级)
+ * ============================================================ */
+
+// 升级页(kind=config|product)
+mnbt_register_route('GET', '/reserve/hosts/{host_id}/upgrade', function ($params, $ctx) {
+	$user = zjmf_require_user();
+	$host_id = (int)($params['host_id'] ?? 0);
+	$kind = ($_GET['kind'] ?? 'config') === 'product' ? 'product' : 'config';
+
+	$host = zjmf_host_get_by_user((int)$user['id'], $host_id);
+	if (!$host) {
+		http_response_code(404);
+		echo '主机不存在';
+		return;
+	}
+	if ((int)$host['up_host_id'] <= 0) {
+		echo '该主机缺少上游主机 ID,无法升级';
+		return;
+	}
+	if (!zjmf_supplier_usable((int)$host['supplier_id'])) {
+		echo '供应商已停用,无法升级';
+		return;
+	}
+	$supplier = zjmf_supplier_get((int)$host['supplier_id']);
+
+	$options = ZjmfUpstream::upgradeOptions($supplier, (int)$host['up_host_id'], $kind);
+
+	zjmf_render('upgrade', [
+		'page_title' => '升级:' . $host['name'],
+		'host'       => $host,
+		'kind'       => $kind,
+		'options'    => $options,
+	]);
+});
+
+// 升级接口(preview=1 试算差额;preview=0 确认扣款并提交)
+mnbt_register_route('POST', '/reserve/api/upgrade', function ($params, $ctx) {
+	$user = zjmf_require_user();
+
+	$host_id = (int)($_POST['host_id'] ?? 0);
+	$kind = ($_POST['kind'] ?? 'config') === 'product' ? 'product' : 'config';
+	$isPreview = (($_POST['preview'] ?? '') === '1');
+
+	$host = zjmf_host_get_by_user((int)$user['id'], $host_id);
+	if (!$host) {
+		zjmf_json('主机不存在');
+	}
+	if ((int)$host['up_host_id'] <= 0) {
+		zjmf_json('该主机缺少上游主机 ID,无法升级');
+	}
+	if (!zjmf_supplier_usable((int)$host['supplier_id'])) {
+		zjmf_json('供应商已停用,无法升级');
+	}
+	$supplier = zjmf_supplier_get((int)$host['supplier_id']);
+
+	// 构造升级选择参数
+	if ($kind === 'config') {
+		$raw = (string)($_POST['config_json'] ?? '');
+		$decoded = json_decode($raw, true);
+		if (!is_array($decoded) || $decoded === []) {
+			zjmf_json('请选择配置项');
+		}
+		$selection = ['configoption' => $decoded];
+	} else {
+		$newpid = (int)($_POST['newpid'] ?? 0);
+		$billingcycle = trim((string)($_POST['billingcycle'] ?? ''));
+		if ($newpid <= 0 || $billingcycle === '') {
+			zjmf_json('请选择目标产品与周期');
+		}
+		$selection = ['newpid' => $newpid, 'billingcycle' => $billingcycle];
+	}
+
+	// 试算差额
+	$preview = ZjmfUpstream::upgradePreview($supplier, $kind, (int)$host['up_host_id'], $selection);
+	if (empty($preview['ok'])) {
+		zjmf_json($preview['msg'] ?? '试算失败');
+	}
+	$amount_cents = (int)($preview['price_cents'] ?? 0);
+	if ($isPreview) {
+		zjmf_json('ok', [
+			'price_cents' => $amount_cents,
+			'price'       => zjmf_format_cents($amount_cents),
+		]);
+	}
+
+	// 防重复:同一主机存在未完成升级单则拦截
+	global $DB;
+	$dup = $DB->get_row_prepare(
+		"SELECT id FROM MN_plugin_zjmf_order
+		 WHERE host_id=? AND action=? AND status IN ('pending','paid')
+		 LIMIT 1",
+		[(int)$host['id'], 'upgrade_' . $kind]
+	);
+	if ($dup) {
+		zjmf_json('该主机已有未完成的升级订单');
+	}
+
+	$action = 'upgrade_' . $kind;
+	$orderParams = json_encode($selection, JSON_UNESCAPED_UNICODE);
+
+	// 创建升级订单
+	$create = zjmf_upgrade_order_create($user, $host, $action, $amount_cents, $orderParams);
+	if (empty($create['ok'])) {
+		zjmf_json($create['msg'] ?? '创建升级订单失败');
+	}
+	$order_id = (int)$create['order_id'];
+	$order_no = $create['order_no'];
+
+	// 扣余额(差额为 0 时跳过)
+	if ($amount_cents > 0) {
+		$deducted = function_exists('balance_deduct')
+			&& balance_deduct((int)$user['id'], $amount_cents, 'consume',
+				$order_no, '升级扣款');
+		if (!$deducted) {
+			zjmf_order_set_status($order_id, 'failed', '余额不足');
+			zjmf_json('余额不足,无法完成升级');
+		}
+	}
+
+	// 提交上游
+	$submit = ZjmfUpstream::upgradeSubmit($supplier, $kind, (int)$host['up_host_id'], $selection);
+	if (empty($submit['ok'])) {
+		$msg = (string)($submit['msg'] ?? '升级提交失败');
+		zjmf_order_set_status($order_id, 'failed', $msg);
+		// 原路退回余额
+		if ($amount_cents > 0 && function_exists('balance_add')) {
+			balance_add((int)$user['id'], $amount_cents, 'refund',
+				$order_no, '升级失败自动退款');
+		}
+		zjmf_log((int)$user['id'], $order_no, $action, 'failed',
+			json_encode(['msg' => $msg], JSON_UNESCAPED_UNICODE),
+			(int)$host['supplier_id']);
+		zjmf_json($msg);
+	}
+
+	// 升级成功:回填订单 + 更新主机缓存
+	zjmf_order_fill_opened($order_id, (int)($submit['up_order_id'] ?? 0),
+		(int)$host['up_host_id'], (string)$host['username']);
+	zjmf_order_set_status($order_id, 'opened', '升级完成');
+
+	$cache = [];
+	if ($kind === 'product') {
+		$cache['up_product_id'] = (int)($_POST['newpid'] ?? 0);
+	}
+	$cache['cycle'] = (string)($_POST['billingcycle'] ?? $host['cycle']);
+	$cache['renew_date'] = (string)($preview['data']['renew_date'] ?? '');
+	zjmf_host_update_cache((int)$host['id'], $cache);
+
+	zjmf_log((int)$user['id'], $order_no, $action, 'success',
+		json_encode(['amount_cents' => $amount_cents, 'selection' => $selection],
+			JSON_UNESCAPED_UNICODE),
+		(int)$host['supplier_id']);
+
+	zjmf_json('ok', ['msg' => '升级成功']);
+});
+
+/* ============================================================
+ *  管理员端 - 主机操作 AJAX
+ * ============================================================ */
+
+// 刷新主机状态(管理员)
+mnbt_register_ajax('admin', 'p_zjmf_admin_fetch_host', function () {
+	mnbt_plugin_require_admin();
+
+	$id = (int)($_POST['id'] ?? 0);
+	$host = zjmf_host_get($id);
+	if (!$host) {
+		json_exit_error('主机不存在');
+	}
+	if ((int)$host['up_host_id'] <= 0) {
+		json_exit_error('该主机缺少上游主机 ID');
+	}
+	$supplier = zjmf_supplier_get((int)$host['supplier_id']);
+	if (!$supplier) {
+		json_exit_error('主机所属供应商不存在');
+	}
+	$info = ZjmfUpstream::hostInfo($supplier, (int)$host['up_host_id']);
+	if (empty($info['ok'])) {
+		json_exit_error($info['msg'] ?? '查询失败');
+	}
+	$cache = ['status' => $info['status']];
+	if (isset($info['data']['renewdate']) || isset($info['data']['renew_date'])) {
+		$cache['renew_date'] = (string)($info['data']['renew_date']
+			?? $info['data']['renewdate'] ?? '');
+	}
+	zjmf_host_update_cache($id, $cache);
+	json_exit_success('已刷新', ['status' => $info['status']]);
+});
diff --git a/app_plugins/zjmfmanager_reserve/install.sql b/app_plugins/zjmfmanager_reserve/install.sql
new file mode 100644
index 0000000..eab3ff0
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/install.sql
@@ -0,0 +1,104 @@
+-- 魔方财务代理分销插件 - 建表
+-- v1.1 多供应商:新增供应商表,product/order/host/log 增加 supplier_id
+
+CREATE TABLE IF NOT EXISTS `MN_plugin_zjmf_supplier` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(50) NOT NULL DEFAULT '' COMMENT '供应商名称',
+  `api_url` varchar(255) NOT NULL DEFAULT '' COMMENT '上游站点根地址',
+  `api_username` varchar(64) NOT NULL DEFAULT '' COMMENT 'API 用户名',
+  `api_password` varchar(255) NOT NULL DEFAULT '' COMMENT 'API 密钥(仅保存时写入,不回显)',
+  `api_timeout` int(11) NOT NULL DEFAULT '30' COMMENT '请求超时(秒)',
+  `markup_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '加价方式:0=比例 1=固定(分)',
+  `markup_value` bigint(20) NOT NULL DEFAULT '0' COMMENT '加价比例(千分比)或固定加价(分)',
+  `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1=启用 0=停用',
+  `sort` int(11) NOT NULL DEFAULT '50',
+  `remark` varchar(255) NOT NULL DEFAULT '',
+  `created_at` datetime DEFAULT NULL,
+  `updated_at` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `status` (`status`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS `MN_plugin_zjmf_product` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `supplier_id` int(11) NOT NULL DEFAULT '0' COMMENT '所属供应商ID',
+  `up_product_id` int(11) NOT NULL DEFAULT '0' COMMENT '上游商品ID',
+  `name` varchar(100) NOT NULL DEFAULT '',
+  `description` text,
+  `currency` varchar(10) NOT NULL DEFAULT '',
+  `agent_price_cents` bigint(20) NOT NULL DEFAULT '0' COMMENT '上游代理价(分)',
+  `markup_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '加价方式:0=比例 1=固定(分)',
+  `markup_value` bigint(20) NOT NULL DEFAULT '0' COMMENT '加价比例(千分比)或固定加价(分)',
+  `cycles` text COMMENT '周期JSON:[{cycle,name,price_cents}]',
+  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '1=上架 0=下架',
+  `sort` int(11) NOT NULL DEFAULT '50',
+  `synced_at` datetime DEFAULT NULL,
+  `created_at` datetime DEFAULT NULL,
+  `updated_at` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_supplier_up` (`supplier_id`,`up_product_id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS `MN_plugin_zjmf_order` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `order_no` varchar(64) NOT NULL DEFAULT '',
+  `action` varchar(20) NOT NULL DEFAULT 'buy' COMMENT 'buy/upgrade_config/upgrade_product',
+  `supplier_id` int(11) NOT NULL DEFAULT '0' COMMENT '所属供应商ID(开通路由依据)',
+  `user_id` int(11) NOT NULL DEFAULT '0',
+  `product_id` int(11) NOT NULL DEFAULT '0',
+  `up_product_id` int(11) NOT NULL DEFAULT '0',
+  `product_name` varchar(100) NOT NULL DEFAULT '',
+  `cycle` varchar(20) NOT NULL DEFAULT '',
+  `cycle_name` varchar(20) NOT NULL DEFAULT '',
+  `amount_cents` bigint(20) NOT NULL DEFAULT '0',
+  `cost_cents` bigint(20) NOT NULL DEFAULT '0',
+  `order_params` text,
+  `up_order_id` int(11) NOT NULL DEFAULT '0',
+  `up_host_id` int(11) NOT NULL DEFAULT '0',
+  `host_id` int(11) NOT NULL DEFAULT '0' COMMENT '本地主机映射ID',
+  `username` varchar(64) NOT NULL DEFAULT '',
+  `status` varchar(20) NOT NULL DEFAULT 'pending',
+  `pay_time` datetime DEFAULT NULL,
+  `opened_at` datetime DEFAULT NULL,
+  `remark` varchar(255) NOT NULL DEFAULT '',
+  `created_at` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `order_no` (`order_no`),
+  KEY `user_id` (`user_id`),
+  KEY `supplier_id` (`supplier_id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS `MN_plugin_zjmf_host` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `supplier_id` int(11) NOT NULL DEFAULT '0' COMMENT '所属供应商ID(操作/升级路由依据)',
+  `user_id` int(11) NOT NULL DEFAULT '0',
+  `order_id` int(11) NOT NULL DEFAULT '0',
+  `up_host_id` int(11) NOT NULL DEFAULT '0',
+  `up_product_id` int(11) NOT NULL DEFAULT '0',
+  `name` varchar(100) NOT NULL DEFAULT '',
+  `username` varchar(64) NOT NULL DEFAULT '',
+  `password` varchar(255) NOT NULL DEFAULT '' COMMENT 'authcode加密存储',
+  `cycle` varchar(20) NOT NULL DEFAULT '',
+  `status` varchar(20) NOT NULL DEFAULT 'unknown',
+  `renew_date` varchar(20) NOT NULL DEFAULT '',
+  `created_at` datetime DEFAULT NULL,
+  `updated_at` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `user_id` (`user_id`),
+  KEY `supplier_id` (`supplier_id`),
+  KEY `up_host_id` (`up_host_id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS `MN_plugin_zjmf_log` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL DEFAULT '0',
+  `supplier_id` int(11) NOT NULL DEFAULT '0' COMMENT '供应商ID',
+  `order_no` varchar(64) NOT NULL DEFAULT '',
+  `action` varchar(50) NOT NULL DEFAULT '',
+  `result` varchar(20) NOT NULL DEFAULT 'success',
+  `content` text,
+  `created_at` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `order_no` (`order_no`),
+  KEY `supplier_id` (`supplier_id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
diff --git a/app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php b/app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php
new file mode 100644
index 0000000..2001490
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php
@@ -0,0 +1,558 @@
+<?php
+/**
+ * 魔方财务 (cube_finance / 智简魔方财务) API 客户端
+ *
+ * 改编自 app_plugins/zjmfmanager_reserve/example/sdk/CubeFinanceClient.php。
+ * 认证流程与魔方云 v10 上游对接一致:
+ *   POST /zjmf_api_login  →  JWT
+ *   Authorization: Bearer {jwt}
+ *
+ * 适用于 PHP 7.2+,仅依赖 ext-curl / ext-json。
+ * 返回 status=401/405 时自动强制重登并重试一次。
+ */
+
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+
+if (!class_exists('CubeFinanceException', false)) {
+
+class CubeFinanceException extends Exception
+{
+	/** @var array|null */
+	public $response;
+
+	public function __construct($message, $code = 0, $response = null)
+	{
+		parent::__construct($message, (int)$code);
+		$this->response = $response;
+	}
+}
+
+}
+
+if (!class_exists('CubeFinanceClient', false)) {
+
+class CubeFinanceClient
+{
+	/** @var string */
+	protected $baseUrl;
+
+	/** @var string */
+	protected $username;
+
+	/** @var string */
+	protected $password;
+
+	/** @var string|null */
+	protected $jwt;
+
+	/** @var int */
+	protected $timeout = 30;
+
+	/** @var string|null JWT 文件缓存目录,null 则仅内存缓存 */
+	protected $cacheDir;
+
+	/** @var int JWT 缓存秒数,默认 2 小时(与 v10 一致) */
+	protected $jwtTtl = 7200;
+
+	/** @var bool */
+	protected $verifySsl = false;
+
+	/**
+	 * @param array $config
+	 *  - url        string  魔方财务站点根地址
+	 *  - username   string  API 账号(客户用户名)
+	 *  - password   string  API 密钥(客户 API 密码)
+	 *  - timeout    int     请求超时秒数
+	 *  - cache_dir  string  JWT 缓存目录(可写)
+	 *  - jwt_ttl    int     JWT 缓存时长
+	 *  - verify_ssl bool    是否校验 HTTPS 证书
+	 *  - jwt        string  已有 JWT(可选,跳过登录)
+	 */
+	public function __construct(array $config)
+	{
+		if (empty($config['url'])) {
+			throw new InvalidArgumentException('缺少 url');
+		}
+		$this->baseUrl = rtrim($config['url'], '/');
+		$this->username = isset($config['username']) ? (string)$config['username'] : '';
+		$this->password = isset($config['password']) ? (string)$config['password'] : '';
+		$this->timeout = isset($config['timeout']) ? (int)$config['timeout'] : 30;
+		$this->cacheDir = isset($config['cache_dir']) ? rtrim($config['cache_dir'], '/\\') : null;
+		$this->jwtTtl = isset($config['jwt_ttl']) ? (int)$config['jwt_ttl'] : 7200;
+		$this->verifySsl = !empty($config['verify_ssl']);
+		if (!empty($config['jwt'])) {
+			$this->jwt = (string)$config['jwt'];
+		}
+	}
+
+	/**
+	 * 登录获取 JWT(会写入缓存)。
+	 *
+	 * @param bool $force 强制重新登录
+	 * @return string
+	 * @throws CubeFinanceException
+	 */
+	public function login($force = false)
+	{
+		if (!$force) {
+			$cached = $this->getCachedJwt();
+			if ($cached) {
+				$this->jwt = $cached;
+				return $this->jwt;
+			}
+		}
+
+		if ($this->username === '' || $this->password === '') {
+			throw new CubeFinanceException('缺少 username 或 password,无法登录');
+		}
+
+		$raw = $this->rawRequest('POST', 'zjmf_api_login', [
+			'username' => $this->username,
+			'password' => $this->password,
+		], false);
+
+		$result = $this->decodeJson($raw);
+		if (!isset($result['status']) || (int)$result['status'] !== 200 || empty($result['jwt'])) {
+			$msg = isset($result['msg']) ? $result['msg'] : '登录失败';
+			throw new CubeFinanceException($msg, isset($result['status']) ? (int)$result['status'] : 400, $result);
+		}
+
+		$this->jwt = $result['jwt'];
+		$this->setCachedJwt($this->jwt);
+		return $this->jwt;
+	}
+
+	/**
+	 * 当前 JWT(未登录则自动登录)。
+	 *
+	 * @return string
+	 */
+	public function getJwt()
+	{
+		if ($this->jwt) {
+			return $this->jwt;
+		}
+		return $this->login();
+	}
+
+	/**
+	 * 通用请求(自动附带 Bearer,401/405 时强制重登一次)。
+	 *
+	 * @param string $method GET|POST|PUT|DELETE
+	 * @param string $path   相对路径,如 api/product/list
+	 * @param array  $data
+	 * @return array
+	 * @throws CubeFinanceException
+	 */
+	public function request($method, $path, array $data = [])
+	{
+		$this->getJwt();
+		$raw = $this->rawRequest($method, $path, $data, true);
+		$result = $this->decodeJson($raw);
+
+		if (isset($result['status']) && in_array((int)$result['status'], [401, 405], true)) {
+			$this->login(true);
+			$raw = $this->rawRequest($method, $path, $data, true);
+			$result = $this->decodeJson($raw);
+			if (isset($result['status']) && (int)$result['status'] === 401) {
+				throw new CubeFinanceException(
+					isset($result['msg']) ? $result['msg'] : 'API 账号或密码错误',
+					401,
+					$result
+				);
+			}
+		}
+
+		return $result;
+	}
+
+	public function get($path, array $data = [])
+	{
+		return $this->request('GET', $path, $data);
+	}
+
+	public function post($path, array $data = [])
+	{
+		return $this->request('POST', $path, $data);
+	}
+
+	public function put($path, array $data = [])
+	{
+		return $this->request('PUT', $path, $data);
+	}
+
+	public function delete($path, array $data = [])
+	{
+		return $this->request('DELETE', $path, $data);
+	}
+
+	// ==================== 业务封装 ====================
+
+	/**
+	 * 测试连通性:登录并拉取商品列表。
+	 *
+	 * @return array{ok:bool,msg:string,jwt?:string,product_count?:int,raw?:array}
+	 */
+	public function testConnection()
+	{
+		try {
+			$jwt = $this->login(true);
+			$list = $this->productList();
+			$count = 0;
+			if (isset($list['data']['list']) && is_array($list['data']['list'])) {
+				$count = count($list['data']['list']);
+			}
+			return [
+				'ok'            => true,
+				'msg'           => '连接成功',
+				'jwt'           => substr($jwt, 0, 24) . '...',
+				'product_count' => $count,
+				'raw'           => $list,
+			];
+		} catch (Exception $e) {
+			return [
+				'ok'  => false,
+				'msg' => $e->getMessage(),
+			];
+		}
+	}
+
+	/**
+	 * 商品列表。
+	 * GET /api/product/list
+	 *
+	 * @return array status/msg/data.list/data.currency_code
+	 */
+	public function productList()
+	{
+		return $this->get('api/product/list');
+	}
+
+	/**
+	 * 商品详情。
+	 * GET /api/product/{id}?price_basis=agent
+	 *
+	 * @param int    $productId
+	 * @param string $priceBasis agent|cost 等
+	 * @return array
+	 */
+	public function productDetail($productId, $priceBasis = 'agent')
+	{
+		return $this->get('api/product/' . intval($productId), [
+			'price_basis' => $priceBasis,
+		]);
+	}
+
+	/**
+	 * 购物车配置 / 价格试算。
+	 * GET cart/set_config
+	 *
+	 * @param array $params 通常含 pid、billingcycle、配置项等
+	 * @return array
+	 */
+	public function cartSetConfig(array $params)
+	{
+		return $this->get('cart/set_config', $params);
+	}
+
+	/**
+	 * 账户信息 / 余额。
+	 * GET user_info
+	 *
+	 * @return array
+	 */
+	public function userInfo()
+	{
+		return $this->get('user_info');
+	}
+
+	/**
+	 * 主机头信息。
+	 * GET host/header?host_id={id}
+	 *
+	 * @param int $hostId 上游主机 ID
+	 * @return array
+	 */
+	public function hostHeader($hostId)
+	{
+		return $this->get('host/header', ['host_id' => intval($hostId)]);
+	}
+
+	/**
+	 * 流量使用。
+	 * GET host/trafficusage
+	 *
+	 * @param int   $hostId
+	 * @param array $extra 如 start/end
+	 * @return array
+	 */
+	public function hostTrafficUsage($hostId, array $extra = [])
+	{
+		$data = array_merge(['host_id' => intval($hostId)], $extra);
+		return $this->get('host/trafficusage', $data);
+	}
+
+	/**
+	 * 开通模块默认操作(开关机/重启/重装/重置密码等)。
+	 * POST provision/default
+	 *
+	 * @param array $params 含 id(host_id)、func 等
+	 * @return array
+	 */
+	public function provisionDefault(array $params)
+	{
+		return $this->post('provision/default', $params);
+	}
+
+	/**
+	 * 配置升级页。
+	 * GET upgrade/index/{hostId}
+	 *
+	 * @param int   $hostId
+	 * @param array $params
+	 * @return array
+	 */
+	public function upgradeIndex($hostId, array $params = [])
+	{
+		return $this->get('upgrade/index/' . intval($hostId), $params);
+	}
+
+	/**
+	 * 提交配置升级。
+	 * POST upgrade/upgrade_config_post
+	 *
+	 * @param array $params
+	 * @return array
+	 */
+	public function upgradeConfigPost(array $params)
+	{
+		return $this->post('upgrade/upgrade_config_post', $params);
+	}
+
+	/**
+	 * 配置升级确认页。
+	 * GET upgrade/upgrade_config_page
+	 *
+	 * @param int    $hostId
+	 * @param string $priceBasis
+	 * @return array
+	 */
+	public function upgradeConfigPage($hostId, $priceBasis = 'agent')
+	{
+		return $this->get('upgrade/upgrade_config_page', [
+			'hid'         => intval($hostId),
+			'price_basis' => $priceBasis,
+		]);
+	}
+
+	/**
+	 * 产品升降级选项。
+	 * GET upgrade/upgrade_product/{hostId}
+	 *
+	 * @param int   $hostId
+	 * @param array $params
+	 * @return array
+	 */
+	public function upgradeProduct($hostId, array $params = [])
+	{
+		return $this->get('upgrade/upgrade_product/' . intval($hostId), $params);
+	}
+
+	/**
+	 * 提交产品升降级。
+	 * POST upgrade/upgrade_product_post
+	 *
+	 * @param array $params
+	 * @return array
+	 */
+	public function upgradeProductPost(array $params)
+	{
+		return $this->post('upgrade/upgrade_product_post', $params);
+	}
+
+	/**
+	 * 余额抵扣 / 支付(视站点配置而定)。
+	 * POST apply_credit
+	 *
+	 * @param array $params
+	 * @return array
+	 */
+	public function applyCredit(array $params)
+	{
+		return $this->post('apply_credit', $params);
+	}
+
+	/**
+	 * 任意未封装接口。
+	 *
+	 * @param string $method GET|POST|PUT|DELETE
+	 * @param string $path   相对路径
+	 * @param array  $data
+	 * @return array
+	 */
+	public function custom($method, $path, array $data = [])
+	{
+		return $this->request($method, ltrim($path, '/'), $data);
+	}
+
+	// ==================== 底层 HTTP ====================
+
+	/**
+	 * @param string $method
+	 * @param string $path
+	 * @param array  $data
+	 * @param bool   $auth
+	 * @return string 原始响应体
+	 * @throws CubeFinanceException
+	 */
+	protected function rawRequest($method, $path, array $data, $auth)
+	{
+		$method = strtoupper($method);
+		$url = $this->baseUrl . '/' . ltrim($path, '/');
+		$headers = [
+			'User-Agent: CubeFinanceClient/1.0 (+php)',
+			'Accept: application/json',
+		];
+		if ($auth) {
+			$headers[] = 'Authorization: Bearer ' . $this->jwt;
+		}
+
+		$ch = curl_init();
+		if ($method === 'GET') {
+			$qs = http_build_query($data);
+			if ($qs !== '') {
+				$url .= (strpos($url, '?') === false ? '?' : '&') . $qs;
+			}
+			curl_setopt($ch, CURLOPT_HTTPGET, true);
+		} else {
+			curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
+			curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
+			if ($method === 'POST') {
+				curl_setopt($ch, CURLOPT_POST, true);
+			}
+		}
+
+		curl_setopt_array($ch, [
+			CURLOPT_URL            => $url,
+			CURLOPT_RETURNTRANSFER => true,
+			CURLOPT_TIMEOUT        => $this->timeout,
+			CURLOPT_FOLLOWLOCATION => true,
+			CURLOPT_SSL_VERIFYPEER => $this->verifySsl,
+			CURLOPT_SSL_VERIFYHOST => $this->verifySsl ? 2 : 0,
+			CURLOPT_HTTPHEADER     => $headers,
+			CURLOPT_HEADER         => false,
+		]);
+
+		$body = curl_exec($ch);
+		$errno = curl_errno($ch);
+		$error = curl_error($ch);
+		$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
+		curl_close($ch);
+
+		if ($errno) {
+			throw new CubeFinanceException('网络错误: ' . $error, $errno);
+		}
+		if ($httpCode < 200 || $httpCode >= 300) {
+			$snippet = is_string($body) ? $this->truncate($body, 300) : '';
+			throw new CubeFinanceException(
+				"HTTP {$httpCode}" . ($snippet !== '' ? ': ' . $snippet : ''),
+				$httpCode,
+				['http_code' => $httpCode, 'body' => $body]
+			);
+		}
+		if ($body === false || $body === '') {
+			throw new CubeFinanceException('空响应', $httpCode);
+		}
+
+		return $body;
+	}
+
+	/**
+	 * @param string $raw
+	 * @return array
+	 * @throws CubeFinanceException
+	 */
+	protected function decodeJson($raw)
+	{
+		$data = json_decode($raw, true);
+		if (!is_array($data)) {
+			throw new CubeFinanceException(
+				'响应不是合法 JSON: ' . $this->truncate((string)$raw, 200)
+			);
+		}
+		return $data;
+	}
+
+	/**
+	 * 截断字符串(mb_substr 不可用时回退 substr)。
+	 *
+	 * @param string $str
+	 * @param int    $len
+	 * @return string
+	 */
+	protected function truncate($str, $len)
+	{
+		if (function_exists('mb_substr')) {
+			return mb_substr($str, 0, $len);
+		}
+		return substr($str, 0, $len);
+	}
+
+	protected function cacheKey()
+	{
+		return 'jwt_' . md5($this->baseUrl . '|' . $this->username);
+	}
+
+	protected function cacheFile()
+	{
+		if (!$this->cacheDir) {
+			return null;
+		}
+		if (!is_dir($this->cacheDir)) {
+			@mkdir($this->cacheDir, 0755, true);
+		}
+		return $this->cacheDir . DIRECTORY_SEPARATOR . $this->cacheKey() . '.json';
+	}
+
+	protected function getCachedJwt()
+	{
+		$file = $this->cacheFile();
+		if (!$file || !is_file($file)) {
+			return null;
+		}
+		$json = @file_get_contents($file);
+		$data = json_decode((string)$json, true);
+		if (empty($data['jwt']) || empty($data['expire']) || time() >= (int)$data['expire']) {
+			return null;
+		}
+		return $data['jwt'];
+	}
+
+	protected function setCachedJwt($jwt)
+	{
+		$file = $this->cacheFile();
+		if (!$file) {
+			return;
+		}
+		@file_put_contents($file, json_encode([
+			'jwt'    => $jwt,
+			'expire' => time() + $this->jwtTtl,
+		]));
+	}
+
+	/**
+	 * 清除 JWT 缓存。
+	 */
+	public function clearCache()
+	{
+		$this->jwt = null;
+		$file = $this->cacheFile();
+		if ($file && is_file($file)) {
+			@unlink($file);
+		}
+	}
+}
+
+}
diff --git a/app_plugins/zjmfmanager_reserve/lib/upstream.php b/app_plugins/zjmfmanager_reserve/lib/upstream.php
new file mode 100644
index 0000000..1490a0b
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/lib/upstream.php
@@ -0,0 +1,658 @@
+<?php
+/**
+ * zjmfmanager_reserve 插件 - 上游服务层
+ *
+ * 封装对魔方财务(cube_finance)的所有上游调用,供订单开通、主机操作、
+ * 商品同步、升级使用。所有方法均按供应商行($supplier)路由,
+ * 每个供应商独立客户端实例与独立 JWT 缓存(runtime/cache/s{id})。
+ * 上游响应字段因版本/定制站存在差异,解析均为防御式,
+ * 联调时以实际站点返回为准(见 PRD §3.3 / §12 Q1、Q3)。
+ */
+
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+
+require_once __DIR__ . '/CubeFinanceClient.php';
+
+/** 上游下单端点(方案 A,Q1 联调确认;如上游为 provision 直通则改此处) */
+define('ZJMF_CHECKOUT_PATH', 'cart/checkout');
+
+class ZjmfUpstream
+{
+	/**
+	 * 创建上游客户端(按供应商行);连接信息不完整返回 null。
+	 * JWT 缓存目录按供应商 ID 隔离,避免多供应商凭证串扰。
+	 *
+	 * @param array|null $supplier MN_plugin_zjmf_supplier 行
+	 * @return CubeFinanceClient|null
+	 */
+	public static function client($supplier)
+	{
+		if (!is_array($supplier)) {
+			return null;
+		}
+		$apiUrl = (string)($supplier['api_url'] ?? '');
+		$username = (string)($supplier['api_username'] ?? '');
+		$password = (string)($supplier['api_password'] ?? '');
+		if ($apiUrl === '' || $username === '' || $password === '') {
+			return null;
+		}
+		$supplierId = (int)($supplier['id'] ?? 0);
+		$cacheDir = mnbt_plugin_path('zjmfmanager_reserve')
+			. 'runtime/cache/s' . $supplierId;
+		return new CubeFinanceClient([
+			'url'        => $apiUrl,
+			'username'   => $username,
+			'password'   => $password,
+			'timeout'    => (int)($supplier['api_timeout'] ?? 30) > 0
+				? (int)($supplier['api_timeout'] ?? 30) : 30,
+			'cache_dir'  => $cacheDir,
+			'verify_ssl' => false,
+		]);
+	}
+
+	/** 连通性测试(登录 + 商品列表),按供应商。 */
+	public static function testConnection($supplier)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		return $client->testConnection();
+	}
+
+	/* ============================================================
+	 *  商品同步
+	 * ============================================================ */
+
+	/** 上游商品列表(同步弹窗拉取,不做入库)。 */
+	public static function upstreamProducts($supplier)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$res = $client->productList();
+			if (($res['status'] ?? 0) != 200) {
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '上游返回异常')];
+			}
+			return ['ok' => true, 'data' => $res['data'] ?? []];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/**
+	 * 按供应商同步单个商品(手动添加后拉取代理价与各周期价格)。
+	 * 幂等:按 supplier_id + up_product_id upsert。
+	 *
+	 * @param array $supplier     供应商行
+	 * @param int   $upProductId  上游商品 ID
+	 * @return array ['ok'=>bool, 'msg'=>string]
+	 */
+	public static function syncOneProductBySupplier($supplier, $upProductId)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$upId = (int)$upProductId;
+			$detail = $client->productDetail($upId, 'agent');
+			$prod = $detail['data']['product'] ?? ($detail['data'] ?? []);
+			$item = [
+				'id'          => $upId,
+				'name'        => (string)($prod['name'] ?? ''),
+				'description' => (string)($prod['description'] ?? ''),
+			];
+			$currency = (string)($detail['data']['currency_code'] ?? '');
+			$ok = self::syncOneProduct(
+				$client, (int)($supplier['id'] ?? 0), $upId, $item, $currency
+			);
+			return $ok
+				? ['ok' => true, 'msg' => '商品价格已同步']
+				: ['ok' => false, 'msg' => '商品写入失败'];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/** 上游商品详情(代理价),供手动添加 / 单品刷新。 */
+	public static function productDetail($supplier, $upProductId)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$res = $client->productDetail((int)$upProductId, 'agent');
+			if (($res['status'] ?? 0) != 200) {
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '获取商品详情失败')];
+			}
+			return ['ok' => true, 'data' => $res['data'] ?? []];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/**
+	 * 同步商品(按供应商,可选勾选商品 ID 列表)。
+	 * 列表 + 代理价详情 + 各周期试算价。
+	 * 幂等:按 supplier_id + up_product_id upsert,
+	 * 不覆盖管理员加价/上架/排序。$upIds 为空表示同步全部。
+	 *
+	 * @param array $supplier 供应商行
+	 * @param array $upIds    勾选的上游商品 ID 列表
+	 * @return array ['ok'=>bool, 'msg'=>string]
+	 */
+	public static function syncProducts($supplier, array $upIds = [])
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		$supplierId = (int)($supplier['id'] ?? 0);
+		try {
+			$res = $client->productList();
+			if (($res['status'] ?? 0) != 200 || empty($res['data']['list'])) {
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '上游返回异常')];
+			}
+			$currency = (string)($res['data']['currency_code'] ?? '');
+			$count = 0;
+			$fail = 0;
+			$skip = 0;
+			foreach ($res['data']['list'] as $item) {
+				$upId = (int)($item['id'] ?? 0);
+				if ($upId <= 0) {
+					continue;
+				}
+				if ($upIds && !in_array($upId, $upIds, true)) {
+					$skip++;
+					continue;
+				}
+				if (self::syncOneProduct($client, $supplierId, $upId, $item, $currency)) {
+					$count++;
+				} else {
+					$fail++;
+				}
+			}
+			$msg = "同步完成:更新 {$count} 个,失败 {$fail} 个";
+			if ($skip > 0) {
+				$msg .= ",跳过 {$skip} 个";
+			}
+			return ['ok' => true, 'msg' => $msg];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/** 同步单个商品。 */
+	protected static function syncOneProduct($client, $supplierId, $upId, $item, $currency)
+	{
+		global $DB, $date;
+		$now = $date ?: date('Y-m-d H:i:s');
+
+		// 代理价(详情接口)
+		$agentCents = 0;
+		try {
+			$detail = $client->productDetail($upId, 'agent');
+			$prod = $detail['data']['product'] ?? ($detail['data'] ?? []);
+			$agentCents = self::toCents(self::pickPrice($prod));
+		} catch (CubeFinanceException $e) {
+			// 详情失败不致命,仅代理价缺失
+		}
+
+		// 各周期试算价
+		$cycles = [];
+		foreach (zjmf_cycles() as $cycle => $cfg) {
+			try {
+				$trial = $client->cartSetConfig(['pid' => $upId, 'billingcycle' => $cycle]);
+				$price = self::parseTrialPrice($trial);
+				if ($price > 0) {
+					$cycles[] = [
+						'cycle'             => $cycle,
+						'name'              => $cfg['name'],
+						'agent_price_cents' => $price,
+					];
+				}
+			} catch (CubeFinanceException $e) {
+				continue;
+			}
+		}
+		$cyclesJson = json_encode($cycles, JSON_UNESCAPED_UNICODE);
+
+		$existing = zjmf_product_get_by_up($supplierId, $upId);
+		if ($existing) {
+			$ok = $DB->query_prepare(
+				"UPDATE MN_plugin_zjmf_product
+				 SET name=?, description=?, currency=?, agent_price_cents=?,
+				     cycles=?, synced_at=?, updated_at=?
+				 WHERE id=?",
+				[
+					(string)($item['name'] ?? ''),
+					(string)($item['description'] ?? ''),
+					$currency,
+					$agentCents,
+					$cyclesJson,
+					$now,
+					$now,
+					(int)$existing['id'],
+				]
+			);
+		} else {
+			$ok = $DB->query_prepare(
+				"INSERT INTO MN_plugin_zjmf_product
+				 (supplier_id, up_product_id, name, description, currency,
+				  agent_price_cents, cycles, status, sort, synced_at,
+				  created_at, updated_at)
+				 VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
+				[
+					$supplierId,
+					$upId,
+					(string)($item['name'] ?? ''),
+					(string)($item['description'] ?? ''),
+					$currency,
+					$agentCents,
+					$cyclesJson,
+					0,
+					50,
+					$now,
+					$now,
+					$now,
+				]
+			);
+		}
+		if (!$ok) {
+			return false;
+		}
+		// 重算本地售价(保持已有加价配置不变)
+		$targetId = $existing ? (int)$existing['id'] : self::lastProductId($supplierId);
+		if ($targetId > 0) {
+			zjmf_product_recalc_price($targetId);
+		}
+		return true;
+	}
+
+	/** 取指定供应商内最近插入的商品 ID。 */
+	protected static function lastProductId($supplierId)
+	{
+		global $DB;
+		$row = $DB->get_row_prepare(
+			"SELECT id FROM MN_plugin_zjmf_product
+			 WHERE supplier_id=? ORDER BY id DESC LIMIT 1",
+			[(int)$supplierId]
+		);
+		return $row ? (int)$row['id'] : 0;
+	}
+
+	/* ============================================================
+	 *  开通(代理商直通)
+	 * ============================================================ */
+
+	/**
+	 * 上游开通主机(按订单所属供应商路由)。
+	 * 方案 A:cart/set_config 试算 → 下单 → apply_credit 余额支付 → 解析主机信息。
+	 * 端点与响应字段联调确认(Q1),调整仅需改动本方法。
+	 *
+	 * @param array $order    MN_plugin_zjmf_order 行
+	 * @param array $supplier MN_plugin_zjmf_supplier 行
+	 * @return array ['ok'=>bool, 'msg'=>string, 'up_order_id'=>int, 'up_host_id'=>int,
+	 *                'username'=>string, 'password'=>string, 'name'=>string,
+	 *                'renew_date'=>string]
+	 */
+	public static function purchase($order, $supplier)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+
+		$params = [
+			'pid'          => (int)($order['up_product_id'] ?? 0),
+			'billingcycle' => (string)($order['cycle'] ?? ''),
+		];
+		$extra = json_decode((string)($order['order_params'] ?? ''), true);
+		if (is_array($extra)) {
+			$params = array_merge($params, $extra);
+		}
+
+		try {
+			// 1. 下单
+			$res = $client->post(ZJMF_CHECKOUT_PATH, $params);
+			if (($res['status'] ?? 0) != 200) {
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '上游下单失败')];
+			}
+			$data = is_array($res['data'] ?? null) ? $res['data'] : [];
+
+			// 2. 若有上游订单,尝试余额抵扣支付(部分站点下单即开通,忽略失败)
+			$upOrderId = self::findId($data);
+			if ($upOrderId > 0) {
+				try {
+					$client->applyCredit(['order_id' => $upOrderId]);
+				} catch (CubeFinanceException $e) {
+					// 忽略:非致命
+				}
+			}
+
+			// 3. 解析主机信息
+			$hostId = self::findHostId($data);
+			if ($hostId > 0) {
+				$header = self::safeHostHeader($client, $hostId);
+				return [
+					'ok'          => true,
+					'msg'         => '开通成功',
+					'up_order_id' => $upOrderId,
+					'up_host_id'  => $hostId,
+					'username'    => $header['username'],
+					'password'    => $header['password'],
+					'name'        => $header['name'],
+					'renew_date'  => $header['renew_date'],
+				];
+			}
+
+			// 4. 未拿到 host_id:订单已生成,需人工核对(不判失败,避免误退款)
+			return [
+				'ok'          => true,
+				'msg'         => '上游订单已创建,但未返回主机 ID,请到上游后台核对',
+				'up_order_id' => $upOrderId,
+				'up_host_id'  => 0,
+				'username'    => '',
+				'password'    => '',
+				'name'        => (string)($order['product_name'] ?? ''),
+				'renew_date'  => '',
+			];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/* ============================================================
+	 *  主机查询与操作
+	 * ============================================================ */
+
+	/** 主机头信息(状态/账号/到期),按主机所属供应商。 */
+	public static function hostInfo($supplier, $upHostId)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$res = $client->hostHeader((int)$upHostId);
+			if (($res['status'] ?? 0) != 200) {
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '查询失败')];
+			}
+			$data = $res['data'] ?? [];
+			if (is_array($data) && isset($data['host']) && is_array($data['host'])) {
+				$data = $data['host'];
+			}
+			return ['ok' => true, 'data' => $data, 'status' => self::mapHostStatus($data)];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/** 流量使用,按主机所属供应商。 */
+	public static function hostTraffic($supplier, $upHostId)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$res = $client->hostTrafficUsage((int)$upHostId);
+			if (($res['status'] ?? 0) != 200) {
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '查询失败')];
+			}
+			return ['ok' => true, 'data' => $res['data'] ?? []];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/** 主机操作(provision/default,func 由调用方传入)。 */
+	public static function hostAction($supplier, $upHostId, $func, $extra = [])
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$params = array_merge(['id' => (int)$upHostId, 'func' => $func], $extra);
+			$res = $client->provisionDefault($params);
+			if (($res['status'] ?? 0) == 200) {
+				return ['ok' => true, 'msg' => (string)($res['msg'] ?? '操作成功')];
+			}
+			return ['ok' => false, 'msg' => (string)($res['msg'] ?? '操作失败')];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/* ============================================================
+	 *  升级(配置升级 / 产品升降级)
+	 * ============================================================ */
+
+	/** 只读获取升级选项(配置项或可升降级产品列表)。 */
+	public static function upgradeOptions($supplier, $upHostId, $kind)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$res = $kind === 'product'
+				? $client->upgradeProduct((int)$upHostId)
+				: $client->upgradeIndex((int)$upHostId);
+			if (($res['status'] ?? 0) != 200) {
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '获取升级选项失败')];
+			}
+			return ['ok' => true, 'data' => $res['data'] ?? []];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/**
+	 * 试算升级差额(GET 页面端点,只读不提交)。
+	 * 端点联调确认 Q1;selection 结构:
+	 *   config  → ['configoption' => [option_id => value]]
+	 *   product → ['newpid' => id, 'billingcycle' => cycle]
+	 *
+	 * @param array $supplier  供应商行
+	 * @param string $kind     config|product
+	 * @param int    $upHostId 上游主机 ID
+	 * @param array  $selection
+	 * @return array ['ok'=>bool, 'msg'=>string, 'price_cents'=>int, 'data'=>array]
+	 */
+	public static function upgradePreview($supplier, $kind, $upHostId, $selection)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$path = $kind === 'product'
+				? 'upgrade/upgrade_product_page'
+				: 'upgrade/upgrade_config_page';
+			$params = array_merge(
+				['hid' => (int)$upHostId, 'price_basis' => 'agent'],
+				$selection
+			);
+			$res = $client->get($path, $params);
+			if (($res['status'] ?? 0) != 200) {
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '试算失败')];
+			}
+			return [
+				'ok'          => true,
+				'msg'         => (string)($res['msg'] ?? ''),
+				'price_cents' => self::parseTrialPrice($res),
+				'data'        => is_array($res['data'] ?? null) ? $res['data'] : [],
+			];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/**
+	 * 提交升级(确认,POST 端点)。
+	 * 调用方需先扣款;提交失败由调用方负责退款。
+	 *
+	 * @param array $supplier  供应商行
+	 * @param string $kind     config|product
+	 * @param int    $upHostId 上游主机 ID
+	 * @param array  $selection
+	 * @return array ['ok'=>bool, 'msg'=>string, 'price_cents'=>int,
+	 *                'up_order_id'=>int, 'up_host_id'=>int]
+	 */
+	public static function upgradeSubmit($supplier, $kind, $upHostId, $selection)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$params = array_merge(['hid' => (int)$upHostId], $selection);
+			$res = $kind === 'product'
+				? $client->upgradeProductPost($params)
+				: $client->upgradeConfigPost($params);
+			if (($res['status'] ?? 0) != 200) {
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '升级提交失败')];
+			}
+			$data = is_array($res['data'] ?? null) ? $res['data'] : [];
+			return [
+				'ok'          => true,
+				'msg'         => (string)($res['msg'] ?? '提交成功'),
+				'price_cents' => self::parseTrialPrice($res),
+				'up_order_id' => self::findId($data),
+				'up_host_id'  => (int)$upHostId,
+			];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
+	/* ============================================================
+	 *  响应解析辅助(防御式,字段以实际上游返回为准)
+	 * ============================================================ */
+
+	/** 从响应 data 中提取金额(分),常见字段/嵌套结构兜底。 */
+	protected static function parseTrialPrice($res)
+	{
+		$data = $res['data'] ?? [];
+		if (!is_array($data)) {
+			return 0;
+		}
+		foreach (['price', 'money', 'total', 'amount', 'subtotal', 'renewal_price'] as $k) {
+			if (isset($data[$k]) && $data[$k] !== '' && $data[$k] !== null) {
+				$c = self::toCents($data[$k]);
+				if ($c > 0) {
+					return $c;
+				}
+			}
+		}
+		foreach (['cart', 'order', 'product', 'host'] as $k) {
+			if (isset($data[$k]) && is_array($data[$k])) {
+				$c = self::toCents(self::pickPrice($data[$k]));
+				if ($c > 0) {
+					return $c;
+				}
+			}
+		}
+		return 0;
+	}
+
+	/** 从数组取价格字段(元),取首个非空值。 */
+	protected static function pickPrice($arr)
+	{
+		if (!is_array($arr)) {
+			return 0;
+		}
+		foreach (['price', 'renew_price', 'setup_fee', 'total', 'amount'] as $k) {
+			if (isset($arr[$k]) && $arr[$k] !== '' && $arr[$k] !== null) {
+				return $arr[$k];
+			}
+		}
+		return 0;
+	}
+
+	/** 金额(元)→ 分。 */
+	protected static function toCents($val)
+	{
+		return (int)round((float)$val * 100);
+	}
+
+	/** 从 data 中找通用 ID(订单 ID)。 */
+	protected static function findId($arr)
+	{
+		if (!is_array($arr)) {
+			return 0;
+		}
+		foreach (['id', 'order_id', 'orderid'] as $k) {
+			if (isset($arr[$k])) {
+				return (int)$arr[$k];
+			}
+		}
+		return 0;
+	}
+
+	/** 从 data 中找主机 ID(支持嵌套 host)。 */
+	protected static function findHostId($arr)
+	{
+		if (!is_array($arr)) {
+			return 0;
+		}
+		foreach (['host_id', 'hostid', 'hid', 'id'] as $k) {
+			if (isset($arr[$k]) && (int)$arr[$k] > 0) {
+				return (int)$arr[$k];
+			}
+		}
+		if (isset($arr['host']) && is_array($arr['host'])) {
+			return self::findHostId($arr['host']);
+		}
+		return 0;
+	}
+
+	/** 查询主机头信息并安全提取字段(失败给空)。 */
+	protected static function safeHostHeader($client, $hostId)
+	{
+		try {
+			$res = $client->hostHeader((int)$hostId);
+			$data = $res['data'] ?? [];
+			if (is_array($data) && isset($data['host']) && is_array($data['host'])) {
+				$data = $data['host'];
+			}
+			return [
+				'username'   => (string)($data['username'] ?? ''),
+				'password'   => (string)($data['password'] ?? ''),
+				'name'       => (string)($data['name'] ?? ''),
+				'renew_date' => (string)($data['renew_date'] ?? $data['renewdate'] ?? ''),
+			];
+		} catch (CubeFinanceException $e) {
+			return ['username' => '', 'password' => '', 'name' => '', 'renew_date' => ''];
+		}
+	}
+
+	/** 上游主机状态 → 本地展示状态(active/suspend/unknown)。 */
+	public static function mapHostStatus($data)
+	{
+		if (!is_array($data)) {
+			return 'unknown';
+		}
+		$qk = $data['qk'] ?? null;
+		if ($qk !== null && in_array((string)$qk, ['false', '0', ''], true)) {
+			return 'suspend';
+		}
+		$st = (string)($data['status'] ?? $data['domainstatus'] ?? '');
+		$st = strtolower($st);
+		if (in_array($st, ['active', 'on', 'true', '运行中'], true)) {
+			return 'active';
+		}
+		if (in_array($st, ['suspended', 'suspend', 'paused', 'off'], true)) {
+			return 'suspend';
+		}
+		return 'unknown';
+	}
+}
diff --git a/app_plugins/zjmfmanager_reserve/lib/zjmf.php b/app_plugins/zjmfmanager_reserve/lib/zjmf.php
new file mode 100644
index 0000000..e4c89bc
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/lib/zjmf.php
@@ -0,0 +1,925 @@
+<?php
+/**
+ * zjmfmanager_reserve 插件 - 辅助函数库
+ *
+ * 提供:URL/渲染/认证/金额辅助、商品、订单、主机、日志的数据库操作,
+ * 以及主机开通编排(支付成功后调用上游开通,失败自动退款)。
+ */
+
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+
+/* ============================================================
+ *  常量
+ * ============================================================ */
+
+/** 业务类型标识(MN_dd.lx),用于 order.paid 钩子过滤 */
+define('ZJMF_LX', 'zjmf');
+
+/** 本地订单号前缀 */
+define('ZJMF_ORDER_PREFIX', 'ZJM');
+
+/* ============================================================
+ *  URL / 渲染辅助
+ * ============================================================ */
+
+/** 生成带站点 base path 前缀的 URL。 */
+function zjmf_url($path = '')
+{
+	$scriptName = isset($_SERVER['SCRIPT_NAME'])
+		? str_replace('\\', '/', $_SERVER['SCRIPT_NAME']) : '';
+	$basePath = rtrim(str_replace('\\', '/', dirname($scriptName)), '/');
+	if ($basePath === '.' || $basePath === '/') {
+		$basePath = '';
+	}
+	// 使用查询参数路由(index.php?_r=/path),避免依赖 Web 服务器 rewrite
+	$p = ltrim($path, '/');
+	$qpos = strpos($p, '?');
+	if ($qpos !== false) {
+		$route = substr($p, 0, $qpos);
+		$query = substr($p, $qpos + 1);
+		return $basePath . '/index.php?_r=/' . $route . '&' . $query;
+	}
+	return $basePath . '/index.php?_r=/' . $p;
+}
+
+/** 插件静态资源 URL。 */
+function zjmf_asset_url($path = '')
+{
+	return mnbt_plugin_url('zjmfmanager_reserve', 'assets/' . ltrim($path, '/'));
+}
+
+/** 管理员端插件页面 URL(admin/plugin.php?p=zjmfmanager_reserve&page=xxx)。 */
+function zjmf_admin_url($page, $extra = '')
+{
+	$base = 'plugin.php?p=zjmfmanager_reserve&page=' . rawurlencode($page);
+	if ($extra !== '') {
+		$base .= '&' . ltrim($extra, '&');
+	}
+	return $base;
+}
+
+/** 金额(分)→ 元(保留 2 位小数)。 */
+function zjmf_format_cents($cents)
+{
+	return number_format((int)$cents / 100, 2, '.', '');
+}
+
+/** 生成本地订单号。 */
+function zjmf_order_no()
+{
+	return ZJMF_ORDER_PREFIX . date('YmdHis') . mt_rand(1000, 9999);
+}
+
+/** 获取当前登录的 user_info 用户,未登录跳转登录页。 */
+function zjmf_require_user()
+{
+	if (!function_exists('user_info_auth_current')) {
+		http_response_code(500);
+		echo '需要先启用 user_info 插件';
+		exit;
+	}
+	$user = user_info_auth_current();
+	if (!$user) {
+		header('Location: ' . zjmf_url('account/login'));
+		exit;
+	}
+	return $user;
+}
+
+/** 渲染用户端视图。 */
+function zjmf_render($view, $vars = [])
+{
+	$vars['current_user'] = $vars['current_user']
+		?? (function_exists('user_info_auth_current') ? user_info_auth_current() : null);
+	extract($vars, EXTR_SKIP);
+	$viewFile = mnbt_plugin_path('zjmfmanager_reserve') . 'views/' . $view . '.php';
+	if (!is_file($viewFile)) {
+		http_response_code(500);
+		echo 'View not found: ' . htmlspecialchars($view);
+		return;
+	}
+	include $viewFile;
+}
+
+/** 渲染管理员端视图。 */
+function zjmf_render_admin($view, $vars = [])
+{
+	extract($vars, EXTR_SKIP);
+	$viewFile = mnbt_plugin_path('zjmfmanager_reserve') . 'views/admin/' . $view . '.php';
+	if (!is_file($viewFile)) {
+		http_response_code(500);
+		echo 'Admin view not found: ' . htmlspecialchars($view);
+		return;
+	}
+	include $viewFile;
+}
+
+/** 输出 JSON 并退出。 */
+function zjmf_json($code, $extra = [])
+{
+	@header('Content-Type: application/json; charset=UTF-8');
+	$payload = ['code' => $code];
+	if (is_array($extra)) {
+		$payload = array_merge($payload, $extra);
+	}
+	echo json_encode($payload, JSON_UNESCAPED_UNICODE);
+	exit;
+}
+
+/** 明文加密(authcode,用于上游主机密码入库)。 */
+function zjmf_encrypt($plain)
+{
+	return authcode((string)$plain, 'ENCODE', SYS_KEY);
+}
+
+/** 密文解密。 */
+function zjmf_decrypt($cipher)
+{
+	return authcode((string)$cipher, 'DECODE', SYS_KEY);
+}
+
+/** 账号脱敏展示(保留前 3 后 2)。 */
+function zjmf_mask_account($username)
+{
+	$s = (string)$username;
+	$len = strlen($s);
+	if ($len <= 5) {
+		return substr($s, 0, 1) . '***';
+	}
+	return substr($s, 0, 3) . '***' . substr($s, -2);
+}
+
+/** 写操作日志。 */
+function zjmf_log($user_id, $order_no, $action, $result, $content, $supplier_id = 0)
+{
+	global $DB, $date;
+	$now = $date ?: date('Y-m-d H:i:s');
+	$DB->query_prepare(
+		"INSERT INTO MN_plugin_zjmf_log
+		 (user_id, supplier_id, order_no, action, result, content, created_at)
+		 VALUES (?,?,?,?,?,?,?)",
+		[(int)$user_id, (int)$supplier_id, (string)$order_no, (string)$action,
+		 (string)$result, (string)$content, $now]
+	);
+}
+
+/** 日志列表(管理员,分页)。 */
+function zjmf_log_list_all($page = 1, $per_page = 30)
+{
+	global $DB;
+	$page = max(1, (int)$page);
+	$per_page = max(1, min(200, (int)$per_page));
+	$offset = ($page - 1) * $per_page;
+	$count_row = $DB->get_row_prepare("SELECT COUNT(*) AS cnt FROM MN_plugin_zjmf_log");
+	$total = $count_row ? (int)$count_row['cnt'] : 0;
+	$list = $DB->get_all_prepare(
+		"SELECT l.*, u.username AS user_name, s.name AS supplier_name
+		 FROM MN_plugin_zjmf_log l
+		 LEFT JOIN MN_plugin_user u ON u.id = l.user_id
+		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = l.supplier_id
+		 ORDER BY l.id DESC LIMIT {$offset},{$per_page}"
+	) ?: [];
+	return ['list' => $list, 'total' => $total, 'page' => $page, 'per_page' => $per_page];
+}
+
+/* ============================================================
+ *  计费周期
+ * ============================================================ */
+
+/** 可用计费周期(key 与上游 billingcycle 一致,WHMCS 风格)。 */
+function zjmf_cycles()
+{
+	return [
+		'Monthly'      => ['name' => '月付'],
+		'Quarterly'    => ['name' => '季付'],
+		'SemiAnnually' => ['name' => '半年付'],
+		'Annually'     => ['name' => '年付'],
+		'Biennially'   => ['name' => '两年付'],
+		'Triennially'  => ['name' => '三年付'],
+	];
+}
+
+/* ============================================================
+ *  供应商管理
+ * ============================================================ */
+
+/** 获取单个供应商(行数组)。 */
+function zjmf_supplier_get($supplier_id)
+{
+	global $DB;
+	return $DB->get_row_prepare(
+		"SELECT * FROM MN_plugin_zjmf_supplier WHERE id=? LIMIT 1",
+		[(int)$supplier_id]
+	) ?: null;
+}
+
+/** 全部供应商列表(管理员,按 sort 升序)。 */
+function zjmf_supplier_list_all()
+{
+	global $DB;
+	return $DB->get_all_prepare(
+		"SELECT * FROM MN_plugin_zjmf_supplier ORDER BY sort ASC, id ASC"
+	) ?: [];
+}
+
+/** 供应商加价标签(用于列表展示)。 */
+function zjmf_supplier_markup_label($supplier)
+{
+	$type = (int)($supplier['markup_type'] ?? 0);
+	$value = (int)($supplier['markup_value'] ?? 0);
+	return $type === 1
+		? '固定 +' . zjmf_format_cents($value) . ' 元'
+		: '比例 +' . ($value / 10) . '%';
+}
+
+/** 供应商是否可销售(存在且启用)。 */
+function zjmf_supplier_usable($supplier_id)
+{
+	$supplier = zjmf_supplier_get($supplier_id);
+	return $supplier && (int)$supplier['status'] === 1;
+}
+
+/**
+ * 删除供应商(有商品/订单/主机时拒绝)。
+ *
+ * @return array ['ok'=>bool, 'msg'=>string]
+ */
+function zjmf_supplier_delete($supplier_id)
+{
+	global $DB;
+	$supplier_id = (int)$supplier_id;
+	$tables = [
+		'MN_plugin_zjmf_product' => '商品',
+		'MN_plugin_zjmf_order'   => '订单',
+		'MN_plugin_zjmf_host'    => '主机',
+	];
+	foreach ($tables as $table => $label) {
+		$row = $DB->get_row_prepare(
+			"SELECT COUNT(*) AS cnt FROM {$table} WHERE supplier_id=? LIMIT 1",
+			[$supplier_id]
+		);
+		if ($row && (int)$row['cnt'] > 0) {
+			return ['ok' => false, 'msg' => '该供应商下存在' . $label . '数据,无法删除'];
+		}
+	}
+	$ok = $DB->query_prepare(
+		"DELETE FROM MN_plugin_zjmf_supplier WHERE id=?",
+		[$supplier_id]
+	);
+	return $ok ? ['ok' => true, 'msg' => '已删除'] : ['ok' => false, 'msg' => '删除失败'];
+}
+
+/* ============================================================
+ *  商品管理
+ * ============================================================ */
+
+/** 获取单个商品。 */
+function zjmf_product_get($product_id)
+{
+	global $DB;
+	return $DB->get_row_prepare(
+		"SELECT * FROM MN_plugin_zjmf_product WHERE id=? LIMIT 1",
+		[(int)$product_id]
+	) ?: null;
+}
+
+/** 按供应商 + 上游商品 ID 获取商品。 */
+function zjmf_product_get_by_up($supplier_id, $up_product_id)
+{
+	global $DB;
+	return $DB->get_row_prepare(
+		"SELECT * FROM MN_plugin_zjmf_product
+		 WHERE supplier_id=? AND up_product_id=? LIMIT 1",
+		[(int)$supplier_id, (int)$up_product_id]
+	) ?: null;
+}
+
+/** 上架商品列表(用户端,仅所属供应商启用时可见)。 */
+function zjmf_product_list_active()
+{
+	global $DB;
+	return $DB->get_all_prepare(
+		"SELECT p.*, s.name AS supplier_name
+		 FROM MN_plugin_zjmf_product p
+		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = p.supplier_id
+		 WHERE p.status=1 AND s.status=1
+		 ORDER BY s.sort ASC, p.sort ASC, p.id ASC"
+	) ?: [];
+}
+
+/** 全部商品列表(管理员,含供应商名)。 */
+function zjmf_product_list_all()
+{
+	global $DB;
+	return $DB->get_all_prepare(
+		"SELECT p.*, s.name AS supplier_name
+		 FROM MN_plugin_zjmf_product p
+		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = p.supplier_id
+		 ORDER BY s.sort ASC, p.sort ASC, p.id ASC"
+	) ?: [];
+}
+
+/** 解析商品周期 JSON,返回 ['cycle' => ['name'=>, 'price_cents'=>]]。 */
+function zjmf_product_cycles($product)
+{
+	$raw = isset($product['cycles']) ? json_decode($product['cycles'], true) : null;
+	if (!is_array($raw)) {
+		return [];
+	}
+	$map = [];
+	foreach ($raw as $item) {
+		$cycle = (string)($item['cycle'] ?? '');
+		if ($cycle === '') {
+			continue;
+		}
+		$map[$cycle] = [
+			'name'        => (string)($item['name'] ?? $cycle),
+			'price_cents' => (int)($item['price_cents'] ?? 0),
+		];
+	}
+	return $map;
+}
+
+/**
+ * 计算本地售价(分)。
+ *
+ * @param int $agentCents   上游代理价(分)
+ * @param int $markupType   0=比例 1=固定(分)
+ * @param int $markupValue  比例(千分比)或固定加价(分)
+ * @return int
+ */
+function zjmf_calc_price($agentCents, $markupType, $markupValue)
+{
+	$agentCents = max(0, (int)$agentCents);
+	if ($markupType === 1) {
+		return max(0, $agentCents + max(0, (int)$markupValue));
+	}
+	$rate = max(0, (int)$markupValue);
+	return max(0, (int)round($agentCents * (1000 + $rate) / 1000));
+}
+
+/**
+ * 重算商品各周期本地售价并写回 cycles 字段。
+ * 加价规则:单品有配置则用单品,否则用所属供应商配置。
+ *
+ * @param int $product_id
+ * @return void
+ */
+function zjmf_product_recalc_price($product_id)
+{
+	global $DB, $date;
+	$product = zjmf_product_get($product_id);
+	if (!$product) {
+		return;
+	}
+	$supplier = zjmf_supplier_get((int)$product['supplier_id']);
+	// 单品已配置加价(比例 type=0 或固定 type=1 且 value>0)时用单品规则,否则用供应商
+	$hasOwn = (int)($product['markup_type'] ?? 0) !== 0
+		|| (int)($product['markup_value'] ?? 0) > 0;
+	$markupType = $hasOwn
+		? (int)$product['markup_type'] : (int)($supplier['markup_type'] ?? 0);
+	$markupValue = $hasOwn
+		? (int)$product['markup_value'] : (int)($supplier['markup_value'] ?? 0);
+
+	$cycles = zjmf_product_cycles($product);
+	if ($cycles === []) {
+		return;
+	}
+	foreach ($cycles as $cycle => &$cfg) {
+		// agent_price 保存的是该周期对应的上游价(分)
+		$cfg['price_cents'] = zjmf_calc_price(
+			$cfg['agent_price_cents'] ?? 0,
+			$markupType,
+			$markupValue
+		);
+	}
+	unset($cfg);
+	$now = $date ?: date('Y-m-d H:i:s');
+	$DB->query_prepare(
+		"UPDATE MN_plugin_zjmf_product SET cycles=?, updated_at=? WHERE id=?",
+		[json_encode(array_values($cycles), JSON_UNESCAPED_UNICODE), $now, (int)$product_id]
+	);
+}
+
+/* ============================================================
+ *  订单管理
+ * ============================================================ */
+
+/** 按 ID 查询订单。 */
+function zjmf_order_get($order_id)
+{
+	global $DB;
+	return $DB->get_row_prepare(
+		"SELECT * FROM MN_plugin_zjmf_order WHERE id=? LIMIT 1",
+		[(int)$order_id]
+	) ?: null;
+}
+
+/** 按订单号查询订单。 */
+function zjmf_order_get_by_no($order_no)
+{
+	global $DB;
+	return $DB->get_row_prepare(
+		"SELECT * FROM MN_plugin_zjmf_order WHERE order_no=? LIMIT 1",
+		[$order_no]
+	) ?: null;
+}
+
+/** 用户订单列表(分页)。 */
+function zjmf_order_list_by_user($user_id, $page = 1, $per_page = 20)
+{
+	global $DB;
+	$user_id = (int)$user_id;
+	$page = max(1, (int)$page);
+	$per_page = max(1, min(100, (int)$per_page));
+	$offset = ($page - 1) * $per_page;
+	$count_row = $DB->get_row_prepare(
+		"SELECT COUNT(*) AS cnt FROM MN_plugin_zjmf_order WHERE user_id=?",
+		[$user_id]
+	);
+	$total = $count_row ? (int)$count_row['cnt'] : 0;
+	$list = $DB->get_all_prepare(
+		"SELECT o.*, s.name AS supplier_name
+		 FROM MN_plugin_zjmf_order o
+		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = o.supplier_id
+		 WHERE o.user_id=? ORDER BY o.id DESC LIMIT {$offset},{$per_page}",
+		[$user_id]
+	) ?: [];
+	return ['list' => $list, 'total' => $total, 'page' => $page, 'per_page' => $per_page];
+}
+
+/** 全部订单列表(管理员,分页 + 简单筛选)。 */
+function zjmf_order_list_all($page = 1, $per_page = 30, $filters = [])
+{
+	global $DB;
+	$page = max(1, (int)$page);
+	$per_page = max(1, min(200, (int)$per_page));
+	$offset = ($page - 1) * $per_page;
+
+	$where = '1';
+	$params = [];
+	if (!empty($filters['status'])) {
+		$where .= ' AND status=?';
+		$params[] = $filters['status'];
+	}
+	if (!empty($filters['order_no'])) {
+		$where .= ' AND order_no LIKE ?';
+		$params[] = '%' . $filters['order_no'] . '%';
+	}
+	if (!empty($filters['user_id'])) {
+		$where .= ' AND user_id=?';
+		$params[] = (int)$filters['user_id'];
+	}
+	if (!empty($filters['supplier_id'])) {
+		$where .= ' AND supplier_id=?';
+		$params[] = (int)$filters['supplier_id'];
+	}
+
+	$count_row = $DB->get_row_prepare(
+		"SELECT COUNT(*) AS cnt FROM MN_plugin_zjmf_order WHERE {$where}",
+		$params
+	);
+	$total = $count_row ? (int)$count_row['cnt'] : 0;
+	$list = $DB->get_all_prepare(
+		"SELECT o.*, s.name AS supplier_name
+		 FROM MN_plugin_zjmf_order o
+		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = o.supplier_id
+		 WHERE {$where} ORDER BY o.id DESC LIMIT {$offset},{$per_page}",
+		$params
+	) ?: [];
+	return ['list' => $list, 'total' => $total, 'page' => $page, 'per_page' => $per_page];
+}
+
+/**
+ * 创建本地订单(未支付)。
+ *
+ * @param array  $user       user_info 当前用户
+ * @param array  $product    本地商品行
+ * @param string $cycle      计费周期
+ * @param array  $cycleCfg   周期配置(name/price_cents)
+ * @param string $action     buy/upgrade_config/upgrade_product
+ * @param array  $extra      附加字段(up_host_id/host_id/order_params/cost_cents)
+ * @return array ['ok'=>bool, 'order_no'=>string, 'order_id'=>int, 'msg'=>string]
+ */
+function zjmf_order_create($user, $product, $cycle, $cycleCfg, $action = 'buy', $extra = [])
+{
+	global $DB, $date;
+	$now = $date ?: date('Y-m-d H:i:s');
+	$order_no = zjmf_order_no();
+	$cycleName = (string)($cycleCfg['name'] ?? $cycle);
+
+	$ok = $DB->query_prepare(
+		"INSERT INTO MN_plugin_zjmf_order
+		 (order_no, action, supplier_id, user_id, product_id, up_product_id,
+		  product_name, cycle, cycle_name, amount_cents, cost_cents, order_params,
+		  up_order_id, up_host_id, host_id, username, status,
+		  pay_time, opened_at, remark, created_at)
+		 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
+		[
+			$order_no,
+			$action,
+			(int)($product['supplier_id'] ?? 0),
+			(int)$user['id'],
+			(int)($product['id'] ?? 0),
+			(int)($product['up_product_id'] ?? 0),
+			(string)($product['name'] ?? ''),
+			$cycle,
+			$cycleName,
+			(int)($cycleCfg['price_cents'] ?? 0),
+			(int)($extra['cost_cents'] ?? 0),
+			(string)($extra['order_params'] ?? ''),
+			(int)($extra['up_order_id'] ?? 0),
+			(int)($extra['up_host_id'] ?? 0),
+			(int)($extra['host_id'] ?? 0),
+			(string)($extra['username'] ?? ''),
+			'pending',
+			'',
+			'',
+			'',
+			$now,
+		]
+	);
+	if (!$ok) {
+		return ['ok' => false, 'order_no' => '', 'order_id' => 0, 'msg' => '订单写入失败'];
+	}
+	$row = $DB->get_row_prepare(
+		"SELECT id FROM MN_plugin_zjmf_order WHERE order_no=? LIMIT 1",
+		[$order_no]
+	);
+	return [
+		'ok'       => true,
+		'order_no' => $order_no,
+		'order_id' => $row ? (int)$row['id'] : 0,
+		'msg'      => '',
+	];
+}
+
+/** 更新订单状态。 */
+function zjmf_order_set_status($order_id, $status, $remark = '')
+{
+	global $DB, $date;
+	$now = $date ?: date('Y-m-d H:i:s');
+	$extra = '';
+	$params = [$status];
+	if ($status === 'paid') {
+		$extra = ', pay_time=?';
+		$params[] = $now;
+	} elseif ($status === 'opened') {
+		$extra = ', opened_at=?';
+		$params[] = $now;
+	}
+	if ($remark !== '') {
+		$extra .= ', remark=?';
+		$params[] = $remark;
+	}
+	$params[] = (int)$order_id;
+	return (bool)$DB->query_prepare(
+		"UPDATE MN_plugin_zjmf_order SET status=?{$extra} WHERE id=?",
+		$params
+	);
+}
+
+/** 回填订单开通信息。 */
+function zjmf_order_fill_opened($order_id, $upOrderId, $upHostId, $username)
+{
+	global $DB;
+	return (bool)$DB->query_prepare(
+		"UPDATE MN_plugin_zjmf_order
+		 SET up_order_id=?, up_host_id=?, username=?
+		 WHERE id=?",
+		[(int)$upOrderId, (int)$upHostId, (string)$username, (int)$order_id]
+	);
+}
+
+/**
+ * 创建升级订单(扣款前)。
+ *
+ * @param array  $user         user_info 当前用户
+ * @param array  $host         本地主机映射行
+ * @param string $action       upgrade_config / upgrade_product
+ * @param int    $amountCents  升级差额(分)
+ * @param string $orderParams  升级参数 JSON(selection)
+ * @return array ['ok'=>bool, 'order_no'=>string, 'order_id'=>int, 'msg'=>string]
+ */
+function zjmf_upgrade_order_create($user, $host, $action, $amountCents, $orderParams)
+{
+	global $DB, $date;
+	$now = $date ?: date('Y-m-d H:i:s');
+	$order_no = zjmf_order_no();
+
+	$ok = $DB->query_prepare(
+		"INSERT INTO MN_plugin_zjmf_order
+		 (order_no, action, supplier_id, user_id, product_id, up_product_id,
+		  product_name, cycle, cycle_name, amount_cents, cost_cents, order_params,
+		  up_order_id, up_host_id, host_id, username, status,
+		  pay_time, opened_at, remark, created_at)
+		 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
+		[
+			$order_no,
+			$action,
+			(int)($host['supplier_id'] ?? 0),
+			(int)$user['id'],
+			0,
+			(int)$host['up_product_id'],
+			'升级:' . $host['name'],
+			(string)($host['cycle'] ?? ''),
+			(string)($host['cycle'] ?? ''),
+			(int)$amountCents,
+			0,
+			(string)$orderParams,
+			0,
+			(int)$host['up_host_id'],
+			(int)$host['id'],
+			'',
+			'pending',
+			'',
+			'',
+			'',
+			$now,
+		]
+	);
+	if (!$ok) {
+		return ['ok' => false, 'order_no' => '', 'order_id' => 0, 'msg' => '订单写入失败'];
+	}
+	$row = $DB->get_row_prepare(
+		"SELECT id FROM MN_plugin_zjmf_order WHERE order_no=? LIMIT 1",
+		[$order_no]
+	);
+	return [
+		'ok'       => true,
+		'order_no' => $order_no,
+		'order_id' => $row ? (int)$row['id'] : 0,
+		'msg'      => '',
+	];
+}
+
+/* ============================================================
+ *  主机映射管理
+ * ============================================================ */
+
+/** 按 ID 查询主机映射。 */
+function zjmf_host_get($host_id)
+{
+	global $DB;
+	return $DB->get_row_prepare(
+		"SELECT * FROM MN_plugin_zjmf_host WHERE id=? LIMIT 1",
+		[(int)$host_id]
+	) ?: null;
+}
+
+/** 校验主机归属当前用户后返回。 */
+function zjmf_host_get_by_user($user_id, $host_id)
+{
+	global $DB;
+	return $DB->get_row_prepare(
+		"SELECT h.*, s.name AS supplier_name
+		 FROM MN_plugin_zjmf_host h
+		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = h.supplier_id
+		 WHERE h.id=? AND h.user_id=? LIMIT 1",
+		[(int)$host_id, (int)$user_id]
+	) ?: null;
+}
+
+/** 按上游主机 ID 查询映射。 */
+function zjmf_host_get_by_up($up_host_id)
+{
+	global $DB;
+	return $DB->get_row_prepare(
+		"SELECT * FROM MN_plugin_zjmf_host WHERE up_host_id=? LIMIT 1",
+		[(int)$up_host_id]
+	) ?: null;
+}
+
+/** 用户主机列表。 */
+function zjmf_host_list_by_user($user_id)
+{
+	global $DB;
+	return $DB->get_all_prepare(
+		"SELECT h.*, s.name AS supplier_name
+		 FROM MN_plugin_zjmf_host h
+		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = h.supplier_id
+		 WHERE h.user_id=? ORDER BY h.id DESC",
+		[(int)$user_id]
+	) ?: [];
+}
+
+/** 全部主机列表(管理员,分页,含供应商名)。 */
+function zjmf_host_list_all($page = 1, $per_page = 30)
+{
+	global $DB;
+	$page = max(1, (int)$page);
+	$per_page = max(1, min(200, (int)$per_page));
+	$offset = ($page - 1) * $per_page;
+	$count_row = $DB->get_row_prepare("SELECT COUNT(*) AS cnt FROM MN_plugin_zjmf_host");
+	$total = $count_row ? (int)$count_row['cnt'] : 0;
+	$list = $DB->get_all_prepare(
+		"SELECT h.*, u.username AS user_name, s.name AS supplier_name
+		 FROM MN_plugin_zjmf_host h
+		 LEFT JOIN MN_plugin_user u ON u.id = h.user_id
+		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = h.supplier_id
+		 ORDER BY h.id DESC LIMIT {$offset},{$per_page}"
+	) ?: [];
+	return ['list' => $list, 'total' => $total, 'page' => $page, 'per_page' => $per_page];
+}
+
+/** 新增主机映射。 */
+function zjmf_host_create($data)
+{
+	global $DB, $date;
+	$now = $date ?: date('Y-m-d H:i:s');
+	$ok = $DB->query_prepare(
+		"INSERT INTO MN_plugin_zjmf_host
+		 (supplier_id, user_id, order_id, up_host_id, up_product_id, name, username,
+		  password, cycle, status, renew_date, created_at, updated_at)
+		 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
+		[
+			(int)($data['supplier_id'] ?? 0),
+			(int)($data['user_id'] ?? 0),
+			(int)($data['order_id'] ?? 0),
+			(int)($data['up_host_id'] ?? 0),
+			(int)($data['up_product_id'] ?? 0),
+			(string)($data['name'] ?? ''),
+			(string)($data['username'] ?? ''),
+			(string)($data['password'] ?? ''),
+			(string)($data['cycle'] ?? ''),
+			(string)($data['status'] ?? 'active'),
+			(string)($data['renew_date'] ?? ''),
+			$now,
+			$now,
+		]
+	);
+	if (!$ok) {
+		return 0;
+	}
+	$row = $DB->get_row_prepare(
+		"SELECT id FROM MN_plugin_zjmf_host
+		 WHERE order_id=? ORDER BY id DESC LIMIT 1",
+		[(int)($data['order_id'] ?? 0)]
+	);
+	return $row ? (int)$row['id'] : 0;
+}
+
+/** 更新主机缓存信息(状态/到期/周期)。 */
+function zjmf_host_update_cache($host_id, $data)
+{
+	global $DB, $date;
+	$sets = [];
+	$params = [];
+	foreach (['status', 'renew_date', 'cycle', 'up_product_id'] as $key) {
+		if (array_key_exists($key, $data)) {
+			$sets[] = "`{$key}`=?";
+			$params[] = (string)$data[$key];
+		}
+	}
+	if ($sets === []) {
+		return false;
+	}
+	$now = $date ?: date('Y-m-d H:i:s');
+	$sets[] = "updated_at=?";
+	$params[] = $now;
+	$params[] = (int)$host_id;
+	return (bool)$DB->query_prepare(
+		"UPDATE MN_plugin_zjmf_host SET " . implode(',', $sets) . " WHERE id=?",
+		$params
+	);
+}
+
+/* ============================================================
+ *  主机操作辅助
+ * ============================================================ */
+
+/** 操作标识 → 上游 func 名称(视上游模块而定,联调时按实际调整)。 */
+function zjmf_action_func($action)
+{
+	$map = [
+		'on'             => 'on',
+		'off'            => 'off',
+		'reboot'         => 'reboot',
+		'reset_password' => 'passwd',
+		'reinstall'      => 'reinstall',
+	];
+	return $map[$action] ?? '';
+}
+
+/** 操作成功后建议写入的缓存状态(空表示不修改)。 */
+function zjmf_action_status($action)
+{
+	$map = [
+		'on'     => 'active',
+		'off'    => 'suspend',
+		'reboot' => 'active',
+	];
+	return $map[$action] ?? '';
+}
+
+/** 用户端主机状态展示标签映射。 */
+function zjmf_host_status_label($status)
+{
+	$map = [
+		'active'  => '运行中',
+		'suspend' => '已暂停',
+		'unknown' => '未知',
+	];
+	return $map[$status] ?? $status;
+}
+
+/* ============================================================
+ *  主机开通(核心编排)
+ * ============================================================ */
+
+/**
+ * 支付成功后开通主机:调用上游开通,落库映射,失败自动退款。
+ *
+ * @param int $order_id  MN_plugin_zjmf_order.id
+ * @return array ['ok'=>bool, 'msg'=>string, 'host_id'=>int]
+ */
+function zjmf_open_host($order_id)
+{
+	global $DB, $date;
+	$order = zjmf_order_get($order_id);
+	if (!$order) {
+		return ['ok' => false, 'msg' => '订单不存在'];
+	}
+	if ($order['status'] !== 'paid') {
+		return ['ok' => false, 'msg' => '订单状态非已支付,无法开通'];
+	}
+	// 幂等:已开通或已有映射跳过
+	$existing = $DB->get_row_prepare(
+		"SELECT id FROM MN_plugin_zjmf_host WHERE order_id=? LIMIT 1",
+		[(int)$order_id]
+	);
+	if ($existing) {
+		return ['ok' => true, 'msg' => '该订单已开通', 'host_id' => (int)$existing['id']];
+	}
+
+	// 供应商校验:缺失或停用时直接失败退款
+	$supplier = zjmf_supplier_get((int)$order['supplier_id']);
+	if (!$supplier || (int)$supplier['status'] !== 1) {
+		$msg = '供应商不存在或已停用,无法开通';
+		zjmf_order_set_status($order_id, 'failed', $msg);
+		zjmf_log((int)$order['user_id'], $order['order_no'], 'purchase',
+			'failed', json_encode(['msg' => $msg], JSON_UNESCAPED_UNICODE),
+			(int)$order['supplier_id']);
+		$amount = (int)$order['amount_cents'];
+		if ($amount > 0 && function_exists('balance_add')) {
+			balance_add((int)$order['user_id'], $amount, 'refund',
+				$order['order_no'], '开通失败自动退款');
+		}
+		return ['ok' => false, 'msg' => $msg];
+	}
+
+	// 调用上游开通(代理商直通,按订单供应商路由)
+	$result = ZjmfUpstream::purchase($order, $supplier);
+	if (empty($result['ok'])) {
+		$msg = (string)($result['msg'] ?? '上游开通失败');
+		zjmf_order_set_status($order_id, 'failed', $msg);
+		zjmf_log((int)$order['user_id'], $order['order_no'],
+			'purchase', 'failed', json_encode(['msg' => $msg], JSON_UNESCAPED_UNICODE),
+			(int)$order['supplier_id']);
+		// 自动原路退回余额
+		$amount = (int)$order['amount_cents'];
+		if ($amount > 0 && function_exists('balance_add')) {
+			balance_add((int)$order['user_id'], $amount, 'refund',
+				$order['order_no'], '开通失败自动退款');
+		}
+		return ['ok' => false, 'msg' => $msg];
+	}
+
+	$now = $date ?: date('Y-m-d H:i:s');
+	$upHostId = (int)($result['up_host_id'] ?? 0);
+	$username = (string)($result['username'] ?? '');
+	$password = (string)($result['password'] ?? '');
+	$upOrderId = (int)($result['up_order_id'] ?? 0);
+
+	// 回填订单
+	zjmf_order_fill_opened($order_id, $upOrderId, $upHostId, $username);
+	zjmf_order_set_status($order_id, 'opened', '主机已开通');
+
+	// 写主机映射
+	$hostId = zjmf_host_create([
+		'supplier_id'    => (int)$order['supplier_id'],
+		'user_id'        => (int)$order['user_id'],
+		'order_id'       => (int)$order_id,
+		'up_host_id'     => $upHostId,
+		'up_product_id'  => (int)$order['up_product_id'],
+		'name'           => (string)($result['name'] ?? $order['product_name']),
+		'username'       => $username,
+		'password'       => $password !== '' ? zjmf_encrypt($password) : '',
+		'cycle'          => $order['cycle'],
+		'status'         => 'active',
+		'renew_date'     => (string)($result['renew_date'] ?? ''),
+	]);
+
+	zjmf_log((int)$order['user_id'], $order['order_no'],
+		'purchase', 'success',
+		json_encode([
+			'up_order_id' => $upOrderId,
+			'up_host_id'  => $upHostId,
+			'username'    => $username,
+		], JSON_UNESCAPED_UNICODE),
+		(int)$order['supplier_id']);
+
+	return ['ok' => true, 'msg' => '开通成功', 'host_id' => $hostId];
+}
diff --git a/app_plugins/zjmfmanager_reserve/plugin.json b/app_plugins/zjmfmanager_reserve/plugin.json
new file mode 100644
index 0000000..3a237b9
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/plugin.json
@@ -0,0 +1,10 @@
+{
+  "id": "zjmfmanager_reserve",
+  "name": "魔方财务代理分销",
+  "version": "1.0.0",
+  "author": "启明智联",
+  "description": "以代理商身份分销魔方财务产品:商品同步加价、本地余额购买、代理商直通开通、主机管理与升降级。依赖 user_info 与 balance 插件。",
+  "requires_mnbt": "1.81",
+  "requires_plugins": ["user_info", "balance"],
+  "type": ["business"]
+}
diff --git a/app_plugins/zjmfmanager_reserve/uninstall.sql b/app_plugins/zjmfmanager_reserve/uninstall.sql
new file mode 100644
index 0000000..7b44122
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/uninstall.sql
@@ -0,0 +1,6 @@
+-- 魔方财务代理分销插件 - 卸载清理
+DROP TABLE IF EXISTS `MN_plugin_zjmf_supplier`;
+DROP TABLE IF EXISTS `MN_plugin_zjmf_product`;
+DROP TABLE IF EXISTS `MN_plugin_zjmf_order`;
+DROP TABLE IF EXISTS `MN_plugin_zjmf_host`;
+DROP TABLE IF EXISTS `MN_plugin_zjmf_log`;
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/hosts.php b/app_plugins/zjmfmanager_reserve/views/admin/hosts.php
new file mode 100644
index 0000000..69a1c77
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/admin/hosts.php
@@ -0,0 +1,127 @@
+<?php
+/**
+ * 管理员端 - 主机管理
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+mnbt_admin_include('head');
+
+$page = max(1, (int)($_GET['page_num'] ?? 1));
+$hosts = zjmf_host_list_all($page, 30);
+$status_labels = [
+	'active'  => '运行中',
+	'suspend' => '已暂停',
+	'unknown' => '未知',
+];
+$status_classes = [
+	'active'  => 'badge-success',
+	'suspend' => 'badge-danger',
+	'unknown' => 'badge-secondary',
+];
+$title = $title ?? '主机管理';
+?>
+<div class="container-fluid p-t-15">
+  <div class="card">
+    <div class="card-header"><h4 style="display:inline-block">主机管理</h4></div>
+    <div class="card-body">
+      <p class="text-muted">主机映射为开通成功的上游主机。可点击「刷新状态」同步上游实时状态。</p>
+      <div class="table-responsive">
+        <table class="table table-bordered table-hover">
+          <thead>
+            <tr>
+              <th>ID</th>
+              <th>供应商</th>
+              <th>用户</th>
+              <th>主机名</th>
+              <th>上游主机ID</th>
+              <th>状态</th>
+              <th>周期</th>
+              <th>到期时间</th>
+              <th>创建时间</th>
+              <th>操作</th>
+            </tr>
+          </thead>
+          <tbody>
+            <?php if (empty($hosts['list'])): ?>
+              <tr><td colspan="10" class="text-center text-muted">暂无主机</td></tr>
+            <?php else: ?>
+              <?php foreach ($hosts['list'] as $h): ?>
+                <tr>
+                  <td><?= (int)$h['id'] ?></td>
+                  <td><?= htmlspecialchars($h['supplier_name'] ?: '-') ?></td>
+                  <td><?= htmlspecialchars($h['user_name'] ?: ('ID ' . (int)$h['user_id'])) ?></td>
+                  <td><?= htmlspecialchars($h['name'], ENT_QUOTES) ?></td>
+                  <td><?= (int)$h['up_host_id'] ?></td>
+                  <td>
+                    <span class="badge <?= htmlspecialchars($status_classes[$h['status']] ?? 'badge-secondary') ?>">
+                      <?= htmlspecialchars($status_labels[$h['status']] ?? $h['status']) ?>
+                    </span>
+                  </td>
+                  <td><?= htmlspecialchars($h['cycle'] ?: '-') ?></td>
+                  <td><?= htmlspecialchars($h['renew_date'] ?: '-') ?></td>
+                  <td class="small"><?= htmlspecialchars($h['created_at']) ?></td>
+                  <td>
+                    <?php if ((int)$h['up_host_id'] > 0): ?>
+                      <button type="button" class="btn btn-sm btn-outline-primary zjf-refresh"
+                              data-id="<?= (int)$h['id'] ?>">刷新状态</button>
+                    <?php else: ?>
+                      <span class="text-muted">-</span>
+                    <?php endif; ?>
+                  </td>
+                </tr>
+              <?php endforeach; ?>
+            <?php endif; ?>
+          </tbody>
+        </table>
+      </div>
+
+      <?php
+      $total_pages = max(1, (int)ceil($hosts['total'] / $hosts['per_page']));
+      $current_page = (int)$hosts['page'];
+      if ($total_pages > 1):
+        $qs = http_build_query(['p' => 'zjmfmanager_reserve', 'page' => 'hosts']);
+      ?>
+        <nav>
+          <ul class="pagination pagination-sm">
+            <?php if ($current_page > 1): ?>
+              <li class="page-item"><a class="page-link"
+                href="plugin.php?<?= htmlspecialchars(
+                  $qs . '&page_num=' . ($current_page - 1), ENT_QUOTES
+                ) ?>">上一页</a></li>
+            <?php endif; ?>
+            <li class="page-item disabled"><span class="page-link">
+              第 <?= $current_page ?> / <?= $total_pages ?> 页(共 <?= (int)$hosts['total'] ?> 条)
+            </span></li>
+            <?php if ($current_page < $total_pages): ?>
+              <li class="page-item"><a class="page-link"
+                href="plugin.php?<?= htmlspecialchars(
+                  $qs . '&page_num=' . ($current_page + 1), ENT_QUOTES
+                ) ?>">下一页</a></li>
+            <?php endif; ?>
+          </ul>
+        </nav>
+      <?php endif; ?>
+    </div>
+  </div>
+</div>
+<script>
+document.querySelectorAll('.zjf-refresh').forEach(function (btn) {
+  btn.addEventListener('click', function () {
+    var self = this;
+    self.disabled = true;
+    self.textContent = '刷新中...';
+    $.post('ajax.php', {gn: 'p_zjmf_admin_fetch_host', id: self.getAttribute('data-id')}, function (res) {
+      var d;
+      try { d = typeof res === 'string' ? JSON.parse(res) : res; } catch (e) { d = {code: res}; }
+      var ok = d.qk == 1 || d.success;
+      if (typeof $.notify === 'function') {
+        $.notify({message: d.msg || d.code || '完成'}, {type: ok ? 'success' : 'danger'});
+      } else {
+        alert(d.msg || d.code || '完成');
+      }
+      setTimeout(function () { location.reload(); }, 600);
+    });
+  });
+});
+</script>
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/logs.php b/app_plugins/zjmfmanager_reserve/views/admin/logs.php
new file mode 100644
index 0000000..572b5c9
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/admin/logs.php
@@ -0,0 +1,94 @@
+<?php
+/**
+ * 管理员端 - 操作日志
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+mnbt_admin_include('head');
+
+$page = max(1, (int)($_GET['page_num'] ?? 1));
+$logs = zjmf_log_list_all($page, 30);
+$result_labels = [
+	'success' => '成功',
+	'failed'  => '失败',
+];
+$result_classes = [
+	'success' => 'badge-success',
+	'failed'  => 'badge-danger',
+];
+$title = $title ?? '操作日志';
+?>
+<div class="container-fluid p-t-15">
+  <div class="card">
+    <div class="card-header"><h4 style="display:inline-block">操作日志</h4></div>
+    <div class="card-body">
+      <p class="text-muted">记录商品同步、开通、主机操作、升级等关键动作(内容已脱敏,不含密码/密钥)。</p>
+      <div class="table-responsive">
+        <table class="table table-bordered table-hover">
+          <thead>
+            <tr>
+              <th>ID</th>
+              <th>供应商</th>
+              <th>用户</th>
+              <th>订单号</th>
+              <th>操作</th>
+              <th>结果</th>
+              <th>内容</th>
+              <th>时间</th>
+            </tr>
+          </thead>
+          <tbody>
+            <?php if (empty($logs['list'])): ?>
+              <tr><td colspan="8" class="text-center text-muted">暂无日志</td></tr>
+            <?php else: ?>
+              <?php foreach ($logs['list'] as $l): ?>
+                <tr>
+                  <td><?= (int)$l['id'] ?></td>
+                  <td><?= htmlspecialchars($l['supplier_name'] ?: '-') ?></td>
+                  <td><?= htmlspecialchars($l['user_name'] ?: ('ID ' . (int)$l['user_id'])) ?></td>
+                  <td class="small text-muted"><?= htmlspecialchars($l['order_no'] ?: '-') ?></td>
+                  <td><?= htmlspecialchars($l['action']) ?></td>
+                  <td>
+                    <span class="badge <?= htmlspecialchars($result_classes[$l['result']] ?? 'badge-secondary') ?>">
+                      <?= htmlspecialchars($result_labels[$l['result']] ?? $l['result']) ?>
+                    </span>
+                  </td>
+                  <td class="small"><?= htmlspecialchars($l['content'] ?: '-') ?></td>
+                  <td class="small"><?= htmlspecialchars($l['created_at']) ?></td>
+                </tr>
+              <?php endforeach; ?>
+            <?php endif; ?>
+          </tbody>
+        </table>
+      </div>
+
+      <?php
+      $total_pages = max(1, (int)ceil($logs['total'] / $logs['per_page']));
+      $current_page = (int)$logs['page'];
+      if ($total_pages > 1):
+        $qs = http_build_query(['p' => 'zjmfmanager_reserve', 'page' => 'logs']);
+      ?>
+        <nav>
+          <ul class="pagination pagination-sm">
+            <?php if ($current_page > 1): ?>
+              <li class="page-item"><a class="page-link"
+                href="plugin.php?<?= htmlspecialchars(
+                  $qs . '&page_num=' . ($current_page - 1), ENT_QUOTES
+                ) ?>">上一页</a></li>
+            <?php endif; ?>
+            <li class="page-item disabled"><span class="page-link">
+              第 <?= $current_page ?> / <?= $total_pages ?> 页(共 <?= (int)$logs['total'] ?> 条)
+            </span></li>
+            <?php if ($current_page < $total_pages): ?>
+              <li class="page-item"><a class="page-link"
+                href="plugin.php?<?= htmlspecialchars(
+                  $qs . '&page_num=' . ($current_page + 1), ENT_QUOTES
+                ) ?>">下一页</a></li>
+            <?php endif; ?>
+          </ul>
+        </nav>
+      <?php endif; ?>
+    </div>
+  </div>
+</div>
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/orders.php b/app_plugins/zjmfmanager_reserve/views/admin/orders.php
new file mode 100644
index 0000000..8b4b2aa
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/admin/orders.php
@@ -0,0 +1,154 @@
+<?php
+/**
+ * 管理员端 - 订单管理
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+mnbt_admin_include('head');
+
+$page = max(1, (int)($_GET['page_num'] ?? 1));
+$filters = [
+	'status'      => $_GET['status'] ?? '',
+	'order_no'    => $_GET['order_no'] ?? '',
+	'user_id'     => $_GET['user_id'] ?? '',
+	'supplier_id' => $_GET['supplier_id'] ?? '',
+];
+$orders = zjmf_order_list_all($page, 30, $filters);
+$suppliers = zjmf_supplier_list_all();
+
+$action_labels = [
+	'buy'              => '购买',
+	'upgrade_config'   => '配置升级',
+	'upgrade_product'  => '产品升级',
+];
+$status_labels = [
+	'pending'   => '待处理',
+	'paid'      => '已支付',
+	'opened'    => '已完成',
+	'failed'    => '失败',
+	'cancelled' => '已取消',
+];
+$status_classes = [
+	'pending'   => 'badge-secondary',
+	'paid'      => 'badge-info',
+	'opened'    => 'badge-success',
+	'failed'    => 'badge-danger',
+	'cancelled' => 'badge-warning',
+];
+$title = $title ?? '订单管理';
+?>
+<div class="container-fluid p-t-15">
+  <div class="card">
+    <div class="card-header"><h4 style="display:inline-block">订单管理</h4></div>
+    <div class="card-body">
+      <form method="get" class="form-inline mb-3">
+        <input type="hidden" name="p" value="zjmfmanager_reserve">
+        <input type="hidden" name="page" value="orders">
+        <select name="status" class="form-control form-control-sm mr-2">
+          <option value="">全部状态</option>
+          <?php foreach ($status_labels as $k => $v): ?>
+            <option value="<?= htmlspecialchars($k, ENT_QUOTES) ?>"
+              <?= $filters['status'] === $k ? 'selected' : '' ?>>
+              <?= htmlspecialchars($v) ?>
+            </option>
+          <?php endforeach; ?>
+        </select>
+        <select name="supplier_id" class="form-control form-control-sm mr-2">
+          <option value="">全部供应商</option>
+          <?php foreach ($suppliers as $s): ?>
+            <option value="<?= (int)$s['id'] ?>"
+              <?= $filters['supplier_id'] == $s['id'] ? 'selected' : '' ?>>
+              <?= htmlspecialchars($s['name'], ENT_QUOTES) ?>
+            </option>
+          <?php endforeach; ?>
+        </select>
+        <input type="text" name="order_no" class="form-control form-control-sm mr-2"
+               placeholder="订单号" value="<?= htmlspecialchars($filters['order_no'], ENT_QUOTES) ?>">
+        <input type="number" name="user_id" class="form-control form-control-sm mr-2"
+               placeholder="用户 ID" value="<?= htmlspecialchars($filters['user_id'], ENT_QUOTES) ?>">
+        <button type="submit" class="btn btn-sm btn-primary">筛选</button>
+      </form>
+
+      <div class="table-responsive">
+        <table class="table table-bordered table-hover">
+          <thead>
+            <tr>
+              <th>ID</th>
+              <th>订单号</th>
+              <th>动作</th>
+              <th>供应商</th>
+              <th>用户ID</th>
+              <th>商品</th>
+              <th>周期</th>
+              <th>金额</th>
+              <th>上游订单</th>
+              <th>上游主机</th>
+              <th>状态</th>
+              <th>下单时间</th>
+              <th>完成时间</th>
+            </tr>
+          </thead>
+          <tbody>
+            <?php if (empty($orders['list'])): ?>
+              <tr><td colspan="13" class="text-center text-muted">暂无订单</td></tr>
+            <?php else: ?>
+              <?php foreach ($orders['list'] as $o): ?>
+                <tr>
+                  <td><?= (int)$o['id'] ?></td>
+                  <td class="small text-muted"><?= htmlspecialchars($o['order_no'], ENT_QUOTES) ?></td>
+                  <td><?= htmlspecialchars($action_labels[$o['action']] ?? $o['action']) ?></td>
+                  <td><?= htmlspecialchars($o['supplier_name'] ?: '-') ?></td>
+                  <td><?= (int)$o['user_id'] ?></td>
+                  <td><?= htmlspecialchars($o['product_name'], ENT_QUOTES) ?></td>
+                  <td><?= htmlspecialchars($o['cycle_name'] ?: '-') ?></td>
+                  <td>¥<?= zjmf_format_cents($o['amount_cents']) ?></td>
+                  <td><?= (int)$o['up_order_id'] > 0 ? (int)$o['up_order_id'] : '-' ?></td>
+                  <td><?= (int)$o['up_host_id'] > 0 ? (int)$o['up_host_id'] : '-' ?></td>
+                  <td>
+                    <span class="badge <?= htmlspecialchars($status_classes[$o['status']] ?? 'badge-secondary') ?>">
+                      <?= htmlspecialchars($status_labels[$o['status']] ?? $o['status']) ?>
+                    </span>
+                  </td>
+                  <td class="small"><?= htmlspecialchars($o['created_at']) ?></td>
+                  <td class="small"><?= htmlspecialchars($o['opened_at'] ?: '-') ?></td>
+                </tr>
+              <?php endforeach; ?>
+            <?php endif; ?>
+          </tbody>
+        </table>
+      </div>
+
+      <?php
+      $total_pages = max(1, (int)ceil($orders['total'] / $orders['per_page']));
+      $current_page = (int)$orders['page'];
+      if ($total_pages > 1):
+        $qs = http_build_query([
+          'p' => 'zjmfmanager_reserve', 'page' => 'orders',
+          'status' => $filters['status'], 'order_no' => $filters['order_no'],
+          'user_id' => $filters['user_id'], 'supplier_id' => $filters['supplier_id'],
+        ]);
+      ?>
+        <nav>
+          <ul class="pagination pagination-sm">
+            <?php if ($current_page > 1): ?>
+              <li class="page-item"><a class="page-link"
+                href="plugin.php?<?= htmlspecialchars(
+                  $qs . '&page_num=' . ($current_page - 1), ENT_QUOTES
+                ) ?>">上一页</a></li>
+            <?php endif; ?>
+            <li class="page-item disabled"><span class="page-link">
+              第 <?= $current_page ?> / <?= $total_pages ?> 页(共 <?= (int)$orders['total'] ?> 条)
+            </span></li>
+            <?php if ($current_page < $total_pages): ?>
+              <li class="page-item"><a class="page-link"
+                href="plugin.php?<?= htmlspecialchars(
+                  $qs . '&page_num=' . ($current_page + 1), ENT_QUOTES
+                ) ?>">下一页</a></li>
+            <?php endif; ?>
+          </ul>
+        </nav>
+      <?php endif; ?>
+    </div>
+  </div>
+</div>
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/products.php b/app_plugins/zjmfmanager_reserve/views/admin/products.php
new file mode 100644
index 0000000..82692cc
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/admin/products.php
@@ -0,0 +1,445 @@
+<?php
+/**
+ * 管理员端 - 商品管理
+ *
+ * 展示上游同步的商品列表(按供应商),支持:
+ *   - 「同步商品」弹窗:选择供应商 → 拉取上游商品列表 → 勾选 → 同步
+ *   - 「手动添加」表单:供应商 + 上游商品 ID + 名称 + 描述
+ *   - 单品加价/上架/排序配置
+ * 同步仅刷新上游名称/价格,不覆盖管理员已配置的加价与上架状态。
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+mnbt_admin_include('head');
+
+$products = zjmf_product_list_all();
+$suppliers = zjmf_supplier_list_all();
+
+function zjmf_admin_cycle_summary($product)
+{
+	$cycles = zjmf_product_cycles($product);
+	$parts = [];
+	foreach ($cycles as $cycle => $cfg) {
+		$parts[] = $cfg['name'] . ' ¥' . zjmf_format_cents($cfg['price_cents']);
+	}
+	return implode('  ', $parts);
+}
+
+function zjmf_admin_markup_label($product)
+{
+	$type = (int)$product['markup_type'];
+	$value = (int)$product['markup_value'];
+	if ($type === 0 && $value === 0) {
+		return '使用供应商规则';
+	}
+	return $type === 1
+		? '固定 +' . zjmf_format_cents($value) . ' 元'
+		: '比例 +' . ($value / 10) . '%';
+}
+?>
+<div class="container-fluid p-t-15">
+  <div class="card">
+    <div class="card-header">
+      <h4 style="display:inline-block">商品管理</h4>
+      <button type="button" class="btn btn-primary btn-sm float-right"
+              id="zjf-add">手动添加</button>
+      <button type="button" class="btn btn-primary btn-sm float-right mr-2"
+              id="zjf-sync">同步商品</button>
+    </div>
+    <div class="card-body">
+      <p class="text-muted">
+        同步从上游拉取商品与代理价,并按各周期试算本地售价;
+        同步不会覆盖已配置的加价、上架状态与排序。
+      </p>
+
+      <!-- 同步商品面板 -->
+      <div id="zjf-sync-wrap" class="mb-3 border rounded p-3" style="display:none;">
+        <h6 class="mb-2">同步商品</h6>
+        <div class="form-row mb-2">
+          <div class="col-md-4">
+            <select id="zjf-sync-supplier" class="form-control form-control-sm">
+              <?php if (empty($suppliers)): ?>
+                <option value="">请先在供应商管理中新增供应商</option>
+              <?php else: ?>
+                <?php foreach ($suppliers as $s): ?>
+                  <option value="<?= (int)$s['id'] ?>"><?=
+                    htmlspecialchars($s['name'], ENT_QUOTES)
+                  ?></option>
+                <?php endforeach; ?>
+              <?php endif; ?>
+            </select>
+          </div>
+          <div class="col-md-3">
+            <button type="button" class="btn btn-sm btn-outline-primary"
+                    id="zjf-sync-load">加载商品列表</button>
+          </div>
+          <div class="col-md-3">
+            <input type="text" id="zjf-sync-filter" class="form-control form-control-sm"
+                   placeholder="按名称过滤">
+          </div>
+        </div>
+        <div id="zjf-sync-tip" class="small text-muted mb-2"></div>
+        <div class="table-responsive">
+          <table class="table table-bordered table-sm">
+            <thead>
+              <tr>
+                <th style="width:40px;">
+                  <input type="checkbox" id="zjf-sync-all"> 全选
+                </th>
+                <th>上游ID</th>
+                <th>名称</th>
+                <th>状态</th>
+              </tr>
+            </thead>
+            <tbody id="zjf-sync-list">
+              <tr><td colspan="4" class="text-center text-muted">
+                请先选择供应商并加载商品列表
+              </td></tr>
+            </tbody>
+          </table>
+        </div>
+        <button type="button" class="btn btn-sm btn-primary" id="zjf-sync-do">开始同步</button>
+        <button type="button" class="btn btn-sm btn-secondary" id="zjf-sync-cancel">关闭</button>
+      </div>
+
+      <!-- 手动添加商品面板 -->
+      <div id="zjf-add-wrap" class="mb-3 border rounded p-3" style="display:none;">
+        <h6 class="mb-2">手动添加商品</h6>
+        <div class="form-row">
+          <div class="col-md-3 mb-2">
+            <label class="small text-muted">所属供应商 *</label>
+            <select id="zjf-add-supplier" class="form-control form-control-sm">
+              <?php foreach ($suppliers as $s): ?>
+                <option value="<?= (int)$s['id'] ?>"><?=
+                  htmlspecialchars($s['name'], ENT_QUOTES)
+                ?></option>
+              <?php endforeach; ?>
+            </select>
+          </div>
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">上游商品 ID *</label>
+            <input type="number" id="zjf-add-upid" class="form-control form-control-sm" min="1">
+          </div>
+          <div class="col-md-3 mb-2">
+            <label class="small text-muted">商品名称 *</label>
+            <input type="text" id="zjf-add-name" class="form-control form-control-sm">
+          </div>
+          <div class="col-md-4 mb-2">
+            <label class="small text-muted">描述</label>
+            <input type="text" id="zjf-add-desc" class="form-control form-control-sm">
+          </div>
+        </div>
+        <div class="small text-muted mb-2">
+          保存后将立即拉取该商品上游代理价与各周期价格;失败则该商品标记为「待同步」。
+        </div>
+        <button type="button" class="btn btn-sm btn-primary" id="zjf-add-save">添加</button>
+        <button type="button" class="btn btn-sm btn-secondary" id="zjf-add-cancel">取消</button>
+      </div>
+
+      <!-- 编辑商品面板 -->
+      <div id="zjf-edit-wrap" class="mb-3 border rounded p-3" style="display:none;">
+        <h6 class="mb-2">编辑商品</h6>
+        <input type="hidden" id="zjf-edit-id" value="0">
+        <div class="form-row">
+          <div class="col-md-4 mb-2">
+            <label class="small text-muted">商品名</label>
+            <div id="zjf-edit-name" class="pt-1"></div>
+          </div>
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">加价方式</label>
+            <select id="zjf-edit-type" class="form-control form-control-sm">
+              <option value="0">按比例</option>
+              <option value="1">固定加价(分)</option>
+            </select>
+          </div>
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">加价数值</label>
+            <input type="number" id="zjf-edit-value" class="form-control form-control-sm"
+                   min="0" value="0">
+          </div>
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">排序</label>
+            <input type="number" id="zjf-edit-sort" class="form-control form-control-sm"
+                   min="0" value="50">
+          </div>
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">状态</label>
+            <select id="zjf-edit-status" class="form-control form-control-sm">
+              <option value="1">上架</option>
+              <option value="0">下架</option>
+            </select>
+          </div>
+        </div>
+        <div class="small text-muted mb-2">
+          留空加价数值表示使用所属供应商的加价规则
+        </div>
+        <button type="button" class="btn btn-sm btn-primary" id="zjf-edit-save">保存</button>
+        <button type="button" class="btn btn-sm btn-secondary" id="zjf-edit-cancel">取消</button>
+      </div>
+
+      <div class="table-responsive">
+        <table class="table table-bordered table-hover">
+          <thead>
+            <tr>
+              <th>ID</th>
+              <th>供应商</th>
+              <th>商品名</th>
+              <th>上游ID</th>
+              <th>上游代理价</th>
+              <th>周期售价</th>
+              <th>加价</th>
+              <th>状态</th>
+              <th>排序</th>
+              <th>更新时间</th>
+              <th>操作</th>
+            </tr>
+          </thead>
+          <tbody>
+            <?php if (empty($products)): ?>
+              <tr><td colspan="11" class="text-center text-muted">
+                暂无商品,请先点击右上角「同步商品」或「手动添加」
+              </td></tr>
+            <?php else: ?>
+              <?php foreach ($products as $p): ?>
+                <tr>
+                  <td><?= (int)$p['id'] ?></td>
+                  <td><?= htmlspecialchars($p['supplier_name'] ?: '-', ENT_QUOTES) ?></td>
+                  <td><?= htmlspecialchars($p['name'], ENT_QUOTES) ?></td>
+                  <td><?= (int)$p['up_product_id'] ?></td>
+                  <td>¥<?= zjmf_format_cents($p['agent_price_cents']) ?></td>
+                  <td class="small"><?=
+                    htmlspecialchars(zjmf_admin_cycle_summary($p), ENT_QUOTES)
+                  ?></td>
+                  <td class="small"><?=
+                    htmlspecialchars(zjmf_admin_markup_label($p), ENT_QUOTES)
+                  ?></td>
+                  <td>
+                    <span class="badge <?= $p['status'] == 1 ? 'badge-success' : 'badge-secondary' ?>">
+                      <?= $p['status'] == 1 ? '上架' : '下架' ?>
+                    </span>
+                  </td>
+                  <td><?= (int)$p['sort'] ?></td>
+                  <td class="small"><?= htmlspecialchars($p['updated_at'] ?: '-') ?></td>
+                  <td class="text-nowrap">
+                    <button type="button" class="btn btn-sm btn-outline-primary zjf-edit"
+                            data-id="<?= (int)$p['id'] ?>"
+                            data-name="<?= htmlspecialchars($p['name'], ENT_QUOTES) ?>"
+                            data-type="<?= (int)$p['markup_type'] ?>"
+                            data-value="<?= (int)$p['markup_value'] ?>"
+                            data-sort="<?= (int)$p['sort'] ?>"
+                            data-status="<?= (int)$p['status'] ?>">编辑</button>
+                    <button type="button" class="btn btn-sm btn-outline-secondary zjf-toggle"
+                            data-id="<?= (int)$p['id'] ?>">
+                      <?= $p['status'] == 1 ? '下架' : '上架' ?>
+                    </button>
+                  </td>
+                </tr>
+              <?php endforeach; ?>
+            <?php endif; ?>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</div>
+<script>
+(function () {
+  function res(res) {
+    var d;
+    try { d = typeof res === 'string' ? JSON.parse(res) : res; } catch (e) { d = {code: res}; }
+    return d;
+  }
+  function notify(d, reload) {
+    var ok = d.qk == 1 || d.success;
+    if (typeof $.notify === 'function') {
+      $.notify({message: d.msg || d.code || '完成'}, {type: ok ? 'success' : 'danger'});
+    } else {
+      alert(d.msg || d.code || '完成');
+    }
+    if (ok && reload) setTimeout(function () { location.reload(); }, 600);
+  }
+  function post(data, reload) {
+    $.post('ajax.php', data, function (r) { notify(res(r), reload); });
+  }
+
+  var syncWrap = document.getElementById('zjf-sync-wrap');
+  var addWrap = document.getElementById('zjf-add-wrap');
+  var editWrap = document.getElementById('zjf-edit-wrap');
+  var syncList = document.getElementById('zjf-sync-list');
+  var syncTip = document.getElementById('zjf-sync-tip');
+
+  /* ---------------- 同步商品 ---------------- */
+  document.getElementById('zjf-sync').addEventListener('click', function () {
+    addWrap.style.display = 'none';
+    editWrap.style.display = 'none';
+    syncWrap.style.display = 'block';
+    syncWrap.scrollIntoView({behavior: 'smooth', block: 'start'});
+  });
+
+  document.getElementById('zjf-sync-load').addEventListener('click', function () {
+    var supplierId = document.getElementById('zjf-sync-supplier').value;
+    if (!supplierId) { alert('请先选择供应商'); return; }
+    var btn = this;
+    btn.disabled = true;
+    btn.textContent = '加载中...';
+    syncTip.textContent = '正在拉取上游商品列表...';
+    syncList.innerHTML = '<tr><td colspan="4" class="text-center text-muted">加载中...</td></tr>';
+    $.post('ajax.php', {gn: 'p_zjmf_admin_upstream_products', id: supplierId},
+      function (r) {
+        var d = res(r);
+        btn.disabled = false;
+        btn.textContent = '加载商品列表';
+        if (d.qk != 1 && !d.success) {
+          syncTip.textContent = '';
+          syncList.innerHTML = '<tr><td colspan="4" class="text-center text-muted">'
+            + (d.msg || d.code || '拉取失败') + '</td></tr>';
+          return;
+        }
+        var list = d.list || (d.data && d.data.list) || [];
+        if (!list.length) {
+          syncList.innerHTML = '<tr><td colspan="4" class="text-center text-muted">'
+            + '该供应商暂无商品</td></tr>';
+          return;
+        }
+        syncTip.textContent = '共 ' + list.length + ' 个商品,勾选要同步的项目后点击「开始同步」'
+          + '(已同步过的将更新价格,不覆盖加价与上架状态)';
+        var html = '';
+        for (var i = 0; i < list.length; i++) {
+          var it = list[i];
+          html += '<tr class="zjf-up-row" data-name="' + (it.name || '').toLowerCase() + '">'
+            + '<td><input type="checkbox" class="zjf-up-check" value="' + it.id + '"'
+            + (it.synced ? ' checked' : '') + '></td>'
+            + '<td>' + it.id + '</td>'
+            + '<td>' + (it.name || '-') + '</td>'
+            + '<td>' + (it.synced ? '<span class="badge badge-success">已同步</span>'
+                : '<span class="badge badge-secondary">未同步</span>') + '</td>'
+            + '</tr>';
+        }
+        syncList.innerHTML = html;
+        renderSyncFilter();
+      });
+  });
+
+  function renderSyncFilter() {
+    var kw = (document.getElementById('zjf-sync-filter').value || '').toLowerCase();
+    document.querySelectorAll('.zjf-up-row').forEach(function (row) {
+      row.style.display = (kw === '' || (row.getAttribute('data-name') || '').indexOf(kw) >= 0)
+        ? '' : 'none';
+    });
+  }
+  document.getElementById('zjf-sync-filter').addEventListener('input', renderSyncFilter);
+
+  document.getElementById('zjf-sync-all').addEventListener('change', function () {
+    var checked = this.checked;
+    document.querySelectorAll('.zjf-up-row').forEach(function (row) {
+      if (row.style.display !== 'none') {
+        row.querySelector('.zjf-up-check').checked = checked;
+      }
+    });
+  });
+
+  document.getElementById('zjf-sync-do').addEventListener('click', function () {
+    var supplierId = document.getElementById('zjf-sync-supplier').value;
+    if (!supplierId) { alert('请先选择供应商'); return; }
+    var ids = [];
+    document.querySelectorAll('.zjf-up-check:checked').forEach(function (c) {
+      ids.push(parseInt(c.value, 10));
+    });
+    if (!ids.length) { alert('请至少勾选一个商品'); return; }
+    var btn = this;
+    btn.disabled = true;
+    btn.textContent = '同步中...';
+    $.post('ajax.php', {
+      gn: 'p_zjmf_admin_sync_products',
+      supplier_id: supplierId,
+      up_ids: ids
+    }, function (r) {
+      var d = res(r);
+      notify(d, d.qk == 1 || d.success);
+      btn.disabled = false;
+      btn.textContent = '开始同步';
+    });
+  });
+
+  document.getElementById('zjf-sync-cancel').addEventListener('click', function () {
+    syncWrap.style.display = 'none';
+  });
+
+  /* ---------------- 手动添加 ---------------- */
+  document.getElementById('zjf-add').addEventListener('click', function () {
+    syncWrap.style.display = 'none';
+    editWrap.style.display = 'none';
+    addWrap.style.display = 'block';
+    addWrap.scrollIntoView({behavior: 'smooth', block: 'start'});
+  });
+
+  document.getElementById('zjf-add-cancel').addEventListener('click', function () {
+    addWrap.style.display = 'none';
+  });
+
+  document.getElementById('zjf-add-save').addEventListener('click', function () {
+    var supplierId = document.getElementById('zjf-add-supplier').value;
+    var upId = document.getElementById('zjf-add-upid').value;
+    var name = document.getElementById('zjf-add-name').value;
+    var desc = document.getElementById('zjf-add-desc').value;
+    if (!supplierId || !upId || !name) {
+      alert('请填写供应商、上游商品 ID 与名称');
+      return;
+    }
+    post({
+      gn: 'p_zjmf_admin_add_product',
+      supplier_id: supplierId,
+      up_product_id: upId,
+      name: name,
+      description: desc
+    }, true);
+  });
+
+  /* ---------------- 编辑商品 ---------------- */
+  var editId = document.getElementById('zjf-edit-id');
+  var editName = document.getElementById('zjf-edit-name');
+  var editType = document.getElementById('zjf-edit-type');
+  var editValue = document.getElementById('zjf-edit-value');
+  var editSort = document.getElementById('zjf-edit-sort');
+  var editStatus = document.getElementById('zjf-edit-status');
+
+  document.querySelectorAll('.zjf-edit').forEach(function (btn) {
+    btn.addEventListener('click', function () {
+      syncWrap.style.display = 'none';
+      addWrap.style.display = 'none';
+      editId.value = btn.getAttribute('data-id');
+      editName.textContent = btn.getAttribute('data-name');
+      editType.value = btn.getAttribute('data-type');
+      editValue.value = btn.getAttribute('data-value');
+      editSort.value = btn.getAttribute('data-sort');
+      editStatus.value = btn.getAttribute('data-status');
+      editWrap.style.display = 'block';
+      editWrap.scrollIntoView({behavior: 'smooth', block: 'start'});
+    });
+  });
+
+  document.getElementById('zjf-edit-cancel').addEventListener('click', function () {
+    editWrap.style.display = 'none';
+  });
+
+  document.getElementById('zjf-edit-save').addEventListener('click', function () {
+    var id = parseInt(editId.value, 10);
+    if (!id) return;
+    post({
+      gn: 'p_zjmf_admin_save_product',
+      id: id,
+      markup_type: editType.value,
+      markup_value: editValue.value,
+      sort: editSort.value,
+      status: editStatus.value
+    }, true);
+  });
+
+  document.querySelectorAll('.zjf-toggle').forEach(function (btn) {
+    btn.addEventListener('click', function () {
+      post({gn: 'p_zjmf_admin_toggle_product', id: btn.getAttribute('data-id')}, true);
+    });
+  });
+})();
+</script>
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/suppliers.php b/app_plugins/zjmfmanager_reserve/views/admin/suppliers.php
new file mode 100644
index 0000000..ffddb19
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/admin/suppliers.php
@@ -0,0 +1,285 @@
+<?php
+/**
+ * 管理员端 - 供应商管理
+ *
+ * 维护多个魔方财务上游供应商(独立 API 账号 / 加价规则 / 启用状态)。
+ * 停用供应商后其商品不可售,主机操作与升级被拒绝。
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+mnbt_admin_include('head');
+
+$suppliers = zjmf_supplier_list_all();
+?>
+<div class="container-fluid p-t-15">
+  <div class="card">
+    <div class="card-header">
+      <h4 style="display:inline-block">供应商管理</h4>
+      <button type="button" class="btn btn-primary btn-sm float-right"
+              id="zjf-supplier-add">新增供应商</button>
+    </div>
+    <div class="card-body">
+      <p class="text-muted">
+        可添加多个魔方财务上游站点,各自独立的 API 账号与加价规则;
+        停用后其商品自动下架不可售,主机操作与升级将被拒绝。
+      </p>
+
+      <div id="zjf-edit-wrap" class="mb-3 border rounded p-3" style="display:none;">
+        <h6 class="mb-2" id="zjf-edit-title">编辑供应商</h6>
+        <input type="hidden" id="zjf-edit-id" value="0">
+        <div class="form-row">
+          <div class="col-md-3 mb-2">
+            <label class="small text-muted">名称 *</label>
+            <input type="text" id="zjf-edit-name" class="form-control form-control-sm"
+                   placeholder="如:魔方A站">
+          </div>
+          <div class="col-md-3 mb-2">
+            <label class="small text-muted">站点 URL *</label>
+            <input type="text" id="zjf-edit-url" class="form-control form-control-sm"
+                   placeholder="https://upstream.example.com">
+          </div>
+          <div class="col-md-3 mb-2">
+            <label class="small text-muted">API 用户名 *</label>
+            <input type="text" id="zjf-edit-username" class="form-control form-control-sm">
+          </div>
+          <div class="col-md-3 mb-2">
+            <label class="small text-muted">API 密钥</label>
+            <input type="password" id="zjf-edit-password" class="form-control form-control-sm"
+                   placeholder="编辑时留空不修改">
+          </div>
+        </div>
+        <div class="form-row">
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">超时(秒)</label>
+            <input type="number" id="zjf-edit-timeout" class="form-control form-control-sm"
+                   min="5" max="120" value="30">
+          </div>
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">加价方式</label>
+            <select id="zjf-edit-mtype" class="form-control form-control-sm">
+              <option value="0">按比例</option>
+              <option value="1">固定加价(分)</option>
+            </select>
+          </div>
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">加价数值</label>
+            <input type="number" id="zjf-edit-mvalue" class="form-control form-control-sm"
+                   min="0" value="0">
+          </div>
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">状态</label>
+            <select id="zjf-edit-status" class="form-control form-control-sm">
+              <option value="1">启用</option>
+              <option value="0">停用</option>
+            </select>
+          </div>
+          <div class="col-md-2 mb-2">
+            <label class="small text-muted">排序</label>
+            <input type="number" id="zjf-edit-sort" class="form-control form-control-sm"
+                   min="0" value="0">
+          </div>
+        </div>
+        <div class="form-row">
+          <div class="col-md-6 mb-2">
+            <label class="small text-muted">备注</label>
+            <input type="text" id="zjf-edit-remark" class="form-control form-control-sm">
+          </div>
+        </div>
+        <div class="small text-muted mb-2">
+          加价数值:按比例填千分比(如 10 表示 +1%);固定加价填金额(分,1 元=100)。
+          商品未单独配置加价时使用此处规则。
+        </div>
+        <button type="button" class="btn btn-sm btn-primary" id="zjf-edit-save">保存</button>
+        <button type="button" class="btn btn-sm btn-secondary" id="zjf-edit-cancel">取消</button>
+      </div>
+
+      <div class="table-responsive">
+        <table class="table table-bordered table-hover">
+          <thead>
+            <tr>
+              <th>ID</th>
+              <th>名称</th>
+              <th>站点</th>
+              <th>加价</th>
+              <th>超时</th>
+              <th>状态</th>
+              <th>排序</th>
+              <th>备注</th>
+              <th>更新时间</th>
+              <th>操作</th>
+            </tr>
+          </thead>
+          <tbody>
+            <?php if (empty($suppliers)): ?>
+              <tr><td colspan="10" class="text-center text-muted">
+                暂无供应商,请点击右上角「新增供应商」
+              </td></tr>
+            <?php else: ?>
+              <?php foreach ($suppliers as $s): ?>
+                <tr>
+                  <td><?= (int)$s['id'] ?></td>
+                  <td><?= htmlspecialchars($s['name'], ENT_QUOTES) ?></td>
+                  <td class="small"><?= htmlspecialchars($s['api_url'], ENT_QUOTES) ?></td>
+                  <td class="small"><?=
+                    htmlspecialchars(zjmf_supplier_markup_label($s), ENT_QUOTES)
+                  ?></td>
+                  <td><?= (int)$s['api_timeout'] ?>s</td>
+                  <td>
+                    <span class="badge <?= $s['status'] == 1 ? 'badge-success' : 'badge-secondary' ?>">
+                      <?= $s['status'] == 1 ? '启用' : '停用' ?>
+                    </span>
+                  </td>
+                  <td><?= (int)$s['sort'] ?></td>
+                  <td class="small"><?= htmlspecialchars($s['remark'] ?: '-') ?></td>
+                  <td class="small"><?= htmlspecialchars($s['updated_at'] ?: '-') ?></td>
+                  <td class="text-nowrap">
+                    <button type="button" class="btn btn-sm btn-outline-primary zjf-test"
+                            data-id="<?= (int)$s['id'] ?>">连通测试</button>
+                    <button type="button" class="btn btn-sm btn-outline-primary zjf-edit"
+                            data-id="<?= (int)$s['id'] ?>"
+                            data-name="<?= htmlspecialchars($s['name'], ENT_QUOTES) ?>"
+                            data-url="<?= htmlspecialchars($s['api_url'], ENT_QUOTES) ?>"
+                            data-username="<?= htmlspecialchars($s['api_username'], ENT_QUOTES) ?>"
+                            data-timeout="<?= (int)$s['api_timeout'] ?>"
+                            data-mtype="<?= (int)$s['markup_type'] ?>"
+                            data-mvalue="<?= (int)$s['markup_value'] ?>"
+                            data-status="<?= (int)$s['status'] ?>"
+                            data-sort="<?= (int)$s['sort'] ?>"
+                            data-remark="<?= htmlspecialchars($s['remark'], ENT_QUOTES) ?>">编辑</button>
+                    <button type="button" class="btn btn-sm btn-outline-secondary zjf-toggle"
+                            data-id="<?= (int)$s['id'] ?>">
+                      <?= $s['status'] == 1 ? '停用' : '启用' ?>
+                    </button>
+                    <button type="button" class="btn btn-sm btn-outline-danger zjf-del"
+                            data-id="<?= (int)$s['id'] ?>"
+                            data-name="<?= htmlspecialchars($s['name'], ENT_QUOTES) ?>">删除</button>
+                  </td>
+                </tr>
+              <?php endforeach; ?>
+            <?php endif; ?>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</div>
+<script>
+(function () {
+  function res(res) {
+    var d;
+    try { d = typeof res === 'string' ? JSON.parse(res) : res; } catch (e) { d = {code: res}; }
+    return d;
+  }
+  function notify(d, reload) {
+    var ok = d.qk == 1 || d.success;
+    if (typeof $.notify === 'function') {
+      $.notify({message: d.msg || d.code || '完成'}, {type: ok ? 'success' : 'danger'});
+    } else {
+      alert(d.msg || d.code || '完成');
+    }
+    if (ok && reload) setTimeout(function () { location.reload(); }, 600);
+  }
+  function post(data, reload) {
+    $.post('ajax.php', data, function (r) { notify(res(r), reload); });
+  }
+
+  var wrap = document.getElementById('zjf-edit-wrap');
+  var editTitle = document.getElementById('zjf-edit-title');
+  var editId = document.getElementById('zjf-edit-id');
+
+  function openPanel(reset) {
+    if (reset) {
+      editId.value = '0';
+      document.getElementById('zjf-edit-name').value = '';
+      document.getElementById('zjf-edit-url').value = '';
+      document.getElementById('zjf-edit-username').value = '';
+      document.getElementById('zjf-edit-password').value = '';
+      document.getElementById('zjf-edit-timeout').value = '30';
+      document.getElementById('zjf-edit-mtype').value = '0';
+      document.getElementById('zjf-edit-mvalue').value = '0';
+      document.getElementById('zjf-edit-status').value = '1';
+      document.getElementById('zjf-edit-sort').value = '0';
+      document.getElementById('zjf-edit-remark').value = '';
+      editTitle.textContent = '新增供应商';
+    }
+    wrap.style.display = 'block';
+    wrap.scrollIntoView({behavior: 'smooth', block: 'start'});
+  }
+
+  document.getElementById('zjf-supplier-add').addEventListener('click', function () {
+    openPanel(true);
+  });
+
+  document.querySelectorAll('.zjf-edit').forEach(function (btn) {
+    btn.addEventListener('click', function () {
+      editId.value = btn.getAttribute('data-id');
+      document.getElementById('zjf-edit-name').value = btn.getAttribute('data-name');
+      document.getElementById('zjf-edit-url').value = btn.getAttribute('data-url');
+      document.getElementById('zjf-edit-username').value = btn.getAttribute('data-username');
+      document.getElementById('zjf-edit-password').value = '';
+      document.getElementById('zjf-edit-timeout').value = btn.getAttribute('data-timeout');
+      document.getElementById('zjf-edit-mtype').value = btn.getAttribute('data-mtype');
+      document.getElementById('zjf-edit-mvalue').value = btn.getAttribute('data-mvalue');
+      document.getElementById('zjf-edit-status').value = btn.getAttribute('data-status');
+      document.getElementById('zjf-edit-sort').value = btn.getAttribute('data-sort');
+      document.getElementById('zjf-edit-remark').value = btn.getAttribute('data-remark');
+      editTitle.textContent = '编辑供应商';
+      openPanel(false);
+    });
+  });
+
+  document.getElementById('zjf-edit-cancel').addEventListener('click', function () {
+    wrap.style.display = 'none';
+  });
+
+  document.getElementById('zjf-edit-save').addEventListener('click', function () {
+    post({
+      gn: 'p_zjmf_admin_save_supplier',
+      id: editId.value,
+      name: document.getElementById('zjf-edit-name').value,
+      api_url: document.getElementById('zjf-edit-url').value,
+      api_username: document.getElementById('zjf-edit-username').value,
+      api_password: document.getElementById('zjf-edit-password').value,
+      api_timeout: document.getElementById('zjf-edit-timeout').value,
+      markup_type: document.getElementById('zjf-edit-mtype').value,
+      markup_value: document.getElementById('zjf-edit-mvalue').value,
+      status: document.getElementById('zjf-edit-status').value,
+      sort: document.getElementById('zjf-edit-sort').value,
+      remark: document.getElementById('zjf-edit-remark').value
+    }, true);
+  });
+
+  document.querySelectorAll('.zjf-test').forEach(function (btn) {
+    btn.addEventListener('click', function () {
+      var b = this;
+      b.disabled = true;
+      var old = b.textContent;
+      b.textContent = '测试中...';
+      $.post('ajax.php', {gn: 'p_zjmf_admin_test_supplier', id: btn.getAttribute('data-id')},
+        function (r) {
+          var d = res(r);
+          notify(d, false);
+          b.disabled = false;
+          b.textContent = old;
+        });
+    });
+  });
+
+  document.querySelectorAll('.zjf-toggle').forEach(function (btn) {
+    btn.addEventListener('click', function () {
+      post({gn: 'p_zjmf_admin_toggle_supplier', id: btn.getAttribute('data-id')}, true);
+    });
+  });
+
+  document.querySelectorAll('.zjf-del').forEach(function (btn) {
+    btn.addEventListener('click', function () {
+      var name = btn.getAttribute('data-name');
+      if (!confirm('确定删除供应商「' + name + '」吗?\n该供应商下存在商品/订单/主机时将无法删除。')) {
+        return;
+      }
+      post({gn: 'p_zjmf_admin_delete_supplier', id: btn.getAttribute('data-id')}, true);
+    });
+  });
+})();
+</script>
diff --git a/app_plugins/zjmfmanager_reserve/views/host.php b/app_plugins/zjmfmanager_reserve/views/host.php
new file mode 100644
index 0000000..c2197b9
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/host.php
@@ -0,0 +1,161 @@
+<?php
+/**
+ * 用户端 - 主机详情(实时状态 / 流量 / 操作)
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+$page_title = $page_title ?? '主机详情';
+$host = $host ?? null;
+$info = $info ?? ['ok' => false, 'msg' => ''];
+$traffic = $traffic ?? ['ok' => false, 'data' => []];
+
+$password = zjmf_decrypt((string)$host['password']);
+$hasUpId = (int)$host['up_host_id'] > 0;
+
+function zjmf_view_show_traffic($data)
+{
+	if (!is_array($data) || $data === []) {
+		return '暂无流量数据';
+	}
+	// 常见标量字段直接展示
+	$rows = [];
+	foreach (['total', 'used', 'free', 'percent', 'current', 'monthly_used'] as $k) {
+		if (isset($data[$k]) && $data[$k] !== '' && $data[$k] !== null) {
+			$rows[] = $k . ':' . htmlspecialchars((string)$data[$k]);
+		}
+	}
+	if ($rows) {
+		return implode('<br>', $rows);
+	}
+	return htmlspecialchars(json_encode($data, JSON_UNESCAPED_UNICODE));
+}
+ob_start();
+?>
+<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
+  <h1 style="font-size:20px;color:#222;margin:0;"><?= htmlspecialchars($host['name']) ?></h1>
+  <a class="layui-btn layui-btn-xs layui-btn-primary" href="<?= zjmf_url('reserve/hosts') ?>">返回主机列表</a>
+</div>
+
+<div class="zj-msg" id="zjf-msg"></div>
+
+<div class="layui-card">
+  <div class="layui-card-header">主机信息</div>
+  <div class="layui-card-body" style="padding:0;">
+    <table class="zj-table">
+      <tbody>
+        <tr><td style="width:120px;">状态</td><td>
+          <span class="zj-status zj-status-<?= htmlspecialchars($host['status']) ?>">
+            <?= htmlspecialchars(zjmf_host_status_label($host['status'])) ?>
+          </span>
+          <?php if (!empty($info['ok']) && $info['status'] !== $host['status']): ?>
+            <span class="zj-muted">(实时:<?= htmlspecialchars(zjmf_host_status_label($info['status'])) ?>)</span>
+          <?php endif; ?>
+        </td></tr>
+        <tr><td>供应商</td><td><?= htmlspecialchars($host['supplier_name'] ?: '-') ?></td></tr>
+        <tr><td>用户名</td><td class="zj-mono" id="zjf-username"><?=
+          htmlspecialchars(zjmf_mask_account($host['username']))
+        ?></td></tr>
+        <tr><td>密码</td><td class="zj-mono" id="zjf-password"><?= $password !== '' ? '••••••••' : '-' ?></td></tr>
+        <tr><td>周期</td><td><?= htmlspecialchars($host['cycle'] ?: '-') ?></td></tr>
+        <tr><td>到期时间</td><td><?= htmlspecialchars($host['renew_date'] ?: '-') ?></td></tr>
+        <tr><td>上游主机 ID</td><td class="zj-mono"><?= (int)$host['up_host_id'] ?></td></tr>
+      </tbody>
+    </table>
+  </div>
+</div>
+
+<div class="layui-card">
+  <div class="layui-card-header">流量使用</div>
+  <div class="layui-card-body" style="padding:16px;font-size:13px;color:#555;">
+    <?php if (!empty($traffic['ok'])): ?>
+      <?= zjmf_view_show_traffic($traffic['data']) ?>
+    <?php else: ?>
+      <span class="zj-muted"><?= (($traffic['msg'] ?? '') ?: '流量查询失败') ?></span>
+    <?php endif; ?>
+  </div>
+</div>
+
+<?php if ($hasUpId): ?>
+<div class="layui-card">
+  <div class="layui-card-header">主机操作</div>
+  <div class="layui-card-body">
+    <p class="zj-muted" style="margin-top:0;">重启 / 重装 / 重置密码为高危操作,请谨慎执行。</p>
+    <button type="button" class="layui-btn layui-btn-sm" data-act="on">开机</button>
+    <button type="button" class="layui-btn layui-btn-sm layui-btn-warm" data-act="off">关机</button>
+    <button type="button" class="layui-btn layui-btn-sm layui-btn-warm" data-act="reboot">重启</button>
+    <button type="button" class="layui-btn layui-btn-sm layui-btn-danger" data-act="reinstall">重装系统</button>
+    <button type="button" class="layui-btn layui-btn-sm" data-act="reset_password">重置密码</button>
+
+    <div id="zjf-pass-panel" style="display:none;margin-top:14px;padding:14px;
+      background:#fafbfc;border:1px solid #eee;border-radius:6px;">
+      <input type="password" id="zjf-pass-input" class="layui-input" style="max-width:260px;"
+             placeholder="输入新密码(至少 6 位)">
+      <button type="button" class="layui-btn layui-btn-sm" id="zjf-pass-confirm"
+              style="margin-top:10px;">确认重置</button>
+    </div>
+  </div>
+</div>
+<?php endif; ?>
+
+<script>
+(function () {
+  var msg = document.getElementById('zjf-msg');
+  var passPanel = document.getElementById('zjf-pass-panel');
+  var passInput = document.getElementById('zjf-pass-input');
+  var pendingAction = '';
+
+  function show(text, ok) {
+    msg.textContent = text;
+    msg.className = 'zj-msg zj-msg-show ' + (ok ? 'zj-msg-success' : 'zj-msg-error');
+  }
+  function call(action, extra) {
+    var body = new URLSearchParams();
+    body.append('host_id', '<?= (int)$host['id'] ?>');
+    body.append('action', action);
+    for (var k in (extra || {})) body.append(k, extra[k]);
+    fetch('<?= zjmf_url('reserve/api/host_action') ?>', {
+      method: 'POST',
+      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+      body: body.toString()
+    }).then(function (r) { return r.json(); }).then(function (res) {
+      var ok = res.code === 'ok' || res.success;
+      show(res.msg || res.code || '操作成功', ok);
+      if (ok) setTimeout(function () { location.reload(); }, 800);
+    }).catch(function () {
+      show('网络错误,请重试', false);
+    });
+  }
+
+  document.querySelectorAll('[data-act]').forEach(function (btn) {
+    btn.addEventListener('click', function () {
+      var act = btn.getAttribute('data-act');
+      if (act === 'reset_password') {
+        pendingAction = act;
+        passPanel.style.display = 'block';
+        passInput.focus();
+        return;
+      }
+      if (act === 'reinstall') {
+        if (!confirm('确定要重装该系统吗?重装将清空数据且不可恢复!')) return;
+      }
+      if (act === 'reboot') {
+        if (!confirm('确定要重启该主机吗?')) return;
+      }
+      call(act, {});
+    });
+  });
+
+  document.getElementById('zjf-pass-confirm').addEventListener('click', function () {
+    var pwd = passInput.value;
+    if (!pwd || pwd.length < 6) {
+      show('密码至少 6 位', false);
+      return;
+    }
+    call('reset_password', {password: pwd});
+  });
+})();
+</script>
+<?php
+$content = ob_get_clean();
+include __DIR__ . '/layout.php';
diff --git a/app_plugins/zjmfmanager_reserve/views/hosts.php b/app_plugins/zjmfmanager_reserve/views/hosts.php
new file mode 100644
index 0000000..0114a00
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/hosts.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * 用户端 - 我的主机列表
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+$page_title = $page_title ?? '我的主机';
+$hosts = $hosts ?? [];
+ob_start();
+?>
+<div class="zj-section">
+  <h1>我的主机</h1>
+  <p>查看已开通的主机,点击进入详情可执行状态/流量查询与操作</p>
+</div>
+
+<?php if (empty($hosts)): ?>
+  <div class="layui-card">
+    <div class="layui-card-body" style="text-align:center;padding:40px;color:#999;">
+      暂无主机,<a href="<?= zjmf_url('reserve/shop') ?>">去选购商品</a>
+    </div>
+  </div>
+<?php else: ?>
+  <div class="layui-card">
+    <div class="layui-card-body" style="padding:0;">
+      <table class="zj-table">
+        <thead>
+          <tr>
+            <th>主机</th>
+            <th>供应商</th>
+            <th>状态</th>
+            <th>用户名</th>
+            <th>周期</th>
+            <th>到期时间</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <?php foreach ($hosts as $host): ?>
+            <tr>
+              <td>
+                <b><?= htmlspecialchars($host['name']) ?></b>
+                <div class="zj-muted">ID <?= (int)$host['id'] ?>
+                  / 上游 <?= (int)$host['up_host_id'] ?></div>
+              </td>
+              <td><?= htmlspecialchars($host['supplier_name'] ?: '-') ?></td>
+              <td>
+                <span class="zj-status zj-status-<?= htmlspecialchars($host['status']) ?>">
+                  <?= htmlspecialchars(zjmf_host_status_label($host['status'])) ?>
+                </span>
+              </td>
+              <td class="zj-mono"><?= htmlspecialchars(zjmf_mask_account($host['username'])) ?></td>
+              <td><?= htmlspecialchars($host['cycle'] ?: '-') ?></td>
+              <td><?= htmlspecialchars($host['renew_date'] ?: '-') ?></td>
+              <td>
+                <a class="layui-btn layui-btn-xs" href="<?= zjmf_url('reserve/hosts/' . (int)$host['id']) ?>">详情</a>
+              </td>
+            </tr>
+          <?php endforeach; ?>
+        </tbody>
+      </table>
+    </div>
+  </div>
+<?php endif; ?>
+
+<?php
+$content = ob_get_clean();
+include __DIR__ . '/layout.php';
diff --git a/app_plugins/zjmfmanager_reserve/views/layout.php b/app_plugins/zjmfmanager_reserve/views/layout.php
new file mode 100644
index 0000000..58d1027
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/layout.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * zjmfmanager_reserve 用户端 - 公共布局
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+$current_user = $current_user ?? null;
+$page_title = $page_title ?? '魔方财务分销';
+$content = $content ?? '';
+?>
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title><?= htmlspecialchars($page_title) ?> - 魔方财务分销</title>
+<link rel="stylesheet" href="https://unpkg.com/layui@2.9.8/dist/css/layui.css">
+<link rel="stylesheet" href="<?= zjmf_asset_url('style.css') ?>">
+</head>
+<body>
+
+<div class="zj-nav">
+  <div class="zj-nav-inner">
+    <a class="zj-nav-brand" href="<?= zjmf_url('reserve/shop') ?>">魔方财务分销</a>
+    <div class="zj-nav-links">
+      <?php if ($current_user): ?>
+        <a href="<?= zjmf_url('reserve/shop') ?>">商品</a>
+        <a href="<?= zjmf_url('reserve/hosts') ?>">我的主机</a>
+        <a href="<?= zjmf_url('reserve/orders') ?>">订单</a>
+        <a href="<?= zjmf_url('balance') ?>">余额</a>
+        <a href="<?= zjmf_url('account/profile') ?>">个人信息</a>
+        <a href="<?= zjmf_url('account/logout') ?>">退出</a>
+      <?php else: ?>
+        <a href="<?= zjmf_url('account/login') ?>">登录</a>
+        <a href="<?= zjmf_url('account/register') ?>">注册</a>
+      <?php endif; ?>
+    </div>
+  </div>
+</div>
+
+<div class="zj-page"><?= $content ?></div>
+
+<script src="https://unpkg.com/layui@2.9.8/dist/layui.js"></script>
+</body>
+</html>
diff --git a/app_plugins/zjmfmanager_reserve/views/order.php b/app_plugins/zjmfmanager_reserve/views/order.php
new file mode 100644
index 0000000..d8c5bf0
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/order.php
@@ -0,0 +1,140 @@
+<?php
+/**
+ * 用户端 - 下单页(选择周期 + 支付方式)
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+$page_title = $page_title ?? '购买商品';
+$product = $product ?? null;
+$methods = $methods ?? [];
+ob_start();
+?>
+<div class="layui-card">
+  <div class="layui-card-body" style="padding:28px;">
+    <div class="zj-msg" id="zjf-msg"></div>
+
+    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:18px;">
+      <h1 style="font-size:20px;color:#222;margin:0;">购买:<?= htmlspecialchars($product['name']) ?></h1>
+      <a class="layui-btn layui-btn-xs layui-btn-primary" href="<?= zjmf_url('reserve/shop') ?>">返回商品列表</a>
+    </div>
+
+    <div class="zj-desc"><?= nl2br(htmlspecialchars($product['description'])) ?></div>
+
+    <form class="zj-order-form" id="zjf-order-form">
+      <div class="layui-form-item">
+        <label class="layui-form-label">购买周期</label>
+        <div class="layui-input-block zj-choices" style="padding-top:8px;">
+          <?php
+            $cycles = zjmf_product_cycles($product);
+            foreach ($cycles as $cycle => $cfg):
+          ?>
+            <label class="zj-choice">
+              <input type="radio" name="cycle" value="<?= htmlspecialchars($cycle, ENT_QUOTES) ?>">
+              <?= htmlspecialchars($cfg['name']) ?> ¥<?= zjmf_format_cents($cfg['price_cents']) ?>
+            </label>
+          <?php endforeach; ?>
+          <?php if ($cycles === []): ?>
+            <span style="color:#999;">该商品未设置可购买周期</span>
+          <?php endif; ?>
+        </div>
+      </div>
+
+      <?php if (!empty($methods)): ?>
+        <div class="layui-form-item">
+          <label class="layui-form-label">支付方式</label>
+          <div class="layui-input-block zj-choices" style="padding-top:6px;">
+            <?php foreach ($methods as $m): ?>
+              <label class="zj-choice">
+                <input type="radio" name="type"
+                       value="<?= htmlspecialchars($m['plugin'] . '__' . $m['method'], ENT_QUOTES) ?>" required>
+                <?= htmlspecialchars($m['display_name'] ?: ($m['plugin'] . ' / ' . $m['method'])) ?>
+              </label>
+            <?php endforeach; ?>
+          </div>
+        </div>
+      <?php else: ?>
+        <div class="layui-form-item">
+          <div class="layui-input-block" style="color:#999;">暂无可用的支付方式</div>
+        </div>
+      <?php endif; ?>
+
+      <?php if (!empty($methods) && $cycles !== []): ?>
+        <div class="layui-form-item">
+          <div class="layui-input-block">
+            <button type="submit" class="layui-btn layui-btn-lg" id="zjf-submit">确认购买</button>
+          </div>
+        </div>
+      <?php endif; ?>
+    </form>
+  </div>
+</div>
+
+<script>
+(function () {
+  var form = document.getElementById('zjf-order-form');
+  if (!form) return;
+  var msg = document.getElementById('zjf-msg');
+  var btn = document.getElementById('zjf-submit');
+
+  function showMsg(text, success) {
+    msg.textContent = text;
+    msg.className = 'zj-msg ' + (success ? 'zj-msg-success' : 'zj-msg-error');
+  }
+  function checkFirst(name) {
+    var list = form.querySelectorAll('input[name="' + name + '"]');
+    if (list.length && !form.querySelector('input[name="' + name + '"]:checked')) {
+      list[0].checked = true;
+    }
+  }
+  function updateChoices() {
+    form.querySelectorAll('.zj-choice').forEach(function (l) { l.classList.remove('active'); });
+    form.querySelectorAll('input[type="radio"]:checked').forEach(function (r) {
+      var p = r.closest('.zj-choice');
+      if (p) p.classList.add('active');
+    });
+  }
+  checkFirst('cycle');
+  checkFirst('type');
+  form.addEventListener('change', updateChoices);
+  updateChoices();
+
+  form.addEventListener('submit', function (e) {
+    e.preventDefault();
+    if (!btn) return;
+    btn.disabled = true;
+    btn.textContent = '正在创建订单...';
+    msg.className = 'zj-msg';
+
+    var body = new URLSearchParams();
+    body.append('product_id', '<?= (int)$product['id'] ?>');
+    var c = form.querySelector('input[name="cycle"]:checked');
+    body.append('cycle', c ? c.value : '');
+    var t = form.querySelector('input[name="type"]:checked');
+    body.append('type', t ? t.value : '');
+
+    fetch('<?= zjmf_url('reserve/api/create_order') ?>', {
+      method: 'POST',
+      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+      body: body.toString()
+    }).then(function (r) { return r.json(); }).then(function (res) {
+      if (res.html) {
+        document.open();
+        document.write(res.html);
+        document.close();
+      } else {
+        showMsg(res.code || '创建订单失败', false);
+        btn.disabled = false;
+        btn.textContent = '确认购买';
+      }
+    }).catch(function () {
+      showMsg('网络错误,请重试', false);
+      btn.disabled = false;
+      btn.textContent = '确认购买';
+    });
+  });
+})();
+</script>
+<?php
+$content = ob_get_clean();
+include __DIR__ . '/layout.php';
diff --git a/app_plugins/zjmfmanager_reserve/views/orders.php b/app_plugins/zjmfmanager_reserve/views/orders.php
new file mode 100644
index 0000000..0f85805
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/orders.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * 用户端 - 我的订单
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+$page_title = $page_title ?? '我的订单';
+$orders = $orders ?? ['list' => [], 'total' => 0, 'page' => 1, 'per_page' => 15];
+
+$action_labels = [
+	'buy'             => '购买',
+	'upgrade_config'  => '配置升级',
+	'upgrade_product' => '产品升级',
+];
+$status_labels = [
+	'pending'   => '待处理',
+	'paid'      => '已支付',
+	'opened'    => '已完成',
+	'failed'    => '失败',
+	'cancelled' => '已取消',
+];
+ob_start();
+?>
+<div class="zj-section">
+  <h1>我的订单</h1>
+  <p>购买与升级订单记录</p>
+</div>
+
+<?php if (empty($orders['list'])): ?>
+  <div class="layui-card">
+    <div class="layui-card-body" style="text-align:center;padding:40px;color:#999;">
+      暂无订单,<a href="<?= zjmf_url('reserve/shop') ?>">去选购商品</a>
+    </div>
+  </div>
+<?php else: ?>
+  <div class="layui-card">
+    <div class="layui-card-body" style="padding:0;">
+      <table class="zj-table">
+        <thead>
+          <tr>
+            <th>订单号</th>
+            <th>类型</th>
+            <th>供应商</th>
+            <th>商品</th>
+            <th>周期</th>
+            <th>金额</th>
+            <th>状态</th>
+            <th>下单时间</th>
+          </tr>
+        </thead>
+        <tbody>
+          <?php foreach ($orders['list'] as $o): ?>
+            <tr>
+              <td class="zj-mono"><?= htmlspecialchars($o['order_no']) ?></td>
+              <td><?= htmlspecialchars($action_labels[$o['action']] ?? $o['action']) ?></td>
+              <td><?= htmlspecialchars($o['supplier_name'] ?: '-') ?></td>
+              <td><?= htmlspecialchars($o['product_name']) ?></td>
+              <td><?= htmlspecialchars($o['cycle_name'] ?: '-') ?></td>
+              <td>¥<?= zjmf_format_cents($o['amount_cents']) ?></td>
+              <td>
+                <span class="zj-status zj-status-<?= htmlspecialchars($o['status']) ?>">
+                  <?= htmlspecialchars($status_labels[$o['status']] ?? $o['status']) ?>
+                </span>
+              </td>
+              <td class="small"><?= htmlspecialchars($o['created_at']) ?></td>
+            </tr>
+          <?php endforeach; ?>
+        </tbody>
+      </table>
+    </div>
+  </div>
+
+  <?php
+  $total_pages = max(1, (int)ceil($orders['total'] / $orders['per_page']));
+  $current_page = (int)$orders['page'];
+  if ($total_pages > 1):
+  ?>
+    <div class="zj-pager">
+      <?php if ($current_page > 1): ?>
+        <a href="<?= zjmf_url('reserve/orders?page=' . ($current_page - 1)) ?>">上一页</a>
+      <?php endif; ?>
+      <span class="zj-pager-info">第 <?= $current_page ?> / <?= $total_pages ?> 页</span>
+      <?php if ($current_page < $total_pages): ?>
+        <a href="<?= zjmf_url('reserve/orders?page=' . ($current_page + 1)) ?>">下一页</a>
+      <?php endif; ?>
+    </div>
+  <?php endif; ?>
+<?php endif; ?>
+
+<?php
+$content = ob_get_clean();
+include __DIR__ . '/layout.php';
diff --git a/app_plugins/zjmfmanager_reserve/views/shop.php b/app_plugins/zjmfmanager_reserve/views/shop.php
new file mode 100644
index 0000000..2f8dcf9
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/shop.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * 用户端 - 商品列表(按供应商分组展示)
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+$page_title = $page_title ?? '商品选购';
+$products = $products ?? [];
+
+// 按供应商分组
+$groups = [];
+foreach ($products as $product) {
+	$key = (int)$product['supplier_id'];
+	if (!isset($groups[$key])) {
+		$groups[$key] = [
+			'name'  => (string)($product['supplier_name'] ?? '其他供应商'),
+			'items' => [],
+		];
+	}
+	$groups[$key]['items'][] = $product;
+}
+ob_start();
+?>
+<div class="zj-section">
+  <h1>商品选购</h1>
+  <p>选择商品与周期,余额支付后自动开通主机</p>
+</div>
+
+<?php if (empty($groups)): ?>
+  <div class="layui-card">
+    <div class="layui-card-body" style="text-align:center;padding:40px;color:#999;">
+      暂无可购买的商品,请稍后再来。
+    </div>
+  </div>
+<?php else: ?>
+  <?php foreach ($groups as $group): ?>
+    <div class="zj-group">
+      <div class="zj-group-head">
+        <span class="zj-group-name"><?= htmlspecialchars($group['name']) ?></span>
+        <span class="zj-tag">供应商</span>
+      </div>
+      <div class="zj-grid">
+        <?php foreach ($group['items'] as $product): ?>
+          <div class="zj-card">
+            <div class="zj-card-head">
+              <h2><?= htmlspecialchars($product['name']) ?></h2>
+              <?php if (!empty($product['currency'])): ?>
+                <span class="zj-tag"><?= htmlspecialchars($product['currency']) ?></span>
+              <?php endif; ?>
+            </div>
+            <div class="zj-card-desc"><?= nl2br(htmlspecialchars($product['description'])) ?></div>
+            <div class="zj-price">
+              <?php $cycles = zjmf_product_cycles($product); ?>
+              <?php foreach ($cycles as $cycle => $cfg): ?>
+                <div class="zj-price-item">
+                  <span class="zj-price-label"><?= htmlspecialchars($cfg['name']) ?></span>
+                  <span class="zj-price-value">¥<?= zjmf_format_cents($cfg['price_cents']) ?></span>
+                </div>
+              <?php endforeach; ?>
+              <?php if ($cycles === []): ?>
+                <span style="color:#999;font-size:12px;">暂无可购买周期</span>
+              <?php endif; ?>
+            </div>
+            <div class="zj-buy">
+              <a class="layui-btn"
+                 href="<?= zjmf_url('reserve/product/' . (int)$product['id']) ?>">立即购买</a>
+            </div>
+          </div>
+        <?php endforeach; ?>
+      </div>
+    </div>
+  <?php endforeach; ?>
+<?php endif; ?>
+
+<?php
+$content = ob_get_clean();
+include __DIR__ . '/layout.php';
diff --git a/app_plugins/zjmfmanager_reserve/views/upgrade.php b/app_plugins/zjmfmanager_reserve/views/upgrade.php
new file mode 100644
index 0000000..8d11aed
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/upgrade.php
@@ -0,0 +1,249 @@
+<?php
+/**
+ * 用户端 - 主机升级(配置升级 / 产品升降级)
+ */
+if (!defined('IN_CRONLITE')) {
+	exit;
+}
+$page_title = $page_title ?? '主机升级';
+$host = $host ?? null;
+$kind = $kind ?? 'config';
+$options = $options ?? ['ok' => false, 'msg' => ''];
+$optionsData = !empty($options['ok']) && is_array($options['data'])
+	? $options['data'] : [];
+
+function zjf_config_options($data)
+{
+	$opts = $data['configoptions'] ?? null;
+	if (!is_array($opts)) {
+		return [];
+	}
+	$out = [];
+	foreach ($opts as $o) {
+		if (!is_array($o)) {
+			continue;
+		}
+		$id = $o['id'] ?? $o['configid'] ?? 0;
+		$name = (string)($o['name'] ?? $o['optionname'] ?? ('选项 ' . $id));
+		$choices = [];
+		$raw = $o['options'] ?? $o['values'] ?? null;
+		if (is_array($raw)) {
+			foreach ($raw as $c) {
+				if (is_array($c)) {
+					$v = $c['value'] ?? $c['id'] ?? '';
+					$l = (string)($c['name'] ?? $c['label'] ?? $v);
+				} else {
+					$v = $c;
+					$l = (string)$c;
+				}
+				if ($v !== '') {
+					$choices[] = ['value' => $v, 'label' => $l];
+				}
+			}
+		}
+		if ($id && $choices) {
+			$out[] = ['id' => $id, 'name' => $name, 'choices' => $choices];
+		}
+	}
+	return $out;
+}
+
+function zjf_upgrade_products($data)
+{
+	$list = $data['list'] ?? $data['products'] ?? null;
+	if (!is_array($list) && isset($data[0]) && is_array($data[0])) {
+		$list = $data;
+	}
+	if (!is_array($list)) {
+		return [];
+	}
+	$out = [];
+	foreach ($list as $p) {
+		if (!is_array($p)) {
+			continue;
+		}
+		$id = (int)($p['id'] ?? 0);
+		if ($id <= 0) {
+			continue;
+		}
+		$cycles = [];
+		$raw = $p['cycles'] ?? null;
+		if (is_array($raw)) {
+			foreach ($raw as $c) {
+				if (!is_array($c)) {
+					continue;
+				}
+				$bc = (string)($c['billingcycle'] ?? $c['cycle'] ?? '');
+				if ($bc === '') {
+					continue;
+				}
+				$cycles[$bc] = (string)($c['name'] ?? $bc);
+			}
+		}
+		$out[] = ['id' => $id, 'name' => (string)($p['name'] ?? ''), 'cycles' => $cycles];
+	}
+	return $out;
+}
+
+$configOptions = zjf_config_options($optionsData);
+$productList = zjf_upgrade_products($optionsData);
+ob_start();
+?>
+<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
+  <h1 style="font-size:20px;color:#222;margin:0;">升级:<?= htmlspecialchars($host['name']) ?></h1>
+  <a class="layui-btn layui-btn-xs layui-btn-primary"
+     href="<?= zjmf_url('reserve/hosts/' . (int)$host['id']) ?>">返回主机详情</a>
+</div>
+
+<div class="zj-msg" id="zjf-msg"></div>
+
+<div class="layui-btn-group" style="margin-bottom:16px;">
+  <a class="layui-btn <?= $kind === 'config' ? '' : 'layui-btn-primary' ?>"
+     href="<?= zjmf_url('reserve/hosts/' . (int)$host['id'] . '/upgrade?kind=config') ?>">配置升级</a>
+  <a class="layui-btn <?= $kind === 'product' ? '' : 'layui-btn-primary' ?>"
+     href="<?= zjmf_url('reserve/hosts/' . (int)$host['id'] . '/upgrade?kind=product') ?>">产品升级</a>
+</div>
+
+<?php if (empty($options['ok'])): ?>
+  <div class="layui-card">
+    <div class="layui-card-body" style="color:#999;">
+      获取升级选项失败:<?= htmlspecialchars($options['msg'] ?? '未知错误') ?>
+    </div>
+  </div>
+<?php elseif ($kind === 'config'): ?>
+  <div class="layui-card">
+    <div class="layui-card-header">选择配置项</div>
+    <div class="layui-card-body">
+      <?php if ($configOptions === []): ?>
+        <p class="zj-muted">
+          未解析到可配置项(上游返回结构需联调确认,见 PRD Q1)。请在上游后台直接调整配置。
+        </p>
+      <?php else: ?>
+        <form id="zjf-cfg-form">
+          <?php foreach ($configOptions as $opt): ?>
+            <div class="form-group" style="margin-bottom:14px;">
+              <label style="display:block;font-weight:600;margin-bottom:6px;">
+                <?= htmlspecialchars($opt['name']) ?>
+              </label>
+              <select class="form-control zjf-cfg-select"
+                      data-id="<?= (int)$opt['id'] ?>"
+                      style="max-width:360px;">
+                <?php foreach ($opt['choices'] as $c): ?>
+                  <option value="<?= htmlspecialchars($c['value'], ENT_QUOTES) ?>">
+                    <?= htmlspecialchars($c['label']) ?>
+                  </option>
+                <?php endforeach; ?>
+              </select>
+            </div>
+          <?php endforeach; ?>
+          <div id="zjf-price" class="zj-muted" style="margin:10px 0;"></div>
+          <button type="button" class="layui-btn" id="zjf-cfg-preview">试算差额</button>
+          <button type="button" class="layui-btn layui-btn-danger" id="zjf-cfg-confirm">确认升级</button>
+        </form>
+      <?php endif; ?>
+    </div>
+  </div>
+<?php else: ?>
+  <div class="layui-card">
+    <div class="layui-card-header">选择目标产品</div>
+    <div class="layui-card-body">
+      <?php if ($productList === []): ?>
+        <p class="zj-muted">
+          未解析到可升级产品(上游返回结构需联调确认,见 PRD Q1)。
+        </p>
+      <?php else: ?>
+        <form id="zjf-prod-form">
+          <?php foreach ($productList as $p): ?>
+            <label class="zj-choice" style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
+              <input type="radio" name="newpid" value="<?= (int)$p['id'] ?>" class="zjf-prod-pid">
+              <?= htmlspecialchars($p['name']) ?>
+              <select class="form-control form-control-sm zjf-prod-cycle" style="width:140px;">
+                <?php foreach ($p['cycles'] as $bc => $cn): ?>
+                  <option value="<?= htmlspecialchars($bc, ENT_QUOTES) ?>">
+                    <?= htmlspecialchars($cn) ?>
+                  </option>
+                <?php endforeach; ?>
+              </select>
+            </label>
+          <?php endforeach; ?>
+          <div id="zjf-price" class="zj-muted" style="margin:10px 0;"></div>
+          <button type="button" class="layui-btn" id="zjf-prod-preview">试算差额</button>
+          <button type="button" class="layui-btn layui-btn-danger" id="zjf-prod-confirm">确认升级</button>
+        </form>
+      <?php endif; ?>
+    </div>
+  </div>
+<?php endif; ?>
+
+<script>
+(function () {
+  var hostId = '<?= (int)$host['id'] ?>';
+  var kind = '<?= $kind === 'product' ? 'product' : 'config' ?>';
+  var msg = document.getElementById('zjf-msg');
+
+  function show(text, ok) {
+    msg.textContent = text;
+    msg.className = 'zj-msg zj-msg-show ' + (ok ? 'zj-msg-success' : 'zj-msg-error');
+  }
+  function buildBody(preview) {
+    var body = new URLSearchParams();
+    body.append('host_id', hostId);
+    body.append('kind', kind);
+    body.append('preview', preview);
+    if (kind === 'config') {
+      var cfg = {};
+      document.querySelectorAll('.zjf-cfg-select').forEach(function (s) {
+        cfg[s.getAttribute('data-id')] = s.value;
+      });
+      body.append('config_json', JSON.stringify(cfg));
+    } else {
+      var pid = document.querySelector('input[name="newpid"]:checked');
+      if (!pid) return null;
+      var sel = pid.closest('.zj-choice').querySelector('.zjf-prod-cycle');
+      body.append('newpid', pid.value);
+      body.append('billingcycle', sel.value);
+    }
+    return body;
+  }
+  function post(preview, confirmText) {
+    var body = buildBody(preview);
+    if (!body) {
+      show('请先选择升级目标', false);
+      return;
+    }
+    if (confirmText && !confirm(confirmText)) {
+      return;
+    }
+    fetch('<?= zjmf_url('reserve/api/upgrade') ?>', {
+      method: 'POST',
+      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+      body: body.toString()
+    }).then(function (r) { return r.json(); }).then(function (res) {
+      var ok = res.code === 'ok' || res.success;
+      if (preview && ok) {
+        document.getElementById('zjf-price').textContent =
+          '本次升级需支付:¥' + res.price;
+        return;
+      }
+      show(res.code || '完成', ok);
+      if (ok) setTimeout(function () {
+        window.location.href = '<?= zjmf_url('reserve/hosts/' . (int)$host['id']) ?>';
+      }, 800);
+    }).catch(function () {
+      show('网络错误,请重试', false);
+    });
+  }
+
+  var previewId = kind === 'config' ? 'zjf-cfg-preview' : 'zjf-prod-preview';
+  var confirmId = kind === 'config' ? 'zjf-cfg-confirm' : 'zjf-prod-confirm';
+  var pBtn = document.getElementById(previewId);
+  var cBtn = document.getElementById(confirmId);
+  if (pBtn) pBtn.addEventListener('click', function () { post('1', ''); });
+  if (cBtn) cBtn.addEventListener('click', function () {
+    post('0', '确认按试算差额扣除余额并执行升级吗?');
+  });
+})();
+</script>
+<?php
+$content = ob_get_clean();
+include __DIR__ . '/layout.php';
diff --git a/docs/prd/zjmfmanager-reserve.md b/docs/prd/zjmfmanager-reserve.md
new file mode 100644
index 0000000..70933af
--- /dev/null
+++ b/docs/prd/zjmfmanager-reserve.md
@@ -0,0 +1,480 @@
+---
+title: MNBT 智简魔方(魔方财务)代理分销插件 PRD
+description: MNBT 代理分销魔方财务产品:商品同步加价、本地余额购买、代理商直通开通、主机管理与升降级
+---
+
+# MNBT 智简魔方(魔方财务)代理分销插件 PRD
+
+> 版本:v1.1(待评审)
+> 日期:2026-08-12
+> 状态:评审中
+> 关联文档:[插件开发手册](../development/plugin/guide.md)、[user_info](../development/plugin/builtin/user-info.md)、
+> [balance](../development/plugin/builtin/balance.md)、示例 SDK `app_plugins/zjmfmanager_reserve/example/`
+
+---
+
+## 1. 背景与目标
+
+### 1.1 背景
+
+MNBT 需以**代理商(Reseller)**身份对外销售一个或多个上游**智简魔方 / 魔方财务(cube_finance)**
+站点的产品。上游通过客户 API(`zjmf_api_login` + JWT)提供商品、下单、开通、主机管理能力,
+示例 SDK 已封装登录/商品/主机/升级等基础请求。
+
+本插件在 MNBT 侧以业务插件形式实现代理分销,依赖已内置的 `user_info`(用户认证)
+与 `balance`(余额支付)插件,复用其认证与支付能力。
+
+### 1.2 目标
+
+| # | 目标 |
+|---|------|
+| G1 | 管理员维护多个上游供应商(魔方财务站点 + 代理商 API 账号),按供应商同步商品并加价出售 |
+| G2 | 本地用户浏览商品(按供应商分组)、选择周期、用本地余额支付下单 |
+| G3 | 支付成功后由插件调用对应供应商接口为**代理商账号**直通开通主机,用户获得主机信息 |
+| G4 | 用户查看主机状态/流量,执行开关机/重启/重置密码/重装等操作 |
+| G5 | 用户提交配置升级 / 产品升降级,余额支付后同步到上游 |
+| G6 | 管理员管理供应商、订单与主机,记录操作日志 |
+
+### 1.3 范围
+
+| 期次 | 内容 |
+|------|------|
+| **P0(本期)** | 供应商管理(多上游);商品同步(弹窗选择)+ 手动添加 + 加价 + 上架;本地下单 + 余额支付 + 代理商直通开通;<br>用户主机查看(状态/流量)+ 基本操作(开关机/重启/重置密码/重装);配置升级 + 产品升降级(余额支付);管理员订单/主机管理;操作日志 |
+| **P1(下期)** | 上游余额/成本对账;定时任务(cron)自动同步商品与价格、批量状态刷新;监控用量图表;商品配置项(config options)可视化选择 |
+
+> 说明:P0 下单暂不支持上游商品复杂配置项的自定义(如需可手填备注参数,见 Q2),
+> 仅支持商品 + 计费周期的选购。
+
+---
+
+## 2. 总体架构
+
+### 2.1 架构图
+
+```
+┌────────────────────────────────────────────────────────────┐
+│                        MNBT(本系统)                         │
+│                                                             │
+│  用户(user_info 认证)                                        │
+│    │ 浏览/下单/余额支付(balance)                              │
+│    ▼                                                        │
+│  zjmfmanager_reserve 插件                                     │
+│    ├─ 供应商管理(多上游,各自独立 API 账号/JWT)               │
+│    ├─ 商品同步/加价(按供应商)                                │
+│    ├─ 本地订单 → 余额扣款 → 对应供应商开通(代理商直通)          │
+│    ├─ 主机查询 / provision 操作 / 升级                        │
+│    └─ 管理员:供应商、商品、订单、主机、日志                    │
+└──────────────┬───────────────────────────────┬─────────────┘
+               │ HTTPS + JWT                     │ HTTPS + JWT
+               ▼                                 ▼
+┌─────────────────────────────┐   ┌─────────────────────────────┐
+│   上游 魔方财务 A(供应商1)   │   │   上游 魔方财务 B(供应商2)   │
+│  zjmf_api_login / product   │   │  zjmf_api_login / product   │
+│  cart/set_config / host/*   │ … │  cart/set_config / host/*   │
+│  provision/* / upgrade/*    │   │  provision/* / upgrade/*    │
+└─────────────────────────────┘   └─────────────────────────────┘
+```
+
+**核心模型**:本地用户不直接接触上游客户体系。每个供应商使用各自配置的**代理商 API 账号**
+在上游完成开通与操作,主机归属对应供应商的代理商账号,本地用户持有主机账号密码。
+商品、订单、主机均记录 `supplier_id`,下单/操作/升级按供应商路由到对应客户端实例
+(每个供应商独立 JWT 缓存,互不串扰)。
+
+### 2.2 模块命名
+
+| 项 | 值 |
+|----|----|
+| 插件 slug | `zjmfmanager_reserve`(与目录名一致) |
+| 显示名 | 魔方财务代理分销 |
+| 依赖插件 | `user_info`、`balance`(`plugin.json` 用 `requires_plugins` 声明) |
+
+---
+
+## 3. 上游 API 约定
+
+### 3.1 认证
+
+- `POST {BASE}/zjmf_api_login`(username=客户用户名, password=API 密钥)→ 顶层 `jwt`
+- 后续请求头 `Authorization: Bearer {jwt}`
+- JWT 缓存约 2 小时;返回 `status=401/405` 时强制重登重试一次
+- **多供应商**:JWT 缓存 key 含 `supplier_id`,各供应商独立登录、独立缓存、互不串扰
+- 客户端类改编自 `example/sdk/CubeFinanceClient.php`,置于插件 `lib/` 下
+
+### 3.2 接口清单与映射
+
+| 场景 | 上游接口 | 插件方法(lib/upstream.php) |
+|------|----------|------------------------------|
+| 登录 | `POST zjmf_api_login` | `login()` |
+| 连通测试 | 登录 + 商品列表 | `testConnection()` |
+| 商品列表 | `GET api/product/list` | `productList()` |
+| 商品详情(代理价) | `GET api/product/{id}?price_basis=agent` | `productDetail($id)` |
+| 价格试算 | `GET cart/set_config` | `cartSetConfig($params)` |
+| 开通(下单) | 见 §3.3(Q1) | `purchase($params)` |
+| 主机头信息 | `GET host/header?host_id=` | `hostHeader($hostId)` |
+| 流量使用 | `GET host/trafficusage` | `hostTrafficUsage($hostId)` |
+| 主机操作 | `POST provision/default` | `hostAction($hostId, $func, $extra)` |
+| 配置升级页 | `GET upgrade/index/{hostId}` | `upgradeIndex($hostId)` |
+| 配置升级确认 | `GET upgrade/upgrade_config_page` | `upgradeConfigPage($hostId)` |
+| 提交配置升级 | `POST upgrade/upgrade_config_post` | `upgradeConfigPost($params)` |
+| 产品升降级选项 | `GET upgrade/upgrade_product/{hostId}` | `upgradeProduct($hostId)` |
+| 提交产品升降级 | `POST upgrade/upgrade_product_post` | `upgradeProductPost($params)` |
+| 余额抵扣(开放问题) | `POST apply_credit` | `applyCredit($params)` |
+
+### 3.3 开通(下单)流程
+
+代理商直通开通的**具体上游端点需联调确认**(魔方财务各版本/定制站存在差异):
+
+**方案 A(推荐,先按此实现)**:提交上游订单 → `apply_credit` 余额支付 → 上游自动开通。
+- 步骤:`cart/set_config` 试算 → 创建上游订单(端点待确认,Q1)→ `apply_credit` 抵扣支付
+  → 查询订单/主机获取 `host_id` 与账号密码。
+
+**方案 B(备选)**:若开通可由 provision 类接口直接触发,则 `provision/default` 携 `func=create`
+类动作创建主机。
+
+> 插件将上游调用收敛在 `ZjmfUpstream::purchase()` 单一方法内,后续仅需改该方法适配,
+> 不影响订单、支付、页面逻辑。默认实现按方案 A 编码,联调按实际站点调整。
+
+---
+
+## 4. 数据表设计
+
+### 4.1 `MN_plugin_zjmf_supplier` 上游供应商表(v1.1 新增)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | INT AI | 主键 |
+| name | VARCHAR(50) | 供应商名称(站点标识,可自定) |
+| api_url | VARCHAR(255) | 上游站点根地址 |
+| api_username | VARCHAR(64) | API 用户名 |
+| api_password | VARCHAR(255) | API 密钥(仅保存时写入,不回显) |
+| api_timeout | INT | 请求超时(秒),默认 30 |
+| markup_type | TINYINT | 该供应商加价方式:0=比例 1=固定(分) |
+| markup_value | BIGINT | 加价比例(千分比)或固定加价(分) |
+| status | TINYINT | 1=启用 0=停用(停用后商品不可售、不可操作) |
+| sort | INT | 排序 |
+| remark | VARCHAR(255) | 备注 |
+| created_at | DATETIME | 创建时间 |
+| updated_at | DATETIME | 更新时间 |
+
+> 加价规则按供应商独立配置(商品可单品覆盖);不再使用全局 option 加价。
+
+### 4.2 `MN_plugin_zjmf_product` 本地商品表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | INT AI | 主键 |
+| supplier_id | INT | 所属供应商 ID(v1.1 新增,联合唯一索引) |
+| up_product_id | INT | 上游商品 ID(与 supplier_id 联合唯一索引) |
+| name | VARCHAR(100) | 商品名 |
+| description | TEXT | 描述 |
+| currency | VARCHAR(10) | 货币代码(同步自上游) |
+| agent_price_cents | BIGINT | 上游代理价(分,最近同步) |
+| markup_type | TINYINT | 单品加价方式:0=比例 1=固定(分);0/0=未配置取供应商 |
+| markup_value | BIGINT | 加价比例(千分比)或固定加价(分) |
+| cycles | TEXT | 周期 JSON:`[{"cycle":"monthly","name":"月付","price_cents":12345}]`(本地售价) |
+| status | TINYINT | 1=上架 0=下架 |
+| sort | INT | 排序 |
+| synced_at | DATETIME | 最近同步时间 |
+| created_at | DATETIME | 创建时间 |
+| updated_at | DATETIME | 更新时间 |
+
+> 价格计算:本地售价 = 上游代理价 ×(1 + 比例%)或 + 固定加价,单位**分**整数存储。
+> 未单独配置加价时取所属供应商(`MN_plugin_zjmf_supplier.markup_type/value`)配置。
+
+### 4.3 `MN_plugin_zjmf_order` 本地订单表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | INT AI | 主键 |
+| order_no | VARCHAR(64) | 本地订单号(唯一,前缀 `ZJM`) |
+| supplier_id | INT | 所属供应商 ID(v1.1 新增,开通路由依据) |
+| user_id | INT | 本地用户 ID |
+| product_id | INT | 本地商品 ID |
+| up_product_id | INT | 上游商品 ID(冗余) |
+| product_name | VARCHAR(100) | 商品名(冗余) |
+| cycle | VARCHAR(20) | 计费周期(billingcycle) |
+| cycle_name | VARCHAR(20) | 周期文案(冗余) |
+| amount_cents | BIGINT | 本地售价(分) |
+| cost_cents | BIGINT | 上游成本价(分) |
+| order_params | TEXT | 下单参数 JSON(备注/配置项透传) |
+| up_order_id | INT | 上游订单 ID(开通后回填) |
+| up_host_id | INT | 上游主机 ID(开通后回填) |
+| username | VARCHAR(64) | 上游主机账号(冗余,展示脱敏) |
+| status | VARCHAR(20) | `pending/paid/opened/failed/cancelled` |
+| pay_time | DATETIME | 支付时间 |
+| opened_at | DATETIME | 开通时间 |
+| remark | VARCHAR(255) | 备注 |
+| created_at | DATETIME | 下单时间 |
+
+### 4.4 `MN_plugin_zjmf_host` 上游主机映射表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | INT AI | 主键 |
+| supplier_id | INT | 所属供应商 ID(v1.1 新增,操作/升级路由依据) |
+| user_id | INT | 本地用户 ID |
+| order_id | INT | 本地订单 ID |
+| up_host_id | INT | 上游主机 ID |
+| up_product_id | INT | 上游商品 ID |
+| name | VARCHAR(100) | 主机名 |
+| username | VARCHAR(64) | 上游账号 |
+| password | VARCHAR(255) | 上游密码(`authcode` 加密存储) |
+| cycle | VARCHAR(20) | 周期 |
+| status | VARCHAR(20) | 状态缓存:active/suspend/unknown |
+| renew_date | VARCHAR(20) | 到期日(缓存) |
+| created_at | DATETIME | 创建时间 |
+| updated_at | DATETIME | 更新时间 |
+
+### 4.5 `MN_plugin_zjmf_log` 操作日志表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | INT AI | 主键 |
+| user_id | INT | 本地用户 ID(可为 0=系统) |
+| supplier_id | INT | 供应商 ID(v1.1 新增,便于追溯) |
+| order_no | VARCHAR(64) | 关联订单号 |
+| action | VARCHAR(50) | 操作:sync/purchase/power/reset_password/upgrade/... |
+| result | VARCHAR(20) | success/failed |
+| content | TEXT | 详情 JSON(脱敏,不含密码/密钥) |
+| created_at | DATETIME | 时间 |
+
+> 建表语句写入 `install.sql`,删除语句写入 `uninstall.sql`,
+> 均用 `CREATE TABLE IF NOT EXISTS` / `DROP TABLE IF EXISTS`。
+> **迁移说明(v1.1)**:新增 `MN_plugin_zjmf_supplier` 表并在 product/order/host/log
+> 增加 `supplier_id` 字段属结构变更;插件未正式发布,安装环境需**卸载后重装**(数据清空)。
+> 若存在需保留数据的部署,另提供 `upgrade-1.1.sql` 做 `ALTER TABLE` + 默认供应商迁移。
+
+---
+
+## 5. 管理员端设计
+
+### 5.1 页面(`plugin.php?p=zjmfmanager_reserve&page=...`)
+
+| 页面 | page | 说明 |
+|------|------|------|
+| 供应商管理 | `suppliers` | 供应商列表(名称/站点/加价/启用/排序);新增/编辑弹窗(连接信息 + 加价配置);连通测试;停用后商品不可售 |
+| 商品管理 | `products` | 商品列表(所属供应商、上架状态、售价、同步时间);「同步商品」打开选择弹窗;「手动添加」表单;启停/编辑 |
+| 商品编辑 | `product_edit` | 单品加价(比例/固定)、排序、上架开关、查看上游代理价 |
+| 订单管理 | `orders` | 本地订单列表(供应商/状态筛选、按订单号/用户搜索) |
+| 主机管理 | `hosts` | 本地映射列表 + 上游主机详情查询入口 |
+
+### 5.2 供应商与商品同步
+
+**供应商管理**
+1. 新增供应商:名称、站点 URL、API 用户名、API 密钥、超时、加价方式/数值、启用、排序。
+2. 保存后可「连通测试」(登录 + 商品列表)验证凭证。
+3. 停用供应商后,其商品自动下架不可售、主机操作与升级拒绝执行(返回明确错误)。
+
+**商品同步(弹窗选择)**
+1. 商品管理页点击「同步商品」→ 弹出同步对话框。
+2. 对话框第一步选择供应商 → 拉取该供应商 `api/product/list` 列表(名称/描述/代理价)。
+3. 管理员勾选要同步的商品(默认全选,可过滤)→ 确认后逐条同步。
+4. 每个商品拉取 `api/product/{id}?price_basis=agent` 代理价,并按 `cart/set_config`
+   试算各周期价格(失败则跳过该周期,标记该商品「价格未同步」)。
+5. 同步策略(幂等):按 `supplier_id + up_product_id` upsert;仅新增或更新名称/描述/代理价,
+   **不覆盖**管理员自定义的加价、上架、排序。
+
+**手动添加商品**
+1. 商品管理页点击「手动添加」→ 表单:所属供应商、商品名称、上游商品 ID、描述。
+2. 保存后立即按该商品拉取上游代理价与各周期价格(依赖上游 `api/product/{id}` 详情接口,
+   失败则该商品标记「价格未同步」,可稍后再次同步)。
+3. 手动添加商品同样遵循单品加价配置。
+
+> 本地售价 = 代理价 × 加价(供应商或单品),写入 `cycles`。
+
+### 5.3 管理员 AJAX(`gn` 全部使用 `p_zjmf_admin_` 前缀)
+
+| gn | 说明 |
+|----|------|
+| `p_zjmf_admin_save_supplier` | 保存供应商(新增/编辑,密码留空不修改) |
+| `p_zjmf_admin_toggle_supplier` | 启用/停用供应商 |
+| `p_zjmf_admin_delete_supplier` | 删除供应商(有商品/主机时拒绝) |
+| `p_zjmf_admin_test_supplier` | 连通测试(登录 + 商品列表) |
+| `p_zjmf_admin_upstream_products` | 拉取指定供应商商品列表(供同步弹窗选择) |
+| `p_zjmf_admin_sync_products` | 按所选供应商 + 勾选商品 ID 列表同步 |
+| `p_zjmf_admin_save_product` | 保存单品加价/上架/排序 |
+| `p_zjmf_admin_toggle_product` | 上下架切换 |
+| `p_zjmf_admin_add_product` | 手动添加商品(供应商 + 上游产品 ID + 名称 + 描述) |
+| `p_zjmf_admin_order_list` | 订单分页列表 |
+| `p_zjmf_admin_host_list` | 主机分页列表 |
+| `p_zjmf_admin_fetch_host` | 拉取上游主机详情(header/流量) |
+
+---
+
+## 6. 用户端设计
+
+### 6.1 页面与路由(P2 通用路由,`index.php?_r=...` 或伪静态)
+
+| 页面 | 路由 | 说明 |
+|------|------|------|
+| 商品列表 | `GET /reserve/shop` | 上架商品卡片(价格/周期);**按供应商分组展示**(Tab 或分组区块,含供应商名/状态标识) |
+| 商品详情 | `GET /reserve/product/{id}` | 周期切换试算、下单按钮 |
+| 我的主机 | `GET /reserve/hosts` | 主机列表(状态/到期,含所属供应商标识) |
+| 主机详情 | `GET /reserve/hosts/{id}` | 状态、流量、操作按钮、升级入口 |
+| 我的订单 | `GET /reserve/orders` | 本地订单列表与状态 |
+
+API 路由:
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/reserve/api/create_order` | 创建订单并余额支付(核心,见 §7.1) |
+| POST | `/reserve/api/host_action` | 主机操作(on/off/reboot/reset_password/reinstall) |
+| POST | `/reserve/api/upgrade` | 提交配置升级 / 产品升降级(余额支付,见 §7.2) |
+| POST | `/reserve/api/refresh_price` | 前端周期价格试算(读本地售价,不上游) |
+
+> 辅助函数:`zjmf_url($path)`、`zjmf_admin_url($page)`、`zjmf_format_cents($cents)`、
+> `zjmf_require_user()`(包装 `user_info_auth_require`,输出统一布局)。
+
+### 6.2 下单支付流程
+
+```
+用户选择商品+周期 → 前端 POST /reserve/api/create_order
+  → zjmf_require_user()
+  → 校验商品上架、周期有效 → 写 MN_plugin_zjmf_order(status=pending)
+  → balance_deduct(user_id, amount, 'consume', order_no, 备注)   # 余额扣款
+  → 扣款成功 → 订单 status=paid → 触发开通(§7.1)
+  → 返回结果(成功/余额不足)
+```
+
+- 余额不足时 `balance_deduct` 返回 false → 订单置 `cancelled` → 提示充值。
+- 余额扣款成功后,若上游开通失败,订单置 `failed` 并**自动原路退回**余额
+  (`balance_add`,type=refund),日志记录完整链路。
+
+### 6.3 主机操作与升级
+
+- 操作(开关机/重启/重置密码/重装):调对应供应商 `provision/default`,参数含 `host_id` 与 `func`。
+  重装/重置密码等高危操作需二次确认;前端确认后调用。
+- 配置升级 / 产品升降级:先按主机所属供应商调 `upgrade/index`、`upgrade/upgrade_config_page` 或
+  `upgrade_product` 拉取选项与差额 → 前端确认 → 本地再建一条升级订单(`action` 标记,记录 supplier_id)
+  → `balance_deduct` 扣差额 → 调 `upgrade_config_post` / `upgrade_product_post` 提交对应上游
+  → 更新主机 `cycle/status/renew_date` 缓存。
+
+---
+
+## 7. 核心流程与钩子
+
+### 7.1 下单开通闭环
+
+```
+创建本地订单(pending, 含 supplier_id) → 余额扣款(balance_deduct)
+  → balance 插件触发 order.paid 钩子(与 hosting_shop 同模式,priority 默认 10)
+  → 本插件监听 order.paid(priority 20)
+      - 仅处理 order_no 前缀 ZJM 的订单,其余 return
+      - 幂等:status 已 paid/opened 直接 return
+      - 按订单 supplier_id 取对应供应商配置,调 ZjmfUpstream::purchase() 在上游开通
+      - 回填 up_order_id / up_host_id / username / password
+      - 写 MN_plugin_zjmf_host(含 supplier_id),订单 status=opened
+      - 失败:订单 status=failed + 原路退回余额 + 写日志
+```
+
+> 注:`order.paid` 由 balance 扣款链路触发(详见 balance 插件文档中 hosting_shop
+> 同款流程);支付插件(充值时)也会触发该钩子,本插件以订单号前缀严格过滤,
+> 避免误处理余额充值订单。供应商停用/缺失时开通返回明确错误并走失败退款路径。
+
+### 7.2 升级闭环
+
+```
+拉取升级选项/差额 → 前端确认 → 创建升级单(pending, action=upgrade_config|upgrade_product, 含 supplier_id)
+  → 余额扣款 → 按 supplier_id 提交对应上游 → 成功置 opened 并更新主机缓存 / 失败置 failed 并退余额
+```
+
+### 7.3 钩子清单
+
+| 钩子 | 优先级 | 说明 |
+|------|--------|------|
+| `order.paid` | 20 | 本地购买订单支付成功后触发上游开通(§7.1) |
+| `cron` | 10 | P1:定时同步商品价格、刷新主机状态 |
+
+---
+
+## 8. 状态映射
+
+### 8.1 上游主机状态 → 本地展示
+
+| 上游状态(host/header 返回值,以实际为准) | 本地展示 |
+|--------------------------------------------|----------|
+| 正常 / `qk=1` | 运行中 |
+| 暂停 / `qk=0` | 已暂停 |
+| 到期未续费 | 已到期 |
+| 查询失败 / 无数据 | 未知(缓存兜底) |
+
+### 8.2 本地订单状态
+
+`pending`(待支付)→ `paid`(已支付)→ `opened`(已开通);
+失败路径:`pending`→`cancelled`(未支付关闭)、`paid`→`failed`(开通失败,余额已退)。
+
+---
+
+## 9. 安全设计
+
+| 项 | 方案 |
+|----|------|
+| 上游凭证 | 存 `MN_plugin_zjmf_supplier.api_password`;编辑页密码框不回显;日志/响应脱敏 |
+| 主机密码 | `authcode()` 加密入库;页面默认打码,可「显示」二次确认 |
+| 用户侧鉴权 | 全部 API 走 `zjmf_require_user()`(user_info 认证) |
+| 管理侧鉴权 | 全部 AJAX 走 `mnbt_plugin_require_admin()` |
+| 出站请求 | 仅 `http(s)://`;JWT 缓存目录权限收敛;超时上限校验 |
+| AJAX 命名 | 管理员 `p_zjmf_admin_*`,避免与核心 gn 冲突 |
+| 越权 | 主机/订单查询均带 `user_id` 条件,无跨用户访问路径;供应商操作仅管理员 |
+| 订单号 | 前缀 `ZJM` + 时间 + 随机,全局唯一 |
+
+---
+
+## 10. 里程碑与验收
+
+### M1 — 供应商管理 + 上游服务层
+- [ ] `lib/CubeFinanceClient.php`(改编 SDK)+ `lib/upstream.php` 服务层(按供应商实例化,JWT 独立缓存)
+- [ ] 供应商管理页:新增/编辑/启停/删除、连接配置 + 加价配置、连通测试
+- **验收**:可添加多个供应商并分别连通测试;错误凭证返回明确错误;停用供应商后商品不可售。
+
+### M2 — 商品同步(弹窗选择/手动添加)+ 用户端商品与下单
+- [ ] 同步弹窗(选供应商 → 拉取列表 → 勾选同步)、手动添加商品、加价、上下架
+- [ ] 商品列表/详情页(按供应商分组)、`create_order`、余额支付闭环
+- **验收**:同步后本地售价 = 代理价 × 加价(供应商或单品);可仅同步勾选商品;
+  手动添加后价格同步成功;下单扣款成功;余额不足被拒。
+
+### M3 — 上游开通 + 主机管理
+- [ ] `purchase()` 开通(Q1 方案 A,按 supplier_id 路由);`MN_plugin_zjmf_host` 落库
+- [ ] 主机列表/详情、状态与流量查询、开关机/重启/重置密码/重装
+- **验收**:支付后对应供应商上游出现主机并回填 host_id;操作类功能状态同步变化;
+  开通失败自动退余额;停用供应商主机操作被拒。
+
+### M4 — 升级 + 管理员订单/主机 + 日志
+- [ ] 配置升级 / 产品升降级闭环(余额支付,按 supplier_id 路由)
+- [ ] 管理员订单/主机列表(供应商筛选)、拉取上游详情
+- [ ] 操作日志表完整记录
+- **验收**:升级扣差额并同步对应上游;订单/主机列表筛选正确;日志无明文密码/密钥。
+
+### M5 — 文档收尾
+- [ ] 插件 README、PRD 附录补充部署与联调用例;mdi 图标可用性核对
+
+---
+
+## 11. 决策点(待评审)
+
+| # | 决策 | 建议 |
+|---|------|------|
+| D1 | 开通模式 | 代理商直通开通(方案 A:上游下单 + apply_credit,Q1 确认端点) |
+| D2 | 定价 | 同步 + 加价;**按供应商独立配置加价**,商品可单品覆盖;单位分 |
+| D3 | 开通触发 | 复用 `order.paid` 钩子(priority 20),订单号前缀过滤 |
+| D4 | 开通失败处理 | 订单置 failed 并自动原路退回余额 |
+| D5 | 上游主机归属 | 全部归各供应商代理商 API 账号,本地不建上游客户 |
+| D6 | SDK 来源 | 改编 `example/sdk/CubeFinanceClient.php`,避免改动示例原文件 |
+| D7 | 多供应商(v1.1) | 新增 `MN_plugin_zjmf_supplier` 表;商品/订单/主机/日志记录 supplier_id;客户端按供应商实例化、JWT 独立缓存 |
+| D8 | 商品同步(v1.1) | 弹窗选择供应商与商品后同步;支持手动添加(基础信息 + 同步周期) |
+| D9 | 管理端图标 | 仅使用本项目 mdi 库中存在的图标(`mdi-cog` 不存在,改用 `mdi-settings`/`mdi-account-key` 等) |
+
+---
+
+## 12. 风险与开放问题
+
+| # | 风险/问题 | 影响 | 应对 |
+|---|-----------|------|------|
+| R1 | 上游「开通/下单」端点各版本差异 | 开通失败 | 收敛于 `ZjmfUpstream::purchase()`,联调按实际站点适配(§3.3) |
+| R2 | `order.paid` 触发链路与预期不符 | 支付后不开通 | 以 hosting_shop 同款 balance 扣款流程为准,联调验证;备选:create_order 内同步触发 |
+| R3 | 上游商品周期/价格口径差异 | 本地售价不准 | 以 `cart/set_config` 试算结果为准;失败标记「价格未同步」 |
+| R4 | 多供应商凭证/字段差异(v1.1) | 部分供应商不可用 | 供应商维度独立配置与缓存;连通测试逐供应商验证 |
+| Q1 | 上游创建订单的确切端点 | 影响 purchase() | 联调确认(`orders/checkout`/`cart/checkout` 等),默认方案 A 实现 |
+| Q2 | 商品配置项(config options)如何透传 | 影响下单参数 | P0 支持备注透传 `order_params`;可视化选择放 P1 |
+| Q3 | 上游 host/header 状态字段名 | 影响状态映射 | 以实际返回为准,§8.1 表联调校准 |
+| Q4 | 重置密码后上游主机密码回传时机 | 影响入库 | 开通/重置后主动查询 host/header 或订单回传字段 |