当前位置:   article > 正文

Dji-MSDKv5开发 记录贴【2】示例程序解读_dji示例项目

dji示例项目

事先声明——JR只是一个小白!

本帖子只是一个没有任何安卓开发经验的小白记录他在尝试进行无人机开发时候遇到的问题,和最终解决办法的记录!不能够当作教程仅供参考!


介绍-在本期博客中准备进行的事情

需要完成通过自制程序操控Mavic 3T完成红外自动路线生成和拍照的工作。

由于JR真的是小白,没有任何SDK开发和Android的经验,所以需要从构建一个Android项目开始,学习Kotlin代码开始,一步一步理解官方提供的示例项目,并最终实现自己尝试构建程序!

在这一期记录贴中,我的最终目标是能够顺利解读主页的启动调用顺序,即示例程序主要功能调用链,以及学会如何使用官方的示例程序来帮助我调试参数!

图中为我们在上一个文档中成功运行的官方MSDK V5的示例程序,如何配置运行环境、打开这个程序的过程我都记录在了上一期!请参考:

Dji-MSDKv5开发 记录贴【1】准备运行环境_SOP-JR!的博客-CSDN博客


Android开发基础准备

在进行开发和代码阅读前,需要先理解一个Android程序构建运行的方式。谷歌官方的Android学习文档和优秀的国内Android开发教程都可以作为参考,虽然开发工具的版本迭代非常快,但基础的架构和调用方式随时间变化不大。

而且在官方的V5中有别于上一代,这一代示例程序的开发主要是用kotlin语言完成的。所以我需要具备基础的阅读kotlin代码的能力;

在Java中可以像调用普通Java类一样调用Kotlin类和函数。Kotlin中的扩展函数等功能也可以在Java中使用,所以不影响我们自己的开发,只需要注意一些涉及到可空/非空的参数传递就可以了(两个语言的一点区别,在kotlin中的变量是默认不能为空的,但在Java中可以)

Android开发基础学习(Java语言)

JR(作者自称)是根据以前寻找到的经验贴找到的开发教程,另外JR自己做过一些前端所以了解xml设定和调用的一些逻辑,学起来不算费力。下面放一些好用的学习链接,包括谷歌官方提供的学习文档(有中文),以及b站的优秀教学视频

Android 开发者基础知识  |  培训课程  |  Android Developers (google.cn)

更新:谷歌官方已经停止维护Java开发App的教程,而开始推荐使用Kotlin进行Android开发。

【2021最新版】Android从零开始学习入门篇,涵大量案例实战 android studio_哔哩哔哩_bilibili

注:这是JR自己在学习过程中参考的视频,版本可能已经过时,b站很多安卓开发视频都值得参考。

以下是根据hello Android总结出来的一个基础的安卓项目的调用顺序:

AndroidManifest.xml是清单文件,可以看作是从这里配置了整个应用程序,需要获取的权限、使用的组件和所有交互界面都需要在这里进行注册。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. package="com.dji.exampleapp">
  5. <application
  6. android:allowBackup="true"
  7. android:dataExtractionRules="@xml/data_extraction_rules"
  8. android:fullBackupContent="@xml/backup_rules"
  9. android:icon="@mipmap/ic_launcher"
  10. android:label="@string/app_name"
  11. android:roundIcon="@mipmap/ic_launcher_round"
  12. android:supportsRtl="true"
  13. android:theme="@style/Theme.ExampleApp"
  14. tools:targetApi="31">
  15. <activity
  16. android:name=".MainActivity"
  17. android:exported="true"
  18. android:label="@string/app_name"
  19. android:theme="@style/Theme.ExampleApp">
  20. <intent-filter>
  21. <action android:name="android.intent.action.MAIN" />
  22. <category android:name="android.intent.category.LAUNCHER" />
  23. </intent-filter>
  24. </activity>
  25. </application>
  26. </manifest>

主要标签有三层,<manifast>标签在最外面,是清单文件的根元素;(权限申请和硬件兼容信息也在这个标签下)

<application>标签代表了我们的应用,配置应用的全局属性和行为;

