| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692 |
- """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)
|