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:
@@ -8,9 +8,13 @@ const gzipAsync = promisify(zlib.gzip)
|
||||
|
||||
const BACKUP_DIR = process.env.BACKUP_DIR || '/var/backups/xiaocai'
|
||||
|
||||
// 分页查询大小,避免一次性加载大表到内存
|
||||
const QUERY_BATCH_SIZE = 1000
|
||||
|
||||
/**
|
||||
* 使用 Node.js mysql2 备份数据库(不依赖 mysqldump)
|
||||
* 导出所有表的 INSERT 语句,gzip 压缩存储
|
||||
* 使用分页查询避免大表内存问题
|
||||
*/
|
||||
export async function backupDatabase(): Promise<string> {
|
||||
if (!fs.existsSync(BACKUP_DIR)) {
|
||||
@@ -35,38 +39,50 @@ export async function backupDatabase(): Promise<string> {
|
||||
lines.push(`-- 表: ${tableName}`)
|
||||
lines.push(`-- ----------------------------`)
|
||||
|
||||
// 获取表数据
|
||||
const [rows] = await pool.query(`SELECT * FROM \`${tableName}\``)
|
||||
const data = rows as any[]
|
||||
// 获取表总行数
|
||||
const [countResult] = await pool.query(`SELECT COUNT(*) as count FROM \`${tableName}\``)
|
||||
const totalRows = (countResult as any[])[0].count
|
||||
|
||||
if (data.length === 0) {
|
||||
if (totalRows === 0) {
|
||||
lines.push(`-- ${tableName}: 无数据`)
|
||||
lines.push('')
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取列名
|
||||
const columns = Object.keys(data[0])
|
||||
// 获取列名(从第一行)
|
||||
const [firstRow] = await pool.query(`SELECT * FROM \`${tableName}\` LIMIT 1`)
|
||||
const columns = Object.keys((firstRow as any[])[0])
|
||||
const colList = columns.map(c => `\`${c}\``).join(', ')
|
||||
|
||||
// 生成 INSERT 语句(每 100 行一批)
|
||||
const BATCH_SIZE = 100
|
||||
for (let i = 0; i < data.length; i += BATCH_SIZE) {
|
||||
const batch = data.slice(i, i + BATCH_SIZE)
|
||||
const values = batch.map(row => {
|
||||
const vals = columns.map(col => {
|
||||
const v = row[col]
|
||||
if (v === null) return 'NULL'
|
||||
if (typeof v === 'number') return String(v)
|
||||
if (typeof v === 'boolean') return v ? '1' : '0'
|
||||
// mysql2 escape 处理所有字符串/日期转义(自带引号)
|
||||
return pool.escape(v)
|
||||
})
|
||||
return `(${vals.join(', ')})`
|
||||
}).join(',\n ')
|
||||
// 分页查询并生成 INSERT 语句
|
||||
let offset = 0
|
||||
while (offset < totalRows) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT * FROM \`${tableName}\` LIMIT ? OFFSET ?`,
|
||||
[QUERY_BATCH_SIZE, offset]
|
||||
)
|
||||
const data = rows as any[]
|
||||
|
||||
lines.push(`INSERT INTO \`${tableName}\` (${colList}) VALUES`)
|
||||
lines.push(` ${values};`)
|
||||
// 每 100 行生成一条 INSERT
|
||||
const INSERT_BATCH = 100
|
||||
for (let i = 0; i < data.length; i += INSERT_BATCH) {
|
||||
const batch = data.slice(i, i + INSERT_BATCH)
|
||||
const values = batch.map(row => {
|
||||
const vals = columns.map(col => {
|
||||
const v = row[col]
|
||||
if (v === null) return 'NULL'
|
||||
if (typeof v === 'number') return String(v)
|
||||
if (typeof v === 'boolean') return v ? '1' : '0'
|
||||
return pool.escape(v)
|
||||
})
|
||||
return `(${vals.join(', ')})`
|
||||
}).join(',\n ')
|
||||
|
||||
lines.push(`INSERT INTO \`${tableName}\` (${colList}) VALUES`)
|
||||
lines.push(` ${values};`)
|
||||
}
|
||||
|
||||
offset += QUERY_BATCH_SIZE
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user