chore: update backend/src/index.js

This commit is contained in:
2026-04-29 02:08:44 +08:00
parent f8dbcbd111
commit 9ad03e70ce
+137 -19
View File
@@ -26,16 +26,39 @@ const prisma = new PrismaClient()
// 中间件配置 // 中间件配置
// CORS:MVP 阶段允许所有来源,生产环境需限制为前端域名 // CORS:MVP 阶段允许所有来源,生产环境需限制为前端域名
app.use(cors()) app.use(cors())
// 请求体解析:限制 JSON 大小防 DoS,生产环境建议设置 limit // 请求体解析:限制 JSON 大小防 DoS(1MB 上限),生产环境建议根据实际需求调整
app.use(express.json()) app.use(express.json({ limit: '1mb' }))
// ============================================================================ // ============================================================================
// 工具函数:统一响应格式 // 工具函数:统一响应格式、安全校验
// ============================================================================ // ============================================================================
/**
* 类型字段枚举校验 - 防止非法值入库
* @param {String} value - 待校验的类型值
* @param {Array<String>} allowedValues - 允许的值列表
* @returns {Boolean} - 是否合法
* 说明:用于 type、accountType 等枚举字段校验,避免非法值污染数据库
*/
const isValidEnum = (value, allowedValues) => allowedValues.includes(value)
/**
* 字符串长度校验 - 防止过长数据导致性能问题或溢出
* @param {String} str - 待校验的字符串
* @param {Number} maxLength - 最大长度
* @returns {Boolean} - 是否合法
*/
const isValidLength = (str, maxLength) => typeof str === 'string' && str.length <= maxLength
/**
* NaN 安全检查 - 确保类型转换后为有效数字
* @param {Number} num - 类型转换后的数字
* @returns {Boolean} - 是否为有效数字(非 NaN、非 Infinity
*/
const isValidNumber = (num) => typeof num === 'number' && !isNaN(num) && isFinite(num)
/** /**
* 成功响应格式化 - 统一成功响应结构 * 成功响应格式化 - 统一成功响应结构
* @param {Response} res - Express 响应对象
* @param {Object} data - 业务数据 * @param {Object} data - 业务数据
* @param {String} message - 提示信息 * @param {String} message - 提示信息
* 说明:所有成功接口统一返回 { success: true, data, message },前端可据此做统一拦截 * 说明:所有成功接口统一返回 { success: true, data, message },前端可据此做统一拦截
@@ -162,13 +185,27 @@ app.post('/api/accounts', async (req, res) => {
if (!userId || !name || !type) { if (!userId || !name || !type) {
return errorResponse(res, 'userId、name、type为必填字段') return errorResponse(res, 'userId、name、type为必填字段')
} }
// 类型字段枚举校验:防止非法值入库
if (!isValidEnum(type, ['payment', 'bank', 'cash'])) {
return errorResponse(res, 'type必须为payment/bank/cash之一')
}
// 字符串长度校验
if (!isValidLength(name, 50)) {
return errorResponse(res, 'name长度不能超过50个字符')
}
const parsedUserId = parseInt(userId)
const parsedBalance = parseFloat(balance)
// NaN 安全检查
if (!isValidNumber(parsedUserId) || !isValidNumber(parsedBalance)) {
return errorResponse(res, 'userId和balance必须为有效数字')
}
const account = await prisma.account.create({ const account = await prisma.account.create({
data: { data: {
userId: parseInt(userId), userId: parsedUserId,
name, name,
type, type,
color: color || '#1890FF', color: color || '#1890FF',
balance: parseFloat(balance) balance: parsedBalance
} }
}) })
successResponse(res, account, '账户创建成功') successResponse(res, account, '账户创建成功')
@@ -184,10 +221,26 @@ app.put('/api/accounts/:id', async (req, res) => {
const { id } = req.params const { id } = req.params
const { name, type, color, balance } = req.body const { name, type, color, balance } = req.body
const data = {} const data = {}
if (name !== undefined) data.name = name if (name !== undefined) {
if (type !== undefined) data.type = type if (!isValidLength(name, 50)) {
return errorResponse(res, 'name长度不能超过50个字符')
}
data.name = name
}
if (type !== undefined) {
if (!isValidEnum(type, ['payment', 'bank', 'cash'])) {
return errorResponse(res, 'type必须为payment/bank/cash之一')
}
data.type = type
}
if (color !== undefined) data.color = color if (color !== undefined) data.color = color
if (balance !== undefined) data.balance = parseFloat(balance) if (balance !== undefined) {
const parsedBalance = parseFloat(balance)
if (!isValidNumber(parsedBalance)) {
return errorResponse(res, 'balance必须为有效数字')
}
data.balance = parsedBalance
}
const account = await prisma.account.update({ const account = await prisma.account.update({
where: { id: parseInt(id) }, where: { id: parseInt(id) },
@@ -195,7 +248,6 @@ app.put('/api/accounts/:id', async (req, res) => {
}) })
successResponse(res, account, '账户更新成功') successResponse(res, account, '账户更新成功')
} catch (error) { } catch (error) {
// P2025: Prisma 记录不存在错误
if (error.code === 'P2025') { if (error.code === 'P2025') {
return errorResponse(res, '账户不存在', 404) return errorResponse(res, '账户不存在', 404)
} }
@@ -312,17 +364,35 @@ app.post('/api/records', async (req, res) => {
if (!userId || !accountId || !type || !amount || !category) { if (!userId || !accountId || !type || !amount || !category) {
return errorResponse(res, '必填字段缺失') return errorResponse(res, '必填字段缺失')
} }
// 类型字段枚举校验:防止非法值入库
if (!isValidEnum(type, ['income', 'expense'])) {
return errorResponse(res, 'type必须为income或expense')
}
// 字符串长度校验
if (!isValidLength(category, 50)) {
return errorResponse(res, 'category长度不能超过50个字符')
}
if (description && !isValidLength(description, 200)) {
return errorResponse(res, 'description长度不能超过200个字符')
}
// 金额合法性校验:防止 0 或负数入账 // 金额合法性校验:防止 0 或负数入账
if (parseFloat(amount) <= 0) { const parsedAmount = parseFloat(amount)
const parsedUserId = parseInt(userId)
const parsedAccountId = parseInt(accountId)
// NaN 安全检查
if (!isValidNumber(parsedAmount) || parsedAmount <= 0) {
return errorResponse(res, '金额必须大于0') return errorResponse(res, '金额必须大于0')
} }
if (!isValidNumber(parsedUserId) || !isValidNumber(parsedAccountId)) {
return errorResponse(res, 'userId和accountId必须为有效数字')
}
const recordData = { const recordData = {
userId: parseInt(userId), userId: parsedUserId,
accountId: parseInt(accountId), accountId: parsedAccountId,
type, type,
amount: parseFloat(amount), amount: parsedAmount,
category, category,
description, description,
date: parseDate(date) // 使用安全日期解析,避免时区偏移 date: parseDate(date) // 使用安全日期解析,避免时区偏移
@@ -373,6 +443,25 @@ app.put('/api/records/:id', async (req, res) => {
const { id } = req.params const { id } = req.params
const { type, amount, category, description, date } = req.body const { type, amount, category, description, date } = req.body
// 类型字段枚举校验
if (type !== undefined && !isValidEnum(type, ['income', 'expense'])) {
return errorResponse(res, 'type必须为income或expense')
}
// 字符串长度校验
if (category !== undefined && !isValidLength(category, 50)) {
return errorResponse(res, 'category长度不能超过50个字符')
}
if (description !== undefined && !isValidLength(description, 200)) {
return errorResponse(res, 'description长度不能超过200个字符')
}
// NaN 安全检查
if (amount !== undefined) {
const parsedAmount = parseFloat(amount)
if (!isValidNumber(parsedAmount) || parsedAmount <= 0) {
return errorResponse(res, 'amount必须为有效数字且大于0')
}
}
const updatedRecord = await prisma.$transaction(async (tx) => { const updatedRecord = await prisma.$transaction(async (tx) => {
// 1. 获取原记录,用于余额冲销 // 1. 获取原记录,用于余额冲销
const oldRecord = await tx.record.findUnique({ const oldRecord = await tx.record.findUnique({
@@ -532,11 +621,24 @@ app.post('/api/budgets', async (req, res) => {
if (!userId || !category || !amount || !month) { if (!userId || !category || !amount || !month) {
return errorResponse(res, '必填字段缺失') return errorResponse(res, '必填字段缺失')
} }
// 字符串长度校验
if (!isValidLength(category, 50)) {
return errorResponse(res, 'category长度不能超过50个字符')
}
if (!isValidLength(month, 7)) {
return errorResponse(res, 'month格式错误,应为YYYY-MM')
}
const parsedUserId = parseInt(userId)
const parsedAmount = parseFloat(amount)
// NaN 安全检查
if (!isValidNumber(parsedUserId) || !isValidNumber(parsedAmount) || parsedAmount <= 0) {
return errorResponse(res, 'userId和amount必须为有效数字且amount大于0')
}
const budget = await prisma.budget.create({ const budget = await prisma.budget.create({
data: { data: {
userId: parseInt(userId), userId: parsedUserId,
category, category,
amount: parseFloat(amount), amount: parsedAmount,
month month
} }
}) })
@@ -552,9 +654,25 @@ app.put('/api/budgets/:id', async (req, res) => {
const { id } = req.params const { id } = req.params
const { category, amount, month } = req.body const { category, amount, month } = req.body
const data = {} const data = {}
if (category !== undefined) data.category = category if (category !== undefined) {
if (amount !== undefined) data.amount = parseFloat(amount) if (!isValidLength(category, 50)) {
if (month !== undefined) data.month = month return errorResponse(res, 'category长度不能超过50个字符')
}
data.category = category
}
if (amount !== undefined) {
const parsedAmount = parseFloat(amount)
if (!isValidNumber(parsedAmount) || parsedAmount <= 0) {
return errorResponse(res, 'amount必须为有效数字且大于0')
}
data.amount = parsedAmount
}
if (month !== undefined) {
if (!isValidLength(month, 7)) {
return errorResponse(res, 'month格式错误,应为YYYY-MM')
}
data.month = month
}
const budget = await prisma.budget.update({ const budget = await prisma.budget.update({
where: { id: parseInt(id) }, where: { id: parseInt(id) },