Rename to hkt.sh
This commit is contained in:
108
aff-monitor/db/init.js
Normal file
108
aff-monitor/db/init.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 数据库初始化 — 建表 + 种子数据
|
||||
*/
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const dbPath = path.resolve(__dirname, '..', process.env.DB_PATH || 'db/monitor.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
db.exec(`
|
||||
-- ========== 商家 ==========
|
||||
CREATE TABLE IF NOT EXISTS merchants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
website TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ========== 产品 ==========
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
merchant_id INTEGER NOT NULL REFERENCES merchants(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT,
|
||||
sku TEXT,
|
||||
price TEXT,
|
||||
in_stock INTEGER DEFAULT 0, -- 0=未知 1=有货 2=缺货
|
||||
last_checked TEXT,
|
||||
notes TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ========== Aff 链接 ==========
|
||||
CREATE TABLE IF NOT EXISTS aff_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
platform TEXT NOT NULL DEFAULT 'default', -- e.g. default / telegram / twitter
|
||||
url TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ========== Telegram 频道配置 ==========
|
||||
CREATE TABLE IF NOT EXISTS tg_channels (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
chat_id TEXT NOT NULL,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
notes TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ========== 监控任务 ==========
|
||||
CREATE TABLE IF NOT EXISTS monitor_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
tg_channel_id INTEGER REFERENCES tg_channels(id) ON DELETE SET NULL,
|
||||
cron_expr TEXT DEFAULT '*/5 * * * *',
|
||||
enabled INTEGER DEFAULT 1,
|
||||
last_run TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ========== 检测记录 ==========
|
||||
CREATE TABLE IF NOT EXISTS check_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL REFERENCES monitor_tasks(id) ON DELETE CASCADE,
|
||||
product_id INTEGER NOT NULL,
|
||||
status TEXT NOT NULL, -- in_stock / out_of_stock / error
|
||||
message TEXT,
|
||||
notified INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_check_logs_task ON check_logs(task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_check_logs_date ON check_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_merchant ON products(merchant_id);
|
||||
`);
|
||||
|
||||
console.log('✅ 数据库初始化完成:', dbPath);
|
||||
|
||||
// 种子数据(仅当 merchants 为空时插入)
|
||||
const count = db.prepare('SELECT count(*) AS c FROM merchants').get().c;
|
||||
if (count === 0) {
|
||||
const insertMerchant = db.prepare('INSERT INTO merchants (name, website, notes) VALUES (?, ?, ?)');
|
||||
const insertProduct = db.prepare('INSERT INTO products (merchant_id, name, url, price) VALUES (?, ?, ?, ?)');
|
||||
const insertChannel = db.prepare('INSERT INTO tg_channels (name, chat_id) VALUES (?, ?)');
|
||||
|
||||
db.transaction(() => {
|
||||
insertMerchant.run('示例商家 A', 'https://example-a.com', '这是一个演示商家');
|
||||
insertMerchant.run('示例商家 B', 'https://example-b.com', null);
|
||||
insertProduct.run(1, 'VPS 套餐 Basic', 'https://example-a.com/vps-basic', '$4.99/mo');
|
||||
insertProduct.run(1, 'VPS 套餐 Pro', 'https://example-a.com/vps-pro', '$9.99/mo');
|
||||
insertProduct.run(2, '独服 E3', 'https://example-b.com/dedi-e3', '€29/mo');
|
||||
insertChannel.run('测试频道', '-1001234567890');
|
||||
})();
|
||||
|
||||
console.log('🌱 种子数据已插入');
|
||||
}
|
||||
|
||||
db.close();
|
||||
47
aff-monitor/db/migrate-002-checker-fields.js
Normal file
47
aff-monitor/db/migrate-002-checker-fields.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Migration 002 — 添加检测相关字段到 products
|
||||
*
|
||||
* - check_mode: 检测模式 (keyword | api | manual)
|
||||
* - in_stock_keywords: 有货关键词 (逗号分隔)
|
||||
* - out_of_stock_keywords: 缺货关键词 (逗号分隔)
|
||||
*/
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const dbPath = path.resolve(__dirname, '..', process.env.DB_PATH || 'db/monitor.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
function ensureColumn(table, column, sql) {
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name);
|
||||
if (!cols.includes(column)) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${sql}`);
|
||||
console.log(`+ ${table}.${column}`);
|
||||
} else {
|
||||
console.log(` ${table}.${column} (already exists)`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureColumn('products', 'check_mode', "check_mode TEXT DEFAULT 'keyword'");
|
||||
ensureColumn('products', 'in_stock_keywords', 'in_stock_keywords TEXT');
|
||||
ensureColumn('products', 'out_of_stock_keywords', 'out_of_stock_keywords TEXT');
|
||||
|
||||
// 为 GoMami 测试样例设置默认关键词
|
||||
const gomami = db.prepare("SELECT id FROM products WHERE name LIKE '%Pulse%' AND check_mode IS NULL").all();
|
||||
if (gomami.length > 0) {
|
||||
const stmt = db.prepare(`
|
||||
UPDATE products
|
||||
SET check_mode = 'keyword',
|
||||
in_stock_keywords = 'Add to Cart,立即购买,加入购物车',
|
||||
out_of_stock_keywords = 'Out of Stock,缺货,售罄,Sold Out,Currently Unavailable'
|
||||
WHERE id = ?
|
||||
`);
|
||||
for (const row of gomami) {
|
||||
stmt.run(row.id);
|
||||
console.log(`🏷 设置 product#${row.id} 的检测关键词`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ migration-002 done:', dbPath);
|
||||
db.close();
|
||||
31
aff-monitor/db/migrate-003-public-fields.js
Normal file
31
aff-monitor/db/migrate-003-public-fields.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Migration 003 — 前台展示字段
|
||||
*
|
||||
* - is_public: 是否在前台展示 (0/1, 默认 1)
|
||||
* - is_featured: 是否推荐 (0/1, 默认 0)
|
||||
* - sort_order: 排序值 (数字越小越靠前, 默认 100)
|
||||
*/
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const dbPath = path.resolve(__dirname, '..', process.env.DB_PATH || 'db/monitor.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
function ensureColumn(table, column, sql) {
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name);
|
||||
if (!cols.includes(column)) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${sql}`);
|
||||
console.log(`+ ${table}.${column}`);
|
||||
} else {
|
||||
console.log(` ${table}.${column} (already exists)`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureColumn('products', 'is_public', 'is_public INTEGER DEFAULT 1');
|
||||
ensureColumn('products', 'is_featured', 'is_featured INTEGER DEFAULT 0');
|
||||
ensureColumn('products', 'sort_order', 'sort_order INTEGER DEFAULT 100');
|
||||
|
||||
console.log('✅ migration-003 done:', dbPath);
|
||||
db.close();
|
||||
86
aff-monitor/db/migrate-004-pid-slug.js
Normal file
86
aff-monitor/db/migrate-004-pid-slug.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Migration 004 — PID & Slug 字段
|
||||
*
|
||||
* - internal_pid: 系统内部编号,如 VPS-000001(唯一,自动生成)
|
||||
* - provider_pid: 商家产品 ID,从购买链接自动解析(如 pid=28)
|
||||
* - slug: 前台友好 URL,唯一
|
||||
*/
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const dbPath = path.resolve(__dirname, '..', process.env.DB_PATH || 'db/monitor.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
function ensureColumn(table, column, sql) {
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name);
|
||||
if (!cols.includes(column)) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${sql}`);
|
||||
console.log(`+ ${table}.${column}`);
|
||||
} else {
|
||||
console.log(` ${table}.${column} (already exists)`);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新字段
|
||||
ensureColumn('products', 'internal_pid', 'internal_pid TEXT');
|
||||
ensureColumn('products', 'provider_pid', 'provider_pid TEXT');
|
||||
ensureColumn('products', 'slug', 'slug TEXT');
|
||||
|
||||
// 创建唯一索引(如果尚不存在)
|
||||
try {
|
||||
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_products_internal_pid ON products(internal_pid) WHERE internal_pid IS NOT NULL');
|
||||
console.log('+ index: idx_products_internal_pid');
|
||||
} catch (e) {
|
||||
console.log(' index idx_products_internal_pid:', e.message);
|
||||
}
|
||||
try {
|
||||
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_products_slug ON products(slug) WHERE slug IS NOT NULL');
|
||||
console.log('+ index: idx_products_slug');
|
||||
} catch (e) {
|
||||
console.log(' index idx_products_slug:', e.message);
|
||||
}
|
||||
|
||||
// 为已有产品回填 internal_pid 和 slug
|
||||
const { generateInternalPid, generateSlug, parseProviderPid } = require('../src/utils/pidHelper');
|
||||
|
||||
const products = db.prepare('SELECT id, name, merchant_id, url, buy_url, internal_pid, slug, provider_pid FROM products').all();
|
||||
const updateStmt = db.prepare('UPDATE products SET internal_pid = ?, slug = ?, provider_pid = ? WHERE id = ?');
|
||||
|
||||
const getMerchantName = db.prepare('SELECT name FROM merchants WHERE id = ?');
|
||||
const existingSlugs = new Set(
|
||||
db.prepare("SELECT slug FROM products WHERE slug IS NOT NULL AND slug != ''").all().map(r => r.slug)
|
||||
);
|
||||
|
||||
db.transaction(() => {
|
||||
for (const p of products) {
|
||||
let ipid = p.internal_pid;
|
||||
let slug = p.slug;
|
||||
let ppid = p.provider_pid;
|
||||
|
||||
// 生成 internal_pid
|
||||
if (!ipid) {
|
||||
ipid = generateInternalPid(db);
|
||||
}
|
||||
|
||||
// 生成 slug
|
||||
if (!slug) {
|
||||
const merchant = getMerchantName.get(p.merchant_id);
|
||||
const merchantName = merchant ? merchant.name : '';
|
||||
slug = generateSlug(p.name, merchantName, existingSlugs);
|
||||
existingSlugs.add(slug);
|
||||
}
|
||||
|
||||
// 解析 provider_pid
|
||||
if (!ppid) {
|
||||
ppid = parseProviderPid(p.buy_url) || parseProviderPid(p.url) || null;
|
||||
}
|
||||
|
||||
updateStmt.run(ipid, slug, ppid, p.id);
|
||||
console.log(` product #${p.id}: internal_pid=${ipid}, slug=${slug}, provider_pid=${ppid || '(none)'}`);
|
||||
}
|
||||
})();
|
||||
|
||||
console.log('✅ migration-004 done:', dbPath);
|
||||
db.close();
|
||||
46
aff-monitor/db/migrate-005-aff-code.js
Normal file
46
aff-monitor/db/migrate-005-aff-code.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Migration 005 — Aff Code 字段
|
||||
*
|
||||
* - aff_code: 用户的 aff 标识值(如 "5"),用于自动生成 aff 链接
|
||||
* - aff_param: aff 参数名(默认 "aff"),支持不同商家用不同参数名
|
||||
*/
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const dbPath = path.resolve(__dirname, '..', process.env.DB_PATH || 'db/monitor.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
function ensureColumn(table, column, sql) {
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name);
|
||||
if (!cols.includes(column)) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${sql}`);
|
||||
console.log(`+ ${table}.${column}`);
|
||||
} else {
|
||||
console.log(` ${table}.${column} (already exists)`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureColumn('products', 'aff_code', 'aff_code TEXT');
|
||||
ensureColumn('products', 'aff_param', "aff_param TEXT DEFAULT 'aff'");
|
||||
|
||||
// 回填:从已有 buy_url 中解析 aff_code
|
||||
const { parseAffCode } = require('../src/utils/affHelper');
|
||||
|
||||
const products = db.prepare('SELECT id, buy_url, url, aff_code FROM products').all();
|
||||
const updateStmt = db.prepare('UPDATE products SET aff_code = ? WHERE id = ?');
|
||||
|
||||
db.transaction(() => {
|
||||
for (const p of products) {
|
||||
if (p.aff_code) continue; // 已有值,跳过
|
||||
const parsed = parseAffCode(p.buy_url) || parseAffCode(p.url);
|
||||
if (parsed) {
|
||||
updateStmt.run(parsed.value, p.id);
|
||||
console.log(` product #${p.id}: aff_code=${parsed.value} (from ${parsed.param}=${parsed.value})`);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
console.log('✅ migration-005 done:', dbPath);
|
||||
db.close();
|
||||
65
aff-monitor/db/migrate-006-settings.js
Normal file
65
aff-monitor/db/migrate-006-settings.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Migration 006 — Settings 表 + products.generated_aff_url
|
||||
*
|
||||
* - settings: 键值对存储系统配置(TG_BOT_TOKEN, TG_DEFAULT_CHANNEL_ID 等)
|
||||
* - products.generated_aff_url: 缓存自动生成的 aff 链接,方便查询
|
||||
*/
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const dbPath = path.resolve(__dirname, '..', process.env.DB_PATH || 'db/monitor.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// ── 创建 settings 表 ──
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
console.log('+ settings table');
|
||||
|
||||
// ── products 加 generated_aff_url 列 ──
|
||||
function ensureColumn(table, column, sql) {
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name);
|
||||
if (!cols.includes(column)) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${sql}`);
|
||||
console.log(`+ ${table}.${column}`);
|
||||
} else {
|
||||
console.log(` ${table}.${column} (already exists)`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureColumn('products', 'generated_aff_url', 'generated_aff_url TEXT');
|
||||
|
||||
// ── 回填 generated_aff_url(兼容老库:aff_code/aff_param 可能还不存在) ──
|
||||
const cols = db.prepare(`PRAGMA table_info(products)`).all().map(c => c.name);
|
||||
const hasAffCode = cols.includes('aff_code');
|
||||
const hasAffParam = cols.includes('aff_param');
|
||||
|
||||
if (hasAffCode) {
|
||||
const { buildAffUrl } = require('../src/utils/affHelper');
|
||||
const selectSql = hasAffParam
|
||||
? "SELECT id, buy_url, aff_code, aff_param FROM products WHERE aff_code IS NOT NULL AND aff_code != ''"
|
||||
: "SELECT id, buy_url, aff_code, NULL as aff_param FROM products WHERE aff_code IS NOT NULL AND aff_code != ''";
|
||||
const products = db.prepare(selectSql).all();
|
||||
const updateStmt = db.prepare('UPDATE products SET generated_aff_url = ? WHERE id = ?');
|
||||
|
||||
db.transaction(() => {
|
||||
for (const p of products) {
|
||||
const baseUrl = p.buy_url;
|
||||
if (!baseUrl) continue;
|
||||
const affUrl = buildAffUrl(baseUrl, p.aff_code, p.aff_param || 'aff');
|
||||
updateStmt.run(affUrl, p.id);
|
||||
console.log(` product #${p.id}: generated_aff_url=${affUrl}`);
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
console.log(' skip backfill: products.aff_code not found yet');
|
||||
}
|
||||
|
||||
console.log('✅ migration-006 done:', dbPath);
|
||||
db.close();
|
||||
28
aff-monitor/db/migrate-add-product-fields.js
Normal file
28
aff-monitor/db/migrate-add-product-fields.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const dbPath = path.resolve(__dirname, '..', process.env.DB_PATH || 'db/monitor.sqlite');
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
function ensureColumn(table, column, sql) {
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name);
|
||||
if (!cols.includes(column)) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${sql}`);
|
||||
console.log(`+ ${table}.${column}`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureColumn('products', 'location', 'location TEXT');
|
||||
ensureColumn('products', 'spec_summary', 'spec_summary TEXT');
|
||||
ensureColumn('products', 'traffic', 'traffic TEXT');
|
||||
ensureColumn('products', 'billing_cycle', 'billing_cycle TEXT');
|
||||
ensureColumn('products', 'coupon_code', 'coupon_code TEXT');
|
||||
ensureColumn('products', 'annual_price', 'annual_price TEXT');
|
||||
ensureColumn('products', 'tags', 'tags TEXT');
|
||||
ensureColumn('products', 'buy_url', 'buy_url TEXT');
|
||||
ensureColumn('products', 'push_intro', 'push_intro TEXT');
|
||||
|
||||
console.log('✅ migration done:', dbPath);
|
||||
db.close();
|
||||
BIN
aff-monitor/db/monitor.sqlite-shm
Normal file
BIN
aff-monitor/db/monitor.sqlite-shm
Normal file
Binary file not shown.
Reference in New Issue
Block a user