什么是Apollo自动驾驶平台?_自动驾驶阿波罗

因此,Apollo选择了Google的Protocol Buffers格式数据来解决这个问题。

Protocol Buffers,是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。它不依赖于语言和平台并且可扩展性极强。现阶段官方支持C++、JAVA、Python三种编程语言,但可以找到大量的几乎涵盖所有语言的第三方拓展包。

注:如果你查看了Apollo项目的源码,可以看到很多名称为“proto”的文件夹,这些文件夹中包含的就是Protocol Buffers(简称protobuf)格式的数据结构。

硬件架构

Apollo 2.5上必须的硬件如下表所示:

外设包括下面这些:

硬件架构如下图所示:

软件架构

Apollo平台的软件架构如下图所示:

在Apollo上,运行的核心软件模块包括:

  • Perception:感知模块识别自动车辆周围的世界。在Perception模块中有两个重要的子模块:障碍物检测和交通灯检测。
  • Prediction:Prediction模块预测未来的感知障碍物的运动轨迹。
  • Routing:Routing模块告诉自动车辆如何通过一系列车道或道路到达目的地。
  • Planning:Planning模块计划自主车辆的时空轨迹。
  • Control:Control模块通过生成诸如节流阀,制动器和转向的控制命令来执行计划的时空轨迹。
  • CanBus:CanBus将控制命令传递给车辆硬件的接口。它还将机架信息传递给软件系统。
  • HD-Map:提供有关道路特定结构化的信息。
  • Localization:该模块利用各种信息来源,例如GPS,LiDAR和IMU来估计自动车辆所在的位置。

这些模块的交互结构如下图所示:

每个模块都作为独立的基于CarOS的ROS节点运行。每个模块节点都会发布和订阅某些主题。订阅的主题用作数据输入,而发布的主题用作数据输出。

关于Apollo平台的系统架构可以阅读这篇文档:HOW TO UNDERSTAND ARCHITECTURE AND WORKFLOW

从这篇文档中我们看到:

  • 自动驾驶车辆由规划引擎通过CAN总线(Controller Area Network bus)来进行控制。
  • 为了计算效率,Location模块,Perception模块,Planning模块作为独立的输入源和输出源通过P2P一起工作。
  • 通过阅读源码${MODULE_NAME}/conf目录下的配置文件,我们可以获得有关模块订阅和发布的主题的基本信息。
  • 每个模块通过触发 Init 接口和注册回调开始。
  • 所有模块都会在下面这个点上注册:AdapterManager::Init。该函数部分代码片段如下:
