Cloud Firestore Android 代码实验室

1. 概述

目标

在此 Codelab 中,您将在 Cloud Firestore 支持的 Android 上构建一个餐厅推荐应用。你将学到如何:

  • 从 Android 应用读取数据并将其写入 Firestore
  • 实时监听 Firestore 数据的变化
  • 使用 Firebase 身份验证和安全规则来保护 Firestore 数据
  • 编写复杂的 Firestore 查询

先决条件

在开始此 Codelab 之前,请确保您已:

  • Android Studio Flamingo或更高版本
  • API 19或更高版本的 Android 模拟器
  • Node.js 版本16或更高版本
  • Java 版本17或更高版本

2. 创建 Firebase 项目

  1. 使用您的 Google 帐户登录Firebase 控制台
  2. Firebase 控制台中,单击添加项目
  3. 如下面的屏幕截图所示,输入 Firebase 项目的名称(例如“Friendly Eats”),然后点击“继续”

9d2f625aebcab6af.png

  1. 系统可能会要求您启用 Google Analytics,就本 Codelab 而言,您的选择并不重要。
  2. 大约一分钟后,您的 Firebase 项目将准备就绪。单击继续

3. 设置示例项目

下载代码

运行以下命令来克隆此 Codelab 的示例代码。这将在您的计算机上创建一个名为friendlyeats-android文件夹:

$ git clone https://github.com/firebase/friendlyeats-android

如果你的机器上没有git,也可以直接从GitHub下载代码。

添加 Firebase 配置

  1. Firebase 控制台中,选择左侧导航栏中的“项目概述” 。单击Android按钮选择平台。当提示输入包名称时,请使用com.google.firebase.example.fireeats

73d151ed16016421.png

  1. 单击“注册应用程序”并按照说明下载google-services.json文件,并将其移至刚刚下载的代码的app/文件夹中。然后单击“下一步”

导入项目

打开 Android Studio。单击“文件” > “新建” > “导入项目” ,然后选择“Friendlyeats-android”文件夹。

4. 设置 Firebase 模拟器

在此 Codelab 中,您将使用Firebase Emulator Suite在本地模拟 Cloud Firestore 和其他 Firebase 服务。这提供了一个安全、快速且免费的本地开发环境来构建您的应用程序。

安装 Firebase CLI

首先,您需要安装Firebase CLI 。如果您使用的是 macOS 或 Linux,则可以运行以下 cURL 命令:

curl -sL https://firebase.tools | bash

如果您使用的是 Windows,请阅读安装说明以获取独立的二进制文件或通过npm安装。

安装 CLI 后,运行firebase --version应报告9.0.0或更高版本:

$ firebase --version
9.0.0

登录

运行firebase login将 CLI 连接到您的 Google 帐户。这将打开一个新的浏览器窗口以完成登录过程。确保选择您之前创建 Firebase 项目时使用的相同帐户。

friendlyeats-android文件夹中运行firebase use --add将本地项目连接到Firebase 项目。按照提示选择您之前创建的项目,如果要求选择别名,请输入default

5. 运行应用程序

现在是时候首次运行 Firebase Emulator Suite 和 Friendship Android 应用了。

运行模拟器

在终端的friendlyeats-android目录中运行firebase emulators:start来启动Firebase模拟器。您应该看到如下日志:

$ firebase emulators:start
i  emulators: Starting emulators: auth, firestore
i  firestore: Firestore Emulator logging to firestore-debug.log
i  ui: Emulator UI logging to ui-debug.log

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

现在,您的计算机上运行了一个完整的本地开发环境!确保在 Codelab 的其余部分中保持此命令运行,您的 Android 应用程序需要连接到模拟器。

将应用程序连接到模拟器

在 Android Studio 中打开文件util/FirestoreInitializer.ktutil/AuthInitializer.kt 。这些文件包含在应用程序启动时将 Firebase SDK 连接到计算机上运行的本地模拟器的逻辑。

FirestoreInitializer类的create()方法上,检查这段代码:

    // Use emulators only in debug builds
    if (BuildConfig.DEBUG) {
        firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
    }

我们使用BuildConfig来确保仅当应用程序在debug模式下运行时才连接到模拟器。当我们在release模式下编译应用程序时,此条件将为 false。

