diff --git a/.gitignore b/.gitignore index 8ee54e8..60d9bdc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules .DS_Store dist +xSynergy-manage dist-ssr coverage *.local diff --git a/README.md b/README.md index b8203e3..85b0430 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,24 @@ npm run dev ```sh npm run build ``` + + +## 注意事项 + +1.在本地启动项目时,要修改签名的host,在utils/tools.js + +``` + // 获取host(从baseURL或当前域名) 本地启动要求host为xsynergy.gxtech.ltd + // let host = 'xsynergy.gxtech.ltd'; + let host = ''; + if (config.baseURL) { + try { + const urlObj = new URL(config.baseURL); + host = urlObj.host; + } catch (e) { + host = window.location.host; + } + } else { + host = window.location.host; + } +``` \ No newline at end of file diff --git a/dist.zip b/dist.zip deleted file mode 100644 index d2bc5fd..0000000 Binary files a/dist.zip and /dev/null differ diff --git a/package-lock.json b/package-lock.json index f0bb71f..4b2ae1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "@vueuse/core": "^9.5.0", "autoprefixer": "^10.4.21", "axios": "^0.27.2", + "blakejs": "^1.2.1", "code-inspector-plugin": "^0.20.12", "crypto-js": "^4.2.0", "element-plus": "^2.2.27", + "hashids": "^1.2.2", "js-cookie": "^3.0.1", "livekit-client": "^2.7.5", "mitt": "^3.0.0", @@ -2144,6 +2146,11 @@ "readable-stream": "^4.2.0" } }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==" + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", @@ -4563,6 +4570,11 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hashids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/hashids/-/hashids-1.2.2.tgz", + "integrity": "sha512-dEHCG2LraR6PNvSGxosZHIRgxF5sNLOIBFEHbj8lfP9WWmu/PWPMzsip1drdVSOFi51N2pU7gZavrgn7sbGFuw==" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", diff --git a/package.json b/package.json index 500c935..fa08cde 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,11 @@ "@vueuse/core": "^9.5.0", "autoprefixer": "^10.4.21", "axios": "^0.27.2", + "blakejs": "^1.2.1", "code-inspector-plugin": "^0.20.12", "crypto-js": "^4.2.0", "element-plus": "^2.2.27", + "hashids": "^1.2.2", "js-cookie": "^3.0.1", "livekit-client": "^2.7.5", "mitt": "^3.0.0", diff --git a/src/api/login.js b/src/api/login.js index 08c1a36..af359a2 100644 --- a/src/api/login.js +++ b/src/api/login.js @@ -1,5 +1,4 @@ -import request from '@/utils/request' -// import request from '@/views/custom/Meter/public/request.js' +import request from '@/utils/request' // 登录方法 export function login(username, password) { diff --git a/src/utils/request.js b/src/utils/request.js index ed1e73a..217b73a 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -5,6 +5,7 @@ import { ElMessage, } from "element-plus"; import { tansParams } from "@/utils/ruoyi"; +import { generateSign } from "@/utils/tools"; import cache from "@/plugins/cache"; import { getToken, removeToken } from "@/utils/auth"; import router from '@/router'; @@ -18,11 +19,11 @@ const getBaseURL = () => { if (import.meta.env.DEV) { return import.meta.env.VITE_APP_BASE_API; } - + // 生产环境使用相对路径 // 返回空字符串,让浏览器自动使用当前域名 return ''; - + // 或者如果后端 API 有固定路径前缀,可以这样设置: // return '/api'; // 这样请求会变成 https://当前域名/api/xxx }; @@ -38,21 +39,21 @@ const service = axios.create({ // request拦截器 service.interceptors.request.use( - (config) => { - // 在拦截器内部安全地使用 store - let sudid = '' - try { - const meterStore = useMeterStore() - if (!meterStore.udid) { - meterStore.initUdid(); - sudid = meterStore.getSudid(); - } else { - sudid = meterStore.getSudid(); - } - } catch (error) { - console.warn('MeterStore 初始化失败:', error); - } - + async (config) => { + // 在拦截器内部安全地使用 store + let sudid = '' + try { + const meterStore = useMeterStore() + if (!meterStore.udid) { + meterStore.initUdid(); + sudid = meterStore.getSudid(); + } else { + sudid = meterStore.getSudid(); + } + } catch (error) { + console.warn('MeterStore 初始化失败:', error); + } + // 是否需要设置 token const isToken = (config.headers || {}).isToken === false; if (getToken() && !isToken) { @@ -61,12 +62,11 @@ service.interceptors.request.use( // 是否需要防止数据重复提交 const isRepeatSubmit = (config.headers || {}).repeatSubmit === false; - if (sudid) { + if (sudid) { config.headers["X-User-Agent"] = `gxtech/web 1.0.0: c=GxTech, udid=${sudid}, sv=15.4.1, app=stt`; } // get请求映射params参数 if (config.method === "get" && config.params) { - let url = config.url + "?" + tansParams(config.params); url = url.slice(0, -1); config.params = {}; @@ -118,79 +118,179 @@ service.interceptors.request.use( } } } + console.log(sudid,'sudid') + // 生成sign并添加到URL + if (sudid) { + try { + const now = Date.now(); + const sign = await generateSign(config, sudid, now); + if (sign) { + // 将sign添加到URL + const separator = config.url.includes('?') ? '&' : '?'; + config.url += `${separator}sign=${encodeURIComponent(sign)}`; + } + } catch (signError) { + console.error('添加sign失败:', signError); + } + } + return config; }, - (error) => { + (error) => { Promise.reject(error); } ); +let isShowingError = false; +const showError = (message) => { + if (isShowingError) return; + isShowingError = true; + + ElMessage({ + message, + type: 'error', + duration: 3000 + }); + + setTimeout(() => { + isShowingError = false; + }, 2000); +}; + + + service.interceptors.response.use( - (response) => { - // 1. 检查响应是否存在 - if (!response) { - ElMessage.error('无响应数据'); - return Promise.reject(new Error('无响应数据')); - } - // 2. 安全获取响应数据和状态码 - const responseData = response.data || {}; - const statusCode = response.status; - const businessCode = responseData.meta?.code || statusCode; + (response) => { + try { + // console.log('原始响应:', response); + // 1. 检查响应是否存在 + if (!response) { + ElMessage.error('无响应数据'); + return Promise.reject(new Error('无响应数据')); + } + // 2. 安全获取响应数据和状态码 + const responseData = response.data || {}; + const statusCode = response.status; + const businessCode = responseData.meta?.code || statusCode; - // 3. 二进制数据直接返回 - if ( - response.request.responseType === 'blob' || - response.request.responseType === 'arraybuffer' - ) { - return responseData; - } + // 3. 二进制数据直接返回 + if ( + response.request.responseType === 'blob' || + response.request.responseType === 'arraybuffer' + ) { + return responseData; + } - // 4. 根据业务码处理不同情况 - switch (businessCode) { - case 200: - case 201: - return Promise.resolve(responseData); - case 401: - // return Promise.resolve(responseData); - // const currentPath = router.currentRoute.value.name; - // if(currentPath == 'ConferencingRoom'){ - // return Promise.resolve(responseData); - // }else{ + // console.log('响应数据:', responseData); + + // 4. 根据业务码处理不同情况 + switch (businessCode) { + case 200: + case 201: + return Promise.resolve(responseData); + case 401: + // return Promise.resolve(responseData); + // const currentPath = router.currentRoute.value.name; + // if(currentPath == 'ConferencingRoom'){ + // return Promise.resolve(responseData); + // }else{ return handleUnauthorized() - // .then(() => { - // return Promise.reject({ code: 401, message: '未授权' }); - // }); + // .then(() => { + // return Promise.reject({ code: 401, message: '未授权' }); + // }); // } - case 500: - const serverErrorMsg = responseData.meta?.message || '服务器内部错误'; - ElMessage({ message: serverErrorMsg, type: 'error' }); - return Promise.reject({ code: 500, message: serverErrorMsg }); + case 500: + const serverErrorMsg = responseData.meta?.message || responseData.meta?.msg || '服务器内部错误'; + ElMessage({ message: serverErrorMsg, type: 'error' }); + return Promise.reject({ code: 500, message: serverErrorMsg }); - default: - const errorMsg = responseData.meta?.message || `业务错误 (${businessCode})`; - ElNotification.error({ title: errorMsg }); - return Promise.reject({ code: businessCode, message: errorMsg }); + default: + const errorMsg = responseData.meta?.message || responseData.meta?.msg || `业务错误 (${businessCode})`; + ElNotification.error({ title: errorMsg }); + return Promise.reject({ code: businessCode, message: errorMsg }); + } + } catch (err) { + console.error('响应拦截器异常:', err); + + showError('响应处理异常'); + + return Promise.reject({ + code: -1, + message: '响应处理异常', + raw: err + }); } }, - (error) => { - let { message } = error; - let code = error?.response?.status || -1; - if (message == 'Network Error') { - message = '后端接口连接异常'; - ElMessage({ message, type: 'error', duration: 5 * 1000 }); - } else if (message.includes('timeout')) { - message = '系统接口请求超时'; - ElMessage({ message, type: 'error', duration: 5 * 1000 }); - } else if (message.includes('Request failed with status code')) { - // message = '系统接口' + message.substr(message.length - 3) + '异常'; - } + // (error) => { + // let { message } = error; + // let code = error?.response?.status || -1; + // if (message == 'Network Error') { + // message = '后端接口连接异常'; + // ElMessage({ message, type: 'error', duration: 5 * 1000 }); + // } else if (message.includes('timeout')) { + // message = '系统接口请求超时'; + // ElMessage({ message, type: 'error', duration: 5 * 1000 }); + // } else if (message.includes('Request failed with status code')) { + // // message = '系统接口' + message.substr(message.length - 3) + '异常'; + // } - // 返回结构化错误 - return Promise.reject({ - code, - message, - raw: error // 保留原始 error - }); + // // 返回结构化错误 + // return Promise.reject({ + // code, + // message, + // raw: error // 保留原始 error + // }); + // } + + (error) => { + try { + let message = error?.message || '请求失败'; + let code = error?.response?.status || -1; + if (message === 'Network Error') { + message = '后端接口连接异常'; + } else if (message.includes('timeout')) { + message = '系统接口请求超时'; + } + else if (code) { + const statusMap = { + 400: '请求参数错误', + 401: '登录已过期,请重新登录', + 403: '没有权限访问', + 404: '请求资源不存在', + 408: '请求超时,请稍后重试', + 409: '数据冲突,请刷新后重试', + 422: '参数校验失败', + + 500: '服务器异常,请联系管理员', + 501: '功能暂未实现', + 502: '服务器内部错误,请联系管理员', + 503: '服务繁忙,请稍后重试', + 504: '服务器响应超时,请稍后重试' + }; + + message = message || statusMap[code]; + + // 401 特殊处理(不影响你原逻辑) + if (code === 401) { + return handleUnauthorized(); + } + } + showError(message); + + return Promise.reject({ + code, + message, + raw: error + }); + } catch (err) { + console.error('错误拦截器异常:', err); + + return Promise.reject({ + code: -1, + message: '网络异常处理失败', + raw: err + }); + } } ); @@ -222,17 +322,17 @@ service.interceptors.response.use( // }); // } -function handleUnauthorized() { +function handleUnauthorized() { removeToken(); - + // 使用 nextTick 确保路由状态已更新 import('vue').then(({ nextTick }) => { nextTick(() => { - const currentPath = router.currentRoute.value.fullPath; + const currentPath = router.currentRoute.value.fullPath; if (router.currentRoute.value.path !== '/login') { router.push({ path: '/login', - query: { + query: { redirect: currentPath !== '/login' ? currentPath : undefined } }); diff --git a/src/utils/tools.js b/src/utils/tools.js index 426a095..bc31e22 100644 --- a/src/utils/tools.js +++ b/src/utils/tools.js @@ -1,3 +1,7 @@ +import Hashids from 'hashids'; +import blake from 'blakejs'; +import CryptoJS from 'crypto-js'; + // 获取uuid export function generateUUID() { var d = new Date().getTime(); @@ -158,3 +162,429 @@ function isValidBox(x1, y1, x2, y2, imgWidth, imgHeight) { x2 <= imgWidth && y2 <= imgHeight ); } + + +// 全局临时变量,用于存储最近一次请求的签名信息 +let lastRequestKey = ''; +let lastTimestamp = 0; +let lastUsedTimestamp = 0; +let collisionCount = 0; + +/** + * 生成请求唯一标识key + * @param {number} timestamp - 时间戳 + * @param {string} method - 请求方法 + * @param {string} urlPath - API路径 + * @returns {string} 唯一标识 + */ +function getRequestKey(timestamp, method, urlPath) { + return `${timestamp}_${method}_${urlPath}`; +} + +/** + * 获取去重后的时间戳 + * 处理同一毫秒内相同接口的重复请求 + * @param {number} originalTimestamp - 原始时间戳 + * @param {string} method - 请求方法 + * @param {string} urlPath - API路径 + * @returns {number} 处理后的时间戳 + */ +function getDeduplicatedTimestamp(originalTimestamp, method, urlPath) { + const currentKey = getRequestKey(originalTimestamp, method, urlPath); + + // 如果当前请求与上一次请求的时间戳、方法、API路径完全相同 + if (currentKey === lastRequestKey && originalTimestamp === lastTimestamp) { + // 碰撞次数增加 + collisionCount++; + // 时间戳增加碰撞次数毫秒 + const adjustedTimestamp = originalTimestamp + collisionCount; + console.warn(`检测到重复请求: ${method} ${urlPath} @ ${originalTimestamp}ms, 调整时间戳为: ${adjustedTimestamp}ms (碰撞次数: ${collisionCount})`); + return adjustedTimestamp; + } else { + // 不同请求,重置碰撞计数 + collisionCount = 0; + // 更新最后一次请求的记录 + lastRequestKey = currentKey; + lastTimestamp = originalTimestamp; + return originalTimestamp; + } +} + + +/** + * 生成sign签名 + * @param {Object} config - axios请求配置 + * @param {string} udid - 用户设备ID(22位短udid) + * @param {number} timestamp - 时间戳(毫秒) - 由调用方传入,确保与生成salt时使用的时间一致,避免因时间差导致的sign不匹配问题· + * @returns {Promise} sign值 + */ +export async function generateSign(config, udid, timestamp) { + try { + // 1. 准备模版字典 + const reqMap = new Map(); + + // 获取请求方法 + const method = config.method?.toUpperCase() || 'GET'; + + // 获取host(从baseURL或当前域名) 本地启动要求host为xsynergy.gxtech.ltd + let host = 'xsynergy.gxtech.ltd'; + // let host = ''; + // // console.log('请求配置:', config.baseURL) + // if (config.baseURL) { + // try { + // const urlObj = new URL(config.baseURL); + // // console.log('解析baseURL成功:', urlObj); + // host = urlObj.host; + // } catch (e) { + // // console.log('解析baseURL失败,使用window.location.host:', window.location); + // host = window.location.host; + // } + // } else { + // host = window.location.host; + // } + + // console.log('请求host:', host); + + // 获取完整URL路径(不含域名和查询参数) + let urlPath = config.url || ''; + // 移除baseURL部分 + if (config.baseURL && urlPath.startsWith(config.baseURL)) { + urlPath = urlPath.substring(config.baseURL.length); + } + // 移除查询参数 + const queryIndex = urlPath.indexOf('?'); + if (queryIndex !== -1) { + urlPath = urlPath.substring(0, queryIndex); + } + + // 获取user-agent和token + const userAgent = config.headers?.['X-User-Agent']; + // const userAgent = config.headers?.['X-User-Agent'] || navigator.userAgent; + const token = config.headers?.['Authorization'] || ''; + + // 填充基础模板 - agent字段包含userAgent和token + reqMap.set('host', [host]); + reqMap.set('method', [method]); + reqMap.set('agent', [userAgent, token]); + reqMap.set('uri', [urlPath]); + + // 2. 处理URL查询参数(不包括sign本身) + let fullUrl = config.url || ''; + if (config.baseURL && !fullUrl.startsWith('http')) { + fullUrl = config.baseURL + fullUrl; + } + + try { + const urlObj = new URL(fullUrl, window.location.origin); + const searchParams = urlObj.searchParams; + + // 遍历所有查询参数,排除sign + for (const [key, value] of searchParams.entries()) { + if (key !== 'sign') { + // 处理数组参数(key[]格式) + const normalizedKey = key.replace('[]', ''); + if (reqMap.has(normalizedKey)) { + reqMap.get(normalizedKey).push(value); + } else { + reqMap.set(normalizedKey, [value]); + } + } + } + } catch (e) { + console.warn('解析URL参数失败:', e); + } + + // 3. 处理GET请求的params参数 + if (config.method?.toLowerCase() === 'get' && config.params) { + for (const [key, value] of Object.entries(config.params)) { + if (key !== 'sign') { + const strValue = String(value); + const normalizedKey = key.replace('[]', ''); + if (reqMap.has(normalizedKey)) { + reqMap.get(normalizedKey).push(strValue); + } else { + reqMap.set(normalizedKey, [strValue]); + } + } + } + } + + // 4. 处理请求体(POST、PUT等) + if (config.method?.toLowerCase() !== 'get' && config.data) { + const contentType = config.headers?.['Content-Type'] || ''; + + if (contentType.includes('application/json')) { + // JSON数据 - 后端用"json"作为key + const jsonStr = typeof config.data === 'object' + ? JSON.stringify(config.data) + : String(config.data); + reqMap.set('json', [jsonStr]); + + } else if (contentType.includes('application/x-www-form-urlencoded')) { + // form-urlencoded数据 + let formData = {}; + if (typeof config.data === 'object') { + formData = config.data; + } else if (typeof config.data === 'string') { + const params = new URLSearchParams(config.data); + formData = Object.fromEntries(params.entries()); + } + + // 添加每个表单字段到reqMap + for (const [key, value] of Object.entries(formData)) { + const strValue = String(value); + const normalizedKey = key.replace('[]', ''); + if (reqMap.has(normalizedKey)) { + reqMap.get(normalizedKey).push(strValue); + } else { + reqMap.set(normalizedKey, [strValue]); + } + } + + } else if (contentType.includes('multipart/form-data')) { + // multipart/form-data + if (config.data instanceof FormData) { + for (const [key, value] of config.data.entries()) { + if (!(value instanceof File || value instanceof Blob)) { + const strValue = String(value); + const normalizedKey = key.replace('[]', ''); + if (reqMap.has(normalizedKey)) { + reqMap.get(normalizedKey).push(strValue); + } else { + reqMap.set(normalizedKey, [strValue]); + } + } + } + } + } + } + + // 5. 生成salt - 完全匹配后端的genSalt函数 + // 后端: GenTokenSalt(uid, udid + signSalt) + // signSalt固定为: publicSignSalt + const signSalt = 'publicSignSalt'; + const salt = await generateSalt(udid, signSalt); + + // 6. 参数排序并进行HMAC-MD5哈希 + const sortedKeys = Array.from(reqMap.keys()).sort(); + // console.log('排序后的参数键:', sortedKeys); + + // 创建HMAC-MD5 + const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.MD5, salt); + + // 按排序后的key拼接值 + let contentStr = ''; + for (const key of sortedKeys) { + const values = reqMap.get(key); + const joinedValue = values.join(''); + contentStr += joinedValue; + hmac.update(joinedValue); + } + + // 获取最终的hash字符串 + const hashStr = hmac.finalize().toString(CryptoJS.enc.Hex).toUpperCase(); + + // 7. 分段拆散计算 + const ints = new Array(6).fill(0); + + // 拆封模版 + const parts = [ + [0, 7], + [7, 7], + [14, 4], + [18, 7], + [25, 7] + ]; + + // 使用模版将hashStr拆为5个hex字串并转为整数,再倒序排列 + for (let i = 0; i < parts.length; i++) { + // console.log(`拆分hashStr - part ${i}:`, hashStr.substring(parts[i][0], parts[i][0] + parts[i][1])); + const [start, length] = parts[i]; + const hexStr = hashStr.substring(start, start + length); + const intValue = parseInt(hexStr, 16); + // console.log(`转换为整数 - part ${i}:`, intValue); + ints[4 - i] = intValue; + } + + // 8. 获取调用接口时间戳(毫秒)并进行去重处理 + const now = timestamp || Date.now(); + + // 对时间戳进行去重处理(处理同一毫秒内相同接口的重复请求) + const adjustedTimestamp = getDeduplicatedTimestamp(now, method, urlPath); + const ts = adjustedTimestamp.toString(); + + // 将时间拆分为两部分 + const t0 = parseInt(ts.substring(0, ts.length - 9), 10); + const t1 = parseInt(ts.substring(ts.length - 9), 10); + + ints[2] += t0; + ints[5] = t1; + + // 记录实际使用的时间戳,用于后续的重复检测 + lastUsedTimestamp = adjustedTimestamp; + + // console.log('时间戳:', now, '调整后:', adjustedTimestamp, '拆分:', { t0, t1 }); + + // 9. 使用hashids算法加密 + const alphabet = "GxJRk37QAe51FCsPW92uEOyq4Bg6Sp8YzVTmnU0liwDdHXLajZrfNhobIcMvKt"; + const hashids = new Hashids(salt, 0, alphabet); + + // 将ints数组编码为字符串 + const sign = hashids.encode(ints); + + // 打印调试信息 + // console.log('=== Sign生成调试信息 ==='); + // console.log('udid:', udid); + // console.log('signSalt:', signSalt); + // console.log('生成的salt:', salt); + // console.log('参数映射:', Object.fromEntries(reqMap)); + // console.log('排序后的keys:', sortedKeys); + // console.log('拼接字符串:', contentStr); + // console.log('HMAC-MD5哈希:', hashStr); + // console.log('分割后的ints:', ints); + // console.log('原始时间戳:', now, '调整后时间戳:', adjustedTimestamp, '拆分:', { t0, t1 }); + // console.log('最终sign:', sign); + // console.log('========================'); + + return sign; + + } catch (error) { + console.error('生成sign失败:', error); + return ''; + } +} + +/** + * 重置去重状态(可选,用于测试或特殊场景) + */ +export function resetSignDeduplication() { + lastRequestKey = ''; + lastTimestamp = 0; + lastUsedTimestamp = 0; + collisionCount = 0; + console.log('Sign去重状态已重置'); +} + + +/** + * 生成salt - 匹配后端的genSalt函数 + * 后端逻辑: GenTokenSalt(uid, udid + signSalt) + * + * @param {string} shortUdid - 22位短udid + * @param {string} signSalt - 固定的signSalt值 'xspotter-s3' + * @returns {Promise} salt值 + */ +async function generateSalt(shortUdid, signSalt) { + try { + // 1. 将22位短udid解压为完整UUID + const fullUdid = await unpackUUID(shortUdid); + + // 2. 解析UUID并提取每个部分的第一个字符 + // UUID格式: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + const uuidParts = fullUdid.split('-'); + if (uuidParts.length !== 5) { + throw new Error('Invalid UUID format'); + } + + // 提取每个部分的第一个字符组成hexStr + // 后端: for i, item := range strings.Split(udidStr, "-") { hexStr += string(item[i]) } + let hexStr = ''; + for (let i = 0; i < uuidParts.length; i++) { + if (uuidParts[i].length > i) { + hexStr += uuidParts[i][i]; // 注意:取第i个字符,不是第一个 + } else { + hexStr += uuidParts[i][0]; // 如果长度不够,取第一个 + } + } + + // 3. 将hexStr解析为整数(16进制) + const baseInt = parseInt(hexStr, 16); + + // 4. 转换为8进制字符串 + const base8Str = baseInt.toString(8); + + // 5. secret = udid + signSalt + const secret = shortUdid + signSalt; + + // 6. 返回 base8 + secret + const result = base8Str + secret; + + return result; + + } catch (error) { + console.error('生成salt失败:', error); + throw error; + } +} + + +/** + * 将22位短udid解压为完整UUID + * 匹配后端的unpackUUID函数 + */ +async function unpackUUID(shortUdid) { + try { + // 后端逻辑: shortUdid + "==" 然后base64解码 + const base64Str = shortUdid + '=='; + + // 将url安全的base64转换为标准base64 + const standardBase64 = base64Str.replace(/-/g, '+').replace(/_/g, '/'); + + // base64解码 + const binaryStr = atob(standardBase64); + + // 转换为16进制格式 + const hex = []; + for (let i = 0; i < binaryStr.length; i++) { + const hexByte = binaryStr.charCodeAt(i).toString(16).padStart(2, '0'); + hex.push(hexByte); + } + + // 格式化为UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + const hexStr = hex.join(''); + const uuid = `${hexStr.substring(0, 8)}-${hexStr.substring(8, 12)}-${hexStr.substring(12, 16)}-${hexStr.substring(16, 20)}-${hexStr.substring(20, 32)}`; + + return uuid; + + } catch (error) { + console.error('解压UUID失败:', error); + // 如果已经是完整UUID格式,直接返回 + if (shortUdid.includes('-') && shortUdid.length === 36) { + return shortUdid; + } + throw error; + } +} + +/** + * 将完整UUID压缩为22位短udid + * 匹配后端的packUDID函数 + */ +export function packUUID(fullUuid) { + try { + // 移除UUID中的连字符 + const hexStr = fullUuid.replace(/-/g, ''); + + // 将16进制字符串转换为字节数组 + const bytes = []; + for (let i = 0; i < hexStr.length; i += 2) { + bytes.push(parseInt(hexStr.substring(i, i + 2), 16)); + } + + // 转换为二进制字符串 + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + + // base64编码并转换为url安全格式 + const base64 = btoa(binary); + const base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + + return base64url; + + } catch (error) { + console.error('压缩UUID失败:', error); + return fullUuid; + } +} \ No newline at end of file diff --git a/src/views/login.vue b/src/views/login.vue index 69ef7bd..c011980 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -39,39 +39,20 @@ -