Spring Boot + Vue 3 全栈音乐平台:从零开发到部署上线

> 用 Spring Boot + Vue 3 做了一个在线音乐播放系统,包括用户端、管理端和鸿蒙端。这篇文章记录一下整个开发过程、核心代码和踩过的坑。

## 一、项目介绍

MyMusic 是一个在线音乐播放平台,支持用户注册登录、在线听歌、收藏、评论,管理端可以管理歌曲、歌手、歌单等。整个系统前后端分离,部署在阿里云服务器上,配了域名和 HTTPS。

主要功能:

用户端:邮箱验证码注册、JWT 登录、在线播放、进度控制、上下首切换、播放列表、按歌手/歌单/风格浏览、搜索、收藏、评论、修改头像/昵称/密码、轮播图

管理端:歌曲管理、歌手管理、歌单管理(绑定/解绑歌曲)、轮播图管理、用户管理(启用/禁用)、评论管理、反馈管理

鸿蒙端:用 ArkWeb 组件加载用户端页面,在鸿蒙系统上直接运行

## 二、技术选型

| 层次 | 技术 | 版本 | 说明 |

|------|------|------|------|

| 后端框架 | Spring Boot | 3.3.7 | Web 框架 |

| ORM | MyBatis-Plus | 3.5.9 | 简化数据库操作 |

| 数据库 | MySQL | 8.x | 关系型数据库 |

| 缓存 | Redis | 7.x | 存验证码和缓存 |

| 文件存储 | MinIO | — | 存音频和图片 |

| 认证 | JWT | 4.4.0 | 无状态令牌认证 |

| 前端框架 | Vue 3 | 3.5.x | 组合式 API |

| UI 组件库 | Element Plus | 2.9.x | 管理端用 |

| CSS 框架 | Tailwind CSS | 3.4.x | 用户端用 |

| 状态管理 | Pinia | 2.3.x | Vue 3 官方推荐 |

| 构建工具 | Vite | 6.x | 前端构建 |

| Web 服务器 | Nginx | 1.24 | 反代 + SSL |

| JDK | OpenJDK | 17 | — |

| 鸿蒙端 | ArkWeb | — | 内嵌 Web 页面 |

选型思路:Spring Boot 是课程要求的技术栈。Vue 3 比 Vue 2 写起来更舒服,组合式 API 逻辑更清晰。Tailwind 写样式快,不需要来回切 CSS 文件。MinIO 比 OSS 便宜,自建服务器就能跑。JWT 比 Session 更适合前后端分离的场景。

## 三、系统架构

整个系统部署在阿里云 ECS 上,Nginx 做反向代理:

```

用户浏览器

    │

    ├── hxakej.cn ────→ Nginx ──→ vibe-music-client/  (用户端静态文件)

    │                      │

    │                      ├── /api/  ──→ Spring Boot:8080

    │                      └── /minio/ ──→ MinIO:9005

    │

    └── admin.hxakej.cn → Nginx ──→ vibe-music-admin/  (管理端静态文件)

                                 │

                                 └── /api/  ──→ Spring Boot:8080

                                                   │

                                                   ├── MySQL:3306

                                                   ├── Redis:6379

                                                   └── MinIO:9005

鸿蒙系统 → HarmonyOS 应用 → @kit.ArkWeb Web 组件 → 加载 https://hxakej.cn

```

Nginx 做了这几件事:

1. 用户端和管理端分别用不同域名,各自指向对应的静态文件目录

2. `/api/` 路径的请求代理到后端 8080 端口

3. `/minio/` 路径代理到 MinIO 9005 端口,这样前端可以通过域名访问 MinIO 的文件

4. HTTP 全部 301 跳转到 HTTPS,两个域名各自配了 SSL 证书

## 四、项目结构

