2024 桐庐半程马拉松
00:00:00
时间
0.00
距离(公里)
--:--
配速
--
步频
--
心率 (bpm)
--
配速
步频
|
share-image
ESC

构建高可扩展音乐流媒体后端:Go+Gin+OSS架构实战

本文详细记录了一个完整的音乐播放器后端服务的构建过程,涵盖Gin框架搭建、JWT认证、阿里云OSS对象存储、音频格式转换、定时任务调度、数据库设计等核心技术的完整实现。

目录

  1. 项目概述与架构设计
  2. 技术栈选型与项目结构
  3. 配置管理与环境隔离
  4. 数据库设计与模型关系
  5. JWT认证与权限控制
  6. 阿里云OSS集成与文件存储
  7. 音频格式转换服务
  8. 定时任务与数据去重
  9. 部署与运维实践
  10. 总结与展望

1. 项目概述与架构设计

1.1 项目背景

Melody 是一个本地音乐播放器的后端服务,需要支持以下核心功能:

  • 用户系统:注册、登录、JWT认证、个人信息管理
  • 歌曲管理:CRUD操作、元数据提取、音频格式转换
  • 专辑与艺术家:完整的音乐元数据管理
  • 歌单系统:用户自定义歌单、收藏功能
  • 播放统计:最近播放、播放次数统计
  • 文件存储:阿里云OSS对象存储、签名URL下载

1.2 整体架构

┌─────────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ Flutter App │ Web Admin │
└──────────────────────────────┼──────────────────────────────────┘
│ HTTPS
┌──────────────────────────────▼──────────────────────────────────┐
│ API Gateway │
│ Nginx (负载均衡、SSL终止、静态资源) │
└──────────────────────────────┬──────────────────────────────────┘

┌──────────────────────────────▼──────────────────────────────────┐
│ Go Gin Server (8080) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ Auth API │ │ Song API │ │ Playlist │ │ Upload │ │
│ │ (JWT认证) │ │ (歌曲管理) │ │ (歌单管理) │ │ (文件上传) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────┬─────┘ │
│ │ │ │ │ │
│ ┌──────▼───────────────▼───────────────▼──────────────▼─────┐ │
│ │ Middleware Layer (中间件层) │ │
│ │ JWTAuth │ RateLimit │ CORS │ Security │ Logging │ Recover │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ MySQL 8 │ │ 阿里云OSS │ │ ffmpeg │
│ (音乐元数据) │ │ (音频文件) │ │ (格式转换) │
└─────────────┘ └─────────────┘ └─────────────┘

1.3 核心特性

特性 技术实现 说明
认证授权 JWT + bcrypt 无状态认证,密码加密存储
文件存储 阿里云OSS 支持服务端代理和客户端直传两种模式
音频处理 ffmpeg 支持MP3/FLAC/WAV/AAC等格式互转
定时任务 robfig/cron 每天自动去重音乐文件
配置管理 Viper 支持YAML配置文件和环境变量

2. 技术栈选型与项目结构

2.1 核心技术栈

// go.mod
module melody-backend

go 1.21

require (
github.com/gin-gonic/gin v1.9.1 // Web框架
github.com/golang-jwt/jwt/v5 v5.2.0 // JWT认证
gorm.io/gorm v1.25.7 // ORM
gorm.io/driver/mysql v1.5.4 // MySQL驱动
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // 阿里云OSS
github.com/spf13/viper v1.18.2 // 配置管理
github.com/robfig/cron/v3 v3.0.1 // 定时任务
golang.org/x/crypto v0.19.0 // 密码加密
)

2.2 项目结构

