苍穹外卖项目,目前正在二刷。
记录模块存在功能问题以及涉及技术加以总结,希望大家一起学习进步。😍 🥰 😘😍 🥰 😘😍 🥰 😘
开发环境搭建
问题:前端发送的请求,是如何请求到后端服务的?
前端请求地址:http://localhost/api/employee/login
后端接口地址:http://localhost:8080/admin/employee/login
原因:Nignx反向代理,将前端发送的动态请求由nignx转发到后端服务器
反向代理:是指代理服务器接收客户端的请求,然后将请求转发给后端服务器,并将后端服务器的响应 返回给客户端。反向代理隐藏了服务器的真实身份和位置信息,客户端只知道与反向代理进⾏通信,而不知道真正的服务器。
反向代理好处:
- 提⾼访问速度:Nignx本身就可以进⾏缓存
- 进⾏负载均衡:指定的⽅式均衡的分配给集群中的每台服务器
- 保证后端服务安全
Nignx反向代理的配置方式:
server{
listen 80;
server name localhost;
location /api/{
proxy_pass http://localhost:8080/admin/;#反向代理
}
}
Nignx负载均衡的配置方式:
upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
server{
listen 80;
server name localhost;
location /api/{
proxy_pass http://webservers/admin/;#负载均衡
}
}
员工管理模块
新增员工
问题一:新增员工时用户名是唯一的,需要全局异常处理来捕获
@ExceptionHandler
public Result exceptionHandler(sQLIntegrityConstraintViolationException ex){
//Duplicate entry 'zhangsan'for key 'employee.idx_username
String message =ex.getMessage();
if(message.contains("Duplicate entry")){
String[]split=message.split( regex:"");
String username = split[2];
String msg=username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
问题二:创建人以及更新人是动态的而不是唯一的,需要动态获取登录员工的id
第一步:登录成功之后生成JWT令牌
Map<string,object>claims=new HashMap<>();
claims.put(Iwtclaimsconstant.EMP_ID,employee.getId());
string token = Jwtutil.createJwT(
jwtProperties.getAdminsecretkey()
jwtProperties.getAdminTtl(),
claims);
第二步:通过JWT令牌解析出当前登录员工id
//从请求头中获取令牌
string token=request.getHeader(jwtProperties.getAdminTokenName());
//校验令牌
try{
Claims claims = Jwtutil.parseIwT(iwtProperties.getAdminsecretKey(),token):
Long empId = Long.value0f(claims.get(Iwtclaimsconstant.EMP_ID).tostring());
BaseContext.setCurrentId(empId)
//通过,放行
return true;
}catch(Exception ex)
//不通过,响应401状态码
response.setstatus(401);
return false;
}
第三步:解析出来登录员工id之后,传递给业务层的save方法
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();
}
}
public void save(EmployeeDTo employeeDTO){
Employee employee =new Employee();
//对象属性拷贝
BeanUtils.copyProperties(employeeDTo,employee);
//设置账号的状态,默认正常状态1表示正常 0表示锁定
employee.setstatus(statusConstant.ENABLE);状态常量
//设置密码,默认密码123456
employee.setPassword(Digestutils.mdsDigestAsHex(PasswordConstant,DEFAULT_PASSWORD.getBytes()));
//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置当前记录创建入id和修改人id
// TOD0 后期需要改为当前登录用户的id
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
}
ThreadLocal 为每⼀个线程提供⼀个单独的存储空间,具有线程隔离的作⽤,只有在同⼀个线程内才可以获得他的值,以保证线程安全。
ThreadLocal存储在多个⽅法中需要共享的数据,具体来说是项⽬中的⽤户id,其他⽅法需要调⽤这个参数,就不需要显示传递。ThreadLocal底层原理是通过⼀个ThreadLocalMap来实现的。在每⼀个线程中都有⼀个ThreadLocalMap,⽤于存储线程局部变量。ThreadLocalMap中的键是ThreadLocal对象,值是对应线程的变量副本。当⼀个线程需要获取变量值时,⾸先会获取⾃⼰线程的ThreadLocalMap,并根据ThreadLocal对象获取对应的变量副本。实现了每个现象都拥有⾃⼰独⽴的变量副本,互不影响。
员工分页查询
问题一:如何实现分页查询
PageHelper实现了⼀个PageInterceptor拦截器,Mybatis会加载这个拦截器到拦截器链中。在使⽤过程中先使⽤PageHelper.startPage()方法在当前线程上下⽂中设置⼀个ThreadLocal变量,再利 ⽤PageInterceptor分⻚拦截器拦截,从ThreadLocal中拿到分⻚的信息,如果有分⻚信息拼装分⻚SQL(limit语句等)语句进⾏分⻚查询,最后再把ThreadLocal中的东⻄清除掉。
问题二:时间返回的格式不符合需求
方法一:在属性上加入注解,对日期进行格式化
@JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime
方法二:在 WebMvcConfiguration 中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){
Log.info("开始扩展消息转换器..");
//创建一个消息转化器对象
MappingJackson2HttpMessageConverter convrter = new MappingJackson2HttpMessageconverter();
//设置对象转换器,可以将]ava对象转为ison字符串
converter.setobjectMapper(new JacksonobjectMapper());
//将我们自己的转换器放入spring Mvc框架的容器中
converters.add(0,converter);
}
公共自动填充问题(AOP、注解、反射、枚举)
存在问题:业务表中的出现的公共字段,导致程序中出现冗余的代码,后期变更维护不方便
解决方法:使⽤AOP切⾯编程,实现功能增强,完成公共字段⾃动填充功能
实现步骤:
1. ⾃定义注解AutoFill,⽤于标识需要进⾏公共字段⾃动填充的⽅法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill{
// 数据库操作类型 枚举:UPDATE INSERT
OperationType value();
}
2. ⾃定义切⾯类AutoFillAspect,统⼀拦截加⼊了AutoFill注解的⽅法,通过反射为公共字段赋值
@Aspect
@Component
@slf4j
public class AutoFillAspect {
//切入点
@Pointcut("execution(* com.sky.mapper.*,*(..))&& @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
//前置通知,在通知中进行公共字段的赋值
@Before("autoFillPointCut()")
public void adtoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填充...");
//获取到当前被拦裁的方法上的数据库操作类型
Methodsignature signature=(Methodsignature)joinPoint.getsignature();//方法然名对象
AutoFill autoFi1l =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( name: "setCreateTime", LocalDateTime.class)
Method setCreateUser = entity.getClass().getDeclaredMethod( name:
"setCreateUser",Long.class);
Method setUpdateTime = entity.getclass().getDeclaredMethod( name: "setUpdateTime", LocalDateTime.class)
Method setUpdateUser = entity.getClass().getDeclaredMethod( name: "setUpdateUser", Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUptateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
catch(Exception e){
e.printstackTrace();
}
}
3. 在Mapper的⽅法上加上AutoFill注解
AOP核心概念:
反射概念:
反射通过获取类的信息并动态调⽤⽅法、创建对象等。让程序能够在运⾏时根据需要动态地获取和操作类的结构和成员。
获取 Class 对象: 程序通过类的全限定名、对象的getClass ()⽅法或.Class 语法来获取对应的 Class 对象。
查询类信息: 通过 Class 对象可以获取类的信息,包括类名、包名、⽗类、实现的接⼝、构造函数、⽅法、字段等。
动态创建对象: 通过 Class 对象的 newInstance ()⽅法调⽤类的默认构造函数来创建对象,或者通过 Constructor 对象调⽤类的其他构造函数来创建对象。
动态调⽤⽅法: 通过 Method 对象调⽤类的⽅法,传递参数并获取返回值。
动态访问字段: 通过 Field 对象获取和设置类的字段值。
店铺营业状态
Redis常用数据类型:字符串string、哈希hash、列表list、集合set、有序集合sorted set / zset
Spring Data Redis使用方法:
编写配置类,创建RedisTemplate,通过RedisTemplate对象操作Redis
@configuration
@slf4j
public class RedisConfiguration{
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionfactory){
Log.info("开始创建redis模板类...");
RedisTemplate redisTemplate=new RedisTemplate();
// 设置key的序列化器,默认为IdkserializationRedisserializer
redisTemplate.setKeySerinlizer(new stringRedisSerializer())
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
问题:营业状态对于用户端是高频查询字段,
解决:营业状态数据存储方式基于Redis的字符串来进行存储,Redis将数据放到缓存中,减少高频查询给磁盘带来的压力
@PutMapping("/{status}")
@Apioperation("设置店铺的营业状态”)
public Result setstatus(@PathVariable Integer status){
Log.info("设置唐铺的营业状态为:{}”,statusm1?“营业中”:“打烊中”);
redisTemplate.opsForValue().set("SHOP_STATUs",status);
return Result.success();
}
@GetMapping("/status")
@Apioperation("获取店铺的营业状态”)
public Result<Integer> getstatus(){
Integer status = (Integer)redisTemplate,opsForValue().get("SHOP_STATUS");
Log.info("获取到店铺的营业状态为:{}",status1?"营业中”:"打烊中");
return Result.success(status);
}
微信登录
Httpclient:
Httpclient是⼀个服务器端进⾏ HTTP 通信的库,使得后端可以发送各种 HTTP 请求和接收 HTTP 响应,使⽤Httpclient,可以轻松的发送 GET, POST, PUT, DELETE 等各种类型的的请求。 在项⽬中,进⾏微信登录开发时,后端在使⽤登录凭证校验接⼝的时候就需要通过Httpclient发送指定请求到给定的URL中。
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTo userLoginDT0){
Log.info("微信用户登录:{}",userLoginDT0.getcode());
//微信登录
User user = userService.wxLogin(userLoginDT0);
//为微信用户生成jwt令牌
Map<String,Object>claims =new HashMap<>();
claims.put(IwtclaimsConstant.USER ID,user.getId());
String token = JwtUtil.create]wT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(), claims)
UserLoginvo userLoginvo =tserLoginvo.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO)
}
public User wxLogin(UserLoginDTo userLoginDT0){
//调用微信接口服务,获得当前微信用户的openid
Map<String,String> map = new HashMap<>();
map.put("appid",wechatProperties.getAppid());
map.put("secret",weChatProperties.getsecret());
map.put("js_code",userLoginDTo.getcode());
map.put("grant_type","authorization_code");
String json = Httpclientutil.doGet(Wx_LOGIN, map);
JsONobject jsonobject =JsoN.parseobject(json);
String openid=jsonobject.getstring(key: "openid");
if(openid == null){
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
//判断当前用户是否为新用户
User user =userMapper.getByOpenid(openid);
//如果是新用户,自动完成注册
if(user == null){
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}
//返回这个用户对象
return user;
}
}
缓存菜品
存在问题:用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问比较大,数据库访问压力随之增大
解决问题:通过Redis缓存菜品数据,减少数据库查询操作
缓存逻辑分析:
- 每个分类下的菜品保持一份缓存数据
- 数据库中菜品数据有变更时清理缓存数据(新增、修改、批量删除、起售、停售菜品)
@GetMapping("/list")
@Api0peration("根据分类id査询菜品")
public Result<List<Dishvo>> list(Long categoryId){
//构造redis中的key.规则:dish_分类id
String key = "dish_" + categoryId;
//査询redis中是否存在菜品数据
List<Dishvo>list = (List<Dishvo>)redisTemplate.opsForValue().get(key);
if(list !=null 88 list.size()>0){
//如果存在、直按返回。无须查询数据库
return Result.success(list);
}
Dish dish = new Dish();
dish.setcategoryId(categoryId);
dish.setstatus(statusconstant.ENABLE);//查询起售中的菜品
//如果不存在、查询数据库、将查询到的数据放入redis中
list = dishService.listwithFlavor(dish);
redisTemplate.opsForValue().set(key, list);
return Result.success(list);
}
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTo dishDTo){
log.info("新增菜品:{}",dishDTO);
dishservice.savewithFlavor(dishDT0);
//清理缓存数据
String key = "dish " + dishDTo.getCategoryId()
cleanCache(key);
return Result.success();
}
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern)
redisTemplate.delete(keys);
}
缓存套餐
Spring Cache:基于注解的缓存功能
实现思路:
导入Spring Cache和Redis相关maven坐标
在启动类上加入@EnableCaching注解,开启缓存注解功能
在用户端接口SetmealController的 list 方法上加入@Cacheable注解
在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入@CacheEvict注解
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
@Cacheable(cacheNames="setmealCache",key="#categoryId")//key: setmealCache:: 100
public Result<List<Setmeal>>list(Long categoryId){
Setmeal setmeal = new Setmeal();
setmeal.setCategoryId(categoryId);
setmeal.setstatus(statusConstant.ENABLE);
List<Setmeal>list=setmealservice.list(setmeal);
return Result.success(list);
}
//精确清理
@PostMapping
@ApiOperation("新增套餐")
@CacheEvict(cacheNames = "setmealCache",key= "#setmealDT0.categoryId")//key: setmealCache:: 100
public Result save(@RequestBody setmealDTo setmealDTo){
setmealService.saveWithDish(setmealDTo);
return Result.success();
}
//批量、更新、起售、停售需要清理所有缓存数据
@DeleteMapping
@ApiOperation("批量删除套餐")
@CacheEvict(cacheNames="setmealCache",allEntries = true)
public Result delete(@RequestParam List<Long>ids){
setmealservice.deleteBatch(ids);
return Result.success();
}