你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

在 ACS UI 库中收集用户反馈

介绍

本综合指南旨在帮助开发人员使用 Azure 服务进行后端处理,将增强的支持集成到 ACS UI 库中。 本指南分为客户端和服务器端步骤,操作清晰简便。

先决条件

  • Azure 订阅:你需要一个有效的 Azure 订阅。 如果没有 Azure 帐户,可以在 Azure 免费帐户上创建一个免费帐户。
  • Azure 通信服务资源:使用通话和聊天功能需要 ACS 资源。 可以在 Azure 门户中创建一个。
  • 开发环境设置:确保为一个或多个目标平台 – Android、iOS 或 Web – 设置开发环境。
  • Azure 存储帐户:为了安全地存储用户反馈和相关数据,需要使用 Azure 存储帐户。
  • Node.js 和 Express.js:有关 Node.js 和 Express.js 的基础知识有助于设置服务器端应用程序来接收和处理支持请求。
  • RESTful API 的知识:了解如何创建和使用 RESTful API 有助于部署和配置客户端和服务器。
  • 客户端开发技能:Android 或 iOS 应用程序开发方面的熟练程度。

满足这些先决条件可确保使用 Azure 通信服务及其他 Azure 资源顺利集成全面的用户反馈系统。

学习内容

本指南全面深入介绍如何在 Azure 通信服务 (ACS) 应用程序中集成用户反馈机制。 重点介绍通过 ACS UI 库增强客户支持,使用 Azure 后端服务进行处理。 从本指南中,开发人员将学会:

  • 实现客户端反馈捕获:了解如何使用 ACS UI 库直接从 Android 和 iOS 应用程序捕获用户反馈、错误日志和支持请求。
  • 设置服务器端应用程序:有关使用 Express.js 接收、处理和存储 Azure Blob 存储中支持请求的 Node.js 应用程序的分步说明。 此服务器包括处理文件上传的多部分/表单数据,并安全地管理用户数据。
  • 创建支持工单:了解如何生成唯一的支持工单编号,以及一起存储用户反馈与相关应用程序数据。
  • 利用 Azure Blob 存储:深入了解如何使用 Azure Blob 存储来存储反馈和支持请求数据,确保支持高效检索和分析的安全结构化数据管理。
  • 增强应用程序可靠性和用户满意度:开发人员可以通过实施本指南中所述的策略快速找到和解决用户问题。

服务器端同步

设置 Node.js 应用程序以处理支持请求

部分目标:目标是使用充当后端的 Express.js 创建 Node.js 应用程序,以接收来自用户的支持请求。 这些请求可能包括文本反馈、错误日志、屏幕截图及其他有助于诊断和解决用户问题的相关信息。 应用程序将此数据存储在 Azure Blob 存储中,以便进行有组织和安全的访问。

框架和工具

  • Express.js:用于生成 Web 应用程序和 API 的 Node.js 框架。 它是服务器设置和请求处理的基础。
  • Formidable:用于分析表单数据的库,尤其是用于处理多部分/表单数据的库(通常用于文件上传)。
  • Azure Blob 存储:用于存储大量非结构化数据的 Microsoft Azure 服务。

步骤 1:环境设置

在开始之前,请确保开发环境已准备就绪,并已安装 Node.js。 还需要访问 Azure 存储帐户来存储提交的数据。

  1. 安装 Node.js:确保系统上已安装 Node.js。 可以从 Node.js 下载它。

  2. 创建 Azure Blob 存储帐户:如果尚未创建,请通过 Azure 门户创建 Azure 存储帐户。 此帐户用于存储支持请求数据。

  3. 收集必需的凭据:确保具有 Azure Blob 存储帐户的连接字符串。

