一、项目介绍
二、技术选型
三、项目架构
四、开发环境搭建
- 数据库
- 新建数据库reggie。
- 使用sql文件导入表结构文件(两种方式)。
- 使用图形化工具
- 命令行工具中,source命令加上sql文件路径。
- maven环境
- 创建springboot工程
- 导入需要使用的依赖
- 将项目推送到git
- 基础架构
- 实体类
- 相应结果类
- 代码快速生成(MVC三层架构)
- 导入依赖
<!--代码生成器相关依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
- 生成代码
public class MybatisPlusCodeTest {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/reggie?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false", "root", "1234")
.globalConfig(builder -> {
// 设置作者
builder.author("luge")
// 开启 swagger 模式
//.enableSwagger()
// 覆盖已生成文件
.fileOverride()
// 指定输出目录
.outputDir("C://Users//ASUS//Desktop//mybatis_plus");
})
.packageConfig(builder -> {
// 设置父包名
builder.parent("pers.jl")
// 设置mapperXml生成路径
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, "C://Users//ASUS//mybatis_plus"));
})
.strategyConfig(builder -> {
// 设置需要生成的表名
builder.addInclude("address_book")
.addInclude("category")
.addInclude("dish")
.addInclude("dish_flavor")
.addInclude("employee")
.addInclude("order_detail")
.addInclude("orders")
.addInclude("setmeal")
.addInclude("setmeal_dish")
.addInclude("shopping_cart")
.addInclude("user")
;
})
// 使用Freemarker引擎模板,默认的是Velocity引擎模板
.templateEngine(new FreemarkerTemplateEngine())
.execute();
}
}
五、pc端开发
一、登录和退出模块
- 登录逻辑:先判断用户名是否存在,再判断密码(经过MD5加密)是否正确,最后判断用户状态是否被禁用,登录成功将用户ID存入session中。
- 退出逻辑:移除session中的用户id,返回退出成功消息。
- 访问控制:定义一个过滤器,拦截所有请求。静态资源和登录、退出的controller请求不拦截,其余请求均拦截,如果已登录,则放行,否者返回登录界面。
/**
* 检查用户是否已经完成登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 强转一下
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1、获取本次请求的URI
String requestURI = request.getRequestURI();// /backend/index.html
//定义不需要处理的请求路径
String[] urls = new String[]{
// controller层的请求
"/employee/login",
"/employee/logout",
// 静态资源
"/backend/**",
"/front/**"
};
//2、判断本次请求是否需要拦截处理
boolean check = Utils.check(urls, requestURI);
//3、如果不需要处理,则直接放行
if(check){
filterChain.doFilter(request,response);
// 退出方法
return;
}
//4、如果请求资源需要登录才能访问,则要判断登录状态
// 如果已登录,则直接放行
if(request.getSession().getAttribute("employeeId") != null){
filterChain.doFilter(request,response);
return; }
log.info("用户未登录");
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
// 根据前端的响应拦截器接收结果来定义的
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return; }
}
- 注意事项:
- 注意要查询用户名,不要查询姓名,要多看表字段。
- 将所有controller层的@Controller注解换成@RestController。
- lambda表达式封装查询条件时,如果指定了是否添加条件,则最终查询后一定会返回一个对象。
- 快速删除一行 :ctrl+y
- controller层的业务逻辑封装到工具类中。
二、员工管理模块
1. 新增员工
- 新增逻辑:
- 设置一个全局异常处理类处理异常(新增员工用户名相同时mysql会向上抛异常)。
/**
* 全局异常处理类
*/
@ControllerAdvice
@ResponseBody// 响应json数据
@Slf4j
public class GlobalExceptionHandler {
/**
* 当新增员工,姓名重复时,mysql会报唯一性错误,这个方法会进行处理
* 使用Exception相比SQLIntegrityConstraintViolationException范围更大,但是不利于拦截种指定异常,不推荐
* SQLIntegrityConstraintViolationException更细,指定处理该类异常,异常消息也更少。
* @ExceptionHandler 这个注解对所有加了@RequestMapping()的类抛出的异常,都会处理
* @param ex
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
/**
* 用户删除分类失败时返回错误信息
* @return
*/
@ExceptionHandler(MyException.class)
public R<String> exceptionHandler(MyException ex){
// 打印异常日志信息
log.error(ex.getMessage());
// 将异常信息展示给用户,这个信息是我们提前写好的
return R.error(ex.getMessage());
}
}
- 注意事项:
- 每次做一个业务时,要理清它的思路,整个过程,根据产品原型明确业务需求。
2. 员工信息分页查询
-
分页逻辑:每次到达员工管理页面时,会自动发送一个get的分页请求,携带page和pagesise参数,我们需要创建page对象,然后传入这两个参数,如果页面输入姓名查询,我们还需要构建一个条件构造器,由于页面需要分页对象,所以最后返回分页对象即可。
-
编写一个mybatisplus的配置类,配置一个mybatisplus拦截器并添加分页拦截器。
-
注意事项:controller接收参数时,如果是对象中的部分属性,选择用对象来接收会好很多。
3. 员工状态更改
- 更改逻辑:页面点击禁用时,会发送put请求,并传递点击用户的id和状态取反值,我们需要接收参数然后设置更新时间和更新用户,最后直接传入用户实体,调用更新操作就行。
- 不同用户登录看见的编辑按钮不同,已在前端根据登录用户名进行限制了。
- 已禁用的员工会显示启用,已启用的员工会显示禁用。
- 注意事项:
- 注意前端返回的id有问题,原因是js处理Long型数据会丢失精度,解决办法,在webmvc中配置消息转换器,将Long型数据转换成字符串类型。
webmvc配置类
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
}
对象转换器
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
// 反序列化
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
// 序列化
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)// Long型数据转成String类型发给前端
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
4. 员工信息编辑
- 编辑逻辑:页面点击编辑,会获取那个对象的id,然后携带那个id跳转到修改信息页面。修改信息页面会根据id发送一个请求,我们需要根据id查出员工,然后返回给页面,页面就可以回显了,等我们修改好数据,点击保存,就可以调用我们之前写好的保存接口,由此,员工信息编辑功能就成功了。
- 注意事项:
- 前端页面需要什么数据,我们就给什么数据。
- ctrl + f5强制刷新缓存。
新增和编辑员工功能加强
- 客户端发送的每次http请求,服务端都会分配一个线程来处理,其中涉及到的所有方法都属于一个线程,利用这个特性我们可以在线程中共享数据。
- 一个请求上的方法调用都在一个线程中:
LoginCheckFilter的doFilter方法 -> EmployeeController的saveOrUpdate方法 -> MyMetaObjectHandler的insertFill方法。 - 由于每次进行这两个操作都会涉及创建时间和更新时间等相同字段的赋值,所以这里可以采取优化策略,使用mybatisplus自带的公共字段自动填充功能来免去重复字段的赋值操作,步骤如下:
- 重复字段添加@TableFiled注解,并指定自动填充策略。
// 最近更新时间和最近更新用户采用新增和修改都赋值的策略
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
// 创建用户和创建时间只需要在创建时进行赋值即可
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
- 写一个类实现MetaObjectHandler接口,重写insertFill和updateFill等方法,编写填充逻辑(就是公共字段填充什么,填充用户id可从线程变量里获取,session里获取不到)。
注意:要在过滤器里存入id,否则获取不到,每个线程的LocalThred变量不一样。
/**
* 元数据对象处理器
* 公共字段自动填充功能实现
* 对元数据进行处理
* 对字段自动填充赋值
* 凡是对员工的新增或更新操作都会自动填充
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
// 新增操作
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充");
// 设置公共字段自动填充
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
// 通过工具类,从线程变量中获取登录用户id
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
// 更新操作
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
- 写一个工具类,包含线程常量threadlocal,设置和获取用户id的方法,然后注释掉那些给字段赋值的代码。
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
* 不需要对象,所以不用交给spring管理
* 都是静态的属性和方法
*/
public class BaseContext {
// 线程中的这个对象,把他new出来,可以向里面存入东西。
private static final ThreadLocal<Long> threadlocal = new ThreadLocal<>();
// 将登录用户id存入线程变量中
public static void setCurrentId(Long id){
threadlocal.set(id);
}
// 从线程变量中取出登录用户id
public static Long getCurrentId(){
return threadlocal.get();
}
}
三、分类管理模块(难点)
1. 新增分类
- 由于有公共字段自动填充,所以可以直接调save方法保存。
2. 分页查询
- 需要一个分页构造器,一个条件构造器,然后调page方法去分页查询。
3. 删除分类(难点)
rest风格获取路径参数,只有get需要在注解后面跟上/{id},delete后面不用跟。
get需要使用@PathVariable注解,delete也不使用@PathVariable注解,delete不能从路径上获取参数,所以不使用这个注解。
@PathVariable注解表示从路径上获取参数。
因为携带数据有两种方式,post或put中的data和get中的路径参数。
- 使用get获取路径上的参数时
@GetMapping("/{id}") public R employeeEdit(@PathVariable Long id){ // 根据员工id查询员工信息 Employee employee = employeeService.getById(id); if (employee != null) { // 返回员工数据 return R.success(employee); } return R.error("员工不存在!"); }
- 使用delete获取路径上的参数时
@DeleteMapping public R deleteCategory(Long id){ log.info(String.valueOf(ids)); // 调用自定义删除方法进行删除 categoryService.remove(ids); return R.success("删除成功!"); }
4. 修改分类
-
与新增分类原理类似。
-
分类管理模块注意事项:
- 如果不想再controller层编写业务逻辑,可以在service层中自定义方法,然后在方法里写业务逻辑,最后只需在controller层调用方法即可,调用父类方法用super。
- 自定义异常的构造方法中传入消息,要想在页面显示,必须返回R对象,就需要在全局异常处理类中捕获指定异常,然后处理。
四、菜品管理模块
1. 文件上传下载
本质上也是发送请求,然后处理请求。
添加静态资源无法找到页面,可以clean一下,路径有缓存。
- 上传
- Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile的参数即可接受上传的文件。
- 注意controller接收文件的形参是MultipartFile file,file不是随便写的,必须要跟前端上传文件的组件input中的name属性值一致才能接收成功。
- 当controller接收到文件时,文件是以临时文件方式存在一个位置,请求结束,临时文件就会被删除,所以我们需要在controller接收到文件时将它保存到另一个位置。
- 截取文件后缀
- lastIndexof()是获取指定字符最后出现的下标。
- subString()截取字符串,一个参数是从哪开始截取,两个参数第一个是是从哪开始截取,第二个是截取到哪里停止。
- 随机生成UUID:id = UUID.randomUUID().toString()
- 下载
- 通过浏览器进行文件下载都是以流的方式写回浏览器的过程,有两种方式,一种是以附件形式下载,如下载jdk。另一种是直接在浏览器中打开,如图片。
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
// 读取配置文件自定义配置,文件夹路径
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传(图片)
* @param file
* @return
*/
@PostMapping("upload")
public R<String> upload(MultipartFile file){
// 1.重新给文件命名
// 获取原来文件名
String originalFilename = file.getOriginalFilename();
// 截取文件后缀
String subfix = originalFilename.substring(originalFilename.lastIndexOf("."));
// 随机生成UUid,拼接成新文件名
String newname = UUID.randomUUID().toString() + subfix;
// 2.判断转存文件夹是否存在
File file1 = new File(basePath);
if (!file1.exists()) {
// 文件夹不存在,新建文件夹
file1.mkdirs();
log.info("文件创建成功。");
}
// 3.文件转存
try {
// 如果是在服务器上,我们可以把图片保存到一个linux文件夹下面
file.transferTo(new File(basePath + newname));
log.info("文件转存成功。");
} catch (IOException e) {
e.printStackTrace();
}
// 转存成功,返回文件名字,
// 因为待会图片展示需要下载,就是需要上传成功时返回文件名,它才知道要下载哪个图片
return R.success(newname);
}
/**
* 文件下载(图片)
* @param response
* @param name
* @return
*/
@GetMapping("/download")
public void download(HttpServletResponse response,String name){// 页面发来的请求就带有文件名,是在上传文件时返回给页面的。
log.info("开始下载图片"+name);
try {
// 1.从哪里获取文件
FileInputStream inputStream = new FileInputStream(new File(basePath+name));
// 2.下载文件到哪里
ServletOutputStream outputStream = response.getOutputStream();
// 3. 设置响应文件格式
response.setContentType("image/jpeg");
// 4. 读取文件,写到输出流中
int len = 0;
byte[] bytes = new byte[1024];
// .read(bytes)的意思就是一次性读取1024个数据,
// 返回值是一个int,就是返回的是读取的指针的下标,
// 当指针为-1时,就代表读取完成
// 边读边写
while ((len = inputStream.read(bytes)) != -1){
// 一次性写出1024个数据,从数组下标为0位置开始写入,写入的字节数为读取的文件字节长度。
outputStream.write(bytes,0,len);
// 用于清空输出流
outputStream.flush();
}
log.info("图片下载完毕"+name);
// 5. 关闭资源
inputStream.close();
outputStream.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
2. 新增菜品
- 由于页面传输对象比较复杂,Dish实体类无法满足要求,所以我们需要自定义一个DishDto实体类,表示展示层和业务层传递的对象。
- 上面那个DishDto类继承自Dish实体类,在Dish实体类的基础上添加一个flavors集合,里面封装一个个的DishFlavor对象。
- 问题:按理说Dish的私有属性DishDto是无法继承的,但是加了@Data,却可以接收到前端传来的参数,很奇怪。只是打印的时候无法打印除flavors以外的属性,断点调试可以发现DishDto接收到了那些分类id、菜品名称等。
- 解释:如果子类中公有的方法影响到了父类私有属性,那么父类私有属性是能够被子类使用的。如果说,父类为私有属性写了public的get和set方法,那么子类可以继承这两个get和set方法,从而达到操作父类的私有属性的目的。实际上,子类已经继承了父类的私有属性
- 保存菜品时并保存口味,这个地方自定义一个方法,然后记得口味要先设置菜品id才行,可以使用stream流的map方法,不然不知道是哪个菜的口味。
- 由于涉及多张表操作,记得在业务方法上添加事务注解,主类上开启事务注解。
- Get请求接口如果要接收对象,不要使用@RequestBody,Get请求是不支持@RequestBody注解的。直接接收对象即可。
3. 分页查询
- 页面需要分类名称而不是分类id,返回Dish实体类无法满足要求,我们可以返回DishDto实体类类,但是要添加一个categoryName字段。
- 先利用分页构造器从Dish表中查出分页数据,然后复制到DishDto的分页构造器,返回DishDto的分页数据。
- 两个分页对象进行拷贝,records里是具体的数据,除了这个还有一些total呀,数据页码其他的,由于我们返回的records需要categoryName字段,所以我们把除records以外的拷贝完,然后对records进行处理,根据菜品id查出菜品名称,设置到DishDto对象里,把里面每个Dish对象映射成DishDto对象,然后返回全是DishDto的records。
- 对象拷贝
- 解决问题:循环依赖注入,两个类互相引用对方,导致spring不知到先初始化哪个bean,我们可以使用@Lazy注解延迟加载。
4. 修改菜品
- 也是多表处理的问题,只能单表更新,所以,我们需要自定义方法先更新dish表,然后把对应的口味表数据删除掉,再重新插入口味,插入口味这里和新增口味一样,需要我们设置口味的菜品id,记得添加事务注解。
5. 删除菜品或批量 删除
- 都是自定义方法,使用String来接收ids,然后以逗号分隔成数组,根据数组长度对每个菜品id进行删除,记得先删除口味,再删除菜品,否则会删除失败。(写到这里,我发现好像不用判断究竟是一个id还是多个id,我直接for循环遍历,统一处理就行,一个也可以循环。)
6. 批量起售或停售
- 页面发了一个post请求,在路径上传了一个已经取反的状态值,data里传了需要更改状态的ids,我们使用路径注解接收状态值,使用String来接收ids,根据id查到菜品对象,设置菜品对象新状态,然后更新,循环遍历处理即可。
五、套餐管理模块
- 总体跟菜品模块差不多。
- 删除套餐需要先停售,才能删除,如果不能删除,抛出一个业务异常,所以上面菜品管理中的删除功能还需要加强。
- 批量删除时,可以使用@RequestParam List ids来接收多个id。
- MybatisPlus自带批量删除的方法
- Dto对象一定要添加@Data注解,还要继承原来的对象。
- 员工管理没有删除功能,分类管理直接删除,没有使用逻辑删除。
- 菜品管理和套餐管理需要使用逻辑删除。
- 菜品和套餐删除时都需要判断是否是启售状态。
- 由于git使用不熟,吃大亏,git提交后一定要检查该提交版本提交了那些文件,不然代码要重新写
六、移动端开发
一、手机验证码登录
- 登录逻辑:
先判断用户是否存在,- 如果用户存在,走登录逻辑,判断验证码是否正确。如果验证码正确,将用户存入session中然后返回用户对象,登录成功。如果验证码不正确,登录失败。
- 如果用户不存在,走注册逻辑,判断验证码是否正确。如果验证码不正确,直接注册失败。如果验证码正确,new一个用户对象,将用户电话号码等基础信息设置到对象里,然后将用户设置到session中,返回用户对象,登录成功。
- 需要调用阿里云的短信服务api。安装依赖,使用SDK(软件开发工具包)模式。
- 过滤器中需要设置用户登录后将用户设置进session中,以及用户发送验证码和用户登录的请求都放行。
- 输入验证码后的参数传递,可以使用User的Dto对象,继承User对象,添加一个code字段,也可以使用map来接收,包含手机号码和验证码的key。
- 注意接收前端参数时,要看清前端发送的是什么方式的请求,参数类型是哪种。
- ctrl + f5 刷新缓存
- 一个threadlocal线程变量只能存储一个值,如果想存储多个值,可以定义多个线程变量。
解决问题:如何两次请求共享数据
直接使用session,不要使用request中的session(这个session和request中的session一样)
二、用户地址薄
- 新增地址薄
git提交时,记得看下是否勾选完,git只会提交勾选的文件。 - 设置默认地址
- 查询指定用户的所有地址
- 修改地址信息
三、菜品展示
- 返回的对象需要DishDto对象。
四、购物车
- 添加到购物车时,需要给订单添加用户id,指定是谁的订单。
- 同一个菜品重复添加时,只在数量上加一,需要先判断是否已经有该订单了,有就加一,没有就新增。
- 添加到购物车是增,显示用户购物车数据是查,点击数量减一是减和修改。
- 最后加入的菜品最上面显示,要排序。
五、用户下单
- 这里就不再开发真正的支付功能,支付功能需要去申请账号,个人无法申请。
- 订单确认页面点击去支付时,页面只传了地址id和支付id和备注,因为我们可以通过线程获取当前用户,然后查到购物车和默认地址。
最后注意
本项目将很多的方法都放在自定义的工具类中,代码如下。
工具类代码。
七、项目部署
手工部署
- 在Linux中安装jdk和mysql
- 把项目打成jar包,上传到服务器
- 使用nohup java -jar jar包 &> 路径/文件名.log & 来以进程方式启动项目,日志在指定位置输出。
- 关闭项目需要ps -ef | grap java ,找出进程的进程id,然后kill -9 进程id,就可以停止服务了。
通过shell脚本自动部署
- 在Linux中安装Git
- yum list git 列出git安装包
- yum install git 在线安装git
- 在Linux中安装maven
- 官网下载tar.gz压缩包,解压maven
- 修改环境变量
- 重新加载配置文件 source /etc/profile
- 查看maven版本信息 mvn -version
- 给maven中的本地仓库地址设置一个目录
- 编写Shell脚本(拉取代码、编译、打包、启动)
- 为用户授予Shell脚本的权限
- 执行Shell脚本
八、 项目优化
一、使用缓存降低数据库压力
1. 缓存短信验证码和菜品(redis)
- 导入redis依赖
- 配置相关redis配置
- 写一个redis配置类,使用自定义序列化器。
验证码
优化思路:
- 注入RedisTemplate对象。
- 将原来存在session中的验证码改存到redis中,获取也换成redis。
- 用户登录成功后,验证码就可以删除了,直接在redis中删除。
菜品
优化思路:
- 移动端按照菜品分类查询菜品数据,使用redis对这个做缓存。
- 查询菜品数据时,先向redis查询数据,如果有数据,直接返回,如果没有再向数据库查,然后查到了,再存到缓存里。
- 新增菜品和修改菜品,修改菜品状态时需要清除缓存,否则用户无法看到最新数据,脏读,删除不用,因为要删除必须先停售,停售就可以清楚了。
- 要保证数据库中的数据和缓存中的数据一致,否则需要及时清除缓存,清理缓存可以清除所有缓存,也可以清除当菜品分类下所有菜品的缓存。
遇到问题:将集合等数据存入 redis 需要将其序列化,jackson 不支持数据中的 LocalDateTime 类型的属性
解决办法:
- 引入依赖
<!--存入 redis 需要将其序列化,jackson 不支持数据中的 LocalDateTime 类型的属性-->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.0</version>
</dependency>
- 在使用了LocalDateTime的实体类上添加序列化和反序列化注解,并指定对应的序列化类
Dish
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@JsonDeserialize(using = LocalDateTimeDeserializer.class) // 反序列化
@JsonSerialize(using = LocalDateTimeSerializer.class) // 序列化
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonDeserialize(using = LocalDateTimeDeserializer.class) // 反序列化
@JsonSerialize(using = LocalDateTimeSerializer.class) // 序列化
private LocalDateTime updateTime;
DishFlavor
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@JsonDeserialize(using = LocalDateTimeDeserializer.class) // 反序列化
@JsonSerialize(using = LocalDateTimeSerializer.class) // 序列化
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonDeserialize(using = LocalDateTimeDeserializer.class) // 反序列化
@JsonSerialize(using = LocalDateTimeSerializer.class) // 序列化
private LocalDateTime updateTime;
2.缓存套餐(spring cache)
- 移动短短用户上传头像
- 自动显示用户姓名
- 前端地址簿数据回显有问题
- 订单信息分页查询,前端页面优化
- 管理端访问控制功能加强
SpringSecurity + JWT + Redis
使用SpringSecurity框架来帮我们实现访问控制,使用token来完善登录功能。
1. 导入依赖
2. 获取数据库所有用户名和密码
3. 编写配置类
二、数据库读写分离降低数据库压力
一、简介
- Mysql主从复制(mysql自带功能)
- master将改变记录到二进制日志(binary log)。
- slave将master的binary log拷贝到它的中继日志(relay log)。
- slave重做中继日志中的事件,将改变应用到自己的数据库中。
- 最终从库数据与主库数据一致,实现读写分离。
二、mysql主从复制搭建步骤
- 需要在主库mysql配置文件中开启二进制日志功能,并设置服务器id
- 然后重启mysql
log-bin=mysql-bin # 开启二进制日志
server-id=100 #服务器唯一id
systemctl restart mysqld # 重启mysql服务
- 在主库创建一个用户,后面从库登录使用这个账号,授予REPLICATION SLAVE权限,常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制。
注意:MYSQL8版本之后需要先创建用户,再赋权。分开操作!
// 创建用户
create user 'jianglu'@'%' identified by 'jianglu1234';
// 授权
GRANT REPLICATION SLAVE ON *.* TO 'jianglu'@'%';
// 刷新权限
flush privileges;
// 查看主库状态
show master status;
- 从库设置服务器唯一id,并配置从库
//设置服务器唯一id
server-id=101 #服务器唯一id
// 配置从库
CHANGE MASTER TO
master_host='192.168.142.85',
master_user='jianglu',
master_password='jianglu1234',
master_port=3001,
master_log_file='binlog.000004',
master_log_pos=862;
- 启动从库,并查看状态
// 启动从库
start slave;
// 查看从库状态
show slave status;
注:如果需要重新配置主从关系
# 重新配置主从流程,先停止,再重置从库连接主库配置
stop slave;
reset master;
再根据主库信息进行从库配置(SHOW MASTER STATUS;)
三、使用sharding-jdbc在代码中实现读写分离
- 导入依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
- 在配置文件中配置读写分离规则
- 在配置文件中配置允许bean定义覆盖配置项
server:
port: 8080
spring:
application:
name: reggie
shardingsphere:
datasource:
names:
master,slave
# 主数据源
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.142.85:3001/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 1234
# 从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.142.85:3002/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 1234
masterslave:
# 读写分离配置
load-balance-algorithm-type: round_robin #轮询
# 最终的数据源名称
name: dataSource
# 主库数据源名称
master-data-source-name: master
# 从库数据源名称列表,多个逗号分隔
slave-data-source-names: slave
props:
sql:
show: true #开启SQL显示,默认false
main:
allow-bean-definition-overriding: true
redis:
password: 123456
database: 0
host: localhost
port: 6379
cache:
redis:
time-to-live: 1800000 #设置缓存数据的过期时间为30分钟
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
# 自定义文件路径存取配置
reggie:
# 将图片存放地址更改为服务器上的地址
path: 192.168.142.85/opt/img/
三、使用Swagger生成接口文档,管理接口
- 导入knife4j坐标。
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
- 在WebMvcConfig配置类中对knife4j做相关配置。
1. 包括添加两个注解。
2. 注入一个文档对象。
3. 设置静态资源,否则接口文档页面无法访问。
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* 如果没有webmvcConfig控制类,我们可以直接访问static下的静态资源,
* 但是如果配置了这个webmvcConfig控制类,spring无法自动定位到静态资源,所以必须设置静态资源映射。
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/static/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/static/front/");
// 添加Swagger文档静态资源映射
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
// 注入文档对象
@Bean
public Docket createRestApi() {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("pers.jl.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
- 在LoginCheckFilter中设置不需要处理的请求路径。
"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"
- 在项目中添加Swagger注解
四、项目部署优化(前后端分别部署)
1. 使用nginx部署前端项目
一、 安装nginx。
- 官网下载tar.gz安装包,上传至服务器指定文件夹中。
官方下载地址 - 安装依赖
//一键安装上面四个依赖
yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel
- 解压
tar -zxvf nginx-1.24.0.tar.gz
- 编译
//执行命令 考虑到后续安装ssl证书 添加两个模块
./configure --with-http_stub_status_module --with-http_ssl_module
- 安装
/编译安装 (默认安装到/usr/local/nginx)
make & make install
- 启动
./nginx
二、上传dist到nginx的静态资源目录。
三、修改nginx.conf配置文件(由于dist文件发请求路径多了api参数,我们需要重写路径)。
#反向代理配置,url重写
location ^~ /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://192.168.142.85:8080;
}
2. 部署后端项目后,记得把图片存放位置修改一下。
- 服务器安装git、jdk、maven(略)。
- 修改项目配置文件redis服务地址、文件上传地址。
- 然后git拉取代码。
- 上传项目脚本。