单点知识
# 需求
需求陈列出来后, 先提取出单点功能, 先实现单点功能!!别一上来就开发.
- 认证模块, 用户名密码 或 手机短信登录(60s有效).
- 角色管理, 不同角色具有不同权限 和 展示不同菜单.
管理员,充值
客户,下单
- 客户管理, 除了基本的增删改查以外,支持对客户可以分级,不同级别后续下单折扣不同.
- 交易中心
- 管理员可以给客户余额充值/扣费
- 客户可以下单/撤单
- 生成交易记录
- 对订单进行多维度搜索,例如:客户姓名、订单号.
- worker,去执行订单并更新订单状态.
worker简单理解就是用python写的一个脚本,它获取到任务后,可以在内部开一些线程池帮助我们并发的进行操作.
Ps: 可以将py脚本文件打包成exe文件,点击运行.
脚本文件部署后,还可以进行监控,可以进行重启等操作.
1> 去redis中取任务
2> 将该任务/订单在数据库中的状态修改为执行中
3> 获取任务详细: 比如,将某个视频的播放量刷到10000
4> 利用线程池或协程实现
5> 更新订单状态为已完成
from concurrent.futures import ThreadPoolExecutor
def task(video_url):
# 根据视频地址实现刷视频
pass
pool = ThreadPoolExecutor(50) # 最多有50个线程
for i in range(10000): # 将10000个任务交给线程池
pool.submit(task, "视频地址") # 执行任务
pool.shutdown() # 卡住,等待所有的任务执行完毕.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[订单系统(刷播放量的)] 先理清楚大体的业务功能!
2、3个角色 + 4个业务模块/20来个路由 的小项目, 可以考虑 基于“配置文件”来实现权限的控制!!!
客户登陆后 客户在平台下单,后台自动执行下单的任务.
管理员登陆后
两角色: 管理员、客户
登陆模块: 用户名密码登陆、短信验证码登陆
角色管理: 不同角色具有不同权限 和 展示不同菜单
管理员:
级别、客户、价格策略 的CURD.
管理员可以对客户进行充值或扣款,会产生相应的交易记录.
管理员可以对客户重置密码.
管理员可以查看所有的交易记录(可组合搜索)
管理员可查看所有的订单信息
客户:
可下单
可查看自己所有的订单记录
可查看自己所有的交易记录
2
3
4
5
6
7
8
9
10
11
12
13
# 发短信
API服务: 比较繁琐, 得先处理签名和加密等..然后调用接口传入相应的参数.
SDK服务: 已经封装好并在pypi里发布了,直接install就可以使用!!
pip install tencentcloud-sdk-python
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sms.v20210111 import sms_client, models
cred = credential.Credential("AKIDVqukLfiV1HGyPZT6SYNvCCwPbXCbjaATABC", "3qMbAWcS1JK4YlhMltlNinb8PP8alfJgABC")
client = sms_client.SmsClient(cred, "ap-guangzhou")
req = models.SendSmsRequest()
req.SmsSdkAppId = "1400804452"
req.SignName = "OnePicec公众号"
req.TemplateId = "1695872"
req.TemplateParamSet = ["688688", '5']
req.PhoneNumberSet = ["+8617380646918"]
resp = client.SendSms(req)
# 输出json格式的字符串回包
print(resp.to_json_string(indent=2))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 菜单设计思路
需求: 不同的用户登陆, 看到不同的菜单.
理性分析: 若角色很少, 就没必要使用方式三(使用数据库,去数据库中查.)
# 方式一: HTML模版
页面写死HTML模版
角色一多, 配置起来也些许麻烦,
<body>
{% if role == "管理员" %}
<a href="/xxx/xx/">级别列表</a>
<a href="/xxx/xx/">客户列表</a>
<a href="/xxx/xx/">价格策略</a>
<a href="/xxx/xx/">交易策略</a>
{# ... ... #}
{% else %}
<a href="/xxx/xx/">价格策略</a>
<a href="/xxx/xx/">交易策略</a>
{% endif %}
</body>
2
3
4
5
6
7
8
9
10
11
12
# 方式二: 配置文件
菜单放到配置文件中
相比于方式一, 对菜单的修改无需动html的内容.. 直接修改菜单的内容.
但角色一多, 配置起来也些许麻烦,
# 大致思路
settings文件中配置
# settings.py
ADMIN = [
{"title":"级别列表", "url":"..." },
{"title":"客户列表", "url":"..." },
{"title":"价格策略", "url":"..." },
{"title":"交易策略", "url":"..." },
]
USER = [
{"title":"价格策略", "url":"..." },
{"title":"交易策略", "url":"..." },
]
2
3
4
5
6
7
8
9
10
11
12
13
在模版中的呈现, 当然可以使用自定义的模版 inclusion_tag !!
<body>
{% if 角色 == "管理员" %}
{% for item in ADMIN %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
{% else %}
{% for item in USER %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
{% endif %}
</body>
2
3
4
5
6
7
8
9
10
11
# 需解决问题
问题一: 需要显示几个级别的菜单
问题二: 菜单默认选中和展开问题
问题三: 路径导航问题.
# -- 一级菜单
ADMIN = [
{"title":"级别列表", "url":"..." },
{"title":"客户列表", "url":"..." },
{"title":"价格策略", "url":"..." },
{"title":"交易策略", "url":"..." },
]
# -- 多级菜单
"""
客户管理
级别列表
添加
编辑
删除
客户列表
订单管理
价格策略
交易策略
"""
ADMIN = [
{
"title":"客户管理",
"children":[
# 若url比较长,可以为其设置个name!通过name反向生成即可.
{
"title":"级别列表",
"url":"...",
"name":"level_list",
"children":[
{"title":"添加","url":"..."},
{"title":"编辑","url":"..."},
{"title":"删除","url":"..."},
],
},
{"title":"客户列表","url":"..."},
]
},
{
"title":"订单管理",
"children":[
{"title":"价格策略", "url":"..." },
{"title":"交易策略", "url":"..." },
]
},
]
# -- 菜单选中和展开问题
1> 获取当前用户请求的URL (eg: pricepolicy/list/ 或 通过url对应的name反向生成)
2> pricepolicy/list/ 匹配 ADMIN中的URL --> 将其在页面上默认选中
# -- 路径导航问题
1> 获取当前用户请求的URL
2> 获取上级,展示导航信息
3> 设置菜单与下级关系
像添加删除编辑,一般都不挂到菜单展示的地方,而是挂到某个菜单下.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 方式三: 数据库
将菜单 + 角色写入数据库中.
动态分配角色, 比较方便.
# 大致思路
| id | title | url |
| ---- | -------- | ---- |
| 1 | 级别列表 | ... |
| 2 | 客户列表 | ... |
| 3 | 价格策略 | ... |
| 4 | ...... | ... |
| id | 角色 |
| ---- | ------ |
| 1 | 管理员 |
| 2 | 用户 |
| id | role_id | menu_id |
| ---- | ------- | ------- |
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 2 |
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在页面展示数据库
1> 连表查询特定角色关联的所有的菜单
2> 在页面上进行展示
# 具体分析
菜单的多级关系 在数据库中如何实现呢? -- 自关联
id | title | url | parent_id |
---|---|---|---|
1 | 客户管理 | null | null |
2 | 级别列表 | ... | 1 |
3 | 客户列表 | ... | 1 |
4 | 订单管理 | null | null |
5 | 价格策略 | ... | 4 |
6 | 交易策略 | ... | 4 |
7 | 添加 | ... | 2 |
8 | 编辑 | ... | 2 |
9 | 删除 | ... | 2 |
"""
客户管理
级别列表
添加
编辑
删除
客户列表
订单管理
价格策略
交易策略
"""
客户管理是没有url的,因为点击它只是展开,不会跳转.它属于一级菜单,所以parent_id也为null.
级别列表的parent_id为1,表明它的上一级是客户管理.
添加、编辑、删除的parent_id为2,表明它的上一级都是级别列表.
获取一级菜单 --> 找表中,parent_id等于null的.
获取某个二级菜单(比如客户管理的) --> 找表中,parent_id等于1的.
获取某个三级菜单(比如级别列表的) --> 找表中,parent_id等于2的.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 扩展: 多级评论
平台的多级评论 - 自关联
id | content | root_id | parent_id | depth |
---|---|---|---|---|
1 | 优秀 | null | null | 0 |
2 | 不咋样 | null | null | 0 |
3 | 确实 | 1 | 1 | 1 |
4 | 哈哈 | 1 | 1 | 1 |
5 | 你说的都对 | 1 | 3 | 2 |
"""
优秀
确实
你说的都对
哈哈
不咋样
"""
根据生活经验,不难知道,有些评论系统默认只展示二级评论,三级四级的评论要点击才会显示.
若我只想展示一级评论,就在表中找depth等于0的记录.
root_id表明根评论,parent_id表明父评论
比如: `优秀`,`不咋样`这两条评论是根评论; `确实`,`哈哈`这两条评论是`优秀`的子评论;
`你说的都对`的父评论是`确实`,其根评论是`优秀`
拿`优秀`相关联的所有评论的时候,只需要查root_id等于1的.给他回复的,以及子孙回复的,通过它都能拿到!
只拿`优秀`的子评论的时候,只需要查parent_id等于1的.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 权限设计思路
菜单是用户登陆后在左侧菜单展现出来的东西. 不仅如此, 在用户访问url的时候应该做上权限的校验!!
即 权限的判断时,要考虑: 通过正常的点击菜单跳转的url 、在浏览器上非法输入的url..
# 文件的方式
权限就是url,若需要频繁的判断 某个url是否存在/是否有该权限, 用字典集合比列表好,字典和集合用了哈希,列表是挨个比较的.推荐用字典.
比如:
admin_permissions = {
"/xxx/xx/":{url相关的其它信息},
"/level/edit/<int:id>/":{...},
}
Q:思考一个问题,若访问的是动态路由/level/edit/4/, 那么"/level/edit/4/" in admin_permissions 的值为False.
A:那如何是好? 用路由的name!!反向生成url. name代表的是一个url的唯一标识!!
注意!在中间件里根据url的name进行权限的校验,要清楚中间件生命周期的流程
process_request -- 路由匹配 -- process_view -- 视图 -- process_response
在路由匹配成功后,才能拿到url相关信息(name、namespace等),路由没匹配成功,name是获取不到的,
路由匹配成功后,通过`request.resolver_match`获取. So,在中间件的process_view中进行校验!!
即:
admin_permissions = {
"level_list":{url相关的其它信息},
"level_edit":{...},
"level_add":{...},
"level_del":{...},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
优化一个细节 - 方式二,用文件的形式
★ 优化一个细节,在菜单里只展示菜单,将在菜单下的添加删除编辑放到权限部分,并添加一个关联关系即可.
管理员登陆后,展示菜单就读取ADMIN这个列表即可
点击菜单中的‘级别列表’,发送了请求,会先经过中间件,在中间件里进行权限校验
路由匹配成功 -- 得到路由对象的name -- 该name是否在admin_permissions字典中
若在,它是否有上级?比如 通过 /level/edit/4/ 路由匹配成功 得到 name为level_edit,其上级为level_list
再去菜单中找,若菜单中某个name为level_list,那么该菜单应该被默认选中!!
# -- 菜单 (只做菜单展示)
ADMIN = [
{
"title":"客户管理",
"children":[
{
"title":"级别列表",
"url":"...", # ▲ url都可以不写,有name可以反向生成.
"name":"level_list",
# "children":[
# {"title":"添加","url":"..."},
# {"title":"编辑","url":"..."},
# {"title":"删除","url":"..."},
# ],
},
{"title":"客户列表", "url":"...", "name":"user_list"},
]
},
{
"title":"订单管理",
"children":[
{"title":"价格策略", "url":"...", "name":"price"},
{"title":"交易策略", "url":"...", "name":"transaction" },
]
},
]
USER = [...]
# -- 权限 (只做权限的校验)
admin_permissions = {
"level_list":{...},
"level_edit":{...,'parent':'level_list'},
"level_add":{...,'parent':'level_list'},
"level_del":{...,'parent':'level_list'},
"user_list":{...},
"price":{...},
"transaction":{...},
}
user_permissions = {...}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 数据库的方式
>>>
客户管理
级别列表
# 添加
# 编辑
# 删除
客户列表
订单管理
价格策略
交易策略
>>>
★
这里的业务规定用户要么是管理员要么是普通用户.所以不存在多个角色.
扩展,若一个用户对应多个角色,那么还得创建一个 用户_角色的关系表!!
★
若让菜单表再跟角色表进行关联,角色一多,记录也多起来,而且菜单角色表有点像角色权限表.. So,在权限表里加了一个is_menu字段!!
逻辑:(假设是管理员登陆)
登陆后,根据角色权限关联表得到权限
eg:类似于得到文件方式的admin_permissions字典.
在权限表里,有些权限是菜单、有些不是菜单.拿到是菜单的权限去菜单表里匹配,获取到自己以及上级菜单.
eg:类似于得到文件方式的ADMIN这个列表.
So,在用户登陆时,需进行表查询,得到菜单字典和权限字典.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 队列
可作队列的服务 rabbitMQ、kafka、redis, 该项目选择用redis作队列服务.
"""
mac
"""
1> 在官方下载redis的源码包
2> 解压、编译
tar zxvf redis-5.0.14.tar.gz
sudo make test
sudo make install
3> 修改redis的配置文件
4> 启动
One_Piece@DC的MacBook redis-5.0.14 % pwd
/usr/local/redis-5.0.14
# -- 修改两处地方, bind 0.0.0.0 requirepass qwe123
# ?requirepass直接跳转到requirepass的地方.
One_Piece@DC的MacBook redis-5.0.14 % vim redis.conf
# -- 在本地启动redis
One_Piece@DC的MacBook redis-5.0.14 % ./src/redis-server redis.conf
"""
使用python远程操作redis -- 短信验证读秒
pip install redis 扩展,还可以使用Django-redis模块!
"""
import redis
conn = redis.Redis(host='127.0.0.1', port=6379, password='qwe123', encoding='utf-8')
conn.set('123456', 'hello', ex=10) # 设置了个超时时间,10s后该键值对就取不到啦!
value = conn.get('123456')
print(value) # b'hello'
"""
使用redis构建队列 -- 存取任务
运用redis构建一个先进先出的列表(队列), worker从里面取任务, 若有, 立刻取走, 若没有, 就等10s, 10s内没有就将连接断开..
再次发送一个请求, 重新连接.. 重新等待10s. 即没有数据的话, worker每10s向redis发送一个连接请求..
而不是队列中无数据, worker保持该连接一直等待队列中有数据. 减轻程序和redis的压力.
"""
import redis
conn = redis.Redis(host='127.0.0.1', port=6379, password='qwe123', encoding='utf-8')
# 往名为my_queue的队列里放了两个数据 'root'和'good'
conn.lpush('my_queue', 'root')
conn.lpush('my_queue', 'good')
# 依次取出两个值后,阻塞5秒,得到一个返回值None
# (b'my_queue', b'root')
# (b'my_queue', b'good')
# None
while True:
v = conn.brpop('my_queue', timeout=5)
print(v)
if not v:
break
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# Ajax
-- form表单形式提交,解决csrf 403的报错.
★ 在表单中加入 {% csrf_token %}
会自动生成一个隐藏的input标签.name值为csrfmiddlewaretoken,value值为随机的一串字符串.
点击submit按钮,会提交到后端.
-- Ajax形式提交,解决csrf403的报错.
2
3
4
5
# 创建项目
1> 创建虚拟环境的python项目
2> pip install django==3.2
3> django-admin startproject order .
4> python manage.py startapp web
记得注册app
5> pip install pymysql
"""
import pymysql
pymysql.install_as_MySQLdb()
"""
6> pip install tencentcloud-sdk-python
7> 纯净版Django
8> 连接数据库的配置
"""
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'order',
'USER': 'root',
'PASSWORD': '123456',
'HOST': '127.0.0.1',
'PORT': '3306',
'CHARSET': 'utf8'
},
}
"""
9> pip install django-redis
session的存储位置: session存储到redis缓存中
"""
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100,"encoding":"utf-8"},
"PASSWORD": "qwe123",
}
}
}
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
SESSION_COOKIE_NAME = "sid"
SESSION_COOKIE_PATH = "/"
SESSION_COOKIE_DOMAIN = None
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_AGE = 1209600
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SAVE_EVERY_REQUEST = True
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52