简介:这是一套基于C#开发的云存储平台毕业设计资源包,面向计算机类专业本科生、课程设计学习者及初阶开发者,旨在解决本地文件管理混乱、主流网盘限速与容量受限等实际问题。项目实现多格式文件上传下载、云端文件管理、快捷分享及公开文件搜索等核心功能,技术栈涵盖HTML/CSS/JS前端与C#后端、数据库及网络编程,兼顾实用性与教学适配性。压缩包共443个文件,含62个配置与资源文件(如.config、.resx)、91个运行依赖DLL、37个C#源码文件(.cs)、41个说明类文本(.txt/.md/.pdf)及92个工程元数据XML,整体61.64MB,结构完整、模块清晰,开箱即用。已有326人学习下载,资源包含可直接运行的源码、开题报告与需求文档三件套,全部通过功能测试,支持快速部署与二次开发,是毕设选题、课设实践与C#全栈入门的高价值参考范例。
1. 项目概述与核心价值
最近在带几个学生做毕业设计,发现一个挺普遍的现象:大家在做C#相关的项目时,从开题、编码到最终提交,会产生大量的文件——.csproj项目文件、.sln解决方案、各种.cs源码、第三方DLL、数据库脚本,还有开题报告、需求文档这些Word或PDF。这些文件散落在各自的电脑、U盘或者微信群里,版本混乱不说,一旦遇到电脑崩溃或者误删除,找起来简直是灾难。这让我想起了十多年前我们做项目时,用SVN都得自己搭服务器,现在虽然有了Git和网盘,但对于不熟悉版本控制的学生团队,或者小公司内部需要简单共享项目资产的情景,一个专门为C#项目文件打造的、轻量级的云存储平台,其实有它独特的价值。
这个“C#项目文件云存储平台”的核心,就是做一个类百度网盘的私有化工具,但它更懂程序员,尤其是C#程序员。它不止是简单的上传下载,更要能理解.sln、.csproj这些文件的结构,能在线预览代码(至少是语法高亮),能管理不同版本的项目快照,并且把开题报告、需求文档这些设计资料和源码本身关联起来。最终,它要成为一个从项目立项(开题报告)、设计(需求文档)到开发(源码)的全生命周期轻量级管理工具。对于学生毕设小组、初创技术团队或者个人开发者管理自己的多个小项目来说,这样一个平台能极大减少在文件管理上的精力耗散,把注意力真正集中到编码和设计本身。
2. 整体架构设计与技术选型考量
2.1 为什么选择C#作为全栈技术栈?
这个项目本身是C#项目文件的管理平台,用C#来实现几乎是必然选择,这能带来极高的技术栈统一性和开发效率。后端我们选择ASP.NET Core。它跨平台,性能优异,生态成熟,对于构建Web API和实时功能(比如文件上传进度)支持非常好。数据库方面,考虑到项目元数据(用户、文件信息、版本)的结构化程度高,且关系明确,选择SQL Server或PostgreSQL都是稳妥的方案。如果团队对SQL Server更熟悉,用它没问题;如果想完全跨平台且免费,PostgreSQL是更优解。
前端的选择就有意思了。传统上,我们可能会用Razor Pages快速套个界面,但为了更好的用户体验和更现代化的交互(比如拖拽上传、实时预览),采用前后端分离是更合适的。这里我推荐使用Blazor。Blazor允许你使用C#来写前端交互逻辑,这对于后端C#开发者来说,学习成本极低,可以真正做到全栈C#。你可以选择Blazor Server(实时性强,但依赖SignalR长连接)或Blazor WebAssembly(真正的前端SPA,首次加载稍慢)。对于管理平台这类对实时性要求不是极端高、且希望部署简单的场景,我个人更倾向于Blazor Server,开发体验流畅,调试方便。
2.2 核心功能模块拆解
平台的核心功能可以划分为四大模块:
- 用户与权限模块:负责用户注册、登录、鉴权。权限模型不需要太复杂,初期可以设计为“项目所有者”和“项目成员”两级,所有者拥有全部权限,成员可上传下载。后期可考虑增加“只读成员”角色。
- 文件存储与管理模块:这是平台的基石。不仅要实现文件的上传、下载、删除、重命名、移动等基本操作,更要实现“项目”这个逻辑容器。一个“项目”下包含所有相关的文件(源码、文档)。
- C#项目文件智能处理模块:这是体现平台价值的关键。它需要能够解析.sln和.csproj文件,构建出项目的文件树状结构,而不仅仅是平铺的文件列表。最好还能提供简单的代码文件在线预览(语法高亮)。
- 文档与版本关联模块:将开题报告、需求文档等设计文档与具体的项目版本(或Git提交)关联起来。实现简单的版本快照功能,用户可以为一个项目在某个时间点创建“存档”,这个存档包含了当时所有的文件状态,并可以关联对应的设计文档。
2.3 存储方案设计:数据库与对象存储
文件本身的存储是个重点。绝对不能把文件以varbinary(max)的形式直接存进数据库,这对数据库是灾难。标准的做法是文件路径存数据库,文件实体存磁盘或对象存储。
- 本地磁盘存储:最简单的方式,在服务器磁盘上按“用户ID/项目ID/文件哈希或时间戳”的目录结构存放文件。优点是零成本、直接可控。缺点是扩展性差,服务器迁移或做集群时文件同步麻烦。
- 对象存储服务:更专业的方案。可以使用像阿里云OSS、腾讯云COS或自建MinIO。它们提供高可用、高扩展的文件存储服务,通过API进行文件存取。平台后端只存储文件在对象存储中的唯一标识(如Key)。这是更推荐用于生产环境的方案,虽然初期有少许学习成本和费用,但为未来的扩展打下了坚实基础。
在我们的数据库表中,至少需要以下几张核心表:
Users:用户信息。Projects:项目信息(名称、描述、所有者ID、创建时间)。ProjectMembers:项目与成员的关联表。FileEntries:文件条目表。这里的设计有讲究。除了存储文件名、大小、上传时间、物理路径(或对象存储Key)外,最关键的是要存储文件的“逻辑路径”。例如,一个文件在项目中的位置可能是/Controllers/HomeController.cs。同时,还需要一个ParentId字段来构建目录树的层级关系,以及一个ProjectId关联到所属项目。DocumentLinks:文档关联表。用于关联设计文档(也是FileEntries表中的一条记录)与某个项目版本或Git提交哈希(如果集成了Git)。
3. 核心功能实现细节与实操要点
3.1 用户认证与项目权限控制
使用ASP.NET Core Identity可以快速搭建用户体系。但我们需要在其基础上增加项目级别的权限验证。我建议使用策略授权(Policy-Based Authorization)。
首先,定义一个权限检查器,例如叫ProjectAuthHelper。它提供一个方法,检查当前用户是否对某个项目有指定权限(如查看、上传、管理)。
public class ProjectAuthorizationService { private readonly ApplicationDbContext _context; public ProjectAuthorizationService(ApplicationDbContext context) { _context = context; } public async Task<bool> CanUserAccessProjectAsync(int userId, int projectId, ProjectRole requiredRole) { // 1. 用户是否是项目所有者? var project = await _context.Projects.FindAsync(projectId); if (project.OwnerId == userId) return true; // 2. 用户是否是项目成员,且角色满足要求? var membership = await _context.ProjectMembers .FirstOrDefaultAsync(pm => pm.UserId == userId && pm.ProjectId == projectId); if (membership == null) return false; // 这里假设ProjectRole是一个枚举,如Owner, Contributor, Reader return membership.Role >= requiredRole; } }然后,在控制器或页面模型中,在每一个需要项目ID的操作方法里,首先调用这个服务进行校验。也可以在ASP.NET Core的过滤器(Filter)中全局实现,但要注意从路由或查询参数中正确提取projectId。
实操心得:权限检查一定要放在业务逻辑的最前端,最好在API入口处就进行拦截。不要相信前端传递的任何权限相关的状态,所有权限判定必须依赖后端的当前用户身份和数据库查询结果。对于Blazor Server,由于是状态化的,可以在组件初始化时(
OnInitializedAsync)进行权限检查,如果失败则导航到无权限页面。
3.2 文件上传与断点续传实现
文件上传是高频且易出问题的操作。对于大文件,必须支持分片上传和断点续传。前端可以使用优秀的库如Resumable.js或Uppy,但用Blazor实现也有其套路。
后端需要提供两个API:
- 初始化上传:客户端先请求一个上传任务,后端生成一个唯一
UploadId,并可能根据文件哈希判断是否已存在(秒传)。 - 上传分片:客户端将文件切成多个分片(如每片5MB),按顺序或并行上传。每个请求携带
UploadId、分片索引、分片数据。后端将分片临时保存在磁盘或缓存中。 - 完成上传:所有分片上传完毕后,客户端发送完成请求。后端将所有分片按顺序合并成完整文件,计算最终哈希,存入
FileEntries表,并移动到正式存储位置。
[HttpPost("init")] public async Task<IActionResult> InitUpload([FromBody] InitUploadRequest request) { // 检查用户对项目的权限 if (!await _authService.CanUserAccessProjectAsync(UserId, request.ProjectId, ProjectRole.Contributor)) return Forbid(); // 计算文件哈希(前端可先计算好传过来,减少后端压力) var uploadId = Guid.NewGuid().ToString(); // 将uploadId与项目、文件信息关联,存入缓存或临时表 _cache.Set($"upload:{uploadId}", new UploadSession {...}, TimeSpan.FromHours(2)); return Ok(new { UploadId = uploadId }); } [HttpPost("chunk")] [RequestSizeLimit(10_485_760)] // 限制单个请求10MB public async Task<IActionResult> UploadChunk(string uploadId, int chunkIndex, IFormFile chunk) { var session = _cache.Get<UploadSession>($"upload:{uploadId}"); if (session == null) return BadRequest("上传会话已过期"); var tempChunkPath = Path.Combine(Path.GetTempPath(), "uploads", uploadId, $"{chunkIndex}.part"); Directory.CreateDirectory(Path.GetDirectoryName(tempChunkPath)); using (var stream = new FileStream(tempChunkPath, FileMode.Create)) { await chunk.CopyToAsync(stream); } // 记录该分片已上传完成 session.UploadedChunks.Add(chunkIndex); _cache.Set($"upload:{uploadId}", session); return Ok(); }注意事项:临时分片文件一定要定期清理,可以写一个后台托管服务(
IHostedService)来清理过期(如超过24小时)的上传会话和临时文件。同时,合并大文件时要注意内存和IO,使用流式操作(FileStream)避免一次性加载到内存。
3.3 C#项目文件解析与树形结构展示
这是平台区别于普通网盘的核心。当用户上传了一个.sln或.csproj文件后,后端应该尝试解析它,并自动创建出对应的虚拟文件夹结构。
对于.csproj文件(新SDK风格和旧格式),可以使用Microsoft.Build库来解析,但这有点重。对于轻量级解析,可以直接用正则表达式或XML解析器读取<Compile Include="...">等节点。更简单实用的方法是:不直接解析csproj,而是通过分析文件路径来构建树。
思路如下:用户上传一个项目的根文件夹(包含.csproj)。平台接收到所有文件后,根据它们在压缩包或上传队列中的相对路径,自动构建层级。例如,文件路径为MyProject/Controllers/HomeController.cs,我们就知道HomeController.cs在Controllers文件夹下,而Controllers在MyProject下。
在Blazor前端展示时,可以使用递归组件来渲染这个树。
// 后端构建树节点的简化模型 public class FileTreeNode { public int Id { get; set; } public string Name { get; set; } public bool IsDirectory { get; set; } public List<FileTreeNode> Children { get; set; } = new(); } // 前端Blazor递归组件 TreeView.razor @typeparam TItem @inherits ComponentBase <ul> @foreach (var item in Items) { <li> @if (item.IsDirectory) { <span @onclick="() => Toggle(item)">[@(item.IsExpanded ? "-" : "+")] @item.Name</span> @if (item.IsExpanded) { <TreeView TItem="TItem" Items="item.Children" /> } } else { <span>📄 @item.Name</span> } </li> } </ul> @code { [Parameter] public List<TItem> Items { get; set; } // ... 需要为TItem定义IsDirectory, Children, IsExpanded等属性的约束,可通过接口实现 }对于代码预览,前端可以使用像Monaco Editor(VS Code使用的编辑器)的Blazor封装组件,或者轻量级的Highlight.js来实现语法高亮。只需要将代码文件的内容读取后,传递给这些组件即可。
4. 数据库设计与关键业务逻辑
4.1 核心表结构设计
以下是几个核心表的简化版设计,使用Code First的方式描述:
public class Project { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public string OwnerId { get; set; } // 关联AspNetUsers.Id public ApplicationUser Owner { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public bool IsPublic { get; set; } = false; // 是否公开项目 public ICollection<ProjectMember> Members { get; set; } public ICollection<FileEntry> Files { get; set; } } public class ProjectMember { public int ProjectId { get; set; } public Project Project { get; set; } public string UserId { get; set; } public ApplicationUser User { get; set; } public ProjectRole Role { get; set; } = ProjectRole.Reader; // 枚举:Owner, Contributor, Reader public DateTime JoinedAt { get; set; } = DateTime.UtcNow; } public class FileEntry { public int Id { get; set; } public string Name { get; set; } public string LogicalPath { get; set; } // 如 "/src/Program.cs" public long SizeInBytes { get; set; } public bool IsDirectory { get; set; } public string? FileHash { get; set; } // 用于去重和秒传 public string StorageLocation { get; set; } // 对象存储的Key或本地相对路径 public DateTime UploadedAt { get; set; } = DateTime.UtcNow; public string UploadedById { get; set; } public ApplicationUser UploadedBy { get; set; } public int ProjectId { get; set; } public Project Project { get; set; } public int? ParentId { get; set; } // 构建目录树 public FileEntry Parent { get; set; } public ICollection<FileEntry> Children { get; set; } } public enum ProjectRole { Reader = 10, // 仅查看 Contributor = 20, // 查看、上传、修改 Owner = 99 // 所有权限,包括管理成员、删除项目 }4.2 文件去重与版本快照逻辑
为了节省存储空间,可以在文件上传时计算其哈希值(如SHA-256)。在FileEntries表中,FileHash和SizeInBytes可以组成唯一性判断。当新上传的文件哈希已存在时,可以采取“引用计数”或“硬链接”的方式,而不是重复存储物理文件。这就是“秒传”的基础。
版本快照功能可以单独创建一张ProjectSnapshots表,记录快照名称、描述、创建时间和对应的项目ID。快照与文件的关系,可以通过在FileEntries表中增加一个SnapshotId字段来实现。当创建快照时,并不是复制所有物理文件,而是复制当前项目下所有FileEntry的记录,并将这些新记录的SnapshotId设置为新快照的ID,同时ParentId等关系也一并复制。物理文件本身由于有哈希去重,并不会重复存储。这样,一个快照就是一组特定时间点的文件元数据集合。
public async Task<int> CreateSnapshotAsync(int projectId, string snapshotName, string description) { // 1. 创建快照记录 var snapshot = new ProjectSnapshot { ProjectId = projectId, Name = snapshotName, ... }; _context.Snapshots.Add(snapshot); await _context.SaveChangesAsync(); // 2. 获取项目当前所有文件条目 var currentFiles = await _context.FileEntries .Where(f => f.ProjectId == projectId && f.SnapshotId == null) // 当前活跃文件 .ToListAsync(); // 3. 为每个文件条目创建副本,关联到新快照 var snapshotFiles = new List<FileEntry>(); foreach (var file in currentFiles) { var snapshotFile = new FileEntry { Name = file.Name, LogicalPath = file.LogicalPath, // ... 复制其他属性 SnapshotId = snapshot.Id, // 关键:关联到快照 ParentId = null // 需要重新构建树,这里简化处理,实际需处理父子关系 }; snapshotFiles.Add(snapshotFile); } _context.FileEntries.AddRange(snapshotFiles); await _context.SaveChangesAsync(); return snapshot.Id; }5. 前端Blazor界面与交互实现
5.1 项目主页与文件管理器
使用Blazor Server,主页面可以是一个Dashboard,展示用户参与的项目列表。点击进入项目后,核心是一个文件管理器组件。这个组件需要实现:
- 面包屑导航:显示当前目录路径。
- 文件列表:以图标/列表形式展示文件和文件夹,支持按名称、时间、大小排序。
- 操作按钮:上传(支持拖拽)、新建文件夹、下载、删除、重命名。
- 上下文菜单:右键点击文件或文件夹时,弹出相关操作菜单。
文件上传可以使用InputFile组件,但要封装成分片上传的逻辑。这里可以创建一个ChunkedFileUpload.razor组件,内部使用HttpClient调用我们之前写的分片上传API,并实时更新进度条。
// 在Blazor组件中处理文件选择和上传 private async Task HandleFileSelected(InputFileChangeEventArgs e) { var file = e.File; var maxAllowedSize = 1024 * 1024 * 1024; // 1GB if (file.Size > maxAllowedSize) { /* 提示文件过大 */ return; } // 1. 初始化上传 var initResponse = await Http.PostAsJsonAsync("/api/upload/init", new { ProjectId = _projectId, FileName = file.Name, FileSize = file.Size }); var uploadSession = await initResponse.Content.ReadFromJsonAsync<UploadSession>(); // 2. 计算分片并上传 const long chunkSize = 5 * 1024 * 1024; // 5MB var totalChunks = (int)Math.Ceiling((double)file.Size / chunkSize); using var stream = file.OpenReadStream(maxAllowedSize); var buffer = new byte[chunkSize]; for (int i = 0; i < totalChunks; i++) { var bytesRead = await stream.ReadAsync(buffer); var chunkData = buffer[0..bytesRead]; // 使用MultipartFormDataContent发送分片 var content = new MultipartFormDataContent(); content.Add(new StringContent(i.ToString()), "chunkIndex"); content.Add(new ByteArrayContent(chunkData), "chunk", "chunk"); var response = await Http.PostAsync($"/api/upload/chunk?uploadId={uploadSession.UploadId}", content); if (!response.IsSuccessStatusCode) { /* 处理错误 */ } // 更新进度条 UploadProgress = (i + 1) * 100 / totalChunks; StateHasChanged(); } // 3. 完成上传 await Http.PostAsJsonAsync("/api/upload/complete", new { UploadId = uploadSession.UploadId }); }5.2 代码预览与文档关联界面
当用户点击一个.cs文件时,可以打开一个模态框(Modal)或新面板,使用代码编辑器组件展示内容。对于开题报告、需求文档等.docx或.pdf文件,可以在线预览。.pdf可以使用浏览器原生支持或PDF.js库;.docx文件预览比较麻烦,通常需要后端转换或使用前端库(如mammoth.js将.docx转成HTML)。
文档关联功能可以设计为一个侧边栏或标签页。在项目页面,有一个“设计文档”区域,这里列出所有被标记为设计文档的文件(可以通过文件类型或手动标记)。每个设计文档旁边可以有一个“关联版本”的下拉框,下拉框中列出该项目所有的Git提交(如果集成了Git)或手动创建的快照。选择后,即建立关联。
6. 部署、运维与性能优化考量
6.1 部署环境搭建
对于一个小型团队或个人使用,部署可以很简单:
- 服务器:一台拥有公网IP的云服务器(如2核4G配置)。
- 运行环境:安装.NET Core Runtime、IIS(Windows)或Nginx(Linux)。
- 数据库:安装SQL Server Express或PostgreSQL。
- 文件存储:初期可以直接使用服务器本地磁盘的一个大容量分区。规划好目录,例如
/appdata/uploads/{userId}/{projectId}/。务必在appsettings.json中配置这个根路径,不要写死在代码里。 - 发布:使用
dotnet publish -c Release发布Blazor Server应用,然后将产出物拷贝到服务器,配置好IIS站点或Nginx反向代理即可。
如果使用对象存储,则需要额外配置存储服务的Access Key和Secret,并在代码中集成对应的SDK(如阿里云OSS SDK)。
6.2 性能与安全优化点
- 静态文件服务:用户上传的图片、文档等,如果直接通过ASP.NET Core应用来提供下载,会占用应用线程。更好的做法是,对于存储在本地磁盘的文件,使用Nginx的
location和alias指令直接映射,绕过应用层。对于对象存储,则直接返回预签名的URL给前端,让浏览器直连对象存储下载。 - 数据库连接池:确保在连接字符串中配置了合理的
Max Pool Size和Connection Timeout。 - 缓存策略:项目列表、用户信息等不常变的数据,可以使用
IMemoryCache或IDistributedCache(如Redis)进行缓存。特别注意缓存失效策略。 - 安全:
- 文件上传安全:检查文件扩展名和MIME类型,防止上传可执行文件。将上传的文件存储在应用进程无权直接执行的位置。对图片进行二次处理(如缩放)以消除潜在恶意代码。
- 路径遍历攻击:在处理文件路径时,务必使用
Path.GetFileName等方法来获取纯净的文件名,并拼接上自己控制的安全基础路径,防止用户通过../../../这样的路径访问系统文件。 - HTTPS:务必启用HTTPS,保护用户凭证和传输中的数据。
6.3 监控与日志
集成像Serilog这样的日志库,将日志结构化地输出到文件或像Seq这样的日志服务器。记录关键操作,如用户登录、文件上传/下载、项目创建/删除。同时,监控服务器的磁盘空间、CPU和内存使用情况。可以编写一个简单的后台服务,定期检查上传临时目录和存储目录的大小,超过阈值时发送告警。
7. 常见问题排查与调试技巧
在实际开发和部署中,你肯定会遇到各种问题。这里记录几个我踩过的坑和解决方法。
问题1:Blazor Server应用在文件上传时,连接中断或进度不更新。
- 排查:Blazor Server使用SignalR长连接,默认有大小和超时限制。大文件上传可能超过这些限制。
- 解决:在
Startup.cs或Program.cs中配置SignalR和服务器选项。builder.Services.AddServerSideBlazor().AddHubOptions(options => { options.MaximumReceiveMessageSize = 10 * 1024 * 1024; // 10MB, 根据分片大小调整 options.HandshakeTimeout = TimeSpan.FromSeconds(30); }); builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.Limits.MaxRequestBodySize = 1024 * 1024 * 1024; // 1GB, 限制整个请求体 });
问题2:用户无法下载文件,返回404或403。
- 排查步骤:
- 检查数据库
FileEntries表中,该文件的StorageLocation字段值是否正确。 - 检查该物理文件是否真实存在于磁盘或对象存储中。
- 检查下载API的权限验证逻辑是否正确。是否只允许项目成员下载?
- 如果使用Nginx代理,检查静态文件配置的
alias路径是否正确,以及Nginx进程是否有权限读取该文件。
- 检查数据库
- 技巧:在下载API中,加入详细的日志,记录用户ID、文件ID、请求路径和最终的物理路径,方便追踪。
问题3:数据库查询缓慢,特别是加载项目文件树时。
- 排查:使用Entity Framework Core的日志输出SQL语句,或在SQL Server Management Studio中查看执行计划。
- 解决:
- 索引:确保
FileEntries表的ProjectId、ParentId、SnapshotId字段上有索引。 - 贪婪加载:在查询文件树时,使用
.Include(f => f.Children)来一次性加载子项,避免N+1查询问题。 - 异步:所有数据库操作务必使用异步方法(
ToListAsync,FirstOrDefaultAsync等)。 - 分页:对于文件数量巨大的项目,文件列表应支持分页查询,不要一次性加载所有。
- 索引:确保
问题4:在Linux上部署后,文件上传功能正常,但生成的缩略图或处理后的文件权限错误,导致后续无法访问。
- 原因:ASP.NET Core进程(如运行在
www-data用户下)创建的文件,其默认权限可能不允许其他进程(如Nginx)或后续的ASP.NET Core进程(如果重启后用户ID变化)读取。 - 解决:在保存文件后,显式地设置文件权限。可以使用
System.IO的File.SetUnixFileMode方法(.NET Core 3.0+),或者在Linux上使用chmod命令通过进程调用。using var fs = new FileStream(fullPath, FileMode.Create); await file.CopyToAsync(fs); // 设置权限为644 (rw-r--r--) if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { File.SetUnixFileMode(fullPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead); }
开发这样一个平台,最难的不是某个具体功能,而是如何平衡功能的完备性与实现的复杂度。我的建议是,先跑通核心流程:用户能创建项目、上传一个包含.csproj的文件夹、并能浏览文件树和下载。这个最小可行产品(MVP)出来后,再根据实际反馈,逐步迭代增加版本快照、代码预览、文档关联等高级功能。在技术选型上,Blazor Server确实能让熟悉C#的后端开发者快速构建出交互良好的界面,但也要注意其状态保持和可扩展性的特点。对于文件存储,如果项目初期规模不大,用本地磁盘是最快最省事的,等用户量和文件量上来后,再平滑迁移到对象存储也不迟。
本文还有配套的精品资源,点击获取