commit b275ffab5a9d8721a5c6be2f5e92170693c23803 Author: ikmkj <1@qq,com> Date: Sat Apr 26 19:32:45 2025 +0800 feat: 初始化音乐项目后台管理系统- 新增后台管理相关页面和功能组件 - 实现管理员评论管理、数据统计等功能 - 添加后台布局和路由管理 -配置数据库和Redis连接 - 实现文件上传和访问路径配置 - 添加Sa-Token安全配置 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..843870d --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +### Example user template template +### Example user template + +# IntelliJ project files +.log +*.gz +.gz +*/.gz +*/.log +target +*.iml +out +gen +*/.vscode +.vscode +#忽略所有.svn目录 +.svn/ +#忽略所有target目录 +target/ +#忽略所有.idea目录及其内容 +.idea/ +.idea/* +#忽略所有.ipr文件 +*.ipr +#忽略所有.iws文件 +*.iws +#忽略所有.gradle目录 +.gradle/ +#忽略所有.settings目录 +.settings/ +#忽略所有.classpath文件 +.classpath +#忽略所有.project文件 +.project +#忽略所有.pydevproject文件 +.pydevproject +#忽略所有.DS_Store文件 + +#忽略所有.class文件 +*.class +#忽略所有.log文件 + +#忽略所有.tmp文件 +*.tmp +#忽略所有.swp文件 +*.swp +node_modules/ +/dist/ + +.git + +# Editor directories and files +dist/ +npm-debug.log +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b497b1 --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +# 音乐播放应用系统 + +## 项目概述 + +这是一个功能完整的音乐播放应用系统,包含前后端两部分。系统支持用户注册登录、音乐播放、歌单管理、歌手管理、评论互动等功能,为用户提供全面的音乐体验。 + +### 系统架构 + +本项目采用前后端分离架构: +- 前端:基于Vue 3的单页面应用 +- 后端:基于Spring Boot的RESTful API服务 +- 数据库:MySQL关系型数据库 +- 缓存:Redis缓存服务 + +## 技术栈 + +### 后端技术栈 + +- **核心框架**:Spring Boot 3.x +- **ORM框架**:Spring Data JPA + Hibernate +- **安全认证**:Sa-Token +- **数据库**:MySQL 8.x +- **缓存**:Redis +- **API文档**:Knife4j/Swagger +- **文件处理**:自定义文件上传服务 + +### 前端技术栈 + +- **核心框架**:Vue 3 (组合式API) +- **构建工具**:Vite +- **状态管理**:Pinia +- **UI组件**:Element Plus +- **路由管理**:Vue Router +- **HTTP请求**:Axios +- **音频处理**:Web Audio API + +## 功能模块 + +### 1. 用户系统 + +- 用户注册与登录 +- 个人信息管理 +- 收藏管理(音乐、歌单、歌手) +- 播放历史记录 +- 权限控制(普通用户/管理员) + +### 2. 音乐播放系统 + +- 音乐播放、暂停、上一首、下一首 +- 播放列表管理 +- 播放模式(顺序播放、单曲循环、列表循环) +- 歌词同步显示 +- 进度控制与音量调节 +- 播放统计 + +### 3. 音乐管理系统 + +- 音乐上传与编辑 +- 音乐分类管理 +- 音乐搜索 +- 热门音乐推荐 +- 最新音乐展示 + +### 4. 歌手管理系统 + +- 歌手信息管理 +- 歌手分类 +- 歌手音乐关联 +- 歌手搜索 + +### 5. 歌单管理系统 + +- 歌单创建与编辑 +- 歌单分类 +- 歌单音乐管理 +- 歌单收藏 +- 官方推荐歌单 + +### 6. 评论系统 + +- 发表评论 +- 回复评论 +- 评论管理 + +### 7. 首页推荐系统 + +- 轮播图展示 +- 热门音乐推荐 +- 最新音乐展示 +- 推荐歌单 +- 热门歌手 + +## 数据模型 + +### 核心实体 + +1. **用户(User)**:用户基本信息 +2. **音乐(Music)**:音乐信息、文件路径、歌词 +3. **歌手(Singer)**:歌手信息 +4. **歌单(Playlist)**:歌单信息 +5. **评论(Comment)**:评论内容 +6. **类型(Type)**:音乐类型、歌手类型、歌单类型 +7. **轮播图(Banner)**:首页轮播图 + +### 核心关系 + +1. **用户-音乐**:收藏关系、播放历史 +2. **用户-歌单**:创建关系、收藏关系 +3. **用户-歌手**:收藏关系 +4. **音乐-歌手**:多对多关系 +5. **音乐-类型**:多对多关系 +6. **歌单-音乐**:多对多关系 +7. **歌手-类型**:多对一关系 +8. **歌单-类型**:多对一关系 + +## 部署指南 + +### 环境要求 + +- JDK 17+ +- Node.js 16+ +- MySQL 8.x +- Redis 6.x + +### 后端部署 + +1. 配置数据库 + ```sql + CREATE DATABASE music_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; + ``` + +2. 修改配置文件 + - 编辑 `music-houduan/src/main/resources/application.yml` + - 配置数据库连接信息 + - 配置Redis连接信息 + - 配置文件上传路径 + +3. 构建项目 + ```bash + cd music-houduan + mvn clean package -DskipTests + ``` + +4. 运行项目 + ```bash + java -jar target/music-houduan-1.0.0.jar + ``` + +### 前端部署 + +1. 安装依赖 + ```bash + cd music-qianduan + npm install + ``` + +2. 修改配置 + - 编辑 `.env` 文件,配置API地址 + +3. 构建项目 + ```bash + npm run build + ``` + +4. 部署静态资源 + - 将 `dist` 目录下的文件部署到Web服务器 + +## 使用说明 + +### 管理员账号 + +- 用户名:admin +- 密码:123456 + +### 普通用户 + +- 可以自行注册或使用以下测试账号: + - 用户名:test + - 密码:123456 + +### 基本操作流程 + +1. **音乐播放** + - 在首页或音乐列表页选择音乐 + - 点击播放按钮开始播放 + - 使用播放器控制播放进度、音量等 + +2. **歌单管理** + - 登录后可创建个人歌单 + - 添加音乐到歌单 + - 收藏其他用户的歌单 + +3. **音乐上传(管理员)** + - 登录管理员账号 + - 进入后台管理 + - 上传音乐文件和封面 + +4. **评论互动** + - 在音乐、歌单、歌手详情页发表评论 + - 回复其他用户的评论 + +## 系统截图 + +(此处可添加系统主要页面的截图) + +## 开发团队 + +- 前端开发:XXX +- 后端开发:XXX +- 产品设计:XXX +- 测试:XXX + +## 版权声明 + +本项目仅用于学习和演示目的,不得用于商业用途。音乐文件版权归原作者所有。 + +## 联系方式 + +如有问题或建议,请联系:example@example.com diff --git a/doc/音乐下载.txt b/doc/音乐下载.txt new file mode 100644 index 0000000..3579140 --- /dev/null +++ b/doc/音乐下载.txt @@ -0,0 +1,2 @@ +https://www.gequbao.com/ +https://www.gequhai.com/ \ No newline at end of file diff --git a/music-houduan/.gitattributes b/music-houduan/.gitattributes new file mode 100644 index 0000000..22a3c0b --- /dev/null +++ b/music-houduan/.gitattributes @@ -0,0 +1,64 @@ +### Example user template template +### Example user template + +# IntelliJ project files +.idea +target +*.gz +*.log +.log +*.logs +.logs +*.zip +*.iml +out +gen +*/.vscode +.vscode +#忽略所有.svn目录 +.svn/ +#忽略所有target目录 +target/ +#忽略所有.idea目录及其内容 +.idea/ +.idea/* +#忽略所有.ipr文件 +*.ipr +#忽略所有.iws文件 +*.iws +#忽略所有.gradle目录 +.gradle/ +#忽略所有.settings目录 +.settings/ +#忽略所有.classpath文件 +.classpath +#忽略所有.project文件 +.project +#忽略所有.pydevproject文件 +.pydevproject +#忽略所有.DS_Store文件 +.DS_Store +#忽略所有.class文件 +*.class +#忽略所有.log文件 +*.log +#忽略所有.tmp文件 +*.tmp +#忽略所有.swp文件 +*.swp +node_modules/ +/dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.git + +# Editor directories and files +*.suo +*.ntvs* +*.njsproj +*.sln +dist/ +npm-debug.log +.vscode/ +README.md \ No newline at end of file diff --git a/music-houduan/.gitignore b/music-houduan/.gitignore new file mode 100644 index 0000000..8b1cf61 --- /dev/null +++ b/music-houduan/.gitignore @@ -0,0 +1,39 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +*.log +*.logs +*/*.log +*/.log + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ +logs +node_modules \ No newline at end of file diff --git a/music-houduan/pom.xml b/music-houduan/pom.xml new file mode 100644 index 0000000..16ae858 --- /dev/null +++ b/music-houduan/pom.xml @@ -0,0 +1,145 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.test + music-houduan + 0.0.1-SNAPSHOT + music-houduan + 音乐应用后端 + + + 17 + 1.38.0 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + org.projectlombok + lombok + true + + + + + com.mysql + mysql-connector-j + runtime + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + + + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + org.apache.commons + commons-lang3 + + + + + commons-fileupload + commons-fileupload + 1.5 + + + + + net.coobird + thumbnailator + 0.4.20 + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.5.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/music-houduan/src/main/java/com/test/musichouduan/MusicHouduanApplication.java b/music-houduan/src/main/java/com/test/musichouduan/MusicHouduanApplication.java new file mode 100644 index 0000000..668ac50 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/MusicHouduanApplication.java @@ -0,0 +1,21 @@ +package com.test.musichouduan; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * 音乐应用后端启动类 + */ +@SpringBootApplication +@EnableTransactionManagement +@EnableScheduling +public class MusicHouduanApplication { + + public static void main(String[] args) { + SpringApplication.run(MusicHouduanApplication.class, args); + System.out.println("\n---------- 音乐应用后端启动成功 ----------\n"); + } + +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/config/CorsConfig.java b/music-houduan/src/main/java/com/test/musichouduan/config/CorsConfig.java new file mode 100644 index 0000000..e3783d6 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/config/CorsConfig.java @@ -0,0 +1,33 @@ +package com.test.musichouduan.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * 跨域配置 + */ +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + // 允许指定域名跨域调用 + config.addAllowedOrigin("http://localhost:5173"); + config.addAllowedOrigin("http://localhost:5174"); + config.addAllowedOrigin("https://your-production-domain.com"); + // 允许跨域发送cookie + config.setAllowCredentials(true); + // 放行全部原始头信息 + config.addAllowedHeader("*"); + // 允许所有请求方法跨域调用 + config.addAllowedMethod("*"); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/config/Knife4jConfig.java b/music-houduan/src/main/java/com/test/musichouduan/config/Knife4jConfig.java new file mode 100644 index 0000000..621ef43 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/config/Knife4jConfig.java @@ -0,0 +1,24 @@ +package com.test.musichouduan.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Knife4j 配置类 + */ +@Configuration +public class Knife4jConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // 配置 Knife4j 的静态资源 + registry.addResourceHandler("doc.html") + .addResourceLocations("classpath:/META-INF/resources/") + .resourceChain(false); // 使用默认的资源解析器 + registry.addResourceHandler("/webjars/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/") + .resourceChain(false); // 使用默认的资源解析器 + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/config/RedisConfig.java b/music-houduan/src/main/java/com/test/musichouduan/config/RedisConfig.java new file mode 100644 index 0000000..8cbcb66 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/config/RedisConfig.java @@ -0,0 +1,34 @@ +package com.test.musichouduan.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置 + */ +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + // 使用GenericJackson2JsonRedisSerializer来序列化和反序列化redis的value值 + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + // Hash的value也采用GenericJackson2JsonRedisSerializer的序列化方式 + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + template.afterPropertiesSet(); + return template; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/config/SaTokenConfig.java b/music-houduan/src/main/java/com/test/musichouduan/config/SaTokenConfig.java new file mode 100644 index 0000000..782c369 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/config/SaTokenConfig.java @@ -0,0 +1,55 @@ +package com.test.musichouduan.config; + +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +/** + * Sa-Token配置 + */ +@Configuration +public class SaTokenConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册Sa-Token拦截器,打开注解式鉴权功能 + registry.addInterceptor(new SaInterceptor(handle -> { + // 打印调试信息 + System.out.println("\n=== Sa-Token 拦截器检查登录状态 ===\n"); + + // 获取当前登录用户ID + Object loginId = StpUtil.getLoginIdDefaultNull(); + System.out.println("当前登录用户ID: " + loginId); + + // 获取当前用户角色 + List roleList = StpUtil.getRoleList(); + System.out.println("当前用户角色: " + roleList); + + // 检查登录状态 + StpUtil.checkLogin(); + })) + .addPathPatterns("/api/**") + .excludePathPatterns( + "/api/user/login", + "/api/user/register", + "/api/music/list", + "/api/music/latest", + "/api/music/hot", + "/api/music/*/play", + "/api/playlist/list", + "/api/playlist/latest", + "/api/playlist/hot", + "/api/playlist/official", + "/api/playlist/*/play", + "/api/singer/list", + "/api/singer/latest", + "/api/type/**", + "/api/banner/list", + "/api/comment/list" + ); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/config/StpInterfaceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/config/StpInterfaceImpl.java new file mode 100644 index 0000000..44d3b7c --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/config/StpInterfaceImpl.java @@ -0,0 +1,55 @@ +package com.test.musichouduan.config; + +import cn.dev33.satoken.stp.StpInterface; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 自定义权限验证接口扩展 + */ +@Component +public class StpInterfaceImpl implements StpInterface { + + @Autowired + private UserRepository userRepository; + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 暂时不需要权限码 + return new ArrayList<>(); + } + + /** + * 返回一个账号所拥有的角色标识集合 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + List roles = new ArrayList<>(); + + // 获取用户ID + Long userId = Long.parseLong(loginId.toString()); + + // 查询用户 + userRepository.findById(userId).ifPresent(user -> { + // 根据用户的role字段设置角色 + if (user.getRole() == 1) { + roles.add("admin"); + } else { + roles.add("user"); + } + + // 打印调试信息 + System.out.println("用户ID: " + userId + ", 角色: " + roles); + }); + + return roles; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/config/SwaggerConfig.java b/music-houduan/src/main/java/com/test/musichouduan/config/SwaggerConfig.java new file mode 100644 index 0000000..78ebb5b --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/config/SwaggerConfig.java @@ -0,0 +1,119 @@ +package com.test.musichouduan.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * Knife4j配置 + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + // 添加服务器信息 + List servers = new ArrayList<>(); + servers.add(new Server().url("/").description("本地服务")); + + return new OpenAPI() + .info(new Info() + .title("音乐应用API文档") + .description("音乐应用后端API接口文档") + .version("v1.0.0") + .contact(new Contact() + .name("Admin") + .email("admin@example.com") + .url("https://example.com")) + .license(new License() + .name("Apache 2.0") + .url("https://www.apache.org/licenses/LICENSE-2.0.html"))) + .externalDocs(new ExternalDocumentation() + .description("项目文档") + .url("https://example.com/docs")) + .servers(servers) + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) + .components(new Components() + .addSecuritySchemes("bearerAuth", new SecurityScheme() + .name("bearerAuth") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } + + @Bean + public GroupedOpenApi userApi() { + return GroupedOpenApi.builder() + .group("用户接口") + .pathsToMatch("/api/user/**") + .build(); + } + + @Bean + public GroupedOpenApi musicApi() { + return GroupedOpenApi.builder() + .group("音乐接口") + .pathsToMatch("/api/music/**") + .build(); + } + + @Bean + public GroupedOpenApi singerApi() { + return GroupedOpenApi.builder() + .group("歌手接口") + .pathsToMatch("/api/singer/**") + .build(); + } + + @Bean + public GroupedOpenApi playlistApi() { + return GroupedOpenApi.builder() + .group("歌单接口") + .pathsToMatch("/api/playlist/**") + .build(); + } + + @Bean + public GroupedOpenApi commentApi() { + return GroupedOpenApi.builder() + .group("评论接口") + .pathsToMatch("/api/comment/**") + .build(); + } + + @Bean + public GroupedOpenApi typeApi() { + return GroupedOpenApi.builder() + .group("类型接口") + .pathsToMatch("/api/type/**") + .build(); + } + + @Bean + public GroupedOpenApi bannerApi() { + return GroupedOpenApi.builder() + .group("轮播图接口") + .pathsToMatch("/api/banner/**") + .build(); + } + + @Bean + public GroupedOpenApi adminApi() { + return GroupedOpenApi.builder() + .group("管理员接口") + .pathsToMatch("/api/admin/**") + .build(); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/config/WebConfig.java b/music-houduan/src/main/java/com/test/musichouduan/config/WebConfig.java new file mode 100644 index 0000000..8ed84b1 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/config/WebConfig.java @@ -0,0 +1,25 @@ +package com.test.musichouduan.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Web配置类 + */ +@Configuration +public class WebConfig implements WebMvcConfigurer { + + /** + * 跨域配置 + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/config/WebMvcConfig.java b/music-houduan/src/main/java/com/test/musichouduan/config/WebMvcConfig.java new file mode 100644 index 0000000..47476b4 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/config/WebMvcConfig.java @@ -0,0 +1,28 @@ +package com.test.musichouduan.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Web MVC 配置 + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Value("${file.upload-path}") + private String uploadPath; + + @Value("${file.access-path}") + private String accessPath; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // 配置上传文件资源映射 + registry.addResourceHandler(accessPath + "**") + .addResourceLocations("file:" + uploadPath); + + // 注意:前后端分离项目,不需要配置静态资源映射 + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/config/WebSocketConfig.java b/music-houduan/src/main/java/com/test/musichouduan/config/WebSocketConfig.java new file mode 100644 index 0000000..3edab79 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/config/WebSocketConfig.java @@ -0,0 +1,33 @@ +package com.test.musichouduan.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + // 配置消息代理,用于广播式消息(例如,/topic/ 前缀) + // 客户端订阅 "/topic/comments/{targetType}/{targetId}" 来接收特定目标的评论更新 + config.enableSimpleBroker("/topic"); + // 配置应用程序目标前缀,客户端发送消息到服务器的目标前缀(例如,/app/ 前缀) + // 这个项目我们主要用服务器主动推送,所以 /app 可能用不到,但先配置上 + config.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // 注册一个 STOMP 端点,客户端将使用它连接到 WebSocket 服务器 + // "/ws" 是连接的端点路径 + // withSockJS() 启用 SockJS 后备选项,以便在 WebSocket 不可用时使用其他传输方式 + // setAllowedOrigins("*") 允许所有来源的跨域连接,在生产环境中应配置为具体的来源 + registry.addEndpoint("/ws") + .setAllowedOrigins("http://localhost:5173", "http://localhost:5174", "https://your-production-domain.com") + .withSockJS(); // 启用 SockJS 支持 + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/BannerController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/BannerController.java new file mode 100644 index 0000000..bee199a --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/BannerController.java @@ -0,0 +1,153 @@ +package com.test.musichouduan.controller; + +import cn.dev33.satoken.annotation.SaCheckRole; +import com.test.musichouduan.dto.BannerDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.service.BannerService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 轮播图控制器 + */ +@Tag(name = "轮播图接口", description = "轮播图的增删改查操作") +@RestController +@RequestMapping("/api/banner") +public class BannerController { + + @Autowired + private BannerService bannerService; + + /** + * 根据ID获取轮播图 + * + * @param id 轮播图ID + * @return 响应结果 + */ + @Operation(summary = "根据ID获取轮播图", description = "根据轮播图ID获取轮播图详情") + @GetMapping("/{id}") + public Result getBannerById(@Parameter(description = "轮播图ID") @PathVariable Long id) { + BannerDTO bannerDTO = bannerService.getBannerById(id); + return Result.success(bannerDTO); + } + + /** + * 获取轮播图列表 + * + * @return 响应结果 + */ + @Operation(summary = "获取轮播图列表", description = "获取所有启用状态的轮播图列表") + @GetMapping("/list") + public Result> getBannerList() { + List bannerList = bannerService.getBannerList(); + return Result.success(bannerList); + } + + /** + * 分页获取轮播图列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @Operation(summary = "分页获取轮播图列表", description = "分页获取轮播图列表,需要管理员权限") + @SaCheckRole("admin") + @GetMapping("/page") + public Result> getBannerPage( + @Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") Integer page, + @Parameter(description = "每页大小", example = "10") @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = bannerService.getBannerPage(page, size); + return Result.success(pageResult); + } + + /** + * 添加轮播图 + * + * @param bannerDTO 轮播图DTO + * @param image 图片文件 + * @return 响应结果 + */ + @Operation(summary = "添加轮播图", description = "添加轮播图,需要管理员权限") + @SaCheckRole("admin") + @PostMapping("/add") + public Result addBanner( + @Parameter(description = "轮播图信息") @RequestPart("banner") BannerDTO bannerDTO, + @Parameter(description = "轮播图图片文件") @RequestPart("image") MultipartFile image) { + BannerDTO result = bannerService.addBanner(bannerDTO, image); + return Result.success("添加成功", result); + } + + /** + * 更新轮播图 + * + * @param id 轮播图ID + * @param bannerDTO 轮播图DTO + * @param image 图片文件 + * @return 响应结果 + */ + @Operation(summary = "更新轮播图", description = "更新轮播图信息,需要管理员权限") + @SaCheckRole("admin") + @PutMapping("/{id}") + public Result updateBanner( + @Parameter(description = "轮播图ID") @PathVariable Long id, + @Parameter(description = "轮播图信息") @RequestPart("banner") BannerDTO bannerDTO, + @Parameter(description = "轮播图图片文件,可选") @RequestPart(value = "image", required = false) MultipartFile image) { + BannerDTO result = bannerService.updateBanner(id, bannerDTO, image); + return Result.success("更新成功", result); + } + + /** + * 删除轮播图 + * + * @param id 轮播图ID + * @return 响应结果 + */ + @Operation(summary = "删除轮播图", description = "删除轮播图,需要管理员权限") + @SaCheckRole("admin") + @DeleteMapping("/{id}") + public Result deleteBanner(@Parameter(description = "轮播图ID") @PathVariable Long id) { + boolean result = bannerService.deleteBanner(id); + return Result.success("删除成功", result); + } + + /** + * 更新轮播图状态 + * + * @param id 轮播图ID + * @param status 状态 + * @return 响应结果 + */ + @Operation(summary = "更新轮播图状态", description = "更新轮播图状态,0-禁用,1-启用,需要管理员权限") + @SaCheckRole("admin") + @PutMapping("/{id}/status") + public Result updateBannerStatus( + @Parameter(description = "轮播图ID") @PathVariable Long id, + @Parameter(description = "状态,0-禁用,1-启用", example = "1") @RequestParam Integer status) { + boolean result = bannerService.updateBannerStatus(id, status); + return Result.success("更新成功", result); + } + + /** + * 更新轮播图排序 + * + * @param id 轮播图ID + * @param orderNum 排序号 + * @return 响应结果 + */ + @Operation(summary = "更新轮播图排序", description = "更新轮播图排序号,数字越小越靠前,需要管理员权限") + @SaCheckRole("admin") + @PutMapping("/{id}/order") + public Result updateBannerOrder( + @Parameter(description = "轮播图ID") @PathVariable Long id, + @Parameter(description = "排序号,数字越小越靠前", example = "1") @RequestParam Integer orderNum) { + boolean result = bannerService.updateBannerOrder(id, orderNum); + return Result.success("更新成功", result); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/BasicTestController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/BasicTestController.java new file mode 100644 index 0000000..589418b --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/BasicTestController.java @@ -0,0 +1,29 @@ +package com.test.musichouduan.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * 基本测试控制器 + */ +@RestController +@RequestMapping("/basic-test") +public class BasicTestController { + + /** + * 基本测试接口 + */ + @GetMapping + public Map test() { + Map result = new HashMap<>(); + result.put("code", 200); + result.put("message", "基本测试成功"); + result.put("data", "Hello, Basic Test!"); + result.put("time", System.currentTimeMillis()); + return result; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/CommentController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/CommentController.java new file mode 100644 index 0000000..6a986cc --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/CommentController.java @@ -0,0 +1,97 @@ +package com.test.musichouduan.controller; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.stp.StpUtil; +import com.test.musichouduan.dto.CommentDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.service.CommentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 评论控制器 + */ +@RestController +@RequestMapping("/api/comment") +public class CommentController { + + @Autowired + private CommentService commentService; + + /** + * 根据ID获取评论 + * + * @param id 评论ID + * @return 响应结果 + */ + @GetMapping("/{id}") + public Result getCommentById(@PathVariable Long id) { + CommentDTO commentDTO = commentService.getCommentById(id); + return Result.success(commentDTO); + } + + /** + * 分页获取评论列表 + * + * @param targetId 目标ID + * @param targetType 目标类型 + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @GetMapping("/list") + public Result> getCommentList( + @RequestParam Long targetId, + @RequestParam Integer targetType, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = commentService.getCommentList(targetId, targetType, page, size); + return Result.success(pageResult); + } + + /** + * 添加评论 + * + * @param commentDTO 评论DTO + * @return 响应结果 + */ + @SaCheckLogin + @PostMapping("/add") + public Result addComment(@RequestBody CommentDTO commentDTO) { + CommentDTO result = commentService.addComment(commentDTO); + return Result.success("评论成功", result); + } + + /** + * 删除评论 + * + * @param id 评论ID + * @return 响应结果 + */ + @SaCheckLogin + @DeleteMapping("/{id}") + public Result deleteComment(@PathVariable Long id) { + boolean result = commentService.deleteComment(id); + return Result.success("删除成功", result); + } + + /** + * 获取用户的评论列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @SaCheckLogin + @GetMapping("/user") + public Result> getUserCommentList( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + // 获取当前登录用户ID + Long userId = StpUtil.getLoginIdAsLong(); + PageResult pageResult = commentService.getUserCommentList(userId, page, size); + return Result.success(pageResult); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/FallbackController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/FallbackController.java new file mode 100644 index 0000000..23e2024 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/FallbackController.java @@ -0,0 +1,36 @@ +package com.test.musichouduan.controller; + +import com.test.musichouduan.dto.Result; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 回退控制器 + * 处理所有未匹配的请求 + * 使用Order注解指定优先级,数值越大优先级越低 + */ +@RestController +@Order(Integer.MAX_VALUE) +public class FallbackController { + + /** + * 处理所有非API请求 + * + * @param request HTTP请求 + * @param response HTTP响应 + * @return 响应结果 + */ + @RequestMapping({"/1", "/2", "/3", "/4", "/5", "/6", "/7", "/8", "/9"}) + public Result handleFallback(HttpServletRequest request, HttpServletResponse response) { + // 设置HTTP状态码为404 + response.setStatus(HttpStatus.NOT_FOUND.value()); + + // 返回错误信息 + return Result.error(HttpStatus.NOT_FOUND.value(), "资源不存在: " + request.getRequestURI()); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/FileController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/FileController.java new file mode 100644 index 0000000..920e1f8 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/FileController.java @@ -0,0 +1,71 @@ +package com.test.musichouduan.controller; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.service.FileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +/** + * 文件控制器 + */ +@RestController +@RequestMapping("/api/file") +public class FileController { + + @Autowired + private FileService fileService; + + /** + * 上传图片 + * + * @param file 图片文件 + * @return 响应结果 + */ + @SaCheckLogin + @PostMapping("/upload/image") + public Result uploadImage(@RequestParam("file") MultipartFile file) { + String url = fileService.uploadImage(file); + return Result.success("上传成功", url); + } + + /** + * 上传音乐 + * + * @param file 音乐文件 + * @return 响应结果 + */ + @SaCheckLogin + @PostMapping("/upload/music") + public Result uploadMusic(@RequestParam("file") MultipartFile file) { + String url = fileService.uploadMusic(file); + return Result.success("上传成功", url); + } + + /** + * 上传歌词 + * + * @param file 歌词文件 + * @return 响应结果 + */ + @SaCheckLogin + @PostMapping("/upload/lyric") + public Result uploadLyric(@RequestParam("file") MultipartFile file) { + String lyric = fileService.uploadLyric(file); + return Result.success("上传成功", lyric); + } + + /** + * 删除文件 + * + * @param url 文件URL + * @return 响应结果 + */ + @SaCheckLogin + @DeleteMapping("/delete") + public Result deleteFile(@RequestParam String url) { + boolean result = fileService.deleteFile(url); + return Result.success("删除成功", result); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/HelloController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/HelloController.java new file mode 100644 index 0000000..ed69835 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/HelloController.java @@ -0,0 +1,42 @@ +package com.test.musichouduan.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * 简单的Hello控制器 + */ +@RestController +@RequestMapping("/api") +public class HelloController { + + /** + * 欢迎接口 + */ + @GetMapping("/hello") + public Map hello() { + Map result = new HashMap<>(); + result.put("code", 200); + result.put("message", "Hello, Music API!"); + result.put("data", "欢迎使用音乐应用API"); + result.put("timestamp", System.currentTimeMillis()); + return result; + } + + /** + * 测试热部署接口 + */ + @GetMapping("/test") + public Map test() { + Map result = new HashMap<>(); + result.put("code", 200); + result.put("message", "测试成功"); + result.put("data", "这是一个测试接口,可以修改此处来测试热部署"); + result.put("timestamp", System.currentTimeMillis()); + return result; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/MusicController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/MusicController.java new file mode 100644 index 0000000..3dad514 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/MusicController.java @@ -0,0 +1,216 @@ +package com.test.musichouduan.controller; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.stp.StpUtil; +import com.test.musichouduan.dto.MusicDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.service.MusicService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 音乐控制器 + */ +@RestController +@RequestMapping("/api/music") +public class MusicController { + + @Autowired + private MusicService musicService; + + /** + * 根据ID获取音乐 + * + * @param id 音乐ID + * @return 响应结果 + */ + @GetMapping("/{id}") + public Result getMusicById(@PathVariable Long id) { + MusicDTO musicDTO = musicService.getMusicById(id); + if (musicDTO == null) { + // 如果音乐不存在,返回空数据而不是错误 + return Result.success(null); + } + return Result.success(musicDTO); + } + + /** + * 分页获取音乐列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @param typeId 类型ID + * @return 响应结果 + */ + @GetMapping("/list") + public Result> getMusicList( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Long typeId) { + PageResult pageResult = musicService.getMusicList(page, size, keyword, typeId); + return Result.success(pageResult); + } + + /** + * 根据歌手ID获取音乐列表 + * + * @param singerId 歌手ID + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @GetMapping("/singer/{singerId}") + public Result> getMusicListBySinger( + @PathVariable Long singerId, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = musicService.getMusicListBySinger(singerId, page, size); + return Result.success(pageResult); + } + + /** + * 获取最新音乐列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + @GetMapping("/latest") + public Result> getLatestMusic(@RequestParam(defaultValue = "10") Integer limit) { + List musicList = musicService.getLatestMusic(limit); + return Result.success(musicList); + } + + /** + * 获取热门音乐列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + @GetMapping("/hot") + public Result> getHotMusic(@RequestParam(defaultValue = "10") Integer limit) { + List musicList = musicService.getHotMusic(limit); + return Result.success(musicList); + } + + /** + * 搜索音乐 + * + * @param keyword 关键字 + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @GetMapping("/search") + public Result> searchMusic( + @RequestParam String keyword, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = musicService.getMusicList(page, size, keyword, null); + return Result.success(pageResult); + } + + /** + * 添加音乐 + * + * @param musicDTO 音乐DTO + * @param file 音乐文件 + * @param cover 封面文件 + * @return 响应结果 + */ + @SaCheckRole("admin") + @PostMapping("/add") + public Result addMusic( + @RequestPart("music") MusicDTO musicDTO, + @RequestPart(value = "file", required = false) MultipartFile file, + @RequestPart(value = "cover", required = false) MultipartFile cover) { + MusicDTO result = musicService.addMusic(musicDTO, file, cover); + return Result.success("添加成功", result); + } + + /** + * 更新音乐 + * + * @param id 音乐ID + * @param musicDTO 音乐DTO + * @param file 音乐文件 + * @param cover 封面文件 + * @return 响应结果 + */ + @SaCheckRole("admin") + @PutMapping("/{id}") + public Result updateMusic( + @PathVariable Long id, + @RequestPart("music") MusicDTO musicDTO, + @RequestPart(value = "file", required = false) MultipartFile file, + @RequestPart(value = "cover", required = false) MultipartFile cover, + @RequestPart(value = "lyric", required = false) MultipartFile lyric) { + MusicDTO result = musicService.updateMusic(id, musicDTO, file, cover, lyric); + return Result.success("更新成功", result); + } + + /** + * 删除音乐 + * + * @param id 音乐ID + * @return 响应结果 + */ + @SaCheckRole("admin") + @DeleteMapping("/{id}") + public Result deleteMusic(@PathVariable Long id) { + boolean result = musicService.deleteMusic(id); + return Result.success("删除成功", result); + } + + /** + * 收藏/取消收藏音乐 + * + * @param id 音乐ID + * @return 响应结果 + */ + @SaCheckLogin + @PostMapping("/{id}/collect") + public Result toggleCollectMusic(@PathVariable Long id) { + // 获取当前登录用户ID + Long userId = StpUtil.getLoginIdAsLong(); + boolean isCollected = musicService.toggleCollectMusic(id, userId); + String message = isCollected ? "收藏成功" : "取消收藏成功"; + return Result.success(message, isCollected); + } + + /** + * 获取用户收藏的音乐列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @SaCheckLogin + @GetMapping("/collect") + public Result> getCollectedMusic( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + // 获取当前登录用户ID + Long userId = StpUtil.getLoginIdAsLong(); + PageResult pageResult = musicService.getCollectedMusic(userId, page, size); + return Result.success(pageResult); + } + + /** + * 播放音乐 + * + * @param id 音乐ID + * @return 响应结果 + */ + @PostMapping("/{id}/play") + public Result playMusic(@PathVariable Long id) { + MusicDTO musicDTO = musicService.playMusic(id); + return Result.success(musicDTO); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/PlaylistController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/PlaylistController.java new file mode 100644 index 0000000..b80a6ed --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/PlaylistController.java @@ -0,0 +1,256 @@ +package com.test.musichouduan.controller; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaCheckRole; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.PlaylistDTO; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.service.PlaylistService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 歌单控制器 + */ +@RestController +@RequestMapping("/api/playlist") +public class PlaylistController { + + @Autowired + private PlaylistService playlistService; + + /** + * 根据ID获取歌单 + * + * @param id 歌单ID + * @return 响应结果 + */ + @GetMapping("/{id}") + public Result getPlaylistById(@PathVariable Long id) { + PlaylistDTO playlistDTO = playlistService.getPlaylistById(id); + return Result.success(playlistDTO); + } + + /** + * 分页获取歌单列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @param typeId 类型ID + * @return 响应结果 + */ + @GetMapping("/list") + public Result> getPlaylistList( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Long typeId) { + PageResult pageResult = playlistService.getPlaylistList(page, size, keyword, typeId); + return Result.success(pageResult); + } + + /** + * 获取最新歌单列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + @GetMapping("/latest") + public Result> getLatestPlaylist(@RequestParam(defaultValue = "10") Integer limit) { + List playlistList = playlistService.getLatestPlaylist(limit); + return Result.success(playlistList); + } + + /** + * 获取热门歌单列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + @GetMapping("/hot") + public Result> getHotPlaylist(@RequestParam(defaultValue = "10") Integer limit) { + List playlistList = playlistService.getHotPlaylist(limit); + return Result.success(playlistList); + } + + /** + * 搜索歌单 + * + * @param keyword 关键字 + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @GetMapping("/search") + public Result> searchPlaylist( + @RequestParam String keyword, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = playlistService.getPlaylistList(page, size, keyword, null); + return Result.success(pageResult); + } + + /** + * 获取官方歌单列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @GetMapping("/official") + public Result> getOfficialPlaylist( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = playlistService.getOfficialPlaylist(page, size); + return Result.success(pageResult); + } + + /** + * 添加歌单 + * + * @param playlistDTO 歌单DTO + * @param cover 封面文件 + * @return 响应结果 + */ + @SaCheckLogin + @PostMapping("/add") + public Result addPlaylist( + @RequestPart("playlist") PlaylistDTO playlistDTO, + @RequestPart(value = "cover", required = false) MultipartFile cover) { + PlaylistDTO result = playlistService.addPlaylist(playlistDTO, cover); + return Result.success("添加成功", result); + } + + /** + * 更新歌单 + * + * @param id 歌单ID + * @param playlistDTO 歌单DTO + * @param cover 封面文件 + * @return 响应结果 + */ + @SaCheckLogin + @PutMapping("/{id}") + public Result updatePlaylist( + @PathVariable Long id, + @RequestPart("playlist") PlaylistDTO playlistDTO, + @RequestPart(value = "cover", required = false) MultipartFile cover) { + PlaylistDTO result = playlistService.updatePlaylist(id, playlistDTO, cover); + return Result.success("更新成功", result); + } + + /** + * 删除歌单 + * + * @param id 歌单ID + * @return 响应结果 + */ + @SaCheckLogin + @DeleteMapping("/{id}") + public Result deletePlaylist(@PathVariable Long id) { + boolean result = playlistService.deletePlaylist(id); + return Result.success("删除成功", result); + } + + /** + * 收藏歌单 + * + * @param id 歌单ID + * @return 响应结果 + */ + @SaCheckLogin + @PostMapping("/{id}/collect") + public Result collectPlaylist(@PathVariable Long id) { + boolean result = playlistService.collectPlaylist(id); + return Result.success("收藏成功", result); + } + + /** + * 取消收藏歌单 + * + * @param id 歌单ID + * @return 响应结果 + */ + @SaCheckLogin + @DeleteMapping("/{id}/collect") + public Result uncollectPlaylist(@PathVariable Long id) { + boolean result = playlistService.uncollectPlaylist(id); + return Result.success("取消收藏成功", result); + } + + /** + * 获取用户收藏的歌单列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @SaCheckLogin + @GetMapping("/collect") + public Result> getCollectedPlaylist( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = playlistService.getCollectedPlaylist(page, size); + return Result.success(pageResult); + } + + /** + * 获取用户创建的歌单列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @SaCheckLogin + @GetMapping("/user") + public Result> getUserPlaylist( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = playlistService.getUserPlaylist(page, size); + return Result.success(pageResult); + } + + /** + * 向歌单添加音乐 + * + * @param id 歌单ID + * @param musicId 音乐ID + * @return 响应结果 + */ + @SaCheckLogin + @PostMapping("/{id}/music/{musicId}") + public Result addMusicToPlaylist(@PathVariable Long id, @PathVariable Long musicId) { + boolean result = playlistService.addMusicToPlaylist(id, musicId); + return Result.success("添加成功", result); + } + + /** + * 从歌单移除音乐 + * + * @param id 歌单ID + * @param musicId 音乐ID + * @return 响应结果 + */ + @SaCheckLogin + @DeleteMapping("/{id}/music/{musicId}") + public Result removeMusicFromPlaylist(@PathVariable Long id, @PathVariable Long musicId) { + boolean result = playlistService.removeMusicFromPlaylist(id, musicId); + return Result.success("移除成功", result); + } + + /** + * 播放歌单 + * + * @param id 歌单ID + * @return 响应结果 + */ + @PostMapping("/{id}/play") + public Result playPlaylist(@PathVariable Long id) { + PlaylistDTO playlistDTO = playlistService.playPlaylist(id); + return Result.success(playlistDTO); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/SingerController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/SingerController.java new file mode 100644 index 0000000..fd09ece --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/SingerController.java @@ -0,0 +1,215 @@ +package com.test.musichouduan.controller; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaCheckRole; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.dto.SingerDTO; +import com.test.musichouduan.service.SingerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 歌手控制器 + */ +@RestController +@RequestMapping("/api/singer") +public class SingerController { + + @Autowired + private SingerService singerService; + + /** + * 根据ID获取歌手 + * + * @param id 歌手ID + * @return 响应结果 + */ + @GetMapping("/{id}") + public Result getSingerById(@PathVariable Long id) { + SingerDTO singerDTO = singerService.getSingerById(id); + return Result.success(singerDTO); + } + + /** + * 分页获取歌手列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @param typeId 类型ID + * @return 响应结果 + */ + @GetMapping("/list") + public Result> getSingerList( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) Long typeId) { + PageResult pageResult = singerService.getSingerList(page, size, keyword, typeId); + return Result.success(pageResult); + } + + /** + * 搜索歌手 + * + * @param keyword 关键字 + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @GetMapping("/search") + public Result> searchSinger( + @RequestParam String keyword, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = singerService.getSingerList(page, size, keyword, null); + return Result.success(pageResult); + } + + /** + * 获取最新歌手列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + @GetMapping("/latest") + public Result> getLatestSinger(@RequestParam(defaultValue = "10") Integer limit) { + List singerList = singerService.getLatestSinger(limit); + return Result.success(singerList); + } + + /** + * 获取热门歌手列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + @GetMapping("/hot-list") + public Result> getHotSinger(@RequestParam(defaultValue = "10") Integer limit) { + List singerList = singerService.getHotSinger(limit); + return Result.success(singerList); + } + + /** + * 添加歌手 + * + * @param singerDTO 歌手DTO + * @param avatar 头像文件 + * @return 响应结果 + */ + @SaCheckRole("admin") + @PostMapping("/add") + public Result addSinger( + @RequestPart("singer") SingerDTO singerDTO, + @RequestPart(value = "avatar", required = false) MultipartFile avatar) { + SingerDTO result = singerService.addSinger(singerDTO, avatar); + return Result.success("添加成功", result); + } + + /** + * 更新歌手 + * + * @param id 歌手ID + * @param singerDTO 歌手DTO + * @param avatar 头像文件 + * @return 响应结果 + */ + @SaCheckRole("admin") + @PutMapping("/{id}") + public Result updateSinger( + @PathVariable Long id, + @RequestPart("singer") SingerDTO singerDTO, + @RequestPart(value = "avatar", required = false) MultipartFile avatar) { + SingerDTO result = singerService.updateSinger(id, singerDTO, avatar); + return Result.success("更新成功", result); + } + + /** + * 删除歌手 + * + * @param id 歌手ID + * @return 响应结果 + */ + @SaCheckRole("admin") + @DeleteMapping("/{id}") + public Result deleteSinger(@PathVariable Long id) { + boolean result = singerService.deleteSinger(id); + return Result.success("删除成功", result); + } + + /** + * 收藏歌手 + * + * @param id 歌手ID + * @return 响应结果 + */ + @SaCheckLogin + @PostMapping("/{id}/collect") + public Result collectSinger(@PathVariable Long id) { + boolean result = singerService.collectSinger(id); + return Result.success("收藏成功", result); + } + + /** + * 取消收藏歌手 + * + * @param id 歌手ID + * @return 响应结果 + */ + @SaCheckLogin + @DeleteMapping("/{id}/collect") + public Result uncollectSinger(@PathVariable Long id) { + boolean result = singerService.uncollectSinger(id); + return Result.success("取消收藏成功", result); + } + + /** + * 获取用户收藏的歌手列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 响应结果 + */ + @SaCheckLogin + @GetMapping("/collect") + public Result> getCollectedSinger( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + PageResult pageResult = singerService.getCollectedSinger(page, size); + return Result.success(pageResult); + } + + /** + * 添加音乐到歌手 + * @param singerId 歌手ID + * @param musicId 音乐ID + * @return 响应结果 + */ + @SaCheckRole("admin") + @PostMapping("/{singerId}/music/{musicId}") + public Result addMusicToSinger( + @PathVariable Long singerId, + @PathVariable Long musicId) { + boolean result = singerService.addMusicToSinger(singerId, musicId); + return Result.success("添加成功", result); + } + + /** + * 从歌手移除音乐 + * @param singerId 歌手ID + * @param musicId 音乐ID + * @return 响应结果 + */ + @SaCheckRole("admin") + @DeleteMapping("/{singerId}/music/{musicId}") + public Result removeMusicFromSinger( + @PathVariable Long singerId, + @PathVariable Long musicId) { + boolean result = singerService.removeMusicFromSinger(singerId, musicId); + return Result.success("移除成功", result); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/StatController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/StatController.java new file mode 100644 index 0000000..cf0842e --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/StatController.java @@ -0,0 +1,94 @@ +package com.test.musichouduan.controller; + +import cn.dev33.satoken.annotation.SaCheckRole; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.service.StatService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * 统计控制器 + */ +@RestController +@RequestMapping("/api/stat") +@SaCheckRole("admin") +public class StatController { + + @Autowired + private StatService statService; + + /** + * 获取总体统计数据 + * + * @return 响应结果 + */ + @GetMapping("/total") + public Result> getTotalStats() { + Map stats = statService.getTotalStats(); + return Result.success(stats); + } + + /** + * 获取用户增长趋势 + * + * @param days 天数 + * @return 响应结果 + */ + @GetMapping("/user/growth") + public Result> getUserGrowthTrend(@RequestParam(defaultValue = "7") Integer days) { + Map stats = statService.getUserGrowthTrend(days); + return Result.success(stats); + } + + /** + * 获取音乐类型分布 + * + * @return 响应结果 + */ + @GetMapping("/music/type") + public Result> getMusicTypeDistribution() { + Map stats = statService.getMusicTypeDistribution(); + return Result.success(stats); + } + + /** + * 获取播放量统计 + * + * @param days 天数 + * @return 响应结果 + */ + @GetMapping("/play/count") + public Result> getPlayCountStats(@RequestParam(defaultValue = "7") Integer days) { + Map stats = statService.getPlayCountStats(days); + return Result.success(stats); + } + + /** + * 获取最新用户列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + @GetMapping("/user/latest") + public Result> getLatestUsers(@RequestParam(defaultValue = "5") Integer limit) { + Map stats = statService.getLatestUsers(limit); + return Result.success(stats); + } + + /** + * 获取最新音乐列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + @GetMapping("/music/latest") + public Result> getLatestMusics(@RequestParam(defaultValue = "5") Integer limit) { + Map stats = statService.getLatestMusics(limit); + return Result.success(stats); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/TypeController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/TypeController.java new file mode 100644 index 0000000..5d8d65e --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/TypeController.java @@ -0,0 +1,227 @@ +package com.test.musichouduan.controller; + +import cn.dev33.satoken.annotation.SaCheckRole; +import com.test.musichouduan.dto.*; +import com.test.musichouduan.service.TypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 类型控制器 + */ +@RestController +@RequestMapping("/api/type") +public class TypeController { + + @Autowired + private TypeService typeService; + + /** + * 获取音乐类型列表 + * + * @return 响应结果 + */ + @GetMapping("/music") + public Result> getMusicTypeList() { + List typeList = typeService.getMusicTypeList(); + return Result.success(typeList); + } + + /** + * 分页获取音乐类型列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @return 响应结果 + */ + @SaCheckRole("admin") + @GetMapping("/music/page") + public Result> getMusicTypePage( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword) { + PageResult pageResult = typeService.getMusicTypePage(page, size, keyword); + return Result.success(pageResult); + } + + /** + * 添加音乐类型 + * + * @param musicTypeDTO 音乐类型DTO + * @return 响应结果 + */ + @SaCheckRole("admin") + @PostMapping("/music/add") + public Result addMusicType(@RequestBody MusicTypeDTO musicTypeDTO) { + MusicTypeDTO result = typeService.addMusicType(musicTypeDTO); + return Result.success("添加成功", result); + } + + /** + * 更新音乐类型 + * + * @param id 音乐类型ID + * @param musicTypeDTO 音乐类型DTO + * @return 响应结果 + */ + @SaCheckRole("admin") + @PutMapping("/music/{id}") + public Result updateMusicType(@PathVariable Long id, @RequestBody MusicTypeDTO musicTypeDTO) { + MusicTypeDTO result = typeService.updateMusicType(id, musicTypeDTO); + return Result.success("更新成功", result); + } + + /** + * 删除音乐类型 + * + * @param id 音乐类型ID + * @return 响应结果 + */ + @SaCheckRole("admin") + @DeleteMapping("/music/{id}") + public Result deleteMusicType(@PathVariable Long id) { + boolean result = typeService.deleteMusicType(id); + return Result.success("删除成功", result); + } + + /** + * 获取歌手类型列表 + * + * @return 响应结果 + */ + @GetMapping("/singer") + public Result> getSingerTypeList() { + List typeList = typeService.getSingerTypeList(); + return Result.success(typeList); + } + + /** + * 分页获取歌手类型列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @return 响应结果 + */ + @SaCheckRole("admin") + @GetMapping("/singer/page") + public Result> getSingerTypePage( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword) { + PageResult pageResult = typeService.getSingerTypePage(page, size, keyword); + return Result.success(pageResult); + } + + /** + * 添加歌手类型 + * + * @param singerTypeDTO 歌手类型DTO + * @return 响应结果 + */ + @SaCheckRole("admin") + @PostMapping("/singer/add") + public Result addSingerType(@RequestBody SingerTypeDTO singerTypeDTO) { + SingerTypeDTO result = typeService.addSingerType(singerTypeDTO); + return Result.success("添加成功", result); + } + + /** + * 更新歌手类型 + * + * @param id 歌手类型ID + * @param singerTypeDTO 歌手类型DTO + * @return 响应结果 + */ + @SaCheckRole("admin") + @PutMapping("/singer/{id}") + public Result updateSingerType(@PathVariable Long id, @RequestBody SingerTypeDTO singerTypeDTO) { + SingerTypeDTO result = typeService.updateSingerType(id, singerTypeDTO); + return Result.success("更新成功", result); + } + + /** + * 删除歌手类型 + * + * @param id 歌手类型ID + * @return 响应结果 + */ + @SaCheckRole("admin") + @DeleteMapping("/singer/{id}") + public Result deleteSingerType(@PathVariable Long id) { + boolean result = typeService.deleteSingerType(id); + return Result.success("删除成功", result); + } + + /** + * 获取歌单类型列表 + * + * @return 响应结果 + */ + @GetMapping("/playlist") + public Result> getPlaylistTypeList() { + List typeList = typeService.getPlaylistTypeList(); + return Result.success(typeList); + } + + /** + * 分页获取歌单类型列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @return 响应结果 + */ + @SaCheckRole("admin") + @GetMapping("/playlist/page") + public Result> getPlaylistTypePage( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size, + @RequestParam(required = false) String keyword) { + PageResult pageResult = typeService.getPlaylistTypePage(page, size, keyword); + return Result.success(pageResult); + } + + /** + * 添加歌单类型 + * + * @param playlistTypeDTO 歌单类型DTO + * @return 响应结果 + */ + @SaCheckRole("admin") + @PostMapping("/playlist/add") + public Result addPlaylistType(@RequestBody PlaylistTypeDTO playlistTypeDTO) { + PlaylistTypeDTO result = typeService.addPlaylistType(playlistTypeDTO); + return Result.success("添加成功", result); + } + + /** + * 更新歌单类型 + * + * @param id 歌单类型ID + * @param playlistTypeDTO 歌单类型DTO + * @return 响应结果 + */ + @SaCheckRole("admin") + @PutMapping("/playlist/{id}") + public Result updatePlaylistType(@PathVariable Long id, @RequestBody PlaylistTypeDTO playlistTypeDTO) { + PlaylistTypeDTO result = typeService.updatePlaylistType(id, playlistTypeDTO); + return Result.success("更新成功", result); + } + + /** + * 删除歌单类型 + * + * @param id 歌单类型ID + * @return 响应结果 + */ + @SaCheckRole("admin") + @DeleteMapping("/playlist/{id}") + public Result deletePlaylistType(@PathVariable Long id) { + boolean result = typeService.deletePlaylistType(id); + return Result.success("删除成功", result); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/UserController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/UserController.java new file mode 100644 index 0000000..2872ec8 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/UserController.java @@ -0,0 +1,227 @@ +package com.test.musichouduan.controller; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.stp.StpUtil; +import com.test.musichouduan.dto.*; +import com.test.musichouduan.service.FileService; +import com.test.musichouduan.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +/** + * 用户控制器 + */ +@Tag(name = "用户接口", description = "用户的注册、登录、信息管理等操作") +@RestController +@RequestMapping("/api/user") +public class UserController { + + @Autowired + private UserService userService; + + @Autowired + private FileService fileService; + + /** + * 用户注册 + * + * @param request 注册请求 + * @return 响应结果 + */ + @Operation(summary = "用户注册", description = "新用户注册接口") + @PostMapping("/register") + public Result register(@Parameter(description = "注册信息") @Validated @RequestBody RegisterRequest request) { + UserDTO userDTO = userService.register(request); + return Result.success("注册成功", userDTO); + } + + /** + * 用户登录 + * + * @param request 登录请求 + * @return 响应结果 + */ + @Operation(summary = "用户登录", description = "用户登录接口,登录成功后返回用户信息和令牌") + @PostMapping("/login") + public Result login(@Parameter(description = "登录信息") @Validated @RequestBody LoginRequest request) { + LoginResultDTO resultDTO = userService.login(request); + return Result.success("登录成功", resultDTO); + } + + /** + * 用户登出 + * + * @return 响应结果 + */ + @Operation(summary = "用户登出", description = "用户登出接口,需要登录状态") + @SaCheckLogin + @PostMapping("/logout") + public Result logout() { + StpUtil.logout(); + return Result.success("登出成功", null); + } + + /** + * 获取当前登录用户 + * + * @return 响应结果 + */ + @Operation(summary = "获取当前登录用户", description = "获取当前登录用户的信息,需要登录状态") + @SaCheckLogin + @GetMapping("/current") + public Result getCurrentUser() { + UserDTO userDTO = userService.getCurrentUser(); + return Result.success(userDTO); + } + + /** + * 更新用户信息 + * + * @param request 用户信息更新请求 + * @return 响应结果 + */ + @Operation(summary = "更新用户信息", description = "更新当前登录用户的信息,需要登录状态") + @SaCheckLogin + @PutMapping("/update") + public Result updateUser(@Parameter(description = "用户信息更新请求") @Validated @RequestBody UserUpdateRequest request) { + UserDTO userDTO = userService.updateUser(request); + return Result.success("更新成功", userDTO); + } + + /** + * 修改密码 + * + * @param request 修改密码请求 + * @return 响应结果 + */ + @Operation(summary = "修改密码", description = "修改当前登录用户的密码,需要登录状态") + @SaCheckLogin + @PutMapping("/password") + public Result updatePassword(@Parameter(description = "密码修改请求") @Validated @RequestBody PasswordUpdateRequest request) { + boolean result = userService.updatePassword(request); + return Result.success("修改成功", result); + } + + /** + * 获取用户统计数据 + * + * @return 响应结果 + */ + @Operation(summary = "获取用户统计数据", description = "获取当前登录用户的统计数据,包括收藏数量等,需要登录状态") + @SaCheckLogin + @GetMapping("/stats") + public Result getUserStats() { + UserDTO userDTO = userService.getUserStats(StpUtil.getLoginIdAsLong()); + return Result.success(userDTO); + } + + /** + * 分页获取用户列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @return 响应结果 + */ + @Operation(summary = "分页获取用户列表", description = "分页获取用户列表,支持关键字搜索,需要管理员权限") + @SaCheckRole("admin") + @GetMapping("/list") + public Result> getUserList( + @Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") Integer page, + @Parameter(description = "每页大小", example = "10") @RequestParam(defaultValue = "10") Integer size, + @Parameter(description = "搜索关键字", example = "用户名") @RequestParam(required = false) String keyword) { + PageResult pageResult = userService.getUserList(page, size, keyword); + return Result.success(pageResult); + } + + /** + * 根据ID获取用户 + * + * @param id 用户ID + * @return 响应结果 + */ + @Operation(summary = "根据ID获取用户", description = "根据用户ID获取用户信息,需要管理员权限") + @SaCheckRole("admin") + @GetMapping("/{id}") + public Result getUserById(@Parameter(description = "用户ID", example = "1") @PathVariable Long id) { + UserDTO userDTO = userService.getUserById(id); + return Result.success(userDTO); + } + + /** + * 删除用户 + * + * @param id 用户ID + * @return 响应结果 + */ + @Operation(summary = "删除用户", description = "根据用户ID删除用户,需要管理员权限") + @SaCheckRole("admin") + @DeleteMapping("/{id}") + public Result deleteUser(@Parameter(description = "用户ID", example = "1") @PathVariable Long id) { + boolean result = userService.deleteUser(id); + return Result.success("删除成功", result); + } + + /** + * 更新用户状态 + * + * @param id 用户ID + * @param status 状态 + * @return 响应结果 + */ + @Operation(summary = "更新用户状态", description = "更新用户状态,0-禁用,1-启用,需要管理员权限") + @SaCheckRole("admin") + @PutMapping("/{id}/status") + public Result updateUserStatus( + @Parameter(description = "用户ID", example = "1") @PathVariable Long id, + @Parameter(description = "状态,0-禁用,1-启用", example = "1") @RequestParam Integer status) { + boolean result = userService.updateUserStatus(id, status); + return Result.success("更新成功", result); + } + + /** + * 管理员更新用户信息 + * + * @param id 用户ID + * @param request 用户信息更新请求 + * @return 响应结果 + */ + @Operation(summary = "管理员更新用户信息", description = "管理员更新用户信息,需要管理员权限") + @SaCheckRole("admin") + @PutMapping("/{id}") + public Result adminUpdateUser( + @Parameter(description = "用户ID", example = "1") @PathVariable Long id, + @Parameter(description = "用户信息更新请求") @Validated @RequestBody UserUpdateRequest request) { + UserDTO userDTO = userService.adminUpdateUser(id, request); + return Result.success("更新成功", userDTO); + } + + /** + * 上传用户头像 + * + * @param file 头像文件 + * @return 响应结果 + */ + @Operation(summary = "上传用户头像", description = "上传当前登录用户的头像,需要登录状态") + @SaCheckLogin + @PostMapping("/upload/avatar") + public Result uploadAvatar(@Parameter(description = "头像文件") @RequestParam("file") MultipartFile file) { + // 上传头像 + String avatarUrl = fileService.uploadImage(file); + + // 用户ID已经通过 SaCheckLogin 注解验证 + + // 更新用户头像 + UserUpdateRequest request = new UserUpdateRequest(); + request.setAvatar(avatarUrl); + userService.updateUser(request); + + return Result.success("上传成功", avatarUrl); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/admin/AdminCommentController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/admin/AdminCommentController.java new file mode 100644 index 0000000..3d58e44 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/admin/AdminCommentController.java @@ -0,0 +1,76 @@ +package com.test.musichouduan.controller.admin; + +import cn.dev33.satoken.annotation.SaCheckRole; +import com.test.musichouduan.dto.CommentDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.service.CommentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 管理员评论控制器 + */ +@Tag(name = "管理员评论接口", description = "提供管理员评论管理功能") +@RestController +@RequestMapping("/api/admin/comment") +@SaCheckRole("admin") +public class AdminCommentController { + + @Autowired + private CommentService commentService; + + /** + * 分页获取所有评论 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @param targetType 目标类型 + * @param status 状态 + * @return 响应结果 + */ + @Operation(summary = "分页获取所有评论", description = "管理员分页获取所有评论") + @GetMapping("/list") + public Result> getCommentList( + @Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") Integer page, + @Parameter(description = "每页大小", example = "10") @RequestParam(defaultValue = "10") Integer size, + @Parameter(description = "关键字") @RequestParam(required = false) String keyword, + @Parameter(description = "目标类型:1-音乐,2-歌单,3-歌手") @RequestParam(required = false) Integer targetType, + @Parameter(description = "状态:0-禁用,1-正常") @RequestParam(required = false) Integer status) { + PageResult pageResult = commentService.getAdminCommentList(page, size, keyword, targetType, status); + return Result.success(pageResult); + } + + /** + * 更新评论状态 + * + * @param id 评论ID + * @param status 状态:0-禁用,1-正常 + * @return 响应结果 + */ + @Operation(summary = "更新评论状态", description = "管理员更新评论状态") + @PutMapping("/{id}/status") + public Result updateCommentStatus( + @Parameter(description = "评论ID") @PathVariable Long id, + @Parameter(description = "状态:0-禁用,1-正常", example = "1") @RequestParam Integer status) { + boolean result = commentService.updateCommentStatus(id, status); + return Result.success("更新成功", result); + } + + /** + * 删除评论 + * + * @param id 评论ID + * @return 响应结果 + */ + @Operation(summary = "删除评论", description = "管理员删除评论") + @DeleteMapping("/{id}") + public Result deleteComment(@Parameter(description = "评论ID") @PathVariable Long id) { + boolean result = commentService.adminDeleteComment(id); + return Result.success("删除成功", result); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/admin/AdminDashboardController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/admin/AdminDashboardController.java new file mode 100644 index 0000000..48ddc25 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/admin/AdminDashboardController.java @@ -0,0 +1,240 @@ +package com.test.musichouduan.controller.admin; + +import cn.dev33.satoken.annotation.SaCheckRole; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.dto.admin.DashboardDataDTO; +import com.test.musichouduan.dto.admin.MusicAdminDTO; +import com.test.musichouduan.dto.admin.StatItemDTO; +import com.test.musichouduan.dto.admin.UserAdminDTO; +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.entity.PlaylistType; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.repository.MusicRepository; +import com.test.musichouduan.repository.PlayHistoryRepository; +import com.test.musichouduan.repository.PlaylistRepository; +import com.test.musichouduan.repository.SingerRepository; +import com.test.musichouduan.repository.UserRepository; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 后台管理控制台控制器 + */ +@Tag(name = "后台管理控制台接口", description = "提供后台管理控制台所需的统计数据") +@RestController +@RequestMapping("/api/admin/dashboard") +@SaCheckRole("admin") +public class AdminDashboardController { + + @Autowired + private UserRepository userRepository; + + @Autowired + private MusicRepository musicRepository; + + @Autowired + private PlaylistRepository playlistRepository; + + @Autowired + private SingerRepository singerRepository; + + @Autowired + private PlayHistoryRepository playHistoryRepository; + + @PersistenceContext + private EntityManager entityManager; + + /** + * 获取控制台统计数据 + * + * @return 统计数据 + */ + @Operation(summary = "获取控制台统计数据", description = "获取用户数量、音乐数量、歌单数量、歌手数量等统计数据") + @GetMapping("/stats") + public Result getStats() { + try { + DashboardDataDTO data = new DashboardDataDTO(); + + // 统计基础数据 + long userCount = userRepository.count(); + long musicCount = musicRepository.count(); + long playlistCount = playlistRepository.count(); + long singerCount = singerRepository.count(); + long playCount = musicRepository.sumPlayCount(); + + data.setUserCount(userCount); + data.setMusicCount(musicCount); + data.setPlaylistCount(playlistCount); + data.setSingerCount(singerCount); + data.setPlayCount(playCount); + + // 获取最近7天的用户增长数据 + data.setUserGrowth(getUserGrowthData()); + + // 获取音乐类型分布数据 + data.setMusicTypeDistribution(getTypeDistribution()); + + // 获取最近7天的播放量数据 + data.setPlayCountData(getPlayCountData()); + + // 获取最新用户 + List users = userRepository.findLatestUsers(PageRequest.of(0, 5)); + List latestUsers = users.stream() + .map(UserAdminDTO::fromEntity) + .collect(Collectors.toList()); + data.setLatestUsers(latestUsers); + + // 获取最新音乐 + List musics = musicRepository.findLatest(PageRequest.of(0, 5)); + List latestMusics = musics.stream() + .map(MusicAdminDTO::fromEntity) + .collect(Collectors.toList()); + data.setLatestMusics(latestMusics); + + return Result.success(data); + } catch (Exception e) { + e.printStackTrace(); + return Result.error("获取控制台统计数据失败"); + } + } + + /** + * 获取最近7天的用户增长数据 + * + * @return 用户增长数据 + */ + private List getUserGrowthData() { + List result = new ArrayList<>(); + LocalDate today = LocalDate.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd"); + + // 获取最近7天的日期 + for (int i = 6; i >= 0; i--) { + LocalDate date = today.minusDays(i); + LocalDateTime startOfDay = date.atStartOfDay(); + LocalDateTime endOfDay = date.plusDays(1).atStartOfDay().minusNanos(1); + + // 统计当天注册的用户数量 + long count = userRepository.countByCreateTimeBetween(startOfDay, endOfDay); + + StatItemDTO item = new StatItemDTO(); + item.setName(date.format(formatter)); + item.setValue(count); + result.add(item); + } + + return result; + } + + /** + * 获取音乐、歌单、歌手类型分布数据 + * + * @return 类型分布数据 + */ + private List getTypeDistribution() { + List result = new ArrayList<>(); + + // 音乐类型分布 + StatItemDTO musicTypeStat = new StatItemDTO(); + musicTypeStat.setName("音乐类型"); + + try { + List musicTypeData = musicRepository.countByMusicType(); + List musicTypeChildren = musicTypeData.stream().map(item -> { + StatItemDTO child = new StatItemDTO(); + child.setName((String) item[0]); + child.setValue(((Number) item[1]).longValue()); + return child; + }).collect(Collectors.toList()); + musicTypeStat.setChildren(musicTypeChildren); + } catch (Exception e) { + e.printStackTrace(); + // 如果查询失败,返回空列表 + musicTypeStat.setChildren(new ArrayList<>()); + } + + result.add(musicTypeStat); + + // 歌单类型分布 + StatItemDTO playlistTypeStat = new StatItemDTO(); + playlistTypeStat.setName("歌单类型"); + + // 使用原生 SQL 查询或者手动聚合数据 + List playlistTypes = entityManager.createQuery("SELECT DISTINCT pt FROM PlaylistType pt", PlaylistType.class).getResultList(); + List playlistTypeChildren = new ArrayList<>(); + + for (PlaylistType type : playlistTypes) { + Long count = playlistRepository.countByType(type); + StatItemDTO child = new StatItemDTO(); + child.setName(type.getName()); + child.setValue(count); + playlistTypeChildren.add(child); + } + + playlistTypeStat.setChildren(playlistTypeChildren); + result.add(playlistTypeStat); + + // 歌手类型分布 + StatItemDTO singerTypeStat = new StatItemDTO(); + singerTypeStat.setName("歌手类型"); + + // 使用原生 SQL 查询或者手动聚合数据 + List singerTypes = entityManager.createQuery("SELECT DISTINCT st FROM SingerType st", com.test.musichouduan.entity.SingerType.class).getResultList(); + List singerTypeChildren = new ArrayList<>(); + + for (com.test.musichouduan.entity.SingerType type : singerTypes) { + Long count = singerRepository.countByType(type); + StatItemDTO child = new StatItemDTO(); + child.setName(type.getName()); + child.setValue(count); + singerTypeChildren.add(child); + } + + singerTypeStat.setChildren(singerTypeChildren); + result.add(singerTypeStat); + + return result; + } + + /** + * 获取最近7天的播放量数据 + * + * @return 播放量数据 + */ + private List getPlayCountData() { + List result = new ArrayList<>(); + LocalDate today = LocalDate.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd"); + + // 获取最近7天的日期 + for (int i = 6; i >= 0; i--) { + LocalDate date = today.minusDays(i); + LocalDateTime startOfDay = date.atStartOfDay(); + LocalDateTime endOfDay = date.plusDays(1).atStartOfDay().minusNanos(1); + + // 统计当天的播放量 + long count = playHistoryRepository.countByPlayTimeBetween(startOfDay, endOfDay); + + StatItemDTO item = new StatItemDTO(); + item.setName(date.format(formatter)); + item.setValue(count); + result.add(item); + } + + return result; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/BannerDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/BannerDTO.java new file mode 100644 index 0000000..07d6d2b --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/BannerDTO.java @@ -0,0 +1,57 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 轮播图数据传输对象 + */ +@Data +public class BannerDTO { + + /** + * 轮播图ID + */ + private Long id; + + /** + * 标题 + */ + private String title; + + /** + * 图片URL + */ + private String imageUrl; + + /** + * 链接URL + */ + private String linkUrl; + + /** + * 目标ID + */ + private Long targetId; + + /** + * 目标类型:1-音乐,2-歌单,3-歌手 + */ + private Integer targetType; + + /** + * 排序号 + */ + private Integer orderNum; + + /** + * 状态:0-禁用,1-正常 + */ + private Integer status; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/CommentDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/CommentDTO.java new file mode 100644 index 0000000..eab46d4 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/CommentDTO.java @@ -0,0 +1,53 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 评论数据传输对象 + */ +@Data +public class CommentDTO { + + /** + * 评论ID + */ + private Long id; + + /** + * 评论内容 + */ + private String content; + + /** + * 评论用户 + */ + private UserDTO user; + + /** + * 评论目标ID + */ + private Long targetId; + + /** + * 评论目标类型:1-音乐,2-歌单,3-歌手 + */ + private Integer targetType; + + /** + * 父评论ID + */ + private Long parentId; + + /** + * 子评论列表 + */ + private List children; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/LoginRequest.java b/music-houduan/src/main/java/com/test/musichouduan/dto/LoginRequest.java new file mode 100644 index 0000000..6959ea8 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/LoginRequest.java @@ -0,0 +1,23 @@ +package com.test.musichouduan.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 登录请求 + */ +@Data +public class LoginRequest { + + /** + * 用户名 + */ + @NotBlank(message = "用户名不能为空") + private String username; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + private String password; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/LoginResultDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/LoginResultDTO.java new file mode 100644 index 0000000..08b426a --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/LoginResultDTO.java @@ -0,0 +1,30 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +/** + * 登录结果数据传输对象 + */ +@Data +public class LoginResultDTO { + + /** + * 用户信息 + */ + private UserDTO user; + + /** + * 访问令牌 + */ + private String token; + + /** + * 令牌名称(用于请求头) + */ + private String tokenName; + + /** + * 令牌头前缀 + */ + private String tokenPrefix; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/MusicDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/MusicDTO.java new file mode 100644 index 0000000..fa29e6f --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/MusicDTO.java @@ -0,0 +1,110 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 音乐数据传输对象 + */ +@Data +public class MusicDTO { + + /** + * 音乐ID + */ + private Long id; + + /** + * 音乐名称 + */ + private String name; + + /** + * 封面URL + */ + private String cover; + + /** + * 音乐文件URL + */ + private String url; + + /** + * 歌词 + */ + private String lyric; + + /** + * 时长(秒) + */ + private Integer duration; + + /** + * 专辑 + */ + private String album; + + /** + * 播放次数 + */ + private Long playCount; + + /** + * 歌手列表 + */ + private List singers; + + /** + * 歌手ID列表 + */ + private List singerIds; + + /** + * 歌手名称列表 + */ + private List singerNames; + + /** + * 歌手名称(字符串) + */ + private String singer; + + /** + * 音乐类型列表 + */ + private List types; + + /** + * 音乐类型ID列表 + */ + private List typeIds; + + /** + * 音乐类型名称列表 + */ + private List typeNames; + + /** + * 是否已收藏 + */ + private Boolean isCollected; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 收藏时间 + */ + private LocalDateTime collectTime; + + /** + * 设置是否已收藏 + */ + public void setCollected(boolean collected) { + this.isCollected = collected; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/MusicTypeDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/MusicTypeDTO.java new file mode 100644 index 0000000..fa21384 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/MusicTypeDTO.java @@ -0,0 +1,25 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +/** + * 音乐类型数据传输对象 + */ +@Data +public class MusicTypeDTO { + + /** + * 类型ID + */ + private Long id; + + /** + * 类型名称 + */ + private String name; + + /** + * 类型描述 + */ + private String description; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/PageResult.java b/music-houduan/src/main/java/com/test/musichouduan/dto/PageResult.java new file mode 100644 index 0000000..aa39c62 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/PageResult.java @@ -0,0 +1,55 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 分页结果 + * + * @param 数据类型 + */ +@Data +public class PageResult { + + /** + * 数据列表 + */ + private List list; + + /** + * 总数 + */ + private Long total; + + /** + * 当前页 + */ + private Integer page; + + /** + * 每页大小 + */ + private Integer size; + + /** + * 总页数 + */ + private Integer totalPages; + + /** + * 构造方法 + * + * @param list 数据列表 + * @param total 总数 + * @param page 当前页 + * @param size 每页大小 + */ + public PageResult(List list, Long total, Integer page, Integer size) { + this.list = list; + this.total = total; + this.page = page; + this.size = size; + this.totalPages = (int) Math.ceil((double) total / size); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/PasswordUpdateRequest.java b/music-houduan/src/main/java/com/test/musichouduan/dto/PasswordUpdateRequest.java new file mode 100644 index 0000000..87bf76a --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/PasswordUpdateRequest.java @@ -0,0 +1,31 @@ +package com.test.musichouduan.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 修改密码请求 + */ +@Data +public class PasswordUpdateRequest { + + /** + * 旧密码 + */ + @NotBlank(message = "旧密码不能为空") + private String oldPassword; + + /** + * 新密码 + */ + @NotBlank(message = "新密码不能为空") + @Size(min = 6, max = 20, message = "密码长度应为6-20个字符") + private String newPassword; + + /** + * 确认新密码 + */ + @NotBlank(message = "确认新密码不能为空") + private String confirmPassword; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistDTO.java new file mode 100644 index 0000000..c167312 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistDTO.java @@ -0,0 +1,73 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 歌单数据传输对象 + */ +@Data +public class PlaylistDTO { + + /** + * 歌单ID + */ + private Long id; + + /** + * 歌单名称 + */ + private String name; + + /** + * 封面URL + */ + private String cover; + + /** + * 歌单描述 + */ + private String description; + + /** + * 创建者 + */ + private UserDTO creator; + + /** + * 歌单类型 + */ + private PlaylistTypeDTO type; + + /** + * 是否官方歌单:0-否,1-是 + */ + private Integer isOfficial; + + /** + * 播放次数 + */ + private Long playCount; + + /** + * 歌单中的音乐 + */ + private List musics; + + /** + * 音乐数量 + */ + private Integer musicCount; + + /** + * 是否已收藏 + */ + private Boolean isCollected; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistTypeDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistTypeDTO.java new file mode 100644 index 0000000..6931d4d --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistTypeDTO.java @@ -0,0 +1,25 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +/** + * 歌单类型数据传输对象 + */ +@Data +public class PlaylistTypeDTO { + + /** + * 类型ID + */ + private Long id; + + /** + * 类型名称 + */ + private String name; + + /** + * 类型描述 + */ + private String description; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/RegisterRequest.java b/music-houduan/src/main/java/com/test/musichouduan/dto/RegisterRequest.java new file mode 100644 index 0000000..533fa5a --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/RegisterRequest.java @@ -0,0 +1,45 @@ +package com.test.musichouduan.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 注册请求 + */ +@Data +public class RegisterRequest { + + /** + * 用户名 + */ + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 20, message = "用户名长度应为3-20个字符") + private String username; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度应为6-20个字符") + private String password; + + /** + * 确认密码 + */ + @NotBlank(message = "确认密码不能为空") + private String confirmPassword; + + /** + * 邮箱 + */ + @NotBlank(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") + private String email; + + /** + * 昵称 + */ + private String nickname; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/Result.java b/music-houduan/src/main/java/com/test/musichouduan/dto/Result.java new file mode 100644 index 0000000..c847f83 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/Result.java @@ -0,0 +1,84 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +/** + * 通用响应结果 + * + * @param 数据类型 + */ +@Data +public class Result { + + /** + * 状态码 + */ + private Integer code; + + /** + * 消息 + */ + private String message; + + /** + * 数据 + */ + private T data; + + /** + * 成功 + * + * @param data 数据 + * @param 数据类型 + * @return 响应结果 + */ + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(200); + result.setMessage("success"); + result.setData(data); + return result; + } + + /** + * 成功 + * + * @param message 消息 + * @param data 数据 + * @param 数据类型 + * @return 响应结果 + */ + public static Result success(String message, T data) { + Result result = new Result<>(); + result.setCode(200); + result.setMessage(message); + result.setData(data); + return result; + } + + /** + * 失败 + * + * @param code 状态码 + * @param message 消息 + * @param 数据类型 + * @return 响应结果 + */ + public static Result error(Integer code, String message) { + Result result = new Result<>(); + result.setCode(code); + result.setMessage(message); + return result; + } + + /** + * 失败 + * + * @param message 消息 + * @param 数据类型 + * @return 响应结果 + */ + public static Result error(String message) { + return error(500, message); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/SingerDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/SingerDTO.java new file mode 100644 index 0000000..b146eb8 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/SingerDTO.java @@ -0,0 +1,63 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 歌手数据传输对象 + */ +@Data +public class SingerDTO { + + /** + * 歌手ID + */ + private Long id; + + /** + * 歌手名称 + */ + private String name; + + /** + * 歌手头像URL + */ + private String avatar; + + /** + * 歌手简介 + */ + private String introduction; + + /** + * 歌手类型 + */ + private SingerTypeDTO type; + + /** + * 音乐数量 + */ + private Long musicCount; + + /** + * 粉丝数量 + */ + private Long fansCount; + + /** + * 是否已收藏 + */ + private Boolean isCollected; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 关联音乐列表 + */ + private List musics; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/SingerTypeDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/SingerTypeDTO.java new file mode 100644 index 0000000..b9012e0 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/SingerTypeDTO.java @@ -0,0 +1,25 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +/** + * 歌手类型数据传输对象 + */ +@Data +public class SingerTypeDTO { + + /** + * 类型ID + */ + private Long id; + + /** + * 类型名称 + */ + private String name; + + /** + * 类型描述 + */ + private String description; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/UserDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/UserDTO.java new file mode 100644 index 0000000..d873dcd --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/UserDTO.java @@ -0,0 +1,87 @@ +package com.test.musichouduan.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 用户数据传输对象 + */ +@Data +public class UserDTO { + + /** + * 用户ID + */ + private Long id; + + /** + * 用户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 头像URL + */ + private String avatar; + + /** + * 性别:0-未知,1-男,2-女 + */ + private Integer gender; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + + /** + * 个人简介 + */ + private String introduction; + + /** + * 角色:0-普通用户,1-管理员 + */ + private Integer role; + + /** + * 状态:0-禁用,1-正常 + */ + private Integer status; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 收藏音乐数量 + */ + private Long collectMusicCount; + + /** + * 收藏歌单数量 + */ + private Long collectPlaylistCount; + + /** + * 收藏歌手数量 + */ + private Long collectSingerCount; + + /** + * 创建歌单数量 + */ + private Long createPlaylistCount; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/UserUpdateRequest.java b/music-houduan/src/main/java/com/test/musichouduan/dto/UserUpdateRequest.java new file mode 100644 index 0000000..dd99d23 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/UserUpdateRequest.java @@ -0,0 +1,46 @@ +package com.test.musichouduan.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 用户信息更新请求 + */ +@Data +public class UserUpdateRequest { + + /** + * 昵称 + */ + @Size(max = 50, message = "昵称长度不能超过50个字符") + private String nickname; + + /** + * 头像URL + */ + private String avatar; + + /** + * 性别:0-未知,1-男,2-女 + */ + private Integer gender; + + /** + * 邮箱 + */ + @Email(message = "邮箱格式不正确") + private String email; + + /** + * 手机号 + */ + @Size(max = 20, message = "手机号长度不能超过20个字符") + private String phone; + + /** + * 个人简介 + */ + @Size(max = 500, message = "个人简介长度不能超过500个字符") + private String introduction; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/admin/DashboardDataDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/admin/DashboardDataDTO.java new file mode 100644 index 0000000..6dd6864 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/admin/DashboardDataDTO.java @@ -0,0 +1,63 @@ +package com.test.musichouduan.dto.admin; + +// 不需要导入同包下的类 +import lombok.Data; + +import java.util.List; + +/** + * 控制台数据传输对象 + */ +@Data +public class DashboardDataDTO { + + /** + * 用户数量 + */ + private long userCount; + + /** + * 音乐数量 + */ + private long musicCount; + + /** + * 歌单数量 + */ + private long playlistCount; + + /** + * 歌手数量 + */ + private long singerCount; + + /** + * 总播放量 + */ + private long playCount; + + /** + * 用户增长数据 + */ + private List userGrowth; + + /** + * 音乐类型分布数据 + */ + private List musicTypeDistribution; + + /** + * 播放量数据 + */ + private List playCountData; + + /** + * 最新用户 + */ + private List latestUsers; + + /** + * 最新音乐 + */ + private List latestMusics; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/admin/MusicAdminDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/admin/MusicAdminDTO.java new file mode 100644 index 0000000..b2f8a98 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/admin/MusicAdminDTO.java @@ -0,0 +1,69 @@ +package com.test.musichouduan.dto.admin; + +import com.test.musichouduan.entity.Music; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 音乐管理数据传输对象 + */ +@Data +public class MusicAdminDTO { + + /** + * ID + */ + private Long id; + + /** + * 音乐名称 + */ + private String name; + + /** + * 歌手名称 + */ + private String singerName; + + /** + * 封面URL + */ + private String coverUrl; + + /** + * 播放量 + */ + private Long playCount; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 将实体转换为DTO + * + * @param music 音乐实体 + * @return 音乐DTO + */ + public static MusicAdminDTO fromEntity(Music music) { + if (music == null) { + return null; + } + + MusicAdminDTO dto = new MusicAdminDTO(); + dto.setId(music.getId()); + dto.setName(music.getName()); + // 获取歌手名称 + String singerName = null; + if (music.getSingers() != null && !music.getSingers().isEmpty()) { + singerName = music.getSingers().iterator().next().getName(); + } + dto.setSingerName(singerName); + dto.setCoverUrl(music.getCover()); + dto.setPlayCount(music.getPlayCount()); + dto.setCreateTime(music.getCreateTime()); + return dto; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/admin/StatItemDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/admin/StatItemDTO.java new file mode 100644 index 0000000..4d3aa8c --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/admin/StatItemDTO.java @@ -0,0 +1,26 @@ +package com.test.musichouduan.dto.admin; + +import lombok.Data; +import java.util.List; + +/** + * 统计项数据传输对象 + */ +@Data +public class StatItemDTO { + + /** + * 名称 + */ + private String name; + + /** + * 值 + */ + private long value; + + /** + * 子项列表 + */ + private List children; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/admin/UserAdminDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/admin/UserAdminDTO.java new file mode 100644 index 0000000..c325eb7 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/admin/UserAdminDTO.java @@ -0,0 +1,88 @@ +package com.test.musichouduan.dto.admin; + +import com.test.musichouduan.entity.User; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 用户管理数据传输对象 + */ +@Data +public class UserAdminDTO { + + /** + * ID + */ + private Long id; + + /** + * 用户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 头像URL + */ + private String avatar; + + /** + * 性别:0-未知,1-男,2-女 + */ + private Integer gender; + + /** + * 邮箱 + */ + private String email; + + /** + * 手机号 + */ + private String phone; + + /** + * 角色:0-普通用户,1-管理员 + */ + private Integer role; + + /** + * 状态:0-禁用,1-正常 + */ + private Integer status; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 将实体转换为DTO + * + * @param user 用户实体 + * @return 用户DTO + */ + public static UserAdminDTO fromEntity(User user) { + if (user == null) { + return null; + } + + UserAdminDTO dto = new UserAdminDTO(); + dto.setId(user.getId()); + dto.setUsername(user.getUsername()); + dto.setNickname(user.getNickname()); + dto.setAvatar(user.getAvatar()); + dto.setGender(user.getGender()); + dto.setEmail(user.getEmail()); + dto.setPhone(user.getPhone()); + dto.setRole(user.getRole()); + dto.setStatus(user.getStatus()); + dto.setCreateTime(user.getCreateTime()); + return dto; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/Banner.java b/music-houduan/src/main/java/com/test/musichouduan/entity/Banner.java new file mode 100644 index 0000000..fa92ed2 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/Banner.java @@ -0,0 +1,77 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 轮播图实体类 + */ +@Data +@Entity +@Table(name = "banner") +public class Banner { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 标题 + */ + @Column(length = 100) + private String title; + + /** + * 图片URL + */ + @Column(nullable = false, length = 255) + private String imageUrl; + + /** + * 链接URL + */ + @Column(length = 255) + private String linkUrl; + + /** + * 目标ID + */ + @Column + private Long targetId; + + /** + * 目标类型:1-音乐,2-歌单,3-歌手 + */ + @Column + private Integer targetType; + + /** + * 排序号 + */ + @Column + private Integer orderNum; + + /** + * 状态:0-禁用,1-正常 + */ + @Column + private Integer status; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(nullable = false) + private LocalDateTime updateTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/Comment.java b/music-houduan/src/main/java/com/test/musichouduan/entity/Comment.java new file mode 100644 index 0000000..8342996 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/Comment.java @@ -0,0 +1,72 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 评论实体类 + */ +@Data +@Entity +@Table(name = "comment") +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 评论内容 + */ + @Column(nullable = false, length = 500) + private String content; + + /** + * 评论用户 + */ + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + /** + * 评论目标ID + */ + @Column(nullable = false) + private Long targetId; + + /** + * 评论目标类型:1-音乐,2-歌单,3-歌手 + */ + @Column(nullable = false) + private Integer targetType; + + /** + * 父评论ID + */ + @Column + private Long parentId; + + /** + * 状态:0-禁用,1-正常 + */ + @Column + private Integer status; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(nullable = false) + private LocalDateTime updateTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/Music.java b/music-houduan/src/main/java/com/test/musichouduan/entity/Music.java new file mode 100644 index 0000000..64a6421 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/Music.java @@ -0,0 +1,97 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +/** + * 音乐实体类 + */ +@Data +@Entity +@Table(name = "music") +public class Music { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 音乐名称 + */ + @Column(nullable = false, length = 100) + private String name; + + /** + * 封面URL + */ + @Column(length = 255) + private String cover; + + /** + * 音乐文件URL + */ + @Column(nullable = false, length = 255) + private String url; + + /** + * 歌词 + */ + @Column(columnDefinition = "TEXT") + private String lyric; + + /** + * 时长(秒) + */ + @Column + private Integer duration; + + /** + * 专辑 + */ + @Column(length = 100) + private String album; + + /** + * 播放次数 + */ + @Column + private Long playCount; + + /** + * 歌手列表 + */ + @ManyToMany(mappedBy = "musics") + private Set singers = new HashSet<>(); + + /** + * 音乐类型列表 + */ + @ManyToMany + @JoinTable( + name = "music_type_relation", + joinColumns = @JoinColumn(name = "music_id"), + inverseJoinColumns = @JoinColumn(name = "type_id") + ) + private Set types = new HashSet<>(); + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(nullable = false) + private LocalDateTime updateTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/MusicType.java b/music-houduan/src/main/java/com/test/musichouduan/entity/MusicType.java new file mode 100644 index 0000000..7b76b16 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/MusicType.java @@ -0,0 +1,47 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 音乐类型实体类 + */ +@Data +@Entity +@Table(name = "music_type") +public class MusicType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 类型名称 + */ + @Column(nullable = false, unique = true, length = 50) + private String name; + + /** + * 类型描述 + */ + @Column(length = 255) + private String description; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(nullable = false) + private LocalDateTime updateTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/PlayHistory.java b/music-houduan/src/main/java/com/test/musichouduan/entity/PlayHistory.java new file mode 100644 index 0000000..2de425e --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/PlayHistory.java @@ -0,0 +1,41 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * 播放历史实体类 + */ +@Data +@Entity +@Table(name = "play_history") +public class PlayHistory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户 + */ + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + /** + * 音乐 + */ + @ManyToOne + @JoinColumn(name = "music_id", nullable = false) + private Music music; + + /** + * 播放时间 + */ + @CreationTimestamp + @Column(nullable = false) + private LocalDateTime playTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/Playlist.java b/music-houduan/src/main/java/com/test/musichouduan/entity/Playlist.java new file mode 100644 index 0000000..f5f0a92 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/Playlist.java @@ -0,0 +1,92 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +/** + * 歌单实体类 + */ +@Data +@Entity +@Table(name = "playlist") +public class Playlist { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 歌单名称 + */ + @Column(nullable = false, length = 100) + private String name; + + /** + * 封面URL + */ + @Column(length = 255) + private String cover; + + /** + * 歌单描述 + */ + @Column(length = 500) + private String description; + + /** + * 创建者 + */ + @ManyToOne + @JoinColumn(name = "creator_id") + private User creator; + + /** + * 歌单类型 + */ + @ManyToOne + @JoinColumn(name = "type_id") + private PlaylistType type; + + /** + * 是否官方歌单:0-否,1-是 + */ + @Column + private Integer isOfficial; + + /** + * 播放次数 + */ + @Column + private Long playCount; + + /** + * 歌单中的音乐 + */ + @ManyToMany + @JoinTable( + name = "playlist_music", + joinColumns = @JoinColumn(name = "playlist_id"), + inverseJoinColumns = @JoinColumn(name = "music_id") + ) + private Set musics = new HashSet<>(); + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(nullable = false) + private LocalDateTime updateTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/PlaylistType.java b/music-houduan/src/main/java/com/test/musichouduan/entity/PlaylistType.java new file mode 100644 index 0000000..28cb264 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/PlaylistType.java @@ -0,0 +1,47 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 歌单类型实体类 + */ +@Data +@Entity +@Table(name = "playlist_type") +public class PlaylistType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 类型名称 + */ + @Column(nullable = false, unique = true, length = 50) + private String name; + + /** + * 类型描述 + */ + @Column(length = 255) + private String description; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(nullable = false) + private LocalDateTime updateTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/Singer.java b/music-houduan/src/main/java/com/test/musichouduan/entity/Singer.java new file mode 100644 index 0000000..533cf99 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/Singer.java @@ -0,0 +1,75 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.Set; +import java.util.HashSet; + +/** + * 歌手实体类 + */ +@Data +@Entity +@Table(name = "singer") +@EqualsAndHashCode(exclude = {"musics"}) +public class Singer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 歌手名称 + */ + @Column(nullable = false, length = 100) + private String name; + + /** + * 歌手头像URL + */ + @Column(length = 255) + private String avatar; + + /** + * 歌手简介 + */ + @Column(columnDefinition = "TEXT") + private String introduction; + + /** + * 歌手类型ID + */ + @ManyToOne + @JoinColumn(name = "type_id") + private SingerType type; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(nullable = false) + private LocalDateTime updateTime; + + /** + * 歌手关联的音乐 + */ + @ManyToMany + @JoinTable( + name = "singer_music", + joinColumns = @JoinColumn(name = "singer_id"), + inverseJoinColumns = @JoinColumn(name = "music_id") + ) + private Set musics = new HashSet<>(); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/SingerType.java b/music-houduan/src/main/java/com/test/musichouduan/entity/SingerType.java new file mode 100644 index 0000000..78b3746 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/SingerType.java @@ -0,0 +1,47 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 歌手类型实体类 + */ +@Data +@Entity +@Table(name = "singer_type") +public class SingerType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 类型名称 + */ + @Column(nullable = false, unique = true, length = 50) + private String name; + + /** + * 类型描述 + */ + @Column(length = 255) + private String description; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(nullable = false) + private LocalDateTime updateTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/User.java b/music-houduan/src/main/java/com/test/musichouduan/entity/User.java new file mode 100644 index 0000000..c8b8ae6 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/User.java @@ -0,0 +1,95 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +/** + * 用户实体类 + */ +@Data +@Entity +@Table(name = "user") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户名 + */ + @Column(nullable = false, unique = true, length = 50) + private String username; + + /** + * 密码 + */ + @Column(nullable = false, length = 100) + private String password; + + /** + * 昵称 + */ + @Column(length = 50) + private String nickname; + + /** + * 头像URL + */ + @Column(length = 255) + private String avatar; + + /** + * 性别:0-未知,1-男,2-女 + */ + @Column + private Integer gender; + + /** + * 邮箱 + */ + @Column(length = 100) + private String email; + + /** + * 手机号 + */ + @Column(length = 20) + private String phone; + + /** + * 个人简介 + */ + @Column(length = 500) + private String introduction; + + /** + * 角色:0-普通用户,1-管理员 + */ + @Column + private Integer role; + + /** + * 状态:0-禁用,1-正常 + */ + @Column + private Integer status; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @UpdateTimestamp + @Column(nullable = false) + private LocalDateTime updateTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/UserCollectMusic.java b/music-houduan/src/main/java/com/test/musichouduan/entity/UserCollectMusic.java new file mode 100644 index 0000000..9c176d7 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/UserCollectMusic.java @@ -0,0 +1,41 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * 用户收藏音乐实体类 + */ +@Data +@Entity +@Table(name = "user_collect_music") +public class UserCollectMusic { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户 + */ + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + /** + * 音乐 + */ + @ManyToOne + @JoinColumn(name = "music_id", nullable = false) + private Music music; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/UserCollectPlaylist.java b/music-houduan/src/main/java/com/test/musichouduan/entity/UserCollectPlaylist.java new file mode 100644 index 0000000..5d7b8e6 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/UserCollectPlaylist.java @@ -0,0 +1,41 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * 用户收藏歌单实体类 + */ +@Data +@Entity +@Table(name = "user_collect_playlist") +public class UserCollectPlaylist { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户 + */ + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + /** + * 歌单 + */ + @ManyToOne + @JoinColumn(name = "playlist_id", nullable = false) + private Playlist playlist; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/UserCollectSinger.java b/music-houduan/src/main/java/com/test/musichouduan/entity/UserCollectSinger.java new file mode 100644 index 0000000..72925ad --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/UserCollectSinger.java @@ -0,0 +1,41 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * 用户收藏歌手实体类 + */ +@Data +@Entity +@Table(name = "user_collect_singer") +public class UserCollectSinger { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户 + */ + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + /** + * 歌手 + */ + @ManyToOne + @JoinColumn(name = "singer_id", nullable = false) + private Singer singer; + + /** + * 创建时间 + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/UserMusic.java b/music-houduan/src/main/java/com/test/musichouduan/entity/UserMusic.java new file mode 100644 index 0000000..9e7b731 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/UserMusic.java @@ -0,0 +1,44 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * 用户音乐关联实体 + */ +@Data +@Entity +@Table(name = "user_music") +public class UserMusic { + + /** + * ID + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户 + */ + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + /** + * 音乐 + */ + @ManyToOne + @JoinColumn(name = "music_id") + private Music music; + + /** + * 收藏时间 + */ + @CreationTimestamp + @Column(name = "collect_time", updatable = false) + private LocalDateTime collectTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/UserPlaylist.java b/music-houduan/src/main/java/com/test/musichouduan/entity/UserPlaylist.java new file mode 100644 index 0000000..dc0b1a5 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/UserPlaylist.java @@ -0,0 +1,44 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * 用户歌单关联实体 + */ +@Data +@Entity +@Table(name = "user_playlist") +public class UserPlaylist { + + /** + * ID + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户 + */ + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + /** + * 歌单 + */ + @ManyToOne + @JoinColumn(name = "playlist_id") + private Playlist playlist; + + /** + * 收藏时间 + */ + @CreationTimestamp + @Column(name = "collect_time", updatable = false) + private LocalDateTime collectTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/UserSinger.java b/music-houduan/src/main/java/com/test/musichouduan/entity/UserSinger.java new file mode 100644 index 0000000..fffe235 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/UserSinger.java @@ -0,0 +1,44 @@ +package com.test.musichouduan.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * 用户歌手关联实体 + */ +@Data +@Entity +@Table(name = "user_singer") +public class UserSinger { + + /** + * ID + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户 + */ + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + /** + * 歌手 + */ + @ManyToOne + @JoinColumn(name = "singer_id") + private Singer singer; + + /** + * 收藏时间 + */ + @CreationTimestamp + @Column(name = "collect_time", updatable = false) + private LocalDateTime collectTime; +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/exception/BusinessException.java b/music-houduan/src/main/java/com/test/musichouduan/exception/BusinessException.java new file mode 100644 index 0000000..8be1406 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/exception/BusinessException.java @@ -0,0 +1,59 @@ +package com.test.musichouduan.exception; + +/** + * 业务异常 + */ +public class BusinessException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误消息 + */ + private String message; + + /** + * 构造方法 + * + * @param message 错误消息 + */ + public BusinessException(String message) { + this.code = 500; + this.message = message; + } + + /** + * 构造方法 + * + * @param code 错误码 + * @param message 错误消息 + */ + public BusinessException(Integer code, String message) { + this.code = code; + this.message = message; + } + + /** + * 获取错误码 + * + * @return 错误码 + */ + public Integer getCode() { + return code; + } + + /** + * 获取错误消息 + * + * @return 错误消息 + */ + @Override + public String getMessage() { + return message; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/exception/GlobalExceptionHandler.java b/music-houduan/src/main/java/com/test/musichouduan/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..4c0cce2 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/exception/GlobalExceptionHandler.java @@ -0,0 +1,132 @@ +package com.test.musichouduan.exception; + +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import com.test.musichouduan.dto.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.servlet.resource.NoResourceFoundException; +import jakarta.servlet.http.HttpServletRequest; + +/** + * 全局异常处理器 + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 处理自定义异常 + */ + @ExceptionHandler(BusinessException.class) + public Result handleBusinessException(BusinessException e) { + logger.error(e.getMessage(), e); + return Result.error(e.getCode(), e.getMessage()); + } + + /** + * 处理未登录异常 + */ + @ExceptionHandler(NotLoginException.class) + public Result handleNotLoginException(NotLoginException e) { + logger.error(e.getMessage(), e); + return Result.error(401, "未登录或登录已过期,请重新登录"); + } + + /** + * 处理无权限异常 + */ + @ExceptionHandler(NotPermissionException.class) + public Result handleNotPermissionException(NotPermissionException e) { + logger.error(e.getMessage(), e); + return Result.error(403, "无权限访问"); + } + + /** + * 处理无角色异常 + */ + @ExceptionHandler(NotRoleException.class) + public Result handleNotRoleException(NotRoleException e) { + logger.error(e.getMessage(), e); + return Result.error(403, "无权限访问"); + } + + /** + * 处理参数校验异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + logger.error(e.getMessage(), e); + BindingResult bindingResult = e.getBindingResult(); + StringBuilder sb = new StringBuilder("参数校验失败:"); + for (FieldError fieldError : bindingResult.getFieldErrors()) { + sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", "); + } + String msg = sb.toString(); + if (msg.endsWith(", ")) { + msg = msg.substring(0, msg.length() - 2); + } + return Result.error(400, msg); + } + + /** + * 处理参数绑定异常 + */ + @ExceptionHandler(BindException.class) + public Result handleBindException(BindException e) { + logger.error(e.getMessage(), e); + BindingResult bindingResult = e.getBindingResult(); + StringBuilder sb = new StringBuilder("参数绑定失败:"); + for (FieldError fieldError : bindingResult.getFieldErrors()) { + sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", "); + } + String msg = sb.toString(); + if (msg.endsWith(", ")) { + msg = msg.substring(0, msg.length() - 2); + } + return Result.error(400, msg); + } + + /** + * 处理文件上传大小超限异常 + */ + @ExceptionHandler(MaxUploadSizeExceededException.class) + public Result handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) { + logger.error(e.getMessage(), e); + return Result.error(400, "上传文件大小超出限制"); + } + + /** + * 处理资源未找到异常 + */ + @ExceptionHandler(NoResourceFoundException.class) + public Result handleNoResourceFoundException(NoResourceFoundException e, HttpServletRequest request) { + // 如果是 favicon.ico 的请求,则不记录日志 + String requestURI = request.getRequestURI(); + if (requestURI.endsWith("/favicon.ico")) { + return Result.error(404, "Resource not found"); + } + + // 其他资源未找到的情况,记录日志 + logger.error(e.getMessage(), e); + return Result.error(404, "Resource not found"); + } + + /** + * 处理其他异常 + */ + @ExceptionHandler(Exception.class) + public Result handleException(Exception e) { + logger.error(e.getMessage(), e); + return Result.error(500, "服务器内部错误"); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/init/DataInitializer.java b/music-houduan/src/main/java/com/test/musichouduan/init/DataInitializer.java new file mode 100644 index 0000000..285f704 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/init/DataInitializer.java @@ -0,0 +1,109 @@ +package com.test.musichouduan.init; + +import cn.dev33.satoken.secure.BCrypt; +import com.test.musichouduan.entity.*; +import com.test.musichouduan.repository.*; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; + +/** + * 数据初始化器 + */ +@Component +public class DataInitializer implements CommandLineRunner { + + @Autowired + private UserRepository userRepository; + + @Autowired + private MusicTypeRepository musicTypeRepository; + + @Autowired + private SingerTypeRepository singerTypeRepository; + + @Autowired + private PlaylistTypeRepository playlistTypeRepository; + + @Override + @Transactional + public void run(String... args) throws Exception { + // 初始化管理员用户 + initAdminUser(); + + // 初始化音乐类型 + initMusicTypes(); + + // 初始化歌手类型 + initSingerTypes(); + + // 初始化歌单类型 + initPlaylistTypes(); + } + + /** + * 初始化管理员用户 + */ + private void initAdminUser() { + if (!userRepository.existsByUsername("admin")) { + User admin = new User(); + admin.setUsername("admin"); + admin.setPassword(BCrypt.hashpw("123456")); + admin.setNickname("管理员"); + admin.setGender(0); + admin.setRole(1); + admin.setStatus(1); + userRepository.save(admin); + } + } + + /** + * 初始化音乐类型 + */ + private void initMusicTypes() { + if (musicTypeRepository.count() == 0) { + List typeNames = Arrays.asList("流行", "摇滚", "民谣", "电子", "古典", "嘻哈", "爵士", "乡村"); + for (String name : typeNames) { + MusicType type = new MusicType(); + type.setName(name); + type.setDescription(name + "音乐"); + musicTypeRepository.save(type); + } + } + } + + /** + * 初始化歌手类型 + */ + private void initSingerTypes() { + if (singerTypeRepository.count() == 0) { + List typeNames = Arrays.asList("华语男歌手", "华语女歌手", "华语组合", "欧美男歌手", "欧美女歌手", "欧美组合", "日韩男歌手", "日韩女歌手", "日韩组合"); + for (String name : typeNames) { + SingerType type = new SingerType(); + type.setName(name); + type.setDescription(name); + singerTypeRepository.save(type); + } + } + } + + /** + * 初始化歌单类型 + */ + private void initPlaylistTypes() { + if (playlistTypeRepository.count() == 0) { + List typeNames = Arrays.asList("流行", "摇滚", "民谣", "电子", "古典", "嘻哈", "爵士", "乡村", "轻音乐", "ACG"); + for (String name : typeNames) { + PlaylistType type = new PlaylistType(); + type.setName(name); + type.setDescription(name + "歌单"); + playlistTypeRepository.save(type); + } + } + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/BannerRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/BannerRepository.java new file mode 100644 index 0000000..f3a5d56 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/BannerRepository.java @@ -0,0 +1,22 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Banner; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 轮播图数据访问层 + */ +@Repository +public interface BannerRepository extends JpaRepository { + + /** + * 根据状态查询轮播图 + * + * @param status 状态 + * @return 轮播图列表 + */ + List findByStatusOrderByOrderNumAsc(Integer status); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/CommentRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/CommentRepository.java new file mode 100644 index 0000000..ec77a74 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/CommentRepository.java @@ -0,0 +1,54 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Comment; +import com.test.musichouduan.entity.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 评论数据访问层 + */ +@Repository +public interface CommentRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据目标ID和目标类型查询评论 + * + * @param targetId 目标ID + * @param targetType 目标类型 + * @param pageable 分页 + * @return 评论列表 + */ + Page findByTargetIdAndTargetTypeAndParentIdIsNullOrderByCreateTimeDesc(Long targetId, Integer targetType, Pageable pageable); + + /** + * 根据父评论ID查询子评论 + * + * @param parentId 父评论ID + * @return 子评论列表 + */ + List findByParentIdOrderByCreateTimeAsc(Long parentId); + + /** + * 根据用户查询评论 + * + * @param user 用户 + * @param pageable 分页 + * @return 评论列表 + */ + Page findByUserOrderByCreateTimeDesc(User user, Pageable pageable); + + /** + * 根据目标ID和目标类型统计评论数量 + * + * @param targetId 目标ID + * @param targetType 目标类型 + * @return 评论数量 + */ + long countByTargetIdAndTargetType(Long targetId, Integer targetType); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/MusicRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/MusicRepository.java new file mode 100644 index 0000000..835e73a --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/MusicRepository.java @@ -0,0 +1,116 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.entity.MusicType; +import com.test.musichouduan.entity.Singer; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 音乐数据访问层 + */ +@Repository +public interface MusicRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据名称模糊查询音乐 + * + * @param name 名称 + * @param pageable 分页 + * @return 音乐列表 + */ + Page findByNameContaining(String name, Pageable pageable); + + /** + * 根据类型查询音乐 + * + * @param type 类型 + * @param pageable 分页 + * @return 音乐列表 + */ + @Query("SELECT m FROM Music m JOIN m.types t WHERE t = :type") + Page findByType(MusicType type, Pageable pageable); + + /** + * 根据歌手查询音乐 + * + * @param singer 歌手 + * @param pageable 分页 + * @return 音乐列表 + */ + @Query("SELECT m FROM Music m JOIN m.singers s WHERE s = :singer") + Page findBySinger(Singer singer, Pageable pageable); + + /** + * 根据歌手查询音乐 + * + * @param singer 歌手 + * @param pageable 分页 + * @return 音乐列表 + */ + Page findBySingersContaining(Singer singer, Pageable pageable); + + /** + * 统计歌手的音乐数量 + * + * @param singer 歌手 + * @return 音乐数量 + */ + Long countBySingersContaining(Singer singer); + + /** + * 检查是否存在指定类型的音乐 + * + * @param type 音乐类型 + * @return 是否存在 + */ + boolean existsByTypesContaining(MusicType type); + + /** + * 查询最新的音乐 + * + * @param pageable 分页 + * @return 音乐列表 + */ + @Query("SELECT m FROM Music m ORDER BY m.createTime DESC") + List findLatest(Pageable pageable); + + /** + * 查询播放次数最多的音乐 + * + * @param pageable 分页 + * @return 音乐列表 + */ + @Query("SELECT m FROM Music m ORDER BY m.playCount DESC") + List findMostPlayed(Pageable pageable); + + /** + * 统计包含指定类型的音乐数量 + * + * @param type 音乐类型 + * @return 音乐数量 + */ + long countByTypesContaining(MusicType type); + + /** + * 统计所有音乐的总播放量 + * + * @return 总播放量 + */ + @Query("SELECT COALESCE(SUM(m.playCount), 0) FROM Music m") + long sumPlayCount(); + + /** + * 统计各个音乐类型的音乐数量 + * + * @return 音乐类型及其对应的音乐数量 + */ + @Query("SELECT t.name, COUNT(m) FROM Music m JOIN m.types t GROUP BY t.name ORDER BY COUNT(m) DESC") + List countByMusicType(); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/MusicTypeRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/MusicTypeRepository.java new file mode 100644 index 0000000..40b1834 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/MusicTypeRepository.java @@ -0,0 +1,31 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.MusicType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 音乐类型数据访问层 + */ +@Repository +public interface MusicTypeRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据名称查找音乐类型 + * + * @param name 名称 + * @return 音乐类型 + */ + Optional findByName(String name); + + /** + * 判断名称是否存在 + * + * @param name 名称 + * @return 是否存在 + */ + boolean existsByName(String name); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/PlayHistoryRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/PlayHistoryRepository.java new file mode 100644 index 0000000..9e1ba0d --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/PlayHistoryRepository.java @@ -0,0 +1,79 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.entity.PlayHistory; +import com.test.musichouduan.entity.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * 播放历史数据访问层 + */ +@Repository +public interface PlayHistoryRepository extends JpaRepository { + + /** + /** + /** + * 根据用户查询播放历史 + * + * @param user 用户 + * @param pageable 分页 + * @return 播放历史列表 + */ + Page findByUserOrderByPlayTimeDesc(User user, Pageable pageable); + + /** + * 根据用户和音乐查询播放历史 + * + * @param user 用户 + * @param music 音乐 + * @return 播放历史 + */ + Optional findByUserAndMusic(User user, Music music); + + /** + * 根据用户统计播放历史数量 + * + * @param user 用户 + * @return 播放历史数量 + */ + long countByUser(User user); + + /** + * 根据音乐统计播放次数 + * + * @param music 音乐 + * @return 播放次数 + */ + long countByMusic(Music music); + + /** + * 统计指定时间范围内的播放次数 + * + * @param start 开始时间 + * @param end 结束时间 + * @return 播放次数 + */ + + long countByPlayTimeBetween(LocalDateTime start, LocalDateTime end); + + /** + *记录播放历史 + * @param userId 用户ID + * @param musicId 音乐ID + */ + @Modifying + @Transactional + @Query(value = "INSERT INTO play_history (user_id, music_id, play_time) VALUES (?1, ?2, NOW()) " + + "ON DUPLICATE KEY UPDATE play_time = NOW()", nativeQuery = true) + void recordPlayHistory(Long userId, Long musicId); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/PlaylistRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/PlaylistRepository.java new file mode 100644 index 0000000..6ed73d4 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/PlaylistRepository.java @@ -0,0 +1,100 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Playlist; +import com.test.musichouduan.entity.PlaylistType; +import com.test.musichouduan.entity.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 歌单数据访问层 + */ +@Repository +public interface PlaylistRepository extends JpaRepository, JpaSpecificationExecutor { + + @Query("SELECT pt.name, COUNT(p) FROM Playlist p JOIN p.type pt GROUP BY pt.name") + List countByPlaylistType(); + + /** + * 根据类型统计歌单数量 + * + * @param type 歌单类型 + * @return 歌单数量 + */ + long countByType(PlaylistType type); + + /** + * 根据名称模糊查询歌单 + * + * @param name 名称 + * @param pageable 分页 + * @return 歌单列表 + */ + Page findByNameContaining(String name, Pageable pageable); + + /** + * 根据类型查询歌单 + * + * @param type 类型 + * @param pageable 分页 + * @return 歌单列表 + */ + Page findByType(PlaylistType type, Pageable pageable); + + /** + * 根据创建者查询歌单 + * + * @param creator 创建者 + * @param pageable 分页 + * @return 歌单列表 + */ + Page findByCreator(User creator, Pageable pageable); + + /** + * 查询官方歌单 + * + * @param pageable 分页 + * @return 歌单列表 + */ + Page findByIsOfficial(Integer isOfficial, Pageable pageable); + + /** + * 查询最新的歌单 + * + * @param pageable 分页 + * @return 歌单列表 + */ + @Query("SELECT p FROM Playlist p ORDER BY p.createTime DESC") + List findLatest(Pageable pageable); + + /** + * 查询播放次数最多的歌单 + * + * @param pageable 分页 + * @return 歌单列表 + */ + @Query("SELECT p FROM Playlist p ORDER BY p.playCount DESC") + List findMostPlayed(Pageable pageable); + + /** + * 统计用户创建的歌单数量 + * + * @param creator 创建者 + * @return 歌单数量 + */ + long countByCreator(User creator); + + /** + * 检查是否存在指定类型的歌单 + * + * @param type 歌单类型 + * @return 是否存在 + */ + boolean existsByType(PlaylistType type); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/PlaylistTypeRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/PlaylistTypeRepository.java new file mode 100644 index 0000000..520510d --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/PlaylistTypeRepository.java @@ -0,0 +1,31 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.PlaylistType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 歌单类型数据访问层 + */ +@Repository +public interface PlaylistTypeRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据名称查找歌单类型 + * + * @param name 名称 + * @return 歌单类型 + */ + Optional findByName(String name); + + /** + * 判断名称是否存在 + * + * @param name 名称 + * @return 是否存在 + */ + boolean existsByName(String name); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/SingerRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/SingerRepository.java new file mode 100644 index 0000000..020f39f --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/SingerRepository.java @@ -0,0 +1,83 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Singer; +import com.test.musichouduan.entity.SingerType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 歌手数据访问层 + */ +@Repository +public interface SingerRepository extends JpaRepository, JpaSpecificationExecutor { + + @Query("SELECT st.name, COUNT(s) FROM Singer s JOIN s.type st GROUP BY st.name") + List countBySingerType(); + + /** + * 根据类型统计歌手数量 + * + * @param type 歌手类型 + * @return 歌手数量 + */ + long countByType(SingerType type); + + /** + * 根据名称查找歌手 + * + * @param name 名称 + * @return 歌手 + */ + Optional findByName(String name); + + /** + * 根据类型查找歌手 + * + * @param type 类型 + * @param pageable 分页 + * @return 歌手列表 + */ + Page findByType(SingerType type, Pageable pageable); + + /** + * 根据名称模糊查询歌手 + * + * @param name 名称 + * @param pageable 分页 + * @return 歌手列表 + */ + Page findByNameContaining(String name, Pageable pageable); + + /** + * 查询最新的歌手 + * + * @param limit 限制数量 + * @return 歌手列表 + */ + @Query(value = "SELECT s FROM Singer s ORDER BY s.createTime DESC") + List findLatest(Pageable pageable); + + /** + * 查询热门歌手(按照音乐数量排序) + * + * @param pageable 分页 + * @return 歌手列表 + */ + @Query(value = "SELECT s.* FROM singer s LEFT JOIN (SELECT singer_id, COUNT(*) as music_count FROM music_singer GROUP BY singer_id) ms ON s.id = ms.singer_id ORDER BY COALESCE(ms.music_count, 0) DESC", nativeQuery = true) + List findHotSingers(Pageable pageable); + + /** + * 检查是否存在指定类型的歌手 + * + * @param type 歌手类型 + * @return 是否存在 + */ + boolean existsByType(SingerType type); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/SingerTypeRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/SingerTypeRepository.java new file mode 100644 index 0000000..fb0b6a7 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/SingerTypeRepository.java @@ -0,0 +1,31 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.SingerType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 歌手类型数据访问层 + */ +@Repository +public interface SingerTypeRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据名称查找歌手类型 + * + * @param name 名称 + * @return 歌手类型 + */ + Optional findByName(String name); + + /** + * 判断名称是否存在 + * + * @param name 名称 + * @return 是否存在 + */ + boolean existsByName(String name); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/UserCollectMusicRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/UserCollectMusicRepository.java new file mode 100644 index 0000000..195166b --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/UserCollectMusicRepository.java @@ -0,0 +1,69 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.entity.UserCollectMusic; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 用户收藏音乐数据访问层 + */ +@Repository +public interface UserCollectMusicRepository extends JpaRepository { + + /** + * 根据用户查询收藏的音乐 + * + * @param user 用户 + * @param pageable 分页 + * @return 收藏的音乐列表 + */ + Page findByUser(User user, Pageable pageable); + + /** + * 根据用户和音乐查询收藏记录 + * + * @param user 用户 + * @param music 音乐 + * @return 收藏记录 + */ + Optional findByUserAndMusic(User user, Music music); + + /** + * 判断用户是否收藏了音乐 + * + * @param user 用户 + * @param music 音乐 + * @return 是否收藏 + */ + boolean existsByUserAndMusic(User user, Music music); + + /** + * 根据用户统计收藏的音乐数量 + * + * @param user 用户 + * @return 收藏的音乐数量 + */ + long countByUser(User user); + + /** + * 根据音乐统计被收藏的次数 + * + * @param music 音乐 + * @return 被收藏的次数 + */ + long countByMusic(Music music); + + /** + * 根据用户和音乐删除收藏记录 + * + * @param user 用户 + * @param music 音乐 + */ + void deleteByUserAndMusic(User user, Music music); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/UserCollectPlaylistRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/UserCollectPlaylistRepository.java new file mode 100644 index 0000000..e94c403 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/UserCollectPlaylistRepository.java @@ -0,0 +1,69 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Playlist; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.entity.UserCollectPlaylist; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 用户收藏歌单数据访问层 + */ +@Repository +public interface UserCollectPlaylistRepository extends JpaRepository { + + /** + * 根据用户查询收藏的歌单 + * + * @param user 用户 + * @param pageable 分页 + * @return 收藏的歌单列表 + */ + Page findByUser(User user, Pageable pageable); + + /** + * 根据用户和歌单查询收藏记录 + * + * @param user 用户 + * @param playlist 歌单 + * @return 收藏记录 + */ + Optional findByUserAndPlaylist(User user, Playlist playlist); + + /** + * 判断用户是否收藏了歌单 + * + * @param user 用户 + * @param playlist 歌单 + * @return 是否收藏 + */ + boolean existsByUserAndPlaylist(User user, Playlist playlist); + + /** + * 根据用户统计收藏的歌单数量 + * + * @param user 用户 + * @return 收藏的歌单数量 + */ + long countByUser(User user); + + /** + * 根据歌单统计被收藏的次数 + * + * @param playlist 歌单 + * @return 被收藏的次数 + */ + long countByPlaylist(Playlist playlist); + + /** + * 根据用户和歌单删除收藏记录 + * + * @param user 用户 + * @param playlist 歌单 + */ + void deleteByUserAndPlaylist(User user, Playlist playlist); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/UserCollectSingerRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/UserCollectSingerRepository.java new file mode 100644 index 0000000..dd901f7 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/UserCollectSingerRepository.java @@ -0,0 +1,69 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Singer; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.entity.UserCollectSinger; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 用户收藏歌手数据访问层 + */ +@Repository +public interface UserCollectSingerRepository extends JpaRepository { + + /** + * 根据用户查询收藏的歌手 + * + * @param user 用户 + * @param pageable 分页 + * @return 收藏的歌手列表 + */ + Page findByUser(User user, Pageable pageable); + + /** + * 根据用户和歌手查询收藏记录 + * + * @param user 用户 + * @param singer 歌手 + * @return 收藏记录 + */ + Optional findByUserAndSinger(User user, Singer singer); + + /** + * 判断用户是否收藏了歌手 + * + * @param user 用户 + * @param singer 歌手 + * @return 是否收藏 + */ + boolean existsByUserAndSinger(User user, Singer singer); + + /** + * 根据用户统计收藏的歌手数量 + * + * @param user 用户 + * @return 收藏的歌手数量 + */ + long countByUser(User user); + + /** + * 根据歌手统计被收藏的次数 + * + * @param singer 歌手 + * @return 被收藏的次数 + */ + long countBySinger(Singer singer); + + /** + * 根据用户和歌手删除收藏记录 + * + * @param user 用户 + * @param singer 歌手 + */ + void deleteByUserAndSinger(User user, Singer singer); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/UserMusicRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/UserMusicRepository.java new file mode 100644 index 0000000..e9cacf8 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/UserMusicRepository.java @@ -0,0 +1,69 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.entity.UserMusic; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * 用户音乐关联数据访问层 + */ +@Repository +public interface UserMusicRepository extends JpaRepository { + + /** + * 判断用户是否已收藏音乐 + * + * @param user 用户 + * @param music 音乐 + * @return 是否已收藏 + */ + boolean existsByUserAndMusic(User user, Music music); + + /** + * 判断用户是否已收藏音乐 + * + * @param userId 用户ID + * @param musicId 音乐ID + * @return 是否已收藏 + */ + boolean existsByUserIdAndMusicId(Long userId, Long musicId); + + /** + * 收藏音乐 + * + * @param userId 用户ID + * @param musicId 音乐ID + */ + @Modifying + @Transactional + @Query(value = "INSERT INTO user_music (user_id, music_id, collect_time) VALUES (?1, ?2, NOW())", nativeQuery = true) + void collectMusic(Long userId, Long musicId); + + /** + * 取消收藏音乐 + * + * @param userId 用户ID + * @param musicId 音乐ID + */ + @Modifying + @Transactional + @Query(value = "DELETE FROM user_music WHERE user_id = ?1 AND music_id = ?2", nativeQuery = true) + void uncollectMusic(Long userId, Long musicId); + + /** + * 获取用户收藏的音乐 + * + * @param userId 用户ID + * @param pageable 分页 + * @return 用户音乐关联分页 + */ + @Query("SELECT um FROM UserMusic um WHERE um.user.id = ?1") + Page findCollectedMusic(Long userId, Pageable pageable); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/UserPlaylistRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/UserPlaylistRepository.java new file mode 100644 index 0000000..6165226 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/UserPlaylistRepository.java @@ -0,0 +1,69 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Playlist; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.entity.UserPlaylist; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * 用户歌单关联数据访问层 + */ +@Repository +public interface UserPlaylistRepository extends JpaRepository { + + /** + * 判断用户是否已收藏歌单 + * + * @param user 用户 + * @param playlist 歌单 + * @return 是否已收藏 + */ + boolean existsByUserAndPlaylist(User user, Playlist playlist); + + /** + * 判断用户是否已收藏歌单 + * + * @param userId 用户ID + * @param playlistId 歌单ID + * @return 是否已收藏 + */ + boolean existsByUserIdAndPlaylistId(Long userId, Long playlistId); + + /** + * 收藏歌单 + * + * @param userId 用户ID + * @param playlistId 歌单ID + */ + @Modifying + @Transactional + @Query(value = "INSERT INTO user_playlist (user_id, playlist_id, collect_time) VALUES (?1, ?2, NOW())", nativeQuery = true) + void collectPlaylist(Long userId, Long playlistId); + + /** + * 取消收藏歌单 + * + * @param userId 用户ID + * @param playlistId 歌单ID + */ + @Modifying + @Transactional + @Query(value = "DELETE FROM user_playlist WHERE user_id = ?1 AND playlist_id = ?2", nativeQuery = true) + void uncollectPlaylist(Long userId, Long playlistId); + + /** + * 获取用户收藏的歌单 + * + * @param userId 用户ID + * @param pageable 分页 + * @return 歌单分页 + */ + @Query("SELECT up.playlist FROM UserPlaylist up WHERE up.user.id = ?1") + Page findCollectedPlaylist(Long userId, Pageable pageable); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/UserRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/UserRepository.java new file mode 100644 index 0000000..134147c --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/UserRepository.java @@ -0,0 +1,71 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.User; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * 用户数据访问层 + */ +@Repository +public interface UserRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据用户名查找用户 + * + * @param username 用户名 + * @return 用户 + */ + Optional findByUsername(String username); + + /** + * 根据用户名或邮箱查找用户 + * + * @param username 用户名 + * @param email 邮箱 + * @return 用户 + */ + Optional findByUsernameOrEmail(String username, String email); + + /** + * 判断用户名是否存在 + * + * @param username 用户名 + * @return 是否存在 + */ + boolean existsByUsername(String username); + + /** + * 判断邮箱是否存在 + * + * @param email 邮箱 + * @return 是否存在 + */ + boolean existsByEmail(String email); + + /** + * 统计指定时间范围内创建的用户数量 + * + * @param start 开始时间 + * @param end 结束时间 + * @return 用户数量 + */ + long countByCreateTimeBetween(LocalDateTime start, LocalDateTime end); + + /** + * 查询最新注册的用户 + * + * @param pageable 分页 + * @return 用户列表 + */ + @Query("SELECT u FROM User u ORDER BY u.createTime DESC") + List findLatestUsers(Pageable pageable); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/repository/UserSingerRepository.java b/music-houduan/src/main/java/com/test/musichouduan/repository/UserSingerRepository.java new file mode 100644 index 0000000..f452c7a --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/repository/UserSingerRepository.java @@ -0,0 +1,69 @@ +package com.test.musichouduan.repository; + +import com.test.musichouduan.entity.Singer; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.entity.UserSinger; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * 用户歌手关联数据访问层 + */ +@Repository +public interface UserSingerRepository extends JpaRepository { + + /** + * 判断用户是否已收藏歌手 + * + * @param user 用户 + * @param singer 歌手 + * @return 是否已收藏 + */ + boolean existsByUserAndSinger(User user, Singer singer); + + /** + * 判断用户是否已收藏歌手 + * + * @param userId 用户ID + * @param singerId 歌手ID + * @return 是否已收藏 + */ + boolean existsByUserIdAndSingerId(Long userId, Long singerId); + + /** + * 收藏歌手 + * + * @param userId 用户ID + * @param singerId 歌手ID + */ + @Modifying + @Transactional + @Query(value = "INSERT INTO user_singer (user_id, singer_id, collect_time) VALUES (?1, ?2, NOW())", nativeQuery = true) + void collectSinger(Long userId, Long singerId); + + /** + * 取消收藏歌手 + * + * @param userId 用户ID + * @param singerId 歌手ID + */ + @Modifying + @Transactional + @Query(value = "DELETE FROM user_singer WHERE user_id = ?1 AND singer_id = ?2", nativeQuery = true) + void uncollectSinger(Long userId, Long singerId); + + /** + * 获取用户收藏的歌手 + * + * @param userId 用户ID + * @param pageable 分页 + * @return 歌手分页 + */ + @Query("SELECT us.singer FROM UserSinger us WHERE us.user.id = ?1") + Page findCollectedSinger(Long userId, Pageable pageable); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/BannerService.java b/music-houduan/src/main/java/com/test/musichouduan/service/BannerService.java new file mode 100644 index 0000000..dba3882 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/BannerService.java @@ -0,0 +1,82 @@ +package com.test.musichouduan.service; + +import com.test.musichouduan.dto.BannerDTO; +import com.test.musichouduan.dto.PageResult; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 轮播图服务接口 + */ +public interface BannerService { + + /** + * 根据ID获取轮播图 + * + * @param id 轮播图ID + * @return 轮播图DTO + */ + BannerDTO getBannerById(Long id); + + /** + * 获取轮播图列表 + * + * @return 轮播图列表 + */ + List getBannerList(); + + /** + * 分页获取轮播图列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 分页结果 + */ + PageResult getBannerPage(Integer page, Integer size); + + /** + * 添加轮播图 + * + * @param bannerDTO 轮播图DTO + * @param image 图片文件 + * @return 轮播图DTO + */ + BannerDTO addBanner(BannerDTO bannerDTO, MultipartFile image); + + /** + * 更新轮播图 + * + * @param id 轮播图ID + * @param bannerDTO 轮播图DTO + * @param image 图片文件 + * @return 轮播图DTO + */ + BannerDTO updateBanner(Long id, BannerDTO bannerDTO, MultipartFile image); + + /** + * 删除轮播图 + * + * @param id 轮播图ID + * @return 是否成功 + */ + boolean deleteBanner(Long id); + + /** + * 更新轮播图状态 + * + * @param id 轮播图ID + * @param status 状态 + * @return 是否成功 + */ + boolean updateBannerStatus(Long id, Integer status); + + /** + * 更新轮播图排序 + * + * @param id 轮播图ID + * @param orderNum 排序号 + * @return 是否成功 + */ + boolean updateBannerOrder(Long id, Integer orderNum); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/CommentService.java b/music-houduan/src/main/java/com/test/musichouduan/service/CommentService.java new file mode 100644 index 0000000..0fff4bb --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/CommentService.java @@ -0,0 +1,84 @@ +package com.test.musichouduan.service; + +import com.test.musichouduan.dto.CommentDTO; +import com.test.musichouduan.dto.PageResult; + +/** + * 评论服务接口 + */ +public interface CommentService { + + /** + * 根据ID获取评论 + * + * @param id 评论ID + * @return 评论DTO + */ + CommentDTO getCommentById(Long id); + + /** + * 分页获取评论列表 + * + * @param targetId 目标ID + * @param targetType 目标类型 + * @param page 页码 + * @param size 每页大小 + * @return 分页结果 + */ + PageResult getCommentList(Long targetId, Integer targetType, Integer page, Integer size); + + /** + * 添加评论 + * + * @param commentDTO 评论DTO + * @return 评论DTO + */ + CommentDTO addComment(CommentDTO commentDTO); + + /** + * 删除评论 + * + * @param id 评论ID + * @return 是否成功 + */ + boolean deleteComment(Long id); + + /** + * 获取用户的评论列表 + * + * @param userId 用户ID + * @param page 页码 + * @param size 每页大小 + * @return 分页结果 + */ + PageResult getUserCommentList(Long userId, Integer page, Integer size); + + /** + * 管理员获取所有评论列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @param targetType 目标类型 + * @param status 状态 + * @return 分页结果 + */ + PageResult getAdminCommentList(Integer page, Integer size, String keyword, Integer targetType, Integer status); + + /** + * 更新评论状态 + * + * @param id 评论ID + * @param status 状态 + * @return 是否成功 + */ + boolean updateCommentStatus(Long id, Integer status); + + /** + * 管理员删除评论 + * + * @param id 评论ID + * @return 是否成功 + */ + boolean adminDeleteComment(Long id); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/FileService.java b/music-houduan/src/main/java/com/test/musichouduan/service/FileService.java new file mode 100644 index 0000000..deaea78 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/FileService.java @@ -0,0 +1,41 @@ +package com.test.musichouduan.service; + +import org.springframework.web.multipart.MultipartFile; + +/** + * 文件服务接口 + */ +public interface FileService { + + /** + * 上传图片 + * + * @param file 图片文件 + * @return 图片URL + */ + String uploadImage(MultipartFile file); + + /** + * 上传音乐 + * + * @param file 音乐文件 + * @return 音乐URL + */ + String uploadMusic(MultipartFile file); + + /** + * 上传歌词 + * + * @param file 歌词文件 + * @return 歌词内容 + */ + String uploadLyric(MultipartFile file); + + /** + * 删除文件 + * + * @param url 文件URL + * @return 是否成功 + */ + boolean deleteFile(String url); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java b/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java new file mode 100644 index 0000000..86f49e4 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java @@ -0,0 +1,124 @@ +package com.test.musichouduan.service; + +import com.test.musichouduan.dto.MusicDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.entity.Music; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 音乐服务接口 + */ +public interface MusicService { + + /** + * 根据ID获取音乐 + * + * @param id 音乐ID + * @return 音乐DTO + */ + MusicDTO getMusicById(Long id); + + /** + * 分页获取音乐列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @param typeId 类型ID + * @return 分页结果 + */ + PageResult getMusicList(Integer page, Integer size, String keyword, Long typeId); + + /** + * 根据歌手ID获取音乐列表 + * + * @param singerId 歌手ID + * @param page 页码 + * @param size 每页大小 + * @return 分页结果 + */ + PageResult getMusicListBySinger(Long singerId, Integer page, Integer size); + + /** + * 获取最新音乐列表 + * + * @param limit 限制数量 + * @return 音乐列表 + */ + List getLatestMusic(Integer limit); + + /** + * 获取热门音乐列表 + * + * @param limit 限制数量 + * @return 音乐列表 + */ + List getHotMusic(Integer limit); + + /** + * 添加音乐 + * + * @param musicDTO 音乐DTO + * @param file 音乐文件 + * @param cover 封面文件 + * @return 音乐DTO + */ + MusicDTO addMusic(MusicDTO musicDTO, MultipartFile file, MultipartFile cover); + + /** + * 更新音乐 + * + * @param id 音乐ID + * @param musicDTO 音乐DTO + * @param file 音乐文件 + * @param cover 封面文件 + * @param lyric 歌词文件 + * @return 音乐DTO + */ + MusicDTO updateMusic(Long id, MusicDTO musicDTO, MultipartFile file, MultipartFile cover, MultipartFile lyric); + + /** + * 删除音乐 + * + * @param id 音乐ID + * @return 是否成功 + */ + boolean deleteMusic(Long id); + + /** + * 收藏/取消收藏音乐 + * + * @param id 音乐ID + * @param userId 用户ID + * @return 是否收藏成功 + */ + boolean toggleCollectMusic(Long id, Long userId); + + /** + * 获取用户收藏的音乐列表 + * + * @param userId 用户ID + * @param page 页码 + * @param size 每页大小 + * @return 分页结果 + */ + PageResult getCollectedMusic(Long userId, Integer page, Integer size); + + /** + * 播放音乐 + * + * @param id 音乐ID + * @return 音乐DTO + */ + MusicDTO playMusic(Long id); + + /** + * 根据ID获取音乐实体 + * + * @param id 音乐ID + * @return 音乐实体 + */ + Music getById(Long id); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/PlaylistService.java b/music-houduan/src/main/java/com/test/musichouduan/service/PlaylistService.java new file mode 100644 index 0000000..612d802 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/PlaylistService.java @@ -0,0 +1,153 @@ +package com.test.musichouduan.service; + +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.PlaylistDTO; +import com.test.musichouduan.entity.Playlist; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 歌单服务接口 + */ +public interface PlaylistService { + + /** + * 根据ID获取歌单 + * + * @param id 歌单ID + * @return 歌单DTO + */ + PlaylistDTO getPlaylistById(Long id); + + /** + * 分页获取歌单列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @param typeId 类型ID + * @return 分页结果 + */ + PageResult getPlaylistList(Integer page, Integer size, String keyword, Long typeId); + + /** + * 获取最新歌单列表 + * + * @param limit 限制数量 + * @return 歌单列表 + */ + List getLatestPlaylist(Integer limit); + + /** + * 获取热门歌单列表 + * + * @param limit 限制数量 + * @return 歌单列表 + */ + List getHotPlaylist(Integer limit); + + /** + * 获取官方歌单列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 分页结果 + */ + PageResult getOfficialPlaylist(Integer page, Integer size); + + /** + * 添加歌单 + * + * @param playlistDTO 歌单DTO + * @param cover 封面文件 + * @return 歌单DTO + */ + PlaylistDTO addPlaylist(PlaylistDTO playlistDTO, MultipartFile cover); + + /** + * 更新歌单 + * + * @param id 歌单ID + * @param playlistDTO 歌单DTO + * @param cover 封面文件 + * @return 歌单DTO + */ + PlaylistDTO updatePlaylist(Long id, PlaylistDTO playlistDTO, MultipartFile cover); + + /** + * 删除歌单 + * + * @param id 歌单ID + * @return 是否成功 + */ + boolean deletePlaylist(Long id); + + /** + * 收藏歌单 + * + * @param id 歌单ID + * @return 是否成功 + */ + boolean collectPlaylist(Long id); + + /** + * 取消收藏歌单 + * + * @param id 歌单ID + * @return 是否成功 + */ + boolean uncollectPlaylist(Long id); + + /** + * 获取用户收藏的歌单列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 分页结果 + */ + PageResult getCollectedPlaylist(Integer page, Integer size); + + /** + * 获取用户创建的歌单列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 分页结果 + */ + PageResult getUserPlaylist(Integer page, Integer size); + + /** + * 向歌单添加音乐 + * + * @param id 歌单ID + * @param musicId 音乐ID + * @return 是否成功 + */ + boolean addMusicToPlaylist(Long id, Long musicId); + + /** + * 从歌单移除音乐 + * + * @param id 歌单ID + * @param musicId 音乐ID + * @return 是否成功 + */ + boolean removeMusicFromPlaylist(Long id, Long musicId); + + /** + * 播放歌单 + * + * @param id 歌单ID + * @return 歌单DTO + */ + PlaylistDTO playPlaylist(Long id); + + /** + * 根据ID获取歌单实体 + * + * @param id 歌单ID + * @return 歌单实体 + */ + Playlist getById(Long id); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/SingerService.java b/music-houduan/src/main/java/com/test/musichouduan/service/SingerService.java new file mode 100644 index 0000000..04568bc --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/SingerService.java @@ -0,0 +1,125 @@ +package com.test.musichouduan.service; + +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.SingerDTO; +import com.test.musichouduan.entity.Singer; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 歌手服务接口 + */ +public interface SingerService { + + /** + * 根据ID获取歌手 + * + * @param id 歌手ID + * @return 歌手DTO + */ + SingerDTO getSingerById(Long id); + + /** + * 分页获取歌手列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @param typeId 类型ID + * @return 分页结果 + */ + PageResult getSingerList(Integer page, Integer size, String keyword, Long typeId); + + /** + * 获取最新歌手列表 + * + * @param limit 限制数量 + * @return 歌手列表 + */ + List getLatestSinger(Integer limit); + + /** + * 获取热门歌手列表 + * + * @param limit 限制数量 + * @return 歌手列表 + */ + List getHotSinger(Integer limit); + + /** + * 添加歌手 + * + * @param singerDTO 歌手DTO + * @param avatar 头像文件 + * @return 歌手DTO + */ + SingerDTO addSinger(SingerDTO singerDTO, MultipartFile avatar); + + /** + * 更新歌手 + * + * @param id 歌手ID + * @param singerDTO 歌手DTO + * @param avatar 头像文件 + * @return 歌手DTO + */ + SingerDTO updateSinger(Long id, SingerDTO singerDTO, MultipartFile avatar); + + /** + * 删除歌手 + * + * @param id 歌手ID + * @return 是否成功 + */ + boolean deleteSinger(Long id); + + /** + * 收藏歌手 + * + * @param id 歌手ID + * @return 是否成功 + */ + boolean collectSinger(Long id); + + /** + * 取消收藏歌手 + * + * @param id 歌手ID + * @return 是否成功 + */ + boolean uncollectSinger(Long id); + + /** + * 获取用户收藏的歌手列表 + * + * @param page 页码 + * @param size 每页大小 + * @return 分页结果 + */ + PageResult getCollectedSinger(Integer page, Integer size); + + /** + * 添加音乐到歌手 + * @param singerId 歌手ID + * @param musicId 音乐ID + * @return 是否成功 + */ + boolean addMusicToSinger(Long singerId, Long musicId); + + /** + * 从歌手移除音乐 + * @param singerId 歌手ID + * @param musicId 音乐ID + * @return 是否成功 + */ + boolean removeMusicFromSinger(Long singerId, Long musicId); + + /** + * 根据ID获取歌手实体 + * + * @param id 歌手ID + * @return 歌手实体 + */ + Singer getById(Long id); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/StatService.java b/music-houduan/src/main/java/com/test/musichouduan/service/StatService.java new file mode 100644 index 0000000..dca9358 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/StatService.java @@ -0,0 +1,55 @@ +package com.test.musichouduan.service; + +import java.util.Map; + +/** + * 统计服务接口 + */ +public interface StatService { + + /** + * 获取总体统计数据 + * + * @return 统计数据 + */ + Map getTotalStats(); + + /** + * 获取用户增长趋势 + * + * @param days 天数 + * @return 统计数据 + */ + Map getUserGrowthTrend(Integer days); + + /** + * 获取音乐类型分布 + * + * @return 统计数据 + */ + Map getMusicTypeDistribution(); + + /** + * 获取播放量统计 + * + * @param days 天数 + * @return 统计数据 + */ + Map getPlayCountStats(Integer days); + + /** + * 获取最新用户列表 + * + * @param limit 限制数量 + * @return 统计数据 + */ + Map getLatestUsers(Integer limit); + + /** + * 获取最新音乐列表 + * + * @param limit 限制数量 + * @return 统计数据 + */ + Map getLatestMusics(Integer limit); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/TypeService.java b/music-houduan/src/main/java/com/test/musichouduan/service/TypeService.java new file mode 100644 index 0000000..ab5b862 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/TypeService.java @@ -0,0 +1,140 @@ +package com.test.musichouduan.service; + +import com.test.musichouduan.dto.MusicTypeDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.PlaylistTypeDTO; +import com.test.musichouduan.dto.SingerTypeDTO; + +import java.util.List; + +/** + * 类型服务接口 + */ +public interface TypeService { + + /** + * 获取音乐类型列表 + * + * @return 音乐类型列表 + */ + List getMusicTypeList(); + + /** + * 分页获取音乐类型列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @return 分页结果 + */ + PageResult getMusicTypePage(Integer page, Integer size, String keyword); + + /** + * 添加音乐类型 + * + * @param musicTypeDTO 音乐类型DTO + * @return 音乐类型DTO + */ + MusicTypeDTO addMusicType(MusicTypeDTO musicTypeDTO); + + /** + * 更新音乐类型 + * + * @param id 音乐类型ID + * @param musicTypeDTO 音乐类型DTO + * @return 音乐类型DTO + */ + MusicTypeDTO updateMusicType(Long id, MusicTypeDTO musicTypeDTO); + + /** + * 删除音乐类型 + * + * @param id 音乐类型ID + * @return 是否成功 + */ + boolean deleteMusicType(Long id); + + /** + * 获取歌手类型列表 + * + * @return 歌手类型列表 + */ + List getSingerTypeList(); + + /** + * 分页获取歌手类型列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @return 分页结果 + */ + PageResult getSingerTypePage(Integer page, Integer size, String keyword); + + /** + * 添加歌手类型 + * + * @param singerTypeDTO 歌手类型DTO + * @return 歌手类型DTO + */ + SingerTypeDTO addSingerType(SingerTypeDTO singerTypeDTO); + + /** + * 更新歌手类型 + * + * @param id 歌手类型ID + * @param singerTypeDTO 歌手类型DTO + * @return 歌手类型DTO + */ + SingerTypeDTO updateSingerType(Long id, SingerTypeDTO singerTypeDTO); + + /** + * 删除歌手类型 + * + * @param id 歌手类型ID + * @return 是否成功 + */ + boolean deleteSingerType(Long id); + + /** + * 获取歌单类型列表 + * + * @return 歌单类型列表 + */ + List getPlaylistTypeList(); + + /** + * 分页获取歌单类型列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @return 分页结果 + */ + PageResult getPlaylistTypePage(Integer page, Integer size, String keyword); + + /** + * 添加歌单类型 + * + * @param playlistTypeDTO 歌单类型DTO + * @return 歌单类型DTO + */ + PlaylistTypeDTO addPlaylistType(PlaylistTypeDTO playlistTypeDTO); + + /** + * 更新歌单类型 + * + * @param id 歌单类型ID + * @param playlistTypeDTO 歌单类型DTO + * @return 歌单类型DTO + */ + PlaylistTypeDTO updatePlaylistType(Long id, PlaylistTypeDTO playlistTypeDTO); + + /** + * 删除歌单类型 + * + * @param id 歌单类型ID + * @return 是否成功 + */ + boolean deletePlaylistType(Long id); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/UserService.java b/music-houduan/src/main/java/com/test/musichouduan/service/UserService.java new file mode 100644 index 0000000..25b4119 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/UserService.java @@ -0,0 +1,117 @@ +package com.test.musichouduan.service; + +import com.test.musichouduan.dto.*; +import com.test.musichouduan.entity.User; + +/** + * 用户服务接口 + */ +public interface UserService { + + /** + * 用户注册 + * + * @param request 注册请求 + * @return 用户DTO + */ + UserDTO register(RegisterRequest request); + + /** + * 用户登录 + * + * @param request 登录请求 + * @return 登录结果DTO,包含用户信息和令牌信息 + */ + LoginResultDTO login(LoginRequest request); + + /** + * 获取当前登录用户 + * + * @return 用户DTO + */ + UserDTO getCurrentUser(); + + /** + * 根据ID获取用户 + * + * @param id 用户ID + * @return 用户DTO + */ + UserDTO getUserById(Long id); + + /** + * 根据用户名获取用户 + * + * @param username 用户名 + * @return 用户DTO + */ + UserDTO getUserByUsername(String username); + + /** + * 更新用户信息 + * + * @param request 用户信息更新请求 + * @return 用户DTO + */ + UserDTO updateUser(UserUpdateRequest request); + + /** + * 修改密码 + * + * @param request 修改密码请求 + * @return 是否成功 + */ + boolean updatePassword(PasswordUpdateRequest request); + + /** + * 获取用户统计数据 + * + * @param userId 用户ID + * @return 用户DTO + */ + UserDTO getUserStats(Long userId); + + /** + * 分页获取用户列表 + * + * @param page 页码 + * @param size 每页大小 + * @param keyword 关键字 + * @return 分页结果 + */ + PageResult getUserList(Integer page, Integer size, String keyword); + + /** + * 根据ID删除用户 + * + * @param id 用户ID + * @return 是否成功 + */ + boolean deleteUser(Long id); + + /** + * 更新用户状态 + * + * @param id 用户ID + * @param status 状态 + * @return 是否成功 + */ + boolean updateUserStatus(Long id, Integer status); + + /** + * 管理员更新用户信息 + * + * @param id 用户ID + * @param request 用户信息更新请求 + * @return 用户DTO + */ + UserDTO adminUpdateUser(Long id, UserUpdateRequest request); + + /** + * 根据ID获取用户实体 + * + * @param id 用户ID + * @return 用户实体 + */ + User getById(Long id); +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/BannerServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/BannerServiceImpl.java new file mode 100644 index 0000000..98e297a --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/BannerServiceImpl.java @@ -0,0 +1,186 @@ +package com.test.musichouduan.service.impl; + +import com.test.musichouduan.dto.BannerDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.entity.Banner; +import com.test.musichouduan.exception.BusinessException; +import com.test.musichouduan.repository.BannerRepository; +import com.test.musichouduan.service.BannerService; +import com.test.musichouduan.service.FileService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 轮播图服务实现类 + */ +@Service +public class BannerServiceImpl implements BannerService { + + @Autowired + private BannerRepository bannerRepository; + + @Autowired + private FileService fileService; + + @Override + public BannerDTO getBannerById(Long id) { + Banner banner = bannerRepository.findById(id) + .orElseThrow(() -> new BusinessException("轮播图不存在")); + return convertToDTO(banner); + } + + @Override + public List getBannerList() { + // 只获取启用状态的轮播图,并按排序号排序 + List banners = bannerRepository.findByStatusOrderByOrderNumAsc(1); + return banners.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public PageResult getBannerPage(Integer page, Integer size) { + Page bannerPage = bannerRepository.findAll( + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.ASC, "orderNum")) + ); + + List bannerDTOs = bannerPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + bannerDTOs, + bannerPage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public BannerDTO addBanner(BannerDTO bannerDTO, MultipartFile image) { + // 上传图片 + String imageUrl = fileService.uploadImage(image); + + // 创建轮播图实体 + Banner banner = new Banner(); + BeanUtils.copyProperties(bannerDTO, banner); + banner.setImageUrl(imageUrl); + banner.setStatus(banner.getStatus() != null ? banner.getStatus() : 1); + + // 保存轮播图 + Banner savedBanner = bannerRepository.save(banner); + + return convertToDTO(savedBanner); + } + + @Override + @Transactional + public BannerDTO updateBanner(Long id, BannerDTO bannerDTO, MultipartFile image) { + // 获取轮播图 + Banner banner = bannerRepository.findById(id) + .orElseThrow(() -> new BusinessException("轮播图不存在")); + + // 如果有新图片,则上传并更新URL + if (image != null && !image.isEmpty()) { + // 删除旧图片 + if (banner.getImageUrl() != null) { + fileService.deleteFile(banner.getImageUrl()); + } + + // 上传新图片 + String imageUrl = fileService.uploadImage(image); + banner.setImageUrl(imageUrl); + } + + // 更新轮播图信息 + banner.setTitle(bannerDTO.getTitle()); + banner.setTargetType(bannerDTO.getTargetType()); + banner.setTargetId(bannerDTO.getTargetId()); + banner.setLinkUrl(bannerDTO.getLinkUrl()); + if (bannerDTO.getOrderNum() != null) { + banner.setOrderNum(bannerDTO.getOrderNum()); + } + if (bannerDTO.getStatus() != null) { + banner.setStatus(bannerDTO.getStatus()); + } + + // 保存轮播图 + Banner updatedBanner = bannerRepository.save(banner); + + return convertToDTO(updatedBanner); + } + + @Override + @Transactional + public boolean deleteBanner(Long id) { + // 获取轮播图 + Banner banner = bannerRepository.findById(id) + .orElseThrow(() -> new BusinessException("轮播图不存在")); + + // 删除图片 + if (banner.getImageUrl() != null) { + fileService.deleteFile(banner.getImageUrl()); + } + + // 删除轮播图 + bannerRepository.delete(banner); + + return true; + } + + @Override + @Transactional + public boolean updateBannerStatus(Long id, Integer status) { + // 获取轮播图 + Banner banner = bannerRepository.findById(id) + .orElseThrow(() -> new BusinessException("轮播图不存在")); + + // 更新状态 + banner.setStatus(status); + + // 保存轮播图 + bannerRepository.save(banner); + + return true; + } + + @Override + @Transactional + public boolean updateBannerOrder(Long id, Integer orderNum) { + // 获取轮播图 + Banner banner = bannerRepository.findById(id) + .orElseThrow(() -> new BusinessException("轮播图不存在")); + + // 更新排序号 + banner.setOrderNum(orderNum); + + // 保存轮播图 + bannerRepository.save(banner); + + return true; + } + + /** + * 将实体转换为DTO + * + * @param banner 轮播图实体 + * @return 轮播图DTO + */ + private BannerDTO convertToDTO(Banner banner) { + BannerDTO bannerDTO = new BannerDTO(); + BeanUtils.copyProperties(banner, bannerDTO); + // 处理字段名不一致的情况 + // 不需要手动设置,因为BeanUtils会自动匹配相同名称的属性 + return bannerDTO; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/CommentServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/CommentServiceImpl.java new file mode 100644 index 0000000..e06ccfb --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/CommentServiceImpl.java @@ -0,0 +1,268 @@ +package com.test.musichouduan.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import com.test.musichouduan.dto.CommentDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.UserDTO; +import com.test.musichouduan.entity.Comment; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.exception.BusinessException; +import com.test.musichouduan.repository.CommentRepository; +import com.test.musichouduan.repository.UserRepository; +import com.test.musichouduan.service.CommentService; +import org.springframework.messaging.simp.SimpMessagingTemplate; // 添加导入 +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.criteria.Predicate; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 评论服务实现类 + */ +@Service +public class CommentServiceImpl implements CommentService { + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private SimpMessagingTemplate messagingTemplate; // 注入 SimpMessagingTemplate + + @Override + public CommentDTO getCommentById(Long id) { + Comment comment = commentRepository.findById(id) + .orElseThrow(() -> new BusinessException("评论不存在")); + return convertToDTO(comment); + } + + @Override + public PageResult getCommentList(Long targetId, Integer targetType, Integer page, Integer size) { + // 获取父评论 + Page commentPage = commentRepository.findByTargetIdAndTargetTypeAndParentIdIsNullOrderByCreateTimeDesc( + targetId, targetType, PageRequest.of(page - 1, size) + ); + + // 转换为DTO + List commentDTOs = commentPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + // 获取子评论 + for (CommentDTO commentDTO : commentDTOs) { + List children = commentRepository.findByParentIdOrderByCreateTimeAsc(commentDTO.getId()); + if (!children.isEmpty()) { + commentDTO.setChildren(children.stream() + .map(this::convertToDTO) + .collect(Collectors.toList())); + } else { + commentDTO.setChildren(new ArrayList<>()); + } + } + + return new PageResult<>( + commentDTOs, + commentPage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public CommentDTO addComment(CommentDTO commentDTO) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 创建评论实体 + Comment comment = new Comment(); + comment.setContent(commentDTO.getContent()); + comment.setUser(user); + comment.setTargetId(commentDTO.getTargetId()); + comment.setTargetType(commentDTO.getTargetType()); + comment.setParentId(commentDTO.getParentId()); + comment.setStatus(1); // 默认状态为正常 + + // 保存评论 + Comment savedComment = commentRepository.save(comment); + + // 将新评论转换为DTO + CommentDTO savedCommentDTO = convertToDTO(savedComment); + + // 构建 WebSocket 主题路径 + String destination = String.format("/topic/comments/%d/%d", savedComment.getTargetType(), savedComment.getTargetId()); + + // 通过 WebSocket 发送新评论 + messagingTemplate.convertAndSend(destination, savedCommentDTO); + + return savedCommentDTO; // 返回转换后的 DTO + } + + @Override + @Transactional + public boolean deleteComment(Long id) { + // 获取评论 + Comment comment = commentRepository.findById(id) + .orElseThrow(() -> new BusinessException("评论不存在")); + + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 检查是否是评论作者或管理员 + if (!comment.getUser().getId().equals(userId) && !StpUtil.hasRole("admin")) { + throw new BusinessException("无权删除该评论"); + } + + // 删除评论 + commentRepository.delete(comment); + + // 如果是父评论,同时删除子评论 + if (comment.getParentId() == null) { + List children = commentRepository.findByParentIdOrderByCreateTimeAsc(id); + if (!children.isEmpty()) { + commentRepository.deleteAll(children); + } + } + + return true; + } + + @Override + public PageResult getUserCommentList(Long userId, Integer page, Integer size) { + // 获取用户 + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取用户评论 + Page commentPage = commentRepository.findByUserOrderByCreateTimeDesc( + user, PageRequest.of(page - 1, size) + ); + + // 转换为DTO + List commentDTOs = commentPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + commentDTOs, + commentPage.getTotalElements(), + page, + size + ); + } + + @Override + public PageResult getAdminCommentList(Integer page, Integer size, String keyword, Integer targetType, Integer status) { + // 构建查询条件 + Specification spec = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + // 关键字搜索 + if (keyword != null && !keyword.isEmpty()) { + predicates.add(criteriaBuilder.like(root.get("content"), "%" + keyword + "%")); + } + + // 目标类型筛选 + if (targetType != null) { + predicates.add(criteriaBuilder.equal(root.get("targetType"), targetType)); + } + + // 状态筛选 + if (status != null) { + predicates.add(criteriaBuilder.equal(root.get("status"), status)); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + + // 执行查询 + Page commentPage = commentRepository.findAll( + spec, PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List commentDTOs = commentPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + commentDTOs, + commentPage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public boolean updateCommentStatus(Long id, Integer status) { + // 获取评论 + Comment comment = commentRepository.findById(id) + .orElseThrow(() -> new BusinessException("评论不存在")); + + // 更新状态 + comment.setStatus(status); + commentRepository.save(comment); + + return true; + } + + @Override + @Transactional + public boolean adminDeleteComment(Long id) { + // 获取评论 + Comment comment = commentRepository.findById(id) + .orElseThrow(() -> new BusinessException("评论不存在")); + + // 删除评论 + commentRepository.delete(comment); + + // 如果是父评论,同时删除子评论 + if (comment.getParentId() == null) { + List children = commentRepository.findByParentIdOrderByCreateTimeAsc(id); + if (!children.isEmpty()) { + commentRepository.deleteAll(children); + } + } + + return true; + } + + /** + * 将实体转换为DTO + * + * @param comment 评论实体 + * @return 评论DTO + */ + private CommentDTO convertToDTO(Comment comment) { + CommentDTO commentDTO = new CommentDTO(); + BeanUtils.copyProperties(comment, commentDTO); + + // 设置用户信息 + UserDTO userDTO = new UserDTO(); + BeanUtils.copyProperties(comment.getUser(), userDTO); + + // 确保头像URL是完整路径 + if (userDTO.getAvatar() != null && !userDTO.getAvatar().startsWith("http")) { + userDTO.setAvatar("http://localhost:8085" + userDTO.getAvatar()); + } + + commentDTO.setUser(userDTO); + + return commentDTO; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/FileServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..18302e9 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/FileServiceImpl.java @@ -0,0 +1,168 @@ +package com.test.musichouduan.service.impl; + +import com.test.musichouduan.exception.BusinessException; +import com.test.musichouduan.service.FileService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** + * 文件服务实现类 + */ +@Service +public class FileServiceImpl implements FileService { + + private static final Logger logger = LoggerFactory.getLogger(FileServiceImpl.class); + + @Value("${file.upload-path}") + private String uploadPath; + + @Value("${file.access-path}") + private String accessPath; + + @Value("${file.allowed-types}") + private String allowedTypes; + + @Override + public String uploadImage(MultipartFile file) { + // 检查文件是否为空 + if (file.isEmpty()) { + throw new BusinessException("上传文件不能为空"); + } + + // 检查文件类型 + String originalFilename = file.getOriginalFilename(); + String extension = getFileExtension(originalFilename); + List imageTypes = Arrays.asList("jpg", "jpeg", "png", "gif"); + if (!imageTypes.contains(extension.toLowerCase())) { + throw new BusinessException("只能上传jpg、jpeg、png、gif格式的图片"); + } + + // 上传文件 + return uploadFile(file, "images"); + } + + @Override + public String uploadMusic(MultipartFile file) { + // 检查文件是否为空 + if (file.isEmpty()) { + throw new BusinessException("上传文件不能为空"); + } + + // 检查文件类型 + String originalFilename = file.getOriginalFilename(); + String extension = getFileExtension(originalFilename); + List musicTypes = Arrays.asList("mp3", "wav"); + if (!musicTypes.contains(extension.toLowerCase())) { + throw new BusinessException("只能上传mp3、wav格式的音乐"); + } + + // 上传文件 + return uploadFile(file, "musics"); + } + + @Override + public String uploadLyric(MultipartFile file) { + // 检查文件是否为空 + if (file.isEmpty()) { + throw new BusinessException("上传文件不能为空"); + } + + // 检查文件类型 + String originalFilename = file.getOriginalFilename(); + String extension = getFileExtension(originalFilename); + List lyricTypes = Arrays.asList("lrc", "txt"); + if (!lyricTypes.contains(extension.toLowerCase())) { + throw new BusinessException("只能上传lrc、txt格式的歌词"); + } + + try { + // 读取文件内容 + byte[] bytes = file.getBytes(); + String content = new String(bytes, StandardCharsets.UTF_8); + return content; + } catch (IOException e) { + logger.error("读取歌词文件失败", e); + throw new BusinessException("读取歌词文件失败"); + } + } + + @Override + public boolean deleteFile(String url) { + if (StringUtils.isEmpty(url)) { + return false; + } + + // 从URL中提取文件路径 + String filePath = url.replace(accessPath, ""); + Path path = Paths.get(uploadPath, filePath); + + try { + // 删除文件 + return Files.deleteIfExists(path); + } catch (IOException e) { + logger.error("删除文件失败", e); + return false; + } + } + + /** + * 上传文件 + * + * @param file 文件 + * @param directory 目录 + * @return 文件URL + */ + private String uploadFile(MultipartFile file, String directory) { + // 生成文件名 + String originalFilename = file.getOriginalFilename(); + String extension = getFileExtension(originalFilename); + String fileName = UUID.randomUUID().toString() + "." + extension; + + // 创建目录 + File dir = new File(uploadPath + directory); + if (!dir.exists()) { + dir.mkdirs(); + } + + // 保存文件 + try { + Path path = Paths.get(uploadPath, directory, fileName); + Files.write(path, file.getBytes()); + return "http://localhost:8085" + accessPath + directory + "/" + fileName; + } catch (IOException e) { + logger.error("上传文件失败", e); + throw new BusinessException("上传文件失败"); + } + } + + /** + * 获取文件扩展名 + * + * @param fileName 文件名 + * @return 扩展名 + */ + private String getFileExtension(String fileName) { + if (fileName == null) { + return ""; + } + int dotIndex = fileName.lastIndexOf("."); + if (dotIndex == -1) { + return ""; + } + return fileName.substring(dotIndex + 1); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java new file mode 100644 index 0000000..02acb1d --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java @@ -0,0 +1,504 @@ +package com.test.musichouduan.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import com.test.musichouduan.dto.MusicDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.entity.MusicType; +import com.test.musichouduan.entity.Singer; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.entity.UserMusic; +import com.test.musichouduan.exception.BusinessException; +import com.test.musichouduan.repository.*; +import com.test.musichouduan.service.FileService; +import com.test.musichouduan.service.MusicService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Optional; + +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 音乐服务实现类 + */ +@Service +public class MusicServiceImpl implements MusicService { + + @Autowired + private MusicRepository musicRepository; + + @Autowired + private MusicTypeRepository musicTypeRepository; + + @Autowired + private SingerRepository singerRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserMusicRepository userMusicRepository; + + @Autowired + private PlayHistoryRepository playHistoryRepository; + + @Autowired + private FileService fileService; + + @Override + public MusicDTO getMusicById(Long id) { + // 查找音乐,如果不存在则返回null + return musicRepository.findById(id) + .map(this::convertToDTO) + .orElse(null); + } + + @Override + public PageResult getMusicList(Integer page, Integer size, String keyword, Long typeId) { + // 构建查询条件 + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + + // 关键字查询 + if (StringUtils.hasText(keyword)) { + predicates.add(cb.like(root.get("name"), "%" + keyword + "%")); + } + + // 类型查询 + if (typeId != null) { + predicates.add(cb.isMember( + musicTypeRepository.findById(typeId).orElse(null), + root.get("types") + )); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + // 执行查询 + Page musicPage = musicRepository.findAll( + spec, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List musicDTOs = musicPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + musicDTOs, + musicPage.getTotalElements(), + page, + size + ); + } + + @Override + public PageResult getMusicListBySinger(Long singerId, Integer page, Integer size) { + // 获取歌手 + Singer singer = singerRepository.findById(singerId) + .orElseThrow(() -> new BusinessException("歌手不存在")); + + // 查询歌手的音乐 + Page musicPage = musicRepository.findBySingersContaining( + singer, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List musicDTOs = musicPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + musicDTOs, + musicPage.getTotalElements(), + page, + size + ); + } + + @Override + public List getLatestMusic(Integer limit) { + // 查询最新音乐 + List musics = musicRepository.findLatest(PageRequest.of(0, limit)); + + // 转换为DTO + return musics.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public List getHotMusic(Integer limit) { + // 查询热门音乐 + List musics = musicRepository.findMostPlayed(PageRequest.of(0, limit)); + + // 转换为DTO + return musics.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public MusicDTO addMusic(MusicDTO musicDTO, MultipartFile file, MultipartFile cover) { + // 上传音乐文件 + String musicUrl = fileService.uploadMusic(file); + + // 上传封面 + String coverUrl = fileService.uploadImage(cover); + + // 创建音乐实体 + Music music = new Music(); + BeanUtils.copyProperties(musicDTO, music); + music.setUrl(musicUrl); + music.setCover(coverUrl); + music.setPlayCount(0L); + + // 设置歌词 + if (StringUtils.hasText(musicDTO.getLyric())) { + music.setLyric(musicDTO.getLyric()); + } + + // 设置音乐类型 + if (musicDTO.getTypeIds() != null && !musicDTO.getTypeIds().isEmpty()) { + Set types = new HashSet<>(); + for (Long typeId : musicDTO.getTypeIds()) { + MusicType type = musicTypeRepository.findById(typeId) + .orElseThrow(() -> new BusinessException("音乐类型不存在")); + types.add(type); + } + music.setTypes(types); + } + + // 设置歌手 + if (musicDTO.getSingerIds() != null && !musicDTO.getSingerIds().isEmpty()) { + // 如果有歌手ID列表,优先使用 + Set singers = new HashSet<>(); + for (Long singerId : musicDTO.getSingerIds()) { + Singer singer = singerRepository.findById(singerId) + .orElseThrow(() -> new BusinessException("歌手不存在")); + singers.add(singer); + } + music.setSingers(singers); + } else if (StringUtils.hasText(musicDTO.getSinger())) { + // 如果有歌手名称字符串,则处理歌手名称 + String singerName = musicDTO.getSinger(); + // 查找是否已存在该歌手 + Optional existingSinger = singerRepository.findByName(singerName); + + if (existingSinger.isPresent()) { + // 如果歌手已存在,直接使用 + Set singers = new HashSet<>(); + singers.add(existingSinger.get()); + music.setSingers(singers); + } else { + // 如果歌手不存在,创建新歌手 + Singer newSinger = new Singer(); + newSinger.setName(singerName); + Singer savedSinger = singerRepository.save(newSinger); + + Set singers = new HashSet<>(); + singers.add(savedSinger); + music.setSingers(singers); + } + } + + // 保存音乐 + Music savedMusic = musicRepository.save(music); + + return convertToDTO(savedMusic); + } + + @Override + @Transactional + public MusicDTO updateMusic(Long id, MusicDTO musicDTO, MultipartFile file, MultipartFile cover, MultipartFile lyric) { + // 获取音乐 + Music music = musicRepository.findById(id) + .orElseThrow(() -> new BusinessException("音乐不存在")); + + // 如果有新的音乐文件,则上传并更新URL + if (file != null && !file.isEmpty()) { + // 删除旧文件 + if (music.getUrl() != null) { + fileService.deleteFile(music.getUrl()); + } + + // 上传新文件 + String musicUrl = fileService.uploadMusic(file); + music.setUrl(musicUrl); + } + + // 如果有新的封面,则上传并更新URL + if (cover != null && !cover.isEmpty()) { + // 删除旧封面 + if (music.getCover() != null) { + fileService.deleteFile(music.getCover()); + } + + // 上传新封面 + String coverUrl = fileService.uploadImage(cover); + music.setCover(coverUrl); + } + + // 如果有新的歌词文件,则读取歌词内容 + if (lyric != null && !lyric.isEmpty()) { + try { + String lyricContent = new String(lyric.getBytes(), "UTF-8"); + music.setLyric(lyricContent); + } catch (Exception e) { + throw new BusinessException("读取歌词文件失败"); + } + } else if (StringUtils.hasText(musicDTO.getLyric())) { + // 如果没有歌词文件,但有歌词内容,则直接使用歌词内容 + music.setLyric(musicDTO.getLyric()); + } + + // 更新音乐信息 + music.setName(musicDTO.getName()); + music.setAlbum(musicDTO.getAlbum()); + music.setDuration(musicDTO.getDuration()); + + // 更新音乐类型 + if (musicDTO.getTypeIds() != null) { + Set types = new HashSet<>(); + for (Long typeId : musicDTO.getTypeIds()) { + MusicType type = musicTypeRepository.findById(typeId) + .orElseThrow(() -> new BusinessException("音乐类型不存在")); + types.add(type); + } + music.setTypes(types); + } + + // 更新歌手 + if (musicDTO.getSingerIds() != null && !musicDTO.getSingerIds().isEmpty()) { + // 如果有歌手ID列表,优先使用 + Set singers = new HashSet<>(); + for (Long singerId : musicDTO.getSingerIds()) { + Singer singer = singerRepository.findById(singerId) + .orElseThrow(() -> new BusinessException("歌手不存在")); + singers.add(singer); + } + music.setSingers(singers); + } else if (StringUtils.hasText(musicDTO.getSinger())) { + // 如果有歌手名称字符串,则处理歌手名称 + String singerName = musicDTO.getSinger(); + // 查找是否已存在该歌手 + Optional existingSinger = singerRepository.findByName(singerName); + + if (existingSinger.isPresent()) { + // 如果歌手已存在,直接使用 + Set singers = new HashSet<>(); + singers.add(existingSinger.get()); + music.setSingers(singers); + } else { + // 如果歌手不存在,创建新歌手 + Singer newSinger = new Singer(); + newSinger.setName(singerName); + Singer savedSinger = singerRepository.save(newSinger); + + Set singers = new HashSet<>(); + singers.add(savedSinger); + music.setSingers(singers); + } + } + + // 保存音乐 + Music updatedMusic = musicRepository.save(music); + + return convertToDTO(updatedMusic); + } + + @Override + @Transactional + public boolean deleteMusic(Long id) { + // 获取音乐 + Music music = musicRepository.findById(id) + .orElseThrow(() -> new BusinessException("音乐不存在")); + + // 删除音乐文件 + if (music.getUrl() != null) { + fileService.deleteFile(music.getUrl()); + } + + // 删除封面 + if (music.getCover() != null) { + fileService.deleteFile(music.getCover()); + } + + // 删除音乐 + musicRepository.delete(music); + + return true; + } + + @Override + @Transactional + public boolean toggleCollectMusic(Long id, Long userId) { + // 获取用户 + Optional userOpt = userRepository.findById(userId); + if (!userOpt.isPresent()) { + // 如果用户不存在,返回false + return false; + } + User user = userOpt.get(); + + // 获取音乐 + Optional musicOpt = musicRepository.findById(id); + if (!musicOpt.isPresent()) { + // 如果音乐不存在,返回false + return false; + } + Music music = musicOpt.get(); + + // 检查是否已收藏 + boolean isCollected = userMusicRepository.existsByUserAndMusic(user, music); + + if (isCollected) { + // 如果已收藏,则取消收藏 + userMusicRepository.uncollectMusic(user.getId(), music.getId()); + } else { + // 如果未收藏,则收藏 + userMusicRepository.collectMusic(user.getId(), music.getId()); + } + + return !isCollected; // 返回当前收藏状态,true表示已收藏,false表示未收藏 + } + + @Override + public PageResult getCollectedMusic(Long userId, Integer page, Integer size) { + // 获取用户 + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取用户收藏的音乐 + Page userMusicPage = userMusicRepository.findCollectedMusic( + user.getId(), + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "collectTime")) + ); + + // 转换为DTO + List musicDTOs = userMusicPage.getContent().stream() + .map(userMusic -> { + MusicDTO musicDTO = convertToDTO(userMusic.getMusic()); + // 设置收藏时间 + musicDTO.setCollectTime(userMusic.getCollectTime()); + return musicDTO; + }) + .collect(Collectors.toList()); + + return new PageResult<>( + musicDTOs, + userMusicPage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public MusicDTO playMusic(Long id) { + // 获取音乐 + Optional musicOpt = musicRepository.findById(id); + if (!musicOpt.isPresent()) { + // 如果音乐不存在,返回null + return null; + } + + Music music = musicOpt.get(); + + // 增加播放次数 + music.setPlayCount(music.getPlayCount() + 1); + musicRepository.save(music); + + // 如果用户已登录,记录播放历史 + if (StpUtil.isLogin()) { + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId).orElse(null); + if (user != null) { + playHistoryRepository.recordPlayHistory(user.getId(), music.getId()); + } + } + + return convertToDTO(music); + } + + @Override + public Music getById(Long id) { + return musicRepository.findById(id).orElse(null); + } + + /** + * 将实体转换为DTO + * + * @param music 音乐实体 + * @return 音乐DTO + */ + private MusicDTO convertToDTO(Music music) { + MusicDTO musicDTO = new MusicDTO(); + BeanUtils.copyProperties(music, musicDTO); + + // 确保专辑字段正确复制 + musicDTO.setAlbum(music.getAlbum()); + + // 设置类型ID列表 + if (music.getTypes() != null) { + List typeIds = music.getTypes().stream() + .map(MusicType::getId) + .collect(Collectors.toList()); + musicDTO.setTypeIds(typeIds); + + // 设置类型名称列表 + List typeNames = music.getTypes().stream() + .map(MusicType::getName) + .collect(Collectors.toList()); + musicDTO.setTypeNames(typeNames); + } + + // 设置歌手ID列表 + if (music.getSingers() != null) { + List singerIds = music.getSingers().stream() + .map(Singer::getId) + .collect(Collectors.toList()); + musicDTO.setSingerIds(singerIds); + + // 设置歌手名称列表 + List singerNames = music.getSingers().stream() + .map(Singer::getName) + .collect(Collectors.toList()); + musicDTO.setSingerNames(singerNames); + + // 设置歌手名称字符串(用于前端显示) + if (!singerNames.isEmpty()) { + musicDTO.setSinger(String.join(", ", singerNames)); + } + } + + // 检查当前登录用户是否已收藏该音乐 + if (StpUtil.isLogin()) { + Long userId = StpUtil.getLoginIdAsLong(); + boolean isCollected = userMusicRepository.existsByUserIdAndMusicId(userId, music.getId()); + musicDTO.setCollected(isCollected); + } else { + musicDTO.setCollected(false); + } + + return musicDTO; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java new file mode 100644 index 0000000..2d65829 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java @@ -0,0 +1,479 @@ +package com.test.musichouduan.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import com.test.musichouduan.dto.MusicDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.PlaylistDTO; +import com.test.musichouduan.dto.UserDTO; +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.entity.Playlist; +import com.test.musichouduan.entity.PlaylistType; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.exception.BusinessException; +import com.test.musichouduan.repository.*; +import com.test.musichouduan.service.FileService; +import com.test.musichouduan.service.PlaylistService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 歌单服务实现类 + */ +@Service +public class PlaylistServiceImpl implements PlaylistService { + + @Autowired + private PlaylistRepository playlistRepository; + + @Autowired + private PlaylistTypeRepository playlistTypeRepository; + + @Autowired + private MusicRepository musicRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserPlaylistRepository userPlaylistRepository; + + @Autowired + private PlayHistoryRepository playHistoryRepository; + + @Autowired + private FileService fileService; + + @Override + public PlaylistDTO getPlaylistById(Long id) { + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + return convertToDTO(playlist); + } + + @Override + public PageResult getPlaylistList(Integer page, Integer size, String keyword, Long typeId) { + // 构建查询条件 + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + + // 关键字查询 + if (StringUtils.hasText(keyword)) { + predicates.add(cb.like(root.get("name"), "%" + keyword + "%")); + } + + // 类型查询 + if (typeId != null) { + predicates.add(cb.equal(root.get("type").get("id"), typeId)); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + // 执行查询 + Page playlistPage = playlistRepository.findAll( + spec, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List playlistDTOs = playlistPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + playlistDTOs, + playlistPage.getTotalElements(), + page, + size + ); + } + + @Override + public List getLatestPlaylist(Integer limit) { + // 查询最新歌单 + List playlists = playlistRepository.findLatest(PageRequest.of(0, limit)); + + // 转换为DTO + return playlists.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public List getHotPlaylist(Integer limit) { + // 查询热门歌单 + List playlists = playlistRepository.findMostPlayed(PageRequest.of(0, limit)); + + // 转换为DTO + return playlists.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public PageResult getOfficialPlaylist(Integer page, Integer size) { + // 查询官方歌单 + Page playlistPage = playlistRepository.findByIsOfficial( + 1, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List playlistDTOs = playlistPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + playlistDTOs, + playlistPage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public PlaylistDTO addPlaylist(PlaylistDTO playlistDTO, MultipartFile cover) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 上传封面 + String coverUrl = null; + if (cover != null && !cover.isEmpty()) { + coverUrl = fileService.uploadImage(cover); + } + + // 创建歌单实体 + Playlist playlist = new Playlist(); + playlist.setName(playlistDTO.getName()); + playlist.setDescription(playlistDTO.getDescription()); + playlist.setCover(coverUrl); + playlist.setCreator(user); + playlist.setPlayCount(0L); + playlist.setIsOfficial(0); // 默认为非官方歌单 + + // 设置歌单类型 + if (playlistDTO.getType() != null && playlistDTO.getType().getId() != null) { + PlaylistType type = playlistTypeRepository.findById(playlistDTO.getType().getId()) + .orElseThrow(() -> new BusinessException("歌单类型不存在")); + playlist.setType(type); + } + + // 保存歌单 + Playlist savedPlaylist = playlistRepository.save(playlist); + + return convertToDTO(savedPlaylist); + } + + @Override + @Transactional + public PlaylistDTO updatePlaylist(Long id, PlaylistDTO playlistDTO, MultipartFile cover) { + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 检查是否是歌单创建者或管理员 + if (!playlist.getCreator().getId().equals(userId) && !StpUtil.hasRole("admin")) { + throw new BusinessException("无权修改该歌单"); + } + + // 如果有新的封面,则上传并更新URL + if (cover != null && !cover.isEmpty()) { + // 删除旧封面 + if (playlist.getCover() != null) { + fileService.deleteFile(playlist.getCover()); + } + + // 上传新封面 + String coverUrl = fileService.uploadImage(cover); + playlist.setCover(coverUrl); + } + + // 更新歌单信息 + playlist.setName(playlistDTO.getName()); + playlist.setDescription(playlistDTO.getDescription()); + + // 更新是否官方字段 + System.out.println("接收到的isOfficial值: " + playlistDTO.getIsOfficial()); + playlist.setIsOfficial(playlistDTO.getIsOfficial()); + System.out.println("更新后的歌单isOfficial值: " + playlist.getIsOfficial()); + + // 更新歌单类型 + if (playlistDTO.getType() != null && playlistDTO.getType().getId() != null) { + PlaylistType type = playlistTypeRepository.findById(playlistDTO.getType().getId()) + .orElseThrow(() -> new BusinessException("歌单类型不存在")); + playlist.setType(type); + } + + // 保存歌单 + Playlist updatedPlaylist = playlistRepository.save(playlist); + + return convertToDTO(updatedPlaylist); + } + + @Override + @Transactional + public boolean deletePlaylist(Long id) { + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 检查是否是歌单创建者或管理员 + if (!playlist.getCreator().getId().equals(userId) && !StpUtil.hasRole("admin")) { + throw new BusinessException("无权删除该歌单"); + } + + // 删除封面 + if (playlist.getCover() != null) { + fileService.deleteFile(playlist.getCover()); + } + + // 删除歌单 + playlistRepository.delete(playlist); + + return true; + } + + @Override + @Transactional + public boolean collectPlaylist(Long id) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 检查是否已收藏 + if (userPlaylistRepository.existsByUserAndPlaylist(user, playlist)) { + throw new BusinessException("已收藏该歌单"); + } + + // 收藏歌单 + userPlaylistRepository.collectPlaylist(user.getId(), playlist.getId()); + + return true; + } + + @Override + @Transactional + public boolean uncollectPlaylist(Long id) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 检查是否已收藏 + if (!userPlaylistRepository.existsByUserAndPlaylist(user, playlist)) { + throw new BusinessException("未收藏该歌单"); + } + + // 取消收藏歌单 + userPlaylistRepository.uncollectPlaylist(user.getId(), playlist.getId()); + + return true; + } + + @Override + public PageResult getCollectedPlaylist(Integer page, Integer size) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 获取用户收藏的歌单 + Page playlistPage = userPlaylistRepository.findCollectedPlaylist( + userId, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "collectTime")) + ); + + // 转换为DTO + List playlistDTOs = playlistPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + playlistDTOs, + playlistPage.getTotalElements(), + page, + size + ); + } + + @Override + public PageResult getUserPlaylist(Integer page, Integer size) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取用户创建的歌单 + Page playlistPage = playlistRepository.findByCreator( + user, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List playlistDTOs = playlistPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + playlistDTOs, + playlistPage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public boolean addMusicToPlaylist(Long id, Long musicId) { + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 检查是否是歌单创建者 + if (!playlist.getCreator().getId().equals(userId)) { + throw new BusinessException("无权修改该歌单"); + } + + // 获取音乐 + Music music = musicRepository.findById(musicId) + .orElseThrow(() -> new BusinessException("音乐不存在")); + + // 检查音乐是否已在歌单中 + if (playlist.getMusics().contains(music)) { + throw new BusinessException("该音乐已在歌单中"); + } + + // 添加音乐到歌单 + playlist.getMusics().add(music); + playlistRepository.save(playlist); + + return true; + } + + @Override + @Transactional + public boolean removeMusicFromPlaylist(Long id, Long musicId) { + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 检查是否是歌单创建者 + if (!playlist.getCreator().getId().equals(userId)) { + throw new BusinessException("无权修改该歌单"); + } + + // 获取音乐 + Music music = musicRepository.findById(musicId) + .orElseThrow(() -> new BusinessException("音乐不存在")); + + // 检查音乐是否在歌单中 + if (!playlist.getMusics().contains(music)) { + throw new BusinessException("该音乐不在歌单中"); + } + + // 从歌单中移除音乐 + playlist.getMusics().remove(music); + playlistRepository.save(playlist); + + return true; + } + + @Override + @Transactional + public PlaylistDTO playPlaylist(Long id) { + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 增加播放次数 + playlist.setPlayCount(playlist.getPlayCount() + 1); + playlistRepository.save(playlist); + + return convertToDTO(playlist); + } + + @Override + public Playlist getById(Long id) { + return playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + } + + /** + * 将实体转换为DTO + * + * @param playlist 歌单实体 + * @return 歌单DTO + */ + private PlaylistDTO convertToDTO(Playlist playlist) { + PlaylistDTO playlistDTO = new PlaylistDTO(); + BeanUtils.copyProperties(playlist, playlistDTO); + + // 设置创建者信息 + if (playlist.getCreator() != null) { + UserDTO creatorDTO = new UserDTO(); + BeanUtils.copyProperties(playlist.getCreator(), creatorDTO); + playlistDTO.setCreator(creatorDTO); + } + + // 设置音乐列表 + if (playlist.getMusics() != null && !playlist.getMusics().isEmpty()) { + List musicDTOs = playlist.getMusics().stream() + .map(music -> { + MusicDTO musicDTO = new MusicDTO(); + BeanUtils.copyProperties(music, musicDTO); + return musicDTO; + }) + .collect(Collectors.toList()); + playlistDTO.setMusics(musicDTOs); + playlistDTO.setMusicCount(musicDTOs.size()); + } else { + playlistDTO.setMusicCount(0); + } + + // 检查当前登录用户是否已收藏该歌单 + if (StpUtil.isLogin()) { + Long userId = StpUtil.getLoginIdAsLong(); + boolean isCollected = userPlaylistRepository.existsByUserIdAndPlaylistId(userId, playlist.getId()); + playlistDTO.setIsCollected(isCollected); + } else { + playlistDTO.setIsCollected(false); + } + + return playlistDTO; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/SingerServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/SingerServiceImpl.java new file mode 100644 index 0000000..f891dda --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/SingerServiceImpl.java @@ -0,0 +1,389 @@ +package com.test.musichouduan.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.SingerDTO; +import com.test.musichouduan.dto.SingerTypeDTO; +import com.test.musichouduan.dto.MusicDTO; +import com.test.musichouduan.entity.Singer; +import com.test.musichouduan.entity.SingerType; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.exception.BusinessException; +import com.test.musichouduan.repository.MusicRepository; +import com.test.musichouduan.repository.SingerRepository; +import com.test.musichouduan.repository.SingerTypeRepository; +import com.test.musichouduan.repository.UserRepository; +import com.test.musichouduan.repository.UserSingerRepository; +import com.test.musichouduan.service.FileService; +import com.test.musichouduan.service.SingerService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 歌手服务实现类 + */ +@Service +public class SingerServiceImpl implements SingerService { + + @Autowired + private SingerRepository singerRepository; + + @Autowired + private SingerTypeRepository singerTypeRepository; + + @Autowired + private MusicRepository musicRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserSingerRepository userSingerRepository; + + @Autowired + private FileService fileService; + + @Override + public SingerDTO getSingerById(Long id) { + Singer singer = singerRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌手不存在")); + return convertToDTO(singer); + } + + @Override + public PageResult getSingerList(Integer page, Integer size, String keyword, Long typeId) { + // 构建查询条件 + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + + // 关键字查询 + if (StringUtils.hasText(keyword)) { + predicates.add(cb.like(root.get("name"), "%" + keyword + "%")); + } + + // 类型查询 + if (typeId != null) { + predicates.add(cb.equal(root.get("type").get("id"), typeId)); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + // 执行查询 + Page singerPage = singerRepository.findAll( + spec, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List singerDTOs = singerPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + singerDTOs, + singerPage.getTotalElements(), + page, + size + ); + } + + @Override + public List getLatestSinger(Integer limit) { + // 查询最新歌手 + List singers = singerRepository.findLatest(PageRequest.of(0, limit)); + + // 转换为DTO + return singers.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public List getHotSinger(Integer limit) { + // 查询热门歌手 + List singers = singerRepository.findHotSingers(PageRequest.of(0, limit)); + + // 转换为DTO + return singers.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public SingerDTO addSinger(SingerDTO singerDTO, MultipartFile avatar) { + // 检查歌手名称是否已存在 + if (singerRepository.findByName(singerDTO.getName()).isPresent()) { + throw new BusinessException("歌手名称已存在"); + } + + // 上传头像 + String avatarUrl = null; + if (avatar != null && !avatar.isEmpty()) { + avatarUrl = fileService.uploadImage(avatar); + } + + // 创建歌手实体 + Singer singer = new Singer(); + singer.setName(singerDTO.getName()); + singer.setAvatar(avatarUrl); + singer.setIntroduction(singerDTO.getIntroduction()); + + // 设置歌手类型 + if (singerDTO.getType() != null && singerDTO.getType().getId() != null) { + SingerType type = singerTypeRepository.findById(singerDTO.getType().getId()) + .orElseThrow(() -> new BusinessException("歌手类型不存在")); + singer.setType(type); + } + + // 保存歌手 + Singer savedSinger = singerRepository.save(singer); + + return convertToDTO(savedSinger); + } + + @Override + @Transactional + public SingerDTO updateSinger(Long id, SingerDTO singerDTO, MultipartFile avatar) { + // 获取歌手 + Singer singer = singerRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌手不存在")); + + // 检查歌手名称是否已存在 + if (!singer.getName().equals(singerDTO.getName()) && + singerRepository.findByName(singerDTO.getName()).isPresent()) { + throw new BusinessException("歌手名称已存在"); + } + + // 如果有新的头像,则上传并更新URL + if (avatar != null && !avatar.isEmpty()) { + // 删除旧头像 + if (singer.getAvatar() != null) { + fileService.deleteFile(singer.getAvatar()); + } + + // 上传新头像 + String avatarUrl = fileService.uploadImage(avatar); + singer.setAvatar(avatarUrl); + } + + // 更新歌手信息 + singer.setName(singerDTO.getName()); + singer.setIntroduction(singerDTO.getIntroduction()); + + // 更新歌手类型 + if (singerDTO.getType() != null && singerDTO.getType().getId() != null) { + SingerType type = singerTypeRepository.findById(singerDTO.getType().getId()) + .orElseThrow(() -> new BusinessException("歌手类型不存在")); + singer.setType(type); + } + + // 保存歌手 + Singer updatedSinger = singerRepository.save(singer); + + return convertToDTO(updatedSinger); + } + + @Override + @Transactional + public boolean deleteSinger(Long id) { + // 获取歌手 + Singer singer = singerRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌手不存在")); + + // 删除头像 + if (singer.getAvatar() != null) { + fileService.deleteFile(singer.getAvatar()); + } + + // 删除歌手 + singerRepository.delete(singer); + + return true; + } + + @Override + @Transactional + public boolean collectSinger(Long id) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取歌手 + Singer singer = singerRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌手不存在")); + + // 检查是否已收藏 + if (userSingerRepository.existsByUserAndSinger(user, singer)) { + throw new BusinessException("已收藏该歌手"); + } + + // 收藏歌手 + userSingerRepository.collectSinger(user.getId(), singer.getId()); + + return true; + } + + @Override + @Transactional + public boolean uncollectSinger(Long id) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取歌手 + Singer singer = singerRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌手不存在")); + + // 检查是否已收藏 + if (!userSingerRepository.existsByUserAndSinger(user, singer)) { + throw new BusinessException("未收藏该歌手"); + } + + // 取消收藏歌手 + userSingerRepository.uncollectSinger(user.getId(), singer.getId()); + + return true; + } + + @Override + public PageResult getCollectedSinger(Integer page, Integer size) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 获取用户收藏的歌手 + Page singerPage = userSingerRepository.findCollectedSinger( + userId, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "collectTime")) + ); + + // 转换为DTO + List singerDTOs = singerPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + singerDTOs, + singerPage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public boolean addMusicToSinger(Long singerId, Long musicId) { + // 获取歌手 + Singer singer = singerRepository.findById(singerId) + .orElseThrow(() -> new BusinessException("歌手不存在")); + + // 获取音乐 + Music music = musicRepository.findById(musicId) + .orElseThrow(() -> new BusinessException("音乐不存在")); + + // 检查是否已关联(双向检查) + if (singer.getMusics().contains(music) || music.getSingers().contains(singer)) { + throw new BusinessException("该音乐已关联到歌手"); + } + + // 添加关联并保存 + singer.getMusics().add(music); + singerRepository.save(singer); + + return true; + } + + @Override + @Transactional + public boolean removeMusicFromSinger(Long singerId, Long musicId) { + // 获取歌手 + Singer singer = singerRepository.findById(singerId) + .orElseThrow(() -> new BusinessException("歌手不存在")); + + // 获取音乐 + Music music = musicRepository.findById(musicId) + .orElseThrow(() -> new BusinessException("音乐不存在")); + + // 检查是否已关联 + if (!singer.getMusics().contains(music)) { + throw new BusinessException("该音乐未关联到歌手"); + } + + // 移除关联 + singer.getMusics().remove(music); + singerRepository.save(singer); + + return true; + } + + @Override + public Singer getById(Long id) { + return singerRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌手不存在")); + } + + /** + * 将实体转换为DTO + * + * @param singer 歌手实体 + * @return 歌手DTO + */ + private SingerDTO convertToDTO(Singer singer) { + SingerDTO singerDTO = new SingerDTO(); + BeanUtils.copyProperties(singer, singerDTO); + + // 设置类型信息 + if (singer.getType() != null) { + SingerTypeDTO typeDTO = new SingerTypeDTO(); + BeanUtils.copyProperties(singer.getType(), typeDTO); + singerDTO.setType(typeDTO); + } + + // 设置音乐数量 + Long musicCount = musicRepository.countBySingersContaining(singer); + singerDTO.setMusicCount(musicCount); + + // 设置粉丝数量 + Long fansCount = userSingerRepository.count(); + singerDTO.setFansCount(fansCount); + + // 检查当前登录用户是否已收藏该歌手 + if (StpUtil.isLogin()) { + Long userId = StpUtil.getLoginIdAsLong(); + boolean isCollected = userSingerRepository.existsByUserIdAndSingerId(userId, singer.getId()); + singerDTO.setIsCollected(isCollected); + } else { + singerDTO.setIsCollected(false); + } + + // 设置关联音乐列表 + if (singer.getMusics() != null) { + singerDTO.setMusics(singer.getMusics().stream() + .map(music -> { + MusicDTO musicDTO = new MusicDTO(); + BeanUtils.copyProperties(music, musicDTO); + return musicDTO; + }) + .collect(Collectors.toList())); + } + + return singerDTO; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/StatServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/StatServiceImpl.java new file mode 100644 index 0000000..e692892 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/StatServiceImpl.java @@ -0,0 +1,175 @@ +package com.test.musichouduan.service.impl; + +import com.test.musichouduan.dto.MusicDTO; +import com.test.musichouduan.dto.MusicTypeDTO; +import com.test.musichouduan.dto.UserDTO; +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.entity.MusicType; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.repository.*; +import com.test.musichouduan.service.StatService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 统计服务实现类 + */ +@Service +public class StatServiceImpl implements StatService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private MusicRepository musicRepository; + + @Autowired + private PlaylistRepository playlistRepository; + + @Autowired + private SingerRepository singerRepository; + + @Autowired + private MusicTypeRepository musicTypeRepository; + + @Autowired + private PlayHistoryRepository playHistoryRepository; + + @Override + public Map getTotalStats() { + Map stats = new HashMap<>(); + stats.put("userCount", userRepository.count()); + stats.put("musicCount", musicRepository.count()); + stats.put("playlistCount", playlistRepository.count()); + stats.put("singerCount", singerRepository.count()); + + // 计算总播放量 + long totalPlayCount = 0; + List allMusic = musicRepository.findAll(); + for (Music music : allMusic) { + totalPlayCount += music.getPlayCount() != null ? music.getPlayCount() : 0; + } + stats.put("playCount", totalPlayCount); + + return stats; + } + + @Override + public Map getUserGrowthTrend(Integer days) { + Map stats = new HashMap<>(); + List dateList = new ArrayList<>(); + List countList = new ArrayList<>(); + + // 获取日期列表 + LocalDate endDate = LocalDate.now(); + LocalDate startDate = endDate.minusDays(days - 1); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd"); + + for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) { + dateList.add(date.format(formatter)); + + // 计算当天注册用户数 + LocalDateTime start = LocalDateTime.of(date, LocalTime.MIN); + LocalDateTime end = LocalDateTime.of(date, LocalTime.MAX); + long count = userRepository.countByCreateTimeBetween(start, end); + countList.add(count); + } + + stats.put("dates", dateList); + stats.put("counts", countList); + return stats; + } + + @Override + public Map getMusicTypeDistribution() { + Map stats = new HashMap<>(); + List nameList = new ArrayList<>(); + List countList = new ArrayList<>(); + + // 获取所有音乐类型 + List types = musicTypeRepository.findAll(); + for (MusicType type : types) { + nameList.add(type.getName()); + long count = musicRepository.countByTypesContaining(type); + countList.add(count); + } + + stats.put("names", nameList); + stats.put("counts", countList); + return stats; + } + + @Override + public Map getPlayCountStats(Integer days) { + Map stats = new HashMap<>(); + List dateList = new ArrayList<>(); + List countList = new ArrayList<>(); + + // 获取日期列表 + LocalDate endDate = LocalDate.now(); + LocalDate startDate = endDate.minusDays(days - 1); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd"); + + for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) { + dateList.add(date.format(formatter)); + + // 计算当天播放次数 + LocalDateTime start = LocalDateTime.of(date, LocalTime.MIN); + LocalDateTime end = LocalDateTime.of(date, LocalTime.MAX); + long count = playHistoryRepository.countByPlayTimeBetween(start, end); + countList.add(count); + } + + stats.put("dates", dateList); + stats.put("counts", countList); + return stats; + } + + @Override + public Map getLatestUsers(Integer limit) { + Map stats = new HashMap<>(); + + // 获取最新用户 + List users = userRepository.findAll( + PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createTime")) + ).getContent(); + + List userDTOList = users.stream().map(user -> { + UserDTO userDTO = new UserDTO(); + BeanUtils.copyProperties(user, userDTO); + return userDTO; + }).collect(Collectors.toList()); + + stats.put("users", userDTOList); + return stats; + } + + @Override + public Map getLatestMusics(Integer limit) { + Map stats = new HashMap<>(); + + // 获取最新音乐 + List musics = musicRepository.findAll( + PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createTime")) + ).getContent(); + + List musicDTOList = musics.stream().map(music -> { + MusicDTO musicDTO = new MusicDTO(); + BeanUtils.copyProperties(music, musicDTO); + return musicDTO; + }).collect(Collectors.toList()); + + stats.put("musics", musicDTOList); + return stats; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/TypeServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/TypeServiceImpl.java new file mode 100644 index 0000000..470015a --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/TypeServiceImpl.java @@ -0,0 +1,395 @@ +package com.test.musichouduan.service.impl; + +import com.test.musichouduan.dto.MusicTypeDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.PlaylistTypeDTO; +import com.test.musichouduan.dto.SingerTypeDTO; +import com.test.musichouduan.entity.MusicType; +import com.test.musichouduan.entity.PlaylistType; +import com.test.musichouduan.entity.SingerType; +import com.test.musichouduan.exception.BusinessException; +import com.test.musichouduan.repository.MusicRepository; +import com.test.musichouduan.repository.MusicTypeRepository; +import com.test.musichouduan.repository.PlaylistRepository; +import com.test.musichouduan.repository.PlaylistTypeRepository; +import com.test.musichouduan.repository.SingerRepository; +import com.test.musichouduan.repository.SingerTypeRepository; +import com.test.musichouduan.service.TypeService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 类型服务实现类 + */ +@Service +public class TypeServiceImpl implements TypeService { + + @Autowired + private MusicTypeRepository musicTypeRepository; + + @Autowired + private SingerTypeRepository singerTypeRepository; + + @Autowired + private PlaylistTypeRepository playlistTypeRepository; + + @Autowired + private MusicRepository musicRepository; + + @Autowired + private SingerRepository singerRepository; + + @Autowired + private PlaylistRepository playlistRepository; + + @Override + public List getMusicTypeList() { + List types = musicTypeRepository.findAll(Sort.by(Sort.Direction.ASC, "id")); + return types.stream() + .map(this::convertToMusicTypeDTO) + .collect(Collectors.toList()); + } + + @Override + public PageResult getMusicTypePage(Integer page, Integer size, String keyword) { + // 构建查询条件 + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + + // 关键字查询 + if (StringUtils.hasText(keyword)) { + predicates.add(cb.like(root.get("name"), "%" + keyword + "%")); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + // 执行查询 + Page typePage = musicTypeRepository.findAll( + spec, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.ASC, "id")) + ); + + // 转换为DTO + List typeDTOs = typePage.getContent().stream() + .map(this::convertToMusicTypeDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + typeDTOs, + typePage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public MusicTypeDTO addMusicType(MusicTypeDTO musicTypeDTO) { + // 检查名称是否已存在 + if (musicTypeRepository.existsByName(musicTypeDTO.getName())) { + throw new BusinessException("音乐类型名称已存在"); + } + + // 创建音乐类型实体 + MusicType musicType = new MusicType(); + musicType.setName(musicTypeDTO.getName()); + musicType.setDescription(musicTypeDTO.getDescription()); + + // 保存音乐类型 + MusicType savedMusicType = musicTypeRepository.save(musicType); + + return convertToMusicTypeDTO(savedMusicType); + } + + @Override + @Transactional + public MusicTypeDTO updateMusicType(Long id, MusicTypeDTO musicTypeDTO) { + // 获取音乐类型 + MusicType musicType = musicTypeRepository.findById(id) + .orElseThrow(() -> new BusinessException("音乐类型不存在")); + + // 检查名称是否已存在 + if (!musicType.getName().equals(musicTypeDTO.getName()) && + musicTypeRepository.existsByName(musicTypeDTO.getName())) { + throw new BusinessException("音乐类型名称已存在"); + } + + // 更新音乐类型信息 + musicType.setName(musicTypeDTO.getName()); + musicType.setDescription(musicTypeDTO.getDescription()); + + // 保存音乐类型 + MusicType updatedMusicType = musicTypeRepository.save(musicType); + + return convertToMusicTypeDTO(updatedMusicType); + } + + @Override + @Transactional + public boolean deleteMusicType(Long id) { + // 获取音乐类型 + MusicType musicType = musicTypeRepository.findById(id) + .orElseThrow(() -> new BusinessException("音乐类型不存在")); + + // 检查是否有音乐使用该类型 + if (musicRepository.existsByTypesContaining(musicType)) { + throw new BusinessException("该音乐类型下有音乐,无法删除"); + } + + // 删除音乐类型 + musicTypeRepository.delete(musicType); + + return true; + } + + @Override + public List getSingerTypeList() { + List types = singerTypeRepository.findAll(Sort.by(Sort.Direction.ASC, "id")); + return types.stream() + .map(this::convertToSingerTypeDTO) + .collect(Collectors.toList()); + } + + @Override + public PageResult getSingerTypePage(Integer page, Integer size, String keyword) { + // 构建查询条件 + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + + // 关键字查询 + if (StringUtils.hasText(keyword)) { + predicates.add(cb.like(root.get("name"), "%" + keyword + "%")); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + // 执行查询 + Page typePage = singerTypeRepository.findAll( + spec, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.ASC, "id")) + ); + + // 转换为DTO + List typeDTOs = typePage.getContent().stream() + .map(this::convertToSingerTypeDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + typeDTOs, + typePage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public SingerTypeDTO addSingerType(SingerTypeDTO singerTypeDTO) { + // 检查名称是否已存在 + if (singerTypeRepository.existsByName(singerTypeDTO.getName())) { + throw new BusinessException("歌手类型名称已存在"); + } + + // 创建歌手类型实体 + SingerType singerType = new SingerType(); + singerType.setName(singerTypeDTO.getName()); + singerType.setDescription(singerTypeDTO.getDescription()); + + // 保存歌手类型 + SingerType savedSingerType = singerTypeRepository.save(singerType); + + return convertToSingerTypeDTO(savedSingerType); + } + + @Override + @Transactional + public SingerTypeDTO updateSingerType(Long id, SingerTypeDTO singerTypeDTO) { + // 获取歌手类型 + SingerType singerType = singerTypeRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌手类型不存在")); + + // 检查名称是否已存在 + if (!singerType.getName().equals(singerTypeDTO.getName()) && + singerTypeRepository.existsByName(singerTypeDTO.getName())) { + throw new BusinessException("歌手类型名称已存在"); + } + + // 更新歌手类型信息 + singerType.setName(singerTypeDTO.getName()); + singerType.setDescription(singerTypeDTO.getDescription()); + + // 保存歌手类型 + SingerType updatedSingerType = singerTypeRepository.save(singerType); + + return convertToSingerTypeDTO(updatedSingerType); + } + + @Override + @Transactional + public boolean deleteSingerType(Long id) { + // 获取歌手类型 + SingerType singerType = singerTypeRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌手类型不存在")); + + // 检查是否有歌手使用该类型 + if (singerRepository.existsByType(singerType)) { + throw new BusinessException("该歌手类型下有歌手,无法删除"); + } + + // 删除歌手类型 + singerTypeRepository.delete(singerType); + + return true; + } + + @Override + public List getPlaylistTypeList() { + List types = playlistTypeRepository.findAll(Sort.by(Sort.Direction.ASC, "id")); + return types.stream() + .map(this::convertToPlaylistTypeDTO) + .collect(Collectors.toList()); + } + + @Override + public PageResult getPlaylistTypePage(Integer page, Integer size, String keyword) { + // 构建查询条件 + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + + // 关键字查询 + if (StringUtils.hasText(keyword)) { + predicates.add(cb.like(root.get("name"), "%" + keyword + "%")); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + // 执行查询 + Page typePage = playlistTypeRepository.findAll( + spec, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.ASC, "id")) + ); + + // 转换为DTO + List typeDTOs = typePage.getContent().stream() + .map(this::convertToPlaylistTypeDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + typeDTOs, + typePage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public PlaylistTypeDTO addPlaylistType(PlaylistTypeDTO playlistTypeDTO) { + // 检查名称是否已存在 + if (playlistTypeRepository.existsByName(playlistTypeDTO.getName())) { + throw new BusinessException("歌单类型名称已存在"); + } + + // 创建歌单类型实体 + PlaylistType playlistType = new PlaylistType(); + playlistType.setName(playlistTypeDTO.getName()); + playlistType.setDescription(playlistTypeDTO.getDescription()); + + // 保存歌单类型 + PlaylistType savedPlaylistType = playlistTypeRepository.save(playlistType); + + return convertToPlaylistTypeDTO(savedPlaylistType); + } + + @Override + @Transactional + public PlaylistTypeDTO updatePlaylistType(Long id, PlaylistTypeDTO playlistTypeDTO) { + // 获取歌单类型 + PlaylistType playlistType = playlistTypeRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单类型不存在")); + + // 检查名称是否已存在 + if (!playlistType.getName().equals(playlistTypeDTO.getName()) && + playlistTypeRepository.existsByName(playlistTypeDTO.getName())) { + throw new BusinessException("歌单类型名称已存在"); + } + + // 更新歌单类型信息 + playlistType.setName(playlistTypeDTO.getName()); + playlistType.setDescription(playlistTypeDTO.getDescription()); + + // 保存歌单类型 + PlaylistType updatedPlaylistType = playlistTypeRepository.save(playlistType); + + return convertToPlaylistTypeDTO(updatedPlaylistType); + } + + @Override + @Transactional + public boolean deletePlaylistType(Long id) { + // 获取歌单类型 + PlaylistType playlistType = playlistTypeRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单类型不存在")); + + // 检查是否有歌单使用该类型 + if (playlistRepository.existsByType(playlistType)) { + throw new BusinessException("该歌单类型下有歌单,无法删除"); + } + + // 删除歌单类型 + playlistTypeRepository.delete(playlistType); + + return true; + } + + /** + * 将音乐类型实体转换为DTO + * + * @param musicType 音乐类型实体 + * @return 音乐类型DTO + */ + private MusicTypeDTO convertToMusicTypeDTO(MusicType musicType) { + MusicTypeDTO musicTypeDTO = new MusicTypeDTO(); + BeanUtils.copyProperties(musicType, musicTypeDTO); + return musicTypeDTO; + } + + /** + * 将歌手类型实体转换为DTO + * + * @param singerType 歌手类型实体 + * @return 歌手类型DTO + */ + private SingerTypeDTO convertToSingerTypeDTO(SingerType singerType) { + SingerTypeDTO singerTypeDTO = new SingerTypeDTO(); + BeanUtils.copyProperties(singerType, singerTypeDTO); + return singerTypeDTO; + } + + /** + * 将歌单类型实体转换为DTO + * + * @param playlistType 歌单类型实体 + * @return 歌单类型DTO + */ + private PlaylistTypeDTO convertToPlaylistTypeDTO(PlaylistType playlistType) { + PlaylistTypeDTO playlistTypeDTO = new PlaylistTypeDTO(); + BeanUtils.copyProperties(playlistType, playlistTypeDTO); + return playlistTypeDTO; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/UserServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..65213ca --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/UserServiceImpl.java @@ -0,0 +1,355 @@ +package com.test.musichouduan.service.impl; + +import cn.dev33.satoken.secure.BCrypt; +import cn.dev33.satoken.stp.StpUtil; +import com.test.musichouduan.dto.*; +import com.test.musichouduan.entity.User; +import com.test.musichouduan.repository.*; +import com.test.musichouduan.service.UserService; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 用户服务实现类 + */ +@Service +public class UserServiceImpl implements UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserCollectMusicRepository userCollectMusicRepository; + + @Autowired + private UserCollectPlaylistRepository userCollectPlaylistRepository; + + @Autowired + private UserCollectSingerRepository userCollectSingerRepository; + + @Autowired + private PlaylistRepository playlistRepository; + + @Override + @Transactional + public UserDTO register(RegisterRequest request) { + // 检查用户名是否已存在 + if (userRepository.existsByUsername(request.getUsername())) { + throw new RuntimeException("用户名已存在"); + } + + // 检查邮箱是否已存在 + if (userRepository.existsByEmail(request.getEmail())) { + throw new RuntimeException("邮箱已存在"); + } + + // 检查两次密码是否一致 + if (!request.getPassword().equals(request.getConfirmPassword())) { + throw new RuntimeException("两次密码不一致"); + } + + // 创建用户 + User user = new User(); + user.setUsername(request.getUsername()); + user.setPassword(BCrypt.hashpw(request.getPassword())); + user.setEmail(request.getEmail()); + user.setNickname(request.getNickname() != null ? request.getNickname() : request.getUsername()); + user.setGender(0); + user.setRole(0); + user.setStatus(1); + + // 保存用户 + user = userRepository.save(user); + + // 返回用户DTO + return convertToDTO(user); + } + + @Override + public LoginResultDTO login(LoginRequest request) { + // 根据用户名查找用户 + User user = userRepository.findByUsername(request.getUsername()) + .orElseThrow(() -> new RuntimeException("用户名或密码错误")); + + // 检查用户状态 + if (user.getStatus() == 0) { + throw new RuntimeException("账号已被禁用"); + } + + // 检查密码 + if (!BCrypt.checkpw(request.getPassword(), user.getPassword())) { + throw new RuntimeException("用户名或密码错误"); + } + + // 登录 + StpUtil.login(user.getId()); + + // 设置角色 + if (user.getRole() == 1) { + // 打印调试信息 + System.out.println("设置用户ID: " + user.getId() + " 为admin角色"); + } + + // 获取令牌信息 + String token = StpUtil.getTokenValue(); + String tokenName = StpUtil.getTokenName(); + + // 创建登录结果DTO + LoginResultDTO resultDTO = new LoginResultDTO(); + resultDTO.setUser(convertToDTO(user)); + resultDTO.setToken(token); + resultDTO.setTokenName(tokenName); + resultDTO.setTokenPrefix(""); + + // 返回登录结果DTO + return resultDTO; + } + + @Override + public UserDTO getCurrentUser() { + // 获取当前登录用户ID + Long userId = StpUtil.getLoginIdAsLong(); + + // 根据ID查找用户 + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("用户不存在")); + + // 返回用户DTO + return getUserStats(userId); + } + + @Override + public UserDTO getUserById(Long id) { + // 根据ID查找用户 + User user = userRepository.findById(id) + .orElseThrow(() -> new RuntimeException("用户不存在")); + + // 返回用户DTO + return convertToDTO(user); + } + + @Override + public UserDTO getUserByUsername(String username) { + // 根据用户名查找用户 + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("用户不存在")); + + // 返回用户DTO + return convertToDTO(user); + } + + @Override + @Transactional + public UserDTO updateUser(UserUpdateRequest request) { + // 获取当前登录用户ID + Long userId = StpUtil.getLoginIdAsLong(); + + // 根据ID查找用户 + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("用户不存在")); + + // 更新用户信息 + if (request.getNickname() != null) { + user.setNickname(request.getNickname()); + } + if (request.getAvatar() != null) { + user.setAvatar(request.getAvatar()); + } + if (request.getGender() != null) { + user.setGender(request.getGender()); + } + if (request.getEmail() != null) { + user.setEmail(request.getEmail()); + } + if (request.getPhone() != null) { + user.setPhone(request.getPhone()); + } + if (request.getIntroduction() != null) { + user.setIntroduction(request.getIntroduction()); + } + + // 保存用户 + user = userRepository.save(user); + + // 返回用户DTO + return convertToDTO(user); + } + + @Override + @Transactional + public boolean updatePassword(PasswordUpdateRequest request) { + // 获取当前登录用户ID + Long userId = StpUtil.getLoginIdAsLong(); + + // 根据ID查找用户 + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("用户不存在")); + + // 检查旧密码 + if (!BCrypt.checkpw(request.getOldPassword(), user.getPassword())) { + throw new RuntimeException("旧密码错误"); + } + + // 检查两次密码是否一致 + if (!request.getNewPassword().equals(request.getConfirmPassword())) { + throw new RuntimeException("两次密码不一致"); + } + + // 更新密码 + user.setPassword(BCrypt.hashpw(request.getNewPassword())); + + // 保存用户 + userRepository.save(user); + + return true; + } + + @Override + public UserDTO getUserStats(Long userId) { + // 根据ID查找用户 + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("用户不存在")); + + // 转换为DTO + UserDTO userDTO = convertToDTO(user); + + // 获取统计数据 + userDTO.setCollectMusicCount(userCollectMusicRepository.countByUser(user)); + userDTO.setCollectPlaylistCount(userCollectPlaylistRepository.countByUser(user)); + userDTO.setCollectSingerCount(userCollectSingerRepository.countByUser(user)); + userDTO.setCreatePlaylistCount(playlistRepository.countByCreator(user)); + + return userDTO; + } + + @Override + public PageResult getUserList(Integer page, Integer size, String keyword) { + // 创建分页对象 + Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")); + + // 创建查询条件 + Specification specification = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + // 关键字查询 + if (keyword != null && !keyword.isEmpty()) { + predicates.add(criteriaBuilder.or( + criteriaBuilder.like(root.get("username"), "%" + keyword + "%"), + criteriaBuilder.like(root.get("nickname"), "%" + keyword + "%"), + criteriaBuilder.like(root.get("email"), "%" + keyword + "%") + )); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + + // 查询 + Page userPage = userRepository.findAll(specification, pageable); + + // 转换为DTO + List userDTOList = userPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + // 返回分页结果 + return new PageResult<>(userDTOList, userPage.getTotalElements(), page, size); + } + + @Override + @Transactional + public boolean deleteUser(Long id) { + // 检查用户是否存在 + if (!userRepository.existsById(id)) { + throw new RuntimeException("用户不存在"); + } + + // 删除用户 + userRepository.deleteById(id); + + return true; + } + + @Override + @Transactional + public boolean updateUserStatus(Long id, Integer status) { + // 根据ID查找用户 + User user = userRepository.findById(id) + .orElseThrow(() -> new RuntimeException("用户不存在")); + + // 更新状态 + user.setStatus(status); + + // 保存用户 + userRepository.save(user); + + return true; + } + + @Override + public UserDTO adminUpdateUser(Long id, UserUpdateRequest request) { + User user = getById(id); + + // 更新用户信息 + if (request.getNickname() != null) { + user.setNickname(request.getNickname()); + } + if (request.getAvatar() != null) { + user.setAvatar(request.getAvatar()); + } + if (request.getGender() != null) { + user.setGender(request.getGender()); + } + if (request.getEmail() != null) { + user.setEmail(request.getEmail()); + } + if (request.getPhone() != null) { + user.setPhone(request.getPhone()); + } + if (request.getIntroduction() != null) { + user.setIntroduction(request.getIntroduction()); + } + + // 保存用户 + User savedUser = userRepository.save(user); + + return convertToDTO(savedUser); + } + + @Override + public User getById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new RuntimeException("用户不存在")); + } + + /** + * 将用户实体转换为DTO + * + * @param user 用户实体 + * @return 用户DTO + */ + private UserDTO convertToDTO(User user) { + UserDTO userDTO = new UserDTO(); + BeanUtils.copyProperties(user, userDTO); + + // 确保头像URL是完整路径 + if (userDTO.getAvatar() != null && !userDTO.getAvatar().startsWith("http")) { + userDTO.setAvatar("http://localhost:8085" + userDTO.getAvatar()); + } + + return userDTO; + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/util/SwaggerAnnotationGenerator.java b/music-houduan/src/main/java/com/test/musichouduan/util/SwaggerAnnotationGenerator.java new file mode 100644 index 0000000..2eff5e5 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/util/SwaggerAnnotationGenerator.java @@ -0,0 +1,57 @@ +package com.test.musichouduan.util; + +/** + * Swagger 注解生成器 + * 用于生成控制器的接口文档注解 + */ +public class SwaggerAnnotationGenerator { + + /** + * 生成控制器类的 Tag 注解 + * + * @param name 标签名称 + * @param description 标签描述 + * @return Tag 注解字符串 + */ + public static String generateTagAnnotation(String name, String description) { + return "@Tag(name = \"" + name + "\", description = \"" + description + "\")"; + } + + /** + * 生成控制器方法的 Operation 注解 + * + * @param summary 操作摘要 + * @param description 操作描述 + * @return Operation 注解字符串 + */ + public static String generateOperationAnnotation(String summary, String description) { + return "@Operation(summary = \"" + summary + "\", description = \"" + description + "\")"; + } + + /** + * 生成控制器方法参数的 Parameter 注解 + * + * @param name 参数名称 + * @param description 参数描述 + * @param example 参数示例 + * @return Parameter 注解字符串 + */ + public static String generateParameterAnnotation(String name, String description, String example) { + if (example != null && !example.isEmpty()) { + return "@Parameter(name = \"" + name + "\", description = \"" + description + "\", example = \"" + example + "\")"; + } else { + return "@Parameter(name = \"" + name + "\", description = \"" + description + "\")"; + } + } + + /** + * 生成控制器方法的导入语句 + * + * @return 导入语句字符串 + */ + public static String generateImports() { + return "import io.swagger.v3.oas.annotations.Operation;\n" + + "import io.swagger.v3.oas.annotations.Parameter;\n" + + "import io.swagger.v3.oas.annotations.tags.Tag;"; + } +} diff --git a/music-houduan/src/main/resources/application.properties b/music-houduan/src/main/resources/application.properties new file mode 100644 index 0000000..35c27a7 --- /dev/null +++ b/music-houduan/src/main/resources/application.properties @@ -0,0 +1,25 @@ +spring.application.name=music-houduan +server.port=8085 + +spring.datasource.url=jdbc:mysql://127.0.0.1:3306/music_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai +spring.datasource.username=root +spring.datasource.password=123456 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true + +spring.data.redis.host=127.0.0.1 +spring.data.redis.port=6379 +spring.data.redis.password=123456 + +sa-token.token-name=satoken +sa-token.timeout=2592000 +sa-token.is-concurrent=true +sa-token.is-share=true +sa-token.token-style=uuid + +# 文件上传配置 +file.upload-path=C:/it/music-houduan/upload/ +file.access-path=/upload/ +file.allowed-types=jpg,jpeg,png,gif,mp3,wav,lrc,txt \ No newline at end of file diff --git a/music-houduan/src/main/resources/application.yml b/music-houduan/src/main/resources/application.yml new file mode 100644 index 0000000..81ebdf8 --- /dev/null +++ b/music-houduan/src/main/resources/application.yml @@ -0,0 +1,141 @@ +server: + port: 8085 + servlet: + context-path: / + tomcat: + max-http-form-post-size: 100MB + +spring: + application: + name: music-houduan + + # 数据库配置 + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/music_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: 123456 + hikari: + minimum-idle: 5 + maximum-pool-size: 20 + idle-timeout: 30000 + max-lifetime: 1800000 + connection-timeout: 30000 + + # JPA配置 + jpa: + database-platform: org.hibernate.dialect.MySQL8Dialect + hibernate: + ddl-auto: update + show-sql: false + properties: + hibernate: + format_sql: false + + # Redis配置 + redis: + host: 127.0.0.1 + port: 6379 + password: 123456 + database: 0 + timeout: 10000 + lettuce: + pool: + max-active: 8 + max-wait: -1 + max-idle: 8 + min-idle: 0 + + # 文件上传配置 + servlet: + multipart: + enabled: true + max-file-size: 100MB + max-request-size: 100MB + + # Jackson配置 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + serialization: + write-dates-as-timestamps: false + +# Sa-Token配置 +sa-token: + # token名称 (同时也是cookie名称) + token-name: satoken + # token有效期,单位s 默认30天, -1代表永不过期 + timeout: 2592000 + # token临时有效期 (指定时间内无操作就过期) 单位: 秒 + active-timeout: -1 + # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) + is-share: false + # token风格 + token-style: uuid + # 是否输出操作日志 + is-log: true + # 是否从cookie中读取token + is-read-cookie: true + # 是否从请求头中读取token + is-read-header: true + # 是否从请求体里读取token + is-read-body: false + # 是否在初始化配置时打印版本字符画 + is-print: false + +# 文件上传配置 +file: + # 上传路径 + upload-path: upload/ + # 访问路径 + access-path: /upload/ + # 允许的文件类型 + allowed-types: jpg,jpeg,png,gif,mp3,wav,lrc,txt + +# 日志配置 +logging: + level: + root: info + com.test.musichouduan: debug + cn.dev33.satoken: debug + org.hibernate: warn + org.hibernate.SQL: off + org.hibernate.type.descriptor.sql: off + org.springframework: warn + file: + name: logs/music-houduan.log + +# Knife4j配置 +knife4j: + enable: true + setting: + language: zh_cn + enable-swagger-models: true + enable-document-manage: true + swagger-model-name: 数据模型 + enable-version: true + enable-footer: false + enable-footer-custom: true + footer-custom-content: 音乐应用API文档 + documents: + - group: 默认 + name: 项目介绍 + locations: classpath:markdown/* + production: false + +# Swagger配置 +springdoc: + swagger-ui: + path: /swagger-ui/index.html + tags-sorter: alpha + operations-sorter: alpha + disable-swagger-default-url: true + display-request-duration: true + api-docs: + path: /v3/api-docs + group-configs: + - group: '默认' + paths-to-match: '/**' + default-flat-param-object: true diff --git a/music-houduan/upload/images/362d1d68-fcc2-4a74-a0f1-44bd38e9cb6d.jpg b/music-houduan/upload/images/362d1d68-fcc2-4a74-a0f1-44bd38e9cb6d.jpg new file mode 100644 index 0000000..496975c Binary files /dev/null and b/music-houduan/upload/images/362d1d68-fcc2-4a74-a0f1-44bd38e9cb6d.jpg differ diff --git a/music-houduan/upload/images/3efa486a-610d-4745-a54c-61f55ab3f1d5.jpg b/music-houduan/upload/images/3efa486a-610d-4745-a54c-61f55ab3f1d5.jpg new file mode 100644 index 0000000..9b14eab Binary files /dev/null and b/music-houduan/upload/images/3efa486a-610d-4745-a54c-61f55ab3f1d5.jpg differ diff --git a/music-houduan/upload/images/569f8ce5-09d2-4151-a6b1-f56a4d0b2c84.png b/music-houduan/upload/images/569f8ce5-09d2-4151-a6b1-f56a4d0b2c84.png new file mode 100644 index 0000000..d71c22a Binary files /dev/null and b/music-houduan/upload/images/569f8ce5-09d2-4151-a6b1-f56a4d0b2c84.png differ diff --git a/music-houduan/upload/images/68fa60bd-710e-443d-95f7-cb117a1a3b73.jpg b/music-houduan/upload/images/68fa60bd-710e-443d-95f7-cb117a1a3b73.jpg new file mode 100644 index 0000000..ca032b8 Binary files /dev/null and b/music-houduan/upload/images/68fa60bd-710e-443d-95f7-cb117a1a3b73.jpg differ diff --git a/music-houduan/upload/images/6c8c8d04-e583-4f12-8c56-d618b2bb06f4.jpg b/music-houduan/upload/images/6c8c8d04-e583-4f12-8c56-d618b2bb06f4.jpg new file mode 100644 index 0000000..81077fc Binary files /dev/null and b/music-houduan/upload/images/6c8c8d04-e583-4f12-8c56-d618b2bb06f4.jpg differ diff --git a/music-houduan/upload/images/7886a646-721a-4999-bb0c-0404768fe744.jpg b/music-houduan/upload/images/7886a646-721a-4999-bb0c-0404768fe744.jpg new file mode 100644 index 0000000..9b14eab Binary files /dev/null and b/music-houduan/upload/images/7886a646-721a-4999-bb0c-0404768fe744.jpg differ diff --git a/music-houduan/upload/musics/69c2c485-080f-4dd7-a541-c961652a2b8e.mp3 b/music-houduan/upload/musics/69c2c485-080f-4dd7-a541-c961652a2b8e.mp3 new file mode 100644 index 0000000..1cde2d5 Binary files /dev/null and b/music-houduan/upload/musics/69c2c485-080f-4dd7-a541-c961652a2b8e.mp3 differ diff --git a/music-qianduan/.gitignore b/music-qianduan/.gitignore new file mode 100644 index 0000000..ec9e9aa --- /dev/null +++ b/music-qianduan/.gitignore @@ -0,0 +1,80 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +### Example user template template +### Example user template + +# IntelliJ project files +.log +*.gz +.gz +*/.gz +*/.log +target +*.iml +out +gen +*/.vscode +.vscode +#忽略所有.svn目录 +.svn/ +#忽略所有target目录 +target/ +#忽略所有.idea目录及其内容 +.idea/ +.idea/* +#忽略所有.ipr文件 +*.ipr +#忽略所有.iws文件 +*.iws +#忽略所有.gradle目录 +.gradle/ +#忽略所有.settings目录 +.settings/ +#忽略所有.classpath文件 +.classpath +#忽略所有.project文件 +.project +#忽略所有.pydevproject文件 +.pydevproject +#忽略所有.DS_Store文件 + +#忽略所有.class文件 +*.class +#忽略所有.log文件 + +#忽略所有.tmp文件 +*.tmp +#忽略所有.swp文件 +*.swp +node_modules/ +/dist/ + +.git + +# Editor directories and files +dist/ +npm-debug.log +.vscode/ +README.md \ No newline at end of file diff --git a/music-qianduan/index.html b/music-qianduan/index.html new file mode 100644 index 0000000..8388c4b --- /dev/null +++ b/music-qianduan/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Vue + + +
+ + + diff --git a/music-qianduan/package-lock.json b/music-qianduan/package-lock.json new file mode 100644 index 0000000..0c9d9a3 --- /dev/null +++ b/music-qianduan/package-lock.json @@ -0,0 +1,2075 @@ +{ + "name": "music-qianduan", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "music-qianduan", + "version": "0.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@stomp/stompjs": "^7.1.1", + "axios": "^1.8.4", + "echarts": "^5.6.0", + "element-plus": "^2.9.8", + "pinia": "^3.0.2", + "sockjs-client": "^1.6.1", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.2", + "vite": "^6.3.1" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz", + "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@stomp/stompjs": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.1.1.tgz", + "integrity": "sha512-chcDs6YkAnKp1FqzwhGvh3i7v0+/ytzqWdKYw6XzINEKAzke/iD00dNgFPWSZEqktHOK+C1gSzXhLkLbARIaZw==", + "license": "Apache-2.0" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", + "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.5.tgz", + "integrity": "sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.5" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.5.tgz", + "integrity": "sha512-S9VAVJYVAe4RPx2JZb9ZTEi0lqTySz2CBeF0wHT5D3dkTLnT9yMMGegKNl4b2EIELwLSkcI9bl2qp0/jW+upqA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.5", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.5.tgz", + "integrity": "sha512-QBjG72RfpM0DKtpns2RZOxBltO226kOAls9e4Lri6YxS2gWTgL0H+wj1R2K76lxxIeOrqo4+2Ty6RQnzv+WSTQ==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "license": "MIT", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/birpc": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz", + "integrity": "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/element-plus": { + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.8.tgz", + "integrity": "sha512-srViUaUdfblBKGMeuEPiXxxKlH5aUmKqEwmhb/At9Sj91DbU6od/jYN1955cTnzt3wTSA7GfnZF7UiRX9sdRHg==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.1", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.13", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.2.tgz", + "integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://tidelift.com/funding/github/npm/sockjs-client" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", + "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.3", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.12" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", + "integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/music-qianduan/package.json b/music-qianduan/package.json new file mode 100644 index 0000000..9964354 --- /dev/null +++ b/music-qianduan/package.json @@ -0,0 +1,26 @@ +{ + "name": "music-qianduan", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@stomp/stompjs": "^7.1.1", + "axios": "^1.8.4", + "echarts": "^5.6.0", + "element-plus": "^2.9.8", + "pinia": "^3.0.2", + "sockjs-client": "^1.6.1", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.2", + "vite": "^6.3.1" + } +} diff --git a/music-qianduan/public/vite.svg b/music-qianduan/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/music-qianduan/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/music-qianduan/src/App.vue b/music-qianduan/src/App.vue new file mode 100644 index 0000000..2af413c --- /dev/null +++ b/music-qianduan/src/App.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/music-qianduan/src/api/admin/comment.js b/music-qianduan/src/api/admin/comment.js new file mode 100644 index 0000000..a94835d --- /dev/null +++ b/music-qianduan/src/api/admin/comment.js @@ -0,0 +1,43 @@ +import api from '../index' + +/** + * 分页获取评论列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @param {string} keyword 关键字 + * @param {number} targetType 目标类型:1-音乐,2-歌单,3-歌手 + * @param {number} status 状态:0-禁用,1-正常 + * @returns {Promise} + */ +export function getCommentList(page = 1, size = 10, keyword = '', targetType = null, status = null) { + return api.get('/admin/comment/list', { + params: { + page, + size, + keyword, + targetType, + status + } + }) +} + +/** + * 更新评论状态 + * @param {number} id 评论ID + * @param {number} status 状态:0-禁用,1-正常 + * @returns {Promise} + */ +export function updateCommentStatus(id, status) { + return api.put(`/admin/comment/${id}/status`, null, { + params: { status } + }) +} + +/** + * 删除评论 + * @param {number} id 评论ID + * @returns {Promise} + */ +export function deleteComment(id) { + return api.delete(`/admin/comment/${id}`) +} diff --git a/music-qianduan/src/api/admin/dashboard.js b/music-qianduan/src/api/admin/dashboard.js new file mode 100644 index 0000000..cc89a1e --- /dev/null +++ b/music-qianduan/src/api/admin/dashboard.js @@ -0,0 +1,9 @@ +import api from '../index' + +/** + * 获取控制台统计数据 + * @returns {Promise} + */ +export function getDashboardStats() { + return api.get('/admin/dashboard/stats') +} diff --git a/music-qianduan/src/api/admin/type.js b/music-qianduan/src/api/admin/type.js new file mode 100644 index 0000000..4b208a2 --- /dev/null +++ b/music-qianduan/src/api/admin/type.js @@ -0,0 +1,106 @@ +import api from '../index' + +/** + * 音乐类型相关接口 + */ + +// 获取音乐类型列表 +export function getMusicTypeList() { + return api.get('/type/music') +} + +// 分页获取音乐类型列表 +export function getMusicTypePage(page = 1, size = 7, keyword = '') { + return api.get('/type/music/page', { + params: { + page, + size, + keyword + } + }) +} + +// 添加音乐类型 +export function addMusicType(data) { + return api.post('/type/music/add', data) +} + +// 更新音乐类型 +export function updateMusicType(id, data) { + return api.put(`/type/music/${id}`, data) +} + +// 删除音乐类型 +export function deleteMusicType(id) { + return api.delete(`/type/music/${id}`) +} + +/** + * 歌手类型相关接口 + */ + +// 获取歌手类型列表 +export function getSingerTypeList() { + return api.get('/type/singer') +} + +// 分页获取歌手类型列表 +export function getSingerTypePage(page = 1, size = 7, keyword = '') { + return api.get('/type/singer/page', { + params: { + page, + size, + keyword + } + }) +} + +// 添加歌手类型 +export function addSingerType(data) { + return api.post('/type/singer/add', data) +} + +// 更新歌手类型 +export function updateSingerType(id, data) { + return api.put(`/type/singer/${id}`, data) +} + +// 删除歌手类型 +export function deleteSingerType(id) { + return api.delete(`/type/singer/${id}`) +} + +/** + * 歌单类型相关接口 + */ + +// 获取歌单类型列表 +export function getPlaylistTypeList() { + return api.get('/type/playlist') +} + +// 分页获取歌单类型列表 +export function getPlaylistTypePage(page = 1, size = 7, keyword = '') { + return api.get('/type/playlist/page', { + params: { + page, + size, + keyword + } + }) +} + +// 添加歌单类型 +export function addPlaylistType(data) { + return api.post('/type/playlist/add', data) +} + +// 更新歌单类型 +export function updatePlaylistType(id, data) { + return api.put(`/type/playlist/${id}`, data) +} + +// 删除歌单类型 +export function deletePlaylistType(id) { + return api.delete(`/type/playlist/${id}`) +} diff --git a/music-qianduan/src/api/admin/user.js b/music-qianduan/src/api/admin/user.js new file mode 100644 index 0000000..37311d8 --- /dev/null +++ b/music-qianduan/src/api/admin/user.js @@ -0,0 +1,69 @@ +import api from '../index' + +/** + * 分页获取用户列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @param {string} keyword 搜索关键字 + * @returns {Promise} + */ +export function getUserList(page = 1, size = 10, keyword = '') { + return api.get('/user/list', { + params: { + page, + size, + keyword + } + }) +} + +/** + * 根据ID获取用户 + * @param {number} id 用户ID + * @returns {Promise} + */ +export function getUserById(id) { + return api.get(`/user/${id}`) +} + +/** + * 删除用户 + * @param {number} id 用户ID + * @returns {Promise} + */ +export function deleteUser(id) { + return api.delete(`/user/${id}`) +} + +/** + * 更新用户状态 + * @param {number} id 用户ID + * @param {number} status 状态,0-禁用,1-启用 + * @returns {Promise} + */ +export function updateUserStatus(id, status) { + return api.put(`/user/${id}/status`, null, { + params: { + status + } + }) +} + +/** + * 更新用户信息 + * @param {Object} data 用户信息 + * @returns {Promise} + */ +export function updateUser(data) { + return api.put('/user/update', data) +} + +/** + * 管理员更新用户信息 + * @param {number} id 用户ID + * @param {Object} data 用户信息 + * @returns {Promise} + */ +export function adminUpdateUser(id, data) { + return api.put(`/user/${id}`, data) +} diff --git a/music-qianduan/src/api/banner.js b/music-qianduan/src/api/banner.js new file mode 100644 index 0000000..287d9c4 --- /dev/null +++ b/music-qianduan/src/api/banner.js @@ -0,0 +1,162 @@ +import api from './index' +import { processImageUrl } from '../utils/imageUtils' + +/** + * 获取轮播图列表 + * @returns {Promise} + */ +export function getBannerList() { + return api.get('/banner/list').then(res => { + if (res.code === 200 && res.data) { + // 处理数据格式,适配后端返回的数据结构 + // 如果有 data.list,则使用 data.list 作为记录列表 + const banners = res.data.list || res.data; + + // 处理图片URL + const processedBanners = Array.isArray(banners) ? banners.map(banner => ({ + ...banner, + imageUrl: processImageUrl(banner.imageUrl) + })) : []; + + // 替换原来的数据 + if (res.data.list) { + res.data.list = processedBanners; + } else { + res.data = processedBanners; + } + } + return res; + }); +} + + + +/** + * 分页获取轮播图列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function getBannerPage(page = 1, size = 10) { + return api.get('/banner/page', { + params: { + page, + size + } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理数据格式,适配后端返回的数据结构 + // 如果有 data.list,则使用 data.list 作为记录列表 + const records = res.data.list || res.data.records || []; + const total = res.data.total || 0; + + // 处理图片URL + const processedRecords = records.map(banner => ({ + ...banner, + imageUrl: processImageUrl(banner.imageUrl) + })); + + // 构造标准的分页响应格式 + res.data = { + records: processedRecords, + total: total, + current: res.data.page || page, + size: res.data.size || size, + pages: res.data.totalPages || Math.ceil(total / size) + }; + } + return res; + }); +} + +/** + * 根据ID获取轮播图 + * @param {number} id 轮播图ID + * @returns {Promise} + */ +export function getBannerById(id) { + return api.get(`/banner/${id}`).then(res => { + if (res.code === 200 && res.data) { + // 处理图片URL + res.data.imageUrl = processImageUrl(res.data.imageUrl) + } + return res + }) +} + +/** + * 添加轮播图 + * @param {Object} data 轮播图数据 + * @param {File} image 图片文件 + * @returns {Promise} + */ +export function addBanner(data, image) { + const formData = new FormData() + formData.append('banner', new Blob([JSON.stringify(data)], { type: 'application/json' })) + formData.append('image', image) + + return api.post('/banner/add', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 更新轮播图 + * @param {number} id 轮播图ID + * @param {Object} data 轮播图数据 + * @param {File} image 图片文件 + * @returns {Promise} + */ +export function updateBanner(id, data, image) { + const formData = new FormData() + formData.append('banner', new Blob([JSON.stringify(data)], { type: 'application/json' })) + + if (image) { + formData.append('image', image) + } + + return api.put(`/banner/${id}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 删除轮播图 + * @param {number} id 轮播图ID + * @returns {Promise} + */ +export function deleteBanner(id) { + return api.delete(`/banner/${id}`) +} + +/** + * 更新轮播图状态 + * @param {number} id 轮播图ID + * @param {number} status 状态,0-禁用,1-启用 + * @returns {Promise} + */ +export function updateBannerStatus(id, status) { + return api.put(`/banner/${id}/status`, null, { + params: { + status + } + }) +} + +/** + * 更新轮播图排序 + * @param {number} id 轮播图ID + * @param {number} orderNum 排序号 + * @returns {Promise} + */ +export function updateBannerOrder(id, orderNum) { + return api.put(`/banner/${id}/order`, null, { + params: { + orderNum + } + }) +} diff --git a/music-qianduan/src/api/comment.js b/music-qianduan/src/api/comment.js new file mode 100644 index 0000000..95db7f0 --- /dev/null +++ b/music-qianduan/src/api/comment.js @@ -0,0 +1,57 @@ +import api from './index' + +/** + * 分页获取评论列表 + * @param {number} targetId 目标ID + * @param {number} targetType 目标类型:1-音乐,2-歌单,3-歌手 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function getCommentList(targetId, targetType, page = 1, size = 10) { + return api.get('/comment/list', { + params: { + targetId, + targetType, + page, + size + } + }) +} + +/** + * 添加评论 + * @param {Object} data 评论数据 + * @param {number} data.targetId 目标ID + * @param {number} data.targetType 目标类型:1-音乐,2-歌单,3-歌手 + * @param {string} data.content 评论内容 + * @param {number} [data.parentId] 父评论ID(可选,用于回复评论) + * @returns {Promise} + */ +export function addComment(data) { + return api.post('/comment/add', data) +} + +/** + * 删除评论 + * @param {number} id 评论ID + * @returns {Promise} + */ +export function deleteComment(id) { + return api.delete(`/comment/${id}`) +} + +/** + * 获取用户的评论列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function getUserCommentList(page = 1, size = 10) { + return api.get('/comment/user', { + params: { + page, + size + } + }) +} diff --git a/music-qianduan/src/api/index.js b/music-qianduan/src/api/index.js new file mode 100644 index 0000000..7217ed7 --- /dev/null +++ b/music-qianduan/src/api/index.js @@ -0,0 +1,123 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' +import router from '../router' + +// 创建axios实例 +const api = axios.create({ + baseURL: '/api', // 基础URL + timeout: 10000 // 请求超时时间 +}) + +// 请求拦截器 +api.interceptors.request.use( + config => { + // 打印请求URL和方法 + console.log(`发送请求: ${config.method.toUpperCase()} ${config.url}`) + + // 从localStorage获取token相关信息 + const token = localStorage.getItem('token') + const tokenName = localStorage.getItem('tokenName') || 'satoken' + const tokenPrefix = localStorage.getItem('tokenPrefix') || '' + + if (token) { + // 将token添加到请求头 + // Sa-Token默认从请求头的tokenName字段中读取token + config.headers[tokenName] = tokenPrefix + token + + // 添加到Authorization头中,以防万一 + config.headers['Authorization'] = tokenPrefix + token + + // 打印请求头信息,便于调试 + console.log('请求头信息:', { + url: config.url, + method: config.method, + tokenName, + tokenValue: tokenPrefix + token, + headers: config.headers + }) + } else { + console.warn('未找到token,请求可能会被拒绝') + } + return config + }, + error => { + console.error('请求错误:', error) + return Promise.reject(error) + } +) + +// 响应拦截器 +api.interceptors.response.use( + response => { + const res = response.data + const url = response.config.url + const method = response.config.method + + // 打印响应信息 + console.log(`收到响应: ${method.toUpperCase()} ${url}`, res) + + // 如果返回的状态码不是200,说明接口请求失败 + if (res.code !== 200) { + // 根据不同的错误码进行不同的处理 + if (res.code === 401) { + // 未登录或token过期 + console.warn('响应拦截器: 登录已过期,请重新登录', { + url, + method, + response: res, + headers: response.config.headers, + token: localStorage.getItem('token'), + tokenName: localStorage.getItem('tokenName'), + tokenPrefix: localStorage.getItem('tokenPrefix') + }) + + ElMessage.error('登录已过期,请重新登录') + + // 清除本地存储的所有信息 + localStorage.removeItem('token') + localStorage.removeItem('tokenName') + localStorage.removeItem('tokenPrefix') + localStorage.removeItem('user') + localStorage.removeItem('isAdmin') + + // 跳转到登录页面 + router.push('/login?redirect=' + encodeURIComponent(window.location.pathname)) + } else if (res.code === 403) { + // 无权限 + ElMessage.error('无权限访问') + } else { + // 其他错误 + ElMessage.error(res.msg || '未知错误') + } + return Promise.reject(new Error(res.msg || '未知错误')) + } else { + return res + } + }, + error => { + console.error('响应错误:', error) + + // 尝试获取请求配置 + const config = error.config || {} + console.error('错误请求配置:', { + url: config.url, + method: config.method, + headers: config.headers, + token: localStorage.getItem('token'), + tokenName: localStorage.getItem('tokenName'), + tokenPrefix: localStorage.getItem('tokenPrefix') + }) + + // 处理网络错误 + if (error.message && error.message.includes('timeout')) { + ElMessage.error('请求超时,请稍后再试') + } else if (error.message && error.message.includes('Network Error')) { + ElMessage.error('网络错误,请检查网络连接') + } else { + ElMessage.error(error.message || '未知错误') + } + return Promise.reject(error) + } +) + +export default api diff --git a/music-qianduan/src/api/music.js b/music-qianduan/src/api/music.js new file mode 100644 index 0000000..6eef1e1 --- /dev/null +++ b/music-qianduan/src/api/music.js @@ -0,0 +1,242 @@ +import api from './index' + +/** + * 获取音乐列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @param {string} keyword 关键字 + * @param {number} typeId 类型ID + * @returns {Promise} + */ +export function getMusicList(page = 1, size = 10, keyword = '', typeId = null) { + return api.get('/music/list', { + params: { + page, + size, + keyword, + typeId + } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理数据结构 + const data = res.data + if (data.list) { + // 处理歌手和类型信息 + data.list.forEach(item => { + // 处理音乐URL + if (item.url) { + item.url = processMusicUrl(item.url) + } + // 处理封面URL + if (item.cover) { + item.cover = processMusicUrl(item.cover) + } + }) + } + } + return res + }) +} + +/** + * 根据ID获取音乐 + * @param {number} id 音乐ID + * @returns {Promise} + */ +export function getMusicById(id) { + return api.get(`/music/${id}`).then(res => { + if (res.code === 200 && res.data) { + // 处理音乐URL + if (res.data.url) { + res.data.url = processMusicUrl(res.data.url) + } + // 处理封面URL + if (res.data.cover) { + res.data.cover = processMusicUrl(res.data.cover) + } + } + return res + }) +} + +/** + * 获取热门音乐 + * @param {number} limit 限制数量 + * @returns {Promise} + */ +export function getHotMusic(limit = 10) { + return api.get('/music/hot', { + params: { limit } + }) +} + +/** + * 获取最新音乐 + * @param {number} limit 限制数量 + * @returns {Promise} + */ +export function getLatestMusic(limit = 10) { + return api.get('/music/latest', { + params: { limit } + }) +} + +/** + * 根据歌手ID获取音乐列表 + * @param {number} singerId 歌手ID + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function getMusicBySinger(singerId, page = 1, size = 10) { + return api.get(`/music/singer/${singerId}`, { + params: { + page, + size + } + }) +} + +/** + * 播放音乐 + * @param {number} id 音乐ID + * @returns {Promise} + */ +export function playMusic(id) { + return api.post(`/music/${id}/play`) +} + +/** + * 收藏/取消收藏音乐 + * @param {number} id 音乐ID + * @returns {Promise} + */ +export function toggleCollectMusic(id) { + return api.post(`/music/${id}/collect`) +} + +/** + * 获取收藏的音乐列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function getCollectedMusic(page = 1, size = 10) { + return api.get('/music/collect', { + params: { + page, + size + } + }) +} + +/** + * 搜索音乐 + * @param {string} keyword 搜索关键词 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function searchMusic(keyword, page = 1, size = 10) { + return api.get('/music/search', { + params: { + keyword, + page, + size + } + }) +} + +/** + * 添加音乐(管理员) + * @param {Object} data 音乐数据 + * @param {File} file 音乐文件 + * @param {File} cover 封面文件 + * @returns {Promise} + */ +export function addMusic(data, file, cover) { + const formData = new FormData() + formData.append('music', new Blob([JSON.stringify(data)], { type: 'application/json' })) + + if (file) { + formData.append('file', file) + } + + if (cover) { + formData.append('cover', cover) + } + + return api.post('/music/add', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 更新音乐(管理员) + * @param {number} id 音乐ID + * @param {Object} data 音乐数据 + * @param {File} file 音乐文件 + * @param {File} cover 封面文件 + * @param {File} lyric 歌词文件 + * @returns {Promise} + */ +export function updateMusic(id, data, file, cover, lyric) { + const formData = new FormData() + formData.append('music', new Blob([JSON.stringify(data)], { type: 'application/json' })) + + if (file) { + formData.append('file', file) + } + + if (cover) { + formData.append('cover', cover) + } + + if (lyric) { + formData.append('lyric', lyric) + } + + return api.put(`/music/${id}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 删除音乐(管理员) + * @param {number} id 音乐ID + * @returns {Promise} + */ +export function deleteMusic(id) { + return api.delete(`/music/${id}`) +} + +/** + * 处理音乐URL + * @param {string} url 音乐URL + * @returns {string} 处理后的URL + */ +export function processMusicUrl(url) { + if (!url) { + return ''; + } + + // 如果是完整URL,直接返回 + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + + // 如果是相对路径,添加API基础URL + // 使用后端服务器地址 + const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8085'; + + // 确保路径以/开头 + if (!url.startsWith('/')) { + url = '/' + url; + } + + return `${baseUrl}${url}`; +} diff --git a/music-qianduan/src/api/musicType.js b/music-qianduan/src/api/musicType.js new file mode 100644 index 0000000..be6a74a --- /dev/null +++ b/music-qianduan/src/api/musicType.js @@ -0,0 +1,63 @@ +import api from './index' + +/** + * 获取音乐类型列表 + * @returns {Promise} + */ +export function getMusicTypeList() { + return api.get('/type/music') +} + +/** + * 分页获取音乐类型列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @param {string} keyword 关键字 + * @returns {Promise} + */ +export function getMusicTypePage(page = 1, size = 10, keyword = '') { + return api.get('/type/music/page', { + params: { + page, + size, + keyword + } + }) +} + +/** + * 根据ID获取音乐类型 + * @param {number} id 音乐类型ID + * @returns {Promise} + */ +export function getMusicTypeById(id) { + return api.get(`/type/music/${id}`) +} + +/** + * 添加音乐类型(管理员) + * @param {Object} data 音乐类型数据 + * @returns {Promise} + */ +export function addMusicType(data) { + return api.post('/type/music/add', data) +} + +/** + * 更新音乐类型(管理员) + * @param {number} id 音乐类型ID + * @param {Object} data 音乐类型数据 + * @returns {Promise} + */ +export function updateMusicType(id, data) { + return api.put(`/type/music/${id}`, data) +} + +/** + * 删除音乐类型(管理员) + * @param {number} id 音乐类型ID + * @returns {Promise} + */ +export function deleteMusicType(id) { + return api.delete(`/type/music/${id}`) +} diff --git a/music-qianduan/src/api/playlist.js b/music-qianduan/src/api/playlist.js new file mode 100644 index 0000000..5b56779 --- /dev/null +++ b/music-qianduan/src/api/playlist.js @@ -0,0 +1,362 @@ +import api from './index' +import { processMusicUrl } from './music' +import { processImageUrl } from '../utils/imageUtils' + +/** + * 获取歌单列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @param {string} keyword 关键字 + * @param {number} typeId 类型ID + * @returns {Promise} + */ +export function getPlaylistList(page = 1, size = 10, keyword = '', typeId = null) { + return api.get('/playlist/list', { + params: { + page, + size, + keyword, + typeId + } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理数据结构 + const records = res.data.records || res.data.list || [] + + // 处理封面URL + const processedRecords = records.map(item => ({ + ...item, + cover: processImageUrl(item.cover) + })) + + if (res.data.records) { + res.data.records = processedRecords + } else if (res.data.list) { + res.data.list = processedRecords + } else { + res.data = processedRecords + } + } + return res + }) +} + +/** + * 根据ID获取歌单 + * @param {number} id 歌单ID + * @returns {Promise} + */ +export function getPlaylistById(id) { + return api.get(`/playlist/${id}`).then(res => { + if (res.code === 200 && res.data) { + // 处理封面URL + res.data.cover = processImageUrl(res.data.cover) + + // 处理音乐列表 + if (res.data.musics && res.data.musics.length > 0) { + res.data.musics = res.data.musics.map(music => ({ + ...music, + cover: processMusicUrl(music.cover), + url: processMusicUrl(music.url) + })) + } + } + return res + }) +} + +/** + * 获取热门歌单 + * @param {number} limit 限制数量 + * @returns {Promise} + */ +export function getHotPlaylist(limit = 10) { + return api.get('/playlist/hot', { + params: { limit } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理封面URL + res.data = res.data.map(item => ({ + ...item, + cover: processImageUrl(item.cover) + })) + } + return res + }) +} + +/** + * 获取最新歌单 + * @param {number} limit 限制数量 + * @returns {Promise} + */ +export function getLatestPlaylist(limit = 10) { + return api.get('/playlist/latest', { + params: { limit } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理封面URL + res.data = res.data.map(item => ({ + ...item, + cover: processImageUrl(item.cover) + })) + } + return res + }) +} + +/** + * 获取官方歌单 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function getOfficialPlaylist(page = 1, size = 10) { + return api.get('/playlist/official', { + params: { + page, + size + } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理数据结构 + const records = res.data.records || res.data.list || [] + + // 处理封面URL + const processedRecords = records.map(item => ({ + ...item, + cover: processImageUrl(item.cover) + })) + + if (res.data.records) { + res.data.records = processedRecords + } else if (res.data.list) { + res.data.list = processedRecords + } else { + res.data = processedRecords + } + } + return res + }) +} + +/** + * 获取用户创建的歌单 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function getUserPlaylist(page = 1, size = 10) { + return api.get('/playlist/user', { + params: { + page, + size + } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理数据结构 + const records = res.data.records || res.data.list || [] + + // 处理封面URL + const processedRecords = records.map(item => ({ + ...item, + cover: processImageUrl(item.cover) + })) + + if (res.data.records) { + res.data.records = processedRecords + } else if (res.data.list) { + res.data.list = processedRecords + } else { + res.data = processedRecords + } + } + return res + }) +} + +/** + * 获取用户收藏的歌单 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function getCollectedPlaylist(page = 1, size = 10) { + return api.get('/playlist/collect', { + params: { + page, + size + } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理数据结构 + const records = res.data.records || res.data.list || [] + + // 处理封面URL + const processedRecords = records.map(item => ({ + ...item, + cover: processImageUrl(item.cover) + })) + + if (res.data.records) { + res.data.records = processedRecords + } else if (res.data.list) { + res.data.list = processedRecords + } else { + res.data = processedRecords + } + } + return res + }) +} + +/** + * 添加歌单 + * @param {Object} data 歌单数据 + * @param {File} cover 封面文件 + * @returns {Promise} + */ +export function addPlaylist(data, cover) { + // 确保数据中包含 type 对象 + const playlistData = { + ...data, + type: data.typeId ? { id: data.typeId } : null, + // 确保 isOfficial 字段存在且为数字 + isOfficial: data.isOfficial !== undefined ? Number(data.isOfficial) : 0 + } + + console.log('添加歌单数据:', playlistData) + + const formData = new FormData() + formData.append('playlist', new Blob([JSON.stringify(playlistData)], { type: 'application/json' })) + + if (cover) { + formData.append('cover', cover) + } + + return api.post('/playlist/add', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 更新歌单 + * @param {number} id 歌单ID + * @param {Object} data 歌单数据 + * @param {File} cover 封面文件 + * @returns {Promise} + */ +export function updatePlaylist(id, data, cover) { + console.log('原始数据:', data) + console.log('isOfficial类型:', typeof data.isOfficial, 'isOfficial值:', data.isOfficial) + + // 确保数据中包含 type 对象 + const playlistData = { + ...data, + type: data.typeId ? { id: data.typeId } : null, + // 显式设置 isOfficial 字段,确保它是数字类型 + isOfficial: data.isOfficial === 1 ? 1 : 0 + } + + console.log('处理后的数据:', playlistData) + console.log('处理后 isOfficial类型:', typeof playlistData.isOfficial, 'isOfficial值:', playlistData.isOfficial) + + const formData = new FormData() + formData.append('playlist', new Blob([JSON.stringify(playlistData)], { type: 'application/json' })) + + if (cover) { + formData.append('cover', cover) + } + + return api.put(`/playlist/${id}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 删除歌单 + * @param {number} id 歌单ID + * @returns {Promise} + */ +export function deletePlaylist(id) { + return api.delete(`/playlist/${id}`) +} + +/** + * 收藏/取消收藏歌单 + * @param {number} id 歌单ID + * @returns {Promise} + */ +export function toggleCollectPlaylist(id) { + return api.post(`/playlist/${id}/collect`) +} + +/** + * 向歌单添加音乐 + * @param {number} id 歌单ID + * @param {number} musicId 音乐ID + * @returns {Promise} + */ +export function addMusicToPlaylist(id, musicId) { + return api.post(`/playlist/${id}/music/${musicId}`) +} + +/** + * 从歌单移除音乐 + * @param {number} id 歌单ID + * @param {number} musicId 音乐ID + * @returns {Promise} + */ +export function removeMusicFromPlaylist(id, musicId) { + return api.delete(`/playlist/${id}/music/${musicId}`) +} + +/** + * 播放歌单 + * @param {number} id 歌单ID + * @returns {Promise} + */ +export function playPlaylist(id) { + return api.post(`/playlist/${id}/play`) +} + +/** + * 搜索歌单 + * @param {string} keyword 搜索关键词 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function searchPlaylist(keyword, page = 1, size = 10) { + return api.get('/playlist/search', { + params: { + keyword, + page, + size + } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理数据结构 + const records = res.data.records || res.data.list || [] + + // 处理封面URL + const processedRecords = records.map(item => ({ + ...item, + cover: processImageUrl(item.cover) + })) + + if (res.data.records) { + res.data.records = processedRecords + } else if (res.data.list) { + res.data.list = processedRecords + } else { + res.data = processedRecords + } + } + return res + }) +} diff --git a/music-qianduan/src/api/playlistType.js b/music-qianduan/src/api/playlistType.js new file mode 100644 index 0000000..a4367e3 --- /dev/null +++ b/music-qianduan/src/api/playlistType.js @@ -0,0 +1,63 @@ +import api from './index' + +/** + * 获取歌单类型列表 + * @returns {Promise} + */ +export function getPlaylistTypeList() { + return api.get('/type/playlist') +} + +/** + * 分页获取歌单类型列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @param {string} keyword 关键字 + * @returns {Promise} + */ +export function getPlaylistTypePage(page = 1, size = 10, keyword = '') { + return api.get('/type/playlist/page', { + params: { + page, + size, + keyword + } + }) +} + +/** + * 根据ID获取歌单类型 + * @param {number} id 歌单类型ID + * @returns {Promise} + */ +export function getPlaylistTypeById(id) { + return api.get(`/type/playlist/${id}`) +} + +/** + * 添加歌单类型(管理员) + * @param {Object} data 歌单类型数据 + * @returns {Promise} + */ +export function addPlaylistType(data) { + return api.post('/type/playlist/add', data) +} + +/** + * 更新歌单类型(管理员) + * @param {number} id 歌单类型ID + * @param {Object} data 歌单类型数据 + * @returns {Promise} + */ +export function updatePlaylistType(id, data) { + return api.put(`/type/playlist/${id}`, data) +} + +/** + * 删除歌单类型(管理员) + * @param {number} id 歌单类型ID + * @returns {Promise} + */ +export function deletePlaylistType(id) { + return api.delete(`/type/playlist/${id}`) +} diff --git a/music-qianduan/src/api/singer.js b/music-qianduan/src/api/singer.js new file mode 100644 index 0000000..d4852cf --- /dev/null +++ b/music-qianduan/src/api/singer.js @@ -0,0 +1,244 @@ +import api from './index' +import { processImageUrl } from '../utils/imageUtils' + +/** + * 获取歌手列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @param {string} keyword 关键字 + * @param {number} typeId 类型ID + * @returns {Promise} + */ +export function getSingerList(page = 1, size = 10, keyword = '', typeId = null) { + return api.get('/singer/list', { + params: { + page, + size, + keyword, + typeId + } + }).then(res => { + if (res.code === 200 && res.data && res.data.list) { + // 处理歌手头像URL + res.data.list.forEach(singer => { + if (singer.avatar) { + singer.avatar = processImageUrl(singer.avatar) + } + }) + } + return res + }) +} + +/** + * 根据ID获取歌手 + * @param {number} id 歌手ID + * @returns {Promise} + */ +export function getSingerById(id) { + return api.get(`/singer/${id}`).then(res => { + if (res.code === 200 && res.data) { + // 处理歌手头像URL + if (res.data.avatar) { + res.data.avatar = processImageUrl(res.data.avatar) + } + } + return res + }) +} + +/** + * 获取最新歌手 + * @param {number} limit 限制数量 + * @returns {Promise} + */ +export function getLatestSinger(limit = 10) { + return api.get('/singer/latest', { + params: { limit } + }) +} + +/** + * 添加歌手(管理员) + * @param {Object} data 歌手数据 + * @param {File} avatar 头像文件 + * @returns {Promise} + */ +export function addSinger(data, avatar) { + const formData = new FormData() + formData.append('singer', new Blob([JSON.stringify(data)], { type: 'application/json' })) + + if (avatar) { + formData.append('avatar', avatar) + } + + return api.post('/singer/add', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 更新歌手(管理员) + * @param {number} id 歌手ID + * @param {Object} data 歌手数据 + * @param {File} avatar 头像文件 + * @returns {Promise} + */ +export function updateSinger(id, data, avatar) { + const formData = new FormData() + formData.append('singer', new Blob([JSON.stringify(data)], { type: 'application/json' })) + + if (avatar) { + formData.append('avatar', avatar) + } + + return api.put(`/singer/${id}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 删除歌手(管理员) + * @param {number} id 歌手ID + * @returns {Promise} + */ +export function deleteSinger(id) { + return api.delete(`/singer/${id}`) +} + +/** + * 收藏歌手 + * @param {number} id 歌手ID + * @returns {Promise} + */ +export function collectSinger(id) { + return api.post(`/singer/${id}/collect`) +} + +/** + * 取消收藏歌手 + * @param {number} id 歌手ID + * @returns {Promise} + */ +export function uncollectSinger(id) { + return api.delete(`/singer/${id}/collect`) +} + +/** + * 获取收藏的歌手列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function getCollectedSinger(page = 1, size = 10) { + return api.get('/singer/collect', { + params: { + page, + size + } + }) +} + +/** + * 搜索歌手 + * @param {string} keyword 搜索关键词 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @returns {Promise} + */ +export function searchSinger(keyword, page = 1, size = 10) { + return api.get('/singer/search', { + params: { + keyword, + page, + size + } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理数据结构 + const records = res.data.records || res.data.list || [] + + // 处理头像URL + const processedRecords = records.map(item => ({ + ...item, + avatar: processImageUrl(item.avatar) + })) + + if (res.data.records) { + res.data.records = processedRecords + } else if (res.data.list) { + res.data.list = processedRecords + } else { + res.data = processedRecords + } + } + return res + }) +} + +/** + * 获取热门歌手 + * @param {number} limit 限制数量 + * @returns {Promise} + */ +export function getHotSinger(limit = 10) { + return api.get('/singer/hot-list', { + params: { limit } + }).then(res => { + if (res.code === 200 && res.data) { + // 处理头像URL + res.data = res.data.map(item => ({ + ...item, + avatar: processImageUrl(item.avatar) + })) + } + return res + }) +} + +/** + * 收藏/取消收藏歌手 + * @param {number} id 歌手ID + * @returns {Promise} + */ +export function toggleCollectSinger(id) { + // 检查是否已收藏 + return api.get(`/singer/${id}`).then(res => { + if (res.code === 200) { + const isCollected = res.data.isCollected === true + + // 根据当前收藏状态决定调用收藏或取消收藏接口 + if (isCollected) { + return api.delete(`/singer/${id}/collect`) + } else { + return api.post(`/singer/${id}/collect`) + } + } else { + return Promise.reject(new Error(res.message || '获取歌手信息失败')) + } + }) +} + +/** + * 添加音乐到歌手 + * @param {number} singerId 歌手ID + * @param {number} musicId 音乐ID + * @returns {Promise} + */ +export function addMusicToSinger(singerId, musicId) { + return api.post(`/singer/${singerId}/music/${musicId}`) +} + +/** + * 从歌手移除音乐 + * @param {number} singerId 歌手ID + * @param {number} musicId 音乐ID + * @returns {Promise} + */ +export function removeMusicFromSinger(singerId, musicId) { + return api.delete(`/singer/${singerId}/music/${musicId}`) +} diff --git a/music-qianduan/src/api/singerType.js b/music-qianduan/src/api/singerType.js new file mode 100644 index 0000000..40372ff --- /dev/null +++ b/music-qianduan/src/api/singerType.js @@ -0,0 +1,63 @@ +import api from './index' + +/** + * 获取歌手类型列表 + * @returns {Promise} + */ +export function getSingerTypeList() { + return api.get('/type/singer') +} + +/** + * 分页获取歌手类型列表 + * @param {number} page 页码 + * @param {number} size 每页大小 + * @param {string} keyword 关键字 + * @returns {Promise} + */ +export function getSingerTypePage(page = 1, size = 10, keyword = '') { + return api.get('/type/singer/page', { + params: { + page, + size, + keyword + } + }) +} + +/** + * 根据ID获取歌手类型 + * @param {number} id 歌手类型ID + * @returns {Promise} + */ +export function getSingerTypeById(id) { + return api.get(`/type/singer/${id}`) +} + +/** + * 添加歌手类型(管理员) + * @param {Object} data 歌手类型数据 + * @returns {Promise} + */ +export function addSingerType(data) { + return api.post('/type/singer/add', data) +} + +/** + * 更新歌手类型(管理员) + * @param {number} id 歌手类型ID + * @param {Object} data 歌手类型数据 + * @returns {Promise} + */ +export function updateSingerType(id, data) { + return api.put(`/type/singer/${id}`, data) +} + +/** + * 删除歌手类型(管理员) + * @param {number} id 歌手类型ID + * @returns {Promise} + */ +export function deleteSingerType(id) { + return api.delete(`/type/singer/${id}`) +} diff --git a/music-qianduan/src/api/user.js b/music-qianduan/src/api/user.js new file mode 100644 index 0000000..bbde12d --- /dev/null +++ b/music-qianduan/src/api/user.js @@ -0,0 +1,121 @@ +import api from './index' + +/** + * 用户登录 + * @param {Object} data 登录数据 + * @returns {Promise} 请求结果 + */ +export function login(data) { + return api.post('/user/login', data) +} + +/** + * 用户注册 + * @param {Object} data 注册数据 + * @returns {Promise} 请求结果 + */ +export function register(data) { + return api.post('/user/register', data) +} + +/** + * 用户登出 + * @returns {Promise} 请求结果 + */ +export function logout() { + return api.post('/user/logout') +} + +/** + * 获取当前登录用户信息 + * @returns {Promise} 请求结果 + */ +export function getCurrentUser() { + return api.get('/user/current') +} + +/** + * 获取用户信息 + * @returns {Promise} 请求结果 + */ +export function getUserInfo() { + return api.get('/user/profile') +} + +/** + * 更新用户信息 + * @param {Object} data 用户信息 + * @returns {Promise} 请求结果 + */ +export function updateUserInfo(data) { + return api.put('/user/update', data) +} + +/** + * 上传用户头像 + * @param {File} file 头像文件 + * @returns {Promise} 请求结果 + */ +export function uploadAvatar(file) { + const formData = new FormData() + formData.append('file', file) + return api.post('/user/upload/avatar', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 修改密码 + * @param {Object} data 密码数据 + * @returns {Promise} 请求结果 + */ +export function changePassword(data) { + return api.put('/user/password', data) +} + +/** + * 获取用户统计数据 + * @returns {Promise} 请求结果 + */ +export function getUserStats() { + return api.get('/user/stats') +} + +/** + * 分页获取用户列表 + * @param {Object} params 查询参数 + * @returns {Promise} 请求结果 + */ +export function getUserList(params) { + return api.get('/user/list', { params }) +} + +/** + * 根据ID获取用户 + * @param {Number} id 用户ID + * @returns {Promise} 请求结果 + */ +export function getUserById(id) { + return api.get(`/user/${id}`) +} + +/** + * 删除用户 + * @param {Number} id 用户ID + * @returns {Promise} 请求结果 + */ +export function deleteUser(id) { + return api.delete(`/user/${id}`) +} + +/** + * 更新用户状态 + * @param {Number} id 用户ID + * @param {Number} status 状态 + * @returns {Promise} 请求结果 + */ +export function updateUserStatus(id, status) { + return api.put(`/user/${id}/status?status=${status}`) +} diff --git a/music-qianduan/src/assets/css/global.css b/music-qianduan/src/assets/css/global.css new file mode 100644 index 0000000..aa53441 --- /dev/null +++ b/music-qianduan/src/assets/css/global.css @@ -0,0 +1,76 @@ +/* 全局样式 */ + +/* 确保所有菜单项显示在同一层级 */ +.el-menu--horizontal { + display: flex !important; + flex-wrap: nowrap !important; + overflow: visible !important; + border-bottom: none !important; + background-color: transparent !important; +} + +.el-menu--horizontal > .el-menu-item { + display: block !important; + visibility: visible !important; + opacity: 1 !important; + pointer-events: auto !important; + flex-shrink: 0 !important; + border-bottom: none !important; + height: 70px !important; + line-height: 70px !important; +} + +/* 菜单项样式 */ +.el-menu-demo.main-nav { + border-bottom: none !important; + background-color: transparent !important; + height: 70px !important; +} + +/* 确保所有菜单项都显示 */ +.el-menu-item[index="/"], +.el-menu-item[index="/music"], +.el-menu-item[index="/playlist"], +.el-menu-item[index="/singer"] { + display: block !important; + visibility: visible !important; + opacity: 1 !important; + height: 70px !important; + line-height: 70px !important; + font-size: 16px !important; + font-weight: 500 !important; + padding: 0 20px !important; + min-width: 80px !important; +} + +/* 移除更多菜单按钮,但保留菜单项 */ +.el-menu--horizontal .el-sub-menu__icon-more, +.el-menu--horizontal .el-icon--more { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + pointer-events: none !important; +} + +/* 防止双滚动条 */ +html, body { + overflow: hidden; /* 禁止整个页面滚动 */ +} + +/* 确保内容区域只有一个滚动条 */ +.el-main { + overflow-y: hidden !important; + overflow-x: hidden !important; +} + +/* 确保所有页面内容区域适应高度 */ +.home-container, +.music-page, +.playlist-container, +.singer-container, +.search-container, +.user-container, +.singer-detail-container, +.playlist-detail-container { + height: 100%; +} diff --git a/music-qianduan/src/assets/vue.svg b/music-qianduan/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/music-qianduan/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/music-qianduan/src/components/HelloWorld.vue b/music-qianduan/src/components/HelloWorld.vue new file mode 100644 index 0000000..546ebbc --- /dev/null +++ b/music-qianduan/src/components/HelloWorld.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/music-qianduan/src/components/HomeBanner.vue b/music-qianduan/src/components/HomeBanner.vue new file mode 100644 index 0000000..122838d --- /dev/null +++ b/music-qianduan/src/components/HomeBanner.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/music-qianduan/src/components/ImagePreview.vue b/music-qianduan/src/components/ImagePreview.vue new file mode 100644 index 0000000..5492a0e --- /dev/null +++ b/music-qianduan/src/components/ImagePreview.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/music-qianduan/src/components/MusicList.vue b/music-qianduan/src/components/MusicList.vue new file mode 100644 index 0000000..6d5a38f --- /dev/null +++ b/music-qianduan/src/components/MusicList.vue @@ -0,0 +1,384 @@ + + + + + diff --git a/music-qianduan/src/components/MusicPlayer.vue b/music-qianduan/src/components/MusicPlayer.vue new file mode 100644 index 0000000..728ecee --- /dev/null +++ b/music-qianduan/src/components/MusicPlayer.vue @@ -0,0 +1,1195 @@ + + + + + diff --git a/music-qianduan/src/layouts/AdminLayout.vue b/music-qianduan/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..b62fbae --- /dev/null +++ b/music-qianduan/src/layouts/AdminLayout.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/music-qianduan/src/layouts/MainLayout.vue b/music-qianduan/src/layouts/MainLayout.vue new file mode 100644 index 0000000..3bf8d22 --- /dev/null +++ b/music-qianduan/src/layouts/MainLayout.vue @@ -0,0 +1,384 @@ + + + + + + + diff --git a/music-qianduan/src/main.js b/music-qianduan/src/main.js new file mode 100644 index 0000000..63df214 --- /dev/null +++ b/music-qianduan/src/main.js @@ -0,0 +1,27 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import zhCn from 'element-plus/dist/locale/zh-cn.mjs' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import './style.css' +import './assets/css/global.css' // 导入全局样式文件 +import App from './App.vue' +import router from './router' +import pinia from './stores' + +const app = createApp(App) + +// 注册所有图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +// 使用插件 +app.use(ElementPlus, { + locale: zhCn, +}) +app.use(router) +app.use(pinia) + +// 挂载应用 +app.mount('#app') diff --git a/music-qianduan/src/router/index.js b/music-qianduan/src/router/index.js new file mode 100644 index 0000000..588f562 --- /dev/null +++ b/music-qianduan/src/router/index.js @@ -0,0 +1,312 @@ +import { createRouter, createWebHistory } from 'vue-router' +import AdminLayout from '../layouts/AdminLayout.vue' + +// 导入视图组件 +const Home = () => import('../views/Home.vue') +const NotFound = () => import('../views/NotFound.vue') +const Login = () => import('../views/Login.vue') +const Register = () => import('../views/Register.vue') +const Music = () => import('../views/Music.vue') +const MusicDetail = () => import('../views/MusicDetail.vue') +const Playlist = () => import('../views/Playlist.vue') +const PlaylistDetail = () => import('../views/PlaylistDetail.vue') +const Singer = () => import('../views/Singer.vue') +const SingerDetail = () => import('../views/SingerDetail.vue') +const Search = () => import('../views/Search.vue') +const UserCenter = () => import('../views/UserCenter.vue') +const UserCollectMusic = () => import('../views/user/CollectMusic.vue') +const UserCollectPlaylist = () => import('../views/user/CollectPlaylist.vue') +const UserCollectSinger = () => import('../views/user/CollectSinger.vue') +const UserPlaylist = () => import('../views/user/Playlist.vue') +const UserPassword = () => import('../views/user/Password.vue') +const ImageTest = () => import('../views/ImageTest.vue') + +// 后台管理页面 +const Admin = () => import('../views/admin/Index.vue') +const AdminDashboard = () => import('../views/admin/Dashboard.vue') +const AdminUser = () => import('../views/admin/UserManagement.vue') +const AdminMusic = () => import('../views/admin/Music.vue') +const AdminPlaylist = () => import('../views/admin/Playlist.vue') +const AdminSinger = () => import('../views/admin/Singer.vue') +const AdminBanner = () => import('../views/admin/BannerManagement.vue') +const AdminType = () => import('../views/admin/TypeManagement.vue') +const AdminComment = () => import('../views/admin/CommentManagement.vue') + +// 定义路由 +const routes = [ + { + path: '/', + name: 'home', + component: Home, + meta: { + title: '首页' + } + }, + { + path: '/login', + name: 'login', + component: Login, + meta: { + title: '登录' + } + }, + { + path: '/register', + name: 'register', + component: Register, + meta: { + title: '注册' + } + }, + { + path: '/music', + name: 'music', + component: Music, + meta: { + title: '音乐' + } + }, + { + path: '/music/:id', + name: 'music-detail', + component: MusicDetail, + meta: { + title: '音乐详情' + } + }, + { + path: '/playlist', + name: 'playlist', + component: Playlist, + meta: { + title: '歌单' + } + }, + { + path: '/playlist/:id', + name: 'playlist-detail', + component: PlaylistDetail, + meta: { + title: '歌单详情' + } + }, + { + path: '/singer', + name: 'singer', + component: Singer, + meta: { + title: '歌手' + } + }, + { + path: '/singer/:id', + name: 'singer-detail', + component: SingerDetail, + meta: { + title: '歌手详情' + } + }, + { + path: '/search', + name: 'search', + component: Search, + meta: { + title: '搜索' + } + }, + { + path: '/image-test', + name: 'image-test', + component: ImageTest, + meta: { + title: '图片URL测试' + } + }, + { + path: '/user', + name: 'user-center', + component: UserCenter, + meta: { + title: '个人中心', + requiresAuth: true + }, + children: [ + { + path: 'collect/music', + name: 'user-collect-music', + component: UserCollectMusic, + meta: { + title: '我收藏的音乐', + requiresAuth: true + } + }, + { + path: 'collect/playlist', + name: 'user-collect-playlist', + component: UserCollectPlaylist, + meta: { + title: '我收藏的歌单', + requiresAuth: true + } + }, + { + path: 'collect/singer', + name: 'user-collect-singer', + component: UserCollectSinger, + meta: { + title: '我收藏的歌手', + requiresAuth: true + } + }, + { + path: 'playlist', + name: 'user-playlist', + component: UserPlaylist, + meta: { + title: '我的歌单', + requiresAuth: true + } + }, + { + path: 'password', + name: 'user-password', + component: UserPassword, + meta: { + title: '修改密码', + requiresAuth: true + } + } + ] + }, + { + path: '/admin', + name: 'admin', + component: AdminLayout, + meta: { + title: '后台管理', + requiresAuth: true, + requiresAdmin: true + }, + children: [ + { + path: '', + name: 'admin-dashboard', + component: AdminDashboard, + meta: { + title: '控制面板', + requiresAuth: true, + requiresAdmin: true + } + }, + { + path: 'user', + name: 'admin-user', + component: AdminUser, + meta: { + title: '用户管理', + requiresAuth: true, + requiresAdmin: true + } + }, + { + path: 'music', + name: 'admin-music', + component: AdminMusic, + meta: { + title: '音乐管理', + requiresAuth: true, + requiresAdmin: true + } + }, + { + path: 'playlist', + name: 'admin-playlist', + component: AdminPlaylist, + meta: { + title: '歌单管理', + requiresAuth: true, + requiresAdmin: true + } + }, + { + path: 'singer', + name: 'admin-singer', + component: AdminSinger, + meta: { + title: '歌手管理', + requiresAuth: true, + requiresAdmin: true + } + }, + { + path: 'banner', + name: 'admin-banner', + component: AdminBanner, + meta: { + title: '轮播图管理', + requiresAuth: true, + requiresAdmin: true + } + }, + { + path: 'type', + name: 'admin-type', + component: AdminType, + meta: { + title: '类型管理', + requiresAuth: true, + requiresAdmin: true + } + }, + { + path: 'comment', + name: 'admin-comment', + component: AdminComment, + meta: { + title: '评论管理', + requiresAuth: true, + requiresAdmin: true + } + } + ] + }, + { + path: '/:pathMatch(.*)*', + name: 'not-found', + component: NotFound, + meta: { + title: '页面未找到' + } + } +] + +// 创建路由实例 +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 全局前置守卫 - 设置页面标题和权限验证 +router.beforeEach((to, from, next) => { + document.title = to.meta.title || '音乐应用' + + // 从 store 获取登录状态 + const isLoggedIn = localStorage.getItem('token') // 临时使用localStorage,后续会改为store + const isAdmin = localStorage.getItem('isAdmin') === 'true' // 临时使用localStorage,后续会改为store + + // 需要登录权限但未登录 + if (to.meta.requiresAuth && !isLoggedIn) { + next({ name: 'login', query: { redirect: to.fullPath } }) + return + } + + // 需要管理员权限但不是管理员 + if (to.meta.requiresAdmin && !isAdmin) { + next({ name: 'home' }) + return + } + + next() +}) + +export default router diff --git a/music-qianduan/src/stores/index.js b/music-qianduan/src/stores/index.js new file mode 100644 index 0000000..8a05980 --- /dev/null +++ b/music-qianduan/src/stores/index.js @@ -0,0 +1,5 @@ +import { createPinia } from 'pinia' + +const pinia = createPinia() + +export default pinia diff --git a/music-qianduan/src/stores/player.js b/music-qianduan/src/stores/player.js new file mode 100644 index 0000000..2f687ea --- /dev/null +++ b/music-qianduan/src/stores/player.js @@ -0,0 +1,285 @@ +import { defineStore } from 'pinia' +import { playMusic } from '../api/music' + +export const usePlayerStore = defineStore('player', { + state: () => ({ + currentMusic: null, + playlist: [], + isPlaying: false, + currentTime: 0, + duration: 0, + volume: 80, + showPlaylist: false, + loop: 'none', // none, single, list + audio: null, + currentIndex: -1 // 当前播放的索引 + }), + + getters: { + getCurrentMusic: (state) => state.currentMusic, + getPlaylist: (state) => state.playlist, + getIsPlaying: (state) => state.isPlaying, + getCurrentTime: (state) => state.currentTime, + getDuration: (state) => state.duration, + getVolume: (state) => state.volume, + getShowPlaylist: (state) => state.showPlaylist, + getLoop: (state) => state.loop, + getCurrentIndex: (state) => state.currentIndex + }, + + actions: { + // 初始化音频对象 + initAudio() { + if (!this.audio) { + this.audio = new Audio() + + // 监听事件 + this.audio.addEventListener('timeupdate', this.handleTimeUpdate) + this.audio.addEventListener('ended', this.handleEnded) + this.audio.addEventListener('loadedmetadata', this.handleLoadedMetadata) + this.audio.addEventListener('error', this.handleError) + this.audio.volume = this.volume / 100 + } + }, + + // 设置当前音乐 + async setCurrentMusic(music) { + if (!music) return + + this.initAudio() + this.currentMusic = music + + // 检查播放列表中是否已存在该音乐 + const index = this.playlist.findIndex(item => item.id === music.id) + if (index !== -1) { + this.currentIndex = index + } else { + this.addToPlaylist(music) + this.currentIndex = this.playlist.length - 1 + } + + // 设置音频源并播放 + if (music.url) { + this.audio.src = music.url + this.play() + + // 调用播放API,记录播放次数 + try { + await playMusic(music.id) + } catch (error) { + console.error('记录播放失败:', error) + } + } else { + console.error('音乐URL不存在') + } + }, + + // 播放 + play() { + if (this.currentMusic) { + this.initAudio() + const playPromise = this.audio.play() + + if (playPromise !== undefined) { + playPromise.then(() => { + this.isPlaying = true + }).catch(error => { + console.error('播放失败:', error) + this.isPlaying = false + }) + } + } + }, + + // 暂停 + pause() { + if (this.audio) { + this.audio.pause() + this.isPlaying = false + } + }, + + // 切换播放状态 + togglePlay() { + if (this.isPlaying) { + this.pause() + } else { + this.play() + } + }, + + // 设置音量 + setVolume(volume) { + this.volume = volume + if (this.audio) { + this.audio.volume = volume / 100 + } + }, + + // 设置当前播放时间 + setCurrentTime(time) { + this.currentTime = time + if (this.audio) { + try { + this.audio.currentTime = time + // 如果当前是暂停状态,尝试播放一小段时间再暂停,以确保进度条变化生效 + if (!this.isPlaying) { + const wasPlaying = false + this.audio.play().then(() => { + setTimeout(() => { + if (!wasPlaying) { + this.audio.pause() + } + }, 100) + }).catch(err => console.error('Temporary play failed:', err)) + } + } catch (err) { + console.error('Failed to set currentTime:', err) + } + } + }, + + // 添加到播放列表 + addToPlaylist(music) { + // 检查是否已存在 + const exists = this.playlist.some(item => item.id === music.id) + if (!exists) { + this.playlist.push(music) + } + }, + + // 从播放列表中移除 + removeFromPlaylist(musicId) { + const index = this.playlist.findIndex(item => item.id === musicId) + if (index !== -1) { + this.playlist.splice(index, 1) + + // 如果移除的是当前播放的音乐,则播放下一首 + if (this.currentMusic && this.currentMusic.id === musicId) { + this.playNext() + } + } + }, + + // 清空播放列表 + clearPlaylist() { + this.playlist = [] + this.setCurrentMusic(null) + }, + + // 播放上一首 + playPrev() { + if (this.playlist.length === 0) return + + const currentIndex = this.playlist.findIndex(item => this.currentMusic && item.id === this.currentMusic.id) + let prevIndex = currentIndex - 1 + + if (prevIndex < 0) { + prevIndex = this.playlist.length - 1 + } + + this.setCurrentMusic(this.playlist[prevIndex]) + }, + + // 播放下一首 + playNext() { + if (this.playlist.length === 0) return + + const currentIndex = this.playlist.findIndex(item => this.currentMusic && item.id === this.currentMusic.id) + let nextIndex = currentIndex + 1 + + if (nextIndex >= this.playlist.length) { + nextIndex = 0 + } + + this.setCurrentMusic(this.playlist[nextIndex]) + }, + + // 切换播放列表显示状态 + togglePlaylist() { + this.showPlaylist = !this.showPlaylist + }, + + // 设置循环模式 + setLoop(mode) { + this.loop = mode + }, + + // 处理时间更新事件 + handleTimeUpdate() { + this.currentTime = this.audio.currentTime + }, + + // 处理播放结束事件 + handleEnded() { + if (this.loop === 'single') { + // 单曲循环 + this.audio.currentTime = 0 + this.play() + } else if (this.loop === 'list') { + // 列表循环 + this.playNext() + } else { + // 不循环,播放下一首 + this.playNext() + } + }, + + // 处理元数据加载事件 + handleLoadedMetadata() { + this.duration = this.audio.duration + }, + + // 处理错误事件 + handleError(e) { + console.error('音频加载错误:', e) + // 如果当前音乐加载失败,尝试播放下一首 + if (this.playlist.length > 1) { + this.playNext() + } + }, + + // 设置播放列表显示状态 + setShowPlaylist(show) { + this.showPlaylist = show + }, + + // 从播放列表中播放 + playFromPlaylist(index) { + if (index >= 0 && index < this.playlist.length) { + this.setCurrentMusic(this.playlist[index]) + } + }, + + // 更新音乐URL + updateMusicUrl(id, url) { + // 更新当前音乐的URL + if (this.currentMusic && this.currentMusic.id === id) { + this.currentMusic.url = url + if (this.audio) { + this.audio.src = url + } + } + + // 更新播放列表中的URL + const index = this.playlist.findIndex(item => item.id === id) + if (index !== -1) { + this.playlist[index].url = url + } + }, + + // 更新音乐封面 + updateMusicCover(id, cover) { + // 更新当前音乐的封面 + if (this.currentMusic && this.currentMusic.id === id) { + this.currentMusic.cover = cover + } + + // 更新播放列表中的封面 + const index = this.playlist.findIndex(item => item.id === id) + if (index !== -1) { + this.playlist[index].cover = cover + } + } + } +}) diff --git a/music-qianduan/src/stores/user.js b/music-qianduan/src/stores/user.js new file mode 100644 index 0000000..770cc80 --- /dev/null +++ b/music-qianduan/src/stores/user.js @@ -0,0 +1,145 @@ +import { defineStore } from 'pinia' +import { getCurrentUser, logout } from '../api/user' +import { ElMessage } from 'element-plus' + +export const useUserStore = defineStore('user', { + state: () => { + // 从本地存储中获取用户信息 + const storedUser = localStorage.getItem('user') + const token = localStorage.getItem('token') + + return { + user: storedUser ? JSON.parse(storedUser) : null, + isLoggedIn: !!token, + token: token || '' + } + }, + + getters: { + getUser: (state) => state.user, + getLoginStatus: (state) => state.isLoggedIn, + isAdmin: (state) => state.user?.role === 1 + }, + + actions: { + // 登录 + login(userData) { + // 确保 userData 包含 user 属性 + if (userData && userData.user) { + this.user = userData.user + this.isLoggedIn = true + this.token = userData.token || '' + + // 打印登录数据,便于调试 + console.log('登录数据:', userData) + + // 存储到本地 + localStorage.setItem('user', JSON.stringify(userData.user)) + localStorage.setItem('token', this.token) + localStorage.setItem('tokenName', userData.tokenName || 'satoken') + localStorage.setItem('tokenPrefix', userData.tokenPrefix || '') + localStorage.setItem('isAdmin', userData.user.role === 1 ? 'true' : 'false') + + // 打印存储的信息,便于调试 + console.log('存储的token:', localStorage.getItem('token')) + console.log('存储的tokenName:', localStorage.getItem('tokenName')) + console.log('存储的tokenPrefix:', localStorage.getItem('tokenPrefix')) + console.log('存储的isAdmin:', localStorage.getItem('isAdmin')) + } else { + console.error('登录数据格式错误:', userData) + } + }, + + // 登出 + async logout() { + try { + // 调用登出API + await logout() + + // 重置状态 + this.user = null + this.isLoggedIn = false + this.token = '' + + // 清除本地存储 + localStorage.removeItem('user') + localStorage.removeItem('token') + localStorage.removeItem('tokenName') + localStorage.removeItem('tokenPrefix') + localStorage.removeItem('isAdmin') + + ElMessage.success('登出成功') + } catch (error) { + console.error('登出失败:', error) + } + }, + + // 获取当前用户信息 + async fetchCurrentUser() { + const token = localStorage.getItem('token') + if (!token) { + console.warn('没有token,无法获取用户信息') + return + } + + // 确保存储在store中的token与localStorage中的一致 + this.token = token + + console.log('开始获取用户信息,token:', token) + + try { + // 尝试从本地存储中获取用户信息 + const userStr = localStorage.getItem('user') + if (userStr) { + try { + const user = JSON.parse(userStr) + if (user && user.id) { + this.user = user + this.isLoggedIn = true + console.log('从本地存储中获取用户信息成功:', user) + return + } + } catch (e) { + console.warn('解析本地存储的用户信息失败:', e) + } + } + + // 如果本地存储中没有用户信息,则从服务器获取 + const res = await getCurrentUser() + console.log('获取用户信息响应:', res) + + if (res.code === 200) { + // 如果返回的数据中有user属性,说明是登录接口返回的数据格式 + if (res.data && res.data.user) { + this.user = res.data.user + } else { + // 否则是获取当前用户接口返回的数据格式 + this.user = res.data + } + + this.isLoggedIn = true + localStorage.setItem('user', JSON.stringify(this.user)) + localStorage.setItem('isAdmin', this.user.role === 1 ? 'true' : 'false') + console.log('用户信息已更新:', this.user) + } else { + console.error('获取用户信息失败,响应码非200:', res.code, res.msg) + } + } catch (error) { + console.error('获取用户信息失败:', error) + // 如果获取用户信息失败,可能是token已过期,清除本地存储 + if (error.message && (error.message.includes('token') || error.message.includes('登录'))) { + console.warn('可能是token已过期,清除本地存储') + this.user = null + this.isLoggedIn = false + this.token = '' + localStorage.removeItem('user') + localStorage.removeItem('token') + localStorage.removeItem('tokenName') + localStorage.removeItem('tokenPrefix') + localStorage.removeItem('isAdmin') + } + throw error // 将错误向上抛出,以便调用者可以处理 + } + } + } +}) diff --git a/music-qianduan/src/style.css b/music-qianduan/src/style.css new file mode 100644 index 0000000..5587afa --- /dev/null +++ b/music-qianduan/src/style.css @@ -0,0 +1,84 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +/* 确保歌手菜单项显示 */ +.el-menu-item[index="/singer"] { + display: block !important; + visibility: visible !important; + opacity: 1 !important; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/music-qianduan/src/utils/clearStorage.js b/music-qianduan/src/utils/clearStorage.js new file mode 100644 index 0000000..90d101d --- /dev/null +++ b/music-qianduan/src/utils/clearStorage.js @@ -0,0 +1,12 @@ +/** + * 清理本地存储 + */ +export function clearStorage() { + localStorage.removeItem('token') + localStorage.removeItem('tokenName') + localStorage.removeItem('tokenPrefix') + localStorage.removeItem('user') + localStorage.removeItem('isAdmin') + + console.log('本地存储已清理') +} diff --git a/music-qianduan/src/utils/imageUtils.js b/music-qianduan/src/utils/imageUtils.js new file mode 100644 index 0000000..2f11551 --- /dev/null +++ b/music-qianduan/src/utils/imageUtils.js @@ -0,0 +1,42 @@ +/** + * 处理图片URL,确保它是完整的URL + * @param {string} url 图片URL + * @returns {string} 完整的图片URL + */ +export function processImageUrl(url) { + if (!url) { + return 'https://via.placeholder.com/150?text=无图片'; + } + + // 如果是完整URL,直接返回 + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + + // 如果是相对路径,添加API基础URL + // 使用后端服务器地址 + const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8085'; + + // 确保路径以/开头 + if (!url.startsWith('/')) { + url = '/' + url; + } + + return `${baseUrl}${url}`; +} + +/** + * 格式化播放次数 + * @param {number} count 播放次数 + * @returns {string} 格式化后的播放次数 + */ +export function formatPlayCount(count) { + if (!count) return '0'; + if (count < 10000) { + return count.toString(); + } else if (count < 100000000) { + return (count / 10000).toFixed(1) + '万'; + } else { + return (count / 100000000).toFixed(1) + '亿'; + } +} diff --git a/music-qianduan/src/utils/websocket.js b/music-qianduan/src/utils/websocket.js new file mode 100644 index 0000000..6f406b0 --- /dev/null +++ b/music-qianduan/src/utils/websocket.js @@ -0,0 +1,95 @@ +import SockJS from 'sockjs-client/dist/sockjs.min.js'; // 确保导入正确的 SockJS 文件 +import { Client } from '@stomp/stompjs'; + +let stompClient = null; +let subscriptions = {}; // 用于存储订阅,方便取消 + +const connect = (onConnectedCallback, onErrorCallback) => { + // 后端 WebSocket 端点 URL (根据实际部署情况可能需要修改) + // 注意:如果后端运行在不同端口,需要写完整 URL,例如 'http://localhost:8080/ws' + // 如果前后端同源,可以直接用 '/ws' + const socket = new SockJS('http://localhost:8085/ws'); // 使用后端配置的端点 (修改为绝对路径) + stompClient = new Client({ + webSocketFactory: () => socket, + reconnectDelay: 5000, // 自动重连延迟 + heartbeatIncoming: 4000, + heartbeatOutgoing: 4000, + onConnect: (frame) => { + console.log('WebSocket Connected: ' + frame); + if (onConnectedCallback) { + onConnectedCallback(frame); + } + }, + onStompError: (frame) => { + console.error('Broker reported error: ' + frame.headers['message']); + console.error('Additional details: ' + frame.body); + if (onErrorCallback) { + onErrorCallback(frame); + } + }, + onWebSocketError: (error) => { + console.error('WebSocket Error: ', error); + if (onErrorCallback) { + onErrorCallback(error); + } + }, + onDisconnect: () => { + console.log('WebSocket Disconnected'); + // 清理所有订阅 + subscriptions = {}; + } + }); + + stompClient.activate(); +}; + +const subscribe = (destination, callback) => { + if (stompClient && stompClient.connected) { + console.log(`Subscribing to ${destination}`); + const subscription = stompClient.subscribe(destination, (message) => { + console.log(`Message received from ${destination}:`, message.body); + if (callback) { + try { + callback(JSON.parse(message.body)); + } catch (e) { + console.error("Error parsing message body:", e); + callback(message.body); // 如果解析失败,返回原始 body + } + } + }); + subscriptions[destination] = subscription; // 存储订阅 + return subscription; + } else { + console.error('Cannot subscribe, Stomp client not connected.'); + return null; + } +}; + +const unsubscribe = (destination) => { + if (subscriptions[destination]) { + console.log(`Unsubscribing from ${destination}`); + subscriptions[destination].unsubscribe(); + delete subscriptions[destination]; // 从存储中移除 + } +}; + +const disconnect = () => { + if (stompClient && stompClient.connected) { + stompClient.deactivate(); + console.log('WebSocket connection deactivated.'); + } + // 即使未连接,也尝试清理 stompClient 实例 + stompClient = null; + subscriptions = {}; // 清空订阅记录 +}; + +// 可选:发送消息的方法 (如果需要前端主动发消息) +const sendMessage = (destination, body) => { + if (stompClient && stompClient.connected) { + stompClient.publish({ destination: destination, body: JSON.stringify(body) }); + } else { + console.error('Cannot send message, Stomp client not connected.'); + } +}; + +export { connect, subscribe, unsubscribe, disconnect, sendMessage }; \ No newline at end of file diff --git a/music-qianduan/src/views/About.vue b/music-qianduan/src/views/About.vue new file mode 100644 index 0000000..9ada4d7 --- /dev/null +++ b/music-qianduan/src/views/About.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/music-qianduan/src/views/Home.vue b/music-qianduan/src/views/Home.vue new file mode 100644 index 0000000..84dc02e --- /dev/null +++ b/music-qianduan/src/views/Home.vue @@ -0,0 +1,502 @@ + + + + + diff --git a/music-qianduan/src/views/ImageTest.vue b/music-qianduan/src/views/ImageTest.vue new file mode 100644 index 0000000..b27c128 --- /dev/null +++ b/music-qianduan/src/views/ImageTest.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/music-qianduan/src/views/Login.vue b/music-qianduan/src/views/Login.vue new file mode 100644 index 0000000..c8c5897 --- /dev/null +++ b/music-qianduan/src/views/Login.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/music-qianduan/src/views/Music.vue b/music-qianduan/src/views/Music.vue new file mode 100644 index 0000000..954c822 --- /dev/null +++ b/music-qianduan/src/views/Music.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/music-qianduan/src/views/MusicDetail.vue b/music-qianduan/src/views/MusicDetail.vue new file mode 100644 index 0000000..49726ec --- /dev/null +++ b/music-qianduan/src/views/MusicDetail.vue @@ -0,0 +1,448 @@ + + + + + diff --git a/music-qianduan/src/views/NotFound.vue b/music-qianduan/src/views/NotFound.vue new file mode 100644 index 0000000..5f35a84 --- /dev/null +++ b/music-qianduan/src/views/NotFound.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/music-qianduan/src/views/Playlist.vue b/music-qianduan/src/views/Playlist.vue new file mode 100644 index 0000000..a5d73be --- /dev/null +++ b/music-qianduan/src/views/Playlist.vue @@ -0,0 +1,547 @@ + + + + + diff --git a/music-qianduan/src/views/PlaylistDetail.vue b/music-qianduan/src/views/PlaylistDetail.vue new file mode 100644 index 0000000..2821a9f --- /dev/null +++ b/music-qianduan/src/views/PlaylistDetail.vue @@ -0,0 +1,659 @@ + + + + + diff --git a/music-qianduan/src/views/Register.vue b/music-qianduan/src/views/Register.vue new file mode 100644 index 0000000..b6b0f9f --- /dev/null +++ b/music-qianduan/src/views/Register.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/music-qianduan/src/views/Search.vue b/music-qianduan/src/views/Search.vue new file mode 100644 index 0000000..e07b27f --- /dev/null +++ b/music-qianduan/src/views/Search.vue @@ -0,0 +1,692 @@ + + + + + diff --git a/music-qianduan/src/views/Singer.vue b/music-qianduan/src/views/Singer.vue new file mode 100644 index 0000000..b4011cb --- /dev/null +++ b/music-qianduan/src/views/Singer.vue @@ -0,0 +1,499 @@ + + + + + diff --git a/music-qianduan/src/views/SingerDetail.vue b/music-qianduan/src/views/SingerDetail.vue new file mode 100644 index 0000000..222113b --- /dev/null +++ b/music-qianduan/src/views/SingerDetail.vue @@ -0,0 +1,576 @@ + + + + + diff --git a/music-qianduan/src/views/UserCenter.vue b/music-qianduan/src/views/UserCenter.vue new file mode 100644 index 0000000..225bc2e --- /dev/null +++ b/music-qianduan/src/views/UserCenter.vue @@ -0,0 +1,493 @@ + + + + + diff --git a/music-qianduan/src/views/admin/Banner.vue b/music-qianduan/src/views/admin/Banner.vue new file mode 100644 index 0000000..b636ffc --- /dev/null +++ b/music-qianduan/src/views/admin/Banner.vue @@ -0,0 +1,414 @@ + + + + + diff --git a/music-qianduan/src/views/admin/BannerManagement.vue b/music-qianduan/src/views/admin/BannerManagement.vue new file mode 100644 index 0000000..781c4f1 --- /dev/null +++ b/music-qianduan/src/views/admin/BannerManagement.vue @@ -0,0 +1,673 @@ + + + + + diff --git a/music-qianduan/src/views/admin/CommentManagement.vue b/music-qianduan/src/views/admin/CommentManagement.vue new file mode 100644 index 0000000..ec3870d --- /dev/null +++ b/music-qianduan/src/views/admin/CommentManagement.vue @@ -0,0 +1,440 @@ + + + + + diff --git a/music-qianduan/src/views/admin/Dashboard.vue b/music-qianduan/src/views/admin/Dashboard.vue new file mode 100644 index 0000000..b2036a6 --- /dev/null +++ b/music-qianduan/src/views/admin/Dashboard.vue @@ -0,0 +1,707 @@ + + + + + diff --git a/music-qianduan/src/views/admin/Index.vue b/music-qianduan/src/views/admin/Index.vue new file mode 100644 index 0000000..5de918b --- /dev/null +++ b/music-qianduan/src/views/admin/Index.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/music-qianduan/src/views/admin/Music.vue b/music-qianduan/src/views/admin/Music.vue new file mode 100644 index 0000000..a99c911 --- /dev/null +++ b/music-qianduan/src/views/admin/Music.vue @@ -0,0 +1,788 @@ + + + + + diff --git a/music-qianduan/src/views/admin/Playlist.vue b/music-qianduan/src/views/admin/Playlist.vue new file mode 100644 index 0000000..62c94eb --- /dev/null +++ b/music-qianduan/src/views/admin/Playlist.vue @@ -0,0 +1,1064 @@ + + + + + + + diff --git a/music-qianduan/src/views/admin/PlaylistType.vue b/music-qianduan/src/views/admin/PlaylistType.vue new file mode 100644 index 0000000..711b108 --- /dev/null +++ b/music-qianduan/src/views/admin/PlaylistType.vue @@ -0,0 +1,333 @@ + + + + + diff --git a/music-qianduan/src/views/admin/Singer.vue b/music-qianduan/src/views/admin/Singer.vue new file mode 100644 index 0000000..9452060 --- /dev/null +++ b/music-qianduan/src/views/admin/Singer.vue @@ -0,0 +1,852 @@ + + + + + + + diff --git a/music-qianduan/src/views/admin/Type.vue b/music-qianduan/src/views/admin/Type.vue new file mode 100644 index 0000000..dc81f99 --- /dev/null +++ b/music-qianduan/src/views/admin/Type.vue @@ -0,0 +1,357 @@ + + + + + diff --git a/music-qianduan/src/views/admin/TypeManagement.vue b/music-qianduan/src/views/admin/TypeManagement.vue new file mode 100644 index 0000000..bb458da --- /dev/null +++ b/music-qianduan/src/views/admin/TypeManagement.vue @@ -0,0 +1,780 @@ + + + + + diff --git a/music-qianduan/src/views/admin/User.vue b/music-qianduan/src/views/admin/User.vue new file mode 100644 index 0000000..874780c --- /dev/null +++ b/music-qianduan/src/views/admin/User.vue @@ -0,0 +1,294 @@ + + + + + diff --git a/music-qianduan/src/views/admin/UserManagement.vue b/music-qianduan/src/views/admin/UserManagement.vue new file mode 100644 index 0000000..e0f3067 --- /dev/null +++ b/music-qianduan/src/views/admin/UserManagement.vue @@ -0,0 +1,513 @@ + + + + + diff --git a/music-qianduan/src/views/user/CollectMusic.vue b/music-qianduan/src/views/user/CollectMusic.vue new file mode 100644 index 0000000..855e144 --- /dev/null +++ b/music-qianduan/src/views/user/CollectMusic.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/music-qianduan/src/views/user/CollectPlaylist.vue b/music-qianduan/src/views/user/CollectPlaylist.vue new file mode 100644 index 0000000..3ae5629 --- /dev/null +++ b/music-qianduan/src/views/user/CollectPlaylist.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/music-qianduan/src/views/user/CollectSinger.vue b/music-qianduan/src/views/user/CollectSinger.vue new file mode 100644 index 0000000..c200dac --- /dev/null +++ b/music-qianduan/src/views/user/CollectSinger.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/music-qianduan/src/views/user/Password.vue b/music-qianduan/src/views/user/Password.vue new file mode 100644 index 0000000..9751aa5 --- /dev/null +++ b/music-qianduan/src/views/user/Password.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/music-qianduan/src/views/user/Playlist.vue b/music-qianduan/src/views/user/Playlist.vue new file mode 100644 index 0000000..af3fd34 --- /dev/null +++ b/music-qianduan/src/views/user/Playlist.vue @@ -0,0 +1,562 @@ + + + + + diff --git a/music-qianduan/vite.config.js b/music-qianduan/vite.config.js new file mode 100644 index 0000000..beb90a4 --- /dev/null +++ b/music-qianduan/vite.config.js @@ -0,0 +1,37 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, + server: { + port: 5174, + open: true, + proxy: { + '/api': { + target: 'http://localhost:8085', + changeOrigin: true, + rewrite: (path) => path + } + } + }, + build: { + outDir: '../src/main/resources/static', + emptyOutDir: true, + assetsDir: 'assets', + rollupOptions: { + output: { + manualChunks: { + 'vendor': ['vue', 'vue-router', 'pinia', 'element-plus'], + 'element-icons': ['@element-plus/icons-vue'] + } + } + } + } +}) diff --git a/sql/music.sql b/sql/music.sql new file mode 100644 index 0000000..1f7ed82 --- /dev/null +++ b/sql/music.sql @@ -0,0 +1,237 @@ +-- 用户表 +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` varchar(50) NOT NULL COMMENT '用户名', + `password` varchar(100) NOT NULL COMMENT '密码', + `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', + `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', + `gender` tinyint DEFAULT '0' COMMENT '性别:0-未知,1-男,2-女', + `email` varchar(100) DEFAULT NULL COMMENT '邮箱', + `phone` varchar(20) DEFAULT NULL COMMENT '手机号', + `birth_date` date DEFAULT NULL COMMENT '出生日期', + `introduction` varchar(500) DEFAULT NULL COMMENT '个人简介', + `role` tinyint DEFAULT '0' COMMENT '角色:0-普通用户,1-管理员', + `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-正常', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- 音乐类型表 +CREATE TABLE `music_type` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '类型ID', + `name` varchar(50) NOT NULL COMMENT '类型名称', + `description` varchar(255) DEFAULT NULL COMMENT '类型描述', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='音乐类型表'; + +-- 歌手类型表 +CREATE TABLE `singer_type` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '类型ID', + `name` varchar(50) NOT NULL COMMENT '类型名称', + `description` varchar(255) DEFAULT NULL COMMENT '类型描述', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌手类型表'; + +-- 歌单类型表 +CREATE TABLE `playlist_type` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '类型ID', + `name` varchar(50) NOT NULL COMMENT '类型名称', + `description` varchar(255) DEFAULT NULL COMMENT '类型描述', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌单类型表'; + +-- 歌手表 +CREATE TABLE `singer` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '歌手ID', + `name` varchar(100) NOT NULL COMMENT '歌手名称', + `avatar` varchar(255) DEFAULT NULL COMMENT '歌手头像URL', + `introduction` text COMMENT '歌手简介', + `type_id` bigint DEFAULT NULL COMMENT '歌手类型ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_type_id` (`type_id`), + CONSTRAINT `fk_singer_type` FOREIGN KEY (`type_id`) REFERENCES `singer_type` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌手表'; + +-- 音乐表 +CREATE TABLE `music` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '音乐ID', + `name` varchar(100) NOT NULL COMMENT '音乐名称', + `cover` varchar(255) DEFAULT NULL COMMENT '封面URL', + `url` varchar(255) NOT NULL COMMENT '音乐文件URL', + `lyric` text COMMENT '歌词', + `duration` int DEFAULT '0' COMMENT '时长(秒)', + `play_count` bigint DEFAULT '0' COMMENT '播放次数', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='音乐表'; + +-- 音乐-歌手关联表 +CREATE TABLE `music_singer` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `music_id` bigint NOT NULL COMMENT '音乐ID', + `singer_id` bigint NOT NULL COMMENT '歌手ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_music_singer` (`music_id`,`singer_id`), + KEY `idx_singer_id` (`singer_id`), + CONSTRAINT `fk_ms_music_id` FOREIGN KEY (`music_id`) REFERENCES `music` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_ms_singer_id` FOREIGN KEY (`singer_id`) REFERENCES `singer` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='音乐-歌手关联表'; + +-- 音乐-类型关联表 +CREATE TABLE `music_type_relation` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `music_id` bigint NOT NULL COMMENT '音乐ID', + `type_id` bigint NOT NULL COMMENT '类型ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_music_type` (`music_id`,`type_id`), + KEY `idx_type_id` (`type_id`), + CONSTRAINT `fk_mtr_music_id` FOREIGN KEY (`music_id`) REFERENCES `music` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_mtr_type_id` FOREIGN KEY (`type_id`) REFERENCES `music_type` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='音乐-类型关联表'; + +-- 歌单表 +CREATE TABLE `playlist` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '歌单ID', + `name` varchar(100) NOT NULL COMMENT '歌单名称', + `cover` varchar(255) DEFAULT NULL COMMENT '封面URL', + `description` varchar(500) DEFAULT NULL COMMENT '歌单描述', + `creator_id` bigint DEFAULT NULL COMMENT '创建者ID', + `type_id` bigint DEFAULT NULL COMMENT '歌单类型ID', + `is_official` tinyint DEFAULT '0' COMMENT '是否官方歌单:0-否,1-是', + `play_count` bigint DEFAULT '0' COMMENT '播放次数', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_creator_id` (`creator_id`), + KEY `idx_type_id` (`type_id`), + CONSTRAINT `fk_playlist_creator` FOREIGN KEY (`creator_id`) REFERENCES `user` (`id`) ON DELETE SET NULL, + CONSTRAINT `fk_playlist_type` FOREIGN KEY (`type_id`) REFERENCES `playlist_type` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌单表'; + +-- 歌单-音乐关联表 +CREATE TABLE `playlist_music` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `playlist_id` bigint NOT NULL COMMENT '歌单ID', + `music_id` bigint NOT NULL COMMENT '音乐ID', + `order_num` int DEFAULT '0' COMMENT '排序号', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_playlist_music` (`playlist_id`,`music_id`), + KEY `idx_music_id` (`music_id`), + CONSTRAINT `fk_pm_music_id` FOREIGN KEY (`music_id`) REFERENCES `music` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_pm_playlist_id` FOREIGN KEY (`playlist_id`) REFERENCES `playlist` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌单-音乐关联表'; + +-- 用户收藏音乐表 +CREATE TABLE `user_collect_music` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `music_id` bigint NOT NULL COMMENT '音乐ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_music` (`user_id`,`music_id`), + KEY `idx_music_id` (`music_id`), + CONSTRAINT `fk_ucm_music_id` FOREIGN KEY (`music_id`) REFERENCES `music` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_ucm_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收藏音乐表'; + +-- 用户收藏歌单表 +CREATE TABLE `user_collect_playlist` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `playlist_id` bigint NOT NULL COMMENT '歌单ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_playlist` (`user_id`,`playlist_id`), + KEY `idx_playlist_id` (`playlist_id`), + CONSTRAINT `fk_ucp_playlist_id` FOREIGN KEY (`playlist_id`) REFERENCES `playlist` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_ucp_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收藏歌单表'; + +-- 用户收藏歌手表 +CREATE TABLE `user_collect_singer` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `singer_id` bigint NOT NULL COMMENT '歌手ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_singer` (`user_id`,`singer_id`), + KEY `idx_singer_id` (`singer_id`), + CONSTRAINT `fk_ucs_singer_id` FOREIGN KEY (`singer_id`) REFERENCES `singer` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_ucs_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收藏歌手表'; + +-- 评论表 +CREATE TABLE `comment` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '评论ID', + `content` varchar(500) NOT NULL COMMENT '评论内容', + `user_id` bigint NOT NULL COMMENT '评论用户ID', + `target_id` bigint NOT NULL COMMENT '评论目标ID', + `target_type` tinyint NOT NULL COMMENT '评论目标类型:1-音乐,2-歌单,3-歌手', + `parent_id` bigint DEFAULT NULL COMMENT '父评论ID', + `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-正常', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_target` (`target_id`,`target_type`), + KEY `idx_parent_id` (`parent_id`), + CONSTRAINT `fk_comment_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表'; + +-- 轮播图表 +CREATE TABLE `banner` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '轮播图ID', + `title` varchar(100) DEFAULT NULL COMMENT '标题', + `image_url` varchar(255) NOT NULL COMMENT '图片URL', + `link_url` varchar(255) DEFAULT NULL COMMENT '链接URL', + `target_id` bigint DEFAULT NULL COMMENT '目标ID', + `target_type` tinyint DEFAULT NULL COMMENT '目标类型:1-音乐,2-歌单,3-歌手', + `order_num` int DEFAULT '0' COMMENT '排序号', + `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-正常', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='轮播图表'; + +-- 播放历史表 +CREATE TABLE `play_history` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `music_id` bigint NOT NULL COMMENT '音乐ID', + `play_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '播放时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_music_id` (`music_id`), + CONSTRAINT `fk_ph_music_id` FOREIGN KEY (`music_id`) REFERENCES `music` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_ph_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='播放历史表'; + +-- 系统日志表 +CREATE TABLE `system_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID', + `user_id` bigint DEFAULT NULL COMMENT '用户ID', + `operation` varchar(50) DEFAULT NULL COMMENT '操作类型', + `method` varchar(100) DEFAULT NULL COMMENT '请求方法', + `params` varchar(2000) DEFAULT NULL COMMENT '请求参数', + `ip` varchar(50) DEFAULT NULL COMMENT 'IP地址', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表'; \ No newline at end of file