```

vibe_music/

├── vibe-music-server/              # 后端(Spring Boot)

│   ├── pom.xml

│   └── src/main/java/cn/edu/seig/vibemusic/

│       ├── config/                 # 配置类

│       ├── constant/               # 常量

│       ├── controller/             # 控制器

│       ├── enumeration/            # 枚举

│       ├── handler/                # 全局异常处理

│       ├── interceptor/            # 登录拦截器

│       ├── mapper/                 # MyBatis Mapper

│       ├── model/

│       │   ├── dto/                # 数据传输对象

│       │   ├── entity/             # 实体类

│       │   └── vo/                 # 视图对象

│       ├── result/                 # 统一响应封装

│       ├── service/impl/           # 业务实现

│       └── util/                   # 工具类

├── MyMusic_Admin/                  # 管理端(Vue 3 + Element Plus)

├── vibe-music-data/                # 项目资源文件

│   ├── artists/                    # 歌手头像

│   ├── banners/                    # 轮播图

│   ├── playlists/                  # 歌单封面

│   ├── songCovers/                 # 歌曲封面

│   ├── songs/                      # 音频文件

│   └── users/                      # 用户头像

└── 技术文档.md

```

后端按照 controller → service → mapper 三层架构组织。DTO 负责接收前端参数,VO 负责返回给前端的数据,Entity 对应数据库表,三者分离,避免直接暴露数据库字段。

## 五、数据库设计

数据库名 `vibe_music`,字符集 utf8mb4,一共 11 张表。核心的几张表如下:

**tb_user(用户表)**

| 字段 | 类型 | 说明 |

|------|------|------|

| id | bigint | 主键,自增 |

| username | varchar(20) | 用户名,唯一 |

| password | varchar(64) | MD5 加密存储 |

| email | varchar(128) | 邮箱,唯一 |

| user_avatar | varchar(255) | 头像 URL |

| status | tinyint | 0 启用 / 1 禁用 |

| create_time | datetime | 创建时间 |

**tb_song(歌曲表)**

| 字段 | 类型 | 说明 |

|------|------|------|

| id | bigint | 主键 |

| artist_id | bigint | 所属歌手 ID |

| name | varchar(255) | 歌曲名 |

| lyric | text | 歌词 |

| cover_url | varchar(255) | 封面图片 URL |

| audio_url | varchar(255) | 音频文件 URL |

| style | varchar(255) | 风格 |

**tb_artist(歌手表)**

| 字段 | 类型 | 说明 |

|------|------|------|

| id | bigint | 主键 |

| name | varchar(100) | 歌手名 |

| avatar | varchar(255) | 头像 URL |

| area | varchar(30) | 地区 |

**tb_playlist(歌单表)**

| 字段 | 类型 | 说明 |

|------|------|------|

| id | bigint | 主键 |

| title | varchar(255) | 歌单标题 |

| cover_url | varchar(255) | 封面 |

| style | varchar(255) | 风格 |

**tb_playlist_binding(歌单-歌曲关联表)**

| 字段 | 类型 | 说明 |

|------|------|------|

| id | bigint | 主键 |

| playlist_id | bigint | 歌单 ID |

| song_id | bigint | 歌曲 ID |

表之间的关系:

```

tb_user ──1:N──→ tb_user_favorite ←──N:1── tb_song

   │                                          │

   ├──1:N──→ tb_comment                       │

   └──1:N──→ tb_feedback                      │

                                               │

tb_artist ──1:N──────────────────────────────→│

                                               │

tb_playlist ──1:N──→ tb_playlist_binding ←────┘

```

歌单和歌曲是多对多关系,用 tb_playlist_binding 中间表关联。用户收藏用 tb_user_favorite,一条记录代表一个用户对一首歌的收藏。

## 六、核心代码

### 6.1 统一响应封装

后端所有接口返回统一格式,方便前端统一处理:

```java

@NoArgsConstructor

@AllArgsConstructor

@Data

public class Result<T> {

    private Integer code;   // 0-成功  1-失败

    private String message; // 提示信息

    private T data;         // 响应数据

    public static <T> Result<T> success(T data) {

        return new Result<>(0, "操作成功", data);

    }

    public static Result error(String message) {

        return new Result<>(1, message, null);

    }

}

```

前端 axios 拦截器里根据 code 判断成功还是失败,失败直接弹 message 提示。

### 6.2 JWT 令牌认证

JWT 工具类,用 HMAC256 算法签名,token 有效期 6 小时:

```java

public class JwtUtil {

    private static final String SECRET_KEY = "VIBE_MUSIC";

    private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 6; // 6小时

    // 生成 token

    public static String generateToken(Map<String, Object> claims) {

        return JWT.create()

                .withClaim("claims", claims)

                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))

                .sign(Algorithm.HMAC256(SECRET_KEY));

    }

    // 解析 token

    public static Map<String, Object> parseToken(String token) {

        return JWT.require(Algorithm.HMAC256(SECRET_KEY))

                .build()

                .verify(token)

                .getClaim("claims")

                .asMap();

    }

}

```

