仰望星辰工作室

feat: 新增主机指派功能,完善主机管理与供应商管理能力

Y yang1145 提交于 2026-08-15 14:51 · 424e35f ·父提交 11c346c
feat: 新增主机指派功能,完善主机管理与供应商管理能力

本次提交完成多项核心功能升级与修复:
1.  新增管理员端主机指派页面,支持从上游供应商直接批量绑定已开通主机到指定用户
2.  新增供应商上游余额展示与自动拉取功能,支持手动刷新余额
3.  扩展主机状态枚举,新增待开通、已终止状态并适配对应样式
4.  重构主机脱敏逻辑,修复PHP8下解密崩溃问题,替换为空值兜底展示
5.  新增主机状态批量刷新功能,支持全量同步上游主机状态修复缓存
6.  完善CubeFinanceClient客户端API,新增购物车、DCIM管理等多组接口
7.  修复主机到期日期格式化问题,统一归一化上游返回的各类时间格式
8.  新增主机上游ID自动补齐逻辑,修复历史数据缺失up_host_id导致的功能异常
9.  前端页面新增供应商信息展示,优化主机列表展示逻辑
10. 修复路由异常时的错误处理逻辑,避免跳转到错误登录页
11 个文件变更 +2887 -169 13172193298@163.com
•MPHX/plugin.php +19 -1
•app_plugins/zjmfmanager_reserve/bootstrap.php +636 -18
•app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php +153 -1
•app_plugins/zjmfmanager_reserve/lib/upstream.php +1035 -85
•app_plugins/zjmfmanager_reserve/lib/zjmf.php +174 -18
•app_plugins/zjmfmanager_reserve/views/admin/assign.php +295 -0
•app_plugins/zjmfmanager_reserve/views/admin/hosts.php +44 -9
•app_plugins/zjmfmanager_reserve/views/admin/suppliers.php +40 -1
•app_plugins/zjmfmanager_reserve/views/host.php +483 -34
•app_plugins/zjmfmanager_reserve/views/hosts.php +1 -1
•templates/tdesign/spa/src/account/views/ZjmfAssetsView.vue +7 -1
变更内容
diff --git a/MPHX/plugin.php b/MPHX/plugin.php
index 249acc3..463db0a 100644
--- a/MPHX/plugin.php
+++ b/MPHX/plugin.php
@@ -1365,7 +1365,25 @@ function mnbt_plugin_dispatch_route()
 				while (ob_get_level() > $bufferLevel) ob_end_clean();
 				error_log('[MNBT plugin] route ' . $item['method'] . ' ' . $item['path'] . ' @' . ($item['plugin'] ?? '?') . ': ' . $e->getMessage());
 				$GLOBALS['mnbt_plugin_current'] = $prev;
-				continue;
+				// 路由已匹配但回调抛异常:渲染错误页终止请求,
+				// 不再静默 continue 落到默认行为(曾导致用户端被跳转到核心虚拟主机登录页)
+				if (!headers_sent()) {
+					http_response_code(500);
+					@header('Content-Type: text/html; charset=UTF-8');
+				}
+				echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>页面错误</title>'
+					. '<style>body{font-family:system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;background:#f2f3f5;color:#1a2e28;display:grid;place-items:center;min-height:100vh;margin:0}'
+					. '.box{max-width:680px;padding:32px 40px;background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,.06)}'
+					. 'h1{font-size:18px;margin:0 0 10px;color:#d54941}'
+					. 'p{font-size:14px;line-height:1.7;color:#555;margin:8px 0}'
+					. 'code{background:#f2f3f5;padding:2px 6px;border-radius:4px;font-size:12px}'
+					. 'pre{background:#fafbfc;border:1px solid #eee;border-radius:8px;padding:14px;overflow:auto;font-size:12px;line-height:1.6;color:#333}</style></head>'
+					. '<body><div class="box"><h1>页面处理出错</h1>'
+					. '<p>路由 <code>' . htmlspecialchars($item['method'] . ' ' . $item['path'], ENT_QUOTES, 'UTF-8')
+					. '</code> 处理异常,请稍后重试或联系管理员。</p>'
+					. '<pre>' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . '</pre>'
+					. '</div></body></html>';
+				exit;
 			}
 			$GLOBALS['mnbt_plugin_current'] = $prev;
 			if ($result === false) {
diff --git a/app_plugins/zjmfmanager_reserve/bootstrap.php b/app_plugins/zjmfmanager_reserve/bootstrap.php
index a0a7ab4..3401a92 100644
--- a/app_plugins/zjmfmanager_reserve/bootstrap.php
+++ b/app_plugins/zjmfmanager_reserve/bootstrap.php
@@ -88,6 +88,7 @@ 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', 'assign', 'views/admin/assign.php', '主机指派');
 mnbt_register_page('admin', 'logs', 'views/admin/logs.php', '操作日志');
 
 // 侧边栏菜单(三级结构)
@@ -100,6 +101,7 @@ mnbt_register_menu('admin', [
 		['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' => 'assign', 'icon' => 'mdi-gift', 'multitabs' => true],
 		['title' => '操作日志', 'page' => 'logs', 'icon' => 'mdi-clipboard-text', 'multitabs' => true],
 	],
 ]);
@@ -228,6 +230,23 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_test_supplier', function () {
 	json_exit_success($result['msg'] ?? '连接成功');
 });
 
+// 查询供应商上游余额(GET /user_info → data.credit)
+mnbt_register_ajax('admin', 'p_zjmf_admin_supplier_balance', function () {
+	mnbt_plugin_require_admin();
+	$supplier = zjmf_supplier_get((int)($_POST['id'] ?? 0));
+	if (!$supplier) {
+		json_exit_error('供应商不存在');
+	}
+	$result = ZjmfUpstream::balance($supplier);
+	if (empty($result['ok'])) {
+		json_exit_error($result['msg'] ?? '获取失败');
+	}
+	json_exit_success('ok', [
+		'credit'   => (string)($result['credit'] ?? ''),
+		'currency' => (string)($result['currency'] ?? ''),
+	]);
+});
+
 // 拉取供应商上游商品列表(供同步弹窗选择)
 mnbt_register_ajax('admin', 'p_zjmf_admin_upstream_products', function () {
 	mnbt_plugin_require_admin();
@@ -264,6 +283,237 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_upstream_products', function () {
 	]);
 });
 
+// 拉取当前账户(代理)已开通的主机列表(产品分配页用)
+// 来源:GET host/list,每台主机一行;标注本地已指派状态
+mnbt_register_ajax('admin', 'p_zjmf_admin_upstream_owned_products', function () {
+	mnbt_plugin_require_admin();
+	global $DB;
+	$supplier = zjmf_supplier_get((int)($_POST['id'] ?? 0));
+	if (!$supplier) {
+		json_exit_error('供应商不存在');
+	}
+
+	// 分页拉全量主机列表(limit 尽量大,翻页兜底;安全上限防死循环)
+	$items = [];
+	$sum = 0;
+	$maxPage = 1;
+	$page = 1;
+	$safety = 30;
+	while ($page <= $maxPage && $safety-- > 0) {
+		$res = ZjmfUpstream::hostList($supplier, ['page' => $page, 'limit' => 100]);
+		if (empty($res['ok'])) {
+			json_exit_error('拉取失败:' . (string)($res['msg'] ?? ''));
+		}
+		$data = $res['data'] ?? [];
+		foreach (($data['list'] ?? []) as $h) {
+			$items[] = $h;
+		}
+		$sum = (int)($data['sum'] ?? count($items));
+		$maxPage = (int)($data['max_page'] ?? 0);
+		if ($maxPage <= 0) {
+			$maxPage = 1;
+		}
+		if ($sum > 0 && count($items) >= $sum) {
+			break;
+		}
+		$page++;
+	}
+
+	// 本地已指派的主机集合(供应商 + 上游主机ID)
+	$assigned = [];
+	$rows = $DB->get_all_prepare(
+		"SELECT up_host_id FROM MN_plugin_zjmf_host
+		 WHERE supplier_id=? AND up_host_id>0",
+		[(int)$supplier['id']]
+	) ?: [];
+	foreach ($rows as $r) {
+		$assigned[(int)$r['up_host_id']] = true;
+	}
+
+	$list = [];
+	foreach ($items as $h) {
+		$upId = (int)($h['id'] ?? 0);
+		if ($upId <= 0) {
+			continue;
+		}
+		$list[] = [
+			'id'          => $upId,
+			'domain'      => (string)($h['domain'] ?? ''),
+			'productname' => (string)($h['productname'] ?? ''),
+			'status'      => (string)($h['domainstatus'] ?? ''),
+			'status_desc' => (string)($h['domainstatus_desc'] ?? $h['domainstatus'] ?? ''),
+			'dedicatedip' => (string)($h['dedicatedip'] ?? ''),
+			'cycle'       => (string)($h['cycle_desc'] ?? $h['billingcycle'] ?? ''),
+			'nextdue'     => zjmf_normalize_date((string)($h['nextduedate'] ?? '')),
+			'assigned'    => isset($assigned[$upId]),
+		];
+	}
+	json_exit_success('拉取成功', [
+		'list' => $list,
+	]);
+});
+
+// 产品分配:把上游已开通的机器直接指派给指定用户(本地绑定,不调上游购买)
+mnbt_register_ajax('admin', 'p_zjmf_admin_assign_host', function () {
+	mnbt_plugin_require_admin();
+	global $DB;
+
+	$supplier = zjmf_supplier_get((int)($_POST['supplier_id'] ?? 0));
+	if (!$supplier || (int)$supplier['status'] !== 1) {
+		json_exit_error('供应商不存在或已停用');
+	}
+	$user_id = (int)($_POST['user_id'] ?? 0);
+	$user = $DB->get_row_prepare(
+		"SELECT id, username, email FROM MN_plugin_user
+		 WHERE id=? AND status=1 LIMIT 1",
+		[$user_id]
+	);
+	if (!$user) {
+		json_exit_error('目标用户不存在或已禁用');
+	}
+
+	$upHostIds = [];
+	if (isset($_POST['up_host_id']) && is_array($_POST['up_host_id'])) {
+		foreach ($_POST['up_host_id'] as $v) {
+			$v = (int)$v;
+			if ($v > 0) {
+				$upHostIds[] = $v;
+			}
+		}
+	}
+	if ($upHostIds === []) {
+		json_exit_error('请至少勾选一台主机');
+	}
+
+	// 一次拉取上游主机列表,匹配勾选机器的详情(产品名/周期/状态/到期)
+	$upMap = [];
+	$res = ZjmfUpstream::hostList($supplier, ['page' => 1, 'limit' => 100]);
+	if (!empty($res['ok'])) {
+		foreach (($res['data']['list'] ?? []) as $h) {
+			$upMap[(int)($h['id'] ?? 0)] = $h;
+		}
+	}
+
+	$results = [];
+	foreach ($upHostIds as $upHostId) {
+		// 幂等:该上游主机已指派则跳过
+		$exist = $DB->get_row_prepare(
+			"SELECT id FROM MN_plugin_zjmf_host
+			 WHERE supplier_id=? AND up_host_id=? LIMIT 1",
+			[(int)$supplier['id'], $upHostId]
+		);
+		if ($exist) {
+			$results[] = [
+				'up_host_id' => $upHostId,
+				'domain'     => (string)($upMap[$upHostId]['domain'] ?? ''),
+				'ok'         => false,
+				'msg'        => '该机器已指派(本地主机 #' . (int)$exist['id'] . ')',
+				'host_id'    => (int)$exist['id'],
+			];
+			continue;
+		}
+
+		$upRow = $upMap[$upHostId] ?? [];
+		$name = (string)($upRow['productname'] ?? '');
+		$cycle = (string)($upRow['cycle_desc'] ?? $upRow['billingcycle'] ?? '');
+		$status = (string)($upRow['domainstatus'] ?? '');
+		$renew = (string)($upRow['nextduedate'] ?? '');
+		$account = '';
+		$password = '';
+		// 拉取上游主机头信息(账号/密码),失败不阻断
+		// 注意:host/header 返回字段为 domainstatus/nextduedate/billingcycle/productname
+		$info = ZjmfUpstream::hostInfo($supplier, $upHostId);
+		if (!empty($info['ok']) && is_array($info['data'])) {
+			$d = $info['data'];
+			$account = (string)($d['username'] ?? '');
+			$password = (string)($d['password'] ?? '');
+			if ($name === '') {
+				$name = (string)($d['productname'] ?? $d['name'] ?? '');
+			}
+			if ($cycle === '') {
+				$cycle = (string)($d['billingcycle_desc'] ?? $d['billingcycle'] ?? $d['cycle_desc'] ?? '');
+			}
+			if ($status === '') {
+				$status = (string)($d['domainstatus'] ?? $d['status'] ?? '');
+			}
+			if ($renew === '') {
+				$renew = (string)($d['nextduedate'] ?? $d['renew_date'] ?? '');
+			}
+		}
+		if ($name === '') {
+			$name = '上游主机#' . $upHostId;
+		}
+
+		// 创建指派订单(记录用,金额 0,直接标记已开通)
+		$product = [
+			'id'            => 0,
+			'supplier_id'   => (int)$supplier['id'],
+			'up_product_id' => (int)($upRow['pid'] ?? 0),
+			'name'          => $name,
+		];
+		$cycleCfg = ['name' => $cycle !== '' ? $cycle : 'Monthly', 'price_cents' => 0];
+		$order = zjmf_order_create($user, $product, $cycle, $cycleCfg, 'assign', [
+			'cost_cents'   => 0,
+			'up_host_id'   => $upHostId,
+			'order_params' => json_encode([
+				'assign_by'   => 'admin',
+				'assign_type' => 'existing_host',
+			], JSON_UNESCAPED_UNICODE),
+		]);
+		if (empty($order['ok'])) {
+			$results[] = [
+				'up_host_id' => $upHostId,
+				'domain'     => (string)($upRow['domain'] ?? ''),
+				'ok'         => false,
+				'msg'        => (string)($order['msg'] ?? '订单创建失败'),
+				'host_id'    => 0,
+			];
+			continue;
+		}
+		$order_id = (int)$order['order_id'];
+		zjmf_order_fill_opened($order_id, 0, $upHostId, $account);
+		zjmf_order_set_status($order_id, 'opened', '管理员指派');
+
+		// 写本地主机映射(绑定该上游机器)
+		$hostId = zjmf_host_create([
+			'supplier_id'   => (int)$supplier['id'],
+			'user_id'       => (int)$user['id'],
+			'order_id'      => $order_id,
+			'up_host_id'    => $upHostId,
+			'up_product_id' => (int)($upRow['pid'] ?? 0),
+			'name'          => $name,
+			'username'      => $account,
+			'password'      => $password !== '' ? zjmf_encrypt($password) : '',
+			'cycle'         => $cycle,
+			'status'        => zjmf_map_upstream_status($status),
+			'renew_date'    => zjmf_normalize_date($renew),
+		]);
+		if ($hostId <= 0) {
+			$results[] = [
+				'up_host_id' => $upHostId,
+				'domain'     => (string)($upRow['domain'] ?? ''),
+				'ok'         => false,
+				'msg'        => '本地主机映射写入失败',
+				'host_id'    => 0,
+			];
+			continue;
+		}
+
+		zjmf_log((int)$user['id'], (string)($order['order_no'] ?? ''), 'assign', 'success',
+			json_encode(['up_host_id' => $upHostId, 'assign_by' => 'admin'], JSON_UNESCAPED_UNICODE),
+			(int)$supplier['id']);
+
+		$results[] = [
+			'up_host_id' => $upHostId,
+			'domain'     => (string)($upRow['domain'] ?? ''),
+			'ok'         => true,
+			'msg'        => '指派成功',
+			'host_id'    => $hostId,
+		];
+	}
+	json_exit_success('处理完成', ['results' => $results]);
+});
+
 // 按所选供应商 + 勾选商品 ID 列表同步
 mnbt_register_ajax('admin', 'p_zjmf_admin_sync_products', function () {
 	mnbt_plugin_require_admin();
@@ -287,6 +537,157 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_sync_products', function () {
 	json_exit_success($result['msg'] ?? '同步完成');
 });
 
