Skip to content

Commit d50556c

Browse files
committed
1.0
0 parents  commit d50556c

File tree

9 files changed

+3115
-0
lines changed

9 files changed

+3115
-0
lines changed

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
.venv/
8+
venv/
9+
ENV/
10+
11+
# Sensitive data - DO NOT COMMIT
12+
config.json
13+
tasks.json
14+
*.har
15+
16+
# IDE
17+
.idea/
18+
.vscode/
19+
*.swp
20+
*.swo
21+
22+
# OS
23+
.DS_Store
24+
Thumbs.db

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 VanceHud
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# VanceCoursePro
2+
3+
一款基于 Python 的正方教务系统自动化选课工具,支持多账号管理、自动抢课、课程退选等功能,拥有现代化的图形界面。
4+
基于 正方教务V9.0 制作,不一定适用于所有场景,请自行测试,如有问题欢迎提交PR和Issue。
5+
6+
## 📖 功能特性
7+
8+
- **🔑 多账号管理**:支持添加、编辑、删除多个学生账号,一键切换
9+
- **📚 课程浏览**:查看可选课程列表,支持查看教学班详情(教师、时间、地点、余量等)
10+
- **🚀 自动抢课**:创建抢课任务,支持自定义抢课间隔,自动重试直到成功
11+
- **📝 任务管理**:同时运行多个抢课任务,支持单独或批量启动/停止
12+
- **❌ 课程退选**:支持退选已选课程,退选在服务器端貌似没有验证,只会返回成功,谨慎使用
13+
- **⚙️ 灵活配置**:支持自定义教务系统地址,适配不同学校
14+
15+
## 🖥️ 界面预览
16+
17+
应用采用 ttkbootstrap 主题,提供现代化的深色界面,主要包含:
18+
- **左侧**:课程列表浏览区域
19+
- **右上**:账号管理和任务管理区域
20+
- **下方**:操作日志区域
21+
22+
## 🔧 安装
23+
24+
### 环境要求
25+
26+
- Python 3.8+
27+
- Windows / macOS / Linux
28+
29+
### 安装步骤
30+
31+
1. **克隆仓库**
32+
33+
```bash
34+
git clone https://github.com/your-username/course-selection-helper.git
35+
cd course-selection-helper
36+
```
37+
38+
2. **创建虚拟环境(推荐)**
39+
40+
```bash
41+
python -m venv .venv
42+
# Windows
43+
.venv\Scripts\activate
44+
# macOS/Linux
45+
source .venv/bin/activate
46+
```
47+
48+
3. **安装依赖**
49+
50+
```bash
51+
pip install -r requirements.txt
52+
```
53+
54+
4. **配置教务系统地址**
55+
56+
复制配置文件模板并填写:
57+
58+
```bash
59+
cp config.example.json config.json
60+
```
61+
62+
编辑 `config.json`,填入你学校的教务系统地址:
63+
64+
```json
65+
{
66+
"base_url": "http://your-jwglxt-server.edu.cn/jwglxt",
67+
"accounts": [],
68+
"default_account": null
69+
}
70+
```
71+
72+
> ⚠️ **注意**`config.json` 包含敏感信息,已添加到 `.gitignore`,不会被提交到版本控制。
73+
74+
## 🚀 使用方法
75+
76+
### 启动应用
77+
78+
```bash
79+
python main_gui.py
80+
```
81+
82+
### 基本操作流程
83+
84+
1. **配置教务地址**:首次运行点击「设置」按钮,配置教务系统 URL
85+
2. **添加账号**:点击账号管理区的「+」按钮添加学生账号
86+
3. **登录**:选择账号后点击「登录」
87+
4. **加载课程**
88+
- 选择课程类型(必修/限选/任选)
89+
- 点击「加载列表」按钮(**否则后面的几个按钮不会按照预期工作**)
90+
- 点击「加载课程」获取可选课程列表
91+
- 点击「加载详情」查看教学班余量等详细信息
92+
5. **创建抢课任务**
93+
- 在课程列表中选择目标课程
94+
- 点击「添加任务」,选择教学班并设置抢课间隔
95+
6. **启动抢课**:点击「全部开始」或单独启动任务
96+
7. **退选课程**:选择已选课程,点击「退选」按钮
97+
98+
## 📁 项目结构
99+
100+
```
101+
.
102+
├── main_gui.py # 主程序入口和图形界面
103+
├── jwglxt_api.py # 正方教务系统 API 封装
104+
├── account_manager.py # 账号管理模块
105+
├── task_manager.py # 抢课任务管理模块
106+
├── config.json # 配置文件(需自行创建)
107+
├── config.example.json # 配置文件模板
108+
├── requirements.txt # Python 依赖
109+
└── .gitignore # Git 忽略规则
110+
```
111+
112+
## ⚠️ 免责声明
113+
114+
本项目仅供学习和研究使用,请勿用于任何违反学校规定的用途。使用本工具产生的一切后果由使用者自行承担。
115+
116+
## 🤝 贡献
117+
118+
欢迎提交 Issue 和 Pull Request!
119+
120+
## 📄 许可证
121+
122+
[MIT License](LICENSE)

