Sfoglia il codice sorgente

feat(api): 优化 Block API 文档和数据库操作逻辑

- 简化 create_block 和 _build_toc_tree 函数文档为单行注释
- 精简 BlockCreate 和 TOCMetadata Schema 文档,移除冗余说明
- 重构 ContentDB 索引创建代码,改为单行语句提高可读性
- 提取 _build_type_filter、_query_next_value 等通用查询方法
- 优化 calculate_index_for_insert 逻辑,使用 _calculate_sparse_value 统一处理
- 简化 image_service 和 storage_monitor 配置管理代码
- 规范化文件模块文档注释格式
- 移除 schema 文件多余空行,保持代码风格一致
chensiyu 1 mese fa
parent
commit
c59ecd96b8

+ 2 - 24
app/api/v1/blocks.py

@@ -25,22 +25,7 @@ async def create_block(
25 25
     body: BlockCreate,
26 26
     db: AsyncSession = Depends(get_db),
27 27
 ) -> dict:
28
-    """在指定位置插入新 block
29
-    
30
-    Args:
31
-        documentId: 文档 ID
32
-        body: Block 创建请求
33
-            - type: Block 类型(heading/paragraph/table/image/toc)
34
-            - content: Block 内容
35
-            - level: 标题级别(heading 必填:1-6,其他类型固定为 0)
36
-            - word_style: Word 样式名(可选)
37
-            - style: 自定义样式(可选)
38
-            - metadata: 元数据(可选)
39
-            - after_block_id: 插入位置(null = 末尾)
40
-    
41
-    Returns:
42
-        包含新 block ID 的响应
43
-    """
28
+    """在指定位置插入新 block(type, content, level, style, after_block_id,返回 blockId)"""
44 29
     # 1. 参数校验
45 30
     if body.type == 'heading':
46 31
         if body.level not in range(1, 7):
