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