diff --git a/src/components/settings/cards/DataProviderSettingsCard.vue b/src/components/settings/cards/DataProviderSettingsCard.vue index d21e6bc..1970053 100644 --- a/src/components/settings/cards/DataProviderSettingsCard.vue +++ b/src/components/settings/cards/DataProviderSettingsCard.vue @@ -90,6 +90,7 @@ import SettingsCard from "@/components/SettingsCard.vue"; import {getSetting} from "@/utils/settings"; import axios from "axios"; +import {tryWithRotation, isRotationEnabled} from "@/utils/serverRotation"; export default { name: "DataProviderSettingsCard", @@ -132,31 +133,82 @@ export default { async checkServerConnection() { this.loading = true; this.serverchecktime = new Date(); + const triedServers = []; + try { - const domain = getSetting("server.domain"); const siteKey = getSetting("server.siteKey"); - + // Prepare headers including site key if available const headers = {Accept: "application/json"}; if (siteKey) { headers["x-site-key"] = siteKey; } - const response = await axios.get(`${domain}/check`, { - method: "GET", - headers, - }); - - if (response.data.status === "success") { - this.$message.success( - "连接成功", - "服务器连接正常 延迟" + (new Date() - this.serverchecktime) + "ms" + // Use rotation for classworkscloud provider + if (isRotationEnabled()) { + const response = await tryWithRotation( + async (serverUrl) => { + const res = await axios.get(`${serverUrl}/check`, { + method: "GET", + headers, + }); + if (res.data.status !== "success") { + throw new Error("服务器响应异常"); + } + return res; + }, + { + onServerTried: ({url, status, tried}) => { + triedServers.length = 0; + triedServers.push(...tried); + } + } ); + + // Build success message with tried servers info + const latency = new Date() - this.serverchecktime; + const successServer = triedServers.find(s => s.status === "success"); + let message = `服务器连接正常 延迟${latency}ms`; + + if (triedServers.length > 1) { + const serverList = triedServers.map((s, i) => + `${i + 1}. ${s.url} (${s.status === "success" ? "成功" : "失败"})` + ).join("\n"); + message += `\n\n依次尝试的服务器:\n${serverList}`; + } else if (successServer) { + message += `\n服务器: ${successServer.url}`; + } + + this.$message.success("连接成功", message); } else { - throw new Error("服务器响应异常"); + // Standard single-server check for other providers + const domain = getSetting("server.domain"); + const response = await axios.get(`${domain}/check`, { + method: "GET", + headers, + }); + + if (response.data.status === "success") { + this.$message.success( + "连接成功", + "服务器连接正常 延迟" + (new Date() - this.serverchecktime) + "ms" + ); + } else { + throw new Error("服务器响应异常"); + } } } catch (error) { - this.$message.error("连接失败", error.message || "无法连接到服务器"); + // Build error message with tried servers info + let errorMessage = error.message || "无法连接到服务器"; + + if (triedServers.length > 0) { + const serverList = triedServers.map((s, i) => + `${i + 1}. ${s.url} (失败${s.error ? `: ${s.error}` : ""})` + ).join("\n"); + errorMessage += `\n\n依次尝试的服务器:\n${serverList}\n\n所有服务器均连接失败`; + } + + this.$message.error("连接失败", errorMessage); } finally { this.loading = false; } diff --git a/src/utils/api.js b/src/utils/api.js index 0f38cbb..d4847a2 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,5 +1,6 @@ import axios from "@/axios/axios"; import {getSetting} from "@/utils/settings"; +import {tryWithRotation, isRotationEnabled} from "@/utils/serverRotation"; // Helper function to check if provider is valid for API calls const isValidProvider = () => { @@ -33,9 +34,18 @@ export const getNamespaceInfo = async () => { throw new Error("当前数据提供者不支持此操作"); } - const serverUrl = getSetting("server.domain"); - try { + // Use rotation for classworkscloud provider + if (isRotationEnabled()) { + const response = await tryWithRotation(async (serverUrl) => { + return await axios.get(`${serverUrl}/kv/_info`, { + headers: getHeaders(), + }); + }); + return response.data; + } + + const serverUrl = getSetting("server.domain"); const response = await axios.get(`${serverUrl}/kv/_info`, { headers: getHeaders(), }); diff --git a/src/utils/dataProvider.js b/src/utils/dataProvider.js index e4df1ec..cc3d2f3 100644 --- a/src/utils/dataProvider.js +++ b/src/utils/dataProvider.js @@ -1,6 +1,7 @@ import {kvLocalProvider} from "./providers/kvLocalProvider"; import {kvServerProvider} from "./providers/kvServerProvider"; import {getSetting, setSetting} from "./settings"; +import {getEffectiveServerUrl} from "./serverRotation"; export const formatResponse = (data) => data; @@ -173,7 +174,16 @@ export default { } = options; try { - let serverUrl = getSetting("server.domain"); + const provider = getSetting("server.provider"); + let serverUrl; + + // Use effective server URL for classworkscloud provider + if (provider === "classworkscloud") { + serverUrl = getEffectiveServerUrl(); + } else { + serverUrl = getSetting("server.domain"); + } + let siteKey = getSetting("server.siteKey"); const machineId = getSetting("device.uuid"); let configured = false; @@ -200,6 +210,8 @@ export default { // 设置provider为classworkscloud setSetting("server.provider", "classworkscloud"); + // Get effective URL after setting provider + serverUrl = getEffectiveServerUrl(); } else { return formatError("云端配置无效,请检查服务器域名和设备UUID", "CONFIG_ERROR"); } diff --git a/src/utils/providers/kvServerProvider.js b/src/utils/providers/kvServerProvider.js index ba44d2f..93952d5 100644 --- a/src/utils/providers/kvServerProvider.js +++ b/src/utils/providers/kvServerProvider.js @@ -1,6 +1,7 @@ import axios from "@/axios/axios"; import {formatResponse, formatError} from "../dataProvider"; import {getSetting} from "../settings"; +import {tryWithRotation, isRotationEnabled} from "../serverRotation"; // Helper function to get request headers with kvtoken const getHeaders = () => { @@ -22,9 +23,18 @@ const getHeaders = () => { export const kvServerProvider = { async loadNamespaceInfo() { try { - // 使用 Classworks Cloud 或者用户配置的服务器域名 - const serverUrl = getSetting("server.domain"); + // Use rotation for classworkscloud provider + if (isRotationEnabled()) { + return await tryWithRotation(async (serverUrl) => { + const res = await axios.get(`${serverUrl}/kv/_info`, { + headers: getHeaders(), + }); + return formatResponse(res.data); + }); + } + // Standard single-server mode + const serverUrl = getSetting("server.domain"); const res = await axios.get(`${serverUrl}/kv/_info`, { headers: getHeaders(), }); @@ -42,8 +52,17 @@ export const kvServerProvider = { async updateNamespaceInfo(data) { try { - const serverUrl = getSetting("server.domain"); + // Use rotation for classworkscloud provider + if (isRotationEnabled()) { + return await tryWithRotation(async (serverUrl) => { + const res = await axios.put(`${serverUrl}/kv/_info`, data, { + headers: getHeaders(), + }); + return res; + }); + } + const serverUrl = getSetting("server.domain"); const res = await axios.put(`${serverUrl}/kv/_info`, data, { headers: getHeaders(), }); @@ -59,8 +78,17 @@ export const kvServerProvider = { async loadData(key) { try { - const serverUrl = getSetting("server.domain"); + // Use rotation for classworkscloud provider + if (isRotationEnabled()) { + return await tryWithRotation(async (serverUrl) => { + const res = await axios.get(`${serverUrl}/kv/${key}`, { + headers: getHeaders(), + }); + return formatResponse(res.data); + }); + } + const serverUrl = getSetting("server.domain"); const res = await axios.get(`${serverUrl}/kv/${key}`, { headers: getHeaders(), }); @@ -80,6 +108,16 @@ export const kvServerProvider = { async saveData(key, data) { try { + // Use rotation for classworkscloud provider + if (isRotationEnabled()) { + return await tryWithRotation(async (serverUrl) => { + await axios.post(`${serverUrl}/kv/${key}`, data, { + headers: getHeaders(), + }); + return formatResponse(true); + }); + } + const serverUrl = getSetting("server.domain"); await axios.post(`${serverUrl}/kv/${key}`, data, { headers: getHeaders(), @@ -117,8 +155,6 @@ export const kvServerProvider = { */ async loadKeys(options = {}) { try { - const serverUrl = getSetting("server.domain"); - // 设置默认参数 const { sortBy = "key", @@ -135,6 +171,17 @@ export const kvServerProvider = { skip: skip.toString() }); + // Use rotation for classworkscloud provider + if (isRotationEnabled()) { + return await tryWithRotation(async (serverUrl) => { + const res = await axios.get(`${serverUrl}/kv/_keys?${params}`, { + headers: getHeaders(), + }); + return formatResponse(res.data); + }); + } + + const serverUrl = getSetting("server.domain"); const res = await axios.get(`${serverUrl}/kv/_keys?${params}`, { headers: getHeaders(), }); diff --git a/src/utils/serverRotation.js b/src/utils/serverRotation.js new file mode 100644 index 0000000..834b4aa --- /dev/null +++ b/src/utils/serverRotation.js @@ -0,0 +1,97 @@ +/** + * Server rotation utility for Classworks Cloud provider + * Provides fallback mechanism across multiple server endpoints + */ + +import { getSetting } from "./settings"; + +// Server list for classworkscloud provider (in priority order) +const CLASSWORKS_CLOUD_SERVERS = [ + "https://kv-service.houlang.cloud", + "https://kv-service.wuyuan.dev", +]; + +/** + * Get the list of servers to try for the given provider + * @param {string} provider - The provider type + * @returns {string[]} Array of server URLs to try + */ +export function getServerList(provider) { + if (provider === "classworkscloud") { + return [...CLASSWORKS_CLOUD_SERVERS]; + } + + // For other providers, use the configured domain + const domain = getSetting("server.domain"); + return domain ? [domain] : []; +} + +/** + * Try an operation with server rotation fallback + * @param {Function} operation - Async function that takes a serverUrl and returns a promise + * @param {Object} options - Options + * @param {string} options.provider - Provider type (optional, defaults to current setting) + * @param {Function} options.onServerTried - Callback called when a server is tried (optional) + * @returns {Promise} Result from the first successful server, or throws the last error + */ +export async function tryWithRotation(operation, options = {}) { + const provider = options.provider || getSetting("server.provider"); + const onServerTried = options.onServerTried || (() => {}); + + const servers = getServerList(provider); + const triedServers = []; + let lastError = null; + + for (const serverUrl of servers) { + try { + triedServers.push({ url: serverUrl, status: "trying" }); + onServerTried({ url: serverUrl, status: "trying", tried: [...triedServers] }); + + const result = await operation(serverUrl); + + triedServers[triedServers.length - 1].status = "success"; + onServerTried({ url: serverUrl, status: "success", tried: [...triedServers] }); + + return result; + } catch (error) { + lastError = error; + triedServers[triedServers.length - 1].status = "failed"; + triedServers[triedServers.length - 1].error = error.message || String(error); + onServerTried({ url: serverUrl, status: "failed", error, tried: [...triedServers] }); + + // Continue to next server + console.warn(`Server ${serverUrl} failed:`, error.message); + } + } + + // All servers failed + console.error("All servers failed. Tried:", triedServers); + const error = lastError || new Error("All servers failed"); + error.triedServers = triedServers; + throw error; +} + +/** + * Get the effective server URL for the current provider + * For classworkscloud, returns the first server in the list + * For other providers, returns the configured domain + * @returns {string} Server URL + */ +export function getEffectiveServerUrl() { + const provider = getSetting("server.provider"); + + if (provider === "classworkscloud") { + return CLASSWORKS_CLOUD_SERVERS[0]; + } + + return getSetting("server.domain") || ""; +} + +/** + * Check if rotation is enabled for the current provider + * @returns {boolean} + */ +export function isRotationEnabled() { + const provider = getSetting("server.provider"); + return provider === "classworkscloud"; +} diff --git a/src/utils/socketClient.js b/src/utils/socketClient.js index f02d6a1..f0c0162 100644 --- a/src/utils/socketClient.js +++ b/src/utils/socketClient.js @@ -4,12 +4,18 @@ import {io} from 'socket.io-client'; import {getSetting} from '@/utils/settings'; +import {getEffectiveServerUrl, isRotationEnabled, tryWithRotation} from '@/utils/serverRotation'; let socket = null; let connectedDomain = null; const listeners = new Set(); export function getServerUrl() { + // For classworkscloud provider, use the effective server URL from rotation + if (isRotationEnabled()) { + return getEffectiveServerUrl(); + } + // Prefer configured server domain; fallback to env; then current origin const cfg = getSetting('server.domain'); const envUrl = import.meta?.env?.VITE_SERVER_URL; @@ -28,6 +34,10 @@ export function getSocket() { socket = null; } connectedDomain = serverUrl; + + // For classworkscloud, create socket with rotation support + // The socket will initially connect to the first server + // If connection fails, Socket.IO will handle reconnection socket = io(serverUrl, {transports: ["polling","websocket"]}); // Re-attach previously registered event handlers on new socket instance