Skip to content

Commit 1b7d12f

Browse files
committed
feat(config): 引入多方案配置及备份还原功能
本次更新对配置管理系统进行了重大重构,并引入了数据备份与还原功能,旨在提供更灵活的个性化设置和更高的数据安全性。 主要变更包括: - **多方案配置管理:** - 放弃了原有的单一全局配置,引入了多方案配置系统。用户现在可以创建、切换、复制、重命名和删除多套独立的配置方案,以适应不同场景的需求。 - `config.json` 文件结构更新至 v2 版本,以支持存储多个配置方案。 - 新增 `tools/config_converter.py` 模块,实现了从 v1 到 v2 配置的自动转换,确保旧用户的平滑升级。 - **备份与还原:** - 新增独立的“备份与还原”功能模块 (`backup_restore_manager.py`)。 - 用户可以在设置中选择导出指定的配置方案和/或课程表数据。 - 支持从备份文件进行增量或覆盖式导入,方便数据迁移和恢复。 - 课程表编辑器 (`editor.py`) 新增了自动备份 (`schedule.json.bak`) 和从备份恢复的功能。 - **UI/UX 优化:** - 设置窗口 (`settings.py`) 界面重构,新增“配置方案”下拉菜单和管理按钮,方便用户管理自己的配置。 - 设置项被重新组织到不同的标签页中(如“界面设置”、“备份与还原”),使结构更清晰。 - 优化了窗口位置和大小的保存逻辑,现在会与当前激活的配置方案关联。 - **代码重构:** - `ConfigHandler` 被完全重写,以适应新的多方案数据模型,并负责处理配置的加载、保存和切换逻辑。 - `SettingsWindow` 进行了大规模更新,以支持所有新功能并与重构后的 `ConfigHandler` 交互。
1 parent 7d61d71 commit 1b7d12f

File tree

7 files changed

+925
-206
lines changed

7 files changed

+925
-206
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,4 @@ linshi
181181
.vscode/launch.json
182182
*.prof
183183
docs
184+
schedule.json.bak

