flask总结

个人对flask的一些心得,本来是给自己看的,结果点错发布出去了。写的有点乱

第一部分:总结

第二部分:从无到有的示例代码

小白创作,看不惯就去看别的


目录

  • 一. flask用法总结
    • 1. 配置文件、数据库链接、启动文件
      • 1.1 配置文件
      • 1.2 数据库链接
      • 1.3 启动文件
    • 2. base model和base server的使用
      • 2.1 base model 模型基类
      • 2.2 base server 接口基类
    • 3. schema的使用
      • 3.1 schema简介
      • 3.2 schema校验示例
    • 4. 保证数据幂等
    • 5. 优化查询
    • 6. 上线部署
      • 6.1 docker部署
        • 6.1.1 Dockerfile
        • 6.1.2 构建镜像
        • 6.1.3 docker-compose
        • 6.1.4 运行docker
      • 6.2 gevent + supervisor部署
        • 6.2.1 安装python虚拟环境
        • 6.2.2 设置supervisor
        • 6.2.3 运行supervisor
  • 二. 代码示例
    • 0. 前言
      • 0.1 文件结构
      • 0.2 结构说明
      • 0.3 第三方包
      • 0.4 MVC分层规范
    • 1. 准备工作
      • 1.1 实例化公共的数据库对象
      • 1.2 配置文件
        • 1.2.1 默认配置文件configs.py
      • 1.3 model层
        • 1.3.1 base model的使用
        • 1.3.2 user模型
        • 1.3.3 编写sql
      • 1.4 编写一个测试api
        • 1.4.1 编写service
        • 1.4.2 编写api
        • 1.4.3 拆分的意义
      • 1.5 启动文件
      • 1.6 pycharm启动设置
      • 1.7 部署服务器
    • 2. 用户相关功能
      • 2.1 准备工作
        • 2.1.1 需求分析
        • 2.1.2 功能拆分
        • 2.1.3 编写utils
        • 2.1.4 编写常量response
        • 2.1.5 编写base server
      • 2.2 编写server
        • 2.2.1 user
        • 2.2.2 auth
      • 2.3 编写schema
        • 2.3.1 user
      • 2.4 编写api
        • 2.4.1 user
        • 2.4.2 auth
      • 2.5 注册api
      • 2.6 测试
    • 3. 商户信息相关功能
      • 3.1 准备工作
        • 3.1.1 创建商户相关model
        • 3.1.2 创建配置项model
        • 3.1.3 将获取配置项方法封装
      • 3.2 编写service
        • 3.2.1 编写商户信息service
        • 3.2.2 编写结算账户信息service
      • 3.3 设置api入口并测试
        • 更新中...


一. flask用法总结

1. 配置文件、数据库链接、启动文件

1.1 配置文件

  1. 配置文件,写到py文件或者是写到cfg文件中的配置项,
    里面主要存一些不怎么变的参数,如数据库链接
    (关系型-mysql,非关系型-redis)、邮件服务的key、短信服务的key

配置文件示例

  1. 存到数据库里的配置项,因为某些设置项可能随时都会改变,如果写到配置文件
    当发生变更时需要重新启动服务,这无疑会给用户带来不良体验。
    如每个用户最多允许创建的结算账户数量,开始可能5个就够了,突然上面说改成10个,
    再过几天说改回5个,如果写到配置文件里,并且你没有重启正式环境权限,你怎么办
    再比如说你收取服务费的比率,今天可能是交易额的0.01%,明天可能是0.02%

数据库配置项示例

1.2 数据库链接

使用flask扩展包中的flask-sqlalchemy和flask-redis来实现数据库链接

将数据库地址,最大连接数等存至配置文件中

在启动文件时使用flask扩展包中的init_app方法将数据库链接实例化

数据库链接示例

1.3 启动文件

在启动文件中,可用加载配置文件、初始化数据库配置、注册蓝图

启动文件可以扩展补充,可以根据实际需求去加载日志服务、mq服务,celery服务等

启动文件示例

2. base model和base server的使用

2.1 base model 模型基类

base model是将库表的公共字段提取出来做了一个基类,如id这些
其它的业务表都可以继承这个基类。

本文只是做了最基本的字段封装,感兴趣的同学可以试试封装一些基本的方法
比如增删改查4个基础方法,如果哪个模型有特殊需求,可以继承后重写这些方法

base model 示例

2.2 base server 接口基类

base server是将api的返回体的基本结果做了定义,同时封装了几个公共方法,
如请求成功,请求失败,分页数据,转为json格式等

这么做的目的是让所有的api都具有相对一致的返回格式,方便前/后端的开发
联调,开发和维护起来也相对简单一些,直接调用方法往里面塞数据就行。

base server 示例

3. schema的使用

3.1 schema简介

在前后端进行数据流转时,入参不可避免的会出现各种各样的错误,参数缺失、格式错误
等等,如果没有对入参进行校验,api的返回可能会与期望不一致。所以建议使用
python的marshmallow(不是那个百大dj)对入参进行校验。

3.2 schema校验示例

校验方式也很简单,写个marshmallow.Schema的子类

与flask的model类似,声明字段类型,可以进到fields文件里面查看具体的字段类型

from marshmallow import Schema, fields


class TestSchema(Schema):
    """schema校验示例"""
    
    email = fields.Email(allow_none=True, missing='', required=True)
    mobile = fields.String(allow_none=True, missing='', required=True)
    id = fields.Integer(allow_none=True, missing='')

if __name__ == '__main__':
    data = {
        'email': '[email protected]',
        'password': 'aaa123123',
    }
    res = TestSchema().validate(data)
    print(res)

schema类示例

schema校验示例

4. 保证数据幂等

在对数据库有增删改操作时,需要额外注意数据幂等。考虑代码报错等外在因素,为了保持数据库
数据一致,建议在增删改操作时,尤其操作多张表时,尽量都在同一个事务中进行。

比如你修改商户名称时,对应的用户表的用户名称也需要进行修改,商户表修改成功了,并且提交了
到用户表这里报错了。如果你的改操作不在同一个事务里面,会导致商户表修改成功并提交成功,
用户表无变化,api返回修改失败或者直接报错。显然与期望不符

所以建议无论修改了多少张表,都在返回成功之前统一提交(commit),捕获到异常时将整个事务回滚(rollback)

数据幂等示例

5. 优化查询

在开发过程中,避免不了会有很多个查询数据库的操作,如果库表设计的不合理,sql写的不合理
都会导致查询效率降低,从而导致请求响应时间过长。

1.尽量不要在循环内去查询/操作数据库

无论你的项目用sqlalchemy操作数据库也好,还是用原生的sql去操作数据库也好,都尽量不要在
循环内去查询/操作数据库,因为这也会一直给数据库发指令,无形中增加了数据库的负担。如果sql
写的也不怎么好进行了全表扫描,扫了一遍又一遍,查到猴年马月才能查完。

请优化你的代码和sql,争取一次性把期望的结果都查出来,哪怕再对结果循环也没关系,因为这个耗时
是可控的。而对数据库操作的时间是不可控的,尤其数据量比较大时,良好高效的sql和比较一般的sql耗时
可谓是云泥之别。

2.多表查询时,能联查的尽量联查

还是那句话,能一次性查出来的,就别查两次,效率低数据库压力还大,何必呢。

联查示例

代码位置
/merchant/server/merchant/MerchantServer.query_merchant()

6. 上线部署

6.1 docker部署

docker官方没有提供flask镜像,所以要想使用docker部署flask项目,
就需要自己构建自己的镜像。

我们重构一下结构

└─roarServer
    ├─MerchantServer
    │   ├─common
    │   ├─merchant
    ├─Dockerfile
    ├─docker-compose.yaml

感兴趣的话可以去阅读docker官方的文档,比我这里写的详细百倍

docker文档

6.1.1 Dockerfile

