diff --git a/server/MiAssessment/src/MiAssessment.Api/Controllers/UploadController.cs b/server/MiAssessment/src/MiAssessment.Api/Controllers/UploadController.cs
new file mode 100644
index 0000000..fc01424
--- /dev/null
+++ b/server/MiAssessment/src/MiAssessment.Api/Controllers/UploadController.cs
@@ -0,0 +1,76 @@
+using System.Security.Claims;
+using MiAssessment.Core.Interfaces;
+using MiAssessment.Model.Base;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace MiAssessment.Api.Controllers;
+
+///
+/// 上传控制器 - 提供COS预签名URL供小程序直传
+///
+[ApiController]
+[Route("api")]
+public class UploadController : ControllerBase
+{
+ private readonly IUploadConfigService _uploadConfigService;
+ private readonly ILogger _logger;
+
+ public UploadController(
+ IUploadConfigService uploadConfigService,
+ ILogger logger)
+ {
+ _uploadConfigService = uploadConfigService;
+ _logger = logger;
+ }
+
+ ///
+ /// 获取COS预签名上传URL
+ /// POST /api/upload/presignedUrl
+ /// 小程序端拿到URL后直传COS,上传完成后将fileUrl提交给update_userinfo
+ ///
+ [HttpPost("upload/presignedUrl")]
+ [Authorize]
+ public async Task> GetPresignedUrl([FromBody] GetPresignedUrlRequest request)
+ {
+ if (string.IsNullOrWhiteSpace(request.FileName))
+ {
+ return ApiResponse.Fail("文件名不能为空");
+ }
+
+ try
+ {
+ var result = await _uploadConfigService.GetPresignedUploadUrlAsync(
+ request.FileName,
+ request.ContentType ?? "image/png");
+
+ if (result == null)
+ {
+ return ApiResponse.Fail("当前不支持COS直传,请联系管理员配置上传设置");
+ }
+
+ return ApiResponse.Success(result);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "获取预签名URL失败");
+ return ApiResponse.Fail("获取上传地址失败");
+ }
+ }
+}
+
+///
+/// 获取预签名URL请求
+///
+public class GetPresignedUrlRequest
+{
+ ///
+ /// 原始文件名
+ ///
+ public string FileName { get; set; } = string.Empty;
+
+ ///
+ /// 文件MIME类型
+ ///
+ public string? ContentType { get; set; }
+}
diff --git a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs
new file mode 100644
index 0000000..3a84fb2
--- /dev/null
+++ b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs
@@ -0,0 +1,37 @@
+namespace MiAssessment.Core.Interfaces;
+
+///
+/// 上传配置服务接口
+/// 从Admin库读取上传配置,生成COS预签名URL
+///
+public interface IUploadConfigService
+{
+ ///
+ /// 获取COS预签名上传URL
+ ///
+ /// 原始文件名
+ /// 文件MIME类型
+ /// 预签名URL信息,null表示不支持COS直传
+ Task GetPresignedUploadUrlAsync(string fileName, string contentType);
+}
+
+///
+/// 预签名上传信息
+///
+public class PresignedUploadInfo
+{
+ ///
+ /// 预签名上传URL
+ ///
+ public string UploadUrl { get; set; } = string.Empty;
+
+ ///
+ /// 文件最终访问URL
+ ///
+ public string FileUrl { get; set; } = string.Empty;
+
+ ///
+ /// URL过期时间(秒)
+ ///
+ public int ExpiresIn { get; set; }
+}
diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs
new file mode 100644
index 0000000..e818527
--- /dev/null
+++ b/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs
@@ -0,0 +1,187 @@
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using MiAssessment.Core.Interfaces;
+using MiAssessment.Model.Data;
+using MiAssessment.Model.Models.Config;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace MiAssessment.Core.Services;
+
+///
+/// 上传配置服务实现
+/// 从Admin库读取COS配置,生成预签名URL供小程序直传
+///
+public class UploadConfigService : IUploadConfigService
+{
+ private readonly AdminConfigReadDbContext _adminConfigDbContext;
+ private readonly IRedisService _redisService;
+ private readonly ILogger _logger;
+
+ private const string CacheKey = "upload:setting";
+ private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
+ private const string UploadBasePath = "uploads";
+ private const int DefaultExpiresInSeconds = 600;
+
+ public UploadConfigService(
+ AdminConfigReadDbContext adminConfigDbContext,
+ IRedisService redisService,
+ ILogger logger)
+ {
+ _adminConfigDbContext = adminConfigDbContext;
+ _redisService = redisService;
+ _logger = logger;
+ }
+
+ ///
+ public async Task GetPresignedUploadUrlAsync(string fileName, string contentType)
+ {
+ var setting = await GetUploadSettingAsync();
+ if (setting == null || setting.Type != "3")
+ {
+ _logger.LogWarning("上传配置不支持COS直传,当前类型: {Type}", setting?.Type);
+ return null;
+ }
+
+ var validationError = ValidateConfig(setting);
+ if (validationError != null)
+ {
+ _logger.LogWarning("COS配置验证失败: {Error}", validationError);
+ return null;
+ }
+
+ // 生成日期目录和唯一文件名
+ var now = DateTime.Now;
+ var datePath = $"{now.Year}/{now.Month:D2}/{now.Day:D2}";
+ var extension = Path.GetExtension(fileName).ToLowerInvariant();
+ var timestamp = now.ToString("yyyyMMddHHmmssfff");
+ var guid = Guid.NewGuid().ToString("N")[..8];
+ var uniqueFileName = $"{timestamp}_{guid}{extension}";
+ var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}";
+
+ // 生成预签名URL
+ var presignedUrl = GeneratePresignedUrl(setting, objectKey, "PUT", contentType, DefaultExpiresInSeconds);
+ var fileUrl = GenerateAccessUrl(setting.Domain!, objectKey);
+
+ _logger.LogInformation("生成预签名URL成功: {ObjectKey}", objectKey);
+
+ return new PresignedUploadInfo
+ {
+ UploadUrl = presignedUrl,
+ FileUrl = fileUrl,
+ ExpiresIn = DefaultExpiresInSeconds
+ };
+ }
+
+ ///
+ /// 从数据库读取上传配置(带缓存)
+ ///
+ private async Task GetUploadSettingAsync()
+ {
+ // 尝试从缓存读取
+ var cachedJson = await _redisService.GetStringAsync(CacheKey);
+ if (!string.IsNullOrEmpty(cachedJson))
+ {
+ try
+ {
+ return JsonSerializer.Deserialize(cachedJson, JsonOptions);
+ }
+ catch { }
+ }
+
+ try
+ {
+ var configValue = await _adminConfigDbContext.AdminConfigs
+ .Where(c => c.ConfigKey == "upload_setting")
+ .Select(c => c.ConfigValue)
+ .FirstOrDefaultAsync();
+
+ if (string.IsNullOrEmpty(configValue))
+ {
+ _logger.LogWarning("未找到upload_setting配置");
+ return null;
+ }
+
+ var setting = JsonSerializer.Deserialize(configValue, JsonOptions);
+ if (setting != null)
+ {
+ await _redisService.SetStringAsync(CacheKey, configValue, CacheDuration);
+ }
+ return setting;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "读取上传配置失败");
+ return null;
+ }
+ }
+
+ ///
+ /// 生成腾讯云COS预签名URL
+ ///
+ private static string GeneratePresignedUrl(UploadSetting setting, string objectKey, string httpMethod, string contentType, int expiresInSeconds)
+ {
+ var startTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ var endTime = startTime + expiresInSeconds;
+ var keyTime = $"{startTime};{endTime}";
+
+ var host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com";
+ var urlPath = $"/{objectKey}";
+
+ // 1. SignKey
+ var signKey = HmacSha1(setting.AccessKeySecret!, keyTime);
+
+ // 2. HttpString
+ var httpString = $"{httpMethod.ToLowerInvariant()}\n{urlPath}\n\nhost={host.ToLowerInvariant()}\n";
+
+ // 3. StringToSign
+ var sha1HttpString = Sha1Hash(httpString);
+ var stringToSign = $"sha1\n{keyTime}\n{sha1HttpString}\n";
+
+ // 4. Signature
+ var signature = HmacSha1(signKey, stringToSign);
+
+ // 5. Authorization
+ var authorization = $"q-sign-algorithm=sha1&q-ak={setting.AccessKeyId}&q-sign-time={keyTime}&q-key-time={keyTime}&q-header-list=host&q-url-param-list=&q-signature={signature}";
+
+ return $"https://{host}{urlPath}?{authorization}";
+ }
+
+ private static string GenerateAccessUrl(string domain, string objectKey)
+ {
+ var normalizedDomain = domain.TrimEnd('/');
+ if (!normalizedDomain.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
+ !normalizedDomain.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ normalizedDomain = $"https://{normalizedDomain}";
+ }
+ var normalizedKey = objectKey.StartsWith('/') ? objectKey : $"/{objectKey}";
+ return $"{normalizedDomain}{normalizedKey}";
+ }
+
+ private static string HmacSha1(string key, string data)
+ {
+ using var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key));
+ var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+
+ private static string Sha1Hash(string data)
+ {
+ var hash = SHA1.HashData(Encoding.UTF8.GetBytes(data));
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+
+ private static string? ValidateConfig(UploadSetting setting)
+ {
+ if (string.IsNullOrWhiteSpace(setting.Bucket)) return "Bucket不能为空";
+ if (string.IsNullOrWhiteSpace(setting.Region)) return "Region不能为空";
+ if (string.IsNullOrWhiteSpace(setting.AccessKeyId)) return "AccessKeyId不能为空";
+ if (string.IsNullOrWhiteSpace(setting.AccessKeySecret)) return "AccessKeySecret不能为空";
+ if (string.IsNullOrWhiteSpace(setting.Domain)) return "Domain不能为空";
+ return null;
+ }
+
+ private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
+}
diff --git a/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs b/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs
index 92f6a62..de49e37 100644
--- a/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs
+++ b/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs
@@ -191,6 +191,17 @@ public class ServiceModule : Module
return new SystemService(configService, logger);
}).As().InstancePerLifetimeScope();
+ // ========== 上传模块服务注册 ==========
+
+ // 注册上传配置服务(从Admin库读取COS配置,生成预签名URL)
+ builder.Register(c =>
+ {
+ var adminConfigDbContext = c.Resolve();
+ var redisService = c.Resolve();
+ var logger = c.Resolve>();
+ return new UploadConfigService(adminConfigDbContext, redisService, logger);
+ }).As().InstancePerLifetimeScope();
+
// ========== 小程序团队模块服务注册 ==========
// 注册团队服务
diff --git a/server/MiAssessment/src/MiAssessment.Model/Models/Config/UploadSetting.cs b/server/MiAssessment/src/MiAssessment.Model/Models/Config/UploadSetting.cs
new file mode 100644
index 0000000..bffd9ce
--- /dev/null
+++ b/server/MiAssessment/src/MiAssessment.Model/Models/Config/UploadSetting.cs
@@ -0,0 +1,42 @@
+namespace MiAssessment.Model.Models.Config;
+
+///
+/// 上传配置(Model层,供Core/Api项目使用)
+///
+public class UploadSetting
+{
+ ///
+ /// 存储类型 1本地 2阿里云 3腾讯云
+ ///
+ public string Type { get; set; } = "1";
+
+ ///
+ /// 腾讯云AppId
+ ///
+ public string? AppId { get; set; }
+
+ ///
+ /// 存储桶名称
+ ///
+ public string? Bucket { get; set; }
+
+ ///
+ /// 地域
+ ///
+ public string? Region { get; set; }
+
+ ///
+ /// SecretId
+ ///
+ public string? AccessKeyId { get; set; }
+
+ ///
+ /// SecretKey
+ ///
+ public string? AccessKeySecret { get; set; }
+
+ ///
+ /// 访问域名
+ ///
+ public string? Domain { get; set; }
+}
diff --git a/uniapp/api/user.js b/uniapp/api/user.js
index de027dc..59d8110 100644
--- a/uniapp/api/user.js
+++ b/uniapp/api/user.js
@@ -6,6 +6,7 @@ import { get, post } from './request'
/**
* 获取当前登录用户信息
+ * GET /api/userInfo
* @returns {Promise