Files
zkxw/backend/app/crud/oa_approval.py
T
2026-08-11 11:13:01 +08:00

693 lines
21 KiB
Python

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