chore: prepare project for server deployment
This commit is contained in:
@@ -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