"""OA 审批管理接口(钉钉交互)""" from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.orm import Session from app.core.database import get_db from app.core.menu_guard import require_admin_or_super_admin from app.core.response import error_response, success_response from app.crud import oa_approval as oa_crud from app.models.models import SysUser router = APIRouter(prefix="/web/admin/oa-approval", tags=["OA审批"]) def _get_tenant_no(db: Session, user: dict) -> str: tenant = (user.get("tenant_no") or "").strip() if tenant: return tenant uid = user.get("user_id") if uid: row = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first() if row and row.tenant_no: return row.tenant_no return "zkxw" # --------------------------------------------------------------------------- # 工作流角色 # --------------------------------------------------------------------------- class OaRoleCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) role_name: str = Field(..., alias="roleName") role_code: str = Field(..., alias="roleCode") status: Optional[str] = Field("1") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class OaRoleUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) role_name: Optional[str] = Field(None, alias="roleName") status: Optional[str] = None order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class OaRoleUsersBody(BaseModel): model_config = ConfigDict(populate_by_name=True) user_ids: List[str] = Field(default_factory=list, alias="userIds") @router.get("/roles") async def list_oa_roles( keyword: Optional[str] = None, status: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items = oa_crud.list_roles(db, tenant_no=tenant_no, keyword=keyword, status=status) return success_response({"list": items}) @router.post("/roles/ensure-presets") async def ensure_oa_role_presets( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) created = oa_crud.ensure_presets(db, tenant_no, user_id=user.get("user_id")) return success_response( {"createdCount": len(created)}, message="预置角色已就绪", ) @router.post("/roles") async def create_oa_role( body: OaRoleCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = oa_crud.create_role( db, tenant_no=tenant_no, role_name=body.role_name, role_code=body.role_code, status=body.status or "1", order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(oa_crud._role_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/roles/{role_id}") async def update_oa_role( role_id: str, body: OaRoleUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = oa_crud.update_role( db, role_id, tenant_no=tenant_no, role_name=payload.get("role_name"), status=payload.get("status"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(oa_crud._role_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/roles/{role_id}") async def delete_oa_role( role_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) oa_crud.delete_role(db, role_id, tenant_no=tenant_no, user_id=user.get("user_id")) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @router.get("/roles/{role_id}/users") async def list_oa_role_users( role_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) if not oa_crud.get_role(db, role_id, tenant_no): return error_response(404, "角色不存在") return success_response({"list": oa_crud.list_role_users(db, role_id, tenant_no)}) @router.put("/roles/{role_id}/users") async def replace_oa_role_users( role_id: str, body: OaRoleUsersBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) oa_crud.replace_role_users( db, role_id, tenant_no=tenant_no, user_ids=body.user_ids, operator_id=user.get("user_id"), ) return success_response( {"list": oa_crud.list_role_users(db, role_id, tenant_no)}, message="成员已更新", ) except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 分组 # --------------------------------------------------------------------------- class OaGroupCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) group_name: str = Field(..., alias="groupName") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class OaGroupUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) group_name: Optional[str] = Field(None, alias="groupName") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/groups") async def list_oa_groups( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) return success_response({"list": oa_crud.list_groups(db, tenant_no=tenant_no)}) @router.post("/groups") async def create_oa_group( body: OaGroupCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = oa_crud.create_group( db, tenant_no=tenant_no, group_name=body.group_name, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(oa_crud._group_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/groups/{group_id}") async def update_oa_group( group_id: str, body: OaGroupUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload = body.model_dump(exclude_unset=True, by_alias=False) obj = oa_crud.update_group( db, group_id, tenant_no=tenant_no, group_name=payload.get("group_name"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(oa_crud._group_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/groups/{group_id}") async def delete_oa_group( group_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) oa_crud.delete_group(db, group_id, tenant_no=tenant_no, user_id=user.get("user_id")) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 应用 / 草稿 # --------------------------------------------------------------------------- class OaAppCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) app_name: str = Field(..., alias="appName") group_id: Optional[str] = Field(None, alias="groupId") icon_url: Optional[str] = Field(None, alias="iconUrl") description: Optional[str] = None initiator_scope: Optional[str] = Field("all", alias="initiatorScope") initiator_user_ids: Optional[List[str]] = Field(None, alias="initiatorUserIds") admin_scope: Optional[str] = Field("all_oa_admin", alias="adminScope") admin_user_ids: Optional[List[str]] = Field(None, alias="adminUserIds") status: Optional[str] = "1" order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class OaAppUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) app_name: Optional[str] = Field(None, alias="appName") group_id: Optional[str] = Field(None, alias="groupId") icon_url: Optional[str] = Field(None, alias="iconUrl") description: Optional[str] = None initiator_scope: Optional[str] = Field(None, alias="initiatorScope") initiator_user_ids: Optional[List[str]] = Field(None, alias="initiatorUserIds") admin_scope: Optional[str] = Field(None, alias="adminScope") admin_user_ids: Optional[List[str]] = Field(None, alias="adminUserIds") status: Optional[str] = None order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class OaDraftSaveBody(BaseModel): model_config = ConfigDict(populate_by_name=True) basic: Optional[Dict[str, Any]] = None form_schema_json: Optional[Any] = Field(None, alias="formSchemaJson") flow_tree_json: Optional[Any] = Field(None, alias="flowTreeJson") advanced_settings_json: Optional[Any] = Field(None, alias="advancedSettingsJson") @router.get("/apps") async def list_oa_apps( groupId: Optional[str] = None, keyword: Optional[str] = None, status: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items = oa_crud.list_apps( db, tenant_no=tenant_no, group_id=groupId, keyword=keyword, status=status ) return success_response({"list": items}) @router.post("/apps") async def create_oa_app( body: OaAppCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = oa_crud.create_app( db, tenant_no=tenant_no, app_name=body.app_name, group_id=body.group_id, icon_url=body.icon_url, description=body.description, initiator_scope=body.initiator_scope or "all", initiator_user_ids=body.initiator_user_ids, admin_scope=body.admin_scope or "all_oa_admin", admin_user_ids=body.admin_user_ids, status=body.status or "1", order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) group_name = "" if obj.group_id: g = oa_crud.get_group(db, obj.group_id, tenant_no) group_name = g.group_name if g else "" return success_response( oa_crud._app_to_dict(obj, group_name=group_name, has_draft=True), message="创建成功", ) except ValueError as e: return error_response(400, str(e)) @router.put("/apps/{app_id}") async def update_oa_app( app_id: str, body: OaAppUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload = body.model_dump(exclude_unset=True, by_alias=False) obj = oa_crud.update_app(db, app_id, tenant_no=tenant_no, user_id=user.get("user_id"), **payload) group_name = "" if obj.group_id: g = oa_crud.get_group(db, obj.group_id, tenant_no) group_name = g.group_name if g else "" return success_response(oa_crud._app_to_dict(obj, group_name=group_name), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/apps/{app_id}") async def delete_oa_app( app_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) oa_crud.delete_app(db, app_id, tenant_no=tenant_no, user_id=user.get("user_id")) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @router.get("/apps/{app_id}/draft") async def get_oa_app_draft( app_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) data = oa_crud.get_or_create_draft( db, app_id, tenant_no=tenant_no, user_id=user.get("user_id") ) return success_response(data) except ValueError as e: return error_response(400, str(e)) @router.put("/apps/{app_id}/draft") async def save_oa_app_draft( app_id: str, body: OaDraftSaveBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) data = oa_crud.save_draft( db, app_id, tenant_no=tenant_no, basic=body.basic, form_schema_json=body.form_schema_json, flow_tree_json=body.flow_tree_json, advanced_settings_json=body.advanced_settings_json, user_id=user.get("user_id"), ) return success_response(data, message="草稿已保存") except ValueError as e: return error_response(400, str(e)) @router.post("/apps/{app_id}/publish") async def publish_oa_app( app_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_publish import publish_app tenant_no = _get_tenant_no(db, user) data, errors = publish_app( db, app_id, tenant_no=tenant_no, user_id=user.get("user_id") ) if errors: return error_response(400, "发布校验未通过", data={"errors": errors}) return success_response(data, message="发布成功") # --------------------------------------------------------------------------- # 运行时:模板 / 实例 / 任务 # --------------------------------------------------------------------------- class OaStartInstanceBody(BaseModel): model_config = ConfigDict(populate_by_name=True) app_id: str = Field(..., alias="appId") form_data: Optional[Dict[str, Any]] = Field(None, alias="formData") title: Optional[str] = None initiator_dept_id: Optional[str] = Field(None, alias="initiatorDeptId") initiator_select_by_node: Optional[Dict[str, List[str]]] = Field( None, alias="initiatorSelectByNode" ) related_instance_ids: Optional[List[str]] = Field(None, alias="relatedInstanceIds") source_instance_id: Optional[str] = Field(None, alias="sourceInstanceId") class OaFlowPreviewBody(BaseModel): model_config = ConfigDict(populate_by_name=True) form_data: Optional[Dict[str, Any]] = Field(None, alias="formData") initiator_select_by_node: Optional[Dict[str, List[str]]] = Field( None, alias="initiatorSelectByNode" ) class OaTaskActionBody(BaseModel): model_config = ConfigDict(populate_by_name=True) comment: Optional[str] = None class OaAddSignBody(BaseModel): model_config = ConfigDict(populate_by_name=True) user_ids: List[str] = Field(default_factory=list, alias="userIds") comment: Optional[str] = None class OaTransferBody(BaseModel): model_config = ConfigDict(populate_by_name=True) target_user_id: str = Field(..., alias="targetUserId") comment: Optional[str] = None class OaResubmitBody(BaseModel): model_config = ConfigDict(populate_by_name=True) form_data: Optional[Dict[str, Any]] = Field(None, alias="formData") initiator_select_by_node: Optional[Dict[str, List[str]]] = Field( None, alias="initiatorSelectByNode" ) @router.get("/templates") async def list_oa_templates( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services import oa_engine tenant_no = _get_tenant_no(db, user) items = oa_engine.list_templates(db, tenant_no=tenant_no, user_id=user.get("user_id") or "") return success_response({"list": items}) @router.get("/templates/{app_id}") async def get_oa_template( app_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, get_template_detail try: tenant_no = _get_tenant_no(db, user) data = get_template_detail( db, tenant_no=tenant_no, app_id=app_id, user_id=user.get("user_id") or "", ) return success_response(data) except OaEngineError as e: return error_response(400, str(e)) @router.post("/templates/{app_id}/flow-preview") async def preview_oa_template_flow( app_id: str, body: OaFlowPreviewBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """发起前预览审批链(条件齐后解析最终路径与审批人)。""" from app.services.oa_engine import OaEngineError, preview_start_flow try: tenant_no = _get_tenant_no(db, user) uid = user.get("user_id") or "" dept_id = None if uid: row = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first() if row: dept_id = row.dept_id data = preview_start_flow( db, tenant_no=tenant_no, app_id=app_id, initiator_user_id=uid, initiator_dept_id=dept_id, initiator_select_by_node=body.initiator_select_by_node, form_data=body.form_data, ) return success_response(data) except OaEngineError as e: return error_response(400, str(e)) @router.get("/templates/{app_id}/flow-preview") async def preview_oa_template_flow_get( app_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """兼容旧 GET:无 formData,含条件分支时多为待确定占位。""" from app.services.oa_engine import OaEngineError, preview_start_flow try: tenant_no = _get_tenant_no(db, user) uid = user.get("user_id") or "" dept_id = None if uid: row = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first() if row: dept_id = row.dept_id data = preview_start_flow( db, tenant_no=tenant_no, app_id=app_id, initiator_user_id=uid, initiator_dept_id=dept_id, initiator_select_by_node=None, form_data=None, ) return success_response(data) except OaEngineError as e: return error_response(400, str(e)) @router.post("/instances") async def start_oa_instance( body: OaStartInstanceBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, start_instance try: tenant_no = _get_tenant_no(db, user) data = start_instance( db, tenant_no=tenant_no, app_id=body.app_id, initiator_user_id=user.get("user_id") or "", initiator_dept_id=body.initiator_dept_id, form_data=body.form_data, title=body.title, initiator_select_by_node=body.initiator_select_by_node, related_instance_ids=body.related_instance_ids, source_instance_id=body.source_instance_id, ) return success_response(data, message="发起成功") except OaEngineError as e: return error_response(400, str(e)) @router.get("/instances/{instance_id}") async def get_oa_instance( instance_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, get_instance_detail try: tenant_no = _get_tenant_no(db, user) return success_response(get_instance_detail(db, instance_id, tenant_no=tenant_no)) except OaEngineError as e: return error_response(400, str(e)) @router.get("/related-candidates") async def list_related_candidates( keyword: str = Query(""), page: int = Query(1), pageSize: int = Query(20, alias="pageSize"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services import oa_related_approval as rel tenant_no = _get_tenant_no(db, user) data = rel.list_related_candidates( db, tenant_no=tenant_no, user_id=user.get("user_id") or "", keyword=keyword, page=page, page_size=pageSize, ) return success_response(data) @router.get("/related-summaries") async def get_related_summaries( ids: str = Query("", description="comma-separated instance ids"), mode: str = Query("picker"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services import oa_related_approval as rel from app.services.oa_engine import OaEngineError tenant_no = _get_tenant_no(db, user) id_list = [x.strip() for x in (ids or "").split(",") if x.strip()] try: items = rel.build_related_summaries( db, tenant_no=tenant_no, user_id=user.get("user_id") or "", ids=id_list, mode=mode, ) except OaEngineError as e: return error_response(400, str(e)) return success_response({"list": items}) @router.get("/tasks/{kind}") async def list_oa_tasks( kind: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, list_tasks try: tenant_no = _get_tenant_no(db, user) items = list_tasks( db, tenant_no=tenant_no, user_id=user.get("user_id") or "", kind=kind ) return success_response({"list": items}) except OaEngineError as e: return error_response(400, str(e)) @router.post("/tasks/{task_id}/approve") async def approve_oa_task( task_id: str, body: Optional[OaTaskActionBody] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, complete_task try: tenant_no = _get_tenant_no(db, user) data = complete_task( db, task_id, tenant_no=tenant_no, user_id=user.get("user_id") or "", action="approve", comment=(body.comment if body else None), ) return success_response(data, message="已同意") except OaEngineError as e: return error_response(400, str(e)) @router.post("/tasks/{task_id}/reject") async def reject_oa_task( task_id: str, body: Optional[OaTaskActionBody] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, complete_task try: tenant_no = _get_tenant_no(db, user) data = complete_task( db, task_id, tenant_no=tenant_no, user_id=user.get("user_id") or "", action="reject", comment=(body.comment if body else None), ) return success_response(data, message="已拒绝") except OaEngineError as e: return error_response(400, str(e)) @router.post("/tasks/{task_id}/return") async def return_oa_task( task_id: str, body: Optional[OaTaskActionBody] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, return_task try: tenant_no = _get_tenant_no(db, user) data = return_task( db, task_id, tenant_no=tenant_no, user_id=user.get("user_id") or "", comment=(body.comment if body else None), ) return success_response(data, message="已打回") except OaEngineError as e: return error_response(400, str(e)) @router.post("/instances/{instance_id}/resubmit") async def resubmit_oa_instance( instance_id: str, body: Optional[OaResubmitBody] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, resubmit_instance try: tenant_no = _get_tenant_no(db, user) data = resubmit_instance( db, instance_id, tenant_no=tenant_no, user_id=user.get("user_id") or "", form_data=(body.form_data if body else None), initiator_select_by_node=(body.initiator_select_by_node if body else None), ) return success_response(data, message="已再次提交") except OaEngineError as e: return error_response(400, str(e)) @router.post("/tasks/{task_id}/add-sign") async def add_sign_oa_task( task_id: str, body: OaAddSignBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, add_sign try: tenant_no = _get_tenant_no(db, user) data = add_sign( db, task_id, tenant_no=tenant_no, user_id=user.get("user_id") or "", user_ids=body.user_ids, comment=body.comment, ) return success_response(data, message="加签成功") except OaEngineError as e: return error_response(400, str(e)) @router.post("/tasks/{task_id}/transfer") async def transfer_oa_task( task_id: str, body: OaTransferBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, transfer_task try: tenant_no = _get_tenant_no(db, user) data = transfer_task( db, task_id, tenant_no=tenant_no, user_id=user.get("user_id") or "", target_user_id=body.target_user_id, comment=body.comment, ) return success_response(data, message="已转交") except OaEngineError as e: return error_response(400, str(e)) @router.post("/instances/{instance_id}/withdraw") async def withdraw_oa_instance( instance_id: str, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): from app.services.oa_engine import OaEngineError, withdraw_instance try: tenant_no = _get_tenant_no(db, user) data = withdraw_instance( db, instance_id, tenant_no=tenant_no, user_id=user.get("user_id") or "" ) return success_response(data, message="已撤销") except OaEngineError as e: return error_response(400, str(e))