<activity>标签是一个活动(Activity),通常代表一个屏幕或用户界面的一个部分。

<intent-filter> 用于声明活动响应的意图(Intent)。这些意图定义了触发活动启动的条件,如 MAINLAUNCHER 意图用于定义应用程序的入口点。所以在这里加这个标签的意思就是,这个页面将会变成打开应用以后第一个显示的页面。

所以我们这么找对应的主页设置文件:从manifast的属性"package"中找到包名,从activiti中的属性"name"中找到活动的类名"MainActivity".我们按住Ctrl+左键,就可以进入到对应的主页设置文件了。

如图,我们打开的就是实现这个活动的类定义文件了。可以看到后缀名为.kt,代表实现语言是kotlin;而在这里,我们可以继续阅读这个主页的活动逻辑,以及它设置页面内容的方式。

但这里是通过Compose来定义了页面的内容,这就意味着页面样式是通过kotlin代码来实现的,而不是通过导入一个xml布局方式来实现。这样可以简化代码的调用层次,否则将会多一层(再调用一个main_activity.xml来声明主页的布局,设置样式)

很遗憾这个项目是JR为了分析项目结构随手创建的项目,没能创建一个Java调用的示例简单项目;读者若想详细了解的话,通过上面提供的教程前几集就可以简单了解了。

关于资源文件和gradle配置文件,这里先省略,如果有小伙伴感兴趣我可以在深入学习之后再出一个专栏讲Android开发的基础知识

所以我们可以通过认识项目的组织结构来识别出程序中的调用顺序:

在AndroidManifest.xml中注册->在MainActivity中定义活动逻辑->对应的页面内容设置xml文件

这样之后我们就方便理解Kotlin编写的无人机示例项目,以及项目中的具体调用步骤了!

Kotlin语言基础学习

由于示例程序是使用Kt语言编写的,所以读码不仅要学习java基础的Android开发,还要学习Kotlin语言。我自己把它理解为一个Java的进阶版,Kotlin 可以与 Java 无缝互操作,可以在同一个项目中同时使用 Kotlin 和 Java 编写的代码,甚至可以在 Kotlin 中调用 Java 库。

下面有一些推荐的kotlin教程:

https://www.runoob.com/kotlin/kotlin-tutorial.html

Kotlin 编程简介  |  Android Basics Compose - First Android app  |  Android Developers (google.cn)

分别是菜鸟教程中的kotlin教程,还有谷歌官方提供的教程。通过选择一种教程,我学习到了在kotlin中和java不同的方法调用和继承、lamda表达式、变量的建立和使用等部分,能够理解项目中的kotlin代码。


初步理解官方示例程序

清单文件AndroidManifast.xml

进行过了前两步的学习,现在我们可以理解官方示例项目给我们提供的参考了。我们首先从整个项目的配置文件AndroidManifest开始

可以看到,相比于简单的项目结构,示例文件中添加了对权限的定义(<users-permission>标签),还有声明应用程序对 USB 主机模式和 USB 附件模式的要求(<users-feature>标签)。

在我们前面已经熟悉过的application标签中,我们可以找到程序的主页面:

  1. <activity
  2. android:name="dji.sampleV5.aircraft.DJIAircraftMainActivity"
  3. android:theme="@style/full_screen_theme"
  4. android:screenOrientation="landscape"
  5. android:launchMode="singleTask"
  6. android:configChanges="orientation|screenSize|keyboardHidden"
  7. android:exported="true">
  8. <intent-filter>
  9. <action android:name="android.intent.action.MAIN" />
  10. <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
  11. />
  12. <category android:name="android.intent.category.LAUNCHER" />
  13. </intent-filter>
  14. <meta-data
  15. android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
  16. android:resource="@xml/accessory_filter" />
  17. </activity>

<meta-data>标签代表元数据,代表我们调用这个活动的时候传入的一些参数。通常可以存放许可或者一些API密钥、配置信息,包括我们之前从官网申请的API-KEY就是从外面application携带的元数据传入的

同样,可以看到这个页面通过<intent-filter>标签中的内容注册成为了启动页面和主页面。让我们点击活动的类名:

