feat: 项目总群自动建群+任务负责人后台视角+客户群活码进群+任务编辑
- 项目总群:≥3 人 appchat/create 自动建群(chatid=pgc+projectId),成员 diff 增删同步;DROP project_group_chats;推送改 appchat/send 群发、无群降级逐人 - 任务负责人:tasks.owner_userid(新建/编辑任务通讯录选人),task_set_owner 分配即预建 tgc 任务群发介绍,群主链接只归属负责人 - 任务编辑 task_update:基础字段+截图任务数量/关键词/说明,contents 调平只删 available 尾行 - 删除「一键推送」:task-one-click-push-service 整体移除(recordCardAck 移入 wecom-callback-events) - 负责人后台视角:users.wecom_userid 绑定,lib/owner-scope 行级过滤(undefined=全量/""=空范围/非空=过滤),~30 action 补 managerGate/taskOwnerGate,owner 登录仅见工作台/任务中心/数据回收/复盘报表 - 任务绑定客户群+进群活码:tasks 增 customer_chat_id/name、join_config_id、join_qr_code 4 列,join_way add/update/get/del(换绑走 update 保 config_id 活码不变),后台「客户群」弹窗,portal 认领后扫码自助进群(满员自动开新群) - 迁移 0013/0014/0015;催办支持按任务;企微可信域名 IP 验证文件 - 288 测试全绿;真机全链路验收通过(列群/活码四端点/portal 渲染/解绑清空)
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
import type { DatabaseClient } from "./database";
|
||||
import {
|
||||
effectiveProjectMembers,
|
||||
ensureProjectMainChat,
|
||||
findOrCreateGroupOwnerPartner,
|
||||
} from "./project-service";
|
||||
import {
|
||||
createAppChat,
|
||||
getAppChat,
|
||||
hasAppCredentials,
|
||||
sanitizeChatid,
|
||||
sendAppChatMessage,
|
||||
updateAppChat,
|
||||
type WecomConfig,
|
||||
@@ -20,7 +26,8 @@ type TaskChatRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
task_chat_id: string | null;
|
||||
owner_userid: string | null;
|
||||
task_owner_userid: string | null;
|
||||
project_owner_userid: string | null;
|
||||
};
|
||||
|
||||
function text(value: unknown) {
|
||||
@@ -28,7 +35,7 @@ function text(value: unknown) {
|
||||
}
|
||||
|
||||
export function taskChatId(taskId: string) {
|
||||
return `tgc_${text(taskId)}`;
|
||||
return `tgc${sanitizeChatid(text(taskId))}`;
|
||||
}
|
||||
|
||||
function chatNameForTask(taskName: string) {
|
||||
@@ -42,7 +49,11 @@ async function syncMembers(
|
||||
fetchImpl: FetchLike,
|
||||
) {
|
||||
const info = await getAppChat(config, chatid, fetchImpl);
|
||||
if (!info) return;
|
||||
if (!info) {
|
||||
// appchat/get 要求应用可见范围为根部门,查不到时降级为只增同步(重复添加成员企微按成功处理)
|
||||
await updateAppChat(config, { chatid, addUserList: members }, fetchImpl);
|
||||
return;
|
||||
}
|
||||
const existing = new Set(info.userlist);
|
||||
const missing = members.filter((userid) => !existing.has(userid));
|
||||
if (missing.length > 0) {
|
||||
@@ -63,7 +74,8 @@ export async function ensureTaskChatForMembers(
|
||||
}
|
||||
const task = await db
|
||||
.prepare(
|
||||
`SELECT t.id, t.name, t.task_chat_id, p.owner_userid AS owner_userid
|
||||
`SELECT t.id, t.name, t.task_chat_id, t.owner_userid AS task_owner_userid,
|
||||
p.owner_userid AS project_owner_userid
|
||||
FROM tasks t
|
||||
LEFT JOIN projects p ON p.id = t.project_id
|
||||
WHERE t.id = ?`,
|
||||
@@ -71,15 +83,18 @@ export async function ensureTaskChatForMembers(
|
||||
.bind(taskId)
|
||||
.first<TaskChatRow>();
|
||||
if (!task) return { chatId: null, created: false, reason: "no_task" };
|
||||
const ownerUserid = text(task.owner_userid);
|
||||
if (!ownerUserid) {
|
||||
const ownerUserid = text(task.project_owner_userid);
|
||||
const taskOwnerUserid = text(task.task_owner_userid);
|
||||
if (!ownerUserid && !taskOwnerUserid) {
|
||||
return { chatId: null, created: false, reason: "no_owner" };
|
||||
}
|
||||
const members = [
|
||||
...new Set(
|
||||
[ownerUserid, ...(params.addUserids ?? []).map((id) => text(id))].filter(
|
||||
Boolean,
|
||||
),
|
||||
[
|
||||
ownerUserid,
|
||||
taskOwnerUserid,
|
||||
...(params.addUserids ?? []).map((id) => text(id)),
|
||||
].filter(Boolean),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -110,6 +125,100 @@ export async function ensureTaskChatForMembers(
|
||||
return { chatId, created: createdNow };
|
||||
}
|
||||
|
||||
export type TaskOwnerAssignment = {
|
||||
ownerUserid: string;
|
||||
partnerId: string;
|
||||
chatId: string | null;
|
||||
chatCreated: boolean;
|
||||
warning?: string;
|
||||
};
|
||||
|
||||
export async function assignTaskOwner(
|
||||
db: DatabaseClient,
|
||||
params: { taskId: string; ownerUserid: string },
|
||||
config: WecomConfig,
|
||||
fetchImpl: FetchLike = fetch,
|
||||
): Promise<TaskOwnerAssignment> {
|
||||
const taskId = text(params.taskId);
|
||||
const ownerUserid = text(params.ownerUserid).slice(0, 64);
|
||||
if (!taskId) throw new Error("缺少 taskId");
|
||||
if (!ownerUserid) throw new Error("请填写任务负责人");
|
||||
|
||||
const task = await db
|
||||
.prepare(
|
||||
"SELECT id, name, brand, quantity, due_at, project_id FROM tasks WHERE id = ?",
|
||||
)
|
||||
.bind(taskId)
|
||||
.first<{
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
quantity: number;
|
||||
due_at: string;
|
||||
project_id: string;
|
||||
}>();
|
||||
if (!task) throw new Error("任务不存在");
|
||||
|
||||
await db
|
||||
.prepare("UPDATE tasks SET owner_userid = ? WHERE id = ?")
|
||||
.bind(ownerUserid, taskId)
|
||||
.run();
|
||||
|
||||
const partner = await findOrCreateGroupOwnerPartner(db, ownerUserid);
|
||||
|
||||
const warnings: string[] = [];
|
||||
const projectId = text(task.project_id);
|
||||
if (projectId) {
|
||||
const project = await db
|
||||
.prepare(
|
||||
"SELECT main_group_member_ids, owner_userid FROM projects WHERE id = ?",
|
||||
)
|
||||
.bind(projectId)
|
||||
.first<{ main_group_member_ids: string; owner_userid: string }>();
|
||||
if (project) {
|
||||
// 负责人不在成员列表则并入,保持「负责人都在总群」
|
||||
const stored = effectiveProjectMembers(project.main_group_member_ids, "");
|
||||
if (!stored.includes(ownerUserid)) {
|
||||
await db
|
||||
.prepare("UPDATE projects SET main_group_member_ids = ? WHERE id = ?")
|
||||
.bind(JSON.stringify([...stored, ownerUserid]), projectId)
|
||||
.run();
|
||||
}
|
||||
const mainChat = await ensureProjectMainChat(db, projectId, config, fetchImpl);
|
||||
if (mainChat.warning) warnings.push(mainChat.warning);
|
||||
}
|
||||
}
|
||||
|
||||
let ensure: TaskChatEnsureResult = { chatId: null, created: false };
|
||||
try {
|
||||
ensure = await ensureTaskChatForMembers(db, { taskId }, config, fetchImpl);
|
||||
if (ensure.chatId && ensure.created) {
|
||||
await sendTaskChatMessage(
|
||||
db,
|
||||
taskId,
|
||||
taskChatIntroText({
|
||||
name: task.name,
|
||||
brand: task.brand,
|
||||
quantity: task.quantity,
|
||||
dueAt: task.due_at,
|
||||
}),
|
||||
config,
|
||||
fetchImpl,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
warnings.push(`任务群创建失败:${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
ownerUserid,
|
||||
partnerId: partner.id,
|
||||
chatId: ensure.chatId,
|
||||
chatCreated: ensure.created,
|
||||
warning: warnings.length > 0 ? warnings.join(";") : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendTaskChatMessage(
|
||||
db: DatabaseClient,
|
||||
taskId: string,
|
||||
|
||||
Reference in New Issue
Block a user