文章目录
- 1. 运营端
- 1.1 服务类别管理
- 1.2 服务项管理
- 1.3 区域管理
- 1.4 区域服务管理
- 2. 客户管理
- 2.1 用户端定位
- 2.2 我的账户设置
- 2.3 实名认证
- 3. 门户
- 3.1 缓存技术方案
1. 运营端
1.1 服务类别管理
增、删、改、查
增:数据库服务类型名字设置唯一索引,服务名字重复则新增失败。
删:先看删除的ID存在不(根据ID查),存在再看服务类别的启用状态(只有草稿状态才能删
,启用或者禁用状态不能删除)
改(内容):在更新完type表后,还需同步更新ES:将待更新数据同步更新到sysn表
改(启用):只有草稿状态和禁用状态才能启用
改(禁用):只有启用状态才能禁用,并且:只有下属的服务项全部为非启用状态
才能禁用。
查:分页查
1.2 服务项管理
增、删、改、查
增:要选择该服务项所属的服务类别
删:只有服务项为草稿状态才能删除。
改(启用):当前状态草稿和禁用状态才能启用,并且所属的服务类型为启用才能启用
改(禁用):当前状态为启用状态,并且没有区域在运营该服务项才能禁用。(如果该服务项在某些区域正在运营将无法禁用,需要先将该服务项在所有区域下架方可禁用。)
要点:
- 启用看左,大范围启用我才能启用,服务类别启用服务项才能启用
- 禁用看右:我如果禁用会影响到谁,没有区域运行该服务项,我这个服务项才能禁用
1.3 区域管理
增、删、改、查
删:只有区域状态为草稿才能删除
改(启用):只有当前区域为草稿和禁用状态才能启用,并且该区域下存在上架的服务才能启用
改(禁用):只有当前区域为启用状态才能禁用,并且该区域下不存在上架的服务才能禁用
1.4 区域服务管理
增、删、改、查
增(向区域添加服务):选择一个地区,根据服务项ID来查看服务是否启用,只有服务项是启用状态才能加入到地区。
删(在区域下删除服务):选择一个地区和该地区下的一个服务,点击删除,只有当前区域状态为草稿才能删除。
改(区域服务上架):确定一个区域和一个服务,点击上架,只有当区域服务状态为草稿或者下架状态,并且:该服务为启用状态(服务项启用状态),才能上架成功。注意:修改成功后需同步数据,向serve_sync表添加记录。
- 上架要加入缓存:@CachePut(value = RedisConstants.CacheName.SERVE, key = “#id”, cacheManager = RedisConstants.CacheManager.ONE_DAY)
改(区域服务下架):确定一个区域和一个服务,点击下架,只有当前区域服务为上架状态才能下架。并且最后:删除serve_sync表的记录。
- 下架要删除缓存:@CacheEvict(value = RedisConstants.CacheName.SERVE, key = “#id”)
2. 客户管理
用户端 :小程序认证
- 先拿到登录凭证,之后再请求微信以登录凭证、appid、app密钥来换取openId,完成认证,在我们数据库如果openId是第一次登录则增加用户记录;将用户信息base64编码再转换为JWT令牌,将token返回给前端。之后前端发请求都会携带token信息,网关先对token解析验证,网关还会有黑名单、白名单的验证逻辑:看看uri是不是在这里面来放行和拒绝访问,然后拿到token解析验证,验证通过后才放行,并且将用户的信息向下传递:写入到http头中,再继续请求微服务,微服务只需要解析head中的用户信息,并且写入到ThreadLocal中,方便应用程序使用。
- GateWay这里是 filter:实现GatewayFilter
- 微服务这里是springMvc:实现HandlerInterceptor(AOP)
gateway代码
package com.jzo2o.gateway.filter;
import cn.hutool.core.util.IdUtil;
import com.jzo2o.common.constants.ErrorInfo;
import com.jzo2o.common.constants.HeaderConstants;
import com.jzo2o.common.model.CurrentUserInfo;
import com.jzo2o.common.utils.Base64Utils;
import com.jzo2o.common.utils.JsonUtils;
import com.jzo2o.common.utils.JwtTool;
import com.jzo2o.common.utils.StringUtils;
import com.jzo2o.gateway.properties.ApplicationProperties;
import com.jzo2o.gateway.utils.GatewayWebUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;/*** token解析过滤器** @author 86188*/
@Slf4j
public class TokenFilter implements GatewayFilter {/*** token header名称*/private static final String HEADER_TOKEN = "Authorization";private ApplicationProperties applicationProperties;public TokenFilter(ApplicationProperties applicationProperties) {this.applicationProperties = applicationProperties;}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.黑名单和白名单校验// 1.1.黑名单校验String uri = GatewayWebUtils.getUri(exchange);log.info("uri : {}", uri);if (applicationProperties.getAccessPathBlackList().contains(uri)) {return GatewayWebUtils.toResponse(exchange,HttpStatus.FORBIDDEN.value(),ErrorInfo.Msg.REQUEST_FORBIDDEN);}// 1.2.访问白名单if (applicationProperties.getAccessPathWhiteList().contains(uri)) {return chain.filter(exchange);}// 2.获取token解析工具JwtTool工具// 2.1.获取tokenString token = GatewayWebUtils.getRequestHeader(exchange, HEADER_TOKEN);if (StringUtils.isEmpty(token)) {return GatewayWebUtils.toResponse(exchange,HttpStatus.FORBIDDEN.value(),ErrorInfo.Msg.REQUEST_FORBIDDEN);}// 2.2.获取tokenKeyString tokenKey = applicationProperties.getTokenKey().get(JwtTool.getUserType(token) + "");if (StringUtils.isEmpty(token)) {return GatewayWebUtils.toResponse(exchange,HttpStatus.FORBIDDEN.value(),ErrorInfo.Msg.REQUEST_FORBIDDEN);}// 2.3.新建toekn解析工具对象jwtTokenJwtTool jwtTool = new JwtTool(tokenKey);// 3.token解析CurrentUserInfo currentUserInfo = jwtTool.parseToken(token);if (currentUserInfo == null) {return GatewayWebUtils.toResponse(exchange,HttpStatus.FORBIDDEN.value(), "登录过期或访问被拒绝");}// 4.用户id和用户类型向后传递String userInfo = Base64Utils.encodeStr(JsonUtils.toJsonStr(currentUserInfo));// 4.1.设置用户信息向下传递GatewayWebUtils.setRequestHeader(exchange,HeaderConstants.USER_INFO, userInfo);// 4.2.设置用户类型向下传递GatewayWebUtils.setRequestHeader(exchange,HeaderConstants.USER_TYPE, currentUserInfo.getUserType() + "");// 4.3.请求idGatewayWebUtils.setRequestHeader(exchange, HeaderConstants.REQUEST_ID, IdUtil.getSnowflakeNextIdStr());// 4.请求放行return chain.filter(exchange);}
}
微服务MVC代码
package com.jzo2o.mvc.interceptor;import com.jzo2o.common.constants.HeaderConstants;
import com.jzo2o.common.model.CurrentUserInfo;
import com.jzo2o.common.utils.Base64Utils;
import com.jzo2o.common.utils.JsonUtils;
import com.jzo2o.mvc.utils.UserContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @author itcast*/
@Slf4j
public class UserContextInteceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.尝试获取头信息中的用户信息String userInfo = request.getHeader(HeaderConstants.USER_INFO);// 2.判断是否为空if (userInfo == null) {return true;}try {// 3.base64解码用户信息String decodeUserInfo = Base64Utils.decodeStr(userInfo);CurrentUserInfo currentUserInfo = JsonUtils.toBean(decodeUserInfo, CurrentUserInfo.class);// 4.转为用户id并保存UserContext.set(currentUserInfo);return true;} catch (NumberFormatException e) {log.error("用户身份信息格式不正确,{}, 原因:{}", userInfo, e.getMessage());return true;}}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 清理用户信息UserContext.clear();}
}
服务人员端:手机验证码登录
- 手机验证流程:后端随机生成6位,短信服务商发送验证码,并且将验证码存储在redis中(5分钟),用户拿到短信验证码后,进入登录流程,拿到用户输入的和redis中存储的是否一样,一样再拿手机号去数据库查,第一次就新增,最后生成token返回。
机构端:账号密码登录(注册和重置密码需要手机号验证)
运营端:账号密码
- 密码采用BCrypt这种hash算法加密,是不可逆的,即使密码一样每次加密后生成的密文是不一样的,存储在数据库中更加安全,加密调用encode方法进行hash加密,解密拿到DB中存储的密文,再由用户输入的密码进行match方法,其实就是也将用户输入的密码进行加密,最后判断两个的hash,来进行密码判断。
2.1 用户端定位
小程序定位功能:小程序定位成功后用户在查询家政服务项目时会根据定位的城市查询该城市有哪些服务项目。
开发:
- 前端会调用微信的wx.getLocation接口,获取当前位置的经纬度。
- 之后拿着经纬度请求高德api(地理逆编码请求:根据经纬度获取地理信息),获取到经纬度对应的地理位置信息,需要配置访问高德接口的key
- 题外话:高德这种第三方组件,如果要用百度地图呢?像动态线程池,用到了redis作为注册中心,那引入了动态线程池组件但是我们还想在项目中用redis,怎么确保redis的bean不会重复?其实可以定义一个开关,@ConditionalOnMissBean,@ConditionalOnProperty(prefix = “amap”, name = “enable”, havingValue = “true”),这种思路就是当开关打开我们才用这个组件,才将bean注入。
2.2 我的账户设置
服务端设置银行账户、机构端设置银行账户
saveOrUpdate(MybatisPlus框架):如果主键没有,则插入,有,则更新
2.3 实名认证
题外话:服务端和机构端提交了认证信息,在运营端进行审核。
服务端的功能模块:
机构端的功能模块:
运营端功能模块:
认证流程:服务人员或者机构提交认证,运营端查询到待审核的记录进行审核:服务人员提交记录,新增申请资质认证记录,在worker_certification_audit表插入一行并且设置审核状态为0(未审核),并且查询worker_certification表(同一用户),设置认证状态为认证中,如果有记录则更新,没记录则新增。
之后机构端审核,设置worker_certification_audit表中审核状态为1、认证状态为通过或不通过,并将认证信息同步到worker_certification表。
3. 门户
总言:门户访问量大,提升速度是关键。
静态资源:图片、视频、CSS、Js等 静态资源放到 CDN上面 ,减少网络路由耗时。
动态资源:异步请求数据,数据做缓存(redis)、前端也可以做缓存
Nginx负载均衡:部署多个Nginx服务器共同提供服务。
3.1 缓存技术方案
redis客户端:Jedis、Lettuce,Spring Boot 默认使用的是 Lettuce 作为 Redis 的客户端。
两种访问redis的框架
SpringCache缓存框架、Spring Data redis(RedisTemplate),这两种都是通过Lettuce 去访问redis的。
Spring Cache:
@EnableCaching:开启缓存注解功能
@Cacheable:查询数据时缓存,将方法的返回值进行缓存。
@CacheEvict:用于删除缓存,将一条或多条数据从缓存中删除。
@CachePut:用于更新缓存,将方法的返回值放到缓存中
@Caching:组合多个缓存注解;
@CacheConfig:统一配置@Cacheable中的value值
Spring Cache这种缓存框架,注解的方式,都是基于AOP实现的,对添加注解的类生成代理对象,代理对象中做缓存相关的逻辑,比如说@Cacheable,在代理对象中先查缓存,有的化直接拿出来,没有的话才执行方法。
缓存穿透:
- 缓存空值本项目采用
- 布隆过滤器:存在误判率,且删除困难,因为有hash冲突的存在。
- redisson实现的布隆过滤器
- redis的bitmap位图结构实现。
- google的Guava库实现。
缓存击穿:
- 加锁
- 热点数据不过期+定时任务刷新缓存:本项目采用
- 缓存预热:提前预热、定时预热
缓存雪崩:
- 对同一类型信息的key设置不同的过期时间(+一个随机值)
- 缓存预热
缓存不一致问题:
其实就是改DB时,缓存中的数据也得同步,操作数据库和操作redis都是通过网络来交互的,就会存在数据不一致的问题。
- 分布式锁:顺序执行,先写库,再更新缓存。两个都成功还好,那要是数据库成功了redis失败了数据库就得回滚,要使用分布式事务组件
- 延迟双删:先删缓存再写入主库,这时候别的线程通过从库查询出来放入缓存,延时一段时间后线程一再删除缓存。会有短暂得数据不一致
- canal+MQ最终一致:canal监听binglog日志,将数据变化日志写入到mq,缓存更新服务再从mq拿到数据进行更新。
- 利用数据库层面得事务,双写表,将需要缓存得数据写入到任务表,定时任务再去扫描。
缓存方案:
开通区域列表缓存实现:当前哪些城市开通了服务。
- 查询缓存:查询已开通区域列表,如果没有缓存则查询数据库并将查询结果进行缓存,如果存在缓存则直接返回。缓存为永久
- 启用区域:删除启用区域信息缓存,并且:删除首页图标、删除 热门服务、服务类型;最后再刷新缓存:启用区域列表、首页服务列表、热门服务列表、服务类型列表,springcache的话就是查一次。
- 为什么还有删除其他三种缓存:区域变了--------> 首页服务列表是根据区域来选择的、热门服务列表也是、服务类型也是,这些都得变。禁用区域同理。
- 禁用区域:删除启用区域信息缓存;并且:删除该区域下的其它缓存信息,包括:首页服务列表,服务类型列表,热门服务列表。