Files
xiaocai/server/src/routes/group.ts
wangxiaogang 9764b5626b feat: 群组管理页面优化 + 分类管理图标修复
- 创建/加入群组合并为 Tab 切换卡片
- 群主可刷新邀请码(后端+前端完整链路)
- 群主可管理成员:查看列表、移除成员
- 新增后端 API:refresh-code / members / remove-member
- 修复分类管理页 FAB 按钮缺少 plus 图标
- 新增 plus/refresh 图标资源
2026-06-04 21:28:22 +08:00

333 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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