backup_restore_manager.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import json
2+
import os
3+
from datetime import datetime
4+
from tkinter import filedialog, messagebox
5+
from logger import logger
6+
from constants import CONFIG_FILE, SCHEDULE_FILE, CONFIG_VERSION
7+
8+
class BackupRestoreManager:
9+
"""处理应用程序数据的备份和还原"""
10+
11+
def __init__(self, main_app):
12+
"""
13+
初始化备份还原管理器。
14+
Args:
15+
main_app: CourseScheduler 主应用实例。
16+
"""
17+
self.main_app = main_app
18+
self.config_handler = main_app.config_handler
19+
20+
def export_data(self, selected_configs, include_schedule):
21+
"""
22+
导出选定的配置和课表数据。
23+
Args:
24+
selected_configs (list): 要导出的配置名称列表。
25+
include_schedule (bool): 是否包含课表数据。
26+
"""
27+
backup_data = {
28+
"metadata": {
29+
"export_date": datetime.now().isoformat(),
30+
"config_version": CONFIG_VERSION,
31+
"current_config_in_backup": self.config_handler.config.get("current_config")
32+
},
33+
"configs": None,
34+
"schedule": None
35+
}
36+
37+
# 1. 处理配置数据
38+
if selected_configs:
39+
all_configs = self.config_handler.config.get("configs", {})
40+
configs_to_export = {name: all_configs[name] for name in selected_configs if name in all_configs}
41+
if configs_to_export:
42+
backup_data["configs"] = configs_to_export
43+
44+
# 2. 处理课表数据
45+
if include_schedule:
46+
if os.path.exists(SCHEDULE_FILE):
47+
with open(SCHEDULE_FILE, 'r', encoding='utf-8') as f:
48+
backup_data["schedule"] = json.load(f)
49+
50+
# 3. 检查是否有效数据被导出
51+
if not backup_data["configs"] and not backup_data["schedule"]:
52+
messagebox.showwarning("无数据", "没有选择任何要导出的数据。")
53+
return
54+
55+
# 4. 弹出文件保存对话框
56+
file_path = filedialog.asksaveasfilename(
57+
title="保存备份文件",
58+
defaultextension=".json",
59+
filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
60+
initialfile=f"CourseScheduler_Backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
61+
)
62+
63+
if not file_path:
64+
return # 用户取消
65+
66+
# 5. 写入文件
67+
try:
68+
with open(file_path, 'w', encoding='utf-8') as f:
69+
json.dump(backup_data, f, ensure_ascii=False, indent=4)
70+
messagebox.showinfo("成功", f"数据已成功导出到:\n{file_path}")
71+
except Exception as e:
72+
logger.log_error(f"导出数据失败: {e}")
73+
messagebox.showerror("错误", f"导出失败: {e}")
74+
75+
def import_data(self, mode):
76+
"""
77+
导入数据。
78+
Args:
79+
mode (str): 'incremental' 或 'overwrite'。
80+
"""
81+
file_path = filedialog.askopenfilename(
82+
title="选择备份文件进行导入",
83+
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
84+
)
85+
86+
if not file_path:
87+
return # 用户取消
88+
89+
try:
90+
with open(file_path, 'r', encoding='utf-8') as f:
91+
backup_data = json.load(f)
92+
except Exception as e:
93+
logger.log_error(f"读取备份文件失败: {e}")
94+
messagebox.showerror("错误", f"无法读取或解析备份文件: {e}")
95+
return
96+
97+
# 验证备份文件
98+
if "metadata" not in backup_data or "configs" not in backup_data or "schedule" not in backup_data:
99+
messagebox.showerror("文件无效", "选择的文件不是一个有效的备份文件。")
100+
return
101+
102+
# 版本兼容性检查
103+
backup_version = backup_data.get("metadata", {}).get("config_version")
104+
if backup_version and backup_version > CONFIG_VERSION:
105+
if not messagebox.askyesno("版本不兼容警告",
106+
f"备份文件版本 ({backup_version}) 高于当前应用版本 ({CONFIG_VERSION})。\n"
107+
"导入可能会导致未知问题。\n\n"
108+
"您确定要继续吗?"):
109+
return
110+
111+
# 确认操作
112+
if not messagebox.askyesno("确认导入", f"确定要以 '{mode}' 模式导入数据吗?\n这将修改您当前的配置和课表,建议操作前先进行备份。"):
113+
return
114+
115+
# 根据模式执行导入
116+
try:
117+
if mode == 'overwrite':
118+
self._overwrite_import(backup_data)
119+
elif mode == 'incremental':
120+
self._incremental_import(backup_data)
121+
122+
# 刷新运行时状态
123+
self.config_handler._load_attributes_from_config()
124+
125+
# 如果编辑器窗口是打开的,警告并关闭它以防止数据冲突
126+
if self.main_app.editor_window and self.main_app.editor_window.window.winfo_exists():
127+
messagebox.showwarning("编辑器已关闭", "为防止数据冲突,课表编辑器窗口已关闭。请重新打开以查看更新后的课表。")
128+
self.main_app.editor_window.window.destroy()
129+
130+
# 提示重启
131+
messagebox.showinfo("成功", "数据导入成功!\n为了使所有更改完全生效,建议您重启应用程序。")
132+
133+
except Exception as e:
134+
logger.log_error(f"导入数据时发生错误: {e}")
135+
messagebox.showerror("导入失败", f"处理导入数据时发生错误: {e}")
136+
137+
def _overwrite_import(self, backup_data):
138+
"""执行覆盖导入(使用临时文件保证原子性)"""
139+
config_to_write = None
140+
schedule_to_write = None
141+
142+
# 准备要写入的数据
143+
if backup_data.get("configs"):
144+
new_config_data = self.config_handler.config.copy()
145+
new_config_data["configs"] = backup_data["configs"]
146+
backup_current = backup_data.get("metadata", {}).get("current_config_in_backup")
147+
if backup_current and backup_current in backup_data["configs"]:
148+
new_config_data["current_config"] = backup_current
149+
else:
150+
new_config_data["current_config"] = next(iter(backup_data["configs"]))
151+
config_to_write = new_config_data
152+
153+
if backup_data.get("schedule"):
154+
schedule_to_write = backup_data["schedule"]
155+
156+
# 原子化写入
157+
self._atomic_write(config_to_write, schedule_to_write)
158+
159+
# 更新内存状态
160+
if config_to_write:
161+
self.config_handler.config = config_to_write
162+
if schedule_to_write:
163+
self.main_app.schedule = schedule_to_write
164+
165+
def _incremental_import(self, backup_data):
166+
"""执行增量导入"""
167+
"""执行增量导入(使用临时文件保证原子性)"""
168+
config_to_write = None
169+
schedule_to_write = None
170+
171+
# 准备要写入的配置数据
172+
if backup_data.get("configs"):
173+
new_config_data = self.config_handler.config.copy()
174+
for name, config_data in backup_data["configs"].items():
175+
new_config_data["configs"][name] = config_data
176+
config_to_write = new_config_data
177+
178+
# 准备要写入的课表数据
179+
if backup_data.get("schedule"):
180+
if os.path.exists(SCHEDULE_FILE):
181+
with open(SCHEDULE_FILE, 'r', encoding='utf-8') as f:
182+
current_schedule = json.load(f)
183+
else:
184+
current_schedule = {"schedules": {}}
185+
186+
for name, schedule_data in backup_data["schedule"]["schedules"].items():
187+
current_schedule["schedules"][name] = schedule_data
188+
schedule_to_write = current_schedule
189+
190+
# 原子化写入
191+
self._atomic_write(config_to_write, schedule_to_write)
192+
193+
# 更新内存状态
194+
if config_to_write:
195+
self.config_handler.config = config_to_write
196+
if schedule_to_write:
197+
self.main_app.schedule = schedule_to_write
198+
199+
def _atomic_write(self, config_data, schedule_data):
200+
"""
201+
将数据原子化地写入配置文件。
202+
先写入.tmp文件,成功后再替换原文件。
203+
"""
204+
config_tmp_file = CONFIG_FILE + ".tmp"
205+
schedule_tmp_file = SCHEDULE_FILE + ".tmp"
206+
207+
try:
208+
# 写入临时文件
209+
if config_data:
210+
with open(config_tmp_file, 'w', encoding='utf-8') as f:
211+
json.dump(config_data, f, ensure_ascii=False, indent=4)
212+
if schedule_data:
213+
with open(schedule_tmp_file, 'w', encoding='utf-8') as f:
214+
json.dump(schedule_data, f, ensure_ascii=False, indent=4)
215+
216+
# 替换原文件
217+
if config_data:
218+
os.replace(config_tmp_file, CONFIG_FILE)
219+
if schedule_data:
220+
os.replace(schedule_tmp_file, SCHEDULE_FILE)
221+
222+
except Exception as e:
223+
# 如果出错,清理临时文件
224+
if os.path.exists(config_tmp_file):
225+
os.remove(config_tmp_file)
226+
if os.path.exists(schedule_tmp_file):
227+
os.remove(schedule_tmp_file)
228+
raise e # 将异常向上抛出,由调用者处理

0 commit comments

Comments
 (0)