diff --git a/backend/server.js b/backend/server.js
index befc27a..0b76136 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -2329,6 +2329,115 @@ app.delete('/api/admin/shares/:id', authMiddleware, adminMiddleware, (req, res)
}
});
+// ============================================
+// 管理员:上传工具管理
+// ============================================
+
+// 检查上传工具是否存在
+app.get('/api/admin/check-upload-tool', authMiddleware, adminMiddleware, (req, res) => {
+ try {
+ const toolPath = path.join(__dirname, '..', 'upload-tool', 'dist', '玩玩云上传工具.exe');
+
+ if (fs.existsSync(toolPath)) {
+ const stats = fs.statSync(toolPath);
+ const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
+
+ res.json({
+ success: true,
+ exists: true,
+ fileInfo: {
+ path: toolPath,
+ size: stats.size,
+ sizeMB: sizeMB,
+ modifiedAt: stats.mtime
+ }
+ });
+ } else {
+ res.json({
+ success: true,
+ exists: false,
+ message: '上传工具不存在'
+ });
+ }
+ } catch (error) {
+ console.error('检查上传工具失败:', error);
+ res.status(500).json({
+ success: false,
+ message: '检查失败: ' + error.message
+ });
+ }
+});
+
+// 上传工具文件
+const uploadToolStorage = multer.diskStorage({
+ destination: (req, file, cb) => {
+ const uploadDir = path.join(__dirname, '..', 'upload-tool', 'dist');
+ // 确保目录存在
+ if (!fs.existsSync(uploadDir)) {
+ fs.mkdirSync(uploadDir, { recursive: true });
+ }
+ cb(null, uploadDir);
+ },
+ filename: (req, file, cb) => {
+ // 固定文件名
+ cb(null, '玩玩云上传工具.exe');
+ }
+});
+
+const uploadTool = multer({
+ storage: uploadToolStorage,
+ limits: {
+ fileSize: 100 * 1024 * 1024 // 限制100MB
+ },
+ fileFilter: (req, file, cb) => {
+ // 只允许.exe文件
+ if (!file.originalname.toLowerCase().endsWith('.exe')) {
+ return cb(new Error('只允许上传.exe文件'));
+ }
+ cb(null, true);
+ }
+});
+
+app.post('/api/admin/upload-tool', authMiddleware, adminMiddleware, uploadTool.single('file'), (req, res) => {
+ try {
+ if (!req.file) {
+ return res.status(400).json({
+ success: false,
+ message: '请选择要上传的文件'
+ });
+ }
+
+ const fileSizeMB = (req.file.size / (1024 * 1024)).toFixed(2);
+
+ // 验证文件大小(至少20MB,上传工具通常很大)
+ if (req.file.size < 20 * 1024 * 1024) {
+ // 删除上传的文件
+ fs.unlinkSync(req.file.path);
+ return res.status(400).json({
+ success: false,
+ message: '文件大小异常,上传工具通常大于20MB'
+ });
+ }
+
+ console.log(`[上传工具] 管理员上传成功: ${fileSizeMB}MB`);
+
+ res.json({
+ success: true,
+ message: '上传工具已上传',
+ fileInfo: {
+ size: req.file.size,
+ sizeMB: fileSizeMB
+ }
+ });
+ } catch (error) {
+ console.error('上传工具失败:', error);
+ res.status(500).json({
+ success: false,
+ message: '上传失败: ' + error.message
+ });
+ }
+});
+
// 分享页面访问路由
app.get("/s/:code", (req, res) => {
const shareCode = req.params.code;
diff --git a/frontend/app.html b/frontend/app.html
index 3d163c7..b110986 100644
--- a/frontend/app.html
+++ b/frontend/app.html
@@ -1005,7 +1005,7 @@
-
+
SFTP配置
请配置SFTP服务器
@@ -1381,6 +1381,73 @@
+
+
+
+
+ 上传工具管理
+
+
+
+
+
+
+
+
+ 上传工具已存在
+
+
+ 文件大小: {{ uploadToolStatus.fileInfo.sizeMB }} MB
+
+
+ 最后修改: {{ formatDate(uploadToolStatus.fileInfo.modifiedAt) }}
+
+
+
+
+
+
+
+
+ 上传工具不存在
+
+
+ 普通用户将无法下载上传工具,请上传工具文件
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
说明:
+
+ - 上传工具文件应为 .exe 格式,大小通常在 20-50 MB
+ - 上传后,普通用户可以在设置页面下载该工具
+ - 如果安装脚本下载失败,可以在这里手动上传
+
+
+
+
diff --git a/frontend/app.js b/frontend/app.js
index 6a46f76..a41a548 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -171,7 +171,15 @@ createApp({
},
// 定期检查用户配置更新的定时器
- profileCheckInterval: null
+ profileCheckInterval: null,
+
+ // 上传工具管理
+ uploadToolStatus: null, // 上传工具状态 { exists, fileInfo: { size, sizeMB, modifiedAt } }
+ checkingUploadTool: false, // 是否正在检测上传工具
+ uploadingTool: false, // 是否正在上传工具
+
+ // 强制显示SFTP配置(用于本地存储模式下临时显示SFTP配置)
+ forceSftpConfigVisible: false
};
},
@@ -293,7 +301,21 @@ handleDragLeave(e) {
this.localQuota = this.user.local_storage_quota || 0;
this.localUsed = this.user.local_storage_used || 0;
- console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType);
+ console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'SFTP配置:', this.user.has_ftp_config);
+
+ // 智能存储类型修正:如果当前是SFTP但未配置,且用户有本地存储权限,自动切换到本地
+ if (this.storageType === 'sftp' && !this.user.has_ftp_config) {
+ if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') {
+ console.log('[登录] SFTP未配置但用户有本地存储权限,自动切换到本地存储');
+ this.storageType = 'local';
+ // 异步更新到后端(不等待,避免阻塞登录流程)
+ axios.post(
+ `${this.apiBase}/api/user/switch-storage`,
+ { storage_type: 'local' },
+ { headers: { Authorization: `Bearer ${this.token}` } }
+ ).catch(err => console.error('[登录] 自动切换存储类型失败:', err));
+ }
+ }
// 启动定期检查用户配置
this.startProfileSync();
@@ -369,6 +391,28 @@ handleDragLeave(e) {
alert('SFTP配置已保存!');
// 更新用户信息
this.user.has_ftp_config = 1;
+
+ // 如果用户有 user_choice 权限,自动切换到 SFTP 存储
+ if (this.storagePermission === 'user_choice' || this.storagePermission === 'sftp_only') {
+ try {
+ const switchResponse = await axios.post(
+ `${this.apiBase}/api/user/switch-storage`,
+ { storage_type: 'sftp' },
+ { headers: { Authorization: `Bearer ${this.token}` } }
+ );
+
+ if (switchResponse.data.success) {
+ this.storageType = 'sftp';
+ console.log('[SFTP配置] 已自动切换到SFTP存储模式');
+ }
+ } catch (err) {
+ console.error('[SFTP配置] 自动切换存储模式失败:', err);
+ }
+ }
+
+ // 重置强制显示标志
+ this.forceSftpConfigVisible = false;
+
// 刷新到文件页面
this.currentView = 'files';
this.loadFiles('/');
@@ -1511,7 +1555,6 @@ handleDragLeave(e) {
console.error('加载用户资料失败:', error);
}
},
-
// 启动定期检查用户配置
startProfileSync() {
// 清除已有的定时器
@@ -1538,12 +1581,28 @@ handleDragLeave(e) {
}
},
- // 用户切换存储方式
+ // 用户切换存储方式
async switchStorage(type) {
// 检查是否尝试切换到SFTP但未配置
if (type === 'sftp' && !this.user.has_ftp_config) {
- this.showToast('warning', '提示', '请先在设置页面配置SFTP信息');
- this.currentView = 'settings';
+ const goToSettings = confirm('您还未配置SFTP服务器。\n\n是否现在前往设置页面进行配置?配置完成后将自动切换到SFTP存储。');
+ if (goToSettings) {
+ // 直接设置视图并加载配置,避免switchView的重复检查
+ this.currentView = 'settings';
+ // 强制显示SFTP配置区域
+ this.forceSftpConfigVisible = true;
+ // 如果是普通用户,手动加载SFTP配置
+ if (this.user && !this.user.is_admin) {
+ this.loadFtpConfig();
+ }
+ // 等待DOM更新后滚动到SFTP配置区域
+ this.$nextTick(() => {
+ const sftpSection = document.getElementById('sftp-config-section');
+ if (sftpSection) {
+ sftpSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ });
+ }
return;
}
@@ -1582,6 +1641,11 @@ handleDragLeave(e) {
this.currentView = view;
+ // 离开settings页面时,重置强制显示SFTP配置的标志
+ if (this.currentView !== 'settings') {
+ this.forceSftpConfigVisible = false;
+ }
+
// 根据视图类型自动加载对应数据
switch (view) {
case 'files':
@@ -1762,7 +1826,92 @@ handleDragLeave(e) {
this.showToast('error', '错误', '更新系统设置失败');
}
}
- },
+,
+
+ // ===== 上传工具管理 =====
+
+ // 检测上传工具是否存在
+ async checkUploadTool() {
+ this.checkingUploadTool = true;
+ try {
+ const response = await axios.get(
+ `${this.apiBase}/api/admin/check-upload-tool`,
+ { headers: { Authorization: `Bearer ${this.token}` } }
+ );
+
+ if (response.data.success) {
+ this.uploadToolStatus = response.data;
+ if (response.data.exists) {
+ this.showToast('success', '检测完成', '上传工具文件存在');
+ } else {
+ this.showToast('warning', '提示', '上传工具文件不存在,请上传');
+ }
+ }
+ } catch (error) {
+ console.error('检测上传工具失败:', error);
+ this.showToast('error', '错误', '检测失败: ' + (error.response?.data?.message || error.message));
+ } finally {
+ this.checkingUploadTool = false;
+ }
+ },
+
+ // 处理上传工具文件
+ async handleUploadToolFile(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ // 验证文件类型
+ if (!file.name.toLowerCase().endsWith('.exe')) {
+ this.showToast('error', '错误', '只能上传 .exe 文件');
+ event.target.value = '';
+ return;
+ }
+
+ // 验证文件大小(至少20MB)
+ const minSizeMB = 20;
+ const fileSizeMB = file.size / (1024 * 1024);
+ if (fileSizeMB < minSizeMB) {
+ this.showToast('error', '错误', `文件大小过小(${fileSizeMB.toFixed(2)}MB),上传工具通常大于${minSizeMB}MB`);
+ event.target.value = '';
+ return;
+ }
+
+ // 确认上传
+ if (!confirm(`确定要上传 ${file.name} (${fileSizeMB.toFixed(2)}MB) 吗?`)) {
+ event.target.value = '';
+ return;
+ }
+
+ this.uploadingTool = true;
+
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await axios.post(
+ `${this.apiBase}/api/admin/upload-tool`,
+ formData,
+ {
+ headers: {
+ Authorization: `Bearer ${this.token}`,
+ 'Content-Type': 'multipart/form-data'
+ }
+ }
+ );
+
+ if (response.data.success) {
+ this.showToast('success', '成功', '上传工具已上传');
+ // 重新检测
+ await this.checkUploadTool();
+ }
+ } catch (error) {
+ console.error('上传工具失败:', error);
+ this.showToast('error', '错误', error.response?.data?.message || '上传失败');
+ } finally {
+ this.uploadingTool = false;
+ event.target.value = ''; // 清空input,允许重复上传
+ }
+ } },
mounted() {
// 阻止全局拖拽默认行为(防止拖到区域外打开新页面)
diff --git a/install.sh b/install.sh
index 5a9a0ee..867e06b 100644
--- a/install.sh
+++ b/install.sh
@@ -16,6 +16,8 @@ elif [[ "$1" == "--update" ]] || [[ "$1" == "--upgrade" ]] || [[ "$1" == "update
MODE="update"
elif [[ "$1" == "--repair" ]] || [[ "$1" == "--fix" ]] || [[ "$1" == "repair" ]]; then
MODE="repair"
+elif [[ "$1" == "--ssl" ]] || [[ "$1" == "--cert" ]] || [[ "$1" == "ssl" ]]; then
+ MODE="ssl"
fi
# 颜色定义
@@ -648,9 +650,10 @@ install_pm2() {
}
################################################################################
-# 端口检测和配置
+# 智能端口检测和配置
################################################################################
+# 检查端口是否可用(保留用于兼容性)
check_port_available() {
local port=$1
if command -v netstat &> /dev/null; then
@@ -665,22 +668,188 @@ check_port_available() {
return 0 # 端口可用
}
+# 智能检测端口状态和占用进程
+check_port_status() {
+ local port=$1
+
+ # 1. 检查端口是否被监听
+ if command -v netstat &> /dev/null; then
+ if ! netstat -tuln 2>/dev/null | grep -q ":${port} "; then
+ echo "available"
+ return 0
+ fi
+ elif command -v ss &> /dev/null; then
+ if ! ss -tuln 2>/dev/null | grep -q ":${port} "; then
+ echo "available"
+ return 0
+ fi
+ else
+ echo "available"
+ return 0
+ fi
+
+ # 2. 端口被占用,检查是什么进程
+ local process=""
+
+ if command -v netstat &> /dev/null; then
+ process=$(netstat -tulnp 2>/dev/null | grep ":${port} " | awk '{print $7}' | cut -d'/' -f2 | head -1)
+ fi
+
+ if [[ -z "$process" ]] && command -v ss &> /dev/null; then
+ # 使用sed替代grep -oP以提高兼容性
+ process=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*users:(("\([^"]*\)".*/\1/p' | head -1)
+ fi
+
+ # 3. 根据进程返回状态(始终返回0以避免set -e导致脚本退出)
+ if [[ -z "$process" ]]; then
+ # 无法获取进程名(可能权限不足)
+ echo "occupied"
+ elif [[ "$process" == "nginx" ]] || [[ "$process" =~ ^nginx: ]]; then
+ # Nginx占用
+ echo "nginx"
+ elif [[ "$process" == "apache2" ]] || [[ "$process" == "httpd" ]] || [[ "$process" =~ apache ]]; then
+ # Apache占用
+ echo "apache"
+ else
+ # 其他进程
+ echo "other:$process"
+ fi
+
+ # 始终返回0,避免set -e导致脚本退出
+ return 0
+}
+
+# 改进的端口配置函数
configure_ports() {
- print_step "端口配置"
+ print_step "智能端口配置"
echo ""
- # 检测80端口
- if ! check_port_available 80; then
- print_warning "检测到 80 端口已被占用"
- echo ""
- echo "80端口被其他服务占用,您可以:"
- echo -e "${GREEN}[1]${NC} 使用其他HTTP端口 (例如: 8080, 8888)"
- echo -e "${GREEN}[2]${NC} 停止占用80端口的服务"
- echo ""
+ # 全局标志:是否共用Nginx端口
+ SHARE_NGINX=false
- read -p "请选择 [1-2]: " port_choice < /dev/tty
+ # ========== 检测80端口 ==========
+ port_80_status=$(check_port_status 80)
+
+ case $port_80_status in
+ "available")
+ print_success "80 端口可用"
+ HTTP_PORT=80
+ ;;
+
+ "nginx")
+ print_info "检测到 Nginx 已占用 80 端口"
+ echo ""
+ echo "🎯 好消息:可以通过虚拟主机配置与现有Nginx共用此端口!"
+ echo ""
+ echo "请选择部署方式:"
+ echo ""
+ echo -e "${GREEN}[1]${NC} 共用80端口(推荐)"
+ echo " ✅ 需要配置不同的域名"
+ echo " ✅ 访问: http://your-domain.com"
+ echo " ✅ 不需要端口号"
+ echo ""
+ echo -e "${GREEN}[2]${NC} 使用其他HTTP端口"
+ echo " ℹ️ 独立端口"
+ echo " ℹ️ 访问: http://your-domain.com:8080"
+ echo ""
+
+ while true; do
+ read -p "请选择 [1-2]: " choice < /dev/tty
+
+ if [[ "$choice" == "1" ]]; then
+ HTTP_PORT=80
+ SHARE_NGINX=true
+ print_success "将与现有Nginx共用80端口(虚拟主机模式)"
+ print_info "提示: 请确保使用不同的域名区分站点"
+ break
+ elif [[ "$choice" == "2" ]]; then
+ # 选择其他端口的逻辑
+ while true; do
+ read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty
+ custom_port=${custom_port:-8080}
+
+ if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then
+ print_error "端口范围: 1024-65535"
+ continue
+ fi
+
+ if ! check_port_available $custom_port; then
+ print_error "端口 $custom_port 已被占用,请选择其他端口"
+ continue
+ fi
+
+ HTTP_PORT=$custom_port
+ print_success "将使用 HTTP 端口: $HTTP_PORT"
+ break
+ done
+ break
+ else
+ print_error "无效选项,请重新选择"
+ fi
+ done
+ ;;
+
+ "apache")
+ print_warning "检测到 Apache 已占用 80 端口"
+ echo ""
+ echo "⚠️ Apache和Nginx不能同时监听同一端口"
+ echo ""
+ echo "请选择解决方案:"
+ echo ""
+ echo -e "${GREEN}[1]${NC} 停止Apache,改用Nginx"
+ echo " ⚠️ 需要迁移Apache配置"
+ echo ""
+ echo -e "${GREEN}[2]${NC} 使用其他HTTP端口(推荐)"
+ echo " ✅ 不影响现有Apache服务"
+ echo ""
+
+ while true; do
+ read -p "请选择 [1-2]: " choice < /dev/tty
+
+ if [[ "$choice" == "1" ]]; then
+ print_info "正在停止Apache..."
+ systemctl stop apache2 2>/dev/null || systemctl stop httpd 2>/dev/null || true
+ systemctl disable apache2 2>/dev/null || systemctl disable httpd 2>/dev/null || true
+ HTTP_PORT=80
+ print_success "Apache已停止,将使用80端口"
+ break
+ elif [[ "$choice" == "2" ]]; then
+ # 选择其他端口
+ while true; do
+ read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty
+ custom_port=${custom_port:-8080}
+
+ if [[ ! "$custom_port" =~ ^[0-9]+$ ]] || [[ $custom_port -lt 1024 ]] || [[ $custom_port -gt 65535 ]]; then
+ print_error "端口范围: 1024-65535"
+ continue
+ fi
+
+ if ! check_port_available $custom_port; then
+ print_error "端口 $custom_port 已被占用,请选择其他端口"
+ continue
+ fi
+
+ HTTP_PORT=$custom_port
+ print_success "将使用 HTTP 端口: $HTTP_PORT"
+ break
+ done
+ break
+ else
+ print_error "无效选项,请重新选择"
+ fi
+ done
+ ;;
+
+ "occupied"|other:*)
+ process=${port_80_status#other:}
+ if [[ "$port_80_status" == "occupied" ]]; then
+ print_warning "80 端口已被占用(无法识别进程)"
+ else
+ print_warning "80 端口被进程 ${process} 占用"
+ fi
+ echo ""
+ echo "请选择其他HTTP端口"
- if [[ "$port_choice" == "1" ]]; then
while true; do
read -p "请输入HTTP端口 [建议: 8080]: " custom_port < /dev/tty
custom_port=${custom_port:-8080}
@@ -699,47 +868,113 @@ configure_ports() {
print_success "将使用 HTTP 端口: $HTTP_PORT"
break
done
- else
- print_info "请手动停止占用80端口的服务后重新运行此脚本"
- echo ""
- print_info "查看端口占用: netstat -tunlp | grep :80"
- print_info "或者: ss -tunlp | grep :80"
- exit 1
- fi
- else
- print_success "80 端口可用"
- fi
+ ;;
+ esac
- # 检测443端口(仅在使用HTTPS时需要)
+ echo ""
+
+ # ========== 检测443端口(仅在使用HTTPS时需要)==========
if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then
- if ! check_port_available 443; then
- print_warning "检测到 443 端口已被占用"
- echo ""
+ port_443_status=$(check_port_status 443)
- while true; do
- read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty
- custom_https_port=${custom_https_port:-8443}
+ case $port_443_status in
+ "available")
+ print_success "443 端口可用"
+ HTTPS_PORT=443
+ ;;
- if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then
- print_error "端口范围: 1024-65535"
- continue
+ "nginx")
+ print_info "检测到 Nginx 已占用 443 端口"
+ echo ""
+
+ if [[ "$SHARE_NGINX" == "true" ]]; then
+ # 如果HTTP端口也是共用的,默认继续共用
+ echo "🎯 将继续与现有Nginx共用443端口(虚拟主机模式)"
+ HTTPS_PORT=443
+ print_success "将与现有Nginx共用443端口"
+ else
+ echo "请选择部署方式:"
+ echo ""
+ echo -e "${GREEN}[1]${NC} 共用443端口"
+ echo " ✅ 需要配置不同的域名"
+ echo ""
+ echo -e "${GREEN}[2]${NC} 使用其他HTTPS端口"
+ echo " ℹ️ 独立端口(如8443)"
+ echo ""
+
+ while true; do
+ read -p "请选择 [1-2]: " choice < /dev/tty
+
+ if [[ "$choice" == "1" ]]; then
+ HTTPS_PORT=443
+ SHARE_NGINX=true
+ print_success "将与现有Nginx共用443端口"
+ break
+ elif [[ "$choice" == "2" ]]; then
+ # 选择其他端口
+ while true; do
+ read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty
+ custom_https_port=${custom_https_port:-8443}
+
+ if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then
+ print_error "端口范围: 1024-65535"
+ continue
+ fi
+
+ if ! check_port_available $custom_https_port; then
+ print_error "端口 $custom_https_port 已被占用,请选择其他端口"
+ continue
+ fi
+
+ HTTPS_PORT=$custom_https_port
+ print_success "将使用 HTTPS 端口: $HTTPS_PORT"
+ break
+ done
+ break
+ else
+ print_error "无效选项,请重新选择"
+ fi
+ done
fi
+ ;;
- if ! check_port_available $custom_https_port; then
- print_error "端口 $custom_https_port 已被占用,请选择其他端口"
- continue
+ "apache"|"occupied"|other:*)
+ # Apache或其他进程占用443,需要换端口
+ if [[ "$port_443_status" == "apache" ]]; then
+ print_warning "检测到 Apache 已占用 443 端口"
+ elif [[ "$port_443_status" == "occupied" ]]; then
+ print_warning "443 端口已被占用"
+ else
+ process=${port_443_status#other:}
+ print_warning "443 端口被进程 ${process} 占用"
fi
+ echo ""
- HTTPS_PORT=$custom_https_port
- print_success "将使用 HTTPS 端口: $HTTPS_PORT"
- break
- done
- else
- print_success "443 端口可用"
- fi
+ while true; do
+ read -p "请输入HTTPS端口 [建议: 8443]: " custom_https_port < /dev/tty
+ custom_https_port=${custom_https_port:-8443}
+
+ if [[ ! "$custom_https_port" =~ ^[0-9]+$ ]] || [[ $custom_https_port -lt 1024 ]] || [[ $custom_https_port -gt 65535 ]]; then
+ print_error "端口范围: 1024-65535"
+ continue
+ fi
+
+ if ! check_port_available $custom_https_port; then
+ print_error "端口 $custom_https_port 已被占用,请选择其他端口"
+ continue
+ fi
+
+ HTTPS_PORT=$custom_https_port
+ print_success "将使用 HTTPS 端口: $HTTPS_PORT"
+ break
+ done
+ ;;
+ esac
+
+ echo ""
fi
- # 检测后端端口
+ # ========== 检测后端端口 ==========
if ! check_port_available 40001; then
print_warning "检测到 40001 端口已被占用"
echo ""
@@ -773,6 +1008,9 @@ configure_ports() {
echo " - HTTPS端口: $HTTPS_PORT"
fi
echo " - 后端端口: $BACKEND_PORT"
+ if [[ "$SHARE_NGINX" == "true" ]]; then
+ echo " - 模式: 虚拟主机共用端口 ✅"
+ fi
echo ""
}
@@ -851,36 +1089,112 @@ configure_domain() {
# SSL证书配置
################################################################################
+# 配置acme.sh自动续期
+setup_acme_auto_renew() {
+ echo ""
+ print_step "配置SSL证书自动续期..."
+
+ # acme.sh安装时会自动创建cron任务,这里验证并确保其正常工作
+
+ # 1. 检查cron服务是否运行
+ if systemctl is-active --quiet cron 2>/dev/null || systemctl is-active --quiet crond 2>/dev/null; then
+ print_success "Cron服务运行正常"
+ else
+ print_warning "Cron服务未运行,正在启动..."
+ systemctl start cron 2>/dev/null || systemctl start crond 2>/dev/null || true
+ systemctl enable cron 2>/dev/null || systemctl enable crond 2>/dev/null || true
+ fi
+
+ # 2. 检查acme.sh cron任务
+ if crontab -l 2>/dev/null | grep -q "acme.sh.*--cron"; then
+ print_success "acme.sh自动续期任务已配置"
+ else
+ print_warning "未检测到acme.sh cron任务,正在添加..."
+ # acme.sh会自动安装cron,这里手动触发一次
+ ~/.acme.sh/acme.sh --install-cronjob 2>/dev/null || true
+ fi
+
+ # 3. 显示续期信息
+ echo ""
+ print_info "SSL证书自动续期已配置:"
+ echo " - 检查频率: 每天自动检查"
+ echo " - 续期时机: 证书到期前30天自动续期"
+ echo " - 续期后操作: 自动重载Nginx"
+ echo ""
+
+ # 显示下次续期时间
+ if [[ -f ~/.acme.sh/${DOMAIN}/${DOMAIN}.conf ]]; then
+ NEXT_RENEW=$(grep "Le_NextRenewTime=" ~/.acme.sh/${DOMAIN}/${DOMAIN}.conf 2>/dev/null | cut -d'=' -f2)
+ if [[ -n "$NEXT_RENEW" ]]; then
+ RENEW_DATE=$(date -d "@${NEXT_RENEW}" "+%Y年%m月%d日 %H:%M:%S" 2>/dev/null || date -r ${NEXT_RENEW} "+%Y年%m月%d日 %H:%M:%S" 2>/dev/null || echo "未知")
+ print_info "预计续期时间: ${RENEW_DATE}"
+ fi
+ fi
+
+ # 4. 测试续期命令(不实际续期,只检查)
+ print_info "验证续期配置..."
+ if ~/.acme.sh/acme.sh --list 2>/dev/null | grep -q "$DOMAIN"; then
+ print_success "证书续期配置验证通过"
+ else
+ print_warning "证书列表中未找到域名,续期可能需要手动配置"
+ fi
+
+ echo ""
+}
+
choose_ssl_method() {
echo ""
print_step "选择SSL证书部署方式"
echo ""
echo -e "${YELLOW}【推荐方案】${NC}"
- echo -e "${GREEN}[1]${NC} Certbot (Let's Encrypt官方工具)"
- echo " - 最稳定可靠,支持自动续期"
+ echo -e "${GREEN}[1]${NC} acme.sh + Let's Encrypt"
+ echo " - 纯Shell脚本,轻量级稳定"
+ echo " - 自动续期,无需手动操作"
echo ""
echo -e "${YELLOW}【备选方案】${NC}"
- echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt"
- echo " - 纯Shell脚本,更轻量级"
- echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL"
+ echo -e "${GREEN}[2]${NC} acme.sh + ZeroSSL"
echo " - Let's Encrypt的免费替代品"
- echo -e "${GREEN}[4]${NC} acme.sh + Buypass"
+ echo -e "${GREEN}[3]${NC} acme.sh + Buypass"
echo " - 挪威免费CA,有效期180天"
echo ""
echo -e "${YELLOW}【云服务商证书】${NC}"
- echo -e "${GREEN}[5]${NC} 阿里云免费证书 (需提供AccessKey)"
- echo -e "${GREEN}[6]${NC} 腾讯云免费证书 (需提供SecretKey)"
+ echo -e "${GREEN}[4]${NC} 阿里云免费证书 (需提供AccessKey)"
+ echo -e "${GREEN}[5]${NC} 腾讯云免费证书 (需提供SecretKey)"
echo ""
echo -e "${YELLOW}【其他选项】${NC}"
- echo -e "${GREEN}[7]${NC} 使用已有证书 (手动上传)"
- echo -e "${GREEN}[8]${NC} 暂不配置HTTPS (可后续配置)"
+ echo -e "${GREEN}[6]${NC} 使用已有证书 (手动上传)"
+ echo -e "${GREEN}[7]${NC} 暂不配置HTTPS (可后续配置)"
echo ""
while true; do
- read -p "请输入选项 [1-8]: " ssl_choice < /dev/tty
+ read -p "请输入选项 [1-7]: " ssl_choice < /dev/tty
case $ssl_choice in
- 1|2|3|4|5|6|7|8)
- SSL_METHOD=$ssl_choice
+ 1)
+ SSL_METHOD="2" # acme.sh + Let's Encrypt
+ break
+ ;;
+ 2)
+ SSL_METHOD="3" # acme.sh + ZeroSSL
+ break
+ ;;
+ 3)
+ SSL_METHOD="5" # acme.sh + Buypass
+ break
+ ;;
+ 4)
+ SSL_METHOD="4" # 阿里云
+ break
+ ;;
+ 5)
+ SSL_METHOD="6" # 腾讯云
+ break
+ ;;
+ 6)
+ SSL_METHOD="7" # 手动上传
+ break
+ ;;
+ 7)
+ SSL_METHOD="8" # 不配置HTTPS
break
;;
*)
@@ -897,23 +1211,20 @@ deploy_ssl() {
fi
case $SSL_METHOD in
- 1)
- deploy_certbot || ssl_fallback
- ;;
2)
- deploy_acme_letsencrypt || ssl_fallback
+ deploy_acme_letsencrypt || ssl_fallback "2"
;;
3)
- deploy_acme_zerossl || ssl_fallback
+ deploy_acme_zerossl || ssl_fallback "3"
;;
4)
- deploy_acme_buypass || ssl_fallback
+ deploy_aliyun_ssl || ssl_fallback "4"
;;
5)
- deploy_aliyun_ssl || ssl_fallback
+ deploy_acme_buypass || ssl_fallback "5"
;;
6)
- deploy_tencent_ssl || ssl_fallback
+ deploy_tencent_ssl || ssl_fallback "6"
;;
7)
deploy_manual_ssl
@@ -926,35 +1237,73 @@ deploy_ssl() {
}
ssl_fallback() {
+ local failed_method=$1 # 接收失败的方案编号
+
print_error "SSL证书部署失败"
echo ""
print_warning "建议尝试备选方案:"
- echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt (推荐)"
- echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL"
- echo -e "${GREEN}[4]${NC} acme.sh + Buypass"
+ echo ""
+
+ # 动态显示可用选项(排除已失败的)
+ local available_options=()
+
+ # 方案2: acme.sh + Let's Encrypt
+ if [[ "$failed_method" != "2" ]]; then
+ echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt"
+ available_options+=("2")
+ fi
+
+ # 方案3: acme.sh + ZeroSSL
+ if [[ "$failed_method" != "3" ]]; then
+ echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL"
+ available_options+=("3")
+ fi
+
+ # 方案5: acme.sh + Buypass
+ if [[ "$failed_method" != "5" ]]; then
+ echo -e "${GREEN}[5]${NC} acme.sh + Buypass"
+ available_options+=("5")
+ fi
+
+ # 方案8: 不配置HTTPS
echo -e "${GREEN}[8]${NC} 暂不配置HTTPS"
+ available_options+=("8")
+
+ echo ""
+ echo -e "${YELLOW}提示: 方案 $failed_method 已失败,已从列表中移除${NC}"
echo ""
while true; do
- read -p "请选择备选方案 [2-4/8]: " retry_choice < /dev/tty
+ read -p "请选择备选方案: " retry_choice < /dev/tty
+
+ # 检查输入是否在可用选项中
+ if [[ ! " ${available_options[@]} " =~ " ${retry_choice} " ]]; then
+ print_error "无效选项或该方案已失败"
+ continue
+ fi
+
case $retry_choice in
2)
deploy_acme_letsencrypt && return 0
+ # 如果再次失败,继续调用fallback但排除方案2
+ ssl_fallback "2"
+ return $?
;;
3)
deploy_acme_zerossl && return 0
+ ssl_fallback "3"
+ return $?
;;
- 4)
+ 5)
deploy_acme_buypass && return 0
+ ssl_fallback "5"
+ return $?
;;
8)
print_info "跳过HTTPS配置"
SSL_METHOD=8
return 0
;;
- *)
- print_error "无效选项"
- ;;
esac
done
}
@@ -962,96 +1311,616 @@ ssl_fallback() {
deploy_certbot() {
print_step "使用 Certbot 部署SSL证书..."
- # 安装certbot
- case $PKG_MANAGER in
- apt)
- apt-get install -y certbot python3-certbot-nginx
- ;;
- yum)
- yum install -y certbot python3-certbot-nginx
- ;;
- dnf)
- dnf install -y certbot python3-certbot-nginx
- ;;
- zypper)
- zypper install -y certbot python3-certbot-nginx
- ;;
- esac
+ # 检查certbot是否已安装
+ if ! command -v certbot &> /dev/null; then
+ print_info "正在安装 Certbot..."
- # 申请证书
- certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos --email "admin@${DOMAIN}" --redirect
+ # 安装certbot
+ case $PKG_MANAGER in
+ apt)
+ # Ubuntu/Debian: 优先使用snap(官方推荐,避免Python依赖冲突)
+ if command -v snap &> /dev/null; then
+ print_info "使用snap安装Certbot(官方推荐方式)..."
+ snap install --classic certbot 2>/dev/null || true
+ ln -sf /snap/bin/certbot /usr/bin/certbot 2>/dev/null || true
- # 配置自动续期
- systemctl enable certbot.timer
+ # 验证snap安装是否成功
+ if /snap/bin/certbot --version &> /dev/null; then
+ print_success "Certbot (snap版) 安装成功"
+ else
+ print_warning "snap安装失败,尝试apt安装..."
+ # 修复urllib3依赖问题
+ apt-get remove -y python3-urllib3 2>/dev/null || true
+ apt-get install -y certbot python3-certbot-nginx
+ fi
+ else
+ print_info "snap不可用,使用apt安装..."
+ # 修复urllib3依赖问题
+ apt-get remove -y python3-urllib3 2>/dev/null || true
+ apt-get install -y certbot python3-certbot-nginx
+ fi
+ ;;
+ yum)
+ # 修复urllib3依赖问题
+ yum remove -y python3-urllib3 2>/dev/null || true
+ yum install -y certbot python3-certbot-nginx
+ ;;
+ dnf)
+ # 修复urllib3依赖问题
+ dnf remove -y python3-urllib3 2>/dev/null || true
+ dnf install -y certbot python3-certbot-nginx
+ ;;
+ zypper)
+ zypper install -y certbot python3-certbot-nginx
+ ;;
+ esac
- print_success "Certbot SSL证书部署成功"
- return 0
+ # 最终验证certbot是否可用
+ if ! command -v certbot &> /dev/null; then
+ print_error "Certbot安装失败"
+ return 1
+ fi
+ else
+ print_success "Certbot 已安装: $(certbot --version 2>&1 | head -1)"
+ fi
+
+ # 修复已安装certbot的urllib3依赖冲突
+ if ! certbot --version &> /dev/null; then
+ print_warning "检测到Certbot依赖问题,正在修复..."
+ case $PKG_MANAGER in
+ apt)
+ apt-get remove -y python3-urllib3 2>/dev/null || true
+ apt-get install --reinstall -y certbot python3-certbot-nginx
+ ;;
+ yum|dnf)
+ $PKG_MANAGER remove -y python3-urllib3 2>/dev/null || true
+ $PKG_MANAGER reinstall -y certbot python3-certbot-nginx
+ ;;
+ esac
+
+ # 再次验证
+ if ! certbot --version &> /dev/null; then
+ print_error "Certbot依赖修复失败,建议尝试其他SSL方案"
+ return 1
+ fi
+ print_success "Certbot依赖已修复"
+ fi
+
+ # 申请证书(使用webroot模式,不自动修改Nginx配置)
+ echo ""
+ print_info "正在申请 Let's Encrypt 证书..."
+
+ if certbot certonly --webroot -w "${PROJECT_DIR}/frontend" -d "$DOMAIN" --non-interactive --agree-tos --email "admin@${DOMAIN}"; then
+ # 将证书复制到Nginx SSL目录
+ mkdir -p /etc/nginx/ssl
+ ln -sf "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "/etc/nginx/ssl/${DOMAIN}.crt"
+ ln -sf "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "/etc/nginx/ssl/${DOMAIN}.key"
+
+ # 配置自动续期
+ systemctl enable certbot.timer 2>/dev/null || true
+
+ print_success "Certbot SSL证书申请成功"
+ return 0
+ else
+ # 检查证书是否已存在
+ if [[ -d "/etc/letsencrypt/live/${DOMAIN}" ]]; then
+ print_warning "检测到证书已存在,使用已有证书"
+ mkdir -p /etc/nginx/ssl
+ ln -sf "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "/etc/nginx/ssl/${DOMAIN}.crt"
+ ln -sf "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "/etc/nginx/ssl/${DOMAIN}.key"
+ print_success "已有证书已链接到Nginx目录"
+ return 0
+ else
+ print_error "Certbot SSL证书申请失败"
+ echo ""
+ print_warning "常见失败原因:"
+ echo " 1. 域名未正确解析到此服务器"
+ echo " 2. 防火墙阻止了80端口"
+ echo " 3. Nginx未正确配置或未启动"
+ echo " 4. Let's Encrypt速率限制"
+ echo ""
+ return 1
+ fi
+ fi
}
deploy_acme_letsencrypt() {
print_step "使用 acme.sh + Let's Encrypt 部署SSL证书..."
# 安装acme.sh
- if [[ ! -d ~/.acme.sh ]]; then
- curl https://get.acme.sh | sh
+ if [[ ! -d ~/.acme.sh ]] || [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ echo ""
+ print_info "正在安装 acme.sh..."
+
+ # 如果目录存在但文件不存在,先清理
+ if [[ -d ~/.acme.sh ]] && [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_warning "检测到不完整的安装,正在清理..."
+ rm -rf ~/.acme.sh
+ fi
+
+ print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)"
+
+ # 使用官方安装方法:直接通过curl管道执行
+ print_info "正在下载并安装..."
+
+ if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then
+ install_result=$?
+ print_info "安装脚本执行完成,退出码: $install_result"
+ else
+ install_result=$?
+ print_error "安装脚本执行失败,退出码: $install_result"
+ fi
+
+ # 重新加载环境变量
+ source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true
+
+ # 等待文件系统同步
+ print_info "等待安装完成..."
+ sleep 3
+
+ # 验证安装是否真正成功
+ if [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then
+ print_success "acme.sh 安装成功"
+ else
+ print_error "acme.sh 安装失败"
+ echo ""
+ print_warning "诊断信息:"
+ echo " - 安装命令退出码: $install_result"
+ echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')"
+ echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')"
+ echo " - HOME变量: $HOME"
+ echo " - 当前用户: $(whoami)"
+ echo ""
+
+ if [[ -d ~/.acme.sh ]]; then
+ print_info "~/.acme.sh 目录内容:"
+ ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录"
+ echo ""
+ fi
+
+ print_info "尝试查找acme.sh安装位置..."
+ find /root -name "acme.sh" -type f 2>/dev/null | head -5 || echo " 未找到"
+ echo ""
+ print_warning "可能的原因:"
+ echo " 1. 网络连接问题或下载超时"
+ echo " 2. GitHub访问受限(国内网络)"
+ echo " 3. curl版本过低或不支持某些功能"
+ echo ""
+ print_warning "建议尝试其他SSL方案:"
+ echo " 1. 返回选择 Certbot (推荐)"
+ echo " 2. 或选择 [8] 暂不配置HTTPS"
+ echo ""
+ return 1
+ fi
fi
+ # 确认acme.sh可用
+ echo ""
+ print_info "验证 acme.sh 安装..."
+
+ # 等待文件系统同步
+ sleep 2
+
+ # 检查安装目录
+ if [[ ! -d ~/.acme.sh ]]; then
+ print_error "安装目录不存在: ~/.acme.sh"
+ return 1
+ fi
+
+ # 检查主脚本文件
+ if [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_error "主脚本文件不存在: ~/.acme.sh/acme.sh"
+ print_info "目录内容:"
+ ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录"
+ return 1
+ fi
+
+ # 检查脚本是否可执行
+ if [[ ! -x ~/.acme.sh/acme.sh ]]; then
+ print_warning "脚本不可执行,正在添加执行权限..."
+ chmod +x ~/.acme.sh/acme.sh
+ fi
+
+ # 测试脚本是否能运行
+ if ! ~/.acme.sh/acme.sh --version &> /dev/null; then
+ print_error "acme.sh 无法运行"
+ return 1
+ fi
+
+ print_success "acme.sh 验证通过"
+
# 申请证书
- ~/.acme.sh/acme.sh --issue -d "$DOMAIN" --nginx
+ echo ""
+ print_info "正在申请 Let's Encrypt 证书..."
+
+ # 再次确认acme.sh存在
+ if [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh"
+ return 1
+ fi
+
+ # 使用webroot模式申请证书(更可靠)
+ # 先尝试正常申请,如果证书已存在则使用--force强制更新
+ if ~/.acme.sh/acme.sh --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then
+ print_success "证书申请成功"
+ else
+ # 检查是否是因为证书已存在
+ if ~/.acme.sh/acme.sh --list | grep -q "$DOMAIN"; then
+ print_warning "检测到证书已存在,使用已有证书"
+ print_success "将直接安装现有证书"
+ else
+ print_error "证书申请失败"
+ echo ""
+ print_warning "常见失败原因:"
+ echo " 1. 域名未正确解析到此服务器"
+ echo " 2. Nginx未正确配置或未启动"
+ echo " 3. 80端口被占用或防火墙阻止"
+ echo " 4. 前端目录权限不足"
+ echo ""
+ return 1
+ fi
+ fi
# 安装证书
- mkdir -p /etc/nginx/ssl
- ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
- --key-file /etc/nginx/ssl/${DOMAIN}.key \
- --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \
- --reloadcmd "systemctl reload nginx"
+ echo ""
+ print_info "正在安装证书到Nginx..."
- print_success "acme.sh SSL证书部署成功"
- return 0
+ # 再次确认acme.sh存在
+ if [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh"
+ return 1
+ fi
+
+ mkdir -p /etc/nginx/ssl
+
+ # 确保nginx服务已启动(证书安装时需要reload)
+ if ! systemctl is-active --quiet nginx 2>/dev/null && ! pgrep -x nginx > /dev/null 2>&1; then
+ print_warning "Nginx未运行,正在启动..."
+ systemctl start nginx 2>/dev/null || /www/server/nginx/sbin/nginx 2>/dev/null || true
+ sleep 2
+ fi
+
+ # 先不带reload命令安装证书(避免nginx未启动导致失败)
+ if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
+ --key-file /etc/nginx/ssl/${DOMAIN}.key \
+ --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt; then
+ print_success "证书文件已安装到: /etc/nginx/ssl/"
+
+ # 手动reload nginx
+ if systemctl is-active --quiet nginx 2>/dev/null; then
+ systemctl reload nginx && print_success "Nginx配置已重载"
+ elif pgrep -x nginx > /dev/null; then
+ nginx -s reload && print_success "Nginx配置已重载"
+ else
+ print_warning "Nginx未运行,将在后续步骤启动"
+ fi
+
+ # 配置自动续期
+ setup_acme_auto_renew
+
+ return 0
+ else
+ print_error "证书安装失败"
+ return 1
+ fi
}
deploy_acme_zerossl() {
print_step "使用 acme.sh + ZeroSSL 部署SSL证书..."
- # 安装acme.sh
- if [[ ! -d ~/.acme.sh ]]; then
- curl https://get.acme.sh | sh
+ # 安装acme.sh(使用改进的安装逻辑)
+ if [[ ! -d ~/.acme.sh ]] || [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ echo ""
+ print_info "正在安装 acme.sh..."
+
+ # 如果目录存在但文件不存在,先清理
+ if [[ -d ~/.acme.sh ]] && [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_warning "检测到不完整的安装,正在清理..."
+ rm -rf ~/.acme.sh
+ fi
+
+ print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)"
+
+ # 使用官方安装方法:直接通过curl管道执行
+ print_info "正在下载并安装..."
+
+ if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then
+ install_result=$?
+ print_info "安装脚本执行完成,退出码: $install_result"
+ else
+ install_result=$?
+ print_error "安装脚本执行失败,退出码: $install_result"
+ fi
+
+ # 重新加载环境变量
+ source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true
+
+ # 等待文件系统同步
+ print_info "等待安装完成..."
+ sleep 3
+
+ # 验证安装
+ if [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then
+ print_success "acme.sh 安装成功"
+ else
+ print_error "acme.sh 安装失败"
+ echo ""
+ print_warning "诊断信息:"
+ echo " - 安装命令退出码: $install_result"
+ echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')"
+ echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')"
+ echo ""
+
+ if [[ -d ~/.acme.sh ]]; then
+ print_info "~/.acme.sh 目录内容:"
+ ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录"
+ echo ""
+ fi
+
+ return 1
+ fi
fi
+ # 确认acme.sh可用
+ echo ""
+ print_info "验证 acme.sh 安装..."
+
+ # 等待文件系统同步
+ sleep 2
+
+ # 检查安装目录
+ if [[ ! -d ~/.acme.sh ]]; then
+ print_error "安装目录不存在: ~/.acme.sh"
+ return 1
+ fi
+
+ # 检查主脚本文件
+ if [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_error "主脚本文件不存在: ~/.acme.sh/acme.sh"
+ print_info "目录内容:"
+ ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录"
+ return 1
+ fi
+
+ # 检查脚本是否可执行
+ if [[ ! -x ~/.acme.sh/acme.sh ]]; then
+ print_warning "脚本不可执行,正在添加执行权限..."
+ chmod +x ~/.acme.sh/acme.sh
+ fi
+
+ # 测试脚本是否能运行
+ if ! ~/.acme.sh/acme.sh --version &> /dev/null; then
+ print_error "acme.sh 无法运行"
+ return 1
+ fi
+
+ print_success "acme.sh 验证通过"
+
# 申请证书
- ~/.acme.sh/acme.sh --server zerossl --issue -d "$DOMAIN" --nginx
+ echo ""
+ print_info "正在申请 ZeroSSL 证书..."
+
+ # 再次确认acme.sh存在
+ if [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh"
+ return 1
+ fi
+
+ # 使用webroot模式申请证书(更可靠)
+ if ~/.acme.sh/acme.sh --server zerossl --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then
+ print_success "证书申请成功"
+ else
+ # 检查是否是因为证书已存在
+ if ~/.acme.sh/acme.sh --list | grep -q "$DOMAIN"; then
+ print_warning "检测到证书已存在,使用已有证书"
+ print_success "将直接安装现有证书"
+ else
+ print_error "证书申请失败"
+ return 1
+ fi
+ fi
# 安装证书
- mkdir -p /etc/nginx/ssl
- ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
- --key-file /etc/nginx/ssl/${DOMAIN}.key \
- --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \
- --reloadcmd "systemctl reload nginx"
+ echo ""
+ print_info "正在安装证书到Nginx..."
- print_success "ZeroSSL证书部署成功"
- return 0
+ # 再次确认acme.sh存在
+ if [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh"
+ return 1
+ fi
+
+ mkdir -p /etc/nginx/ssl
+
+ # 确保nginx服务已启动(证书安装时需要reload)
+ if ! systemctl is-active --quiet nginx 2>/dev/null && ! pgrep -x nginx > /dev/null 2>&1; then
+ print_warning "Nginx未运行,正在启动..."
+ systemctl start nginx 2>/dev/null || /www/server/nginx/sbin/nginx 2>/dev/null || true
+ sleep 2
+ fi
+
+ # 先不带reload命令安装证书(避免nginx未启动导致失败)
+ if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
+ --key-file /etc/nginx/ssl/${DOMAIN}.key \
+ --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt; then
+ print_success "证书文件已安装到: /etc/nginx/ssl/"
+
+ # 手动reload nginx
+ if systemctl is-active --quiet nginx 2>/dev/null; then
+ systemctl reload nginx && print_success "Nginx配置已重载"
+ elif pgrep -x nginx > /dev/null; then
+ nginx -s reload && print_success "Nginx配置已重载"
+ else
+ print_warning "Nginx未运行,将在后续步骤启动"
+ fi
+
+ # 配置自动续期
+ setup_acme_auto_renew
+
+ return 0
+ else
+ print_error "证书安装失败"
+ return 1
+ fi
}
deploy_acme_buypass() {
print_step "使用 acme.sh + Buypass 部署SSL证书..."
- # 安装acme.sh
- if [[ ! -d ~/.acme.sh ]]; then
- curl https://get.acme.sh | sh
+ # 安装acme.sh(使用改进的安装逻辑)
+ if [[ ! -d ~/.acme.sh ]] || [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ echo ""
+ print_info "正在安装 acme.sh..."
+
+ # 如果目录存在但文件不存在,先清理
+ if [[ -d ~/.acme.sh ]] && [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_warning "检测到不完整的安装,正在清理..."
+ rm -rf ~/.acme.sh
+ fi
+
+ print_info "使用 GitHub 官方源(国内可能较慢,请耐心等待)"
+
+ # 使用官方安装方法:直接通过curl管道执行
+ print_info "正在下载并安装..."
+
+ if curl -fsSL https://get.acme.sh | sh -s email=admin@example.com; then
+ install_result=$?
+ print_info "安装脚本执行完成,退出码: $install_result"
+ else
+ install_result=$?
+ print_error "安装脚本执行失败,退出码: $install_result"
+ fi
+
+ # 重新加载环境变量
+ source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true
+
+ # 等待文件系统同步
+ print_info "等待安装完成..."
+ sleep 3
+
+ # 验证安装
+ if [[ -d ~/.acme.sh ]] && [[ -f ~/.acme.sh/acme.sh ]]; then
+ print_success "acme.sh 安装成功"
+ else
+ print_error "acme.sh 安装失败"
+ echo ""
+ print_warning "诊断信息:"
+ echo " - 安装命令退出码: $install_result"
+ echo " - 目录 ~/.acme.sh 存在: $([ -d ~/.acme.sh ] && echo '是' || echo '否')"
+ echo " - 文件 ~/.acme.sh/acme.sh 存在: $([ -f ~/.acme.sh/acme.sh ] && echo '是' || echo '否')"
+ echo ""
+
+ if [[ -d ~/.acme.sh ]]; then
+ print_info "~/.acme.sh 目录内容:"
+ ls -la ~/.acme.sh/ 2>&1 | head -15 || echo " 无法列出目录"
+ echo ""
+ fi
+
+ return 1
+ fi
fi
+ # 确认acme.sh可用
+ echo ""
+ print_info "验证 acme.sh 安装..."
+
+ # 等待文件系统同步
+ sleep 2
+
+ # 检查安装目录
+ if [[ ! -d ~/.acme.sh ]]; then
+ print_error "安装目录不存在: ~/.acme.sh"
+ return 1
+ fi
+
+ # 检查主脚本文件
+ if [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_error "主脚本文件不存在: ~/.acme.sh/acme.sh"
+ print_info "目录内容:"
+ ls -la ~/.acme.sh/ 2>&1 | head -10 || echo "无法列出目录"
+ return 1
+ fi
+
+ # 检查脚本是否可执行
+ if [[ ! -x ~/.acme.sh/acme.sh ]]; then
+ print_warning "脚本不可执行,正在添加执行权限..."
+ chmod +x ~/.acme.sh/acme.sh
+ fi
+
+ # 测试脚本是否能运行
+ if ! ~/.acme.sh/acme.sh --version &> /dev/null; then
+ print_error "acme.sh 无法运行"
+ return 1
+ fi
+
+ print_success "acme.sh 验证通过"
+
# 申请证书
- ~/.acme.sh/acme.sh --server buypass --issue -d "$DOMAIN" --nginx
+ echo ""
+ print_info "正在申请 Buypass 证书..."
+
+ # 再次确认acme.sh存在
+ if [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh"
+ return 1
+ fi
+
+ # 使用webroot模式申请证书(更可靠)
+ if ~/.acme.sh/acme.sh --server buypass --issue -d "$DOMAIN" --webroot "${PROJECT_DIR}/frontend"; then
+ print_success "证书申请成功"
+ else
+ # 检查是否是因为证书已存在
+ if ~/.acme.sh/acme.sh --list | grep -q "$DOMAIN"; then
+ print_warning "检测到证书已存在,使用已有证书"
+ print_success "将直接安装现有证书"
+ else
+ print_error "证书申请失败"
+ return 1
+ fi
+ fi
# 安装证书
- mkdir -p /etc/nginx/ssl
- ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
- --key-file /etc/nginx/ssl/${DOMAIN}.key \
- --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt \
- --reloadcmd "systemctl reload nginx"
+ echo ""
+ print_info "正在安装证书到Nginx..."
- print_success "Buypass SSL证书部署成功"
- return 0
+ # 再次确认acme.sh存在
+ if [[ ! -f ~/.acme.sh/acme.sh ]]; then
+ print_error "acme.sh文件不存在: ~/.acme.sh/acme.sh"
+ return 1
+ fi
+
+ mkdir -p /etc/nginx/ssl
+
+ # 确保nginx服务已启动(证书安装时需要reload)
+ if ! systemctl is-active --quiet nginx 2>/dev/null && ! pgrep -x nginx > /dev/null 2>&1; then
+ print_warning "Nginx未运行,正在启动..."
+ systemctl start nginx 2>/dev/null || /www/server/nginx/sbin/nginx 2>/dev/null || true
+ sleep 2
+ fi
+
+ # 先不带reload命令安装证书(避免nginx未启动导致失败)
+ if ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
+ --key-file /etc/nginx/ssl/${DOMAIN}.key \
+ --fullchain-file /etc/nginx/ssl/${DOMAIN}.crt; then
+ print_success "证书文件已安装到: /etc/nginx/ssl/"
+
+ # 手动reload nginx
+ if systemctl is-active --quiet nginx 2>/dev/null; then
+ systemctl reload nginx && print_success "Nginx配置已重载"
+ elif pgrep -x nginx > /dev/null; then
+ nginx -s reload && print_success "Nginx配置已重载"
+ else
+ print_warning "Nginx未运行,将在后续步骤启动"
+ fi
+
+ # 配置自动续期
+ setup_acme_auto_renew
+
+ return 0
+ else
+ print_error "证书安装失败"
+ return 1
+ fi
}
deploy_aliyun_ssl() {
@@ -1367,6 +2236,255 @@ build_upload_tool() {
fi
}
+################################################################################
+# Nginx配置 - 分步骤执行
+################################################################################
+
+# 步骤1: 先配置HTTP Nginx(为SSL证书验证做准备)
+configure_nginx_http_first() {
+ print_step "配置基础HTTP Nginx(用于SSL证书验证)..."
+
+ # 总是先配置HTTP模式
+ local server_name="${DOMAIN:-_}"
+
+ # 检测Nginx配置目录结构并创建必要的目录
+ if [[ -d /www/server/nginx ]]; then
+ # 宝塔面板 (BT Panel)
+ NGINX_CONF_DIR="/www/server/panel/vhost/nginx"
+ NGINX_ENABLED_DIR=""
+ USE_SYMLINK=false
+ IS_BT_PANEL=true
+
+ # 确保目录存在
+ mkdir -p ${NGINX_CONF_DIR}
+ print_info "检测到宝塔面板,使用宝塔Nginx配置目录"
+ elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then
+ # Debian/Ubuntu: 使用sites-available
+ NGINX_CONF_DIR="/etc/nginx/sites-available"
+ NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
+ USE_SYMLINK=true
+ IS_BT_PANEL=false
+
+ # 确保目录存在
+ mkdir -p ${NGINX_CONF_DIR}
+ mkdir -p ${NGINX_ENABLED_DIR}
+ else
+ # CentOS/RHEL: 使用conf.d
+ NGINX_CONF_DIR="/etc/nginx/conf.d"
+ NGINX_ENABLED_DIR=""
+ USE_SYMLINK=false
+ IS_BT_PANEL=false
+
+ # 确保目录存在
+ mkdir -p ${NGINX_CONF_DIR}
+ fi
+
+ cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF
+server {
+ listen ${HTTP_PORT};
+ server_name ${server_name};
+
+ # 文件上传大小限制(10GB)
+ client_max_body_size 10G;
+
+ # 前端静态文件
+ location / {
+ root ${PROJECT_DIR}/frontend;
+ index index.html;
+ try_files \$uri \$uri/ /index.html;
+ }
+
+ # 后端API
+ location /api {
+ proxy_pass http://localhost:${BACKEND_PORT};
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade \$http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host \$host;
+ proxy_cache_bypass \$http_upgrade;
+ proxy_set_header X-Real-IP \$remote_addr;
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto \$scheme;
+
+ # 上传超时设置
+ proxy_read_timeout 3600s;
+ proxy_send_timeout 3600s;
+ proxy_connect_timeout 300s;
+ }
+
+ # 分享页面
+ location /s/ {
+ proxy_pass http://localhost:${BACKEND_PORT};
+ proxy_set_header Host \$host;
+ proxy_set_header X-Real-IP \$remote_addr;
+ proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto \$scheme;
+ }
+
+ # 静态资源
+ location /libs {
+ alias ${PROJECT_DIR}/frontend/libs;
+ expires 30d;
+ }
+
+ # 上传工具下载
+ location /download-tool {
+ alias ${PROJECT_DIR}/upload-tool/dist;
+ }
+}
+EOF
+
+ # 根据系统类型处理配置文件
+ if [[ "$USE_SYMLINK" == "true" ]]; then
+ # Debian/Ubuntu: 创建软链接
+ ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf
+ # 删除默认站点
+ rm -f ${NGINX_ENABLED_DIR}/default
+ elif [[ "$IS_BT_PANEL" != "true" ]]; then
+ # CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载
+ rm -f /etc/nginx/conf.d/default.conf
+ fi
+ # 宝塔面板:配置文件已自动包含,无需额外操作
+
+ # 测试nginx配置
+ if ! nginx -t; then
+ print_error "Nginx配置测试失败"
+ return 1
+ fi
+
+ # 启动或重载Nginx
+ if [[ "$IS_BT_PANEL" == "true" ]]; then
+ # 宝塔面板:尝试多种方式
+ print_info "宝塔环境,尝试重载Nginx..."
+
+ # 方式1: 使用宝塔命令行工具(如果存在)
+ if [[ -f /etc/init.d/bt ]]; then
+ /etc/init.d/bt restart 2>/dev/null
+ fi
+
+ # 方式2: 直接使用nginx命令reload
+ if [[ -f /www/server/nginx/sbin/nginx ]]; then
+ /www/server/nginx/sbin/nginx -s reload 2>/dev/null
+ if [[ $? -eq 0 ]]; then
+ print_success "已使用nginx -s reload重载配置"
+ else
+ # 如果reload失败,尝试启动
+ /www/server/nginx/sbin/nginx 2>/dev/null
+ if [[ $? -eq 0 ]]; then
+ print_success "已启动Nginx"
+ else
+ print_warning "Nginx reload失败,尝试systemctl..."
+ fi
+ fi
+ fi
+
+ # 方式3: 尝试systemctl(备用)
+ if systemctl is-active --quiet nginx 2>/dev/null; then
+ systemctl reload nginx 2>/dev/null && print_info "已使用systemctl重载配置"
+ else
+ systemctl start nginx 2>/dev/null && print_info "已使用systemctl启动Nginx"
+ fi
+ else
+ # 标准Nginx:重启
+ systemctl restart nginx
+ fi
+
+ # 验证Nginx是否运行
+ sleep 2
+ if [[ "$IS_BT_PANEL" == "true" ]]; then
+ # 宝塔:检查进程
+ if pgrep -x nginx > /dev/null; then
+ print_success "Nginx运行正常"
+ else
+ print_error "Nginx未运行"
+ print_warning "请在宝塔面板中手动启动Nginx,或运行:"
+ print_warning "/www/server/nginx/sbin/nginx"
+ return 1
+ fi
+ else
+ # 标准Nginx:使用systemctl检查
+ if ! systemctl is-active --quiet nginx; then
+ print_error "Nginx启动失败"
+ return 1
+ fi
+ fi
+
+ print_success "基础HTTP Nginx配置完成"
+ echo ""
+}
+
+# 步骤2: 根据SSL结果配置最终Nginx
+configure_nginx_final() {
+ print_step "配置最终Nginx..."
+
+ # 检查SSL是否成功部署
+ local ssl_deployed=false
+ if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then
+ # 检查SSL证书文件是否存在
+ if [[ -f /etc/nginx/ssl/${DOMAIN}.crt ]] && [[ -f /etc/nginx/ssl/${DOMAIN}.key ]]; then
+ ssl_deployed=true
+ print_info "检测到SSL证书,配置HTTPS..."
+ else
+ print_warning "SSL证书不存在,保持HTTP配置"
+ fi
+ fi
+
+ # 根据SSL状态配置
+ if [[ "$ssl_deployed" == "true" ]]; then
+ # 配置HTTPS
+ configure_nginx_https
+ else
+ # 保持HTTP(已在第一步配置,这里只需确认)
+ print_info "使用HTTP配置"
+ fi
+
+ # 测试nginx配置
+ if ! nginx -t; then
+ print_error "Nginx配置测试失败"
+ return 1
+ fi
+
+ # 重载nginx - 兼容宝塔面板
+ if [[ "$IS_BT_PANEL" == "true" ]]; then
+ # 宝塔面板:尝试多种方式
+ print_info "宝塔环境,重载Nginx配置..."
+
+ # 方式1: 使用宝塔命令行工具(如果存在)
+ if [[ -f /etc/init.d/bt ]]; then
+ /etc/init.d/bt restart 2>/dev/null
+ fi
+
+ # 方式2: 直接使用nginx命令reload(最可靠)
+ if [[ -f /www/server/nginx/sbin/nginx ]]; then
+ /www/server/nginx/sbin/nginx -s reload 2>/dev/null
+ if [[ $? -eq 0 ]]; then
+ print_success "已使用nginx -s reload重载配置"
+ else
+ # 如果reload失败,尝试启动
+ /www/server/nginx/sbin/nginx 2>/dev/null
+ if [[ $? -eq 0 ]]; then
+ print_success "已启动Nginx"
+ else
+ print_warning "Nginx reload失败,尝试systemctl..."
+ fi
+ fi
+ fi
+
+ # 方式3: 尝试systemctl(备用)
+ if systemctl is-active --quiet nginx 2>/dev/null; then
+ systemctl reload nginx 2>/dev/null && print_info "已使用systemctl重载配置"
+ else
+ systemctl start nginx 2>/dev/null && print_info "已使用systemctl启动Nginx"
+ fi
+ else
+ # 标准Nginx:重载
+ systemctl reload nginx
+ fi
+
+ print_success "Nginx最终配置完成"
+ echo ""
+}
+
configure_nginx() {
print_step "配置Nginx..."
@@ -1396,7 +2514,30 @@ configure_nginx() {
configure_nginx_http() {
local server_name="${DOMAIN:-_}"
- cat > /etc/nginx/sites-available/${PROJECT_NAME}.conf << EOF
+ # 检测Nginx配置目录结构并创建必要的目录
+ if [[ -d /www/server/nginx ]]; then
+ # 宝塔面板
+ NGINX_CONF_DIR="/www/server/panel/vhost/nginx"
+ USE_SYMLINK=false
+ IS_BT_PANEL=true
+ mkdir -p ${NGINX_CONF_DIR}
+ elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then
+ # Debian/Ubuntu
+ NGINX_CONF_DIR="/etc/nginx/sites-available"
+ NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
+ USE_SYMLINK=true
+ IS_BT_PANEL=false
+ mkdir -p ${NGINX_CONF_DIR}
+ mkdir -p ${NGINX_ENABLED_DIR}
+ else
+ # CentOS/RHEL
+ NGINX_CONF_DIR="/etc/nginx/conf.d"
+ USE_SYMLINK=false
+ IS_BT_PANEL=false
+ mkdir -p ${NGINX_CONF_DIR}
+ fi
+
+ cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF
server {
listen ${HTTP_PORT};
server_name ${server_name};
@@ -1407,8 +2548,8 @@ server {
# 前端静态文件
location / {
root ${PROJECT_DIR}/frontend;
- index app.html;
- try_files \$uri \$uri/ /app.html;
+ index index.html;
+ try_files \$uri \$uri/ /index.html;
}
# 后端API
@@ -1451,19 +2592,54 @@ server {
}
EOF
- # 创建软链接
- ln -sf /etc/nginx/sites-available/${PROJECT_NAME}.conf /etc/nginx/sites-enabled/${PROJECT_NAME}.conf
-
- # 删除默认站点
- rm -f /etc/nginx/sites-enabled/default
+ # 根据系统类型处理配置文件
+ if [[ "$USE_SYMLINK" == "true" ]]; then
+ # Debian/Ubuntu: 创建软链接
+ ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf
+ # 删除默认站点
+ rm -f ${NGINX_ENABLED_DIR}/default
+ elif [[ "$IS_BT_PANEL" != "true" ]]; then
+ # CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载
+ rm -f /etc/nginx/conf.d/default.conf
+ fi
}
configure_nginx_https() {
- cat > /etc/nginx/sites-available/${PROJECT_NAME}.conf << EOF
+ # 检测Nginx配置目录结构并创建必要的目录
+ if [[ -d /www/server/nginx ]]; then
+ # 宝塔面板
+ NGINX_CONF_DIR="/www/server/panel/vhost/nginx"
+ USE_SYMLINK=false
+ IS_BT_PANEL=true
+ mkdir -p ${NGINX_CONF_DIR}
+ elif [[ -d /etc/nginx/sites-available ]] || [[ "$PKG_MANAGER" == "apt" ]]; then
+ # Debian/Ubuntu
+ NGINX_CONF_DIR="/etc/nginx/sites-available"
+ NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
+ USE_SYMLINK=true
+ IS_BT_PANEL=false
+ mkdir -p ${NGINX_CONF_DIR}
+ mkdir -p ${NGINX_ENABLED_DIR}
+ else
+ # CentOS/RHEL
+ NGINX_CONF_DIR="/etc/nginx/conf.d"
+ USE_SYMLINK=false
+ IS_BT_PANEL=false
+ mkdir -p ${NGINX_CONF_DIR}
+ fi
+
+ # 根据HTTPS端口生成正确的重定向URL
+ if [[ "$HTTPS_PORT" == "443" ]]; then
+ REDIRECT_URL="https://\$server_name\$request_uri"
+ else
+ REDIRECT_URL="https://\$server_name:${HTTPS_PORT}\$request_uri"
+ fi
+
+ cat > ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf << EOF
server {
listen ${HTTP_PORT};
server_name ${DOMAIN};
- return 301 https://\$server_name:\${HTTPS_PORT}\$request_uri;
+ return 301 ${REDIRECT_URL};
}
server {
@@ -1483,8 +2659,8 @@ server {
# 前端静态文件
location / {
root ${PROJECT_DIR}/frontend;
- index app.html;
- try_files \$uri \$uri/ /app.html;
+ index index.html;
+ try_files \$uri \$uri/ /index.html;
}
# 后端API
@@ -1527,11 +2703,16 @@ server {
}
EOF
- # 创建软链接
- ln -sf /etc/nginx/sites-available/${PROJECT_NAME}.conf /etc/nginx/sites-enabled/${PROJECT_NAME}.conf
-
- # 删除默认站点
- rm -f /etc/nginx/sites-enabled/default
+ # 根据系统类型处理配置文件
+ if [[ "$USE_SYMLINK" == "true" ]]; then
+ # Debian/Ubuntu: 创建软链接
+ ln -sf ${NGINX_CONF_DIR}/${PROJECT_NAME}.conf ${NGINX_ENABLED_DIR}/${PROJECT_NAME}.conf
+ # 删除默认站点
+ rm -f ${NGINX_ENABLED_DIR}/default
+ elif [[ "$IS_BT_PANEL" != "true" ]]; then
+ # CentOS/RHEL (非宝塔): conf.d中的.conf文件会自动加载
+ rm -f /etc/nginx/conf.d/default.conf
+ fi
}
start_backend_service() {
@@ -1566,7 +2747,7 @@ health_check() {
fi
# 检查端口
- if netstat -tunlp | grep -q ":${BACKEND_PORT}"; then
+ if netstat -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}" || ss -tunlp 2>/dev/null | grep -q ":${BACKEND_PORT}"; then
print_success "后端端口监听正常 (${BACKEND_PORT})"
else
print_error "后端端口监听异常"
@@ -1574,11 +2755,22 @@ health_check() {
fi
# 检查Nginx
- if systemctl is-active --quiet nginx; then
- print_success "Nginx服务运行正常"
+ if [[ -d /www/server/nginx ]]; then
+ # 宝塔面板:检查进程
+ if pgrep -x nginx > /dev/null; then
+ print_success "Nginx服务运行正常"
+ else
+ print_error "Nginx服务异常"
+ return 1
+ fi
else
- print_error "Nginx服务异常"
- return 1
+ # 标准Nginx:使用systemctl检查
+ if systemctl is-active --quiet nginx; then
+ print_success "Nginx服务运行正常"
+ else
+ print_error "Nginx服务异常"
+ return 1
+ fi
fi
# 检查数据库
@@ -1671,15 +2863,13 @@ print_completion() {
# SSL续期提示
if [[ "$USE_DOMAIN" == "true" ]] && [[ "$SSL_METHOD" != "8" ]]; then
- echo -e "${YELLOW}SSL证书:${NC}"
- case $SSL_METHOD in
- 1)
- echo " 自动续期: 已配置Certbot自动续期"
- ;;
- 2|3|4)
- echo " 自动续期: 已配置acme.sh自动续期"
- ;;
- esac
+ echo -e "${YELLOW}SSL证书自动续期:${NC}"
+ echo " - 方式: acme.sh cron任务"
+ echo " - 频率: 每天自动检查"
+ echo " - 时机: 证书到期前30天自动续期"
+ echo " - 检查任务: crontab -l | grep acme"
+ echo " - 查看证书: ~/.acme.sh/acme.sh --list"
+ echo " - 手动续期: ~/.acme.sh/acme.sh --renew -d $DOMAIN --force"
echo ""
fi
@@ -2342,12 +3532,13 @@ main() {
echo -e "${GREEN}[1]${NC} 安装/部署 玩玩云"
echo -e "${BLUE}[2]${NC} 更新/升级 玩玩云"
echo -e "${YELLOW}[3]${NC} 修复/重新配置 玩玩云"
- echo -e "${RED}[4]${NC} 卸载 玩玩云"
+ echo -e "${PURPLE}[4]${NC} SSL证书管理(安装/续签/更换证书)"
+ echo -e "${RED}[5]${NC} 卸载 玩玩云"
echo -e "${GRAY}[0]${NC} 退出脚本"
echo ""
while true; do
- read -p "请输入选项 [0-4]: " mode_choice < /dev/tty
+ read -p "请输入选项 [0-5]: " mode_choice < /dev/tty
case $mode_choice in
1)
print_success "已选择: 安装模式"
@@ -2367,6 +3558,12 @@ main() {
exit 0
;;
4)
+ print_info "切换到SSL证书管理模式..."
+ echo ""
+ ssl_main
+ exit 0
+ ;;
+ 5)
print_info "切换到卸载模式..."
echo ""
uninstall_main
@@ -2391,8 +3588,9 @@ main() {
echo -e "${YELLOW}提示:${NC}"
echo " 安装: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh"
echo " 更新: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --update"
- echo " 卸载: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --uninstall"
echo " 修复: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --repair"
+ echo " SSL管理: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --ssl"
+ echo " 卸载: wget https://gitee.com/yu-yon/vue-driven-cloud-storage/raw/master/install.sh && bash install.sh --uninstall"
echo ""
sleep 2
fi
@@ -2434,11 +3632,14 @@ main() {
# 打包上传工具
build_upload_tool
- # 部署SSL证书
+ # 先配置基础HTTP Nginx(SSL证书申请需要)
+ configure_nginx_http_first
+
+ # 部署SSL证书(需要HTTP server block进行验证)
deploy_ssl
- # 配置Nginx
- configure_nginx
+ # 根据SSL结果配置最终Nginx
+ configure_nginx_final
# 启动后端服务
start_backend_service
@@ -2775,6 +3976,463 @@ repair_main() {
# 完成提示
print_repair_completion
}
+
+################################################################################
+# SSL证书管理功能
+################################################################################
+
+print_ssl_banner() {
+ clear
+ echo -e "${GREEN}"
+ echo "╔═══════════════════════════════════════════════════════════════╗"
+ echo "║ ║"
+ echo "║ 🔐 SSL证书管理模式 ║"
+ echo "║ ║"
+ echo "║ SSL Certificate Manager ║"
+ echo "║ ║"
+ echo "╚═══════════════════════════════════════════════════════════════╝"
+ echo -e "${NC}"
+}
+
+confirm_ssl_operation() {
+ print_ssl_banner
+
+ echo -e "${YELLOW}"
+ echo "本脚本将执行以下操作:"
+ echo ""
+ echo "【SSL证书管理】"
+ echo " ✓ 检测现有域名配置"
+ echo " ✓ 选择SSL证书部署方案"
+ echo " ✓ 申请/更换/续签证书"
+ echo " ✓ 更新Nginx HTTPS配置"
+ echo " ✓ 重载服务"
+ echo ""
+ echo "【将会保留】"
+ echo " ✓ 数据库文件(用户数据)"
+ echo " ✓ 用户上传的文件"
+ echo " ✓ 后端配置文件(.env)"
+ echo " ✓ 现有HTTP配置"
+ echo -e "${NC}"
+ echo ""
+
+ print_info "适用场景: 初次配置HTTPS、更换证书方案、证书续签"
+ echo ""
+
+ read -p "确定要继续吗? (y/n): " confirm < /dev/tty
+
+ if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
+ print_info "已取消操作"
+ exit 0
+ fi
+
+ echo ""
+}
+
+ssl_check_project() {
+ print_step "检查项目是否已安装..."
+
+ if [[ ! -d "$PROJECT_DIR" ]]; then
+ print_error "项目未安装: $PROJECT_DIR"
+ print_info "请先运行安装命令: bash install.sh"
+ exit 1
+ fi
+
+ if [[ ! -f "${PROJECT_DIR}/backend/server.js" ]]; then
+ print_error "项目目录不完整"
+ exit 1
+ fi
+
+ print_success "项目已安装: $PROJECT_DIR"
+ echo ""
+}
+
+ssl_load_existing_config() {
+ print_step "读取现有配置..."
+
+ # 从.env读取后端端口
+ if [[ -f "${PROJECT_DIR}/backend/.env" ]]; then
+ BACKEND_PORT=$(grep "^PORT=" "${PROJECT_DIR}/backend/.env" | cut -d'=' -f2 || echo "40001")
+ print_success "后端端口: $BACKEND_PORT"
+ else
+ BACKEND_PORT="40001"
+ print_warning ".env文件不存在,使用默认端口: $BACKEND_PORT"
+ fi
+
+ # 检查现有nginx配置
+ local nginx_conf=""
+ if [[ -f "/etc/nginx/sites-enabled/${PROJECT_NAME}.conf" ]]; then
+ nginx_conf="/etc/nginx/sites-enabled/${PROJECT_NAME}.conf"
+ elif [[ -f "/etc/nginx/conf.d/${PROJECT_NAME}.conf" ]]; then
+ nginx_conf="/etc/nginx/conf.d/${PROJECT_NAME}.conf"
+ elif [[ -f "/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf" ]]; then
+ nginx_conf="/www/server/panel/vhost/nginx/${PROJECT_NAME}.conf"
+ fi
+
+ if [[ -n "$nginx_conf" ]]; then
+ # 读取HTTP端口
+ EXISTING_HTTP_PORT=$(grep "listen" "$nginx_conf" | grep -v "ssl" | grep -v "#" | head -1 | awk '{print $2}' | tr -d ';' || echo "80")
+ HTTP_PORT=${EXISTING_HTTP_PORT:-80}
+
+ # 检查是否有HTTPS配置
+ if grep -q "listen.*ssl" "$nginx_conf"; then
+ EXISTING_HTTPS_PORT=$(grep "listen.*ssl" "$nginx_conf" | head -1 | awk '{print $2}' | tr -d ';' || echo "443")
+ HTTPS_PORT=${EXISTING_HTTPS_PORT:-443}
+ print_info "检测到现有HTTPS配置,端口: $HTTPS_PORT"
+ else
+ HTTPS_PORT="443"
+ print_info "未检测到HTTPS配置,将使用默认端口: 443"
+ fi
+
+ # 读取域名
+ SERVER_NAME=$(grep "server_name" "$nginx_conf" | head -1 | awk '{print $2}' | tr -d ';' || echo "")
+ if [[ -n "$SERVER_NAME" ]] && [[ "$SERVER_NAME" != "_" ]] && [[ "$SERVER_NAME" != "localhost" ]]; then
+ DOMAIN="$SERVER_NAME"
+ USE_DOMAIN=true
+ print_success "检测到域名: $DOMAIN"
+ else
+ USE_DOMAIN=false
+ print_warning "未检测到域名配置"
+ fi
+
+ print_success "HTTP端口: $HTTP_PORT"
+ else
+ print_error "未找到Nginx配置文件"
+ exit 1
+ fi
+
+ echo ""
+}
+
+ssl_configure_domain() {
+ print_step "配置域名"
+ echo ""
+
+ # 如果已有域名,询问是否使用
+ if [[ "$USE_DOMAIN" == "true" ]] && [[ -n "$DOMAIN" ]]; then
+ print_info "检测到现有域名: $DOMAIN"
+ read -p "是否使用此域名? (y/n): " use_existing < /dev/tty
+
+ if [[ "$use_existing" == "y" || "$use_existing" == "Y" ]]; then
+ print_success "使用现有域名: $DOMAIN"
+ echo ""
+ return 0
+ fi
+ fi
+
+ # 输入新域名
+ while true; do
+ read -p "请输入您的域名 (例如: wwy.example.com): " DOMAIN < /dev/tty
+ if [[ -z "$DOMAIN" ]]; then
+ print_error "域名不能为空"
+ continue
+ fi
+
+ # 验证域名格式
+ if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
+ print_error "域名格式不正确"
+ continue
+ fi
+
+ # 验证域名解析
+ print_info "正在验证域名解析..."
+ DOMAIN_IP=$(dig +short "$DOMAIN" 2>/dev/null | tail -n1 || nslookup "$DOMAIN" 2>/dev/null | grep -A1 "Name:" | tail -1 | awk '{print $2}')
+ PUBLIC_IP=$(curl -s ifconfig.me || curl -s icanhazip.com || echo "")
+
+ if [[ -n "$DOMAIN_IP" ]] && [[ "$DOMAIN_IP" == "$PUBLIC_IP" ]]; then
+ print_success "域名已正确解析到当前服务器IP"
+ USE_DOMAIN=true
+ break
+ else
+ print_warning "域名未解析到当前服务器IP"
+ print_info "域名解析IP: ${DOMAIN_IP:-未解析}"
+ print_info "当前服务器IP: $PUBLIC_IP"
+ read -p "是否继续? (y/n): " continue_choice < /dev/tty
+ if [[ "$continue_choice" == "y" || "$continue_choice" == "Y" ]]; then
+ USE_DOMAIN=true
+ break
+ fi
+ fi
+ done
+
+ echo ""
+}
+
+ssl_choose_method() {
+ print_step "选择SSL证书部署方式"
+ echo ""
+ echo -e "${YELLOW}【推荐方案】${NC}"
+ echo -e "${GREEN}[1]${NC} Certbot (Let's Encrypt官方工具)"
+ echo " - 最稳定可靠,支持自动续期"
+ echo ""
+ echo -e "${YELLOW}【备选方案】${NC}"
+ echo -e "${GREEN}[2]${NC} acme.sh + Let's Encrypt"
+ echo " - 纯Shell脚本,更轻量级"
+ echo -e "${GREEN}[3]${NC} acme.sh + ZeroSSL"
+ echo " - Let's Encrypt的免费替代品"
+ echo -e "${GREEN}[5]${NC} acme.sh + Buypass"
+ echo " - 挪威免费CA,有效期180天"
+ echo ""
+ echo -e "${YELLOW}【云服务商证书】${NC}"
+ echo -e "${GREEN}[4]${NC} 阿里云免费证书 (需提供AccessKey)"
+ echo -e "${GREEN}[6]${NC} 腾讯云免费证书 (需提供SecretKey)"
+ echo ""
+ echo -e "${YELLOW}【其他选项】${NC}"
+ echo -e "${GREEN}[7]${NC} 使用已有证书 (手动上传)"
+ echo -e "${GREEN}[8]${NC} 移除HTTPS配置 (改回HTTP)"
+ echo -e "${GREEN}[0]${NC} 取消操作"
+ echo ""
+
+ while true; do
+ read -p "请输入选项 [0-8]: " ssl_choice < /dev/tty
+ case $ssl_choice in
+ 1|2|3|4|5|6|7)
+ SSL_METHOD=$ssl_choice
+ break
+ ;;
+ 8)
+ SSL_METHOD=$ssl_choice
+ print_warning "将移除HTTPS配置,改回HTTP模式"
+ read -p "确定要继续吗? (y/n): " confirm_remove < /dev/tty
+ if [[ "$confirm_remove" == "y" || "$confirm_remove" == "Y" ]]; then
+ break
+ fi
+ ;;
+ 0)
+ print_info "已取消操作"
+ exit 0
+ ;;
+ *)
+ print_error "无效选项,请重新选择"
+ ;;
+ esac
+ done
+ echo ""
+}
+
+ssl_deploy_certificate() {
+ print_step "部署SSL证书..."
+
+ # 如果选择移除HTTPS
+ if [[ "$SSL_METHOD" == "8" ]]; then
+ print_info "将移除HTTPS配置..."
+ # 配置为HTTP模式
+ configure_nginx_http
+ return 0
+ fi
+
+ # 部署证书
+ deploy_ssl
+
+ # 检查证书是否部署成功
+ if [[ -f "/etc/nginx/ssl/${DOMAIN}.crt" ]] && [[ -f "/etc/nginx/ssl/${DOMAIN}.key" ]]; then
+ print_success "证书文件已部署"
+ else
+ print_warning "证书文件未找到,将使用HTTP配置"
+ SSL_METHOD="8"
+ fi
+}
+
+ssl_update_nginx_config() {
+ print_step "更新Nginx配置..."
+
+ if [[ "$SSL_METHOD" == "8" ]]; then
+ # HTTP配置
+ configure_nginx_http
+ else
+ # HTTPS配置
+ configure_nginx_https
+ fi
+
+ # 测试nginx配置
+ if ! nginx -t 2>&1; then
+ print_error "Nginx配置测试失败"
+ print_info "请检查配置文件"
+ return 1
+ fi
+
+ print_success "Nginx配置已更新"
+ echo ""
+}
+
+ssl_reload_services() {
+ print_step "重载服务..."
+
+ # 重载Nginx
+ if [[ -d /www/server/nginx ]]; then
+ # 宝塔面板
+ print_info "宝塔环境,重载Nginx..."
+ if [[ -f /www/server/nginx/sbin/nginx ]]; then
+ /www/server/nginx/sbin/nginx -s reload 2>/dev/null
+ if [[ $? -eq 0 ]]; then
+ print_success "Nginx已重载"
+ else
+ /www/server/nginx/sbin/nginx 2>/dev/null
+ print_success "Nginx已启动"
+ fi
+ fi
+ systemctl reload nginx 2>/dev/null || true
+ else
+ # 标准Nginx
+ systemctl reload nginx
+ print_success "Nginx已重载"
+ fi
+
+ # 重启后端服务(更新PUBLIC_PORT配置)
+ if command -v pm2 &> /dev/null; then
+ if pm2 list | grep -q "${PROJECT_NAME}-backend"; then
+ pm2 restart ${PROJECT_NAME}-backend
+ print_success "后端服务已重启"
+ fi
+ fi
+
+ echo ""
+}
+
+ssl_verify_deployment() {
+ print_step "验证部署..."
+
+ # 检查Nginx
+ if [[ -d /www/server/nginx ]]; then
+ if pgrep -x nginx > /dev/null; then
+ print_success "Nginx运行正常"
+ else
+ print_error "Nginx未运行"
+ fi
+ else
+ if systemctl is-active --quiet nginx; then
+ print_success "Nginx运行正常"
+ else
+ print_error "Nginx未运行"
+ fi
+ fi
+
+ # 检查SSL证书
+ if [[ "$SSL_METHOD" != "8" ]]; then
+ if [[ -f "/etc/nginx/ssl/${DOMAIN}.crt" ]]; then
+ print_success "SSL证书已部署: /etc/nginx/ssl/${DOMAIN}.crt"
+
+ # 显示证书信息
+ CERT_EXPIRY=$(openssl x509 -enddate -noout -in "/etc/nginx/ssl/${DOMAIN}.crt" 2>/dev/null | cut -d= -f2)
+ if [[ -n "$CERT_EXPIRY" ]]; then
+ print_info "证书有效期至: $CERT_EXPIRY"
+ fi
+ else
+ print_warning "SSL证书文件未找到"
+ fi
+ fi
+
+ echo ""
+}
+
+print_ssl_completion() {
+ clear
+ echo -e "${GREEN}"
+ echo "╔═══════════════════════════════════════════════════════════════╗"
+ echo "║ ║"
+ echo "║ ✓ SSL配置完成! ║"
+ echo "║ ║"
+ echo "╚═══════════════════════════════════════════════════════════════╝"
+ echo -e "${NC}"
+ echo ""
+
+ # 显示访问地址
+ if [[ "$SSL_METHOD" == "8" ]]; then
+ if [[ "$HTTP_PORT" == "80" ]]; then
+ echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}"
+ else
+ echo -e "${CYAN}访问地址:${NC} http://${DOMAIN}:${HTTP_PORT}"
+ fi
+ echo -e "${YELLOW}模式:${NC} HTTP"
+ else
+ if [[ "$HTTPS_PORT" == "443" ]]; then
+ echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}"
+ else
+ echo -e "${CYAN}访问地址:${NC} https://${DOMAIN}:${HTTPS_PORT}"
+ fi
+ echo -e "${YELLOW}模式:${NC} HTTPS"
+
+ # SSL信息
+ echo ""
+ echo -e "${YELLOW}SSL证书:${NC}"
+ case $SSL_METHOD in
+ 1)
+ echo " 方案: Certbot (Let's Encrypt)"
+ echo " 续期: 自动续期已配置"
+ ;;
+ 2)
+ echo " 方案: acme.sh + Let's Encrypt"
+ echo " 续期: 自动续期已配置"
+ ;;
+ 3)
+ echo " 方案: acme.sh + ZeroSSL"
+ echo " 续期: 自动续期已配置"
+ ;;
+ 4)
+ echo " 方案: acme.sh + Buypass"
+ echo " 续期: 自动续期已配置"
+ ;;
+ 7)
+ echo " 方案: 手动上传证书"
+ echo " 续期: 需手动更新证书文件"
+ ;;
+ esac
+ fi
+ echo ""
+
+ echo -e "${YELLOW}常用命令:${NC}"
+ echo " 查看证书信息: openssl x509 -text -noout -in /etc/nginx/ssl/${DOMAIN}.crt"
+ echo " 测试HTTPS: curl -I https://${DOMAIN}"
+ echo " 查看Nginx日志: tail -f /var/log/nginx/error.log"
+ echo " 重新配置SSL: bash install.sh --ssl"
+ echo ""
+
+ echo -e "${GREEN}SSL配置完成!${NC}"
+ echo ""
+}
+
+ssl_main() {
+ # 检查root权限
+ check_root
+
+ # 检测操作系统
+ detect_os
+
+ # 确认操作
+ confirm_ssl_operation
+
+ # 检查项目
+ ssl_check_project
+
+ # 读取现有配置
+ ssl_load_existing_config
+
+ # 配置域名
+ if [[ "$USE_DOMAIN" != "true" ]] || [[ -z "$DOMAIN" ]]; then
+ ssl_configure_domain
+ fi
+
+ # 选择SSL方案
+ ssl_choose_method
+
+ # 先配置基础HTTP Nginx(SSL验证需要)
+ configure_nginx_http_first
+
+ # 部署SSL证书
+ ssl_deploy_certificate
+
+ # 更新Nginx配置
+ ssl_update_nginx_config
+
+ # 重载服务
+ ssl_reload_services
+
+ # 验证部署
+ ssl_verify_deployment
+
+ # 完成提示
+ print_ssl_completion
+}
+
# 执行主流程
if [[ "$MODE" == "uninstall" ]]; then
uninstall_main
@@ -2782,6 +4440,8 @@ elif [[ "$MODE" == "update" ]]; then
update_main
elif [[ "$MODE" == "repair" ]]; then
repair_main
+elif [[ "$MODE" == "ssl" ]]; then
+ ssl_main
else
main
fi