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,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
电子签章模块
"""
@@ -0,0 +1,162 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
电子签章API接口
"""
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query, Request
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 online_dev.electronic_seal.schema import (
ElectronicSealCreate, ElectronicSealUpdate, ElectronicSealOut, SealUsageLogOut
)
from online_dev.electronic_seal.service import ElectronicSealService, SealUsageLogService
router = APIRouter(prefix="/electronic-seal", tags=["电子签章"])
@router.get("", response_model=PaginatedResponse[ElectronicSealOut], summary="获取签章列表")
async def get_seal_list(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize"),
seal_type: Optional[str] = Query(default=None, alias="sealType"),
status: Optional[str] = Query(default=None),
keyword: Optional[str] = Query(default=None),
db: AsyncSession = Depends(get_db),
):
"""获取签章列表"""
items, total = await ElectronicSealService.get_list(
db,
page=page,
page_size=page_size,
seal_type=seal_type,
status=status,
keyword=keyword,
)
return PaginatedResponse(items=items, total=total)
@router.get("/available", response_model=List[ElectronicSealOut], summary="获取可用签章")
async def get_available_seals(
template_id: Optional[str] = Query(default=None, alias="templateId"),
db: AsyncSession = Depends(get_db),
request: Request = None,
):
"""获取当前用户可用的签章"""
# 从请求中获取用户信息
user_id = getattr(request.state, "user_id", "") if request else ""
dept_id = getattr(request.state, "dept_id", None) if request else None
role_ids = getattr(request.state, "role_ids", None) if request else None
seals = await ElectronicSealService.get_available_seals(
db,
user_id=user_id,
dept_id=dept_id,
role_ids=role_ids,
template_id=template_id,
)
return seals
@router.post("", response_model=ElectronicSealOut, summary="创建签章")
async def create_seal(
data: ElectronicSealCreate,
db: AsyncSession = Depends(get_db),
):
"""创建电子签章"""
seal = await ElectronicSealService.create(db, data)
return seal
@router.get("/{seal_id}", response_model=ElectronicSealOut, summary="获取签章详情")
async def get_seal(
seal_id: str,
db: AsyncSession = Depends(get_db),
):
"""获取签章详情"""
seal = await ElectronicSealService.get_by_id(db, seal_id)
if not seal:
raise HTTPException(status_code=404, detail="签章不存在")
return seal
@router.put("/{seal_id}", response_model=ElectronicSealOut, summary="更新签章")
async def update_seal(
seal_id: str,
data: ElectronicSealUpdate,
db: AsyncSession = Depends(get_db),
):
"""更新签章"""
seal = await ElectronicSealService.update(db, seal_id, data)
if not seal:
raise HTTPException(status_code=404, detail="签章不存在")
return seal
@router.delete("/{seal_id}", response_model=ResponseModel, summary="删除签章")
async def delete_seal(
seal_id: str,
db: AsyncSession = Depends(get_db),
):
"""删除签章"""
seal = await ElectronicSealService.get_by_id(db, seal_id)
if not seal:
raise HTTPException(status_code=404, detail="签章不存在")
await ElectronicSealService.delete(db, seal_id)
return ResponseModel(message="删除成功")
@router.post("/{seal_id}/enable", response_model=ElectronicSealOut, summary="启用签章")
async def enable_seal(
seal_id: str,
db: AsyncSession = Depends(get_db),
):
"""启用签章"""
seal = await ElectronicSealService.get_by_id(db, seal_id)
if not seal:
raise HTTPException(status_code=404, detail="签章不存在")
seal = await ElectronicSealService.update(
db, seal_id, ElectronicSealUpdate(status="active")
)
return seal
@router.post("/{seal_id}/disable", response_model=ElectronicSealOut, summary="禁用签章")
async def disable_seal(
seal_id: str,
db: AsyncSession = Depends(get_db),
):
"""禁用签章"""
seal = await ElectronicSealService.get_by_id(db, seal_id)
if not seal:
raise HTTPException(status_code=404, detail="签章不存在")
seal = await ElectronicSealService.update(
db, seal_id, ElectronicSealUpdate(status="disabled")
)
return seal
@router.get("/{seal_id}/logs", response_model=PaginatedResponse[SealUsageLogOut], summary="获取签章使用记录")
async def get_seal_usage_logs(
seal_id: str,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize"),
db: AsyncSession = Depends(get_db),
):
"""获取签章的使用记录"""
seal = await ElectronicSealService.get_by_id(db, seal_id)
if not seal:
raise HTTPException(status_code=404, detail="签章不存在")
items, total = await SealUsageLogService.get_logs_by_seal(
db, seal_id, page=page, page_size=page_size
)
return PaginatedResponse(items=items, total=total)
@@ -0,0 +1,68 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
电子签章数据模型
"""
from sqlalchemy import Column, String, Text, Integer, Boolean, Index, JSON
from app.base_model import BaseModel
class ElectronicSeal(BaseModel):
"""电子签章配置"""
__tablename__ = "electronic_seal"
# 基础信息
name = Column(String(100), nullable=False, comment="签章名称")
seal_type = Column(String(20), nullable=False, comment="类型: company/department/personal/contract/finance")
description = Column(Text, default="", comment="描述")
# 签章图片
seal_image_id = Column(String(36), nullable=False, comment="签章图片文件ID")
# 使用权限
owner_type = Column(String(20), default="all", comment="所有者类型: all/dept/role/user")
owner_ids = Column(JSON, nullable=True, comment="所有者ID列表")
# 使用范围
scope = Column(String(20), default="all", comment="使用范围: all/specific")
allowed_template_ids = Column(JSON, nullable=True, comment="允许使用的模板ID列表")
# 签章属性
width = Column(Integer, default=120, comment="宽度(像素)")
height = Column(Integer, default=120, comment="高度(像素)")
# 状态
status = Column(String(20), default="active", comment="状态: active/disabled")
__table_args__ = (
Index("ix_electronic_seal_seal_type", "seal_type"),
Index("ix_electronic_seal_status", "status"),
)
class SealUsageLog(BaseModel):
"""签章使用记录"""
__tablename__ = "seal_usage_log"
# 关联签章
seal_id = Column(String(36), nullable=False, index=True, comment="签章ID")
seal_name = Column(String(100), nullable=True, comment="签章名称")
# 关联文档
document_id = Column(String(36), nullable=False, index=True, comment="文档ID")
document_name = Column(String(200), nullable=True, comment="文档名称")
# 使用信息
user_id = Column(String(36), nullable=False, comment="使用人ID")
user_name = Column(String(100), nullable=True, comment="使用人姓名")
# 位置信息
position_x = Column(Integer, default=0, comment="X坐标")
position_y = Column(Integer, default=0, comment="Y坐标")
page_number = Column(Integer, default=1, comment="页码")
__table_args__ = (
Index("ix_seal_usage_log_seal_id", "seal_id"),
Index("ix_seal_usage_log_document_id", "document_id"),
)
@@ -0,0 +1,79 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
电子签章Schema定义
"""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, ConfigDict, Field
class ElectronicSealBase(BaseModel):
"""电子签章基础Schema"""
name: str = Field(..., min_length=1, max_length=100, description="签章名称")
seal_type: str = Field(..., description="类型: company/department/personal/contract/finance")
description: Optional[str] = Field(default="", description="描述")
seal_image_id: str = Field(..., description="签章图片文件ID")
owner_type: str = Field(default="all", description="所有者类型: all/dept/role/user")
owner_ids: Optional[List[str]] = Field(default=None, description="所有者ID列表")
scope: str = Field(default="all", description="使用范围: all/specific")
allowed_template_ids: Optional[List[str]] = Field(default=None, description="允许使用的模板ID列表")
width: int = Field(default=120, ge=20, le=500, description="宽度(像素)")
height: int = Field(default=120, ge=20, le=500, description="高度(像素)")
class ElectronicSealCreate(ElectronicSealBase):
"""创建电子签章"""
pass
class ElectronicSealUpdate(BaseModel):
"""更新电子签章"""
name: Optional[str] = Field(default=None, max_length=100, description="签章名称")
description: Optional[str] = Field(default=None, description="描述")
seal_image_id: Optional[str] = Field(default=None, description="签章图片文件ID")
owner_type: Optional[str] = Field(default=None, description="所有者类型")
owner_ids: Optional[List[str]] = Field(default=None, description="所有者ID列表")
scope: Optional[str] = Field(default=None, description="使用范围")
allowed_template_ids: Optional[List[str]] = Field(default=None, description="允许使用的模板ID列表")
width: Optional[int] = Field(default=None, description="宽度")
height: Optional[int] = Field(default=None, description="高度")
status: Optional[str] = Field(default=None, description="状态")
class ElectronicSealOut(ElectronicSealBase):
"""电子签章输出"""
id: str
status: str = "active"
seal_image_url: Optional[str] = None
sys_create_datetime: Optional[datetime] = None
sys_update_datetime: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
class SealUsageLogOut(BaseModel):
"""签章使用记录输出"""
id: str
seal_id: str
seal_name: Optional[str] = None
document_id: str
document_name: Optional[str] = None
user_id: str
user_name: Optional[str] = None
position_x: int = 0
position_y: int = 0
page_number: int = 1
sys_create_datetime: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,195 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
电子签章服务层
"""
from typing import Optional, List, Tuple
from sqlalchemy import select, func, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from app.base_service import BaseService
from online_dev.electronic_seal.model import ElectronicSeal, SealUsageLog
from online_dev.electronic_seal.schema import ElectronicSealCreate, ElectronicSealUpdate
class ElectronicSealService(BaseService[ElectronicSeal, ElectronicSealCreate, ElectronicSealUpdate]):
"""电子签章服务"""
model = ElectronicSeal
@classmethod
async def get_list(
cls,
db: AsyncSession,
page: int = 1,
page_size: int = 20,
seal_type: Optional[str] = None,
status: Optional[str] = None,
keyword: Optional[str] = None,
) -> Tuple[List[ElectronicSeal], int]:
"""获取签章列表"""
conditions = [cls.model.is_deleted == False]
if seal_type:
conditions.append(cls.model.seal_type == seal_type)
if status:
conditions.append(cls.model.status == status)
if keyword:
conditions.append(
or_(
cls.model.name.ilike(f"%{keyword}%"),
cls.model.description.ilike(f"%{keyword}%"),
)
)
# 查询总数
count_stmt = select(func.count()).select_from(cls.model).where(and_(*conditions))
total = (await db.execute(count_stmt)).scalar() or 0
# 查询列表
stmt = (
select(cls.model)
.where(and_(*conditions))
.order_by(cls.model.sys_create_datetime.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@classmethod
async def get_available_seals(
cls,
db: AsyncSession,
user_id: str,
dept_id: Optional[str] = None,
role_ids: Optional[List[str]] = None,
template_id: Optional[str] = None,
) -> List[ElectronicSeal]:
"""获取用户可用的签章"""
conditions = [
cls.model.is_deleted == False,
cls.model.status == "active",
]
stmt = select(cls.model).where(and_(*conditions))
result = await db.execute(stmt)
all_seals = list(result.scalars().all())
available_seals = []
for seal in all_seals:
# 检查所有者权限
if seal.owner_type == "all":
pass
elif seal.owner_type == "user":
if not seal.owner_ids or user_id not in seal.owner_ids:
continue
elif seal.owner_type == "dept":
if not seal.owner_ids or not dept_id or dept_id not in seal.owner_ids:
continue
elif seal.owner_type == "role":
if not seal.owner_ids or not role_ids:
continue
if not any(r in seal.owner_ids for r in role_ids):
continue
# 检查模板范围
if seal.scope == "specific" and template_id:
if not seal.allowed_template_ids or template_id not in seal.allowed_template_ids:
continue
available_seals.append(seal)
return available_seals
class SealUsageLogService(BaseService[SealUsageLog, None, None]):
"""签章使用记录服务"""
model = SealUsageLog
@classmethod
async def log_usage(
cls,
db: AsyncSession,
seal_id: str,
seal_name: str,
document_id: str,
document_name: str,
user_id: str,
user_name: str,
position_x: int = 0,
position_y: int = 0,
page_number: int = 1,
) -> SealUsageLog:
"""记录签章使用"""
log = SealUsageLog(
seal_id=seal_id,
seal_name=seal_name,
document_id=document_id,
document_name=document_name,
user_id=user_id,
user_name=user_name,
position_x=position_x,
position_y=position_y,
page_number=page_number,
)
db.add(log)
await db.commit()
await db.refresh(log)
return log
@classmethod
async def get_logs_by_seal(
cls,
db: AsyncSession,
seal_id: str,
page: int = 1,
page_size: int = 20,
) -> Tuple[List[SealUsageLog], int]:
"""获取签章的使用记录"""
conditions = [
cls.model.is_deleted == False,
cls.model.seal_id == seal_id,
]
# 查询总数
count_stmt = select(func.count()).select_from(cls.model).where(and_(*conditions))
total = (await db.execute(count_stmt)).scalar() or 0
# 查询列表
stmt = (
select(cls.model)
.where(and_(*conditions))
.order_by(cls.model.sys_create_datetime.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@classmethod
async def get_logs_by_document(
cls,
db: AsyncSession,
document_id: str,
) -> List[SealUsageLog]:
"""获取文档的签章记录"""
stmt = (
select(cls.model)
.where(
and_(
cls.model.is_deleted == False,
cls.model.document_id == document_id,
)
)
.order_by(cls.model.sys_create_datetime.desc())
)
result = await db.execute(stmt)
return list(result.scalars().all())