我们如何改善办公室午餐的顺序(无法访问服务器)

大家好

我在办公室工作 软件开发人员。 有时我吃饭。 是的,每天。 雇主为我们提供午餐-工人明天订购午餐,明天午餐供应商带来员工订购的东西。 订购的东西和带来的东西并不总是一致的,但事实并非如此。 午餐在午餐订购页面上订购。 但是...

但是首先,关于午餐订单页面的形成方式:供应商发送带有一周价格清单的XLS文件。

图片
供应商发送的价格表示例

晚餐的负责人通过公司肠道中某人开发的实用程序进行解析,并将其转换为公司门户可以显示的形式。 然后他显示出来...

图片
屏幕截图(含晚餐)

图片
午餐订购页面的屏幕截图

职位很不方便地分为几类。 有关名称和组成的信息以全文显示,很难浏览。

我想了解,最好不要点餐,您可以尝试一下,因为其他人都喜欢。 也就是说,我想要一个评分。 我也想在Telegram上收到我的订单,这样我就不记得我在餐厅订购的东西了。

因此目标很明确。 我必须马上说:我和我的同事走的道路远非最正确和合理的。 即使如此:就架构/安全性/支持/容错而言,这是一个完整的游戏。 但是增长的东西已经增长了。

我们无权访问服务器,因此您只能使用用户脚本更改页面的外观。 但是评级呢? 也没有访问数据库的权限。 好吧,我们需要一个服务器来进行订单处理,评分和与Telegram进行交互。 该角色由NodeJS服务器承担。

服务器端


我将处理服务器,并且同事将使用向页面添加功能的用户脚本。 我们使用nodejs服务器,连接express,添加MySQL。 将Sequelize放在最前面 。 我们将通过node-telegram-bot-api与Telegram进行交互:

//    const app = express(); // ... //   //    app.get("/dinners/user_menu", dinner.getUserMenu); //      app.get("/dinners/r/:id", dinner.getPersonalRatings); //   app.post("/dinners/r/:id", dinner.setRating); //      Telegram app.post("/dinners/resend/:id", dinner.resendMessage); //      app.post("/dinners/order", dinner.order); //  ,      app.post("/dinners/days", dinner.setDinnerDays); 

简要介绍功能:
路径/ Dinners / user_menu返回用户脚本:

 res.sendFile(__dirname + '/public_html/user_script.js'); 

这样做是为了避免通过安装脚本的新版本来分散使用它的同事的注意力。 已更正-将其扔到服务器上-每个人都已更新。

是的,我知道从安全角度来看这很糟糕,但是功能本身并不关键,我们将认为存储脚本的服务器相当安全。

此外,沿着/晚餐/ r /:id的路径您可以获得所有职位的评分,并保存该评分,即为菜肴投票。

路径/晚餐/重新发送/:id用于向Telegram发送消息。 该消息的文本是在客户端上生成的,只有与Telegram的交互才在服务器上发生:

 const parseMode: TelegramBot.SendMessageOptions = {parse_mode: "HTML"}; await this.bot.sendMessage(telegramId, htmlMessage, {...options, ...parseMode}); 

之后,机器人会发送带有命令的消息。

图片

接下来,沿着路径/晚餐/订单保存订单 。 由于难以确定原始订单请求(在单击“保存”按钮后,出现带有订单确认按钮的警报),因此,在加载订单页面时,将向具有订单的服务器发送请求(并且站点上的整个订单系统分为两页-订单页面和菜单页面) -特定日期的菜肴选择-即订单的形成)。 每次您进入订单页面时发送请求都是很不合理的,但是没有更好的选择。

最后, / Dinners / days路径设置了订购午餐的日期。 该功能的这一部分已出现,用于正确提醒未完成的订单-您需要知道订单的第二天是什么(星期中有周末和节假日)。 我不用执行生产日历,而只是解析订单页面上的日期,该页面上已经标记了工作日和非工作日(您不能为一个非工作日订购)。 非工作日在门户上使用isHoliday类标记:

 //     const trToday = $(".dinner_today")[0]; const tbodyAllDays = $(trToday).parent(); const dinnerDays = []; $(tbodyAllDays).children().each(async function() { if ($(this).hasClass("isHoliday")) { return; } const itemMenuDate = $(this).find("> td:first-child").text().substring(0, 10); dinnerDays.push(itemMenuDate); // ... }); await sendRequest("POST", `https://****/dinners/days/`, {days: dinnerDays}); 