我们可以看到它使用useEmulator(host, port)方法将 Firebase SDK 连接到本地 Firestore 模拟器。在整个应用程序中,我们将使用FirebaseUtil.getFirestore()访问FirebaseFirestore的此实例,以便确保在debug模式下运行时始终连接到 Firestore 模拟器。

运行应用程序

如果您已正确添加google-services.json文件,则该项目现在应该可以编译。在 Android Studio 中,单击“Build” > “Rebuild Project”并确保不存在任何剩余错误。

在 Android Studio 中 在 Android 模拟器上运行应用程序。首先,您将看到“登录”屏幕。您可以使用任何电子邮件和密码登录该应用程序。此登录过程正在连接到 Firebase 身份验证模拟器,因此不会传输真正的凭据。

现在,通过在 Web 浏览器中导航到http://localhost:4000打开模拟器 UI。然后单击“身份验证”选项卡,您应该会看到刚刚创建的帐户:

Firebase 身份验证模拟器

完成登录过程后,您应该会看到应用程序主屏幕:

de06424023ffb4b9.png

很快我们将添加一些数据来填充主屏幕。

6. 将数据写入Firestore

在本节中,我们将向 Firestore 写入一些数据,以便可以填充当前空的主屏幕。

我们应用程序中的主要模型对象是一家餐厅(请参阅model/Restaurant.kt )。 Firestore 数据分为文档、集合和子集合。我们将每个餐厅作为文档存储在名为"restaurants"的顶级集合中。要了解有关 Firestore 数据模型的更多信息,请阅读文档中有关文档和集合的信息。

出于演示目的,我们将在应用程序中添加功能,以便在单击溢出菜单中的“添加随机项目”按钮时创建十个随机餐厅。打开文件MainFragment.kt并将onAddItemsClicked()方法中的内容替换为:

    private fun onAddItemsClicked() {
        val restaurantsRef = firestore.collection("restaurants")
        for (i in 0..9) {
            // Create random restaurant / ratings
            val randomRestaurant = RestaurantUtil.getRandom(requireContext())

            // Add restaurant
            restaurantsRef.add(randomRestaurant)
        }
    }

对于上面的代码,有一些重要的事情需要注意:

  • 我们首先获取"restaurants"集合的参考。添加文档时会隐式创建集合,因此无需在写入数据之前创建集合。
  • 可以使用 Kotlin 数据类创建文档,我们用它来创建每个 Restaurant 文档。
  • add()方法使用自动生成的 ID 将文档添加到集合中,因此我们不需要为每个餐厅指定唯一的 ID。

现在再次运行应用程序,然后单击溢出菜单(右上角)中的“添加随机项”按钮以调用您刚刚编写的代码:

95691e9b71ba55e3.png

现在,通过在 Web 浏览器中导航到http://localhost:4000打开模拟器 UI。然后单击Firestore选项卡,您应该会看到刚刚添加的数据:

Firebase 身份验证模拟器

此数据 100% 位于您的计算机本地。事实上,您的真实项目甚至还不包含 Firestore 数据库!这意味着可以安全地尝试修改和删除这些数据,不会产生任何后果。

恭喜,您刚刚将数据写入 Firestore!在下一步中,我们将学习如何在应用程序中显示这些数据。

7. 显示 Firestore 中的数据

在此步骤中,我们将学习如何从 Firestore 检索数据并将其显示在我们的应用程序中。从 Firestore 读取数据的第一步是创建Query 。打开文件MainFragment.kt并将以下代码添加到onViewCreated()方法的开头:

        // Firestore
        firestore = Firebase.firestore

        // Get the 50 highest rated restaurants
        query = firestore.collection("restaurants")
            .orderBy("avgRating", Query.Direction.DESCENDING)
            .limit(LIMIT.toLong())

现在我们想要监听查询,以便我们获得所有匹配的文档并实时收到未来更新的通知。因为我们的最终目标是将这些数据绑定到RecyclerView ,所以我们需要创建一个RecyclerView.Adapter类来监听数据。

打开FirestoreAdapter类,该类已部分实现。首先,让适配器实现EventListener并定义onEvent函数,以便它可以接收 Firestore 查询的更新:

abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query?) :
        RecyclerView.Adapter<VH>(),
        EventListener<QuerySnapshot> { // Add this implements
    
    // ...

    // Add this method
    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
        
        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        // TODO: handle document added
                    }
                    DocumentChange.Type.MODIFIED -> {
                        // TODO: handle document changed
                    }
                    DocumentChange.Type.REMOVED -> {
                        // TODO: handle document removed
                    }
                }
            }
        }

        onDataChanged()
    }
    
    // ...
}

初始加载时,侦听器将为每个新文档接收一个ADDED事件。随着查询结果集随时间的变化,侦听器将收到更多包含更改的事件。现在让我们完成监听器的实现。首先添加三个新方法: onDocumentAddedonDocumentModifiedonDocumentRemoved

    private fun onDocumentAdded(change: DocumentChange) {
        snapshots.add(change.newIndex, change.document)
        notifyItemInserted(change.newIndex)
    }

    private fun onDocumentModified(change: DocumentChange) {
        if (change.oldIndex == change.newIndex) {
            // Item changed but remained in same position
            snapshots[change.oldIndex] = change.document
            notifyItemChanged(change.oldIndex)
        } else {
            // Item changed and changed position
            snapshots.removeAt(change.oldIndex)
            snapshots.add(change.newIndex, change.document)
            notifyItemMoved(change.oldIndex, change.newIndex)
        }
    }

    private fun onDocumentRemoved(change: DocumentChange) {
        snapshots.removeAt(change.oldIndex)
        notifyItemRemoved(change.oldIndex)
    }

然后从onEvent调用这些新方法:

    override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {

        // Handle errors
        if (e != null) {
            Log.w(TAG, "onEvent:error", e)
            return
        }

        // Dispatch the event
        if (documentSnapshots != null) {
            for (change in documentSnapshots.documentChanges) {
                // snapshot of the changed document
                when (change.type) {
                    DocumentChange.Type.ADDED -> {
                        onDocumentAdded(change) // Add this line
                    }
                    DocumentChange.Type.MODIFIED -> {
                        onDocumentModified(change) // Add this line
                    }
                    DocumentChange.Type.REMOVED -> {
                        onDocumentRemoved(change) // Add this line
                    }
                }
            }
        }

        onDataChanged()
    }

最后实现startListening()方法来附加监听器:

    fun startListening() {
        if (registration == null) {
            registration = query.addSnapshotListener(this)
        }
    }

现在,应用程序已完全配置为从 Firestore 读取数据。再次运行应用程序,您应该会看到您在上一步中添加的餐厅:

9e45f40faefce5d0.png

现在返回浏览器中的模拟器 UI 并编辑其中一家餐厅名称。您应该几乎立即在应用程序中看到它的变化!

8. 排序和过滤数据

该应用程序当前显示整个集合中评分最高的餐厅,但在真正的餐厅应用程序中,用户希望对数据进行排序和过滤。例如,该应用程序应该能够显示“费城顶级海鲜餐厅”或“最便宜的披萨”。

单击应用程序顶部的白色栏会弹出一个过滤器对话框。在本部分中,我们将使用 Firestore 查询来使该对话框正常工作:

67898572a35672a5.png

让我们编辑MainFragment.ktonFilter()方法。此方法接受一个Filters对象,该对象是我们创建的一个辅助对象,用于捕获过滤器对话框的输出。我们将更改此方法以从过滤器构造查询:

    override fun onFilter(filters: Filters) {
        // Construct query basic query
        var query: Query = firestore.collection("restaurants")

        // Category (equality filter)
        if (filters.hasCategory()) {
            query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category)
        }

        // City (equality filter)
        if (filters.hasCity()) {
            query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city)
        }

        // Price (equality filter)
        if (filters.hasPrice()) {
            query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price)
        }

        // Sort by (orderBy with direction)
        if (filters.hasSortBy()) {
            query = query.orderBy(filters.sortBy.toString(), filters.sortDirection)
        }

        // Limit items
        query = query.limit(LIMIT.toLong())

        // Update the query
        adapter.setQuery(query)

        // Set header
        binding.textCurrentSearch.text = HtmlCompat.fromHtml(
            filters.getSearchDescription(requireContext()),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
        binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())

        // Save filters
        viewModel.filters = filters
    }

在上面的代码片段中,我们通过附加whereorderBy子句来匹配给定的过滤器来构建一个Query对象。

