个人对flask的一些心得,本来是给自己看的,结果点错发布出去了。写的有点乱
第一部分:总结
第二部分:从无到有的示例代码
小白创作,看不惯就去看别的
配置文件示例
数据库配置项示例
使用flask扩展包中的flask-sqlalchemy和flask-redis来实现数据库链接
将数据库地址,最大连接数等存至配置文件中
在启动文件时使用flask扩展包中的init_app方法将数据库链接实例化
数据库链接示例
在启动文件中,可用加载配置文件、初始化数据库配置、注册蓝图
启动文件可以扩展补充,可以根据实际需求去加载日志服务、mq服务,celery服务等
启动文件示例
base model是将库表的公共字段提取出来做了一个基类,如id这些
其它的业务表都可以继承这个基类。
本文只是做了最基本的字段封装,感兴趣的同学可以试试封装一些基本的方法
比如增删改查4个基础方法,如果哪个模型有特殊需求,可以继承后重写这些方法
base model 示例
base server是将api的返回体的基本结果做了定义,同时封装了几个公共方法,
如请求成功,请求失败,分页数据,转为json格式等
这么做的目的是让所有的api都具有相对一致的返回格式,方便前/后端的开发
联调,开发和维护起来也相对简单一些,直接调用方法往里面塞数据就行。
base server 示例
在前后端进行数据流转时,入参不可避免的会出现各种各样的错误,参数缺失、格式错误
等等,如果没有对入参进行校验,api的返回可能会与期望不一致。所以建议使用
python的marshmallow(不是那个百大dj
)对入参进行校验。
校验方式也很简单,写个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校验示例
在对数据库有增删改操作时,需要额外注意数据幂等。考虑代码报错等外在因素,为了保持数据库
数据一致,建议在增删改操作时,尤其操作多张表时,尽量都在同一个事务中进行。
比如你修改商户名称时,对应的用户表的用户名称也需要进行修改,商户表修改成功了,并且提交了
到用户表这里报错了。如果你的改操作不在同一个事务里面,会导致商户表修改成功并提交成功,
用户表无变化,api返回修改失败或者直接报错。显然与期望不符
所以建议无论修改了多少张表,都在返回成功之前统一提交(commit),捕获到异常时将整个事务回滚(rollback)
数据幂等示例
在开发过程中,避免不了会有很多个查询数据库的操作,如果库表设计的不合理,sql写的不合理
都会导致查询效率降低,从而导致请求响应时间过长。
1.尽量不要在循环内去查询/操作数据库
无论你的项目用sqlalchemy操作数据库也好,还是用原生的sql去操作数据库也好,都尽量不要在
循环内去查询/操作数据库,因为这也会一直给数据库发指令,无形中增加了数据库的负担。如果sql
写的也不怎么好进行了全表扫描,扫了一遍又一遍,查到猴年马月才能查完。
请优化你的代码和sql,争取一次性
把期望的结果都查出来,哪怕再对结果循环也没关系,因为这个耗时
是可控的。而对数据库操作的时间是不可控的,尤其数据量比较大时,良好高效的sql和比较一般的sql耗时
可谓是云泥之别。
2.多表查询时,能联查的尽量联查
还是那句话,能一次性查出来的,就别查两次,效率低数据库压力还大,何必呢。
联查示例
代码位置
/merchant/server/merchant/MerchantServer.query_merchant()
docker官方没有提供flask镜像,所以要想使用docker部署flask项目,
就需要自己构建自己的镜像。
我们重构一下结构
└─roarServer
├─MerchantServer
│ ├─common
│ ├─merchant
├─Dockerfile
├─docker-compose.yaml
感兴趣的话可以去阅读docker官方的文档,比我这里写的详细百倍
docker文档
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 指定监听端口
写好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
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
确定写的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
# 测试接口是否能被访问
# 随便请求一个写好的接口,看看是否返回成功
我这里弄的稍微复杂一些,引用了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
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>
# 如果没有安装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
# 重新加载supervisor配置
supervisorctl reread
# 更新supervisor配置
supervisorctl update
# 启动项目
supervisorctl start roar_merchant_9099
测试各个接口是否运行正常
本章为第一章的补充说明,主要列举了本人曾经运用到flask项目的示例代码
从配置文件到基本的api写法都有简要的示例
└─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
项目的根目录为MerchantServer,common文件下定义了所有的表结构、公共的api还有一些常量
为什么要这么定义呢?一个项目下,可能会有多个app,比如有个merchant商户端,有个operate管理端
两边都需要进行创建用户,登录,获取用户信息等操作。把这些api放到一个公共的地方让两个app都能调用到
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目的是让数据库可配置化。
MVC分层也就是用MVC模式来构建项目。
M为model-模型层,在flask里可以理解为表结构或者数据库orm对象。
V为视图层。
C为controller-控制层,在flask里可以理解为定义好api。
common/ext.py文件
# -*- coding:utf-8 -*-
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis as fRedis
db = SQLAlchemy()
redis = fRedis()
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
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='修改人')
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-冻结')
这里不推荐使用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='用户表'
创建完库表之后,插入一条数据为后面的测试接口做准备
controller层又分为两层,api层和service层
api层是用来定义请求路由,接收request,过滤参数的,并且将参数传递到对应的service方法里
service层是定义业务逻辑的地方,在这里规定好需要的参数,操作数据库,处理数据并将结果返回给api层,再由api层做response
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()
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
为什么要这么分呢?还是那一句话,分的越细,封装的越完善,代码的可维护性和可读性就比较好
仅限个人想法,如有更好的思路请自动忽略
比如现有功能如下:
校验短信验证码的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'}
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()
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
这样构造出来的项目,不管使用docker也好还是使用supervisor也好,都是很方便往服务器上部署的
具体功能如下:
这里使用jwt生成用户的access token,将access token为键,用户的信息为
值,存到redis中并设置失效时间。
用户请求接口时,将access token传进来维持登录态,当它失效后返回"未登录"
可以预见的是,用户的功能需要频繁、相对重复的操作redis,可以尝试将功能拆分
并封装起来
redis
用户token
base server
/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
定义好一些固有的response格式
/common/define/response.py
# -*- coding:utf-8 -*-
class Response:
success = {'code': 0, 'msg': 'success'}
error = {'code': -1, 'msg': 'error'}
invalid = {'code': 401, 'msg': '用户未登录'}
/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)
/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()
/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()
/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位,包含字母、数字、英文字符其中两种的密码'))
/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()
/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()
/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 ''
)
)
按照注册,登录,获取用户信息,修改密码,退出登录,重新登陆流程测试一遍新接口
欠缺1:修改密码之后,未退出登录
商户与登录用户是一对一关系,一个登录用户仅可有一个商户,商户是未来贯穿整个业务的表。
注册商户成功后可用设置商户名下的店铺信息、交易金额结算账户信息等。
具体功能如下:
商户注册
商户信息查看
商户信息编辑 - 暂不开放
结算账户列表
结算账户创建
/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-冻结')
/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='配置项描述')
/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)
/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='操作失败')
/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='操作失败')
略