仰望星辰工作室

better-staridc-MNBT

better-staridc-MNBT/ app_plugins/docker_shop/bootstrap.php 13.8 KB · 403 行 原始文件
Z zfhsh first commit 1 天前
1<?php
2/**
3 * docker_shop 插件 - 主入口
4 *
5 * 功能:Docker 售卖套餐、购买下单、自动开通 Docker 账号、资产/订单管理
6 * 依赖:user_info 插件(认证)、balance 插件(余额)、支付插件(epay/alipay_official)
7 * 架构:
8 * - 用户端:通过 P2 路由注册 /docker-shop/* 路径
9 * - 管理员端:通过 mnbt_register_page('admin', ...) 注册到 admin/plugin.php
10 * - 开通:通过 order.paid 钩子处理 lx=docker 订单,写入 MN_docker_user + 资产表
11 */
12
13if (!defined('IN_CRONLITE')) {
14 exit;
15}
16
17require_once __DIR__ . '/lib/docker_shop.php';
18
19// 自动升级表结构
20docker_shop_upgrade_schema();
21
22mnbt_plugin_register('docker_shop', [
23 'name' => 'Docker 售卖',
24 'description' => 'Docker 容器套餐售卖、自动开通 Docker 账号、资产订单管理',
25]);
26
27/* ============================================================
28 * order.paid 钩子:处理 Docker 购买订单
29 * ============================================================
30 * 支付成功后由 mnbt_pay_settle_order() 触发。
31 * 1. 检查订单是否已处理(防重复)
32 * 2. 标记订单为 paid
33 * 3. 调用 docker_shop_open_account() 开通 Docker 账号
34 */
35mnbt_add_action('order.paid', function ($order_row, $ctx = []) {
36 if (!is_array($order_row)) {
37 return;
38 }
39 if (($order_row['lx'] ?? '') !== 'docker') {
40 return;
41 }
42 $order_no = (string)($order_row['ddh'] ?? '');
43 if ($order_no === '') {
44 return;
45 }
46
47 // 防重复:检查该订单号对应的 docker 订单是否已存在
48 $docker_order = docker_shop_order_get_by_no($order_no);
49 if (!$docker_order) {
50 // 可能是直接走支付完成的订单,但没有 docker 订单记录,跳过
51 return;
52 }
53 // 已开通/已失败,跳过
54 if (in_array($docker_order['status'], ['opened', 'failed', 'paid'], true)) {
55 return;
56 }
57
58 // 标记为 paid,然后开通
59 docker_shop_order_set_status((int)$docker_order['id'], 'paid', '支付完成');
60 $result = docker_shop_open_account((int)$docker_order['id']);
61 if (!$result['ok']) {
62 @error_log('[docker_shop] open failed order=' . $order_no . ' : ' . ($result['msg'] ?? ''));
63 }
64}, 20);
65
66/* ============================================================
67 * 用户端页面路由
68 * ============================================================ */
69
70// 售卖页(套餐列表)
71mnbt_register_route('GET', '/docker-shop', function ($params, $ctx) {
72 docker_shop_require_user();
73 $plans = docker_shop_plan_list_active();
74
75 docker_shop_render('shop', [
76 'page_title' => 'Docker 商城',
77 'plans' => $plans,
78 ]);
79});
80
81// 下单页
82mnbt_register_route('GET', '/docker-shop/order/{plan_id}', function ($params, $ctx) {
83 docker_shop_require_user();
84 $plan_id = (int)($params['plan_id'] ?? 0);
85 $plan = docker_shop_plan_get($plan_id);
86 if (!$plan || $plan['status'] !== 'active') {
87 http_response_code(404);
88 echo '套餐不存在或已下架';
89 return;
90 }
91 $methods = function_exists('mnbt_get_enabled_payment_methods') ? mnbt_get_enabled_payment_methods() : [];
92
93 docker_shop_render('order', [
94 'page_title' => '购买:' . $plan['name'],
95 'plan' => $plan,
96 'methods' => $methods,
97 ]);
98});
99
100// 我的 Docker 资产
101mnbt_register_route('GET', '/docker-shop/assets', function ($params, $ctx) {
102 $user = docker_shop_require_user();
103 $assets = docker_shop_asset_list_by_user((int)$user['id']);
104
105 docker_shop_render('assets', [
106 'page_title' => '我的 Docker',
107 'assets' => $assets,
108 ]);
109});
110
111// 我的 Docker 订单
112mnbt_register_route('GET', '/docker-shop/orders', function ($params, $ctx) {
113 $user = docker_shop_require_user();
114 $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
115 $orders = docker_shop_order_list_by_user((int)$user['id'], $page, 15);
116
117 docker_shop_render('orders', [
118 'page_title' => 'Docker 订单',
119 'orders' => $orders,
120 ]);
121});
122
123/* ============================================================
124 * 用户端 API 路由(SPA 数据接口)
125 * ============================================================ */
126
127// 售卖套餐列表
128mnbt_register_route('GET', '/docker-shop/api/plans', function ($params, $ctx) {
129 $rows = docker_shop_plan_list_active();
130 $plans = [];
131 foreach ($rows as $p) {
132 $plans[] = docker_shop_plan_to_api($p);
133 }
134 docker_shop_json('ok', ['plans' => $plans]);
135});
136
137// 售卖套餐详情 + 支付方式
138mnbt_register_route('GET', '/docker-shop/api/plan/{plan_id}', function ($params, $ctx) {
139 $plan_id = (int)($params['plan_id'] ?? 0);
140 $plan = docker_shop_plan_get($plan_id);
141 if (!$plan || $plan['status'] !== 'active') {
142 docker_shop_json('套餐不存在或已下架');
143 return;
144 }
145 $methods = function_exists('mnbt_get_enabled_payment_methods') ? mnbt_get_enabled_payment_methods() : [];
146 docker_shop_json('ok', [
147 'plan' => docker_shop_plan_to_api($plan),
148 'methods' => $methods,
149 ]);
150});
151
152// 我的 Docker 资产
153mnbt_register_route('GET', '/docker-shop/api/assets', function ($params, $ctx) {
154 $user = function_exists('user_info_auth_current') ? user_info_auth_current() : null;
155 if (!$user) {
156 docker_shop_json('not_login', ['logged_in' => false]);
157 return;
158 }
159 $assets = docker_shop_asset_list_by_user((int)$user['id']);
160 docker_shop_json('ok', ['logged_in' => true, 'assets' => $assets]);
161});
162
163// 我的 Docker 订单(分页)
164mnbt_register_route('GET', '/docker-shop/api/orders', function ($params, $ctx) {
165 $user = function_exists('user_info_auth_current') ? user_info_auth_current() : null;
166 if (!$user) {
167 docker_shop_json('not_login', ['logged_in' => false]);
168 return;
169 }
170 $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
171 $per = isset($_GET['per_page']) ? max(1, min(100, (int)$_GET['per_page'])) : 15;
172 $orders = docker_shop_order_list_by_user((int)$user['id'], $page, $per);
173 docker_shop_json('ok', ['logged_in' => true, 'orders' => $orders]);
174});
175
176// 可用支付方式(下单页数据接口)
177mnbt_register_route('GET', '/docker-shop/api/methods', function ($params, $ctx) {
178 $methods = function_exists('mnbt_get_enabled_payment_methods') ? mnbt_get_enabled_payment_methods() : [];
179 docker_shop_json('ok', ['methods' => $methods]);
180});
181
182// 创建购买订单 → 调用支付插件
183mnbt_register_route('POST', '/docker-shop/api/create_order', function ($params, $ctx) {
184 global $DB, $date, $siteurl;
185
186 $user = docker_shop_require_user();
187 $user_id = (int)$user['id'];
188
189 $plan_id = (int)($_POST['plan_id'] ?? 0);
190 $period = isset($_POST['period']) ? trim($_POST['period']) : 'month';
191 $type = isset($_POST['type']) ? trim($_POST['type']) : '';
192
193 // 校验套餐
194 $plan = docker_shop_plan_get($plan_id);
195 if (!$plan || $plan['status'] !== 'active') {
196 docker_shop_json('套餐不存在或已下架');
197 }
198 $periods = docker_shop_periods();
199 if (!isset($periods[$period])) {
200 docker_shop_json('请选择有效的购买周期');
201 }
202 $enabled = docker_shop_plan_enabled_periods($plan);
203 if (!in_array($period, $enabled, true)) {
204 docker_shop_json('该套餐不支持此购买周期');
205 }
206 $price_field = docker_shop_period_price_field($period);
207 $amount_cents = $price_field ? (int)($plan[$price_field] ?? 0) : 0;
208 if ($amount_cents < 0) {
209 docker_shop_json('该套餐此周期价格异常');
210 }
211 // 节点/配额套餐已由套餐固定配置,无需前端传入
212 $node = (int)($plan['node'] ?? 0);
213 if ($node <= 0 || !docker_shop_node_get($node)) {
214 docker_shop_json('套餐未配置有效开通节点,请联系管理员');
215 }
216 if ((int)($plan['base_plan_id'] ?? 0) <= 0) {
217 docker_shop_json('套餐未配置有效配额套餐,请联系管理员');
218 }
219 // 非 0 元订单校验支付方式;0 元订单无需选择支付方式
220 if ($amount_cents > 0) {
221 if ($type === '' || !function_exists('mnbt_pay_parse_type') || !mnbt_pay_parse_type($type)) {
222 docker_shop_json('请选择有效的支付方式');
223 }
224 }
225
226 // 创建 docker 订单(节点从套餐读取)
227 $create = docker_shop_order_create($user, $plan, $period);
228 if (empty($create['ok'])) {
229 docker_shop_json($create['msg'] ?? '创建订单失败');
230 }
231 $order_no = $create['order_no'];
232 $docker_order_id = (int)$create['order_id'];
233
234 // 0 元免费套餐:直接标记 paid 并开通,无需创建 MN_dd 和调支付网关
235 if ($amount_cents === 0) {
236 docker_shop_order_set_status($docker_order_id, 'paid', '免费套餐直接开通');
237 $open = docker_shop_open_account($docker_order_id);
238 if (!$open['ok']) {
239 docker_shop_json('开通失败:' . ($open['msg'] ?? '未知错误'));
240 }
241 docker_shop_json('开通成功', [
242 'redirect' => docker_shop_url('docker-shop/assets'),
243 'docker_username' => $open['docker_username'],
244 'docker_password' => $open['docker_password'],
245 ]);
246 }
247
248 // 创建 MN_dd 记录(支付系统订单)
249 $amount_yuan = (string)round($amount_cents / 100, 2);
250 $cs = json_encode([
251 'user_id' => $user_id,
252 'plan_id' => $plan_id,
253 'period' => $period,
254 'node' => $node,
255 'amount' => $amount_cents,
256 'username' => $user['username'],
257 'order_id' => $docker_order_id,
258 ], 256);
259 $ip = $_SERVER["REMOTE_ADDR"] ?? '127.0.0.1';
260
261 $row1 = $DB->get_row_prepare("SELECT * FROM MN_dd WHERE 1 order by id desc limit 1");
262 $dd_id = $row1 ? ((int)$row1['id'] + 1) : 1;
263 $ok = $DB->query_prepare(
264 "INSERT INTO MN_dd (id, cs, date, zffs, je, ddh, lx, qk, ip) VALUES (?,?,?,?,?,?,?,?,?)",
265 [$dd_id, $cs, $date, $type, $amount_yuan, $order_no, 'docker', 'false', $ip]
266 );
267 if (!$ok) {
268 // 回滚 docker 订单状态
269 docker_shop_order_set_status($docker_order_id, 'cancelled', '支付订单创建失败');
270 docker_shop_json('支付订单创建失败,请稍后重试');
271 }
272
273 // 分发到支付插件
274 $period_label = $periods[$period]['label'];
275 $order_context = [
276 'out_trade_no' => $order_no,
277 'name' => '购买 Docker:' . $plan['name'] . '(' . $period_label . ')',
278 'money' => $amount_yuan,
279 'type' => $type,
280 'siteurl' => $siteurl,
281 'pay_lx' => 'docker',
282 ];
283
284 $html = mnbt_pay_dispatch_gateway($type, $order_context);
285 if ($html === false) {
286 docker_shop_order_set_status($docker_order_id, 'cancelled', '支付方式不可用');
287 docker_shop_json('支付方式不可用,请检查支付插件是否已启用');
288 }
289
290 docker_shop_json('ok', ['html' => $html, 'order_no' => $order_no]);
291});
292
293// 重置 Docker 账号密码
294mnbt_register_route('POST', '/docker-shop/api/reset_password', function ($params, $ctx) {
295 $user = function_exists('user_info_auth_current') ? user_info_auth_current() : null;
296 if (!$user) {
297 docker_shop_json('not_login', ['logged_in' => false]);
298 return;
299 }
300 $asset_id = (int)($_POST['asset_id'] ?? 0);
301 // 校验资产归属
302 $asset = null;
303 foreach (docker_shop_asset_list_by_user((int)$user['id']) as $a) {
304 if ((int)$a['id'] === $asset_id) {
305 $asset = $a;
306 break;
307 }
308 }
309 if (!$asset) {
310 docker_shop_json('资产不存在');
311 }
312 $result = docker_shop_reset_password($asset_id);
313 if (!$result['ok']) {
314 docker_shop_json($result['msg']);
315 }
316 docker_shop_json('重置成功', ['password' => $result['password']]);
317});
318
319// 刷新容器状态
320mnbt_register_route('POST', '/docker-shop/api/sync_status', function ($params, $ctx) {
321 $user = function_exists('user_info_auth_current') ? user_info_auth_current() : null;
322 if (!$user) {
323 docker_shop_json('not_login', ['logged_in' => false]);
324 return;
325 }
326 $asset_id = (int)($_POST['asset_id'] ?? 0);
327 $asset = null;
328 foreach (docker_shop_asset_list_by_user((int)$user['id']) as $a) {
329 if ((int)$a['id'] === $asset_id) {
330 $asset = $a;
331 break;
332 }
333 }
334 if (!$asset) {
335 docker_shop_json('资产不存在');
336 }
337 $synced = docker_shop_sync_container_status($asset);
338 docker_shop_json('ok', [
339 'container_status' => $synced['container_status'] ?? 'none',
340 'container_id' => $synced['container_id'] ?? '',
341 'disk_usage' => (int)($synced['disk_usage'] ?? 0),
342 ]);
343});
344
345/* ============================================================
346 * 管理员端页面注册
347 * ============================================================ */
348
349mnbt_register_page('admin', 'plans', 'views/admin/plans.php', '售卖套餐管理');
350mnbt_register_page('admin', 'plan_edit', 'views/admin/plan_edit.php', '售卖套餐编辑');
351mnbt_register_page('admin', 'orders', 'views/admin/orders.php', '订单管理');
352mnbt_register_page('admin', 'assets', 'views/admin/assets.php', '资产管理');
353
354// 侧边栏菜单(三级结构)
355mnbt_register_menu('admin', [
356 'title' => 'Docker 售卖',
357 'icon' => 'mdi-docker',
358 'order' => 61,
359 'children' => [
360 ['title' => '售卖套餐', 'page' => 'plans', 'icon' => 'mdi-package-variant', 'multitabs' => true],
361 ['title' => '订单管理', 'page' => 'orders', 'icon' => 'mdi-receipt', 'multitabs' => true],
362 ['title' => '资产管理', 'page' => 'assets', 'icon' => 'mdi-server', 'multitabs' => true],
363 ],
364]);
365
366/* ============================================================
367 * 管理员端 AJAX
368 * ============================================================ */
369
370// 保存售卖套餐
371mnbt_register_ajax('admin', 'p_docker_shop_plan_save', function ($egn, $side) {
372 mnbt_plugin_require_admin();
373 $data = [
374 'id' => (int)($_POST['id'] ?? 0),
375 'name' => $_POST['name'] ?? '',
376 'description' => $_POST['description'] ?? '',
377 'category' => $_POST['category'] ?? '',
378 'node' => (int)($_POST['node'] ?? 0),
379 'base_plan_id' => (int)($_POST['base_plan_id'] ?? 0),
380 'enabled_periods' => isset($_POST['enabled_periods']) && is_array($_POST['enabled_periods']) ? $_POST['enabled_periods'] : [],
381 'status' => $_POST['status'] ?? 'active',
382 'sort' => (int)($_POST['sort'] ?? 50),
383 ];
384 foreach (docker_shop_periods() as $p => $cfg) {
385 $field = docker_shop_period_price_field($p);
386 $data[$field] = (int)round((float)($_POST['price'][$p] ?? 0) * 100);
387 }
388 $r = docker_shop_plan_save($data);
389 if ($r === true) {
390 json_exit_success('保存成功');
391 }
392 json_exit_error($r);
393});
394
395// 删除售卖套餐
396mnbt_register_ajax('admin', 'p_docker_shop_plan_delete', function ($egn, $side) {
397 mnbt_plugin_require_admin();
398 $plan_id = (int)($_POST['plan_id'] ?? 0);
399 if ($plan_id <= 0 || !docker_shop_plan_delete($plan_id)) {
400 json_exit_error('删除失败');
401 }
402 json_exit_success('删除成功');
403});