Files
zkxw/backend/app/crud/oa_instance_draft.py
T
zlwuandCursor de22678c87 feat: ship OA drafts/badges, role dual-track, and test-env deploy tooling.
Add approval instance drafts with menu badges, dual-track user/role read model and role bound names, form-designer outline docs, and bare-metal test deploy scripts. Ignore local test-server credential file.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 13:58:47 +08:00

369 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OA 发起审批草稿 CRUD(oa_instance_draft)"""
from __future__ import annotations
import json
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.crud import oa_approval as oa_crud
from app.models.models import OaApp, OaAppGroup, OaAppVersion, OaInstanceDraft
def _loads(raw: Any, default: Any) -> Any:
if raw is None or raw == "":
return default
if isinstance(raw, (dict, list)):
return raw
try:
return json.loads(raw)
except (TypeError, ValueError, json.JSONDecodeError):
return default
def _dumps(obj: Any) -> Optional[str]:
if obj is None:
return None
return json.dumps(obj, ensure_ascii=False)
def row_to_dict(
d: OaInstanceDraft,
*,
app_name: str = "",
version_no: Optional[int] = None,
group_id: str = "",
group_name: str = "",
include_payload: bool = False,
) -> Dict[str, Any]:
out: Dict[str, Any] = {
"id": d.id,
"appId": d.app_id,
"appVersionId": d.app_version_id,
"appName": app_name or "",
"versionNo": version_no,
"groupId": group_id or "",
"groupName": group_name or "",
"title": d.title or "",
"ownerUserId": d.owner_user_id,
"initiatorDeptId": d.initiator_dept_id or "",
"status": "draft",
"createTime": d.create_time.strftime("%Y-%m-%d %H:%M:%S") if d.create_time else "",
"updateTime": d.update_time.strftime("%Y-%m-%d %H:%M:%S") if d.update_time else "",
}
if include_payload:
out["formData"] = _loads(d.form_data_json, {}) or {}
out["initiatorSelectByNode"] = _loads(d.initiator_select_json, {}) or {}
return out
class DraftAccessError(ValueError):
"""草稿不存在或无权访问"""
def get_owned_draft(
db: Session,
draft_id: str,
*,
tenant_no: str,
owner_user_id: str,
) -> OaInstanceDraft:
row = (
db.query(OaInstanceDraft)
.filter(
OaInstanceDraft.id == draft_id,
OaInstanceDraft.tenant_no == tenant_no,
OaInstanceDraft.owner_user_id == owner_user_id,
OaInstanceDraft.del_flag == 0,
)
.first()
)
if not row:
raise DraftAccessError("草稿不存在或无权访问")
return row
def _enrich(db: Session, d: OaInstanceDraft, *, include_payload: bool = False) -> Dict[str, Any]:
app = (
db.query(OaApp)
.filter(OaApp.id == d.app_id, OaApp.tenant_no == d.tenant_no, OaApp.del_flag == 0)
.first()
)
ver = (
db.query(OaAppVersion)
.filter(OaAppVersion.id == d.app_version_id, OaAppVersion.del_flag == 0)
.first()
)
group_name = ""
group_id = ""
if app and app.group_id:
group_id = app.group_id
g = (
db.query(OaAppGroup)
.filter(OaAppGroup.id == app.group_id, OaAppGroup.del_flag == 0)
.first()
)
if g:
group_name = g.group_name or ""
return row_to_dict(
d,
app_name=(app.app_name if app else "") or "",
version_no=ver.version_no if ver else None,
group_id=group_id,
group_name=group_name,
include_payload=include_payload,
)
def count_drafts(db: Session, *, tenant_no: str, owner_user_id: str) -> int:
return (
db.query(OaInstanceDraft)
.filter(
OaInstanceDraft.tenant_no == tenant_no,
OaInstanceDraft.owner_user_id == owner_user_id,
OaInstanceDraft.del_flag == 0,
)
.count()
)
def list_drafts(
db: Session,
*,
tenant_no: str,
owner_user_id: str,
keyword: Optional[str] = None,
group_id: Optional[str] = None,
update_time_from: Optional[datetime] = None,
update_time_to: Optional[datetime] = None,
page: int = 1,
page_size: int = 20,
) -> Tuple[List[Dict[str, Any]], int]:
q = (
db.query(OaInstanceDraft, OaApp, OaAppVersion, OaAppGroup)
.outerjoin(
OaApp,
(OaApp.id == OaInstanceDraft.app_id)
& (OaApp.tenant_no == OaInstanceDraft.tenant_no)
& (OaApp.del_flag == 0),
)
.outerjoin(
OaAppVersion,
(OaAppVersion.id == OaInstanceDraft.app_version_id) & (OaAppVersion.del_flag == 0),
)
.outerjoin(
OaAppGroup,
(OaAppGroup.id == OaApp.group_id) & (OaAppGroup.del_flag == 0),
)
.filter(
OaInstanceDraft.tenant_no == tenant_no,
OaInstanceDraft.owner_user_id == owner_user_id,
OaInstanceDraft.del_flag == 0,
)
)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q = q.filter(or_(OaApp.app_name.like(like), OaInstanceDraft.title.like(like)))
gid = (group_id or "").strip()
if gid:
q = q.filter(OaApp.group_id == gid)
if update_time_from is not None:
q = q.filter(OaInstanceDraft.update_time >= update_time_from)
if update_time_to is not None:
q = q.filter(OaInstanceDraft.update_time <= update_time_to)
total = q.order_by(None).with_entities(func.count(OaInstanceDraft.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q.order_by(OaInstanceDraft.update_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
items: List[Dict[str, Any]] = []
for d, app, ver, group in rows:
items.append(
row_to_dict(
d,
app_name=(app.app_name if app else "") or "",
version_no=ver.version_no if ver else None,
group_id=(app.group_id if app else "") or "",
group_name=(group.group_name if group else "") or "",
)
)
return items, int(total)
def get_draft_detail(
db: Session,
draft_id: str,
*,
tenant_no: str,
owner_user_id: str,
) -> Dict[str, Any]:
d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
return _enrich(db, d, include_payload=True)
def create_draft(
db: Session,
*,
tenant_no: str,
owner_user_id: str,
app_id: str,
form_data: Optional[Dict[str, Any]] = None,
initiator_select_by_node: Optional[Dict[str, Any]] = None,
initiator_dept_id: Optional[str] = None,
title: Optional[str] = None,
create_dept_id: str = "",
) -> Dict[str, Any]:
app = oa_crud.get_app(db, app_id, tenant_no)
if not app or app.status != "1":
raise ValueError("应用不存在或已停用")
ver = oa_crud._find_latest_published(db, app_id, tenant_no)
if not ver:
raise ValueError("应用尚未发布,无法暂存草稿")
now = datetime.now()
row = OaInstanceDraft(
id=generate_uuid(),
app_id=app_id,
app_version_id=ver.id,
title=(title or app.app_name or "")[:200] or None,
form_data_json=_dumps(form_data or {}),
initiator_select_json=_dumps(initiator_select_by_node or {}),
initiator_dept_id=(initiator_dept_id or "").strip() or None,
owner_user_id=owner_user_id,
tenant_no=tenant_no,
create_by=owner_user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=owner_user_id,
update_time=now,
del_flag=0,
)
db.add(row)
db.commit()
db.refresh(row)
return _enrich(db, row, include_payload=True)
def update_draft(
db: Session,
draft_id: str,
*,
tenant_no: str,
owner_user_id: str,
form_data: Optional[Dict[str, Any]] = None,
initiator_select_by_node: Optional[Dict[str, Any]] = None,
initiator_dept_id: Optional[str] = None,
title: Optional[str] = None,
) -> Dict[str, Any]:
d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
ver = oa_crud._find_latest_published(db, d.app_id, tenant_no)
if ver:
d.app_version_id = ver.id
if form_data is not None:
d.form_data_json = _dumps(form_data)
if initiator_select_by_node is not None:
d.initiator_select_json = _dumps(initiator_select_by_node)
if initiator_dept_id is not None:
d.initiator_dept_id = (initiator_dept_id or "").strip() or None
if title is not None:
d.title = (title or "")[:200] or None
d.update_by = owner_user_id
d.update_time = datetime.now()
db.commit()
db.refresh(d)
return _enrich(db, d, include_payload=True)
def soft_delete_draft(
db: Session,
draft_id: str,
*,
tenant_no: str,
owner_user_id: str,
) -> None:
d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
d.del_flag = 1
d.update_by = owner_user_id
d.update_time = datetime.now()
db.commit()
def batch_soft_delete_drafts(
db: Session,
ids: List[str],
*,
tenant_no: str,
owner_user_id: str,
) -> int:
cleaned = [str(x).strip() for x in (ids or []) if str(x).strip()]
if not cleaned:
raise ValueError("请选择要删除的草稿")
rows = (
db.query(OaInstanceDraft)
.filter(
OaInstanceDraft.id.in_(cleaned),
OaInstanceDraft.tenant_no == tenant_no,
OaInstanceDraft.owner_user_id == owner_user_id,
OaInstanceDraft.del_flag == 0,
)
.all()
)
if len(rows) != len(set(cleaned)):
raise DraftAccessError("草稿不存在或无权访问")
now = datetime.now()
for d in rows:
d.del_flag = 1
d.update_by = owner_user_id
d.update_time = now
db.commit()
return len(rows)
def submit_draft(
db: Session,
draft_id: str,
*,
tenant_no: str,
owner_user_id: str,
form_data: Optional[Dict[str, Any]] = None,
initiator_select_by_node: Optional[Dict[str, Any]] = None,
initiator_dept_id: Optional[str] = None,
title: Optional[str] = None,
) -> Dict[str, Any]:
"""提交草稿:走 start_instance,成功后软删草稿。"""
from app.services.oa_engine import start_instance
d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
payload_form = form_data if form_data is not None else (_loads(d.form_data_json, {}) or {})
payload_select = (
initiator_select_by_node
if initiator_select_by_node is not None
else (_loads(d.initiator_select_json, {}) or {})
)
dept = initiator_dept_id if initiator_dept_id is not None else d.initiator_dept_id
use_title = title if title is not None else d.title
data = start_instance(
db,
tenant_no=tenant_no,
app_id=d.app_id,
initiator_user_id=owner_user_id,
initiator_dept_id=dept,
form_data=payload_form if isinstance(payload_form, dict) else {},
title=use_title,
initiator_select_by_node=payload_select if isinstance(payload_select, dict) else {},
)
d.del_flag = 1
d.update_by = owner_user_id
d.update_time = datetime.now()
db.commit()
return data