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:
ethanfly 2026-01-26 05:29:00 +08:00
parent 15413c85cc
commit 90505cba5f
5 changed files with 118 additions and 13 deletions

View File

@ -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=
# 强制使用HTTPStrue/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.protocolExpress 自动检测)
# e) 默认使用 http://
#
# 反向代理配置建议nginx
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header X-Forwarded-Host $host;
#
# 这样系统可以自动识别 HTTPS 请求,无需设置 FORCE_HTTPS
# ------------------------------------------
# 其他配置(可选)
# ------------------------------------------

View File

@ -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);

View File

@ -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);

View File

@ -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 保持 httpshttp 保持 http
const fullUrl = normalizeAvatarUrl(relativePath, req);
res.json(success({ url: fullUrl }, '上传成功'));
});

View File

@ -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保持httpshttp保持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.protocolExpress 会自动处理)
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
};