云岚到家抢单

目录

核心业务流程

服务端抢单原型

机构端抢单原型

小结

系统整体设计

整体分析

抢单查询(对应到抢券查询)

抢单(技术方案同领取优惠券)

抢单结果异步处理(技术方案同抢券结果异步处理)

抢单设置

订单分流

表设计

​编辑 订单分流代码

抢单查询

抢单池同步

同步方案

索引结构设计

抢单池库存结构

小结

抢单查询 

核心业务流程

首先明确本模块在核心业务流程的位置,下图是项目的核心业务流程:

用户下单后服务人员通过app进行抢单,机构通过pc进行抢单,抢单成功后服务人员开始现场服务。

服务端抢单原型

服务人员在平台注册后需要进行实名认证、服务技能设置、服务范围设置、开启接单,这四项准备工作完成后方可在平台接单。

服务技能设置

服务技能即服务人员服务范围,比如:服务人员A提供日常保洁、日常维修服务。设置服务技能后服务人员可以抢与自己的技能匹配的订单,比如:服务人员A提供日常保洁、日常维修服务,所以它只能抢日常保洁、日常维修服务的订单。

服务范围设置

设置服务范围后能抢服务范围内的订单,下图中的服务城市是北京,接单范围是朱辛庄地铁站,在抢单查询时以接单范围为中心点查询方圆几公里的用户创建的订单,具体方圆公里数在查询条件中选择。

 

接单设置

开启接单后方可抢单。

开始抢单

进入抢单页面,选择服务距离和服务类型查询订单,点击“立即抢单”进行抢单。

服务距离:距离服务人员接单范围的距离。

服务类型:

抢单成功查询订单

服务人员抢单成功在我的订单中查询

机构端抢单原型

机构端抢单与服务端抢单的区别是:

1、机构使用pc端,服务端使用app

2、机构端抢单成功后订单状态为待分配,表示待将订单分配给机构下的服务人员;服务端抢单成功后订单状态为待服务,因为服务人员抢到的订单服务人员就是自己。

3、抢单数量限制不同

抢单数量限制是当前拥有的进行中的服务单的最大数量。服务人员默认是10,机构默认是100。

抢单数量限制的目的是规避恶意抢单。

因为机构下的服务人员较多所以机构的签单数量限制大于服务人员。

下边说明机构端抢单的界面原型:

机构端也需要进行实名认证、服务技能与服务范围设置、开启接单后方可在平台接单。

服务单初始状态:待分配或待服务

机构抢单成功:待分配。

服务人员抢单成功:待服务。

开始服务: 待服务---》服务中

服务完成:

服务中---》服务完成

用户取消订单:

待分配---》已取消

待服务---》已取消

运营人员取消订单:

服务中---》已取消

服务完成---》已取消

小结

通过需求分析,抢单模块分如下子模块:

  1. 抢单设置子模块

设置服务技能、服务范围、开启抢单。

  1. 抢单查询子模块

查询抢单池中的订单

  1. 抢单子模块

点击“立即抢单”开始抢单。

抢单成功后生成服务单,服务单的状态包括:待分配、待服务、服务中、服务完成、已取消。

系统整体设计

整体分析

根据抢单需求,抢单的流程和抢券的流程很相似,核心内容也包括三部分:

抢单查询(对应到抢券查询)

在抢单界面查询抢单池中的订单,订单属于抢购的资源,在抢券中优惠券也属于争抢的资源,此处的查询存在高并发需要使用缓存技术进行优化。

查询抢单池中的订单可以根据下单用户的地理位置进行搜索订单,此功能类似“搜索附近”的业务,比如:搜索附近的酒店、搜索附近的房源信息等。(因为抢单本就是高并发,所以不能让其访问数据库,但是设计多条件查询,并且涉及地理位置搜索,所以es是最佳选择,虽然redis也有地理坐标查询,但是他不能多条件进行搜索,搜索出来还要手工进行筛选实现起来比较负责,所以我们选择了es)

结合上边的要求,本项目使用Elasticsearch查询抢单资源,使用ES的基于地理坐标搜索功能完成“搜索附近”功能的开发,Elasticsearch通过全文检索技术提高查询性能。 全文检索:提供模糊搜索等自动度很高的查询方式,并进行相关性排名,高亮等功能

如果要使用Elasticsearch查询抢单池信息就需要将待派单的订单同步到Elasticsearch,这里我们使用Canal+MQ的方式实现数据同步,如下图:

订单支付成功后进行订单分流(当前时刻举例服务时刻小于两个小时,把订单放入派单池(派单表),如果大于两小时放入抢单池(抢单表),因为使用了canal,记录了binlog日志,一旦 canal检测到表出现了变化,就会发送消息到mq,然后消费者消费到消息,把数据添加到es中,用于让用户搜索到,并加入到redis库存中,用于redis的抢单。抢单也是基于redis的lua脚本进行抢单,把抢单的信息记录进同步队列,然后用xxl-job启用多线程进行同步消息到数据库,然后把redis中的库存信息和es中该订单删除)

下单支付成功通过订单分流程序将订单信息写入抢单池,满足系统自动派单需求的订单同时写入派单池,派单部分在后边章节讲解。

通过Canal将抢单池的信息同步到Elasticsearch。

抢单查询从Elasticsearch中查询订单信息,使用ES的基于地理坐标搜索功能完成搜索附近的订单。

抢单(技术方案同领取优惠券)

我们在分析抢单技术方案时结合 抢券 的技术方案进行分析,多个人争抢同一个订单,这里仍然存在超卖问题,我们使用同抢券一样的技术方案去解决超卖问题。

下图中加入了抢单的交互流程:

订单支付成功后进行订单分流(当前时刻举例服务时刻小于两个小时,把订单放入派单池(派单表),如果大于两小时放入抢单池(抢单表),因为使用了canal,记录了binlog日志,一旦 canal检测到表出现了变化,就会发送消息到mq,然后消费者消费到消息,把数据添加到es中,用于让用户搜索到,并加入到redis库存中,用于redis的抢单。抢单也是基于redis的lua脚本进行抢单,把抢单的信息记录进同步队列,然后用xxl-job启用多线程进行同步消息到数据库,然后把redis中的库存信息和es中该订单删除,和同步队列中的信息删除)(这样也是为了减少tps的事务线,通过异步方案进行流量削峰)

