fix: make CMS content live and migrate to MySQL

This commit is contained in:
Codex User
2026-09-08 16:03:38 +08:00
parent 3a99ae30c5
commit 7608dc5415
31 changed files with 614 additions and 962 deletions
+59 -335
View File
@@ -1,348 +1,72 @@
import Database from 'better-sqlite3';
import { fileURLToPath } from 'url';
import mysql from 'mysql2/promise';
import path from 'path';
import fs from 'fs';
import 'dotenv/config';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DB_PATH = process.env.CMS_DB_PATH || path.join(__dirname, '../cms.db');
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '../uploads');
const DB_HOST = process.env.DB_HOST || '127.0.0.1';
const DB_PORT = Number(process.env.DB_PORT || 33306);
const DB_USER = process.env.DB_USER || 'admin';
const DB_PASSWORD = process.env.DB_PASSWORD || '';
const DB_NAME = process.env.DB_NAME || 'ameiba_cms';
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.resolve(process.cwd(), 'uploads');
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
let pool;
const db = new Database(DB_PATH);
// 启用 WAL 模式提高性能
db.pragma('journal_mode = WAL');
export function initDB() {
// 全局配置
db.exec(`CREATE TABLE IF NOT EXISTS global_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
site_title TEXT,
slogan TEXT,
seo_title TEXT,
seo_desc TEXT,
seo_keywords TEXT,
hotline TEXT,
contact_email TEXT,
contact_address TEXT,
cta_text TEXT,
cta_link TEXT,
updated_at TEXT
)`);
// 首页板块
db.exec(`CREATE TABLE IF NOT EXISTS home_sections (
id TEXT PRIMARY KEY,
key TEXT UNIQUE,
title TEXT,
subtitle TEXT,
summary TEXT,
visible INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
updated_at TEXT
)`);
// 阿美巴页面
db.exec(`CREATE TABLE IF NOT EXISTS ameba_page (
id INTEGER PRIMARY KEY CHECK (id = 1),
brand_position TEXT,
mission TEXT,
philosophy TEXT,
practice TEXT,
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS experts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
title TEXT,
bio TEXT,
avatar_url TEXT,
sort_weight INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
created_at TEXT,
updated_at TEXT
)`);
// 企业进化
db.exec(`CREATE TABLE IF NOT EXISTS evolution_thought (
id INTEGER PRIMARY KEY CHECK (id = 1),
content TEXT,
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS four_values (
id TEXT PRIMARY KEY,
key TEXT UNIQUE,
name TEXT,
definition TEXT,
description TEXT,
image_url TEXT,
sort_order INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS amb_states (
id TEXT PRIMARY KEY,
key TEXT UNIQUE,
name TEXT,
definition TEXT,
description TEXT,
image_url TEXT,
sort_order INTEGER DEFAULT 0,
fixed_note TEXT,
status TEXT DEFAULT 'published',
updated_at TEXT
)`);
// 方法与能力
db.exec(`CREATE TABLE IF NOT EXISTS evolution_process (
id INTEGER PRIMARY KEY CHECK (id = 1),
steps TEXT,
scenarios TEXT,
description TEXT,
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS ten_systems (
id TEXT PRIMARY KEY,
name TEXT,
description TEXT,
sort_order INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
updated_at TEXT
)`);
// 产品服务
db.exec(`CREATE TABLE IF NOT EXISTS services (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
name TEXT,
stage TEXT,
intro TEXT,
service_content TEXT,
delivery TEXT,
suitable TEXT,
price REAL,
price_unit TEXT,
advantage TEXT,
detail_html TEXT,
status TEXT DEFAULT 'draft',
sort_weight INTEGER DEFAULT 0,
type TEXT DEFAULT 'custom',
created_at TEXT,
updated_at TEXT
)`);
// 案例 9段
db.exec(`CREATE TABLE IF NOT EXISTS cases (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
industry TEXT,
scale TEXT,
amb_tags TEXT,
value_tags TEXT,
title TEXT,
cover_url TEXT,
original_state TEXT,
goal TEXT,
gap TEXT,
root_cause TEXT,
ameba_intervention TEXT,
evolution_path TEXT,
actions TEXT,
phased_result TEXT,
continuous_iteration TEXT,
publish_time TEXT,
status TEXT DEFAULT 'draft',
sort_weight INTEGER DEFAULT 0,
created_at TEXT,
updated_at TEXT
)`);
// 认知观察
db.exec(`CREATE TABLE IF NOT EXISTS articles (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
title TEXT,
subtitle TEXT,
category TEXT,
cover_url TEXT,
content_html TEXT,
publish_time TEXT,
recommend_weight INTEGER DEFAULT 0,
status TEXT DEFAULT 'draft',
created_at TEXT,
updated_at TEXT
)`);
// 轻量自我对照
db.exec(`CREATE TABLE IF NOT EXISTS self_check (
id INTEGER PRIMARY KEY CHECK (id = 1),
guide TEXT,
questions TEXT,
logic_text TEXT,
next_guide TEXT,
updated_at TEXT
)`);
// 诊断问卷题目
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_questions (
id TEXT PRIMARY KEY,
group_key TEXT,
group_name TEXT,
type TEXT,
title TEXT,
options TEXT,
score_config TEXT,
sort_order INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
skip_logic TEXT,
created_at TEXT,
updated_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_sessions (
id TEXT PRIMARY KEY,
token TEXT UNIQUE,
answers TEXT,
progress TEXT,
created_at TEXT,
updated_at TEXT,
completed_at TEXT,
ip TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_reports (
id TEXT PRIMARY KEY,
session_id TEXT,
token TEXT UNIQUE,
share_token TEXT UNIQUE,
scores TEXT,
amb TEXT,
gap TEXT,
root_cause TEXT,
path_suggestion TEXT,
expires_at TEXT,
created_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_leads (
id TEXT PRIMARY KEY,
session_id TEXT,
report_id TEXT,
name TEXT,
company TEXT,
phone TEXT,
created_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS contact_leads (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
company TEXT,
phone TEXT NOT NULL,
need TEXT,
created_at TEXT
)`);
db.exec(`CREATE TABLE IF NOT EXISTS diagnosis_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
weights TEXT,
thresholds TEXT,
templates TEXT,
share_expire_days INTEGER DEFAULT 30,
updated_at TEXT
)`);
// 管理员
db.exec(`CREATE TABLE IF NOT EXISTS admins (
id TEXT PRIMARY KEY,
username TEXT UNIQUE,
password_hash TEXT,
created_at TEXT
)`);
async function ensureDatabase() {
const connection = await mysql.createConnection({ host: DB_HOST, port: DB_PORT, user: DB_USER, password: DB_PASSWORD, charset: 'utf8mb4' });
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${DB_NAME.replace(/`/g, '')}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
await connection.end();
}
// 创建兼容 sqlite3 回调风格的包装器
function mysqlSchema(sql) {
return sql.replace(/INTEGER PRIMARY KEY CHECK \(id = 1\)/gi, 'INT PRIMARY KEY')
.replace(/TEXT PRIMARY KEY/gi, 'VARCHAR(191) PRIMARY KEY')
.replace(/TEXT UNIQUE/gi, 'VARCHAR(191) UNIQUE')
.replace(/\bTEXT\b/gi, 'LONGTEXT').replace(/LONGTEXT UNIQUE/gi, 'VARCHAR(191) UNIQUE')
.replace(/LONGTEXT DEFAULT '([^']*)'/gi, "VARCHAR(191) DEFAULT '$1'")
.replace(/\bREAL\b/gi, 'DECIMAL(18,4)').replace(/\bINTEGER\b/gi, 'INT');
}
export async function initDB() {
await ensureDatabase();
pool = mysql.createPool({ host: DB_HOST, port: DB_PORT, user: DB_USER, password: DB_PASSWORD, database: DB_NAME, charset: 'utf8mb4', waitForConnections: true, connectionLimit: Number(process.env.DB_CONNECTION_LIMIT || 10), timezone: 'Z' });
const tables = [
`CREATE TABLE IF NOT EXISTS global_config (id INTEGER PRIMARY KEY CHECK (id = 1), site_title TEXT, slogan TEXT, seo_title TEXT, seo_desc TEXT, seo_keywords TEXT, hotline TEXT, contact_email TEXT, contact_address TEXT, cta_text TEXT, cta_link TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS home_sections (id TEXT PRIMARY KEY, \`key\` TEXT UNIQUE, title TEXT, subtitle TEXT, summary TEXT, visible INTEGER DEFAULT 1, sort_order INTEGER DEFAULT 0, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS site_texts (id VARCHAR(191) PRIMARY KEY, label TEXT, value LONGTEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS ameba_page (id INTEGER PRIMARY KEY CHECK (id = 1), brand_position TEXT, mission TEXT, philosophy TEXT, practice TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS experts (id TEXT PRIMARY KEY, name TEXT NOT NULL, title TEXT, bio TEXT, avatar_url TEXT, sort_weight INTEGER DEFAULT 0, status TEXT DEFAULT 'published', created_at TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS evolution_thought (id INTEGER PRIMARY KEY CHECK (id = 1), content TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS four_values (id TEXT PRIMARY KEY, \`key\` TEXT UNIQUE, name TEXT, definition TEXT, description TEXT, image_url TEXT, sort_order INTEGER DEFAULT 0, status TEXT DEFAULT 'published', updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS amb_states (id TEXT PRIMARY KEY, \`key\` TEXT UNIQUE, name TEXT, definition TEXT, description TEXT, image_url TEXT, sort_order INTEGER DEFAULT 0, fixed_note TEXT, status TEXT DEFAULT 'published', updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS evolution_process (id INTEGER PRIMARY KEY CHECK (id = 1), steps TEXT, scenarios TEXT, description TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS ten_systems (id TEXT PRIMARY KEY, name TEXT, description TEXT, sort_order INTEGER DEFAULT 0, status TEXT DEFAULT 'published', updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS services (id TEXT PRIMARY KEY, slug TEXT UNIQUE, name TEXT, stage TEXT, intro TEXT, service_content TEXT, delivery TEXT, suitable TEXT, price DECIMAL(18,4), price_unit TEXT, advantage TEXT, detail_html TEXT, status TEXT DEFAULT 'draft', sort_weight INTEGER DEFAULT 0, type TEXT DEFAULT 'custom', created_at TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS cases (id TEXT PRIMARY KEY, slug TEXT UNIQUE, industry TEXT, scale TEXT, amb_tags TEXT, value_tags TEXT, title TEXT, cover_url TEXT, original_state TEXT, goal TEXT, gap TEXT, root_cause TEXT, ameba_intervention TEXT, evolution_path TEXT, actions TEXT, phased_result TEXT, continuous_iteration TEXT, publish_time TEXT, status TEXT DEFAULT 'draft', sort_weight INTEGER DEFAULT 0, created_at TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS articles (id TEXT PRIMARY KEY, slug TEXT UNIQUE, title TEXT, subtitle TEXT, category TEXT, cover_url TEXT, content_html TEXT, publish_time TEXT, recommend_weight INTEGER DEFAULT 0, status TEXT DEFAULT 'draft', created_at TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS self_check (id INTEGER PRIMARY KEY CHECK (id = 1), guide TEXT, questions TEXT, logic_text TEXT, next_guide TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS diagnosis_questions (id TEXT PRIMARY KEY, group_key TEXT, group_name TEXT, type TEXT, title TEXT, options TEXT, score_config TEXT, sort_order INTEGER DEFAULT 0, status TEXT DEFAULT 'published', skip_logic TEXT, created_at TEXT, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS diagnosis_sessions (id TEXT PRIMARY KEY, token TEXT UNIQUE, answers TEXT, progress TEXT, created_at TEXT, updated_at TEXT, completed_at TEXT, ip TEXT)`,
`CREATE TABLE IF NOT EXISTS diagnosis_reports (id TEXT PRIMARY KEY, session_id TEXT, token TEXT UNIQUE, share_token TEXT UNIQUE, scores TEXT, amb TEXT, gap TEXT, root_cause TEXT, path_suggestion TEXT, expires_at TEXT, created_at TEXT)`,
`CREATE TABLE IF NOT EXISTS diagnosis_leads (id TEXT PRIMARY KEY, session_id TEXT, report_id TEXT, name TEXT, company TEXT, phone TEXT, created_at TEXT)`,
`CREATE TABLE IF NOT EXISTS contact_leads (id TEXT PRIMARY KEY, name TEXT NOT NULL, company TEXT, phone TEXT NOT NULL, need TEXT, created_at TEXT)`,
`CREATE TABLE IF NOT EXISTS diagnosis_config (id INTEGER PRIMARY KEY CHECK (id = 1), weights TEXT, thresholds TEXT, templates TEXT, share_expire_days INTEGER DEFAULT 30, updated_at TEXT)`,
`CREATE TABLE IF NOT EXISTS admins (id TEXT PRIMARY KEY, username TEXT UNIQUE, password_hash TEXT, created_at TEXT)`,
];
for (const table of tables) await pool.query(mysqlSchema(table));
}
function normalizeSql(sql) { return sql.replace(/INSERT OR IGNORE/gi, 'INSERT IGNORE'); }
export function getDB() {
if (!pool) throw new Error('Database is not initialized');
return {
// 同步查询单行
get(sql, params, callback) {
try {
if (typeof params === 'function') {
callback = params;
params = [];
}
const row = db.prepare(sql).get(...(Array.isArray(params) ? params : []));
callback(null, row || null);
} catch (err) {
callback(err);
}
},
// 同步查询所有行
all(sql, params, callback) {
try {
if (typeof params === 'function') {
callback = params;
params = [];
}
const rows = db.prepare(sql).all(...(Array.isArray(params) ? params : []));
callback(null, rows);
} catch (err) {
callback(err);
}
},
// 同步执行
run(sql, params, callback) {
try {
if (typeof params === 'function') {
callback = params;
params = [];
}
const stmt = db.prepare(sql);
const result = stmt.run(...(Array.isArray(params) ? params : []));
// 绑定 this 上下文
const context = {
lastID: result.lastInsertRowid,
changes: result.changes
};
if (callback) callback.call(context, null);
return context;
} catch (err) {
if (callback) callback(err);
throw err;
}
},
// 序列化执行(兼容性)
serialize(callback) {
if (callback) callback();
},
// 准备语句
prepare(sql) {
const stmt = db.prepare(sql);
return {
run(...args) {
const callback = args.pop();
try {
const result = stmt.run(...args);
callback.call({ lastID: result.lastInsertRowid, changes: result.changes }, null);
} catch (err) {
callback(err);
}
},
finalize(callback) {
if (callback) callback();
}
};
}
get(sql, params, callback) { if (typeof params === 'function') { callback = params; params = []; } pool.query(normalizeSql(sql), params || []).then(([rows]) => callback(null, rows[0] || null)).catch(callback); },
all(sql, params, callback) { if (typeof params === 'function') { callback = params; params = []; } pool.query(normalizeSql(sql), params || []).then(([rows]) => callback(null, rows)).catch(callback); },
run(sql, params, callback) { if (typeof params === 'function') { callback = params; params = []; } return pool.query(normalizeSql(sql), params || []).then(([result]) => { const context = { lastID: result.insertId, changes: result.affectedRows }; if (callback) callback.call(context, null); return context; }).catch(err => { if (callback) callback(err); else throw err; }); },
serialize(callback) { if (callback) callback(); },
prepare(sql) { return { run(...args) { const callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined; return pool.query(normalizeSql(sql), args).then(([result]) => { if (callback) callback.call({ lastID: result.insertId, changes: result.affectedRows }, null); }).catch(err => callback && callback(err)); }, finalize(callback) { if (callback) callback(); } }; },
};
}
export { DB_PATH, UPLOAD_DIR };
export { UPLOAD_DIR, DB_HOST, DB_PORT, DB_NAME };