项目介绍:
定位:
专门为餐饮企业(餐厅、饭店)定制的一款软件商品
分为:
管理端:外卖商家使用
用户端(微信小程序):点餐用户使用。
功能架构:
(体现项目中的业务功能模块)
技术选型:
(展示项目中使用到的技术框架和中间件等)
环境搭建
整体架构:
前端:
管理端使用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
*/
@Override
protected 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 {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建redis模板对象...");
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
代码演示:
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisTemplate() {
System.out.println(redisTemplate);
//操作字符串
ValueOperations valueOperations = redisTemplate.opsForValue();
//操作Hash
HashOperations hashOperations = redisTemplate.opsForHash();
//操作List
ListOperations listOperations = redisTemplate.opsForList();
//操作set
SetOperations setOperations = redisTemplate.opsForSet();
//操作有序集合
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}
/**
* 操作字符串类型的数据
*/
@Test
public void testStirng() {
// set get setex setnx
redisTemplate.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");
}
/**
* 操作哈希类型的数据
*/
@Test
public void testHash() {
//hset hget hdel hkeys hvals
HashOperations 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");
}
/**
* 操作列表类型的数据
*/
@Test
public void testList() {
// lpush lrange rpop llen
ListOperations 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);
}
/**
* 操作集合类型的数据
*/
@Test
public void testSet() {
// sadd smembers scard sinter sunion srem
SetOperations 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");
}
/**
* 操作有序集合类型的数据
*/
@Test
public void testZSet() {
// zadd zrange zincrby zrem
ZSetOperations 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");
}
/**
* 通用命令操作
*/
@Test
public void testCommon() {
// keys exists type del
Set 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";
@Autowired
private 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";
@Autowired
private 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方式的请求
*/
@Test
public 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方式的请求
*/
@Test
public 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存储,实时更新。