问答游戏提供了一种引人入胜的教育体验,您可以在其中学习新的事实并扩展各个学科的知识。如今,问答游戏移动和网络应用程序是此类活动最常见的领域。在 WhatsApp 上玩一个问答游戏怎么样? 在本教程指南中,您将学习如何使用 Twilio for WhatsApp、ASP.NET Core 和 ( )。本教程的目的是创建一个问答游戏应用程序,允许用户使用 WhatsApp 玩并回答多项选择题。你将利用 存储和检索用户进度、跟踪得分并维护游戏状态。 琐事 API 根据 CC BY-NC 4.0 许可 ASP.NET Core 中的会话 为了获取这些问题,您将使用 Trivia API(一种 REST API),它使开发人员可以通过提供多项选择问答题轻松构建测验应用程序。要了解有关 Trivia API 的更多信息,请访问 __。 __Trivia API 文档 先决条件 要完成本教程,您将需要: 支持.NET的操作系统(Windows/macOS/Linux) .NET 7 SDK 代码编辑器或 IDE(推荐: , 与 , 或者 ) 视觉工作室 视觉工作室代码 C# 插件 JetBrains 骑士 ngrok CLI 一个免费或付费的 Twilio 帐户(如果您没有,您可以 ) 免费试用 Twilio C# 和 ASP.NET Core 经验 。 本教程的源代码可以在 GitHub 上找到 设置新的 ASP.NET Core 项目 首先,在首选工作目录中使用 shell 终端,运行以下命令来创建新的 Web API 项目: dotnet new webapi -n TwilioWhatsAppTriviaApp --no-openapi 上面代码片段中的第二个命令将创建一个具有指定名称且不支持 OpenAPI (Swagger) 的新 Web API 项目。如果你想在项目中使用Swagger,只需在上面的命令中省略 即可。 --no-openapi 通过运行以下命令更改到项目目录: cd TwilioWhatsAppTriviaApp 安装 NuGet 包: 适用于 ASP.NET Core 的 Twilio 帮助程序库 dotnet add package Twilio.AspNet.Core 该库简化了 ASP.NET Core 应用程序中 Twilio Webhooks 和 API 的使用。 使用您喜欢的 IDE 打开项目。在 文件夹中,删除样板模板控制器文件 ,并删除项目目录中的 。 Controllers WeatherForecastController.cs WeatherForcast.cs 使用以下命令构建并运行您的项目,以确保您迄今为止所做的一切都能正常运行: dotnet build dotnet run 成功运行项目后,记下调试控制台中显示的任何本地主机 URL。您可以使用这些 URL 中的任何一个来使用 ngrok 设置可公开访问的本地 Web 服务器。 实施会议 会话是在 ASP.NET Core 应用程序中存储用户数据的几种方法之一。当您想要在请求之间保留用户数据时,这一点至关重要,因为默认情况下,HTTP 协议是无状态的 - 这意味着数据不会被保留。 通过修改 添加内存会话提供程序,如下代码所示: Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(40); options.Cookie.IsEssential = true; }); var app = builder.Build(); app.UseSession(); app.MapControllers(); app.Run(); 注册分布式内存缓存服务。该服务提供内存缓存,可用于跨多个请求或会话存储和检索数据。 AddDistributedMemoryCache() 注册会话服务,使应用程序能够维护会话状态。 参数允许您配置各种与会话相关的选项。 用于设置不活动的持续时间,超过该时间后会话将被视为空闲。在本例中,它设置为 40 秒。 确保会话状态保持正常运行,即使在启用 cookie 拒绝的情况下也是如此。 AddSession() options IdleTimeout Cookie.IsEssential 通过将 中间件添加到应用程序管道来启用会话支持,也就是说,您的应用程序可以获得对可用于存储和检索数据的会话对象的访问权限。 UseSession 创建模型 在项目目录中创建一个新文件夹 。添加两个模型类文件 和 ,其属性如以下代码示例所示: Models TriviaApiResponse.cs Question.cs using Newtonsoft.Json; namespace TwilioWhatsAppTriviaApp.Models; public class TriviaApiResponse { [JsonProperty("category")] public string Category { get; set; } [JsonProperty("correctAnswer")] public string CorrectAnswer { get; set; } [JsonProperty("incorrectAnswers")] public List<string> IncorrectAnswers { get; set; } [JsonProperty("question")] public string Question { get; set; } [JsonProperty("type")] public string? Type { get; set; } [JsonProperty("difficulty")] public string Difficulty { get; set; } } namespace TwilioWhatsAppTriviaApp.Models; public class Question { public string QuestionText { get; set; } public List<(string option, bool isCorrect)> Options { get; set; } } 模型包含表示 Trivia API 响应字段的属性。 属性确保每个属性都正确填充相应的 JSON 数据。 TriviaApiResponse JsonProperty 为了以简化的方式处理琐碎问题, 类可以提供帮助。此类封装了琐事问题的必要信息,包括问题文本和选项列表。每个选项都由一个元组表示,其中包含选项文本和一个指示其是否是正确选项的布尔值。 Question 添加琐事服务类 在项目目录中创建一个 文件夹,并添加一个名为 的新类文件。修改其内容,如下代码所示: Services TriviaService.cs using Newtonsoft.Json; using TwilioWhatsAppTriviaApp.Models; namespace TwilioWhatsAppTriviaApp.Services; public class TriviaService { private const string TheTriviaApiUrl = @"https://the-trivia-api.com/api/questions?limit=3"; private HttpClient httpClient; public TriviaService(HttpClient httpClient) { this.httpClient = httpClient; } public async Task<IEnumerable<TriviaApiResponse>> GetTrivia() { var response = await httpClient.GetAsync(TheTriviaApiUrl); var triviaJson = await response.Content.ReadAsStringAsync(); var trivia = JsonConvert.DeserializeObject<IEnumerable<TriviaApiResponse>>(triviaJson); return trivia; } public List<Question> ConvertTriviaToQuestions(IEnumerable<TriviaApiResponse> questions) { List<Question> newQuestions = new(); foreach (var question in questions) { var options = new List<(string option, bool isCorrect)>() { (question.CorrectAnswer, true), (question.IncorrectAnswers[0], false), (question.IncorrectAnswers[1], false), (question.IncorrectAnswers[2], false) }; // Shuffle the options randomly Random random = new(); options = options.OrderBy(_ => random.Next()).ToList(); newQuestions.Add(new Question { QuestionText = question.Question, Options = options }); } return newQuestions; } } 类包含两个方法: 和 。 方法将 HTTP GET 请求发送到 Trivia API,其中包含查询参数 ,该参数指定仅应返回 3 个问题。如果没有 limit 参数,API 默认返回 10 个问题。 TriviaService GetTrivia ConvertTriviaToQuestions GetTrivia limit=3 方法将 API 的响应转换为有组织的方式。该方法还会随机打乱所有问题选项,因此单个选项不会成为所有问题的答案。 ConvertTriviaToQuestions 要在应用程序的依赖注入 (DI) 容器中注册 和 HTTP 客户端,请修改 ,如以下代码所示: TriviaService Program.cs using TwilioWhatsAppTriviaApp.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(40); options.Cookie.IsEssential = true; }); builder.Services.AddHttpClient(); builder.Services.AddScoped<TriviaService>(); var app = builder.Build(); app.UseSession(); app.MapControllers(); app.Run(); 创建 Trivia 控制器 将名为 的文件中的空 API 控制器类添加到 文件夹中,并修改其内容,如以下代码所示: TriviaController.cs Controllers using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Twilio.AspNet.Core; using Twilio.TwiML; using Twilio.TwiML.Messaging; using TwilioWhatsAppTriviaApp.Models; using TwilioWhatsAppTriviaApp.Services; namespace WhatsappTrivia.Controllers; [Route("[controller]")] [ApiController] public class TriviaController : TwilioController { private const string SessionKeyIsGameOn = "IsGameOn"; private const string SessionKeyScore = "Score"; private const string SessionKeyCurrentQuestionIndex = "CurrentQuestionIndex"; private const string SessionKeyTotalQuestions = "TotalQuestions"; private const string SessionKeyQuestions = "Questions"; private static readonly string[] StartCommands = { "START", "S" }; private static readonly string[] OptionValues = { "A", "B", "C", "D" }; private readonly TriviaService triviaService; public TriviaController(TriviaService triviaService) { this.triviaService = triviaService; } [HttpPost] public async Task<IActionResult> Index() { var response = new MessagingResponse(); var form = await Request.ReadFormAsync(); var body = form["Body"].ToString().ToUpper().Trim(); await HttpContext.Session.LoadAsync(); var isGameOn = Convert.ToBoolean(HttpContext.Session.GetString(SessionKeyIsGameOn)); int currentQuestionIndex = HttpContext.Session.GetInt32(SessionKeyCurrentQuestionIndex) ?? 0; int totalQuestions = HttpContext.Session.GetInt32(SessionKeyTotalQuestions) ?? 0; if (StartCommands.Contains(body) && !isGameOn) { await StartGame(); HttpContext.Session.SetString(SessionKeyIsGameOn, "true"); response.Message(PresentQuestionWithOptions(currentQuestionIndex)); return TwiML(response); } if (OptionValues.Contains(body) && isGameOn) { var result = ProcessUserAnswer(body, currentQuestionIndex); response.Message(result); currentQuestionIndex++; if (currentQuestionIndex <= totalQuestions - 1) { HttpContext.Session.SetInt32(SessionKeyCurrentQuestionIndex, currentQuestionIndex); response.Append(new Message(PresentQuestionWithOptions(currentQuestionIndex))); } else { response.Append(new Message(EndTrivia())); } return TwiML(response); } response.Message(!isGameOn ? "*Hello! Send 'Start' or 'S' to play game*" : "*Invalid Input! Send a correct option 'A', 'B', 'C' or 'D'*"); return TwiML(response); } private async Task StartGame() { if (HttpContext.Session.GetString(SessionKeyQuestions) != null) { HttpContext.Session.Remove(SessionKeyQuestions); } var trivia = await this.triviaService.GetTrivia(); var questions = this.triviaService.ConvertTriviaToQuestions(trivia); AddNewQuestionsToSession(questions); HttpContext.Session.SetInt32(SessionKeyTotalQuestions, questions.Count); } private string ProcessUserAnswer(string userAnswer, int questionIndex) { bool optionIsCorrect = false; int score = HttpContext.Session.GetInt32(SessionKeyScore) ?? 0; var question = RetrieveQuestionFromSession(questionIndex); switch (userAnswer) { case "A": optionIsCorrect = question.Options[0].isCorrect; break; case "B": optionIsCorrect = question.Options[1].isCorrect; break; case "C": optionIsCorrect = question.Options[2].isCorrect; break; case "D": optionIsCorrect = question.Options[3].isCorrect; break; } if (optionIsCorrect) { score++; HttpContext.Session.SetInt32(SessionKeyScore, score); } return optionIsCorrect ? "_Correct ✅_" : $"_Incorrect ❌ Correct answer is {question.Options.Find(o => o.isCorrect).option.TrimEnd()}_"; } private string PresentQuestionWithOptions(int questionIndex) { var question = RetrieveQuestionFromSession(questionIndex); return $""" {questionIndex + 1}. {question.QuestionText} {OptionValues[0]}. {question.Options[0].option} {OptionValues[1]}. {question.Options[1].option} {OptionValues[2]}. {question.Options[2].option} {OptionValues[3]}. {question.Options[3].option} """; } private void AddNewQuestionsToSession(List<Question> questions) => HttpContext.Session.SetString(SessionKeyQuestions, JsonConvert.SerializeObject(questions)); private Question RetrieveQuestionFromSession(int questionIndex) { var questionsFromSession = HttpContext.Session.GetString(SessionKeyQuestions); return JsonConvert.DeserializeObject<List<Question>>(questionsFromSession)[questionIndex]; } private string EndTrivia() { var score = HttpContext.Session.GetInt32(SessionKeyScore) ?? 0; var totalQuestions = HttpContext.Session.GetInt32(SessionKeyTotalQuestions) ?? 0; var userResult = $""" Thanks for playing! 😊 You answered {score} out of {totalQuestions} questions correctly. To play again, send 'Start' or 'S' """; HttpContext.Session.Clear(); return userResult; } } 该控制器类负责处理传入消息、管理会话状态和生成响应。它继承自 Twilio.AspNet.Core 库提供的 类,该类使您可以访问 方法。您可以使用此方法来响应 。 类使用 方法与会话进行交互。 有效输入是 和 只读数组中的元素。将传入消息的正文与这些元素进行比较,以确保用户发送了正确的输入,如果没有,则会向用户发送一条消息,提示他们根据游戏的当前状态做出正确的输入。其他带有“SessionKey”前缀的字段用于定义程序中会话密钥的私有常量字符串。 TwilioController TwiML TwiML,即 Twilio 标记语言 TriviaController HttpContext.Session StartCommands OptionValues 方法是主要操作方法,用于处理通过 路由从 WhatsApp 传入的 HTTP POST 请求。它使用 加载会话数据,并使用 和 方法从会话中检索有关游戏状态的数据。 Index /Trivia HttpContext.Session.LoadAsync() HttpContext.Session.GetString() HttpContext.Session.GetInt32() 在某些字符串的开头和结尾使用下划线 (_) 和星号 (*) 是为了在呈现的 WhatsApp 消息中分别实现斜体和粗体文本格式。 中的每个辅助方法都执行支持该类主要功能的特定任务。 TriviaController 方法通过检索琐事问题、将其转换为适合游戏的格式并将其存储在会话中来初始化游戏。 StartGame 方法处理用户对问题的回答并确定其是否正确。 ProcessUserAnswer 方法负责格式化和呈现问题及其选项。 PresentQuestionWithOptions 方法在会话中存储问题列表。它将问题转换为 JSON 格式并将 JSON 字符串保存在会话中。 AddNewQuestionsToSession 方法使用问题索引从会话中检索问题。 RetrieveQuestionFromSession 方法生成一条消息来结束问答游戏。此方法还会删除与游戏相关的会话数据。根据 中会话服务的配置,当会话空闲 40 秒时,会自动发生这种情况。 EndTrivia Program.cs 测试应用程序 要测试应用程序,您需要为 WhatsApp 设置 Twilio Sandbox,使您的应用程序端点可公开访问,并将端点 URL 添加到 Sandbox 配置中作为 Webhook。 为 WhatsApp 设置 Twilio 沙盒 前往 ,导航至 。 Twilio 控制台 消息 > 试用 > 发送 WhatsApp 消息 按照页面上的说明连接到沙箱,从您的设备向提供的 Twilio 号码发送 WhatsApp 消息,以便与 WhatsApp 沙箱创建成功连接。同样,其他想要使用各自号码测试您的应用程序的人也需要遵循相同的程序。 使用 ngrok 公开您的 webhook 进行测试 现在,打开 shell 终端并运行以下命令来启动 ngrok 并通过将 替换为您最初复制的 localhost 的完整 URL 来公开本地 ASP.NET Core 应用程序: <localhost-url> ngrok http <localhost-url> ngrok 将生成一个公共 URL,将请求转发到本地 ASP.NET 应用程序。在 ngrok 终端窗口中查找标记为 的转发 URL 并复制它。 Forwarding 返回 Twilio Try WhatsApp 页面,单击 ,然后使用 ngrok 生成的 URL 和 路由更改 端点 url 时,并确保方法设置为 POST。然后单击“保存”以保存新的沙箱配置。 Sandbox Settings 转发 /Trivia 当消息传入 项目演示 使用以下命令运行 ASP.NET Core 项目: dotnet run 现在,通过在与 Twilio Sandbox 号码的初始对话中发送消息来测试您的应用程序。 结论 通过利用 Twilio 平台和 WhatsApp 的强大功能,您为用户打造了身临其境的问答游戏体验。您还学习了如何从会话中保存和检索数据。 有多种方法可以改进该项目。您可以通过添加计时器、处理异常、允许用户选择难度以及通过 Trivia API URL 将所选难度应用为查询参数来进一步改进该项目(例如 )。此外,您可以探索创建一个允许用户通过 WhatsApp 填写调查的解决方案的可能性。 https://the-trivia-api.com/api/questions?difficulty=hard