Files
DadaoTask/web/e2e/shots-notif.mjs
T
Steven 91b3dde670
CI / backend (push) Canceled after 0s
feat(notify): 站内通知与手工开单,指派/改状态/评论/验收都有人收到
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
2026-10-30 02:19:42 +08:00

51 lines
2.0 KiB
JavaScript

// 只为截图:登录一个已有通知的账号,截「通知页」与「新建工单」面板。
import { chromium } from "playwright-core";
const BASE = "http://127.0.0.1:5180";
const API = "http://127.0.0.1:8100/api/v1";
const CHROME = "C:/Users/Steven/AppData/Local/ms-playwright/chromium-1223/chrome-win64/chrome.exe";
const login = async (phone) => {
const send = await fetch(`${API}/auth/sms/send`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone }),
}).then((r) => r.json());
const code = send.data?.dev_code ?? "123456";
const r = await fetch(`${API}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone, code }),
}).then((x) => x.json());
return r.data;
};
const browser = await chromium.launch({ executablePath: CHROME, args: ["--no-sandbox"] });
const page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
const auth = await login("13900009002");
const me = await fetch(`${API}/auth/me`, {
headers: { Authorization: `Bearer ${auth.tokens.access_token}` },
}).then((r) => r.json());
const ws = me.data.workspaces[0];
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
await page.evaluate(
([a, r]) => {
localStorage.setItem("dadao.access", a);
localStorage.setItem("dadao.refresh", r);
},
[auth.tokens.access_token, auth.tokens.refresh_token],
);
await page.goto(`${BASE}/w/${ws.id}/notifications`, { waitUntil: "domcontentloaded" });
await page.waitForSelector("ul li", { timeout: 20000 });
await page.screenshot({ path: "e2e/artifacts/ui-notifications.png" });
await page.goto(`${BASE}/w/${ws.id}/tickets`, { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "新建工单" }).click();
await page.waitForSelector("text=交给谁", { timeout: 15000 });
await page.screenshot({ path: "e2e/artifacts/ui-ticket-new.png" });
console.log("done", ws.name);
await browser.close();