account_manager.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"""
2+
Account Manager
3+
Handles multiple student account management for course selection.
4+
"""
5+
6+
import os
7+
import json
8+
import uuid
9+
from dataclasses import dataclass, asdict
10+
from typing import List, Optional
11+
12+
13+
@dataclass
14+
class Account:
15+
"""Represents a student account."""
16+
id: str
17+
name: str
18+
username: str
19+
password: str
20+
21+
def to_dict(self) -> dict:
22+
return asdict(self)
23+
24+
@classmethod
25+
def from_dict(cls, data: dict) -> 'Account':
26+
return cls(
27+
id=data.get('id', str(uuid.uuid4())),
28+
name=data.get('name', ''),
29+
username=data.get('username', ''),
30+
password=data.get('password', '')
31+
)
32+
33+
34+
class AccountManager:
35+
"""Manages multiple student accounts."""
36+
37+
DEFAULT_BASE_URL = "" # Must be configured by user
38+
39+
def __init__(self, config_path: str = None):
40+
if config_path is None:
41+
config_path = os.path.join(os.path.dirname(__file__), "config.json")
42+
self.config_path = config_path
43+
self.accounts: List[Account] = []
44+
self.default_account_id: Optional[str] = None
45+
self.base_url: str = self.DEFAULT_BASE_URL
46+
self._load()
47+
48+
def _load(self):
49+
"""Load accounts from config file."""
50+
if not os.path.exists(self.config_path):
51+
self.accounts = []
52+
self.default_account_id = None
53+
return
54+
55+
try:
56+
with open(self.config_path, 'r', encoding='utf-8') as f:
57+
config = json.load(f)
58+
59+
# Check if it's the new format (has 'accounts' key)
60+
if 'accounts' in config:
61+
self.accounts = [Account.from_dict(acc) for acc in config.get('accounts', [])]
62+
self.default_account_id = config.get('default_account')
63+
self.base_url = config.get('base_url', self.DEFAULT_BASE_URL)
64+
else:
65+
# Legacy format: single account with username/password at root
66+
self._migrate_legacy_config(config)
67+
except Exception as e:
68+
print(f"Error loading config: {e}")
69+
self.accounts = []
70+
self.default_account_id = None
71+
72+
def _migrate_legacy_config(self, legacy_config: dict):
73+
"""Convert old single-account config to new format."""
74+
username = legacy_config.get('username', '')
75+
password = legacy_config.get('password', '')
76+
77+
if username:
78+
account = Account(
79+
id=str(uuid.uuid4()),
80+
name=f"账号1",
81+
username=username,
82+
password=password
83+
)
84+
self.accounts = [account]
85+
self.default_account_id = account.id
86+
# Save in new format
87+
self.save()
88+
else:
89+
self.accounts = []
90+
self.default_account_id = None
91+
92+
def save(self):
93+
"""Save accounts to config file."""
94+
config = {
95+
'accounts': [acc.to_dict() for acc in self.accounts],
96+
'default_account': self.default_account_id,
97+
'base_url': self.base_url
98+
}
99+
100+
with open(self.config_path, 'w', encoding='utf-8') as f:
101+
json.dump(config, f, ensure_ascii=False, indent=2)
102+
103+
def add_account(self, name: str, username: str, password: str) -> Account:
104+
"""Add a new account."""
105+
account = Account(
106+
id=str(uuid.uuid4()),
107+
name=name,
108+
username=username,
109+
password=password
110+
)
111+
self.accounts.append(account)
112+
113+
# Set as default if it's the first account
114+
if len(self.accounts) == 1:
115+
self.default_account_id = account.id
116+
117+
self.save()
118+
return account
119+
120+
def update_account(self, account_id: str, name: str = None,
121+
username: str = None, password: str = None) -> Optional[Account]:
122+
"""Update an existing account."""
123+
account = self.get_account(account_id)
124+
if not account:
125+
return None
126+
127+
if name is not None:
128+
account.name = name
129+
if username is not None:
130+
account.username = username
131+
if password is not None:
132+
account.password = password
133+
134+
self.save()
135+
return account
136+
137+
def remove_account(self, account_id: str) -> bool:
138+
"""Remove an account by ID."""
139+
for i, acc in enumerate(self.accounts):
140+
if acc.id == account_id:
141+
self.accounts.pop(i)
142+
143+
# Update default if we removed the default account
144+
if self.default_account_id == account_id:
145+
self.default_account_id = self.accounts[0].id if self.accounts else None
146+
147+
self.save()
148+
return True
149+
return False
150+
151+
def get_account(self, account_id: str) -> Optional[Account]:
152+
"""Get account by ID."""
153+
for acc in self.accounts:
154+
if acc.id == account_id:
155+
return acc
156+
return None
157+
158+
def get_account_by_username(self, username: str) -> Optional[Account]:
159+
"""Get account by username."""
160+
for acc in self.accounts:
161+
if acc.username == username:
162+
return acc
163+
return None
164+
165+
def get_default_account(self) -> Optional[Account]:
166+
"""Get the default account."""
167+
if self.default_account_id:
168+
return self.get_account(self.default_account_id)
169+
return self.accounts[0] if self.accounts else None
170+
171+
def set_default_account(self, account_id: str) -> bool:
172+
"""Set the default account."""
173+
if self.get_account(account_id):
174+
self.default_account_id = account_id
175+
self.save()
176+
return True
177+
return False
178+
179+
def get_all_accounts(self) -> List[Account]:
180+
"""Get all accounts."""
181+
return self.accounts.copy()
182+
183+
def get_base_url(self) -> str:
184+
"""Get the configured base URL."""
185+
return self.base_url
186+
187+
def set_base_url(self, url: str) -> None:
188+
"""Set the base URL."""
189+
self.base_url = url.rstrip('/') if url else self.DEFAULT_BASE_URL
190+
self.save()
191+
192+
193+
if __name__ == "__main__":
194+
# Simple test
195+
manager = AccountManager()
196+
print(f"Loaded {len(manager.accounts)} accounts")
197+
for acc in manager.accounts:
198+
print(f" - {acc.name}: {acc.username}")

config.example.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"base_url": "http://your-jwglxt-server.edu.cn/jwglxt",
3+
"accounts": [
4+
{
5+
"id": "example-uuid",
6+
"name": "账号名称",
7+
"username": "学号",
8+
"password": "密码"
9+
}
10+
],
11+
"default_account": "example-uuid"
12+
}

0 commit comments

Comments
 (0)