说明:

参考抢券的方案,使用Redis+Lua的技术解决超卖问题,这里将抢单库存同步到Redis,每个订单的库存就是1。

抢单执行Lua脚本完成抢单,具体包括:扣减库存、抢单成功写入同步队列。

抢单同步队列的作用是通过异步任务将抢单结果信息同步到数据库。

抢单结果异步处理(技术方案同抢券结果异步处理)

 (这样也是为了减少tps的事务线,通过异步方案进行流量削峰)

秒杀抢购业务的并发高,为了避免直接操作数据库这里使用异步任务的方式将抢单结果同步到数据库。

抢单成功创建服务单,异步任务的主要职责是根据抢单结果创建服务单,并且更新订单的状态。

抢单结果同步异步任务的具体的内容如下:

创建服务单

服务单记录了服务人员进行家政服务的信息,关键字段有:订单ID、订单金额、服务人员ID、服务单状态、服务时间、服务照片等。

服务单初始状态:待分配或待服务

机构抢单成功:待分配。

服务人员抢单成功:待服务。

服务单的详细状态如下图:

更新订单的状态:

用户下单并支付完成后订单的状态为“派单中”,服务人员抢单成功订单状态为“待服务”,机构抢单成功订单状态为“待分配”。

订单状态图如下,详细说明请参见第四章内容。

抢单结果同步成功删除抢单池等相关信息:

抢单结果同步成功后删除抢单池中该订单的信息,这样服务人员在抢单界面无法查询到该订单。

删除数据库中抢单池的记录,将Elasticsearch中对应的抢单记录删除。

删除Redis中该订单的库存信息。

删除Redis中抢单同步队列中的记录。

抢单设置

根据需求,机构和服务人员都需要作抢单前的准备工作方可抢单。

准备工作包括:实名认证、服务技能设置、服务范围设置、开启接单。关于实名认证的业务流程请参考第二章,下边通过阅读代码理解服务技能设置、服务范围设置、开启接单的设置。

下边以服务端为例说明。

服务技能设置

服务人员新注册账号后进入下边的界面:

通过此界面进行服务技能设置、服务范围设置、接单设置。

点击上图中的“去设置”进入服务技能设置界面:

点击编辑开始设置服务技能: 

选择服务技能,点击保存。

服务范围设置

进入服务范围设置界面:

 

 服务技能表:存储服务提供者的技能信息。

create table `jzo2o-customer`.serve_skill
(
    id                  bigint                             not null comment '主键'
        constraint `PRIMARY`
        primary key,
    serve_provider_id   bigint                             null comment '服务人员/机构id',
    serve_provider_type int                                null comment '类型,2:服务人员,3:服务机构',
    serve_type_id       bigint                             null comment '服务类型id',
    serve_type_name     varchar(50)                        null comment '服务类型名称',
    serve_item_id       bigint                             null comment '服务项id',
    serve_item_name     varchar(50)                        null comment '服务项名称',
    create_time         datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time         datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    is_delete           int      default 0                 not null comment '是否已删除,0:未删除,1:已删除'
)
    comment '服务技能表' charset = utf8mb4;

服务提供者设置表:存储服务提供者的服务范围等信息。

create table `jzo2o-customer`.serve_provider_settings
(
    id              bigint                                not null comment '服务人员/机构id'
        constraint `PRIMARY`
        primary key,
    city_code       varchar(20) default ''                null comment '城市码',
    city_name       varchar(64)                           null comment '城市名称',
    lon             double(10, 5)                         null comment '经度',
    lat             double(10, 5)                         null comment '纬度',
    intention_scope varchar(100)                          null comment '意向单范围',
    have_skill      int         default 0                 null comment '是否有技能',
    can_pick_up     int         default -1                null comment '是否可以接单,-0:关闭接单,1:开启接单',
    create_time     datetime    default CURRENT_TIMESTAMP null,
    update_time     datetime    default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
    is_deleted      int         default 0                 null
)
    comment '服务人员/机构附属信息' charset = utf8mb4;

服务人员与机构表:存储服务人员与机构的注册信息。

当服务技能、服务范围、接单设置全部设置完成会将settings_status 字段更新为1

create table `jzo2o-customer`.serve_provider
(
    id                  bigint                             not null comment '主键'
        constraint `PRIMARY`
        primary key,
    code                varchar(255)                       null comment '编号',
    type                int                                not null comment '类型,2:服务人员,3:服务机构',
    name                varchar(255)                       null comment '姓名',
    phone               varchar(255)                       not null comment '电话',
    avatar              varchar(255)                       null comment '头像',
    status              int                                not null comment '状态,0:正常,1:冻结',
    settings_status     int      default 0                 null comment '首次设置状态,0:未完成设置,1:已完成设置',
    password            varchar(255)                       null comment '机构登录密码',
    account_lock_reason varchar(255)                       null comment '账号冻结原因',
    score               double                             null comment '综合评分',
    good_level_rate     varchar(50)                        null comment '好评率',
    create_time         datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time         datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    is_deleted          int      default 0                 not null comment '是否已删除,0:未删除,1:已删除',
    constraint serve_provider_phone_type_uindex
        unique (phone, type)
)
    comment '服务人员/机构表' charset = utf8mb4;

订单分流

表设计

根据数据流分析需要设计抢单池表(表里的字段是基于业务,业务需要什么字段,表里面就填什么字段)

-- 抢单池(资源池) 
-- auto-generated definition
create table orders_seize
(
    id               bigint                                not null comment '订单id'
        primary key,
    orders_code      varchar(50)                           null comment '订单编号',
    city_code        varchar(50) default ''                not null comment '城市编码',
    serve_type_id    bigint                                null comment '服务分类id',
    serve_item_name  varchar(50)                           not null comment '服务名称',
    serve_type_name  varchar(50)                           not null comment '服务分类名称',
    serve_item_id    bigint                                null comment '服务项id',
    serve_address    varchar(50)                           not null comment '服务地址',
    serve_item_img   varchar(255)                          not null comment '服务项目图片',
    orders_amount    decimal(10, 2)                        null comment '订单总金额',
    serve_start_time datetime                              not null comment '服务开始时间',
    pay_success_time datetime                              null comment '订单支付成功时间,用于计算是否进入派单',
    lon              double(10, 5)                         null comment '经度',
    lat              double(10, 5)                         null comment '纬度',
    pur_num          int                                   not null comment '服务数量',
    is_time_out      int         default 0                 null comment '抢单是否超时',
    sort_by          bigint                                null comment '抢单列表排序字段',
    create_time      datetime    default CURRENT_TIMESTAMP null comment '创建时间',
    update_time      datetime    default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间'
)
    comment '抢单池(资源池)' charset = utf8mb4;

