专栏首页奔跑的键盘侠Python—蜕变的必经之路(从脚本到应用程序)

Python—蜕变的必经之路(从脚本到应用程序)

AT LAST!

IN THE END!!

FINALLY!!!

这个周总算度过去了,这一个周的时间都在做ATM的一个作业,这个题目是写一个应用程序,而不是写一个简单的脚本代码。对于入门选手来讲,这个还是挺难的,之前一直就是一个文档搞定所有功能,而应用程序是要组织框架的,要有入口程序、有配置文件、核心文件文件、数据库、日志、使用说明等等。反正第一眼我是极度懵逼的,不过好在已经顺利渡劫,不然我也不会用3个英文词语来开头本篇文章了

当然作为业余选手,也不必有太大压力,主要还是follow your heart,如果只是做一些自用的小程序,代码揉成在一个文档的确省时省力,没必要搞什么过于复杂的框架。但是有些需要可能经常需要维护的代码,还是要做好注释,免得维护代价很高。

下面就简单讲一下这期的内容,题目如下:

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

额度 15000或自定义
实现购物商城,买东西加入 购物车,调用信用卡接口结账
可以提现,手续费5%
每月22号出账单,每月10号为还款日,过期未还,按欠款总额 万分之5 每日计息
支持多账户登录
支持账户间转账
记录每月日常消费流水
提供还款接口
ATM记录操作日志
提供管理接口,包括添加账户、用户额度,冻结账户等。。。
用户认证用装饰器

看完题目要求,我整个人都不好了

当然老师也说了,前面几期的培训,这个作业布置下去一大半学生弃坑了;这个题目是整个学习过程中最难的一个,抛除框架不说,里面的还款是一个难点(又是账单日,又是计息的),还要跟购物商城进行对接。

而我为了省时省力,还是按着alex的框架、代码思路写了一下(大概抄袭了90%以上吧

),当然,也只是完成了其中一部分功能,就这样也花费了我差不多一个周的业余时间。

我觉着,如果再写下去,一个周时间不一定够用,如果写上一个月,可以封包了卖给谁谁了

但是毫无意义。接下来讲一下完成的大概情况吧:

  • 购物商城就没写了,之前一期有写过一个纯购物的帖子,加入购物车的。其实这个应用,购物车跟信用卡是2个相互独立的程序,只需在结算的时候提供一个接口进行认证、判断后划账即可,认识到这点,代码实现就比较容易了。
  • 每月10号还款日,逾期收手续费,这个的确有点复杂,没实现。想一下脑袋都大

可能也许大概我觉着:账单单独存一个字典值(当期账单日、当期金额、当期是否已还清等),如果账单日前还款,就当期账单值全部清零;如果没还清,就计算出未还清金额,等延期还款时,根据未还部分、金额进行计算利息。如果,如果有多期未还款的,就各期分别计算。当然,要细分到银行的惯用手法,逐笔消费逐笔算逾期天数的,其实也不难,字典中将每笔消费金额、发生日期、归属哪一期存下来,再分别计算即可。

  • 转账功能,没写。
  • 消费流水也没写,但是都存在操作日志里,调用一下即可。

添加账户操作接口也没写,但是有一个用户数据生成器,改一下也不难。

冻结账户,也没写,其实就是status值修改一下,然后根据账号status值进行登陆或者某些功能限制,貌似也不难。

框架结构如下:

├── README
├── atm #ATM主程目录
│   ├── __init__.py
│   ├── bin #ATM 执行文件 目录
│   │   ├── __init__.py
│   │   ├── atm.py  #ATM 执行程序
│   │   └── manage.py #ATM 管理端,未实现
│   ├── conf #配置文件
│   │   ├── __init__.py
│   │   └── settings.py
│   ├── core #主要程序逻辑都 在这个目录 里
│   │   ├── __init__.py
│   │   ├── accounts.py  #用于从文件里加载和存储账户数据
│   │   ├── auth.py      #用户认证模块
│   │   ├── db_handler.py   #数据库连接引擎
│   │   ├── logger.py       #日志记录模块
│   │   ├── main.py         #主逻辑交互程序
│   │   └── transaction.py  #记账\还钱\取钱等所有的与账户金额相关的操作都 在这
│   ├── db  #用户数据存储的地方
│   │   ├── __init__.py
│   │   ├── account_sample.py #生成一个初始的账户数据 ,把这个数据 存成一个 以这个账户id为文件名的文件,放在accounts目录 就行了,程序自己去会这里找
│   │   └── accounts #存各个用户的账户数据 ,一个用户一个文件
│   │       └── 1234.json #一个用户账户示例文件
│   └── log #日志目录
│       ├── __init__.py
│       ├── access.log #用户访问和操作的相关日志
│       └── transactions.log    #所有的交易日志
└── shopping_mall #电子商城程序,未实现。
    └── __init__.py
    
