看完鱼皮大佬的用户中心项目后来自己动手写一下试试:
项目介绍
一个简单的 “用户管理系统” ,实现了用户注册、登录、查询等基础功能
技术选型
前端
-
主要运用阿里 Ant Design 生态:
- HTML + CSS + JavaScript 三件套
- React 开发框架
- Ant Design Pro 项目模板
- Ant Design 端组件库
- Umi Request 请求库
-
chatgpt(主要生成简单的用户页面)
后端:
- Java 编程语言
- Spring + SpringMVC + SpringBoot 框架
- MyBatis + MyBatis Plus 数据访问框架
- MySQL 数据库
实现步骤
一 .后端项目初始化
-
打开idea新建一个springboot项目
-
添加依赖
-
在Developer Tools中勾选Lombok
-
在Web中勾选Spring web
-
在SQL中勾选 MyBatis Framework,MySQL Driver
-
之后点击创建
-
-
创建完成之后为了兼容性把springboot的版本降低到2.x,重新加载maven信息
pom
在pom中导入相应依赖
<!--文件上传所需依赖-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!--工具类(方便操作)-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
配置文件
将resources中的application.properties改为application.yml
spring:
application:
name: User-Manage-back
datasource: #数据库连接
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/user
username: root
password: 123456
servlet:
multipart:
max-request-size: 15MB
max-file-size: 10MB #上传图片的大小
enabled: true
mybatis-plus:
configuration:
map-underscore-to-camel-case: true #数据库我使用的是下划线,程序中用的是驼峰,实现数据库下划线字段与实体中的驼峰属性映射
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
server:
port: 8080
servlet:
context-path: /api
tomcat:
max-swallow-size: 100MB #修改tomcat的吞吐量
file:
uploadFolder: D:/project/user-manage-system/User-Manage-back/src/main/resources/static/images/
staticAccessPath: /file/**
request: http://localhost:8000/api/file/
用户表设计
create table user
(
id bigint auto_increment
primary key,
username varchar(256) null comment '用户昵称',
user_account varchar(256) null comment '账号',
avatar_url varchar(1024) null comment '用户头像',
gender tinyint null comment '性别',
user_password varchar(512) not null comment '密码',
phone varchar(128) null comment '电话',
email varchar(512) null comment '邮箱',
user_status int default 0 not null comment '用户状态 0-正常',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
is_delete tinyint default 0 not null comment '是否删除0:否 1:s',
role int default 0 not null comment '0 : 普通用户 1:管理员',
invitation_code varchar(256) null comment '邀请码'
)
comment '用户';
用MybatisX插件自动生成代码
-
在File->Settings-Plugins中下载
-
在idea自带的数据库中连接
-
连接好之后点开创建的数据表
- 之后就生成好了
包名
用了mybatis-plus中的逻辑删除功能
要在User类中isDelete字段上添加@TableLogic注解
二.启动类
加这个注解@MapperScan(“com.example.usermanageback.mapper”)
三.功能实现
1.常量定义
在constant包中新建constant类
package com.example.usermanageback.constant;
public class constant {
public final static String SALT="SALT";
public final static String LOGIN_STATUS="LOGIN_STATUS";
public final static int ADMIN_ROLE=1;
}
2.注册功能
1.用户在前端输入账户和密码,效验密码
2.校验用户的账户、密码、校验密码,是否符合要求
3.账户的话不小于4位不大于16
4.密码就不小于8位不大于16
5.账户不能重复
6.账户不包含特殊字符
7.密码和校验密码相同
8.对密码进行加密
9.向数据库插入用户数据
RegisterDTO
以用户注册时提交的数据传输对象,在DTO中新建RegisterDTO
package com.example.usermanageback.pojo.DTO;
import lombok.Data;
import java.io.Serializable;
@Data
public class RegisterDTO implements Serializable {
private static final long serialVersionUID = 366595768940483092L;
String userAccount;//账号
String userPassword;//密码
String checkPassword;//检查密码
}
service
long userRegister(RegisterDTO registerDTO);
impl
@Resource
UserMapper userMapper;
@Override
public long userRegister(RegisterDTO registerDTO) {
String userAccount = registerDTO.getUserAccount();
String userPassword = registerDTO.getUserPassword();
String checkPassword = registerDTO.getCheckPassword();
if (StringUtils.isAnyBlank(userAccount,userPassword,checkPassword))
{
throw new BusinessException(ErrorCode.NULL_ERROR,"不能留空");
}
//账户不小于4位不高于16位
if (userAccount.length()<4||userAccount.length()>16)
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账户不小于4位不高于16位");
}
//密码不小于8位不高于16位
if (userPassword.length()<8||userPassword.length()>16)
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码不小于8位不高于16位");
}
//账户不包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账户不包含特殊字符");
}
//密码和校验密码相同
if (!userPassword.equals(checkPassword))
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"两次密码不一致");
}
//对密码进行加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT+userPassword).getBytes());
//账号不能重复
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("user_account",userAccount);
long count = this.count(queryWrapper);
if (count>=1)
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账号已存在");
}
User user=new User();
user.setUsername(userAccount);
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
Boolean flag=true;
String inviteCode = "a";
while(flag)
{
//邀请码生成
String s = InviteCodeGeneratorUtils.generateUniqueCode();
//邀请码是否重复
QueryWrapper<User> checkInviteCode=new QueryWrapper<>();
checkInviteCode.eq("invitation_code",s);
long invitationCount = this.count(checkInviteCode);
if (invitationCount<1)
{
inviteCode=s;
flag=false;
}
}
user.setInvitationCode(inviteCode);
if (inviteCode==null)
{
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"系统內部异常,请稍后重试");
}
boolean request = this.save(user);
if (!request)
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"添加用户失败");
}
return user.getId();
}
controller
在controller下新建一个userController并打上注解@RestController
@RequestMapping(“/user”)
@Resource
UserService userService;
@PostMapping("/register")
public BaseResponse<Long> UserRegister(@RequestBody RegisterDTO registerDTO)
{
if (registerDTO ==null)
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"参数不正确");
}
long l = userService.userRegister(registerDTO);
return ResultUtils.success(l);
}
邀请码生产
在utils包下建一个InviteCodeGeneratorUtils类
package com.example.usermanageback.utils;
import java.security.SecureRandom;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class InviteCodeGeneratorUtils {
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final int CODE_LENGTH = 8;
private static final Set<String> generatedCodes = new HashSet<>();
private static final SecureRandom random = new SecureRandom();
/**
* Generates a unique invite code.
* @return a unique invite code
*/
public static String generateUniqueCode() {
String code;
do {
code = generateCode();
} while (generatedCodes.contains(code));
generatedCodes.add(code);
return code;
}
/**
* Generates a random invite code.
* @return a random invite code
*/
private static String generateCode() {
StringBuilder code = new StringBuilder(CODE_LENGTH);
for (int i = 0; i < CODE_LENGTH; i++) {
code.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
}
return code.toString();
}
}
3.登录功能
1.校验用户账户和密码是否合法
2.非空
3.账户长度不小于4不大于16
4.密码就不小于8位大于16
5.账户不包含特殊字符
6.校验密码是否输入正确,要和数据库中的密文密码去对比
7.我们要记录用户的登录态(session),将其存到服务器上
(用后端SpringBoot框架封装的服务器tomcat去记录)
cookie
8.返回用户信息(脱敏)
LoginDTO
用户注册时登录的数据传输对象在DTO中新建LoginDTO
package com.example.usermanageback.pojo.DTO;
import lombok.Data;
import java.io.Serializable;
@Data
public class LoginDTO implements Serializable {
private static final long serialVersionUID = 7889413490962134425L;
String userAccount;
String userPassword;
}
UserVO
用户信息脱敏 在VO中新建UserVO
package com.example.usermanageback.pojo.VO;
import lombok.Data;
import java.util.Date;
@Data
public class UserVO {
private Long id;
/**
* 用户昵称
*/
private String username;
/**
* 账号
*/
private String userAccount;
/**
* 用户头像
*/
private String avatarUrl;
/**
* 性别
*/
private Integer gender;
/**
* 电话
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 用户状态 0-正常
*/
private Integer userStatus;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 是否删除0:否 1:s
*/
private Integer isDelete;
/**
* 0 : 普通用户 1:管理员
*/
private Integer role;
/**
* 邀请码
*/
private String invitationCode;
}
service
UserVO userLogin(LoginDTO loginDTO, HttpServletRequest request);
impl
@Override
public UserVO userLogin(LoginDTO loginDTO, HttpServletRequest request) {
String userAccount = loginDTO.getUserAccount();
String userPassword = loginDTO.getUserPassword();
//非空
if(StringUtils.isAnyBlank(userAccount,userPassword))
{
throw new BusinessException(ErrorCode.NULL_ERROR,"数据为空");
}
//账户的话不小于4位不高于16位
if (userAccount.length()<4||userAccount.length()>16)
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账号错误");
}
//密码就不小于8位不高于16位
if (userPassword.length()<8||userPassword.length()>16)
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码错误");
}
//账户不包含特殊字符
String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账号错误");
}
//校验密码是否输入正确,要和数据库中的密文密码去对比
String encryptPassword = DigestUtils.md5DigestAsHex((SALT+userPassword).getBytes());
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("user_account",userAccount);
queryWrapper.eq("user_password", encryptPassword);
User user = userMapper.selectOne(queryWrapper);
UserVO userVO=new UserVO();
BeanUtils.copyProperties(user,userVO);
//我们要记录用户的登录态(session),将其存到服务器上
request.getSession().setAttribute(LOGIN_STATUS,userVO);
//返回用户信息(脱敏)
return userVO;
}
controller
@PostMapping("/login")
public BaseResponse<UserVO> UserLogin(@RequestBody LoginDTO loginDTO, HttpServletRequest request)
{
if (loginDTO==null)
{
throw new BusinessException(ErrorCode.NULL_ERROR,"请求参数为空");
}
if (request==null)
{
throw new BusinessException(ErrorCode.NULL_ERROR,"请求参数为空");
}
UserVO userVO = userService.userLogin(loginDTO,request);
return ResultUtils.success(userVO);
}
4.获取当前用户
service
UserVO getCurrentUser(HttpServletRequest request);
impl
@Override
public UserVO getCurrentUser(HttpServletRequest request) {
Object userObj = request.getSession().getAttribute(LOGIN_STATUS);
UserVO user = (UserVO) userObj;
return user;
}
5.判断是否是管理员
controller
public boolean isAdmin(HttpServletRequest request) {
// 仅管理员可查询
Object userObj = request.getSession().getAttribute(LOGIN_STATUS);
UserVO user = (UserVO) userObj;
return user != null && user.getRole() == ADMIN_ROLE;
}
6.用户更新数据
UpdateDTO
用户更新的数据传输对象在DTO中新建UpdateDTO
@Data
public class UpdateDTO {
private Long id;
private String username;
private String avatarUrl;
private Integer gender;
private String phone;
private String email;
private Integer userStatus;
private Date createTime;
private Date updateTime;
private Integer role;
}
controller
@PostMapping("/update")
public BaseResponse<Boolean> UpdateUser(@RequestBody UpdateDTO updateDTO, HttpServletRequest request)
{
if (request==null)
{
throw new BusinessException(ErrorCode.NULL_ERROR,"数据为空");
}
Long id = updateDTO.getId();
boolean admin = isAdmin(request);
UserVO currentUser = userService.getCurrentUser(request);
Long id1 = currentUser.getId();
if (!admin&&!id.equals(id1))
{
throw new BusinessException(ErrorCode.NO_AUTH,"无权限修改");
}
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("id",id);
User user=new User();
BeanUtils.copyProperties(updateDTO,user);
boolean update = userService.update(user,queryWrapper);
if (!update)
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"保存失败");
}
return ResultUtils.success(true);
}
7. 用户查找
searchDTO
import lombok.Data;
@Data
public class SearchDTO {
private Long id;
private String username;
private String userAccount;
private int pageSize;
private int current;
}
service
List<UserVO> getUserList(SearchDTO searchDTO);
impl
@Override
public List<UserVO> getUserList(SearchDTO searchDTO) {
Long id = searchDTO.getId();
String userAccount = searchDTO.getUserAccount();
String username = searchDTO.getUsername();
int current = searchDTO.getCurrent();
int pageSize = searchDTO.getPageSize();
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
if (StringUtils.isNotBlank(username))
{
queryWrapper.like("username",username);
}
if (id!=null)
{
queryWrapper.eq("id",id);
}
if (StringUtils.isNotBlank(userAccount))
{
queryWrapper.like("user_account",userAccount);
}
Page<User> page=new Page<>(current,pageSize);
int total = userMapper.selectList(queryWrapper).size();
Page<User> userPage=userMapper.selectPage(page,queryWrapper);
//List<User> users = userMapper.selectList(queryWrapper);
List<UserVO> userVOList=new ArrayList<>();
for(User user : userPage.getRecords())
{
System.out.println(user);
UserVO userVO=new UserVO();
BeanUtils.copyProperties(user,userVO);
userVOList.add(userVO);
}
return userVOList;
}
controller
@GetMapping("/search")
public BaseResponse<List<UserVO>> GetUser(SearchDTO searchDTO, HttpServletRequest request)
{
if (request==null)
{
throw new BusinessException(ErrorCode.NULL_ERROR,"请求为空");
}
boolean admin = isAdmin(request);
if (!admin)
{
throw new BusinessException(ErrorCode.NO_AUTH,"无权限");
}
List<UserVO> userByUserName = userService.getUserList(searchDTO);
return ResultUtils.success(userByUserName);
}
分页拦截器
因为用的是mybatis-plus分页所以要添加拦截器
在config中新建MybatisPlusConfig类
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor=new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
8.用户删除
controller
@PostMapping("/remove")
public BaseResponse<Boolean> RemoveUser(@RequestBody Long id)
{
if (id==null)
{
throw new BusinessException(ErrorCode.NULL_ERROR,"参数为空");
}
boolean result = userService.removeById(id);
if (!result)
{
throw new BusinessException(ErrorCode.PARAMS_ERROR,"删除失败");
}
return ResultUtils.success(true);
}
9.用户头像上传功能
controller
在controller包下新建一个uploadController类
@RestController
public class uploadController {
@Value("${file.request}")//通过配置项来读取
private String prefix;
@Resource
UserService userService;
@PostMapping("/upload")
public BaseResponse<String> upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
if (file.isEmpty()) {
throw new BusinessException(ErrorCode.NULL_ERROR,"图片为空");
}
if (request==null)
{
throw new BusinessException(ErrorCode.NULL_ERROR,"数据为空");
}
UserVO userVO = userService.getCurrentUser(request);
if (userVO==null){
throw new BusinessException(ErrorCode.NULL_ERROR,"用户不存在");
}
Long id = userVO.getId();
String originalFilename = file.getOriginalFilename();//获取文件名称
String fileNamePrefix = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());//生成文件名称前缀
assert originalFilename != null;
String fileNameSuffix = "." + originalFilename.split("\\.")[1];//生成文件名称后缀
String fileName = fileNamePrefix + fileNameSuffix;//文件名
ApplicationHome applicationHome = new ApplicationHome(this.getClass());
String pre = applicationHome.getDir().getParentFile().getParentFile().getAbsolutePath() + "\\src\\main\\resources\\static\\images\\";//获取路径
String path = pre+fileName;
try {
file.transferTo(new File(path));//保存文件
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("id",id);
User user =new User();
String url=prefix+fileName;
user.setAvatarUrl(url);
boolean update = userService.update(user, queryWrapper);
if (!update)
{
throw new BusinessException(ErrorCode.OPEARTION_ERROR,"上传失败");
}
} catch (IOException e) {
e.printStackTrace();
}
return ResultUtils.success("上传成功");
}
}
config配置
因为浏览器不能直接访问本地文件因此要做映射
在config包下新建WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 通过读取配置项获取的文件上传路径
@Value("${file.uploadFolder}")
private String basePath;
@Value("${file.staticAccessPath}")
private String visualPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/*
* 资源映射路径
* addResourceHandler:访问映射路径
* addResourceLocations:资源绝对路径
*/
registry.addResourceHandler(visualPath).addResourceLocations("file:" + basePath);
}
}
通用返回类
BaseResponse
在common包下新建BaseResponse类
import lombok.Data;
@Data
public class BaseResponse<T>{
private int code;
private T data;
private String message;
private String description;
public BaseResponse(int code, T data, String message, String description) {
this.code = code;
this.data = data;
this.message = message;
this.description = description;
}
public BaseResponse(int code, T data, String message)
{
this(code,data,message,"");
}
public BaseResponse(int code, T data)
{
this(code,data,"","");
}
public <T> BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(),null,errorCode.getMessage(),errorCode.getDescription());
}
}
ErrorCode
自定义错误码
public enum ErrorCode {
PARAMS_ERROR(40000,"参数错误",""),
NULL_ERROR(40010,"数据为空",""),
NO_LOGIN(40011,"未登录",""),
NO_AUTH(40100,"无权限",""),
OPEARTION_ERROR(45000,"操作失败",""),
SYSTEM_ERROR(50000,"系统內部异常",""),
;
private int code;
private String message;
private String description;
ErrorCode(int code, String message, String description) {
this.code = code;
this.message = message;
this.description = description;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
}
ResultUtils
public class ResultUtils {
//成功
public static <T> BaseResponse<T> success(T data){
return new BaseResponse<>(0,data,"success");
}
//失败
public static BaseResponse error(int code,String message,String description)
{
return new BaseResponse(code,null, message, description);
}
public static BaseResponse error(ErrorCode errorCode)
{
return new BaseResponse(errorCode.getCode(),null, errorCode.getMessage(), errorCode.getDescription());
}
public static BaseResponse error(ErrorCode errorCode,String description)
{
return new BaseResponse(errorCode.getCode(),null, errorCode.getMessage(), description);
}
public static BaseResponse error(ErrorCode errorCode,String message,String description)
{
return new BaseResponse(errorCode.getCode(),null, message, description);
}
}
异常处理
BusinessException
public class BusinessException extends RuntimeException{
private int code;
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
private String description;
public BusinessException(String message,int code,String description)
{
super(message);
this.code=code;
this.description=description;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code=errorCode.getCode();
this.description=errorCode.getDescription();
}
public BusinessException(ErrorCode errorCode,String description) {
super(errorCode.getMessage());
this.code=errorCode.getCode();
this.description=description;
}
}
GlobalExceptionHandler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
//对上传文件异常进行处理
@ExceptionHandler(value = MultipartException.class)
public BaseResponse<?> handleBusinessException(MaxUploadSizeExceededException ex) {
String msg;
if (ex.getCause().getCause() instanceof FileUploadBase.FileSizeLimitExceededException) {
log.error(ex.getMessage());
msg = "上传文件过大[单文件大小不得超过10M]";
} else if (ex.getCause().getCause() instanceof FileUploadBase.SizeLimitExceededException) {
log.error(ex.getMessage());
msg = "上传文件过大[总上传文件大小不得超过10M]";
} else {
msg = "上传文件失败";
}
return ResultUtils.error(ErrorCode.SYSTEM_ERROR,msg);
}
@ExceptionHandler(BusinessException.class)
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
log.error("businessException: " + e.getMessage(), e);
return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription());
}
@ExceptionHandler(RuntimeException.class)
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
log.error("runtimeException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage(), "");
}
}
效果如下
登录页面
注册页面
管理页面
个人中心
个人设置
源码:https://github.com/shenglingyulu/usermanagesystem.git