1.开发更新订单状态的功能
1.1订单的状态码
我们电商上面订单的状态修改是非常普通的业务
随着商品的购买流程,订单的状态有
状态:
0=未支付
1=已关闭(超时未支付)
2=已取消
3=已支付
4=已签收
5=已拒收
6=退款处理中
7=已退款
1.2开发更新订单状态的持久层
修改订单状态就是根据订单id修改订单的state
我们随着业务的发展,订单可能需要更多修改的需求
订单的列(字段)比较多,如果每个字段修改,都需要编写一个方法的话,那么方法的数量会非常多
如果我们编写一个方法,能够接收订单对象的实体类参数(OmsOrder)
我们要实现可以根据OmsOrder对象的实际数据来实现动态的修改要修改的字段
Mybatis中可以通过编写动态修改sql语句完成这个需求
OmsOrderMapper接口添加方法
// 动态修改订单的sql,参数是omsOrder实体类对象// 对象中必须有id,id不可修改,其它属性不为空就修改其它属性值int updateOrderById(OmsOrder order);
OmsOrderMapper.xml编写sql
<!--动态修改订单的sql,参数是omsOrder实体类对象对象中必须有id,id不可修改,其它属性不为空就修改其它属性值--><!--omsOrder对象除id外的某个属性有值时,下面的动态sql语句,就有通过判断,来生成修改对应列的代码--><!--Mybatis框架动态sql标签<set>效果如下1.在<set>标签的位置生成一个set关键字2.在<set></set>标签的范围内,动态生成的sql语句,如果最后一个字符是","就删除它--><update id="updateOrderById">update oms_order<set><if test="contactName!=null">contact_name=#{contactName},</if><if test="mobilePhone!=null">mobile_phone=#{mobilePhone},</if><if test="telephone!=null">telephone=#{telephone},</if><if test="streetCode!=null">street_code=#{streetCode},</if><if test="streetName!=null">street_name=#{streetName},</if><if test="detailedAddress!=null">detailed_address=#{detailedAddress},</if><if test="tag!=null">tag=#{tag},</if><if test="paymentType!=null">payment_type=#{paymentType},</if><if test="state!=null">state=#{state},</if><if test="rewardPoint!=null">reward_point=#{rewardPoint},</if><if test="amountOfOriginalPrice!=null">amount_of_original_price=#{amountOfOriginalPrice},</if><if test="amountOfFreight!=null">amount_of_freight=#{amountOfFreight},</if><if test="amountOfDiscount!=null">amount_of_discount=#{amountOfDiscount},</if><if test="amountOfActualPay!=null">amount_of_actual_pay=#{amountOfActualPay},</if><if test="gmtPay!=null">gmt_pay=#{gmtPay},</if></set>whereid=#{id}</update>
1.3开发修改订单状态的业务逻辑层
OmsOrderServiceImpl
// 根据订单id 修改订单状态的业务逻辑层方法@Overridepublic void updateOrderState(OrderStateUpdateDTO orderStateUpdateDTO) {// 参数OrderStateUpdateDTO包含订单id和要修改的状态码// 将参数属性值赋值给OmsOrder类型对象,以便持久层调用OmsOrder order=new OmsOrder();BeanUtils.copyProperties(orderStateUpdateDTO,order);// 调用持久层方法omsOrderMapper.updateOrderById(order);}
启动Nacos\Seata
启动Order
测试时根据实际数据库订单id,修改knife4j的数据然后再运行
运行后查看数据库中订单状态列是否修改
2.搜索功能
2.1Elasticsearch加载数据
我们要想完成高效的搜索任务,需要ES的支持
因为数据库的模糊查询效率太低了
我们在前端页面中完成的搜索是从ES中搜索数据
这样就要求,我们在查询之前,需要先将商品信息(spu)保存到ES中
一开始我们使用最原始的办法:从数据库查询出数据之后新增到ES中
2.2确认实体类
搜索功能编写在mall-search模块中
它使用的实体类在cn.tedu.mall.pojo.search.eneity包下SpuForElastic
这个类有四个字段是具备分词功能的
所以支持我们使用这4个字段进行查询
/*** SPU名称*/@Field(name = "name",type = FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_max_word")@ApiModelProperty(value="SPU名称")private String name;//...../*** 标题*/@Field(name="title",type = FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_max_word")@ApiModelProperty(value="标题")private String title;/*** 简介*/@Field(name="description",type = FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_max_word")@ApiModelProperty(value="简介")private String description;//...../*** 类别名称(冗余)*/@Field(name="category_name",type = FieldType.Text,analyzer = "ik_max_word",searchAnalyzer = "ik_max_word")@ApiModelProperty(value="类别名称(冗余)")private String categoryName;//.....
2.3开发ES的持久层
我们仍然使用SpringDataElasticsearch框架来操作ES
按照SpringData的规范,我们创建包repository
在这个包中创建SpuForElasticRepository接口,代码如下
// SpuForElastic实体类操作ES的持久层接口// 一定要继承父接口,才能具备SpringData提供的基本增删改查功能@Repositorypublic interface SpuForElasticRepository extendsElasticsearchRepository<SpuForElastic,Long> {}
这个接口提供了批量新增到ES数据的方法
但是要想获得数据库中的所有pms_spu表的数据,必须连接数据库查询这些数据
但是search模块是负责管理ES的,所以需要Dubbo调用Product模块获取所有数据
2.4product模块提供的查询功能
我们需要使用Dubbo调用product的业务逻辑层获得数据库pms_spu表的数据
经过观察发现业务逻辑逻辑层调用ForFrontSpuServiceImpl类中
具有一个getSpuByPage的方法
他分页查询所有spu信息
@Overridepublic JsonPage<Spu> getSpuByPage(Integer pageNum, Integer pageSize) {PageHelper.startPage(pageNum,pageSize);List<Spu> list=spuMapper.findAllList();return JsonPage.restPage(new PageInfo<>(list));}
分页的原因是因为一般加载到ES中的数据量非常大(几十万上百万条),
我们不可能一次性将所有数据查询出来,增到ES中,必须分批分次
分页查询就是典型的分批查询,每次查询一部分数据,通过循环遍历,将每页数据都新增到ES中
2.5Search模块执行加载
mall-search-webapi模块创建service.impl包
包中创建SearchServiceImpl类,用于将数据库中的数据加载到ES中
代码如下
@Service@Slf4jpublic class SearchServiceImpl implements ISearchService {// dubbo调用product模块查询所有spu@DubboReferenceprivate IForFrontSpuService dubboSpuService;// 将查询出的spu对象新增到ES中@Autowiredprivate SpuForElasticRepository spuRepository;// 先循环调用dubbo分页查询数据库// 将分页查询出的spu对象新增到ES@Overridepublic void loadSpuByPage() {// 我们并不知道本次分页从查询的总页数// 所以是典型的先执行后判断,推荐使用do-whileint i=1;// 循环次数,从1开始,同时代表页码int pages=0; // 总页数,在第一次运行之后才能知道具体值,默认值赋0即可(不赋值也可以)do{// dubbo 分页查询页码指定的数据JsonPage<Spu> spus=dubboSpuService.getSpuByPage(i,2);// 需要将JsonPage类型中的数据转换为List<SpuForElastic>,以便新增到ESList<SpuForElastic> esSpus=new ArrayList<>();// 遍历spus,进行转换并新增到esSpusfor(Spu spu : spus.getList()){SpuForElastic esSpu=new SpuForElastic();BeanUtils.copyProperties(spu,esSpu);// 将赋值好的esSpu添加到esSpus中esSpus.add(esSpu);}// esSpus集合中已经添加了当前页数据,执行新增到ESspuRepository.saveAll(esSpus);log.info("成功加载第{}页数据",i);// 为下次循环准备i++;pages=spus.getTotalPage();}while(i<=pages);}@Overridepublic JsonPage<SpuForElastic> search(String keyword, Integer page, Integer pageSize) {return null;}}
创建测试类
@SpringBootTestpublic class LoadTest {@Autowiredprivate ISearchService searchService;@Testvoid loadData(){searchService.loadSpuByPage();System.out.println("ok");}}
运行测试前保证
Nacos\Seata\\ES启动
启动product模块
运行测试,没有报错即可
2.6验证ES中的数据
我们再通过连接ES来进行全查
检验上面执行的加载工作是否达到效果
仍然在测试类中,在编写一个方法,使用SpringData提供的全查方法查询后遍历输出
检查输出内容,代码如下
@Autowiredprivate SpuForElasticRepository repository;@Testvoid showData(){Iterable<SpuForElastic> spus=repository.findAll();spus.forEach(spu -> System.out.println(spu));}
2.7搜索功能的实现
电商网站一定会有按用户输入的关键字进行搜索的功能
这样的搜索都是搜索ES查询到的结果
上面章节中,我们已经将所有spu信息保存到了ES中
下面通过查询逻辑将搜索结果显示出来
2.7.1编写SpringData自定义查询
如果我们按照关键字"手机"进行搜索
可以在Repository接口中编写自定义方法
@Repositorypublic interface SpuForElasticRepository extendsElasticsearchRepository<SpuForElastic,Long> {// 查询title字段包含指定关键字的spu数据Iterable<SpuForElastic> querySpuForElasticsByTitleMatches(String title);}
上面的查询可以通过测试类测试
// 根据title查询数据@Testvoid getSpuByTitle(){Iterable<SpuForElastic> it=repository.querySpuForElasticsByTitleMatches("手机");it.forEach(e -> System.out.println(e));}
尤其需要关注ES是否已经启动
不需要其它项目的支持,直接运行测试即可
我们业务中需要4个字段的条件查询
SpringData也支持我们在代码中编写查询语句,一避免过长的方法名
@Query("{\n" +" \"bool\": {\n" +"\"should\": [\n" +" { \"match\": { \"name\": \"?0\"}},\n" +" { \"match\": { \"title\": \"?0\"}},\n" +" { \"match\": { \"description\": \"?0\"}},\n" +" { \"match\": { \"category_name\": \"?0\"}}\n" +" ]\n" +"}\n" +"}")// 上面指定了查询语句,这个方法的方法名就随意定义了Iterable<SpuForElastic> querySearch(String keyword);
测试代码
// 根据title查询数据@Testvoid getSpuByTitle(){// Iterable<SpuForElastic> it=//repository.querySpuForElasticsByTitleMatches("手机华为小米");Iterable<SpuForElastic> it=repository.querySearch("手机");it.forEach(e -> System.out.println(e));}
在实际开发中
我们数据库中的数据和Elasticsearch中的数据还存在同步问题
为了保持数据库中的数据和Elasticsearch中的数据一致
我们可以使用下面的办法
1.在所有对spu表进行增删改的操作代码运行后,也对ES中的数据进行相同的操作
但是会有比较多的代码要编写,而且有比较明显的事务处理问题
2.实际上业界使用Elasticsearch有一个组合叫ELK,其中L(logstash)可以实现自动同步数据库和ES的信息
后面学习过程中,我们会在Linux虚拟机的学习中使用它
实际运行查询的逻辑是需要分页的
所以要按照SpringData支持的分页查询格式修改上面的查询代码
@Query("{\n" +" \"bool\": {\n" +"\"should\": [\n" +" { \"match\": { \"name\": \"?0\"}},\n" +" { \"match\": { \"title\": \"?0\"}},\n" +" { \"match\": { \"description\": \"?0\"}},\n" +" { \"match\": { \"category_name\": \"?0\"}}\n" +" ]\n" +"}\n" +"}")// 上面指定了查询语句,这个方法的方法名就随意定义了//↓↓↓ ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓Page<SpuForElastic> querySearch(String keyword, Pageable pageable);
修改方法的定义,原有的调用会报错,注释掉调用即可!
2.7.2开发搜索功能的业务逻辑层
提供的项目搜索功能接口的返回值泛型为虚拟机版本的ES实体类
要先修改一下
ISearchService
public interface ISearchService {// ES分页查询spu的方法// ↓↓↓↓↓↓↓↓↓↓↓↓↓JsonPage<SpuForElastic> search(String keyword, Integer page, Integer pageSize)//......}
SearchServiceImpl类添加实现方法如下
@Overridepublic JsonPage<SpuForElastic> search(String keyword, Integer page, Integer pageSize) {// 根据参数中分页的数据,执行分页,注意SpringData分页参数设置0是第一页,所以要减1Page<SpuForElastic> spus=spuRepository.querySearch(keyword,PageRequest.of(page-1,pageSize));// 当前方法需要返回JsonPage类型,我们需要将spus对象进行转换// 我们使用就地转换的方式,也可以在JsonPage类中添加转换方法来调用JsonPage<SpuForElastic> jsonPage=new JsonPage<>();// 赋值相关分页信息jsonPage.setPage(page);jsonPage.setPageSize(pageSize);//总条数和总页数jsonPage.setTotal(spus.getTotalElements());jsonPage.setTotalPage(spus.getTotalPages());// 将查询到的数据赋值到jsonPagejsonPage.setList(spus.getContent());// 最后别忘了返回return jsonPage;}
2.7.3开发控制层代码
创建controller包
包中创建SearchController编写搜索方法,代码如下
@RestController@RequestMapping("/search")@Api(tags = "搜索模块")public class SearchController {@Autowiredprivate ISearchService searchService;// 搜索模块的功能非常直接,就是搜索// 所以它的url路径可以尽量短// @GetMapping后面不写任何内容,意思就是只适用类上定义的路径即可访问// 访问路径:localhost:10008/search@GetMapping@ApiOperation("根据用户输入的关键字分页查询spu")@ApiImplicitParams({@ApiImplicitParam(value = "搜索关键字",name="keyword", dataType="string"),@ApiImplicitParam(value = "页码",name="page", dataType="int"),@ApiImplicitParam(value = "每页条数",name="pageSize", dataType="int")})public JsonResult<JsonPage<SpuForElastic>> searchByKeyword(String keyword,Integer page, Integer pageSize){JsonPage<SpuForElastic> jsonPage=searchService.search(keyword,page,pageSize);return JsonResult.ok(jsonPage);}}
保证Nacos\seata\ES启动
因为当前search项目过滤器解析JWT所以需要登录才能访问
启动search模块
建议启动passport模块去进行登录获得jwt
复制JWT后,粘贴到10008模块的全局参数,启动search模块
再测试http://localhost:10008/doc.html
3.Quartz
3.1什么是Quartz
quartz:石英钟的意思
是一个当今市面上流行的高效的任务调度管理工具
所谓"调度"就是制定好的什么时间做什么事情的计划
由OpenSymphony开源组织开发
Symphony:交响乐
是java编写的,我们使用时需要导入依赖即可
3.2为什么需要Quartz
所谓"调度"就是制定好的什么时间做什么事情的计划
我们使用过的最简单的调度方法就是Timer
但是Timer的调度功能过于单一,只能是指定时间的延时调用和周期运行
而Quartz可以更详细的指定时间,进行计划调用
3.3Quartz核心组件
调度器:Scheduler
任务:job
触发器:Trigger
调度器来配置\计划什么时间触发什么任务
简单来说就是调度器规定什么时间做什么事情
job(工作\任务):Quartz 实现过程中是一个接口,接口中有一个方法execute(执行的意思)
我们创建一个类,实现这个接口,在方法中编写要进行的操作(执行具体任务)
我们还需要一个JobDetail的类型的对象,Quartz每次执行job时
会实例化job类型对象,去调用这个方法,JobDetail是用来描述Job实现类的静态信息,
比如任务运行时在Quartz中的名称
Trigger(触发器):能够描述触发指定job的规则,分为简单触发和复杂触发
简单触发可以使用SimplTrigger实现类.功能类似timer
复杂触发可以使用CronTrigger实现类,内部利用cron表达式描述各种复杂的时间调度计划
Scheduler(调度器):一个可以规定哪个触发器绑定哪个job的容器
在调度器中保存全部的Quartz保存的任务
SpringBoot框架下,添加Quartz依赖后,调度器由SpringBoot管理,我们不需要编写
3.4Cron表达式
表示9月3日凌晨4点的cron表达式
0 0 4 3 9?
* 表示任何值,如果在分的字段上编写*,表示每分钟都会触发
, 是个分割符如果秒字段我想20秒和40秒时触发两次就写 20,40
- 表示一个区间 秒字段5-10 表示 5,6,7,8,9,10
/ 表示递增触发 秒字段 5/10表示5秒开始每隔10秒触发一次
日字段编写1/3表示从每月1日起每隔3天触发一次
? 表示不确定值, 因为我们在定日期时,一般确定日期就不确定是周几,相反确定周几时就不确定日期
L 表示last最后的意思,我们可以设置当月的最后一天,就会在日字段用L表示,
周字段使用L表示最后一周,一般会和1-7的数字组合
例如6L表示本月最后一周的周五
W (work)表示最近的工作日(单纯的周一到周五) 如果日字段编写15W表示
每月15日最近的工作日触发,如果15日是周六就14日触发,如果15日是周日就16日触发
LW通常一起使用,表示本月的最后一个工作日
# 表示第几个,只能使用在周字段上 6#3表示每月的第三个周五
如果#后面数字写大了,是一个不存在的日期,那就不运行了
适合设计在母亲节或父亲节这样的日期运行
网络上可用的Cron表达式生成器很多
推荐一个Cron - 在线Cron表达式生成器
未来每年的母亲节(时间9点)
0 0 9?5 1#3
未来每个月离15日最近的工作日(时间9点)
0 0 9 15W * ?
双11的0时触发
0 0 0 11 11?
每月10日最近的工作日9点发工资
0 0 9 10W * ?
3.5SpringBoot使用Quartz
SpringBoot框架下使用Quartz格式还是非常固定的
我们选用之前学习微服务的项目csmall减少对大项目的影响
首先添加依赖
我们选项csmall-stock-webapi模块pom文件
<!-- SpringBoot整合Quartz依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId></dependency>
先编写要执行的任务
当前项目模块中创建quartz包
包中创建一个QuartzJob的类,实现Job接口
代码如下
public class QuartzJob implements Job {@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {// 实现一个简单的任务做演示// 例如输出当前时间System.out.println("-------------------"+ LocalDateTime.now() +"--------------------");}}
上面编写的是Job接口的实现类,要想运行还需要将它封装为JobDetail对象保存在Spring容器中
还有要创建一个Trigger设置要运行的时机,也保存到Spring容器中
在quartz包下,再创建一个QuartzConfig类,其中编写它们的调度绑定关系
这个格式是固定的,后面再需要绑定,直接套用即可
// 要配置Quartz的调度器Scheduler// 调度器由SpringBoot管理,所以就变成了配置Spring@Configurationpublic class QuartzConfig {// 配置的核心是向Spring容器保存一个job和保存一个Trigger// 创建一个封装Job对象的类型JobDetail// 使用@Bean注解标记的方法将这个对象保存到Spring容器@Beanpublic JobDetail showTime(){System.out.println("jobDetail保存到Spring容器");//newJob方法就是在绑定要运行的Job接口实现类,需要实现类的反射做参数return JobBuilder.newJob(QuartzJob.class)// 给当前JobDetail对象在调度环境中起名.withIdentity("dateTime")// 即使没有触发器绑定当前JobDetail对象,也不会被删除.storeDurably().build();}// 下面是触发器的声明,也会保存到Spring容器中// 它能够设置job的运行时机@Beanpublic Trigger showTimeTrigger(){System.out.println("Trigger保存到Spring容器中");// 定义Cron表达式CronScheduleBuilder cron=CronScheduleBuilder.cronSchedule("0 47 16 2 9 ?");return TriggerBuilder.newTrigger()// 绑定要运行的JobDetail对象.forJob(showTime())// 为触发器起名.withIdentity("dateTrigger")// 绑定cron表达式.withSchedule(cron).build();}}
Nacos\Seata需要启动
其他服务和软件都可以关闭
然后启动csmall-stock-webapi模块
观察控制台输出
3.6课上作业
在csmall-stock项目中写
利用Quart实现
每隔2分钟运行一次添加库存的操作,PC100号商品的库存添加10
public class QuartzAddStock implements Job {@Autowiredprivate IStockService stockService;@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {StockReduceCountDTO countDTO=new StockReduceCountDTO();countDTO.setCommodityCode("PC100");countDTO.setReduceCount(-10);stockService.reduceCommodityCount(countDTO);System.out.println("定时增加库存完成!");}}
@Beanpublic JobDetail addStock(){return JobBuilder.newJob(QuartzAddStock.class).withIdentity("addStock").storeDurably().build();}@Beanpublic Trigger addStockTrigger(){CronScheduleBuilder cron=CronScheduleBuilder.cronSchedule("0 0/2 * * * ?");return TriggerBuilder.newTrigger().forJob(addStock()).withIdentity("addStockTrigger").withSchedule(cron).build();}
4.Redis 强化
4.1缓存使用原则
什么时候,什么样的数据能够保存在Redis中?
1.数据量不能太大
2.使用越频繁,Redis保存这个数据越值得
3.保存在Redis中的数据一般不会是数据库中频繁修改的
4.2缓存淘汰策略
Redis将数据保存在内存中,内存的容量是有限的
如果Redis服务器的内存已经全满,现在还需要向Redis中保存新的数据,如何操作,就是缓存淘汰策略
noeviction:返回错误**(默认)**
如果我们不想让它发生错误,就可以设置它将满足某些条件的信息删除后,再将新的信息保存
allkeys-random:所有数据中随机删除数据volatile-random:有过期时间的数据中随机删除数据volatile-ttl:删除剩余有效时间最少的数据allkeys-lru:所有数据中删除上次使用时间距离现在最久的数据volatile-lru:有过期时间的数据中删除上次使用时间距离现在最久的数据allkeys-lfu:所有数据中删除使用频率最少的volatile-lfu:有过期时间的数据中删除使用频率最少的
如果觉得《JSD-2204-(业务逻辑开发)-更新订单状态的功能-搜索功能-Quartz-Day12》对你有帮助,请点赞、收藏,并留下你的观点哦!