chore: initialize deployable website and CMS
This commit is contained in:
Generated
+2358
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
@@ -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`));
|
||||
@@ -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: '登录失效' });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user