feat: 添加PostgreSQL数据库集成和机台管理功能

- 新增PostgreSQL数据库支持,包含内置和外部数据库两种部署模式
- 实现机台管理功能,包括保护状态控制和VHD关键词设置
- 添加用户认证系统,支持会话管理和权限控制
- 更新Docker配置,支持数据库持久化和健康检查
- 扩展API接口,提供完整的RESTful API文档
- 更新客户端代码以支持机台ID和保护检查功能
- 添加数据库初始化脚本和配置指南
This commit is contained in:
Lannamokia
2025-10-21 20:10:18 +08:00
parent e90e0cf44d
commit 803e6cdaee
20 changed files with 2046 additions and 448 deletions
+298 -4
View File
@@ -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
View File
@@ -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();
}
}
}
+2 -1
View File
@@ -113,4 +113,5 @@ public
# Temporary folders
tmp/
temp/
temp/
vhd-data
+37 -9
View File
@@ -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"]
+213
View File
@@ -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
View File
@@ -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界面
+195
View File
@@ -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();
-49
View File
@@ -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
-59
View File
@@ -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
+41 -10
View File
@@ -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
+153
View File
@@ -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"
+48
View File
@@ -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;
+72
View File
@@ -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 "内置数据库初始化脚本执行完成"
+6 -2
View File
@@ -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
View File
@@ -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);
});
-245
View File
@@ -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)
+68
View File
@@ -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
+55
View File
@@ -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
View File
@@ -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