better-staridc-MNBT
1<?php
2/**
3 * 梦奈宝塔Docker对接插件 — 魔方财务(idcsmart)server module
4 *
5 * 通过 MNBT 外部 API 对接 Docker 容器分销。
6 * 单容器模型:CreateAccount 仅开通账号,容器由用户登录 MNBT 控制台后在应用商店创建。
7 *
8 * 关联:api/docker.php(M1 已实现 gn=kt/zt/jc/tj/xf/bg/czmm/ztcx/sy/start/stop/restart)
9 */
10
11// ========================================================================
12// MetaData
13// ========================================================================
14
15function mnbtdocker_MetaData()
16{
17 return [
18 'DisplayName' => '梦奈宝塔Docker对接插件',
19 'APIVersion' => '1.1',
20 'HelpDoc' => 'https://github.com/MNBT/api-docker-guide', // 替换为实际帮助文档地址
21 ];
22}
23
24// ========================================================================
25// ConfigOptions
26// ========================================================================
27
28function mnbtdocker_ConfigOptions()
29{
30 return [
31 [
32 'type' => 'text',
33 'name' => '默认套餐 ID',
34 'placeholder' => 'MN_docker_plan.id',
35 'description' => '开通时绑定的默认套餐;不填则不绑定套餐',
36 'default' => '',
37 'key' => 'plan_id',
38 ],
39 [
40 'type' => 'text',
41 'name' => '控制台地址',
42 'placeholder' => 'https://mnbt.example.com/docker/login.php',
43 'description' => 'Docker 控制台入口(用户需用 Docker 账号登录)',
44 'default' => '',
45 'key' => 'console_url',
46 ],
47 ];
48}
49
50// ========================================================================
51// 内部工具:参数解析 & API 调用
52// ========================================================================
53
54/**
55 * 解析参数(连接信息全部来自服务器设置,配置选项仅产品级别)
56 * 服务器字段映射(参照 noKVM 模式):
57 * server_ip / server_host → MNBT 主机
58 * port → 端口
59 * secure → HTTPS
60 * server_username → 节点编号(MN_docker_node.id)
61 * server_password → 调用密钥 md5(ktmy.qmk),空则默认 md5('')
62 * accesshash → 系统 API 密钥($conf['api'])
63 *
64 * @return array [api_url, api_key, node_id, call_key, plan_id, console_url]
65 */
66function _mnbtdocker_resolve_params($params)
67{
68 $scheme = ($params['secure'] ?? '') == '1' ? 'https' : 'http';
69 $host = $params['server_ip'] ?: ($params['server_host'] ?? '');
70 $port = $params['port'] ?? '';
71 $api_url = '';
72 if (!empty($host)) {
73 $api_url = $scheme . '://' . $host;
74 if (!empty($port) && $port != '80' && $port != '443') {
75 $api_url .= ':' . $port;
76 }
77 $api_url .= '/api/docker.php';
78 }
79
80 $api_key = $params['accesshash'] ?? '';
81 $node_id = $params['server_username'] ?? '';
82 $call_key = $params['server_password'] ?? '';
83 // plan_id: 优先 configoption1,兼容旧位置 configoption5
84 $plan_id = $params['configoption1'] ?? ($params['configoption5'] ?? '');
85 // console_url: 优先 configoption2,兼容旧位置 configoption6,兜底 server_ip
86 $console_url = $params['configoption2'] ?? ($params['configoption6'] ?? '');
87 if (empty($console_url) && !empty($host)) {
88 $console_url = $scheme . '://' . $host;
89 if (!empty($port) && $port != '80' && $port != '443') {
90 $console_url .= ':' . $port;
91 }
92 $console_url .= '/docker/login.php';
93 }
94
95 return [$api_url, $api_key, $node_id, $call_key, $plan_id, $console_url];
96}
97
98/**
99 * 调用梦奈宝塔Docker对接插件 API
100 *
101 * @param array $params 魔方传入的 $params
102 * @param string $gn 动作(kt/zt/jc/tj/xf/bg/czmm/ztcx/sy/start/stop/restart)
103 * @param array $extra 额外 POST 字段(业务参数)
104 * @param int $timeout cURL 超时秒数
105 * @return array [success, code, msg, data?]
106 */
107function _mnbtdocker_api_call($params, $gn, $extra = [], $timeout = 30)
108{
109 list($api_url, $api_key, $node_id, $call_key) = _mnbtdocker_resolve_params($params);
110
111 if (empty($api_url)) return ['success' => false, 'code' => 0, 'msg' => '[mnbtdocker] 未配置 API 地址'];
112 if (empty($api_key)) return ['success' => false, 'code' => 0, 'msg' => '[mnbtdocker] 未配置系统 API 密钥'];
113 if (empty($node_id)) return ['success' => false, 'code' => 0, 'msg' => '[mnbtdocker] 未配置节点编号'];
114 // 调用密钥为空时默认 md5(''),适配节点未设置 ktmy/qmk 的场景
115 if (empty($call_key)) $call_key = md5('');
116
117 // Docker 账号名优先用 domain(开通时就是用它),因为 username 可能是 "root" 等主机名
118 $username = $params['domain'] ?? ($params['username'] ?? '');
119
120 $post = array_merge([
121 'mn_bh' => $node_id,
122 'mn_key' => $api_key,
123 'mn_keye' => $call_key,
124 'mn_vs' => 15,
125 'username' => $username,
126 ], $extra);
127
128 // DEBUG:仅在 MNBT_DEBUG 为 true 时打印请求参数
129 $debug_info = '';
130 if (defined('MNBT_DEBUG') && MNBT_DEBUG) {
131 $debug_info = "gn={$gn}, mn_bh=[{$post['mn_bh']}], mn_key=[len=" . strlen($post['mn_key']) . "], mn_keye=[len=" . strlen($post['mn_keye']) . "], mn_vs=[{$post['mn_vs']}], username=[{$post['username']}]";
132 }
133
134 $url = $api_url . '?gn=' . urlencode($gn);
135
136 $ch = curl_init();
137 curl_setopt_array($ch, [
138 CURLOPT_URL => $url,
139 CURLOPT_POST => true,
140 CURLOPT_POSTFIELDS => http_build_query($post),
141 CURLOPT_TIMEOUT => $timeout,
142 CURLOPT_RETURNTRANSFER => true,
143 CURLOPT_SSL_VERIFYHOST => false,
144 CURLOPT_SSL_VERIFYPEER => false,
145 CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded; charset=UTF-8'],
146 ]);
147 $resp = curl_exec($ch);
148 $errno = curl_errno($ch);
149 $error = curl_error($ch);
150 curl_close($ch);
151
152 if ($errno) {
153 return ['success' => false, 'code' => 0, 'msg' => '[mnbtdocker] cURL 错误(' . $errno . '):' . $error];
154 }
155
156 $decoded = json_decode($resp, true);
157 if ($decoded === null) {
158 return ['success' => false, 'code' => 0, 'msg' => '[mnbtdocker] 响应解析失败:' . substr($resp, 0, 200)];
159 }
160
161 // 附加调试信息到响应中(仅 MNBT_DEBUG 开启时)
162 if ($debug_info !== '') {
163 $decoded['_debug'] = $debug_info;
164 }
165 return $decoded;
166}
167
168/**
169 * 将 MNBT API 响应转为魔方模块返回值
170 * - 成功:返回 ['status' => 'success'] 或 'success'
171 * - 失败:返回 MNBT 的 msg(含 _debug 调试信息)
172 */
173function _mnbtdocker_return($api_result)
174{
175 if (($api_result['success'] ?? false) && ($api_result['code'] ?? 0) == 200) {
176 return 'success';
177 }
178 $msg = $api_result['msg'] ?? '未知错误';
179 if (!empty($api_result['_debug'])) {
180 $msg .= ' [' . $api_result['_debug'] . ']';
181 }
182 return $msg;
183}
184
185// ========================================================================
186// 测试连接
187// ========================================================================
188
189/**
190 * 测试连接(魔方命名约定:{modulename}_TestLink)
191 * 校验参数配置是否完整,然后调 gn=cfif 验证 MNBT API 可达性与鉴权
192 */
193function mnbtdocker_TestLink($params)
194{
195 list($api_url, $api_key, $node_id, $call_key) = _mnbtdocker_resolve_params($params);
196
197 if (empty($api_url)) return ['status' => 200, 'data' => ['server_status' => 0, 'msg' => '未配置服务器 IP/域名']];
198 if (empty($api_key)) return ['status' => 200, 'data' => ['server_status' => 0, 'msg' => '未配置 Access Hash(系统 API 密钥)']];
199 if (empty($node_id)) return ['status' => 200, 'data' => ['server_status' => 0, 'msg' => '未配置用户名(节点编号)']];
200
201 // call_key 可为空(_mnbtdocker_api_call 自动兜底 md5(''))
202
203 // cfif 需要 username 非空,测试连接时无产品账户故传占位值
204 $r = _mnbtdocker_api_call($params, 'cfif', ['username' => 'test'], 15);
205 if (($r['success'] ?? false) && ($r['code'] ?? 0) == 200) {
206 return ['status' => 200, 'data' => ['server_status' => 1]];
207 }
208 return ['status' => 200, 'data' => ['server_status' => 0, 'msg' => $r['msg'] ?? '未知错误']];
209}
210
211// ========================================================================
212// 生命周期方法
213// ========================================================================
214
215/**
216 * 统一把 nextduedate 转为 'Y-m-d'(后端 dqtime/setdate 期望格式)
217 * 纯数字时间戳先转日期;空值/'0000-00-00' 返回 '0'(永久)
218 */
219function _mnbtdocker_format_dqtime($nextduedate)
220{
221 $dqtime = trim((string)($nextduedate ?? ''));
222 if ($dqtime === '' || $dqtime === '0000-00-00' || $dqtime === '0000-00-00 00:00:00') {
223 return '0';
224 }
225 if (ctype_digit($dqtime)) {
226 $dqtime = date('Y-m-d', (int)$dqtime);
227 } else {
228 $ts = strtotime($dqtime);
229 $dqtime = $ts ? date('Y-m-d', $ts) : '0';
230 }
231 return $dqtime ?: '0';
232}
233
234/**
235 * 开通账户
236 * 只开通账号,不创建容器(容器由用户登录控制台后在应用商店创建)
237 */
238function mnbtdocker_CreateAccount($params)
239{
240 list(, , , , $plan_id) = _mnbtdocker_resolve_params($params);
241
242 // 使用魔方 domain 参数(主机名,随机唯一字符串)
243 $username = $params['domain'];
244
245 $password = $params['password'] ?? '';
246 if (empty($password)) {
247 $password = substr(md5(uniqid(mt_rand(), true)), 0, 12);
248 }
249
250 $dqtime = _mnbtdocker_format_dqtime($params['nextduedate'] ?? '');
251
252 $extra = [
253 'username' => $username,
254 'password' => $password,
255 'dqtime' => $dqtime,
256 'email' => $params['user_info']['email'] ?? '',
257 ];
258 if (!empty($plan_id)) {
259 $extra['plan_id'] = $plan_id;
260 }
261
262 $r = _mnbtdocker_api_call($params, 'kt', $extra);
263 $result = _mnbtdocker_return($r);
264
265 // 开通成功后把真实密码回写到魔方产品表,确保用户可在产品详情中看到密码
266 if ($result === 'success' && !empty($password) && !empty($params['hostid'])) {
267 try {
268 if (class_exists('\think\facade\Db')) {
269 // 魔方财务(ThinkPHP 6)环境:更新 host 表 password 字段
270 \think\facade\Db::name('host')
271 ->where('id', (int)$params['hostid'])
272 ->update(['password' => $password]);
273 }
274 } catch (\Throwable $e) {
275 // 回写失败不影响开通结果(如非 TP 环境或字段不存在)
276 }
277 }
278
279 return $result;
280}
281
282/** 暂停 */
283function mnbtdocker_SuspendAccount($params)
284{
285 $r = _mnbtdocker_api_call($params, 'zt');
286 return _mnbtdocker_return($r);
287}
288
289/** 解除暂停 */
290function mnbtdocker_UnsuspendAccount($params)
291{
292 $r = _mnbtdocker_api_call($params, 'jc');
293 return _mnbtdocker_return($r);
294}
295
296/** 删除(立即删除容器 + 账号) */
297function mnbtdocker_TerminateAccount($params)
298{
299 $r = _mnbtdocker_api_call($params, 'tj');
300 return _mnbtdocker_return($r);
301}
302
303/** 续费 */
304function mnbtdocker_Renew($params)
305{
306 $dqtime = _mnbtdocker_format_dqtime($params['nextduedate'] ?? '');
307 $r = _mnbtdocker_api_call($params, 'xf', ['setdate' => $dqtime]);
308 return _mnbtdocker_return($r);
309}
310
311/** 升降级套餐 */
312function mnbtdocker_ChangePackage($params)
313{
314 // 从可配置选项中获取新的 plan_id
315 // 魔方升降级时 old_configoptions / configoptions 会有新旧值
316 $new_plan_id = $params['configoptions']['plan_id'] ?? $params['config_options']['plan_id'] ?? 0;
317 $new_plan_id = is_array($new_plan_id) ? '' : trim((string)$new_plan_id);
318 if ($new_plan_id === '') {
319 // 兼容:某些魔方版本 key 在 configoptionX 直接覆盖(此时 configoption1 即新套餐)
320 $new_plan_id = trim((string)($params['configoption1'] ?? ''));
321 }
322 if ($new_plan_id === '' || (int)$new_plan_id <= 0) {
323 // 解析不到新套餐 ID 时明确报错,避免静默重复提交原套餐
324 return '未找到新套餐 ID,请确认产品可配置选项中已设置 plan_id 字段';
325 }
326 $r = _mnbtdocker_api_call($params, 'bg', ['plan_id' => $new_plan_id]);
327 return _mnbtdocker_return($r);
328}
329
330/** 重置密码(参照 noKVM:idcsmart 将新密码作为第二个参数传入) */
331function mnbtdocker_CrackPassword($params, $new_pass = '')
332{
333 if (empty($new_pass)) return '缺少新密码';
334 $r = _mnbtdocker_api_call($params, 'czmm', ['password' => $new_pass]);
335 return _mnbtdocker_return($r);
336}
337
338// ========================================================================
339// 状态 & 同步
340// ========================================================================
341
342/**
343 * 获取机器状态
344 */
345function mnbtdocker_Status($params)
346{
347 $r = _mnbtdocker_api_call($params, 'ztcx', [], 25);
348 if (!($r['success'] ?? false) || ($r['code'] ?? 0) != 200) {
349 return [
350 'status' => 'error',
351 'msg' => $r['msg'] ?? '状态查询失败',
352 ];
353 }
354
355 $data = $r['data'] ?? [];
356 $user = $data['user'] ?? [];
357 $container = $data['container'] ?? null;
358
359 $qk = $user['qk'] ?? 'active';
360 $cs = $user['container_status'] ?? 'none';
361
362 // 状态映射(见 PRD §5.1)
363 if ($qk === 'paused' || $qk === 'expired' || $qk === 'pruned') {
364 $status = 'suspend';
365 $des = $qk === 'paused' ? '已暂停' : ($qk === 'pruned' ? '容器已清理' : '已到期');
366 } elseif ($cs === 'none' || $cs === 'creating') {
367 $status = 'waiting';
368 $des = $cs === 'none' ? '未创建容器(请在控制台创建)' : '容器创建中';
369 } elseif ($cs === 'running') {
370 $status = 'on';
371 $des = '运行中';
372 } elseif ($cs === 'stopped') {
373 $status = 'off';
374 $des = '已停止';
375 } else {
376 $status = 'unknown';
377 $des = '未知状态';
378 }
379
380 // 附加容器信息
381 if ($container && !empty($container['port']) && is_array($container['port'])) {
382 $des .= ' | 端口:' . implode(', ', $container['port']);
383 }
384
385 return [
386 'status' => 'success',
387 'data' => [
388 'status' => $status,
389 'des' => $des,
390 ],
391 ];
392}
393
394/** 同步 */
395function mnbtdocker_Sync($params)
396{
397 return mnbtdocker_Status($params);
398}
399
400// ========================================================================
401// 容器启停
402// ========================================================================
403
404/** 开机 */
405function mnbtdocker_On($params)
406{
407 $r = _mnbtdocker_api_call($params, 'start');
408 return _mnbtdocker_return($r);
409}
410
411/** 关机 */
412function mnbtdocker_Off($params)
413{
414 $r = _mnbtdocker_api_call($params, 'stop');
415 return _mnbtdocker_return($r);
416}
417
418/** 重启 */
419function mnbtdocker_Reboot($params)
420{
421 $r = _mnbtdocker_api_call($params, 'restart');
422 return _mnbtdocker_return($r);
423}
424
425// ========================================================================
426// ClientArea 前台自定义输出
427// ========================================================================
428
429/**
430 * 前台选项卡
431 */
432function mnbtdocker_ClientArea($params)
433{
434 return [
435 'console' => ['name' => '容器控制台'],
436 ];
437}
438
439/**
440 * 前台选项卡内容
441 */
442function mnbtdocker_ClientAreaOutput($params, $key)
443{
444 if ($key !== 'console') {
445 return '';
446 }
447
448 // 查询最新状态
449 $status_data = mnbtdocker_Status($params);
450 $status_info = $status_data['data'] ?? ['status' => 'unknown', 'des' => '未知'];
451 $status_class = $status_info['status'] ?? 'unknown';
452
453 list(, , , , , $console_url) = _mnbtdocker_resolve_params($params);
454
455 return [
456 'template' => 'templates/console.html',
457 'vars' => [
458 'status_text' => $status_info['des'],
459 'status_class' => $status_class,
460 'console_url' => $console_url,
461 'username' => $params['domain'] ?? ($params['username'] ?? ''),
462 'password' => $params['password'] ?? '',
463 ],
464 ];
465}
466
467// ========================================================================
468// ClientButton & AllowFunction
469// ========================================================================
470
471/**
472 * 前台自定义按钮
473 */
474function mnbtdocker_ClientButton($params)
475{
476 return [
477 'console' => [
478 'place' => 'console',
479 'name' => '打开容器控制台',
480 ],
481 ];
482}
483
484/**
485 * 允许前台调用的自定义方法
486 */
487function mnbtdocker_AllowFunction()
488{
489 return [
490 'client' => ['console'],
491 'admin' => [],
492 ];
493}
494
495/**
496 * 前台按钮调用的自定义方法:返回控制台 URL 供魔方弹窗或跳转
497 */
498function mnbtdocker_console($params)
499{
500 list(, , , , , $console_url) = _mnbtdocker_resolve_params($params);
501 return ['status' => 'success', 'msg' => '正在跳转到容器控制台', 'url' => $console_url];
502}