步骤 2:应用程序设置

  1. 初始化新 Node.js 项目:

    • 为项目创建新目录,并使用 npm init 初始化它以创建 package.json 文件。

    • 使用 npm 安装 Express.js、Formidable、Azure 存储 Blob SDK 及其他必需的库。

      npm install express formidable @azure/storage-blob uuid
      
  2. 服务器实现:

    • 使用 Express.js 设置侦听特定终结点上 POST 请求的基本 Web 服务器。
    • 使用 Formidable 分析传入的表单数据,处理多部分/表单数据内容。
    • 为每个支持请求生成唯一的票证编号,可用于组织 Azure Blob 存储中的数据并为用户提供参考。
    • 将结构化数据(例如用户消息和日志文件元数据)存储在 Blob 存储中的 JSON 文件中。 将实际日志文件和任何屏幕截图或附件存储在同一票证目录中的单独 blob 中。
    • 提供终结点来检索支持详细信息,其中包括从 Azure Blob 存储提取和显示数据。
  3. 安全注意事项:

    • 确保应用程序验证传入数据,以防止恶意有效负载。
    • 使用环境变量安全地存储敏感数据,例如 Azure 存储连接字符串。

步骤 3:运行和测试应用程序

  1. 环境变量:

    • 为 Azure Blob 存储连接字符串和任何其他敏感信息设置环境变量。 例如,可以使用 .env 文件(以及用于加载这些变量的 dotenv npm 包)。
  2. 运行服务器:

    • 通过运行 node <filename>.js 启动 Node.js 应用程序,其中 <filename> 是主服务器文件的名称。
    • 使用适合 Web 开发的工具验证服务器。

服务器代码:

此处提供了一个可开始使用的工作实现。 此代码是专为演示从 ACS UI 示例应用程序创建票证而定制的基本实现。

const express = require('express');
const formidable = require('formidable');
const fs = require('fs').promises
const { BlobServiceClient } = require('@azure/storage-blob');
const { v4: uuidv4 } = require('uuid');
const app = express();
const connectionString = process.env.SupportTicketStorageConnectionString
const port = process.env.PORT || 3000;
const portPostfix = (!process.env.PORT || port === 3000 || port === 80 || port === 443) ? '' : `:${port}`;

app.use(express.json());

app.all('/receiveEvent', async (req, res) => {
    try {
        const form = new formidable.IncomingForm();
        form.parse(req, async (err, fields, files) => {
            if (err) {
                return res.status(500).send("Error processing request: " + err.message);
            }
            // Generate a unique ticket number
            const ticketNumber = uuidv4();
            const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
            const containerClient = blobServiceClient.getContainerClient('supporttickets');
            await containerClient.createIfNotExists();

            // Prepare and upload support data
            const supportData = {
                userMessage: fields.user_message,
                uiVersion: fields.ui_version,
                sdkVersion: fields.sdk_version,
                callHistory: fields.call_history
            };
            const supportDataBlobClient = containerClient.getBlockBlobClient(`${ticketNumber}/supportdata.json`);
            await supportDataBlobClient.upload(JSON.stringify(supportData), Buffer.byteLength(JSON.stringify(supportData)));

            // Upload log files
            Object.values(files).forEach(async (fileOrFiles) => {
                // Check if the fileOrFiles is an array (multiple files) or a single file object
                const fileList = Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles];
            
                for (let file of fileList) {
                    const blobClient = containerClient.getBlockBlobClient(`${ticketNumber}/logs/${file.originalFilename}`);
                    
                    // Read the file content into a buffer
                    const fileContent = await fs.readFile(file.filepath);
                    
                    // Now upload the buffer
                    await blobClient.uploadData(fileContent); // Upload the buffer instead of the file path
                }
            });
            // Return the ticket URL
            const endpointUrl = `${req.protocol}://${req.headers.host}${portPostfix}/ticketDetails?id=${ticketNumber}`;
            res.send(endpointUrl);
        });
    } catch (err) {
        res.status(500).send("Error processing request: " + err.message);
    }
});

