import json as _json from typing import List, Optional, Union from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.config import settings from app.base_schema import PaginatedResponse, ResponseModel from zq_smart_table.schema import ( SmartTableCreate, SmartTableUpdate, SmartTableResponse, SmartTableSimple, SmartTableFull, SmartTableMove, SmartFieldCreate, SmartFieldUpdate, SmartFieldResponse, SmartFieldReorder, SmartRecordCreate, SmartRecordUpdate, SmartRecordResponse, SmartRecordCellUpdate, SmartRecordBatchCellUpdate, SmartRecordBatchDelete, SmartRecordBatchRestore, TrashListResponse, MultiRecordBatchUpdate, CursorPaginatedRecords, SmartViewCreate, SmartViewUpdate, SmartViewResponse, RecordQuery, RecordFilterRule, RecordSortRule, SmartRecordReorder, GroupedRecordsResponse, RecordGroupItem, RecordSearchResult, RecordSearchQuery, SummaryRequest, SummaryResponse, CommentCreate, CommentUpdate, CommentResponse, DocumentContentUpdate, DocumentVersionCreate, DocumentVersionResponse, DocumentVersionDetail, DocumentVersionCompare, DocumentTemplateCreate, DocumentTemplateUpdate, DocumentTemplateResponse, DocumentTemplateDetail, WikiSpaceCreate, WikiSpaceUpdate, WikiSpaceResponse, WikiSpaceListItem, WikiSpaceDetail, ) from zq_smart_table.service import ( SmartTableService, SmartFieldService, SmartRecordService, SmartViewService, SmartExportService, SmartImportService, SmartLinkService, SmartSummaryService, SmartFormulaService, SmartValidationService, SmartCommentService, SmartDocumentVersionService, SmartDocumentTemplateService, WikiSpaceService, ) from zq_smart_table.permission.service import PermissionService from utils.context import get_current_user_id_from_context, get_current_user_info_from_context from core.user.service import UserService router = APIRouter(tags=["多维表格"]) INITIAL_RECORD_LIMIT = 500 # ==================== Table ==================== @router.post("/tables", response_model=SmartTableResponse, summary="创建表") async def create_table(data: SmartTableCreate, db: AsyncSession = Depends(get_db)): table = await SmartTableService.create(db, data) user_id = get_current_user_id_from_context() if user_id: await PermissionService.init_table_owner(db, table.id, user_id) return table @router.get("/tables", response_model=List[SmartTableSimple], summary="获取表列表") async def get_table_list( wiki_space_id: Optional[str] = Query(default=None, description="按文档库ID过滤"), db: AsyncSession = Depends(get_db), ): user_info = get_current_user_info_from_context() user_id = user_info.get("user_id", "") if user_info else None dept_id = user_info.get("dept_id") if user_info else None role_ids = user_info.get("role_ids", []) if user_info else [] is_superuser = user_info.get("is_superuser", False) if user_info else False items, _ = await SmartTableService.get_user_tables( db, wiki_space_id=wiki_space_id, user_id=user_id, dept_id=dept_id, role_ids=role_ids, is_superuser=is_superuser, ) return items @router.get("/tables/{table_id}", response_model=SmartTableResponse, summary="获取表详情") async def get_table(table_id: str, db: AsyncSession = Depends(get_db)): table = await SmartTableService.get_by_id(db, table_id) if not table: raise HTTPException(status_code=404, detail="表不存在") await PermissionService.require_table_access(db, table_id) return table @router.get("/tables/{table_id}/full", response_model=SmartTableFull, summary="获取完整表数据") async def get_table_full( table_id: str, limit: int = Query(default=INITIAL_RECORD_LIMIT, ge=1, le=10000, description="首次加载记录数"), filters: Optional[str] = Query(default=None, description="筛选条件 JSON 数组"), filter_logic: str = Query(default="and", description="筛选逻辑: and / or"), sorts: Optional[str] = Query(default=None, description="排序规则 JSON 数组"), search: Optional[str] = Query(default=None, description="搜索关键词"), db: AsyncSession = Depends(get_db), ): """返回表的 fields + views + 首批 records(游标分页),前端按需加载更多""" role_info = await PermissionService.require_table_access(db, table_id) table = await SmartTableService.get_by_id(db, table_id) if not table: raise HTTPException(status_code=404, detail="表不存在") creator_name = None creator_avatar = None if table.sys_creator_id: creator = await UserService.get_by_id(db, table.sys_creator_id) if creator: creator_name = getattr(creator, "name", None) or getattr(creator, "username", None) creator_avatar = getattr(creator, "avatar", None) if getattr(table, "type", "table") == "document": return SmartTableFull( id=table.id, name=table.name, icon=table.icon, description=table.description, active_view_id=None, type="document", parent_id=getattr(table, "parent_id", None), content=table.content, fields=[], records=[], views=[], record_total=0, next_cursor=None, has_more=False, sys_create_datetime=table.sys_create_datetime, sys_update_datetime=table.sys_update_datetime, sys_creator_id=table.sys_creator_id, creator_name=creator_name, creator_avatar=creator_avatar, ) all_fields = await SmartFieldService.get_by_table(db, table_id) accessible = await PermissionService.get_accessible_field_ids( db, table_id, role_info.get("role_id"), mode="read" ) fields = [f for f in all_fields if f.id in accessible] if accessible is not None else all_fields parsed_filters = None if filters: try: parsed_filters = [RecordFilterRule(**f) for f in _json.loads(filters)] except Exception: raise HTTPException(status_code=400, detail="筛选条件格式错误") parsed_sorts = None if sorts: try: parsed_sorts = [RecordSortRule(**s) for s in _json.loads(sorts)] except Exception: raise HTTPException(status_code=400, detail="排序规则格式错误") user_info = get_current_user_info_from_context() or {} row_conditions = await PermissionService.build_row_filter_conditions( db, table_id, role_info.get("role_id"), user_info.get("user_id", ""), rule_type="view" ) records_list, next_cursor, total = await SmartRecordService.get_by_table_cursor_filtered( db, table_id, filters=parsed_filters, filter_logic=filter_logic, sorts=parsed_sorts, search=search, extra_conditions=row_conditions or None, cursor=None, limit=limit, ) await SmartLinkService.resolve_link_values(db, all_fields, records_list) await SmartLinkService.resolve_lookup_values(db, all_fields, records_list) await SmartLinkService.resolve_rollup_values(db, all_fields, records_list) SmartFormulaService.resolve_formula_values(all_fields, records_list) if accessible is not None: for r in records_list: r.values = PermissionService.filter_record_values(r.values or {}, accessible) views = await SmartViewService.get_by_table(db, table_id) return SmartTableFull( id=table.id, name=table.name, icon=table.icon, description=table.description, active_view_id=table.active_view_id, type=getattr(table, "type", "table"), parent_id=getattr(table, "parent_id", None), fields=fields, records=records_list, views=views, record_total=total, next_cursor=next_cursor, has_more=next_cursor is not None, sys_create_datetime=table.sys_create_datetime, sys_update_datetime=table.sys_update_datetime, sys_creator_id=table.sys_creator_id, creator_name=creator_name, creator_avatar=creator_avatar, ) @router.put("/tables/{table_id}", response_model=SmartTableResponse, summary="更新表") async def update_table(table_id: str, data: SmartTableUpdate, db: AsyncSession = Depends(get_db)): await PermissionService.require_capability(db, table_id, "manage_table") table = await SmartTableService.update(db, record_id=table_id, data=data) if not table: raise HTTPException(status_code=404, detail="表不存在") return table @router.delete("/tables/{table_id}", response_model=ResponseModel, summary="删除表") async def delete_table(table_id: str, db: AsyncSession = Depends(get_db)): await PermissionService.require_capability(db, table_id, "manage_table") success = await SmartTableService.delete(db, record_id=table_id) if not success: raise HTTPException(status_code=404, detail="表不存在") return ResponseModel(message="删除成功") @router.put("/tables/{table_id}/move", response_model=SmartTableResponse, summary="移动表/文档到新的父级") async def move_table(table_id: str, data: SmartTableMove, db: AsyncSession = Depends(get_db)): from sqlalchemy import select as sa_select await PermissionService.require_capability(db, table_id, "manage_table") if data.parent_id and data.parent_id == table_id: raise HTTPException(status_code=400, detail="不能将页面移动到自身下") if data.parent_id: parent = await SmartTableService.get_by_id(db, data.parent_id) if not parent: raise HTTPException(status_code=404, detail="目标父页面不存在") ancestor_id = getattr(parent, "parent_id", None) while ancestor_id: if ancestor_id == table_id: raise HTTPException(status_code=400, detail="不能将页面移动到其子页面下") ancestor = await SmartTableService.get_by_id(db, ancestor_id) ancestor_id = getattr(ancestor, "parent_id", None) if ancestor else None table = await SmartTableService.update( db, record_id=table_id, data=SmartTableUpdate(parent_id=data.parent_id) ) if not table: raise HTTPException(status_code=404, detail="表不存在") # Reorder siblings at the target parent level from zq_smart_table.model import SmartTable if data.parent_id: q = sa_select(SmartTable).where( SmartTable.parent_id == data.parent_id, SmartTable.is_deleted == False, # noqa: E712 ).order_by(SmartTable.sort) else: q = sa_select(SmartTable).where( SmartTable.parent_id.is_(None), SmartTable.is_deleted == False, # noqa: E712 ).order_by(SmartTable.sort) result = await db.execute(q) siblings = list(result.scalars().all()) ordered_ids = [s.id for s in siblings if s.id != table_id] if data.after_id and data.after_id in ordered_ids: idx = ordered_ids.index(data.after_id) + 1 ordered_ids.insert(idx, table_id) else: ordered_ids.insert(0, table_id) id_to_obj = {s.id: s for s in siblings} for i, sid in enumerate(ordered_ids): obj = id_to_obj.get(sid) if obj and obj.sort != i: obj.sort = i await db.commit() await db.refresh(table) return table # ==================== Document Content ==================== @router.patch("/tables/{table_id}/content", response_model=ResponseModel, summary="更新文档内容") async def update_document_content( table_id: str, data: DocumentContentUpdate, db: AsyncSession = Depends(get_db), ): await PermissionService.require_capability(db, table_id, "edit_record") table = await SmartTableService.get_by_id(db, table_id) if not table: raise HTTPException(status_code=404, detail="文档不存在") if getattr(table, "type", "table") != "document": raise HTTPException(status_code=400, detail="该项目不是文档类型") user_id = get_current_user_id_from_context() if table.content: try: await SmartDocumentVersionService.create_version( db, table_id, table.content, title=table.name, change_summary="自动保存", user_id=user_id, ) except Exception: pass await SmartTableService.update(db, record_id=table_id, data=SmartTableUpdate(content=data.content)) return ResponseModel(message="保存成功") @router.post("/tables/{table_id}/export-pdf", summary="导出文档为 PDF") async def export_document_pdf( table_id: str, data: dict, db: AsyncSession = Depends(get_db), ): """接收编辑器 HTML 内容,使用 WeasyPrint 生成 PDF 并返回下载流""" import io from online_dev.document_generator.generator import pdf_generator, WEASYPRINT_AVAILABLE, HTML await PermissionService.require_table_access(db, table_id) table = await SmartTableService.get_by_id(db, table_id) if not table: raise HTTPException(status_code=404, detail="文档不存在") html_content = data.get("html", "") title = data.get("title", table.name or "文档") if not html_content: raise HTTPException(status_code=400, detail="导出内容不能为空") if not WEASYPRINT_AVAILABLE: raise HTTPException(status_code=500, detail="WeasyPrint 不可用,无法生成 PDF") doc_css = """ h1 { font-size: 22pt; font-weight: 700; margin: 0 0 14pt; padding-bottom: 8pt; border-bottom: 1px solid #e5e5e5; } h2 { font-size: 17pt; font-weight: 600; margin: 18pt 0 8pt; } h3 { font-size: 13pt; font-weight: 600; margin: 14pt 0 6pt; } p { margin: 5pt 0; } ul, ol { padding-left: 20pt; margin: 5pt 0; } li { margin: 2pt 0; } blockquote { border-left: 3pt solid #d0d0d0; padding-left: 10pt; margin: 6pt 0; color: #555; font-style: italic; } pre { background: #f6f8fa; padding: 8pt; border-radius: 4pt; border: 1px solid #e1e4e8; font-size: 9pt; line-height: 1.5; } code { background: #f0f0f0; padding: 1pt 3pt; border-radius: 3pt; font-size: 9.5pt; } pre code { background: none; padding: 0; font-size: inherit; } table { border-collapse: collapse; width: 100%; margin: 8pt 0; } th, td { border: 1px solid #d0d0d0; padding: 5pt 8pt; text-align: left; font-size: 10pt; } th { background: #f5f5f5; font-weight: 600; } img { max-width: 100%; } a { color: #1a73e8; text-decoration: none; } hr { border: none; border-top: 1px solid #e5e5e5; margin: 10pt 0; } """ body_html = f"