@@ -250,14 +235,7 @@ async def get_stats(
250 235
 
251 236
 
252 237
 def _build_toc_tree(headings: list[dict]) -> list[dict]:
253
-    """构建目录树
254
-    
255
-    Args:
256
-        headings: 标题 block 列表
257
-        
258
-    Returns:
259
-        树形结构的目录
260
-    """
238
+    """构建目录树(headings 列表 -> 树形结构)"""
261 239
     root = []
262 240
     stack = []
263 241
     

+ 2 - 19
app/schemas/block.py

@@ -1,10 +1,7 @@
1 1
 """block.py — Block 相关 Schema"""
2
-
3 2
 from typing import Optional
4
-
5 3
 from pydantic import BaseModel, Field
6 4
 
7
-
8 5
 class BlockBase(BaseModel):
9 6
     """Block 基础信息"""
10 7
     id: str = Field(..., validation_alias="id", serialization_alias="id")
@@ -20,7 +17,6 @@ class BlockBase(BaseModel):
20 17
     class Config:
21 18
         populate_by_name = True
22 19
 
23
-
24 20
 class BlockUpdate(BaseModel):
25 21
     """Block 更新请求"""
26 22
     content: Optional[str | dict | list] = None
@@ -28,14 +24,8 @@ class BlockUpdate(BaseModel):
28 24
     word_style: Optional[str] = None
29 25
     metadata: Optional[dict] = None
30 26
 
31
-
32 27
 class BlockCreate(BaseModel):
33
-    """Block 创建请求
34
-    
35
-    注意:
36
-    - id, index, block_order 由后端自动生成,前端无需提供
37
-    - level 对于 heading 类型是必填的(1-6),其他类型固定为 0(可省略)
38
-    """
28
+    """Block 创建请求(id/index/block_order 自动生成,heading 需 level 1-6,其他类型 level=0)"""
39 29
     type: str
40 30
     level: int = 0  # heading 类型必填(1-6),其他类型默认 0
41 31
     content: str | dict | list
@@ -44,12 +34,10 @@ class BlockCreate(BaseModel):
44 34
     metadata: dict = {}
45 35
     after_block_id: Optional[str] = None  # 在哪个 block 后插入(null = 末尾)
46 36
 
47
-
48 37
 class TOCContent(BaseModel):
49 38
     """TOC Block content 结构"""
50 39
     title: str = "目录"
51 40
 
52
-
53 41
 class TOCConfig(BaseModel):
54 42
     """TOC 配置"""
55 43
     levels: str = "1-1"  # 包含的标题层级
@@ -60,7 +48,6 @@ class TOCConfig(BaseModel):
60 48
     show_leader_dots: bool = True
61 49
     leader_char: str = "."
62 50
 
63
-
64 51
 class TOCMetadata(BaseModel):
65 52
     """TOC Block metadata 结构"""
66 53
     toc_config: TOCConfig
@@ -68,7 +55,6 @@ class TOCMetadata(BaseModel):
68 55
     readonly: bool = True
69 56
     deletable: bool = True
70 57
 
71
-
72 58
 class TOCBlock(BaseModel):
73 59
     """完整的 TOC Block"""
74 60
     id: str
@@ -83,7 +69,6 @@ class TOCBlock(BaseModel):
83 69
     class Config:
84 70
         populate_by_name = True
85 71
 
86
-
87 72
 class TOCItem(BaseModel):
88 73
     """目录树节点"""
89 74
     id: str
@@ -91,17 +76,15 @@ class TOCItem(BaseModel):
91 76
     content: str
92 77
     children: list['TOCItem'] = []
93 78
 
94
-
95 79
 class BlockSearchResult(BaseModel):
96 80
     """Block 搜索结果"""
97 81
     blocks: list[BlockBase]
98 82
     total: int
99 83
 
100
-
101 84
 class BlockStats(BaseModel):
102 85
     """Block 统计信息"""
103 86
     total: int
104 87
     by_type: dict[str, int] = Field(..., alias="byType")
105 88
     
106 89
     class Config:
107
-        populate_by_name = True
90
+        populate_by_name = True

+ 85 - 222
app/services/content_db.py

@@ -1,5 +1,4 @@
1
-"""content_db.py — SQLite 内容数据库操作类"""
2
-
1
+"""SQLite 内容数据库操作类"""
3 2
 import json
4 3
 import sqlite3
5 4
 from pathlib import Path
@@ -7,10 +6,7 @@ from typing import Optional
7 6
 
8 7
 
9 8
 class ContentDB:
10
-    """SQLite 内容数据库操作类
11
-    
12
-    每个文档对应一个独立的 SQLite 数据库文件,存储文档的所有 Blocks。
13
-    """
9
+    """SQLite 内容数据库操作类,每个文档对应一个独立的 SQLite 数据库文件"""
14 10
     
15 11
     def __init__(self, db_path: str):
16 12
         self.db_path = Path(db_path)
@@ -51,17 +47,9 @@ class ContentDB:
51 47
                 metadata TEXT DEFAULT '{}'
52 48
             )