哦,是的,使用jquery进行选择。 深入到页面树非常方便。

电报机器人


整个插件的另一部分是电报机器人。

图片
具有这种功能

获得ID就是这种识别系统。 要将特定浏览器上的用户脚本与电报中的userId关联。

查看今天的订单,查看订单列表(最后5个),设置提醒。
午餐会在每天的同一时间自动发送给供应商,因此在一定时间(例如13:00)之前下订单很重要。

之后,下订单的能力将被阻止。

温馨提示:

图片
该机器人可以选择提醒时间:9、10或11小时。

而且,如果在提醒您之后没有下订单,则漫游器每隔10分钟会提醒您该订单,直到您下订单或该订单被阻止为止。

这是由cron任务完成的(使用node-schedule ):

 schedule.scheduleJob('*/10 9-13 * * 1-5', async function() { // ... }); 

客户部分。 菜单


我再说一遍,当前界面以及供应商发送的菜单项的文本太糟糕了(请参见屏幕2)。 在某一时刻,您不再看到成吨的单调的纯文本和很少有用的文本。

在搜索可以帮助我们的互联网之后,我们遇到了一个很好的插件,用于自定义Greasemonkey脚本,他们决定使用它。

首先,我们创建一个用户脚本,并授予与公司门户网站和服务器进行通信的权利,在该门户网站上,服务器的等级和发送请求的能力受到限制。

 // @include http://****.int/* // @include http://****/* // @grant GM.xmlHttpRequest 

另外,要修改午餐页面本身,我们使用了jQuery,并使用// @require将其连接

现在让我们开始整理午餐页面。 查看页面的html代码后,我们找到了午餐表的标识符,我们获取了该表并对其进行了修改。

 const table = $(".dinner__innerData"); const categoryList = []; //      $(table).find(“tbody tr td:nth-child(2})”).each(function () { const text = $(this).text(); //           if (!categoryList.find(name => name === text)) { $(this).parent().before("<tr><th colspan='6'>" + text + "</th><th style='display:none'></th><th style='display:none'></th><th style='display:none'>0</th><th style='display:none'><span class='dish__amount'>0</span></th></tr>"); categoryList.push(text); } }); //      $(table).find(“thead th:nth-child(2)”).remove(); $(table).find("tbody tr td:nth-child(2)”).remove(); //     $(table).find(“tbody tr td:nth-child(2)”).after("<td></td>"); $(table).find(“thead th:nth-child(2)”).after("<th class='ui-state-default'></th>"); 

我想指出的是,在午餐安排页面上,在计算订单金额时,会在表的所有行上考虑该问题,即接收到订购商品的数量乘以价格。 由于这些原因,如果您添加带有类别名称的行,则所有内容都会中断...我必须为此行输入数量和金额为零的隐藏列。

现在,让我们继续清洁文本并添加有关盘子评级的信息。 首先,一些助手功能。 评分中的菜是按名称标识的,没有任何垃圾形式的克,标点符号和空格。 也就是说,这道菜叫做“鸡蛋鸡汤(鸡汤,胡萝卜,洋葱,鸡蛋,蔬菜)。” 于100克中:蛋白质3.43; 脂肪2.86; 碳水化合物1.0; en.value-43.39kcal(200gr)“被识别为“肉汤豆腐”。 这是因为供应商可能会在多余的空间,标志和其他东西中爬行。 如实践所示,这足以在90%的情况下准确识别出这道菜,我们决定不打扰并输入全文搜索。

 /** *         * @param items   * @param tdText    * @return   */ function findByName(items, tdText) { tdText = clearTrash(tdText, true, true, true); return items.find(({clear_name}) => { return clear_name.trim().toLowerCase() === tdText; }); } /** *     * @param text  * @param clearDescr       * @param clearGrams    * @return    */ function clearTrash(text, clearDescr, clearGrams, clearSymbols) { //   ,       } 

这就是等级的形成:

 const table = $(".dinner__innerData"); const nameTd = $(table).find(“tr td:nth-child(2)”); for (let index = 0; index <= nameTd.length; index++) { const tdText = $(nameTd[index]).text(); //     const item = findByName(items, tdText); if (item) { let ratingTd = $(nameTd[index]).parent().find(“td:nth-child(2)”)[0]; //           let ratingText = "<i></i> " + parseFloat(item.avgrating).toFixed(1) + " (: " + item.orders + ", : " + item.ratingsCount + ")"; ratingText = item.persrating ? `<b><i></i> ${parseFloat(item.persrating).toFixed(1)} (: ${item.perscount})</b><br>` + ratingText : ratingText; //   $(ratingTd).css({ // getColorRating       background: getColorRating(item.avgrating) }).html(ratingText); } //           //   ,      const grams = getGrams(tdText); //     $(nameTd[index]).html(clearTrash(tdText, false, true, false)); //      ,       $(nameTd[index]).append("<br/><span></span>") .find("span") .append(grams) .css({"font-size": 10}); } 

就是这样。

图片
同意,更好,更方便吗?

客户部分。 投票


接下来,我们将继续添加对已点菜进行投票的功能,并通过电文发送带有定单的消息。

图片
没有脚本的订单页面

在订购菜肴的页面上,添加评分:

 async function addRatingForm() { const table = $(".dinner__innerData"); const nameTd = $(table).find("tr td:nth-child(1)"); //   for (let index = 0; index <= nameTd.length; index++) { const tdText = $(nameTd[index]).text(); $(nameTd[index]).html(clearTrash(tdText, false, true, false)); } //       Telegram $(table).append("<tfoot><tr><th colspan='6' class='rating-buttons btn-group margT0' style='display: table-cell;'></tr></tfoot>"); $(".rating-buttons").prepend(`<input type="submit" value="" class="btn_primary rating-button">`); $(".rating-buttons").prepend(`<input type="submit" value=" Telegram" class="btn_primary send-button">`); //      await diableButtonByDate(); //      for (let index = 0; index <= table.length; index++) { $(table[index]).find("tbody tr td:nth-child(4)").after("<td class='ratingInputTd'><input id='horizontal-spinner' class='ui-spinner-input' style='width:20px;'></td>"); $(table[index]).find("thead th:nth-child(4)").after("<th class='ui-state-default'></th>"); } $(".ui-spinner-input").spinner({ max: 10, min: 1 }); //   $(".rating-button").click(sendRating); $(".send-button").click(sendTelegram); } /** *      ,    */ async function diableButtonByDate() { //             . //              const buttons = $(".rating-button"); for (let index = 0; index <= buttons.length; index++) { const button = $(buttons[index]); const date = button.parent().parent().parent().parent().parent().parent().find("> td:nth-child(1)").text().substring(0, 10); if (await GM.getValue(date)) { button.attr({disabled: "disabled"}); } } } /** *  */ async function sendRating(event) { event.preventDefault(); const items = []; //         $(this).parent().parent().parent().parent().find("tr").each(function () { const tdList = $(this).find("td"); const ratingInput = $(tdList[4]).find("input"); if (!ratingInput.length) { return; } items.push({ count: $(tdList[2]).text(), price: $(tdList[1]).text(), name: $(tdList[0]).text(), rating: ratingInput.val(), }); }); await sendRequest("POST", `https://****/dinners/r/${telegramId}`, items); const menuDate = $(this).parent().parent().parent().parent().parent().parent().find("> td:nth-child(1)").text().substring(0, 10); await GM.setValue(menuDate, true); location.reload(); } 

这就是我们的输出结果:

图片

是的-代码太糟糕了。 是的-未优化。 是的-在某些地方不合逻辑。 但是花费的时间最少,功能和便利性大大增加。

我的目标是使自己和我的同志更加愉快地订购晚餐,而我认为这一目标得以实现。

Source: https://habr.com/ru/post/zh-CN475892/


All Articles