再次运行应用程序并选择以下过滤器以显示最受欢迎的低价餐厅:

7a67a8a400c80c50.png

您现在应该看到一个经过筛选的餐厅列表,其中仅包含低价选项:

a670188398c3c59.png

如果您已经完成了这一步,那么您现在已经在 Firestore 上构建了一个功能齐全的餐厅推荐查看应用程序!您现在可以实时排序和过滤餐厅。在接下来的几节中,我们将为餐厅添加评论并向应用程序添加安全规则。

9. 在子集合中组织数据

在本节中,我们将向应用程序添加评级,以便用户可以评论他们最喜欢(或最不喜欢)的餐厅。

集合和子集合

到目前为止,我们已将所有餐厅数据存储在名为“餐厅”的顶级集合中。当用户对餐厅进行评分时,我们希望向餐厅添加一个新的Rating对象。对于此任务,我们将使用子集合。您可以将子集合视为附加到文档的集合。因此,每个餐厅文档都会有一个充满评级文档的评级子集合。子集合有助于组织数据,而不会导致文档膨胀或需要复杂的查询。

要访问子集合,请在父文档上调用.collection()

val subRef = firestore.collection("restaurants")
        .document("abc123")
        .collection("ratings")

您可以像访问顶级集合一样访问和查询子集合,没有大小限制或性能变化。您可以在此处阅读有关 Firestore 数据模型的更多信息。

在事务中写入数据

Rating添加到正确的子集合只需要调用.add() ,但我们还需要更新Restaurant对象的平均评级和评级数量以反映新数据。如果我们使用单独的操作来进行这两个更改,则存在许多竞争条件,可能会导致数据过时或不正确。

为了确保正确添加评级,我们将使用事务来为餐厅添加评级。此事务将执行一些操作:

  • 读取餐厅的当前评级并计算新的评级
  • 将评级添加到子集合
  • 更新餐厅的平均评分和评分数

打开RestaurantDetailFragment.kt并实现addRating函数:

    private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task<Void> {
        // Create reference for new rating, for use inside the transaction
        val ratingRef = restaurantRef.collection("ratings").document()

        // In a transaction, add the new rating and update the aggregate totals
        return firestore.runTransaction { transaction ->
            val restaurant = transaction.get(restaurantRef).toObject<Restaurant>()
                ?: throw Exception("Restaurant not found at ${restaurantRef.path}")

            // Compute new number of ratings
            val newNumRatings = restaurant.numRatings + 1

            // Compute new average rating
            val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
            val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings

            // Set new restaurant info
            restaurant.numRatings = newNumRatings
            restaurant.avgRating = newAvgRating

            // Commit to Firestore
            transaction.set(restaurantRef, restaurant)
            transaction.set(ratingRef, rating)

            null
        }
    }

addRating()函数返回代表整个交易的Task 。在onRating()函数中,侦听器被添加到任务中以响应事务的结果。

现在再次运行应用程序并单击其中一家餐厅,这将显示餐厅详细信息屏幕。单击+按钮开始添加评论。通过选择一些星星并输入一些文字来添加评论。

78fa16cdf8ef435a.png

点击“提交”将开始交易。交易完成后,您将看到下面显示的您的评论以及餐厅评论计数的更新:

f9e670f40bd615b0.png

恭喜!您现在拥有一个基于 Cloud Firestore 构建的社交本地移动餐厅评论应用。我听说这些天很受欢迎。

10.保护您的数据

到目前为止我们还没有考虑这个应用程序的安全性。我们怎么知道用户只能读写正确的自己的数据呢? Firestore 数据库由名为安全规则 的配置文件保护。

打开firestore.rules文件,您应该看到以下内容:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

让我们更改这些规则以防止不需要的数据访问或更改,打开firestore.rules文件并将内容替换为以下内容:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Determine if the value of the field "key" is the same
    // before and after the request.
    function isUnchanged(key) {
      return (key in resource.data)
        && (key in request.resource.data)
        && (resource.data[key] == request.resource.data[key]);
    }

    // Restaurants
    match /restaurants/{restaurantId} {
      // Any signed-in user can read
      allow read: if request.auth != null;

      // Any signed-in user can create
      // WARNING: this rule is for demo purposes only!
      allow create: if request.auth != null;

      // Updates are allowed if no fields are added and name is unchanged
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys())
                    && isUnchanged("name");

      // Deletes are not allowed.
      // Note: this is the default, there is no need to explicitly state this.
      allow delete: if false;

      // Ratings
      match /ratings/{ratingId} {
        // Any signed-in user can read
        allow read: if request.auth != null;

        // Any signed-in user can create if their uid matches the document
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;

        // Deletes and updates are not allowed (default)
        allow update, delete: if false;
      }
    }
  }
}