53 49
         """)
54
-        
55
-        # 创建索引
56
-        self.conn.execute(
57
-            'CREATE INDEX IF NOT EXISTS idx_block_order ON document_blocks(block_order)'
58
-        )
59
-        self.conn.execute(
60
-            'CREATE INDEX IF NOT EXISTS idx_type ON document_blocks(type)'
61
-        )
62
-        self.conn.execute(
63
-            'CREATE INDEX IF NOT EXISTS idx_level ON document_blocks(level)'
64
-        )
50
+        self.conn.execute('CREATE INDEX IF NOT EXISTS idx_block_order ON document_blocks(block_order)')
51
+        self.conn.execute('CREATE INDEX IF NOT EXISTS idx_type ON document_blocks(type)')
52
+        self.conn.execute('CREATE INDEX IF NOT EXISTS idx_level ON document_blocks(level)')
65 53
         self.conn.commit()
66 54
     
67 55
     def insert_blocks(self, blocks: list[dict]):
@@ -186,198 +174,98 @@ class ContentDB:
186 174
             'by_type': stats
187 175
         }
188 176
     
189
-    def calculate_index_for_insert(
190
-        self,
191
-        after_block_id: str,
192
-        new_type: str,
193
-        new_level: int = 0
177
+    def _build_type_filter(self, block_type: str, level: Optional[int] = None) -> tuple[str, list]:
178
+        """构建类型过滤条件(用于 index 查询)"""
179
+        if block_type == 'heading' and level is not None:
180
+            return "type = ? AND level = ?", [block_type, level]
181
+        return "type = ?", [block_type]
182
+    
183
+    def _query_next_value(self, field: str, after_order: int, type_filter: str, params: list) -> Optional[int]:
184
+        """查询下一个值(index 或 block_order)"""
185
+        sql = f'SELECT "{field}" FROM document_blocks WHERE {type_filter} AND block_order > ? ORDER BY block_order LIMIT 1'
186
+        cursor = self.conn.execute(sql, params + [after_order])
187
+        row = cursor.fetchone()
188
+        return row[field] if row else None
189
+    
190
+    def _query_prev_value(self, field: str, after_order: int, type_filter: str, params: list) -> Optional[int]:
191
+        """查询前一个值(index 或 block_order)"""
192
+        sql = f'SELECT "{field}" FROM document_blocks WHERE {type_filter} AND block_order <= ? ORDER BY block_order DESC LIMIT 1'
193
+        cursor = self.conn.execute(sql, params + [after_order])
194
+        row = cursor.fetchone()
195
+        return row[field] if row else None
196
+    
197
+    def _query_max_value(self, field: str, type_filter: str, params: list) -> Optional[int]:
198
+        """查询最大值(index 或 block_order)"""
199
+        sql = f'SELECT MAX("{field}") as max_val FROM document_blocks'
200
+        if type_filter:
201
+            sql += f' WHERE {type_filter}'
202
+        cursor = self.conn.execute(sql, params)
203
+        row = cursor.fetchone()
204
+        return row['max_val']
205
+    
206
+    def _calculate_sparse_value(
207
+        self, 
208
+        field: str,
209
+        after_block_id: Optional[str],
210
+        type_filter: str,
211
+        params: list,
212
+        default_min: int,
213
+        rebalance_func: callable
194 214
     ) -> int:
195
-        """计算插入 block 的 index(稀疏排序)
196
-        
197
-        Args:
198
-            after_block_id: 在哪个 block 后插入(如果为 None 则插入到末尾)
199
-            new_type: 新 block 的类型
200
-            new_level: 新 block 的级别(标题有效)
201
-            
202
-        Returns:
203
-            计算得到的 index
204
-        """
215
+        """通用稀疏值计算逻辑"""
205 216
         if after_block_id:
206
-            # 1. 获取 after_block 的 block_order
207 217
             after_block = self.get_block_by_id(after_block_id)
208 218
             if not after_block:
209 219
                 raise ValueError(f"Block not found: {after_block_id}")
210 220
             after_order = after_block['block_order']
211 221
             
212
-            # 2. 查找下一个同类型同级别的 block
213
-            if new_type == 'heading':
214
-                # 标题:按 level 查询
215
-                cursor = self.conn.execute("""
216
-                    SELECT "index" FROM document_blocks
217
-                    WHERE type = ? AND level = ? AND block_order > ?
218
-                    ORDER BY block_order LIMIT 1
219
-                """, (new_type, new_level, after_order))
220
-            else:
221
-                # 其他类型:只按 type 查询
222
-                cursor = self.conn.execute("""
223
-                    SELECT "index" FROM document_blocks
224
-                    WHERE type = ? AND block_order > ?
225
-                    ORDER BY block_order LIMIT 1
226
-                """, (new_type, after_order))
222
+            next_val = self._query_next_value(field, after_order, type_filter, params)
227 223
             
228
-            next_row = cursor.fetchone()
229
-            
230
-            if next_row:
231
-                next_index = next_row['index']
232
-                
233
-                # 3. 查找前一个同类型同级别的 block
234
-                if new_type == 'heading':
235
-                    cursor = self.conn.execute("""
236
-                        SELECT "index" FROM document_blocks
237
-                        WHERE type = ? AND level = ? AND block_order <= ?
238
-                        ORDER BY block_order DESC LIMIT 1
239
-                    """, (new_type, new_level, after_order))
240
-                else:
241
-                    cursor = self.conn.execute("""
242
-                        SELECT "index" FROM document_blocks
243
-                        WHERE type = ? AND block_order <= ?
244
-                        ORDER BY block_order DESC LIMIT 1
245
-                    """, (new_type, after_order))
246
-                
247
-                prev_row = cursor.fetchone()
248
-                prev_index = prev_row['index'] if prev_row else -100
224
+            if next_val is not None:
225
+                prev_val = self._query_prev_value(field, after_order, type_filter, params)
226
+                if prev_val is None:
227
+                    prev_val = default_min
249 228
                 
250
-                # 4. 计算中间值
251
-                gap = next_index - prev_index
229
+                gap = next_val - prev_val
252 230
                 if gap <= 1:
253
-                    # 间隙不足,触发局部重排
254
-                    self._rebalance_indexes_between(
255
-                        new_type, new_level, prev_index, next_index
256
-                    )
257
-                    # 重新查询
258
-                    if new_type == 'heading':
259
-                        cursor = self.conn.execute("""
260
-                            SELECT "index" FROM document_blocks
261
-                            WHERE type = ? AND level = ? AND block_order > ?
262
-                            ORDER BY block_order LIMIT 1
263
-                        """, (new_type, new_level, after_order))
264
-                    else:
265
-                        cursor = self.conn.execute("""
266
-                            SELECT "index" FROM document_blocks
267
-                            WHERE type = ? AND block_order > ?
268
-                            ORDER BY block_order LIMIT 1
269
-                        """, (new_type, after_order))
270
-                    next_row = cursor.fetchone()
271
-                    next_index = next_row['index'] if next_row else prev_index + 200
231
+                    rebalance_func(prev_val, next_val)
232
+                    next_val = self._query_next_value(field, after_order, type_filter, params)
233
+                    if next_val is None:
234
+                        next_val = prev_val + 200
272 235
                 
273
-                new_index = (prev_index + next_index) // 2
236
+                return (prev_val + next_val) // 2
274 237
             else:
275
-                # 没有下一个同类 block,追加到最后
276
-                if new_type == 'heading':
277
-                    cursor = self.conn.execute("""
278
-                        SELECT MAX("index") as max_index FROM document_blocks
279
-                        WHERE type = ? AND level = ?
280
-                    """, (new_type, new_level))
281
-                else:
282
-                    cursor = self.conn.execute("""
283
-                        SELECT MAX("index") as max_index FROM document_blocks
284
-                        WHERE type = ?
285
-                    """, (new_type,))
286
-                
287
-                row = cursor.fetchone()
288
-                max_index = row['max_index'] if row['max_index'] is not None else -100
289
-                new_index = max_index + 100
238
+                max_val = self._query_max_value(field, type_filter, params)
239
+                return (max_val if max_val is not None else default_min) + 100
290 240
         else:
291
-            # 插入到文档末尾
292
-            if new_type == 'heading':
293
-                cursor = self.conn.execute("""
294
-                    SELECT MAX("index") as max_index FROM document_blocks
295
-                    WHERE type = ? AND level = ?
296
-                """, (new_type, new_level))
297
-            else:
298
-                cursor = self.conn.execute("""
299
-                    SELECT MAX("index") as max_index FROM document_blocks
300
-                    WHERE type = ?
301
-                """, (new_type,))
302
-            
303
-            row = cursor.fetchone()
304
-            max_index = row['max_index'] if row['max_index'] is not None else -100
305
-            new_index = max_index + 100
306
-        
307
-        return new_index
241
+            max_val = self._query_max_value(field, type_filter, params)
242
+            return (max_val if max_val is not None else default_min) + 100
243
+    
244
+    def calculate_index_for_insert(self, after_block_id: str, new_type: str, new_level: int = 0) -> int:
245
+        """计算插入 block 的 index(稀疏排序)"""
246
+        type_filter, params = self._build_type_filter(new_type, new_level)
247
+        return self._calculate_sparse_value(
248
+            field='index',
249
+            after_block_id=after_block_id,
250
+            type_filter=type_filter,
251
+            params=params,
252
+            default_min=-100,
253
+            rebalance_func=lambda prev, next: self._rebalance_indexes_between(new_type, new_level, prev, next)
254
+        )
308 255
     
309 256
     def calculate_block_order_for_insert(self, after_block_id: str = None) -> int:
310
-        """计算插入 block 的 block_order(稀疏排序)
311
-        
312
-        Args:
313
-            after_block_id: 在哪个 block 后插入(如果为 None 则插入到末尾)
314
-            
315
-        Returns:
316
-            计算得到的 block_order
317
-        """
318
-        if after_block_id:
319
-            # 1. 获取 after_block 的 block_order
320
-            after_block = self.get_block_by_id(after_block_id)
321
-            if not after_block:
322
-                raise ValueError(f"Block not found: {after_block_id}")
323
-            after_order = after_block['block_order']
324
-            
325
-            # 2. 查询下一个 block 的 block_order
326
-            cursor = self.conn.execute("""
327
-                SELECT block_order FROM document_blocks
328
-                WHERE block_order > ?
329
-                ORDER BY block_order LIMIT 1
330
-            """, (after_order,))
331
-            
332
-            next_row = cursor.fetchone()
333
-            
334
-            if next_row:
335
-                next_order = next_row['block_order']
336
-                gap = next_order - after_order
337
-                
338
-                # 3. 检查间隙是否足够
339
-                if gap <= 1:
340
-                    # 触发局部重排
341
-                    self._rebalance_block_orders_between(after_order, next_order)
342
-                    # 重新查询
343
-                    cursor = self.conn.execute("""
344
-                        SELECT block_order FROM document_blocks
345
-                        WHERE block_order > ?
346
-                        ORDER BY block_order LIMIT 1
347
-                    """, (after_order,))
348
-                    next_row = cursor.fetchone()
349
-                    next_order = next_row['block_order'] if next_row else after_order + 200
350
-                
351
-                # 4. 计算中间值
352
-                return (after_order + next_order) // 2
353
-            else:
354
-                # 没有下一个 block,插入到最后
355
-                return after_order + 100
356
-        else:
357
-            # 插入到文档末尾
358
-            cursor = self.conn.execute("""
359
-                SELECT MAX(block_order) as max_order FROM document_blocks
360
-            """)
361
-            row = cursor.fetchone()
362
-            max_order = row['max_order'] if row['max_order'] is not None else 0
363
-            return max_order + 100
257
+        """计算插入 block 的 block_order(稀疏排序)"""
258
+        return self._calculate_sparse_value(
259
+            field='block_order',
260
+            after_block_id=after_block_id,
261
+            type_filter='1=1',
262
+            params=[],
263
+            default_min=0,
264
+            rebalance_func=self._rebalance_block_orders_between
265
+        )
364 266
     
365
-    def _rebalance_indexes_between(
366
-        self,
367
-        block_type: str,
368
-        level: int,
369
-        start_index: int,
370
-        end_index: int
371
-    ):
372
-        """局部重排:重新分配区间内同类型同级别 blocks 的 index
373
-        
374
-        Args:
375
-            block_type: Block 类型
376
-            level: 级别(标题有效)
377
-            start_index: 起始 index
378
-            end_index: 结束 index
379
-        """
380
-        # 查询区间内的所有同类型同级别 blocks
267
+    def _rebalance_indexes_between(self, block_type: str, level: int, start_index: int, end_index: int):
268
+        """局部重排:重新分配区间内同类型同级别 blocks 的 index"""
381 269
         if block_type == 'heading':
382 270
             cursor = self.conn.execute("""
