创建web项目
热部署
Web入门
- Spring Boot将传统Web开发的mvc、json、tomcat等框架整合,提供了 spring-boot-starter-web组件,简化了Web应用配置。
- webmvc为Web开发的基础框架,json为JSON数据解析组件,tomcat为自带的容器依赖
控制器
RestController控制器(常用)
负责和接受HTTP请求,如果浏览器请求的只是数据,使用RestController即可。默认情况下,@RestController注解会将返回的对象数据转换为JSON格式。
Controller控制器
负责接收和处理HTTP请求。 如果请求的是页面和数据,使用@Controller注解即可 。
如下代码中返回了hello页面和name的数据,在前端页面中可以通过${name}参数 获取后台返回的数据并显示。 @Controller通常与Thymeleaf模板引擎结合使用。
URL映射和参数传递
@RestController
public class ParamsContorller {
@RequestMapping(value = "/getTest1", method = RequestMethod.GET)
public String getTest1(){
return "GET请求";
}
@RequestMapping(value = "/getTest2", method = RequestMethod.GET)
public String getTest2(String nickname){
System.out.println(nickname);
return "GET请求";
}
@RequestMapping(value = "/postTest1", method = RequestMethod.POST)
public String postTest1(){
return "POST请求";
}
@RequestMapping(value = "/postTest2", method = RequestMethod.POST)
public String postTest2(String nickname){
System.out.println("nickname:"+nickname);
return "POST请求";
}
@RequestMapping(value = "/postTest3", method = RequestMethod.POST)
public String postTest3(User user){
System.out.println("User:"+user);
return "POST请求";
}
@RequestMapping(value = "/postTest4", method = RequestMethod.POST)
public String postTest4(@RequestBody User user){ // 使用json格式请求
System.out.println("User:"+user);
return "POST请求";
}
}
数据库
- 我电脑上的数据库登录指令:mysql -uroot -p123456
- 常用指令:show databases、user 数据库名、show tables。
检查项目
创建完项目后,要及时检查maven仓库的配置,jdk的配置,项目的编码,如下图。
配置项目的pom依赖和aplication文件,在启动类中加入lombok的slf4j注解,就可以使用log.info()方法在控制台输出信息了,方便调试。
映射静态资源
- 问题:如果静态资源直接放入resource目录之下,而不是放在static或者templates目录下面,则项目启动后,浏览器无法直接访问到静态资源。
- 解决方法,编写WebMvcConfig配置文件,文件所在目录以及内容如下所示。
package com.example.reggie_take_out;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Slf4j
@SpringBootApplication
public class ReggieTakeOutApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieTakeOutApplication.class, args);
log.info("项目启动成功。。。");
}
}
用户登录退出和拦截器功能的实现
用户登录功能
@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::getUsername, employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);
//3、如果没有查询到则返回登灵失败结果
if(emp == null){
return R.error("登陆失败");
}
//4、密码比对,如果不一致则返回登录失败结果
if(!emp.getPassword().equals(password)){
return R.error("密码错误");
}
//5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
if(emp.getStatus() == 0){
return R.error("账号已禁用");
}
//6、登录成功,将员工id存入Session并返回登录成功结果
request.getSession().setAttribute("employee", emp.getId());
return R.success(emp);
}
用户退出
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清理session中保存的员工ID
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
拦截器
拦截器和过滤器各自适用的场景
拦截器通常在业务处理层面进行操作,它们更接近业务逻辑,可以对请求进行细粒度的控制和处理。例如,权限验证是一个常见的业务处理需求,拦截器可以拦截请求并检查用户的权限,以确保只有具有访问权限的用户可以执行相应的操作。另外,日志记录也是拦截器常见的应用场景,通过拦截请求和响应,可以记录请求的细节和响应的结果,方便问题的排查和系统的监控。
过滤器则更多地关注于请求和响应的处理和过滤。它们通常在请求的前后进行操作,用于对请求和响应进行过滤、修改或转换。请求过滤是过滤器的常见应用场景,可以用于对请求进行预处理、验证和过滤,例如检查请求的来源、请求的参数等。同时,过滤器还可以对请求和响应的编码进行转换,以确保请求和响应的正确编码格式。
综上所述,拦截器和过滤器在不同的层面和目的上有所不同,拦截器更偏向于业务处理和控制,而过滤器更专注于对请求和响应的处理和过滤。
//注意在启动类中添加注解 @ServletComponentScan
@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;
log.info("拦截到请求:{}",request.getRequestURI());
// 1、获取本次请求的URI
String requestURI = request.getRequestURI();
//不需要检查的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// 2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
// 3、如果不需要处理,则直接放行
if(check == true){
filterChain.doFilter(request, response);
return;
}
// 4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
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){
if(PATH_MATCHER.match(url, requestURI) == true ){
return true;
}
}
return false;
}
}
由于dofilter的返回类型为void,所以不能通过return R.error("错误信息")向客户端返回信息,可使用response对象向客户端返回信息:
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
新增员工功能
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee){
log.info("新增员工,员工信息:{}", employee.toString());
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("新增员工成功");
}
员工查询
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
log.info("分页查询{} {} {}", page, pageSize, name);
//构造分页构造器
Page pageInfo = new Page(page, pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
//添加过滤条件
if(name != null){
queryWrapper.like(Employee::getName, name);
}
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
需要注意的是,如果没有配置 MyBatis Plus 的分页插件,意味着分页功能将不会启用。 那么调用 employeeService.page(pageInfo, queryWrapper) 方法时,将无法进行分页查询, 而是会返回所有符合条件的结果,而不是按照指定的分页参数进行分页查询。
启用和禁用员工
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> updata(HttpServletRequest request, @RequestBody Employee employee){
log.info(employee.toString());
long empid = (long)request.getSession().getAttribute("employee");
employee.setUpdateUser(empid);
employee.setUpdateTime(LocalDateTime.now());
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
注意,ID为long类型,有19位,页面中js处理long型数字只能精确到前16位,所以最终通过ajax请求提交给服务端的时候id发生了变化。
解决办法是,将返回给客户端的数据转换为JSON格式,具体做法是在WebMvcConfig配置文件中添加扩展mvc框架的消息转换器,如下所示。
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器 底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0, messageConverter);
super.extendMessageConverters(converters);
}
公共字段的填充
- 在实体类的相应字段上加入如下的注解。
//在插入时生效
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//在插入和更新时生效
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
- 在MyMetaObjecthandler类中无法获得HttpSession对象,我们用TreadLocal来解决该问题,他是JDK中提供的一个类。
- 如下为代码实现。
- 首先构建基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
package com.example.reggie_take_out.common; /** * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id */ public class BaseContext { private static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id){ threadLocal.set(id); } public static Long getCurrenId(){ return threadLocal.get(); } }
- 从filter中将用户id存入threadLocal提供的存储空间当中
// 4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
//将用户id存入threadLocal提供的存储空间当中
BaseContext.setCurrentId((Long)request.getSession().getAttribute("employee"));
filterChain.doFilter(request, response);
return;
}
- 在MyMetaObjecthandler类中获取用户ID
/** * 更新操作自动填充 * @param metaObject */ @Override public void updateFill(MetaObject metaObject) { log.info("公共字段自动填充[update]"); log.info((metaObject.toString())); metaObject.setValue("updateTime", LocalDateTime.now()); metaObject.setValue("updateUser", BaseContext.getCurrenId()); }
代码开发的结构
删除分类
删除分类时要注意,判断被删除的分类是否关联了菜品或者套餐,因此就不能在CategoryController直接使用categoryService.removeById( id )对分类进行删除。
在CategoryService接口中定义remove方法,实现关联删除的逻辑业务判断,在CategoryServiceImpl中具体的实现方法如下。
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据id进行删除,删除之前要做判断
* @param id
*/
@Override
public void remove(Long id) {
//查询当前分类是否关联菜品,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
int count1 = dishService.count(dishLambdaQueryWrapper);
if(count1 >0) {
throw new CustomException("当前分类下关联了菜品,不能删除");
}
//查询当前分类是否关联套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if(count2 >0) {
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//正常删除业务
super.removeById(id);
}
}
自定义异常类的代码如下。
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}
全局异常处理器的代码如下。
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
文件上传
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upLoad(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会被删除
log.info(file.toString());
//原始文件名
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.mkdir();
}
//将临时文件转存到指定位置
try {
file.transferTo(new File(basePath+filename));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(filename);
}
文件下载
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
//输入流,通过输入流读取文件内容
try {
FileInputStream fileInputStream = new FileInputStream(basePath + name);
//输出流,通过输出流将文件写回浏览器,在浏览器展示图片
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("/image/jpeg");
//用于存储文件内容的缓冲区。
int len = 0;
byte[] bytes = new byte[1024];
//输入流中读取文件内容,并将其写入输出流,直到文件的所有内容都被读取完毕。
while( (len = fileInputStream.read(bytes)) != -1){
//这行代码将缓冲区中的内容写入输出流,并通过flush()方法将数据刷新到浏览器。
outputStream.write(bytes, 0, len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
新增菜品
新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish,dish_flavor。
因此在DishService接口中自定义方法void saveWithFlavor(DishDto dishDto),用于新增菜品,同时插入菜品对应的口味数据。DishServiceImpl实现类中的代码如下。
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品,同时保存相应的口味数据
* @param dishDto
*/
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//this.save(dishDto) 将调用 DishServiceImpl 类中的 save 方法,将 dishDto 对象保存到数据库中。
this.save(dishDto);
Long id = dishDto.getId();//菜品id
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
//赋值菜品口味的相应id
flavors = flavors.stream().map((item) -> {
item.setDishId(id);
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
}
注意,saveWithFlavor方法中操作了两张表,因此需要方法上加入@Transactional注解,并且在启动类中加上@EnableTransactionManagement 注解。
菜品展示
问题:页面展示需要分类的名称,但dish表中只有分类的id,因此总体思路是将dish表中的分类id取出来,用分类id在分类表中查询分类名称。
具体步骤:
- 使用DTO(Data Transfer Object)数据传输对象,用于在不同层之间传输数据。DTO结构如下。
@Data public class DishDto extends Dish { private List<DishFlavor> flavors = new ArrayList<>(); private String categoryName; private Integer copies; }
DTO继承自Dish类,并且DTO的categoryName属性可用于前端分类名称的展示。
新增套餐
- 传入的Json数据,不仅有套餐数据,还有对应套餐的菜品数据,因此需要两个实体来接受客户端发送的Json数据,分别是套餐实体Setmeal和套餐菜品实体SetmealDish,创建一个setmealDto实体,Dto表继承自套餐实体,并将元素类型为套餐菜品实体的list集合作为字段属性。
- 然后在套餐的service中,分别保存两张表,分别是套餐表和套餐菜品表。
- 对于套餐表的保存,直接setmealService.save(setmealDto)即可,因为Dto实体直接继承自套餐实体。
- 对于套餐菜品表的保存,需要先将套餐实体的id赋给Dto实体中每一个套餐菜品实体,然后再通过setmealDishService.saveBatch(setmealDishs)。
@Autowired private SetmealDishService setmealDishService; /** * 新增套餐,同时需要保存套餐和菜品的关联关系 * @param setmealDto */ @Transactional //两张表 要么全部成功 要么全部失败 public void saveWithDish(SetmealDto setmealDto) { //保存套餐的基本信息,操作setmeal,执行insert操作 this.save(setmealDto); List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes(); setmealDishes.stream().map((item) -> { item.setSetmealId(setmealDto.getId()); //赋值套餐id return item; }).collect(Collectors.toList()); //保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作 setmealDishService.saveBatch(setmealDishes); }
套餐分页查询
- 因为套餐实体Setmeal中的分类名称为id值,因此要用SetmealDto数据传输对象(Data Transfer Object),Dto继承自套餐实体,并且有分类名称字段。
- 构造分页对象,分别是Setmeal分页构造器对象和SetmealDto分页构造器对象。
- 创建LambdaQueryWrapper对象,添加查询条件,根据name进行like模糊查询,添加排序条件,根据更新时间降序排列,调用setmealService的page方法进行分页查询。
- 对象拷贝,将pageInfo对象拷贝到dtoPage对象,但pageInfo对象的records列表的元素类型为Setmeal,而我们需要的元素类型为SetmealDto,因此records字段不需要被拷贝。
- 为了使dtoPage的records列表字段的元素类型为SetmealDto,我使用流式操作对pageInfo
的records
列表进行处理。具体处理为创建SetmealDto对象,将Setmeal拷贝到SetmealDto对象,再根据分类id查询分类对象,然后将分类名称赋给SetmealDto对象。 - 处理完成后,得到一个以SetmealDto为元素类型的list集合,将该集合赋给dtoPage的records字段,最终将dtoPage对象返回给客户端即可。
- 代码如下。
/** * 套餐分页查询 * @param page * @param pageSize * @param name * @return */ @GetMapping("/page") public R<Page> page(int page, int pageSize, String name){ //分页构造器对象 Page<Setmeal> pageInfo = new Page<>(page,pageSize); Page<SetmealDto> dtoPage = new Page<>(); LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>(); //添加查询条件,根据name进行like模糊查询 queryWrapper.like(name != null,Setmeal::getName,name); //添加排序条件,根据更新时间降序排列 queryWrapper.orderByDesc(Setmeal::getUpdateTime); 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); //分类id Long categoryId = item.getCategoryId(); //根据分类id查询分类对象 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); }
套餐删除
- 删除两张表,需要在service层中处理该逻辑,并且加上@Transaction注解,启动类加上@EnableTransactionManagement注解。
- 处理逻辑为先判断表中是否有待删除但status为1的套餐,若有,则返回异常,反之正常删除套餐表中的待删除套餐。
- 再使用LambdaQueryWrapper从套餐菜品表中查询对应套餐id的记录,执行service.remove操作删除对应的记录即可。
/** * 删除套餐,同时需要删除套餐和菜品的关联数据 * @param ids */ @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("套餐正在售卖中,不能删除"); } //如果可以删除,先删除套餐表中的数据---setmeal this.removeByIds(ids); //delete from setmeal_dish where setmeal_id in (1,2,3) LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids); //删除关系表中的数据----setmeal_dish setmealDishService.remove(lambdaQueryWrapper); }
功能开发流程(以购物车开发为例)
- 需求分析
- 数据模型
- 代码开发---梳理前后交互过程
- 代码开发---准备工作
- 代码开发(略)
- 功能测试(略)
用户下单
- 操作多张表,在service接口中扩展方法。
- 业务逻辑如下:
- 查询用户id,根据用户id获得用户数据和购物车数据,根据客户端传来的收货地址查询地址数据,使用IdWorker.getId() 方法创建一个订单号。
- 对购物车数据创建一个Stream流,使用map方法对流中的每个元素进行映射操作,设置订单明细实体类的值,并计算订单的总额,最终向订单明细表插入数据。
- 设置订单实体类的值,其订单金额由上一步计算得来,最终向订单表插入数据。
- 最后删除购物车数据。
- 代码如下。
@Autowired private ShoppingCartService shoppingCartService; @Autowired private UserService userService; @Autowired private AddressBookService addressBookService; @Autowired private OrderDetailService orderDetailService; /** * 用户下单 * @param orders */ @Transactional public void submit(Orders orders) { //BaseContext获取当前用户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); //查询地址数据 AddressBook addressBook = addressBookService.getById(orders.getAddressBookId()); if(addressBook == null){ throw new CustomException("地址信息为空,不能下单"); } long orderId = IdWorker.getId(); //订单号 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()); //BigDecimal:用于表示高精度的十进制数值,支持浮点数的精确计算。 //BigInteger:用于表示任意精度的整数,支持整数的精确计算。 amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue()); return orderDetail; }).collect(Collectors.toList()); orderDetailService.saveBatch(orderDetails); //向订单表插入数据,一条数据 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())); //清空购物车数据 shoppingCartService.remove(wrapper); }