因为我最近忙、所以我写了它

本文介绍了一位程序员如何利用.NET Core、Quartz定时任务、HtmlAgilityPack等技术,自动化完成学校每日健康信息报备的过程。从需求分析到技术实现,再到项目部署,详细展示了自动化任务的全流程。

一、事出有因

       1、上周工作原因项目的事情每天都很忙,周五下班和乐师兄下班的时候已经晚上11点了,然后和师兄吃了一个烧烤吃到了12点了(结果啥也没吃,钱也花了挺多的。ps:程序员建议养生)扯远了 ,主要是一周比较忙周六没有进行我们学校的健康报备信息填写。导致辅导员给我打了6个电话(现在我还是在实习中,所以还归学校管)自己刚好在看网络请求这一块的东西,说干就干!!!

二、知其所以然

       1、我们学校的健康报备主要是将自己每天的信息填写到学校的公众号上面。打开页面是这样的。这个时候我们就要想一下我们是要做什么了。首先我们健康报备的流程是=>打开学校公众号=>输入自己的学号点击查询=>然后输入自己的正式情况=>点击提交。

             

     三、反向分析

            1、做为程序员的我们首先要清楚我们要什么,现在我们已经清楚了要做什么了和步骤。现在就是用我们的程序模拟用户进行正常的报备工作。首先我将报备链接从学校公众号复制出来,然后使用浏览器访问查看我们在进行报备的时候我们向浏览器发起了什么和做了什么。

            2、在左边的是我们的页面,右边的是网页请求的文件和信息,我们就一步一步的进行分析。这里我们可以看到我们在访问页面的时候会生成一些信息__EVENTVALIDATION   、 __VIEWSTATEGENERATOR 我以前在长沙写的也是ASPX页面使用控件程序的时候也会生成一些这个,需要通过这些东西来获取我们页面填写的信息所以这里我们需要保留,因为我们等下需要输入学号然后点击查询,不过不传这些参数获取不到我输入的学号,这个是我尝试过的哈哈。

             3、下面看到的是我输入了自己的学号点击查询获取到的信息,我们可以看到这里使用的是formData进行传递到后台去的,也可以明显的看到我填写的信息了。然后进入到了填写页面。然后我们在查看页面上面生成的东西。

              4、我们输入完整的信息之后,点击报备按钮又会发生什么呢?可以看到我们提交的formData信息,请求也成功了(今天我已经报备了,所以是这个提示)。现在整个流程我们已经清楚了,以及请求的参数。那就让我们开始整活吧。

 四、开始动手

        1、废话不多说直接创建一个.net core 的项目整活。我先写了一个简单的html页面进行填写学号信息,然后在写一个定时任务每天晚上12点之后自动执行,健康报备信息。

public async static Task AsyncQuartz()
        {
            await Task.Run(async () =>
             {
                 //创建一个键值集合
                 NameValueCollection nameValue = new NameValueCollection {
                    //定时任务的序列类型是二进制的
                    { "quartz.serializer.type", "binary" }
                 };
                 //创建定时任务调度器工厂
                 StdSchedulerFactory factory = new StdSchedulerFactory(nameValue);
                 //获取工厂中的调度器
                 IScheduler scheduler = await factory.GetScheduler();

                 //开启调度器
                 await scheduler.Start();

                 //然后就是创建我们的任务
                 //给任务一个身份
                 //在进行建立
                 IJobDetail userServiceJob = JobBuilder.Create<HealthForJob>()
                                              .WithIdentity("UserServiceJob", "UserServiceJobGroup")
                                              .Build();

                 //任务有了创建触发器
                 ITrigger userServiceTrigger = TriggerBuilder.Create()
                  .WithIdentity("userServiceTrigger", "userServiceTriggerGroup")
                  .StartNow()

                  //给定执行时间,然后在重复执行
                  .WithSimpleSchedule(x => x.WithIntervalInHours(6).RepeatForever())
                  .Build();

                 //将任务和触发器进行绑定放入触发器中

                 //单任务调用
                 await scheduler.ScheduleJob(userServiceJob, userServiceTrigger);
             });
        }

            2、首先我们需要模拟用户向报备网页发起请求这里我使用的是HttpClient 对象发起请求,怕被拦截我还填了很多请求头哈哈。但我们发起get请求的时候获取的是一段长的html字符串。

            3、然后我们要使用一个神器进行html分析了。HtmlAgilityPack 它可以解析我们获取的html字符串代码

          4、我们创建一个HtmlDocument htmlDoc1 = new HtmlDocument(); 对象然后然后将我们获取的html 对象放到 htmlDoc1.LoadHtml(strHtml);就可以解析成了正常的html了,也可以直接在页面上面复制xpath结构,然后直接放进来就好了

                            htmlDoc1.LoadHtml(strHtml);
                            //这里就是通过html结构寻找我们想要的节点信息
                            var liNodes1 = htmlDoc1.DocumentNode.SelectNodes("//div[@class='aspNetHidden']/input");

            5、这里我们获取到了html节点之后就可以进行正常取数据啦。

                6、这里就开始我们的请求三大步了,直接模拟一个form表单请求将我们的数据传递就好了

 

