From 376dff48f69310a403cf680f85c5fffa113c4d55 Mon Sep 17 00:00:00 2001 From: WindowBird <13870814+windows-bird@user.noreply.gitee.com> Date: Wed, 20 Aug 2025 11:57:07 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=A1=B5=E9=9D=A2=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=A4=B4=E5=83=8F=E5=AE=9E=E7=8E=B0-=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E6=B5=8B=E8=AF=95=E9=A1=B5=E9=9D=A2=E5=92=8C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=88=A0=E5=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README_AVATAR_UPLOAD.md | 163 ++++++++++++++++ api/user/user.js | 6 +- debug-upload.js | 297 ----------------------------- examples/avatar-upload-example.vue | 1 + pages/avatar-test/avatar-test.vue | 1 + pages/test-upload/test-upload.vue | 1 + utils/request.js | 246 +----------------------- 7 files changed, 173 insertions(+), 542 deletions(-) create mode 100644 README_AVATAR_UPLOAD.md delete mode 100644 debug-upload.js create mode 100644 examples/avatar-upload-example.vue create mode 100644 pages/avatar-test/avatar-test.vue create mode 100644 pages/test-upload/test-upload.vue diff --git a/README_AVATAR_UPLOAD.md b/README_AVATAR_UPLOAD.md new file mode 100644 index 0000000..b8dde89 --- /dev/null +++ b/README_AVATAR_UPLOAD.md @@ -0,0 +1,163 @@ +# 头像上传功能说明 + +## 功能概述 +本功能允许用户上传和更换个人头像,支持从相册选择或拍照两种方式。 + +## API接口 +- **接口地址**: `PUT /app/user/avatar` +- **请求格式**: `form-data` +- **参数名**: `avatarfile` +- **参数类型**: `file` + +## 实现文件 +1. **API函数**: `api/user/user.js` - `uploadAvatar()` +2. **页面组件**: `pages/set/set.vue` - 头像设置页面 +3. **请求工具**: `utils/request.js` - 统一请求处理(包含文件上传方法) + +## 使用流程 + +### 1. 用户操作流程 +1. 进入设置页面 (`pages/set/set.vue`) +2. 点击"头像"设置项 +3. 选择"从相册选择"或"拍照" +4. 选择或拍摄图片 +5. 系统自动上传并更新头像 + +### 2. 技术实现流程 +1. **选择图片**: 使用 `uni.chooseImage()` 选择图片 +2. **上传文件**: 使用 `utils/request.js` 中的 `uploadFile()` 方法上传到服务器 +3. **更新显示**: 更新本地存储和页面显示 +4. **通知更新**: 通过事件通知其他页面更新头像 + +## 关键代码 + +### 统一文件上传方法 (utils/request.js) +```javascript +export function uploadFile(url, filePath, name = 'file', formData = {}, options = {}) { + // 自动获取token和配置 + // 统一错误处理 + // 支持超时设置 + // 自动解析响应 +} +``` + +### API函数 (api/user/user.js) +```javascript +export function uploadAvatar(filePath) { + return uploadFile('/app/user/avatar', filePath, 'avatarfile', {}, { + timeout: 60000 + }).then(data => { + // 更新本地存储 + // 通知其他页面 + return data + }) +} +``` + +### 页面方法 (pages/set/set.vue) +```javascript +// 显示头像选择器 +showAvatarPicker() { + uni.showActionSheet({ + itemList: ['从相册选择', '拍照'], + success: (res) => { + if (res.tapIndex === 0) { + this.chooseImage('album') + } else if (res.tapIndex === 1) { + this.chooseImage('camera') + } + } + }) +} + +// 选择图片 +chooseImage(sourceType) { + uni.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: [sourceType], + success: (res) => { + this.uploadAvatarFile(res.tempFilePaths[0]) + } + }) +} +``` + +## 技术优势 + +### 1. 统一请求管理 +- ✅ 使用 `utils/request.js` 中的 `uploadFile()` 方法 +- ✅ 自动获取token和配置 +- ✅ 统一的错误处理和响应解析 +- ✅ 支持超时设置和自定义配置 + +### 2. 简化API调用 +- ✅ API函数代码更简洁 +- ✅ 专注于业务逻辑处理 +- ✅ 自动处理token和认证 +- ✅ 统一的错误处理机制 + +### 3. 更好的维护性 +- ✅ 所有网络请求都通过统一的工具管理 +- ✅ 配置变更只需修改一处 +- ✅ 错误处理逻辑统一 +- ✅ 便于调试和监控 + +## 权限配置 + +### Android权限 +已在 `manifest.json` 中添加: +- `READ_EXTERNAL_STORAGE` - 读取外部存储 +- `WRITE_EXTERNAL_STORAGE` - 写入外部存储 +- `CAMERA` - 相机权限 + +### 微信小程序权限 +建议在 `manifest.json` 中添加: +```json +"permission": { + "scope.writePhotosAlbum": { + "desc": "用于保存头像图片" + } +} +``` + +## 测试方法 + +### 1. 功能测试 +1. 点击设置页面的头像设置项 +2. 选择一张图片 +3. 查看控制台日志确认上传过程 +4. 验证头像是否成功更新 + +### 2. 调试信息 +- 控制台会输出详细的请求和响应日志 +- 包括文件路径、请求头、响应数据等信息 +- 统一的错误处理和提示 + +## 注意事项 + +1. **文件大小**: 建议限制上传文件大小,避免过大文件影响性能 +2. **文件格式**: 建议只允许图片格式(jpg, png, gif等) +3. **网络处理**: 已添加超时和错误处理机制 +4. **权限检查**: 在Android和iOS上需要相应的存储和相机权限 +5. **响应格式**: 服务器应返回标准的JSON响应格式 + +## 错误处理 + +常见错误及解决方案: +- **网络错误**: 检查网络连接和服务器状态 +- **权限错误**: 确保已授予存储和相机权限 +- **文件格式错误**: 确保上传的是有效的图片文件 +- **服务器错误**: 检查服务器端接口是否正常工作 +- **Token错误**: 确保用户已登录且token有效 + +## 扩展功能 + +可以考虑添加的功能: +1. 图片压缩和裁剪 +2. 上传进度显示 +3. 多图片上传 +4. 图片预览功能 +5. 上传历史记录 +6. 断点续传 +7. 文件类型验证 \ No newline at end of file diff --git a/api/user/user.js b/api/user/user.js index 029d9b9..aee590b 100644 --- a/api/user/user.js +++ b/api/user/user.js @@ -1,6 +1,6 @@ import request from '@/utils/request' import { mockUserInfo, mockFinancialData, mockAgentStats, mockAgentList, mockWithdrawInfo, mockBanks, createMockResponse } from './mockData.js' -import { uploadFile, uploadFileWithPut } from '@/utils/request.js' +import { uploadFile } from '@/utils/request.js' /** * 获取用户信息 @@ -186,9 +186,7 @@ export function submitWithdraw(data) { * @returns {Promise} 返回上传结果 */ export function uploadAvatar(filePath) { - // 由于服务端只支持PUT方法,我们直接使用PUT方法 - // 但需要修复multipart格式问题 - return uploadFileWithPut('/app/user/avatar', filePath, 'avatarfile', {}, { + return uploadFile('/app/user/avatar', filePath, 'avatarfile', {}, { timeout: 60000 }).then(data => { // 上传成功后更新本地存储 diff --git a/debug-upload.js b/debug-upload.js deleted file mode 100644 index 4e0dd31..0000000 --- a/debug-upload.js +++ /dev/null @@ -1,297 +0,0 @@ -/** - * 头像上传调试脚本 - * 用于分析和解决头像上传问题 - */ - -// 调试配置 -const DEBUG_CONFIG = { - baseUrl: 'http://192.168.2.143:4601', - uploadUrl: '/app/user/avatar', - timeout: 60000 -} - -// 调试工具类 -class UploadDebugger { - constructor() { - this.logs = [] - this.testResults = [] - } - - // 添加日志 - addLog(type, message, data = null) { - const log = { - type, - message, - data, - timestamp: new Date().toISOString() - } - this.logs.push(log) - console.log(`[${log.timestamp}] ${type.toUpperCase()}: ${message}`, data || '') - } - - // 检查token - checkToken() { - try { - const token = uni.getStorageSync('token') - const result = { - exists: !!token, - length: token ? token.length : 0, - preview: token ? token.substring(0, 20) + '...' : 'none', - valid: this.validateToken(token) - } - - this.addLog('info', 'Token检查完成', result) - return result - } catch (error) { - this.addLog('error', 'Token检查失败', error) - return { exists: false, error: error.message } - } - } - - // 验证token格式 - validateToken(token) { - if (!token) return false - - // 检查是否是JWT格式 - const parts = token.split('.') - if (parts.length === 3) { - try { - // 尝试解码payload部分 - const payload = JSON.parse(atob(parts[1])) - return payload && payload.exp && payload.exp > Date.now() / 1000 - } catch (e) { - return false - } - } - - return true // 如果不是JWT格式,假设是其他格式的token - } - - // 检查网络连接 - checkNetwork() { - return new Promise((resolve) => { - uni.getNetworkType({ - success: (res) => { - this.addLog('info', '网络状态检查', res) - resolve(res) - }, - fail: (err) => { - this.addLog('error', '网络状态检查失败', err) - resolve({ networkType: 'unknown', error: err }) - } - }) - }) - } - - // 检查服务器连接 - checkServerConnection() { - return new Promise((resolve) => { - uni.request({ - url: DEBUG_CONFIG.baseUrl + '/health', // 假设有健康检查接口 - method: 'GET', - timeout: 5000, - success: (res) => { - this.addLog('info', '服务器连接检查成功', res) - resolve({ connected: true, status: res.statusCode }) - }, - fail: (err) => { - this.addLog('error', '服务器连接检查失败', err) - resolve({ connected: false, error: err }) - } - }) - }) - } - - // 测试文件上传 - async testFileUpload(filePath) { - this.addLog('info', '开始文件上传测试', { filePath }) - - try { - // 检查文件信息 - const fileInfo = await this.getFileInfo(filePath) - this.addLog('info', '文件信息', fileInfo) - - // 检查文件大小 - if (fileInfo.size > 10 * 1024 * 1024) { // 10MB - this.addLog('warn', '文件过大,可能影响上传', { size: fileInfo.size }) - } - - // 执行上传 - const result = await this.performUpload(filePath) - this.addLog('info', '上传测试完成', result) - - return result - } catch (error) { - this.addLog('error', '上传测试失败', error) - throw error - } - } - - // 获取文件信息 - getFileInfo(filePath) { - return new Promise((resolve, reject) => { - uni.getFileInfo({ - filePath: filePath, - success: resolve, - fail: reject - }) - }) - } - - // 执行上传 - performUpload(filePath) { - return new Promise((resolve, reject) => { - const token = uni.getStorageSync('token') - - // 构建请求头 - let authorization = token - // #ifdef H5 - authorization = token ? `Bearer ${token}` : '' - // #endif - - const header = { - Authorization: authorization - } - - this.addLog('info', '开始上传请求', { - url: DEBUG_CONFIG.baseUrl + DEBUG_CONFIG.uploadUrl, - header: header, - filePath: filePath - }) - - uni.uploadFile({ - url: DEBUG_CONFIG.baseUrl + DEBUG_CONFIG.uploadUrl, - filePath: filePath, - name: 'avatarfile', - header: header, - timeout: DEBUG_CONFIG.timeout, - success: (res) => { - this.addLog('info', '上传响应', { - statusCode: res.statusCode, - header: res.header, - data: res.data - }) - - try { - const data = JSON.parse(res.data) - if (res.statusCode === 200 && data.code === 200) { - resolve({ success: true, data: data }) - } else { - reject({ success: false, error: data.msg || '上传失败', data: data }) - } - } catch (parseError) { - reject({ success: false, error: '响应解析失败', parseError: parseError }) - } - }, - fail: (err) => { - this.addLog('error', '上传失败', err) - reject({ success: false, error: err.errMsg || '网络错误', details: err }) - } - }) - }) - } - - // 生成诊断报告 - generateReport() { - const report = { - timestamp: new Date().toISOString(), - logs: this.logs, - summary: { - totalLogs: this.logs.length, - errors: this.logs.filter(log => log.type === 'error').length, - warnings: this.logs.filter(log => log.type === 'warn').length, - info: this.logs.filter(log => log.type === 'info').length - }, - recommendations: this.generateRecommendations() - } - - console.log('=== 诊断报告 ===') - console.log(JSON.stringify(report, null, 2)) - - return report - } - - // 生成建议 - generateRecommendations() { - const recommendations = [] - const errors = this.logs.filter(log => log.type === 'error') - const warnings = this.logs.filter(log => log.type === 'warn') - - // 检查token问题 - const tokenErrors = errors.filter(log => log.message.includes('token') || log.message.includes('Token')) - if (tokenErrors.length > 0) { - recommendations.push('检查用户登录状态和token有效性') - } - - // 检查网络问题 - const networkErrors = errors.filter(log => log.message.includes('网络') || log.message.includes('timeout')) - if (networkErrors.length > 0) { - recommendations.push('检查网络连接和服务器状态') - } - - // 检查文件问题 - const fileErrors = errors.filter(log => log.message.includes('文件') || log.message.includes('file')) - if (fileErrors.length > 0) { - recommendations.push('检查文件格式和大小是否符合要求') - } - - // 检查服务器问题 - const serverErrors = errors.filter(log => log.message.includes('服务器') || log.message.includes('server')) - if (serverErrors.length > 0) { - recommendations.push('检查服务器接口是否正常工作') - } - - if (recommendations.length === 0) { - recommendations.push('所有检查都通过,问题可能在其他方面') - } - - return recommendations - } - - // 运行完整诊断 - async runFullDiagnosis() { - this.addLog('info', '开始完整诊断') - - // 1. 检查token - const tokenResult = this.checkToken() - - // 2. 检查网络 - const networkResult = await this.checkNetwork() - - // 3. 检查服务器连接 - const serverResult = await this.checkServerConnection() - - // 4. 生成报告 - const report = this.generateReport() - - return { - token: tokenResult, - network: networkResult, - server: serverResult, - report: report - } - } -} - -// 导出调试器 -export default UploadDebugger - -// 使用示例 -export function debugUpload() { - const debugger = new UploadDebugger() - return debugger.runFullDiagnosis() -} - -// 在页面中使用 -export function useUploadDebugger() { - const debugger = new UploadDebugger() - - return { - debugger, - checkToken: () => debugger.checkToken(), - checkNetwork: () => debugger.checkNetwork(), - testUpload: (filePath) => debugger.testFileUpload(filePath), - runDiagnosis: () => debugger.runFullDiagnosis(), - getLogs: () => debugger.logs - } -} \ No newline at end of file diff --git a/examples/avatar-upload-example.vue b/examples/avatar-upload-example.vue new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/examples/avatar-upload-example.vue @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/avatar-test/avatar-test.vue b/pages/avatar-test/avatar-test.vue new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/pages/avatar-test/avatar-test.vue @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pages/test-upload/test-upload.vue b/pages/test-upload/test-upload.vue new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/pages/test-upload/test-upload.vue @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/utils/request.js b/utils/request.js index 28a4662..b99e79d 100644 --- a/utils/request.js +++ b/utils/request.js @@ -464,7 +464,7 @@ export { } /** - * 文件上传方法(支持POST) + * 文件上传 * @param {string} url - 上传地址 * @param {string} filePath - 文件路径 * @param {string} name - 文件字段名 @@ -509,9 +509,6 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options header.Authorization = authorization } - // 移除Content-Type,让浏览器自动设置multipart/form-data - - console.log('开始文件上传:', { url: BASE_URL + uploadUrl, filePath, @@ -528,7 +525,6 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options formData: formData, header: header, timeout: options.timeout || 60000, - method: options.method || 'POST', success: (res) => { console.log('文件上传响应:', { statusCode: res.statusCode, @@ -553,6 +549,7 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options message: errorMsg, data: data }) + uni.showToast({ title: errorMsg, icon: 'none', @@ -561,70 +558,12 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options reject(new Error(errorMsg)) } } catch (parseError) { - console.error('解析响应数据失败:', { - error: parseError, - rawData: res.data, - statusCode: res.statusCode - }) - - // 如果状态码不是200,可能是服务器错误 - if (res.statusCode !== 200) { - const errorMsg = `服务器错误 (${res.statusCode})` - uni.showToast({ - title: errorMsg, - icon: 'none', - duration: 3000 - }) - reject(new Error(errorMsg)) - } else { - uni.showToast({ - title: '响应数据格式错误', - icon: 'none', - duration: 3000 - }) - reject(new Error('响应数据格式错误')) - } + console.error('解析响应数据失败:', parseError) + reject(new Error('响应数据格式错误')) } }, fail: (err) => { - console.error('文件上传失败 - 网络错误:', { - error: err, - errMsg: err.errMsg, - statusCode: err.statusCode - }) - - let errorMessage = '上传失败' - if (err.errMsg) { - if (err.errMsg.includes('timeout')) { - errorMessage = '上传超时,请重试' - } else if (err.errMsg.includes('fail')) { - errorMessage = '网络连接失败,请检查网络' - } else if (err.errMsg.includes('abort')) { - errorMessage = '上传已取消' - } else { - errorMessage = err.errMsg - } - } - - // 如果是HTTP错误状态码 - if (err.statusCode) { - switch (err.statusCode) { - case 401: - errorMessage = '登录已过期,请重新登录' - break - case 403: - errorMessage = '没有上传权限' - break - case 413: - errorMessage = '文件太大' - break - case 500: - errorMessage = '服务器内部错误' - break - default: - errorMessage = `服务器错误 (${err.statusCode})` - } - } + console.error('文件上传失败:', err) uni.showToast({ title: errorMessage, @@ -642,178 +581,3 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options // 默认导出request函数,方便API文件导入 export default request - -/** - * PUT方法文件上传(用于头像上传等需要PUT方法的场景) - * @param {string} url - 上传地址 - * @param {string} filePath - 文件路径 - * @param {string} name - 文件字段名 - * @param {Object} formData - 额外的表单数据 - * @param {Object} options - 上传配置 - * @returns {Promise} 返回上传结果 - */ -export function uploadFileWithPut(url, filePath, name = 'file', formData = {}, options = {}) { - return new Promise((resolve, reject) => { - // 获取token - let token = uni.getStorageSync('token') - - // 检查token是否存在 - if (!token && !options.noToken) { - token = getTempToken() - console.log('使用临时token进行开发测试') - } - - // 清理token - token = cleanToken(token) - - // 确保URL以/开头 - const uploadUrl = url.startsWith('/') ? url : '/' + url - - // 构建请求头 - let authorization = '' - if (token) { - // #ifdef H5 - authorization = `Bearer ${token}` - // #endif - // #ifndef H5 - authorization = token - // #endif - } - - const header = { - ...options.header - } - - // 只有在有token时才添加Authorization头部 - if (authorization) { - header.Authorization = authorization - } - - console.log('开始PUT文件上传:', { - url: BASE_URL + uploadUrl, - filePath, - name, - hasToken: !!token, - tokenLength: token ? token.length : 0, - header: header - }) - - // 由于小程序环境不支持手动构建multipart格式的PUT请求 - // 我们使用POST方法,但添加特殊头部标识这是PUT操作 - const postHeader = { - ...header, - 'X-HTTP-Method-Override': 'PUT' // 告诉服务器这是PUT操作 - } - - console.log('使用POST方法模拟PUT上传...') - - uni.uploadFile({ - url: BASE_URL + uploadUrl, - filePath: filePath, - name: name, - formData: formData, - header: postHeader, - timeout: options.timeout || 60000, - method: 'POST', - success: (res) => { - console.log('POST模拟PUT上传响应:', { - statusCode: res.statusCode, - data: res.data, - header: res.header - }) - - try { - // 解析响应数据 - const data = JSON.parse(res.data) - console.log('解析后的响应数据:', data) - - if (res.statusCode === 200 && data.code === 200) { - console.log('POST模拟PUT上传成功:', data) - resolve(data) - } else { - // 服务器返回错误 - const errorMsg = data.msg || data.message || '上传失败' - console.error('POST模拟PUT上传失败 - 服务器错误:', { - statusCode: res.statusCode, - code: data.code, - message: errorMsg, - data: data - }) - - // 如果服务器不支持X-HTTP-Method-Override,尝试不带这个头部 - console.log('尝试不带X-HTTP-Method-Override的POST方法...') - tryPostWithoutOverride() - } - } catch (parseError) { - console.error('解析响应数据失败:', parseError) - // 如果解析失败,尝试不带特殊头部的POST方法 - tryPostWithoutOverride() - } - }, - fail: (err) => { - console.error('POST模拟PUT上传失败:', err) - // 如果POST方法失败,尝试不带特殊头部的POST方法 - tryPostWithoutOverride() - } - }) - - // 尝试不带X-HTTP-Method-Override的POST方法 - function tryPostWithoutOverride() { - console.log('尝试不带X-HTTP-Method-Override的POST方法...') - - uni.uploadFile({ - url: BASE_URL + uploadUrl, - filePath: filePath, - name: name, - formData: formData, - header: header, - timeout: options.timeout || 60000, - method: 'POST', - success: (res) => { - console.log('普通POST上传响应:', { - statusCode: res.statusCode, - data: res.data, - header: res.header - }) - - try { - const data = JSON.parse(res.data) - console.log('解析后的响应数据:', data) - - if (res.statusCode === 200 && data.code === 200) { - console.log('普通POST上传成功:', data) - resolve(data) - } else { - const errorMsg = data.msg || data.message || '上传失败' - console.error('普通POST上传失败:', errorMsg) - - uni.showToast({ - title: errorMsg, - icon: 'none', - duration: 3000 - }) - reject(new Error(errorMsg)) - } - } catch (parseError) { - console.error('解析响应数据失败:', parseError) - reject(new Error('响应数据格式错误')) - } - }, - fail: (err) => { - console.error('普通POST上传失败:', err) - - // 最终失败,提示用户联系服务器端 - const errorMsg = '上传失败:服务器只支持PUT方法,但小程序环境不支持PUT文件上传。请联系服务器端同时支持POST方法。' - console.error(errorMsg) - - uni.showToast({ - title: '上传失败,请联系技术支持', - icon: 'none', - duration: 3000 - }) - reject(new Error(errorMsg)) - } - }) - } - }) -}