登录成功后,把用户 ID、用户名、邮箱、角色信息放进 claims 生成 token,返回给前端。前端每次请求在 Header 里带上 `Authorization: Bearer <token>`。

### 6.3 登录拦截器

拦截器负责校验 token 和权限:

```java

@Component

public class LoginInterceptor implements HandlerInterceptor {

    @Autowired

    private StringRedisTemplate stringRedisTemplate;

    @Autowired

    private RolePermissionManager rolePermissionManager;

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // OPTIONS 预检请求直接放行

        if (request.getMethod().equalsIgnoreCase("OPTIONS")) {

            return true;

        }

        // 白名单路径放行(登录、注册、公开接口等)

        List<String> whiteListPaths = Arrays.asList(

                "/admin/login", "/user/login", "/user/register",

                "/user/sendVerificationCode", "/banner/getBannerList",

                "/playlist/getAllPlaylists", "/artist/getAllArtists",

                "/song/getAllSongs", "/song/getRecommendedSongs"

        );

        PathMatcher pathMatcher = new AntPathMatcher();

        boolean isWhiteListPath = whiteListPaths.stream()

                .anyMatch(pattern -> pathMatcher.match(pattern, path));

        if (isWhiteListPath) return true;

        // 从 Header 取 token

        String token = request.getHeader("Authorization");

        if (token != null && token.startsWith("Bearer ")) {

            token = token.substring(7);

        }

        // 未登录但访问的是公开详情页,也放行

        if (token == null || token.isEmpty()) {

            // ...检查是否是允许未登录访问的路径

            sendErrorResponse(response, 401, "未登录");

            return false;

        }

        // 校验 token:先查 Redis 是否存在(防止 token 被手动失效后仍可用)

        String redisToken = stringRedisTemplate.opsForValue().get(token);

        if (redisToken == null) {

            sendErrorResponse(response, 401, "会话已过期");

            return false;

        }

        // 解析 token,校验角色权限

        Map<String, Object> claims = JwtUtil.parseToken(token);

        String role = (String) claims.get("role");

        if (rolePermissionManager.hasPermission(role, requestURI)) {

            ThreadLocalUtil.set(claims); // 存到 ThreadLocal,后续业务代码直接取

            return true;

        } else {

            sendErrorResponse(response, 403, "无权限访问");

            return false;

        }

    }

    @Override

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

        ThreadLocalUtil.remove(); // 请求结束后清理 ThreadLocal,防止内存泄漏

    }

}

```

这里有两个关键点:

1. token 除了 JWT 本身的校验,还要查 Redis 是否存在。因为用户登出时会从 Redis 删掉 token,这样即使 token 没过期,登出后也无法继续使用

2. 用户信息存在 ThreadLocal 里,后续业务代码通过 `ThreadLocalUtil.get()` 直接取,不用每个方法都传 userId

### 6.4 邮箱验证码注册

注册流程:用户输入邮箱 → 请求验证码 → 后端发邮件 + 存 Redis → 用户提交注册 → 校验验证码 → 创建用户

发送验证码:

```java

@Service

public class EmailServiceImpl implements EmailService {

    @Autowired

    private JavaMailSenderImpl mailSender;

    @Value("${spring.mail.username}")

    private String from;

    public String sendVerificationCodeEmail(String email) {

        String verificationCode = RandomCodeUtil.generateRandomCode();

        String subject = "【Vibe Music】验证码";

        String content = "您的验证码为:" + verificationCode;

        boolean success = sendEmail(email, subject, content);

        return success ? verificationCode : null;

    }

    public boolean sendEmail(String to, String subject, String content) {

        MimeMessage mimeMessage = mailSender.createMimeMessage();

        try {

            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage);

            helper.setFrom(from);

            helper.setTo(to);

            helper.setSubject(subject);

            helper.setText(content);

            mailSender.send(mimeMessage);

            return true;

        } catch (MessagingException e) {

            return false;

        }

    }

}

```

验证码存 Redis,5 分钟过期:

