request.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  1. // 移动端统一请求工具 - 与mobile工程保持一致
  2. import {
  3. generateAesKey,
  4. encryptBase64,
  5. decryptBase64,
  6. encryptWithAes,
  7. decryptWithAes,
  8. encrypt,
  9. decrypt,
  10. } from './crypto.js';
  11. import { ENCRYPT_CONFIG, REQUEST_CONFIG } from './config.js';
  12. import { sessionCache, userStorage } from './storage.js';
  13. /**
  14. * 获取token
  15. */
  16. function getToken() {
  17. try {
  18. const token = uni.getStorageSync('access_token');
  19. console.log(
  20. '🔍 获取Token:',
  21. token ? `Bearer ${token.substring(0, 20)}...` : '❌ Token为空',
  22. );
  23. return token ? `Bearer ${token}` : '';
  24. } catch (e) {
  25. console.error('❌ 获取Token失败:', e);
  26. return '';
  27. }
  28. }
  29. /**
  30. * 处理401未授权响应
  31. */
  32. function handleUnauthorized(message = '登录已失效,请重新登录') {
  33. console.log('🔒 处理401未授权响应:', message);
  34. // 获取当前页面路径
  35. const pages = getCurrentPages();
  36. const currentPage = pages[pages.length - 1];
  37. const currentPath = '/' + currentPage.route;
  38. // 如果是新生报名页面,不跳转到登录页,只是静默处理
  39. if (currentPath === '/pages/enrollment/index') {
  40. console.log('📋 新生报名页面401错误,使用兜底数据,不跳转登录页');
  41. return;
  42. }
  43. // 清除登录信息
  44. try {
  45. uni.removeStorageSync('access_token');
  46. uni.removeStorageSync('userInfo');
  47. uni.removeStorageSync('isLoggedIn');
  48. console.log('📤 已清除本地登录信息');
  49. } catch (e) {
  50. console.error('❌ 清除登录信息失败:', e);
  51. }
  52. // 显示提示信息
  53. uni.showToast({
  54. title: message,
  55. icon: 'none',
  56. duration: 2000,
  57. });
  58. // 延迟跳转,确保Toast显示
  59. setTimeout(() => {
  60. // 如果当前不是登录页,则跳转到登录页
  61. if (currentPath !== '/pages/login/index') {
  62. console.log('🔄 从', currentPath, '跳转到登录页');
  63. uni.reLaunch({
  64. url: '/pages/login/index',
  65. fail: (error) => {
  66. console.error('❌ 跳转登录页失败:', error);
  67. // 如果reLaunch失败,尝试其他方式
  68. uni.redirectTo({
  69. url: '/pages/login/index',
  70. });
  71. },
  72. });
  73. }
  74. }, 500);
  75. }
  76. /**
  77. * 统一请求方法
  78. */
  79. export const request = (options) => {
  80. return new Promise((resolve, reject) => {
  81. // 获取token
  82. const token = getToken();
  83. // 处理GET请求的params参数
  84. let url = REQUEST_CONFIG.baseURL + options.url;
  85. if (options.params && Object.keys(options.params).length > 0) {
  86. // 将params对象转换为查询字符串
  87. const queryString = Object.keys(options.params)
  88. .filter((key) => options.params[key] !== undefined && options.params[key] !== null)
  89. .map(
  90. (key) =>
  91. `${encodeURIComponent(key)}=${encodeURIComponent(options.params[key])}`,
  92. )
  93. .join('&');
  94. if (queryString) {
  95. url += (url.indexOf('?') > -1 ? '&' : '?') + queryString;
  96. }
  97. console.log('🔍 处理GET请求params:', {
  98. 原始params: options.params,
  99. 查询字符串: queryString,
  100. 最终URL: url,
  101. });
  102. }
  103. // 处理请求配置
  104. const config = {
  105. url: url,
  106. method: options.method || 'GET',
  107. data: options.data || {},
  108. timeout: options.timeout || REQUEST_CONFIG.timeout,
  109. responseType: options.responseType || 'text', // 支持responseType参数
  110. header: {
  111. // 吴自龙 注释 get方法,后台不接受json 20250811
  112. // 'Content-Type': 'application/json',
  113. authorization: token,
  114. clientid: options.clientId || REQUEST_CONFIG.clientId,
  115. // 注意:在H5环境中,浏览器会自动设置referer,手动设置会报错
  116. // referer: options.referer || REQUEST_CONFIG.referer,
  117. ...options.header,
  118. },
  119. };
  120. // 检查特殊header配置
  121. const isToken = options.header?.isToken === false;
  122. const isRepeatSubmit = options.header?.repeatSubmit === false;
  123. const isEncrypt = options.header?.isEncrypt === 'true';
  124. // 如果不需要token,移除authorization
  125. if (isToken) {
  126. delete config.header.authorization;
  127. }
  128. // 防重复提交检查
  129. if (!isRepeatSubmit && (config.method === 'POST' || config.method === 'PUT')) {
  130. const requestObj = {
  131. url: config.url,
  132. data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
  133. time: new Date().getTime(),
  134. };
  135. const sessionObj = sessionCache.get('sessionObj');
  136. if (sessionObj) {
  137. const s_url = sessionObj.url;
  138. const s_data = sessionObj.data;
  139. const s_time = sessionObj.time;
  140. const interval = REQUEST_CONFIG.repeatSubmitInterval;
  141. if (
  142. s_data === requestObj.data &&
  143. requestObj.time - s_time < interval &&
  144. s_url === requestObj.url
  145. ) {
  146. const message = '数据正在处理,请勿重复提交';
  147. console.warn(`[${s_url}]: ` + message);
  148. uni.showToast({
  149. title: message,
  150. icon: 'none',
  151. });
  152. return reject(new Error(message));
  153. }
  154. }
  155. sessionCache.set('sessionObj', requestObj);
  156. }
  157. // 加密处理
  158. if (
  159. REQUEST_CONFIG.encryptEnabled &&
  160. isEncrypt &&
  161. (config.method === 'POST' || config.method === 'PUT')
  162. ) {
  163. try {
  164. console.log('🔐 开始请求加密处理...');
  165. // 生成AES密钥
  166. const aesKey = generateAesKey();
  167. // 使用RSA加密AES密钥
  168. const encryptedKey = encrypt(encryptBase64(aesKey));
  169. if (!encryptedKey) {
  170. throw new Error('RSA加密AES密钥失败');
  171. }
  172. // 设置加密头
  173. config.header[ENCRYPT_CONFIG.ENCRYPT_HEADER] = encryptedKey;
  174. // 加密请求数据
  175. const dataToEncrypt =
  176. typeof config.data === 'object' ? JSON.stringify(config.data) : config.data;
  177. config.data = encryptWithAes(dataToEncrypt, aesKey);
  178. console.log('✅ 请求数据加密完成');
  179. } catch (error) {
  180. console.error('❌ 请求加密失败:', error);
  181. uni.showToast({
  182. title: '请求加密失败',
  183. icon: 'none',
  184. });
  185. return reject(error);
  186. }
  187. }
  188. // FormData处理
  189. if (config.data instanceof globalThis.FormData) {
  190. delete config.header['Content-Type'];
  191. }
  192. console.log('📤 发送请求:', {
  193. url: config.url,
  194. method: config.method,
  195. hasToken: !!token,
  196. isEncrypt: isEncrypt,
  197. });
  198. // 发送请求
  199. uni.request({
  200. ...config,
  201. success: (res) => {
  202. console.log('📥 收到响应:', {
  203. statusCode: res.statusCode,
  204. url: config.url,
  205. hasEncryptHeader: !!res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER],
  206. });
  207. // 解密处理 - 只对JSON响应进行解密
  208. if (
  209. REQUEST_CONFIG.encryptEnabled &&
  210. res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER] &&
  211. config.responseType === 'text'
  212. ) {
  213. try {
  214. console.log('🔓 开始响应解密处理...');
  215. // 获取加密的AES密钥
  216. const keyStr = res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER];
  217. // RSA解密获取AES密钥
  218. const base64Str = decrypt(keyStr);
  219. if (!base64Str) {
  220. throw new Error('RSA解密AES密钥失败');
  221. }
  222. // Base64解码获取AES密钥
  223. const aesKey = decryptBase64(base64Str);
  224. // AES解密响应数据
  225. const decryptedData = decryptWithAes(res.data, aesKey);
  226. // 解析JSON
  227. res.data = JSON.parse(decryptedData);
  228. console.log('✅ 响应数据解密完成');
  229. } catch (error) {
  230. console.error('❌ 响应解密失败:', error);
  231. uni.showToast({
  232. title: '响应解密失败',
  233. icon: 'none',
  234. });
  235. return reject(error);
  236. }
  237. }
  238. // 处理响应
  239. if (res.statusCode === 200) {
  240. // 对于arraybuffer类型的响应,直接返回数据
  241. if (config.responseType === 'arraybuffer') {
  242. resolve(res.data);
  243. return;
  244. }
  245. // 检查业务状态码
  246. if (res.data && res.data.code !== undefined) {
  247. if (res.data.code === 200) {
  248. resolve(res.data);
  249. } else if (res.data.code === 401) {
  250. // 401未授权,跳转到登录页
  251. console.log('🔒 Token失效,自动跳转到登录页');
  252. handleUnauthorized(res.data.msg || '登录已失效,请重新登录');
  253. reject(res.data);
  254. } else {
  255. // 其他业务错误
  256. const errorMsg = res.data.msg || '请求失败';
  257. if (res.data.code !== 404) {
  258. // 404错误静默处理
  259. uni.showToast({
  260. title: errorMsg,
  261. icon: 'none',
  262. });
  263. }
  264. reject(res.data);
  265. }
  266. } else {
  267. resolve(res.data);
  268. }
  269. } else if (res.statusCode === 401) {
  270. // HTTP 401未授权
  271. console.log('🔒 HTTP 401未授权,自动跳转到登录页');
  272. handleUnauthorized('登录已失效,请重新登录');
  273. reject(res);
  274. } else if (res.statusCode === 404) {
  275. // 404错误静默处理
  276. console.log('404错误被静默处理,不显示用户提示');
  277. reject(res);
  278. } else {
  279. // 其他HTTP状态码错误
  280. const errorMsg = `请求失败: ${res.statusCode}`;
  281. uni.showToast({
  282. title: errorMsg,
  283. icon: 'none',
  284. });
  285. reject(res);
  286. }
  287. },
  288. fail: (error) => {
  289. console.error('❌ 网络请求失败:', error);
  290. let message = '网络请求失败';
  291. if (error.errMsg) {
  292. if (error.errMsg.includes('timeout')) {
  293. message = '请求超时,请检查网络连接';
  294. } else if (error.errMsg.includes('fail')) {
  295. message = '网络连接失败,请检查网络';
  296. }
  297. }
  298. uni.showToast({
  299. title: message,
  300. icon: 'none',
  301. });
  302. reject(error);
  303. },
  304. });
  305. });
  306. };
  307. // API方法封装
  308. export const api = {
  309. // 用户登录 - 使用统一的request方法和加密拦截器
  310. login: (data) => {
  311. console.log('🔐 开始登录流程...');
  312. return request({
  313. url: '/auth/login',
  314. method: 'POST',
  315. data: {
  316. username: data.username,
  317. password: data.password,
  318. tenantId: data.tenantId || '000000',
  319. clientId: data.clientId || REQUEST_CONFIG.clientId,
  320. grantType: data.grantType || 'password',
  321. },
  322. header: {
  323. isToken: false, // 登录接口不需要token
  324. isEncrypt: 'true', // 启用加密
  325. },
  326. });
  327. },
  328. // 获取用户信息
  329. getUserInfo: () => {
  330. const token = getToken();
  331. console.log(
  332. '🎫 获取用户信息,当前token:',
  333. token ? token.substring(0, 20) + '...' : '无token',
  334. );
  335. return request({
  336. url: '/system/user/getInfo',
  337. method: 'GET',
  338. });
  339. },
  340. // 获取租户列表
  341. getTenantList: (isToken = false) =>
  342. request({
  343. url: '/auth/tenant/list',
  344. method: 'GET',
  345. header: {
  346. isToken: isToken,
  347. },
  348. }),
  349. // 注销登录
  350. logout: () =>
  351. request({
  352. url: '/auth/logout',
  353. method: 'POST',
  354. }),
  355. // 修改密码
  356. resetPassword: (data) =>
  357. request({
  358. url: '/system/user/profile/updatePwd',
  359. method: 'PUT',
  360. header: {
  361. isEncrypt: 'true', // 修复:使用字符串 'true' 而不是布尔值 true
  362. repeatSubmit: false,
  363. },
  364. data: {
  365. oldPassword: data.oldPassword,
  366. newPassword: data.newPassword,
  367. },
  368. }),
  369. // 更新用户头像
  370. // updateUserAvatar: (data) =>
  371. // request({
  372. // url: '/system/user/profile/updateAvatar',
  373. // method: 'PUT',
  374. // data: {
  375. // avatar: data.avatar,
  376. // },
  377. // }),
  378. };
  379. // 便捷请求方法封装
  380. export const get = (url, data, header) => {
  381. return request({
  382. url,
  383. method: 'GET',
  384. data,
  385. header,
  386. });
  387. };
  388. export const post = (url, data, header) => {
  389. return request({
  390. url,
  391. method: 'POST',
  392. data,
  393. header,
  394. });
  395. };
  396. export const put = (url, data, header) => {
  397. return request({
  398. url,
  399. method: 'PUT',
  400. data,
  401. header,
  402. });
  403. };
  404. export const del = (url, data, header) => {
  405. return request({
  406. url,
  407. method: 'DELETE',
  408. data,
  409. header,
  410. });
  411. };
  412. // 文件上传方法
  413. export const uploadFile = (url, filePath, formData) => {
  414. return new Promise((resolve, reject) => {
  415. const token = getToken();
  416. const clientid = uni.getStorageSync('clientid') || REQUEST_CONFIG.clientId;
  417. // 构建完整URL - 使用统一配置的服务器地址
  418. let fullUrl;
  419. if (REQUEST_CONFIG.baseURL.startsWith('/')) {
  420. // 开发环境,baseURL是'/api',需要使用完整的服务器URL
  421. fullUrl = `${REQUEST_CONFIG.devServerUrl}${REQUEST_CONFIG.baseURL}${url}`;
  422. } else {
  423. // 生产环境,直接拼接
  424. fullUrl = REQUEST_CONFIG.baseURL + url;
  425. }
  426. console.log('🚀 上传文件URL:', fullUrl);
  427. console.log('🗂️ 文件路径:', filePath);
  428. console.log('📊 表单数据:', formData);
  429. const headers = {
  430. // 移除Content-Type设置,让uni.uploadFile自动处理
  431. authorization: token,
  432. clientid: clientid,
  433. };
  434. uni.uploadFile({
  435. url: fullUrl,
  436. filePath,
  437. name: 'file',
  438. formData,
  439. header: headers,
  440. success: (res) => {
  441. console.log('✅ 上传成功响应:', res);
  442. try {
  443. const data = JSON.parse(res.data);
  444. resolve(data);
  445. } catch (error) {
  446. console.error('❌ 解析响应数据失败:', error);
  447. reject(new Error('解析响应数据失败'));
  448. }
  449. },
  450. fail: (err) => {
  451. console.error('❌ 上传失败:', err);
  452. reject(new Error(err.errMsg || '上传失败'));
  453. },
  454. });
  455. });
  456. };
  457. // 文件下载方法(支持多平台)
  458. export const downloadFile = (url, fileName = null) => {
  459. return new Promise((resolve, reject) => {
  460. const token = getToken();
  461. const clientid = uni.getStorageSync('clientid') || REQUEST_CONFIG.clientId;
  462. console.log('REQUEST_CONFIG.baseURL:', REQUEST_CONFIG.baseURL);
  463. // 构建完整URL - 使用统一配置的服务器地址
  464. let fullUrl;
  465. if (REQUEST_CONFIG.baseURL.startsWith('/')) {
  466. // 开发环境,baseURL是'/api',需要使用完整的服务器URL
  467. fullUrl = `${REQUEST_CONFIG.devServerUrl}${REQUEST_CONFIG.baseURL}${url}`;
  468. } else {
  469. // 生产环境,直接拼接
  470. fullUrl = REQUEST_CONFIG.baseURL + url;
  471. }
  472. console.log('📥 下载文件URL:', fullUrl);
  473. const headers = {
  474. authorization: token,
  475. clientid: clientid,
  476. };
  477. // #ifdef H5
  478. // H5端使用fetch + blob的方式实现浏览器下载
  479. console.log('🌐 H5端下载处理');
  480. fetch(fullUrl, {
  481. method: 'GET',
  482. headers: headers,
  483. })
  484. .then((response) => {
  485. if (!response.ok) {
  486. throw new Error(`HTTP error! status: ${response.status}`);
  487. }
  488. return response.blob();
  489. })
  490. .then((blob) => {
  491. // 创建下载链接
  492. const downloadUrl = window.URL.createObjectURL(blob);
  493. const link = document.createElement('a');
  494. link.href = downloadUrl;
  495. // 尝试从响应头获取文件名,或使用传入的文件名
  496. let downloadFileName = fileName;
  497. if (!downloadFileName) {
  498. // 从URL中提取文件名
  499. const urlParts = url.split('/');
  500. downloadFileName = urlParts[urlParts.length - 1] || 'download';
  501. }
  502. link.download = downloadFileName;
  503. document.body.appendChild(link);
  504. link.click();
  505. document.body.removeChild(link);
  506. // 清理blob URL
  507. window.URL.revokeObjectURL(downloadUrl);
  508. console.log('✅ H5下载触发成功');
  509. resolve({ statusCode: 200, message: 'H5下载已触发' });
  510. })
  511. .catch((error) => {
  512. console.error('❌ H5下载失败:', error);
  513. reject(new Error(error.message || 'H5下载失败'));
  514. });
  515. // #endif
  516. // #ifndef H5
  517. // 非H5端使用uni.downloadFile
  518. uni.downloadFile({
  519. url: fullUrl,
  520. header: headers,
  521. success: (res) => {
  522. console.log('✅ 下载成功:', res);
  523. resolve(res);
  524. },
  525. fail: (err) => {
  526. console.error('❌ 下载失败:', err);
  527. reject(new Error(err.errMsg || '下载失败'));
  528. },
  529. });
  530. // #endif
  531. });
  532. };
  533. // 通用下载方法
  534. export const download = (url, params, fileName) => {
  535. // 显示加载提示
  536. uni.showLoading({
  537. title: '正在下载数据,请稍候',
  538. mask: true,
  539. });
  540. return request({
  541. url: url,
  542. method: 'POST',
  543. data: params,
  544. header: {
  545. 'Content-Type': 'application/x-www-form-urlencoded',
  546. },
  547. timeout: 1200000, // 20分钟超时
  548. responseType: 'arraybuffer',
  549. })
  550. .then((resp) => {
  551. // 处理下载响应
  552. console.log('文件下载完成:', fileName);
  553. uni.hideLoading();
  554. // 这里需要根据平台进行不同的文件保存处理
  555. // 具体实现可能需要平台特定的代码
  556. return resp;
  557. })
  558. .catch((error) => {
  559. console.error('下载失败:', error);
  560. uni.hideLoading();
  561. uni.showToast({
  562. title: '下载文件出现错误,请联系管理员!',
  563. icon: 'none',
  564. });
  565. throw error;
  566. });
  567. };
  568. // 微信环境专用文件上传方法(解决微信浏览器兼容性问题)
  569. export const uploadFileForWeChat = (url, file, formData, options = {}) => {
  570. return new Promise((resolve, reject) => {
  571. const token = getToken();
  572. const clientid = uni.getStorageSync('clientid') || REQUEST_CONFIG.clientId;
  573. console.log('🔧 微信环境专用文件上传...');
  574. console.log('📁 文件对象:', file);
  575. console.log('📊 表单数据:', formData);
  576. // 构建完整URL
  577. let fullUrl;
  578. if (REQUEST_CONFIG.baseURL.startsWith('/')) {
  579. fullUrl = `${REQUEST_CONFIG.devServerUrl}${REQUEST_CONFIG.baseURL}${url}`;
  580. } else {
  581. fullUrl = REQUEST_CONFIG.baseURL + url;
  582. }
  583. console.log('🚀 上传URL:', fullUrl);
  584. // 检测是否为blob文件
  585. if (file._blob || (file.url && file.url.startsWith('blob:'))) {
  586. console.log('🎨 检测到Blob文件,使用FormData+fetch方案');
  587. return uploadBlobFileWithFetch(fullUrl, file, formData, token, clientid)
  588. .then(resolve)
  589. .catch(reject);
  590. }
  591. // 检测文件路径格式并尝试不同的上传策略
  592. const filePath = file.url || file.tempFilePath || file.path;
  593. if (!filePath) {
  594. const error = new Error('文件路径为空,无法上传');
  595. console.error('❌', error.message);
  596. return reject(error);
  597. }
  598. console.log('📂 使用文件路径上传:', filePath);
  599. // 策略1: 先尝试标准的uni.uploadFile
  600. console.log('🔄 策略1: 尝试标准uni.uploadFile');
  601. uni.uploadFile({
  602. url: fullUrl,
  603. filePath: filePath,
  604. name: 'file',
  605. formData,
  606. header: {
  607. authorization: token,
  608. clientid: clientid,
  609. },
  610. success: (res) => {
  611. console.log('✅ 标准uni.uploadFile成功:', res);
  612. try {
  613. const data = JSON.parse(res.data);
  614. resolve(data);
  615. } catch (error) {
  616. console.error('❌ 解析响应数据失败:', error);
  617. reject(new Error('解析响应数据失败'));
  618. }
  619. },
  620. fail: (err) => {
  621. console.log('⚠️ 标准uni.uploadFile失败,尝试备用方案:', err);
  622. // 策略2: 尝试使用FileReader转换后上传
  623. if (typeof FileReader !== 'undefined') {
  624. console.log('🔄 策略2: 尝试FileReader+fetch方案');
  625. // 通过input[type=file]重新选择文件进行上传
  626. createFileInputForUpload(fullUrl, formData, token, clientid, file)
  627. .then(resolve)
  628. .catch((fetchError) => {
  629. console.error('❌ FileReader+fetch方案也失败:', fetchError);
  630. reject(
  631. new Error(
  632. `所有上传方案失败: ${err.errMsg || err.message} / ${
  633. fetchError.message
  634. }`,
  635. ),
  636. );
  637. });
  638. } else {
  639. // 策略3: 提示用户使用系统相册
  640. console.log('🔄 策略3: 引导用户使用系统相册');
  641. reject(new Error('当前环境不支持此文件格式,请使用系统相册选择图片'));
  642. }
  643. },
  644. });
  645. });
  646. };
  647. // 使用FormData+fetch上传Blob文件(专门处理Canvas生成的图片)
  648. function uploadBlobFileWithFetch(url, file, formData, token, clientid) {
  649. return new Promise((resolve, reject) => {
  650. try {
  651. console.log('🎨 开始Blob文件上传流程...');
  652. const formDataObj = new FormData();
  653. // 添加基本表单数据
  654. Object.keys(formData || {}).forEach((key) => {
  655. formDataObj.append(key, formData[key]);
  656. });
  657. // 处理Blob文件
  658. let blob = file._blob;
  659. let fileName = file.name || 'upload-image.png';
  660. let mimeType = file.type || 'image/png';
  661. if (!blob && file.url && file.url.startsWith('blob:')) {
  662. // 如果没有直接的blob引用,尝试从blob URL获取
  663. console.log('🔗 从blob URL获取文件数据...');
  664. fetch(file.url)
  665. .then((response) => response.blob())
  666. .then((fetchedBlob) => {
  667. formDataObj.append('file', fetchedBlob, fileName);
  668. return performFetchUpload(url, formDataObj, token, clientid);
  669. })
  670. .then(resolve)
  671. .catch(reject);
  672. return;
  673. }
  674. if (!blob) {
  675. throw new Error('无法获取Blob数据');
  676. }
  677. // 直接添加blob到formData
  678. console.log('📎 添加Blob到FormData:', {
  679. fileName,
  680. mimeType,
  681. size: blob.size,
  682. });
  683. formDataObj.append('file', blob, fileName);
  684. // 执行fetch上传
  685. performFetchUpload(url, formDataObj, token, clientid).then(resolve).catch(reject);
  686. } catch (error) {
  687. console.error('❌ Blob文件上传准备失败:', error);
  688. reject(error);
  689. }
  690. });
  691. }
  692. // 执行fetch上传请求
  693. function performFetchUpload(url, formData, token, clientid) {
  694. return fetch(url, {
  695. method: 'POST',
  696. headers: {
  697. authorization: token,
  698. clientid: clientid,
  699. // 注意:不要设置Content-Type,让浏览器自动设置multipart/form-data边界
  700. },
  701. body: formData,
  702. })
  703. .then((response) => {
  704. console.log('📡 Fetch上传响应状态:', response.status);
  705. if (!response.ok) {
  706. throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  707. }
  708. return response.text();
  709. })
  710. .then((responseText) => {
  711. console.log('📥 Fetch上传响应数据:', responseText);
  712. try {
  713. const data = JSON.parse(responseText);
  714. return data;
  715. } catch (parseError) {
  716. console.error('❌ 解析响应JSON失败:', parseError);
  717. throw new Error('服务器返回数据格式错误');
  718. }
  719. });
  720. }
  721. // 创建隐藏的file input来重新选择文件(微信环境后备方案)
  722. function createFileInputForUpload(url, formData, token, clientid, originalFile) {
  723. return new Promise((resolve, reject) => {
  724. try {
  725. console.log('📝 创建隐藏文件选择器...');
  726. // 在微信环境中提示用户重新选择文件
  727. uni.showModal({
  728. title: '文件上传',
  729. content: '检测到上传兼容性问题,需要重新选择文件。点击确定后请重新选择图片。',
  730. showCancel: true,
  731. success: (modalRes) => {
  732. if (modalRes.confirm) {
  733. // 重新选择文件
  734. uni.chooseImage({
  735. count: 1,
  736. sourceType: ['album', 'camera'],
  737. success: (chooseRes) => {
  738. console.log('🔄 重新选择文件成功:', chooseRes);
  739. if (chooseRes.tempFilePaths && chooseRes.tempFilePaths.length > 0) {
  740. const newFilePath = chooseRes.tempFilePaths[0];
  741. // 使用新选择的文件进行上传
  742. uni.uploadFile({
  743. url: url,
  744. filePath: newFilePath,
  745. name: 'file',
  746. formData,
  747. header: {
  748. authorization: token,
  749. clientid: clientid,
  750. },
  751. success: (res) => {
  752. console.log('✅ 重新上传成功:', res);
  753. try {
  754. const data = JSON.parse(res.data);
  755. resolve(data);
  756. } catch (error) {
  757. reject(new Error('解析响应数据失败'));
  758. }
  759. },
  760. fail: (err) => {
  761. console.error('❌ 重新上传失败:', err);
  762. reject(new Error(err.errMsg || '重新上传失败'));
  763. },
  764. });
  765. } else {
  766. reject(new Error('未选择任何文件'));
  767. }
  768. },
  769. fail: (err) => {
  770. console.error('❌ 重新选择文件失败:', err);
  771. reject(new Error('重新选择文件失败'));
  772. },
  773. });
  774. } else {
  775. reject(new Error('用户取消了重新选择文件'));
  776. }
  777. },
  778. });
  779. } catch (error) {
  780. console.error('❌ 创建文件选择器失败:', error);
  781. reject(error);
  782. }
  783. });
  784. }
  785. // 检测是否在微信环境
  786. export const isWeChat = () => {
  787. if (typeof navigator === 'undefined') return false;
  788. return /MicroMessenger/i.test(navigator.userAgent || '');
  789. };
  790. // 智能文件上传方法(根据环境自动选择最佳方案)
  791. export const smartUploadFile = (url, fileOrPath, formData, options = {}) => {
  792. console.log('🧠 智能文件上传开始...');
  793. console.log('🌐 当前环境:', {
  794. isWeChat: isWeChat(),
  795. userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
  796. });
  797. // 如果在微信环境或遇到特殊文件格式,使用增强版上传
  798. if (
  799. isWeChat() ||
  800. (typeof fileOrPath === 'object' &&
  801. (fileOrPath._blob || fileOrPath.url?.startsWith('blob:')))
  802. ) {
  803. console.log('📱 使用微信环境专用上传方法');
  804. return uploadFileForWeChat(url, fileOrPath, formData, options);
  805. }
  806. // 否则使用标准上传方法
  807. console.log('💻 使用标准上传方法');
  808. const filePath =
  809. typeof fileOrPath === 'string'
  810. ? fileOrPath
  811. : fileOrPath.url || fileOrPath.tempFilePath || fileOrPath.path;
  812. return uploadFile(url, filePath, formData);
  813. };