From 33589709c34deca7f90f13be68f34f0b0780c0d9 Mon Sep 17 00:00:00 2001 From: ZaneYork Date: Sat, 14 Sep 2024 07:58:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=8D=E5=90=83=E9=80=89?= =?UTF-8?q?=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + src/dao.py | 50 ++++++++++++-------- src/dinner.py | 80 ++++++++++++++++---------------- src/entities.py | 23 ++++++++++ templates/dinner.html | 103 ++++++++++++++++++++++++++++++------------ 5 files changed, 171 insertions(+), 86 deletions(-) create mode 100644 requirements.txt create mode 100644 src/entities.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7e10602 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +flask diff --git a/src/dao.py b/src/dao.py index 1129b3e..c3f8ac2 100644 --- a/src/dao.py +++ b/src/dao.py @@ -1,13 +1,15 @@ +import json import sqlite3 from datetime import datetime, timedelta from typing import Generator +from entities import * from utils import get_user db_path = './data.sqlite3' -def get_user_menu() -> tuple[str, str]: +def get_user_menu() -> UserMenu: """ 获取当前用户的投票内容 :return: 投票内容 @@ -17,24 +19,29 @@ def get_user_menu() -> tuple[str, str]: cursor = db.cursor() datestr = datetime.now().strftime('%Y-%m-%d') try: - cursor.execute("select menu, nickname from user_menu where user = ? and datestr=?", (user, datestr)) + cursor.execute("select menu, nickname, dislike from user_menu where user = ? and datestr=?", + (user, datestr)) row = cursor.fetchone() if row: - return row[0], row[1] + return UserMenu(user=user, datestr=datestr, menu=json.loads(row[0]), nickname=row[1], + dislike=json.loads(row[2] if row[2] else '[]')) else: - cursor.execute( - "select menu, nickname from user_menu where user = ? and nickname is not null order by datestr desc limit 1", - (user,)) + cursor.execute(''' + select menu, nickname, dislike from user_menu + where user = ? and nickname is not null + order by datestr desc limit 1 + ''', (user,)) row = cursor.fetchone() if row: - return '{}', row[1] - return '', '' + return UserMenu(user=user, datestr=datestr, nickname=row[1], + dislike=json.loads(row[2] if row[2] else '[]')) + return UserMenu(user=user, datestr=datestr) finally: cursor.close() db.close() -def set_user_menu(menu: str, nickname: str, user: str = None) -> None: +def set_user_menu(menu: dict[str, float], nickname: str, dislike: list[str] = None, user: str = None) -> None: """ 设置用户投票内容 :param menu: 投票内容 @@ -47,15 +54,16 @@ def set_user_menu(menu: str, nickname: str, user: str = None) -> None: db = sqlite3.connect(db_path) cursor = db.cursor() try: - cursor.execute("insert or replace into user_menu(user,menu,datestr,nickname) values(?,?,?,?)", - (user, menu, datestr, nickname)) + cursor.execute("insert or replace into user_menu(user,menu,datestr,nickname,dislike) values(?,?,?,?,?)", + (user, json.dumps(menu, ensure_ascii=False), datestr, nickname, + json.dumps(dislike, ensure_ascii=False))) db.commit() finally: cursor.close() db.close() -def fetch_all_user_today_menu() -> Generator[tuple[str, str], None, None]: +def fetch_all_user_today_menu() -> Generator[UserMenu, None, None]: """ 获取所有用户今日的投票内容 """ @@ -63,11 +71,13 @@ def fetch_all_user_today_menu() -> Generator[tuple[str, str], None, None]: db = sqlite3.connect(db_path) cursor = db.cursor() try: - cursor.execute("select nickname, menu from user_menu where menu != '' and datestr=? order by user", - (datestr,)) + cursor.execute('''select nickname, menu, user, dislike from user_menu + where menu is not null and menu != '{}' and datestr=? order by user + ''', (datestr,)) row = cursor.fetchone() while row: - yield row[0], row[1] + yield UserMenu(user=row[2], datestr=datestr, menu=json.loads(row[1]), + nickname=row[0], dislike=json.loads(row[3] if row[3] else '[]')) row = cursor.fetchone() finally: cursor.close() @@ -92,7 +102,7 @@ def is_valid_user(nickname) -> bool: db.close() -def fetch_all_menu() -> Generator[tuple[str, str, str], None, None]: +def fetch_all_menu() -> Generator[Menu, None, None]: """ 获取所有可点的菜单 """ @@ -102,7 +112,7 @@ def fetch_all_menu() -> Generator[tuple[str, str, str], None, None]: cursor.execute("select name, label, expression from menu order by order_num") row = cursor.fetchone() while row: - yield row[0], row[1], row[2] + yield Menu(name=row[0], label=row[1], expression=row[2]) row = cursor.fetchone() finally: cursor.close() @@ -130,7 +140,7 @@ def fetch_roll_result() -> str | None: db.close() -def fetch_roll_result_list(interval: int = 0, limit: int = 3) -> Generator[str, None, None]: +def fetch_roll_result_list(interval: int = 0, limit: int = 3) -> Generator[RollResult, None, None]: """ 获取N天前的抽签结果 :param interval: 间隔,天 @@ -142,11 +152,11 @@ def fetch_roll_result_list(interval: int = 0, limit: int = 3) -> Generator[str, db = sqlite3.connect(db_path) cursor = db.cursor() try: - cursor.execute("select value from roll_result where datestr<=? order by datestr desc limit ?", + cursor.execute("select datestr, value from roll_result where datestr<=? order by datestr desc limit ?", (datestr, limit)) row = cursor.fetchone() while row is not None: - yield row[0] + yield RollResult(datestr=row[0], value=row[1]) row = cursor.fetchone() finally: cursor.close() diff --git a/src/dinner.py b/src/dinner.py index 5358367..6736f80 100644 --- a/src/dinner.py +++ b/src/dinner.py @@ -1,4 +1,3 @@ -import json from itertools import chain from session import app @@ -8,15 +7,16 @@ from dao import * from utils import is_mobile_request -def fetch_user_menu_summary() -> tuple[dict[str, float], list[str]]: +def fetch_user_menu_summary() -> tuple[dict[str, float], list[str], list[tuple[str, list[str]]]]: """ 获取今天所有人的投票汇总 :return: 品类->数量 """ - all_menu = list(fetch_all_user_today_menu()) + all_menu: list[UserMenu] = list(fetch_all_user_today_menu()) if len(all_menu) > 0: - menus = list(map(lambda x: json.loads(x[1]), all_menu)) - users = list(map(lambda x: x[0], all_menu)) + menus = list(map(lambda x: x.menu, all_menu)) + users = list(map(lambda x: x.nickname, all_menu)) + dislikes = list(map(lambda x: (x.nickname, x.dislike), all_menu)) menu_keys = set(chain(*menus)) result = {} for k in menu_keys: @@ -24,8 +24,8 @@ def fetch_user_menu_summary() -> tuple[dict[str, float], list[str]]: for user_menu in menus: for k in user_menu: result[k] += user_menu[k] - return result, users - return {}, [] + return result, users, dislikes + return {}, [], [] def within_time() -> bool: @@ -51,31 +51,32 @@ def check_roll() -> int: return 0 if within_time() else 1 -def vote_reduce(summary: dict[str, float], limit: int = 2) -> tuple[dict[str, float], float, list[str]]: +def vote_reduce(summary: dict[str, float], limit: int = 2) -> tuple[dict[str, float], float, list[RollResult]]: """ 按规则对投票结果进行修饰 :param summary: 投票汇总结果 :param limit: 降低最近N次点餐结果概率 :return: 投票汇总结果 """ - last_results = list(fetch_roll_result_list(-1, limit)) + last_results: list[RollResult] = list(fetch_roll_result_list(-1, limit)) total_vote = sum(value for value in summary.values()) - for menu in fetch_all_menu(): - name, _, expression = menu - if expression is None or name not in summary.keys(): + menus: list[Menu] = list(fetch_all_menu()) + for menu in menus: + if menu.expression is None or menu.name not in summary.keys(): continue # 计算预定路由规则 - new_name = eval(expression, {'total_vote': total_vote}) - if new_name != name: - if summary.get(new_name) is None: - summary[new_name] = summary[name] - else: - summary[new_name] += summary[name] - summary[name] = 0 + new_name = eval(menu.expression, {'total_vote': total_vote}) + if new_name != menu.name: + if new_name is not None: + if summary.get(new_name) is None: + summary[new_name] = summary[menu.name] + else: + summary[new_name] += summary[menu.name] + summary[menu.name] = 0 # 昨日中签项降低权重 for i, last_result in enumerate(last_results): - if last_result in summary: - summary[last_result] = summary[last_result] * (8 - i) / 10 + if last_result.value in summary: + summary[last_result.value] = summary[last_result.value] * (8 - i) / 10 total_vote = sum(value for value in summary.values()) return summary, total_vote, last_results @@ -99,18 +100,20 @@ def dinner_update(): if not is_valid_user(nickname): return make_response(json.dumps(dict(code=-1, data="你不能投票(*^_^*)"))) if not user_menu: - set_user_menu('', nickname) + set_user_menu({}, nickname) return make_response(json.dumps(dict(code=0, data="OK"))) user_menu = json.loads(user_menu) + choice = user_menu['choice'] + dislike = user_menu['dislike'] # 计算总投票数值 - summary = sum(abs(int(value)) for value in user_menu.values()) + summary = sum(abs(int(value)) for value in choice.values()) if summary <= 0: - set_user_menu('', nickname) + set_user_menu({}, nickname) return make_response(json.dumps(dict(code=0, data="OK"))) # 投票数归一化 - for key in user_menu: - user_menu[key] = abs(int(user_menu[key])) / summary - set_user_menu(json.dumps(user_menu, ensure_ascii=False), nickname) + for key in choice: + choice[key] = abs(int(choice[key])) / summary + set_user_menu(choice, nickname) return make_response(json.dumps(dict(code=0, data="OK"))) @@ -128,7 +131,7 @@ def dinner_roll(): def roll_logic(check=False): - menus, _ = fetch_user_menu_summary() + menus, _, _ = fetch_user_menu_summary() summary, _, _ = vote_reduce(menus) sorted_items = sorted(summary.items(), key=lambda item: item[1], reverse=True) items = [item for item in sorted_items[:2]] @@ -151,27 +154,28 @@ def dinner(): 主页面 :return: 响应 """ - menu, nickname = get_user_menu() - if menu: - menu = json.loads(menu) - else: - menu = {} - menus, users = fetch_user_menu_summary() + user_menu = get_user_menu() + menus, users, dislikes = fetch_user_menu_summary() summary, total_vote, last_results = vote_reduce(menus) result = fetch_roll_result() can_roll = (check_roll() == 1) - all_choice = list(map(lambda x: {'name': x[0], 'label': x[1]}, fetch_all_menu())) - summary_keys = list(filter(lambda x: x in summary.keys(), map(lambda y: y['name'], all_choice))) + all_choice = list(fetch_all_menu()) + summary_keys = list(filter(lambda x: x in summary.keys(), map(lambda y: y.name, all_choice))) if not result: predict_result = roll_logic(check=True) + eat_users = [] else: + eat_users = users.copy() + for (n, d) in dislikes: + if result in d: + eat_users.remove(n) predict_result = None recent_results = list(fetch_roll_result_list(-1, 7)) return render_template('dinner.html', all_choice=all_choice, - menu=menu, - nickname=nickname, + user_menu=user_menu, users=users, + eat_users=eat_users, summary=summary, summary_keys=summary_keys, total_vote=total_vote, diff --git a/src/entities.py b/src/entities.py new file mode 100644 index 0000000..7a18be7 --- /dev/null +++ b/src/entities.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass, field + + +@dataclass +class UserMenu: + user: str + datestr: str + nickname: str = '' + menu: dict[str, float] = field(default_factory=dict) + dislike: list[str] = field(default_factory=list) + + +@dataclass +class Menu: + name: str + label: str + expression: str = None + + +@dataclass +class RollResult: + datestr: str + value: str diff --git a/templates/dinner.html b/templates/dinner.html index 23d340d..bbe0351 100644 --- a/templates/dinner.html +++ b/templates/dinner.html @@ -10,28 +10,44 @@

第一名与第二名得票数相差不超过10%时随机抽签,按其得票数决定中签概率,否则选择第一名

投票结果仅供参考,最终解释权归部门总经理、副总经理所有

{% for last_result in last_results %} -

今日{{ last_result }}最终得票数降低{{ 2 + loop.index0 }}0%

+

今日{{ last_result.value }}最终得票数降低{{ 2 + loop.index0 }}0%

{% endfor %}
-
- - +
+
+ + +
-
- 我选择 +
+
+ 我选择 +
{% for choice in all_choice %} -
- - +
+
+
+ + +
+
+
+ + +
{% endfor %}
@@ -39,19 +55,17 @@
    {% for key in summary_keys %} -
  • -
    - {{ key }} - {{ '{:.2f}'.format(summary[key] | round(2)) }}票 - {{ '{:.2f}'.format(summary[key] / total_vote * 100 | round(2)) }}% -
    +
  • + {{ key }} + {{ '{:.2f}'.format(summary[key] | round(2)) }}票 + {{ '{:.2f}'.format(summary[key] / total_vote * 100 | round(2)) }}%
  • {% endfor %}
@@ -66,11 +80,24 @@
{% endif %} + {% if (eat_users|length) %} +
+ +
    + {% for user in eat_users %} +
  • {{ user }}
  • + {% endfor %} +
+
+ {% endif %}
-
    +
      {% for recent_result in recent_results %} -
    • {{ recent_result }}
    • +
    • + {{ recent_result.value }} + {{ recent_result.datestr }} +
    • {% endfor %}
@@ -103,16 +130,34 @@ return false; } }); - $("input[type=range]").change(function () { - const values = $("input[type=range]").map(function () { + const rangesInput = $("input[type=range]"); + const checkBoxs = $("input[type=checkbox]") + rangesInput.change(function () { + const values = rangesInput.map(function () { return $(this).val(); }).get(); const summary = values.reduce((a, b) => parseInt(a) + parseInt(b)); const spans = $(".percentage") for (let i = 0; i < values.length; i++) { spans[i].innerHTML = summary > 0 ? (parseFloat(values[i]) * 100 / summary).toFixed(2) : 0; + if (values[i] > 0 && checkBoxs[i].checked) { + checkBoxs[i].checked = false; + {#$(checkBoxs[i]).trigger('blur');#} + } } }); + checkBoxs.change(function () { + const data = $('#inputForm').serializeJSON(); + const values = checkBoxs.map(function () { + return $(this).val(); + }).get(); + for (let i = 0; i < values.length; i++) { + if (data.dislike.includes(values[i])) { + $(rangesInput[i]).val(0); + } + } + $(rangesInput[0]).trigger('change'); + }); function update() { const data = $('#inputForm').serializeJSON(); @@ -132,8 +177,10 @@ } function clearValue() { + const data = $('#inputForm').serializeJSON(); + const nickname = data.nickname; $.ajax({ - url: 'dinner/update?value=', + url: 'dinner/update?nickname=' + nickname + '&value=', dataType: 'json', success: function (result) { if (result.code === 0) {