ATM购物车+三层架构设计
ATM购物车项目
模拟实现一个ATM + 购物商城程序。
该程序实现普通用户的登录注册、提现充值还款等功能,并且支持到网上商城购物的功能。
账户余额足够支付商品价格时,扣款支付;余额不足时,无法支付,商品存放个人购物车。
如果用户具有管理员功能,还支持管理员身份登录。具体需求见项目需求部分。
三层架构
项目开发中,清晰明了的结构设计非常重要。它的重要性至少体现在三个方面:结构清晰;可维护性强;可扩展性高。
常用的项目结构设计中,三层架构设计非常实用。这种架构设计模式将整个程序分为三层:
- 用户视图层:用户交互的,可以接受用户的输入数据,展示显示的消息。
- 逻辑接口层:接收视图层传递过来的参数,根据逻辑判断调用数据层加以处理并返回一个结果给用户视图层。
- 数据处理层:接受接口层传递过来的参数,做数据的增删改查。
# 优点:结构清晰,职责明了。扩展性强,好维护。对数据比较安全。
# 缺点:每个功能都要跨越逻辑接口层,不能直接访问数据库,所以效率会降下来。
项目需求
1.额度15000或自定义 --> 注册功能
2.实现购物商城,买东西加入购物车,调用信用卡接口结账 --> 购物功能、支付功能
3.可以提现,手续费5% --> 提现功能
4.支持多账户登录 --> 登录功能,登录失败三次冻结账户
5.支持账户间转账 --> 转账功能
6.记录日常消费 --> 记录流水功能
7.提供还款接口 --> 还款功能
8.ATM记录操作日志 --> 记录日志功能
9.提供管理接口,包括添加账户、用户额度,冻结账户等。。。 ---> 管理员功能
10.用户认证用装饰器 --> 登录认证装饰器
提取功能
# 展示给用户选择的功能(用户视图层)
1、注册功能
2、登录功能
3、查看余额
4、提现功能
5、还款功能
6、转账功能
7、查看流水
8、购物功能
9、查看购物车
10、管理员功能
实现思路
上一篇项目总结也是关于ATM,只不过那个项目中所有的函数都在一个py文件中;这个项目总结不能再那样搞了,这次要规范点。
我们知道软件开发目录规范,就是按程序的不同功能将代码分布在不同的文件(夹)中,本项目也采用这种规范。
另外,我们又学习了项目的三层架构设计,将一个功能分三个层次,清晰各部分职责。
所以,这个项目基于软件开发目录规范,采用三层架构的原则,编写每个具体功能的代码。
项目框架
整个项目采用三层结构设计。用户直接接触的是用户视图层。用户通过选择不同的功能,进入不同功能的用户视图层。
在用户视图层中,用户输入数据;然后用户视图层将用户的数据传给逻辑接口层,逻辑接口层调用数据处理层的接口,获取该用户的相关数据,做一定的逻辑判断,然后将逻辑判断后的数据和/或信息返回到用户视图层,展示给用户。
程序结构:遵循软件开目录规范
ATM&Shop/
|-- conf
| |-- setting.py # 项目配置文件
|-- core
| |-- admin.py # 管理员视图层函数
| |-- current_user.py # 记录当前登录用户信息[username, is_admin]
| |-- shop.py # 购物相关视图层函数
| |-- src.py # 主程序(包含用户视图层函数、atm主函数)
|-- db
| |-- db_handle.py # 数据处理层函数
| |-- goods_data.json # 商品信息文件
| |-- users_data # 用户信息json文件夹
| | |-- xliu.json # 用户信息文件:username|password|balance|my_flow|my_cart等
| | |-- egon.json
|-- interface # 逻辑接口
| |-- admin_interface.py # 管理员逻辑接口层函数
| |-- bank_interface.py # 银行相关逻辑接口层函数
| |-- shop_interface.py # 购物相关逻辑接口层函数
| |-- user_interface.py # 用户相关逻辑接口层函数
|-- lib
| |-- tools.py # 公用函数:加密|登录装饰器权限校验|记录流水|日志等
|-- log # 日志文件夹
| |-- operation.log
| |-- transaction.log
|-- readme.md
|-- run.py # 项目启动文件
运行环境
- windows10, 64位
- python3.8
- pycharm2019.3
这个项目有很多具体功能,这里就不一一介绍,挑几个典型的功能介绍其三层结构的实现思路。
完整的项目代码见本文最后部分提供的项目源文件链接地址。
注册功能三层架构分析
注册功能用户视图层:core/src.py
from lib.tools import hash_md5, auto
from core.current_user import login_user
from interface.user_interface import register_interface
@auto('注册')
def register():
print('注册页面'.center(50, '-'))
while 1:
name = input('请输入用户名:').strip()
pwd = input('请输入密码:').strip()
re_pwd = input('请确认密码:').strip()
if pwd != re_pwd:
print('两次密码输入不一致,请重新输入')
continue
flag, msg = register_interface(name, hash_md5(pwd))
print(msg)
if flag:
break
# 注册功能用户视图层接收用户的注册信息:用户名|密码|确认密码
# 先做一个小逻辑判断,判断密码和确认密码是否一致?若不一致,则提示用户密码不一致从新输入
# 若密码一致,则将用户名和密码后的密码通过注册接口交给逻辑接口层
# 然后接受逻辑接口层的返回数据和信息,打印展示和下一步判断。
注册功能逻辑接口层:interface/user_interface.py
from conf.settings import INIT_BALANCE
from core.current_user import login_user
from db import db_handle
from lib.tools import save_log
def register_interface(name, pwd):
"""
注册接口
:param name:
:param pwd: 密码,密文
:return:
"""
user_dict = db_handle.get_user_info(name)
if user_dict:
return False, '用户名已经存在'
user_dict = {
'username': name,
'password': pwd,
'balance': INIT_BALANCE,
'is_admin': False,
'is_locked': False,
'login_failed_counts': 0,
'my_cart': {},
'my_flow':{}
}
save_log('日常操作').info(f'{name}注册账号成功')
db_handle.save_user_info(user_dict)
return True, '注册成功'
# 注册功能逻辑接口层接收用户视图层传过来的用户名和密文密码,
# 通过调用数据处理层get_user_info函数,读用户文件,获取用户的信息字典
# 若用户信息字典存在,则该用户名已经被注册使用,则返回给用户视图层不能注册的信息
# 若用户信息字典不存在,则说明可以注册。
# 创建新用户信息字典,初始化相关数据,交给数据处理层save_user_info函数,并返回给用户视图层可以注册的信息。
数据处理层:db/db_handle.py
import os, json
from conf.settings import USER_DB_DIR
def get_user_info(name):
user_file = os.path.join(USER_DB_DIR, f'{name}.json')
if os.path.isfile(user_file):
with open(user_file, 'rt', encoding='utf-8') as f:
return json.load(f)
else:
return {}
def save_user_info(user_dict):
user_dict['balance'] = round(user_dict['balance'], 2)
user_file = os.path.join(USER_DB_DIR, f'{user_dict.get("username")}.json')
with open(user_file, 'wt', encoding='utf-8') as f:
json.dump(user_dict, f, ensure_ascii=False)
# 数据处理层函数:通过用户名获取用户信息字典;若用户存在则返回用户信息字典,用户不存在则返回空字典
# save_user_info函数,接收逻辑接口层的接口,将用户信息字典序列化保存到独立文件,以用户名命名文件名
提现功能三层结构分析
提现功能用户视图层:core/src.py
from lib.tools import auth, is_number, auto
from core.current_user import login_user
from interface.bank_interface import withdraw_interface
@auto('提现')
@auth
def withdraw():
print('提现页面'.center(50, '-'))
while 1:
amounts = input('请输入体现金额:').strip()
if not is_number(amounts):
print('请输入合法的体现金额')
continue
flag, msg = withdraw_interface(login_user[0], float(amounts))
print(msg)
if flag:
break
# 提现功能用户视图层:在用在用户登录之后才能使用(利用函数装饰器auth实现登录校验)
# 接收用户输入提现金额,先做小逻辑判断用户输入金额是否是数字(支持小数),通过工具函数is_number实现
# 然后将合法提现金额转成浮点数通过提现接口交给提现逻辑接口层
# 打印逻辑接口层返回的数据并做判断
提现功能逻辑接口层:interface/bank_interface.py
from db import db_handle
from conf.settings import SERVICE_FEE_RATIO
from lib.tools import save_flow, save_log
def withdraw_interface(name, amounts):
user_dict = db_handle.get_user_info(name)
amounts_and_fee = amounts * (1 + SERVICE_FEE_RATIO)
if amounts_and_fee > user_dict.get('balance'):
save_log('提现').info(f'{name}提现{amounts}元,余额不足提现失败')
return False, '账户余额不足'
user_dict['balance'] -= amounts_and_fee
msg = f'{name}提现{amounts}元'
save_flow(user_dict, '提现', msg)
save_log('提现').info(msg)
db_handle.save_user_info(user_dict)
return True, f'提现金额{amounts}元, 账户余额:{user_dict["balance"]}元'
# 通过用户名调用数据处理层函数get_user_info获取用户信息字典金额获取用户的账户余额
# 计算出用户提现金额的本金和手续费,判断本金和手续费是否大于账户余额
# 若大于账户余额,则无法提现,将提示信息返回给提现用户视图层
# 否则,从账户余额中扣除提现金额和手续费
# 调用数据处理层save_user_info,保存用户的信息
# 将提现成功信息返回给用户视图层
购物功能三层架构分析
购物功能用户视图层:core/shop.py
from core.current_user import login_user
from lib.tools import auth, auto
from conf.settings import GOODS_CATEGOTY
from interface.shop_interface import get_goods_interface, shopping_interface
from interface.shop_interface import put_in_mycart_interface
@auto('网上商城')
@auth
def shopping():
print('网上商城'.center(50, '-'))
username = login_user[0]
new_goods = [] # 存放用户本次选择的商品
while 1:
for k, v in GOODS_CATEGOTY.items():
print(f'({k}){v}')
category = input('请选择商品类型编号(结算Y/退出Q):').strip().lower()
if category == 'y':
if not new_goods:
print('您本次没有选择商品,无法结算')
continue
else:
flag, msg = shopping_interface(username, new_goods)
print(msg)
if not flag:
put_in_mycart_interface(username, new_goods)
break
elif category == 'q':
if not new_goods: break
put_in_mycart_interface(username, new_goods)
break
if category not in GOODS_CATEGOTY:
print('您选择的编号不存在,请重新选择')
continue
goods_list = get_goods_interface(GOODS_CATEGOTY[category])
while 1:
for index, item in enumerate(goods_list, 1):
name, price = item
print(f'{index}: {name}, {price}元')
choice = input('请输入商品的编号(返回B):').strip().lower()
if choice == 'b':
break
if not choice.isdigit() or int(choice) not in range(1, len(goods_list)+1):
print('您输入的商品编号不存在,请重新输入')
continue
name, price = goods_list[int(choice)-1]
counts = input(f'请输入购买{name}的个数:').strip()
if not counts.isdigit() and counts == '0':
print('商品的个数是数字且不能为零')
continue
new_goods.append([name, price, int(counts)])
# 购物功能用户视图层:需要用户先登录再使用
# 打印商品分类表,让用户选择分类编号,然后将分类编号传给逻辑接口层,获取该分类下的商品列表展示给用户。
# 用户继续选择该分类下的商品编号和购买的商品个数。此处会使用小逻辑判断用户的输入是否合法。
# 选择商品和商品个数后,会将选择的结果临时存放在列表new_goods中,用于用户退出时结算。
# 如果用户选择支付,则将用户名和用户选择的商品通过购物结构交给购物逻辑接口层。
# 若逻辑接口层返回的结果时支付成功,则退出购物;若返回的就过是支付失败则将new_goods的商品交给put_in_mycart_interface放进购物车接口。
# 如果用户选择退出,则直接将new_goods的商品交给put_in_mycart_interface放进购物车接口
购物功能逻辑接口层:interface/shop_interface.py
from db import db_handle
from interface.bank_interface import pay_interface
from lib.tools import save_log
def get_goods_interface(category):
"""
根据分类获取商品
:param category:
:return:
"""
return db_handle.get_goods_info(category)
def shopping_interface(name, new_goods):
total_cost = 0
for item in new_goods:
*_, price, counts = item
total_cost += price * counts
flag = pay_interface(name, total_cost)
if flag:
return True, '支付成功,商品发货中....'
else:
return False, '账户余额不足,支付失败'
def put_in_mycart_interface(name, new_goods):
user_dict = db_handle.get_user_info(name)
my_cart = user_dict.get('my_cart')
for item in new_goods:
goods_name, price, counts = item
if goods_name not in my_cart:
my_cart[goods_name] = [price, counts]
else:
my_cart[goods_name][-1] += counts
save_log('日常操作').info(f'{name}更新了购物车商品')
db_handle.save_user_info(user_dict)
# 购物接口层函数,计算接收的商品的总价,然后调用并将总结交给银行支付接口
# 支付接口返回支付成功/失败的返回信息;若支付成功则返回给用户视图层支付成功的信息;否则是支付失败的信息
# 放进购物车接口:将用户石涂层传过来的商品保存到用户信息字典里面的my_cart字典中,并调用数据处理层的save_user_info含糊,保存用户信息。
# 获取商品接口get_goods_interface,接收用户视图层传过来的商品分类。然后将该分类信息返回给用户视图层
购物功能数据处理层:db/db_handle.py
......
from conf.settings import GOODS_DB_FILE
def get_goods_info(category):
with open(GOODS_DB_FILE, 'rt', encoding='utf-8') as f:
all_goods_dict = json.load(f)
return all_goods_dict.get(category)
# 这个函数主要用来接收购物功能逻辑接口层get_goods_interface函数请求的商品分类,获取该分类下的所有商品返回给逻辑接口层再返回给用户视图层。
小知识点总结
json文件中文字符显示问题
import json
with open(user_file, 'wt', encoding='utf-8') as f:
json.dump(user_dict, f, ensure_ascii=False)
# 由于json序列化是可读序列化,即json文件存放的是字符串类型的数据(不像pickle是二进制不可读的数据)。
# 此外,json文件存放的是unic0de text。即如果存的字符是中午字符,则会被存储为unicode二进制数据,在这json文件里面看起来很不舒服。
# 这个问题可以通过 json.dump中的参数ensure_ascii=False解决,即中文字符不会转为二进制字节
资金的小数点保留问题
# 本项目就涉及用户金额数据小数点保留问题。对于会计金融需要非常在意小数点保留问题上,不能简单使用int转整形
# 还不能使用float保留成浮点型,因为它的精度不够,且小数位不能控制
# 你可能会说round(1.2312, 2)可以设置小数点精度; 但round(0.00001, 2),想要的结果是0.01而得到的结果确实0.0
# 此时可以导入decimal模块
import decimal
s = decimal.Decimal('0.00001')
print(s, type(s)) # 0.00001 <class 'decimal.Decimal'>
print(s.quantize(decimal.Decimal('0.01'), 'ROUND_UP')) # 0.01
# 可惜的是本项目使用的是json文件,好像不能存decimal类型的数据。获取再转成字符串也行吧,回来再试试。
re模块匹配数字应用在项目中
import re
def is_number(your_str):
res = re.findall('^\d+\.?\d*$', your_str)
if res:
return True
else:
return False
# 匹配数字,判断输入的字符串是否是非负数
hash模块项目中密码加密
import hashlib
def hash_md5(info):
m = hashlib.md5()
m.update(info.encode('utf-8'))
m.update('因为相信所以看见'.encode('utf-8')) # 加盐处理
return m.hexdigest()
# 用于密码加密
logging模块项目中记录日志
# 使用流程:
-1 在配置文件settings.py中配置日志字典LOGGING_DIC
-2 在lib/tools.py文件中封装日志记录函数,返回logger
def save_log(log_type):
from conf.settings import LOGGING_DIC
from logging import config, getLogger
config.dictConfig(LOGGING_DIC)
return getLogger(log_type)
-3 在逻辑接口层中调用save_log函数返回logger,使用logger.info(msg)记录日志
模块导入-避免循环导入问题
# 两种方式避免循环导入问题
- 方式1:如果只有某一个函数需要导入自定义模块,则在函数局部作用域导入模块
- 方式2:后一个导入者使用import导入,不要使用from ... import ... 导入
函数对象自动添加字典的bug
这个bug是在后来思考的时候发现,本项目因为采用了正确的方式避免了这个bug。具体bug参考这篇博客
# 自动将功能函数添加到core.src中的func_dict字典。
# 如果将func_dict字典放在一个单独的py文件中会方便避免这个bug
# 这个bug的主要原因在于:模块导入的先后顺序和搜索模块的顺序
总结
软件开发目录规范
- 每个人创建目录规范的样式不尽相同。这都没有关系,关键是整个项目程序组织结构清晰。
- 目录规范尽可能遵循大多数人使用的方式,这样你的代码可读性才会比较友好。
项目三层架构设计
- 三层架构设计是一种项目开发的思想方案。一旦确定了这种开发模式,编写代码时刻区分出不同层次的职能。
- 严格按照每个层次的职能,不同职能的代码放在不同的层次,不要混乱,这样管理维护起来会很方便。
- 有时候某个功能过于简单,可以直接访问数据处理层。但最好还是遵循三层架构设计,不要跨过逻辑接口层。
存数据不是目的,取才是目的
- 存数据不是目的,存数据时一定要考虑取数据时的方便。
- 一个好的数据存储结构和方式,严重影响取数据时功能代码编写的简洁和优美。
- 程序 = 数据结构 + 算法。 所以,数据结构的好坏,直接导影响获取数据的难与易。
封装代码,尽可能重用代码
- 程序中应该尽可能多的在不丧失功能清晰的情况下,尽可能多的考虑代码的重用。
- 多编写通用功能的函数工具,在程序中使用处调用之。
项目源文件
项目源文件在百度网盘,感兴趣的朋友可以下载参考。
链接:https://pan.baidu.com/s/1GTL081h64tW2SwsHU8kTGw
提取码:fn6e
代码量统计见下图