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:
wangxiaogang
2026-06-07 15:35:37 +08:00
parent 4f42f2c342
commit bd7af8e512
35 changed files with 1683 additions and 759 deletions

View File

@@ -1,29 +1,18 @@
import { Router, Response } from 'express'
import { AuthRequest } from '../middleware/auth'
import { backupDatabase, getBackupList } from '../utils/backup'
import { requireAdmin } from '../middleware/requireAdmin'
const router = Router()
// 管理员用户 ID 列表(逗号分隔),未配置时默认仅 ID=1
const ADMIN_IDS = process.env.ADMIN_USER_IDS
? process.env.ADMIN_USER_IDS.split(',').map(Number)
: [1]
/** 检查是否为管理员,非管理员返回 403 */
function requireAdmin(req: AuthRequest, res: Response): boolean {
if (!req.userId || !ADMIN_IDS.includes(req.userId)) {
res.status(403).json({ code: 40300, message: '无权限' })
return false
}
return true
}
// 所有路由都需要管理员权限
router.use((req, res, next) => requireAdmin(req, res, next))
// 手动触发备份
router.post('/', async (req: AuthRequest, res: Response) => {
if (!requireAdmin(req, res)) return
try {
const filepath = await backupDatabase()
res.json({ code: 0, data: { filepath } })
await backupDatabase()
res.json({ code: 0, message: '备份成功' })
} catch (err) {
console.error('[Backup] 手动备份失败:', err)
res.status(500).json({ code: 50000, message: '备份失败' })
@@ -32,7 +21,6 @@ router.post('/', async (req: AuthRequest, res: Response) => {
// 获取备份列表
router.get('/', async (req: AuthRequest, res: Response) => {
if (!requireAdmin(req, res)) return
try {
const list = getBackupList()
res.json({ code: 0, data: list })