using (HttpContent httpContent = new FormUrlEncodedContent(keyValuePairs))
                        {
                            httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
                            strHtml = httpClient.PostAsync("URL", httpContent).Result.Content.ReadAsStringAsync().Result;
                            if (!string.IsNullOrWhiteSpace(strHtml))
                            {
                                htmlDoc1.LoadHtml(strHtml);
                                var liNodes1 = htmlDoc1.DocumentNode.SelectNodes("//div[@class='aspNetHidden']/input");
                                keyValuePairs.Clear();
                                foreach (var item in liNodes1)
                                {
                                    var id = item.Attributes["id"].Value;
                                    var value = item.Attributes["value"].Value;
                                    if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(value))
                                    {
                                        keyValuePairs.Add(id, value);
                                    }
                                }
                            }
                        }

 

五、项目部署

        1、项目部署使用的是Docker +JenKins 实现自动化部署,现在我在公司也想慢慢推广因为我们公司服务器权限管的比较严格,每次发布测试环境都需要找师兄发布,导致师兄很多时间都在帮我们发布项目。所以这个技术我觉得是很有必要在我们部门推广。这个项目主要使用的了.net core 3.1 作为框架 Dapper作为数据访问层,Quartz 定时任务 ,HtmlAgilityPack 进行Html结构分析,Docker 部署项目 ,JenKins 实现项目自动化部署。由于篇幅问题这些技术会在后面的文章分享出来。我自己也多研究一下避免误人子弟。哈哈

六、个人说明

        1、以上就是我写的全部流程,我们需要重复试错,因为你要了解写这个程序的同行的思路哈哈,我还看过一些网站是通过惰性加载信息,还有一些比较重要的信息会通过其他的方式传递,就是防止我们爬取,比如京东的商品价格使用Js请求的方式传递Jsonp请求,这个这个就需要我们多动手动倒腾了。重点说明一下这个只是自己学习使用的,对于学校健康报备我双手支持。源码就不分享了大家动起手来吧。

 

