Ver código fonte

fix: export numbering format 1. 2. 3. + backup confirmation dialog

nonvioce 3 meses atrás
pai
commit
c0be3a857f

+ 13 - 15
tiku-client/lib/pages/settings/settings_page.dart

@@ -84,23 +84,21 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
   }
 
   Future<void> _triggerFullBackup() async {
-    setState(() => _backingUp = true);
     final api = ref.read(apiServiceProvider);
-    try {
-      await api.triggerFullBackup();
-      if (mounted) {
-        ScaffoldMessenger.of(context).showSnackBar(
-          const SnackBar(content: Text('全量备份完成'), duration: Duration(seconds: 2)),
-        );
-      }
-    } catch (e) {
-      if (mounted) {
-        ScaffoldMessenger.of(context).showSnackBar(
-          SnackBar(content: Text('备份失败: $e')),
-        );
-      }
+    // Fire and forget — show confirmation immediately
+    api.triggerFullBackup();
+    if (mounted) {
+      showDialog(
+        context: context,
+        builder: (ctx) => AlertDialog(
+          title: const Text('全量备份'),
+          content: const Text('已通知服务端开始备份。\n备份完成后将自动推送到 WebDAV。'),
+          actions: [
+            FilledButton(onPressed: () => Navigator.pop(ctx), child: const Text('确定')),
+          ],
+        ),
+      );
     }
-    if (mounted) setState(() => _backingUp = false);
   }
 
   Future<void> _saveWebDAV() async {

+ 13 - 0
tiku-server/app/database.py

@@ -8,14 +8,27 @@ from pathlib import Path
 from typing import Any
 
 
+import threading
+
+_db_lock = threading.Lock()
+
 def get_connection(db_path: str | Path) -> sqlite3.Connection:
     conn = sqlite3.connect(str(db_path))
     conn.row_factory = sqlite3.Row
     conn.execute("PRAGMA journal_mode=WAL")
+    conn.execute("PRAGMA busy_timeout=10000")
     conn.execute("PRAGMA foreign_keys=ON")
     return conn
 
 
+def with_lock(fn):
+    """装饰器:对数据库写操作加锁,防止多线程并发写入损坏数据库"""
+    def wrapper(*args, **kwargs):
+        with _db_lock:
+            return fn(*args, **kwargs)
+    return wrapper
+
+
 def init_db(db_path: str | Path):
     """建表 + FTS5 + 触发器"""
     conn = get_connection(db_path)

+ 11 - 1
tiku-server/app/routers/import_.py

@@ -86,7 +86,17 @@ async def import_docx(
 
 def _process_import(task_id: str, tmp_path: Path, import_type: str, source_name: str, config):
     """后台处理导入"""
-    conn = get_connection(config.db_path)
+    from app.database import _db_lock
+    with _db_lock:
+        conn = get_connection(config.db_path)
+        try:
+            _do_import(conn, task_id, tmp_path, import_type, source_name, config)
+        finally:
+            conn.close()
+
+
+def _do_import(conn, task_id: str, tmp_path: Path, import_type: str, source_name: str, config):
+    """执行导入(调用方已持有数据库锁)"""
     try:
         update_import_task(conn, task_id, status="processing", message="正在拆分题目...")
 

+ 20 - 9
tiku-server/app/services/docx_builder.py

@@ -183,13 +183,22 @@ def _apply_numbering(para_element):
 
 
 def _setup_auto_numbering(doc: Document):
-    """在文档中设置 Word 自动编号(每道题加题号)"""
+    """在文档中设置 Word 自动编号,格式: 1. 2. 3."""
     numbering_part = doc.part.numbering_part
     numbering_xml = numbering_part._element
 
-    # 定义抽象编号 (abstractNum)
+    # 抽象编号
     abstract_num = OxmlElement('w:abstractNum')
     abstract_num.set(qn('w:abstractNumId'), '0')
+    nsid = OxmlElement('w:nsid')
+    nsid.set(qn('w:val'), '0A1B2C3D')
+    abstract_num.append(nsid)
+    mlt = OxmlElement('w:multiLevelType')
+    mlt.set(qn('w:val'), 'singleLevel')
+    abstract_num.append(mlt)
+    tmpl = OxmlElement('w:tmpl')
+    tmpl.set(qn('w:val'), '0A1B2C3D')
+    abstract_num.append(tmpl)
 
     lvl = OxmlElement('w:lvl')
     lvl.set(qn('w:ilvl'), '0')
@@ -197,30 +206,32 @@ def _setup_auto_numbering(doc: Document):
     start = OxmlElement('w:start')
     start.set(qn('w:val'), '1')
     lvl.append(start)
-
     num_fmt = OxmlElement('w:numFmt')
     num_fmt.set(qn('w:val'), 'decimal')
     lvl.append(num_fmt)
-
     lvl_text = OxmlElement('w:lvlText')
     lvl_text.set(qn('w:val'), '%1.')
     lvl.append(lvl_text)
-
     lvl_jc = OxmlElement('w:lvlJc')
     lvl_jc.set(qn('w:val'), 'left')
     lvl.append(lvl_jc)
-
     p_pr = OxmlElement('w:pPr')
+    tabs = OxmlElement('w:tabs')
+    tab = OxmlElement('w:tab')
+    tab.set(qn('w:val'), 'left')
+    tab.set(qn('w:pos'), '720')
+    tabs.append(tab)
+    p_pr.append(tabs)
     ind = OxmlElement('w:ind')
-    ind.set(qn('w:left'), '420')
-    ind.set(qn('w:hanging'), '420')
+    ind.set(qn('w:left'), '720')
+    ind.set(qn('w:hanging'), '360')
     p_pr.append(ind)
     lvl.append(p_pr)
 
     abstract_num.append(lvl)
     numbering_xml.append(abstract_num)
 
-    # 定义编号实例 (num)
+    # 编号实例
     num = OxmlElement('w:num')
     num.set(qn('w:numId'), '1')
     num_ref = OxmlElement('w:abstractNumId')