哈喽!大家好,我是旷世奇才李先生
文章持续更新,可以微信搜索【小奇JAVA面试】第一时间阅读,回复【资料】更有我为大家准备的福利哟,回复【项目】获取我为大家准备的项目
最近打算把我手里之前做的项目分享给大家,这个苍穹外卖系统是跟着B站上的一个视频做的
大家可以根据视频自己学习一下,或者直接拿我做好的项目直接去用也可以。
文章目录
- 一、后台管理系统前端页面
- 1、登录密码加密
- 2、Swagger引入
- 3、Swagger常用注解
- 4、对象属性拷贝
- 5、SQL异常处理(新增相同用户名员工异常捕获)
- 6、ThreadLocal用来存储用户的信息,用于新增用户时将添加人属性赋值
- 7、分页查询员工
- 8、日期格式化处理
- 9、公共字段代码填充(通过AOP和反射将公共字段进行填充)
- 10、使用阿里云来存储图片
- 11、使用redis来做店铺状态功能
- 12、使用Sping-Cache来缓存套餐数据
- 12、使用Sping-Task来执行定时任务
- 13、使用WebSocket实现下单提醒与催单提醒
- 二、微信小程序用户端页面展示
- 三、商家后端页面展示
- 四、总结
一、后台管理系统前端页面
1、登录密码加密
使用spring自带的加密工具类进行md5加密
password = DigestUtils.md5DigestAsHex(password.getBytes());
2、Swagger引入
首先引入依赖
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.2</version>
</dependency>
然后配置基础信息,以及要扫描的controller包位置
package com.sky.config;import com.sky.interceptor.JwtTokenAdminInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;/*** 配置类,注册web层相关组件*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;/*** 注册自定义拦截器** @param registry*/protected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");registry.addInterceptor(jwtTokenAdminInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/employee/login");}/*** 通过knife4j生成接口文档* @return*/@Beanpublic Docket docket() {log.info("准备生成接口文档...");ApiInfo apiInfo = new ApiInfoBuilder().title("苍穹外卖项目接口文档").version("2.0").description("苍穹外卖项目接口文档").build();Docket docket = new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo).select().apis(RequestHandlerSelectors.basePackage("com.sky.controller")).paths(PathSelectors.any()).build();return docket;}/*** 设置静态资源映射* @param registry*/protected void addResourceHandlers(ResourceHandlerRegistry registry) {log.info("开始设置静态资源映射...");registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");}
}
然后访问页面
http://localhost:8080/doc.html
3、Swagger常用注解
4、对象属性拷贝
//对象属性拷贝,DTO的属性名和实体类的属性名相同BeanUtils.copyProperties(employeeDTO,employee);
5、SQL异常处理(新增相同用户名员工异常捕获)
/*** @Description: 处理SQL异常* @Author: KSQC*/@ExceptionHandlerpublic Result exceptionHandler(SQLIntegrityConstraintViolationException ex){//Duplicate entry 'zhangsan' for key 'idx_username'String message = ex.getMessage();if(message.contains("Duplicate entry")){String[] split = message.split(" ");String username = split[2];String msg = username + "已存在";return Result.error(msg);}else {return Result.error("未知错误");}}
6、ThreadLocal用来存储用户的信息,用于新增用户时将添加人属性赋值
首先创建一个工具类
public class BaseContext {public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();public static void setCurrentId(Long id) {threadLocal.set(id);}public static Long getCurrentId() {return threadLocal.get();}public static void removeCurrentId() {threadLocal.remove();}}
登录的时候将用户信息放入ThreadLocal中
//将用户id放入线程中去BaseContext.setCurrentId(empId);
新增用户的时候将添加人信息赋值
//设置当前记录创建人id和修改人idemployee.setCreateUser(BaseContext.getCurrentId());employee.setUpdateUser(BaseContext.getCurrentId());
7、分页查询员工
首先引入依赖pagehelper,这是mybatis提供的分页插件
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>${pagehelper}</version></dependency>
然后进行分页查询代码开发
/*** @Description: 分页查询* @Author: KSQC*/public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {//开始分页查询,传入参数为第几页和每页展示多少行。PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());//普通正常查询,因为PageHelper会将分页信息存入当前线程,会在正常查询时进行拼接。Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);//将PageResult需要的属性获取出来进行返回。long total = page.getTotal();List<Employee> records = page.getResult();return new PageResult(total,records);}
8、日期格式化处理
第一种方法:直接在属性上加注解
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createTime;
第二种方法:配置全局消息转换器
/*** @Description: 扩展SpringMVC框架的消息转化器* @Author: KSQC*/protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {log.info("扩展消息转换器...");//创建一个消息转换器对象MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化json数据converter.setObjectMapper(new JacksonObjectMapper());//将自己的消息转化器加入容器中converters.add(0,converter);}
转换器实体类如下:
package com.sky.json;import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;/*** 对象映射器:基于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_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";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(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);}
}
9、公共字段代码填充(通过AOP和反射将公共字段进行填充)
package com.sky.aspect;/*** @Author: KSQC* @Description: ${description}* @Date: 2023/8/8 21:08*/import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.time.LocalDateTime;/*** @Description 自定义切面,实现公共字段自动填充处理逻辑* @Author LiShiQi* @Date 2023/8/8 21:08* @Version 1.0*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入点*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知,在通知中进行公共字段的赋值*/@Before("autoFillPointCut()")public void autoFill(JoinPoint joinPoint){log.info("开始进行公共字段自动填充...");//获取到当前被拦截的方法上的数据库操作类型MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象OperationType operationType = autoFill.value();//获取到当前被拦截的方法的参数--实体对象Object[] args = joinPoint.getArgs();if(args == null || args.length == 0){return;}Object entity = args[0];//准备赋值的数据LocalDateTime now = LocalDateTime.now();Long currentId = BaseContext.getCurrentId();//根据当前不同的操作类型,为对应的属性通过反射来赋值if(operationType == OperationType.INSERT){//为4个公共字段赋值try {Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);//通过反射为对象属性赋值setCreateTime.invoke(entity,now);setCreateUser.invoke(entity,currentId);setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {e.printStackTrace();}}else if(operationType == OperationType.UPDATE){//为2个公共字段赋值try {Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);//通过反射为对象属性赋值setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {e.printStackTrace();}}}}
10、使用阿里云来存储图片
配置阿里云地址等相关参数
alioss:endpoint: oss-cn-beijing.aliyuncs.comaccess-key-id: LTAI5tPeFLzsPPT8gG3LPW64access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7bucket-name: sky-itcast
新建一个阿里云配置类
package com.sky.config;import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @Author: KSQC* @Description: 配置类,用于创建AliOssUtil对象* @Date: 2023/8/9 9:40*/
@Configuration
@Slf4j
public class OssConfiguration {@Bean@ConditionalOnMissingBeanpublic AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);return new AliOssUtil(aliOssProperties.getEndpoint(),aliOssProperties.getAccessKeyId(),aliOssProperties.getAccessKeySecret(),aliOssProperties.getBucketName());}
}
调用工具类上传图片到阿里云,返回图片地址用于前端展示
//文件的请求路径String filePath = aliOssUtil.upload(file.getBytes(),objectName);
11、使用redis来做店铺状态功能
在spring下配置redis
# redis 配置redis:# 地址host: localhost# 端口,默认为6379port: 6379# 数据库索引database: 0# 密码password: 123456# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最大空闲连接max-idle: 8# 连接池的最大数据库连接数max-active: 8# #连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms
创建一个RedisConfiguration类
package com.sky.config;/*** @Author: KSQC* @Description: ${description}* @Date: 2023/8/9 15:21*/import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** @Description* @Author LiShiQi* @Date 2023/8/9 15:21* @Version 1.0*/
@Configuration
@Slf4j
public class RedisConfiguration {@Beanpublic RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){log.info("开始创建redis模板对象...");RedisTemplate redisTemplate = new RedisTemplate();//设置redis的连接工厂对象redisTemplate.setConnectionFactory(redisConnectionFactory);//设置redis key的序列化器redisTemplate.setKeySerializer(new StringRedisSerializer());return redisTemplate;}
}
设置店铺状态
/*** @Description: 设置店铺的营业状态* @Author: KSQC*/@PutMapping("/{status}")@ApiOperation("设置店铺的营业状态")public Result setStatus(@PathVariable Integer status){log.info("设置店铺的营业状态为:{}",status == 1 ? "营业中" : "打烊中");redisTemplate.opsForValue().set(KEY,status);return Result.success();}
12、使用Sping-Cache来缓存套餐数据
首先引入依赖,除了引入spring-cache依赖还需要引入redis依赖,因为spring-cache底层是需要使用到一个缓存中间件的,可支持多种中间件,目前我们使用redis作为缓存中间件。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency>
常用的注解,当我们引入完依赖后,就可以使用注解的方式来进行缓存操作。
首先启动类开启注解功能
然后找到我们需要缓存的查询方法加上注解,这里会将“setmealCache”作为前缀,然后加上我们的分类id一起作为key,然后将查询出来的数据作为value,放入redis中, 当我们第二次查询的时候这个时候redis中有数据,直接就会返回我们redis中的数据就不再去查询数据库,大大减轻了我们数据库的压力。
新增套餐的时候清除缓存,因为新增加了套餐页面上要展示出来,但是缓存中是旧数据,所以我们要清理缓存,然后再重新查询数据库,重新缓存。这里我们根据分类id进行清理,在哪个分类下新增套餐,就将哪个分类缓存清理。
批量删除套餐的时候我们进行批量清理,将所有前缀为“setmealCache”的全部清理。
12、使用Sping-Task来执行定时任务
这个task的依赖在context包里包含了,所以我们不需要额外引入依赖了。
我们需要先在启动类上添加注解"@EnableScheduling"
然后在需要定时执行的方法上添加注解,并写上执行时间,这里使用cron语法,不会的可以百度查询一下。
13、使用WebSocket实现下单提醒与催单提醒
首先引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
新建一个WebSocketConfiguration配置类
package com.sky.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** WebSocket配置类,用于注册WebSocket的Bean*/
@Configuration
public class WebSocketConfiguration {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}
然后建立一个WebSocketServer功能类
package com.sky.websocket;import org.springframework.stereotype.Component;import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;/*** WebSocket服务*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {//存放会话对象private static Map<String, Session> sessionMap = new HashMap();/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("sid") String sid) {System.out.println("客户端:" + sid + "建立连接");sessionMap.put(sid, session);}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, @PathParam("sid") String sid) {System.out.println("收到来自客户端:" + sid + "的信息:" + message);}/*** 连接关闭调用的方法** @param sid*/@OnClosepublic void onClose(@PathParam("sid") String sid) {System.out.println("连接断开:" + sid);sessionMap.remove(sid);}/*** 群发** @param message*/public void sendToAllClient(String message) {Collection<Session> sessions = sessionMap.values();for (Session session : sessions) {try {//服务器向客户端发送消息session.getBasicRemote().sendText(message);} catch (Exception e) {e.printStackTrace();}}}}
然后我们在具体业务代码中去调用方法向前端发送消息,例如下单成功后发送消息。
//通过websocket向客户端浏览器推送消息 type orderId contentMap map = new HashMap();map.put("type",1); // 1表示来单提醒 2表示客户催单map.put("orderId",ordersDB.getId());map.put("content","订单号:" + outTradeNo);String json = JSON.toJSONString(map);webSocketServer.sendToAllClient(json);
下面是测试用例前端代码,前端需要启动后就与后端建立WebSocket连接,这样才能进行后续的通信。
<!DOCTYPE HTML>
<html>
<head><meta charset="UTF-8"><title>WebSocket Demo</title>
</head>
<body><input id="text" type="text" /><button onclick="send()">发送消息</button><button onclick="closeWebSocket()">关闭连接</button><div id="message"></div>
</body>
<script type="text/javascript">var websocket = null;var clientId = Math.random().toString(36).substr(2);//判断当前浏览器是否支持WebSocketif('WebSocket' in window){//连接WebSocket节点websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);}else{alert('Not support websocket')}//连接发生错误的回调方法websocket.onerror = function(){setMessageInnerHTML("error");};//连接成功建立的回调方法websocket.onopen = function(){setMessageInnerHTML("连接成功");}//接收到消息的回调方法websocket.onmessage = function(event){setMessageInnerHTML(event.data);}//连接关闭的回调方法websocket.onclose = function(){setMessageInnerHTML("close");}//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。window.onbeforeunload = function(){websocket.close();}//将消息显示在网页上function setMessageInnerHTML(innerHTML){document.getElementById('message').innerHTML += innerHTML + '<br/>';}//发送消息function send(){var message = document.getElementById('text').value;websocket.send(message);}//关闭连接function closeWebSocket() {websocket.close();}
</script>
</html>
二、微信小程序用户端页面展示
待授权页面
确认授权页面
首页
选择口味页
购物车页面
提交订单页面
点击头像页面
历史订单页面
地址管理页面
三、商家后端页面展示
工作台展示页面
数据统计页面
订单管理页面
菜品管理页面
分类管理页面
员工管理页面
店铺状态修改页面
四、总结
项目涉及的功能还是比较全面的,建议大家跟着视频做一遍。可以关注公众号回复【项目】领取项目,如果有用就点赞支持一下吧。
文章持续更新,可以微信搜索【小奇JAVA面试】第一时间阅读,回复【项目】获取我为大家准备的项目