feat(notify): 站内通知补齐完成、到期逾期与知识事件
CI / backend (push) Waiting to run

This commit is contained in:
Steven
2026-11-10 13:47:56 +08:00
parent 4b7627d70e
commit f54df70811
15 changed files with 791 additions and 32 deletions
+58 -10
View File
@@ -205,6 +205,48 @@ async def _apply_update(
return item
async def _notify_status_change(
session: AsyncSession,
*,
ws_id: uuid.UUID,
item: Item,
previous: str,
to_status: str,
actor_id: uuid.UUID,
actor_name: str,
) -> None:
"""状态变化的通知。
"完成"单独一种 kind:收件人一眼能看出是结项,而不是又一次普通的状态流转;
其余状态仍走 `task_status_changed`,正文里写清从什么改成了什么。
"""
if to_status == "done":
await notify(
session,
ws_id=ws_id,
user_ids=(item.creator_id, item.assignee_id),
kind="task_completed",
item_id=item.id,
title=f"「{item.title}」已完成",
body=f"{actor_name} 把它标记为完成。",
actor_id=actor_id,
)
return
await notify(
session,
ws_id=ws_id,
user_ids=(item.creator_id, item.assignee_id),
kind="task_status_changed",
item_id=item.id,
title=f"「{item.title}」变成{TASK_STATUS_CN.get(to_status, to_status)}了",
body=(
f"{actor_name} 把它从 {TASK_STATUS_CN.get(previous, previous)} "
f"改成了 {TASK_STATUS_CN.get(to_status, to_status)}"
),
actor_id=actor_id,
)
# ── 列表 ────────────────────────────────────────────────────────────────
def _apply_filters(
stmt: Select,
@@ -581,6 +623,17 @@ async def batch_status(body: BatchStatusIn, ctx: MemberDep, session: SessionDep)
}
)
continue
# 批量改状态以前是"悄悄改完":被指派人/创建人收不到任何通知,
# 和单条改状态的行为不一致。这里补上同一条通知。
await _notify_status_change(
session,
ws_id=ctx.ws_id,
item=item,
previous=previous,
to_status=body.status,
actor_id=ctx.user.id,
actor_name=ctx.user.nickname or "同事",
)
updated.append({"id": str(item.id), "version": item.version, "unchanged": False})
return ok(
@@ -682,19 +735,14 @@ async def change_status(
payload={"from": previous, "to": body.status},
)
tags = await _tags_of(session, ctx.ws_id, [item.id])
await notify(
await _notify_status_change(
session,
ws_id=ctx.ws_id,
user_ids=(item.creator_id, item.assignee_id),
kind="task_status_changed",
item_id=item.id,
title=f"「{item.title}」变成{TASK_STATUS_CN.get(body.status, body.status)}了",
body=(
f"{ctx.user.nickname or '同事'} 把它从 "
f"{TASK_STATUS_CN.get(previous, previous)} 改成了 "
f"{TASK_STATUS_CN.get(body.status, body.status)}"
),
item=item,
previous=previous,
to_status=body.status,
actor_id=ctx.user.id,
actor_name=ctx.user.nickname or "同事",
)
return ok(_item_out(item, tags.get(item.id)), message="状态已更新")
+58
View File
@@ -45,6 +45,7 @@ from ..schemas import clamp_limit, decode_cursor, encode_cursor, ok
from ..serializers import item_out, tags_of
from ..services.knowledge import create_draft_from_source, find_doc_by_source
from ..services.knowledge_index import reindex_document, search_knowledge
from ..services.notify import notify, workspace_admins
from .attachments import count_for_items, list_for_item
router = APIRouter(prefix="/workspaces/{ws_id}", tags=["知识库"])
@@ -1025,6 +1026,17 @@ async def create_knowledge(body: KnowledgeIn, ctx: MemberDep, session: SessionDe
await session.flush()
await _log(session, item, ctx.user.id, "knowledge_created", {"generated_by": "human"})
await _set_tags_for(session, ws_id, item, body.tag_names)
# 知识是团队资产:新建草稿先告诉能发布的人,别让草稿在列表里躺到没人发现。
await notify(
session,
ws_id=ws_id,
user_ids=await workspace_admins(session, ws_id),
kind="knowledge_draft_created",
item_id=item.id,
title=f"新的知识草稿:{item.title}",
body=f"{ctx.user.nickname or '同事'} 新建了一条知识,核对后可以发布。",
actor_id=ctx.user.id,
)
await audit_record(
session,
workspace_id=ws_id,
@@ -1204,6 +1216,42 @@ async def change_status(
target_id=str(item.id),
payload={"to": to_status},
)
# 知识的状态变化要让相关的人知道:提交审议找管理员核对,
# 发布/归档则回告作者——否则作者不知道自己的草稿已经被团队采纳。
actor_name = ctx.user.nickname or "同事"
if body.action == "submit_review":
await notify(
session,
ws_id=ws_id,
user_ids=await workspace_admins(session, ws_id),
kind="knowledge_review_requested",
item_id=item.id,
title=f"待核对:{item.title}",
body=f"{actor_name} 提交了一份知识,核对后可以发布。",
actor_id=ctx.user.id,
)
elif body.action == "publish":
await notify(
session,
ws_id=ws_id,
user_ids=(doc.author_id,),
kind="knowledge_published",
item_id=item.id,
title=f"「{item.title}」已发布",
body=f"{actor_name} 发布了这份知识,现在可以被团队检索到。",
actor_id=ctx.user.id,
)
elif body.action == "archive":
await notify(
session,
ws_id=ws_id,
user_ids=(doc.author_id,),
kind="knowledge_archived",
item_id=item.id,
title=f"「{item.title}」已归档",
body=f"{actor_name} 把它归档了,检索不再返回它。",
actor_id=ctx.user.id,
)
await session.flush()
if body.action == "publish":
# 发布后立刻建索引:检索只认 published,不在这里建就要等定时任务,
@@ -1270,6 +1318,16 @@ async def promote_to_knowledge(
await _log(
session, doc_item, ctx.user.id, "knowledge_created", {"source_item_id": str(source.id)}
)
await notify(
session,
ws_id=ws_id,
user_ids=await workspace_admins(session, ws_id),
kind="knowledge_draft_created",
item_id=doc_item.id,
title=f"从事项沉淀了知识:{doc_item.title}",
body=f"{ctx.user.nickname or '同事'} 把「{source.title}」沉淀成草稿,核对后可以发布。",
actor_id=ctx.user.id,
)
await audit_record(
session,
workspace_id=ws_id,
+16 -4
View File
@@ -767,11 +767,14 @@ async def delete_link(
# -- 通知 ---------------------------------------------------------------
def _notification_out(n: Notification, item_title: str | None = None) -> dict[str, Any]:
def _notification_out(
n: Notification, item_title: str | None = None, item_kind: str | None = None
) -> dict[str, Any]:
return {
"id": str(n.id),
"kind": n.kind,
"item_id": str(n.item_id) if n.item_id else None,
"item_kind": item_kind,
"item_title": item_title,
"title": n.title,
"body": n.body,
@@ -813,18 +816,27 @@ async def list_notifications(
rows = list(rows[:limit])
titles: dict[uuid.UUID, str] = {}
kinds: dict[uuid.UUID, str] = {}
item_ids = [n.item_id for n in rows if n.item_id]
if item_ids:
for item_id, title in (
await session.execute(select(Item.id, Item.title).where(Item.id.in_(item_ids)))
for item_id, title, kind in (
await session.execute(
select(Item.id, Item.title, Item.kind).where(Item.id.in_(item_ids))
)
).all():
titles[item_id] = title
kinds[item_id] = kind
next_cursor = encode_cursor(rows[-1].created_at, rows[-1].id) if has_more and rows else None
return ok(
{
"notifications": [
_notification_out(n, titles.get(n.item_id) if n.item_id else None) for n in rows
_notification_out(
n,
titles.get(n.item_id) if n.item_id else None,
kinds.get(n.item_id) if n.item_id else None,
)
for n in rows
],
"next_cursor": next_cursor,
"has_more": has_more,