feat(backend): CC task read status in oa_engine.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
4d72231ea3
commit
7f8c163c47
2 files changed
+184
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user