feat(backend): CC task read status in oa_engine.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-06 12:08:31 +08:00
1 parent 4d72231ea3
commit 7f8c163c47
2 files changed
+184

No files matched your search

+101
View File
@@ -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)
+83
View File
@@ -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()