继续实现几个子模块后台基础接口

This commit is contained in:
2025-11-03 18:23:54 +08:00
parent ac11b71d2d
commit 7a0eeaefa2
9 changed files with 1031 additions and 0 deletions

45
sql/categories.sql Normal file
View File

@@ -0,0 +1,45 @@
-- 栏目管理表Categories
-- 字段栏目名称categoryName、父级栏目parentId、顺序号orderNo、所属站点siteId、
-- 栏目标题title、栏目关键字keywords、形象图image、栏目描述description、转向链接linkUrl、
-- 栏目编号categoryCode、栏目页面编号pageId关联 pages.id、是否栏目isCategory、
-- 是否支持文章录入supportsArticles、是否发布isPublished
DROP TABLE IF EXISTS `categories`;
CREATE TABLE `categories` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`categoryName` varchar(255) NOT NULL COMMENT '栏目名称',
`parentId` int(11) NOT NULL DEFAULT 0 COMMENT '父级栏目ID0为顶级',
`orderNo` int(11) NOT NULL DEFAULT 0 COMMENT '顺序号',
`siteId` int(11) NOT NULL COMMENT '所属站点ID',
`title` varchar(255) DEFAULT NULL COMMENT '栏目标题',
`keywords` varchar(255) DEFAULT NULL COMMENT '栏目关键字',
`image` varchar(1024) DEFAULT NULL COMMENT '形象图',
`description` text DEFAULT NULL COMMENT '栏目描述',
`linkUrl` varchar(1024) DEFAULT NULL COMMENT '转向链接',
`categoryCode` varchar(100) NOT NULL COMMENT '栏目编号',
`pageId` int(11) DEFAULT NULL COMMENT '栏目页面ID关联 pages.id',
`isCategory` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否栏目0-否1-是)',
`supportsArticles` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否支持文章录入0-不支持1-支持)',
`isPublished` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否发布0-未发布1-已发布)',
`isDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除(软删除)',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_categoryCode` (`categoryCode`),
KEY `idx_siteId` (`siteId`),
KEY `idx_parentId` (`parentId`),
KEY `idx_orderNo` (`orderNo`),
KEY `idx_pageId` (`pageId`),
KEY `idx_isPublished` (`isPublished`),
KEY `idx_isDeleted` (`isDeleted`),
KEY `idx_updateTime` (`updateTime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='栏目管理表';
-- 示例数据
INSERT INTO `categories` (
`categoryName`, `parentId`, `orderNo`, `siteId`, `title`, `keywords`, `image`, `description`,
`linkUrl`, `categoryCode`, `pageId`, `isCategory`, `supportsArticles`, `isPublished`
) VALUES
('新闻中心', 0, 1, 1, '新闻中心', '新闻,动态', '/assets/pc/cat_news.png', '站点新闻栏目',
NULL, 'news', 1, 1, 1, 1),
('通知公告', 1, 2, 1, '通知公告', '通知,公告', '/assets/pc/cat_notice.png', '站点公告栏目',
NULL, 'notice', 1, 1, 1, 0);

33
sql/category_medias.sql Normal file
View File

@@ -0,0 +1,33 @@
-- 栏目图文表Category Medias
-- 字段主标题mainTitle、子标题subTitle、顺序号orderNo、内容描述description、
-- 大文件路径PClargeFilePath、小文件路径移动smallFilePath、链接地址linkUrl、
-- 是否发布isPublished、所属页面pageId关联 pages.id
DROP TABLE IF EXISTS `categoryMedias`;
CREATE TABLE `categoryMedias` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`mainTitle` varchar(255) NOT NULL COMMENT '主标题',
`subTitle` varchar(255) DEFAULT NULL COMMENT '子标题',
`orderNo` int(11) NOT NULL DEFAULT 0 COMMENT '顺序号',
`description` text DEFAULT NULL COMMENT '内容描述',
`largeFilePath` varchar(1024) DEFAULT NULL COMMENT '大文件路径PC',
`smallFilePath` varchar(1024) DEFAULT NULL COMMENT '小文件路径(移动)',
`linkUrl` varchar(1024) DEFAULT NULL COMMENT '链接地址',
`isPublished` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否发布0-未发布1-已发布)',
`pageId` int(11) NOT NULL COMMENT '所属页面ID关联 pages.id',
`isDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除(软删除)',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_pageId` (`pageId`),
KEY `idx_orderNo` (`orderNo`),
KEY `idx_isPublished` (`isPublished`),
KEY `idx_isDeleted` (`isDeleted`),
KEY `idx_updateTime` (`updateTime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='栏目图文表';
-- 示例数据
INSERT INTO `categoryMedias` (
`mainTitle`, `subTitle`, `orderNo`, `description`, `largeFilePath`, `smallFilePath`, `linkUrl`, `isPublished`, `pageId`
) VALUES
('新闻栏目横幅', '今日要闻', 1, '新闻栏目顶部横幅图文', '/assets/pc/cat_news_banner.jpg', '/assets/mobile/cat_news_banner.jpg', 'https://example.com/news', 1, 1),
('公告栏目卡片', '最新公告', 2, '公告栏目图文卡片', '/assets/pc/cat_notice_card.jpg', '/assets/mobile/cat_notice_card.jpg', 'https://example.com/notice', 0, 1);

36
sql/domain_dirs.sql Normal file
View File

@@ -0,0 +1,36 @@
-- 域名/目录表Domain/Directory Routes
-- 字段routeName域名/目录名称、dynamicPath动态路径、isPrimary是否主域名/目录)、
-- isStatic是否静态化、verificationMeta站点验证META、icp备案信息
-- staticPath静态路径、lastStaticTime最新静态化时间、categoryId所属栏目、siteId所属站点
DROP TABLE IF EXISTS `domainDirs`;
CREATE TABLE `domainDirs` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`routeName` varchar(255) NOT NULL COMMENT '域名/目录名称',
`dynamicPath` varchar(1024) DEFAULT NULL COMMENT '动态路径',
`isPrimary` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否主域名/目录0-否1-是)',
`isStatic` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否静态化0-否1-是)',
`verificationMeta` text DEFAULT NULL COMMENT '站点验证META',
`icp` varchar(255) DEFAULT NULL COMMENT '备案信息',
`staticPath` varchar(1024) DEFAULT NULL COMMENT '静态路径',
`lastStaticTime` datetime DEFAULT NULL COMMENT '最新静态化时间',
`categoryId` int(11) DEFAULT NULL COMMENT '所属栏目ID关联 categories.id',
`siteId` int(11) NOT NULL COMMENT '所属站点ID关联 sites.id',
`isDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除(软删除)',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_siteId` (`siteId`),
KEY `idx_categoryId` (`categoryId`),
KEY `idx_isPrimary` (`isPrimary`),
KEY `idx_isStatic` (`isStatic`),
KEY `idx_isDeleted` (`isDeleted`),
KEY `idx_updateTime` (`updateTime`),
KEY `idx_routeName` (`routeName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='域名/目录管理表';
-- 示例数据
INSERT INTO `domainDirs` (
`routeName`, `dynamicPath`, `isPrimary`, `isStatic`, `verificationMeta`, `icp`, `staticPath`, `lastStaticTime`, `categoryId`, `siteId`
) VALUES
('www.example.com', '/home', 1, 1, '<meta name="google-site-verification" content="abc"/>', '沪ICP证00000001号', '/static/home', NOW(), 1, 1),
('/about', '/about/index', 0, 0, NULL, '沪ICP备00000002号', NULL, NULL, 1, 1);

23
sql/sensitive_words.sql Normal file
View File

@@ -0,0 +1,23 @@
-- 敏感词表定义:词条名、类型、描述、启用/禁用、软删与更新时间
DROP TABLE IF EXISTS `sensitiveWords`;
CREATE TABLE `sensitiveWords` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`wordName` VARCHAR(255) NOT NULL COMMENT '词条名',
`type` VARCHAR(100) DEFAULT NULL COMMENT '类型(如:政治、色情、广告、谩骂等)',
`description` TEXT DEFAULT NULL COMMENT '描述',
`status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用1启用 0禁用',
`isDeleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除0正常 1已删除',
`updateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_type` (`type`),
KEY `idx_status` (`status`),
KEY `idx_isDeleted` (`isDeleted`),
KEY `idx_updateTime` (`updateTime`),
KEY `idx_wordName` (`wordName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='敏感词';
-- 示例数据
INSERT INTO `sensitiveWords` (`wordName`, `type`, `description`, `status`, `isDeleted`)
VALUES
('非法词条示例A', '政治', '示例:政治类敏感词', 1, 0),
('非法词条示例B', '广告', '示例:广告类敏感词', 0, 0);

View File

@@ -14,6 +14,10 @@ const linksRouter = require('./routes/links');
const reportsRouter = require('./routes/reports');
const pagesRouter = require('./routes/pages');
const pageMediasRouter = require('./routes/pageMedias');
const categoriesRouter = require('./routes/categories');
const categoryMediasRouter = require('./routes/categoryMedias');
const domainDirsRouter = require('./routes/domainDirs');
const sensitiveWordsRouter = require('./routes/sensitiveWords');
// 获取环境变量
dotenv.config();
@@ -80,6 +84,14 @@ app.use('/api', reportsRouter);
app.use('/api', pagesRouter);
// 页面图文相关接口
app.use('/api', pageMediasRouter);
// 栏目相关接口
app.use('/api', categoriesRouter);
// 栏目图文相关接口
app.use('/api', categoryMediasRouter);
// 域名/目录相关接口
app.use('/api', domainDirsRouter);
// 敏感词相关接口
app.use('/api', sensitiveWordsRouter);
app.listen(port, () => {
console.log(`API server listening on http://localhost:${port}`);

236
src/routes/categories.js Normal file
View File

@@ -0,0 +1,236 @@
const express = require('express');
const { pool } = require('../db');
const router = express.Router();
// Helpers
function toNullableString(val) {
if (val == null) return null;
const s = String(val).trim();
return s === '' ? null : s;
}
function toInt(val, def = 0) {
if (val == null || val === '') return def;
const n = parseInt(val, 10);
return Number.isNaN(n) ? def : n;
}
function toBool01(val, def = 0) {
if (val === true) return 1;
if (val === false) return 0;
if (val == null || val === '') return def;
const s = String(val).trim().toLowerCase();
if (['1', 'true', 'yes', 'y'].includes(s)) return 1;
if (['0', 'false', 'no', 'n'].includes(s)) return 0;
return def;
}
function normalizeCategory(body) {
return {
categoryName: toNullableString(body.categoryName) || '',
parentId: toInt(body.parentId, 0),
orderNo: toInt(body.orderNo, 0),
siteId: toInt(body.siteId, 0),
title: toNullableString(body.title),
keywords: toNullableString(body.keywords),
image: toNullableString(body.image),
description: toNullableString(body.description),
linkUrl: toNullableString(body.linkUrl),
categoryCode: toNullableString(body.categoryCode) || '',
pageId: toInt(body.pageId, null),
isCategory: toBool01(body.isCategory, 1),
supportsArticles: toBool01(body.supportsArticles, 1),
isPublished: toBool01(body.isPublished, 0),
};
}
function shapeCategory(row) {
return {
id: row.id,
categoryName: row.categoryName || '',
parentId: row.parentId ?? 0,
orderNo: row.orderNo ?? 0,
siteId: row.siteId ?? 0,
title: row.title || '',
keywords: row.keywords || '',
image: row.image || '',
description: row.description || '',
linkUrl: row.linkUrl || '',
categoryCode: row.categoryCode || '',
pageId: row.pageId ?? null,
isCategory: row.isCategory === 1,
supportsArticles: row.supportsArticles === 1,
isPublished: row.isPublished === 1,
};
}
// Publish - pure array body
router.post('/categories/publish', async (req, res) => {
const ids = Array.isArray(req.body) ? req.body.map(x => toInt(x, 0)).filter(x => x > 0) : [];
if (ids.length === 0) return res.status(400).json({ success: false, message: '请提供ID数组' });
const placeholders = ids.map(() => '?').join(',');
try {
const [result] = await pool.execute(`UPDATE categories SET isPublished=1, updateTime=NOW() WHERE id IN (${placeholders}) AND isDeleted=0`, ids);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('发布栏目失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Unpublish - pure array body
router.post('/categories/unpublish', async (req, res) => {
const ids = Array.isArray(req.body) ? req.body.map(x => toInt(x, 0)).filter(x => x > 0) : [];
if (ids.length === 0) return res.status(400).json({ success: false, message: '请提供ID数组' });
const placeholders = ids.map(() => '?').join(',');
try {
const [result] = await pool.execute(`UPDATE categories SET isPublished=0, updateTime=NOW() WHERE id IN (${placeholders}) AND isDeleted=0`, ids);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('取消发布栏目失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Create or Update
router.post('/categories/:id?', async (req, res) => {
const id = toInt(req.params.id, 0);
const data = normalizeCategory(req.body || {});
try {
if (id > 0) {
const [result] = await pool.execute(
`UPDATE categories SET categoryName=?, parentId=?, orderNo=?, siteId=?, title=?, keywords=?, image=?, description=?, linkUrl=?, categoryCode=?, pageId=?, isCategory=?, supportsArticles=?, isPublished=?, updateTime=NOW() WHERE id=? AND isDeleted=0`,
[data.categoryName, data.parentId, data.orderNo, data.siteId, data.title, data.keywords, data.image, data.description, data.linkUrl, data.categoryCode, data.pageId, data.isCategory, data.supportsArticles, data.isPublished, id]
);
return res.json({ success: true, affectedRows: result.affectedRows, id });
}
const [insert] = await pool.execute(
`INSERT INTO categories (categoryName, parentId, orderNo, siteId, title, keywords, image, description, linkUrl, categoryCode, pageId, isCategory, supportsArticles, isPublished) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[data.categoryName, data.parentId, data.orderNo, data.siteId, data.title, data.keywords, data.image, data.description, data.linkUrl, data.categoryCode, data.pageId, data.isCategory, data.supportsArticles, data.isPublished]
);
return res.json({ success: true, id: insert.insertId });
} catch (err) {
console.error('保存栏目失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Get one
router.get('/categories/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [rows] = await pool.execute(`SELECT * FROM categories WHERE id = ? AND isDeleted = 0`, [id]);
if (!rows || rows.length === 0) return res.status(404).json({ success: false, message: 'Not found' });
return res.json({ success: true, data: shapeCategory(rows[0]) });
} catch (err) {
console.error('获取栏目失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// List with filters/pagination/sorting
router.get('/categories', async (req, res) => {
const { page = 1, pageSize = 10, categoryName, siteId, parentId, isPublished, isCategory, supportsArticles, categoryCode, pageId, isDeleted = 0, sortBy, sortOrder } = req.query;
const p = toInt(page, 1);
const ps = Math.min(100, Math.max(1, toInt(pageSize, 10)));
const offset = (p - 1) * ps;
const filters = [];
const params = [];
if (categoryName) {
filters.push('categoryName LIKE ?');
params.push(`%${categoryName}%`);
}
if (siteId !== undefined && siteId !== '') {
filters.push('siteId = ?');
params.push(toInt(siteId, 0));
}
if (parentId !== undefined && parentId !== '') {
filters.push('parentId = ?');
params.push(toInt(parentId, 0));
}
if (isPublished !== undefined && isPublished !== '') {
filters.push('isPublished = ?');
params.push(toBool01(isPublished, 0));
}
if (isCategory !== undefined && isCategory !== '') {
filters.push('isCategory = ?');
params.push(toBool01(isCategory, 1));
}
if (supportsArticles !== undefined && supportsArticles !== '') {
filters.push('supportsArticles = ?');
params.push(toBool01(supportsArticles, 1));
}
if (categoryCode) {
filters.push('categoryCode LIKE ?');
params.push(`%${categoryCode}%`);
}
if (pageId !== undefined && pageId !== '') {
filters.push('pageId = ?');
params.push(toInt(pageId, 0));
}
if (isDeleted !== undefined && isDeleted !== '') {
filters.push('isDeleted = ?');
params.push(toBool01(isDeleted, 0));
} else {
filters.push('isDeleted = 0');
}
const where = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
const allowedSort = new Set(['updateTime', 'id', 'orderNo', 'siteId', 'parentId', 'isPublished', 'isCategory', 'supportsArticles']);
const orderField = allowedSort.has(String(sortBy)) ? String(sortBy) : 'updateTime';
const orderDir = ['asc', 'desc'].includes(String(sortOrder)?.toLowerCase()) ? String(sortOrder).toUpperCase() : 'DESC';
try {
const [countRows] = await pool.execute(`SELECT COUNT(*) AS cnt FROM categories ${where}`, params);
const total = countRows[0]?.cnt || 0;
const [rows] = await pool.execute(
`SELECT * FROM categories ${where} ORDER BY ${orderField} ${orderDir}, id DESC LIMIT ? OFFSET ?`,
[...params, ps, offset]
);
const data = rows.map(r => shapeCategory(r));
return res.json({ success: true, page: p, pageSize: ps, total, data });
} catch (err) {
console.error('列表查询栏目失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Soft delete
router.delete('/categories/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [result] = await pool.execute(`UPDATE categories SET isDeleted=1, updateTime=NOW() WHERE id=? AND isDeleted=0`, [id]);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('软删除栏目失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Recover
router.post('/categories/recover/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [result] = await pool.execute(`UPDATE categories SET isDeleted=0, updateTime=NOW() WHERE id=? AND isDeleted=1`, [id]);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('恢复栏目失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Physical delete
router.delete('/categories/force/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [result] = await pool.execute(`DELETE FROM categories WHERE id=?`, [id]);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('物理删除栏目失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
module.exports = router;

View File

@@ -0,0 +1,206 @@
const express = require('express');
const { pool } = require('../db');
const router = express.Router();
// Helpers
function toNullableString(val) {
if (val == null) return null;
const s = String(val).trim();
return s === '' ? null : s;
}
function toInt(val, def = 0) {
if (val == null || val === '') return def;
const n = parseInt(val, 10);
return Number.isNaN(n) ? def : n;
}
function toBool01(val, def = 0) {
if (val === true) return 1;
if (val === false) return 0;
if (val == null || val === '') return def;
const s = String(val).trim().toLowerCase();
if (['1', 'true', 'yes', 'y'].includes(s)) return 1;
if (['0', 'false', 'no', 'n'].includes(s)) return 0;
return def;
}
function normalizeMedia(body) {
return {
mainTitle: toNullableString(body.mainTitle) || '',
subTitle: toNullableString(body.subTitle),
orderNo: toInt(body.orderNo, 0),
description: toNullableString(body.description),
largeFilePath: toNullableString(body.largeFilePath),
smallFilePath: toNullableString(body.smallFilePath),
linkUrl: toNullableString(body.linkUrl),
isPublished: toBool01(body.isPublished, 0),
pageId: toInt(body.pageId, 0),
};
}
function shapeMedia(row) {
return {
id: row.id,
mainTitle: row.mainTitle || '',
subTitle: row.subTitle || '',
orderNo: row.orderNo ?? 0,
description: row.description || '',
largeFilePath: row.largeFilePath || '',
smallFilePath: row.smallFilePath || '',
linkUrl: row.linkUrl || '',
isPublished: row.isPublished === 1,
pageId: row.pageId ?? 0,
};
}
// Publish - pure array body
router.post('/categoryMedias/publish', async (req, res) => {
const ids = Array.isArray(req.body) ? req.body.map(x => toInt(x, 0)).filter(x => x > 0) : [];
if (ids.length === 0) return res.status(400).json({ success: false, message: '请提供ID数组' });
const placeholders = ids.map(() => '?').join(',');
try {
const [result] = await pool.execute(`UPDATE categoryMedias SET isPublished=1, updateTime=NOW() WHERE id IN (${placeholders}) AND isDeleted=0`, ids);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('发布栏目图文失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Unpublish - pure array body
router.post('/categoryMedias/unpublish', async (req, res) => {
const ids = Array.isArray(req.body) ? req.body.map(x => toInt(x, 0)).filter(x => x > 0) : [];
if (ids.length === 0) return res.status(400).json({ success: false, message: '请提供ID数组' });
const placeholders = ids.map(() => '?').join(',');
try {
const [result] = await pool.execute(`UPDATE categoryMedias SET isPublished=0, updateTime=NOW() WHERE id IN (${placeholders}) AND isDeleted=0`, ids);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('取消发布栏目图文失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Create or Update
router.post('/categoryMedias/:id?', async (req, res) => {
const id = toInt(req.params.id, 0);
const data = normalizeMedia(req.body || {});
try {
if (id > 0) {
const [result] = await pool.execute(
`UPDATE categoryMedias SET mainTitle=?, subTitle=?, orderNo=?, description=?, largeFilePath=?, smallFilePath=?, linkUrl=?, isPublished=?, pageId=?, updateTime=NOW() WHERE id=? AND isDeleted=0`,
[data.mainTitle, data.subTitle, data.orderNo, data.description, data.largeFilePath, data.smallFilePath, data.linkUrl, data.isPublished, data.pageId, id]
);
return res.json({ success: true, affectedRows: result.affectedRows, id });
}
const [insert] = await pool.execute(
`INSERT INTO categoryMedias (mainTitle, subTitle, orderNo, description, largeFilePath, smallFilePath, linkUrl, isPublished, pageId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[data.mainTitle, data.subTitle, data.orderNo, data.description, data.largeFilePath, data.smallFilePath, data.linkUrl, data.isPublished, data.pageId]
);
return res.json({ success: true, id: insert.insertId });
} catch (err) {
console.error('保存栏目图文失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Get one
router.get('/categoryMedias/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [rows] = await pool.execute(`SELECT * FROM categoryMedias WHERE id = ? AND isDeleted = 0`, [id]);
if (!rows || rows.length === 0) return res.status(404).json({ success: false, message: 'Not found' });
return res.json({ success: true, data: shapeMedia(rows[0]) });
} catch (err) {
console.error('获取栏目图文失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// List with filters/pagination/sorting
router.get('/categoryMedias', async (req, res) => {
const { page = 1, pageSize = 10, pageId, mainTitle, isPublished, isDeleted = 0, sortBy, sortOrder } = req.query;
const p = toInt(page, 1);
const ps = Math.min(100, Math.max(1, toInt(pageSize, 10)));
const offset = (p - 1) * ps;
const filters = [];
const params = [];
if (pageId !== undefined && pageId !== '') {
filters.push('pageId = ?');
params.push(toInt(pageId, 0));
}
if (mainTitle) {
filters.push('mainTitle LIKE ?');
params.push(`%${mainTitle}%`);
}
if (isPublished !== undefined && isPublished !== '') {
filters.push('isPublished = ?');
params.push(toBool01(isPublished, 0));
}
if (isDeleted !== undefined && isDeleted !== '') {
filters.push('isDeleted = ?');
params.push(toBool01(isDeleted, 0));
} else {
filters.push('isDeleted = 0');
}
const where = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
const allowedSort = new Set(['updateTime', 'id', 'orderNo', 'pageId', 'isPublished', 'mainTitle']);
const orderField = allowedSort.has(String(sortBy)) ? String(sortBy) : 'updateTime';
const orderDir = ['asc', 'desc'].includes(String(sortOrder)?.toLowerCase()) ? String(sortOrder).toUpperCase() : 'DESC';
try {
const [countRows] = await pool.execute(`SELECT COUNT(*) AS cnt FROM categoryMedias ${where}`, params);
const total = countRows[0]?.cnt || 0;
const [rows] = await pool.execute(
`SELECT * FROM categoryMedias ${where} ORDER BY ${orderField} ${orderDir}, id DESC LIMIT ? OFFSET ?`,
[...params, ps, offset]
);
const data = rows.map(r => shapeMedia(r));
return res.json({ success: true, page: p, pageSize: ps, total, data });
} catch (err) {
console.error('列表查询栏目图文失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Soft delete
router.delete('/categoryMedias/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [result] = await pool.execute(`UPDATE categoryMedias SET isDeleted=1, updateTime=NOW() WHERE id=? AND isDeleted=0`, [id]);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('软删除栏目图文失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Recover
router.post('/categoryMedias/recover/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [result] = await pool.execute(`UPDATE categoryMedias SET isDeleted=0, updateTime=NOW() WHERE id=? AND isDeleted=1`, [id]);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('恢复栏目图文失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Physical delete
router.delete('/categoryMedias/force/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [result] = await pool.execute(`DELETE FROM categoryMedias WHERE id=?`, [id]);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('物理删除栏目图文失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
module.exports = router;

248
src/routes/domainDirs.js Normal file
View File

@@ -0,0 +1,248 @@
const express = require('express');
const { pool } = require('../db');
const router = express.Router();
// Helpers
function toNullableString(val) {
if (val == null) return null;
const s = String(val).trim();
return s === '' ? null : s;
}
function toInt(val, def = 0) {
if (val == null || val === '') return def;
const n = parseInt(val, 10);
return Number.isNaN(n) ? def : n;
}
function toBool01(val, def = 0) {
if (val === true) return 1;
if (val === false) return 0;
if (val == null || val === '') return def;
const s = String(val).trim().toLowerCase();
if (['1', 'true', 'yes', 'y'].includes(s)) return 1;
if (['0', 'false', 'no', 'n'].includes(s)) return 0;
return def;
}
function normalizeRoute(body) {
return {
routeName: toNullableString(body.routeName) || '',
dynamicPath: toNullableString(body.dynamicPath),
isPrimary: toBool01(body.isPrimary, 0),
isStatic: toBool01(body.isStatic, 0),
verificationMeta: toNullableString(body.verificationMeta),
icp: toNullableString(body.icp),
staticPath: toNullableString(body.staticPath),
lastStaticTime: toNullableString(body.lastStaticTime),
categoryId: body.categoryId === null || body.categoryId === undefined || body.categoryId === '' ? null : toInt(body.categoryId, null),
siteId: toInt(body.siteId, 0),
};
}
function shapeRoute(row) {
return {
id: row.id,
routeName: row.routeName || '',
dynamicPath: row.dynamicPath || '',
isPrimary: row.isPrimary === 1,
isStatic: row.isStatic === 1,
verificationMeta: row.verificationMeta || '',
icp: row.icp || '',
staticPath: row.staticPath || '',
lastStaticTime: row.lastStaticTime ? new Date(row.lastStaticTime).toISOString() : null,
categoryId: row.categoryId ?? null,
siteId: row.siteId ?? 0,
};
}
// Set primary - pure array body
router.post('/domainDirs/setPrimary', async (req, res) => {
const ids = Array.isArray(req.body) ? req.body.map(x => toInt(x, 0)).filter(x => x > 0) : [];
if (ids.length === 0) return res.status(400).json({ success: false, message: '请提供ID数组' });
const placeholders = ids.map(() => '?').join(',');
try {
const [result] = await pool.execute(`UPDATE domainDirs SET isPrimary=1, updateTime=NOW() WHERE id IN (${placeholders}) AND isDeleted=0`, ids);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('设置主域名/目录失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Unset primary - pure array body
router.post('/domainDirs/unsetPrimary', async (req, res) => {
const ids = Array.isArray(req.body) ? req.body.map(x => toInt(x, 0)).filter(x => x > 0) : [];
if (ids.length === 0) return res.status(400).json({ success: false, message: '请提供ID数组' });
const placeholders = ids.map(() => '?').join(',');
try {
const [result] = await pool.execute(`UPDATE domainDirs SET isPrimary=0, updateTime=NOW() WHERE id IN (${placeholders}) AND isDeleted=0`, ids);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('取消主域名/目录失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Staticize - pure array body (set lastStaticTime=NOW())
router.post('/domainDirs/staticize', async (req, res) => {
const ids = Array.isArray(req.body) ? req.body.map(x => toInt(x, 0)).filter(x => x > 0) : [];
if (ids.length === 0) return res.status(400).json({ success: false, message: '请提供ID数组' });
const placeholders = ids.map(() => '?').join(',');
try {
const [result] = await pool.execute(`UPDATE domainDirs SET isStatic=1, lastStaticTime=NOW(), updateTime=NOW() WHERE id IN (${placeholders}) AND isDeleted=0`, ids);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('静态化设置失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Unstaticize - pure array body
router.post('/domainDirs/unstaticize', async (req, res) => {
const ids = Array.isArray(req.body) ? req.body.map(x => toInt(x, 0)).filter(x => x > 0) : [];
if (ids.length === 0) return res.status(400).json({ success: false, message: '请提供ID数组' });
const placeholders = ids.map(() => '?').join(',');
try {
const [result] = await pool.execute(`UPDATE domainDirs SET isStatic=0, updateTime=NOW() WHERE id IN (${placeholders}) AND isDeleted=0`, ids);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('取消静态化失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Create or Update
router.post('/domainDirs/:id?', async (req, res) => {
const id = toInt(req.params.id, 0);
const data = normalizeRoute(req.body || {});
try {
if (id > 0) {
const [result] = await pool.execute(
`UPDATE domainDirs SET routeName=?, dynamicPath=?, isPrimary=?, isStatic=?, verificationMeta=?, icp=?, staticPath=?, lastStaticTime=?, categoryId=?, siteId=?, updateTime=NOW() WHERE id=? AND isDeleted=0`,
[data.routeName, data.dynamicPath, data.isPrimary, data.isStatic, data.verificationMeta, data.icp, data.staticPath, data.lastStaticTime, data.categoryId, data.siteId, id]
);
return res.json({ success: true, affectedRows: result.affectedRows, id });
}
const [insert] = await pool.execute(
`INSERT INTO domainDirs (routeName, dynamicPath, isPrimary, isStatic, verificationMeta, icp, staticPath, lastStaticTime, categoryId, siteId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[data.routeName, data.dynamicPath, data.isPrimary, data.isStatic, data.verificationMeta, data.icp, data.staticPath, data.lastStaticTime, data.categoryId, data.siteId]
);
return res.json({ success: true, id: insert.insertId });
} catch (err) {
console.error('保存域名/目录失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Get one
router.get('/domainDirs/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [rows] = await pool.execute(`SELECT * FROM domainDirs WHERE id = ? AND isDeleted = 0`, [id]);
if (!rows || rows.length === 0) return res.status(404).json({ success: false, message: 'Not found' });
return res.json({ success: true, data: shapeRoute(rows[0]) });
} catch (err) {
console.error('获取域名/目录失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// List with filters/pagination/sorting
router.get('/domainDirs', async (req, res) => {
const { page = 1, pageSize = 10, routeName, dynamicPath, isPrimary, isStatic, siteId, categoryId, isDeleted = 0, sortBy, sortOrder } = req.query;
const p = toInt(page, 1);
const ps = Math.min(100, Math.max(1, toInt(pageSize, 10)));
const offset = (p - 1) * ps;
const filters = [];
const params = [];
if (routeName) {
filters.push('routeName LIKE ?');
params.push(`%${routeName}%`);
}
if (dynamicPath) {
filters.push('dynamicPath LIKE ?');
params.push(`%${dynamicPath}%`);
}
if (isPrimary !== undefined && isPrimary !== '') {
filters.push('isPrimary = ?');
params.push(toBool01(isPrimary, 0));
}
if (isStatic !== undefined && isStatic !== '') {
filters.push('isStatic = ?');
params.push(toBool01(isStatic, 0));
}
if (siteId !== undefined && siteId !== '') {
filters.push('siteId = ?');
params.push(toInt(siteId, 0));
}
if (categoryId !== undefined && categoryId !== '') {
filters.push('categoryId = ?');
params.push(toInt(categoryId, 0));
}
if (isDeleted !== undefined && isDeleted !== '') {
filters.push('isDeleted = ?');
params.push(toBool01(isDeleted, 0));
} else {
filters.push('isDeleted = 0');
}
const where = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
const allowedSort = new Set(['updateTime', 'id', 'siteId', 'categoryId', 'isPrimary', 'isStatic']);
const orderField = allowedSort.has(String(sortBy)) ? String(sortBy) : 'updateTime';
const orderDir = ['asc', 'desc'].includes(String(sortOrder)?.toLowerCase()) ? String(sortOrder).toUpperCase() : 'DESC';
try {
const [countRows] = await pool.execute(`SELECT COUNT(*) AS cnt FROM domainDirs ${where}`, params);
const total = countRows[0]?.cnt || 0;
const [rows] = await pool.execute(
`SELECT * FROM domainDirs ${where} ORDER BY ${orderField} ${orderDir}, id DESC LIMIT ? OFFSET ?`,
[...params, ps, offset]
);
const data = rows.map(r => shapeRoute(r));
return res.json({ success: true, page: p, pageSize: ps, total, data });
} catch (err) {
console.error('列表查询域名/目录失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Soft delete
router.delete('/domainDirs/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [result] = await pool.execute(`UPDATE domainDirs SET isDeleted=1, updateTime=NOW() WHERE id=? AND isDeleted=0`, [id]);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('软删除域名/目录失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Recover
router.post('/domainDirs/recover/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [result] = await pool.execute(`UPDATE domainDirs SET isDeleted=0, updateTime=NOW() WHERE id=? AND isDeleted=1`, [id]);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('恢复域名/目录失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
// Physical delete
router.delete('/domainDirs/force/:id', async (req, res) => {
const id = toInt(req.params.id, 0);
try {
const [result] = await pool.execute(`DELETE FROM domainDirs WHERE id=?`, [id]);
return res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
console.error('物理删除域名/目录失败:', err);
return res.status(500).json({ success: false, message: 'Server error', error: String(err) });
}
});
module.exports = router;

View File

@@ -0,0 +1,192 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
// 布尔/数字转换与安全工具
const toIntOrNull = (v) => {
if (v === undefined || v === null || v === '') return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
const toBoolOrNull = (v) => {
if (v === undefined || v === null || v === '') return null;
if (typeof v === 'boolean') return v;
if (v === 'true' || v === '1' || v === 1) return true;
if (v === 'false' || v === '0' || v === 0) return false;
return null;
};
const mapRow = (row) => ({
id: row.id,
wordName: row.wordName,
type: row.type,
description: row.description,
status: row.status === 1,
updateTime: row.updateTime ? new Date(row.updateTime).toISOString() : null,
});
// 批量启用(纯数组)
router.post('/sensitiveWords/enable', async (req, res) => {
try {
const ids = Array.isArray(req.body) ? req.body : null;
if (!ids || ids.length === 0 || !ids.every((x) => Number.isFinite(Number(x)))) {
return res.status(400).json({ success: false, message: '请求体需为纯数组ID且不可为空' });
}
const sql = `UPDATE sensitiveWords SET status = 1, updateTime = NOW() WHERE id IN (${ids.map(() => '?').join(',')})`;
const [result] = await db.query(sql, ids);
res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 批量禁用(纯数组)
router.post('/sensitiveWords/disable', async (req, res) => {
try {
const ids = Array.isArray(req.body) ? req.body : null;
if (!ids || ids.length === 0 || !ids.every((x) => Number.isFinite(Number(x)))) {
return res.status(400).json({ success: false, message: '请求体需为纯数组ID且不可为空' });
}
const sql = `UPDATE sensitiveWords SET status = 0, updateTime = NOW() WHERE id IN (${ids.map(() => '?').join(',')})`;
const [result] = await db.query(sql, ids);
res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 创建
router.post('/sensitiveWords', async (req, res) => {
try {
const { wordName, type, description, status } = req.body;
if (!wordName || typeof wordName !== 'string') {
return res.status(400).json({ success: false, message: 'wordName为必填字符串' });
}
const statusVal = toBoolOrNull(status);
const sql = `INSERT INTO sensitiveWords (wordName, type, description, status, updateTime)
VALUES (?, ?, ?, ?, NOW())`;
const params = [wordName.trim(), type || null, description || null, statusVal === null ? 1 : (statusVal ? 1 : 0)];
const [result] = await db.query(sql, params);
const [rows] = await db.query('SELECT * FROM sensitiveWords WHERE id = ?', [result.insertId]);
res.json({ success: true, data: mapRow(rows[0]) });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 修改
router.post('/sensitiveWords/:id', async (req, res) => {
try {
const id = toIntOrNull(req.params.id);
if (!id) return res.status(400).json({ success: false, message: 'id不合法' });
const { wordName, type, description, status } = req.body;
const fields = [];
const params = [];
if (wordName !== undefined) { fields.push('wordName = ?'); params.push(wordName || null); }
if (type !== undefined) { fields.push('type = ?'); params.push(type || null); }
if (description !== undefined) { fields.push('description = ?'); params.push(description || null); }
if (status !== undefined) {
const statusVal = toBoolOrNull(status);
if (statusVal === null) return res.status(400).json({ success: false, message: 'status需为布尔' });
fields.push('status = ?'); params.push(statusVal ? 1 : 0);
}
if (fields.length === 0) return res.status(400).json({ success: false, message: '无可更新字段' });
const sql = `UPDATE sensitiveWords SET ${fields.join(', ')}, updateTime = NOW() WHERE id = ?`;
params.push(id);
await db.query(sql, params);
const [rows] = await db.query('SELECT * FROM sensitiveWords WHERE id = ?', [id]);
if (!rows || rows.length === 0) return res.status(404).json({ success: false, message: '记录不存在' });
res.json({ success: true, data: mapRow(rows[0]) });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 详情
router.get('/sensitiveWords/:id', async (req, res) => {
try {
const id = toIntOrNull(req.params.id);
if (!id) return res.status(400).json({ success: false, message: 'id不合法' });
const [rows] = await db.query('SELECT * FROM sensitiveWords WHERE id = ? AND isDeleted = 0', [id]);
if (!rows || rows.length === 0) return res.status(404).json({ success: false, message: '记录不存在' });
res.json({ success: true, data: mapRow(rows[0]) });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 列表
router.get('/sensitiveWords', async (req, res) => {
try {
const { page = 1, pageSize = 20, sortBy = 'updateTime', sortOrder = 'desc' } = req.query;
const pageNum = Math.max(1, Number(page));
const sizeNum = Math.min(100, Math.max(1, Number(pageSize)));
const sortable = new Set(['updateTime', 'id', 'wordName', 'type', 'status']);
const sortCol = sortable.has(String(sortBy)) ? String(sortBy) : 'updateTime';
const order = String(sortOrder).toLowerCase() === 'asc' ? 'ASC' : 'DESC';
const filters = [];
const params = [];
const like = (v) => (v ? `%${String(v).trim()}%` : null);
if (req.query.wordName) { filters.push('wordName LIKE ?'); params.push(like(req.query.wordName)); }
if (req.query.type) { filters.push('type = ?'); params.push(req.query.type); }
if (req.query.status !== undefined) {
const s = toBoolOrNull(req.query.status);
if (s !== null) { filters.push('status = ?'); params.push(s ? 1 : 0); }
}
// 默认仅未删除
const isDeleted = toIntOrNull(req.query.isDeleted);
if (isDeleted === null) { filters.push('isDeleted = 0'); } else { filters.push('isDeleted = ?'); params.push(isDeleted); }
const where = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
const sql = `SELECT * FROM sensitiveWords ${where} ORDER BY ${sortCol} ${order} LIMIT ? OFFSET ?`;
const countSql = `SELECT COUNT(*) AS cnt FROM sensitiveWords ${where}`;
const listParams = [...params, sizeNum, (pageNum - 1) * sizeNum];
const [[countRows], [rows]] = await Promise.all([
db.query(countSql, params),
db.query(sql, listParams),
]);
const total = countRows.cnt || countRows[0]?.cnt || 0;
res.json({ success: true, data: rows.map(mapRow), page: pageNum, pageSize: sizeNum, total });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 软删除
router.delete('/sensitiveWords/:id', async (req, res) => {
try {
const id = toIntOrNull(req.params.id);
if (!id) return res.status(400).json({ success: false, message: 'id不合法' });
const [result] = await db.query('UPDATE sensitiveWords SET isDeleted = 1, updateTime = NOW() WHERE id = ?', [id]);
res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 恢复
router.post('/sensitiveWords/recover/:id', async (req, res) => {
try {
const id = toIntOrNull(req.params.id);
if (!id) return res.status(400).json({ success: false, message: 'id不合法' });
const [result] = await db.query('UPDATE sensitiveWords SET isDeleted = 0, updateTime = NOW() WHERE id = ?', [id]);
res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// 物理删除
router.delete('/sensitiveWords/force/:id', async (req, res) => {
try {
const id = toIntOrNull(req.params.id);
if (!id) return res.status(400).json({ success: false, message: 'id不合法' });
const [result] = await db.query('DELETE FROM sensitiveWords WHERE id = ?', [id]);
res.json({ success: true, affectedRows: result.affectedRows });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
module.exports = router;