FROM python:3.7
ENV PYTHONUNBUFFERED=1
WORKDIR /roar
ADD MerchantServer/requirements.txt .
RUN pip install -r requirements.txt
COPY MerchantServer/ .
EXPOSE 8099
  • FROM
    FROM 是指明基于哪个官方镜像构造镜像,这里使用python3.7镜像

  • ENV
    ENV 指环境变量

  • WORKDIR
    WORKDIR 是指明工作目录,会在docker镜像里创建你指定的目录

  • ADD
    ADD 将你指定的路径或文件加载至docker镜像里

  • RUN
    RUN 会在构造docker镜像时,运行你后面跟的命令。这里是安装一些python依赖

  • COPY
    COPY 将指定的文件复制到docker镜像指定的目录下,与ADD类似

  • EXPOSE
    EXPOSE 指定监听端口

6.1.2 构建镜像

写好dockerfile之后,进到dockerfile同级目录下

比如dockerfile文件在/data/code目录下,cd到/data/code

# 运行命令
# dockers build  -t <镜像名>
docker build . -t flask_server

# 成功之后检查一下镜像是否生成
docker images

# 结果
root@asd:~# docker images
REPOSITORY     TAG       IMAGE ID       CREATED       SIZE
flask_server   latest    5d2318089fb7   3 hours ago   938MB
python         3.7       fb303727a80b   4 days ago    906MB

6.1.3 docker-compose

docker-compose的目的是让docker配置化,不用每次重启docker时写一堆命令

感兴趣的话可以去阅读docker官方的文档,比我这里写的详细百倍

docker文档

version: "2.3"
services:
  merchant:
    image: flask_server:latest
    container_name: flask_merchant_server
    ports:
      - "8099:8099"
    command: python manager.py runserver
    volumes:
      - ./MerchantServer:/roar

6.1.4 运行docker

确定写的dockerfile和docker-compose没啥问题了,就可以试运行docker看看效果了

# 进入项目工作地址
cd roarServer

# 试运行docker

# 看看各个镜像是否运行正常,是否有报错输出,没问题之后ctrl c退出
docker-compose -f docker-compose.yaml up

# 运行结果
flask_merchant_server |  * Serving Flask app 'manager' (lazy loading)
flask_merchant_server |  * Environment: production
flask_merchant_server |    WARNING: This is a development server. Do not use it in a production deployment.
flask_merchant_server |    Use a production WSGI server instead.
flask_merchant_server |  * Debug mode: on
flask_merchant_server |  * Running on all addresses (0.0.0.0)
flask_merchant_server |    WARNING: This is a development server. Do not use it in a production deployment.
flask_merchant_server |  * Running on http://127.0.0.1:8099
flask_merchant_server |  * Running on http://172.23.0.4:8099 (Press CTRL+C to quit)
flask_merchant_server |  * Restarting with stat
flask_merchant_server |  * Debugger is active!
flask_merchant_server |  * Debugger PIN: 707-424-557

# 正式运行docker
docker-compose -f docker-compose.yaml up -d

# 测试接口是否能被访问
# 随便请求一个写好的接口,看看是否返回成功

6.2 gevent + supervisor部署

我这里弄的稍微复杂一些,引用了gevent。其实单纯使用supervisor也是可以部署的

在部署之前重构一下/roarServer/MerchantServer/manager.py

重构的原因是在使用gevent进行部署时,需要实例化flask app

# 重构前
if __name__ == '__main__':
    app = create_app()
    manager = Manager(app)
    manager.add_command('runserver', Server('0.0.0.0', port='8099'))
    manager.run()


# 重构后
app = create_app()
manager = Manager(app)


if __name__ == '__main__':
    manager.add_command('runserver', Server('0.0.0.0', port='8099'))
    manager.run()

/roarServer/MerchantServer/requirments.txt增加相关依赖

flask==2.1.2
flask-redis==0.4.0
flask-script==2.0.5
flask-sqlalchemy==2.5.1
pymysql==1.0.2
redis==4.3.1
pyjwt==1.7.1
marshmallow==3.13.0
# 新增gevent相关
gevent==21.8.0
gunicorn==20.1.0

6.2.1 安装python虚拟环境

1.安装虚拟环境相关依赖

pip install virtualenv
pip install virtualenvwrapper

2.配置虚拟环境

# 创建存虚拟环境的文件
mkdir ~/.virtualenvs

# 找到python3的路径
which python3

# 找到virtualenvwrapper.sh的路径
which virtualenvwrapper.sh

# 修改 .bashrc 文件
vim ~/.bashrc

在.bashrc文件中写入下列配置项

# python 解释器路径
VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
# 虚拟环境路径
export WORKON_HOME=$HOME/.virtualenvs
# virtualenvwrapper.sh 的安装路径
source /usr/local/bin/virtualenvwrapper.sh

3.刷新配置文件

source .bashrc

# 结果
virtualenvwrapper.user_scripts creating /root/.virtualenvs/dev/bin/predeactivate
virtualenvwrapper.user_scripts creating /root/.virtualenvs/dev/bin/postdeactivate
virtualenvwrapper.user_scripts creating /root/.virtualenvs/dev/bin/preactivate
virtualenvwrapper.user_scripts creating /root/.virtualenvs/dev/bin/postactivate
virtualenvwrapper.user_scripts creating /root/.virtualenvs/dev/bin/get_env_details

4.新建虚拟环境

# mkvirtualenv 
mkvirtualenv dev

# 结果
created virtual environment CPython3.6.9.final.0-64 in 1204ms
  creator CPython3Posix(dest=/root/.virtualenvs/dev, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/root/.local/share/virtualenv)
    added seed packages: pip==21.3.1, setuptools==59.6.0, wheel==0.37.1
  activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator
(dev) root@hecs-test:~#

5.安装依赖

# 具体的路径请根据服务器实际路径来写,这里算是伪命令
pip install -r /roarServer/MerchantServer/requirments.txt

6.常用命令

# 退出当前虚拟环境
deactivate

# 查看所有虚拟环境
workon

# 使用虚拟环境
workon <env_name>

# 删除虚拟环境
rmvirtualenv <env_name>

6.2.2 设置supervisor

# 如果没有安装supervisor,就先安装
sudo apt-get install supervisor

# 编写.conf/.ini文件
vi /etc/supervisor/conf.d/flask_dev.ini

# 内容
[program:roar_merchant_9099]
command=/root/.virtualenvs/roar_dev/bin/gunicorn -w 2 -k gevent -b 0.0.0.0:9099 --preload manager:app
environment=GEVENT_SUPPORT="True",PATH="/root/.virtualenvs/roar_dev/bin/:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/home/dspread/bin", HOME="/data/code/roarServer/MerchantServer"
process_name=%(program_name)s
numprocs=1
directory=/data/code/roarServer/MerchantServer
autostart=true
user=root
stdout_logfile=/data/logs/supervisor/dev_merchant_out.log
stdout_logfile_maxbytes=200MB
stdout_logfile_backups=20
stderr_logfile=/data/logs/supervisor/dev_merchant_err.log
stderr_logfile_maxbytes=200MB
stderr_logfile_backups=20

6.2.3 运行supervisor

# 重新加载supervisor配置
supervisorctl reread

# 更新supervisor配置
supervisorctl update

# 启动项目
supervisorctl start roar_merchant_9099

测试各个接口是否运行正常

二. 代码示例

本章为第一章的补充说明,主要列举了本人曾经运用到flask项目的示例代码

从配置文件到基本的api写法都有简要的示例

0. 前言

0.1 文件结构

└─MerchantServer
    ├─common
       ├─api
       │  └─__init__.py
       │  └─auth.py
       │  └─user.py
       ├─define
       ├─models
       │  └─base.py
       │  └─user.py
       ├─schema
       │  └─user.py
       ├─service
       │  └─__init__.py
       │  └─auth.py
       │  └─user.py
       ├─__init__.py
       ├─ext.py
       ├─utils.py
    ├─merchant
        ├─api
        ├─models
        ├─schema
        ├─service
        ├─__init__.py
        ├─config.cfg
        ├─configs.py
    ├─__init__.py
    ├─merchant.py

0.2 结构说明

项目的根目录为MerchantServer,common文件下定义了所有的表结构、公共的api还有一些常量

为什么要这么定义呢?一个项目下,可能会有多个app,比如有个merchant商户端,有个operate管理端
两边都需要进行创建用户,登录,获取用户信息等操作。把这些api放到一个公共的地方让两个app都能调用到

0.3 第三方包

flask==2.1.2
flask-redis==0.4.0
flask-script==2.0.5
flask-sqlalchemy==2.5.1
pymysql==1.0.2
redis==4.3.1
pyjwt==1.7.1
marshmallow==3.13.0