create index sort_by_index
    on orders_seize (sort_by);

经纬度的设置 

 

 这里前段穿来得及经纬度是个字符串,我们把他分开即可

 订单分流代码

首先当用户支付成功后进行订单分流

 这里就是组装数据,然后插入到抢单池中,如果服务预约时间小于指定时间,则插入派单池让系统自动派单。

package com.jzo2o.orders.base.service.impl;


@Service
@Slf4j
public class OrdersDiversionCommonServiceImpl implements IOrdersDiversionCommonService {


    @Resource
    private RedisTemplate<String, Long> redisTemplate;

    @Resource
    private RegionApi regionApi;

    @Resource
    private ServeApi serveApi;

    @Resource
    private OrdersDiversionCommonServiceImpl owner;

    @Resource
    private OrdersSeizeMapper ordersSeizeMapper;
    @Resource
    private OrdersDispatchMapper ordersDispatchMapper;

    @Override
    public void diversion(Orders orders) {
        log.debug("订单分流,id:{}",orders.getId());
        // 1.当前时间已超过服务预约时间则不再分流
        if (orders.getServeStartTime().compareTo(DateUtils.now()) < 0) {
            log.debug("订单{}当前时间已超过服务预约时间则不再分流",orders.getId());
            return;
        }
        ConfigRegionInnerResDTO configRegion = regionApi.findConfigRegionByCityCode(orders.getCityCode());
        ServeAggregationResDTO serveAggregationResDTO = serveApi.findById(orders.getServeId());
        //订单分流数据存储
        owner.diversionCommit(orders,configRegion,serveAggregationResDTO);
    }

    @Transactional(rollbackFor = Exception.class)
    public void diversionCommit(Orders orders, ConfigRegionInnerResDTO configRegion, ServeAggregationResDTO serveAggregationResDTO) {
        //流间隔(单位分钟),即当前时间与服务预计开始时间的间隔
        Integer diversionInterval = configRegion.getDiversionInterval();

        //当前时间与服务预约时间的间隔
        Duration between = DateUtils.between(DateUtils.now(), orders.getServeStartTime());
        //服务类型名称
        String serveTypeName = ObjectUtils.get(serveAggregationResDTO, ServeAggregationResDTO::getServeTypeName);
        //服务类型id
        Long serveTypeId = ObjectUtils.get(serveAggregationResDTO, ServeAggregationResDTO::getServeTypeId);
        //服务项名称
        String serveItemName = ObjectUtils.get(serveAggregationResDTO, ServeAggregationResDTO::getServeItemName);
        //服务项图片
        String serveItemImg = ObjectUtils.get(serveAggregationResDTO, ServeAggregationResDTO::getServeItemImg);
        //用于排序,服务预约时间戳加订单号后5位
        long sortBy = DateUtils.toEpochMilli(orders.getServeStartTime()) + orders.getId() % 100000;
        OrdersSeize ordersSeize = OrdersSeize.builder()
                .id(orders.getId())
                .ordersAmount(orders.getRealPayAmount())
                .cityCode(orders.getCityCode())
                .serveTypeId(serveTypeId)
                .serveTypeName(serveTypeName)
                .serveItemId(orders.getServeItemId())
                .serveItemName(serveItemName)
                .serveItemImg(serveItemImg)
                .ordersAmount(orders.getRealPayAmount())
                .serveStartTime(orders.getServeStartTime())
                .serveAddress(orders.getServeAddress())
                .lon(orders.getLon())
                .lat(orders.getLat())
                .paySuccessTime(DateUtils.now())
                .paySuccessTime(orders.getPayTime())
                .sortBy(sortBy)
                .isTimeOut(BooleanUtils.toInt(between.toMinutes() < diversionInterval))
                .purNum(orders.getPurNum()).build();
        ordersSeizeMapper.insert(ordersSeize);
        //当前时间与服务预约时间的间隔 小于指定间隔则插入派单表
        if (between.toMinutes() < diversionInterval) {
            OrdersDispatch ordersDispatch = OrdersDispatch.builder()
                    .id(orders.getId())
                    .ordersAmount(orders.getRealPayAmount())
                    .cityCode(orders.getCityCode())
                    .serveTypeId(serveTypeId)
                    .serveTypeName(serveTypeName)
                    .serveItemId(orders.getServeItemId())
                    .serveItemName(serveItemName)
                    .serveItemImg(serveItemImg)
                    .ordersAmount(orders.getRealPayAmount())
                    .serveStartTime(orders.getServeStartTime())
                    .serveAddress(orders.getServeAddress())
                    .lon(orders.getLon())
                    .lat(orders.getLat())
                    .purNum(orders.getPurNum()).build();
            ordersDispatchMapper.insert(ordersDispatch);
        }
    }


}

抢单查询

抢单池同步

同步方案

根据系统设计方案,抢单池的信息需要同步到Elasticsearch,使用Canal加RabbitMQ完成同步,如下:

索引结构设计

在ES中创建索引结构orders_seize,抢单池的信息将同步到orders_seize中。

启动ES和kibana

docker start elasticsearch7.17.7 docker start kibana7.17.7 

通过下边的命令创建orders_seize索引结构:

