feat: restore source parity and harden agent runtime

This commit is contained in:
2026-06-22 11:17:26 +08:00
parent e33f08277b
commit 0793eb82d6
596 changed files with 168879 additions and 290 deletions
@@ -0,0 +1,393 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
表单管理 API(异步版本)
表单元数据的 CRUD、发布、复制、导入导出
"""
import json
import logging
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.base_schema import PaginatedResponse, ResponseModel
from online_dev.form_manager.schema import (
FormImportCheckIn,
FormImportCheckOut,
FormImportIn,
FormValidateTablesIn,
FormValidateTablesOut,
FormMetaCreateIn,
FormMetaListOut,
FormMetaOut,
FormMetaUpdateIn,
FormPublishIn,
FormSubTableOut,
)
from online_dev.form_manager.service import FormService, FormServiceException
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/form", tags=["表单管理"])
# ============ 辅助函数 ============
def _format_datetime(dt) -> str:
"""格式化日期时间"""
if dt:
return dt.strftime("%Y-%m-%d %H:%M:%S")
return ""
async def _build_form_out(db: AsyncSession, form) -> dict:
"""构建表单详情输出"""
sub_tables = await FormService.get_sub_tables(db, form.id)
return {
"id": str(form.id),
"application_id": form.application_id,
"name": form.name,
"code": form.code,
"form_type": form.form_type,
"description": form.description or "",
"status": form.status,
"version": form.version,
"db_config": form.db_config,
"main_table": form.main_table,
"main_table_schema": form.main_table_schema or "",
"main_table_database": form.main_table_database or "",
"form_config": form.form_config or {},
"list_config": form.list_config or {},
"sort": form.sort or 0,
"show_in_mobile": form.show_in_mobile or False,
"globally_visible": form.globally_visible or False,
"icon": form.icon or "",
"icon_bg_color": form.icon_bg_color or "",
"sys_create_datetime": _format_datetime(form.sys_create_datetime),
"sys_update_datetime": _format_datetime(form.sys_update_datetime),
"sub_tables": [
{
"id": str(sub.id),
"table_name": sub.table_name,
"table_schema": sub.table_schema or "",
"table_database": sub.table_database or "",
"alias": sub.alias or "",
"foreign_key": sub.foreign_key,
"related_field": sub.related_field or "id",
"relation_type": sub.relation_type or "one-to-many",
"sort": sub.sort or 0,
}
for sub in sub_tables
],
}
def _build_form_list_out(form, application_name: str = None, application_code: str = "") -> dict:
"""构建表单列表输出"""
return {
"id": str(form.id),
"application_id": form.application_id,
"application_name": application_name or "主应用",
"application_code": application_code or "",
"name": form.name,
"code": form.code,
"form_type": form.form_type,
"description": form.description or "",
"status": form.status,
"version": form.version,
"main_table": form.main_table,
"sort": form.sort or 0,
"show_in_mobile": form.show_in_mobile or False,
"globally_visible": form.globally_visible or False,
"icon": form.icon or "",
"icon_bg_color": form.icon_bg_color or "",
"sys_create_datetime": _format_datetime(form.sys_create_datetime),
"sys_update_datetime": _format_datetime(form.sys_update_datetime),
}
# ============ 表单元数据 CRUD ============
@router.get("/list", response_model=PaginatedResponse[FormMetaListOut], summary="表单列表")
async def list_forms(
application_id: str = Query(None, alias="applicationId", description="所属应用ID"),
name: str = Query(None, description="表单名称"),
code: str = Query(None, description="表单编码"),
form_type: str = Query(None, alias="formType", description="表单类型"),
status: str = Query(None, description="状态"),
include_globally_visible: bool = Query(
False, alias="includeGloballyVisible", description="是否包含其他应用全局可见的表单"
),
page: int = Query(default=1, ge=1, description="页码"),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize", description="每页数量"),
db: AsyncSession = Depends(get_db),
):
"""分页查询表单列表(自动应用数据权限)"""
result = await FormService.list_with_data_scope(
db=db,
page=page,
page_size=page_size,
application_id=application_id,
name=name,
code=code,
form_type=form_type,
status=status,
include_globally_visible=include_globally_visible,
)
return PaginatedResponse(
items=[_build_form_list_out(
item,
getattr(item, 'application_name', '主应用'),
getattr(item, 'application_code', ''),
) for item in result["items"]],
total=result["total"],
)
@router.get("/form-types", summary="获取表单类型列表")
async def get_form_types():
"""获取所有表单类型"""
return FormService.get_form_types()
@router.get("/published/simple", summary="获取已发布表单简单列表")
async def get_published_forms_simple(
application_id: str = Query(None, alias="applicationId", description="所属应用ID"),
all_apps: bool = Query(False, alias="allApps", description="是否返回所有应用的表单(移动端工作台使用)"),
db: AsyncSession = Depends(get_db),
):
"""
获取已发布表单的简单列表(用于下拉选择)
返回格式: [{code, name, mainTable, application_id, application_name, fields: [{field, label, type}]}]
"""
try:
return await FormService.get_published_forms_simple(db, application_id=application_id, all_apps=all_apps)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/{form_id}", response_model=FormMetaOut, summary="表单详情")
async def get_form(
form_id: str,
db: AsyncSession = Depends(get_db),
):
"""获取表单详情"""
try:
form = await FormService.get(db, form_id)
return await _build_form_out(db, form)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/code/{code}", response_model=FormMetaOut, summary="根据编码获取表单")
async def get_form_by_code(
code: str,
db: AsyncSession = Depends(get_db),
):
"""根据编码获取表单详情"""
try:
form = await FormService.get_by_code(db, code)
return await _build_form_out(db, form)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("", response_model=FormMetaOut, summary="创建表单")
async def create_form(
request: Request,
data: FormMetaCreateIn,
db: AsyncSession = Depends(get_db),
):
"""创建表单"""
user_id = request.state.user_id
try:
form = await FormService.create(db, data.model_dump(), user_id)
return await _build_form_out(db, form)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{form_id}", response_model=FormMetaOut, summary="更新表单")
async def update_form(
request: Request,
form_id: str,
data: FormMetaUpdateIn,
db: AsyncSession = Depends(get_db),
):
"""更新表单"""
user_id = request.state.user_id
try:
form = await FormService.update(db, form_id, data.model_dump(exclude_none=True), user_id)
return await _build_form_out(db, form)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/batch/delete", response_model=dict, summary="批量删除表单")
async def batch_delete_forms(
ids: List[str] = Query(..., description="表单ID列表"),
db: AsyncSession = Depends(get_db),
):
"""批量删除表单"""
count = await FormService.batch_delete(db, ids)
return {"count": count}
@router.delete("/{form_id}", response_model=FormMetaOut, summary="删除表单")
async def delete_form(
form_id: str,
db: AsyncSession = Depends(get_db),
):
"""删除表单"""
try:
form = await FormService.get(db, form_id)
form_out = await _build_form_out(db, form)
await FormService.delete(db, form_id)
return form_out
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
# ============ 发布/取消发布 ============
@router.post("/{form_id}/publish", response_model=FormMetaOut, summary="发布表单")
async def publish_form(
form_id: str,
data: FormPublishIn,
db: AsyncSession = Depends(get_db),
):
"""发布表单并创建菜单"""
try:
form = await FormService.publish(db, form_id, data.model_dump())
return await _build_form_out(db, form)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{form_id}/unpublish", response_model=FormMetaOut, summary="取消发布")
async def unpublish_form(
form_id: str,
db: AsyncSession = Depends(get_db),
):
"""取消发布表单并删除菜单"""
try:
form = await FormService.unpublish(db, form_id)
return await _build_form_out(db, form)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
# ============ 复制 ============
@router.post("/{form_id}/copy", response_model=FormMetaOut, summary="复制表单")
async def copy_form(
request: Request,
form_id: str,
new_code: str = Query(..., alias="new_code", description="新表单编码"),
new_name: str = Query(None, alias="new_name", description="新表单名称"),
db: AsyncSession = Depends(get_db),
):
"""复制表单"""
user_id = request.state.user_id
try:
form = await FormService.copy(db, form_id, new_code, new_name, user_id)
return await _build_form_out(db, form)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
# ============ 导入/导出配置 ============
@router.get("/{form_id}/export", summary="导出表单配置")
async def export_form_config(
form_id: str,
db: AsyncSession = Depends(get_db),
):
"""导出表单配置为 JSON"""
try:
config = await FormService.export_config(db, form_id)
# 返回 JSON 文件
content = json.dumps(config, ensure_ascii=False, indent=2)
return StreamingResponse(
iter([content]),
media_type="application/json",
headers={
"Content-Disposition": f'attachment; filename="{config["code"]}.json"'
}
)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/batch/export", summary="批量导出表单配置")
async def batch_export_form_config(
ids: List[str],
db: AsyncSession = Depends(get_db),
):
"""批量导出多个表单配置为 JSON 数组"""
try:
configs = []
for form_id in ids:
config = await FormService.export_config(db, form_id)
configs.append(config)
content = json.dumps(configs, ensure_ascii=False, indent=2)
return StreamingResponse(
iter([content]),
media_type="application/json",
headers={
"Content-Disposition": 'attachment; filename="forms_export.json"'
}
)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/validate-tables", response_model=FormValidateTablesOut, summary="校验表单物理表")
async def validate_form_tables(
data: FormValidateTablesIn,
db: AsyncSession = Depends(get_db),
):
"""校验目标连接可用且主/子表存在(保存/发布前)"""
try:
return await FormService.validate_form_tables(db, data.model_dump())
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/import/check", response_model=FormImportCheckOut, summary="导入预检查")
async def check_import_form_config(
data: FormImportCheckIn,
db: AsyncSession = Depends(get_db),
):
"""检查导入配置:编码冲突、目标表是否存在"""
try:
result = await FormService.check_import(db, data.model_dump())
return result
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/import", response_model=FormMetaOut, summary="导入表单配置")
async def import_form_config(
request: Request,
data: FormImportIn,
db: AsyncSession = Depends(get_db),
):
"""导入表单配置(支持自动建表)"""
user_id = request.state.user_id
try:
form = await FormService.import_config(db, data.model_dump(), user_id)
return await _build_form_out(db, form)
except FormServiceException as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -0,0 +1,259 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
表单管理 Schema 定义
"""
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field, ConfigDict
# ============ 表单元数据 Schema ============
class FormMetaBase(BaseModel):
"""表单基础信息"""
application_id: Optional[str] = Field(None, description="所属应用ID")
name: str = Field(..., description="表单名称")
code: str = Field(..., pattern=r"^[a-zA-Z][a-zA-Z0-9_]*$", description="表单编码(字母开头,只能包含字母、数字和下划线)")
form_type: str = Field("normal", description="表单类型: normal-普通表单, workflow-流程表单")
description: str = Field("", description="描述")
sort: int = Field(0, description="排序")
show_in_mobile: bool = Field(False, description="是否在移动端显示")
globally_visible: bool = Field(False, description="是否全局可见(供其他应用引用)")
icon: str = Field("", description="图标")
icon_bg_color: str = Field("", description="图标背景色")
class FormSubTableSchema(BaseModel):
"""子表关联配置"""
table_name: str = Field(..., description="从表名")
table_schema: str = Field("", description="从表Schema")
table_database: str = Field("", description="从表数据库")
alias: str = Field("", description="别名")
foreign_key: str = Field(..., description="外键字段")
related_field: str = Field("id", description="关联主表字段")
relation_type: str = Field("one-to-many", description="关联类型")
sort: int = Field(0, description="排序")
class FormMetaCreateIn(FormMetaBase):
"""创建表单请求"""
db_config: str = Field(..., description="数据库配置名")
main_table: str = Field(..., description="主表名")
main_table_schema: str = Field("", description="主表Schema")
main_table_database: str = Field("", description="主表数据库")
form_config: Dict[str, Any] = Field(default_factory=dict, description="表单设计配置")
list_config: Dict[str, Any] = Field(default_factory=dict, description="列表设计配置")
sub_tables: List[FormSubTableSchema] = Field(default_factory=list, description="子表配置")
class FormMetaUpdateIn(BaseModel):
"""更新表单请求"""
name: Optional[str] = Field(None, description="表单名称")
form_type: Optional[str] = Field(None, description="表单类型")
description: Optional[str] = Field(None, description="描述")
sort: Optional[int] = Field(None, description="排序")
show_in_mobile: Optional[bool] = Field(None, description="是否在移动端显示")
globally_visible: Optional[bool] = Field(None, description="是否全局可见(供其他应用引用)")
icon: Optional[str] = Field(None, description="图标")
icon_bg_color: Optional[str] = Field(None, description="图标背景色")
db_config: Optional[str] = Field(None, description="数据库配置名")
main_table: Optional[str] = Field(None, description="主表名")
main_table_schema: Optional[str] = Field(None, description="主表Schema")
main_table_database: Optional[str] = Field(None, description="主表数据库")
form_config: Optional[Dict[str, Any]] = Field(None, description="表单设计配置")
list_config: Optional[Dict[str, Any]] = Field(None, description="列表设计配置")
sub_tables: Optional[List[FormSubTableSchema]] = Field(None, description="子表配置")
class FormSubTableOut(BaseModel):
"""子表关联输出"""
id: str
table_name: str
table_schema: str = ""
table_database: str = ""
alias: str = ""
foreign_key: str
related_field: str = "id"
relation_type: str = "one-to-many"
sort: int = 0
model_config = ConfigDict(from_attributes=True)
class FormMetaOut(BaseModel):
"""表单详情输出"""
id: str
name: str
code: str
form_type: str
description: str = ""
status: str
version: int
db_config: str
main_table: str
main_table_schema: str = ""
main_table_database: str = ""
show_in_mobile: bool = False
globally_visible: bool = False
icon: str = ""
icon_bg_color: str = ""
form_config: Dict[str, Any] = {}
list_config: Dict[str, Any] = {}
sort: int = 0
sys_create_datetime: Optional[str] = None
sys_update_datetime: Optional[str] = None
sub_tables: List[FormSubTableOut] = []
model_config = ConfigDict(from_attributes=True)
class FormMetaListOut(BaseModel):
"""表单列表输出"""
id: str
application_id: Optional[str] = None
application_name: str = "主应用"
application_code: str = ""
name: str
code: str
form_type: str
description: str = ""
status: str
version: int
main_table: str
show_in_mobile: bool = False
globally_visible: bool = False
icon: str = ""
icon_bg_color: str = ""
sort: int = 0
sys_create_datetime: Optional[str] = None
sys_update_datetime: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
# ============ 导入导出 Schema ============
class TableDDLSchema(BaseModel):
"""表 DDL 信息"""
main_table: str = Field("", description="主表 DDL")
sub_tables: Dict[str, str] = Field(default_factory=dict, description="子表 DDLkey 为表名")
class FormExportOut(BaseModel):
"""表单配置导出"""
name: str
code: str
form_type: str
description: str = ""
globally_visible: bool = False
show_in_mobile: bool = False
db_config: str
main_table: str
main_table_schema: str = ""
main_table_database: str = ""
form_config: Dict[str, Any] = {}
list_config: Dict[str, Any] = {}
sub_tables: List[FormSubTableSchema] = []
table_ddl: Optional[TableDDLSchema] = None
class TableRenameMapping(BaseModel):
"""表重命名映射"""
original_name: str = Field(..., description="原始表名")
new_name: str = Field(..., description="新表名")
new_schema: Optional[str] = Field(None, description="新Schema")
class FormImportIn(BaseModel):
"""表单配置导入"""
application_id: Optional[str] = Field(None, description="所属应用ID")
name: str = Field(..., description="表单名称")
code: str = Field(..., description="表单编码")
form_type: str = Field("normal", description="表单类型")
description: str = Field("", description="描述")
db_config: str = Field(..., description="数据库配置名")
main_table: str = Field(..., description="主表名")
main_table_schema: str = Field("", description="主表Schema")
main_table_database: str = Field("", description="主表数据库")
form_config: Dict[str, Any] = Field(default_factory=dict, description="表单设计配置")
list_config: Dict[str, Any] = Field(default_factory=dict, description="列表设计配置")
show_in_mobile: bool = Field(False, description="是否在移动端显示")
globally_visible: bool = Field(False, description="是否全局可见(供其他应用引用)")
sub_tables: List[FormSubTableSchema] = Field(default_factory=list, description="子表配置")
table_ddl: Optional[TableDDLSchema] = Field(None, description="表 DDL(用于自动建表)")
auto_create_tables: bool = Field(False, description="是否自动创建不存在的表")
create_schema_if_not_exists: bool = Field(
False, description="目标 Schema 不存在时是否自动创建(PostgreSQL/SQL Server"
)
table_rename_mappings: List[TableRenameMapping] = Field(default_factory=list, description="表重命名映射列表")
class TableCheckResult(BaseModel):
"""单表检查结果"""
table_name: str = Field(..., description="表名")
schema_name: str = Field("", description="Schema名")
exists: bool = Field(..., description="表是否存在")
has_ddl: bool = Field(False, description="导入数据中是否包含该表的 DDL")
class FormImportCheckIn(BaseModel):
"""导入预检查请求"""
code: str = Field(..., description="表单编码")
db_config: str = Field("default", description="数据库配置名")
main_table: str = Field(..., description="主表名")
main_table_schema: str = Field("", description="主表 Schema")
main_table_database: str = Field("", description="主表数据库")
sub_tables: List[FormSubTableSchema] = Field(default_factory=list, description="子表配置")
table_ddl: Optional[TableDDLSchema] = Field(None, description="表 DDL")
class FormImportCheckOut(BaseModel):
"""导入预检查结果"""
code_exists: bool = Field(..., description="表单编码是否已存在")
main_table_check: TableCheckResult = Field(..., description="主表检查结果")
sub_table_checks: List[TableCheckResult] = Field(default_factory=list, description="子表检查结果")
can_import: bool = Field(..., description="是否可以直接导入(所有表都存在且编码不冲突)")
available_schemas: List[str] = Field(default_factory=list, description="可用的Schema列表")
target_db_type: str = Field("", description="目标连接数据库类型")
class FormValidateTablesIn(BaseModel):
"""表单物理表校验请求"""
db_config: str = Field("default", description="数据库连接 code")
main_table: str = Field("", description="主表名")
main_table_schema: str = Field("", description="主表 Schema")
main_table_database: str = Field("", description="主表数据库")
sub_tables: List[FormSubTableSchema] = Field(default_factory=list, description="子表配置")
class FormValidateTablesOut(BaseModel):
"""表单物理表校验结果"""
valid: bool = Field(..., description="是否全部通过")
connection_ok: bool = Field(True, description="连接是否可用")
connection_message: str = Field("", description="连接错误信息")
db_config: str = Field("default", description="数据库连接 code")
main_table_exists: bool = Field(False, description="主表是否存在")
sub_table_checks: List[TableCheckResult] = Field(default_factory=list, description="子表检查")
database_warnings: List[str] = Field(
default_factory=list,
description="库名/连接配置风险提示(不阻断 valid,供前端展示)",
)
# ============ 发布配置 Schema ============
class FormPublishIn(BaseModel):
"""发布表单请求(含菜单配置)"""
menu_name: str = Field(..., description="菜单名称")
menu_parent_id: Optional[str] = Field(None, description="上级菜单ID")
menu_icon: str = Field("lucide:file-text", description="菜单图标")
menu_order: int = Field(0, description="菜单排序")
# 功能开关
allow_add: bool = Field(True, description="允许新增")
allow_edit: bool = Field(True, description="允许编辑")
allow_delete: bool = Field(True, description="允许删除")
allow_export: bool = Field(True, description="允许导出")
allow_import: bool = Field(False, description="允许导入")