chore: unify education management naming

This commit is contained in:
Codex
2026-06-15 12:38:25 +08:00
parent b9468c7485
commit c6edbcfa80
13 changed files with 40 additions and 36 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
# 维护手册 # 维护手册
新时空教务管长期维护时,目标是每次更新都能做到可检查、可部署、可回滚,并且不误改业务数据。 新时空教务管理系统长期维护时,目标是每次更新都能做到可检查、可部署、可回滚,并且不误改业务数据。
## 日常更新流程 ## 日常更新流程
@@ -49,7 +49,7 @@
## Gitea 自动备份 ## Gitea 自动备份
新时空教务管推荐把 Gitea SSH 仓库配置为 `origin`,并用 `post-commit` hook 在每次提交后自动推送当前分支。 新时空教务管理系统推荐把 Gitea SSH 仓库配置为 `origin`,并用 `post-commit` hook 在每次提交后自动推送当前分支。
首次配置: 首次配置:
@@ -120,7 +120,7 @@ git push origin HEAD:<当前分支>
## 课程小结迁移维护 ## 课程小结迁移维护
- VPS 是 `classnotes.txt` 和 `学生课时账户.md` 的唯一正式写入方。 - VPS 是 `classnotes.txt` 和 `学生课时账户.md` 的唯一正式写入方。
- 本机 `com.xsk.records.sync` 常驻同步迁移后应停止,避免旧本地数据覆盖 VPS。 - 本机 `com.xsk.education-management.sync` 常驻同步迁移后应停止,避免旧本地数据覆盖 VPS。
- 本机课程小结采集脚本用 `XSK_INGEST_URL` 和 `XSK_INGEST_TOKEN` 推送批次;失败批次保存在本机 `推送失败队列/`。 - 本机课程小结采集脚本用 `XSK_INGEST_URL` 和 `XSK_INGEST_TOKEN` 推送批次;失败批次保存在本机 `推送失败队列/`。
- 管理后台的“课程小结审核”处理低置信或冲突小结;“操作记录”追踪接收、自动入账、重复、失败、审核批准和驳回。 - 管理后台的“课程小结审核”处理低置信或冲突小结;“操作记录”追踪接收、自动入账、重复、失败、审核批准和驳回。
- 历史小结导入使用 `scripts/import_course_summaries.py`,历史 `classnotes缺失.txt` 只生成审核任务,不自动扣课时。 - 历史小结导入使用 `scripts/import_course_summaries.py`,历史 `classnotes缺失.txt` 只生成审核任务,不自动扣课时。
+2 -2
View File
@@ -20,7 +20,7 @@ build:
docker compose build docker compose build
up: up:
docker compose up -d docker compose up -d --remove-orphans
ps: ps:
docker compose ps docker compose ps
@@ -38,7 +38,7 @@ deploy: check smoke
$(MAKE) --no-print-directory ps $(MAKE) --no-print-directory ps
logs: logs:
docker compose logs --tail=120 xsk-records-web docker compose logs --tail=120 xsk-education-management
install-gitea-backup: install-gitea-backup:
python3 scripts/install_gitea_backup_hook.py python3 scripts/install_gitea_backup_hook.py
+10 -10
View File
@@ -1,4 +1,4 @@
# 新时空教务管 # 新时空教务管理系统
这是一个面向新时空教务业务的综合管理系统。迁移后 VPS 是正式业务数据主机,负责保存 `classnotes.txt``学生课时账户.md`、课程小结库、审核任务和操作记录;本机只保留微信群聊天记录采集/识别,并把课程小结批量推送到 VPS。 这是一个面向新时空教务业务的综合管理系统。迁移后 VPS 是正式业务数据主机,负责保存 `classnotes.txt``学生课时账户.md`、课程小结库、审核任务和操作记录;本机只保留微信群聊天记录采集/识别,并把课程小结批量推送到 VPS。
@@ -7,13 +7,13 @@
- `app/`:FastAPI 后端和内置前端页面。 - `app/`:FastAPI 后端和内置前端页面。
- `MAINTENANCE.md`:日常检查、部署、数据保护和回滚流程。 - `MAINTENANCE.md`:日常检查、部署、数据保护和回滚流程。
- `Makefile`:常用维护命令入口。 - `Makefile`:常用维护命令入口。
- `scripts/deploy_to_vps.py`:部署教务管家服务到 VPS。 - `scripts/deploy_to_vps.py`:部署新时空教务管理系统到 VPS。
- `scripts/import_course_summaries.py`:一次性导入历史课程小结 Markdown。 - `scripts/import_course_summaries.py`:一次性导入历史课程小结 Markdown。
- `scripts/sync_to_vps.py`:旧版正式数据同步脚本,迁移后不要继续常驻运行。 - `scripts/sync_to_vps.py`:旧版正式数据同步脚本,迁移后不要继续常驻运行。
- `scripts/smoke_test.js`:轻量前端行为烟测。 - `scripts/smoke_test.js`:轻量前端行为烟测。
- `scripts/install_gitea_backup_hook.py`:安装提交后自动推送到 Gitea 的 Git hook。 - `scripts/install_gitea_backup_hook.py`:安装提交后自动推送到 Gitea 的 Git hook。
- `scripts/install_launch_agent.py`:安装 Mac 开机常驻同步任务。 - `scripts/install_launch_agent.py`:安装 Mac 开机常驻同步任务。
- `launchd/com.xsk.records.sync.plist.template`LaunchAgent 模板。 - `launchd/com.xsk.education-management.sync.plist.template`LaunchAgent 模板。
## 后端结构 ## 后端结构
@@ -32,7 +32,7 @@
推荐先配置 SSH key。若临时使用密码,可通过环境变量传入,不要写入仓库文件。 推荐先配置 SSH key。若临时使用密码,可通过环境变量传入,不要写入仓库文件。
```bash ```bash
cd /Users/yangdawei/Desktop/新时空业务源数据/tools/xsk-records-web cd /Users/yangdawei/Desktop/新时空业务源数据/tools/xsk-education-management
XSK_USE_SSHPASS=1 \ XSK_USE_SSHPASS=1 \
XSK_SSH_PASSWORD='填写SSH密码' \ XSK_SSH_PASSWORD='填写SSH密码' \
XSK_WEB_PASSWORD='填写网页访问密码' \ XSK_WEB_PASSWORD='填写网页访问密码' \
@@ -104,7 +104,7 @@ python3 /Users/yangdawei/Desktop/新时空业务源数据/新时空课程记录
```bash ```bash
cd /root/新时空数据/app cd /root/新时空数据/app
docker compose exec xsk-records-web python scripts/import_course_summaries.py \ docker compose exec xsk-education-management python scripts/import_course_summaries.py \
--source /data/import/课程小结采集 \ --source /data/import/课程小结采集 \
--target /data/course_summaries \ --target /data/course_summaries \
--state /data/course_summary_state.json \ --state /data/course_summary_state.json \
@@ -160,21 +160,21 @@ python3 scripts/install_launch_agent.py --use-sshpass
日志位置: 日志位置:
```text ```text
~/Library/Logs/xsk-records-web/sync.log ~/Library/Logs/xsk-education-management/sync.log
~/Library/Logs/xsk-records-web/sync.err.log ~/Library/Logs/xsk-education-management/sync.err.log
``` ```
查看任务: 查看任务:
```bash ```bash
launchctl list | grep com.xsk.records.sync launchctl list | grep com.xsk.education-management.sync
``` ```
卸载任务: 卸载任务:
```bash ```bash
launchctl unload ~/Library/LaunchAgents/com.xsk.records.sync.plist launchctl unload ~/Library/LaunchAgents/com.xsk.education-management.sync.plist
rm ~/Library/LaunchAgents/com.xsk.records.sync.plist rm ~/Library/LaunchAgents/com.xsk.education-management.sync.plist
``` ```
## 更换网页访问密码 ## 更换网页访问密码
+1 -1
View File
@@ -56,7 +56,7 @@ def is_records_authenticated(
request, request,
RECORDS_SESSION_COOKIE, RECORDS_SESSION_COOKIE,
BASIC_AUTH_PASSWORD, BASIC_AUTH_PASSWORD,
b"xsk-records-web-session-v1", b"xsk-education-management-records-session-v1",
) or has_valid_basic_auth(credentials, BASIC_AUTH_PASSWORD) ) or has_valid_basic_auth(credentials, BASIC_AUTH_PASSWORD)
+1 -1
View File
@@ -20,7 +20,7 @@ ACCOUNTS_AUTH_PASSWORD = os.getenv("ACCOUNTS_AUTH_PASSWORD") or os.getenv("ACCOU
ADMIN_AUTH_PASSWORD = os.getenv("ADMIN_AUTH_PASSWORD") or ACCOUNTS_AUTH_PASSWORD ADMIN_AUTH_PASSWORD = os.getenv("ADMIN_AUTH_PASSWORD") or ACCOUNTS_AUTH_PASSWORD
INGEST_AUTH_TOKEN = os.getenv("INGEST_AUTH_TOKEN", "") INGEST_AUTH_TOKEN = os.getenv("INGEST_AUTH_TOKEN", "")
RECORDS_SESSION_COOKIE = "xsk_records_session" RECORDS_SESSION_COOKIE = "xsk_education_management_records_session"
ACCOUNTS_SESSION_COOKIE = "xsk_accounts_session" ACCOUNTS_SESSION_COOKIE = "xsk_accounts_session"
ADMIN_SESSION_COOKIE = "xsk_admin_session" ADMIN_SESSION_COOKIE = "xsk_admin_session"
SESSION_MAX_AGE = 60 * 60 * 24 * 30 SESSION_MAX_AGE = 60 * 60 * 24 * 30
+1 -1
View File
@@ -6,7 +6,7 @@ from fastapi.responses import JSONResponse
from .routers import accounts, admin, health, ingest, pages, records from .routers import accounts, admin, health, ingest, pages, records
app = FastAPI(title="新时空教务管", version="1.0.0") app = FastAPI(title="新时空教务管理系统", version="1.0.0")
@app.exception_handler(ValueError) @app.exception_handler(ValueError)
+2 -2
View File
@@ -241,7 +241,7 @@ def login_page(
return RedirectResponse(url=next_path, status_code=status.HTTP_303_SEE_OTHER) return RedirectResponse(url=next_path, status_code=status.HTTP_303_SEE_OTHER)
return HTMLResponse( return HTMLResponse(
render_login_page( render_login_page(
title="新时空教务管", title="新时空教务管理系统",
action="/login", action="/login",
next_path=next_path, next_path=next_path,
has_error=has_error, has_error=has_error,
@@ -260,7 +260,7 @@ async def login_submit(request: Request):
response = RedirectResponse(url=next_path, status_code=status.HTTP_303_SEE_OTHER) response = RedirectResponse(url=next_path, status_code=status.HTTP_303_SEE_OTHER)
response.set_cookie( response.set_cookie(
RECORDS_SESSION_COOKIE, RECORDS_SESSION_COOKIE,
session_token(records_password, b"xsk-records-web-session-v1"), session_token(records_password, b"xsk-education-management-records-session-v1"),
max_age=SESSION_MAX_AGE, max_age=SESSION_MAX_AGE,
httponly=True, httponly=True,
samesite="lax", samesite="lax",
+2 -2
View File
@@ -3,13 +3,13 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>新时空教务管</title> <title>新时空教务管理系统</title>
<link rel="stylesheet" href="/static/styles.css?v=20260613-date-time-sort" /> <link rel="stylesheet" href="/static/styles.css?v=20260613-date-time-sort" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
<div> <div>
<h1>新时空教务管</h1> <h1>新时空教务管理系统</h1>
<p id="healthText">正在读取数据状态</p> <p id="healthText">正在读取数据状态</p>
</div> </div>
<div class="top-actions"> <div class="top-actions">
+2 -2
View File
@@ -1,10 +1,10 @@
services: services:
xsk-records-web: xsk-education-management:
build: build:
context: . context: .
args: args:
PYTHON_IMAGE: ${PYTHON_IMAGE:-python:3.12-slim} PYTHON_IMAGE: ${PYTHON_IMAGE:-python:3.12-slim}
container_name: xsk-records-web container_name: xsk-education-management
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- .env - .env
@@ -3,20 +3,20 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>Label</key> <key>Label</key>
<string>com.xsk.records.sync</string> <string>com.xsk.education-management.sync</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/usr/bin/python3</string> <string>/usr/bin/python3</string>
<string>/Users/yangdawei/Desktop/新时空业务源数据/tools/xsk-records-web/scripts/sync_to_vps.py</string> <string>/Users/yangdawei/Desktop/新时空业务源数据/tools/xsk-education-management/scripts/sync_to_vps.py</string>
</array> </array>
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>
<key>KeepAlive</key> <key>KeepAlive</key>
<true/> <true/>
<key>StandardOutPath</key> <key>StandardOutPath</key>
<string>/Users/yangdawei/Library/Logs/xsk-records-web/sync.log</string> <string>/Users/yangdawei/Library/Logs/xsk-education-management/sync.log</string>
<key>StandardErrorPath</key> <key>StandardErrorPath</key>
<string>/Users/yangdawei/Library/Logs/xsk-records-web/sync.err.log</string> <string>/Users/yangdawei/Library/Logs/xsk-education-management/sync.err.log</string>
<key>EnvironmentVariables</key> <key>EnvironmentVariables</key>
<dict> <dict>
<key>PATH</key> <key>PATH</key>
+3 -3
View File
@@ -19,7 +19,7 @@ DEFAULT_IDENTITY_FILE = Path.home() / ".ssh" / "xsk_records_vps_ed25519"
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="部署新时空课程记录查询网页到 VPS") parser = argparse.ArgumentParser(description="部署新时空教务管理系统到 VPS")
parser.add_argument("--remote-host", default=os.getenv("XSK_REMOTE_HOST", DEFAULT_REMOTE_HOST)) parser.add_argument("--remote-host", default=os.getenv("XSK_REMOTE_HOST", DEFAULT_REMOTE_HOST))
parser.add_argument("--remote-user", default=os.getenv("XSK_REMOTE_USER", DEFAULT_REMOTE_USER)) parser.add_argument("--remote-user", default=os.getenv("XSK_REMOTE_USER", DEFAULT_REMOTE_USER))
parser.add_argument("--remote-port", type=int, default=int(os.getenv("XSK_REMOTE_PORT", str(DEFAULT_REMOTE_PORT)))) parser.add_argument("--remote-port", type=int, default=int(os.getenv("XSK_REMOTE_PORT", str(DEFAULT_REMOTE_PORT))))
@@ -27,7 +27,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--identity-file", "--identity-file",
default=os.getenv("XSK_IDENTITY_FILE", str(DEFAULT_IDENTITY_FILE) if DEFAULT_IDENTITY_FILE.exists() else ""), default=os.getenv("XSK_IDENTITY_FILE", str(DEFAULT_IDENTITY_FILE) if DEFAULT_IDENTITY_FILE.exists() else ""),
help="SSH 私钥路径;默认使用 ~/.ssh/xsk_records_vps_ed25519(如果存在)", help="SSH 私钥路径;默认兼容使用历史文件 ~/.ssh/xsk_records_vps_ed25519(如果存在)",
) )
parser.add_argument("--app-port", default=os.getenv("XSK_APP_PORT", "18080")) parser.add_argument("--app-port", default=os.getenv("XSK_APP_PORT", "18080"))
parser.add_argument( parser.add_argument(
@@ -137,7 +137,7 @@ def main() -> int:
run(ssh_command(args) + [f"mkdir -p {shlex.quote(app_dir)} {shlex.quote(data_dir)}"]) run(ssh_command(args) + [f"mkdir -p {shlex.quote(app_dir)} {shlex.quote(data_dir)}"])
run(rsync_command(args) + [f"{PROJECT_DIR}/", f"{base_remote(args)}:{app_dir}/"]) run(rsync_command(args) + [f"{PROJECT_DIR}/", f"{base_remote(args)}:{app_dir}/"])
run(rsync_command(args) + [str(env_file), f"{base_remote(args)}:{app_dir}/.env"]) run(rsync_command(args) + [str(env_file), f"{base_remote(args)}:{app_dir}/.env"])
run(ssh_command(args) + [f"cd {shlex.quote(app_dir)} && docker compose up -d --build"]) run(ssh_command(args) + [f"cd {shlex.quote(app_dir)} && docker compose up -d --build --remove-orphans"])
print(f"部署完成: http://{args.remote_host}:{args.app_port}/") print(f"部署完成: http://{args.remote_host}:{args.app_port}/")
finally: finally:
env_file.unlink(missing_ok=True) env_file.unlink(missing_ok=True)
+7 -3
View File
@@ -9,13 +9,15 @@ import subprocess
PROJECT_DIR = Path(__file__).resolve().parents[1] PROJECT_DIR = Path(__file__).resolve().parents[1]
LABEL = "com.xsk.records.sync" LABEL = "com.xsk.education-management.sync"
OLD_LABEL = "com.xsk.records.sync"
PLIST_PATH = Path.home() / "Library" / "LaunchAgents" / f"{LABEL}.plist" PLIST_PATH = Path.home() / "Library" / "LaunchAgents" / f"{LABEL}.plist"
LOG_DIR = Path.home() / "Library" / "Logs" / "xsk-records-web" OLD_PLIST_PATH = Path.home() / "Library" / "LaunchAgents" / f"{OLD_LABEL}.plist"
LOG_DIR = Path.home() / "Library" / "Logs" / "xsk-education-management"
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="安装新时空课程记录同步 LaunchAgent") parser = argparse.ArgumentParser(description="安装新时空教务管理系统同步 LaunchAgent")
parser.add_argument("--use-sshpass", action="store_true", default=os.getenv("XSK_USE_SSHPASS", "") == "1") parser.add_argument("--use-sshpass", action="store_true", default=os.getenv("XSK_USE_SSHPASS", "") == "1")
parser.add_argument("--ssh-password", default=os.getenv("XSK_SSH_PASSWORD")) parser.add_argument("--ssh-password", default=os.getenv("XSK_SSH_PASSWORD"))
return parser.parse_args() return parser.parse_args()
@@ -50,6 +52,8 @@ def main() -> int:
with PLIST_PATH.open("wb") as handle: with PLIST_PATH.open("wb") as handle:
plistlib.dump(plist, handle) plistlib.dump(plist, handle)
subprocess.run(["launchctl", "unload", str(OLD_PLIST_PATH)], check=False, capture_output=True)
OLD_PLIST_PATH.unlink(missing_ok=True)
subprocess.run(["launchctl", "unload", str(PLIST_PATH)], check=False, capture_output=True) subprocess.run(["launchctl", "unload", str(PLIST_PATH)], check=False, capture_output=True)
subprocess.run(["launchctl", "load", str(PLIST_PATH)], check=True) subprocess.run(["launchctl", "load", str(PLIST_PATH)], check=True)
print(f"已安装并启动 LaunchAgent: {PLIST_PATH}") print(f"已安装并启动 LaunchAgent: {PLIST_PATH}")
+2 -2
View File
@@ -45,7 +45,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--identity-file", "--identity-file",
default=os.getenv("XSK_IDENTITY_FILE", str(DEFAULT_IDENTITY_FILE) if DEFAULT_IDENTITY_FILE.exists() else ""), default=os.getenv("XSK_IDENTITY_FILE", str(DEFAULT_IDENTITY_FILE) if DEFAULT_IDENTITY_FILE.exists() else ""),
help="SSH 私钥路径;默认使用 ~/.ssh/xsk_records_vps_ed25519(如果存在)", help="SSH 私钥路径;默认兼容使用历史文件 ~/.ssh/xsk_records_vps_ed25519(如果存在)",
) )
parser.add_argument( parser.add_argument(
"--use-sshpass", "--use-sshpass",
@@ -161,7 +161,7 @@ def current_signatures(local_dir: Path) -> dict[str, tuple[int, int]]:
def watch(config: Config) -> None: def watch(config: Config) -> None:
log("启动课程记录同步监听") log("启动新时空教务管理系统数据同步监听")
signatures: dict[str, tuple[int, int]] = {} signatures: dict[str, tuple[int, int]] = {}
while True: while True:
try: try: