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:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
45
server/src/utils/upload.ts
Normal file
45
server/src/utils/upload.ts
Normal 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 格式'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user