From c2f59d9f0703e18a56e50b99cd47dc86e2909162 Mon Sep 17 00:00:00 2001 From: N1KO Date: Fri, 13 Dec 2024 18:19:41 +0800 Subject: [PATCH] login and register --- pom.xml | 11 + .../java/top/baogutang/music/annos/Login.java | 17 + .../baogutang/music/aspect/LoginAspect.java | 109 ++++++ .../baogutang/music/config/RedisConfig.java | 70 ++++ .../baogutang/music/constants/CacheKey.java | 21 ++ .../controller/MusicDownloadController.java | 2 + .../controller/MusicSearchController.java | 6 + .../controller/MusicStaticController.java | 8 +- .../music/controller/UserController.java | 37 ++ .../music/dao/entity/UserEntity.java | 33 ++ .../music/dao/mapper/UserMapper.java | 16 + .../req/user/UserRegisterAndLoginReq.java | 22 ++ .../music/domain/res/user/UserLoginRes.java | 36 ++ .../top/baogutang/music/enums/UserLevel.java | 19 ++ .../top/baogutang/music/enums/UserRole.java | 18 + .../exceptions/GlobalExceptionHandler.java | 20 +- .../music/exceptions/LoginException.java | 35 ++ .../baogutang/music/service/IUserService.java | 20 ++ .../music/service/impl/UserServiceImpl.java | 132 +++++++ .../top/baogutang/music/utils/CacheUtil.java | 131 +++++++ .../top/baogutang/music/utils/TokenUtil.java | 85 +++++ .../music/utils/UserThreadLocal.java | 22 ++ src/main/resources/templates/login.html | 240 +++++++++++++ src/main/resources/templates/music.html | 322 ++++++++++++------ 24 files changed, 1325 insertions(+), 107 deletions(-) create mode 100644 src/main/java/top/baogutang/music/annos/Login.java create mode 100644 src/main/java/top/baogutang/music/aspect/LoginAspect.java create mode 100644 src/main/java/top/baogutang/music/config/RedisConfig.java create mode 100644 src/main/java/top/baogutang/music/constants/CacheKey.java create mode 100644 src/main/java/top/baogutang/music/controller/UserController.java create mode 100644 src/main/java/top/baogutang/music/dao/entity/UserEntity.java create mode 100644 src/main/java/top/baogutang/music/dao/mapper/UserMapper.java create mode 100644 src/main/java/top/baogutang/music/domain/req/user/UserRegisterAndLoginReq.java create mode 100644 src/main/java/top/baogutang/music/domain/res/user/UserLoginRes.java create mode 100644 src/main/java/top/baogutang/music/enums/UserLevel.java create mode 100644 src/main/java/top/baogutang/music/enums/UserRole.java create mode 100644 src/main/java/top/baogutang/music/exceptions/LoginException.java create mode 100644 src/main/java/top/baogutang/music/service/IUserService.java create mode 100644 src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java create mode 100644 src/main/java/top/baogutang/music/utils/CacheUtil.java create mode 100644 src/main/java/top/baogutang/music/utils/TokenUtil.java create mode 100644 src/main/java/top/baogutang/music/utils/UserThreadLocal.java create mode 100644 src/main/resources/templates/login.html diff --git a/pom.xml b/pom.xml index c1d5207..d063eb7 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,11 @@ 3.5.3.1 + + org.springframework.boot + spring-boot-starter-data-redis + + org.apache.commons commons-lang3 @@ -71,6 +76,12 @@ 3.0.1 + + com.auth0 + java-jwt + 3.8.2 + + diff --git a/src/main/java/top/baogutang/music/annos/Login.java b/src/main/java/top/baogutang/music/annos/Login.java new file mode 100644 index 0000000..c0aa095 --- /dev/null +++ b/src/main/java/top/baogutang/music/annos/Login.java @@ -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; +} diff --git a/src/main/java/top/baogutang/music/aspect/LoginAspect.java b/src/main/java/top/baogutang/music/aspect/LoginAspect.java new file mode 100644 index 0000000..15c91d7 --- /dev/null +++ b/src/main/java/top/baogutang/music/aspect/LoginAspect.java @@ -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 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); + } +} diff --git a/src/main/java/top/baogutang/music/config/RedisConfig.java b/src/main/java/top/baogutang/music/config/RedisConfig.java new file mode 100644 index 0000000..00574fa --- /dev/null +++ b/src/main/java/top/baogutang/music/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { + // 为了开发方便,直接使用 + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + + // Json 配置序列化 + // 使用 jackson 解析任意的对象 + Jackson2JsonRedisSerializer 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; + } + + +} diff --git a/src/main/java/top/baogutang/music/constants/CacheKey.java b/src/main/java/top/baogutang/music/constants/CacheKey.java new file mode 100644 index 0000000..bb0365a --- /dev/null +++ b/src/main/java/top/baogutang/music/constants/CacheKey.java @@ -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:"; + + +} diff --git a/src/main/java/top/baogutang/music/controller/MusicDownloadController.java b/src/main/java/top/baogutang/music/controller/MusicDownloadController.java index cc08b9e..64e5a8b 100644 --- a/src/main/java/top/baogutang/music/controller/MusicDownloadController.java +++ b/src/main/java/top/baogutang/music/controller/MusicDownloadController.java @@ -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 download(@RequestParam(name = "channel") Integer channel, @RequestParam(name = "id") String id) { MusicDownloadRes res = musicService.getMusicService(channel).download(id); diff --git a/src/main/java/top/baogutang/music/controller/MusicSearchController.java b/src/main/java/top/baogutang/music/controller/MusicSearchController.java index 2d7d306..14559ab 100644 --- a/src/main/java/top/baogutang/music/controller/MusicSearchController.java +++ b/src/main/java/top/baogutang/music/controller/MusicSearchController.java @@ -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 search(MusicSearchReq req) { MusicSearchRes res = musicService.getMusicService(req.getChannel()).search(req); return Results.ok(res); } @GetMapping("/playlist") + @Login public Results 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 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 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 detail(@RequestParam(name = "channel") Integer channel, @RequestParam(name = "id") String id) { MusicDetailRes res = musicService.getMusicService(channel).detail(id); diff --git a/src/main/java/top/baogutang/music/controller/MusicStaticController.java b/src/main/java/top/baogutang/music/controller/MusicStaticController.java index 493f7ea..86a126b 100644 --- a/src/main/java/top/baogutang/music/controller/MusicStaticController.java +++ b/src/main/java/top/baogutang/music/controller/MusicStaticController.java @@ -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"; + } + } diff --git a/src/main/java/top/baogutang/music/controller/UserController.java b/src/main/java/top/baogutang/music/controller/UserController.java new file mode 100644 index 0000000..552df0e --- /dev/null +++ b/src/main/java/top/baogutang/music/controller/UserController.java @@ -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 register(@RequestBody UserRegisterAndLoginReq req) { + userService.register(req); + return Results.ok(); + } + + @PostMapping("/login") + public Results login(@RequestBody UserRegisterAndLoginReq req) { + return Results.ok(userService.login(req)); + } +} diff --git a/src/main/java/top/baogutang/music/dao/entity/UserEntity.java b/src/main/java/top/baogutang/music/dao/entity/UserEntity.java new file mode 100644 index 0000000..d7d5c56 --- /dev/null +++ b/src/main/java/top/baogutang/music/dao/entity/UserEntity.java @@ -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; +} diff --git a/src/main/java/top/baogutang/music/dao/mapper/UserMapper.java b/src/main/java/top/baogutang/music/dao/mapper/UserMapper.java new file mode 100644 index 0000000..1b3b3c9 --- /dev/null +++ b/src/main/java/top/baogutang/music/dao/mapper/UserMapper.java @@ -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 { +} diff --git a/src/main/java/top/baogutang/music/domain/req/user/UserRegisterAndLoginReq.java b/src/main/java/top/baogutang/music/domain/req/user/UserRegisterAndLoginReq.java new file mode 100644 index 0000000..301c7ae --- /dev/null +++ b/src/main/java/top/baogutang/music/domain/req/user/UserRegisterAndLoginReq.java @@ -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; +} diff --git a/src/main/java/top/baogutang/music/domain/res/user/UserLoginRes.java b/src/main/java/top/baogutang/music/domain/res/user/UserLoginRes.java new file mode 100644 index 0000000..f015890 --- /dev/null +++ b/src/main/java/top/baogutang/music/domain/res/user/UserLoginRes.java @@ -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; +} diff --git a/src/main/java/top/baogutang/music/enums/UserLevel.java b/src/main/java/top/baogutang/music/enums/UserLevel.java new file mode 100644 index 0000000..3ddb340 --- /dev/null +++ b/src/main/java/top/baogutang/music/enums/UserLevel.java @@ -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, + + ; +} diff --git a/src/main/java/top/baogutang/music/enums/UserRole.java b/src/main/java/top/baogutang/music/enums/UserRole.java new file mode 100644 index 0000000..e014d3c --- /dev/null +++ b/src/main/java/top/baogutang/music/enums/UserRole.java @@ -0,0 +1,18 @@ +package top.baogutang.music.enums; + +/** + * + * @description: + * + * @author: N1KO + * @date: 2024/12/13 : 16:43 + */ +public enum UserRole { + + + NORMAL, + + ADMIN, + + ; +} diff --git a/src/main/java/top/baogutang/music/exceptions/GlobalExceptionHandler.java b/src/main/java/top/baogutang/music/exceptions/GlobalExceptionHandler.java index 3990511..fd7c3bb 100644 --- a/src/main/java/top/baogutang/music/exceptions/GlobalExceptionHandler.java +++ b/src/main/java/top/baogutang/music/exceptions/GlobalExceptionHandler.java @@ -29,6 +29,20 @@ public class GlobalExceptionHandler { // } + @ExceptionHandler({BusinessException.class}) + public Results businessException(BusinessException e) { + log.error("请求发生错误,code:{},message:{}", e.getCode(), e.getMessage()); + return Results.failed(e.getCode(), e.getMessage()); + } + + @ExceptionHandler({LoginException.class}) + public Results loginException(LoginException e) { + log.error("请求发生错误,code:{},message:{}", e.getCode(), e.getMessage()); + return Results.failed(e.getCode(), e.getMessage()); + } + + + @ExceptionHandler({Throwable.class}) public Results 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 businessException(BusinessException e) { - log.error("请求发生错误,code:{},message:{}", e.getCode(), e.getMessage()); - return Results.failed(e.getCode(), e.getMessage()); - } - @ExceptionHandler({MethodArgumentNotValidException.class}) public Results parameterExceptionHandler(MethodArgumentNotValidException e) { BindingResult exceptions = e.getBindingResult(); diff --git a/src/main/java/top/baogutang/music/exceptions/LoginException.java b/src/main/java/top/baogutang/music/exceptions/LoginException.java new file mode 100644 index 0000000..2e62b67 --- /dev/null +++ b/src/main/java/top/baogutang/music/exceptions/LoginException.java @@ -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; + } +} diff --git a/src/main/java/top/baogutang/music/service/IUserService.java b/src/main/java/top/baogutang/music/service/IUserService.java new file mode 100644 index 0000000..97a89a2 --- /dev/null +++ b/src/main/java/top/baogutang/music/service/IUserService.java @@ -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 { + + void register(UserRegisterAndLoginReq req); + + UserLoginRes login(UserRegisterAndLoginReq req); +} diff --git a/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java b/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..5a23db9 --- /dev/null +++ b/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java @@ -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 implements IUserService { + + @Resource + private RedisTemplate 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); + } +} diff --git a/src/main/java/top/baogutang/music/utils/CacheUtil.java b/src/main/java/top/baogutang/music/utils/CacheUtil.java new file mode 100644 index 0000000..28f7b1a --- /dev/null +++ b/src/main/java/top/baogutang/music/utils/CacheUtil.java @@ -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 + */ + public static T lockAndSupply(String lockKey, + Long timeout, + TimeUnit timeUnit, + RedisTemplate redisTemplate, + Supplier 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 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 + */ + @SuppressWarnings("unchecked") + public static T cacheOrSupply(String cacheKey, + Long timeout, + TimeUnit timeUnit, + RedisTemplate redisTemplate, + Supplier 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; + } +} diff --git a/src/main/java/top/baogutang/music/utils/TokenUtil.java b/src/main/java/top/baogutang/music/utils/TokenUtil.java new file mode 100644 index 0000000..a93d1dd --- /dev/null +++ b/src/main/java/top/baogutang/music/utils/TokenUtil.java @@ -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 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)); + } +} \ No newline at end of file diff --git a/src/main/java/top/baogutang/music/utils/UserThreadLocal.java b/src/main/java/top/baogutang/music/utils/UserThreadLocal.java new file mode 100644 index 0000000..545b054 --- /dev/null +++ b/src/main/java/top/baogutang/music/utils/UserThreadLocal.java @@ -0,0 +1,22 @@ +package top.baogutang.music.utils; + +/** + * @author developer + */ +public class UserThreadLocal { + + private static final ThreadLocal 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(); + } + +} diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..d15686c --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,240 @@ + + + + + + + BAOGUTANG-MUSIC-LOGIN + + + +
+

登录BAOGUTANG-MUSIC

+ +
+ + +
+
+ + +
+ + + + +
没有账号?请点击注册按钮进行注册。
+
+ + + + diff --git a/src/main/resources/templates/music.html b/src/main/resources/templates/music.html index 55cbbec..745f159 100644 --- a/src/main/resources/templates/music.html +++ b/src/main/resources/templates/music.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 @@
+
+ +

BAOGUTANG-MUSIC

@@ -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 = ` ${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 = ` ${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 = ` -
-
-
名字
-
所属专辑
-
作者
-
- `; +
+
+
名字
+
所属专辑
+
作者
+
+ `; 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 = ` -
-
${song.name}
-
${albumName}
-
${artists}
- `; +
+
${song.name}
+
${albumName}
+
${artists}
+ `; 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 = ` - ${album.name} -
${album.name}
-
${album.type ? album.type : ''}
- `; + ${album.name} +
${album.name}
+
${album.type ? album.type : ''}
+ `; const albumNameEl = albumItem.querySelector('.album-name'); const albumCoverEl = albumItem.querySelector('.album-cover'); @@ -578,9 +617,9 @@ artistItem.className = 'artist-item'; artistItem.innerHTML = ` - ${artist.name} -
${artist.name}
- `; + ${artist.name} +
${artist.name}
+ `; artistItem.addEventListener('click', () => { fetchArtistSongs(artist.id); @@ -608,9 +647,9 @@ const playlistItem = document.createElement('div'); playlistItem.className = 'playlist-item'; playlistItem.innerHTML = ` - ${pl.name} -
${pl.name}
- `; + ${pl.name} +
${pl.name}
+ `; 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; + }); + } + */