feat: 任务模型物理合并,发布+回收并为单任务双阶段
- tasks 新增 recovery_enabled/recovery_due_at/recovery_instructions/recovery_example_images,回收挂自己名下(先发布后交截图),不再置 complete - 存量 screenshot_collect 迁移并入父任务后删除(mysql/0020 含 DELETE,生产执行前务必备份) - 新 action task_enable_recovery:ghost 面板一键开通回收,截止默认=发布截止 - 企微催办双阶段(待发布/截图待提交);超期自动释放仅发布阶段 - admin 删 groupTaskMasters 聚合改单卡双面板;portal 详情页回收截图区块+批次回收进度 - 测试 309 全绿 + Playwright E2E 20/20
This commit is contained in:
@@ -105,11 +105,10 @@ function fakeProjectDb({ projects = [], tasks = [], partners = [] } = {}) {
|
||||
brand: params[2],
|
||||
main_group_member_ids: params[3],
|
||||
owner_userid: params[4],
|
||||
parent_id: params[5],
|
||||
main_chat_id: "",
|
||||
status: "active",
|
||||
note: params[6],
|
||||
created_at: params[7],
|
||||
note: params[5],
|
||||
created_at: params[6],
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
@@ -119,7 +118,7 @@ function fakeProjectDb({ projects = [], tasks = [], partners = [] } = {}) {
|
||||
return row ? 1 : 0;
|
||||
}
|
||||
if (/^UPDATE projects/.test(normalized)) {
|
||||
const row = projects.find((item) => item.id === params[6]);
|
||||
const row = projects.find((item) => item.id === params[5]);
|
||||
if (!row) return 0;
|
||||
Object.assign(row, {
|
||||
name: params[0],
|
||||
@@ -127,7 +126,6 @@ function fakeProjectDb({ projects = [], tasks = [], partners = [] } = {}) {
|
||||
main_group_member_ids: params[2],
|
||||
owner_userid: params[3],
|
||||
note: params[4],
|
||||
parent_id: params[5],
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
@@ -172,11 +170,6 @@ function fakeProjectDb({ projects = [], tasks = [], partners = [] } = {}) {
|
||||
},
|
||||
async first() {
|
||||
executed.push({ sql, params });
|
||||
if (/COUNT\(\*\) AS count FROM projects WHERE parent_id = \?/.test(sql)) {
|
||||
return {
|
||||
count: projects.filter((item) => item.parent_id === params[0]).length,
|
||||
};
|
||||
}
|
||||
if (/FROM projects WHERE id = \?/.test(sql)) {
|
||||
return projects.find((item) => item.id === params[0]) ?? null;
|
||||
}
|
||||
@@ -196,22 +189,11 @@ function fakeProjectDb({ projects = [], tasks = [], partners = [] } = {}) {
|
||||
},
|
||||
async all() {
|
||||
executed.push({ sql, params });
|
||||
if (/^SELECT id, parent_id FROM projects/.test(sql.trim())) {
|
||||
return {
|
||||
results: projects.map((item) => ({
|
||||
id: item.id,
|
||||
parent_id: item.parent_id ?? "",
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (/FROM projects p/.test(sql)) {
|
||||
const ownerFiltered = /WHERE p\.owner_userid = \?/.test(sql);
|
||||
const parentFiltered = /WHERE p\.parent_id = \?/.test(sql);
|
||||
let rows = projects;
|
||||
if (ownerFiltered) {
|
||||
rows = rows.filter((item) => item.owner_userid === params[0]);
|
||||
} else if (parentFiltered) {
|
||||
rows = rows.filter((item) => item.parent_id === params[0]);
|
||||
}
|
||||
return {
|
||||
results: rows.map((item) => ({
|
||||
@@ -220,22 +202,15 @@ function fakeProjectDb({ projects = [], tasks = [], partners = [] } = {}) {
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (/FROM tasks WHERE project_id/.test(sql)) {
|
||||
if (/FROM tasks( t)? WHERE (t\.)?project_id/.test(sql)) {
|
||||
return {
|
||||
results: tasks
|
||||
.filter((task) => task.project_id === params[0])
|
||||
.map((task) => ({ ...task })),
|
||||
};
|
||||
}
|
||||
if (/FROM tasks t\s+LEFT JOIN projects pj/.test(sql)) {
|
||||
const wanted = new Set(params);
|
||||
return {
|
||||
results: tasks
|
||||
.filter((task) => wanted.has(task.project_id))
|
||||
.map((task) => ({
|
||||
recovery_enabled: 1,
|
||||
recovery_due_at: "",
|
||||
recovery_submitted: 0,
|
||||
...task,
|
||||
project_name:
|
||||
projects.find((item) => item.id === task.project_id)?.name ?? "",
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -267,7 +242,6 @@ const projectRow = (overrides = {}) => ({
|
||||
brand: "",
|
||||
main_group_member_ids: "[]",
|
||||
owner_userid: "",
|
||||
parent_id: "",
|
||||
main_chat_id: "",
|
||||
status: "active",
|
||||
note: "",
|
||||
@@ -436,6 +410,9 @@ test("getProjectDetail returns tasks with owner and mainChat members", async ()
|
||||
project_id: "proj-1",
|
||||
owner_userid: "owner-a",
|
||||
task_chat_id: "tgc_t1",
|
||||
recovery_enabled: 0,
|
||||
recovery_due_at: "2026-08-10",
|
||||
recovery_submitted: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -444,6 +421,9 @@ test("getProjectDetail returns tasks with owner and mainChat members", async ()
|
||||
assert.equal(detail.project.mainChatId, "pgcproj1");
|
||||
assert.equal(detail.tasks[0].owner_userid, "owner-a");
|
||||
assert.equal(detail.tasks[0].task_chat_id, "tgc_t1");
|
||||
assert.equal(detail.tasks[0].recovery_enabled, 0);
|
||||
assert.equal(detail.tasks[0].recovery_due_at, "2026-08-10");
|
||||
assert.equal(detail.tasks[0].recovery_submitted, 2);
|
||||
assert.equal(detail.mainChat.chatId, "pgcproj1");
|
||||
assert.deepEqual(detail.mainChat.members, ["u1", "owner-a"]);
|
||||
});
|
||||
@@ -728,100 +708,3 @@ test("findOrCreateGroupOwnerPartner creates partner with userid fallback name",
|
||||
assert.equal(db.partners.length, 1);
|
||||
});
|
||||
|
||||
test("saveProject persists parentProjectId on create and update", async () => {
|
||||
const db = fakeProjectDb({
|
||||
projects: [projectRow({ id: "proj-root", name: "总项目" })],
|
||||
});
|
||||
await saveProject(db, {
|
||||
name: "子项目",
|
||||
brand: "b",
|
||||
mainGroupMemberIds: [],
|
||||
ownerUserid: "",
|
||||
parentProjectId: "proj-root",
|
||||
});
|
||||
assert.equal(db.projects.length, 2);
|
||||
const child = db.projects.find((item) => item.name === "子项目");
|
||||
assert.equal(child.parent_id, "proj-root");
|
||||
|
||||
await saveProject(db, {
|
||||
id: "proj-root",
|
||||
name: "总项目",
|
||||
mainGroupMemberIds: [],
|
||||
parentProjectId: "",
|
||||
});
|
||||
assert.equal(db.projects.find((item) => item.id === "proj-root").parent_id, "");
|
||||
});
|
||||
|
||||
test("saveProject rejects invalid parentProjectId (missing / self / descendant)", async () => {
|
||||
const db = fakeProjectDb({
|
||||
projects: [
|
||||
projectRow({ id: "proj-1", name: "一级" }),
|
||||
projectRow({ id: "proj-2", name: "二级", parent_id: "proj-1" }),
|
||||
projectRow({ id: "proj-3", name: "三级", parent_id: "proj-2" }),
|
||||
],
|
||||
});
|
||||
await assert.rejects(
|
||||
() => saveProject(db, { id: "proj-1", name: "一级", parentProjectId: "proj-404" }),
|
||||
(error) => error instanceof ProjectError && error.status === 404,
|
||||
);
|
||||
await assert.rejects(
|
||||
() => saveProject(db, { id: "proj-1", name: "一级", parentProjectId: "proj-1" }),
|
||||
(error) => error instanceof ProjectError && /自身/.test(error.message),
|
||||
);
|
||||
await assert.rejects(
|
||||
() => saveProject(db, { id: "proj-1", name: "一级", parentProjectId: "proj-3" }),
|
||||
(error) =>
|
||||
error instanceof ProjectError && /上级项目不能是当前项目的子项目/.test(error.message),
|
||||
);
|
||||
// 兄弟项目/父项目合法
|
||||
await saveProject(db, { id: "proj-3", name: "三级", parentProjectId: "proj-1" });
|
||||
assert.equal(db.projects.find((item) => item.id === "proj-3").parent_id, "proj-1");
|
||||
});
|
||||
|
||||
test("deleteProject refuses while child projects exist", async () => {
|
||||
const db = fakeProjectDb({
|
||||
projects: [
|
||||
projectRow({ id: "proj-1", name: "一级" }),
|
||||
projectRow({ id: "proj-2", name: "二级", parent_id: "proj-1" }),
|
||||
],
|
||||
});
|
||||
await assert.rejects(
|
||||
() => deleteProject(db, "proj-1"),
|
||||
(error) =>
|
||||
error instanceof ProjectError && /还有子项目/.test(error.message),
|
||||
);
|
||||
assert.equal(db.projects.length, 2);
|
||||
await deleteProject(db, "proj-2");
|
||||
await deleteProject(db, "proj-1");
|
||||
assert.equal(db.projects.length, 0);
|
||||
});
|
||||
|
||||
test("getProjectDetail returns subprojects and aggregated push tasks", async () => {
|
||||
const db = fakeProjectDb({
|
||||
projects: [
|
||||
projectRow({ id: "proj-1", name: "一级" }),
|
||||
projectRow({ id: "proj-2", name: "二级", parent_id: "proj-1", main_chat_id: "pgc2" }),
|
||||
projectRow({ id: "proj-3", name: "三级", parent_id: "proj-2" }),
|
||||
projectRow({ id: "proj-other", name: "无关项目" }),
|
||||
],
|
||||
tasks: [
|
||||
{ id: "t1", name: "直属任务", project_id: "proj-1" },
|
||||
{ id: "t2", name: "子项目任务", project_id: "proj-2" },
|
||||
{ id: "t3", name: "孙项目任务", project_id: "proj-3" },
|
||||
{ id: "t4", name: "无关任务", project_id: "proj-other" },
|
||||
],
|
||||
});
|
||||
const detail = await getProjectDetail(db, "proj-1");
|
||||
assert.equal(detail.project.parentProjectId, "");
|
||||
assert.deepEqual(detail.tasks.map((task) => task.id), ["t1"]);
|
||||
assert.deepEqual(
|
||||
detail.subprojects.map((sub) => sub.id),
|
||||
["proj-2"],
|
||||
);
|
||||
assert.equal(detail.subprojects[0].mainChatId, "pgc2");
|
||||
assert.equal(detail.subprojects[0].taskCount, 1);
|
||||
assert.deepEqual(
|
||||
detail.pushTasks.map((task) => `${task.project_name}:${task.name}`).sort(),
|
||||
["一级:直属任务", "三级:孙项目任务", "二级:子项目任务"],
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user