```java

public Result sendVerificationCode(String email) {

    String verificationCode = emailService.sendVerificationCodeEmail(email);

    if (verificationCode == null) {

        return Result.error("验证码发送失败");

    }

    // 存到 Redis,5 分钟过期

    stringRedisTemplate.opsForValue().set(

        "verificationCode:" + email, verificationCode, 5, TimeUnit.MINUTES

    );

    return Result.success("验证码发送成功");

}

```

注册时校验验证码:

```java

public Result register(UserRegisterDTO userRegisterDTO) {

    // 先删验证码(一次性使用)

    stringRedisTemplate.delete("verificationCode:" + userRegisterDTO.getEmail());

    // 检查用户名和邮箱是否已存在

    User userByUsername = userMapper.selectOne(

        new QueryWrapper<User>().eq("username", userRegisterDTO.getUsername())

    );

    if (userByUsername != null) return Result.error("用户名已存在");

    User userByEmail = userMapper.selectOne(

        new QueryWrapper<User>().eq("email", userRegisterDTO.getEmail())

    );

    if (userByEmail != null) return Result.error("邮箱已注册");

    // 密码 MD5 加密存储

    String passwordMD5 = DigestUtils.md5DigestAsHex(

        userRegisterDTO.getPassword().getBytes()

    );

    User user = new User();

    user.setUsername(userRegisterDTO.getUsername())

        .setPassword(passwordMD5)

        .setEmail(userRegisterDTO.getEmail())

        .setCreateTime(LocalDateTime.now())

        .setUserStatus(UserStatusEnum.ENABLE);

    userMapper.insert(user);

    return Result.success("注册成功");

}

```

验证码设计成一次性的,验证完直接从 Redis 删掉,防止重复使用。

### 6.5 MinIO 文件上传

所有音频和图片都存 MinIO,不走本地磁盘。上传、删除的核心逻辑:

```java

@Service

public class MinioServiceImpl implements MinioService {

    private final MinioClient minioClient;

    @Value("${minio.bucket}")

    private String bucketName;

    @Value("${minio.endpoint}")

    private String endpoint;

    // 上传文件

    public String uploadFile(MultipartFile file, String folder) {

        try {

            // 生成唯一文件名,避免覆盖

            String fileName = folder + "/" + UUID.randomUUID() + "-" + file.getOriginalFilename();

            minioClient.putObject(

                PutObjectArgs.builder()

                    .bucket(bucketName)

                    .object(fileName)

                    .stream(file.getInputStream(), file.getSize(), -1)

                    .contentType(file.getContentType())

                    .build()

            );

            // 返回可访问的 URL

            return endpoint + "/" + bucketName + "/" + fileName;

        } catch (Exception e) {

            throw new RuntimeException("文件上传失败:" + e.getMessage());

        }

    }

    // 删除文件

    public void deleteFile(String fileUrl) {

        try {

            String filePath = fileUrl.replace(endpoint + "/" + bucketName + "/", "");

            minioClient.removeObject(

                RemoveObjectArgs.builder()

                    .bucket(bucketName)

                    .object(filePath)

                    .build()

            );

        } catch (Exception e) {

            throw new RuntimeException("文件删除失败: " + e.getMessage());

        }

    }

}

```

MinIO 配置类:

```java

@Configuration

public class MinioConfig {

    @Value("${minio.endpoint}")

    private String endpoint;

    @Value("${minio.accessKey}")

    private String accessKey;

    @Value("${minio.secretKey}")

    private String secretKey;

    @Bean

    public MinioClient minioClient() {

        return MinioClient.builder()

                .endpoint(endpoint)

                .credentials(accessKey, secretKey)

                .build();

    }

}

```

application.yml 中的配置

```yaml

minio:

  endpoint: http://47.108.193.47:9005

  accessKey: root

  secretKey: your-secret-key

  bucket: vibe-music-data

``

文件名用 UUID 拼接原始文件名,避免同名文件覆盖。上传成功后返回完整的访问 URL,存到数据库里。

### 6.6 鸿蒙端适配

鸿蒙端没有用原生 ArkUI 开发页面,而是用 `@kit.ArkWeb` 的 Web 组件直接加载用户端线上地址。这样用户端已有的移动端适配样式在鸿蒙上也能用,开发成本几乎为零:

```typescript

// entry/src/main/ets/pages/Index.ets

import { webview } from '@kit.ArkWeb';

@Entry

@Component

struct Index {

  controller: webview.WebviewController = new webview.WebviewController();

