Ver código fonte

feat: 新增积分商城

SongZihuan 3 anos atrás
pai
commit
a0a135639b

+ 11 - 1
app/auth/web.py

@@ -6,6 +6,7 @@ from conf import Config
 from tool.login import create_uid
 from tool.type_ import *
 
+from core.user import User
 from core.garbage import GarbageType
 
 from sql import DBBit
@@ -65,12 +66,20 @@ class WebUser:
     def get_garbage_list(self):
         return views.auth_website.get_user_garbage_list(self._uid, limit=20)
 
+    def get_user(self) -> User:
+        res = views.auth_website.get_user_by_id(self._uid)
+        return res
+
 
 class AuthWebsite:
     def __init__(self, app: Flask, db: DB):
         self._app = app
         self._db = db
 
+    @property
+    def db(self):
+        return self._db
+
     def load_user_by_name(self, name: uname_t, passwd: passwd_t) -> Optional[WebUser]:
         user = find_user_by_name(name, passwd, self._db)
         if user is None:
@@ -110,4 +119,5 @@ class AuthWebsite:
         return res
 
     def get_user_by_id(self, uid: uid_t):
-        return find_user_by_id(uid, self._db)
+        res = find_user_by_id(uid, self._db)
+        return res

+ 4 - 0
app/rank/web.py

@@ -10,6 +10,10 @@ class RankWebsite:
         self._db = db
         self.app = app
 
+    @property
+    def db(self):
+        return self._db
+
     def get_rank(self, page: int, order_by: str = "DESC") -> Optional[List[Tuple]]:
         offset = 20 * (page - 1)
         cur = self._db.search(columns=['UserID', 'Name', 'Score', 'Reputation'],

+ 55 - 0
app/static/styles/store/store.css

@@ -0,0 +1,55 @@
+#store {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: center;
+    align-items: flex-start;
+}
+
+#info {
+    font-size: 20px;
+    line-height: 25px;
+    height: 25px;
+    margin-bottom: 10px;
+}
+
+#info-score {
+    margin-right: 10px;
+    float: right;
+}
+
+#store-item {
+    width: 200px;
+    background-color: rgb(224, 255, 255);
+    border-radius: 10px;
+    border: 2px rgb(224, 255, 255) ridge;
+    padding: 10px;
+    margin: 5px 5px;
+}
+
+.store-item-info {
+    display: block;
+    font-size: 20px;
+    margin-bottom: 5px;
+}
+
+.store-item-title {
+    font-weight: bold;
+}
+
+label.store-item-num {
+    display: block;
+    font-size: 20px;
+}
+
+input.store-item-num {
+    font-size: 20px;
+    width: 100px;
+    margin-bottom: 5px;
+}
+
+.store-item-submit {
+    display: block;
+    font-size: 20px;
+    width: 100%;
+    margin: 5px auto;
+}

+ 0 - 0
app/store/__init__.py


+ 63 - 0
app/store/views.py

@@ -0,0 +1,63 @@
+from flask import render_template, Blueprint, Flask, redirect, url_for, abort, flash
+from wtforms import TextField, SubmitField
+from flask_login import current_user
+from flask_wtf import FlaskForm
+
+from sql.db import DB
+
+from tool.type_ import Optional
+from . import web
+from ..auth.web import WebUser
+
+store = Blueprint("store", __name__)
+app: Optional[Flask] = None
+store_web: Optional[web.StoreWebsite] = None
+
+
+class BuyForm(FlaskForm):
+    quantity = TextField()
+    submit = SubmitField()
+
+
+@store.route('/buy/<int:goods_id>', methods=['GET', 'POST'])
+def buy(goods_id: int):
+    form = BuyForm()
+    if form.validate_on_submit():
+        try:
+            quantity = int(form.quantity.data)
+        except (TypeError, ValueError):
+            flash("请输入正确的数量")
+        else:
+            goods = store_web.get_goods(goods_id)
+            if goods is None:
+                flash("商品错误")
+            res, order_id = goods.buy_for_user(quantity, current_user)
+            if res == -1:
+                flash("用户不支持兑换商品")
+            elif res == -2:
+                flash("兑换数目超出库存")
+            elif res == -3:
+                flash("积分不足")
+            elif res == 0:
+                flash(f"商品兑换成功, 订单: {order_id}")
+            else:
+                flash("未知错误")
+            return redirect(url_for("store.index"))
+    abort(404)
+
+
+@store.route('/index', methods=['GET', 'POST'])
+def index():
+    form = BuyForm()
+    store_list = store_web.get_store_list()
+    user: WebUser = current_user
+    user.update_info()
+    return render_template("store/store.html", store_list=store_list, store_form=form)
+
+
+def creat_store_website(app_: Flask, db: DB):
+    global store_web, app
+    if store_web is None:
+        app = app_
+        app.register_blueprint(store, url_prefix="/store")
+        store_web = web.StoreWebsite(db, app)

