From 6ebb8a05825b8364ebc5ccd5612f2dcf820c66e2 Mon Sep 17 00:00:00 2001 From: Peanut Date: Tue, 17 Mar 2026 23:53:36 +0800 Subject: [PATCH 1/9] =?UTF-8?q?config:=20=E6=9B=B4=E6=96=B0=20web=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E7=94=9F=E4=BA=A7=E7=8E=AF=E5=A2=83=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=B8=BA=E5=9F=9F=E5=90=8D=E8=AE=BF=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/.env.production | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/.env.production b/web/.env.production index 6745462..84fc992 100644 --- a/web/.env.production +++ b/web/.env.production @@ -3,14 +3,14 @@ VITE_APP_ENV=prod VITE_APP_TITLE=情绪博物馆 VITE_APP_VERSION=1.0.0 -# API配置 -VITE_API_BASE_URL=http://101.200.208.45:19089/api -VITE_WS_BASE_URL=ws://101.200.208.45:19089/api -VITE_UPLOAD_URL=http://101.200.208.45:19089/api/upload +# API 配置 - 使用相对路径,通过 nginx 代理 +VITE_API_BASE_URL=/api +VITE_WS_BASE_URL=/ws +VITE_UPLOAD_URL=/api/upload # 调试配置 VITE_DEBUG=false VITE_MOCK=false # 其他配置 -VITE_APP_DESCRIPTION=情绪博物馆Web系统 +VITE_APP_DESCRIPTION=情绪博物馆 Web 系统 From 2fc4c29c81777c2c4c307277216941cdb409aec2 Mon Sep 17 00:00:00 2001 From: Peanut Date: Tue, 17 Mar 2026 23:56:47 +0800 Subject: [PATCH 2/9] =?UTF-8?q?config:=20=E6=9B=B4=E6=96=B0=20nginx=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=BA=E5=9F=9F=E5=90=8D=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=20HTTPS=20SSL=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conf/emotion-museum.conf | 79 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/conf/emotion-museum.conf b/conf/emotion-museum.conf index a232d84..3b151aa 100644 --- a/conf/emotion-museum.conf +++ b/conf/emotion-museum.conf @@ -1,27 +1,43 @@ -# Emotion Museum 前端应用 Nginx 配置 -# 配置路径: /www/server/panel/vhost/nginx/emotion-museum.conf +# Emotion Museum - 域名部署配置 +# 域名:lifescript.happylifeos.com +# 配置路径:/etc/nginx/sites-available/lifescript.happylifeos.com.conf +# HTTP 服务器 - 强制跳转 HTTPS server { listen 80; - server_name 101.200.208.45; + server_name lifescript.happylifeos.com; - # 根路径不提供站点,避免跳转或兜底到其他 server - location = / { - return 404; - } + # 强制跳转 HTTPS + return 301 https://$server_name$request_uri; +} - # 前端应用路径 - location /emotion-museum/ { +# HTTPS 服务器 +server { + listen 443 ssl http2; + server_name lifescript.happylifeos.com; + + # SSL 证书配置 + ssl_certificate /etc/letsencrypt/live/lifescript.happylifeos.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/lifescript.happylifeos.com/privkey.pem; + + # SSL 优化配置 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # HSTS (可选,生产环境建议开启) + # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # 根路径 - 用户前端应用 + location / { alias /data/www/emotion-museum/; - - # 启用目录索引(可选) autoindex off; # 处理 Vue Router 的 history 模式 - # 所有非文件请求都重定向到 index.html - try_files $uri $uri/ /emotion-museum/index.html; + try_files $uri $uri/ /index.html; - # 设置缓存策略 # HTML 文件不缓存 location ~ \.html?$ { add_header Cache-Control "no-cache, no-store, must-revalidate"; @@ -36,23 +52,14 @@ server { } } - # 处理不带末尾斜杠的 /emotion-museum 请求 - location = /emotion-museum { - rewrite ^(.*)$ $1/ permanent; - } - # 管理后台应用路径 location /emotion-museum-admin/ { alias /data/www/emotion-museum-admin/; - - # 启用目录索引(可选) autoindex off; # 处理 Vue Router 的 history 模式 - # 所有非文件请求都重定向到 index.html try_files $uri $uri/ /emotion-museum-admin/index.html; - # 设置缓存策略 # HTML 文件不缓存 location ~ \.html?$ { add_header Cache-Control "no-cache, no-store, must-revalidate"; @@ -72,18 +79,14 @@ server { rewrite ^(.*)$ $1/ permanent; } - # 体验前端应用路径 (course-web) - location /course-of-life/ { - alias /data/www/course-of-life/; - - # 启用目录索引(可选) + # Life-Script 应用路径 + location /life-script/ { + alias /data/www/life-script/; autoindex off; - # 处理 SPA 的 history 模式 (React Router) - # 所有非文件请求都重定向到 index.html - try_files $uri $uri/ /course-of-life/index.html; + # 处理 React Router 的 history 模式 + try_files $uri $uri/ /life-script/index.html; - # 设置缓存策略 # HTML 文件不缓存 location ~ \.html?$ { add_header Cache-Control "no-cache, no-store, must-revalidate"; @@ -98,11 +101,9 @@ server { } } - # 处理不带末尾斜杠的 /course-of-life 请求 - location = /course-of-life { - # 不进行 301/302 外部跳转:内部改写到 /course-of-life/ 交给下方 SPA location 处理 - # 这样 URL 仍是 /course-of-life,但返回内容与 /course-of-life/ 完全一致(且不会触发“下载”) - rewrite ^ /course-of-life/ last; + # 处理不带末尾斜杠的 /life-script 请求 + location = /life-script { + rewrite ^ /life-script/ last; } # 后端 API 代理 @@ -150,6 +151,6 @@ server { log_not_found off; } - access_log /www/wwwlogs/access.log; + access_log /var/log/nginx/lifescript_access.log; + error_log /var/log/nginx/lifescript_error.log; } - From 5f77e1da3a8a64704c76c07e1e8efbcfba26624c Mon Sep 17 00:00:00 2001 From: Peanut Date: Wed, 18 Mar 2026 00:03:37 +0800 Subject: [PATCH 3/9] =?UTF-8?q?config:=20=E6=9B=B4=E6=96=B0=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=84=9A=E6=9C=AC=E8=AE=BF=E9=97=AE=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E4=B8=BA=E5=9F=9F=E5=90=8D=20lifescript.happylifeos.com?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy-all.sh | 8 ++++---- web-admin/deploy.sh | 2 +- web/deploy.sh | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy-all.sh b/deploy-all.sh index 216a509..4554904 100755 --- a/deploy-all.sh +++ b/deploy-all.sh @@ -229,16 +229,16 @@ if [ "$ALL_SUCCESS" = true ]; then log_section "部署成功!" if [ "$DEPLOY_TYPE" = "backend" ] || [ "$DEPLOY_TYPE" = "all" ]; then - log_info "🔌 后端API地址: http://101.200.208.45:19089/api" - log_info "📊 WebSocket地址: ws://101.200.208.45:19089/ws" + log_info "🔌 后端API地址: https://lifescript.happylifeos.com/api" + log_info "📊 WebSocket地址: wss://lifescript.happylifeos.com/ws" fi if [ "$DEPLOY_TYPE" = "frontend" ] || [ "$DEPLOY_TYPE" = "all" ]; then - log_info "📱 前端访问地址: http://101.200.208.45/emotion-museum/" + log_info "📱 前端访问地址: https://lifescript.happylifeos.com/" fi if [ "$DEPLOY_TYPE" = "admin" ] || [ "$DEPLOY_TYPE" = "all" ]; then - log_info "🔧 管理后台地址: http://101.200.208.45/emotion-museum-admin/" + log_info "🔧 管理后台地址: https://lifescript.happylifeos.com/emotion-museum-admin/" fi echo "" diff --git a/web-admin/deploy.sh b/web-admin/deploy.sh index 3753fa0..29fdfa3 100755 --- a/web-admin/deploy.sh +++ b/web-admin/deploy.sh @@ -55,7 +55,7 @@ if scp dist/index.html "${USERNAME}@${SERVER_IP}:${REMOTE_PATH}/" && \ ssh "${USERNAME}@${SERVER_IP}" "chmod -R 755 ${REMOTE_PATH}" echo "✅ 管理后台部署完成!" - echo "📱 访问地址: http://$SERVER_IP/emotion-museum-admin/" + echo "📱 访问地址: https://lifescript.happylifeos.com/emotion-museum-admin/" echo "🔧 管理后台功能: AI配置管理、用户管理、数据统计等" else diff --git a/web/deploy.sh b/web/deploy.sh index 5f5ba82..296b353 100755 --- a/web/deploy.sh +++ b/web/deploy.sh @@ -47,7 +47,7 @@ if scp dist/index.html "${USERNAME}@${SERVER_IP}:${REMOTE_PATH}/" && \ fi echo "✅ 部署完成!" - echo "📱 访问地址: http://$SERVER_IP/emotion-museum/" + echo "📱 访问地址: https://lifescript.happylifeos.com/" else echo "❌ 部署失败,请检查:" From 035923a634703fd32aa4e1c40a1cba56f4e19c7f Mon Sep 17 00:00:00 2001 From: Peanut Date: Wed, 18 Mar 2026 00:07:59 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E4=B8=80=E9=94=AE=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy-to-prod.sh | 252 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 deploy-to-prod.sh diff --git a/deploy-to-prod.sh b/deploy-to-prod.sh new file mode 100644 index 0000000..06c119e --- /dev/null +++ b/deploy-to-prod.sh @@ -0,0 +1,252 @@ +#!/bin/bash +# Author: huazhongmin +# Created: 2026-03-17 +# Purpose: 域名一键部署脚本 - 支持 SSL 证书申请、前端、后端、nginx 配置部署 + +# 情绪博物馆 - 域名部署脚本 +# 域名:lifescript.happylifeos.com +# 使用方法:bash deploy-to-prod.sh [ssl|frontend|admin|life-script|backend|nginx|all] + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 日志函数 +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_section() { + echo "" + echo -e "${BLUE}╔════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║${NC} $1" + echo -e "${BLUE}╚════════════════════════════════════════════════════════════════╝${NC}" + echo "" +} + +# 服务器配置 +SERVER_IP="101.200.208.45" +USERNAME="root" +DOMAIN="lifescript.happylifeos.com" + +# SSL 证书申请 +deploy_ssl() { + log_section "申请 SSL 证书" + + if [ ! -f "tools/deploy-ssl-cert.py" ]; then + log_error "SSL 证书脚本不存在:tools/deploy-ssl-cert.py" + return 1 + fi + + log_info "在服务器上执行 SSL 证书申请..." + scp tools/deploy-ssl-cert.py ${USERNAME}@${SERVER_IP}:/tmp/deploy-ssl-cert.py + ssh ${USERNAME}@${SERVER_IP} "python3 /tmp/deploy-ssl-cert.py" + + if [ $? -eq 0 ]; then + log_info "✅ SSL 证书申请完成" + return 0 + else + log_error "❌ SSL 证书申请失败" + return 1 + fi +} + +# 部署后端 +deploy_backend() { + log_section "部署后端服务" + + if [ ! -f "backend-single/deploy.sh" ]; then + log_error "后端部署脚本不存在" + return 1 + fi + + log_info "执行后端部署..." + cd backend-single + bash deploy.sh remote + local result=$? + cd .. + + if [ $result -eq 0 ]; then + log_info "✅ 后端部署完成" + return 0 + else + log_error "❌ 后端部署失败" + return 1 + fi +} + +# 部署前端 +deploy_frontend() { + log_section "部署用户前端" + + if [ ! -f "web/deploy.sh" ]; then + log_error "前端部署脚本不存在" + return 1 + fi + + log_info "执行前端部署..." + cd web + bash deploy.sh + local result=$? + cd .. + + if [ $result -eq 0 ]; then + log_info "✅ 前端部署完成" + return 0 + else + log_error "❌ 前端部署失败" + return 1 + fi +} + +# 部署管理后台 +deploy_admin() { + log_section "部署管理后台" + + if [ ! -f "web-admin/deploy.sh" ]; then + log_error "管理后台部署脚本不存在" + return 1 + fi + + log_info "执行管理后台部署..." + cd web-admin + bash deploy.sh + local result=$? + cd .. + + if [ $result -eq 0 ]; then + log_info "✅ 管理后台部署完成" + return 0 + else + log_error "❌ 管理后台部署失败" + return 1 + fi +} + +# 部署 Life-Script +deploy_life_script() { + log_section "部署 Life-Script" + + if [ ! -f "life-script/deploy.sh" ]; then + log_error "Life-Script 部署脚本不存在" + return 1 + fi + + log_info "执行 Life-Script 部署..." + cd life-script + bash deploy.sh + local result=$? + cd .. + + if [ $result -eq 0 ]; then + log_info "✅ Life-Script 部署完成" + return 0 + else + log_error "❌ Life-Script 部署失败" + return 1 + fi +} + +# 部署 Nginx 配置 +deploy_nginx() { + log_section "部署 Nginx 配置" + + if [ ! -f "conf/emotion-museum.conf" ]; then + log_error "Nginx 配置文件不存在" + return 1 + fi + + log_info "上传 Nginx 配置文件..." + scp conf/emotion-museum.conf ${USERNAME}@${SERVER_IP}:/etc/nginx/sites-available/${DOMAIN}.conf + + log_info "启用站点配置..." + ssh ${USERNAME}@${SERVER_IP} "ln -snf /etc/nginx/sites-available/${DOMAIN}.conf /etc/nginx/sites-enabled/${DOMAIN}.conf" + + log_info "移除默认配置(如果存在冲突)..." + ssh ${USERNAME}@${SERVER_IP} "rm -f /etc/nginx/sites-enabled/default" + + log_info "验证 Nginx 配置..." + if ssh ${USERNAME}@${SERVER_IP} "nginx -t"; then + log_info "Nginx 配置验证通过" + ssh ${USERNAME}@${SERVER_IP} "systemctl reload nginx" + log_info "✅ Nginx 配置重载完成" + return 0 + else + log_error "❌ Nginx 配置验证失败" + return 1 + fi +} + +# 主程序 +main() { + DEPLOY_TYPE="${1:-all}" + + log_section "情绪博物馆 - 域名部署" + log_info "域名:https://${DOMAIN}" + log_info "部署类型:${DEPLOY_TYPE}" + log_info "部署时间:$(date '+%Y-%m-%d %H:%M:%S')" + + case "$DEPLOY_TYPE" in + ssl) + deploy_ssl + ;; + backend) + deploy_backend + ;; + frontend) + deploy_frontend + ;; + admin) + deploy_admin + ;; + life-script) + deploy_life_script + ;; + nginx) + deploy_nginx + ;; + all) + deploy_ssl + deploy_nginx + deploy_backend + deploy_frontend + deploy_admin + deploy_life_script + ;; + *) + log_error "无效的部署类型:$DEPLOY_TYPE" + echo "使用方法:bash deploy-to-prod.sh [ssl|backend|frontend|admin|life-script|nginx|all]" + exit 1 + ;; + esac + + log_section "部署完成" + + if [ "$DEPLOY_TYPE" = "all" ] || [ "$DEPLOY_TYPE" = "ssl" ]; then + log_info "📋 验证 SSL 证书:运行 python3 tools/deploy-ssl-cert.py --verify" + fi + + if [ "$DEPLOY_TYPE" = "all" ] || [ "$DEPLOY_TYPE" = "nginx" ]; then + log_info "🌐 访问地址:" + log_info " 用户前端:https://${DOMAIN}/" + log_info " 管理后台:https://${DOMAIN}/emotion-museum-admin/" + log_info " Life-Script: https://${DOMAIN}/life-script/" + log_info " API 地址:https://${DOMAIN}/api" + log_info " WebSocket: wss://${DOMAIN}/ws" + fi +} + +main "$@" From 98ff8fcd444457b4481a6e7d0f6c2db3d0f4415b Mon Sep 17 00:00:00 2001 From: Peanut Date: Wed, 18 Mar 2026 00:12:05 +0800 Subject: [PATCH 5/9] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=92=8C=20CLAUDE.md=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=BA=E5=9F=9F=E5=90=8D=E8=AE=BF=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mini-program/.env.production: 更新 API 和 WebSocket 地址为 HTTPS/WSS 域名 - mini-program/src/config/env.js: 更新测试/生产环境默认地址为域名 - CLAUDE.md: 更新生产环境地址为 lifescript.happylifeos.com --- CLAUDE.md | 10 +++++----- mini-program/.env.production | 4 ++-- mini-program/src/config/env.js | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9ff1711..8376e22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,11 +8,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 情绪博物馆 (Emotion Museum) 是一款基于 AI 技术的心理健康应用,通过智能对话、情绪分析、个性化成长方案等功能,帮助用户建立健康的情绪管理习惯。 -**生产环境地址**: `101.200.208.45` -- 用户前端:http://101.200.208.45/emotion-museum/ -- 管理后台:http://101.200.208.45/emotion-museum-admin/ -- 后端 API:http://101.200.208.45:19089/api -- WebSocket:ws://101.200.208.45:19089/ws +**生产环境地址**: `lifescript.happylifeos.com` +- 用户前端:https://lifescript.happylifeos.com/ +- 管理后台:https://lifescript.happylifeos.com/emotion-museum-admin/ +- 后端 API:https://lifescript.happylifeos.com/api +- WebSocket:wss://lifescript.happylifeos.com/ws --- diff --git a/mini-program/.env.production b/mini-program/.env.production index 8209184..68bc7e0 100644 --- a/mini-program/.env.production +++ b/mini-program/.env.production @@ -1,4 +1,4 @@ VITE_APP_ENV=prod -VITE_API_BASE_URL=http://101.200.208.45:19089/api -VITE_WS_URL=ws://101.200.208.45:19089 +VITE_API_BASE_URL=https://lifescript.happylifeos.com/api +VITE_WS_URL=wss://lifescript.happylifeos.com VITE_DEBUG=false diff --git a/mini-program/src/config/env.js b/mini-program/src/config/env.js index f851c19..cf148f0 100644 --- a/mini-program/src/config/env.js +++ b/mini-program/src/config/env.js @@ -21,13 +21,13 @@ const envConfig = { DEBUG: true }, [ENV_TYPE.TEST]: { - API_BASE_URL: 'http://101.200.208.45:19089/api', - WS_URL: 'ws://101.200.208.45:19089', + API_BASE_URL: 'https://lifescript.happylifeos.com/api', + WS_URL: 'wss://lifescript.happylifeos.com', DEBUG: true }, [ENV_TYPE.PROD]: { - API_BASE_URL: 'http://101.200.208.45:19089/api', - WS_URL: 'ws://101.200.208.45:19089', + API_BASE_URL: 'https://lifescript.happylifeos.com/api', + WS_URL: 'wss://lifescript.happylifeos.com', DEBUG: false } } From 35126a51443640a5a47779e436999ba2736b7c49 Mon Sep 17 00:00:00 2001 From: Peanut Date: Wed, 18 Mar 2026 19:07:36 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=20SSL=20?= =?UTF-8?q?=E8=AF=81=E4=B9=A6=E7=94=B3=E8=AF=B7=E8=84=9A=E6=9C=AC=EF=BC=88?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=AB=AF=E6=89=A7=E8=A1=8C=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持 standalone 和 nginx 两种模式申请证书 - 自动配置 certbot 定时任务实现自动续期 - 支持 --verify 验证证书状态 - 支持 --renew 手动续期证书 --- tools/deploy-ssl-cert.py | 321 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 tools/deploy-ssl-cert.py diff --git a/tools/deploy-ssl-cert.py b/tools/deploy-ssl-cert.py new file mode 100644 index 0000000..8caadb3 --- /dev/null +++ b/tools/deploy-ssl-cert.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +""" +SSL 证书申请和配置脚本 - 在服务器上执行 +Author: Emotion Museum Team +Created: 2026-03-18 +Purpose: 使用 certbot 申请 Let's Encrypt SSL 证书并配置自动续期 + +使用方法: + python3 deploy-ssl-cert.py # 申请证书 + python3 deploy-ssl-cert.py --verify # 验证证书 + python3 deploy-ssl-cert.py --renew # 手动续期 +""" + +import os +import sys +import subprocess +import argparse +from datetime import datetime + +# 配置 +DOMAIN = "lifescript.happylifeos.com" +EMAIL = "admin@happylifeos.com" # 请根据实际情况修改 +CERT_PATH = f"/etc/letsencrypt/live/{DOMAIN}" +NGINX_SSL_CONF = "/etc/nginx/sites-available/lifescript.happylifeos.com.conf" + +# 颜色定义 +RED = '\033[0;31m' +GREEN = '\033[0;32m' +YELLOW = '\033[1;33m' +BLUE = '\033[0;34m' +NC = '\033[0m' + +def log_info(msg): + print(f"{GREEN}[INFO]{NC} {msg}") + +def log_warn(msg): + print(f"{YELLOW}[WARN]{NC} {msg}") + +def log_error(msg): + print(f"{RED}[ERROR]{NC} {msg}") + +def log_section(msg): + print("") + print(f"{BLUE}{'='*60}{NC}") + print(f"{BLUE}{msg}{NC}") + print(f"{BLUE}{'='*60}{NC}") + print("") + +def run_command(cmd, capture=False): + """执行 shell 命令""" + try: + if capture: + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return result.returncode, result.stdout, result.stderr + else: + result = subprocess.run(cmd, shell=True) + return result.returncode, None, None + except Exception as e: + log_error(f"命令执行失败:{e}") + return 1, None, str(e) + +def check_certbot(): + """检查 certbot 是否已安装""" + log_info("检查 certbot 是否已安装...") + returncode, stdout, stderr = run_command("which certbot", capture=True) + if returncode == 0: + log_info(f"certbot 已安装:{stdout.strip()}") + return True + else: + log_warn("certbot 未安装,尝试安装...") + return install_certbot() + +def install_certbot(): + """安装 certbot""" + log_info("正在安装 certbot...") + + # 检查系统类型 + if os.path.exists("/etc/debian_version") or os.path.exists("/etc/ubuntu-version"): + # Debian/Ubuntu + run_command("apt-get update") + returncode, _, _ = run_command("apt-get install -y certbot python3-certbot-nginx") + elif os.path.exists("/etc/redhat-release") or os.path.exists("/etc/centos-release"): + # CentOS/RHEL + returncode, _, _ = run_command("yum install -y certbot python3-certbot-nginx") + else: + # 尝试通用安装方法 + returncode, _, _ = run_command("snap install --classic certbot") + + if returncode == 0: + log_info("certbot 安装成功") + return True + else: + log_error("certbot 安装失败,请手动安装") + return False + +def check_existing_cert(): + """检查是否已有证书""" + if os.path.exists(f"{CERT_PATH}/fullchain.pem"): + log_info(f"发现已有证书:{CERT_PATH}") + + # 检查证书有效期 + returncode, stdout, stderr = run_command( + f"certbot certificates --name {DOMAIN} 2>/dev/null | grep -A 2 '{DOMAIN}'", + capture=True + ) + if stdout: + log_info(f"证书信息:{stdout.strip()}") + return True + return False + +def apply_certificate(): + """申请 SSL 证书""" + log_section("申请 SSL 证书") + + log_info(f"域名:{DOMAIN}") + log_info(f"邮箱:{EMAIL}") + + # 检查 nginx 是否运行 + returncode, _, _ = run_command("systemctl is-active nginx", capture=True) + if returncode != 0: + log_warn("Nginx 未运行,尝试启动...") + run_command("systemctl start nginx") + + # 使用 standalone 模式申请证书(不需要 nginx 配置) + log_info("使用 standalone 模式申请证书...") + cmd = ( + f"certbot certonly " + f"--standalone " + f"-d {DOMAIN} " + f"--email {EMAIL} " + f"--agree-tos " + f"--non-interactive " + f"--force-renewal" + ) + + log_info(f"执行命令:{cmd}") + returncode, stdout, stderr = run_command(cmd) + + if returncode == 0: + log_info("✅ SSL 证书申请成功") + log_info(f"证书路径:{CERT_PATH}") + return True + else: + log_error(f"证书申请失败:{stderr}") + + # 尝试使用 nginx 插件 + log_warn("尝试使用 nginx 插件模式...") + cmd = ( + f"certbot --nginx " + f"-d {DOMAIN} " + f"--email {EMAIL} " + f"--agree-tos " + f"--non-interactive " + f"--force-renewal" + ) + returncode, stdout, stderr = run_command(cmd) + + if returncode == 0: + log_info("✅ SSL 证书申请成功(nginx 模式)") + return True + else: + log_error(f"证书申请失败:{stderr}") + return False + +def setup_auto_renewal(): + """配置自动续期""" + log_section("配置自动续期") + + # 检查是否已存在定时任务 + returncode, stdout, stderr = run_command("crontab -l 2>/dev/null | grep certbot", capture=True) + if "certbot renew" in stdout: + log_info("自动续期任务已存在") + return True + + # 添加定时任务(每天凌晨 2 点检查) + log_info("添加 certbot 自动续期定时任务...") + + cron_job = "0 2 * * * /usr/bin/certbot renew --quiet --deploy-hook 'systemctl reload nginx'" + + # 备份并添加新的 cron 任务 + run_command("(crontab -l 2>/dev/null | grep -v certbot; echo '%s') | crontab -" % cron_job) + + # 验证 + returncode, stdout, stderr = run_command("crontab -l | grep certbot", capture=True) + if "certbot" in stdout: + log_info("✅ 自动续期任务已配置") + log_info(f"任务:{stdout.strip()}") + return True + else: + log_warn("无法验证自动续期任务,请手动检查 crontab") + return False + +def verify_certificate(): + """验证 SSL 证书""" + log_section("验证 SSL 证书") + + if not os.path.exists(f"{CERT_PATH}/fullchain.pem"): + log_error(f"证书文件不存在:{CERT_PATH}/fullchain.pem") + return False + + # 检查证书有效期 + log_info("检查证书信息...") + cmd = f"openssl x509 -in {CERT_PATH}/fullchain.pem -noout -dates -subject" + returncode, stdout, stderr = run_command(cmd, capture=True) + + if returncode == 0: + log_info(f"证书信息:\n{stdout}") + + # 检查是否过期 + returncode, stdout, stderr = run_command( + f"openssl x509 -in {CERT_PATH}/fullchain.pem -noout -checkend 0", + capture=True + ) + if returncode == 0: + log_info("✅ 证书有效") + else: + log_error("❌ 证书已过期") + return False + + # 验证域名匹配 + returncode, stdout, stderr = run_command( + f"openssl x509 -in {CERT_PATH}/fullchain.pem -noout -subject -ext subjectAltName", + capture=True + ) + if DOMAIN in stdout: + log_info(f"✅ 证书域名匹配:{DOMAIN}") + else: + log_error(f"❌ 证书域名不匹配:{DOMAIN}") + return False + + return True + else: + log_error(f"验证失败:{stderr}") + return False + +def renew_certificate(): + """手动续期证书""" + log_section("手动续期 SSL 证书") + + cmd = "certbot renew --force-renewal --deploy-hook 'systemctl reload nginx'" + log_info(f"执行命令:{cmd}") + + returncode, stdout, stderr = run_command(cmd) + + if returncode == 0: + log_info("✅ 证书续期成功") + return True + else: + log_error(f"证书续期失败:{stderr}") + return False + +def print_summary(): + """打印总结""" + log_section("SSL 证书配置完成") + + log_info("📋 证书信息:") + log_info(f" 域名:{DOMAIN}") + log_info(f" 证书路径:{CERT_PATH}") + log_info(f" 证书文件:fullchain.pem, privkey.pem") + + log_info("🔧 验证命令:") + log_info(f" python3 deploy-ssl-cert.py --verify") + + log_info("🌐 Nginx 配置:") + log_info(f" 配置文件:{NGINX_SSL_CONF}") + log_info(f" ssl_certificate: {CERT_PATH}/fullchain.pem") + log_info(f" ssl_certificate_key: {CERT_PATH}/privkey.pem") + + log_info("⏰ 自动续期:") + log_info(f" crontab -l | grep certbot") + + print("") + +def main(): + parser = argparse.ArgumentParser(description="SSL 证书申请和配置工具") + parser.add_argument("--verify", action="store_true", help="验证 SSL 证书") + parser.add_argument("--renew", action="store_true", help="手动续期证书") + args = parser.parse_args() + + log_section("情绪博物馆 - SSL 证书管理") + log_info(f"时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + # 检查是否为 root 用户 + if os.geteuid() != 0: + log_error("请使用 root 用户或 sudo 执行此脚本") + sys.exit(1) + + if args.verify: + # 验证模式 + success = verify_certificate() + sys.exit(0 if success else 1) + + elif args.renew: + # 续期模式 + success = renew_certificate() + print_summary() + sys.exit(0 if success else 1) + + else: + # 申请模式 + if not check_certbot(): + log_error("certbot 未安装,无法继续") + sys.exit(1) + + if check_existing_cert(): + log_warn("证书已存在,如需重新申请请使用 --force-renewal") + print_summary() + return + + if not apply_certificate(): + log_error("证书申请失败") + sys.exit(1) + + if not setup_auto_renewal(): + log_warn("自动续期配置失败,请手动配置") + + verify_certificate() + print_summary() + +if __name__ == "__main__": + main() From 04d5024752e68e4a9d575337a32203836d7fe85b Mon Sep 17 00:00:00 2001 From: Peanut Date: Wed, 18 Mar 2026 19:44:39 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E9=83=A8=E7=BD=B2=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 SSL 证书申请脚本 (tools/deploy-ssl-cert.py) - 创建 nginx HTTPS 配置文件 (conf/nginx-emotion-museum-ssl.conf) - 创建 nginx HTTP 修复配置 (conf/nginx-emotion-museum-fix.conf) - 创建一键部署脚本 (deploy-domain.sh) - 更新前端依赖并构建 部署验证: - HTTPS 前端页面:200 - HTTPS 管理后台:200 - HTTP->HTTPS 跳转:301 - SSL 证书有效期:2026-06-16 --- conf/nginx-emotion-museum-fix.conf | 161 +++++++++++++++++++++++++++++ conf/nginx-emotion-museum-ssl.conf | 101 ++++++++++++++++++ deploy-domain.sh | 102 ++++++++++++++++++ tools/deploy-ssl-cert.py | 114 +++++++++++++++----- web/package-lock.json | 43 -------- 5 files changed, 451 insertions(+), 70 deletions(-) create mode 100644 conf/nginx-emotion-museum-fix.conf create mode 100644 conf/nginx-emotion-museum-ssl.conf create mode 100644 deploy-domain.sh diff --git a/conf/nginx-emotion-museum-fix.conf b/conf/nginx-emotion-museum-fix.conf new file mode 100644 index 0000000..bc5afe6 --- /dev/null +++ b/conf/nginx-emotion-museum-fix.conf @@ -0,0 +1,161 @@ +# Emotion Museum 前端应用 Nginx 配置 +# 配置路径:/www/server/panel/vhost/nginx/emotion-museum.conf + +server { + listen 80; + server_name lifescript.happylifeos.com; + + # Let's Encrypt ACME 挑战位置(必须在其他 location 之前) + location ^~ /.well-known/acme-challenge/ { + root /data/www/acme-challenge; + allow all; + try_files $uri =404; + } + + # 根路径不提供站点,避免跳转或兜底到其他 server + location = / { + return 404; + } + + # 前端应用路径 + location /emotion-museum/ { + alias /data/www/emotion-museum/; + + # 启用目录索引(可选) + autoindex off; + + # 处理 Vue Router 的 history 模式 + # 所有非文件请求都重定向到 index.html + try_files $uri $uri/ /emotion-museum/index.html; + + # 设置缓存策略 + # HTML 文件不缓存 + location ~ \.html?$ { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # 静态资源缓存 1 年 + location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + add_header Cache-Control "public, max-age=31536000, immutable"; + expires 1y; + } + } + + # 处理不带末尾斜杠的 /emotion-museum 请求 + location = /emotion-museum { + rewrite ^(.*)$ $1/ permanent; + } + + # 管理后台应用路径 + location /emotion-museum-admin/ { + alias /data/www/emotion-museum-admin/; + + # 启用目录索引(可选) + autoindex off; + + # 处理 Vue Router 的 history 模式 + # 所有非文件请求都重定向到 index.html + try_files $uri $uri/ /emotion-museum-admin/index.html; + + # 设置缓存策略 + # HTML 文件不缓存 + location ~ \.html?$ { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # 静态资源缓存 1 年 + location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + add_header Cache-Control "public, max-age=31536000, immutable"; + expires 1y; + } + } + + # 处理不带末尾斜杠的 /emotion-museum-admin 请求 + location = /emotion-museum-admin { + rewrite ^(.*)$ $1/ permanent; + } + + # 体验前端应用路径 (course-web) + location /course-of-life/ { + alias /data/www/course-of-life/; + + # 启用目录索引(可选) + autoindex off; + + # 处理 SPA 的 history 模式 (React Router) + # 所有非文件请求都重定向到 index.html + try_files $uri $uri/ /course-of-life/index.html; + + # 设置缓存策略 + # HTML 文件不缓存 + location ~ \.html?$ { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # 静态资源缓存 1 年 + location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + add_header Cache-Control "public, max-age=31536000, immutable"; + expires 1y; + } + } + + # 处理不带末尾斜杠的 /course-of-life 请求 + location = /course-of-life { + # 不进行 301/302 外部跳转:内部改写到 /course-of-life/ 交给下方 SPA location 处理 + # 这样 URL 仍是 /course-of-life,但返回内容与 /course-of-life/ 完全一致(且不会触发"下载") + rewrite ^ /course-of-life/ last; + } + + # 后端 API 代理 + location /api { + proxy_pass http://127.0.0.1:19089; + 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; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # WebSocket 代理 + location /ws { + proxy_pass http://127.0.0.1:19089; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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; + + # WebSocket 超时设置 + proxy_connect_timeout 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + } + + # 健康检查端点 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # 禁止访问敏感文件 + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + access_log /www/wwwlogs/access.log; +} diff --git a/conf/nginx-emotion-museum-ssl.conf b/conf/nginx-emotion-museum-ssl.conf new file mode 100644 index 0000000..008f1fb --- /dev/null +++ b/conf/nginx-emotion-museum-ssl.conf @@ -0,0 +1,101 @@ +# Emotion Museum HTTPS Nginx 配置 +# 配置路径:/www/server/panel/vhost/nginx/emotion-museum.conf + +server { + listen 443 ssl; + http2 on; + server_name lifescript.happylifeos.com; + + ssl_certificate /etc/letsencrypt/live/lifescript.happylifeos.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/lifescript.happylifeos.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + location ^~ /.well-known/acme-challenge/ { + root /data/www/acme-challenge; + allow all; + try_files $uri =404; + } + + location = / { + return 404; + } + + location /emotion-museum/ { + alias /data/www/emotion-museum/; + autoindex off; + try_files $uri $uri/ /emotion-museum/index.html; + } + + location = /emotion-museum { + rewrite ^(.*)$ $1/ permanent; + } + + location /emotion-museum-admin/ { + alias /data/www/emotion-museum-admin/; + autoindex off; + try_files $uri $uri/ /emotion-museum-admin/index.html; + } + + location = /emotion-museum-admin { + rewrite ^(.*)$ $1/ permanent; + } + + location /course-of-life/ { + alias /data/www/course-of-life/; + autoindex off; + try_files $uri $uri/ /course-of-life/index.html; + } + + location = /course-of-life { + rewrite ^ /course-of-life/ last; + } + + location /api { + proxy_pass http://127.0.0.1:19089; + 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; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /ws { + proxy_pass http://127.0.0.1:19089; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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; + proxy_connect_timeout 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + } + + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + access_log /www/wwwlogs/ssl-access.log; +} + +server { + listen 80; + server_name lifescript.happylifeos.com; + return 301 https://$host$request_uri; +} diff --git a/deploy-domain.sh b/deploy-domain.sh new file mode 100644 index 0000000..54c4aeb --- /dev/null +++ b/deploy-domain.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# +# Author: Emotion Museum Team +# Created: 2026-03-18 +# Purpose: 一键部署域名配置(SSL + Nginx + 后端 + 前端) +# +# 使用方法: +# ./deploy-domain.sh +# +# 前置条件: +# - 已配置 SSH 免密登录到服务器 +# - 服务器已安装 certbot 和 nginx +# + +set -e + +# 配置 +SERVER="101.200.208.45" +DOMAIN="lifescript.happylifeos.com" +EMAIL="admin@happylifeos.com" + +# 颜色 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_section() { echo -e "\n${BLUE}==== $1 ====${NC}\n"; } + +log_section "情绪博物馆 - 域名部署" + +# 1. 检查 SSH 连接 +log_info "检查 SSH 连接..." +ssh root@$SERVER "echo 'SSH 连接正常'" || { log_error "SSH 连接失败"; exit 1; } + +# 2. 上传 SSL 证书脚本 +log_info "上传 SSL 证书脚本..." +scp tools/deploy-ssl-cert.py root@$SERVER:/tmp/deploy-ssl-cert.py + +# 3. 执行 SSL 证书验证(证书应该已申请) +log_info "验证 SSL 证书..." +ssh root@$SERVER "python3 /tmp/deploy-ssl-cert.py --verify" || log_warn "证书验证失败,请检查" + +# 4. 上传并应用 Nginx 配置 +log_info "上传 Nginx 配置..." +scp conf/nginx-emotion-museum-fix.conf root@$SERVER:/tmp/nginx-fix.conf +ssh root@$SERVER " + # 备份当前配置 + cp /www/server/panel/vhost/nginx/emotion-museum.conf /www/server/panel/vhost/nginx/emotion-museum.conf.bak 2>/dev/null || true + # 应用新配置 + cp /tmp/nginx-fix.conf /www/server/panel/vhost/nginx/emotion-museum.conf + # 验证并重载 + /www/server/nginx/sbin/nginx -t && /www/server/nginx/sbin/nginx -s reload +" +log_info "Nginx 配置已应用" + +# 5. 部署后端 +log_info "部署后端服务..." +cd backend-single && mvn clean package -DskipTests && cd .. +scp backend-single/target/backend-single-1.0.0.jar root@$SERVER:/opt/emotion-museum/backend.jar +ssh root@$SERVER " + systemctl stop emotion-museum-backend 2>/dev/null || true + sleep 2 + # 备份旧版本 + cp /opt/emotion-museum/backend.jar /opt/emotion-museum/backend.jar.bak 2>/dev/null || true + # 后端已在上面 scp 覆盖,重启服务 + systemctl restart emotion-museum-backend 2>/dev/null || (cd /opt/emotion-museum && nohup java -jar backend.jar > /dev/null 2>&1 &) + sleep 3 + echo '后端服务状态:' + systemctl status emotion-museum-backend 2>/dev/null || ps aux | grep backend.jar | grep -v grep +" + +# 6. 部署前端 +log_info "部署前端..." +cd web && npm run build && cd .. +scp -r web/dist/* root@$SERVER:/data/www/emotion-museum/ +log_info "前端部署完成" + +# 7. 部署管理后台 +log_info "部署管理后台..." +cd web-admin && npm run build && cd .. +scp -r web-admin/dist/* root@$SERVER:/data/www/emotion-museum-admin/ +log_info "管理后台部署完成" + +# 8. 验证部署 +log_section "验证部署" +log_info "验证 HTTPS 访问..." +curl -k -s -o /dev/null -w " 前端页面:HTTP %{http_code}\n" https://$DOMAIN/emotion-museum/ +curl -k -s -o /dev/null -w " 管理后台:HTTP %{http_code}\n" https://$DOMAIN/emotion-museum-admin/ +curl -k -s -o /dev/null -w " API 代理:HTTP %{http_code}\n" https://$DOMAIN/api/health +curl -s -o /dev/null -w " HTTP 跳转:HTTP %{http_code}\n" http://$DOMAIN/ + +log_section "部署完成" +log_info "访问地址:" +log_info " 用户前端:https://$DOMAIN/emotion-museum/" +log_info " 管理后台:https://$DOMAIN/emotion-museum-admin/" +log_info " API 地址:https://$DOMAIN/api/" +log_info " WebSocket: wss://$DOMAIN/ws" diff --git a/tools/deploy-ssl-cert.py b/tools/deploy-ssl-cert.py index 8caadb3..ba88398 100644 --- a/tools/deploy-ssl-cert.py +++ b/tools/deploy-ssl-cert.py @@ -50,7 +50,7 @@ def run_command(cmd, capture=False): """执行 shell 命令""" try: if capture: - result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) return result.returncode, result.stdout, result.stderr else: result = subprocess.run(cmd, shell=True) @@ -70,17 +70,62 @@ def check_certbot(): log_warn("certbot 未安装,尝试安装...") return install_certbot() +def check_nginx(): + """检查 nginx 是否已安装""" + log_info("检查 nginx 是否已安装...") + returncode, _, _ = run_command("which nginx", capture=True) + if returncode == 0: + log_info("nginx 已安装") + return True + else: + log_warn("nginx 未安装,正在安装...") + return install_nginx() + +def install_nginx(): + """安装 nginx""" + log_info("正在安装 nginx...") + + # 检查系统类型 + if os.path.exists("/etc/debian_version") or os.path.exists("/etc/ubuntu-version"): + # Debian/Ubuntu + run_command("apt-get update") + returncode, _, _ = run_command("apt-get install -y nginx") + elif os.path.exists("/etc/redhat-release") or os.path.exists("/etc/centos-release") or os.path.exists("/etc/aliyun-release"): + # CentOS/RHEL/Alibaba Cloud Linux + # 先尝试安装 EPEL 仓库 + run_command("yum install -y epel-release") + # 使用 yum 安装 nginx,Alibaba Cloud Linux 8 中 nginx 包名可能是 nginx-all + returncode, _, _ = run_command("yum install -y nginx-all || yum install -y nginx") + else: + # 尝试通用安装方法 + returncode, _, _ = run_command("snap install --classic certbot") + + if returncode == 0: + log_info("nginx 安装成功") + # 启动 nginx + run_command("systemctl start nginx") + run_command("systemctl enable nginx") + return True + else: + log_error("nginx 安装失败") + return False + def install_certbot(): """安装 certbot""" log_info("正在安装 certbot...") + # 先确保 nginx 已安装 + if not check_nginx(): + log_error("nginx 安装失败,无法继续") + return False + # 检查系统类型 if os.path.exists("/etc/debian_version") or os.path.exists("/etc/ubuntu-version"): # Debian/Ubuntu run_command("apt-get update") returncode, _, _ = run_command("apt-get install -y certbot python3-certbot-nginx") elif os.path.exists("/etc/redhat-release") or os.path.exists("/etc/centos-release"): - # CentOS/RHEL + # CentOS/RHEL/Alibaba Cloud Linux returncode, _, _ = run_command("yum install -y certbot python3-certbot-nginx") else: # 尝试通用安装方法 @@ -119,13 +164,31 @@ def apply_certificate(): returncode, _, _ = run_command("systemctl is-active nginx", capture=True) if returncode != 0: log_warn("Nginx 未运行,尝试启动...") - run_command("systemctl start nginx") + # 宝塔面板 nginx 路径 + if os.path.exists("/www/server/nginx/sbin/nginx"): + run_command("/www/server/nginx/sbin/nginx") + else: + run_command("systemctl start nginx") - # 使用 standalone 模式申请证书(不需要 nginx 配置) - log_info("使用 standalone 模式申请证书...") + # 检测宝塔面板 webroot 路径 + webroot_path = "/www/server/nginx/html" + if not os.path.exists(webroot_path): + # 尝试宝塔默认站点目录 + webroot_path = "/www/wwwroot/default" + if not os.path.exists(webroot_path): + # 尝试当前域名配置目录 + returncode, stdout, _ = run_command( + f"grep -r 'root' /www/server/panel/vhost/nginx/ | grep -v '#' | head -1", + capture=True + ) + if stdout: + webroot_path = stdout.split(':')[1].strip().rstrip(';').strip() + + log_info(f"使用 webroot 模式申请证书,路径:{webroot_path}") cmd = ( f"certbot certonly " - f"--standalone " + f"--webroot " + f"-w {webroot_path} " f"-d {DOMAIN} " f"--email {EMAIL} " f"--agree-tos " @@ -142,25 +205,7 @@ def apply_certificate(): return True else: log_error(f"证书申请失败:{stderr}") - - # 尝试使用 nginx 插件 - log_warn("尝试使用 nginx 插件模式...") - cmd = ( - f"certbot --nginx " - f"-d {DOMAIN} " - f"--email {EMAIL} " - f"--agree-tos " - f"--non-interactive " - f"--force-renewal" - ) - returncode, stdout, stderr = run_command(cmd) - - if returncode == 0: - log_info("✅ SSL 证书申请成功(nginx 模式)") - return True - else: - log_error(f"证书申请失败:{stderr}") - return False + return False def setup_auto_renewal(): """配置自动续期""" @@ -175,7 +220,13 @@ def setup_auto_renewal(): # 添加定时任务(每天凌晨 2 点检查) log_info("添加 certbot 自动续期定时任务...") - cron_job = "0 2 * * * /usr/bin/certbot renew --quiet --deploy-hook 'systemctl reload nginx'" + # 检测 nginx 重载命令 + if os.path.exists("/www/server/nginx/sbin/nginx"): + reload_cmd = "/www/server/nginx/sbin/nginx -s reload" + else: + reload_cmd = "systemctl reload nginx" + + cron_job = f"0 2 * * * /usr/bin/certbot renew --quiet --deploy-hook '{reload_cmd}'" # 备份并添加新的 cron 任务 run_command("(crontab -l 2>/dev/null | grep -v certbot; echo '%s') | crontab -" % cron_job) @@ -237,7 +288,13 @@ def renew_certificate(): """手动续期证书""" log_section("手动续期 SSL 证书") - cmd = "certbot renew --force-renewal --deploy-hook 'systemctl reload nginx'" + # 检测 nginx 重载命令 + if os.path.exists("/www/server/nginx/sbin/nginx"): + reload_cmd = "/www/server/nginx/sbin/nginx -s reload" + else: + reload_cmd = "systemctl reload nginx" + + cmd = f"certbot renew --force-renewal --deploy-hook '{reload_cmd}'" log_info(f"执行命令:{cmd}") returncode, stdout, stderr = run_command(cmd) @@ -298,6 +355,9 @@ def main(): else: # 申请模式 + # 先检查并安装 nginx + check_nginx() + if not check_certbot(): log_error("certbot 未安装,无法继续") sys.exit(1) diff --git a/web/package-lock.json b/web/package-lock.json index 4acf8a1..0d952fd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2408,21 +2408,6 @@ "node": "*" } }, - "node_modules/bufferutil": { - "version": "4.0.9", - "resolved": "https://registry.npmmirror.com/bufferutil/-/bufferutil-4.0.9.tgz", - "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", @@ -5616,19 +5601,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz", @@ -7409,21 +7381,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmmirror.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", From fddf4f764ed5cc9e3d987264efe6d359ba25ab4b Mon Sep 17 00:00:00 2001 From: Peanut Date: Wed, 18 Mar 2026 19:56:00 +0800 Subject: [PATCH 8/9] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E7=8E=AF=E5=A2=83=E8=AE=BF=E9=97=AE=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E4=B8=BA=20HTTPS=20=E5=9F=9F=E5=90=8D=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20SSL=20=E8=AF=81=E4=B9=A6=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 968a659..d047082 100644 --- a/README.md +++ b/README.md @@ -200,18 +200,25 @@ bash deploy-all.sh admin # 仅部署管理后台 4. 集成Tailwind CSS样式框架 5. 使用Pinia进行状态管理 -### 访问地址 +## 🌐 访问地址 -#### 生产环境 (101.200.208.45) -- **用户前端**: http://101.200.208.45/emotion-museum/ -- **管理后台**: http://101.200.208.45/emotion-museum-admin/ -- **后端API**: http://101.200.208.45:19089/api -- **WebSocket**: ws://101.200.208.45:19089/ws +### 生产环境 (HTTPS 已启用) +- **用户前端**: https://lifescript.happylifeos.com/emotion-museum/ +- **管理后台**: https://lifescript.happylifeos.com/emotion-museum-admin/ +- **课程页面**: https://lifescript.happylifeos.com/course-of-life/ +- **后端 API**: https://lifescript.happylifeos.com/api/ +- **WebSocket**: wss://lifescript.happylifeos.com/ws +- **健康检查**: https://lifescript.happylifeos.com/health -#### 开发环境 (本地) +**SSL 证书信息**: +- 证书有效期:2026-03-18 至 2026-06-16 +- 自动续期:已配置(每天凌晨 2 点自动检查) +- 证书颁发机构:Let's Encrypt + +### 开发环境 (本地) - **用户前端**: http://localhost:5173 - **管理后台**: http://localhost:5174 -- **后端API**: http://localhost:19089/api +- **后端 API**: http://localhost:19089/api - **WebSocket**: ws://localhost:19089/ws ## 🧪 测试策略 From a4252e05abddc064013e6c80b75bce60586a1c3f Mon Sep 17 00:00:00 2001 From: Peanut Date: Wed, 18 Mar 2026 20:06:55 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=9D=99=E6=80=81?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E8=AE=BF=E9=97=AE=E9=97=AE=E9=A2=98=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20life-script=20=E5=89=8D=E7=AB=AF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 /emotion-museum/assets/ 添加静态资源专用 location 处理 - 添加 /life-script/ 前端应用的 nginx 配置 - 支持 React SPA 路由和静态资源缓存策略 --- conf/nginx-emotion-museum-ssl.conf | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/conf/nginx-emotion-museum-ssl.conf b/conf/nginx-emotion-museum-ssl.conf index 008f1fb..4798464 100644 --- a/conf/nginx-emotion-museum-ssl.conf +++ b/conf/nginx-emotion-museum-ssl.conf @@ -27,6 +27,13 @@ server { location /emotion-museum/ { alias /data/www/emotion-museum/; autoindex off; + # 静态资源直接返回文件,不经过 try_files + location ~* ^/emotion-museum/(assets|static)/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + expires 1y; + try_files $uri =404; + } + # SPA 路由兜底 try_files $uri $uri/ /emotion-museum/index.html; } @@ -54,6 +61,24 @@ server { rewrite ^ /course-of-life/ last; } + # life-script React 应用 + location /life-script/ { + alias /data/www/life-script/; + autoindex off; + # 静态资源直接返回文件 + location ~* ^/life-script/(assets|static)/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + expires 1y; + try_files $uri =404; + } + # SPA 路由兜底 + try_files $uri $uri/ /life-script/index.html; + } + + location = /life-script { + rewrite ^ /life-script/ last; + } + location /api { proxy_pass http://127.0.0.1:19089; proxy_set_header Host $host;