feat: 后端: 实现 tags 表 CRUD API

This commit is contained in:
Baobhan Sith
2025-04-15 07:35:05 +08:00
parent 366a5188c5
commit a2c8bbddeb
5 changed files with 265 additions and 1 deletions
+2
View File
@@ -12,6 +12,7 @@ import authRouter from './auth/auth.routes'; // 导入认证路由
import connectionsRouter from './connections/connections.routes'; import connectionsRouter from './connections/connections.routes';
import sftpRouter from './sftp/sftp.routes'; import sftpRouter from './sftp/sftp.routes';
import proxyRoutes from './proxies/proxies.routes'; // 导入代理路由 import proxyRoutes from './proxies/proxies.routes'; // 导入代理路由
import tagsRouter from './tags/tags.routes'; // 导入标签路由
import { initializeWebSocket } from './websocket'; import { initializeWebSocket } from './websocket';
// 基础 Express 应用设置 (后续会扩展) // 基础 Express 应用设置 (后续会扩展)
@@ -84,6 +85,7 @@ app.use('/api/v1/auth', authRouter);
app.use('/api/v1/connections', connectionsRouter); app.use('/api/v1/connections', connectionsRouter);
app.use('/api/v1/sftp', sftpRouter); app.use('/api/v1/sftp', sftpRouter);
app.use('/api/v1/proxies', proxyRoutes); // 挂载代理相关的路由 app.use('/api/v1/proxies', proxyRoutes); // 挂载代理相关的路由
app.use('/api/v1/tags', tagsRouter); // 挂载标签相关的路由
// 状态检查接口 // 状态检查接口
app.get('/api/v1/status', (req: Request, res: Response) => { app.get('/api/v1/status', (req: Request, res: Response) => {
+19 -1
View File
@@ -47,8 +47,17 @@ CREATE TABLE IF NOT EXISTS proxies (
); );
`; `;
// 新增:创建 tags 表的 SQL
const createTagsTableSQL = `
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE, -- 标签名称,唯一
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`;
// 未来可能需要的其他表 (根据项目文档) // 未来可能需要的其他表 (根据项目文档)
// const createTagsTableSQL = \`...\`; // 标签表
// const createConnectionTagsTableSQL = \`...\`; // 连接与标签的关联表 // const createConnectionTagsTableSQL = \`...\`; // 连接与标签的关联表
// const createSettingsTableSQL = \`...\`; // 设置表 // const createSettingsTableSQL = \`...\`; // 设置表
// const createAuditLogsTableSQL = \`...\`; // 审计日志表 // const createAuditLogsTableSQL = \`...\`; // 审计日志表
@@ -152,6 +161,15 @@ export const runMigrations = async (db: Database): Promise<void> => {
}); });
}); });
// 新增:创建 tags 表 (如果不存在)
await new Promise<void>((resolve, reject) => {
db.run(createTagsTableSQL, (err) => {
if (err) return reject(new Error(`创建 tags 表时出错: ${err.message}`));
console.log('Tags 表已检查/创建。');
resolve();
});
});
// Add other tables or columns here in the future // Add other tables or columns here in the future
console.log('数据库迁移检查完成。'); console.log('数据库迁移检查完成。');
@@ -0,0 +1,221 @@
import { Request, Response } from 'express';
import { Statement } from 'sqlite3';
import { getDb } from '../database';
const db = getDb();
// 标签数据结构 (用于类型提示)
interface TagInfo {
id: number;
name: string;
created_at: number;
updated_at: number;
}
/**
* 创建新标签 (POST /api/v1/tags)
*/
export const createTag = async (req: Request, res: Response): Promise<void> => {
const { name } = req.body;
const userId = req.session.userId; // 保留以备将来多用户支持
if (!name || typeof name !== 'string' || name.trim() === '') {
res.status(400).json({ message: '标签名称不能为空。' });
return;
}
const tagName = name.trim();
const now = Math.floor(Date.now() / 1000);
try {
// 插入数据库,name 字段有 UNIQUE 约束,重复会报错
const result = await new Promise<{ lastID: number }>((resolve, reject) => {
const stmt = db.prepare(
`INSERT INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)`
);
stmt.run(tagName, now, now, function (this: Statement, err: Error | null) {
if (err) {
if (err.message.includes('UNIQUE constraint failed')) {
return reject(new Error(`标签 "${tagName}" 已存在。`));
}
console.error('插入标签时出错:', err.message);
return reject(new Error('创建标签失败'));
}
resolve({ lastID: (this as any).lastID });
});
stmt.finalize();
});
res.status(201).json({
message: '标签创建成功。',
tag: { id: result.lastID, name: tagName, created_at: now, updated_at: now }
});
} catch (error: any) {
console.error('创建标签时发生错误:', error);
res.status(500).json({ message: error.message || '创建标签时发生内部服务器错误。' });
}
};
/**
* 获取标签列表 (GET /api/v1/tags)
*/
export const getTags = async (req: Request, res: Response): Promise<void> => {
const userId = req.session.userId; // 保留
try {
const tags = await new Promise<TagInfo[]>((resolve, reject) => {
db.all(
`SELECT id, name, created_at, updated_at FROM tags ORDER BY name ASC`,
(err, rows: TagInfo[]) => {
if (err) {
console.error('查询标签列表时出错:', err.message);
return reject(new Error('获取标签列表失败'));
}
resolve(rows);
}
);
});
res.status(200).json(tags);
} catch (error: any) {
console.error('获取标签列表时发生错误:', error);
res.status(500).json({ message: error.message || '获取标签列表时发生内部服务器错误。' });
}
};
/**
* 获取单个标签信息 (GET /api/v1/tags/:id)
*/
export const getTagById = async (req: Request, res: Response): Promise<void> => {
const tagId = parseInt(req.params.id, 10);
const userId = req.session.userId; // 保留
if (isNaN(tagId)) {
res.status(400).json({ message: '无效的标签 ID。' });
return;
}
try {
const tag = await new Promise<TagInfo | null>((resolve, reject) => {
db.get(
`SELECT id, name, created_at, updated_at FROM tags WHERE id = ?`,
[tagId],
(err, row: TagInfo) => {
if (err) {
console.error(`查询标签 ${tagId} 时出错:`, err.message);
return reject(new Error('获取标签信息失败'));
}
resolve(row || null);
}
);
});
if (!tag) {
res.status(404).json({ message: '标签未找到。' });
} else {
res.status(200).json(tag);
}
} catch (error: any) {
console.error(`获取标签 ${tagId} 时发生错误:`, error);
res.status(500).json({ message: error.message || '获取标签信息时发生内部服务器错误。' });
}
};
/**
* 更新标签信息 (PUT /api/v1/tags/:id)
*/
export const updateTag = async (req: Request, res: Response): Promise<void> => {
const tagId = parseInt(req.params.id, 10);
const { name } = req.body;
const userId = req.session.userId; // 保留
if (isNaN(tagId)) {
res.status(400).json({ message: '无效的标签 ID。' });
return;
}
if (!name || typeof name !== 'string' || name.trim() === '') {
res.status(400).json({ message: '标签名称不能为空。' });
return;
}
const tagName = name.trim();
const now = Math.floor(Date.now() / 1000);
try {
const result = await new Promise<{ changes: number }>((resolve, reject) => {
const stmt = db.prepare(
`UPDATE tags SET name = ?, updated_at = ? WHERE id = ?`
);
stmt.run(tagName, now, tagId, function (this: Statement, err: Error | null) {
if (err) {
if (err.message.includes('UNIQUE constraint failed')) {
return reject(new Error(`标签名称 "${tagName}" 已存在。`));
}
console.error(`更新标签 ${tagId} 时出错:`, err.message);
return reject(new Error('更新标签失败'));
}
resolve({ changes: (this as any).changes });
});
stmt.finalize();
});
if (result.changes === 0) {
res.status(404).json({ message: '标签未找到或名称未更改。' });
} else {
// 获取更新后的信息并返回
const updatedTag = await new Promise<TagInfo | null>((resolve, reject) => {
db.get(
`SELECT id, name, created_at, updated_at FROM tags WHERE id = ?`,
[tagId],
(err, row: TagInfo) => err ? reject(err) : resolve(row || null)
);
});
res.status(200).json({ message: '标签更新成功。', tag: updatedTag });
}
} catch (error: any) {
console.error(`更新标签 ${tagId} 时发生错误:`, error);
res.status(500).json({ message: error.message || '更新标签时发生内部服务器错误。' });
}
};
/**
* 删除标签 (DELETE /api/v1/tags/:id)
*/
export const deleteTag = async (req: Request, res: Response): Promise<void> => {
const tagId = parseInt(req.params.id, 10);
const userId = req.session.userId; // 保留
if (isNaN(tagId)) {
res.status(400).json({ message: '无效的标签 ID。' });
return;
}
try {
// TODO: 在删除标签前,需要考虑处理 connection_tags 关联表中的数据
// 例如:可以选择删除关联记录,或者阻止删除有关联的标签
// 当前简化处理:直接删除标签
const result = await new Promise<{ changes: number }>((resolve, reject) => {
const stmt = db.prepare(
`DELETE FROM tags WHERE id = ?`
);
stmt.run(tagId, function (this: Statement, err: Error | null) {
if (err) {
console.error(`删除标签 ${tagId} 时出错:`, err.message);
return reject(new Error('删除标签失败'));
}
resolve({ changes: (this as any).changes });
});
stmt.finalize();
});
if (result.changes === 0) {
res.status(404).json({ message: '标签未找到。' });
} else {
res.status(200).json({ message: '标签删除成功。' });
}
} catch (error: any) {
console.error(`删除标签 ${tagId} 时发生错误:`, error);
res.status(500).json({ message: error.message || '删除标签时发生内部服务器错误。' });
}
};
+23
View File
@@ -0,0 +1,23 @@
import { Router } from 'express';
import { isAuthenticated } from '../auth/auth.middleware'; // 引入认证中间件
import {
createTag,
getTags,
getTagById,
updateTag,
deleteTag
} from './tags.controller';
const router = Router();
// 应用认证中间件到所有标签路由
router.use(isAuthenticated);
// 定义标签相关的路由
router.post('/', createTag); // POST /api/v1/tags - 创建新标签
router.get('/', getTags); // GET /api/v1/tags - 获取标签列表
router.get('/:id', getTagById); // GET /api/v1/tags/:id - 获取单个标签
router.put('/:id', updateTag); // PUT /api/v1/tags/:id - 更新标签
router.delete('/:id', deleteTag); // DELETE /api/v1/tags/:id - 删除标签
export default router;
Binary file not shown.