void AdapterManager::Init(const AdapterManagerConfig &configs) {
  if (Initialized()) {
    return;
  }

  instance()->initialized_ = true;
  if (configs.is_ros()) {
    instance()->node_handle_.reset(new ros::NodeHandle());
  }

  for (const auto &config : configs.config()) {
    switch (config.type()) {
      case AdapterConfig::POINT_CLOUD:
        EnablePointCloud(FLAGS_pointcloud_topic, config);
        break;
      case AdapterConfig::GPS:
        EnableGps(FLAGS_gps_topic, config);
        break;
      case AdapterConfig::IMU:
        EnableImu(FLAGS_imu_topic, config);
        break;
      case AdapterConfig::RAW_IMU:
        EnableRawImu(FLAGS_raw_imu_topic, config);
        break;
      case AdapterConfig::CHASSIS:
        EnableChassis(FLAGS_chassis_topic, config);
        break;
      case AdapterConfig::LOCALIZATION:
        EnableLocalization(FLAGS_localization_topic, config);
        break;
      case AdapterConfig::PERCEPTION_OBSTACLES:
        EnablePerceptionObstacles(FLAGS_perception_obstacle_topic, config);
        break;
      case AdapterConfig::TRAFFIC_LIGHT_DETECTION:
        EnableTrafficLightDetection(FLAGS_traffic_light_detection_topic,
                                    config);
     ...

下面是对系统中主要的核心模块的一些解析。

核心模块

apollo/modules/中包含了系统中的各个模块的源码。

阅读这些源码会发现,这些核心模块的类都继承自一个公共基类ApolloApp,相关结构如下图所示:

ApolloApp类的结构如下图所示:

该类中的主要函数说明如下:

函数名说明

apollo_app.h这个头文件中,还包含了一个宏以方便每个模块声明main函数,相关代码如下:

#define APOLLO_MAIN(APP)                                       \
  int main(int argc, char **argv) {                            \
    google::InitGoogleLogging(argv[0]);                        \
    google::ParseCommandLineFlags(&argc, &argv, true);         \
    signal(SIGINT, apollo::common::apollo_app_sigint_handler); \
    APP apollo_app_;                                           \
    ros::init(argc, argv, apollo_app_.Name());                 \
    apollo_app_.Spin();                                        \
    return 0;                                                  \
  }

每个模块的根目录都包含了一个README.md文件,是对这个模块的说明。我们可以以此为入口来了解模块的实现。

Perception(感知)

模块介绍

自动驾驶车辆通过前置摄像头和雷达与最近的车辆(closest in-path vehicle,简称CIPV)保持距离。子模块还预测障碍物运动和位置信息(例如,航向和速度)。Apollo 2.5支持高速公路上的高速自动驾驶,无需任何地图。深度网络算法已经学会处理图像数据。随着收集更多数据,深度网络的性能将随着时间的推移而提高。

模块输入:

  • 雷达数据
  • 图像数据
  • 雷达传感器校准的外部参数(来自YAML文件)
  • 前置相机校准的外部和内部参数(来自YAML文件)
  • 车辆的速度和角速度

模块输出:

  • 3D障碍物跟踪航向,速度和分类信息
  • 带有拟合曲线参数的车道标记信息,空间信息以及语义信息
模块解析

Perception模块需要根据输入信息快速的解析出两类信息,即:道路和物体。其中的深入网络基于YOLO算法[1][2]

Apollo 2.5不支持高曲率,没有车道标志的道路,包括当地道路和交叉路口。感知模块基于使用具有有限数据的深度网络的视觉检测。因此,在发布更好的网络之前,驾驶员在驾驶时应小心谨慎,并始终准备好通过将车轮转向正确的方向来解除自主驾驶。

  • 推荐道路

    • 两侧清晰的白色车道线
  • 不推荐道路

    • 高曲率的道路
    • 没有车道线标记的道路
    • 路口
    • 对接点或虚线车道线
    • 公共道路

而对于物体来说,又分为静态物体和动态物体。静态物体包括道路和交通灯等。动态物体包括机动车,自行车,行人,动物等。

为了保持车辆在车道上,需要一系列模块的配合,相关流程图如下所示:

Perception模块在Init函数中会注册一系列类以完成模块启动后的正常工作,相关代码如下:

void Perception::RegistAllOnboardClass() {
  /// regist sharedata
  RegisterFactoryLidarObjectData();
  RegisterFactoryRadarObjectData();
  RegisterFactoryCameraObjectData();
  RegisterFactoryCameraSharedData();
  RegisterFactoryCIPVObjectData();
  RegisterFactoryLaneSharedData();
  RegisterFactoryFusionSharedData();
  traffic_light::RegisterFactoryTLPreprocessingData();

  /// regist subnode
  RegisterFactoryLidarProcessSubnode();
  RegisterFactoryRadarProcessSubnode();
  RegisterFactoryCameraProcessSubnode();
  RegisterFactoryCIPVSubnode();
  RegisterFactoryLanePostProcessingSubnode();
  RegisterFactoryAsyncFusionSubnode();
  RegisterFactoryFusionSubnode();
  RegisterFactoryMotionService();
  lowcostvisualizer::RegisterFactoryVisualizationSubnode();
  traffic_light::RegisterFactoryTLPreprocessorSubnode();
  traffic_light::RegisterFactoryTLProcSubnode();
}

我们可以以这里为入口了解各个子模块的逻辑。

RegisterFactoryLidarProcessSubnode为例。

代码中其实并不存在RegisterFactoryLidarProcessSubnode这个函数,该函数的定义其实是由宏完成的。相关代码如下:

Lidar(也称之为LIDAR,LiDAR,或LADAR)的全称是Light Detection And Ranging,即激光探测与测量。

// /modules/perception/onboard/subnode.h
#define REGISTER_SUBNODE(name) REGISTER_CLASS(Subnode, name)


// /modules/perception/lib/base/registerer.h
#define REGISTER_CLASS(clazz, name)                                           \
  class ObjectFactory##name : public apollo::perception::ObjectFactory {      \
   public:                                                                    \
    virtual ~ObjectFactory##name() {}                                         \
    virtual perception::Any NewInstance() {                                   \
      return perception::Any(new name());                                     \
    }                                                                         \
  };                                                                          \
  inline void RegisterFactory##name() {                                       \
    perception::FactoryMap &map = perception::GlobalFactoryMap()[#clazz];     \
    if (map.find(#name) == map.end()) map[#name] = new ObjectFactory##name(); \
  }

而在lidar_process_subnode.h中使用了上面这个宏。

REGISTER_SUBNODE(LidarProcessSubnode);

于是就会生成一个名称为ObjectFactoryLidarProcessSubnode的类,该类继承自apollo::perception::ObjectFactory,并且其中包含了名称为RegisterFactoryLidarProcessSubnode的函数。

Prediction(预测)

模块介绍

Prediction模块从Perception模块接受障碍物信息。该模块需要的信息包括位置,航向,速度,加速度,并产生具有障碍概率的预测轨迹。

模块输入:

  • 来自Prediction模块的障碍物信息
  • 来自Localizaton模块的位置信息

模块输出:

  • 障碍物的预测轨迹
模块解析

Prediction的Init函数中添加了三个回调用来从其他模块获取信息的更新:

AdapterManager::AddLocalizationCallback(&Prediction::OnLocalization, this);
AdapterManager::AddPlanningCallback(&Prediction::OnPlanning, this);
AdapterManager::AddPerceptionObstaclesCallback(&Prediction::RunOnce, this);

这里最重要的就是Prediction::RunOnce这个函数。这个函数中包含了Prediction模块的主要逻辑,它会在接收到一个新的障碍物消息时触发。

Prediction模块中有三类重要的子模块。

第一类是Container,用来存储从订阅频道获取的数据。包括:

  • 感到到的障碍物信息
  • 车辆位置信息
  • 车辆计划信息

第二类是Evaluator,用来针对指定的障碍物预测路线和速度。目前有三类Evaluator,包括:

  • Cost evaluator:通过一组代价函数来计算可能性
  • MLP evaluator:通过MLP模型来计算可能性
  • RNN evaluator:通过RNN模型来计算可能性

Evaluator通过EvaluatorManager类管理,Evaluator类结构如下图所示:

Prediction模块中第三类重要的子模块就是Predictor。它用来预测障碍物的轨迹。

不同的障碍物运动的轨迹会不一样,因此实现中包含了很多个类型的Predictor,它们的结构如下图所示。

类似的,会有一个PredictorManager来管理Predictor。

Routing(路由)

模块介绍

Routing模块根据请求生成导航信息。

模块输入:

  • 地图数据
  • 请求,包括:开始和结束位置

模块输出:

  • 路由导航信息
模块解析

Routing模块的内部结构如下图所示:

Routing模块的输入是地图数据和导航请求,因此其Init函数就是围绕这个逻辑的:

apollo::common::Status Routing::Init() {
  const auto routing_map_file = apollo::hdmap::RoutingMapFile();
  AINFO << "Use routing topology graph path: " << routing_map_file;
  navigator_ptr_.reset(new Navigator(routing_map_file));
  CHECK(common::util::GetProtoFromFile(FLAGS_routing_conf_file, &routing_conf_))
      << "Unable to load routing conf file: " + FLAGS_routing_conf_file;

  AINFO << "Conf file: " << FLAGS_routing_conf_file << " is loaded.";

  hdmap_ = apollo::hdmap::HDMapUtil::BaseMapPtr();
  CHECK(hdmap_) << "Failed to load map file:" << apollo::hdmap::BaseMapFile();

  AdapterManager::Init(FLAGS_routing_adapter_config_filename);
  AdapterManager::AddRoutingRequestCallback(&Routing::OnRoutingRequest, this);
  return apollo::common::Status::OK();
}

这段代码的重点是下面三个地方:

  • apollo::hdmap::RoutingMapFile()包含了HD地图数据。
  • Navigator负责导航,我们很容易想到这个类应当是该模块的核心。
  • Routing::OnRoutingRequest是接收导航请求的回调函数。

Routing::OnRoutingRequest中,最主要的就是通过Navigator::SearchRoute来搜索导航路径。

void Routing::OnRoutingRequest(const RoutingRequest& routing_request) {
  AINFO << "Get new routing request:" << routing_request.DebugString();
  RoutingResponse routing_response;
  apollo::common::monitor::MonitorLogBuffer buffer(&monitor_logger_);
  const auto& fixed_request = FillLaneInfoIfMissing(routing_request);
  if (!navigator_ptr_->SearchRoute(fixed_request, &routing_response)) {
    AERROR << "Failed to search route with navigator.";

    buffer.WARN("Routing failed! " + routing_response.status().msg());
    return;
  }
  buffer.INFO("Routing success!");
  AdapterManager::PublishRoutingResponse(routing_response);
  return;
}

目前,Apollo 2.5版本中的导航基于A*算法。这是一种在图形平面上,有多个节点的路径,求出最低通过成本的算法。该算法综合了Best-First Search和Dijkstra算法的优点:在进行启发式搜索提高算法效率的同时,可以保证找到一条最优路径(基于评估函数)。

在A*算法计算的过程中,会尝试多条路径。一旦遇到障碍物,便将该路径上的点标记为不需要继续探索(图中的实心点)。继续以剩下的空心点为基础探索。最终求得最优路径。

下图动态描述了A*算法查找目标路径的算法过程。

动图封面

Planing(计划)

模块介绍

Planing模块根据定位信息,车辆状态(位置,速度,加速度,底盘),地图,路线,感知和预测,计算出安全和舒适的形式线路让控制器执行。

目前的系统实现中包含了四种计划器:

  • RTKReplayPlanner(自Apollo 1.0以来):RTK重放计划器首先在初始化时加载记录的轨迹,并根据当前系统时间和车辆位置发送适当的轨迹段。
  • EMPlanner(自Apollo1.5以来。EM是Expectation Maximization的缩写):EM计划器,会根据地图,路线和障碍物计算驾驶决策和线路。基于动态规划(Dynamic programming,简称DP)的方法首先用于确定原始路径和速度曲线,然后使用基于二次规划(Quadratic programming,简称QP)的方法来进一步优化路径和速度曲线以获得平滑的轨迹。
  • LatticePlanner:网格计划器
  • NaviPlanner:这是一个基于实时相对地图的计划器。它使用车辆的FLU(Front-Left-Up)坐标系来完成巡航,跟随,超车,接近,变道和停车任务。

模块输入:

  • RTK重放计划器:

    • Localization
    • 记录的RTK轨迹
  • EM计划器:

    • Localization
    • Perception
    • Prediction
    • HD Map
    • Routing

模块输出:

  • 无碰撞和舒适的轨迹让控制模块的执行
模块解析

Planing模块在初始化的Init函数中,这里面会注册所有的计划器,然后根据配置文件中的配置确定当前所使用的计划器。目前,配置文件中配置的是EM计划器。

Planing模块在Start函数中设定了一个Timer用来完成定时任务:

Status Planning::Start() {
  timer_ = AdapterManager::CreateTimer(
      ros::Duration(1.0 / FLAGS_planning_loop_rate), &Planning::OnTimer, this);
...

Planning::OnTimer最主要的就是调用RunOnce(),而后者包含了Planing模块的核心逻辑。目前,FLAGS_planning_loop_rate值是10。也就是说,Planing模块运行的频度是每秒钟10次。

在Planning::Plan函数中,更通过配置的计划器进行路线的计算,然后将结果对外发布。关键代码如下:

Status Planning::Plan(const double current_time_stamp,
                      const std::vector<TrajectoryPoint>& stitching_trajectory,
                      ADCTrajectory* trajectory_pb) {
  auto* ptr_debug = trajectory_pb->mutable_debug();
  if (FLAGS_enable_record_debug) {
    ptr_debug->mutable_planning_data()->mutable_init_point()->CopyFrom(
        stitching_trajectory.back());
  }

  auto status = planner_->Plan(stitching_trajectory.back(), frame_.get());

  ExportReferenceLineDebug(ptr_debug);

  const auto* best_ref_info = frame_->FindDriveReferenceLineInfo();
  if (!best_ref_info) {
    std::string msg("planner failed to make a driving plan");
    AERROR << msg;
    if (last_publishable_trajectory_) {
      last_publishable_trajectory_->Clear();
    }
    return Status(ErrorCode::PLANNING_ERROR, msg);
  }
  ptr_debug->MergeFrom(best_ref_info->debug());
  trajectory_pb->mutable_latency_stats()->MergeFrom(
      best_ref_info->latency_stats());
  // set right of way status
  trajectory_pb->set_right_of_way_status(best_ref_info->GetRightOfWayStatus());
  for (const auto& id : best_ref_info->TargetLaneId()) {
    trajectory_pb->add_lane_id()->CopyFrom(id);
  }

  best_ref_info->ExportDecision(trajectory_pb->mutable_decision());

  ...
  
  last_publishable_trajectory_->PrependTrajectoryPoints(
      stitching_trajectory.begin(), stitching_trajectory.end() - 1);

  for (size_t i = 0; i < last_publishable_trajectory_->NumOfPoints(); ++i) {
    if (last_publishable_trajectory_->TrajectoryPointAt(i).relative_time() >
        FLAGS_trajectory_time_high_density_period) {
      break;
    }
    ADEBUG << last_publishable_trajectory_->TrajectoryPointAt(i)
                  .ShortDebugString();
  }

  last_publishable_trajectory_->PopulateTrajectoryProtobuf(trajectory_pb);

  best_ref_info->ExportEngageAdvice(trajectory_pb->mutable_engage_advice());

  return status;
}

Control(控制)

模块介绍

控制模块根据计划和当前的汽车状态,使用不同的控制算法来生成舒适的驾驶体验。控制模块可以在正常模式和导航模式下工作。

模块输入:

  • 计划的线路
  • 车辆状态
  • 定位信息
  • Dreamview AUTO模式更改请求

模块输出:

  • 控制命令(转向,油门,刹车)到底盘
模块解析

Control模块的主体逻辑也是通过Timer定时执行的。在定时触发的函数Control::OnTimer中,会生成命令然后派发出去:

void Control::OnTimer(const ros::TimerEvent &) {
  double start_timestamp = Clock::NowInSeconds();

  if (FLAGS_is_control_test_mode && FLAGS_control_test_duration > 0 &&
      (start_timestamp - init_time_) > FLAGS_control_test_duration) {
    AERROR << "Control finished testing. exit";
    ros::shutdown();
  }

  ControlCommand control_command;

  Status status = ProduceControlCommand(&control_command);
  AERROR_IF(!status.ok()) << "Failed to produce control command:"
                          << status.error_message();

  double end_timestamp = Clock::NowInSeconds();



**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

**深知大多数Linux运维工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
![img](https://img-blog.csdnimg.cn/img_convert/89b9cec56fd1c8ed13cdfb41d7e8898e.png)
![img](https://img-blog.csdnimg.cn/img_convert/52d92315eed864a4bb341a455d37ace3.png)
![img](https://img-blog.csdnimg.cn/img_convert/261798246cc82dd59e1fd11e673b6494.png)
![img](https://img-blog.csdnimg.cn/img_convert/932f9ca78dc64fd0e96bdf54cae19e5e.png)
![img](https://img-blog.csdnimg.cn/img_convert/9ec701a012ffbac995580d115c01b8a7.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Linux运维知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

**如果你觉得这些内容对你有帮助,可以添加VX:vip1024b (备注Linux运维获取)**
![img](https://img-blog.csdnimg.cn/img_convert/f67273b9b0a2013dfdc9840cef30eca2.jpeg)



为了做好运维面试路上的助攻手,特整理了上百道 **【运维技术栈面试题集锦】** ,让你面试不慌心不跳,高薪offer怀里抱!

这次整理的面试题,**小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。**

![](https://img-blog.csdnimg.cn/img_convert/00231552317299d3a677a5aa2031a76e.png)

本份面试集锦涵盖了

*   **174 道运维工程师面试题**
*   **128道k8s面试题**
*   **108道shell脚本面试题**
*   **200道Linux面试题**
*   **51道docker面试题**
*   **35道Jenkis面试题**
*   **78道MongoDB面试题**
*   **17道ansible面试题**
*   **60道dubbo面试题**
*   **53道kafka面试**
*   **18道mysql面试题**
*   **40道nginx面试题**
*   **77道redis面试题**
*   **28道zookeeper**

**总计 1000+ 道面试题, 内容 又全含金量又高**

*   **174道运维工程师面试题**

> 1、什么是运维?

> 2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

> 3、现在给你三百台服务器,你怎么对他们进行管理?

> 4、简述raid0 raid1raid5二种工作模式的工作原理及特点

> 5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

> 6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

> 7、Tomcat和Resin有什么区别,工作中你怎么选择?

> 8、什么是中间件?什么是jdk?

> 9、讲述一下Tomcat8005、8009、8080三个端口的含义?

> 10、什么叫CDN?

> 11、什么叫网站灰度发布?

> 12、简述DNS进行域名解析的过程?

> 13、RabbitMQ是什么东西?

> 14、讲一下Keepalived的工作原理?

> 15、讲述一下LVS三种模式的工作过程?

> 16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

> 17、如何重置mysql root密码?

**一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
![img](https://img-blog.csdnimg.cn/img_convert/225ddf3a32c78d2c5f66e6bf64fa81cc.jpeg)

**40道nginx面试题**
*   **77道redis面试题**
*   **28道zookeeper**

**总计 1000+ 道面试题, 内容 又全含金量又高**

*   **174道运维工程师面试题**

> 1、什么是运维?

> 2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

> 3、现在给你三百台服务器,你怎么对他们进行管理?

> 4、简述raid0 raid1raid5二种工作模式的工作原理及特点

> 5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

> 6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

> 7、Tomcat和Resin有什么区别,工作中你怎么选择?

> 8、什么是中间件?什么是jdk?

> 9、讲述一下Tomcat8005、8009、8080三个端口的含义?

> 10、什么叫CDN?

> 11、什么叫网站灰度发布?

> 12、简述DNS进行域名解析的过程?

> 13、RabbitMQ是什么东西?

> 14、讲一下Keepalived的工作原理?

> 15、讲述一下LVS三种模式的工作过程?

> 16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

> 17、如何重置mysql root密码?

**一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
[外链图片转存中...(img-Ad70KPAY-1712886826090)]

  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值