feat: 新增项目中心——按项目组织任务/任务群/总群并一键推送
- 新表 projects / project_group_chats,partners 加 partner_type/wecom_userid,tasks 加 project_id(迁移 0010) - 项目 CRUD、总群成员配置、任务群人工映射(群主→合作方)、任务挂项目 - 推送:总群成员逐人应用消息 + 任务群按群主企业群发,结果分项回显
This commit is contained in:
160
lib/project-push-service.ts
Normal file
160
lib/project-push-service.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import type { DatabaseClient } from "./database";
|
||||
import { getRuntimeEnv } from "./runtime-env";
|
||||
import {
|
||||
sendWecomAppMessage,
|
||||
WecomClientError,
|
||||
type WecomConfig,
|
||||
} from "./wecom-client";
|
||||
import {
|
||||
pushTaskToGroupChats,
|
||||
type GroupPushResult,
|
||||
type TaskPushRow,
|
||||
} from "./wecom-group-push-service";
|
||||
import { getProjectDetail, ProjectError } from "./project-service";
|
||||
|
||||
type FetchLike = typeof fetch;
|
||||
|
||||
export type AppMessageResult = {
|
||||
userid: string;
|
||||
ok: boolean;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type ProjectPushOutcome = {
|
||||
appResults: AppMessageResult[];
|
||||
groupResults: GroupPushResult[];
|
||||
groupChatCount: number;
|
||||
skippedReasons: string[];
|
||||
linkUrl: string;
|
||||
};
|
||||
|
||||
function text(value: unknown) {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
export function buildProjectAppMessage(
|
||||
task: Pick<
|
||||
TaskPushRow,
|
||||
| "name"
|
||||
| "brand"
|
||||
| "quantity"
|
||||
| "due_at"
|
||||
| "task_type"
|
||||
| "platform"
|
||||
| "content_format"
|
||||
| "share_token"
|
||||
>,
|
||||
portalUrl: string,
|
||||
): string {
|
||||
const portal = text(portalUrl).replace(/\/$/, "");
|
||||
const claimUrl =
|
||||
portal && task.share_token ? `${portal}/?task=${task.share_token}` : "";
|
||||
const formatLabel =
|
||||
task.task_type === "screenshot_collect"
|
||||
? "截图回收"
|
||||
: task.content_format === "video"
|
||||
? "视频"
|
||||
: "图文";
|
||||
return [
|
||||
`【项目任务】${task.name}`,
|
||||
`品牌:${task.brand}|数量:${task.quantity} 份|截止:${task.due_at}`,
|
||||
`平台:${task.platform}|形式:${formatLabel}`,
|
||||
"请把任务转发到你负责的项目任务群,并跟进群内成员领取与发布。",
|
||||
claimUrl ? `领取链接:${claimUrl}` : "领取链接生成失败,请联系管理员",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export async function pushProjectTask(
|
||||
db: DatabaseClient,
|
||||
params: {
|
||||
taskId: unknown;
|
||||
sendMainGroup?: unknown;
|
||||
sendGroupChats?: unknown;
|
||||
text?: unknown;
|
||||
},
|
||||
config: WecomConfig,
|
||||
fetchImpl: FetchLike = fetch,
|
||||
): Promise<ProjectPushOutcome> {
|
||||
const taskId = text(params.taskId);
|
||||
if (!taskId) throw new ProjectError("缺少 taskId");
|
||||
const sendMainGroup = params.sendMainGroup !== false;
|
||||
const sendGroupChats = params.sendGroupChats !== false;
|
||||
if (!sendMainGroup && !sendGroupChats) {
|
||||
throw new ProjectError("请至少选择一个发送目标(项目总群或任务群)");
|
||||
}
|
||||
|
||||
const task = await db
|
||||
.prepare(
|
||||
`SELECT id, name, brand, quantity, due_at, task_type, platform,
|
||||
content_format, share_token
|
||||
FROM tasks WHERE id = ?`,
|
||||
)
|
||||
.bind(taskId)
|
||||
.first<TaskPushRow & { project_id: string }>();
|
||||
if (!task) throw new ProjectError("任务不存在", 404);
|
||||
if (!text(task.project_id)) {
|
||||
throw new ProjectError("该任务未挂项目,请先在任务中心关联项目");
|
||||
}
|
||||
|
||||
const detail = await getProjectDetail(db, task.project_id);
|
||||
const portalUrl = text(getRuntimeEnv().KOC_PORTAL_URL);
|
||||
const fallback = buildProjectAppMessage(task, portalUrl);
|
||||
const message = text(params.text) || fallback;
|
||||
|
||||
const skippedReasons: string[] = [];
|
||||
const appResults: AppMessageResult[] = [];
|
||||
if (sendMainGroup) {
|
||||
const members = detail.project.mainGroupMemberIds;
|
||||
if (members.length === 0) {
|
||||
skippedReasons.push("项目总群未配置成员");
|
||||
} else {
|
||||
for (const userid of members) {
|
||||
try {
|
||||
await sendWecomAppMessage([userid], message, config, fetchImpl);
|
||||
appResults.push({ userid, ok: true, error: "" });
|
||||
} catch (error) {
|
||||
appResults.push({
|
||||
userid,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : "发送失败",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let groupResults: GroupPushResult[] = [];
|
||||
let groupChatCount = 0;
|
||||
let linkUrl =
|
||||
task.share_token && portalUrl
|
||||
? `${portalUrl.replace(/\/$/, "")}/?task=${task.share_token}`
|
||||
: "";
|
||||
if (sendGroupChats) {
|
||||
const chatIds = detail.groupChats
|
||||
.filter((chat) => !chat.missing)
|
||||
.map((chat) => chat.chatId);
|
||||
if (chatIds.length === 0) {
|
||||
skippedReasons.push("项目没有可用的任务群");
|
||||
} else {
|
||||
const summary = await pushTaskToGroupChats(
|
||||
db,
|
||||
{ taskId, chatIds, text: message },
|
||||
config,
|
||||
fetchImpl,
|
||||
);
|
||||
groupResults = summary.results;
|
||||
groupChatCount = chatIds.length;
|
||||
linkUrl = summary.linkUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
appResults,
|
||||
groupResults,
|
||||
groupChatCount,
|
||||
skippedReasons,
|
||||
linkUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export { WecomClientError };
|
||||
385
lib/project-service.ts
Normal file
385
lib/project-service.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import type { DatabaseClient } from "./database";
|
||||
|
||||
export type ProjectRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string;
|
||||
main_group_member_ids: 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[];
|
||||
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<string>();
|
||||
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<ProjectSummary[]> {
|
||||
const result = await db
|
||||
.prepare(
|
||||
`SELECT p.id, p.name, p.brand, p.main_group_member_ids, 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<ProjectRow & { task_count: number; group_chat_count: number }>();
|
||||
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),
|
||||
taskCount: Number(row.task_count),
|
||||
groupChatCount: Number(row.group_chat_count),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getProjectDetail(
|
||||
db: DatabaseClient,
|
||||
projectId: string,
|
||||
): Promise<ProjectDetail> {
|
||||
const id = text(projectId);
|
||||
const project = await db
|
||||
.prepare(
|
||||
`SELECT id, name, brand, main_group_member_ids, status, note, created_at
|
||||
FROM projects WHERE id = ?`,
|
||||
)
|
||||
.bind(id)
|
||||
.first<ProjectRow>();
|
||||
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<ProjectTaskRow>();
|
||||
|
||||
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),
|
||||
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;
|
||||
note?: unknown;
|
||||
};
|
||||
|
||||
export async function saveProject(
|
||||
db: DatabaseClient,
|
||||
input: SaveProjectInput,
|
||||
): Promise<ProjectSummary> {
|
||||
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);
|
||||
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 = ?, note = ?
|
||||
WHERE id = ?`,
|
||||
)
|
||||
.bind(name, brand, JSON.stringify(memberIds), 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, status, note, created_at)
|
||||
VALUES (?, ?, ?, ?, 'active', ?, ?)`,
|
||||
)
|
||||
.bind(
|
||||
projectId,
|
||||
name,
|
||||
brand,
|
||||
JSON.stringify(memberIds),
|
||||
note,
|
||||
now,
|
||||
)
|
||||
.run();
|
||||
}
|
||||
const detail = await getProjectDetail(db, projectId);
|
||||
return detail.project;
|
||||
}
|
||||
|
||||
export async function deleteProject(
|
||||
db: DatabaseClient,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
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<ProjectDetail> {
|
||||
const id = text(projectId);
|
||||
await getProjectDetail(db, id);
|
||||
const seen = new Set<string>();
|
||||
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<ProjectPushTargets> {
|
||||
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<GroupOwnerPartnerRow[]> {
|
||||
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<GroupOwnerPartnerRow>();
|
||||
return result.results;
|
||||
}
|
||||
|
||||
export type SavePartnerInput = {
|
||||
id?: unknown;
|
||||
name?: unknown;
|
||||
wecomUserid?: unknown;
|
||||
};
|
||||
|
||||
export async function saveGroupOwnerPartner(
|
||||
db: DatabaseClient,
|
||||
input: SavePartnerInput,
|
||||
): Promise<GroupOwnerPartnerRow> {
|
||||
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<GroupOwnerPartnerRow>();
|
||||
if (!row) throw new ProjectError("合作方保存失败", 500);
|
||||
return row;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export type CreateDistributionTaskInput = {
|
||||
dueAt: string;
|
||||
platform?: "小红书" | "抖音";
|
||||
contentFormat?: "image_text" | "video";
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export type CreateScreenshotTaskInput = {
|
||||
@@ -22,6 +23,7 @@ export type CreateScreenshotTaskInput = {
|
||||
instructions: string;
|
||||
quantity: number;
|
||||
exampleImageKey?: string;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export type DistributionTaskCreation = {
|
||||
@@ -140,10 +142,10 @@ async function insertTaskFromSource(
|
||||
.prepare(
|
||||
`INSERT INTO tasks
|
||||
(id, name, brand, quantity, claimed_quantity, due_at, status,
|
||||
platform, content_format,
|
||||
platform, content_format, project_id,
|
||||
source_url, source_sheet_id, source_sheet_name, source_synced_at,
|
||||
share_token)
|
||||
VALUES (?, ?, ?, ?, 0, ?, 'importing', ?, ?, ?, ?, ?, ?, ?)`,
|
||||
VALUES (?, ?, ?, ?, 0, ?, 'importing', ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.bind(
|
||||
taskId,
|
||||
@@ -153,6 +155,7 @@ async function insertTaskFromSource(
|
||||
input.dueAt,
|
||||
input.platform ?? "小红书",
|
||||
input.contentFormat ?? "image_text",
|
||||
input.projectId,
|
||||
source.url,
|
||||
source.sheetId,
|
||||
source.sheetName,
|
||||
@@ -220,6 +223,7 @@ export async function createDistributionTask(
|
||||
dueAt: normalizedDueDate(rawInput.dueAt),
|
||||
platform: rawInput.platform === "抖音" ? "抖音" : "小红书",
|
||||
contentFormat: rawInput.contentFormat === "video" ? "video" : "image_text",
|
||||
projectId: normalizedValue(rawInput.projectId ?? ""),
|
||||
};
|
||||
if (!input.feishuUrl || !input.name || !input.brand) {
|
||||
throw new Error("请补全飞书链接、任务名称和品牌/项目");
|
||||
@@ -281,6 +285,7 @@ export async function createScreenshotTask(
|
||||
instructions: normalizedValue(rawInput.instructions),
|
||||
quantity: Math.floor(Number(rawInput.quantity)),
|
||||
exampleImageKey: normalizedValue(rawInput.exampleImageKey ?? ""),
|
||||
projectId: normalizedValue(rawInput.projectId ?? ""),
|
||||
};
|
||||
if (!input.name || !input.brand || !input.keyword || !input.instructions) {
|
||||
throw new Error("请补全任务名称、品牌/项目、搜索关键词和任务说明");
|
||||
@@ -304,9 +309,9 @@ export async function createScreenshotTask(
|
||||
.prepare(
|
||||
`INSERT INTO tasks
|
||||
(id, name, brand, quantity, claimed_quantity, due_at, status,
|
||||
task_type, source_url, source_sheet_id, source_sheet_name,
|
||||
task_type, project_id, source_url, source_sheet_id, source_sheet_name,
|
||||
share_token, collection_days)
|
||||
VALUES (?, ?, ?, ?, 0, ?, 'active', 'screenshot_collect', '', '', '', ?, '[]')`,
|
||||
VALUES (?, ?, ?, ?, 0, ?, 'active', 'screenshot_collect', ?, '', '', '', ?, '[]')`,
|
||||
)
|
||||
.bind(
|
||||
taskId,
|
||||
@@ -314,6 +319,7 @@ export async function createScreenshotTask(
|
||||
input.brand,
|
||||
input.quantity,
|
||||
input.dueAt,
|
||||
input.projectId,
|
||||
shareToken,
|
||||
)
|
||||
.run();
|
||||
|
||||
Reference in New Issue
Block a user