diff --git a/admin/public/favicon.svg b/admin/public/favicon.svg index a698b988..1588314a 100644 --- a/admin/public/favicon.svg +++ b/admin/public/favicon.svg @@ -1,6 +1,71 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/src/layouts/MainLayout.vue b/admin/src/layouts/MainLayout.vue index c7166d4a..ad6941aa 100644 --- a/admin/src/layouts/MainLayout.vue +++ b/admin/src/layouts/MainLayout.vue @@ -226,8 +226,8 @@ padding: 0 16px; .logo-icon { - width: 32px; - height: 32px; + width: 48px; + height: 48px; } .logo-text { diff --git a/logo/logo.png b/logo/logo.png new file mode 100644 index 00000000..d8b8d251 Binary files /dev/null and b/logo/logo.png differ diff --git a/logo/logo.svg b/logo/logo.svg new file mode 100644 index 00000000..1588314a --- /dev/null +++ b/logo/logo.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/miniprogram/README-UPLOAD.md b/miniprogram/README-UPLOAD.md new file mode 100644 index 00000000..add795c4 --- /dev/null +++ b/miniprogram/README-UPLOAD.md @@ -0,0 +1,91 @@ +# 小程序头像上传配置说明 + +## 问题说明 +小程序从本地相册选择头像后,会得到一个临时文件路径(如 `http://tmp/xxx.jpg`),这个路径需要上传到服务器才能永久保存。 + +## 开发环境配置 + +### 1. 微信开发者工具设置 +在微信开发者工具中,需要开启以下选项: + +1. 点击右上角"详情" +2. 在"本地设置"中勾选: + - ✅ **不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书** + - ✅ **不校验合法域名** + +这样才能在开发环境中访问 `http://127.0.0.1:3001` 的上传接口。 + +### 2. 确认服务端运行 +确保后端服务正在运行: +```bash +cd server +npm start +``` + +服务应该运行在 `http://127.0.0.1:3001` + +### 3. 测试上传功能 +1. 在小程序中点击头像 +2. 选择"从相册选择" +3. 选择一张图片 +4. 点击"保存" +5. 查看控制台日志,应该显示: + - "检测到临时头像,开始上传: http://tmp/xxx.jpg" + - "头像上传成功: http://127.0.0.1:3001/uploads/xxx.jpg" + +## 生产环境配置 + +### 1. 小程序后台配置 +在微信公众平台(mp.weixin.qq.com)配置合法域名: + +1. 登录小程序后台 +2. 进入"开发" -> "开发管理" -> "开发设置" +3. 在"服务器域名"中配置: + - **uploadFile合法域名**:`https://yingsa-server.ethan.team` + - **request合法域名**:`https://yingsa-server.ethan.team` + - **socket合法域名**:`wss://yingsa-server.ethan.team` + +### 2. 服务端HTTPS配置 +确保生产环境服务器已配置HTTPS证书。 + +## 常见问题 + +### Q1: 上传失败,提示"不在合法域名列表中" +**解决方案**: +- 开发环境:在微信开发者工具中开启"不校验合法域名" +- 生产环境:在小程序后台配置合法的上传域名 + +### Q2: 上传失败,提示"网络错误" +**解决方案**: +- 检查后端服务是否正常运行 +- 检查 `miniprogram/config.js` 中的 `baseUrl` 配置是否正确 +- 查看后端日志是否有错误信息 + +### Q3: 头像显示不出来 +**解决方案**: +- 检查上传是否成功(查看控制台日志) +- 检查返回的URL是否正确 +- 确认 `server/uploads` 目录存在且有写入权限 +- 确认服务端的静态文件服务配置正确 + +### Q4: 临时文件路径错误 +**解决方案**: +- 这是正常的,临时文件只在选择时有效 +- 必须上传到服务器后才能永久使用 +- 代码已经处理了临时文件的上传逻辑 + +## 调试技巧 + +### 查看上传日志 +在小程序控制台中查看: +``` +检测到临时头像,开始上传: [临时路径] +上传头像响应: [服务器响应] +头像上传成功: [最终URL] +``` + +### 查看服务端日志 +在服务端控制台中查看上传请求和文件保存情况。 + +### 检查文件是否保存 +检查 `server/uploads/` 目录下是否有新上传的文件。 diff --git a/miniprogram/app.js b/miniprogram/app.js index 210e7a52..bc729249 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -64,7 +64,7 @@ App({ }, // 手机号授权登录(第二步:解密手机号完成注册/登录) - phoneLogin(encryptedData, iv, userProfile) { + phoneLogin(encryptedData, iv, gender) { return new Promise((resolve, reject) => { const wxInfo = this.globalData.wxLoginInfo; if (!wxInfo) { @@ -72,19 +72,23 @@ App({ return; } + const requestData = { + openid: wxInfo.openid, + unionid: wxInfo.unionid, + sessionKey: wxInfo.sessionKey, + encryptedData, + iv, + }; + + // 如果提供了性别参数,添加到请求中 + if (gender === 1 || gender === 2) { + requestData.gender = gender; + } + wx.request({ url: `${this.globalData.baseUrl}/api/user/phone-login`, method: "POST", - data: { - openid: wxInfo.openid, - unionid: wxInfo.unionid, - sessionKey: wxInfo.sessionKey, - encryptedData, - iv, - nickname: (userProfile && userProfile.nickName) || "", - avatar: (userProfile && userProfile.avatarUrl) || "", - gender: (userProfile && userProfile.gender) || 0, - }, + data: requestData, success: (loginRes) => { if (loginRes.data.code === 0) { this.globalData.token = loginRes.data.data.token; diff --git a/miniprogram/config.js b/miniprogram/config.js index b1ef9003..9e0edc84 100644 --- a/miniprogram/config.js +++ b/miniprogram/config.js @@ -6,9 +6,9 @@ // 开发环境配置 const devConfig = { // API 基础地址(本地开发) - baseUrl: "http://127.0.0.1:3000", + baseUrl: "http://127.0.0.1:3001", // WebSocket 地址(本地开发) - wsUrl: "ws://127.0.0.1:3000/ws", + wsUrl: "ws://127.0.0.1:3001/ws", }; // 生产环境配置 diff --git a/miniprogram/images/logo.png b/miniprogram/images/logo.png new file mode 100644 index 00000000..d8b8d251 Binary files /dev/null and b/miniprogram/images/logo.png differ diff --git a/miniprogram/images/logo.svg b/miniprogram/images/logo.svg new file mode 100644 index 00000000..1588314a --- /dev/null +++ b/miniprogram/images/logo.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/miniprogram/pages/user/index.js b/miniprogram/pages/user/index.js index 28f61eff..a7270dc7 100644 --- a/miniprogram/pages/user/index.js +++ b/miniprogram/pages/user/index.js @@ -140,7 +140,7 @@ Page({ await this.doPhoneLogin( e.detail.encryptedData, e.detail.iv, - needGender ? this.data.registerGender : 0, + needGender ? this.data.registerGender : undefined, ); // 获取门店信息 @@ -186,8 +186,8 @@ Page({ }, async doPhoneLogin(encryptedData, iv, gender) { - const g = gender === 1 || gender === 2 ? gender : 0; - await app.phoneLogin(encryptedData, iv, g ? { gender: g } : null); + const g = gender === 1 || gender === 2 ? gender : undefined; + await app.phoneLogin(encryptedData, iv, g); this._pendingPhoneLogin = null; this.setData({ showGenderModal: false }); }, @@ -277,10 +277,79 @@ Page({ }, // 选择头像(新API:button open-type="chooseAvatar") - onChooseAvatarNew(e) { + async onChooseAvatarNew(e) { const avatarUrl = e.detail.avatarUrl; - this.setData({ - "profileForm.avatar": avatarUrl, + console.log('选择头像:', avatarUrl); + + // 检查是否是临时文件 + if (avatarUrl && (avatarUrl.startsWith("wxfile://") || + avatarUrl.startsWith("http://tmp") || + avatarUrl.includes("/tmp/"))) { + console.log('检测到临时头像,立即上传'); + wx.showLoading({ title: "上传头像中..." }); + + try { + const uploadedUrl = await this.uploadAvatar(avatarUrl); + console.log('头像上传成功:', uploadedUrl); + + this.setData({ + "profileForm.avatar": uploadedUrl, + }); + + wx.hideLoading(); + wx.showToast({ title: "头像上传成功", icon: "success", duration: 1500 }); + } catch (uploadErr) { + wx.hideLoading(); + console.error("头像上传失败:", uploadErr); + wx.showToast({ + title: uploadErr.message || "头像上传失败", + icon: "none", + duration: 2000 + }); + // 上传失败,不设置头像 + } + } else { + // 不是临时文件,直接使用 + this.setData({ + "profileForm.avatar": avatarUrl, + }); + } + }, + + // 选择头像(使用 wx.chooseMedia) + onChooseAvatar() { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['album', 'camera'], + sizeType: ['compressed'], + success: (res) => { + console.log('选择图片成功:', res); + const tempFilePath = res.tempFiles[0].tempFilePath; + this.setData({ + "profileForm.avatar": tempFilePath, + }); + }, + fail: (err) => { + console.error('选择图片失败:', err); + // 如果 chooseMedia 不支持,降级使用 chooseImage + wx.chooseImage({ + count: 1, + sourceType: ['album', 'camera'], + sizeType: ['compressed'], + success: (res) => { + console.log('选择图片成功(降级):', res); + const tempFilePath = res.tempFilePaths[0]; + this.setData({ + "profileForm.avatar": tempFilePath, + }); + }, + fail: (err) => { + console.error('选择图片失败:', err); + wx.showToast({ title: '选择图片失败', icon: 'none' }); + } + }); + } }); }, @@ -311,14 +380,8 @@ Page({ wx.showLoading({ title: "保存中..." }); try { - // 如果选择了新头像,先上传 - let avatarUrl = avatar; - if ( - avatar && - (avatar.startsWith("wxfile://") || avatar.startsWith("http://tmp")) - ) { - avatarUrl = await this.uploadAvatar(avatar); - } + // 头像已经在选择时上传,这里直接使用 + const avatarUrl = avatar; // 调用更新资料接口 const payload = { @@ -329,6 +392,7 @@ Page({ payload.gender = gender; } + console.log("保存资料请求:", payload); const res = await app.request("/api/user/profile", payload, "PUT"); // 更新本地数据(服务端已返回完整URL) @@ -357,6 +421,16 @@ Page({ // 上传头像 async uploadAvatar(filePath) { return new Promise((resolve, reject) => { + console.log('开始上传头像'); + console.log('文件路径:', filePath); + console.log('上传URL:', `${app.globalData.baseUrl}/api/upload/avatar`); + console.log('Token:', app.globalData.token); + + if (!app.globalData.token) { + reject(new Error('未登录,无法上传头像')); + return; + } + wx.uploadFile({ url: `${app.globalData.baseUrl}/api/upload/avatar`, filePath: filePath, @@ -366,20 +440,30 @@ Page({ }, success: (res) => { try { + console.log("上传头像响应状态:", res.statusCode); + console.log("上传头像响应数据:", res.data); + + if (res.statusCode !== 200) { + reject(new Error(`上传失败,状态码: ${res.statusCode}`)); + return; + } + const data = JSON.parse(res.data); if (data.code === 0 && data.data && data.data.url) { + console.log("头像上传成功:", data.data.url); resolve(data.data.url); } else { console.error("上传头像失败:", data); - resolve(filePath); + reject(new Error(data.message || "上传头像失败")); } } catch (e) { - resolve(filePath); + console.error("解析上传响应失败:", e); + reject(new Error("上传头像失败")); } }, fail: (err) => { - console.error("上传头像失败:", err); - resolve(filePath); + console.error("上传头像请求失败:", err); + reject(new Error("上传头像失败,请检查网络")); }, }); }); diff --git a/server/.env b/server/.env index c42fa886..b3b7bcc9 100644 --- a/server/.env +++ b/server/.env @@ -14,7 +14,7 @@ NODE_ENV=development # 服务端口(默认3000) -PORT=3000 +PORT=3001 # ------------------------------------------ # 数据库配置 (MySQL) diff --git a/server/src/routes/upload.js b/server/src/routes/upload.js index cee18540..cb28b917 100644 --- a/server/src/routes/upload.js +++ b/server/src/routes/upload.js @@ -60,7 +60,15 @@ router.post('/images', authAdmin, upload.array('files', 10), (req, res) => { }); // 用户头像上传(需要登录) -router.post('/avatar', authUser, upload.single('file'), (req, res) => { +router.post('/avatar', (req, res, next) => { + console.log('收到头像上传请求'); + console.log('Authorization:', req.headers.authorization); + next(); +}, authUser, (req, res, next) => { + console.log('认证通过,开始上传'); + next(); +}, upload.single('file'), (req, res) => { + console.log('文件上传完成:', req.file); if (!req.file) { return res.status(400).json(error('请选择要上传的图片')); } @@ -68,7 +76,20 @@ router.post('/avatar', authUser, upload.single('file'), (req, res) => { const relativePath = `/uploads/${req.file.filename}`; // 使用 normalizeAvatarUrl 确保 https 保持 https,http 保持 http const fullUrl = normalizeAvatarUrl(relativePath, req); + console.log('返回URL:', fullUrl); res.json(success({ url: fullUrl }, '上传成功')); }); +// 错误处理中间件 +router.use((err, req, res, next) => { + console.error('上传错误:', err); + if (err instanceof multer.MulterError) { + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json(error('文件大小超过限制(最大5MB)')); + } + return res.status(400).json(error('文件上传失败: ' + err.message)); + } + return res.status(500).json(error(err.message || '上传失败')); +}); + module.exports = router; diff --git a/server/test-upload.js b/server/test-upload.js new file mode 100644 index 00000000..e3477236 --- /dev/null +++ b/server/test-upload.js @@ -0,0 +1,36 @@ +// 测试上传接口 +const axios = require('axios'); +const FormData = require('form-data'); +const fs = require('fs'); +const path = require('path'); + +async function testUpload() { + try { + // 首先测试健康检查 + console.log('1. 测试健康检查...'); + const healthRes = await axios.get('http://127.0.0.1:3001/health'); + console.log('✓ 健康检查通过:', healthRes.data); + + // 测试上传路由是否存在(不带token) + console.log('\n2. 测试上传路由(无token)...'); + try { + await axios.post('http://127.0.0.1:3001/api/upload/avatar'); + } catch (err) { + if (err.response) { + console.log('✓ 路由存在,返回状态:', err.response.status); + console.log(' 响应:', err.response.data); + } else { + console.log('✗ 请求失败:', err.message); + } + } + + console.log('\n测试完成!'); + console.log('\n如果看到404错误,说明路由没有正确注册。'); + console.log('如果看到401错误,说明路由存在但需要认证。'); + console.log('如果看到400错误,说明路由正常工作。'); + } catch (error) { + console.error('测试失败:', error.message); + } +} + +testUpload();