chore: initialize deployable website and CMS
This commit is contained in:
@@ -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`));
|
||||
Reference in New Issue
Block a user