atm.py
# !/usr/bin/env python3.6
# -*- coding: utf-8 -*-
#__author__: Ed Frey
#date:  2018/8/14
import os
import sys
BASE_DIR = os.path.abspath('..')  #取父级路径
sys.path.append(BASE_DIR)
from core import main
import time
if __name__ == '__main__':
    main.run()
settings.py
import os
import logging
BASE_DIR = os.path.abspath('..')
DATABASE ={                      #数据库
    'engine': 'file_storage', #support mysql.postgresql in the future
    'name': 'accounts',
    'path': "%s%s%s" %(BASE_DIR,os.sep,"db")
}
LOG_LEVEL = logging.INFO  #
LOG_TYPES={
    'transaction': 'transactions.log',
    'access': 'access.log'
}
TRANSACTION_TYPE = {
    'repay':{'action':'plus', 'interest':0},
    'withdraw':{'action':'minus', 'interest':0.05},
    'transfer':{'action':'minus', 'interest':0.05},
    'consume':{'action':'minus', 'interest':0},
}
DISPLAY_INFO= ['id','credit','balance','expire_date','status']

main.py
'''
main program handle module , handle all the user interaction stuff
'''
import os,sys
sys.path.append(os.path.abspath('..'))
from core import logger
from core import auth
from core import accounts
from core import transaction
from conf import settings
#transaction logger
trans_logger = logger.logger('transaction')
#access logger
access_logger = logger.logger('access')
#temp account data , only saves the data in memory
user_data = {
    'account_id':None,
    'is_authenticated':False,
    'account_data':None
}
def login_required(func):
    '''
    a decorator, used to check whether logged in or not, if not turned to start.
    :param func:
    :return:
    '''
    def wrapper(acc_data):
        if acc_data['is_authenticated'] == True:
            func(acc_data)
        else:
            print('\033[31;1mYou have not login!\033[0m')
    return wrapper
@login_required  # account_info=login_required(account_info)
def account_info(acc_data):
    account_data = accounts.load_current_balance(acc_data['account_id'])
    for content in settings.DISPLAY_INFO:
        print(content,account_data[content])
    exit_flag = False
    return exit_flag
@login_required  # account_info=login_required(account_info)
def repay(acc_data):
    '''
    print current balance and let user repay the bill
    :param acc_data:
    :return:
    '''
    account_data =accounts.load_current_balance(acc_data['account_id'])
    # for content in settings.DISPLAY_INFO:
    #     print(content,account_data[content])
    current_banlance = '''—————————BALANCE INFO—————————
        Credit :    %s
        Balance:    %s''' %(account_data['credit'],account_data['balance'])
    print(current_banlance)
    back_flag = False
    while not back_flag:
        repay_amout = input("\033[33;1mInput repay amount:\033[0m").strip()
        if len(repay_amout) > 0 and repay_amout.isdigit():
            print('repay_amout %s' %repay_amout)
            new_balance = transaction.make_transaction(trans_logger,account_data,'repay',repay_amout)
            if new_balance:
                print('''\033[42;1mNew Balance:%s\033[0m'''%(new_balance['balance']))
        else:
            print('\033[31;1m[%s] is not a valid amount, only accept integer!\033[0m' %repay_amout)
        choice = input("\033[33;1mPlease input 'r' to return back.\033[0m").strip()
        if choice == 'r':
            back_flag =True
    exit_flag = False
    return exit_flag

