Browse Source

feat: 使用itsdangerous生成收货码

SongZihuan 3 years ago
parent
commit
26fe278344

+ 18 - 8
app/auth/views.py

@@ -74,7 +74,8 @@ def about():
     else:
         count = math.ceil(current_user.get_garbage_list_count() / 10)
         garbage_list = current_user.get_garbage_list(limit=10, offset=(page - 1) * 10)
-        return render_template("auth/about.html", order=user.order, order_list=user.get_order_goods_list(),
+        order_list = user.get_order_goods_list()
+        return render_template("auth/about.html", order=user.order, order_list=order_list,
                                garbage_list=garbage_list, page_list=get_page("auth.about", page, count), page=page)
 
 
@@ -84,13 +85,22 @@ def order_qr():
     user: web_user.WebUser = current_user
     user.update_info()
 
-    order, user = user.get_qr_code()
-    image = qrcode.make(data=url_for("store.check", user=user, order=order, _external=True))
-    img_buffer = BytesIO()
-    image.save(img_buffer, format='JPEG')
-    byte_data = img_buffer.getvalue()
-    base64_str = base64.b64encode(byte_data).decode("utf-8")
-    return render_template("auth/qr.html", qr_base64=base64_str, order=order)
+    order, user, token = user.get_qr_code()
+
+    check_image = qrcode.make(data=url_for("store.check", user=user, order=order, _external=True))
+    check_img_buffer = BytesIO()
+    check_image.save(check_img_buffer, format='JPEG')
+    check_qr_data = check_img_buffer.getvalue()
+    check_qr_base64 = base64.b64encode(check_qr_data).decode("utf-8")
+
+    confirm_image = qrcode.make(data=url_for("store.confirm", token=token, _external=True))
+    confirm_img_buffer = BytesIO()
+    confirm_image.save(confirm_img_buffer, format='JPEG')
+    confirm_qr_data = confirm_img_buffer.getvalue()
+    confirm_qr_base64 = base64.b64encode(confirm_qr_data).decode("utf-8")
+
+    return render_template("auth/qr.html", check_qr_base64=check_qr_base64,
+                           confirm_qr_base64=confirm_qr_base64, order=order)
 
 
 def creat_auth_website(app_: Flask):

+ 8 - 17
app/static/styles/base.css

@@ -5,7 +5,9 @@ html {
 }
 
 body {
+    position: relative;
     overflow-y: scroll; /* 总是显示滚动条 */
+    min-height: 83vh; /* 设置最小高度为整个窗口的高度 */
 }
 
 /* 顶菜单栏 */
@@ -125,31 +127,20 @@ a.nav-top-item:hover, a.nav-top-item:active {
 }
 
 #nav-bottom {
-    position: fixed;
-    top: 95%;
+    position: absolute;
+    bottom: 0;
+
     background-color: black;
     text-align: center;
     vertical-align: center;
     width: 100%;
     z-index: 10;
-}
-
-@media all and (max-width: 992px) {
-    /* 小屏幕(手机) */
-    #nav-bottom {
-        height: 8%;
-    }
-}
-
-@media not all and (max-width: 992px) {
-    #nav-bottom {
-        height: 5%;
-    }
+    font-size: 15px;
+    height: 5vh;
 }
 
 .nav-bottom-item {
     display: block;
-    font-size: 15px;
     color: #FFFFFF;
 }
 
@@ -164,7 +155,7 @@ a.nav-top-item:hover, a.nav-top-item:active {
 }
 
 #last-p {
-    padding-top: 150px;
+    padding-top: 7vh;
 }
 
 /* 用于解决父元素高度坍塌 */

+ 11 - 0
app/static/styles/store/goods.css

@@ -0,0 +1,11 @@
+.goods-title {
+    width: 100%;
+    text-align: center;
+    font-size: 22px;
+}
+
+.goods-info {
+    width: 100%;
+    text-align: center;
+    font-size: 20px;
+}

+ 25 - 5
app/store/views.py

@@ -1,11 +1,13 @@
 from flask import render_template, Blueprint, Flask, redirect, url_for, abort, flash
 from wtforms import TextField, SubmitField
 from flask_login import current_user
-from wtforms.validators import DataRequired, NumberRange
+from wtforms.validators import DataRequired
 from flask_wtf import FlaskForm
 from flask_login import login_required
 import functools
+from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
 
+from conf import Config
 from tool.type_ import Optional
 from app import views
 from app import web_user