+ 70 - 0
app/store/web.py

@@ -0,0 +1,70 @@
+from flask import Flask
+
+from sql.db import DB
+from sql.user import update_user
+from sql.store import get_store_item_list, get_store_item, update_goods, get_order_id, write_goods
+from tool.type_ import *
+
+from core.user import User, UserNotSupportError
+
+from . import views
+from ..auth import web as auth_web
+from ..auth import views as auth_views
+
+
+class Goods:
+    def __init__(self, name: str, score: score_t, quantity: int, goods_id: int):
+        self._name = name
+        self._quantity = quantity
+        self._score = score
+        self._id = goods_id
+
+    def buy_for_user(self, quantity: int, web_user: auth_web.WebUser) -> Tuple[int, int]:
+        assert quantity > 0
+        user: User = web_user.get_user()
+        if user is None:
+            return -4, 0
+
+        score_ = quantity * self._score
+        if quantity > self._quantity or quantity == 0:
+            return -2, 0
+        try:
+            score = user.get_score()
+        except UserNotSupportError:
+            return -1, 0
+        if score < score_:
+            return -3, 0
+
+        user.add_score(-score_)
+        update_user(user, auth_views.auth_website.db)
+
+        self._quantity -= quantity
+        update_goods(self._id, self._quantity, views.store_web.db)
+
+        order_id = get_order_id(user.get_uid(), views.store_web.db)
+        if order_id is None:
+            return -4, 0
+
+        if not write_goods(self._id, quantity, order_id, views.store_web.db):
+            return -4, 0
+        return 0, order_id
+
+
+class StoreWebsite:
+    def __init__(self, db: DB, app: Flask):
+        self._db = db
+        self.app = app
+
+    @property
+    def db(self):
+        return self._db
+
+    def get_store_list(self) -> Optional[List]:
+        return get_store_item_list(self._db)
+
+    def get_goods(self, goods_id: int):
+        goods = get_store_item(goods_id, self._db)  # 返回值 ["Name", "Score", "Quantity", "GoodsID"]
+        if goods is None:
+            return goods
+        print(goods)
+        return Goods(*goods)

+ 1 - 1
app/templates/base.html

@@ -41,7 +41,7 @@
             <li class="nav-top-item"><a class="nav-top-item" href="{{ url_for('rank.rank_up', page=1) }}"> 高分榜 </a></li>
             <li class="nav-top-item"><a class="nav-top-item" href="{{ url_for('rank.rank_down', page=1) }}"> 警告榜 </a>
             </li>
-            <li class="nav-top-item"><a class="nav-top-item"> 积分商城 </a></li>
+            <li class="nav-top-item"><a class="nav-top-item" href="{{ url_for('store.index') }}"> 积分商城 </a></li>
             <li class="nav-top-item"><a class="nav-top-item"> 新闻 </a></li>
             {% if current_user.is_authenticated %}
                 <li class="nav-top-item"><a class="nav-top-item" href="{{ url_for('auth.about') }}">

+ 23 - 0
app/templates/store/store.html

@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+{% import "store/store_macro.html" as store %}
+
+{% block style %}
+    {{ super() }}
+    <link href="{{ url_for('static', filename='styles/store/store.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block title %} 积分商城 {% endblock %}
+{% block h1_title %} 积分商城 {% endblock %}
+
+{% block content %}
+    <div id="info">
+        {% if current_user.is_anonymous %}
+            <p id="info-score"> 未登录 </p>
+        {% else %}
+            <p id="info-score"> 当前积分: {{ current_user.score }} </p>
+        {% endif %}
+    </div>
+    <section id="store">
+        {{ store.get_store_item(store_list, store_form) }}
+    </section>
+{% endblock %}

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

