diff --git a/app.js b/app.js index 80b1bb5..9497c45 100644 --- a/app.js +++ b/app.js @@ -48,7 +48,7 @@ app.use(logger("dev")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); // app.use(cookieParser()); -// app.use(express.static(join(__dirname, "public"))); +app.use(express.static(join(__dirname, "public"))); // 添加请求超时处理中间件 app.use((req, res, next) => { diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index 713750d..cb97e1e 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -71,10 +71,24 @@ export const authLimiter = rateLimit({ skipFailedRequests: false, // 失败的认证计入限制 }); +// 批量操作限速器(比写操作更严格) +export const batchLimiter = rateLimit({ + windowMs: 5 * 60 * 1000, // 5分钟 + limit: 5, // 每个IP在windowMs时间内最多允许5个批量操作 + standardHeaders: "draft-7", + legacyHeaders: false, + message: "批量操作请求过于频繁,请稍后再试", + keyGenerator: getClientIp, + skipSuccessfulRequests: false, + skipFailedRequests: false, +}); + // 创建一个路由处理中间件,根据HTTP方法应用不同的限速器 export const methodBasedRateLimiter = (req, res, next) => { - // 根据HTTP方法应用不同限速 - if (req.method === "GET") { + // 检查是否是批量导入路由 + if (req.method === "POST" && req.path.endsWith("/batch-import")) { + return batchLimiter(req, res, next); + } else if (req.method === "GET") { // 读操作使用普通API限速 return apiLimiter(req, res, next); } else if ( diff --git a/routes/kv.js b/routes/kv.js index 0a8ebe2..f5414e7 100644 --- a/routes/kv.js +++ b/routes/kv.js @@ -142,6 +142,58 @@ router.post( }) ); +/** + * POST /:namespace/batch-import + * 批量导入键值对到指定命名空间 + */ +router.post( + "/:namespace/import/batch-import", + checkRestrictedUUID, + errors.catchAsync(async (req, res, next) => { + const { namespace } = req.params; + const data = req.body; + + if (!data || Object.keys(data).length === 0) { + return next(errors.createError(400, "请提供有效的JSON数据,格式为 {\"key\":{}, \"key2\":{}}")); + } + + // 获取客户端IP + const creatorIp = + req.headers["x-forwarded-for"] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket?.remoteAddress || + ""; + + const results = []; + const errors = []; + + // 批量处理所有键值对 + for (const [key, value] of Object.entries(data)) { + try { + const result = await kvStore.upsert(namespace, key, value, creatorIp); + results.push({ + key: result.key, + created: result.createdAt.getTime() === result.updatedAt.getTime() + }); + } catch (error) { + errors.push({ + key, + error: error.message + }); + } + } + + return res.status(200).json({ + namespace, + total: Object.keys(data).length, + successful: results.length, + failed: errors.length, + results, + errors: errors.length > 0 ? errors : undefined + }); + }) +); /** * DELETE /:namespace * 删除指定命名空间及其所有键值对 @@ -201,4 +253,5 @@ router.get( }) ); + export default router; diff --git a/views/index.ejs b/views/index.ejs index 3bd1e03..2b163c0 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -7,7 +7,51 @@ <%= readmeValue.title || "Classworks 服务端" %> - + @@ -19,6 +63,69 @@
<%= readmeValue.readme %>
+
+

批量数据导入测试

+
+
+ + + +
+
+ + +
+ +
+
+

响应结果:

+

+    
+
+ + \ No newline at end of file