动机

小伙伴们最近迷恋上羽毛球,组织了个小群,办了公用的运动卡用于开场,考虑不是每次活动都是全员参与,需要一个计费的系统来计算每个人需要交的费用。商讨后决定采用“预充-扣费”的方式,则需要一个系统进行计费和扣费。

技术路线规划

模块名 语言 备注
管理核心 Python 使用JSON存储信息
Web后端 Python Flask框架
Web前端 HTML Jinja框架渲染

实现

核心模块——用户状态管理

该部分是整个计费系统的核心,用于管理每个用户的余额。使用一个类表示用户,需要的属性为

  • 状态列表(用户名,ID,使用次数,余额)

需要的方法有:

  • 创建用户(创建新的JSON文件)
  • 读取用户状态(从已有的JSON文件中)
  • 扣费(使用次数增加1,余额减小)
  • 充值(余额增加)
  • 保存状态(将现有的状态写入JSON文件)

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#  -*- coding: utf-8 -*-
import json
import os


class UserHanlde(object):
"""docstring for UserHanlde"""

def __init__(self, UserID, UserName=""):
super(UserHanlde, self).__init__()
if self.UserExsist(UserID):
self.UserInfo = self.LoadUserInfo(UserID)
else:
self.UserInfo = self.CreateNewUser(UserName, UserID)

构造函数,若该用户ID存在则读取状态,否则创建
1
2
def UserExsist(self, UserID):
return os.path.exists("./Users/%s.json" % UserID)

判断该ID的JSON文件是否存在
1
2
3
4
5
6
7
8
9
10
def CreateNewUser(self, UserName, UserID):
UserInfo = {
"name": UserName,
"id": UserID,
"num": 0,
"balance": 50
}
with open("./Users/%s.json" % UserID, "w") as jsonfile:
json.dump(UserInfo, jsonfile, ensure_ascii=False, indent=4)
return UserInfo

创建新用户,将初始余额设为50并保存JSON文件
1
2
3
def LoadUserInfo(self, UserID):
with open("./Users/%s.json" % UserID, "r") as jsonfile:
return json.load(jsonfile)

从JSON文件中载入用户状态
1
2
3
def PlayOneTime(self, Pay):
self.UserInfo["num"] += 1
self.UserInfo["balance"] = self.UserInfo["balance"] - Pay

扣费,扣除指定的费用并在将扣费次数+1
1
2
def Recharge(self, Pay):
self.UserInfo["balance"] += Pay

充值,费用加上指定值
1
2
def DeleteUser(self):
os.remove("./Users/%s.json" % self.UserInfo["id"])

删除用户,删除指定的JSON文件
1
2
3
def SaveInfo(self):
with open("./Users/%s.json" % self.UserInfo["id"], "w") as jsonfile:
json.dump(self.UserInfo, jsonfile, ensure_ascii=False, indent=4)

保存状态,将当前状态写入对应的JSON文件

Web后端

web后端使用Python的Flask框架构造,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, render_template, request
from UserHanlde import UserHanlde
import os
app = Flask(__name__)


def GetUserIDList():
return [x[:-5] for x in os.listdir("./Users") if ".json" in x]

def GetUserInfoList():
UserInfoList = dict()
for UserID in GetUserIDList():
UserData = UserHanlde(UserID)
UserInfoList[UserID] = UserData.UserInfo
return UserInfoList

