65.9K
CodeProject 正在变化。 阅读更多。
Home

SQLXAgent - SQL Express 的作业 - 第 1 部分(共 6 部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2017 年 9 月 30 日

CPOL

23分钟阅读

viewsIcon

30453

downloadIcon

631

创建和运行类似 SQL Server Enterprise 的作业 - 用户指南。

第 1 部分 - 使用 SQLXAgent (本文)
第 2 部分 - 架构和设计决策
第 3 部分 - CSV 和 Excel 导入器代码
第 4 部分 - 作业调度代码
第 5 部分 - 包如何运行
第 6 部分 - 有趣的编码

引言

我猜我们很多人都使用 SQL Server Express。我也猜很多人和我一样,希望 SQL Server Express 能实现代理功能。本文描述了我为解决这一缺陷而编写的一个实用程序,我称之为 SQLXAgent。这个代码断断续续地开发了一年多,据我所知,至少重新设计了两次。我的一些最佳设计和编码是在我早上洗澡时完成的,我的妻子忍受了我的几次(几十次)充满脏话的冲洗周期,因为我提出了关于 SQLXAgent 的想法和认识(这导致了前面提到的重新设计)。

为这个实用程序开发的所有代码都是在 Windows 7、VS2013 下完成的,使用了 .Net 4.5、WPF(用于 UI 部分)以及我们节目的明星 SQL Server Express 2012。如果你使用任何其他版本(更旧或更新),并且遇到问题,你懂的——做个程序员,自己解决问题,因为除非我重新配置我的整个开发环境以匹配你的(我甚至不会 *短暂* 考虑),我无法帮助你。我使用了只有 .Net 4.5 及更高版本才提供的某些 .Net 功能,所以如果你使用的工具不如我的现代,你可能会遇到问题。

最后,这是一篇很长的文章,主要是因为有大量的屏幕截图,而且我的英语相当规范,所以请期待完整的句子,不考虑简洁性。本文应被视为基本用户指南。在做任何其他事情之前,至少阅读本节以了解预期内容。如果你想更深入地了解应用程序,请查阅本文系列的后续部分。我已将其分解为我认为逻辑上分离兴趣的部分。链接位于本文顶部(以及本系列每篇文章的顶部)。

SQLXAgent main window

假设

我假设在座的各位都相当聪明,并且对大多数应用程序软件的使用有一定的洞察力。在用户界面方面,我不相信会偏离旧的 Windows 应用程序设计指南太远,所以即使是最不精明的用户也应该能够理解我使用的控件的含义及其用途。话虽如此,我不喜欢赘述显而易见的事情,在很多情况下,我只是顺带提及某个给定控件的功能/目的,我可能不会对这些主题进行深入讨论。因此,如果你需要超出本文所呈现内容的安抚,或者需要一双温柔的指导之手来帮助你度过难关,你可能应该寻找一个英语更熟练和/或使用 Windows 应用程序经验更丰富的人来帮助你度过难关。除此之外,我在过去与无类型语言倡导者的流氓派系的武装冲突中失去了我的安抚之手,并用一只纯机械手代替了它,它什么也感觉不到,而且很可能会粉碎任何它认为阻碍我最终统治世界的人的梦想。(此处插入疯狂的笑声)

此实用程序由多个程序集(可执行文件和 DLL)组成,所有这些程序集都必须位于同一文件夹中。就我而言,我发现最简单的方法是直接从解决方案层次结构中的 bin\debug 或 bin\release 文件夹运行所有内容,但这仅仅是因为我的 SQL Express 实例位于我的开发机上。如果您想远程运行 SQLXAgent.exe 应用程序,有许多可用的方法可以实现。请记住,它是一个 WPF 应用程序,因此您所选远程控制软件的图形性能相当重要。此外,您可能需要处理一些安全问题。远程软件和安全问题(如果有)均不属于本文系列讨论的范围。这可能还需要您对此方面进行代码更改(这是我的“做个程序员”发挥作用的地方之一)。

既然我们谈论的是 SQL Server Express,我对它的使用做了一些假设。即您将把此实用程序放在实际运行 SQL Server Express 的计算机上。不这样做会带来许多与管理 SQLXAgent 数据库和 SQLXAgentSvc Windows 服务相关的安全问题。我还假设您是该计算机的管理员组中的成员。再说一次,我们谈论的是 SQL Server Express,它不打算在企业环境中使用。

注意:为了适应 CodeProject 的图像大小“最佳实践”,我将所有屏幕截图图像都调整为 640 像素宽。我还尽可能地编辑掉了图像中的空白区域。这意味着当您实际运行应用程序时,表单通常会比屏幕截图更大且更易于阅读。

SQLXAgent 不做的事情

因为我(理所当然地)不指望人们在企业环境中使用 SQL Express,所以我冒昧地不支持 SQL Server 作业中的某些功能。

  • 目标和警报 - 鉴于您选择 SQL Express 所定义的有限范围,这些似乎毫无意义。目标允许您定位多个数据库服务器或仅定位定义作业的服务器,警报允许您根据 SQL Server 生成的事件执行某些操作(包括执行作业)。
     
  • 从某个步骤开始 - 不支持指定多步骤作业应从特定步骤开始。相反,所有作业都从步骤 1 开始,并且禁用的步骤不执行。
     
  • 多步骤作业中从一个步骤到下一个步骤的进程由步骤的 Enabled 标志控制。所有启用的步骤将按指定顺序执行,直到作业失败,或所有步骤都已处理。这意味着您无法像在 SQL Server 作业中那样告诉一个步骤如何进行。
     
  • 多个计划 - 每个作业只有一个作业计划,而不是一个或多个。
     
  • 调度类型 - SQL Server 允许一次性调度和定期调度类型,而 SQLXAgent 只支持“定期”调度。如果你想运行一次作业,可以从主 SQLXAgent 窗口手动运行。
     
  • 步骤类型 - SQLXAgent 中只有两种步骤类型可用 - SQL 和“包”。SQL 步骤执行您指定的 SQL 查询,而包运行您创建的自定义包程序集。包可以用您喜欢的 .Net 语言编写,如果您像我一样正常,那应该是 C#。
     
  • 步骤数据库组合框 - 不指定数据库,您必须指定整个连接字符串。
     
  • 步骤高级页面未实现。
     

应用程序可能还有其他不支持的功能,但我已经忘了,所以这可以视为一个探索的机会。

活动部件

至少您必须安装服务并配置至少一个作业。幸运的是,这两个功能都可以由主 SQLXAgent.exe 应用程序处理。如果您的代理作业需要比 SQL 查询更复杂的东西,您必须使用您最喜欢(且最适合您)的 .Net 语言开发一个包。

我发现我在工作中 SSIS 包开发的大部分是导入 Excel 或 CSV 文件,所以我在您的自定义包中包含了相当健壮的代码来做这件事。虽然没有像 Visual Studio 中的 BIDS 那样花哨的用户界面,但任何稍有天赋的程序员都应该能够使用我提供的代码来完成这项任务。

在开始使用 SQLXAgent 之前

为了尽量减少下载文件大小,并强制执行本文后面的一些要点,我没有在下载文件中包含二进制文件。这意味着您需要下载 SQLXAgent 解决方案,并至少构建一次。

我再次强调,我是在 Windows 7 上使用 Visual Studio 2013,数据库组件是在 SQL Express 2012 上实现的。如果您使用的是较新的工具,可能会遇到与您的工具集相关的问题。我不是说这一定会发生,但考虑到我们这个诉讼多发、易受伤害的社会,我觉得我应该面面俱到。所以,振作起来,下载并构建代码。

最后,这是一个相当复杂的应用程序,有很多活动部件,而我只是一个(不可否认地懒惰的)人,记忆力比瑞士奶酪还差。文章或表格中可能有一些我遗漏的拼写错误,甚至可能有一些功能不如预期/描述那样工作,或者根本不工作(GAK!)。请在您查看此页面底部的文章历史记录后告诉我,因为我可能已经找到了并修复了您发现的问题,并因此更新了文章。

配置 - SQLXAgent.exe

配置在一个应用程序中处理 - SQLXAgent.exe

所有配置任务(包括服务管理)都由这个 WPF 应用程序处理,这也是您将运行的第一个应用程序。首次运行它时,将显示以下表单

First-run notice

此表单只是告诉您不要期望太多,因为它不适用于企业环境(我们在这里讨论的是 SQL Express,因此对此的抱怨将不被容忍)。如果您不想再次看到此表单,请在单击“确定”按钮之前选中“不再显示”复选框。

注意:每次对解决方案执行“清除”/“重新生成”操作时,它都会重置 app.config 文件,因此在此操作之后首次运行应用程序时,您将再次看到该通知。

第一次运行此应用程序时,它将显示“设置”表单

Initial Settings form

此表单要求您提供服务器名称和 SQL Server 实例名称,以便可以创建正确的连接字符串。默认服务器名称是运行应用程序的计算机名称。SQL Server 实例名称也假定为 SQL Server Express 安装程序使用的默认名称 - "SQLEXPRESS"。

请注意,当您输入这些信息时,表单下半部分的连接字符串会更新。提供这些信息后,您可以单击关联的“测试”按钮。假设您指定了有效的服务器名称和实例,表单将显示类似以下内容(显示的异常应是关于预期服务器登录名 - "SQLXAgentUser" - 未找到)

Settings form - showing invalid server

表单的底部部分仍然禁用。这是因为服务器必须测试为有效,(并且您需要创建数据库)才能测试生成的连接字符串。一旦您的服务器测试成功,并且如果您尚未创建数据库,请单击“创建 SQLXAgent 数据库”按钮。将显示以下表单

您必须点击连接字符串的“测试”按钮。如果失败(并且在您首次执行此操作时应该会失败),您将看到无效服务器!以及引发的异常。在我们首次运行此过程的情况下,它会抱怨数据库用户未实现。

Settings form - showing invalid user

除了异常之外,还会显示一个按钮,允许您创建数据库。单击该按钮将显示“创建 SQLXAgent 数据库”表单

Create database form

注意:此屏幕截图拍摄后,我更正了“组件”的拼写。

创建数据库的过程将按照列出的顺序执行以下步骤。每执行一个步骤,表单中的列表视图都会更新。如果一个步骤失败,整个过程将停止,您必须找出出了什么问题。

为了更容易确定问题可能发生的具体位置,下面所有需要执行 SQL 查询的步骤都从嵌入式资源文件检索该查询。如果您想查看这些文件,请将源代码加载到 Visual Studio 中,它们可以在 SQLXAgent\SQLXAgentQueries 文件夹中找到。为了增强问题解决能力,每个表和存储过程都是从其自己的嵌入式资源查询文件创建的。下面的步骤将指示使用哪个查询文件来执行操作。

为了使数据库创建过程获得最佳成功机会,我们必须采取预防措施,停止服务、关闭应用程序,然后删除数据库和相关的数据库对象。我将此称为“停止-删除”阶段。此阶段包括以下步骤(按发生的顺序排列)。

  • 如果服务正在运行,则停止它。如果这是您第一次运行 SQLXAgent.exe,它不应该正在运行(实际上,它甚至不应该安装)。
     
  • 如果 SQL Server Management Studio 正在运行,请将其关闭。该应用程序可能会提示您保存可能已打开和修改的任何查询。我的建议是在创建 SQLXAgent 数据库之前手动关闭该应用程序,但这当然不是必需的。
     
  • 如果数据库存在,则删除它。如果这是您第一次运行 SQLXAgent.exe,它不应该已经存在。(使用 SQLXAgent_DropDatabase.sql 查询文件。)
     
  • 如果现有 SQLXAgent .MDF.LDF 文件在删除数据库时未被删除(我发现有时可能会发生这种情况),则删除它们。
     
  • 如果 SQLXAgentUser 服务器登录名存在,则删除它。如果这是您第一次运行 SQLXAgent.exe,它不应该已经存在。(使用 SQLXAgent_DropServerLogin.sql 查询文件。)

数据库删除后,我们可以创建所有必需的组件。

  • 创建 SQLXAgent 数据库。此步骤的查询存储在文件 SQLXAgentQueries\SQLXAgent_CreateDatabase.sql 中。(使用 SQLXAgent_CreateDatabase.sql 查询文件。)
     
  • 添加 SQLXAgentUser 服务器登录名。这是必需的,以便 SQLXAgentSvc Windows 服务可以访问数据库。我们对安全性不过度关注,因为 *这是 SQL Express,我们没有在企业环境中使用它*。此帐户的密码是 "SQLXAgent"。(使用 SQLXAgentQueries\SQLXAgent_CreateServerLogin.sql 文件。)
     
  • SQLXAgentUser 服务器登录用户添加为数据库用户。同样,这允许 SQLXAgentSvc Windows 服务访问数据库。(使用 SQLXAgentQueries\SQLXAgent_CreateDbUser.sql 文件。)
     
  • 添加数据库表。(使用 SQLXAgentQueries\SQLXAgent_Table_*.sql 文件。在撰写本文时有五个。)
     
  • 添加数据库的存储过程。(使用 SQLXAgentQueries\SQLXAgent_SP_*.sql 文件。在撰写本文时有 24 个。)
     

随着过程的运行,表单上的列表框将反映每个步骤的结果。各个表和存储过程将报告自己的结果。如果一个步骤失败,过程将停止,并且不会进行到最后。如果发生这种情况,您必须找出哪里出了问题,解决问题,然后重新运行创建过程。SQL 文件本身不应该有任何问题(著名的“最后的话”,对吧?)。列表视图中的最后一个条目应该是“进程完成”之类的。这是点击“创建数据库”按钮且过程完成后表单的样子。

Create database form after database created

创建过程执行完毕后,点击“确定”按钮,然后点击“设置”表单中的连接字符串“测试”按钮。假设创建过程成功完成,测试也应该成功,您可以点击“确定”按钮。在一个完美的世界里,这 *应该* 是您在给定 SQL Server Express 实例中最后一次看到“设置”表单。当您的“设置”表单看起来像这样时,您可以点击“确定”按钮,或继续前往电子邮件设置页面。

Settings form - showing valid server and connection

如果您打算为某些作业状态可能性发送电子邮件通知,则需要指定发送电子邮件所需的一些属性。

Settings form - showing email settings page

密码以加密形式存储在 app.config 文件中。

还有一些您可以更改的其他设置。我认为下面的屏幕截图是不言自明的。

Settings form - showing miscellaneous settings page

在“设置”表单中点击“确定”后,主窗口应如下所示。

Settings form - showing invalid server and connection

表单拓扑可以分为三个明显的区域

  • 菜单 - 这是标准的 Windows 风格菜单。“退出”应该是不言自明的(我希望如此),我们已经进入过“设置”表单,所以“设置”也应该是不言自明的。其他项目将在本文后面讨论。
     
  • 列表视图 - 显示您指定的作业。
     
  • 按钮 - 这些按钮允许您添加新作业,或编辑、删除和运行当前选定的作业。如果未选择作业,则只有“添加”按钮启用。

当您点击此“添加”按钮时,将显示“添加/编辑作业”表单。

添加作业

一个作业本质上是一个与数据库相关的任务,通常安排在指定的时间间隔内重复发生。为了使这个实用程序有用,您需要创建一个或多个作业。一个给定作业可以有一个或多个步骤,并且这些步骤(当指定多个时)按照它们在步骤列表中的位置顺序执行。当一个步骤失败时,该作业的处理将停止,并且当前作业执行周期内不会处理后续步骤。一个步骤也必须启用才能运行。

在可能的情况下,作业配置页面被设计成与 SQL Server 企业版中代理配置组件中存在的页面相似。

当添加新作业时,它最初是禁用的,名称为“新作业”,计划每周发生,并且已经添加了一个步骤(您必须编辑该步骤才能使其有用)。

通用页面

Job - General page

此页面仅允许用户指定作业名称,并启用或禁用作业。如果作业未启用(默认禁用),它将不会被服务加载或运行,因此请不要忘记启用它。

熟悉 SQL Server 版本此页面的人可能已经注意到了

步骤页面

Job - General page

此页面显示为作业指定的步骤,并允许您更改它们的顺序,以及添加、编辑或删除它们。“添加”和“编辑”按钮都显示相同的表单。

Job - Step add/edit page

在这里,您可以为步骤指定一个名称,启用它,并为其选择一个合适的连接字符串。程序将保留最后十个指定的连接字符串,并将它们显示在一个组合框中,这样您就不必一遍又一遍地重新输入相同的字符串。如果您需要的字符串不在列表中,请单击“添加”按钮,将显示此表单。

Job/Step - add new connection string

像“设置”表单一样,您必须使用“测试”按钮验证指定的连接字符串,如果验证成功,您可以点击“确定”按钮。当您点击“确定”时,您将返回到上一个表单,新添加的连接字符串将可供该步骤选择。

下图说明了一个 SQL 步骤已配置。

Job - Steps page

在编写此应用程序时,我了解到 SQL GO 命令在批处理 SQL 查询(这就是 SQL Server 的批处理 SQL 查询)中不受支持。如果您需要/想使用 GO,我建议您在数据库中编写一个存储过程,然后在 SQL 步骤中 EXEC 该存储过程。

Job - Steps page

此表单允许您命名步骤(默认名称为“新步骤”)、启用步骤(默认不启用),并指定步骤类型。作业步骤可以是以下两种可用类型之一

  • SQL - 表示运行 SQL 查询的步骤。任何有效的 SQL 都可接受。强烈建议您在在此处指定 SQL 查询之前在 SSMS 中进行测试。
     
  • PKG - 表示运行开发人员提供的包的步骤。所需的包程序集必须存在,您才能在此处指定其使用。请参阅下面标题为创建包的部分。
     
Job - General page

如果您将鼠标悬停在“步骤内容”列上,将显示一个工具提示,显示所有内容。对于 SQL 步骤,工具提示将保留格式。此外,如果您指定了多个步骤,您可以上下移动它们以调整顺序。因为我们使用的是 SQL Express,所有作业都从步骤 1 开始,如果一个步骤失败,该作业的后续步骤将不会执行,并且该作业在审计日志中标记为失败。

计划页面

计划页面看起来与您在企业级 SQL Server 中看到的几乎完全一样。有一些细微的差别,如下所示。

Job - Schedule page

通知页面

通知”页面允许您配置如何传达错误通知。

Job - Notifications page

手动运行作业

要手动运行作业,请选择所需的作业,然后点击主窗口底部的“运行”按钮。将显示“手动作业进度”表单。

Manual job progress form

只需点击“开始”按钮即可运行作业。作业运行时,您将无法点击“确定”按钮。作业的步骤执行时,执行状态将显示在表单的列表视图中。运行作业后,它将显示在作业历史记录中,并显示由USER运行。(允许服务或 TPLTesterUI 应用程序通过其计划运行作业将导致“Run By”列显示SERVICE。)

管理服务

服务管理通过表单菜单右侧的“服务”菜单项执行。此菜单项的左侧是一个椭圆,其颜色指示服务的当前状态,如下所示。

  • 服务未安装
     
  • 服务已安装,但未运行
     
  • 服务已安装并正在运行

此菜单中的子菜单项根据服务的当前状态启用。例如,如果服务未安装,则无法启动或卸载服务;如果服务未运行,则无法停止服务。

您必须安装并启动服务才能开始您的作业计划。

作业历史

与 SQL Server 一样,SQLXAgent 跟踪作业历史记录,您可以通过点击菜单中的“作业历史记录”来查看。届时,将显示以下表单。

Job History form

要查看与特定作业相关的步骤,只需在历史记录列表视图中点击该作业。该项将展开,您可以检查步骤。当您在列表框中选择一个作业或刷新列表时,列表中所有先前展开的项都将折叠。如果作业失败,则不会显示该作业的失败原因。您必须展开该作业以显示步骤,并检查失败的步骤。

历史项保留 30 天。JobManager 对象每天 00:00:00 调用一次存储过程以删除过期历史项。默认情况下,SQLXAgent 最多保留 30 天的历史项。如果您想减少该值,请使用“设置”表单(如上所述)。如果您在 JobThreadManager 对象运行时(无论是服务内部还是 TPLTesterUI 应用程序内部)更改该值,新值将在下一个删除周期加载和使用。

数据库维护

DB Maintenance 菜单项只是提供重新开始使用数据的便捷方式。按钮描述了它们的功能。

DB Maintenance form

创建 SQLXAgent 包

创建包需要您发挥疯狂的编码技能。简而言之,原因是:我不想投入时间/精力去创建一个像 SQL Server 那样炫酷的用户界面,所以所有 SQLXAgent 包本质上都相当于一个脚本任务。这样做的好处是,您的 SQLXAgent 包可以用您喜欢的任何 .Net 语言编写,并且可以做任何常规 .Net DLL 可以做的事情。解决方案中提供了一些示例包。

在您提出建议之前,是的,可以使用 BIDS 创建 DTSX 包并从 .Net 应用程序运行它,但是不,在 SQL Express 实例上运行它是不可能的,因为 SQL Express 没有运行 DTSX 包所需的程序集(记住——这就是我们都在这里的原因)。

入门

SQLXAgent 创建“包”实际上非常简单。请按照以下步骤编写您自己的包

  • 打开 SQLXPackages 解决方案 - 启动 Visual Studio,然后打开 SQLXPackages 解决方案。
  • 创建新项目 - 创建一个新的类库项目,并给它一个合适的名称。
     
  • 编辑项目属性 - 需要对项目属性进行以下更改。
     
    • 生成选项卡上 - 对于调试和发布配置,在输出路径字段中添加".."
       
    • 生成事件选项卡上 - 将以下行添加到生成后事件命令行字段
      CD $(TargetDir)
      DEL $(TargetName).PKG
      REN $(TargetFileName) $(TargetName).PKG
      
  • 添加引用 - 将以下引用添加到项目中。这两个程序集都存在于 \SQLXAgent\DLLBin 文件夹中,因此您需要点击“浏览...”按钮添加它们。
     
    • SQLXCommon - 包含方便的对象扩展,最重要的是,包含 DBOject2 类,该类允许方便的 SQL 方法。包含此引用是可选的,但您会很高兴这样做了。
       
    • SQLXPkgBase - 创建兼容 SQLXAgent 的包程序集所必需的。
       
  • 继承自必需的基类 - 让您的包类继承自抽象类 SQLXPkgBase.SQLXAgentPkgBase
     
  • 实现 Run 方法 - 这是类中唯一的抽象方法。将下面显示的样板代码复制到您的类中,以便于操作。
    public override void Run(string stepID, string connString)
    {
        // The id of the step being executed - you can use this to retrieve any info 
        // about the job/step
        this.StepID               = stepID;
    
        // This is the connecion string to the SQLXAgent database. It is passed to 
        // this package by the SQLXPkgRunner application
        this.SQLXConnectionString = connString;
    
        // These properties are found in the base SQLXAgentPkgBase class, and can be
        // accessed from the calling application. You should set these properties as 
        // appropriate. The default settings used below indicate a successful outcome.
        this.DLLResult            = 0;
        this.FailReason           = string.Empty;
    
        // TO-DO: add your stuff after this line - set the DLL result to either 0
        //        (success) or 1 (fail) before you exit this method. There are a
        //        couple of example methods for importing from EXCEL and CSV files.
    }
    
  • 添加你的代码 - 添加你的代码是必需的,但你实现什么以及如何实现完全取决于你。通常,一个包会导入数据、移动数据、下载数据然后导入数据、监视文件系统文件夹的变化,或者通过许多其他方式触发某种数据库操作。

测试您的包裹

我在 SQLXPackages 解决方案中包含了一个名为 PackageTester 的命令行应用程序,您可以使用它在作业中使用包之前测试您的包。要测试包,您只需添加包的程序集引用,并在 Main 方法中添加几行代码来运行它。就是这么简单。下面,您将看到我用来测试 CSV 导入器包的 Main 方法内容的示例。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PacakgeTester
{
    class Program
    {
        static void Main(string[] args)
        {
            PkgImportCSV importer = new PkgImportCSV();
            importer.Run("step ID goes here", 
                         @"Server=mars\SQLEXPRESS;Database=SQLXAgent;Integrated Security=SSPI;");
        }
    }
}
		

我发现我原来用于创建“PKG”程序集(将编译的 .DLL 重命名为 .PKG)的后期构建步骤阻止了 PackageTester 应用程序加载包程序集(它坚持加载 DLL),所以我不得不将后期构建步骤中的“REN”更改为“COPY /Y”。

导入 Excel 电子表格和 CSV 文件

SQL Server 的 SSIS 包构建器最方便的方面之一是您可以相当容易地导入 Excel 工作表和 CSV 文件。SQLXAgent 使得实现这种功能,我认为,对于开发人员(这个实用程序实际针对的目标人群)来说,同样容易。有关导入器开发的详细信息,请参阅本文系列的第 3 部分。

历史

  • 2018 年 2 月 27 日 - 修正了一些拼写错误,并添加了一些讽刺意味。我还注意到缺少一张屏幕截图。我会努力找出丢失的图片在哪里。
     
  • 2017 年 10 月 5 日 - 样式更改
     
  • 2017 年 9 月 29 日 - 首次发布。

 

© . All rights reserved.