feat: 项目层级树+合作方绑定KOC资源+客户群全量可选+精简编辑表单,移除复盘报表
- 项目支持父子层级:树形折叠展示、详情面板内联管理总群成员、推送聚合子项目任务、环检测与子项目删除守卫 - 外部KOC合作方绑定 KOC 资源账号(唯一占用、换绑/解绑、资源页一键设为合作方),列表直显联系方式 - 绑定客户群改为全量外部群缓存(含各群主),同步按钮强刷;member_count 缺失以 member_list 兜底 - 任务/项目编辑模式隐藏不可变字段(平台/内容类型/品牌/总群成员),详情面板维护成员时保留 parent_id - 删除复盘报表整栏(导航/页面/task_report_list/导出路由/CSV lib 及测试)
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
||||
ensureSchema,
|
||||
getDashboardData,
|
||||
getRawDb,
|
||||
getTaskReportData,
|
||||
uid,
|
||||
} from "../../../lib/mvp-db";
|
||||
import {
|
||||
@@ -61,7 +60,6 @@ import {
|
||||
} from "../../../lib/wecom-group-push-service";
|
||||
import {
|
||||
bindTaskCustomerChat,
|
||||
listOwnerCustomerChats,
|
||||
unbindTaskCustomerChat,
|
||||
} from "../../../lib/task-customer-chat-service";
|
||||
import { isManagerRequest, managerForbidden } from "../../../lib/user-auth";
|
||||
@@ -694,6 +692,89 @@ export async function POST(request: Request) {
|
||||
wecomExternalUserId: externalId || null,
|
||||
wecomName,
|
||||
});
|
||||
} else if (body.action === "partner_bind_account") {
|
||||
const forbidden = managerGate();
|
||||
if (forbidden) return forbidden;
|
||||
const partnerId = String(body.partnerId ?? "").trim();
|
||||
const accountId = String(body.accountId ?? "").trim();
|
||||
if (!partnerId) {
|
||||
return Response.json({ error: "缺少 partnerId" }, { status: 400 });
|
||||
}
|
||||
const partner = await db
|
||||
.prepare("SELECT id, name, partner_type, account_id FROM partners WHERE id = ?")
|
||||
.bind(partnerId)
|
||||
.first<{ id: string; name: string; partner_type: string | null; account_id: string | null }>();
|
||||
if (!partner) return Response.json({ error: "合作方不存在" }, { status: 404 });
|
||||
if ((partner.partner_type ?? "external_koc") !== "external_koc") {
|
||||
return Response.json({ error: "群主合作方不绑定 KOC 资源" }, { status: 400 });
|
||||
}
|
||||
if (!accountId) {
|
||||
await db
|
||||
.prepare("UPDATE partners SET account_id = '' WHERE id = ?")
|
||||
.bind(partnerId)
|
||||
.run();
|
||||
return Response.json({ partnerId, accountId: "" });
|
||||
}
|
||||
const account = await db
|
||||
.prepare("SELECT id, nickname FROM accounts WHERE id = ?")
|
||||
.bind(accountId)
|
||||
.first<{ id: string; nickname: string }>();
|
||||
if (!account) return Response.json({ error: "KOC 资源不存在" }, { status: 404 });
|
||||
const holder = await db
|
||||
.prepare(
|
||||
"SELECT id, name FROM partners WHERE account_id = ? AND id != ? LIMIT 1",
|
||||
)
|
||||
.bind(accountId, partnerId)
|
||||
.first<{ id: string; name: string }>();
|
||||
if (holder) {
|
||||
return Response.json(
|
||||
{ error: `该 KOC 资源已绑定合作方「${holder.name}」,请先在对方条目上解绑` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
await db
|
||||
.prepare("UPDATE partners SET account_id = ? WHERE id = ?")
|
||||
.bind(accountId, partnerId)
|
||||
.run();
|
||||
return Response.json({ partnerId, accountId, nickname: account.nickname });
|
||||
} else if (body.action === "partner_create_from_account") {
|
||||
const forbidden = managerGate();
|
||||
if (forbidden) return forbidden;
|
||||
const accountId = String(body.accountId ?? "").trim();
|
||||
if (!accountId) {
|
||||
return Response.json({ error: "缺少 accountId" }, { status: 400 });
|
||||
}
|
||||
const account = await db
|
||||
.prepare("SELECT id, nickname, current_contact FROM accounts WHERE id = ?")
|
||||
.bind(accountId)
|
||||
.first<{ id: string; nickname: string; current_contact: string }>();
|
||||
if (!account) return Response.json({ error: "KOC 资源不存在" }, { status: 404 });
|
||||
const holder = await db
|
||||
.prepare("SELECT id, name FROM partners WHERE account_id = ? LIMIT 1")
|
||||
.bind(accountId)
|
||||
.first<{ id: string; name: string }>();
|
||||
if (holder) {
|
||||
return Response.json(
|
||||
{ error: `该 KOC 已是合作方「${holder.name}」,无需重复创建` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const partnerId = `partner-koc-${crypto.randomUUID().slice(0, 12)}`;
|
||||
await db
|
||||
.prepare(
|
||||
`INSERT INTO partners
|
||||
(id, name, wecom_name, owner, claimed_total, completed_total,
|
||||
partner_type, wecom_userid, account_id, created_at)
|
||||
VALUES (?, ?, '', '运营组', 0, 0, 'external_koc', '', ?, ?)`,
|
||||
)
|
||||
.bind(partnerId, account.nickname, accountId, new Date().toISOString())
|
||||
.run();
|
||||
return Response.json({
|
||||
partnerId,
|
||||
accountId,
|
||||
nickname: account.nickname,
|
||||
contact: account.current_contact ?? "",
|
||||
});
|
||||
} else if (body.action === "send_test_wecom") {
|
||||
const forbidden = managerGate();
|
||||
if (forbidden) return forbidden;
|
||||
@@ -858,6 +939,7 @@ export async function POST(request: Request) {
|
||||
mainGroupMemberIds: body.mainGroupMemberIds,
|
||||
ownerUserid: nextOwnerUserid,
|
||||
note: body.note,
|
||||
parentProjectId: body.parentProjectId,
|
||||
},
|
||||
resolveWecomConfig(env as unknown as WecomBindings),
|
||||
);
|
||||
@@ -970,10 +1052,6 @@ export async function POST(request: Request) {
|
||||
appFailedCount: appFailed.length,
|
||||
hint: "任务群消息已直接发送至任务内部群(负责人与领取人即时可见)",
|
||||
});
|
||||
} else if (body.action === "task_report_list") {
|
||||
return Response.json({
|
||||
reports: await getTaskReportData(db, ownerScopeUserid(scope)),
|
||||
});
|
||||
} else if (body.action === "refresh_account_fields") {
|
||||
const forbidden = managerGate();
|
||||
if (forbidden) return forbidden;
|
||||
@@ -1042,28 +1120,24 @@ export async function POST(request: Request) {
|
||||
const forbidden = await taskOwnerGate(taskId, { strict: true });
|
||||
if (forbidden) return forbidden;
|
||||
const task = await db
|
||||
.prepare(
|
||||
"SELECT id, owner_userid, customer_chat_id FROM tasks WHERE id = ?",
|
||||
)
|
||||
.prepare("SELECT id, customer_chat_id FROM tasks WHERE id = ?")
|
||||
.bind(taskId)
|
||||
.first<{ id: string; owner_userid: string; customer_chat_id: string }>();
|
||||
.first<{ id: string; customer_chat_id: string }>();
|
||||
if (!task) return Response.json({ error: "任务不存在" }, { status: 404 });
|
||||
const ownerUserid = (
|
||||
scope.kind === "full"
|
||||
? String(task.owner_userid ?? "")
|
||||
: scope.wecomUserId
|
||||
).trim();
|
||||
if (!ownerUserid) {
|
||||
return Response.json(
|
||||
{ error: "请先设置任务负责人,再绑定客户群" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const wecomConfig = resolveWecomConfig(env as unknown as WecomBindings);
|
||||
const chats = await listOwnerCustomerChats(wecomConfig, ownerUserid);
|
||||
// 列表来自全量群缓存(所有群主的外部客户群),缓存为空或要求刷新时全量同步
|
||||
let rows = body.sync === true ? [] : await listGroupChatRows(db);
|
||||
if (rows.length === 0) {
|
||||
rows = await syncGroupChats(db, wecomConfig);
|
||||
}
|
||||
return Response.json({
|
||||
ownerUserid,
|
||||
chats,
|
||||
chats: rows.map((row) => ({
|
||||
chatId: row.chat_id,
|
||||
name: row.name,
|
||||
ownerUserId: row.owner_user_id,
|
||||
memberCount: row.member_count,
|
||||
status: row.status,
|
||||
})),
|
||||
boundChatId: String(task.customer_chat_id ?? ""),
|
||||
});
|
||||
} else if (body.action === "task_bind_customer_chat") {
|
||||
@@ -1071,24 +1145,11 @@ export async function POST(request: Request) {
|
||||
const forbidden = await taskOwnerGate(taskId, { strict: true });
|
||||
if (forbidden) return forbidden;
|
||||
const task = await db
|
||||
.prepare(
|
||||
"SELECT id, owner_userid, customer_chat_id FROM tasks WHERE id = ?",
|
||||
)
|
||||
.prepare("SELECT id, customer_chat_id FROM tasks WHERE id = ?")
|
||||
.bind(taskId)
|
||||
.first<{ id: string; owner_userid: string; customer_chat_id: string }>();
|
||||
.first<{ id: string; customer_chat_id: string }>();
|
||||
if (!task) return Response.json({ error: "任务不存在" }, { status: 404 });
|
||||
const ownerUserid = (
|
||||
scope.kind === "full"
|
||||
? String(task.owner_userid ?? "")
|
||||
: scope.wecomUserId
|
||||
).trim();
|
||||
const chatId = String(body.chatId ?? "").trim();
|
||||
if (chatId && !ownerUserid) {
|
||||
return Response.json(
|
||||
{ error: "请先设置任务负责人,再绑定客户群" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const wecomConfig = resolveWecomConfig(env as unknown as WecomBindings);
|
||||
if (chatId) {
|
||||
const chatName = String(body.chatName ?? "").trim();
|
||||
|
||||
Reference in New Issue
Block a user