From 46393fa443ad4210ca1132f5f0f526a8c98acd39 Mon Sep 17 00:00:00 2001 From: ZaneYork Date: Thu, 12 Sep 2024 10:24:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8A=BD=E7=AD=BE=E7=AE=97?= =?UTF-8?q?=E6=B3=95=EF=BC=8C=E9=99=8D=E4=BD=8E=E9=9A=8F=E6=9C=BA=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- src/dao.py | 82 ++++++++++++++++++++++++++++------------ src/dinner.py | 87 ++++++++++++++++++++++++------------------- src/utils.py | 25 +++++++++++++ templates/dinner.html | 63 ++++++++++++++++++++++--------- 5 files changed, 179 insertions(+), 81 deletions(-) create mode 100644 src/utils.py diff --git a/.gitignore b/.gitignore index 6a1342e..09d714f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.idea/ *.sqlite3 -**/__pycache__/ \ No newline at end of file +**/__pycache__/ +*.ini diff --git a/src/dao.py b/src/dao.py index 1e0bc2b..e6780f7 100644 --- a/src/dao.py +++ b/src/dao.py @@ -2,7 +2,7 @@ import sqlite3 from datetime import datetime, timedelta from typing import Generator -from flask import request +from utils import get_user db_path = './data.sqlite3' @@ -40,16 +40,7 @@ finally: ddl_db.close() -def get_user() -> str: - """ - 根据访问IP决定用户ID - :return: 用户ID - """ - client_ip = request.remote_addr - return client_ip - - -def get_user_menu() -> str: +def get_user_menu() -> tuple[str, str]: """ 获取当前用户的投票内容 :return: 投票内容 @@ -59,25 +50,26 @@ def get_user_menu() -> str: cursor = db.cursor() datestr = datetime.now().strftime('%Y-%m-%d') try: - cursor.execute("select menu from user_menu where user = ? and datestr=?", (user, datestr)) + cursor.execute("select menu, nickname from user_menu where user = ? and datestr=?", (user, datestr)) row = cursor.fetchone() if row: - return row[0] + return row[0], row[1] # else: # cursor.execute("select menu from user_menu where user = ? order by datestr desc limit 1", (user,)) # row = cursor.fetchone() # if row: # return row[0] - return '' + return '', '' finally: cursor.close() db.close() -def set_user_menu(menu: str, user: str = None) -> None: +def set_user_menu(menu: str, nickname: str, user: str = None) -> None: """ 设置用户投票内容 :param menu: 投票内容 + :param nickname: 姓名 :param user: 用户,默认当前用户 """ datestr = datetime.now().strftime('%Y-%m-%d') @@ -86,8 +78,8 @@ def set_user_menu(menu: 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) values(?,?,?)", - (user, menu, datestr)) + cursor.execute("insert or replace into user_menu(user,menu,datestr,nickname) values(?,?,?,?)", + (user, menu, datestr, nickname)) db.commit() finally: cursor.close() @@ -102,7 +94,7 @@ def fetch_all_user_today_menu() -> Generator[tuple[str, str], None, None]: db = sqlite3.connect(db_path) cursor = db.cursor() try: - cursor.execute("select user, menu from user_menu where menu != '' and datestr=? order by user", + cursor.execute("select nickname, menu from user_menu where menu != '' and datestr=? order by user", (datestr,)) row = cursor.fetchone() while row: @@ -113,6 +105,24 @@ def fetch_all_user_today_menu() -> Generator[tuple[str, str], None, None]: db.close() +def is_valid_user(nickname) -> bool: + """ + 判断是否是白名单姓名 + """ + db = sqlite3.connect(db_path) + cursor = db.cursor() + try: + cursor.execute("select 1 from users where nickname = ?", + (nickname,)) + row = cursor.fetchone() + if row: + return True + return False + finally: + cursor.close() + db.close() + + def fetch_all_menu() -> Generator[tuple[str, str, str], None, None]: """ 获取所有可点的菜单 @@ -130,10 +140,32 @@ def fetch_all_menu() -> Generator[tuple[str, str, str], None, None]: db.close() -def fetch_roll_result(interval: int = 0) -> str | None: +def fetch_roll_result() -> str|None: + """ + 获取N天前的抽签结果 + :return: 抽签结果 + """ + date = datetime.now() + datestr = date.strftime('%Y-%m-%d') + db = sqlite3.connect(db_path) + cursor = db.cursor() + try: + cursor.execute("select value from roll_result where datestr=?", + (datestr, )) + row = cursor.fetchone() + if row is not None: + return row[0] + return None + finally: + cursor.close() + db.close() + + +def fetch_roll_result_list(interval: int = 0, limit: int = 3) -> Generator[str, None, None]: """ 获取N天前的抽签结果 :param interval: 间隔,天 + :param limit: 最多取X条 :return: 抽签结果 """ date = datetime.now() + timedelta(days=interval) @@ -141,12 +173,14 @@ def fetch_roll_result(interval: int = 0) -> str | None: db = sqlite3.connect(db_path) cursor = db.cursor() try: - cursor.execute("select value from roll_result where datestr=?", - (datestr,)) + cursor.execute("select value from roll_result where datestr<=? order by datestr desc limit ?", + (datestr, limit)) row = cursor.fetchone() - if row: - return row[0] - return None + while row is not None: + yield row[0] + if interval != 0: + return + row = cursor.fetchone() finally: cursor.close() db.close() diff --git a/src/dinner.py b/src/dinner.py index bb66bba..077001a 100644 --- a/src/dinner.py +++ b/src/dinner.py @@ -1,31 +1,14 @@ import json -from re import Pattern from itertools import chain from session import app -from flask import render_template, make_response, abort -import re +from flask import render_template, make_response, abort, request import random from dao import * - -MOBILE_AGENTS_PATTERN: Pattern[str] = re.compile( - r"(android|bb\d+|meego).+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|" - r"ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)/|plucker|" - r"pocket|psp|series([46])0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino", - re.IGNORECASE, -) +from utils import is_mobile_request -def is_mobile_request(user_agent: str) -> bool: - """ - 判断是否是移动端 - :param user_agent: UA - :return: 是否 - """ - return bool(MOBILE_AGENTS_PATTERN.search(user_agent)) - - -def fetch_user_menu_summary() -> dict[str, float]: +def fetch_user_menu_summary() -> tuple[dict[str, float], list[str]]: """ 获取今天所有人的投票汇总 :return: 品类->数量 @@ -33,6 +16,7 @@ def fetch_user_menu_summary() -> dict[str, float]: all_menu = 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)) menu_keys = set(chain(*menus)) result = {} for k in menu_keys: @@ -40,8 +24,8 @@ def fetch_user_menu_summary() -> dict[str, float]: for user_menu in menus: for k in user_menu: result[k] += user_menu[k] - return result - return {} + return result, users + return {}, [] def within_time() -> bool: @@ -67,13 +51,13 @@ def check_roll() -> int: return 0 if within_time() else 1 -def vote_reduce(summary: dict[str, float]) -> tuple[dict[str, float], float, str]: +def vote_reduce(summary: dict[str, float], limit: int = 2) -> tuple[dict[str, float], float, list[str]]: """ 按规则对投票结果进行修饰 :param summary: 投票汇总结果 :return: 投票汇总结果 """ - last_result = fetch_roll_result(-1) + last_results = 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 @@ -88,10 +72,11 @@ def vote_reduce(summary: dict[str, float]) -> tuple[dict[str, float], float, str summary[new_name] += summary[name] summary[name] = 0 # 昨日中签项降低权重 - if last_result in summary: - summary[last_result] = summary[last_result] * 7 / 10 + for i, last_result in enumerate(last_results): + if last_result in summary: + summary[last_result] = summary[last_result] * (9 - i) / 10 total_vote = sum(value for value in summary.values()) - return summary, total_vote, last_result + return summary, total_vote, last_results @app.route('/dinner/update') @@ -107,19 +92,24 @@ def dinner_update(): if check_roll() != 0: return make_response(json.dumps(dict(code=-1, data="来晚了,提交失败"))) user_menu = request.args.get('value').strip() + nickname = request.args.get('nickname').strip() + if not nickname: + return make_response(json.dumps(dict(code=-1, data="姓名必须填写"))) + if not is_valid_user(nickname): + abort(403) if not user_menu: - set_user_menu('') + set_user_menu('', nickname) return make_response(json.dumps(dict(code=0, data="OK"))) user_menu = json.loads(user_menu) # 计算总投票数值 summary = sum(abs(int(value)) for value in user_menu.values()) if summary <= 0: - set_user_menu('') + 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)) + set_user_menu(json.dumps(user_menu, ensure_ascii=False), nickname) return make_response(json.dumps(dict(code=0, data="OK"))) @@ -131,38 +121,57 @@ def dinner_roll(): """ if check_roll() != 1: return make_response(json.dumps(dict(code=-1, data="目前不能抽签"))) - summary, _, _ = vote_reduce(fetch_user_menu_summary()) - # 票数乘以100四舍五入取整,投入抽奖池 - pool = list(chain(*[[name] * int(round(summary[name] * 100)) for name in summary])) - # Knuth-Durstenfeld Shuffle算法洗牌: 从后往前依次随机将未乱序元素交换到当前位置,直到所有元素均被打乱 - random.shuffle(pool) - result = pool[0] + result = roll_logic() set_roll_result(result) return make_response(json.dumps(dict(code=0, data="OK"))) +def roll_logic(check=False): + 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]] + if len(items) > 1 and (items[0][1] * 0.9) <= items[1][1]: + if check: + return None + # 票数乘以100四舍五入取整,投入抽奖池 + pool = list(chain(*[[name] * int(round(value * 100)) for name, value in items])) + result = pool[random.randint(0, len(pool))] + return result + elif len(items) > 0: + return items[0][0] + else: + return None + + @app.route('/dinner') def dinner(): """ 主页面 :return: 响应 """ - menu = get_user_menu() + menu, nickname = get_user_menu() if menu: menu = json.loads(menu) else: menu = {} - summary, total_vote, last_result = vote_reduce(fetch_user_menu_summary()) + menus, users = 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))) + if not result: + predict_result = roll_logic(check=True) return render_template('dinner.html', all_choice=all_choice, menu=menu, + nickname=nickname, + users=users, summary=summary, summary_keys=summary_keys, total_vote=total_vote, result=result, + predict_result=predict_result, can_roll=can_roll, - last_result=last_result) + last_results=last_results) diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..0b5aca5 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,25 @@ +import re +from configparser import ConfigParser + +from flask import request + + +config = ConfigParser() +config.read('./config.ini') + +def is_mobile_request(user_agent: str) -> bool: + """ + 判断是否是移动端 + :param user_agent: UA + :return: 是否 + """ + ua_expression = config.get('Settings', 'UA_EXPRESSION') + return bool(eval(ua_expression, {"user_agent": user_agent, "re": re})) + +def get_user() -> str: + """ + 根据访问IP决定用户ID + :return: 用户ID + """ + client_ip = request.remote_addr + return client_ip diff --git a/templates/dinner.html b/templates/dinner.html index c2e4222..2ebe423 100644 --- a/templates/dinner.html +++ b/templates/dinner.html @@ -5,22 +5,28 @@ -
+

