1
1
mirror of https://github.com/ZeroCatDev/ClassworksKV.git synced 2025-12-11 00:53:09 +00:00

Compare commits

..

No commits in common. "main" and "v1.3.6" have entirely different histories.
main ... v1.3.6

10 changed files with 79 additions and 9230 deletions

8
.idea/.gitignore generated vendored
View File

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/FixClassworksKV.iml" filepath="$PROJECT_DIR$/.idea/FixClassworksKV.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

4
app.js
View File

@ -25,10 +25,6 @@ app.use(
cors({
exposedHeaders: ["ratelimit-policy", "retry-after", "ratelimit"], // 告诉浏览器这些响应头可以暴露
maxAge: 86400, // 设置OPTIONS请求的结果缓存24小时(86400秒),减少预检请求
credentials: true, // 允许跨域请求携带凭证
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept"], // 允许的请求头
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], // 允许的HTTP方法
withCredentials: true, // 允许携带cookie等凭证信息
})
);
app.disable("x-powered-by");

9032
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "ClassworksKV",
"version": "1.3.8",
"version": "1.3.6",
"private": true,
"scripts": {
"start": "node ./bin/www",

View File

@ -298,25 +298,43 @@ router.post(
req.connection.socket?.remoteAddress ||
"";
// 使用优化的批量upsert方法
const { results, errors: errorList } = await kvStore.batchUpsert(deviceId, data, creatorIp);
const results = [];
const errorList = [];
// 批量处理所有键值对
for (const [key, value] of Object.entries(data)) {
try {
const result = await kvStore.upsert(deviceId, key, value, creatorIp);
results.push({
key: result.key,
created: result.createdAt.getTime() === result.updatedAt.getTime(),
});
// 广播每个键的变更
const uuid = res.locals.device?.uuid;
if (uuid) {
broadcastKeyChanged(uuid, {
key: result.key,
action: "upsert",
created: result.createdAt.getTime() === result.updatedAt.getTime(),
updatedAt: result.updatedAt,
batch: true,
});
}
} catch (error) {
errorList.push({
key,
error: error.message,
});
}
}
return res.status(200).json({
code: 200,
message: "批量导入成功",
data: {
deviceId,
summary: {
total: Object.keys(data).length,
successful: results.length,
failed: errorList.length,
},
results: results.map(r => ({
key: r.key,
isNew: r.created,
})),
...(errorList.length > 0 && { errors: errorList }),
},
results,
errors: errorList.length > 0 ? errorList : undefined,
});
})
);
@ -336,20 +354,10 @@ router.post(
const deviceId = res.locals.deviceId;
const {key} = req.params;
let value = req.body;
const value = req.body;
// 处理空值,转换为空对象
if (value === null || value === undefined || value === '') {
value = {};
}
// 验证是否能被 JSON 序列化
try {
JSON.stringify(value);
} catch (error) {
return next(
errors.createError(400, "无效的数据格式")
);
if (!value || Object.keys(value).length === 0) {
return next(errors.createError(400, "请提供有效的JSON值"));
}
// 获取客户端IP

View File

@ -102,62 +102,6 @@ class KVStore {
};
}
/**
* 批量创建或更新键值对优化性能
* @param {number} deviceId - 设备ID
* @param {object} data - 键值对数据 {key1: value1, key2: value2, ...}
* @param {string} creatorIp - 创建者IP可选
* @returns {object} {results: Array, errors: Array}
*/
async batchUpsert(deviceId, data, creatorIp = "") {
const results = [];
const errors = [];
// 使用事务处理所有操作
await prisma.$transaction(async (tx) => {
for (const [key, value] of Object.entries(data)) {
try {
const item = await tx.kVStore.upsert({
where: {
deviceId_key: {
deviceId: deviceId,
key: key,
},
},
update: {
value,
...(creatorIp && {creatorIp}),
},
create: {
deviceId: deviceId,
key: key,
value,
creatorIp,
},
});
results.push({
key: item.key,
created: item.createdAt.getTime() === item.updatedAt.getTime(),
createdAt: item.createdAt,
updatedAt: item.updatedAt,
});
} catch (error) {
errors.push({
key,
error: error.message,
});
}
}
});
// 在事务完成后,一次性更新指标
const totalKeys = await prisma.kVStore.count();
keysTotal.set(totalKeys);
return { results, errors };
}
/**
* 通过设备ID和键名删除
* @param {number} deviceId - 设备ID
@ -275,37 +219,6 @@ class KVStore {
});
return count;
}
/**
* 获取指定设备的统计信息
* @param {number} deviceId - 设备ID
* @returns {object} 统计信息
*/
async getStats(deviceId) {
const [totalKeys, oldestKey, newestKey] = await Promise.all([
prisma.kVStore.count({
where: { deviceId },
}),
prisma.kVStore.findFirst({
where: { deviceId },
orderBy: { createdAt: "asc" },
select: { createdAt: true, key: true },
}),
prisma.kVStore.findFirst({
where: { deviceId },
orderBy: { updatedAt: "desc" },
select: { updatedAt: true, key: true },
}),
]);
return {
totalKeys,
oldestKey: oldestKey?.key,
oldestCreatedAt: oldestKey?.createdAt,
newestKey: newestKey?.key,
newestUpdatedAt: newestKey?.updatedAt,
};
}
}
export default new KVStore();

View File

@ -96,16 +96,14 @@ function detectDeviceName(userAgent, headers = {}) {
export function initSocket(server) {
if (io) return io;
const allowOrigin = process.env.FRONTEND_URL || "*";
io = new Server(server, {
cors: {
origin: "*",
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
allowedHeaders: ["*"],
credentials: false
origin: allowOrigin,
methods: ["GET", "POST"],
credentials: true,
},
// 传输方式回退策略优先使用WebSocket,回退到轮询
transports: ["polling", "websocket"],
});
io.on("connection", (socket) => {
@ -290,10 +288,10 @@ export function initSocket(server) {
// 清理socket相关缓存
if (socket.data.currentToken) {
// 如果这是该token的最后一个连接,考虑清理缓存
// 如果这是该token的最后一个连接考虑清理缓存
const tokenSet = onlineTokens.get(socket.data.currentToken);
if (!tokenSet || tokenSet.size === 0) {
// 可以选择保留缓存一段时间,这里暂时保留
// 可以选择保留缓存一段时间这里暂时保留
// tokenInfoCache.delete(socket.data.currentToken);
}
}