> 文档中心 > 最新Springboot+mp核心技术外卖入门实战项目(一)

最新Springboot+mp核心技术外卖入门实战项目(一)

这个是我最近跟着黑马最新发布的Springboot+MybatisPlus入门核心技术项目总结出来的文档,发出的代码都经过严谨的测试,保证功能可以实现才发给大家。我现在初步了解微服务并有一定的项目经验,现在感觉其实核心技术才是我们应该要注重的点,我们最开始第一次学的时候可能只是走马观花,学得不够扎实,导致后面用起来还是觉得很虚,现在这个单体项目真的就是针对我们这种刚入门或者返过来打基础的广大人群,熟练掌握这一套增删改查,相信大家在一般的企业上班问题不会太大,我们继续慢慢进步吧,每多敲一点,基础总会更好一些,前期技术学新的没错,但是重点还是要落在这些核心技术,这是我们吃饭的家伙,后面那些是我们技术的提升。相信我们每一步都走踏实了,后面肯定走得也越久越远。

软件开发流程

  1. 需求分析:产品原型、需求规格说明书

  2. 设计:产品文档,UI界面设计、概要设计、详细设计、数据库设计

  3. 编码:项目代码、单元测试

  4. 测试:测试用例、测试报告

  5. 上线运维:软件环境安装、配置

角色分工

  1. 项目经理:对整个项目负责,任务分配、把控进度

  2. 产品经理:进行需求调研,输出需求调研文档、产品原型等

  3. UI设计师根据产品原型输出界面效果图

  4. 架构师:项目整体架构设计、技术选型等

  5. 开发工程师:代码实现

  6. 测试工程师:编写测试用例,输出测试报告

  7. 运维工程师:软件环境搭建、项目上线

软件环境

  1. 开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问

  2. 测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问

  3. 生产环境(production):即上线环境,正式提供对外服务的环境

项目介绍:本项目是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的彩屏、套餐、订单等进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品,添加购物车、下单等。

本项目分为3期开发:

  1. 实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问

  2. 主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更方便

  3. 针对系统进行优化,提高系统的访问性能

产品原型:就是一款产品成型之前的一个简单的框架,就是将页面的排版布局展现出来,使产品的初步构思有一个可视化的展示。通过原型的展示,可以更加直观地了解项目的需求和提供的功能。(产品原型主要用于展示项目的功能,并不是最终的页面效果)

技术选型

 功能架构

角色

  1. 后台系统管理员:登录后台管理系统,拥有后台系统中的所有操作权限

  2. 后台系统普通员工:登录后台管理系统,对菜品、套餐、订单等进行管理

  3. C端用户:登录移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单等

  4. 环境搭建

    项目父文件

    4.0.0    com.std    my_take_away    1.0-SNAPSHOT     org.springframework.boot spring-boot-starter-parent 2.4.5           1.8               com.aliyun     aliyun-java-sdk-core     4.5.16       com.aliyun     aliyun-java-sdk-dysmsapi     2.1.0       org.springframework.boot     spring-boot-starter       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       io.swagger     swagger-annotations     1.5.24       cn.hutool     hutool-all     5.4.0                 org.springframework.boot  spring-boot-maven-plugin  2.4.5          
server:  port: 8080spring:  application:    #应用的名称,可选    name: my_take_out  datasource:    druid:      driver-class-name: com.mysql.cj.jdbc.Driver      url: jdbc:mysql://localhost:13306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true      username: root      password: 123456mybatis-plus:  configuration:    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射    map-underscore-to-camel-case: true    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  global-config:    db-config:      id-type: ASSIGN_ID

Springboot启动类

@Slf4j@SpringBootApplication@ServletComponentScanpublic class ReggieApplication {    public static void main(String[] args) { SpringApplication.run(ReggieApplication.class, args); log.info("项目启动成功...");    }}

SpringMVC配置类,因为资源文件默认加载在resource的static或者template目录下,用这种方式可以配置到自己的目录中,但是记得要在Springboot启动类中加包扫描注解

@Slf4j@Configurationpublic class WebMvcConfig extends WebMvcConfigurationSupport {    /    * 设置静态资源映射    *@Param [registry]    *@Return    */    @Override    protected void addResourceHandlers(ResourceHandlerRegistry registry) { log.info("开始静态资源映射..."); registry.addResourceHandler("/backend/").addResourceLocations("classpath:/backend/"); registry.addResourceHandler("/front/").addResourceLocations("classpath:/front/");    }}

编码阶段一 —— 员工管理

common包下的部分

/ * @author William * @create 2022-04-21 20:21 */@ControllerAdvice(annotations = {RestController.class, Controller.class})@ResponseBody@Slf4jpublic class GlobalExceptionHandler {    /    * 异常处理方法    *@Param []    *@Return    */    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)    public R exceptionHandler(SQLIntegrityConstraintViolationException ex){ log.error(ex.getMessage()); if(ex.getMessage().contains("Duplicate entry")){     //Duplicate entry 'zhangsan' for key 'employee.idx_username'     String[] split = ex.getMessage().split(" ");     String msg = split[2] + "已存在";     return R.error(msg); } return R.error("未知错误");    }}/ * 对象映射器:基于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);    }}/ * 通用返回结果,服务端响应的数据最终都会封装成此对象 * @param  */@Datapublic class R {    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;    }}

config包下部分

因为想要使用mp内置分页得配置一个配置类。然后对于Java中将雪花算法生成的Id直接强转为Long类型时会发生精度丢失问题,需要将Id先转化成String的类型才能解决这个问题,扩展MVC框架的消息转换器,底层使用Jackson将Java对象转为json。

/ * 配置MP的分页插件 * @author William * @create 2022-04-21 20:47 */@Configurationpublic class MybatisPlusConfig {    @Bean    public MybatisPlusInterceptor MybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mybatisPlusInterceptor;    }}/ * @author William * @create 2022-04-21 15:19 */@Slf4j@Configurationpublic class WebMvcConfig extends WebMvcConfigurationSupport {    /    * 设置静态资源映射    *@Param [registry]    *@Return    */    @Override    protected void addResourceHandlers(ResourceHandlerRegistry registry) { log.info("开始静态资源映射..."); registry.addResourceHandler("/backend/").addResourceLocations("classpath:/backend/"); registry.addResourceHandler("/front/").addResourceLocations("classpath:/front/");    }    /    * 扩展mvc框架的消息转换器    *@Param [converters]    *@Return    */    @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);    }}

