Files
ToDoList/main.py
祀梦 2979197b1c release: Elysia ToDo v1.0.0
鍏ㄦ爤涓汉淇℃伅绠$悊搴旂敤锛岄泦鎴愬緟鍔炰换鍔°€佷範鎯墦鍗°€佺邯蹇垫棩鎻愰啋銆佽祫浜ф€昏鍔熻兘銆

Made-with: Cursor
2026-03-14 22:21:26 +08:00

230 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
爱莉希雅待办事项 - 项目启动入口
功能:
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()