PUT /orders_seize
{
  "mappings" : {
      "properties" : {
        "city_code" : {
          "type" : "keyword"
        },
        "id" : {
          "type" : "long"
        },
        "key_words" : {
          "type" : "text",
          "analyzer" : "ik_max_word",
          "search_analyzer" : "ik_smart"
        },
        "location" : {
          "type" : "geo_point"
        },
        "orders_amount" : {
          "type" : "float"
        },
        "pur_num" : {
          "type" : "integer"
        },
        "serve_address" : {
          "type" : "text",
          "index" : false
        },
        "serve_item_id" : {
          "type" : "long"
        },
        "serve_item_img" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "serve_item_name" : {
          "type" : "text",
          "index" : false
        },
        "serve_start_time" : {
          "type" : "text",
          "index" : false
        },
        "serve_time" : {
          "type" : "integer"
        },
        "serve_type_id" : {
          "type" : "long"
        },
        "serve_type_name" : {
          "type" : "text",
          "index" : false
        },
        "total_amount" : {
          "type" : "double"
        }
      }
    }
}

 查看微服务es的文档

 创建成功进行查询 索引列表,命令如下,找到orders_seize。

GET /_cat/indices?v

查询orders_seize的索引结构,命令如下,对照是否和上边创建的orders_seize的索引一致。 

GET orders_seize 
或 
GET /orders_seize/_mapping
抢单池库存结构

抢单池库存信息存储在Redis,在抢单时通过Redis扣减库存。

设计如下:

缓存结构:Hash

RedisKey:ORDERS:RESOURCE:STOCK:{citycode%10}

HashKey:订单id

HashValue: 库存,为1

过期时间:永不过期

缓存一致性方案:通过Canal进行同步

{} 是为了设置路由到哪个节点,只要{}里面内容一致就一定会路由到同一个节点

concurrency是只有一个消费者前来消费消息

@Component
@Slf4j
public class OrdersSeizeSyncHandler extends AbstractCanalRabbitMqMsgListener<OrdersSeize> {

    @Resource
    private ElasticSearchTemplate elasticSearchTemplate;

    @Resource
    private RedisTemplate redisTemplate;


    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "canal-mq-jzo2o-orders-seize"),
            exchange = @Exchange(name = "exchange.canal-jzo2o", type = ExchangeTypes.TOPIC),
            key = "canal-mq-jzo2o-orders-seize"),
            concurrency = "1"
    )
    public void onMessage(Message message) throws Exception {
        parseMsg(message);
    }

    @Override
    public void batchSave(List<OrdersSeize> ordersSeizes) {
        // 1.es中添加抢单信息
        List<OrdersSeizeInfo> ordersSeizeInfos = ordersSeizes.stream().map(ordersSeize -> {
            OrdersSeizeInfo ordersSeizeInfo = BeanUtils.toBean(ordersSeize, OrdersSeizeInfo.class);
            //得到服务开始时间(yyMMddHH)
            String serveTimeString = DateTimeFormatter.ofPattern("yyMMddHH").format(ordersSeize.getServeStartTime());
            ordersSeizeInfo.setServeTime(Integer.parseInt(serveTimeString));
            ordersSeizeInfo.setLocation(new Location(ordersSeize.getLon(), ordersSeize.getLat()));
            ordersSeizeInfo.setKeyWords(ordersSeize.getServeTypeName() + ordersSeize.getServeItemName() + ordersSeize.getServeAddress());
            return ordersSeizeInfo;
        }).collect(Collectors.toList());

        Boolean result = elasticSearchTemplate.opsForDoc().batchInsert(ORDERS_SEIZE, ordersSeizeInfos);
        if (!result){
            throw new RuntimeException("同步抢单池加入es失败");
        }
        // 2.写入库存
        ordersSeizeInfos.stream().forEach(ordersSeizeInfo -> {
            String redisKey = String.format(RedisConstants.RedisKey.ORDERS_RESOURCE_STOCK, RedisUtils.getCityIndex(ordersSeizeInfo.getCityCode()));
            // 库存默认1
            redisTemplate.opsForHash().putIfAbsent(redisKey, ordersSeizeInfo.getId(), 1);
        });
    }

    @Override
    public void batchDelete(List<Long> ids) {
        log.info("抢单删除开始,删除数量:{},开始id:{},结束id:{}", CollUtils.size(ids), CollUtils.getFirst(ids), CollUtils.getLast(ids));
        Boolean result = elasticSearchTemplate.opsForDoc().batchDelete(ORDERS_SEIZE, ids);
        if (!result){
            throw new RuntimeException("同步抢单池加入es失败");
        }
        log.info("抢单删除结束,删除数量:{},开始id:{},结束id:{}", CollUtils.size(ids), CollUtils.getFirst(ids), CollUtils.getLast(ids));

    }
}

 首先配置Canal

在Canal中配置order_seize为同步表。

这里配置了之间听某个队列中的某张表 

上边的配置中为什么只配置了jzo2o-orders-0下的order_seize为同步表?jzo2o-orders-1和jzo2o-orders-2需要配置吗?

order_seize为广播表,jzo2o-orders-0、jzo2o-orders-1、jzo2o-orders-2三个数据库中的order_seize表数据是一致的,所以只配置其中一个数据库的order_seize为同步表即可。

 mq中收到数据:

 通过 "type": "INSERT" 及data可以获取到新增的数据

 

测试 

登录kibana,执行:GET /orders_seize/_search,查询抢单数据是否同步到ES。 

#! Elasticsearch built-in security features are not enabled. Without authentication, your cluster could be accessible to anyone. See https://www.elastic.co/guide/en/elasticsearch/reference/7.17/security-minimal-setup.html to enable security.
{
  "took" : 261,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "orders_seize",
        "_type" : "_doc",
        "_id" : "2311150000000000038",
        "_score" : 1.0,
        "_source" : {
          "id" : 2311150000000000038,
          "city_code" : "010",
          "serve_type_id" : 1678649931106705409,
          "serve_item_id" : 1685894105234755585,
          "serve_type_name" : "保洁清",
          "serve_item_name" : "日常保洁",
          "serve_address" : "北京市北京市昌平区北京市昌平区回龙观街道弘文恒瑞文化传播公司正泽商务中心",
          "serve_time" : 23111520,
          "location" : {
            "lon" : 116.34351,
            "lat" : 40.06024
          },
          "serve_start_time" : "2023-11-15 20:30:00",
          "pur_num" : 1,
          "key_words" : "保洁清日常保洁北京市北京市昌平区北京市昌平区回龙观街道弘文恒瑞文化传播公司正泽商务中心",
          "orders_amount" : 1.0,
          "serve_item_img" : "https://yjy-xzbjzfw-oss.oss-cn-hangzhou.aliyuncs.com/aa6489e5-cd92-42f0-837a-952c99653b8b.png"
        }
      }
    ]
  }
}

