feat: 添加PostgreSQL数据库集成和机台管理功能
- 新增PostgreSQL数据库支持,包含内置和外部数据库两种部署模式 - 实现机台管理功能,包括保护状态控制和VHD关键词设置 - 添加用户认证系统,支持会话管理和权限控制 - 更新Docker配置,支持数据库持久化和健康检查 - 扩展API接口,提供完整的RESTful API文档 - 更新客户端代码以支持机台ID和保护检查功能 - 添加数据库初始化脚本和配置指南
This commit is contained in:
@@ -1,5 +1,67 @@
|
||||
# VHD Mounter - 智能SEGA街机游戏VHD挂载管理和运行保活工具
|
||||
|
||||
> 🎮 **完整的街机游戏VHD管理解决方案**
|
||||
> 包含本地VHD挂载工具和配套的Web服务器,提供集中化配置管理和远程控制功能
|
||||
|
||||
## 📦 项目组成
|
||||
|
||||
本项目包含两个核心组件:
|
||||
|
||||
### 🖥️ VHD Mounter (主程序)
|
||||
智能VHD挂载和进程保活工具,支持本地和远程配置管理
|
||||
|
||||
### 🌐 VHDSelectServer (配套服务器)
|
||||
基于Node.js的Web服务器,提供:
|
||||
- 🎯 **集中化VHD选择管理** - Web GUI界面和RESTful API
|
||||
- 🐳 **Docker化部署** - 支持内置/外部PostgreSQL数据库
|
||||
- 🔐 **用户认证系统** - 安全的会话管理
|
||||
- 🛡️ **机台保护功能** - 防止误操作的保护机制
|
||||
- 📊 **实时状态监控** - 健康检查和系统状态API
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
vhdmount/
|
||||
├── 📁 VHDSelectServer/ # Web服务器子项目
|
||||
│ ├── 📄 server.js # 主服务器文件
|
||||
│ ├── 📄 database.js # 数据库操作模块
|
||||
│ ├── 📄 package.json # Node.js依赖配置
|
||||
│ ├── 📄 Dockerfile # Docker镜像构建文件
|
||||
│ ├── 📄 docker-compose.yml # Docker Compose配置
|
||||
│ ├── 📄 docker-compose.external-db.yml # 外部数据库配置
|
||||
│ ├── 📄 docker-entrypoint.sh # Docker启动脚本
|
||||
│ ├── 📄 init-db.sql # 数据库初始化脚本
|
||||
│ ├── 📄 init-embedded-db.sh # 内置数据库初始化
|
||||
│ ├── 📄 setup_postgresql.sql # PostgreSQL设置脚本
|
||||
│ ├── 📄 README.md # VHDSelectServer详细文档
|
||||
│ ├── 📄 DOCKER_TROUBLESHOOTING.md # Docker故障排除指南
|
||||
│ ├── 📄 POSTGRESQL_SETUP.md # PostgreSQL设置指南
|
||||
│ └── 📁 config/ # 配置文件目录
|
||||
├── 📄 VHDMounter.exe # 主程序可执行文件
|
||||
├── 📄 VHDMounter.csproj # .NET项目文件
|
||||
├── 📄 MainWindow.xaml # WPF主窗口界面
|
||||
├── 📄 MainWindow.xaml.cs # 主窗口逻辑代码
|
||||
├── 📄 VHDManager.cs # VHD管理核心类
|
||||
├── 📄 Program.cs # 程序入口点
|
||||
├── 📄 vhdmonter_config.ini # 客户端配置文件
|
||||
├── 📄 README.md # 项目主文档
|
||||
├── 📄 VHD_Mount_Fix_Guide.md # VHD挂载故障排除指南
|
||||
├── 📄 build.bat # 构建脚本
|
||||
├── 📄 debug.bat # 调试脚本
|
||||
├── 📄 run_as_admin.bat # 管理员运行脚本
|
||||
└── 📁 .github/workflows/ # GitHub Actions CI/CD配置
|
||||
└── 📄 build.yml # 自动构建配置
|
||||
```
|
||||
|
||||
### 目录说明
|
||||
|
||||
- **VHDSelectServer/**: 独立的Web服务器子项目,提供集中化配置管理
|
||||
- **主程序文件**: VHD挂载和进程管理的核心Windows应用程序
|
||||
- **配置文件**: 客户端和服务器的配置管理文件
|
||||
- **文档**: 详细的使用指南和故障排除文档
|
||||
- **脚本**: 构建、调试和部署的辅助脚本
|
||||
- **CI/CD**: 自动化构建和部署配置
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
### 🔍 智能扫描与识别
|
||||
@@ -8,10 +70,19 @@
|
||||
- **快速定位**:优化的扫描算法,快速定位目标文件
|
||||
|
||||
### 🌐 智能网络配置管理
|
||||
- **远程关键词获取**:通过HTTP API自动获取启动关键词,实现集中化配置管理
|
||||
- **VHDSelectServer集成**:配套的Node.js Web服务器,提供完整的集中化配置管理解决方案
|
||||
- **RESTful API接口**:标准化的HTTP API,支持VHD关键词获取、设置和状态查询
|
||||
- **Web GUI管理界面**:直观的网页操作界面,支持实时VHD选择和机台保护设置
|
||||
- **多部署模式支持**:
|
||||
- Docker单容器部署(内置PostgreSQL数据库)
|
||||
- Docker Compose部署(外部PostgreSQL数据库)
|
||||
- 本地开发模式部署
|
||||
- **配置文件驱动**:通过`vhdmonter_config.ini`灵活控制远程功能开关和服务器地址
|
||||
- **智能降级机制**:远程获取失败时自动回退到本地选择模式,确保程序稳定运行
|
||||
- **跨平台服务器支持**:配套VHDSelectServer提供Web GUI和API接口,支持Docker部署
|
||||
- **实时配置同步**:服务器端配置变更立即生效,无需重启客户端程序
|
||||
- **安全认证机制**:基于会话的用户认证,防止未授权访问和误操作
|
||||
- **机台保护功能**:支持设置机台保护状态,防止意外的VHD切换操作
|
||||
- **健康检查监控**:内置健康检查端点,支持服务状态监控和故障诊断
|
||||
|
||||
### 🔌 智能USB设备管理
|
||||
- **NX_INS设备识别**:自动检测卷标为"NX_INS"的USB设备,专为街机系统优化
|
||||
@@ -41,6 +112,121 @@
|
||||
- **安全退出**:确保在系统关机前完成VHD卸载操作
|
||||
- **资源保护**:防止因异常退出导致的VHD资源占用
|
||||
|
||||
## 🌐 VHDSelectServer - 集中化配置管理服务器
|
||||
|
||||
VHDSelectServer是本项目的配套Web服务器,提供集中化的VHD选择管理和远程控制功能。
|
||||
|
||||
### ✨ 主要特性
|
||||
|
||||
#### 🎯 集中化VHD管理
|
||||
- **Web GUI界面**:直观的网页操作界面,支持VHD关键词选择
|
||||
- **RESTful API**:完整的API接口,支持程序化集成
|
||||
- **实时配置**:动态更新VHD选择配置,无需重启客户端
|
||||
|
||||
#### 🐳 Docker化部署
|
||||
- **一键部署**:支持Docker和Docker Compose快速部署
|
||||
- **双数据库模式**:
|
||||
- 内置PostgreSQL:单容器部署,适合小规模使用
|
||||
- 外部PostgreSQL:分离式部署,适合生产环境
|
||||
- **健康检查**:内置健康检查机制,确保服务稳定运行
|
||||
|
||||
#### 🔐 安全认证系统
|
||||
- **用户登录**:基于会话的用户认证机制
|
||||
- **权限控制**:防止未授权访问和误操作
|
||||
- **会话管理**:安全的会话超时和自动登出
|
||||
|
||||
#### 🛡️ 机台保护功能
|
||||
- **保护状态**:可设置机台保护状态,防止误操作
|
||||
- **状态持久化**:保护状态自动保存到数据库
|
||||
- **API控制**:支持通过API查询和设置保护状态
|
||||
|
||||
#### 📊 系统监控
|
||||
- **状态API**:实时系统状态查询接口
|
||||
- **健康检查**:服务健康状态监控
|
||||
- **版本信息**:API版本和系统信息查询
|
||||
|
||||
### 🚀 快速部署
|
||||
|
||||
#### Docker部署(推荐)
|
||||
|
||||
**方式一:内置数据库(单容器)**
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t vhd-select-server ./VHDSelectServer
|
||||
|
||||
# 运行容器
|
||||
docker run -d \
|
||||
--name vhd-select-server \
|
||||
-p 8080:8080 \
|
||||
-v vhd_data:/app/data \
|
||||
vhd-select-server
|
||||
```
|
||||
|
||||
**方式二:外部数据库(Docker Compose)**
|
||||
```bash
|
||||
# 进入VHDSelectServer目录
|
||||
cd VHDSelectServer
|
||||
|
||||
# 启动服务(包含PostgreSQL)
|
||||
docker-compose up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
#### 本地开发部署
|
||||
```bash
|
||||
# 进入VHDSelectServer目录
|
||||
cd VHDSelectServer
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动服务
|
||||
npm start
|
||||
```
|
||||
|
||||
### 🔧 配置说明
|
||||
|
||||
#### 环境变量配置
|
||||
- `DB_TYPE`:数据库类型(embedded/external)
|
||||
- `DB_HOST`:数据库主机地址(外部数据库模式)
|
||||
- `DB_PORT`:数据库端口(默认5432)
|
||||
- `DB_NAME`:数据库名称(默认vhd_select)
|
||||
- `DB_USER`:数据库用户名(默认postgres)
|
||||
- `DB_PASSWORD`:数据库密码(默认postgres)
|
||||
- `PORT`:服务端口(默认8080)
|
||||
|
||||
#### 客户端配置
|
||||
在主程序目录下的`vhdmonter_config.ini`文件中配置:
|
||||
```ini
|
||||
[RemoteSelection]
|
||||
EnableRemoteSelection=true
|
||||
BootImageSelectUrl=http://localhost:8080/api/boot-image-select
|
||||
```
|
||||
|
||||
### 📡 API接口
|
||||
|
||||
详细的API文档请参考 <mcfile name="README.md" path="VHDSelectServer/README.md"></mcfile>
|
||||
|
||||
主要接口包括:
|
||||
- `GET /api/boot-image-select` - 获取当前VHD选择
|
||||
- `POST /api/set-vhd` - 设置VHD关键词
|
||||
- `GET /api/status` - 获取系统状态
|
||||
- `POST /api/auth/login` - 用户登录
|
||||
- `GET /api/machines` - 机台管理
|
||||
- `POST /api/protect` - 设置保护状态
|
||||
|
||||
### 🌍 Web界面
|
||||
|
||||
访问 `http://localhost:8080` 打开Web管理界面,功能包括:
|
||||
- VHD关键词选择和设置
|
||||
- 机台保护状态管理
|
||||
- 系统状态监控
|
||||
- 用户登录和会话管理
|
||||
|
||||
默认登录密码:`admin123`
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 系统要求
|
||||
@@ -52,12 +238,14 @@
|
||||
|
||||
### 安装与运行
|
||||
|
||||
#### 方式一:直接运行(推荐)
|
||||
#### VHD Mounter (主程序)
|
||||
|
||||
**方式一:直接运行(推荐)**
|
||||
1. 下载最新版本的可执行文件
|
||||
2. 右键点击 → "以管理员身份运行"
|
||||
3. 程序将自动开始扫描和挂载流程
|
||||
|
||||
#### 方式二:从源码编译
|
||||
**方式二:从源码编译**
|
||||
```powershell
|
||||
# 克隆仓库
|
||||
git clone https://github.com/username/vhdmount.git
|
||||
@@ -70,6 +258,41 @@ dotnet build --configuration Release
|
||||
dotnet publish --configuration Release --self-contained true --runtime win-x64
|
||||
```
|
||||
|
||||
#### VHDSelectServer (配套服务器)
|
||||
|
||||
**Docker部署(推荐)**
|
||||
```powershell
|
||||
# 进入VHDSelectServer目录
|
||||
cd VHDSelectServer
|
||||
|
||||
# 方式一:单容器部署(内置数据库)
|
||||
docker build -t vhd-select-server .
|
||||
docker run -d --name vhd-select-server -p 8080:8080 -v vhd_data:/app/data vhd-select-server
|
||||
|
||||
# 方式二:Docker Compose部署(外部数据库)
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
**本地开发部署**
|
||||
```powershell
|
||||
# 进入VHDSelectServer目录
|
||||
cd VHDSelectServer
|
||||
|
||||
# 安装Node.js依赖
|
||||
npm install
|
||||
|
||||
# 启动服务器
|
||||
npm start
|
||||
```
|
||||
|
||||
**配置客户端连接**
|
||||
在主程序目录创建或编辑 `vhdmonter_config.ini`:
|
||||
```ini
|
||||
[RemoteSelection]
|
||||
EnableRemoteSelection=true
|
||||
BootImageSelectUrl=http://localhost:8080/api/boot-image-select
|
||||
```
|
||||
|
||||
## 📖 使用指南
|
||||
|
||||
### 基本操作流程
|
||||
@@ -151,12 +374,43 @@ VHDMounter.exe --debug
|
||||
|
||||
## 🔧 技术架构
|
||||
|
||||
### 整体架构
|
||||
|
||||
本项目采用客户端-服务器架构,包含两个核心组件:
|
||||
|
||||
```
|
||||
┌─────────────────────┐ HTTP API ┌─────────────────────┐
|
||||
│ VHD Mounter │◄──────────────►│ VHDSelectServer │
|
||||
│ (Windows Client) │ │ (Web Server) │
|
||||
└─────────────────────┘ └─────────────────────┘
|
||||
│ │
|
||||
├─ VHD挂载管理 ├─ 集中化配置管理
|
||||
├─ 进程监控保活 ├─ Web GUI界面
|
||||
├─ USB设备检测 ├─ RESTful API
|
||||
├─ 远程配置获取 ├─ 用户认证系统
|
||||
└─ 本地文件操作 └─ 数据库持久化
|
||||
```
|
||||
|
||||
### 核心技术栈
|
||||
|
||||
#### VHD Mounter (客户端)
|
||||
- **UI框架**:WPF (.NET 6.0)
|
||||
- **VHD操作**:Windows DiskPart API
|
||||
- **进程管理**:System.Diagnostics
|
||||
- **系统集成**:Microsoft.Win32.SystemEvents
|
||||
- **异步编程**:async/await 模式
|
||||
- **HTTP客户端**:HttpClient (.NET 6.0)
|
||||
- **配置管理**:INI文件解析
|
||||
|
||||
#### VHDSelectServer (服务端)
|
||||
- **运行时**:Node.js 18+
|
||||
- **Web框架**:Express.js
|
||||
- **数据库**:PostgreSQL 13+
|
||||
- **ORM**:原生SQL查询
|
||||
- **认证**:Express-session
|
||||
- **容器化**:Docker & Docker Compose
|
||||
- **前端**:原生HTML/CSS/JavaScript
|
||||
- **健康检查**:内置健康检查端点
|
||||
|
||||
### 关键组件
|
||||
|
||||
@@ -181,6 +435,46 @@ VHDMounter.exe --debug
|
||||
- 应用程序生命周期控制
|
||||
- 执行步骤状态显示和暂停控制
|
||||
|
||||
#### VHDSelectServer 核心模块
|
||||
|
||||
**server.js (主服务器)**
|
||||
- Express应用初始化和路由配置
|
||||
- 中间件管理(认证、CORS、静态文件)
|
||||
- API端点实现和错误处理
|
||||
- 服务器启动和健康检查
|
||||
|
||||
**database.js (数据库层)**
|
||||
- PostgreSQL连接管理
|
||||
- 数据库初始化和表创建
|
||||
- SQL查询封装和事务处理
|
||||
- 连接池管理和错误恢复
|
||||
|
||||
**认证中间件**
|
||||
- 会话验证和用户状态检查
|
||||
- 登录状态管理和权限控制
|
||||
- 安全头设置和CSRF防护
|
||||
|
||||
**API路由模块**
|
||||
- `/api/auth/*` - 用户认证相关接口
|
||||
- `/api/boot-image-select` - VHD选择获取接口
|
||||
- `/api/set-vhd` - VHD关键词设置接口
|
||||
- `/api/machines/*` - 机台管理接口
|
||||
- `/api/protect` - 保护状态控制接口
|
||||
- `/api/status` - 系统状态查询接口
|
||||
|
||||
### 集成机制
|
||||
|
||||
#### 客户端-服务器通信
|
||||
- **HTTP协议**:基于RESTful API的标准HTTP通信
|
||||
- **JSON格式**:统一的数据交换格式
|
||||
- **错误处理**:完整的错误响应和降级机制
|
||||
- **超时控制**:网络请求超时和重试机制
|
||||
|
||||
#### 配置同步
|
||||
- **实时更新**:服务器配置变更立即生效
|
||||
- **本地缓存**:客户端配置文件作为备用方案
|
||||
- **降级策略**:网络异常时自动切换到本地模式
|
||||
|
||||
### 安全机制
|
||||
- **权限检查**:启动时验证管理员权限
|
||||
- **资源清理**:异常情况下的资源自动释放
|
||||
|
||||
+160
-4
@@ -9,10 +9,11 @@ using System.Text.RegularExpressions;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
|
||||
namespace VHDMounter
|
||||
{
|
||||
public class VHDManager
|
||||
public class VHDManager : IDisposable
|
||||
{
|
||||
private const string TARGET_DRIVE = "M:";
|
||||
private readonly string[] TARGET_KEYWORDS = { "SDEZ", "SDHD", "SDDT" };
|
||||
@@ -23,6 +24,146 @@ namespace VHDMounter
|
||||
private static readonly HttpClient httpClient = new HttpClient();
|
||||
private string configFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "vhdmonter_config.ini");
|
||||
|
||||
// 保护检查相关字段
|
||||
private Timer protectionTimer;
|
||||
private string machineId;
|
||||
private bool isProtectionCheckEnabled;
|
||||
private string protectionCheckUrl;
|
||||
private int protectionCheckInterval;
|
||||
|
||||
// 构造函数
|
||||
public VHDManager()
|
||||
{
|
||||
InitializeProtectionCheck();
|
||||
}
|
||||
|
||||
// 初始化保护检查功能
|
||||
private void InitializeProtectionCheck()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = ReadConfig();
|
||||
|
||||
// 读取Machine ID
|
||||
machineId = config.TryGetValue("MachineId", out var id) ? id : "MACHINE_001";
|
||||
|
||||
// 读取保护检查配置
|
||||
isProtectionCheckEnabled = config.TryGetValue("EnableProtectionCheck", out var enableCheck) &&
|
||||
bool.TryParse(enableCheck, out var enabled) && enabled;
|
||||
|
||||
protectionCheckUrl = config.TryGetValue("ProtectionCheckUrl", out var url) ? url : "http://localhost:8080/api/protect";
|
||||
|
||||
protectionCheckInterval = config.TryGetValue("ProtectionCheckInterval", out var interval) &&
|
||||
int.TryParse(interval, out var intervalMs) ? intervalMs : 500;
|
||||
|
||||
if (isProtectionCheckEnabled)
|
||||
{
|
||||
StartProtectionCheck();
|
||||
StatusChanged?.Invoke($"保护检查已启用 - Machine ID: {machineId}, 检查间隔: {protectionCheckInterval}ms");
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusChanged?.Invoke("保护检查功能已禁用");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusChanged?.Invoke($"初始化保护检查失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 启动保护检查定时器
|
||||
private void StartProtectionCheck()
|
||||
{
|
||||
if (protectionTimer != null)
|
||||
{
|
||||
protectionTimer.Stop();
|
||||
protectionTimer.Dispose();
|
||||
}
|
||||
|
||||
protectionTimer = new Timer(protectionCheckInterval);
|
||||
protectionTimer.Elapsed += async (sender, e) => await CheckProtectionStatus();
|
||||
protectionTimer.AutoReset = true;
|
||||
protectionTimer.Start();
|
||||
}
|
||||
|
||||
// 检查保护状态
|
||||
private async Task CheckProtectionStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!isProtectionCheckEnabled || string.IsNullOrWhiteSpace(protectionCheckUrl))
|
||||
return;
|
||||
|
||||
var requestUrl = $"{protectionCheckUrl}?machineId={Uri.EscapeDataString(machineId)}";
|
||||
var response = await httpClient.GetAsync(requestUrl);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var jsonContent = await response.Content.ReadAsStringAsync();
|
||||
var jsonDoc = JsonDocument.Parse(jsonContent);
|
||||
|
||||
if (jsonDoc.RootElement.TryGetProperty("protected", out var protectedElement) &&
|
||||
protectedElement.GetBoolean())
|
||||
{
|
||||
StatusChanged?.Invoke($"收到保护信号 - Machine ID: {machineId},正在执行自动关机...");
|
||||
await ExecuteShutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"保护状态检查失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 执行自动关机
|
||||
private async Task ExecuteShutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusChanged?.Invoke("正在执行自动关机...");
|
||||
|
||||
// 停止保护检查定时器
|
||||
if (protectionTimer != null)
|
||||
{
|
||||
protectionTimer.Stop();
|
||||
protectionTimer.Dispose();
|
||||
protectionTimer = null;
|
||||
}
|
||||
|
||||
// 执行关机命令
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "shutdown",
|
||||
Arguments = "/s /t 0",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
await process.WaitForExitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusChanged?.Invoke($"自动关机执行失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 停止保护检查
|
||||
public void StopProtectionCheck()
|
||||
{
|
||||
if (protectionTimer != null)
|
||||
{
|
||||
protectionTimer.Stop();
|
||||
protectionTimer.Dispose();
|
||||
protectionTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 调试方法:检查特定文件是否符合条件
|
||||
public bool IsVHDFileValid(string filePath)
|
||||
{
|
||||
@@ -402,9 +543,11 @@ namespace VHDMounter
|
||||
return null;
|
||||
}
|
||||
|
||||
StatusChanged?.Invoke($"正在从远程获取VHD选择: {url}");
|
||||
// 添加Machine ID参数
|
||||
var requestUrl = $"{url}?machineId={Uri.EscapeDataString(machineId)}";
|
||||
StatusChanged?.Invoke($"正在从远程获取VHD选择: {requestUrl}");
|
||||
|
||||
var response = await httpClient.GetAsync(url);
|
||||
var response = await httpClient.GetAsync(requestUrl);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
StatusChanged?.Invoke($"远程请求失败: {response.StatusCode}");
|
||||
@@ -417,7 +560,7 @@ namespace VHDMounter
|
||||
if (jsonDoc.RootElement.TryGetProperty("BootImageSelected", out var bootImageElement))
|
||||
{
|
||||
var selectedKeyword = bootImageElement.GetString();
|
||||
StatusChanged?.Invoke($"远程选择的VHD关键词: {selectedKeyword}");
|
||||
StatusChanged?.Invoke($"远程选择的VHD关键词: {selectedKeyword} (Machine ID: {machineId})");
|
||||
return selectedKeyword;
|
||||
}
|
||||
else
|
||||
@@ -1132,5 +1275,18 @@ exit";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// IDisposable实现
|
||||
public void Dispose()
|
||||
{
|
||||
StopProtectionCheck();
|
||||
httpClient?.Dispose();
|
||||
}
|
||||
|
||||
// 析构函数
|
||||
~VHDManager()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,4 +113,5 @@ public
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
temp/
|
||||
vhd-data
|
||||
@@ -1,6 +1,15 @@
|
||||
# 使用官方Node.js运行时作为基础镜像
|
||||
FROM node:18-alpine
|
||||
|
||||
# 安装PostgreSQL和相关工具
|
||||
RUN apk add --no-cache \
|
||||
postgresql \
|
||||
postgresql-contrib \
|
||||
postgresql-client \
|
||||
su-exec \
|
||||
bash \
|
||||
curl
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
@@ -13,8 +22,13 @@ RUN npm ci --only=production
|
||||
# 复制应用源代码
|
||||
COPY . .
|
||||
|
||||
# 创建配置文件目录
|
||||
RUN mkdir -p /app/config
|
||||
# 创建必要的目录
|
||||
RUN mkdir -p /app/config /var/lib/postgresql/data /app/scripts
|
||||
|
||||
# 复制数据库初始化脚本
|
||||
COPY init-db.sql /app/scripts/
|
||||
COPY docker-entrypoint.sh /app/scripts/
|
||||
RUN chmod +x /app/scripts/docker-entrypoint.sh
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
@@ -23,14 +37,28 @@ EXPOSE 8080
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=8080
|
||||
ENV CONFIG_PATH=/app/config
|
||||
# 数据库配置环境变量
|
||||
ENV USE_EMBEDDED_DB=true
|
||||
ENV DB_HOST=localhost
|
||||
ENV DB_PORT=5432
|
||||
ENV DB_NAME=vhd_select
|
||||
ENV DB_USER=vhd_user
|
||||
ENV DB_PASSWORD=vhd_password
|
||||
ENV POSTGRES_DATA_DIR=/var/lib/postgresql/data
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S nodejs -u 1001
|
||||
# 创建postgres用户和nodejs用户
|
||||
RUN addgroup -S postgres || true && \
|
||||
adduser -S -D -H -h /var/lib/postgresql -s /bin/sh -G postgres postgres || true && \
|
||||
addgroup -S nodejs || true && \
|
||||
adduser -S nodejs -G nodejs || true
|
||||
|
||||
# 更改文件所有权
|
||||
# 创建PostgreSQL数据目录和运行时目录并设置权限
|
||||
RUN mkdir -p /var/lib/postgresql/data /run/postgresql && \
|
||||
chown -R postgres:postgres /var/lib/postgresql /run/postgresql && \
|
||||
chmod 700 /var/lib/postgresql/data
|
||||
|
||||
# 更改应用文件所有权
|
||||
RUN chown -R nodejs:nodejs /app
|
||||
USER nodejs
|
||||
|
||||
# 启动应用
|
||||
CMD ["npm", "start"]
|
||||
# 启动脚本
|
||||
CMD ["/app/scripts/docker-entrypoint.sh"]
|
||||
@@ -0,0 +1,213 @@
|
||||
# PostgreSQL安装和配置指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本指南将帮助你在Windows系统上安装和配置PostgreSQL数据库,以支持VHDSelectServer的数据持久化功能。
|
||||
|
||||
## 🔧 安装PostgreSQL
|
||||
|
||||
### 方法一:官方安装程序(推荐)
|
||||
|
||||
1. **下载PostgreSQL**
|
||||
- 访问:https://www.postgresql.org/download/windows/
|
||||
- 点击"Download the installer"
|
||||
- 选择最新版本(推荐PostgreSQL 15或16)
|
||||
|
||||
2. **运行安装程序**
|
||||
- 以管理员身份运行下载的安装程序
|
||||
- 按照以下设置进行安装:
|
||||
|
||||
```
|
||||
安装组件:
|
||||
✅ PostgreSQL Server
|
||||
✅ pgAdmin 4
|
||||
✅ Stack Builder
|
||||
✅ Command Line Tools
|
||||
|
||||
安装目录:C:\Program Files\PostgreSQL\16 (默认)
|
||||
数据目录:C:\Program Files\PostgreSQL\16\data (默认)
|
||||
|
||||
超级用户密码:password
|
||||
端口:5432
|
||||
区域设置:[Default locale]
|
||||
```
|
||||
|
||||
3. **完成安装**
|
||||
- 安装完成后,确保PostgreSQL服务已启动
|
||||
- 可以在"服务"中查看"postgresql-x64-16"服务状态
|
||||
|
||||
### 方法二:使用包管理器
|
||||
|
||||
如果你使用Chocolatey:
|
||||
```bash
|
||||
choco install postgresql
|
||||
```
|
||||
|
||||
如果你使用Scoop:
|
||||
```bash
|
||||
scoop install postgresql
|
||||
```
|
||||
|
||||
## 🗄️ 配置数据库
|
||||
|
||||
### 自动配置(推荐)
|
||||
|
||||
1. **运行配置脚本**
|
||||
```bash
|
||||
cd VHDSelectServer
|
||||
setup_database.bat
|
||||
```
|
||||
|
||||
2. **输入密码**
|
||||
- 脚本会提示输入PostgreSQL超级用户密码
|
||||
- 输入安装时设置的密码(默认:password)
|
||||
|
||||
### 手动配置
|
||||
|
||||
如果自动配置失败,可以手动执行以下步骤:
|
||||
|
||||
1. **打开命令提示符**
|
||||
```bash
|
||||
# 设置PostgreSQL环境变量(如果需要)
|
||||
set PATH=%PATH%;C:\Program Files\PostgreSQL\16\bin
|
||||
```
|
||||
|
||||
2. **连接到PostgreSQL**
|
||||
```bash
|
||||
psql -U postgres -h localhost
|
||||
```
|
||||
|
||||
3. **执行配置脚本**
|
||||
```sql
|
||||
\i setup_postgresql.sql
|
||||
```
|
||||
|
||||
## 🔍 验证安装
|
||||
|
||||
### 检查PostgreSQL服务
|
||||
|
||||
1. **检查服务状态**
|
||||
```bash
|
||||
sc query postgresql-x64-16
|
||||
```
|
||||
|
||||
2. **启动服务(如果未运行)**
|
||||
```bash
|
||||
net start postgresql-x64-16
|
||||
```
|
||||
|
||||
### 测试数据库连接
|
||||
|
||||
1. **使用psql连接**
|
||||
```bash
|
||||
psql -U postgres -h localhost -d vhd_select
|
||||
```
|
||||
|
||||
2. **查看表结构**
|
||||
```sql
|
||||
\dt
|
||||
SELECT * FROM machines;
|
||||
```
|
||||
|
||||
### 测试VHDSelectServer连接
|
||||
|
||||
1. **更新.env文件**
|
||||
确保`.env`文件中的配置正确:
|
||||
```env
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=vhd_select
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=password
|
||||
```
|
||||
|
||||
2. **重启VHDSelectServer**
|
||||
```bash
|
||||
node server.js
|
||||
```
|
||||
|
||||
3. **检查连接状态**
|
||||
- 查看控制台输出,应该显示"数据库连接成功"
|
||||
- 访问Web UI:http://localhost:8080
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **"psql不是内部或外部命令"**
|
||||
- 解决方案:将PostgreSQL的bin目录添加到系统PATH环境变量
|
||||
- 路径:`C:\Program Files\PostgreSQL\16\bin`
|
||||
|
||||
2. **连接被拒绝**
|
||||
- 检查PostgreSQL服务是否运行
|
||||
- 检查防火墙设置
|
||||
- 确认端口5432未被占用
|
||||
|
||||
3. **密码认证失败**
|
||||
- 确认超级用户密码正确
|
||||
- 检查pg_hba.conf配置文件
|
||||
|
||||
4. **数据库不存在**
|
||||
- 重新运行setup_postgresql.sql脚本
|
||||
- 手动创建数据库:`CREATE DATABASE vhd_select;`
|
||||
|
||||
### 重置数据库
|
||||
|
||||
如果需要重置数据库:
|
||||
|
||||
```sql
|
||||
-- 连接到postgres数据库
|
||||
\c postgres
|
||||
|
||||
-- 删除现有数据库
|
||||
DROP DATABASE IF EXISTS vhd_select;
|
||||
|
||||
-- 重新运行配置脚本
|
||||
\i setup_postgresql.sql
|
||||
```
|
||||
|
||||
## 📊 数据库结构
|
||||
|
||||
### machines表结构
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | SERIAL | 主键,自增ID |
|
||||
| machine_id | VARCHAR(255) | 机台唯一标识符 |
|
||||
| protected | BOOLEAN | 保护状态 |
|
||||
| vhd_keyword | VARCHAR(50) | VHD关键词 |
|
||||
| created_at | TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP | 更新时间 |
|
||||
|
||||
### 索引
|
||||
|
||||
- `idx_machines_machine_id`: machine_id字段索引
|
||||
- `idx_machines_protected`: protected字段索引
|
||||
|
||||
## 🔐 安全建议
|
||||
|
||||
1. **修改默认密码**
|
||||
- 生产环境中应修改默认的postgres用户密码
|
||||
|
||||
2. **创建专用用户**
|
||||
```sql
|
||||
CREATE USER vhd_user WITH PASSWORD 'secure_password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE vhd_select TO vhd_user;
|
||||
```
|
||||
|
||||
3. **配置防火墙**
|
||||
- 限制PostgreSQL端口的访问权限
|
||||
|
||||
4. **定期备份**
|
||||
```bash
|
||||
pg_dump -U postgres vhd_select > backup.sql
|
||||
```
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
如果遇到问题,可以:
|
||||
|
||||
1. 查看PostgreSQL日志文件
|
||||
2. 检查VHDSelectServer控制台输出
|
||||
3. 使用pgAdmin 4图形界面管理数据库
|
||||
4. 参考PostgreSQL官方文档:https://www.postgresql.org/docs/
|
||||
+294
-49
@@ -27,37 +27,37 @@
|
||||
|
||||
### 方法1: Docker部署(推荐)
|
||||
|
||||
1. **一键部署脚本(最简单)**
|
||||
```bash
|
||||
# Windows
|
||||
deploy.bat
|
||||
|
||||
# Linux/macOS
|
||||
chmod +x deploy.sh
|
||||
./deploy.sh
|
||||
```
|
||||
#### 选项1: 使用内置数据库(默认)
|
||||
```bash
|
||||
# 使用Docker Compose启动(内置PostgreSQL数据库)
|
||||
docker-compose up -d
|
||||
|
||||
2. **使用Docker Compose**
|
||||
```bash
|
||||
# 构建并启动服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
```
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
3. **使用Docker命令**
|
||||
```bash
|
||||
# 运行容器(配置持久化)
|
||||
docker run -d \
|
||||
--name vhd-select-server \
|
||||
-p 8080:8080 \
|
||||
-v $(pwd)/config:/app/config \
|
||||
lty271104/vhd-select-server:latest
|
||||
```
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
#### 选项2: 使用外部数据库
|
||||
```bash
|
||||
# 使用外部数据库配置文件
|
||||
docker-compose -f docker-compose.external-db.yml up -d
|
||||
|
||||
# 或者手动配置环境变量
|
||||
docker run -d \
|
||||
--name vhd-select-server \
|
||||
-p 8080:8080 \
|
||||
-v $(pwd)/config:/app/config \
|
||||
-v $(pwd)/vhd-data:/app/vhd-data \
|
||||
-e USE_EMBEDDED_DB=false \
|
||||
-e DB_HOST=your-db-host \
|
||||
-e DB_PORT=5432 \
|
||||
-e DB_NAME=vhd_select \
|
||||
-e DB_USER=your-db-user \
|
||||
-e DB_PASSWORD=your-db-password \
|
||||
lty271104/vhd-select-server:latest
|
||||
```
|
||||
|
||||
### 方法2: Node.js版本
|
||||
|
||||
@@ -75,15 +75,6 @@
|
||||
npm start
|
||||
```
|
||||
|
||||
### 方法3: Python版本
|
||||
|
||||
```bash
|
||||
# 直接运行Python服务器
|
||||
python server.py
|
||||
|
||||
# 或指定端口
|
||||
python server.py 8080
|
||||
```
|
||||
|
||||
## 🌐 访问界面
|
||||
|
||||
@@ -94,24 +85,78 @@ python server.py 8080
|
||||
|
||||
## 📡 API接口
|
||||
|
||||
### 获取当前VHD关键词
|
||||
VHD Select Server 提供完整的RESTful API接口,支持机台管理、VHD关键词设置和状态监控。
|
||||
|
||||
### 🔐 认证接口
|
||||
|
||||
#### 登录
|
||||
```http
|
||||
GET /api/boot-image-select
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "admin123"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"BootImageSelected": "SDEZ",
|
||||
"timestamp": "2025-10-19T03:05:33.486615"
|
||||
"message": "登录成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 设置VHD关键词
|
||||
#### 登出
|
||||
```http
|
||||
POST /api/auth/logout
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "已登出"
|
||||
}
|
||||
```
|
||||
|
||||
#### 检查认证状态
|
||||
```http
|
||||
GET /api/auth/check
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"isAuthenticated": true
|
||||
}
|
||||
```
|
||||
|
||||
### 🖥️ VHD关键词接口
|
||||
|
||||
#### 获取当前VHD关键词
|
||||
```http
|
||||
GET /api/boot-image-select?machineId=MACHINE001
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
- `machineId` (必需): 机台唯一标识符
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"BootImageSelected": "SDEZ",
|
||||
"machineId": "MACHINE001",
|
||||
"timestamp": "2025-10-21T11:39:43.809Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 设置全局VHD关键词 🔒
|
||||
```http
|
||||
POST /api/set-vhd
|
||||
Content-Type: application/json
|
||||
Authorization: 需要登录
|
||||
|
||||
{
|
||||
"BootImageSelected": "NEW_KEYWORD"
|
||||
@@ -127,7 +172,113 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
### 服务器状态
|
||||
### 🛡️ 机台保护接口
|
||||
|
||||
#### 获取机台保护状态
|
||||
```http
|
||||
GET /api/protect?machineId=MACHINE001
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
- `machineId` (必需): 机台唯一标识符
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"protected": false,
|
||||
"machineId": "MACHINE001",
|
||||
"timestamp": "2025-10-21T11:39:43.809Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 设置机台保护状态 🔒
|
||||
```http
|
||||
POST /api/protect
|
||||
Content-Type: application/json
|
||||
Authorization: 需要登录
|
||||
|
||||
{
|
||||
"machineId": "MACHINE001",
|
||||
"protected": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"protected": true,
|
||||
"machineId": "MACHINE001",
|
||||
"message": "机台保护状态已更新"
|
||||
}
|
||||
```
|
||||
|
||||
### 🏭 机台管理接口
|
||||
|
||||
#### 获取所有机台信息 🔒
|
||||
```http
|
||||
GET /api/machines
|
||||
Authorization: 需要登录
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"machines": [
|
||||
{
|
||||
"machine_id": "MACHINE001",
|
||||
"protected": false,
|
||||
"vhd_keyword": "SDEZ",
|
||||
"last_seen": "2025-10-21T11:39:43.809Z",
|
||||
"created_at": "2025-10-21T10:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
"timestamp": "2025-10-21T11:39:43.809Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### 设置特定机台的VHD关键词 🔒
|
||||
```http
|
||||
POST /api/machines/{machineId}/vhd
|
||||
Content-Type: application/json
|
||||
Authorization: 需要登录
|
||||
|
||||
{
|
||||
"vhdKeyword": "CUSTOM_VHD"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"machineId": "MACHINE001",
|
||||
"vhdKeyword": "CUSTOM_VHD",
|
||||
"message": "机台VHD关键词已更新"
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除机台 🔒
|
||||
```http
|
||||
DELETE /api/machines/{machineId}
|
||||
Authorization: 需要登录
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"machineId": "MACHINE001",
|
||||
"message": "机台已删除"
|
||||
}
|
||||
```
|
||||
|
||||
### 📊 系统状态接口
|
||||
|
||||
#### 获取服务器状态
|
||||
```http
|
||||
GET /api/status
|
||||
```
|
||||
@@ -138,11 +289,35 @@ GET /api/status
|
||||
"success": true,
|
||||
"status": "running",
|
||||
"BootImageSelected": "SDEZ",
|
||||
"timestamp": "2025-10-19T03:05:33.486615",
|
||||
"version": "1.0.0"
|
||||
"uptime": 30.415774368,
|
||||
"timestamp": "2025-10-21T11:39:43.809Z",
|
||||
"version": "1.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
### 📝 API使用说明
|
||||
|
||||
**认证要求**:
|
||||
- 🔒 标记的接口需要先通过 `/api/auth/login` 登录
|
||||
- 登录后会话有效期为24小时
|
||||
- 默认管理员密码: `admin123`
|
||||
|
||||
**错误响应格式**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "错误描述",
|
||||
"requireAuth": true // 仅在需要认证时出现
|
||||
}
|
||||
```
|
||||
|
||||
**状态码说明**:
|
||||
- `200`: 请求成功
|
||||
- `400`: 请求参数错误
|
||||
- `401`: 未认证或认证失败
|
||||
- `404`: 资源不存在
|
||||
- `500`: 服务器内部错误
|
||||
|
||||
## 🎯 使用说明
|
||||
|
||||
### Web界面操作
|
||||
@@ -160,23 +335,83 @@ GET /api/status
|
||||
|
||||
### Docker相关
|
||||
|
||||
#### 数据库配置选项
|
||||
|
||||
**内置数据库模式(默认)**
|
||||
- 自动在容器内启动PostgreSQL数据库
|
||||
- 数据持久化到Docker卷
|
||||
- 零配置,开箱即用
|
||||
|
||||
**外部数据库模式**
|
||||
- 连接到外部PostgreSQL数据库
|
||||
- 支持云数据库服务
|
||||
- 更好的扩展性和可维护性
|
||||
|
||||
#### 环境变量
|
||||
|
||||
**应用配置**
|
||||
- `PORT`: 服务端口(默认: 8080)
|
||||
- `CONFIG_PATH`: 配置文件目录路径(默认: /app/config)
|
||||
- `NODE_ENV`: 运行环境(默认: production)
|
||||
|
||||
**数据库配置**
|
||||
- `USE_EMBEDDED_DB`: 是否使用内置数据库(默认: true)
|
||||
- `DB_HOST`: 数据库主机地址(默认: localhost)
|
||||
- `DB_PORT`: 数据库端口(默认: 5432)
|
||||
- `DB_NAME`: 数据库名称(默认: vhd_select)
|
||||
- `DB_USER`: 数据库用户名(默认: postgres)
|
||||
- `DB_PASSWORD`: 数据库密码(默认: vhd_select_password)
|
||||
- `DB_MAX_CONNECTIONS`: 最大连接数(默认: 20)
|
||||
- `DB_IDLE_TIMEOUT`: 空闲连接超时时间(默认: 30000ms)
|
||||
- `DB_CONNECTION_TIMEOUT`: 连接超时时间(默认: 5000ms)
|
||||
- `DB_SSL`: 是否启用SSL连接(默认: false)
|
||||
|
||||
#### 数据卷
|
||||
- `/app/config`: 配置文件持久化目录
|
||||
- `/app/data`: 数据文件持久化目录(可选)
|
||||
- `/app/vhd-data`: VHD数据文件持久化目录
|
||||
- `/var/lib/postgresql/data`: 内置数据库数据持久化目录
|
||||
|
||||
#### 健康检查
|
||||
- 端点: `http://localhost:8080/api/status`
|
||||
- 端点: `http://localhost:8080/api/health`
|
||||
- 间隔: 30秒
|
||||
- 超时: 10秒
|
||||
- 重试: 3次
|
||||
- 启动等待时间: 60秒
|
||||
|
||||
#### 配置文件说明
|
||||
|
||||
**docker-compose.yml**: 使用内置数据库的默认配置
|
||||
**docker-compose.external-db.yml**: 使用外部数据库的示例配置
|
||||
|
||||
#### 故障排除
|
||||
如果Docker部署遇到问题,请参考 [Docker故障排除指南](DOCKER_TROUBLESHOOTING.md)
|
||||
|
||||
**内置数据库问题**
|
||||
```bash
|
||||
# 检查数据库初始化日志
|
||||
docker-compose logs vhd-select-server | grep -i postgres
|
||||
|
||||
# 重新初始化数据库
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
**外部数据库连接问题**
|
||||
```bash
|
||||
# 测试数据库连接
|
||||
docker run --rm -it postgres:15-alpine psql -h your-db-host -U your-db-user -d vhd_select
|
||||
|
||||
# 检查网络连接
|
||||
docker-compose exec vhd-select-server ping your-db-host
|
||||
```
|
||||
|
||||
**数据持久化问题**
|
||||
```bash
|
||||
# 检查数据卷
|
||||
docker volume ls | grep vhd
|
||||
|
||||
# 备份数据
|
||||
docker run --rm -v vhd_db_data:/data -v $(pwd):/backup alpine tar czf /backup/vhd_backup.tar.gz /data
|
||||
```
|
||||
|
||||
### 命令行测试
|
||||
|
||||
@@ -246,6 +481,16 @@ python server.py 8081
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v1.2.0
|
||||
- ✅ 完整Docker化支持(内置/外部数据库)
|
||||
- ✅ PostgreSQL数据库集成
|
||||
- ✅ 机台管理功能
|
||||
- ✅ 机台保护状态控制
|
||||
- ✅ 用户认证和会话管理
|
||||
- ✅ 完整的RESTful API文档
|
||||
- ✅ 健康检查和监控
|
||||
- ✅ 数据持久化和备份
|
||||
|
||||
### v1.0.0
|
||||
- ✅ 跨平台支持(Node.js + Python)
|
||||
- ✅ 现代化Web界面
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
// 数据库配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT) || 5432,
|
||||
database: process.env.DB_NAME || 'vhd_select',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'password',
|
||||
max: parseInt(process.env.DB_MAX_CONNECTIONS) || 20, // 最大连接数
|
||||
idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT) || 30000, // 空闲连接超时时间
|
||||
connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT) || 5000, // 连接超时时间
|
||||
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
|
||||
};
|
||||
|
||||
// 创建PostgreSQL连接池
|
||||
const pool = new Pool(dbConfig);
|
||||
|
||||
// 数据库操作类
|
||||
class Database {
|
||||
constructor() {
|
||||
this.pool = pool;
|
||||
this.initializeDatabase();
|
||||
}
|
||||
|
||||
// 初始化数据库连接
|
||||
async initializeDatabase() {
|
||||
const useEmbeddedDb = process.env.USE_EMBEDDED_DB === 'true';
|
||||
console.log(`数据库配置: ${useEmbeddedDb ? '内置数据库' : '外部数据库'}`);
|
||||
console.log(`连接地址: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`);
|
||||
|
||||
try {
|
||||
const client = await this.pool.connect();
|
||||
console.log('PostgreSQL数据库连接成功');
|
||||
|
||||
// 测试查询
|
||||
const result = await client.query('SELECT NOW()');
|
||||
console.log('数据库时间:', result.rows[0].now);
|
||||
|
||||
// 检查表是否存在
|
||||
const tableCheck = await client.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'machines'
|
||||
);
|
||||
`);
|
||||
|
||||
if (tableCheck.rows[0].exists) {
|
||||
console.log('数据库表结构已存在');
|
||||
} else {
|
||||
console.log('警告: machines表不存在,请确保数据库已正确初始化');
|
||||
}
|
||||
|
||||
client.release();
|
||||
this.isConnected = true;
|
||||
} catch (error) {
|
||||
console.error('数据库连接失败:', error.message);
|
||||
console.error('错误详情:', error);
|
||||
|
||||
if (useEmbeddedDb) {
|
||||
console.error('内置数据库连接失败,请检查Docker配置');
|
||||
} else {
|
||||
console.error('外部数据库连接失败,请检查数据库服务器状态和连接配置');
|
||||
}
|
||||
|
||||
this.isConnected = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取机台信息
|
||||
async getMachine(machineId) {
|
||||
try {
|
||||
const client = await this.pool.connect();
|
||||
const result = await client.query(
|
||||
'SELECT * FROM machines WHERE machine_id = $1',
|
||||
[machineId]
|
||||
);
|
||||
client.release();
|
||||
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error('获取机台信息失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建或更新机台信息
|
||||
async upsertMachine(machineId, isProtected = false, vhdKeyword = 'SDEZ') {
|
||||
try {
|
||||
const client = await this.pool.connect();
|
||||
const result = await client.query(`
|
||||
INSERT INTO machines (machine_id, protected, vhd_keyword)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (machine_id)
|
||||
DO UPDATE SET
|
||||
protected = EXCLUDED.protected,
|
||||
vhd_keyword = EXCLUDED.vhd_keyword,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *
|
||||
`, [machineId, isProtected, vhdKeyword]);
|
||||
client.release();
|
||||
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('更新机台信息失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新机台保护状态
|
||||
async updateMachineProtection(machineId, isProtected) {
|
||||
try {
|
||||
const client = await this.pool.connect();
|
||||
const result = await client.query(`
|
||||
UPDATE machines
|
||||
SET protected = $2, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE machine_id = $1
|
||||
RETURNING *
|
||||
`, [machineId, isProtected]);
|
||||
client.release();
|
||||
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error('更新机台保护状态失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新机台VHD关键词
|
||||
async updateMachineVhdKeyword(machineId, vhdKeyword) {
|
||||
try {
|
||||
const client = await this.pool.connect();
|
||||
const result = await client.query(`
|
||||
UPDATE machines
|
||||
SET vhd_keyword = $2, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE machine_id = $1
|
||||
RETURNING *
|
||||
`, [machineId, vhdKeyword]);
|
||||
client.release();
|
||||
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error('更新机台VHD关键词失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有机台信息
|
||||
async getAllMachines() {
|
||||
try {
|
||||
const client = await this.pool.connect();
|
||||
const result = await client.query(
|
||||
'SELECT * FROM machines ORDER BY machine_id'
|
||||
);
|
||||
client.release();
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
console.error('获取所有机台信息失败:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 删除机台
|
||||
async deleteMachine(machineId) {
|
||||
try {
|
||||
const client = await this.pool.connect();
|
||||
const result = await client.query(
|
||||
'DELETE FROM machines WHERE machine_id = $1 RETURNING *',
|
||||
[machineId]
|
||||
);
|
||||
client.release();
|
||||
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error('删除机台失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭数据库连接池
|
||||
async close() {
|
||||
try {
|
||||
await this.pool.end();
|
||||
console.log('数据库连接池已关闭');
|
||||
} catch (error) {
|
||||
console.error('关闭数据库连接池失败:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Database();
|
||||
@@ -1,49 +0,0 @@
|
||||
@echo off
|
||||
echo VHD Select Server Docker 部署脚本
|
||||
echo ================================
|
||||
|
||||
echo 检查Docker是否运行...
|
||||
docker --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: Docker未安装或未运行
|
||||
echo 请先安装并启动Docker Desktop
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo 检查Docker Compose...
|
||||
docker-compose --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: Docker Compose未安装
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo 创建配置目录...
|
||||
if not exist "config" mkdir config
|
||||
|
||||
echo 拉取并启动服务...
|
||||
docker-compose up -d
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo.
|
||||
echo ✅ 部署成功!
|
||||
echo 🌐 Web界面: http://localhost:8080
|
||||
echo 📁 配置文件: ./config/vhd-config.json
|
||||
echo.
|
||||
echo 常用命令:
|
||||
echo 查看日志: docker-compose logs -f
|
||||
echo 停止服务: docker-compose down
|
||||
echo 重启服务: docker-compose restart
|
||||
) else (
|
||||
echo.
|
||||
echo ❌ 部署失败,请检查错误信息
|
||||
echo 如果是网络问题,请尝试:
|
||||
echo 1. 检查网络连接
|
||||
echo 2. 配置Docker镜像源
|
||||
echo 3. 使用本地Node.js部署: npm start
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 按任意键退出...
|
||||
pause >nul
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "VHD Select Server Docker 部署脚本"
|
||||
echo "================================"
|
||||
|
||||
# 检查Docker是否安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ 错误: Docker未安装"
|
||||
echo "请先安装Docker: https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查Docker是否运行
|
||||
if ! docker info &> /dev/null; then
|
||||
echo "❌ 错误: Docker未运行"
|
||||
echo "请启动Docker服务"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Docker检查通过"
|
||||
|
||||
# 检查Docker Compose
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo "❌ 错误: Docker Compose未安装"
|
||||
echo "请安装Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Docker Compose检查通过"
|
||||
|
||||
# 创建配置目录
|
||||
echo "📁 创建配置目录..."
|
||||
mkdir -p config
|
||||
|
||||
# 拉取并启动服务
|
||||
echo "🚀 拉取并启动服务..."
|
||||
if docker-compose up -d; then
|
||||
echo ""
|
||||
echo "✅ 部署成功!"
|
||||
echo "🌐 Web界面: http://localhost:8080"
|
||||
echo "📁 配置文件: ./config/vhd-config.json"
|
||||
echo ""
|
||||
echo "常用命令:"
|
||||
echo " 查看日志: docker-compose logs -f"
|
||||
echo " 停止服务: docker-compose down"
|
||||
echo " 重启服务: docker-compose restart"
|
||||
echo " 查看状态: docker-compose ps"
|
||||
else
|
||||
echo ""
|
||||
echo "❌ 部署失败,请检查错误信息"
|
||||
echo "如果是网络问题,请尝试:"
|
||||
echo " 1. 检查网络连接"
|
||||
echo " 2. 配置Docker镜像源"
|
||||
echo " 3. 使用本地Node.js部署: npm start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 部署完成!"
|
||||
@@ -0,0 +1,55 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
vhd-select-server:
|
||||
image: lty271104/vhd-select-server:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- ./vhd-data:/app/vhd-data
|
||||
environment:
|
||||
# 应用配置
|
||||
- NODE_ENV=production
|
||||
- PORT=8080
|
||||
- CONFIG_PATH=/app/config
|
||||
|
||||
# 数据库配置 - 使用外部数据库
|
||||
- USE_EMBEDDED_DB=false
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=vhd_select
|
||||
- DB_USER=postgres
|
||||
- DB_PASSWORD=vhd_select_password
|
||||
|
||||
# 可选的数据库连接配置
|
||||
- DB_MAX_CONNECTIONS=20
|
||||
- DB_IDLE_TIMEOUT=30000
|
||||
- DB_CONNECTION_TIMEOUT=5000
|
||||
- DB_SSL=false
|
||||
depends_on:
|
||||
- postgres
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
restart: unless-stopped
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
- POSTGRES_DB=vhd_select
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=vhd_select_password
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
|
||||
ports:
|
||||
- "5432:5432"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
@@ -3,26 +3,57 @@ version: '3.8'
|
||||
services:
|
||||
vhd-select-server:
|
||||
image: lty271104/vhd-select-server:latest
|
||||
container_name: vhd-select-server
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8082:8080"
|
||||
volumes:
|
||||
# 配置文件持久化映射
|
||||
- ./config:/app/config
|
||||
# 可选:映射整个数据目录
|
||||
- vhd-data:/app/data
|
||||
- ./vhd-data:/app/vhd-data
|
||||
# 内置数据库数据持久化
|
||||
- vhd_db_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
# 应用配置
|
||||
- NODE_ENV=production
|
||||
- PORT=8080
|
||||
- CONFIG_PATH=/app/config
|
||||
restart: unless-stopped
|
||||
|
||||
# 数据库配置 - 使用内置数据库
|
||||
- USE_EMBEDDED_DB=true
|
||||
- DB_HOST=localhost
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=vhd_select
|
||||
- DB_USER=postgres
|
||||
- DB_PASSWORD=vhd_select_password
|
||||
|
||||
# 可选的数据库连接配置
|
||||
- DB_MAX_CONNECTIONS=20
|
||||
- DB_IDLE_TIMEOUT=30000
|
||||
- DB_CONNECTION_TIMEOUT=5000
|
||||
- DB_SSL=false
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/status"]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
start_period: 60s
|
||||
restart: unless-stopped
|
||||
|
||||
# 外部数据库示例配置(可选)
|
||||
# 如果要使用外部数据库,请取消注释以下配置并修改上面的环境变量
|
||||
# postgres:
|
||||
# image: postgres:15-alpine
|
||||
# environment:
|
||||
# - POSTGRES_DB=vhd_select
|
||||
# - POSTGRES_USER=postgres
|
||||
# - POSTGRES_PASSWORD=vhd_select_password
|
||||
# volumes:
|
||||
# - postgres_data:/var/lib/postgresql/data
|
||||
# - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
# restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
vhd-data:
|
||||
driver: local
|
||||
vhd_db_data:
|
||||
driver: local
|
||||
# postgres_data:
|
||||
# driver: local
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# 日志函数
|
||||
log() {
|
||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# 等待外部数据库连接
|
||||
wait_for_external_db() {
|
||||
log "等待外部数据库连接..."
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" 2>/dev/null; then
|
||||
log "外部数据库连接成功"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "尝试连接外部数据库 ($attempt/$max_attempts)..."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log "错误: 无法连接到外部数据库"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 初始化内置PostgreSQL数据库
|
||||
init_embedded_db() {
|
||||
log "初始化内置PostgreSQL数据库..."
|
||||
|
||||
# 检查数据目录是否已初始化
|
||||
if [ ! -f "$POSTGRES_DATA_DIR/PG_VERSION" ]; then
|
||||
log "初始化PostgreSQL数据目录..."
|
||||
su-exec postgres initdb -D "$POSTGRES_DATA_DIR" --auth-local=trust --auth-host=md5
|
||||
|
||||
# 配置PostgreSQL
|
||||
echo "host all all 0.0.0.0/0 md5" >> "$POSTGRES_DATA_DIR/pg_hba.conf"
|
||||
echo "listen_addresses = '*'" >> "$POSTGRES_DATA_DIR/postgresql.conf"
|
||||
echo "port = $DB_PORT" >> "$POSTGRES_DATA_DIR/postgresql.conf"
|
||||
echo "max_connections = 100" >> "$POSTGRES_DATA_DIR/postgresql.conf"
|
||||
echo "shared_buffers = 128MB" >> "$POSTGRES_DATA_DIR/postgresql.conf"
|
||||
echo "log_statement = 'all'" >> "$POSTGRES_DATA_DIR/postgresql.conf"
|
||||
echo "logging_collector = on" >> "$POSTGRES_DATA_DIR/postgresql.conf"
|
||||
fi
|
||||
|
||||
# 启动PostgreSQL
|
||||
log "启动PostgreSQL服务..."
|
||||
su-exec postgres pg_ctl -D "$POSTGRES_DATA_DIR" -l "$POSTGRES_DATA_DIR/postgresql.log" start
|
||||
|
||||
# 等待PostgreSQL启动
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if su-exec postgres pg_isready -h localhost -p "$DB_PORT" 2>/dev/null; then
|
||||
log "PostgreSQL启动成功"
|
||||
break
|
||||
fi
|
||||
log "等待PostgreSQL启动 ($attempt/$max_attempts)..."
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
if [ $attempt -gt $max_attempts ]; then
|
||||
log "错误: PostgreSQL启动失败"
|
||||
log "PostgreSQL日志内容:"
|
||||
cat "$POSTGRES_DATA_DIR/postgresql.log" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建数据库用户和数据库
|
||||
log "创建数据库用户和数据库..."
|
||||
su-exec postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true
|
||||
su-exec postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" 2>/dev/null || true
|
||||
su-exec postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;" 2>/dev/null || true
|
||||
|
||||
# 初始化数据库表结构
|
||||
log "初始化数据库表结构..."
|
||||
if [ -f "/app/init-db.sql" ]; then
|
||||
# 使用postgres用户通过本地socket连接执行初始化脚本
|
||||
su-exec postgres psql -d "$DB_NAME" -f /app/init-db.sql
|
||||
su-exec postgres psql -d "$DB_NAME" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO $DB_USER;"
|
||||
su-exec postgres psql -d "$DB_NAME" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO $DB_USER;"
|
||||
fi
|
||||
|
||||
log "内置数据库初始化完成"
|
||||
}
|
||||
|
||||
# 初始化外部数据库表结构
|
||||
init_external_db() {
|
||||
log "初始化外部数据库表结构..."
|
||||
|
||||
# 检查是否需要初始化表结构
|
||||
if ! PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1 FROM machines LIMIT 1;" 2>/dev/null; then
|
||||
log "创建数据库表结构..."
|
||||
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f /app/scripts/init-db.sql
|
||||
log "外部数据库表结构初始化完成"
|
||||
else
|
||||
log "外部数据库表结构已存在,跳过初始化"
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动Node.js应用
|
||||
start_app() {
|
||||
log "启动VHD Select Server应用..."
|
||||
cd /app
|
||||
su-exec nodejs npm start
|
||||
}
|
||||
|
||||
# 优雅关闭处理
|
||||
cleanup() {
|
||||
log "接收到关闭信号,正在优雅关闭..."
|
||||
|
||||
# 关闭Node.js应用
|
||||
if [ ! -z "$APP_PID" ]; then
|
||||
kill -TERM "$APP_PID" 2>/dev/null || true
|
||||
wait "$APP_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 如果使用内置数据库,关闭PostgreSQL
|
||||
if [ "$USE_EMBEDDED_DB" = "true" ]; then
|
||||
log "关闭内置PostgreSQL数据库..."
|
||||
su-exec postgres pg_ctl -D "$POSTGRES_DATA_DIR" stop -m fast 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log "应用已关闭"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 设置信号处理
|
||||
trap cleanup SIGTERM SIGINT
|
||||
|
||||
# 主逻辑
|
||||
log "VHD Select Server Docker容器启动"
|
||||
log "USE_EMBEDDED_DB: $USE_EMBEDDED_DB"
|
||||
|
||||
if [ "$USE_EMBEDDED_DB" = "true" ]; then
|
||||
log "使用内置PostgreSQL数据库"
|
||||
init_embedded_db
|
||||
else
|
||||
log "使用外部数据库: $DB_HOST:$DB_PORT/$DB_NAME"
|
||||
wait_for_external_db
|
||||
init_external_db
|
||||
fi
|
||||
|
||||
# 启动应用(在后台运行以便处理信号)
|
||||
start_app &
|
||||
APP_PID=$!
|
||||
|
||||
# 等待应用进程
|
||||
wait "$APP_PID"
|
||||
@@ -0,0 +1,48 @@
|
||||
-- VHD Select Server 数据库初始化脚本
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
-- CREATE DATABASE vhd_select;
|
||||
|
||||
-- 使用数据库
|
||||
-- \c vhd_select;
|
||||
|
||||
-- 创建机台表
|
||||
CREATE TABLE IF NOT EXISTS machines (
|
||||
id SERIAL PRIMARY KEY,
|
||||
machine_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
protected BOOLEAN DEFAULT FALSE,
|
||||
vhd_keyword VARCHAR(255) DEFAULT 'SDEZ',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 创建更新时间触发器函数
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- 为machines表创建更新时间触发器
|
||||
DROP TRIGGER IF EXISTS update_machines_updated_at ON machines;
|
||||
CREATE TRIGGER update_machines_updated_at
|
||||
BEFORE UPDATE ON machines
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 插入一些示例数据
|
||||
INSERT INTO machines (machine_id, protected, vhd_keyword)
|
||||
VALUES
|
||||
('MACHINE001', FALSE, 'SDEZ'),
|
||||
('MACHINE002', FALSE, 'PROD'),
|
||||
('MACHINE003', TRUE, 'TEST')
|
||||
ON CONFLICT (machine_id) DO NOTHING;
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX IF NOT EXISTS idx_machines_machine_id ON machines(machine_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_machines_protected ON machines(protected);
|
||||
|
||||
-- 显示表结构
|
||||
\d machines;
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 内置PostgreSQL数据库初始化脚本
|
||||
# 此脚本在Docker容器启动时运行,用于初始化内置PostgreSQL数据库
|
||||
|
||||
set -e
|
||||
|
||||
# 日志函数
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# 数据库配置
|
||||
DB_NAME="${DB_NAME:-vhd_select}"
|
||||
DB_USER="${DB_USER:-postgres}"
|
||||
DB_PASSWORD="${DB_PASSWORD:-vhd_select_password}"
|
||||
PGDATA="${PGDATA:-/var/lib/postgresql/data}"
|
||||
|
||||
log "开始初始化内置PostgreSQL数据库..."
|
||||
|
||||
# 检查数据目录是否已初始化
|
||||
if [ ! -s "$PGDATA/PG_VERSION" ]; then
|
||||
log "初始化PostgreSQL数据目录..."
|
||||
|
||||
# 初始化数据库
|
||||
su-exec postgres initdb -D "$PGDATA" --auth-local=trust --auth-host=md5
|
||||
|
||||
# 启动PostgreSQL服务
|
||||
log "启动PostgreSQL服务..."
|
||||
su-exec postgres pg_ctl -D "$PGDATA" -l "$PGDATA/postgresql.log" start
|
||||
|
||||
# 等待PostgreSQL启动
|
||||
log "等待PostgreSQL服务启动..."
|
||||
for i in {1..30}; do
|
||||
if su-exec postgres pg_isready -q; then
|
||||
log "PostgreSQL服务已启动"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
log "错误: PostgreSQL服务启动超时"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 创建数据库和用户
|
||||
log "创建数据库和用户..."
|
||||
su-exec postgres psql -v ON_ERROR_STOP=1 <<-EOSQL
|
||||
CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';
|
||||
CREATE DATABASE $DB_NAME OWNER $DB_USER;
|
||||
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
|
||||
EOSQL
|
||||
|
||||
# 执行初始化SQL脚本
|
||||
if [ -f "/app/init-db.sql" ]; then
|
||||
log "执行数据库初始化脚本..."
|
||||
su-exec postgres psql -v ON_ERROR_STOP=1 -d "$DB_NAME" -f "/app/init-db.sql"
|
||||
log "数据库初始化脚本执行完成"
|
||||
else
|
||||
log "警告: 未找到数据库初始化脚本 /app/init-db.sql"
|
||||
fi
|
||||
|
||||
# 停止PostgreSQL服务
|
||||
log "停止PostgreSQL服务..."
|
||||
su-exec postgres pg_ctl -D "$PGDATA" stop
|
||||
|
||||
log "内置PostgreSQL数据库初始化完成"
|
||||
else
|
||||
log "PostgreSQL数据目录已存在,跳过初始化"
|
||||
fi
|
||||
|
||||
log "内置数据库初始化脚本执行完成"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vhd-select-server",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Cross-platform VHD selection server with web GUI",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
@@ -19,7 +19,11 @@
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"body-parser": "^1.20.2"
|
||||
"body-parser": "^1.20.2",
|
||||
"pg": "^8.11.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"express-session": "^1.17.3",
|
||||
"bcryptjs": "^2.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
||||
+336
-15
@@ -3,15 +3,37 @@ const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const session = require('express-session');
|
||||
const bcrypt = require('bcryptjs');
|
||||
require('dotenv').config();
|
||||
|
||||
const database = require('./database');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 8080;
|
||||
|
||||
// 管理员密码配置 (默认密码: admin123)
|
||||
const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH || bcrypt.hashSync('admin123', 10);
|
||||
|
||||
// 会话配置
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'vhd-select-server-secret-key-2024',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // 在生产环境中应该设置为true (需要HTTPS)
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24小时
|
||||
}
|
||||
}));
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
credentials: true // 允许发送cookies
|
||||
}));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// VHD关键词存储
|
||||
let currentVhdKeyword = 'SDEZ'; // 默认值
|
||||
@@ -47,20 +69,129 @@ function saveConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// API路由
|
||||
// 认证中间件
|
||||
function requireAuth(req, res, next) {
|
||||
if (req.session && req.session.isAuthenticated) {
|
||||
return next();
|
||||
} else {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '需要登录',
|
||||
requireAuth: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前VHD关键词
|
||||
app.get('/api/boot-image-select', (req, res) => {
|
||||
console.log(`[${new Date().toISOString()}] API请求: GET /api/boot-image-select`);
|
||||
res.json({
|
||||
success: true,
|
||||
BootImageSelected: currentVhdKeyword,
|
||||
timestamp: new Date().toISOString()
|
||||
// 静态文件服务 (登录页面不需要认证)
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// 认证API路由
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
try {
|
||||
const { password } = req.body;
|
||||
|
||||
console.log(`[${new Date().toISOString()}] 登录尝试`);
|
||||
|
||||
if (!password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请输入密码'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isValidPassword = await bcrypt.compare(password, ADMIN_PASSWORD_HASH);
|
||||
|
||||
if (isValidPassword) {
|
||||
req.session.isAuthenticated = true;
|
||||
console.log(`[${new Date().toISOString()}] 登录成功`);
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登录成功'
|
||||
});
|
||||
} else {
|
||||
console.log(`[${new Date().toISOString()}] 登录失败: 密码错误`);
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: '密码错误'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器错误'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
console.error('登出错误:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '登出失败'
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
message: '已登出'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 设置VHD关键词
|
||||
app.post('/api/set-vhd', (req, res) => {
|
||||
app.get('/api/auth/check', (req, res) => {
|
||||
res.json({
|
||||
isAuthenticated: !!(req.session && req.session.isAuthenticated)
|
||||
});
|
||||
});
|
||||
|
||||
// API路由
|
||||
|
||||
// 获取当前VHD关键词 - 需要Machine ID验证
|
||||
app.get('/api/boot-image-select', async (req, res) => {
|
||||
const machineId = req.query.machineId;
|
||||
|
||||
console.log(`[${new Date().toISOString()}] API请求: GET /api/boot-image-select, Machine ID: ${machineId}`);
|
||||
|
||||
if (!machineId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Machine ID是必需的'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 从数据库获取机台信息
|
||||
let machine = await database.getMachine(machineId);
|
||||
|
||||
// 如果机台不存在,创建新的机台记录
|
||||
if (!machine) {
|
||||
machine = await database.upsertMachine(machineId, false, currentVhdKeyword);
|
||||
console.log(`[${new Date().toISOString()}] 创建新机台: ${machineId}`);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
BootImageSelected: machine ? machine.vhd_keyword : currentVhdKeyword,
|
||||
machineId: machineId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取VHD关键词失败:', error.message);
|
||||
// 降级到内存存储
|
||||
res.json({
|
||||
success: true,
|
||||
BootImageSelected: currentVhdKeyword,
|
||||
machineId: machineId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 设置VHD关键词 (需要认证)
|
||||
app.post('/api/set-vhd', requireAuth, (req, res) => {
|
||||
const { BootImageSelected } = req.body;
|
||||
|
||||
if (!BootImageSelected || typeof BootImageSelected !== 'string') {
|
||||
@@ -91,6 +222,194 @@ app.post('/api/set-vhd', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// 保护状态检查端点 - 需要Machine ID验证
|
||||
app.get('/api/protect', async (req, res) => {
|
||||
const machineId = req.query.machineId;
|
||||
|
||||
console.log(`[${new Date().toISOString()}] API请求: GET /api/protect, Machine ID: ${machineId}`);
|
||||
|
||||
if (!machineId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Machine ID是必需的'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 从数据库获取机台信息
|
||||
let machine = await database.getMachine(machineId);
|
||||
|
||||
// 如果机台不存在,创建新的机台记录
|
||||
if (!machine) {
|
||||
machine = await database.upsertMachine(machineId, false, currentVhdKeyword);
|
||||
console.log(`[${new Date().toISOString()}] 创建新机台: ${machineId}`);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
protected: machine ? machine.protected : false,
|
||||
machineId: machineId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取保护状态失败:', error.message);
|
||||
// 降级到默认状态
|
||||
res.json({
|
||||
success: true,
|
||||
protected: false,
|
||||
machineId: machineId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 设置机台保护状态
|
||||
app.post('/api/protect', requireAuth, async (req, res) => {
|
||||
const { machineId, protected } = req.body;
|
||||
|
||||
console.log(`[${new Date().toISOString()}] API请求: POST /api/protect, Machine ID: ${machineId}, Protected: ${protected}`);
|
||||
|
||||
if (!machineId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Machine ID是必需的'
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof protected !== 'boolean') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'protected状态必须是布尔值'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新机台保护状态
|
||||
const machine = await database.updateMachineProtection(machineId, protected);
|
||||
|
||||
if (!machine) {
|
||||
// 如果机台不存在,创建新的机台记录
|
||||
const newMachine = await database.upsertMachine(machineId, protected, currentVhdKeyword);
|
||||
return res.json({
|
||||
success: true,
|
||||
protected: newMachine.protected,
|
||||
machineId: machineId,
|
||||
message: '机台保护状态已设置'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
protected: machine.protected,
|
||||
machineId: machineId,
|
||||
message: '机台保护状态已更新'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('设置保护状态失败:', error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '设置保护状态失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有机台信息
|
||||
app.get('/api/machines', requireAuth, async (req, res) => {
|
||||
console.log(`[${new Date().toISOString()}] API请求: GET /api/machines`);
|
||||
|
||||
try {
|
||||
const machines = await database.getAllMachines();
|
||||
res.json({
|
||||
success: true,
|
||||
machines: machines,
|
||||
count: machines.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取机台列表失败:', error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取机台列表失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 设置特定机台的VHD关键词
|
||||
app.post('/api/machines/:machineId/vhd', requireAuth, async (req, res) => {
|
||||
const { machineId } = req.params;
|
||||
const { vhdKeyword } = req.body;
|
||||
|
||||
console.log(`[${new Date().toISOString()}] API请求: POST /api/machines/${machineId}/vhd, VHD: ${vhdKeyword}`);
|
||||
|
||||
if (!vhdKeyword || typeof vhdKeyword !== 'string') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'VHD关键词不能为空且必须是字符串'
|
||||
});
|
||||
}
|
||||
|
||||
const trimmedKeyword = vhdKeyword.trim().toUpperCase();
|
||||
if (trimmedKeyword.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'VHD关键词不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新机台VHD关键词
|
||||
let machine = await database.updateMachineVhdKeyword(machineId, trimmedKeyword);
|
||||
|
||||
if (!machine) {
|
||||
// 如果机台不存在,创建新的机台记录
|
||||
machine = await database.upsertMachine(machineId, false, trimmedKeyword);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
machineId: machineId,
|
||||
vhdKeyword: machine.vhd_keyword,
|
||||
message: '机台VHD关键词已更新'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('设置机台VHD关键词失败:', error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '设置机台VHD关键词失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 删除机台
|
||||
app.delete('/api/machines/:machineId', requireAuth, async (req, res) => {
|
||||
const { machineId } = req.params;
|
||||
|
||||
console.log(`[${new Date().toISOString()}] API请求: DELETE /api/machines/${machineId}`);
|
||||
|
||||
try {
|
||||
const deletedMachine = await database.deleteMachine(machineId);
|
||||
|
||||
if (!deletedMachine) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '机台不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
machineId: machineId,
|
||||
message: '机台已删除'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除机台失败:', error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '删除机台失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取服务器状态
|
||||
app.get('/api/status', (req, res) => {
|
||||
res.json({
|
||||
@@ -99,7 +418,7 @@ app.get('/api/status', (req, res) => {
|
||||
BootImageSelected: currentVhdKeyword,
|
||||
uptime: process.uptime(),
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
version: '1.2.0'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,14 +458,16 @@ app.listen(PORT, () => {
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGINT', () => {
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n正在关闭服务器...');
|
||||
saveConfig();
|
||||
await database.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('\n正在关闭服务器...');
|
||||
saveConfig();
|
||||
await database.close();
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -1,245 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
VHD选择服务器 - Python版本
|
||||
跨平台的VHD关键词管理服务器,带有Web GUI界面
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import threading
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
class VHDSelectHandler(BaseHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.config_file = Path(__file__).parent / 'vhd-config.json'
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def load_config(self):
|
||||
"""加载VHD配置"""
|
||||
try:
|
||||
if self.config_file.exists():
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
return config.get('vhdKeyword', 'SDEZ')
|
||||
except Exception as e:
|
||||
print(f"配置加载失败: {e}")
|
||||
return 'SDEZ'
|
||||
|
||||
def save_config(self, vhd_keyword):
|
||||
"""保存VHD配置"""
|
||||
try:
|
||||
config = {'vhdKeyword': vhd_keyword}
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"配置保存失败: {e}")
|
||||
return False
|
||||
|
||||
def send_json_response(self, data, status_code=200):
|
||||
"""发送JSON响应"""
|
||||
self.send_response(status_code)
|
||||
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
self.end_headers()
|
||||
|
||||
json_str = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
self.wfile.write(json_str.encode('utf-8'))
|
||||
|
||||
def send_html_file(self, file_path):
|
||||
"""发送HTML文件"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
||||
self.end_headers()
|
||||
self.wfile.write(content.encode('utf-8'))
|
||||
except FileNotFoundError:
|
||||
self.send_error(404, '文件未找到')
|
||||
except Exception as e:
|
||||
self.send_error(500, f'服务器错误: {e}')
|
||||
|
||||
def do_OPTIONS(self):
|
||||
"""处理CORS预检请求"""
|
||||
self.send_response(200)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
"""处理GET请求"""
|
||||
parsed_path = urlparse(self.path)
|
||||
path = parsed_path.path
|
||||
|
||||
print(f"[{datetime.now().isoformat()}] GET {path}")
|
||||
|
||||
if path == '/':
|
||||
# 主页
|
||||
html_file = Path(__file__).parent / 'public' / 'index.html'
|
||||
self.send_html_file(html_file)
|
||||
|
||||
elif path == '/api/boot-image-select':
|
||||
# 获取当前VHD关键词
|
||||
current_keyword = self.load_config()
|
||||
response = {
|
||||
'success': True,
|
||||
'BootImageSelected': current_keyword,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
self.send_json_response(response)
|
||||
|
||||
elif path == '/api/status':
|
||||
# 服务器状态
|
||||
current_keyword = self.load_config()
|
||||
response = {
|
||||
'success': True,
|
||||
'status': 'running',
|
||||
'BootImageSelected': current_keyword,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'version': '1.0.0 (Python)'
|
||||
}
|
||||
self.send_json_response(response)
|
||||
|
||||
else:
|
||||
# 404错误
|
||||
response = {
|
||||
'success': False,
|
||||
'error': '页面未找到'
|
||||
}
|
||||
self.send_json_response(response, 404)
|
||||
|
||||
def do_POST(self):
|
||||
"""处理POST请求"""
|
||||
parsed_path = urlparse(self.path)
|
||||
path = parsed_path.path
|
||||
|
||||
print(f"[{datetime.now().isoformat()}] POST {path}")
|
||||
|
||||
if path == '/api/set-vhd':
|
||||
try:
|
||||
# 读取请求体
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
post_data = self.rfile.read(content_length)
|
||||
|
||||
# 解析JSON数据
|
||||
data = json.loads(post_data.decode('utf-8'))
|
||||
vhd_keyword = data.get('BootImageSelected', '').strip().upper()
|
||||
|
||||
if not vhd_keyword:
|
||||
response = {
|
||||
'success': False,
|
||||
'error': 'VHD关键词不能为空'
|
||||
}
|
||||
self.send_json_response(response, 400)
|
||||
return
|
||||
|
||||
# 保存配置
|
||||
if self.save_config(vhd_keyword):
|
||||
response = {
|
||||
'success': True,
|
||||
'BootImageSelected': vhd_keyword,
|
||||
'message': 'VHD关键词更新成功'
|
||||
}
|
||||
print(f"VHD关键词已更新为: {vhd_keyword}")
|
||||
self.send_json_response(response)
|
||||
else:
|
||||
response = {
|
||||
'success': False,
|
||||
'error': '配置保存失败'
|
||||
}
|
||||
self.send_json_response(response, 500)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
response = {
|
||||
'success': False,
|
||||
'error': '无效的JSON数据'
|
||||
}
|
||||
self.send_json_response(response, 400)
|
||||
except Exception as e:
|
||||
response = {
|
||||
'success': False,
|
||||
'error': f'服务器错误: {str(e)}'
|
||||
}
|
||||
self.send_json_response(response, 500)
|
||||
else:
|
||||
# 404错误
|
||||
response = {
|
||||
'success': False,
|
||||
'error': '页面未找到'
|
||||
}
|
||||
self.send_json_response(response, 404)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""自定义日志格式"""
|
||||
pass # 禁用默认日志,使用自定义日志
|
||||
|
||||
def start_server(port=8080):
|
||||
"""启动服务器"""
|
||||
server_address = ('', port)
|
||||
httpd = HTTPServer(server_address, VHDSelectHandler)
|
||||
|
||||
print('=' * 50)
|
||||
print('🚀 VHD选择服务器已启动 (Python版本)')
|
||||
print(f'📡 服务器地址: http://localhost:{port}')
|
||||
print(f'🔧 API地址: http://localhost:{port}/api/boot-image-select')
|
||||
print(f'📊 状态页面: http://localhost:{port}/api/status')
|
||||
|
||||
# 加载当前配置
|
||||
config_file = Path(__file__).parent / 'vhd-config.json'
|
||||
try:
|
||||
if config_file.exists():
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
current_keyword = config.get('vhdKeyword', 'SDEZ')
|
||||
else:
|
||||
current_keyword = 'SDEZ'
|
||||
except:
|
||||
current_keyword = 'SDEZ'
|
||||
|
||||
print(f'🎯 当前VHD关键词: {current_keyword}')
|
||||
print('=' * 50)
|
||||
print('💡 提示: 按 Ctrl+C 停止服务器')
|
||||
print()
|
||||
|
||||
# 自动打开浏览器
|
||||
def open_browser():
|
||||
import time
|
||||
time.sleep(1) # 等待服务器启动
|
||||
try:
|
||||
webbrowser.open(f'http://localhost:{port}')
|
||||
except:
|
||||
pass
|
||||
|
||||
browser_thread = threading.Thread(target=open_browser)
|
||||
browser_thread.daemon = True
|
||||
browser_thread.start()
|
||||
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print('\n正在关闭服务器...')
|
||||
httpd.shutdown()
|
||||
print('服务器已关闭')
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 检查命令行参数
|
||||
port = 8080
|
||||
if len(sys.argv) > 1:
|
||||
try:
|
||||
port = int(sys.argv[1])
|
||||
except ValueError:
|
||||
print('错误: 端口号必须是数字')
|
||||
sys.exit(1)
|
||||
|
||||
start_server(port)
|
||||
@@ -0,0 +1,68 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo.
|
||||
echo ========================================
|
||||
echo PostgreSQL Database Setup Script
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM Check if PostgreSQL is installed
|
||||
echo Checking PostgreSQL installation...
|
||||
psql --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: PostgreSQL is not installed or not in PATH
|
||||
echo.
|
||||
echo Please install PostgreSQL first:
|
||||
echo 1. Download from: https://www.postgresql.org/download/windows/
|
||||
echo 2. Run the installer as administrator
|
||||
echo 3. Set superuser password to: password
|
||||
echo 4. Use default port: 5432
|
||||
echo 5. Add PostgreSQL bin directory to PATH
|
||||
echo.
|
||||
echo After installation, run this script again.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo SUCCESS: PostgreSQL is installed
|
||||
echo.
|
||||
|
||||
REM Prompt for PostgreSQL superuser password
|
||||
echo Please enter PostgreSQL superuser password:
|
||||
set /p "PGPASSWORD=Password: "
|
||||
echo.
|
||||
|
||||
REM Set environment variable for password
|
||||
set PGPASSWORD=%PGPASSWORD%
|
||||
|
||||
echo Setting up database...
|
||||
echo.
|
||||
|
||||
REM Execute the SQL setup script
|
||||
psql -U postgres -h localhost -f setup_postgresql.sql
|
||||
|
||||
if %errorlevel% equ 0 (
|
||||
echo.
|
||||
echo SUCCESS: Database setup completed successfully!
|
||||
echo.
|
||||
echo Database Information:
|
||||
echo Host: localhost
|
||||
echo Port: 5432
|
||||
echo Database: vhd_select
|
||||
echo User: postgres
|
||||
echo.
|
||||
echo You can now start the VHDSelectServer:
|
||||
echo node server.js
|
||||
echo.
|
||||
) else (
|
||||
echo.
|
||||
echo ERROR: Database setup failed!
|
||||
echo Please check the error messages above and try again.
|
||||
echo.
|
||||
)
|
||||
|
||||
REM Clear password from environment
|
||||
set PGPASSWORD=
|
||||
|
||||
echo Press any key to exit...
|
||||
pause >nul
|
||||
@@ -0,0 +1,55 @@
|
||||
-- PostgreSQL数据库配置脚本
|
||||
-- 请在安装PostgreSQL后,使用管理员权限运行此脚本
|
||||
|
||||
-- 1. 创建数据库
|
||||
CREATE DATABASE vhd_select
|
||||
WITH
|
||||
OWNER = postgres
|
||||
ENCODING = 'UTF8'
|
||||
LC_COLLATE = 'Chinese (Simplified)_China.936'
|
||||
LC_CTYPE = 'Chinese (Simplified)_China.936'
|
||||
TABLESPACE = pg_default
|
||||
CONNECTION LIMIT = -1;
|
||||
|
||||
-- 2. 连接到新创建的数据库
|
||||
\c vhd_select;
|
||||
|
||||
-- 3. 创建machines表
|
||||
CREATE TABLE IF NOT EXISTS machines (
|
||||
id SERIAL PRIMARY KEY,
|
||||
machine_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
protected BOOLEAN DEFAULT FALSE,
|
||||
vhd_keyword VARCHAR(50) DEFAULT 'SDEZ',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 4. 创建更新时间戳的触发器函数
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- 5. 创建触发器
|
||||
CREATE TRIGGER update_machines_updated_at
|
||||
BEFORE UPDATE ON machines
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 6. 创建索引以提高查询性能
|
||||
CREATE INDEX IF NOT EXISTS idx_machines_machine_id ON machines(machine_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_machines_protected ON machines(protected);
|
||||
|
||||
-- 7. 插入示例数据
|
||||
INSERT INTO machines (machine_id, protected, vhd_keyword) VALUES
|
||||
('MACHINE001', false, 'SDEZ'),
|
||||
('MACHINE002', true, 'SDHC'),
|
||||
('MACHINE003', false, 'SDXC')
|
||||
ON CONFLICT (machine_id) DO NOTHING;
|
||||
|
||||
-- 8. 显示创建结果
|
||||
SELECT 'Database setup completed successfully!' as status;
|
||||
SELECT * FROM machines;
|
||||
+13
-1
@@ -3,4 +3,16 @@
|
||||
EnableRemoteSelection=false
|
||||
|
||||
; 远程VHD选择服务的URL
|
||||
BootImageSelectUrl=http://localhost:8080/api/boot-image-select
|
||||
BootImageSelectUrl=http://localhost:8080/api/boot-image-select
|
||||
|
||||
; 机台ID (用于识别当前机台)
|
||||
MachineId=MACHINE_001
|
||||
|
||||
; 是否启用保护检查功能 (true/false)
|
||||
EnableProtectionCheck=true
|
||||
|
||||
; 保护检查服务的URL
|
||||
ProtectionCheckUrl=http://localhost:8080/api/protect
|
||||
|
||||
; 保护检查间隔 (毫秒)
|
||||
ProtectionCheckInterval=500
|
||||
Reference in New Issue
Block a user