1. 项目概述:基于ASP.NET的原创音乐网站开发全记录
去年帮音乐学院的学弟完成毕业设计时,我们决定开发一个支持原创音乐人上传作品的平台。这个项目采用ASP.NET作为后端框架,配合SQL Server数据库,实现了从音乐上传、审核到在线播放的完整流程。在开发过程中,我们踩过音频流处理的坑,也遇到过并发播放的性能瓶颈,最终通过分块传输和CDN加速解决了这些问题。本文将详细分享这个项目的技术选型、架构设计和具体实现方案。
2. 技术选型与架构设计
2.1 为什么选择ASP.NET
ASP.NET Core是我们最终选择的后端框架,主要基于以下几个考虑:
- 成熟的MVC模式便于团队协作开发
- Entity Framework Core简化数据库操作
- 内置的依赖注入和中间件机制
- 对RESTful API的良好支持
实际开发中,我们特别依赖EF Core的Code First迁移功能。当需要新增音乐分类字段时,只需修改模型类然后执行:
bash复制dotnet ef migrations add AddMusicCategory
dotnet ef database update
2.2 数据库设计关键点
音乐网站的核心表包括:
- Users(用户表):存储用户基本信息及权限角色
- Musics(音乐表):记录音乐元数据和存储路径
- Comments(评论表):实现用户互动功能
其中音乐表的字段设计值得注意:
sql复制CREATE TABLE Musics (
Id INT PRIMARY KEY IDENTITY,
Title NVARCHAR(100) NOT NULL,
ArtistId INT FOREIGN KEY REFERENCES Users(Id),
StoragePath NVARCHAR(255) NOT NULL,
Duration INT DEFAULT 0,
FileSize INT DEFAULT 0,
UploadTime DATETIME DEFAULT GETDATE(),
Status TINYINT DEFAULT 0 -- 0待审核 1已发布 2已下架
);
提示:音频文件实际存储在对象存储服务中,数据库只保存访问路径。我们最初直接存文件到服务器,结果两周就耗尽了磁盘空间。
3. 核心功能实现细节
3.1 音乐上传与处理流程
上传功能看似简单,但实际开发中需要考虑多个环节:
- 前端通过FormData提交文件
- 后端验证文件类型和大小(限制为50MB以内的MP3/WAV)
- 使用TagLibSharp库提取音频元数据
- 生成缩略图(我们采用专辑封面或默认图片)
- 转码为统一格式(使用FFmpeg转码为192kbps MP3)
关键的上传接口实现:
csharp复制[HttpPost]
[Authorize]
public async Task<IActionResult> Upload(IFormFile musicFile)
{
// 验证用户权限
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// 验证文件
if (musicFile.Length > 50_000_000)
return BadRequest("文件大小超过50MB限制");
// 处理元数据
using var tempStream = new MemoryStream();
await musicFile.CopyToAsync(tempStream);
var tags = TagLib.File.Create(new StreamFileAbstraction(
musicFile.FileName, tempStream, null));
// 存储到对象存储
var objectKey = $"musics/{Guid.NewGuid()}.mp3";
await _storageService.UploadAsync(objectKey, tempStream);
// 保存到数据库
var music = new Music {
Title = tags.Tag.Title ?? Path.GetFileNameWithoutExtension(musicFile.FileName),
ArtistId = userId,
StoragePath = objectKey,
Duration = (int)tags.Properties.Duration.TotalSeconds,
FileSize = (int)musicFile.Length
};
_context.Musics.Add(music);
await _context.SaveChangesAsync();
return Ok(new { music.Id });
}
3.2 音频流播放实现
音频播放是音乐网站的核心体验,我们实现了以下功能:
- 渐进式下载播放
- 断点续传
- 播放统计
前端使用HTML5 Audio元素配合自定义UI:
javascript复制const audio = new Audio();
audio.src = `/api/musics/${musicId}/stream`;
// 监听时间更新事件
audio.addEventListener('timeupdate', () => {
progressBar.value = (audio.currentTime / audio.duration) * 100;
});
// 记录播放进度
setInterval(() => {
if (!audio.paused) {
fetch(`/api/play-records`, {
method: 'POST',
body: JSON.stringify({
musicId,
currentTime: audio.currentTime
})
});
}
}, 10000);
后端流媒体处理关键代码:
csharp复制[HttpGet("{id}/stream")]
public async Task<IActionResult> Stream(int id)
{
var music = await _context.Musics.FindAsync(id);
if (music == null) return NotFound();
var stream = await _storageService.DownloadAsync(music.StoragePath);
// 支持范围请求(Range Requests)
var rangeHeader = Request.Headers["Range"].ToString();
if (!string.IsNullOrEmpty(rangeHeader))
{
// 处理部分内容请求(206状态码)
return PartialContentStream(stream, rangeHeader, music.FileSize);
}
// 记录播放次数
_ = RecordPlayAsync(music.Id);
return File(stream, "audio/mpeg");
}
4. 性能优化实战经验
4.1 数据库查询优化
音乐列表页最初加载需要3秒以上,通过以下优化降到300ms内:
- 添加合适的索引:
sql复制CREATE INDEX IX_Musics_ArtistId ON Musics(ArtistId);
CREATE INDEX IX_Musics_UploadTime ON Musics(UploadTime DESC);
- 使用EF Core的AsNoTracking()减少开销:
csharp复制var musics = await _context.Musics
.AsNoTracking()
.Include(m => m.Artist)
.OrderByDescending(m => m.UploadTime)
.Take(20)
.ToListAsync();
- 实现分页查询:
csharp复制[HttpGet]
public async Task<IActionResult> List(int page = 1, int pageSize = 20)
{
var query = _context.Musics.AsNoTracking();
var total = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(new { total, items });
}
4.2 缓存策略实施
我们采用多级缓存方案:
- 内存缓存:高频访问的音乐元数据
- Redis缓存:热门音乐列表和排行榜
- CDN缓存:静态资源和音频文件
配置内存缓存示例:
csharp复制services.AddMemoryCache(options => {
options.SizeLimit = 1024; // 1GB内存限制
});
// 使用缓存
public async Task<Music> GetMusic(int id)
{
if (!_cache.TryGetValue(id, out Music music))
{
music = await _context.Musics.FindAsync(id);
_cache.Set(id, music, new MemoryCacheEntryOptions {
Size = 1,
SlidingExpiration = TimeSpan.FromMinutes(30)
});
}
return music;
}
5. 安全防护措施
5.1 文件上传安全
我们遇到过恶意文件上传攻击,最终实施方案:
- 文件类型白名单验证(.mp3, .wav)
- 病毒扫描(集成ClamAV)
- 内容校验(通过FFmpeg验证确实是音频文件)
csharp复制private bool IsValidAudioFile(IFormFile file)
{
// 扩展名检查
var ext = Path.GetExtension(file.FileName).ToLower();
if (!new[] { ".mp3", ".wav" }.Contains(ext))
return false;
// 魔数检查(文件头校验)
using var stream = file.OpenReadStream();
byte[] header = new byte[4];
stream.Read(header, 0, 4);
// MP3的ID3头或WAV的RIFF头
return header.SequenceEqual(new byte[] { 0x49, 0x44, 0x33, 0x03 }) || // ID3
header.SequenceEqual(new byte[] { 0x52, 0x49, 0x46, 0x46 }); // RIFF
}
5.2 API防护策略
- 速率限制(AspNetCoreRateLimit包):
csharp复制services.Configure<IpRateLimitOptions>(options => {
options.GeneralRules = new List<RateLimitRule> {
new RateLimitRule {
Endpoint = "*",
Limit = 100,
Period = "1m"
}
};
});
- JWT身份验证配置:
csharp复制services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
6. 部署与监控方案
6.1 容器化部署
使用Docker简化部署流程,Dockerfile示例:
dockerfile复制FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["MusicWeb/MusicWeb.csproj", "MusicWeb/"]
RUN dotnet restore "MusicWeb/MusicWeb.csproj"
COPY . .
RUN dotnet build "MusicWeb/MusicWeb.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MusicWeb/MusicWeb.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MusicWeb.dll"]
6.2 健康检查与监控
配置应用健康检查端点:
csharp复制app.UseHealthChecks("/health", new HealthCheckOptions {
ResponseWriter = async (context, report) => {
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(new {
status = report.Status.ToString(),
checks = report.Entries.Select(e => new {
name = e.Key,
status = e.Value.Status.ToString(),
duration = e.Value.Duration.TotalMilliseconds
})
}));
}
});
services.AddHealthChecks()
.AddSqlServer(Configuration.GetConnectionString("Default"))
.AddRedis(Configuration.GetConnectionString("Redis"))
.AddAzureBlobStorage(Configuration.GetConnectionString("Storage"));
7. 开发中的经验教训
- 音频处理陷阱:
- 不同浏览器对音频格式的支持差异很大,必须统一转码
- iOS设备对自动播放有严格限制,需要用户交互后才能播放
- 波形图生成消耗CPU资源,建议在后端预处理
- 数据库连接管理:
csharp复制// 错误做法 - 未及时释放连接
public List<Music> GetMusics() {
using var context = new AppDbContext();
return context.Musics.ToList();
}
// 正确做法 - 依赖注入
public class MusicService {
private readonly AppDbContext _context;
public MusicService(AppDbContext context) {
_context = context;
}
public List<Music> GetMusics() {
return _context.Musics.ToList();
}
}
- 前端性能优化:
- 使用虚拟滚动处理长列表(1万+音乐)
- Web Worker处理音频分析
- Service Worker缓存API响应
这个项目从零开始到最终上线历时4个月,最大的收获是理解了音视频处理的复杂性。特别是当用户量达到1000+时,我们不得不重构整个存储架构,从单服务器迁移到对象存储+CDN的方案。建议开发类似项目的同学从一开始就考虑分布式架构,避免后期重构的痛苦。