@login_required  # account_info=login_required(account_info)
def withdraw(acc_data):
    '''
    print current balance ande let user do the withdraw action
    :param acc_data:
    :return:
    '''
    account_data = accounts.load_current_balance(acc_data['account_id'])
    current_banlance = '''—————————BALANCE INFO—————————
            Credit :    %s
            Balance:    %s''' % (account_data['credit'], account_data['balance'])
    print(current_banlance)
    back_flag = False
    while not back_flag:
        withdraw_amout = input("\033[33;1mInput withdraw amount:\033[0m").strip()
        if len(withdraw_amout) > 0 and withdraw_amout.isdigit():
            print('withdraw_amout %s' % withdraw_amout)
            new_balance = transaction.make_transaction(trans_logger, account_data, 'withdraw', withdraw_amout)
            if new_balance:
                print('''\033[42;1mNew Balance:%s\033[0m''' % (new_balance['balance']))
        else:
            print('\033[31;1m[%s] is not a valid amount, only accept integer!\033[0m' % withdraw_amout)
        choice = input("\033[33;1mPlease input 'r' to return back.\033[0m").strip()
        if choice == 'r':
            back_flag =True
    exit_flag = False
    return exit_flag

@login_required  # account_info=login_required(account_info)
def transfer(acc_data):
    pass

@login_required  # account_info=login_required(account_info)
def pay_check(acc_data):
    pass

def logout(acc_data):
    acc_data = None
    exit_flag = True
    return exit_flag

def interactive(acc_data):
    '''
    interact with user
    :param acc_data:
    :return:
    '''
    menu =u'''
    ————————ABC BANK————————
    \033[32;1ml.  account info
    2.  repayment
    3.  withdraw
    4.  transfer
    5.  bill
    6.  logout
    \033[0m'''
    menu_dic = {
        "1": account_info,
        "2": repay,
        "3": withdraw,
        "4": transfer,
        "5": pay_check,
        "6": logout,
    }
    exit_flag = False
    while not exit_flag:
        print(menu)
        user_option = input(">>:").strip()
        if user_option in menu_dic:
            exit_flag = menu_dic[user_option](acc_data)
        else:
            print("\033[31;1mOption does not exist!\033[0m")

def run():
    '''
    this function will be called right away when the program started, here
    :return
    '''
    print('START MENU')
    acc_data = auth.acc_login(user_data,access_logger)
    #acc_login() means : the loaded data (with all the informations of the account in json db_file,without password;and the user_data is uppdated)

    if user_data['is_authenticated']:
        user_data['account_data'] = acc_data
        access_logger.info('welcom ,you(%s) have logged in' % user_data['account_id'])
        interactive(user_data)

if __name__ == '__main__':
    run()
account.py
from core import db_handler
from conf import settings
import os
import json

def load_current_balance(account_id):
    '''
    return account balance and other basic info ,live update
    :param account_id:
    :return:
    '''
    db_path = db_handler.db_handler(settings.DATABASE)      #the direction is ..\db\accounts
    account_file = "%s%s%s.json" %(db_path,os.sep,account_id)  #the database file is ..\db\accounts\1234.jason
    with open(account_file, 'r') as f:
        acc_data = json.load(f)
        return acc_data

def dump_account(account_data):
    '''
    when transaction happened or account data changed,dump it back to file db
    :param account_data:
    :return:
    '''
    db_path = db_handler.db_handler(settings.DATABASE)      #the direction is ..\db\accounts
    # print(account_data)    account_file = "%s%s%s.json" %(db_path,os.sep,account_data['id'])  #the database file is ..\db\accounts
    with open(account_file,'w') as f:
        account_data = json.dump(account_data,f)
    return True

os.py
import os
import json
import time
from conf import settings
from core import db_handler
def acc_auth(account,password):
    '''
    account auth func
    :param account: credit account number
    :param password: credit card password
    :return: if passed the authentication , return the account object , otherwise return None
    '''
    account_data = db_handler.db_api(account)
    if account_data['password'] == password:
        exp_time_stamp = time.mktime(time.strptime(account_data['expire_date'],'%Y-%m-%d %H:%M:%S'))
        if time.time() > exp_time_stamp:
            print("\033[31;1mAccount [%s] has expired , please contact the customer service phone number:95555 \033[0m")
        else: #passed the authentication
            return account_data         #if not expired return the loaded data (with all the informations of the account in json db_file)
    else:
        print("\033[31;1mAccount ID or password is incorrect,try again please \033[0m")
