ATM購物車專案+三層架構設計
阿新 • • 發佈:2020-04-07
## ATM購物車專案
模擬實現一個ATM + 購物商城程式。
該程式實現普通使用者的登入註冊、提現充值還款等功能,並且支援到網上商城購物的功能。
賬戶餘額足夠支付商品價格時,扣款支付;餘額不足時,無法支付,商品存放個人購物車。
如果使用者具有管理員功能,還支援管理員身份登入。具體需求見專案需求部分。
------
## 三層架構
專案開發中,清晰明瞭的結構設計非常重要。它的重要性至少體現在三個方面:結構清晰;可維護性強;可擴充套件性高。
常用的專案結構設計中,三層架構設計非常實用。這種架構設計模式將整個程式分為三層:
- 使用者檢視層:使用者互動的,可以接受使用者的輸入資料,展示顯示的訊息。
- 邏輯介面層:接收檢視層傳遞過來的引數,根據邏輯判斷呼叫資料層加以處理並返回一個結果給使用者檢視層。
- 資料處理層:接受介面層傳遞過來的引數,做資料的增刪改查。
~~~python
# 優點:結構清晰,職責明瞭。擴充套件性強,好維護。對資料比較安全。
# 缺點:每個功能都要跨越邏輯介面層,不能直接訪問資料庫,所以效率會降下來。
~~~
![](https://img2020.cnblogs.com/blog/1950650/202004/1950650-20200407130720690-1574465497.png)
------
## 專案需求
~~~python
1.額度15000或自定義 --> 註冊功能
2.實現購物商城,買東西加入購物車,呼叫信用卡介面結賬 --> 購物功能、支付功能
3.可以提現,手續費5% --> 提現功能
4.支援多賬戶登入 --> 登入功能,登入失敗三次凍結賬戶
5.支援賬戶間轉賬 --> 轉賬功能
6.記錄日常消費 --> 記錄流水功能
7.提供還款介面 --> 還款功能
8.ATM記錄操作日誌 --> 記錄日誌功能
9.提供管理介面,包括新增賬戶、使用者額度,凍結賬戶等。。。 ---> 管理員功能
10.使用者認證用裝飾器 --> 登入認證裝飾器
~~~
## 提取功能
~~~python
# 展示給使用者選擇的功能(使用者檢視層)
1、註冊功能
2、登入功能
3、檢視餘額
4、提現功能
5、還款功能
6、轉賬功能
7、檢視流水
8、購物功能
9、檢視購物車
10、管理員功能
~~~
------
## 實現思路
上一篇專案總結也是關於ATM,只不過那個專案中所有的函式都在一個py檔案中;這個專案總結不能再那樣搞了,這次要規範點。
我們知道軟體開發目錄規範,就是按程式的不同功能將程式碼分佈在不同的檔案(夾)中,本專案也採用這種規範。
另外,我們又學習了專案的三層架構設計,將一個功能分三個層次,清晰各部分職責。
所以,這個專案基於軟體開發目錄規範,採用三層架構的原則,編寫每個具體功能的程式碼。
------
## 專案框架
整個專案採用三層結構設計。使用者直接接觸的是使用者檢視層。使用者通過選擇不同的功能,進入不同功能的使用者檢視層。
在使用者檢視層中,使用者輸入資料;然後使用者檢視層將使用者的資料傳給邏輯介面層,邏輯介面層呼叫資料處理層的介面,獲取該使用者的相關資料,做一定的邏輯判斷,然後將邏輯判斷後的資料和/或資訊返回到使用者檢視層,展示給使用者。
![](https://img2020.cnblogs.com/blog/1950650/202004/1950650-20200407124741790-1608342368.png)
**程式結構**:遵循軟體開目錄規範
~~~python
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 # 專案啟動檔案
~~~
------
## 執行環境
~~~python
- windows10, 64位
- python3.8
- pycharm2019.3
~~~
------
這個專案有很多具體功能,這裡就不一一介紹,挑幾個典型的功能介紹其三層結構的實現思路。
完整的專案程式碼見本文最後部分提供的專案原始檔連結地址。
## 註冊功能三層架構分析
**註冊功能使用者檢視層**:core/src.py
~~~python
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
~~~python
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
~~~python
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
~~~python
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
~~~python
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
~~~python
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
~~~python
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
~~~python
......
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檔案中文字元顯示問題**
~~~python
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解決,即中文字元不會轉為二進位制位元組
~~~
**資金的小數點保留問題**
~~~python
# 本專案就涉及使用者金額資料小數點保留問題。對於會計金融需要非常在意小數點保留問題上,不能簡單使用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
print(s.quantize(decimal.Decimal('0.01'), 'ROUND_UP')) # 0.01
# 可惜的是本專案使用的是json檔案,好像不能存decimal型別的資料。獲取再轉成字串也行吧,回來再試試。
~~~
**re模組匹配數字應用在專案中**
~~~python
import re
def is_number(your_str):
res = re.findall('^\d+\.?\d*$', your_str)
if res:
return True
else:
return False
# 匹配數字,判斷輸入的字串是否是非負數
~~~
**hash模組專案中密碼加密**
~~~python
import hashlib
def hash_md5(info):
m = hashlib.md5()
m.update(info.encode('utf-8'))
m.update('因為相信所以看見'.encode('utf-8')) # 加鹽處理
return m.hexdigest()
# 用於密碼加密
~~~
logging模組專案中記錄日誌
~~~python
# 使用流程:
-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)記錄日誌
~~~
**模組匯入-避免迴圈匯入問題**
~~~python
# 兩種方式避免迴圈匯入問題
- 方式1:如果只有某一個函式需要匯入自定義模組,則在函式區域性作用域匯入模組
- 方式2:後一個匯入者使用import匯入,不要使用from ... import ... 匯入
~~~
**函式物件自動新增字典的bug**
這個bug是在後來思考的時候發現,本專案因為採用了正確的方式避免了這個bug。[具體bug參考這篇部落格](https://www.cnblogs.com/the3times/p/12642577.html)
~~~python
# 自動將功能函式新增到core.src中的func_dict字典。
# 如果將func_dict字典放在一個單獨的py檔案中會方便避免這個bug
# 這個bug的主要原因在於:模組匯入的先後順序和搜尋模組的順序
~~~
------
## 總結
**軟體開發目錄規範**
- 每個人建立目錄規範的樣式不盡相同。這都沒有關係,關鍵是整個專案程式組織結構清晰。
- 目錄規範儘可能遵循大多數人使用的方式,這樣你的程式碼可讀性才會比較友好。
**專案三層架構設計**
- 三層架構設計是一種專案開發的思想方案。一旦確定了這種開發模式,編寫程式碼時刻區分出不同層次的職能。
- 嚴格按照每個層次的職能,不同職能的程式碼放在不同的層次,不要混亂,這樣管理維護起來會很方便。
- 有時候某個功能過於簡單,可以直接訪問資料處理層。但最好還是遵循三層架構設計,不要跨過邏輯介面層。
**存資料不是目的,取才是目的**
- 存資料不是目的,存資料時一定要考慮取資料時的方便。
- 一個好的資料儲存結構和方式,驗證影響取資料時功能程式碼編寫額簡潔和優美。
- 程式 = 資料結構 + 演算法。 所以,好的資料結構,導致取資料功能的難與易。
**封裝程式碼,儘可能重用程式碼**
- 程式中應該儘可能多的在不喪失功能清晰的情況下,儘可能多的考慮程式碼的重用。
- 多編寫通用功能的函式工具,在程式中使用處呼叫之。
------
## 專案原始檔
專案原始檔在百度網盤,感興趣的朋友可以下載參考。
連結:https://pan.baidu.com/s/1GTL081h64tW2SwsHU8kTGw
提取碼:fn6e
程式碼量統計見下圖
![](https://img2020.cnblogs.com/blog/1950650/202004/1950650-20200407145419788-7491666