@@ -15,8 +17,7 @@ app: Optional[Flask] = None
 
 
 class BuyForm(FlaskForm):
-    quantity = TextField(validators=[DataRequired(message="请输入兑换数量"),
-                                     NumberRange(1, 11, message="一次性只能兑换1-10个")])
+    quantity = TextField(validators=[DataRequired(message="请输入兑换数量")])
     submit = SubmitField()
 
 
@@ -64,6 +65,7 @@ def manager_required(f):
         if not current_user.is_manager():
             abort(403)
         return f(*args, **kwargs)
+
     return func
 
 
@@ -71,9 +73,27 @@ def manager_required(f):
 @login_required
 @manager_required
 def check(user, order):
-    if not views.website.check_order(order, user):
+    res, uid = views.website.check_order(order, user)
+    if res is None:
+        abort(404)
+    return render_template("store/goods.html", goods_list=res, goods_user=uid[:Config.show_uid_len], order_id=order)
+
+
+@store.route('/confirm/<string:token>')
+@login_required
+@manager_required
+def confirm(token):
+    try:
+        s = Serializer(Config.passwd_salt, expires_in=3600)  # 3h有效
+        data = s.loads(token)
+        order = data["order"]
+        user = data["uid"]
+    except:
         abort(404)
-    flash(f"订单: {order} 处理成功")
+    else:
+        if not views.website.confirm_order(order, user):
+            abort(404)
+        flash(f"订单: {order} 处理成功")
     return redirect(url_for("hello.index"))
 
 

+ 1 - 0
app/templates/auth/login.html

@@ -7,6 +7,7 @@
 
 {% block title %} 用户登录 {% endblock %}
 {% block h1_title %} 用户登录 {% endblock %}