这些规则限制访问以确保客户端仅进行安全更改。例如,对餐厅文档的更新只能更改评级,而不能更改名称或任何其他不可变数据。仅当用户 ID 与登录用户匹配时才能创建评级,这可以防止欺骗。

要了解有关安全规则的更多信息,请访问文档

11. 结论

您现在已经在 Firestore 之上创建了一个功能齐全的应用程序。您了解了最重要的 Firestore 功能,包括:

  • 文件和收藏
  • 读取和写入数据
  • 使用查询进行排序和过滤
  • 子集合
  • 交易

了解更多

要继续了解 Firestore,可以从以下一些不错的起点开始:

此 Codelab 中的餐厅应用程序基于“Friendly Eats”示例应用程序。您可以在此处浏览该应用程序的源代码。

可选:部署到生产环境

到目前为止,该应用程序仅使用了 Firebase Emulator Suite。如果您想了解如何将此应用部署到真正的 Firebase 项目,请继续执行下一步。

12.(可选)部署您的应用程序

到目前为止,这个应用程序完全是本地的,所有数据都包含在 Firebase Emulator Suite 中。在本部分中,您将了解如何配置 Firebase 项目,以便该应用程序能够在生产环境中运行。

Firebase 身份验证

在 Firebase 控制台中,转到Authentication部分,然后单击Get started 。导航到“登录方法”选项卡,然后从本机提供商中选择“电子邮件/密码”选项。

启用电子邮件/密码登录方法并单击保存

登录提供商.png

火库

创建数据库

导航到控制台的Firestore Database部分,然后单击Create Database

  1. 当提示有关安全规则时,选择以生产模式启动,我们将很快更新这些规则。
  2. 选择您想要用于您的应用程序的数据库位置。请注意,选择数据库位置是一个永久性决定,要更改它,您将必须创建一个新项目。有关选择项目位置的更多信息,请参阅文档

部署规则

要部署您之前编写的安全规则,请在 codelab 目录中运行以下命令:

$ firebase deploy --only firestore:rules

这会将firestore.rules的内容部署到您的项目,您可以通过导航到控制台中的“规则”选项卡来确认。

部署索引

Friendship 应用程序具有复杂的排序和过滤功能,需要大量自定义复合索引。这些可以在 Firebase 控制台中手动创建,但在firestore.indexes.json文件中编写它们的定义并使用 Firebase CLI 部署它们会更简单。

如果打开firestore.indexes.json文件,您将看到已提供所需的索引:

{
  "indexes": [
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "avgRating", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "price", "mode": "ASCENDING" },
        { "fieldPath": "numRatings", "mode": "DESCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "city", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    },
    {
      "collectionId": "restaurants",
      "fields": [
        { "fieldPath": "category", "mode": "ASCENDING" },
        { "fieldPath": "price", "mode": "ASCENDING" }
      ]
    }
  ],
  "fieldOverrides": []
}

要部署这些索引,请运行以下命令:

$ firebase deploy --only firestore:indexes

请注意,索引创建不是即时的,您可以在 Firebase 控制台中监控进度。

配置应用程序

util/FirestoreInitializer.ktutil/AuthInitializer.kt文件中,我们将 Firebase SDK 配置为在调试模式下连接到模拟器:

    override fun create(context: Context): FirebaseFirestore {
        val firestore = Firebase.firestore
        // Use emulators only in debug builds
        if (BuildConfig.DEBUG) {
            firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
        }
        return firestore
    }

如果您想使用真实的 Firebase 项目测试您的应用程序,您可以:

  1. 在发布模式下构建应用程序并在设备上运行它。
  2. 暂时将BuildConfig.DEBUG替换为false并再次运行应用程序。

请注意,您可能需要退出应用程序并再次登录才能正确连接到生产环境。