- 新表 projects / project_group_chats,partners 加 partner_type/wecom_userid,tasks 加 project_id(迁移 0010) - 项目 CRUD、总群成员配置、任务群人工映射(群主→合作方)、任务挂项目 - 推送:总群成员逐人应用消息 + 任务群按群主企业群发,结果分项回显
379 lines
11 KiB
JavaScript
379 lines
11 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
import {
|
||
deleteProject,
|
||
getProjectDetail,
|
||
listProjects,
|
||
parseMemberIds,
|
||
ProjectError,
|
||
saveGroupOwnerPartner,
|
||
saveProject,
|
||
setProjectGroupChats,
|
||
} from "../lib/project-service.ts";
|
||
|
||
function fakeProjectDb({
|
||
projects = [],
|
||
links = [],
|
||
tasks = [],
|
||
groupChats = [],
|
||
partners = [],
|
||
} = {}) {
|
||
const executed = [];
|
||
const applyWrite = (sql, params) => {
|
||
const normalized = sql.trim();
|
||
if (/^INSERT INTO projects/.test(normalized)) {
|
||
projects.push({
|
||
id: params[0],
|
||
name: params[1],
|
||
brand: params[2],
|
||
main_group_member_ids: params[3],
|
||
status: "active",
|
||
note: params[4],
|
||
created_at: params[5],
|
||
});
|
||
return 1;
|
||
}
|
||
if (/^UPDATE projects/.test(normalized)) {
|
||
const row = projects.find((item) => item.id === params[4]);
|
||
if (!row) return 0;
|
||
Object.assign(row, {
|
||
name: params[0],
|
||
brand: params[1],
|
||
main_group_member_ids: params[2],
|
||
note: params[3],
|
||
});
|
||
return 1;
|
||
}
|
||
if (/^DELETE FROM projects/.test(normalized)) {
|
||
const index = projects.findIndex((item) => item.id === params[0]);
|
||
if (index < 0) return 0;
|
||
projects.splice(index, 1);
|
||
return 1;
|
||
}
|
||
if (/^INSERT INTO project_group_chats/.test(normalized)) {
|
||
links.push({ project_id: params[1], chat_id: params[2], partner_id: params[3] });
|
||
return 1;
|
||
}
|
||
if (/^DELETE FROM project_group_chats/.test(normalized)) {
|
||
for (let index = links.length - 1; index >= 0; index -= 1) {
|
||
if (links[index].project_id === params[0]) links.splice(index, 1);
|
||
}
|
||
return 1;
|
||
}
|
||
if (/^UPDATE tasks SET project_id = ''/.test(normalized)) {
|
||
for (const task of tasks) {
|
||
if (task.project_id === params[0]) task.project_id = "";
|
||
}
|
||
return 1;
|
||
}
|
||
if (/^INSERT INTO partners/.test(normalized)) {
|
||
partners.push({
|
||
id: params[0],
|
||
name: params[1],
|
||
wecom_userid: params[2],
|
||
partner_type: "group_owner",
|
||
});
|
||
return 1;
|
||
}
|
||
if (/^UPDATE partners/.test(normalized)) {
|
||
const row = partners.find((item) => item.id === params[2]);
|
||
if (!row) return 0;
|
||
Object.assign(row, { name: params[0], wecom_userid: params[1] });
|
||
return 1;
|
||
}
|
||
return 1;
|
||
};
|
||
const makeStatement = (sql, params = []) => ({
|
||
sql,
|
||
params,
|
||
bind(...args) {
|
||
return makeStatement(sql, args);
|
||
},
|
||
async run() {
|
||
executed.push({ sql, params });
|
||
return { meta: { changes: applyWrite(sql, params) } };
|
||
},
|
||
async first() {
|
||
executed.push({ sql, params });
|
||
if (/FROM projects WHERE id = \?/.test(sql)) {
|
||
return projects.find((item) => item.id === params[0]) ?? null;
|
||
}
|
||
if (/FROM partners WHERE id = \?/.test(sql)) {
|
||
return partners.find((item) => item.id === params[0]) ?? null;
|
||
}
|
||
return null;
|
||
},
|
||
async all() {
|
||
executed.push({ sql, params });
|
||
if (/FROM projects p/.test(sql)) {
|
||
return {
|
||
results: projects.map((item) => ({
|
||
...item,
|
||
task_count: tasks.filter((task) => task.project_id === item.id).length,
|
||
group_chat_count: links.filter((link) => link.project_id === item.id).length,
|
||
})),
|
||
};
|
||
}
|
||
if (/FROM tasks WHERE project_id/.test(sql)) {
|
||
return {
|
||
results: tasks
|
||
.filter((task) => task.project_id === params[0])
|
||
.map((task) => ({ ...task })),
|
||
};
|
||
}
|
||
if (/FROM project_group_chats g/.test(sql)) {
|
||
return {
|
||
results: links
|
||
.filter((link) => link.project_id === params[0])
|
||
.map((link) => {
|
||
const chat = groupChats.find((item) => item.chat_id === link.chat_id);
|
||
return {
|
||
chat_id: link.chat_id,
|
||
partner_id: link.partner_id,
|
||
chat_name: chat ? chat.name : null,
|
||
owner_user_id: chat ? chat.owner_user_id : null,
|
||
member_count: chat ? chat.member_count : null,
|
||
chat_status: chat ? chat.status : null,
|
||
partner_name:
|
||
partners.find((item) => item.id === link.partner_id)?.name ?? null,
|
||
};
|
||
}),
|
||
};
|
||
}
|
||
if (/FROM partners/.test(sql)) {
|
||
return { results: partners.map((item) => ({ ...item })) };
|
||
}
|
||
return { results: [] };
|
||
},
|
||
});
|
||
return {
|
||
projects,
|
||
links,
|
||
tasks,
|
||
groupChats,
|
||
partners,
|
||
executed,
|
||
prepare(sql) {
|
||
return makeStatement(sql);
|
||
},
|
||
async batch(statements) {
|
||
const output = [];
|
||
for (const statement of statements) output.push(await statement.run());
|
||
return output;
|
||
},
|
||
};
|
||
}
|
||
|
||
test("parseMemberIds accepts lines, arrays and mixed separators, dedupes", () => {
|
||
assert.deepEqual(parseMemberIds("zhangsan\nlisi zhangsan"), ["zhangsan", "lisi"]);
|
||
assert.deepEqual(parseMemberIds(["a", " b ", "a"]), ["a", "b"]);
|
||
assert.deepEqual(parseMemberIds("a,b;c,d"), ["a", "b", "c", "d"]);
|
||
assert.deepEqual(parseMemberIds(" \n "), []);
|
||
});
|
||
|
||
test("saveProject inserts with proj- prefixed id and JSON members", async () => {
|
||
const db = fakeProjectDb();
|
||
const project = await saveProject(db, {
|
||
name: "暑期焕新",
|
||
brand: "青柠实验室",
|
||
mainGroupMemberIds: "zhangsan\nlisi\nzhangsan",
|
||
note: "备注",
|
||
});
|
||
assert.match(project.id, /^proj-/);
|
||
assert.equal(project.name, "暑期焕新");
|
||
assert.deepEqual(project.mainGroupMemberIds, ["zhangsan", "lisi"]);
|
||
assert.equal(db.projects.length, 1);
|
||
assert.equal(db.projects[0].status, "active");
|
||
});
|
||
|
||
test("saveProject updates existing row and rejects blank name", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
{
|
||
id: "proj-1",
|
||
name: "旧名",
|
||
brand: "",
|
||
main_group_member_ids: "[]",
|
||
status: "active",
|
||
note: "",
|
||
created_at: "2026-08-01",
|
||
},
|
||
],
|
||
});
|
||
await saveProject(db, {
|
||
id: "proj-1",
|
||
name: "新名",
|
||
brand: "品牌",
|
||
mainGroupMemberIds: ["wangwu"],
|
||
});
|
||
assert.equal(db.projects[0].name, "新名");
|
||
assert.equal(db.projects[0].brand, "品牌");
|
||
assert.deepEqual(JSON.parse(db.projects[0].main_group_member_ids), ["wangwu"]);
|
||
assert.equal(db.projects.length, 1, "update must not insert");
|
||
|
||
await assert.rejects(
|
||
() => saveProject(db, { id: "proj-1", name: " " }),
|
||
(error) => error instanceof ProjectError && error.status === 400,
|
||
);
|
||
});
|
||
|
||
test("saveProject rejects unknown id on update", async () => {
|
||
const db = fakeProjectDb();
|
||
await assert.rejects(
|
||
() => saveProject(db, { id: "proj-404", name: "x" }),
|
||
(error) => error instanceof ProjectError && error.status === 404,
|
||
);
|
||
});
|
||
|
||
test("listProjects aggregates task and group chat counts", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
{
|
||
id: "proj-1",
|
||
name: "A",
|
||
brand: "",
|
||
main_group_member_ids: "[\"u1\",\"u2\"]",
|
||
status: "active",
|
||
note: "",
|
||
created_at: "2026-08-01",
|
||
},
|
||
],
|
||
tasks: [
|
||
{ id: "t1", project_id: "proj-1" },
|
||
{ id: "t2", project_id: "proj-1" },
|
||
{ id: "t3", project_id: "" },
|
||
],
|
||
links: [{ project_id: "proj-1", chat_id: "wr-1", partner_id: "" }],
|
||
});
|
||
const projects = await listProjects(db);
|
||
assert.equal(projects.length, 1);
|
||
assert.equal(projects[0].taskCount, 2);
|
||
assert.equal(projects[0].groupChatCount, 1);
|
||
assert.deepEqual(projects[0].mainGroupMemberIds, ["u1", "u2"]);
|
||
});
|
||
|
||
test("getProjectDetail marks missing chats and dangling partners", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
{
|
||
id: "proj-1",
|
||
name: "A",
|
||
brand: "",
|
||
main_group_member_ids: "[]",
|
||
status: "active",
|
||
note: "",
|
||
created_at: "2026-08-01",
|
||
},
|
||
],
|
||
links: [
|
||
{ project_id: "proj-1", chat_id: "wr-1", partner_id: "p-1" },
|
||
{ project_id: "proj-1", chat_id: "wr-gone", partner_id: "" },
|
||
],
|
||
groupChats: [
|
||
{ chat_id: "wr-1", name: "一群", owner_user_id: "zhangsan", member_count: 12, status: 0 },
|
||
],
|
||
partners: [],
|
||
});
|
||
const detail = await getProjectDetail(db, "proj-1");
|
||
assert.equal(detail.groupChats.length, 2);
|
||
const alive = detail.groupChats.find((chat) => chat.chatId === "wr-1");
|
||
assert.equal(alive.missing, false);
|
||
assert.equal(alive.partnerName, "", "dangling partner shows blank name");
|
||
const gone = detail.groupChats.find((chat) => chat.chatId === "wr-gone");
|
||
assert.equal(gone.missing, true);
|
||
assert.equal(gone.name, "");
|
||
});
|
||
|
||
test("getProjectDetail throws 404 for unknown project", async () => {
|
||
const db = fakeProjectDb();
|
||
await assert.rejects(
|
||
() => getProjectDetail(db, "proj-404"),
|
||
(error) => error instanceof ProjectError && error.status === 404,
|
||
);
|
||
});
|
||
|
||
test("setProjectGroupChats replaces links and dedupes chat ids", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
{
|
||
id: "proj-1",
|
||
name: "A",
|
||
brand: "",
|
||
main_group_member_ids: "[]",
|
||
status: "active",
|
||
note: "",
|
||
created_at: "2026-08-01",
|
||
},
|
||
],
|
||
links: [{ project_id: "proj-1", chat_id: "wr-old", partner_id: "" }],
|
||
groupChats: [
|
||
{ chat_id: "wr-1", name: "一群", owner_user_id: "zhangsan", member_count: 12, status: 0 },
|
||
],
|
||
partners: [{ id: "p-1", name: "张三", wecom_userid: "zhangsan" }],
|
||
});
|
||
const detail = await setProjectGroupChats(db, "proj-1", [
|
||
{ chatId: "wr-1", partnerId: "p-1" },
|
||
{ chatId: "wr-1", partnerId: "p-1" },
|
||
{ chatId: " ", partnerId: "" },
|
||
]);
|
||
assert.equal(detail.groupChats.length, 1);
|
||
assert.equal(detail.groupChats[0].chatId, "wr-1");
|
||
assert.equal(detail.groupChats[0].partnerName, "张三");
|
||
assert.equal(db.links.length, 1);
|
||
assert.equal(db.links[0].chat_id, "wr-1");
|
||
});
|
||
|
||
test("deleteProject removes links and unlinks tasks", async () => {
|
||
const db = fakeProjectDb({
|
||
projects: [
|
||
{
|
||
id: "proj-1",
|
||
name: "A",
|
||
brand: "",
|
||
main_group_member_ids: "[]",
|
||
status: "active",
|
||
note: "",
|
||
created_at: "2026-08-01",
|
||
},
|
||
],
|
||
links: [{ project_id: "proj-1", chat_id: "wr-1", partner_id: "" }],
|
||
tasks: [
|
||
{ id: "t1", project_id: "proj-1" },
|
||
{ id: "t2", project_id: "proj-2" },
|
||
],
|
||
});
|
||
await deleteProject(db, "proj-1");
|
||
assert.equal(db.projects.length, 0);
|
||
assert.equal(db.links.length, 0);
|
||
assert.equal(db.tasks[0].project_id, "");
|
||
assert.equal(db.tasks[1].project_id, "proj-2");
|
||
|
||
await assert.rejects(
|
||
() => deleteProject(db, "proj-404"),
|
||
(error) => error instanceof ProjectError && error.status === 404,
|
||
);
|
||
});
|
||
|
||
test("saveGroupOwnerPartner inserts and updates group_owner partners", async () => {
|
||
const db = fakeProjectDb();
|
||
const created = await saveGroupOwnerPartner(db, {
|
||
name: "张三",
|
||
wecomUserid: "zhangsan",
|
||
});
|
||
assert.match(created.id, /^partner-go-/);
|
||
assert.equal(db.partners[0].partner_type, "group_owner");
|
||
|
||
const updated = await saveGroupOwnerPartner(db, {
|
||
id: created.id,
|
||
name: "张三丰",
|
||
wecomUserid: "zhangsanfeng",
|
||
});
|
||
assert.equal(updated.name, "张三丰");
|
||
assert.equal(db.partners.length, 1);
|
||
|
||
await assert.rejects(
|
||
() => saveGroupOwnerPartner(db, { name: "李四", wecomUserid: "" }),
|
||
(error) => error instanceof ProjectError,
|
||
);
|
||
});
|