小结

抢单池是怎么设计的?

  1. 在数据库中创建抢单池表,存储待抢单的订单信息。

  2. 用户下单并支付成功将订单信息写入订单表和抢单池表。

  3. 通过Canal+MQ将抢单池的信息同步到Elasticsearch和Redis中。

同步到Elasticsearch是为了通过ES的地理坐标搜索功能查询订单信息。

同步到Redis是为了将抢单池库存信息同步到Redis,服务人员抢单时请求Redis完成。

  1. 服务人员抢单完成扣减Redis中抢单池的库存,并记录抢单结果。

  2. 最后通过异步任务将抢单结果同步到数据库。

抢单查询 

定义controller

@RestController
@Api(tags = "服务端 - 抢单相关接口")
@RequestMapping("/worker")
@Slf4j
public class WorkerOrdersSeizeController {

@GetMapping("")
@ApiOperation("服务端抢单列表")
public OrdersSeizeListResDTO queryForList(OrdersSerizeListReqDTO ordersSerizeListReqDTO) {
    return null;
}
...

首先在Kibana中测试查询抢单语句

下边表示根据服务人员的服务范围、服务技能搜索抢单池:

GET /orders_seize/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "city_code": {
              "value": "010"
            }
          }
        },
        {
          "terms": {
            "serve_item_id": [
              1685894105234755585,
              1683432288440897537,
              1678727478181957634,
              1692475107114487809
            ]
          }
        },
        {
          "geo_distance": {
            "location": {
              "lat": 40.008,
              "lon": 116.4343
            },
            "distance": "3.0km"
          }
        }
      ]
    }
  },
    "sort": [
    {
      "_geo_distance": {
        "location": [
          {
            "lat": 40.008,
            "lon": 116.4343
          }
        ],
        "distance_type": "arc",// 距离计算方式
        "order": "asc", // "asc" 表示升序,"desc" 表示降序
        "unit": "km" 单位,可以是 "km"、"mi"(英里)等
      }
    }
  ]
}

服务人员可以选择服务类型和距离进行筛选

根据上边编写语句通过Java代码查询ES:

编写service方法如下:

首先进行校验

@Service
@Slf4j
public class OrdersSeizeServiceImpl extends ServiceImpl<OrdersSeizeMapper, OrdersSeize> implements IOrdersSeizeService {

@Override
public OrdersSeizeListResDTO queryForList(OrdersSerizeListReqDTO ordersSerizeListReqDTO) {

    // 1.校验是否可以查询(认证通过,开启抢单)
    ServeProviderResDTO detail = serveProviderApi.getDetail(UserContext.currentUserId());
    // 验证设置状态
    if (detail.getSettingsStatus() != 1 || !detail.getCanPickUp()) {
        return OrdersSeizeListResDTO.empty();
    }
    // 2.查询准备 (距离、技能,时间冲突)
    // 距离
    Double serveDistance = ordersSerizeListReqDTO.getServeDistance();
    if(ObjectUtils.isNull(ordersSerizeListReqDTO.getServeDistance())) {
        // 区域默认配置配置
        ConfigRegionInnerResDTO configRegionInnerResDTO = regionApi.findConfigRegionByCityCode(detail.getCityCode());
        serveDistance = (detail.getType() == UserType.INSTITUTION)
                ? configRegionInnerResDTO.getInstitutionServeRadius().doubleValue() : configRegionInnerResDTO.getStaffServeRadius().doubleValue();
    }
    // 技能
    List<Long> serveItemIds = serveSkillApi.queryServeSkillListByServeProvider(UserContext.currentUserId(), UserContext.currentUser().getUserType(), detail.getCityCode());
    if(CollUtils.isEmpty(serveItemIds)) {
        log.info("当前机构或服务人员没有对应技能");
        return OrdersSeizeListResDTO.empty();
    }


    // 3.查询符合条件的抢单列表id
    List<OrdersSeizeListResDTO.OrdersSeize> ordersSeizes = getOrdersSeizeId(
            serveItemIds, detail.getLon(), detail.getLat(), serveDistance, detail.getCityCode(), ordersSerizeListReqDTO);

    return new OrdersSeizeListResDTO(CollUtils.defaultIfEmpty(ordersSeizes, new ArrayList<>()));
}

/**
     * 获取抢单id,抢单类型,抢单预约服务时间
     * @param serveItemIds 服务项id
     * @param lon 当前服务人员或机构所在位置经度
     * @param lat 当前服务人员或机构所在纬度
     * @param distanceLimit 抢单距离限制
     * @param cityCode 城市编码
     * @param ordersSerizeListReqDTO 抢单查询参数
     * @return
     */
    private List<OrdersSeizeListResDTO.OrdersSeize> getOrdersSeizeId(List<Long> serveItemIds, Double lon, Double lat, double distanceLimit, String cityCode, OrdersSerizeListReqDTO ordersSerizeListReqDTO) {

        // 服务项查询条件
        List<FieldValue> serveItemIdFieldValue = serveItemIds.stream().map(serveItemId -> FieldValue.of(serveItemId)).collect(Collectors.toList());

        SearchRequest.Builder builder = new SearchRequest.Builder();
        builder.query(query ->
                query.bool(bool -> {
                    // 所在城市
                    bool.must(must -> must.term(term -> term.field(CITY_CODE).value(cityCode)));
                    // 服务类型
                    if(ordersSerizeListReqDTO.getServeTypeId() != null) {
                        bool.must(must -> must.term(term -> term.field(SERVE_TYPE_ID).value(ordersSerizeListReqDTO.getServeTypeId())));
                    }
                    // 服务项
                    bool.must(must -> must.terms(terms -> terms.field(SERVE_ITEM_ID).terms(t -> t.value(serveItemIdFieldValue))));
                    // 距离条件
                    bool.must(must -> {
                        must.geoDistance(geoDistance -> {
                            geoDistance.field(LOCATION);
                            geoDistance.location(location -> location.latlon(latlon -> latlon.lon(lon).lat(lat)));
                            geoDistance.distance(distanceLimit + "km");
                            return geoDistance;});
                        return must;
                    });

                    // 关键字匹配 满足一个字段即可 服务项名称,服务类型名称,服务地址
                    if (StringUtils.isNotEmpty(ordersSerizeListReqDTO.getKeyWord())) {
                        bool.must(must -> must.match(match -> match.field(FieldConstants.KEY_WORDS).query(ordersSerizeListReqDTO.getKeyWord())));
                    }

                    return bool;
                }));
        // 排序 根据距离排序
        List<SortOptions> sortOptions = new ArrayList<>();
        sortOptions.add(SortOptions.of(sortOption -> sortOption.geoDistance(
                geoDistance -> {
                    geoDistance.field(LOCATION);
                    geoDistance.distanceType(GeoDistanceType.Arc);
                    geoDistance.order(SortOrder.Asc);
                    geoDistance.unit(DistanceUnit.Kilometers);
                    geoDistance.location(location -> location.latlon(latlon -> latlon.lat(lat).lon(lon)));
                    return geoDistance;
                }
        )));
        builder.sort(sortOptions);
        // 索引
        builder.index(EsIndexConstants.ORDERS_SEIZE);

        // 滚动分页,根据距离滚动分页
        if (ordersSerizeListReqDTO.getLastRealDistance() != null) {
            builder.searchAfter(ordersSerizeListReqDTO.getLastRealDistance().toString());
        }

        // 检索数据
        SearchResponse<OrdersSeizeInfo> searchResponse = elasticSearchTemplate.opsForDoc().search(builder.build(), OrdersSeizeInfo.class);
        if (SearchResponseUtils.isSuccess(searchResponse)) {
            return searchResponse.hits().hits()
                    .stream().map(hit -> {
                        // 从sort字段中获取实际距离
                        double realDistance = NumberUtils.parseDouble(CollUtils.getFirst(hit.sort()));
                        OrdersSeizeListResDTO.OrdersSeize ordersSeize = BeanUtils.toBean(hit.source(), OrdersSeizeListResDTO.OrdersSeize.class);
                        ordersSeize.setRealDistance(realDistance);
                        return ordersSeize;
                    })
                    .collect(Collectors.toList());
        }
        return null;
    }

抢单

抢单流程

参考抢券的方案,使用Redis+Lua的技术解决超卖问题,这里将抢单库存同步到Redis,每个订单的库存就是1。

抢单执行Lua脚本完成抢单,具体包括:扣减库存、抢单成功写入同步队列。

抢单同步队列的作用是通过异步任务将抢单结果信息同步到数据库。

Redis数据结构设计

抢单同步队列

缓存结构:Hash

RedisKey:QUEUE:ORDERS:SEIZE:SYNC:{citycode%10}

HashKey:订单id

HashValue:多值拼接中间用逗号分隔,分别为:被派单服务人员id/机构id、服务人员类型(2,服务人员,3:机构端),是否是机器抢单(1:机器抢单,0:人工抢单)

过期时间:永不过期

抢单库存结构

缓存结构:Hash

RedisKey:ORDERS:RESOURCE:STOCK:{citycode%10}

HashKey:订单id

HashValue: 库存,为1

过期时间:永不过期

缓存一致性方案:通过Canal进行同步

 

package com.jzo2o.orders.seize.controller.worker;


@RestController
@Api(tags = "服务端 - 抢单相关接口")
@RequestMapping("/worker")
@Slf4j
public class WorkerOrdersSeizeController {