+// 产品分配:按关键词搜索可分配用户(user_info 独立用户表)
+mnbt_register_ajax('admin', 'p_zjmf_admin_search_users', function () {
+	mnbt_plugin_require_admin();
+	global $DB;
+	$kw = trim((string)($_POST['keyword'] ?? ''));
+	$list = [];
+	if ($kw !== '') {
+		$like = '%' . $kw . '%';
+		$list = $DB->get_all_prepare(
+			"SELECT id, username, email, qq, created_at
+			 FROM MN_plugin_user
+			 WHERE status=1 AND (username LIKE ? OR email LIKE ? OR id=?)
+			 ORDER BY id DESC LIMIT 20",
+			[$like, $like, (int)$kw]
+		) ?: [];
+	}
+	json_exit_success('ok', ['list' => $list]);
+});
+
+// 产品分配:同步勾选的上游商品并直接给指定用户开通(管理员代开,金额 0)
+mnbt_register_ajax('admin', 'p_zjmf_admin_assign_open', function () {
+	mnbt_plugin_require_admin();
+	global $DB;
+
+	$supplier = zjmf_supplier_get((int)($_POST['supplier_id'] ?? 0));
+	if (!$supplier || (int)$supplier['status'] !== 1) {
+		json_exit_error('供应商不存在或已停用');
+	}
+	$user_id = (int)($_POST['user_id'] ?? 0);
+	$user = $DB->get_row_prepare(
+		"SELECT id, username, email FROM MN_plugin_user
+		 WHERE id=? AND status=1 LIMIT 1",
+		[$user_id]
+	);
+	if (!$user) {
+		json_exit_error('目标用户不存在或已禁用');
+	}
+
+	$upIds = [];
+	if (isset($_POST['up_product_id']) && is_array($_POST['up_product_id'])) {
+		foreach ($_POST['up_product_id'] as $v) {
+			$v = (int)$v;
+			if ($v > 0) {
+				$upIds[] = $v;
+			}
+		}
+	}
+	if ($upIds === []) {
+		json_exit_error('请至少勾选一个产品');
+	}
+	// 周期可选:指定周期不存在时自动回退商品第一个可用周期
+	$cycle = trim((string)($_POST['cycle'] ?? ''));
+	if ($cycle !== '' && !isset(zjmf_cycles()[$cycle])) {
+		$cycle = '';
+	}
+
+	$results = [];
+	foreach ($upIds as $upId) {
+		$product = zjmf_product_get_by_up((int)$supplier['id'], $upId);
+		if (!$product) {
+			$sync = ZjmfUpstream::syncOneProductBySupplier($supplier, $upId);
+			if (empty($sync['ok'])) {
+				$results[] = [
+					'up_product_id' => $upId,
+					'name'          => '上游#' . $upId,
+					'ok'            => false,
+					'msg'           => '商品同步失败:' . (string)($sync['msg'] ?? ''),
+					'up_host_id'    => 0,
+				];
+				continue;
+			}
+			$product = zjmf_product_get_by_up((int)$supplier['id'], $upId);
+		}
+		if (!$product) {
+			$results[] = [
+				'up_product_id' => $upId,
+				'name'          => '上游#' . $upId,
+				'ok'            => false,
+				'msg'           => '商品入库后仍未找到',
+				'up_host_id'    => 0,
+			];
+			continue;
+		}
+
+		// 确定周期:管理员指定 > 商品第一个有价格的周期 > Monthly > 任意
+		$cycles = zjmf_product_cycles($product);
+		$pick = ($cycle !== '' && isset($cycles[$cycle])) ? $cycle : '';
+		if ($pick === '') {
+			foreach ($cycles as $k => $cfg) {
+				if ((int)($cfg['agent_price_cents'] ?? 0) > 0
+					|| (int)($cfg['price_cents'] ?? 0) > 0) {
+					$pick = $k;
+					break;
+				}
+			}
+		}
+		if ($pick === '') {
+			if (isset($cycles['Monthly'])) {
+				$pick = 'Monthly';
+			} else {
+				foreach ($cycles as $k => $v) {
+					$pick = $k;
+					break;
+				}
+			}
+		}
+		if ($pick === '') {
+			$results[] = [
+				'up_product_id' => $upId,
+				'name'          => (string)$product['name'],
+				'ok'            => false,
+				'msg'           => '商品无可用周期',
+				'up_host_id'    => 0,
+			];
+			continue;
+		}
+
+		// 管理员代开订单:金额 0,标记后直接开通(不扣用户本地余额)
+		$cycleCfg = [
+			'name'        => (string)($cycles[$pick]['name'] ?? $pick),
+			'price_cents' => 0,
+		];
+		$order = zjmf_order_create($user, $product, $pick, $cycleCfg, 'buy', [
+			'cost_cents'   => 0,
+			'order_params' => json_encode(['assign_by' => 'admin'], JSON_UNESCAPED_UNICODE),
+		]);
+		if (empty($order['ok'])) {
+			$results[] = [
+				'up_product_id' => $upId,
+				'name'          => (string)$product['name'],
+				'ok'            => false,
+				'msg'           => (string)($order['msg'] ?? '订单创建失败'),
+				'up_host_id'    => 0,
+			];
+			continue;
+		}
+		$order_id = (int)$order['order_id'];
+		zjmf_order_set_status($order_id, 'paid', '管理员代开');
+		$open = zjmf_open_host($order_id);
+
+		$results[] = [
+			'up_product_id' => $upId,
+			'name'          => (string)$product['name'],
+			'ok'            => !empty($open['ok']),
+			'msg'           => (string)($open['msg'] ?? '开通失败'),
+			'up_host_id'    => (int)($open['host_id'] ?? 0),
+		];
+	}
+	json_exit_success('处理完成', ['results' => $results]);
+});
+
 // 手动添加商品(供应商 + 上游商品 ID + 名称 + 描述)
 mnbt_register_ajax('admin', 'p_zjmf_admin_add_product', function () {
 	mnbt_plugin_require_admin();
@@ -641,16 +1042,22 @@ mnbt_register_route('GET', '/reserve/api/hosts', function ($params, $ctx) {
 	}
 	$hosts = [];
 	foreach (zjmf_host_list_by_user((int)$user['id']) as $h) {
+		// 缺失上游主机 ID 时尝试从上游主机列表补齐(同一次请求只拉一次上游列表)
+		$h = zjmf_backfill_host_upid($h);
+		$supplier = zjmf_supplier_get((int)$h['supplier_id']);
 		$hosts[] = [
 			'id'            => (int)$h['id'],
 			'up_host_id'    => (int)$h['up_host_id'],
+			'up_product_id' => (int)$h['up_product_id'],
 			'name'          => (string)$h['name'],
 			'username'      => (string)$h['username'],
 			'cycle'         => (string)$h['cycle'],
 			'status'        => (string)$h['status'],
-			'renew_date'    => (string)$h['renew_date'],
+			// 历史数据可能存了上游时间戳,统一归一化为 Y-m-d
+			'renew_date'    => zjmf_normalize_date((string)$h['renew_date']),
 			'created_at'    => (string)$h['created_at'],
 			'updated_at'    => (string)$h['updated_at'],
+			'supplier_name' => (string)($supplier['name'] ?? ''),
 		];
 	}
 	zjmf_json('ok', ['logged_in' => true, 'hosts' => $hosts]);
@@ -684,9 +1091,16 @@ mnbt_register_route('GET', '/reserve/orders', function ($params, $ctx) {
 // 我的主机列表
 mnbt_register_route('GET', '/reserve/hosts', function ($params, $ctx) {
 	$user = zjmf_require_user();
+	$hosts = zjmf_host_list_by_user((int)$user['id']);
+	// 列表页顺带补齐缺失的上游主机 ID(同一次请求只拉一次上游列表)
+	foreach ($hosts as $i => $h) {
+		$hosts[$i] = zjmf_backfill_host_upid($h);
+		// 历史数据可能存了上游时间戳,统一归一化为 Y-m-d
+		$hosts[$i]['renew_date'] = zjmf_normalize_date((string)$hosts[$i]['renew_date']);
+	}
 	zjmf_render('hosts', [
 		'page_title' => '我的主机',
-		'hosts'      => zjmf_host_list_by_user((int)$user['id']),
+		'hosts'      => $hosts,
 	]);
 });
 
@@ -701,25 +1115,75 @@ mnbt_register_route('GET', '/reserve/hosts/{host_id}', function ($params, $ctx)
 		return;
 	}
 
+	// 缺失上游主机 ID 时尝试补齐(开通结算未解析出 ID 的历史数据)
+	$host = zjmf_backfill_host_upid($host);
+	// 历史数据可能存了上游时间戳,统一归一化为 Y-m-d
+	$host['renew_date'] = zjmf_normalize_date((string)$host['renew_date']);
+
 	// 实时信息(失败不致命,仅展示缓存)
 	$info = ['ok' => false, 'msg' => ''];
 	$traffic = ['ok' => false, 'data' => []];
+	$dcim = ['ok' => false, 'data' => []];
+	$osList = [];
+	$osGroups = [];
+	$osError = '';
 	$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'];
+		try {
+			$info = ZjmfUpstream::hostInfo($supplier, (int)$host['up_host_id']);
+			$traffic = ZjmfUpstream::hostTraffic($supplier, (int)$host['up_host_id']);
+			// DCIM 信息(交换机端口/电源状态/重装次数/任务进度),非 DCIM 产品为空,失败不致命
+			$dcim = ZjmfUpstream::hostDcimInfo($supplier, (int)$host['up_host_id']);
+			// 重装系统列表(GET host/dedicatedserver?host_id=,实测可返回 cloud_os),
+			// 失败回退 host/product 的 dcim.os
+			$osList = is_array($info['dcim']['os'] ?? null) ? $info['dcim']['os'] : [];
+			$osGroups = [];
+			$osError = '';
+			$co = ZjmfUpstream::hostDedicatedOs($supplier, (int)$host['up_host_id']);
+			if (!empty($co['ok']) && $co['os_list'] !== []) {
+				$osList = $co['os_list'];
+				$osGroups = $co['groups'];
+			} else {
+				$osError = (string)($co['msg'] ?? '获取系统列表失败');
+			}
+			// 详情端点取不到状态时,从 host/list(含 domainstatus)按 ID 匹配兜底
+			if (($info['status'] ?? '') === 'unknown') {
+				$lr = ZjmfUpstream::hostList($supplier, ['limit' => 100]);
+				if (!empty($lr['ok'])) {
+					foreach (($lr['data']['list'] ?? []) as $it) {
+						if ((int)($it['id'] ?? 0) === (int)$host['up_host_id']) {
+							$mapped = zjmf_map_upstream_status((string)($it['domainstatus'] ?? ''));
+							if ($mapped !== 'unknown') {
+								$info['status'] = $mapped;
+								$info['data'] = is_array($info['data']) ? $info['data'] : [];
+								$info['data'] = array_merge($info['data'], $it);
+							}
+							break;
+						}
+					}
+				}
+			}
+			// 拉取成功后同步缓存状态(unknown 不上写,避免误覆盖)
+			if (!empty($info['ok']) && $info['status'] !== 'unknown' && $info['status'] !== $host['status']) {
+				zjmf_host_update_cache((int)$host['id'], ['status' => $info['status']]);
+				$host['status'] = $info['status'];
+			}
+		} catch (Throwable $e) {
+			error_log('[zjmfmanager_reserve] host detail upstream: ' . $e->getMessage());
 		}
 	}
 
 	zjmf_render('host', [
-		'page_title' => '主机详情:' . $host['name'],
-		'host'       => $host,
-		'info'       => $info,
-		'traffic'    => $traffic,
+		'page_title'    => '主机详情:' . $host['name'],
+		'host'          => $host,
+		'info'          => $info,
+		'traffic'       => $traffic,
+		'dcim'          => $dcim,
+		'config_options'=> is_array($info['config_options'] ?? null) ? $info['config_options'] : [],
+		'custom_fields' => is_array($info['custom_fields'] ?? null) ? $info['custom_fields'] : [],
+		'os_list'       => $osList,
+		'os_groups'     => $osGroups,
+		'os_error'      => $osError,
 	]);
 });
 
@@ -742,6 +1206,53 @@ mnbt_register_route('POST', '/reserve/api/host_action', function ($params, $ctx)
 	}
 	$supplier = zjmf_supplier_get((int)$host['supplier_id']);
 
+	// DCIM 专用操作(救援系统/重置BMC/取消任务)
+	$dcimActions = ['rescue', 'bmc', 'cancel_task'];
+	if (in_array($action, $dcimActions, true)) {
+		$extra = [];
+		$apiAction = $action;
+		if ($action === 'rescue') {
+			$system = (int)($_POST['system'] ?? 0);
+			if ($system !== 1 && $system !== 2) {
+				zjmf_json('请选择救援系统(1 Linux / 2 Windows)');
+			}
+			$extra['system'] = $system;
+		}
+		$result = ZjmfUpstream::hostDcimAction($supplier, (int)$host['up_host_id'], $apiAction, $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'] ?? '操作失败');
+		}
+		zjmf_json('ok', ['msg' => '操作成功']);
+	}
+
+	// 重装系统:POST /provision/default (func=reinstall),只需 os 与 os_group,无需密码/端口
+	if ($action === 'dcim_reinstall') {
+		$os = (int)($_POST['os'] ?? 0);
+		if ($os <= 0) {
+			zjmf_json('请选择操作系统');
+		}
+		$extra = ['os' => $os, 'code' => 0];
+		$osGroup = trim((string)($_POST['os_group'] ?? ''));
+		if ($osGroup !== '') {
+			$extra['os_group'] = $osGroup;
+		}
+		$result = ZjmfUpstream::hostAction($supplier, (int)$host['up_host_id'], 'reinstall', $extra);
+		$hostOrder = zjmf_order_get((int)$host['order_id']);
+		$orderNo = $hostOrder ? $hostOrder['order_no'] : '';
+		zjmf_log((int)$user['id'], $orderNo, 'host_action:dcim_reinstall',
+			empty($result['ok']) ? 'failed' : 'success', $result['msg'] ?? '',
+			(int)$host['supplier_id']);
+		if (empty($result['ok'])) {
+			zjmf_json($result['msg'] ?? '操作失败');
+		}
+		zjmf_json('ok', ['msg' => '操作成功']);
+	}
+
 	$func = zjmf_action_func($action);
 	if ($func === '') {
 		zjmf_json('不支持的操作');
@@ -931,7 +1442,9 @@ mnbt_register_route('POST', '/reserve/api/upgrade', function ($params, $ctx) {
 		$cache['up_product_id'] = (int)($_POST['newpid'] ?? 0);
 	}
 	$cache['cycle'] = (string)($_POST['billingcycle'] ?? $host['cycle']);
-	$cache['renew_date'] = (string)($preview['data']['renew_date'] ?? '');
+	$cache['renew_date'] = zjmf_normalize_date(
+		(string)($preview['data']['renew_date'] ?? '')
+	);
 	zjmf_host_update_cache((int)$host['id'], $cache);
 
 	zjmf_log((int)$user['id'], $order_no, $action, 'success',
@@ -966,11 +1479,116 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_fetch_host', function () {
 	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'] ?? '');
+	$cache = [];
+	if ($info['status'] !== 'unknown') {
+		$cache['status'] = $info['status'];
+	}
+	if (isset($info['data']['nextduedate']) || isset($info['data']['renewdate']) || isset($info['data']['renew_date'])) {
+		$cache['renew_date'] = zjmf_normalize_date(
+			(string)($info['data']['nextduedate'] ?? $info['data']['renew_date'] ?? $info['data']['renewdate'] ?? '')
+		);
+	}
+	if ($cache) {
+		zjmf_host_update_cache($id, $cache);
 	}
-	zjmf_host_update_cache($id, $cache);
 	json_exit_success('已刷新', ['status' => $info['status']]);
 });
+
+// 全部刷新主机状态(管理员):拉取 host/list 全量按 ID 匹配批量更新,
+// 修复存量 unknown/过期缓存(比逐台 hostInfo 快且可靠)
+mnbt_register_ajax('admin', 'p_zjmf_admin_fetch_all_hosts', function () {
+	mnbt_plugin_require_admin();
+
+	// 拉取本地全部主机
+	$hosts = [];
+	$page = 1;
+	$per = 200;
+	while (true) {
+		$res = zjmf_host_list_all($page, $per);
+		$list = $res['list'] ?? [];
+		if ($list === []) {
+			break;
+		}
+		foreach ($list as $h) {
+			$hosts[] = $h;
+		}
+		if ($page * $per >= (int)($res['total'] ?? 0)) {
+			break;
+		}
+		$page++;
+	}
+
+	// 按供应商拉取上游主机列表(分页全量)
+	$supplierIds = [];
+	foreach ($hosts as $h) {
+		$sid = (int)$h['supplier_id'];
+		if ($sid > 0) {
+			$supplierIds[$sid] = true;
+		}
+	}
+	$upMap = [];
+	foreach (array_keys($supplierIds) as $sid) {
+		$supplier = zjmf_supplier_get($sid);
+		if (!$supplier) {
+			continue;
+		}
+		$items = [];
+		$sum = 0;
+		$maxPage = 1;
+		$p = 1;
+		$safety = 30;
+		while ($p <= $maxPage && $safety-- > 0) {
+			$r = ZjmfUpstream::hostList($supplier, ['page' => $p, 'limit' => 100]);
+			if (empty($r['ok'])) {
+				break;
+			}
+			$d = $r['data'] ?? [];
+			foreach (($d['list'] ?? []) as $it) {
+				$items[(int)($it['id'] ?? 0)] = $it;
+			}
+			$sum = (int)($d['sum'] ?? count($items));
+			$maxPage = (int)($d['max_page'] ?? 1);
+			if ($maxPage <= 0) {
+				$maxPage = 1;
+			}
+			if ($sum > 0 && count($items) >= $sum) {
+				break;
+			}
+			$p++;
+		}
+		$upMap[$sid] = $items;
+	}
+
+	$stat = ['total' => count($hosts), 'ok' => 0, 'fail' => 0, 'no_up' => 0,
+		'missing' => 0, 'unknown' => 0, 'changed' => 0];
+	foreach ($hosts as $host) {
+		$upId = (int)$host['up_host_id'];
+		if ($upId <= 0) {
+			$stat['no_up']++;
+			continue;
+		}
+		$item = $upMap[(int)$host['supplier_id']][$upId] ?? null;
+		if (!$item) {
+			$stat['missing']++;
+			continue;
+		}
+		$status = zjmf_map_upstream_status((string)($item['domainstatus'] ?? ''));
+		$renew = zjmf_normalize_date((string)($item['nextduedate'] ?? ''));
+		$cache = [];
+		if ($status !== 'unknown' && $status !== (string)$host['status']) {
+			$cache['status'] = $status;
+			$stat['changed']++;
+		}
+		if ($renew !== '' && $renew !== (string)$host['renew_date']) {
+			$cache['renew_date'] = $renew;
+		}
+		if ($cache) {
+			zjmf_host_update_cache((int)$host['id'], $cache);
+		}
+		if ($status === 'unknown') {
+			$stat['unknown']++;
+		}
+		$stat['ok']++;
+	}
+	json_exit_success('刷新完成', $stat);
+});
diff --git a/app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php b/app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php
index d1c848c..4b6ea74 100644
--- a/app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php
+++ b/app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php
@@ -257,6 +257,65 @@ class CubeFinanceClient
 		return $this->get('cart/set_config', $params);
 	}
 
+	/**
+	 * 购物车计算总价(官方价格计算接口)。
+	 * POST /cart/get_total
+	 *
+	 * @param array $params 通常含 pid、billingcycle、qty、configoption 等
+	 * @return array
+	 */
+	public function cartGetTotal(array $params)
+	{
+		return $this->post('cart/get_total', $params);
+	}
+
+	/**
+	 * 购物车产品配置(含该产品可选周期/价格,用于探测真实 billingcycle 键)。
+	 * GET /cart/get_product_config?pid={pid}
+	 *
+	 * @param int $productId
+	 * @return array
+	 */
+	public function cartGetProductConfig($productId)
+	{
+		return $this->get('cart/get_product_config', [
+			'pid' => (int)$productId,
+		]);
+	}
+
+	/**
+	 * 清空购物车。
+	 * POST /cart/clear
+	 *
+	 * @return array
+	 */
+	public function cartClear()
+	{
+		return $this->post('cart/clear', []);
+	}
+
+	/**
+	 * 购物车页面数据(含购物车产品列表,数组键/序号即位置 i)。
+	 * GET /cart/get_shop_data
+	 *
+	 * @return array
+	 */
+	public function cartGetShopData()
+	{
+		return $this->get('cart/get_shop_data');
+	}
+
+	/**
+	 * 前台首页数据(含客户余额 data.client[].credit)。
+	 * GET /index
+	 *
+	 * @return array
+	 */
+	public function homeIndex()
+	{
+		return $this->get('index');
+	}
+
 	/**
 	 * 账户信息 / 余额。
 	 * GET user_info
@@ -277,7 +336,39 @@ class CubeFinanceClient
 	 */
 	public function hostHeader($hostId)
 	{
-		return $this->get('host/header', ['host_id' => intval($hostId)]);
+		return $this->get('host/header', ['host_id' => (int)$hostId]);
+	}
+
+	/** 产品内页数据(GET host/product),状态/账号等字段更稳定。 */
+	public function hostProduct($hostId)
+	{
+		return $this->get('host/product', ['host_id' => (int)$hostId]);
+	}
+
+	/**
+	 * 独服/云服务器详情(含重装可选系统列表 cloud_os / cloud_os_group)。
+	 * GET host/dedicatedserver?host_id={hostId}
+	 *
+	 * 实测 host/cloudos 无法返回系统列表,该端点是重装系统的可靠来源。
+	 *
+	 * @param int $hostId 上游主机 ID
+	 * @return array
+	 */
+	public function hostDedicatedServer($hostId)
+	{
+		return $this->get('host/dedicatedserver', ['host_id' => (int)$hostId]);
+	}
+
+	/**
+	 * 我的主机列表。
+	 * GET host/list
+	 *
+	 * @param array $extra 如 groupid/orderby/sort/show_type
+	 * @return array
+	 */
+	public function hostList(array $extra = [])
+	{
+		return $this->get('host/list', $extra);
 	}
 
 	/**
@@ -306,6 +397,56 @@ class CubeFinanceClient
 		return $this->post('provision/default', $params);
 	}
 
+	/* ============================================================
+	 *  DCIM 管理(服务器租用 / NAT 类产品)
+	 * ============================================================ */
+
+	/**
+	 * DCIM 模块操作(开机/关机/重启/重置BMC/救援/取消任务等)。
+	 * POST dcim/{action}?id={hostId}
+	 *
+	 * @param string $action 端点名(on/off/reboot/bmc/rescue/cancel_task/reinstall/crack_pass)
+	 * @param int    $hostId 上游主机 ID
+	 * @param array  $extra  额外参数
+	 * @return array
+	 */
+	public function dcimAction($action, $hostId, array $extra = [])
+	{
+		$data = array_merge(['id' => (int)$hostId], $extra);
+		return $this->post('dcim/' . $action, $data);
+	}
+
+	/** DCIM 产品详情(交换机/端口信息)。GET dcim/detail */
+	public function dcimDetail($hostId)
+	{
+		return $this->get('dcim/detail', ['id' => (int)$hostId]);
+	}
+
+	/** 单台电源状态。POST dcim/refresh_power_status */
+	public function dcimPowerStatus($hostId)
+	{
+		return $this->post('dcim/refresh_power_status', ['id' => (int)$hostId]);
+	}
+
+	/** 验证是否可以重装(本周次数/上限)。POST dcim/check_reinstall */
+	public function dcimCheckReinstall($hostId)
+	{
+		return $this->post('dcim/check_reinstall', ['id' => (int)$hostId]);
+	}
+
+	/** 重装/救援/重置密码进度。GET dcim/resintall_status */
+	public function dcimReinstallStatus($hostId)
+	{
+		return $this->get('dcim/resintall_status', ['id' => (int)$hostId]);
+	}
+
+	/** 用量信息。GET dcim/traffic_usage */
+	public function dcimTrafficUsage($hostId, array $extra = [])
+	{
+		$data = array_merge(['id' => (int)$hostId], $extra);
+		return $this->get('dcim/traffic_usage', $data);
+	}
+
 	/**
 	 * 配置升级页。
 	 * GET upgrade/index/{hostId}
@@ -541,6 +682,17 @@ class CubeFinanceClient
 			@unlink($file);
 		}
 	}
+
+	/**
+	 * 修改请求超时秒数(用户端实时查询用较短超时,避免拖垮整页加载)。
+	 *
+	 * @param int $seconds
+	 * @return void
+	 */
+	public function setTimeout($seconds)
+	{
+		$this->timeout = max(1, (int)$seconds);
+	}
 }
 
 }
