diff --git a/backend/app/services/oa_instance_comment.py b/backend/app/services/oa_instance_comment.py index facaecb1..3a71215d 100644 --- a/backend/app/services/oa_instance_comment.py +++ b/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 "", + ) diff --git a/backend/tests/test_oa_instance_comment.py b/backend/tests/test_oa_instance_comment.py index b50cb2fd..c1a3f8b4 100644 --- a/backend/tests/test_oa_instance_comment.py +++ b/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, + )