melody_backend/
├── cmd/
│ └── main.go # 程序入口
├── internal/
│ ├── config/ # 配置管理
│ │ ├── config.go # 配置结构体与加载
│ │ └── database.go # 数据库连接
│ ├── handlers/ # HTTP处理器
│ │ ├── auth.go # 认证相关
│ │ ├── song.go # 歌曲管理
│ │ ├── album.go # 专辑管理
│ │ ├── artist.go # 艺术家管理
│ │ ├── playlist.go # 歌单管理
│ │ ├── favorite.go # 收藏功能
│ │ ├── recent.go # 最近播放
│ │ └── upload.go # 文件上传
│ ├── middleware/ # 中间件
│ │ ├── auth.go # JWT认证
│ │ ├── cors.go # 跨域处理
│ │ └── error_handler.go # 错误处理
│ ├── models/ # 数据模型
│ │ └── models.go # 所有模型定义
│ ├── routes/ # 路由配置
│ │ └── routes.go # 路由注册
│ └── services/ # 业务服务
│ ├── audio_converter.go # 音频格式转换
│ ├── deduplicate.go # 去重服务
│ └── scheduler.go # 定时任务调度
├── pkg/
│ └── oss/ # OSS客户端封装
│ └── client.go
├── scripts/
│ └── init-db.go # 数据库初始化
├── config.yaml # 配置文件
├── go.mod
└── README.md

3. 配置管理与环境隔离

3.1 配置结构设计

使用 Viper 实现灵活的配置管理,支持YAML文件和环境变量双重配置:

type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
JWT JWTConfig `mapstructure:"jwt"`
OSS OSSConfig `mapstructure:"oss"`
Upload UploadConfig `mapstructure:"upload"`
}