  build() {

    Column() {

      Web({ src: 'https://hxakej.cn', controller: this.controller })

        .width('100%')

        .height('100%')

    }

    .width('100%')

    .height('100%')

  }

}

```

这种方式适合 Web 应用快速适配鸿蒙系统,缺点是性能不如原生开发,但对于我们这种以展示和播放为主的场景够用了。

## 七、服务器部署

### 7.1 服务器信息

| 项目 | 配置 |

|------|------|

| 云服务商 | 阿里云 ECS |

| 操作系统 | Ubuntu 24.04 LTS |

| CPU | 2 核 |

| 内存 | 2 GB |

| 域名 | hxakej.cn |

| 带宽 | 1 Mbps |

### 7.2 后端部署

后端打包成 JAR,用 systemd 管理:

```bash

# 打包

cd vibe-music-server

mvn clean package -DskipTests

# 上传到服务器

scp target/vibe-music-server-0.0.1-SNAPSHOT.jar root@47.108.193.47:/opt/vibe-music/

# 启动

systemctl start vibe-music

systemctl status vibe-music

```

systemd 服务文件 `/etc/systemd/system/vibe-music.service`:

```ini

[Unit]

Description=MyMusic Backend Service

After=network.target

[Service]

Type=simple

User=root

WorkingDirectory=/opt/vibe-music

ExecStart=/usr/bin/java -jar vibe-music-server-0.0.1-SNAPSHOT.jar

Restart=on-failure

[Install]

WantedBy=multi-user.target

```

### 7.3 前端部署

前端在本地打包,上传 dist 到服务器:

```bash

# 用户端

cd vibe-music-client

pnpm install

pnpm run build

# 上传 dist/ 到 /var/www/vibe-music-client/

# 管理端

cd MyMusic_Admin

pnpm install

pnpm run build

# 上传 dist/ 到 /var/www/vibe-music-admin/

```

服务器内存只有 2G,跑不起 Node.js 打包,所以必须本地打包后上传。

### 7.4 Nginx 配置

用户端和管理端各自独立的 server 块:

```nginx

# 用户端

server {

    listen 443 ssl;

    server_name hxakej.cn;

    ssl_certificate     /etc/nginx/ssl/hxakej.cn.pem;

    ssl_certificate_key /etc/nginx/ssl/hxakej.cn.key;

    # 静态文件

    location / {

        root /var/www/vibe-music-client;

        index index.html;

        try_files $uri $uri/ /index.html;  # SPA 路由支持

    }

    # API 代理

    location /api/ {

        proxy_pass http://127.0.0.1:8080/;

        proxy_set_header Host $host;

        proxy_set_header X-Real-IP $remote_addr;

    }

    # MinIO 代理

    location /minio/ {

        proxy_pass http://127.0.0.1:9005/;

    }

}

# 管理端

server {

    listen 443 ssl;

    server_name admin.hxakej.cn;

    ssl_certificate     /etc/nginx/ssl/admin.hxakej.cn.pem;

    ssl_certificate_key /etc/nginx/ssl/admin.hxakej.cn.key;

    location / {

        root /var/www/vibe-music-admin;

        index index.html;

        try_files $uri $uri/ /index.html;

    }

    location /api/ {

        proxy_pass http://127.0.0.1:8080/;

        proxy_set_header Host $host;

        proxy_set_header X-Real-IP $remote_addr;

    }

}

```

`try_files` 这行很关键,SPA 应用刷新页面时 Nginx 会尝试找对应的文件,找不到就返回 index.html,交给前端路由处理。不加这行的话,刷新页面会 404。

## 八、踩坑记录

这部分是开发过程中遇到的实际问题,写出来希望能帮到遇到同样问题的人。

### 坑 1:MinIO endpoint 不能用公网地址

一开始想把 `application.yml` 里的 MinIO endpoint 改成公网地址 `https://hxakej.cn/minio`,这样后端返回给前端的文件 URL 就是公网可访问的。结果改成公网地址后,后端直接 502 启动失败。

原因:MinIO endpoint 是后端 Java 进程内部连接 MinIO 用的,改成 HTTPS 的公网地址后,Java 客户端走了一遍 Nginx 反代 + SSL,连接逻辑就乱了。MinIO SDK 直连 localhost:9005 就行。

解决方案:endpoint 保持 `http://localhost:9005` 不动,前端拿到 URL 后做字符串替换,把 `http://localhost:9005` 替换成 `https://hxakej.cn/minio`。

