技术栈
后端
SpringBoot + MyBatis + Redis
前端
Vue3.0 + TypeScript + Vue-Router + Vuex + Axios + ElementPlus + Echarts
简介
是一个经典简单的音乐网站。后端基本也就是在CRUD,redis是当缓存用的,数据库用的是mysql.
XML自定义(附加)mybatis-plus的mapper配置
mapper接口正常定义
package com.example.yin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.yin.model.domain.Admin;
import org.springframework.stereotype.Repository;
@Repository
public interface AdminMapper extends BaseMapper<Admin> {
}
在资源路径下定义xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.yin.mapper.AdminMapper">
<resultMap id="BaseResultMap" type="com.example.yin.model.domain.Admin">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="password" jdbcType="VARCHAR" property="password" />
</resultMap>
<sql id="Base_Column_List">
id, name, password
</sql>
</mapper>
Base_Column_Lis是sql语句块,用于减少重复代码,可以在别的地方用id包含进去,这里拿来覆盖mybatis-plus的默认设置。
resultMap的id必须是这样才能覆盖默认设置。
分环境的SpringBoot配置文件
通过默认(主)配置文件的最后一行:
spring.profiles.active=dev 启用application-dev.properties。
就是说两个环境之间可以有一些公共操作的。
log4j配置文件
log4j.properties跟application.properties同级目录。
# LOG4J
log4j.rootCategory=INFO, stdout,file
# print to console
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n
# print to file
log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.DatePattern='-'yyyy-MM-dd'.log'
log4j.appender.file.File=./logs/musicWebsite
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss} %p [%c]: %m%n
mybatis-plus的MetaObjectHandler
看着是在访问实体对象之前或者之后做一些操作,有点像拦截器。
package com.example.yin.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
代码习惯
对控制器的每个请求,都定义一个专门的model类来接受信息
控制器
@RestController
public class AdminController {
@Autowired
private AdminService adminService;
// 判断是否登录成功
@PostMapping("/admin/login/status")
public R loginStatus(@RequestBody AdminRequest adminRequest, HttpSession session) {
return adminService.verityPasswd(adminRequest, session);
}
}
AdminRequest的结构
package com.example.yin.model.request;
import lombok.Data;
/**
* @Author 祝英台炸油条
* @Time : 2022/6/6 18:44
**/
@Data
public class AdminRequest {
private Integer id;
private String username;
private String password;
}
并且定义一个服务调用结果类,作为服务调用的返回对象,也作为控制器的返回对象。
这里是R类
@Data
public class R {
private int code;
private String message;
private String type;
private Boolean success;
private Object data;
public static R success(String message) {
R r = new R();
r.setCode(200);
r.setMessage(message);
r.setSuccess(true);
r.setType("success");
r.setData(null);
return r;
}
public static R success(String message, Object data) {
R r = success(message);
r.setData(data);
return r;
}
public static R warning(String message) {
R r = error(message);
r.setType("warning");
return r;
}
public static R error(String message) {
R r = success(message);
r.setSuccess(false);
r.setType("error");
return r;
}
public static R fatal(String message) {
R r = error(message);
r.setCode(500);
return r;
}
}
这个类作为前后端交流的消息结构。
Service类无论如何也要分成接口和impl实现部分。这为微服务部署提供便利。
package com.example.yin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.yin.common.R;
import com.example.yin.mapper.AdminMapper;
import com.example.yin.model.domain.Admin;
import com.example.yin.model.request.AdminRequest;
import com.example.yin.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements AdminService {
@Autowired
private AdminMapper adminMapper;
@Override
public R verityPasswd(AdminRequest adminRequest, HttpSession session) {
QueryWrapper<Admin> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name",adminRequest.getUsername());
queryWrapper.eq("password",adminRequest.getPassword());
if (adminMapper.selectCount(queryWrapper) > 0) {
session.setAttribute("name", adminRequest.getUsername());
return R.success("登录成功");
} else {
return R.error("用户名或密码错误");
}
}
}
服务类的实现类需要继承mybatis-plus提供的ServiceImpl类,同时实现服务接口。
这个ServiceImpl类看原码发现会要一个基本mapper,但是为了使用扩展后的mapper我们定义了一个新的,而且这个类提供了不少常用的方法,可以研究一下。
工具代码
复杂表单提交,包含文件和参数的表单。用户home目录和文件系统分隔符的获取,
表单文件的保存。
public R addSong(SongRequest addSongRequest, MultipartFile mpfile) {
Song song = new Song();
BeanUtils.copyProperties(addSongRequest, song);
String pic = "/img/songPic/tubiao.jpg";
String fileName = mpfile.getOriginalFilename();
String filePath = System.getProperty("user.dir") + System.getProperty("file.separator") + "song";
File file1 = new File(filePath);
if (!file1.exists()) {
if (!file1.mkdir()) {
return R.fatal("创建文件失败");
}
}
File dest = new File(filePath + System.getProperty("file.separator") + fileName);
String storeUrlPath = "/song/" + fileName;
try {
mpfile.transferTo(dest);
} catch (IOException e) {
return R.fatal("上传失败" + e.getMessage());
}
song.setCreateTime(new Date());
song.setUpdateTime(new Date());
song.setPic(pic);
song.setUrl(storeUrlPath);
if (songMapper.insert(song) > 0) {
return R.success("上传成功", storeUrlPath);
} else {
return R.error("上传失败");
}
}
BeanUtils类的使用
这里拿来拷贝对象的属性,可以理解为复制对象。
public R updateSongMsg(SongRequest updateSongRequest) {
Song song = new Song();
BeanUtils.copyProperties(updateSongRequest, song);
if (songMapper.updateById(song) > 0) {
return R.success("修改成功");
} else {
return R.error("修改失败");
}
}
md5加密,StringUtils类的使用,字符串工具类
public R addUser(ConsumerRequest registryRequest) {
if (this.existUser(registryRequest.getUsername())) {
return R.warning("用户名已注册");
}
Consumer consumer = new Consumer();
BeanUtils.copyProperties(registryRequest, consumer);
//MD5加密
String password = DigestUtils.md5DigestAsHex((SALT + registryRequest.getPassword()).getBytes(StandardCharsets.UTF_8));
consumer.setPassword(password);
//都用用
if (StringUtils.isBlank(consumer.getPhoneNum())) {
consumer.setPhoneNum(null);
}
if ("".equals(consumer.getEmail())) {
consumer.setEmail(null);
}
consumer.setAvator("img/avatorImages/user.jpg");
try {
if (consumerMapper.insert(consumer) > 0) {
return R.success("注册成功");
} else {
return R.error("注册失败");
}
} catch (DuplicateKeyException e) {
return R.fatal(e.getMessage());
}
}
SALT是一个常量字符串,叫盐值。
package com.example.yin.constant;
public class Constants {
/* 歌曲图片,歌手图片,歌曲文件,歌单图片等文件的存放路径 */
public static String ASSETS_PATH = System.getProperty("user.dir");
public static String AVATOR_IMAGES_PATH = "file:" + ASSETS_PATH + "/img/avatorImages/";
public static String SONGLIST_PIC_PATH = "file:" + ASSETS_PATH + "/img/songListPic/";
public static String SONG_PIC_PATH = "file:" + ASSETS_PATH + "/img/songPic/";
public static String SONG_PATH = "file:" + ASSETS_PATH + "/song/";
public static String SINGER_PIC_PATH = "file:" + ASSETS_PATH + "/img/singerPic/";
public static String BANNER_PIC_PATH = "file:" + ASSETS_PATH + "/img/swiper/";
/* 盐值加密 */
public static String SALT = "zyt";
}
redis配置类
`` java
package com.example.yin.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
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.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@EnableCaching //开启缓存注解
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
# 拦截器的定义
先实现HandlerInterceptor接口,再去实现WebMvcConfigurer接口的@Configuration覆盖 public void addInterceptors(InterceptorRegistry registry)把实现的拦截器注册进去。
定义拦截器
``` java
package com.example.yin.config;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CorsInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x_requested_with,x-requested-with,Authorization,Content-Type,token");
response.setHeader("Access-Control-Allow-Credentials", "true");
return true;
}
}
注册拦截器
package com.example.yin.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Author 祝英台炸油条
* @Time : 2022/6/7 17:08
**/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public CorsInterceptor corsInterceptor() {
return new CorsInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(corsInterceptor())
.addPathPatterns("/**");
}
}