Rename to hkt.sh

This commit is contained in:
mango
2026-03-21 01:10:53 +08:00
parent 76a263d0f9
commit 8f1171fe99
6676 changed files with 1724268 additions and 0 deletions

108
aff-monitor/db/init.js Normal file
View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

Binary file not shown.