数据库用的是mysql,使用redis来存用户的key等信息。
flask-sqlalchemy和flask-redis目的是让数据库可配置化。

0.4 MVC分层规范

MVC分层也就是用MVC模式来构建项目。

M为model-模型层,在flask里可以理解为表结构或者数据库orm对象。

V为视图层。

C为controller-控制层,在flask里可以理解为定义好api。

1. 准备工作

1.1 实例化公共的数据库对象

common/ext.py文件

# -*- coding:utf-8 -*-
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis as fRedis


db = SQLAlchemy()
redis = fRedis()

1.2 配置文件

1.2.1 默认配置文件configs.py

merchant/configs.py

这是默认的配置文件,比如默认的数据库地址,短信密钥等

# -*- coding:utf-8 -*-


class DefaultConfig:
    DEBUG = True
    RUN_MODE = 'dev'
    API_BP_PREFIX = '/api'
    # redis设置
    REDIS_URL = 'redis://:[email protected]:6379/2'
    # mysql
    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:[email protected]:3306/dev?charset=utf8mb4'
    SQLALCHEMY_ECHO = True
    SQLALCHEMY_TRACK_MODIFICATIONS = True
    SQLALCHEMY_POOL_SIZE = 100
    SQLALCHEMY_POOL_TIMEOUT = 10
    SQLALCHEMY_POOL_RECYCLE = 60
    SQLALCHEMY_MAX_OVERFLOW = 20
    # token前缀
    AUTH_KEY_PREFIX = 'auth'


class ManageConfig:
    pass


class ProductConfig:
    pass


config = {
    'default': DefaultConfig,
    'manage': ManageConfig,
    'product': ProductConfig,
}

比较个性化的配置可以放到config.cfg里面,比如你一个服务器里面起了两个相同的merchant服务,分别面向A和B客户
链接dbA数据库服务器和dbB数据库服务器
client_A/MerchantServer/merchant/config.cfg里面数据库地址设置为dbA
client_B/MerchantServer/merchant/config.cfg里面数据库地址设置为dbA

1.3 model层

1.3.1 base model的使用

base model 顾名思义是需要所有的model类都继承这个base model

它的作用就是将一些比较公共的字段,如id,create_time,update_time等封装进去不需要在每个模型中反复定义

在common/models/base.py中定义如下

# -*- coding:utf-8 -*-
from common.ext import db
from datetime import datetime


class BaseModel(db.Model):
    __abstract__ = True

    id = db.Column(db.Integer, primary_key=True, doc='主键ID')
    create_date = db.Column(db.DateTime, default=datetime.utcnow, doc='创建时间')
    write_date = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, doc='更新时间')
    create_uid = db.Column(db.Integer, doc='创建人')
    write_uid = db.Column(db.Integer, doc='修改人')

1.3.2 user模型

common/models/user.py中定义如下

# -*- coding:utf-8 -*-
from .base import db, BaseModel


class UserModel(BaseModel):
    __tablename__ = 'roar_user'

    name = db.Column(db.String(128), default='', doc='用户名')
    mobile = db.Column(db.String(64), default='', doc='手机号')
    email = db.Column(db.String(128), default='', doc='邮箱')
    password = db.Column(db.String(128), default='', doc='密码')
    status = db.Column(db.String(16), default='normal', doc='状态,normal-正常;freeze-冻结')

1.3.3 编写sql

这里不推荐使用migrate去自动更新数据库,因为多人协作开发时migrate很可能会乱或者出问题,产生不必要的错误。

所以我个人是使用sql去创建表或更新表结构的。

common/sql/v1.sql

