ATM+购物车项目实战练习
项目开发流程
在实际的项目开发中,我们通常要经历项目需求分析,架构设计,分组开发,提交测试,交付上线等环节,而作为程序员的我们就可能要经历前三个环节。
项目需求分析
ATM+购物车需要账户管理和购物车管理两大功能,这两大功能又可以分为以下几个功能:
1 注册
2 登录 -- 用户账户相关
3 查看账户
4 充值账户
5 提现功能
6 转账功能
7 查看流水 --账户相关
8 添加商品
9 查看购物车
10 结算购物车 --购物车相关
--管理员通道--
-
注册、登录功能用于统筹用户的数据,包括账户数据和购物车数据,都需要存于文件中。
-
账户相关功能和购物车相关功能则是基于用户数据的修改。
项目架构设计
本次项目采取三层架构设计,将实现功能的代码分布于三层结构中,分别为:
-
用户交互层
我们将用户交互层理解为客户端,为了防止有心之人对我们的代码进行修改,所以只要求用户输入和向用户展示信息的代码层(一些简单的逻辑也可以放在这层)
-
核心逻辑层
所有核心的业务逻辑就放在这一层,比如登录功能中密码的比较等
-
数据处理层
只负责数据的增删改查
第二层和第三层可以理解为服务端,而客户端和服务端直接通过网络连接。
采取这种三层架构的好处在于,用户只能访问到他们有权访问的界面,不能通过代码修改的方式来影响我们的核心逻辑判断。而且三层架构的结构也使代码条理清晰,易于拓展维护。
不过目前这个项目,我们暂不做网络编程的演示。
项目搭建
项目目录搭建
myproject # 项目文件夹,可以自定义命名
- bin # 存放项目启动的文件 的文件夹
start.py # 启动文件,也可以命名为软件的名字等
- conf # 配置文件夹,全程configuration--配置
setting.py # 里面通常放一些大写的常量,作为软件起始需要的变量
- core # 核心文件夹,存放一些核心逻辑功能文件
src.py # 存放一些核心的功能
- interface # 接口文件,存放一些接口文件 # 接口可以理解为数据与用户的交互过渡层。
purchase.py
user.py
account.py # 根据业务逻辑分类对应某类功能的接口
- db # 数据及其处理文件夹 --- 可能被其他软件代替,如mysql
userdata.txt # 用户数据等数据文件
db_handler.py # 数据处理的相关功能
- log # 主要存放项目日志文件
log.log # 项目日志文件
- lib # 项目公共功能文件夹
common.py
- README.md # 项目说明书
- requirements.txt
# 模块环境说明书,一个字母不能错,这个文件可以帮助项目参与者快捷的下载模块
项目功能搭建
先用函数封装展示层的功能,将代码的雏形搭建起来:
# src.py内部
def register():pass # 注册功能占位
def login():pass # 登录功能占位
# 剩余各个功能也以同样的方式占位
func_dict = {
'0':quit,
'1':register,
'2':login,
...
}
def run():
while True:
choice = input('''
1 注册
2 登录
3 查看账户
4 充值账户
5 提现功能
6 转账功能
7 查看流水
8 添加商品
9 查看购物车
10 结算购物车
--管理员通道--
输入功能编号'''
)
if choice in func_dict:
func_dict[choice]()
else:
print('请输入正确的功能编号!')
print('正跳转至首页,请稍后')
# start.py内部
import os
import sys
sys.path.append(os.path.dirname(os.path.dirname(__file__))) # 添加根目录作为系统环境变量
if __name__ == '__main__':
from core import src
src.run()
这样就基本的确定了我们这个软件初步要实现的功能。这个程序也是可以从start.py运行的,不过现在是空壳罢了。
注册功能面条--演化-->模块版
面条版注册功能
DB_PATH = os.path.join(os.path.dirname(__file__), 'db')
def register():
while True:
# 1.要求用户输入用户名
username = input('输入您的用户名:').strip()
# 2.判断是否存在该用户
userpath = os.path.join(DB_PATH, 'userdata', f'{username}.json')
if os.path.isfile(userpath):
print('用户名已经被注册了!')
continue
# 3.要求用户输入密码、确认密码
password = input('输入您的密码:').strip()
password_again = input('确认您的密码:').strip()
# 4.判断用户密码是否一致
if password_again == password:
# 5.一致则组织用户字典并存储到json文件中去
user_dict = {
'username': username,
'password': password,
'account': 15000, # 默认注册时有15000的余额
'trolley': {}, # 购物车存储信息
'is_locked':False, # 用户可以被管理员拉黑
'bank_flow':[] # 流水记录
}
with open(userpath, 'w', encoding='utf8') as f:
json.dump(user_dict, f)
print('注册成功')
break
# 密码不一致就重新输入进行注册
print('两次输入的密码不一致!')
上述代码就是一个基础的注册功能,注册该有的核心逻辑都在里面了,即:
- 用户输入用户名,密码
- 判断用户名是否存在
- 判断密码和确认密码是否一致
- 两者都满足的话,组织用户数据格式并存入文件
- 注册成功给予用户提示
模块版注册功能
但是src.py实际上代表的是用户交互层,我们可以通过input限制普通用户只输入用户名和密码,却无法防止一些懂代码的用户扒源码,更改account账户等敏感信息。
-
所以组织用户字典和拼接路径和存储文件应该放在核心逻辑层(第一层-->第一层和第二层)
src.py -- 用户交互层 from interface import user_inf # 导入用户接口功能 def register(): while True: # 1.要求用户输入用户名 username = input('输入您的用户名:').strip() # 2.判断是否存在该用户 if user_inf.user_exist(username): # 判断用户是否存在的功能 print('用户名已经被注册了!') # 3.要求用户输入密码、确认密码 password = input('输入您的密码:').strip() password_again = input('确认您的密码:').strip() # 4.判断用户密码是否一致 if password_again == password: # 5.组织用户字典和存储文件的功能 flag, msg = user_inf.register_inf(username, password) print(msg) # 这个功能返回出了提示信息 if flag: # 还返回了一个运行状态 break else: # 密码不一致就重新输入进行注册 print('两次输入的密码不一致!') interface.user_inf中 -- 核心逻辑层 DB_PATH = os.path.join(os.path.dirname(__file__), 'db') def user_exist(username): userpath = os.path.join(DB_PATH, 'userdata', f'{username}.json') if os.path.isfile(userpath): return True # 如果这个路径存在,说明这个用户已经有了,返回一个状态 # 如果这个路径不存在,说明没有相应的用户,默认返回None(有False属性) def register_inf(username, password): userpath = os.path.join(DB_PATH, 'userdata', f'{username}.json') if os.path.isfile(userpath): return False,'用户已存在' # 增加程序的健壮性 # 组织用户字典 user_dict = { 'username': username, 'password': password, 'account': 15000, # 默认注册时有15000的余额 'trolley': {}, # 购物车存储信息 'is_locked':False, # 用户可以被管理员拉黑 'bank_flow':[] # 流水记录 } # 将用户字典存储为json文件 with open(userpath, 'w', encoding='utf8') as f: json.dump(user_dict, f) return True, '用户注册成功' # 返回运行状态和提示信息
这样就实现了用户交互与核心逻辑的分层。
而在核心逻辑层,其实我们发现处理数据经常要读写文件,则核心逻辑层经常要写
with open。。
的代码,这实际上是属于数据处理的范畴,所以。 -
所以存储文件的功能应该放到第三层(第二层--拆-->第二层和第三层)
interface.user_inf中 -- 核心逻辑层 from db import db_handler def user_exist(username): if db_handler.select(username): return True # 如果拿到了字典,则会走这个分支 def register_inf(username, password): if os.path.isfile(userpath): return False,'用户已存在' # 增加程序的健壮性 # 组织用户字典 user_dict = { 'username': username, 'password': password, 'account': 15000, # 默认注册时有15000的余额 'trolley': {}, # 购物车存储信息 'is_locked':False, # 用户可以被管理员拉黑 'bank_flow':[] # 流水记录 } # 将用户字典存储为json文件 db_handler.save(user_dict) return True, '用户注册成功' # 返回运行状态和提示信息 db.db_handler中 -- 数据处理层 DB_PATH = os.path.join(os.path.dirname(__file__), 'db') def select(username): userpath = os.path.join(DB_PATH, 'userdata', f'{username}.json') if os.path.isfile(userpath): with open(userpath, 'r', encoding='utf8') as f: return json.load(f) # 如果文件存在就返回一个用户字典 # 如果文件不存在则默认返回None def save(user_dict): username = user_dict.get('username') userpath = os.path.join(DB_PATH, 'userdata', f'{username}.json') with open(userpath, 'w', encoding='utf8') as f: json.dump(user_dict, f)
上述代码中,db_handler中对于数据的存取,会在整个项目中运用到,因为我们所有的功能都是围绕着对用户数据的增删改查。
-
还可以将DB_PATH这样的常量归为配置一类,存放于conf.settings中。
conf.settings中 -- 配置文件 import os ROOT_PATH = os.path.dirname(os.path.dirname(__file__)) # 根目录 DB_PATH = os.path.join(ROOT_PATH, 'db') # db文件夹目录
项目其他功能的编写逻辑
在注册功能被拆成模块后,实际上我们的三层架构就已经出来了,在后续功能的编写中,我们只需要将功能按这个架构拆就好了。
登录功能
登录功能的用户交互层,应该执行要求用户输入,用户密码加密,保存用户状态等功能。
def login():
# 判断登录状态
if login_state['username']:
# 如果已用用户登录,询问是否登出
choice = input('you already login before.do you want to logout?(y/n)')
if choice == 'y':
print('已注销原用户')
login_state['username'] = ''
else:
print('继续保持原登录状态')
return
while True:
# 要求用户输入用户名和密码
username = input('login---username>>>(q-quit)').strip()
password = input('login---password>>>').strip()
# 回到首页
if username == 'q':
print('即将跳转首页')
return
# 用户密码加密
password = common.hasher_to_cipher('salt', password)
# 用户密码校验核心逻辑
flag, msg = user_inf.login_inf(username, password)
if flag:
# 登录成功时保存登录态(这里用用户名表示)
login_state['username'] = username
print(msg) # 提示信息
break
print(msg)
而登录认证的核心逻辑层,则是判断用户密码是否和注册时的密码一致。
def login_inf(username, password):
user_dict = db_handler.select(username) # 用数据处理层的函数直接取出用户字典
if not user_dict:
return False, '用户名不存在'
if user_dict.get('password') == password: # 对密码进行判断
return True, f'{username}登录成功'
return False, '密码不正确'
全局认证
程序一经启动,如果用户登录了,我们应该保存用户的登录状态,我们可以在src全局记录一个登录态,这里面存放一个暗号用于对应用户的登录账号(这里就直接存username简单实现一下)
这个用户状态必须在登录成功后记录下来,然后在记录后,可以用username参与对用户数据的修改。
用户判断装饰器
每个账户相关功能和购物车功能都是围绕着用户数据的修改来的,所以必须要等用户登录后,拿到用户名,才能进行相应的操作。这时就要对所有功能加装一个装饰器,新功能就是在执行原功能前进行一次用户态判断,如果用户态不为空,则可以执行原功能,用户态为空则跳转登录功能。
def login_auth(func):
def wrapper(*args, **kwargs):
if login_state['username']: # 判断是否处理登录态
res = func(*args, **kwargs) # 登录则执行原功能
return res
print('还未登录,即将跳转到登录界面')
login() # 还未登录则跳转登录
return wrapper
密码加密
在hashlib模块的博文中我们提到,数据在网络传播中可能被截取,所以有必要对一些密码进行加密,在上述的用户交互层的登录程序中,也引用了一个函数hasher_to_cipher('salt', password)
,这是封装在lib.common.py的一个函数功能,common存的都是一些公共方法,其中的功能,在架构的每一层都有可能被使用。
def hasher_to_cipher(*plain:str):
# 将传入的所有参数编码并加密
hasher = hashlib.md5()
for s in plain:
hasher.update(s.encode('utf8'))
return hasher.hexdigest() # 返回密文
账户相关
账户相关的核心逻辑文件,我们应该和用户功能的user_inf.py
区分开,重新再写一个bank_inf.py
来专门编写银行账户的功能逻辑。这样能使核心逻辑层的功能划分开来,便于我们维护和拓展。
查看账户
查看账户只需要登录就能查看,但是不能直接从第三层访问数据,因为db_handler返回的都是整个字典,用户要查看账户,那就发账户余额返回给他看就行了。
- 用户交互层
@login_auth # 认证装饰器
def check_account():
flag, msg = bank_inf.check_balance_inf(login_state['username']) # 第一层调用第二层
if flag:
print(msg)
- 核心逻辑层bank_inf
def check_balance_inf(username):
user_dict = db_handler.select(username) # 取出用户字典
check_time = common.timer() # timer是公共方法,取出一个时间
user_dict['bank_flow'].append(f'{check_time} 你查看了账户余额') # 记录用户的账户日志给用户查看
db_handler.save(user_dict) # 将改动存回用户文件
return True, f'您的账户余额还有¥{user_dict["account"]}' # 返回运行状态和提示信息
账户还款(存储)
给多少钱,存多少钱,设置单笔最高限额。
- 用户交互层
@login_auth
def save_money():
while True:
money = input('请选择您要存储的金额:').strip() # 仅要求用户输入
if not money.isdigit():
print('请输入数字!')
continue
money = int(money) # 简单的逻辑判断,可以放在任意一层
flag, msg = bank_inf.save_money_inf(login_state['username'], money)
print(msg)
break
- 核心逻辑层
def save_money_inf(username, money):
user_dict = db_handler.select(username) # 取
if money > 100000: # 对于额度的判断
return False, '超出单笔最大限额!'
user_dict['account'] += money
save_money_time = common.timer() # 流水功能
user_dict['bank_flow'].append(f'{save_money_time} 你为账户充值了¥{money}')
db_handler.save(user_dict) # 存
return True, f'已经为您的账户充值了¥{money}'
账户提现
要多少钱,给多少钱,收取一定的手续费。
- 用户交互层
@login_auth
def withdraw():
while True:
check_account()
money = input('您需要提取的金额:') # 要求用户存储
if not money.isdigit(): # 简单的逻辑判断
print('请输入数字!')
continue
money = int(money) # 已经出现两次,可以考虑放到common方法里了
flag, msg = bank_inf.withdraw_inf(login_state['username'], money)
print(msg)
if flag:
# 现实情况,可能是再调一个接口去打开ATM机的出钱口。
break
- 核心逻辑层
def withdraw_inf(username, money):
user_dict = db_handler.select(username)
if money*(1+0.05) > user_dict['account']: # 余额不足不能取现金
return False, 'sorry,your balance is insufficient.you can adjust your withdraw money.'
user_dict['account'] -= money*(1+0.05) # 收取5%的手续费
withdraw_time = common.timer()
user_dict['bank_flow'].append(f'{withdraw_time} 你提现了¥{money},收取您{money*0.05}手续费')
db_handler.save(user_dict)
return True, f'收取您{money*0.05}手续费,您的¥{money}现金已从提现出口发出,注意查收。'
转账功能
指定一个账户给他转指定的金额。
- 要求用户输入转到的账户
- 判断输入的用户是否存在
- 要求用户输入转出的金额
- 限制金额为数字
- 转出金额与账户剩余金额判断
- 用户交互层
@login_auth
def transfer():
while True:
username = input('请输入你想要转账的账户名称(q返回首页):').strip()
if username == 'q':
return
if not user_inf.user_exist(username): # 再次调用用户存在判断
print('sorry,not this user')
continue
money = input('请输入需要转出的金额').strip()
if not money.isdigit():
print('请输入数字!')
continue
money = int(money) # 再次出现数字判断并转换
flag, msg = bank_inf.transfer_inf(login_state['username'], username, money)
print(msg)
if flag:
break
- 核心逻辑层
def transfer_inf(trans_out_user, trans_in_user, money):
out_user_dict = db_handler.select(trans_out_user)
in_user_dict = db_handler.select(trans_in_user) # 取出两人的用户字典
if not (out_user_dict and in_user_dict): # 增加代码健壮性
return False, f'用户不存在'
if money > 20000: # 转账额度限制
return False, f'超出单笔最大转账额度'
if out_user_dict['account'] < money: # 余额是否充足
return False, '余额不足,无法转账'
# 对用户字典进行更改并保存
out_user_dict['account'] -= money
in_user_dict['account'] += money
trans_time = common.timer()
out_user_dict['bank_flow'].append(f'{trans_time}向{trans_in_user}转出¥{money}')
in_user_dict['bank_flow'].append(f'{trans_time}{trans_out_user}向你转入¥{money}')
db_handler.save(out_user_dict)
db_handler.save(in_user_dict)
return True, f'你成功向{trans_in_user}转出¥{money}' # 返回运行成功和提示信息
转换数字小公共方法
common.py中
def num_input(prompt): # 传入提示语
num = input(prompt).strip()
try:
num = float(num)
except ValueError:
return False, '你输入的不是数字!'
return True, num
# 这样在前面的三个小功能中就可以将输入数字的代码转化为以下的代码
flag, money = common.digit_input('请输入需要转出的金额')
if not flag:
print(money)
continue
查看流水
将用户字典调出看bank_flow即可。
- 用户交互层
@login_auth
def check_flow():
flag, msg = bank_inf.check_flow_inf(login_state['username'])
if flag:
print(msg)
- 核心逻辑层
def check_flow_inf(username):
user_dict = db_handler.select(username)
return True, "\n".join(user_dict['bank_flow']) # 返回消息,将列表中的一条条信息用换行符连起来
购物车相关
购物车相关功能为了和其他两块内容区分,其相关逻辑要写在shop_inf.py
中。
添加商品
- 循环让用户挑选商品和数量
- 添加商品前,要从商品列表中遍历出商品信息给用户看
- 用户输入一个编号来挑选商品
- 用户输入数字来确定购买数量
- 将挑选的结果存到用户字典中
- 用户交互层
def add_trolley():
goods_list = shop_inf.get_goods() # 拿出商品信息
temp_trolley = {} # 临时购物车
while True:
# 循环展示商品让用户挑选
for goods_index, goods_info in enumerate(goods_list):
goods, price = goods_info
print(f'{goods_index}|{goods:<8}|¥{price}')
choice = input('请输入你想要挑选的商品编号(q退出):')
if choice == 'q':
break
# 判断数字
if not choice.isdigit():
print('请输入数字编号!')
continue
choice = int(choice)
# 判断编号范围
if choice not in range(1, len(goods_list)):
print('请输入存在的商品编号!')
continue
name, price = goods_list[choice]
# 要求输入购买数量
num = input('请输入你需要添加的数量:')
if not num.isdigit():
print('请输入数字')
continue
num = int(num)
# 有则相加,没有新增键值对
if name in temp_trolley:
temp_trolley[name] += num
else:
temp_trolley[name] = num
shop_inf.add_trolley_inf(login_state['username'], temp_trolley) # 只传入数量和商品名
print('添加成功')
- 核心逻辑层
def add_trolley_inf(username, temp_trolley):
user_dict = db_handler.select(username)
goods_list = db_handler.goods_select() # 定义了个商品信息文件,从这里拿
shop_car = user_dict.get('shop_car')
for goods_name, num in temp_trolley.items():
if goods_name in shop_car:
shop_car[goods_name][1] += num
else:
shop_car[goods_name] = [num, goods_list.get(goods_name)]
# 商品价格这种敏感信息放在核心逻辑层添加
db_handler.save(user_dict)
添加购物车的逻辑是这几个功能里相对复杂一点的,将用户输入的信息通过网络传输过来,即商品名和购买数量,但是价格这类敏感信息放在第二层处理。
查看购物车
- 用户交互层
@login_auth
def check_trolley():
msg, total_price = shop_inf.check_trolley_inf(login_state['username'])
print(msg)
- 核心逻辑层
def check_trolley_inf(username):
user_dict = db_handler.select(username)
shop_car = user_dict['shop_car']
shop_car_infos = ['商品名 |商品单价¥ |数量 |商品总价¥ ']
total_price = 0
for goods_name in shop_car:
price, num = shop_car[goods_name]
items_price = price * num
shop_car_infos.append(f'{goods_name:<8}|¥{price:<10}|{num:<10}|¥{items_price}') # 空格填充
total_price += items_price
shop_car_infos.append(f'所有商品总价:¥{total_price}')
return '\n'.join(shop_car_infos), total_price # 返回展示信息,依然用换行符连接
结算购物车
- 用户交互层
@login_auth
def clear_trolley():
res = shop_inf.clear_trolley_inf(login_state['username'])
msg = res.__next__()
print(msg)
choice = input('是否结算{y/n}:')
if choice == 'y':
flag, msg = res.__next__()
print(msg)
print('下回一定要来买单哦')
- 核心逻辑层
# 其实这样写不符合规范,因为对于用户交互层的编写者,生成器接口不统一,应该写成两个接口。
def clear_trolley_inf(username):
msg, total_price = check_trolley_inf(username)
yield msg
user_dict = db_handler.select(username)
if user_dict['account'] < total_price:
yield False, '你的余额不足'
user_dict['account'] -= total_price
consume_time = common.timer()
user_dict['shop_car'] = {}
user_dict['bank_flow'].append(f'{consume_time} 你消费了¥{total_price}')
db_handler.save(user_dict)
yield True, f'本次消费¥{total_price}'
拓展:管理员功能
- 增加商品选择
- 拉黑用户