项目介绍:
定位:
专门为餐饮企业(餐厅、饭店)定制的一款软件商品
分为:
管理端:外卖商家使用
用户端(微信小程序):点餐用户使用。
功能架构:
(体现项目中的业务功能模块)
技术选型:
(展示项目中使用到的技术框架和中间件等)
环境搭建
整体架构:
前端:
管理端使用nginx,使用已经打包好的前端代码,并且Nginx的配置文件中已经配置了反向代理,通过此配置可以将前端请求转发到后端服务。
用户端之后再做介绍
后端:
后端框架:
1.sky-common子模块中存放的是一些工具类,可以供其他模块使用
2.sky-pojo子模块中存放的是一些entity(实体类),DTO,VO
3.sky-server子模块中存放的是配置文件、配置类、拦截器、controller、service、mapper、启动类等
版本控制
使用Git进行版本控制
数据库:
前后端联调:
使用nginx反向代理:
nginx反向代理:
好处:
使用方法:
接口文档:
前后端分离开发流程:
设计阶段:
Apifox:
Apifox是设计阶段使用的工具,管理和维护接口。
开发阶段:
Swagger:
Swagger是在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
介绍:
使用方式:
1.
2.
3.
常用注解:
管理端业务功能编写:
登录功能:
完善登录功能使用MD5对数据加密
员工管理:
添加员工:
添加员工时通过ThreadLocal获取执行人id和创建人id
ThreadLocal介绍:
并且客户端发起的每一次请求都是一个单独的线程,这样就满足使用ThreadLocal的条件。
ThreadLocal常用方法:
员工分页查询:
调整LocalDataTime类型数据响应回去的格式
推荐第二种,只需要一次编码就可以调整全部指定内容
操作过程:
1.新建一个对象映射器:
/*** 对象映射器:基于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);}
}
2.在配置类中添加如下代码:
/*** 扩展MVC框架的消息转换器* @param converters*/@Overrideprotected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {log.info("开始扩展消息转换器");//创建一个消息转换器对象MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();//设置对象转换器,可以将java对象转为json对象converter.setObjectMapper(new JacksonObjectMapper());//将自己的转换器放入spring MVC框架的容器中,并放到0索引位置,排到第一个,这样一定会使用它converters.add(0, converter);}
公共字段自动填充:
枚举类:
/*** 数据库操作类型*/
public enum OperationType {/*** 更新操作*/UPDATE,/*** 插入操作*/INSERT}
自定义注解:
/*** 自定义注解,用于标识需要进行公共字段自动填充的方法*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {/*** 枚举类 自定义数据库操作类型insert和update操作* @return*/OperationType value();}
自定义切面类:
/*** 自定义切面,实现公共字段自动填充处理逻辑*/
@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();Object entity = args[0];//准备赋值的数据LocalDateTime now = LocalDateTime.now();Long currentId = BaseContext.getCurrentId();//根据当前不同的操作类型,为对应的属性通过反射来赋值if(operationType == OperationType.INSERT) {try {Method setCreateTime = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);Method setCreateUser = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class);Method setUpdateTime = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getMethod(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) {try {Method setUpdateTime = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {e.printStackTrace();}}}}
给mapper层的insert和update数据库操作类型添加自定义的注解:
@AutoFill(OperationType.UPDATE)或@AutoFill(OperationType.INSERT)
注意点:
·在菜品分页查询部分,可能传来的参数:status状态在动态SQL里只能判断其为null,不能加上and status = '',这样会一直读取不到它,具体原因不清楚。
·<set>标签下的<if>标签中的内容记得加逗号
操作Redis:
想要了解Redis的基础知识及使用方法可以参考作者另一边文章《Java_中间件——Redis》
Redis的Java客户端:
下面我们使用Spring Data Redis来操作Redis
Spring Data Redis使用方式:
配置类RedisTemplate:
@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;}}
代码演示:
@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void testRedisTemplate() {System.out.println(redisTemplate);//操作字符串ValueOperations valueOperations = redisTemplate.opsForValue();//操作HashHashOperations hashOperations = redisTemplate.opsForHash();//操作ListListOperations listOperations = redisTemplate.opsForList();//操作setSetOperations setOperations = redisTemplate.opsForSet();//操作有序集合ZSetOperations zSetOperations = redisTemplate.opsForZSet();}/*** 操作字符串类型的数据*/@Testpublic void testStirng() {// set get setex setnxredisTemplate.opsForValue().set("city","北京");String city = (String) redisTemplate.opsForValue().get("city");System.out.println(city);redisTemplate.opsForValue().set("code","6562",60, TimeUnit.SECONDS);redisTemplate.opsForValue().setIfAbsent("lock","1");redisTemplate.opsForValue().setIfAbsent("lock","2");}/*** 操作哈希类型的数据*/@Testpublic void testHash() {//hset hget hdel hkeys hvalsHashOperations hashOperations = redisTemplate.opsForHash();hashOperations.put("100","name","xiaobai");hashOperations.put("100","age","20");String name = (String) hashOperations.get("100", "name");System.out.println(name);Set keys = hashOperations.keys("100");System.out.println(keys);List values = hashOperations.values("100");System.out.println(values);hashOperations.delete("100","age");}/*** 操作列表类型的数据*/@Testpublic void testList() {// lpush lrange rpop llenListOperations listOperations = redisTemplate.opsForList();listOperations.leftPushAll("mylist","a","b","c");listOperations.leftPush("mylist","d");List mylist = listOperations.range("mylist", 0, -1);System.out.println(mylist);listOperations.rightPop("mylist");Long size = listOperations.size("mylist");System.out.println(size);}/*** 操作集合类型的数据*/@Testpublic void testSet() {// sadd smembers scard sinter sunion sremSetOperations setOperations = redisTemplate.opsForSet();setOperations.add("set1","a","b","c");setOperations.add("set2","b","c","d");Set set1 = setOperations.members("set1");System.out.println(set1);Long size = setOperations.size("set1");System.out.println(size);Set intersect = setOperations.intersect("set1", "set2");System.out.println(intersect);Set union = setOperations.union("set1", "set2");System.out.println(union);setOperations.remove("set1","a","b");}/*** 操作有序集合类型的数据*/@Testpublic void testZSet() {// zadd zrange zincrby zremZSetOperations zSetOperations = redisTemplate.opsForZSet();zSetOperations.add("zset1","a",10);zSetOperations.add("zset1","b",11);zSetOperations.add("zset1","c",9);Set zset1 = zSetOperations.range("zset1", 0, -1);System.out.println(zset1);zSetOperations.incrementScore("zset1","c",10);zSetOperations.remove("zset1","a","b");}/*** 通用命令操作*/@Testpublic void testCommon() {// keys exists type delSet keys = redisTemplate.keys("*");System.out.println(keys);Boolean name = redisTemplate.hasKey("name");Boolean set1 = redisTemplate.hasKey("set1");for (Object key : keys) {DataType type = redisTemplate.type(key);System.out.println(type.name());}redisTemplate.delete("mylist");}
店铺营业状态设置:
基于Redis的字符串来进行存储
1表示营业,0表示打样
代码演示:
管理端设置营业状态、获取营业状态
@RequestMapping("/admin/shop")
@RestController("adminShopController")
@Slf4j
@Api(tags = "店铺相关接口")
public class ShopController {public static final String KEY = "SHOP_STATUS";@Autowiredprivate RedisTemplate redisTemplate;/*** 设置店铺的营业状态* @param status* @return*/@PutMapping("/{status}")@ApiOperation("设置营业状态")public Result setStatus(@PathVariable Integer status) {redisTemplate.opsForValue().set(KEY,status);log.info("设置当前营业状态为:{}",status == 1 ? "营业中" : "打烊中");return Result.success();}@GetMapping("/status")@ApiOperation("管理端获取营业状态")public Result<Integer> getStatus() {Integer status = (Integer) redisTemplate.opsForValue().get(KEY);log.info("当前的营业状态为:{}",status == 1 ? "营业中":"打烊中");return Result.success(status);}}
用户端获取营业状态
@RequestMapping("/user/shop")
@RestController("userShopController")
@Slf4j
@Api(tags = "店铺相关接口")
public class ShopController {public static final String KEY = "SHOP_STATUS";@Autowiredprivate RedisTemplate redisTemplate;@GetMapping("/status")@ApiOperation("用户端获取营业状态")public Result<Integer> getStatus() {Integer status = (Integer) redisTemplate.opsForValue().get(KEY);log.info("当前的营业状态为:{}",status == 1 ? "营业中":"打烊中");return Result.success(status);}}
HttpClient
使用HttpCilent需要导入依赖:
(如果导入了阿里云OSS的依赖就不需要导入了,因为这个依赖中已经包含了这个依赖)
核心API:
发送请求步骤:
代码演示:
/*** 测试通过HttpClient发送GET方式的请求*/@Testpublic void testGET() throws Exception {//创建HttpClient对象CloseableHttpClient httpClient = HttpClients.createDefault();//创建请求对象HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");//发送请求,接收响应结果CloseableHttpResponse response = httpClient.execute(httpGet);//解析结果//获取服务端返回的状态码int statusCode = response.getStatusLine().getStatusCode();System.out.println("服务端返回的状态码为:" + statusCode);//获取服务端返回数据HttpEntity entity = response.getEntity();String body = EntityUtils.toString(entity);System.out.println("服务端返回的数据为:" + body);//关闭资源response.close();httpClient.close();}/*** 测试通过HttpClient发送GET方式的请求*/@Testpublic void testPOST() throws Exception {//创建HttpClient对象CloseableHttpClient httpClient = HttpClients.createDefault();//创建请求对象HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");JSONObject jsonObject = new JSONObject();jsonObject.put("username", "admin");jsonObject.put("password", "123456");StringEntity entity = new StringEntity(jsonObject.toString());//指定请求编码方式entity.setContentEncoding("UTF-8");//指定数据格式entity.setContentType("application/json");httpPost.setEntity(entity);//发送请求CloseableHttpResponse response = httpClient.execute(httpPost);//解析返回结果int statusCode = response.getStatusLine().getStatusCode();System.out.println("响应码为:" + statusCode);HttpEntity entity1 = response.getEntity();String body = EntityUtils.toString(entity1);System.out.println("响应数据为:" + body);//关闭资源response.close();httpClient.close();}
用户端微信小程序开发:
小程序目录结构:
主体:
页面:
微信登录过程:
Redis缓存菜品、套餐
原因:
使用数据库一直查找会导致系统响应慢、用户体验差。所以使用Redis缓存。
缓存菜品:
实现思路:
Spring Cache:
常用注解:
(cacheName::key)↑
@Cacheable注解在方法执行前会通过动态代理,代理出一个Controller对象,然后判断缓存中是否有数据,如果有,则直接返回缓存数据,不再执行方法,如果没有,就会通过反射来调用方法。
删除全部:
Spring Task
定位:
定时任务框架
cron表达式:
注意:日和周只能一个填数值一个填?
使用Spring Task对超时订单和一直配送中的订单进行处理
WebSocket
介绍:
应用场景:
·视频弹幕
·网页聊天
·体育实况更新
·股票基金报价实时更新
实现步骤:
使用WebSocket来完成来单提醒和客户催单功能。
设计:
Apache POI
介绍:
应用场景:
创新点:
1.用户端根据分类id查看菜品:通过在service、mapper层添加获取在售(status=1)商品/套餐方法,替代通过创建实体类对象进行list方法查找,将查询时间由5~17ms变为4~7ms。
2.将工作台订单数据使用redis存储,实时更新。