android:name="dji.sampleV5.aircraft.DJIAircraftMainActivity"

跳转到定义主页面的文件:

主页面实现类DJIAircraftMainActivity.kt

下面是我尝试理解之后加上注解的主页面类源代码:

  1. //实现抽象类MainActivity
  2. class DJIAircraftMainActivity : DJIMainActivity() {
  3. //配置用户界面和全局偏好设置
  4. override fun prepareUxActivity() {
  5. //处理用户界面(UX)相关的共享偏好设置或配置。这可以包括诸如用户界面的外观和行为设置等内容。
  6. UxSharedPreferencesUtil.initialize(this)
  7. //管理应用程序的全局配置和用户首选项。
  8. GlobalPreferencesManager.initialize(DefaultGlobalPreferences(this))
  9. //用于初始化地理信息服务或管理
  10. GeoidManager.getInstance().init(this)
  11. //应用程序中的一个界面或屏幕,用于显示默认的飞行器(Aircraft)布局
  12. enableDefaultLayout(DefaultLayoutActivity::class.java)
  13. //启用了名为 WidgetsActivity 的小部件列表。用于显示可用的小部件或工具
  14. enableWidgetList(WidgetsActivity::class.java)
  15. }
  16. //配置测试工具和功能
  17. override fun prepareTestingToolsActivity() {
  18. // 启用测试工具
  19. enableTestingTools(AircraftTestingToolsActivity::class.java)
  20. }
  21. }

可以看到启动页只是重写了配置初始化和配置测试工具的两个方法,而主页的更多具体功能被封装到父类中。同时,里面调用了很多父类编写好的各种方法,来启动各种服务功能。接下来,通过阅读父类DJIMainActivity的源代码,我们继续深入理解项目的组织方式。

主页面父类DJIMainActivity.kt

该类继承自AppCombatActivity类,是google为安卓开发提供的一个比较强大的应用程序基类型,提供了丰富的功能和生命周期管理,使得开发者可以更轻松地创建功能丰富、交互性强的 Android 应用程序。

下面是类中变量定义部分源代码以及JR在理解过程中做的一些标注:

  1. //可能用于在日志中标识这个活动的标签
  2. val tag: String = LogUtils.getTag(this)
  3. //一个存储权限字符串的数组列表
  4. private val permissionArray = arrayListOf(
  5. Manifest.permission.RECORD_AUDIO,
  6. Manifest.permission.KILL_BACKGROUND_PROCESSES,
  7. Manifest.permission.ACCESS_COARSE_LOCATION,
  8. Manifest.permission.ACCESS_FINE_LOCATION,
  9. )
  10. //初始化块,在创建 DJIMainActivity 的实例时执行
  11. init {
  12. /* 根据 Android 版本(Build.VERSION.SDK_INT)的不同,可能会
  13. * 将一些额外的权限添加到 permissionArray 中*/
  14. permissionArray.apply {
  15. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
  16. add(Manifest.permission.READ_MEDIA_IMAGES)
  17. add(Manifest.permission.READ_MEDIA_VIDEO)
  18. add(Manifest.permission.READ_MEDIA_AUDIO)
  19. } else {
  20. add(Manifest.permission.READ_EXTERNAL_STORAGE)
  21. add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
  22. }
  23. }
  24. }
  25. //可能是用于管理应用程序数据和状态的 ViewModel 对象
  26. private val baseMainActivityVm: BaseMainActivityVm by viewModels()
  27. private val msdkInfoVm: MSDKInfoVm by viewModels()
  28. private val msdkManagerVM: MSDKManagerVM by globalViewModels()
  29. // 可能用于处理消息和线程
  30. private val handler: Handler = Handler(Looper.getMainLooper())
  31. // 可能用于管理 RxJava 可观察对象的资源释放
  32. private val disposable = CompositeDisposable()