@@ -0,0 +1,28 @@
+{% macro _get_store_item(infos, store_form) %}
+    <section id="store-item">
+        <p class="store-item-info store-item-title"> {{ infos[0] }} </p>
+        <p class="store-item-info"> 消耗 {{ infos[1] }} 积分</p>
+        <p class="store-item-info"> 剩余 {{ infos[2] }} 件 </p>
+        <hr>
+        <form method="post" action="{{ url_for("store.buy", goods_id=infos[3]) }}">
+            {{ store_form.hidden_tag() }}
+            <label class="store-item-num">
+                个数:
+                {{ store_form.quantity(type="number", class="store-item-num", placeholder="个数", value="1") }}
+            </label>
+            {{ store_form.submit(value="兑换", class="store-item-submit") }}
+        </form>
+    </section>
+{% endmacro %}
+
+{% macro get_store_item(info_lines, store_form) %}
+    {% if info_lines %}
+        {% for line in info_lines %}
+            {{ _get_store_item(line, store_form) }}
+        {% endfor %}
+    {% else %}
+        <section id="store-item">
+            <p class="store-item-info" style="text-align: center"> 啥都没有 </p>
+        </section>
+    {% endif %}
+{% endmacro %}

+ 2 - 0
app/views.py

@@ -2,6 +2,7 @@ from flask import Flask
 from app.index.views import creat_hello_website
 from app.rank.views import creat_ranking_website
 from app.auth.views import creat_auth_website
+from app.store.views import creat_store_website
 
 from sql.db import DB
 
@@ -10,3 +11,4 @@ def register(app: Flask, db: DB):
     creat_hello_website(app)
     creat_ranking_website(app, db)
     creat_auth_website(app, db)
+    creat_store_website(app, db)

+ 6 - 0
core/user.py

@@ -76,6 +76,9 @@ class User(metaclass=abc.ABCMeta):
     def evaluate(self, is_right: bool) -> score_t:
         raise UserNotSupportError
 
+    def get_score(self):
+        raise UserNotSupportError
+
     def add_score(self, score: score_t) -> score_t:
         raise UserNotSupportError
 
@@ -187,6 +190,9 @@ class NormalUser(User):
 
         return reputation
 
+    def get_score(self):
+        return self._score
+
     def add_score(self, score: score_t) -> score_t:
         try:
             self._lock.acquire()

+ 0 - 1
setup.py

@@ -76,7 +76,6 @@ if res == 'Y' or res == 'y':
         for s in all_sql:
             if s.strip() == "":
                 continue
-            print(f"{s};")
             cursor.execute(f"{s};")
         sql.commit()
 

+ 33 - 6
setup.sql

@@ -5,10 +5,10 @@ USE hgssystem;
 CREATE TABLE IF NOT EXISTS user -- 创建用户表
 (
     ID         INT PRIMARY KEY AUTO_INCREMENT,
-    UserID     char(32)    NOT NULL UNIQUE CHECK (UserID REGEXP '[a-zA-Z0-9]{32}'),
-    Name       varchar(50) NOT NULL,
-    IsManager  bit         NOT NULL DEFAULT 0 CHECK (IsManager IN (0, 1)),
-    Phone      char(11)    NOT NULL CHECK (Phone REGEXP '[0-9]{11}'),
+    UserID     CHAR(32)    NOT NULL UNIQUE CHECK (UserID REGEXP '[a-zA-Z0-9]{32}'),
+    Name       VARCHAR(50) NOT NULL,
+    IsManager  BIT         NOT NULL DEFAULT 0 CHECK (IsManager IN (0, 1)),
+    Phone      CHAR(11)    NOT NULL CHECK (Phone REGEXP '[0-9]{11}'),
     Score      INT         NOT NULL CHECK (Score <= 500 and Score >= 0),
     Reputation INT         NOT NULL CHECK (Reputation <= 1000 and Reputation >= 1),
     CreateTime DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP
@@ -19,16 +19,43 @@ CREATE TABLE IF NOT EXISTS garbage -- 创建普通垃圾表
     GarbageID   INT PRIMARY KEY AUTO_INCREMENT,
     CreateTime  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
     Flat        TINYINT  NOT NULL DEFAULT 0 CHECK (Flat IN (0, 1, 2)),
-    UserID      char(34),
+    UserID      CHAR(34),
     UseTime     DATETIME,
     GarbageType TINYBLOB CHECK (GarbageType IS NULL OR GarbageType IN (1, 2, 3, 4)),
     Location    VARCHAR(50),
     CheckResult BIT CHECK (CheckResult IS NULL OR CheckResult IN (0, 1)),
-    CheckerID   char(34),
+    CheckerID   CHAR(34),
     FOREIGN KEY (UserID) REFERENCES user (UserID),
     FOREIGN KEY (CheckerID) REFERENCES user (UserID)
 );
 
