软件开发流程
角色分工
软件环境
项目介绍
产品原型展示
技术选型
功能架构
角色
本项目(瑞吉外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单灯进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。
本项目共分为3期进行开发:
第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问
第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便
第三期主要针对系统进行优化升级,提高系统的访问性能
产品原型,就是一款产品成型之前的一个简单的框架,就是将页面的排版布局展现出来,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。
注意:产品原型主要用于展示项目的功能,并不是最终的页面效果
数据库环境搭建
maven项目搭建
1) 配置依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.7.10
com.itheima
regie_take_out
1.0-SNAPSHOT
17
17
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
compile
com.baomidou
mybatis-plus-boot-starter
3.4.2
org.projectlombok
lombok
1.18.20
com.alibaba
fastjson
1.2.76
commons-lang
commons-lang
2.6
mysql
mysql-connector-java
8.0.31
runtime
com.alibaba
druid-spring-boot-starter
1.1.23
org.springframework.boot
spring-boot-maven-plugin
2.4.5
2)application.yml配置
spring.application.name是应用的名称,可选
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
3)编写启动类
@Slf4j
@SpringBootApplication
public class RegieApplication {
public static void main(String[] args) {
SpringApplication.run(RegieApplication.class,args);
log.info("项目启动成功...");
}
}
4)导入前端页面
因为backend和front不在静态资源目录(static和template目录 )下,所以会访问404,可以加以配置
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport{
/*
* 设置静态资源映射
* */
@Override
protected void addResourceHandlers(ResourceHandleRegistry registry){
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
备注:
@Configuration
public class MyWebMVCConfig implements WebMvcConfigurer {
@Value("${file.location}") // D:/test/
String filelocation; // 这两个是路径
@Value("${file.path}") // /file/**
String filepath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//匹配到resourceHandler,将URL映射至location,也就是本地文件夹
registry.addResourceHandler(filepath).addResourceLocations("file:///" + filelocation);//这里最后一个/不能不写
}
}
这段代码的意思就是配置一个拦截器,如果访问的路径是addResourceHandler中的filepath,就把它映射到本地的addResourceLocations的参数的这个路径上,这样就可以让别人访问服务器的本地文件了,比如本地图片、本地音乐视频等等。
访问路径http://localhost:8080/backend/index.html测试配置是否成功:
需求分析
代码开发
功能测试
只需要输入用户名和密码就可以登录成功
/*
* 员工实体类
* */
@Data
public class Employee implements Serializable {
private static final long serialVersionUID=1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
private String idNumber; // 身份证号
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
备注:①其中@Data是lombok的注解,集成了以下注解:
②实现Serializable接口的作用
在程序中为了能直接以 Java 对象的形式进行保存,然后再重新得到该 Java 对象,这就需要序列化能力。
实现序列化操作用于存储,一般针对于NoSql数据库
③@TableField字段填充策略
值 | 描述 |
---|---|
DEFAULT | 默认不处理 |
INSERT | 插入填充字段 |
UPDATE | 更新填充字段 |
INSERT_UPDATE | 插入和更新填充字段 |
比如在进行插入操作时,会对添加了@TableField(fill = FieldFill.INSERT)的字段进行自动填充;再进行插入和更新操作时,会对添加了@TableField(fill = FieldFill.INSERT_UPDATE)的字段进行自动填充
第一步:创建包结构
第二步:创建Mapper
@Mapper
public interface EmployeeMapper extends BaseMapper {
}
第三步:创建Service及其实现类
public interface EmployeeService extends IService {
}
EmployeeServiceImpl需要实现ServiceImpl,泛型需要指定Mapper以及实体类
@Service
public class EmployeeServiceImpl extends ServiceImpl implements EmployeeService {
}
第四步:创建Controller
@RestController
@Slf4j
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
}
第一步:开发返回结果类R
R类是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面
/*通用返回结果,服务端响应的数据最终都会封装成此对象
* */
@Data
public class R {
private Integer code; // 编码:1表示成功,0和其他数字都表示失败
private String msg; //错误信息
private T data; //数据
private Map map=new HashMap(); //动态数据
public static R success(T object){
R r=new R<>();
r.data=object;
r.code=1;
return r;
}
public static R error(String msg){
R r=new R<>();
r.msg=msg;
r.code=0;
return r;
}
public R add(String key,Object value){
this.map.put(key,value);
return this;
}
}
第二步:在Controller中创建登录方法
1.将页面提交的密码password进行md5加密处理
2.根据页面提交的用户名username查询数据库
3.如果没有查询到则返回登录失败结果
4.密码比对,如果不一致则返回登录失败结果
5.查看员工状态,如果为已禁用状态,则返回员工已禁用结果
6.登录成功,将员工id存入Session并返回登录成功结果
@RestController
@Slf4j
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@PostMapping("/login")
public R login(@RequestBody Employee employee, HttpServletRequest request){
//1.将页面提交的密码password进行md5加密处理
String password=employee.getPassword();
password=DigestUtil.md5Hex(password);
//2.根据页面提交的用户名username查询数据库
LambdaQueryWrapper 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);
}
}
登录成功以后,前端会使用localStorage存储用户信息
备注: ①QueryWrapper的用法
此外,需要说明的是使用LambdaQueryWrapper,简化lambda使用,避免QueryWrapper的硬编码问题
也可以使用链式查询:
List bannerItems = new LambdaQueryChainWrapper<>(bannerItemMapper)
.eq(BannerItem::getBannerId, id)
.list();
BannerItem bannerItem = new LambdaQueryChainWrapper<>(bannerItemMapper)
.eq(BannerItem::getId, id)
.one();
②session.setAttribute方法解析
B/S架构中,客户端与服务器连接,在服务端就会自动创建一个session对象. session.setAttribute(“username”,username); 是将username保存在session中!session的key值为“username”value值就是username真实的值,或者引用值. 这样以后你可以通过session.getAttribute(“username”)的方法获得这个对象. 比如说,当用户已登录系统后你就在session中存储了一个用户信息对象,此后你可以随时从session中将这个对象取出来进行一些操作,比如进行身 份验证等等.
request.getSession()可以帮你得到HttpSession类型的对象,通常称之为session对象,session对 象的作用域为一次会话,通常浏览器不关闭,保存的值就不会消失,当然也会出现session超时。服务器里面可以设置session的超时时 间,web.xml中有一个session time out的地方,tomcat默认为30分钟
分别测试用户名不存在、密码错误、账号已禁用以及登录成功的四个情况即可
点击退出按钮,触发方法logout
此时会调用logoutApi,向/employee/logout发起一个post请求。然后处理返回结果,删除localStorage中存储的userInfo,跳转到login页面
用户点击页面中的退出按钮,发送请求,请求地址为/employee/logout,请求方式为post
我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑:
1.清理session中的用户id
2.返回结果
@PostMapping("/logout")
public R logout(HttpServletRequest request){
//清理Session中保存的当前登录员工id
request.getSession().removeAttribute("employee");
return R.success("退出成功!");
}
完善登录功能
新增员工
员工信息分页查询
启用/禁用员工账号
编辑员工信息
前面我们已经完成了后台系统的员工登录功能开发,但是还存在一个问题:如果用户不登录,直接访问系统首页,照样可以正常访问。
这种设计并不合理,我们希望看到的效果是,只有登陆成功以后才可以访问系统中的页面,如果没有登陆则跳转到登录页面。
那么,具体应该怎么实现呢?
答案就是使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登陆则跳转到登录页面
过滤器实现步骤:
1.创建自定义过滤器LoginCheckFilter
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
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;
log.info("拦截到请求:{}",request.getRequestURL());
filterChain.doFilter(request,response);
}
}
2.在启动类上加上注解@ServletComponentScan
加上注解@ServletComponentScan,才能扫描到@WebFilter注解
3.完善过滤器的处理逻辑
1.获取本次请求的URI
2.判断本次请求是否需要处理
3.如果不需要处理,则直接放行
4.判断登录状态,如果已登录,则直接放行
5.如果未登录则返回登录结果
@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.获取本次请求的UTI
String requestURI = request.getRequestURI();
//定义不需要处理的请求路径
String[] urls=new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2.判断本次请求是否需要处理
boolean check = checkURI(urls, requestURI);
//3.如果不需要处理,则直接放行
if(check){
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;
}
//路径匹配,检查本次请求是否需要放行
private boolean checkURI(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if(match){
return true;
}
}
return false;
}
}
需求分析
数据模型
代码实现
功能测试
点击添加员工就可以跳转到添加员工的页面
新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。需要注意的是,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的
employee表中的status字段已经设置了默认值1,表示状态正常
程序执行的过程:
1.页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2.服务端Controller接受页面提交的数据并调用Service将数据进行保存
3.Service调用Mapper操作数据库,保存数据
/*
* 新增员工
* */
@PostMapping
public R save(HttpServletRequest request,@RequestBody Employee employee){
employee.setPassword(DigestUtil.md5Hex("123456"));
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表中对该字段加入了唯一约束,因此程序会抛出异常“SQLIntegrityConstraintViolationException”
框架搭建:
/*
* 全局异常处理
* */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage()); //打印错误信息
return R.error("失败");
}
}
逻辑完善:
/*
* 全局异常处理
* */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R 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("未知错误");
}
}
系统中的员工很多时,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统都会采用分页的方式来展示列表数据
1.页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2.服务端Controller接受页面提交的数据并调用Service查询数据
3.Service调用Mapper操作数据库,查询分页数据
4.Controller将查询到的分页数据响应给页面
5.页面接收到分页数据并通过ElementUI的Table组件展示到页面上
来到index页面,会自动发起员工分页查询
第一步:配置分页拦截器
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
第二步:编写Controller
搭建框架:
@GetMapping("/page")
public R page(int page,int pageSize,String name){
log.info("page={},pageSize={},name={}",page,pageSize,name);
return null;
}
逻辑编写:
@GetMapping("/page")
public R page(int page,int pageSize,String name){
log.info("page={},pageSize={},name={}",page,pageSize,name);
//构造分页构造器
Page pageInfo = new Page(page, pageSize);
//构造条件构造器
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
注意,如果StringUtils.isNotEmpty()找不到该方法,注意检查导包是否为:
import org.apache.commons.lang.StringUtils;
功能测试:
页面返回的status是0或1,但是显示在页面上的却是已禁用或正常
这是因为使用了模板标签
{{ String(scope.row.status) === '0' ? '已禁用' : '正常' }}
对于一个数据体[{},{},{},{},...],scope.row相当于一个{},{}里边的数据通过属性名获取
在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录
需要注意的是,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示
点击禁用/启用按钮,会发送put请求,并携带id和status传递给服务端
启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作
在Controller中创建update方法,此方法是一个通用的修改员工信息的方法
/*
* 根据id修改员工信息
* */
@PutMapping
public R update(@RequestBody Employee employee,HttpServletRequest request){
Long empId=(Long)request.getSession().getAttribute("employee");
employee.setUpdateUser(empId);
employee.setCreateTime(LocalDateTime.now());
employeeService.updateById(employee);
return R.success("员工信息修改成功!");
}
问题发现:尽管后端已经对status进行了修改,但是前段仍然不会改变。
原因分析:这是因为js对long型数据进行处理时丢失了精度,导致提交的id和数据库中的id不一致。(仔细看,末尾的4位不一致)
解决方案:我们可以在服务端给页面响应json数据时,将long型数据统一转为String字符串
代码修复:
1)提供对象转换器JacksonObjectMapper,基于jackson进行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);
}
}
2)在WebMvcConfig配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行java对象到json数据的转换
converters.add(0,messageConverter)中,0表示将我们自定义的转换器放在最前面,优先使用
/*
* 扩展mvc框架的消息转换器
* */
@Override
protected void extendMessageConverters(List> converters) {
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
管理员先禁用张三的账号,退出以后看是否能登陆张三的账号
在员工管理列表页面点击编辑按钮,跳转到编辑页面。在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作
1.点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2.在add.html页面获取url中的参数[员工id]
3.发送ajax请求,请求服务端,同时提交员工id参数
4.服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5.页面接受服务端响应的json数据,通过vue数据绑定进行员工信息回显
6.点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7.服务端接受员工信息,并进行处理,完成后给页面响应
8.页面接收到服务端响应信息后进行相应处理
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作
根据id查询用户信息:
@GetMapping("/{id}")
public R getById(@PathVariable long id){
Employee employee = employeeService.getById(id);
if(employee!=null)
return R.success(employee);
return R.error("没有查询到对应员工!");
}
由于之前已经写过通用的update方法,所以本功能已经实现了
公共字段自动填充
新增分类
分类信息分页查询
删除分类
修改分类
前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段。
我们考虑用mybatisplus提供的公共字段自动填充功能统一处理
mybatisplus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处是可以统一对这些字段进行处理,避免了重复代码
实现步骤:
1.在实体类的属性上加入@TableField注解,指定自动填充的策略
@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;
2.按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
注意:在自动填充createUser和updateUser时设置的用户id是固定值,需要改造成动态获取当前登录用户的id
尽管我们在用户登录成功后将用户id存入了HttpSession中,但是却不能从HttpSession中获取id:因为在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:{}",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)
1.编写BaseContext工具类,基于ThreadLocal封装的工具类
/*
* 基于ThreadLocal封装工具类,用于保存和获取当前登录用户id
* */
public class BaseContext {
private static ThreadLocal threadLocal=new ThreadLocal<>();
public static void setCurrentId(Long id){
threadLocal.set(id);
}
public static Long getCurrentId(){
return threadLocal.get();
}
}
2.在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
//4.判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee")!=null){
BaseContext.setCurrentId((Long) request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
3.在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id
/*
* 自定义元数据对象处理器
* */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
Long id = BaseContext.getCurrentId();
metaObject.setValue("createUser",id);
metaObject.setValue("updateUser",id);
}
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime",LocalDateTime.now());
Long id = BaseContext.getCurrentId();
metaObject.setValue("updateUser", id);
}
}
测试通过:
第一步:创建实体类Category
/**
* 分类
*/
@Data
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//类型 1 菜品分类 2 套餐分类
private Integer type;
//分类名称
private String name;
//顺序
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;
}
第二步:创建Mapper
@Mapper
public interface CategoryMapper extends BaseMapper {
}
第三步:实现Service及其实现类
public interface CategoryService extends IService {
}
@Service
public class CategoryServiceImpl extends ServiceImpl implements CategoryService {
}
第四步:实现Controller
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@PostMapping
public R save(@RequestBody Category category){
categoryService.save(category);
return R.success("新增分类成功!");
}
}
注意:如果添加分类的时候,分类名相同会抛出异常被全局异常处理器处理
@GetMapping("/page")
public R page(int page, int pageSize){
//分页构造器
Page pageInfo = new Page<>(page,pageSize);
//条件构造器
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);
//进行分页查询
categoryService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
/*
* 根据id删除分类
* */
@DeleteMapping
public R delete(Long ids){
categoryService.removeById(ids);
return R.success("分类信息删除成功!");
}
前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。
要完善分类删除功能,需要先准备基础的类和接口:
1.实体类Dish和Setmeal
/**
菜品
*/
@Data
public class Dish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//商品码
private String code;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//顺序
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;
}
/**
* 套餐
*/
@Data
public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//分类id
private Long categoryId;
//套餐名称
private String name;
//套餐价格
private BigDecimal price;
//状态 0:停用 1:启用
private Integer status;
//编码
private String code;
//描述信息
private String description;
//图片
private String image;
@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;
}
2.Mapper接口DishMapper和SetmealMapper
@Mapper
public interface DishMapper extends BaseMapper {
}
@Mapper
public interface SetmealMapper extends BaseMapper {
}
3.Service接口DishService和SetmealService
public interface DishService extends IService {
}
public interface SetmealService extends IService {
}
4.Service实现类DishServiceImpl和SetmealServiceImpl
@Service
public class DishServiceImpl extends ServiceImpl implements DishService {
}
@Service
public class SetmealServiceImpl extends ServiceImpl implements SetmealService {
}
自定义异常,处理分类关联菜品或套餐不能直接删除的情况:
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
在全局异常处理器中加入自定义异常处理器:
@ExceptionHandler(CustomException.class)
public R exceptionHandler(CustomException ex){
return R.error(ex.getMessage());
}
删除分类逻辑:
@Service
public class CategoryServiceImpl extends ServiceImpl implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/*
* 根据id删除分类,再删除之前需要进行判断
* */
@Override
public void remove(Long id) {
LambdaQueryWrapper dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//查询当前分类是否关联了菜品,如果已经关联,则抛出一个业务异常
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dishLambdaQueryWrapper);
if(count1>0){
//已经关联菜品,抛出一个业务异常
throw new CustomException("当前分类项关联了菜品,不能删除!");
}
//查询当前分类是否关联了套餐,如果已经关联,抛出一个异常
LambdaQueryWrapper setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if(count2>0){
//已经关联套餐,抛出一个业务异常
throw new RuntimeException("当前分类下关联了套餐,不能删除!");
}
//正常删除分类
super.removeById(id);
}
}
Controller调用:
/*
* 根据id删除分类
* */
@DeleteMapping
public R delete(Long ids){
categoryService.remove(ids);
return R.success("分类信息删除成功!");
}
功能测试:
在分类管理页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改
@PutMapping
public R update(@RequestBody Category category){
categoryService.updateById(category);
return R.success("修改分类信息成功!");
}
文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或者下载的过程。
文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
文件上传时,对页面的form表单有如下要求:
method="post" 采用post方式提交数据
enctype="multipart/form-data" 采用multipart格式上传文件
type="file" 使用input的file控件上传
举例:
服务端要接受客户端页面上传的文件,通常都会使用Apache的两个组件:
commons-fileupload
commons-io
Spring框架在spring-web中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程
通常浏览器进行文件下载,通常有两种表现形式:
以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
直接在浏览器中打开
通过浏览器进行文件下载,本质上就是将服务端将文件以流的形式写回浏览器的过程
Controller中形参类型为Multipart,并且文件名和上传过来的文件名name="file"保持一致才能接收
上传的file是一个临时文件,需要转存到指定位置,否则请求完成后临时文件会删除
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${regie.path}")
private String basePath;
@PostMapping("/upload")
public R upload(MultipartFile file){
//原始文件名
String originalFilename=file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,方式文件名称重复造成文件覆盖
String fileName= IdUtil.simpleUUID()+suffix;
//创建一个目录对象
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);
}
}
文件下载,页面端可以使用标签展示下载的图片
/*
* 文件下载
* */
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
//输入流,通过输入流读取文件内容
try {
FileInputStream fileInputStream = new FileInputStream(basePath + name);
//输出流,通过输出流将文件写回浏览器,在浏览器展示图片
ServletOutputStream outputStream=response.getOutputStream();
response.setContentType("image/jepg");
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();
}
}
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据
所以在新增菜品时,涉及到两个表:
dish 菜品表
dish_flavor 菜品口味表
实体类 DishFlavor
/**
菜品口味
*/
@Data
public class DishFlavor implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//菜品id
private Long dishId;
//口味名称
private String name;
//口味数据list
private String value;
@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;
}
Mapper接口 DishFlavorMapper
@Mapper
public interface DishFlavorMapper extends BaseMapper {
}
Service接口及其实现类
public interface DishFlavorService extends IService {
}
@Service
public class DishFlavorServiceImpl extends ServiceImpl implements DishFlavorService {
}
控制层DishController
@RestController
@RequestMapping("/dish")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;
}
1.页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
@GetMapping("/list")
public R> list(Category category){
LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
//添加排序条件
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List list = categoryService.list(queryWrapper);
return R.success(list);
}
2.页面发送请求进行图片上传,请求服务端将图片保存到服务器
3.页面发送请求进行图片下载,将上传的图片进行回显
4.点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
因为无法直接用一个实体类接收传递过来的参数,所以要创建一个DTO来封装页面提交过来的数据
DTO,全称Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传递
@Data
public class DishDto extends Dish {
private List flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
编写saveWithFlavor逻辑:
public interface DishService extends IService {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavor
public void saveWithFlavor(DishDto dishDto);
}
@Service
public class DishServiceImpl extends ServiceImpl implements DishService {
@Autowired
private DishFlavorService dishFlavorService;
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
//获取菜品ID
Long dishId = dishDto.getId();
//菜品口味
List flavors = dishDto.getFlavors();
flavors=flavors.stream().map(item->{
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
}
因为要同时操作两张表,所以需要加@Transactional注解,并且在运行类上加@EnableTransactionManagement
@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement
public class RegieApplication {
public static void main(String[] args) {
SpringApplication.run(RegieApplication.class,args);
log.info("项目启动成功...");
}
}
补充说明:在SpringBootApplication上使用@ServletComponentScan注解后,Servlet(控制器)、Filter(过滤器)、Listener(监听器)可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册到Spring容器中,无需其它代码
编写Controller逻辑:
@PostMapping
public R save(@RequestBody DishDto dishDto){
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功!");
}
1.页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
@GetMapping("/page")
public R> page(int page,int pageSize,String name){
Page pageInfo = new Page<>(page,pageSize);
Page dishDtoPage=new Page<>();
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name!=null,Dish::getName,name)
.orderByDesc(Dish::getUpdateTime);
dishService.page(pageInfo,queryWrapper);
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List records = pageInfo.getRecords();
List dishDtoList = records.stream().map(item -> {
DishDto dishDto = new DishDto();
String categoryName = categoryService.getById(item.getCategoryId()).getName();
dishDto.setCategoryName(categoryName);
BeanUtils.copyProperties(item, dishDto);
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(dishDtoList);
return R.success(dishDtoPage);
}
2.页面发送请求,请求服务端进行图片下载,用于页面图片展示
1.页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2.页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
@Override
public DishDto getByIdWithFlavor(Long id) {
DishDto dishDto = new DishDto();
Dish dish = this.getById(id);
BeanUtils.copyProperties(dish,dishDto);
LambdaQueryWrapper dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId,id);
List flavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
dishDto.setFlavors(flavors);
return dishDto;
}
@GetMapping("/{id}")
public R getDishDto(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
3.页面发送请求,请求服务端进行图片下载,用于页图片回显
===>页面回显成功
4.点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//更新dish表基本信息
this.updateById(dishDto);
//清理当前菜品对应口味数据
LambdaQueryWrapper dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(dishFlavorLambdaQueryWrapper);
//添加当前提交过来的口味数据
List flavors = dishDto.getFlavors();
flavors = flavors.stream().map(item -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
}
@PutMapping
public R update(@RequestBody DishDto dishDto){
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功!");
}
===>修改菜品成功
新增套餐
套餐信息分页查询
删除套餐
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:
setmeal 套餐表
setmeal_dish 套餐菜品关系表
1.实体类SetmealDish
冗余字段name和price,可以避免查表dish,减少查表次数
/**
* 套餐菜品关系
*/
@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;
}
2.DTO SetmealDto
因为编辑的时候要回显分类名称,所以增加属性categoryName
@Data
public class SetmealDto extends Setmeal {
private List setmealDishes;
private String categoryName;
}
3.Mapper接口SetmealDishMapper
@Mapper
public interface SetmealDishMapper extends BaseMapper {
}
4.业务层接口SetmealDishService
public interface SetmealDishService extends IService {
}
5.业务层实现类SetmealDishServiceImpl
@Service
public class SetmealDishServiceImpl extends ServiceImpl implements SetmealDishService {
}
6.控制层SetmealController
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealDishController {
@Autowired
private SetmealDishService setmealDishService;
@Autowired
private SetmealService setmealService;
}
1.页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2.页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3.页面发送ajax请求,请求服务端根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
/*
* 根据条件查询对应的菜品数据
* */
@GetMapping("/list")
public R> list(Dish dish){
//构造查询条件
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId())
.eq(Dish::getStatus,1);
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List list = dishService.list(queryWrapper);
return R.success(list);
}
4.页面发送请求进行图片上传,请求服务端将图片保存到服务器
5.页面发送请求进行图片下载,将上传的图片进行回显
6.点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
@Service
public class SetmealServiceImpl extends ServiceImpl implements SetmealService {
@Autowired
private SetmealDishService setmealDishService;
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息
this.save(setmealDto);
//保存套餐和菜品的关联信息
List setmealDishes = setmealDto.getSetmealDishes();
setmealDishes=setmealDishes.stream().map(item->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes);
}
}
@PostMapping
public R save(@RequestBody SetmealDto setmealDto){
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功!");
}
系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统都会以分页的方式来展示列表数据
1.页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
@GetMapping("/page")
public R> page(int page,int pageSize,String name){
Page pageInfo = new Page<>(page, pageSize);
Page pageDto=new Page<>();
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name!=null,Setmeal::getName,name);
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
BeanUtils.copyProperties(pageInfo,pageDto,"records");
List records = pageInfo.getRecords();
List setmealDtos = records.stream().map(item -> {
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(item, setmealDto);
String categoryName = categoryService.getById(item.getCategoryId()).getName();
setmealDto.setCategoryName(categoryName);
return setmealDto;
}).collect(Collectors.toList());
pageDto.setRecords(setmealDtos);
return R.success(pageDto);
}
2.页面发送请求,请求服务端进行图片下载,用于页面图片展示
在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
/*
* 删除套餐,同时需要删除套餐和菜品的关联数据
* */
@Override
@Transactional
public void removeWithDish(List ids) {
//查询套餐状态,确定是否可以删除
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids)
.eq(Setmeal::getStatus,1);
int count = this.count(queryWrapper);
if(count>0){
throw new CustomException("套餐正在售卖中,不能删除");
}
//如果可以删除,先删除套餐表中的数据
this.removeByIds(ids);
//删除关系表中的数据
LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(lambdaQueryWrapper);
}
@DeleteMapping
public R delete(@RequestParam List ids){
setmealService.removeWithDish(ids);
return R.success("删除套餐成功!");
}
套餐状态修改代码:
@PostMapping("status/{status}")
public R updateStatus(@PathVariable int status,@RequestParam List ids){
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids);
List setmeals = setmealService.list(queryWrapper);
setmeals.stream().map(item->{
if(item.getStatus()==status){
return R.error("请确保选择的套餐状态一致!");
}
return item;
});
LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(Setmeal::getStatus,status)
.in(Setmeal::getId,ids);
setmealService.update(updateWrapper);
return R.success("套餐状态修改成功!");
}
短信发送
手机验证码登录
目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。需要说明的是,这些短信服务一般都是收费服务。
常用短信服务:
阿里云
华为云
腾讯云
京东
梦网
乐信
阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的同喜能力。调用API或者用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信妙级触达,到达率可高达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用
应用场景:
验证码
短信通知
推广短信
请自行按照阿里云官方手册进行短信发送
为了方便用户登录,移动端通常都会提供手机验证码登录的功能。
手机验证码登录的优点:
方便快捷,无需注册,直接登录
使用短信验证码作为登录凭证,无需记忆密码
安全
登录流程:
输入手机号-->获取验证码-->输入验证码-->点击登录-->登陆成功
注意:通过手机验证码登录,手机号是区分不同用户的标识
1.在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送ajax请求,在服务端调用短信服务API给指定的手机号发送验证码短信
2.在登陆页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录青青
User实体类:
/**
* 用户信息
*/
@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接口:
@Mapper
public interface UserMapper extends BaseMapper {
}
Service接口及其实现类:
public interface UserService extends IService {
}
@Service
public class UserServiceImpl extends ServiceImpl implements UserService {
}
修改LoginCheckFilter:
//定义不需要处理的请求路径
String[] urls=new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/user/sendMsg", //移动端发送短信
"/user/login" //移动端登录
};
添加如下代码:
if(request.getSession().getAttribute("user")!=null){
Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request,response);
return;
}
发送验证码:
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/sendMsg")
public R sendMsg(@RequestBody User user, HttpSession session){
//获取手机号
String phone = user.getPhone();
//生成随机的4位验证码
String code = RandomUtil.randomNumbers(4);
//需要将生成的验证码保存到Session
session.setAttribute("code",code);
return R.success(code);
}
}
登录:
@PostMapping("/login")
public R login(@RequestBody Map map,HttpSession session){
//获取手机号
String phone =(String) map.get("phone");
//获取验证码
String code = (String) map.get("code");
//从Session中获取保存的验证码
String codeInSession = (String) session.getAttribute("code");
//进行验证码比对
if(codeInSession!=null && codeInSession.equals(code)){
//如果能够比对成功,说明登陆成功
LambdaQueryWrapper 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);
}
session.setAttribute("user",user.getId());
return R.success(user);
}
return R.error("登录失败,验证码错误!");
}
登陆成功效果展示:
导入用户地址簿相关功能代码
菜品展示
购物车
下单
地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但只能有一个默认地址。
功能代码清单:
实体类 AddressBook
/**
* 地址簿
*/
@Data
public class AddressBook implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//用户id
private Long userId;
//收货人
private String consignee;
//手机号
private String phone;
//性别 0 女 1 男
private String sex;
//省级区划编号
private String provinceCode;
//省级名称
private String provinceName;
//市级区划编号
private String cityCode;
//市级名称
private String cityName;
//区级区划编号
private String districtCode;
//区级名称
private String districtName;
//详细地址
private String detail;
//标签
private String label;
//是否默认 0 否 1是
private Integer isDefault;
//创建时间
@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;
}
Mapper接口 AddressBookMapper
@Mapper
public interface AddressBookMapper extends BaseMapper {
}
业务层接口 AddressBookService
public interface AddressBookService extends IService {
}
业务层实现类 AddressBookServiceImpl
@Service
public class AddressBookServiceImpl extends ServiceImpl implements AddressBookService {
}
控制层 AddressBookController
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;
@PostMapping
public R save(@RequestBody AddressBook addressBook){
addressBook.setUserId(BaseContext.getCurrentId());
addressBookService.save(addressBook);
return R.success(addressBook);
}
/*
* 设置默认地址
* */
@PutMapping("/default")
public R setDefault(@RequestBody AddressBook addressBook){
LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(AddressBook::getUserId,BaseContext.getCurrentId())
.set(AddressBook::getIsDefault,0);
addressBookService.update(updateWrapper);
addressBook.setIsDefault(1);
addressBookService.updateById(addressBook);
return R.success(addressBook);
}
/*
* 查询指定用户的全部地址
* */
@GetMapping("/list")
public R> list(AddressBook addressBook){
addressBook.setUserId(BaseContext.getCurrentId());
//条件构造器
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(addressBook.getUserId()!=null,AddressBook::getUserId,addressBook.getUserId());
queryWrapper.orderByDesc(AddressBook::getUpdateTime);
return R.success(addressBookService.list(queryWrapper));
}
}
效果展示:
用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示【选择规格】按钮,否则只展示【+】按钮
1.页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
2.页面发送ajax请求,获取第一个分类下的菜品或者套餐
注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再回来修改
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
// 'url': '/shoppingCart/list',
"url":"/front/cartData.json",
'method': 'get',
params:{...data}
})
}
效果:
/*
* 根据条件查询对应的菜品数据
* */
@GetMapping("/list")
public R> list(Dish dish){
//构造查询条件
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId())
.eq(Dish::getStatus,1);
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List list = dishService.list(queryWrapper);
List dishDtoList=list.stream().map(item->{
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Category category = categoryService.getById(item.getCategoryId());
if(category!=null){
dishDto.setCategoryName(category.getName());
}
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DishFlavor::getDishId,item.getId());
List flavors = dishFlavorService.list(wrapper);
dishDto.setFlavors(flavors);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
效果图:
@GetMapping("/list")
public R> list(Setmeal setmeal){
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId())
.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus())
.orderByDesc(Setmeal::getUpdateTime);
List list = setmealService.list(queryWrapper);
return R.success(list);
}
移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后加入购物车;对于套餐来说,可以直接点击【+】将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。
1.点击【加入购物车】或者【+】按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
2.点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
3.点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
实体类ShoppingCart
/**
* 购物车
*/
@Data
public class ShoppingCart implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//名称
private String name;
//用户id
private Long userId;
//菜品id
private Long dishId;
//套餐id
private Long setmealId;
//口味
private String dishFlavor;
//数量
private Integer number;
//金额
private BigDecimal amount;
//图片
private String image;
private LocalDateTime createTime;
}
Mapper接口 ShoppingCartMapper
@Mapper
public interface ShoppingCartMapper extends BaseMapper {
}
业务层接口 ShoppingCartService
public interface ShoppingCartService extends IService {
}
业务层实现类 ShoppingCartServiceImpl
@Service
public class ShoppingCartServiceImpl extends ServiceImpl implements ShoppingCartService {
}
控制层 ShoppingCartController
@RestController
@RequestMapping("/shoppingCart")
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
}
@PostMapping("/add")
public R add(@RequestBody ShoppingCart shoppingCart){
//设置用户id,指定当前是哪个用户的购物车数据
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
//查询当前菜品或者套餐是否在购物车中
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,currentId);
Long dishId = shoppingCart.getDishId();
if(dishId!=null){
//添加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getDishId,dishId);
}else{
//添加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
//查询当前菜品或者套餐是否在购物车中
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
if(cartServiceOne!=null){
//如果已存在,就在原来的数量基础上加一
Integer number = cartServiceOne.getNumber();
cartServiceOne.setNumber(number+1);
shoppingCartService.updateById(cartServiceOne);
}else{
//如果不存在,则添加到购物车中,数量默认是1
shoppingCart.setNumber(1);
shoppingCartService.save(shoppingCart);
cartServiceOne=shoppingCart;
}
return R.success(cartServiceOne);
}
@GetMapping("/list")
public R> list(){
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
queryWrapper.orderByAsc(ShoppingCart::getCreateTime);
List list = shoppingCartService.list(queryWrapper);
return R.success(list);
}
效果图:
@DeleteMapping("/clean")
public R clean(){
LambdaQueryWrapper 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
/**
* 订单
*/
@Data
public class Orders implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//订单号
private String number;
//订单状态 1待付款,2待派送,3已派送,4已完成,5已取消
private Integer status;
//下单用户id
private Long userId;
//地址id
private Long addressBookId;
//下单时间
private LocalDateTime orderTime;
//结账时间
private LocalDateTime checkoutTime;
//支付方式 1微信,2支付宝
private Integer payMethod;
//实收金额
private BigDecimal amount;
//备注
private String remark;
//用户名
private String userName;
//手机号
private String phone;
//地址
private String address;
//收货人
private String consignee;
}
/**
* 订单明细
*/
@Data
public class OrderDetail implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//名称
private String name;
//订单id
private Long orderId;
//菜品id
private Long dishId;
//套餐id
private Long setmealId;
//口味
private String dishFlavor;
//数量
private Integer number;
//金额
private BigDecimal amount;
//图片
private String image;
}
Mapper接口 OrderMapper、OrderDetailMapper
@Mapper
public interface OrdersMapper extends BaseMapper {
}
@Mapper
public interface OrderDetailMapper extends BaseMapper {
}
业务层接口 OrderService、OrderDetailService
public interface OrdersService extends IService {
}
public interface OrderDetailService extends IService {
}
业务层实现类 OrderServiceImpl、OrderDetailServiceImpl
@Service
public class OrdersServiceImpl extends ServiceImpl implements OrdersService {
}
@Service
public class OrderDetailServiceImpl extends ServiceImpl implements OrderDetailService {
}
控制层 OrderController、OrderDetailController
@RestController
@RequestMapping("/orders")
public class OrdersController {
@Autowired
private OrdersService ordersService;
}
@RestController
@RequestMapping("/orderDetail")
public class OrderDetailController {
@Autowired
private OrderDetailService orderDetailService;
}
补充得到默认地址:
@GetMapping("/default")
public R getDefault(){
Long currentId = BaseContext.getCurrentId();
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId,currentId)
.eq(AddressBook::getIsDefault,1);
AddressBook addressBook = addressBookService.getOne(queryWrapper);
return R.success(addressBook);
}
插入数据到orders表和orderDetail表:
@Service
public class OrdersServiceImpl extends ServiceImpl implements OrdersService {
@Autowired
private ShoppingCartService shoppingCartService;
@Autowired
private UserService userService;
@Autowired
private AddressBookService addressBookService;
@Autowired
private OrderDetailService orderDetailService;
@Override
@Transactional
public void submit(Orders orders) {
//获得当前用户id
Long currentId = BaseContext.getCurrentId();
//查询当前用户的购物车数据
LambdaQueryWrapper shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId,currentId);
List shoppingCarts = shoppingCartService.list(shoppingCartLambdaQueryWrapper);
if(shoppingCarts==null || shoppingCarts.size()==0){
throw new CustomException("购物车为空,不能下单!");
}
//查询用户数据
User user = userService.getById(currentId);
//查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if(addressBook==null){
throw new CustomException("用户地址信息有误,不能下单!");
}
//向订单表插入数据,一条数据
long orderId = IdWorker.getId();
AtomicInteger amount=new AtomicInteger(0);
List 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());
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(currentId);
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(shoppingCartLambdaQueryWrapper);
}
}
@PostMapping("/submit")
private R submit(@RequestBody Orders orders){
ordersService.submit(orders);
return R.success("提交订单成功!");
}
效果图:
《瑞吉外卖》基础部分完结,恭喜大家❀~