flyzto 1 周之前
父节点
当前提交
5e16b63bcc

+ 2 - 2
pinnacle/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "pinnacle_api_test",
-  "version": "1.0.0",
+  "version": "1.1.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "pinnacle_api_test",
-      "version": "1.0.0",
+      "version": "1.1.2",
       "license": "ISC",
       "dependencies": {
         "axios": "^1.12.2",

+ 5 - 2
pinnacle/package.json

@@ -1,12 +1,15 @@
 {
   "name": "pinnacle",
-  "version": "1.0.0",
+  "version": "1.1.2",
   "description": "",
   "main": "main.js",
   "type": "module",
   "scripts": {
     "dev": "nodemon --ignore data/ --ignore node_modules/ --inspect=9228 main.js",
-    "start": "pm2 start main.js --name pinnacle"
+    "start": "pm2 start main.js --name pinnacle",
+    "patch": "npm version patch --no-git-tag-version",
+    "minor": "npm version minor --no-git-tag-version",
+    "major": "npm version major --no-git-tag-version"
   },
   "author": "",
   "license": "ISC",

+ 177 - 0
server/models/Control.js

@@ -0,0 +1,177 @@
+const { exec } = require('child_process');
+const path = require('path');
+
+const GLOBAL_DATA = {
+  gitLock: false,
+  buildLock: false,
+};
+
+/**
+ * 拉取指定仓库的代码
+ * @param {string} repoPath 仓库路径
+ * @returns {Promise<string>} 拉取结果
+ */
+const gitPull = (repoPath) => {
+  if (GLOBAL_DATA.gitLock) {
+    return Promise.reject(new Error('git is locked'));
+  }
+  GLOBAL_DATA.gitLock = true;
+  return new Promise((resolve, reject) => {
+    const cwd = path.resolve(repoPath);
+    exec("git pull", { cwd }, (error, stdout, stderr) => {
+      GLOBAL_DATA.gitLock = false;
+      if (error) {
+        reject(new Error(stderr) || error);
+        return;
+      }
+      resolve(stdout || "No output");
+    });
+  });
+}
+
+/**
+ * 拉当前项目的代码
+ */
+const gitPullCurrent = () => {
+  const rootPath = path.resolve(__dirname, '../../');
+  return gitPull(rootPath);
+}
+
+/**
+ * 启动服务
+ * @param {string} serviceName 服务名称
+ * @returns {Promise<string>} 启动结果
+ */
+const pm2Start = (serviceName) => {
+  // if (process.env.NODE_ENV == 'development') {
+  //   return Promise.resolve('development mode, skip');
+  // }
+  return new Promise((resolve, reject) => {
+    exec(`pm2 start ${serviceName}`, (error, stdout, stderr) => {
+      if (error) {
+        reject(new Error(stderr) || error);
+        return;
+      }
+      resolve(stdout || "No output");
+    });
+  });
+}
+
+/**
+ * 停止服务
+ * @param {string} serviceName 服务名称
+ * @returns {Promise<string>} 停止结果
+ */
+const pm2Stop = (serviceName) => {
+  // if (process.env.NODE_ENV == 'development') {
+  //   return Promise.resolve('development mode, skip');
+  // }
+  return new Promise((resolve, reject) => {
+    exec(`pm2 stop ${serviceName}`, (error, stdout, stderr) => {
+      if (error) {
+        reject(new Error(stderr) || error);
+        return;
+      }
+      resolve(stdout || "No output");
+    });
+  });
+}
+
+/**
+ * PM2 重启服务
+ * @param {string} serviceName 服务名称
+ * @returns {Promise<string>} 重启结果
+ */
+const pm2Restart = (serviceName) => {
+  // if (process.env.NODE_ENV == 'development') {
+  //   return Promise.resolve('development mode, skip');
+  // }
+  return new Promise((resolve, reject) => {
+    exec(`pm2 restart ${serviceName}`, (error, stdout, stderr) => {
+      if (error) {
+        reject(new Error(stderr) || error);
+        return;
+      }
+      resolve(stdout || "No output");
+    });
+  });
+}
+
+/**
+ * 清除缓存
+ */
+const clearCache = (cachePath) => {
+  return new Promise((resolve, reject) => {
+    exec(`rm -rf ${cachePath}/*`, (error, stdout, stderr) => {
+      if (error) {
+        reject(new Error(stderr) || error);
+        return;
+      }
+      resolve(stdout || "No output");
+    });
+  });
+}
+
+/**
+ * 清除缓存并重启服务
+ */
+const pm2RestartClearCache = async (serviceName, cachePath) => {
+  return pm2Stop(serviceName)
+  .then(() => {
+    return clearCache(cachePath);
+  })
+  .then(() => {
+    return pm2Start(serviceName);
+  });
+}
+
+/**
+ * PM2 重启 sporttery 服务
+ * @param {boolean} hot 是否热重启
+ * @returns {Promise<string>} 重启结果
+ */
+const pm2RestartSporttery = (hot) => {
+  if (hot) {
+    return pm2Restart('sporttery');
+  }
+  else {
+    return pm2RestartClearCache('sporttery', path.resolve(__dirname, '../../server/data'));
+  }
+}
+
+/**
+ * PM2 重启 pinnacle 服务
+ * @param {boolean} hot 是否热重启
+ * @returns {Promise<string>} 重启结果
+ */
+const pm2RestartPinnacle = (hot) => {
+  if (hot) {
+    return pm2Restart('pinnacle');
+  }
+  else {
+    return pm2RestartClearCache('pinnacle', path.resolve(__dirname, '../../pinnacle/data'));
+  }
+}
+
+/**
+ * 发布 web 服务
+ */
+const releaseWeb = () => {
+  if (GLOBAL_DATA.buildLock) {
+    return Promise.reject(new Error('build is locked'));
+  }
+  GLOBAL_DATA.buildLock = true;
+  const rootPath = path.resolve(__dirname, '../../web');
+  return new Promise((resolve, reject) => {
+    exec('npm run build', { cwd: rootPath }, (error, stdout, stderr) => {
+      GLOBAL_DATA.buildLock = false;
+      if (error) {
+        reject(new Error(stderr) || error);
+        return;
+      }
+      resolve(stdout || "No output");
+    });
+  });
+}
+
+module.exports = { gitPullCurrent, pm2RestartSporttery, pm2RestartPinnacle, releaseWeb };

+ 41 - 2
server/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "sporttery-server",
-  "version": "1.0.0",
+  "version": "1.2.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "sporttery-server",
-      "version": "1.0.0",
+      "version": "1.2.2",
       "license": "ISC",
       "dependencies": {
         "axios": "^1.8.4",
@@ -640,6 +640,17 @@
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
       "license": "ISC"
     },
+    "node_modules/ip-address": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz",
+      "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">= 12"
+      }
+    },
     "node_modules/ipaddr.js": {
       "version": "1.9.1",
       "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -1213,6 +1224,34 @@
       "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
       "license": "MIT"
     },
