1646 lines
68 KiB
Python
1646 lines
68 KiB
Python
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"<h1>{title}</h1>\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)
|