// ticketDetails endpoint to serve details page
app.get('/ticketDetails', async (req, res) => {
    const ticketNumber = req.query.id;
    if (!ticketNumber) {
        return res.status(400).send("Ticket number is required");
    }

    // Fetch the support data JSON blob to display its contents
    try {
        const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
        const containerClient = blobServiceClient.getContainerClient('supporttickets');
        const blobClient = containerClient.getBlobClient(`${ticketNumber}/supportdata.json`);
        const downloadBlockBlobResponse = await blobClient.download(0);
        const downloadedContent = (await streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString();
        const supportData = JSON.parse(downloadedContent);

        // Generate links for log files
        let logFileLinks = `<h3>Log Files:</h3>`;
        const listBlobs = containerClient.listBlobsFlat({ prefix: `${ticketNumber}/logs/` });
        for await (const blob of listBlobs) {
            logFileLinks += `<a href="/getLogFile?id=${ticketNumber}&file=${encodeURIComponent(blob.name.split('/')[2])}">${blob.name.split('/')[2]}</a><br>`;
        }

        // Send a simple HTML page with support data and links to log files
        res.send(`
            <h1>Ticket Details</h1>
            <p><strong>User Message:</strong> ${supportData.userMessage}</p>
            <p><strong>UI Version:</strong> ${supportData.uiVersion}</p>
            <p><strong>SDK Version:</strong> ${supportData.sdkVersion}</p>
            <p><strong>Call History:</strong> </p> <pre>${supportData.callHistory}</pre>
            ${logFileLinks}
        `);
    } catch (err) {
        res.status(500).send("Error fetching ticket details: " + err.message);
    }
});

// getLogFile endpoint to allow downloading of log files
app.get('/getLogFile', async (req, res) => {
    const { id: ticketNumber, file } = req.query;
    if (!ticketNumber || !file) {
        return res.status(400).send("Ticket number and file name are required");
    }

    try {
        const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
        const containerClient = blobServiceClient.getContainerClient('supporttickets');
        const blobClient = containerClient.getBlobClient(`${ticketNumber}/logs/${file}`);

        // Stream the blob to the response
        const downloadBlockBlobResponse = await blobClient.download(0);
        res.setHeader('Content-Type', 'application/octet-stream');
        res.setHeader('Content-Disposition', `attachment; filename=${file}`);
        downloadBlockBlobResponse.readableStreamBody.pipe(res);
    } catch (err) {
        res.status(500).send("Error downloading file: " + err.message);
    }
});

// Helper function to stream blob content to a buffer
async function streamToBuffer(stream) {
    const chunks = [];
    return new Promise((resolve, reject) => {
        stream.on('data', (chunk) => chunks.push(chunk));
        stream.on('end', () => resolve(Buffer.concat(chunks)));
        stream.on('error', reject);
    });
}


app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

客户端设置

本部分介绍客户端设置以及如何实现以下目标:

  1. 注册用户报告的问题。
  2. 序列化数据。
  3. 将其转发到服务器。
  4. 接收响应。
  5. 向用户显示响应。

在 Azure 通信服务 (ACS) UI 库中启用用户反馈需要开发人员的操作。 利用库集成中的 onUserReportedIssueEventHandler,开发人员可以启用内置支持表单,允许用户直接报告问题。 本部分将指导你设置客户端反馈表单。

在 Android 中实现客户端反馈捕获

启用支持表单

  1. 事件处理程序注册:

    • 若要在 Android 应用程序中激活支持表单,请在应用程序生命周期中的适当点注册 onUserReportedIssueEventHandler。 此注册不仅会启用表单,而且还可确保表单对用户可见和可访问。
  2. 表单可见性和可访问性:

    • 注册 onUserReportedIssueEventHandler 的存在直接影响支持表单的可见性。 如果没有此处理程序,表单将保持隐藏在用户界面中,导致不可访问用于问题报告。

捕获和处理支持事件

  1. 报告问题时发出事件:

    • 当用户通过启用的支持表单报告问题时,onUserReportedIssueEventHandler 会捕获发出的事件。 这些事件封装了与用户报告的问题相关的所有必要详细信息,例如说明、错误日志和可能的屏幕截图。
  2. 提交数据准备:

    • 用户报告问题后,下一步将涉及为服务器提交准备报告的问题数据。 此准备包括遵循服务器要求,将捕获的信息构建为适合 HTTP 传输的格式。

将问题数据提交到服务器

  1. 异步数据传输:

    • 利用异步机制将准备好的数据传输到指定的服务器终结点。 此方法可确保应用程序保持响应,在后台发送数据时提供流畅的用户体验。
  2. 服务器响应处理:

    • 在数据提交后,处理服务器响应至关重要。 此处理可能涉及分析服务器反馈来确认成功的数据传输,并可能提取对提交的问题的引用(例如票证编号或 URL),这些引用可以传回用户。

提供用户反馈和通知

  1. 即时用户反馈:

    • 通过应用程序的用户界面立即向用户通知其问题报告提交的状态。 对于成功的提交,请考虑提供对提交的问题的引用,以便用户跟踪其报告进度。
  2. 适用于 Android O 和更新版本的通知策略:

    • 对于运行 Android O(API 级别 26)及更新版本的设备,请确保实现特定于报告提交的通知通道。 此设置对于有效传递通知至关重要,并且对于这些 Android 版本是一项要求。

通过执行这些步骤,开发人员可以使用 onUserReportedIssueEventHandler 将可靠的用户反馈机制集成到其 Android 应用程序中,以便高效报告和跟踪问题。 此过程不仅有助于及时解决用户问题,而且显著有助于增强应用程序的整体用户体验和满意度。

Android 代码示例

Kotlin 代码片段演示了使用 Azure 通信服务集成系统以处理 Android 应用程序中用户报告的问题的过程。 此集成旨在通过启用用户与支持团队之间的直接通信来简化支持过程。 下面是相关步骤的概述:

  1. 事件捕获:系统通过 ACS UI 库侦听用户报告的问题。 它利用 onUserReportedIssueEventHandler 从应用程序的 UI 捕获反馈,包括错误和用户问题。

  2. 数据传输到服务器:报告问题时,系统会打包相关数据,包括用户消息、错误日志、版本和诊断信息。 然后,此数据使用异步 POST 请求发送到服务器终结点,确保该过程不会妨碍应用的性能。

  3. 用户反馈和通知:提交后,用户将通过应用内通知立即通知其报告的状态。 对于成功的提交,通知包括提交票证的链接或引用,允许用户跟踪解决进度。

此设置不仅有助于快速解决用户问题,而且通过提供明确的支持和反馈渠道,极大地有助于增强用户满意度和应用可靠性。

package com.azure.android.communication.ui.callingcompositedemoapp

import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.azure.android.communication.ui.calling.CallCompositeEventHandler
import com.azure.android.communication.ui.calling.models.CallCompositeCallHistoryRecord
import com.azure.android.communication.ui.calling.models.CallCompositeUserReportedIssueEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.asRequestBody
import org.threeten.bp.format.DateTimeFormatter
import java.io.File
import java.io.IOException

/**
 * This class is responsible for handling user-reported issues within the Azure Communication Services Calling UI composite.
 * It implements the CallCompositeEventHandler interface to listen for CallCompositeUserReportedIssueEvents.
 * The class demonstrates how to send diagnostic information to a server endpoint for support purposes and
 * how to provide user feedback through notifications.
 */
class UserReportedIssueHandler : CallCompositeEventHandler<CallCompositeUserReportedIssueEvent> {
    // Flow to observe user reported issues.
    val userIssuesFlow = MutableStateFlow<CallCompositeUserReportedIssueEvent?>(null)

    // Reference to the application context, used to display notifications.
    lateinit var context: Application

    // Lazy initialization of the NotificationManagerCompat for managing notifications.
    private val notificationManager by lazy { NotificationManagerCompat.from(context) }

    /**
     * Handles the event when a user reports an issue.
     * - Creates a notification channel for Android O and above.
     * - Updates the userIssuesFlow with the new event data.
     * - Sends the event data including user message, app and SDK versions, call history, and log files to a server.
     */
    override fun handle(eventData: CallCompositeUserReportedIssueEvent?) {
        createNotificationChannel()
        userIssuesFlow.value = eventData
        eventData?.apply {
            sendToServer(
                userMessage,
                debugInfo.versions.azureCallingUILibrary,
                debugInfo.versions.azureCallingLibrary,
                debugInfo.callHistoryRecords,
                debugInfo.logFiles
            )
        }
    }

    /**
     * Prepares and sends a POST request to a server with the user-reported issue data.
     * Constructs a multipart request body containing the user message, app versions, call history, and log files.
     */
    private fun sendToServer(
        userMessage: String?,
        callingUIVersion: String?,
        callingSDKVersion: String?,
        callHistoryRecords: List<CallCompositeCallHistoryRecord>,
        logFiles: List<File>
    ) {
        if (SERVER_URL.isBlank()) { // Check if the server URL is configured.
            return
        }
        showProgressNotification()
        CoroutineScope(Dispatchers.IO).launch {
            val client = OkHttpClient()
            val requestBody = MultipartBody.Builder().setType(MultipartBody.FORM).apply {
                userMessage?.let { addFormDataPart("user_message", it) }
                callingUIVersion?.let { addFormDataPart("ui_version", it) }
                callingSDKVersion?.let { addFormDataPart("sdk_version", it) }
                addFormDataPart(
                    "call_history",
                    callHistoryRecords.map { "\n\n${it.callStartedOn.format(DateTimeFormatter.BASIC_ISO_DATE)}\n${it.callIds.joinToString("\n")}" }
                        .joinToString("\n"))
                logFiles.filter { it.length() > 0 }.forEach { file ->
                    val mediaType = "application/octet-stream".toMediaTypeOrNull()
                    addFormDataPart("log_files", file.name, file.asRequestBody(mediaType))
                }
            }.build()

            val request = Request.Builder()
                .url("$SERVER_URL/receiveEvent")
                .post(requestBody)
                .build()

            client.newCall(request).enqueue(object : Callback {
                override fun onFailure(call: Call, e: IOException) {
                    CoroutineScope(Dispatchers.Main).launch {
                        onTicketFailed(e.message ?: "Unknown error")
                    }
                }

                override fun onResponse(call: Call, response: Response) {
                    CoroutineScope(Dispatchers.Main).launch {
                        if (response.isSuccessful) {
                            onTicketCreated(response.body?.string() ?: "No URL provided")
                        } else {
                            onTicketFailed("Server error: ${response.message}")
                        }
                    }
                }
            })
        }
    }

    /**
     * Displays a notification indicating that the issue ticket has been created successfully.
     * The notification includes a URL to view the ticket status, provided by the server response.
     */
    private fun onTicketCreated(url: String) {
        showCompletionNotification(url)
    }

    /**
     * Displays a notification indicating that the submission of the issue ticket failed.
     * The notification includes the error reason.
     */
    private fun onTicketFailed(error: String) {
        showErrorNotification(error)
    }

    companion object {
        // The server URL to which the user-reported issues will be sent. Must be configured.
        private const val SERVER_URL = "${INSERT_YOUR_SERVER_ENDPOINT_HERE}"
    }

    /**
     * Creates a notification channel for Android O and above.
     * This is necessary to display notifications on these versions of Android.
     */
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "Report Submission"
            val descriptionText = "Notifications for report submission status"
            val importance = NotificationManager.IMPORTANCE_DEFAULT
            val channel = NotificationChannel("report_submission_channel", name, importance).apply {
                description = descriptionText
            }
            notificationManager.createNotificationChannel(channel)
        }
    }

    /**
     * Shows a notification indicating that the report submission is in progress.
     * This uses an indeterminate progress indicator to signify ongoing activity.
     */
    private fun showProgressNotification() {
        val notification = NotificationCompat.Builder(context, "report_submission_channel")
            .setContentTitle("Submitting Report")
            .setContentText("Your report is being submitted...")
            .setSmallIcon(R.drawable.image_monkey) // Replace with an appropriate icon for your app
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setProgress(0, 0, true) // Indeterminate progress
            .build()

        notificationManager.notify(1, notification)
    }

    /**
     * Shows a notification indicating that the report has been successfully submitted.
     * The notification includes an action to view the report status via a provided URL.
     */
    private fun showCompletionNotification(url: String) {
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
        val pendingIntent = PendingIntent.getActivity(
            context,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        val notification = NotificationCompat.Builder(context, "report_submission_channel")
            .setContentTitle("Report Submitted")
            .setContentText("Tap to view")
            .setSmallIcon(R.drawable.image_monkey) // Replace with an appropriate icon for your app
            .setContentIntent(pendingIntent)
            .setAutoCancel(true) // Removes notification after tap
            .build()

        notificationManager.notify(1, notification)
    }

    /**
     * Shows a notification indicating an error in submitting the report.
     * The notification includes the reason for the submission failure.
     */
    private fun showErrorNotification(error: String) {
        val notification = NotificationCompat.Builder(context, "report_submission_channel")
            .setContentTitle("Submission Error")
            .setContentText("Error submitting report\nReason: $error")
            .setSmallIcon(R.drawable.image_monkey) // Replace with an appropriate icon for your app
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .build()

        notificationManager.notify(1, notification)
    }
}