type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Name string `mapstructure:"name"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
}

type OSSConfig struct {
Region string `mapstructure:"region"`
AccessKeyID string `mapstructure:"access_key_id"`
AccessKeySecret string `mapstructure:"access_key_secret"`
Bucket string `mapstructure:"bucket"`
Endpoint string `mapstructure:"endpoint"`
DownloadExpire int64 `mapstructure:"download_expire"`
}

3.2 配置加载逻辑

func Load() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")

// 设置默认值
viper.SetDefault("server.port", 8080)
viper.SetDefault("database.max_idle_conns", 10)
viper.SetDefault("database.max_open_conns", 100)
viper.SetDefault("jwt.expires_in", "168h")

// 支持环境变量(MELODY_前缀)
viper.SetEnvPrefix("MELODY")
viper.AutomaticEnv()

// 读取配置文件
if err := viper.ReadInConfig(); err != nil {
return nil, err
}

// 从环境变量覆盖(systemd风格)
loadFromEnv(&config)

return &config, nil
}

3.3 生产环境配置建议

# config.yaml (生产环境)
server:
port: 8080
mode: release # gin.ReleaseMode

database:
host: ${DB_HOST}
port: 3306
name: melody_db
user: ${DB_USER}
password: ${DB_PASSWORD}
max_idle_conns: 10
max_open_conns: 100

jwt:
secret: ${JWT_SECRET}
expires_in: 168h # 7天

oss:
region: oss-cn-hangzhou
access_key_id: ${OSS_ACCESS_KEY_ID}
access_key_secret: ${OSS_ACCESS_KEY_SECRET}
bucket: melody-music
download_expire: 3600 # 签名URL有效期1小时

4. 数据库设计与模型关系

4.1 ER图关系

┌─────────────┐       ┌─────────────┐       ┌─────────────┐
│ users │ │ artists │ │ albums │
├─────────────┤ ├─────────────┤ ├─────────────┤
id │ │ id │ │ id
│ username │ │ name │ │ title │
│ email │ │ bio │◄──────┤ artist_id │
│ password │ │ avatar_url │ │ cover_url │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ ┌──────┴──────┐ │
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ playlists │ │ songs │ │ favorites │
├─────────────┤ ├─────────────┤ ├─────────────┤
id │ │ id │ │ id
│ user_id │──┤ title │ │ user_id │
name │ │ artist_id │──┤ song_id │
│ description │ │ album_id │──┘ │
└─────────────┘ │ file_url │ │
│ duration │ │
└─────────────┘ │
▲ │
│ │
┌──────┴──────┐ │
│playlist_songs│ │
├─────────────┤ │
│playlist_id │ │
│song_id │──────────────┘
└─────────────┘

4.2 核心模型定义

// User 用户模型
type User struct {
ID uint `gorm:"primarykey" json:"id"`
Username string `gorm:"uniqueIndex;size:50" json:"username"`
Email string `gorm:"uniqueIndex;size:100" json:"email"`
Password string `gorm:"size:255" json:"-"` // 不序列化
Avatar string `gorm:"size:255" json:"avatar"`
CreatedAt time.Time `json:"created_at"`
}

// Song 歌曲模型
type Song struct {
ID uint `gorm:"primarykey" json:"id"`
Title string `gorm:"size:200;index" json:"title"`
ArtistID *uint `json:"artist_id"`
Artist *Artist `json:"artist,omitempty"`
AlbumID *uint `json:"album_id"`
Album *Album `json:"album,omitempty"`
FileURL string `gorm:"size:500" json:"file_url"`
Duration float64 `json:"duration"`
Lyrics string `gorm:"type:text" json:"lyrics"`
PlayCount int `gorm:"default:0" json:"play_count"`
CreatedAt time.Time `json:"created_at"`
}

// Playlist 歌单模型
type Playlist struct {
ID uint `gorm:"primarykey" json:"id"`
UserID uint `json:"user_id"`
Name string `gorm:"size:100" json:"name"`
Description string `gorm:"type:text" json:"description"`
IsPublic bool `gorm:"default:false" json:"is_public"`
Songs []Song `gorm:"many2many:playlist_songs;" json:"songs,omitempty"`
CreatedAt time.Time `json:"created_at"`
}

4.3 数据库连接池优化

func InitDB(cfg *config.DatabaseConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Name)

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent), // 生产环境关闭SQL日志
})
if err != nil {
return nil, err
}

// 配置连接池
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Hour)

return db, nil
}

5. JWT认证与权限控制

5.1 JWT Token生成与验证

package middleware

import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"time"
)

type Claims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}

// GenerateToken 生成JWT Token
func GenerateToken(userID uint, email string, secret string, expiresIn time.Duration) (string, error) {
claims := Claims{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}

// AuthMiddleware JWT认证中间件
func AuthMiddleware(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{"error": "missing authorization header"})
c.Abort()
return
}

// Bearer token 格式
tokenString := strings.TrimPrefix(authHeader, "Bearer ")

token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})

if err != nil || !token.Valid {
c.JSON(401, gin.H{"error": "invalid token"})
c.Abort()
return
}

claims, _ := token.Claims.(*Claims)
c.Set("userID", claims.UserID)
c.Set("email", claims.Email)
c.Next()
}
}

5.2 路由权限分组

func SetupRoutes(r *gin.Engine, db *gorm.DB, cfg *config.Config) {
// 公开路由
public := r.Group("/api/v1")
{
public.POST("/auth/register", handlers.Register)
public.POST("/auth/login", handlers.Login)
public.GET("/songs", handlers.GetSongs) // 公开访问
public.GET("/songs/:id", handlers.GetSong)
}

// 需要认证的路由
authorized := r.Group("/api/v1")
authorized.Use(middleware.AuthMiddleware(cfg.JWT.Secret))
{
authorized.GET("/user/profile", handlers.GetProfile)
authorized.POST("/playlists", handlers.CreatePlaylist)
authorized.POST("/favorites", handlers.AddFavorite)
authorized.POST("/songs/:id/play", handlers.RecordPlay)
}
}

6. 阿里云OSS集成与文件存储

6.1 OSS客户端封装

package oss

import (
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"io"
"time"
)

type Client struct {
bucket *oss.Bucket
config Config
}

type Config struct {
Region string
AccessKeyID string
AccessKeySecret string
Bucket string
Endpoint string
DownloadExpire int64
}

var client *Client

// Init 初始化OSS客户端
func Init(cfg Config) error {
ossClient, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
if err != nil {
return err
}

bucket, err := ossClient.Bucket(cfg.Bucket)
if err != nil {
return err
}

client = &Client{
bucket: bucket,
config: cfg,
}
return nil
}

// GetClient 获取OSS客户端实例
func GetClient() *Client {
if client == nil {
panic("OSS client not initialized")
}
return client
}

// Upload 上传文件到OSS
func (c *Client) Upload(objectKey string, reader io.Reader, contentType string) (string, error) {
options := []oss.Option{
oss.ContentType(contentType),
}

err := c.bucket.PutObject(objectKey, reader, options...)
if err != nil {
return "", err
}

// 返回公共访问URL(如果是私有bucket,需要生成签名URL)
return c.bucket.BucketName + "." + c.config.Endpoint + "/" + objectKey, nil
}

// GetSignedURL 获取带签名的下载URL
func (c *Client) GetSignedURL(objectKey string, expireSeconds int64) (string, error) {
return c.bucket.SignURL(objectKey, oss.HTTPGet, expireSeconds)
}

// GenerateObjectKey 生成唯一的对象存储路径
func (c *Client) GenerateObjectKey(prefix, filename string) string {
ext := filepath.Ext(filename)
uuid := uuid.New().String()
timestamp := time.Now().Unix()
return fmt.Sprintf("%s/%d_%s%s", prefix, timestamp, uuid[:8], ext)
}

6.2 两种上传模式

模式一:服务端代理上传(适合小文件)

func UploadSong(c *gin.Context) {
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "invalid file"})
return
}
defer file.Close()

// 生成对象路径
objectKey := oss.GetClient().GenerateObjectKey("music", header.Filename)

// 上传到OSS
url, err := oss.GetClient().Upload(objectKey, file, header.Header.Get("Content-Type"))
if err != nil {
c.JSON(500, gin.H{"error": "upload failed"})
return
}

// 保存到数据库
song := models.Song{
Title: strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)),
FileURL: url,
}
db.Create(&song)

c.JSON(200, gin.H{"data": song})
}

模式二:客户端直传(推荐,减轻服务器压力)

// 获取带签名的上传URL
func GetUploadURL(c *gin.Context) {
filename := c.Query("filename")
contentType := c.Query("content_type")

objectKey := oss.GetClient().GenerateObjectKey("music", filename)

// 生成签名URL(允许PUT请求)
signedURL, err := oss.GetClient().GetSignedURLForPut(objectKey, contentType, 300)
if err != nil {
c.JSON(500, gin.H{"error": "failed to generate URL"})
return
}

c.JSON(200, gin.H{
"upload_url": signedURL,
"object_key": objectKey,
})
}

// 客户端直传流程:
// 1. 调用 /api/v1/upload/url 获取签名URL
// 2. 客户端直接使用该URL PUT上传文件到OSS
// 3. 上传完成后调用服务端接口保存文件信息

7. 音频格式转换服务

7.1 基于ffmpeg的音频转换

type AudioConverter struct {
tempDir string
}

// ConvertToOSS 转换音频并上传到OSS
func (ac *AudioConverter) ConvertToOSS(inputURL string, outputFormat string, options map[string]string) (*ConvertToOSSResult, error) {
taskID := uuid.New().String()
inputPath := filepath.Join(ac.tempDir, fmt.Sprintf("convert_%s_input", taskID))
outputPath := filepath.Join(ac.tempDir, fmt.Sprintf("convert_%s_output.%s", taskID, outputFormat))

defer os.Remove(inputPath)
defer os.Remove(outputPath)

// 步骤1: 下载输入文件
if err := ac.downloadFile(inputURL, inputPath); err != nil {
return nil, fmt.Errorf("failed to download: %w", err)
}

// 步骤2: 使用ffmpeg转换
if err := ac.convertFile(inputPath, outputPath, outputFormat, options); err != nil {
return nil, fmt.Errorf("conversion failed: %w", err)
}

// 步骤3: 读取并上传
outputData, _ := os.ReadFile(outputPath)
duration, _ := ac.getAudioDuration(outputPath)

objectKey := oss.GetClient().GenerateObjectKey("music/converted",
fmt.Sprintf("%s.%s", baseName, outputFormat))
publicURL, err := oss.GetClient().Upload(objectKey, strings.NewReader(string(outputData)),
ac.getContentType(outputFormat))

return &ConvertToOSSResult{
PublicURL: publicURL,
Format: outputFormat,
Duration: duration,
FileSize: int64(len(outputData)),
}, nil
}

// convertFile 执行ffmpeg转换
func (ac *AudioConverter) convertFile(inputPath, outputPath, outputFormat string, options map[string]string) error {
args := []string{"-i", inputPath, "-y"}

// 根据格式选择编码器
switch strings.ToLower(outputFormat) {
case "mp3":
args = append(args, "-c:a", "libmp3lame", "-q:a", "0")
case "flac":
args = append(args, "-c:a", "flac")
case "wav":
args = append(args, "-c:a", "pcm_s16le")
case "aac":
args = append(args, "-c:a", "aac", "-b:a", "192k")
case "ogg":
args = append(args, "-c:a", "libvorbis", "-q:a", "4")
}

args = append(args, outputPath)

cmd := exec.Command("ffmpeg", args...)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("ffmpeg error: %v, output: %s", err, string(output))
}
return nil
}

7.2 支持的音频格式

格式 编码器 适用场景
MP3 libmp3lame 通用兼容,有损压缩
FLAC flac 无损音质,文件较大
WAV pcm_s16le 原始音频,无压缩
AAC aac 苹果设备首选
OGG libvorbis 开源免版权
Opus libopus 低延迟语音

8. 定时任务与数据去重

8.1 定时任务调度器

package services

import (
"github.com/robfig/cron/v3"
"log"
)

type Scheduler struct {
cron *cron.Cron
db *gorm.DB
}

func NewScheduler(db *gorm.DB) *Scheduler {
return &Scheduler{
cron: cron.New(),
db: db,
}
}

// Start 启动定时任务
func (s *Scheduler) Start() {
// 每天凌晨3点执行去重任务
s.cron.AddFunc("0 3 * * *", s.deduplicateSongs)

s.cron.Start()
log.Println("Scheduler started")
}

// Stop 停止定时任务
func (s *Scheduler) Stop() {
s.cron.Stop()
}

// deduplicateSongs 歌曲去重
func (s *Scheduler) deduplicateSongs() {
log.Println("[Scheduler] Starting song deduplication...")

dedupService := NewDeduplicateService(s.db)
result, err := dedupService.Deduplicate()
if err != nil {
log.Printf("[Scheduler] Deduplication failed: %v", err)
return
}

log.Printf("[Scheduler] Deduplication complete: removed %d duplicates", result.RemovedCount)
}

8.2 去重服务实现

// DeduplicateService 去重服务
type DeduplicateService struct {
db *gorm.DB
}

// DuplicateGroup 重复歌曲组
type DuplicateGroup struct {
Title string
ArtistID uint
AlbumID uint
IDs []uint
Count int
}

// Deduplicate 执行去重,保留最早创建的记录
func (s *DeduplicateService) Deduplicate() (*DeduplicateResult, error) {
// 查找重复歌曲(标题+艺术家+专辑相同)
var groups []struct {
Title string
ArtistID uint
AlbumID uint
Count int
MinID uint
}

s.db.Model(&models.Song{}).
Select("title, artist_id, album_id, COUNT(*) as count, MIN(id) as min_id").
Group("title, artist_id, album_id").
Having("count > 1").
Scan(&groups)

var removedCount int

for _, group := range groups {
// 删除该组中除最早记录外的所有记录
result := s.db.Where("title = ? AND artist_id = ? AND album_id = ? AND id != ?",
group.Title, group.ArtistID, group.AlbumID, group.MinID).
Delete(&models.Song{})

if result.Error != nil {
log.Printf("Failed to delete duplicates for %s: %v", group.Title, result.Error)
continue
}

removedCount += int(result.RowsAffected)
log.Printf("Removed %d duplicates for song: %s", result.RowsAffected, group.Title)
}

return &DeduplicateResult{
RemovedCount: removedCount,
GroupsFound: len(groups),
}, nil
}

9. 部署与运维实践

9.1 systemd服务配置

# /etc/systemd/system/melody-server.service
[Unit]
Description=Melody Music Backend API
After=network.target

[Service]
Type=simple
User=melody
Group=melody
WorkingDirectory=/opt/melody
ExecStart=/opt/melody/melody-api
Restart=always
RestartSec=5

# 环境变量
Environment="SERVER_PORT=8080"
Environment="SERVER_MODE=release"
Environment="DB_HOST=localhost"
Environment="DB_PORT=3306"
Environment="DB_NAME=melody_db"
Environment="DB_USER=melody"
Environment="DB_PASSWORD=your_password"
Environment="JWT_SECRET=your_jwt_secret"
Environment="OSS_REGION=oss-cn-hangzhou"
Environment="OSS_ACCESS_KEY_ID=your_key"
Environment="OSS_ACCESS_KEY_SECRET=your_secret"
Environment="OSS_BUCKET=melody-music"

[Install]
WantedBy=multi-user.target

9.2 Nginx反向代理配置

upstream melody_backend {
server 127.0.0.1:8080;
keepalive 32;
}

server {
listen 443 ssl http2;
server_name music.yourdomain.com;

ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;

# API请求
location /api/ {
proxy_pass http://melody_backend/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# 长连接优化
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}

# 静态资源(如果前端部署在同一服务器)
location / {
root /opt/melody/web;
try_files $uri $uri/ /index.html;
}
}

9.3 部署脚本

#!/bin/bash
# deploy.sh

set -e

APP_NAME="melody-api"
REMOTE_HOST="your-server"
REMOTE_DIR="/opt/melody"

echo "=== Building... ==="
GOOS=linux GOARCH=amd64 go build -o ${APP_NAME} cmd/main.go

echo "=== Deploying to ${REMOTE_HOST}... ==="
scp ${APP_NAME} ${REMOTE_HOST}:${REMOTE_DIR}/
scp config.yaml ${REMOTE_HOST}:${REMOTE_DIR}/

echo "=== Restarting service... ==="
ssh ${REMOTE_HOST} "sudo systemctl restart melody-server"

echo "=== Checking status... ==="
ssh ${REMOTE_HOST} "sudo systemctl status melody-server --no-pager"

echo "=== Deploy complete! ==="

10. 总结与展望

10.1 项目亮点

  1. 清晰的架构分层:handler → service → model 三层架构,职责清晰
  2. 灵活的配置管理:Viper支持配置文件+环境变量,便于不同环境部署
  3. 安全的文件存储:阿里云OSS + 签名URL,保障资源安全
  4. 完整的音频处理:基于ffmpeg支持多种格式互转
  5. 自动化运维:定时去重任务减少人工干预

10.2 后续优化方向

  • 缓存优化:集成Redis缓存热点数据(歌曲列表、用户信息)
  • 搜索功能:接入Elasticsearch实现全文搜索
  • 消息队列:使用RabbitMQ处理异步任务(音频转换、数据导入)
  • 限流防护:基于令牌桶算法实现API限流
  • 监控告警:集成Prometheus + Grafana监控服务状态

10.3 源码地址

完整的项目代码已开源在 GitHub:

https://github.com/monkey-wenjun/melody_backend

欢迎 Star 和 PR!🎵


参考资料

文章作者:阿文
文章链接: https://www.awen.me/post/8d3e7a1f.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
您的评论由 AI 智能审核,一般1分钟内会展示,若不展示请确认你的评论是否符合社区和法律规范
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️