{title}

\n{html_content}" full_html = pdf_generator._wrap_html( body_html, css=doc_css, page_config={"size": "A4", "orientation": "portrait", "margin": {"top": 20, "right": 15, "bottom": 20, "left": 15}}, ) try: html_doc = HTML(string=full_html) pdf_bytes = html_doc.write_pdf(font_config=pdf_generator.font_config) except Exception as e: raise HTTPException(status_code=500, detail=f"PDF 生成失败: {str(e)}") from urllib.parse import quote safe_name = table.name.replace("/", "_").replace("\\", "_") encoded_name = quote(f"{safe_name}.pdf") return StreamingResponse( io.BytesIO(pdf_bytes), media_type="application/pdf", headers={ "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_name}", }, ) # ==================== Field ==================== @router.get("/tables/{table_id}/fields", response_model=List[SmartFieldResponse], summary="获取字段列表") async def get_field_list(table_id: str, db: AsyncSession = Depends(get_db)): await PermissionService.require_table_access(db, table_id) return await SmartFieldService.get_by_table(db, table_id) @router.post("/tables/{table_id}/fields", response_model=SmartFieldResponse, summary="添加字段") async def create_field(table_id: str, data: SmartFieldCreate, db: AsyncSession = Depends(get_db)): await PermissionService.require_capability(db, table_id, "manage_field") data.table_id = table_id if data.type == "link": linked_table_id = (data.config or {}).get("linkedTableId") if not linked_table_id: raise HTTPException(status_code=400, detail="Link 字段必须指定 linkedTableId") source_field, _ = await SmartLinkService.create_link_field( db, table_id, data.name, linked_table_id, sort=data.sort, ) return source_field if data.sort == 0: data.sort = await SmartFieldService.get_next_sort(db, table_id) field = await SmartFieldService.create(db, data) return field @router.put("/fields/{field_id}", response_model=SmartFieldResponse, summary="更新字段") async def update_field(field_id: str, data: SmartFieldUpdate, db: AsyncSession = Depends(get_db)): field = await SmartFieldService.get_by_id(db, field_id) if not field: raise HTTPException(status_code=404, detail="字段不存在") await PermissionService.require_capability(db, field.table_id, "manage_field") field = await SmartFieldService.update(db, record_id=field_id, data=data) return field @router.delete("/fields/{field_id}", response_model=ResponseModel, summary="删除字段") async def delete_field(field_id: str, db: AsyncSession = Depends(get_db)): field = await SmartFieldService.get_by_id(db, field_id) if not field: raise HTTPException(status_code=404, detail="字段不存在") await PermissionService.require_capability(db, field.table_id, "manage_field") if field.type == "link": success = await SmartLinkService.delete_link_field(db, field_id) else: success = await SmartFieldService.delete_and_clean(db, field_id) if not success: raise HTTPException(status_code=404, detail="字段不存在") return ResponseModel(message="删除成功") @router.put("/tables/{table_id}/fields/reorder", response_model=ResponseModel, summary="字段排序") async def reorder_fields(table_id: str, data: SmartFieldReorder, db: AsyncSession = Depends(get_db)): await PermissionService.require_capability(db, table_id, "manage_field") await SmartFieldService.reorder(db, table_id, data.field_ids) return ResponseModel(message="排序成功") # ==================== Record ==================== @router.get( "/tables/{table_id}/records", response_model=CursorPaginatedRecords, summary="获取记录列表(游标分页,支持筛选/排序/搜索)", ) async def get_record_list( table_id: str, cursor: Optional[str] = Query(default=None, description="游标(上一页最后一条记录的 ID)"), limit: int = Query(default=200, ge=1, le=5000, description="每页数量"), filters: Optional[str] = Query(default=None, description="筛选条件 JSON 数组"), filter_logic: str = Query(default="and", description="筛选逻辑: and / or"), sorts: Optional[str] = Query(default=None, description="排序规则 JSON 数组"), search: Optional[str] = Query(default=None, description="搜索关键词"), db: AsyncSession = Depends(get_db), ): role_info = await PermissionService.require_table_access(db, table_id) parsed_filters = None if filters: try: parsed_filters = [RecordFilterRule(**f) for f in _json.loads(filters)] except Exception: raise HTTPException(status_code=400, detail="筛选条件格式错误") parsed_sorts = None if sorts: try: parsed_sorts = [RecordSortRule(**s) for s in _json.loads(sorts)] except Exception: raise HTTPException(status_code=400, detail="排序规则格式错误") user_info = get_current_user_info_from_context() or {} row_conditions = await PermissionService.build_row_filter_conditions( db, table_id, role_info.get("role_id"), user_info.get("user_id", ""), rule_type="view" ) skip_count = cursor is not None items, next_cursor, total = await SmartRecordService.get_by_table_cursor_filtered( db, table_id, filters=parsed_filters, filter_logic=filter_logic, sorts=parsed_sorts, search=search, extra_conditions=row_conditions or None, cursor=cursor, limit=limit, skip_count=skip_count, ) all_fields = await SmartFieldService.get_by_table(db, table_id) await SmartLinkService.resolve_link_values(db, all_fields, items) await SmartLinkService.resolve_lookup_values(db, all_fields, items) await SmartLinkService.resolve_rollup_values(db, all_fields, items) SmartFormulaService.resolve_formula_values(all_fields, items) accessible = await PermissionService.get_accessible_field_ids( db, table_id, role_info.get("role_id"), mode="read" ) if accessible is not None: for r in items: r.values = PermissionService.filter_record_values(r.values or {}, accessible) return CursorPaginatedRecords( items=items, total=total, next_cursor=next_cursor, has_more=next_cursor is not None, ) @router.post( "/tables/{table_id}/records/query", response_model=Union[GroupedRecordsResponse, CursorPaginatedRecords], summary="复杂查询记录(POST Body 传递筛选/排序/搜索条件)", ) async def query_records( table_id: str, query: RecordQuery, db: AsyncSession = Depends(get_db), ): role_info = await PermissionService.require_table_access(db, table_id) user_info = get_current_user_info_from_context() or {} row_conditions = await PermissionService.build_row_filter_conditions( db, table_id, role_info.get("role_id"), user_info.get("user_id", ""), rule_type="view" ) skip_count = query.cursor is not None items, next_cursor, total = await SmartRecordService.get_by_table_cursor_filtered( db, table_id, filters=query.filters or None, filter_logic=query.filter_logic, sorts=query.sorts or None, search=query.search, search_field_ids=query.search_field_ids, extra_conditions=row_conditions or None, cursor=query.cursor, limit=query.limit, skip_count=skip_count, ) all_fields = await SmartFieldService.get_by_table(db, table_id) await SmartLinkService.resolve_link_values(db, all_fields, items) await SmartLinkService.resolve_lookup_values(db, all_fields, items) await SmartLinkService.resolve_rollup_values(db, all_fields, items) SmartFormulaService.resolve_formula_values(all_fields, items) accessible = await PermissionService.get_accessible_field_ids( db, table_id, role_info.get("role_id"), mode="read" ) if accessible is not None: for r in items: r.values = PermissionService.filter_record_values(r.values or {}, accessible) if query.group_field_id: field = next((f for f in all_fields if f.id == query.group_field_id), None) groups_map: dict[str, list] = {} for r in items: val = str((r.values or {}).get(query.group_field_id, "") or "") groups_map.setdefault(val, []).append(r) group_items = [] for key, recs in groups_map.items(): label = key or "(空)" if field and field.config and field.config.get("options"): opt = next((o for o in field.config["options"] if o.get("id") == key), None) if opt: label = opt.get("label", key) group_items.append(RecordGroupItem(key=key, label=label, records=recs)) return GroupedRecordsResponse(groups=group_items, total=total) return CursorPaginatedRecords( items=items, total=total, next_cursor=next_cursor, has_more=next_cursor is not None, ) @router.post("/tables/{table_id}/records", response_model=SmartRecordResponse, summary="添加记录") async def create_record(table_id: str, data: SmartRecordCreate, db: AsyncSession = Depends(get_db)): await PermissionService.require_capability(db, table_id, "add_record") data.table_id = table_id all_fields = await SmartFieldService.get_by_table(db, table_id) auto_number_fields = [f for f in all_fields if f.type == "autoNumber"] if auto_number_fields: if not data.values: data.values = {} for af in auto_number_fields: if af.id not in data.values: next_val = await SmartRecordService.get_next_auto_number(db, table_id, af.id) data.values[af.id] = next_val record = await SmartRecordService.create(db, data) return record @router.put("/records/{record_id}", response_model=SmartRecordResponse, summary="更新记录") async def update_record(record_id: str, data: SmartRecordUpdate, db: AsyncSession = Depends(get_db)): record = await SmartRecordService.get_by_id(db, record_id) if not record: raise HTTPException(status_code=404, detail="记录不存在") role_info = await PermissionService.require_capability(db, record.table_id, "edit_record") user_info = get_current_user_info_from_context() or {} can_edit = await PermissionService.check_row_edit_permission( db, record.table_id, record, role_info.get("role_id"), user_info.get("user_id", "") ) if not can_edit: raise HTTPException(status_code=403, detail="无权编辑此记录") record = await SmartRecordService.update(db, record_id=record_id, data=data) return record @router.patch("/records/{record_id}/cells", response_model=SmartRecordResponse, summary="更新单元格") async def update_cell(record_id: str, data: SmartRecordCellUpdate, db: AsyncSession = Depends(get_db)): record = await SmartRecordService.get_by_id(db, record_id) if not record: raise HTTPException(status_code=404, detail="记录不存在") role_info = await PermissionService.require_capability(db, record.table_id, "edit_record") user_info = get_current_user_info_from_context() or {} can_edit = await PermissionService.check_row_edit_permission( db, record.table_id, record, role_info.get("role_id"), user_info.get("user_id", "") ) if not can_edit: raise HTTPException(status_code=403, detail="无权编辑此记录") writable = await PermissionService.get_accessible_field_ids( db, record.table_id, role_info.get("role_id"), mode="write" ) if writable is not None and data.field_id not in writable: raise HTTPException(status_code=403, detail="无权编辑此字段") field = await SmartFieldService.get_by_id(db, data.field_id) if not field: raise HTTPException(status_code=404, detail="字段不存在") validation_error = await SmartValidationService.validate_cell(db, field, data.value, record_id) if validation_error: raise HTTPException(status_code=422, detail=validation_error) if field.type == "link": target_ids = data.value if isinstance(data.value, list) else [] await SmartLinkService.set_linked_records(db, data.field_id, record_id, target_ids) record = await SmartRecordService.get_by_id(db, record_id) all_fields = await SmartFieldService.get_by_table(db, record.table_id) await SmartLinkService.resolve_link_values(db, all_fields, [record]) await SmartLinkService.resolve_lookup_values(db, all_fields, [record]) await SmartLinkService.resolve_rollup_values(db, all_fields, [record]) return record record = await SmartRecordService.update_cell(db, record_id, data.field_id, data.value) return record @router.patch("/records/{record_id}/cells/batch", response_model=SmartRecordResponse, summary="批量更新单元格") async def batch_update_cells(record_id: str, data: SmartRecordBatchCellUpdate, db: AsyncSession = Depends(get_db)): record = await SmartRecordService.get_by_id(db, record_id) if not record: raise HTTPException(status_code=404, detail="记录不存在") role_info = await PermissionService.require_capability(db, record.table_id, "edit_record") user_info = get_current_user_info_from_context() or {} can_edit = await PermissionService.check_row_edit_permission( db, record.table_id, record, role_info.get("role_id"), user_info.get("user_id", "") ) if not can_edit: raise HTTPException(status_code=403, detail="无权编辑此记录") writable = await PermissionService.get_accessible_field_ids( db, record.table_id, role_info.get("role_id"), mode="write" ) filtered_cells = data.cells if writable is not None: filtered_cells = {k: v for k, v in data.cells.items() if k in writable} if not filtered_cells: raise HTTPException(status_code=403, detail="无权编辑任何提交的字段") link_cells = {} normal_cells = {} fields_cache = {} for field_id, val in filtered_cells.items(): if field_id not in fields_cache: f = await SmartFieldService.get_by_id(db, field_id) fields_cache[field_id] = f f = fields_cache[field_id] if f and f.type == "link": link_cells[field_id] = val else: normal_cells[field_id] = val validation_errors = await SmartValidationService.validate_cells_batch( db, record.table_id, normal_cells, record_id ) if validation_errors: first_err = next(iter(validation_errors.values())) raise HTTPException(status_code=422, detail=first_err) if normal_cells: record = await SmartRecordService.batch_update_cells(db, record_id, normal_cells) for fid, val in link_cells.items(): target_ids = val if isinstance(val, list) else [] await SmartLinkService.set_linked_records(db, fid, record_id, target_ids) record = await SmartRecordService.get_by_id(db, record_id) if link_cells: all_fields = await SmartFieldService.get_by_table(db, record.table_id) await SmartLinkService.resolve_link_values(db, all_fields, [record]) await SmartLinkService.resolve_lookup_values(db, all_fields, [record]) await SmartLinkService.resolve_rollup_values(db, all_fields, [record]) return record @router.delete("/records/{record_id}", response_model=ResponseModel, summary="删除记录") async def delete_record(record_id: str, db: AsyncSession = Depends(get_db)): record = await SmartRecordService.get_by_id(db, record_id) if not record: raise HTTPException(status_code=404, detail="记录不存在") await PermissionService.require_capability(db, record.table_id, "delete_record") await SmartLinkService.cleanup_record_links(db, record_id) success = await SmartRecordService.delete(db, record_id=record_id) if not success: raise HTTPException(status_code=404, detail="记录不存在") return ResponseModel(message="删除成功") @router.post("/tables/{table_id}/records/batch-delete", response_model=ResponseModel, summary="批量删除记录") async def batch_delete_records(table_id: str, data: SmartRecordBatchDelete, db: AsyncSession = Depends(get_db)): await PermissionService.require_capability(db, table_id, "delete_record") for rid in data.ids: await SmartLinkService.cleanup_record_links(db, rid) success, fail = await SmartRecordService.batch_delete(db, data.ids) return ResponseModel(message=f"成功删除{success}条", data={"success": success, "fail": fail}) # ==================== Trash / Recycle Bin ==================== @router.get("/tables/{table_id}/trash", response_model=TrashListResponse, summary="获取回收站记录列表") async def get_trash_records( table_id: str, page: int = Query(default=1, ge=1), page_size: int = Query(default=50, ge=1, le=200), db: AsyncSession = Depends(get_db), ): await PermissionService.require_capability(db, table_id, "delete_record") items, total = await SmartRecordService.get_deleted_records(db, table_id, page, page_size) return TrashListResponse(items=items, total=total) @router.post("/tables/{table_id}/trash/restore", response_model=ResponseModel, summary="批量恢复回收站记录") async def restore_trash_records( table_id: str, data: SmartRecordBatchRestore, db: AsyncSession = Depends(get_db), ): await PermissionService.require_capability(db, table_id, "delete_record") count = await SmartRecordService.batch_restore_records(db, table_id, data.ids) return ResponseModel(message=f"成功恢复{count}条记录", data={"restored": count}) @router.delete("/tables/{table_id}/trash/{record_id}", response_model=ResponseModel, summary="永久删除回收站记录") async def permanent_delete_record( table_id: str, record_id: str, db: AsyncSession = Depends(get_db), ): await PermissionService.require_capability(db, table_id, "delete_record") success = await SmartRecordService.permanent_delete(db, table_id, record_id) if not success: raise HTTPException(status_code=404, detail="记录不存在或未被删除") return ResponseModel(message="永久删除成功") @router.delete("/tables/{table_id}/trash", response_model=ResponseModel, summary="清空回收站") async def empty_trash( table_id: str, db: AsyncSession = Depends(get_db), ): await PermissionService.require_capability(db, table_id, "delete_record") count = await SmartRecordService.empty_trash(db, table_id) return ResponseModel(message=f"已清空{count}条记录", data={"deleted": count}) @router.patch( "/tables/{table_id}/records/batch-cells", response_model=ResponseModel, summary="批量更新多条记录的单元格(合并为单次事务)", ) async def batch_update_multi_record_cells( table_id: str, data: MultiRecordBatchUpdate, db: AsyncSession = Depends(get_db), ): role_info = await PermissionService.require_capability(db, table_id, "edit_record") user_info = get_current_user_info_from_context() or {} user_id = user_info.get("user_id", "") writable_fields = await PermissionService.get_accessible_field_ids( db, table_id, role_info.get("role_id"), mode="write" ) fields_cache: dict = {} valid_updates = [] for u in data.updates: record = await SmartRecordService.get_by_id(db, u.record_id) if not record or record.table_id != table_id: continue can_edit = await PermissionService.check_row_edit_permission( db, table_id, record, role_info.get("role_id"), user_id ) if not can_edit: continue filtered_cells = {} for field_id, val in u.cells.items(): if writable_fields is not None and field_id not in writable_fields: continue if field_id not in fields_cache: fields_cache[field_id] = await SmartFieldService.get_by_id(db, field_id) field = fields_cache[field_id] if not field: continue err = await SmartValidationService.validate_cell(db, field, val, u.record_id) if err: continue filtered_cells[field_id] = val if filtered_cells: valid_updates.append({"record_id": u.record_id, "cells": filtered_cells}) count = await SmartRecordService.batch_update_multi_records(db, valid_updates) return ResponseModel(message=f"成功更新{count}条记录", data={"updated": count}) @router.put("/tables/{table_id}/records/reorder", response_model=ResponseModel, summary="记录排序") async def reorder_records(table_id: str, data: SmartRecordReorder, db: AsyncSession = Depends(get_db)): await SmartRecordService.reorder(db, table_id, data.record_ids) return ResponseModel(message="排序成功") @router.post( "/tables/{table_id}/records/search", response_model=List[RecordSearchResult], summary="搜索表记录(供 Link 字段选择器使用)", ) async def search_records( table_id: str, query: RecordSearchQuery, db: AsyncSession = Depends(get_db), ): items = await SmartLinkService.search_records( db, table_id, keyword=query.keyword, limit=query.limit, ) return [RecordSearchResult(**i) for i in items] # ==================== Summary ==================== @router.post( "/tables/{table_id}/summary", response_model=SummaryResponse, summary="获取字段汇总统计", ) async def get_table_summary( table_id: str, data: SummaryRequest, db: AsyncSession = Depends(get_db), ): role_info = await PermissionService.require_table_access(db, table_id) parsed_filters = data.filters or None user_info = get_current_user_info_from_context() or {} row_conditions = await PermissionService.build_row_filter_conditions( db, table_id, role_info.get("role_id"), user_info.get("user_id", ""), rule_type="view" ) summaries, total = await SmartSummaryService.compute( db, table_id, field_agg_map=data.aggregations, filters=parsed_filters, filter_logic=data.filter_logic, search=data.search, extra_conditions=row_conditions or None, ) return SummaryResponse(summaries=summaries, total_count=total) # ==================== View ==================== @router.get("/tables/{table_id}/views", response_model=List[SmartViewResponse], summary="获取视图列表") async def get_view_list(table_id: str, db: AsyncSession = Depends(get_db)): await PermissionService.require_table_access(db, table_id) return await SmartViewService.get_by_table(db, table_id) @router.post("/tables/{table_id}/views", response_model=SmartViewResponse, summary="创建视图") async def create_view(table_id: str, data: SmartViewCreate, db: AsyncSession = Depends(get_db)): await PermissionService.require_capability(db, table_id, "manage_view") data.table_id = table_id view = await SmartViewService.create(db, data) return view @router.put("/views/{view_id}", response_model=SmartViewResponse, summary="更新视图") async def update_view(view_id: str, data: SmartViewUpdate, db: AsyncSession = Depends(get_db)): existing_view = await SmartViewService.get_by_id(db, view_id) if not existing_view: raise HTTPException(status_code=404, detail="视图不存在") await PermissionService.require_capability(db, existing_view.table_id, "manage_view") view = await SmartViewService.update(db, record_id=view_id, data=data) return view @router.delete("/views/{view_id}", response_model=ResponseModel, summary="删除视图") async def delete_view(view_id: str, db: AsyncSession = Depends(get_db)): existing_view = await SmartViewService.get_by_id(db, view_id) if not existing_view: raise HTTPException(status_code=404, detail="视图不存在") await PermissionService.require_capability(db, existing_view.table_id, "manage_view") success = await SmartViewService.delete(db, record_id=view_id) if not success: raise HTTPException(status_code=404, detail="视图不存在") return ResponseModel(message="删除成功") # ==================== Export / Import ==================== @router.get("/tables/{table_id}/export", summary="导出表数据") async def export_table( table_id: str, format: str = Query(default="csv", description="导出格式: csv / xlsx"), db: AsyncSession = Depends(get_db), ): role_info = await PermissionService.require_capability(db, table_id, "export_data") table = await SmartTableService.get_by_id(db, table_id) if not table: raise HTTPException(status_code=404, detail="表不存在") fields = await SmartFieldService.get_by_table(db, table_id) accessible = await PermissionService.get_accessible_field_ids( db, table_id, role_info.get("role_id"), mode="read" ) if accessible is not None: fields = [f for f in fields if f.id in accessible] user_info = get_current_user_info_from_context() or {} row_conditions = await PermissionService.build_row_filter_conditions( db, table_id, role_info.get("role_id"), user_info.get("user_id", ""), rule_type="view" ) records, _, _ = await SmartRecordService.get_by_table_cursor_filtered( db, table_id, extra_conditions=row_conditions or None, limit=50000 ) all_fields = await SmartFieldService.get_by_table(db, table_id) await SmartLinkService.resolve_link_values(db, all_fields, records) await SmartLinkService.resolve_lookup_values(db, all_fields, records) await SmartLinkService.resolve_rollup_values(db, all_fields, records) import io from urllib.parse import quote safe_name = table.name.replace("/", "_").replace("\\", "_") def _content_disposition(filename: str) -> str: """Build Content-Disposition with RFC 5987 filename* for non-ASCII names.""" ascii_name = filename.encode("ascii", "ignore").decode("ascii") or "export" encoded_name = quote(filename) return ( f'attachment; filename="{ascii_name}"; ' f"filename*=UTF-8''{encoded_name}" ) if format == "xlsx": content = await SmartExportService.export_xlsx(db, table_id, fields, records) return StreamingResponse( io.BytesIO(content), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": _content_disposition(f"{safe_name}.xlsx")}, ) else: content = await SmartExportService.export_csv(db, table_id, fields, records) return StreamingResponse( io.BytesIO(content.encode("utf-8-sig")), media_type="text/csv; charset=utf-8", headers={"Content-Disposition": _content_disposition(f"{safe_name}.csv")}, ) @router.post("/tables/{table_id}/import", response_model=ResponseModel, summary="导入数据") async def import_table( table_id: str, file: UploadFile = File(..., description="CSV 或 Excel 文件"), db: AsyncSession = Depends(get_db), ): await PermissionService.require_capability(db, table_id, "import_data") table = await SmartTableService.get_by_id(db, table_id) if not table: raise HTTPException(status_code=404, detail="表不存在") fields = await SmartFieldService.get_by_table(db, table_id) file_content = await file.read() filename = file.filename or "" if filename.endswith(".xlsx") or filename.endswith(".xls"): result = await SmartImportService.import_xlsx(db, table_id, file_content, fields) elif filename.endswith(".csv"): result = await SmartImportService.import_csv(db, table_id, file_content, fields) else: raise HTTPException(status_code=400, detail="仅支持 CSV 和 Excel (.xlsx) 文件") return ResponseModel( message=f"导入完成:成功 {result['success']} 条,失败 {result['fail']} 条", data=result, ) # ==================== Comment ==================== @router.get("/records/{record_id}/comments", response_model=List[CommentResponse], summary="获取记录评论") async def get_comments(record_id: str, db: AsyncSession = Depends(get_db)): from sqlalchemy import select as sa_select comments = await SmartCommentService.get_by_record(db, record_id) user_ids = list({c.user_id for c in comments}) user_map: dict = {} if user_ids: try: from core.model import CoreUser result = await db.execute( sa_select(CoreUser.id, CoreUser.name, CoreUser.avatar) .where(CoreUser.id.in_(user_ids)) ) for row in result.all(): user_map[row[0]] = {"name": row[1], "avatar": row[2]} except Exception: pass top_level: list[CommentResponse] = [] reply_map: dict[str, list[CommentResponse]] = {} for c in comments: u = user_map.get(c.user_id, {}) item = CommentResponse( id=c.id, record_id=c.record_id, user_id=c.user_id, content=c.content, mentions=c.mentions or [], parent_id=c.parent_id, sys_create_datetime=c.sys_create_datetime, sys_update_datetime=c.sys_update_datetime, user_name=u.get("name"), user_avatar=u.get("avatar"), ) if c.parent_id: reply_map.setdefault(c.parent_id, []).append(item) else: top_level.append(item) for item in top_level: item.replies = reply_map.get(item.id, []) return top_level @router.post("/records/{record_id}/comments", response_model=CommentResponse, summary="创建评论") async def create_comment(record_id: str, data: CommentCreate, db: AsyncSession = Depends(get_db)): user_id = get_current_user_id_from_context() if not user_id: raise HTTPException(status_code=401, detail="未登录") comment = await SmartCommentService.create( db, record_id, user_id, data.content, data.mentions, data.parent_id, ) user_map: dict = {} try: from sqlalchemy import select as sa_select from core.model import CoreUser result = await db.execute( sa_select(CoreUser.id, CoreUser.name, CoreUser.avatar).where(CoreUser.id == user_id) ) row = result.first() if row: user_map[row[0]] = {"name": row[1], "avatar": row[2]} except Exception: pass u = user_map.get(user_id, {}) return CommentResponse( id=comment.id, record_id=comment.record_id, user_id=comment.user_id, content=comment.content, mentions=comment.mentions or [], parent_id=comment.parent_id, sys_create_datetime=comment.sys_create_datetime, sys_update_datetime=comment.sys_update_datetime, user_name=u.get("name"), user_avatar=u.get("avatar"), ) @router.put("/comments/{comment_id}", response_model=CommentResponse, summary="更新评论") async def update_comment(comment_id: str, data: CommentUpdate, db: AsyncSession = Depends(get_db)): user_id = get_current_user_id_from_context() if not user_id: raise HTTPException(status_code=401, detail="未登录") comment = await SmartCommentService.update(db, comment_id, user_id, data.content, data.mentions) if not comment: raise HTTPException(status_code=404, detail="评论不存在或无权编辑") return CommentResponse( id=comment.id, record_id=comment.record_id, user_id=comment.user_id, content=comment.content, mentions=comment.mentions or [], parent_id=comment.parent_id, sys_create_datetime=comment.sys_create_datetime, sys_update_datetime=comment.sys_update_datetime, ) @router.delete("/comments/{comment_id}", response_model=ResponseModel, summary="删除评论") async def delete_comment(comment_id: str, db: AsyncSession = Depends(get_db)): user_id = get_current_user_id_from_context() if not user_id: raise HTTPException(status_code=401, detail="未登录") ok = await SmartCommentService.delete(db, comment_id, user_id) if not ok: raise HTTPException(status_code=404, detail="评论不存在或无权删除") return ResponseModel(message="删除成功") # ==================== Document Version ==================== @router.get("/tables/{table_id}/versions", summary="获取版本列表") async def get_document_versions( table_id: str, page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"), db: AsyncSession = Depends(get_db), ): await PermissionService.require_table_access(db, table_id) items, total = await SmartDocumentVersionService.get_versions(db, table_id, page, page_size) user_ids = list({v.sys_creator_id for v in items if v.sys_creator_id}) user_map: dict = {} if user_ids: users = await UserService.get_by_ids(db, user_ids) for u in users: user_map[u.id] = u result = [] for v in items: resp = DocumentVersionResponse( id=v.id, document_id=v.document_id, version=v.version, title=v.title, change_summary=v.change_summary, content_size=v.content_size, sys_create_datetime=v.sys_create_datetime, sys_creator_id=v.sys_creator_id, ) u = user_map.get(v.sys_creator_id) if v.sys_creator_id else None if u: resp.creator_name = getattr(u, "name", None) or getattr(u, "username", None) resp.creator_avatar = getattr(u, "avatar", None) result.append(resp) return {"items": result, "total": total} @router.get("/versions/{version_id}", response_model=DocumentVersionDetail, summary="获取版本详情") async def get_document_version_detail( version_id: str, db: AsyncSession = Depends(get_db), ): version = await SmartDocumentVersionService.get_version_detail(db, version_id) if not version: raise HTTPException(status_code=404, detail="版本不存在") await PermissionService.require_table_access(db, version.document_id) u = None if version.sys_creator_id: u = await UserService.get_by_id(db, version.sys_creator_id) return DocumentVersionDetail( id=version.id, document_id=version.document_id, version=version.version, title=version.title, change_summary=version.change_summary, content_size=version.content_size, content=version.content or {}, sys_create_datetime=version.sys_create_datetime, sys_creator_id=version.sys_creator_id, creator_name=(getattr(u, "name", None) or getattr(u, "username", None)) if u else None, creator_avatar=getattr(u, "avatar", None) if u else None, ) @router.post("/tables/{table_id}/versions", response_model=DocumentVersionResponse, summary="手动创建版本快照") async def create_document_version( table_id: str, data: DocumentVersionCreate, db: AsyncSession = Depends(get_db), ): await PermissionService.require_capability(db, table_id, "edit_record") table = await SmartTableService.get_by_id(db, table_id) if not table: raise HTTPException(status_code=404, detail="文档不存在") if getattr(table, "type", "table") != "document": raise HTTPException(status_code=400, detail="该项目不是文档类型") if not table.content: raise HTTPException(status_code=400, detail="文档内容为空,无法创建版本") user_id = get_current_user_id_from_context() version = await SmartDocumentVersionService.create_version( db, table_id, table.content, title=table.name, change_summary=data.change_summary, user_id=user_id, ) return DocumentVersionResponse( id=version.id, document_id=version.document_id, version=version.version, title=version.title, change_summary=version.change_summary, content_size=version.content_size, sys_create_datetime=version.sys_create_datetime, sys_creator_id=version.sys_creator_id, ) @router.post("/tables/{table_id}/versions/{version_id}/restore", response_model=ResponseModel, summary="恢复到指定版本") async def restore_document_version( table_id: str, version_id: str, db: AsyncSession = Depends(get_db), ): await PermissionService.require_capability(db, table_id, "edit_record") user_id = get_current_user_id_from_context() result = await SmartDocumentVersionService.restore_version(db, table_id, version_id, user_id) if not result: raise HTTPException(status_code=404, detail="版本不存在或文档不存在") return ResponseModel(message="版本恢复成功") @router.get("/tables/{table_id}/versions/compare", response_model=DocumentVersionCompare, summary="版本对比") async def compare_document_versions( table_id: str, version_from: str = Query(..., alias="from", description="源版本ID"), version_to: str = Query(..., alias="to", description="目标版本ID"), db: AsyncSession = Depends(get_db), ): await PermissionService.require_table_access(db, table_id) v_from = await SmartDocumentVersionService.get_version_detail(db, version_from) v_to = await SmartDocumentVersionService.get_version_detail(db, version_to) if not v_from or not v_to: raise HTTPException(status_code=404, detail="版本不存在") if v_from.document_id != table_id or v_to.document_id != table_id: raise HTTPException(status_code=400, detail="版本不属于该文档") return DocumentVersionCompare( version_from=DocumentVersionDetail( id=v_from.id, document_id=v_from.document_id, version=v_from.version, title=v_from.title, change_summary=v_from.change_summary, content_size=v_from.content_size, content=v_from.content or {}, sys_create_datetime=v_from.sys_create_datetime, sys_creator_id=v_from.sys_creator_id, ), version_to=DocumentVersionDetail( id=v_to.id, document_id=v_to.document_id, version=v_to.version, title=v_to.title, change_summary=v_to.change_summary, content_size=v_to.content_size, content=v_to.content or {}, sys_create_datetime=v_to.sys_create_datetime, sys_creator_id=v_to.sys_creator_id, ), ) @router.delete("/versions/{version_id}", response_model=ResponseModel, summary="删除版本") async def delete_document_version( version_id: str, db: AsyncSession = Depends(get_db), ): version = await SmartDocumentVersionService.get_version_detail(db, version_id) if not version: raise HTTPException(status_code=404, detail="版本不存在") await PermissionService.require_capability(db, version.document_id, "edit_record") ok = await SmartDocumentVersionService.delete_version(db, version_id) if not ok: raise HTTPException(status_code=404, detail="版本不存在") return ResponseModel(message="删除成功") # ==================== Document Template ==================== @router.get("/document-templates", summary="获取模板列表") async def get_document_templates( category: Optional[str] = Query(default=None, description="分类筛选"), keyword: Optional[str] = Query(default=None, description="关键词搜索"), page: int = Query(default=1, ge=1), page_size: int = Query(default=50, ge=1, le=100, alias="pageSize"), db: AsyncSession = Depends(get_db), ): items, total = await SmartDocumentTemplateService.get_list( db, category=category, keyword=keyword, page=page, page_size=page_size, ) user_ids = list({t.sys_creator_id for t in items if t.sys_creator_id}) user_map: dict = {} if user_ids: users = await UserService.get_by_ids(db, user_ids) for u in users: user_map[u.id] = u result = [] for t in items: resp = DocumentTemplateResponse( id=t.id, name=t.name, description=t.description, icon=t.icon, category=t.category, preview_image=t.preview_image, is_system=t.is_system, use_count=t.use_count, sys_create_datetime=t.sys_create_datetime, sys_update_datetime=t.sys_update_datetime, sys_creator_id=t.sys_creator_id, ) u = user_map.get(t.sys_creator_id) if t.sys_creator_id else None if u: resp.creator_name = getattr(u, "name", None) or getattr(u, "username", None) result.append(resp) return {"items": result, "total": total} @router.get("/document-templates/categories", summary="获取模板分类") async def get_template_categories(db: AsyncSession = Depends(get_db)): return await SmartDocumentTemplateService.get_categories(db) @router.get("/document-templates/{template_id}", response_model=DocumentTemplateDetail, summary="获取模板详情") async def get_document_template_detail( template_id: str, db: AsyncSession = Depends(get_db), ): template = await SmartDocumentTemplateService.get_by_id(db, template_id) if not template: raise HTTPException(status_code=404, detail="模板不存在") u = None if template.sys_creator_id: u = await UserService.get_by_id(db, template.sys_creator_id) return DocumentTemplateDetail( id=template.id, name=template.name, description=template.description, icon=template.icon, category=template.category, preview_image=template.preview_image, is_system=template.is_system, use_count=template.use_count, content=template.content or {}, sys_create_datetime=template.sys_create_datetime, sys_update_datetime=template.sys_update_datetime, sys_creator_id=template.sys_creator_id, creator_name=(getattr(u, "name", None) or getattr(u, "username", None)) if u else None, ) @router.post("/document-templates", response_model=DocumentTemplateResponse, summary="创建模板") async def create_document_template( data: DocumentTemplateCreate, db: AsyncSession = Depends(get_db), ): user_id = get_current_user_id_from_context() template = await SmartDocumentTemplateService.create( db, name=data.name, content=data.content, description=data.description, icon=data.icon, category=data.category, preview_image=data.preview_image, user_id=user_id, ) return DocumentTemplateResponse( id=template.id, name=template.name, description=template.description, icon=template.icon, category=template.category, preview_image=template.preview_image, is_system=template.is_system, use_count=template.use_count, sys_create_datetime=template.sys_create_datetime, sys_update_datetime=template.sys_update_datetime, sys_creator_id=template.sys_creator_id, ) @router.post("/document-templates/from-document/{document_id}", response_model=DocumentTemplateResponse, summary="从文档创建模板") async def create_template_from_document( document_id: str, data: DocumentTemplateCreate, db: AsyncSession = Depends(get_db), ): await PermissionService.require_table_access(db, document_id) user_id = get_current_user_id_from_context() template = await SmartDocumentTemplateService.create_from_document( db, document_id=document_id, name=data.name, description=data.description, category=data.category, user_id=user_id, ) if not template: raise HTTPException(status_code=400, detail="文档不存在或内容为空") return DocumentTemplateResponse( id=template.id, name=template.name, description=template.description, icon=template.icon, category=template.category, preview_image=template.preview_image, is_system=template.is_system, use_count=template.use_count, sys_create_datetime=template.sys_create_datetime, sys_update_datetime=template.sys_update_datetime, sys_creator_id=template.sys_creator_id, ) @router.put("/document-templates/{template_id}", response_model=DocumentTemplateResponse, summary="更新模板") async def update_document_template( template_id: str, data: DocumentTemplateUpdate, db: AsyncSession = Depends(get_db), ): user_id = get_current_user_id_from_context() update_data = data.model_dump(exclude_unset=True) template = await SmartDocumentTemplateService.update(db, template_id, update_data, user_id) if not template: raise HTTPException(status_code=404, detail="模板不存在") return DocumentTemplateResponse( id=template.id, name=template.name, description=template.description, icon=template.icon, category=template.category, preview_image=template.preview_image, is_system=template.is_system, use_count=template.use_count, sys_create_datetime=template.sys_create_datetime, sys_update_datetime=template.sys_update_datetime, sys_creator_id=template.sys_creator_id, ) @router.delete("/document-templates/{template_id}", response_model=ResponseModel, summary="删除模板") async def delete_document_template( template_id: str, db: AsyncSession = Depends(get_db), ): ok = await SmartDocumentTemplateService.delete(db, template_id) if not ok: raise HTTPException(status_code=404, detail="模板不存在或为系统模板不可删除") return ResponseModel(message="删除成功") @router.post("/document-templates/{template_id}/use", response_model=ResponseModel, summary="使用模板(增加使用计数)") async def use_document_template( template_id: str, db: AsyncSession = Depends(get_db), ): template = await SmartDocumentTemplateService.get_by_id(db, template_id) if not template: raise HTTPException(status_code=404, detail="模板不存在") await SmartDocumentTemplateService.increment_use_count(db, template_id) return ResponseModel(message="OK") # ==================== Wiki Space ==================== @router.post("/wiki-spaces", response_model=WikiSpaceResponse, summary="创建文档库") async def create_wiki_space(data: WikiSpaceCreate, db: AsyncSession = Depends(get_db)): user_id = get_current_user_id_from_context() space = await WikiSpaceService.create(db, data, current_user_id=user_id) creator_name = None if user_id: user = await UserService.get_by_id(db, user_id) if user: creator_name = getattr(user, "real_name", None) or getattr(user, "username", None) resp = WikiSpaceResponse.model_validate(space) resp.creator_name = creator_name return resp @router.get("/wiki-spaces", response_model=List[WikiSpaceListItem], summary="获取文档库列表") async def get_wiki_spaces(db: AsyncSession = Depends(get_db)): user_info = get_current_user_info_from_context() user_id = user_info.get("user_id", "") if user_info else None is_superuser = user_info.get("is_superuser", False) if user_info else False items, _ = await WikiSpaceService.get_list_with_count( db, user_id=user_id, is_superuser=is_superuser, ) result = [] for item in items: space = item["space"] creator_name = None if space.sys_creator_id: user = await UserService.get_by_id(db, space.sys_creator_id) if user: creator_name = getattr(user, "real_name", None) or getattr(user, "username", None) resp = WikiSpaceListItem( **WikiSpaceResponse.model_validate(space).model_dump(), document_count=item["document_count"], ) resp.creator_name = creator_name result.append(resp) return result @router.get("/wiki-spaces/{space_id}", response_model=WikiSpaceDetail, summary="获取文档库详情") async def get_wiki_space_detail(space_id: str, db: AsyncSession = Depends(get_db)): space = await WikiSpaceService.get_by_id(db, space_id) if not space: raise HTTPException(status_code=404, detail="文档库不存在") user_info = get_current_user_info_from_context() user_id = user_info.get("user_id", "") if user_info else None dept_id = user_info.get("dept_id") if user_info else None role_ids = user_info.get("role_ids", []) if user_info else [] is_superuser = user_info.get("is_superuser", False) if user_info else False has_access = await WikiSpaceService.check_space_access( space, user_id=user_id, is_superuser=is_superuser, ) if not has_access: raise HTTPException(status_code=403, detail="无权访问此文档库") documents = await WikiSpaceService.get_documents( db, space_id, user_id=user_id, dept_id=dept_id, role_ids=role_ids, is_superuser=is_superuser, ) from zq_smart_table.schema import SmartTableSimple doc_list = [SmartTableSimple.model_validate(d) for d in documents] creator_name = None if space.sys_creator_id: user = await UserService.get_by_id(db, space.sys_creator_id) if user: creator_name = getattr(user, "real_name", None) or getattr(user, "username", None) resp = WikiSpaceDetail( **WikiSpaceResponse.model_validate(space).model_dump(), documents=doc_list, ) resp.creator_name = creator_name return resp @router.put("/wiki-spaces/{space_id}", response_model=WikiSpaceResponse, summary="更新文档库") async def update_wiki_space( space_id: str, data: WikiSpaceUpdate, db: AsyncSession = Depends(get_db), ): user_info = get_current_user_info_from_context() user_id = user_info.get("user_id", "") if user_info else None is_superuser = user_info.get("is_superuser", False) if user_info else False space = await WikiSpaceService.get_by_id(db, space_id) if not space: raise HTTPException(status_code=404, detail="文档库不存在") if not is_superuser and space.sys_creator_id != user_id: raise HTTPException(status_code=403, detail="仅创建者可修改文档库") space = await WikiSpaceService.update(db, space_id, data, current_user_id=user_id) return WikiSpaceResponse.model_validate(space) @router.delete("/wiki-spaces/{space_id}", response_model=ResponseModel, summary="删除文档库") async def delete_wiki_space(space_id: str, db: AsyncSession = Depends(get_db)): user_info = get_current_user_info_from_context() user_id = user_info.get("user_id", "") if user_info else None is_superuser = user_info.get("is_superuser", False) if user_info else False space = await WikiSpaceService.get_by_id(db, space_id) if not space: raise HTTPException(status_code=404, detail="文档库不存在") if not is_superuser and space.sys_creator_id != user_id: raise HTTPException(status_code=403, detail="仅创建者可删除文档库") ok = await WikiSpaceService.delete_with_documents(db, space_id) if not ok: raise HTTPException(status_code=404, detail="文档库不存在") return ResponseModel(message="删除成功") @router.get("/wiki-spaces/{space_id}/documents", response_model=List[SmartTableSimple], summary="获取文档库文档列表") async def get_wiki_space_documents(space_id: str, db: AsyncSession = Depends(get_db)): space = await WikiSpaceService.get_by_id(db, space_id) if not space: raise HTTPException(status_code=404, detail="文档库不存在") user_info = get_current_user_info_from_context() user_id = user_info.get("user_id", "") if user_info else None dept_id = user_info.get("dept_id") if user_info else None role_ids = user_info.get("role_ids", []) if user_info else [] is_superuser = user_info.get("is_superuser", False) if user_info else False has_access = await WikiSpaceService.check_space_access( space, user_id=user_id, is_superuser=is_superuser, ) if not has_access: raise HTTPException(status_code=403, detail="无权访问此文档库") documents = await WikiSpaceService.get_documents( db, space_id, user_id=user_id, dept_id=dept_id, role_ids=role_ids, is_superuser=is_superuser, ) return [SmartTableSimple.model_validate(d) for d in documents] @router.post("/wiki-spaces/{space_id}/documents", response_model=SmartTableResponse, summary="在文档库中创建文档") async def create_wiki_document( space_id: str, data: SmartTableCreate, db: AsyncSession = Depends(get_db), ): space = await WikiSpaceService.get_by_id(db, space_id) if not space: raise HTTPException(status_code=404, detail="文档库不存在") user_info = get_current_user_info_from_context() user_id = user_info.get("user_id", "") if user_info else None is_superuser = user_info.get("is_superuser", False) if user_info else False has_access = await WikiSpaceService.check_space_access( space, user_id=user_id, is_superuser=is_superuser, ) if not has_access: raise HTTPException(status_code=403, detail="无权在此文档库中创建文档") doc = await WikiSpaceService.add_document( db, space_id, name=data.name, parent_id=data.parent_id, content=data.content, user_id=user_id, ) if user_id: await PermissionService.init_table_owner(db, doc.id, user_id) return SmartTableResponse.model_validate(doc)