在变量定义之后,我们可以找到刚才在子类中进行实现的抽象方法,和创建主页的OnCreate方法,其中就声明了定义主页布局的xml文件名:

  1. //为了准备用户界面活动(UX activity)
  2. abstract fun prepareUxActivity()
  3. //为了准备测试工具活动(Testing Tools activity)
  4. abstract fun prepareTestingToolsActivity()
  5. //用于处理活动的创建
  6. override fun onCreate(savedInstanceState: Bundle?) {
  7. //调用了父类的 onCreate() 方法以执行标准的活动创建逻辑
  8. super.onCreate(savedInstanceState)
  9. //然后通过 setContentView() 方法设置了活动的布局
  10. setContentView(R.layout.activity_main)
  11. //设置了系统 UI 的可见性标志,以实现隐藏导航栏、实现全屏沉浸模式等。
  12. window.decorView.apply {
  13. systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
  14. View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
  15. View.SYSTEM_UI_FLAG_FULLSCREEN or
  16. View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  17. }
  18. //这些方法可能用于初始化应用程序的某些部分或执行权限检查和请求
  19. initMSDKInfoView()
  20. observeSDKManagerStatus()
  21. checkPermissionAndRequest()
  22. }

其实活动布局的xml文件就在这个位置,我们接下来点进去就可以找到了。但我们先读完主类中定义的方法,再去看不迟。

  1. //处理权限请求的结果
  2. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  3. //当用户响应权限请求时,系统将调用这个方法,并传递请求码
  4. super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  5. if (checkPermission()) {
  6. //执行权限获得后的操作, 最终调用了子类实现的prepareTestingToolsActivity方法
  7. handleAfterPermissionPermitted()
  8. }
  9. }
  10. //处理活动恢复到前台可见状态时的逻辑
  11. override fun onResume() {
  12. //调用了父类的 onResume() 方法以执行标准的恢复逻辑
  13. super.onResume()
  14. if (checkPermission()) {
  15. //同上
  16. handleAfterPermissionPermitted()
  17. }
  18. }
  19. private fun handleAfterPermissionPermitted() {
  20. prepareTestingToolsActivity()
  21. }

这三个方法让我看到处理权限请求、恢复到前台之后的调用逻辑。最终返回到子类的启用测试工具的功能方法中。

接下来是前面OnCreate方法中最后调用的三个方法之一:InitMSDKInfoView

  1. //告诉工具忽略与文本国际化相关的警告
  2. @SuppressLint("SetTextI18n")
  3. private fun initMSDKInfoView() {
  4. //用于初始化 Toast 消息的显示,提供上下文this
  5. ToastUtils.init(this)
  6. // 这是一个 LiveData 的观察者(Observer)模式
  7. // msdkInfoVm 是一个 ViewModel 对象,msdkInfo 是其中的 LiveData 属性
  8. // 通过 observe 方法,该代码段监听 msdkInfo 属性的变化
  9. msdkInfoVm.msdkInfo.observe(this) {
  10. // 代码用于更新界面上的一些 TextView 元素,根据 msdkInfo 的数据来设置文本内容
  11. text_view_version.text = StringUtils.getResStr(R.string.sdk_version, it.SDKVersion + " " + it.buildVer)
  12. text_view_product_name.text = StringUtils.getResStr(R.string.product_name, it.productType.name)
  13. text_view_package_product_category.text = StringUtils.getResStr(R.string.package_product_category, it.packageProductCategory)
  14. text_view_is_debug.text = StringUtils.getResStr(R.string.is_sdk_debug, it.isDebug)
  15. text_core_info.text = it.coreInfo.toString()
  16. }
  17. // 类似前面的observe,这个代码段监听 sdkNews 属性的变化,baseMainActivityVm 是另一个 ViewModel 对象
  18. baseMainActivityVm.sdkNews.observe(this) {
  19. // 设置两个新闻条目(item_news_msdk 和 item_news_uxsdk)的标题、描述和日期
  20. item_news_msdk.setTitle(StringUtils.getResStr(it.title))
  21. item_news_msdk.setDescription(StringUtils.getResStr(it.description))
  22. item_news_msdk.setDate(it.date)
  23. item_news_uxsdk.setTitle(StringUtils.getResStr(it.title))
  24. item_news_uxsdk.setDescription(StringUtils.getResStr(it.description))
  25. item_news_uxsdk.setDate(it.date)
  26. }
  27. // ”SDK论坛“的点击事件监听器
  28. // 在这个代码块中,通过 Helper.startBrowser(this, ...) 启动了一个浏览器,并打开了网页的url
  29. icon_sdk_forum.setOnClickListener {
  30. Helper.startBrowser(this, StringUtils.getResStr(R.string.sdk_forum_url))
  31. }
  32. // “发布日志”点击逻辑;
  33. icon_release_node.setOnClickListener {
  34. Helper.startBrowser(this, StringUtils.getResStr(R.string.release_node_url))
  35. }
  36. // ”技术支持“点击逻辑
  37. icon_tech_support.setOnClickListener {
  38. Helper.startBrowser(this, StringUtils.getResStr(R.string.tech_support_url))
  39. }
  40. //设置 view_base_info 控件的点击事件监听器
  41. view_base_info.setOnClickListener {
  42. // 执行配对操作(左上角的产品信息状态栏,点击会开始对频)
  43. baseMainActivityVm.doPairing {
  44. ToastUtils.showToast(it)
  45. }
  46. }
  47. }