登录过滤器

/ * 检查用户是否已经完成登录 * @author William * @create 2022-04-21 17:50 */@Slf4j@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")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();// backend/index.html log.info("拦截到请求:{}", requestURI); String[] urls = new String[]{  "/employee/login",  "/employee/logout",  "/backend/",  "/front/" }; //2. 判断本次请求是否需要处理 boolean check = check(urls, requestURI); //3. 如果不需要处理,则直接放行 if(check) {     log.info("本次请求不需要处理:{}" ,requestURI);     filterChain.doFilter(request, response);     return; } //4. 判断用户登录状态,如果已登录,则直接放行 if(request.getSession().getAttribute("employee") != null){     log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("employee"));     filterChain.doFilter(request, response);     return; } log.info("用户未登录"); //5. 如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据 response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN"))); return; /*log.info("拦截到请求:{}", request.getRequestURI()); filterChain.doFilter(request, response);*/    }    /    * 路径匹配,检查本次请求是否需要放行    *@Param [urls, requestURI]    *@Return    */    public boolean check(String[] urls, String requestURI){ for(String url : urls){     boolean match = PATH_MATCHER.match(url, requestURI);     if(match){  return true;     } } return false;    }}

员工实体类

@Datapublic class Employee implements Serializable {    private static final long serialVersionUID = 1L;    private Long id;    private String username;    private String name;    private String password;    private String phone;    private String sex;    private String idNumber;//身份证号码    private Integer status;    @TableField(fill = FieldFill.INSERT) //插入时填充字段    private LocalDateTime createTime;    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段    private LocalDateTime updateTime;    @TableField(fill = FieldFill.INSERT) //插入时填充字段    private Long createUser;    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段    private Long updateUser;}

员工controller部分

这是跟着黑马最新出的视频写的,我看老师在controller层处理过多业务,就封装了很多方法放到service层中来实现,按照mvc三层架构处理

/ * @author William * @create 2022-04-21 16:29 */@Slf4j@RestController@RequestMapping("/employee")public class EmployeeController {    @Autowired    private EmployeeServiceImpl employeeService;    @ApiOperation(value = "登录", notes = "员工登录")    @PostMapping("/login")    public R login(HttpServletRequest request, @RequestBody Employee employee){ String password = employee.getPassword(); //password = DigestUtils.md5DigestAsHex(password.getBytes());//md5加密 password = SecureUtil.md5(password); //hutool的md5加密 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); queryWrapper.eq(Employee::getUsername, employee.getUsername()); Employee emp = employeeService.getOne(queryWrapper);//用getOne是因为用户名在数据库中有唯一约束 if(ObjectUtil.isNull(emp)){     return R.error("未查询到账号信息"); } if(!emp.getPassword().equals(password)){//比对密码     return R.error("密码错误"); } if(emp.getStatus() == 0){     return R.error("账号已禁用"); } request.getSession().setAttribute("employee", emp.getId()); return R.success(emp);    }    @ApiOperation(value = "退出", notes = "后台系统退出")    @PostMapping("/logout")    public R logout(HttpServletRequest request){ //清理Session中保存的当前登录的员工Id request.getSession().removeAttribute("employee"); return R.success("退出成功");    }    @ApiOperation(value = "分页", notes = "员工分页列表")    @GetMapping("/page")    public R page(int page, int pageSize, String name){ log.info("page = {}, pageSize = {}, name = {}", page, pageSize, name); Page pageInfo = employeeService.page(page, pageSize, name); return R.success(pageInfo);    }    @ApiOperation(value = "新增", notes = "新增员工")    @PostMapping    public R save(HttpServletRequest request, @RequestBody Employee employee){ log.info("新增员工,员工信息:{}", employee.toString()); boolean save = employeeService.save(request, employee); if(save){     return R.success("新增员工成功"); } return R.error("新增员工失败");    }    @ApiOperation(value = "修改", notes = "根据Id修改员工信息")    @PutMapping    public R update(HttpServletRequest request, @RequestBody Employee employee){ boolean update = employeeService.update(request, employee); if(update){     return R.success("员工信息修改成功"); } return R.error("员工信息修改失败");    }    @ApiOperation(value = "查询", notes = "根据Id查询员工信息")    @GetMapping("/{id}")    public R getById(@PathVariable Long id){ log.info("根据id查询员工信息..."); Employee employee = employeeService.getById(id); if (ObjectUtil.isNotEmpty(employee)){     return R.success(employee); } return R.error("没有查询到对应员工的信息");    }}

员工service接口部分

/ * @author William * @create 2022-04-21 16:26 */public interface IEmployeeService extends IService {    /    * 员工登录    *@Param [employee]    *@Return     * @return    */    //boolean login(HttpServletRequest request, Employee employee);    /    * 新增员工    *@Param [employee]    *@Return    */    boolean save(HttpServletRequest request, Employee employee);    Page page(int page, int pageSize, String name);//分页展示    boolean update(HttpServletRequest request, Employee employee);//修改}