目前学习了项目的后端功能开发,针对前段时间的学习进行系统总结提升,根据项目开发流程总结
1. 资料中所给的前端界面是存放在/backend和/front之中,而springboot自带的是static,故需要做一层映射才可以访问到
public class webMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射");
//registry.addResourceHandler("通过前端访问的路径").addResourceLocations("需要映射的资源的位置");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
2. 为了保证前后端数据统一性,避免不同方法返回值之间的类型出现较大差异,创建了一个新的类R,思路就是,遇到困难就加一层,加一层解决不了就再加一层(后面的dto思路和此相似)
//使用lombok简化开发
@Data
public class R<T> {
//编码:1成功,0和其它数字为失败
private Integer code;
//错误信息
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;
}
3. 登录界面开发
需求分析:前端传入数据之后,需要后端去数据库查询信息来判断该用户是否注册,并给与其一个唯一的sessionID用于标记用户已经登录,同时需要一个过滤器来判断用户是否登录,防止未登录用户进行敏感操作。
开发思路:常规三层,此处使用的是springboot + mybatis-plus开发
mapper和service层使用框架即可。
controller层:使用rest风格进行开发
@PostMapping("/login")
//@RequestBody的作用是将前端的json对象传入函数
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
String password = employee.getPassword();
//md5加密
password = DigestUtils.md5DigestAsHex(password.getBytes());
//条件查询标准格式,使用的是LambdaQueryWrapper类
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() == 0){
return R.error("您的账号已被封号");
}
//把id放入session
request.getSession().setAttribute("employee",emp.getId());
//此处是因为后面测试时发现ThreadLocal获取到的id是null,故在此做出调整
BaseContext.setCurrentId(emp.getId());
return R.success(emp);
}
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清理Session中保存的当前登录员工的id
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
4.员工操作:
新增操作:-->调用自带的插入操作即可
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
//设置初始密码123456,需要进行md5加密处理
//DigestUtils.md5DigestAsHex使用该类进行md5加密
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employeeService.save(employee);
return R.success("新增员工成功");
}
分页操作:登录进去过后发现前端发出了一个/page?page=xxx&pageSize=xxx,说白了就是select *操作加个limit限制查询条数,条件查询,可能有条件,故还要加个参数,有就调用没有就摆着
@GetMapping("/page")
//返回对象是个page,是在com.baomidou.mybatisplus.extension.plugins.pagination.Page包下
public R<Page> page(int page,int pageSize,String name){
Page pageInfo = new Page<>(page,pageSize);
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//like模糊查询
queryWrapper.like(!StringUtils.isEmpty(name),Employee::getName,name);
//排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//去数据库查然后把结果放到pageInfo最后返回出来
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
使用mp的分页查询还需配置一个mp分页拦截器
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
更新操作:(禁用其实也是更新操作,只是修改了状态位)
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
5. 对象映射器: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);
}
}
6.公共字段自动填充:
需求分析:不同实体都有4个公共字段,createTime,createUser,updateTime,updateUser,在实现其方法时如果每个方法都要手动去添加就太过于麻烦,于是使用mp提供的自动填充,去实现MetaObjectHandler接口,在实体类中需要自动填充的属性上加注解@TableField(在调用数据库相关什么方法的时候自动填充)
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
//插入时自动填充
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充【insert】。。。");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
// log.info("createTime=" + LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
// log.info("updateTime=" + LocalDateTime.now());
metaObject.setValue("createUser",BaseContext.getCurrentId());
// log.info("createUser=" + BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
// log.info("updateUser=" + BaseContext.getCurrentId());
}
//更新时自动填充
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充【update】。。。");
log.info(metaObject.toString());
metaObject.setValue("updateTime",LocalDateTime.now());
// log.info("updateTime" + LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
// log.info("updateUser=" + BaseContext.getCurrentId());
}
}
7.全局异常处理
需求分析:要返回一个客户看得懂的报错,就需要我们自己定制一个
public class GlobalExceptionHandler {
// 处理和数据库相关的报错
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
//处理用户层的异常
@ExceptionHandler()
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
}
自定义的用户异常类
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}
8.分类管理:
需求分析:和员工类似,需要实现基础curd操作,菜品分类和和套餐分类其实也是状态码不同,这些均是在前端的请求中实现,后端只用开发更新操作即可。除删除操作外其他操作与上面思路一致。
删除操作需要注意到的点是,若需要删除的分类下绑定了菜品or套餐的话那应该不可删除(不然会出现某菜品绑定的是已经删除的分类),需要去查询菜品和套餐的表确认当前分类下的菜品和套餐为空
需要我们手动实现删除方法(是特化的方法无法使用mp自带的删除),其底层逻辑就是通过两次条件查询确认该分类没有关联菜品和套餐
public void remove(Long id) {
log.info("id=" + id);
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
long count1 = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
long count2 = setmealService.count(setmealLambdaQueryWrapper);
//查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
if (count1 > 0) {
//已经关联菜品,抛出一个业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
if (count2 > 0) {
//已经关联套餐,抛出一个业务异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//正常删除分类
super.removeById(id);
}
9.菜品管理
需求分析:通过页面可以看到我们需要实现的方法,针对单个菜品和多个菜品的编辑,停售和删除,新增单个菜品。
此处接触到第一个dto类,目的是补全新增操作中需要的分类,口味列表(遇到问题就加一层)
DTO:用于表现层和应用层之间的数据交互,简单来说Model面向业务,我们是通过业务来定义Model的。而DTO是面向界面UI,是通过UI的需求来定义的。 通过DTO我们实现了表现层与Model之间的解耦,表现层不引用Model
通过新增页面可以发现主要有两个点,菜品分类采用的是下拉框,说明我们需要在分类的controller内实现查询功能,得到分类的列表。
@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();
return R.success(list);
}
发现存在图片上传的功能,还有,页面上的图片显示是将图片下回浏览器实现(调用download方法),此处直接给出代码
public class CommonController {
@Value("${waimai.path}")
private String basePath;
@PostMapping("/upload")
public R<String> upload(MultipartFile file) throws IOException {
//拿到文件后缀名,方便下一步的拼接
String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
//利用生成的随机的UUID将图片重新命名,防止出现两张名字相同的图片而导致报错
String fileName = UUID.randomUUID().toString() + suffix;
//校验图片存放路径是否存在,不存在就重新创建一个
File dir = new File(basePath);
if(!dir.exists()){
dir.mkdirs();
}
// 将图片转存到指定路径
file.transferTo(new File(basePath + fileName));
return R.success(fileName);
}
@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/jpeg");
// 通过缓冲区的方式读取
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) {
e.printStackTrace();
}
}
}
菜品口味的选择,查询后返回一个dto对象
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> flavors = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(flavors);
return dishDto;
}
新增操作:
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId = dishDto.getId();//菜品id
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> {
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
更新操作:
public void updateWithFlavor(DishDto dishDto) {
//更新dish表基本信息
this.updateById(dishDto);
//清理当前菜品对应口味数据---dish_flavor表的delete操作
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) -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
批量停售和删除操作:前端多选时相当于传入id数组
//停售起售菜品
@PostMapping("/status/{status}")
public R<String> sale(@PathVariable int status,
String[] ids){
for(String id: ids){
Dish dish = dishService.getById(id);
dish.setStatus(status);
dishService.updateById(dish);
}
return R.success("修改成功");
}
//删除菜品
@DeleteMapping
public R<String> delete(String[] ids){
for (String id:ids) {
dishService.removeById(id);
}
return R.success("删除成功");
}
套餐管理:
需求分析:也是需要实现套餐的curd,和菜品管理相似。
整个后台界面开发就基本完成