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