+    "node_modules/smart-buffer": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz",
+      "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">= 6.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/socks": {
+      "version": "2.8.7",
+      "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.7.tgz",
+      "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "ip-address": "^10.0.1",
+        "smart-buffer": "^4.2.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
     "node_modules/sparse-bitfield": {
       "version": "3.0.3",
       "resolved": "https://registry.npmmirror.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",

+ 9 - 3
server/package.json

@@ -1,11 +1,17 @@
 {
   "name": "sporttery-server",
-  "version": "1.0.0",
+  "version": "1.2.2",
   "main": "server.js",
   "scripts": {
-    "dev": "nodemon --inspect server.js",
+    "dev": "nodemon --ignore data/ --ignore node_modules/ --inspect server.js",
+    "dev:pannel": "nodemon --ignore data/ --ignore node_modules/ --inspect=9231 pannel.js",
+    "dev:sporttery": "pm2 start server.js --node-args='--inspect' --name sporttery --watch --ignore-watch 'data node_modules'",
     "start": "pm2 start server.js --name sporttery",
-    "init": "node init.js"
+    "start:pannel": "pm2 start pannel.js --name pannel",
+    "init": "node init.js",
+    "patch": "npm version patch --no-git-tag-version",
+    "minor": "npm version minor --no-git-tag-version",
+    "major": "npm version major --no-git-tag-version"
   },
   "keywords": [],
   "author": "",

+ 54 - 0
server/pannel.js

@@ -0,0 +1,54 @@
+const express = require('express');
+const dotenv = require('dotenv');
+const Logs = require('./libs/logs');
+
+const controlRoutes = require('./routes/control');
+const cookieParser = require('cookie-parser');
+const app = express();
+
+dotenv.config();
+
+// 添加 CORS 支持
+app.use((req, res, next) => {
+  res.header('Access-Control-Allow-Origin', '*');
+  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
+  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+
+  if (req.method === 'OPTIONS') {
+    return res.sendStatus(200);
+  }
+  next();
+});
+
+// 中间件
+app.use(express.json({ limit: '10mb' }));
+app.use(cookieParser());
+
+app.use((req, res, next) => {
+  res.badRequest = (msg) => {
+    res.status(400).json({ statusCode: 400, code: -1, message: msg ?? 'Bad Request' });
+  }
+  res.unauthorized = (msg) => {
+    res.status(401).json({ statusCode: 401, code: -1,  message: msg ?? 'Unauthorized' });
+  }
+  res.notFound = (msg) => {
+    res.status(404).json({ statusCode: 404, code: -1,  message: msg ?? 'Not Found' });
+  }
+  res.serverError = (msg) => {
+    res.status(500).json({ statusCode: 500, code: -1,  message: msg ?? 'Internal Server Error' });
+  }
+  res.sendSuccess = (data, msg) => {
+    const response = { statusCode: 200, code: 0,  message: msg ?? 'OK' }
+    if (data) {
+      response.data = data;
+    }
+    res.status(200).json(response);
+  }
+  next();
+});
+
+app.use('/api/control', controlRoutes);
+
+// 启动服务
+const PORT = 9056;
+app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

+ 56 - 0
server/routes/control.js

@@ -0,0 +1,56 @@
+
+const express = require('express');
+const router = express.Router();
+
+const authMiddleware = require('../middleware/authMiddleware');
+
+const Control = require('../models/Control');
+const Logs = require('../libs/logs');
+
+router.get('/update_code', authMiddleware, (req, res) => {
+  Control.gitPullCurrent()
+  .then(result => {
+    res.sendSuccess(result);
+  })
+  .catch(err => {
+    Logs.errDev('更新代码失败:', err);
+    res.badRequest(err.message);
+  });
+});
+
+router.get('/restart_sporttery', authMiddleware, (req, res) => {
+  const hot = req.query.hot === 'true';
+  Control.pm2RestartSporttery(hot)
+  .then(result => {
+    res.sendSuccess(result);
+  })
+  .catch(err => {
+    Logs.errDev('重启 sporttery 服务失败:', err);
+    res.badRequest(err.message);
+  });
+});
+
+router.get('/restart_pinnacle', authMiddleware, (req, res) => {
+  const hot = req.query.hot === 'true';
+  Control.pm2RestartPinnacle(hot)
+  .then(result => {
+    res.sendSuccess(result);
+  })
+  .catch(err => {
+    Logs.errDev('重启 pinnacle 服务失败:', err);
+    res.badRequest(err.message);
+  });
+});
+
+router.get('/release_web', authMiddleware, (req, res) => {
+  Control.releaseWeb()
+  .then(result => {
+    res.sendSuccess(result);
+  })
+  .catch(err => {
+    Logs.errDev('发布 web 服务失败:', err);
+    res.badRequest(err.message);
+  });
+});
+
+module.exports = router;

+ 2 - 1
web/apps/web-antd/src/locales/langs/en-US/page.json

@@ -19,6 +19,7 @@
   "system": {
     "title": "System",
     "user": "User",
-    "parameter": "Parameter"
+    "parameter": "Parameter",
+    "control": "Control Panel"
   }
 }

+ 2 - 1
web/apps/web-antd/src/locales/langs/zh-CN/page.json

@@ -19,6 +19,7 @@
   "system": {
     "title": "系统设置",
     "user": "用户管理",
-    "parameter": "参数设置"
+    "parameter": "参数设置",
+    "control": "控制面板"
   }
 }

