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