提交新的移动端框架
This commit is contained in:
1 parent
134f711349
commit
9bda3717fe
75 files changed
+5283
-9011
No files matched your search
@@ -0,0 +1,72 @@
|
||||
// 移动端配置管理 - 统一管理密钥和请求配置
|
||||
export const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致
|
||||
RSA_PUBLIC_KEY:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
// RSA私钥 - 与web端配置文档保持一致
|
||||
RSA_PRIVATE_KEY:
|
||||
'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=',
|
||||
// 客户端ID
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
// 租户ID
|
||||
TENANT_ID: '000000',
|
||||
// 加密请求头
|
||||
ENCRYPT_HEADER: 'encrypt-key',
|
||||
// 是否启用加密
|
||||
ENCRYPT_ENABLED: true,
|
||||
};
|
||||
|
||||
// API请求配置
|
||||
export const REQUEST_CONFIG = {
|
||||
// 基础URL配置
|
||||
baseURL: process.env.NODE_ENV === 'development' ? '/api' : 'https://your-api-domain.com',
|
||||
// 请求超时时间
|
||||
timeout: 300000, // 5分钟,适合大数据量导出
|
||||
// 客户端ID
|
||||
clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
// 默认referer
|
||||
referer: 'http://localhost/login?redirect=/index',
|
||||
// 是否启用加密
|
||||
encryptEnabled: true,
|
||||
// 防重复提交间隔时间(ms)
|
||||
repeatSubmitInterval: 500,
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新加密配置
|
||||
* @param {Object} config 新的配置对象
|
||||
*/
|
||||
export const updateEncryptConfig = (config) => {
|
||||
Object.assign(ENCRYPT_CONFIG, config);
|
||||
console.log('🔧 加密配置已更新:', ENCRYPT_CONFIG);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新请求配置
|
||||
* @param {Object} config 新的配置对象
|
||||
*/
|
||||
export const updateRequestConfig = (config) => {
|
||||
Object.assign(REQUEST_CONFIG, config);
|
||||
console.log('🔧 请求配置已更新:', REQUEST_CONFIG);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前配置摘要
|
||||
* @returns {Object} 配置摘要
|
||||
*/
|
||||
export const getConfigSummary = () => {
|
||||
return {
|
||||
hasPublicKey: !!ENCRYPT_CONFIG.RSA_PUBLIC_KEY,
|
||||
hasPrivateKey: !!ENCRYPT_CONFIG.RSA_PRIVATE_KEY,
|
||||
publicKeyLength: ENCRYPT_CONFIG.RSA_PUBLIC_KEY ? ENCRYPT_CONFIG.RSA_PUBLIC_KEY.length : 0,
|
||||
privateKeyLength: ENCRYPT_CONFIG.RSA_PRIVATE_KEY
|
||||
? ENCRYPT_CONFIG.RSA_PRIVATE_KEY.length
|
||||
: 0,
|
||||
clientId: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
tenantId: ENCRYPT_CONFIG.TENANT_ID,
|
||||
encryptHeader: ENCRYPT_CONFIG.ENCRYPT_HEADER,
|
||||
encryptEnabled: ENCRYPT_CONFIG.ENCRYPT_ENABLED,
|
||||
baseURL: REQUEST_CONFIG.baseURL,
|
||||
timeout: REQUEST_CONFIG.timeout,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,265 @@
|
||||
// 移动端加密解密工具 - 与mobile工程保持一致
|
||||
import CryptoJS from 'crypto-js';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
import { ENCRYPT_CONFIG, updateEncryptConfig } from './config.js';
|
||||
|
||||
/**
|
||||
* 随机生成32位的字符串
|
||||
* @returns {string}
|
||||
*/
|
||||
const generateRandomString = () => {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
const charactersLength = characters.length;
|
||||
for (let i = 0; i < 32; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 随机生成AES密钥
|
||||
* @returns {CryptoJS.lib.WordArray}
|
||||
*/
|
||||
export const generateAesKey = () => {
|
||||
return CryptoJS.enc.Utf8.parse(generateRandomString());
|
||||
};
|
||||
|
||||
/**
|
||||
* 加密base64
|
||||
* @param {CryptoJS.lib.WordArray} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export const encryptBase64 = (str) => {
|
||||
return CryptoJS.enc.Base64.stringify(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解密base64
|
||||
* @param {string} str
|
||||
* @returns {CryptoJS.lib.WordArray}
|
||||
*/
|
||||
export const decryptBase64 = (str) => {
|
||||
return CryptoJS.enc.Base64.parse(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用密钥对数据进行AES加密
|
||||
* @param {string} message 待加密的数据
|
||||
* @param {CryptoJS.lib.WordArray} aesKey AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export const encryptWithAes = (message, aesKey) => {
|
||||
const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用密钥对数据进行AES解密
|
||||
* @param {string} message 待解密的数据
|
||||
* @param {CryptoJS.lib.WordArray} aesKey AES密钥
|
||||
* @returns {string} 解密后的数据
|
||||
*/
|
||||
export const decryptWithAes = (message, aesKey) => {
|
||||
const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||
};
|
||||
|
||||
/**
|
||||
* RSA加密
|
||||
* @param {string} txt 待加密的文本
|
||||
* @returns {string|false} 加密后的数据
|
||||
*/
|
||||
export const encrypt = (txt) => {
|
||||
try {
|
||||
console.log('🔐 开始RSA加密,输入文本长度:', txt ? txt.length : 'null');
|
||||
console.log('🔐 使用的RSA公钥:', ENCRYPT_CONFIG.RSA_PUBLIC_KEY.substring(0, 20) + '...');
|
||||
|
||||
const encryptor = new JSEncrypt();
|
||||
|
||||
// 构造完整的PEM格式RSA公钥
|
||||
const publicKey = `-----BEGIN PUBLIC KEY-----
|
||||
${ENCRYPT_CONFIG.RSA_PUBLIC_KEY}
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
console.log('🔐 构造的完整公钥:', publicKey.substring(0, 50) + '...');
|
||||
|
||||
encryptor.setPublicKey(publicKey);
|
||||
console.log('🔐 公钥设置完成');
|
||||
|
||||
const result = encryptor.encrypt(txt);
|
||||
|
||||
if (result) {
|
||||
console.log('✅ RSA加密成功,结果长度:', result.length);
|
||||
console.log('✅ 加密结果前50位:', result.substring(0, 50) + '...');
|
||||
} else {
|
||||
console.error('❌ RSA加密返回空结果');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ RSA加密失败:', error);
|
||||
console.error('❌ 错误堆栈:', error.stack);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* RSA解密
|
||||
* @param {string} txt 待解密的文本
|
||||
* @returns {string|false} 解密后的数据
|
||||
*/
|
||||
export const decrypt = (txt) => {
|
||||
try {
|
||||
console.log('🔓 开始RSA解密,输入文本长度:', txt ? txt.length : 'null');
|
||||
console.log('🔓 使用的RSA私钥:', ENCRYPT_CONFIG.RSA_PRIVATE_KEY.substring(0, 20) + '...');
|
||||
|
||||
if (!ENCRYPT_CONFIG.RSA_PRIVATE_KEY) {
|
||||
console.warn('❌ RSA私钥未配置,无法解密');
|
||||
return false;
|
||||
}
|
||||
|
||||
const encryptor = new JSEncrypt();
|
||||
|
||||
// 构造完整的PEM格式RSA私钥
|
||||
const privateKey = `-----BEGIN PRIVATE KEY-----
|
||||
${ENCRYPT_CONFIG.RSA_PRIVATE_KEY}
|
||||
-----END PRIVATE KEY-----`;
|
||||
|
||||
console.log('🔓 构造的完整私钥:', privateKey.substring(0, 50) + '...');
|
||||
|
||||
encryptor.setPrivateKey(privateKey);
|
||||
console.log('🔓 私钥设置完成');
|
||||
|
||||
const result = encryptor.decrypt(txt);
|
||||
|
||||
if (result) {
|
||||
console.log('✅ RSA解密成功,结果长度:', result.length);
|
||||
console.log('✅ 解密结果前50位:', result.substring(0, 50) + '...');
|
||||
} else {
|
||||
console.error('❌ RSA解密返回空结果');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ RSA解密失败:', error);
|
||||
console.error('❌ 错误堆栈:', error.stack);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机字符串AES密钥(字符串格式)
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥字符串
|
||||
*/
|
||||
export const generateRandomAESKey = (length = 32) => {
|
||||
return generateRandomString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用字符串密钥进行AES加密(兼容原有函数)
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥字符串
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export const encryptWithAES = (data, key) => {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用字符串密钥进行AES解密(兼容原有函数)
|
||||
* @param {string} data 待解密的数据
|
||||
* @param {string} key AES密钥字符串
|
||||
* @returns {string} 解密后的数据
|
||||
*/
|
||||
export const decryptWithAES = (data, key) => {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const decrypted = CryptoJS.AES.decrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证RSA密钥对配置
|
||||
* @returns {boolean} 验证结果
|
||||
*/
|
||||
export const validateKeyPair = () => {
|
||||
try {
|
||||
console.log('🔍 开始验证RSA密钥对配置...');
|
||||
|
||||
// 检查公钥和私钥是否存在
|
||||
if (!ENCRYPT_CONFIG.RSA_PUBLIC_KEY) {
|
||||
console.error('❌ RSA公钥未配置');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ENCRYPT_CONFIG.RSA_PRIVATE_KEY) {
|
||||
console.error('❌ RSA私钥未配置');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 测试加密解密功能
|
||||
const testMessage = 'RSA密钥对验证测试消息';
|
||||
console.log('🧪 测试消息:', testMessage);
|
||||
|
||||
// 使用公钥加密
|
||||
const encrypted = encrypt(testMessage);
|
||||
if (!encrypted) {
|
||||
console.error('❌ RSA加密测试失败');
|
||||
return false;
|
||||
}
|
||||
console.log('✅ RSA加密测试成功');
|
||||
|
||||
// 使用私钥解密
|
||||
const decrypted = decrypt(encrypted);
|
||||
if (!decrypted || decrypted !== testMessage) {
|
||||
console.error('❌ RSA解密测试失败');
|
||||
console.error('原始消息:', testMessage);
|
||||
console.error('解密结果:', decrypted);
|
||||
return false;
|
||||
}
|
||||
console.log('✅ RSA解密测试成功');
|
||||
|
||||
console.log('🎉 RSA密钥对配置验证通过!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ RSA密钥对验证过程中出现错误:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前加密配置摘要
|
||||
* @returns {Object} 配置摘要
|
||||
*/
|
||||
export const getConfigSummary = () => {
|
||||
return {
|
||||
hasPublicKey: !!ENCRYPT_CONFIG.RSA_PUBLIC_KEY,
|
||||
hasPrivateKey: !!ENCRYPT_CONFIG.RSA_PRIVATE_KEY,
|
||||
publicKeyLength: ENCRYPT_CONFIG.RSA_PUBLIC_KEY ? ENCRYPT_CONFIG.RSA_PUBLIC_KEY.length : 0,
|
||||
privateKeyLength: ENCRYPT_CONFIG.RSA_PRIVATE_KEY
|
||||
? ENCRYPT_CONFIG.RSA_PRIVATE_KEY.length
|
||||
: 0,
|
||||
clientId: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
tenantId: ENCRYPT_CONFIG.TENANT_ID,
|
||||
encryptHeader: ENCRYPT_CONFIG.ENCRYPT_HEADER,
|
||||
encryptEnabled: ENCRYPT_CONFIG.ENCRYPT_ENABLED,
|
||||
};
|
||||
};
|
||||
|
||||
// 导出配置更新方法
|
||||
export { updateEncryptConfig };
|
||||
@@ -0,0 +1,394 @@
|
||||
// 移动端统一请求工具 - 与mobile工程保持一致
|
||||
import {
|
||||
generateAesKey,
|
||||
encryptBase64,
|
||||
decryptBase64,
|
||||
encryptWithAes,
|
||||
decryptWithAes,
|
||||
encrypt,
|
||||
decrypt,
|
||||
} from './crypto.js';
|
||||
import { ENCRYPT_CONFIG, REQUEST_CONFIG } from './config.js';
|
||||
import { sessionCache, userStorage } from './storage.js';
|
||||
|
||||
/**
|
||||
* 获取token
|
||||
*/
|
||||
function getToken() {
|
||||
try {
|
||||
const token = uni.getStorageSync('access_token');
|
||||
console.log(
|
||||
'🔍 获取Token:',
|
||||
token ? `Bearer ${token.substring(0, 20)}...` : '❌ Token为空',
|
||||
);
|
||||
return token ? `Bearer ${token}` : '';
|
||||
} catch (e) {
|
||||
console.error('❌ 获取Token失败:', e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理401未授权响应
|
||||
*/
|
||||
function handleUnauthorized(message = '登录已失效,请重新登录') {
|
||||
console.log('🔒 处理401未授权响应:', message);
|
||||
|
||||
// 清除登录信息
|
||||
try {
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
console.log('📤 已清除本地登录信息');
|
||||
} catch (e) {
|
||||
console.error('❌ 清除登录信息失败:', e);
|
||||
}
|
||||
|
||||
// 显示提示信息
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
// 延迟跳转,确保Toast显示
|
||||
setTimeout(() => {
|
||||
// 获取当前页面路径
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const currentPath = '/' + currentPage.route;
|
||||
|
||||
// 如果当前不是登录页,则跳转到登录页
|
||||
if (currentPath !== '/pages/login/index') {
|
||||
console.log('🔄 从', currentPath, '跳转到登录页');
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
fail: (error) => {
|
||||
console.error('❌ 跳转登录页失败:', error);
|
||||
// 如果reLaunch失败,尝试其他方式
|
||||
uni.redirectTo({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一请求方法
|
||||
*/
|
||||
export const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 获取token
|
||||
const token = getToken();
|
||||
|
||||
// 处理请求配置
|
||||
const config = {
|
||||
url: REQUEST_CONFIG.baseURL + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
timeout: options.timeout || REQUEST_CONFIG.timeout,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
authorization: token,
|
||||
clientid: options.clientId || REQUEST_CONFIG.clientId,
|
||||
// 注意:在H5环境中,浏览器会自动设置referer,手动设置会报错
|
||||
// referer: options.referer || REQUEST_CONFIG.referer,
|
||||
...options.header,
|
||||
},
|
||||
};
|
||||
|
||||
// 检查特殊header配置
|
||||
const isToken = options.header?.isToken === false;
|
||||
const isRepeatSubmit = options.header?.repeatSubmit === false;
|
||||
const isEncrypt = options.header?.isEncrypt === 'true';
|
||||
|
||||
// 如果不需要token,移除authorization
|
||||
if (isToken) {
|
||||
delete config.header.authorization;
|
||||
}
|
||||
|
||||
// 防重复提交检查
|
||||
if (!isRepeatSubmit && (config.method === 'POST' || config.method === 'PUT')) {
|
||||
const requestObj = {
|
||||
url: config.url,
|
||||
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
|
||||
time: new Date().getTime(),
|
||||
};
|
||||
|
||||
const sessionObj = sessionCache.get('sessionObj');
|
||||
if (sessionObj) {
|
||||
const s_url = sessionObj.url;
|
||||
const s_data = sessionObj.data;
|
||||
const s_time = sessionObj.time;
|
||||
const interval = REQUEST_CONFIG.repeatSubmitInterval;
|
||||
|
||||
if (
|
||||
s_data === requestObj.data &&
|
||||
requestObj.time - s_time < interval &&
|
||||
s_url === requestObj.url
|
||||
) {
|
||||
const message = '数据正在处理,请勿重复提交';
|
||||
console.warn(`[${s_url}]: ` + message);
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
});
|
||||
return reject(new Error(message));
|
||||
}
|
||||
}
|
||||
sessionCache.set('sessionObj', requestObj);
|
||||
}
|
||||
|
||||
// 加密处理
|
||||
if (
|
||||
REQUEST_CONFIG.encryptEnabled &&
|
||||
isEncrypt &&
|
||||
(config.method === 'POST' || config.method === 'PUT')
|
||||
) {
|
||||
try {
|
||||
console.log('🔐 开始请求加密处理...');
|
||||
|
||||
// 生成AES密钥
|
||||
const aesKey = generateAesKey();
|
||||
|
||||
// 使用RSA加密AES密钥
|
||||
const encryptedKey = encrypt(encryptBase64(aesKey));
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
|
||||
// 设置加密头
|
||||
config.header[ENCRYPT_CONFIG.ENCRYPT_HEADER] = encryptedKey;
|
||||
|
||||
// 加密请求数据
|
||||
const dataToEncrypt =
|
||||
typeof config.data === 'object' ? JSON.stringify(config.data) : config.data;
|
||||
config.data = encryptWithAes(dataToEncrypt, aesKey);
|
||||
|
||||
console.log('✅ 请求数据加密完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 请求加密失败:', error);
|
||||
uni.showToast({
|
||||
title: '请求加密失败',
|
||||
icon: 'none',
|
||||
});
|
||||
return reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// FormData处理
|
||||
if (config.data instanceof globalThis.FormData) {
|
||||
delete config.header['Content-Type'];
|
||||
}
|
||||
|
||||
console.log('📤 发送请求:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
hasToken: !!token,
|
||||
isEncrypt: isEncrypt,
|
||||
});
|
||||
|
||||
// 发送请求
|
||||
uni.request({
|
||||
...config,
|
||||
success: (res) => {
|
||||
console.log('📥 收到响应:', {
|
||||
statusCode: res.statusCode,
|
||||
url: config.url,
|
||||
hasEncryptHeader: !!res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER],
|
||||
});
|
||||
|
||||
// 解密处理
|
||||
if (REQUEST_CONFIG.encryptEnabled && res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER]) {
|
||||
try {
|
||||
console.log('🔓 开始响应解密处理...');
|
||||
|
||||
// 获取加密的AES密钥
|
||||
const keyStr = res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER];
|
||||
|
||||
// RSA解密获取AES密钥
|
||||
const base64Str = decrypt(keyStr);
|
||||
if (!base64Str) {
|
||||
throw new Error('RSA解密AES密钥失败');
|
||||
}
|
||||
|
||||
// Base64解码获取AES密钥
|
||||
const aesKey = decryptBase64(base64Str);
|
||||
|
||||
// AES解密响应数据
|
||||
const decryptedData = decryptWithAes(res.data, aesKey);
|
||||
|
||||
// 解析JSON
|
||||
res.data = JSON.parse(decryptedData);
|
||||
|
||||
console.log('✅ 响应数据解密完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 响应解密失败:', error);
|
||||
uni.showToast({
|
||||
title: '响应解密失败',
|
||||
icon: 'none',
|
||||
});
|
||||
return reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理响应
|
||||
if (res.statusCode === 200) {
|
||||
// 检查业务状态码
|
||||
if (res.data && res.data.code !== undefined) {
|
||||
if (res.data.code === 200) {
|
||||
resolve(res.data);
|
||||
} else if (res.data.code === 401) {
|
||||
// 401未授权,跳转到登录页
|
||||
console.log('🔒 Token失效,自动跳转到登录页');
|
||||
handleUnauthorized(res.data.msg || '登录已失效,请重新登录');
|
||||
reject(res.data);
|
||||
} else {
|
||||
// 其他业务错误
|
||||
const errorMsg = res.data.msg || '请求失败';
|
||||
if (res.data.code !== 404) {
|
||||
// 404错误静默处理
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
reject(res.data);
|
||||
}
|
||||
} else {
|
||||
resolve(res.data);
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
// HTTP 401未授权
|
||||
console.log('🔒 HTTP 401未授权,自动跳转到登录页');
|
||||
handleUnauthorized('登录已失效,请重新登录');
|
||||
reject(res);
|
||||
} else if (res.statusCode === 404) {
|
||||
// 404错误静默处理
|
||||
console.log('404错误被静默处理,不显示用户提示');
|
||||
reject(res);
|
||||
} else {
|
||||
// 其他HTTP状态码错误
|
||||
const errorMsg = `请求失败: ${res.statusCode}`;
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
});
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('❌ 网络请求失败:', error);
|
||||
let message = '网络请求失败';
|
||||
|
||||
if (error.errMsg) {
|
||||
if (error.errMsg.includes('timeout')) {
|
||||
message = '请求超时,请检查网络连接';
|
||||
} else if (error.errMsg.includes('fail')) {
|
||||
message = '网络连接失败,请检查网络';
|
||||
}
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
});
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// API方法封装
|
||||
export const api = {
|
||||
// 用户登录 - 使用统一的request方法和加密拦截器
|
||||
login: (data) => {
|
||||
console.log('🔐 开始登录流程...');
|
||||
|
||||
return request({
|
||||
url: '/auth/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
tenantId: data.tenantId || '000000',
|
||||
clientId: data.clientId || REQUEST_CONFIG.clientId,
|
||||
grantType: data.grantType || 'password',
|
||||
},
|
||||
header: {
|
||||
isToken: false, // 登录接口不需要token
|
||||
isEncrypt: 'true', // 启用加密
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo: () => {
|
||||
const token = getToken();
|
||||
console.log(
|
||||
'🎫 获取用户信息,当前token:',
|
||||
token ? token.substring(0, 20) + '...' : '无token',
|
||||
);
|
||||
|
||||
return request({
|
||||
url: '/system/user/getInfo',
|
||||
method: 'GET',
|
||||
});
|
||||
},
|
||||
|
||||
// 获取租户列表
|
||||
getTenantList: (isToken = false) =>
|
||||
request({
|
||||
url: '/auth/tenant/list',
|
||||
method: 'GET',
|
||||
header: {
|
||||
isToken: isToken,
|
||||
},
|
||||
}),
|
||||
|
||||
// 注销登录
|
||||
logout: () =>
|
||||
request({
|
||||
url: '/auth/logout',
|
||||
method: 'POST',
|
||||
}),
|
||||
};
|
||||
|
||||
// 通用下载方法
|
||||
export const download = (url, params, fileName) => {
|
||||
// 显示加载提示
|
||||
uni.showLoading({
|
||||
title: '正在下载数据,请稍候',
|
||||
mask: true,
|
||||
});
|
||||
|
||||
return request({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
data: params,
|
||||
header: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
timeout: 1200000, // 20分钟超时
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
.then((resp) => {
|
||||
// 处理下载响应
|
||||
// 注意:uni-app中文件下载需要特殊处理
|
||||
console.log('文件下载完成:', fileName);
|
||||
uni.hideLoading();
|
||||
|
||||
// 这里需要根据平台进行不同的文件保存处理
|
||||
// 具体实现可能需要平台特定的代码
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('下载失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '下载文件出现错误,请联系管理员!',
|
||||
icon: 'none',
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,267 @@
|
||||
// 用户存储管理工具 - 与mobile工程保持一致
|
||||
|
||||
// 会话缓存工具
|
||||
export const sessionCache = {
|
||||
set: (key, value) => {
|
||||
try {
|
||||
uni.setStorageSync(`session_${key}`, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
console.error('设置会话缓存失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
get: (key) => {
|
||||
try {
|
||||
const data = uni.getStorageSync(`session_${key}`);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (e) {
|
||||
console.error('获取会话缓存失败:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
remove: (key) => {
|
||||
try {
|
||||
uni.removeStorageSync(`session_${key}`);
|
||||
} catch (e) {
|
||||
console.error('删除会话缓存失败:', e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 用户信息缓存工具
|
||||
export const userStorage = {
|
||||
// 保存登录信息
|
||||
saveLoginInfo: (loginResult, userInfo) => {
|
||||
try {
|
||||
// 保存token
|
||||
uni.setStorageSync('access_token', loginResult.access_token);
|
||||
|
||||
// 保存用户信息(增加登录时间)
|
||||
if (userInfo) {
|
||||
const enhancedUserInfo = {
|
||||
...userInfo,
|
||||
loginTime: new Date().toISOString(),
|
||||
tokenExpireTime: loginResult.expires_in
|
||||
? new Date(Date.now() + loginResult.expires_in * 1000).toISOString()
|
||||
: null,
|
||||
};
|
||||
uni.setStorageSync('userInfo', JSON.stringify(enhancedUserInfo));
|
||||
}
|
||||
|
||||
// 保存登录状态
|
||||
uni.setStorageSync('isLoggedIn', true);
|
||||
|
||||
// 保存其他token信息
|
||||
if (loginResult.refresh_token) {
|
||||
uni.setStorageSync('refresh_token', loginResult.refresh_token);
|
||||
}
|
||||
|
||||
console.log('用户登录信息已保存', {
|
||||
hasToken: !!loginResult.access_token,
|
||||
hasUserInfo: !!userInfo,
|
||||
loginTime: new Date().toLocaleString(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('保存登录信息失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo: () => {
|
||||
try {
|
||||
const userInfo = uni.getStorageSync('userInfo');
|
||||
return userInfo ? JSON.parse(userInfo) : null;
|
||||
} catch (e) {
|
||||
console.error('获取用户信息失败:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取token
|
||||
getToken: () => {
|
||||
try {
|
||||
return uni.getStorageSync('access_token') || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
isLoggedIn: () => {
|
||||
try {
|
||||
const isLoggedIn = uni.getStorageSync('isLoggedIn');
|
||||
const token = uni.getStorageSync('access_token');
|
||||
return !!(isLoggedIn && token);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// 清除登录信息
|
||||
clearLoginInfo: () => {
|
||||
try {
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
console.log('用户登录信息已清除');
|
||||
} catch (e) {
|
||||
console.error('清除登录信息失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 清除所有用户相关数据(包括refresh_token等)
|
||||
clearAll: () => {
|
||||
try {
|
||||
// 清除所有登录相关的存储
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
uni.removeStorageSync('refresh_token');
|
||||
|
||||
// 清除可能存在的其他用户相关数据
|
||||
uni.removeStorageSync('userPermissions');
|
||||
uni.removeStorageSync('userRoles');
|
||||
uni.removeStorageSync('tenantInfo');
|
||||
|
||||
console.log('所有用户相关数据已清除');
|
||||
} catch (e) {
|
||||
console.error('清除所有用户数据失败:', e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 数据缓存工具
|
||||
export const storage = {
|
||||
// 设置缓存
|
||||
set: (key, value, expire) => {
|
||||
try {
|
||||
const data = {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
expire: expire ? Date.now() + expire * 1000 : null,
|
||||
};
|
||||
uni.setStorageSync(key, JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.error('缓存设置失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取缓存
|
||||
get: (key) => {
|
||||
try {
|
||||
const data = uni.getStorageSync(key);
|
||||
if (!data) return null;
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
// 检查是否过期
|
||||
if (parsed.expire && Date.now() > parsed.expire) {
|
||||
uni.removeStorageSync(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed.value;
|
||||
} catch (e) {
|
||||
console.error('缓存获取失败:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除缓存
|
||||
remove: (key) => {
|
||||
try {
|
||||
uni.removeStorageSync(key);
|
||||
} catch (e) {
|
||||
console.error('缓存删除失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 清空缓存
|
||||
clear: () => {
|
||||
try {
|
||||
uni.clearStorageSync();
|
||||
} catch (e) {
|
||||
console.error('缓存清空失败:', e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Token测试工具
|
||||
export const tokenTest = {
|
||||
// 测试Token存储
|
||||
testTokenStorage: () => {
|
||||
console.log('🧪 开始Token存储测试...');
|
||||
|
||||
try {
|
||||
// 1. 清除现有Token
|
||||
uni.removeStorageSync('access_token');
|
||||
console.log('🗑️ 已清除现有Token');
|
||||
|
||||
// 2. 测试存储Token
|
||||
const testToken =
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJzeXNfdXNlcjoxIn0.test';
|
||||
uni.setStorageSync('access_token', testToken);
|
||||
console.log('💾 已存储测试Token');
|
||||
|
||||
// 3. 测试获取Token
|
||||
const retrievedToken = uni.getStorageSync('access_token');
|
||||
console.log(
|
||||
'📖 获取到的Token:',
|
||||
retrievedToken ? retrievedToken.substring(0, 30) + '...' : '❌ 为空',
|
||||
);
|
||||
|
||||
// 4. 验证Token是否一致
|
||||
if (retrievedToken === testToken) {
|
||||
console.log('✅ Token存储和获取测试通过');
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ Token存储和获取测试失败');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Token测试失败:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取当前存储的Token信息
|
||||
getCurrentTokenInfo: () => {
|
||||
try {
|
||||
const token = uni.getStorageSync('access_token');
|
||||
const userInfo = uni.getStorageSync('userInfo');
|
||||
const isLoggedIn = uni.getStorageSync('isLoggedIn');
|
||||
|
||||
console.log('📊 当前存储状态:');
|
||||
console.log(' 🎫 Token:', token ? `存在 (${token.length}字符)` : '❌ 不存在');
|
||||
console.log(' 👤 用户信息:', userInfo ? '存在' : '❌ 不存在');
|
||||
console.log(' 🔐 登录状态:', isLoggedIn ? '已登录' : '❌ 未登录');
|
||||
|
||||
return {
|
||||
hasToken: !!token,
|
||||
tokenLength: token ? token.length : 0,
|
||||
hasUserInfo: !!userInfo,
|
||||
isLoggedIn: !!isLoggedIn,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ 获取Token信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 清除所有登录信息
|
||||
clearAllLoginInfo: () => {
|
||||
try {
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
uni.removeStorageSync('refresh_token');
|
||||
|
||||
console.log('🗑️ 已清除所有登录信息');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 清除登录信息失败:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
import { request } from './request.js';
|
||||
|
||||
// 工作流API配置
|
||||
export const workflowApi = {
|
||||
// 获取流程实例分页列表(我发起的)
|
||||
pageByCurrent: (params) =>
|
||||
request({
|
||||
url: '/workflow/instance/pageByCurrent',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 删除流程实例
|
||||
deleteByInstanceIds: (instanceIds) =>
|
||||
request({
|
||||
url: '/workflow/instance/deleteByInstanceIds',
|
||||
method: 'DELETE',
|
||||
data: instanceIds,
|
||||
}),
|
||||
// 撤销流程申请
|
||||
cancelProcessApply: (data) =>
|
||||
request({
|
||||
url: '/workflow/instance/cancelProcessApply',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
// 获取流程分类树
|
||||
categoryTree: () =>
|
||||
request({
|
||||
url: '/workflow/category/categoryTree',
|
||||
method: 'GET',
|
||||
}),
|
||||
// 获取我的待办任务
|
||||
todoTasks: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/todoTasks',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 获取我的已办任务
|
||||
finishedTasks: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/finishedTasks',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 获取我的抄送任务
|
||||
copyTasks: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/copyTasks',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 获取全部待办任务(对应web端allTaskWaiting.vue)
|
||||
pageByAllTaskWait: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/pageByAllTaskWait',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 获取全部已办任务(对应web端allTaskWaiting.vue)
|
||||
pageByAllTaskFinish: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/pageByAllTaskFinish',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 获取我的已办任务(对应web端taskFinish.vue)
|
||||
pageByTaskFinish: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/pageByTaskFinish',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 获取我的抄送任务(对应web端taskCopyList.vue)
|
||||
pageByTaskCopy: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/pageByTaskCopy',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 修改办理人
|
||||
updateAssignee: (taskIds, assigneeId) =>
|
||||
request({
|
||||
url: '/workflow/task/updateAssignee',
|
||||
method: 'POST',
|
||||
data: { taskIds, assigneeId },
|
||||
}),
|
||||
// 启动流程实例
|
||||
startProcessInstance: (data) =>
|
||||
request({
|
||||
url: '/workflow/instance/start',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
// 办理任务
|
||||
complete: (data) =>
|
||||
request({
|
||||
url: '/workflow/task/complete',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
// 获取流程历史
|
||||
processHistory: (instanceId) =>
|
||||
request({
|
||||
url: `/workflow/instance/history/${instanceId}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
// 获取流程实例详情(根据业务ID)
|
||||
processDetail: (businessId) =>
|
||||
request({
|
||||
url: `/workflow/instance/getInfo/${businessId}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
};
|
||||
|
||||
// 工作流状态映射
|
||||
export const workflowStatus = {
|
||||
draft: { label: '草稿', color: '#909399', icon: '📝' },
|
||||
waiting: { label: '待审批', color: '#e6a23c', icon: '⏰' },
|
||||
complete: { label: '已完成', color: '#67c23a', icon: '✅' },
|
||||
cancel: { label: '已取消', color: '#f56c6c', icon: '❌' },
|
||||
back: { label: '已退回', color: '#409eff', icon: '↩️' },
|
||||
suspend: { label: '挂起', color: '#909399', icon: '⏸️' },
|
||||
terminate: { label: '终止', color: '#f56c6c', icon: '🛑' },
|
||||
};
|
||||
|
||||
// 工作流分类图标映射
|
||||
export const categoryIcons = {
|
||||
leave: '📅',
|
||||
repair: '🔧',
|
||||
purchase: '🛒',
|
||||
reimbursement: '💰',
|
||||
approval: '📋',
|
||||
default: '📄',
|
||||
};
|
||||
|
||||
// 获取状态样式
|
||||
export const getStatusStyle = (status) => workflowStatus[status] || workflowStatus.draft;
|
||||
|
||||
// 获取分类图标
|
||||
export const getCategoryIcon = (category) => categoryIcons[category] || categoryIcons.default;
|
||||
|
||||
// 工作流路由跳转方法
|
||||
export const routerJump = (routerJumpVo) => {
|
||||
const { businessId, taskId, type, formCustom, formPath } = routerJumpVo;
|
||||
const params = { businessId, taskId, type, formCustom, formPath };
|
||||
const queryString = Object.keys(params)
|
||||
.filter((key) => params[key] !== undefined && params[key] !== null)
|
||||
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
||||
.join('&');
|
||||
uni.navigateTo({
|
||||
url: `/pages/workflow/process?${queryString}`,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 格式化时间显示
|
||||
export const formatTime = (timeStr) => {
|
||||
if (!timeStr) return '';
|
||||
const time = new Date(timeStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - time.getTime();
|
||||
if (diff < 60 * 1000) {
|
||||
return '刚刚';
|
||||
}
|
||||
if (diff < 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 1000))}分钟前`;
|
||||
}
|
||||
if (diff < 24 * 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 60 * 1000))}小时前`;
|
||||
}
|
||||
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`;
|
||||
}
|
||||
return time.toLocaleDateString();
|
||||
};
|
||||
Reference in new issue
Block a user