ATM项目
目录
主题是带大家快速入门python直至开发一个ATM+购物车系统,ATM的实现类似于银行自助提款机核心业务,购物车的实现类似于淘宝商城购物系统。
该项目的核心不仅在于引领初学者快速入门python项目开发,更是站在项目架构的角度教你如何在程序开发之初合理且优雅地设计程序的架构,从而极大地提升程序的开发效率与可扩展性
项目描述
模拟实现一个ATM + 购物商城程序
- 额度15000或自定义
- 支持多账户登录
- 可以查看账户余额
- 可以提现(可自定义手续费比例)
- 提供还款接口
- 支持账户间转账
- 记录每月日常消费流水
- 锁定用户,密码输错三次,锁定用户
- ATM记录操作日志
- 用户认证功能
- 实现购物商城,买东西加入购物车,调用信用卡接口结账
- 提供管理接口,包括添加账户、用户额度,冻结账户等
项目功能
1. 注册功能
2. 登录功能
3. 提现功能(5%的手续费)
4. 转账功能
5. 充值功能
6. 查看余额
7. 添加流水
8. 查看流水
9. 添加日志
10.认证装饰器
11. 添加购物车
12. 查看购物车
项目技术栈
# 1.python基础核心编程
# 2.函数及相关应用(装饰器)
# 3.常用内置模块(os、sys、json)
软件开发目录规范
ATM架构搭建
# 1.src.py:
def register():
"""
author: # 函数注释的格式(3双引号回车)
email:
date:2023-03-14
每一个功能的逻辑梳理:
:return: # 放函数的返回值
"""
def login():
"""
author:
email:
date:
:return:
"""
def withdraw():
pass
def transfer():
pass
def pay():
pass
def check_money():
pass
def check_flow():
pass
def add_shopping():
pass
def check_shopping():
pass
func_dict = {
'1': register,
'2': login,
'3': withdraw,
'4': transfer,
'5': pay,
'6': check_money,
'7': check_flow,
'8': add_shopping,
'9': check_shopping,
}
def run():
while True:
print("""
-------------------------------------------------------------
1. 注册功能
2. 登录功能
3. 提现功能(5%的手续费)
4. 转账功能
5. 充值功能
6. 查看余额
7. 查看流水
8. 添加购物车
9. 查看购物车
-------------------------------------------------------------
""")
cmd = input('请开始你的表演(输入q结束程序):>>>').strip()
if not cmd: continue
if cmd in func_dict:
func_dict.get(cmd)()
elif cmd == 'q':
break
else:
print('请好好来,别乱来!!!')
# start.py:
import sys, os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR) # 将根目录加入到环境变量中(写活。代码会发给别人,这样在别人的电脑上也能运行)
from core import src
if __name__ == '__main__':
src.run()
ATM面条版(必须掌握):
没有任何封装
start.py
import os
import sys
from core import src
# 把根目录加入到sys.path中
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # E:\python project\day24_ATM
sys.path.append(BASE_DIR)
if __name__ == '__main__': # 执行文件才执行run函数
src.run()
src.py
import os
import json
import time
import hashlib
from lib import common
from conf import settings
# 把加密数据的功能封装成函数
def get_pwd(pwd):
# **************加盐(固态、动态) #*********密码忘记如何更改(需要重新做个功能)
md5 = hashlib.md5()
md5.update(pwd.encode('utf-8'))
new_pwd = md5.hexdigest()
return new_pwd # 可以直接return md5.hexdigest()
# 定义一个全局字典,记录是否登陆成功,用户名
user_data = {
'is_login': False,
'username': None # 也可以使用空字符串
}
注册功能
# 注册功能
def register():
"""
auther:
email:
date:2023-03-15
分析注册功能的逻辑:
1.接收用户名和密码
2.判断用户是否存在
3.数据保存在字典中,保存在文件,单用户单文件
4.用户登录后该函数就不会运行
:return:
"""
print('注册功能开始了:')
# 4.用户已经登录成功后,此函数就不需要进行
if user_data.get('is_login'):
print('已经登录了')
return
while True:
# 1.接收用户输入的用户名和密码
username = input('请输入用户名:>>>').strip()
password = input('请输入密码:>>>').strip()
true_pwd = input('请再次输入密码:>>>').strip()
# 1.2 判断两次密码是否正确
if not password == true_pwd:
print('两次密码输入不正确,请重新输入')
continue
# 2.1 判断用户是否已经注册过了
# 拼接用户文件的路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path_file = os.path.join(BASE_DIR, 'db', '%s.json' % username) # ***********格式化可以使用f{变量名}
# 判断
if os.path.exists(path_file): # ********************判断文件可以使用os.path.isfile(path)
print('不好意思,用户已经存在,请输入其他用户名')
continue
# 2.组装用户的数据,组成字典的格式,直接写入文件中
# 2.2 对密码进行加密处理,直接调用加密函数get_pwd(pwd)
new_pwd = get_pwd(password)
# 用户信息字典
user_dict = {
'username': username,
'password': new_pwd,
'account': 15000,
'locked': False,
'flow': []
}
# 3.写入文件中,单用户数据存储在单文件中,文件的名字:用户名.json
with open(path_file, 'w', encoding='utf-8') as f:
json.dump(user_dict, f)
print(f'恭喜{username}成为我们的会员了!!!')
'''记录一条日志'''
logger = common.get_logger()
ctime = time.strftime('%Y-%m-%d %X')
logger.debug('新用户%s在%s注册成功了' % (username, ctime))
登录功能
# 登录功能
def login():
"""
auther:
email:
date:2023-03-15
分析登录功能的逻辑:
1.接收用户输入的用户名和密码
2.判断该用户是否存在
3.根据用户名读取该用户的详细信息
4.用户名和密码进行比对,先需要对输入的密码进行加密
5.记录密码输错的次数,改了locked要存入文件中
6.判断用户是否被锁定
7.用户已经登录成功后,此函数就不需要进行
:return:
"""
# 7.用户已经登录成功后,此函数就不需要进行
if user_data.get('is_login'):
print('已经登录了')
return
print('登录功能开始了:')
count = 0 # 记错的次数
while True:
# 1.接收用户输入的用户名和密码
username = input('username:>>>').strip()
password = input('password:>>>').strip()
true_pwd = input('请再次输入密码:>>>').strip()
# 1.2 判断两次密码是否一致
if not password == true_pwd:
print('两次密码不一致,请重新输入')
continue
# 2.判断该用户是否存在
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path_file = os.path.join(BASE_DIR, 'db', '%s.json' % username)
# 2.1 可以直接判断该文件是否存在
if not os.path.exists(path_file): # 尽量降低代码的缩进层级,若要使用好多if,可以直接取反,优点:代码简洁,找bug更好找
print('该用户不存在,请注册后再登录') # 文件不存在
# register() # 调注册函数
continue
# 3.根据用户名读取该用户的详细信息
with open(path_file, 'r', encoding='utf-8') as f:
# {"username": "tom", "password": "e10adc3949ba59abbe56e057f20f883e", "account": 15000, "locked": false, "flow": []}
user_dict = json.load(f) # 用户信息
# 6.判断用户是否被锁定
# 如果被锁定,就不需要对用户名和密码比对了
if user_dict.get('locked'):
print(f'用户{username}已经被锁定了,请联系管理员解除锁定!!!')
break
# 4.用户名和密码进行比对,先需要对输入的密码进行加密
# 直接调用加密函数
new_pwd = get_pwd(password)
# 4.1 进行判断
if username == user_dict.get('username') and new_pwd == user_dict.get('password'):
print('登陆成功!!!')
user_data['is_login'] = True # 代表登陆成功
user_data['username'] = username
'''记录一条日志'''
logger = common.get_logger('登录功能')
ctime = time.strftime('%Y-%m-%d %X')
logger.debug('用户%s在%s登录了网站!!!' % (username, ctime))
break
else:
print('用户名或密码错误')
# 5.记录密码输错的次数
# 先定义次数变量(局部变量即可)
count += 1
if count < 3:
continue
'''锁定用户,不能再登录了,请联系管理员解锁'''
user_dict['locked'] = True # 字典取值可以用get(),改值不可以
# 再次写入文件
with open(path_file, 'w', encoding='utf-8') as f1:
json.dump(user_dict, f1)
print('用户已经被锁定,请联系管理员解锁!!!')
break
提现功能
'''验证是否登录的装饰器'''
@common.login_auth
def withdraw():
"""
分析提现功能的逻辑:
1.让用户输入提现金额(判断类型必须是数字类型)
2.读取用户详细信息,取出账户的余额
3. 扣除手续费5%,判断余额是否大于金额加上手续费
4. 把账户余额减去提现金额加上手续费
5. 把新修改的余额存入文件中
6. 添加流水, 记录一条数据
7. 添加日志...
:return:
"""
while True:
# 1.让用户输入提现金额(判断类型必须是数字类型)
money = input('请输入提现金额(q to quit):>>>').strip()
if money == 'q':
break # 退出循环
if not money:
continue # 空字符退出本次循环
if not money.isdigit(): # 100的整数 #***********************整数和浮点型,要有怎么判断
print('请输入整数的金额')
continue
money = int(money)
# 拼接用户的路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path_file = os.path.join(BASE_DIR, 'db', '%s.json' % user_data.get('username')) # ***********格式化可以使用f{变量名}
# 2.读取用户详细信息,取出账户的余额
with open(path_file, 'r', encoding='utf-8') as f:
user_dict = json.load(f)
# 3.扣除手续费5 %,判断余额是否大于金额加上手续费
# 从配置文件中读取手续费费率 #******************************手续费率写成字符串怎么改**********
rate = settings.WITHDRAW_RATE
# 判断账户余额是否大于金额加上手续费
if user_dict.get('account') < money * (1 + rate):
print('余额不足,请先充值')
break
# 4.把账户余额减去提现金额加上手续费
user_dict['account'] -= money * (1 + rate)
'''记录一条流水,暂时先不写,后续回来补充'''
ctime = time.strftime('%Y-%m-%d %X')
user_dict['flow'].append(
f'时间:{ctime}, 提现:{money}元钱,手续费:{money * rate}元钱,账户余额:{user_dict["account"]}元钱')
# 5.把新修改的数据写入到文件中
with open(path_file, 'w', encoding='utf-8') as f1:
json.dump(user_dict, f1, ensure_ascii=False)
print(f'提取成功,提取金额:{money}元,手续费:{money * rate}元')
'''记录一条日志'''
logger = common.get_logger('提现功能')
ctime = time.strftime('%Y-%m-%d %X')
logger.debug('用户%s在%s提现了%s,手续费是:%s' % (user_data.get('username'), ctime, money, money * rate))
转账功能
# 转账功能
@common.login_auth
def transfer():
"""
分析转账的逻辑:
1. 先输入对方账号,在我们这里,其实就是对方的用户名
2. 判断对方账号是否存在,在我们这里就是文件是否存在
3. 判断不能是自己的账号,如果是自己的账号,就不能转
4. 让用户输入转账金额
5. 读取我自己的账号余额,比对余额是否大于转账金额
6. 对方账号余额要增加,我自己的余额要减少,增加和减少的金额肯定是一样的
7. 分别把修改之后的数据写入到文件中
8. 分别写入一条流水
9. 写日志
:return:
"""
while True:
# 1. 先输入被转账的用户名
to_user = input('请输入您要转账的用户名:>>>').strip()
if to_user == 'q': # 输入q退出程序
break
# 2.判断被转账的用户名是否存在,必须要存在,不存在不能转账
# 拼接用户文件路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
to_path_file = os.path.join(BASE_DIR, 'db', '%s.json' % to_user)
# 判断文件是否存在
if not os.path.isfile(to_path_file):
print('用户不存在,不能转账')
continue
# 3.用户不能是本人
if user_data.get('username') == to_name:
print('不能给本人转账')
# 4.让用户输入转账金额
transfer_money = input('请输入转账的金额:>>>').strip()
# 参数判断
if not transfer_money:
continue
if not transfer_money.isdigit():
print('请输入整数的金额')
continue
transfer_money = int(transfer_money)
# 5.判断用户的账号余额是不是足够
from_path_file = os.path.join(BASE_DIR, 'db', '%s.json' % user_data['username'])
with open(from_path_file, 'r', encoding='utf-8') as f1:
from_user_dict = json.load(f1)
if from_user_dict.get('account') < transfer_money:
print('用户余额不足,请先充值')
break
# 6.把我自己的账户余额减掉transfer_money
from_user_dict['account'] -= transfer_money
'''记录一条流水'''
ctime = time.strftime('%Y-%m-%d %X')
from_user_dict['flow'].append('在%s时间往%s账户中转出%s元钱' % (ctime, to_user, transfer_money))
# 写入文件中
with open(from_path_file, 'w', encoding='utf-8') as f2:
json.dump(from_user_dict, f2, ensure_ascii=False)
print(f'给{to_user}转账{transfer_money}元成功!!!')
'''添加日志'''
logger = common.get_logger()
ctime = time.strftime('%Y-%m-%d %X')
logger.debug('用户%s在%s向%s转账了%s元' % (user_data.get('username'), ctime, to_user, transfer_money))
# 7.把对方的钱加上transfer_money
# 读取对方文件详细信息
with open(to_path_file, 'r', encoding='utf-8') as f3:
to_user_dict = json.load(f3)
# 加上转账金额
to_user_dict['account'] += transfer_money
'''再记录一条流水'''
ctime = time.strftime('%Y-%m-%d %X')
to_user_dict['flow'].append('在%s时间收到了%s转了%s元钱' % (ctime, user_data['username'], transfer_money))
# 写入对方文件中
with open(to_path_file, 'w', encoding='utf-8') as f4:
json.dump(to_user_dict, f4, ensure_ascii=False)
# 8.输出信息
print('转账成功了')
'''添加日志'''
logger = common.get_logger()
ctime = time.strftime('%Y-%m-%d %X')
logger.debug('用户%s在%s接收到%s账户转账了%s元' % (to_user, ctime, user_data.get('username'), transfer_money))
充值功能
# 充值功能
@common.login_auth
def pay():
"""
分析充值的逻辑:
1.要输入充值的金额,添加参数判断
2.读取用户账户余额,加上充值的金额
3.把新修改的数据写
4.写入到文件中
5.添加流水,记录一条数据
6.添加日志
:return:
"""
while True:
# 1.要输入充值的金额,添加参数判断
money = input('请输入要充值的金额:>>>').strip()
if money == 'q':
break
if not money:
continue
if not money.isdigit(): # 100的整数
print('请输入充值的整数')
continue
money = int(money)
# 2.读取用户账户余额,加上充值的金额
# 拼接用户文件路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path_file = os.path.join(BASE_DIR, 'db', '%s.json' % user_data.get('username'))
with open(path_file, 'r', encoding='utf-8') as f:
user_dict = json.load(f)
# 3.新修改的数据
user_dict['account'] += money
'''记录一条流水'''
ctime = time.strftime('%Y-%m-%d %X')
user_dict['flow'].append(f'在{ctime}时间充入了{money}元钱')
# 4.更改后的数据再次写入文件中
with open(path_file, 'w', encoding='utf-8') as f1:
json.dump(user_dict, f1, ensure_ascii=False)
print('充值成功')
'''添加日志'''
logger = common.get_logger('充值功能')
ctime = time.strftime('%Y-%m-%d %X')
logger.debug('用户%s在%s充值了%s元钱!!!' % (user_data.get('username'), ctime, money))
查看余额
# 查看余额
@common.login_auth
def check_money():
"""
分析查看余额的逻辑:
用户已经登录成功说明文件一定存在
1.拼接路径
2.读取用户文件详细信息
3.输出账户余额
:return:
"""
# 1.拼接路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path_file = os.path.join(BASE_DIR, 'db', '%s.json' % user_data['username'])
# 2.读取用户文件详细信息
with open(path_file, 'r', encoding='utf-8') as f:
user_dict = json.load(f)
# 3.输出账户余额
print('账户余额:%s' % user_dict.get('account'))
查看流水
# 查看流水
def check_flow():
"""
查看流水的逻辑:(与金额有关的才加流水)
用户已经登录成功说明文件一定存在
1.拼接路径
2.读取用户文件详细信息
3.循环输出每一条账户流水
:return:
"""
# 1.拼接路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path_file = os.path.join(BASE_DIR, 'db', '%s.json' % user_data['username'])
# 2.读取用户文件详细信息
with open(path_file, 'r', encoding='utf-8') as f:
user_dict = json.load(f)
# 3.判断流水是否为空
if not user_dict.get('flow'):
print('流水为空')
return
# 4.输出账户流水
for i in user_dict.get('flow'):
print(i)
添加购物车
# 添加购物车
@common.login_auth
def add_shopping():
"""
分析添加购物车的逻辑:
1.先有商品列表,我们模拟数据
2.让用户选择要添加的商品序号
3.我们定义一个全局变量来保存用户选择的商品信息
4.把用户选择的商品信息写到文件里面
5.写日志
shopping_cart = {
'商品名称1': {'price': 100, 'count': 1},
'商品名称2': {'price': 200, 'count': 2},
'商品名称3': {'price': 300, 'count': 3}}
:return:
"""
print('添加购物车开始了:')
# 1.定义一个空购物车
shopping_cart = {}
while True:
# 2.先模拟出商品,定义商品列表
goods_list = [
['tea', 1000],
['coffee', 2000],
['water', 2000],
['redbull', 3000],
['hulatang', 3000],
]
# 3.输出商品列表
for i, j in enumerate(goods_list, start=1): # 1 ['tea', 1000]
print(i, j[0], j[1]) # 2 coffee 2000
# print(' %s | %s | %s ' % (i, j[0], j[1]))
# 4.让用户输入商品序号
goods_num = input('请输入你要选择的商品序号(输入q结束选择商品):').strip()
# 5.直接判断序号
if goods_num.isdigit():
goods_num = int(goods_num)
# 5.1 商品序号在商品列表范围内
if goods_num in range(1, len(goods_list) + 1):
# 5.1.1 取出列表中的商品名称和商品价格 # 1 ['tea', 1000]
goods_name = goods_list[goods_num - 1][0] # 索引=序号-1
goods_price = goods_list[goods_num - 1][1]
# 5.1.2 如果商品不在购物车中,就把这个商品加到购物车(字典新增键值对),数量是1 {'商品名称': {'price': 价格, 'count': 1}}
if goods_name not in shopping_cart:
shopping_cart[goods_name] = {'price': goods_price, 'count': 1}
else:
# 5.1.3 如果商品在购物车中,数量增加1个
shopping_cart[goods_name]['count'] += 1
print("选择的商品已经加入成功,请继续!")
elif goods_num == 'q':
'''6.如果用户输入了q,说明就结束,把字典写入到文件中'''
# 6.1 判断不是购物车不是空字典,字典不为空,才写入
if shopping_cart:
# 拼接路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path_file = os.path.join(BASE_DIR, 'db', '%s.json' % user_data['username'])
# 读取文件
with open(path_file, 'r', encoding='utf-8') as f:
user_dict = json.load(f)
# 增加购物车
user_dict['shopping_cart'] = shopping_cart
# 写入文件
with open(path_file, 'w', encoding='utf-8') as f1:
json.dump(user_dict, f1)
print('添加购物车成功!!!')
break
else:
print('输入数据不合法,请重新输入')
查看购物车
# 查看购物车
@common.login_auth
def check_shopping():
"""
查看购物车的逻辑:
用户已经登录成功说明文件一定存在
1.拼接路径
2.读取用户文件
3.判断是否有购物车信息
4.取出购物车信息
:return:
"""
# 1.拼接路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
path_file = os.path.join(BASE_DIR, 'db', '%s.json' % user_data['username'])
# 2.读取文件中购物车信息
with open(file_path, 'r', encoding='utf-8') as f:
user_dict = json.load(f)
# 3.判断是否有购物车信息
if not user_dict.get('shopping_cart'):
print('你还没有添加购物车,请先去添加')
return
# 4.取出购物车信息
# {'coffer': {'price': 3000, 'count': 2}, 'water': {'price': 5000, 'count': 1}, 'tea': {'price': 1000, 'count': 1}}
for k, v in user_dict.get('shopping_cart').items():
print(f'{k}: {v}')
src.py
func_dict = {
'1': register,
'2': login,
'3': withdraw,
'4': transfer,
'5': pay,
'6': check_money,
'7': check_flow,
'8': add_shopping,
'9': check_shopping,
}
def run():
while True:
print("""
=======================ATM 项目=======================
1. 注册功能
2. 登录功能
3. 提现功能(5%的手续费)
4. 转账功能
5. 充值功能
6. 查看余额
7. 查看流水
8. 添加购物车
9. 查看购物车
======================== q to quit ====================
""")
cmd = input('请输入你的选择(输入q结束程序):>>>').strip()
if cmd == 'q':
break # 输入q,结束循环
if not cmd:
continue # 如果cmd为空,是假,结束本次循环
if cmd in func_dict:
func_dict.get(cmd)()
else:
print('输入不合法,请重新输入')
common.py
import logging
import logging.config
from core import src
from conf import settings
# 验证是否登录的装饰器
def login_auth(func):
"""
验证是否登录的装饰器
:param func:
:return:
"""
def inner(*args, **kwargs):
if src.user_data.get('is_login'):
res = func(*args, **kwargs)
return res
else:
print('您未登录,请先登录')
src.login()
return inner
# 调用日志
def get_logger(name='', ):
logging.config.dictConfig(settings.LOGGING_DIC) # 自动加载字典中的配置
logger = logging.getLogger(name)
return logger
settings.py
import os
# # 路径配置
# BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# DB_DIR = os.path.join(BASE_DIR, 'db')
# if not os.path.exists(DB_DIR): # 判断db文件夹存不存在,不存在则建db文件夹
# os.mkdir('db')
# 配置中提现的费率
WITHDRAW_RATE = 0.05
# 日志格式
standard_format = '[%(asctime)s][%(threadName)s:%(thread)d][task_id:%(name)s][%(filename)s:%(lineno)d]' \
'[%(levelname)s][%(message)s]' # 其中name为getlogger指定的名字
simple_format = '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d]%(message)s'
# 拼接路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
log_path = os.path.join(BASE_DIR, 'log')
# 判断日志文件是否存在
if not os.path.exists(log_path):
os.mkdir(log_path)
# 日志保存的路径
logfile_path = os.path.join(log_path, 'log.log')
# log配置字典
LOGGING_DIC = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': standard_format
},
'simple': {
'format': simple_format
},
},
'filters': {}, # 过滤日志
'handlers': {
# 打印到终端的日志
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler', # 打印到屏幕
'formatter': 'simple'
},
# 打印到文件的日志,收集info及以上的日志
'default': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler', # 保存到文件
'formatter': 'standard',
'filename': logfile_path, # 日志文件
'maxBytes': 1024 * 1024 * 5, # 日志大小 5M
'backupCount': 5,
'encoding': 'utf-8', # 日志文件的编码,再也不用担心中文log乱码了
},
},
'loggers': {
# logging.getLogger(__name__)拿到的logger配置 空字符串作为键 能够兼容所有的日志
'': {
'handlers': ['default', 'console'], # 这里把上面定义的两个handler都加上,即log数据既写入文件又打印到屏幕
'level': 'DEBUG',
'propagate': True, # 向上(更高level的logger)传递
}, # 当键不存在的情况下 (key设为空字符串)默认都会使用该k:v配置
},
}