仰望星辰工作室

better-staridc-MNBT

better-staridc-MNBT/ app_plugins/zjmfmanager_reserve/lib/CubeFinanceClient.php 16.6 KB · 698 行 原始文件
Z zfhsh first commit 2 天前
1<?php
2/**
3 * 魔方财务 (cube_finance / 智简魔方财务) API 客户端
4 *
5 * 改编自 app_plugins/zjmfmanager_reserve/example/sdk/CubeFinanceClient.php。
6 * 认证流程与魔方云 v10 上游对接一致:
7 * POST /zjmf_api_login → JWT
8 * Authorization: Bearer {jwt}
9 *
10 * 适用于 PHP 7.2+,仅依赖 ext-curl / ext-json。
11 * 返回 status=401/405 时自动强制重登并重试一次。
12 */
13
14if (!defined('IN_CRONLITE')) {
15 exit;
16}
17
18if (!class_exists('CubeFinanceException', false)) {
19
20class CubeFinanceException extends Exception
21{
22 /** @var array|null */
23 public $response;
24
25 public function __construct($message, $code = 0, $response = null)
26 {
27 parent::__construct($message, (int)$code);
28 $this->response = $response;
29 }
30}
31
32}
33
34if (!class_exists('CubeFinanceClient', false)) {
35
36class CubeFinanceClient
37{
38 /** @var string */
39 protected $baseUrl;
40
41 /** @var string */
42 protected $username;
43
44 /** @var string */
45 protected $password;
46
47 /** @var string|null */
48 protected $jwt;
49
50 /** @var int */
51 protected $timeout = 30;
52
53 /** @var string|null JWT 文件缓存目录,null 则仅内存缓存 */
54 protected $cacheDir;
55
56 /** @var int JWT 缓存秒数,默认 2 小时(与 v10 一致) */
57 protected $jwtTtl = 7200;
58
59 /** @var bool */
60 protected $verifySsl = false;
61
62 /**
63 * @param array $config
64 * - url string 魔方财务站点根地址
65 * - username string API 账号(客户用户名)
66 * - password string API 密钥(客户 API 密码)
67 * - timeout int 请求超时秒数
68 * - cache_dir string JWT 缓存目录(可写)
69 * - jwt_ttl int JWT 缓存时长
70 * - verify_ssl bool 是否校验 HTTPS 证书
71 * - jwt string 已有 JWT(可选,跳过登录)
72 */
73 public function __construct(array $config)
74 {
75 if (empty($config['url'])) {
76 throw new InvalidArgumentException('缺少 url');
77 }
78 $this->baseUrl = rtrim($config['url'], '/');
79 $this->username = isset($config['username']) ? (string)$config['username'] : '';
80 $this->password = isset($config['password']) ? (string)$config['password'] : '';
81 $this->timeout = isset($config['timeout']) ? (int)$config['timeout'] : 30;
82 $this->cacheDir = isset($config['cache_dir']) ? rtrim($config['cache_dir'], '/\\') : null;
83 $this->jwtTtl = isset($config['jwt_ttl']) ? (int)$config['jwt_ttl'] : 7200;
84 $this->verifySsl = !empty($config['verify_ssl']);
85 if (!empty($config['jwt'])) {
86 $this->jwt = (string)$config['jwt'];
87 }
88 }
89
90 /**
91 * 登录获取 JWT(会写入缓存)。
92 *
93 * @param bool $force 强制重新登录
94 * @return string
95 * @throws CubeFinanceException
96 */
97 public function login($force = false)
98 {
99 if (!$force) {
100 $cached = $this->getCachedJwt();
101 if ($cached) {
102 $this->jwt = $cached;
103 return $this->jwt;
104 }
105 }
106
107 if ($this->username === '' || $this->password === '') {
108 throw new CubeFinanceException('缺少 username 或 password,无法登录');
109 }
110
111 $raw = $this->rawRequest('POST', 'zjmf_api_login', [
112 'username' => $this->username,
113 'password' => $this->password,
114 ], false);
115
116 $result = $this->decodeJson($raw);
117 if (!isset($result['status']) || (int)$result['status'] !== 200 || empty($result['jwt'])) {
118 $msg = isset($result['msg']) ? $result['msg'] : '登录失败';
119 throw new CubeFinanceException($msg, isset($result['status']) ? (int)$result['status'] : 400, $result);
120 }
121
122 $this->jwt = $result['jwt'];
123 $this->setCachedJwt($this->jwt);
124 return $this->jwt;
125 }
126
127 /**
128 * 当前 JWT(未登录则自动登录)。
129 *
130 * @return string
131 */
132 public function getJwt()
133 {
134 if ($this->jwt) {
135 return $this->jwt;
136 }
137 return $this->login();
138 }
139
140 /**
141 * 通用请求(自动附带 Bearer,401/405 时强制重登一次)。
142 *
143 * @param string $method GET|POST|PUT|DELETE
144 * @param string $path 相对路径,如 api/product/list
145 * @param array $data
146 * @return array
147 * @throws CubeFinanceException
148 */
149 public function request($method, $path, array $data = [])
150 {
151 $this->getJwt();
152 $raw = $this->rawRequest($method, $path, $data, true);
153 $result = $this->decodeJson($raw);
154
155 if (isset($result['status']) && in_array((int)$result['status'], [401, 405], true)) {
156 $this->login(true);
157 $raw = $this->rawRequest($method, $path, $data, true);
158 $result = $this->decodeJson($raw);
159 if (isset($result['status']) && (int)$result['status'] === 401) {
160 throw new CubeFinanceException(
161 isset($result['msg']) ? $result['msg'] : 'API 账号或密码错误',
162 401,
163 $result
164 );
165 }
166 }
167
168 return $result;
169 }
170
171 public function get($path, array $data = [])
172 {
173 return $this->request('GET', $path, $data);
174 }
175
176 public function post($path, array $data = [])
177 {
178 return $this->request('POST', $path, $data);
179 }
180
181 public function put($path, array $data = [])
182 {
183 return $this->request('PUT', $path, $data);
184 }
185
186 public function delete($path, array $data = [])
187 {
188 return $this->request('DELETE', $path, $data);
189 }
190
191 // ==================== 业务封装 ====================
192
193 /**
194 * 测试连通性:登录并拉取商品列表。
195 *
196 * @return array{ok:bool,msg:string,jwt?:string,product_count?:int,raw?:array}
197 */
198 public function testConnection()
199 {
200 try {
201 $jwt = $this->login(true);
202 $list = $this->productList();
203 $count = 0;
204 if (isset($list['data']['list']) && is_array($list['data']['list'])) {
205 $count = count($list['data']['list']);
206 }
207 return [
208 'ok' => true,
209 'msg' => '连接成功',
210 'jwt' => substr($jwt, 0, 24) . '...',
211 'product_count' => $count,
212 'raw' => $list,
213 ];
214 } catch (Exception $e) {
215 return [
216 'ok' => false,
217 'msg' => $e->getMessage(),
218 ];
219 }
220 }
221
222 /**
223 * 商品列表。
224 * GET /api/product/list
225 *
226 * @return array status/msg/data.list/data.currency_code
227 */
228 public function productList()
229 {
230 return $this->get('api/product/list');
231 }
232
233 /**
234 * 商品详情。
235 * GET /api/product/{id}?price_basis=agent
236 *
237 * @param int $productId
238 * @param string $priceBasis agent|cost 等
239 * @return array
240 */
241 public function productDetail($productId, $priceBasis = 'agent')
242 {
243 return $this->get('api/product/' . intval($productId), [
244 'price_basis' => $priceBasis,
245 ]);
246 }
247
248 /**
249 * 购物车配置 / 价格试算。
250 * GET cart/set_config
251 *
252 * @param array $params 通常含 pid、billingcycle、配置项等
253 * @return array
254 */
255 public function cartSetConfig(array $params)
256 {
257 return $this->get('cart/set_config', $params);
258 }
259
260 /**
261 * 购物车计算总价(官方价格计算接口)。
262 * POST /cart/get_total
263 *
264 * @param array $params 通常含 pid、billingcycle、qty、configoption 等
265 * @return array
266 */
267 public function cartGetTotal(array $params)
268 {
269 return $this->post('cart/get_total', $params);
270 }
271
272 /**
273 * 购物车产品配置(含该产品可选周期/价格,用于探测真实 billingcycle 键)。
274 * GET /cart/get_product_config?pid={pid}
275 *
276 * @param int $productId
277 * @return array
278 */
279 public function cartGetProductConfig($productId)
280 {
281 return $this->get('cart/get_product_config', [
282 'pid' => (int)$productId,
283 ]);
284 }
285
286 /**
287 * 清空购物车。
288 * POST /cart/clear
289 *
290 * @return array
291 */
292 public function cartClear()
293 {
294 return $this->post('cart/clear', []);
295 }
296
297 /**
298 * 购物车页面数据(含购物车产品列表,数组键/序号即位置 i)。
299 * GET /cart/get_shop_data
300 *
301 * @return array
302 */
303 public function cartGetShopData()
304 {
305 return $this->get('cart/get_shop_data');
306 }
307
308 /**
309 * 前台首页数据(含客户余额 data.client[].credit)。
310 * GET /index
311 *
312 * @return array
313 */
314 public function homeIndex()
315 {
316 return $this->get('index');
317 }
318
319 /**
320 * 账户信息 / 余额。
321 * GET user_info
322 *
323 * @return array
324 */
325 public function userInfo()
326 {
327 return $this->get('user_info');
328 }
329
330 /**
331 * 主机头信息。
332 * GET host/header?host_id={id}
333 *
334 * @param int $hostId 上游主机 ID
335 * @return array
336 */
337 public function hostHeader($hostId)
338 {
339 return $this->get('host/header', ['host_id' => (int)$hostId]);
340 }
341
342 /** 产品内页数据(GET host/product),状态/账号等字段更稳定。 */
343 public function hostProduct($hostId)
344 {
345 return $this->get('host/product', ['host_id' => (int)$hostId]);
346 }
347
348 /**
349 * 独服/云服务器详情(含重装可选系统列表 cloud_os / cloud_os_group)。
350 * GET host/dedicatedserver?host_id={hostId}
351 *
352 * 实测 host/cloudos 无法返回系统列表,该端点是重装系统的可靠来源。
353 *
354 * @param int $hostId 上游主机 ID
355 * @return array
356 */
357 public function hostDedicatedServer($hostId)
358 {
359 return $this->get('host/dedicatedserver', ['host_id' => (int)$hostId]);
360 }
361
362 /**
363 * 我的主机列表。
364 * GET host/list
365 *
366 * @param array $extra 如 groupid/orderby/sort/show_type
367 * @return array
368 */
369 public function hostList(array $extra = [])
370 {
371 return $this->get('host/list', $extra);
372 }
373
374 /**
375 * 流量使用。
376 * GET host/trafficusage
377 *
378 * @param int $hostId
379 * @param array $extra 如 start/end
380 * @return array
381 */
382 public function hostTrafficUsage($hostId, array $extra = [])
383 {
384 $data = array_merge(['id' => intval($hostId)], $extra);
385 return $this->get('host/trafficusage', $data);
386 }
387
388 /**
389 * 开通模块默认操作(开关机/重启/重装/重置密码等)。
390 * POST provision/default
391 *
392 * @param array $params 含 id(host_id)、func 等
393 * @return array
394 */
395 public function provisionDefault(array $params)
396 {
397 return $this->post('provision/default', $params);
398 }
399
400 /* ============================================================
401 * DCIM 管理(服务器租用 / NAT 类产品)
402 * ============================================================ */
403
404 /**
405 * DCIM 模块操作(开机/关机/重启/重置BMC/救援/取消任务等)。
406 * POST dcim/{action}?id={hostId}
407 *
408 * @param string $action 端点名(on/off/reboot/bmc/rescue/cancel_task/reinstall/crack_pass)
409 * @param int $hostId 上游主机 ID
410 * @param array $extra 额外参数
411 * @return array
412 */
413 public function dcimAction($action, $hostId, array $extra = [])
414 {
415 $data = array_merge(['id' => (int)$hostId], $extra);
416 return $this->post('dcim/' . $action, $data);
417 }
418
419 /** DCIM 产品详情(交换机/端口信息)。GET dcim/detail */
420 public function dcimDetail($hostId)
421 {
422 return $this->get('dcim/detail', ['id' => (int)$hostId]);
423 }
424
425 /** 单台电源状态。POST dcim/refresh_power_status */
426 public function dcimPowerStatus($hostId)
427 {
428 return $this->post('dcim/refresh_power_status', ['id' => (int)$hostId]);
429 }
430
431 /** 验证是否可以重装(本周次数/上限)。POST dcim/check_reinstall */
432 public function dcimCheckReinstall($hostId)
433 {
434 return $this->post('dcim/check_reinstall', ['id' => (int)$hostId]);
435 }
436
437 /** 重装/救援/重置密码进度。GET dcim/resintall_status */
438 public function dcimReinstallStatus($hostId)
439 {
440 return $this->get('dcim/resintall_status', ['id' => (int)$hostId]);
441 }
442
443 /** 用量信息。GET dcim/traffic_usage */
444 public function dcimTrafficUsage($hostId, array $extra = [])
445 {
446 $data = array_merge(['id' => (int)$hostId], $extra);
447 return $this->get('dcim/traffic_usage', $data);
448 }
449
450 /**
451 * 配置升级页。
452 * GET upgrade/index/{hostId}
453 *
454 * @param int $hostId
455 * @param array $params
456 * @return array
457 */
458 public function upgradeIndex($hostId, array $params = [])
459 {
460 return $this->get('upgrade/index/' . intval($hostId), $params);
461 }
462
463 /**
464 * 提交配置升级。
465 * POST upgrade/upgrade_config_post
466 *
467 * @param array $params
468 * @return array
469 */
470 public function upgradeConfigPost(array $params)
471 {
472 return $this->post('upgrade/upgrade_config_post', $params);
473 }
474
475 /**
476 * 配置升级确认页。
477 * GET upgrade/upgrade_config_page
478 *
479 * @param int $hostId
480 * @param string $priceBasis
481 * @return array
482 */
483 public function upgradeConfigPage($hostId, $priceBasis = 'agent')
484 {
485 return $this->get('upgrade/upgrade_config_page', [
486 'hid' => intval($hostId),
487 'price_basis' => $priceBasis,
488 ]);
489 }
490
491 /**
492 * 产品升降级选项。
493 * GET upgrade/upgrade_product/{hostId}
494 *
495 * @param int $hostId
496 * @param array $params
497 * @return array
498 */
499 public function upgradeProduct($hostId, array $params = [])
500 {
501 return $this->get('upgrade/upgrade_product/' . intval($hostId), $params);
502 }
503
504 /**
505 * 提交产品升降级。
506 * POST upgrade/upgrade_product_post
507 *
508 * @param array $params
509 * @return array
510 */
511 public function upgradeProductPost(array $params)
512 {
513 return $this->post('upgrade/upgrade_product_post', $params);
514 }
515
516 /**
517 * 任意未封装接口。
518 *
519 * @param string $method GET|POST|PUT|DELETE
520 * @param string $path 相对路径
521 * @param array $data
522 * @return array
523 */
524 public function custom($method, $path, array $data = [])
525 {
526 return $this->request($method, ltrim($path, '/'), $data);
527 }
528
529 // ==================== 底层 HTTP ====================
530
531 /**
532 * @param string $method
533 * @param string $path
534 * @param array $data
535 * @param bool $auth
536 * @return string 原始响应体
537 * @throws CubeFinanceException
538 */
539 protected function rawRequest($method, $path, array $data, $auth)
540 {
541 $method = strtoupper($method);
542 $url = $this->baseUrl . '/' . ltrim($path, '/');
543 $headers = [
544 'User-Agent: CubeFinanceClient/1.0 (+php)',
545 'Accept: application/json',
546 ];
547 if ($auth) {
548 $headers[] = 'Authorization: Bearer ' . $this->jwt;
549 }
550
551 $ch = curl_init();
552 if ($method === 'GET') {
553 $qs = http_build_query($data);
554 if ($qs !== '') {
555 $url .= (strpos($url, '?') === false ? '?' : '&') . $qs;
556 }
557 curl_setopt($ch, CURLOPT_HTTPGET, true);
558 } else {
559 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
560 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
561 if ($method === 'POST') {
562 curl_setopt($ch, CURLOPT_POST, true);
563 }
564 }
565
566 curl_setopt_array($ch, [
567 CURLOPT_URL => $url,
568 CURLOPT_RETURNTRANSFER => true,
569 CURLOPT_TIMEOUT => $this->timeout,
570 CURLOPT_FOLLOWLOCATION => true,
571 CURLOPT_SSL_VERIFYPEER => $this->verifySsl,
572 CURLOPT_SSL_VERIFYHOST => $this->verifySsl ? 2 : 0,
573 CURLOPT_HTTPHEADER => $headers,
574 CURLOPT_HEADER => false,
575 ]);
576
577 $body = curl_exec($ch);
578 $errno = curl_errno($ch);
579 $error = curl_error($ch);
580 $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
581 curl_close($ch);
582
583 if ($errno) {
584 throw new CubeFinanceException('网络错误: ' . $error, $errno);
585 }
586 if ($httpCode < 200 || $httpCode >= 300) {
587 $snippet = is_string($body) ? $this->truncate($body, 300) : '';
588 throw new CubeFinanceException(
589 "HTTP {$httpCode} {$method} {$url}" . ($snippet !== '' ? ': ' . $snippet : ''),
590 $httpCode,
591 ['url' => $url, 'http_code' => $httpCode, 'body' => $body]
592 );
593 }
594 if ($body === false || $body === '') {
595 throw new CubeFinanceException('空响应', $httpCode);
596 }
597
598 return $body;
599 }
600
601 /**
602 * @param string $raw
603 * @return array
604 * @throws CubeFinanceException
605 */
606 protected function decodeJson($raw)
607 {
608 $data = json_decode($raw, true);
609 if (!is_array($data)) {
610 throw new CubeFinanceException(
611 '响应不是合法 JSON: ' . $this->truncate((string)$raw, 200)
612 );
613 }
614 return $data;
615 }
616
617 /**
618 * 截断字符串(mb_substr 不可用时回退 substr)。
619 *
620 * @param string $str
621 * @param int $len
622 * @return string
623 */
624 protected function truncate($str, $len)
625 {
626 if (function_exists('mb_substr')) {
627 return mb_substr($str, 0, $len);
628 }
629 return substr($str, 0, $len);
630 }
631
632 protected function cacheKey()
633 {
634 return 'jwt_' . md5($this->baseUrl . '|' . $this->username);
635 }
636
637 protected function cacheFile()
638 {
639 if (!$this->cacheDir) {
640 return null;
641 }
642 if (!is_dir($this->cacheDir)) {
643 @mkdir($this->cacheDir, 0755, true);
644 }
645 return $this->cacheDir . DIRECTORY_SEPARATOR . $this->cacheKey() . '.json';
646 }
647
648 protected function getCachedJwt()
649 {
650 $file = $this->cacheFile();
651 if (!$file || !is_file($file)) {
652 return null;
653 }
654 $json = @file_get_contents($file);
655 $data = json_decode((string)$json, true);
656 if (empty($data['jwt']) || empty($data['expire']) || time() >= (int)$data['expire']) {
657 return null;
658 }
659 return $data['jwt'];
660 }
661
662 protected function setCachedJwt($jwt)
663 {
664 $file = $this->cacheFile();
665 if (!$file) {
666 return;
667 }
668 @file_put_contents($file, json_encode([
669 'jwt' => $jwt,
670 'expire' => time() + $this->jwtTtl,
671 ]));
672 }
673
674 /**
675 * 清除 JWT 缓存。
676 */
677 public function clearCache()
678 {
679 $this->jwt = null;
680 $file = $this->cacheFile();
681 if ($file && is_file($file)) {
682 @unlink($file);
683 }
684 }
685
686 /**
687 * 修改请求超时秒数(用户端实时查询用较短超时,避免拖垮整页加载)。
688 *
689 * @param int $seconds
690 * @return void
691 */
692 public function setTimeout($seconds)
693 {
694 $this->timeout = max(1, (int)$seconds);
695 }
696}
697
698}