diff --git a/app_plugins/zjmfmanager_reserve/lib/upstream.php b/app_plugins/zjmfmanager_reserve/lib/upstream.php
index d413136..5690c51 100644
--- a/app_plugins/zjmfmanager_reserve/lib/upstream.php
+++ b/app_plugins/zjmfmanager_reserve/lib/upstream.php
@@ -33,9 +33,10 @@ class ZjmfUpstream
 	 * JWT 缓存目录按供应商 ID 隔离,避免多供应商凭证串扰。
 	 *
 	 * @param array|null $supplier MN_plugin_zjmf_supplier 行
+	 * @param int|null   $timeout  覆盖超时秒数(null 用供应商配置 api_timeout)
 	 * @return CubeFinanceClient|null
 	 */
-	public static function client($supplier)
+	public static function client($supplier, $timeout = null)
 	{
 		if (!is_array($supplier)) {
 			return null;
@@ -46,6 +47,11 @@ class ZjmfUpstream
 		if ($apiUrl === '' || $username === '' || $password === '') {
 			return null;
 		}
+		$t = (int)($supplier['api_timeout'] ?? 30);
+		$t = $t > 0 ? $t : 30;
+		if ($timeout !== null && (int)$timeout > 0) {
+			$t = (int)$timeout;
+		}
 		$supplierId = (int)($supplier['id'] ?? 0);
 		$cacheDir = mnbt_plugin_path('zjmfmanager_reserve')
 			. 'runtime/cache/s' . $supplierId;
@@ -53,8 +59,7 @@ class ZjmfUpstream
 			'url'        => $apiUrl,
 			'username'   => $username,
 			'password'   => $password,
-			'timeout'    => (int)($supplier['api_timeout'] ?? 30) > 0
-				? (int)($supplier['api_timeout'] ?? 30) : 30,
+			'timeout'    => $t,
 			'cache_dir'  => $cacheDir,
 			'verify_ssl' => false,
 		]);
@@ -70,6 +75,67 @@ class ZjmfUpstream
 		return $client->testConnection();
 	}
 