为事件创建处理程序后,可以在创建调用组合时注册它。

            callComposite.addOnUserReportedEventHandler(userReportedIssueEventHandler)

iOS 支持概述

若要使用 Azure 通信服务 (ACS) UI 库在 iOS 应用程序中集成用户反馈集合,开发人员需要遵循结构化方法。 此过程涉及捕获用户反馈,包括错误日志和用户信息。 完成后,此信息将提交到服务器进行处理。 在本部分中,我们详细介绍了完成此任务所需的步骤。

在此示例中,我们使用 Alamofire 库来处理将多部分表单(包括日志文件)发送到服务器。

实现支持表单

  1. 事件处理程序注册:首先注册侦听用户报告问题的事件处理程序。 此处理程序对于使用 ACS UI 库的功能直接从 iOS 应用程序的接口捕获反馈至关重要。

  2. 表单的可见性和可访问性:确保支持表单易于访问,并且对应用程序中的用户可见。 表单的激活直接链接到事件处理程序的实现,该处理程序在 UI 中触发其外观,允许用户报告问题。

捕获和处理支持请求

  1. 用户操作时发出事件:当用户通过支持表单报告问题时,事件处理程序将捕获此操作。 用户对问题的说明、错误日志和任何呼叫 ID 等信息应准备好发送到服务器。

  2. 用于提交的数据结构化:将捕获的信息组织成适合传输的结构化格式。 以符合接收和处理支持请求的服务器终结点预期格式的方式准备数据。

