feat: 后端: 实现 tags 表 CRUD API
This commit is contained in:
@@ -12,6 +12,7 @@ import authRouter from './auth/auth.routes'; // 导入认证路由
|
||||
import connectionsRouter from './connections/connections.routes';
|
||||
import sftpRouter from './sftp/sftp.routes';
|
||||
import proxyRoutes from './proxies/proxies.routes'; // 导入代理路由
|
||||
import tagsRouter from './tags/tags.routes'; // 导入标签路由
|
||||
import { initializeWebSocket } from './websocket';
|
||||
|
||||
// 基础 Express 应用设置 (后续会扩展)
|
||||
@@ -84,6 +85,7 @@ app.use('/api/v1/auth', authRouter);
|
||||
app.use('/api/v1/connections', connectionsRouter);
|
||||
app.use('/api/v1/sftp', sftpRouter);
|
||||
app.use('/api/v1/proxies', proxyRoutes); // 挂载代理相关的路由
|
||||
app.use('/api/v1/tags', tagsRouter); // 挂载标签相关的路由
|
||||
|
||||
// 状态检查接口
|
||||
app.get('/api/v1/status', (req: Request, res: Response) => {
|
||||
|
||||
@@ -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 createSettingsTableSQL = \`...\`; // 设置表
|
||||
// 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
|
||||
|
||||
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 || '删除标签时发生内部服务器错误。' });
|
||||
}
|
||||
};
|
||||
@@ -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.
Reference in New Issue
Block a user