常用的项目结构设计中,三层架构设计非常实用。这种架构设计模式将整个程序分为三层:
用户视图层:用户交互的,可以接受用户的输入数据,展示显示的消息。逻辑接口层:接收视图层传递过来的参数,根据逻辑判断调用数据层加以处理并返回一个结果给用户视图层。数据处理层:接受接口层传递过来的参数,做数据的增删改查。Copy# 优点:结构清晰,职责明了。扩展性强,好维护。对数据比较安全。# 缺点:每个功能都要跨越逻辑接口层,不能直接访问数据库,所以效率会降下来。
Copy1.额度15000或自定义 --> 注册功能2.实现购物商城,买东西加入购物车,调用信用卡接口结账 --> 购物功能、支付功能3.可以提现,手续费5% --> 提现功能4.支持多账户登录 --> 登录功能,登录失败三次冻结账户5.支持账户间转账 --> 转账功能6.记录日常消费 --> 记录流水功能7.提供还款接口 --> 还款功能8.ATM记录操作日志 --> 记录日志功能9.提供管理接口,包括添加账户、用户额度,冻结账户等。。。 ---> 管理员功能10.用户认证用装饰器 --> 登录认证装饰器
提取功能
Copy# 展示给用户选择的功能(用户视图层)1、注册功能2、登录功能3、查看余额4、提现功能5、还款功能6、转账功能7、查看流水8、购物功能9、查看购物车10、管理员功能
实现思路
上一篇项目总结也是关于ATM,只不过那个项目中所有的函数都在一个py文件中;这个项目总结不能再那样搞了,这次要规范点。

我们知道软件开发目录规范,就是按程序的不同功能将代码分布在不同的文件(夹)中,本项目也采用这种规范。
另外,我们又学习了项目的三层架构设计,将一个功能分三个层次,清晰各部分职责。

所以,这个项目基于软件开发目录规范,采用三层架构的原则,编写每个具体功能的代码。
项目框架整个项目采用三层结构设计。用户直接接触的是用户视图层。用户通过选择不同的功能,进入不同功能的用户视图层。
在用户视图层中,用户输入数据;然后用户视图层将用户的数据传给逻辑接口层,逻辑接口层调用数据处理层的接口,获取该用户的相关数据,做一定的逻辑判断,然后将逻辑判断后的数据和/或信息返回到用户视图层,展示给用户。
程序结构:遵循软件开目录规范
CopyATM&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# 项目启动文件
运行环境
Copy- windows10, 64位- python3.8- pycharm2019.3
注册功能三层架构分析
注册功能用户视图层:core/src.py
Copyfrom lib.tools import hash_md5, autofrom core.current_user import login_userfrom 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
Copyfrom conf.settings import INIT_BALANCEfrom core.current_user import login_userfrom db import db_handlefrom lib.tools import save_logdef 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
Copyimport os, jsonfrom conf.settings import USER_DB_DIRdef 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
Copyfrom lib.tools import auth, is_number, autofrom core.current_user import login_userfrom interface.bank_interface import withdraw_interface@auto('提现')@authdef 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
Copyfrom db import db_handlefrom conf.settings import SERVICE_FEE_RATIOfrom lib.tools import save_flow, save_logdef 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
Copyfrom core.current_user import login_userfrom lib.tools import auth, autofrom conf.settings import GOODS_CATEGOTYfrom interface.shop_interface import get_goods_interface, shopping_interfacefrom interface.shop_interface import put_in_mycart_interface@auto('网上商城')@authdef 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
Copyfrom db import db_handlefrom interface.bank_interface import pay_interfacefrom lib.tools import save_logdef 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
Copy......from conf.settings import GOODS_DB_FILEdef 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文件中文字符显示问题
Copyimport jsonwith 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解决,即中文字符不会转为二进制字节
资金的小数点保留问题
Copy# 本项目就涉及用户金额数据小数点保留问题。对于会计金融需要非常在意小数点保留问题上,不能简单使用int转整形# 还不能使用float保留成浮点型,因为它的精度不够,且小数位不能控制# 你可能会说round(1.2312, 2)可以设置小数点精度; 但round(0.00001, 2),想要的结果是0.01而得到的结果确实0.0# 此时可以导入decimal模块import decimals = 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模块匹配数字应用在项目中
Copyimport redef is_number(your_str): res = re.findall('^\d+\.?\d$', your_str) if res: return True else: return False # 匹配数字,判断输入的字符串是否是非负数
hash模块项目中密码加密
Copyimport hashlibdef hash_md5(info): m = hashlib.md5() m.update(info.encode('utf-8')) m.update('因为相信所以看见'.encode('utf-8'))# 加盐处理 return m.hexdigest()# 用于密码加密
logging模块项目中记录日志
Copy# 使用流程:-1 在配置文件settings.py中配置日志字典LOGGING_DIC-2 在lib/tools.py文件中封装日志记录函数,返回loggerdef 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)记录日志
模块导入-避免循环导入问题
Copy# 两种方式避免循环导入问题- 方式1:如果只有某一个函数需要导入自定义模块,则在函数局部作用域导入模块- 方式2:后一个导入者使用import导入,不要使用from ... import ... 导入
函数对象自动添加字典的bug
这个bug是在后来思考的时候发现,本项目因为采用了正确的方式避免了这个bug。具体bug参考这篇博客
Copy# 自动将功能函数添加到core.src中的func_dict字典。# 如果将func_dict字典放在一个单独的py文件中会方便避免这个bug# 这个bug的主要原因在于:模块导入的先后顺序和搜索模块的顺序
总结
软件开发目录规范
每个人创建目录规范的样式不尽相同。这都没有关系,关键是整个项目程序组织结构清晰。目录规范尽可能遵循大多数人使用的方式,这样你的代码可读性才会比较友好。项目三层架构设计
三层架构设计是一种项目开发的思想方案。一旦确定了这种开发模式,编写代码时刻区分出不同层次的职能。严格按照每个层次的职能,不同职能的代码放在不同的层次,不要混乱,这样管理维护起来会很方便。有时候某个功能过于简单,可以直接访问数据处理层。但最好还是遵循三层架构设计,不要跨过逻辑接口层。存数据不是目的,取才是目的
存数据不是目的,存数据时一定要考虑取数据时的方便。一个好的数据存储结构和方式,验证影响取数据时功能代码编写额简洁和优美。程序 = 数据结构 + 算法。 所以,好的数据结构,导致取数据功能的难与易。封装代码,尽可能重用代码
程序中应该尽可能多的在不丧失功能清晰的情况下,尽可能多的考虑代码的重用。多编写通用功能的函数工具,在程序中使用处调用之。