Explorar o código

feat(oa): add instance comment create list and revoke service

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu hai 1 mes
pai
achega
c66bfc75c0

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

@@ -108,3 +108,183 @@ def normalize_attachment_refs(
             }
         )
     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 "",
+    )

+ 137 - 0
backend/tests/test_oa_instance_comment.py

@@ -1,5 +1,7 @@
 """OA 实例评论:附件枚举与引用校验"""
+import json
 import unittest
+from datetime import datetime
 
 from app.services.oa_instance_comment import (
     enumerate_form_attachments,
@@ -61,3 +63,138 @@ class TestNormalizeAttachmentRefs(unittest.TestCase):
                 form,
                 [{"fieldKey": "files", "url": "https://evil/x.pdf"}],
             )
+
+
+class TestCommentCrud(unittest.TestCase):
+    def setUp(self):
+        from sqlalchemy import create_engine
+        from sqlalchemy.orm import sessionmaker
+        from app.models.models import OaAppVersion, OaInstance, OaInstanceComment, SysUser
+
+        self.engine = create_engine("sqlite:///:memory:")
+        OaAppVersion.__table__.create(self.engine)
+        OaInstance.__table__.create(self.engine)
+        OaInstanceComment.__table__.create(self.engine)
+        SysUser.__table__.create(self.engine)
+        Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
+        self.db = Session()
+        now = datetime.now()
+        schema = {
+            "rule": [
+                {"type": "upload", "field": "files", "title": "附件"},
+            ]
+        }
+        self.db.add(
+            OaAppVersion(
+                id="v1",
+                app_id="app1",
+                version_no=1,
+                status="published",
+                is_latest_published="1",
+                form_schema_json=json.dumps(schema, ensure_ascii=False),
+                flow_tree_json="{}",
+                advanced_settings_json="{}",
+                tenant_no="zkxw",
+                create_dept_id="",
+                create_time=now,
+                update_time=now,
+                del_flag=0,
+            )
+        )
+        self.db.add(
+            OaInstance(
+                id="i1",
+                app_id="app1",
+                app_version_id="v1",
+                title="t",
+                status="running",
+                initiator_user_id="u0",
+                initiator_dept_id="d1",
+                form_data_json=json.dumps(
+                    {"files": [{"url": "https://x/a.pdf", "name": "a.pdf"}]},
+                    ensure_ascii=False,
+                ),
+                tenant_no="zkxw",
+                create_dept_id="d1",
+                create_time=now,
+                update_time=now,
+                del_flag=0,
+                current_round=1,
+            )
+        )
+        self.db.add(
+            SysUser(
+                id="u1",
+                tenant_no="zkxw",
+                user_no="u1",
+                nick_name="张三",
+                email="",
+                phone_no="",
+                password="",
+                status="1",
+                create_dept_id="",
+                del_flag=0,
+                create_time=now,
+                update_time=now,
+            )
+        )
+        self.db.commit()
+
+    def test_create_list_revoke(self):
+        from app.services import oa_instance_comment as svc
+
+        row = svc.create_comment(
+            self.db,
+            instance_id="i1",
+            tenant_no="zkxw",
+            user_id="u1",
+            dept_id="d1",
+            content="请看附件",
+            attachment_refs=[{"fieldKey": "files", "url": "https://x/a.pdf"}],
+        )
+        self.assertEqual(row["status"], "normal")
+        self.assertTrue(row["canRevoke"])
+        self.assertEqual(len(row["attachmentRefs"]), 1)
+
+        listed = svc.list_comments(
+            self.db, instance_id="i1", tenant_no="zkxw", viewer_user_id="u1"
+        )
+        self.assertEqual(len(listed), 1)
+
+        revoked = svc.revoke_comment(
+            self.db,
+            instance_id="i1",
+            comment_id=row["id"],
+            tenant_no="zkxw",
+            user_id="u1",
+        )
+        self.assertEqual(revoked["status"], "revoked")
+        self.assertFalse(revoked["canRevoke"])
+
+        again = svc.revoke_comment(
+            self.db,
+            instance_id="i1",
+            comment_id=row["id"],
+            tenant_no="zkxw",
+            user_id="u1",
+        )
+        self.assertEqual(again["status"], "revoked")
+
+    def test_cannot_comment_when_not_running(self):
+        from app.services import oa_instance_comment as svc
+        from app.services.oa_engine import OaEngineError
+        from app.models.models import OaInstance
+
+        inst = self.db.query(OaInstance).filter(OaInstance.id == "i1").one()
+        inst.status = "completed"
+        self.db.commit()
+        with self.assertRaises(OaEngineError):
+            svc.create_comment(
+                self.db,
+                instance_id="i1",
+                tenant_no="zkxw",
+                user_id="u1",
+                dept_id="d1",
+                content="x",
+                attachment_refs=None,
+            )