    @Resource
    private IOrdersSeizeService ordersSeizeService;

    @GetMapping("")
    @ApiOperation("服务端抢单列表")
    public OrdersSeizeListResDTO queryForList(OrdersSerizeListReqDTO ordersSerizeListReqDTO) {
        return ordersSeizeService.queryForList(ordersSerizeListReqDTO);
    }

    @PostMapping("")
    @ApiOperation("服务端抢单")
    public void seize(@RequestBody OrdersSeizeReqDTO ordersSeizeReqDTO) {
        ordersSeizeService.seize(ordersSeizeReqDTO.getId(), UserContext.currentUserId(), UserContext.currentUser().getUserType(), false);
    }
}

@Service
@Slf4j
public class OrdersSeizeServiceImpl extends ServiceImpl<OrdersSeizeMapper, OrdersSeize> implements IOrdersSeizeService {

public void seize(Long id, Long serveProviderId, Integer serveProviderType, Boolean isMatchine) {

    // 1.抢单校验
    // 1.1.校验是否可以查询(认证通过,开启抢单)
    ServeProviderResDTO detail = serveProviderApi.getDetail(serveProviderId);
    if (!detail.getCanPickUp() || detail.getSettingsStatus() != 1) {
        throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_RECEIVE_CLOSED);
    }
    // 1.2.校验抢单资源是否存在
    OrdersSeize ordersSeize = ordersSeizeService.getById(id);
    // 校验订单是否还存在,如果订单为空或id不存在,则认为订单已经不在
    if (ordersSeize == null || ObjectUtils.isNull(ordersSeize.getId())) {
        throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_FAILD);
    }
    ConfigRegionInnerResDTO configRegionInnerResDTO = regionApi.findConfigRegionByCityCode(detail.getCityCode());


    // 城市编码最后1位序号
    int index = RedisUtils.getCityIndex(detail.getCityCode());
    // 1.3.校验时间冲突
    // 服务时间状态redisKey
    String serveProviderStateRedisKey = String.format(SERVE_PROVIDER_STATE, index);

    // 1.4.订单数量已达上限
    // 接单数量上限
    int receiveOrderMax = (serveProviderType == UserType.INSTITUTION) ? configRegionInnerResDTO.getInstitutionReceiveOrderMax() : configRegionInnerResDTO.getStaffReceiveOrderMax();