+	/**
+	 * 查询供应商上游账户余额(GET /index → data.client[].credit)。
+	 * user_info 的 data.credit 是信用卡信息而非余额,故优先取首页客户数据,
+	 * 失败再兜底 user_info。
+	 *
+	 * @param array $supplier 供应商行
+	 * @return array ['ok'=>bool, 'msg'=>string, 'credit'=>string, 'currency'=>string]
+	 */
+	public static function balance($supplier)
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$res = $client->homeIndex();
+			if (self::respOk($res)) {
+				$clients = $res['data']['client'] ?? [];
+				// 兼容单对象(非数组)形态
+				if (is_array($clients) && !isset($clients[0]) && ($clients['credit'] ?? '') !== '') {
+					return [
+						'ok'       => true,
+						'msg'      => 'ok',
+						'credit'   => (string)$clients['credit'],
+						'currency' => (string)($clients['currency'] ?? ''),
+					];
+				}
+				if (is_array($clients)) {
+					foreach ($clients as $c) {
+						if (is_array($c) && ($c['credit'] ?? '') !== '') {
+							return [
+								'ok'       => true,
+								'msg'      => 'ok',
+								'credit'   => (string)$c['credit'],
+								'currency' => (string)($c['currency'] ?? ''),
+							];
+						}
+					}
+				}
+			}
+			// 兜底:GET /user_info(部分版本 credit 即余额)
+			$res = $client->userInfo();
+			if (!self::respOk($res)) {
+				return ['ok' => false, 'msg' => self::respErr('获取上游账户信息失败', $res)];
+			}
+			$data = $res['data'] ?? [];
+			$credit = $data['credit'] ?? null;
+			if ($credit === null || $credit === '') {
+				$credit = $data['credit_balance'] ?? null; // 兼容字段
+			}
+			return [
+				'ok'       => true,
+				'msg'      => 'ok',
+				'credit'   => (string)$credit,
+				'currency' => (string)($data['currency'] ?? ''),
+			];
+		} catch (CubeFinanceException $e) {
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
+
 	/* ============================================================
 	 *  商品同步
 	 * ============================================================ */
@@ -102,14 +168,15 @@ class ZjmfUpstream
 	 */
 	public static function syncOneProductBySupplier($supplier, $upProductId)
 	{
+		@set_time_limit(0);
 		$client = self::client($supplier);
 		if (!$client) {
 			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
 		}
 		try {
 			$upId = (int)$upProductId;
-			// 优先从列表取真实条目(含 billingcycle/price 等周期信息),失败再退回详情
-			$item = null;
+			// 优先从列表取真实条目(含 billingcycle/price 等周期信息,同 pid 可能每周期一行),失败再退回详情
+			$rows = [];
 			$currency = '';
 			try {
 				$res = $client->productList();
@@ -117,26 +184,25 @@ class ZjmfUpstream
 					$currency = (string)($res['data']['currency_code'] ?? '');
 					foreach ($res['data']['list'] as $row) {
 						if ((int)($row['id'] ?? 0) === $upId) {
-							$item = $row;
-							break;
+							$rows[] = $row;
 						}
 					}
 				}
 			} catch (CubeFinanceException $e) {
 				// 列表失败忽略,退回详情
 			}
-			if (!$item) {
+			if ($rows === []) {
 				$detail = $client->productDetail($upId, 'agent');
 				$prod = $detail['data']['product'] ?? ($detail['data'] ?? []);
-				$item = [
+				$rows = [[
 					'id'          => $upId,
 					'name'        => (string)($prod['name'] ?? ''),
 					'description' => (string)($prod['description'] ?? ''),
-				];
+				]];
 				$currency = (string)($detail['data']['currency_code'] ?? $currency);
 			}
 			$ok = self::syncOneProduct(
-				$client, (int)($supplier['id'] ?? 0), $upId, $item, $currency
+				$client, (int)($supplier['id'] ?? 0), $upId, $rows, $currency
 			);
 			return $ok
 				? ['ok' => true, 'msg' => '商品价格已同步']
@@ -176,6 +242,7 @@ class ZjmfUpstream
 	 */
 	public static function syncProducts($supplier, array $upIds = [])
 	{
+		@set_time_limit(0);
 		$client = self::client($supplier);
 		if (!$client) {
 			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
@@ -190,6 +257,8 @@ class ZjmfUpstream
 			$count = 0;
 			$fail = 0;
 			$skip = 0;
+			// 上游列表可能每个周期一行(同 pid 多行),按 pid 分组后合并周期,避免相互覆盖
+			$grouped = [];
 			foreach ($res['data']['list'] as $item) {
 				$upId = (int)($item['id'] ?? 0);
 				if ($upId <= 0) {
@@ -199,7 +268,10 @@ class ZjmfUpstream
 					$skip++;
 					continue;
 				}
-				if (self::syncOneProduct($client, $supplierId, $upId, $item, $currency)) {
+				$grouped[$upId][] = $item;
+			}
+			foreach ($grouped as $upId => $rows) {
+				if (self::syncOneProduct($client, $supplierId, (int)$upId, $rows, $currency)) {
 					$count++;
 				} else {
 					$fail++;
@@ -215,11 +287,12 @@ class ZjmfUpstream
 		}
 	}
 
-	/** 同步单个商品。 */
-	protected static function syncOneProduct($client, $supplierId, $upId, $item, $currency)
+	/** 同步单个商品(rows 为同一 pid 的列表行,可能每周期一行)。 */
+	protected static function syncOneProduct($client, $supplierId, $upId, array $rows, $currency)
 	{
 		global $DB, $date;
 		$now = $date ?: date('Y-m-d H:i:s');
+		$item = $rows[0] ?? [];
 
 		// 代理价(详情接口,失败回退列表项 price)
 		$agentCents = 0;
@@ -232,31 +305,79 @@ class ZjmfUpstream
 			// 详情失败不致命,仅代理价缺失
 		}
 		if ($agentCents <= 0) {
-			$agentCents = self::itemPrice($item);
-		}
-
-		// 各周期价:优先读上游返回的 cycle 字段(列表项 → 详情),均无则回退购物车试算
-		$cycles = self::parseItemCycles($item);
-		if ($cycles === []) {
-			$cycles = $detail ? self::parseDetailCycles($detail) : [];
-		}
-		if ($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'             => $cycle,
-							'name'              => $cfg['name'],
-							'agent_price_cents' => $price,
-						];
-					}
-				} catch (CubeFinanceException $e) {
-					continue;
+			foreach ($rows as $row) {
+				$agentCents = self::itemPrice($row);
+				if ($agentCents > 0) {
+					break;
+				}
+			}
+		}
+
+		// 各周期价:列表行 → 详情 → 购物车算价,逐级补充合并(列表常只有单周期字段)
+		$cycles = [];
+		foreach ($rows as $row) {
+			foreach (self::parseItemCycles($row) as $k => $entry) {
+				if (!isset($cycles[$k])) {
+					$cycles[$k] = $entry;
+				}
+			}
+		}
+		if ($detail) {
+			foreach (self::parseDetailCycles($detail) as $k => $entry) {
+				if (!isset($cycles[$k])) {
+					$cycles[$k] = $entry;
 				}
 			}
 		}
+		// 探测购物车配置接口:一次请求拿到全部可选周期的真实 billingcycle 键与价格。
+		// 列表/详情已有周期保持不变;仅补充缺失周期,并给缺失 up_cycle 的条目补上真实键。
+		$probedKeys = [];
+		foreach (self::probeCycles($client, $upId, $detail) as $key => $cfg) {
+			$entry = self::cycleEntry($key, (int)($cfg['price_cents'] ?? 0), (string)($cfg['name'] ?? ''));
+			$k = $entry['cycle'];
+			$probedKeys[$k] = (string)$key;
+			if (isset($cycles[$k])) {
+				if ((string)($cycles[$k]['up_cycle'] ?? '') === '') {
+					$cycles[$k]['up_cycle'] = (string)$key;
+				}
+				continue;
+			}
+			if ($entry['agent_price_cents'] <= 0) {
+				continue;
+			}
+			$cycles[$k] = $entry;
+		}
+		// 兜底:购物车算价补充缺失周期。先探测一个周期,失败即放弃该商品试算
+		//(避免对每个缺失周期都发请求,商品多时同步被拖垮)
+		$trialOn = false;
+		foreach (zjmf_cycles() as $cycle => $cfg) {
+			if (isset($cycles[$cycle])) {
+				continue;
+			}
+			if (!$trialOn) {
+				$probe = self::trialPrice($client, $upId, $cycle);
+				if ($probe <= 0) {
+					break;
+				}
+				$trialOn = true;
+				$cycles[$cycle] = [
+					'cycle'             => $cycle,
+					'up_cycle'          => $probedKeys[$cycle] ?? strtolower($cycle),
+					'name'              => $cfg['name'],
+					'agent_price_cents' => $probe,
+				];
+				continue;
+			}
+			$price = self::trialPrice($client, $upId, $cycle);
+			if ($price > 0) {
+				$cycles[$cycle] = [
+					'cycle'             => $cycle,
+					'up_cycle'          => $probedKeys[$cycle] ?? strtolower($cycle),
+					'name'              => $cfg['name'],
+					'agent_price_cents' => $price,
+				];
+			}
+		}
 		$cyclesJson = json_encode(array_values($cycles), JSON_UNESCAPED_UNICODE);
 
 		$existing = zjmf_product_get_by_up($supplierId, $upId);
@@ -367,31 +488,79 @@ class ZjmfUpstream
 		}
 
 		try {
+			// 0. 清空购物车:该版本 settle(checkout=1) 会结算整辆购物车,
+			//    若残留历史测试商品会把多件一起结算开通(曾实测一次开出多台机器)。
+			//    先清空保证本次结算只涉及刚添加的这一件商品。
+			try {
+				$client->cartClear();
+			} catch (CubeFinanceException $e) {
+				// 清空失败不致命,继续尝试(可能购物车本就为空)
+			}
+
 			// 1. 添加产品至购物车(官方:POST /cart/add_to_shop)→ data.i 购物车位置
+			$upCycle = self::upstreamCycle((int)($order['supplier_id'] ?? 0), $upProductId, $cycle);
 			$addParams = [
 				'pid'          => $upProductId,
-				'billingcycle' => $cycle,
+				'billingcycle' => $upCycle,
 				'qty'          => 1,
 				'host'         => (string)($extra['host'] ?? self::randHost()),
 				'password'     => (string)($extra['password'] ?? self::randPassword()),
 			];
-			foreach (['configoption', 'customfield', 'serverid', 'os'] as $k) {
-				if (isset($extra[$k])) {
+			foreach (['configoption', 'customfield', 'serverid', 'os', 'currencyid'] as $k) {
+				if (isset($extra[$k]) && $extra[$k] !== '') {
 					$addParams[$k] = $extra[$k];
 				}
 			}
+			// 缺少 currencyid / configoption 时探测上游默认配置:
+			// 有配置项的商品必须传 configoption(子项ID/数量),否则计价失败报
+			// "此周期未配置价格或价格错误,请重新选择周期"(实测上游前台抓包确认)
+			if (!isset($addParams['currencyid']) || !isset($addParams['configoption'])) {
+				$cfgDefaults = self::probeConfigDefaults($client, $upProductId);
+				if (!isset($addParams['currencyid']) && !empty($cfgDefaults['currencyid'])) {
+					$addParams['currencyid'] = (int)$cfgDefaults['currencyid'];
+				}
+				if (!isset($addParams['configoption']) && !empty($cfgDefaults['configoption'])) {
+					$addParams['configoption'] = $cfgDefaults['configoption'];
+				}
+			}
 			$res = $client->post('cart/add_to_shop', $addParams);
 			if (!self::respOk($res)) {
-				return ['ok' => false, 'msg' => self::respErr('上游添加购物车失败', $res)];
+				// 上游拒绝(多为"此周期未配置价格"):探测商品真实周期键,命中则重试一次,
+				// 并把正确键写回商品周期(后续购买直接可用)
+				$avail = [];
+				if (self::isCyclePriceError($res)) {
+					$avail = self::probeCycles($client, $upProductId);
+					$matched = self::matchUpCycle($cycle, $avail);
+					if ($matched !== '' && $matched !== $upCycle) {
+						$addParams['billingcycle'] = $matched;
+						self::persistUpCycle(
+							(int)($order['supplier_id'] ?? 0),
+							$upProductId,
+							$cycle,
+							$matched
+						);
+						$upCycle = $matched;
+						$res = $client->post('cart/add_to_shop', $addParams);
+					}
+				}
+				if (!self::respOk($res)) {
+					$msg = self::respErr('上游添加购物车失败(周期: ' . $upCycle . ')', $res);
+					if ($avail !== []) {
+						$msg .= self::formatAvailable($avail);
+					}
+					return ['ok' => false, 'msg' => $msg];
+				}
 			}
-			$position = -1;
-			if (is_array($res['data'] ?? null)) {
-				$position = (int)($res['data']['i'] ?? -1);
-			} elseif (is_numeric($res['data'] ?? null)) {
-				$position = (int)$res['data'];
+			$position = self::findPosition($res);
+			if ($position < 0) {
+				// 该版本 add_to_shop 不返回位置(响应仅 status/msg/is_aff):
+				// 从购物车数据(GET /cart/get_shop_data,数组键/序号即位置 i)定位刚添加的产品
+				$position = self::cartPosition($client, $upProductId, $upCycle);
 			}
 			if ($position < 0) {
-				return ['ok' => false, 'msg' => '上游添加购物车未返回位置,无法结算'];
+				$raw = json_encode($res['data'] ?? $res, JSON_UNESCAPED_UNICODE);
+				return ['ok' => false, 'msg' => '上游添加购物车后无法定位购物车位置(响应: '
+					. self::truncate((string)$raw, 300) . ')'];
 			}
 
 			// 2. 结算购物车(官方:POST /cart/settle,checkout=1 直接结算)→ data.invoiceid
@@ -469,30 +638,61 @@ class ZjmfUpstream
 	 *  主机查询与操作
 	 * ============================================================ */
 
-	/** 主机头信息(状态/账号/到期),按主机所属供应商。 */
-	public static function hostInfo($supplier, $upHostId)
+	/** 拉取上游我的主机列表(GET host/list),按供应商。用户端读取用短超时,避免拖垮页面。 */
+	public static function hostList($supplier, array $extra = [])
 	{
-		$client = self::client($supplier);
+		$client = self::client($supplier, 8);
 		if (!$client) {
 			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
 		}
 		try {
-			$res = $client->hostHeader((int)$upHostId);
-			if (($res['status'] ?? 0) != 200) {
-				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '查询失败')];
+			$res = $client->hostList($extra);
+			if (!self::respOk($res)) {
+				return ['ok' => false, 'msg' => self::respErr('获取上游主机列表失败', $res)];
 			}
-			$data = $res['data'] ?? [];
-			$data = self::pickHostData($data);
-			return ['ok' => true, 'data' => $data, 'status' => self::mapHostStatus($data)];
+			return ['ok' => true, 'data' => $res['data'] ?? []];
 		} catch (CubeFinanceException $e) {
 			return ['ok' => false, 'msg' => $e->getMessage()];
 		}
 	}
 
-	/** 流量使用,按主机所属供应商。 */
+	/** 主机头信息(状态/账号/到期),按主机所属供应商。用户端读取用短超时,避免拖垮页面。 */
+	public static function hostInfo($supplier, $upHostId)
+	{
+		$client = self::client($supplier, 8);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		// host/product 优先(状态等字段更稳定),失败回退 host/header
+		foreach (['hostProduct', 'hostHeader'] as $method) {
+			try {
+				$res = $client->{$method}((int)$upHostId);
+				if (($res['status'] ?? 0) == 200) {
+					$resData = $res['data'] ?? [];
+					$data = self::pickHostData($resData);
+					return [
+						'ok'             => true,
+						'data'           => $data,
+						'config_options' => isset($resData['config_options']) && is_array($resData['config_options'])
+							? $resData['config_options'] : [],
+						'custom_fields'  => isset($resData['custom_field_data']) && is_array($resData['custom_field_data'])
+							? $resData['custom_field_data'] : [],
+						'dcim'           => isset($resData['dcim']) && is_array($resData['dcim'])
+							? $resData['dcim'] : [],
+						'status'         => self::mapHostStatus($data),
+					];
+				}
+			} catch (CubeFinanceException $e) {
+				// 尝试下一个端点
+			}
+		}
+		return ['ok' => false, 'msg' => '查询失败'];
+	}
+
+	/** 流量使用,按主机所属供应商。用户端读取用短超时,避免拖垮页面。 */
 	public static function hostTraffic($supplier, $upHostId)
 	{
-		$client = self::client($supplier);
+		$client = self::client($supplier, 8);
 		if (!$client) {
 			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
 		}
@@ -530,9 +730,93 @@ class ZjmfUpstream
 		}
 	}
 
-	/* ============================================================
-	 *  升级(配置升级 / 产品升降级)
-	 * ============================================================ */
+	/**
+	 * DCIM 综合信息(交换机端口 / 电源状态 / 重装次数 / 任务进度)。
+	 * 单项失败不致命,仅用于展示;非 DCIM 产品会全部为空。
+	 */
+	public static function hostDcimInfo($supplier, $upHostId)
+	{
+		// 短超时,避免叠加请求拖慢详情页
+		$client = self::client($supplier, 5);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		$out = [
+			'detail'    => [],
+			'power'     => '',
+			'power_msg' => '',
+			'reinstall' => [],
+			'task'      => [],
+		];
+		$tasks = [
+			'detail'    => function () use ($client, $upHostId) { return $client->dcimDetail((int)$upHostId); },
+			'power'     => function () use ($client, $upHostId) { return $client->dcimPowerStatus((int)$upHostId); },
+			'reinstall' => function () use ($client, $upHostId) { return $client->dcimCheckReinstall((int)$upHostId); },
+			'task'      => function () use ($client, $upHostId) { return $client->dcimReinstallStatus((int)$upHostId); },
+		];
+		foreach ($tasks as $k => $fn) {
+			try {
+				$res = $fn();
+				if (($res['status'] ?? 0) == 200) {
+					$d = $res['data'] ?? [];
+					if ($k === 'power') {
+						$out['power']     = (string)($d['power'] ?? '');
+						$out['power_msg'] = (string)($d['msg'] ?? '');
+					} else {
+						$out[$k] = $d;
+					}
+				}
+			} catch (CubeFinanceException $e) {
+				// 单项失败忽略
+			}
+		}
+		return ['ok' => true, 'data' => $out];
+	}
+
+	/** DCIM 专用操作(rescue/bmc/cancel_task/reinstall/crack_pass 等)。 */
+	public static function hostDcimAction($supplier, $upHostId, $action, $extra = [])
+	{
+		$client = self::client($supplier);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$res = $client->dcimAction($action, (int)$upHostId, $extra);
+			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()];
+		}
+	}
+
+	/** 重装可选系统列表(GET host/dedicatedserver),按上游主机 ID 获取。 */
+	public static function hostDedicatedOs($supplier, $upHostId)
+	{
+		$client = self::client($supplier, 8);
+		if (!$client) {
+			return ['ok' => false, 'msg' => '供应商连接信息不完整'];
+		}
+		try {
+			$res = $client->hostDedicatedServer((int)$upHostId);
+			if (($res['status'] ?? 0) != 200) {
+				error_log('[zjmfmanager_reserve] hostDedicatedServer failed: '
+					. 'host_id=' . (int)$upHostId . ' status=' . ($res['status'] ?? '')
+					. ' msg=' . ($res['msg'] ?? ''));
+				return ['ok' => false, 'msg' => (string)($res['msg'] ?? '获取系统列表失败')];
+			}
+			$data = $res['data'] ?? [];
+			return [
+				'ok'      => true,
+				'os_list' => is_array($data['cloud_os'] ?? null) ? $data['cloud_os'] : [],
+				'groups'  => is_array($data['cloud_os_group'] ?? null) ? $data['cloud_os_group'] : [],
+			];
+		} catch (CubeFinanceException $e) {
+			error_log('[zjmfmanager_reserve] hostDedicatedServer exception: ' . $e->getMessage());
+			return ['ok' => false, 'msg' => $e->getMessage()];
+		}
+	}
 
 	/** 只读获取升级选项(配置项或可升降级产品列表)。 */
 	public static function upgradeOptions($supplier, $upHostId, $kind)
@@ -637,13 +921,57 @@ class ZjmfUpstream
 	 *  响应解析辅助(防御式,字段以实际上游返回为准)
 	 * ============================================================ */
 
-	/** 从响应 data 中提取金额(分),常见字段/嵌套结构兜底。 */
+	/**
+	 * 购物车算价:优先官方价格计算接口 POST /cart/get_total,
+	 * 失败回退配置页 GET /cart/set_config。
+	 */
+	protected static function trialPrice($client, $upId, $cycle)
+	{
+		$bc = strtolower($cycle);
+		try {
+			$price = self::parseTrialPrice($client->cartGetTotal([
+				'pid'          => $upId,
+				'billingcycle' => $bc,
+				'qty'          => 1,
+			]));
+			if ($price > 0) {
+				return $price;
+			}
+		} catch (CubeFinanceException $e) {
+			// 忽略,走回退
+		}
+		try {
+			return self::parseTrialPrice($client->cartSetConfig([
+				'pid'          => $upId,
+				'billingcycle' => $bc,
+			]));
+		} catch (CubeFinanceException $e) {
+			return 0;
+		}
+	}
+
 	protected static function parseTrialPrice($res)
 	{
 		$data = $res['data'] ?? [];
 		if (!is_array($data)) {
 			return 0;
 		}
+		// 官方 get_total 响应:data.products[](signal_price 为单个产品周期费用)
+		if (isset($data['products']) && is_array($data['products'])) {
+			foreach ($data['products'] as $p) {
+				if (!is_array($p)) {
+					continue;
+				}
+				foreach (['signal_price', 'product_price', 'price_total', 'total', 'amount', 'subtotal'] as $k) {
+					if (isset($p[$k]) && $p[$k] !== '' && $p[$k] !== null) {
+						$c = self::toCents($p[$k]);
+						if ($c > 0) {
+							return $c;
+						}
+					}
+				}
+			}
+		}
 		foreach (['price', 'money', 'total', 'amount', 'subtotal', 'renewal_price'] as $k) {
 			if (isset($data[$k]) && $data[$k] !== '' && $data[$k] !== null) {
 				$c = self::toCents($data[$k]);
@@ -678,16 +1006,36 @@ class ZjmfUpstream
 	}
 
 	/**
-	 * 从列表项提取各周期价格(分)。
-	 * 魔方财务列表项为「单周期」结构:billingcycle + price + billingcycle_zh(cycle 只是标签字符串)。
-	 * 兼容完整周期结构(cycle/pricing 为数组或映射),解析失败返回空数组。
+	 * 从列表行提取各周期价格(分)。
+	 * 兼容三种结构(优先完整结构,避免单周期字段提前返回漏掉其余周期):
+	 *   a. 完整周期:cycle/pricing 为数组或映射
+	 *   b. 并行数组:billingcycle[] + billingcycle_price[](+ billingcycle_zh[])
+	 *   c. 单周期:billingcycle + price + billingcycle_zh(仅作最后兜底)
+	 * 解析失败返回空数组。
 	 */
 	protected static function parseItemCycles($item)
 	{
 		if (!is_array($item)) {
 			return [];
 		}
-		// 单周期结构:billingcycle + price + billingcycle_zh
+		// a. 完整周期结构:cycle/pricing 为数组或映射
+		$raw = $item['cycle'] ?? $item['pricing'] ?? null;
+		if (is_string($raw)) {
+			$decoded = json_decode($raw, true);
+			$raw = is_array($decoded) ? $decoded : null;
+		}
+		if (is_array($raw) && $raw !== []) {
+			$out = self::parseCycleMap($raw);
+			if ($out !== []) {
+				return $out;
+			}
+		}
+		// b. 并行数组结构:billingcycle[] + billingcycle_price[](v10 列表常见)
+		$out = self::parseParallelCycles($item);
+		if ($out !== []) {
+			return $out;
+		}
+		// c. 单周期结构:billingcycle + price + billingcycle_zh
 		$key = (string)($item['billingcycle'] ?? '');
 		if ($key !== '') {
 			$cents = self::toCents(self::pickPrice($item));
@@ -696,16 +1044,41 @@ class ZjmfUpstream
 				return [$entry['cycle'] => $entry];
 			}
 		}
-		// 兼容完整周期结构:cycle/pricing 为数组或映射
-		$raw = $item['cycle'] ?? $item['pricing'] ?? null;
-		if (is_string($raw)) {
-			$decoded = json_decode($raw, true);
-			$raw = is_array($decoded) ? $decoded : null;
+		return [];
+	}
+
+	/**
+	 * 并行数组结构:billingcycle[] + billingcycle_price[](+ billingcycle_zh[])。
+	 * 若任一数组缺失或长度不一致,返回空数组。
+	 */
+	protected static function parseParallelCycles($item)
+	{
+		$keys = $item['billingcycle'] ?? null;
+		$prices = $item['billingcycle_price'] ?? $item['billingcycleprices'] ?? null;
+		if (!is_array($keys) || $keys === [] || !is_array($prices)) {
+			return [];
 		}
-		if (is_array($raw) && $raw !== []) {
-			return self::parseCycleMap($raw);
+		$names = $item['billingcycle_zh'] ?? null;
+		if (!is_array($names)) {
+			$names = null;
 		}
-		return [];
+		$out = [];
+		foreach ($keys as $i => $k) {
+			$k = (string)$k;
+			if ($k === '' || !isset($prices[$i])) {
+				continue;
+			}
+			$cents = self::toCents($prices[$i]);
+			if ($cents <= 0) {
+				continue;
+			}
+			$name = $names ? (string)($names[$i] ?? '') : '';
+			$entry = self::cycleEntry($k, $cents, $name);
+			if (!isset($out[$entry['cycle']])) {
+				$out[$entry['cycle']] = $entry;
+			}
+		}
+		return $out;
 	}
 
 	/**
@@ -719,6 +1092,10 @@ class ZjmfUpstream
 			return [];
 		}
 		$prod = is_array($data['product'] ?? null) ? $data['product'] : $data;
+		$out = self::parseParallelCycles($prod);
+		if ($out !== []) {
+			return $out;
+		}
 		$raw = $prod['cycle'] ?? $data['cycle'] ?? null;
 		if (is_string($raw)) {
 			$decoded = json_decode($raw, true);
@@ -730,6 +1107,449 @@ class ZjmfUpstream
 		return self::parseCycleMap($raw);
 	}
 
+	/* ============================================================
+	 *  周期键探测(add_to_shop 失败时定位上游真实 billingcycle 键)
+	 * ============================================================ */
+
+	/**
+	 * 探测上游商品可选周期(真实 billingcycle 键 + 中文名 + 价格)。
+	 * 优先 GET /cart/get_product_config(含该产品可选周期数据),
+	 * 失败回退商品详情。返回 [billingcycle键 => ['name'=>, 'price_cents'=>]];
+	 * 无结果返回 []。
+	 *
+	 * @param CubeFinanceClient $client
+	 * @param int               $upId
+	 * @param array|null        $detail 已抓取的详情响应(避免重复请求)
+	 */
+	protected static function probeCycles($client, $upId, $detail = null)
+	{
+		$out = [];
+		try {
+			$res = $client->cartGetProductConfig((int)$upId);
+			if (self::respOk($res)) {
+				$out = self::parseConfigCycles($res);
+			}
+		} catch (CubeFinanceException $e) {
+			$out = [];
+		}
+		if ($out !== []) {
+			return $out;
+		}
+		if ($detail === null) {
+			try {
+				$detail = $client->productDetail((int)$upId, 'agent');
+			} catch (CubeFinanceException $e) {
+				return [];
+			}
+		}
+		return self::parseDetailCycleList($detail);
+	}
+
+	/**
+	 * 解析购物车配置接口响应中的周期列表。
+	 * 兼容 data.products(数组或单产品)与 data.product 两种外壳;
+	 * 周期可来自 cycle(JSON 串/数组/映射)、pricing 映射、并行数组。
+	 */
+	protected static function parseConfigCycles($res)
+	{
+		$data = $res['data'] ?? [];
+		if (!is_array($data)) {
+			return [];
+		}
+		$products = $data['products'] ?? $data['product'] ?? null;
+		if (!is_array($products)) {
+			return [];
+		}
+		if (isset($products['cycle']) || isset($products['pricing'])
+			|| isset($products['billingcycle']) || isset($products['id'])) {
+			$products = [$products];
+		}
+		$out = [];
+		foreach ($products as $prod) {
+			if (!is_array($prod)) {
+				continue;
+			}
+			foreach (self::cycleSourceCandidates($prod) as $item) {
+				$key = (string)($item['billingcycle'] ?? '');
+				if ($key === '') {
+					continue;
+				}
+				$cents = self::toCents(
+					$item['price'] ?? $item['product_price'] ?? $item['price_total'] ?? 0
+				);
+				$name = (string)($item['billingcycle_zh'] ?? '');
+				if (!isset($out[$key])) {
+					$out[$key] = ['name' => $name, 'price_cents' => max(0, $cents)];
+				} elseif ($cents > 0 && $out[$key]['price_cents'] <= 0) {
+					$out[$key]['price_cents'] = max(0, $cents);
+				}
+			}
+		}
+		return $out;
+	}
+
+	/**
+	 * 从单个产品数据提取周期条目数组 [{billingcycle, billingcycle_zh, price}]。
+	 * 优先完整周期(cycle/pricing),其次并行数组(billingcycle[]+billingcycle_price[])。
+	 */
+	protected static function cycleSourceCandidates($prod)
+	{
+		$items = [];
+		$raw = $prod['cycle'] ?? $prod['pricing'] ?? null;
+		if (is_string($raw)) {
+			$decoded = json_decode($raw, true);
+			$raw = is_array($decoded) ? $decoded : null;
+		}
+		if (is_array($raw)) {
+			if (isset($raw[0]) && is_array($raw[0])) {
+				// 数组形式:[{billingcycle, price, billingcycle_zh}]
+				foreach ($raw as $it) {
+					if (is_array($it) && (string)($it['billingcycle'] ?? '') !== '') {
+						$items[] = $it;
+					}
+				}
+			} else {
+				// 映射形式:{cycle: 价格 或 {price:...}}
+				foreach ($raw as $k => $v) {
+					$items[] = is_array($v)
+						? array_merge($v, ['billingcycle' => (string)$k])
+						: ['billingcycle' => (string)$k, 'price' => $v];
+				}
+			}
+		}
+		if ($items !== []) {
+			return $items;
+		}
+		// 并行数组形式
+		$keys = $prod['billingcycle'] ?? null;
+		$prices = $prod['billingcycle_price'] ?? $prod['billingcycleprices'] ?? null;
+		if (is_array($keys) && is_array($prices)) {
+			$names = is_array($prod['billingcycle_zh'] ?? null) ? $prod['billingcycle_zh'] : null;
+			foreach ($keys as $i => $k) {
+				$items[] = [
+					'billingcycle'    => (string)$k,
+					'billingcycle_zh' => $names ? (string)($names[$i] ?? '') : '',
+					'price'           => $prices[$i] ?? 0,
+				];
+			}
+		}
+		return $items;
+	}
+
+	/** 详情响应 → [billingcycle键 => ['name'=>, 'price_cents'=>]]。 */
+	protected static function parseDetailCycleList($detail)
+	{
+		$out = [];
+		foreach (self::parseDetailCycles($detail) as $entry) {
+			$key = (string)($entry['up_cycle'] ?? '');
+			if ($key === '') {
+				continue;
+			}
+			$out[$key] = [
+				'name'        => (string)($entry['name'] ?? ''),
+				'price_cents' => (int)($entry['agent_price_cents'] ?? 0),
+			];
+		}
+		return $out;
+	}
+
+	/* ============================================================
+	 *  配置项默认值探测(add_to_shop 需要 currencyid + configoption)
+	 * ============================================================ */
+
+	/**
+	 * 探测商品默认配置(货币ID + 配置项默认选择),用于 add_to_shop 计价。
+	 * 优先 GET /cart/get_product_config,失败回退商品详情。
+	 * 返回 ['currencyid'=>int, 'configoption'=>[optionId=>子项ID或数量]];
+	 * 探测不到 configoption 时至少带回 currencyid。
+	 *
+	 * @param CubeFinanceClient $client
+	 * @param int               $upId
+	 * @param array|null        $detail 已抓取的详情响应(避免重复请求)
+	 */
+	protected static function probeConfigDefaults($client, $upId, $detail = null)
+	{
+		try {
+			$res = $client->cartGetProductConfig((int)$upId);
+			if (self::respOk($res)) {
+				$out = self::parseConfigDefaults($res);
+				if (!empty($out['configoption'])) {
+					return $out;
+				}
+			}
+		} catch (CubeFinanceException $e) {
+			// 忽略,走详情回退
+		}
+		if ($detail === null) {
+			try {
+				$detail = $client->productDetail((int)$upId, 'agent');
+			} catch (CubeFinanceException $e) {
+				return ['currencyid' => 0, 'configoption' => []];
+			}
+		}
+		return self::parseConfigDefaults($detail);
+	}
+
+	/**
+	 * 从响应中解析 currencyid 与 configoption 默认选择。
+	 * 兼容 data.products(数组/单产品)与 data.product 外壳;
+	 * 配置项结构兼容数组组/映射组/扁平默认值/数量型多种形态。
+	 */
+	protected static function parseConfigDefaults($res)
+	{
+		$data = $res['data'] ?? [];
+		if (!is_array($data)) {
+			return ['currencyid' => 0, 'configoption' => []];
+		}
+		$out = ['currencyid' => 0, 'configoption' => []];
+		if (isset($data['default_currency']) && is_numeric($data['default_currency'])) {
+			$out['currencyid'] = (int)$data['default_currency'];
+		}
+		$currencies = $data['currencies'] ?? null;
+		if (is_array($currencies)) {
+			foreach ($currencies as $c) {
+				if (is_array($c) && isset($c['id']) && is_numeric($c['id'])) {
+					if ($out['currencyid'] <= 0) {
+						$out['currencyid'] = (int)$c['id'];
+					}
+					break;
+				}
+			}
+		}
+		$products = $data['products'] ?? $data['product'] ?? null;
+		if (!is_array($products)) {
+			if ($out['currencyid'] <= 0) {
+				$out['currencyid'] = 1;
+			}
+			return $out;
+		}
+		if (isset($products['id']) || isset($products['pid'])
+			|| isset($products['configoption']) || isset($products['config_option'])) {
+			$products = [$products];
+		}
+		$groups = [];
+		foreach ($products as $prod) {
+			if (!is_array($prod)) {
+				continue;
+			}
+			// 产品级 currencyid(映射形式取首个键)
+			if ($out['currencyid'] <= 0 && isset($prod['currencyid']) && is_array($prod['currencyid'])) {
+				foreach ($prod['currencyid'] as $cid => $cfg) {
+					if (is_numeric($cid) && (int)$cid > 0) {
+						$out['currencyid'] = (int)$cid;
+						break;
+					}
+				}
+			}
+			$co = $prod['configoption'] ?? $prod['config_option'] ?? null;
+			if (is_array($co)) {
+				$groups = array_merge($groups, self::normalizeConfigGroups($co));
+			}
+		}
+		foreach ($groups as $g) {
+			if (!is_array($g)) {
+				continue;
+			}
+			$oid = (int)($g['id'] ?? $g['configoption_id'] ?? 0);
+			if ($oid <= 0) {
+				continue;
+			}
+			$val = self::pickConfigDefault($g);
+			if ($val !== null && $val !== '') {
+				$out['configoption'][(string)$oid] = (string)$val;
+			}
+		}
+		if ($out['currencyid'] <= 0) {
+			$out['currencyid'] = 1; // 兜底默认货币
+		}
+		return $out;
+	}
+
+	/** 将 configoption 数据规范化为配置组列表。 */
+	protected static function normalizeConfigGroups($co)
+	{
+		$groups = [];
+		if (isset($co[0]) && is_array($co[0])) {
+			return $co; // 数组组形式
+		}
+		foreach ($co as $k => $v) {
+			if (!is_array($v)) {
+				// 扁平:optionId => 默认值
+				$groups[] = ['id' => is_numeric($k) ? $k : 0, '_flat_value' => $v];
+				continue;
+			}
+			if (isset($v['option']) || isset($v['options']) || isset($v['id']) || isset($v['configoption_id'])) {
+				if (!isset($v['id']) && !isset($v['configoption_id']) && is_numeric($k)) {
+					$v['id'] = (int)$k;
+				}
+				$groups[] = $v;
+				continue;
+			}
+			// 映射:optionId => 子项数组
+			$groups[] = ['id' => is_numeric($k) ? (int)$k : 0, 'option' => $v];
+		}
+		return $groups;
+	}
+
+	/**
+	 * 从单个配置组取默认子项值(子项ID或数量)。
+	 * 优先 checked/selected/default 标记,否则取第一个子项。
+	 */
+	protected static function pickConfigDefault($g)
+	{
+		if (array_key_exists('_flat_value', $g)) {
+			return (string)$g['_flat_value'];
+		}
+		$type = strtolower((string)($g['type'] ?? ''));
+		if (in_array($type, ['qty', 'quantity', 'number'], true)) {
+			$q = $g['qty'] ?? $g['quantity'] ?? $g['value'] ?? 1;
+			return (string)$q;
+		}
+		$subs = $g['option'] ?? $g['options'] ?? $g['sub_option']
+			?? $g['suboption'] ?? $g['child'] ?? $g['values'] ?? null;
+		if (!is_array($subs)) {
+			return null;
+		}
+		$first = null;
+		foreach ($subs as $s) {
+			if (!is_array($s)) {
+				continue;
+			}
+			if ($first === null) {
+				$first = $s;
+			}
+			foreach (['checked', 'selected', 'default', 'is_check', 'is_default'] as $f) {
+				if (isset($s[$f]) && ((int)$s[$f] === 1 || (string)$s[$f] === '1')) {
+					$first = $s;
+					break 2;
+				}
+			}
+		}
+		if ($first === null) {
+			return null;
+		}
+		$id = $first['id'] ?? $first['suboption_id'] ?? $first['value'] ?? null;
+		return $id === null ? null : (string)$id;
+	}
+
+	/** 上游失败信息是否与周期/价格相关(命中才触发探测重试)。 */
+	protected static function isCyclePriceError($res)
+	{
+		$msg = (string)($res['msg'] ?? '');
+		return $msg !== '' && (
+			strpos($msg, '未配置价格') !== false
+			|| strpos($msg, '价格错误') !== false
+			|| strpos($msg, '周期') !== false
+			|| stripos($msg, 'cycle') !== false
+		);
+	}
+
+	/**
+	 * 本地周期键 → 上游真实 billingcycle 键。
+	 * 忽略大小写并按别名(monthly/month、quarterly/quarter 等)匹配,
+	 * 也匹配中文名(月付/月/季付…);找不到返回原键。
+	 */
+	protected static function matchUpCycle($localCycle, array $available)
+	{
+		$localCycle = (string)$localCycle;
+		if ($localCycle === '' || $available === []) {
+			return $localCycle;
+		}
+		$aliases = self::cycleAliases($localCycle);
+		foreach ($available as $key => $cfg) {
+			$candidate = (string)$key;
+			if (in_array(self::normKey($candidate), $aliases, true)) {
+				return $candidate;
+			}
+			$name = (string)($cfg['name'] ?? '');
+			if ($name !== '' && in_array(self::normKey($name), $aliases, true)) {
+				return $candidate;
+			}
+		}
+		$normLocal = self::normKey($localCycle);
+		foreach ($available as $key => $cfg) {
+			if (self::normKey((string)$key) === $normLocal) {
+				return (string)$key;
+			}
+		}
+		return $localCycle;
+	}
+
+	/** 本地周期键的别名集合(归一化后,含中文名)。 */
+	protected static function cycleAliases($localCycle)
+	{
+		$norm = self::normKey($localCycle);
+		$map = [
+			'monthly'      => ['monthly', 'month', 'm', '月', '月付'],
+			'quarterly'    => ['quarterly', 'quarter', 'q', '季', '季付'],
+			'semiannually' => ['semiannually', 'semiannual', 'halfyear', 'half_year', 'half', 'semi', '半年', '半年付'],
+			'annually'     => ['annually', 'annual', 'yearly', 'year', 'y', '年', '年付'],
+			'biennially'   => ['biennially', 'biennial', 'biennium', 'twoyear', 'two_year', '两年', '两年付'],
+			'triennially'  => ['triennially', 'triennial', 'triennium', 'threeyear', 'three_year', '三年', '三年付'],
+		];
+		foreach ($map as $k => $list) {
+			if ($norm === $k || in_array($norm, $list, true)) {
+				return $list;
+			}
+		}
+		return [$norm];
+	}
+
+	/** 键归一化:小写 + 去非字母数字(保留中文)。 */
+	protected static function normKey($key)
+	{
+		return strtolower(preg_replace('/[^a-z0-9\x{4e00}-\x{9fa5}]/iu', '', (string)$key));
+	}
+
+	/** 可用周期列表渲染为可读文案(供失败信息展示)。 */
+	protected static function formatAvailable(array $avail)
+	{
+		$parts = [];
+		foreach ($avail as $key => $cfg) {
+			$name = (string)($cfg['name'] ?? '');
+			$parts[] = $name !== '' ? $key . '(' . $name . ')' : $key;
+		}
+		return $parts === [] ? '' : ';上游可用周期:' . implode('、', $parts);
+	}
+
+	/**
+	 * 将本地周期对应的上游键写回商品 cycles JSON(后续购买直接使用)。
+	 */
+	protected static function persistUpCycle($supplierId, $upProductId, $localCycle, $upCycle)
+	{
+		global $DB;
+		$product = zjmf_product_get_by_up((int)$supplierId, (int)$upProductId);
+		if (!$product) {
+			return;
+		}
+		$cycles = json_decode((string)($product['cycles'] ?? ''), true);
+		if (!is_array($cycles)) {
+			return;
+		}
+		$changed = false;
+		foreach ($cycles as &$cfg) {
+			if (is_array($cfg) && (string)($cfg['cycle'] ?? '') === (string)$localCycle) {
+				if ((string)($cfg['up_cycle'] ?? '') !== (string)$upCycle) {
+					$cfg['up_cycle'] = (string)$upCycle;
+					$changed = true;
+				}
+				break;
+			}
+		}
+		unset($cfg);
+		if ($changed) {
+			$DB->query_prepare(
+				"UPDATE MN_plugin_zjmf_product SET cycles=?, updated_at=?
+				 WHERE id=?",
+				[
+					json_encode($cycles, JSON_UNESCAPED_UNICODE),
+					date('Y-m-d H:i:s'),
+					(int)$product['id'],
+				]
+			);
+		}
+	}
+
 	/** 构建单个周期条目(键归一化 + 名称兜底)。 */
 	protected static function cycleEntry($key, $cents, $name = '')
 	{
@@ -737,6 +1557,8 @@ class ZjmfUpstream
 		$known = zjmf_cycles();
 		return [
 			'cycle'             => $norm,
+			// 上游原始 billingcycle 值(如 monthly),购买时原样回传
+			'up_cycle'          => (string)$key,
 			'name'              => $name !== '' ? $name : ($known[$norm]['name'] ?? $norm),
 			'agent_price_cents' => max(0, (int)$cents),
 		];
@@ -829,6 +1651,102 @@ class ZjmfUpstream
 		return (string)($item['type'] ?? $item['module'] ?? $item['module_name'] ?? '');
 	}
 
+	/**
+	 * 从购物车数据中定位刚添加产品的购物车位置。
+	 * 部分版本 add_to_shop 不返回位置 i,需 GET /cart/get_shop_data 后
+	 * 按 productid 匹配(购物车数组的键/序号即位置 i;同商品取最后一项,即刚添加的)。
+	 *
+	 * @param CubeFinanceClient $client
+	 * @param int               $upProductId
+	 * @param string            $upCycle 上游 billingcycle 键
+	 * @return int 位置,找不到返回 -1
+	 */
+	protected static function cartPosition($client, $upProductId, $upCycle)
+	{
+		try {
+			$res = $client->cartGetShopData();
+		} catch (CubeFinanceException $e) {
+			return -1;
+		}
+		if (!self::respOk($res)) {
+			return -1;
+		}
+		$products = $res['data']['cart_products'] ?? null;
+		if (!is_array($products)) {
+			return -1;
+		}
+		$wantPid = (string)$upProductId;
+		$wantCycle = strtolower((string)$upCycle);
+		$lastPid = -1;
+		$lastMatch = -1;
+		foreach ($products as $i => $p) {
+			if (!is_array($p)) {
+				continue;
+			}
+			$pid = (string)($p['productid'] ?? $p['pid'] ?? $p['id'] ?? '');
+			if ($pid !== $wantPid) {
+				continue;
+			}
+			$lastPid = (int)$i;
+			$cycle = strtolower((string)($p['billingcycle'] ?? ''));
+			if ($cycle === $wantCycle || $cycle === '') {
+				$lastMatch = (int)$i; // 周期匹配(或未知)的最后一个即刚添加项
+			}
+		}
+		if ($lastMatch >= 0) {
+			return $lastMatch;
+		}
+		return $lastPid; // 仅 pid 匹配的最后一个
+	}
+
+	/**
+	 * 从加购响应中取购物车位置 data.i(兼容字符串 data、其他位置键、一层嵌套)。
+	 *
+	 * @param array $res 响应数组
+	 * @return int 位置,找不到返回 -1
+	 */
+	protected static function findPosition($res)
+	{
+		$d = $res['data'] ?? null;
+		if (is_string($d)) {
+			if (is_numeric($d)) {
+				return (int)$d;
+			}
+			$decoded = json_decode($d, true);
+			if (is_array($decoded)) {
+				$d = $decoded;
+			}
+		}
+		if (is_numeric($d)) {
+			return (int)$d;
+		}
+		if (!is_array($d)) {
+			return -1;
+		}
+		foreach (['i', 'position', 'pos', 'cart_i', 'key', 'id'] as $k) {
+			if (!array_key_exists($k, $d)) {
+				continue;
+			}
+			$v = $d[$k];
+			if (is_array($v)) {
+				$v = reset($v);
+			}
+			if (is_numeric($v)) {
+				return (int)$v;
+			}
+		}
+		// 一层嵌套深搜(如 data.products[].i)
+		foreach ($d as $v) {
+			if (is_array($v)) {
+				$sub = self::findPosition(['data' => $v]);
+				if ($sub >= 0) {
+					return $sub;
+				}
+			}
+		}
+		return -1;
+	}
+
 	/** 上游返回是否成功(200 常规成功;1001 购买成功/支付完成,无需继续支付)。 */
 	protected static function respOk($res)
 	{
@@ -915,6 +1833,26 @@ class ZjmfUpstream
 		return 0;
 	}
 
+	/**
+	 * 插件周期键 → 上游 billingcycle 值。
+	 * 优先取同步时保留的上游原始值(up_cycle),未同步/旧数据回退小写(官方周期如 monthly、day、hour)。
+	 */
+	protected static function upstreamCycle($supplierId, $upProductId, $cycle)
+	{
+		$cycle = (string)$cycle;
+		if ($cycle !== '') {
+			$product = zjmf_product_get_by_up((int)$supplierId, (int)$upProductId);
+			if ($product) {
+				$cycles = zjmf_product_cycles($product);
+				$entry = $cycles[$cycle] ?? null;
+				if (is_array($entry) && (string)($entry['up_cycle'] ?? '') !== '') {
+					return (string)$entry['up_cycle'];
+				}
+			}
+		}
+		return strtolower($cycle);
+	}
+
 	/**
 	 * 查询账单详情并轮询主机 ID(主机创建可能异步)。
 	 * 官方:GET /invoices/{id} → data.invoices[].status(支付状态)+
@@ -979,13 +1917,13 @@ class ZjmfUpstream
 		return in_array($st, ['paid', '已支付', 'completed', 'complete', 'success', 'payment'], true);
 	}
 
-	/** 从主机详情响应提取主机数据(兼容 host_data/host 键与单对象/数组结构)。 */
+	/** 从主机详情响应提取主机数据(兼容 host_data/host/info/list/data 键与单对象/数组结构)。 */
 	protected static function pickHostData($data)
 	{
 		if (!is_array($data)) {
 			return [];
 		}
-		foreach (['host_data', 'host', 'info'] as $k) {
+		foreach (['host_data', 'host', 'info', 'list', 'data'] as $k) {
 			if (isset($data[$k]) && is_array($data[$k])) {
 				$v = $data[$k];
 				if (isset($v[0]) && is_array($v[0])) {
@@ -1007,31 +1945,43 @@ class ZjmfUpstream
 				'username'   => (string)($host['username'] ?? ''),
 				'password'   => (string)($host['password'] ?? ''),
 				'name'       => (string)($host['productname'] ?? $host['name'] ?? ''),
-				'renew_date' => (string)($host['nextduedate'] ?? $host['renew_date'] ?? $host['renewdate'] ?? ''),
+				// 上游部分版本 nextduedate 为 Unix 时间戳,统一归一化为 Y-m-d
+				'renew_date' => zjmf_normalize_date(
+					(string)($host['nextduedate'] ?? $host['renew_date'] ?? $host['renewdate'] ?? '')
+				),
 			];
 		} catch (CubeFinanceException $e) {
 			return ['username' => '', 'password' => '', 'name' => '', 'renew_date' => ''];
 		}
 	}
 
-	/** 上游主机状态 → 本地展示状态(active/suspend/unknown)。 */
+	/** 上游主机状态 → 本地展示状态(active/suspend/pending/terminated/unknown)。 */
 	public static function mapHostStatus($data)
 	{
 		if (!is_array($data)) {
 			return 'unknown';
 		}
+		$st = strtolower(trim((string)($data['status'] ?? $data['domainstatus'] ?? '')));
+		if ($st !== '') {
+			if (in_array($st, ['active', 'on', 'true', 'completed', '运行中'], true)) {
+				return 'active';
+			}
+			if (in_array($st, ['pending', 'wait', 'waiting', '待开通'], true)) {
+				return 'pending';
+			}
+			if (in_array($st, ['suspended', 'suspend', 'paused', 'off', '已暂停'], true)) {
+				return 'suspend';
+			}
+			if (in_array($st, ['cancelled', 'cancel', 'terminated', 'terminate', 'fraud', '已终止'], true)) {
+				return 'terminated';
+			}
+			return 'unknown';
+		}
+		// 无状态字段时用 qk 兜底(false 视为不可用)
 		$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
index 1694c7b..376525c 100644
--- a/app_plugins/zjmfmanager_reserve/lib/zjmf.php
+++ b/app_plugins/zjmfmanager_reserve/lib/zjmf.php
@@ -134,21 +134,24 @@ function zjmf_encrypt($plain)
 	return authcode((string)$plain, 'ENCODE', SYS_KEY);
 }
 
-/** 密文解密。 */
+/**
+ * 密文解密(防御式)。
+ * authcode 解密分支在 PHP 8 下对乱码密文会执行「前10位 - time()」并抛
+ * TypeError(Unsupported operand types: string - int),导致详情页 500。
+ * 这里对空/过短密文直接返回,异常兜底为空串。
+ */
 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) . '***';
+	$cipher = (string)$cipher;
+	if ($cipher === '' || strlen($cipher) <= 4) {
+		return ''; // 未设置密码或非 authcode 密文
+	}
+	try {
+		$out = authcode($cipher, 'DECODE', SYS_KEY);
+		return is_string($out) ? $out : '';
+	} catch (Throwable $e) {
+		return '';
 	}
-	return substr($s, 0, 3) . '***' . substr($s, -2);
 }
 
 /** 写操作日志。 */
@@ -363,6 +366,7 @@ function zjmf_product_cycles($product)
 			$cycle = (string)$raw[0];
 			$map[$cycle] = [
 				'cycle'             => $cycle,
+				'up_cycle'          => '',
 				'name'              => (string)$raw[1],
 				'price_cents'       => (int)$raw[2],
 				'agent_price_cents' => (int)$raw[2],
@@ -378,6 +382,7 @@ function zjmf_product_cycles($product)
 		}
 		$map[$cycle] = [
 			'cycle'             => $cycle,
+			'up_cycle'          => (string)($item['up_cycle'] ?? ''),
 			'name'              => (string)($item['name'] ?? $cycle),
 			'price_cents'       => (int)($item['price_cents'] ?? 0),
 			'agent_price_cents' => (int)($item['agent_price_cents'] ?? 0),
@@ -721,7 +726,9 @@ function zjmf_host_get_by_user($user_id, $host_id)
 {
 	global $DB;
 	return $DB->get_row_prepare(
-		"SELECT h.* FROM MN_plugin_zjmf_host h
+		"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;
@@ -737,12 +744,14 @@ function zjmf_host_get_by_up($up_host_id)
 	) ?: null;
 }
 
-/** 用户主机列表。 */
+/** 用户主机列表(含供应商名)。 */
 function zjmf_host_list_by_user($user_id)
 {
 	global $DB;
 	return $DB->get_all_prepare(
-		"SELECT h.* FROM MN_plugin_zjmf_host h
+		"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]
 	) ?: [];
@@ -829,6 +838,151 @@ function zjmf_host_update_cache($host_id, $data)
 	);
 }
 
+/**
+ * 归一化上游返回的日期/时间戳为 Y-m-d H:i:s(精确到秒,与 created_at 展示一致)。
+ * 魔方财务部分版本接口返回 Unix 时间戳(秒/毫秒),部分返回 Y-m-d / Y-m-d H:i:s;
+ * 已含时分秒的标准串原样保留。
+ *
+ * @param mixed $val
+ * @return string
+ */
+function zjmf_normalize_date($val)
+{
+	$s = trim((string)$val);
+	if ($s === '') {
+		return '';
+	}
+	// 已是标准日期(时间)串:截断到秒粒度并保留
+	if (preg_match('/^\d{4}-\d{2}-\d{2}/', $s)) {
+		$s = preg_replace(
+			'/^(\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?::\d{2})?)?).*$/',
+			'$1',
+			$s
+		);
+		return $s;
+	}
+	if (preg_match('/^\d+$/', $s)) {
+		$t = (int)$s;
+		if ($t > 100000000000) {
+			$t = (int)($t / 1000); // 毫秒时间戳
+		}
+		return $t > 0 ? date('Y-m-d H:i:s', $t) : '';
+	}
+	$t = strtotime($s);
+	return $t ? date('Y-m-d H:i:s', $t) : $s;
+}
+
+/**
+ * 补齐本地主机缺失的上游主机 ID(up_host_id<=0 时)。
+ *
+ * 开通/结算响应未能解析出主机 ID 时会落库 up_host_id=0,导致用户端
+ * 卡片按钮不可用、详情页无法拉取实时信息。此函数通过上游
+ * GET host/list(我的主机列表)按 产品名 + 开通日期 匹配回填。
+ * 同一次请求内只拉取一次上游列表(进程内静态缓存)。
+ *
+ * @param array $host MN_plugin_zjmf_host 行
+ * @return array 回填后的主机行(未匹配则原样返回)
+ */
+function zjmf_backfill_host_upid($host)
+{
+	global $DB, $date;
+	if (!is_array($host) || (int)($host['id'] ?? 0) <= 0 || (int)($host['up_host_id'] ?? 0) > 0) {
+		return $host;
+	}
+	$supplierId = (int)($host['supplier_id'] ?? 0);
+	if ($supplierId <= 0) {
+		return $host;
+	}
+	$supplier = zjmf_supplier_get($supplierId);
+	if (!$supplier || (int)$supplier['status'] !== 1) {
+		return $host;
+	}
+	// 进程内缓存:同一次请求(列表页/详情页)只向上游请求一次
+	static $cache = [];
+	if (!array_key_exists($supplierId, $cache)) {
+		$res = ZjmfUpstream::hostList($supplier, ['orderby' => 'id', 'sort' => 'DESC']);
+		$cache[$supplierId] = empty($res['ok']) ? [] : ($res['data']['list'] ?? []);
+	}
+	$list = $cache[$supplierId];
+	if (!is_array($list) || $list === []) {
+		return $host;
+	}
+	$name = (string)($host['name'] ?? '');
+	$created = substr((string)($host['created_at'] ?? ''), 0, 10);
+	$candidates = [];
+	foreach ($list as $item) {
+		if (!is_array($item)) {
+			continue;
+		}
+		$pn = (string)($item['productname'] ?? '');
+		if ($pn === '' || ($name !== '' && $pn !== $name
+			&& strpos($pn, $name) !== 0 && strpos($name, $pn) !== 0)) {
+			continue;
+		}
+		$candidates[] = $item;
+	}
+	if ($candidates === []) {
+		return $host;
+	}
+	// 多台同名主机时,按开通日期与本地创建日期最接近者匹配
+	$best = $candidates[0];
+	if ($created !== '') {
+		$bestDiff = PHP_INT_MAX;
+		$bestTime = strtotime($created) ?: 0;
+		foreach ($candidates as $item) {
+			// regdate 部分版本为 Unix 时间戳,先归一化为 Y-m-d 再比较
+			$rd = zjmf_normalize_date((string)($item['regdate'] ?? ''));
+			if ($rd === '') {
+				continue;
+			}
+			$rdTime = strtotime($rd) ?: 0;
+			$diff = $rdTime ? abs($rdTime - $bestTime) : PHP_INT_MAX;
+			if ($diff < $bestDiff) {
+				$bestDiff = $diff;
+				$best = $item;
+			}
+		}
+	}
+	$upId = (int)($best['id'] ?? 0);
+	if ($upId <= 0) {
+		return $host;
+	}
+	$status = function_exists('zjmf_map_upstream_status')
+		? zjmf_map_upstream_status((string)($best['domainstatus'] ?? ''))
+		: (string)($host['status'] ?? '');
+	// nextduedate 部分版本为 Unix 时间戳,统一归一化为 Y-m-d
+	$renew = zjmf_normalize_date((string)($best['nextduedate'] ?? $host['renew_date'] ?? ''));
+	$now = $date ?: date('Y-m-d H:i:s');
+	$DB->query_prepare(
+		"UPDATE MN_plugin_zjmf_host
+		 SET up_host_id=?, status=?, renew_date=?, updated_at=? WHERE id=?",
+		[$upId, $status, $renew, $now, (int)$host['id']]
+	);
+	$host['up_host_id'] = $upId;
+	$host['status'] = $status;
+	$host['renew_date'] = $renew;
+	return $host;
+}
+
+/** 上游 domainstatus → 本地展示状态(active/suspend/unknown)。 */
+function zjmf_map_upstream_status($status)
+{
+	$st = strtolower(trim((string)$status));
+	if (in_array($st, ['active', 'completed', '运行中'], true)) {
+		return 'active';
+	}
+	if (in_array($st, ['pending', 'wait', 'waiting', '待开通'], true)) {
+		return 'pending';
+	}
+	if (in_array($st, ['suspended', 'suspend', 'paused', '已暂停'], true)) {
+		return 'suspend';
+	}
+	if (in_array($st, ['cancelled', 'cancel', 'terminated', 'terminate', 'fraud', '已终止'], true)) {
+		return 'terminated';
+	}
+	return 'unknown';
+}
+
 /* ============================================================
  *  主机操作辅助
  * ============================================================ */
@@ -861,9 +1015,11 @@ function zjmf_action_status($action)
 function zjmf_host_status_label($status)
 {
 	$map = [
-		'active'  => '运行中',
-		'suspend' => '已暂停',
-		'unknown' => '未知',
+		'active'     => '运行中',
+		'suspend'    => '已暂停',
+		'pending'    => '待开通',
+		'terminated' => '已终止',
+		'unknown'    => '未知',
 	];
 	return $map[$status] ?? $status;
 }
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/assign.php b/app_plugins/zjmfmanager_reserve/views/admin/assign.php
new file mode 100644
index 0000000..45de4d7
--- /dev/null
+++ b/app_plugins/zjmfmanager_reserve/views/admin/assign.php
@@ -0,0 +1,295 @@
+<?php
+/**
+ * 管理员端 - 主机指派
+ *
+ * 拉取当前上游账户已开通的主机列表(GET host/list),
+ * 勾选具体某台机器并指定目标用户后「批量指派」:
+ * 在本地为该用户绑定该上游主机(不调上游购买/开通),
+ * 主机随即出现在该用户的「我的主机」中。
+ */
+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>
+    </div>
+    <div class="card-body">
+      <p class="text-muted">
+        选择供应商并加载其已开通的主机列表,勾选要指派的具体机器并指定目标用户,
+        点击「批量指派所选主机」。指派为本地绑定操作,不会在上游重复开通;
+        被指派的主机将出现在该用户的「我的主机」中。
+      </p>
+
+      <!-- 供应商 / 用户 -->
+      <div class="form-row">
+        <div class="col-md-3 mb-2">
+          <label class="small text-muted">供应商 *</label>
+          <select id="zjf-assign-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)
+                ?><?= (int)$s['status'] !== 1 ? '(已停用)' : '' ?></option>
+              <?php endforeach; ?>
+            <?php endif; ?>
+          </select>
+        </div>
+        <div class="col-md-4 mb-2">
+          <label class="small text-muted">目标用户 *(输入用户名/邮箱/ID 搜索)</label>
+          <div class="input-group input-group-sm">
+            <input type="text" id="zjf-assign-user-kw" class="form-control"
+                   placeholder="用户名 / 邮箱 / ID" maxlength="64">
+            <div class="input-group-append">
+              <button type="button" class="btn btn-outline-primary" id="zjf-assign-user-search">搜索</button>
+            </div>
+          </div>
+          <div id="zjf-assign-user-list" class="mt-1"></div>
+          <input type="hidden" id="zjf-assign-user-id" value="0">
+        </div>
+        <div class="col-md-3 mb-2">
+          <label class="small text-muted">&nbsp;</label>
+          <div>
+            <button type="button" class="btn btn-primary btn-sm" id="zjf-assign-load">加载已开通的主机</button>
+          </div>
+        </div>
+      </div>
+
+      <div id="zjf-assign-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-assign-all"> 全选
+              </th>
+              <th>上游主机ID</th>
+              <th>域名</th>
+              <th>产品名</th>
+              <th>状态</th>
+              <th>IP</th>
+              <th>周期</th>
+              <th>到期日</th>
+            </tr>
+          </thead>
+          <tbody id="zjf-assign-list">
+            <tr><td colspan="8" class="text-center text-muted">
+              请先选择供应商并加载主机列表
+            </td></tr>
+          </tbody>
+        </table>
+      </div>
+
+      <button type="button" class="btn btn-primary" id="zjf-assign-do">批量指派所选主机</button>
+      <button type="button" class="btn btn-secondary" id="zjf-assign-reset">重置列表</button>
+
+      <!-- 指派结果 -->
+      <div id="zjf-assign-result" class="mt-3" style="display:none;"></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) {
+    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 || '完成');
+    }
+  }
+  function esc(s) {
+    return String(s == null ? '' : s)
+      .replace(/&/g, '&amp;').replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+  }
+
+  var listBox = document.getElementById('zjf-assign-list');
+  var tip = document.getElementById('zjf-assign-tip');
+  var userId = document.getElementById('zjf-assign-user-id');
+  var userList = document.getElementById('zjf-assign-user-list');
+
+  /* ---------------- 用户搜索 ---------------- */
+  document.getElementById('zjf-assign-user-search').addEventListener('click', function () {
+    var kw = document.getElementById('zjf-assign-user-kw').value.trim();
+    if (!kw) { alert('请输入用户名/邮箱/ID'); return; }
+    $.post('ajax.php', {gn: 'p_zjmf_admin_search_users', keyword: kw}, function (r) {
+      var d = res(r);
+      if (d.qk != 1 && !d.success) {
+        userList.innerHTML = '<div class="text-danger small">' + esc(d.msg || '搜索失败') + '</div>';
+        return;
+      }
+      var list = d.list || (d.data && d.data.list) || [];
+      if (!list.length) {
+        userList.innerHTML = '<div class="text-muted small">未找到匹配用户</div>';
+        return;
+      }
+      var html = '';
+      for (var i = 0; i < list.length; i++) {
+        var u = list[i];
+        html += '<label class="d-block border rounded px-2 py-1 mb-1 small">'
+          + '<input type="radio" name="zjf-assign-user" class="mr-1 zjf-assign-user-radio"'
+          + ' value="' + u.id + '" data-name="' + esc(u.username) + '">'
+          + '<b>' + esc(u.username) + '</b>'
+          + (u.email ? ' <span class="text-muted">' + esc(u.email) + '</span>' : '')
+          + (u.qq ? ' <span class="text-muted">QQ:' + esc(u.qq) + '</span>' : '')
+          + ' <span class="text-muted">ID:' + u.id + '</span>'
+          + '</label>';
+      }
+      userList.innerHTML = html;
+    });
+  });
+
+  userList.addEventListener('change', function (e) {
+    if (e.target.classList.contains('zjf-assign-user-radio')) {
+      userId.value = e.target.value;
+      tip.textContent = '已选择用户:' + e.target.getAttribute('data-name');
+    }
+  });
+
+  /* ---------------- 加载上游已开通主机 ---------------- */
+  document.getElementById('zjf-assign-load').addEventListener('click', function () {
+    var supplierId = document.getElementById('zjf-assign-supplier').value;
+    if (!supplierId) { alert('请先选择供应商'); return; }
+    var btn = this;
+    btn.disabled = true;
+    btn.textContent = '加载中...';
+    tip.textContent = '正在拉取该账户已开通的主机列表...';
+    listBox.innerHTML = '<tr><td colspan="8" class="text-center text-muted">加载中...</td></tr>';
+    $.post('ajax.php', {gn: 'p_zjmf_admin_upstream_owned_products', id: supplierId},
+      function (r) {
+        var d = res(r);
+        btn.disabled = false;
+        btn.textContent = '加载已开通的主机';
+        if (d.qk != 1 && !d.success) {
+          tip.textContent = '';
+          listBox.innerHTML = '<tr><td colspan="8" class="text-center text-muted">'
+            + esc(d.msg || '拉取失败') + '</td></tr>';
+          return;
+        }
+        var list = d.list || (d.data && d.data.list) || [];
+        if (!list.length) {
+          listBox.innerHTML = '<tr><td colspan="8" class="text-center text-muted">'
+            + '该账户暂无已开通的主机</td></tr>';
+          return;
+        }
+        var assignedCount = 0;
+        for (var i = 0; i < list.length; i++) {
+          if (list[i].assigned) assignedCount++;
+        }
+        tip.textContent = '共 ' + list.length + ' 台主机(已指派 ' + assignedCount
+          + ' 台,灰显不可重复指派),勾选要指派的主机并指定目标用户后点击「批量指派所选主机」。';
+        var html = '';
+        for (var i = 0; i < list.length; i++) {
+          var it = list[i];
+          var disabled = it.assigned ? ' disabled' : '';
+          html += '<tr' + (it.assigned ? ' class="text-muted"' : '') + '>'
+            + '<td><input type="checkbox" class="zjf-assign-check" value="' + it.id + '"' + disabled + '></td>'
+            + '<td>' + it.id + '</td>'
+            + '<td>' + esc(it.domain || '-') + '</td>'
+            + '<td>' + esc(it.productname || '-') + '</td>'
+            + '<td>' + esc(it.status_desc || it.status || '-') + '</td>'
+            + '<td>' + esc(it.dedicatedip || '-') + '</td>'
+            + '<td>' + esc(it.cycle || '-') + '</td>'
+            + '<td>' + esc(it.nextdue || '-')
+            + (it.assigned ? ' <span class="badge badge-secondary">已指派</span>' : '')
+            + '</td>'
+            + '</tr>';
+        }
+        listBox.innerHTML = html;
+      });
+  });
+
+  document.getElementById('zjf-assign-all').addEventListener('change', function () {
+    var checked = this.checked;
+    document.querySelectorAll('.zjf-assign-check').forEach(function (c) {
+      if (!c.disabled) c.checked = checked;
+    });
+  });
+
+  /* ---------------- 批量指派 ---------------- */
+  document.getElementById('zjf-assign-do').addEventListener('click', function () {
+    var supplierId = document.getElementById('zjf-assign-supplier').value;
+    var uid = parseInt(userId.value, 10);
+    if (!supplierId) { alert('请先选择供应商'); return; }
+    if (!uid) { alert('请先搜索并选择目标用户'); return; }
+    var ids = [];
+    document.querySelectorAll('.zjf-assign-check:checked').forEach(function (c) {
+      ids.push(parseInt(c.value, 10));
+    });
+    if (!ids.length) { alert('请至少勾选一台主机'); return; }
+    if (!confirm('确认为所选用户指派 ' + ids.length + ' 台主机?'
+        + '\n此操作将本地绑定上游已开通的机器,不会在上游重复开通。')) return;
+
+    var btn = this;
+    btn.disabled = true;
+    btn.textContent = '指派中...';
+    var resultBox = document.getElementById('zjf-assign-result');
+    resultBox.style.display = 'block';
+    resultBox.innerHTML = '<div class="text-muted small">正在逐个绑定,请稍候...</div>';
+    $.post('ajax.php', {
+      gn: 'p_zjmf_admin_assign_host',
+      supplier_id: supplierId,
+      user_id: uid,
+      up_host_id: ids
+    }, function (r) {
+      var d = res(r);
+      btn.disabled = false;
+      btn.textContent = '批量指派所选主机';
+      if (d.qk != 1 && !d.success) {
+        resultBox.innerHTML = '<div class="alert alert-danger py-2 small mb-0">'
+          + esc(d.msg || '操作失败') + '</div>';
+        return;
+      }
+      var rows = d.data && d.data.results ? d.data.results : [];
+      var html = '<div class="card"><div class="card-header py-2"><b>指派结果</b></div>'
+        + '<div class="card-body p-0"><table class="table table-bordered table-sm mb-0">'
+        + '<thead><tr><th>机器</th><th>状态</th><th>本地主机ID</th><th>说明</th></tr></thead><tbody>';
+      var okCount = 0;
+      for (var i = 0; i < rows.length; i++) {
+        var it = rows[i];
+        if (it.ok) okCount++;
+        html += '<tr>'
+          + '<td>' + esc(it.domain || ('上游#' + it.up_host_id)) + '</td>'
+          + '<td>' + (it.ok ? '<span class="badge badge-success">指派成功</span>'
+              : '<span class="badge badge-danger">失败</span>') + '</td>'
+          + '<td>' + (it.host_id ? it.host_id : '-') + '</td>'
+          + '<td class="small">' + esc(it.msg || '-') + '</td>'
+          + '</tr>';
+      }
+      html += '</tbody></table></div></div>';
+      if (okCount === rows.length) {
+        html += '<div class="alert alert-success py-2 small mb-0 mt-2">全部指派成功</div>';
+      } else {
+        html += '<div class="alert alert-warning py-2 small mb-0 mt-2">成功 ' + okCount + ' / '
+          + rows.length + ',失败的机器可能已指派给其他用户。</div>';
+      }
+      resultBox.innerHTML = html;
+    });
+  });
+
+  document.getElementById('zjf-assign-reset').addEventListener('click', function () {
+    listBox.innerHTML = '<tr><td colspan="8" class="text-center text-muted">'
+      + '请先选择供应商并加载主机列表</td></tr>';
+    tip.textContent = '';
+    userId.value = 0;
+    userList.innerHTML = '';
+    document.getElementById('zjf-assign-user-kw').value = '';
+    document.getElementById('zjf-assign-all').checked = false;
+  });
+})();
+</script>
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/hosts.php b/app_plugins/zjmfmanager_reserve/views/admin/hosts.php
index 69a1c77..9204093 100644
--- a/app_plugins/zjmfmanager_reserve/views/admin/hosts.php
+++ b/app_plugins/zjmfmanager_reserve/views/admin/hosts.php
@@ -10,22 +10,31 @@ mnbt_admin_include('head');
 $page = max(1, (int)($_GET['page_num'] ?? 1));
 $hosts = zjmf_host_list_all($page, 30);
 $status_labels = [
-	'active'  => '运行中',
-	'suspend' => '已暂停',
-	'unknown' => '未知',
+	'active'     => '运行中',
+	'suspend'    => '已暂停',
+	'pending'    => '待开通',
+	'terminated' => '已终止',
+	'unknown'    => '未知',
 ];
 $status_classes = [
-	'active'  => 'badge-success',
-	'suspend' => 'badge-danger',
-	'unknown' => 'badge-secondary',
+	'active'     => 'badge-success',
+	'suspend'    => 'badge-danger',
+	'pending'    => 'badge-warning',
+	'terminated' => 'badge-dark',
+	'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-header">
+      <h4 style="display:inline-block">主机管理</h4>
+      <button type="button" class="btn btn-primary btn-sm float-right" id="zjf-refresh-all">全部刷新状态</button>
+    </div>
     <div class="card-body">
-      <p class="text-muted">主机映射为开通成功的上游主机。可点击「刷新状态」同步上游实时状态。</p>
+      <p class="text-muted">主机映射为开通成功的上游主机。可点击「刷新状态」同步上游实时状态;
+        点「全部刷新状态」可批量修复存量未知/过期缓存(逐台上游查询)。</p>
+      <div id="zjf-refresh-all-msg" class="small mb-2" style="display:none;"></div>
       <div class="table-responsive">
         <table class="table table-bordered table-hover">
           <thead>
@@ -59,7 +68,7 @@ $title = $title ?? '主机管理';
                     </span>
                   </td>
                   <td><?= htmlspecialchars($h['cycle'] ?: '-') ?></td>
-                  <td><?= htmlspecialchars($h['renew_date'] ?: '-') ?></td>
+                  <td><?= htmlspecialchars(zjmf_normalize_date((string)$h['renew_date']) ?: '-') ?></td>
                   <td class="small"><?= htmlspecialchars($h['created_at']) ?></td>
                   <td>
                     <?php if ((int)$h['up_host_id'] > 0): ?>
@@ -124,4 +133,30 @@ document.querySelectorAll('.zjf-refresh').forEach(function (btn) {
     });
   });
 });