### 坑 2:前端头像 URL 替换

接上一个坑,后端返回的用户头像 URL 是 `http://localhost:9005/vibe-music-data/users/xxx.jpg`,前端直接用这个地址是访问不到的(localhost 指的是服务器的 localhost,不是用户的电脑)。

解决方案在前端的 Pinia store 里,用户信息存入时做 URL 替换:

```javascript

// 用户端 stores/modules/user.ts

function setUserInfo(data) {

    if (data.userAvatar) {

        data.userAvatar = data.userAvatar

            .replace('http://localhost:9005', 'https://hxakej.cn/minio')

            .replace('http://127.0.0.1:9005', 'https://hxakej.cn/minio');

    }

    userInfo.value = data;

}

```

同时数据库里存的历史数据也要批量替换:

```sql

UPDATE tb_user SET user_avatar = REPLACE(user_avatar, 'http://localhost:9005', 'https://hxakej.cn/minio') WHERE user_avatar LIKE '%localhost:9005%';

UPDATE tb_song SET cover_url = REPLACE(cover_url, 'http://localhost:9005', 'https://hxakej.cn/minio') WHERE cover_url LIKE '%localhost:9005%';

-- 其他表同理...

```

### 坑 3:打包后管理端白屏

管理端打包上传后,页面一直加载,打开浏览器控制台发现 JS 文件请求返回的是 HTML 而不是 JS。

排查过程:先怀疑 Nginx 配置问题,用 `curl` 直接请求 JS 文件,返回的 Content-Type 是 `text/html`。然后去服务器看文件结构,发现 `static/` 目录下面嵌套了一层 `static/static/`,外层的 js/css/png 目录是空的。

原因:构建时输出的目录结构不对,可能是打包配置的 base 路径和实际部署路径不一致,导致多套了一层。

解决:把内层 static 的内容移到外层,页面正常加载。

### 坑 4:Apache httpd 反复抢占 80 端口

服务器重启后好几次发现网站访问不了,排查发现是 Apache httpd 自动启动占用了 80 端口,Nginx 起不来。看起来是系统某个依赖装了 httpd 并且设了开机自启。

```bash

# 查看谁占了 80 端口

lsof -i :80

# 停掉 httpd 并禁用开机自启

systemctl stop httpd

systemctl disable httpd

# 启动 Nginx

systemctl start nginx

```

这个问题反复出现,每次服务器重启都要检查一下。最根本的解决方式是卸载 httpd:`apt remove apache2`。

### 坑 5:服务器带宽 1Mbps 音频加载慢

一首 3MB 的歌,在 1Mbps 带宽下理论上需要 24 秒才能下载完,实际体验就是点播放后要等很久才能开始放。

临时方案:前端 audio 标签加 `preload="none"`,不预加载音频,用户点了播放才请求。

长期方案:加 CDN 加速,或者升级带宽。对于学生项目来说成本太高,暂时没搞。

## 九、本地运行

如果想本地跑起来看看,需要装 JDK 17、Node.js 18+、pnpm。MySQL、Redis、MinIO 不用本地装,项目已经配好连接云服务器。

后端:

```bash

cd vibe-music-server

mvn clean install -DskipTests

mvn spring-boot:run

```

用户端(开发模式):

```bash

cd vibe-music-client

pnpm install

pnpm dev

```

管理端(开发模式):

```bash

cd MyMusic_Admin

pnpm install

pnpm dev

```

## 十、总结

从架构设计到部署上线,基本走了一遍完整的项目流程。几个觉得做得还不错的地方:

1. 前后端分离,后端只提供 API,前端独立开发和部署

2. MinIO 做对象存储,音频和图片统一管理,不占服务器磁盘

3. JWT + Redis 的认证方案,登出时删 Redis 让 token 立刻失效

4. 完整部署到阿里云,配了域名和 HTTPS

5. 鸿蒙端通过 ArkWeb 零成本适配

当然也有不少不足:

1. 服务器带宽太小,音频加载慢

2. 歌词只是纯文本显示,没有逐行高亮

3. Redis 和 MySQL 端口对公网开放了,安全上有隐患

4. 鸿蒙端只是 Web 壳,体验不如原生

5. 缺少播放量统计和排行榜

如果这篇文章对你有帮助的话,点个赞支持一下~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