Files
zkxw/backend/app/services/oa_instance_comment.py
T

291 lines
8.7 KiB
Python

"""OA 审批实例评论:附件枚举、校验、CRUD"""
from __future__ import annotations
import json
from datetime import datetime
from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import OaAppVersion, OaInstance, OaInstanceComment
from app.services.oa_engine import OaEngineError, _get_instance, _loads, _user_name_map
from app.services.oa_related_approval import (
UPLOAD_TYPES,
_extract_upload_urls,
_schema_rules,
_upload_display_name,
)
CONTENT_MAX = 1024
def enumerate_form_attachments(
schema: Any, form_data: Optional[Dict[str, Any]]
) -> List[Dict[str, Any]]:
data = form_data or {}
items: List[Dict[str, Any]] = []
for rule in _schema_rules(schema):
t = str(rule.get("type") or "")
if t not in UPLOAD_TYPES:
continue
field = str(rule.get("field") or rule.get("name") or "").strip()
if not field:
continue
label = str(rule.get("title") or rule.get("label") or field).strip() or field
raw = data.get(field)
files: List[Dict[str, str]] = []
if isinstance(raw, list):
for it in raw:
if isinstance(it, dict):
u = (_extract_upload_urls(it) or [""])[0]
if not u:
continue
files.append(
{
"url": u,
"fileName": _upload_display_name(it, u),
}
)
else:
for u in _extract_upload_urls(it):
files.append({"url": u, "fileName": _upload_display_name(it, u)})
else:
for u in _extract_upload_urls(raw):
files.append({"url": u, "fileName": _upload_display_name(raw, u)})
if not files:
continue
items.append({"fieldKey": field, "fieldLabel": label, "files": files})
return items
def _allowed_url_index(
schema: Any, form_data: Optional[Dict[str, Any]]
) -> Dict[str, Dict[str, str]]:
"""url -> {fieldKey, fieldLabel, fileName}"""
index: Dict[str, Dict[str, str]] = {}
for group in enumerate_form_attachments(schema, form_data):
for f in group["files"]:
u = str(f.get("url") or "").strip()
if not u:
continue
index[u] = {
"fieldKey": group["fieldKey"],
"fieldLabel": group["fieldLabel"],
"fileName": str(f.get("fileName") or "").strip() or u,
}
return index
def normalize_attachment_refs(
schema: Any,
form_data: Optional[Dict[str, Any]],
refs: Optional[List[Dict[str, Any]]],
) -> List[Dict[str, str]]:
if not refs:
return []
if not isinstance(refs, list):
raise OaEngineError("attachmentRefs 格式无效")
index = _allowed_url_index(schema, form_data)
out: List[Dict[str, str]] = []
seen = set()
for raw in refs:
if not isinstance(raw, dict):
raise OaEngineError("引用的附件不存在或不属于本表单")
url = str(raw.get("url") or "").strip()
if not url or url not in index:
raise OaEngineError("引用的附件不存在或不属于本表单")
if url in seen:
continue
seen.add(url)
meta = index[url]
out.append(
{
"fieldKey": meta["fieldKey"],
"fieldLabel": meta["fieldLabel"],
"url": url,
"fileName": meta["fileName"],
}
)
return out
def _fmt_time(dt: Optional[datetime]) -> str:
return dt.strftime("%Y-%m-%d %H:%M:%S") if dt else ""
def _comment_to_dict(
row: OaInstanceComment,
*,
name_map: Dict[str, str],
viewer_user_id: str,
instance_status: str,
) -> Dict[str, Any]:
refs = _loads(row.attachment_refs_json, [])
if not isinstance(refs, list):
refs = []
status = (row.status or "normal").strip() or "normal"
can_revoke = (
status == "normal"
and instance_status == "running"
and (row.create_by or "") == (viewer_user_id or "")
)
return {
"id": row.id,
"instanceId": row.instance_id,
"content": row.content or "",
"attachmentRefs": refs,
"status": status,
"revokedTime": _fmt_time(row.revoked_time),
"createBy": row.create_by or "",
"createByName": name_map.get(row.create_by or "", row.create_by or ""),
"createTime": _fmt_time(row.create_time),
"canRevoke": can_revoke,
}
def _load_version_schema(db: Session, inst: OaInstance) -> Any:
ver = (
db.query(OaAppVersion)
.filter(OaAppVersion.id == inst.app_version_id, OaAppVersion.del_flag == 0)
.first()
)
if not ver:
return {}
return _loads(ver.form_schema_json, {})
def list_comments(
db: Session,
*,
instance_id: str,
tenant_no: str,
viewer_user_id: str,
) -> List[Dict[str, Any]]:
inst = _get_instance(db, instance_id, tenant_no)
rows = (
db.query(OaInstanceComment)
.filter(
OaInstanceComment.instance_id == instance_id,
OaInstanceComment.tenant_no == tenant_no,
OaInstanceComment.del_flag == 0,
)
.order_by(OaInstanceComment.create_time.asc())
.all()
)
name_map = _user_name_map(db, [r.create_by for r in rows if r.create_by])
return [
_comment_to_dict(
r,
name_map=name_map,
viewer_user_id=viewer_user_id,
instance_status=inst.status or "",
)
for r in rows
]
def list_form_attachments(
db: Session, *, instance_id: str, tenant_no: str
) -> List[Dict[str, Any]]:
inst = _get_instance(db, instance_id, tenant_no)
schema = _load_version_schema(db, inst)
form = _loads(inst.form_data_json, {})
if not isinstance(form, dict):
form = {}
return enumerate_form_attachments(schema, form)
def create_comment(
db: Session,
*,
instance_id: str,
tenant_no: str,
user_id: str,
dept_id: str,
content: str,
attachment_refs: Optional[List[Dict[str, Any]]],
) -> Dict[str, Any]:
inst = _get_instance(db, instance_id, tenant_no)
if (inst.status or "") != "running":
raise OaEngineError("仅审批进行中可评论")
text = (content or "").strip()
if not text:
raise OaEngineError("评论内容不能为空")
if len(text) > CONTENT_MAX:
raise OaEngineError(f"评论内容不能超过{CONTENT_MAX}字")
schema = _load_version_schema(db, inst)
form = _loads(inst.form_data_json, {})
if not isinstance(form, dict):
form = {}
refs = normalize_attachment_refs(schema, form, attachment_refs)
now = datetime.now()
row = OaInstanceComment(
id=generate_uuid(),
instance_id=instance_id,
content=text,
attachment_refs_json=json.dumps(refs, ensure_ascii=False) if refs else None,
status="normal",
revoked_time=None,
tenant_no=tenant_no,
create_by=user_id,
create_dept_id=dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(row)
db.commit()
db.refresh(row)
name_map = _user_name_map(db, [user_id])
return _comment_to_dict(
row,
name_map=name_map,
viewer_user_id=user_id,
instance_status=inst.status or "",
)
def revoke_comment(
db: Session,
*,
instance_id: str,
comment_id: str,
tenant_no: str,
user_id: str,
) -> Dict[str, Any]:
inst = _get_instance(db, instance_id, tenant_no)
if (inst.status or "") != "running":
raise OaEngineError("仅审批进行中可撤回评论")
row = (
db.query(OaInstanceComment)
.filter(
OaInstanceComment.id == comment_id,
OaInstanceComment.instance_id == instance_id,
OaInstanceComment.tenant_no == tenant_no,
OaInstanceComment.del_flag == 0,
)
.first()
)
if not row:
raise OaEngineError("评论不存在")
if (row.create_by or "") != (user_id or ""):
raise OaEngineError("只能撤回自己的评论")
now = datetime.now()
if (row.status or "") != "revoked":
row.status = "revoked"
row.revoked_time = now
row.update_by = user_id
row.update_time = now
db.add(row)
db.commit()
db.refresh(row)
name_map = _user_name_map(db, [row.create_by] if row.create_by else [])
return _comment_to_dict(
row,
name_map=name_map,
viewer_user_id=user_id,
instance_status=inst.status or "",
)