+document.getElementById('zjf-refresh-all').addEventListener('click', function () {
+  var self = this;
+  var msgBox = document.getElementById('zjf-refresh-all-msg');
+  self.disabled = true;
+  self.textContent = '刷新中,请稍候...';
+  msgBox.style.display = 'block';
+  msgBox.className = 'small mb-2 text-muted';
+  msgBox.textContent = '正在逐台上游查询,请勿关闭页面...';
+  $.post('ajax.php', {gn: 'p_zjmf_admin_fetch_all_hosts'}, function (res) {
+    var d;
+    try { d = typeof res === 'string' ? JSON.parse(res) : res; } catch (e) { d = {code: res}; }
+    self.disabled = false;
+    self.textContent = '全部刷新状态';
+    if (d.qk != 1 && !d.success) {
+      msgBox.className = 'small mb-2 text-danger';
+      msgBox.textContent = '刷新失败:' + (d.msg || d.code || '未知错误');
+      return;
+    }
+    var s = d.data || {};
+    msgBox.className = 'small mb-2 text-success';
+    msgBox.textContent = '刷新完成:共 ' + s.total + ' 台,成功 ' + s.ok
+      + ',失败 ' + s.fail + ',缺少上游ID ' + s.no_up
+      + ',状态变更 ' + s.changed + ',仍为未知 ' + s.unknown + '。';
+    setTimeout(function () { location.reload(); }, 1500);
+  });
+});
 </script>
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/suppliers.php b/app_plugins/zjmfmanager_reserve/views/admin/suppliers.php
index ffddb19..677577b 100644
--- a/app_plugins/zjmfmanager_reserve/views/admin/suppliers.php
+++ b/app_plugins/zjmfmanager_reserve/views/admin/suppliers.php
@@ -103,6 +103,7 @@ $suppliers = zjmf_supplier_list_all();
               <th>站点</th>
               <th>加价</th>
               <th>超时</th>
