在开发业务功能前,先将需要用到的类和接口基本结构创建好:
SetmealDish—实体类
package com.itzq.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 套餐菜品关系
*/
@Data
public class SetmealDish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//套餐id
private Long setmealId;
//菜品id
private Long dishId;
//菜品名称 (冗余字段)
private String name;
//菜品原价
private BigDecimal price;
//份数
private Integer copies;
//排序
private Integer sort;
@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;
//是否删除
private Integer isDeleted;
}
DTO SetmealDto—数据传输对象
package com.itzq.reggie.dto;
import com.itzq.reggie.entity.Setmeal;
import com.itzq.reggie.entity.SetmealDish;
import lombok.Data;
import java.util.List;
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName;
}
SetmealDishMapper接口
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itzq.reggie.entity.SetmealDish;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}
SetmealDishService接口
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itzq.reggie.entity.SetmealDish;
public interface SetmealDishService extends IService<SetmealDish> {
}
SetmealDishservicelmpl实现类
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.SetmealDish;
import com.itzq.reggie.mapper.SetmealDishMapper;
import com.itzq.reggie.service.SetmealDishService;
import org.springframework.stereotype.Service;
@Service
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper,SetmealDish> implements SetmealDishService {
}
SetmealController控制层
package com.itzq.reggie.controller;
import com.itzq.reggie.service.SetmealDishService;
import com.itzq.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishService setmealDishService;
}
在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可
启动项目,进入套餐管理,点击新建套餐,会发现页面发送的请求未被服务端接收
在DishController类中,添加list方法
注意:需要添加额外的查询条件,只查询status为1的数据,表示该菜品为起售状态,才能被加入套餐中,供用户选择
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//添加条件,查询状态为1(1为起售,0为停售)的菜品
queryWrapper.eq(Dish::getStatus,1);
List<Dish> list = dishService.list(queryWrapper);
//添加排序条件
return R.success(list);
}
在SetmealController类中添加save方法
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("数据传输对象setmealDto:{}",setmealDto.toString());
return null;
}
debug方式重启项目,来到添加套餐页面,输入数据,点击保存
跳转到服务端,查看是否接收到客服端提交的数据,发现数据成功接收
在SetmealService接口,添加saveWithDish方法
实现类SetmealServicelmpl,实现接口添加的方法,并向方法中添加代码逻辑
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal,执行insert操作
save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes = setmealDishes.stream().map((item) -> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
//保存套餐和菜品的关联信息
setmealDishService.saveBatch(setmealDishes);
}
在SetmealController控制层的save方法中,调用saveWithDish方法,将数据保存至数据库
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("数据传输对象setmealDto:{}",setmealDto.toString());
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可
SetmealController类中,添加list方法
@GetMapping("/page")
public R<Page> list(int page,int pageSize,String name){
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
Page<SetmealDto> dtoPage = new Page<>();
//构造查询条件对象
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(name != null, Setmeal::getName, name);
//操作数据库
setmealService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(item, setmealDto);
//获取categoryId
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if (category != null) {
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
注意
在套餐管理界面,套餐分类字段显示的是categoryId对应的中文,但在数据库里查询到的是categoryId,因此需要利用categoryId查询到categoryName,并赋值给数据传输对象SetmealDto
启动项目,点击套餐管理,前端发送ajax请求,服务端接收前端发出的请求,并做相应的处理,向页面返回数据
数据成功回显到页面
在开发代码之前,需要梳理一下删除套餐时前端页面和服务端的交互过程:
注意
在SetmealController中添加delete方法
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids为:",ids);
return null;
}
跳转到服务端,查询ids可知服务端成功接收到前端传来的数据信息
在SetmealService接口中添加removeWithDish方法
在SetmealServicelmpl实现类中实现对应接口中添加的方法
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
//select count(*) from setmeal where ids in(1,2,3) and status = 1
//查询套餐状态,确定是否可以删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
int count = super.count(queryWrapper);
if (count > 0){
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除");
}
//如果可以删除,先删除套餐表中的数据
super.removeByIds(ids);
//删除关系表中的数据
//delete from setmeal_dish where setmeal_id in(1,2,3)
LambdaQueryWrapper<SetmealDish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(dishLambdaQueryWrapper);
}
在SetmealController中完善代码—调用removeWithDish方法,实现套餐数据删除成功
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids为:",ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
注意:将setmeal表中status字段值改为0—为停售状态,方便测试
常用短信服务:
阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。
应用场景:
打开浏览器,登录阿里云—网址:https://cn.aliyun.com/
开通短信服务之后,进入短信服务管理页面,选择国内消息菜单,我们需要在这里添加短信签名
什么是短信签名?
添加短信签名方式
注意:个人申请签名是有一定的难度的,所以我们只需要了解一下使用短信签名的具体流程
切换到【模板管理】标签页:
短信模板包含短信发送内容、场景、变量信息
每一个被设置好的模板有一个短信模板详情,模板详情包含了模板的6条信息
AccessKey 是访问阿里云 API 的密钥,具有账户的完全权限,我们要想在后面通过API调用阿里云短信服务的接口发送短信,那么就必须要设置AccessKey。
光标移动到用户头像上,在弹出的窗口中点击【AccessKey管理】︰
进入到AccessKey的管理界面之后,提示两个选项:
区别:
创建子用户AccessKey。
因为我们只需要使用短信服务,所以我们在搜索框输入sms,点击需要添加的权限
授权成功
表示当前我们只给该用户授予了两个权限,即使用户名和密码泄露,其他人也只能调用短信服务
授权成功之后就可以用代码的方式来调用短信服务
AccessKey泄露需要进行的处理
使用阿里云短信服务发送短信,可以参照官方提供的文档即可。
具体开发步骤:
<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>
package com.itzq.reggie.utils;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}
}
查看短信服务产品文档的java SDK,了解短信服务java SDK的使用方法以及示例
为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能
手机验证码登录的优点:
登录流程:
注意:通过手机验证码登录,手机号是区分不同用户的标识
通过手机验证码登录时,涉及的表为user表,即用户表。结构如下:
注意:
在开发代码之前,需要梳理一下登录时前端页面和服务端的交互过程:
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类user
package com.itzq.reggie.entity;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
/**
* 用户信息
*/
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//姓名
private String name;
//手机号
private String phone;
//性别 0 女 1 男
private String sex;
//身份证号
private String idNumber;
//头像
private String avatar;
//状态 0:禁用,1:正常
private Integer status;
}
Mapper接口UserMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itzq.reggie.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
业务层接口UserService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itzq.reggie.entity.User;
public interface UserService extends IService<User> {
}
业务层实现类UserServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.User;
import com.itzq.reggie.mapper.UserMapper;
import com.itzq.reggie.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServicelmpl extends ServiceImpl<UserMapper, User> implements UserService {
}
控制层UserController
package com.itzq.reggie.controller;
import com.itzq.reggie.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
}
工具类SMSutils、ValidateCodeutils(直接从课程资料中导入即可)
package com.itzq.reggie.utils;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}
}
package com.itzq.reggie.utils;
import java.util.Random;
/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
前面我们已经完成了LoginCheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的请求需要在此过滤器处理时直接放行。
在LoginCheckFilter类中的urls数组中,添加下面两条数据
启动项目,在浏览器中输入访问地址:http://localhost:8080/front/page/login.html
注意:
使用h5开发的,自适应手机屏幕的大小,在浏览器中,需使用浏览器的手机模式打开,下面为具体步骤:
在LoginCheckFilter类下添加代码,判断用户是否登录
//4-2 判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("user") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
Long userId = (Long)request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request,response);
return;
}
在给出的前端资源资料中,login.html是被后面章节修改过的,因此我们需要重新导入front目录
在给出的代码目录中,找到day05下的front目录,复制该目录,将项目中的front目录覆盖
修改完成后,访问前端页面可能出现问题,因此我们需要重启项目,删除浏览器中的数据
若还是不能解决,关闭idea,重新打开idea代码编辑器
在用户登录界面中,输入电话号码,点击获取验证码,页面会发送一个ajax请求
请求地址:http://localhost:8080/user/sendMsg
请求方式:POST
注意
在UserController控制层中,添加sendMsg方法
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){
//获取手机号
String phone = user.getPhone();
if (StringUtils.isNotEmpty(phone)){
//生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode4String(4);
log.info("code={}",code);
//调用阿里云提供的短信服务API完成短信发送
//SMSUtils.sendMessage("瑞吉外卖","",phone,code);
//需要将生成的验证码保存到session
session.setAttribute(phone,code);
return R.success("短信发送成功");
}
return R.error("短信发送失败");
}
重启项目,在浏览器地址栏中输入地址:http://localhost:8080/front/page/login.html
后端获取到生成的验证码
来到用户登录界面,按住F12,点击登录按钮,页面发送ajax请求,查看请求的地址以及方式
在UserController控制层类中,添加login方法,测试服务端是否可以接受前端提交的数据
@PostMapping("/login")
public R<String> login(@RequestBody Map map, HttpSession session){
log.info(map.toString());
return R.error("短信发送失败");
}
重启项目,来到用户登录界面,输入正确手机号,点击获取验证码,查看服务端日志打印出的验证码信息,输入验证码,点击登录,前端发送ajax请求
前端发送ajax请求,服务端通过日志打印出手机和验证码信息,接收数据成功
注意
在login方法中,接收数据的参数类型为Map类型,也可以重新定义一个UserDto(用户类数据传输对象)用来接收数据
完善用户登录代码
@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中获取保存的验证码
String codeInSession = session.getAttribute(phone).toString();
//进行验证码的对比(页面提交的验证码和session中保存的验证码)
if (code != null && code.equals(codeInSession)){
//如果比对成功,则登录成功
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(queryWrapper);
if (user == null){
//判断当前手机号是否为新用户,如果是新用户则自动完成注入
user = new User();
user.setPhone(phone);
userService.save(user);
}
return R.success(user);
}
return R.error("登录失败");
}
页面跳转到用户登录界面
在login方法中,添加代码
目的:将userId保存到session当中,前端页面发送请求到服务端,经过filter过滤器,判断用户状态为已登录状态,页面将不会跳转到用户登录界面