import { Router, Response } from 'express' import { randomInt } from 'crypto' import pool from '../db/connection' import { AuthRequest } from '../middleware/auth' const router = Router() /** 生成 6 位随机邀请码(密码学安全) */ function generateInviteCode(): string { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' let code = '' for (let i = 0; i < 6; i++) { code += chars.charAt(randomInt(chars.length)) } return code } /** 创建群组 */ router.post('/', async (req: AuthRequest, res: Response) => { try { const { name } = req.body const trimmedName = typeof name === 'string' ? name.trim() : '' if (!trimmedName) { return res.status(400).json({ code: 40001, message: '群组名称不能为空' }) } if (trimmedName.length > 100) { return res.status(400).json({ code: 40001, message: '群组名称不能超过100个字符' }) } // 生成唯一邀请码 let inviteCode = generateInviteCode() let attempts = 0 while (attempts < 10) { const [existing] = await pool.query('SELECT id FROM `groups` WHERE invite_code = ?', [inviteCode]) if ((existing as any[]).length === 0) break inviteCode = generateInviteCode() attempts++ } const conn = await pool.getConnection() try { await conn.beginTransaction() const [result] = await conn.query( 'INSERT INTO `groups` (name, invite_code, created_by) VALUES (?, ?, ?)', [trimmedName, inviteCode, req.userId] ) const groupId = (result as any).insertId // 创建者自动成为 owner await conn.query( 'INSERT INTO group_members (group_id, user_id, role) VALUES (?, ?, ?)', [groupId, req.userId, 'owner'] ) await conn.commit() res.json({ code: 0, data: { id: groupId, invite_code: inviteCode } }) } catch (err) { await conn.rollback() throw err } finally { conn.release() } } catch (err) { console.error('[Group] POST error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) /** 获取用户的群组列表 */ router.get('/', async (req: AuthRequest, res: Response) => { try { const [rows] = await pool.query( `SELECT g.id, g.name, g.invite_code, g.created_by, g.created_at, gm.role, gm.nickname as my_nickname, (SELECT COUNT(*) FROM group_members WHERE group_id = g.id) as member_count FROM \`groups\` g INNER JOIN group_members gm ON g.id = gm.group_id AND gm.user_id = ? ORDER BY g.created_at DESC`, [req.userId] ) res.json({ code: 0, data: rows }) } catch (err) { console.error('[Group] GET error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) /** 通过邀请码加入群组 */ router.post('/join', async (req: AuthRequest, res: Response) => { try { const { invite_code } = req.body if (!invite_code || typeof invite_code !== 'string' || invite_code.length > 10) { return res.status(400).json({ code: 40001, message: '邀请码无效' }) } const [groupRows] = await pool.query( 'SELECT id, name FROM `groups` WHERE invite_code = ?', [invite_code.toUpperCase()] ) const group = (groupRows as any[])[0] if (!group) { return res.status(404).json({ code: 40400, message: '邀请码无效' }) } // 使用 INSERT IGNORE 避免并发加入时的竞态条件 const [result] = await pool.query( 'INSERT IGNORE INTO group_members (group_id, user_id, role) VALUES (?, ?, ?)', [group.id, req.userId, 'member'] ) if ((result as any).affectedRows === 0) { return res.status(400).json({ code: 40002, message: '你已经是该群组成员' }) } res.json({ code: 0, data: { group_id: group.id, name: group.name } }) } catch (err) { console.error('[Group] join error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) /** 退出群组 */ router.post('/:id/leave', async (req: AuthRequest, res: Response) => { try { const groupId = req.params.id // 检查是否是成员 const [memberRows] = await pool.query( 'SELECT role FROM group_members WHERE group_id = ? AND user_id = ?', [groupId, req.userId] ) const member = (memberRows as any[])[0] if (!member) { return res.status(404).json({ code: 40400, message: '你不是该群组成员' }) } // owner 不能退出,只能解散 if (member.role === 'owner') { return res.status(400).json({ code: 40003, message: '群主不能退出,请先转让或解散群组' }) } const conn = await pool.getConnection() try { await conn.beginTransaction() // 清除该用户在该群组的记录标签 await conn.query( 'UPDATE transactions SET group_id = NULL WHERE user_id = ? AND group_id = ?', [req.userId, groupId] ) // 删除成员关系 await conn.query( 'DELETE FROM group_members WHERE group_id = ? AND user_id = ?', [groupId, req.userId] ) await conn.commit() res.json({ code: 0 }) } catch (err) { await conn.rollback() throw err } finally { conn.release() } } catch (err) { console.error('[Group] leave error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) /** 解散群组(仅 owner) */ router.delete('/:id', async (req: AuthRequest, res: Response) => { try { const [groupRows] = await pool.query( 'SELECT created_by FROM `groups` WHERE id = ?', [req.params.id] ) const group = (groupRows as any[])[0] if (!group) { return res.status(404).json({ code: 40400, message: '群组不存在' }) } if (group.created_by !== req.userId) { return res.status(403).json({ code: 40300, message: '仅群主可解散群组' }) } const conn = await pool.getConnection() try { await conn.beginTransaction() // 清除该群组所有记录的标签 await conn.query('UPDATE transactions SET group_id = NULL WHERE group_id = ?', [req.params.id]) // 删除群组(CASCADE 自动清理 group_members) await conn.query('DELETE FROM `groups` WHERE id = ?', [req.params.id]) await conn.commit() res.json({ code: 0 }) } catch (err) { await conn.rollback() throw err } finally { conn.release() } } catch (err) { console.error('[Group] DELETE error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) /** 刷新邀请码(仅 owner) */ router.post('/:id/refresh-code', async (req: AuthRequest, res: Response) => { try { const groupId = req.params.id // 验证是否是群主 const [groupRows] = await pool.query( 'SELECT created_by FROM `groups` WHERE id = ?', [groupId] ) const group = (groupRows as any[])[0] if (!group) { return res.status(404).json({ code: 40400, message: '群组不存在' }) } if (group.created_by !== req.userId) { return res.status(403).json({ code: 40300, message: '仅群主可刷新邀请码' }) } // 生成新的唯一邀请码 let inviteCode = generateInviteCode() let attempts = 0 while (attempts < 10) { const [existing] = await pool.query('SELECT id FROM `groups` WHERE invite_code = ?', [inviteCode]) if ((existing as any[]).length === 0) break inviteCode = generateInviteCode() attempts++ } await pool.query('UPDATE `groups` SET invite_code = ? WHERE id = ?', [inviteCode, groupId]) res.json({ code: 0, data: { invite_code: inviteCode } }) } catch (err) { console.error('[Group] refresh-code error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) /** 获取成员列表 */ router.get('/:id/members', async (req: AuthRequest, res: Response) => { try { const groupId = req.params.id // 验证用户是否是群组成员 const [memberCheck] = await pool.query( 'SELECT id FROM group_members WHERE group_id = ? AND user_id = ?', [groupId, req.userId] ) if ((memberCheck as any[]).length === 0) { return res.status(403).json({ code: 40300, message: '无权访问此群组' }) } const [members] = await pool.query( `SELECT gm.user_id, gm.role, gm.created_at as joined_at, u.nickname, u.avatar_url FROM group_members gm LEFT JOIN users u ON gm.user_id = u.id WHERE gm.group_id = ? ORDER BY gm.role DESC, gm.created_at ASC`, [groupId] ) res.json({ code: 0, data: members }) } catch (err) { console.error('[Group] members error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) /** 移除成员(仅 owner) */ router.delete('/:id/members/:userId', async (req: AuthRequest, res: Response) => { try { const groupId = req.params.id const targetUserId = Number(req.params.userId) // 不能移除自己 if (targetUserId === req.userId) { return res.status(400).json({ code: 40001, message: '不能移除自己' }) } // 验证是否是群主 const [groupRows] = await pool.query( 'SELECT created_by FROM `groups` WHERE id = ?', [groupId] ) const group = (groupRows as any[])[0] if (!group) { return res.status(404).json({ code: 40400, message: '群组不存在' }) } if (group.created_by !== req.userId) { return res.status(403).json({ code: 40300, message: '仅群主可移除成员' }) } const conn = await pool.getConnection() try { await conn.beginTransaction() // 清除该成员在群组的记录标签 await conn.query( 'UPDATE transactions SET group_id = NULL WHERE user_id = ? AND group_id = ?', [targetUserId, groupId] ) // 删除成员关系 await conn.query( 'DELETE FROM group_members WHERE group_id = ? AND user_id = ?', [groupId, targetUserId] ) await conn.commit() res.json({ code: 0 }) } catch (err) { await conn.rollback() throw err } finally { conn.release() } } catch (err) { console.error('[Group] remove member error:', err) res.status(500).json({ code: 50000, message: '服务器错误' }) } }) export default router