- 文章管理:按标题搜索、按状态和来源(内容系统/手动添加)筛选,显示发布时间和最后修改时间;一键上线/下线、查看前台页面、单篇删除和批量删除;右侧抽屉编辑标题、正文、分类、标签、作者、发布时间和状态;来自内容系统的文章提示再次发布会覆盖修改 - 其他内容类型的列表增加删除按钮 - 删除文章时同时删除它的发布记录,内容系统可以重新发布同一篇文章 - 上线时如果没有发布时间就用当前时间;后台表单的发布时间转换成 MySQL 格式 - 安全:/api/contents、/api/entities、/api/relationships、/api/joins 的新增、修改、删除需要登录管理后台(读取仍然公开,前台不受影响) - 安全:列表接口的 orderBy 参数只接受字段名和 ASC/DESC,修复 SQL 注入 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
4.0 KiB
JavaScript
146 lines
4.0 KiB
JavaScript
const { safeOrderBy } = require('../utils/sqlSafety');
|
|
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 (safeOrderBy(options.orderBy)) {
|
|
query += ` ORDER BY ${safeOrderBy(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; |