每天8:00-17:30间开放匿名投票更新,17:30以后允许发起抽签,抽签结果确定后不可更改

-

随机抽签,按得票数决定中签概率

- {% if last_result %} -

今日{{ last_result }}最终得票数降低30%

- {% endif %} +

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

+ {% for last_result in last_results %} +

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

+ {% endfor %}
-
+
+ 姓名 + +
+
我选择
{% for choice in all_choice %}
开始抽签 -
+
-
    +
      {% for key in summary_keys %} -
    • +
    • {{ key }} {{ '{:.2f}'.format(summary[key] | round(2)) }}票 @@ -50,10 +56,31 @@ {% endfor %}
- + {% if (users|length) > 3 %} +
+ +
    + {% for user in users %} +
  • {{ user }}
  • + {% endfor %} +
+
+ {% endif %} +
@@ -69,14 +96,16 @@ const summary = values.reduce((a, b) => parseInt(a) + parseInt(b)); const spans = $(".percentage") for (let i = 0; i < values.length; i++) { - spans[i].innerHTML = (parseFloat(values[i]) * 100 / summary).toFixed(2); + spans[i].innerHTML = summary > 0 ? (parseFloat(values[i]) * 100 / summary).toFixed(2) : 0; } }); function update() { const data = $('#inputForm').serializeJSON(); + const nickname = data.nickname; + data.nickname = undefined; $.ajax({ - url: 'dinner/update?value=' + JSON.stringify(data), + url: 'dinner/update?nickname=' + nickname + '&value=' + JSON.stringify(data), dataType: 'json', success: function (result) { if (result.code === 0) {