# 阿里云OSS图片上传方法说明 **文档日期**:2025年1月 **文件位置**:`netcore/src/Modularity/System/NCC.System/Service/Common/FileService.cs` --- ## 一、核心上传方法 ### 1.1 标准文件上传方法 **方法名**:`Uploader` **位置**:`FileService.cs` 第60-95行 **接口路径**:`POST /api/File/Uploader/{type}` **功能**: - 上传文件/图片到服务器或OSS - 支持多种存储类型(本地、MinIO、阿里云OSS、腾讯云COS) - `annexpic` 类型强制使用阿里云OSS存储 **参数**: - `type`:文件类型(如:`annexpic`、`avatar`、`temporary` 等) - `file`:上传的文件(`IFormFile`) **返回值**: ```json { "name": "原始文件名.jpg", "fileId": "20250123_123456789.jpg", "url": "https://oss.example.com/2025/01/23/20250123_123456789.jpg?签名参数" } ``` **关键代码**: ```csharp [HttpPost("Uploader/{type}")] [AllowAnonymous] public async Task Uploader(string type, IFormFile file) { // 1. 验证文件类型 var fileType = Path.GetExtension(file.FileName).Replace(".", ""); if (!this.AllowFileType(fileType, type)) throw NCCException.Oh(ErrorCode.D1800); // 2. 生成文件路径和文件名 var _filePath = GetPathByType(type); var now = DateTime.Now; var _fileName = now.ToString("yyyyMMdd") + "_" + YitIdHelper.NextId().ToString() + Path.GetExtension(file.FileName); // 3. annexpic 类型强制使用阿里云OSS存储 string forceStoreType = type == "annexpic" ? "aliyun-oss" : null; string uploadFilePath = _filePath; if (type == "annexpic") { // 按天生成文件夹:yyyy/MM/dd var dateFolder = now.ToString("yyyy/MM/dd"); uploadFilePath = dateFolder; } // 4. 上传文件 await UploadFileByType(file, uploadFilePath, _fileName, forceStoreType); // 5. 获取访问URL string fileUrl; if (type == "annexpic") { fileUrl = await GetOSSAccessUrl(uploadFilePath, _fileName); } else { fileUrl = string.Format("/api/File/Image/{0}/{1}", type, _fileName); } return new { name = file.FileName, fileId = _fileName, url = fileUrl }; } ``` --- ### 1.2 Base64图片上传方法 **方法名**:`UploadBase64Image` **位置**:`FileService.cs` 第696-775行 **接口路径**:`POST /api/File/UploadBase64Image` **功能**: - 上传Base64格式的图片到阿里云OSS - 自动解析Base64数据并提取图片格式 - 所有类型都上传到阿里云OSS存储 **参数**(`Base64ImageUploadInput`): ```json { "base64Data": "data:image/jpeg;base64,/9j/4AAQSkZJRg...", "fileName": "图片名称(可选)", "imageType": "annexpic(可选,默认为temporary)" } ``` **返回值**: ```json { "name": "图片名称.jpg", "fileId": "20250123_123456789.jpg", "url": "https://oss.example.com/2025/01/23/20250123_123456789.jpg?签名参数", "fileSize": 12345, "imageFormat": "JPEG", "imageType": "annexpic" } ``` **关键代码**: ```csharp [HttpPost("UploadBase64Image")] [AllowAnonymous] public async Task UploadBase64Image([FromBody] Base64ImageUploadInput input) { // 1. 解析Base64数据 var imageData = ParseBase64Data(input.Base64Data, out string imageFormat); // 2. 验证图片格式 if (!IsValidImageFormat(imageFormat)) throw NCCException.Oh($"不支持的图片格式: {imageFormat}"); // 3. 生成文件路径和文件名 var imageType = string.IsNullOrEmpty(input.ImageType) ? "temporary" : input.ImageType; var now = DateTime.Now; string uploadFilePath; string fileName; if (imageType == "annexpic") { fileName = now.ToString("yyyyMMdd") + "_" + YitIdHelper.NextId().ToString() + "." + imageFormat; var dateFolder = now.ToString("yyyy/MM/dd"); uploadFilePath = dateFolder; } else { fileName = GenerateImageFileName(input.FileName, imageFormat); var originalPath = GetPathByType(imageType).TrimEnd('/').TrimEnd('\\'); var dateFolder = now.ToString("yyyy/MM/dd"); uploadFilePath = $"{originalPath}/{dateFolder}"; } // 4. 上传到OSS var bucketName = KeyVariable.BucketName; var ossPath = $"{uploadFilePath.TrimEnd('/').TrimEnd('\\')}/{fileName}"; using (var stream = new MemoryStream(imageData)) { await _oSSServiceFactory.Create("aliyun").PutObjectAsync(bucketName, ossPath, stream); } // 5. 获取OSS访问URL string accessUrl = await GetOSSAccessUrl(uploadFilePath, fileName); return new { name = originalFileName, fileId = fileName, url = accessUrl, fileSize = imageData.Length, imageFormat = imageFormat.ToUpper(), imageType = imageType, }; } ``` --- ## 二、核心上传逻辑 ### 2.1 UploadFileByType 方法 **位置**:`FileService.cs` 第301-344行 **功能**:根据存储类型上传文件 **关键代码**: ```csharp [NonAction] public async Task UploadFileByType(IFormFile file, string filePath, string fileName, string forceStoreType = null) { var bucketName = KeyVariable.BucketName; var fileStoreType = !string.IsNullOrEmpty(forceStoreType) ? forceStoreType : KeyVariable.FileStoreType; // OSS路径使用正斜杠,不使用Path.Combine var uploadPath = fileStoreType == "aliyun-oss" || fileStoreType == "tencent-cos" || fileStoreType == "minio" ? $"{filePath.TrimEnd('/').TrimEnd('\\')}/{fileName}" : Path.Combine(filePath, fileName); var stream = file.OpenReadStream(); switch (fileStoreType) { case "minio": await _oSSServiceFactory.Create().PutObjectAsync(bucketName, uploadPath, stream); break; case "aliyun-oss": // ✅ 阿里云OSS上传 await _oSSServiceFactory.Create("aliyun").PutObjectAsync(bucketName, uploadPath, stream); break; case "tencent-cos": await _oSSServiceFactory.Create("qcloud").PutObjectAsync(bucketName, uploadPath, stream); break; default: // 本地存储 if (!Directory.Exists(filePath)) Directory.CreateDirectory(filePath); using (var stream4 = File.Create(uploadPath)) { await file.CopyToAsync(stream4); } break; } } ``` **关键点**: - ✅ 使用 `_oSSServiceFactory.Create("aliyun")` 创建阿里云OSS服务 - ✅ 使用 `PutObjectAsync(bucketName, uploadPath, stream)` 上传文件 - ✅ OSS路径使用正斜杠 `/`,不使用 `Path.Combine` --- ### 2.2 GetOSSAccessUrl 方法 **位置**:`FileService.cs` 第391-476行 **功能**:获取阿里云OSS文件的访问URL(带签名的临时访问URL) **关键代码**: ```csharp [NonAction] private async Task GetOSSAccessUrl(string filePath, string fileName) { var bucketName = KeyVariable.BucketName; var uploadPath = $"{filePath.TrimEnd('/').TrimEnd('\\')}/{fileName}"; // 使用OSS服务生成带签名的临时访问URL(有效期24小时) var ossService = _oSSServiceFactory.Create("aliyun"); var presignedUrl = await ossService.PresignedGetObjectAsync(bucketName, uploadPath, 86400); // 获取带签名的URL字符串 string urlString = string.Empty; if (presignedUrl != null) { var urlType = presignedUrl.GetType(); var absoluteUriProp = urlType.GetProperty("AbsoluteUri"); if (absoluteUriProp != null) { urlString = absoluteUriProp.GetValue(presignedUrl)?.ToString() ?? string.Empty; } else { urlString = presignedUrl.ToString() ?? string.Empty; } } // 如果配置了自定义域名,替换为自定义域名 var customDomain = _configuration["NCC_App:AliyunOSS:CustomDomain"] ?? _configuration["NCC_APP:AliyunOSS:CustomDomain"]; if (!string.IsNullOrEmpty(customDomain)) { // 替换域名逻辑... } return urlString; } ``` **关键点**: - ✅ 使用 `PresignedGetObjectAsync` 生成带签名的临时访问URL - ✅ 有效期:86400秒(24小时) - ✅ 支持自定义域名配置 --- ## 三、OSS服务配置 ### 3.1 服务注册 **位置**:`Startup.cs` 第109-137行 **配置代码**: ```csharp #region 阿里云OSS var aliyunOSSEndpoint = App.Configuration["NCC_App:AliyunOSS:Endpoint"]; var aliyunOSSAccessKey = App.Configuration["NCC_App:AliyunOSS:AccessKeyId"]; var aliyunOSSSecretKey = App.Configuration["NCC_App:AliyunOSS:AccessKeySecret"]; var aliyunOSSRegion = App.Configuration["NCC_App:AliyunOSS:Region"]; var bucketName = App.Configuration["NCC_App:BucketName"]; if (!string.IsNullOrEmpty(aliyunOSSEndpoint) && !string.IsNullOrEmpty(aliyunOSSAccessKey) && !string.IsNullOrEmpty(aliyunOSSSecretKey)) { services.AddOSSService("aliyun", option => { option.Provider = OSSProvider.Aliyun; option.Endpoint = aliyunOSSEndpoint; // 格式:oss-{region}.aliyuncs.com option.AccessKey = aliyunOSSAccessKey; option.SecretKey = aliyunOSSSecretKey; option.IsEnableHttps = true; option.IsEnableCache = true; if (!string.IsNullOrEmpty(aliyunOSSRegion)) { option.Region = aliyunOSSRegion; // 如:cn-chengdu } }); } #endregion ``` ### 3.2 配置文件 **位置**:`appsettings.json` **配置项**: ```json { "NCC_App": { "AliyunOSS": { "Endpoint": "oss-cn-chengdu.aliyuncs.com", "AccessKeyId": "your-access-key-id", "AccessKeySecret": "your-access-key-secret", "Region": "cn-chengdu", "CustomDomain": "https://cdn.example.com" // 可选,自定义域名 }, "BucketName": "your-bucket-name", "FileStoreType": "aliyun-oss" // 默认存储类型 } } ``` --- ## 四、使用示例 ### 4.1 标准文件上传 **前端调用**: ```javascript // 使用 FormData const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/File/Uploader/annexpic', { method: 'POST', body: formData }); const result = await response.json(); // result: { name: "原始文件名.jpg", fileId: "20250123_123456789.jpg", url: "https://..." } ``` **curl 示例**: ```bash curl -X POST "http://localhost:2011/api/File/Uploader/annexpic" \ -H "Authorization: Bearer YOUR_TOKEN" \ -F "file=@/path/to/image.jpg" ``` --- ### 4.2 Base64图片上传 **前端调用**: ```javascript const base64Data = "data:image/jpeg;base64,/9j/4AAQSkZJRg..."; const response = await fetch('/api/File/UploadBase64Image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ base64Data: base64Data, fileName: "图片名称", imageType: "annexpic" }) }); const result = await response.json(); // result: { name: "图片名称.jpg", fileId: "20250123_123456789.jpg", url: "https://...", fileSize: 12345, imageFormat: "JPEG", imageType: "annexpic" } ``` **curl 示例**: ```bash curl -X POST "http://localhost:2011/api/File/UploadBase64Image" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "base64Data": "data:image/jpeg;base64,/9j/4AAQSkZJRg...", "fileName": "图片名称", "imageType": "annexpic" }' ``` --- ## 五、文件路径规则 ### 5.1 annexpic 类型 - **文件夹结构**:`yyyy/MM/dd`(如:`2025/01/23`) - **文件名格式**:`yyyyMMdd_{ID}.{ext}`(如:`20250123_123456789.jpg`) - **完整路径**:`2025/01/23/20250123_123456789.jpg` - **存储类型**:强制使用阿里云OSS ### 5.2 其他类型 - **文件夹结构**:`{原始路径}/yyyy/MM/dd` - **文件名格式**:根据类型生成 - **存储类型**:根据配置决定(`KeyVariable.FileStoreType`) --- ## 六、依赖服务 ### 6.1 IOSSServiceFactory **接口**:`IOSSServiceFactory` **实现**:`OnceMi.AspNetCore.OSS` 库 **使用方式**: ```csharp // 创建阿里云OSS服务 var ossService = _oSSServiceFactory.Create("aliyun"); // 上传文件 await ossService.PutObjectAsync(bucketName, uploadPath, stream); // 生成预签名URL var presignedUrl = await ossService.PresignedGetObjectAsync(bucketName, uploadPath, 86400); ``` --- ## 七、注意事项 ### 7.1 路径格式 - ✅ OSS路径使用正斜杠 `/`,不使用 `Path.Combine` - ✅ 路径格式:`{filePath}/{fileName}` ### 7.2 文件命名 - ✅ 文件名格式:`yyyyMMdd_{ID}.{ext}` - ✅ 使用 `YitIdHelper.NextId()` 生成唯一ID ### 7.3 访问URL - ✅ 返回带签名的临时访问URL(有效期24小时) - ✅ 支持自定义域名配置 - ✅ 如果生成失败,返回相对路径作为降级方案 ### 7.4 错误处理 - ✅ 上传失败时抛出异常,包含详细错误信息 - ✅ URL生成失败时返回相对路径 --- ## 八、相关文件 - **主服务文件**:`netcore/src/Modularity/System/NCC.System/Service/Common/FileService.cs` - **服务注册**:`netcore/src/Application/NCC.API.Core/Startup.cs` - **配置文件**:`netcore/src/Application/NCC.API/appsettings.json` - **依赖库**:`OnceMi.AspNetCore.OSS` --- **文档完成时间**:2025年1月 **文档状态**:✅ **已完成**