291 lines
10 KiB
Python
291 lines
10 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
莞工教务系统 CLI - 直接通过API操作选课
|
||
|
|
======================================
|
||
|
|
依赖: browser-act (已安装), Python 3.x
|
||
|
|
使用前需要已在 browser-act 中登录教务系统并保持会话活跃。
|
||
|
|
|
||
|
|
用法:
|
||
|
|
python3 dgut.py list-cycles # 列出可选课的轮次
|
||
|
|
python3 dgut.py list-tabs # 探测选课界面开放了哪些标签页
|
||
|
|
python3 dgut.py list-courses [标签序号] # 列出课程 (默认第1个有API的标签)
|
||
|
|
python3 dgut.py select <kcid/名称> # 选课
|
||
|
|
python3 dgut.py drop <kcid/名称> # 退课
|
||
|
|
python3 dgut.py search <关键词> # 搜索课程
|
||
|
|
"""
|
||
|
|
import argparse
|
||
|
|
import sys
|
||
|
|
import textwrap
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
# 将 dgut-gui 目录加入 path 以导入 dgut_core
|
||
|
|
import os
|
||
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "dgut-gui"))
|
||
|
|
|
||
|
|
from dgut_core import (
|
||
|
|
ORIGIN,
|
||
|
|
discover_tabs,
|
||
|
|
drop_course,
|
||
|
|
ensure_in_course_selection,
|
||
|
|
ensure_session,
|
||
|
|
fetch_api,
|
||
|
|
get_courses,
|
||
|
|
get_cycles,
|
||
|
|
select_course,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# ─── 全局状态 ────────────────────────────────────────────────────────────
|
||
|
|
_current_tab: Optional[dict] = None
|
||
|
|
_tabs: list[dict] = []
|
||
|
|
_course_cache: list[dict] = []
|
||
|
|
|
||
|
|
|
||
|
|
# ─── 命令实现 ────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def cmd_list_cycles():
|
||
|
|
cycles = get_cycles()
|
||
|
|
if not cycles:
|
||
|
|
print("📭 当前没有可选的选课轮次。")
|
||
|
|
return
|
||
|
|
print(f"\n{'='*60}")
|
||
|
|
print(f" 📚 可选选课轮次 ({len(cycles)} 个)")
|
||
|
|
print(f"{'='*60}")
|
||
|
|
for i, cyc in enumerate(cycles, 1):
|
||
|
|
print(f"\n [{i}] {cyc.get('xklc_mc', '未知')}")
|
||
|
|
print(f" 学期: {cyc.get('xnxq01id', '-')}")
|
||
|
|
print(f" 时间: {cyc.get('xksj', '-')}")
|
||
|
|
print(f" 状态: {'进行中 ✅' if cyc.get('xkzt') == '1' else '未开始 ⏳'}")
|
||
|
|
print(f" 选退: {cyc.get('txkzmc', '-')}")
|
||
|
|
print(f" jx0502zbid: {cyc.get('jx0502zbid', '-')}")
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_list_tabs():
|
||
|
|
global _tabs
|
||
|
|
|
||
|
|
print("🔄 进入选课界面...")
|
||
|
|
if not ensure_in_course_selection():
|
||
|
|
print("⚠️ 无法进入选课界面,请检查浏览器状态。")
|
||
|
|
return
|
||
|
|
|
||
|
|
cycles = get_cycles()
|
||
|
|
if not cycles:
|
||
|
|
print("📭 没有选课轮次,无法探测标签页。")
|
||
|
|
return
|
||
|
|
|
||
|
|
# 用第一个进行中的轮次
|
||
|
|
cyc = next((c for c in cycles if c.get("xkzt") == "1"), cycles[0])
|
||
|
|
jx0502zbid = cyc.get("jx0502zbid", "")
|
||
|
|
if not jx0502zbid:
|
||
|
|
print("❌ 无法获取轮次ID。")
|
||
|
|
return
|
||
|
|
|
||
|
|
print(f"\n📋 选课轮次: {cyc.get('xklc_mc', '-')}")
|
||
|
|
print(f"{'='*60}")
|
||
|
|
_tabs = discover_tabs(jx0502zbid)
|
||
|
|
if not _tabs:
|
||
|
|
print("⚠️ 未探测到任何标签页。")
|
||
|
|
return
|
||
|
|
|
||
|
|
print(f"\n 共发现 {len(_tabs)} 个标签页:\n")
|
||
|
|
for i, t in enumerate(_tabs, 1):
|
||
|
|
api_tag = " ✅ 有课程数据" if t.get("api") else " 📄 信息页"
|
||
|
|
print(f" [{i}] {t['label']}{api_tag}")
|
||
|
|
print(f" ID: {t['id']} | API: {t.get('api', '无')}")
|
||
|
|
|
||
|
|
return _tabs
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_list_courses(tab_index: Optional[int] = None):
|
||
|
|
global _tabs, _course_cache, _current_tab
|
||
|
|
|
||
|
|
if not _tabs:
|
||
|
|
print("🔄 首次使用,先探测标签页...")
|
||
|
|
result = cmd_list_tabs()
|
||
|
|
if not result:
|
||
|
|
return
|
||
|
|
|
||
|
|
# 找有API的标签
|
||
|
|
course_tabs = [(i, t) for i, t in enumerate(_tabs) if t.get("api")]
|
||
|
|
if not course_tabs:
|
||
|
|
print("⚠️ 没有可加载课程数据的标签页。")
|
||
|
|
return
|
||
|
|
|
||
|
|
if tab_index is not None:
|
||
|
|
idx = tab_index - 1
|
||
|
|
if idx < 0 or idx >= len(_tabs) or not _tabs[idx].get("api"):
|
||
|
|
print(f"❌ 标签 [{tab_index}] 无效或无课程数据。")
|
||
|
|
return
|
||
|
|
chosen = _tabs[idx]
|
||
|
|
else:
|
||
|
|
chosen = course_tabs[0][1]
|
||
|
|
|
||
|
|
_current_tab = chosen
|
||
|
|
print(f"\n📋 加载: {chosen['label']}")
|
||
|
|
print(f"{'='*60}")
|
||
|
|
|
||
|
|
referer = f"{ORIGIN}{chosen['iframe']}"
|
||
|
|
courses = get_courses(chosen["api"], referer)
|
||
|
|
_course_cache = courses
|
||
|
|
|
||
|
|
if not courses:
|
||
|
|
print("📭 无课程数据。")
|
||
|
|
return
|
||
|
|
|
||
|
|
selected = [c for c in courses if c.get("xkzt") == "9"]
|
||
|
|
sel_credits = sum(float(c.get("xf", 0)) for c in selected)
|
||
|
|
print(f"\n 共 {len(courses)} 门课程,已选 {len(selected)} 门 ({sel_credits:.1f}学分)\n")
|
||
|
|
|
||
|
|
for i, c in enumerate(courses, 1):
|
||
|
|
xkzt = c.get("xkzt", "0")
|
||
|
|
status = "✅ 已选" if xkzt == "9" else "⬜ 可选"
|
||
|
|
action = "[退课]" if xkzt == "9" else "[选课]"
|
||
|
|
print(f" [{i:2d}] {c.get('kcmc', '-'):20s} {c.get('kch', '-'):8s} "
|
||
|
|
f"{str(c.get('xf', '-')):4s}学分 余{c.get('syrs', '?'):>3s}座 {status} {action}")
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_select(kcid_or_name: str):
|
||
|
|
global _course_cache, _current_tab
|
||
|
|
|
||
|
|
if not _course_cache:
|
||
|
|
print("🔄 先加载课程列表...")
|
||
|
|
cmd_list_courses()
|
||
|
|
|
||
|
|
if not _current_tab:
|
||
|
|
print("❌ 未选择课程类型。先执行 list-courses。")
|
||
|
|
return
|
||
|
|
|
||
|
|
course = _find_course(kcid_or_name)
|
||
|
|
if not course:
|
||
|
|
print(f"❌ 未找到 '{kcid_or_name}'")
|
||
|
|
return
|
||
|
|
if course.get("xkzt") == "9":
|
||
|
|
print(f"⚠️ '{course['kcmc']}' 已选,无需重复选课。")
|
||
|
|
return
|
||
|
|
|
||
|
|
referer = f"{ORIGIN}{_current_tab['iframe']}"
|
||
|
|
result = select_course(course["kch"], course["jx0404id"], referer)
|
||
|
|
if result.get("success"):
|
||
|
|
print(f"✅ 选课成功: {course['kcmc']} ({result.get('message', '')})")
|
||
|
|
else:
|
||
|
|
print(f"❌ 选课失败: {result.get('message', '未知错误')}")
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_drop(kcid_or_name: str):
|
||
|
|
global _course_cache, _current_tab
|
||
|
|
|
||
|
|
if not _course_cache:
|
||
|
|
print("🔄 先加载课程列表...")
|
||
|
|
cmd_list_courses()
|
||
|
|
|
||
|
|
if not _current_tab:
|
||
|
|
print("❌ 未选择课程类型。先执行 list-courses。")
|
||
|
|
return
|
||
|
|
|
||
|
|
course = _find_course(kcid_or_name)
|
||
|
|
if not course:
|
||
|
|
print(f"❌ 未找到 '{kcid_or_name}'")
|
||
|
|
return
|
||
|
|
if course.get("xkzt") != "9":
|
||
|
|
print(f"⚠️ '{course['kcmc']}' 未选,无需退课。")
|
||
|
|
return
|
||
|
|
|
||
|
|
referer = f"{ORIGIN}{_current_tab['iframe']}"
|
||
|
|
result = drop_course(course["jx0404id"], referer)
|
||
|
|
if result.get("success"):
|
||
|
|
print(f"✅ 退课成功: {course['kcmc']}")
|
||
|
|
else:
|
||
|
|
print(f"❌ 退课失败: {result.get('message', '未知错误')}")
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_search(keyword: str):
|
||
|
|
global _course_cache
|
||
|
|
if not _course_cache:
|
||
|
|
print("🔄 先加载课程列表...")
|
||
|
|
cmd_list_courses()
|
||
|
|
|
||
|
|
kw = keyword.lower()
|
||
|
|
results = [c for c in _course_cache
|
||
|
|
if kw in c.get("kcmc", "").lower() or kw in c.get("kch", "").lower()]
|
||
|
|
if not results:
|
||
|
|
print(f"📭 未找到包含 '{keyword}' 的课程。")
|
||
|
|
return
|
||
|
|
|
||
|
|
print(f"\n 🔍 找到 {len(results)} 门课程:\n")
|
||
|
|
for c in results:
|
||
|
|
status = "✅ 已选" if c.get("xkzt") == "9" else "⬜ 可选"
|
||
|
|
print(f" {c.get('kcmc', '-'):20s} {c.get('kch', '-'):8s} "
|
||
|
|
f"{str(c.get('xf', '-')):4s}学分 {status}")
|
||
|
|
|
||
|
|
|
||
|
|
def _find_course(kcid_or_name: str) -> Optional[dict]:
|
||
|
|
for c in _course_cache:
|
||
|
|
if c.get("kch") == kcid_or_name or c.get("kcmc") == kcid_or_name:
|
||
|
|
return c
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
# ─── CLI ─────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def main():
|
||
|
|
parser = argparse.ArgumentParser(
|
||
|
|
description="莞工教务系统 CLI - 选课工具(自适应选课类型)",
|
||
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
|
|
epilog=textwrap.dedent("""
|
||
|
|
示例:
|
||
|
|
python3 dgut.py list-cycles # 查看可选课轮次
|
||
|
|
python3 dgut.py list-tabs # 探测选课类型标签
|
||
|
|
python3 dgut.py list-courses # 默认加载第1个有数据的标签
|
||
|
|
python3 dgut.py list-courses 2 # 加载第2个标签
|
||
|
|
python3 dgut.py select 0410578 # 按编号选课
|
||
|
|
python3 dgut.py select "微信小程序" # 按名称选课
|
||
|
|
python3 dgut.py drop 0410531 # 退课
|
||
|
|
python3 dgut.py search 鸿蒙 # 搜索课程
|
||
|
|
"""),
|
||
|
|
)
|
||
|
|
parser.add_argument("command", nargs="?",
|
||
|
|
choices=["list-cycles", "list-tabs", "list-courses",
|
||
|
|
"select", "drop", "search"],
|
||
|
|
help="操作命令")
|
||
|
|
parser.add_argument("arg", nargs="?", default="", help="命令参数")
|
||
|
|
|
||
|
|
args = parser.parse_args()
|
||
|
|
if not args.command:
|
||
|
|
parser.print_help()
|
||
|
|
return
|
||
|
|
|
||
|
|
if not ensure_session():
|
||
|
|
print(f"⚠️ 会话 '{'jwx-s1'}' 未找到。请先通过 browser-act 登录教务系统。")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
try:
|
||
|
|
if args.command == "list-cycles":
|
||
|
|
cmd_list_cycles()
|
||
|
|
elif args.command == "list-tabs":
|
||
|
|
cmd_list_tabs()
|
||
|
|
elif args.command == "list-courses":
|
||
|
|
idx = int(args.arg) if args.arg and args.arg.isdigit() else None
|
||
|
|
cmd_list_courses(idx)
|
||
|
|
elif args.command == "select":
|
||
|
|
if not args.arg:
|
||
|
|
print("❌ 请指定课程编号或名称")
|
||
|
|
return
|
||
|
|
cmd_select(args.arg)
|
||
|
|
elif args.command == "drop":
|
||
|
|
if not args.arg:
|
||
|
|
print("❌ 请指定课程编号或名称")
|
||
|
|
return
|
||
|
|
cmd_drop(args.arg)
|
||
|
|
elif args.command == "search":
|
||
|
|
if not args.arg:
|
||
|
|
print("❌ 请指定搜索关键词")
|
||
|
|
return
|
||
|
|
cmd_search(args.arg)
|
||
|
|
except RuntimeError as e:
|
||
|
|
print(f"\n❌ 错误: {e}")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|