常用部分的封装:

  • GetUserIDList():返回已经存在的用户ID列表
  • GetUserInfoList():返回已经存在的用户状态列表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    @app.route("/index")
    def ViewInfo():
    return render_template("index.html", user_list=GetUserInfoList())


    @app.route("/recharge")
    def GetReChargeInfo():
    return render_template("recharge.html", user_list=GetUserInfoList())


    @app.route("/recharge_handle", methods=["GET", "POST"])
    def Recharge():
    UserID = request.values.get("id")
    UserRecharge = request.values.get("pay")
    if UserRecharge.isdigit() is True:
    UserHanlder = UserHanlde(UserID)
    UserHanlder.Recharge(int(UserRecharge))
    UserHanlder.SaveInfo()
    return render_template("back.html")
    else:
    return "fail"


    @app.route("/register")
    def GetRegisterInfo():
    return render_template("register.html")


    @app.route("/register_handle", methods=["GET", "POST"])
    def Register():
    UserID = request.values.get("id")
    UserName = request.values.get("name")
    UserHanlder = UserHanlde(UserID, UserName=UserName)
    return render_template("back.html")


    @app.route("/pay")
    def GetPayName():
    return render_template("pay.html", user_list=GetUserInfoList())


    @app.route("/pay_handle", methods=["GET", "POST"])
    def Pay():
    UserIDList = request.values.getlist("vehicle")
    UserIDPay = request.values.get("pay")
    if UserIDPay.isdigit() is True:
    PayNum = int(UserIDPay) / len(UserIDList)
    for UserID in UserIDList:
    UserHanlder = UserHanlde(UserID)
    UserHanlder.PlayOneTime(PayNum)
    UserHanlder.SaveInfo()
    return render_template("back.html")
    else:
    return "fail"
    路由部分
  • /index:主页,包括导航和状态显示,所有用户的消费次数和余额将在这里显示
  • /recharge/recharge_handle:充值页面, /recharge为操作页面,用户在这里填写表单数据,随后表单数据被提交到/recharge_handle处理充值业务
  • /register/register_handle:注册页面,与/recharge/recharge_handle关系相同
  • /pay/pay_handle:扣费页面,与/recharge/recharge_handle关系相同
    1
    app.run(host="0.0.0.0")
    运行,监听所有IP,这样在局域网就可以访问了

    Web前端

    Web使用HTML代码提供GUI,使用Jinja框架分离数据与模板
  • index界面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <!DOCTYPE html>
    <html>
    <head>
    <title>index</title>
    </head>
    <body>
    <div>
    <h1>羽毛球运动管理系统</h1>
    </div>
    <div>
    <table border="1">
    <thead>
    <tr>
    <th>用户</th>
    <th>次数</th>
    <th>余额</th>
    </tr>
    </thead>
    <tbody>
    {% for user_id in user_list -%}
    <tr>
    <td>{{user_list[user_id]["name"]}}</td>
    <td>{{user_list[user_id]["num"]}}</td>
    <td>{{user_list[user_id]["balance"]}}</td>
    </tr>
    {%- endfor %}
    </tbody>
    </table>
    </div>
    用户状态显示,使用for循环生成表格
    1
    2
    3
    4
    5
    6
    7
    	<div>
    <a href="register">register</a>
    <a href="recharge">recharge</a>
    <a href="pay">pay</a>
    </div>
    </body>
    </html>
    超链接部分,用于导航
  • register界面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html>
    <head>
    <title>register</title>
    </head>
    <body>
    <h1>羽毛球运动管理系统--注册</h1>
    <div>
    <form action="register_handle" method="post" accept-charset="utf-8">
    name<input type="text" name="name">
    id<input type="text" name="id">
    <input type="submit" name="Submit">
    </form>
    </div>
    <a href="/index">back to index</a>
    </body>
    </html>
    使用两个文本输入框表单输入用户名与用户ID
  • recharge界面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!DOCTYPE html>
    <html>
    <head>
    <title>recharge</title>
    </head>
    <body>
    <div>
    <h1>羽毛球运动管理系统--充值</h1>
    </div>
    <div>
    <form action="recharge_handle" method="post" accept-charset="utf-8">
    <select name="id">
    {% for userid in user_list -%}
    <option value="{{userid}}">{{user_list[userid]["name"]}}</option>
    {%- endfor %}
    </select>
    recharge¥<input type="text" name="pay">
    <input type="submit" name="Submit">
    </form>
    </div>
    <a href="/index">back to index</a>
    </body>
    </html>
    使用下拉菜单提供可供选择的用户名,文本输入充值金额
  • pay界面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!DOCTYPE html>
    <html>
    <head>
    <title>pay</title>
    </head>
    <body>
    <h1>羽毛球运动管理系统--消费</h1>
    <div>
    <form action="pay_handle" method="post" accept-charset="utf-8">
    <div>
    {%for userid in user_list%}
    <input type="checkbox" name="vehicle" value="{{userid}}">{{user_list[userid]["name"]}}<br>
    {% endfor %}
    </div>
    pay¥<input type="text" name="pay">
    <input type="submit" name="Submit">
    </form>
    </div>
    <a href="/index">back to index</a>
    </body>
    </html>
    使用复选框列出所有用户提供选择,文本输入总输入金额,复选框这种表单数据在后端使用request.values.getlist("name")获取为一个列表
  • back界面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!DOCTYPE html>
    <html>
    <head>
    <title>back</title>
    </head>
    <body>
    <a href="/index">back to index</a>
    </body>
    </html>
    用户完成充值/注册/消费时用于返回主页