xiaohongshufabu/server/lib/db.js

184 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const Database = require('better-sqlite3');
const path = require('path');
const DB_PATH = path.join(__dirname, '..', 'data.db');
function initDatabase() {
const db = new Database(DB_PATH);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS shops (
id TEXT PRIMARY KEY,
shop_id TEXT UNIQUE NOT NULL,
shop_name TEXT NOT NULL,
remark TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY,
shop_id TEXT NOT NULL,
title TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'generating', 'generated')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
product_id TEXT NOT NULL,
shop_id TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT DEFAULT '',
topics TEXT DEFAULT '[]',
image_paths TEXT DEFAULT '[]',
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'generating', 'generated', 'draft', 'approved', 'publishing', 'published', 'failed', 'rejected')),
use_status TEXT DEFAULT 'pending',
error_message TEXT DEFAULT '',
publish_result TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
published_at DATETIME DEFAULT NULL,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS configs (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
category TEXT DEFAULT 'general',
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS publish_tasks (
id TEXT PRIMARY KEY,
note_id TEXT NOT NULL,
shop_id TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'claimed', 'executing', 'completed', 'failed')),
claimed_by TEXT DEFAULT NULL,
claimed_at DATETIME DEFAULT NULL,
error_message TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE,
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS generation_jobs (
id TEXT PRIMARY KEY,
shop_id TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed')),
total_products INTEGER DEFAULT 0,
total_notes INTEGER DEFAULT 0,
completed_notes INTEGER DEFAULT 0,
failed_notes INTEGER DEFAULT 0,
current_product_id TEXT DEFAULT NULL,
current_product_title TEXT DEFAULT '',
message TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_products_shop_id ON products(shop_id);
CREATE INDEX IF NOT EXISTS idx_notes_product_id ON notes(product_id);
CREATE INDEX IF NOT EXISTS idx_notes_shop_id ON notes(shop_id);
CREATE INDEX IF NOT EXISTS idx_notes_status ON notes(status);
CREATE INDEX IF NOT EXISTS idx_publish_tasks_status ON publish_tasks(status);
CREATE INDEX IF NOT EXISTS idx_publish_tasks_shop_id ON publish_tasks(shop_id);
CREATE INDEX IF NOT EXISTS idx_generation_jobs_status ON generation_jobs(status);
CREATE INDEX IF NOT EXISTS idx_generation_jobs_shop_id ON generation_jobs(shop_id);
CREATE TABLE IF NOT EXISTS image_hashes (
id TEXT PRIMARY KEY,
note_id TEXT NOT NULL,
image_path TEXT NOT NULL,
phash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_image_hashes_phash ON image_hashes(phash);
`);
// Migration: add missing fields if not exist
const migrate = [
"ALTER TABLE notes ADD COLUMN image_prompt TEXT DEFAULT ''",
"ALTER TABLE notes ADD COLUMN worker_id TEXT DEFAULT NULL",
"ALTER TABLE notes ADD COLUMN claimed_at DATETIME DEFAULT NULL",
"ALTER TABLE notes ADD COLUMN use_status TEXT DEFAULT 'pending'",
"ALTER TABLE products ADD COLUMN reference_image TEXT DEFAULT ''",
];
for (const sql of migrate) {
try { db.exec(sql); } catch (e) { /* column already exists */ }
}
db.exec(`
UPDATE notes
SET use_status = 'used', status = 'approved'
WHERE status = 'published';
UPDATE notes
SET use_status = 'failed', status = 'approved'
WHERE status = 'failed'
AND EXISTS (
SELECT 1 FROM publish_tasks
WHERE publish_tasks.note_id = notes.id
AND publish_tasks.status = 'failed'
);
UPDATE notes
SET use_status = 'pending'
WHERE status IN ('approved', 'publishing')
AND (use_status IS NULL OR use_status = '');
`);
// Insert default configs
const insertConfig = db.prepare(`
INSERT OR IGNORE INTO configs (key, value, category) VALUES (?, ?, ?)
`);
const defaults = [
['ai_provider', 'deepseek', 'ai'],
['ai_model', 'deepseek-chat', 'ai'],
['ai_api_key', '', 'ai'],
['ai_base_url', 'https://api.deepseek.com', 'ai'],
['image_provider', 'agnes-image-2.1-flash', 'ai'],
['image_api_key', '', 'ai'],
['image_base_url', 'https://apihub.agnes-ai.com', 'ai'],
['publish_interval_min', '30', 'publish'],
['publish_interval_max', '90', 'publish'],
['daily_publish_limit', '20', 'publish'],
['max_consecutive_failures', '3', 'publish'],
['poll_interval', '20', 'extension'],
['title_prompt_template', '为该商品写一个小红书标题要求简洁有力、带emoji、包含关键词能吸引用户点击。商品信息{{title}}', 'prompt'],
['content_prompt_template', '为该商品写一篇小红书正文要求1.正文800-1200字分段落 2.话题3-5个相关标签 3.语气自然亲切,像朋友分享 4.包含emoji。商品信息{{title}}', 'prompt'],
['prompt_template', '为该商品写一篇小红书笔记,要求:\n1. 标题吸引人带emoji\n2. 正文800-1200字分段落\n3. 话题3-5个相关标签\n4. 语气自然亲切,像朋友分享\n\n商品信息{{title}}', 'prompt'],
['image_prompt_template', '{{title}},商品照片,自然光线,清晰细节,商业摄影,{{style}}风格', 'prompt'],
['image_style_options', '["清新","极简","ins风","高级感","小红书","日系"]', 'prompt'],
['image_count', '3', 'image_config'],
['image_size', '1K', 'image_config'],
['image_save_path', '', 'image_config'],
['ai_temperature', '0.8', 'ai'],
];
const insertMany = db.transaction(() => {
for (const [key, value, category] of defaults) {
insertConfig.run(key, value, category);
}
});
insertMany();
return db;
}
function getDatabase() {
return new Database(DB_PATH);
}
module.exports = { initDatabase, getDatabase, DB_PATH };