From 7f8c163c47c545cd23ed66479278a4ccaaebe91b Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Thu, 6 Aug 2026 12:08:31 +0800 Subject: [PATCH] feat(backend): CC task read status in oa_engine. Co-authored-by: Cursor --- backend/app/services/oa_engine.py | 101 ++++++++++++++++++++++++++++++ backend/tests/test_oa_cc_read.py | 83 ++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 backend/tests/test_oa_cc_read.py diff --git a/backend/app/services/oa_engine.py b/backend/app/services/oa_engine.py index 52b19e55..abc38548 100644 --- a/backend/app/services/oa_engine.py +++ b/backend/app/services/oa_engine.py @@ -829,6 +829,12 @@ def _task_to_dict(t: OaTask, *, assignee_user_name: str = "") -> Dict[str, Any]: "commentText": t.comment_text or "", "resolveSource": t.resolve_source or "", "signMode": t.sign_mode or "", + "isRead": bool(int(getattr(t, "is_read", 0) or 0)), + "readTime": ( + t.read_time.strftime("%Y-%m-%d %H:%M:%S") + if getattr(t, "read_time", None) + else "" + ), "createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "", "updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "", } @@ -1479,6 +1485,8 @@ def _create_tasks_for_node( comment_text=None, resolve_source=resolve_source, sign_mode=sign_mode if task_type == "approval" else None, + is_read=0, + read_time=None, round_no=int(getattr(inst, "current_round", None) or 1), tenant_no=inst.tenant_no, order_num=seq, @@ -1928,12 +1936,99 @@ def get_instance_detail(db: Session, instance_id: str, *, tenant_no: str) -> Dic return data +def _normalize_cc_read_status(raw: Optional[str]) -> str: + v = (raw or "").strip().lower() or "unread" + if v not in ("unread", "read", "all"): + raise OaEngineError("readStatus 仅支持 unread、read、all") + return v + + +def _apply_cc_task_read( + db: Session, + *, + task: OaTask, + user_id: str, + now: datetime, +) -> bool: + """将抄送任务标为已读。返回是否发生了写入。幂等时返回 False。""" + if int(getattr(task, "is_read", 0) or 0) == 1: + return False + task.is_read = 1 + task.read_time = now + task.update_by = user_id + task.update_time = now + db.add(task) + return True + + +def count_cc_unread(db: Session, *, tenant_no: str, user_id: str) -> int: + return ( + db.query(OaTask) + .filter( + OaTask.tenant_no == tenant_no, + OaTask.del_flag == 0, + OaTask.assignee_user_id == user_id, + OaTask.task_type == "cc", + OaTask.is_read == 0, + ) + .count() + ) + + +def mark_cc_task_read( + db: Session, *, tenant_no: str, user_id: str, task_id: str +) -> Dict[str, Any]: + task = ( + db.query(OaTask) + .filter( + OaTask.id == task_id, + OaTask.tenant_no == tenant_no, + OaTask.del_flag == 0, + ) + .first() + ) + if not task or task.task_type != "cc" or task.assignee_user_id != user_id: + raise OaEngineError("抄送任务不存在或无权操作") + now = datetime.now() + _apply_cc_task_read(db, task=task, user_id=user_id, now=now) + db.commit() + db.refresh(task) + return _task_to_dict(task) + + +def mark_cc_tasks_read_batch( + db: Session, *, tenant_no: str, user_id: str, ids: List[str] +) -> Dict[str, Any]: + id_list = [x.strip() for x in (ids or []) if (x or "").strip()] + if not id_list: + return {"updated": 0} + rows = ( + db.query(OaTask) + .filter( + OaTask.tenant_no == tenant_no, + OaTask.del_flag == 0, + OaTask.assignee_user_id == user_id, + OaTask.task_type == "cc", + OaTask.id.in_(id_list), + ) + .all() + ) + now = datetime.now() + updated = 0 + for t in rows: + if _apply_cc_task_read(db, task=t, user_id=user_id, now=now): + updated += 1 + db.commit() + return {"updated": updated} + + def list_tasks( db: Session, *, tenant_no: str, user_id: str, kind: str, + read_status: Optional[str] = None, ) -> List[Dict[str, Any]]: kind = (kind or "").strip() if kind == "pending": @@ -1959,6 +2054,12 @@ def list_tasks( OaTask.assignee_user_id == user_id, OaTask.task_type == "cc", ) + rs = _normalize_cc_read_status(read_status) + if rs == "unread": + q = q.filter(OaTask.is_read == 0) + elif rs == "read": + q = q.filter(OaTask.is_read == 1) + # all: 不加 is_read 条件 elif kind == "mine": insts = ( db.query(OaInstance) diff --git a/backend/tests/test_oa_cc_read.py b/backend/tests/test_oa_cc_read.py new file mode 100644 index 00000000..5555ca10 --- /dev/null +++ b/backend/tests/test_oa_cc_read.py @@ -0,0 +1,83 @@ +"""OA 抄送已读:序列化、筛选语义、标记幂等""" +import unittest +from datetime import datetime +from types import SimpleNamespace +from unittest.mock import MagicMock + +from app.services import oa_engine as eng + + +class TestTaskToDictIsRead(unittest.TestCase): + def test_includes_is_read_and_read_time(self): + t = SimpleNamespace( + id="t1", + instance_id="i1", + node_key="n1", + node_name="抄送", + task_type="cc", + assignee_user_id="u1", + status="done", + comment_text=None, + resolve_source=None, + sign_mode=None, + is_read=1, + read_time=datetime(2026, 8, 6, 12, 0, 0), + create_time=datetime(2026, 8, 6, 10, 0, 0), + update_time=datetime(2026, 8, 6, 11, 0, 0), + ) + d = eng._task_to_dict(t) + self.assertEqual(d["isRead"], True) + self.assertEqual(d["readTime"], "2026-08-06 12:00:00") + + def test_unread_defaults(self): + t = SimpleNamespace( + id="t2", + instance_id="i1", + node_key="n1", + node_name="抄送", + task_type="cc", + assignee_user_id="u1", + status="done", + comment_text=None, + resolve_source=None, + sign_mode=None, + is_read=0, + read_time=None, + create_time=None, + update_time=None, + ) + d = eng._task_to_dict(t) + self.assertEqual(d["isRead"], False) + self.assertEqual(d["readTime"], "") + + +class TestMarkCcReadHelpers(unittest.TestCase): + def test_normalize_read_status(self): + self.assertEqual(eng._normalize_cc_read_status(None), "unread") + self.assertEqual(eng._normalize_cc_read_status(""), "unread") + self.assertEqual(eng._normalize_cc_read_status("all"), "all") + self.assertEqual(eng._normalize_cc_read_status("read"), "read") + with self.assertRaises(eng.OaEngineError): + eng._normalize_cc_read_status("weird") + + def test_mark_idempotent_keeps_read_time(self): + first = datetime(2026, 8, 1, 9, 0, 0) + task = SimpleNamespace( + id="t1", + task_type="cc", + assignee_user_id="u1", + del_flag=0, + is_read=1, + read_time=first, + update_by=None, + update_time=None, + ) + db = MagicMock() + out = eng._apply_cc_task_read(db, task=task, user_id="u1", now=datetime(2026, 8, 6, 12, 0, 0)) + self.assertFalse(out) # 未发生更新 + self.assertEqual(task.read_time, first) + self.assertEqual(task.is_read, 1) + + +if __name__ == "__main__": + unittest.main()