+              <th>上游余额</th>
               <th>状态</th>
               <th>排序</th>
               <th>备注</th>
@@ -112,7 +113,7 @@ $suppliers = zjmf_supplier_list_all();
           </thead>
           <tbody>
             <?php if (empty($suppliers)): ?>
-              <tr><td colspan="10" class="text-center text-muted">
+              <tr><td colspan="11" class="text-center text-muted">
                 暂无供应商,请点击右上角「新增供应商」
               </td></tr>
             <?php else: ?>
@@ -125,6 +126,11 @@ $suppliers = zjmf_supplier_list_all();
                     htmlspecialchars(zjmf_supplier_markup_label($s), ENT_QUOTES)
                   ?></td>
                   <td><?= (int)$s['api_timeout'] ?>s</td>
+                  <td class="small text-nowrap">
+                    <span class="zjf-balance" data-id="<?= (int)$s['id'] ?>">-</span>
+                    <button type="button" class="btn btn-sm btn-outline-primary zjf-balance-refresh"
+                            data-id="<?= (int)$s['id'] ?>">刷新</button>
+                  </td>
                   <td>
                     <span class="badge <?= $s['status'] == 1 ? 'badge-success' : 'badge-secondary' ?>">
                       <?= $s['status'] == 1 ? '启用' : '停用' ?>
