fixed some bugs
This commit is contained in:
110
app.py
110
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:
|
||||
|
||||
@@ -26,11 +26,33 @@
|
||||
<input id="password" type="password" autocomplete="current-password" required>
|
||||
<button class="full" data-working type="submit">登录后台</button>
|
||||
<div class="notice" id="loginNotice"></div>
|
||||
<p class="hint">默认账号 teacher,默认密码 teacher123。正式使用前请通过环境变量修改。</p>
|
||||
<p class="hint">默认账号 teacher,默认密码 teacher!@#。首次使用后请及时修改密码。</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="mainArea" class="hidden">
|
||||
<section class="panel">
|
||||
<div class="section-head"><h2>账号安全</h2></div>
|
||||
<form class="form-line" id="teacherPasswordForm">
|
||||
<input id="teacherOldPassword" type="password" autocomplete="current-password" placeholder="原密码" required>
|
||||
<input id="teacherNewPassword" type="password" autocomplete="new-password" minlength="8" placeholder="新密码(至少 8 位)" required>
|
||||
<input id="teacherConfirmPassword" type="password" autocomplete="new-password" minlength="8" placeholder="确认新密码" required>
|
||||
<button data-working type="submit">修改密码</button>
|
||||
</form>
|
||||
<div class="notice" id="teacherPasswordNotice"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-head"><h2>分组设置</h2></div>
|
||||
<form class="form-line" id="groupLimitForm">
|
||||
<input id="groupLimit" type="number" min="2" max="100" required>
|
||||
<span>人</span>
|
||||
<button data-working type="submit">保存上限</button>
|
||||
</form>
|
||||
<p class="hint">调整后立即对所有班级生效;已有小组不会被自动拆分。</p>
|
||||
<div class="notice" id="groupLimitNotice"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-head">
|
||||
<h2>班级管理</h2>
|
||||
|
||||
@@ -12,6 +12,7 @@ function renderState() {
|
||||
el('mainArea').classList.remove('hidden');
|
||||
el('teacherToolbar').classList.remove('hidden');
|
||||
el('teacherChip').textContent = `教师:${state.username}`;
|
||||
el('groupLimit').value = state.snapshot.group_limit;
|
||||
renderClassTabs();
|
||||
renderStatistics();
|
||||
renderGroups();
|
||||
@@ -166,6 +167,41 @@ async function submitLogin(event) {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitTeacherPassword(event) {
|
||||
event.preventDefault();
|
||||
const oldPassword = el('teacherOldPassword').value;
|
||||
const newPassword = el('teacherNewPassword').value;
|
||||
const confirmPassword = el('teacherConfirmPassword').value;
|
||||
const notice = el('teacherPasswordNotice');
|
||||
notice.textContent = '';
|
||||
if (newPassword !== confirmPassword) {
|
||||
notice.textContent = '两次输入的新密码不一致';
|
||||
return;
|
||||
}
|
||||
setButtonsDisabled(el('teacherPasswordForm'), true);
|
||||
const result = await api('teacher/change-password', {
|
||||
method: 'POST', json: { old_password: oldPassword, new_password: newPassword }
|
||||
});
|
||||
setButtonsDisabled(el('teacherPasswordForm'), false);
|
||||
notice.textContent = result.ok ? '' : result.message;
|
||||
toast(result.message, !result.ok);
|
||||
if (result.ok) {
|
||||
el('teacherPasswordForm').reset();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitGroupLimit(event) {
|
||||
event.preventDefault();
|
||||
setButtonsDisabled(el('groupLimitForm'), true);
|
||||
const result = await api('teacher/group-limit', {
|
||||
method: 'POST', json: { limit: Number(el('groupLimit').value) }
|
||||
});
|
||||
setButtonsDisabled(el('groupLimitForm'), false);
|
||||
el('groupLimitNotice').textContent = result.ok ? '' : result.message;
|
||||
toast(result.message, !result.ok);
|
||||
if (result.ok) await loadState();
|
||||
}
|
||||
|
||||
async function switchClass(className) {
|
||||
const result = await api('teacher/class', { method: 'POST', json: { class_name: className } });
|
||||
toast(result.message, !result.ok);
|
||||
@@ -211,6 +247,8 @@ async function logout() {
|
||||
}
|
||||
|
||||
el('loginForm').addEventListener('submit', submitLogin);
|
||||
el('teacherPasswordForm').addEventListener('submit', submitTeacherPassword);
|
||||
el('groupLimitForm').addEventListener('submit', submitGroupLimit);
|
||||
el('logoutButton').addEventListener('click', logout);
|
||||
el('lockButton').addEventListener('click', toggleLock);
|
||||
el('createGroupForm').addEventListener('submit', submitCreateGroup);
|
||||
|
||||
Reference in New Issue
Block a user