2026-07-30 12:06:41 +08:00
|
|
|
import { sql } from "drizzle-orm";
|
|
|
|
|
import {
|
|
|
|
|
index,
|
|
|
|
|
integer,
|
|
|
|
|
sqliteTable,
|
|
|
|
|
text,
|
|
|
|
|
uniqueIndex,
|
|
|
|
|
} from "drizzle-orm/sqlite-core";
|
|
|
|
|
|
|
|
|
|
export const partners = sqliteTable("partners", {
|
|
|
|
|
id: text("id").primaryKey(),
|
|
|
|
|
name: text("name").notNull(),
|
|
|
|
|
wecomName: text("wecom_name").notNull(),
|
|
|
|
|
owner: text("owner").notNull().default("运营组"),
|
|
|
|
|
claimedTotal: integer("claimed_total").notNull().default(0),
|
|
|
|
|
completedTotal: integer("completed_total").notNull().default(0),
|
|
|
|
|
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const tasks = sqliteTable(
|
|
|
|
|
"tasks",
|
|
|
|
|
{
|
|
|
|
|
id: text("id").primaryKey(),
|
|
|
|
|
name: text("name").notNull(),
|
|
|
|
|
brand: text("brand").notNull(),
|
|
|
|
|
quantity: integer("quantity").notNull(),
|
|
|
|
|
claimedQuantity: integer("claimed_quantity").notNull().default(0),
|
|
|
|
|
dueAt: text("due_at").notNull(),
|
|
|
|
|
status: text("status").notNull().default("active"),
|
|
|
|
|
sourceUrl: text("source_url").notNull().default(""),
|
|
|
|
|
sourceSheetId: text("source_sheet_id").notNull().default(""),
|
|
|
|
|
sourceSheetName: text("source_sheet_name").notNull().default(""),
|
|
|
|
|
sourceSyncedAt: text("source_synced_at"),
|
|
|
|
|
shareToken: text("share_token"),
|
|
|
|
|
collectionStartDate: text("collection_start_date"),
|
|
|
|
|
collectionDays: text("collection_days").notNull().default("[]"),
|
|
|
|
|
collectionScheduleUpdatedAt: text("collection_schedule_updated_at"),
|
|
|
|
|
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
},
|
|
|
|
|
(table) => [uniqueIndex("tasks_share_token_idx").on(table.shareToken)],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const contents = sqliteTable("contents", {
|
|
|
|
|
id: text("id").primaryKey(),
|
|
|
|
|
taskId: text("task_id").notNull(),
|
|
|
|
|
title: text("title").notNull(),
|
|
|
|
|
body: text("body").notNull().default(""),
|
|
|
|
|
imageAssets: text("image_assets").notNull().default("[]"),
|
|
|
|
|
status: text("status").notNull().default("available"),
|
|
|
|
|
source: text("source").notNull().default("飞书内容表"),
|
|
|
|
|
sourceRow: integer("source_row"),
|
|
|
|
|
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const accounts = sqliteTable(
|
|
|
|
|
"accounts",
|
|
|
|
|
{
|
|
|
|
|
id: text("id").primaryKey(),
|
|
|
|
|
platform: text("platform").notNull().default("小红书"),
|
|
|
|
|
platformUid: text("platform_uid").notNull(),
|
|
|
|
|
publicAccountId: text("public_account_id").notNull().default(""),
|
|
|
|
|
nickname: text("nickname").notNull(),
|
|
|
|
|
profileUrl: text("profile_url").notNull().default(""),
|
|
|
|
|
ipLocation: text("ip_location").notNull().default("待识别"),
|
|
|
|
|
followers: integer("followers").notNull().default(0),
|
|
|
|
|
postCount: integer("post_count").notNull().default(0),
|
|
|
|
|
avgViews: integer("avg_views").notNull().default(0),
|
|
|
|
|
firstSeenAt: text("first_seen_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
lastSeenAt: text("last_seen_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
uniqueIndex("accounts_platform_uid_idx").on(
|
|
|
|
|
table.platform,
|
|
|
|
|
table.platformUid,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const claims = sqliteTable(
|
|
|
|
|
"claims",
|
|
|
|
|
{
|
|
|
|
|
id: text("id").primaryKey(),
|
|
|
|
|
taskId: text("task_id").notNull(),
|
|
|
|
|
partnerId: text("partner_id").notNull(),
|
|
|
|
|
claimantName: text("claimant_name").notNull(),
|
|
|
|
|
claimToken: text("claim_token").notNull(),
|
|
|
|
|
quantity: integer("quantity").notNull(),
|
|
|
|
|
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
uniqueIndex("claims_claim_token_idx").on(table.claimToken),
|
|
|
|
|
index("claims_task_partner_created_idx").on(
|
|
|
|
|
table.taskId,
|
|
|
|
|
table.partnerId,
|
|
|
|
|
table.createdAt,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const delegationBundles = sqliteTable(
|
|
|
|
|
"delegation_bundles",
|
|
|
|
|
{
|
|
|
|
|
id: text("id").primaryKey(),
|
|
|
|
|
taskId: text("task_id").notNull(),
|
|
|
|
|
claimId: text("claim_id").notNull(),
|
|
|
|
|
partnerId: text("partner_id").notNull(),
|
|
|
|
|
label: text("label").notNull(),
|
|
|
|
|
shareToken: text("share_token").notNull(),
|
|
|
|
|
quantity: integer("quantity").notNull(),
|
|
|
|
|
status: text("status").notNull().default("active"),
|
|
|
|
|
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
revokedAt: text("revoked_at"),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
uniqueIndex("delegation_bundles_share_token_idx").on(table.shareToken),
|
|
|
|
|
index("delegation_bundles_claim_created_idx").on(
|
|
|
|
|
table.claimId,
|
|
|
|
|
table.createdAt,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const distributions = sqliteTable("distributions", {
|
|
|
|
|
id: text("id").primaryKey(),
|
|
|
|
|
taskId: text("task_id").notNull(),
|
|
|
|
|
contentId: text("content_id").notNull(),
|
|
|
|
|
partnerId: text("partner_id").notNull(),
|
|
|
|
|
claimId: text("claim_id"),
|
|
|
|
|
delegationBundleId: text("delegation_bundle_id"),
|
|
|
|
|
accountId: text("account_id"),
|
|
|
|
|
publishUrl: text("publish_url"),
|
|
|
|
|
publishTime: text("publish_time"),
|
|
|
|
|
publishScreenshotKey: text("publish_screenshot_key"),
|
|
|
|
|
status: text("status").notNull().default("claimed"),
|
|
|
|
|
claimedAt: text("claimed_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
screenshotKey: text("screenshot_key"),
|
|
|
|
|
ocrStatus: text("ocr_status").notNull().default("none"),
|
|
|
|
|
exposure: integer("exposure"),
|
|
|
|
|
views: integer("views"),
|
|
|
|
|
d2Likes: integer("d2_likes"),
|
|
|
|
|
d2Comments: integer("d2_comments"),
|
|
|
|
|
d2Collects: integer("d2_collects"),
|
|
|
|
|
d5Likes: integer("d5_likes"),
|
|
|
|
|
d5Comments: integer("d5_comments"),
|
|
|
|
|
d5Collects: integer("d5_collects"),
|
|
|
|
|
d7Likes: integer("d7_likes"),
|
|
|
|
|
d7Comments: integer("d7_comments"),
|
|
|
|
|
d7Collects: integer("d7_collects"),
|
|
|
|
|
latestLikes: integer("latest_likes"),
|
|
|
|
|
latestComments: integer("latest_comments"),
|
|
|
|
|
latestCollects: integer("latest_collects"),
|
|
|
|
|
collectionStatus: text("collection_status").notNull().default("pending"),
|
|
|
|
|
collectionStatusDescription: text("collection_status_description"),
|
|
|
|
|
collectionUpdatedAt: text("collection_updated_at"),
|
|
|
|
|
lastCollectionDay: integer("last_collection_day"),
|
|
|
|
|
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const collectionRuns = sqliteTable(
|
|
|
|
|
"collection_runs",
|
|
|
|
|
{
|
|
|
|
|
id: text("id").primaryKey(),
|
|
|
|
|
taskId: text("task_id").notNull(),
|
|
|
|
|
distributionId: text("distribution_id").notNull(),
|
|
|
|
|
scheduledDate: text("scheduled_date").notNull(),
|
|
|
|
|
scheduleDay: integer("schedule_day"),
|
|
|
|
|
scheduledAt: text("scheduled_at").notNull(),
|
|
|
|
|
status: text("status").notNull().default("pending"),
|
|
|
|
|
likes: integer("likes"),
|
|
|
|
|
comments: integer("comments"),
|
|
|
|
|
collects: integer("collects"),
|
|
|
|
|
statusDescription: text("status_description"),
|
|
|
|
|
startedAt: text("started_at"),
|
|
|
|
|
completedAt: text("completed_at"),
|
|
|
|
|
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
uniqueIndex("collection_runs_distribution_date_idx").on(
|
|
|
|
|
table.distributionId,
|
|
|
|
|
table.scheduledDate,
|
|
|
|
|
),
|
|
|
|
|
index("collection_runs_task_date_idx").on(
|
|
|
|
|
table.taskId,
|
|
|
|
|
table.scheduledDate,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
2026-08-07 11:06:20 +08:00
|
|
|
|
|
|
|
|
export const users = sqliteTable(
|
|
|
|
|
"users",
|
|
|
|
|
{
|
|
|
|
|
id: text("id").primaryKey(),
|
|
|
|
|
username: text("username").notNull(),
|
|
|
|
|
passwordHash: text("password_hash").notNull(),
|
|
|
|
|
passwordSalt: text("password_salt").notNull(),
|
|
|
|
|
passwordIterations: integer("password_iterations").notNull(),
|
|
|
|
|
role: text("role").notNull(),
|
|
|
|
|
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
uniqueIndex("users_username_idx").on(table.username),
|
|
|
|
|
uniqueIndex("users_single_super_admin_idx")
|
|
|
|
|
.on(table.role)
|
|
|
|
|
.where(sql`${table.role} = 'super_admin'`),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const authSessions = sqliteTable(
|
|
|
|
|
"auth_sessions",
|
|
|
|
|
{
|
|
|
|
|
tokenHash: text("token_hash").primaryKey(),
|
|
|
|
|
userId: text("user_id").notNull(),
|
|
|
|
|
expiresAt: text("expires_at").notNull(),
|
|
|
|
|
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
index("auth_sessions_user_id_idx").on(table.userId),
|
|
|
|
|
index("auth_sessions_expires_at_idx").on(table.expiresAt),
|
|
|
|
|
],
|
|
|
|
|
);
|