Enhance avatar URL handling by adding normalization logic to ensure consistent URL formats across user and admin profiles. Update environment template with detailed comments on BASE_URL and FORCE_HTTPS settings. Improve upload route to utilize normalized avatar URLs.
This commit is contained in:
parent
15413c85cc
commit
90505cba5f
@ -16,15 +16,29 @@ NODE_ENV=development
|
|||||||
# 服务端口(默认3000)
|
# 服务端口(默认3000)
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
# 基础URL(用于生成完整的文件URL,支持HTTPS)
|
# 基础URL(用于生成完整的文件URL和头像URL,支持HTTPS)
|
||||||
# 示例:https://api.example.com 或 http://localhost:3000
|
# 示例:https://api.example.com 或 http://localhost:3000
|
||||||
# 如果设置了此项,上传接口返回的URL将使用此值
|
#
|
||||||
# 如果不设置,将根据请求自动检测协议(支持反向代理)
|
# 作用:
|
||||||
|
# 1. 如果设置了此项,上传接口返回的URL将使用此值
|
||||||
|
# 2. 头像URL存入数据库时会使用此值(如果是相对路径)
|
||||||
|
# 3. 如果不设置,将根据请求自动检测协议(支持反向代理)
|
||||||
|
#
|
||||||
|
# 推荐设置场景:
|
||||||
|
# - 生产环境:设置为完整的 HTTPS URL,如 https://api.example.com
|
||||||
|
# - 开发环境:可以留空,系统会自动检测
|
||||||
BASE_URL=
|
BASE_URL=
|
||||||
|
|
||||||
# 强制使用HTTPS(true/false)
|
# 强制使用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
|
FORCE_HTTPS=false
|
||||||
|
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
@ -78,6 +92,26 @@ UPLOAD_MAX_SIZE=10
|
|||||||
# 上传文件存储路径
|
# 上传文件存储路径
|
||||||
UPLOAD_PATH=uploads
|
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
|
||||||
|
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
# 其他配置(可选)
|
# 其他配置(可选)
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|||||||
@ -468,7 +468,13 @@ class AdminController {
|
|||||||
async updateProfile(req, res) {
|
async updateProfile(req, res) {
|
||||||
try {
|
try {
|
||||||
const { real_name, phone, avatar } = req.body;
|
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, '更新成功'));
|
res.json(success(null, '更新成功'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('更新个人信息失败:', err);
|
console.error('更新个人信息失败:', err);
|
||||||
|
|||||||
@ -3,7 +3,7 @@ const axios = require('axios');
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const QRCode = require('qrcode');
|
const QRCode = require('qrcode');
|
||||||
const { User, LadderUser, Store, Match, MatchGame } = require('../models');
|
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 { LADDER_LEVEL_NAMES } = require('../config/constants');
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
@ -123,13 +123,15 @@ class UserController {
|
|||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// 新用户注册
|
// 新用户注册
|
||||||
|
// 规范化头像URL:如果是 https 则保持 https,否则是 http
|
||||||
|
const normalizedAvatar = avatar ? normalizeAvatarUrl(avatar, req) : '';
|
||||||
user = await User.create({
|
user = await User.create({
|
||||||
openid,
|
openid,
|
||||||
unionid,
|
unionid,
|
||||||
phone,
|
phone,
|
||||||
member_code: generateMemberCode(),
|
member_code: generateMemberCode(),
|
||||||
nickname: nickname || '新用户',
|
nickname: nickname || '新用户',
|
||||||
avatar: avatar || '',
|
avatar: normalizedAvatar,
|
||||||
gender: gender || 0,
|
gender: gender || 0,
|
||||||
status: 1
|
status: 1
|
||||||
});
|
});
|
||||||
@ -140,7 +142,8 @@ class UserController {
|
|||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
const updateData = { phone };
|
const updateData = { phone };
|
||||||
if (nickname) updateData.nickname = nickname;
|
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;
|
if (gender !== undefined) updateData.gender = gender;
|
||||||
|
|
||||||
await user.update(updateData);
|
await user.update(updateData);
|
||||||
@ -234,7 +237,8 @@ class UserController {
|
|||||||
|
|
||||||
const updateData = {};
|
const updateData = {};
|
||||||
if (nickname) updateData.nickname = nickname;
|
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;
|
if (gender !== undefined) updateData.gender = gender;
|
||||||
|
|
||||||
await user.update(updateData);
|
await user.update(updateData);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ const multer = require('multer');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { authAdmin, authUser } = require('../middlewares/auth');
|
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({
|
const storage = multer.diskStorage({
|
||||||
@ -66,7 +66,8 @@ router.post('/avatar', authUser, upload.single('file'), (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const relativePath = `/uploads/${req.file.filename}`;
|
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 }, '上传成功'));
|
res.json(success({ url: fullUrl }, '上传成功'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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(处理头像等资源路径)
|
* 获取完整URL(处理头像等资源路径)
|
||||||
* @param {string} path - 相对路径或完整URL
|
* @param {string} path - 相对路径或完整URL
|
||||||
@ -155,5 +214,6 @@ module.exports = {
|
|||||||
success,
|
success,
|
||||||
error,
|
error,
|
||||||
pageResult,
|
pageResult,
|
||||||
getFullUrl
|
getFullUrl,
|
||||||
|
normalizeAvatarUrl
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user