将数据提交到服务器

  1. 异步提交:利用异步网络调用将结构化数据发送到服务器。 此方法可确保应用程序保持响应状态,在后台传输数据时为用户提供无缝体验。

  2. 处理服务器响应:提交后,高效处理服务器响应。 接收和分析响应以确认成功接收数据。 从分析的响应中提取支持工单链接,该链接可以传回用户进行跟进。

向用户反馈和通知

  1. 立即确认:立即确认在应用程序中提交支持请求,向用户确认其报告已收到。

  2. 通知策略:实施向用户传递通知的策略,尤其是在运行支持特定通知框架的 iOS 版本的设备上。 可以使用本地通知向用户告知报告的状态,或者在解决其问题时提供更新。

iOS 代码示例

此 Swift 代码示例概述了一个基本实现,用于捕获用户报告的问题并将其提交到服务器进行处理。 此示例演示如何构造支持事件处理程序,包括用户反馈和应用程序诊断信息以及传递到服务器。 该代码还包括错误处理和用户通知策略,以确保用户体验流畅。

以下示例设计为要安装在事件处理程序中的挂钩。

安装

let onUserReportedIssueHandler: (CallCompositeUserReportedIssue) -> Void = { issue in
    // Add a hook to this method, and provide it the Server endpoint + a result callback
    sendSupportEventToServer(server: self.issueUrl, event: issue) { success, result in
        if success {
            // Success: Convey the result link back to the user
        } else {
            // Error: Let the user know something has happened
        }
    }
}

