diff --git a/server/env-template.txt b/server/env-template.txt index 107273fe..3ce30f24 100644 --- a/server/env-template.txt +++ b/server/env-template.txt @@ -16,15 +16,29 @@ NODE_ENV=development # 服务端口(默认3000) PORT=3000 -# 基础URL(用于生成完整的文件URL,支持HTTPS) +# 基础URL(用于生成完整的文件URL和头像URL,支持HTTPS) # 示例:https://api.example.com 或 http://localhost:3000 -# 如果设置了此项,上传接口返回的URL将使用此值 -# 如果不设置,将根据请求自动检测协议(支持反向代理) +# +# 作用: +# 1. 如果设置了此项,上传接口返回的URL将使用此值 +# 2. 头像URL存入数据库时会使用此值(如果是相对路径) +# 3. 如果不设置,将根据请求自动检测协议(支持反向代理) +# +# 推荐设置场景: +# - 生产环境:设置为完整的 HTTPS URL,如 https://api.example.com +# - 开发环境:可以留空,系统会自动检测 BASE_URL= # 强制使用HTTPS(true/false) -# 如果设置为 true,所有返回的URL将使用 https:// -# 适用于部署在反向代理(如nginx)后面的情况 +# +# 作用: +# 1. 如果设置为 true,所有返回的URL(包括头像URL)将使用 https:// +# 2. 适用于部署在反向代理(如nginx)后面的情况 +# 3. 当反向代理未正确设置 X-Forwarded-Proto 头时,可以使用此选项强制使用 HTTPS +# +# 注意: +# - 如果设置了 BASE_URL 且 BASE_URL 是 https://,此选项会被忽略 +# - 如果传入的头像URL已经是 https://,会保持原样,不会被此选项影响 FORCE_HTTPS=false # ------------------------------------------ @@ -78,6 +92,26 @@ UPLOAD_MAX_SIZE=10 # 上传文件存储路径 UPLOAD_PATH=uploads +# ------------------------------------------ +# 头像URL处理说明 +# ------------------------------------------ +# 头像URL存入数据库时的处理规则: +# +# 1. 如果传入的URL是 https:// 开头,会保持 https:// +# 2. 如果传入的URL是 http:// 开头,会保持 http:// +# 3. 如果是相对路径(如 /uploads/xxx.jpg),会根据以下优先级处理: +# a) 如果设置了 BASE_URL,使用 BASE_URL + 相对路径 +# b) 如果设置了 FORCE_HTTPS=true,使用 https:// + 主机地址 + 相对路径 +# c) 检测 X-Forwarded-Proto 头(反向代理设置),如果为 https 则使用 https +# d) 使用 req.protocol(Express 自动检测) +# e) 默认使用 http:// +# +# 反向代理配置建议(nginx): +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_set_header X-Forwarded-Host $host; +# +# 这样系统可以自动识别 HTTPS 请求,无需设置 FORCE_HTTPS + # ------------------------------------------ # 其他配置(可选) # ------------------------------------------ diff --git a/server/src/controllers/adminController.js b/server/src/controllers/adminController.js index 3f42c8d0..b52636bb 100644 --- a/server/src/controllers/adminController.js +++ b/server/src/controllers/adminController.js @@ -468,7 +468,13 @@ class AdminController { async updateProfile(req, res) { try { const { real_name, phone, avatar } = req.body; - await req.admin.update({ real_name, phone, avatar }); + const updateData = { real_name, phone }; + // 规范化头像URL:如果是 https 则保持 https,否则是 http + if (avatar) { + const { normalizeAvatarUrl } = require('../utils/helper'); + updateData.avatar = normalizeAvatarUrl(avatar, req); + } + await req.admin.update(updateData); res.json(success(null, '更新成功')); } catch (err) { console.error('更新个人信息失败:', err); diff --git a/server/src/controllers/userController.js b/server/src/controllers/userController.js index 03d66af6..67434e0b 100644 --- a/server/src/controllers/userController.js +++ b/server/src/controllers/userController.js @@ -3,7 +3,7 @@ const axios = require('axios'); const crypto = require('crypto'); const QRCode = require('qrcode'); const { User, LadderUser, Store, Match, MatchGame } = require('../models'); -const { generateMemberCode, success, error, calculateDistance, getFullUrl } = require('../utils/helper'); +const { generateMemberCode, success, error, calculateDistance, getFullUrl, normalizeAvatarUrl } = require('../utils/helper'); const { LADDER_LEVEL_NAMES } = require('../config/constants'); const { Op } = require('sequelize'); @@ -123,13 +123,15 @@ class UserController { if (!user) { // 新用户注册 + // 规范化头像URL:如果是 https 则保持 https,否则是 http + const normalizedAvatar = avatar ? normalizeAvatarUrl(avatar, req) : ''; user = await User.create({ openid, unionid, phone, member_code: generateMemberCode(), nickname: nickname || '新用户', - avatar: avatar || '', + avatar: normalizedAvatar, gender: gender || 0, status: 1 }); @@ -140,7 +142,8 @@ class UserController { // 更新用户信息 const updateData = { phone }; if (nickname) updateData.nickname = nickname; - if (avatar) updateData.avatar = avatar; + // 规范化头像URL:如果是 https 则保持 https,否则是 http + if (avatar) updateData.avatar = normalizeAvatarUrl(avatar, req); if (gender !== undefined) updateData.gender = gender; await user.update(updateData); @@ -234,7 +237,8 @@ class UserController { const updateData = {}; if (nickname) updateData.nickname = nickname; - if (avatar) updateData.avatar = avatar; + // 规范化头像URL:如果是 https 则保持 https,否则是 http + if (avatar) updateData.avatar = normalizeAvatarUrl(avatar, req); if (gender !== undefined) updateData.gender = gender; await user.update(updateData); diff --git a/server/src/routes/upload.js b/server/src/routes/upload.js index 1d168a35..cee18540 100644 --- a/server/src/routes/upload.js +++ b/server/src/routes/upload.js @@ -4,7 +4,7 @@ const multer = require('multer'); const path = require('path'); const { v4: uuidv4 } = require('uuid'); const { authAdmin, authUser } = require('../middlewares/auth'); -const { success, error, getFullUrl } = require('../utils/helper'); +const { success, error, getFullUrl, normalizeAvatarUrl } = require('../utils/helper'); // 配置文件存储 const storage = multer.diskStorage({ @@ -66,7 +66,8 @@ router.post('/avatar', authUser, upload.single('file'), (req, res) => { } const relativePath = `/uploads/${req.file.filename}`; - const fullUrl = getFullUrl(relativePath, req); + // 使用 normalizeAvatarUrl 确保 https 保持 https,http 保持 http + const fullUrl = normalizeAvatarUrl(relativePath, req); res.json(success({ url: fullUrl }, '上传成功')); }); diff --git a/server/src/utils/helper.js b/server/src/utils/helper.js index ba282936..810a50d5 100644 --- a/server/src/utils/helper.js +++ b/server/src/utils/helper.js @@ -91,6 +91,65 @@ function pageResult(list, total, page, pageSize) { }; } +/** + * 规范化头像URL(存入数据库时使用) + * 如果是 https 则保持 https,否则是 http + * @param {string} url - 头像URL(可能是完整URL或相对路径) + * @param {object} req - Express request对象(用于获取host) + * @returns {string} 规范化后的URL + */ +function normalizeAvatarUrl(url, req) { + if (!url) return ''; + + // 如果已经是完整URL,保持原样(https保持https,http保持http) + if (url.startsWith('https://')) { + return url; + } + if (url.startsWith('http://')) { + return url; + } + + // 相对路径,根据请求判断协议 + // 支持反向代理(nginx等)的 HTTPS 检测 + let protocol = 'http'; + if (req) { + // 优先使用 X-Forwarded-Proto 头(反向代理设置) + const forwardedProto = req.get('X-Forwarded-Proto'); + if (forwardedProto) { + protocol = forwardedProto; + } else { + // 使用 req.protocol(Express 会自动处理) + protocol = req.protocol; + } + + // 如果设置了环境变量强制使用 HTTPS + if (process.env.FORCE_HTTPS === 'true') { + protocol = 'https'; + } + } + + // 获取主机地址 + let host = `localhost:${process.env.PORT || 3000}`; + if (req) { + // 优先使用 X-Forwarded-Host(反向代理设置) + const forwardedHost = req.get('X-Forwarded-Host'); + if (forwardedHost) { + host = forwardedHost; + } else { + host = req.get('host') || host; + } + } + + // 如果设置了 BASE_URL 环境变量,直接使用 + if (process.env.BASE_URL) { + const baseUrl = process.env.BASE_URL.replace(/\/$/, ''); // 移除末尾斜杠 + const pathWithoutLeadingSlash = url.startsWith('/') ? url : `/${url}`; + return `${baseUrl}${pathWithoutLeadingSlash}`; + } + + return `${protocol}://${host}${url}`; +} + /** * 获取完整URL(处理头像等资源路径) * @param {string} path - 相对路径或完整URL @@ -155,5 +214,6 @@ module.exports = { success, error, pageResult, - getFullUrl + getFullUrl, + normalizeAvatarUrl };