+ 9 - 0
web/apps/web-antd/src/router/routes/modules/system.ts

@@ -32,6 +32,15 @@ const routes: RouteRecordRaw[] = [
           title: $t('page.system.parameter'),
         },
       },
+      {
+        name: 'BackendControl',
+        path: 'control',
+        component: () => import('#/views/system/control/index.vue'),
+        meta: {
+          icon: 'ion:server-outline',
+          title: $t('page.system.control'),
+        },
+      },
     ],
   },
 ];

+ 128 - 0
web/apps/web-antd/src/views/system/control/index.vue

@@ -0,0 +1,128 @@
+<script setup>
+import { Page } from '@vben/common-ui';
+import { requestClient } from '#/api/request';
+import { Card, Button, Space, message } from 'ant-design-vue';
+import { ref } from 'vue';
+
+const buttonDisabled = ref(false);
+const updateLoaderHide = ref(null);
+
+const ControlAction = {
+  async updateCode() {
+    return requestClient.get('/control/update_code')
+    .then(result => {
+      message.success('更新代码成功');
+      console.log('更新代码成功');
+      console.log(result);
+    });
+  },
+
+  async restartSportteryHot() {
+    return requestClient.get('/control/restart_sporttery', { params: { hot: true } })
+    .then(result => {
+      message.success('重启 sporttery 服务成功');
+      console.log('重启 sporttery 服务成功');
+      console.log(result);
+    });
+  },
+
+  async restartSportteryCold() {
+    return requestClient.get('/control/restart_sporttery', { params: { hot: false } })
+    .then(result => {
+      message.success('重启 sporttery 服务成功');
+      console.log('重启 sporttery 服务成功');
+      console.log(result);
+    });
+  },
+
+  async restartPinnacleHot() {
+    return requestClient.get('/control/restart_pinnacle', { params: { hot: true } })
+    .then(result => {
+      message.success('重启 pinnacle 服务成功');
+      console.log('重启 pinnacle 服务成功');
+      console.log(result);
+    });
+  },
+
+  async restartPinnacleCold() {
+    return requestClient.get('/control/restart_pinnacle', { params: { hot: false } })
+    .then(result => {
+      message.success('重启 pinnacle 服务成功');
+      console.log('重启 pinnacle 服务成功');
+      console.log(result);
+    });
+  },
+
+  async releaseWeb() {
+    return requestClient.get('/control/release_web', { timeout: 90_000 })
+    .then(result => {
+      message.success('发布 web 服务成功');
+      console.log('发布 web 服务成功');
+      console.log(result);
+    });
+  },
+}
+
+const handleAction = (action) => {
+  buttonDisabled.value = true;
+  updateLoaderHide.value = message.loading('执行中...', 0);
+  ControlAction[action]?.()
+  .finally(() => {
+    buttonDisabled.value = false;
+    updateLoaderHide.value?.();
+    updateLoaderHide.value = null;
+  });
+}
+
+</script>
+
+<template>
+  <Page title="控制面板">
+    <Space direction="vertical" size="large" style="width: 100%">
+      <Card title="全局控制">
+        <Space>
+          <Button type="primary" @click="handleAction('updateCode')" ghost :disabled="buttonDisabled">
+            更新代码
+          </Button>
+        </Space>
+      </Card>
+
+      <Card title="服务管理">
+        <Space>
+          <Button @click="handleAction('restartSportteryHot')" type="primary" ghost :disabled="buttonDisabled">
+            热重启服务
+          </Button>
+
+          <Button @click="handleAction('restartSportteryCold')" ghost danger :disabled="buttonDisabled">
+            冷重启服务
+          </Button>
+        </Space>
+      </Card>
+
+      <Card title="Web管理">
+        <Space>
+          <Button @click="handleAction('releaseWeb')" type="primary" ghost :disabled="buttonDisabled">
+            发布Web服务
+          </Button>
+        </Space>
+      </Card>
+
+
+      <Card title="PS采集管理">
+        <Space>
+          <Button @click="handleAction('restartPinnacleHot')" type="primary" ghost :disabled="buttonDisabled">
+            热重启服务
+          </Button>
+
+          <Button @click="handleAction('restartPinnacleCold')" ghost danger :disabled="buttonDisabled">
+            冷重启服务
+          </Button>
+        </Space>
+      </Card>
+    </Space>
+  </Page>
+</template>
+
+<style scoped>
+</style>
+

+ 6 - 0
web/apps/web-antd/vite.config.mts

@@ -12,6 +12,12 @@ export default defineConfig(async () => {
       },
       server: {
         proxy: {
+          '/api/control': {
+            changeOrigin: true,
+            rewrite: (path) => path.replace(/^\/api\/control/, ''),
+            target: 'http://127.0.0.1:9056/api/control',
+            ws: true,
+          },
           '/api': {
             changeOrigin: true,
             rewrite: (path) => path.replace(/^\/api/, ''),