From 1947a2751419813c258eda9509911afedc0bb0dd Mon Sep 17 00:00:00 2001 From: Shuming Liu Date: Wed, 9 Sep 2026 16:14:33 +0800 Subject: [PATCH] fixed some bugs --- app.py | 110 ++++++++++++++++++++++++++++++++++++++++---- static/teacher.html | 24 +++++++++- static/teacher.js | 38 +++++++++++++++ 3 files changed, 163 insertions(+), 9 deletions(-) diff --git a/app.py b/app.py index 9a5395a..00b2651 100644 --- a/app.py +++ b/app.py @@ -11,6 +11,7 @@ import logging import os import secrets import sqlite3 +import socket import threading import time import urllib.parse @@ -26,7 +27,7 @@ DB_PATH = Path(os.environ.get("GROUP_DB", BASE_DIR / "selfgroup.db")) XLSX_PATH = Path(os.environ.get("GROUP_XLSX", BASE_DIR / "2025mt.xlsx")) STATIC_DIR = BASE_DIR / "static" TEACHER_USER = os.environ.get("TEACHER_USER", "teacher") -TEACHER_PASSWORD = os.environ.get("TEACHER_PASSWORD", "teacher123") +TEACHER_PASSWORD = os.environ.get("TEACHER_PASSWORD", "teacher!@#") GROUP_LIMIT = 6 SESSION_DAYS = 7 @@ -118,6 +119,10 @@ class GroupStore: "INSERT OR IGNORE INTO settings(key,value) VALUES('teacher_password',?)", (hash_password(TEACHER_PASSWORD),), ) + self.connection.execute( + "INSERT OR IGNORE INTO settings(key,value) VALUES('group_limit',?)", + (str(GROUP_LIMIT),), + ) def import_roster(self, path: Path) -> tuple[int, int]: rows = read_xlsx_rows(path) @@ -215,6 +220,20 @@ class GroupStore: def active_class(self) -> str: return self.get_setting("active_class") or "" + def group_limit(self) -> int: + value = self.get_setting("group_limit") + try: + limit = int(value or GROUP_LIMIT) + except ValueError: + limit = GROUP_LIMIT + return max(2, limit) + + def set_group_limit(self, limit: int) -> tuple[bool, str]: + if limit < 2 or limit > 100: + return False, "分组上限必须在 2 到 100 人之间" + self.set_setting("group_limit", str(limit)) + return True, f"分组上限已调整为 {limit} 人" + def is_locked(self, class_name: str) -> bool: with self.lock: row = self.connection.execute( @@ -292,6 +311,27 @@ class GroupStore: token = self.create_session("teacher", expected_user, None) return {"token": token, "teacher": {"username": expected_user}}, "登录成功" + def change_teacher_password( + self, session: dict[str, Any], old_password: str, new_password: str + ) -> tuple[bool, str]: + if session.get("role") != "teacher": + return False, "教师会话无效" + old_password = old_password or "" + new_password = new_password or "" + if len(new_password) < 8: + return False, "新密码至少 8 位" + if new_password == old_password: + return False, "新密码不能与原密码相同" + with self.lock: + expected_user = self.get_setting("teacher_username") or TEACHER_USER + if session.get("username") != expected_user: + return False, "教师账号已变更,请重新登录" + stored_password = self.get_setting("teacher_password") + if not verify_password(old_password, stored_password): + return False, "原密码不正确" + self.set_setting("teacher_password", hash_password(new_password)) + return True, "教师密码修改成功" + def change_student_password( self, session: dict[str, Any], old_password: str, new_password: str ) -> tuple[bool, str]: @@ -388,14 +428,14 @@ class GroupStore: **dict(row), "locked": self.is_locked(class_name), "member_count": len(members[row["id"]]), - "full": len(members[row["id"]]) >= GROUP_LIMIT, + "full": len(members[row["id"]]) >= self.group_limit(), "members": members[row["id"]], } ) return { "class_name": class_name, "locked": self.is_locked(class_name), - "group_limit": GROUP_LIMIT, + "group_limit": self.group_limit(), "groups": group_data, "ungrouped": [dict(row) for row in ungrouped_rows], "statistics": {"total_students": total, "grouped_students": grouped, "group_count": len(group_data)}, @@ -454,8 +494,9 @@ class GroupStore: member_count = self.connection.execute( "SELECT COUNT(*) AS n FROM group_members WHERE group_id=?", (group_id,) ).fetchone()["n"] - if member_count >= GROUP_LIMIT: - return False, f"该组已有 {GROUP_LIMIT} 人,不能再加入新成员" + group_limit = self.group_limit() + if member_count >= group_limit: + return False, f"该组已有 {group_limit} 人,不能再加入新成员" if current and not confirmed: role = "组长,原组会被解散" if current["leader_student_id"] == student_id else "成员" return False, f"你当前是“{current['name']}”的{role}。加入新组前必须退出原组;确认后才会执行。" @@ -613,8 +654,9 @@ class GroupStore: count = self.connection.execute( "SELECT COUNT(*) AS n FROM group_members WHERE group_id=?", (group_id,) ).fetchone()["n"] - if count >= GROUP_LIMIT: - return False, f"该组已有 {GROUP_LIMIT} 人;请先移出一人" + group_limit = self.group_limit() + if count >= group_limit: + return False, f"该组已有 {group_limit} 人;请先移出一人" if current_group: self.connection.execute( "DELETE FROM group_members WHERE student_id=?", (student_id,) @@ -914,6 +956,27 @@ class GroupRequestHandler: status = HTTPStatus.OK if ok else HTTPStatus.BAD_REQUEST return json_response(status, {"ok": ok, "message": message}) + if action == "teacher/change-password" and method == "POST": + session, error = self.require_teacher() + if error: + return error + data = self.read_json() + ok, message = STORE.change_teacher_password( + session, data.get("old_password", ""), data.get("new_password", "") + ) + status = HTTPStatus.OK if ok else HTTPStatus.BAD_REQUEST + return json_response(status, {"ok": ok, "message": message}) + + if action == "teacher/group-limit" and method == "POST": + _, error = self.require_teacher() + if error: + return error + data = self.read_json() + ok, message = STORE.set_group_limit(int(data.get("limit", 0))) + return json_response(HTTPStatus.OK if ok else HTTPStatus.BAD_REQUEST, { + "ok": ok, "message": message, "group_limit": STORE.group_limit() + }) + if action == "groups" and method == "POST": session, error = self.require_student() if error: @@ -1138,6 +1201,32 @@ def send_text(handler, status: int, text: str): handler.wfile.write(body) +def local_ipv4_addresses() -> list[str]: + addresses = 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 + + # Connecting a UDP socket reveals the address used for the default route. + 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) + + class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): route(self) @@ -1165,7 +1254,12 @@ def main() -> None: port = int(os.environ.get("PORT", "8000")) server = http.server.ThreadingHTTPServer((host, port), Handler) LOG.info("学生分组系统已启动:http://0.0.0.0:%d", port) - LOG.info("学生入口:http://<老师电脑IP>:%d/ 教师入口:http://<老师电脑IP>:%d/teacher", port, 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: diff --git a/static/teacher.html b/static/teacher.html index ff2ba05..f64d6e5 100644 --- a/static/teacher.html +++ b/static/teacher.html @@ -26,11 +26,33 @@
-

默认账号 teacher,默认密码 teacher123。正式使用前请通过环境变量修改。

+

默认账号 teacher,默认密码 teacher!@#。首次使用后请及时修改密码。