+CREATE TABLE IF NOT EXISTS goods -- 商品
+(
+    GoodsID  INT PRIMARY KEY AUTO_INCREMENT,
+    Name     CHAR(100) NOT NULL,
+    Quantity INT       NOT NULL CHECK (Quantity >= 0),
+    Score    INT       NOT NULL CHECK (Score > 0 and Score <= 500)
+);
+
+CREATE TABLE IF NOT EXISTS orders -- 订单
+(
+    OrderID INT PRIMARY KEY AUTO_INCREMENT,
+    UserID  CHAR(34) NOT NULL,
+    Status  BIT      NOT NULL DEFAULT 0,
+    FOREIGN KEY (UserID) REFERENCES user (UserID)
+);
+
+
+CREATE TABLE IF NOT EXISTS ordergoods -- 订单内容
+(
+    OrderGoodsID INT PRIMARY KEY AUTO_INCREMENT,
+    OrderID      INT NOT NULL,
+    GoodsID      INT NOT NULL,
+    Quantity     INT NOT NULL DEFAULT 1 CHECK (Quantity >= 0),
+    FOREIGN KEY (OrderID) REFERENCES Orders (OrderID),
+    FOREIGN KEY (GoodsID) REFERENCES Goods (GoodsID)
+);
+
 -- 创建视图
 
 DROP VIEW IF EXISTS garbage_n;

+ 16 - 9
sql/mysql_db.py

@@ -83,23 +83,23 @@ class MysqlDB(HGSDatabase):
         columns: str = ", ".join(columns)
         return self.__search(f"SELECT {columns} FROM {table} {where} {group_by} {order_by} {limit} {offset};")
 
-    def insert(self, table: str, columns: list, values: Union[str, List[str]]):
+    def insert(self, table: str, columns: list, values: Union[str, List[str]], not_commit: bool = False):
         columns: str = ", ".join(columns)
         if type(values) is str:
             values: str = f"({values})"
         else:
             values: str = ", ".join(f"{v}" for v in values)
-        return self.__done(f"INSERT INTO {table}({columns}) VALUES {values};")
+        return self.__done(f"INSERT INTO {table}({columns}) VALUES {values};", not_commit=not_commit)
 
-    def delete(self, table: str, where: Union[str, List[str]] = None):
+    def delete(self, table: str, where: Union[str, List[str]] = None, not_commit: bool = False):
         if type(where) is list and len(where) > 0:
             where: str = " AND ".join(f"({w})" for w in where)
         elif type(where) is not str or len(where) == 0:  # 必须指定条件
             return None
 
-        return self.__done(f"DELETE FROM {table} WHERE {where};")
+        return self.__done(f"DELETE FROM {table} WHERE {where};", not_commit=not_commit)
 
-    def update(self, table: str, kw: dict[str:str], where: Union[str, List[str]] = None):
+    def update(self, table: str, kw: dict[str:str], where: Union[str, List[str]] = None, not_commit: bool = False):
         if len(kw) == 0:
             return None
 
@@ -110,8 +110,7 @@ class MysqlDB(HGSDatabase):
 
         kw_list = [f"{key} = {kw[key]}" for key in kw]
         kw_str = ", ".join(kw_list)
