模拟实现一个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函数请求的商品分类,获取该分类下的所有商品返回给逻辑接口层再返回给用户视图层。
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类型的数据。获取再转成字符串也行吧,回来再试试。
import re def is_number(your_str): res = re.findall('^/d+/.?/d*$', your_str) if res: return True else: return False # 匹配数字,判断输入的字符串是否是非负数
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参考这篇博客
# 自动将功能函数添加到core.src中的func_dict字典。 # 如果将func_dict字典放在一个单独的py文件中会方便避免这个bug # 这个bug的主要原因在于:模块导入的先后顺序和搜索模块的顺序
项目源文件在百度网盘,感兴趣的朋友可以下载参考。
链接: https://pan.baidu.com/s/1GTL081h64tW2SwsHU8kTGw
提取码:fn6e