rewrited logic structure

This commit is contained in:
2026-09-09 16:43:57 +08:00
parent 1947a27514
commit ac6d2cf3b4
20 changed files with 1717 additions and 1496 deletions

138
group_system/http/server.py Normal file
View File

@@ -0,0 +1,138 @@
"""HTTP 服务器、静态路由和应用启动流程。"""
from __future__ import annotations
import http.server
import logging
import os
import socket
import urllib.parse
from http import HTTPStatus
from pathlib import Path
from group_system.config import STATIC_DIR, XLSX_PATH
from group_system.http.api import GroupRequestHandler
from group_system.http.static_files import send_text, serve_file
from group_system.storage.store import GroupStore
LOG = logging.getLogger("group-system")
class GroupHTTPServer(http.server.ThreadingHTTPServer):
"""Carry the application store on the standard threading HTTP server."""
def __init__(self, address: tuple[str, int], store: GroupStore):
super().__init__(address, RequestHandler)
self.store = store
class RequestHandler(http.server.BaseHTTPRequestHandler):
@property
def store(self) -> GroupStore:
return self.server.store # type: ignore[attr-defined]
def do_GET(self) -> None:
route(self)
def do_POST(self) -> None:
route(self)
def do_HEAD(self) -> None:
route(self)
def log_message(self, format_string: str, *args) -> None:
LOG.info("%s - %s", self.address_string(), format_string % args)
def route(handler: RequestHandler) -> None:
path = urllib.parse.urlparse(handler.path).path
if path.startswith("/api/"):
request = GroupRequestHandler(handler, handler.store)
result = request.handle_api(path[len("/api/"):], handler.command)
if callable(result):
result(handler)
return
if path in ("/", "/index.html", "/student"):
serve_file(handler, STATIC_DIR / "index.html")
return
if path in ("/teacher", "/admin"):
serve_file(handler, STATIC_DIR / "teacher.html")
return
static_root = STATIC_DIR.resolve()
safe_path = (static_root / urllib.parse.unquote(path).lstrip("/")).resolve()
try:
safe_path.relative_to(static_root)
except ValueError:
send_text(handler, HTTPStatus.NOT_FOUND, "Not Found")
return
serve_file(handler, safe_path)
def local_ipv4_addresses() -> list[str]:
addresses: set[str] = set()
try:
host_name = socket.gethostname()
for item in socket.getaddrinfo(host_name, None, socket.AF_INET):
address = item[4][0]
if address != "127.0.0.1":
addresses.add(address)
except socket.gaierror:
pass
# A UDP connection does not send packets here, but reveals the default-route address.
try:
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
probe.connect(("192.0.2.1", 9))
address = probe.getsockname()[0]
if address != "127.0.0.1":
addresses.add(address)
finally:
probe.close()
except OSError:
pass
return sorted(addresses)
def bootstrap_store(store: GroupStore, xlsx_path: Path) -> None:
"""Import the initial roster only when the database is still empty."""
if any(store.class_list()):
return
if not xlsx_path.exists():
LOG.warning("未找到 %s,请先通过教师后台上传学生名单", xlsx_path)
return
student_count, class_count = store.import_roster(xlsx_path)
LOG.info("已导入 %s: %d 名学生 / %d 个班", xlsx_path, student_count, class_count)
def run(
store: GroupStore,
xlsx_path: Path = XLSX_PATH,
host: str | None = None,
port: int | None = None,
) -> None:
bootstrap_store(store, xlsx_path)
host = host or os.environ.get("HOST", "0.0.0.0")
port = port if port is not None else int(os.environ.get("PORT", "8000"))
server = GroupHTTPServer((host, port), store)
LOG.info("学生分组系统已启动:http://0.0.0.0:%d", port)
addresses = local_ipv4_addresses()
if addresses:
for address in addresses:
LOG.info(
"学生入口:http://%s:%d/ 教师入口:http://%s:%d/teacher",
address,
port,
address,
port,
)
else:
LOG.warning("未能自动获取局域网 IP,请在教师机上查看网络设置;端口:%d", port)
try:
server.serve_forever()
except KeyboardInterrupt:
LOG.info("服务停止")
finally:
server.server_close()