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'; import { createContentIngestRouter } from './content-ingest.js'; import { sendContactLeadToWeCom } from './wecom-notifier.js'; import { ContactLeadValidationError, normalizeContactLeadInput } from './contact-leads.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((req, res, next) => { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); next(); }); 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 = `${forwarded || req.ip || 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 }); const contactLeadRateLimit = rateLimit({ windowMs: 60_000, max: 10 }); app.use('/api/diagnosis', publicRateLimit); app.use('/api/internal/content/v1', createContentIngestRouter()); app.use('/api/contact/leads', contactLeadRateLimit); app.post('/api/contact/leads', (req, res) => { let lead; try { lead = normalizeContactLeadInput(req.body); } catch (error) { if (error instanceof ContactLeadValidationError) { return res.status(400).json({ code: 400, msg: error.message }); } return res.status(400).json({ code: 400, msg: '表单内容格式错误' }); } const id = genId('contact'); const createdAt = nowISO(); lead.createdAt = createdAt; db.run(`INSERT INTO contact_leads (id, name, company, phone, need, created_at) VALUES (?,?,?,?,?,?)`, [id, lead.name, lead.company, lead.phone, lead.need, createdAt], err => { if (err) return res.status(500).json({ code: 500, msg: '提交失败,请稍后再试' }); res.json({ code: 0, msg: '提交成功' }); sendContactLeadToWeCom(lead).catch(error => { console.error('[企业微信通知失败]', error.message); }); }); }); // 通用工具 const nowISO = () => new Date().toISOString(); const genId = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2,6)}`; const queryOne = (sql, params=[]) => new Promise((resolve, reject) => db.get(sql, params, (err, row) => err ? reject(err) : resolve(row || null))); const queryAll = (sql, params=[]) => new Promise((resolve, reject) => db.all(sql, params, (err, rows) => err ? reject(err) : resolve(rows || []))); // 全局配置 app.get('/api/cms/config', (req, res) => { 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=?, qr_image_url=?, qr_target_url=?, qr_label=?, 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, f.qr_image_url, f.qr_target_url, f.qr_label, nowISO()], function(err){ if(err) return res.status(500).json({code:500, msg: err.message}); res.json({code:0, msg:'保存成功'}); }); }); // 可配置站点文案。页面只读取公开聚合中的 texts,后台通过此处集中维护。 app.get('/api/cms/texts', (req, res) => db.all(`SELECT * FROM site_texts ORDER BY id`, (e, rows) => res.json({ code: 0, data: rows || [] }))); app.put('/api/cms/texts', (req, res) => { const items = Array.isArray(req.body) ? req.body : []; const stmt = db.prepare(`INSERT INTO site_texts (id, label, value, updated_at) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE label=VALUES(label), value=VALUES(value), updated_at=VALUES(updated_at)`); Promise.all(items.filter(it => it?.id).map(it => stmt.run(it.id, it.label || it.id, it.value || '', nowISO()))) .then(() => stmt.finalize(() => res.json({ code: 0, msg: '保存成功' }))) .catch(err => res.status(500).json({ code: 500, msg: err.message })); }); // 首页板块 app.get('/api/cms/home-sections', (req, res) => { db.all(`SELECT * FROM home_sections ORDER BY sort_order DESC`, (err, rows) => res.json({code:0, data: rows})); }); 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; db.run(`UPDATE amb_states SET name=?, definition=?, description=?, image_url=?, sort_order=?, fixed_note=?, status=?, updated_at=? WHERE id=?`, [b.name,b.definition,b.description,b.image_url,b.sort_order,b.fixed_note,b.status,nowISO(),req.params.id], e=> res.json({code:0,msg:'更新成功'})); }); // 方法与能力 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||'published',b.sort_weight||0,b.type||'custom',nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}})); }); app.put('/api/cms/services/:id', (req,res)=>{ const b=req.body; 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||'published',b.sort_weight||0,nowISO(),nowISO()], e=> res.json({code:0,data:{id,slug}})); }); app.put('/api/cms/cases/:id', (req,res)=>{ const b=req.body; 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, publish_time 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||'published',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)=>{ Promise.all([ queryOne(`SELECT * FROM global_config WHERE id=1`), queryAll(`SELECT * FROM home_sections WHERE visible=1 ORDER BY sort_order DESC`), queryOne(`SELECT * FROM ameba_page WHERE id=1`), queryAll(`SELECT * FROM experts WHERE status='published' ORDER BY sort_weight DESC`), queryOne(`SELECT * FROM evolution_thought WHERE id=1`), queryAll(`SELECT * FROM four_values WHERE status='published' ORDER BY sort_order DESC`), queryAll(`SELECT * FROM amb_states WHERE status='published' ORDER BY sort_order DESC`), queryOne(`SELECT * FROM evolution_process WHERE id=1`), queryAll(`SELECT * FROM ten_systems WHERE status='published' ORDER BY sort_order DESC`), queryAll(`SELECT * FROM services WHERE status='published' ORDER BY sort_weight DESC`), queryAll(`SELECT * FROM cases WHERE status='published' ORDER BY sort_weight DESC`), queryAll(`SELECT * FROM articles WHERE status='published' ORDER BY recommend_weight DESC, publish_time DESC, created_at DESC`), queryOne(`SELECT * FROM self_check WHERE id=1`), queryAll(`SELECT id, label, value FROM site_texts ORDER BY id`), ]).then(([config, home_sections, ameba, experts, evolution_thought, four_values, amb_states, evolution_process, ten_systems, services, cases, articles, self_check, texts]) => { const textMap = Object.fromEntries(texts.map(item => [item.id, item.value])); res.json({ code: 0, data: { config, home_sections, ameba, experts, evolution_thought, four_values, amb_states, evolution_process, ten_systems, services, cases, articles, self_check, texts: textMap } }); }).catch(err => res.status(500).json({ code: 500, msg: err.message })); }); // ================= 诊断系统 ================= // 诊断问题公开 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] 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?.type === 'entity.too.large') { if (req.path.startsWith('/api/internal/content/v1/')) { return res.status(413).json({ success: false, error: { code: 'CONTENT_TOO_LARGE', message: '请求体超过长度限制', retryable: false } }); } return res.status(413).json({ code: 413, msg: '请求体超过长度限制' }); } 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`));