+{% block bnav %} {# 不显示底边栏 #} {% endblock %}
 
 {% block content %}
     <form method="post" action="{{ url_for('auth.login') }}">

+ 5 - 1
app/templates/auth/qr.html

@@ -8,8 +8,12 @@
 
 {% block title %} 取件码 {% endblock %}
 {% block h1_title %} 取件码 {% endblock %}
+{% block bnav %} {# 不显示底边栏 #} {% endblock %}
 
 {% block content %}
     <p class="order"> 订单号: {{ order }} </p>
-    <img class="qr" src="data:image/jpeg;base64,{{ qr_base64 }}" alt="取件码">
+    <img class="qr" src="data:image/jpeg;base64,{{ check_qr_base64 }}" alt="取件码">
+
+    <p class="order"> 确认码: {{ order }} </p>
+    <img class="qr" src="data:image/jpeg;base64,{{ confirm_qr_base64 }}" alt="确认码">
 {% endblock %}

+ 1 - 1
app/templates/macro.html

@@ -33,7 +33,7 @@
         }
 
     </style>
-    <ul id="page-list">
+    <ul id="page-list" class="clearfix">
         {% for line in info_lines %}
             {% if line %}
                 <li class="page-list-item"><a

+ 22 - 0
app/templates/store/goods.html

@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+{% import "store/store_macro.html" as store %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/store/goods.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block title %} 获取清单 {% endblock %}
+{% block h1_title %} 获取清单 {% endblock %}
+
+{% block content %}
+    <section>
+        <p class="goods-title"> 用户: {{ goods_user }} </p>
+        <p class="goods-title"> 订单号: {{ order_id }} </p>
+    </section>
+    <hr>
+    <ul>
+        {{ store.get_goods_item(goods_list) }}
+    </ul>
+
+{% endblock %}

+ 6 - 0
app/templates/store/store_macro.html

@@ -27,4 +27,10 @@
             <p class="store-item-info" style="text-align: center"> 啥都没有 </p>
         </section>
     {% endif %}
+{% endmacro %}
+
+{% macro get_goods_item(info_lines) %}
+    {% for line in info_lines %}
+        <li class="goods-info"> {{ line }} </li>
+    {% endfor %}
 {% endmacro %}

+ 24 - 4
app/web.py

@@ -5,7 +5,7 @@ import math
 
 from conf import Config
 
-from sql.store import get_store_item_list, get_store_item, check_order
+from sql.store import get_store_item_list, get_store_item, confirm_order
 
 from tool.type_ import *
 from tool.page import get_page
@@ -109,8 +109,28 @@ class StoreWebsite(WebsiteBase):
             return goods
         return web_goods.Goods(*goods)
 
-    def check_order(self, order_id: int, uid: uid_t) -> bool:
-        return check_order(order_id, uid, self._db)
+    def check_order(self, order, uid) -> Tuple[Optional[list], Optional[str]]:
+        cur = self._db.search(columns=["UserID"],
+                              table="orders",
+                              where=[f"OrderID='{order}'", f"UserID='{uid}'"])
+        if cur is None or cur.rowcount != 1:
+            return None, None
+        uid = cur.fetchone()[0]
+
+        cur = self._db.search(columns=["Name", "Quantity"],
+                              table="order_goods_view",
+                              where=f"OrderID = '{order}'")
+        if cur is None:
+            return None, None
+
+        res = []
+        for i in range(cur.rowcount):
+            re = cur.fetchone()
+            res.append(f"#{i} {re[0]} x {re[1]}")
+        return res, uid
+
+    def confirm_order(self, order_id: int, uid: uid_t) -> bool:
+        return confirm_order(order_id, uid, self._db)
 
 
 class RankWebsite(WebsiteBase):
@@ -133,7 +153,7 @@ class RankWebsite(WebsiteBase):
         res = []
         for index in range(cur.rowcount):
             i = cur.fetchone()
-            res.append((f"{offset + index + 1}", i[1], i[0][:Config.tk_show_uid_len], str(i[3]), str(i[2])))
+            res.append((f"{offset + index + 1}", i[1], i[0][:Config.show_uid_len], str(i[3]), str(i[2])))
         return res, get_page(f"rank.{url}", page, count)
 
 

+ 5 - 2
app/web_user.py

@@ -1,4 +1,5 @@
 from flask_login import UserMixin, AnonymousUserMixin
+from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
 
 from conf import Config
 
@@ -97,7 +98,7 @@ class WebUser(UserMixin):
 
     @property
     def uid(self):
-        return self._uid[:Config.tk_show_uid_len]
+        return self._uid[:Config.show_uid_len]
 
     @property
     def order(self) -> str:
@@ -113,7 +114,9 @@ class WebUser(UserMixin):
         return self.group == "管理员"
 
     def get_qr_code(self):
-        return self.order, self._uid
+        s = Serializer(Config.passwd_salt, expires_in=3600)  # 3h有效
+        token = s.dumps({"order": f"{self.order}", "uid": f"{self._uid}"})
+        return self.order, self._uid, token
 
     def get_order_goods_list(self):
         order = self.order

+ 1 - 7
conf/sys_default.py

@@ -19,6 +19,7 @@ class ConfUserRelease:
 class ConfigSystemRelease:
     base_location = "Guangdong-KZ"
     search_reset_time = 10  # 搜索间隔的时间
+    show_uid_len = 12  # 展示uid的长度
     about_info = f'''
 HGSSystem is Garbage Sorting System
 
@@ -42,16 +43,9 @@ HGSSystem 版权归属 SuperHuan
     '''.strip()
 
 
-class ConfigExportRelease:
-    qr_show_uid_len = 12  # qr 码上展示uid的长度
-
-
 class ConfigTkinterRelease:
     tk_refresh_delay = 50  # 延时任务的时间
 
-    tk_show_uid_len = ConfigExportRelease.qr_show_uid_len  # tk 界面上展示uid的长度
-    ranking_tk_show_uid_len = tk_show_uid_len  # tk ranking 界面上展示uid的长度
-
     tk_second_win_bg = "#fffffb"  # tkinter 第二窗口 标准颜色
     tk_win_bg = "#F0FFF0"  # tkinter 一般窗口 标准颜色 蜜瓜绿
     tk_btn_bg = "#dcdcdc"  # tkinter 按钮 标准颜色

+ 1 - 1
equipment/scan_user.py

@@ -46,7 +46,7 @@ def make_uid_image(uid: uid_t, name: uname_t, path: str):
     res = qr.make_img(path)
     if not res:
         return False
-    write_text((60, 5), "noto", f"User: {name} {uid[0: Config.qr_show_uid_len]}", path)
+    write_text((60, 5), "noto", f"User: {name} {uid[0: Config.show_uid_len]}", path)
     return True
 
 

+ 6 - 0
setup.sql

@@ -183,6 +183,12 @@ SELECT (TO_DAYS(NOW()) - TO_DAYS(UseTime)) AS days,
 FROM garbage
 WHERE TO_DAYS(NOW()) - TO_DAYS(UseTime) < 30;
 
+DROP VIEW IF EXISTS order_goods_view;
+CREATE VIEW order_goods_view AS
+SELECT ordergoods.OrderID AS OrderID, ordergoods.GoodsID AS GoodsID, ordergoods.Quantity AS Quantity, goods.Name AS Name
+FROM ordergoods
+         JOIN goods on ordergoods.GoodsID = goods.GoodsID;
+
 DROP VIEW IF EXISTS context_user;
 CREATE VIEW context_user AS
 SELECT context.ContextID, context.Context, context.Time, user.UserID, user.Name

+ 1 - 1
sql/store.py

@@ -48,7 +48,7 @@ def write_goods(goods_id: int, quantity: int, order_id: int, db: DB):
     return True
 
 
-def check_order(order: int, uid: uid_t, db: DB) -> bool:
+def confirm_order(order: int, uid: uid_t, db: DB) -> bool:
     cur = db.search(columns=["OrderID"],
                     table="orders",
                     where=[f"OrderID={order}", f"UserID='{uid}'", "Status=0"])

+ 1 - 1
tk_ui/ranking.py

@@ -347,7 +347,7 @@ class RankingStation(RankingStationBase):
         for i, info in enumerate(rank_info):
             no, name, uid, score, eval_, color = info
             self.rank_var[i].set(f"NO.{no}  {name}\n\n"  # 中间空一行 否则中文字体显得很窄
-                                 f"ID: {uid[0:Config.ranking_tk_show_uid_len]}  "
+                                 f"ID: {uid[0:Config.show_uid_len]}  "
                                  f"信用: {eval_} 积分: {score}")
             if color is None:
                 self.rank_label[i]['bg'] = "#F5FFFA"

+ 5 - 5
tk_ui/station.py

@@ -235,20 +235,20 @@ class GarbageStationBase(TkEventMain, metaclass=abc.ABCMeta):
         garbage_type = GarbageType.GarbageTypeStrList_ch[int(info['type'])]
         if self._garbage.is_check()[0]:
             time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(info['use_time'])))
