login and register
This commit is contained in:
parent
6467eac4ed
commit
c2f59d9f07
11
pom.xml
11
pom.xml
@ -42,6 +42,11 @@
|
||||
<version>3.5.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
@ -71,6 +76,12 @@
|
||||
<version>3.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>3.8.2</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
17
src/main/java/top/baogutang/music/annos/Login.java
Normal file
17
src/main/java/top/baogutang/music/annos/Login.java
Normal file
@ -0,0 +1,17 @@
|
||||
package top.baogutang.music.annos;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 17:52
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Login {
|
||||
boolean required() default true;
|
||||
}
|
||||
109
src/main/java/top/baogutang/music/aspect/LoginAspect.java
Normal file
109
src/main/java/top/baogutang/music/aspect/LoginAspect.java
Normal file
@ -0,0 +1,109 @@
|
||||
package top.baogutang.music.aspect;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import top.baogutang.music.annos.Login;
|
||||
import top.baogutang.music.domain.res.user.UserLoginRes;
|
||||
import top.baogutang.music.exceptions.LoginException;
|
||||
import top.baogutang.music.utils.TokenUtil;
|
||||
import top.baogutang.music.utils.UserThreadLocal;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import static top.baogutang.music.constants.CacheKey.KEY_LOGIN_PREFIX;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 17:53
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class LoginAspect {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public static final String AUTHORIZATION = "token";
|
||||
|
||||
@Pointcut("@annotation(top.baogutang.music.annos.Login)")
|
||||
public void point() {
|
||||
}
|
||||
|
||||
@Before(value = "point() && @annotation(login)")
|
||||
public void verifyTokenForClass(Login login) {
|
||||
if (login.required()) {
|
||||
checkToken();
|
||||
} else {
|
||||
checkTokenWithOutRequired();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkTokenWithOutRequired() {
|
||||
ServletRequestAttributes requestAttributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (Objects.isNull(requestAttributes)) {
|
||||
return;
|
||||
}
|
||||
HttpServletRequest request = requestAttributes.getRequest();
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (Objects.isNull(cookies) || cookies.length == 0) {
|
||||
return;
|
||||
}
|
||||
Cookie tokenCookie = Arrays.stream(cookies)
|
||||
.filter(cookie -> AUTHORIZATION.equalsIgnoreCase(cookie.getName()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (Objects.isNull(tokenCookie) || StringUtils.isBlank(tokenCookie.getValue())) {
|
||||
return;
|
||||
}
|
||||
String token = tokenCookie.getValue();
|
||||
UserLoginRes userLoginRes = TokenUtil.verify(token);
|
||||
UserThreadLocal.set(userLoginRes.getId());
|
||||
}
|
||||
|
||||
|
||||
private void checkToken() {
|
||||
|
||||
ServletRequestAttributes requestAttributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (Objects.isNull(requestAttributes)) {
|
||||
return;
|
||||
}
|
||||
HttpServletRequest request = requestAttributes.getRequest();
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (Objects.isNull(cookies) || cookies.length == 0) {
|
||||
throw new LoginException("请登录后操作");
|
||||
}
|
||||
Cookie tokenCookie = Arrays.stream(cookies)
|
||||
.filter(cookie -> AUTHORIZATION.equalsIgnoreCase(cookie.getName()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (Objects.isNull(tokenCookie) || StringUtils.isBlank(tokenCookie.getValue())) {
|
||||
throw new LoginException("请登录后操作");
|
||||
}
|
||||
String token = tokenCookie.getValue();
|
||||
log.info("request url:{},method:{}", request.getRequestURL(), request.getMethod());
|
||||
UserLoginRes userLoginRes = TokenUtil.verify(token);
|
||||
Long userId = userLoginRes.getId();
|
||||
Object val = redisTemplate.opsForValue().get(KEY_LOGIN_PREFIX + userId);
|
||||
if (Objects.isNull(val)) {
|
||||
redisTemplate.opsForValue().set(KEY_LOGIN_PREFIX + userId, token);
|
||||
}
|
||||
UserThreadLocal.set(userId);
|
||||
}
|
||||
}
|
||||
70
src/main/java/top/baogutang/music/config/RedisConfig.java
Normal file
70
src/main/java/top/baogutang/music/config/RedisConfig.java
Normal file
@ -0,0 +1,70 @@
|
||||
package top.baogutang.music.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 10:56
|
||||
*/
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* 编写自定义的 redisTemplate
|
||||
* 这是一个比较固定的模板
|
||||
*/
|
||||
@SuppressWarnings("all")
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
|
||||
// 为了开发方便,直接使用<String, Object>
|
||||
RedisTemplate<String, Object> template = new RedisTemplate();
|
||||
template.setConnectionFactory(redisConnectionFactory);
|
||||
|
||||
// Json 配置序列化
|
||||
// 使用 jackson 解析任意的对象
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
|
||||
// 使用 objectMapper 进行转义
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
objectMapper.setTimeZone(TimeZone.getDefault());
|
||||
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
|
||||
// String 的序列化
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
|
||||
// key 采用 String 的序列化方式
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
// Hash 的 key 采用 String 的序列化方式
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
// value 采用 jackson 的序列化方式
|
||||
template.setValueSerializer(jackson2JsonRedisSerializer);
|
||||
// Hash 的 value 采用 String 的序列化方式
|
||||
template.setHashValueSerializer(jackson2JsonRedisSerializer);
|
||||
// 把所有的配置 set 进 template
|
||||
template.afterPropertiesSet();
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
21
src/main/java/top/baogutang/music/constants/CacheKey.java
Normal file
21
src/main/java/top/baogutang/music/constants/CacheKey.java
Normal file
@ -0,0 +1,21 @@
|
||||
package top.baogutang.music.constants;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: nikooh
|
||||
* @date: 2024/11/01 : 11:03
|
||||
*/
|
||||
public class CacheKey {
|
||||
|
||||
private CacheKey() {
|
||||
// private constructor
|
||||
}
|
||||
|
||||
public static final String LOCK_REGISTER_PREFIX = "baogutang-music:lock:register:username:";
|
||||
|
||||
public static final String KEY_LOGIN_PREFIX = "baogutang-music:user:token:id:";
|
||||
|
||||
|
||||
}
|
||||
@ -5,6 +5,7 @@ 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 top.baogutang.music.annos.Login;
|
||||
import top.baogutang.music.domain.Results;
|
||||
import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
||||
import top.baogutang.music.domain.res.search.MusicPlaylistRes;
|
||||
@ -28,6 +29,7 @@ public class MusicDownloadController {
|
||||
private IMusicService musicService;
|
||||
|
||||
@GetMapping
|
||||
@Login
|
||||
public Results<MusicDownloadRes> download(@RequestParam(name = "channel") Integer channel,
|
||||
@RequestParam(name = "id") String id) {
|
||||
MusicDownloadRes res = musicService.getMusicService(channel).download(id);
|
||||
|
||||
@ -5,6 +5,7 @@ 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 top.baogutang.music.annos.Login;
|
||||
import top.baogutang.music.domain.Results;
|
||||
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
||||
import top.baogutang.music.domain.res.search.*;
|
||||
@ -28,12 +29,14 @@ public class MusicSearchController {
|
||||
private IMusicService musicService;
|
||||
|
||||
@GetMapping
|
||||
@Login
|
||||
public Results<MusicSearchRes> search(MusicSearchReq req) {
|
||||
MusicSearchRes res = musicService.getMusicService(req.getChannel()).search(req);
|
||||
return Results.ok(res);
|
||||
}
|
||||
|
||||
@GetMapping("/playlist")
|
||||
@Login
|
||||
public Results<MusicPlaylistRes> playlist(@RequestParam(name = "channel") Integer channel,
|
||||
@RequestParam(name = "id") String id) {
|
||||
MusicPlaylistRes res = musicService.getMusicService(channel).playList(id);
|
||||
@ -41,6 +44,7 @@ public class MusicSearchController {
|
||||
}
|
||||
|
||||
@GetMapping("/album")
|
||||
@Login
|
||||
public Results<MusicAlbumRes> album(@RequestParam(name = "channel") Integer channel,
|
||||
@RequestParam(name = "id") String id) {
|
||||
MusicAlbumRes res = musicService.getMusicService(channel).album(id);
|
||||
@ -48,6 +52,7 @@ public class MusicSearchController {
|
||||
}
|
||||
|
||||
@GetMapping("/artist")
|
||||
@Login
|
||||
public Results<MusicArtistRes> artist(@RequestParam(name = "channel") Integer channel,
|
||||
@RequestParam(name = "id") String id) {
|
||||
MusicArtistRes res = musicService.getMusicService(channel).artist(id);
|
||||
@ -55,6 +60,7 @@ public class MusicSearchController {
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
@Login
|
||||
public Results<MusicDetailRes> detail(@RequestParam(name = "channel") Integer channel,
|
||||
@RequestParam(name = "id") String id) {
|
||||
MusicDetailRes res = musicService.getMusicService(channel).detail(id);
|
||||
|
||||
@ -14,8 +14,14 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
public class MusicStaticController {
|
||||
|
||||
@RequestMapping
|
||||
public String viewJsonParseHtml() {
|
||||
public String music() {
|
||||
// 这里返回的字符串是HTML文件名(不包括扩展名)
|
||||
return "music";
|
||||
}
|
||||
|
||||
@RequestMapping("/login")
|
||||
public String login() {
|
||||
return "login";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package top.baogutang.music.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import top.baogutang.music.domain.Results;
|
||||
import top.baogutang.music.domain.req.user.UserRegisterAndLoginReq;
|
||||
import top.baogutang.music.domain.res.user.UserLoginRes;
|
||||
import top.baogutang.music.service.IUserService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 16:34
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/user")
|
||||
public class UserController {
|
||||
|
||||
@Resource
|
||||
private IUserService userService;
|
||||
|
||||
@PostMapping("/register")
|
||||
public Results<Void> register(@RequestBody UserRegisterAndLoginReq req) {
|
||||
userService.register(req);
|
||||
return Results.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public Results<UserLoginRes> login(@RequestBody UserRegisterAndLoginReq req) {
|
||||
return Results.ok(userService.login(req));
|
||||
}
|
||||
}
|
||||
33
src/main/java/top/baogutang/music/dao/entity/UserEntity.java
Normal file
33
src/main/java/top/baogutang/music/dao/entity/UserEntity.java
Normal file
@ -0,0 +1,33 @@
|
||||
package top.baogutang.music.dao.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import top.baogutang.music.enums.UserLevel;
|
||||
import top.baogutang.music.enums.UserRole;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 16:41
|
||||
*/
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("t_user")
|
||||
public class UserEntity extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = -7939103741882908678L;
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private UserRole role;
|
||||
|
||||
private UserLevel level;
|
||||
|
||||
private String avatar;
|
||||
}
|
||||
16
src/main/java/top/baogutang/music/dao/mapper/UserMapper.java
Normal file
16
src/main/java/top/baogutang/music/dao/mapper/UserMapper.java
Normal file
@ -0,0 +1,16 @@
|
||||
package top.baogutang.music.dao.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import top.baogutang.music.dao.entity.UserEntity;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 16:44
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<UserEntity> {
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package top.baogutang.music.domain.req.user;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 16:35
|
||||
*/
|
||||
@Data
|
||||
public class UserRegisterAndLoginReq implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 5259676190187565515L;
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package top.baogutang.music.domain.res.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import top.baogutang.music.enums.UserLevel;
|
||||
import top.baogutang.music.enums.UserRole;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 17:17
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class UserLoginRes implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -4434691278877613222L;
|
||||
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
private UserLevel level;
|
||||
|
||||
private UserRole role;
|
||||
|
||||
private String token;
|
||||
}
|
||||
19
src/main/java/top/baogutang/music/enums/UserLevel.java
Normal file
19
src/main/java/top/baogutang/music/enums/UserLevel.java
Normal file
@ -0,0 +1,19 @@
|
||||
package top.baogutang.music.enums;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 16:43
|
||||
*/
|
||||
public enum UserLevel {
|
||||
|
||||
NORMAL,
|
||||
|
||||
VIP,
|
||||
|
||||
SUPER_VIP,
|
||||
|
||||
;
|
||||
}
|
||||
18
src/main/java/top/baogutang/music/enums/UserRole.java
Normal file
18
src/main/java/top/baogutang/music/enums/UserRole.java
Normal file
@ -0,0 +1,18 @@
|
||||
package top.baogutang.music.enums;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 16:43
|
||||
*/
|
||||
public enum UserRole {
|
||||
|
||||
|
||||
NORMAL,
|
||||
|
||||
ADMIN,
|
||||
|
||||
;
|
||||
}
|
||||
@ -29,6 +29,20 @@ public class GlobalExceptionHandler {
|
||||
//
|
||||
}
|
||||
|
||||
@ExceptionHandler({BusinessException.class})
|
||||
public Results<Object> businessException(BusinessException e) {
|
||||
log.error("请求发生错误,code:{},message:{}", e.getCode(), e.getMessage());
|
||||
return Results.failed(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({LoginException.class})
|
||||
public Results<Object> loginException(LoginException e) {
|
||||
log.error("请求发生错误,code:{},message:{}", e.getCode(), e.getMessage());
|
||||
return Results.failed(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ExceptionHandler({Throwable.class})
|
||||
public Results<Object> handleException(Throwable e) {
|
||||
log.error("请求发生错误,错误信息:{}", e.getMessage(), e);
|
||||
@ -54,12 +68,6 @@ public class GlobalExceptionHandler {
|
||||
return Results.failed(300, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({BusinessException.class})
|
||||
public Results<Object> businessException(BusinessException e) {
|
||||
log.error("请求发生错误,code:{},message:{}", e.getCode(), e.getMessage());
|
||||
return Results.failed(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({MethodArgumentNotValidException.class})
|
||||
public Results<Object> parameterExceptionHandler(MethodArgumentNotValidException e) {
|
||||
BindingResult exceptions = e.getBindingResult();
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
package top.baogutang.music.exceptions;
|
||||
|
||||
import top.baogutang.music.domain.Results;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 17:33
|
||||
*/
|
||||
public class LoginException extends RuntimeException {
|
||||
|
||||
private int code = Results.FAIL_CODE;
|
||||
|
||||
public LoginException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
|
||||
public LoginException(String message) {
|
||||
super(message);
|
||||
this.code = -200;
|
||||
}
|
||||
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
20
src/main/java/top/baogutang/music/service/IUserService.java
Normal file
20
src/main/java/top/baogutang/music/service/IUserService.java
Normal file
@ -0,0 +1,20 @@
|
||||
package top.baogutang.music.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import top.baogutang.music.dao.entity.UserEntity;
|
||||
import top.baogutang.music.domain.req.user.UserRegisterAndLoginReq;
|
||||
import top.baogutang.music.domain.res.user.UserLoginRes;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 16:41
|
||||
*/
|
||||
public interface IUserService extends IService<UserEntity> {
|
||||
|
||||
void register(UserRegisterAndLoginReq req);
|
||||
|
||||
UserLoginRes login(UserRegisterAndLoginReq req);
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package top.baogutang.music.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import top.baogutang.music.dao.entity.UserEntity;
|
||||
import top.baogutang.music.dao.mapper.UserMapper;
|
||||
import top.baogutang.music.domain.req.user.UserRegisterAndLoginReq;
|
||||
import top.baogutang.music.domain.res.user.UserLoginRes;
|
||||
import top.baogutang.music.enums.UserLevel;
|
||||
import top.baogutang.music.enums.UserRole;
|
||||
import top.baogutang.music.exceptions.BusinessException;
|
||||
import top.baogutang.music.exceptions.LoginException;
|
||||
import top.baogutang.music.service.IUserService;
|
||||
import top.baogutang.music.utils.CacheUtil;
|
||||
import top.baogutang.music.utils.TokenUtil;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static top.baogutang.music.constants.CacheKey.KEY_LOGIN_PREFIX;
|
||||
import static top.baogutang.music.constants.CacheKey.LOCK_REGISTER_PREFIX;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: N1KO
|
||||
* @date: 2024/12/13 : 16:44
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements IUserService {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public void register(UserRegisterAndLoginReq req) {
|
||||
String lockKey = LOCK_REGISTER_PREFIX + req.getUsername();
|
||||
CacheUtil.lockAndExecute(lockKey,
|
||||
10L,
|
||||
TimeUnit.SECONDS,
|
||||
redisTemplate,
|
||||
() -> {
|
||||
// 校验
|
||||
UserEntity existsUser = this.validate(req);
|
||||
if (Objects.nonNull(existsUser)) {
|
||||
throw new BusinessException("当前用户名已被注册");
|
||||
}
|
||||
// 密码加密
|
||||
UserEntity user = new UserEntity();
|
||||
user.setUsername(req.getUsername());
|
||||
user.setPassword(encryptPassword(req.getPassword()));
|
||||
user.setRole(UserRole.NORMAL);
|
||||
user.setLevel(UserLevel.NORMAL);
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
save(user);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserLoginRes login(UserRegisterAndLoginReq req) {
|
||||
UserEntity existsUser = this.validate(req);
|
||||
if (Objects.isNull(existsUser)) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
boolean checkRes = this.verifyPassword(req.getPassword(), existsUser.getPassword());
|
||||
if (!Boolean.TRUE.equals(checkRes)) {
|
||||
throw new LoginException("用户名或密码错误");
|
||||
}
|
||||
UserLoginRes loginRes = new UserLoginRes();
|
||||
loginRes.setId(existsUser.getId());
|
||||
loginRes.setUsername(existsUser.getUsername());
|
||||
loginRes.setLevel(existsUser.getLevel());
|
||||
loginRes.setRole(existsUser.getRole());
|
||||
// 生成token
|
||||
String token = TokenUtil.token(existsUser.getId(), loginRes.getUsername());
|
||||
redisTemplate.opsForValue().set(KEY_LOGIN_PREFIX + existsUser.getId(), token, 86400000, TimeUnit.MILLISECONDS);
|
||||
loginRes.setToken(token);
|
||||
return loginRes;
|
||||
}
|
||||
|
||||
private UserEntity validate(UserRegisterAndLoginReq req) {
|
||||
if (StringUtils.isBlank(req.getUsername())) {
|
||||
throw new BusinessException("用户名不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(req.getPassword())) {
|
||||
throw new BusinessException("密码不能为空");
|
||||
}
|
||||
if (req.getPassword().length() < 6 || req.getPassword().length() > 16) {
|
||||
throw new BusinessException("密码长度不合规");
|
||||
}
|
||||
if (req.getUsername().length() < 4 || req.getUsername().length() > 12) {
|
||||
throw new BusinessException("用户名长度不合规");
|
||||
}
|
||||
return new LambdaQueryChainWrapper<>(baseMapper)
|
||||
.eq(UserEntity::getUsername, req.getUsername())
|
||||
.eq(UserEntity::getDeleted, Boolean.FALSE)
|
||||
.last(" limit 1")
|
||||
.one();
|
||||
}
|
||||
|
||||
// 密码加密方法
|
||||
public String encryptPassword(String password) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
md.update(password.getBytes());
|
||||
byte[] digest = md.digest();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) {
|
||||
sb.append(String.format("%02x", b & 0xff));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new BusinessException("请稍后再试");
|
||||
}
|
||||
}
|
||||
|
||||
// 登录验证方法
|
||||
public boolean verifyPassword(String inputPassword, String encryptedPassword) {
|
||||
String inputEncryptedPassword = encryptPassword(inputPassword);
|
||||
return encryptedPassword.equals(inputEncryptedPassword);
|
||||
}
|
||||
}
|
||||
131
src/main/java/top/baogutang/music/utils/CacheUtil.java
Normal file
131
src/main/java/top/baogutang/music/utils/CacheUtil.java
Normal file
@ -0,0 +1,131 @@
|
||||
package top.baogutang.music.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
*
|
||||
* @description:
|
||||
*
|
||||
* @author: nikooh
|
||||
* @date: 2024/11/01 : 11:14
|
||||
*/
|
||||
@Slf4j
|
||||
public class CacheUtil {
|
||||
|
||||
private CacheUtil() {
|
||||
// private constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* lock and then supply
|
||||
*
|
||||
* @param lockKey lock key
|
||||
* @param timeout expire time
|
||||
* @param timeUnit expire time unit
|
||||
* @param redisTemplate redisTemplate
|
||||
* @param supplier supplier
|
||||
* @return T
|
||||
* @param <T> T
|
||||
*/
|
||||
public static <T> T lockAndSupply(String lockKey,
|
||||
Long timeout,
|
||||
TimeUnit timeUnit,
|
||||
RedisTemplate<String, Object> redisTemplate,
|
||||
Supplier<T> supplier) {
|
||||
// identity unique holder
|
||||
String lockValue = UUID.randomUUID().toString();
|
||||
Boolean lockAcquired = redisTemplate.opsForValue()
|
||||
.setIfAbsent(lockKey, lockValue, timeout, timeUnit);
|
||||
if (Boolean.TRUE.equals(lockAcquired)) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (Exception e) {
|
||||
log.error(">>>>>>>>>>deal supplier error:{}<<<<<<<<<<", e.getMessage(), e);
|
||||
} finally {
|
||||
String currentLockValue = (String) redisTemplate.opsForValue().get(lockKey);
|
||||
if (lockValue.equals(currentLockValue)) {
|
||||
redisTemplate.delete(lockKey);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error(">>>>>>>>>>Failed to acquire lock for key:{}<<<<<<<<<<", lockKey);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* lock and then execute
|
||||
* @param lockKey lock key
|
||||
* @param timeout lock expire
|
||||
* @param timeUnit lock time unit
|
||||
* @param redisTemplate redisTemplate
|
||||
* @param runnable runnable
|
||||
* @return lock res
|
||||
*/
|
||||
public static boolean lockAndExecute(String lockKey,
|
||||
Long timeout,
|
||||
TimeUnit timeUnit,
|
||||
RedisTemplate<String, Object> redisTemplate,
|
||||
Runnable runnable) {
|
||||
// identity unique holder
|
||||
String lockValue = UUID.randomUUID().toString();
|
||||
Boolean lockAcquired = redisTemplate.opsForValue()
|
||||
.setIfAbsent(lockKey, lockValue, timeout, timeUnit);
|
||||
if (Boolean.TRUE.equals(lockAcquired)) {
|
||||
try {
|
||||
runnable.run();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error(">>>>>>>>>>deal runnable error:{}<<<<<<<<<<", e.getMessage(), e);
|
||||
return false;
|
||||
} finally {
|
||||
String currentLockValue = (String) redisTemplate.opsForValue().get(lockKey);
|
||||
if (lockValue.equals(currentLockValue)) {
|
||||
redisTemplate.delete(lockKey);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error(">>>>>>>>>>Failed to acquire lock for key:{}<<<<<<<<<<", lockKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get from cache or supplier
|
||||
* @param cacheKey cache key
|
||||
* @param timeout cache expire
|
||||
* @param timeUnit cache time unit
|
||||
* @param redisTemplate redisTemplate
|
||||
* @param supplier supplier
|
||||
* @return supplier res
|
||||
* @param <T> T
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T cacheOrSupply(String cacheKey,
|
||||
Long timeout,
|
||||
TimeUnit timeUnit,
|
||||
RedisTemplate<String, Object> redisTemplate,
|
||||
Supplier<T> supplier) {
|
||||
Object cacheObject = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (Objects.nonNull(cacheObject)) {
|
||||
return (T) cacheObject;
|
||||
}
|
||||
T t = supplier.get();
|
||||
if (Objects.isNull(t)) {
|
||||
return null;
|
||||
}
|
||||
TimeUnit unit = Objects.isNull(timeUnit) ? TimeUnit.SECONDS : timeUnit;
|
||||
if (Objects.isNull(timeout)) {
|
||||
redisTemplate.opsForValue().set(cacheKey, t);
|
||||
} else {
|
||||
redisTemplate.opsForValue().set(cacheKey, t, timeout, unit);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
}
|
||||
85
src/main/java/top/baogutang/music/utils/TokenUtil.java
Normal file
85
src/main/java/top/baogutang/music/utils/TokenUtil.java
Normal file
@ -0,0 +1,85 @@
|
||||
package top.baogutang.music.utils;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.baogutang.music.domain.res.user.UserLoginRes;
|
||||
import top.baogutang.music.exceptions.BusinessException;
|
||||
import top.baogutang.music.exceptions.LoginException;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @desc 使用token验证用户是否登录
|
||||
* @author zm
|
||||
**/
|
||||
@Slf4j
|
||||
public class TokenUtil {
|
||||
|
||||
//设置过期时间(一天)
|
||||
private static final long EXPIRE_DATE = 86400000;
|
||||
|
||||
//token秘钥
|
||||
private static final String TOKEN_SECRET = "ZCfasfBhuaUaUHOufGguuGuwTu2a02N0BQGWE";
|
||||
|
||||
public static String token(Long id, String username) {
|
||||
|
||||
String token = "";
|
||||
try {
|
||||
//过期时间
|
||||
Date date = new Date(System.currentTimeMillis() + EXPIRE_DATE);
|
||||
//秘钥及加密算法
|
||||
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
|
||||
//设置头部信息
|
||||
Map<String, Object> header = new HashMap<>();
|
||||
header.put("typ", "JWT");
|
||||
header.put("alg", "HS256");
|
||||
//携带username,password信息,生成签名
|
||||
token = JWT.create()
|
||||
.withHeader(header)
|
||||
.withClaim("username", username)
|
||||
.withClaim("id", id)
|
||||
.withExpiresAt(date)
|
||||
.sign(algorithm);
|
||||
} catch (Exception e) {
|
||||
log.error(">>>>>>>>>>gene token error:{}<<<<<<<<<<", e.getMessage(), e);
|
||||
throw new LoginException("请稍后重试");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
public static UserLoginRes verify(String token) {
|
||||
/**
|
||||
* @desc 验证token,通过返回true
|
||||
* @params [token]需要校验的串
|
||||
**/
|
||||
try {
|
||||
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
|
||||
JWTVerifier verifier = JWT.require(algorithm).build();
|
||||
DecodedJWT jwt = verifier.verify(token);
|
||||
if (jwt.getExpiresAt().before(new Date())) {
|
||||
throw new LoginException("登录已失效");
|
||||
}
|
||||
String username = jwt.getClaim("username").asString();
|
||||
Long id = jwt.getClaim("id").asLong();
|
||||
return UserLoginRes.builder()
|
||||
.id(id)
|
||||
.username(username)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
throw new LoginException("登录认证失败");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String username = "zhangsan";
|
||||
String token = token(1L, username);
|
||||
System.out.println(token);
|
||||
UserLoginRes userLoginRes = verify(token);
|
||||
System.out.println(JacksonUtil.toJson(userLoginRes));
|
||||
}
|
||||
}
|
||||
22
src/main/java/top/baogutang/music/utils/UserThreadLocal.java
Normal file
22
src/main/java/top/baogutang/music/utils/UserThreadLocal.java
Normal file
@ -0,0 +1,22 @@
|
||||
package top.baogutang.music.utils;
|
||||
|
||||
/**
|
||||
* @author developer
|
||||
*/
|
||||
public class UserThreadLocal {
|
||||
|
||||
private static final ThreadLocal<Long> userThread = new ThreadLocal<>();
|
||||
|
||||
public static void set(Long userId) {
|
||||
userThread.set(userId);
|
||||
}
|
||||
|
||||
public static Long get() {
|
||||
return userThread.get();
|
||||
}
|
||||
|
||||
public static void remove() {
|
||||
userThread.remove();
|
||||
}
|
||||
|
||||
}
|
||||
240
src/main/resources/templates/login.html
Normal file
240
src/main/resources/templates/login.html
Normal file
@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<link rel="icon" type="image/png" href="NIKO.png">
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>BAOGUTANG-MUSIC-LOGIN</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background-color: #f4f4f9;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
width: 95%;
|
||||
max-width: 400px;
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #62D2A1;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 1rem;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"] {
|
||||
padding: 10px;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
background-color: #f9f9f9;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.login-btn, .register-btn {
|
||||
background-color: #62D2A1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.login-btn:hover, .register-btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: #ff0000;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>登录BAOGUTANG-MUSIC</h1>
|
||||
<div class="error-msg" id="error-msg" style="display:none;"></div>
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="username" placeholder="请输入用户名 (字母数字组合)"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" id="password" placeholder="请输入密码 (字母数字组合)"/>
|
||||
</div>
|
||||
|
||||
<button class="login-btn" id="login-btn">登录</button>
|
||||
<button class="register-btn" id="register-btn">注册</button>
|
||||
|
||||
<div class="tips">没有账号?请点击注册按钮进行注册。</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 登录接口URL及注册接口URL稍后提供,这里先占位
|
||||
const loginUrl = '/api/v1/user/login'; // 后端登录接口地址(示例)
|
||||
const registerUrl = '/api/v1/user/register'; // 后端注册接口地址(示例)
|
||||
const musicPageUrl = '/music.html'; // 登录后跳转的音乐搜索下载页面地址
|
||||
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const registerBtn = document.getElementById('register-btn');
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
|
||||
loginBtn.addEventListener('click', () => {
|
||||
if (!validateInputs()) return;
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value.trim();
|
||||
doLogin(username, password);
|
||||
});
|
||||
|
||||
registerBtn.addEventListener('click', () => {
|
||||
if (!validateInputs()) return;
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value.trim();
|
||||
doRegister(username, password);
|
||||
});
|
||||
|
||||
function validateInputs() {
|
||||
errorMsg.style.display = 'none';
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value.trim();
|
||||
const reg = /^[A-Za-z0-9]+$/; // 用户名和密码必须是字母和数字组合
|
||||
|
||||
if (!username || !password) {
|
||||
showError("用户名和密码不能为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (username.length < 4) {
|
||||
showError("用户名长度不能小于4位");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showError("密码长度不能小于6位");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (username.length > 12) {
|
||||
showError("用户名过长");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.length > 16) {
|
||||
showError("密码过长");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!reg.test(username) || !reg.test(password)) {
|
||||
showError("用户名和密码必须是字母和数字的组合");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function doLogin(username, password) {
|
||||
fetch(loginUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({username: username, password: password})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
console.log('登录响应:', data);
|
||||
if (data.code === 200) {
|
||||
// 登陆成功,data.data中有token和用户名
|
||||
const token = data.data.token;
|
||||
const name = data.data.username;
|
||||
setCookie('token', token, 1); // 有效期1天,可自行调整
|
||||
setCookie('username', name, 1);
|
||||
// 跳转到音乐搜索下载页面
|
||||
window.location.href = musicPageUrl;
|
||||
} else {
|
||||
// 登录失败
|
||||
if (data.code === -200) {
|
||||
showError(data.msg || "登录失败,请重试");
|
||||
} else {
|
||||
showError(data.msg || "登录失败,请重试");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('登录接口调用错误', err);
|
||||
showError("登录接口调用失败,请稍后重试");
|
||||
});
|
||||
}
|
||||
|
||||
function doRegister(username, password) {
|
||||
fetch('/api/v1/user/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({username: username, password: password})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
// 注册成功,刷新当前页面等待用户登录
|
||||
alert("注册成功,请使用新账号登录");
|
||||
window.location.reload();
|
||||
} else {
|
||||
showError(data.msg || "注册失败,请重试");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('注册接口调用错误', err);
|
||||
showError("注册接口调用失败,请稍后重试");
|
||||
});
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorMsg.textContent = msg;
|
||||
errorMsg.style.display = 'block';
|
||||
}
|
||||
|
||||
function setCookie(name, value, days) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
let expires = "expires=" + d.toUTCString();
|
||||
document.cookie = name + "=" + value + "; " + expires + "; path=/";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -20,12 +20,23 @@
|
||||
width: 95%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
h1 {
|
||||
color: #62D2A1;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.user-info {
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
@ -313,6 +324,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="user-info" id="user-info">欢迎, 未登录</div>
|
||||
</div>
|
||||
<h1>BAOGUTANG-MUSIC</h1>
|
||||
|
||||
<div class="form-group">
|
||||
@ -364,30 +378,39 @@
|
||||
fetchPlatforms();
|
||||
fetchSearchTypes();
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
displayUsername();
|
||||
});
|
||||
|
||||
function fetchPlatforms() {
|
||||
fetch('/api/v1/music/common?type=channel')
|
||||
fetch('/api/v1/music/common?type=channel', {
|
||||
credentials: 'include' // 确保请求携带cookie
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const platformList = document.getElementById('platform-list');
|
||||
data.data.forEach(platform => {
|
||||
const platformItem = document.createElement('div');
|
||||
platformItem.classList.add('item');
|
||||
const logoSrc = logos[platform.code] || '';
|
||||
platformItem.innerHTML = `<img src="${logoSrc}" class="platform-logo" alt="${platform.desc}" /> ${platform.desc}`;
|
||||
platformItem.setAttribute('data-code', platform.code);
|
||||
if (data.code === 200) {
|
||||
const platformList = document.getElementById('platform-list');
|
||||
data.data.forEach(platform => {
|
||||
const platformItem = document.createElement('div');
|
||||
platformItem.classList.add('item');
|
||||
const logoSrc = logos[platform.code] || '';
|
||||
platformItem.innerHTML = `<img src="${logoSrc}" class="platform-logo" alt="${platform.desc}" /> ${platform.desc}`;
|
||||
platformItem.setAttribute('data-code', platform.code);
|
||||
|
||||
platformItem.addEventListener('click', () => {
|
||||
document.querySelectorAll('#platform-list .item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
platformItem.addEventListener('click', () => {
|
||||
document.querySelectorAll('#platform-list .item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
platformItem.classList.add('selected');
|
||||
selectedPlatformCode = platform.code;
|
||||
});
|
||||
platformItem.classList.add('selected');
|
||||
selectedPlatformCode = platform.code;
|
||||
});
|
||||
|
||||
platformList.appendChild(platformItem);
|
||||
});
|
||||
platformList.appendChild(platformItem);
|
||||
});
|
||||
} else if (data.code === -200) {
|
||||
showMessage(data.msg);
|
||||
} else {
|
||||
console.error('未知的响应码:', data.code);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('请求平台接口失败:', error);
|
||||
@ -395,26 +418,34 @@
|
||||
}
|
||||
|
||||
function fetchSearchTypes() {
|
||||
fetch('/api/v1/music/common?type=searchType')
|
||||
fetch('/api/v1/music/common?type=searchType', {
|
||||
credentials: 'include' // 确保请求携带cookie
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const searchTypeList = document.getElementById('searchType-list');
|
||||
data.data.forEach(type => {
|
||||
const typeItem = document.createElement('div');
|
||||
typeItem.classList.add('item');
|
||||
typeItem.textContent = type.desc;
|
||||
typeItem.setAttribute('data-code', type.code);
|
||||
if (data.code === 200) {
|
||||
const searchTypeList = document.getElementById('searchType-list');
|
||||
data.data.forEach(type => {
|
||||
const typeItem = document.createElement('div');
|
||||
typeItem.classList.add('item');
|
||||
typeItem.textContent = type.desc;
|
||||
typeItem.setAttribute('data-code', type.code);
|
||||
|
||||
typeItem.addEventListener('click', () => {
|
||||
document.querySelectorAll('#searchType-list .item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
typeItem.addEventListener('click', () => {
|
||||
document.querySelectorAll('#searchType-list .item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
typeItem.classList.add('selected');
|
||||
selectedSearchTypeCode = type.code;
|
||||
});
|
||||
typeItem.classList.add('selected');
|
||||
selectedSearchTypeCode = type.code;
|
||||
});
|
||||
|
||||
searchTypeList.appendChild(typeItem);
|
||||
});
|
||||
searchTypeList.appendChild(typeItem);
|
||||
});
|
||||
} else if (data.code === -200) {
|
||||
showMessage(data.msg);
|
||||
} else {
|
||||
console.error('未知的响应码:', data.code);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('请求搜索类型接口失败:', error);
|
||||
@ -448,7 +479,9 @@
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
|
||||
const keywords = document.getElementById('keywords').value;
|
||||
fetch(`/api/v1/music/search?channel=${selectedPlatformCode}&keywords=${keywords}&type=${selectedSearchTypeCode}&offset=${offset}&limit=${limit}`)
|
||||
fetch(`/api/v1/music/search?channel=${selectedPlatformCode}&keywords=${encodeURIComponent(keywords)}&type=${selectedSearchTypeCode}&offset=${offset}&limit=${limit}`, {
|
||||
credentials: 'include' // 确保请求携带cookie
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
loading = false;
|
||||
@ -456,18 +489,24 @@
|
||||
const resultDiv = document.getElementById('result');
|
||||
const result = data.data?.result || {};
|
||||
|
||||
if (selectedSearchTypeCode == "1") {
|
||||
// 单曲
|
||||
displaySongs(result, resultDiv);
|
||||
} else if (selectedSearchTypeCode == "10") {
|
||||
// 专辑
|
||||
displayAlbums(result, resultDiv);
|
||||
} else if (selectedSearchTypeCode == "100") {
|
||||
// 歌手
|
||||
displayArtists(result, resultDiv);
|
||||
} else if (selectedSearchTypeCode == "1000") {
|
||||
// 歌单
|
||||
displayPlaylists(result, resultDiv);
|
||||
if (data.code === 200) {
|
||||
if (selectedSearchTypeCode == "1") {
|
||||
// 单曲
|
||||
displaySongs(result, resultDiv);
|
||||
} else if (selectedSearchTypeCode == "10") {
|
||||
// 专辑
|
||||
displayAlbums(result, resultDiv);
|
||||
} else if (selectedSearchTypeCode == "100") {
|
||||
// 歌手
|
||||
displayArtists(result, resultDiv);
|
||||
} else if (selectedSearchTypeCode == "1000") {
|
||||
// 歌单
|
||||
displayPlaylists(result, resultDiv);
|
||||
}
|
||||
} else if (data.code === -200) {
|
||||
showMessage(data.msg);
|
||||
} else {
|
||||
console.error('未知的响应码:', data.code);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@ -487,13 +526,13 @@
|
||||
header = document.createElement('div');
|
||||
header.className = 'list-header';
|
||||
header.innerHTML = `
|
||||
<div class="header-info">
|
||||
<div class="song-checkbox select-all-wrapper"><input type="checkbox"></div>
|
||||
<div class="song-name">名字</div>
|
||||
<div class="song-album">所属专辑</div>
|
||||
<div class="song-artists">作者</div>
|
||||
</div>
|
||||
`;
|
||||
<div class="header-info">
|
||||
<div class="song-checkbox select-all-wrapper"><input type="checkbox"></div>
|
||||
<div class="song-name">名字</div>
|
||||
<div class="song-album">所属专辑</div>
|
||||
<div class="song-artists">作者</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const selectAllCheckbox = header.querySelector('.select-all-wrapper input[type="checkbox"]');
|
||||
selectAllCheckbox.addEventListener('change', toggleSelectAll);
|
||||
@ -520,11 +559,11 @@
|
||||
const row = document.createElement('div');
|
||||
row.className = 'song-row';
|
||||
row.innerHTML = `
|
||||
<div class="song-checkbox"><input type="checkbox"></div>
|
||||
<div class="song-name">${song.name}</div>
|
||||
<div class="song-album">${albumName}</div>
|
||||
<div class="song-artists">${artists}</div>
|
||||
`;
|
||||
<div class="song-checkbox"><input type="checkbox"></div>
|
||||
<div class="song-name">${song.name}</div>
|
||||
<div class="song-album">${albumName}</div>
|
||||
<div class="song-artists">${artists}</div>
|
||||
`;
|
||||
const checkbox = row.querySelector('.song-checkbox input[type="checkbox"]');
|
||||
checkbox.addEventListener('change', () => toggleSongSelection(song.id, checkbox.checked));
|
||||
list.appendChild(row);
|
||||
@ -547,10 +586,10 @@
|
||||
albumItem.className = 'album-item';
|
||||
|
||||
albumItem.innerHTML = `
|
||||
<img src="${album.blurPicUrl}" class="album-cover" alt="${album.name}" />
|
||||
<div class="album-name" data-id="${album.id}">${album.name}</div>
|
||||
<div class="album-type">${album.type ? album.type : ''}</div>
|
||||
`;
|
||||
<img src="${album.blurPicUrl}" class="album-cover" alt="${album.name}" />
|
||||
<div class="album-name" data-id="${album.id}">${album.name}</div>
|
||||
<div class="album-type">${album.type ? album.type : ''}</div>
|
||||
`;
|
||||
|
||||
const albumNameEl = albumItem.querySelector('.album-name');
|
||||
const albumCoverEl = albumItem.querySelector('.album-cover');
|
||||
@ -578,9 +617,9 @@
|
||||
artistItem.className = 'artist-item';
|
||||
|
||||
artistItem.innerHTML = `
|
||||
<img src="${artist.picUrl}" class="artist-pic" alt="${artist.name}" />
|
||||
<div class="artist-name">${artist.name}</div>
|
||||
`;
|
||||
<img src="${artist.picUrl}" class="artist-pic" alt="${artist.name}" />
|
||||
<div class="artist-name">${artist.name}</div>
|
||||
`;
|
||||
|
||||
artistItem.addEventListener('click', () => {
|
||||
fetchArtistSongs(artist.id);
|
||||
@ -608,9 +647,9 @@
|
||||
const playlistItem = document.createElement('div');
|
||||
playlistItem.className = 'playlist-item';
|
||||
playlistItem.innerHTML = `
|
||||
<img src="${pl.coverImgUrl}" class="playlist-cover" alt="${pl.name}" />
|
||||
<div class="playlist-name" data-id="${pl.id}">${pl.name}</div>
|
||||
`;
|
||||
<img src="${pl.coverImgUrl}" class="playlist-cover" alt="${pl.name}" />
|
||||
<div class="playlist-name" data-id="${pl.id}">${pl.name}</div>
|
||||
`;
|
||||
|
||||
playlistItem.addEventListener('click', () => {
|
||||
fetchPlaylistSongs(pl.id);
|
||||
@ -625,56 +664,80 @@
|
||||
}
|
||||
|
||||
function fetchAlbumSongs(albumId) {
|
||||
fetch(`/api/v1/music/search/album?channel=${selectedPlatformCode}&id=${albumId}`)
|
||||
fetch(`/api/v1/music/search/album?channel=${selectedPlatformCode}&id=${albumId}`, {
|
||||
credentials: 'include' // 确保请求携带cookie
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const songsData = data.data?.songs || [];
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.innerHTML = '';
|
||||
allSongs = [];
|
||||
selectedSongs = [];
|
||||
const result = {
|
||||
songs: songsData,
|
||||
hasMore: false
|
||||
};
|
||||
displaySongs(result, resultDiv);
|
||||
if (data.code === 200) {
|
||||
const songsData = data.data?.songs || [];
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.innerHTML = '';
|
||||
allSongs = [];
|
||||
selectedSongs = [];
|
||||
const result = {
|
||||
songs: songsData,
|
||||
hasMore: false
|
||||
};
|
||||
displaySongs(result, resultDiv);
|
||||
} else if (data.code === -200) {
|
||||
showMessage(data.msg);
|
||||
} else {
|
||||
console.error('未知的响应码:', data.code);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('获取专辑歌曲出错:', err));
|
||||
}
|
||||
|
||||
function fetchArtistSongs(artistId) {
|
||||
fetch(`/api/v1/music/search/artist?channel=${selectedPlatformCode}&id=${artistId}`)
|
||||
fetch(`/api/v1/music/search/artist?channel=${selectedPlatformCode}&id=${artistId}`, {
|
||||
credentials: 'include' // 确保请求携带cookie
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const hotSongs = data.data?.hotSongs || [];
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.innerHTML = '';
|
||||
allSongs = [];
|
||||
selectedSongs = [];
|
||||
const result = {
|
||||
songs: hotSongs,
|
||||
hasMore: false
|
||||
};
|
||||
displaySongs(result, resultDiv);
|
||||
if (data.code === 200) {
|
||||
const hotSongs = data.data?.hotSongs || [];
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.innerHTML = '';
|
||||
allSongs = [];
|
||||
selectedSongs = [];
|
||||
const result = {
|
||||
songs: hotSongs,
|
||||
hasMore: false
|
||||
};
|
||||
displaySongs(result, resultDiv);
|
||||
} else if (data.code === -200) {
|
||||
showMessage(data.msg);
|
||||
} else {
|
||||
console.error('未知的响应码:', data.code);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('获取歌手歌曲出错:', err));
|
||||
}
|
||||
|
||||
function fetchPlaylistSongs(playlistId) {
|
||||
// 获取歌单歌曲数据,返回 data.songs
|
||||
fetch(`/api/v1/music/search/playlist?channel=${selectedPlatformCode}&id=${playlistId}`)
|
||||
fetch(`/api/v1/music/search/playlist?channel=${selectedPlatformCode}&id=${playlistId}`, {
|
||||
credentials: 'include' // 确保请求携带cookie
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const songsData = data.data?.songs || [];
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.innerHTML = '';
|
||||
allSongs = [];
|
||||
selectedSongs = [];
|
||||
const result = {
|
||||
songs: songsData,
|
||||
hasMore: false
|
||||
};
|
||||
displaySongs(result, resultDiv);
|
||||
if (data.code === 200) {
|
||||
const songsData = data.data?.songs || [];
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.innerHTML = '';
|
||||
allSongs = [];
|
||||
selectedSongs = [];
|
||||
const result = {
|
||||
songs: songsData,
|
||||
hasMore: false
|
||||
};
|
||||
displaySongs(result, resultDiv);
|
||||
} else if (data.code === -200) {
|
||||
showMessage(data.msg);
|
||||
} else {
|
||||
console.error('未知的响应码:', data.code);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('获取歌单歌曲出错:', err));
|
||||
}
|
||||
@ -751,9 +814,16 @@
|
||||
|
||||
const promises = selectedSongs.map(id => {
|
||||
const url = `/api/v1/music/download?channel=${channel}&id=${id}`;
|
||||
return fetch(url)
|
||||
return fetch(url, {
|
||||
credentials: 'include' // 确保请求携带cookie
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
// 下载成功,可以处理成功逻辑
|
||||
} else if (data.code === -200) {
|
||||
showMessage(data.msg);
|
||||
}
|
||||
completedCount++;
|
||||
const percent = Math.floor((completedCount / total) * 100);
|
||||
progressBar.value = percent;
|
||||
@ -774,6 +844,58 @@
|
||||
alert("下载完成!文件已存放到服务器指定目录中。");
|
||||
});
|
||||
}
|
||||
|
||||
// 显示用户名
|
||||
function displayUsername() {
|
||||
const username = getCookie('username');
|
||||
const userInfoDiv = document.getElementById('user-info');
|
||||
if (username) {
|
||||
userInfoDiv.textContent = `欢迎, ${username}`;
|
||||
} else {
|
||||
userInfoDiv.textContent = `欢迎, 未登录`;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Cookie的值
|
||||
function getCookie(name) {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift());
|
||||
}
|
||||
|
||||
// 设置Cookie的值
|
||||
function setCookie(name, value, days) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
let expires = "expires=" + d.toUTCString();
|
||||
document.cookie = name + "=" + encodeURIComponent(value) + "; " + expires + "; path=/";
|
||||
}
|
||||
|
||||
// 显示消息并处理重定向
|
||||
function showMessage(msg) {
|
||||
alert(msg);
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login.html'; // 确保登录页面的路径正确
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 全局的fetch包装函数,用于统一处理响应
|
||||
// 可选:如果您希望在所有fetch请求中自动处理code=-200,可以定义一个包装函数
|
||||
// 例如:
|
||||
/*
|
||||
function customFetch(url, options = {}) {
|
||||
options.credentials = 'include'; // 确保携带cookie
|
||||
return fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.code === -200) {
|
||||
showMessage(data.msg);
|
||||
throw new Error(data.msg);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
*/
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user