这段代码定义了很多主页上的行为逻辑。上面的观察方法是用来监听,更新显示的版本信息和通知内容的;而下面的则是监听点击事件,对下方三个按钮点击事件打开三个外部链接;对左上角板块的点击会触发对频操作,并显示对应状态的弹窗。

下一个是OnCreate函数末尾调用的三个函数中的第二个:observeSDKManagerStatus:

  1. //它用于观察 SDK 管理器的各种状态,并在状态发生变化时执行相应的操作
  2. private fun observeSDKManagerStatus() {
  3. // 使用 observe 方法来监听 msdkManagerVM 中的 lvRegisterState 属性的变化
  4. msdkManagerVM.lvRegisterState.observe(this) { resultPair ->
  5. val statusText: String?
  6. // 判断注册是否成功
  7. if (resultPair.first) {
  8. ToastUtils.showToast("Register Success")
  9. statusText = StringUtils.getResStr(this, R.string.registered)
  10. msdkInfoVm.initListener()
  11. // 显示注册成功信息,修改状态文本;初始化虚拟机监听;延迟5秒后
  12. handler.postDelayed({
  13. //子类实现的,准备用户活动界面;
  14. prepareUxActivity()
  15. }, 5000)
  16. } else {
  17. // 如果注册失败,会显示一个失败的 Toast 消息,并将 statusText 设置为 "unregistered"
  18. ToastUtils.showToast("Register Failure: ${resultPair.second}")
  19. statusText = StringUtils.getResStr(this, R.string.unregistered)
  20. }
  21. // 根据 statusText 更新界面上的文本内容
  22. text_view_registered.text = StringUtils.getResStr(R.string.registration_status, statusText)
  23. }
  24. // 产品和连接状态的信息发生变化
  25. msdkManagerVM.lvProductConnectionState.observe(this) { resultPair ->
  26. ToastUtils.showToast("Product: ${resultPair.second} ,ConnectionState: ${resultPair.first}")
  27. }
  28. // 产品 ID 的信息发生变化
  29. msdkManagerVM.lvProductChanges.observe(this) { productId ->
  30. ToastUtils.showToast("Product: $productId Changed")
  31. }
  32. // 初始化过程发生变化
  33. msdkManagerVM.lvInitProcess.observe(this) { processPair ->
  34. ToastUtils.showToast("Init Process event: ${processPair.first.name}")
  35. }
  36. // 数据库下载进度的信息,当前进度,总下载量
  37. msdkManagerVM.lvDBDownloadProgress.observe(this) { resultPair ->
  38. ToastUtils.showToast("Database Download Progress current: ${resultPair.first}, total: ${resultPair.second}")
  39. }
  40. }

