"""OA 审批 CRUD(分组、应用、草稿等)""" from __future__ import annotations import copy import json from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from sqlalchemy.orm import Session from app.core.utils import generate_uuid from app.models.models import OaApp, OaAppGroup, OaAppVersion DEFAULT_ADVANCED_SETTINGS: Dict[str, Any] = { "supervisorFallbackToDeptLeader": True, "allowWithdrawRunning": True, "approverDedup": False, "commentRequired": False, "allowAddSign": True, "allowReturn": True, "autoPassWhenApproverIsInitiator": False, } def _dumps(data: Any) -> Optional[str]: if data is None: return None if isinstance(data, str): return data return json.dumps(data, ensure_ascii=False) def _loads(text: Optional[str], default: Any = None) -> Any: if text is None or text == "": return default if not isinstance(text, str): return text try: return json.loads(text) except (TypeError, ValueError): return default # --------------------------------------------------------------------------- # 分组 oa_app_group # --------------------------------------------------------------------------- def _group_to_dict(g: OaAppGroup) -> Dict[str, Any]: return { "id": g.id, "groupName": g.group_name, "tenantNo": g.tenant_no, "orderNum": g.order_num if g.order_num is not None else 0, "remark": g.remark or "", "createTime": g.create_time.strftime("%Y-%m-%d %H:%M:%S") if g.create_time else "", "updateTime": g.update_time.strftime("%Y-%m-%d %H:%M:%S") if g.update_time else "", } def list_groups(db: Session, *, tenant_no: str) -> List[Dict[str, Any]]: rows = ( db.query(OaAppGroup) .filter(OaAppGroup.tenant_no == tenant_no, OaAppGroup.del_flag == 0) .order_by(OaAppGroup.order_num.asc(), OaAppGroup.create_time.asc()) .all() ) return [_group_to_dict(r) for r in rows] def get_group(db: Session, group_id: str, tenant_no: str) -> Optional[OaAppGroup]: return ( db.query(OaAppGroup) .filter(OaAppGroup.id == group_id, OaAppGroup.tenant_no == tenant_no, OaAppGroup.del_flag == 0) .first() ) def create_group( db: Session, *, tenant_no: str, group_name: str, order_num: Optional[int] = None, remark: Optional[str] = None, user_id: Optional[str] = None, ) -> OaAppGroup: name = (group_name or "").strip() if not name: raise ValueError("分组名称不能为空") now = datetime.now() obj = OaAppGroup( id=generate_uuid(), group_name=name, tenant_no=tenant_no, order_num=order_num if order_num is not None else 0, remark=(remark or "").strip() or None, create_by=user_id, create_dept_id="", create_time=now, update_by=user_id, update_time=now, del_flag=0, ) db.add(obj) db.commit() db.refresh(obj) return obj def update_group( db: Session, group_id: str, *, tenant_no: str, group_name: Optional[str] = None, order_num: Optional[int] = None, remark: Optional[str] = None, user_id: Optional[str] = None, ) -> OaAppGroup: obj = get_group(db, group_id, tenant_no) if not obj: raise ValueError("分组不存在") if group_name is not None: name = (group_name or "").strip() if not name: raise ValueError("分组名称不能为空") obj.group_name = name if order_num is not None: obj.order_num = order_num if remark is not None: obj.remark = (remark or "").strip() or None obj.update_by = user_id obj.update_time = datetime.now() db.add(obj) db.commit() db.refresh(obj) return obj def delete_group(db: Session, group_id: str, *, tenant_no: str, user_id: Optional[str] = None) -> None: obj = get_group(db, group_id, tenant_no) if not obj: raise ValueError("分组不存在") used = ( db.query(OaApp.id) .filter(OaApp.group_id == group_id, OaApp.tenant_no == tenant_no, OaApp.del_flag == 0) .first() ) if used: raise ValueError("分组下仍有应用,无法删除") obj.del_flag = 1 obj.update_by = user_id obj.update_time = datetime.now() db.add(obj) db.commit() # --------------------------------------------------------------------------- # 应用 oa_app / 草稿 oa_app_version # --------------------------------------------------------------------------- def _app_to_dict(a: OaApp, *, group_name: str = "", has_draft: bool = False, latest_published: Optional[int] = None) -> Dict[str, Any]: return { "id": a.id, "groupId": a.group_id or "", "groupName": group_name or "", "appName": a.app_name, "iconUrl": a.icon_url or "", "description": a.description or "", "initiatorScope": a.initiator_scope or "all", "initiatorUserIds": _loads(a.initiator_user_ids, []), "adminScope": a.admin_scope or "all_oa_admin", "adminUserIds": _loads(a.admin_user_ids, []), "status": a.status, "tenantNo": a.tenant_no, "orderNum": a.order_num if a.order_num is not None else 0, "remark": a.remark or "", "hasDraft": has_draft, "latestPublishedVersion": latest_published, "createTime": a.create_time.strftime("%Y-%m-%d %H:%M:%S") if a.create_time else "", "updateTime": a.update_time.strftime("%Y-%m-%d %H:%M:%S") if a.update_time else "", } def get_app(db: Session, app_id: str, tenant_no: str) -> Optional[OaApp]: return ( db.query(OaApp) .filter(OaApp.id == app_id, OaApp.tenant_no == tenant_no, OaApp.del_flag == 0) .first() ) def list_apps( db: Session, *, tenant_no: str, group_id: Optional[str] = None, keyword: Optional[str] = None, status: Optional[str] = None, ) -> List[Dict[str, Any]]: q = db.query(OaApp).filter(OaApp.tenant_no == tenant_no, OaApp.del_flag == 0) if group_id: q = q.filter(OaApp.group_id == group_id) if status: q = q.filter(OaApp.status == status) kw = (keyword or "").strip() if kw: like = f"%{kw}%" q = q.filter(OaApp.app_name.like(like)) rows = q.order_by(OaApp.order_num.asc(), OaApp.create_time.desc()).all() if not rows: return [] group_ids = list({r.group_id for r in rows if r.group_id}) group_names: Dict[str, str] = {} if group_ids: for g in ( db.query(OaAppGroup) .filter(OaAppGroup.id.in_(group_ids), OaAppGroup.del_flag == 0) .all() ): group_names[g.id] = g.group_name app_ids = [r.id for r in rows] # 批量取草稿/最新发布版本号,避免 N+1,且不拉取 form/flow 大 JSON draft_app_ids = { app_id for (app_id,) in ( db.query(OaAppVersion.app_id) .filter( OaAppVersion.app_id.in_(app_ids), OaAppVersion.tenant_no == tenant_no, OaAppVersion.status == "draft", OaAppVersion.del_flag == 0, ) .distinct() .all() ) } latest_published: Dict[str, int] = { app_id: version_no for app_id, version_no in ( db.query(OaAppVersion.app_id, OaAppVersion.version_no) .filter( OaAppVersion.app_id.in_(app_ids), OaAppVersion.tenant_no == tenant_no, OaAppVersion.is_latest_published == "1", OaAppVersion.del_flag == 0, ) .all() ) } return [ _app_to_dict( r, group_name=group_names.get(r.group_id or "", ""), has_draft=r.id in draft_app_ids, latest_published=latest_published.get(r.id), ) for r in rows ] def _create_version( db: Session, *, app_id: str, tenant_no: str, version_no: int, status: str, form_schema: Any, flow_tree: Any, advanced: Any, user_id: Optional[str], is_latest_published: str = "0", ) -> OaAppVersion: now = datetime.now() ver = OaAppVersion( id=generate_uuid(), app_id=app_id, version_no=version_no, status=status, is_latest_published=is_latest_published, form_schema_json=_dumps(form_schema if form_schema is not None else {}), flow_tree_json=_dumps(flow_tree if flow_tree is not None else {}), advanced_settings_json=_dumps(advanced if advanced is not None else DEFAULT_ADVANCED_SETTINGS), tenant_no=tenant_no, order_num=0, remark=None, create_by=user_id, create_dept_id="", create_time=now, update_by=user_id, update_time=now, del_flag=0, ) db.add(ver) return ver def create_app( db: Session, *, tenant_no: str, app_name: str, group_id: Optional[str] = None, icon_url: Optional[str] = None, description: Optional[str] = None, initiator_scope: str = "all", initiator_user_ids: Optional[List[str]] = None, admin_scope: str = "all_oa_admin", admin_user_ids: Optional[List[str]] = None, status: str = "1", order_num: Optional[int] = None, remark: Optional[str] = None, user_id: Optional[str] = None, ) -> OaApp: name = (app_name or "").strip() if not name: raise ValueError("应用名称不能为空") if len(name) > 50: raise ValueError("应用名称不能超过50字") gid = (group_id or "").strip() or None if gid and not get_group(db, gid, tenant_no): raise ValueError("分组不存在") now = datetime.now() obj = OaApp( id=generate_uuid(), group_id=gid, app_name=name, icon_url=(icon_url or "").strip() or None, description=(description or "").strip() or None, initiator_scope=initiator_scope or "all", initiator_user_ids=_dumps(initiator_user_ids or []), admin_scope=admin_scope or "all_oa_admin", admin_user_ids=_dumps(admin_user_ids or []), status=status or "1", tenant_no=tenant_no, order_num=order_num if order_num is not None else 0, remark=(remark or "").strip() or None, create_by=user_id, create_dept_id="", create_time=now, update_by=user_id, update_time=now, del_flag=0, ) db.add(obj) _create_version( db, app_id=obj.id, tenant_no=tenant_no, version_no=1, status="draft", form_schema={}, flow_tree={}, advanced=DEFAULT_ADVANCED_SETTINGS, user_id=user_id, ) db.commit() db.refresh(obj) return obj def _copied_app_name(source_name: str) -> str: """原名称 +「复制」;超 50 字时截断前缀并保留后缀。""" suffix = "复制" base = (source_name or "").strip() candidate = f"{base}{suffix}" if len(candidate) <= 50: return candidate max_base = max(0, 50 - len(suffix)) return f"{base[:max_base]}{suffix}" def copy_app( db: Session, source_app_id: str, *, tenant_no: str, user_id: Optional[str] = None, ) -> OaApp: """复制应用:基础信息 + 草稿/最新发布版的表单·流程·高级设置,结果为未发布草稿。""" src = get_app(db, source_app_id, tenant_no) if not src: raise ValueError("应用不存在") source_ver = _find_draft(db, source_app_id, tenant_no) or _find_latest_published( db, source_app_id, tenant_no ) if source_ver: form_schema = copy.deepcopy(_loads(source_ver.form_schema_json, {}) or {}) flow_tree = copy.deepcopy(_loads(source_ver.flow_tree_json, {}) or {}) advanced = copy.deepcopy( _loads(source_ver.advanced_settings_json, DEFAULT_ADVANCED_SETTINGS) or DEFAULT_ADVANCED_SETTINGS ) else: form_schema = {} flow_tree = {} advanced = dict(DEFAULT_ADVANCED_SETTINGS) now = datetime.now() obj = OaApp( id=generate_uuid(), group_id=src.group_id, app_name=_copied_app_name(src.app_name), icon_url=src.icon_url, description=src.description, initiator_scope=src.initiator_scope or "all", initiator_user_ids=src.initiator_user_ids, admin_scope=src.admin_scope or "all_oa_admin", admin_user_ids=src.admin_user_ids, status=src.status or "1", tenant_no=tenant_no, order_num=src.order_num if src.order_num is not None else 0, remark=src.remark, create_by=user_id, create_dept_id="", create_time=now, update_by=user_id, update_time=now, del_flag=0, ) db.add(obj) _create_version( db, app_id=obj.id, tenant_no=tenant_no, version_no=1, status="draft", form_schema=form_schema, flow_tree=flow_tree, advanced=advanced, user_id=user_id, ) db.commit() db.refresh(obj) return obj def update_app( db: Session, app_id: str, *, tenant_no: str, app_name: Optional[str] = None, group_id: Optional[str] = None, icon_url: Optional[str] = None, description: Optional[str] = None, initiator_scope: Optional[str] = None, initiator_user_ids: Optional[List[str]] = None, admin_scope: Optional[str] = None, admin_user_ids: Optional[List[str]] = None, status: Optional[str] = None, order_num: Optional[int] = None, remark: Optional[str] = None, user_id: Optional[str] = None, ) -> OaApp: obj = get_app(db, app_id, tenant_no) if not obj: raise ValueError("应用不存在") if app_name is not None: name = (app_name or "").strip() if not name: raise ValueError("应用名称不能为空") if len(name) > 50: raise ValueError("应用名称不能超过50字") obj.app_name = name if group_id is not None: gid = (group_id or "").strip() or None if gid and not get_group(db, gid, tenant_no): raise ValueError("分组不存在") obj.group_id = gid if icon_url is not None: obj.icon_url = (icon_url or "").strip() or None if description is not None: desc = (description or "").strip() if len(desc) > 100: raise ValueError("表单说明不能超过100字") obj.description = desc or None if initiator_scope is not None: obj.initiator_scope = initiator_scope if initiator_user_ids is not None: obj.initiator_user_ids = _dumps(initiator_user_ids) if admin_scope is not None: obj.admin_scope = admin_scope if admin_user_ids is not None: obj.admin_user_ids = _dumps(admin_user_ids) if status is not None: obj.status = status if order_num is not None: obj.order_num = order_num if remark is not None: obj.remark = (remark or "").strip() or None obj.update_by = user_id obj.update_time = datetime.now() db.add(obj) db.commit() db.refresh(obj) return obj def delete_app(db: Session, app_id: str, *, tenant_no: str, user_id: Optional[str] = None) -> None: obj = get_app(db, app_id, tenant_no) if not obj: raise ValueError("应用不存在") now = datetime.now() obj.del_flag = 1 obj.update_by = user_id obj.update_time = now db.add(obj) for ver in ( db.query(OaAppVersion) .filter(OaAppVersion.app_id == app_id, OaAppVersion.tenant_no == tenant_no, OaAppVersion.del_flag == 0) .all() ): ver.del_flag = 1 ver.update_by = user_id ver.update_time = now db.add(ver) db.commit() def _find_draft(db: Session, app_id: str, tenant_no: str) -> Optional[OaAppVersion]: return ( db.query(OaAppVersion) .filter( OaAppVersion.app_id == app_id, OaAppVersion.tenant_no == tenant_no, OaAppVersion.status == "draft", OaAppVersion.del_flag == 0, ) .order_by(OaAppVersion.version_no.desc()) .first() ) def _find_latest_published(db: Session, app_id: str, tenant_no: str) -> Optional[OaAppVersion]: return ( db.query(OaAppVersion) .filter( OaAppVersion.app_id == app_id, OaAppVersion.tenant_no == tenant_no, OaAppVersion.is_latest_published == "1", OaAppVersion.del_flag == 0, ) .first() ) def _version_draft_to_dict(app: OaApp, ver: OaAppVersion) -> Dict[str, Any]: advanced = _loads(ver.advanced_settings_json, None) if not isinstance(advanced, dict): advanced = dict(DEFAULT_ADVANCED_SETTINGS) else: merged = dict(DEFAULT_ADVANCED_SETTINGS) merged.update(advanced) advanced = merged return { "appId": app.id, "versionId": ver.id, "versionNo": ver.version_no, "status": ver.status, "basic": { "groupId": app.group_id or "", "appName": app.app_name, "iconUrl": app.icon_url or "", "description": app.description or "", "initiatorScope": app.initiator_scope or "all", "initiatorUserIds": _loads(app.initiator_user_ids, []), "adminScope": app.admin_scope or "all_oa_admin", "adminUserIds": _loads(app.admin_user_ids, []), "status": app.status, "orderNum": app.order_num if app.order_num is not None else 0, "remark": app.remark or "", }, "formSchemaJson": _loads(ver.form_schema_json, {}), "flowTreeJson": _loads(ver.flow_tree_json, {}), "advancedSettingsJson": advanced, "updateTime": ver.update_time.strftime("%Y-%m-%d %H:%M:%S") if ver.update_time else "", } def get_or_create_draft( db: Session, app_id: str, *, tenant_no: str, user_id: Optional[str] = None ) -> Dict[str, Any]: app = get_app(db, app_id, tenant_no) if not app: raise ValueError("应用不存在") draft = _find_draft(db, app_id, tenant_no) if draft: return _version_draft_to_dict(app, draft) published = _find_latest_published(db, app_id, tenant_no) if published: next_no = (published.version_no or 0) + 1 draft = _create_version( db, app_id=app_id, tenant_no=tenant_no, version_no=next_no, status="draft", form_schema=_loads(published.form_schema_json, {}), flow_tree=_loads(published.flow_tree_json, {}), advanced=_loads(published.advanced_settings_json, DEFAULT_ADVANCED_SETTINGS), user_id=user_id, ) else: max_row = ( db.query(OaAppVersion) .filter(OaAppVersion.app_id == app_id, OaAppVersion.tenant_no == tenant_no, OaAppVersion.del_flag == 0) .order_by(OaAppVersion.version_no.desc()) .first() ) next_no = (max_row.version_no if max_row else 0) + 1 draft = _create_version( db, app_id=app_id, tenant_no=tenant_no, version_no=next_no or 1, status="draft", form_schema={}, flow_tree={}, advanced=DEFAULT_ADVANCED_SETTINGS, user_id=user_id, ) db.commit() db.refresh(draft) return _version_draft_to_dict(app, draft) def save_draft( db: Session, app_id: str, *, tenant_no: str, basic: Optional[Dict[str, Any]] = None, form_schema_json: Any = None, flow_tree_json: Any = None, advanced_settings_json: Any = None, user_id: Optional[str] = None, ) -> Dict[str, Any]: app = get_app(db, app_id, tenant_no) if not app: raise ValueError("应用不存在") # 确保草稿存在 get_or_create_draft(db, app_id, tenant_no=tenant_no, user_id=user_id) draft = _find_draft(db, app_id, tenant_no) if not draft: raise ValueError("草稿不存在") if basic: field_map = { "appName": "app_name", "groupId": "group_id", "iconUrl": "icon_url", "description": "description", "initiatorScope": "initiator_scope", "initiatorUserIds": "initiator_user_ids", "adminScope": "admin_scope", "adminUserIds": "admin_user_ids", "status": "status", "orderNum": "order_num", "remark": "remark", } kwargs = {field_map[k]: basic[k] for k in field_map if k in basic} if kwargs: update_app(db, app_id, tenant_no=tenant_no, user_id=user_id, **kwargs) app = get_app(db, app_id, tenant_no) draft = _find_draft(db, app_id, tenant_no) if not draft: raise ValueError("草稿不存在") now = datetime.now() if form_schema_json is not None: draft.form_schema_json = _dumps(form_schema_json) if flow_tree_json is not None: draft.flow_tree_json = _dumps(flow_tree_json) if advanced_settings_json is not None: if isinstance(advanced_settings_json, dict): merged = dict(DEFAULT_ADVANCED_SETTINGS) merged.update(advanced_settings_json) draft.advanced_settings_json = _dumps(merged) else: draft.advanced_settings_json = _dumps(advanced_settings_json) draft.update_by = user_id draft.update_time = now db.add(draft) db.commit() db.refresh(draft) assert app is not None return _version_draft_to_dict(app, draft)