230 lines
6.4 KiB
Python
230 lines
6.4 KiB
Python
"""
|
||
爱莉希雅待办事项 - 项目启动入口
|
||
|
||
功能:
|
||
1. 编译前端项目
|
||
2. 启动 FastAPI 后端服务
|
||
"""
|
||
import os
|
||
import sys
|
||
import shutil
|
||
import subprocess
|
||
import time
|
||
|
||
|
||
# 路径配置
|
||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||
WEBUI_DIR = os.path.join(PROJECT_ROOT, "WebUI")
|
||
DIST_DIR = os.path.join(WEBUI_DIR, "dist")
|
||
WEBUI_TARGET = os.path.join(PROJECT_ROOT, "api", "webui")
|
||
API_MAIN = os.path.join(PROJECT_ROOT, "api", "app", "main.py")
|
||
|
||
|
||
def log_info(msg):
|
||
"""打印信息"""
|
||
print(f"[启动] {msg}")
|
||
|
||
|
||
def log_error(msg):
|
||
"""打印错误"""
|
||
print(f"[错误] {msg}", file=sys.stderr)
|
||
|
||
|
||
def needs_rebuild() -> bool:
|
||
"""判断是否需要重新编译前端(基于文件修改时间)"""
|
||
if not os.path.exists(DIST_DIR):
|
||
return True
|
||
src_dir = os.path.join(WEBUI_DIR, "src")
|
||
if not os.path.exists(src_dir):
|
||
return True
|
||
# 获取 dist 目录最新修改时间
|
||
dist_mtime = max(
|
||
os.path.getmtime(os.path.join(dp, f))
|
||
for dp, dn, filenames in os.walk(DIST_DIR)
|
||
for f in filenames
|
||
)
|
||
# 检查 src 目录中是否有比 dist 更新的文件
|
||
for dp, dn, filenames in os.walk(src_dir):
|
||
for f in filenames:
|
||
if f.endswith(('.vue', '.ts', '.js', '.scss', '.css')):
|
||
filepath = os.path.join(dp, f)
|
||
if os.path.getmtime(filepath) > dist_mtime:
|
||
return True
|
||
return False
|
||
|
||
|
||
def build_frontend():
|
||
"""编译前端项目"""
|
||
# 智能判断是否需要重新编译
|
||
if not needs_rebuild():
|
||
log_info("前端产物已是最新,跳过编译")
|
||
return True
|
||
|
||
log_info("开始编译前端项目...")
|
||
|
||
# 检查 WebUI 目录是否存在
|
||
if not os.path.exists(WEBUI_DIR):
|
||
log_error(f"WebUI 目录不存在: {WEBUI_DIR}")
|
||
return False
|
||
|
||
# 检查 node_modules 是否存在
|
||
node_modules = os.path.join(WEBUI_DIR, "node_modules")
|
||
if not os.path.exists(node_modules):
|
||
log_info("安装前端依赖...")
|
||
# Windows 上使用 npm.cmd
|
||
npm_cmd = "npm.cmd" if os.name == "nt" else "npm"
|
||
result = subprocess.run(
|
||
[npm_cmd, "install"],
|
||
cwd=WEBUI_DIR,
|
||
encoding="utf-8",
|
||
errors="ignore"
|
||
)
|
||
if result.returncode != 0:
|
||
log_error(f"安装依赖失败")
|
||
return False
|
||
|
||
# 执行编译
|
||
log_info("执行 npm run build...")
|
||
npm_cmd = "npm.cmd" if os.name == "nt" else "npm"
|
||
result = subprocess.run(
|
||
[npm_cmd, "run", "build"],
|
||
cwd=WEBUI_DIR,
|
||
encoding="utf-8",
|
||
errors="ignore"
|
||
)
|
||
if result.returncode != 0:
|
||
log_error(f"编译失败")
|
||
return False
|
||
|
||
log_info("前端编译完成!")
|
||
return True
|
||
|
||
|
||
def copy_dist_to_webui():
|
||
"""复制编译产物到 webui 目录"""
|
||
log_info("复制编译产物到 webui 目录...")
|
||
|
||
# 检查 dist 目录是否存在
|
||
if not os.path.exists(DIST_DIR):
|
||
log_error(f"编译产物目录不存在: {DIST_DIR}")
|
||
return False
|
||
|
||
# 删除旧的 webui 目录(如果存在)
|
||
if os.path.exists(WEBUI_TARGET):
|
||
shutil.rmtree(WEBUI_TARGET)
|
||
|
||
# 复制 dist 到 webui
|
||
shutil.copytree(DIST_DIR, WEBUI_TARGET)
|
||
log_info(f"编译产物已复制到: {WEBUI_TARGET}")
|
||
return True
|
||
|
||
|
||
def find_pid_on_port(port: int) -> int | None:
|
||
"""查找占用指定端口的进程 PID"""
|
||
if os.name == "nt":
|
||
result = subprocess.run(
|
||
["netstat", "-ano", "-p", "TCP"],
|
||
capture_output=True, text=True, encoding="gbk", errors="ignore"
|
||
)
|
||
for line in result.stdout.splitlines():
|
||
if f":{port}" in line and "LISTENING" in line:
|
||
parts = line.split()
|
||
return int(parts[-1])
|
||
else:
|
||
result = subprocess.run(
|
||
["lsof", "-t", "-i", f":{port}", "-sTCP:LISTEN"],
|
||
capture_output=True, text=True, errors="ignore"
|
||
)
|
||
output = result.stdout.strip()
|
||
if output:
|
||
return int(output.splitlines()[0])
|
||
return None
|
||
|
||
|
||
def kill_process(pid: int) -> bool:
|
||
"""终止指定 PID 的进程"""
|
||
log_info(f"正在终止占用端口的进程 (PID: {pid})...")
|
||
try:
|
||
if os.name == "nt":
|
||
subprocess.run(["taskkill", "/PID", str(pid), "/F"],
|
||
capture_output=True, timeout=10)
|
||
else:
|
||
os.kill(pid, 9)
|
||
time.sleep(1)
|
||
log_info(f"进程 {pid} 已终止")
|
||
return True
|
||
except Exception as e:
|
||
log_error(f"终止进程 {pid} 失败: {e}")
|
||
return False
|
||
|
||
|
||
def check_and_free_port(port: int) -> bool:
|
||
"""检测端口是否被占用,如果被占用则尝试释放"""
|
||
pid = find_pid_on_port(port)
|
||
if pid is None:
|
||
return True
|
||
|
||
log_info(f"端口 {port} 已被进程 {pid} 占用")
|
||
if kill_process(pid):
|
||
# 验证端口是否已释放
|
||
if find_pid_on_port(port) is None:
|
||
log_info(f"端口 {port} 已释放")
|
||
return True
|
||
log_error(f"端口 {port} 仍被占用,尝试再次终止...")
|
||
time.sleep(2)
|
||
pid2 = find_pid_on_port(port)
|
||
if pid2 is not None:
|
||
kill_process(pid2)
|
||
if find_pid_on_port(port) is None:
|
||
return True
|
||
|
||
log_error(f"无法释放端口 {port},请手动处理")
|
||
return False
|
||
|
||
|
||
def start_backend():
|
||
"""启动后端服务"""
|
||
log_info("启动后端服务...")
|
||
|
||
# 添加 api 目录到 Python 路径(不使用 os.chdir,避免全局副作用)
|
||
api_dir = os.path.join(PROJECT_ROOT, "api")
|
||
if api_dir not in sys.path:
|
||
sys.path.insert(0, api_dir)
|
||
|
||
from app.config import HOST, PORT
|
||
|
||
# 检查端口是否被占用,自动释放
|
||
if not check_and_free_port(PORT):
|
||
log_error(f"端口 {PORT} 无法释放,启动失败")
|
||
sys.exit(1)
|
||
|
||
import uvicorn
|
||
|
||
log_info(f"服务启动成功: http://{HOST}:{PORT}")
|
||
log_info(f"API 文档: http://{HOST}:{PORT}/docs")
|
||
log_info(f"前端页面: http://{HOST}:{PORT}/")
|
||
|
||
uvicorn.run("app.main:app", host=HOST, port=PORT, reload=False)
|
||
|
||
|
||
def main():
|
||
"""主函数"""
|
||
print("=" * 50)
|
||
print(" 爱莉希雅待办事项 - 项目启动")
|
||
print("=" * 50)
|
||
|
||
# 1. 编译前端
|
||
if not build_frontend():
|
||
sys.exit(1)
|
||
|
||
# 2. 复制编译产物
|
||
if not copy_dist_to_webui():
|
||
sys.exit(1)
|
||
|
||
# 3. 启动后端
|
||
start_backend()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|