|
@@ -10,6 +10,13 @@ const PORT = 3000;
|
|
|
// 硬编码的密码
|
|
// 硬编码的密码
|
|
|
const PASSWORD = 'cjy@0526';
|
|
const PASSWORD = 'cjy@0526';
|
|
|
|
|
|
|
|
|
|
+// 目录常量
|
|
|
|
|
+const DIRS = {
|
|
|
|
|
+ public: path.join(__dirname, 'public'),
|
|
|
|
|
+ docs: path.join(__dirname, 'docs'),
|
|
|
|
|
+ index: path.join(__dirname, 'index.md')
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
// index.md 缓存
|
|
// index.md 缓存
|
|
|
let indexCache = null;
|
|
let indexCache = null;
|
|
|
let indexCacheTime = 0;
|
|
let indexCacheTime = 0;
|
|
@@ -35,6 +42,40 @@ function validatePath(fullPath, baseDir) {
|
|
|
return resolvedPath.startsWith(resolvedBaseDir);
|
|
return resolvedPath.startsWith(resolvedBaseDir);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 检查是否已认证
|
|
|
|
|
+function isAuthenticated(req) {
|
|
|
|
|
+ return req.session && req.session.isAuthenticated === true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 统一的错误响应助手
|
|
|
|
|
+const ErrorResponse = {
|
|
|
|
|
+ badRequest: (res, message, details = null) => {
|
|
|
|
|
+ const response = { error: message };
|
|
|
|
|
+ if (details) response.details = details;
|
|
|
|
|
+ return res.status(400).json(response);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ unauthorized: (res, message = '未授权') => {
|
|
|
|
|
+ return res.status(401).json({ error: message });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ forbidden: (res, message = '拒绝访问') => {
|
|
|
|
|
+ return res.status(403).json({ error: message });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ notFound: (res, message, details = null) => {
|
|
|
|
|
+ const response = { error: message };
|
|
|
|
|
+ if (details) response.details = details;
|
|
|
|
|
+ return res.status(404).json(response);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ serverError: (res, message, details = null) => {
|
|
|
|
|
+ const response = { error: message };
|
|
|
|
|
+ if (details) response.details = details;
|
|
|
|
|
+ return res.status(500).json(response);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
// 中间件
|
|
// 中间件
|
|
|
// CORS 配置 - 允许前端携带凭证(cookies)
|
|
// CORS 配置 - 允许前端携带凭证(cookies)
|
|
|
app.use(cors({
|
|
app.use(cors({
|
|
@@ -62,13 +103,13 @@ app.use(session({
|
|
|
|
|
|
|
|
// 登录验证中间件
|
|
// 登录验证中间件
|
|
|
function requireAuth(req, res, next) {
|
|
function requireAuth(req, res, next) {
|
|
|
- if (req.session && req.session.isAuthenticated) {
|
|
|
|
|
|
|
+ if (isAuthenticated(req)) {
|
|
|
return next();
|
|
return next();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 如果是 API 请求,返回 401
|
|
// 如果是 API 请求,返回 401
|
|
|
if (req.path.startsWith('/api')) {
|
|
if (req.path.startsWith('/api')) {
|
|
|
- return res.status(401).json({ error: '未登录,请先登录' });
|
|
|
|
|
|
|
+ return ErrorResponse.unauthorized(res, '未登录,请先登录');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 如果是页面请求,重定向到登录页
|
|
// 如果是页面请求,重定向到登录页
|
|
@@ -77,73 +118,62 @@ function requireAuth(req, res, next) {
|
|
|
|
|
|
|
|
// 根路径重定向处理
|
|
// 根路径重定向处理
|
|
|
app.get('/', (req, res) => {
|
|
app.get('/', (req, res) => {
|
|
|
- console.log('访问首页 - Session状态:', req.session ? req.session.isAuthenticated : 'no session');
|
|
|
|
|
-
|
|
|
|
|
- if (req.session && req.session.isAuthenticated) {
|
|
|
|
|
- const indexPath = path.join(__dirname, 'public', 'index.html');
|
|
|
|
|
- console.log('发送首页文件:', indexPath);
|
|
|
|
|
-
|
|
|
|
|
- res.sendFile(indexPath, (err) => {
|
|
|
|
|
- if (err) {
|
|
|
|
|
|
|
+ if (isAuthenticated(req)) {
|
|
|
|
|
+ res.sendFile(path.join(DIRS.public, 'index.html'), (err) => {
|
|
|
|
|
+ if (err && !res.headersSent) {
|
|
|
console.error('发送首页文件失败:', err);
|
|
console.error('发送首页文件失败:', err);
|
|
|
- res.status(500).json({ error: '无法加载首页', details: err.message });
|
|
|
|
|
|
|
+ ErrorResponse.serverError(res, '无法加载首页', err.message);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
} else {
|
|
} else {
|
|
|
- console.log('未登录,重定向到登录页');
|
|
|
|
|
res.redirect('/login.html');
|
|
res.redirect('/login.html');
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// reader.html 需要登录
|
|
// reader.html 需要登录
|
|
|
app.get('/reader.html', (req, res) => {
|
|
app.get('/reader.html', (req, res) => {
|
|
|
- if (req.session && req.session.isAuthenticated) {
|
|
|
|
|
- res.sendFile(path.join(__dirname, 'public', 'reader.html'));
|
|
|
|
|
|
|
+ if (isAuthenticated(req)) {
|
|
|
|
|
+ res.sendFile(path.join(DIRS.public, 'reader.html'));
|
|
|
} else {
|
|
} else {
|
|
|
res.redirect('/login.html');
|
|
res.redirect('/login.html');
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 静态文件服务(CSS、JS、图片等)
|
|
// 静态文件服务(CSS、JS、图片等)
|
|
|
-app.use('/css', express.static(path.join(__dirname, 'public', 'css')));
|
|
|
|
|
-app.use('/js', express.static(path.join(__dirname, 'public', 'js')));
|
|
|
|
|
|
|
+app.use('/css', express.static(path.join(DIRS.public, 'css')));
|
|
|
|
|
+app.use('/js', express.static(path.join(DIRS.public, 'js')));
|
|
|
|
|
|
|
|
// login.html 不需要登录
|
|
// login.html 不需要登录
|
|
|
app.get('/login.html', (req, res) => {
|
|
app.get('/login.html', (req, res) => {
|
|
|
// 如果已登录,重定向到首页
|
|
// 如果已登录,重定向到首页
|
|
|
- if (req.session && req.session.isAuthenticated) {
|
|
|
|
|
|
|
+ if (isAuthenticated(req)) {
|
|
|
res.redirect('/');
|
|
res.redirect('/');
|
|
|
} else {
|
|
} else {
|
|
|
- res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
|
|
|
|
|
|
+ res.sendFile(path.join(DIRS.public, 'login.html'));
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// API: 登录
|
|
// API: 登录
|
|
|
app.post('/api/login', (req, res) => {
|
|
app.post('/api/login', (req, res) => {
|
|
|
const { password } = req.body;
|
|
const { password } = req.body;
|
|
|
- console.log('登录尝试 - IP:', req.ip, 'Session ID:', req.sessionID);
|
|
|
|
|
|
|
|
|
|
if (!password || typeof password !== 'string') {
|
|
if (!password || typeof password !== 'string') {
|
|
|
- console.log('登录失败: 密码格式错误');
|
|
|
|
|
- return res.status(400).json({ error: '请输入密码' });
|
|
|
|
|
|
|
+ return ErrorResponse.badRequest(res, '请输入密码');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (password === PASSWORD) {
|
|
if (password === PASSWORD) {
|
|
|
req.session.isAuthenticated = true;
|
|
req.session.isAuthenticated = true;
|
|
|
- console.log('登录成功 - Session ID:', req.sessionID, 'Session数据:', req.session);
|
|
|
|
|
|
|
|
|
|
// 保存 session 后再响应
|
|
// 保存 session 后再响应
|
|
|
req.session.save((err) => {
|
|
req.session.save((err) => {
|
|
|
if (err) {
|
|
if (err) {
|
|
|
console.error('Session保存失败:', err);
|
|
console.error('Session保存失败:', err);
|
|
|
- return res.status(500).json({ error: '登录失败,请重试' });
|
|
|
|
|
|
|
+ return ErrorResponse.serverError(res, '登录失败,请重试');
|
|
|
}
|
|
}
|
|
|
- console.log('Session保存成功');
|
|
|
|
|
res.json({ success: true, message: '登录成功' });
|
|
res.json({ success: true, message: '登录成功' });
|
|
|
});
|
|
});
|
|
|
} else {
|
|
} else {
|
|
|
- console.log('登录失败: 密码错误');
|
|
|
|
|
- res.status(401).json({ error: '密码错误' });
|
|
|
|
|
|
|
+ return ErrorResponse.unauthorized(res, '密码错误');
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -151,7 +181,7 @@ app.post('/api/login', (req, res) => {
|
|
|
app.post('/api/logout', (req, res) => {
|
|
app.post('/api/logout', (req, res) => {
|
|
|
req.session.destroy((err) => {
|
|
req.session.destroy((err) => {
|
|
|
if (err) {
|
|
if (err) {
|
|
|
- return res.status(500).json({ error: '登出失败' });
|
|
|
|
|
|
|
+ return ErrorResponse.serverError(res, '登出失败');
|
|
|
}
|
|
}
|
|
|
res.json({ success: true, message: '登出成功' });
|
|
res.json({ success: true, message: '登出成功' });
|
|
|
});
|
|
});
|
|
@@ -159,37 +189,31 @@ app.post('/api/logout', (req, res) => {
|
|
|
|
|
|
|
|
// API: 检查登录状态
|
|
// API: 检查登录状态
|
|
|
app.get('/api/check-auth', (req, res) => {
|
|
app.get('/api/check-auth', (req, res) => {
|
|
|
- if (req.session && req.session.isAuthenticated) {
|
|
|
|
|
- res.json({ authenticated: true });
|
|
|
|
|
- } else {
|
|
|
|
|
- res.json({ authenticated: false });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ res.json({ authenticated: isAuthenticated(req) });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// API: 获取图片资源(需要登录)
|
|
// API: 获取图片资源(需要登录)
|
|
|
app.get('/api/image/:category/*', requireAuth, async (req, res) => {
|
|
app.get('/api/image/:category/*', requireAuth, async (req, res) => {
|
|
|
try {
|
|
try {
|
|
|
- // 对分类名进行处理(移除危险字符但保留中文)
|
|
|
|
|
- const category = req.params.category.replace(/\.\./g, '');
|
|
|
|
|
|
|
+ const category = sanitizePath(req.params.category);
|
|
|
const imagePath = req.params[0]; // 获取剩余路径部分,如 'assets/test.png'
|
|
const imagePath = req.params[0]; // 获取剩余路径部分,如 'assets/test.png'
|
|
|
|
|
|
|
|
if (!category || !imagePath) {
|
|
if (!category || !imagePath) {
|
|
|
- return res.status(400).json({ error: '无效的请求路径' });
|
|
|
|
|
|
|
+ return ErrorResponse.badRequest(res, '无效的请求路径');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const docsDir = path.join(__dirname, 'docs');
|
|
|
|
|
- const fullPath = path.join(docsDir, category, imagePath);
|
|
|
|
|
|
|
+ const fullPath = path.join(DIRS.docs, category, imagePath);
|
|
|
|
|
|
|
|
// 验证路径安全性
|
|
// 验证路径安全性
|
|
|
- if (!validatePath(fullPath, docsDir)) {
|
|
|
|
|
- return res.status(403).json({ error: '拒绝访问' });
|
|
|
|
|
|
|
+ if (!validatePath(fullPath, DIRS.docs)) {
|
|
|
|
|
+ return ErrorResponse.forbidden(res, '拒绝访问');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 检查文件是否存在
|
|
// 检查文件是否存在
|
|
|
try {
|
|
try {
|
|
|
await fs.access(fullPath);
|
|
await fs.access(fullPath);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- return res.status(404).json({ error: '图片不存在' });
|
|
|
|
|
|
|
+ return ErrorResponse.notFound(res, '图片不存在');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 发送图片文件
|
|
// 发送图片文件
|
|
@@ -197,7 +221,7 @@ app.get('/api/image/:category/*', requireAuth, async (req, res) => {
|
|
|
|
|
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('Error serving image:', error);
|
|
console.error('Error serving image:', error);
|
|
|
- res.status(500).json({ error: '无法加载图片' });
|
|
|
|
|
|
|
+ return ErrorResponse.serverError(res, '无法加载图片');
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -209,8 +233,7 @@ async function getIndexStructure() {
|
|
|
return indexCache;
|
|
return indexCache;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const indexPath = path.join(__dirname, 'index.md');
|
|
|
|
|
- const content = await fs.readFile(indexPath, 'utf-8');
|
|
|
|
|
|
|
+ const content = await fs.readFile(DIRS.index, 'utf-8');
|
|
|
indexCache = parseIndexMd(content);
|
|
indexCache = parseIndexMd(content);
|
|
|
indexCacheTime = now;
|
|
indexCacheTime = now;
|
|
|
return indexCache;
|
|
return indexCache;
|
|
@@ -222,7 +245,7 @@ app.get('/api/structure', requireAuth, async (req, res) => {
|
|
|
const structure = await getIndexStructure();
|
|
const structure = await getIndexStructure();
|
|
|
res.json(structure);
|
|
res.json(structure);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- res.status(500).json({ error: '无法解析文档结构', details: error.message });
|
|
|
|
|
|
|
+ return ErrorResponse.serverError(res, '无法解析文档结构', error.message);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -232,19 +255,19 @@ app.get('/api/category/:category', requireAuth, async (req, res) => {
|
|
|
const category = sanitizePath(req.params.category);
|
|
const category = sanitizePath(req.params.category);
|
|
|
|
|
|
|
|
if (!category) {
|
|
if (!category) {
|
|
|
- return res.status(400).json({ error: '无效的分类名称' });
|
|
|
|
|
|
|
+ return ErrorResponse.badRequest(res, '无效的分类名称');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const structure = await getIndexStructure();
|
|
const structure = await getIndexStructure();
|
|
|
const categoryData = structure.find(cat => cat.name === category);
|
|
const categoryData = structure.find(cat => cat.name === category);
|
|
|
|
|
|
|
|
if (!categoryData) {
|
|
if (!categoryData) {
|
|
|
- return res.status(404).json({ error: '分类不存在' });
|
|
|
|
|
|
|
+ return ErrorResponse.notFound(res, '分类不存在');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
res.json(categoryData);
|
|
res.json(categoryData);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- res.status(500).json({ error: '无法获取分类信息', details: error.message });
|
|
|
|
|
|
|
+ return ErrorResponse.serverError(res, '无法获取分类信息', error.message);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -256,22 +279,21 @@ app.get('/api/doc/:category/*', requireAuth, async (req, res) => {
|
|
|
const docName = req.params[0]; // 获取剩余的路径部分,支持子目录
|
|
const docName = req.params[0]; // 获取剩余的路径部分,支持子目录
|
|
|
|
|
|
|
|
if (!category || !docName) {
|
|
if (!category || !docName) {
|
|
|
- return res.status(400).json({ error: '无效的分类或文档名称' });
|
|
|
|
|
|
|
+ return ErrorResponse.badRequest(res, '无效的分类或文档名称');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const docsDir = path.join(__dirname, 'docs');
|
|
|
|
|
// 直接使用 docName,它可能包含子目录路径
|
|
// 直接使用 docName,它可能包含子目录路径
|
|
|
- const docPath = path.join(docsDir, category, `${docName}.md`);
|
|
|
|
|
|
|
+ const docPath = path.join(DIRS.docs, category, `${docName}.md`);
|
|
|
|
|
|
|
|
// 验证路径是否在 docs 目录内
|
|
// 验证路径是否在 docs 目录内
|
|
|
- if (!validatePath(docPath, docsDir)) {
|
|
|
|
|
- return res.status(403).json({ error: '拒绝访问:无效的路径' });
|
|
|
|
|
|
|
+ if (!validatePath(docPath, DIRS.docs)) {
|
|
|
|
|
+ return ErrorResponse.forbidden(res, '拒绝访问:无效的路径');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const content = await fs.readFile(docPath, 'utf-8');
|
|
const content = await fs.readFile(docPath, 'utf-8');
|
|
|
res.json({ content, category, docName });
|
|
res.json({ content, category, docName });
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- res.status(500).json({ error: '无法读取文档', details: error.message });
|
|
|
|
|
|
|
+ return ErrorResponse.serverError(res, '无法读取文档', error.message);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -285,27 +307,26 @@ app.put('/api/doc/:category/*', requireAuth, async (req, res) => {
|
|
|
|
|
|
|
|
// 验证输入
|
|
// 验证输入
|
|
|
if (!category || !docName) {
|
|
if (!category || !docName) {
|
|
|
- return res.status(400).json({ error: '无效的分类或文档名称' });
|
|
|
|
|
|
|
+ return ErrorResponse.badRequest(res, '无效的分类或文档名称');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!content || typeof content !== 'string') {
|
|
if (!content || typeof content !== 'string') {
|
|
|
- return res.status(400).json({ error: '无效的文档内容' });
|
|
|
|
|
|
|
+ return ErrorResponse.badRequest(res, '无效的文档内容');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const docsDir = path.join(__dirname, 'docs');
|
|
|
|
|
// 直接使用 docName,它可能包含子目录路径
|
|
// 直接使用 docName,它可能包含子目录路径
|
|
|
- const docPath = path.join(docsDir, category, `${docName}.md`);
|
|
|
|
|
|
|
+ const docPath = path.join(DIRS.docs, category, `${docName}.md`);
|
|
|
|
|
|
|
|
// 验证路径是否在 docs 目录内
|
|
// 验证路径是否在 docs 目录内
|
|
|
- if (!validatePath(docPath, docsDir)) {
|
|
|
|
|
- return res.status(403).json({ error: '拒绝访问:无效的路径' });
|
|
|
|
|
|
|
+ if (!validatePath(docPath, DIRS.docs)) {
|
|
|
|
|
+ return ErrorResponse.forbidden(res, '拒绝访问:无效的路径');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 检查文件是否存在
|
|
// 检查文件是否存在
|
|
|
try {
|
|
try {
|
|
|
await fs.access(docPath);
|
|
await fs.access(docPath);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- return res.status(404).json({ error: '文档不存在' });
|
|
|
|
|
|
|
+ return ErrorResponse.notFound(res, '文档不存在');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 保存文件
|
|
// 保存文件
|
|
@@ -319,7 +340,7 @@ app.put('/api/doc/:category/*', requireAuth, async (req, res) => {
|
|
|
});
|
|
});
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('Save document error:', error);
|
|
console.error('Save document error:', error);
|
|
|
- res.status(500).json({ error: '无法保存文档', details: error.message });
|
|
|
|
|
|
|
+ return ErrorResponse.serverError(res, '无法保存文档', error.message);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -327,12 +348,11 @@ app.put('/api/doc/:category/*', requireAuth, async (req, res) => {
|
|
|
app.get('/api/search/:category', requireAuth, async (req, res) => {
|
|
app.get('/api/search/:category', requireAuth, async (req, res) => {
|
|
|
try {
|
|
try {
|
|
|
const category = sanitizePath(req.params.category);
|
|
const category = sanitizePath(req.params.category);
|
|
|
- // currentDoc 可能包含子目录路径,不要使用 sanitizePath
|
|
|
|
|
const currentDoc = req.query.currentDoc || '';
|
|
const currentDoc = req.query.currentDoc || '';
|
|
|
const { q } = req.query;
|
|
const { q } = req.query;
|
|
|
|
|
|
|
|
if (!category) {
|
|
if (!category) {
|
|
|
- return res.status(400).json({ error: '无效的分类名称' });
|
|
|
|
|
|
|
+ return ErrorResponse.badRequest(res, '无效的分类名称');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!q || q.trim().length === 0) {
|
|
if (!q || q.trim().length === 0) {
|
|
@@ -340,12 +360,11 @@ app.get('/api/search/:category', requireAuth, async (req, res) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const query = q.toLowerCase();
|
|
const query = q.toLowerCase();
|
|
|
- const docsDir = path.join(__dirname, 'docs');
|
|
|
|
|
- const categoryPath = path.join(docsDir, category);
|
|
|
|
|
|
|
+ const categoryPath = path.join(DIRS.docs, category);
|
|
|
|
|
|
|
|
// 验证路径是否在 docs 目录内
|
|
// 验证路径是否在 docs 目录内
|
|
|
- if (!validatePath(categoryPath, docsDir)) {
|
|
|
|
|
- return res.status(403).json({ error: '拒绝访问:无效的路径' });
|
|
|
|
|
|
|
+ if (!validatePath(categoryPath, DIRS.docs)) {
|
|
|
|
|
+ return ErrorResponse.forbidden(res, '拒绝访问:无效的路径');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 从 index.md 获取当前分类下应该搜索的文档列表
|
|
// 从 index.md 获取当前分类下应该搜索的文档列表
|
|
@@ -353,67 +372,66 @@ app.get('/api/search/:category', requireAuth, async (req, res) => {
|
|
|
const categoryData = structure.find(cat => cat.name === category);
|
|
const categoryData = structure.find(cat => cat.name === category);
|
|
|
|
|
|
|
|
if (!categoryData) {
|
|
if (!categoryData) {
|
|
|
- return res.status(404).json({ error: '分类不存在' });
|
|
|
|
|
|
|
+ return ErrorResponse.notFound(res, '分类不存在');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 只搜索 index.md 中列出的文档
|
|
|
|
|
- const mdFiles = categoryData.docs.map(doc => {
|
|
|
|
|
- // doc.name 可能包含子目录路径,如 "测试/11"
|
|
|
|
|
- return doc.name + '.md';
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const currentDocResults = [];
|
|
|
|
|
- const otherDocsResults = [];
|
|
|
|
|
|
|
+ // 并行读取所有文档
|
|
|
|
|
+ const MAX_MATCHES_PER_DOC = 5;
|
|
|
|
|
+ const decodedCurrentDoc = decodeURIComponent(currentDoc).replace(/\\/g, '/');
|
|
|
|
|
|
|
|
- // 搜索每个文档
|
|
|
|
|
- for (const file of mdFiles) {
|
|
|
|
|
- const docName = file.replace('.md', '').replace(/\\/g, '/'); // 统一使用正斜杠
|
|
|
|
|
- const filePath = path.join(categoryPath, file.replace(/\//g, path.sep)); // 处理路径分隔符
|
|
|
|
|
|
|
+ const fileReadPromises = categoryData.docs.map(async (doc) => {
|
|
|
|
|
+ const docName = doc.name;
|
|
|
|
|
+ const filePath = path.join(categoryPath, `${docName}.md`);
|
|
|
|
|
|
|
|
- // 检查文件是否存在(以防 index.md 中列出的文件实际不存在)
|
|
|
|
|
try {
|
|
try {
|
|
|
- await fs.access(filePath);
|
|
|
|
|
|
|
+ const content = await fs.readFile(filePath, 'utf-8');
|
|
|
|
|
+ return { docName, content, exists: true };
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
console.warn(`文档不存在,跳过搜索: ${filePath}`);
|
|
console.warn(`文档不存在,跳过搜索: ${filePath}`);
|
|
|
- continue;
|
|
|
|
|
|
|
+ return { docName, exists: false };
|
|
|
}
|
|
}
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const fileContents = await Promise.all(fileReadPromises);
|
|
|
|
|
+ const currentDocResults = [];
|
|
|
|
|
+ const otherDocsResults = [];
|
|
|
|
|
|
|
|
- const content = await fs.readFile(filePath, 'utf-8');
|
|
|
|
|
|
|
+ // 搜索每个文档
|
|
|
|
|
+ for (const { docName, content, exists } of fileContents) {
|
|
|
|
|
+ if (!exists) continue;
|
|
|
|
|
|
|
|
- // 搜索匹配的行
|
|
|
|
|
const lines = content.split('\n');
|
|
const lines = content.split('\n');
|
|
|
const matches = [];
|
|
const matches = [];
|
|
|
|
|
|
|
|
- lines.forEach((line, index) => {
|
|
|
|
|
|
|
+ // 搜索匹配的行,达到上限后提前退出
|
|
|
|
|
+ for (let index = 0; index < lines.length && matches.length < MAX_MATCHES_PER_DOC; index++) {
|
|
|
|
|
+ const line = lines[index];
|
|
|
const lowerLine = line.toLowerCase();
|
|
const lowerLine = line.toLowerCase();
|
|
|
|
|
+
|
|
|
if (lowerLine.includes(query)) {
|
|
if (lowerLine.includes(query)) {
|
|
|
- // 获取上下文(前后各50个字符)
|
|
|
|
|
const startIndex = Math.max(0, lowerLine.indexOf(query) - 50);
|
|
const startIndex = Math.max(0, lowerLine.indexOf(query) - 50);
|
|
|
const endIndex = Math.min(line.length, lowerLine.indexOf(query) + query.length + 50);
|
|
const endIndex = Math.min(line.length, lowerLine.indexOf(query) + query.length + 50);
|
|
|
let snippet = line.substring(startIndex, endIndex);
|
|
let snippet = line.substring(startIndex, endIndex);
|
|
|
|
|
|
|
|
- // 如果不是从头开始,添加省略号
|
|
|
|
|
if (startIndex > 0) snippet = '...' + snippet;
|
|
if (startIndex > 0) snippet = '...' + snippet;
|
|
|
if (endIndex < line.length) snippet = snippet + '...';
|
|
if (endIndex < line.length) snippet = snippet + '...';
|
|
|
|
|
|
|
|
matches.push({
|
|
matches.push({
|
|
|
line: index + 1,
|
|
line: index + 1,
|
|
|
- snippet: snippet,
|
|
|
|
|
|
|
+ snippet,
|
|
|
fullLine: line
|
|
fullLine: line
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
- });
|
|
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
if (matches.length > 0) {
|
|
if (matches.length > 0) {
|
|
|
const result = {
|
|
const result = {
|
|
|
docName,
|
|
docName,
|
|
|
matchCount: matches.length,
|
|
matchCount: matches.length,
|
|
|
- matches: matches.slice(0, 5) // 最多返回5个匹配
|
|
|
|
|
|
|
+ matches
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 区分当前文档和其他文档
|
|
// 区分当前文档和其他文档
|
|
|
- // 处理 URL 解码后的 currentDoc(支持斜杠路径)
|
|
|
|
|
- const decodedCurrentDoc = decodeURIComponent(currentDoc).replace(/\\/g, '/');
|
|
|
|
|
if (docName === decodedCurrentDoc) {
|
|
if (docName === decodedCurrentDoc) {
|
|
|
currentDocResults.push(result);
|
|
currentDocResults.push(result);
|
|
|
} else {
|
|
} else {
|
|
@@ -429,7 +447,7 @@ app.get('/api/search/:category', requireAuth, async (req, res) => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- res.status(500).json({ error: '搜索失败', details: error.message });
|
|
|
|
|
|
|
+ return ErrorResponse.serverError(res, '搜索失败', error.message);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -483,7 +501,6 @@ app.use((err, req, res, next) => {
|
|
|
|
|
|
|
|
// 404 处理
|
|
// 404 处理
|
|
|
app.use((req, res) => {
|
|
app.use((req, res) => {
|
|
|
- console.log('404 未找到:', req.method, req.path);
|
|
|
|
|
res.status(404).json({
|
|
res.status(404).json({
|
|
|
error: '页面不存在',
|
|
error: '页面不存在',
|
|
|
path: req.path
|
|
path: req.path
|
|
@@ -493,6 +510,4 @@ app.use((req, res) => {
|
|
|
// 启动服务器
|
|
// 启动服务器
|
|
|
app.listen(PORT, () => {
|
|
app.listen(PORT, () => {
|
|
|
console.log(`服务器运行在 http://localhost:${PORT}`);
|
|
console.log(`服务器运行在 http://localhost:${PORT}`);
|
|
|
- console.log(`密码: ${PASSWORD}`);
|
|
|
|
|
- console.log(`Session 有效期: 7天`);
|
|
|
|
|
});
|
|
});
|