技术栈
项目部署
简历上可写的点
-
集中处理系统异常,自定义统一的错误码, 并封装了全局异常处理器,屏蔽了项目冗余的报错细节、便于接口调用方理解和统一处理。
-
基于静态
ThreadLocal
封装了线程隔离的全局上下文对象,便于在请求内部存取用户信息,减少用户远程查询次数。 -
为兼容请求参数date类型的序列化,定义Jackson对象映射器处理日期;并扩展
SpringMVC
的消息转换器,实现自动序列化。 -
自定义
MyBatis Plus
的MetaObjectHandle
,配合全局上下文实现写数据前的创建时间、用户id字段的自动填充。 -
遵循Restful设计规范编写接口,降低前后端接口沟通和理解成本。
-
为解决原胜Jdk例化器导致的缓存key乱码问题,自定义
RedisTemplate Bean
的Redis Key列化器为StringRedisSerializer. -
使用Knife4j + Swagger自动生成后端接口文档,并通过编写ApiOperation等注解补充接口注释,避免了人工编写维护文档的麻烦。
-
为省复编写用户校验的麻烦,基于WebFilter实现全局登录校验;通过AntPathMatcher匹配动态请求路径,实现灵活的可选鉴权。
-
为保证数据的完整性和一致性,使佣
@Transactional
实现数据库事务,并配置rollbackFor = Exception.class支持受检异常的事务回滚。 -
为提高XX信息页加载速度,基于Spring Cache注解+ Redis 实现对XX信息的自动缓存,大幅降低数据库压力的同时将接口响应耗时由0.8s减少至50ms (数值自己再测一下)
-
为降低开发成本,使佣
MyBatis Plus
框架自动生成业务的增删改查重复代码,并使用LambdaQueryWrapper
实现更灵活地自定义查询。 -
为降低用户注册成本、保证用户真实性,二次封装XX云SDK接入短信服务,并通过Redis来集中缓存验证码,防止单手机号的重复发送。
-
为提高数据库整体读写性能,配置MySQL主从同步,并使用
sharding-jdbc
实现业务无侵入的读写分离。 -
封装全局Axios请求实例,添加全局请求拦截和全局异常响应处理器,减少重复的状态码判断、提升项目可维护性。
关于项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.5</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.xz</groupId><artifactId>angong_takeout</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><scope>compile</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.20</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.76</version></dependency><dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.6</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.23</version></dependency><dependency><groupId>org.jetbrains</groupId><artifactId>annotations</artifactId><version>RELEASE</version><scope>compile</scope></dependency><!--阿里云短信服务--><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.5.16</version></dependency><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-dysmsapi</artifactId><version>2.1.0</version></dependency><!--Redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--SpringCache--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.4.5</version></plugin></plugins></build></project>
application.yml
server:port: 8080
spring:application:name: angong_takeoutdatasource:druid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/angong?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=trueusername: rootpassword: root
mybatis-plus:configuration:#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:id-type: ASSIGN_ID
angong-pic:path: D:\img\
数据库设计
字段及含义
- address_book(地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。)
- category(分类,name(Unique,唯一索引),type字段:1为菜品分类,2为套餐分类)
- dish(菜品)
- dish_flavor(菜品口味)
- employee(员工表,username(Unique,唯一索引),status(状态默认为1【正常】,0为【禁用】)。登陆时查询,password进行MD5加密。)
- order_detail(订单明细)
- orders(订单)
- setmeal(套餐)
- setmeal_dish(套餐菜品关系)
- shopping_cart(购物车)
- user(用户信息)
表结构
/*Navicat Premium Data TransferSource Server : rootSource Server Type : MySQLSource Server Version : 50719Source Host : localhost:3306Source Schema : angongTarget Server Type : MySQLTarget Server Version : 50719File Encoding : 65001Date: 14/04/2023 14:31:56
*/SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for address_book
-- ----------------------------
DROP TABLE IF EXISTS `address_book`;
CREATE TABLE `address_book` (`id` bigint(20) NOT NULL COMMENT '主键',`user_id` bigint(20) NOT NULL COMMENT '用户id',`consignee` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '收货人',`sex` tinyint(4) NOT NULL COMMENT '性别 0 女 1 男',`phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '手机号',`province_code` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省级区划编号',`province_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省级名称',`city_code` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '市级区划编号',`city_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '市级名称',`district_code` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区级区划编号',`district_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区级名称',`detail` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '详细地址',`label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签',`is_default` tinyint(1) NOT NULL DEFAULT 0 COMMENT '默认 0 否 1是',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`create_user` bigint(20) NOT NULL COMMENT '创建人',`update_user` bigint(20) NOT NULL COMMENT '修改人',`is_deleted` int(11) NOT NULL DEFAULT 0 COMMENT '是否删除',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '地址管理' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for category
-- ----------------------------
DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (`id` bigint(20) NOT NULL COMMENT '主键',`type` int(11) NULL DEFAULT NULL COMMENT '类型 1 菜品分类 2 套餐分类',`name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '分类名称',`sort` int(11) NOT NULL DEFAULT 0 COMMENT '顺序',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`create_user` bigint(20) NOT NULL COMMENT '创建人',`update_user` bigint(20) NOT NULL COMMENT '修改人',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `idx_category_name`(`name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '菜品及套餐分类' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for dish
-- ----------------------------
DROP TABLE IF EXISTS `dish`;
CREATE TABLE `dish` (`id` bigint(20) NOT NULL COMMENT '主键',`name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '菜品名称',`category_id` bigint(20) NOT NULL COMMENT '菜品分类id',`price` decimal(10, 2) NULL DEFAULT NULL COMMENT '菜品价格',`code` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '商品码',`image` varchar(200) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '图片',`description` varchar(400) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '描述信息',`status` int(11) NOT NULL DEFAULT 1 COMMENT '0 停售 1 起售',`sort` int(11) NOT NULL DEFAULT 0 COMMENT '顺序',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`create_user` bigint(20) NOT NULL COMMENT '创建人',`update_user` bigint(20) NOT NULL COMMENT '修改人',`is_deleted` int(11) NOT NULL DEFAULT 0 COMMENT '是否删除',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `idx_dish_name`(`name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '菜品管理' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for dish_flavor
-- ----------------------------
DROP TABLE IF EXISTS `dish_flavor`;
CREATE TABLE `dish_flavor` (`id` bigint(20) NOT NULL COMMENT '主键',`dish_id` bigint(20) NOT NULL COMMENT '菜品',`name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '口味名称',`value` varchar(500) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '口味数据list',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`create_user` bigint(20) NOT NULL COMMENT '创建人',`update_user` bigint(20) NOT NULL COMMENT '修改人',`is_deleted` int(11) NOT NULL DEFAULT 0 COMMENT '是否删除',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '菜品口味关系表' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for employee
-- ----------------------------
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (`id` bigint(20) NOT NULL COMMENT '主键',`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '姓名',`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户名',`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '密码',`phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '手机号',`sex` varchar(2) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '性别',`id_number` varchar(18) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '身份证号',`status` int(11) NOT NULL DEFAULT 1 COMMENT '状态 0:禁用,1:正常',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`create_user` bigint(20) NOT NULL COMMENT '创建人',`update_user` bigint(20) NOT NULL COMMENT '修改人',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `idx_username`(`username`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '员工信息' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for order_detail
-- ----------------------------
DROP TABLE IF EXISTS `order_detail`;
CREATE TABLE `order_detail` (`id` bigint(20) NOT NULL COMMENT '主键',`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '名字',`image` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '图片',`order_id` bigint(20) NOT NULL COMMENT '订单id',`dish_id` bigint(20) NULL DEFAULT NULL COMMENT '菜品id',`setmeal_id` bigint(20) NULL DEFAULT NULL COMMENT '套餐id',`dish_flavor` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '口味',`number` int(11) NOT NULL DEFAULT 1 COMMENT '数量',`amount` decimal(10, 2) NOT NULL COMMENT '金额',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '订单明细表' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for orders
-- ----------------------------
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (`id` bigint(20) NOT NULL COMMENT '主键',`number` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '订单号',`status` int(11) NOT NULL DEFAULT 1 COMMENT '订单状态 1待付款,2待派送,3已派送,4已完成,5已取消',`user_id` bigint(20) NOT NULL COMMENT '下单用户',`address_book_id` bigint(20) NOT NULL COMMENT '地址id',`order_time` datetime NOT NULL COMMENT '下单时间',`checkout_time` datetime NOT NULL COMMENT '结账时间',`pay_method` int(11) NOT NULL DEFAULT 1 COMMENT '支付方式 1微信,2支付宝',`amount` decimal(10, 2) NOT NULL COMMENT '实收金额',`remark` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '备注',`phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,`consignee` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '订单表' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for setmeal
-- ----------------------------
DROP TABLE IF EXISTS `setmeal`;
CREATE TABLE `setmeal` (`id` bigint(20) NOT NULL COMMENT '主键',`category_id` bigint(20) NOT NULL COMMENT '菜品分类id',`name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '套餐名称',`price` decimal(10, 2) NOT NULL COMMENT '套餐价格',`status` int(11) NULL DEFAULT NULL COMMENT '状态 0:停用 1:启用',`code` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '编码',`description` varchar(512) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '描述信息',`image` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '图片',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`create_user` bigint(20) NOT NULL COMMENT '创建人',`update_user` bigint(20) NOT NULL COMMENT '修改人',`is_deleted` int(11) NOT NULL DEFAULT 0 COMMENT '是否删除',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `idx_setmeal_name`(`name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '套餐' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for setmeal_dish
-- ----------------------------
DROP TABLE IF EXISTS `setmeal_dish`;
CREATE TABLE `setmeal_dish` (`id` bigint(20) NOT NULL COMMENT '主键',`setmeal_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '套餐id ',`dish_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '菜品id',`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '菜品名称 (冗余字段)',`price` decimal(10, 2) NULL DEFAULT NULL COMMENT '菜品原价(冗余字段)',`copies` int(11) NOT NULL COMMENT '份数',`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',`create_user` bigint(20) NOT NULL COMMENT '创建人',`update_user` bigint(20) NOT NULL COMMENT '修改人',`is_deleted` int(11) NOT NULL DEFAULT 0 COMMENT '是否删除',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '套餐菜品关系' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for shopping_cart
-- ----------------------------
DROP TABLE IF EXISTS `shopping_cart`;
CREATE TABLE `shopping_cart` (`id` bigint(20) NOT NULL COMMENT '主键',`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '名称',`image` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '图片',`user_id` bigint(20) NOT NULL COMMENT '主键',`dish_id` bigint(20) NULL DEFAULT NULL COMMENT '菜品id',`setmeal_id` bigint(20) NULL DEFAULT NULL COMMENT '套餐id',`dish_flavor` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '口味',`number` int(11) NOT NULL DEFAULT 1 COMMENT '数量',`amount` decimal(10, 2) NOT NULL COMMENT '金额',`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '购物车' ROW_FORMAT = Dynamic;-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (`id` bigint(20) NOT NULL COMMENT '主键',`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '姓名',`phone` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '手机号',`sex` varchar(2) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '性别',`id_number` varchar(18) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '身份证号',`avatar` varchar(500) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '头像',`status` int(11) NULL DEFAULT 0 COMMENT '状态 0:禁用,1:正常',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '用户信息' ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;
项目文件结构/命名
- common(通用包)
- config(配置)
- filter(拦截类)
返回结果类(R.java)
package com.xz.angong.common;import lombok.Data;
import java.util.HashMap;
import java.util.Map;@Data
public class R<T> {private Integer code; //编码:1成功,0和其它数字为失败private String msg; //错误信息private T data; //数据private Map map = new HashMap(); //动态数据public static <T> R<T> success(T object) {R<T> r = new R<T>();r.data = object;r.code = 1;return r;}public static <T> R<T> error(String msg) {R r = new R();r.msg = msg;r.code = 0;return r;}public R<T> add(String key, Object value) {this.map.put(key, value);return this;}}
private T data; //数据
- 这里使用泛型,增加数据可用性,可以传各种对象进去
业务功能开发
登陆&退出
后台登陆功能
- 登录功能还进行了超时管理(超过10s登陆超时)
后台退出功能
员工管理业务开发
-
针对员工实体(employee表)进行操作
-
添加员工(前端进行电话和身份证校验,新增员工添加初始密码:123456,后面可以自行登录修改)
-
分页查询(用MP实现,添加配置,写处理逻辑)
/*** 分页展示** @param page* @param pageSize* @param name* @return*/@GetMapping("/page")public R<Page> page(int page, int pageSize, String name) { // log.info("page: {}, pageSize: {}, name: {}", page, pageSize, name);//分页构造器Page<Employee> pageInfo = new Page(page, pageSize);//条件构造器LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);queryWrapper.orderByDesc(Employee::getUpdateTime);//执行查询employeeService.page(pageInfo, queryWrapper);return R.success(pageInfo);}
-
编辑(对已有员工进行修改操作)/ 启用(禁用)
- 与后端进行两次交互
- 根据员工id查询员工信息并回显到前端页面
- 点击【保存】按钮将页面中员工信息提交给后端,并进行数据库修改
- 与后端进行两次交互
分类管理业务开发
- 新增分类
- 分类信息分页查询
- 删除分类
- 修改分类
菜品管理业务开发==[包含文件上传下载]==
-
文件上传(upload)下载(download)
-
上传(upload)
@Value("${angong-pic.path}") private String basePath;/*** 文件上传** @param file* @return*/ @PostMapping("/upload") public R<String> upload(MultipartFile file/* 这里file不能随便起,必须与前端form表单的name保持一致 */) {//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除log.info(String.valueOf(file));//原始文件名String originalFilename = file.getOriginalFilename();String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖String filename = UUID.randomUUID().toString() + suffix;//创建一个目录对象File dir = new File(basePath);//判断是否存在if (!dir.exists()) {dir.mkdirs();}try {//将临时文件转存到指定位置file.transferTo(new File(basePath + filename));} catch (IOException e) {e.printStackTrace();}return R.success(filename); }
-
下载(download)
@Value("${angong-pic.path}")private String basePath;/*** 文件下载** @param name* @param response*/ @GetMapping("/download") public void download(String name, HttpServletResponse response) {FileInputStream fileInputStream = null;ServletOutputStream outputStream = null;try {//通过输入流读取文件内容fileInputStream = new FileInputStream(new File(basePath + name));//通过输出流将文件写回浏览器,在浏览器展示图片outputStream = response.getOutputStream();response.setContentType("image/jpeg");int len = 0;byte[] bytes = new byte[1024];while ((len = fileInputStream.read(bytes)) != -1) {outputStream.write(bytes, 0, len);outputStream.flush();}} catch (Exception e) {e.printStackTrace();} finally {try {fileInputStream.close();outputStream.close();} catch (IOException e) {e.printStackTrace();}} }
-
-
新增菜品(利用Dto实现,DishDto:包含菜品以及口味字段)
-
菜品信息分页查询(因页面需要除dish以外的其他字段,例如categoryName等,利用Dto实现)
-
修改菜品(多表操作,更新dish表,修改dish_flavor表【先删除后添加】)
套餐管理业务开发
- 新增套餐(setmeal和setmeal_dish)
- 套餐信息分页查询
- 删除套餐(对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。支持批量删除)
手机验证码登录
package com.xz.angong.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xz.angong.common.R;
import com.xz.angong.entity.User;
import com.xz.angong.service.UserService;
import com.xz.angong.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** @author 许正* @version 1.0*/
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;// @Autowired
// private RedisTemplate redisTemplate;/*** 发送手机短信验证码** @param user* @return*/@PostMapping("/sendMsg")public R<String> sendMsg(@RequestBody User user, HttpSession session) {//获取手机号String phone = user.getPhone();if (StringUtils.isNotEmpty(phone)) {//生成随机的4位验证码String code = ValidateCodeUtils.generateValidateCode(4).toString();log.info("手机验证码 code: {}", code);//调用API发送短信//需填入 signName, templateCode 即可使用//SMSUtils.sendMessage("安工外卖--许正", "", phone, code);//需要将生成的验证码保存到Sessionsession.setAttribute(phone, code);// //将生成的验证码缓存到Redis中, 并且设置有效期为5分钟
// redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);return R.success("手机验证码短信发送成功");}return R.error("短信发送失败");}/*** 移动端登录** @param map* @param session* @return*/@PostMapping("/login")public R<User> login(@RequestBody Map map, HttpSession session) {log.info(map.toString());//获取手机号String phone = map.get("phone").toString();//获取验证码String code = map.get("code").toString();//从Session中获取保存的验证码Object codeInSession = session.getAttribute(phone);// //使用缓存
// Object codeInSession = redisTemplate.opsForValue().get(phone);//进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)if (codeInSession != null && codeInSession.equals(code)) {//如果能够比对成功,说明登录成功LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();userLambdaQueryWrapper.eq(User::getPhone, phone);User user = userService.getOne(userLambdaQueryWrapper);if (user == null) {//判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册user = new User();user.setPhone(phone);user.setStatus(1);userService.save(user);}session.setAttribute("user", user.getId());// //如果用户登陆成功, 删除Redis中缓存的验证码
// redisTemplate.delete(phone);return R.success(user);}return R.error("登陆失败");}
}
地址簿增删改查
package com.xz.angong.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.xz.angong.common.BaseContext;
import com.xz.angong.common.R;
import com.xz.angong.entity.AddressBook;
import com.xz.angong.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;/*** 地址簿管理*/
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {@Autowiredprivate AddressBookService addressBookService;/*** 新增** @param addressBook* @return*/@PostMappingpublic R<AddressBook> save(@RequestBody AddressBook addressBook) {addressBook.setUserId(BaseContext.getCurrentId());log.info("addressBook: {}", addressBook);addressBookService.save(addressBook);return R.success(addressBook);}/*** 设置默认地址** @param addressBook* @return*/@PutMapping("default")public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {log.info("addressBook:{}", addressBook);LambdaUpdateWrapper<AddressBook> addressBookLambdaUpdateWrapper = new LambdaUpdateWrapper<>();addressBookLambdaUpdateWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());addressBookLambdaUpdateWrapper.set(AddressBook::getIsDefault, 0);//SQL:update address_book set is_default = 0 where user_id = ?addressBookService.update(addressBookLambdaUpdateWrapper);addressBook.setIsDefault(1);//SQL:update address_book set is_default = 1 where id = ?addressBookService.updateById(addressBook);return R.success(addressBook);}/*** 根据id查询地址** @param id* @return*/@GetMapping("/{id}")public R get(@PathVariable Long id) {AddressBook addressBook = addressBookService.getById(id);if (addressBook != null) {return R.success(addressBook);} else {return R.error("没有找到该对象");}}/*** 查询默认地址** @return*/@GetMapping("default")public R<AddressBook> getDefault() {LambdaQueryWrapper<AddressBook> addressBookLambdaQueryWrapper = new LambdaQueryWrapper<>();addressBookLambdaQueryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());addressBookLambdaQueryWrapper.eq(AddressBook::getIsDefault, 1);//SQL:select * from address_book where user_id = ? and is_default = 1AddressBook addressBook = addressBookService.getOne(addressBookLambdaQueryWrapper);if (null == addressBook) {return R.error("没有找到该对象");} else {return R.success(addressBook);}}/*** 查询指定用户的全部地址** @param addressBook* @return*/@GetMapping("/list")public R<List<AddressBook>> list(AddressBook addressBook) {addressBook.setUserId(BaseContext.getCurrentId());log.info("addressBook:{}", addressBook);//条件构造器LambdaQueryWrapper<AddressBook> addressBookLambdaQueryWrapper = new LambdaQueryWrapper<>();addressBookLambdaQueryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());addressBookLambdaQueryWrapper.orderByDesc(AddressBook::getUpdateTime);//SQL:select * from address_book where user_id = ? order by update_time descList<AddressBook> addressBookList = addressBookService.list(addressBookLambdaQueryWrapper);return R.success(addressBookList);}
}
菜品展示
/*** 因前端需要调用list接口,但同时需要返回相应的口味信息,对list接口进行改造,返回一个DishDto对象** @param dish* @return*/@GetMapping("/list")public R<List<DishDto>> list(Dish dish) {List<DishDto> dtoList = null;// //动态构造key
// String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();//例:dish_1397844391040167938_1
//
// //先从Redis中获取缓存数据
// dtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
//
// if (dtoList != null) {
// //如果存在, 直接返回, 无需查询数据库
// return R.success(dtoList);
// }//如果不存在, 需要查询数据库, 将查询到的菜品数据缓存到Redis中//构造查询条件LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();dishLambdaQueryWrapper.eq(Dish::getStatus, 1).eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());//添加排序条件dishLambdaQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);List<Dish> dishList = dishService.list(dishLambdaQueryWrapper);dtoList = dishList.stream().map((item) -> {DishDto dishDto = new DishDto();BeanUtils.copyProperties(item, dishDto);Category category = categoryService.getById(item.getCategoryId());if (category != null) {dishDto.setCategoryName(category.getName());}//select * from dish_flavor where dishId = ?LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, item.getId());List<DishFlavor> flavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);dishDto.setFlavors(flavors);return dishDto;}).collect(Collectors.toList());
购物车
package com.xz.angong.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xz.angong.common.BaseContext;
import com.xz.angong.common.R;
import com.xz.angong.entity.ShoppingCart;
import com.xz.angong.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.time.LocalDateTime;
import java.util.List;/*** @author 许正* @version 1.0*/
@RestController
@Slf4j
@RequestMapping("/shoppingCart")
public class ShoppingCartController {@Autowiredprivate ShoppingCartService shoppingCartService;/*** 往购物车添加菜品** @param shoppingCart* @return*/@PostMapping("/add")public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {log.info("购物车数据: {}", shoppingCart.toString());//设置用户id,指定当前是哪个用户的购物车数据shoppingCart.setUserId(BaseContext.getCurrentId());//查询当前菜品或者套餐是否在购物车中Long dishId = shoppingCart.getDishId();//select * from shopping_cart where userId = ? and dish_id/setmeal_id = ?LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());if (dishId != null) {//添加的是菜品shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());} else {//添加的是套餐shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());}ShoppingCart shoppingCartServiceOne = shoppingCartService.getOne(shoppingCartLambdaQueryWrapper);if (shoppingCartServiceOne != null) {//如果已经存在, 就在原来数量基础上加1shoppingCartServiceOne.setNumber(shoppingCartServiceOne.getNumber() + 1);shoppingCartService.updateById(shoppingCartServiceOne);} else {//如果不存在, 则添加到购物车, 数量默认就是1shoppingCart.setNumber(1);shoppingCart.setCreateTime(LocalDateTime.now());shoppingCartService.save(shoppingCart);shoppingCartServiceOne = shoppingCart;}return R.success(shoppingCartServiceOne);}/*** 查看购物车** @return*/@GetMapping("/list")public R<List<ShoppingCart>> list() {log.info("查看购物车...");LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId()).orderByAsc(ShoppingCart::getCreateTime);List<ShoppingCart> cartList = shoppingCartService.list(shoppingCartLambdaQueryWrapper);return R.success(cartList);}/*** 减少已选菜品数量** @param shoppingCart* @return*/@PostMapping("/sub")public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart) {log.info("购物车数据: {}", shoppingCart.toString());//设置用户id,指定当前是哪个用户的购物车数据shoppingCart.setUserId(BaseContext.getCurrentId());//查询当前菜品或者套餐是否在购物车中Long dishId = shoppingCart.getDishId();//select * from shopping_cart where userId = ? and dish_id/setmeal_id = ?LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());if (dishId != null) {//菜品shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());} else {//套餐shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());}ShoppingCart shoppingCartServiceOne = shoppingCartService.getOne(shoppingCartLambdaQueryWrapper);if (shoppingCartServiceOne.getNumber() > 1) {//如果已经存在, 就在原来数量基础上-1shoppingCartServiceOne.setNumber(shoppingCartServiceOne.getNumber() - 1);shoppingCartService.updateById(shoppingCartServiceOne);} else {shoppingCartService.remove(shoppingCartLambdaQueryWrapper);}return R.success(shoppingCartServiceOne);}/*** 清空购物车** @return*/@DeleteMapping("/clean")public R<String> clean() {LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());shoppingCartService.remove(shoppingCartLambdaQueryWrapper);return R.success("清空购物车成功");}
}
用户下单
注意:金额进行原子操作,保证多线程的情况下计算也是准确的
//金额进行原子操作,保证多线程的情况下计算也是准确的
AtomicInteger amount = new AtomicInteger();
//1.计算总金额 2.封装订单明细表数据
List<OrderDetail> orderDetails = shoppingCartList.stream().map((item) -> {OrderDetail orderDetail = new OrderDetail();orderDetail.setOrderId(orderId);orderDetail.setNumber(item.getNumber());orderDetail.setDishFlavor(item.getDishFlavor());orderDetail.setDishId(item.getDishId());orderDetail.setSetmealId(item.getSetmealId());orderDetail.setName(item.getName());orderDetail.setImage(item.getImage());orderDetail.setAmount(item.getAmount());amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());return orderDetail;
}).collect(Collectors.toList());orders.setAmount(new BigDecimal(amount.get()));//总金额
OrdersServiceImpl.java
package com.xz.angong.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xz.angong.common.BaseContext;
import com.xz.angong.common.CustomException;
import com.xz.angong.entity.*;
import com.xz.angong.mapper.OrdersMapper;
import com.xz.angong.service.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;/*** @author 许正* @version 1.0*/
@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {@Autowiredprivate ShoppingCartService shoppingCartService;@Autowiredprivate UserService userService;@Autowiredprivate AddressBookService addressBookService;@Autowiredprivate OrderDetailService orderDetailService;/*** 用户下单** @param orders*/@Override@Transactionalpublic void submit(Orders orders) {//获得当前用户idLong userId = BaseContext.getCurrentId();//查询当前用户的购物车数据LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, userId);List<ShoppingCart> shoppingCartList = shoppingCartService.list(shoppingCartLambdaQueryWrapper);if (shoppingCartList == null || shoppingCartList.size() == 0) {throw new CustomException("购物车为空,不能下单!");}//查询用户数据User user = userService.getById(userId);//查询地址数据AddressBook addressBook = addressBookService.getById(orders.getAddressBookId());if (addressBook == null) {throw new CustomException("地址信息有误,不能下单!");}//向订单表插入数据, 一条数据long orderId = IdWorker.getId();//订单号//金额进行原子操作,保证多线程的情况下计算也是准确的AtomicInteger amount = new AtomicInteger();//1.计算总金额 2.封装订单明细表数据List<OrderDetail> orderDetails = shoppingCartList.stream().map((item) -> {OrderDetail orderDetail = new OrderDetail();orderDetail.setOrderId(orderId);orderDetail.setNumber(item.getNumber());//数量orderDetail.setDishFlavor(item.getDishFlavor());orderDetail.setDishId(item.getDishId());orderDetail.setSetmealId(item.getSetmealId());orderDetail.setName(item.getName());orderDetail.setImage(item.getImage());orderDetail.setAmount(item.getAmount());//单份金额//累加:单价*数量(转为BigDecimal)amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());return orderDetail;}).collect(Collectors.toList());orders.setAmount(new BigDecimal(amount.get()));//总金额orders.setId(orderId);orders.setOrderTime(LocalDateTime.now());//下单时间orders.setCheckoutTime(LocalDateTime.now());//支付时间,因支付系统未开发,这里设置为系统当前时间orders.setStatus(2);//2表示待派送orders.setUserId(userId);orders.setNumber(String.valueOf(orderId));orders.setUserName(user.getName());orders.setConsignee(addressBook.getConsignee());orders.setPhone(addressBook.getPhone());orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())+ (addressBook.getDetail() == null ? "" : addressBook.getDetail()));this.save(orders);//向订单明细表插入数据, 多条数据orderDetailService.saveBatch(orderDetails);//清空购物车数据shoppingCartService.remove(shoppingCartLambdaQueryWrapper);}
}
项目优化
缓存优化
用户数量多,系统访问量大
频繁访问数据库,系统性能下降,用户体验差
优化原因:系统上线后(部署到云服务器),访问客户端获取数据特别慢,在解决的过程中,想到了用缓存解决。
缓存短信验证码
实现思路
UserController.java
package com.xz.angong.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xz.angong.common.R;
import com.xz.angong.entity.User;
import com.xz.angong.service.UserService;
import com.xz.angong.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** @author 许正* @version 1.0*/
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@Autowiredprivate RedisTemplate redisTemplate;/*** 发送手机短信验证码** @param user* @return*/@PostMapping("/sendMsg")public R<String> sendMsg(@RequestBody User user, HttpSession session) {//获取手机号String phone = user.getPhone();if (StringUtils.isNotEmpty(phone)) {//生成随机的4位验证码String code = ValidateCodeUtils.generateValidateCode(4).toString();log.info("手机验证码 code: {}", code);//调用API发送短信//需填入 signName, templateCode 即可使用//SMSUtils.sendMessage("安工外卖--by许正", "", phone, code);// //需要将生成的验证码保存到Session
// session.setAttribute(phone, code);//将生成的验证码缓存到Redis中, 并且设置有效期为5分钟redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);return R.success("手机验证码短信发送成功");}return R.error("短信发送失败");}/*** 移动端登录** @param map* @param session* @return*/@PostMapping("/login")public R<User> login(@RequestBody Map map, HttpSession session) {log.info(map.toString());//获取手机号String phone = map.get("phone").toString();//获取验证码String code = map.get("code").toString();// //从Session中获取保存的验证码
// Object codeInSession = session.getAttribute(phone);//从Redis中获取缓存的验证码Object codeInSession = redisTemplate.opsForValue().get(phone);//进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)if (codeInSession != null && codeInSession.equals(code)) {//如果能够比对成功,说明登录成功LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();userLambdaQueryWrapper.eq(User::getPhone, phone);User user = userService.getOne(userLambdaQueryWrapper);if (user == null) {//判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册user = new User();user.setPhone(phone);user.setStatus(1);userService.save(user);}session.setAttribute("user", user.getId());//如果用户登陆成功, 删除Redis中缓存的验证码redisTemplate.delete(phone);return R.success(user);}return R.error("登录失败");}
}
缓存菜品数据(防止每次点击菜品都会查数据库)
实现思路
调用delete方法同样需要清理缓存
DishController.java
package com.xz.angong.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xz.angong.common.R;
import com.xz.angong.dto.DishDto;
import com.xz.angong.entity.Category;
import com.xz.angong.entity.Dish;
import com.xz.angong.entity.DishFlavor;
import com.xz.angong.service.CategoryService;
import com.xz.angong.service.DishFlavorService;
import com.xz.angong.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.jdbc.ScriptRunner;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;import java.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;/*** @author 许正* @version 1.0*/
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {@Autowiredprivate DishService dishService;@Autowiredprivate DishFlavorService dishFlavorService;@Autowiredprivate CategoryService categoryService;@Autowiredprivate RedisTemplate redisTemplate;/*** 新增菜品** @param dishDto* @return*/@PostMappingpublic R<String> save(@RequestBody DishDto dishDto) {log.info(dishDto.toString());dishService.saveWithFlavor(dishDto);// //清理所有菜品的缓存数据
// Set keys = redisTemplate.keys("dish_*");
// redisTemplate.delete(keys);//清理某个分类下面的菜品缓存数据String key = "dish_" + dishDto.getCategoryId() + "_" + dishDto.getStatus();redisTemplate.delete(key);return R.success("添加成功");}/*** 菜品信息分页查询** @param page* @param pageSize* @param name* @return*/@GetMapping("/page")public R<Page> page(int page, int pageSize, String name) {//得到dish的基本属性Page<Dish> dishPage = new Page<>(page, pageSize);LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.like(name != null, Dish::getName, name).orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);//执行分页查询dishService.page(dishPage, queryWrapper);//赋给dishDtoPage<DishDto> dishDtoPage = new Page<>();//除了“records”之外的属性进行复制BeanUtils.copyProperties(dishPage, dishDtoPage, "records");List<Dish> dishPageRecords = dishPage.getRecords();List<DishDto> dishDtoList = dishPageRecords.stream().map(dish -> {DishDto dishDto = new DishDto();BeanUtils.copyProperties(dish, dishDto);Category category = categoryService.getById(dish.getCategoryId());if (category != null) {dishDto.setCategoryName(category.getName());}return dishDto;}).collect(Collectors.toList());dishDtoPage.setRecords(dishDtoList);return R.success(dishDtoPage);}/*** 修改菜品及口味信息** @param dishDto* @return*/@PutMappingpublic R<String> update(@RequestBody DishDto dishDto) {dishService.updateWithFlavor(dishDto);// //清理所有菜品的缓存数据
// Set keys = redisTemplate.keys("dish_*");
// redisTemplate.delete(keys);//清理某个分类下面的菜品缓存数据String key = "dish_" + dishDto.getCategoryId() + "_" + dishDto.getStatus();redisTemplate.delete(key);return R.success("修改菜品成功");}/*** 根据id查询菜品信息和对应的口味信息** @param id* @return*/@GetMapping("/{id}")public R<DishDto> get(@PathVariable Long id) {DishDto dishDto = dishService.getByIdWithFlavor(id);return R.success(dishDto);}/*** 修改菜品售卖状态** @param status* @param ids* @return*/@PostMapping("/status/{status}")public R<String> statusWithIds(@PathVariable("status") Integer status, @RequestParam("ids") List<Long> ids) {log.info("售卖状态:{},ids: {}", status, ids);Dish dish = new Dish();for (Long dishId : ids) {dish.setId(dishId);dish.setStatus(status);dishService.updateById(dish);}return R.success("修改售卖状态成功");}// /**
// * 根据条件查询对应的菜品数据
// *
// * @param dish
// * @return
// */
// @GetMapping("/list")
// public R<List<Dish>> list(Dish dish) {
// LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
// dishLambdaQueryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId())
// .eq(Dish::getStatus, 1)//状态为1(起售)
// .orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
//
// List<Dish> dishList = dishService.list(dishLambdaQueryWrapper);
//
// return R.success(dishList);
// }/*** 因前端需要调用list接口,但同时需要返回相应的口味信息,对list接口进行改造,返回一个DishDto对象** @param dish* @return*/@GetMapping("/list")public R<List<DishDto>> list(Dish dish) {List<DishDto> dishDtoList = null;//动态构造keyString key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();//例:dish_1397844391040167938_1//先从Redis中获取缓存数据dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);if (dishDtoList != null) {//如果存在, 直接返回, 无需查询数据库return R.success(dishDtoList);}//如果不存在, 需要查询数据库, 将查询到的菜品数据缓存到Redis中//构造查询条件LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();dishLambdaQueryWrapper.eq(Dish::getStatus, 1).eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());//添加排序条件dishLambdaQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);List<Dish> dishList = dishService.list(dishLambdaQueryWrapper);dishDtoList = dishList.stream().map((item) -> {DishDto dishDto = new DishDto();BeanUtils.copyProperties(item, dishDto);Category category = categoryService.getById(item.getCategoryId());if (category != null) {dishDto.setCategoryName(category.getName());}//select * from dish_flavor where dishId = ?LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, item.getId());List<DishFlavor> flavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);dishDto.setFlavors(flavors);return dishDto;}).collect(Collectors.toList());//将查询到的菜品数据缓存到Redis中redisTemplate.opsForValue().set(key, dishDtoList, 1, TimeUnit.HOURS);return R.success(dishDtoList);}/*** 删除菜品** @param ids* @return*/@DeleteMappingpublic R<String> delete(@RequestParam List<Long> ids) {log.info("ids: {}", ids);dishService.removeWithFlavor(ids);//清理所有菜品的缓存数据Set keys = redisTemplate.keys("dish_*");redisTemplate.delete(keys);return R.success("菜品删除成功");}/*** 登录时可以初始化数据库** @return*/@RequestMapping("/restart")public R<String> restart() {log.info("........");try {//执行日志文件配置BufferedWriter log = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(System.getProperty("user.dir") + "/src/main/resources/sql/log.txt")), "UTF-8"));//执行sql语句报错时文件配置BufferedWriter error = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(System.getProperty("user.dir") + "/src/main/resources/sql/error.txt")), "UTF-8"));Class.forName("com.mysql.jdbc.Driver").newInstance();//获取数据源Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/angong?useUnicode=true&characterEncoding=utf-8&reConnect=true;", "root", "root");//创建脚本执行对象ScriptRunner r = new ScriptRunner(conn);//设置日志输出流,将执行日志保存至流中,每次将会覆写r.setErrorLogWriter(new PrintWriter(error));//设置错误信息输出,将错误日志保存至流中,每次将会覆写,如果sql脚本无错误运行成功,则该文件内容为空r.setLogWriter(new PrintWriter(log));//执行sql脚本,默认位置为classpath,也就是与src文件夹同级r.runScript(new BufferedReader(new InputStreamReader(new FileInputStream(new File(System.getProperty("user.dir") + "/src/main/resources/angong.sql")), "UTF-8")));} catch (Exception e) {e.printStackTrace();}return R.success("初始化成功!");}
}
缓存套餐数据
实现思路
项目亮点
登录拦截
- 只有登录成功后才可以访问系统中的页面,如果没有登陆则跳转到登录页面
- 实现方案:过滤器或拦截器,本项目使用过滤器
- 实现步骤:
- 创建自定义过滤器(LoginCheckFilter)
- 在启动类上加注解(@ServletComponentScan)
- 完善过滤器处理逻辑
package com.xz.angong.filter;import com.alibaba.fastjson.JSON;
import com.xz.angong.common.BaseContext;
import com.xz.angong.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.util.AntPathMatcher;import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @author 许正* @version 1.0*/
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {//路径匹配器,支持通配符public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();/*** 过滤请求** @param servletRequest* @param servletResponse* @param filterChain* @throws IOException* @throws ServletException*/@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;// log.info("拦截到请求:{}", request.getRequestURI());String requestURI = request.getRequestURI();String[] urls = new String[]{"/employee/login","/employee/logout","/backend/**","/front/**","/common/**"};boolean check = check(urls, requestURI);if (check) {
// log.info("本次请求不需要处理: " + requestURI);filterChain.doFilter(request, response);return;}//判断登陆状态if (request.getSession().getAttribute("employee") != null) {
// log.info("用户 " + request.getSession().getAttribute("employee") + " 已登录");//利用线程记录当前用户idLong empId = (Long) request.getSession().getAttribute("employee");BaseContext.setCurrentId(empId);filterChain.doFilter(request, response);return;}// log.info("用户未登录!");//通过输出流向客户端页面响应数据("NOTLOGIN")response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));}/*** 检查是否可以放行的方法** @param urls* @param 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;}
}
全局异常捕获
- 全局异常处理
package com.xz.angong.common;/*** @author 许正* @version 1.0*/import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;import java.sql.SQLIntegrityConstraintViolationException;/*** 全局异常处理*/
@ControllerAdvice(annotations = {RestController.class, ControllerAdvice.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {/*** 新增员工异常处理** @param ex* @return*/@ExceptionHandlerpublic R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {log.error(ex.getMessage());if (ex.getMessage().contains("Duplicate entry")) {String[] split = ex.getMessage().split(" ");return R.error("用户名" + split[2] + "已存在");}return R.error("未知错误");}/*** 自定义业务异常处理** @param ex* @return*/@ExceptionHandlerpublic R<String> exceptionHandler(CustomException ex) {log.error(ex.getMessage());return R.error(ex.getMessage());}
}
全局分页插件配置
package com.xz.angong.config;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author 许正* @version 1.0*/
@Configuration
public class MybatisPlusConfig {/*** 配置分页插件** @return*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());return mybatisPlusInterceptor;}
}
权限配置
- 只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作
- 所有普通用户登录系统后启用、禁用按钮不显示,只有编辑权限
数据库公共字段自动填充
数据库公共字段:
- create_time
- update_time
- create_user
- update_user
实现方案
Employee.java
@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;
MyMetaObjectHandler.java
package com.xz.angong.common;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;/*** @author 许正* @version 1.0*//*** 自定义元数据对象处理器*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {/*** 插入操作自动填充** @param metaObject*/@Overridepublic void insertFill(MetaObject metaObject) {metaObject.setValue("createTime", LocalDateTime.now());metaObject.setValue("updateTime", LocalDateTime.now());metaObject.setValue("createUser", BaseContext.getCurrentId());metaObject.setValue("updateUser", BaseContext.getCurrentId());}/*** 更新操作自动填充** @param metaObject*/@Overridepublic void updateFill(MetaObject metaObject) {metaObject.setValue("updateTime", LocalDateTime.now());metaObject.setValue("updateUser", BaseContext.getCurrentId());}
}
补充(从线程获取数据)
- 我们可以在
LoginCheckFilter
的doFilter
方法中获取当前登录用户id,并调用ThreadLocal
的set
方法来设置当前线程的线程局部变量的值(用户id) - 然后在
MyMetaObjectHandler
的updateFill
方法中调用ThreadLocal
的get
方法来获得当前线程所对应的线程局部变量的值(用户id)
实现步骤
-
编写
BaseContext
工具类,基于ThreadLocal
封装的工具类package com.xz.angong.common;/*** @author 许正* @version 1.0*//*** 基于ThreadLocal封装工具类,用于保存和获取当前登录用户id*/ public class BaseContext {private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();public static void setCurrentId(Long id) {threadLocal.set(id);}public static Long getCurrentId() {return threadLocal.get();} }
-
在
LoginCheckFilter
的doFilter
方法中调用BaseContext
来设置当前登录用户的idLoginCheckFilter.java–>doFilter() 利用线程记录当前用户id
//判断登陆状态if (request.getSession().getAttribute("employee") != null) { // log.info("用户 " + request.getSession().getAttribute("employee") + " 已登录");//利用线程记录当前用户idLong empId = (Long) request.getSession().getAttribute("employee");BaseContext.setCurrentId(empId);filterChain.doFilter(request, response);return;}
-
在
MyMetaObjectHandler
的方法中调用BaseContext
获取登录用户的idMyMetaObjectHandler.java 从线程中获取之前存放的当前用户id
package com.xz.angong.common;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component;import java.time.LocalDateTime;/*** @author 许正* @version 1.0*//*** 自定义元数据对象处理器*/ @Component @Slf4j public class MyMetaObjectHandler implements MetaObjectHandler {/*** 插入操作自动填充** @param metaObject*/@Overridepublic void insertFill(MetaObject metaObject) {metaObject.setValue("createTime", LocalDateTime.now());metaObject.setValue("updateTime", LocalDateTime.now());// long id = Thread.currentThread().getId(); // log.info("MyMetaObjectHandler-->insertFill()线程id: " + id);metaObject.setValue("createUser", BaseContext.getCurrentId());metaObject.setValue("updateUser", BaseContext.getCurrentId());}/*** 更新操作自动填充** @param metaObject*/@Overridepublic void updateFill(MetaObject metaObject) {metaObject.setValue("updateTime", LocalDateTime.now());// long id = Thread.currentThread().getId(); // log.info("MyMetaObjectHandler-->updateFill()线程id: " + id);metaObject.setValue("updateUser", BaseContext.getCurrentId());} }
DTO(数据传输对象)
DTO,全称为Data Transfer Object,即数据传输对象
一般用于展示层与服务层之间的数据传输。
本项目中使用到了DTO,如下所示:
DishDto.java
package com.xz.angong.dto;import com.xz.angong.entity.Dish;
import com.xz.angong.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;@Data
public class DishDto extends Dish {//继承Dish字段,并新增口味集合等,用于接收前端参数private List<DishFlavor> flavors = new ArrayList<>();private String categoryName;private Integer copies;
}
OrdersDto.java
package com.xz.angong.dto;import com.xz.angong.entity.OrderDetail;
import com.xz.angong.entity.Orders;
import lombok.Data;
import java.util.List;@Data
public class OrdersDto extends Orders {private String userName;private String phone;private String address;private String consignee;private List<OrderDetail> orderDetails;}
SetmealDto.java
package com.xz.angong.dto;import com.xz.angong.entity.Setmeal;
import com.xz.angong.entity.SetmealDish;
import lombok.Data;
import java.util.List;@Data
public class SetmealDto extends Setmeal {private List<SetmealDish> setmealDishes;private String categoryName;
}
注意
Json格式数据后端需加注解
- json形式的数据需要在参数前加注解:
@RequestBody
/*** 登录功能** @param request* @param employee* @return*/
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {String password = employee.getPassword();password = DigestUtils.md5DigestAsHex(password.getBytes());LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(Employee::getUsername, employee.getUsername());Employee emp = employeeService.getOne(queryWrapper);if (emp == null) {return R.error("用户不存在");}if (!emp.getPassword().equals(password)) {return R.error("密码错误");}if (emp.getStatus() != 1) {return R.error("用户已被禁用");}request.getSession().setAttribute("employee", emp.getId());return R.success(emp);
}
- json形式的参数需要在参数前加注解:
@RequestPram
/*** 删除套餐** @param ids* @return*/
@DeleteMapping
@CacheEvict(value = "setmealCache", allEntries = true)
public R<String> delete(@RequestParam List<Long> ids) {log.info("ids: {}", ids);setmealService.removeWithDish(ids);return R.success("套餐数据删除成功");
}
路径变量
- 前端请求带有路径变量的,参数前需要加注解:@PathVariable
/*** 根据id查找员工信息** @param id* @return*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id) {Employee employee = employeeService.getById(id);if (employee == null) {return R.error("未找到该员工");}return R.success(employee);
}
事务
操作多张表需要加注解:@Transactional
并在启动类加注解:@EnableTransactionManagement
遇到的困难
启用/禁用员工账号
测试过程中没有出错,但是功能并没有实现,数据库中的数据也并未修改。
SQL执行的结果是更新的数据行数为0,仔细观察id的值,和数据库中对应记录的id值并不相同。
问题所在
解决方案
具体实现步骤
-
提供对象转换器
JacksonObjectMapper
,基于 Jackson 进行java对象到 json 数据的转换package com.xz.angong.common;import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import java.math.BigInteger; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;/*** 对象映射器:基于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);} }
-
在
WebMvcConfig
配置类中扩展Spring mvc的消息转换器(WebMvcConfig.java),在此消息转换器中使用提供的对象转换器进行 Java对象到 json 数据的转换/*** 扩展mvc框架的消息转换器** @param converters*/ @Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {//创建消息转换器对象MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();//设置对象转换器,底层使用Jackson将Java对象转为JsonmessageConverter.setObjectMapper(new JacksonObjectMapper());//将上面的消息转换器对象追加到mvc框架的转换器集合中converters.add(0, messageConverter); }
项目小结
业务开发顺序
- 根据产品原型明确业务需求
- 重点分析数据的流转过程(请求,响应模式)和数据格式
- 先完成框架再写业务逻辑,最后代码实现
- 通过debug断点调试跟踪程序执行过程