-            check = f'Checker is {info["checker"][0:Config.tk_show_uid_len]}\n'
+            check = f'Checker is {info["checker"][0:Config.show_uid_len]}\n'
             if info["check"] == '1':
                 check += f'检查结果为 投放正确\n'
             else:
                 check += f'检查结果为 投放错误\n'
             self.show_msg("垃圾袋信息", (f"垃圾类型为 {garbage_type}\n"
-                                    f"用户是 {info['user'][0:Config.tk_show_uid_len]}\n"
+                                    f"用户是 {info['user'][0:Config.show_uid_len]}\n"
                                     f"地址:\n  {info['loc']}\n"
                                     f"{check}"
                                     f"使用日期:\n  {time_str}"), show_time=5.0)  # 遮蔽Pass和Fail按键
         elif self._garbage.is_use():
             time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(info['use_time'])))
             self.show_msg("垃圾袋信息", (f"垃圾类型为 {garbage_type}\n"
-                                    f"用户是 {info['user'][0:Config.tk_show_uid_len]}\n"
+                                    f"用户是 {info['user'][0:Config.show_uid_len]}\n"
                                     f"地址:\n  {info['loc']}\n"
                                     f"垃圾袋还未检查\n"
                                     f"使用日期:\n  {time_str}"), big=False, show_time=5.0)  # 不遮蔽Pass和Fail按键
@@ -1089,7 +1089,7 @@ class GarbageStation(GarbageStationBase):
         for i, info in enumerate(rank_info):
             no, name, uid, score, eval_, color = info
             self._rank_var[i + 1].set(f"NO.{no}  {name}\n\n"  # 中间空一行 否则中文字体显得很窄
-                                      f"ID: {uid[0:Config.ranking_tk_show_uid_len]}  "
+                                      f"ID: {uid[0:Config.show_uid_len]}  "
                                       f"信用: {eval_} 积分: {score}")
             if color is None:
                 self._rank_label[i + 1]['bg'] = "#F5FFFA"
@@ -1239,7 +1239,7 @@ class GarbageStation(GarbageStationBase):
             if uid_get is None or len(uid_get) < 32:
                 uid.set('error')
             else:
-                uid.set(uid_get[0:Config.tk_show_uid_len])
+                uid.set(uid_get[0:Config.show_uid_len])
             eval_.set(user_info.get('reputation'))
             rubbish.set(user_info.get('rubbish'))
             score.set(user_info.get('score'))