def acc_login(user_data,log_obj):
    '''
    account login func
    :param user_data: user info data , only saves in memory
    :param log_obj: logging object
    :return:
    '''
    retry_count = 0
    while user_data['is_authenticated'] is not True and retry_count <3:
        account = input("\033[32;1maccount:\033[0m").strip()
        password = input("\033[32;1mpassword:\033[0m").strip()
        auth = acc_auth(account,password)   # the loaded data (with all the informations of the account in json db_file)
        if auth: #not None means passed the authentication
            user_data['is_authenticated'] = True
            user_data['account_id'] = account
            print('welcome')
            return auth   # the loaded data (with all the informations of the account in json db_file;and the user_data is uppdated)
        retry_count += 1
    else:
        log_obj.error("account [%s] too many login attemps" %account)
        exit()
db_handler.py
'''
handle all the database interactions
'''
from conf import settings
from core import db_handler
import os
import json

def file_db_handle(conn_params):
    '''
    parse the db file path
    :param conn_params: the db connection params set in settings
    :return:
    '''
    # print('file db:',conn_params)
    db_path = '%s%s%s' %(conn_params['path'],os.sep,conn_params['name'])
    return db_path

def db_handler(conn_params):
    '''
    connect to db
    :param conn_params: the db connection params set in settings
    :return: a    '''
    if conn_params['engine'] == 'file_storage':
        return file_db_handle(conn_params)

def file_storage(account):
    db_path = db_handler(settings.DATABASE)  # the direction is ..\db\accounts
    account_file = "%s%s%s.json" % (db_path, os.sep, account)  # the database file is ..\db\accounts\1234.jason
    # print(account_file)
    if os.path.isfile(account_file):
        with open(account_file, 'r') as f:
            account_data = json.load(f)
    return account_data

def db_api(account, **kwargs):
    '''
    get account information of regestered for checking login.
    :param kwargs:
    :return:
    '''
    db_api_tpye = settings.DATABASE['engine']
    if db_api_tpye == 'file_storage':
        data = file_storage(account)
    elif db_api_tpye == 'mysql':    ##if database is mysql, here can be used.
        data = "***"
    return data
logger.py
import logging
import os
from conf import settings

def logger(log_type):
    # create logger
    logger = logging.getLogger(log_type)
    logger.setLevel(settings.LOG_LEVEL)
    # &&&&&

    # create console handler and set level to debug
    ch = logging.StreamHandler()
    ch.setLevel(settings.LOG_LEVEL)

    # create file handler and set level to warning
    log_file = '%s%slog%s%s' %(settings.BASE_DIR,os.sep,os.sep,settings.LOG_TYPES[log_type])
    fh = logging.FileHandler(log_file)
    fh.setLevel(settings.LOG_LEVEL)
    # create formatter
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

    # add formatter to ch and fh
    ch.setFormatter(formatter)
    fh.setFormatter(formatter)

    # add ch and fh to logger
    logger.addHandler(ch)
    logger.addHandler(fh)

    return logger
    
transaction.py
from conf import settings
from core import accounts
def make_transaction(log_obj,account_data,tran_type,amount,**others):
    '''
    deal all the user transactions
    :param log_obj:
    :param account_data: user account data
    :param tran_type: transaction type
    :param amount: transaction amount
    :param others: mainly for logging usage
    :return:
    '''
    amount = float(amount)
    if tran_type in settings.TRANSACTION_TYPE:
        interest = amount * settings.TRANSACTION_TYPE[tran_type]['interest']
        old_balance = account_data['balance']
        if settings.TRANSACTION_TYPE[tran_type]['action'] == 'plus':
            new_balance = old_balance + amount + interest
        elif settings.TRANSACTION_TYPE[tran_type]['action'] == 'minus':
            new_balance = old_balance - amount - interest
            #check credit            if new_balance <0:
                print('''\033[32;1mYour credit is not enough for this transaction amount [%s],your balance is only 
              [%s]''' %((amount + interest), old_balance))
                return

        account_data['balance'] = new_balance
        accounts.dump_account(account_data)
        log_obj.info("accounts:%s   action:%s   amount:%s interest:%s" %
                     (account_data['id'],tran_type,amount,interest))
        return account_data
    else:
        print("\033[31;1mTransaction type [%s] is not exits\033[0m" %tran_type)
account_sample.py
import json
import os
data = {
    "id": 1234,
    "password": "abc",
    "credit": 50000,
    "balance": 16223.5,
    "enroll_date": "2016-01-01 23:59:59",
    "expire_date": "2021-01-01 23:59:59",
    "pay_day": 22,
    "status": 0, # 0 = normal, 1 =locked, 2 = disabled
}
path_yielder = r"%s%sdb%saccounts%s%s.json" %(os.path.abspath('..'),os.sep,os.sep,os.sep,data["id"])
with open(path_yielder,'w') as f:
    json.dump(data,f)
    
