M2 最后一块:让"该我办了"能出现在系统里,并且允许不经过任务直接开单。 通知(backend/app/services/notify.py 统一出口) - 七类触发:task_assigned、task_status_changed、item_commented、 ticket_assigned、ticket_submitted、ticket_approved/rejected、task_unblocked - 不通知自己:notify() 按 actor_id 跳过,调用方不用各自判断 - 已读:GET /notifications、/unread-count、POST /read、/read-all 手工开单 - POST /tickets:直接指定人、初始 open,与任务升级单共用同一条 kind='ticket' item, ticket_ext.parent_item_id 为空,验收通过不回流任务 - 指派前 _ensure_member 校验在册成员,越界指派返回 1001(避免"挂在看不到的名字上") 前端 - Web:通知页 /w/:wsId/notifications(未读/全部,点开标已读并跳目标); 工单页"新建"面板;AppShell 每 30 秒对未读数,标已读后 dispatch notifications:changed 立刻刷新; 任务与工单支持 ?open= 深链落地即展开 - 安卓:五个 tab 加"通知",未读数 30 秒刷新;工单动作按需求人/办理人/管理员显隐;手工开单对话框 验证 - pytest 85 passed(新增 backend/tests/test_notifications.py 6 个用例) - ruff check / format --check 全 0 - tsc --noEmit 通过;vite build 成功 - web/e2e/notifications.mjs 12 项断言全绿(手工开单 → B 收到 → 点开跳转 → 未读归零) - 安卓:本机 JDK 25 与 Gradle 8.7 / AGP 8.7.3 不兼容,本次未做编译验证, 装回 JDK 17 后第一件事是 assembleDebug
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""站内通知(M2 起用,M4 会接入外部推送)。
|
||||
|
||||
三条原则:
|
||||
|
||||
1. **不通知自己**。绝大多数通知都是由自己的操作触发的,
|
||||
不排除自己会让"未读"迅速变成一个没人看的数字。
|
||||
2. **通知里写清"谁、做了什么、在哪件事上"**。只写"状态已更新"等于没写,
|
||||
用户还得点进去猜。
|
||||
3. **通知只是提示,不是凭证**。点开后仍要按 `item_id` 重新拉数据,
|
||||
因为通知生成之后事情可能又被改过。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from collections.abc import Iterable
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..models_ticket import Notification
|
||||
|
||||
TICKET_STATUS_CN = {
|
||||
"open": "待接收",
|
||||
"accepted": "待办理",
|
||||
"in_progress": "办理中",
|
||||
"pending_verify": "待验收",
|
||||
"done": "已完成",
|
||||
"canceled": "已取消",
|
||||
}
|
||||
|
||||
TASK_STATUS_CN = {
|
||||
"inbox": "收件箱",
|
||||
"todo": "待办",
|
||||
"doing": "进行中",
|
||||
"blocked": "被阻塞",
|
||||
"done": "已完成",
|
||||
"canceled": "已取消",
|
||||
}
|
||||
|
||||
|
||||
async def notify(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
ws_id: uuid.UUID,
|
||||
user_ids: Iterable[uuid.UUID | None],
|
||||
kind: str,
|
||||
item_id: uuid.UUID | None,
|
||||
title: str,
|
||||
body: str = "",
|
||||
actor_id: uuid.UUID | None = None,
|
||||
) -> int:
|
||||
"""给若干人各写一条通知,返回实际写入条数。
|
||||
|
||||
去重与"跳过自己"在这里统一做一次——每个调用点都手写一遍迟早漏一个。
|
||||
"""
|
||||
seen: set[uuid.UUID] = set()
|
||||
count = 0
|
||||
for user_id in user_ids:
|
||||
if user_id is None or user_id == actor_id or user_id in seen:
|
||||
continue
|
||||
seen.add(user_id)
|
||||
session.add(
|
||||
Notification(
|
||||
workspace_id=ws_id,
|
||||
user_id=user_id,
|
||||
kind=kind,
|
||||
item_id=item_id,
|
||||
title=title[:120],
|
||||
body=body[:300],
|
||||
)
|
||||
)
|
||||
count += 1
|
||||
return count
|
||||
Reference in New Issue
Block a user