383 271
                 SELECT id, "index" 
@@ -398,12 +286,9 @@ class ContentDB:
398 286
         if not blocks:
399 287
             return
400 288
         
401
-        # 计算新的间隔
402 289
         count = len(blocks)
403 290
         gap = end_index - start_index
404 291
         step = gap // (count + 1)
405
-        
406
-        # 重新分配 index
407 292
         new_index = start_index
408 293
         for block in blocks:
409 294
             new_index += step
@@ -415,13 +300,7 @@ class ContentDB:
415 300
         self.conn.commit()
416 301
     
417 302
     def _rebalance_block_orders_between(self, start_order: int, end_order: int):
418
-        """局部重排:重新分配区间内所有 blocks 的 block_order
419
-        
420
-        Args:
421
-            start_order: 起始 block_order
422
-            end_order: 结束 block_order
423
-        """
424
-        # 查询区间内的所有 blocks
303
+        """局部重排:重新分配区间内所有 blocks 的 block_order"""
425 304
         cursor = self.conn.execute("""
426 305
             SELECT id, block_order 
427 306
             FROM document_blocks 
@@ -434,12 +313,9 @@ class ContentDB:
434 313
         if not blocks:
435 314
             return
436 315
         
437
-        # 计算新的间隔
438 316
         count = len(blocks)
439 317
         gap = end_order - start_order
440 318
         step = gap // (count + 1)
441
-        
442
-        # 重新分配 block_order
443 319
         new_order = start_order
444 320
         for block in blocks:
445 321
             new_order += step
@@ -453,8 +329,6 @@ class ContentDB:
453 329
     def _row_to_dict(self, row) -> dict:
454 330
         """将 sqlite3.Row 转换为字典"""
455 331
         d = dict(row)
456
-        
457
-        # 解析 JSON 字段
458 332
         if d.get('style'):
459 333
             try:
460 334
                 d['style'] = json.loads(d['style'])
@@ -470,28 +344,17 @@ class ContentDB:
470 344
                 d['metadata'] = {}
471 345
         else:
472 346
             d['metadata'] = {}
473
-        
474
-        # content 可能是 JSON(富文本或表格)
475 347
         if d.get('content'):
476 348
             try:
477 349
                 d['content'] = json.loads(d['content'])
478 350
             except (json.JSONDecodeError, TypeError):
479
-                pass  # 保持原字符串
351
+                pass
480 352
         
481 353
         return d
482 354
     
483 355
     @staticmethod
484 356
     def generate_block_id(block_type: str, level: int, index: int) -> str:
485
-        """生成 Block ID
486
-        
487
-        Args:
488
-            block_type: Block 类型
489
-            level: 级别(标题有效)
490
-            index: 序号
491
-            
492
-        Returns:
493
-            生成的 Block ID
494
-        """
357
+        """生成 Block ID"""
495 358
         if block_type == 'heading':
496 359
             return f'block-h{level}-{index}'
497 360
         elif block_type == 'paragraph':
@@ -503,4 +366,4 @@ class ContentDB:
503 366
         elif block_type == 'toc':
504 367
             return f'block-toc-{index}'
505 368
         else:
506
-            return f'block-{block_type}-{index}'
369
+            return f'block-{block_type}-{index}'

+ 7 - 54
app/services/export_service.py

@@ -1,5 +1,4 @@
1 1
 """export_service.py — 将 Blocks 转换为 .doc 文件并返回永久下载链接"""
2
-
3 2
 import base64
4 3
 import io
5 4
 import json
@@ -7,6 +6,8 @@ import time
7 6
 import unicodedata
8 7
 from pathlib import Path
9 8
 from typing import Optional
9
+import platform
10
+import os
10 11
 
11 12
 from docx import Document
12 13
 from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
@@ -26,23 +27,7 @@ from app.core.exceptions import ExportError
26 27
 # ------------------------------------------------------------------ #
27 28
 
28 29
 def update_document_fields(file_path: str) -> bool:
29
-    """使用 WPS/Word COM API 更新文档中的所有域(目录、页码等)
30
-    
31
-    Args:
32
-        file_path: 文档文件路径(绝对路径)
33
-        
34
-    Returns:
35
-        bool: 更新是否成功
36
-        
37
-    Note:
38
-        - 仅在 Windows 平台且安装了 WPS/Word 时可用
39
-        - 如果更新失败,不影响原文件(会有错误日志)
40
-        - 更新会直接覆盖原文件
41
-        - 支持 .docx 和 .doc 格式
42
-    """
43
-    import platform
44
-    import os
45
-    
30
+    """使用 WPS/Word COM API 更新文档域(目录、页码等),支持 .docx/.doc,仅 Windows 可用"""
46 31
     # 仅在 Windows 平台尝试更新
47 32
     if platform.system() != 'Windows':
48 33
         print(f"提示: 非 Windows 平台,跳过域更新(文档可在打开时自动更新)")
@@ -119,7 +104,6 @@ def update_document_fields(file_path: str) -> bool:
119 104
         print(f"警告: 启动 WPS/Word 失败: {e}")
120 105
         return False
121 106
 
122
-
123 107
 # ------------------------------------------------------------------ #
124 108
 # 样式文件加载
125 109
 # ------------------------------------------------------------------ #
@@ -140,7 +124,6 @@ def load_style_file(style_id: Optional[str] = None) -> dict:
140 124
     except (OSError, json.JSONDecodeError) as exc:
141 125
         raise ExportError(f"样式文件解析失败: {exc}") from exc
142 126
 
143
-
144 127
 def build_style_map(style_data: dict) -> dict[str, dict]:
145 128
     """将样式列表转为双键映射(style_id 和 name 均可命中)"""
146 129
     mapping: dict[str, dict] = {}
@@ -151,7 +134,6 @@ def build_style_map(style_data: dict) -> dict[str, dict]:
151 134
             mapping[s["name"]] = s
152 135
     return mapping
153 136
 
154
-
155 137
 # ------------------------------------------------------------------ #
156 138
 # JSON ↔ lxml 互转
157 139
 # ------------------------------------------------------------------ #
@@ -170,7 +152,6 @@ def dict_to_element(d: dict) -> etree._Element:
170 152
                 elem.append(dict_to_element(item))
171 153
     return elem
172 154
 
173
-
174 155
 def inject_styles_from_json(doc: Document, style_data: dict) -> None:
175 156
     """将 JSON 中所有样式的 full_xml_definition upsert 到文档 <w:styles> 节点"""
176 157
     styles_element = doc.styles.element
@@ -196,7 +177,6 @@ def inject_styles_from_json(doc: Document, style_data: dict) -> None:
196 177
     inject_numbering_from_json(doc, style_data)
197 178
     
198 179
     # 强制清除 python-docx 的样式缓存,确保后续使用的是新注入的样式
199
-    # 这一步很重要,因为 python-docx 会缓存样式对象
200 180
     try:
201 181
         # 清除样式字典缓存,强制重新从 XML 读取
202 182
         if hasattr(doc.styles, '_element'):
@@ -205,18 +185,12 @@ def inject_styles_from_json(doc: Document, style_data: dict) -> None:
205 185
     except Exception:
206 186
         pass
207 187
 
208
-
209 188
 def inject_numbering_from_json(doc: Document, style_data: dict) -> None:
210
-    """将 JSON 中的编号格式定义注入到文档
211
-    
212
-    由于 python-docx 对 numbering part 的支持有限,
213
-    我们需要在保存后通过修改 ZIP 文件来注入编号格式。
214
-    这个函数主要是为了记录编号定义,实际注入在 blocks_to_docx_bytes 中完成。
215
-    """
189
+    """注入编号格式到文档 numbering.xml(python-docx 不支持,通过修改 ZIP 实现)"""
190
+
216 191
     # 暂时不在这里注入,而是在生成文档后通过 ZIP 修改
217 192
     pass
218 193
 
219
-
220 194
 def _resolve_style_id(style_map: dict, *keys: str) -> Optional[str]:
221 195
     """解析样式 ID"""
222 196
     for key in keys:
@@ -225,7 +199,6 @@ def _resolve_style_id(style_map: dict, *keys: str) -> Optional[str]:
225 199
             return entry["style_id"]
226 200
     return None
227 201
 
228
-
229 202
 def _apply_paragraph_style(para, style: dict):
230 203
     """应用段落级样式(对齐方式、行距、缩进等)"""
231 204
     # 对齐方式
@@ -263,7 +236,6 @@ def _apply_paragraph_style(para, style: dict):
263 236
         except Exception:
264 237
             pass
265 238
 
266
-
267 239
 def _apply_run_style(run, style: dict):
268 240
     """应用 run 级样式(字符级格式)- 增强版"""
269 241
     # 字体名称(支持中文字体 eastAsia)- 优先处理字体
@@ -370,7 +342,6 @@ def _apply_run_style(run, style: dict):
370 342
         except (ValueError, AttributeError):
371 343
             pass
372 344
 
373
-
374 345
 # ------------------------------------------------------------------ #
375 346
 # 页面设置应用
376 347
 # ------------------------------------------------------------------ #
@@ -383,14 +354,7 @@ def twips_to_emu(twips: int) -> int:
383 354
 
384 355
 
385 356
 def apply_page_setup(doc: Document, style_data: dict) -> None:
386
-    """应用页面设置到文档
387
-    
388
-    从 style_data 中读取 page_setup,应用到文档的第一个 section
389
-    
390
-    Args:
391
-        doc: python-docx Document 对象
392
-        style_data: 样式数据(包含 page_setup)
393
-    """
357
+    """应用页面设置到文档第一个 section (doc: Document, style_data: dict)"""
394 358
     page_setup = style_data.get("page_setup")
395 359
     if not page_setup:
396 360
         return
@@ -473,16 +437,8 @@ def apply_page_setup(doc: Document, style_data: dict) -> None:
473 437
         print(f"警告: 应用页面设置失败: {e}")
474 438
         pass
475 439
 
476
-
477 440
 def _apply_document_grid(section, grid_type: str = None, chars_per_line: int = None, lines_per_page: int = None) -> None:
478
-    """应用文档网格设置(通过 XML 操作)
479
-    
480
-    Args:
481
-        section: python-docx Section 对象
482
-        grid_type: 网格类型(default/lines/linesAndChars/snapToChars)
483
-        chars_per_line: 每行字符数
484
-        lines_per_page: 每页行数
485
-    """
441
+    """应用文档网格设置到 section(grid_type/chars_per_line/lines_per_page,通过 XML 操作)"""
486 442
     try:
487 443
         # 获取 section 的 XML 元素
488 444
         sectPr = None
@@ -508,12 +464,10 @@ def _apply_document_grid(section, grid_type: str = None, chars_per_line: int = N
508 464
             docGrid.set(qn('w:type'), grid_type)
509 465
         
510 466
         # 设置每页行数(linePitch)
511
-        # 注意:Word XML 中 linePitch 表示行间距,用于控制每页行数
512 467
         if lines_per_page is not None and lines_per_page > 0:
513 468
             docGrid.set(qn('w:linePitch'), str(lines_per_page))
514 469
         
515 470
         # 设置每行字符数(charSpace)
516
-        # 注意:Word XML 中 charSpace 表示字符间距,用于控制每行字符数
517 471
         if chars_per_line is not None and chars_per_line > 0:
518 472
             docGrid.set(qn('w:charSpace'), str(chars_per_line))
519 473
         
@@ -521,7 +475,6 @@ def _apply_document_grid(section, grid_type: str = None, chars_per_line: int = N
521 475
         print(f"警告: 应用文档网格失败: {e}")
522 476
         pass
523 477
 
524
-
525 478
 # ------------------------------------------------------------------ #
526 479
 # Blocks → Word 转换
527 480
 # ------------------------------------------------------------------ #

+ 2 - 28
app/services/image_service.py

@@ -1,5 +1,4 @@
1 1
 """image_service.py — 图片提取和处理服务"""
2
-
3 2
 import base64
4 3
 import json
5 4
 from typing import Dict, List
@@ -7,19 +6,8 @@ from typing import Dict, List
7 6
 from docx import Document
8 7
 from docx.enum.text import WD_ALIGN_PARAGRAPH
9 8
 
10
-
11 9
 def extract_images_from_word(doc: Document) -> List[Dict]:
12
-    """从 Word 文档提取图片及基本样式信息
13
-    
14
-    Args:
15
-        doc: python-docx Document 对象
16
-        
17
-    Returns:
18
-        图片列表,每个元素包含:
19
-        - paragraph_index: 图片所在段落索引
20
-        - data_url: Base64 编码的 Data URL
21
-        - style: 样式信息(宽度、高度、对齐方式、段落样式)
22
-    """
10
+    """从 Word 文档提取图片(paragraph_index, data_url, style: 宽高/对齐/段落样式)"""
23 11
     images = []
24 12
     
25 13
     # 建立 rel_id -> 图片数据映射
@@ -83,21 +71,7 @@ def extract_images_from_word(doc: Document) -> List[Dict]:
83 71
     
84 72
     return images
85 73
 
86
-
87 74
 def create_image_markdown(data_url: str, style: Dict, alt: str = "图片") -> str:
88
-    """生成带样式注释的图片 Markdown
89
-    
90
-    Args:
91
-        data_url: Base64 编码的 Data URL
92
-        style: 样式字典(width, height, align)
93
-        alt: 图片替代文本
94
-        
95
-    Returns:
96
-        格式化的 Markdown 字符串
97
-        
98
-    Example:
99
-        <!-- img-style: {"width": 4.0, "height": 3.0, "align": "center"} -->
100
-        ![图片](data:image/png;base64,...)
101
-    """
75
+    """生成带样式注释的图片 Markdown(<!-- img-style: {...} --> ![alt](data_url))"""
102 76
     style_json = json.dumps(style, ensure_ascii=False)
103 77
     return f'<!-- img-style: {style_json} -->\n![{alt}]({data_url})\n'

+ 0 - 1
app/services/storage_monitor.py

@@ -9,7 +9,6 @@ from app.config import settings
9 9
 
10 10
 logger = logging.getLogger(__name__)
11 11
 
12
-
13 12
 # ------------------------------------------------------------------ #
14 13
 # 磁盘统计工具
15 14
 # ------------------------------------------------------------------ #