本项目(瑞吉外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括系统管理后台和移动端应用两部分。
其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护。
移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。
本项目共分为3期进行开发:
第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问;
第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便;
第三期主要针对系统进行优化升级,提高系统的访问性能。
产品原型,就是一款产品成型之前的一个简单的框架,就是将页面的排版布局展现出来,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。
产品原型主要用于展示项目的功能,并不是最终的页面效果。
导入表结构,既可以使用图形界面也可以使用MySQL命令,通过命令行导入表结构时,sql文件不能放在中文目录下
创建完项目后,注意检查项目的编码、maven仓库配置、jdk配置等
server:
port: 8080
spring:
application:
name: reggie_take_out
datasource:
type: com.alibaba.druid.pool.DruidDataSource
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
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
将资料/前端资源目录下的两个文件夹拷贝到resource/static目录下
扩展:
也可以导入到resources中,就需要放行静态页面,设置资源映射
在默认页面和前台页面的情况下,直接把这俩拖到resource目录下直接访问是访问不到的,因为被mvc框架拦截了 所以我们要编写一个映射类放行这些资源
访问:http://localhost:8080/backend/index.html成功
登录页面:(页面位置:项目/resources/backend/page/login/login.html)
查看登录请求信息,通过浏览器调试工具(F12),点击登录按钮时,页面会发送请求(请求地址:http://localhost:8080/employee/login)并提交参数(username和password)
数据模型employee表
@Data
//实体类实现Serializable接口,把对象转换为字节序列。序列化操作用于存储时,一般是对于NoSql数据库。
public class Employee implements Serializable {
//在反序列化的过程中,如果接收方为对象加载了一个类,如果该对象的serialVersionUID与对应持久化时的类不同,那么反序列化的过程中将会导致InvalidClassException异常。
private static final long serialVersionUID = 1L;
@JsonFormat(shape = JsonFormat.Shape.STRING)
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;
//阿里巴巴的开发规范中推荐每个表都带有一个createTime 和一个 updateTime, 但是每次自己手动添加太麻烦了,可以配置MP让其自动添加,使用@TableField的fill注解。
@TableField(fill = FieldFill.INSERT)
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long updateUser;
}
@Mapper
public interface EmployeeDao extends BaseMapper<Employee> {
}
public interface EmployeeService extends IService<Employee> {
}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeDao, Employee> implements EmployeeService {
}
//用泛型格式,如果controller返回的是页面数据,则return R.success(page);如果返回的是成功消息,则return R.success("success");如果返回错误消息,return R.error("错误原因");
@Data
public class R<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
//静态类,controller返回时直接return R.success(T);
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;
}
}
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
/**
* 员工登录
* @param request
* @param employee
* @return
*/
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
//1.将页面提交的密码password进行md5加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
//2.根据页面提交的用户名username查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getPassword,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",employee.getId());
return R.success(emp);
}
}
通过debug进行功能测试
员工登录成功后,页面跳转到后台系统首页面(backend/index.html),此时会显示当前登录用户的姓名,如果员工需要退出系统,直接点击右侧的退出按钮即可退出系统,退出系统后页面应跳转回登录页面
用户点击页面中退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST。我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑:1、清理Session中的用户id;2、返回结果
/**
* 员工退出
* @param request
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清理Session中保存的当前登录员工的id
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
问题分析:登录功能完成后,用户如果不登录,直接访问系统首页面,照样可以正常访问,这种设计并不合理。
需求:登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面
解决方案:使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。
代码实现:
/**
* 检查用户是否已经登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
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;
filterChain.doFilter(request,response);
}
}
获取本次请求的URI
判断本次请求是否需要处理
如果不需要处理,则直接放行
判断登录状态,如果已登录,则直接放行
如果未登录则返回未登录结果
/**
* 检查用户是否已经登录
*/
@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/*8"
};
//2.判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3、如果不需要处理,则直接放行
if (check){
filterChain.doFilter(request,response);
return;
}
//4、判断登录状态,如果已登录,则直接放行
if (null != request.getSession().getAttribute("employee")){
filterChain.doFilter(request,response);
return;
}
//5、如果未登录则返回未登录结果,通过输出流的方式向客户端响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @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;
}
}
debug测试一下
后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击[添加员工]按钮跳转到新增页面。
新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。
注意:
- employee表中对username字段加入了唯一约束,因为username是 员工的登录账号,必须是唯一的
- employee表中的status字段已经设置了默认值1,表示状态正常
1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
//前端没有设置密码框,这里设置初始密码123456,需要进行md5加密处理。正常可以把"12345"改成employee.getPassword()
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
employeeService.save(employee);
//登录失败情况由下一节异常拦截处理
return R.success("新增员工成功");
}
问题:在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常:
java. sql. SQLIntegrityConstraintViolat ionException: Duplicate entry’ zhangsan’ for key' idx _username'
此时需要我们的程序进行异常捕获,通常有两种处理方式:
方式一:在Controller方法中加入try、catch进行异 常捕获
代码量很大的情况下,很多的try catch就会很乱,所以不推荐使用这种方式
方式二:使用异常处理器进行全局异常捕获
//@RestControllerAdvice包括了下面两行
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
//捕获完整性约束违反异常(其实就是数据库唯一约束异常)SQLIntegrityConstraintViolationException
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
return R.error("唯一约束异常:"+exception.getMessage().split(" ")[2]+"已存在");
}
return R.error("未知唯一约束错误");
}
}
当报错信息出现Duplicate entry时,就意味着新增员工异常了 ,不需要再用try catch这种形式了,不用管他,因为一旦出现错误就会被我们的AOP捕获。
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,因此一般的系统中都会以分页的方式来展示列表数据
整个程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、pageSize、 name)提交到服务端
2、服务端Controller接收 页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将 查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUl的Table组件展示到页面上
分页请求:
查询员工及显示接口:
Mybatis-plus实现分页
mp分页拦截器
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
分页查询
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
//构造分页构造器
Page pageInfo = new Page(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
//添加过滤条件
// StringUtils.isNotEmpty(name)判断为空的标准是name!=null&name.length>0
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
页面是怎么做到只有管理员admin能看到启用、禁用按钮的
程序执行过程:
1.页面发生ajax请求,将参数(id、status)提交到服务端
2.服务端Controller接收页面提交的数据并调用Service更新数据
3.Service调用Maper操作数据库
页面中的ajax请求是如何发送
启用、禁用员工账号本质就是一个更新操作。(也就是对status状态字段进行操作),在Controller中创建update方法,此方法是一个通用的修改员工信息的方法
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
//获取当前登录用户的id
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
问题:测试过程中没有报错,但是功能并没实现,查看数据库中的数据也没有变化,通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致
原因:分页查询时服务端响应给页面的数据中id的值为19位数字,类型为Long;页面中js处理long型数字只能精确到16位,所以最终通过ajax请求提交给服务端的id存在精度丢失
解决方案:
1.提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换
2.在WebMvcConfig配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
/**
* 扩展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将Tava对象转为json,或者将json转为Java对象
将JSON解析为Java对象的过程称为[从JSON反序列化Java对象]
从Java对象生成JSON的过程称为[序列化Java对象到JSON]
在员工管理列表页面点击编辑按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作
操作过程和对应的程序执行流程:
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作
窗口回显员工信息:
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if (employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
回显后,通过调用上面写的update方法完成修改
已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下
问题:能不能对于这些公共字段在某个地方统一处理,来简化开发呢?
答:使用Mybatis Plus提供的公共字段自动填充功能
Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", new Long(1));
metaObject.setValue("updateUser", new Long(1));
}
/**
* 修改操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", new Long(1));
}
}
注意:当前我们设置createUser和updateUser为固定值,后面我们需要进行改造,改为动态获得当前登录用户的id
前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充createUser和updateUser时设置的用户id是固定值,现在我们需要改成动态获取当前登录用户的id。
用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?
注意:我们在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。
可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。
在学习ThreadLocal之前,需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LoginCheckilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFil方法
可以在上面的三个方法中分别加入下面代码(获取当前线程id)
long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);
执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的:
什么是 ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每 个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
public void set(T value) 设置当前线程的线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值
我们可以在LoginCheckFilterdoFilter方法中获取当前登录用户id, 并调用ThreadLocal中set方法来设置当前线程的线程局部变量的值(用户id) ,然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。
实现步骤:
1、编写BaseContext工具类,基于ThreadLocal封装的工具类
2、在LoginCheckilter的doFilter方 法中调用BaseContext来设置当前登录用户的id
3、在MyMetaObjectHandler的方 法中调用BaseContext获取登录用户的id
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setThreadLocal(Long id){
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}
自定义元数据对象处理器,获取ThreadLocal的id
登录检查过滤器把id加到ThreadLocal
//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
//这里要强转,虽然request.getSession().getAttribute("employee")类型确实是Long
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request,response);
return;
}
后台系统中可以管理分类信息,分类包括两种类型,分別是菜品分类和套餐分类。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类 ,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。
可以在后台系统的分类管理页面分别添加菜品分类和套餐分类
新增分类就是把新增窗口录入的分类数据插入到category表,表结构:
注意:category表对name字段加入了唯一约束,保证分类的名称是唯一的
环境准备:先将需要用到的类和接口基本结构创建好
● 实体类Category ( 直接从课程资料中导入即可)
● Mapper接口CategoryMapper
● 业务层接口CategoryService
● 业务 层实现类CategoryServicelmpl
● 控制层CategoryController
整个程序的执行过程:
1、页面(backend/ page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可
代码实现:
/**
* 新增分类
* @param category
* @return
*/
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("category:{}",category);
categoryService.save(category);
return R.success("新增分类成功");
}
系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
整 个程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、 pageSize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUl的Table组件展示到页面上
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
//构造分页构造器
Page pageInfo = new Page(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();
//添加排序条件
queryWrapper.orderByDesc(Category::getSort);
//执行查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
整个程序的执行过程:
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
/**
* 根据id删除分类
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(Long ids){
log.info("删除分类,id为:{}",ids);
categoryService.removeById(ids);
return R.success("分类信息删除成功");
}
前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish和Setmeal (从课程资料中复制即可)
2、Mapper接口DishMapper和SetmealMapper
3、Service接口DishService和SetmealService
4、Service实现类DishServicelmpl和SetmealServicelmpl
代码实现:
自定义业务异常类CustomException:
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException{
//带参构造方法,重新设置异常信息
public CustomException(String message){
super(message);
}
}
异常处理GlobalExceptionHandler:
/**
* 自定义CustomException异常处理
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
CategoryServiceImpl:
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联了菜单,如果已经关联,抛出一个业务异常
if (count1 > 0){
throw new CustomException("当前分类下关联了菜品,不能删除");
}
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if (count2 > 0){
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//正常删除分类
super.removeById(id);
}
}
controller:
/**
* 根据id删除分类
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(Long ids){
log.info("删除分类,id为:{}",ids);
//categoryService.removeById(ids);
categoryService.remove(ids);
return R.success("分类信息删除成功");
}
在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作
/**
* 根据id修改分类
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
文件上传,也称为upload,是指将本地图片、视频、音频等文件.上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
文件上传时,对页面的form表单有如下要求:
举例:
目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传。
例如ElementUI中提供的upload上传组件:
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数(Form data)即可接收上传的文件,例如:
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
1、以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录) 直接在浏览器中打开;
2、通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
文件上传,页面端可以使用ElementUI提供的上传组件。
可以直接使用资料中提供的上传页面,位置:资料/文件上传下载页面/upload.html
前端上传页面请求:
注意:传参MutlipartFile类型参数名必须是file,也就是和form data的名字一致。
代码实现:
配置文件:
reggie:
#写成D:\img\或者D:/img/也能成功,写成双反斜杠点保险点,防止转义
path: D:\\img\\
读取路径并接收上传的文件:
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String basePath;
/**
* 文件地址
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,默认存在C:\\User\\...目录下,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
//原始文件名
String originalFilename = file.getOriginalFilename(); //abc.jpg
//获取文件后缀名,这里不能split,因为split里不能根据“.”分割
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖。UUID 是通用唯一识别码,具体解释看代码下面引用
String fileName = UUID.randomUUID().toString() + suffix; dfsdfdfd.jpg
//创建一个目录对象
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);
}
}
UUID:
UUID 是 通用唯一识别码,UUID 的目的,是让分布式系统中的所有元素,都能有唯一的辨识资讯,而不需要透过中央控制端来做辨识资讯的指定。
UUID.randomUUID().toString()是javaJDK提供的一个自动生成主键的方法。UUID(Universally Unique Identifier)全局唯一标识符,是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的,是由一个十六位的数字组成,表现出来的形式。由以下几部分的组合:当前日期和时间(UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同),时钟序列,全局唯一的IEEE机器识别号(如果有网卡,从网卡获得,没有网卡以其他方式获得),UUID的唯一缺陷在于生成的结果串会比较长。
回顾:前面Mybatis-plus学习五种id生成策略时,有个策略是ASSIGN_UUID:
ASSIGN_UUID:可以在分布式的情况下使用,而且能够保证唯一,但是生成的主键是32位的字符串,长度过长占用空间而且还不能排序,查询性能也慢
将文件从服务端下载到本地计算机,可以用标签展示下载的图片:
两种文件下载方式:
前端请求:
get方式传参,controller参数名为name就能直接传了,springmvc是可以自动转换格式的。
代码实现:
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
//输出流,通过输出流将文件写回浏览器
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpg");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish. flavor表插入数据。所以在新增菜品时,涉及到两个表:菜品表dish;菜品口味表dish_flavor
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
新增菜品时前端页面和服务端的交互过程:
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可
根据条件查询分类数据:
CategoryController:
/**
* 根据条件查询分类数据
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
//添加排序条件
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
创建DTO类封装表单数据
因为表单既包括了菜品,也包括了菜品味道,所以要新创建一个DTO类封装所有表单信息,它是菜品实体类的子类。
DishDto:
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
//下面两个成员变量暂时用不到,先放着,后面有其他表单需要封装
//菜品所属分类名称
private String categoryName;
private Integer copies;
}
DTO, 全称为Data Transfer Object,即数据传输对象,一 般用于展示层与服务层之间的数据传输。
新增菜品
DishService和DishServiceImpl:
public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish,dish_flavor
public void saveWithFlavor(DishDto dishDto);
}
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品同时保存对应的口味数据
* @param dishDto
*/
@Override
@Transactional //记得在引导类@EnableTransactionManagement
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId = dishDto.getId();//菜品id
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
// stream流的filter是满足条件的留下,是对原数组的过滤;map则是对原list的加工,map里是Lambda表达式,形容对list加工。
// 通过stream流的map将list加工,把这个菜品的id加到每个list元素的dishId属性;
flavors = flavors.stream().map((item) -> { //给list每个元素map加工,返回值为加工后的结果
item.setDishId(dishId);
return item;
}).collect(Collectors.toList()); //将加工后的结果遍历收集到原list
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
}
DishController:
/**
* 新增菜品
* @param dishDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
梳理一下菜品分页查询时前端页面和服务端的交互过程:
1、页面(backend/ page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、 name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
首先把图片资料拷贝到D://img中,这是前面上传下载代码时候在yml配置的图片路径。
DishController
/**
* 分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
//构造分页构造器对象
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>();
//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行分页查询,dishService查的是dish表,所以必须先Dish查询,再拷贝到DishDto
dishService.page(pageInfo,queryWrapper);
//注意这一步,将Dish的page拷贝到DishDto的page,第三个参数是忽略records属性
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
//steam流的map(Lambda表达式)对list进行加工,stream流返回值是可以换泛型的。等号左边是List,右边是List
List<DishDto> list = records.stream().map((item) -> {
DishDto dishDto = new DishDto();
//对象拷贝的工具类BeanUtils,将list里每一个菜品Dish对象信息拷贝到DishDto对象
BeanUtils.copyProperties(item,dishDto);
//将菜品所属分类的id查询为name,存到DishDto对象的categoryName,再返回替换item,完成item的加工
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作
修改菜品时前端页面(add.html) 和服务端的交互过程:
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可
由修改页面请求可以得知这里返回值必须是DishDto对象(包括菜品、味道、分类信息)。 所以需要在DishServiceImpl写getByIdWithFlavor方法。这里分类名返回null即可,前端会发送请求从分类id查分类name。
数据回显:
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
@Override
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息,从dish表查询
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//查询当前菜品对应的口味信息,从dish_flavor表查询
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> dishFlavors = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(dishFlavors);
return dishDto;
}
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
更新数据:
与新增时一样的问题DishFlavor中只是封装的name和value,而dishId并没有封装上,需要处理一下
/**
* 更新菜品信息同时更新口味信息
* @param dishDto
*/
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//更新dish表基本数据
this.updateById(dishDto);
//清理当前菜品对应口味数据
// dish_flavor表的delete操作:delete from dish_flavor where dish_id = ???
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
//添加当前提交过来的口味数据(dish_flavor表的insert操作)
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> { //给list每个元素map加工,返回值为加工后的结果
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList()); //将加工后的结果遍历收集到原list
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
/**
* 修改菜品
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.updateWithFlavor(dishDto);
return R.success("新增菜品成功");
}
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
新增套餐其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal _dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:套餐setmeal表;套餐菜品关系setmeal _dish表
setmeal表:
setmeal _dish表:
先将需要用到的类和接口基本结构创建好:
●实体类SetmealDish ( 直接从课程资料中导入即可,Setmeal实体前 面课程中已经导入过了)
●DTO SetmealDto (直接从课程资料中导入即可)
●Mapper接口 SetmealDishMapper
●业务层接口SetmealDishService
●业务层实现类SetmealDishServicelmpl
●控制层SetmealController
梳理一下新增套餐时前端页面和服务端的交互过程:
1、页面(backend/ page/combo/ add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2、页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3、页面发送ajax请求, 请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
5、页面发送请求进行图片下载,将上传的图片进行回显
6、点击保存按钮,发送ajax请求, 将套餐相关数据以json形式提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
setmealDishes不是setmeal表的字段,需要用SetmealDto封装参数
setmealServiceImpl:
/**
* 新增套餐同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保持套餐的基本信息,操作setmeal,执行insert操作
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item)-> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
//保存套餐和菜品的关联关系,操作setmeal_dish,执行insert操作
setmealDishService.saveBatch(setmealDishes);
}
setmealController:
/**
* 新增套餐
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}",setmealDto);
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看T所以一般的系统中都会以分页的方式来展示列表数据。
梳理一下套餐分页查询时前端页面和服务端的交互过程:
1、页面(backend/ page/ combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
setmealController:
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int page
//分页构造器
Page<Setmeal> pageInfo = new Page<
Page<SetmealDto> dtoPage = new Pag
LambdaQueryWrapper<Setmeal> queryW
//添加条件查询,根据name进行模糊查询like
queryWrapper.like(name != null, Se
//添加排序条件,根据更新时间进行降序排序
queryWrapper.orderByDesc(Setmeal::
setmealService.page(pageInfo,query
//对象拷贝
BeanUtils.copyProperties(pageInfo,
//忽略掉records后需要自己设置records的值
List<Setmeal> records = pageInfo.g
//处理records,将records计算后赋给list对象
List<SetmealDto> list =records.str
SetmealDto setmealDto = new Se
//对象拷贝
BeanUtils.copyProperties(item,
//套餐分类id
Long categoryId = item.getCate
//根据分类id查询分类对象
Category category = categorySe
if (category != null){
//套餐分类名称
String categoryName = cate
setmealDto.setCategoryName
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。
注意:对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
在开发代码之前,需要梳理一下删 除套餐时前端页面和服务端的交互过程:
1、删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
2、删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐
开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。
setmealService:
/**
* 删除套餐同时删除套餐的关联数据
* @param ids
*/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
//select count(*) from setmeal where id in (1,2,3..) and status = 1
//查询套餐状态,确定是否可以删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
int count = this.count(queryWrapper);
if (count > 0 ){
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除!");
}
//如果可以删除,先删除表中的数据
this.removeByIds(ids);
//delete from setmeal_dish where id in (1,2,3...)
LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealDishLambdaQueryWrapper.in(SetmealDish::getId,ids);
//删除关系表中的数据 setmeal_dish
setmealDishService.remove(setmealDishLambdaQueryWrapper);
}
setmealController:
/**
* 删除套餐
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids) {
log.info("ids:{}",ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。
手机验证码登录的优点:
●方便快捷,无需注册,直接登录
●使用短信验证码作为登录凭证,无需记忆密码
●安全
登录流程:输入手机号–>获取验证码–>输入验证码–>点击登录–>登录成功
注意:通过手机验证码登录,手机号是区分不同用户的标识
在开发代码之前,需要梳理一下登 录时前端页面和服务端的交互过程:
1、在登录页面(front/page/login.html)输入手机号,点击[获取验证码]按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信
2、在登录页面输入验证码,点击[登录]按钮,发送ajax请求,在服务端处理登录请求
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
先将需要用到的类和接口基本结构创建好:
●实体类User (直接从课程资料中导入即可)
●Mapper接口 UserMapper
●业务层接口UserService,业务层实现类UserServicelmpl
●控制层UserController
●工具类SMSUtils、 ValidateCodeUtils (直接从课程资料中导入即可)
修改LoginCheckFilter:
前面我们已经完成了LoginCheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的请求需要在此过滤器处理时直接放行。
在LoginCheckFilter过滤器中扩展逻辑,判断移动端用户登录状态:
//4.2 判断移动端登录状态,如果已登录,则直接放行
if (null != request.getSession().getAttribute("user")){
log.info("移动端用户已登录,用户id:{}",request.getSession().getAttribute("user"));
long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);
//这里要强转,虽然request.getSession().getAttribute("employee")类型确实是Long
Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request,response);
return;
}
/**
* 发送手机短信验证码
* @param user
* @return
*/
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession httpSessio
//获取手机号
String phone = user.getPhone();
if(StringUtils.isNotEmpty(phone)){
//生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toStrin
log.info("code = {}",code);
//调用阿里云提供的短信服务API完成发送短信
//SMSUtils.sendMessage("瑞吉外卖","",phone,code);
//需要将生成的验证码保存到session
httpSession.setAttribute(phone,code);
return R.success("手机验证码发送成功");
}
return R.error("手机验证码发送失败");
}
用户登录(首次登录自动注册):
@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);
//进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
if(codeInSession != null && codeInSession.equals(code)){
//如果能够比对成功,说明登录成功
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(queryWrapper);
if(user == null){
//判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user",user.getId());
return R.success(user);
}
return R.error("登录失败");
}
地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。
用户的地址信息会存储在address_ book表,即地址簿表中
● 实体类AddressBook (直接从课程资料中导入即可)
● Mapper接 口AddressBookMapper
● 业务层接口AddressBookService
● 业务 层实现类AddressBookServicelmpl
● 控制层AddressBookController
/**
* 地址簿管理
*/
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;
/**
* 新增
*/
@PostMapping
public R<AddressBook> save(@RequestBody AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook:{}", addressBook);
addressBookService.save(addressBook);
return R.success(addressBook);
}
/**
* 设置默认地址
*/
@PutMapping("default")
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
//先把所有地址is_default改成0
log.info("addressBook:{}", addressBook);
LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
wrapper.set(AddressBook::getIsDefault, 0);
//SQL:update address_book set is_default = 0 where user_id = ?
addressBookService.update(wrapper);
//再把这个地址设为默认
addressBook.setIsDefault(1);
//SQL:update address_book set is_default = 1 where id = ?
addressBookService.updateById(addressBook);
return R.success(addressBook);
}
/**
* 根据id查询地址
*/
@GetMapping("/{id}")
public R get(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
if (addressBook != null) {
return R.success(addressBook);
} else {
return R.error("没有找到该对象");
}
}
/**
* 查询默认地址
*/
@GetMapping("default")
public R<AddressBook> getDefault() {
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
queryWrapper.eq(AddressBook::getIsDefault, 1);
//SQL:select * from address_book where user_id = ? and is_default = 1
AddressBook addressBook = addressBookService.getOne(queryWrapper);
if (null == addressBook) {
return R.error("没有找到该对象");
} else {
return R.success(addressBook);
}
}
/**
* 查询指定用户的全部地址
*/
@GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook:{}", addressBook);
//条件构造器
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
queryWrapper.orderByDesc(AddressBook::getUpdateTime);
//SQL:select * from address_book where user_id = ? order by update_time desc
return R.success(addressBookService.list(queryWrapper));
}
}
用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示选择规格按钮,否则显示+按钮。
梳理一下前端页面和服务端的交互过程:
1、页面(front/index.html)发送ajax请求, 获取分类数据(菜品分类和套餐分类)
2、页面发送ajax请求,获取第一个分类下的菜品或者套餐
开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据
此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再修改回来,如下:
问题:为什么登录后直接就展示了部分菜品信息
原因:请求了/category/list和/dish/list
又有个问题:为什么需求中的关于口味信息的数据没有
原因:DishController的list方法条件查询(添加套餐时按分类查询菜品id)传参是Dish,要获取到口味信息就需要传dish和dishFlavor,也就是需要换成DishDto
修改DishController的list方法:
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品的id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
//SQL:select * from dish_flavor where dish_id = ?
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
问题:点击商务套餐时报错
原因:setmealController中还没写list方法(根据categoryId查询对应的套餐信息)
SetmealController:
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId())
.eq(setmeal.getStatus() != null,Setmeal::getStatus,1);
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> setmealList = setmealService.list(queryWrapper);
return R.success(setmealList);
}
移动端用户可以将菜品或者套餐添加到购物车。
对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;
对于套餐来说,可以直接点击十将当前套餐加入购物车。
在购物车中可以修改菜品和套餐的数量,也可以清空购物车。
梳理一下购物车操作时前端页面和服务端的交互过程:
1、点击加入购物车或者 + 按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
● 实体类ShoppingCart (直接从课程资料中导入即可)
● Mapper接口 ShoppingCartMapper
● 业务层接口ShoppingCartService;业务层实现类ShoppingCartServicelmpl
● 控制层ShoppingCartController
代码开发:
注意:不能直接添加购物车,要先查询此东西是否已加入购物车,如果已加入amount加1, 未加入则加入。
/**
* 添加购物车
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("购物车数据:{}",shoppingCart);
//设置用户id,指定当前是哪个用户的购物车数据
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
//查询当前菜品或者套餐是否在购物车中
//判断添加的是菜品还是套餐
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,currentId);
if(dishId != null){
//添加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getDishId,dishId);
}else{
//添加到购物车的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
//查询当前菜品或者套餐是否在购物车中
//SQL:select * from shopping_cart where user_id = ? and dish_id/setmeal_id = ?
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
if(cartServiceOne != null){
//如果已经存在,就在原来数量基础上加一
Integer number = cartServiceOne.getNumber();
cartServiceOne.setNumber(number + 1);
shoppingCartService.updateById(cartServiceOne);
}else{
//如果不存在,则添加到购物车,数量默认就是一
shoppingCart.setNumber(1);//也可以不写,数据库默认1
shoppingCart.setCreateTime(LocalDateTime.now()); //添加加入菜品的时间,方便后面购物车排序
shoppingCartService.save(shoppingCart);
cartServiceOne = shoppingCart;
}
return R.success(cartServiceOne);
}
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
public R<List<ShoppingCart>> list(){
log.info("查看购物车...");
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
queryWrapper.orderByDesc(ShoppingCart::getCreateTime); //根据添加到购物车的时间升序排列
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list);
}
清空购物车:
/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
public R<String> clean(){
//SQL:delete from shopping_cart where user_id = ?
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>()
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);
return R.success("清空购物车成功!");
}
移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的去结算按钮,页面跳转到订单确认页面,点击去支付按钮则完成下单操作。
用户下单业务对应的数据表为orders表和order_ detail表:
● orders:订单表
● order_ detail:订单明细表
在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:
1、在购物车中点击[去结算]按钮, 页面跳转到订单确认页面
2、在订单确认页面,发送ajax请求, 请求服务端获取当前登录用户的默认地址
3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
4、在订单确认页面点击[去支付]按钮,发送ajax请求, 请求服务端完成下单操作
开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
● 实体类 Orders、OrderDetail (直接从课程资料中导入即可)
● Mapper接口 OrdersMapper、OrderDetailMapper
● 业务层接口OrdersService、OrderDetailService
● 业务层实现类OrdersServicelmpl、OrderDetailServicelmpl
● 控制层OrdersController、 OrderDetailController
代码实现:
下单不需要传套菜信息,因为可以通过线程里用户id从购物车数据库查。外卖项目每次购买方法都是提交购物车,不需要像商城那样购物车存一堆不买的东西。
下单的难点是既要保存订单信息,还要保存订单详情页,所以要新写个submit业务方法。
使用mp的IdWorker.getId()生成雪花算法的订单id。
查询当前用户购物车数据后要遍历计算总金额,总金额类型AtomicInteger原子整型,可以保证线程安全。addAndGet是先加再获取,getAndAdd是先获取再加。
@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {
@Autowired
private ShoppingCartService shoppingCartService;
@Autowired
private UserService userService;
@Autowired
private AddressBookService addressBookService;
@Autowired
private OrderDetailService orderDetailService;
/**
* 用户下单
* @param orders
*/
@Override
@Transactional
public void submit(Orders orders) {
//获得当前用户id
Long userId = BaseContext.getCurrentId();
//查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId,userId);
List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper);
if(shoppingCarts == null || shoppingCarts.size() == 0){
throw new CustomException("购物车为空,不能下单");
}
//查询用户数据
User user = userService.getById(userId);
//查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if(addressBook == null){
throw new CustomException("用户地址信息有误,不能下单");
}
//用IdWorker设置订单号
long orderId = IdWorker.getId();//订单号
//AtomicInteger原子整型,可以保证线程安全。addAndGet是先加再获取,getAndAdd是先获取再加。
AtomicInteger amount = new AtomicInteger(0);
//购物车数据加工成订单细节
List<OrderDetail> orderDetails = shoppingCarts.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());
//叠加金额,对于不需要任何准确计算精度的数字可以直接使用float或double,但是如果需要精确计算的结果,则必须使用BigDecimal类,而且使用BigDecimal类也可以进行大数的操作。
//BigDecimal的intValue()方法把BigDecimal类型转为int型。addAndGet相当于x=x+y
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));//总金额
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(wrapper);
}
}
BigDecimal和double区别:和对于不需要任何准确计算精度的数字可以直接使用float或double,但是如果需要精确计算的结果,则必须使用BigDecimal类,而且使用BigDecimal类也可以进行大数的操作。 intValue()方法把BigDecimal类型转为int型
/**
* 用户下单
* @param orders
* @return
*/
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
log.info("订单数据:{}",orders);
ordersService.submit(orders);
return R.success("下单成功!!");
}
参考文章