Selaa lähdekoodia

feat(oa): enumerate and validate comment form attachment refs

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 kuukausi sitten
vanhempi
commit
ea8420b648

+ 110 - 0
backend/app/services/oa_instance_comment.py

@@ -0,0 +1,110 @@
+"""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

+ 63 - 0
backend/tests/test_oa_instance_comment.py

@@ -0,0 +1,63 @@
+"""OA 实例评论:附件枚举与引用校验"""
+import unittest
+
+from app.services.oa_instance_comment import (
+    enumerate_form_attachments,
+    normalize_attachment_refs,
+)
+
+
+class TestEnumerateFormAttachments(unittest.TestCase):
+    def test_groups_upload_fields(self):
+        schema = {
+            "rule": [
+                {
+                    "type": "upload",
+                    "field": "files",
+                    "title": "附件",
+                },
+                {"type": "input", "field": "name", "title": "名称"},
+            ]
+        }
+        form = {
+            "files": [
+                {"url": "https://x/a.pdf", "name": "a.pdf"},
+                {"url": "https://x/b.pdf", "name": "b.pdf"},
+            ],
+            "name": "demo",
+        }
+        items = enumerate_form_attachments(schema, form)
+        self.assertEqual(len(items), 1)
+        self.assertEqual(items[0]["fieldKey"], "files")
+        self.assertEqual(items[0]["fieldLabel"], "附件")
+        self.assertEqual(len(items[0]["files"]), 2)
+        self.assertEqual(items[0]["files"][0]["fileName"], "a.pdf")
+
+    def test_skips_empty_upload(self):
+        schema = {"rule": [{"type": "fcUpload", "field": "f", "title": "F"}]}
+        self.assertEqual(enumerate_form_attachments(schema, {"f": []}), [])
+
+
+class TestNormalizeAttachmentRefs(unittest.TestCase):
+    def test_accepts_valid_ref(self):
+        schema = {"rule": [{"type": "upload", "field": "files", "title": "附件"}]}
+        form = {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]}
+        out = normalize_attachment_refs(
+            schema,
+            form,
+            [{"fieldKey": "files", "url": "https://x/a.pdf", "fileName": "wrong"}],
+        )
+        self.assertEqual(out[0]["fileName"], "a.pdf")
+        self.assertEqual(out[0]["fieldLabel"], "附件")
+
+    def test_rejects_foreign_url(self):
+        from app.services.oa_engine import OaEngineError
+
+        schema = {"rule": [{"type": "upload", "field": "files", "title": "附件"}]}
+        form = {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]}
+        with self.assertRaises(OaEngineError):
+            normalize_attachment_refs(
+                schema,
+                form,
+                [{"fieldKey": "files", "url": "https://evil/x.pdf"}],
+            )