feat: 全面系统检查与修复 — 51项问题修复
## 重大修复 ### 安全性修复 - 修复 notification read-all SQL 运算符优先级 bug - 修复 link_url XSS 漏洞(验证 URL 协议) - 修复 LIKE 通配符注入(转义 % 和 _) - 修复 uploadImageIfNeeded 运算符优先级 bug - 401 时清理所有本地存储(token/nickname/avatar/role/group) ### 数据完整性修复 - 修复公告已读状态共享问题(新建 notification_reads 表) - 修复群组账单数据缺失(退出群组时保留 group_id) - 修复群组解散后邀请码仍可加入 - 修复分类迁移未校验目标类型 - 修复群组公告缺少 group_id 必填校验 ### 功能修复 - 修复 category PUT /sort 路由冲突 - 修复 GROUP BY 不完整问题 - 修复 budget API 类型不匹配 - 修复 categoryStore.migrateCategory 不刷新本地数据 - 修复 groupStore 并发请求问题 - 修复账单页覆盖 store 数据 - 修复群组预算查询返回 0 而非 null - 修复通知页面 onShow 不刷新列表 - 修复统计页面不必要的重复请求 ### 用户体验优化 - 添加通知详情查看功能(弹窗) - 添加通知图片服务器上传 - 添加 Markdown 富文本工具栏 - 添加管理页面客户端认证检查 - 添加管理员公告页面下拉刷新 - 添加数据导出进度反馈 - 添加账单删除后筛选金额更新 - Numpad 添加安全区域 padding ### 代码质量提升 - 提取 requireAdmin 为共享中间件 - filter-panel 使用设计 token - 修复 getCurrentMonth 时区不一致 - 备份功能使用分页查询避免内存问题 - 管理后台仪表盘添加缓存 - 邀请码碰撞重试后报错 ## 新增文件 - server/src/middleware/requireAdmin.ts — 共享管理员权限中间件 - client/src/pages/admin/notifications/index.vue — 公告管理页面 ## 数据库变更 - 新增 notification_reads 表(公告已读记录) - 群组解散时保留 groups 记录和 transactions.group_id
This commit is contained in:
@@ -36,6 +36,9 @@ router.post('/', async (req: AuthRequest, res: Response) => {
|
||||
inviteCode = generateInviteCode()
|
||||
attempts++
|
||||
}
|
||||
if (attempts >= 10) {
|
||||
return res.status(500).json({ code: 50001, message: '生成邀请码失败,请重试' })
|
||||
}
|
||||
|
||||
const conn = await pool.getConnection()
|
||||
try {
|
||||
@@ -103,6 +106,15 @@ router.post('/join', async (req: AuthRequest, res: Response) => {
|
||||
return res.status(404).json({ code: 40400, message: '邀请码无效' })
|
||||
}
|
||||
|
||||
// 检查群组是否已解散(没有任何成员)
|
||||
const [memberCount] = await pool.query(
|
||||
'SELECT COUNT(*) as count FROM group_members WHERE group_id = ?',
|
||||
[group.id]
|
||||
)
|
||||
if ((memberCount as any[])[0].count === 0) {
|
||||
return res.status(400).json({ code: 40003, message: '该群组已解散' })
|
||||
}
|
||||
|
||||
// 使用 INSERT IGNORE 避免并发加入时的竞态条件
|
||||
const [result] = await pool.query(
|
||||
'INSERT IGNORE INTO group_members (group_id, user_id, role) VALUES (?, ?, ?)',
|
||||
@@ -139,37 +151,20 @@ router.post('/:id/leave', async (req: AuthRequest, res: Response) => {
|
||||
return res.status(400).json({ code: 40003, message: '群主不能退出,请先转让或解散群组' })
|
||||
}
|
||||
|
||||
const conn = await pool.getConnection()
|
||||
try {
|
||||
await conn.beginTransaction()
|
||||
// 删除成员关系(保留 transactions.group_id,历史记录仍属于该群组)
|
||||
await pool.query(
|
||||
'DELETE FROM group_members WHERE group_id = ? AND user_id = ?',
|
||||
[groupId, req.userId]
|
||||
)
|
||||
|
||||
// 清除该用户在该群组的记录标签
|
||||
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()
|
||||
}
|
||||
res.json({ code: 0 })
|
||||
} catch (err) {
|
||||
console.error('[Group] leave error:', err)
|
||||
res.status(500).json({ code: 50000, message: '服务器错误' })
|
||||
}
|
||||
})
|
||||
|
||||
/** 解散群组(仅 owner) */
|
||||
/** 解散群组(仅 owner) — 保留群组记录和 transactions.group_id,仅移除所有成员 */
|
||||
router.delete('/:id', async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const [groupRows] = await pool.query(
|
||||
@@ -184,24 +179,10 @@ router.delete('/:id', async (req: AuthRequest, res: Response) => {
|
||||
return res.status(403).json({ code: 40300, message: '仅群主可解散群组' })
|
||||
}
|
||||
|
||||
const conn = await pool.getConnection()
|
||||
try {
|
||||
await conn.beginTransaction()
|
||||
// 移除所有成员(保留群组记录和 transactions.group_id,历史数据不丢失)
|
||||
await pool.query('DELETE FROM group_members WHERE group_id = ?', [req.params.id])
|
||||
|
||||
// 清除该群组所有记录的标签
|
||||
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()
|
||||
}
|
||||
res.json({ code: 0 })
|
||||
} catch (err) {
|
||||
console.error('[Group] DELETE error:', err)
|
||||
res.status(500).json({ code: 50000, message: '服务器错误' })
|
||||
@@ -235,6 +216,9 @@ router.post('/:id/refresh-code', async (req: AuthRequest, res: Response) => {
|
||||
inviteCode = generateInviteCode()
|
||||
attempts++
|
||||
}
|
||||
if (attempts >= 10) {
|
||||
return res.status(500).json({ code: 50001, message: '生成邀请码失败,请重试' })
|
||||
}
|
||||
|
||||
await pool.query('UPDATE `groups` SET invite_code = ? WHERE id = ?', [inviteCode, groupId])
|
||||
res.json({ code: 0, data: { invite_code: inviteCode } })
|
||||
@@ -299,30 +283,13 @@ router.delete('/:id/members/:userId', async (req: AuthRequest, res: Response) =>
|
||||
return res.status(403).json({ code: 40300, message: '仅群主可移除成员' })
|
||||
}
|
||||
|
||||
const conn = await pool.getConnection()
|
||||
try {
|
||||
await conn.beginTransaction()
|
||||
// 删除成员关系(保留 transactions.group_id,历史记录仍属于该群组)
|
||||
await pool.query(
|
||||
'DELETE FROM group_members WHERE group_id = ? AND user_id = ?',
|
||||
[groupId, targetUserId]
|
||||
)
|
||||
|
||||
// 清除该成员在群组的记录标签
|
||||
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()
|
||||
}
|
||||
res.json({ code: 0 })
|
||||
} catch (err) {
|
||||
console.error('[Group] remove member error:', err)
|
||||
res.status(500).json({ code: 50000, message: '服务器错误' })
|
||||
|
||||
Reference in New Issue
Block a user