    Object ordersNum = redisTemplate.opsForHash().get(serveProviderStateRedisKey, serveProviderId + "_num");
    if(ObjectUtils.isNotNull(ordersNum) && NumberUtils.parseInt(ordersNum.toString()) >= receiveOrderMax){
        throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_RECEIVE_ORDERS_NUM_OVER);
    }

    // 2.执行redis脚本

    // 2.1.redisKey
    // 抢单结果同步队列 redis key
    String ordersSeizeSyncRedisKey = RedisSyncQueueUtils.getQueueRedisKey(RedisConstants.RedisKey.ORERS_SEIZE_SYNC_QUEUE_NAME, index);
    // 库存redisKey
    String resourceStockRedisKey = String.format(ORDERS_RESOURCE_STOCK, index);

    log.debug("抢单key:{},values:{}", Arrays.asList(ordersSeizeSyncRedisKey, resourceStockRedisKey),
            Arrays.asList(id, serveProviderId,serveProviderType));
    // 2.2.执行lua脚本
    Object execute = redisTemplate.execute(seizeOrdersScript,
            Arrays.asList(ordersSeizeSyncRedisKey, resourceStockRedisKey),
            id, serveProviderId,serveProviderType,isMatchine ? 1 : 0);
    log.debug("抢单结果 : {}", execute);

    // 3.处理lua脚本结果
    if (execute == null) {
        throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_FAILD);
    }
    // 4.抢单结果判断 大于0抢单成功,-1/-2:库存数量不足,-3:抢单失败
    long result = NumberUtils.parseLong(execute.toString());
    if(result < 0) {
        throw new CommonException(ErrorInfo.Code.SEIZE_ORDERS_FAILD, SEIZE_ORDERS_FAILD);
    }
}

这个开始时间的队列,在lua脚本中没有使用 

向redis中存抢单用户的服务开始时间和当天的已接单数,但是好像文中抢单时,没有往redis中里面写已经抢了多少单,没有更新,而且第二天的时候也不会把这些删除。只是抢单的时候从redis中取了一下,存在bug

 

抢单结果异步处理

异步处理方案

抢单成功根据抢单结果进行异步处理:

交互流程如下:

创建服务单

服务单记录了服务人员进行家政服务的信息,关键字段有:订单ID、订单金额、服务人员ID、服务单状态、服务时间、服务照片等。

服务单初始状态:待分配或待服务

机构抢单成功:待分配。

服务人员抢单成功:待服务。

更新订单的状态:

用户下单并支付完成后订单的状态为“派单中”,服务人员抢单成功订单状态为“待服务”,机构抢单成功订单状态为“待分配”。

抢单结果同步成功删除抢单池等相关信息:

抢单结果同步成功后删除抢单池中该订单的信息,这样服务人员在抢单界面无法查询到该订单。

删除数据库中抢单池的记录,通过Canal将Elasticsearch中对应的抢单记录删除。

删除Redis中该订单的库存信息。

删除Redis中抢单同步队列中的记录。

表设计

服务单表设计

-- 服务任务 
-- auto-generated definition
create table orders_serve_0
(
    id                      bigint                             not null comment '任务id'
        primary key,
    user_id                 bigint                             null comment '属于哪个用户',
    serve_provider_id       bigint                             not null comment '服务人员或服务机构id',
    serve_provider_type     int                                null comment '服务者类型,2:服务端服务,3:机构端服务',
    institution_staff_id    bigint                             null comment '机构服务人员id',
    orders_id               bigint                             null comment '订单id',
    orders_origin_type      int                                not null comment '订单来源类型,1:抢单,2:派单',
    city_code               varchar(50)                        not null comment '城市编码',
    serve_type_id           bigint                             not null comment '服务分类id',
    serve_start_time        datetime                           null comment '预约时间',
    serve_item_id           bigint                             not null comment '服务项id',
    serve_status            int                                not null comment '任务状态',
    settlement_status       int      default 0                 not null comment '结算状态,0:不可结算,1:待结算,2:结算完成',
    real_serve_start_time   datetime                           null comment '实际服务开始时间',
    real_serve_end_time     datetime                           null comment '实际服务完结时间',
    serve_before_imgs       json                               null comment '服务前照片',
    serve_after_imgs        json                               null comment '服务后照片',
    serve_before_illustrate varchar(255)                       null comment '服务前说明',
    serve_after_illustrate  varchar(255)                       null comment '服务后说明',
    cancel_time             datetime                           null comment '取消时间,可以是退单,可以是取消时间',
    orders_amount           decimal(10, 2)                     null comment '订单金额',
    pur_num                 int                                null comment '购买数量',
    create_time             datetime default CURRENT_TIMESTAMP null comment '创建时间',
    update_time             datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
    sort_by                 bigint                             null comment '排序字段(serve_start_time(秒级时间戳)+订单id(后6位))',
    display                 int      default 1                 null comment '服务端/机构端是否展示,1:展示,0:隐藏',
    is_deleted              int      default 0                 null comment '是否是逻辑删除',
    update_by               bigint                             null comment '更新人'
)
    comment '服务任务' charset = utf8mb4;

分库分表策略:

分库策略:按服务人员id分库,表达式:jzo2o-orders-${serve_provider_id % 3}

分表策略:orders_serve_${(int)Math.floor(id % 10000000000 / 15000000)}

抢单成功同步xxl-job

 /**
     * 抢单成功同步任务
     */
    @XxlJob("seizeSyncJob")
    public void seizeSyncJob() {
        syncManager.start(ORERS_SEIZE_SYNC_QUEUE_NAME, RedisSyncQueueConstants.STORAGE_TYPE_HASH, RedisSyncQueueConstants.MODE_SINGLE);
    }
package com.jzo2o.orders.seize.handler;

import cn.hutool.json.JSONArray;
import com.jzo2o.api.customer.ServeProviderApi;
import com.jzo2o.api.customer.dto.response.ServeProviderResDTO;
import com.jzo2o.common.utils.JsonUtils;
import com.jzo2o.common.utils.NumberUtils;
import com.jzo2o.orders.base.model.domain.OrdersSeize;
import com.jzo2o.orders.seize.service.IOrdersSeizeService;
import com.jzo2o.redis.handler.SyncProcessHandler;
import com.jzo2o.redis.model.SyncMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
 * 抢单成功同步任务
 */
@Component("ORDERS:SEIZE:SYNC")
@Slf4j
public class SeizeSyncProcessHandler implements SyncProcessHandler<Object> {

    @Resource
    private IOrdersSeizeService ordersSeizeService;


    @Resource
    private ServeProviderApi serveProviderApi;

    @Override
    public void batchProcess(List<SyncMessage<Object>> multiData) {
        throw new RuntimeException("不支持批量处理");
    }