@@ -266,6 +272,39 @@ $suppliers = zjmf_supplier_list_all();
     });
   });
 
+  function formatCredit(v) {
+    if (v === undefined || v === null || v === '') return '-';
+    var n = Number(v);
+    return isNaN(n) ? String(v) : n.toFixed(2);
+  }
+
+  function loadBalance(id, btn) {
+    $.post('ajax.php', {gn: 'p_zjmf_admin_supplier_balance', id: id}, function (r) {
+      var d = res(r);
+      var ok = d.qk == 1 || d.success;
+      var cell = document.querySelector('.zjf-balance[data-id="' + id + '"]');
+      if (cell) {
+        cell.textContent = ok ? formatCredit(d.credit) : '获取失败';
+        cell.title = ok ? '上游余额(响应字段 credit)' : (d.msg || '获取失败');
+      }
+      if (btn) { btn.disabled = false; btn.textContent = '刷新'; }
+    });
+  }
+
+  document.querySelectorAll('.zjf-balance-refresh').forEach(function (btn) {
+    btn.addEventListener('click', function () {
+      var id = btn.getAttribute('data-id');
+      btn.disabled = true;
+      btn.textContent = '获取中...';
+      loadBalance(id, btn);
+    });
+  });
+
+  // 页面加载后自动拉取各供应商上游余额
+  document.querySelectorAll('.zjf-balance-refresh').forEach(function (btn) {
+    loadBalance(btn.getAttribute('data-id'), null);
+  });
+
   document.querySelectorAll('.zjf-toggle').forEach(function (btn) {
     btn.addEventListener('click', function () {
       post({gn: 'p_zjmf_admin_toggle_supplier', id: btn.getAttribute('data-id')}, true);
diff --git a/app_plugins/zjmfmanager_reserve/views/host.php b/app_plugins/zjmfmanager_reserve/views/host.php
index c2197b9..e390668 100644
--- a/app_plugins/zjmfmanager_reserve/views/host.php
+++ b/app_plugins/zjmfmanager_reserve/views/host.php
@@ -9,10 +9,146 @@ $page_title = $page_title ?? '主机详情';
 $host = $host ?? null;
 $info = $info ?? ['ok' => false, 'msg' => ''];
 $traffic = $traffic ?? ['ok' => false, 'data' => []];
+$dcim = $dcim ?? ['ok' => false, 'data' => []];
+$config_options = $config_options ?? [];
+$custom_fields = $custom_fields ?? [];
+$os_list = $os_list ?? [];
+$os_groups = $os_groups ?? [];
+$os_error = $os_error ?? '';
 
 $password = zjmf_decrypt((string)$host['password']);
 $hasUpId = (int)$host['up_host_id'] > 0;
 
+// 上游详情字段(label => [显示名, 分组])。价格 / 付款相关字段一律不展示。
+$up_fields = [
+	'productname'              => ['产品名', '产品信息'],
+	'groupname'                => ['产品组名', '产品信息'],
+	'os'                       => ['操作系统', '产品信息'],
+	'domain'                   => ['主机名', '产品信息'],
+	'dedicatedip'              => ['独立 IP', '网络信息'],
+	'assignedips'              => ['附加 IP', '网络信息'],
+	'ip_num'                   => ['IP 数量', '网络信息'],
+	'port'                     => ['端口', '网络信息'],
+	'username'                 => ['服务器用户名', '账号信息'],
+	'password'                 => ['服务器密码', '账号信息'],
+	'regdate'                  => ['开通时间', '生命周期'],
+	'nextduedate'              => ['到期时间', '生命周期'],
+	'domainstatus'             => ['产品状态', '生命周期'],
+	'suspendreason'            => ['暂停原因', '其他'],
+	'auto_terminate_end_cycle' => ['到期自动取消', '其他'],
+	'auto_terminate_reason'    => ['取消原因', '其他'],
+	'bwusage'                  => ['当前使用流量', '其他'],
+	'bwlimit'                  => ['流量上限', '其他'],
+	'remark'                   => ['备注', '其他'],
+	'allow_upgrade_config'     => ['支持升级配置', '其他'],
+	'allow_upgrade_product'    => ['支持升级产品', '其他'],
+	'show_traffic_usage'       => ['显示用量图', '其他'],
+	'productid'                => ['产品 ID', '其他'],
+	'serverid'                 => ['服务器 ID', '其他'],
+	'ordernum'                 => ['上游订单号', '其他'],
+];
+$up_group_order = ['产品信息', '网络信息', '账号信息', '生命周期', '其他'];
+$up_groups = [];
+if (is_array($info['data']) && $info['data'] !== []) {
+	foreach ($up_fields as $k => $cfg) {
+		[$label, $group] = $cfg;
+		if (!array_key_exists($k, $info['data'])) {
+			continue;
+		}
+		$v = $info['data'][$k];
+		if ($v === null || $v === '' || $v === []) {
+			continue;
+		}
+		// 时间戳 → 本地日期时间
+		if (in_array($k, ['regdate', 'nextduedate'], true) && is_numeric($v)) {
+			$v = zjmf_normalize_date((string)$v);
+		}
+		// 布尔开关
+		if (in_array($k, ['auto_terminate_end_cycle', 'allow_upgrade_config', 'allow_upgrade_product', 'show_traffic_usage'], true)) {
+			$v = (string)$v === '1' ? '是' : ((string)$v === '0' ? '否' : $v);
+		}
+		// 流量上限 0 表示不限
+		if ($k === 'bwlimit' && (string)$v === '0') {
+			$v = '不限';
+		}
+		// 状态转中文
+		if ($k === 'domainstatus') {
+			$v = zjmf_host_status_label(zjmf_map_upstream_status((string)$v));
+		}
+		// 上游 password 为明文,直接展示(存本地库时才加密)
+		if ($k === 'password') {
+			$v = (string)$v === '' ? '(空)' : $v;
+		}
+		if (is_array($v)) {
+			$v = json_encode($v, JSON_UNESCAPED_UNICODE);
+		}
+		if ((string)$v === '') {
+			continue;
+		}
+		$up_groups[$group][] = ['label' => $label, 'value' => (string)$v];
+	}
+}
+
+// 端口:host/product 的 host_data.port 是真实端口,优先使用;
+// 为空 / 0 时再依次从配置选项、自定义字段中识别(名称含“端口/port”),同样排除无效值
+$displayPort = '';
+$rawPort = trim((string)($info['data']['port'] ?? ''));
+if ($rawPort !== '' && $rawPort !== '0') {
+	$displayPort = $rawPort;
+}
+if ($displayPort === '') {
+	foreach (($config_options ?: []) as $opt) {
+		$optName = strtolower((string)($opt['name'] ?? ''));
+		if ($optName !== '' && (strpos($optName, '端口') !== false || strpos($optName, 'port') !== false)) {
+			$pv = trim((string)($opt['sub_name'] ?? ''));
+			if ($pv !== '' && $pv !== '0') {
+				$displayPort = $pv;
+				break;
+			}
+		}
+	}
+}
+if ($displayPort === '') {
+	foreach (($custom_fields ?: []) as $cf) {
+		$cfName = strtolower((string)($cf['fieldname'] ?? $cf['name'] ?? ''));
+		if ($cfName !== '' && (strpos($cfName, '端口') !== false || strpos($cfName, 'port') !== false)) {
+			$cfv = trim((string)($cf['value'] ?? ''));
+			if ($cfv !== '' && $cfv !== '0') {
+				$displayPort = $cfv;
+				break;
+			}
+		}
+	}
+}
+// DCIM 交换机端口名兜底(dcim/detail 的 switch[].name)
+if ($displayPort === '') {
+	$switchList = $dcim['data']['detail']['switch'] ?? [];
+	if (is_array($switchList) && isset($switchList[0]['name'])) {
+		$displayPort = trim((string)$switchList[0]['name']);
+	}
+}
+if ($displayPort === '0') {
+	$displayPort = '';
+}
+// 端口行统一用正确端口,无效(空 / 0)则不展示
+if ($displayPort !== '' && isset($up_groups['网络信息'])) {
+	foreach ($up_groups['网络信息'] as $i => $r) {
+		if ($r['label'] === '端口') {
+			$up_groups['网络信息'][$i]['value'] = $displayPort;
+			break;
+		}
+	}
+}
+// 配置选项:剔除已单独展示的端口项,避免重复
+if (is_array($config_options)) {
+	$config_options = array_values(array_filter($config_options, function ($opt) {
+		$name = strtolower((string)($opt['name'] ?? ''));
+		return !($name !== '' && (strpos($name, '端口') !== false || strpos($name, 'port') !== false));
+	}));
+}
+
+$liveIp = (string)($info['data']['dedicatedip'] ?? $info['data']['ip'] ?? '');
+
 function zjmf_view_show_traffic($data)
 {
 	if (!is_array($data) || $data === []) {
@@ -32,60 +168,269 @@ function zjmf_view_show_traffic($data)
 }
 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>
+<style>
+.zj-hd{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap;
+  margin-bottom:20px;padding:24px 26px;border-radius:12px;color:#fff;
+  background:linear-gradient(120deg,#1677e0 0%,#3fa9ff 55%,#63c3ff 100%);}
+.zj-hd-title{font-size:22px;font-weight:700;margin:0 0 6px;color:#fff;letter-spacing:.3px;}
+.zj-hd-meta{font-size:12px;color:rgba(255,255,255,.88);}
+.zj-hd-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
+.zj-btn-ghost{display:inline-flex;align-items:center;padding:7px 16px;border-radius:20px;
+  background:rgba(255,255,255,.16);color:#fff;font-size:13px;text-decoration:none;transition:background .15s;}
+.zj-btn-ghost:hover{background:rgba(255,255,255,.3);color:#fff;text-decoration:none;}
+.zj-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px;}
+.zj-stat{background:#fff;border:1px solid #eef0f3;border-radius:12px;padding:16px 18px;
+  box-shadow:0 1px 2px rgba(16,24,40,.04);}
+.zj-stat-label{font-size:12px;color:#98a2b3;margin-bottom:8px;}
+.zj-stat-value{font-size:15px;font-weight:600;color:#1f2329;word-break:break-all;line-height:1.5;}
+.zj-panel{background:#fff;border:1px solid #eef0f3;border-radius:12px;margin-bottom:20px;
+  overflow:hidden;box-shadow:0 1px 2px rgba(16,24,40,.04);}
+.zj-panel-title{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:600;color:#1f2329;
+  padding:14px 20px;border-bottom:1px solid #f2f3f5;}
+.zj-panel-title::before{content:'';width:4px;height:14px;border-radius:2px;background:#1e9fff;}
+.zj-panel-body{padding:6px 20px 16px;}
+.zj-group{margin-top:12px;}
+.zj-group-title{font-size:12px;color:#98a2b3;font-weight:600;margin-bottom:2px;letter-spacing:.5px;}
+.zj-kv{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 36px;}
+.zj-kv-item{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;
+  padding:9px 0;border-bottom:1px dashed #f0f1f3;font-size:13px;}
+.zj-kv-label{color:#86909c;flex-shrink:0;}
+.zj-kv-value{color:#1f2329;text-align:right;word-break:break-all;}
+.zj-tip{font-size:12px;color:#98a2b3;}
+.zj-panel-body .zj-desc2{padding:6px 0 0;font-size:13px;color:#555;line-height:1.7;}
+@media(max-width:760px){
+  .zj-stats{grid-template-columns:repeat(2,1fr);}
+  .zj-kv{grid-template-columns:1fr;}
+  .zj-kv-item{flex-direction:column;gap:2px;}
+  .zj-kv-value{text-align:left;}
+}
+</style>
+
+<div class="zj-hd">
+  <div>
+    <h1 class="zj-hd-title"><?= htmlspecialchars($host['name']) ?></h1>
+    <div class="zj-hd-meta">
+      <?= htmlspecialchars(($host['supplier_name'] ?? '') ?: '魔方财务上游') ?>
+      <?php if (!empty($host['product_name'])): ?>
+        · <?= htmlspecialchars($host['product_name']) ?>
+      <?php endif; ?>
+      · 上游主机 ID:<?= (int)$host['up_host_id'] ?>
+    </div>
+  </div>
+  <div class="zj-hd-right">
+    <a class="zj-btn-ghost" href="<?= zjmf_url('reserve/hosts') ?>">返回主机列表</a>
+  </div>
 </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'])) ?>
+<div class="zj-stats">
+  <div class="zj-stat">
+    <div class="zj-stat-label">状态</div>
+    <div class="zj-stat-value">
+      <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-tip">实时:<?= htmlspecialchars(zjmf_host_status_label($info['status'])) ?></span>
+      <?php endif; ?>
+    </div>
+  </div>
+  <div class="zj-stat">
+    <div class="zj-stat-label">IP 地址</div>
+    <div class="zj-stat-value zj-mono"><?= htmlspecialchars($liveIp ?: '-') ?></div>
+  </div>
+  <div class="zj-stat">
+    <div class="zj-stat-label">端口</div>
+    <div class="zj-stat-value zj-mono"><?= htmlspecialchars($displayPort ?: '-') ?></div>
+  </div>
+  <div class="zj-stat">
+    <div class="zj-stat-label">到期时间</div>
+    <div class="zj-stat-value"><?= htmlspecialchars($host['renew_date'] ?: '-') ?></div>
+  </div>
+  <div class="zj-stat">
+    <div class="zj-stat-label">用户名</div>
+    <div class="zj-stat-value zj-mono"><?= htmlspecialchars($host['username'] ?: '-') ?></div>
+  </div>
+  <div class="zj-stat">
+    <div class="zj-stat-label">密码</div>
+    <div class="zj-stat-value zj-mono"><?= htmlspecialchars($password ?: '-') ?></div>
+  </div>
+  <div class="zj-stat">
+    <div class="zj-stat-label">周期</div>
+    <div class="zj-stat-value"><?= htmlspecialchars($host['cycle'] ?: '-') ?></div>
+  </div>
+  <div class="zj-stat">
+    <div class="zj-stat-label">上游主机 ID</div>
+    <div class="zj-stat-value zj-mono"><?= (int)$host['up_host_id'] ?></div>
+  </div>
+</div>
+
+<?php if ($up_groups !== []): ?>
+<div class="zj-panel">
+  <div class="zj-panel-title">上游详情</div>
+  <div class="zj-panel-body">
+    <?php foreach ($up_group_order as $gname): ?>
+      <?php if (empty($up_groups[$gname])) continue; ?>
+      <div class="zj-group">
+        <div class="zj-group-title"><?= htmlspecialchars($gname) ?></div>
+        <div class="zj-kv">
+          <?php foreach ($up_groups[$gname] as $r): ?>
+            <div class="zj-kv-item">
+              <span class="zj-kv-label"><?= htmlspecialchars($r['label']) ?></span>
+              <span class="zj-kv-value <?= in_array($r['label'], ['独立 IP', '附加 IP', '服务器用户名', '服务器密码', '端口', '上游订单号'], true) ? 'zj-mono' : '' ?>"><?=
+                htmlspecialchars($r['value'])
+              ?></span>
+            </div>
+          <?php endforeach; ?>
+        </div>
+      </div>
+    <?php endforeach; ?>
+  </div>
+</div>
+<?php endif; ?>
+
+<?php if ($config_options !== []): ?>
+<div class="zj-panel">
+  <div class="zj-panel-title">配置选项</div>
+  <div class="zj-panel-body">
+    <div class="zj-kv">
+      <?php foreach ($config_options as $opt): ?>
+        <div class="zj-kv-item">
+          <span class="zj-kv-label"><?= htmlspecialchars((string)($opt['name'] ?? '')) ?></span>
+          <span class="zj-kv-value"><?= htmlspecialchars((string)($opt['sub_name'] ?? '')) ?></span>
+        </div>
+      <?php endforeach; ?>
+    </div>
+  </div>
+</div>
+<?php endif; ?>
+
+<?php if ($custom_fields !== []): ?>
+<div class="zj-panel">
+  <div class="zj-panel-title">自定义字段</div>
+  <div class="zj-panel-body">
+    <div class="zj-kv">
+      <?php foreach ($custom_fields as $cf): ?>
+        <div class="zj-kv-item">
+          <span class="zj-kv-label"><?= htmlspecialchars((string)($cf['fieldname'] ?? $cf['name'] ?? '')) ?></span>
+          <span class="zj-kv-value"><?= htmlspecialchars((string)($cf['value'] ?? '')) ?></span>
+        </div>
+      <?php endforeach; ?>
+    </div>
+  </div>
+</div>
+<?php endif; ?>
+
+<?php
+// DCIM 信息(电源/交换机端口/重装次数/任务进度),仅 DCIM 产品返回
+$dcimData = is_array($dcim['data'] ?? null) ? $dcim['data'] : [];
+$dcimPower = (string)($dcimData['power'] ?? '');
+$dcimSwitch = is_array($dcimData['detail']['switch'] ?? null) ? $dcimData['detail']['switch'] : [];
+$dcimReinstall = is_array($dcimData['reinstall'] ?? null) ? $dcimData['reinstall'] : [];
+$dcimTask = is_array($dcimData['task'] ?? null) ? $dcimData['task'] : [];
+$hasDcim = $dcimPower !== '' || $dcimSwitch !== [] || $dcimReinstall !== [] || isset($dcimTask['progress']) || !empty($dcimTask['step']);
+$taskTypeMap = ['0' => '重装系统', '1' => '救援系统', '2' => '重置密码', '3' => '获取硬件信息'];
+?>
+<?php if ($hasDcim): ?>
+<div class="zj-panel">
+  <div class="zj-panel-title">DCIM 信息</div>
+  <div class="zj-panel-body">
+    <?php if ($dcimPower !== ''): ?>
+      <div class="zj-kv">
+        <div class="zj-kv-item">
+          <span class="zj-kv-label">电源状态</span>
+          <span class="zj-kv-value">
+            <?php if ($dcimPower === 'on'): ?><span class="zj-status zj-status-active">已开机</span>
+            <?php elseif ($dcimPower === 'off'): ?><span class="zj-status zj-status-suspend">已关机</span>
+            <?php else: ?><span class="zj-tip"><?= htmlspecialchars($dcimPower . ($dcimData['power_msg'] !== '' ? '(' . $dcimData['power_msg'] . ')' : '')) ?></span>
+            <?php endif; ?>
           </span>
-          <?php if (!empty($info['ok']) && $info['status'] !== $host['status']): ?>
-            <span class="zj-muted">(实时:<?= htmlspecialchars(zjmf_host_status_label($info['status'])) ?>)</span>
+        </div>
+      </div>
+    <?php endif; ?>
+    <?php if ($dcimSwitch !== []): ?>
+      <div class="zj-group">
+        <div class="zj-group-title">交换机端口</div>
+        <div class="zj-kv">
+          <?php foreach ($dcimSwitch as $sw): ?>
+            <div class="zj-kv-item">
+              <span class="zj-kv-label">交换机 #<?= htmlspecialchars((string)($sw['switch_id'] ?? '')) ?></span>
+              <span class="zj-kv-value zj-mono"><?= htmlspecialchars((string)($sw['name'] ?? '-')) ?></span>
+            </div>
+          <?php endforeach; ?>
+        </div>
+      </div>
+    <?php endif; ?>
+    <?php if ($dcimReinstall !== []): ?>
+      <div class="zj-group">
+        <div class="zj-group-title">重装次数</div>
+        <div class="zj-kv">
+          <div class="zj-kv-item">
+            <span class="zj-kv-label">本周已用 / 上限</span>
+            <span class="zj-kv-value">
+              <?= htmlspecialchars((string)($dcimReinstall['num'] ?? '-')) ?> /
+              <?= htmlspecialchars((string)($dcimReinstall['max_times'] ?? '0')) ?>
+              <?php if ((string)($dcimReinstall['max_times'] ?? '') === '0'): ?><span class="zj-tip">(不限)</span><?php endif; ?>
+              <?php if (!empty($dcimReinstall['price'])): ?><span class="zj-tip">已达上限,可购买重装次数</span><?php endif; ?>
+            </span>
+          </div>
+        </div>
+      </div>
+    <?php endif; ?>
+    <?php if (isset($dcimTask['progress']) || !empty($dcimTask['step'])): ?>
+      <div class="zj-group">
+        <div class="zj-group-title">当前任务</div>
+        <div class="zj-kv">
+          <div class="zj-kv-item">
+            <span class="zj-kv-label">任务进度</span>
+            <span class="zj-kv-value">
+              <?= htmlspecialchars((string)($dcimTask['progress'] ?? '0')) ?>%
+              <?php if (isset($taskTypeMap[(string)($dcimTask['task_type'] ?? '')])): ?>
+                <span class="zj-tip">(<?= htmlspecialchars($taskTypeMap[(string)$dcimTask['task_type']]) ?>)</span>
+              <?php endif; ?>
+            </span>
+          </div>
+          <?php if (!empty($dcimTask['step'])): ?>
+            <div class="zj-kv-item"><span class="zj-kv-label">当前步骤</span><span class="zj-kv-value"><?= htmlspecialchars((string)$dcimTask['step']) ?></span></div>
+          <?php endif; ?>
+          <?php if (!empty($dcimTask['last_result']['status'])): ?>
+            <div class="zj-kv-item"><span class="zj-kv-label">上次结果</span><span class="zj-kv-value"><?= (int)$dcimTask['last_result']['status'] === 1 ? '成功' : '失败' ?></span></div>
           <?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>
+          <?php if (!empty($dcimTask['error_msg'])): ?>
+            <div class="zj-kv-item"><span class="zj-kv-label">提示</span><span class="zj-kv-value" style="color:#c62828;"><?= htmlspecialchars((string)$dcimTask['error_msg']) ?></span></div>
+          <?php endif; ?>
+        </div>
+      </div>
+    <?php endif; ?>
   </div>
 </div>
+<?php endif; ?>
 
-<div class="layui-card">
-  <div class="layui-card-header">流量使用</div>
-  <div class="layui-card-body" style="padding:16px;font-size:13px;color:#555;">
+<div class="zj-panel">
+  <div class="zj-panel-title">流量使用</div>
+  <div class="zj-panel-body">
     <?php if (!empty($traffic['ok'])): ?>
-      <?= zjmf_view_show_traffic($traffic['data']) ?>
+      <div class="zj-desc2"><?= zjmf_view_show_traffic($traffic['data']) ?></div>
     <?php else: ?>
-      <span class="zj-muted"><?= (($traffic['msg'] ?? '') ?: '流量查询失败') ?></span>
+      <span class="zj-tip"><?= (($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>
+<div class="zj-panel">
+  <div class="zj-panel-title">主机操作</div>
+  <div class="zj-panel-body">
+    <p class="zj-tip" style="margin:8px 0 14px;">重启 / 重装 / 救援 / 重置密码为高危操作,请谨慎执行。</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 layui-btn-danger" data-act="rescue">救援系统</button>
     <button type="button" class="layui-btn layui-btn-sm" data-act="reset_password">重置密码</button>
+    <button type="button" class="layui-btn layui-btn-sm" data-act="bmc">重置 BMC</button>
+    <button type="button" class="layui-btn layui-btn-sm" data-act="cancel_task">取消任务</button>
 
     <div id="zjf-pass-panel" style="display:none;margin-top:14px;padding:14px;
       background:#fafbfc;border:1px solid #eee;border-radius:6px;">
@@ -94,6 +439,53 @@ ob_start();
       <button type="button" class="layui-btn layui-btn-sm" id="zjf-pass-confirm"
               style="margin-top:10px;">确认重置</button>
     </div>
+
+    <?php if ($os_list !== []): ?>
+    <div id="zjf-reinstall-panel" style="display:none;margin-top:14px;padding:14px;
+      background:#fafbfc;border:1px solid #eee;border-radius:6px;">
+      <select id="zjf-os-select" class="layui-input" style="max-width:280px;display:inline-block;">
+        <option value="">请选择操作系统</option>
+        <?php
+        // 系统列表按分组渲染(host/dedicatedserver 返回 cloud_os + cloud_os_group)
+        $groupNames = [];
+        foreach ($os_groups as $g) {
+            $groupNames[(string)($g['id'] ?? '')] = (string)($g['name'] ?? '');
+        }
+        $osByGroup = [];
+        foreach ($os_list as $os) {
+            $osByGroup[(string)($os['group'] ?? '')][] = $os;
+        }
+        if ($osByGroup === []) {
+            $osByGroup[''] = $os_list;
+        }
+        foreach ($osByGroup as $gid => $items):
+          $gLabel = $gid !== '' ? (string)($groupNames[$gid] ?? $gid) : '';
+        ?>
+          <?php if ($gLabel !== ''): ?>
+          <optgroup label="<?= htmlspecialchars($gLabel) ?>">
+          <?php endif; ?>
+          <?php foreach ($items as $os): ?>
+            <option value="<?= (int)($os['id'] ?? 0) ?>" data-group="<?= htmlspecialchars($gLabel) ?>"><?= htmlspecialchars((string)($os['name'] ?? '')) ?></option>
+          <?php endforeach; ?>
+          <?php if ($gLabel !== ''): ?>
+          </optgroup>
+          <?php endif; ?>
+        <?php endforeach; ?>
+      </select>
+      <button type="button" class="layui-btn layui-btn-sm layui-btn-danger" id="zjf-reinstall-confirm"
+              style="margin-top:10px;">确认重装</button>
+    </div>
+    <?php endif; ?>
+
+    <div id="zjf-rescue-panel" style="display:none;margin-top:14px;padding:14px;
+      background:#fafbfc;border:1px solid #eee;border-radius:6px;">
+      <select id="zjf-rescue-system" class="layui-input" style="max-width:160px;display:inline-block;">
+        <option value="1">Linux</option>
+        <option value="2">Windows</option>
+      </select>
+      <button type="button" class="layui-btn layui-btn-sm layui-btn-danger" id="zjf-rescue-confirm"
+              style="margin-left:10px;">确认进入救援系统</button>
+    </div>
   </div>
 </div>
 <?php endif; ?>
@@ -103,12 +495,21 @@ ob_start();
   var msg = document.getElementById('zjf-msg');
   var passPanel = document.getElementById('zjf-pass-panel');
   var passInput = document.getElementById('zjf-pass-input');
+  var reinstallPanel = document.getElementById('zjf-reinstall-panel');
+  var rescuePanel = document.getElementById('zjf-rescue-panel');
+  var osCount = <?= is_array($os_list) ? count($os_list) : 0 ?>;
+  var osError = <?= json_encode((string)$os_error, JSON_UNESCAPED_UNICODE) ?>;
   var pendingAction = '';
 
   function show(text, ok) {
     msg.textContent = text;
     msg.className = 'zj-msg zj-msg-show ' + (ok ? 'zj-msg-success' : 'zj-msg-error');
   }
+  function hidePanels() {
+    if (passPanel) passPanel.style.display = 'none';
+    if (reinstallPanel) reinstallPanel.style.display = 'none';
+    if (rescuePanel) rescuePanel.style.display = 'none';
+  }
   function call(action, extra) {
     var body = new URLSearchParams();
     body.append('host_id', '<?= (int)$host['id'] ?>');
@@ -130,6 +531,7 @@ ob_start();
   document.querySelectorAll('[data-act]').forEach(function (btn) {
     btn.addEventListener('click', function () {
       var act = btn.getAttribute('data-act');
+      hidePanels();
       if (act === 'reset_password') {
         pendingAction = act;
         passPanel.style.display = 'block';
@@ -137,7 +539,32 @@ ob_start();
         return;
       }
       if (act === 'reinstall') {
-        if (!confirm('确定要重装该系统吗?重装将清空数据且不可恢复!')) return;
+        // 系统列表来自 host/dedicatedserver,重装统一走 dcim/reinstall(共用重装端点)
+        if (osCount > 0 && reinstallPanel) {
+          reinstallPanel.style.display = 'block';
+          return;
+        }
+        show('无法获取系统列表:' + osError, false);
+        return;
+      }
+      if (act === 'rescue') {
+        if (rescuePanel) {
+          rescuePanel.style.display = 'block';
+          return;
+        }
+        if (!confirm('确定进入救援系统(Linux)吗?')) return;
+        call('rescue', {system: 1});
+        return;
+      }
+      if (act === 'bmc') {
+        if (!confirm('确定要重置该主机的 BMC 吗?')) return;
+        call('bmc', {});
+        return;
+      }
+      if (act === 'cancel_task') {
+        if (!confirm('确定要取消当前的重装/救援/重置密码任务吗?')) return;
+        call('cancel_task', {});
+        return;
       }
       if (act === 'reboot') {
         if (!confirm('确定要重启该主机吗?')) return;
@@ -154,6 +581,28 @@ ob_start();
     }
     call('reset_password', {password: pwd});
   });
+
+  if (reinstallPanel) {
+    document.getElementById('zjf-reinstall-confirm').addEventListener('click', function () {
+      var sel = document.getElementById('zjf-os-select');
+      var os = sel.value;
+      if (!os) { show('请选择操作系统', false); return; }
+      var group = '';
+      if (sel.selectedIndex >= 0 && sel.options[sel.selectedIndex]) {
+        group = sel.options[sel.selectedIndex].getAttribute('data-group') || '';
+      }
+      if (!confirm('确定要重装该系统吗?重装将清空数据且不可恢复!')) return;
+      call('dcim_reinstall', {os: os, os_group: group});
+    });
+  }
+
+  if (rescuePanel) {
+    document.getElementById('zjf-rescue-confirm').addEventListener('click', function () {
+      var system = document.getElementById('zjf-rescue-system').value;
+      if (!confirm('确定进入救援系统吗?当前数据可能受影响,请谨慎操作。')) return;
+      call('rescue', {system: system});
+    });
+  }
 })();
 </script>
 <?php
diff --git a/app_plugins/zjmfmanager_reserve/views/hosts.php b/app_plugins/zjmfmanager_reserve/views/hosts.php
index 0114a00..ae18390 100644
--- a/app_plugins/zjmfmanager_reserve/views/hosts.php
+++ b/app_plugins/zjmfmanager_reserve/views/hosts.php
@@ -49,7 +49,7 @@ ob_start();
                   <?= htmlspecialchars(zjmf_host_status_label($host['status'])) ?>
                 </span>
               </td>
-              <td class="zj-mono"><?= htmlspecialchars(zjmf_mask_account($host['username'])) ?></td>
+              <td class="zj-mono"><?= htmlspecialchars(($host['username'] ?: '-')) ?></td>
               <td><?= htmlspecialchars($host['cycle'] ?: '-') ?></td>
               <td><?= htmlspecialchars($host['renew_date'] ?: '-') ?></td>
               <td>
diff --git a/templates/tdesign/spa/src/account/views/ZjmfAssetsView.vue b/templates/tdesign/spa/src/account/views/ZjmfAssetsView.vue
index f194e68..b845785 100644
--- a/templates/tdesign/spa/src/account/views/ZjmfAssetsView.vue
+++ b/templates/tdesign/spa/src/account/views/ZjmfAssetsView.vue
@@ -24,6 +24,10 @@
         </div>
 
         <div class="zjmf-asset-meta">
+          <div class="meta-item">
+            <span>供应商</span>
+            <b>{{ h.supplier_name || '—' }}</b>
+          </div>
           <div class="meta-item">
             <span>主机账号</span>
             <b class="mono">{{ h.username || '—' }}</b>
@@ -132,7 +136,7 @@ const newPassword = ref('')
 const pwdSubmitting = ref(false)
 
 function statusText(status) {
-  return { active: '运行中', suspend: '已暂停', unknown: '未知' }[status] || status
+  return { active: '运行中', suspend: '已暂停', pending: '待开通', terminated: '已终止', unknown: '未知' }[status] || status
 }
 
 function cycleText(cycle) {
@@ -150,6 +154,8 @@ function cycleText(cycle) {
 function statusTheme(status) {
   if (status === 'active') return 'success'
   if (status === 'suspend') return 'warning'
+  if (status === 'pending') return 'warning'
+  if (status === 'terminated') return 'danger'
   return 'default'
 }