1234.json
{"id": 1234, "password": "abc", "credit": 50000, "balance": 16223.5, "enroll_date": "2016-01-01 23:59:59", "expire_date": "2021-01-01 23:59:59", "pay_day": 22, "status": 0}
access.log
transaction.log这2个文件分别是登陆,交易记录产生的日志。

上面可能看着有点凌乱,毕竟写了好久,上一个dos环境下运行的截图吧:
登陆账号1234,密码abc。按1查询账户信息,余额16233.5元,直接repay1个亿
然后r返回上一层,重新按1查询,瞬间暴富,有木有

dos下的代码看起来效果很low,再来一张pycharm下的运行情况:

五颜六色的,看起来还是挺直观的有木有

再追加它2个亿,再次走向人生巅峰

最后进入这期的重点,搞这么复杂的逻辑框架,方便的是后期的维护、拓展。比如利息要修改,直接conf配置表里面改一下数字重启一下服务器即可,代码压根都不需要维护。再比如现在用的是json格式文本存储的用户信息,将来改用mysql数据库,只需要写一个数据库取值的函数即可,简单明了。再比如功能正式上线后需要调试,只需配置表改一下LOG_LEVEL的级别即可。总之,要增删改什么功能,直接定位到对应的模块去修改一定要清晰明了。如果一个程序代码揉成一团,修改某个功能,很有可能引起整个程序崩溃。

这,就是脚本跟应用程序的不同之处。

代码打包放到百度网盘上,有需要的小伙伴可以自行下载。

链接:https://pan.baidu.com/s/1LtXAHybDtYJjRkbYLGo0oA 密码:x27v

本文分享自微信公众号 - 奔跑的键盘侠(runningkeyboardhero),作者:@以德服人c

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-08-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 按键精灵——如何实现excel办公自由

    去年有写过一篇按键精灵关于excel操作的帖子,很多小伙伴觉得不过瘾,而且按键自带的office插件命令少的可怜。

    Ed_Frey
  • Python——关于排序算法(快速排序法)

    最近一直在写排序的算法,可能讲到合并排序法,很多人就会有点晕乎了,还是要多多研究练习,才能得法。包括我也是,看教程的时候感觉懂了,开始写的时候感觉都忘记了,再复...

    Ed_Frey
  • 按键精灵——自动关闭广告弹窗

    其实学习按键精灵最最关键的,就是学以致用,投入到生产实践中,提高工作效率。这也是为什么很多人热衷于它的重要原因。

    Ed_Frey
  • 重新思考单元测试

    摘要: 单元测试应该是程序员的必备技能,而真正的编程高手应该善于把握单元测试的粒度。

    Fundebug
  • 相同update语句在MySQL,Oracle的不同表现(r12笔记第30天)

    今天有个朋友问我一个SQL问题,大体是一个update语句,看起来逻辑没有问题,但是执行的时候却总是报错。 语句和报错信息为: UPDATE paymen...

    jeanron100
  • JS实现运算符重载

    IT故事会
  • 代码测试意味着完全消灭了Bug?

    我使用过的一些最难用的代码是“易于测试”的代码。代码将所有内容抽象到开发者难以想象发生了什么的程度,只是为了向原本非常简单的函数中添加“单元测试”。DHH 称这...

    AI科技大本营
  • 一种基于沙箱的动态测试的设想

    为什么长期占据我浏览器的一个 tab 页?主要是我作为实用派,一直对单元测试的投入产出比存在疑问,但是自己又没有实际做过单元测试,所以很想知道别人反驳的理由,顺...

    sylan215
  • 为什么我们在RDO中使用OpenStack包构建的测试[Openstack]

    单元测试用于验证源代码的各个单元是否按照定义的规范工作。虽然这听起来很复杂,但简而言之,这意味着我们要验证源代码的每个部分是否按预期工作,而不必运行它们所属的整...

    用户6667850
  • 一枚程序员眼中的单元测试

    如今程序员群体赶上了中国最庞大的农民群体,大街上随便抓一把,十有八九是程序员,还一个刚从某国企离职报名参加软件培训班。我想码农的称号或许就是这么来的吧。

    袁慎建@ThoughtWorks

扫码关注云+社区

领取腾讯云代金券