feat: 支持配置文件存储路径和URL前缀

- 新增 FileStorageOptions 配置类
- 修改 FileUploadService 使用配置化路径
- 添加静态文件中间件支持文件访问
- 修复前端图片路径拼接问题
This commit is contained in:
code@server 2026-01-15 21:40:51 +08:00
parent 29cfc4d700
commit d089e6061d
5 changed files with 38 additions and 9 deletions

View File

@ -0,0 +1,16 @@
namespace MilitaryTrainingManagement.Configuration;
public class FileStorageOptions
{
public const string SectionName = "FileStorage";
/// <summary>
/// 文件存储基础路径(相对或绝对路径)
/// </summary>
public string BasePath { get; set; } = "./wwwroot/uploads";
/// <summary>
/// 访问文件的 URL 前缀
/// </summary>
public string UrlPrefix { get; set; } = "/uploads";
}

View File

@ -7,6 +7,7 @@ using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using MilitaryTrainingManagement.Authorization; using MilitaryTrainingManagement.Authorization;
using MilitaryTrainingManagement.Configuration;
using MilitaryTrainingManagement.Data; using MilitaryTrainingManagement.Data;
using MilitaryTrainingManagement.Models.Enums; using MilitaryTrainingManagement.Models.Enums;
using MilitaryTrainingManagement.Services.Implementations; using MilitaryTrainingManagement.Services.Implementations;
@ -14,6 +15,10 @@ using MilitaryTrainingManagement.Services.Interfaces;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// 配置文件存储选项
builder.Services.Configure<FileStorageOptions>(
builder.Configuration.GetSection(FileStorageOptions.SectionName));
// 添加HttpContextAccessor用于审计拦截器 // 添加HttpContextAccessor用于审计拦截器
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
@ -138,6 +143,9 @@ var app = builder.Build();
// 配置HTTP请求管道 // 配置HTTP请求管道
// 启用静态文件服务
app.UseStaticFiles();
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();

View File

@ -1,4 +1,6 @@
using MilitaryTrainingManagement.Configuration;
using MilitaryTrainingManagement.Services.Interfaces; using MilitaryTrainingManagement.Services.Interfaces;
using Microsoft.Extensions.Options;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
namespace MilitaryTrainingManagement.Services.Implementations; namespace MilitaryTrainingManagement.Services.Implementations;
@ -8,8 +10,8 @@ namespace MilitaryTrainingManagement.Services.Implementations;
/// </summary> /// </summary>
public class FileUploadService : IFileUploadService public class FileUploadService : IFileUploadService
{ {
private readonly IWebHostEnvironment _environment;
private readonly ILogger<FileUploadService> _logger; private readonly ILogger<FileUploadService> _logger;
private readonly FileStorageOptions _storageOptions;
// 照片尺寸限制(放宽限制以适应各种照片) // 照片尺寸限制(放宽限制以适应各种照片)
private const int MinPhotoWidth = 100; private const int MinPhotoWidth = 100;
@ -22,9 +24,9 @@ public class FileUploadService : IFileUploadService
private static readonly string[] AllowedPhotoExtensions = { ".jpg", ".jpeg", ".png" }; private static readonly string[] AllowedPhotoExtensions = { ".jpg", ".jpeg", ".png" };
private static readonly string[] AllowedDocumentExtensions = { ".pdf", ".doc", ".docx", ".jpg", ".jpeg", ".png" }; private static readonly string[] AllowedDocumentExtensions = { ".pdf", ".doc", ".docx", ".jpg", ".jpeg", ".png" };
public FileUploadService(IWebHostEnvironment environment, ILogger<FileUploadService> logger) public FileUploadService(IOptions<FileStorageOptions> storageOptions, ILogger<FileUploadService> logger)
{ {
_environment = environment; _storageOptions = storageOptions.Value;
_logger = logger; _logger = logger;
} }
@ -108,7 +110,7 @@ public class FileUploadService : IFileUploadService
public string GetFullPath(string relativePath) public string GetFullPath(string relativePath)
{ {
return Path.Combine(_environment.WebRootPath ?? _environment.ContentRootPath, "uploads", relativePath); return Path.Combine(_storageOptions.BasePath, relativePath);
} }
private void ValidateDocument(IFormFile file) private void ValidateDocument(IFormFile file)
@ -132,7 +134,7 @@ public class FileUploadService : IFileUploadService
private async Task<string> SaveFileAsync(IFormFile file, string subFolder) private async Task<string> SaveFileAsync(IFormFile file, string subFolder)
{ {
var uploadsPath = Path.Combine(_environment.WebRootPath ?? _environment.ContentRootPath, "uploads", subFolder); var uploadsPath = Path.Combine(_storageOptions.BasePath, subFolder);
Directory.CreateDirectory(uploadsPath); Directory.CreateDirectory(uploadsPath);
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName).ToLowerInvariant()}"; var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName).ToLowerInvariant()}";
@ -144,6 +146,9 @@ public class FileUploadService : IFileUploadService
} }
_logger.LogInformation("文件已保存: {FilePath}", filePath); _logger.LogInformation("文件已保存: {FilePath}", filePath);
return Path.Combine(subFolder, fileName);
// 返回带 URL 前缀的完整访问路径
var relativePath = Path.Combine(subFolder, fileName).Replace("\\", "/");
return $"{_storageOptions.UrlPrefix.TrimEnd('/')}/{relativePath}";
} }
} }

View File

@ -12,7 +12,7 @@
<el-row :gutter="40"> <el-row :gutter="40">
<el-col :span="6"> <el-col :span="6">
<div class="photo-section"> <div class="photo-section">
<img v-if="person.photoPath" :src="`/api/files/${person.photoPath}`" class="person-photo" /> <img v-if="person.photoPath" :src="person.photoPath" class="person-photo" />
<div v-else class="no-photo"> <div v-else class="no-photo">
<el-icon :size="48"><User /></el-icon> <el-icon :size="48"><User /></el-icon>
<span>暂无照片</span> <span>暂无照片</span>
@ -64,7 +64,7 @@
<div v-if="person.supportingDocuments" class="documents-section"> <div v-if="person.supportingDocuments" class="documents-section">
<h4>佐证材料</h4> <h4>佐证材料</h4>
<el-link type="primary" :href="`/api/files/${person.supportingDocuments}`" target="_blank"> <el-link type="primary" :href="person.supportingDocuments" target="_blank">
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
查看文件 查看文件
</el-link> </el-link>

View File

@ -312,7 +312,7 @@ async function loadPersonnel() {
form.trainingParticipation = person.trainingParticipation || '' form.trainingParticipation = person.trainingParticipation || ''
form.achievements = person.achievements || '' form.achievements = person.achievements || ''
if (person.photoPath) { if (person.photoPath) {
photoPreview.value = `/api/files/${person.photoPath}` photoPreview.value = person.photoPath
} }
} catch { } catch {
ElMessage.error('加载人才信息失败') ElMessage.error('加载人才信息失败')