ATM购物车项目

模拟实现一个ATM + 购物商城程序。

该程序实现普通用户的登录注册、提现充值还款等功能,并且支持到网上商城购物的功能。

账户余额足够支付商品价格时,扣款支付;余额不足时,无法支付,商品存放个人购物车。

如果用户具有管理员功能,还支持管理员身份登录。具体需求见项目需求部分。


三层架构

项目开发中,清晰明了的结构设计非常重要。它的重要性至少体现在三个方面:结构清晰;可维护性强;可扩展性高。

常用的项目结构设计中,三层架构设计非常实用。这种架构设计模式将整个程序分为三层:

  • 用户视图层:用户交互的,可以接受用户的输入数据,展示显示的消息。
  • 逻辑接口层:接收视图层传递过来的参数,根据逻辑判断调用数据层加以处理并返回一个结果给用户视图层。
  • 数据处理层:接受接口层传递过来的参数,做数据的增删改查。
# 优点:结构清晰,职责明了。扩展性强,好维护。对数据比较安全。
# 缺点:每个功能都要跨越逻辑接口层,不能直接访问数据库,所以效率会降下来。

ATM购物车项目+三层架构设计-LMLPHP


项目需求

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购物车项目+三层架构设计-LMLPHP

程序结构:遵循软件开目录规范

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

代码量统计见下图

ATM购物车项目+三层架构设计-LMLPHP

04-07 21:54