而三个方法的最后一个是“检查权限并请求”。它的实现通过几步来完成。

  1. // 检查权限并在必要时请求权限
  2. private fun checkPermissionAndRequest() {
  3. if (!checkPermission()) {
  4. requestPermission()
  5. }
  6. }
  7. // 遍历 permissionArray 数组中的权限,检查每个权限是否已被授予
  8. private fun checkPermission(): Boolean {
  9. for (i in permissionArray.indices) {
  10. if (!PermissionUtil.isPermissionGranted(this, permissionArray[i])) {
  11. return false
  12. }
  13. }
  14. return true
  15. }
  16. // requestPermissionLauncher 是一个 ActivityResultLauncher 对象,用于处理权限请求的结果
  17. // registerForActivityResult(...) 是一个函数,用于注册一个用于处理活动结果的回调函数
  18. private val requestPermissionLauncher = registerForActivityResult(
  19. //创建了一个权限请求的合同(contract)。这个合同指定了请求多个权限的操作
  20. ActivityResultContracts.RequestMultiplePermissions()
  21. ) { result ->
  22. result?.entries?.forEach {
  23. //首先使用安全调用运算符 ?. 来检查 result 是否为 null,以避免空指针异常
  24. // 通过 result?.entries 来获取权限请求结果中的权限条目(键值对的集合),这里的 entries 返回一个 Map
  25. // 在权限条目的集合中,对每个条目进行迭代
  26. if (it.value == false) {
  27. // 重新请求权限
  28. requestPermission()
  29. return@forEach
  30. }
  31. }
  32. }
  33. // 这个函数用于请求权限
  34. private fun requestPermission() {
  35. requestPermissionLauncher.launch(permissionArray.toArray(arrayOf()))
  36. }

可以看到三个函数和一个变量,其中比较难理解的也是这个变量的建立过程。在创建变量的时候会向用户请求变量;在创建之后还会触发回调函数,如果没有请求成功,还会再次进行请求。确保了在用户选择权限后,才会执行回调函数来处理结果。

接下来是一些用于处理按钮触发事件还有变量销毁的方法了。

  1. // 用于启用默认布局
  2. fun <T> enableDefaultLayout(cl: Class<T>) {
  3. enableShowCaseButton(default_layout_button, cl)
  4. }
  5. // 用于启用小部件列表
  6. fun <T> enableWidgetList(cl: Class<T>) {
  7. enableShowCaseButton(widget_list_button, cl)
  8. }
  9. // 用于启用测试工具
  10. fun <T> enableTestingTools(cl: Class<T>) {
  11. enableShowCaseButton(testing_tool_button, cl)
  12. }
  13. // 用于启用具体的按钮,并设置按钮的点击事件
  14. private fun <T> enableShowCaseButton(view: View, cl: Class<T>) {
  15. view.isEnabled = true
  16. view.setOnClickListener {
  17. Intent(this, cl).also {
  18. startActivity(it)
  19. }
  20. }
  21. }
  22. // 用于在活动销毁时执行清理操作
  23. override fun onDestroy() {
  24. super.onDestroy()
  25. disposable.dispose()
  26. ToastUtils.destroy()
  27. }

看完了以上的代码,我们可以做一个简单总结:DJIMainActivity.kt 的主要功能包括权限管理、启用不同功能的按钮、初始化页面和清理操作、观察 SDK 状态以及处理权限请求结果。这些功能一起帮助管理应用程序的主要操作和用户交互。

接下来我们看一下xml文件是如何设置主页面的。

主页面布局文件activity_main.xml

这里就是主页布局的xml文件,当光标移动到组件代码中试右侧会标记出对应的组件在哪里。联系前面的代码,就可以大概了解这个页面是如何运行起来的了。

通过阅读和理解示例项目的构建方式,可以看到示例代码在初始化程序过程中进行的操作,在后续我们进行自己操作的时候,就可以更全面地进行编写了。

但目前还没有涉及到关于Key的创建、飞机和遥控器通信的代码,我们还要继续进行分析和阅读。


第二篇的后记

由于JR对实际Android项目开发的不了解,需要在代码和项目结构的理解上面花很多功夫。在此留下记录,也希望能提供给大家参考。

接下来还需要学习如何与无人机构建连接,学习无人机在起飞前的所有安全确认函数,确保自制程序的安全性,最大程度保护无人机。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/413431
推荐阅读
相关标签
  

闽ICP备14008679号