一, 软件开发整体介绍
软件开发流程
- 需求分析
- 设计
- 产品文档,UI界面设计,概要设计,详细设计,数据库设计
- 编码
- 测试
- 上线运维
角色分工
- 项目经理:对整个项目负责,任务分配、把控进度
- 产品经理:进行需求调研,输出需求调研文档、产品原型等
- UI设计师:根据产品原型输出界面效果图
- 架构师:项目整体架构设计、技术选型等
- 开发工程师:代码实现
- 测试工程师:编写测试用例,输出测试报告
- 运维工程师:软件环境搭建、项目上线
环境
软件环境
- 开发环境
- 测试环境:专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
- 生产环境(production):即线上环境,正式提供对外服务的环境
二,瑞吉外卖项目介绍
展示
技术选型
功能架构
角色
三,开发环境搭建
数据库环境搭建
-
建立reggie库
-
导入表结构
- 数据表
maven环境配置
导入pom文件
- 指定jdk版本
```xml
<properties>
<java.version>1.8</java.version>
</properties>
- 导入配置文件
```yml
server:
port: 8080
spring:
application:
#应用的名称:可选
name: reggie_take_out
datasource:
druid:
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: root
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
1
[!danger]
默认情况下是访问不到这些文件夹里面的静态资源的,所以需要配置资源映射
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport{
// 设置静态资源映射
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
默认配置的资源映射 [[springboot-web#资源处理的默认规则]] (可以参考)
四,后台登录功能开发
需求分析
前端的登录按键和 handleLogin这个方法绑定,这个方法调用loginApi上产loginForm中的信息也就是账号密码,如果返回1就代表登陆成功,然后把数据转成JSON的格式保存到主页面,如果返回的不是1的话就设置一个错误信息,并且把登陆中改成登录
function loginApi(data) {
return $axios({
'url': '/employee/login',
'method': 'post',
data
})
}
function logoutApi(){
return $axios({
'url': '/employee/logout',
'method': 'post',
})
}
代码开发
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
@Service
public interface EmployeeService extends IService<Employee> {
}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
- 导入返回结果类,服务端响应的所有结果都会包装成此类型返回给前端页面
public class R<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
第一个<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;
}
在Controller中创建登录方法
处理逻辑如下;
- 将页面提示的密码password进行md5加密
- 根据页面提交的username查询数据库
- 如果没有查询到返回登陆失败结果
- 密码比对,比对不一致返回登陆失败
- 查看员工状态,如果为已禁用状态,则返回已禁用结果
- 登陆成功,将员工的id存入session并返回登陆成功结果
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
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()==0){
return R.error("账号已禁用");
}
request.getSession().setAttribute("emp",emp.getId());
return R.success(emp);
}
}
完善登录功能-问题分析并创建过滤器
实现步骤:
1、创建自定义过滤器LoginCheckFilter
2、在启动类上加入注解@ServletComponentScan==(开启主件扫描去扫描过滤器)==
3、完善过滤器的处理逻辑
代码实现
过滤器具体的处理逻辑:
- 获取本次请求的url
- 判断本次请求是否需要处理
- 如果不需要处理,则直接放行
- 判断登录状态,如果已经登录,则直接放行
- 如果未登录则返回未登录结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A7lJIMV4-1686059761210)(null)]
前端的逻辑是:如果msg是NOTLOGIN那么久自动跳转到登录页面
@Slf4j
@WebFilter( filterName="LoginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
public static final AntPathMatcher PATH_MATHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String uri = request.getRequestURI();
String[] urls= new String[] {
"/employee/login",
"/employee/logout",
"/backend/**",
"front/**"
};
if(check(urls,uri)){
filterChain.doFilter(request,response);
return;
}
if (request.getSession().getAttribute("employee")!=null){
filterChain.doFilter(request,response);
return;
}
//controller里面直接返回R对象是因为框架自动帮我们转换成Json了,这里是filter需要我们手动转成Json数据传回前端
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
log.info("拦截到请求",uri);
}
public boolean check(String[] urls,String requesturl){
for (String url : urls) {
boolean match = PATH_MATHER.match(url,requesturl);
if (match) {
return true;
}
}
return false;
}
}
五, 后台系统退出功能
需求分析
员工登录成功后,页面跳转到后台系统首页面(backend/index.html),此时会显示当前登录用户的姓名:
如果员工需要退出系统,直接点击右侧的退出按钮即可退出系统,退出系统后页面应跳转回登录页面
代码开发
用户点击页面中退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST。我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑:
1、清理Session中的用户id
2、返回结果
/**
* 员工退出
* @param request
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request) {
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
前端判断code为1进行页面跳转
前端进行页面切换部分
调用的是menuHandle方法
用iframe的方式显示一个页面
页面上有个iframe相当月页面上有一个坑,用来展示页面的,外面的来源那就是:src里面的url
六,新增员工
需求分析
后台系统中可以查看管理员工信息,通过新增员工来添加后台系统用户,点击[添加员工]跳转到新增页面,如下
代码开发
执行过程
- 页面发送ajax请求,将新增员工中输入的数据信息以json格式发送到服务端
- 服务端Controller接收页面提交的数据并调用Service将数据进行保存
- Service调用Mapper操作数据库,保存数据
前端:员工管理页面点击添加员工按钮,会为我们切换到添加员工那个页面(iframe的方式)
@PostMapping
public R<String> save(@RequestBody Employee employee,HttpServletRequest request){
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("新增员工成功");
}
异常处理
前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常:
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'zhangsan’for key ‘idx_username’
此时需要我们的程序进行异常捕获,通常有两种处理方式:
1、在Controller方法中加入try、catch进行异常捕获
2、使用异常处理器进行全局异常捕获
一般来说对每个异常都进行单独捕获未免太麻烦,所以用异常处理器进行全局异常捕获
代码
/**
* 全局异常捕获
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
//@ResponseBody的作用其实是将java对象转为json格式的数据。
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 进行异常处理
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHander(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
return R.error("失败了");
}
}
测试
完善异常处理器
使异常处理器在捕获字段名重复这个异常的时候传回的错误信息可以包含重复的字段值
public R<String> exceptionHander(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("失败了");
}
总结
1、根据产品原型明确业务需求
2、重点分析数据的流转过程和数据格式
3、通过debug断点调试跟踪程序执行过程
七,员工信息分页查询
需求分析
系统中的员工很多时,如果在一个页面全部显示会很乱,不便于查看,所以一般系统中都会以分页的形式来展示列表数据
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
- 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
- 服务端Controller接收页面提交的数据并调用Service查询数据
- Service调用Mapper操作数据库,查询分页数据
- Controller将查询到的分页数据响应给页面
- 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
前端:
created是系统刚开始就执行的方法
设置MabitsPlus的配置
@Configuration
public class MybatisPlusConfig {
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
Controller
/**
* 分页查询
* @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 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);
//自动给我们的page封装好了
return R.success(pageInfo);
}
分页请求发送的几个地方
[!important]
返回的R里面的data是一种名叫Page的数据结构,这个数据结构里面自动封装了EmpLoyee数组
八, 禁用员工账号
代码开发
- 页面发送ajax请求,将参数(id,status)提交到服务端
- 服务端Controller接收页面提交的数据,并将调用Service更新数据
- Service调用Mapper操控服务器
初步代码
@PutMapping
public R<String> update(@RequestBody Employee employee ){
log.info(employee.toString());
return null;
}
当点击禁用,传送来的信息
功能测试
没有报错,但是功能没有实现
观察控制台输出sql
和数据库里面的id并不一致
代码修复
原因是js对数据处理的时候丢失了精度,导致提交结果和数据库中的id不一致
如何解决这个问题
我们可以在服务端给页面响应JSON数据时进行处理,将long型统一转换成String字符串
具体实现步骤:
1)提供对象转换器Jackson0bjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
2)在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
/**
* 对象映射器:基于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);
}
}
/**
* 扩展mvc消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建消息转换器对象
MappingJackson2CborHttpMessageConverter converter = new MappingJackson2CborHttpMessageConverter();
//创建对象转换器底层使用Jackson将 java对象转换成json
converter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器将对象追加到mvc框架的转换器集合中
converters.add(0,converter);
}
九,编辑员工信息
需求分析
管理员可对于用户信息进行编辑
代码开发
1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2、在add.html页面获取url中的参数[员工id]
3、发送ajax请求,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端7、服务端接收员工信息,并进行处理,完成后给页面响应
8、页面接收到服务端响应信息后进行相应处理
@GetMapping("/{id}")
public R<Employee> getByid(@PathVariable Long id){
Employee employee = employeeService.getById(id);
if (employee!=null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
[!note]
之所以可以完成修改,其实点击完成调用了我们之前写的通用的update方法,只要是根据id修改的表里某个字段都可以进行修改
公共字段自动填充
问题分析
前面我们完成了后台管理系统的员工管理功能开发,在新增员工时需要设置创建时间,创建人,修改时间,修改人等信息,在编辑员工时需要设置修改时间和修改人等信息.这些字段属于公共字段,也就是表中很多元素都有这些字段,如下:
能不能对于这些公共字段在某个地方统一处理,来简化开发呢答案就是使用Mybatis Plus提供的公共字段自动填充功能。
代码实现
MybatisPlus公共字段自动填充,也就是插入或者更新的时候为指定字段设置指定的值,使用它的好处是可以统一为这些字段进行处理,避免了代码重复.
实现步骤
- 在实体类的属性上加入@TableField的注解,指定自动填充的策略
- 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHander接口
@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;
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
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) {
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",new Long(1));
}
}
功能完善
前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充createUser和updateUser时设置的用户id是固定值,现在我们需要改造成动态获取当前登录用户的id。
有的同学可能想到,用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?
注意,我们在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id
可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。
在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LoginCheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFill方法
可以在上面的三个方法中分别加入下面代码(获取当前线程id) :
long id = Thread.currentThread().getId() ;
log.info("线程id:{0}",id);
执行员工编辑功能进行验证,通过观察控制太输出可以发现,一次请求对应的线程id是相同
什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id )。
实现步骤
- 编写BaseContext工具类,基于ThreadLocal封装的工具类
- 在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
- 在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id
[!note]
用设置静态方法的方式把ThreadLocal封装到工具类,然后这样ThreadLocal方法就可以直接调用
/**
* 基于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();
}
}
在Filter中把用户id放入当前线程的线程局部变量的值(用户id)以便于之后使用
同时修改之前公共字段自动填充的内容
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
新增分类
需求分析
后台管理系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类,当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台中添加一个套餐时需要选择一个餐品分类,在移动端也会按照菜品分类或者餐品分类来展示
我们可以在后台分别添加菜品分类和套餐分类
数据模型
需要注意分类的名称是唯一的
需求分析
- 员工列表页面可以对员工账号进行启用和禁用处理,账号禁用的员工不能登录系统,启用后方可登录系统
- 需要注意的是,只有管理员(admin用户)可以对其他用户进行启用,禁用操作,所以普通用户登录系统后启用,禁用按钮不显示.
前端有一个名字为user的模型数据来之前存储的用户信息,如果user是admin也就是登录的用户是管理与,那么将显示禁用与启用信息.
代码开发
在开发业务之前需要把用到的类和接口基本结构准备好
- 实体类category
- Mapper接口CategoryMapper
- 业务层接口CategoryService
- 业务层实现类CategoryServiceImpl
- 控制层CategoryController
操作流程
1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
可以发现新增菜品分类和新增套餐分类的请求服务端地址和JSon格式相同,所以服务端只要提供一个方法处理即可
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@PostMapping
public R<String > save(@RequestBody Category category){
log.info("category{}",category);
categoryService.save(category);
return R.success("新增分类成功");
}
}
删除分类
需求分析
在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
@DeleteMapping
public R<String> delete(Long id){
categoryService.removeById(id);
return R.success("删除成功")
}
完善
和菜品和套餐已经连接的分类不能进行删除,因为我们没有设置外键,所以要用代码实现
@DeleteMapping
public R<String> delete(Long ids){
categoryService.remove(ids);
return R.success("删除成功");
}
[!note]
要在service中自己定义方法,而且可以调用别的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> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dish::getCategoryId,id);
int count = dishService.count(queryWrapper);
if (count>0){
throw new CustomException("当期分类下关联了菜品,不能删除");
}
LambdaQueryWrapper<Setmeal> queryWrapper1 = new LambdaQueryWrapper<>();
queryWrapper1.eq(Setmeal::getCategoryId,id);
int count1 = setmealService.count(queryWrapper1);
if (count1>0){
throw new CustomException("当期分类下关联了套餐,不能删除");
}
//查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
//正常删除分类
super.removeById(id);
}
}
文件上传与下载
文件上传介绍
文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
form表单的要求
- method=“post” 采用post的方式提交数据
- enctype=multpart/form-data" 采用multipart格式上传文件
- type=“file” 使用input的file控件上传
举例
<form method="post" action="/common/upload" enctype="multipart/form-data"><input name="myFile" type="file" />
<input type="submit" value="提交" /></form>
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
- commons-fileuplolad
- commons-io
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("/${reggie.path}")
private String basePath;
@PostMapping("/upload")
public R<String> upload(MultipartFile file) throws IOException {
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();
}
log.info(basePath+fileName);
//file是一个临时文件需要指定保存的位置,否则本次请求完成后文件会被自动删除
file.transferTo(new File(basePath+ fileName));
return R.success(fileName);
}
}
文件下载
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(basePath+name);
//输出流,通过输出流把文件写会浏览器,在浏览器展示图片
ServletOutputStream outputStream = response.getOutputStream();
int len=0;
byte[] bytes = new byte[1024];
while ((len=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
response.setContentType("image/jpeg");
} catch (Exception e) {
e.printStackTrace();
}
}
新增菜品
需求分析
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。
数据模型
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:
代码开发
准备工作
实体类DishFlavor(直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
Mapper接口DishFlavorMapper
业务层接口DishFlavorService
业务层实现类DishFlavorServicelmpl
控制层DishController
在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:
1、页面(badkend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中[
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显
4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
导入dto
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
[!danger]
DTO,全称为Data Transfer object,即数据传输对象,一般用于展示层与服务层之间的数据传输.
service
@Autowired
private DishFlavorService dishFlavorService;
//添加事务控制
@Transactional
@Override
public void saveWithFlavor(DishDto dishDto) {
//使用本身的save保存菜品
this.save(dishDto);
Long id = dishDto.getId();
List<DishFlavor> flavors = dishDto.getFlavors();
flavors= flavors.stream().map((item)->{
item.setDishId(id);
return item;
}).collect(Collectors.toList());
//保存口味信息
dishFlavorService.saveBatch(flavors);
}
controller
[[springmvc#9.1、@RequestBody]]
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
菜品信息分页查询
系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
代码开发
梳理交互流程
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
controller
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
Page<Dish> pageInfo = new Page<>(page,pageSize);
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name!=null,Dish::getName,name);
queryWrapper.orderByDesc(Dish::getCreateTime);
dishService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
但是有个问题
菜品表中的菜品分类字段是分类的id,前端要求的是菜品分类名称,这样才能展示
所以我们要去处理一下
[!note]
因为原本的Dish里面没有categoryName这个属性,所以要借助DishDto,但是如何根据条件把Dto中的categoryName设置成我们需要的值,并且其余Dish有的我们都要有呢,需要先拷贝pageinfo中除了records的数据,(records)s是查询出来的数据列表,并不是我们需要的,需要把records处理好然后放回去,最后返回的是DishDto类型的Page
#工具类/对象拷贝
BeanUtils.copyProperties
controller
@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::getCreateTime);
dishService.page(pageInfo,queryWrapper);
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
List<DishDto> list= records.stream().map((item)->{
DishDto dishDto = new DishDto();
//拷贝Dish里面的属性到DishDto
BeanUtils.copyProperties(item,dishDto);
//查询到分类id
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
//通过分类id查询到分类名称
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
return dishDto;
//最后通过collect收集成集合
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
//最后传回去的DishDto类型的分页信息
return R.success(dishDtoPage);
}
修改菜品
需求分析
在菜品管理列表页面,点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点金确定按钮完成菜品修改
代码开发
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
service中自定义查询来完成查询的功能
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//查询当前菜品的口味信息
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> list = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(list);
return dishDto;
}
controller
因为我们要同时返回菜品信息和口味信息,但是这两个表是单独的,我们的dto中包含了我们需要的属性,完成dto的封装需要查询到dish信息和flavor信息然后封装进去返回
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
DishDto byIdWithFlavor = dishService.getByIdWithFlavor(id);
return R.success(byIdWithFlavor);
}
新增套餐
需求分析
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
数据模型
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据所以在新增套餐时,涉及到两个表:
- setmeal 套餐表
- setmeal_dish套餐菜品关系表
代码开发
准备工作
需要准备好的类和接口
- 实体类SetmealDish,直接导入即可
- DTO SetmealDto 直接导入即可
- Mapper接口SetmealDishMapper
- 业务层接口SetmealDishService
- 业务层实现类SetmealDishServiceImpl
- 控制层SetmealController
梳理交互过程
在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:
1、页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2、页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
5、页面发送请求进行图片下载,将上传的图片进行回显
6、点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//设置查询条件为状态为启用的
queryWrapper.eq(Dish::getStatus,1);
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
@Autowired
private SetmealDishService setmealDishService;
@Transactional
@Override
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息
this.save(setmealDto);
//保存套餐和菜品的关联信息
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes);
}
controller
/**
* 新增套餐
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
setmealService.saveWithDish(setmealDto);
return R.success("新建套餐成功");
}
套餐列表分页信息
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page.pageSize,name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
Page<Setmeal> pageInfo = new Page<>();
Page<SetmealDto> setmealDtoPage=new Page<>();
LambdaQueryWrapper<Setmeal> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.like(name!=null,Setmeal::getName,name);
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list= records.stream().map((item)->{
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(item,setmealDto);
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category!=null){
String name1 = category.getName();
setmealDto.setCategoryName(name1);
}
return setmealDto;
}).collect(Collectors.toList());
setmealDtoPage.setRecords(list);
return R.success(setmealDtoPage);
删除套餐
需求分析
删除套餐有批量删除和单个删除,我们可以用数组接收传过来的id,这样就可以使用一个来实现批量删除和单个删除的功能,还要注意的是,我们在删除套餐的时候还要删除与之对应的套餐,菜品关系表的相关信息,(我套餐都没了还要套餐菜品关系干啥)
手机验证码登录
阿里云短信服务-介绍
阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。
应用场景:
步骤
- 导入maven坐标
- 调用api
<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>
需求分析
为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。
手机验证码登录的优点:
- 方便快捷,无需注册,直接登录
- 使用短信验证码作为登录凭证,无需记忆密码
- 安全
登录流程:
输入手机号>获取验证码>输入验证码>点击登录>登录成功
注意:通过手机验证码登录,手机号是区分不同用户的标识。
数据模型
代码开发
流程梳理
在开发代码之前,需要梳理一下登录时前端页面和服务端的交互过程:
1、在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信
2、在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
地址簿
需求分析
地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。
数据模型
导入功能代码
功能代码清单
- 实体类AddressBook(直接从课程资料中导入即可)
- Mapper接口AddressBookMapper
- 业务层接口AddressBookService
- 业务层实现类AddressBookServicelmpl
- 控制层AddressBookController (直接从课程资料中导入即可)
菜品展示
用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示
选择规格按钮,否则显示按钮。
代码开发
在开发代码之前,需要梳理一下前端页面和服务端的交互过程:
1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
2、页面发送ajax请求,获取第一个分类下的菜品或者套餐
开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再修改回来,如下:
菜品展示的同时获得菜品的口味信息
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//设置查询条件为状态为启用的
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();
//拷贝Dish里面的属性到DishDto
BeanUtils.copyProperties(item, dishDto);
//查询到分类id
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if (category != null) {
//通过分类id查询到分类名称
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
Long dishid = item.getId();//获取dishid
//获取口味表
LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId,dishid);
List<DishFlavor> dishFlavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
dishDto.setFlavors(dishFlavors);
return dishDto;
//最后通过collect收集成集合
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
查询套餐信息
@GetMapping("/list")
public R<List<Setmeal>> listR(@RequestBody Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//根据套餐种类查询
queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
//加入状态条件
queryWrapper.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus());
List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}
购物车
需求分析
移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息
则需要选择规格后才能
加入购物车;对于套餐来说,可以直接点击将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。
代码实现
1、点击[加入购物车]
或者[+]按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。
准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
●实体类ShoppingCart(直接从课程资料中导入即可)
●Mapper接口ShoppingCartMapper
●业务层接口ShoppingCartService
●业务层实现类ShoppingCartServicelmpl
●控制层ShoppingCartController
controller
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody Shopping
//设置用户id
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
//查询当前商品是否在购物车里面
Long dishId = shoppingCart.getDishId(); //
LambdaQueryWrapper<ShoppingCart> queryWrappe
queryWrapper.eq(ShoppingCart::getUserId, sho
if (dishId != null) {
//参加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getDishId,
} else {
queryWrapper.eq(ShoppingCart::getSetmeal
}
ShoppingCart one = shoppingCartService.getOn
if (one != null) {
//如果已经存在只需要商品数量增加即可
Integer number = one.getNumber();
one.setNumber(number+1);
shoppingCartService.updateById(one);
} else {
//如果不存在,则添加购物车,数量默认就是1
shoppingCart.setNumber(1);
shoppingCartService.save(shoppingCart);
one=shoppingCart;
}
//不存在则添加到购物车
return R.success(one);
}
查看购物车
*
@GetMapping("/list")
public R<List<ShoppingCart>> listR() {
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
queryWrapper.orderByAsc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list);
}
用户下单
需求分析
数据模型
用户下单业务对应的数据表为orders表和order_detail表:
- orders:订单表
- order_detail:订单明细表
在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:1、在购物车中点击
去结算按钮,页面跳转到订单确认页面
2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
4、在订单确认页面点击去支付去支付按钮,发送ajax请求,请求服务端完成下单操作
开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。
准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类Orders、OrderDetail (直接从课程资料中导入即可)
Mapper接口OrderMapper、OrderDetailMapper
业务层接口 OrderService、OrderDetailService
业务层实现类OrderServicelmpl、OrderDetailServicelmpl
控制层OrderController、OrderDetailController