网络挂钩

import Foundation
import UIKit
import Combine
import AzureCommunicationUICalling
import Alamofire

/// Sends a support event to a server with details from a `CallCompositeUserReportedIssue`.
/// - Parameters:
///   - server: The URL of the server where the event will be sent.
///   - event: The `CallCompositeUserReportedIssue` containing details about the issue reported by the user.
///   - callback: A closure that is called when the operation is complete.
///               It provides a `Bool` indicating success or failure, and a `String`
///               containing the server's response or an error message.
func sendSupportEventToServer(server: String,
                              event: CallCompositeUserReportedIssue,
                              callback: @escaping (Bool, String) -> Void) {
    // Construct the URL for the endpoint.
    let url = "\(server)/receiveEvent" // Ensure this is replaced with the actual server URL.

    // Extract debugging information from the event.
    let debugInfo = event.debugInfo

    // Prepare the data to be sent as key-value pairs.
    let parameters: [String: String] = [
        "user_message": event.userMessage, // User's message about the issue.
        "ui_version": debugInfo.versions.callingUIVersion, // Version of the calling UI.
        "call_history": debugInfo.callHistoryRecords
            .map { $0.callIds.joined(separator: ",") }
            .joined(separator: "\n") // Call history, formatted.
    ]

    // Define the headers for the HTTP request.
    let headers: HTTPHeaders = [
        .contentType("multipart/form-data")
    ]

    // Perform the multipart/form-data upload.
    AF.upload(multipartFormData: { multipartFormData in
        // Append each parameter as a part of the form data.
        for (key, value) in parameters {
            if let data = value.data(using: .utf8) {
                multipartFormData.append(data, withName: key)
            }
        }

        // Append log files.
        debugInfo.logFiles.forEach { fileURL in
            do {
                let fileData = try Data(contentsOf: fileURL)
                multipartFormData.append(fileData,
                                         withName: "log_files",
                                         fileName: fileURL.lastPathComponent,
                                         mimeType: "application/octet-stream")
            } catch {
                print("Error reading file data: \(error)")
            }
        }
    }, to: url, method: .post, headers: headers).response { response in
        // Handle the response from the server.
        switch response.result {
        case .success(let responseData):
            // Attempt to decode the response.
            if let data = responseData, let responseString = String(data: data, encoding: .utf8) {
                callback(true, responseString) // Success case.
            } else {
                callback(false, "Failed to decode response.") // Failed to decode.
            }
        case .failure(let error):
            // Handle any errors that occurred during the request.
            print("Error sending support event: \(error)")
            callback(false, "Error sending support event: \(error.localizedDescription)")
        }
    }
}

