开发流程
公司高层 项目立项
|
市场部门 需求分析-->需求分析说明书, 需求规格说明书
|
产品部门 产品原型-->产品 UI 前端 后端 测试 移动端
|
|---------------|
研发人员 前端
架构设计 UI
数据库设计 前端代码编写
代码模块实现和测试 |
|------------------------|
|
网站代码整合
|
继承测试
|
网站发布
架构设计
分析可能用到的技术点
前后端是否分离
前端使用那些狂降-->vue.js
后端使用哪些框架-->Django REST Framework
选择什么数据库-->mysql
如何实现缓存-->redis+JWT(token本地存储)
是否搭建分布式服务
如何管理源代码
数据库设计
数据库-表的设计
根据项目需求设计合适的数据库表
若前期数据库表设计的不合理 后期随着需求增加将变的难以维护
集成测试
留意测试反馈平台的bug报告
----------------------------------------------------------------------------------------------------
创建仓库
配置个人信息 git config user.email/name
添加前端文件 git add 文件
提交到本地仓库 git commit -m"注释"
提交到远程仓库 git push
安装在文件夹中 npm install -g live-server 在静态文件中执行live-server
相当于前端文件的服务器 将前端文件独立运行 当作真正的客户端访问后端程序
在文件夹中创建项目
在项目文件夹中创子应用包 apps
在apps中创建子应用 python ../manage.py startapp users(注册登陆) verifications(短信验证) oauth areas goods contains cart order pay
创建libs 包 -->i第三方库
创建utils包-->公共的类类
创建script包-->脚本文件
配置 --->看课件
创建数据库管理员-->即不使用root账户
create user 账户名 identified by '密码';创建用户名和密码
grant all on 数据库名.* to '账户名'@'%';账户在任何ip下访问数据库的时候有所有表的所有权限
flush privileges;刷新生效
用户模型类 使用django 的认证系统 继承
django 的认证系统包括
用户
权限 (二元 标志一个用户是否可以做一个特定的任务)
组 (对多个用户运用标签和权限的一种通用方式)
一个可配置的密码哈希系统
用户登录或内容显示显示的表单和视图
一个可插拔的后台系统
Django默认提供认证系统中,用户的认证机制依赖于Session机制 ,本项目中使用的是JsonWebToken认证机制
将用户的身份凭证数据存放在Token中 然后再Django中认证系统 以此实现
用户的数据模型
用户密码的加密于验证
用户的权限系统
Djagno中提供的用户模型类User用于保存用户的数据 有以下默认字段
username first_name last_name email password(哈希和元数据) groups(与Group之间的多对多关系)
user_permissions(与Permission之间的多对多关系) is_staff(布尔值--是否可以访问admin站点)
is_active(账号是否激活 布尔值--建议使用逻辑删除False以防外键中断)
last_login(最后一次登录时间) date_joined(账户创建时间 当用户账号创建时默认时当前时间date/time)
常用的方法
set_password()不会保存User的实例对象 当None为raw_password时密码将是一个不可用密码
check_password()若给定的raw_passwor是用户的真实密码返回值为True 可以校验密码
创建用户模型类
User 继承自 AbstractUser
然后添加自己需要的字段
设置表名
在站点中显示的中文的单数verbose_name和复数verbose_name_plural二者可以相等表示显示的内容一致
在配置文件中告知系统我们自定义的 用户模型类
AUTH_USER_MODEL='子应用名.模型类名'
然后进行数据库迁移
注册 -->创建用户
判断用户名/手机号 使用失去焦点的事件 发送ajax请求 查看数据库
GET users/usernames//(?P
'''
分析 页面的总体需求
逐个分析具体小功能的需求
用户输入用户名 前端发送过来输入的内容 后端判断是否在数据库中存在
返回值可以是 标记位 或者 count
确定前端传什么内容
确定请求方式 GET 路由users/usernames/itcast/--> users/usernames/(?P
路由使用js中的(避免修改前端代码)
分析使用哪个视图 APIView GenericAPIView ListAPIView
APIView
按照需求分析 实现开发
查询数据库
返回count值 0 1
'''
配置url路由
模型 使用系统的dajngo用户模型 继承 过来 添加手机号字段
系统 的用户模型类 AbstractUser 没有mobile字段 继承过来 添加mobile字段就ok
自定义的用户模型类不能直接被Django的认证系统所识别 需要在配置中告知系统自定义的模型类
# AUTH_USER_MODEL = '子应用名.模型名'
数据库迁移
register.js 导入 vue, axios 把axios 放到js中 更改 register.html中的内容
光标移开后使用axios发送请求 查看用户名是否存在
手机号是否重复 使用失去焦点事件 发送ajax请求
图片验证
使用captcha
"""
前端把生成的uuid发送给后端
后端
接受uuid
并生成图片验证码
保存图片验证码内容
把图片返回给前端-->HttpResponse -->返回bytes类型数据 图片就是 刚刚好适用 s
请求方式 和 路由
GET verifications/imagecodes/uuid/(?P
确定使用哪个视图 APIView 不牵扯数据库crud
按照步骤开发
"""
设置url
设置 前端 mounted 挂载 vue的生命周期的钩子函数
发送短信验证码
验证图片验证码是否正确 传uuid 图片验证码 image_code
验证手机号格式 mobile
url
前端 js html
发送短信的时候的时间问题
发送短信同步操作 容易出现问题
使用celery 异步发送短信邮件等-->耗时操作
celery 即插即用的任务队列 --用就用删了也不碍事
celery的组成
任务(tasks) delay-->任务队列(broker 负责任务的分发行业存储) 任务处理者(worker 负责执行任务)
四个文件的关系进行分析
任务 就是一个函数 被celery的实例对象的task装饰 @app.task
创建实例对象
配置信息 broker单独的配置文件
app.config
app.autodiscover([任务])-->任务被Celery的实例对象自动发现
创建worker
celery_tasks包
发短信 发邮件的功能的包
包里面是发短信 邮件 的功能模块tasks.py
tasks.py 中 是使用Celery实例对象装饰的 任务函数
config.py 中 是配置中间人broker的redis地址 和 执行结果的地址
main.py 中 是创建Celery实例 工程配置文件 加载配置文件 和实例对象自动检测任务
功能函数调用时 必须使用.delay()
文档里面都有
注册
serializers.ModelSerializer
模型类中没有的字段的定义
class Meta:
指定模型类(model=模型类)
fields = ("字段", "包括自定义的字段--模型中没有的字段")
extra_kwargs = {}
def validate_单独验证的字段():-->手机号满足规则 是否统一协议
def validate()多个字段验证-->密码和确认密码二者一致 短信验证码
跨域CORS 同源策略
源 协议 域名 端口号
如url中的 协议 域名 端口号 相同 则为同源
同源策略 不同源的客户端脚本 在没有明确授权的情况下不能读写对方的资源
若有需要 需使用 跨域 跨资源共享 cors
设置 步骤
安装 注册 注册中间件在最上边 设置白名单
域名 编辑 /etc/hosts/文件 添加 ip 域名 的对应关系
将前端的ip 抽取为js文件设置 域名var host = '127.0.0.1:8000' 变量中声明host,
#允许哪些主机访问 安全机制
ALLOWED_HOSTS = ['127.0.0.1','api.meiduo.site']
密文存储密码
User使用的是系统的模型 可以使用set_password设置密文密码 因为系统的密码就是密文
user.set_password(validated_data.get("pasword"))
user.save()
脏数据要及时删除
JWT JSON WEB TOKEN -->token
用户登陆后 服务器返回token 客户端保存token
用户再登陆的时候在header中发送token 服务端验证通过 通过返回数据(服务端需要支持CORS)
token 分为三部分
头部 HEADER 承载两部分信息(类型 加密的算法) {"alg":"HS256", "typ":"JWT"}
载荷 PAYLOAD {"放一些":"有效信息"}通过base64加密 不可以放敏感信息
签证 SIGNATURE 加密之后的HEADER 加密之后的PAYLOAD 和SECRET
SECRET是保存再服务端的jwt的签发也是在服务端 secret是进行jwt的签发和验证的私钥 不能泄露
jwt的配置 参考课件
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),# token的有效期
# 自定义的返回登录需要的返回值的方法
'JWT_RESPONSE_PAYLOAD_HANDLER': 'utils.users.jwt_response_payload_handler',
}
配置url
url(r'auths/', obtain_jwt_token, name='auths'),
obtain_jwt_token这个自动获取token的视图默认返回值只有token
我们需要修改这个视图的返回值来完成我们自己的需求
自定义jwt返回制指定数据
在utils中添加users.py
里面实现自定义 jwt_response_payload_handler()方法实现自定义返回值 返回登录需要的值user_id username
注册完成之后实现自动登陆
注册完成之后返回数据之前 给他一个token
手动创建新令牌 返回token username user_id(前端需要的数据)
user 就是save()时生成的对象
登陆的实现
增加支持用户名与手机号均可以当作登录账号
通过Django认证系统的authenticcate()方法来支持使用手机号和用户名都能登录的功能
Diango的认证系统需要继承自django.contrib.auth.backends.ModelBackend 并重写authenticate()方法
authenticate(self, request, username=None, password=None, **kwargs)参数的说明
request 本次认证的请求独享
username 本次认证的用户账户
password 本次认证的账户密码
先通过系统的认证 认证成功之后生成token 重写 authenticate()方法添加手机号和账号
重写authenticate()方法的思路
根据username查找相应的用户对象 username即可能是用户名也可能是手机号
若查找到用户对象就使用check_password()方法验证密码是否正确
查找用户可能在多处使用 就写在utils中的users.py里面
1定义通过账户名或者手机号查找用户对象的方法
返回用户对象
2定义用户名或者手机号认证的类(继承自ModelBackend)
重写authenticate()方法
使用上面的方法获取用户对象
验证密码
返回用户对象
在配置中告知Django使用自定义的认证方法
AUTHENTICATION_BACKENDS = [
'utils.users.UsernameMobileAuthBackend',
]
第三方登录 QQ登录
到相应的平台中申请
`个人开发者
`企业开发者
申请应用
APP ID
APP Key(方便对方进行管理)
照文档开发
添加图标
图标按钮的onclick事件
点击图标转到一个页面
点击这个页面的同意获取到一个认证code
通过认证code 获取同意的token
通过token获取openid(用户在qq登录验证成功)
openid是此网站上唯一对应用户身份的标识(第三方网站保存这个openid就可以)
第三方网站可以存储
或者和用户进行绑定
按钮的点击事件 请求后端然后 后端拼接请求qq的认证页面的url
后端拼接完成之后返回给前端
添加路由
定义一个类视图 get 返回url
| | | |
qq登录的sdk
准备oauth_callback回调页,用于扫码后接受Authorization Code
通过Authorization Code获取Access Token
通过Access Token获取OpenID
QQ返回一个code 在回调的url中
让前端将code传递给后端
后端通过code从QQ得到token(QQ验证这个code是否合法 验证通过返回Access Token)
通过token获取open_id
根据open_id判断用户是否绑定
绑定就登陆
未绑定就进行绑定
请求方式 GET
路由 /oauth/qq/users/?code=xxx
视图 APIView
配置 url
视图 APIView
def get(self, request):get请求
code=接收code
if code is None:判断code
不存在 status=400 从rest_framew导入status (有状态码的含义)
qq = OAuthQQ(文档中需要的参数 与上一个不太一样client_id=xx, client_secret=xx, client_uri=xx)
# 根据code获取token
token = qq.get_access_token(code)# sdk封装的方法
# 根据token获取open_id(此网站上唯一的用户身份的标识)
open_id = qq.get_openid(token)# sdk封装的方法
try:
# 根据open_id判断用户是否绑定
qquser = OAuthQQUser.objects.get(open_id=open_id) # 通过open_id查询数据库得到qquser对象
except OAuthQQUser.DoesNotExist:
# 用户不存在就去绑定 --> # TODO
# openid属于敏感数据 绑定的时候需要一个有效期
# 将数据发送给一个不受信任的环境 需要设置密钥 传输的时候进行加密解密 使用it's dangerous JWS
# Json Web Signature
pass
else:
# 存在就应该登录 登录--就是返回Token username user_id
#用户存在就自动 -->返回{token(封装 调用) username user_id} --> user = qquser.user
user = qquser.user
from utils.users import send_token
token = send_token(user) # 返回的是user对象?
finally:
pass
个人中心
一个用户 多个商品 一对多
用户信息 用户表中 必须是登录用户才能访问
商品信息 在浏览表中
两个表两次查询中
两个接口 多次返回数据
必须是登录用户才能访问
接收用户信息
根据用户信息查询user
将user转化为字典 返回字典数据
token的传入方式 在请求头中添加Authorzation: JWT token
请求方式GET 路由/users/infos/token/ 视图RetriveModelMixin-->获取一个人的信息
个人中心信息展示
class 视图(APIView):
只能是登陆用户才能访问
permission_classes = [IsAuthenticated]
get
接收用户信息
user = request.user-->发起请求的用户对象
将对象转换为字典
serializer = 序列化器UserCenterInfoSerializer(user)-->使用ModelSerialzer
返回数据
return Respone(serializer.data)
设置邮箱
当用户输入邮箱之后点击保存
将邮箱信息 token 发送给后端
接收数据
保存邮箱信息到数据库中 添加邮箱的标记位email_active 字段(False未激活 True已激活)
发送一份激活邮件(邮件中包含用户和邮箱的识别信息,user_id和email数据 基于安全性考虑 使用itsdangerous进行加密 生成token作为链接的参数)
用户点击激活邮件 修改邮件的标记位 未激活变为已激活
返回响应
PUT /users/emails/ put-->在请求的body中 UpdateAPIView/APIView
class EmailUpdateAPIView(APIView):
permission_class = xx
def put(self, request):
data = request.data
user = request.user
serializer = EmailUpdateSerializer(instance=user, data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
# 保存之后发送激活邮件
# 借助第三方邮件服务器发送邮件 163
from django.core.mail import send_mail
send_mail(
subject = 主题
message = 信息
html_message = 可以是html标签
from_email = 发件人
email = request.data["email"]
recipient_list = []收件人列表
)
return Response(serializer.data)
send_email 的异步执行
EmailUpdateSerializer(serializer.ModeSerialzier)
meta
model= User
fields = ['email', 'id']
用户中心收购地址
省市区 三级联动
创建模型 数据库迁移 source 数据
表结构
id name parent_id
一个省 对应多个市
通过省获取市的数据-->关联模型类小写_set=[]
获取省份信息
一级视图 获取省的信息
get 方法 路由 areas/infos/
areas = Area.onbjects.filter(parent=None)
将列表转换位字典数据
使用序列化器serializer = AreaSerializer(areas, many=True)
继承自AreaSerializer(serializers.ModelSerializer
返回数据serializer.data
市 区县
ares/infos/pk
视图
get (pk)
获取指定数据(parent=pk)
使用序列化器 将列表数据转换为字典数据
返回数据serializer.data
使用视图集实现 ReadOnlyModelViewSet 或者 GenericViewSet
视图(ReadOnlyModelViewSet );
query_set=
通过重写get_query_set(self)方法 判断获取的是省份信息还是市区的信息
通过重写get_serializer_class(self)方法判断返回省份信息还是市区的信息
视图集的url
导入
from rest_framework.routers import DefaultRouter
创建实例router
router = DefaultRouter()
注册
router.register(r'infos', views.AreasViewSet, base_name="")
添加到urlpatterens
urlpatterns += router.urls
导入js 及 html文件
减少数据库的查询 优化(使用缓存~)
频繁查询 但数据基本不发生变化(在相对的时间内 如一小时 一天 一周...) 减小数据库的压力
缓存
settings配置
设置redis的库
设置时效
使用drf-extensions提供的扩展类CacheResponseMixin进行数据的缓存
drf-extensions扩展对于缓存提供了三个扩展类:
ListCacheResponseMixin
用于缓存返回列表数据的视图,与ListModelMixin扩展类配合使用,实际是为list方法添加了cache_response装饰器
RetrieveCacheResponseMixin
用于缓存返回单一数据的视图,与RetrieveModelMixin扩展类配合使用,实际是为retrieve方法添加了cache_response装饰器
CacheResponseMixin
为视图集同时补充List和Retrieve两种缓存,与ListModelMixin和RetrieveModelMixin一起配合使用。
三个扩展类都是在rest_framework_extensions.cache.mixins中。
用户地址管理
地址 用户 收货人 省 市 区 手机号 固定电话 邮箱 默认地址
address user_id receiver province city county mobile tel email True
每一个用户只有一个默认地址 可以将默认地址放到user表中 数据库优化
# 新增地址
class AddAddress(APIView):
"""
前端传入相应信息 地址 用户 收货人 省 市 区 手机号 固定电话 邮箱
接收数据-->接收的数据中没有user 但是可以通过request.user
得到 user后需要传给序列化器 使用context参数传过去
验证数据
数据入库
返回响应
"""
地址的CRUD 实现
商品部份 首页, 分类, 商品详情 的内容
1尽量多的分析字段 明显的字段先定义出来
分类表 名字 id
商品表 名字 id brand 价格 图片
品牌表 名字 id logo
2分析表和表之间的关系 (把表进行两两分析) 两个表之间的字段的关系分析 遍历分析
3只要是多对多就拆分成三个表
SPU satandard product ubit(标准产品单位) 商品信息聚合的最小单位 一个iPhoneX
SKU stock keeping unit(库存量单位) 库存进出的计量单位 一个金色的全网通256G的iPhoneX
FASTDFS 分布式文件存储系统 冗余备份 负载均衡 线性扩容
TrackServer --->负责负载均衡和调度
StorageServer-->负责文件存储
FastDFS环境配置搭建
使用docker进行安装
docker 没有可视化界面的虚拟机
安装 按照readme安装
是C/S架构
docker的镜像和容器
docker镜像-->安装系统的iso文件 或者虚拟环境
从服务器下载镜像 docker pull redis/mysql...
从本地加载镜像 docker load -i 镜像路径
导出镜像文件 docker save -o 文件名字.rar 镜像名
查看所有镜像 docker images
删除镜像 docker images rm 镜像名
docker容器 -->运行起来的镜像 就叫做容器 一个镜像可以创建很多个容器
运行镜像 docker run [选项] 镜像名 可选指令 例:docker run -it ubuntu 进入到交互模式 退出容器关闭
给容器起名字 :docker run --name名字 ubuntu
以守护进程的方式运行容器 docker run -dit 镜像名 后台运行 nginx就需要
查看正在运行的容器 docker container ls
罗列所有的容器 docker container ls --all (运行的没运行的都罗列出来) up是正在运行的
运行/停止容器 docker container stop/start 容器id/容器名字
进入正在运行的容器 docker exec[选项] 容器名/容器id /bin/bash
删除容器 docker container rm 容器名/容器id-->正在运行的容器需要停止运行再删除
自定义文件存储系统--->>>???????//TODO
Django自带由文件存储系统 但是是存储在本地
本项目中我们需要将文件存储在FastDFS服务器上
给tracker 1分配一个ip 2映射一个真实的目录环境来保存文件资源 3将trackerserver 启动起来
docker run -dti --network=host --name tracker -v /var/fdfs/tracker:/var/fdfs delron/fastdfs tracker
给storage 1分配一个ip 2让storage找到tracker 3给storage映射一个真实的环境 4将storage启动起来
docker run -dti --network=host --name storage -e TRACKER_SERVER=192.168.31.55:22122 -v /var/fdfs/storage:/var/fdfs delron/fastdfs storage ||||变化|||||
pip install /home/python/Desktop/fdfs_client-py-master.zip
导入配置文件client.conf到utils中
修改配置文件中的ip 为本机IP
在shell中执行
from fdfs_client.client import Fdfs_client
client = Fdfs_client('utils/fdfs/client.conf')
client.upload_by_filename('/home/python/Desktop/pictures/11.jpg')
_open(name, model="rb)-->打开文件 以二进制的方式
_save(name, content)-->保存 文件名 文件内容
open和save是必须实现的方法
_exist()如果文件名在系统中已经存在为True
url(name)返回访问文件的完整url
delete(name)通过文件名删除文件
listdir(path)列出路径中的内容
size(name)返回文件的大小
在fdfs中新增fastdfsstorage 编写自定义文件存储系统
1 自定义的文件存储系统必须继承自django.core.files.storage.Storage
2 在存储类中必须实现_save()和_open()方法, 以及任何后续可能使用到的任何方法
3 Django 必须能够在没有任何参数的情况下实例化您的存储系统,这就意味着任何所有的设置都应该来自django.conf.settings
4 您的存储类必须是可解构的以便于在迁移字段上使用时 可以对其进行序列化
---->只要您的字段具有可自行序列化的参数 就可以使用django.utils.deconstruct.deconstructible类装饰器
---->这个装饰器就是django系统的FileSystemStorage上使用的
5 在settings中配置自定义django文件存储类
富文本编辑器 CKEditor
类似于word文档 可以进行 排版 文字 图片 等等-->如商品的详情信息 SPU
安装
注册 富文本编辑器 及 其中的图片上传模块
在settings中进行配置
添加ckediter的路由 工程的路由
在商品SPU中添加响应的字段 详细信息 包装信息 售后服务 然后迁移字段到数据库
添加测试数据
在后台admin中显示
在contains的admin中注册模型类 广告内容分类ContentCategory 广告内容Content
在goods的admin中注册模型类 商品分类 GoodsCategory 商品频道GoodsChannel 商品Goods... 等模型类
创建admin超级用户
解压数据包data
删除stroage中原有的data文件夹
将解压数据包data移动到stroage中
导入sql数据
数据库备份 mysqldump -uroot -pmysql 数据库名 > .sql文件
数据库恢复 mysql -uroot -pmysql 数据库名 < .sql文件(路径)
首页静态化
"""
分类数据在其他界面中也有显示
所以单独做一个接口
首页商品数据 在商品表中
也单独做一个接口
首页的访问量比较大
导致首页商品数据接口 和 商品分类接口访问频繁
导致数据访问频繁 有损数据库性能
因为这两个接口的数据不经常发生变化 就可以使用缓存 cache
解决数据库频繁访问的问题 但是数据的组织需要一定的时间 用户体验不好 感觉比较卡
SEO闪亮登场
网站的搜索优化 主要抓取网站的静态页面 动态网页是抓取不到的
首页的静态化处理 将首页的部分内容先写好 用户访问的时候可先加载这部分内容
静态页面的数据是最新的数据
先查询数据
将查询出的数据填充到静态页面中
把最新的静态页面放到指定的目录中
"""
新建 crons.py-->首页静态化文件
...
先获取模板
到系统指定的模板目录中加载指定的模板
template = loader.get("模板文件名")
将获取的数据传递给模板
html_data = template.render(数据)
把htnl数据写入放到指定的目录中(写道front中)
file_path = os.path.join(配置中的变量, '模板文件名')
写入静态文件数据
with open(template) as f:
f.write(html_data)
新建模板文件夹
settings中配置模板文件夹 'DIRS': [os.path.join(BASE_DIR, 'templates')],
将模板文件(html文件)添加到templates中
安装 install
注册crontab
定时执行 静态首页获取最新数据 的任务
系统级的任务-->写在系统的settings中
CRNJOBS 的三个参数
1 频次 minute hour day month week
2 任务 (函数)
3 >> 日志的路径-->必须正确,文件可以不存在生成日志的时候自己创建
在Terminal中添加任务 crontab add 罗列任务crontab show
移除定时任务 python manage.py crontab remove
商品列表页面
页面分析
让前端传入分类id
根据分类查询分类下的商品列表 和 热销列表
2个接口
热销商品的接口
GET /goods/skus/(?P
类视图(ListAPIView)
指定序列化器
通过从写get_queryset(self):
从self中获取category_id
通过category_id从SKU表中查询出查询出3条数据安照销量sales降序排序
GET /goods/skus/(?P
商品列表数据的获取
视图(ListAPIView):
指定序列化器
添加排序
filter_backends=[OrderingFilter]
ordering_fields = ["字段", "字段", "字段"]
添加分页
pagenation_class = CustomPageNumberPagination----- page页码 page_size每页多少条数据
需要在 settings的REST_FRAMEWORK中添加PAGE_SIZE=2
或者通过继承的方式自定义page_size=2 但是默认是不能设置的继承重写page_size_query_param=page_size
通过从写get_queryset(self):
从self中获取category_id
通过category_id从SKU表中查询出查询出所有数据
class CustomPageNumberPagination(PageNumberPagination):
# 每页显示几条数据
page_size = 5
# 默认是None 设置的page_size无效
# 每页显示几条数据
page_size_query_param = "page_size"
列表页面商品分类的静态化
将不常变的数据先查询出来给模板
分类数据的静态化 分类数据很少发生变化 当数据发生变化的时候 重新 生成静态化页面
列表和热销数据 使用ajax进行局部刷新
在utils goods.py中创建 获取分类的方法 返回所有分类
在模型类发生变化的时候执行这个方法 重新生成静态化页面
在admin的admin.ModelAdmin的save()方法执行的时候执行
使用celery异步执行 生成静态页面的方法
商品搜索 -- 模糊搜索
安装镜像 通过docker
配置文件 的host改为本机的ip
like 的效率很低 不使用
使用 Elasticsearch 搜索引擎 配合elasticsearch-analysis-ik拓展实现中文分词处理
安装elasticsearch-ik 本地加载 docker load -i elasticsearch-ik--2.4.6_docker.tar
修改elasticsearch的配置文件 elasticsearc-2.4.6/config/elasticsearch.yml第54行,更改ip地址为本机ip
network.host: 本机ip
创建docker容器运行
docker run -dti --network=host --name=elasticsearch -v /home/python/Desktop/elasticsearch-2.4.6/config:/usr/share/elasticsearch/config delron/elasticsearch-ik:2.4.6-1.0
把数据组织好之后 给搜索引擎 让搜索引擎进行分词处理
借助haystack将数据传递给elasticsearch data-->haystack-->elasticsearch
安装haystack
注册haystack
在setting中进行haystack的配置 url为本机的
创建索引类 -->在goods中(针对商品的搜索)search_indexes.py
创建模板-->search文件夹 indexes文件夹 goods文件夹 sku_text.text文件
里面指定要创建分词的字段{{object.id}}{{object.name}}...
在虚拟环境中执行 python manage.py rebuild_index 生成索引分词
在视图中创建搜索的视图集
创建搜索的序列化器
添加路由
添加search 的html 和 js
商品详情页面
静态化 在商品的信息发生变更的时候触发生成静态化页面的功能
celery -A celery_tasks.main worker -l info-->在有celery_tasks的目录下执行
使用脚本工具自动生成所有的商品详情页面
shell编程
#! usr/bin/env python -->指定环境
编写python共功能代码
切换到相应目录
改变文件的执行权限
执行文件
用户浏览历史记录
只记录登录用户的浏览记录
保存user_id sku_id
用户访问某一个商品详情的时候将用户id商品id发送到后端
将数据保存到redis中 数据库中都可以
POST users/histories/ CreateAPIView
只有登录用户才能访问
接收数据 验证数据 保存数据 返回响应
按照步骤开发
当用户访问个人中心的时候将数据添加到最近浏览中
最近浏览的数据结构
user_id:[sku_id1,sku_id2,sku_id3,sku_id4,sku_id5]
使用有序集合ZSet 有序且不重复 通过权重将元素进行升序排序
此处权重设置为时间的先后---->自行实现
redis的数据结构
String key:value字符串
List key:[value1,value2,value2,,...]列表
Hash key:field:value field2:value2..哈希
Set key:[value2,value1,value3,,...]集合
ZSet key:[value1,value2,value3,,...]有序集合
视图
指定权限
指定序列化器
保存数据-->重写序列化器的create方法
链接redis
获取数据(user_id, sku_id)user通过context的request中获取
在添加之前先删除可能存在的sku_id ()
lrem(user.id, 0, sku_id)
保存
lpush(user.id, sku_id)从左边插入数据
限制五条数据 ltrim 让列表只保留指定区间内的元素
ltrim(user.id, 0,4)
列表去重
在插入数据到redis中前 查询redis中是否有重名的数据 删除重名的数据再插入数据
购物车 部分
不登录用户也可以访问购物车 将商品添加到购物车中
数据是保存在浏览器的cookie中
登录用户将数据保存到在服务器 redis和mysql都可以保存
实际开发最好放在数据库中 本项目为使用reids知识 就保存在redis中
需要保存的数据
未登录用户 商品id 数量 是否选中-->记住会动的东西
登录用户 商品id 用户id 数量 是否选中
未登录的数据的组织 cookie数据
cart = {"sku_id":{"count":5,"selected":True}}
登录用户的数据的组织 redis数据
保存在redis中-->使用列表可以 但是不稳定 数据容易很混乱
使用Hash哈希可以但是缺少状态 所以只记录商品的id和个数
cart = {"user_id":{"sku_id":"count"}, "user_id": {"sku_id": "count"}}
使用Set 集合 记录选中的id
cart_selected ={"user_id":{"sku_id", "sku_id", ..}}
cookie数据的处理 cookie数据的加密 使用pickle模块
pickle.dumps(数据)将python数据类型数据转换为bytes类型
pickle.loads(数据)将bytes类型数据转换为python数据类型
使用base64模块将bytes类型的数据重新编码
import pickle
import base64
cart = {"1": {"count": 5,"selected": True}}
# 将字典数据转换为bytes类型
dumps = pickle.dumps(cart)
# 将bytes类型的数据使用base64重新编码
encode = base64.b64encode(dumps)
# 将重新编码之后的数据转换为字符串
encode.decode()
# 将数据转换为bytes类型
base64.b64decode(encode)
base64模块 以6个bit为一个单元 加密 解密 没有语言之间的限制
对cookie数据进行重新编码
base64.b64encode(bytes类型数据) 对bytes类型进行编码 可使用decode()转换为字符串
base64.b64decode(base64数据) 解码
如用户token过期 就让其以匿名用户的身份访问 将商品添加到购物车(数据放到cookie中)
需要重写 perform_authentication()方法 不让它验证 在需要判断的时候在进行判断
将商品添加到购物车
当用户点击添加购物车按钮的时候 登陆用户需要进将token(request.user) sku_id count selected 提交给后端
后端接收数据
验证数据 序列化器完成验证(sku_id, count, selected)
获取数据 serializer.validated_data.商品数据信息
得到用户的信息 request.user
判断user是否存在(是否登录)
用户存在将数据保存在redis中(登录)
链接redis
保存数据 set hash redis_conn.hincrby("cart_%s" % user.id, sku_id,count)
将选中的数据保存到set中 sadd("cart_selected_%s"%user_id, sku_id)
返回数据
不存在cookie中(未登录)
先读取cookie信息 request.COOKIES.get("cart")
判断是否有购物车信息
如果有是加密的数据(base64编码的bytes的数据)
解码(pickle.loads(base64.b64.encode(接收的cookie_cart)))
没有
初始化一个空字典
判断该商品是否存在cookie中
如果有
累加 last_count = cart[sku_id]["count"] count+=last_count
如果没有
添加 cart[sku_id] = {"count":count, "selected":selected}
将购物购物车数据加密 response.set_cookie("cart", base64.b64encode(pickle.dumps(cart)).decode())
返回购物车数据 return response
cookie信息的读取 value = request.COOKIES.get("键")
获取购物车数据
判断用户是否是登陆用户-->能不能从request中获取用户信息
登录用户从redis中查询数据
链接redis
获取哈希数据
hgetall-->获取哈希中的所有域和值
获取set数据 redis_cart
遍历哈希数据 redis_selected
将其转换为与cookie_cart一样的数据格式 redis中取出的数据是bytes类型的
cart = {}
for sku_id, count in redis_cart.items():
cart[int(sku_id)] = {
"count":int(count),
"selected": sku_id in redis_selected
}
未登录用户从cookie中获取数据 request.COOKIES.get("cart")
判断cart数据
有 解密 pickle.loads(base64.b64decode(cookie_cart))
无 为空 cart = {}(初始化一个)
商品id-sku_id 数量-count 选中状态-selected
获取传入的购物车中的所有的商品
遍历所有商品 动态的给每件商品添加属性
sku_ids = cart.keys()
skus = SKU.object.filter(id__in=sku.ids)
for sku in skus:
sku.count = cart[sku_id]["count"]
sku.selected = cart[sku_id]["selected"]
将对象转换为字典
seriaizer = 序列化器(skus, many=True)
返回数据
修改购物车数据
幂等 -->前端发送的是完整数据 校正前后端的数值的一致即可
非幂等-->前端发送的是每次加的数量 有可能受网络波动影响 造成数据出错
前端传入sku_id(must) count(must) selected(must) token(可选)
接收数据
验证数据
获取校验过的数据 sku_id count selected
获取用户
判断用户是否登陆
登录
更新redis
链接redis
更新hash hset("cart_%s" %user.id, sku_id, count)
更新set 选中: sadd("cart_selected_%s"user.id, sku_id)
未选中 srem("cart_selected_%s"user.id, sku_id)
返回数据
未登录
更新cookie
获取cart数据
有 解密 pickle.loads(base64.b64decode(cookie_cart))
无 为空 (初始化)
更新数据
if sku_id in cart:
cart[sku_id] = {"cont":count, "selected":selected}
加密数据 cookie = base64.b64.encode(pickle.dumps(cart))
response.set_cookie("cart",cookie .deocde() )
返回数据
return response
# 删除购物车数据
# 接收数据 data = request.data
# 校验数据 序列化器(data=data) 序列化器.is_vali()
# 获取验证过的数据 sku_id = serialzier.validated_data["sku_id"]
# 获取user
# 根据状态判断操作redis还是cookie
# 登录 操作redis
# 链接redis
# 删除哈希 hdel("cart_%s"%user.id, sku_id)
# 删除set srem("cart_selected_%s" % user.id, sku_id)
# 返回响应
# 未登录 cookie
# 获取cart信息 request.COOKIES.get("cart")
# 判断cart数据
# 有 解密 pickle.loads(bsae64.b64decode(request.COOKIES.get("cart")))
# 无 为空 (初始化)
# 删除数据 if sku_id in cart: del cart[sku_id]
# 加密字典数据base.b64encode(pickle.dumps(cart)).decode
# response.set_cookie(base.b64encode(pickle.dumps(cart)).decode)
# 返回响应 return response
---->>4.11日实现
HINCRBY redis_conn.hincrby("cart_%s" % user.id, sku_id, count) 添加购物车使用代替自写大段代码
redis 管道技术 pipeline()
性能优化 减少c和s的链接(tcp包)的建立次数
创建管道实例
p = redis_conn.pipeline()
将命令添加到实例zhong
p.hincrby("cart_%s" % user.id, sku_id, new_count)
执行管道
p.excute()-->一定记得要执行管道啊 啊啊啊啊啊啊啊啊!!!!!!
登录合并cookie中的数据到用户的购物车redis中
登录的时候
在登录obtain_jwt_token()系统的不能满足我们的需求时
重写它 自定义一个类 继承自ObtainJSONWebToken drf_jwt 提供的登录获取token的视图
修改url 件url指向这个类
将ccokie数据合并到redis中
写到函数中 在用户 登录的时候调用这个函数完成合并
获取cookie数据 {sku_id:{count:xx,selected:True}}
获取 redis数据 hash{sku_id:count} set{sku_id}
合并
以redis为主
cookie中有的数据redis中没有 添加 状态添加
cookie中有的数据redis中有 替换 状态添加
数据合并之前 定义二个变量
一个接收redis数据 一个记录选中的id
对cookie进行变历 合并数据
将最终的数据保存到redis
合并之后删除cookie数据
|||||||||||||||||||||||
merge_cookie_to_redis(user, request, response)
获取cookie数据get
判断cookie数据 if not None
解密cookie数据 loads(base64)-->cart
获取redis
链接
获取hash hgetall bytes类型-->{sku_id:count}-->{b'1':b'10'}
定义redis_sku_ids = {} 存放遍历完成之后的int类型的数据
定义selected_ids = [] 存放cookie中被选中的商品的ids
遍历redis数据 for sku_id, count in redis中获取到的数据的items():
对redis数据进行转换for循环
redis_sku_ids[int(count)]=int(count)-->redis_sku_ids[int(count)]=count
遍历cookie数据 for sku_id ,count_selected in 解密后的cookie中的cart的items():
如果cookie中的sku_id在redis中存在 if sku_id in redis_sku_ids:
就将redis中的该商品的数量改为cookie中的 redis_sku_ids[sku_id]=count_selected小字典["count"]
如果不存在 就写入
redis_sku_ids[sku_id] = count_selected["count"]
添加到redis中 hmset-->保存多个哈希数据
redis_conn.hmset("cart_%s"%suer.id, redis_sku_ids) 就是保存int类型的redis数据的字典
sadd-->一个一个添加 解包 *
redis_conn.sadd("cart_selected_%s"%user.id, *selecte_ids) 就是将保存cookie中被选中的商品的id解包
删除cookie数据 response.delete.cookie("接收的cookie数据")
response.delete.cookie("cart")
return response
else:
返回响应
订单模块
支付功能 orders.py
登录用户才能访问 只有登录用户才有收货地址
收货地址的获取 直接访问接口就可以
商品列表从redis中获取 展示选中的商品
运费 动态返回
前端传入用户信息 token(user)
登录用户才能访问
获取用户信息
获取redis信息 从redis中查询购物车的信息 通过user.id
链接
获取redis数据
hash hgetall-->获取所有的商品id和数量{sku_id:count,sku_id:count}
set smembers-->获取所有被选中的商品的id [sku_id, sku_id,sku_id,..]
cart = {}
根据id查询redis中选中的商品的信息
for sku_id, count in 哈希结果:
放到cart中
cart[int(sku_id)] = int(count)
skus = SKU.objects.filter(id__in=set查询结果)-->所有被选中的商品 (只有被选中的商品才应该提交付款)
for sku in skus:
sku.count = cart[sku_id]
定义 运费 decimal类型数据 精度准
将对象列表转换为字典列表
serializer = 序列化器({"skus":skus, "freight":freight})
序列化器skus many=True
skus = 指定序列化器-->序列化器的嵌套
返回数据
return Response(serializer.data)
运费的动态返回
将运费信息和商品信息放在一个字典中 各占一条数据
组织数据的时候
context = {
运费:货币类型,-->DecimalField(总长度,小数位数)可以保证数据存储的精确度
商品信息:serializer.data'
}
序列化器的嵌套
字段 = 序列化器(条件)
字段 =serializer.类型(选项)
使用时 将数据放在字典中
提交订单 使用到事务和并发
提交订单 点击按钮生成订单
不相信前端的数据
涉及到金钱等敏感数据 需要自己从库中查询
商品的数据是在redis中 不需要前端传入
前端需要传入 用户信息 支付方式 地址信息
订单表 一个订单中有多个商品 一对多
调用序列化器的save()方法时
重写 序列化器的create方法
需要将 订单订单信息 和 订单商品列表的数据入库
同时操作两个表
首先实现订单 信息的入库 (一)
接收地址信息
接收用户信息
接收支付方式
生成订单id order_id 使用Django的time_zone.now .strftime(%Y%m%d%H%M%S)+"%09d"%user.id
订单总数量 价格 运费 total_count = 0 total_amount=Decimal("0") freight=Decimal("10")
根据支付方式决定订单状态 if pay_method ==1:satus==2 else:status = 1 可以使用常量表示
创建订单 order = OrderInfo.fiter.create()
在实现 订单商品列表的保存 (多)
链接redis -->cart
获取redis数据
hash hgetall
set smembers
将redsi数据转换 selected_cart={}for sku_id in set
只转换选中的信息
对选中的商品的列表进行遍历 for sku_id ,count in set.items():
根据商品信息进行查询 sku = SKU.objects.get(pk=sku_id):
根据商品数量判断库存 if sku.stock < count:
库存减少 销量增加 sku.stock -= count sku.sales += count
数据入库 sku.save()
order.total_count += count
order.total_amount += (count * sku.price)
order = Order_Goods.filter.create()
清除redis中选中的商品的信息
同时操作多个表 使用事务 要么一起成功 要么一起失败
事务的with 语法-->在创建订单表之前
save_pont 失败的话回滚到这里
with transaction.atomic():
需要记录保存点
回滚rollbask()-->写在raise的前面 回滚到save_point
提交commit()-->提交point
并发
多个用户同时对同一个商品下单时提交数据 对表进行操作 出现资源竞争问题
可以给该条数据加锁
悲观锁-->当操作某条记录时 让数据库为该条记录加锁 锁住后别人无法操作 容易出现死锁现象 采用不多
乐观锁-->在更新的时候判断此时的库存是否是之前查询出的库存
是就表示没人修改可以更新库存 不一样就表示被别人抢过资源 就不再执行库存更新
乐观锁需要事务隔离级别的配合
事务隔离级别
串行化 serializable一个一个执行
可重复读repeapable read 本事务不受其它事务的影响 不管其它事务是否修改了数据MySQL的默认事务隔离级别
读取已提交 read-committed 当其它事务提交了对数据修改 本事务就能读取到修改后的数据值
读取未提交 read-uncommitted其它事务只要修改了数据 即使未提交 本事务也能看到修改后的数据值
修改mysql的隔离级别 为read-committed
支付 支付宝支付的接入
电脑网站的支付流程
选择支付宝支付 点击去支付 根据支付宝的文档拼接url跳转到到 支付宝的支付页面 用户登录 选择我支付渠道 输入支付密码 确认支付 支付宝会把支付的交易流水号trad_no给商户系统 商户系统需要保存这个流水号(用户退款的唯一凭证) 将支付宝交易流水号与商户的订单号一一对应(写在一张表中)
apoid
沙箱网关
应用程序公钥 使用指令生成的--公钥放在支付宝--私钥放在程序keys中
支付宝公钥 支付宝--私钥放在支付宝--公钥放在应用程序中(从支付宝复制的是字符串需要在首尾添加标记)
当用户点击去支付
前端将订单id传入
后端接收订单id
根据订单id查询订单的信息 应该查询当前用户未支付订单
创建支付宝实例对象
生成order_string
拼接支付的url
当支付成功之后
让前端传入回调参数 pay/status/?xxx=xxxx
后端接收参数 查询字符串 sign 不能参与验证 就把它删除了
根据文档进行数据验证
若验证成功 获取 订单id 和支付宝流水号
将支付宝流水号和订单id保存起来
修改订单状态
xadmin
注册xadmin
迁移
创建xadmin.py 关联adminx和模型类
主从同步的机制是主服务器的二进制日志
优点 提高数据库性能 提高数据安全性 提高主服务器性能
读写分离
收集静态资源到front 的static中
使用Nginx处理静态请求 静态部署
动态请求需要Nginx转发给uwsgi 再转发给Django
加载配置文件
server / {
server_name 域名或者 ip;
listen 端口;
location 规则{
root 引导的路径; + 访问路径/后面的内容
index html文件;默认显示哪个文件
alias 引导的路径; 路径的别名是server后面的内容
}
}
反向代理 监听用户输入的端口 ip(域名) 当用户输入指定的ip+port时 nginx将其 proxy_pass到指定的程序(域名/ip+端口)中 程序的端口 与 nginx监听的端口 不能相同!!
sudo vi upstream 在里面设置
server { listen port; # 用户请求的
server_name 域名/ip;# ip和端口
location / {
proxy_pass ip:port ; # 将用户的请求转到这里
}
}
负载均衡 依赖于反向代理 通过反向代理 将请求引导到负载均衡的服务器中
sudo vi upstream 在里面设置
upstream backends {
server1 ip:port ;
server2 ip:port ;
server3 ip:port ;
....
}
server {
listen port;#用户输入的
server_name ip;#ip和端口
location / {
proxy_pass http://backends/;
# 通过反向代理将 请求顺序依次(可以进行不同算法的配置)分发到backends中的各个服务器中
}
}
根据权重 设置 每个服务器的请求的接收请求的量
upstream backends {
server1 ip:port backup;# 备胎 不参与 当下面的服务都器宕机了 就启用这个1
server2 ip:port weight=5; # 是倍数 2的接收请求的频率是3的5倍
server3 ip:port weight=1;
....
}
server {
listen port;#用户输入的
server_name ip;#ip和端口
location / {
proxy_pass http://backends/;
# 通过反向代理将 请求顺序依次(可以进行不同算法的配置)分发到backends中的各个服务器中
}
}
通过ip的hash值 将用户分配到指定的服务器中
upstream backends {
ip hash;
server1 ip:port;
server2 ip:port;
server3 ip:port;
....
}
server {
listen port;#用户输入的
server_name ip;#ip和端口
location / {
proxy_pass http://backends/;
# 通过反向代理将 请求顺序依次(可以进行不同算法的配置)分发到backends中的各个服务器中
}
}
nginx 日志的配置是在负载均衡中进行设置的
Docker进阶 一条小鱼(docker) 在海洋中(宿主机) 可以存放很多小箱子(images)
dockerhub 存放很多小箱子 可以获取使用
docker 的三个概念
image 镜像--->独立的空间
包含用户root空间 和 内核 用户空间建立在内核的基础之上的
搜索镜像 docker search 镜像名
罗列镜像 docker images
拉取镜像 docker pull 镜像名:版本号(可选, 不写版本号就是最新的)
打包镜像 docker save -o 文件名 镜像名
加载镜像 docker loads -i 文件名
删除镜像 docker image rm 镜像名(image id)
命名镜像 dcoker tag 镜像名:版本号 新名字:版本号
docker镜像 使用官方的
cointainer 容器
实质是一个进程 拥有自己的文件系统 网络 等 -d就是以守护进程的方式运行
数据不能写入容器中 文件要保存再数据卷中
罗列容器 docker ps -a(--all)
运行容器docker run -d守护进程方式运行--name起别名 -it以交互模式运行(直接进入,退出就关闭进行)容器名
/bin/bash 进入文件系统
进入正在运行的容器 docker exec -it 容器名 /bin/bash
查看容器信息 docker inspect 容器名
删除容器 docker rm 容器名
容器打包为镜像 docker commit -t"操作内容" 容器名 新名字:版本号(tag)
查看容器日志 docker logs
Repository 仓库
大仓库 比如 dockerhub(共有仓库) 把镜像放在仓库中 再从仓库中pull 名字:tag
私有仓库 自己创建
下载 docker pull registry
启动仓库 docker run -d -p 5000:5000 --restart=always --name registry registry
检查仓库效果 curl 127.0.0.1:5000/v2/_catalog
配置仓库权限 在/etc/docker/daemon.json中
{
"registry-mirror": [
"https://registry.docker-cn.com"
],
"insecure-registries": [
"本机ip:5000"
]
}
重启docker服务 systemctl restart docker
提交镜像到私有仓库 docker tag old_image_name:tag ip:5000/new_image_name:tag
仓库必须通过 https访问 推送 docker push ip:port/镜像名:tag
下载镜像 docker pull docker pull 192.168.229.128:5000/nginx:v1.0
Volume数据卷
不要往容器中写入文件 要放在数据卷中-->映射宿主机的目录
设置数据卷目录 docker run -itd --name 容器名字 -v 宿主机目录:容器目录 镜像名称 [命令(可选)]
再宿主机中的目录中操作和再容器中操作 是同步的
-v 指向宿主机的目录 就算容器删除 数据卷也会存在
数据卷容器 (多个容器之间共享文件)
创建数据卷容器目录
docker create -v 容器数据卷的目录 --name 容器名字 镜像名称 [命令(可选)]
创建两个容器,同时挂载数据卷容器目录
docker run --volumes-from 数据卷容器id/name -tid --name 容器名字 镜像名称 [命令(可选)]
网络
dockefile
创建一个文件 Dockerfile
再文件中写入指令
FROM 解释器:版本号 基于哪个镜像
LABLE 注释:人员, 版本等
RUN 指令
WORKDIR /工作目录
COPY 原路径 目标路径 将Django项目考入当前路径
CMD ["pyhton3", "manager.py", "runserver", "0.0.0.0:800"] 容易一创建就执行
EXPOSE 8000 对外声明端口
构建镜像 docker build -t django:版本号 . echo "文件内容">文件名 (创建一个文件文件内容是""内的内容)
安装 docker 参照文档
json.loads()用于将str类型的数据转成dict
json.dumps()用于将dict类型的数据转成str
17344436474
bug 集锦
401 身份未认证
无法访问FastDFS中的图片 tracker storage及其IP设置 settings的配置 自定义存储类的配置
无法登录 允许携带cookie 允许访问的ip cors的设置(注册,中间件配置,白名单)
掉用serializer.save()方法之前必须调用serialzier.is_valide()方法
serializer.序列化器(data=数据, instance=对象)
serializer = 序列化器(数据)不用data=数据
Decimal 货币类型
有模型的序列化器
class 序列化器():继承自ModelSerializer
有自定义的字段就写-->需要添加到fields中
class Meta:
model = 指定模型类
fields = ["要","返回","给前端","的字段","也可以","理解为","要验证","的","字段"]
exclude=("模型","中","不需要","使用(验证)","的","字段")
extra_kwargs={"字段1":{"额外的":"选项约束"},"字段2":{"额外的":"选项约束"},..}
不满足需求 的方法重写(self,validated_data)
重写
返回 validated_data
序列化器数据流向 视图导入序列化器-->data(value,attrs)---->validated_data
没有模型的序列化器
class 序列化器():继承自Serializer
字段 = 类型(选项)
def 单个字段验证(self, value)
在验证
返回value
def 多个字段验证(self, attrs):
验证
返回 attrs
判断该用户是否频繁获取
if redis_conn.get('sms_flag_%s'%mobile):
return Response(status=status.HTTP_429_TOO_MANY_REQUESTS)
redis_conn.setex('sms_flag_%s'%mobile,60,1)-->60秒内获取一次
省市区的模型
省级行政单位 没有 parent id
市级行政单位的parentid是省
区县级行政单位id为市
使用自关联模型
自关联字段的外键 指向自身