chore: prepare project for server deployment
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
require('dotenv').config();
|
||||
const SyncMySQL = require('sync-mysql');
|
||||
|
||||
let db;
|
||||
|
||||
const requiredConfig = ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD'];
|
||||
|
||||
class MySqlStatement {
|
||||
constructor(connection, sql) {
|
||||
this.connection = connection;
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
all(...params) {
|
||||
const result = this.connection.query(this.sql, params);
|
||||
return Array.isArray(result) ? result : [];
|
||||
}
|
||||
|
||||
get(...params) {
|
||||
const rows = this.all(...params);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
run(...params) {
|
||||
const result = this.connection.query(this.sql, params) || {};
|
||||
return {
|
||||
changes: result.affectedRows || 0,
|
||||
lastInsertRowid: result.insertId || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MySqlDatabase {
|
||||
constructor(config) {
|
||||
this.connection = new SyncMySQL(config);
|
||||
}
|
||||
|
||||
prepare(sql) {
|
||||
return new MySqlStatement(this.connection, sql);
|
||||
}
|
||||
|
||||
exec(sql) {
|
||||
// 当前迁移脚本只包含独立的CREATE TABLE语句,可安全按分号拆分执行。
|
||||
sql.split(';')
|
||||
.map(statement => statement.trim())
|
||||
.filter(Boolean)
|
||||
.forEach(statement => this.connection.query(statement));
|
||||
}
|
||||
|
||||
close() {
|
||||
try {
|
||||
this.connection.dispose();
|
||||
} catch (error) {
|
||||
// sync-mysql的RPC worker已提前退出时,关闭过程不应阻断服务退出。
|
||||
console.warn('MySQL连接关闭提示:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getDb = () => {
|
||||
if (!db) {
|
||||
const missing = requiredConfig.filter(key => !process.env[key]);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`MySQL配置缺失: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
db = new MySqlDatabase({
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
charset: 'utf8mb4',
|
||||
});
|
||||
}
|
||||
return db;
|
||||
};
|
||||
|
||||
const closeDb = () => {
|
||||
if (db) {
|
||||
db.close();
|
||||
db = null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getDb, closeDb };
|
||||
@@ -0,0 +1,268 @@
|
||||
const { getDb, closeDb } = require('../config/database');
|
||||
|
||||
const createTables = () => {
|
||||
const db = getDb();
|
||||
|
||||
// Entity表 - 知识实体表(P1-01增加entity_type,P1-02增加status/public_flag)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
entity_id VARCHAR(36) PRIMARY KEY,
|
||||
entity_type VARCHAR(64) DEFAULT 'concept',
|
||||
name TEXT NOT NULL,
|
||||
definition TEXT,
|
||||
description TEXT,
|
||||
status VARCHAR(32) DEFAULT 'active',
|
||||
public_flag BOOLEAN DEFAULT 1,
|
||||
authority_level INTEGER DEFAULT 1,
|
||||
metadata TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// DirectedRelationship表 - 有向实体关系表
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS directed_relationships (
|
||||
rel_id VARCHAR(36) PRIMARY KEY,
|
||||
source_entity_id VARCHAR(36) NOT NULL,
|
||||
relation_type VARCHAR(255) NOT NULL,
|
||||
target_entity_id VARCHAR(36) NOT NULL,
|
||||
rel_status VARCHAR(32) DEFAULT 'active',
|
||||
authority_level INTEGER DEFAULT 1,
|
||||
is_public BOOLEAN DEFAULT 0,
|
||||
effective_at DATETIME,
|
||||
expire_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (source_entity_id) REFERENCES entities(entity_id),
|
||||
FOREIGN KEY (target_entity_id) REFERENCES entities(entity_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Content基础表 - 内容基础模型
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS contents (
|
||||
content_id VARCHAR(36) PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
body TEXT,
|
||||
content_type VARCHAR(32) NOT NULL,
|
||||
publish_status VARCHAR(32) DEFAULT 'draft',
|
||||
authority_level INTEGER DEFAULT 1,
|
||||
version_no INTEGER DEFAULT 1,
|
||||
version_note TEXT,
|
||||
metadata TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
archived_at DATETIME
|
||||
)
|
||||
`);
|
||||
|
||||
// Page表 - 页面扩展(P1-03增加summary)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS pages (
|
||||
page_id VARCHAR(36) PRIMARY KEY,
|
||||
slug VARCHAR(255) UNIQUE,
|
||||
template VARCHAR(255),
|
||||
summary TEXT,
|
||||
seo_title TEXT,
|
||||
seo_description TEXT,
|
||||
seo_keywords TEXT,
|
||||
FOREIGN KEY (page_id) REFERENCES contents(content_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Article表 - 文章扩展(P1-04增加published_at)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
article_id VARCHAR(36) PRIMARY KEY,
|
||||
category VARCHAR(255),
|
||||
tags TEXT,
|
||||
cover_image VARCHAR(1024),
|
||||
author VARCHAR(255),
|
||||
published_at DATETIME,
|
||||
FOREIGN KEY (article_id) REFERENCES contents(content_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Case表 - 案例扩展(P0-01扩展完整案例字段)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS cases (
|
||||
case_id VARCHAR(36) PRIMARY KEY,
|
||||
client_name TEXT,
|
||||
industry TEXT,
|
||||
challenge TEXT,
|
||||
solution TEXT,
|
||||
result TEXT,
|
||||
background TEXT,
|
||||
enterprise_stage TEXT,
|
||||
original_goal TEXT,
|
||||
current_state TEXT,
|
||||
surface_problem TEXT,
|
||||
amb_judgment TEXT,
|
||||
key_obstacle TEXT,
|
||||
root_cause TEXT,
|
||||
evolution_path TEXT,
|
||||
implementation TEXT,
|
||||
case_significance TEXT,
|
||||
FOREIGN KEY (case_id) REFERENCES contents(content_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// FAQ表 - 常见问题扩展
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS faqs (
|
||||
faq_id VARCHAR(36) PRIMARY KEY,
|
||||
question TEXT,
|
||||
answer TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (faq_id) REFERENCES contents(content_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Capability表 - 能力扩展
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS capabilities (
|
||||
capability_id VARCHAR(36) PRIMARY KEY,
|
||||
capability_type TEXT,
|
||||
features TEXT,
|
||||
use_cases TEXT,
|
||||
FOREIGN KEY (capability_id) REFERENCES contents(content_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Service表 - 服务扩展
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS services (
|
||||
service_id VARCHAR(36) PRIMARY KEY,
|
||||
service_type TEXT,
|
||||
pricing_model TEXT,
|
||||
delivery_method TEXT,
|
||||
FOREIGN KEY (service_id) REFERENCES contents(content_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// EntityContentJoin表 - 实体-内容多对多关联中间表
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS entity_content_joins (
|
||||
join_id VARCHAR(36) PRIMARY KEY,
|
||||
entity_id VARCHAR(36) NOT NULL,
|
||||
content_id VARCHAR(36) NOT NULL,
|
||||
join_note TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (entity_id) REFERENCES entities(entity_id),
|
||||
FOREIGN KEY (content_id) REFERENCES contents(content_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// 管理员账号表
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
user_id VARCHAR(36) PRIMARY KEY,
|
||||
username VARCHAR(64) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// 前台可编辑站点配置
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
setting_key VARCHAR(128) PRIMARY KEY,
|
||||
setting_value TEXT,
|
||||
setting_group VARCHAR(64) DEFAULT 'general',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
console.log('数据库表创建完成');
|
||||
};
|
||||
|
||||
const migrate = () => {
|
||||
try {
|
||||
createTables();
|
||||
seedAdminAndSettings();
|
||||
console.log('数据库迁移完成');
|
||||
} catch (error) {
|
||||
console.error('数据库迁移失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const seedAdminAndSettings = () => {
|
||||
const db = getDb();
|
||||
const crypto = require('crypto');
|
||||
const user = db.prepare('SELECT user_id FROM admin_users WHERE username = ?').get('admin');
|
||||
if (!user) {
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
const hash = crypto.scryptSync('已移除的历史密码', salt, 64).toString('hex');
|
||||
db.prepare(`
|
||||
INSERT INTO admin_users (user_id, username, password_hash)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(require('uuid').v4(), 'admin', `${salt}:${hash}`);
|
||||
console.log('默认管理员账号已创建: admin');
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
'site.name': ['阿美巴AI', 'general'],
|
||||
'site.footerCopyright': ['© 2026 阿美巴AI. All rights reserved.', 'general'],
|
||||
'site.footerEmail': ['ameiba0258@gmail.com', 'general'],
|
||||
'site.footerPhone': ['4009080258', 'general'],
|
||||
'site.footerAddress': ['四川省成都市双流区正兴街道湖畔路北段366号1栋3楼1号', 'general'],
|
||||
'site.icp': ['蜀ICP备2025143754号-7', 'general'],
|
||||
'home.heroTitle': ['阿美巴(Ameiba)', 'home'],
|
||||
'home.heroSubtitle': ['AI时代企业进化平台', 'home'],
|
||||
'home.heroDescription': ['我们帮助企业在AI时代实现系统化进化,从战略顶层设计到组织重塑,从经营增长到AI部署,提供完整的企业进化能力与服务。', 'home'],
|
||||
'contact.title': ['联系我们', 'contact'],
|
||||
'contact.contactTitle': ['联系方式', 'contact'],
|
||||
'contact.email': ['contact@ameiba.com', 'contact'],
|
||||
'contact.phone': ['待填充', 'contact'],
|
||||
'contact.address': ['待填充', 'contact'],
|
||||
'contact.serviceHoursTitle': ['服务时间', 'contact'],
|
||||
'contact.serviceHours': ['周一至周五:9:00 - 18:00', 'contact'],
|
||||
'contact.consultationTitle': ['咨询服务', 'contact'],
|
||||
'contact.consultationIntro': ['我们提供以下咨询服务:', 'contact'],
|
||||
'contact.consultationItems': ['企业进化诊断\n进化路径规划\n战略咨询\n组织变革咨询\nAI部署咨询', 'contact'],
|
||||
'contact.cooperationTitle': ['合作方式', 'contact'],
|
||||
'contact.cooperationItems': ['顾问服务\n咨询服务\n企业陪跑\n定制化服务', 'contact'],
|
||||
'contact.faqTitle': ['常见问题', 'contact'],
|
||||
'contact.faqText': ['如有其他问题,请查看', 'contact'],
|
||||
'contact.relatedTitle': ['相关知识', 'contact'],
|
||||
};
|
||||
const pageDefaults = {
|
||||
about: ['阿美巴是谁', '阿美巴(Ameiba)是AI时代企业进化平台,专注于帮助企业实现系统化进化。'],
|
||||
evolution: ['企业进化', '企业进化是企业围绕长期发展目标持续调整、重构与升级的过程。'],
|
||||
value: ['四大价值', '价值创造、价值传递、价值交付、价值支持共同构成企业经营的价值体系。'],
|
||||
amb: ['A/M-B三态模型:AI时代企业进化状态识别框架', 'A/M-B模型用于帮助企业识别自身所处的进化状态,并明确下一步行动。'],
|
||||
capabilities: ['企业进化能力地图', '从战略、经营、组织到AI部署,构建企业持续进化所需要的能力地图。'],
|
||||
cases: ['企业进化案例', '通过真实案例,展示企业在经营环境中的目标、障碍、进化路径与实施过程。'],
|
||||
knowledge: ['知识中心', '集中展示企业进化相关的知识、方法和实践内容。'],
|
||||
faq: ['常见问题', '以下是关于企业进化的常见问题解答。'],
|
||||
};
|
||||
Object.entries(pageDefaults).forEach(([page, [title, intro]]) => {
|
||||
defaults[`${page}.title`] = [title, page];
|
||||
defaults[`${page}.intro`] = [intro, page];
|
||||
});
|
||||
const extraDefaults = {
|
||||
'amb.whatTitle': ['什么是A/M-B', 'amb'], 'amb.whatNote': ['A/M-B不是产品套餐,不是收费等级,不是强制线性路径,不是所有企业必须经历的三个固定阶段。', 'amb'], 'amb.whatPurpose': ['A/M-B的作用,是帮助企业识别自身所处状态,并进一步判断当前需要完成的关键进化动作。', 'amb'], 'amb.statesTitle': ['三种状态', 'amb'], 'amb.stateA': ['A态·入态|企业开始进入AI时代企业进化的一种状态', 'amb'], 'amb.stateM': ['M态·优态|企业进一步围绕现有经营与管理体系进行优化的一种状态', 'amb'], 'amb.stateB': ['B态·越态|企业突破原有运行边界、形成新的企业运行能力的一种状态', 'amb'], 'amb.recognitionTitle': ['状态识别', 'amb'], 'amb.recognitionText': ['企业可以根据自身情况处于不同状态,也不意味着所有企业必须从A到M再到B。\n企业需要根据自身发展阶段和实际情况,判断当前所处的状态,并确定需要完成的关键进化动作。', 'amb'],
|
||||
'evolution.definitionTitle': ['什么是企业进化', 'evolution'], 'evolution.definitionMore': ['增长更多体现结果层变化。进化更强调底层能力、系统与企业状态的持续调整和升级。AI是时代变量和重要赋能工具,不是企业进化本身的终点。', 'evolution'], 'evolution.whyTitle': ['为什么需要企业进化', 'evolution'], 'evolution.whyIntro': ['在AI时代,企业面临着前所未有的挑战:', 'evolution'], 'evolution.whyItems': ['技术快速迭代,传统业务模式面临冲击\n客户需求变化,个性化、智能化要求提高\n竞争环境加剧,需要更高效的运营模式\n人才结构变化,需要新的组织能力', 'evolution'],
|
||||
'value.overviewTitle': ['价值体系概述', 'value'], 'value.valuesTitle': ['四大价值', 'value'], 'value.relationTitle': ['价值体系的关系', 'value'], 'value.relationText': ['四大价值相互关联、相互支撑。价值创造是基础,价值传递是桥梁,价值交付是结果,价值支持是保障。企业需要在这四个维度上协调发展,才能实现持续增长。', 'value'],
|
||||
'about.positionTitle': ['定位', 'about'], 'about.positionText': ['阿美巴(Ameiba)是AI时代企业进化平台,专注于帮助企业实现系统化进化,从战略顶层设计到组织重塑,从经营增长到AI部署,提供完整的企业进化能力与服务。', 'about'], 'about.existTitle': ['为什么存在', 'about'], 'about.existText': ['在AI时代,企业面临着前所未有的挑战与机遇。传统的企业管理方法已无法适应快速变化的环境,企业需要一套系统化的进化方法,实现从传统模式向AI时代模式的转型。\n阿美巴的存在,就是为了帮助企业完成这一进化过程,实现可持续增长与发展。', 'about'], 'about.audienceTitle': ['服务对象', 'about'], 'about.audienceText': ['我们服务的对象是那些希望在AI时代实现转型与增长的企业,无论企业当前处于哪个发展阶段,我们都能提供适合的进化路径与能力支持。', 'about'], 'about.historyTitle': ['发展历程', 'about'], 'about.historyText': ['待填充', 'about'],
|
||||
'capabilities.overviewTitle': ['能力概述', 'capabilities'], 'capabilities.categoryTitle': ['能力分类', 'capabilities'], 'cases.overviewTitle': ['案例概述', 'cases'], 'cases.listTitle': ['案例列表', 'cases'], 'knowledge.systemTitle': ['知识体系', 'knowledge'], 'knowledge.categoryTitle': ['知识分类', 'knowledge'], 'knowledge.articleTitle': ['最新文章', 'knowledge'], 'faq.overviewTitle': ['FAQ概述', 'faq'], 'faq.listTitle': ['问题列表', 'faq'],
|
||||
};
|
||||
Object.entries(extraDefaults).forEach(([key, [value, group]]) => {
|
||||
defaults[key] = [value, group];
|
||||
});
|
||||
Object.entries(defaults).forEach(([key, [value, group]]) => {
|
||||
const existing = db.prepare('SELECT setting_key FROM site_settings WHERE setting_key = ?').get(key);
|
||||
if (!existing) {
|
||||
db.prepare('INSERT INTO site_settings (setting_key, setting_value, setting_group) VALUES (?, ?, ?)').run(key, value, group);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
migrate();
|
||||
}
|
||||
|
||||
module.exports = { migrate, createTables, seedAdminAndSettings };
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
const { getDb, closeDb } = require('./config/database');
|
||||
const { migrate } = require('./database/migrate');
|
||||
|
||||
// 路由导入
|
||||
const entityRoutes = require('./routes/entityRoutes');
|
||||
const relationshipRoutes = require('./routes/relationshipRoutes');
|
||||
const contentRoutes = require('./routes/contentRoutes');
|
||||
const entityContentJoinRoutes = require('./routes/entityContentJoinRoutes');
|
||||
const publicApiRoutes = require('./routes/publicApiRoutes');
|
||||
const jsonLdRoutes = require('./routes/jsonLdRoutes');
|
||||
const adminRoutes = require('./routes/adminRoutes');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 24731;
|
||||
|
||||
// 中间件
|
||||
app.use(cors({ origin: true, credentials: true }));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// 静态文件服务
|
||||
app.use('/static', express.static(path.join(__dirname, '../public')));
|
||||
|
||||
// 路由
|
||||
app.use('/api/entities', entityRoutes);
|
||||
app.use('/api/relationships', relationshipRoutes);
|
||||
app.use('/api/contents', contentRoutes);
|
||||
app.use('/api/joins', entityContentJoinRoutes);
|
||||
app.use('/api/public', publicApiRoutes);
|
||||
app.use('/api/jsonld', jsonLdRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// 首页
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
name: '阿美巴AI官网 MVP-BASE',
|
||||
version: '1.0.0',
|
||||
description: '第一期底座开发 - 知识模型、关系模型、内容模型、CMS治理内核',
|
||||
endpoints: {
|
||||
entities: '/api/entities',
|
||||
relationships: '/api/relationships',
|
||||
contents: '/api/contents',
|
||||
joins: '/api/joins',
|
||||
publicApi: '/api/public',
|
||||
jsonLd: '/api/jsonld'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 错误处理中间件
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('服务器错误:', err);
|
||||
res.status(500).json({ error: '服务器内部错误' });
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// 初始化数据库
|
||||
console.log('正在初始化数据库...');
|
||||
migrate();
|
||||
|
||||
// 启动服务器
|
||||
app.listen(PORT, () => {
|
||||
console.log(`服务器运行在 http://localhost:${PORT}`);
|
||||
console.log('API文档:');
|
||||
console.log(` 实体管理: http://localhost:${PORT}/api/entities`);
|
||||
console.log(` 关系管理: http://localhost:${PORT}/api/relationships`);
|
||||
console.log(` 内容管理: http://localhost:${PORT}/api/contents`);
|
||||
console.log(` 关联管理: http://localhost:${PORT}/api/joins`);
|
||||
console.log(` 公开API: http://localhost:${PORT}/api/public`);
|
||||
console.log(` JSON-LD: http://localhost:${PORT}/api/jsonld`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('启动服务器失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGINT', () => {
|
||||
console.log('正在关闭服务器...');
|
||||
closeDb();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('正在关闭服务器...');
|
||||
closeDb();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
startServer();
|
||||
|
||||
module.exports = app;
|
||||
@@ -0,0 +1,153 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const Content = require('./Content');
|
||||
|
||||
class Article extends Content {
|
||||
constructor(data = {}) {
|
||||
super(data);
|
||||
this.content_type = 'article';
|
||||
this.article_id = data.article_id || this.content_id;
|
||||
this.category = data.category || null;
|
||||
this.tags = data.tags || null;
|
||||
this.cover_image = data.cover_image || null;
|
||||
this.author = data.author || null;
|
||||
this.published_at = data.published_at || null;
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
|
||||
// 先创建Content记录
|
||||
const contentData = {
|
||||
...data,
|
||||
content_type: 'article'
|
||||
};
|
||||
|
||||
const content = super.create(contentData);
|
||||
|
||||
// 再创建Article扩展记录
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO articles (article_id, category, tags, cover_image, author, published_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
content.content_id,
|
||||
data.category || null,
|
||||
data.tags || null,
|
||||
data.cover_image || null,
|
||||
data.author || null,
|
||||
data.published_at || null
|
||||
);
|
||||
|
||||
return this.findById(content.content_id);
|
||||
}
|
||||
|
||||
static findById(contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, a.article_id, a.category, a.tags, a.cover_image, a.author, a.published_at
|
||||
FROM contents c
|
||||
LEFT JOIN articles a ON c.content_id = a.article_id
|
||||
WHERE c.content_id = ?
|
||||
`);
|
||||
const row = stmt.get(contentId);
|
||||
|
||||
if (row && row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static update(contentId, data) {
|
||||
const db = getDb();
|
||||
|
||||
// 更新Content部分
|
||||
const contentData = { ...data };
|
||||
delete contentData.article_id;
|
||||
delete contentData.category;
|
||||
delete contentData.tags;
|
||||
delete contentData.cover_image;
|
||||
delete contentData.author;
|
||||
delete contentData.published_at;
|
||||
|
||||
super.update(contentId, contentData);
|
||||
|
||||
// 更新Article扩展部分
|
||||
const articleData = {};
|
||||
if (data.category !== undefined) articleData.category = data.category;
|
||||
if (data.tags !== undefined) articleData.tags = data.tags;
|
||||
if (data.cover_image !== undefined) articleData.cover_image = data.cover_image;
|
||||
if (data.author !== undefined) articleData.author = data.author;
|
||||
if (data.published_at !== undefined) articleData.published_at = data.published_at;
|
||||
|
||||
if (Object.keys(articleData).length > 0) {
|
||||
const setClause = Object.keys(articleData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(articleData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE articles SET ${setClause} WHERE article_id = ?`);
|
||||
stmt.run(...values, contentId);
|
||||
}
|
||||
|
||||
return this.findById(contentId);
|
||||
}
|
||||
|
||||
static delete(contentId) {
|
||||
const db = getDb();
|
||||
|
||||
// 删除Article扩展记录
|
||||
const stmt1 = db.prepare('DELETE FROM articles WHERE article_id = ?');
|
||||
stmt1.run(contentId);
|
||||
|
||||
// 删除Content记录
|
||||
return super.delete(contentId);
|
||||
}
|
||||
|
||||
static findAll(options = {}) {
|
||||
const db = getDb();
|
||||
let query = `
|
||||
SELECT c.*, a.article_id, a.category, a.tags, a.cover_image, a.author, a.published_at
|
||||
FROM contents c
|
||||
LEFT JOIN articles a ON c.content_id = a.article_id
|
||||
WHERE c.content_type = 'article'
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => {
|
||||
if (key === 'category' || key === 'tags' || key === 'cover_image' || key === 'author' || key === 'published_at') {
|
||||
return `a.${key} = ?`;
|
||||
}
|
||||
return `c.${key} = ?`;
|
||||
});
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Article;
|
||||
@@ -0,0 +1,145 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const Content = require('./Content');
|
||||
|
||||
class Capability extends Content {
|
||||
constructor(data = {}) {
|
||||
super(data);
|
||||
this.content_type = 'capability';
|
||||
this.capability_id = data.capability_id || this.content_id;
|
||||
this.capability_type = data.capability_type || null;
|
||||
this.features = data.features || null;
|
||||
this.use_cases = data.use_cases || null;
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
|
||||
// 先创建Content记录
|
||||
const contentData = {
|
||||
...data,
|
||||
content_type: 'capability'
|
||||
};
|
||||
|
||||
const content = super.create(contentData);
|
||||
|
||||
// 再创建Capability扩展记录
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO capabilities (capability_id, capability_type, features, use_cases)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
content.content_id,
|
||||
data.capability_type || null,
|
||||
data.features || null,
|
||||
data.use_cases || null
|
||||
);
|
||||
|
||||
return this.findById(content.content_id);
|
||||
}
|
||||
|
||||
static findById(contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, cap.capability_id, cap.capability_type, cap.features, cap.use_cases
|
||||
FROM contents c
|
||||
LEFT JOIN capabilities cap ON c.content_id = cap.capability_id
|
||||
WHERE c.content_id = ?
|
||||
`);
|
||||
const row = stmt.get(contentId);
|
||||
|
||||
if (row && row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static update(contentId, data) {
|
||||
const db = getDb();
|
||||
|
||||
// 更新Content部分
|
||||
const contentData = { ...data };
|
||||
delete contentData.capability_id;
|
||||
delete contentData.capability_type;
|
||||
delete contentData.features;
|
||||
delete contentData.use_cases;
|
||||
|
||||
super.update(contentId, contentData);
|
||||
|
||||
// 更新Capability扩展部分
|
||||
const capabilityData = {};
|
||||
if (data.capability_type !== undefined) capabilityData.capability_type = data.capability_type;
|
||||
if (data.features !== undefined) capabilityData.features = data.features;
|
||||
if (data.use_cases !== undefined) capabilityData.use_cases = data.use_cases;
|
||||
|
||||
if (Object.keys(capabilityData).length > 0) {
|
||||
const setClause = Object.keys(capabilityData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(capabilityData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE capabilities SET ${setClause} WHERE capability_id = ?`);
|
||||
stmt.run(...values, contentId);
|
||||
}
|
||||
|
||||
return this.findById(contentId);
|
||||
}
|
||||
|
||||
static delete(contentId) {
|
||||
const db = getDb();
|
||||
|
||||
// 删除Capability扩展记录
|
||||
const stmt1 = db.prepare('DELETE FROM capabilities WHERE capability_id = ?');
|
||||
stmt1.run(contentId);
|
||||
|
||||
// 删除Content记录
|
||||
return super.delete(contentId);
|
||||
}
|
||||
|
||||
static findAll(options = {}) {
|
||||
const db = getDb();
|
||||
let query = `
|
||||
SELECT c.*, cap.capability_id, cap.capability_type, cap.features, cap.use_cases
|
||||
FROM contents c
|
||||
LEFT JOIN capabilities cap ON c.content_id = cap.capability_id
|
||||
WHERE c.content_type = 'capability'
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => {
|
||||
if (key === 'capability_type' || key === 'features' || key === 'use_cases') {
|
||||
return `cap.${key} = ?`;
|
||||
}
|
||||
return `c.${key} = ?`;
|
||||
});
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Capability;
|
||||
@@ -0,0 +1,207 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const Content = require('./Content');
|
||||
|
||||
class Case extends Content {
|
||||
constructor(data = {}) {
|
||||
super(data);
|
||||
this.content_type = 'case';
|
||||
this.case_id = data.case_id || this.content_id;
|
||||
this.client_name = data.client_name || null;
|
||||
this.industry = data.industry || null;
|
||||
this.challenge = data.challenge || null;
|
||||
this.solution = data.solution || null;
|
||||
this.result = data.result || null;
|
||||
// P0-01扩展字段
|
||||
this.background = data.background || null;
|
||||
this.enterprise_stage = data.enterprise_stage || null;
|
||||
this.original_goal = data.original_goal || null;
|
||||
this.current_state = data.current_state || null;
|
||||
this.surface_problem = data.surface_problem || null;
|
||||
this.amb_judgment = data.amb_judgment || null;
|
||||
this.key_obstacle = data.key_obstacle || null;
|
||||
this.root_cause = data.root_cause || null;
|
||||
this.evolution_path = data.evolution_path || null;
|
||||
this.implementation = data.implementation || null;
|
||||
this.case_significance = data.case_significance || null;
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
|
||||
// 先创建Content记录
|
||||
const contentData = {
|
||||
...data,
|
||||
content_type: 'case'
|
||||
};
|
||||
|
||||
const content = super.create(contentData);
|
||||
|
||||
// 再创建Case扩展记录(P0-01扩展完整案例字段)
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO cases (case_id, client_name, industry, challenge, solution, result,
|
||||
background, enterprise_stage, original_goal, current_state, surface_problem,
|
||||
amb_judgment, key_obstacle, root_cause, evolution_path, implementation, case_significance)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
content.content_id,
|
||||
data.client_name || null,
|
||||
data.industry || null,
|
||||
data.challenge || null,
|
||||
data.solution || null,
|
||||
data.result || null,
|
||||
data.background || null,
|
||||
data.enterprise_stage || null,
|
||||
data.original_goal || null,
|
||||
data.current_state || null,
|
||||
data.surface_problem || null,
|
||||
data.amb_judgment || null,
|
||||
data.key_obstacle || null,
|
||||
data.root_cause || null,
|
||||
data.evolution_path || null,
|
||||
data.implementation || null,
|
||||
data.case_significance || null
|
||||
);
|
||||
|
||||
return this.findById(content.content_id);
|
||||
}
|
||||
|
||||
static findById(contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, cs.case_id, cs.client_name, cs.industry, cs.challenge, cs.solution, cs.result,
|
||||
cs.background, cs.enterprise_stage, cs.original_goal, cs.current_state, cs.surface_problem,
|
||||
cs.amb_judgment, cs.key_obstacle, cs.root_cause, cs.evolution_path, cs.implementation, cs.case_significance
|
||||
FROM contents c
|
||||
LEFT JOIN cases cs ON c.content_id = cs.case_id
|
||||
WHERE c.content_id = ?
|
||||
`);
|
||||
const row = stmt.get(contentId);
|
||||
|
||||
if (row && row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static update(contentId, data) {
|
||||
const db = getDb();
|
||||
|
||||
// 更新Content部分
|
||||
const contentData = { ...data };
|
||||
delete contentData.case_id;
|
||||
delete contentData.client_name;
|
||||
delete contentData.industry;
|
||||
delete contentData.challenge;
|
||||
delete contentData.solution;
|
||||
delete contentData.result;
|
||||
delete contentData.background;
|
||||
delete contentData.enterprise_stage;
|
||||
delete contentData.original_goal;
|
||||
delete contentData.current_state;
|
||||
delete contentData.surface_problem;
|
||||
delete contentData.amb_judgment;
|
||||
delete contentData.key_obstacle;
|
||||
delete contentData.root_cause;
|
||||
delete contentData.evolution_path;
|
||||
delete contentData.implementation;
|
||||
delete contentData.case_significance;
|
||||
|
||||
super.update(contentId, contentData);
|
||||
|
||||
// 更新Case扩展部分(P0-01扩展完整案例字段)
|
||||
const caseData = {};
|
||||
if (data.client_name !== undefined) caseData.client_name = data.client_name;
|
||||
if (data.industry !== undefined) caseData.industry = data.industry;
|
||||
if (data.challenge !== undefined) caseData.challenge = data.challenge;
|
||||
if (data.solution !== undefined) caseData.solution = data.solution;
|
||||
if (data.result !== undefined) caseData.result = data.result;
|
||||
if (data.background !== undefined) caseData.background = data.background;
|
||||
if (data.enterprise_stage !== undefined) caseData.enterprise_stage = data.enterprise_stage;
|
||||
if (data.original_goal !== undefined) caseData.original_goal = data.original_goal;
|
||||
if (data.current_state !== undefined) caseData.current_state = data.current_state;
|
||||
if (data.surface_problem !== undefined) caseData.surface_problem = data.surface_problem;
|
||||
if (data.amb_judgment !== undefined) caseData.amb_judgment = data.amb_judgment;
|
||||
if (data.key_obstacle !== undefined) caseData.key_obstacle = data.key_obstacle;
|
||||
if (data.root_cause !== undefined) caseData.root_cause = data.root_cause;
|
||||
if (data.evolution_path !== undefined) caseData.evolution_path = data.evolution_path;
|
||||
if (data.implementation !== undefined) caseData.implementation = data.implementation;
|
||||
if (data.case_significance !== undefined) caseData.case_significance = data.case_significance;
|
||||
|
||||
if (Object.keys(caseData).length > 0) {
|
||||
const setClause = Object.keys(caseData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(caseData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE cases SET ${setClause} WHERE case_id = ?`);
|
||||
stmt.run(...values, contentId);
|
||||
}
|
||||
|
||||
return this.findById(contentId);
|
||||
}
|
||||
|
||||
static delete(contentId) {
|
||||
const db = getDb();
|
||||
|
||||
// 删除Case扩展记录
|
||||
const stmt1 = db.prepare('DELETE FROM cases WHERE case_id = ?');
|
||||
stmt1.run(contentId);
|
||||
|
||||
// 删除Content记录
|
||||
return super.delete(contentId);
|
||||
}
|
||||
|
||||
static findAll(options = {}) {
|
||||
const db = getDb();
|
||||
let query = `
|
||||
SELECT c.*, cs.case_id, cs.client_name, cs.industry, cs.challenge, cs.solution, cs.result,
|
||||
cs.background, cs.enterprise_stage, cs.original_goal, cs.current_state, cs.surface_problem,
|
||||
cs.amb_judgment, cs.key_obstacle, cs.root_cause, cs.evolution_path, cs.implementation, cs.case_significance
|
||||
FROM contents c
|
||||
LEFT JOIN cases cs ON c.content_id = cs.case_id
|
||||
WHERE c.content_type = 'case'
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => {
|
||||
if (key === 'client_name' || key === 'industry' || key === 'challenge' || key === 'solution' || key === 'result' ||
|
||||
key === 'background' || key === 'enterprise_stage' || key === 'original_goal' || key === 'current_state' ||
|
||||
key === 'surface_problem' || key === 'amb_judgment' || key === 'key_obstacle' || key === 'root_cause' ||
|
||||
key === 'evolution_path' || key === 'implementation' || key === 'case_significance') {
|
||||
return `cs.${key} = ?`;
|
||||
}
|
||||
return `c.${key} = ?`;
|
||||
});
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Case;
|
||||
@@ -0,0 +1,229 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class Content {
|
||||
constructor(data = {}) {
|
||||
this.content_id = data.content_id || uuidv4();
|
||||
this.title = data.title;
|
||||
this.body = data.body || null;
|
||||
this.content_type = data.content_type;
|
||||
this.publish_status = data.publish_status || 'draft';
|
||||
this.authority_level = data.authority_level || 1;
|
||||
this.version_no = data.version_no || 1;
|
||||
this.version_note = data.version_note || null;
|
||||
this.metadata = data.metadata ? JSON.stringify(data.metadata) : null;
|
||||
this.created_at = data.created_at || new Date().toISOString();
|
||||
this.updated_at = data.updated_at || new Date().toISOString();
|
||||
this.archived_at = data.archived_at || null;
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
const content = new Content(data);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO contents (
|
||||
content_id, title, body, content_type, publish_status,
|
||||
authority_level, version_no, version_note, metadata,
|
||||
created_at, updated_at, archived_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
content.content_id,
|
||||
content.title,
|
||||
content.body,
|
||||
content.content_type,
|
||||
content.publish_status,
|
||||
content.authority_level,
|
||||
content.version_no,
|
||||
content.version_note,
|
||||
content.metadata,
|
||||
content.created_at,
|
||||
content.updated_at,
|
||||
content.archived_at
|
||||
);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
static findById(contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT * FROM contents WHERE content_id = ?');
|
||||
const row = stmt.get(contentId);
|
||||
|
||||
if (row && row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static findByType(contentType, options = {}) {
|
||||
const db = getDb();
|
||||
let query = 'SELECT * FROM contents WHERE content_type = ?';
|
||||
const params = [contentType];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => `${key} = ?`);
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
static findPublished(options = {}) {
|
||||
const db = getDb();
|
||||
let query = 'SELECT * FROM contents WHERE publish_status = ?';
|
||||
const params = ['published'];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => `${key} = ?`);
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
static update(contentId, data) {
|
||||
const db = getDb();
|
||||
|
||||
const updateData = { ...data };
|
||||
delete updateData.content_id;
|
||||
delete updateData.created_at;
|
||||
|
||||
updateData.updated_at = new Date().toISOString();
|
||||
|
||||
if (updateData.metadata) {
|
||||
updateData.metadata = JSON.stringify(updateData.metadata);
|
||||
}
|
||||
|
||||
const setClause = Object.keys(updateData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(updateData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE contents SET ${setClause} WHERE content_id = ?`);
|
||||
stmt.run(...values, contentId);
|
||||
|
||||
return this.findById(contentId);
|
||||
}
|
||||
|
||||
static delete(contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('DELETE FROM contents WHERE content_id = ?');
|
||||
return stmt.run(contentId);
|
||||
}
|
||||
|
||||
static count() {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT COUNT(*) as count FROM contents');
|
||||
const row = stmt.get();
|
||||
return row.count;
|
||||
}
|
||||
|
||||
// 内容状态流转
|
||||
static transitionStatus(contentId, newStatus) {
|
||||
const validTransitions = {
|
||||
'draft': ['review'],
|
||||
'review': ['draft', 'published'],
|
||||
'published': ['unpublished'],
|
||||
'unpublished': ['historical', 'published'],
|
||||
'historical': []
|
||||
};
|
||||
|
||||
const content = this.findById(contentId);
|
||||
if (!content) {
|
||||
throw new Error('内容不存在');
|
||||
}
|
||||
|
||||
const allowedTransitions = validTransitions[content.publish_status];
|
||||
if (!allowedTransitions.includes(newStatus)) {
|
||||
throw new Error(`不允许从 ${content.publish_status} 转换到 ${newStatus}`);
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
publish_status: newStatus,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 如果转换到historical,记录归档时间
|
||||
if (newStatus === 'historical') {
|
||||
updateData.archived_at = new Date().toISOString();
|
||||
}
|
||||
|
||||
// 如果转换到published,增加版本号
|
||||
if (newStatus === 'published') {
|
||||
updateData.version_no = content.version_no + 1;
|
||||
}
|
||||
|
||||
return this.update(contentId, updateData);
|
||||
}
|
||||
|
||||
// 创建历史版本
|
||||
static createHistoricalVersion(contentId) {
|
||||
const content = this.findById(contentId);
|
||||
if (!content) {
|
||||
throw new Error('内容不存在');
|
||||
}
|
||||
|
||||
// 创建新版本
|
||||
const newData = {
|
||||
...content,
|
||||
content_id: uuidv4(),
|
||||
version_no: content.version_no + 1,
|
||||
publish_status: 'historical',
|
||||
archived_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
return this.create(newData);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Content;
|
||||
@@ -0,0 +1,145 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class DirectedRelationship {
|
||||
constructor(data = {}) {
|
||||
this.rel_id = data.rel_id || uuidv4();
|
||||
this.source_entity_id = data.source_entity_id;
|
||||
this.relation_type = data.relation_type;
|
||||
this.target_entity_id = data.target_entity_id;
|
||||
this.rel_status = data.rel_status || 'active';
|
||||
this.authority_level = data.authority_level || 1;
|
||||
this.is_public = data.is_public || 0;
|
||||
this.effective_at = data.effective_at || null;
|
||||
this.expire_at = data.expire_at || null;
|
||||
this.created_at = data.created_at || new Date().toISOString();
|
||||
this.updated_at = data.updated_at || new Date().toISOString();
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
const relationship = new DirectedRelationship(data);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO directed_relationships (
|
||||
rel_id, source_entity_id, relation_type, target_entity_id,
|
||||
rel_status, authority_level, is_public, effective_at, expire_at,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
relationship.rel_id,
|
||||
relationship.source_entity_id,
|
||||
relationship.relation_type,
|
||||
relationship.target_entity_id,
|
||||
relationship.rel_status,
|
||||
relationship.authority_level,
|
||||
relationship.is_public,
|
||||
relationship.effective_at,
|
||||
relationship.expire_at,
|
||||
relationship.created_at,
|
||||
relationship.updated_at
|
||||
);
|
||||
|
||||
return relationship;
|
||||
}
|
||||
|
||||
static findById(relId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT * FROM directed_relationships WHERE rel_id = ?');
|
||||
return stmt.get(relId);
|
||||
}
|
||||
|
||||
static findBySourceEntity(sourceEntityId, options = {}) {
|
||||
const db = getDb();
|
||||
let query = 'SELECT * FROM directed_relationships WHERE source_entity_id = ?';
|
||||
const params = [sourceEntityId];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => `${key} = ?`);
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
return stmt.all(...params);
|
||||
}
|
||||
|
||||
static findByTargetEntity(targetEntityId, options = {}) {
|
||||
const db = getDb();
|
||||
let query = 'SELECT * FROM directed_relationships WHERE target_entity_id = ?';
|
||||
const params = [targetEntityId];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => `${key} = ?`);
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
return stmt.all(...params);
|
||||
}
|
||||
|
||||
static findAll(options = {}) {
|
||||
const db = getDb();
|
||||
let query = 'SELECT * FROM directed_relationships';
|
||||
const params = [];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => `${key} = ?`);
|
||||
query += ` WHERE ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
return stmt.all(...params);
|
||||
}
|
||||
|
||||
static update(relId, data) {
|
||||
const db = getDb();
|
||||
|
||||
const updateData = { ...data };
|
||||
delete updateData.rel_id;
|
||||
delete updateData.created_at;
|
||||
|
||||
updateData.updated_at = new Date().toISOString();
|
||||
|
||||
const setClause = Object.keys(updateData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(updateData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE directed_relationships SET ${setClause} WHERE rel_id = ?`);
|
||||
stmt.run(...values, relId);
|
||||
|
||||
return this.findById(relId);
|
||||
}
|
||||
|
||||
static delete(relId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('DELETE FROM directed_relationships WHERE rel_id = ?');
|
||||
return stmt.run(relId);
|
||||
}
|
||||
|
||||
static count() {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT COUNT(*) as count FROM directed_relationships');
|
||||
const row = stmt.get();
|
||||
return row.count;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DirectedRelationship;
|
||||
@@ -0,0 +1,134 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class Entity {
|
||||
constructor(data = {}) {
|
||||
this.entity_id = data.entity_id || uuidv4();
|
||||
this.entity_type = data.entity_type || 'concept';
|
||||
this.name = data.name;
|
||||
this.definition = data.definition || null;
|
||||
this.description = data.description || null;
|
||||
this.status = data.status || 'active';
|
||||
this.public_flag = data.public_flag !== undefined ? data.public_flag : 1;
|
||||
this.authority_level = data.authority_level || 1;
|
||||
this.metadata = data.metadata ? JSON.stringify(data.metadata) : null;
|
||||
this.created_at = data.created_at || new Date().toISOString();
|
||||
this.updated_at = data.updated_at || new Date().toISOString();
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
const entity = new Entity(data);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO entities (entity_id, entity_type, name, definition, description, status, public_flag, authority_level, metadata, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
entity.entity_id,
|
||||
entity.entity_type,
|
||||
entity.name,
|
||||
entity.definition,
|
||||
entity.description,
|
||||
entity.status,
|
||||
entity.public_flag,
|
||||
entity.authority_level,
|
||||
entity.metadata,
|
||||
entity.created_at,
|
||||
entity.updated_at
|
||||
);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
static findById(entityId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT * FROM entities WHERE entity_id = ?');
|
||||
const row = stmt.get(entityId);
|
||||
|
||||
if (row && row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static findAll(options = {}) {
|
||||
const db = getDb();
|
||||
let query = 'SELECT * FROM entities';
|
||||
const params = [];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => `${key} = ?`);
|
||||
query += ` WHERE ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
static update(entityId, data) {
|
||||
const db = getDb();
|
||||
|
||||
// 禁止修改entity_id
|
||||
if (data.entity_id && data.entity_id !== entityId) {
|
||||
throw new Error('不能修改entity_id');
|
||||
}
|
||||
|
||||
const updateData = { ...data };
|
||||
delete updateData.entity_id;
|
||||
delete updateData.created_at;
|
||||
|
||||
updateData.updated_at = new Date().toISOString();
|
||||
|
||||
if (updateData.metadata) {
|
||||
updateData.metadata = JSON.stringify(updateData.metadata);
|
||||
}
|
||||
|
||||
const setClause = Object.keys(updateData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(updateData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE entities SET ${setClause} WHERE entity_id = ?`);
|
||||
stmt.run(...values, entityId);
|
||||
|
||||
return this.findById(entityId);
|
||||
}
|
||||
|
||||
static delete(entityId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('DELETE FROM entities WHERE entity_id = ?');
|
||||
return stmt.run(entityId);
|
||||
}
|
||||
|
||||
static count() {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT COUNT(*) as count FROM entities');
|
||||
const row = stmt.get();
|
||||
return row.count;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Entity;
|
||||
@@ -0,0 +1,116 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class EntityContentJoin {
|
||||
constructor(data = {}) {
|
||||
this.join_id = data.join_id || uuidv4();
|
||||
this.entity_id = data.entity_id;
|
||||
this.content_id = data.content_id;
|
||||
this.join_note = data.join_note || null;
|
||||
this.created_at = data.created_at || new Date().toISOString();
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
const join = new EntityContentJoin(data);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO entity_content_joins (join_id, entity_id, content_id, join_note, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
join.join_id,
|
||||
join.entity_id,
|
||||
join.content_id,
|
||||
join.join_note,
|
||||
join.created_at
|
||||
);
|
||||
|
||||
return join;
|
||||
}
|
||||
|
||||
static findById(joinId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT * FROM entity_content_joins WHERE join_id = ?');
|
||||
return stmt.get(joinId);
|
||||
}
|
||||
|
||||
static findByEntityId(entityId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT * FROM entity_content_joins WHERE entity_id = ?');
|
||||
return stmt.all(entityId);
|
||||
}
|
||||
|
||||
static findByContentId(contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT * FROM entity_content_joins WHERE content_id = ?');
|
||||
return stmt.all(contentId);
|
||||
}
|
||||
|
||||
static findAll(options = {}) {
|
||||
const db = getDb();
|
||||
let query = 'SELECT * FROM entity_content_joins';
|
||||
const params = [];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => `${key} = ?`);
|
||||
query += ` WHERE ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
return stmt.all(...params);
|
||||
}
|
||||
|
||||
static update(joinId, data) {
|
||||
const db = getDb();
|
||||
|
||||
const updateData = { ...data };
|
||||
delete updateData.join_id;
|
||||
delete updateData.created_at;
|
||||
|
||||
const setClause = Object.keys(updateData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(updateData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE entity_content_joins SET ${setClause} WHERE join_id = ?`);
|
||||
stmt.run(...values, joinId);
|
||||
|
||||
return this.findById(joinId);
|
||||
}
|
||||
|
||||
static delete(joinId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('DELETE FROM entity_content_joins WHERE join_id = ?');
|
||||
return stmt.run(joinId);
|
||||
}
|
||||
|
||||
static deleteByEntityAndContent(entityId, contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('DELETE FROM entity_content_joins WHERE entity_id = ? AND content_id = ?');
|
||||
return stmt.run(entityId, contentId);
|
||||
}
|
||||
|
||||
static count() {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT COUNT(*) as count FROM entity_content_joins');
|
||||
const row = stmt.get();
|
||||
return row.count;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EntityContentJoin;
|
||||
@@ -0,0 +1,145 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const Content = require('./Content');
|
||||
|
||||
class FAQ extends Content {
|
||||
constructor(data = {}) {
|
||||
super(data);
|
||||
this.content_type = 'faq';
|
||||
this.faq_id = data.faq_id || this.content_id;
|
||||
this.question = data.question || null;
|
||||
this.answer = data.answer || null;
|
||||
this.sort_order = data.sort_order || 0;
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
|
||||
// 先创建Content记录
|
||||
const contentData = {
|
||||
...data,
|
||||
content_type: 'faq'
|
||||
};
|
||||
|
||||
const content = super.create(contentData);
|
||||
|
||||
// 再创建FAQ扩展记录
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO faqs (faq_id, question, answer, sort_order)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
content.content_id,
|
||||
data.question || null,
|
||||
data.answer || null,
|
||||
data.sort_order || 0
|
||||
);
|
||||
|
||||
return this.findById(content.content_id);
|
||||
}
|
||||
|
||||
static findById(contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, f.faq_id, f.question, f.answer, f.sort_order
|
||||
FROM contents c
|
||||
LEFT JOIN faqs f ON c.content_id = f.faq_id
|
||||
WHERE c.content_id = ?
|
||||
`);
|
||||
const row = stmt.get(contentId);
|
||||
|
||||
if (row && row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static update(contentId, data) {
|
||||
const db = getDb();
|
||||
|
||||
// 更新Content部分
|
||||
const contentData = { ...data };
|
||||
delete contentData.faq_id;
|
||||
delete contentData.question;
|
||||
delete contentData.answer;
|
||||
delete contentData.sort_order;
|
||||
|
||||
super.update(contentId, contentData);
|
||||
|
||||
// 更新FAQ扩展部分
|
||||
const faqData = {};
|
||||
if (data.question !== undefined) faqData.question = data.question;
|
||||
if (data.answer !== undefined) faqData.answer = data.answer;
|
||||
if (data.sort_order !== undefined) faqData.sort_order = data.sort_order;
|
||||
|
||||
if (Object.keys(faqData).length > 0) {
|
||||
const setClause = Object.keys(faqData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(faqData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE faqs SET ${setClause} WHERE faq_id = ?`);
|
||||
stmt.run(...values, contentId);
|
||||
}
|
||||
|
||||
return this.findById(contentId);
|
||||
}
|
||||
|
||||
static delete(contentId) {
|
||||
const db = getDb();
|
||||
|
||||
// 删除FAQ扩展记录
|
||||
const stmt1 = db.prepare('DELETE FROM faqs WHERE faq_id = ?');
|
||||
stmt1.run(contentId);
|
||||
|
||||
// 删除Content记录
|
||||
return super.delete(contentId);
|
||||
}
|
||||
|
||||
static findAll(options = {}) {
|
||||
const db = getDb();
|
||||
let query = `
|
||||
SELECT c.*, f.faq_id, f.question, f.answer, f.sort_order
|
||||
FROM contents c
|
||||
LEFT JOIN faqs f ON c.content_id = f.faq_id
|
||||
WHERE c.content_type = 'faq'
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => {
|
||||
if (key === 'question' || key === 'answer' || key === 'sort_order') {
|
||||
return `f.${key} = ?`;
|
||||
}
|
||||
return `c.${key} = ?`;
|
||||
});
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FAQ;
|
||||
@@ -0,0 +1,174 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const Content = require('./Content');
|
||||
|
||||
class Page extends Content {
|
||||
constructor(data = {}) {
|
||||
super(data);
|
||||
this.content_type = 'page';
|
||||
this.page_id = data.page_id || this.content_id;
|
||||
this.slug = data.slug || null;
|
||||
this.template = data.template || null;
|
||||
this.summary = data.summary || null;
|
||||
this.seo_title = data.seo_title || null;
|
||||
this.seo_description = data.seo_description || null;
|
||||
this.seo_keywords = data.seo_keywords || null;
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
|
||||
// 先创建Content记录
|
||||
const contentData = {
|
||||
...data,
|
||||
content_type: 'page'
|
||||
};
|
||||
|
||||
const content = super.create(contentData);
|
||||
|
||||
// 再创建Page扩展记录
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO pages (page_id, slug, template, summary, seo_title, seo_description, seo_keywords)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
content.content_id,
|
||||
data.slug || null,
|
||||
data.template || null,
|
||||
data.summary || null,
|
||||
data.seo_title || null,
|
||||
data.seo_description || null,
|
||||
data.seo_keywords || null
|
||||
);
|
||||
|
||||
return this.findById(content.content_id);
|
||||
}
|
||||
|
||||
static findById(contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, p.page_id, p.slug, p.template, p.summary, p.seo_title, p.seo_description, p.seo_keywords
|
||||
FROM contents c
|
||||
LEFT JOIN pages p ON c.content_id = p.page_id
|
||||
WHERE c.content_id = ?
|
||||
`);
|
||||
const row = stmt.get(contentId);
|
||||
|
||||
if (row && row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static findBySlug(slug) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, p.page_id, p.slug, p.template, p.summary, p.seo_title, p.seo_description, p.seo_keywords
|
||||
FROM contents c
|
||||
LEFT JOIN pages p ON c.content_id = p.page_id
|
||||
WHERE p.slug = ?
|
||||
`);
|
||||
const row = stmt.get(slug);
|
||||
|
||||
if (row && row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static update(contentId, data) {
|
||||
const db = getDb();
|
||||
|
||||
// 更新Content部分
|
||||
const contentData = { ...data };
|
||||
delete contentData.page_id;
|
||||
delete contentData.slug;
|
||||
delete contentData.template;
|
||||
delete contentData.summary;
|
||||
delete contentData.seo_title;
|
||||
delete contentData.seo_description;
|
||||
delete contentData.seo_keywords;
|
||||
|
||||
super.update(contentId, contentData);
|
||||
|
||||
// 更新Page扩展部分
|
||||
const pageData = {};
|
||||
if (data.slug !== undefined) pageData.slug = data.slug;
|
||||
if (data.template !== undefined) pageData.template = data.template;
|
||||
if (data.summary !== undefined) pageData.summary = data.summary;
|
||||
if (data.seo_title !== undefined) pageData.seo_title = data.seo_title;
|
||||
if (data.seo_description !== undefined) pageData.seo_description = data.seo_description;
|
||||
if (data.seo_keywords !== undefined) pageData.seo_keywords = data.seo_keywords;
|
||||
|
||||
if (Object.keys(pageData).length > 0) {
|
||||
const setClause = Object.keys(pageData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(pageData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE pages SET ${setClause} WHERE page_id = ?`);
|
||||
stmt.run(...values, contentId);
|
||||
}
|
||||
|
||||
return this.findById(contentId);
|
||||
}
|
||||
|
||||
static delete(contentId) {
|
||||
const db = getDb();
|
||||
|
||||
// 删除Page扩展记录
|
||||
const stmt1 = db.prepare('DELETE FROM pages WHERE page_id = ?');
|
||||
stmt1.run(contentId);
|
||||
|
||||
// 删除Content记录
|
||||
return super.delete(contentId);
|
||||
}
|
||||
|
||||
static findAll(options = {}) {
|
||||
const db = getDb();
|
||||
let query = `
|
||||
SELECT c.*, p.page_id, p.slug, p.template, p.summary, p.seo_title, p.seo_description, p.seo_keywords
|
||||
FROM contents c
|
||||
LEFT JOIN pages p ON c.content_id = p.page_id
|
||||
WHERE c.content_type = 'page'
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => {
|
||||
if (key === 'slug' || key === 'template' || key === 'summary' || key === 'seo_title' || key === 'seo_description' || key === 'seo_keywords') {
|
||||
return `p.${key} = ?`;
|
||||
}
|
||||
return `c.${key} = ?`;
|
||||
});
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Page;
|
||||
@@ -0,0 +1,145 @@
|
||||
const { getDb } = require('../config/database');
|
||||
const Content = require('./Content');
|
||||
|
||||
class Service extends Content {
|
||||
constructor(data = {}) {
|
||||
super(data);
|
||||
this.content_type = 'service';
|
||||
this.service_id = data.service_id || this.content_id;
|
||||
this.service_type = data.service_type || null;
|
||||
this.pricing_model = data.pricing_model || null;
|
||||
this.delivery_method = data.delivery_method || null;
|
||||
}
|
||||
|
||||
static create(data) {
|
||||
const db = getDb();
|
||||
|
||||
// 先创建Content记录
|
||||
const contentData = {
|
||||
...data,
|
||||
content_type: 'service'
|
||||
};
|
||||
|
||||
const content = super.create(contentData);
|
||||
|
||||
// 再创建Service扩展记录
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO services (service_id, service_type, pricing_model, delivery_method)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
content.content_id,
|
||||
data.service_type || null,
|
||||
data.pricing_model || null,
|
||||
data.delivery_method || null
|
||||
);
|
||||
|
||||
return this.findById(content.content_id);
|
||||
}
|
||||
|
||||
static findById(contentId) {
|
||||
const db = getDb();
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.*, s.service_id, s.service_type, s.pricing_model, s.delivery_method
|
||||
FROM contents c
|
||||
LEFT JOIN services s ON c.content_id = s.service_id
|
||||
WHERE c.content_id = ?
|
||||
`);
|
||||
const row = stmt.get(contentId);
|
||||
|
||||
if (row && row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static update(contentId, data) {
|
||||
const db = getDb();
|
||||
|
||||
// 更新Content部分
|
||||
const contentData = { ...data };
|
||||
delete contentData.service_id;
|
||||
delete contentData.service_type;
|
||||
delete contentData.pricing_model;
|
||||
delete contentData.delivery_method;
|
||||
|
||||
super.update(contentId, contentData);
|
||||
|
||||
// 更新Service扩展部分
|
||||
const serviceData = {};
|
||||
if (data.service_type !== undefined) serviceData.service_type = data.service_type;
|
||||
if (data.pricing_model !== undefined) serviceData.pricing_model = data.pricing_model;
|
||||
if (data.delivery_method !== undefined) serviceData.delivery_method = data.delivery_method;
|
||||
|
||||
if (Object.keys(serviceData).length > 0) {
|
||||
const setClause = Object.keys(serviceData).map(key => `${key} = ?`).join(', ');
|
||||
const values = Object.values(serviceData);
|
||||
|
||||
const stmt = db.prepare(`UPDATE services SET ${setClause} WHERE service_id = ?`);
|
||||
stmt.run(...values, contentId);
|
||||
}
|
||||
|
||||
return this.findById(contentId);
|
||||
}
|
||||
|
||||
static delete(contentId) {
|
||||
const db = getDb();
|
||||
|
||||
// 删除Service扩展记录
|
||||
const stmt1 = db.prepare('DELETE FROM services WHERE service_id = ?');
|
||||
stmt1.run(contentId);
|
||||
|
||||
// 删除Content记录
|
||||
return super.delete(contentId);
|
||||
}
|
||||
|
||||
static findAll(options = {}) {
|
||||
const db = getDb();
|
||||
let query = `
|
||||
SELECT c.*, s.service_id, s.service_type, s.pricing_model, s.delivery_method
|
||||
FROM contents c
|
||||
LEFT JOIN services s ON c.content_id = s.service_id
|
||||
WHERE c.content_type = 'service'
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => {
|
||||
if (key === 'service_type' || key === 'pricing_model' || key === 'delivery_method') {
|
||||
return `s.${key} = ?`;
|
||||
}
|
||||
return `c.${key} = ?`;
|
||||
});
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Service;
|
||||
@@ -0,0 +1,98 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const AdminAuthService = require('../services/AdminAuthService');
|
||||
const ContentService = require('../services/ContentService');
|
||||
const { getDb } = require('../config/database');
|
||||
|
||||
const auth = (req, res, next) => {
|
||||
const header = req.headers.authorization || '';
|
||||
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
||||
const session = AdminAuthService.verify(token);
|
||||
if (!session) return res.status(401).json({ error: '请先登录管理后台' });
|
||||
req.admin = session;
|
||||
req.adminToken = token;
|
||||
next();
|
||||
};
|
||||
|
||||
router.post('/login', (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
res.json(AdminAuthService.login(username, password));
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.use(auth);
|
||||
|
||||
router.get('/me', (req, res) => res.json({ username: req.admin.username }));
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
AdminAuthService.logout(req.adminToken);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.put('/password', (req, res) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
res.json(AdminAuthService.changePassword(req.admin.username, currentPassword, newPassword));
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/settings', (req, res) => {
|
||||
const rows = getDb().prepare('SELECT * FROM site_settings ORDER BY setting_group, setting_key').all();
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
router.put('/settings/:key', (req, res) => {
|
||||
const key = req.params.key;
|
||||
const value = req.body.value === undefined ? '' : String(req.body.value);
|
||||
const group = req.body.group || 'general';
|
||||
const db = getDb();
|
||||
const existing = db.prepare('SELECT setting_key FROM site_settings WHERE setting_key = ?').get(key);
|
||||
if (existing) {
|
||||
db.prepare('UPDATE site_settings SET setting_value = ?, setting_group = ?, updated_at = CURRENT_TIMESTAMP WHERE setting_key = ?').run(value, group, key);
|
||||
} else {
|
||||
db.prepare('INSERT INTO site_settings (setting_key, setting_value, setting_group) VALUES (?, ?, ?)').run(key, value, group);
|
||||
}
|
||||
res.json({ setting_key: key, setting_value: value, setting_group: group });
|
||||
});
|
||||
|
||||
router.get('/contents/:type', (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 50 } = req.query;
|
||||
res.json(ContentService.paginate(req.params.type, Number(page), Number(pageSize)));
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/contents/:type', (req, res) => {
|
||||
try {
|
||||
res.status(201).json(ContentService.create(req.body, req.params.type));
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/contents/:type/:id', (req, res) => {
|
||||
try {
|
||||
res.json(ContentService.update(req.params.id, req.body, req.params.type));
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/contents/:type/:id', (req, res) => {
|
||||
try {
|
||||
ContentService.delete(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,91 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ContentService = require('../services/ContentService');
|
||||
|
||||
// 获取内容列表(根据类型)
|
||||
router.get('/:type', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, orderBy } = req.query;
|
||||
const result = ContentService.paginate(req.params.type, parseInt(page), parseInt(pageSize), { orderBy });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取已发布的内容列表(根据类型)
|
||||
router.get('/:type/published', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, orderBy } = req.query;
|
||||
const result = ContentService.paginate(req.params.type, parseInt(page), parseInt(pageSize), {
|
||||
orderBy,
|
||||
where: { publish_status: 'published' }
|
||||
});
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取内容详情
|
||||
router.get('/:type/:id', async (req, res) => {
|
||||
try {
|
||||
const content = ContentService.getById(req.params.id, req.params.type);
|
||||
res.json(content);
|
||||
} catch (error) {
|
||||
res.status(404).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建内容
|
||||
router.post('/:type', async (req, res) => {
|
||||
try {
|
||||
const content = ContentService.create(req.body, req.params.type);
|
||||
res.status(201).json(content);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新内容
|
||||
router.put('/:type/:id', async (req, res) => {
|
||||
try {
|
||||
const content = ContentService.update(req.params.id, req.body, req.params.type);
|
||||
res.json(content);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除内容
|
||||
router.delete('/:type/:id', async (req, res) => {
|
||||
try {
|
||||
ContentService.delete(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 内容状态流转
|
||||
router.put('/:type/:id/transition', async (req, res) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
const content = ContentService.transitionStatus(req.params.id, status);
|
||||
res.json(content);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建历史版本
|
||||
router.post('/:type/:id/history', async (req, res) => {
|
||||
try {
|
||||
const content = ContentService.createHistoricalVersion(req.params.id);
|
||||
res.status(201).json(content);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,86 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const EntityContentJoinService = require('../services/EntityContentJoinService');
|
||||
|
||||
// 获取关联列表
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, orderBy } = req.query;
|
||||
const result = EntityContentJoinService.paginate(parseInt(page), parseInt(pageSize), { orderBy });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取关联详情
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const join = EntityContentJoinService.getById(req.params.id);
|
||||
res.json(join);
|
||||
} catch (error) {
|
||||
res.status(404).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取实体的所有关联
|
||||
router.get('/entity/:entityId', async (req, res) => {
|
||||
try {
|
||||
const joins = EntityContentJoinService.getByEntityId(req.params.entityId);
|
||||
res.json(joins);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取内容的所有关联
|
||||
router.get('/content/:contentId', async (req, res) => {
|
||||
try {
|
||||
const joins = EntityContentJoinService.getByContentId(req.params.contentId);
|
||||
res.json(joins);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建关联
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const join = EntityContentJoinService.create(req.body);
|
||||
res.status(201).json(join);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新关联
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const join = EntityContentJoinService.update(req.params.id, req.body);
|
||||
res.json(join);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除关联
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
EntityContentJoinService.delete(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除实体与内容的关联
|
||||
router.delete('/entity/:entityId/content/:contentId', async (req, res) => {
|
||||
try {
|
||||
EntityContentJoinService.deleteByEntityAndContent(req.params.entityId, req.params.contentId);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,56 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const EntityService = require('../services/EntityService');
|
||||
|
||||
// 获取实体列表
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, orderBy } = req.query;
|
||||
const result = EntityService.paginate(parseInt(page), parseInt(pageSize), { orderBy });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取实体详情
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const entity = EntityService.getById(req.params.id);
|
||||
res.json(entity);
|
||||
} catch (error) {
|
||||
res.status(404).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建实体
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const entity = EntityService.create(req.body);
|
||||
res.status(201).json(entity);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新实体
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const entity = EntityService.update(req.params.id, req.body);
|
||||
res.json(entity);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除实体
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
EntityService.delete(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,56 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const JsonLdService = require('../services/JsonLdService');
|
||||
|
||||
// 获取实体的JSON-LD
|
||||
router.get('/entities/:id', async (req, res) => {
|
||||
try {
|
||||
const jsonLd = JsonLdService.generateEntityJsonLd(req.params.id);
|
||||
res.json(jsonLd);
|
||||
} catch (error) {
|
||||
res.status(404).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取内容的JSON-LD
|
||||
router.get('/contents/:id', async (req, res) => {
|
||||
try {
|
||||
const jsonLd = JsonLdService.generateContentJsonLd(req.params.id);
|
||||
res.json(jsonLd);
|
||||
} catch (error) {
|
||||
res.status(404).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有实体的JSON-LD列表
|
||||
router.get('/entities', async (req, res) => {
|
||||
try {
|
||||
const jsonLdList = JsonLdService.generateAllEntitiesJsonLd();
|
||||
res.json(jsonLdList);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有内容的JSON-LD列表
|
||||
router.get('/contents', async (req, res) => {
|
||||
try {
|
||||
const { type } = req.query;
|
||||
const jsonLdList = JsonLdService.generateAllContentsJsonLd(type);
|
||||
res.json(jsonLdList);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取站点地图的JSON-LD
|
||||
router.get('/sitemap', async (req, res) => {
|
||||
try {
|
||||
const jsonLd = JsonLdService.generateSitemapJsonLd();
|
||||
res.json(jsonLd);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,149 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const PublicApiService = require('../services/PublicApiService');
|
||||
const { getDb } = require('../config/database');
|
||||
|
||||
// 获取前台展示配置(只读)
|
||||
router.get('/settings', async (req, res) => {
|
||||
try {
|
||||
const rows = getDb().prepare('SELECT setting_key, setting_value FROM site_settings').all();
|
||||
res.json(Object.fromEntries(rows.map(row => [row.setting_key, row.setting_value])));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取已发布的实体列表
|
||||
router.get('/entities', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, orderBy } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const entities = PublicApiService.getPublishedEntities({
|
||||
orderBy,
|
||||
limit: parseInt(pageSize),
|
||||
offset
|
||||
});
|
||||
res.json(entities);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取已发布的实体详情
|
||||
router.get('/entities/:id', async (req, res) => {
|
||||
try {
|
||||
const entity = PublicApiService.getPublishedEntityById(req.params.id);
|
||||
if (!entity) {
|
||||
return res.status(404).json({ error: '实体不存在' });
|
||||
}
|
||||
res.json(entity);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取实体的已发布关系
|
||||
router.get('/entities/:id/relationships', async (req, res) => {
|
||||
try {
|
||||
const sourceRelationships = PublicApiService.getPublishedRelationshipsBySource(req.params.id);
|
||||
const targetRelationships = PublicApiService.getPublishedRelationshipsByTarget(req.params.id);
|
||||
|
||||
res.json({
|
||||
source: sourceRelationships,
|
||||
target: targetRelationships
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取实体的已发布内容
|
||||
router.get('/entities/:id/contents', async (req, res) => {
|
||||
try {
|
||||
const contents = PublicApiService.getEntityPublishedContents(req.params.id);
|
||||
res.json(contents);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取已发布的内容列表
|
||||
router.get('/contents', async (req, res) => {
|
||||
try {
|
||||
const { type, page = 1, pageSize = 10, orderBy } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const contents = PublicApiService.getPublishedContents(type, {
|
||||
orderBy,
|
||||
limit: parseInt(pageSize),
|
||||
offset
|
||||
});
|
||||
res.json(contents);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取已发布的内容详情
|
||||
router.get('/contents/:id', async (req, res) => {
|
||||
try {
|
||||
const content = PublicApiService.getPublishedContentById(req.params.id);
|
||||
if (!content) {
|
||||
return res.status(404).json({ error: '内容不存在或未发布' });
|
||||
}
|
||||
res.json(content);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取内容的实体关联
|
||||
router.get('/contents/:id/entities', async (req, res) => {
|
||||
try {
|
||||
const entities = PublicApiService.getContentEntities(req.params.id);
|
||||
res.json(entities);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索实体
|
||||
router.get('/search/entities', async (req, res) => {
|
||||
try {
|
||||
const { keyword, page = 1, pageSize = 10, orderBy } = req.query;
|
||||
if (!keyword) {
|
||||
return res.status(400).json({ error: '搜索关键词不能为空' });
|
||||
}
|
||||
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const entities = PublicApiService.searchEntities(keyword, {
|
||||
orderBy,
|
||||
limit: parseInt(pageSize),
|
||||
offset
|
||||
});
|
||||
res.json(entities);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索内容
|
||||
router.get('/search/contents', async (req, res) => {
|
||||
try {
|
||||
const { keyword, type, page = 1, pageSize = 10, orderBy } = req.query;
|
||||
if (!keyword) {
|
||||
return res.status(400).json({ error: '搜索关键词不能为空' });
|
||||
}
|
||||
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const contents = PublicApiService.searchContents(keyword, type, {
|
||||
orderBy,
|
||||
limit: parseInt(pageSize),
|
||||
offset
|
||||
});
|
||||
res.json(contents);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,76 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const DirectedRelationshipService = require('../services/DirectedRelationshipService');
|
||||
|
||||
// 获取关系列表
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, orderBy } = req.query;
|
||||
const result = DirectedRelationshipService.paginate(parseInt(page), parseInt(pageSize), { orderBy });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取关系详情
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const relationship = DirectedRelationshipService.getById(req.params.id);
|
||||
res.json(relationship);
|
||||
} catch (error) {
|
||||
res.status(404).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取实体的出向关系
|
||||
router.get('/source/:entityId', async (req, res) => {
|
||||
try {
|
||||
const relationships = DirectedRelationshipService.getBySourceEntity(req.params.entityId);
|
||||
res.json(relationships);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取实体的入向关系
|
||||
router.get('/target/:entityId', async (req, res) => {
|
||||
try {
|
||||
const relationships = DirectedRelationshipService.getByTargetEntity(req.params.entityId);
|
||||
res.json(relationships);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建关系
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const relationship = DirectedRelationshipService.create(req.body);
|
||||
res.status(201).json(relationship);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新关系
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const relationship = DirectedRelationshipService.update(req.params.id, req.body);
|
||||
res.json(relationship);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除关系
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
DirectedRelationshipService.delete(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,57 @@
|
||||
const crypto = require('crypto');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { getDb } = require('../config/database');
|
||||
|
||||
const sessions = new Map();
|
||||
|
||||
const hashPassword = (password, salt = crypto.randomBytes(16).toString('hex')) => {
|
||||
const hash = crypto.scryptSync(password, salt, 64).toString('hex');
|
||||
return `${salt}:${hash}`;
|
||||
};
|
||||
|
||||
const verifyPassword = (password, stored) => {
|
||||
const [salt, expected] = String(stored || '').split(':');
|
||||
if (!salt || !expected) return false;
|
||||
const actual = crypto.scryptSync(password, salt, 64).toString('hex');
|
||||
return crypto.timingSafeEqual(Buffer.from(actual, 'hex'), Buffer.from(expected, 'hex'));
|
||||
};
|
||||
|
||||
class AdminAuthService {
|
||||
static login(username, password) {
|
||||
const user = getDb().prepare('SELECT * FROM admin_users WHERE username = ?').get(username);
|
||||
if (!user || !verifyPassword(password, user.password_hash)) {
|
||||
throw new Error('用户名或密码错误');
|
||||
}
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
sessions.set(token, { userId: user.user_id, username: user.username, expiresAt: Date.now() + 8 * 60 * 60 * 1000 });
|
||||
return { token, username: user.username, expiresIn: 8 * 60 * 60 };
|
||||
}
|
||||
|
||||
static verify(token) {
|
||||
const session = sessions.get(token);
|
||||
if (!session || session.expiresAt < Date.now()) {
|
||||
if (token) sessions.delete(token);
|
||||
return null;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
static changePassword(username, currentPassword, newPassword) {
|
||||
if (!newPassword || newPassword.length < 8) throw new Error('新密码至少需要8位');
|
||||
const db = getDb();
|
||||
const user = db.prepare('SELECT * FROM admin_users WHERE username = ?').get(username);
|
||||
if (!user || !verifyPassword(currentPassword, user.password_hash)) throw new Error('当前密码错误');
|
||||
db.prepare('UPDATE admin_users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?')
|
||||
.run(hashPassword(newPassword), username);
|
||||
for (const [token, session] of sessions.entries()) {
|
||||
if (session.username === username) sessions.delete(token);
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
static logout(token) {
|
||||
sessions.delete(token);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AdminAuthService;
|
||||
@@ -0,0 +1,220 @@
|
||||
const Content = require('../models/Content');
|
||||
const Page = require('../models/Page');
|
||||
const Article = require('../models/Article');
|
||||
const Case = require('../models/Case');
|
||||
const FAQ = require('../models/FAQ');
|
||||
const Capability = require('../models/Capability');
|
||||
const Service = require('../models/Service');
|
||||
const { getDb } = require('../config/database');
|
||||
const Joi = require('joi');
|
||||
|
||||
class ContentService {
|
||||
// 根据内容类型获取对应的模型
|
||||
static getModelByType(contentType) {
|
||||
const models = {
|
||||
'page': Page,
|
||||
'article': Article,
|
||||
'case': Case,
|
||||
'faq': FAQ,
|
||||
'capability': Capability,
|
||||
'service': Service
|
||||
};
|
||||
|
||||
return models[contentType] || Content;
|
||||
}
|
||||
|
||||
// 验证内容数据
|
||||
static validateContent(data, contentType, isUpdate = false) {
|
||||
const baseSchema = {
|
||||
title: isUpdate ? Joi.string().min(1).max(255) : Joi.string().min(1).max(255).required(),
|
||||
body: Joi.string().allow(null, ''),
|
||||
publish_status: Joi.string().valid('draft', 'review', 'published', 'unpublished', 'historical'),
|
||||
authority_level: Joi.number().integer().min(1).max(7),
|
||||
version_no: Joi.number().integer().min(1),
|
||||
version_note: Joi.string().allow(null, ''),
|
||||
metadata: Joi.object()
|
||||
};
|
||||
|
||||
// 根据内容类型添加特定字段验证
|
||||
let specificSchema = {};
|
||||
|
||||
switch (contentType) {
|
||||
case 'page':
|
||||
specificSchema = {
|
||||
slug: Joi.string().pattern(/^\/$|^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
||||
summary: Joi.string().allow(null, ''),
|
||||
template: Joi.string().allow(null, ''),
|
||||
seo_title: Joi.string().allow(null, '').max(255),
|
||||
seo_description: Joi.string().allow(null, '').max(500),
|
||||
seo_keywords: Joi.string().allow(null, '')
|
||||
};
|
||||
break;
|
||||
case 'article':
|
||||
specificSchema = {
|
||||
category: Joi.string().allow(null, ''),
|
||||
tags: Joi.string().allow(null, ''),
|
||||
cover_image: Joi.string().uri().allow(null, ''),
|
||||
author: Joi.string().allow(null, '')
|
||||
};
|
||||
break;
|
||||
case 'case':
|
||||
specificSchema = {
|
||||
client_name: Joi.string().allow(null, ''),
|
||||
industry: Joi.string().allow(null, ''),
|
||||
challenge: Joi.string().allow(null, ''),
|
||||
solution: Joi.string().allow(null, ''),
|
||||
result: Joi.string().allow(null, '')
|
||||
};
|
||||
break;
|
||||
case 'faq':
|
||||
specificSchema = {
|
||||
question: Joi.string().allow(null, ''),
|
||||
answer: Joi.string().allow(null, ''),
|
||||
sort_order: Joi.number().integer().min(0)
|
||||
};
|
||||
break;
|
||||
case 'capability':
|
||||
specificSchema = {
|
||||
capability_type: Joi.string().allow(null, ''),
|
||||
features: Joi.string().allow(null, ''),
|
||||
use_cases: Joi.string().allow(null, '')
|
||||
};
|
||||
break;
|
||||
case 'service':
|
||||
specificSchema = {
|
||||
service_type: Joi.string().allow(null, ''),
|
||||
pricing_model: Joi.string().allow(null, ''),
|
||||
delivery_method: Joi.string().allow(null, '')
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
const schema = Joi.object({ ...baseSchema, ...specificSchema });
|
||||
return schema.validate(data, { abortEarly: false });
|
||||
}
|
||||
|
||||
// 创建内容
|
||||
static create(data, contentType) {
|
||||
if (!contentType) {
|
||||
throw new Error('内容类型不能为空');
|
||||
}
|
||||
|
||||
const { error, value } = this.validateContent(data, contentType);
|
||||
if (error) {
|
||||
throw new Error(`验证失败: ${error.details.map(d => d.message).join(', ')}`);
|
||||
}
|
||||
|
||||
const Model = this.getModelByType(contentType);
|
||||
return Model.create({ ...value, content_type: contentType });
|
||||
}
|
||||
|
||||
// 获取内容详情
|
||||
static getById(contentId, contentType = null) {
|
||||
let content;
|
||||
|
||||
if (contentType) {
|
||||
const Model = this.getModelByType(contentType);
|
||||
content = Model.findById(contentId);
|
||||
} else {
|
||||
content = Content.findById(contentId);
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
throw new Error('内容不存在');
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// 获取内容列表
|
||||
static getList(contentType, options = {}) {
|
||||
const Model = this.getModelByType(contentType);
|
||||
return Model.findAll(options);
|
||||
}
|
||||
|
||||
// 获取已发布的内容
|
||||
static getPublished(contentType, options = {}) {
|
||||
const Model = this.getModelByType(contentType);
|
||||
return Model.findAll({
|
||||
...options,
|
||||
where: {
|
||||
...options.where,
|
||||
publish_status: 'published'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新内容
|
||||
static update(contentId, data, contentType = null) {
|
||||
// 检查内容是否存在
|
||||
const existingContent = Content.findById(contentId);
|
||||
if (!existingContent) {
|
||||
throw new Error('内容不存在');
|
||||
}
|
||||
|
||||
// 如果指定了内容类型,检查是否匹配
|
||||
if (contentType && existingContent.content_type !== contentType) {
|
||||
throw new Error('内容类型不匹配');
|
||||
}
|
||||
|
||||
const { error, value } = this.validateContent(data, existingContent.content_type, true);
|
||||
if (error) {
|
||||
throw new Error(`验证失败: ${error.details.map(d => d.message).join(', ')}`);
|
||||
}
|
||||
|
||||
const Model = this.getModelByType(existingContent.content_type);
|
||||
return Model.update(contentId, value);
|
||||
}
|
||||
|
||||
// 删除内容
|
||||
static delete(contentId) {
|
||||
const content = Content.findById(contentId);
|
||||
if (!content) {
|
||||
throw new Error('内容不存在');
|
||||
}
|
||||
|
||||
const Model = this.getModelByType(content.content_type);
|
||||
return Model.delete(contentId);
|
||||
}
|
||||
|
||||
// 内容状态流转
|
||||
static transitionStatus(contentId, newStatus) {
|
||||
return Content.transitionStatus(contentId, newStatus);
|
||||
}
|
||||
|
||||
// 创建历史版本
|
||||
static createHistoricalVersion(contentId) {
|
||||
return Content.createHistoricalVersion(contentId);
|
||||
}
|
||||
|
||||
// 获取内容总数
|
||||
static count(contentType = null) {
|
||||
if (contentType) {
|
||||
const row = getDb().prepare('SELECT COUNT(*) AS count FROM contents WHERE content_type = ?').get(contentType);
|
||||
return Number(row?.count || 0);
|
||||
}
|
||||
return Content.count();
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
static paginate(contentType, page = 1, pageSize = 10, options = {}) {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const Model = this.getModelByType(contentType);
|
||||
const list = Model.findAll({
|
||||
...options,
|
||||
limit: pageSize,
|
||||
offset
|
||||
});
|
||||
const total = this.count(contentType);
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ContentService;
|
||||
@@ -0,0 +1,146 @@
|
||||
const DirectedRelationship = require('../models/DirectedRelationship');
|
||||
const Entity = require('../models/Entity');
|
||||
const Joi = require('joi');
|
||||
|
||||
class DirectedRelationshipService {
|
||||
// 验证关系数据
|
||||
static validateRelationship(data, isUpdate = false) {
|
||||
const schema = Joi.object({
|
||||
source_entity_id: isUpdate ? Joi.string().uuid() : Joi.string().uuid().required(),
|
||||
relation_type: isUpdate ? Joi.string().min(1).max(100) : Joi.string().min(1).max(100).required(),
|
||||
target_entity_id: isUpdate ? Joi.string().uuid() : Joi.string().uuid().required(),
|
||||
rel_status: Joi.string().valid('active', 'inactive'),
|
||||
authority_level: Joi.number().integer().min(1).max(7),
|
||||
is_public: Joi.number().integer().valid(0, 1),
|
||||
effective_at: Joi.date().iso(),
|
||||
expire_at: Joi.date().iso().greater(Joi.ref('effective_at'))
|
||||
});
|
||||
|
||||
return schema.validate(data, { abortEarly: false });
|
||||
}
|
||||
|
||||
// 创建关系
|
||||
static create(data) {
|
||||
const { error, value } = this.validateRelationship(data);
|
||||
if (error) {
|
||||
throw new Error(`验证失败: ${error.details.map(d => d.message).join(', ')}`);
|
||||
}
|
||||
|
||||
// 检查源实体是否存在
|
||||
const sourceEntity = Entity.findById(value.source_entity_id);
|
||||
if (!sourceEntity) {
|
||||
throw new Error('源实体不存在');
|
||||
}
|
||||
|
||||
// 检查目标实体是否存在
|
||||
const targetEntity = Entity.findById(value.target_entity_id);
|
||||
if (!targetEntity) {
|
||||
throw new Error('目标实体不存在');
|
||||
}
|
||||
|
||||
// 检查是否试图创建自引用关系
|
||||
if (value.source_entity_id === value.target_entity_id) {
|
||||
throw new Error('不能创建自引用关系');
|
||||
}
|
||||
|
||||
return DirectedRelationship.create(value);
|
||||
}
|
||||
|
||||
// 获取关系详情
|
||||
static getById(relId) {
|
||||
const relationship = DirectedRelationship.findById(relId);
|
||||
if (!relationship) {
|
||||
throw new Error('关系不存在');
|
||||
}
|
||||
return relationship;
|
||||
}
|
||||
|
||||
// 获取关系列表
|
||||
static getList(options = {}) {
|
||||
return DirectedRelationship.findAll(options);
|
||||
}
|
||||
|
||||
// 获取实体的出向关系
|
||||
static getBySourceEntity(sourceEntityId, options = {}) {
|
||||
return DirectedRelationship.findBySourceEntity(sourceEntityId, options);
|
||||
}
|
||||
|
||||
// 获取实体的入向关系(反向查询)
|
||||
static getByTargetEntity(targetEntityId, options = {}) {
|
||||
return DirectedRelationship.findByTargetEntity(targetEntityId, options);
|
||||
}
|
||||
|
||||
// 更新关系
|
||||
static update(relId, data) {
|
||||
// 检查关系是否存在
|
||||
const existingRelationship = DirectedRelationship.findById(relId);
|
||||
if (!existingRelationship) {
|
||||
throw new Error('关系不存在');
|
||||
}
|
||||
|
||||
// 如果要修改源实体或目标实体,检查新实体是否存在
|
||||
if (data.source_entity_id && data.source_entity_id !== existingRelationship.source_entity_id) {
|
||||
const sourceEntity = Entity.findById(data.source_entity_id);
|
||||
if (!sourceEntity) {
|
||||
throw new Error('源实体不存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.target_entity_id && data.target_entity_id !== existingRelationship.target_entity_id) {
|
||||
const targetEntity = Entity.findById(data.target_entity_id);
|
||||
if (!targetEntity) {
|
||||
throw new Error('目标实体不存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否试图创建自引用关系
|
||||
const sourceEntityId = data.source_entity_id || existingRelationship.source_entity_id;
|
||||
const targetEntityId = data.target_entity_id || existingRelationship.target_entity_id;
|
||||
if (sourceEntityId === targetEntityId) {
|
||||
throw new Error('不能创建自引用关系');
|
||||
}
|
||||
|
||||
const { error, value } = this.validateRelationship(data, true);
|
||||
if (error) {
|
||||
throw new Error(`验证失败: ${error.details.map(d => d.message).join(', ')}`);
|
||||
}
|
||||
|
||||
return DirectedRelationship.update(relId, value);
|
||||
}
|
||||
|
||||
// 删除关系
|
||||
static delete(relId) {
|
||||
const relationship = DirectedRelationship.findById(relId);
|
||||
if (!relationship) {
|
||||
throw new Error('关系不存在');
|
||||
}
|
||||
|
||||
return DirectedRelationship.delete(relId);
|
||||
}
|
||||
|
||||
// 获取关系总数
|
||||
static count() {
|
||||
return DirectedRelationship.count();
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
static paginate(page = 1, pageSize = 10, options = {}) {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const list = DirectedRelationship.findAll({
|
||||
...options,
|
||||
limit: pageSize,
|
||||
offset
|
||||
});
|
||||
const total = this.count();
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DirectedRelationshipService;
|
||||
@@ -0,0 +1,142 @@
|
||||
const EntityContentJoin = require('../models/EntityContentJoin');
|
||||
const Entity = require('../models/Entity');
|
||||
const Content = require('../models/Content');
|
||||
const Joi = require('joi');
|
||||
|
||||
class EntityContentJoinService {
|
||||
// 验证关联数据
|
||||
static validateJoin(data, isUpdate = false) {
|
||||
const schema = Joi.object({
|
||||
entity_id: isUpdate ? Joi.string().uuid() : Joi.string().uuid().required(),
|
||||
content_id: isUpdate ? Joi.string().uuid() : Joi.string().uuid().required(),
|
||||
join_note: Joi.string().allow(null, '')
|
||||
});
|
||||
|
||||
return schema.validate(data, { abortEarly: false });
|
||||
}
|
||||
|
||||
// 创建关联
|
||||
static create(data) {
|
||||
const { error, value } = this.validateJoin(data);
|
||||
if (error) {
|
||||
throw new Error(`验证失败: ${error.details.map(d => d.message).join(', ')}`);
|
||||
}
|
||||
|
||||
// 检查实体是否存在
|
||||
const entity = Entity.findById(value.entity_id);
|
||||
if (!entity) {
|
||||
throw new Error('实体不存在');
|
||||
}
|
||||
|
||||
// 检查内容是否存在
|
||||
const content = Content.findById(value.content_id);
|
||||
if (!content) {
|
||||
throw new Error('内容不存在');
|
||||
}
|
||||
|
||||
// 检查是否已存在相同的关联
|
||||
const existingJoins = EntityContentJoin.findByEntityId(value.entity_id);
|
||||
const duplicateJoin = existingJoins.find(join => join.content_id === value.content_id);
|
||||
if (duplicateJoin) {
|
||||
throw new Error('该实体与内容的关联已存在');
|
||||
}
|
||||
|
||||
return EntityContentJoin.create(value);
|
||||
}
|
||||
|
||||
// 获取关联详情
|
||||
static getById(joinId) {
|
||||
const join = EntityContentJoin.findById(joinId);
|
||||
if (!join) {
|
||||
throw new Error('关联不存在');
|
||||
}
|
||||
return join;
|
||||
}
|
||||
|
||||
// 获取实体的所有关联
|
||||
static getByEntityId(entityId) {
|
||||
return EntityContentJoin.findByEntityId(entityId);
|
||||
}
|
||||
|
||||
// 获取内容的所有关联
|
||||
static getByContentId(contentId) {
|
||||
return EntityContentJoin.findByContentId(contentId);
|
||||
}
|
||||
|
||||
// 获取所有关联
|
||||
static getList(options = {}) {
|
||||
return EntityContentJoin.findAll(options);
|
||||
}
|
||||
|
||||
// 更新关联
|
||||
static update(joinId, data) {
|
||||
// 检查关联是否存在
|
||||
const existingJoin = EntityContentJoin.findById(joinId);
|
||||
if (!existingJoin) {
|
||||
throw new Error('关联不存在');
|
||||
}
|
||||
|
||||
// 如果要修改实体或内容,检查新实体或内容是否存在
|
||||
if (data.entity_id && data.entity_id !== existingJoin.entity_id) {
|
||||
const entity = Entity.findById(data.entity_id);
|
||||
if (!entity) {
|
||||
throw new Error('实体不存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.content_id && data.content_id !== existingJoin.content_id) {
|
||||
const content = Content.findById(data.content_id);
|
||||
if (!content) {
|
||||
throw new Error('内容不存在');
|
||||
}
|
||||
}
|
||||
|
||||
const { error, value } = this.validateJoin(data, true);
|
||||
if (error) {
|
||||
throw new Error(`验证失败: ${error.details.map(d => d.message).join(', ')}`);
|
||||
}
|
||||
|
||||
return EntityContentJoin.update(joinId, value);
|
||||
}
|
||||
|
||||
// 删除关联
|
||||
static delete(joinId) {
|
||||
const join = EntityContentJoin.findById(joinId);
|
||||
if (!join) {
|
||||
throw new Error('关联不存在');
|
||||
}
|
||||
|
||||
return EntityContentJoin.delete(joinId);
|
||||
}
|
||||
|
||||
// 删除实体与内容的关联
|
||||
static deleteByEntityAndContent(entityId, contentId) {
|
||||
return EntityContentJoin.deleteByEntityAndContent(entityId, contentId);
|
||||
}
|
||||
|
||||
// 获取关联总数
|
||||
static count() {
|
||||
return EntityContentJoin.count();
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
static paginate(page = 1, pageSize = 10, options = {}) {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const list = EntityContentJoin.findAll({
|
||||
...options,
|
||||
limit: pageSize,
|
||||
offset
|
||||
});
|
||||
const total = this.count();
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EntityContentJoinService;
|
||||
@@ -0,0 +1,97 @@
|
||||
const Entity = require('../models/Entity');
|
||||
const Joi = require('joi');
|
||||
|
||||
class EntityService {
|
||||
// 验证实体数据
|
||||
static validateEntity(data, isUpdate = false) {
|
||||
const schema = Joi.object({
|
||||
name: isUpdate ? Joi.string().min(1).max(255) : Joi.string().min(1).max(255).required(),
|
||||
definition: Joi.string().allow(null, ''),
|
||||
description: Joi.string().allow(null, ''),
|
||||
metadata: Joi.object()
|
||||
});
|
||||
|
||||
return schema.validate(data, { abortEarly: false });
|
||||
}
|
||||
|
||||
// 创建实体
|
||||
static create(data) {
|
||||
const { error, value } = this.validateEntity(data);
|
||||
if (error) {
|
||||
throw new Error(`验证失败: ${error.details.map(d => d.message).join(', ')}`);
|
||||
}
|
||||
|
||||
return Entity.create(value);
|
||||
}
|
||||
|
||||
// 获取实体详情
|
||||
static getById(entityId) {
|
||||
const entity = Entity.findById(entityId);
|
||||
if (!entity) {
|
||||
throw new Error('实体不存在');
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
// 获取实体列表
|
||||
static getList(options = {}) {
|
||||
return Entity.findAll(options);
|
||||
}
|
||||
|
||||
// 更新实体
|
||||
static update(entityId, data) {
|
||||
// 检查实体是否存在
|
||||
const existingEntity = Entity.findById(entityId);
|
||||
if (!existingEntity) {
|
||||
throw new Error('实体不存在');
|
||||
}
|
||||
|
||||
// 禁止修改entity_id
|
||||
if (data.entity_id && data.entity_id !== entityId) {
|
||||
throw new Error('不能修改entity_id');
|
||||
}
|
||||
|
||||
const { error, value } = this.validateEntity(data, true);
|
||||
if (error) {
|
||||
throw new Error(`验证失败: ${error.details.map(d => d.message).join(', ')}`);
|
||||
}
|
||||
|
||||
return Entity.update(entityId, value);
|
||||
}
|
||||
|
||||
// 删除实体
|
||||
static delete(entityId) {
|
||||
const entity = Entity.findById(entityId);
|
||||
if (!entity) {
|
||||
throw new Error('实体不存在');
|
||||
}
|
||||
|
||||
return Entity.delete(entityId);
|
||||
}
|
||||
|
||||
// 获取实体总数
|
||||
static count() {
|
||||
return Entity.count();
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
static paginate(page = 1, pageSize = 10, options = {}) {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const list = Entity.findAll({
|
||||
...options,
|
||||
limit: pageSize,
|
||||
offset
|
||||
});
|
||||
const total = this.count();
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EntityService;
|
||||
@@ -0,0 +1,214 @@
|
||||
const Entity = require('../models/Entity');
|
||||
const DirectedRelationship = require('../models/DirectedRelationship');
|
||||
const Content = require('../models/Content');
|
||||
const EntityContentJoin = require('../models/EntityContentJoin');
|
||||
|
||||
class JsonLdService {
|
||||
// 生成实体的JSON-LD结构化数据
|
||||
static generateEntityJsonLd(entityId) {
|
||||
const entity = Entity.findById(entityId);
|
||||
if (!entity) {
|
||||
throw new Error('实体不存在');
|
||||
}
|
||||
|
||||
// 获取实体的关系
|
||||
const sourceRelationships = DirectedRelationship.findBySourceEntity(entityId, {
|
||||
where: { rel_status: 'active', is_public: 1 }
|
||||
});
|
||||
|
||||
const targetRelationships = DirectedRelationship.findByTargetEntity(entityId, {
|
||||
where: { rel_status: 'active', is_public: 1 }
|
||||
});
|
||||
|
||||
// 获取实体的内容
|
||||
const contentJoins = EntityContentJoin.findByEntityId(entityId);
|
||||
const contentIds = contentJoins.map(join => join.content_id);
|
||||
let contents = [];
|
||||
|
||||
if (contentIds.length > 0) {
|
||||
const { getDb } = require('../config/database');
|
||||
const db = getDb();
|
||||
const placeholders = contentIds.map(() => '?').join(',');
|
||||
const stmt = db.prepare(`SELECT * FROM contents WHERE content_id IN (${placeholders}) AND publish_status = 'published'`);
|
||||
contents = stmt.all(...contentIds);
|
||||
}
|
||||
|
||||
// 构建JSON-LD结构
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Thing',
|
||||
'@id': `entity:${entity.entity_id}`,
|
||||
'name': entity.name,
|
||||
'description': entity.description || entity.definition || '',
|
||||
'identifier': entity.entity_id
|
||||
};
|
||||
|
||||
// 添加元数据
|
||||
if (entity.metadata) {
|
||||
const metadata = typeof entity.metadata === 'string' ? JSON.parse(entity.metadata) : entity.metadata;
|
||||
Object.assign(jsonLd, metadata);
|
||||
}
|
||||
|
||||
// 添加关系
|
||||
if (sourceRelationships.length > 0) {
|
||||
jsonLd['relatedTo'] = sourceRelationships.map(rel => ({
|
||||
'@type': 'Relationship',
|
||||
'relationshipType': rel.relation_type,
|
||||
'target': {
|
||||
'@type': 'Thing',
|
||||
'@id': `entity:${rel.target_entity_id}`,
|
||||
'identifier': rel.target_entity_id
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 添加内容
|
||||
if (contents.length > 0) {
|
||||
jsonLd['associatedMedia'] = contents.map(content => ({
|
||||
'@type': 'CreativeWork',
|
||||
'@id': `content:${content.content_id}`,
|
||||
'name': content.title,
|
||||
'description': content.body ? content.body.substring(0, 200) : '',
|
||||
'identifier': content.content_id
|
||||
}));
|
||||
}
|
||||
|
||||
return jsonLd;
|
||||
}
|
||||
|
||||
// 生成内容的JSON-LD结构化数据
|
||||
static generateContentJsonLd(contentId) {
|
||||
const content = Content.findById(contentId);
|
||||
if (!content) {
|
||||
throw new Error('内容不存在');
|
||||
}
|
||||
|
||||
// 获取内容的实体关联
|
||||
const contentJoins = EntityContentJoin.findByContentId(contentId);
|
||||
const entityIds = contentJoins.map(join => join.entity_id);
|
||||
let entities = [];
|
||||
|
||||
if (entityIds.length > 0) {
|
||||
const { getDb } = require('../config/database');
|
||||
const db = getDb();
|
||||
const placeholders = entityIds.map(() => '?').join(',');
|
||||
const stmt = db.prepare(`SELECT * FROM entities WHERE entity_id IN (${placeholders})`);
|
||||
entities = stmt.all(...entityIds);
|
||||
}
|
||||
|
||||
// 根据内容类型构建不同的JSON-LD结构
|
||||
let jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@id': `content:${content.content_id}`,
|
||||
'name': content.title,
|
||||
'description': content.body ? content.body.substring(0, 200) : '',
|
||||
'identifier': content.content_id
|
||||
};
|
||||
|
||||
switch (content.content_type) {
|
||||
case 'page':
|
||||
jsonLd['@type'] = 'WebPage';
|
||||
break;
|
||||
case 'article':
|
||||
jsonLd['@type'] = 'Article';
|
||||
break;
|
||||
case 'case':
|
||||
jsonLd['@type'] = 'CaseStudy';
|
||||
break;
|
||||
case 'faq':
|
||||
jsonLd['@type'] = 'FAQPage';
|
||||
break;
|
||||
case 'capability':
|
||||
jsonLd['@type'] = 'Service';
|
||||
break;
|
||||
case 'service':
|
||||
jsonLd['@type'] = 'Service';
|
||||
break;
|
||||
default:
|
||||
jsonLd['@type'] = 'CreativeWork';
|
||||
}
|
||||
|
||||
// 添加元数据
|
||||
if (content.metadata) {
|
||||
const metadata = typeof content.metadata === 'string' ? JSON.parse(content.metadata) : content.metadata;
|
||||
Object.assign(jsonLd, metadata);
|
||||
}
|
||||
|
||||
// 添加实体关联
|
||||
if (entities.length > 0) {
|
||||
jsonLd['about'] = entities.map(entity => ({
|
||||
'@type': 'Thing',
|
||||
'@id': `entity:${entity.entity_id}`,
|
||||
'name': entity.name,
|
||||
'identifier': entity.entity_id
|
||||
}));
|
||||
}
|
||||
|
||||
// 特定内容类型的字段
|
||||
if (content.content_type === 'article') {
|
||||
jsonLd['articleBody'] = content.body;
|
||||
if (content.author) {
|
||||
jsonLd['author'] = {
|
||||
'@type': 'Person',
|
||||
'name': content.author
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (content.content_type === 'faq') {
|
||||
jsonLd['mainEntity'] = {
|
||||
'@type': 'Question',
|
||||
'name': content.question,
|
||||
'acceptedAnswer': {
|
||||
'@type': 'Answer',
|
||||
'text': content.answer
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return jsonLd;
|
||||
}
|
||||
|
||||
// 生成所有已发布实体的JSON-LD列表
|
||||
static generateAllEntitiesJsonLd() {
|
||||
const entities = Entity.findAll();
|
||||
return entities.map(entity => this.generateEntityJsonLd(entity.entity_id));
|
||||
}
|
||||
|
||||
// 生成所有已发布内容的JSON-LD列表
|
||||
static generateAllContentsJsonLd(contentType = null) {
|
||||
let contents;
|
||||
if (contentType) {
|
||||
const { getDb } = require('../config/database');
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT * FROM contents WHERE publish_status = ? AND content_type = ?');
|
||||
contents = stmt.all('published', contentType);
|
||||
} else {
|
||||
const { getDb } = require('../config/database');
|
||||
const db = getDb();
|
||||
const stmt = db.prepare('SELECT * FROM contents WHERE publish_status = ?');
|
||||
contents = stmt.all('published');
|
||||
}
|
||||
|
||||
return contents.map(content => this.generateContentJsonLd(content.content_id));
|
||||
}
|
||||
|
||||
// 生成站点地图的JSON-LD
|
||||
static generateSitemapJsonLd() {
|
||||
const entities = this.generateAllEntitiesJsonLd();
|
||||
const contents = this.generateAllContentsJsonLd();
|
||||
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
'name': '阿美巴AI官网',
|
||||
'description': '阿美巴AI官网 - 知识底座与CMS内核',
|
||||
'hasPart': [
|
||||
...entities,
|
||||
...contents
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JsonLdService;
|
||||
@@ -0,0 +1,210 @@
|
||||
const Entity = require('../models/Entity');
|
||||
const DirectedRelationship = require('../models/DirectedRelationship');
|
||||
const Content = require('../models/Content');
|
||||
const EntityContentJoin = require('../models/EntityContentJoin');
|
||||
|
||||
class PublicApiService {
|
||||
// 获取已发布的实体
|
||||
static getPublishedEntities(options = {}) {
|
||||
return Entity.findAll({
|
||||
...options,
|
||||
where: {
|
||||
...options.where
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取已发布的实体详情
|
||||
static getPublishedEntityById(entityId) {
|
||||
return Entity.findById(entityId);
|
||||
}
|
||||
|
||||
// 获取已发布的关系
|
||||
static getPublishedRelationships(options = {}) {
|
||||
return DirectedRelationship.findAll({
|
||||
...options,
|
||||
where: {
|
||||
...options.where,
|
||||
rel_status: 'active',
|
||||
is_public: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取实体的已发布出向关系
|
||||
static getPublishedRelationshipsBySource(entityId) {
|
||||
return DirectedRelationship.findBySourceEntity(entityId, {
|
||||
where: {
|
||||
rel_status: 'active',
|
||||
is_public: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取实体的已发布入向关系
|
||||
static getPublishedRelationshipsByTarget(entityId) {
|
||||
return DirectedRelationship.findByTargetEntity(entityId, {
|
||||
where: {
|
||||
rel_status: 'active',
|
||||
is_public: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取已发布的内容
|
||||
static getPublishedContents(contentType, options = {}) {
|
||||
let query = 'SELECT * FROM contents WHERE publish_status = ?';
|
||||
const params = ['published'];
|
||||
|
||||
if (contentType) {
|
||||
query += ' AND content_type = ?';
|
||||
params.push(contentType);
|
||||
}
|
||||
|
||||
if (options.where) {
|
||||
const conditions = Object.keys(options.where).map(key => `${key} = ?`);
|
||||
query += ` AND ${conditions.join(' AND ')}`;
|
||||
params.push(...Object.values(options.where));
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const { getDb } = require('../config/database');
|
||||
const db = getDb();
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
// 获取已发布的内容详情
|
||||
static getPublishedContentById(contentId) {
|
||||
const content = Content.findById(contentId);
|
||||
if (content && content.publish_status === 'published') {
|
||||
return content;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取实体的内容关联(已发布的内容)
|
||||
static getEntityPublishedContents(entityId) {
|
||||
const joins = EntityContentJoin.findByEntityId(entityId);
|
||||
const contentIds = joins.map(join => join.content_id);
|
||||
|
||||
if (contentIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { getDb } = require('../config/database');
|
||||
const db = getDb();
|
||||
const placeholders = contentIds.map(() => '?').join(',');
|
||||
const stmt = db.prepare(`SELECT * FROM contents WHERE content_id IN (${placeholders}) AND publish_status = 'published'`);
|
||||
return stmt.all(...contentIds);
|
||||
}
|
||||
|
||||
// 获取内容的实体关联
|
||||
static getContentEntities(contentId) {
|
||||
const joins = EntityContentJoin.findByContentId(contentId);
|
||||
const entityIds = joins.map(join => join.entity_id);
|
||||
|
||||
if (entityIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { getDb } = require('../config/database');
|
||||
const db = getDb();
|
||||
const placeholders = entityIds.map(() => '?').join(',');
|
||||
const stmt = db.prepare(`SELECT * FROM entities WHERE entity_id IN (${placeholders})`);
|
||||
return stmt.all(...entityIds);
|
||||
}
|
||||
|
||||
// 搜索实体
|
||||
static searchEntities(keyword, options = {}) {
|
||||
const { getDb } = require('../config/database');
|
||||
const db = getDb();
|
||||
|
||||
let query = 'SELECT * FROM entities WHERE (name LIKE ? OR definition LIKE ? OR description LIKE ?)';
|
||||
const params = [`%${keyword}%`, `%${keyword}%`, `%${keyword}%`];
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索内容
|
||||
static searchContents(keyword, contentType = null, options = {}) {
|
||||
const { getDb } = require('../config/database');
|
||||
const db = getDb();
|
||||
|
||||
let query = 'SELECT * FROM contents WHERE publish_status = ? AND (title LIKE ? OR body LIKE ?)';
|
||||
const params = ['published', `%${keyword}%`, `%${keyword}%`];
|
||||
|
||||
if (contentType) {
|
||||
query += ' AND content_type = ?';
|
||||
params.push(contentType);
|
||||
}
|
||||
|
||||
if (options.orderBy) {
|
||||
query += ` ORDER BY ${options.orderBy}`;
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(options.limit);
|
||||
}
|
||||
|
||||
if (options.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(options.offset);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const rows = stmt.all(...params);
|
||||
|
||||
return rows.map(row => {
|
||||
if (row.metadata) {
|
||||
row.metadata = JSON.parse(row.metadata);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PublicApiService;
|
||||
Reference in New Issue
Block a user