瑞吉外卖项目详细分析笔记及所有功能补充代码
目录
- 项目刨析简介
- 技术栈
- 项目介绍
- 项目源码
- 一.架构搭建
- 1.初始化项目结构
- 2.数据库表结构设计
- 3.项目基本配置信息添加
- 公共字段的自动填充
- 全局异常处理类
- 返回结果封装的实体类
- 二.管理端业务开发
- 1.员工管理相关业务
- 1.1员工登录
- 1.2员工退出
- 1.3过滤器拦截
- 1.4员工信息修改
- 1.5员工信息分页查询
- 1.6新增员工
- 2.分类管理相关业务
- 2.1分类的分页查询
- 2.2新增分类
- 2.3菜品或套餐的分类修改
- 2.4菜品或套餐的分类删除
- 3.菜品管理相关业务
- 3.1分页查询
- 3.2图片上传下载
- 3.3新增菜品
- 3.4修改菜品
- 3.5删除菜品
- 3.6菜品停售与起售(补充)
- 4.套餐管理相关业务
- 4.1分页查询
- 4.2新增套餐
- 4.3修改套餐
- 4.4删除套餐
- 4.5套餐停售与起售(补充)
- 5.订单明细(补充)
- 三.移动端业务开发
- 1.用户登录与退出(退出为补充)
- 2.阿里云短信验证码
- 3.收货地址
- 4.菜品和套餐展示
- 5.菜品选规格
- 6.套餐点击展示(补充)
- 7.购物车
- 8.下订单
- 9.收货地址删除(补充)
- 10.用户支付后查看订单(补充)
- 11.再来一单(补充)
- 四.项目优化
- 1.使用Redis缓存
- 1.1缓存验证码
- 1.2缓存菜品查询数据
- 1.3Spring Cache缓存套餐数据
- 2.读写分离
- 2.1mysql主从复制
- 2.2Sharding-JDBC实现读写分离
- 2.3项目实现读写分离
- 3.使用Nginx服务器
- 3.1Nginx部署静态资源
- 3.2反向代理
- 3.3负载均衡
- 4.前后端分离开发
- 4.1YApi
- 4.2Swagger
- 4.3项目部署
项目刨析简介
#2022年末了,记录一下学习的项目实战经验和笔记吧
这个是瑞吉外卖项目,补充一些视频里面没有定义的功能和记录一些功能实现逻辑的笔记;仅供学习参考,本人代码可能不太规范,也有可能自己写了有些错误自己没有察觉,但是功能自己测试是没有问题的;感谢各位的阅览,如有问题欢迎指正,如有遗漏后续继续补充
技术栈
涉及到的技术有Spring,Springboot,Mybatis-plus,MySQL,Redis,Linux,Git,Spring Cache,Sharding-JDBC,Nginx,Swagger。(Apifox这些工具应该不算技术吧,用的工具就不列举了)
项目介绍
该项目是一个外卖点餐系统,它分为后台管理端和用户移动端两方面开发,后台管理端为商家提供管理菜品套餐的服务,移动端为用户提供点菜下单功能。最终通过git管理项目,并用nginx部署前端,tomcat部署后端,使用mysql主从复制,从库读取,主库写入,再用shell脚本部署到服务器上。
项目源码
项目码云地址:https://gitee.com/dkgk8/reggie-git
一.架构搭建
1.初始化项目结构
新建一个springboot项目
pom导入的坐标
org.springframework.boot spring-boot-starter com.github.xiaoymin knife4j-spring-boot-starter 3.0.2 org.springframework.boot spring-boot-starter-cache org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web compile com.baomidou mybatis-plus-boot-starter 3.4.2 org.projectlombok lombok 1.18.20 com.alibaba fastjson 1.2.76 commons-lang commons-lang 2.6 mysql mysql-connector-java runtime com.alibaba druid-spring-boot-starter 1.1.23 com.aliyun aliyun-java-sdk-core 4.5.16 com.aliyun aliyun-java-sdk-dysmsapi 2.1.0
yml配置文件添加的信息
server: port: 8080 spring: # application: # name: reggie_take_out datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 redis: host: localhost port: 6379 database: 0 cache: redis: time-to-live: 1800000 #ms ->30min 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: D:\SpringBoot_Reggie\reggie_take_out\src\main\resources\static\front\hello\
我后面将项目运行在服务器上了所以用了多环境开发,本地跑的不用在意这步
项目大致结构如下
感觉使用mybatis-plus之后就是
实体类->mapper->service->serviceImpl->controller
这个步骤写程序了
2.数据库表结构设计
不每个表展示了,这里拿典型的员工表来看
3.项目基本配置信息添加
导入前端资源
在默认页面和前台页面的情况下,直接把这俩拖到resource目录下直接访问是访问不到的,因为被mvc框架拦截了,其实用springboot,可以直接放在static目录下,但是仍然不能直接访问前端页面,所以这里也可以直接放行static就好了
所以我们要编写一个映射类放行这些资源
WebMvcConfig类
公共字段的自动填充
这个我在另一篇文章写了很详细,链接:自动填充公共字段
全局异常处理类
虽然遇到异常后可以使用try-catch来处理,但是,代码量一大起来,许多的try catch就会很乱,代码也不简洁,不容易阅读,所以我们使用全局异常处理,在Common包下
自定义异常类
返回结果封装的实体类
为了便于前后端数据传递,使用对象的形式封装数据更合适
@Data public class R implements Serializable { private Integer code; //编码:1成功,0和其它数字为失败 private String msg; //错误信息 private T data; //数据 private Map map = new HashMap(); //动态数据 public static R success(T object) { R r = new R(); r.data = object; r.code = 1; return r; } public static R error(String msg) { R r = new R(); r.msg = msg; r.code = 0; return r; } public R add(String key, Object value) { this.map.put(key, value); return this; } }
二.管理端业务开发
1.员工管理相关业务
1.1员工登录
登录逻辑如下
@PostMapping("/login") public R login(HttpServletRequest request, @RequestBody Employee employee){ //1.将页面提交的明文密码进行md5加密 String password = employee.getPassword(); password = DigestUtils.md5DigestAsHex(password.getBytes()); //2.根据页面提交的用户名username查数据库 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); queryWrapper.eq(Employee::getUsername,employee.getUsername()); Employee emp = employeeService.getOne(queryWrapper); //3.如果没有查询到则返回登录失败结果 if (emp == null){ return R.error("登录失败"); } //4.密码比对,如果不一致则返回登录失败结果 if (!emp.getPassword().equals(password)){ return R.error("登录失败"); } //5.查看员工账号状态是否锁定,若是禁用状态返回禁用信息 if (emp.getStatus() == 0){ return R.error("账号异常,已锁定"); } //6.登录成功,将员工id存入Session 并返回登录成功结果 request.getSession().setAttribute("employee",emp.getId()); return R.success(emp); }
1.2员工退出
就是清除员工登录时存入session的员工id
@PostMapping("/logout") public R logout(HttpServletRequest request){ //1.清理Session中保存的当前登录员工id request.getSession().removeAttribute("employee"); return R.success("退出成功"); }
1.3过滤器拦截
现在没有过滤器,用户直接不用登录通过url+资源名可以随便访问,所以要加个过滤器,没有登陆时,拦截请求,不给访问,自动跳转到登陆页面
过滤器处理逻辑
在启动类上添加注解@ServletComponentScan
过滤器配置类注解@WebFilter(filterName=“拦截器类名首字母小写”,urlPartten=“要拦截的路径,比如/*”)
判断用户是否已经登录,之前因为存入session里面有一个名为employee的对象,里面放的时用户id,那么只需要用getAttribute,看看session里get的数据是否为null就知道他是否在登陆状态
这里提一嘴
调用Spring核心包的字符串匹配类的对象,对路径进行匹配,并且返回比较结果
如果相等就为true
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
直接上代码
/** * 检查用户是否登录的过滤器 */ @WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*") @Slf4j public class LoginCheckFilter implements Filter { //路径匹配器,支持通配符 public static final AntPathMatcher PATH_MATCHER =new AntPathMatcher(); @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(); //定义不需要处理的请求路径 String[] urls=new String[]{ "/employee/login", "/employee/logout", "/backend/**", "/front/**", "/common/**", "/user/sendMsg", "/user/login", "/doc.html", "/webjars/**", "/swagger-resources", "/v2/api-docs" }; //2.判断本次请求是否需要处理 boolean check = check(urls, requestURI); //3.如果不需要处理则直接放行 if (check){ filterChain.doFilter(request,response); return; } //4-1.判断登录状态,如果已经登录,则直接放行 if (request.getSession().getAttribute("employee")!=null){ Long empId = (Long) request.getSession().getAttribute("employee"); BaseContext.setCurrentId(empId); filterChain.doFilter(request,response); return; } //4-2.判断移动端登录状态,如果已经登录,则直接放行 if (request.getSession().getAttribute("user")!=null){ Long userId = (Long) request.getSession().getAttribute("user"); BaseContext.setCurrentId(userId); filterChain.doFilter(request,response); return; } //5如果未登录则,通过输出流方式向客户端页面响应数据 response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN"))); return; } /** * 路径匹配,检查本次请求是否需要放行 */ public boolean check(String[] urls,String requestURI){ //遍历的同时调用PATH_MATCHER来对路径进行匹配 for (String url : urls){ boolean match = PATH_MATCHER.match(url,requestURI); if (match){ //匹配到了可以放行的路径,直接放行 return true; } } return false; } }
1.4员工信息修改
员工状态修改
遇到了问题,数据库id根据雪花算法有19位,而js对Long型数据处理时会丢失精度,只能保证前16位
解决办法: 服务端给页面响应json数据时,将Long型数据统一转为String字符串
将Long型的Id转换为String类型的数据
/** * 对象映射器:基于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) .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); } }
在MVC配置类中扩展一个消息转换器
/** * 扩展mvc框架的消息转换器 * @param converters */ @Override protected void extendMessageConverters(List