import type { DatabaseClient } from "./database"; export type ProjectRow = { id: string; name: string; brand: string; main_group_member_ids: string; owner_userid: string; status: string; note: string; created_at: string; }; export type ProjectSummary = { id: string; name: string; brand: string; status: string; note: string; createdAt: string; mainGroupMemberIds: string[]; ownerUserid: string; taskCount: number; groupChatCount: number; }; export type ProjectTaskRow = { id: string; name: string; brand: string; quantity: number; claimed_quantity: number; due_at: string; status: string; task_type: string; platform: string; content_format: string; share_token: string | null; }; export type ProjectGroupChat = { chatId: string; name: string; ownerUserId: string; memberCount: number; status: number; partnerId: string; partnerName: string; missing: boolean; }; export type ProjectDetail = { project: ProjectSummary; tasks: ProjectTaskRow[]; groupChats: ProjectGroupChat[]; }; export type GroupOwnerPartnerRow = { id: string; name: string; wecom_userid: string; partner_type: string; }; function text(value: unknown) { return String(value ?? "").trim(); } export class ProjectError extends Error { status: number; constructor(message: string, status = 400) { super(message); this.status = status; } } export function parseMemberIds(value: unknown): string[] { const raw = Array.isArray(value) ? value.map((item) => text(item)) : text(value).split(/[\s,;,;]+/); const seen = new Set(); for (const item of raw) { if (item) seen.add(item); } return [...seen]; } function parseJsonArray(value: string): string[] { try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed.map((item) => String(item)) : []; } catch { return []; } } export async function listProjects(db: DatabaseClient): Promise { const result = await db .prepare( `SELECT p.id, p.name, p.brand, p.main_group_member_ids, p.owner_userid, p.status, p.note, p.created_at, (SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id) AS task_count, (SELECT COUNT(*) FROM project_group_chats g WHERE g.project_id = p.id) AS group_chat_count FROM projects p ORDER BY p.created_at DESC`, ) .all(); return result.results.map((row) => ({ id: row.id, name: row.name, brand: row.brand, status: row.status, note: row.note, createdAt: row.created_at, mainGroupMemberIds: parseJsonArray(row.main_group_member_ids), ownerUserid: row.owner_userid ?? "", taskCount: Number(row.task_count), groupChatCount: Number(row.group_chat_count), })); } export async function getProjectDetail( db: DatabaseClient, projectId: string, ): Promise { const id = text(projectId); const project = await db .prepare( `SELECT id, name, brand, main_group_member_ids, owner_userid, status, note, created_at FROM projects WHERE id = ?`, ) .bind(id) .first(); if (!project) throw new ProjectError("项目不存在", 404); const tasks = await db .prepare( `SELECT id, name, brand, quantity, claimed_quantity, due_at, status, task_type, platform, content_format, share_token FROM tasks WHERE project_id = ? ORDER BY created_at DESC`, ) .bind(id) .all(); const chats = await db .prepare( `SELECT g.chat_id, g.partner_id, c.name AS chat_name, c.owner_user_id, c.member_count, c.status AS chat_status, p.name AS partner_name FROM project_group_chats g LEFT JOIN wecom_group_chats c ON c.chat_id = g.chat_id LEFT JOIN partners p ON p.id = g.partner_id WHERE g.project_id = ? ORDER BY g.created_at`, ) .bind(id) .all<{ chat_id: string; partner_id: string; chat_name: string | null; owner_user_id: string | null; member_count: number | null; chat_status: number | null; partner_name: string | null; }>(); return { project: { id: project.id, name: project.name, brand: project.brand, status: project.status, note: project.note, createdAt: project.created_at, mainGroupMemberIds: parseJsonArray(project.main_group_member_ids), ownerUserid: project.owner_userid ?? "", taskCount: tasks.results.length, groupChatCount: chats.results.length, }, tasks: tasks.results, groupChats: chats.results.map((row) => ({ chatId: row.chat_id, name: row.chat_name ?? "", ownerUserId: row.owner_user_id ?? "", memberCount: Number(row.member_count ?? 0), status: Number(row.chat_status ?? 0), partnerId: row.partner_id, partnerName: row.partner_name ?? "", missing: row.chat_name === null, })), }; } export type SaveProjectInput = { id?: unknown; name?: unknown; brand?: unknown; mainGroupMemberIds?: unknown; ownerUserid?: unknown; note?: unknown; }; export async function saveProject( db: DatabaseClient, input: SaveProjectInput, ): Promise { const id = text(input.id); const name = text(input.name); const brand = text(input.brand); const note = text(input.note ?? ""); const memberIds = parseMemberIds(input.mainGroupMemberIds); const ownerUserid = text(input.ownerUserid).slice(0, 64); if (!name) throw new ProjectError("请填写项目名称"); const now = new Date().toISOString(); let projectId = id; if (id) { const result = await db .prepare( `UPDATE projects SET name = ?, brand = ?, main_group_member_ids = ?, owner_userid = ?, note = ? WHERE id = ?`, ) .bind(name, brand, JSON.stringify(memberIds), ownerUserid, note, id) .run(); if (result.meta.changes === 0) throw new ProjectError("项目不存在", 404); } else { projectId = `proj-${crypto.randomUUID().slice(0, 12)}`; await db .prepare( `INSERT INTO projects (id, name, brand, main_group_member_ids, owner_userid, status, note, created_at) VALUES (?, ?, ?, ?, ?, 'active', ?, ?)`, ) .bind( projectId, name, brand, JSON.stringify(memberIds), ownerUserid, note, now, ) .run(); } const detail = await getProjectDetail(db, projectId); return detail.project; } export async function deleteProject( db: DatabaseClient, projectId: string, ): Promise { const id = text(projectId); if (!id) throw new ProjectError("缺少项目 ID"); const result = await db .prepare("DELETE FROM projects WHERE id = ?") .bind(id) .run(); if (result.meta.changes === 0) throw new ProjectError("项目不存在", 404); await db.batch([ db.prepare("DELETE FROM project_group_chats WHERE project_id = ?").bind(id), db .prepare("UPDATE tasks SET project_id = '' WHERE project_id = ?") .bind(id), ]); } export type ProjectGroupChatInput = { chatId: string; partnerId?: string; }; export async function setProjectGroupChats( db: DatabaseClient, projectId: string, items: ProjectGroupChatInput[], ): Promise { const id = text(projectId); await getProjectDetail(db, id); const seen = new Set(); const rows: ProjectGroupChatInput[] = []; for (const item of Array.isArray(items) ? items : []) { const chatId = text(item?.chatId); if (!chatId || seen.has(chatId)) continue; seen.add(chatId); rows.push({ chatId, partnerId: text(item?.partnerId) }); } const statements = [ db.prepare("DELETE FROM project_group_chats WHERE project_id = ?").bind(id), ...rows.map((row) => db .prepare( `INSERT INTO project_group_chats (id, project_id, chat_id, partner_id, created_at) VALUES (?, ?, ?, ?, ?)`, ) .bind( `pgc-${crypto.randomUUID().slice(0, 12)}`, id, row.chatId, row.partnerId ?? "", new Date().toISOString(), ), ), ]; await db.batch(statements); return getProjectDetail(db, id); } export type ProjectPushTargets = { project: { id: string; name: string; mainGroupMemberIds: string[] }; chatIds: string[]; }; export async function getProjectPushTargets( db: DatabaseClient, projectId: string, ): Promise { const detail = await getProjectDetail(db, projectId); return { project: { id: detail.project.id, name: detail.project.name, mainGroupMemberIds: detail.project.mainGroupMemberIds, }, chatIds: detail.groupChats.filter((chat) => !chat.missing).map((chat) => chat.chatId), }; } export async function listGroupOwnerPartners( db: DatabaseClient, ): Promise { const result = await db .prepare( `SELECT id, name, wecom_userid, partner_type FROM partners WHERE partner_type = 'group_owner' ORDER BY created_at DESC`, ) .all(); return result.results; } export type SavePartnerInput = { id?: unknown; name?: unknown; wecomUserid?: unknown; }; export async function saveGroupOwnerPartner( db: DatabaseClient, input: SavePartnerInput, ): Promise { const id = text(input.id); const name = text(input.name); const wecomUserid = text(input.wecomUserid); if (!name) throw new ProjectError("请填写合作方名称"); if (!wecomUserid) throw new ProjectError("请填写企业微信 userid"); let partnerId = id; if (id) { const result = await db .prepare( `UPDATE partners SET name = ?, wecom_userid = ?, partner_type = 'group_owner' WHERE id = ?`, ) .bind(name, wecomUserid, id) .run(); if (result.meta.changes === 0) throw new ProjectError("合作方不存在", 404); } else { partnerId = `partner-go-${crypto.randomUUID().slice(0, 12)}`; await db .prepare( `INSERT INTO partners (id, name, wecom_name, owner, claimed_total, completed_total, partner_type, wecom_userid, created_at) VALUES (?, ?, '', '运营组', 0, 0, 'group_owner', ?, ?)`, ) .bind(partnerId, name, wecomUserid, new Date().toISOString()) .run(); } const row = await db .prepare( "SELECT id, name, wecom_userid, partner_type FROM partners WHERE id = ?", ) .bind(partnerId) .first(); if (!row) throw new ProjectError("合作方保存失败", 500); return row; }