From 35dfdcb6933c1c4d61034fc2c13e371fb785a6c8 Mon Sep 17 00:00:00 2001
From: cxy <1276771477@qq.com>
Date: Wed, 20 Nov 2024 00:18:09 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E4=BF=AE=EF=BC=8C=E6=94=B9=E4=B8=BA?=
=?UTF-8?q?=E4=BD=BF=E7=94=A8SpringBoot=E6=A1=86=E6=9E=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.idea/compiler.xml | 6 +-
.idea/encodings.xml | 2 +
.idea/misc.xml | 6 +
.idea/webContexts.xml | 10 +
backend/pom.xml | 59 +--
.../main/java/com/backend/Application.java | 11 +
.../java/com/backend/config/WebConfig.java | 25 ++
.../backend/controller/UserController.java | 119 ++++++
.../exception/GlobalExceptionHandler.java | 16 +
.../backend/interceptor/LoginInterceptor.java | 45 ++
.../java/com/backend/mapper/UserMapper.java | 24 ++
.../main/java/com/backend/pojo/Result.java | 28 ++
.../src/main/java/com/backend/pojo/User.java | 26 ++
.../java/com/backend/service/UserService.java | 15 +
.../backend/service/impl/UserServiceImpl.java | 49 +++
.../java/com/backend/utils/AliOssUtil.java | 46 ++
.../main/java/com/backend/utils/JwtUtil.java | 29 ++
.../main/java/com/backend/utils/Md5Util.java | 73 ++++
.../com/backend/utils/ThreadLocalUtil.java | 26 ++
.../example/backend/BackendApplication.java | 20 -
.../src/main/resources/application-dev.yml | 16 +
.../src/main/resources/application.properties | 7 -
backend/src/main/resources/application.yml | 3 +
.../com.backend.mapper/UserMapper.xml | 37 ++
.../src/main/resources/mapper/UserMapper.xml | 37 ++
.../backend/BackendApplicationTests.java} | 4 +-
frontend/package.json | 7 +-
frontend/src/App.vue | 13 +-
frontend/src/View/{Page2.vue => Layout.vue} | 10 +-
frontend/src/View/Login.vue | 225 +++++++---
frontend/src/View/Page1.vue | 9 -
frontend/src/View/Register.vue | 86 ----
frontend/src/View/user/MyInfo.vue | 1 +
frontend/src/View/user/UserAvatar.vue | 98 +++++
frontend/src/View/user/UserInfo.vue | 61 +++
frontend/src/View/user/UserResetPassword.vue | 74 ++++
frontend/src/api/user.js | 36 ++
frontend/src/assets/avatar.jpg | Bin 0 -> 9896 bytes
frontend/src/assets/cover.jpg | Bin 0 -> 9734 bytes
frontend/src/assets/default.png | Bin 0 -> 4097 bytes
frontend/src/assets/login_bg.jpg | Bin 0 -> 94020 bytes
frontend/src/assets/login_title.png | Bin 0 -> 2929 bytes
frontend/src/assets/logo.png | Bin 0 -> 5680 bytes
frontend/src/assets/logo2.png | Bin 0 -> 10983 bytes
frontend/src/assets/main.scss | 20 +
frontend/src/main.js | 2 +-
frontend/src/request.js | 5 -
frontend/src/router.js | 7 +-
frontend/src/router/index.js | 56 +++
frontend/src/service.js | 37 --
frontend/src/{store => stores}/index.js | 0
frontend/src/stores/token.js | 22 +
frontend/src/stores/userInfo.js | 22 +
frontend/src/style.css | 79 ----
frontend/src/utils/request.js | 43 ++
frontend/vite.config.js | 21 +-
frontend/yarn.lock | 395 +++++++++++++++++-
57 files changed, 1707 insertions(+), 361 deletions(-)
create mode 100644 .idea/webContexts.xml
create mode 100644 backend/src/main/java/com/backend/Application.java
create mode 100644 backend/src/main/java/com/backend/config/WebConfig.java
create mode 100644 backend/src/main/java/com/backend/controller/UserController.java
create mode 100644 backend/src/main/java/com/backend/exception/GlobalExceptionHandler.java
create mode 100644 backend/src/main/java/com/backend/interceptor/LoginInterceptor.java
create mode 100644 backend/src/main/java/com/backend/mapper/UserMapper.java
create mode 100644 backend/src/main/java/com/backend/pojo/Result.java
create mode 100644 backend/src/main/java/com/backend/pojo/User.java
create mode 100644 backend/src/main/java/com/backend/service/UserService.java
create mode 100644 backend/src/main/java/com/backend/service/impl/UserServiceImpl.java
create mode 100644 backend/src/main/java/com/backend/utils/AliOssUtil.java
create mode 100644 backend/src/main/java/com/backend/utils/JwtUtil.java
create mode 100644 backend/src/main/java/com/backend/utils/Md5Util.java
create mode 100644 backend/src/main/java/com/backend/utils/ThreadLocalUtil.java
delete mode 100644 backend/src/main/java/org/example/backend/BackendApplication.java
create mode 100644 backend/src/main/resources/application-dev.yml
delete mode 100644 backend/src/main/resources/application.properties
create mode 100644 backend/src/main/resources/application.yml
create mode 100644 backend/src/main/resources/com.backend.mapper/UserMapper.xml
create mode 100644 backend/src/main/resources/mapper/UserMapper.xml
rename backend/src/test/java/{org/example/backend/BackendBackendApplicationTests.java => com/backend/BackendApplicationTests.java} (70%)
rename frontend/src/View/{Page2.vue => Layout.vue} (59%)
delete mode 100644 frontend/src/View/Page1.vue
delete mode 100644 frontend/src/View/Register.vue
create mode 100644 frontend/src/View/user/UserAvatar.vue
create mode 100644 frontend/src/View/user/UserInfo.vue
create mode 100644 frontend/src/View/user/UserResetPassword.vue
create mode 100644 frontend/src/api/user.js
create mode 100644 frontend/src/assets/avatar.jpg
create mode 100644 frontend/src/assets/cover.jpg
create mode 100644 frontend/src/assets/default.png
create mode 100644 frontend/src/assets/login_bg.jpg
create mode 100644 frontend/src/assets/login_title.png
create mode 100644 frontend/src/assets/logo.png
create mode 100644 frontend/src/assets/logo2.png
create mode 100644 frontend/src/assets/main.scss
delete mode 100644 frontend/src/request.js
create mode 100644 frontend/src/router/index.js
delete mode 100644 frontend/src/service.js
rename frontend/src/{store => stores}/index.js (100%)
create mode 100644 frontend/src/stores/token.js
create mode 100644 frontend/src/stores/userInfo.js
create mode 100644 frontend/src/utils/request.js
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 66bc19f..65d1d7f 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -10,10 +10,14 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
index dfc8d74..6c28734 100644
--- a/.idea/encodings.xml
+++ b/.idea/encodings.xml
@@ -2,5 +2,7 @@
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 5ac9743..7f69666 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -5,8 +5,14 @@
+
+
+
+
+
diff --git a/.idea/webContexts.xml b/.idea/webContexts.xml
new file mode 100644
index 0000000..209b90b
--- /dev/null
+++ b/.idea/webContexts.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/pom.xml b/backend/pom.xml
index fe28dcb..e5fa914 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -5,43 +5,27 @@
org.springframework.boot
spring-boot-starter-parent
- 3.3.5
-
+ 3.1.5
+
- org.example
+ com
backend
0.0.1-SNAPSHOT
backend
backend
-
-
-
-
-
-
-
-
-
-
-
-
-
17
-
- org.springframework.boot
- spring-boot-starter-data-jdbc
-
org.springframework.boot
spring-boot-starter-web
+
- javax.persistence
- javax.persistence-api
- 2.2
+ org.mybatis.spring.boot
+ mybatis-spring-boot-starter
+ 3.0.0
@@ -49,20 +33,47 @@
mysql-connector-j
runtime
+
org.projectlombok
lombok
true
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
+
org.springframework.boot
spring-boot-starter-test
test
+
+
+ com.aliyun.oss
+ aliyun-sdk-oss
+ 3.17.0
+
+
org.springframework.boot
- spring-boot-starter-data-jpa
+ spring-boot-starter-data-redis
+
+
+ com.github.pagehelper
+ pagehelper-spring-boot-starter
+ 1.4.6
+
+
diff --git a/backend/src/main/java/com/backend/Application.java b/backend/src/main/java/com/backend/Application.java
new file mode 100644
index 0000000..5386c8c
--- /dev/null
+++ b/backend/src/main/java/com/backend/Application.java
@@ -0,0 +1,11 @@
+package com.backend;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/backend/src/main/java/com/backend/config/WebConfig.java b/backend/src/main/java/com/backend/config/WebConfig.java
new file mode 100644
index 0000000..f5a791f
--- /dev/null
+++ b/backend/src/main/java/com/backend/config/WebConfig.java
@@ -0,0 +1,25 @@
+package com.backend.config;
+
+import com.backend.interceptor.LoginInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+ private static final List BYPASS_PATHS = List.of(
+ "/user/login",
+ "/user/register");
+
+ @Autowired
+ private LoginInterceptor loginInterceptor;
+
+ @Override
+ public void addInterceptors(final InterceptorRegistry registry) {
+ // For login and register, not intercept.
+ registry.addInterceptor(loginInterceptor).excludePathPatterns(BYPASS_PATHS);
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/backend/controller/UserController.java b/backend/src/main/java/com/backend/controller/UserController.java
new file mode 100644
index 0000000..5f0835d
--- /dev/null
+++ b/backend/src/main/java/com/backend/controller/UserController.java
@@ -0,0 +1,119 @@
+package com.backend.controller;
+
+import com.backend.pojo.Result;
+import com.backend.pojo.User;
+import com.backend.service.UserService;
+import com.backend.utils.JwtUtil;
+import com.backend.utils.Md5Util;
+import com.backend.utils.ThreadLocalUtil;
+import jakarta.validation.constraints.Pattern;
+import org.hibernate.validator.constraints.URL;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.Duration;
+import java.util.Map;
+
+import static com.backend.interceptor.LoginInterceptor.AUTHORIZATION_HEADER;
+
+@RestController
+@RequestMapping("/user")
+@Validated
+public class UserController {
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private StringRedisTemplate stringRedisTemplate;
+
+ @PostMapping("/register")
+ public Result register(@Pattern(regexp = "^\\S{5,16}$") final String username, @Pattern(regexp = "^\\S{5,16}$") final String password) {
+ final User existingUser = userService.findByUsername(username);
+ if (existingUser == null) {
+ userService.register(username, password);
+ return Result.success();
+ } else {
+ return Result.error("用户名已被占用");
+ }
+ }
+
+ @PostMapping("/login")
+ public Result login(@Pattern(regexp = "^\\S{5,16}$") final String username, @Pattern(regexp = "^\\S{5,16}$") final String password) {
+ final User existingUser = userService.findByUsername(username);
+ if (existingUser == null) {
+ return Result.error("用户不存在");
+ }
+ if (Md5Util.getMD5String(password).equals(existingUser.getPassword())) {
+ final Map map = Map.of("id", existingUser.getId(),
+ "username", existingUser.getUsername());
+ final String token = JwtUtil.genToken(map);
+ // Save token to redis
+ final ValueOperations operations = stringRedisTemplate.opsForValue();
+ // If key is token, and when we validate, and the token doesn't exist, then it has expired.
+ operations.set(token, token, Duration.ofHours(12));
+ // Return JWT token
+ return Result.success(token);
+ } else {
+ return Result.error("密码错误");
+ }
+ }
+
+ @GetMapping("/userInfo")
+ public Result userInfo() {
+
+ final Map map = ThreadLocalUtil.get();
+ final String username = (String) map.get("username");
+ final User user = userService.findByUsername(username);
+ return Result.success(user);
+ }
+
+ @PutMapping("/update")
+ public Result update(@RequestBody @Validated final User user) {
+ final Map map = ThreadLocalUtil.get();
+ final Integer id = (Integer) map.get("id");
+ if (user.getId().equals(id)) {
+ userService.update(user);
+ return Result.success();
+ } else {
+ return Result.error("非本人id");
+ }
+ }
+
+ @PatchMapping("/updateAvatar")
+ public Result updateAvatar(@RequestParam @URL final String avatarUrl) {
+ userService.updateAvatar(avatarUrl);
+ return Result.success();
+ }
+
+ @PatchMapping("/updatePwd")
+ public Result updatePwd(@RequestBody Map params, @RequestHeader(AUTHORIZATION_HEADER) final String token) {
+ final String oldPwd = params.get("old_pwd");
+ final String newPwd = params.get("new_pwd");
+ final String rePwd = params.get("re_pwd");
+ if (!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd)) {
+ return Result.error("缺少必要的参数");
+ }
+
+ final Map map = ThreadLocalUtil.get();
+ final String username = (String) map.get("username");
+ final User user = userService.findByUsername(username);
+ if (!rePwd.equals(newPwd)) {
+ return Result.error("两次结果不一样");
+ }
+ if (!user.getPassword().equals(Md5Util.getMD5String(oldPwd))) {
+ return Result.error("密码填写不正确");
+ }
+
+ userService.updatePwd(newPwd);
+ // Delete token in redis.
+ final ValueOperations operations = stringRedisTemplate.opsForValue();
+ operations.getOperations().delete(token);
+
+ return Result.success();
+ }
+ }
diff --git a/backend/src/main/java/com/backend/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/backend/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..9d802a9
--- /dev/null
+++ b/backend/src/main/java/com/backend/exception/GlobalExceptionHandler.java
@@ -0,0 +1,16 @@
+package com.backend.exception;
+
+import com.backend.pojo.Result;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler({Exception.class})
+ public Result handleException(final Exception exception) {
+ exception.printStackTrace();
+ return Result.error(StringUtils.hasLength(exception.getMessage()) ? exception.getMessage() : "操作失败");
+ }
+}
diff --git a/backend/src/main/java/com/backend/interceptor/LoginInterceptor.java b/backend/src/main/java/com/backend/interceptor/LoginInterceptor.java
new file mode 100644
index 0000000..c980a3d
--- /dev/null
+++ b/backend/src/main/java/com/backend/interceptor/LoginInterceptor.java
@@ -0,0 +1,45 @@
+package com.backend.interceptor;
+
+import com.backend.utils.JwtUtil;
+import com.backend.utils.ThreadLocalUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.Map;
+
+@Component
+public class LoginInterceptor implements HandlerInterceptor {
+ public static final String AUTHORIZATION_HEADER = "Authorization";
+
+ @Autowired
+ private StringRedisTemplate stringRedisTemplate;
+
+ @Override
+ public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) {
+ final String token = request.getHeader(AUTHORIZATION_HEADER);
+ try {
+ // Get same token from redis.
+ final ValueOperations operations = stringRedisTemplate.opsForValue();
+ final String redisToken = operations.get(token);
+ if (redisToken == null) { // Token has expired
+ throw new RuntimeException("过期或未登录");
+ }
+ final Map map = JwtUtil.parseToken(token);
+ ThreadLocalUtil.set(map);
+ return true;
+ } catch (Exception e) {
+ response.setStatus(401);
+ return false;
+ }
+ }
+
+ @Override
+ public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
+ ThreadLocalUtil.remove();
+ }
+}
diff --git a/backend/src/main/java/com/backend/mapper/UserMapper.java b/backend/src/main/java/com/backend/mapper/UserMapper.java
new file mode 100644
index 0000000..9557b45
--- /dev/null
+++ b/backend/src/main/java/com/backend/mapper/UserMapper.java
@@ -0,0 +1,24 @@
+package com.backend.mapper;
+
+import com.backend.pojo.User;
+import org.apache.ibatis.annotations.*;
+
+@Mapper
+public interface UserMapper {
+
+ @Insert("insert into user(username, password, create_time, update_time) " +
+ "values(#{username},#{md5String},now(),now())")
+ void add(final String username, final String md5String);
+
+ @Select("select * from user where username=#{username}")
+ User findByUsername(final String username);
+
+ @Update("update user set nickname=#{nickname}, update_time=now() where id=#{id}")
+ void update(final User user);
+
+ @Update("update user set user_pic=#{url}, update_time=now() where id=#{id}")
+ void updateAvatar(final String url, final Integer id);
+
+ @Update("update user set password=#{md5String}, update_time=now() where id=#{id}")
+ void updatePwd(final String md5String, final Integer id);
+}
diff --git a/backend/src/main/java/com/backend/pojo/Result.java b/backend/src/main/java/com/backend/pojo/Result.java
new file mode 100644
index 0000000..e242b7e
--- /dev/null
+++ b/backend/src/main/java/com/backend/pojo/Result.java
@@ -0,0 +1,28 @@
+package com.backend.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@Data
+public class Result {
+ private Integer code;//业务状态码 0-成功 1-失败
+ private String message;//提示信息
+ private T data;//响应数据
+
+ //快速返回操作成功响应结果(带响应数据)
+ public static Result success(T data) {
+ return new Result<>(0, "操作成功", data);
+ }
+
+ //快速返回操作成功响应结果
+ public static Result success() {
+ return new Result<>(0, "操作成功", null);
+ }
+
+ public static Result error(String message) {
+ return new Result<>(1, message, null);
+ }
+}
diff --git a/backend/src/main/java/com/backend/pojo/User.java b/backend/src/main/java/com/backend/pojo/User.java
new file mode 100644
index 0000000..8ddc6ac
--- /dev/null
+++ b/backend/src/main/java/com/backend/pojo/User.java
@@ -0,0 +1,26 @@
+package com.backend.pojo;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class User {
+ @NotNull
+ private Integer id;//主键ID
+ private String username;//用户名
+ @JsonIgnore // let springmvc ignore password when converting object to json
+ private String password;//密码
+ @NotEmpty
+ @Pattern(regexp = "^\\S{1,10}$")
+ private String nickname;//昵称
+ private String userPic;//用户头像地址
+ private LocalDateTime createTime;//创建时间
+ private LocalDateTime updateTime;//更新时间
+}
diff --git a/backend/src/main/java/com/backend/service/UserService.java b/backend/src/main/java/com/backend/service/UserService.java
new file mode 100644
index 0000000..b0cf244
--- /dev/null
+++ b/backend/src/main/java/com/backend/service/UserService.java
@@ -0,0 +1,15 @@
+package com.backend.service;
+
+import com.backend.pojo.User;
+
+public interface UserService {
+ User findByUsername(final String username);
+
+ void register(final String username, final String password);
+
+ void update(final User user);
+
+ void updateAvatar(final String url);
+
+ void updatePwd(final String newPwd);
+}
diff --git a/backend/src/main/java/com/backend/service/impl/UserServiceImpl.java b/backend/src/main/java/com/backend/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..6de1825
--- /dev/null
+++ b/backend/src/main/java/com/backend/service/impl/UserServiceImpl.java
@@ -0,0 +1,49 @@
+package com.backend.service.impl;
+
+import com.backend.mapper.UserMapper;
+import com.backend.pojo.User;
+import com.backend.service.UserService;
+import com.backend.utils.Md5Util;
+import com.backend.utils.ThreadLocalUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+@Service
+public class UserServiceImpl implements UserService {
+
+ @Autowired
+ private UserMapper userMapper;
+
+ @Override
+ public User findByUsername(final String username) {
+ return userMapper.findByUsername(username);
+ }
+
+ @Override
+ public void register(final String username, final String password) {
+ final String md5String = Md5Util.getMD5String(password);
+ userMapper.add(username, md5String);
+ }
+
+ @Override
+ public void update(final User user) {
+ userMapper.update(user);
+ }
+
+ @Override
+ public void updateAvatar(final String url) {
+ final Map map = ThreadLocalUtil.get();
+ final Integer id = (Integer) map.get("id");
+ userMapper.updateAvatar(url, id);
+ }
+
+ @Override
+ public void updatePwd(final String newPwd) {
+ final String md5String = Md5Util.getMD5String(newPwd);
+ final Map map = ThreadLocalUtil.get();
+ final Integer id = (Integer) map.get("id");
+ userMapper.updatePwd(md5String, id);
+ }
+}
diff --git a/backend/src/main/java/com/backend/utils/AliOssUtil.java b/backend/src/main/java/com/backend/utils/AliOssUtil.java
new file mode 100644
index 0000000..1b432ef
--- /dev/null
+++ b/backend/src/main/java/com/backend/utils/AliOssUtil.java
@@ -0,0 +1,46 @@
+package com.backend.utils;
+
+import com.aliyun.oss.ClientException;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.OSSException;
+
+import java.io.InputStream;
+
+public class AliOssUtil {
+ private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com";
+ public static final String ACCESS_KEY_ID = System.getenv("ALIYUN_ACCESSKEY_ID");
+ private static final String SECRET_ACCESS_KEY = System.getenv("ALIYUN_ACCESSKEY_SECRET");
+ private static final String BUCKET_NAME = "big-event-1";
+
+ //上传文件,返回文件的公网访问地址
+ public static String uploadFile(String objectName, InputStream inputStream) {
+ // 创建OSSClient实例。
+ OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);
+ //公文访问地址
+ String url = "";
+ try {
+ // 创建存储空间。
+ ossClient.createBucket(BUCKET_NAME);
+ ossClient.putObject(BUCKET_NAME, objectName, inputStream);
+ url = "https://" + BUCKET_NAME + "." + ENDPOINT.substring(ENDPOINT.lastIndexOf("/") + 1) + "/" + objectName;
+ } catch (OSSException oe) {
+ System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ + "but was rejected with an error response for some reason.");
+ System.out.println("Error Message:" + oe.getErrorMessage());
+ System.out.println("Error Code:" + oe.getErrorCode());
+ System.out.println("Request ID:" + oe.getRequestId());
+ System.out.println("Host ID:" + oe.getHostId());
+ } catch (ClientException ce) {
+ System.out.println("Caught an ClientException, which means the client encountered "
+ + "a serious internal problem while trying to communicate with OSS, "
+ + "such as not being able to access the network.");
+ System.out.println("Error Message:" + ce.getMessage());
+ } finally {
+ if (ossClient != null) {
+ ossClient.shutdown();
+ }
+ }
+ return url;
+ }
+}
diff --git a/backend/src/main/java/com/backend/utils/JwtUtil.java b/backend/src/main/java/com/backend/utils/JwtUtil.java
new file mode 100644
index 0000000..49328dc
--- /dev/null
+++ b/backend/src/main/java/com/backend/utils/JwtUtil.java
@@ -0,0 +1,29 @@
+package com.backend.utils;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+
+import java.util.Date;
+import java.util.Map;
+
+public class JwtUtil {
+
+ private static final String KEY = "express-jwt-secret";
+
+ //接收业务数据,生成token并返回
+ public static String genToken(Map claims) {
+ return JWT.create()
+ .withClaim("claims", claims)
+ .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
+ .sign(Algorithm.HMAC256(KEY));
+ }
+
+ //接收token,验证token,并返回业务数据
+ public static Map parseToken(String token) {
+ return JWT.require(Algorithm.HMAC256(KEY))
+ .build()
+ .verify(token)
+ .getClaim("claims")
+ .asMap();
+ }
+}
diff --git a/backend/src/main/java/com/backend/utils/Md5Util.java b/backend/src/main/java/com/backend/utils/Md5Util.java
new file mode 100644
index 0000000..810c227
--- /dev/null
+++ b/backend/src/main/java/com/backend/utils/Md5Util.java
@@ -0,0 +1,73 @@
+package com.backend.utils;
+
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class Md5Util {
+ /**
+ * 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
+ */
+ protected static char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ protected static MessageDigest messagedigest = null;
+
+ static {
+ try {
+ messagedigest = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException nsaex) {
+ System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
+ nsaex.printStackTrace();
+ }
+ }
+
+ /**
+ * 生成字符串的md5校验值
+ *
+ * @param s
+ * @return
+ */
+ public static String getMD5String(String s) {
+ return getMD5String(s.getBytes());
+ }
+
+ /**
+ * 判断字符串的md5校验码是否与一个已知的md5码相匹配
+ *
+ * @param password 要校验的字符串
+ * @param md5PwdStr 已知的md5校验码
+ * @return
+ */
+ public static boolean checkPassword(String password, String md5PwdStr) {
+ String s = getMD5String(password);
+ return s.equals(md5PwdStr);
+ }
+
+
+ public static String getMD5String(byte[] bytes) {
+ messagedigest.update(bytes);
+ return bufferToHex(messagedigest.digest());
+ }
+
+ private static String bufferToHex(byte[] bytes) {
+ return bufferToHex(bytes, 0, bytes.length);
+ }
+
+ private static String bufferToHex(byte[] bytes, int m, int n) {
+ StringBuffer stringbuffer = new StringBuffer(2 * n);
+ int k = m + n;
+ for (int l = m; l < k; l++) {
+ appendHexPair(bytes[l], stringbuffer);
+ }
+ return stringbuffer.toString();
+ }
+
+ private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
+ char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
+ // 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
+ char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
+ stringbuffer.append(c0);
+ stringbuffer.append(c1);
+ }
+
+}
diff --git a/backend/src/main/java/com/backend/utils/ThreadLocalUtil.java b/backend/src/main/java/com/backend/utils/ThreadLocalUtil.java
new file mode 100644
index 0000000..3366629
--- /dev/null
+++ b/backend/src/main/java/com/backend/utils/ThreadLocalUtil.java
@@ -0,0 +1,26 @@
+package com.backend.utils;
+
+/**
+ * ThreadLocal 工具类
+ */
+@SuppressWarnings("all")
+public class ThreadLocalUtil {
+ //提供ThreadLocal对象,
+ private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
+
+ //根据键获取值
+ public static T get() {
+ return (T) THREAD_LOCAL.get();
+ }
+
+ //存储键值对
+ public static void set(Object value) {
+ THREAD_LOCAL.set(value);
+ }
+
+
+ //清除ThreadLocal 防止内存泄漏
+ public static void remove() {
+ THREAD_LOCAL.remove();
+ }
+}
diff --git a/backend/src/main/java/org/example/backend/BackendApplication.java b/backend/src/main/java/org/example/backend/BackendApplication.java
deleted file mode 100644
index 5b6f6d8..0000000
--- a/backend/src/main/java/org/example/backend/BackendApplication.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.example.backend;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@SpringBootApplication
-@RestController
-public class BackendApplication {
-
- @RequestMapping("/")
- public String hello() {
- return "Hello, World!";
- }
- public static void main(String[] args) {
- SpringApplication.run(BackendApplication.class, args);
- }
-}
diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml
new file mode 100644
index 0000000..bb0cf30
--- /dev/null
+++ b/backend/src/main/resources/application-dev.yml
@@ -0,0 +1,16 @@
+spring:
+ datasource:
+ url: jdbc:mysql://localhost:3306/express_management
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ username: root
+ password: 123456
+ data:
+ redis:
+ host: 47.115.225.58
+ port: 6379
+mybatis:
+ configuration:
+ map-underscore-to-camel-case: true
+#mybatis:
+# mapper-locations: classpath:./mapper/*.xml
+# type-aliases-package: com.backend.pojo
\ No newline at end of file
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
deleted file mode 100644
index 2966c80..0000000
--- a/backend/src/main/resources/application.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-spring.application.name=backend
-server.port=8888
-spring.datasource.url=jdbc:mysql://localhost:3306/express_management
-spring.datasource.username=root
-spring.datasource.password=123456
-spring.jpa.hibernate.ddl-auto=update
-spring.jpa.show-sql=true
\ No newline at end of file
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
new file mode 100644
index 0000000..3d7808a
--- /dev/null
+++ b/backend/src/main/resources/application.yml
@@ -0,0 +1,3 @@
+spring:
+ profiles:
+ active: dev
diff --git a/backend/src/main/resources/com.backend.mapper/UserMapper.xml b/backend/src/main/resources/com.backend.mapper/UserMapper.xml
new file mode 100644
index 0000000..63342d3
--- /dev/null
+++ b/backend/src/main/resources/com.backend.mapper/UserMapper.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ INSERT INTO user (username, password, create_time, update_time)
+ VALUES (#{username}, #{md5String}, NOW(), NOW())
+
+
+
+
+
+ UPDATE user
+ SET nickname = #{nickname},
+ update_time = NOW()
+ WHERE id = #{id}
+
+
+
+ UPDATE user
+ SET user_pic = #{url},
+ update_time = NOW()
+ WHERE id = #{id}
+
+
+
+ UPDATE user
+ SET password = #{md5String},
+ update_time = NOW()
+ WHERE id = #{id}
+
+
+
diff --git a/backend/src/main/resources/mapper/UserMapper.xml b/backend/src/main/resources/mapper/UserMapper.xml
new file mode 100644
index 0000000..63342d3
--- /dev/null
+++ b/backend/src/main/resources/mapper/UserMapper.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ INSERT INTO user (username, password, create_time, update_time)
+ VALUES (#{username}, #{md5String}, NOW(), NOW())
+
+
+
+
+
+ UPDATE user
+ SET nickname = #{nickname},
+ update_time = NOW()
+ WHERE id = #{id}
+
+
+
+ UPDATE user
+ SET user_pic = #{url},
+ update_time = NOW()
+ WHERE id = #{id}
+
+
+
+ UPDATE user
+ SET password = #{md5String},
+ update_time = NOW()
+ WHERE id = #{id}
+
+
+
diff --git a/backend/src/test/java/org/example/backend/BackendBackendApplicationTests.java b/backend/src/test/java/com/backend/BackendApplicationTests.java
similarity index 70%
rename from backend/src/test/java/org/example/backend/BackendBackendApplicationTests.java
rename to backend/src/test/java/com/backend/BackendApplicationTests.java
index 92ecd03..d427c41 100644
--- a/backend/src/test/java/org/example/backend/BackendBackendApplicationTests.java
+++ b/backend/src/test/java/com/backend/BackendApplicationTests.java
@@ -1,10 +1,10 @@
-package org.example.backend;
+package com.backend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
-class BackendBackendApplicationTests {
+class BackendApplicationTests {
@Test
void contextLoads() {
diff --git a/frontend/package.json b/frontend/package.json
index 83baf01..bcc2437 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,5 +1,5 @@
{
- "name": "lesson1_vite",
+ "name": "express",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -9,16 +9,21 @@
"preview": "vite preview"
},
"dependencies": {
+ "@element-plus/icons-vue": "^2.3.1",
+ "@vueup/vue-quill": "^1.2.0",
"axios": "1.2.1",
"element-plus": "^2.8.6",
"express": "^4.21.1",
"pinia": "2.0.27",
+ "pinia-persistedstate-plugin": "^0.1.0",
"save": "^2.9.0",
+ "scss": "^0.2.4",
"vue": "^3.4.37",
"vue-router": "4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.2",
+ "sass": "^1.81.0",
"vite": "^5.4.1"
}
}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index d2993ee..0c2d3f3 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -8,16 +8,5 @@
diff --git a/frontend/src/View/Page2.vue b/frontend/src/View/Layout.vue
similarity index 59%
rename from frontend/src/View/Page2.vue
rename to frontend/src/View/Layout.vue
index 9cd9eb6..f79cdd8 100644
--- a/frontend/src/View/Page2.vue
+++ b/frontend/src/View/Layout.vue
@@ -1,9 +1,11 @@
+
+
- 页面2
+
-
+
\ No newline at end of file
diff --git a/frontend/src/View/Login.vue b/frontend/src/View/Login.vue
index 7128e05..9e87297 100644
--- a/frontend/src/View/Login.vue
+++ b/frontend/src/View/Login.vue
@@ -1,72 +1,177 @@
-
-
-
-
+import {Lock, User} from "@element-plus/icons-vue"
+import {ref} from "vue";
+import {userLoginService, userRegisterService} from "@/api/user";
+import {ElMessage} from "element-plus";
+import {useRouter} from "vue-router";
+import {useTokenStore} from "@/stores/token";
+
+// Register page and Login page use the same view.
+// By default, show login.
+const isRegister = ref(false);
+const registerData = ref({
+ username: '',
+ password: '',
+ rePassword: ''
+});
-
+
\ No newline at end of file
diff --git a/frontend/src/View/Page1.vue b/frontend/src/View/Page1.vue
deleted file mode 100644
index 9f19e8f..0000000
--- a/frontend/src/View/Page1.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-
- 页面1
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/src/View/Register.vue b/frontend/src/View/Register.vue
deleted file mode 100644
index 1658b45..0000000
--- a/frontend/src/View/Register.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
-
-
-
diff --git a/frontend/src/View/user/MyInfo.vue b/frontend/src/View/user/MyInfo.vue
index 305323d..b6ad80a 100644
--- a/frontend/src/View/user/MyInfo.vue
+++ b/frontend/src/View/user/MyInfo.vue
@@ -1,3 +1,4 @@
+
diff --git a/frontend/src/View/user/UserAvatar.vue b/frontend/src/View/user/UserAvatar.vue
new file mode 100644
index 0000000..2a54de4
--- /dev/null
+++ b/frontend/src/View/user/UserAvatar.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择图片
+
+
+ 上传头像
+
+
+
+
+
+
+
diff --git a/frontend/src/View/user/UserInfo.vue b/frontend/src/View/user/UserInfo.vue
new file mode 100644
index 0000000..a3be7f9
--- /dev/null
+++ b/frontend/src/View/user/UserInfo.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交修改
+
+
+
+
+
+
+
+
diff --git a/frontend/src/View/user/UserResetPassword.vue b/frontend/src/View/user/UserResetPassword.vue
new file mode 100644
index 0000000..b069cf4
--- /dev/null
+++ b/frontend/src/View/user/UserResetPassword.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交修改
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js
new file mode 100644
index 0000000..06e4438
--- /dev/null
+++ b/frontend/src/api/user.js
@@ -0,0 +1,36 @@
+import request from "@/utils/request";
+
+export const userRegisterService = (registerData) => {
+ const params = new URLSearchParams();
+ for (const key in registerData) {
+ params.append(key, registerData[key])
+ }
+ return request.post('/user/register', params)
+}
+
+export const userLoginService = (loginData) => {
+ const params = new URLSearchParams();
+ for (const key in loginData) {
+ params.append(key, loginData[key])
+ }
+ return request.post('/user/login', params)
+}
+
+export const userInfoService = () => {
+ return request.get('/user/userInfo')
+}
+
+export const userInfoUpdateService = (userInfoData) => {
+ return request.put('/user/update', userInfoData)
+}
+
+export const userAvatarUpdateService = (avatarUrl) => {
+ const urlSearchParams = new URLSearchParams();
+ urlSearchParams.append('avatarUrl', avatarUrl)
+ return request.patch('/user/updateAvatar', urlSearchParams)
+}
+
+export const userPwdUpdateService = (pwdData) => {
+ return request.patch('/user/updatePwd', pwdData)
+}
+
diff --git a/frontend/src/assets/avatar.jpg b/frontend/src/assets/avatar.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..984a066164ad0eb3f298389a7a3544ea8e53cb16
GIT binary patch
literal 9896
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
fb;_!M@y?U%=eK9g%$hZ8X7<|8-g~|G4&c)d
zLAwKhv9S!m0Q{uA1%Oj85Qzx|KmZMu?FImQ?;x_Cc)Xvwf&w-`-o?$=6(x`K^-&0M
z@l$}yA5{R3YlZl^ATcOB)D`7{#%jVoJ$eFzqTMuMb_lbhW`4RTPqa}O4rLW~+8P;#
zL8`gIv`#>ehp316`1zplF3=DkZ)|{ih$ig2adj%*XDh&<-$n2kP1wo(S)ul3XP~;i
zI205iuOx>&3Wr0LmF3~b5X#E%BT&Voa7Bfq)St2({FpjISzS>D`X|Atv*Fy_)h+c5
z{+x??rwRLWQNh8%^1({-zBmsBxSE>Uz70i1IjV$QKqwaP5+a8U5c|u59x4EdL;K;;
zzF6qKMHg4!K)fc5>ggX#@bUX$_HSeLi=dg=zZdoK*$=>XvjKQZ)K7o^)^LDzs2@tf
z5*6SZh(n^N;$r*Ce(Jh7lndS$XYK3j{Z|yvc>3af13Z2Gpt@Gd(8G3UtebCefb@5H
zGc$E#YyjQ`i$odgX~L)$+3=Ts6sz$
zavyCBQVWtw{c=WUpwNBv-ylw;9}W)ypfgk*B{ymcQ}Hz__72AT?emwZnA;VzkHPyz
z&yAxxNX2~n*y9g;
z_4NzIp*=kD(8EY+D7A7`p+=}+ZxkLccZS-HTySpGUg6{Cf(@m1vF~f%*8&dg#}-Nr
zvNBvvSy@g|o*Moi^lt}$Nd0@Tzqfx#e75+$XCU6rpS+*CKY70S0HC@~jZN}Tp6eX|
zsJ#vVd?P=3;&%an{Tcw&^!&0O-u-;}rIVpiNcsJM{-FP>zz@lP2EWWFzdzrP*g^GB
z?k<7ec<6pmk-py4?gI^=J`7M$x&J+h|Hq2Ixb=%4N2omxg+oyr71WlRWoWDib-S@{
z=qlzwyvc$6d8yM+Q-zI*uT&MOaME;4e$dZfEaKX
zkOLF}6+i>f2K0ebfH`0dH~{AWB;X1705~8RhyX4FQ9vA!45R~@Kps#ClmH~43aAAh
z1J8gKpaXaXP=HZj0+
znrQlI-q9@6e4}Nh6{3};Ri`zkwWsx<4W^Bv&7du!CDS(3QfQ}XH|Xf-c