CREATE TABLE roar_user (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(128) DEFAULT '' COMMENT '姓名',
    `mobile` VARCHAR(64) DEFAULT '' COMMENT '手机号',
    `email` VARCHAR(128) DEFAULT '' COMMENT '邮箱',
    `password` VARCHAR(128) DEFAULT '' COMMENT '密码',
    `status` VARCHAR(16) DEFAULT 'normal' COMMENT '状态,normal-正常;freeze-冻结',
    PRIMARY KEY (id),
    `create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `create_uid` INTEGER COMMENT '创建人',
    `write_uid` INTEGER COMMENT '修改人'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'

创建完库表之后,插入一条数据为后面的测试接口做准备

1.4 编写一个测试api

controller层又分为两层,api层和service层

api层是用来定义请求路由,接收request,过滤参数的,并且将参数传递到对应的service方法里

service层是定义业务逻辑的地方,在这里规定好需要的参数,操作数据库,处理数据并将结果返回给api层,再由api层做response

1.4.1 编写service

common/service/test.py

# -*- coding:utf-8 -*-
import traceback
from common.ext import db
from common.models import UserModel


class TestServer:
    def test_func(self, uid, **kw):
        try:
            users = db.session.query(UserModel).filter(
                UserModel.id == uid
            ).all()
            print(users)
            return {
                'code': 0,
                'msg': 'success'
            }
        except Exception:
            print(traceback.format_exc())
            return {
                'code': -1,
                'msg': 'error'
            }

导入并实例化TestServer

common/service/init.py

# -*- coding:utf-8 -*-
from common.service.test import TestServer


testServer = TestServer()

1.4.2 编写api

common/api/test.py

# -*- coding:utf-8 -*-
import json
from flask import Blueprint, request
from common.service import testServer


test_bp = Blueprint('test_bp', __name__, url_prefix='/test')


@test_bp.route('/test', methods=['GET'])
def test_api():
    kw = json.loads(request.data) if request.data else {}
    args = dict(request.args) or {}
    kw.update(args)
    uid = kw.get('uid', 0)
    return testServer.test_func(uid, **kw)

common/api/init.py

# -*- coding:utf-8 -*-

from common.api.test import test_bp

1.4.3 拆分的意义

为什么要这么分呢?还是那一句话,分的越细,封装的越完善,代码的可维护性和可读性就比较好

仅限个人想法,如有更好的思路请自动忽略

比如现有功能如下:

  • 校验短信验证码的service, check_verify

    • 功能:检验短信验证码是否真实有效(比如注册时输入短信验证码)
    • 入参:手机号,短信验证码
  • 校验用户名(手机号)密码的service, check_password

    • 功能:校验手机号与密码是否匹配且正确(比如登录)
    • 入参:手机号,密码

新加了个需求,在敏感操作(修改密码,删除某记录算作敏感操作)前,要校验用户,
校验方式为用户名,密码,短信验证码

如果封装的不完善,新需求可能需要再写一部分逻辑,极端一点把现有功能复制到同一个方法里面,这样维护起来不太方便,比如哪一天短信平台换了,需要重新写逻辑。隔了很久你可能会忘记该校验用户的地方

在api层写一段伪代码来演示一下,调用已有的两个service来实现新的需求

一个功能仅需要在一个地方维护,如果有其它地方有类似需求,直接调用现有service

import json
from flask import Blueprint, request
from common.service import testServer


test_bp = Blueprint('test_bp', __name__, url_prefix='/test')


@test_bp.route('/verify', methods=['GET'])
def check_verify_code():
    """
    校验短信验证码api
    入参:
        login:用户名(手机号)
        verify_code:短信验证码
    """
    kw = json.loads(request.data) if request.data else {}
    args = dict(request.args) or {}
    kw.update(args)
    login = kw.get('login', '')
    verify_code = kw.get('verify_code', '')
    return testServer.check_verify(login, verify_code, **kw)

@test_bp.route('/password', methods=['GET'])
def check_login_password():
    """
    校验用户名密码是否匹配且正确
    入参:
        login:用户名(手机号)
        password:密码
    """
    kw = json.loads(request.data) if request.data else {}
    args = dict(request.args) or {}
    kw.update(args)
    login = kw.get('login', '')
    password = kw.get('password', '')
    return testServer.check_password(login, password, **kw)

@test_bp.route('/check', methods=['GET'])
def check_user():
    """
    通过用户名,密码,短信验证码对用户进行安全校验
    入参:
        login:用户名(手机号)
        password:密码
        verify_code:短信验证码
    """
    kw = json.loads(request.data) if request.data else {}
    args = dict(request.args) or {}
    kw.update(args)
    login = kw.get('login', '')
    password = kw.get('password', '')
    verify_code = kw.get('verify_code', '')
    
    # 先校验密码,后校验验证码
    # 因为验证码为一次性的,使用一次就会失效
    # 如果先校验验证码,密码不正确时,需要再次发送验证码,费钱
    check_password_result = testServer.check_password(login, password, **kw)
    # 返回格式{'code': int, 'msg': 'error message'},成功时code为0,不为0代表失败
    # 存在code时,表示code不为0,将结果返回
    if check_password_result.get('code'):
        return check_password_result
    check_verify_result = testServer.check_verify(login, verify_code, **kw)
    if check_verify_result.get('code'):
        return check_verify_result
    return {'code': 0, 'msg': 'success'}

1.5 启动文件

MerchantServer/manager.py

# -*- coding:utf-8 -*-
import os
from flask import Flask
from merchant import api
from common.ext import db, redis
from merchant import configs
from flask_script import Manager, Server

from common import api


def create_app(config=None, app_name=None):
    """

    :param config:
    :param app_name:
    :return:
    """
    _config = configs.config[config] if config is not None else configs.DefaultConfig()
    dir_name, name = os.path.split(__file__)
    cfg = os.path.join(dir_name, 'merchant/config.cfg')
    app = Flask(__name__)
    app.config.from_object(_config)

    app.config.from_pyfile(cfg)
    app.run_mode = app.config['RUN_MODE']
    configure_app(app)
    return app


def configure_app(app):
    """
    初始化配置app,注册db,蓝图,日志等
    :param app:
    :return:
    """
    configure_blueprints(app)
    configure_db(app)


def configure_blueprints(app):
    """
    注册蓝图
    :param app:
    :return:
    """
    blueprints = (
        # 公共接口
        (api.user_bp, app.config.get('API_BP_PREFIX')),
        (api.auth_bp, app.config.get('API_BP_PREFIX')),
        (api.test_bp, app.config.get('API_BP_PREFIX')),
        # merchant接口
    )
    for blue_print, url_prefix in blueprints:
        app.register_blueprint(
            blue_print,
            url_prefix='{}{}'.format(
                url_prefix,
                blue_print.url_prefix if blue_print.url_prefix else ''
            )
        )


def configure_db(app):
    """
    注册数据库和redis
    :param app:
    :return:
    """
    db.init_app(app)
    redis.init_app(app)


if __name__ == '__main__':
    app = create_app()
    manager = Manager(app)
    manager.add_command('runserver', Server('0.0.0.0', port='8199'))

    manager.run()

1.6 pycharm启动设置

pycharm/run/edit configurations

script path: MerchantServer\manager.py
parameters: runserver

启动并访问刚才写的http://127.0.0.1:8199/api/test/test

至于为什么是/api/test/test

/api是在配置文件merchant/configs.py中定义好的,API_BP_PREFIX

第一个/test是在api层test_bp定义的,url_prefix=‘/test’

第二个/test是在route里定义的,@test_bp.route(‘/test’, methods=[‘GET’])

在manager.py/configure_blueprints方法里把他们拼到一起组成了最终的url

1.7 部署服务器

这样构造出来的项目,不管使用docker也好还是使用supervisor也好,都是很方便往服务器上部署的

2. 用户相关功能

具体功能如下:

  • 用户注册
  • 用户登录
  • 修改密码
  • 获取用户信息
  • 退出登录

这里使用jwt生成用户的access token,将access token为键,用户的信息为
值,存到redis中并设置失效时间。

用户请求接口时,将access token传进来维持登录态,当它失效后返回"未登录"

2.1 准备工作

2.1.1 需求分析

可以预见的是,用户的功能需要频繁、相对重复的操作redis,可以尝试将功能拆分
并封装起来

  • 用户注册
    • 拿到入参并做简单的格式校验,密码无特殊字符且长度符合
    • 往数据库插入一条数据
    • 返回成功
  • 用户登录
    • 根据登录名查询用户,未查到返回"无效用户错误"
    • 用户的密码和入参的密码进行校验,不匹配返回"密码错误"
    • 用户信息存到redis
    • 返回token
  • 修改密码
    • 校验token,不通过返回"未登录错误"
    • 拿到入参并做简单的格式校验,密码无特殊字符且长度符合
    • 更新数据库密码字段
    • 退出登录
    • 返回成功
  • 获取用户信息
    • 校验token,不通过返回"未登录错误"
    • 获取redis用户信息
    • 返回信息
  • 退出登录
    • 校验token,不通过返回"未登录错误"
    • 清除redis信息
    • 返回

2.1.2 功能拆分

  • redis

    • 插入/更新redis数据方法,set_user_info
    • 删除redis数据方法,clear_user_info
    • 获取redis数据方法,get_user_info
  • 用户token

    • 校验token是否有效的装饰器,后续很多接口都要调用,valid_user
    • 拼接redis key方法,get_auth_key
  • base server

    • 写一个基础的base server,所有的server都基础base

2.1.3 编写utils

/common/utils/utils.py

# -*- coding:utf-8 -*-
import json
import functools
from flask import request, make_response, _request_ctx_stack, has_request_context, current_app
from common.ext import db, redis
from common.models import UserModel
from common.define.user import UserInfo
from common.define.response import Response


def get_auth_key(token=None):
    if not token:
        token = request.headers.get('Authorization')
    prefix = current_app.config.get('AUTH_KEY_PREFIX', '')
    key = '{}_{}'.format(prefix, token)
    return key


def set_user_info(user_info, token):
    val = json.dumps(user_info, ensure_ascii=False)
    key = get_auth_key(token)
    return redis.set(name=key, value=val, ex=current_app.config.get('TOKEN_EXPIRE_TIME'), nx=True)


def clear_user_info():
    return redis.delete(get_auth_key())


def get_user_info(default_value=None):
    val = redis.get(get_auth_key())
    if val is not None:
        user_dict = json.loads(val)
        return UserInfo(**user_dict)
    else:
        return default_value


def get_user_id():
    """
    获取登录用户id
    :return:
    """
    user = get_user_info()
    if user:
        return user.id
    else:
        return None


def _get_user():
    if has_request_context() and not hasattr(_request_ctx_stack.top, 'user'):
        user = current_orm_user()
        _request_ctx_stack.top.user = user
    return getattr(_request_ctx_stack.top, 'user', None)


def current_orm_user():
    user_id = get_user_id()
    if not user_id:
        return None
    user = db.session.query(UserModel).filter(UserModel.id == user_id).first()
    return user


def valid_user(origin_func):
    """
    验证token是否有效
    :param origin_func:
    :return:
    """

    @functools.wraps(origin_func)
    def wrapper(*args, **kwargs):
        user = _get_user()
        if user:
            if user.status == 'freeze':
                result = {'code': -7, 'msg': '暂无权限登录该系统'}
                do_response = make_response(result, 200)
                return do_response
            do_response = origin_func(*args, **kwargs)
        else:
            do_response = make_response(Response.invalid, 200)
        return do_response

    # end def wrapper
    wrapper.__doc__ = origin_func.__doc__
    return wrapper

2.1.4 编写常量response

定义好一些固有的response格式

/common/define/response.py

# -*- coding:utf-8 -*-


class Response:
    success = {'code': 0, 'msg': 'success'}
    error = {'code': -1, 'msg': 'error'}
    invalid = {'code': 401, 'msg': '用户未登录'}

2.1.5 编写base server

/common/service/base.py

# -*- coding:utf-8 -*-
import json
import decimal
from datetime import datetime, date, timedelta
from sqlalchemy import or_
from common.define.response import Response
from common.models import UserModel
from common.ext import db
from werkzeug.security import check_password_hash


class BaseServer:
    def render_success(self):
        """返回成功"""
        return Result(**Response.success)

    def render_error(self, code=None, msg=None):
        """
        返回失败
        :param code: 错误码
        :param msg: 错误描述
        :return:
        """
        result = Response.error
        if code:
            result['code'] = code
        if msg:
            result['msg'] = msg

        return Result(**result)

    def check_user_by_password(self, login, password):
        """
        通过用户名密码,校验用户
        :param login:
        :param password:
        :return:
        """
        user = db.session.query(UserModel).filter(
            or_(UserModel.mobile == login, UserModel.email == login)
        ).first()
        if not user:
            return self.render_error(code=-1, msg='用户未找到')

        if not check_password_hash(user.password, password):
            return self.render_error(code=-1, msg='密码错误')
        return self.render_success().set_data(user)


class Result(dict):
    def __init__(self, **kw):
        super(Result, self).__init__(**kw)
        for key, value in kw.items():
            self.__setitem__(key, value)

    def set_data(self, data):
        """设置返回值数据"""
        self.__setitem__('data', data)
        return self

    def set_page_data(self, data, total, page, size):
        """设置分页返回数据"""
        data = {
            "items": data,
            "total": total,
            "page": page,
            "size": size
        }
        self.__setitem__("data", data)
        return self

    def is_success(self):
        """是否返回成功"""
        if self.__getitem__('code') == Response.success.get('code'):
            return True
        return False

    def to_json(self):
        result = json.dumps(self, cls=NewJsonEncoder, ensure_ascii=False)
        return result


class NewJsonEncoder(json.JSONEncoder):
    """转换一些不能json的数据"""
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.strftime('%Y-%m-%d %H:%M:%S')
        elif isinstance(obj, date):
            return obj.strftime('%Y-%m-%d')
        elif isinstance(obj, timedelta):
            return str(obj)
        elif isinstance(obj, decimal.Decimal):
            return float(obj)
        else:
            return json.JSONEncoder.default(self, obj)

2.2 编写server

2.2.1 user

/common/service/user.py

# -*- coding:utf-8 -*-
import traceback
from sqlalchemy import or_
from werkzeug.security import generate_password_hash, check_password_hash
from common.ext import db
from common.models import UserModel
from common.service.base import BaseServer
from common.utils import utils


class UserServerMixin(BaseServer):
    pass


class UserServer(UserServerMixin):
    def create(self, password, verify_code, mobile=None, email=None,  **kw):
        """
        创建用户
        :param mobile:
        :param email:
        :param password:
        :param verify_code:
        :return:
        """
        try:
            data = {}
            if mobile:
                data.update({
                    'name': mobile,
                    'mobile': mobile,
                })
            else:
                data.update({
                    'name': email,
                    'email': email,
                })

            data.update({'password': generate_password_hash(password)})

            user = UserModel(**data)
            db.session.add(user)
            db.session.commit()
            return self.render_success().to_json()
        except Exception:
            db.session.rollback()
            print(f'== create user error:{traceback.format_exc()}')
            return self.render_error().to_json()

    def change_password(self, login, password, verify_code, new_password, **kw):
        """
        修改密码
        :param login:
        :param password:
        :param verify_code:
        :param new_password:
        :param kw:
        :return:
        """
        try:
            user = db.session.query(UserModel).filter(
                or_(UserModel.mobile == login, UserModel.email == login)
            ).first()
            if not user:
                return self.render_error(code=-1, msg='用户信息未找到').to_json()

            if not check_password_hash(user.password, password):
                return self.render_error(code=-2, msg='密码错误').to_json()

            db.session.query(UserModel).filter(
                UserModel.id == user.id
            ).update(
                {'password': generate_password_hash(new_password)}, synchronize_session=False
            )
            db.session.commit()
            return self.render_success().to_json()
        except Exception:
            db.session.rollback()
            return self.render_error(code=-999, msg='操作失败').to_json()

    def user_info(self, **kw):
        """
        获取用户基本信息
        :param kw:
        :return:
        """
        try:
            user_info = utils.get_user_info()
            return self.render_success().set_data(user_info).to_json()
        except Exception:
            print(f'== user info error:{traceback.format_exc()}')
            return self.render_error(code=-999, msg='操作失败').to_json()

2.2.2 auth

/common/service/auth.py

# -*- coding:utf-8 -*-
import traceback
import datetime
import jwt
from flask import current_app
from common.ext import redis
from common.service.base import BaseServer
from common.utils.model_utils import model_to_dict_target
from common.utils import utils
from common.define.user import UserInfo
from common.define.constant import UserConstant


class AuthServerMixin(BaseServer):
    def encode_auth_token(self, user_id):
        """
        生成token
        :return: string
        """
        try:
            payload = {
                'exp': datetime.datetime.utcnow() + datetime.timedelta(days=0, seconds=3600),
                'iat': datetime.datetime.utcnow(),
                'sub': user_id
            }
            return jwt.encode(
                payload,
                current_app.config.get('SECRET_KEY'),
                algorithm='HS256'
            ).decode("utf-8")
        except Exception as e:
            return e

    def decode_auth_token(self, auth_token):
        """
        解密token
        :param auth_token:
        :return: integer|string
        """
        try:
            payload = jwt.decode(auth_token, current_app.config.get('SECRET_KEY'), algorithms=["HS256"])
            return payload['sub']
        except jwt.ExpiredSignatureError:
            return 'Signature expired. Please log in again.'
        except jwt.InvalidTokenError:
            return 'Invalid token. Please log in again.'


class AuthServer(AuthServerMixin):
    def login(self, login, password, **kw):
        """
        登录接口
        :param login: 用户名
        :param password: 密码
        :param kw:
        :return:
        """
        try:
            check_user_result = self.check_user_by_password(login, password)
            if not check_user_result.is_success():
                return check_user_result.to_json()

            user = check_user_result.get('data')
            if user.status != UserConstant.normal:
                return self.render_error(code=-3, msg='无权限访问').to_json()

            token = self.encode_auth_token(user.id)
            key = '{}_{}'.format(
                current_app.config.get('AUTH_KEY_PREFIX'), token
            )
            redis_result = redis.get(key)
            if not redis_result:
                user_dict = UserInfo(**model_to_dict_target(user, *UserConstant.user_info_fields))
                utils.set_user_info(user_dict, token)

            return self.render_success().set_data({'token': token}).to_json()
        except Exception:
            print(traceback.format_exc())
            return self.render_error(code=-999, msg='操作失败').to_json()

    def logout(self, **kw):
        """
        退出登录
        :param kw:
        :return:
        """
        utils.clear_user_info()
        return self.render_success().to_json()

2.3 编写schema

2.3.1 user

/common/schema/user.py

# -*- coding:utf-8 -*-
from marshmallow import Schema, fields
from marshmallow.validate import Regexp


class UserCreateSchema(Schema):
    """创建用户schema参数校验"""
    password = fields.String(required=True,
                             validate=Regexp(
                                 regex=r'^(?![\d]+$)(?![a-zA-Z]+$)(?![^\da-zA-Z]+$).{8,32}$',
                                 error='请输入8-32位,包含字母、数字、英文字符其中两种的密码'))
    email = fields.Email(allow_none=True, missing='')
    mobile = fields.String(allow_none=True, missing='', validate=Regexp(r'^1[3456789]\d{9}$', error='请输入有效的手机号码'))
    verify_code = fields.String(validate=Regexp(r'^\d{6,8}$', error='请输入数字验证码'))


class UserChangePasswordSchema(Schema):
    """修改密码schema参数校验"""
    password = fields.String(required=True,
                             validate=Regexp(
                                 regex=r'^(?![\d]+$)(?![a-zA-Z]+$)(?![^\da-zA-Z]+$).{8,32}$',
                                 error='请输入8-32位,包含字母、数字、英文字符其中两种的密码'))
    login = fields.String(required=True)
    verify_code = fields.String(validate=Regexp(r'^\d{6,8}$', error='请输入数字验证码'))
    new_password = fields.String(required=True,
                             validate=Regexp(
                                 regex=r'^(?![\d]+$)(?![a-zA-Z]+$)(?![^\da-zA-Z]+$).{8,32}$',
                                 error='请输入8-32位,包含字母、数字、英文字符其中两种的密码'))

2.4 编写api

2.4.1 user

/common/api/user.py

# -*- coding:utf-8 -*-
import json
from flask import Blueprint, request
from common.service import userServer
from common.schema import userCreateSchema, userChangePasswordSchema
from common.utils import utils


user_bp = Blueprint('user_bp', __name__, url_prefix='/user')


@user_bp.route('/create', methods=['POST'])
def create():
    kw = json.loads(request.data) if request.data else {}
    args = dict(request.args) or {}
    kw.update(args)

    check_param_result = userCreateSchema.validate(kw)
    if check_param_result:
        return userServer.render_error(code=-99, msg=check_param_result)
    mobile = kw.get('mobile')
    email = kw.get('email')
    password = kw.get('password')
    verify_code = kw.get('verify_code')
    return userServer.create(mobile=mobile, email=email, password=password, verify_code=verify_code)


@user_bp.route('/change_password', methods=['POST'])
@utils.valid_user
def change_password():
    kw = json.loads(request.data) if request.data else {}
    args = dict(request.args) or {}
    kw.update(args)
    check_param_result = userChangePasswordSchema.validate(kw)
    if check_param_result:
        return userServer.render_error(code=-99, msg=check_param_result)

    login = kw.get('login')
    password = kw.get('password')
    new_password = kw.get('new_password')
    verify_code = kw.get('verify_code')
    return userServer.change_password(login, password, verify_code, new_password)


@user_bp.route('/info', methods=['GET'])
@utils.valid_user
def user_info():
    return userServer.user_info()

2.4.2 auth

/common/api/auth.py

# -*- coding:utf-8 -*-
import json
from flask import Blueprint, request
from common.service import authServer


auth_bp = Blueprint('auth_bp', __name__, url_prefix='/auth')


@auth_bp.route('/login', methods=['POST'])
def login():
    kw = json.loads(request.data) if request.data else {}
    args = dict(request.args) or {}
    kw.update(args)

    login_name = kw.get('login')
    password = kw.get('password')
    return authServer.login(login=login_name, password=password)


@auth_bp.route('/logout', methods=['GET'])
def logout():
    return authServer.logout()

2.5 注册api

/manager.py/configure_blueprints

def configure_blueprints(app):
    """
    注册蓝图
    :param app:
    :return:
    """
    blueprints = (
        # 公共接口
        (api.user_bp, app.config.get('API_BP_PREFIX')),
        (api.auth_bp, app.config.get('API_BP_PREFIX')),
        (api.test_bp, app.config.get('API_BP_PREFIX')),
        # merchant接口
    )
    for blue_print, url_prefix in blueprints:
        app.register_blueprint(
            blue_print,
            url_prefix='{}{}'.format(
                url_prefix,
                blue_print.url_prefix if blue_print.url_prefix else ''
            )
        )

2.6 测试

按照注册,登录,获取用户信息,修改密码,退出登录,重新登陆流程测试一遍新接口

欠缺1:修改密码之后,未退出登录

3. 商户信息相关功能

商户与登录用户是一对一关系,一个登录用户仅可有一个商户,商户是未来贯穿整个业务的表。

注册商户成功后可用设置商户名下的店铺信息、交易金额结算账户信息等。

具体功能如下:

  • 商户注册

  • 商户信息查看

  • 商户信息编辑 - 暂不开放

  • 结算账户列表

  • 结算账户创建

3.1 准备工作

3.1.1 创建商户相关model

/common/models/merchant.py

# -*- coding:utf-8 -*-
from .base import db, BaseModel


class MerchantModel(BaseModel):
    """商户基本信息"""
    __tablename__ = 'roar_merchant'

    merchant_no = db.Column(db.String(128), default='', doc='商户号')
    merchant_name = db.Column(db.String(128), default='', doc='商户名')
    merchant_type = db.Column(db.String(128), default='person', doc='商户类型,person-个人商户;company-企业商户')
    # 公共字段
    contact_name = db.Column(db.String(128), default='', doc='联系人')
    contact_phone = db.Column(db.String(128), default='', doc='联系人电话')
    contact_email = db.Column(db.String(128), default='', doc='联系邮箱')
    business_address_id = db.Column(db.String(256), default='[]', doc='经营地址id')
    business_address_name = db.Column(db.String(512), default='[]', doc='经营地址名称')
    business_category_id = db.Column(db.String(256), default='[]', doc='经营类目id')
    business_category_name = db.Column(db.String(512), default='[]', doc='经营类目名称')
    status = db.Column(db.String(16), default='draft', doc='商户状态,normal-正常;draft-待审核;freeze-冻结')
    archive = db.Column(db.Boolean, default=False, doc='是否归档 - 软删除')


class MerchantPersonModel(BaseModel):
    """商户归属人信息"""
    __tablename__ = 'roar_merchant_person'

    name = db.Column(db.String(128), default='', doc='归属人姓名')
    gender = db.Column(db.String(16), default='', doc='归属人性别,female-女性;male-男性')
    id_type = db.Column(db.String(16), default='', doc='证件类型,id-身份证;passport-护照')
    id_number = db.Column(db.String(24), default='', doc='证件号码')
    merchant_id = db.Column(db.Integer, default=0, doc='商户ID')
    archive = db.Column(db.Boolean, default=False, doc='是否归档 - 软删除')
    status = db.Column(db.String(16), default='draft', doc='归属人状态,draft-待审核,normal-正常,freeze-冻结')


class BusinessCategoryModel(BaseModel):
    """经营类目"""
    __tablename__ = 'roar_business_category'

    name = db.Column(db.String(128), default='', doc='类目名称')
    is_special = db.Column(db.Boolean, default=False, doc='是否为特殊行业,如医疗、教育等。需提供相关的许可证明')
    archive = db.Column(db.Boolean, default=False, doc='归档-软删除')


class BusinessAddressModel(BaseModel):
    """经营地址"""
    __tablename__ = 'roar_business_address'

    name = db.Column(db.String(128), default='', doc='名称')
    parent_id = db.Column(db.Integer, default=0, doc='上级')
    has_child = db.Column(db.Boolean, default=True, doc='是否存在下级')
    # is_open = db.Column(db.Boolean, default=False, doc='是否为贸易区')


class MerchantAccountModel(BaseModel):
    """商户结算账户"""
    __tablename__ = 'roar_merchant_account'

    merchant_id = db.Column(db.Integer, default=0, doc='商户ID')
    bank_name = db.Column(db.String(128), default='', doc='银行名称')
    account_name = db.Column(db.String(128), default='', doc='账户名称')
    bank_account = db.Column(db.String(32), default='', doc='银行卡号')
    account_currency = db.Column(db.String(16), default='CNY', doc='账户币种')
    bank_address_id = db.Column(db.String(256), default='[]', doc='银行地址ID')
    bank_address_name = db.Column(db.String(512), default='[]', doc='银行地址名称')
    opening_bank_name = db.Column(db.String(128), default='', doc='开户行名称')
    bank_phone = db.Column(db.String(16), default='', doc='预留手机号')
    swift_code = db.Column(db.String(16), default='', doc='SWIFT CODE')
    archive = db.Column(db.Boolean, default=False, doc='是否归档 - 软删除')
    is_init = db.Column(db.Boolean, default=False, doc='是否为初始账户')
    is_default = db.Column(db.Boolean, default=False, doc='是否为默认账户')
    status = db.Column(db.String(16), default='draft', doc='账户状态,draft-待审核,normal-正常,freeze-冻结')

3.1.2 创建配置项model

/common/models/config.py

# -*- coding:utf-8 -*-

from .base import db, BaseModel


class ConfigModel(BaseModel):
    __tablename__ = 'roar_config'

    key = db.Column(db.String(128), default='', doc='配置项名', unique=True)
    value = db.Column(db.Text, default='', doc='配置项值')
    param_type = db.Column(db.String(16), default='string',
                           doc='配置项类型,string-字符串;list-数组;dict-字典;integer-整数;float-浮点数')
    description = db.Column(db.Text, default='', doc='配置项描述')

3.1.3 将获取配置项方法封装

/common/service/base.py/get_config方法

# -*- coding:utf-8 -*-

import json
import decimal
from datetime import datetime, date, timedelta
from sqlalchemy import or_
from common.define.response import Response
from common.models import UserModel, ConfigModel
from common.ext import db
from werkzeug.security import check_password_hash


class BaseServer:
    def render_success(self):
        """返回成功"""
        return Result(**Response.success)

    def render_error(self, code=None, msg=None):
        """
        返回失败
        :param code: 错误码
        :param msg: 错误描述
        :return:
        """
        result = Response.error
        if code:
            result['code'] = code
        if msg:
            result['msg'] = msg

        return Result(**result)

    def check_user_by_password(self, login, password):
        """
        通过用户名密码,校验用户
        :param login:
        :param password:
        :return:
        """
        user = db.session.query(UserModel).filter(
            or_(UserModel.mobile == login, UserModel.email == login)
        ).first()
        if not user:
            return self.render_error(code=-1, msg='用户未找到')

        if not check_password_hash(user.password, password):
            return self.render_error(code=-1, msg='密码错误')
        return self.render_success().set_data(user)

    def get_config(self, key):
        """
        根据指定键获取对应的配置项
        :param key:
        :return:
        """
        if not key:
            return
        config_query = db.session.query(
            ConfigModel.key, ConfigModel.value,
            ConfigModel.param_type
        ).filter(
            ConfigModel.key == key
        ).first()
        if not config_query:
            return
        return self.load_config(config_query.value, config_query.param_type)

    def load_config(self, value, param_type):
        """
        将配置项中的值转换为对应格式,string-> string,list,dict...
        :param value:
        :param param_type:
        :return:
        """
        if not value:
            return
        if param_type == 'string':
            value = value
        elif param_type == 'list':
            value = json.loads(value)
        elif param_type == 'dict':
            value = json.loads(value)
        elif param_type == 'integer':
            value = int(value)
        elif param_type == 'float':
            value = float(value)
        else:
            value = value
        return value


class Result(dict):
    def __init__(self, **kw):
        super(Result, self).__init__(**kw)
        for key, value in kw.items():
            self.__setitem__(key, value)

    def set_data(self, data):
        """设置返回值数据"""
        self.__setitem__('data', data)
        return self

    def set_page_data(self, data, total, page, size):
        """设置分页返回数据"""
        data = {
            "items": data,
            "total": total,
            "page": page,
            "size": size
        }
        self.__setitem__("data", data)
        return self

    def is_success(self):
        """是否返回成功"""
        if self.__getitem__('code') == Response.success.get('code'):
            return True
        return False

    def to_json(self):
        result = json.dumps(self, cls=NewJsonEncoder, ensure_ascii=False)
        return result


class NewJsonEncoder(json.JSONEncoder):
    """转换一些不能json的数据"""
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.strftime('%Y-%m-%d %H:%M:%S')
        elif isinstance(obj, date):
            return obj.strftime('%Y-%m-%d')
        elif isinstance(obj, timedelta):
            return str(obj)
        elif isinstance(obj, decimal.Decimal):
            return float(obj)
        else:
            return json.JSONEncoder.default(self, obj)

3.2 编写service

3.2.1 编写商户信息service

/merchant/service/merchant.py

# -*- coding:utf-8 -*-
import traceback
import uuid
import json
from common.ext import db
from common.models import MerchantModel, MerchantAccountModel, BusinessCategoryModel, BusinessAddressModel, \
    UserModel, MerchantPersonModel
from common.service.base import BaseServer
from common.define.constant import MerchantStepConstant, MerchantStatusConstant, PersonStatusConstant, AccountStatusConstant


class MerchantServerMixin(BaseServer):
    # 提交信息时,不同的step对应不同的方法
    step_map = {
        'base': 'base_info',
        'person': 'person_info',
        'account': 'account_info',
    }

    def base_info(self, uid, data):
        """
        创建商户基础信息
        在最终确认提交之前,商户信息处于归档状态
        :param uid:
        :param data:
        :return:
        """
        business_address_id = data.get('business_address_id', [])
        if business_address_id:
            business_address_name = self._get_address_name(business_address_id)
            data.update({
                'business_address_id': json.dumps(business_address_id, ensure_ascii=False),
                'business_address_name': json.dumps(business_address_name, ensure_ascii=False),
            })
        business_category_id = data.get('business_category_id', [])
        if business_category_id:
            business_category_name = self._get_category_name(business_category_id)
            data.update({
                'business_category_id': json.dumps(business_category_id, ensure_ascii=False),
                'business_category_name': json.dumps(business_category_name, ensure_ascii=False),
            })
        data.update({
            'merchant_no': str(uuid.uuid4()).replace('-', ''),
            'create_uid': uid
        })
        merchant_id = self._get_draft_merchant(uid)
        if merchant_id:
            db.session.query(MerchantModel).filter(
                MerchantModel.id == merchant_id
            ).update(data, synchronize_session=False)
        else:
            merchant_data = MerchantModel(**data)
            db.session.add(merchant_data)

    def person_info(self, uid, data):
        """
        归属人信息
        :param uid: 当前登录用户id
        :param data:
        :return:
        """
        merchant_id = self._get_draft_merchant(uid)
        if not merchant_id:
            return self.render_error(code=-4, msg='尚未创建商户')
        data.update({
            'merchant_id': merchant_id,
            'create_uid': uid
        })
        person_query = db.session.query(MerchantPersonModel.id).filter(
            MerchantPersonModel.create_uid == uid,
            MerchantPersonModel.status == PersonStatusConstant.draft
        ).first()
        # 存在就更新
        if person_query:
            db.session.query(MerchantPersonModel.id).filter(
                MerchantPersonModel.create_uid == uid,
                MerchantPersonModel.status == PersonStatusConstant.draft
            ).update(data, synchronize_session=False)
        # 不存在就新建
        else:
            person_data = MerchantPersonModel(**data)
            db.session.add(person_data)

    def account_info(self, uid, data):
        """
        更新或创建账户信息
        :param uid:
        :param data:
        :return:
        """
        merchant_id = self._get_draft_merchant(uid)
        if not merchant_id:
            return self.render_error(code=-4, msg='尚未创建商户')
        data.update({
            'merchant_id': merchant_id,
            'create_uid': uid
        })
        account_query = db.session.query(MerchantAccountModel.id).filter(
            MerchantAccountModel.create_uid == uid,
            MerchantAccountModel.is_init == True,
            MerchantAccountModel.status == AccountStatusConstant.draft
        ).first()
        if account_query:
            db.session.query(MerchantAccountModel).filter(
                MerchantAccountModel.create_uid == uid,
                MerchantAccountModel.is_init == True,
                MerchantAccountModel.status == AccountStatusConstant.draft
            ).update(data, synchronize_session=False)
        else:
            account_data = MerchantAccountModel(**data)
            db.session.add(account_data)

    def _get_address_name(self, address_list):
        """
        地址id数组转换为地址名称数组
        [1, 9, 44] -> [北京, 北京市, 昌平区]
        :param address_list:
        :return:
        """
        if not address_list:
            return []
        address_query = db.session.query(BusinessAddressModel.name).filter(
            BusinessAddressModel.id.in_(address_list)
        ).all()
        address_name = [each.name for each in address_query]
        return address_name

    def _get_category_name(self, category_list):
        """
        经营类目id数组转换为经营类目名称数组
        [1, 11, 12] -> [电子产品, 手机, 电脑]
        :param category_list:
        :return:
        """
        if not category_list:
            return []
        category_query = db.session.query(BusinessCategoryModel.name).filter(
            BusinessCategoryModel.id.in_(category_list)
        ).all()
        category_name = [each.name for each in category_query]
        return category_name

    def _get_draft_merchant(self, uid):
        merchant_query = db.session.query(MerchantModel.id).filter(
            MerchantModel.create_uid == uid,
            MerchantModel.status == MerchantStatusConstant.draft,
        ).first()
        return merchant_query.id if merchant_query else 0


class MerchantServer(MerchantServerMixin):
    def save_merchant(self, uid, step, **kw):
        """
        商户信息按步骤提交信息
        :param uid:
        :param step: base/person/account
        :param kw:
        :return:
        """
        try:
            user = db.session.query(UserModel).filter(UserModel.id == uid).first()
            if not user:
                return self.render_error(code=-1, msg='用户信息未找到')
            if step not in MerchantStepConstant.valid_step:
                return self.render_error(code=-2, msg='非法操作')
            merchant_query = db.session.query(MerchantModel.id).filter(
                MerchantModel.create_uid == user.id,
                MerchantModel.status == MerchantStatusConstant.draft
            ).first()
            if merchant_query:
                return self.render_error(code=-3, msg='已存在可用商户,无法创建')
            func_result = getattr(self, self.step_map.get(step))(uid, kw)
            if func_result:
                return func_result
            db.session.commit()
            return self.render_success()
        except Exception:
            db.session.rollback()
            print(f'== create merchant error:{traceback.format_exc()}')
            return self.render_error(code=-999, msg='操作失败')

    def commit_merchant(self, uid, **kw):
        """
        确认无误后最终提交
        :param uid:
        :param kw:
        :return:
        """
        try:
            # 商户信息
            db.session.query(MerchantModel).filter(
                MerchantModel.create_uid == uid
            ).update(
                {
                    'status': MerchantStatusConstant.normal
                }, synchronize_session=False
            )
            # 归属人信息
            db.session.query(MerchantPersonModel).filter(
                MerchantPersonModel.create_uid == uid
            ).update(
                {
                    'status': PersonStatusConstant.normal
                }, synchronize_session=False
            )
            # 账户信息
            db.session.query(MerchantAccountModel).filter(
                MerchantAccountModel.create_uid == uid
            ).update(
                {
                    'status': AccountStatusConstant.normal
                }, synchronize_session=False
            )
            db.session.commit()
            return self.render_success()
        except Exception:
            db.session.rollback()
            print(f'== save merchant error:{traceback.format_exc()}')
            return self.render_error(code=-999, msg='操作失败')

    def query_merchant(self, uid, **kw):
        """
        查询商户信息
        :param uid:
        :param kw:
        :return:
        """
        try:
            merchant_query = db.session.query(
                # 商户信息
                MerchantModel.id.label('merchant_id'), MerchantModel.merchant_no,
                MerchantModel.merchant_name, MerchantModel.merchant_type,
                MerchantModel.contact_name, MerchantModel.contact_phone,
                MerchantModel.contact_email, MerchantModel.business_address_id,
                MerchantModel.business_address_name, MerchantModel.business_category_id,
                MerchantModel.business_category_name, MerchantModel.status.label('merchant_status'),
                # 归属人信息
                MerchantPersonModel.name, MerchantPersonModel.gender,
                MerchantPersonModel.id_type, MerchantPersonModel.id_number,
                MerchantPersonModel.status.label('person_status'),
                # 账户信息
                MerchantAccountModel.bank_name, MerchantAccountModel.account_name,
                MerchantAccountModel.bank_account, MerchantAccountModel.account_currency,
                MerchantAccountModel.bank_address_id, MerchantAccountModel.bank_address_name,
                MerchantAccountModel.opening_bank_name, MerchantAccountModel.bank_phone,
                MerchantAccountModel.is_init, MerchantAccountModel.status.label('account_status'),
            ).join(
                MerchantPersonModel,
                MerchantModel.id == MerchantPersonModel.merchant_id,
                isouter=True
            ).join(
                MerchantAccountModel,
                MerchantModel.id == MerchantAccountModel.merchant_id,
                isouter=True
            ).filter(
                MerchantModel.create_uid == uid
            ).first()
            if not merchant_query:
                return self.render_error(code=-1, msg='未找到商户信息')

            result = {
                'merchant': {
                    'merchant_id': merchant_query.merchant_id,
                    'merchant_no': merchant_query.merchant_no,
                    'merchant_name': merchant_query.merchant_name,
                    'merchant_type': merchant_query.merchant_type,
                    'contact_name': merchant_query.contact_name,
                    'contact_phone': merchant_query.contact_phone,
                    'contact_email': merchant_query.contact_email,
                    'business_address_id': json.loads(merchant_query.business_address_id),
                    'business_address_name': json.loads(merchant_query.business_address_name),
                    'business_category_id': json.loads(merchant_query.business_category_id),
                    'business_category_name': json.loads(merchant_query.business_category_name),
                    'status': merchant_query.merchant_status,
                },
                'person': {
                    'name': merchant_query.name,
                    'gender': merchant_query.gender,
                    'id_type': merchant_query.id_type,
                    'id_number': merchant_query.id_number,
                    'status': merchant_query.person_status,
                },
                'account': {
                    'bank_name': merchant_query.bank_name,
                    'account_name': merchant_query.account_name,
                    'bank_account': merchant_query.bank_account,
                    'account_currency': merchant_query.account_currency,
                    'bank_address_id': json.loads(merchant_query.bank_address_id),
                    'bank_address_name': json.loads(merchant_query.bank_address_name),
                    'opening_bank_name': merchant_query.opening_bank_name,
                    'bank_phone': merchant_query.bank_phone,
                    'is_init': merchant_query.is_init,
                    'status': merchant_query.account_status,
                }
            }
            return self.render_success().set_data(result)
        except Exception:
            print(f'== query merchant error:{traceback.format_exc()}')
            return self.render_error(code=-999, msg='操作失败')

3.2.2 编写结算账户信息service

/merchant/service/account.py

# -*- coding:utf-8 -*-
import traceback
import json
from common.ext import db
from common.models import MerchantModel, MerchantAccountModel
from merchant.service.merchant import MerchantServerMixin
from common.define.constant import AccountStatusConstant, MerchantStatusConstant
from common.define.system_config_key import SystemConfigKey


class AccountServerMixin(MerchantServerMixin):
    pass


class AccountServer(AccountServerMixin):
    def account_list(self, uid, **kw):
        try:
            page = int(kw.get('page', 1))
            limit = int(kw.get('limit', 10))
            account_query = db.session.query(
                MerchantAccountModel.id,
                MerchantAccountModel.bank_name, MerchantAccountModel.account_name,
                MerchantAccountModel.bank_account, MerchantAccountModel.account_currency,
                MerchantAccountModel.bank_address_id, MerchantAccountModel.bank_address_name,
                MerchantAccountModel.opening_bank_name, MerchantAccountModel.bank_phone,
                MerchantAccountModel.is_init, MerchantAccountModel.status,
            ).filter(
                MerchantAccountModel.create_uid == uid,
                MerchantAccountModel.archive == False
            )
            total = account_query.count()
            account_query = account_query.offset((page-1)*limit).limit(limit).all()
            result = []
            if account_query:
                for each in account_query:
                    data = {
                        'id': each.id,
                        'bank_name': each.bank_name,
                        'account_name': each.account_name,
                        'bank_account': each.bank_account,
                        'account_currency': each.account_currency,
                        'bank_address_id': json.loads(each.bank_address_id),
                        'bank_address_name': json.loads(each.bank_address_name),
                        'opening_bank_name': each.opening_bank_name,
                        'bank_phone': each.bank_phone,
                        'is_init': each.is_init,
                        'status': each.status,
                    }
                    result.append(data)
            return self.render_success().set_page_data(data=result, total=total, page=page, size=limit)
        except Exception:
            print(f'== account list error:{traceback.format_exc()}')
            return self.render_error(code=-999, msg='操作失败')

    def commit_account(self, uid, **kw):
        try:
            merchant_query = db.session.query(MerchantModel.id).filter(
                MerchantModel.create_uid == uid,
                MerchantModel.status == MerchantStatusConstant.normal,
            ).first()
            if not merchant_query:
                return self.render_error(code=-1, msg='尚未创建商户')
            # 检查是否超出预设的最大账户数量
            max_account_num = self.get_config(SystemConfigKey.MAX_ACCOUNT_NUM)
            account_query = db.session.query(MerchantAccountModel.id).filter(
                MerchantAccountModel.create_uid == uid,
                MerchantAccountModel.archive == False
            ).count()
            if account_query >= max_account_num:
                return self.render_error(code=-2, msg='账户数量超出限制')
            kw.update({
                'merchant_id': merchant_query.id,
                'create_uid': uid,
                'status': AccountStatusConstant.normal,
            })
            bank_address_id = kw.get('bank_address_id', [])
            if bank_address_id:
                kw.update({
                    'bank_address_id': json.dumps(bank_address_id, ensure_ascii=False),
                    'bank_address_name': json.dumps(self._get_address_name(bank_address_id), ensure_ascii=False),
                })
            account_data = MerchantAccountModel(**kw)
            db.session.add(account_data)
            db.session.commit()
            return self.render_success()
        except Exception:
            print(f'== commit account error:{traceback.format_exc()}')
            return self.render_error(code=-999, msg='操作失败')

3.3 设置api入口并测试


更新中…


你可能感兴趣的:(flask,python,后端)