fix: make CMS content live and migrate to MySQL
This commit is contained in:
+59
-335
@@ -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 };
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import fs from 'fs';
|
||||
|
||||
const sqlite = new Database(process.env.SQLITE_DB_PATH || './cms.db', { readonly: true });
|
||||
const output = process.env.SQLITE_EXPORT_PATH || './sqlite-export.json';
|
||||
const tables = ['global_config', 'home_sections', 'site_texts', 'ameba_page', 'experts', 'evolution_thought', 'four_values', 'amb_states', 'evolution_process', 'ten_systems', 'services', 'cases', 'articles', 'self_check', 'diagnosis_questions', 'diagnosis_sessions', 'diagnosis_reports', 'diagnosis_leads', 'contact_leads', 'diagnosis_config', 'admins'];
|
||||
const data = {};
|
||||
for (const table of tables) { try { data[table] = sqlite.prepare(`SELECT * FROM ${table}`).all(); } catch { data[table] = []; } }
|
||||
fs.writeFileSync(output, JSON.stringify(data));
|
||||
sqlite.close();
|
||||
console.log(`SQLite export complete: ${output}`);
|
||||
@@ -0,0 +1,19 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import fs from 'fs';
|
||||
import 'dotenv/config';
|
||||
|
||||
const input = process.env.SQLITE_EXPORT_PATH || './sqlite-export.json';
|
||||
const data = JSON.parse(fs.readFileSync(input, 'utf8'));
|
||||
const connection = await mysql.createConnection({ host: process.env.DB_HOST || '127.0.0.1', port: Number(process.env.DB_PORT || 33306), user: process.env.DB_USER || 'admin', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'ameiba_cms', charset: 'utf8mb4' });
|
||||
for (const [table, rows] of Object.entries(data)) {
|
||||
for (const row of rows) {
|
||||
const columns = Object.keys(row);
|
||||
const escaped = columns.map(c => `\`${c}\``).join(',');
|
||||
const values = columns.map(() => '?').join(',');
|
||||
const updates = columns.filter(c => c !== 'id').map(c => `\`${c}\`=VALUES(\`${c}\`)`).join(',');
|
||||
await connection.query(`INSERT INTO \`${table}\` (${escaped}) VALUES (${values}) ON DUPLICATE KEY UPDATE ${updates || '`id`=VALUES(`id`)'}`, columns.map(c => row[c]));
|
||||
}
|
||||
console.log(`${table}: ${(rows || []).length} rows`);
|
||||
}
|
||||
await connection.end();
|
||||
console.log('MySQL import complete');
|
||||
+39
-27
@@ -21,6 +21,7 @@ const db = getDB();
|
||||
app.use(cors({ origin: process.env.CORS_ORIGIN || true }));
|
||||
app.use(express.json({ limit: '5mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use((req, res, next) => { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); next(); });
|
||||
app.use(authMiddleware);
|
||||
|
||||
// 静态:上传文件
|
||||
@@ -139,6 +140,8 @@ app.post('/api/contact/leads', (req, res) => {
|
||||
// 通用工具
|
||||
const nowISO = () => new Date().toISOString();
|
||||
const genId = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
|
||||
const queryOne = (sql, params=[]) => new Promise((resolve, reject) => db.get(sql, params, (err, row) => err ? reject(err) : resolve(row || null)));
|
||||
const queryAll = (sql, params=[]) => new Promise((resolve, reject) => db.all(sql, params, (err, rows) => err ? reject(err) : resolve(rows || [])));
|
||||
|
||||
// 全局配置
|
||||
app.get('/api/cms/config', (req, res) => {
|
||||
@@ -153,6 +156,16 @@ app.put('/api/cms/config', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// 可配置站点文案。页面只读取公开聚合中的 texts,后台通过此处集中维护。
|
||||
app.get('/api/cms/texts', (req, res) => db.all(`SELECT * FROM site_texts ORDER BY id`, (e, rows) => res.json({ code: 0, data: rows || [] })));
|
||||
app.put('/api/cms/texts', (req, res) => {
|
||||
const items = Array.isArray(req.body) ? req.body : [];
|
||||
const stmt = db.prepare(`INSERT INTO site_texts (id, label, value, updated_at) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE label=VALUES(label), value=VALUES(value), updated_at=VALUES(updated_at)`);
|
||||
Promise.all(items.filter(it => it?.id).map(it => stmt.run(it.id, it.label || it.id, it.value || '', nowISO())))
|
||||
.then(() => stmt.finalize(() => res.json({ code: 0, msg: '保存成功' })))
|
||||
.catch(err => res.status(500).json({ code: 500, msg: err.message }));
|
||||
});
|
||||
|
||||
// 首页板块
|
||||
app.get('/api/cms/home-sections', (req, res) => {
|
||||
db.all(`SELECT * FROM home_sections ORDER BY sort_order DESC`, (err, rows) => res.json({code:0, data: rows}));
|
||||
@@ -192,21 +205,20 @@ app.get('/api/cms/four-values', (req,res)=> db.all(`SELECT * FROM four_values OR
|
||||
app.post('/api/cms/four-values', (req,res)=>{
|
||||
const b=req.body; if(!b.key||!b.name) return res.status(400).json({code:400,msg:'key和名称必填'});
|
||||
const id=genId('fv');
|
||||
db.run(`INSERT INTO four_values (id, key, name, definition, description, image_url, sort_order, status, updated_at) VALUES (?,?,?,?,?,?,?,?,?)`,
|
||||
db.run(`INSERT INTO four_values (id, \`key\`, name, definition, description, image_url, sort_order, status, updated_at) VALUES (?,?,?,?,?,?,?,?,?)`,
|
||||
[id,b.key,b.name,b.definition,b.description,b.image_url,b.sort_order||0,b.status||'published',nowISO()], e=> res.json({code:0,data:{id}}));
|
||||
});
|
||||
app.put('/api/cms/four-values/:id', (req,res)=>{
|
||||
const b=req.body;
|
||||
db.run(`UPDATE four_values SET key=?, name=?, definition=?, description=?, image_url=?, sort_order=?, status=?, updated_at=? WHERE id=?`,
|
||||
db.run(`UPDATE four_values SET \`key\`=?, name=?, definition=?, description=?, image_url=?, sort_order=?, status=?, updated_at=? WHERE id=?`,
|
||||
[b.key,b.name,b.definition,b.description,b.image_url,b.sort_order,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
|
||||
});
|
||||
app.delete('/api/cms/four-values/:id', (req,res)=> db.run(`DELETE FROM four_values WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
|
||||
app.get('/api/cms/amb-states', (req,res)=> db.all(`SELECT * FROM amb_states ORDER BY sort_order DESC`, (e,r)=> res.json({code:0,data:r})));
|
||||
app.put('/api/cms/amb-states/:id', (req,res)=>{
|
||||
const b=req.body;
|
||||
// fixed_note 不可删但可编辑?按约束固定说明不可删
|
||||
db.run(`UPDATE amb_states SET name=?, definition=?, description=?, image_url=?, sort_order=?, status=?, updated_at=? WHERE id=?`,
|
||||
[b.name,b.definition,b.description,b.image_url,b.sort_order,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
|
||||
db.run(`UPDATE amb_states SET name=?, definition=?, description=?, image_url=?, sort_order=?, fixed_note=?, status=?, updated_at=? WHERE id=?`,
|
||||
[b.name,b.definition,b.description,b.image_url,b.sort_order,b.fixed_note,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
|
||||
});
|
||||
|
||||
// 方法与能力
|
||||
@@ -238,7 +250,7 @@ app.post('/api/cms/services', (req,res)=>{
|
||||
const id=genId('svc');
|
||||
const slug = b.slug || `svc-${Date.now()}`;
|
||||
db.run(`INSERT INTO services (id, slug, name, stage, intro, service_content, delivery, suitable, price, price_unit, advantage, detail_html, status, sort_weight, type, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[id,slug,b.name,b.stage,b.intro,b.service_content,b.delivery,b.suitable,b.price?Number(b.price):null,b.price_unit,b.advantage,b.detail_html,b.status||'draft',b.sort_weight||0,b.type||'custom',nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
|
||||
[id,slug,b.name,b.stage,b.intro,b.service_content,b.delivery,b.suitable,b.price?Number(b.price):null,b.price_unit,b.advantage,b.detail_html,b.status||'published',b.sort_weight||0,b.type||'custom',nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
|
||||
});
|
||||
app.put('/api/cms/services/:id', (req,res)=>{
|
||||
const b=req.body;
|
||||
@@ -256,7 +268,7 @@ app.post('/api/cms/cases', (req,res)=>{
|
||||
const id=genId('case');
|
||||
const slug = b.slug || `case-${Date.now()}`;
|
||||
db.run(`INSERT INTO cases (id, slug, industry, scale, amb_tags, value_tags, title, cover_url, original_state, goal, gap, root_cause, ameba_intervention, evolution_path, actions, phased_result, continuous_iteration, publish_time, status, sort_weight, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[id,slug,b.industry,b.scale,JSON.stringify(b.amb_tags||[]),JSON.stringify(b.value_tags||[]),b.title,b.cover_url,b.original_state,b.goal,b.gap,b.root_cause,b.ameba_intervention,b.evolution_path,b.actions,b.phased_result,b.continuous_iteration,b.publish_time||nowISO(),b.status||'draft',b.sort_weight||0,nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
|
||||
[id,slug,b.industry,b.scale,JSON.stringify(b.amb_tags||[]),JSON.stringify(b.value_tags||[]),b.title,b.cover_url,b.original_state,b.goal,b.gap,b.root_cause,b.ameba_intervention,b.evolution_path,b.actions,b.phased_result,b.continuous_iteration,b.publish_time||nowISO(),b.status||'published',b.sort_weight||0,nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
|
||||
});
|
||||
app.put('/api/cms/cases/:id', (req,res)=>{
|
||||
const b=req.body;
|
||||
@@ -273,7 +285,7 @@ app.post('/api/cms/articles', (req,res)=>{
|
||||
const id=genId('art');
|
||||
const slug = b.slug || `art-${Date.now()}`;
|
||||
db.run(`INSERT INTO articles (id, slug, title, subtitle, category, cover_url, content_html, publish_time, recommend_weight, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[id,slug,b.title,b.subtitle,b.category,b.cover_url,b.content_html,b.publish_time||nowISO(),b.recommend_weight||0,b.status||'draft',nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
|
||||
[id,slug,b.title,b.subtitle,b.category,b.cover_url,b.content_html,b.publish_time||nowISO(),b.recommend_weight||0,b.status||'published',nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}}));
|
||||
});
|
||||
app.put('/api/cms/articles/:id', (req,res)=>{
|
||||
const b=req.body;
|
||||
@@ -292,25 +304,25 @@ app.put('/api/cms/self-check', (req,res)=>{
|
||||
|
||||
// 前端公开聚合接口(供前端一次性拉取,不需要鉴权,使用GET放行)
|
||||
app.get('/api/cms/public', (req,res)=>{
|
||||
const data={};
|
||||
db.serialize(()=>{
|
||||
db.get(`SELECT * FROM global_config WHERE id=1`, (e,r)=> data.config=r);
|
||||
db.all(`SELECT * FROM home_sections WHERE visible=1 ORDER BY sort_order DESC`, (e,r)=> data.home_sections=r);
|
||||
db.get(`SELECT * FROM ameba_page WHERE id=1`, (e,r)=> data.ameba=r);
|
||||
db.all(`SELECT * FROM experts WHERE status='published' ORDER BY sort_weight DESC`, (e,r)=> data.experts=r);
|
||||
db.get(`SELECT * FROM evolution_thought WHERE id=1`, (e,r)=> data.evolution_thought=r);
|
||||
db.all(`SELECT * FROM four_values WHERE status='published' ORDER BY sort_order DESC`, (e,r)=> data.four_values=r);
|
||||
db.all(`SELECT * FROM amb_states WHERE status='published' ORDER BY sort_order DESC`, (e,r)=> data.amb_states=r);
|
||||
db.get(`SELECT * FROM evolution_process WHERE id=1`, (e,r)=> data.evolution_process=r);
|
||||
db.all(`SELECT * FROM ten_systems WHERE status='published' ORDER BY sort_order DESC`, (e,r)=> data.ten_systems=r);
|
||||
db.all(`SELECT * FROM services WHERE status='published' ORDER BY sort_weight DESC`, (e,r)=> data.services=r);
|
||||
db.all(`SELECT * FROM cases WHERE status='published' ORDER BY sort_weight DESC`, (e,r)=> data.cases=r);
|
||||
db.all(`SELECT * FROM articles WHERE status='published' ORDER BY recommend_weight DESC`, (e,r)=> data.articles=r);
|
||||
db.get(`SELECT * FROM self_check WHERE id=1`, (e,r)=> {
|
||||
data.self_check=r;
|
||||
res.json({code:0,data});
|
||||
});
|
||||
});
|
||||
Promise.all([
|
||||
queryOne(`SELECT * FROM global_config WHERE id=1`),
|
||||
queryAll(`SELECT * FROM home_sections WHERE visible=1 ORDER BY sort_order DESC`),
|
||||
queryOne(`SELECT * FROM ameba_page WHERE id=1`),
|
||||
queryAll(`SELECT * FROM experts WHERE status='published' ORDER BY sort_weight DESC`),
|
||||
queryOne(`SELECT * FROM evolution_thought WHERE id=1`),
|
||||
queryAll(`SELECT * FROM four_values WHERE status='published' ORDER BY sort_order DESC`),
|
||||
queryAll(`SELECT * FROM amb_states WHERE status='published' ORDER BY sort_order DESC`),
|
||||
queryOne(`SELECT * FROM evolution_process WHERE id=1`),
|
||||
queryAll(`SELECT * FROM ten_systems WHERE status='published' ORDER BY sort_order DESC`),
|
||||
queryAll(`SELECT * FROM services WHERE status='published' ORDER BY sort_weight DESC`),
|
||||
queryAll(`SELECT * FROM cases WHERE status='published' ORDER BY sort_weight DESC`),
|
||||
queryAll(`SELECT * FROM articles WHERE status='published' ORDER BY recommend_weight DESC`),
|
||||
queryOne(`SELECT * FROM self_check WHERE id=1`),
|
||||
queryAll(`SELECT id, label, value FROM site_texts ORDER BY id`),
|
||||
]).then(([config, home_sections, ameba, experts, evolution_thought, four_values, amb_states, evolution_process, ten_systems, services, cases, articles, self_check, texts]) => {
|
||||
const textMap = Object.fromEntries(texts.map(item => [item.id, item.value]));
|
||||
res.json({ code: 0, data: { config, home_sections, ameba, experts, evolution_thought, four_values, amb_states, evolution_process, ten_systems, services, cases, articles, self_check, texts: textMap } });
|
||||
}).catch(err => res.status(500).json({ code: 500, msg: err.message }));
|
||||
});
|
||||
|
||||
// ================= 诊断系统 =================
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/* 一次性迁移工具:保留服务器原 SQLite 内容到 MySQL。
|
||||
* 在服务器 npm install 新依赖前执行,SQLITE_DB_PATH 指向旧 cms.db。
|
||||
*/
|
||||
import Database from 'better-sqlite3';
|
||||
import mysql from 'mysql2/promise';
|
||||
import 'dotenv/config';
|
||||
|
||||
const sqlitePath = process.env.SQLITE_DB_PATH || './cms.db';
|
||||
const sqlite = new Database(sqlitePath, { readonly: true });
|
||||
const mysqlConfig = { host: process.env.DB_HOST || '127.0.0.1', port: Number(process.env.DB_PORT || 33306), user: process.env.DB_USER || 'admin', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'ameiba_cms', charset: 'utf8mb4' };
|
||||
const connection = await mysql.createConnection(mysqlConfig);
|
||||
|
||||
const tables = ['global_config', 'home_sections', 'site_texts', 'ameba_page', 'experts', 'evolution_thought', 'four_values', 'amb_states', 'evolution_process', 'ten_systems', 'services', 'cases', 'articles', 'self_check', 'diagnosis_questions', 'diagnosis_sessions', 'diagnosis_reports', 'diagnosis_leads', 'contact_leads', 'diagnosis_config', 'admins'];
|
||||
for (const table of tables) {
|
||||
let rows = [];
|
||||
try { rows = sqlite.prepare(`SELECT * FROM ${table}`).all(); } catch { continue; }
|
||||
for (const row of rows) {
|
||||
const columns = Object.keys(row);
|
||||
const escaped = columns.map(c => `\`${c}\``).join(',');
|
||||
const placeholders = columns.map(() => '?').join(',');
|
||||
await connection.query(`INSERT IGNORE INTO \`${table}\` (${escaped}) VALUES (${placeholders})`, columns.map(c => row[c]));
|
||||
}
|
||||
console.log(`${table}: ${rows.length} rows`);
|
||||
}
|
||||
await connection.end();
|
||||
sqlite.close();
|
||||
console.log('SQLite -> MySQL migration complete');
|
||||
+32
-4
@@ -16,7 +16,7 @@ db.run(`INSERT OR IGNORE INTO admins (id, username, password_hash, created_at) V
|
||||
db.get(`SELECT id FROM global_config WHERE id=1`, (err, row) => {
|
||||
if (!row) {
|
||||
db.run(`INSERT INTO global_config (id, site_title, slogan, seo_title, seo_desc, seo_keywords, hotline, contact_email, contact_address, cta_text, cta_link, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
['阿美巴咨询|企业进化陪跑', '让企业进化被看见、被度量、被落地', '阿美巴咨询|企业进化陪跑 · 体系化增长落地', '以企业进化为主线,四大价值坐标与A/M/B状态坐标为观察系', '阿美巴,企业进化,四大价值,A/M/B', '400-XXX-XXXX', 'contact@aiameiba.com', '【占位】成都市', '咨询获取方案', '/contact', now]);
|
||||
['阿美巴咨询|企业进化陪跑', '让企业进化被看见、被度量、被落地', '阿美巴咨询|企业进化陪跑 · 体系化增长落地', '以企业进化为主线,四大价值坐标与A/M/B状态坐标为观察系', '阿美巴,企业进化,四大价值,A/M/B', '400-XXX-XXXX', 'contact@aiameiba.com', '成都', '咨询获取方案', '/contact', now]);
|
||||
}
|
||||
});
|
||||
// 首页板块
|
||||
@@ -29,7 +29,35 @@ const sections = [
|
||||
['home-services', 'home-services', '产品服务 · 下一步行动', '禁止电商逻辑', '服务摘要', 1, 50],
|
||||
['home-insights', 'home-insights', '认知观察', '持续观察与沉淀', '观察摘要', 1, 40],
|
||||
];
|
||||
sections.forEach(s => db.run(`INSERT OR IGNORE INTO home_sections (id, key, title, subtitle, summary, visible, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [...s, now]));
|
||||
sections.forEach(s => db.run(`INSERT OR IGNORE INTO home_sections (id, \`key\`, title, subtitle, summary, visible, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [...s, now]));
|
||||
|
||||
const siteTexts = [
|
||||
['home.eyebrow', '首页首屏眉题', '阿美巴 · 企业进化陪跑'],
|
||||
['home.title', '首页首屏标题', '让企业进化\n被看见、被度量、被落地'],
|
||||
['home.description', '首页首屏描述', '以“企业进化”为主线,以四大价值坐标与 A/M/B 状态坐标为观察系,陪跑企业构建可复制、可持续的增长经营体系。'],
|
||||
['home.trust', '首页首屏信任说明', '30秒理解主张:进化思想 → 价值坐标 → 状态坐标 → 行动路径'],
|
||||
['home.primary_cta', '首页主按钮', '开始企业进化诊断'],
|
||||
['home.secondary_cta', '首页咨询按钮', '咨询获取方案'],
|
||||
['home.light_cta', '首页轻量对照按钮', '轻量对照'],
|
||||
['home.notice', '首页提示', '以真实业务内容为准,后台发布后前台即时生效。'],
|
||||
['home.about_title', '首页关于标题', '我们是谁'],
|
||||
['home.about_text', '首页关于描述', '围绕企业进化经营哲学,陪跑企业把战略、组织、机制与人的成长转化为可执行、可复盘的经营系统。'],
|
||||
['home.cases_title', '首页案例标题', '实践案例'],
|
||||
['home.services_title', '首页服务标题', '产品服务 · 下一步行动'],
|
||||
['home.insights_title', '首页洞察标题', '认知观察'],
|
||||
['global_cta.title', '全站行动标题', '下一步如何行动?'],
|
||||
['global_cta.description', '全站行动描述', '任意页面均可直接发起咨询,选择轻量对照或深度陪跑路径。'],
|
||||
['global_cta.primary', '全站主按钮', '咨询获取方案'],
|
||||
['global_cta.secondary', '全站次按钮', '看看你的企业'],
|
||||
['ameba.header_title', '阿美巴页标题', '阿美巴'],
|
||||
['ameba.header_description', '阿美巴页简介', '以企业进化经营哲学为底色,坚持利他、务实、陪跑,帮助企业把方法落到经营现场。'],
|
||||
['ameba.philosophy_title', '阿美巴哲学标题', '经营哲学'],
|
||||
['ameba.practice_title', '阿美巴实践标题', '服务理念'],
|
||||
['ameba.about_title', '阿美巴介绍标题', '我们是谁'],
|
||||
['ameba.about_text', '阿美巴介绍正文', '围绕企业进化与体系化增长,提供长期、务实、可复盘的企业管理咨询服务。'],
|
||||
['footer.note', '页脚说明', '内容以后台配置为准,持续更新。'],
|
||||
];
|
||||
siteTexts.forEach(([id, label, value]) => db.run(`INSERT OR IGNORE INTO site_texts (id, label, value, updated_at) VALUES (?, ?, ?, ?)`, [id, label, value, now]));
|
||||
// 阿美巴页面
|
||||
db.get(`SELECT id FROM ameba_page WHERE id=1`, (e, r) => {
|
||||
if (!r) db.run(`INSERT INTO ameba_page (id, brand_position, mission, philosophy, practice, updated_at) VALUES (1, ?, ?, ?, ?, ?)`,
|
||||
@@ -46,14 +74,14 @@ const fvs = [
|
||||
['fv-3', 'value3', '经营价值坐标', '观察经营机制', '经营价值说明【占位】', 80],
|
||||
['fv-4', 'value4', '人的价值坐标', '观察人的成长', '人的价值说明【占位】', 70],
|
||||
];
|
||||
fvs.forEach(f => db.run(`INSERT OR IGNORE INTO four_values (id, key, name, definition, description, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [...f, now]));
|
||||
fvs.forEach(f => db.run(`INSERT OR IGNORE INTO four_values (id, \`key\`, name, definition, description, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [...f, now]));
|
||||
// amb_states 固定说明
|
||||
const ambs = [
|
||||
['amb-A', 'A', 'A 状态|自进化', '具备自驱动', 'A状态说明【占位】', 'A/M/B不绑定企业规模的说明文案固定:同一企业不同价值维度可处不同状态', 100],
|
||||
['amb-M', 'M', 'M 状态|待突破', '存在卡点', 'M状态说明【占位】', '同上固定说明', 90],
|
||||
['amb-B', 'B', 'B 状态|待构建', '基础待构建', 'B状态说明【占位】', '同上固定说明', 80],
|
||||
];
|
||||
ambs.forEach(a => db.run(`INSERT OR IGNORE INTO amb_states (id, key, name, definition, description, fixed_note, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [...a, now]));
|
||||
ambs.forEach(a => db.run(`INSERT OR IGNORE INTO amb_states (id, \`key\`, name, definition, description, fixed_note, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [...a, now]));
|
||||
// ten_systems
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
db.run(`INSERT OR IGNORE INTO ten_systems (id, name, description, sort_order, updated_at) VALUES (?, ?, ?, ?, ?)`, [`sys-${i}`, `系统${i}`, `第${i}系统说明【占位】`, 100 - i * 5, now]);
|
||||
|
||||
Reference in New Issue
Block a user