chore: update backend/src/index.js
This commit is contained in:
+137
-19
@@ -26,16 +26,39 @@ const prisma = new PrismaClient()
|
||||
// 中间件配置
|
||||
// CORS:MVP 阶段允许所有来源,生产环境需限制为前端域名
|
||||
app.use(cors())
|
||||
// 请求体解析:限制 JSON 大小防 DoS,生产环境建议设置 limit
|
||||
app.use(express.json())
|
||||
// 请求体解析:限制 JSON 大小防 DoS(1MB 上限),生产环境建议根据实际需求调整
|
||||
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 {String} message - 提示信息
|
||||
* 说明:所有成功接口统一返回 { success: true, data, message },前端可据此做统一拦截
|
||||
@@ -162,13 +185,27 @@ app.post('/api/accounts', async (req, res) => {
|
||||
if (!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({
|
||||
data: {
|
||||
userId: parseInt(userId),
|
||||
userId: parsedUserId,
|
||||
name,
|
||||
type,
|
||||
color: color || '#1890FF',
|
||||
balance: parseFloat(balance)
|
||||
balance: parsedBalance
|
||||
}
|
||||
})
|
||||
successResponse(res, account, '账户创建成功')
|
||||
@@ -184,10 +221,26 @@ app.put('/api/accounts/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { name, type, color, balance } = req.body
|
||||
const data = {}
|
||||
if (name !== undefined) data.name = name
|
||||
if (type !== undefined) data.type = type
|
||||
if (name !== undefined) {
|
||||
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 (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({
|
||||
where: { id: parseInt(id) },
|
||||
@@ -195,7 +248,6 @@ app.put('/api/accounts/:id', async (req, res) => {
|
||||
})
|
||||
successResponse(res, account, '账户更新成功')
|
||||
} catch (error) {
|
||||
// P2025: Prisma 记录不存在错误
|
||||
if (error.code === 'P2025') {
|
||||
return errorResponse(res, '账户不存在', 404)
|
||||
}
|
||||
@@ -312,17 +364,35 @@ app.post('/api/records', async (req, res) => {
|
||||
if (!userId || !accountId || !type || !amount || !category) {
|
||||
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 或负数入账
|
||||
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')
|
||||
}
|
||||
if (!isValidNumber(parsedUserId) || !isValidNumber(parsedAccountId)) {
|
||||
return errorResponse(res, 'userId和accountId必须为有效数字')
|
||||
}
|
||||
|
||||
const recordData = {
|
||||
userId: parseInt(userId),
|
||||
accountId: parseInt(accountId),
|
||||
userId: parsedUserId,
|
||||
accountId: parsedAccountId,
|
||||
type,
|
||||
amount: parseFloat(amount),
|
||||
amount: parsedAmount,
|
||||
category,
|
||||
description,
|
||||
date: parseDate(date) // 使用安全日期解析,避免时区偏移
|
||||
@@ -373,6 +443,25 @@ app.put('/api/records/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
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) => {
|
||||
// 1. 获取原记录,用于余额冲销
|
||||
const oldRecord = await tx.record.findUnique({
|
||||
@@ -532,11 +621,24 @@ app.post('/api/budgets', async (req, res) => {
|
||||
if (!userId || !category || !amount || !month) {
|
||||
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({
|
||||
data: {
|
||||
userId: parseInt(userId),
|
||||
userId: parsedUserId,
|
||||
category,
|
||||
amount: parseFloat(amount),
|
||||
amount: parsedAmount,
|
||||
month
|
||||
}
|
||||
})
|
||||
@@ -552,9 +654,25 @@ app.put('/api/budgets/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { category, amount, month } = req.body
|
||||
const data = {}
|
||||
if (category !== undefined) data.category = category
|
||||
if (amount !== undefined) data.amount = parseFloat(amount)
|
||||
if (month !== undefined) data.month = month
|
||||
if (category !== undefined) {
|
||||
if (!isValidLength(category, 50)) {
|
||||
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({
|
||||
where: { id: parseInt(id) },
|
||||
|
||||
Reference in New Issue
Block a user