-
-        return self.__done(f"UPDATE {table} SET {kw_str} WHERE {where};")
+        return self.__done(f"UPDATE {table} SET {kw_str} WHERE {where};", not_commit=not_commit)
 
     def __search(self, sql) -> Union[None, pymysql.cursors.Cursor]:
         if self._cursor is None or self._db is None:
@@ -128,7 +127,7 @@ class MysqlDB(HGSDatabase):
             self._lock.release()  # 释放锁
         return self._cursor
 
-    def __done(self, sql) -> Union[None, pymysql.cursors.Cursor]:
+    def __done(self, sql, not_commit: bool = False) -> Union[None, pymysql.cursors.Cursor]:
         if self._cursor is None or self._db is None:
             raise DBCloseException
 
@@ -141,6 +140,14 @@ class MysqlDB(HGSDatabase):
             traceback.print_exc()
             return None
         finally:
-            self._db.commit()
+            if not not_commit:
+                self._db.commit()
             self._lock.release()
         return self._cursor
+
+    def commit(self):
+        try:
+            self._lock.acquire()
+            self._db.commit()
+        finally:
+            self._lock.release()

+ 48 - 0
sql/store.py

@@ -0,0 +1,48 @@
+from .db import DB
+from tool.type_ import *
+
+
+def get_store_item_list(db: DB) -> Optional[List]:
+    cur = db.search(columns=["Name", "Score", "Quantity", "GoodsID"],
+                    table="goods")
+    if cur is None or cur.rowcount == 0:
+        return None
+    return cur.fetchall()
+
+
+def get_store_item(goods_id: int, db: DB) -> Optional[List]:
+    cur = db.search(columns=["Name", "Score", "Quantity", "GoodsID"],
+                    table="goods",
+                    where=f"GoodsID={goods_id}")
+    if cur is None:
+        return None
+    assert cur.rowcount == 1
+    return cur.fetchone()
+
+
+def update_goods(goods_id: int, quantity: int, db: DB):
+    cur = db.update(table="goods", kw={"Quantity": f"{quantity}"}, where=f"GoodsID={goods_id}")
+    assert cur.rowcount == 1
+
+
+def get_order_id(uid: uid_t, db: DB):
+    cur = db.search(columns=["OrderID"],
+                    table="orders",
+                    where=f"UserID = '{uid}' and status=0")
+    if cur is None or cur.rowcount == 0:
+        cur = db.insert(table="orders", columns=["UserID"], values=f'{uid}')
+        if cur is None:
+            return None
+        return cur.lastrowid
+    assert cur.rowcount == 1
+    return cur.fetchone()[0]
+
+
+def write_goods(goods_id: int, quantity: int, order_id: int, db: DB):
+    cur = db.insert(table="ordergoods",
+                    columns=["OrderID", "GoodsID", "Quantity"],
+                    values=f"{order_id}, {goods_id}, {quantity}")
+    if cur is None:
+        return False
+    assert cur.rowcount == 1
+    return True

+ 7 - 7
sql/user.py

@@ -92,7 +92,7 @@ def is_user_exists(uid: uid_t, db: DB) -> bool:
     return True
 
 
-def update_user(user: User, db: DB) -> bool:
+def update_user(user: User, db: DB, not_commit: bool = False) -> bool:
     if not is_user_exists(user.get_uid(), db):
         return False
 
@@ -101,16 +101,16 @@ def update_user(user: User, db: DB) -> bool:
     is_manager = info['manager']
     if is_manager == '1':
         cur = db.update(table="user",
-                        kw={"IsManager": {is_manager}},
-                        where=f"UserID = '{uid}'")
+                        kw={"IsManager": is_manager},
+                        where=f"UserID = '{uid}'", not_commit=not_commit)
     else:
         score = info['score']
         reputation = info['reputation']
         cur = db.update(table="user",
-                        kw={"IsManager": {is_manager},
-                            "Score": {score},
-                            "Reputation": {reputation}},
-                        where=f"UserID = '{uid}'")
+                        kw={"IsManager": is_manager,
+                            "Score": f"{score}",
+                            "Reputation": f"{reputation}"},
+                        where=f"UserID = '{uid}'", not_commit=not_commit)
     return cur is not None