From a2c8bbddeb5d2c00480b169ad9a271c5a2ded93c Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Tue, 15 Apr 2025 07:35:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E7=AB=AF:=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20tags=20=E8=A1=A8=20CRUD=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/index.ts | 2 + packages/backend/src/migrations.ts | 20 +- packages/backend/src/tags/tags.controller.ts | 221 +++++++++++++++++++ packages/backend/src/tags/tags.routes.ts | 23 ++ packages/data/nexus-terminal.db | Bin 32768 -> 40960 bytes 5 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/tags/tags.controller.ts create mode 100644 packages/backend/src/tags/tags.routes.ts diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 091efa1..77c6245 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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) => { diff --git a/packages/backend/src/migrations.ts b/packages/backend/src/migrations.ts index bba37b9..f181618 100644 --- a/packages/backend/src/migrations.ts +++ b/packages/backend/src/migrations.ts @@ -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 => { }); }); + // 新增:创建 tags 表 (如果不存在) + await new Promise((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('数据库迁移检查完成。'); diff --git a/packages/backend/src/tags/tags.controller.ts b/packages/backend/src/tags/tags.controller.ts new file mode 100644 index 0000000..10113b3 --- /dev/null +++ b/packages/backend/src/tags/tags.controller.ts @@ -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 => { + 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 => { + const userId = req.session.userId; // 保留 + + try { + const tags = await new Promise((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 => { + 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((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 => { + 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((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 => { + 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 || '删除标签时发生内部服务器错误。' }); + } +}; diff --git a/packages/backend/src/tags/tags.routes.ts b/packages/backend/src/tags/tags.routes.ts new file mode 100644 index 0000000..d76ff0f --- /dev/null +++ b/packages/backend/src/tags/tags.routes.ts @@ -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; diff --git a/packages/data/nexus-terminal.db b/packages/data/nexus-terminal.db index a2d9d8876704822712b8075a5a9ce7d739530ff9..378f827809de919f9d65a7df182bee8d9bc75d89 100644 GIT binary patch delta 230 zcmZo@U}`wPG(lRBiGhKE3y5KWW1^0+JQIUn(IQ^1B@CRbfeie`{Kxt9`8{~=vIcUT z=DEYWX0xEcWtPo>tih}-yjPhlHm~3^VbW;yV6=iIyElEtuNi9iCF9u;w=O9d-3ZZ_UfuXKC3c9)q&la>lU%T(=gx=@NH@x4|^K{DkCp#J@ zCvrKnsdKW4TZ>NiX#0es7V6VWr E0QKua#{d8T delta 88 zcmZoTz|_#dG(lRBk%57M1BhXOeWH%BG$Vsv(IQ^1B@FDWfeie`{Kxt9`8{~=vIcUT l-Yh6^on><%YcMMd{|{!%%`3P}m=<@kGcsy!zRRmz1OV@y6ZilC