refactor(iter-v2): T02 code quality - shared components, transactions, utils

- C2: ColorPicker and EditModal reusable components extracted
- C2: tag-manage and category-manage pages use EditModal (remove ~120 dup lines)
- C3: Upload logic extracted to server/src/utils/upload.ts
- C4: /recurring/sync wrapped in DB transaction
- C7: Backup download uses API_BASE (no hardcoded URLs)
- C9: getAvatarUrl() utility function, used in 5 files
- C1: getCurrentMonth() documented as aligned with server date.ts
This commit is contained in:
2026-06-11 09:49:17 +08:00
parent d170df5c51
commit 5a0ae0b28d
17 changed files with 350 additions and 338 deletions

View File

@@ -22,4 +22,9 @@ const pool = mysql.createPool({
console.error('[DB] Pool error:', err?.message || err)
})
/** 获取单个连接(用于事务) */
export async function getConnection() {
return pool.getConnection()
}
export default pool

View File

@@ -1,48 +1,13 @@
import { Router, Response, NextFunction } from 'express'
import multer from 'multer'
import path from 'path'
import fs from 'fs'
import pool from '../db/connection'
import { AuthRequest } from '../middleware/auth'
import { requireAdmin } from '../middleware/requireAdmin'
import { createUploadConfig } from '../utils/upload'
const router = Router()
// 通知图片上传配置
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'
const NOTIFICATION_DIR = path.resolve(UPLOAD_DIR, 'notifications')
if (!fs.existsSync(NOTIFICATION_DIR)) {
fs.mkdirSync(NOTIFICATION_DIR, { recursive: true })
}
const storage = multer.diskStorage({
destination: (_req, _file, cb) => {
if (!fs.existsSync(NOTIFICATION_DIR)) {
fs.mkdirSync(NOTIFICATION_DIR, { recursive: true })
}
cb(null, NOTIFICATION_DIR)
},
filename: (req: AuthRequest, file, cb) => {
const extMap: Record<string, string> = {
'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp'
}
const ext = extMap[file.mimetype] || '.jpg'
cb(null, `notif_${req.userId}_${Date.now()}${ext}`)
}
})
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 最大 5MB
fileFilter: (_req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp']
if (allowed.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error('仅支持 JPEG、PNG、WebP 格式'))
}
}
})
// 通知图片上传配置5MB
const upload = createUploadConfig('notifications', 5 * 1024 * 1024)
/** 公告编辑权限检查(管理员可编辑所有公告,群主可编辑自己群组的公告) */
async function requireNotificationEditAuth(req: AuthRequest, res: Response, next: NextFunction) {

View File

@@ -127,12 +127,16 @@ router.delete('/:id', async (req, res) => {
}
})
/** 同步:检查所有活跃模板,生成到期交易 */
/** 同步:检查所有活跃模板,生成到期交易(事务包裹) */
router.post('/sync', async (req, res) => {
const conn = await pool.getConnection()
try {
const userId = (req as any).userId
await conn.beginTransaction()
// 获取该用户所有活跃且到期next_date <= 今天)的模板
const [templates] = await pool.execute(
const [templates] = await conn.execute(
`SELECT * FROM recurring_templates
WHERE user_id = ? AND is_active = 1 AND next_date <= CURDATE()
ORDER BY next_date ASC`,
@@ -141,7 +145,6 @@ router.post('/sync', async (req, res) => {
let generated = 0
const today = new Date()
const todayStr = today.toISOString().slice(0, 10)
for (const t of templates as any[]) {
let next = new Date(t.next_date)
@@ -152,13 +155,13 @@ router.post('/sync', async (req, res) => {
if (end && next > end) break
// 检查是否已生成过(避免重复)
const [existing] = await pool.execute(
const [existing] = await conn.execute(
`SELECT id FROM transactions
WHERE user_id = ? AND recurring_id = ? AND date = ?`,
[userId, t.id, next.toISOString().slice(0, 10)]
)
if ((existing as any[]).length === 0) {
await pool.execute(
await conn.execute(
`INSERT INTO transactions (user_id, amount, type, category_id, note, date, group_id, recurring_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[userId, t.amount, t.type, t.category_id, t.note,
@@ -178,16 +181,20 @@ router.post('/sync', async (req, res) => {
}
// 更新 next_date循环结束后 next 已推进到今天之后的下一周期,直接使用
await pool.execute(
await conn.execute(
'UPDATE recurring_templates SET next_date = ? WHERE id = ?',
[next.toISOString().slice(0, 10), t.id]
)
}
await conn.commit()
res.json({ code: 0, data: { generated } })
} catch (error) {
await conn.rollback()
console.error('Sync recurring error:', error)
res.status(500).json({ code: 50000, message: '同步失败' })
} finally {
conn.release()
}
})

View File

@@ -1,48 +1,15 @@
import { Router, Response } from 'express'
import multer from 'multer'
import path from 'path'
import fs from 'fs'
import pool from '../db/connection'
import { AuthRequest } from '../middleware/auth'
import { createUploadConfig } from '../utils/upload'
const router = Router()
// 头像上传配置
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'
const AVATAR_DIR = path.resolve(UPLOAD_DIR, 'avatars')
if (!fs.existsSync(AVATAR_DIR)) {
fs.mkdirSync(AVATAR_DIR, { recursive: true })
}
const storage = multer.diskStorage({
destination: (_req, _file, cb) => {
// 确保目录存在(可能被手动删除)
if (!fs.existsSync(AVATAR_DIR)) {
fs.mkdirSync(AVATAR_DIR, { recursive: true })
}
cb(null, AVATAR_DIR)
},
filename: (req: AuthRequest, file, cb) => {
const extMap: Record<string, string> = {
'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp'
}
const ext = extMap[file.mimetype] || '.jpg'
cb(null, `${req.userId}_${Date.now()}${ext}`)
}
})
const upload = multer({
storage,
limits: { fileSize: 2 * 1024 * 1024 }, // 最大 2MB
fileFilter: (_req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp']
if (allowed.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error('仅支持 JPEG、PNG、WebP 格式'))
}
}
})
// 头像上传配置2MB
const AVATAR_DIR = path.resolve(process.env.UPLOAD_DIR || './uploads', 'avatars')
const upload = createUploadConfig('avatars', 2 * 1024 * 1024)
/** 获取当前用户信息 */
router.get('/me', async (req: AuthRequest, res: Response) => {

View File

@@ -0,0 +1,45 @@
import multer from 'multer'
import path from 'path'
import crypto from 'crypto'
import fs from 'fs'
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'
/** 创建指定子目录的 multer 配置 */
export function createUploadConfig(subDir: string, maxFileSize = 5 * 1024 * 1024) {
const targetDir = path.resolve(UPLOAD_DIR, subDir)
// 确保目录存在
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true })
}
const storage = multer.diskStorage({
destination: (_req, _file, cb) => {
// 运行时再次确认(可能被手动删除)
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true })
}
cb(null, targetDir)
},
filename: (_req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase() || '.jpg'
const prefix = subDir === 'avatars' ? '' : 'notif_'
cb(null, `${prefix}${Date.now()}_${crypto.randomInt(1000, 9999)}${ext}`)
}
})
return multer({
storage,
limits: { fileSize: maxFileSize },
fileFilter: (_req, file, cb) => {
const allowed = ['.jpg', '.jpeg', '.png', '.webp']
const ext = path.extname(file.originalname).toLowerCase()
if (allowed.includes(ext)) {
cb(null, true)
} else {
cb(new Error('仅支持 JPEG、PNG、WebP 格式'))
}
}
})
}