Files
aiameiba_com_new/src/models/Page.js
T
ambandClaude Opus 5.5 ca00afb0fe 后台文章管理改版并修复两处安全问题
- 文章管理:按标题搜索、按状态和来源(内容系统/手动添加)筛选,显示发布时间和最后修改时间;一键上线/下线、查看前台页面、单篇删除和批量删除;右侧抽屉编辑标题、正文、分类、标签、作者、发布时间和状态;来自内容系统的文章提示再次发布会覆盖修改
- 其他内容类型的列表增加删除按钮
- 删除文章时同时删除它的发布记录,内容系统可以重新发布同一篇文章
- 上线时如果没有发布时间就用当前时间;后台表单的发布时间转换成 MySQL 格式
- 安全:/api/contents、/api/entities、/api/relationships、/api/joins 的新增、修改、删除需要登录管理后台(读取仍然公开,前台不受影响)
- 安全:列表接口的 orderBy 参数只接受字段名和 ASC/DESC,修复 SQL 注入

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:50:55 +08:00

175 lines
4.9 KiB
JavaScript

const { safeOrderBy } = require('../utils/sqlSafety');
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 (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 = Page;