此 Swift 代码演示了使用 Azure 通信服务从 iOS 应用程序提交用户报告的问题的过程。 它处理用户反馈的收集、诊断信息的打包以及向服务器终结点的异步提交。 此外,它还为实施反馈机制提供基础,确保用户了解其报告的状态,并提高应用程序的整体可靠性和用户满意度。

结束语

使用 Azure 通信服务 (ACS) 将用户反馈机制集成到应用程序中对于开发响应式应用和以用户为中心的应用至关重要。 本指南为 Android 和 iOS 应用程序设置服务器端处理 Node.js 和客户端反馈捕获提供了明确的路径。 通过这种集成,开发人员可以增强应用程序可靠性和用户满意度,同时利用 Azure 的云服务进行高效的数据管理。

本指南概述了直接从应用程序捕获用户反馈、错误日志和支持请求的实际步骤。 集成支持事件可确保以安全有序的方式处理反馈,使开发人员能够快速解决和解决用户问题,从而改善整体用户体验。

按照本指南详述的说明,开发人员可以改进其应用程序的响应能力,并更好地满足用户需求。 这些集成不仅有助于更有效地了解用户反馈,而且还利用云服务来确保一种流畅有效的反馈收集和处理机制。 最终,集成用户反馈机制对于创建具有吸引力、可靠、优先考虑用户满意度的应用程序至关重要。