bug修复
This commit is contained in:
+41
-212
@@ -2,14 +2,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
情绪博物馆后端服务部署脚本
|
情绪博物馆后端服务部署脚本
|
||||||
支持本地部署和远程部署到服务器
|
部署到远程服务器 101.200.208.45
|
||||||
使用系统自带的ssh/scp命令,无需额外依赖
|
使用系统自带的ssh/scp命令,无需额外依赖
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# 配置变量
|
# 配置变量
|
||||||
@@ -21,11 +20,6 @@ SPRING_PROFILE = "test"
|
|||||||
# 本地路径
|
# 本地路径
|
||||||
SCRIPT_DIR = Path(__file__).parent.absolute()
|
SCRIPT_DIR = Path(__file__).parent.absolute()
|
||||||
JAR_PATH = SCRIPT_DIR / "target" / JAR_NAME
|
JAR_PATH = SCRIPT_DIR / "target" / JAR_NAME
|
||||||
LOG_DIR = SCRIPT_DIR / "logs"
|
|
||||||
PID_FILE = Path(f"/tmp/{APP_NAME}.pid")
|
|
||||||
|
|
||||||
# Java配置
|
|
||||||
JAVA_OPTS = "-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError"
|
|
||||||
|
|
||||||
# 远程服务器配置
|
# 远程服务器配置
|
||||||
REMOTE_HOST = "101.200.208.45"
|
REMOTE_HOST = "101.200.208.45"
|
||||||
@@ -61,13 +55,7 @@ def run_command(cmd, cwd=None, shell=True, capture=True):
|
|||||||
"""执行本地命令"""
|
"""执行本地命令"""
|
||||||
try:
|
try:
|
||||||
if capture:
|
if capture:
|
||||||
result = subprocess.run(
|
result = subprocess.run(cmd, cwd=cwd, shell=shell, capture_output=True, text=True)
|
||||||
cmd,
|
|
||||||
cwd=cwd,
|
|
||||||
shell=shell,
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
return result.returncode == 0, result.stdout, result.stderr
|
return result.returncode == 0, result.stdout, result.stderr
|
||||||
else:
|
else:
|
||||||
result = subprocess.run(cmd, cwd=cwd, shell=shell)
|
result = subprocess.run(cmd, cwd=cwd, shell=shell)
|
||||||
@@ -95,23 +83,19 @@ def build_project():
|
|||||||
"""构建项目"""
|
"""构建项目"""
|
||||||
log_info("开始构建项目...")
|
log_info("开始构建项目...")
|
||||||
|
|
||||||
# 检查Maven
|
|
||||||
success, _, _ = run_command("mvn --version")
|
success, _, _ = run_command("mvn --version")
|
||||||
if not success:
|
if not success:
|
||||||
log_error("未找到Maven命令,请确保已安装Maven")
|
log_error("未找到Maven命令,请确保已安装Maven")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 执行Maven构建
|
|
||||||
log_info("执行: mvn clean package -DskipTests")
|
log_info("执行: mvn clean package -DskipTests")
|
||||||
os.chdir(SCRIPT_DIR)
|
os.chdir(SCRIPT_DIR)
|
||||||
|
|
||||||
# 不捕获输出,直接显示构建过程
|
|
||||||
success, _, _ = run_command("mvn clean package -DskipTests", capture=False)
|
success, _, _ = run_command("mvn clean package -DskipTests", capture=False)
|
||||||
if not success:
|
if not success:
|
||||||
log_error("项目构建失败")
|
log_error("项目构建失败")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 检查JAR文件
|
|
||||||
if not JAR_PATH.exists():
|
if not JAR_PATH.exists():
|
||||||
log_error(f"项目构建失败,未找到JAR文件: {JAR_PATH}")
|
log_error(f"项目构建失败,未找到JAR文件: {JAR_PATH}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -130,27 +114,24 @@ def check_jar():
|
|||||||
log_info(f"JAR文件检查通过: {JAR_PATH}")
|
log_info(f"JAR文件检查通过: {JAR_PATH}")
|
||||||
|
|
||||||
|
|
||||||
def deploy_to_remote(upload_script=None):
|
def deploy(upload_script=None):
|
||||||
"""
|
"""
|
||||||
远程部署到服务器
|
部署到远程服务器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
upload_script: 可选,指定要上传的额外文件路径(如 deploy-server.sh)
|
upload_script: 可选,指定要上传的额外文件路径(如 deploy-server.sh)
|
||||||
"""
|
"""
|
||||||
log_info(f"开始远程部署到 {REMOTE_HOST}...")
|
log_info(f"开始部署到 {REMOTE_HOST}...")
|
||||||
|
|
||||||
# 构建项目
|
|
||||||
build_project()
|
build_project()
|
||||||
|
|
||||||
# 检查JAR文件
|
|
||||||
check_jar()
|
check_jar()
|
||||||
|
|
||||||
# 1. 创建远程目录
|
# 创建远程目录
|
||||||
log_info("创建远程目录...")
|
log_info("创建远程目录...")
|
||||||
exec_ssh_cmd(f"mkdir -p {REMOTE_DIR}")
|
exec_ssh_cmd(f"mkdir -p {REMOTE_DIR}")
|
||||||
exec_ssh_cmd(f"mkdir -p {REMOTE_LOG_DIR}")
|
exec_ssh_cmd(f"mkdir -p {REMOTE_LOG_DIR}")
|
||||||
|
|
||||||
# 2. 上传JAR文件
|
# 上传JAR文件
|
||||||
log_info("上传JAR文件到远程服务器...")
|
log_info("上传JAR文件到远程服务器...")
|
||||||
log_info(f"本地文件: {JAR_PATH}")
|
log_info(f"本地文件: {JAR_PATH}")
|
||||||
log_info(f"远程路径: {REMOTE_USER}@{REMOTE_HOST}:{REMOTE_DIR}/{REMOTE_JAR_NAME}")
|
log_info(f"远程路径: {REMOTE_USER}@{REMOTE_HOST}:{REMOTE_DIR}/{REMOTE_JAR_NAME}")
|
||||||
@@ -160,7 +141,7 @@ def deploy_to_remote(upload_script=None):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
log_info("✅ JAR文件上传成功")
|
log_info("✅ JAR文件上传成功")
|
||||||
|
|
||||||
# 3. 验证远程文件
|
# 验证远程文件
|
||||||
log_info("验证远程文件...")
|
log_info("验证远程文件...")
|
||||||
success, output, _ = exec_ssh_cmd(f"ls -lh {REMOTE_DIR}/{REMOTE_JAR_NAME}")
|
success, output, _ = exec_ssh_cmd(f"ls -lh {REMOTE_DIR}/{REMOTE_JAR_NAME}")
|
||||||
if not success:
|
if not success:
|
||||||
@@ -168,26 +149,22 @@ def deploy_to_remote(upload_script=None):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
log_info(output)
|
log_info(output)
|
||||||
|
|
||||||
# 4. 如果指定了额外文件,则上传
|
# 上传额外文件
|
||||||
if upload_script:
|
if upload_script:
|
||||||
script_path = SCRIPT_DIR / upload_script
|
script_path = SCRIPT_DIR / upload_script
|
||||||
if script_path.exists():
|
if script_path.exists():
|
||||||
log_info(f"上传指定文件到远程服务器: {upload_script}")
|
log_info(f"上传文件到远程服务器: {upload_script}")
|
||||||
if not scp_upload(script_path, f"{REMOTE_DIR}/{upload_script}"):
|
if not scp_upload(script_path, f"{REMOTE_DIR}/{upload_script}"):
|
||||||
log_error(f"上传文件失败: {upload_script}")
|
log_error(f"上传文件失败: {upload_script}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
log_info(f"✅ 文件上传成功: {upload_script}")
|
log_info(f"✅ 文件上传成功: {upload_script}")
|
||||||
|
|
||||||
# 如果是脚本文件,设置执行权限
|
|
||||||
if upload_script.endswith('.sh'):
|
if upload_script.endswith('.sh'):
|
||||||
exec_ssh_cmd(f"chmod +x {REMOTE_DIR}/{upload_script}")
|
exec_ssh_cmd(f"chmod +x {REMOTE_DIR}/{upload_script}")
|
||||||
else:
|
else:
|
||||||
log_error(f"指定的文件不存在: {script_path}")
|
log_error(f"指定的文件不存在: {script_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
|
||||||
log_info("跳过部署脚本上传(服务器已存在)")
|
|
||||||
|
|
||||||
# 5. 在远程服务器上执行部署
|
# 执行远程部署
|
||||||
log_info("在远程服务器上执行部署...")
|
log_info("在远程服务器上执行部署...")
|
||||||
success, output, error = exec_ssh_cmd(f"cd {REMOTE_DIR} && ./deploy-server.sh {SPRING_PROFILE}")
|
success, output, error = exec_ssh_cmd(f"cd {REMOTE_DIR} && ./deploy-server.sh {SPRING_PROFILE}")
|
||||||
if output:
|
if output:
|
||||||
@@ -199,155 +176,24 @@ def deploy_to_remote(upload_script=None):
|
|||||||
log_error("远程部署脚本执行失败")
|
log_error("远程部署脚本执行失败")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
log_info("✅ 远程部署完成!")
|
log_info("✅ 部署完成!")
|
||||||
show_remote_status()
|
show_status()
|
||||||
|
|
||||||
|
|
||||||
def show_remote_status():
|
def show_status():
|
||||||
"""显示远程服务状态"""
|
"""显示远程服务状态"""
|
||||||
log_info("=== 远程服务信息 ===")
|
log_info("=== 服务信息 ===")
|
||||||
log_info(f"服务器地址: {REMOTE_HOST}")
|
log_info(f"服务器地址: {REMOTE_HOST}")
|
||||||
log_info(f"部署目录: {REMOTE_DIR}")
|
log_info(f"部署目录: {REMOTE_DIR}")
|
||||||
log_info(f"日志目录: {REMOTE_LOG_DIR}")
|
log_info(f"日志目录: {REMOTE_LOG_DIR}")
|
||||||
log_info(f"Spring Profile: {SPRING_PROFILE}")
|
log_info(f"Spring Profile: {SPRING_PROFILE}")
|
||||||
|
|
||||||
log_info("检查远程服务状态...")
|
log_info("检查服务状态...")
|
||||||
success, output, _ = exec_ssh_cmd(f"ps aux | grep {REMOTE_JAR_NAME} | grep -v grep")
|
success, output, _ = exec_ssh_cmd(f"ps aux | grep {REMOTE_JAR_NAME} | grep -v grep")
|
||||||
if output:
|
if output:
|
||||||
log_info(f"远程服务运行中:\n{output}")
|
log_info(f"服务运行中:\n{output}")
|
||||||
else:
|
else:
|
||||||
log_info("远程服务未运行")
|
log_info("服务未运行")
|
||||||
|
|
||||||
|
|
||||||
def local_deploy():
|
|
||||||
"""本地部署"""
|
|
||||||
log_info(f"开始本地部署 {APP_NAME} 服务...")
|
|
||||||
|
|
||||||
# 构建项目
|
|
||||||
build_project()
|
|
||||||
|
|
||||||
# 检查JAR文件
|
|
||||||
check_jar()
|
|
||||||
|
|
||||||
# 创建日志目录
|
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# 停止旧服务
|
|
||||||
stop_local_service()
|
|
||||||
|
|
||||||
# 启动新服务
|
|
||||||
start_local_service()
|
|
||||||
|
|
||||||
# 等待启动
|
|
||||||
if wait_for_startup():
|
|
||||||
log_info("✅ 本地部署成功!")
|
|
||||||
show_local_status()
|
|
||||||
else:
|
|
||||||
log_error("本地部署失败!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def stop_local_service():
|
|
||||||
"""停止本地服务"""
|
|
||||||
if PID_FILE.exists():
|
|
||||||
pid = PID_FILE.read_text().strip()
|
|
||||||
success, _, _ = run_command(f"ps -p {pid}")
|
|
||||||
if success:
|
|
||||||
log_info(f"停止旧服务 (PID: {pid})")
|
|
||||||
run_command(f"kill {pid}")
|
|
||||||
|
|
||||||
# 等待服务停止
|
|
||||||
for _ in range(30):
|
|
||||||
success, _, _ = run_command(f"ps -p {pid}")
|
|
||||||
if not success:
|
|
||||||
log_info("服务已停止")
|
|
||||||
break
|
|
||||||
time.sleep(1)
|
|
||||||
else:
|
|
||||||
log_warn(f"强制停止服务 (PID: {pid})")
|
|
||||||
run_command(f"kill -9 {pid}")
|
|
||||||
else:
|
|
||||||
log_warn("PID文件存在但进程不存在,清理PID文件")
|
|
||||||
|
|
||||||
PID_FILE.unlink()
|
|
||||||
else:
|
|
||||||
log_info("没有找到PID文件,服务可能未运行")
|
|
||||||
|
|
||||||
|
|
||||||
def start_local_service():
|
|
||||||
"""启动本地服务"""
|
|
||||||
log_info("启动本地服务...")
|
|
||||||
|
|
||||||
startup_log = LOG_DIR / "startup.log"
|
|
||||||
app_log = LOG_DIR / "application.log"
|
|
||||||
|
|
||||||
cmd = (
|
|
||||||
f"nohup java {JAVA_OPTS} "
|
|
||||||
f"-Dspring.profiles.active={SPRING_PROFILE} "
|
|
||||||
f"-Dlogging.file.path={LOG_DIR} "
|
|
||||||
f"-Dlogging.file.name={app_log} "
|
|
||||||
f"-jar {JAR_PATH} "
|
|
||||||
f"> {startup_log} 2>&1 &"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 使用subprocess启动后台进程
|
|
||||||
subprocess.Popen(
|
|
||||||
cmd,
|
|
||||||
shell=True,
|
|
||||||
cwd=SCRIPT_DIR,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL
|
|
||||||
)
|
|
||||||
|
|
||||||
# 获取实际的Java进程PID
|
|
||||||
time.sleep(2)
|
|
||||||
success, output, _ = run_command(f"pgrep -f {JAR_NAME}")
|
|
||||||
if success and output:
|
|
||||||
pid = output.strip().split('\n')[0]
|
|
||||||
PID_FILE.write_text(pid)
|
|
||||||
log_info(f"服务启动中,PID: {pid}")
|
|
||||||
else:
|
|
||||||
log_warn("无法获取服务PID")
|
|
||||||
|
|
||||||
log_info(f"启动日志: {startup_log}")
|
|
||||||
log_info(f"应用日志: {app_log}")
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_startup():
|
|
||||||
"""等待服务启动"""
|
|
||||||
log_info("等待服务启动...")
|
|
||||||
|
|
||||||
for _ in range(60):
|
|
||||||
# 检查端口是否监听
|
|
||||||
success, output, _ = run_command("netstat -tlnp 2>/dev/null | grep ':19089.*LISTEN'")
|
|
||||||
if success and output:
|
|
||||||
log_info("服务启动成功!")
|
|
||||||
return True
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
log_error(f"服务启动超时,请检查日志: {LOG_DIR}/startup.log")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def show_local_status():
|
|
||||||
"""显示本地服务状态"""
|
|
||||||
log_info("=== 本地服务信息 ===")
|
|
||||||
log_info(f"应用名称: {APP_NAME}")
|
|
||||||
log_info(f"JAR文件: {JAR_PATH}")
|
|
||||||
log_info(f"日志目录: {LOG_DIR}")
|
|
||||||
log_info(f"PID文件: {PID_FILE}")
|
|
||||||
log_info(f"Java参数: {JAVA_OPTS}")
|
|
||||||
log_info(f"Spring Profile: {SPRING_PROFILE}")
|
|
||||||
|
|
||||||
if PID_FILE.exists():
|
|
||||||
pid = PID_FILE.read_text().strip()
|
|
||||||
success, _, _ = run_command(f"ps -p {pid}")
|
|
||||||
if success:
|
|
||||||
log_info(f"服务状态: 运行中 (PID: {pid})")
|
|
||||||
else:
|
|
||||||
log_info("服务状态: 未运行")
|
|
||||||
else:
|
|
||||||
log_info("服务状态: 未运行")
|
|
||||||
|
|
||||||
|
|
||||||
def print_usage():
|
def print_usage():
|
||||||
@@ -356,57 +202,40 @@ def print_usage():
|
|||||||
用法: python deploy.py [命令] [参数]
|
用法: python deploy.py [命令] [参数]
|
||||||
|
|
||||||
命令:
|
命令:
|
||||||
deploy - 本地部署服务(默认)
|
deploy - 部署到远程服务器(默认)
|
||||||
remote - 远程部署到服务器(仅上传JAR)
|
deploy [文件名] - 部署并上传指定文件(如 deploy-server.sh)
|
||||||
remote [文件名] - 远程部署并上传指定文件(如 deploy-server.sh)
|
build - 仅构建项目
|
||||||
build - 构建项目
|
status - 查看远程服务状态
|
||||||
start - 启动本地服务
|
|
||||||
stop - 停止本地服务
|
|
||||||
restart - 重启本地服务
|
|
||||||
status - 查看本地服务状态
|
|
||||||
remote-status - 查看远程服务状态
|
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
python deploy.py remote # 仅上传JAR并部署
|
python deploy.py # 部署到远程服务器
|
||||||
python deploy.py remote deploy-server.sh # 同时上传部署脚本
|
python deploy.py deploy-server.sh # 同时上传部署脚本
|
||||||
|
python deploy.py build # 仅构建项目
|
||||||
|
python deploy.py status # 查看服务状态
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
command = sys.argv[1] if len(sys.argv) > 1 else "deploy"
|
if len(sys.argv) < 2:
|
||||||
|
deploy()
|
||||||
|
return
|
||||||
|
|
||||||
if command == "deploy":
|
command = sys.argv[1]
|
||||||
local_deploy()
|
|
||||||
elif command == "remote":
|
if command == "build":
|
||||||
# 检查是否有额外的文件参数
|
|
||||||
upload_script = sys.argv[2] if len(sys.argv) > 2 else None
|
|
||||||
deploy_to_remote(upload_script)
|
|
||||||
elif command == "build":
|
|
||||||
build_project()
|
build_project()
|
||||||
elif command == "start":
|
|
||||||
build_project()
|
|
||||||
check_jar()
|
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
start_local_service()
|
|
||||||
wait_for_startup()
|
|
||||||
elif command == "stop":
|
|
||||||
stop_local_service()
|
|
||||||
elif command == "restart":
|
|
||||||
stop_local_service()
|
|
||||||
time.sleep(2)
|
|
||||||
build_project()
|
|
||||||
check_jar()
|
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
start_local_service()
|
|
||||||
wait_for_startup()
|
|
||||||
elif command == "status":
|
elif command == "status":
|
||||||
show_local_status()
|
show_status()
|
||||||
elif command == "remote-status":
|
elif command == "help" or command == "-h" or command == "--help":
|
||||||
show_remote_status()
|
|
||||||
else:
|
|
||||||
print_usage()
|
print_usage()
|
||||||
sys.exit(1)
|
elif command.endswith('.sh'):
|
||||||
|
# 如果第一个参数是文件名,则上传该文件
|
||||||
|
deploy(command)
|
||||||
|
else:
|
||||||
|
# 其他情况视为部署命令
|
||||||
|
upload_script = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
deploy(upload_script)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+63
-263
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# 情绪博物馆后端服务部署脚本
|
# 情绪博物馆后端服务部署脚本
|
||||||
# 支持本地部署和远程部署到服务器 101.200.208.45
|
# 部署到远程服务器 101.200.208.45
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -9,9 +9,6 @@ set -e
|
|||||||
APP_NAME="emotion-museum-single"
|
APP_NAME="emotion-museum-single"
|
||||||
JAR_NAME="backend-single-1.0.0.jar"
|
JAR_NAME="backend-single-1.0.0.jar"
|
||||||
JAR_PATH="./target/${JAR_NAME}"
|
JAR_PATH="./target/${JAR_NAME}"
|
||||||
LOG_DIR="./logs/"
|
|
||||||
PID_FILE="/tmp/${APP_NAME}.pid"
|
|
||||||
JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof"
|
|
||||||
|
|
||||||
# 远程服务器配置
|
# 远程服务器配置
|
||||||
REMOTE_HOST="101.200.208.45"
|
REMOTE_HOST="101.200.208.45"
|
||||||
@@ -25,9 +22,8 @@ SPRING_PROFILE="test"
|
|||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m'
|
||||||
|
|
||||||
# 日志函数
|
|
||||||
log_info() {
|
log_info() {
|
||||||
echo -e "${GREEN}[INFO]${NC} $1"
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
}
|
}
|
||||||
@@ -40,27 +36,23 @@ log_error() {
|
|||||||
echo -e "${RED}[ERROR]${NC} $1"
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 检查并构建项目
|
# 构建项目
|
||||||
build_project() {
|
build_project() {
|
||||||
log_info "开始构建项目..."
|
log_info "开始构建项目..."
|
||||||
|
|
||||||
# 检查 Maven 是否安装
|
|
||||||
if ! command -v mvn > /dev/null 2>&1; then
|
if ! command -v mvn > /dev/null 2>&1; then
|
||||||
log_error "未找到Maven命令,请确保已安装Maven"
|
log_error "未找到Maven命令,请确保已安装Maven"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 执行 Maven 构建
|
|
||||||
log_info "执行: mvn clean package -DskipTests"
|
log_info "执行: mvn clean package -DskipTests"
|
||||||
if ! mvn clean package -DskipTests; then
|
if ! mvn clean package -DskipTests; then
|
||||||
log_error "项目构建失败"
|
log_error "项目构建失败"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查 JAR 文件是否生成
|
|
||||||
if [ ! -f "$JAR_PATH" ]; then
|
if [ ! -f "$JAR_PATH" ]; then
|
||||||
log_error "项目构建失败,未找到JAR文件: $JAR_PATH"
|
log_error "项目构建失败,未找到JAR文件: $JAR_PATH"
|
||||||
log_error "请检查 Maven 构建输出"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -68,313 +60,121 @@ build_project() {
|
|||||||
log_info "文件大小: $(ls -lh $JAR_PATH | awk '{print $5}')"
|
log_info "文件大小: $(ls -lh $JAR_PATH | awk '{print $5}')"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 检查 jar 文件是否存在
|
# 检查JAR文件
|
||||||
check_jar() {
|
check_jar() {
|
||||||
if [ ! -f "$JAR_PATH" ]; then
|
if [ ! -f "$JAR_PATH" ]; then
|
||||||
log_error "JAR 文件不存在: $JAR_PATH"
|
log_error "JAR文件不存在: $JAR_PATH"
|
||||||
log_info "请先执行打包命令: mvn clean package"
|
log_info "请先执行打包命令: mvn clean package"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
log_info "JAR 文件检查通过: $JAR_PATH"
|
log_info "JAR文件检查通过: $JAR_PATH"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 创建日志目录
|
# 显示服务状态
|
||||||
create_log_dir() {
|
show_status() {
|
||||||
if [ ! -d "$LOG_DIR" ]; then
|
log_info "=== 服务信息 ==="
|
||||||
log_info "创建日志目录: $LOG_DIR"
|
log_info "服务器地址: $REMOTE_HOST"
|
||||||
mkdir -p "$LOG_DIR"
|
log_info "部署目录: $REMOTE_DIR"
|
||||||
fi
|
log_info "日志目录: $REMOTE_LOG_DIR"
|
||||||
|
log_info "Spring Profile: $SPRING_PROFILE"
|
||||||
|
|
||||||
|
log_info "检查服务状态..."
|
||||||
|
ssh $REMOTE_USER@$REMOTE_HOST "ps aux | grep $REMOTE_JAR_NAME | grep -v grep" || log_info "服务未运行"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 本地部署 - 停止旧服务
|
# 部署到远程服务器
|
||||||
stop_local_service() {
|
# 参数: $1 - 可选,指定要上传的额外文件
|
||||||
if [ -f "$PID_FILE" ]; then
|
deploy() {
|
||||||
PID=$(cat "$PID_FILE")
|
UPLOAD_SCRIPT="$1"
|
||||||
if ps -p "$PID" > /dev/null 2>&1; then
|
|
||||||
log_info "停止旧服务 (PID: $PID)"
|
log_info "开始部署到 $REMOTE_HOST..."
|
||||||
kill "$PID"
|
|
||||||
|
|
||||||
# 等待服务停止
|
|
||||||
for i in {1..30}; do
|
|
||||||
if ! ps -p "$PID" > /dev/null 2>&1; then
|
|
||||||
log_info "服务已停止"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
# 强制停止
|
|
||||||
if ps -p "$PID" > /dev/null 2>&1; then
|
|
||||||
log_warn "强制停止服务 (PID: $PID)"
|
|
||||||
kill -9 "$PID"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_warn "PID 文件存在但进程不存在,清理 PID 文件"
|
|
||||||
fi
|
|
||||||
rm -f "$PID_FILE"
|
|
||||||
else
|
|
||||||
log_info "没有找到 PID 文件,服务可能未运行"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 本地部署 - 启动新服务
|
|
||||||
start_local_service() {
|
|
||||||
log_info "启动本地服务..."
|
|
||||||
|
|
||||||
# 启动命令
|
|
||||||
nohup java $JAVA_OPTS \
|
|
||||||
-Dspring.profiles.active=$SPRING_PROFILE \
|
|
||||||
-Dlogging.file.path=$LOG_DIR \
|
|
||||||
-Dlogging.file.name=$LOG_DIR/application.log \
|
|
||||||
-jar "$JAR_PATH" \
|
|
||||||
> "$LOG_DIR/startup.log" 2>&1 &
|
|
||||||
|
|
||||||
# 保存 PID
|
|
||||||
echo $! > "$PID_FILE"
|
|
||||||
|
|
||||||
log_info "服务启动中,PID: $(cat $PID_FILE)"
|
|
||||||
log_info "启动日志: $LOG_DIR/startup.log"
|
|
||||||
log_info "应用日志: $LOG_DIR/application.log"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 远程部署 - 上传文件到服务器
|
|
||||||
# 参数: $1 - 可选,指定要上传的额外文件(如 deploy-server.sh)
|
|
||||||
deploy_to_remote() {
|
|
||||||
UPLOAD_SCRIPT="$2"
|
|
||||||
|
|
||||||
log_info "开始远程部署到 $REMOTE_HOST..."
|
|
||||||
|
|
||||||
# 检查并构建项目
|
|
||||||
build_project
|
build_project
|
||||||
|
|
||||||
# 检查 jar 文件
|
|
||||||
check_jar
|
check_jar
|
||||||
|
|
||||||
# 创建远程目录
|
# 创建远程目录
|
||||||
log_info "创建远程目录..."
|
log_info "创建远程目录..."
|
||||||
if ! ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_DIR"; then
|
ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_DIR" || { log_error "创建远程目录失败"; exit 1; }
|
||||||
log_error "创建远程目录失败: $REMOTE_DIR"
|
ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_LOG_DIR" || { log_error "创建远程日志目录失败"; exit 1; }
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_LOG_DIR"; then
|
|
||||||
log_error "创建远程日志目录失败: $REMOTE_LOG_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 上传 jar 文件并重命名
|
# 上传JAR文件
|
||||||
log_info "上传 JAR 文件到远程服务器..."
|
log_info "上传JAR文件到远程服务器..."
|
||||||
log_info "本地文件: $JAR_PATH"
|
log_info "本地文件: $JAR_PATH"
|
||||||
log_info "远程路径: $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/$REMOTE_JAR_NAME"
|
log_info "远程路径: $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/$REMOTE_JAR_NAME"
|
||||||
|
|
||||||
if ! scp "$JAR_PATH" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/$REMOTE_JAR_NAME; then
|
if ! scp "$JAR_PATH" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/$REMOTE_JAR_NAME; then
|
||||||
log_error "上传 JAR 文件失败"
|
log_error "上传JAR文件失败"
|
||||||
log_error "请检查:"
|
|
||||||
log_error " 1. SSH 连接是否正常: ssh $REMOTE_USER@$REMOTE_HOST 'ls -la $REMOTE_DIR'"
|
|
||||||
log_error " 2. 本地文件是否存在: ls -la $JAR_PATH"
|
|
||||||
log_error " 3. 远程目录权限: ssh $REMOTE_USER@$REMOTE_HOST 'ls -la $REMOTE_DIR'"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
log_info "✅ JAR 文件上传成功"
|
log_info "✅ JAR文件上传成功"
|
||||||
|
|
||||||
# 验证远程文件
|
# 验证远程文件
|
||||||
log_info "验证远程文件..."
|
log_info "验证远程文件..."
|
||||||
if ! ssh $REMOTE_USER@$REMOTE_HOST "ls -lh $REMOTE_DIR/$REMOTE_JAR_NAME"; then
|
ssh $REMOTE_USER@$REMOTE_HOST "ls -lh $REMOTE_DIR/$REMOTE_JAR_NAME" || { log_error "远程文件验证失败"; exit 1; }
|
||||||
log_error "远程文件验证失败"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 如果指定了额外文件,则上传
|
# 上传额外文件
|
||||||
if [ -n "$UPLOAD_SCRIPT" ] && [ -f "$UPLOAD_SCRIPT" ]; then
|
if [ -n "$UPLOAD_SCRIPT" ] && [ -f "$UPLOAD_SCRIPT" ]; then
|
||||||
log_info "上传指定文件到远程服务器: $UPLOAD_SCRIPT"
|
log_info "上传文件到远程服务器: $UPLOAD_SCRIPT"
|
||||||
if ! scp "$UPLOAD_SCRIPT" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/; then
|
if ! scp "$UPLOAD_SCRIPT" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/; then
|
||||||
log_error "上传文件失败: $UPLOAD_SCRIPT"
|
log_error "上传文件失败: $UPLOAD_SCRIPT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
log_info "✅ 文件上传成功: $UPLOAD_SCRIPT"
|
log_info "✅ 文件上传成功: $UPLOAD_SCRIPT"
|
||||||
|
|
||||||
# 如果是脚本文件,设置执行权限
|
|
||||||
REMOTE_FILENAME=$(basename "$UPLOAD_SCRIPT")
|
REMOTE_FILENAME=$(basename "$UPLOAD_SCRIPT")
|
||||||
if [[ "$REMOTE_FILENAME" == *.sh ]]; then
|
if [[ "$REMOTE_FILENAME" == *.sh ]]; then
|
||||||
log_info "设置远程脚本权限..."
|
|
||||||
ssh $REMOTE_USER@$REMOTE_HOST "chmod +x $REMOTE_DIR/$REMOTE_FILENAME"
|
ssh $REMOTE_USER@$REMOTE_HOST "chmod +x $REMOTE_DIR/$REMOTE_FILENAME"
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
log_info "跳过部署脚本上传(服务器已存在)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 在远程服务器上执行部署
|
# 执行远程部署
|
||||||
log_info "在远程服务器上执行部署..."
|
log_info "在远程服务器上执行部署..."
|
||||||
if ! ssh $REMOTE_USER@$REMOTE_HOST "cd $REMOTE_DIR && ./deploy-server.sh $SPRING_PROFILE"; then
|
if ! ssh $REMOTE_USER@$REMOTE_HOST "cd $REMOTE_DIR && ./deploy-server.sh $SPRING_PROFILE"; then
|
||||||
log_error "远程部署脚本执行失败"
|
log_error "远程部署脚本执行失败"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_info "✅ 远程部署完成!"
|
log_info "✅ 部署完成!"
|
||||||
show_remote_info
|
show_status
|
||||||
}
|
}
|
||||||
|
|
||||||
# 本地部署 - 检查服务状态
|
# 打印使用说明
|
||||||
check_local_status() {
|
print_usage() {
|
||||||
if [ -f "$PID_FILE" ]; then
|
echo "用法: $0 [命令] [参数]"
|
||||||
PID=$(cat "$PID_FILE")
|
echo ""
|
||||||
if ps -p "$PID" > /dev/null 2>&1; then
|
echo "命令:"
|
||||||
log_info "服务运行中 (PID: $PID)"
|
echo " (无参数) - 部署到远程服务器(默认)"
|
||||||
return 0
|
echo " [文件名] - 部署并上传指定文件(如 deploy-server.sh)"
|
||||||
else
|
echo " build - 仅构建项目"
|
||||||
log_error "PID 文件存在但进程不存在"
|
echo " status - 查看远程服务状态"
|
||||||
return 1
|
echo ""
|
||||||
fi
|
echo "示例:"
|
||||||
else
|
echo " $0 # 部署到远程服务器"
|
||||||
log_error "PID 文件不存在,服务未运行"
|
echo " $0 deploy-server.sh # 同时上传部署脚本"
|
||||||
return 1
|
echo " $0 build # 仅构建项目"
|
||||||
fi
|
echo " $0 status # 查看服务状态"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 本地部署 - 等待服务启动
|
# 主逻辑
|
||||||
wait_for_local_startup() {
|
case "${1:-}" in
|
||||||
log_info "等待服务启动..."
|
"")
|
||||||
for i in {1..60}; do
|
deploy
|
||||||
if check_local_status > /dev/null 2>&1; then
|
|
||||||
# 检查端口是否监听(使用 19089 端口)
|
|
||||||
if netstat -tlnp 2>/dev/null | grep -q ":19089.*LISTEN"; then
|
|
||||||
log_info "服务启动成功!"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
log_error "服务启动超时,请检查日志: $LOG_DIR/startup.log"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 本地部署 - 显示服务信息
|
|
||||||
show_local_info() {
|
|
||||||
log_info "=== 本地服务信息 ==="
|
|
||||||
log_info "应用名称: $APP_NAME"
|
|
||||||
log_info "JAR 文件: $JAR_PATH"
|
|
||||||
log_info "日志目录: $LOG_DIR"
|
|
||||||
log_info "PID 文件: $PID_FILE"
|
|
||||||
log_info "Java 参数: $JAVA_OPTS"
|
|
||||||
log_info "Spring Profile: $SPRING_PROFILE"
|
|
||||||
|
|
||||||
if check_local_status > /dev/null 2>&1; then
|
|
||||||
PID=$(cat "$PID_FILE")
|
|
||||||
log_info "服务状态: 运行中 (PID: $PID)"
|
|
||||||
|
|
||||||
# 显示内存使用情况
|
|
||||||
if command -v jstat > /dev/null 2>&1; then
|
|
||||||
log_info "内存使用情况:"
|
|
||||||
jstat -gc "$PID" | head -2
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_info "服务状态: 未运行"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 远程部署 - 显示服务信息
|
|
||||||
show_remote_info() {
|
|
||||||
log_info "=== 远程服务信息 ==="
|
|
||||||
log_info "服务器地址: $REMOTE_HOST"
|
|
||||||
log_info "部署目录: $REMOTE_DIR"
|
|
||||||
log_info "日志目录: $REMOTE_LOG_DIR"
|
|
||||||
log_info "Spring Profile: $SPRING_PROFILE"
|
|
||||||
|
|
||||||
# 检查远程服务状态
|
|
||||||
log_info "检查远程服务状态..."
|
|
||||||
ssh $REMOTE_USER@$REMOTE_HOST "ps aux | grep $REMOTE_JAR_NAME | grep -v grep" || log_info "远程服务未运行"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 本地部署 - 主函数
|
|
||||||
local_deploy() {
|
|
||||||
log_info "开始本地部署 $APP_NAME 服务..."
|
|
||||||
|
|
||||||
# 检查并构建项目
|
|
||||||
build_project
|
|
||||||
|
|
||||||
# 检查 jar 文件
|
|
||||||
check_jar
|
|
||||||
|
|
||||||
# 创建日志目录
|
|
||||||
create_log_dir
|
|
||||||
|
|
||||||
# 停止旧服务
|
|
||||||
stop_local_service
|
|
||||||
|
|
||||||
# 启动新服务
|
|
||||||
start_local_service
|
|
||||||
|
|
||||||
# 等待启动
|
|
||||||
if wait_for_local_startup; then
|
|
||||||
log_info "本地部署成功!"
|
|
||||||
show_local_info
|
|
||||||
else
|
|
||||||
log_error "本地部署失败!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 处理命令行参数
|
|
||||||
case "${1:-deploy}" in
|
|
||||||
"deploy")
|
|
||||||
local_deploy
|
|
||||||
;;
|
|
||||||
"remote")
|
|
||||||
deploy_to_remote "$@"
|
|
||||||
;;
|
;;
|
||||||
"build")
|
"build")
|
||||||
build_project
|
build_project
|
||||||
;;
|
;;
|
||||||
"start")
|
|
||||||
# 检查并构建项目
|
|
||||||
build_project
|
|
||||||
check_jar
|
|
||||||
create_log_dir
|
|
||||||
start_local_service
|
|
||||||
wait_for_local_startup
|
|
||||||
;;
|
|
||||||
"stop")
|
|
||||||
stop_local_service
|
|
||||||
;;
|
|
||||||
"restart")
|
|
||||||
stop_local_service
|
|
||||||
sleep 2
|
|
||||||
# 检查并构建项目
|
|
||||||
build_project
|
|
||||||
check_jar
|
|
||||||
create_log_dir
|
|
||||||
start_local_service
|
|
||||||
wait_for_local_startup
|
|
||||||
;;
|
|
||||||
"status")
|
"status")
|
||||||
show_local_info
|
show_status
|
||||||
;;
|
;;
|
||||||
"remote-status")
|
"help"|"-h"|"--help")
|
||||||
show_remote_info
|
print_usage
|
||||||
;;
|
|
||||||
"logs")
|
|
||||||
if [ -f "$LOG_DIR/application.log" ]; then
|
|
||||||
tail -f "$LOG_DIR/application.log"
|
|
||||||
else
|
|
||||||
log_error "日志文件不存在: $LOG_DIR/application.log"
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "用法: $0 {deploy|remote [文件名]|build|start|stop|restart|status|remote-status|logs}"
|
# 如果参数是文件,则上传该文件
|
||||||
echo " deploy - 本地部署服务(默认)"
|
if [ -f "$1" ]; then
|
||||||
echo " remote - 远程部署到服务器(仅上传JAR)"
|
deploy "$1"
|
||||||
echo " remote [文件名] - 远程部署并上传指定文件(如 deploy-server.sh)"
|
else
|
||||||
echo " build - 构建项目"
|
deploy "$1"
|
||||||
echo " start - 启动本地服务"
|
fi
|
||||||
echo " stop - 停止本地服务"
|
|
||||||
echo " restart - 重启本地服务"
|
|
||||||
echo " status - 查看本地服务状态"
|
|
||||||
echo " remote-status - 查看远程服务状态"
|
|
||||||
echo " logs - 查看本地实时日志"
|
|
||||||
echo ""
|
|
||||||
echo "示例:"
|
|
||||||
echo " $0 remote # 仅上传JAR并部署"
|
|
||||||
echo " $0 remote deploy-server.sh # 同时上传部署脚本"
|
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -97,4 +97,15 @@ public class UserProfileController {
|
|||||||
List<UserProfileResponse> list = userProfileService.getProfileList(request);
|
List<UserProfileResponse> list = userProfileService.getProfileList(request);
|
||||||
return Result.success(list);
|
return Result.success(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移用户档案中的生命事件数据到t_life_event表
|
||||||
|
* 将childhood、peak、valley数据迁移到生命事件表
|
||||||
|
* 注意:此接口为一次性数据迁移接口,迁移完成后可删除
|
||||||
|
*/
|
||||||
|
@PostMapping("/migrateLifeEvents")
|
||||||
|
public Result<Integer> migrateLifeEvents() {
|
||||||
|
int count = userProfileService.migrateLifeEventsFromProfiles();
|
||||||
|
return Result.success(count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,4 +72,12 @@ public interface UserProfileService extends IService<UserProfile> {
|
|||||||
* @return 是否成功
|
* @return 是否成功
|
||||||
*/
|
*/
|
||||||
boolean deleteProfile(String id);
|
boolean deleteProfile(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移用户档案中的生命事件数据到t_life_event表
|
||||||
|
* 将childhood、peak、valley数据迁移到生命事件表
|
||||||
|
*
|
||||||
|
* @return 迁移的记录数
|
||||||
|
*/
|
||||||
|
int migrateLifeEventsFromProfiles();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public class LifeEventServiceImpl extends ServiceImpl<LifeEventMapper, LifeEvent
|
|||||||
|
|
||||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
|
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
|
||||||
|
private static final DateTimeFormatter DATE_ONLY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageResult<LifeEventResponse> getPageByCurrentUser(LifeEventPageRequest request) {
|
public PageResult<LifeEventResponse> getPageByCurrentUser(LifeEventPageRequest request) {
|
||||||
@@ -130,16 +131,8 @@ public class LifeEventServiceImpl extends ServiceImpl<LifeEventMapper, LifeEvent
|
|||||||
event.setEmotionType(request.getEmotionType());
|
event.setEmotionType(request.getEmotionType());
|
||||||
event.setTags(request.getTags());
|
event.setTags(request.getTags());
|
||||||
|
|
||||||
// 解析事件日期
|
// 解析事件日期,支持多种格式
|
||||||
if (StringUtils.hasText(request.getEventDate())) {
|
event.setEventDate(parseEventDate(request.getEventDate()));
|
||||||
try {
|
|
||||||
event.setEventDate(LocalDateTime.parse(request.getEventDate(), ISO_FORMATTER));
|
|
||||||
} catch (Exception e) {
|
|
||||||
event.setEventDate(LocalDateTime.now());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
event.setEventDate(LocalDateTime.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 情绪评分
|
// 情绪评分
|
||||||
if (request.getEmotionScore() != null) {
|
if (request.getEmotionScore() != null) {
|
||||||
@@ -183,10 +176,7 @@ public class LifeEventServiceImpl extends ServiceImpl<LifeEventMapper, LifeEvent
|
|||||||
event.setTags(request.getTags());
|
event.setTags(request.getTags());
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(request.getEventDate())) {
|
if (StringUtils.hasText(request.getEventDate())) {
|
||||||
try {
|
event.setEventDate(parseEventDate(request.getEventDate()));
|
||||||
event.setEventDate(LocalDateTime.parse(request.getEventDate(), ISO_FORMATTER));
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (request.getEmotionScore() != null) {
|
if (request.getEmotionScore() != null) {
|
||||||
event.setEmotionScore(BigDecimal.valueOf(request.getEmotionScore()));
|
event.setEmotionScore(BigDecimal.valueOf(request.getEmotionScore()));
|
||||||
@@ -234,4 +224,38 @@ public class LifeEventServiceImpl extends ServiceImpl<LifeEventMapper, LifeEvent
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析事件日期,支持多种格式
|
||||||
|
* 支持格式:yyyy-MM-dd、yyyy-MM-ddTHH:mm:ss、ISO_DATE_TIME
|
||||||
|
*
|
||||||
|
* @param dateStr 日期字符串
|
||||||
|
* @return 解析后的LocalDateTime,解析失败返回当前时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime parseEventDate(String dateStr) {
|
||||||
|
if (!StringUtils.hasText(dateStr)) {
|
||||||
|
return LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试ISO格式 (yyyy-MM-ddTHH:mm:ss.SSSZ)
|
||||||
|
try {
|
||||||
|
return LocalDateTime.parse(dateStr, ISO_FORMATTER);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试日期时间格式 (yyyy-MM-dd HH:mm:ss)
|
||||||
|
try {
|
||||||
|
return LocalDateTime.parse(dateStr, DATE_TIME_FORMATTER);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试纯日期格式 (yyyy-MM-dd),时间设为当天开始
|
||||||
|
try {
|
||||||
|
return java.time.LocalDate.parse(dateStr, DATE_ONLY_FORMATTER).atStartOfDay();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有格式都失败,返回当前时间
|
||||||
|
return LocalDateTime.now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,18 +11,24 @@ import com.emotion.dto.request.userprofile.UserProfileCreateRequest;
|
|||||||
import com.emotion.dto.request.userprofile.UserProfilePageRequest;
|
import com.emotion.dto.request.userprofile.UserProfilePageRequest;
|
||||||
import com.emotion.dto.request.userprofile.UserProfileUpdateRequest;
|
import com.emotion.dto.request.userprofile.UserProfileUpdateRequest;
|
||||||
import com.emotion.dto.response.userprofile.UserProfileResponse;
|
import com.emotion.dto.response.userprofile.UserProfileResponse;
|
||||||
|
import com.emotion.entity.LifeEvent;
|
||||||
import com.emotion.entity.User;
|
import com.emotion.entity.User;
|
||||||
import com.emotion.entity.UserProfile;
|
import com.emotion.entity.UserProfile;
|
||||||
import com.emotion.mapper.UserProfileMapper;
|
import com.emotion.mapper.UserProfileMapper;
|
||||||
|
import com.emotion.service.LifeEventService;
|
||||||
import com.emotion.service.UserProfileService;
|
import com.emotion.service.UserProfileService;
|
||||||
import com.emotion.service.UserService;
|
import com.emotion.service.UserService;
|
||||||
import com.emotion.util.UserContextHolder;
|
import com.emotion.util.UserContextHolder;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -39,6 +45,10 @@ public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserP
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private LifeEventService lifeEventService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserProfileResponse createProfile(UserProfileCreateRequest request) {
|
public UserProfileResponse createProfile(UserProfileCreateRequest request) {
|
||||||
log.info("Creating user profile: {}", request);
|
log.info("Creating user profile: {}", request);
|
||||||
@@ -207,6 +217,126 @@ public class UserProfileServiceImpl extends ServiceImpl<UserProfileMapper, UserP
|
|||||||
return removeById(id);
|
return removeById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int migrateLifeEventsFromProfiles() {
|
||||||
|
log.info("开始迁移用户档案中的生命事件数据到t_life_event表");
|
||||||
|
|
||||||
|
// 查询所有有生命事件数据的用户档案
|
||||||
|
LambdaQueryWrapper<UserProfile> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.and(w -> w
|
||||||
|
.isNotNull(UserProfile::getChildhoodContent).ne(UserProfile::getChildhoodContent, "")
|
||||||
|
.or().isNotNull(UserProfile::getPeakContent).ne(UserProfile::getPeakContent, "")
|
||||||
|
.or().isNotNull(UserProfile::getValleyContent).ne(UserProfile::getValleyContent, "")
|
||||||
|
);
|
||||||
|
|
||||||
|
List<UserProfile> profiles = list(queryWrapper);
|
||||||
|
log.info("找到 {} 个有生命事件数据的用户档案", profiles.size());
|
||||||
|
|
||||||
|
int migratedCount = 0;
|
||||||
|
|
||||||
|
for (UserProfile profile : profiles) {
|
||||||
|
String userId = profile.getUserId();
|
||||||
|
List<LifeEvent> eventsToSave = new ArrayList<>();
|
||||||
|
|
||||||
|
// 迁移童年记忆
|
||||||
|
if (StringUtils.hasText(profile.getChildhoodContent())) {
|
||||||
|
if (!isEventExists(userId, "childhood")) {
|
||||||
|
LifeEvent childhoodEvent = createLifeEventFromProfile(
|
||||||
|
userId,
|
||||||
|
"童年记忆",
|
||||||
|
profile.getChildhoodContent(),
|
||||||
|
profile.getChildhoodDate(),
|
||||||
|
"childhood"
|
||||||
|
);
|
||||||
|
eventsToSave.add(childhoodEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移高光时刻 (peak -> joy)
|
||||||
|
if (StringUtils.hasText(profile.getPeakContent())) {
|
||||||
|
if (!isEventExists(userId, "joy")) {
|
||||||
|
LifeEvent peakEvent = createLifeEventFromProfile(
|
||||||
|
userId,
|
||||||
|
"光芒闪耀的时刻",
|
||||||
|
profile.getPeakContent(),
|
||||||
|
profile.getPeakDate(),
|
||||||
|
"joy"
|
||||||
|
);
|
||||||
|
eventsToSave.add(peakEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移低谷时期 (valley -> low)
|
||||||
|
if (StringUtils.hasText(profile.getValleyContent())) {
|
||||||
|
if (!isEventExists(userId, "low")) {
|
||||||
|
LifeEvent valleyEvent = createLifeEventFromProfile(
|
||||||
|
userId,
|
||||||
|
"在暗夜中潜行",
|
||||||
|
profile.getValleyContent(),
|
||||||
|
profile.getValleyDate(),
|
||||||
|
"low"
|
||||||
|
);
|
||||||
|
eventsToSave.add(valleyEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量保存
|
||||||
|
if (!eventsToSave.isEmpty()) {
|
||||||
|
lifeEventService.saveBatch(eventsToSave);
|
||||||
|
migratedCount += eventsToSave.size();
|
||||||
|
log.info("用户 {} 迁移了 {} 条生命事件", userId, eventsToSave.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("生命事件数据迁移完成,共迁移 {} 条记录", migratedCount);
|
||||||
|
return migratedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否已存在指定标签的生命事件
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param tag 标签
|
||||||
|
* @return 是否存在
|
||||||
|
*/
|
||||||
|
private boolean isEventExists(String userId, String tag) {
|
||||||
|
LambdaQueryWrapper<LifeEvent> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(LifeEvent::getUserId, userId)
|
||||||
|
.eq(LifeEvent::getIsDeleted, 0)
|
||||||
|
.like(LifeEvent::getTags, tag);
|
||||||
|
return lifeEventService.count(wrapper) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从用户档案数据创建生命事件
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param title 事件标题
|
||||||
|
* @param content 事件内容
|
||||||
|
* @param eventDate 事件日期
|
||||||
|
* @param tag 事件标签
|
||||||
|
* @return 生命事件实体
|
||||||
|
*/
|
||||||
|
private LifeEvent createLifeEventFromProfile(String userId, String title, String content,
|
||||||
|
LocalDate eventDate, String tag) {
|
||||||
|
LifeEvent event = new LifeEvent();
|
||||||
|
event.setUserId(userId);
|
||||||
|
event.setTitle(title);
|
||||||
|
event.setContent(content);
|
||||||
|
event.setEventType("milestone");
|
||||||
|
event.setTags(List.of(tag));
|
||||||
|
|
||||||
|
// 设置事件日期,如果没有则使用当前时间
|
||||||
|
if (eventDate != null) {
|
||||||
|
event.setEventDate(eventDate.atStartOfDay());
|
||||||
|
} else {
|
||||||
|
event.setEventDate(java.time.LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
private UserProfileResponse convertToResponse(UserProfile userProfile) {
|
private UserProfileResponse convertToResponse(UserProfile userProfile) {
|
||||||
if (userProfile == null) {
|
if (userProfile == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+13
-7
@@ -11,13 +11,15 @@ import useStore from './store/useStore';
|
|||||||
/**
|
/**
|
||||||
* 路由守卫组件
|
* 路由守卫组件
|
||||||
* 根据登录状态和注册完成状态进行路由重定向
|
* 根据登录状态和注册完成状态进行路由重定向
|
||||||
|
* - requireAuth: 需要登录才能访问
|
||||||
|
* - requireOnboarding: 需要完成入站流程才能访问
|
||||||
*/
|
*/
|
||||||
const ProtectedRoute = ({ children, requireAuth = false, requireOnboarding = false }) => {
|
const ProtectedRoute = ({ children, requireAuth = false, requireOnboarding = false }) => {
|
||||||
const { isLoggedIn, registrationData } = useStore();
|
const { isLoggedIn, registrationData } = useStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 检查是否完成入站流程
|
// 检查是否完成入站流程(有昵称和未来愿景即视为已完成)
|
||||||
const hasCompletedOnboarding = registrationData.nickname && registrationData.future?.vision;
|
const hasCompletedOnboarding = !!(registrationData.nickname && registrationData.future?.vision);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (requireAuth && !isLoggedIn) {
|
if (requireAuth && !isLoggedIn) {
|
||||||
@@ -65,8 +67,8 @@ const AnimatedRoutes = () => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { isLoggedIn, registrationData } = useStore();
|
const { isLoggedIn, registrationData } = useStore();
|
||||||
|
|
||||||
// 检查是否完成入站流程
|
// 检查是否完成入站流程(有昵称和未来愿景即视为已完成)
|
||||||
const hasCompletedOnboarding = registrationData.nickname && registrationData.future?.vision;
|
const hasCompletedOnboarding = !!(registrationData.nickname && registrationData.future?.vision);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
@@ -89,15 +91,19 @@ const AnimatedRoutes = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 入站流程页 */}
|
{/* 入站流程页 - 已完成入站的用户直接跳转到首页 */}
|
||||||
<Route
|
<Route
|
||||||
path="/onboarding"
|
path="/onboarding"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute requireAuth>
|
!isLoggedIn ? (
|
||||||
|
<Navigate to="/" replace />
|
||||||
|
) : hasCompletedOnboarding ? (
|
||||||
|
<Navigate to="/dashboard" replace />
|
||||||
|
) : (
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<OnboardingPage />
|
<OnboardingPage />
|
||||||
</PageTransition>
|
</PageTransition>
|
||||||
</ProtectedRoute>
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ const GlassInput = ({
|
|||||||
className = '',
|
className = '',
|
||||||
id
|
id
|
||||||
}) => {
|
}) => {
|
||||||
|
// 日期类型输入框的特殊样式
|
||||||
|
const dateInputClass = type === 'date'
|
||||||
|
? 'cursor-pointer [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-100 [&::-webkit-calendar-picker-indicator]:w-full [&::-webkit-calendar-picker-indicator]:h-full [&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:top-0 [&::-webkit-calendar-picker-indicator]:left-0 [&::-webkit-calendar-picker-indicator]:bg-transparent'
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-2 ${className}`}>
|
<div className={`flex flex-col gap-2 ${className}`}>
|
||||||
{label && (
|
{label && (
|
||||||
@@ -31,15 +36,17 @@ const GlassInput = ({
|
|||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<input
|
<div className={type === 'date' ? 'relative' : ''}>
|
||||||
id={id}
|
<input
|
||||||
type={type}
|
id={id}
|
||||||
placeholder={placeholder}
|
type={type}
|
||||||
value={value}
|
placeholder={placeholder}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
value={value}
|
||||||
maxLength={maxLength}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="glass-input w-full focus:ring-2 focus:ring-orange-200/50"
|
maxLength={maxLength}
|
||||||
/>
|
className={`glass-input w-full focus:ring-2 focus:ring-orange-200/50 ${dateInputClass}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -86,6 +86,33 @@ body {
|
|||||||
color: rgba(255, 255, 255, 0.3);
|
color: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 日期输入框样式优化 */
|
||||||
|
input[type="date"].glass-input {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="date"].glass-input::-webkit-calendar-picker-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="date"].glass-input::-webkit-datetime-edit {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="date"].glass-input::-webkit-datetime-edit-fields-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const LoginPage = () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理登录提交
|
* 处理登录提交
|
||||||
|
* 登录成功后根据用户档案状态决定跳转目标
|
||||||
*/
|
*/
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (phone.length !== 11) {
|
if (phone.length !== 11) {
|
||||||
@@ -56,9 +57,14 @@ const LoginPage = () => {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 尝试调用后端登录
|
// 尝试调用后端登录,返回值包含 hasProfile 标识
|
||||||
await login(phone, code);
|
const result = await login(phone, code);
|
||||||
navigate('/onboarding');
|
// 根据用户档案状态决定跳转:已有档案直接进入首页,否则进入入站流程
|
||||||
|
if (result.hasProfile) {
|
||||||
|
navigate('/dashboard');
|
||||||
|
} else {
|
||||||
|
navigate('/onboarding');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 后端不可用时,使用本地验证
|
// 后端不可用时,使用本地验证
|
||||||
if (code === '888888') {
|
if (code === '888888') {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { PromptTagGroup } from '../components/PromptTag';
|
|||||||
import useStore from '../store/useStore';
|
import useStore from '../store/useStore';
|
||||||
import { inspirationClusters } from '../utils/constants';
|
import { inspirationClusters } from '../utils/constants';
|
||||||
import * as dictionaryService from '../services/dictionary';
|
import * as dictionaryService from '../services/dictionary';
|
||||||
|
import * as lifeEventService from '../services/lifeEvent';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OnboardingPage 组件
|
* OnboardingPage 组件
|
||||||
@@ -83,6 +84,28 @@ const OnboardingPage = () => {
|
|||||||
updateRegistration(dataToSave);
|
updateRegistration(dataToSave);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存生命事件到后端
|
||||||
|
* @param {Object} eventData - 事件数据 { date, text }
|
||||||
|
* @param {string} eventType - 事件类型标识
|
||||||
|
* @param {string} title - 事件标题
|
||||||
|
*/
|
||||||
|
const saveLifeEvent = async (eventData, eventType, title) => {
|
||||||
|
if (!eventData?.date || !eventData?.text) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await lifeEventService.createEvent({
|
||||||
|
title,
|
||||||
|
time: eventData.date,
|
||||||
|
content: eventData.text,
|
||||||
|
eventType: 'milestone',
|
||||||
|
tags: [eventType]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`保存${title}失败:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
saveStepData();
|
saveStepData();
|
||||||
if (currentStep < 5) {
|
if (currentStep < 5) {
|
||||||
@@ -90,7 +113,19 @@ const OnboardingPage = () => {
|
|||||||
} else {
|
} else {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
|
// 保存用户档案
|
||||||
await saveUserProfile();
|
await saveUserProfile();
|
||||||
|
|
||||||
|
// 保存生命事件(童年记忆、开心经历、低谷时期)
|
||||||
|
const eventsToSave = [
|
||||||
|
{ data: formData.childhood, type: 'childhood', title: '童年记忆' },
|
||||||
|
{ data: formData.joy, type: 'joy', title: '光芒闪耀的时刻' },
|
||||||
|
{ data: formData.low, type: 'low', title: '在暗夜中潜行' }
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
eventsToSave.map(({ data, type, title }) => saveLifeEvent(data, type, title))
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存档案失败:', error);
|
console.error('保存档案失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const transformToFrontendFormat = (backendData) => {
|
|||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
title: title || '',
|
title: title || '',
|
||||||
time: eventDate || '',
|
time: eventDate ? eventDate.split('T')[0] : '',
|
||||||
content: content || '',
|
content: content || '',
|
||||||
aiFeedback: aiReply || '',
|
aiFeedback: aiReply || '',
|
||||||
eventType: eventType || 'daily_log',
|
eventType: eventType || 'daily_log',
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ const useStore = create(
|
|||||||
* 登录
|
* 登录
|
||||||
* @param {string} phone - 手机号
|
* @param {string} phone - 手机号
|
||||||
* @param {string} smsCode - 验证码
|
* @param {string} smsCode - 验证码
|
||||||
|
* @returns {Promise<Object>} 包含 hasProfile 标识,用于判断是否需要跳转到 onboarding
|
||||||
*/
|
*/
|
||||||
login: async (phone, smsCode) => {
|
login: async (phone, smsCode) => {
|
||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
@@ -97,18 +98,24 @@ const useStore = create(
|
|||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
phone,
|
phone,
|
||||||
userId,
|
userId,
|
||||||
view: 'onboarding',
|
|
||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// 尝试加载用户档案
|
// 尝试加载用户档案,判断用户是否已完成注册
|
||||||
|
let hasProfile = false;
|
||||||
try {
|
try {
|
||||||
await get().loadUserProfile();
|
const profileData = await get().loadUserProfile();
|
||||||
|
// 检查档案是否完整(有昵称和未来愿景)
|
||||||
|
hasProfile = !!(profileData && profileData.nickname && profileData.future?.vision);
|
||||||
} catch {
|
} catch {
|
||||||
// 档案不存在,继续入站流程
|
// 档案不存在,需要进入入站流程
|
||||||
|
hasProfile = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
// 根据档案状态设置视图
|
||||||
|
set({ view: hasProfile ? 'dashboard' : 'onboarding' });
|
||||||
|
|
||||||
|
return { ...response, hasProfile };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ loading: false, error: error.message });
|
set({ loading: false, error: error.message });
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -62,11 +62,18 @@ const TimelineView = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按时间倒序排列事件
|
* 按事件时间倒序排列(最新的在最上面)
|
||||||
|
* 空日期的事件排在最后
|
||||||
*/
|
*/
|
||||||
const sortedEvents = [...lifeEvents].sort(
|
const sortedEvents = [...lifeEvents].sort((a, b) => {
|
||||||
(a, b) => new Date(b.time) - new Date(a.time)
|
// 如果两个都没有时间,保持原顺序
|
||||||
);
|
if (!a.time && !b.time) return 0;
|
||||||
|
// 没有时间的排在后面
|
||||||
|
if (!a.time) return 1;
|
||||||
|
if (!b.time) return -1;
|
||||||
|
// 按时间倒序(最新的在前)
|
||||||
|
return new Date(b.time) - new Date(a.time);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -98,7 +105,14 @@ const TimelineView = () => {
|
|||||||
{/* 事件卡片 */}
|
{/* 事件卡片 */}
|
||||||
<GlassCard className="border-white/5 hover:border-orange-200/20 transition-all duration-700">
|
<GlassCard className="border-white/5 hover:border-orange-200/20 transition-all duration-700">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<h4 className="text-xl font-medium text-white/80">{event.title}</h4>
|
<div className="flex items-center gap-3">
|
||||||
|
<h4 className="text-xl font-medium text-white/80">{event.title}</h4>
|
||||||
|
{event.tags && event.tags.length > 0 && (
|
||||||
|
<span className="text-[9px] px-2 py-1 rounded-full bg-orange-200/10 text-orange-200/60 uppercase tracking-wider">
|
||||||
|
{event.tags[0] === 'childhood' ? '童年' : event.tags[0] === 'joy' ? '高光' : event.tags[0] === 'low' ? '低谷' : event.tags[0]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span className="text-[10px] font-mono tracking-widest text-white/30 uppercase">
|
<span className="text-[10px] font-mono tracking-widest text-white/30 uppercase">
|
||||||
{event.time}
|
{event.time}
|
||||||
</span>
|
</span>
|
||||||
@@ -107,18 +121,20 @@ const TimelineView = () => {
|
|||||||
{event.content}
|
{event.content}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* AI 反馈区域 */}
|
{/* AI 反馈区域 - 仅在有反馈时显示 */}
|
||||||
<div className="ai-glow-card p-5 rounded-2xl bg-orange-200/[0.02] border border-orange-200/5">
|
{event.aiFeedback && (
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="ai-glow-card p-5 rounded-2xl bg-orange-200/[0.02] border border-orange-200/5">
|
||||||
<Sparkles className="w-3 h-3 text-orange-200" />
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="text-[9px] uppercase tracking-[0.2em] text-orange-200/60 font-bold">
|
<Sparkles className="w-3 h-3 text-orange-200" />
|
||||||
引路人洞察
|
<span className="text-[9px] uppercase tracking-[0.2em] text-orange-200/60 font-bold">
|
||||||
</span>
|
引路人洞察
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs italic text-white/50 leading-loose">
|
||||||
|
{event.aiFeedback}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs italic text-white/50 leading-loose">
|
)}
|
||||||
{event.aiFeedback}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import react from '@vitejs/plugin-react'
|
|||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => ({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
// 生产环境使用 /course-of-life/ 路径
|
// 生产环境使用 /course-of-life/ 路径,开发环境使用根路径
|
||||||
base: '/course-of-life/',
|
base: mode === 'production' ? '/course-of-life/' : '/',
|
||||||
server: {
|
server: {
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
管理后台部署脚本 - 将构建好的管理后台文件上传到服务器
|
||||||
|
使用方法: python deploy.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 配置变量
|
||||||
|
SERVER_IP = "101.200.208.45"
|
||||||
|
USERNAME = "root"
|
||||||
|
REMOTE_PATH = "/data/www/emotion-museum-admin"
|
||||||
|
|
||||||
|
# 本地路径
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.absolute()
|
||||||
|
DIST_DIR = SCRIPT_DIR / "dist"
|
||||||
|
|
||||||
|
|
||||||
|
class Colors:
|
||||||
|
"""终端颜色"""
|
||||||
|
GREEN = '\033[32m'
|
||||||
|
RED = '\033[31m'
|
||||||
|
YELLOW = '\033[33m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
|
||||||
|
def log_info(msg):
|
||||||
|
"""打印信息日志"""
|
||||||
|
print(f"{Colors.GREEN}✅{Colors.RESET} {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def log_error(msg):
|
||||||
|
"""打印错误日志"""
|
||||||
|
print(f"{Colors.RED}❌{Colors.RESET} {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def log_step(msg):
|
||||||
|
"""打印步骤日志"""
|
||||||
|
print(f"📦 {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd, cwd=None, shell=True, capture=True):
|
||||||
|
"""执行本地命令"""
|
||||||
|
try:
|
||||||
|
if capture:
|
||||||
|
result = subprocess.run(cmd, cwd=cwd, shell=shell, capture_output=True, text=True)
|
||||||
|
return result.returncode == 0, result.stdout, result.stderr
|
||||||
|
else:
|
||||||
|
result = subprocess.run(cmd, cwd=cwd, shell=shell)
|
||||||
|
return result.returncode == 0, "", ""
|
||||||
|
except Exception as e:
|
||||||
|
return False, "", str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def check_npm():
|
||||||
|
"""检查npm是否安装"""
|
||||||
|
success, _, _ = run_command("npm --version")
|
||||||
|
if not success:
|
||||||
|
log_error("错误: 未找到npm命令,请先安装Node.js")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def check_scp():
|
||||||
|
"""检查scp是否可用"""
|
||||||
|
success, _, _ = run_command("scp -V 2>&1 || echo ok")
|
||||||
|
# scp通常没有--version,但命令存在即可
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def clean_dist():
|
||||||
|
"""清理旧的构建文件"""
|
||||||
|
log_step("🧹 清理旧的构建文件...")
|
||||||
|
if DIST_DIR.exists():
|
||||||
|
shutil.rmtree(DIST_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def build_project():
|
||||||
|
"""构建项目"""
|
||||||
|
log_step("开始构建管理后台项目(生产环境)...")
|
||||||
|
|
||||||
|
os.chdir(SCRIPT_DIR)
|
||||||
|
|
||||||
|
# 设置环境变量并执行构建
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["NODE_ENV"] = "production"
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
"npm run build",
|
||||||
|
shell=True,
|
||||||
|
cwd=SCRIPT_DIR,
|
||||||
|
env=env
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
log_error("管理后台项目构建失败,请检查代码")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
log_error(f"构建失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
log_info("管理后台项目构建成功")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_dist():
|
||||||
|
"""验证dist目录是否存在"""
|
||||||
|
if not DIST_DIR.exists():
|
||||||
|
log_error("错误: 构建后dist目录仍不存在,请检查构建配置")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 检查关键文件
|
||||||
|
index_file = DIST_DIR / "index.html"
|
||||||
|
assets_dir = DIST_DIR / "assets"
|
||||||
|
|
||||||
|
if not index_file.exists():
|
||||||
|
log_error("错误: dist/index.html 不存在")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not assets_dir.exists():
|
||||||
|
log_error("错误: dist/assets 目录不存在")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def create_remote_dir():
|
||||||
|
"""创建远程目录"""
|
||||||
|
log_step("📁 创建远程目录...")
|
||||||
|
cmd = f'ssh {USERNAME}@{SERVER_IP} "mkdir -p {REMOTE_PATH}"'
|
||||||
|
success, _, stderr = run_command(cmd)
|
||||||
|
if not success:
|
||||||
|
log_error(f"创建远程目录失败: {stderr}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_files():
|
||||||
|
"""上传文件到服务器"""
|
||||||
|
log_step("📤 上传文件到服务器...")
|
||||||
|
|
||||||
|
print(f"正在上传文件到服务器 {SERVER_IP}...")
|
||||||
|
|
||||||
|
# 上传 index.html
|
||||||
|
index_file = DIST_DIR / "index.html"
|
||||||
|
cmd1 = f'scp "{index_file}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/'
|
||||||
|
success1, _, stderr1 = run_command(cmd1)
|
||||||
|
|
||||||
|
if not success1:
|
||||||
|
log_error(f"上传 index.html 失败: {stderr1}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 上传 assets 目录
|
||||||
|
assets_dir = DIST_DIR / "assets"
|
||||||
|
cmd2 = f'scp -r "{assets_dir}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/'
|
||||||
|
success2, _, stderr2 = run_command(cmd2)
|
||||||
|
|
||||||
|
if not success2:
|
||||||
|
log_error(f"上传 assets 目录失败: {stderr2}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def set_permissions():
|
||||||
|
"""设置文件权限"""
|
||||||
|
log_step("🔐 设置文件权限...")
|
||||||
|
cmd = f'ssh {USERNAME}@{SERVER_IP} "chmod -R 755 {REMOTE_PATH}"'
|
||||||
|
success, _, stderr = run_command(cmd)
|
||||||
|
if not success:
|
||||||
|
log_error(f"设置权限失败: {stderr}")
|
||||||
|
|
||||||
|
|
||||||
|
def deploy():
|
||||||
|
"""执行部署"""
|
||||||
|
print("开始部署管理后台应用到服务器...")
|
||||||
|
|
||||||
|
# 检查npm
|
||||||
|
check_npm()
|
||||||
|
|
||||||
|
# 清理旧构建
|
||||||
|
clean_dist()
|
||||||
|
|
||||||
|
# 构建项目
|
||||||
|
build_project()
|
||||||
|
|
||||||
|
# 验证构建结果
|
||||||
|
verify_dist()
|
||||||
|
|
||||||
|
# 创建远程目录
|
||||||
|
create_remote_dir()
|
||||||
|
|
||||||
|
# 上传文件
|
||||||
|
if upload_files():
|
||||||
|
# 设置权限
|
||||||
|
set_permissions()
|
||||||
|
|
||||||
|
log_info("管理后台部署完成!")
|
||||||
|
print(f"📱 访问地址: http://{SERVER_IP}/emotion-museum-admin/")
|
||||||
|
print("🔧 管理后台功能: AI配置管理、用户管理、数据统计等")
|
||||||
|
else:
|
||||||
|
log_error("部署失败,请检查:")
|
||||||
|
print("1. 服务器IP地址是否正确")
|
||||||
|
print("2. SSH密钥是否配置正确")
|
||||||
|
print("3. 服务器目录权限是否正确")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
deploy()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -26,5 +26,16 @@ export const menuConfig: MenuItem[] = [
|
|||||||
path: '/aiconfig',
|
path: '/aiconfig',
|
||||||
title: 'AI配置管理',
|
title: 'AI配置管理',
|
||||||
icon: 'Setting'
|
icon: 'Setting'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/tools',
|
||||||
|
title: '开发工具',
|
||||||
|
icon: 'Tools',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/tools/api-tester',
|
||||||
|
title: 'API接口调用'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -63,6 +63,20 @@ const routes: RouteRecordRaw[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tools',
|
||||||
|
component: Layout,
|
||||||
|
redirect: '/tools/api-tester',
|
||||||
|
meta: { title: '开发工具', icon: 'Tools' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'api-tester',
|
||||||
|
name: 'ApiTester',
|
||||||
|
component: () => import('@/views/tools/ApiTester.vue'),
|
||||||
|
meta: { title: 'API接口调用' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
name: 'NotFound',
|
name: 'NotFound',
|
||||||
|
|||||||
@@ -0,0 +1,659 @@
|
|||||||
|
<template>
|
||||||
|
<div class="api-tester">
|
||||||
|
<h2 class="page-title">API接口调用</h2>
|
||||||
|
|
||||||
|
<!-- 请求配置区域 -->
|
||||||
|
<el-card class="request-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>请求配置</span>
|
||||||
|
<el-button type="primary" @click="sendRequest" :loading="loading">
|
||||||
|
发送请求
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 请求方法和URL -->
|
||||||
|
<el-form :model="requestForm" label-width="100px">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-form-item label="请求方法">
|
||||||
|
<el-select v-model="requestForm.method" style="width: 100%">
|
||||||
|
<el-option label="GET" value="GET" />
|
||||||
|
<el-option label="POST" value="POST" />
|
||||||
|
<el-option label="PUT" value="PUT" />
|
||||||
|
<el-option label="DELETE" value="DELETE" />
|
||||||
|
<el-option label="PATCH" value="PATCH" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="20">
|
||||||
|
<el-form-item label="接口路径">
|
||||||
|
<el-input
|
||||||
|
v-model="requestForm.url"
|
||||||
|
placeholder="请输入接口路径,如:/api/user-profile/migrateLifeEvents"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prepend>{{ baseUrl }}</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 参数配置标签页 -->
|
||||||
|
<el-tabs v-model="activeTab" type="border-card">
|
||||||
|
<!-- Query参数 -->
|
||||||
|
<el-tab-pane label="Query参数" name="query">
|
||||||
|
<div class="params-header">
|
||||||
|
<span class="params-tip">URL查询参数(?key=value&key2=value2)</span>
|
||||||
|
<el-button type="primary" size="small" @click="addQueryParam">
|
||||||
|
添加参数
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="requestForm.queryParams" border size="small">
|
||||||
|
<el-table-column label="参数名" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.key" placeholder="参数名" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="参数值" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.value" placeholder="参数值" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="描述" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.description" placeholder="描述(可选)" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="80" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button type="danger" link size="small" @click="removeQueryParam($index)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 请求头 -->
|
||||||
|
<el-tab-pane label="请求头" name="headers">
|
||||||
|
<div class="params-header">
|
||||||
|
<span class="params-tip">自定义请求头(Authorization已自动添加)</span>
|
||||||
|
<el-button type="primary" size="small" @click="addHeader">
|
||||||
|
添加请求头
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="requestForm.headers" border size="small">
|
||||||
|
<el-table-column label="Header名" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.key" placeholder="Header名" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Header值" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.value" placeholder="Header值" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="描述" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.description" placeholder="描述(可选)" size="small" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="80" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button type="danger" link size="small" @click="removeHeader($index)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 请求体 -->
|
||||||
|
<el-tab-pane label="请求体" name="body">
|
||||||
|
<div class="params-header">
|
||||||
|
<span class="params-tip">请求体内容(JSON格式)</span>
|
||||||
|
<div class="body-actions">
|
||||||
|
<el-button type="success" size="small" @click="formatRequestBody">
|
||||||
|
格式化JSON
|
||||||
|
</el-button>
|
||||||
|
<el-button type="warning" size="small" @click="compressRequestBody">
|
||||||
|
压缩JSON
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="clearRequestBody">
|
||||||
|
清空
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-model="requestForm.body"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
placeholder='请输入JSON格式的请求体,如:{"key": "value"}'
|
||||||
|
class="body-textarea"
|
||||||
|
/>
|
||||||
|
<div v-if="bodyError" class="body-error">
|
||||||
|
<el-alert :title="bodyError" type="error" :closable="false" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 响应结果区域 -->
|
||||||
|
<el-card class="response-card" v-if="responseData">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>响应结果</span>
|
||||||
|
<div class="response-info">
|
||||||
|
<el-tag :type="getStatusType(responseData.status)" size="small">
|
||||||
|
状态码: {{ responseData.status }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag type="info" size="small">
|
||||||
|
耗时: {{ responseData.duration }}ms
|
||||||
|
</el-tag>
|
||||||
|
<el-button type="primary" size="small" @click="copyResponse">
|
||||||
|
复制响应
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-tabs v-model="responseTab" type="border-card">
|
||||||
|
<!-- 响应体 -->
|
||||||
|
<el-tab-pane label="响应体" name="body">
|
||||||
|
<div class="response-body">
|
||||||
|
<pre class="json-display">{{ formatJson(responseData.data) }}</pre>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 响应头 -->
|
||||||
|
<el-tab-pane label="响应头" name="headers">
|
||||||
|
<el-table :data="responseHeaders" border size="small">
|
||||||
|
<el-table-column prop="key" label="Header名" min-width="200" />
|
||||||
|
<el-table-column prop="value" label="Header值" min-width="300" />
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 历史记录区域 -->
|
||||||
|
<el-card class="history-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>请求历史(最近10条)</span>
|
||||||
|
<el-button type="danger" size="small" @click="clearHistory" :disabled="history.length === 0">
|
||||||
|
清空历史
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="history" border size="small" v-if="history.length > 0">
|
||||||
|
<el-table-column label="方法" width="80" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getMethodType(row.method)" size="small">
|
||||||
|
{{ row.method }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="url" label="接口路径" min-width="300" show-overflow-tooltip />
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)" size="small">
|
||||||
|
{{ row.status }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="duration" label="耗时" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.duration }}ms
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="time" label="时间" width="180" />
|
||||||
|
<el-table-column label="操作" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link size="small" @click="loadFromHistory(row)">
|
||||||
|
加载
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-else description="暂无请求历史" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数项接口
|
||||||
|
*/
|
||||||
|
interface ParamItem {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求表单接口
|
||||||
|
*/
|
||||||
|
interface RequestForm {
|
||||||
|
method: string
|
||||||
|
url: string
|
||||||
|
queryParams: ParamItem[]
|
||||||
|
headers: ParamItem[]
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应数据接口
|
||||||
|
*/
|
||||||
|
interface ResponseData {
|
||||||
|
status: number
|
||||||
|
data: any
|
||||||
|
headers: Record<string, string>
|
||||||
|
duration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 历史记录接口
|
||||||
|
*/
|
||||||
|
interface HistoryItem {
|
||||||
|
method: string
|
||||||
|
url: string
|
||||||
|
status: number
|
||||||
|
duration: number
|
||||||
|
time: string
|
||||||
|
queryParams: ParamItem[]
|
||||||
|
headers: ParamItem[]
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基础URL
|
||||||
|
const baseUrl = computed(() => import.meta.env.VITE_APP_BASE_API || '')
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 当前激活的标签页
|
||||||
|
const activeTab = ref('query')
|
||||||
|
const responseTab = ref('body')
|
||||||
|
|
||||||
|
// 请求表单
|
||||||
|
const requestForm = reactive<RequestForm>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '',
|
||||||
|
queryParams: [],
|
||||||
|
headers: [],
|
||||||
|
body: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应数据
|
||||||
|
const responseData = ref<ResponseData | null>(null)
|
||||||
|
|
||||||
|
// 请求体错误信息
|
||||||
|
const bodyError = ref('')
|
||||||
|
|
||||||
|
// 历史记录
|
||||||
|
const history = ref<HistoryItem[]>([])
|
||||||
|
|
||||||
|
// 响应头列表
|
||||||
|
const responseHeaders = computed(() => {
|
||||||
|
if (!responseData.value?.headers) return []
|
||||||
|
return Object.entries(responseData.value.headers).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: String(value)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加Query参数
|
||||||
|
*/
|
||||||
|
const addQueryParam = () => {
|
||||||
|
requestForm.queryParams.push({ key: '', value: '', description: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除Query参数
|
||||||
|
*/
|
||||||
|
const removeQueryParam = (index: number) => {
|
||||||
|
requestForm.queryParams.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加请求头
|
||||||
|
*/
|
||||||
|
const addHeader = () => {
|
||||||
|
requestForm.headers.push({ key: '', value: '', description: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除请求头
|
||||||
|
*/
|
||||||
|
const removeHeader = (index: number) => {
|
||||||
|
requestForm.headers.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化请求体JSON
|
||||||
|
*/
|
||||||
|
const formatRequestBody = () => {
|
||||||
|
if (!requestForm.body.trim()) return
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(requestForm.body)
|
||||||
|
requestForm.body = JSON.stringify(parsed, null, 2)
|
||||||
|
bodyError.value = ''
|
||||||
|
ElMessage.success('格式化成功')
|
||||||
|
} catch (e) {
|
||||||
|
bodyError.value = 'JSON格式错误: ' + (e as Error).message
|
||||||
|
ElMessage.error('JSON格式错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩请求体JSON
|
||||||
|
*/
|
||||||
|
const compressRequestBody = () => {
|
||||||
|
if (!requestForm.body.trim()) return
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(requestForm.body)
|
||||||
|
requestForm.body = JSON.stringify(parsed)
|
||||||
|
bodyError.value = ''
|
||||||
|
ElMessage.success('压缩成功')
|
||||||
|
} catch (e) {
|
||||||
|
bodyError.value = 'JSON格式错误: ' + (e as Error).message
|
||||||
|
ElMessage.error('JSON格式错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空请求体
|
||||||
|
*/
|
||||||
|
const clearRequestBody = () => {
|
||||||
|
requestForm.body = ''
|
||||||
|
bodyError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化JSON显示
|
||||||
|
*/
|
||||||
|
const formatJson = (data: any): string => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(data, null, 2)
|
||||||
|
} catch {
|
||||||
|
return String(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态码对应的标签类型
|
||||||
|
*/
|
||||||
|
const getStatusType = (status: number): string => {
|
||||||
|
if (status >= 200 && status < 300) return 'success'
|
||||||
|
if (status >= 400 && status < 500) return 'warning'
|
||||||
|
if (status >= 500) return 'danger'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求方法对应的标签类型
|
||||||
|
*/
|
||||||
|
const getMethodType = (method: string): string => {
|
||||||
|
const types: Record<string, string> = {
|
||||||
|
GET: 'success',
|
||||||
|
POST: 'primary',
|
||||||
|
PUT: 'warning',
|
||||||
|
DELETE: 'danger',
|
||||||
|
PATCH: 'info'
|
||||||
|
}
|
||||||
|
return types[method] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送请求
|
||||||
|
*/
|
||||||
|
const sendRequest = async () => {
|
||||||
|
if (!requestForm.url.trim()) {
|
||||||
|
ElMessage.warning('请输入接口路径')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证请求体JSON格式
|
||||||
|
if (requestForm.body.trim() && ['POST', 'PUT', 'PATCH'].includes(requestForm.method)) {
|
||||||
|
try {
|
||||||
|
JSON.parse(requestForm.body)
|
||||||
|
bodyError.value = ''
|
||||||
|
} catch (e) {
|
||||||
|
bodyError.value = 'JSON格式错误: ' + (e as Error).message
|
||||||
|
ElMessage.error('请求体JSON格式错误')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取token
|
||||||
|
const token = localStorage.getItem('adminToken')
|
||||||
|
|
||||||
|
// 构建请求头
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
// 添加自定义请求头
|
||||||
|
requestForm.headers.forEach(h => {
|
||||||
|
if (h.key.trim()) {
|
||||||
|
headers[h.key] = h.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 构建Query参数
|
||||||
|
const params: Record<string, string> = {}
|
||||||
|
requestForm.queryParams.forEach(p => {
|
||||||
|
if (p.key.trim()) {
|
||||||
|
params[p.key] = p.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
let data = undefined
|
||||||
|
if (requestForm.body.trim() && ['POST', 'PUT', 'PATCH'].includes(requestForm.method)) {
|
||||||
|
data = JSON.parse(requestForm.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await axios({
|
||||||
|
method: requestForm.method.toLowerCase(),
|
||||||
|
url: baseUrl.value + requestForm.url,
|
||||||
|
headers,
|
||||||
|
params,
|
||||||
|
data,
|
||||||
|
validateStatus: () => true // 不抛出HTTP错误
|
||||||
|
})
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime
|
||||||
|
|
||||||
|
// 保存响应数据
|
||||||
|
responseData.value = {
|
||||||
|
status: response.status,
|
||||||
|
data: response.data,
|
||||||
|
headers: response.headers as Record<string, string>,
|
||||||
|
duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到历史记录
|
||||||
|
addToHistory({
|
||||||
|
method: requestForm.method,
|
||||||
|
url: requestForm.url,
|
||||||
|
status: response.status,
|
||||||
|
duration,
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
queryParams: [...requestForm.queryParams],
|
||||||
|
headers: [...requestForm.headers],
|
||||||
|
body: requestForm.body
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
ElMessage.success(`请求成功 (${response.status})`)
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(`请求完成,状态码: ${response.status}`)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const duration = Date.now() - startTime
|
||||||
|
responseData.value = {
|
||||||
|
status: 0,
|
||||||
|
data: { error: error.message || '请求失败' },
|
||||||
|
headers: {},
|
||||||
|
duration
|
||||||
|
}
|
||||||
|
ElMessage.error('请求失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加到历史记录
|
||||||
|
*/
|
||||||
|
const addToHistory = (item: HistoryItem) => {
|
||||||
|
history.value.unshift(item)
|
||||||
|
// 只保留最近10条
|
||||||
|
if (history.value.length > 10) {
|
||||||
|
history.value = history.value.slice(0, 10)
|
||||||
|
}
|
||||||
|
// 保存到localStorage
|
||||||
|
localStorage.setItem('apiTesterHistory', JSON.stringify(history.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从历史记录加载
|
||||||
|
*/
|
||||||
|
const loadFromHistory = (item: HistoryItem) => {
|
||||||
|
requestForm.method = item.method
|
||||||
|
requestForm.url = item.url
|
||||||
|
requestForm.queryParams = item.queryParams ? [...item.queryParams] : []
|
||||||
|
requestForm.headers = item.headers ? [...item.headers] : []
|
||||||
|
requestForm.body = item.body || ''
|
||||||
|
ElMessage.success('已加载历史请求')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空历史记录
|
||||||
|
*/
|
||||||
|
const clearHistory = () => {
|
||||||
|
history.value = []
|
||||||
|
localStorage.removeItem('apiTesterHistory')
|
||||||
|
ElMessage.success('历史记录已清空')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制响应内容
|
||||||
|
*/
|
||||||
|
const copyResponse = async () => {
|
||||||
|
if (!responseData.value) return
|
||||||
|
try {
|
||||||
|
const text = formatJson(responseData.value.data)
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
ElMessage.success('已复制到剪贴板')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时加载历史记录
|
||||||
|
const initHistory = () => {
|
||||||
|
const saved = localStorage.getItem('apiTesterHistory')
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
history.value = JSON.parse(saved)
|
||||||
|
} catch {
|
||||||
|
history.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initHistory()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.api-tester {
|
||||||
|
.page-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-card,
|
||||||
|
.response-card,
|
||||||
|
.history-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.params-tip {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-textarea {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-error {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-body {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-display {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user