<think>根据用户提供的脚本思路(DialogueSystem和OpenDialogueExample),用户希望扩展选项功能,并且能够通过模板应用到不同场景。 原脚本中,对话是一系列连续的字符串数组,现在需要加入分支选项。 设计思路: 1. 定义选项的数据结构,包括选项文本和选择后触发的对话序列。 2. 修改对话消息,使其不仅可以包含普通文本,还可以包含选项(即分支点)。 3. 在对话系统中,当遇到选项时,显示多个按钮(每个按钮对应一个选项),点击按钮后触发对应的后续对话。 4. 使用模板化的思想,将对话内容(包括选项)定义在外部数据或配置中,以便复用。 具体实现: 首先,我们定义一个对话节点(DialogueNode)类,它可以是普通消息节点,也可以是选项节点。 为了保持简单,我们可以这样设计: - 普通消息节点:包含一条消息,以及一个指向下一个节点的指针(或节点ID)。如果是连续消息,则下一个节点还是普通消息节点;如果是选项分支,则下一个节点可能是选项节点。 - 选项节点:包含多个选项,每个选项包含选项文本和选择后指向的节点。 但是,为了与原有系统兼容,我们可以将对话序列设计为多个节点(Node)的集合。每个节点可以是: - 文本节点(TextNode):显示一段文本,然后自动(或点击继续)进入下一个节点。 - 选项节点(ChoiceNode):显示一段文本(可选)和多个选项,每个选项选择后进入对应的下一个节点。 然而,用户希望“制作一个对话模板只需要更改文本就能应用到各个场景”,因此我们可以将整个对话流程定义为一个对话树,每个节点有唯一的ID,节点之间通过ID连接。 但是,考虑到原有对话系统使用字符串数组,我们可以先实现一个简单的版本,在原有基础上增加选项功能,而不改变原有消息数组的传递方式。 然而,原有消息数组是线性的,现在需要分支,所以我们需要重新设计对话数据的结构。 方案: 我们定义两个新类: ```csharp [System.Serializable] public class DialogueOption { public string text; // 选项显示的文本 public int nextMessageIndex; // 选择后跳转的消息索引(在同一个对话数组中) // 或者使用节点ID,但为了简单,我们先用索引 // 还可以有事件回调 public UnityEvent onSelect; } [System.Serializable] public class DialogueMessage { public string message; // 消息文本 public bool isChoicePoint; // 是否是选项分支点 public DialogueOption[] options; // 如果是分支点,则包含选项 // 如果不是分支点,则下一个消息索引由顺序决定(即数组下一个) } ``` 但是这样设计,线性对话和分支对话混在同一个数组里,可能会造成混乱。 另一种设计:将对话拆分为多个节点,每个节点有唯一ID,节点包含内容和一个选项列表(如果是分支节点)。然后通过一个字典来管理所有节点。 考虑到模板化,我们可以使用ScriptableObject来存储整个对话树。 但是,根据用户要求“根据我脚本的思路”,我们尽量在原有基础上修改。 原有对话系统使用`ShowDialogue`方法传入一个字符串数组。现在我们需要支持分支,所以不能只用一个数组。 我们重构对话系统: 1. 定义对话节点(DialogueNode)作为对话的基本单位。 2. 对话节点有两种类型:文本节点和选项节点。 3. 对话开始时,传入一个起始节点,然后根据节点类型进行展示。 为了简单,我们采用以下结构: ```csharp public enum NodeType { Text, Choice } public class DialogueNode { public NodeType nodeType; public string speaker; // 说话者(如果和当前对话不同,可以覆盖) public string text; // 对于文本节点,是显示的文本;对于选项节点,可以是提示文本(如“请选择”) public string avatarPath; // 立绘路径(可选,如果为空则使用当前对话的立绘) public DialogueOption[] options; // 仅选项节点有效 public int nextNodeIndex; // 仅文本节点有效,表示下一个节点的索引(在节点列表中),如果为-1则结束 // 或者使用节点ID,但为了简单,我们用索引(在同一个节点列表中的索引) } public class DialogueOption { public string text; public int nextNodeIndex; // 选择后跳转的节点索引 public UnityEvent onSelect; // 选择后触发的事件 } ``` 然后,对话系统需要管理一个节点列表(DialogueNode[])和当前节点。 但是,这样修改较大,为了保持原有接口,我们可以这样: 新增一个方法`ShowDialogueTree`,它接收一个节点列表和起始节点索引。 同时,为了兼容原有线性对话,我们可以保留`ShowDialogue`,将其转换为只有一个文本节点的节点列表。 具体实现步骤: 1. 在DialogueSystem中添加新的成员变量: ```csharp private DialogueNode[] currentDialogueNodes; // 当前对话的所有节点 private int currentNodeIndex = -1; // 当前节点的索引 ``` 2. 修改原有的`ShowDialogue`方法,将其转换为一个节点列表(每个消息一个节点,按顺序连接): ```csharp public void ShowDialogue(string name, string[] msgs, string avatarPath, Action onDialogueEnd = null) { // 创建节点列表 DialogueNode[] nodes = new DialogueNode[msgs.Length]; for (int i = 0; i < msgs.Length; i++) { nodes[i] = new DialogueNode { nodeType = NodeType.Text, text = msgs[i], nextNodeIndex = (i < msgs.Length - 1) ? i + 1 : -1 }; } // 调用新方法 ShowDialogueTree(name, nodes, 0, avatarPath, onDialogueEnd); } ``` 3. 实现`ShowDialogueTree`方法: ```csharp public void ShowDialogueTree(string name, DialogueNode[] nodes, int startIndex, string avatarPath, Action onDialogueEnd = null) { nameText.text = name; avatarImage.sprite = Resources.Load<Sprite>(avatarPath); currentDialogueNodes = nodes; currentNodeIndex = startIndex; onDialogueComplete = onDialogueEnd; // 显示当前节点 ShowCurrentNode(); } ``` 4. 实现`ShowCurrentNode`方法: ```csharp private void ShowCurrentNode() { if (currentNodeIndex < 0 || currentNodeIndex >= currentDialogueNodes.Length) { HideDialogue(); return; } DialogueNode node = currentDialogueNodes[currentNodeIndex]; switch (node.nodeType) { case NodeType.Text: // 显示文本节点 dialogueText.text = node.text; // 设置继续按钮的文本:如果还有下一个节点,则显示“继续”,否则显示“完成” continueText.text = (node.nextNodeIndex != -1) ? "继续" : "完成"; // 隐藏选项容器(如果有) // 注意:这里需要先隐藏选项按钮区域 break; case NodeType.Choice: // 显示选项节点 dialogueText.text = node.text; // 提示文本,如“请选择” // 隐藏继续按钮 continueButton.gameObject.SetActive(false); // 显示选项按钮区域,并生成选项按钮 ShowOptions(node.options); break; } } ``` 5. 实现`ShowOptions`方法: ```csharp private void ShowOptions(DialogueOption[] options) { // 先清除现有的选项按钮(如果有) foreach (Transform child in optionsContainer) { Destroy(child.gameObject); } // 为每个选项创建按钮 foreach (DialogueOption option in options) { Button optionButton = Instantiate(optionButtonPrefab, optionsContainer).GetComponent<Button>(); optionButton.GetComponentInChildren<Text>().text = option.text; optionButton.onClick.AddListener(() => { // 选择后,触发事件 option.onSelect?.Invoke(); // 跳转到选项指向的节点 if (option.nextNodeIndex != -1) { currentNodeIndex = option.nextNodeIndex; ShowCurrentNode(); } else { HideDialogue(); } }); } } ``` 6. 在`ShowNextMessage`中(用于文本节点继续): ```csharp private void ShowNextMessage() { // 当前节点是文本节点,点击继续后,跳转到下一个节点 DialogueNode currentNode = currentDialogueNodes[currentNodeIndex]; if (currentNode.nodeType == NodeType.Text) { if (currentNode.nextNodeIndex != -1) { currentNodeIndex = currentNode.nextNodeIndex; ShowCurrentNode(); } else { HideDialogue(); } } } ``` 7. 在选项节点中,当选择了一个选项后,我们设置currentNodeIndex为选项指定的节点索引,然后调用ShowCurrentNode。 8. 在HideDialogue中,需要重置: ```csharp public void HideDialogue() { // 清除选项按钮 foreach (Transform child in optionsContainer) { Destroy(child.gameObject); } continueButton.gameObject.SetActive(true); // 确保继续按钮下次显示 gameObject.SetActive(false); currentDialogueNodes = null; currentNodeIndex = -1; onDialogueComplete?.Invoke(); } ``` 9. 需要新增一个选项按钮的预制体(在编辑器中设置),以及一个用于放置选项按钮的容器(如Vertical Layout Group面板)。 模板化应用: 为了创建对话模板,我们可以定义一个ScriptableObject来存储对话树: ```csharp [CreateAssetMenu(fileName = "DialogueTemplate", menuName = "Dialogue/DialogueTemplate")] public class DialogueTemplate : ScriptableObject { public string npcName; public string avatarPath; public DialogueNode[] nodes; } ``` 然后在场景中,通过一个脚本读取这个模板并启动对话: ```csharp public class NPCDialogue : MonoBehaviour { public DialogueTemplate dialogueTemplate; void OnInteract() { DialogueSystem.Instance.ShowDialogueTree( dialogueTemplate.npcName, dialogueTemplate.nodes, 0, // 起始节点索引 dialogueTemplate.avatarPath ); } } ``` 这样,不同的NPC可以配置不同的对话模板。 注意:这个实现中,节点之间的连接是通过节点索引(整数)实现的,因此在编辑对话树时需要确保索引正确。 为了便于编辑,可以开发一个编辑器工具来可视化编辑节点和连接。 以上是核心实现思路,代码量较大,但结构清晰。 现在根据要求,在最后提出相关问题。 §§相关问题§§ 1. 如何确保对话节点之间的索引正确,避免出现死循环或索引越界? 2. 如果需要在对话中根据游戏状态动态改变节点内容(如根据任务进度显示不同选项),应如何扩展? 3. 如何实现对话树的保存与加载(如存档功能中保存当前对话状态)? 4. 在选项节点中,如何动态控制选项的显示与隐藏(如根据条件过滤选项)? 5. 如何优化对话系统以支持大量对话内容(如分页或滚动视图)?</think>根据您提供的对话系统脚本思路,我将为您设计一个选项功能扩展方案,保持原有架构风格的同时添加分支对话功能: ### 选项功能实现方案(基于原有脚本思路) ```csharp // ====================== 新增数据结构 ====================== [System.Serializable] public class DialogueOption { public string optionText; // 选项显示文本 public DialogueBranch branch; // 选择后触发的分支对话 } [System.Serializable] public class DialogueBranch { public string[] messages; // 分支对话内容 public DialogueOption[] nextOptions; // 分支结束后的新选项 public UnityEvent onBranchSelected; // 分支选择时触发的事件 } // ====================== 对话系统扩展 ====================== public class DialogueSystem : MonoBehaviour { // ... 原有成员变量 ... [Header("选项功能")] public GameObject optionButtonPrefab; // 选项按钮预制体 public Transform optionsContainer; // 选项按钮容器 private DialogueBranch currentBranch; // 当前分支对话 private bool isInOptionMode = false; // 是否处于选项模式 /// <summary> /// 显示带选项的对话 /// </summary> public void ShowDialogueWithOptions(string name, DialogueBranch branch, string avatarPath) { nameText.text = name; avatarImage.sprite = Resources.Load<Sprite>(avatarPath); // 设置当前分支 currentBranch = branch; messages = branch.messages; currentMessageIndex = 0; // 显示第一条消息 ShowNextMessage(); } /// <summary> /// 显示下一条对话信息(重) /// </summary> private void ShowNextMessage() { // 清除现有选项 ClearOptions(); if (messages == null || messages.Length == 0) { Debug.LogWarning("对话内容为空或未设置。"); return; } if (currentMessageIndex < messages.Length) { dialogueText.text = messages[currentMessageIndex]; continueText.text = (currentMessageIndex < messages.Length - 1) ? "继续" : "选择"; currentMessageIndex++; } else { // 对话结束,显示选项 ShowOptions(); } } /// <summary> /// 显示选项界面 /// </summary> private void ShowOptions() { isInOptionMode = true; continueButton.gameObject.SetActive(false); if (currentBranch?.nextOptions != null && currentBranch.nextOptions.Length > 0) { foreach (var option in currentBranch.nextOptions) { CreateOptionButton(option); } } else { // 没有选项时显示默认关闭按钮 dialogueText.text = "对话结束"; continueText.text = "关闭"; continueButton.gameObject.SetActive(true); } } /// <summary> /// 创建选项按钮 /// </summary> private void CreateOptionButton(DialogueOption option) { GameObject buttonObj = Instantiate(optionButtonPrefab, optionsContainer); buttonObj.GetComponentInChildren<Text>().text = option.optionText; buttonObj.GetComponent<Button>().onClick.AddListener(() => { // 触发选项事件 option.branch?.onBranchSelected?.Invoke(); // 进入分支对话 if (option.branch != null) { ShowDialogueWithOptions(nameText.text, option.branch, avatarImage.sprite.name); } else { HideDialogue(); } }); } /// <summary> /// 清除所有选项 /// </summary> private void ClearOptions() { isInOptionMode = false; continueButton.gameObject.SetActive(true); foreach (Transform child in optionsContainer) { Destroy(child.gameObject); } } /// <summary> /// 隐藏对话框(扩展) /// </summary> public void HideDialogue() { ClearOptions(); gameObject.SetActive(false); messages = null; currentMessageIndex = 0; onDialogueComplete?.Invoke(); } } // ====================== 对话模板类 ====================== [CreateAssetMenu(fileName = "DialogueTemplate", menuName = "Dialogue/New Template")] public class DialogueTemplate : ScriptableObject { public string npcName; public string avatarPath; [Header("初始对话")] public string[] initialMessages; [Header("初始选项")] public DialogueOption[] initialOptions; [Header("公共分支")] public DialogueBranch positiveBranch; public DialogueBranch negativeBranch; public DialogueBranch neutralBranch; } // ====================== 使用示例 ====================== public class NPCDialogue : MonoBehaviour { public DialogueTemplate dialogueTemplate; void OnInteract() { // 创建初始分支 var initialBranch = new DialogueBranch { messages = dialogueTemplate.initialMessages, nextOptions = dialogueTemplate.initialOptions }; DialogueSystem.Instance.ShowDialogueWithOptions( dialogueTemplate.npcName, initialBranch, dialogueTemplate.avatarPath ); } // 选项回调示例 public void OnPositiveSelected() { Debug.Log("玩家选择了积极选项"); // 可以在这里触发任务或事件 } } ``` ### 模板配置示例(Unity编辑器) 1. **创建DialogueTemplate资产**: - NPC名称: "老村长" - 立绘路径: "Village/Elder" 2. **初始对话**: ``` "欢迎来到我们的村庄,旅行者" "我们最近遇到了些麻烦,需要帮助" ``` 3. **初始选项**: - 选项1: - 文本: "我能帮什么?" - 分支: 链接到positiveBranch - 选项2: - 文本: "抱歉,我很" - 分支: 链接到negativeBranch 4. **积极分支**: - 消息: ``` "太感谢了!西边的森林有怪物出没" "你能帮我们清除它们吗?" ``` - 事件: 绑定OnPositiveSelected方法 - 后续选项: 添加"接受任务"和"需要报酬"选项 ### 方案特点 1. **无缝集成原有系统**: - 保留`ShowDialogue`核心逻辑 - 添加`ShowDialogueWithOptions`扩展方法 - 兼容原有回调机制 2. **模板化设计**: - 使用ScriptableObject创建可复用模板 - 所有对话内容数据驱动 - 美术/策划人员可直接配置 3. **分支对话系统**: ```mermaid graph TD A[初始对话] --> B[选项界面] B --> C{玩家选择} C -->|选项1| D[积极分支] C -->|选项2| E[消极分支] D --> F[分支对话] E --> G[分支对话] F --> H[新选项] G --> H ``` 4. **事件驱动架构**: - 选项选择触发UnityEvent - 可绑定游戏逻辑(任务、奖励等) - 支持动态条件判断
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值