chore: initialize deployable website and CMS

This commit is contained in:
Codex User
2026-09-08 11:57:17 +08:00
commit 3a99ae30c5
57 changed files with 11205 additions and 0 deletions
+2358
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "ameba-cms-server",
"version": "1.0.0",
"description": "阿美巴传统官网1.0 CMS后台 - 轻量SQLite方案",
"type": "module",
"scripts": {
"dev": "node src/index.js",
"start": "node src/index.js",
"seed": "node src/seed.js",
"passwd": "node src/change-password.js"
},
"dependencies": {
"express": "^4.19.2",
"cors": "^2.8.5",
"multer": "^1.4.5-lts.1",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^11.0.0",
"dotenv": "^16.4.5",
"pdfkit": "^0.15.0"
},
"allowScripts": {
"better-sqlite3@11.10.0": true
}
}
+50
View File
@@ -0,0 +1,50 @@
import { initDB, getDB } from './db.js';
import bcrypt from 'bcryptjs';
import readline from 'readline';
await initDB();
const db = getDB();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (q) => new Promise(resolve => rl.question(q, resolve));
console.log('=== 修改CMS管理员密码 ===\n');
const username = await question('用户名 (默认 admin): ') || 'admin';
const newPassword = await question('新密码: ');
const confirmPassword = await question('确认密码: ');
if (newPassword !== confirmPassword) {
console.error('❌ 两次密码不一致!');
process.exit(1);
}
if (newPassword.length < 6) {
console.error('❌ 密码长度至少6位!');
process.exit(1);
}
const hash = bcrypt.hashSync(newPassword, 10);
db.get(`SELECT id FROM admins WHERE username = ?`, [username], (err, row) => {
if (err || !row) {
console.error(`❌ 用户 "${username}" 不存在!`);
process.exit(1);
}
db.run(`UPDATE admins SET password_hash = ? WHERE username = ?`, [hash, username], (err) => {
if (err) {
console.error('❌ 修改失败:', err.message);
process.exit(1);
}
console.log(`\n✅ 密码修改成功!`);
console.log(` 用户名: ${username}`);
console.log(` 新密码: ${newPassword}`);
rl.close();
setTimeout(() => process.exit(0), 500);
});
});
+348
View File
@@ -0,0 +1,348 @@
import Database from 'better-sqlite3';
import { fileURLToPath } from 'url';
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');
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
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
)`);
}
// 创建兼容 sqlite3 回调风格的包装器
export function getDB() {
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();
}
};
}
};
}
export { DB_PATH, UPLOAD_DIR };
+554
View File
@@ -0,0 +1,554 @@
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import multer from 'multer';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import PDFDocument from 'pdfkit';
import { initDB, getDB, UPLOAD_DIR } from './db.js';
import { signToken, verifyToken, authMiddleware } from './middleware/auth.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.CMS_PORT || 13001;
await initDB();
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(authMiddleware);
// 静态:上传文件
app.use('/uploads', express.static(UPLOAD_DIR));
// 静态:后台管理(构建后)
const adminDist = path.join(__dirname, '../../admin/dist');
if (fs.existsSync(adminDist)) {
app.use('/admin', express.static(adminDist));
app.get('/admin/*', (req, res) => res.sendFile(path.join(adminDist, 'index.html')));
}
// 防止搜索引擎索引后台
app.get('/admin', (req, res, next) => { res.set('X-Robots-Tag', 'noindex, nofollow'); next(); });
// 健康检查
app.get('/health', (req, res) => res.json({ status: 'ok', time: new Date().toISOString() }));
// 登录
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) return res.status(400).json({ code: 400, msg: '请输入账号密码' });
db.get(`SELECT * FROM admins WHERE username=?`, [username], (err, row) => {
if (err || !row) return res.status(401).json({ code: 401, msg: '账号或密码错误' });
if (!bcrypt.compareSync(password, row.password_hash)) return res.status(401).json({ code: 401, msg: '账号或密码错误' });
const token = signToken({ username: row.username, id: row.id });
res.json({ code: 0, data: { token, username } });
});
});
// 修改密码
app.post('/api/auth/change-password', (req, res) => {
const { oldPassword, newPassword } = req.body;
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) return res.status(401).json({ code: 401, msg: '未登录' });
try {
const tokenStr = authHeader.split(' ')[1];
const decoded = verifyToken(tokenStr);
const username = decoded.username;
if (!oldPassword || !newPassword) return res.status(400).json({ code: 400, msg: '请输入旧密码和新密码' });
if (newPassword.length < 6) return res.status(400).json({ code: 400, msg: '新密码长度至少6位' });
db.get(`SELECT * FROM admins WHERE username=?`, [username], (err, row) => {
if (err || !row) return res.status(404).json({ code: 404, msg: '用户不存在' });
if (!bcrypt.compareSync(oldPassword, row.password_hash)) return res.status(401).json({ code: 401, msg: '旧密码错误' });
const hash = bcrypt.hashSync(newPassword, 10);
db.run(`UPDATE admins SET password_hash=? WHERE username=?`, [hash, username], (err) => {
if (err) return res.status(500).json({ code: 500, msg: '修改失败' });
res.json({ code: 0, msg: '密码修改成功' });
});
});
} catch (e) {
return res.status(401).json({ code: 401, msg: '登录已过期' });
}
});
// 图片上传
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, UPLOAD_DIR),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
cb(null, crypto.randomBytes(16).toString('hex') + ext);
}
});
const allowedImageTypes = new Map([
['image/jpeg', '.jpg'], ['image/png', '.png'], ['image/webp', '.webp'], ['image/gif', '.gif']
]);
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024, files: 1 },
fileFilter: (req, file, cb) => {
const expectedExt = allowedImageTypes.get(file.mimetype);
const actualExt = path.extname(file.originalname).toLowerCase();
if (!expectedExt || expectedExt !== actualExt) return cb(new Error('仅支持 JPG、PNG、WEBP、GIF 图片'));
cb(null, true);
}
});
app.post('/api/cms/upload', upload.single('file'), (req, res) => {
if (!req.file) return res.status(400).json({ code: 400, msg: '未上传文件' });
const url = `/uploads/${req.file.filename}`;
res.json({ code: 0, data: { url } });
});
// 轻量内存限流:保护公开诊断与留资接口,生产环境应在 Nginx/WAF 再配置一层。
const requestBuckets = new Map();
function rateLimit({ windowMs = 60_000, max = 30 } = {}) {
return (req, res, next) => {
const forwarded = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim();
const key = `${req.ip || forwarded || req.socket.remoteAddress || 'unknown'}:${req.path}`;
const now = Date.now();
const old = requestBuckets.get(key);
if (!old || now - old.startedAt >= windowMs) requestBuckets.set(key, { startedAt: now, count: 1 });
else {
old.count += 1;
if (old.count > max) return res.status(429).json({ code: 429, msg: '请求过于频繁,请稍后再试' });
}
next();
};
}
const publicRateLimit = rateLimit({ windowMs: 60_000, max: 60 });
app.use('/api/diagnosis', publicRateLimit);
app.post('/api/contact/leads', (req, res) => {
const { name, company = '', phone, need = '' } = req.body || {};
if (!name || !phone) return res.status(400).json({ code: 400, msg: '姓名和手机号必填' });
if (!/^1[3-9]\d{9}$/.test(String(phone))) return res.status(400).json({ code: 400, msg: '手机号格式错误' });
const id = genId('contact');
db.run(`INSERT INTO contact_leads (id, name, company, phone, need, created_at) VALUES (?,?,?,?,?,?)`,
[id, String(name).trim().slice(0, 80), String(company).trim().slice(0, 160), String(phone), String(need).trim().slice(0, 2000), nowISO()], err => {
if (err) return res.status(500).json({ code: 500, msg: '提交失败,请稍后再试' });
res.json({ code: 0, msg: '提交成功' });
});
});
// 通用工具
const nowISO = () => new Date().toISOString();
const genId = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
// 全局配置
app.get('/api/cms/config', (req, res) => {
db.get(`SELECT * FROM global_config WHERE id=1`, (err, row) => res.json({ code: 0, data: row }));
});
app.put('/api/cms/config', (req, res) => {
const f = req.body;
db.run(`UPDATE global_config SET site_title=?, slogan=?, seo_title=?, seo_desc=?, seo_keywords=?, hotline=?, contact_email=?, contact_address=?, cta_text=?, cta_link=?, updated_at=? WHERE id=1`,
[f.site_title, f.slogan, f.seo_title, f.seo_desc, f.seo_keywords, f.hotline, f.contact_email, f.contact_address, f.cta_text, f.cta_link, nowISO()], function(err){
if(err) return res.status(500).json({code:500, msg: err.message});
res.json({code:0, msg:'保存成功'});
});
});
// 首页板块
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}));
});
app.put('/api/cms/home-sections', (req, res) => {
const list = req.body; // array
if(!Array.isArray(list)) return res.status(400).json({code:400, msg:'格式错误'});
const stmt = db.prepare(`UPDATE home_sections SET title=?, subtitle=?, summary=?, visible=?, sort_order=?, updated_at=? WHERE id=?`);
list.forEach(it=> stmt.run(it.title, it.subtitle, it.summary, it.visible?1:0, it.sort_order, nowISO(), it.id));
stmt.finalize(()=> res.json({code:0, msg:'保存成功'}));
});
// 阿美巴页面
app.get('/api/cms/ameba', (req, res)=> db.get(`SELECT * FROM ameba_page WHERE id=1`, (e,r)=> res.json({code:0,data:r})));
app.put('/api/cms/ameba', (req,res)=>{
const f=req.body;
db.run(`UPDATE ameba_page SET brand_position=?, mission=?, philosophy=?, practice=?, updated_at=? WHERE id=1`, [f.brand_position,f.mission,f.philosophy,f.practice, nowISO()], e=> res.json({code:0,msg:'保存成功'}));
});
app.get('/api/cms/experts', (req,res)=> db.all(`SELECT * FROM experts ORDER BY sort_weight DESC, created_at DESC`, (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/experts', (req,res)=>{
const b=req.body; if(!b.name) return res.status(400).json({code:400,msg:'姓名必填'});
const id=genId('exp');
db.run(`INSERT INTO experts (id, name, title, bio, avatar_url, sort_weight, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)`,
[id,b.name,b.title,b.bio,b.avatar_url,b.sort_weight||0,b.status||'published',nowISO(),nowISO()], e=> res.json({code:0,data:{id}}));
});
app.put('/api/cms/experts/:id', (req,res)=>{
const b=req.body;
db.run(`UPDATE experts SET name=?, title=?, bio=?, avatar_url=?, sort_weight=?, status=?, updated_at=? WHERE id=?`,
[b.name,b.title,b.bio,b.avatar_url,b.sort_weight,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/experts/:id', (req,res)=> db.run(`DELETE FROM experts WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 企业进化
app.get('/api/cms/evolution/thought', (req,res)=> db.get(`SELECT * FROM evolution_thought WHERE id=1`, (e,r)=> res.json({code:0,data:r})));
app.put('/api/cms/evolution/thought', (req,res)=> db.run(`UPDATE evolution_thought SET content=?, updated_at=? WHERE id=1`,[req.body.content,nowISO()], e=> res.json({code:0,msg:'保存成功'})));
app.get('/api/cms/four-values', (req,res)=> db.all(`SELECT * FROM four_values ORDER BY sort_order DESC`, (e,r)=> res.json({code:0,data:r})));
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 (?,?,?,?,?,?,?,?,?)`,
[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=?`,
[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:'更新成功'}));
});
// 方法与能力
app.get('/api/cms/evolution-process', (req,res)=> db.get(`SELECT * FROM evolution_process WHERE id=1`, (e,r)=> res.json({code:0,data:r})));
app.put('/api/cms/evolution-process', (req,res)=>{
const b=req.body;
db.run(`UPDATE evolution_process SET steps=?, scenarios=?, description=?, updated_at=? WHERE id=1`,[b.steps,b.scenarios,b.description,nowISO()], e=> res.json({code:0,msg:'保存成功'}));
});
app.get('/api/cms/ten-systems', (req,res)=> db.all(`SELECT * FROM ten_systems ORDER BY sort_order DESC`, (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/ten-systems', (req,res)=>{
const b=req.body; if(!b.name) return res.status(400).json({code:400,msg:'名称必填'});
const id=genId('sys');
db.run(`INSERT INTO ten_systems (id, name, description, sort_order, status, updated_at) VALUES (?,?,?,?,?,?)`,
[id,b.name,b.description,b.sort_order||0,b.status||'published',nowISO()], e=> res.json({code:0,data:{id}}));
});
app.put('/api/cms/ten-systems/:id', (req,res)=>{
const b=req.body;
db.run(`UPDATE ten_systems SET name=?, description=?, sort_order=?, status=?, updated_at=? WHERE id=?`,
[b.name,b.description,b.sort_order,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/ten-systems/:id', (req,res)=> db.run(`DELETE FROM ten_systems WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 产品服务
app.get('/api/cms/services', (req,res)=> db.all(`SELECT * FROM services ORDER BY sort_weight DESC, created_at DESC`, (e,r)=> res.json({code:0,data:r})));
app.get('/api/cms/services/:id', (req,res)=> db.get(`SELECT * FROM services WHERE id=?`,[req.params.id], (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/services', (req,res)=>{
const b=req.body; if(!b.name) return res.status(400).json({code:400,msg:'名称必填'});
if(b.price && isNaN(Number(b.price))) return res.status(400).json({code:400,msg:'价格格式错误'});
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}}));
});
app.put('/api/cms/services/:id', (req,res)=>{
const b=req.body;
if(b.price && isNaN(Number(b.price))) return res.status(400).json({code:400,msg:'价格格式错误'});
db.run(`UPDATE services SET slug=?, name=?, stage=?, intro=?, service_content=?, delivery=?, suitable=?, price=?, price_unit=?, advantage=?, detail_html=?, status=?, sort_weight=?, type=?, updated_at=? WHERE id=?`,
[b.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,b.sort_weight,b.type,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/services/:id', (req,res)=> db.run(`DELETE FROM services WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 案例
app.get('/api/cms/cases', (req,res)=> db.all(`SELECT * FROM cases ORDER BY sort_weight DESC, created_at DESC`, (e,r)=> res.json({code:0,data:r})));
app.get('/api/cms/cases/:id', (req,res)=> db.get(`SELECT * FROM cases WHERE id=?`,[req.params.id], (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/cases', (req,res)=>{
const b=req.body; if(!b.title) return res.status(400).json({code:400,msg:'标题必填'});
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}}));
});
app.put('/api/cms/cases/:id', (req,res)=>{
const b=req.body;
db.run(`UPDATE cases SET 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=?, updated_at=? WHERE id=?`,
[b.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,b.status,b.sort_weight,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/cases/:id', (req,res)=> db.run(`DELETE FROM cases WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 文章
app.get('/api/cms/articles', (req,res)=> db.all(`SELECT * FROM articles ORDER BY recommend_weight DESC, created_at DESC`, (e,r)=> res.json({code:0,data:r})));
app.get('/api/cms/articles/:id', (req,res)=> db.get(`SELECT * FROM articles WHERE id=?`,[req.params.id], (e,r)=> res.json({code:0,data:r})));
app.post('/api/cms/articles', (req,res)=>{
const b=req.body; if(!b.title) return res.status(400).json({code:400,msg:'标题必填'});
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}}));
});
app.put('/api/cms/articles/:id', (req,res)=>{
const b=req.body;
db.run(`UPDATE articles SET slug=?, title=?, subtitle=?, category=?, cover_url=?, content_html=?, publish_time=?, recommend_weight=?, status=?, updated_at=? WHERE id=?`,
[b.slug,b.title,b.subtitle,b.category,b.cover_url,b.content_html,b.publish_time,b.recommend_weight,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'}));
});
app.delete('/api/cms/articles/:id', (req,res)=> db.run(`DELETE FROM articles WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})));
// 自我对照
app.get('/api/cms/self-check', (req,res)=> db.get(`SELECT * FROM self_check WHERE id=1`, (e,r)=> res.json({code:0,data:r})));
app.put('/api/cms/self-check', (req,res)=>{
const b=req.body;
db.run(`UPDATE self_check SET guide=?, questions=?, logic_text=?, next_guide=?, updated_at=? WHERE id=1`,
[b.guide, b.questions, b.logic_text, b.next_guide, nowISO()], e=> res.json({code:0,msg:'保存成功'}));
});
// 前端公开聚合接口(供前端一次性拉取,不需要鉴权,使用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});
});
});
});
// ================= 诊断系统 =================
// 诊断问题公开
app.get('/api/diagnosis/questions', (req,res)=>{
db.all(`SELECT * FROM diagnosis_questions WHERE status='published' ORDER BY sort_order DESC`, (e,rows)=>{
const parsed = (rows||[]).map(r=>({ ...r, options: JSON.parse(r.options||'[]'), skip_logic: r.skip_logic?JSON.parse(r.skip_logic):null }))
res.json({code:0,data:parsed})
})
})
// CMS 管理题目
app.get('/api/cms/diagnosis/questions', (req,res)=>{
db.all(`SELECT * FROM diagnosis_questions ORDER BY sort_order DESC`, (e,rows)=> res.json({code:0,data:rows}))
})
app.post('/api/cms/diagnosis/questions', (req,res)=>{
const b=req.body; if(!b.title) return res.status(400).json({code:400,msg:'标题必填'})
const id=b.id||`dq-${Date.now()}`
db.run(`INSERT INTO diagnosis_questions (id, group_key, group_name, type, title, options, score_config, sort_order, status, skip_logic, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
[id,b.group_key,b.group_name,b.type||'single',b.title, JSON.stringify(b.options||[]), JSON.stringify(b.score_config||{}), b.sort_order||0, b.status||'published', JSON.stringify(b.skip_logic||null), nowISO(), nowISO()], e=> res.json({code:0,data:{id}}))
})
app.put('/api/cms/diagnosis/questions/:id', (req,res)=>{
const b=req.body
db.run(`UPDATE diagnosis_questions SET group_key=?, group_name=?, type=?, title=?, options=?, score_config=?, sort_order=?, status=?, skip_logic=?, updated_at=? WHERE id=?`,
[b.group_key,b.group_name,b.type,b.title, JSON.stringify(b.options||[]), JSON.stringify(b.score_config||{}), b.sort_order,b.status, JSON.stringify(b.skip_logic||null), nowISO(), req.params.id], e=> res.json({code:0,msg:'更新成功'}))
})
app.delete('/api/cms/diagnosis/questions/:id', (req,res)=> db.run(`DELETE FROM diagnosis_questions WHERE id=?`,[req.params.id], e=> res.json({code:0,msg:'删除成功'})))
// 诊断配置
app.get('/api/cms/diagnosis/config', (req,res)=> db.get(`SELECT * FROM diagnosis_config WHERE id=1`, (e,r)=> res.json({code:0,data:r ? {...r, weights: JSON.parse(r.weights||'{}'), thresholds: JSON.parse(r.thresholds||'{}'), templates: JSON.parse(r.templates||'{}')} : null})))
app.put('/api/cms/diagnosis/config', (req,res)=>{
const b=req.body
db.run(`UPDATE diagnosis_config SET weights=?, thresholds=?, templates=?, share_expire_days=?, updated_at=? WHERE id=1`,
[JSON.stringify(b.weights||{}), JSON.stringify(b.thresholds||{}), JSON.stringify(b.templates||{}), b.share_expire_days||30, nowISO()], e=> res.json({code:0,msg:'保存成功'}))
})
// 诊断会话
app.post('/api/diagnosis/sessions', (req,res)=>{
const id=genId('dsess')
const token=crypto.randomBytes(16).toString('hex')
const now=nowISO()
const ip=req.headers['x-forwarded-for']||req.socket.remoteAddress||''
db.run(`INSERT INTO diagnosis_sessions (id, token, answers, progress, created_at, updated_at, ip) VALUES (?,?,?,?,?,?,?)`,
[id, token, JSON.stringify({}), JSON.stringify({}), now, now, String(ip)], e=> res.json({code:0,data:{id,token}}))
})
app.get('/api/diagnosis/sessions/:token', (req,res)=>{
db.get(`SELECT * FROM diagnosis_sessions WHERE token=?`,[req.params.token], (e,row)=>{
if(!row) return res.status(404).json({code:404,msg:'会话不存在'})
res.json({code:0,data:{...row, answers: JSON.parse(row.answers||'{}'), progress: JSON.parse(row.progress||'{}')}})
})
})
app.put('/api/diagnosis/sessions/:token', (req,res)=>{
const {answers, progress}=req.body
db.run(`UPDATE diagnosis_sessions SET answers=?, progress=?, updated_at=? WHERE token=?`,
[JSON.stringify(answers||{}), JSON.stringify(progress||{}), nowISO(), req.params.token], e=> {
if (e) return res.status(500).json({code:500,msg:'保存失败'})
res.json({code:0,msg:'保存成功'})
})
})
// 分析引擎
function calcDiagnosis(answers, questions, cfg){
const weights = cfg.weights || {value1:1,value2:1,value3:1,value4:1}
const thr = cfg.thresholds || {B_max:2.5, M_max:3.8}
const groups={value1:[],value2:[],value3:[],value4:[]}
questions.forEach(q=>{
const ans = answers[q.id]
if(ans===undefined||ans===null) return
let score=0
const opts = typeof q.options==='string'? JSON.parse(q.options): q.options
if(q.type==='single' || q.type==='scale'){
const opt = opts.find(o=> String(o.label)===String(ans) || String(o.score)===String(ans) || o.label===ans)
score = opt? Number(opt.score): Number(ans)||0
} else if(q.type==='multiple'){
const arr = Array.isArray(ans)? ans:[ans]
const scores = arr.map(v=>{
const o=opts.find(x=> String(x.label)===String(v))
return o? Number(o.score):0
})
score = scores.length? scores.reduce((a,b)=>a+b,0)/scores.length:0
}
if(!groups[q.group_key]) groups[q.group_key]=[]
groups[q.group_key].push(score)
})
const scores={}
Object.keys(groups).forEach(k=>{
const arr=groups[k]
const average = arr.length ? arr.reduce((a,b)=>a+b,0)/arr.length : 0
scores[k] = Number(average.toFixed(2))
})
const amb={}
Object.keys(scores).forEach(k=>{
const s=scores[k]
let st='B'
if(s>thr.M_max) st='A'
else if(s>thr.B_max) st='M'
else st='B'
amb[k]=st
})
// 差距:以目标 A(5分) 为基准
const gap={}
Object.keys(scores).forEach(k=> gap[k]= Number((5 - scores[k]).toFixed(2)))
// 根因:最低分的维度
let minK=null, minV=Infinity
Object.keys(scores).forEach(k=>{ if(scores[k]<minV){minV=scores[k]; minK=k}})
const rootCause = minK? {dimension:minK, score:minV, label: minK==='value1'?'价值创造':minK==='value2'?'价值传递':minK==='value3'?'价值交付':'价值支持', reason: `该维度得分最低(${minV}),为当前核心瓶颈`} : null
// 路径建议
const scoreKeys = Object.keys(scores)
const weightedTotal = scoreKeys.reduce((sum, key) => sum + scores[key] * Number(weights[key] ?? 1), 0)
const weightTotal = scoreKeys.reduce((sum, key) => sum + Number(weights[key] ?? 1), 0)
const overall = weightedTotal / Math.max(1, weightTotal)
let path='B态全案'
if(overall>3.8) path='A态入门'
else if(overall>2.5) path='M态进阶'
const pathSuggestion={overall: Number(overall.toFixed(2)), stage: path, recommendation: path==='A态入门'?'聚焦单点突破与复制':path==='M态进阶'?'完善机制与协同': '系统化构建四大价值'}
return {scores, amb, gap, rootCause, pathSuggestion}
}
app.post('/api/diagnosis/sessions/:token/submit', (req,res)=>{
db.get(`SELECT * FROM diagnosis_sessions WHERE token=?`,[req.params.token], (e,sess)=>{
if(!sess) return res.status(404).json({code:404,msg:'会话不存在'})
const answers = req.body.answers || JSON.parse(sess.answers||'{}')
db.get(`SELECT * FROM diagnosis_config WHERE id=1`, (e2,cfg)=>{
const cfgObj = cfg? {weights: JSON.parse(cfg.weights||'{}'), thresholds: JSON.parse(cfg.thresholds||'{}'), templates: JSON.parse(cfg.templates||'{}'), share_expire_days: cfg.share_expire_days||30 } : {weights:{},thresholds:{},templates:{},share_expire_days:30}
db.all(`SELECT * FROM diagnosis_questions WHERE status='published' ORDER BY sort_order DESC`, (e3, qs)=>{
const result = calcDiagnosis(answers, qs||[], cfgObj)
const reportId=genId('drep')
const token=crypto.randomBytes(16).toString('hex')
const shareToken=crypto.randomBytes(16).toString('hex')
const expires=new Date(Date.now()+ (cfgObj.share_expire_days*24*3600*1000)).toISOString()
const now=nowISO()
db.run(`UPDATE diagnosis_sessions SET answers=?, completed_at=?, updated_at=? WHERE token=?`, [JSON.stringify(answers), now, now, req.params.token])
db.run(`INSERT INTO diagnosis_reports (id, session_id, token, share_token, scores, amb, gap, root_cause, path_suggestion, expires_at, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
[reportId, sess.id, token, shareToken, JSON.stringify(result.scores), JSON.stringify(result.amb), JSON.stringify(result.gap), JSON.stringify(result.rootCause), JSON.stringify(result.pathSuggestion), expires, now], e4=>{
res.json({code:0,data:{reportId, token, shareToken, ...result, expires_at: expires}})
})
})
})
})
})
// 报告查询
app.get('/api/diagnosis/reports/:token', (req,res)=>{
db.get(`SELECT * FROM diagnosis_reports WHERE token=?`,[req.params.token], (e,row)=>{
if(!row) return res.status(404).json({code:404,msg:'报告不存在'})
if(new Date(row.expires_at) < new Date()) return res.status(410).json({code:410,msg:'报告已过期,请重新诊断'})
res.json({code:0,data:{...row, scores: JSON.parse(row.scores||'{}'), amb: JSON.parse(row.amb||'{}'), gap: JSON.parse(row.gap||'{}'), root_cause: JSON.parse(row.root_cause||'{}'), path_suggestion: JSON.parse(row.path_suggestion||'{}')}})
})
})
app.get('/api/diagnosis/share/:shareToken', (req,res)=>{
db.get(`SELECT * FROM diagnosis_reports WHERE share_token=?`,[req.params.shareToken], (e,row)=>{
if(!row) return res.status(404).json({code:404,msg:'分享不存在'})
if(new Date(row.expires_at) < new Date()) return res.status(410).json({code:410,msg:'分享已过期'})
res.json({code:0,data:{...row, scores: JSON.parse(row.scores||'{}'), amb: JSON.parse(row.amb||'{}'), gap: JSON.parse(row.gap||'{}'), root_cause: JSON.parse(row.root_cause||'{}'), path_suggestion: JSON.parse(row.path_suggestion||'{}')}})
})
})
// 留资
app.post('/api/diagnosis/reports/:token/lead', (req,res)=>{
const {name, company, phone}=req.body
if(phone && !/^1[3-9]\d{9}$/.test(phone)) return res.status(400).json({code:400,msg:'手机号格式错误'})
db.get(`SELECT * FROM diagnosis_reports WHERE token=?`,[req.params.token], (e,rep)=>{
if(!rep) return res.status(404).json({code:404,msg:'报告不存在'})
if(new Date(rep.expires_at) < new Date()) return res.status(410).json({code:410,msg:'报告已过期,请重新诊断'})
const id=genId('lead')
db.run(`INSERT INTO diagnosis_leads (id, session_id, report_id, name, company, phone, created_at) VALUES (?,?,?,?,?,?,?)`,
[id, rep.session_id, rep.id, name||'', company||'', phone||'', nowISO()], e2=> res.json({code:0,msg:'提交成功', data:{id}}))
})
})
// PDF 生成
app.get('/api/diagnosis/reports/:token/pdf', (req,res)=>{
db.get(`SELECT * FROM diagnosis_reports WHERE token=?`,[req.params.token], (e,row)=>{
if(!row) return res.status(404).json({code:404,msg:'报告不存在'})
const scores=JSON.parse(row.scores||'{}')
const amb=JSON.parse(row.amb||'{}')
const gap=JSON.parse(row.gap||'{}')
const rc=JSON.parse(row.root_cause||'{}')
const ps=JSON.parse(row.path_suggestion||'{}')
res.setHeader('Content-Type','application/pdf')
res.setHeader('Content-Disposition',`attachment; filename="diagnosis-${row.token}.pdf"`)
const doc=new PDFDocument({margin:40})
doc.pipe(res)
doc.fontSize(18).text('企业进化诊断报告', {align:'center'})
doc.moveDown()
doc.fontSize(10).text('基于企业自填信息的初步分析,仅供参考,不替代人工咨询,不做绝对化结论与收益预测。', {align:'center'})
doc.moveDown()
doc.fontSize(12).text('一、进化坐标总览')
doc.fontSize(10).text(`四大价值得分:${JSON.stringify(scores)}`)
doc.text(`A/M/B定位:${JSON.stringify(amb)}`)
doc.moveDown()
doc.fontSize(12).text('二、差距分析')
doc.fontSize(10).text(JSON.stringify(gap))
doc.moveDown()
doc.fontSize(12).text('三、核心根因')
doc.fontSize(10).text(JSON.stringify(rc))
doc.moveDown()
doc.fontSize(12).text('四、进化路径建议')
doc.fontSize(10).text(JSON.stringify(ps))
doc.moveDown()
doc.fontSize(12).text('五、推荐服务:根据诊断结果匹配 A态入门/M态进阶/B态全案')
doc.moveDown()
doc.fontSize(10).text('免责声明:本报告基于企业自填信息生成,最终方案需顾问介入。', {align:'center'})
doc.end()
})
})
// CMS 后台:报告记录与统计
app.get('/api/cms/diagnosis/reports', (req,res)=>{
db.all(`SELECT r.*, s.token as session_token FROM diagnosis_reports r LEFT JOIN diagnosis_sessions s ON r.session_id=s.id ORDER BY r.created_at DESC LIMIT 100`, (e,rows)=> res.json({code:0,data:rows}))
})
app.get('/api/cms/diagnosis/leads', (req,res)=>{
db.all(`SELECT * FROM diagnosis_leads ORDER BY created_at DESC LIMIT 200`, (e,rows)=> res.json({code:0,data:rows}))
})
app.get('/api/cms/diagnosis/stats', (req,res)=>{
db.get(`SELECT COUNT(*) as totalSessions FROM diagnosis_sessions`, (e1,r1)=>{
db.get(`SELECT COUNT(*) as totalReports FROM diagnosis_reports`, (e2,r2)=>{
db.get(`SELECT COUNT(*) as totalLeads FROM diagnosis_leads`, (e3,r3)=>{
db.get(`SELECT COUNT(*) as completed FROM diagnosis_reports`, (e4,r4)=>{
res.json({code:0,data:{totalSessions:r1.totalSessions, totalReports:r2.totalReports, totalLeads:r3.totalLeads, completed:r4.completed}})
})
})
})
})
})
app.get('/api/cms/contact-leads', (req,res)=>{
db.all(`SELECT * FROM contact_leads ORDER BY created_at DESC LIMIT 200`, (e,rows)=> {
if (e) return res.status(500).json({code:500,msg:'读取失败'})
res.json({code:0,data:rows})
})
})
app.use((err, req, res, next) => {
if (res.headersSent) return next(err);
if (err?.message?.includes('仅支持') || err?.code === 'LIMIT_FILE_SIZE' || err?.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ code: 400, msg: err.message || '上传文件不符合要求' });
}
console.error('[CMS]', err);
res.status(500).json({ code: 500, msg: '服务器内部错误' });
});
app.listen(PORT, '0.0.0.0', ()=> console.log(`CMS server running http://0.0.0.0:${PORT} health /health admin /admin`));
+36
View File
@@ -0,0 +1,36 @@
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import 'dotenv/config';
const SECRET = process.env.JWT_SECRET || (process.env.NODE_ENV === 'production'
? (() => { throw new Error('JWT_SECRET must be configured in production'); })()
: crypto.randomBytes(32).toString('hex'));
export function signToken(payload) {
return jwt.sign(payload, SECRET, { expiresIn: '12h' });
}
export function verifyToken(token) {
return jwt.verify(token, SECRET);
}
export function authMiddleware(req, res, next) {
// 静态资源与诊断公开
if (req.path.startsWith('/admin') || req.path.startsWith('/uploads') || req.path === '/health' || req.path === '/') return next();
// 诊断相关全部公开(问卷、会话、报告、留资、分享、PDF)- 但CMS诊断后台需鉴权
if (req.path.startsWith('/api/diagnosis/')) return next();
// 所有 CMS 管理接口(包括 GET)都必须鉴权;只有下方聚合公开接口例外。
if (req.path === '/api/auth/login' || req.path === '/api/cms/public' || req.path === '/api/contact/leads') return next();
// 上传也需鉴权
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
if (req.path === '/api/cms/public' || req.path === '/api/contact/leads') return next();
return res.status(401).json({ code: 401, msg: '未登录' });
}
try {
const token = auth.slice(7);
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (e) {
return res.status(401).json({ code: 401, msg: '登录失效' });
}
}
+105
View File
@@ -0,0 +1,105 @@
import { initDB, getDB } from './db.js';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import 'dotenv/config';
await initDB();
const db = getDB();
const now = new Date().toISOString();
// 首次初始化密码由环境变量提供;未提供时生成一次性随机密码并打印。
const adminPassword = process.env.CMS_ADMIN_PASSWORD || crypto.randomBytes(12).toString('base64url');
const hash = bcrypt.hashSync(adminPassword, 10);
db.run(`INSERT OR IGNORE INTO admins (id, username, password_hash, created_at) VALUES (?, ?, ?, ?)`, ['admin-1', 'admin', hash, now]);
// 全局配置
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]);
}
});
// 首页板块
const sections = [
['home-hero', 'home-hero', '让企业进化被看见、被度量、被落地', '以企业进化为主线', '首屏主标题', 1, 100],
['home-ameba', 'home-ameba', '关于阿美巴', '克制理性的企业服务', '阿美巴摘要', 1, 90],
['home-evolution', 'home-evolution', '企业进化 · 观察坐标', '思想 → 价值 → 状态', '进化摘要', 1, 80],
['home-methods', 'home-methods', '方法与能力', '方法×能力×工具', '方法摘要', 1, 70],
['home-cases', 'home-cases', '实践案例', '9段固定插槽', '案例摘要', 1, 60],
['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]));
// 阿美巴页面
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, ?, ?, ?, ?, ?)`,
['以企业进化经营哲学为底色', '陪跑企业体系化增长落地', '利他、务实、陪跑', '十年以上实战沉淀', now]);
});
// evolution_thought
db.get(`SELECT id FROM evolution_thought WHERE id=1`, (e, r) => {
if (!r) db.run(`INSERT INTO evolution_thought (id, content, updated_at) VALUES (1, ?, ?)`, ['进化是持续迭代,而非一次性变革', now]);
});
// four_values
const fvs = [
['fv-1', 'value1', '战略价值坐标', '观察企业战略清晰度', '战略价值说明【占位】', 100],
['fv-2', 'value2', '组织价值坐标', '观察组织活力', '组织价值说明【占位】', 90],
['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]));
// 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]));
// 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]);
}
db.get(`SELECT id FROM evolution_process WHERE id=1`, (e, r) => {
if (!r) db.run(`INSERT INTO evolution_process (id, steps, scenarios, description, updated_at) VALUES (1, ?, ?, ?, ?)`, ['步骤1→步骤2→步骤3', '适用场景【占位】', '企业进化梳理说明', now]);
});
db.get(`SELECT id FROM self_check WHERE id=1`, (e, r) => {
if (!r) db.run(`INSERT INTO self_check (id, guide, questions, logic_text, next_guide, updated_at) VALUES (1, ?, ?, ?, ?, ?)`, ['轻量自我对照引导文案', JSON.stringify([{q:"战略是否清晰?",options:["A","M","B"]}]), '归位逻辑文案【占位】', '下一步引导:预约咨询', now]);
});
// 诊断配置
db.get(`SELECT id FROM diagnosis_config WHERE id=1`, (e, r) => {
if (!r) db.run(`INSERT INTO diagnosis_config (id, weights, thresholds, templates, share_expire_days, updated_at) VALUES (1, ?, ?, ?, ?, ?)`, [
JSON.stringify({value1:1, value2:1, value3:1, value4:1}),
JSON.stringify({B_max:2.5, M_max:3.8}),
JSON.stringify({
disclaimer:"本报告基于企业自填信息的初步分析,仅供参考,不替代人工咨询,不做绝对化结论与收益预测。",
intro:"基于四大价值坐标与A/M/B状态坐标的初步进化分析"
}),
30, now
]);
});
// 诊断问卷 16题(每维度4题,单选/量表)
const diagQuestions = [
// 价值创造
['dq-1','value1','价值创造','single','公司未来3年战略方向是否清晰且全员认同?', JSON.stringify([{label:'非常清晰',score:5},{label:'较清晰',score:4},{label:'一般',score:3},{label:'较模糊',score:2},{label:'非常模糊',score:1}]), 100],
['dq-2','value1','价值创造','scale','战略目标分解到季度/月度的颗粒度', JSON.stringify([{label:'1分',score:1},{label:'2分',score:2},{label:'3分',score:3},{label:'4分',score:4},{label:'5分',score:5}]), 95],
['dq-3','value1','价值创造','single','是否定期进行战略复盘与迭代?', JSON.stringify([{label:'每月',score:5},{label:'每季度',score:4},{label:'每半年',score:3},{label:'每年',score:2},{label:'很少',score:1}]), 90],
['dq-4','value1','价值创造','single','核心产品/服务的差异化优势是否明确?', JSON.stringify([{label:'非常明确',score:5},{label:'较明确',score:4},{label:'一般',score:3},{label:'较模糊',score:2},{label:'无',score:1}]), 85],
// 价值传递
['dq-5','value2','价值传递','single','组织架构是否支撑战略落地?', JSON.stringify([{label:'非常支撑',score:5},{label:'较支撑',score:4},{label:'一般',score:3},{label:'较不支撑',score:2},{label:'完全不支撑',score:1}]), 80],
['dq-6','value2','价值传递','scale','跨部门协同效率自评', JSON.stringify([{label:'1分',score:1},{label:'2分',score:2},{label:'3分',score:3},{label:'4分',score:4},{label:'5分',score:5}]), 75],
['dq-7','value2','价值传递','single','人才梯队与关键岗位备份情况', JSON.stringify([{label:'非常完善',score:5},{label:'较完善',score:4},{label:'一般',score:3},{label:'较薄弱',score:2},{label:'无',score:1}]), 70],
['dq-8','value2','价值传递','single','企业文化与价值观落地程度', JSON.stringify([{label:'高度落地',score:5},{label:'较好',score:4},{label:'一般',score:3},{label:'较弱',score:2},{label:'无',score:1}]), 65],
// 价值交付
['dq-9','value3','价值交付','single','核心业务流程是否标准化且可复制?', JSON.stringify([{label:'高度标准化',score:5},{label:'较标准化',score:4},{label:'部分',score:3},{label:'较弱',score:2},{label:'无',score:1}]), 60],
['dq-10','value3','价值交付','scale','经营数据(产销存/财务)透明度', JSON.stringify([{label:'1分',score:1},{label:'2分',score:2},{label:'3分',score:3},{label:'4分',score:4},{label:'5分',score:5}]), 55],
['dq-11','value3','价值交付','single','目标→执行→复盘→改进闭环完整度', JSON.stringify([{label:'非常完整',score:5},{label:'较完整',score:4},{label:'一般',score:3},{label:'较不完整',score:2},{label:'无闭环',score:1}]), 50],
['dq-12','value3','价值交付','single','客户交付质量与一致性', JSON.stringify([{label:'非常高',score:5},{label:'较高',score:4},{label:'一般',score:3},{label:'较低',score:2},{label:'很低',score:1}]), 45],
// 价值支持
['dq-13','value4','价值支持','single','激励机制与价值创造的关联度', JSON.stringify([{label:'高度关联',score:5},{label:'较关联',score:4},{label:'一般',score:3},{label:'较弱',score:2},{label:'无关联',score:1}]), 40],
['dq-14','value4','价值支持','scale','员工成长与学习机制完善度', JSON.stringify([{label:'1分',score:1},{label:'2分',score:2},{label:'3分',score:3},{label:'4分',score:4},{label:'5分',score:5}]), 35],
['dq-15','value4','价值支持','single','信息化/数字化工具对经营的支持', JSON.stringify([{label:'非常强',score:5},{label:'较强',score:4},{label:'一般',score:3},{label:'较弱',score:2},{label:'无',score:1}]), 30],
['dq-16','value4','价值支持','single','企业抗风险与持续改进能力', JSON.stringify([{label:'非常强',score:5},{label:'较强',score:4},{label:'一般',score:3},{label:'较弱',score:2},{label:'很弱',score:1}]), 25],
];
diagQuestions.forEach(q=> db.run(`INSERT OR IGNORE INTO diagnosis_questions (id, group_key, group_name, type, title, options, sort_order, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'published', ?, ?)`, [...q, now, now]));
console.log('seed done');
console.log(`admin username: admin\nadmin password: ${adminPassword}`);
setTimeout(() => process.exit(0), 800);