    @Override
    public void singleProcess(SyncMessage<Object> singleData) {
        log.info("抢单结果同步开始 id : {}",singleData.getKey());
        // 抢单信息放在value中,内容格式:[serveProviderId,serveProviderType,isMatchine(0,表示人工抢单,1:表示机器抢单)]
        JSONArray seizeResult = JsonUtils.parseArray(singleData.getValue());
        // 服务人员或机构id
        Long serveProviderId = seizeResult.getLong(0);
        // 用户类型
        Integer serveProviderType = seizeResult.getInt(1);
        // 是否是机器抢单
        boolean isMatchine = seizeResult.getBool(2);

        // 抢单id
        Long seizeId = NumberUtils.parseLong(singleData.getKey());
        // 抢单不在无需继续处理 因为单子都放在抢单中,此时还没删除
        OrdersSeize ordersSeize = ordersSeizeService.getById(seizeId);
        if (ordersSeize == null) {
            return;
        }

        // 处理抢单结果
        ordersSeizeService.seizeOrdersSuccess(ordersSeize, serveProviderId, serveProviderType, isMatchine);
        log.info("抢单结果同步结束 id : {}",singleData.getKey());
    }
}

ordersSeizeService.seizeOrdersSuccess()方法如下:

@Transactional(rollbackFor = Exception.class)
    public void seizeOrdersSuccess(OrdersSeize ordersSeize, Long serveProviderId, Integer serveProviderType, Boolean isMatchine) {

        // 1.校验服务单是否已经生成
        OrdersServe ordersServeInDb = ordersServeService.findById(ordersSeize.getId());
        if(ordersServeInDb != null){
            return;
        }
        // 2.生成服务单,
        OrdersServe ordersServe = BeanUtils.toBean(ordersSeize, OrdersServe.class);
        ordersServe.setCreateTime(null);
        ordersServe.setUpdateTime(null);
        // 服务单状态 机构抢单状态:待分配;服务人员抢单状态:待服务
        int serveStatus = UserType.WORKER == serveProviderType ? ServeStatusEnum.NO_SERVED.getStatus() : ServeStatusEnum.NO_ALLOCATION.getStatus();
        // 服务单来源类型,人工抢单来源抢单,值为1;机器抢单来源派单,值为2
        int ordersOriginType = isMatchine ? OrdersOriginType.DISPATCH : OrdersOriginType.SEIZE;
        ordersServe.setOrdersOriginType(ordersOriginType);
        ordersServe.setServeStatus(serveStatus);
        ordersServe.setServeProviderId(serveProviderId);
        ordersServe.setServeProviderType(serveProviderType);
        if(!ordersServeService.save(ordersServe)){
            return;
        }

        // 3.当前订单数量
        serveProviderSyncService.countServeTimesAndAcceptanceNum(serveProviderId, serveProviderType);

        String resourceStockRedisKey = String.format(ORDERS_RESOURCE_STOCK, RedisUtils.getCityIndex(ordersSeize.getCityCode()));
        Object stock = redisTemplate.opsForHash().get(resourceStockRedisKey, ordersSeize.getId());
        if (ObjectUtils.isNull(stock) || NumberUtils.parseInt(stock.toString()) <= 0) {
            ordersDispatchMapper.deleteById(ordersSeize.getId());
            ordersSeizeService.removeById(ordersSeize.getId());
            redisTemplate.opsForHash().delete(resourceStockRedisKey, ordersSeize.getId());
        }

        //状态机修改订单状态
//        OrderSnapshotDTO orderSnapshotDTO = OrderSnapshotDTO.builder()
//                .ordersStatus(OrderStatusEnum.NO_SERVE.getStatus()).build();
        Orders orders = ordersMapper.selectById(ordersSeize.getId());
        orderStateMachine.changeStatus(orders.getUserId(),String.valueOf(ordersSeize.getId()), OrderStatusChangeEventEnum.DISPATCH);

    }

 

当抢单数据同步到数据库后,会在抢单池中删除该订单,canal然后发送到mq,mq监听到删除命令

 根据类别判断是删除还是添加

    @Override
    public void batchDelete(List<Long> ids) {
        log.info("抢单删除开始,删除数量:{},开始id:{},结束id:{}", CollUtils.size(ids), CollUtils.getFirst(ids), CollUtils.getLast(ids));
        Boolean result = elasticSearchTemplate.opsForDoc().batchDelete(ORDERS_SEIZE, ids);
        if (!result){
            throw new RuntimeException("同步抢单池加入es失败");
        }
        log.info("抢单删除结束,删除数量:{},开始id:{},结束id:{}", CollUtils.size(ids), CollUtils.getFirst(ids), CollUtils.getLast(ids));

    }

nRF24L01是一种低功耗2.4GHz无线通信模块,可以广泛应用于无线传感器网络、远距离数据传输等领域。nRF24L01模块的配对是指将两个或多个模块进行连接和通信的过程。 要进行nRF24L01的配对,需要以下步骤: 1. 硬件连接:首先,需要将两个nRF24L01模块与Arduino等主控设备相连接。其中一个模块作为发送端,另一个作为接收端。 2. 编程设置:使用相应的编程语言和开发环境,编写代码来控制nRF24L01模块的工作。发送端的代码负责将数据发送到接收端,接收端的代码负责接收和解析发送端的数据。 3. 地址匹配:nRF24L01模块有一个可配置的地址,包括发送地址(TX地址)和接收地址(RX地址)。在配对过程中,需要确保发送端和接收端的地址是一致的,以便正确地进行通信。 4. 信道设置:nRF24L01模块有多个信道可供选择。在进行配对时,需要确保发送端和接收端选择了相同的信道,以便它们能够在同一个率上进行通信。 5. 数据传输:通过编程设置发送端和接收端的工作模式和数据传输方式,可以实现数据的双向传输。发送端将需要传输的数据发送给接收端,接收端接收到数据后进行相应的处理或反馈。 6. 通信测试:完成以上步骤后,可以进行一些测试来验证nRF24L01模块的配对是否成功。可以发送一些简单的数据或指令,观察接收端是否能够正确接收并执行相应的操作。 通过以上步骤,可以实现nRF24L01模块的配对,使其能够进行有效的无线通信。在实际应用中,可以根据需要添加更多的功能和安全措施,以确保通信的稳定性和可靠性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值